/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.Model/Pieces/PatternPiece.cs
226 строк
9 KB
docNemo
Вкладка деталей выкройки, создание документа и пересчёт производных вкладок
09 авг 2026, 14:03
09 авг 2026, 14:03
dd71293
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.Linq; using ClothesGraph.Model.Drafting; using ClothesGraph.Model.Formulas; namespace ClothesGraph.Model.Pieces; /// <summary>Устойчивый идентификатор детали.</summary> public readonly struct PieceId : IEquatable<PieceId> { private readonly Guid _value; private PieceId(Guid value) => _value = value; public static PieceId New() => new(Guid.NewGuid()); public static PieceId FromGuid(Guid value) => new(value); public Guid Value => _value; public bool IsEmpty => _value == Guid.Empty; public bool Equals(PieceId other) => _value == other._value; public override bool Equals(object? obj) => obj is PieceId other && Equals(other); public override int GetHashCode() => _value.GetHashCode(); public override string ToString() => _value.ToString("N")[..8]; public static bool operator ==(PieceId left, PieceId right) => left.Equals(right); public static bool operator !=(PieceId left, PieceId right) => !left.Equals(right); } /// <summary> /// Участок контура детали — линия чертежа, возможно проходимая в обратную сторону. /// </summary> /// <remarks> /// Деталь ссылается на построения, а не хранит их копию: при изменении чертежа /// контур обязан перестроиться сам, иначе деталь и чертёж разойдутся. /// </remarks> public readonly record struct ContourSegment(NodeId Curve, bool Reversed); /// <summary>Надсечка на контуре детали.</summary> /// <remarks> /// Положение задаётся расстоянием от начала контура, а не координатами: /// так надсечка остаётся на своём месте относительно контура при пересчёте /// на другой размер. /// </remarks> public sealed class Notch(Formula distanceAlongContour, string? label = null) { public Formula DistanceAlongContour { get; set; } = distanceAlongContour; public string? Label { get; set; } = label; } /// <summary>Направление долевой нити.</summary> public sealed class Grainline(Formula angleDegrees) { public Formula AngleDegrees { get; set; } = angleDegrees; } /// <summary> /// Деталь выкройки — замкнутый контур, собранный из участков чертежа. /// </summary> public sealed class PatternPiece { private readonly List<ContourSegment> _contour = []; private readonly Dictionary<int, Formula> _segmentAllowances = []; private readonly List<Notch> _notches = []; public PatternPiece(PieceId id, string name, Formula seamAllowance) { if (id.IsEmpty) throw new ArgumentException("Идентификатор детали не задан", nameof(id)); Id = id; Name = Normalize(name); SeamAllowance = seamAllowance; } public PieceId Id { get; } public string Name { get; set; } /// <summary>Участки контура в порядке обхода.</summary> public IReadOnlyList<ContourSegment> Contour => _contour; /// <summary>Ширина припуска, общая для детали.</summary> public Formula SeamAllowance { get; set; } /// <summary>Переопределения ширины припуска по номеру участка.</summary> public IReadOnlyDictionary<int, Formula> SegmentAllowances => _segmentAllowances; public IReadOnlyList<Notch> Notches => _notches; public Grainline? Grainline { get; set; } /// <summary>Сколько раз деталь выкраивается.</summary> public int Quantity { get; set; } = 1; /// <summary>Дополнительная надпись на детали.</summary> public string? Note { get; set; } /// <summary> /// Номер участка, по которому деталь выкраивается со сгибом. /// </summary> /// <remarks> /// Хранится именно номер участка, а не признак «деталь со сгибом»: без /// указания на конкретную линию невозможно построить развёрнутое полотно. /// </remarks> public int? FoldSegmentIndex { get; set; } public bool IsFolded => FoldSegmentIndex is not null; public void AddSegment(NodeId curve, bool reversed = false) => _contour.Add(new ContourSegment(curve, reversed)); public void InsertSegment(int index, NodeId curve, bool reversed = false) { _contour.Insert(index, new ContourSegment(curve, reversed)); Remap(existing => existing >= index ? existing + 1 : existing); } public void RemoveSegmentAt(int index) { _contour.RemoveAt(index); Remap(existing => existing == index ? null : existing > index ? existing - 1 : existing); } /// <summary>Переставляет участок контура в другое место обхода.</summary> public void MoveSegment(int from, int to) { if (from < 0 || from >= _contour.Count) throw new ArgumentOutOfRangeException(nameof(from)); if (to < 0 || to >= _contour.Count) throw new ArgumentOutOfRangeException(nameof(to)); if (from == to) return; var segment = _contour[from]; _contour.RemoveAt(from); _contour.Insert(to, segment); Remap(existing => { if (existing == from) return to; return from < to ? existing > from && existing <= to ? existing - 1 : existing : existing >= to && existing < from ? existing + 1 : existing; }); } /// <summary>Меняет направление обхода участка.</summary> public void SetSegmentReversed(int index, bool reversed) { if (index < 0 || index >= _contour.Count) throw new ArgumentOutOfRangeException(nameof(index)); _contour[index] = _contour[index] with { Reversed = reversed }; } /// <summary> /// Переносит припуски и сгиб вслед за переехавшими участками. /// </summary> /// <remarks> /// Припуск по участкам и линия сгиба хранятся номерами, а номера сдвигаются /// при любой правке состава контура. Без переноса удаление одного участка /// перевешивало бы заданный на низ детали припуск на соседний, и увидеть /// это можно было бы только на раскрое. /// </remarks> private void Remap(Func<int, int?> map) { var moved = new List<KeyValuePair<int, Formula>>(_segmentAllowances.Count); foreach (var pair in _segmentAllowances) if (map(pair.Key) is { } index) moved.Add(new KeyValuePair<int, Formula>(index, pair.Value)); _segmentAllowances.Clear(); foreach (var pair in moved) _segmentAllowances[pair.Key] = pair.Value; if (FoldSegmentIndex is { } fold) FoldSegmentIndex = map(fold); } public void SetSegmentAllowance(int index, Formula width) { if (index < 0 || index >= _contour.Count) throw new ArgumentOutOfRangeException(nameof(index)); _segmentAllowances[index] = width; } public void ClearSegmentAllowance(int index) => _segmentAllowances.Remove(index); public Formula AllowanceFor(int segmentIndex) => _segmentAllowances.GetValueOrDefault(segmentIndex, SeamAllowance); public void AddNotch(Notch notch) => _notches.Add(notch); public bool RemoveNotch(Notch notch) => _notches.Remove(notch); /// <summary>Построения, от которых зависит деталь.</summary> public IEnumerable<NodeId> Dependencies => _contour.Select(segment => segment.Curve) .Concat(SeamAllowance.Nodes) .Concat(_segmentAllowances.Values.SelectMany(formula => formula.Nodes)) .Concat(_notches.SelectMany(notch => notch.DistanceAlongContour.Nodes)) .Distinct(); internal static string Normalize(string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Имя детали не может быть пустым", nameof(name)); return name.Trim(); } public override string ToString() => $"Деталь «{Name}»"; }