/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.App/ViewModels/PieceEditorViewModel.cs
581 строка
17 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.Collections.ObjectModel; using System.Globalization; using System.Linq; using ClothesGraph.Geometry; using ClothesGraph.Model; using ClothesGraph.Model.Drafting; using ClothesGraph.Model.Formulas; using ClothesGraph.Model.Pieces; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ClothesGraph.App.ViewModels; /// <summary> /// Участок контура детали в списке обхода. /// </summary> /// <remarks> /// Показывает не только линию чертежа, но и её длину: собирая контур из /// участков, конструктор сверяется именно с длинами, а не с именами построений. /// </remarks> public sealed partial class ContourRowViewModel : ObservableObject { private readonly PieceEditorViewModel _owner; private bool _loading = true; [ObservableProperty] private bool _reversed; [ObservableProperty] private bool _isFold; [ObservableProperty] private string _allowance = string.Empty; [ObservableProperty] private string _lengthText = string.Empty; [ObservableProperty] private string _curveName = string.Empty; public ContourRowViewModel(PieceEditorViewModel owner, int index) { _owner = owner; Index = index; } /// <summary>Номер участка в обходе контура.</summary> public int Index { get; } public string Number => (Index + 1).ToString(CultureInfo.CurrentCulture); /// <summary>Заполняет строку из модели, не записывая её обратно.</summary> public void Load(string curveName, bool reversed, bool isFold, string allowance, string lengthText) { _loading = true; try { CurveName = curveName; Reversed = reversed; IsFold = isFold; Allowance = allowance; LengthText = lengthText; } finally { _loading = false; } } partial void OnReversedChanged(bool value) { if (!_loading) _owner.SetReversed(Index, value); } partial void OnIsFoldChanged(bool value) { if (!_loading) _owner.SetFold(Index, value); } partial void OnAllowanceChanged(string value) { if (!_loading) _owner.SetSegmentAllowance(Index, value); } } /// <summary>Надсечка в списке детали.</summary> public sealed partial class NotchRowViewModel : ObservableObject { private readonly PieceEditorViewModel _owner; private bool _loading = true; [ObservableProperty] private string _distance = string.Empty; [ObservableProperty] private string _label = string.Empty; public NotchRowViewModel(PieceEditorViewModel owner, Notch notch) { _owner = owner; Notch = notch; } public Notch Notch { get; } public void Load(string distance, string label) { _loading = true; try { Distance = distance; Label = label; } finally { _loading = false; } } partial void OnDistanceChanged(string value) { if (!_loading) _owner.SetNotchDistance(Notch, value); } partial void OnLabelChanged(string value) { if (!_loading) _owner.SetNotchLabel(Notch, value); } } /// <summary> /// Редактирование одной детали выкройки. /// </summary> /// <remarks> /// Правки пишутся в модель сразу по вводу, как в таблице мерок: ошибка формулы /// не откатывает остальное, а показывается строкой и оставляет прежнее значение /// в силе. Геометрия при этом пересобирается целиком — деталь опирается на /// чертёж, и частичное обновление разошлось бы с ним. /// </remarks> public sealed partial class PieceEditorViewModel : ObservableObject { private readonly PatternDocument _document; private readonly PiecesViewModel _owner; private bool _loading = true; [ObservableProperty] private string _name = string.Empty; [ObservableProperty] private int _quantity = 1; [ObservableProperty] private string _note = string.Empty; [ObservableProperty] private string _seamAllowance = string.Empty; [ObservableProperty] private string _grainline = string.Empty; [ObservableProperty] private bool _showUnfolded; [ObservableProperty] private string? _error; [ObservableProperty] private string _summary = string.Empty; public PieceEditorViewModel(PatternDocument document, PiecesViewModel owner, PatternPiece piece) { _document = document; _owner = owner; Piece = piece; Contour = []; Notches = []; Problems = []; } public PatternPiece Piece { get; } public ObservableCollection<ContourRowViewModel> Contour { get; } public ObservableCollection<NotchRowViewModel> Notches { get; } /// <summary>Разрывы контура и предупреждения припуска.</summary> public ObservableCollection<string> Problems { get; } public bool HasProblems => Problems.Count > 0; /// <summary>Вычисленная геометрия; пусто, пока чертёж не пересчитан.</summary> public PieceGeometry? Geometry { get; private set; } /// <summary> /// Контур для показа: половина детали со сгибом либо развёрнутое полотно. /// </summary> public IReadOnlyList<Point2> DisplayOutline { get; private set; } = []; /// <summary>Линия сгиба, если деталь выкраивается со сгибом.</summary> public (Point2 From, Point2 To)? FoldAxis { get; private set; } /// <summary>Угол долевой нити в градусах; пусто, если нить не задана.</summary> public double? GrainlineDegrees { get; private set; } /// <summary>Деталь со сгибом показывается половиной или полотном по выбору.</summary> public bool CanUnfold => Piece.IsFolded; public string DisplayName => Piece.Name; /// <summary>Пересобирает геометрию и содержимое списков по текущему чертежу.</summary> public void Rebuild(DraftEvaluation evaluation, EvaluationValues values) { _loading = true; try { LoadFields(); Geometry = PieceBuilder.Build(Piece, evaluation, values, _document.FormulaUnit); RebuildContour(evaluation); RebuildNotches(); RebuildProblems(); RebuildDisplay(evaluation, values); } catch (FormulaEvaluationException failure) { // Формула ссылается на построение, которое в этом размере не вышло: // деталь показывать нечего, но остальные должны остаться на месте. Geometry = null; DisplayOutline = []; Problems.Clear(); Problems.Add(failure.Message); Summary = "Деталь не построена"; } finally { _loading = false; } OnPropertyChanged(nameof(HasProblems)); OnPropertyChanged(nameof(CanUnfold)); OnPropertyChanged(nameof(DisplayName)); } private void LoadFields() { Name = Piece.Name; Quantity = Piece.Quantity; Note = Piece.Note ?? string.Empty; SeamAllowance = Piece.SeamAllowance.ToText(_document); Grainline = Piece.Grainline?.AngleDegrees.ToText(_document) ?? string.Empty; if (!Piece.IsFolded) ShowUnfolded = false; } private void RebuildContour(DraftEvaluation evaluation) { // Строки пересоздаются только при изменении состава контура: иначе // пересчёт чертежа сбрасывал бы поле, в котором пользователь набирает // ширину припуска. if (Contour.Count != Piece.Contour.Count) { Contour.Clear(); for (var index = 0; index < Piece.Contour.Count; index++) Contour.Add(new ContourRowViewModel(this, index)); } for (var index = 0; index < Piece.Contour.Count; index++) { var segment = Piece.Contour[index]; var name = _document.GetNodeName(segment.Curve) ?? "линия удалена"; var allowance = Piece.SegmentAllowances.TryGetValue(index, out var width) ? width.ToText(_document) : string.Empty; var length = Geometry is { } geometry ? Format(geometry.LengthOf(index)) : string.Empty; Contour[index].Load( name, segment.Reversed, Piece.FoldSegmentIndex == index, allowance, length); } } private void RebuildNotches() { if (Notches.Count != Piece.Notches.Count || Notches.Where((row, index) => row.Notch != Piece.Notches[index]).Any()) { Notches.Clear(); foreach (var notch in Piece.Notches) Notches.Add(new NotchRowViewModel(this, notch)); } foreach (var row in Notches) row.Load(row.Notch.DistanceAlongContour.ToText(_document), row.Notch.Label ?? string.Empty); } private void RebuildProblems() { Problems.Clear(); if (Geometry is not { } geometry) return; foreach (var gap in geometry.Gaps) Problems.Add( $"Контур разомкнут после участка {gap.SegmentIndex + 1}: " + $"расхождение {Format(gap.Distance)}"); foreach (var warning in geometry.Warnings) Problems.Add(warning.Message); Summary = geometry.Outline.Count < 3 ? "Контур ещё не собран" : geometry.IsClosed ? $"Периметр {Format(geometry.Perimeter)}, участков {Piece.Contour.Count}" : $"Контур не замкнут, участков {Piece.Contour.Count}"; } private void RebuildDisplay(DraftEvaluation evaluation, EvaluationValues values) { if (Geometry is not { } geometry) { DisplayOutline = []; FoldAxis = null; GrainlineDegrees = null; return; } DisplayOutline = ShowUnfolded ? PieceBuilder.Unfold(Piece, geometry, evaluation) : geometry.Outline; FoldAxis = null; if (Piece.FoldSegmentIndex is { } foldIndex && foldIndex < Piece.Contour.Count && evaluation.TryGetCurve(Piece.Contour[foldIndex].Curve, out var curve) && curve is not null) { FoldAxis = (curve.Start, curve.End); } // Направление берётся из геометрии, а не вычисляется здесь заново: // тот же источник обслуживает вывод на лист, и разойтись им нечем. GrainlineDegrees = geometry.GrainlineDegrees; } private string Format(Length length) => length.ToString(_document.DisplayUnit, CultureInfo.CurrentCulture); partial void OnNameChanged(string value) { if (_loading) return; if (string.IsNullOrWhiteSpace(value)) { Error = "Имя детали не может быть пустым"; return; } Piece.Name = value.Trim(); Error = null; OnPropertyChanged(nameof(DisplayName)); _owner.NotifyEdited(rebuild: false); } partial void OnQuantityChanged(int value) { if (_loading) return; Piece.Quantity = Math.Max(1, value); _owner.NotifyEdited(rebuild: false); } partial void OnNoteChanged(string value) { if (_loading) return; Piece.Note = string.IsNullOrWhiteSpace(value) ? null : value.Trim(); _owner.NotifyEdited(rebuild: false); } partial void OnSeamAllowanceChanged(string value) { if (_loading) return; if (TryParse(value) is not { } formula) return; Piece.SeamAllowance = formula; _owner.NotifyEdited(); } partial void OnGrainlineChanged(string value) { if (_loading) return; if (string.IsNullOrWhiteSpace(value)) { Piece.Grainline = null; Error = null; _owner.NotifyEdited(); return; } if (TryParse(value) is not { } formula) return; Piece.Grainline = new Grainline(formula); _owner.NotifyEdited(); } partial void OnShowUnfoldedChanged(bool value) => _owner.NotifyEdited(); internal void SetReversed(int index, bool reversed) { Piece.SetSegmentReversed(index, reversed); _owner.NotifyEdited(); } /// <summary> /// Отмечает участок линией сгиба. /// </summary> /// <remarks> /// Сгиб бывает только один: деталь складывается по одной линии, и вторая /// сделала бы развёртку неоднозначной. /// </remarks> internal void SetFold(int index, bool isFold) { Piece.FoldSegmentIndex = isFold ? index : null; if (!isFold) ShowUnfolded = false; _owner.NotifyEdited(); } internal void SetSegmentAllowance(int index, string text) { if (string.IsNullOrWhiteSpace(text)) { Piece.ClearSegmentAllowance(index); Error = null; _owner.NotifyEdited(); return; } if (TryParse(text) is not { } formula) return; Piece.SetSegmentAllowance(index, formula); _owner.NotifyEdited(); } internal void SetNotchDistance(Notch notch, string text) { if (TryParse(text) is not { } formula) return; notch.DistanceAlongContour = formula; _owner.NotifyEdited(); } internal void SetNotchLabel(Notch notch, string text) { notch.Label = string.IsNullOrWhiteSpace(text) ? null : text.Trim(); _owner.NotifyEdited(rebuild: false); } /// <summary>Добавляет в конец обхода линию, выбранную в списке построений.</summary> [RelayCommand] private void AddSegment() { if (_owner.SelectedCurve is not { } curve) { Error = "Выберите линию чертежа"; return; } Piece.AddSegment(curve.Id); Error = null; _owner.NotifyEdited(); } [RelayCommand] private void RemoveSegment(ContourRowViewModel? row) { if (row is null || row.Index >= Piece.Contour.Count) return; Piece.RemoveSegmentAt(row.Index); // Строк стало меньше: список пересоздаётся, иначе номера участков // разойдутся с моделью. Contour.Clear(); _owner.NotifyEdited(); } [RelayCommand] private void MoveSegmentUp(ContourRowViewModel? row) { if (row is null || row.Index <= 0) return; Piece.MoveSegment(row.Index, row.Index - 1); _owner.NotifyEdited(); } [RelayCommand] private void MoveSegmentDown(ContourRowViewModel? row) { if (row is null || row.Index >= Piece.Contour.Count - 1) return; Piece.MoveSegment(row.Index, row.Index + 1); _owner.NotifyEdited(); } [RelayCommand] private void AddNotch() { Piece.AddNotch(new Notch(Formula.Constant(0))); Notches.Clear(); _owner.NotifyEdited(); } [RelayCommand] private void RemoveNotch(NotchRowViewModel? row) { if (row is null) return; Piece.RemoveNotch(row.Notch); Notches.Clear(); _owner.NotifyEdited(); } /// <summary>Разбирает формулу, оставляя прежнее значение при ошибке.</summary> private Formula? TryParse(string text) { try { var formula = _document.ParseFormula(text); Error = null; return formula; } catch (FormulaException error) { Error = $"{error.Message} (позиция {error.Position + 1})"; return null; } } }