Files
MsystechandClaude Opus 5 26f696ab00 정렬 — 기준은 언제나 '담고 있는 것'. 그리고 속성 패널에 정렬 행
"폼 기준으로 정렬"이 없었다. 정확히는 정렬 자체가 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>
2026-08-14 10:17:02 +09:00

118 lines
4.8 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}