간격을 숫자로 넣는다 — 4px 씩 12왕복에서 한 번으로

여기까지 간격을 정하는 길은 4px 씩 넓게/좁게뿐이었다.
20px 를 32px 로 바꾸려면 3번, 로고 아래 4단 메뉴로 하면 12왕복이고 매번 마우스가 좌상단까지 갔다.
단축키(Alt+[ / Alt+])로 왕복은 없앴지만 여전히 4px 씩이다.

드래그 스냅은 형제 간격을 정확히 계산해 분홍 표식까지 그리는데(SnapSolver),
<b>명령 경로에는 그 값을 넣을 칸이 없었다</b>. 인스펙터에 세로·가로 칸을 붙인다.

<b>둘</b>부터 뜬다 — 라벨과 입력칸 한 쌍이 가장 흔한 경우다.
균등 나누기가 셋을 요구하는 것과 기준이 다르므로 표시 조건을 따로 뒀다(CanSpaceExactly).

<b>첫 항목은 움직이지 않는다.</b> 기준이 흔들리면 같은 값을 두 번 넣어도 자리가 계속 밀린다 —
"24 를 넣었는데 블록이 아래로 기어간다"가 되고, 그건 사용자가 자기 조작을 의심하게 만든다.
검사에서 8 을 두 번 넣어 첫 항목의 Y 가 그대로인지 본다.

<b>값이 갈리면 빈 칸이다.</b> 하나로 보여 주면 그 값이 이미 적용된 것처럼 읽혀서,
사용자는 이미 고른 상태라고 믿고 넘어간다.

음수·NaN 은 무시한다 — 조용히 뒤엉킨 배치가 되는 것보다 아무 일도 안 하는 쪽이 낫다.

실측 6건: 갈린 값 빈 칸 · 24 로 놓으면 24 · 첫 항목 고정 · 재입력 시 안 밀림 · 음수 무시 · NaN 무시.

게이트: 테스트 291/291, --edit-smoke 0실패(신규 6), --dialog-shots 글자 2,531개 FAIL 0 대조군 4/4,
--modal-check 0실패, --cleartype 11/11, --maxrect 0실패, --scale-budget 5/5, 빌드 경고 0,
--db-smoke 1271건 diff 0, --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-14 18:04:57 +09:00
co-authored by Claude Opus 5
parent 6d06f0a4c8
commit 2f5c9bf82d
4 changed files with 162 additions and 0 deletions
@@ -2420,6 +2420,69 @@ public sealed class DesignerViewModel : ViewModelBase
/// 간격 조절 — 가로/세로 균등·넓게·좁게·붙이기 (레거시 TK_Horiz/VertSpace 계열).
/// 축 정렬 순서로 첫 컨트롤 고정, 이후 컨트롤 이동. 넓게/좁게 단위는 그리드 크기.
/// </summary>
/// <summary>
/// 선택한 것들 사이 간격을 <b>정확한 픽셀 값으로</b> 놓는다.
///
/// <b>왜 필요한가.</b> 여기까지 간격을 조절하는 길은 4px 씩 넓게/좁게뿐이었다.
/// 20px 를 32px 로 바꾸려면 3번, 로고 아래 4단 메뉴로 하면 <b>12왕복</b>이었다.
/// 단축키(Alt+[ / Alt+])를 붙여 왕복은 없앴지만 여전히 4px 씩이다.
/// 드래그 스냅은 형제 간격을 정확히 계산해 분홍 표식까지 그리는데(SnapSolver),
/// 명령 경로에는 그 값을 <b>넣을 칸</b>이 없었다.
///
/// 첫 항목은 움직이지 않는다 — 기준이 흔들리면 같은 값을 두 번 넣어도 자리가 계속 밀린다.
/// </summary>
public void SetExactSpacing(double gap, bool horizontal)
{
if (Selection.Items.Count < 2 || !double.IsFinite(gap) || gap < 0)
{
return;
}
var ordered = Selection.Items
.Select(vm => (Vm: vm, World: WorldBoundsOf(vm), Offset: ParentWorldOffset(vm)))
.OrderBy(x => horizontal ? x.World.X : x.World.Y)
.ToList();
Undo.Snapshot();
var cursor = horizontal ? ordered[0].World.Right : ordered[0].World.Bottom;
for (var i = 1; i < ordered.Count; i++)
{
var (vm, world, offset) = ordered[i];
if (horizontal)
{
vm.X = Math.Round(cursor + gap - offset.X);
cursor += gap + world.Width;
continue;
}
vm.Y = Math.Round(cursor + gap - offset.Y);
cursor += gap + world.Height;
}
Selection.NotifyBoundsChanged();
}
/// <summary>
/// 지금 선택의 간격 — 숫자 칸에 무엇을 보여 줄지.
/// 값이 서로 다르면 null(빈 칸) — 하나로 보여 주면 그 값이 이미 적용된 것처럼 읽힌다.
/// </summary>
public double? CurrentSpacing(bool horizontal)
{
if (Selection.Items.Count < 2)
{
return null;
}
var ordered = Selection.Items
.Select(vm => WorldBoundsOf(vm))
.OrderBy(r => horizontal ? r.X : r.Y)
.ToList();
var gaps = new List<double>();
for (var i = 1; i < ordered.Count; i++)
{
gaps.Add(Math.Round(horizontal
? ordered[i].X - ordered[i - 1].Right
: ordered[i].Y - ordered[i - 1].Bottom));
}
return gaps.Distinct().Count() == 1 ? gaps[0] : null;
}
public void AdjustSpacing(string mode)
{
var isHorizontal = mode.StartsWith("Horiz", StringComparison.Ordinal);