새 플랫폼(UbisamBase.Launcher) 위로 옮기면서 12차 A~E 를 한 번에 담는다.
항목별 값과 판정은 doc/history/2026-09-23-12차.md, 요약은 doc/history/changelog.md.
A. 폴더 비교를 읽기 쉽게
- A-1 폴더 줄의 크기가 늘 0B 이던 것 - 안에 든 파일 크기 합계를 아래에서 위로 더해 적는다
(걸러 낸 파일은 합계에서도 빠진다)
- A-2 좌우로 나눠 보이기 - 왼쪽 · 오른쪽 목록을 나란히 두고 같은 줄에 맞춘다.
한쪽에만 있는 항목은 반대편을 비운다
B. 파일 비교 화면
- B-1 2-Way 에서 양쪽으로 머지 - 칸마다 반영 · 저장. 왼쪽 기준점을 따로 세고,
되돌리기 · 저장 백업도 양쪽 모두
- B-2 프로그램 정보 창 - 버전 · 빌드 날짜 · 만든 사람 · 사내 문의처 · 플랫폼 버전 · 로그 폴더
- B-3 칸 아래에 무엇이 달라졌는지 - 칸마다 줄 수 · 추가 · 삭제 · 수정, 가운데에 진행
C. 플랫폼 (플랫폼 저장소에서 함께 나감)
- C-2 하단 탭 줄을 한 단계 작게 - 74 -> 61 DIP. 실측 133 -> 109px 이고
그만큼 코드 칸이 늘어 파일 비교가 47 -> 48줄
E. 저장하지 않아도 남는 편집
- E-2 여러 짝을 동시에(문서 탭). 지금까지는 한 짝만 들어서 짝을 바꾸면
저장하지 않은 편집을 묻고 버리면 사라졌다(재현: 16줄 -> 15줄).
이제 떠 있던 탭이 그 글을 맡아 두므로 묻지 않는다.
- 탭마다 HomeViewModel 을 따로 만들지 않았다 - 이 화면은 GitWorkspace 의 여러 사건과
한 자리짜리 약속(ConfirmDiscardEdits · ConfirmLeaveCommit)에 물려 있다.
화면은 하나로 두고 탭이 파일에 없는 것만 맡는다(고친 글 · 왼쪽 글 · 되돌리기 이력 ·
두 기준점 · 캐럿 · 스크롤 · 저장 형식)
- 탭 이름은 파일 이름, 다르면 왼 <-> 오. 양쪽이 같은 부분은 한 번만 적는다
(WeldController_기준 <-> 내작업.cs, 421 -> 288px). 같은 이름이 겹치면
갈릴 때까지 위 폴더를 한 겹씩 덧붙인다
- 저장하지 않은 탭에 가운뎃점. 사람이 손댄 것만 센다 - 중괄호 맞춤으로만 바뀐 것은
묻는 자리와 같은 기준으로 뺀다(안 그러면 .cs 를 여는 것만으로 늘 점이 붙는다)
- 탭이 사라져도 고친 글은 보관함에 남아 다시 열면 되살아난다(짝 50개).
E-1(껐다 켜도 남는 편집)은 이 보관함을 파일로 옮겨 적으면 된다
- 이전 · 다음 파일은 지금 탭의 짝만 바꾸고, 폴더 비교에서 두 번 누르면 새 탭(사용자 결정)
- Ctrl+Tab · Ctrl+Shift+Tab · Ctrl+W · 가운데 단추 닫기. 닫을 때도 묻지 않는다
- Git 탭 · 병합 중 네 칸은 탭이 맡지 않는다 - 그쪽은 예전처럼 묻는다
하네스 27벌 1,787건 통과 · 실패 0(DocTabTest 106건 새로 만듦).
빌드 경고 0 · 오류 0. 화면으로 E-2 34건 · 회귀 19건 확인.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
431 lines
20 KiB
C#
431 lines
20 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Data;
|
|
using System.Windows.Media;
|
|
using System.Windows.Threading;
|
|
using MergeHub.Models;
|
|
using MergeHub.Services;
|
|
using MergeHub.ViewModels;
|
|
|
|
namespace MergeHub.Views
|
|
{
|
|
/// <summary>
|
|
/// 칸마다 자기 줄만 보이는 화면(11차 F-10, Araxis 식).
|
|
///
|
|
/// 속은 예전처럼 <b>정렬 행</b> 하나(<see cref="HomeViewModel.Lines"/>)다 - 차이 · 반영 · 되돌리기 · 차이 이동이 그대로 쓴다.
|
|
/// 칸마다 그 칸에 있는 줄만 걸러 보이고(빈 줄로 높이를 맞추지 않는다), 두 칸의 짝은 틈에 그리는 잇는 선으로 보인다.
|
|
/// 스크롤은 한 칸을 굴리면 나머지가 <b>가운데 기준</b>으로 따라온다(<see cref="PaneMapping"/>).
|
|
/// </summary>
|
|
public partial class HomeView
|
|
{
|
|
/// <summary>그 칸에 있는 줄만 보이는 목록. 반영 · 되살리기로 LOCAL 줄이 생기고 사라지면 바로 따라간다.</summary>
|
|
private static ListCollectionView PaneView(HomeViewModel vm, MergeLine.TextPane pane)
|
|
{
|
|
var view = new ListCollectionView(vm.Lines) { Filter = o => o is MergeLine line && line.IsIn(pane) };
|
|
if (pane == MergeLine.TextPane.Local)
|
|
{
|
|
view.IsLiveFiltering = true;
|
|
view.LiveFilteringProperties.Add(nameof(MergeLine.HasLocal));
|
|
}
|
|
else if (pane == MergeLine.TextPane.Base)
|
|
{
|
|
// 두 파일 비교는 왼쪽으로도 반영한다(12차 B-1) - 왼쪽에 줄이 생기고 사라지므로 BASE 칸도 따라가야 한다.
|
|
// 이게 없으면 왼쪽에 넣은 줄이 안 보이고, 왼쪽에서 지운 줄이 빈 칸으로 남는다.
|
|
view.IsLiveFiltering = true;
|
|
view.LiveFilteringProperties.Add(nameof(MergeLine.HasBase));
|
|
}
|
|
return view;
|
|
}
|
|
|
|
private HomeViewModel? _linkedVm;
|
|
|
|
/// <summary>네 칸에 자기 줄 목록을 붙인다. 새 ViewModel 이 올 때 한 번.</summary>
|
|
private void BindPaneViews(HomeViewModel? vm)
|
|
{
|
|
if (_linkedVm is not null) _linkedVm.Lines.CollectionChanged -= OnLinesChanged;
|
|
_linkedVm = vm;
|
|
|
|
if (vm is null)
|
|
{
|
|
foreach (var list in new[] { LocalLines, BaseLines, RemoteLines, OriginalLocalLines }) list.ItemsSource = null;
|
|
return;
|
|
}
|
|
|
|
LocalLines.ItemsSource = PaneView(vm, MergeLine.TextPane.Local);
|
|
BaseLines.ItemsSource = PaneView(vm, MergeLine.TextPane.Base);
|
|
RemoteLines.ItemsSource = PaneView(vm, MergeLine.TextPane.Remote);
|
|
OriginalLocalLines.ItemsSource = PaneView(vm, MergeLine.TextPane.OriginalLocal);
|
|
|
|
vm.Lines.CollectionChanged += OnLinesChanged;
|
|
LinkOverlay.Draw = DrawLinks;
|
|
}
|
|
|
|
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e) => ScheduleResync();
|
|
|
|
private bool _resyncQueued;
|
|
|
|
/// <summary>글 · 줄이 바뀌면 칸마다 높이가 달라진다 - 배치가 끝난 뒤 편집기 기준으로 한 번 다시 맞추고 선을 다시 그린다.</summary>
|
|
private void ScheduleResync()
|
|
{
|
|
if (_resyncQueued) return;
|
|
_resyncQueued = true;
|
|
Dispatcher.BeginInvoke(new Action(() =>
|
|
{
|
|
_resyncQueued = false;
|
|
if (FindScrollViewer(LocalEditor) is { } viewer)
|
|
{
|
|
HoldSync();
|
|
SyncPanesFrom(MergeLine.TextPane.Local, viewer.VerticalOffset, viewer.ViewportHeight);
|
|
}
|
|
LinkOverlay.InvalidateVisual();
|
|
}), DispatcherPriority.Loaded);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 칸 목록에서 줄을 눌렀다. 선택 줄은 ViewModel 이 쥔다 - 목록은 한 방향으로만 받는다(OneWay).
|
|
/// 고른 줄이 그 칸에 없으면(다른 칸에만 있는 줄) 목록 선택은 비지만, 그것을 ViewModel 로 되돌려 보내지 않는다.
|
|
/// </summary>
|
|
private void PaneLines_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
{
|
|
if (DataContext is not HomeViewModel vm) return;
|
|
if (e.AddedItems.Count == 0 || e.AddedItems[0] is not MergeLine line) return;
|
|
if (!ReferenceEquals(vm.SelectedLine, line)) vm.SelectedLine = line;
|
|
}
|
|
|
|
/// <summary>지금 보이는 칸들(목록 · 칸 종류). LOCAL 은 편집기가 주인이라 목록과 함께 따로 다룬다.</summary>
|
|
private (ListBox List, MergeLine.TextPane Kind)[] VisiblePanes(HomeViewModel vm) => vm.IsThreeWay
|
|
? new[]
|
|
{
|
|
(OriginalLocalLines, MergeLine.TextPane.OriginalLocal),
|
|
(BaseLines, MergeLine.TextPane.Base),
|
|
(RemoteLines, MergeLine.TextPane.Remote),
|
|
(LocalLines, MergeLine.TextPane.Local),
|
|
}
|
|
: new[]
|
|
{
|
|
(BaseLines, MergeLine.TextPane.Base),
|
|
(LocalLines, MergeLine.TextPane.Local),
|
|
};
|
|
|
|
/// <summary>잇는 선을 그릴 칸 짝(왼쪽 · 오른쪽). 3-Way 는 위 세 칸 사이 두 틈(사용자 결정 - MERGED 는 따라 스크롤만).</summary>
|
|
private (ListBox Left, MergeLine.TextPane LeftKind, ListBox Right, MergeLine.TextPane RightKind)[] LinkPairs(HomeViewModel vm) => vm.IsThreeWay
|
|
? new[]
|
|
{
|
|
(OriginalLocalLines, MergeLine.TextPane.OriginalLocal, BaseLines, MergeLine.TextPane.Base),
|
|
(BaseLines, MergeLine.TextPane.Base, RemoteLines, MergeLine.TextPane.Remote),
|
|
}
|
|
: new[]
|
|
{
|
|
(BaseLines, MergeLine.TextPane.Base, LocalLines, MergeLine.TextPane.Local),
|
|
};
|
|
|
|
/// <summary>워드랩을 켰을 때만 칸마다 잰 줄 높이를 쓴다. 끄면 모든 줄이 한 줄 높이라 그것이 정확하다.</summary>
|
|
private bool MeasuredRows => WrapMode == TextWrapping.Wrap;
|
|
|
|
/// <summary>정렬 행마다 그 칸에서의 높이(없는 줄은 0).</summary>
|
|
private double[] PaneHeights(HomeViewModel vm, MergeLine.TextPane pane)
|
|
{
|
|
var lines = vm.Lines;
|
|
var heights = new double[lines.Count];
|
|
var measured = MeasuredRows;
|
|
for (var i = 0; i < heights.Length; i++) heights[i] = lines[i].PaneHeight(pane, measured);
|
|
return heights;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 한 칸(<paramref name="master"/>)이 굴렀다 - 나머지 칸을 가운데 기준으로 따라오게 한다.
|
|
/// 같은 줄이 이어지는 곳은 1:1, 한쪽에만 있는 덩어리를 지날 때는 비율로 따라가 한 번에 튀지 않는다.
|
|
/// </summary>
|
|
private void SyncPanesFrom(MergeLine.TextPane master, double offset, double viewport)
|
|
{
|
|
if (DataContext is not HomeViewModel vm || vm.Lines.Count == 0) return;
|
|
|
|
var fromHeights = PaneHeights(vm, master);
|
|
var center = offset + viewport / 2;
|
|
|
|
foreach (var (list, kind) in VisiblePanes(vm))
|
|
{
|
|
if (kind == master) continue;
|
|
|
|
var map = new PaneMapping(fromHeights, PaneHeights(vm, kind));
|
|
var target = map.Map(center);
|
|
|
|
if (kind == MergeLine.TextPane.Local)
|
|
{
|
|
// LOCAL 칸의 주인은 편집기다. 아래 표시층은 편집기와 같은 자리로.
|
|
if (FindScrollViewer(LocalEditor) is not { } editor) continue;
|
|
var y = Clamp(target - editor.ViewportHeight / 2, editor.ScrollableHeight);
|
|
editor.ScrollToVerticalOffset(y);
|
|
Scroll(LocalLines, y, null);
|
|
continue;
|
|
}
|
|
|
|
if (FindScrollViewer(list) is not { } viewer) continue;
|
|
viewer.ScrollToVerticalOffset(Clamp(target - viewer.ViewportHeight / 2, viewer.ScrollableHeight));
|
|
}
|
|
|
|
LinkOverlay.InvalidateVisual();
|
|
}
|
|
|
|
private static double Clamp(double value, double max) => Math.Max(0, Math.Min(value, Math.Max(0, max)));
|
|
|
|
// ================= 잇는 선 그리기 =================
|
|
|
|
/// <summary>한 칸의 세로 좌표를 그림판 기준으로 재는 도구. 그릴 때마다 새로 만든다.</summary>
|
|
private sealed class PaneGeometry
|
|
{
|
|
private readonly ListBox _list;
|
|
private readonly Visual _to;
|
|
private readonly int[] _index; // 정렬 행 i 앞에 이 칸의 줄이 몇 개 있는지 (길이 n+1)
|
|
private readonly double[] _tops; // 이 칸 k 번째 줄의 윗변(모델 높이 기준, 길이 count+1)
|
|
private readonly int _anchorIndex = -1;
|
|
private readonly double _anchorTop;
|
|
|
|
public PaneGeometry(ListBox list, Visual to, IList<MergeLine> lines, MergeLine.TextPane pane, bool measured)
|
|
{
|
|
_list = list;
|
|
_to = to;
|
|
|
|
var n = lines.Count;
|
|
_index = new int[n + 1];
|
|
var heights = new List<double>(n);
|
|
for (var i = 0; i < n; i++)
|
|
{
|
|
var h = lines[i].PaneHeight(pane, measured);
|
|
_index[i + 1] = _index[i] + (h > 0 ? 1 : 0);
|
|
if (h > 0) heights.Add(h);
|
|
}
|
|
_tops = PaneLinks.Tops(heights);
|
|
Count = heights.Count;
|
|
|
|
var origin = list.TransformToVisual(to).Transform(new Point(0, 0));
|
|
Left = origin.X;
|
|
Top = origin.Y;
|
|
Right = origin.X + list.ActualWidth;
|
|
Bottom = origin.Y + list.ActualHeight;
|
|
|
|
// 어림의 기준점: 칸 안에 보이는 첫 줄
|
|
if (FirstVisibleContainer(list) is { } first)
|
|
{
|
|
_anchorIndex = list.ItemContainerGenerator.IndexFromContainer(first);
|
|
if (_anchorIndex >= 0) _anchorTop = first.TransformToVisual(to).Transform(new Point(0, 0)).Y;
|
|
}
|
|
else if (FindScrollViewer(list) is { } viewer)
|
|
{
|
|
_anchorIndex = 0;
|
|
_anchorTop = Top - viewer.VerticalOffset;
|
|
}
|
|
}
|
|
|
|
public int Count { get; }
|
|
public double Left { get; }
|
|
public double Right { get; }
|
|
public double Top { get; }
|
|
public double Bottom { get; }
|
|
public bool Ok => _anchorIndex >= 0 && Right > Left;
|
|
|
|
/// <summary>정렬 행 경계 i(행 i 의 윗변)의 세로 자리. 그려진 줄이면 그 자리, 아니면 보이는 첫 줄에서 모델 높이로 어림.</summary>
|
|
public double Y(int i)
|
|
{
|
|
var k = _index[Math.Max(0, Math.Min(i, _index.Length - 1))];
|
|
|
|
if (k < Count && _list.ItemContainerGenerator.ContainerFromIndex(k) is FrameworkElement item && item.IsVisible)
|
|
return item.TransformToVisual(_to).Transform(new Point(0, 0)).Y;
|
|
|
|
if (k == Count && k > 0 && _list.ItemContainerGenerator.ContainerFromIndex(k - 1) is FrameworkElement last && last.IsVisible)
|
|
return last.TransformToVisual(_to).Transform(new Point(0, last.ActualHeight)).Y;
|
|
|
|
return _anchorTop + (_tops[k] - _tops[Math.Min(_anchorIndex, Count)]);
|
|
}
|
|
}
|
|
|
|
private static readonly Dictionary<Color, (Brush Fill, Pen Edge, Pen Strong)> LinkPens = new Dictionary<Color, (Brush, Pen, Pen)>();
|
|
|
|
private static (Brush Fill, Pen Edge, Pen Strong) PensFor(Brush highlight)
|
|
{
|
|
var c = highlight is SolidColorBrush solid ? solid.Color : Colors.Gray;
|
|
if (LinkPens.TryGetValue(c, out var found)) return found;
|
|
|
|
var fill = new SolidColorBrush(Color.FromArgb(0x38, c.R, c.G, c.B));
|
|
var line = new SolidColorBrush(Color.FromArgb(0xD0, c.R, c.G, c.B));
|
|
fill.Freeze();
|
|
line.Freeze();
|
|
var edge = new Pen(line, 1);
|
|
var strong = new Pen(line, 2);
|
|
edge.Freeze();
|
|
strong.Freeze();
|
|
|
|
var pens = (fill, edge, strong);
|
|
LinkPens[c] = pens;
|
|
return pens;
|
|
}
|
|
|
|
private static readonly Pen MovePen = CreateMovePen();
|
|
|
|
private static Pen CreateMovePen()
|
|
{
|
|
var c = ((SolidColorBrush)Controls.LineHighlightBrushes.Moved).Color;
|
|
var pen = new Pen(new SolidColorBrush(Color.FromArgb(0xF0, c.R, c.G, c.B)), 2.5) { DashStyle = DashStyles.Dash };
|
|
pen.Freeze();
|
|
return pen;
|
|
}
|
|
|
|
/// <summary>덩어리 색. 두 칸 가운데 어느 쪽에만 있는지 · 충돌인지로 가른다.</summary>
|
|
private static Brush BlockBrush(IList<MergeLine> lines, LinkBlock block, MergeLine.TextPane left, MergeLine.TextPane right)
|
|
{
|
|
bool anyLeft = false, anyRight = false, conflict = false;
|
|
LineHighlight first = LineHighlight.None;
|
|
for (var i = block.Start; i < block.End; i++)
|
|
{
|
|
var line = lines[i];
|
|
anyLeft |= line.IsIn(left);
|
|
anyRight |= line.IsIn(right);
|
|
conflict |= line.IsThreeWay && line.IsConflict;
|
|
if (first == LineHighlight.None) first = line.Highlight;
|
|
}
|
|
|
|
if (conflict) return Controls.LineHighlightBrushes.Conflict;
|
|
|
|
// BASE 쪽에만 있으면 지운 것, BASE 가 아닌 쪽에만 있으면 넣은 것. BASE 가 왼쪽(2-Way · BASE|REMOTE)일 수도
|
|
// 오른쪽(LOCAL(Mine)|BASE)일 수도 있다 - 방향을 따라 정한다.
|
|
var baseIsLeft = left == MergeLine.TextPane.Base;
|
|
var onlyBase = baseIsLeft ? !anyRight : !anyLeft;
|
|
var onlyOther = baseIsLeft ? !anyLeft : !anyRight;
|
|
if (onlyOther) return right == MergeLine.TextPane.Remote ? Controls.LineHighlightBrushes.RemoteChange : Controls.LineHighlightBrushes.Added;
|
|
if (onlyBase) return Controls.LineHighlightBrushes.Removed;
|
|
if (right == MergeLine.TextPane.Remote) return Controls.LineHighlightBrushes.RemoteChange;
|
|
return first is LineHighlight.Edited or LineHighlight.Applied
|
|
? Controls.LineHighlightBrushes.For(first)
|
|
: Controls.LineHighlightBrushes.Changed;
|
|
}
|
|
|
|
/// <summary>그림판에 잇는 선을 그린다. 칸이 구를 때 · 글이 바뀔 때마다 다시 그린다.</summary>
|
|
private void DrawLinks(DrawingContext dc)
|
|
{
|
|
if (DataContext is not HomeViewModel vm) return;
|
|
var lines = vm.Lines;
|
|
if (lines.Count == 0) return;
|
|
|
|
var measured = MeasuredRows;
|
|
|
|
foreach (var (leftList, leftKind, rightList, rightKind) in LinkPairs(vm))
|
|
{
|
|
if (!leftList.IsVisible || !rightList.IsVisible || leftList.ActualHeight <= 0 || rightList.ActualHeight <= 0) continue;
|
|
|
|
PaneGeometry left, right;
|
|
try
|
|
{
|
|
left = new PaneGeometry(leftList, LinkOverlay, lines, leftKind, measured);
|
|
right = new PaneGeometry(rightList, LinkOverlay, lines, rightKind, measured);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
continue; // 아직 배치 전 - 다음 그리기에서
|
|
}
|
|
if (!left.Ok || !right.Ok) continue;
|
|
|
|
var top = Math.Max(left.Top, right.Top);
|
|
var bottom = Math.Min(left.Bottom, right.Bottom);
|
|
if (bottom <= top) continue;
|
|
|
|
dc.PushClip(new RectangleGeometry(new Rect(left.Left, top, Math.Max(0, right.Right - left.Left), bottom - top)));
|
|
|
|
var changed = new bool[lines.Count];
|
|
for (var i = 0; i < changed.Length; i++) changed[i] = lines[i].DiffersBetween(leftKind, rightKind);
|
|
|
|
foreach (var block in PaneLinks.Blocks(changed))
|
|
{
|
|
var l1 = left.Y(block.Start);
|
|
var l2 = left.Y(block.End);
|
|
var r1 = right.Y(block.Start);
|
|
var r2 = right.Y(block.End);
|
|
if (Math.Max(l2, r2) < top - 2 || Math.Min(l1, r1) > bottom + 2) continue;
|
|
|
|
var (fill, edge, strong) = PensFor(BlockBrush(lines, block, leftKind, rightKind));
|
|
DrawBlock(dc, fill, edge, strong, left, right, l1, l2, r1, r2);
|
|
}
|
|
|
|
// 자리만 옮긴 덩어리 - 떠난 자리(BASE)에서 옮겨 간 자리(LOCAL)까지 점선으로(두 칸 비교만)
|
|
if (!vm.IsThreeWay) DrawMoves(dc, lines, left, right, top, bottom);
|
|
|
|
dc.Pop();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 덩어리 하나: 두 칸 안의 위 · 아래 경계선과 틈의 사다리꼴. 한쪽에 없는 덩어리는 그 칸에 굵은 선 하나(어디에 들어갈 자리인지).
|
|
/// </summary>
|
|
private static void DrawBlock(DrawingContext dc, Brush fill, Pen edge, Pen strong,
|
|
PaneGeometry left, PaneGeometry right, double l1, double l2, double r1, double r2)
|
|
{
|
|
DrawEdges(dc, edge, strong, left.Left, left.Right, l1, l2);
|
|
DrawEdges(dc, edge, strong, right.Left, right.Right, r1, r2);
|
|
|
|
var gap = new StreamGeometry();
|
|
using (var g = gap.Open())
|
|
{
|
|
g.BeginFigure(new Point(left.Right, l1), isFilled: true, isClosed: true);
|
|
g.LineTo(new Point(right.Left, r1), isStroked: true, isSmoothJoin: false);
|
|
g.LineTo(new Point(right.Left, r2), isStroked: false, isSmoothJoin: false);
|
|
g.LineTo(new Point(left.Right, l2), isStroked: true, isSmoothJoin: false);
|
|
}
|
|
gap.Freeze();
|
|
dc.DrawGeometry(fill, edge, gap);
|
|
}
|
|
|
|
private static void DrawEdges(DrawingContext dc, Pen edge, Pen strong, double x1, double x2, double y1, double y2)
|
|
{
|
|
if (y2 - y1 < 0.5)
|
|
{
|
|
dc.DrawLine(strong, new Point(x1, y1), new Point(x2, y1));
|
|
return;
|
|
}
|
|
dc.DrawLine(edge, new Point(x1, y1), new Point(x2, y1));
|
|
dc.DrawLine(edge, new Point(x1, y2), new Point(x2, y2));
|
|
}
|
|
|
|
private static void DrawMoves(DrawingContext dc, IList<MergeLine> lines, PaneGeometry left, PaneGeometry right, double top, double bottom)
|
|
{
|
|
// 짝 번호 → [떠난 자리 시작, 끝, 옮겨 간 자리 시작, 끝] (정렬 행)
|
|
var groups = new Dictionary<int, int[]>();
|
|
for (var i = 0; i < lines.Count; i++)
|
|
{
|
|
var line = lines[i];
|
|
if (line.MoveGroup == 0 || !line.IsDifferent) continue;
|
|
var fromSide = line.MoveFromBase && line.HasBase;
|
|
var toSide = !line.MoveFromBase && line.HasLocal;
|
|
if (!fromSide && !toSide) continue;
|
|
|
|
if (!groups.TryGetValue(line.MoveGroup, out var g)) groups[line.MoveGroup] = g = new[] { -1, -1, -1, -1 };
|
|
var at = fromSide ? 0 : 2;
|
|
if (g[at] < 0) g[at] = i;
|
|
g[at + 1] = i + 1;
|
|
}
|
|
|
|
foreach (var g in groups.Values)
|
|
{
|
|
if (g[0] < 0 || g[2] < 0) continue;
|
|
var l1 = left.Y(g[0]);
|
|
var l2 = left.Y(g[1]);
|
|
var r1 = right.Y(g[2]);
|
|
var r2 = right.Y(g[3]);
|
|
if (Math.Max(l2, r2) < top || Math.Min(l1, r1) > bottom) continue;
|
|
|
|
// 틈을 가로지르는 두 곡선 - 위끼리 · 아래끼리
|
|
var mid = (left.Right + right.Left) / 2;
|
|
var curve = new StreamGeometry();
|
|
using (var c = curve.Open())
|
|
{
|
|
c.BeginFigure(new Point(left.Right, (l1 + l2) / 2), isFilled: false, isClosed: false);
|
|
c.BezierTo(new Point(mid, (l1 + l2) / 2), new Point(mid, (r1 + r2) / 2), new Point(right.Left, (r1 + r2) / 2), isStroked: true, isSmoothJoin: true);
|
|
}
|
|
curve.Freeze();
|
|
dc.DrawGeometry(null, MovePen, curve);
|
|
}
|
|
}
|
|
}
|
|
}
|