초기 커밋: SheetMe 서식생성기 (P0~P5 완료 상태)
레거시 서식생성기(VB.NET WinForms) 대체용 C#/.NET 10 WPF 디자이너. 기준선: 실DB 활성 디자인 1,271건 왕복 의미론 diff 0 / 예외 0, 단위 테스트 49/49. 이 커밋에 함께 포함된 자격증명 분리: - appsettings.json 을 __HOST__/__PASSWORD__ 플레이스홀더로 전환 - 실접속 정보는 appsettings.Development.json 으로 분리(.gitignore 제외, csproj Debug 조건부 복사라 Release 산출물에 실리지 않음) - ConfigLoader 를 환경변수 > Development > appsettings 순 레이어링으로 변경, 미치환 플레이스홀더는 '미설정'으로 간주 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
using System.Windows;
|
||||
using SheetMe.Designer.ViewModels;
|
||||
using SheetMe.Designer.ViewModels.Controls;
|
||||
|
||||
namespace SheetMe.Designer.Services;
|
||||
|
||||
/// <summary>스냅 결과 — 보정된 이동량과 표시할 가이드 좌표(월드)</summary>
|
||||
public readonly record struct SnapResult(double Dx, double Dy, double? GuideX, double? GuideY);
|
||||
|
||||
/// <summary>
|
||||
/// 그리드/정렬 스냅 순수 계산.
|
||||
/// 드래그 시작 시 후보(비선택 형제의 좌/중/우·상/중/하 + 용지 경계/중앙)를 1회 사전수집해
|
||||
/// 프레임당 재수집을 피한다.
|
||||
/// </summary>
|
||||
public sealed class SnapEngine
|
||||
{
|
||||
#region Member Fields
|
||||
private readonly List<double> candidatesX = new();
|
||||
private readonly List<double> candidatesY = new();
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>그리드 간격(px)</summary>
|
||||
public double GridSize { get; set; } = 4;
|
||||
|
||||
/// <summary>정렬 스냅 허용 거리(px)</summary>
|
||||
public double Tolerance { get; set; } = 6;
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
/// <summary>드래그 시작 — 스냅 후보 사전수집(월드 좌표)</summary>
|
||||
public void BeginDrag(PageViewModel page, IReadOnlySet<ControlViewModel> moving)
|
||||
{
|
||||
candidatesX.Clear();
|
||||
candidatesY.Clear();
|
||||
|
||||
// 용지 경계·중앙
|
||||
candidatesX.Add(0);
|
||||
candidatesX.Add(page.WidthDip / 2);
|
||||
candidatesX.Add(page.WidthDip);
|
||||
candidatesY.Add(page.OffsetY);
|
||||
candidatesY.Add(page.OffsetY + page.HeightDip / 2);
|
||||
candidatesY.Add(page.OffsetY + page.HeightDip);
|
||||
|
||||
foreach (var sibling in page.Controls)
|
||||
{
|
||||
if (moving.Contains(sibling) || sibling.Model.Hidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
candidatesX.Add(sibling.X);
|
||||
candidatesX.Add(sibling.X + sibling.Width / 2);
|
||||
candidatesX.Add(sibling.X + sibling.Width);
|
||||
|
||||
var top = page.OffsetY + sibling.Y;
|
||||
candidatesY.Add(top);
|
||||
candidatesY.Add(top + sibling.Height / 2);
|
||||
candidatesY.Add(top + sibling.Height);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이동 스냅 — bbox(월드)에 (dx,dy) 적용 시 정렬 후보와 최근접 정합.
|
||||
/// free=true(Alt)면 스냅 없이 원본 이동량 그대로.
|
||||
/// </summary>
|
||||
public SnapResult SnapMove(Rect bbox, double dx, double dy, bool free)
|
||||
{
|
||||
if (free)
|
||||
{
|
||||
return new SnapResult(dx, dy, null, null);
|
||||
}
|
||||
|
||||
var movedLeft = bbox.X + dx;
|
||||
var movedTop = bbox.Y + dy;
|
||||
|
||||
var (adjustX, guideX) = SnapAxis(new[] { movedLeft, movedLeft + bbox.Width / 2, movedLeft + bbox.Width }, candidatesX);
|
||||
var (adjustY, guideY) = SnapAxis(new[] { movedTop, movedTop + bbox.Height / 2, movedTop + bbox.Height }, candidatesY);
|
||||
|
||||
// 정합 실패 축은 그리드 스냅
|
||||
var resultDx = guideX is not null ? dx + adjustX : Math.Round(movedLeft / GridSize) * GridSize - bbox.X;
|
||||
var resultDy = guideY is not null ? dy + adjustY : Math.Round(movedTop / GridSize) * GridSize - bbox.Y;
|
||||
|
||||
return new SnapResult(resultDx, resultDy, guideX, guideY);
|
||||
}
|
||||
|
||||
/// <summary>리사이즈 스냅 — 움직이는 모서리 좌표만 정합(간이: 그리드 우선)</summary>
|
||||
public double SnapEdge(double value, bool isXAxis, bool free)
|
||||
{
|
||||
if (free)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
var candidates = isXAxis ? candidatesX : candidatesY;
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (Math.Abs(candidate - value) <= Tolerance)
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return Math.Round(value / GridSize) * GridSize;
|
||||
}
|
||||
|
||||
private (double Adjust, double? Guide) SnapAxis(double[] edges, List<double> candidates)
|
||||
{
|
||||
var best = double.MaxValue;
|
||||
double adjust = 0;
|
||||
double? guide = null;
|
||||
foreach (var edge in edges)
|
||||
{
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var distance = candidate - edge;
|
||||
if (Math.Abs(distance) <= Tolerance && Math.Abs(distance) < Math.Abs(best))
|
||||
{
|
||||
best = distance;
|
||||
adjust = distance;
|
||||
guide = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (adjust, guide);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Reference in New Issue
Block a user