/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.App/Controls/PiecePreview.cs
265 строк
10 KB
docNemo
Вкладка деталей выкройки, создание документа и пересчёт производных вкладок
09 авг 2026, 14:03
09 авг 2026, 14:03
dd71293
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Media; using ClothesGraph.App.ViewModels; using ClothesGraph.Geometry; namespace ClothesGraph.App.Controls; /// <summary> /// Предпросмотр детали выкройки: контур, припуск, надсечки, долевая нить. /// </summary> /// <remarks> /// Отдельный вид от чертежа: на чертеже деталь неотличима от вспомогательных /// построений, а собирая её, нужно видеть именно то, что уйдёт под ножницы — /// замкнут ли контур, куда лёг припуск и где встали надсечки. /// </remarks> public sealed class PiecePreview : Control { private const double PaddingPixels = 28; /// <summary>Длина штриха надсечки в пикселях.</summary> private const double NotchPixels = 9; private static readonly IPen OutlinePen = new Pen( new SolidColorBrush(Color.FromRgb(0xE6, 0xEA, 0xF2)), 1.6); private static readonly IPen AllowancePen = new Pen( new SolidColorBrush(Color.FromRgb(0x7E, 0xA8, 0xD8)), 1.2, new DashStyle([4, 3], 0)); private static readonly IPen FoldPen = new Pen( new SolidColorBrush(Color.FromRgb(0xC8, 0x9B, 0xF0)), 2.4, new DashStyle([9, 4, 2, 4], 0)); private static readonly IPen NotchPen = new Pen( new SolidColorBrush(Color.FromRgb(0xF0, 0xC0, 0x70)), 1.6); private static readonly IPen GrainPen = new Pen( new SolidColorBrush(Color.FromRgb(0x8F, 0xD5, 0xA0)), 1.6); private static readonly IPen GapPen = new Pen( new SolidColorBrush(Color.FromRgb(0xFF, 0x6B, 0x6B)), 2.4); private static readonly IBrush OutlineFill = new SolidColorBrush( Color.FromArgb(0x18, 0xE6, 0xEA, 0xF2)); private static readonly IBrush Caption = new SolidColorBrush( Color.FromRgb(0x9A, 0xA2, 0xB4)); public static readonly StyledProperty<PieceEditorViewModel?> PieceProperty = AvaloniaProperty.Register<PiecePreview, PieceEditorViewModel?>(nameof(Piece)); static PiecePreview() => AffectsRender<PiecePreview>(PieceProperty); public PieceEditorViewModel? Piece { get => GetValue(PieceProperty); set => SetValue(PieceProperty, value); } private double _pixelsPerUnit; private double _offsetX; private double _offsetY; public override void Render(DrawingContext context) { base.Render(context); if (Piece is not { } piece || piece.Geometry is not { } geometry) { DrawHint(context, "Выберите деталь"); return; } var outline = piece.DisplayOutline; if (outline.Count < 2 || Bounds.Width < 40 || Bounds.Height < 40) { DrawHint(context, "Контур ещё не собран: добавьте линии чертежа"); return; } if (!Fit(outline, geometry.SeamLine)) return; // Припуск рисуется первым: он лежит снаружи контура и не должен // перекрывать линию шва, по которой деталь стачивается. if (geometry.SeamLine.Count >= 3 && !piece.ShowUnfolded) context.DrawGeometry(null, AllowancePen, Build(geometry.SeamLine, closed: true)); context.DrawGeometry(OutlineFill, OutlinePen, Build(outline, closed: geometry.IsClosed || piece.ShowUnfolded)); DrawFold(context, piece); DrawGaps(context, geometry.Gaps); DrawNotches(context, geometry.Notches, piece.ShowUnfolded); DrawGrainline(context, piece, geometry.Centre); DrawCaption(context, piece); } /// <summary> /// Подбирает масштаб так, чтобы деталь с припуском поместилась целиком. /// </summary> /// <remarks> /// Границы берутся вместе с припуском: он выходит за контур, и подгонка по /// одному контуру срезала бы его на краях кадра. /// </remarks> private bool Fit(IReadOnlyList<Point2> outline, IReadOnlyList<Point2> allowance) { var all = outline.Concat(allowance).ToArray(); var minX = all.Min(point => point.X.Units); var maxX = all.Max(point => point.X.Units); var minY = all.Min(point => point.Y.Units); var maxY = all.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 DrawFold(DrawingContext context, PieceEditorViewModel piece) { if (piece.FoldAxis is not { } axis || piece.ShowUnfolded) return; context.DrawLine(FoldPen, ToScreen(axis.From), ToScreen(axis.To)); } private void DrawGaps(DrawingContext context, IReadOnlyList<Model.Pieces.ContourGap> gaps) { // Разрыв показывается перечёркнутым отрезком между концами, которые не // сошлись: словами место разрыва не найти, а на глаз оно бывает меньше // толщины линии. foreach (var gap in gaps) { var from = ToScreen(gap.From); var to = ToScreen(gap.To); context.DrawLine(GapPen, from, to); context.DrawEllipse(null, GapPen, from, 4, 4); context.DrawEllipse(null, GapPen, to, 4, 4); } } private void DrawNotches( DrawingContext context, IReadOnlyList<Model.Pieces.PlacedNotch> notches, bool unfolded) { if (unfolded) return; foreach (var notch in notches) { var at = ToScreen(notch.Position); // Штрих ставится поперёк контура: надсечка режется внутрь детали // перпендикулярно краю. var radians = notch.DirectionDegrees * Math.PI / 180.0; var normal = new Vector( Math.Sin(radians) * NotchPixels, Math.Cos(radians) * NotchPixels); context.DrawLine(NotchPen, at - normal, at + normal); } } private void DrawGrainline(DrawingContext context, PieceEditorViewModel piece, Point2 centre) { if (piece.GrainlineDegrees is not { } degrees) return; var radians = degrees * Math.PI / 180.0; var half = Math.Min(Bounds.Width, Bounds.Height) * 0.25; var direction = new Vector(Math.Cos(radians) * half, -Math.Sin(radians) * half); var middle = ToScreen(centre); var from = middle - direction; var to = middle + direction; context.DrawLine(GrainPen, from, to); DrawArrowHead(context, to, direction); DrawArrowHead(context, from, -direction); } private static void DrawArrowHead(DrawingContext context, Point tip, Vector direction) { var length = direction.Length; if (length < 1) return; var unit = direction / length; var side = new Vector(-unit.Y, unit.X); context.DrawLine(GrainPen, tip, tip - unit * 10 + side * 5); context.DrawLine(GrainPen, tip, tip - unit * 10 - side * 5); } private void DrawCaption(DrawingContext context, PieceEditorViewModel piece) { var parts = new List<string> { piece.Piece.Name, $"{piece.Piece.Quantity} шт." }; if (piece.Piece.IsFolded) parts.Add(piece.ShowUnfolded ? "развёрнуто" : "со сгибом"); if (!string.IsNullOrWhiteSpace(piece.Piece.Note)) parts.Add(piece.Piece.Note!); Draw(context, string.Join(" · ", parts), PaddingPixels / 2); } private void DrawHint(DrawingContext context, string message) => Draw(context, message, PaddingPixels / 2); private void Draw(DrawingContext context, string message, double left) { var text = new FormattedText( message, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(FontFamily.Default), 12, Caption); context.DrawText(text, new Point(left, Bounds.Height - text.Height - 6)); } }