정렬 — 기준은 언제나 '담고 있는 것'. 그리고 속성 패널에 정렬 행
"폼 기준으로 정렬"이 없었다. 정확히는 정렬 자체가 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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ba838ae54a
commit
26f696ab00
@@ -1093,6 +1093,127 @@ public static class EditSmoke
|
||||
dirtyDoc.Undo.Undo();
|
||||
Check("더티: 분기 후에는 저장 지점 소실로 계속 dirty", dirtyDoc.Undo.IsDirty);
|
||||
|
||||
// 24-1) 정렬 — 기준이 무엇이 되는가. 산술 자체는 단위 테스트(AlignSolverTests)가 보고,
|
||||
// 여기서는 배선을 본다: 어느 사각형을 기준으로 잡았는가.
|
||||
// 별도 문서에 붙인다 — 메인 designer 에 붙이면 아래 '전체 Undo' 의 guard(80)를 갉아먹는다.
|
||||
{
|
||||
var alignDoc = new DesignerViewModel(business.CreateNew());
|
||||
ControlViewModel Put(double x, double y, double w, double h)
|
||||
{
|
||||
alignDoc.AddControlAt("Label", new Point(x, y));
|
||||
var vm = alignDoc.Pages[0].Controls[^1];
|
||||
vm.X = x;
|
||||
vm.Y = y;
|
||||
vm.Width = w;
|
||||
vm.Height = h;
|
||||
return vm;
|
||||
}
|
||||
|
||||
var page0 = alignDoc.Pages[0];
|
||||
var solo = Put(137, 211, 75, 20);
|
||||
|
||||
alignDoc.Selection.SetSingle(solo);
|
||||
alignDoc.Align("Left");
|
||||
Check("정렬: 1개는 페이지 기준 — 왼쪽", solo.X == 0, $"실제 {solo.X}");
|
||||
|
||||
alignDoc.Align("Right");
|
||||
Check("정렬: 1개는 페이지 기준 — 오른쪽", solo.X == page0.WidthDip - solo.Width,
|
||||
$"실제 {solo.X} 기대 {page0.WidthDip - solo.Width}");
|
||||
|
||||
alignDoc.Align("CenterV");
|
||||
Check("정렬: 1개는 페이지 기준 — 세로 가운데", solo.Y > 0 && solo.Y == Math.Round((page0.HeightDip - solo.Height) / 2, MidpointRounding.AwayFromZero),
|
||||
$"실제 {solo.Y}");
|
||||
|
||||
var beforeIdle = alignDoc.Undo.CanUndo;
|
||||
var idleX = solo.X;
|
||||
alignDoc.Align("Right");
|
||||
Check("정렬: 이미 맞은 상태를 다시 눌러도 실행취소가 쌓이지 않는다",
|
||||
solo.X == idleX && alignDoc.Undo.CanUndo == beforeIdle);
|
||||
|
||||
// 실행취소 1회로 완전 복원 — 정렬 1회 = 1스텝
|
||||
var beforeUndoX = solo.X;
|
||||
alignDoc.Align("Left");
|
||||
alignDoc.Undo.Undo();
|
||||
var restored = alignDoc.Pages[0].Controls.FirstOrDefault(c => c.Width == 75);
|
||||
Check("정렬: 실행취소 1회로 되돌아온다", restored is not null && restored.X == beforeUndoX,
|
||||
$"실제 {restored?.X}");
|
||||
|
||||
// 컨테이너 자식은 페이지가 아니라 부모가 기준 — 페이지 기준이면 X 가 음수가 된다
|
||||
var nestDoc = new DesignerViewModel(business.CreateNew());
|
||||
nestDoc.AddControlAt("Panel", new Point(120, 90));
|
||||
var holder = nestDoc.Pages[0].Controls[^1] as ContainerViewModel;
|
||||
if (holder is not null)
|
||||
{
|
||||
holder.X = 120;
|
||||
holder.Y = 90;
|
||||
holder.Width = 300;
|
||||
holder.Height = 200;
|
||||
nestDoc.Selection.SetSingle(holder);
|
||||
nestDoc.AddControlAt("Label", new Point(180, 140));
|
||||
var inner = holder.Children.Count > 0 ? holder.Children[^1] : null;
|
||||
if (inner is not null)
|
||||
{
|
||||
inner.X = 60;
|
||||
inner.Y = 50;
|
||||
inner.Width = 80;
|
||||
inner.Height = 20;
|
||||
|
||||
nestDoc.Selection.SetSingle(inner);
|
||||
nestDoc.Align("Left");
|
||||
Check("정렬: 컨테이너 자식은 부모 기준(음수로 튀지 않는다)", inner.X == 0, $"실제 {inner.X}");
|
||||
|
||||
nestDoc.Align("Right");
|
||||
Check("정렬: 컨테이너 자식 오른쪽도 부모 기준",
|
||||
inner.X == holder.Width - inner.Width, $"실제 {inner.X}");
|
||||
|
||||
// 부모와 자식을 함께 선택하면 자식은 대상에서 빠진다(두 번 옮겨지지 않게)
|
||||
var innerBefore = inner.X;
|
||||
nestDoc.Selection.Set(new List<ControlViewModel> { holder, inner }, holder);
|
||||
nestDoc.Align("Left");
|
||||
Check("정렬: 조상이 선택돼 있으면 자식은 건드리지 않는다", inner.X == innerBefore,
|
||||
$"실제 {inner.X} (시작 {innerBefore})");
|
||||
}
|
||||
}
|
||||
|
||||
// 2페이지 컨트롤 1개 — 그 페이지 사각형을 기준으로 잡아야 한다
|
||||
var twoPage = new DesignerViewModel(business.CreateNew());
|
||||
twoPage.AddControlAt("Label", new Point(80, 120));
|
||||
twoPage.AddPage();
|
||||
if (twoPage.Pages.Count >= 2)
|
||||
{
|
||||
var second = twoPage.Pages[1];
|
||||
twoPage.AddControlAt("Label", new Point(200, second.OffsetY + 300));
|
||||
var onSecond = second.Controls.Count > 0 ? second.Controls[^1] : null;
|
||||
if (onSecond is not null)
|
||||
{
|
||||
onSecond.X = 200;
|
||||
onSecond.Y = 300;
|
||||
onSecond.Width = 60;
|
||||
onSecond.Height = 20;
|
||||
twoPage.Selection.SetSingle(onSecond);
|
||||
twoPage.Align("Top");
|
||||
Check("정렬: 2페이지 컨트롤은 그 페이지 기준", onSecond.Y == 0, $"실제 {onSecond.Y}");
|
||||
|
||||
// 페이지를 넘는 선택 — 세로는 거부, 가로는 허용
|
||||
var onFirst = twoPage.Pages[0].Controls.Count > 0 ? twoPage.Pages[0].Controls[0] : null;
|
||||
if (onFirst is not null)
|
||||
{
|
||||
var mixedY = onSecond.Y;
|
||||
var canUndoBefore = twoPage.Undo.CanUndo;
|
||||
twoPage.Selection.Set(new List<ControlViewModel> { onFirst, onSecond }, onFirst);
|
||||
twoPage.Align("Top");
|
||||
Check("정렬: 페이지를 넘는 선택은 세로 정렬을 거부한다",
|
||||
onSecond.Y == mixedY && twoPage.Undo.CanUndo == canUndoBefore,
|
||||
$"실제 {onSecond.Y} (시작 {mixedY})");
|
||||
|
||||
twoPage.Align("Left");
|
||||
Check("정렬: 페이지를 넘어도 가로는 맞는다", onFirst.X == onSecond.X,
|
||||
$"실제 {onFirst.X} / {onSecond.X}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 24-2) 스냅 — 정렬 가이드와 간격 균등.
|
||||
// 별도 문서에 붙인다. 메인 designer 에 붙이면 아래 '전체 Undo' 의 guard(80)를
|
||||
// 드래그마다 잡아먹어 그 검사가 조용히 깨진다(UndoService 용량 100).
|
||||
|
||||
Reference in New Issue
Block a user