간격을 숫자로 넣는다 — 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:
co-authored by
Claude Opus 5
parent
6d06f0a4c8
commit
2f5c9bf82d
@@ -212,6 +212,34 @@ public static class EditSmoke
|
|||||||
Check("완성: Esc 는 값을 바꾸지 않는다", tagRow.ValueText == before && !tagRow.ShowCompletions);
|
Check("완성: Esc 는 값을 바꾸지 않는다", tagRow.ValueText == before && !tagRow.ShowCompletions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 간격을 숫자로 놓는다 — 4px 씩이 아니라 정확한 값으로.
|
||||||
|
{
|
||||||
|
var gaps = new DesignerViewModel(new FormDesignDataBusiness().CreateNew());
|
||||||
|
gaps.AddControlAt("Label", new Point(60, 100));
|
||||||
|
gaps.AddControlAt("Label", new Point(60, 160));
|
||||||
|
gaps.AddControlAt("Label", new Point(60, 260));
|
||||||
|
var rows = gaps.Pages[0].Controls.Where(c => c.Type == "Label").OrderBy(c => c.Y).ToList();
|
||||||
|
gaps.Selection.Set(rows);
|
||||||
|
|
||||||
|
Check("간격값: 갈린 값은 빈 칸", gaps.CurrentSpacing(horizontal: false) is null);
|
||||||
|
gaps.SetExactSpacing(24, horizontal: false);
|
||||||
|
Check("간격값: 24 로 놓으면 24 가 된다", gaps.CurrentSpacing(horizontal: false) == 24,
|
||||||
|
$"실제 {gaps.CurrentSpacing(horizontal: false)}");
|
||||||
|
|
||||||
|
var head = rows.First();
|
||||||
|
var headY = head.Y;
|
||||||
|
gaps.SetExactSpacing(8, horizontal: false);
|
||||||
|
Check("간격값: 첫 항목은 움직이지 않는다", Math.Abs(head.Y - headY) < 0.5,
|
||||||
|
$"{headY} → {head.Y}");
|
||||||
|
Check("간격값: 다시 넣어도 밀리지 않는다", gaps.CurrentSpacing(horizontal: false) == 8);
|
||||||
|
|
||||||
|
// 음수·헛값은 무시한다 — 조용히 뒤엉킨 배치가 되는 것보다 아무 일도 안 하는 쪽이 낫다
|
||||||
|
gaps.SetExactSpacing(-4, horizontal: false);
|
||||||
|
Check("간격값: 음수는 무시한다", gaps.CurrentSpacing(horizontal: false) == 8);
|
||||||
|
gaps.SetExactSpacing(double.NaN, horizontal: false);
|
||||||
|
Check("간격값: 숫자가 아니면 무시한다", gaps.CurrentSpacing(horizontal: false) == 8);
|
||||||
|
}
|
||||||
|
|
||||||
// 미리보기가 인쇄 필터를 <b>실제로 쓰는가</b>.
|
// 미리보기가 인쇄 필터를 <b>실제로 쓰는가</b>.
|
||||||
// PrintFilter 단위 테스트는 판정이 옳다는 것만 말한다 — 렌더 경로가 그 판정을 부르는지는
|
// PrintFilter 단위 테스트는 판정이 옳다는 것만 말한다 — 렌더 경로가 그 판정을 부르는지는
|
||||||
// 다른 문제이고, 이 앱에서 "코드는 맞아 보이는데 화면은 다른" 일이 여러 번 있었다.
|
// 다른 문제이고, 이 앱에서 "코드는 맞아 보이는데 화면은 다른" 일이 여러 번 있었다.
|
||||||
|
|||||||
@@ -2420,6 +2420,69 @@ public sealed class DesignerViewModel : ViewModelBase
|
|||||||
/// 간격 조절 — 가로/세로 균등·넓게·좁게·붙이기 (레거시 TK_Horiz/VertSpace 계열).
|
/// 간격 조절 — 가로/세로 균등·넓게·좁게·붙이기 (레거시 TK_Horiz/VertSpace 계열).
|
||||||
/// 축 정렬 순서로 첫 컨트롤 고정, 이후 컨트롤 이동. 넓게/좁게 단위는 그리드 크기.
|
/// 축 정렬 순서로 첫 컨트롤 고정, 이후 컨트롤 이동. 넓게/좁게 단위는 그리드 크기.
|
||||||
/// </summary>
|
/// </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)
|
public void AdjustSpacing(string mode)
|
||||||
{
|
{
|
||||||
var isHorizontal = mode.StartsWith("Horiz", StringComparison.Ordinal);
|
var isHorizontal = mode.StartsWith("Horiz", StringComparison.Ordinal);
|
||||||
|
|||||||
@@ -112,6 +112,46 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
/// <summary>간격 균등 버튼을 보일 것인가 — 사이가 있어야 나눌 것이 있다(3개 이상)</summary>
|
/// <summary>간격 균등 버튼을 보일 것인가 — 사이가 있어야 나눌 것이 있다(3개 이상)</summary>
|
||||||
public bool CanDistribute => designer.CanDistribute();
|
public bool CanDistribute => designer.CanDistribute();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 정확한 간격은 <b>둘</b>부터 뜻이 있다 — 라벨과 입력칸 한 쌍이 가장 흔한 경우다.
|
||||||
|
/// 균등 나누기(<see cref="CanDistribute"/>)가 셋을 요구하는 것과 기준이 다르다.
|
||||||
|
/// </summary>
|
||||||
|
public bool CanSpaceExactly => designer.Selection.Items.Count >= 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지금 세로 간격. 값이 갈리면 빈 칸이다 —
|
||||||
|
/// 하나로 보여 주면 그 값이 이미 적용된 것처럼 읽힌다.
|
||||||
|
/// </summary>
|
||||||
|
public string VerticalGapText
|
||||||
|
{
|
||||||
|
get => designer.CurrentSpacing(horizontal: false) is { } gap
|
||||||
|
? gap.ToString("0", System.Globalization.CultureInfo.InvariantCulture)
|
||||||
|
: string.Empty;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (double.TryParse(value, System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var gap))
|
||||||
|
{
|
||||||
|
designer.SetExactSpacing(gap, horizontal: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string HorizontalGapText
|
||||||
|
{
|
||||||
|
get => designer.CurrentSpacing(horizontal: true) is { } gap
|
||||||
|
? gap.ToString("0", System.Globalization.CultureInfo.InvariantCulture)
|
||||||
|
: string.Empty;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (double.TryParse(value, System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var gap))
|
||||||
|
{
|
||||||
|
designer.SetExactSpacing(gap, horizontal: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 정렬 실행 — 뷰의 DataContext 가 인스펙터라 디자이너 커맨드에 직접 닿을 수 없어 위임한다.
|
/// 정렬 실행 — 뷰의 DataContext 가 인스펙터라 디자이너 커맨드에 직접 닿을 수 없어 위임한다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -274,6 +314,9 @@ public sealed class InspectorViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(CanAlignVertical));
|
OnPropertyChanged(nameof(CanAlignVertical));
|
||||||
OnPropertyChanged(nameof(CanDistribute));
|
OnPropertyChanged(nameof(CanDistribute));
|
||||||
|
OnPropertyChanged(nameof(CanSpaceExactly));
|
||||||
|
OnPropertyChanged(nameof(VerticalGapText));
|
||||||
|
OnPropertyChanged(nameof(HorizontalGapText));
|
||||||
OnPropertyChanged(nameof(AlignScopeText));
|
OnPropertyChanged(nameof(AlignScopeText));
|
||||||
OnPropertyChanged(nameof(VerticalScopeText));
|
OnPropertyChanged(nameof(VerticalScopeText));
|
||||||
OnPropertyChanged(nameof(AlignSelectionScopeLabel));
|
OnPropertyChanged(nameof(AlignSelectionScopeLabel));
|
||||||
|
|||||||
@@ -908,6 +908,34 @@
|
|||||||
Content="{Binding Source=distribute-v, Converter={StaticResource IconName}, ConverterParameter=18|B.Muted}"/>
|
Content="{Binding Source=distribute-v, Converter={StaticResource IconName}, ConverterParameter=18|B.Muted}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
간격을 숫자로 — 여기까지 간격을 정하는 길은 4px 씩 넓게/좁게뿐이었다.
|
||||||
|
20px 를 32px 로 바꾸려면 3번, 메뉴로 하면 12왕복이었다.
|
||||||
|
드래그 스냅은 형제 간격을 정확히 계산해 표식까지 그리는데(SnapSolver)
|
||||||
|
명령 경로에는 그 값을 넣을 칸이 없었다.
|
||||||
|
균등(3개 이상)과 달리 <b>둘</b>부터 뜻이 있다 — 라벨과 입력칸 한 쌍이 가장 흔하다.
|
||||||
|
값이 갈리면 빈 칸이다. 하나로 보여 주면 이미 적용된 것처럼 읽힌다.
|
||||||
|
-->
|
||||||
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="12,0,10,6"
|
||||||
|
Visibility="{Binding CanSpaceExactly, Converter={StaticResource BoolToVisibility}}">
|
||||||
|
<TextBlock Text="간격값" VerticalAlignment="Center" FontSize="11.5" Margin="0,0,8,0"
|
||||||
|
Foreground="{DynamicResource B.Muted}"/>
|
||||||
|
<TextBlock Text="세로" VerticalAlignment="Center" FontSize="11"
|
||||||
|
Foreground="{DynamicResource B.Muted}" Margin="0,0,4,0"/>
|
||||||
|
<TextBox Width="46" Style="{StaticResource RowInput}"
|
||||||
|
bh:InspectorFieldBehavior.CommitOnEnter="True"
|
||||||
|
Text="{Binding VerticalGapText, UpdateSourceTrigger=LostFocus}"
|
||||||
|
AutomationProperties.Name="세로 간격 픽셀"
|
||||||
|
ToolTip="숫자를 넣고 Enter — 첫 항목은 움직이지 않는다"/>
|
||||||
|
<TextBlock Text="가로" VerticalAlignment="Center" FontSize="11"
|
||||||
|
Foreground="{DynamicResource B.Muted}" Margin="10,0,4,0"/>
|
||||||
|
<TextBox Width="46" Style="{StaticResource RowInput}"
|
||||||
|
bh:InspectorFieldBehavior.CommitOnEnter="True"
|
||||||
|
Text="{Binding HorizontalGapText, UpdateSourceTrigger=LostFocus}"
|
||||||
|
AutomationProperties.Name="가로 간격 픽셀"
|
||||||
|
ToolTip="숫자를 넣고 Enter — 첫 항목은 움직이지 않는다"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
정렬 기준 — 여럿을 골라 놓고도 '서로 맞추기'와 '종이에 맞추기'는 다른 작업이다.
|
정렬 기준 — 여럿을 골라 놓고도 '서로 맞추기'와 '종이에 맞추기'는 다른 작업이다.
|
||||||
왼쪽 문구는 선택에 따라 바뀐다: 여럿이면 '선택 영역', 하나면 '부모'(또는 '페이지').
|
왼쪽 문구는 선택에 따라 바뀐다: 여럿이면 '선택 영역', 하나면 '부모'(또는 '페이지').
|
||||||
|
|||||||
Reference in New Issue
Block a user