/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.App/Controls/SeamMap.cs
249 строк
8 KB
docNemo
Запрос на слияние 'interface-to-spec' (
#1
) из interface-to-spec в main
09 авг 2026, 20:28
Верифицирован
09 авг 2026, 20:28
1bbc436
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Media; using ClothesGraph.App.ViewModels; using ClothesGraph.Geometry; namespace ClothesGraph.App.Controls; /// <summary> /// Контур детали с участками, которые можно выбрать указанием. /// </summary> /// <remarks> /// Участок раньше задавался вводом номера, и номер приходилось держать в /// голове: перечень незашитых краёв называл его от единицы, поле ввода ждало /// от нуля, и пользователь молча сшивал соседний участок. Указание на нужный /// край снимает вопрос целиком. /// /// Здесь же видно состояние швов: сшитый участок, разошедшийся и свободный /// различаются цветом — проблемный шов должен быть заметен там, где его /// правят, а не только в перечне. /// </remarks> public sealed class SeamMap : Control { private const double PaddingPixels = 18; /// <summary>Допуск попадания указателем в участок, в пикселях.</summary> private const double PickRadiusPixels = 10; private static readonly IBrush Fill = new SolidColorBrush( Color.FromArgb(0x14, 0xE6, 0xEA, 0xF2)); private static readonly IBrush Caption = new SolidColorBrush( Color.FromRgb(0x9A, 0xA2, 0xB4)); public static readonly StyledProperty<PieceSeamMapViewModel?> PieceProperty = AvaloniaProperty.Register<SeamMap, PieceSeamMapViewModel?>(nameof(Piece)); static SeamMap() => AffectsRender<SeamMap>(PieceProperty); public SeamMap() { PointerPressed += OnPointerPressed; PointerMoved += OnPointerMoved; PointerExited += (_, _) => SetHovered(null); } public PieceSeamMapViewModel? Piece { get => GetValue(PieceProperty); set => SetValue(PieceProperty, value); } private double _pixelsPerUnit; private double _offsetX; private double _offsetY; private int? _hovered; public override void Render(DrawingContext context) { base.Render(context); if (Piece is not { } piece || piece.Segments.Count == 0) { Draw(context, "Выберите деталь"); return; } if (!Fit(piece)) { Draw(context, "Контур ещё не собран"); return; } // Заливка кладётся по всему контуру: она обозначает тело детали, // а раскрашены именно края. if (piece.Outline.Count >= 3) context.DrawGeometry(Fill, null, Build(piece.Outline, closed: true)); foreach (var segment in piece.Segments) { if (segment.Points.Count < 2) continue; var width = segment.Index == piece.SelectedSegment ? 4.0 : segment.Index == _hovered ? 3.0 : 2.0; var pen = new Pen(new SolidColorBrush(Parse(segment.Colour)), width); context.DrawGeometry(null, pen, Build(segment.Points, closed: false)); DrawNumber(context, segment); } Draw(context, piece.Hint); } private void DrawNumber(DrawingContext context, SeamSegmentViewModel segment) { var middle = segment.Points[segment.Points.Count / 2]; var text = new FormattedText( segment.Number.ToString(CultureInfo.CurrentCulture), CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(FontFamily.Default), 11, new SolidColorBrush(Parse(segment.Colour))); var at = ToScreen(middle); context.DrawText(text, new Point(at.X - text.Width / 2, at.Y - text.Height / 2)); } private static Color Parse(string value) => Color.Parse(value); private bool Fit(PieceSeamMapViewModel piece) { if (piece.Outline.Count < 2 || Bounds.Width < 40 || Bounds.Height < 40) return false; var minX = piece.Outline.Min(point => point.X.Units); var maxX = piece.Outline.Max(point => point.X.Units); var minY = piece.Outline.Min(point => point.Y.Units); var maxY = piece.Outline.Max(point => point.Y.Units); var width = Math.Max(1, maxX - minX); var height = Math.Max(1, maxY - minY); _pixelsPerUnit = Math.Min( (Bounds.Width - PaddingPixels * 2) / width, (Bounds.Height - PaddingPixels * 2) / height); if (_pixelsPerUnit <= 0) return false; _offsetX = (Bounds.Width - width * _pixelsPerUnit) / 2 - minX * _pixelsPerUnit; // Ось Y чертежа направлена вверх, экранная — вниз. _offsetY = (Bounds.Height - height * _pixelsPerUnit) / 2 + maxY * _pixelsPerUnit; return true; } private Point ToScreen(Point2 point) => new(_offsetX + point.X.Units * _pixelsPerUnit, _offsetY - point.Y.Units * _pixelsPerUnit); private StreamGeometry Build(IReadOnlyList<Point2> points, bool closed) { var geometry = new StreamGeometry(); using (var sink = geometry.Open()) { sink.BeginFigure(ToScreen(points[0]), isFilled: closed); for (var index = 1; index < points.Count; index++) sink.LineTo(ToScreen(points[index])); sink.EndFigure(closed); } return geometry; } private void OnPointerPressed(object? sender, PointerPressedEventArgs e) { if (Piece is not { } piece) return; if (Nearest(e.GetPosition(this)) is { } index) piece.Select(index); } private void OnPointerMoved(object? sender, PointerEventArgs e) => SetHovered(Nearest(e.GetPosition(this))); private void SetHovered(int? index) { if (_hovered == index) return; _hovered = index; InvalidateVisual(); } /// <summary>Участок, ближайший к указателю в пределах допуска.</summary> private int? Nearest(Point position) { if (Piece is not { } piece || _pixelsPerUnit <= 0) return null; var best = (int?)null; var bestDistance = PickRadiusPixels; foreach (var segment in piece.Segments) { for (var index = 1; index < segment.Points.Count; index++) { var distance = DistanceToSegment( position, ToScreen(segment.Points[index - 1]), ToScreen(segment.Points[index])); if (distance <= bestDistance) { bestDistance = distance; best = segment.Index; } } } return best; } private static double DistanceToSegment(Point point, Point from, Point to) { var dx = to.X - from.X; var dy = to.Y - from.Y; var lengthSquared = dx * dx + dy * dy; if (lengthSquared < 1e-9) return Math.Sqrt( (point.X - from.X) * (point.X - from.X) + (point.Y - from.Y) * (point.Y - from.Y)); var t = Math.Clamp( ((point.X - from.X) * dx + (point.Y - from.Y) * dy) / lengthSquared, 0, 1); var nearestX = from.X + dx * t; var nearestY = from.Y + dy * t; return Math.Sqrt( (point.X - nearestX) * (point.X - nearestX) + (point.Y - nearestY) * (point.Y - nearestY)); } private void Draw(DrawingContext context, string message) { if (string.IsNullOrWhiteSpace(message)) return; var text = new FormattedText( message, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(FontFamily.Default), 11, Caption); context.DrawText(text, new Point(8, Bounds.Height - text.Height - 4)); } }