"폼 기준으로 정렬"이 없었다. 정확히는 정렬 자체가 2개 이상에서만 동작했고 기준은 늘 선택 영역이라, 컨트롤 하나를 페이지 가운데 놓을 방법이 메뉴 어디에도 없었다. 규칙을 한 문장으로 바꿨다 — 기준은 그것을 담고 있는 것이다. 선택이 2개 이상이면 선택 영역이 그 자리를 대신하고, 1개면 담고 있는 것 — 컨테이너 안이면 컨테이너, 최상위면 페이지 — 이 기준이 된다. 피그마와 같다. 컨테이너 자식을 페이지 기준으로 맞추지 않는 이유. X/Y 는 부모 상대좌표라 '페이지 왼쪽'에 맞추면 음수가 되어 부모 밖으로 나간다. 디자이너 캔버스는 클리핑을 안 해서 멀쩡히 보이지만 레거시 런타임에서는 부모가 잘라먹는다 — 화면과 인쇄가 갈라지고 눈으로는 못 잡는 종류의 버그다. 같이 고친 것들: · 페이지를 넘는 선택의 세로 정렬을 거부한다. 다른 종이의 '위 맞춤'에는 뜻이 없고, 예전에는 실제로 깨졌다 — bbox 는 첫 페이지 월드 Y 인데 offset 은 자기 페이지 OffsetY 라 vm.Y 가 큰 음수가 되어 컨트롤이 제 종이 위 허공으로 날아갔다. 가로는 모든 페이지의 X 원점이 0 이라 그대로 허용한다. · 부모와 자식을 함께 선택한 경우 자식을 대상에서 뺀다. 부모를 옮기면 자식 월드 좌표가 따라 움직이는데 자식에게도 절대좌표를 대입하면 두 번 옮겨진다(레이어 목록에서 만들 수 있는 선택이다). · 반올림을 AwayFromZero 로 고정했다. 기본 ToEven 은 322.5→322, 323.5→324 로 방향이 값에 따라 뒤집힌다 — 폭이 제각각인 라벨 다섯 개를 가운데 맞춤하면 어떤 건 322, 어떤 건 323 에 앉는다. (스냅에서 반올림을 뺀 것과 반대로 보이지만 이유가 다르다. 거긴 가이드선이 모서리에서 뜨는 게 보이기 때문이고, 정렬은 선을 긋지 않는다. 저장 포맷은 어차피 정수다.) · 변경이 실제로 생길 때만 실행취소를 찍는다. 버튼으로 노출되면 연타되는데, 무조건 찍으면 빈 되돌리기 단계가 쌓이고 '저장 안 한 변경'으로 표시된다. 산술은 Core/Layout/AlignSolver 로 뺐다. 짧지만 경계가 잔소리 같은 규칙 덩어리라(반올림 방향, 기준보다 넓은 항목, 원점이 큰 둘째 장) 디자이너 쪽에서 검사하면 케이스마다 실행취소 스냅샷을 하나씩 먹는다. 단위 테스트 10건이 그걸 공짜로 고정한다. 속성 패널 정렬 행. [200]SheetMe 의 inspector-wireframe.html 을 따랐다 — 30x26 버튼 6개, radius 6, 사이 2px, 가로 축과 세로 축 사이 8px 틈, 아이콘은 stroke 1.5. LucideIcons 의 align-obj-* 6개는 등록만 되고 아무 데서도 안 쓰이던 죽은 항목이라 참조 도형으로 갈아 끼웠다(사각형 둘 + 기준선 → 사각형 하나 + 기준선). 좌표는 24격자다 — Build 가 Canvas 를 24x24 로 하드코딩해서, 참조의 16격자 좌표를 그대로 넣으면 아이콘이 좌상단 2/3 크기로 쏠린다. 크기 18 에서 stroke 2 × 18/24 = 1.5px 로 참조와 같은 굵기가 나온다. 버튼 스타일은 새로 만들었다. SegmentToggle 은 치수가 같지만 채워진 상태 토글 룩이고, 정렬은 상태가 아니라 액션이라 눌려 있는 것처럼 보이면 안 된다 — 채움 없이 호버만 남겼다. 표시 조건에 HasSelection 을 쓰지 않았다. 그건 페이지 속성 모드에서도 true 라 정렬할 컨트롤이 없는데 바가 뜬다. 선택 0 에서는 접어야 하는데, 안 접으면 '컨트롤을 선택하면…' 안내문 뒤에 버튼 줄이 그대로 남는다(같은 Grid 에 겹쳐 그려진다). 툴팁이 지금 무엇을 기준으로 정렬하는지 말한다 — "왼쪽 맞춤 — 페이지 기준" / "— Panel1 기준" / "— 선택 영역 기준". 세로 3버튼은 페이지를 넘는 선택에서 비활성되고 그 이유를 툴팁에 적는다. 아이콘만 있는 버튼이라 AutomationProperties.Name 을 붙였다 — 이 저장소에 버튼 자동화 속성 전례가 없어 새로 여는 관례다. 메뉴는 그대로 두되 툴팁으로 전제를 적었고, '페이지 가로/세로 가운데'는 헤더에 '묶음째'를 붙였다. 선택 1개에서는 CenterH 와 결과가 같아지고, 다중에서만 갈리기 때문이다(그건 상대 배치를 보존한 채 묶음을 옮기고, CenterH 는 각자 가운데로 모은다). 분배(간격 균등) 버튼은 넣지 않았다. 참조 와이어프레임에 없고, 3개 이상에서만 살아서 실사용 대부분(1~2개 선택)에 두 칸이 죽은 채 남아 줄이 고장 난 것처럼 읽힌다. --dialog-shots 에 선택 상태별 인스펙터 3장(12c/12d/12e)을 더했다. 정렬 바가 선택 0 과 페이지 속성 모드에서 사라지는지는 수치로 못 잡는다. 산출물 51 → 57. 기존 인스펙터 5장은 정렬 행만큼 아래로 밀린다 — 정상 diff 다. 알려진 별건(이번에 안 고침): 정렬 후 Ctrl+Z 하면 선택이 풀린다. Undo 가 문서를 통째로 갈아끼우며 Selection.Clear 하기 때문이고, 정렬은 '왼쪽 → 아니네 → 가운데'로 여러 번 시도하는 조작이라 버튼으로 노출되면서 드러난다. Id 기반 선택 복원이 필요해 별도로 다룬다. 게이트: 테스트 267/267(신규 10), --edit-smoke 234건 0실패(정렬 검사 11건 신규), --query-popup 17/17, --maxrect 10/10, --snap-shots 7종 0실패, --db-smoke 3000 diff 0·예외 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일, --dialog-shots 57파일. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.8 KiB
C#
118 lines
4.8 KiB
C#
using SheetMe.Core.Layout;
|
||
|
||
namespace SheetMe.Core.Tests;
|
||
|
||
/// <summary>
|
||
/// 정렬 산술 고정 — 특히 반올림 방향.
|
||
///
|
||
/// 반올림은 눈에 안 띄는 채로 틀린다. 폭 75 짜리를 폭 720 용지 가운데 두면 목표가 정확히 322.5 인데,
|
||
/// .NET 기본 반올림(ToEven)은 322 로 내리고 323.5 는 324 로 올린다 — 폭이 제각각인 라벨 다섯 개를
|
||
/// 가운데 맞춤하면 어떤 건 322, 어떤 건 323 에 앉아 한 픽셀씩 어긋난다.
|
||
/// </summary>
|
||
[TestClass]
|
||
public sealed class AlignSolverTests
|
||
{
|
||
#region Member Fields
|
||
private static readonly SnapRect Page = new(0, 0, 720, 856);
|
||
|
||
/// <summary>둘째 장 — 월드 원점이 0 이 아닌 기준</summary>
|
||
private static readonly SnapRect SecondPage = new(0, 884, 720, 856);
|
||
#endregion
|
||
|
||
#region Methods
|
||
[TestMethod]
|
||
public void 좌우_모서리에_맞춘다()
|
||
{
|
||
var item = new SnapRect(10, 10, 50, 20);
|
||
Assert.AreEqual(0, AlignSolver.Apply(item, Page, AlignMode.Left).X, 1e-9);
|
||
Assert.AreEqual(670, AlignSolver.Apply(item, Page, AlignMode.Right).X, 1e-9);
|
||
Assert.AreEqual(335, AlignSolver.Apply(item, Page, AlignMode.CenterH).X, 1e-9);
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 위아래_모서리에_맞춘다()
|
||
{
|
||
var item = new SnapRect(10, 10, 50, 20);
|
||
Assert.AreEqual(0, AlignSolver.Apply(item, Page, AlignMode.Top).Y, 1e-9);
|
||
Assert.AreEqual(836, AlignSolver.Apply(item, Page, AlignMode.Bottom).Y, 1e-9);
|
||
Assert.AreEqual(418, AlignSolver.Apply(item, Page, AlignMode.CenterV).Y, 1e-9);
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 반정수_중심은_바깥쪽으로_올린다()
|
||
{
|
||
// (720 − 75) / 2 = 322.5 — 기본 반올림(ToEven)이면 322 가 나온다
|
||
var odd = new SnapRect(0, 0, 75, 20);
|
||
Assert.AreEqual(323, AlignSolver.Apply(odd, Page, AlignMode.CenterH).X, 1e-9);
|
||
|
||
// (100 − 51) / 2 = 24.5 — ToEven 이면 24
|
||
var narrow = new SnapRect(0, 0, 51, 20);
|
||
Assert.AreEqual(25, AlignSolver.Apply(narrow, new SnapRect(0, 0, 100, 100), AlignMode.CenterH).X, 1e-9);
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 두_번_맞춰도_같은_자리다()
|
||
{
|
||
var item = new SnapRect(13, 7, 75, 20);
|
||
var once = AlignSolver.Apply(item, Page, AlignMode.CenterH);
|
||
var twice = AlignSolver.Apply(once, Page, AlignMode.CenterH);
|
||
Assert.AreEqual(once.X, twice.X, 1e-9, "기준이 고정이면 두 번째 호출이 자리를 바꿀 이유가 없다");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 기준보다_넓으면_음수가_나오고_그게_맞다()
|
||
{
|
||
var wide = new SnapRect(0, 0, 800, 20);
|
||
Assert.AreEqual(-40, AlignSolver.Apply(wide, Page, AlignMode.CenterH).X, 1e-9,
|
||
"가운데에 두려면 그래야 한다 — 여기서 잘라 붙이면 가운데가 아니게 된다");
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 크기가_0_이어도_터지지_않는다()
|
||
{
|
||
var empty = new SnapRect(300, 300, 0, 0);
|
||
Assert.AreEqual(0, AlignSolver.Apply(empty, Page, AlignMode.Left).X, 1e-9);
|
||
Assert.AreEqual(720, AlignSolver.Apply(empty, Page, AlignMode.Right).X, 1e-9);
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 둘째_장에서도_그_장의_좌표로_맞춘다()
|
||
{
|
||
var item = new SnapRect(10, 1000, 50, 20);
|
||
Assert.AreEqual(884, AlignSolver.Apply(item, SecondPage, AlignMode.Top).Y, 1e-9);
|
||
Assert.AreEqual(1720, AlignSolver.Apply(item, SecondPage, AlignMode.Bottom).Y, 1e-9);
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 맞추지_않는_축은_건드리지_않는다()
|
||
{
|
||
var item = new SnapRect(13, 77, 50, 20);
|
||
var moved = AlignSolver.Apply(item, Page, AlignMode.Left);
|
||
Assert.AreEqual(77, moved.Y, 1e-9);
|
||
Assert.AreEqual(50, moved.W, 1e-9);
|
||
Assert.AreEqual(20, moved.H, 1e-9);
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 명령_문자열을_방향으로_읽는다()
|
||
{
|
||
Assert.AreEqual(AlignMode.Left, AlignSolver.Parse("Left"));
|
||
Assert.AreEqual(AlignMode.CenterV, AlignSolver.Parse("CenterV"));
|
||
Assert.IsNull(AlignSolver.Parse("Sideways"), "모르는 값은 null — 호출부가 아무 일도 하지 않는다");
|
||
Assert.IsNull(AlignSolver.Parse(string.Empty));
|
||
}
|
||
|
||
[TestMethod]
|
||
public void 세로_축을_가려낸다()
|
||
{
|
||
// 여러 페이지에 걸친 선택에서 거부해야 할 것들 — 다른 종이의 '위 맞춤'에는 뜻이 없다
|
||
Assert.IsTrue(AlignSolver.IsVertical(AlignMode.Top));
|
||
Assert.IsTrue(AlignSolver.IsVertical(AlignMode.CenterV));
|
||
Assert.IsTrue(AlignSolver.IsVertical(AlignMode.Bottom));
|
||
Assert.IsFalse(AlignSolver.IsVertical(AlignMode.Left));
|
||
Assert.IsFalse(AlignSolver.IsVertical(AlignMode.CenterH));
|
||
Assert.IsFalse(AlignSolver.IsVertical(AlignMode.Right));
|
||
}
|
||
#endregion
|
||
}
|