/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.App/ViewModels/PrintViewModel.cs
385 строк
13 KB
docNemo
Запрос на слияние 'pdf-range-page-sets' (
#2
) из pdf-range-page-sets в main
09 авг 2026, 21:13
Верифицирован
09 авг 2026, 21:13
7ec0788
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Platform.Storage; using ClothesGraph.Geometry; using ClothesGraph.Model; using ClothesGraph.Model.Grading; using ClothesGraph.Model.Pieces; using ClothesGraph.Printing; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ClothesGraph.App.ViewModels; /// <summary>Деталь в списке вывода на печать.</summary> public sealed partial class PrintPieceRow : ObservableObject { private readonly PrintViewModel _owner; [ObservableProperty] private bool _isIncluded; internal PrintPieceRow(PrintViewModel owner, PatternPiece piece, bool included) { _owner = owner; Piece = piece; _isIncluded = included; } internal PatternPiece Piece { get; } public string Name => Piece.Name; partial void OnIsIncludedChanged(bool value) => _owner.SetIncluded(Piece, value); } /// <summary>Печать и экспорт: настройки листа, предпросмотр, вывод.</summary> public sealed partial class PrintViewModel : ObservableObject { private readonly PatternDocument _document; private readonly PrintService _service = new(); [ObservableProperty] private PageSize _pageSize = PageSize.A4; [ObservableProperty] private bool _isLandscape; [ObservableProperty] private bool _joinWithOverlap = true; [ObservableProperty] private double _overlapMillimetres = 15; [ObservableProperty] private double _marginMillimetres = 8; [ObservableProperty] private SheetGrid? _grid; [ObservableProperty] private PrintContent? _content; [ObservableProperty] private string _summary = string.Empty; [ObservableProperty] private string? _warning; public PrintViewModel(PatternDocument document, Func<Window?>? window = null) { _document = document; _window = window ?? (() => null); Sizes = [.. PageSize.Standard, PageSize.Roll(610)]; Pieces = []; Problems = []; RebuildPieces(); Refresh(); } private readonly Func<Window?> _window; public ObservableCollection<PageSize> Sizes { get; } /// <summary>Детали документа с признаком вывода на печать.</summary> public ObservableCollection<PrintPieceRow> Pieces { get; } /// <summary>Размеры ряда, которые не строятся.</summary> public ObservableCollection<string> Problems { get; } public bool HasProblems => Problems.Count > 0; /// <summary>Выводить весь размерный ряд, а не один размер.</summary> [ObservableProperty] private bool _wholeRange; private void RebuildPieces() { Pieces.Clear(); foreach (var piece in _document.Pieces) Pieces.Add(new PrintPieceRow(this, piece, !_document.Print.ExcludedFromPrint.Contains(piece.Id))); } /// <summary>Включает или исключает деталь из вывода.</summary> internal void SetIncluded(PatternPiece piece, bool included) { if (included) _document.Print.ExcludedFromPrint.Remove(piece.Id); else _document.Print.ExcludedFromPrint.Add(piece.Id); _document.MarkModified(); Refresh(); } public event EventHandler? PreviewChanged; partial void OnPageSizeChanged(PageSize value) => Refresh(); partial void OnIsLandscapeChanged(bool value) => Refresh(); partial void OnJoinWithOverlapChanged(bool value) => Refresh(); partial void OnOverlapMillimetresChanged(double value) => Refresh(); partial void OnMarginMillimetresChanged(double value) => Refresh(); private PageLayout BuildLayout() => new( PageSize, IsLandscape ? PageOrientation.Landscape : PageOrientation.Portrait, Length.FromMillimetres(MarginMillimetres), JoinWithOverlap ? SheetJoinMode.Overlap : SheetJoinMode.Butt, Length.FromMillimetres(OverlapMillimetres)); /// <summary>Пересчитывает раскладку и предпросмотр.</summary> public void Refresh() { try { var layout = BuildLayout(); var content = _service.BuildContent(_document); if (content.Pieces.Count == 0) { Grid = null; Content = null; Summary = "В документе нет деталей для вывода"; Warning = null; PreviewChanged?.Invoke(this, EventArgs.Empty); return; } // Раскладку считает экспортёр: предпросмотр обязан показывать те же // листы, что уйдут в файл, а вторая реализация с ними разошлась бы. var grid = PdfExporter.GridFor(content, layout); Content = content; Grid = grid; Summary = $"Листов: {grid.Count} ({grid.Columns} × {grid.Rows}), формат {layout.Size.Name}"; UpdateWarning(content, layout); } catch (ArgumentException error) { Grid = null; Content = null; Summary = "Не удалось построить раскладку"; Warning = error.Message; } PreviewChanged?.Invoke(this, EventArgs.Empty); } /// <summary> /// Запоминает новое положение детали и пересчитывает раскладку листов. /// </summary> /// <remarks> /// Положение уходит в документ, а не остаётся в содержимом для вывода: /// иначе при смене формата листа раскладка, над которой пользователь /// работал, сбрасывалась бы к автоматической. /// </remarks> public void RememberPlacement(PlacedPiece placed) { _document.Print.Placements[placed.Piece.Id] = new PiecePlacement(placed.Offset, placed.RotationDegrees); _document.MarkModified(); RebuildGrid(); } /// <summary>Возвращает деталям автоматическую раскладку в ряд.</summary> [RelayCommand] private void ResetLayout() { _document.Print.Placements.Clear(); _document.MarkModified(); Refresh(); } /// <summary> /// Раскладывает детали плотнее, чтобы уходило меньше листов. /// </summary> /// <remarks> /// Результат запоминается как ручное размещение: пользователь вправе /// поправить его мышью, и повторный пересчёт не должен затирать правки. /// </remarks> [RelayCommand] private void AutoArrange() { if (Content is null) return; var result = new AutoNester(Length.FromMillimetres(GapMillimetres)) .Arrange(Content, BuildLayout()); foreach (var placed in Content.Pieces) _document.Print.Placements[placed.Piece.Id] = new PiecePlacement(placed.Offset, placed.RotationDegrees); _document.MarkModified(); RebuildGrid(); Summary = result.Improved ? $"Листов: {result.SheetsAfter}, сэкономлено {result.Saved}" : $"Листов: {result.SheetsAfter} — плотнее уложить не удалось"; } /// <summary>Зазор между деталями при автоматической раскладке.</summary> [ObservableProperty] private double _gapMillimetres = 5; /// <summary>Пересчитывает сетку листов, не пересобирая содержимое.</summary> private void RebuildGrid() { if (Content is null) return; try { var layout = BuildLayout(); var grid = PdfExporter.GridFor(Content, layout); Grid = grid; Summary = $"Листов: {grid.Count} ({grid.Columns} × {grid.Rows}), формат {layout.Size.Name}"; UpdateWarning(Content, layout); } catch (ArgumentException error) { Warning = error.Message; } PreviewChanged?.Invoke(this, EventArgs.Empty); } private void UpdateWarning(PrintContent content, PageLayout layout) { var problems = new List<string>(); var oversized = content.PiecesLargerThanSheet(layout); if (oversized.Count > 0) problems.Add( "Не помещаются на лист целиком и будут разбиты между листами: " + string.Join(", ", oversized.Select(piece => piece.Piece.Name))); RebuildProblems(); // Непостроимые размеры перечисляются до вывода: обнаружить их на // бумаге значит перепечатать весь ряд. if (WholeRange && Problems.Count > 0) problems.Add( "Не строятся и в вывод не попадут: " + string.Join(", ", Problems)); var ungraded = _document.UngradedTracings(); if (WholeRange && ungraded.Count > 0) problems.Add( "Не привязаны к каркасу и во всех размерах останутся на месте: " + string.Join(", ", ungraded.Select(node => node.Name))); Warning = problems.Count == 0 ? null : string.Join("\n", problems); } private void RebuildProblems() { Problems.Clear(); foreach (var report in new GradingService(_document).ProblemSizes()) Problems.Add(report.Describe()); OnPropertyChanged(nameof(HasProblems)); } partial void OnWholeRangeChanged(bool value) => Refresh(); /// <summary> /// Сохраняет выкройку в PDF, спросив куда. /// </summary> /// <remarks> /// Раньше файл молча уходил в «Мои документы» под именем с датой: найти /// его потом можно было только зная это правило. /// </remarks> [RelayCommand] private async Task ExportPdfAsync() { if (Grid is null || Content is null) return; if (_window() is not { } window) return; var file = await window.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = WholeRange ? "Сохранить размерный ряд" : "Сохранить выкройку", SuggestedFileName = $"Выкройка-{DateTime.Now:yyyy-MM-dd-HHmm}.pdf", DefaultExtension = "pdf", FileTypeChoices = [new FilePickerFileType("PDF") { Patterns = ["*.pdf"] }] }); if (file?.TryGetLocalPath() is not { } path) return; Summary = WholeRange ? ExportRange(path) : ExportSingle(path); } private string ExportSingle(string path) { new PdfExporter().Save(Grid!, Content!, path); return $"Сохранено: {path}"; } /// <summary> /// Выводит весь размерный ряд наборами страниц по размерам. /// </summary> /// <remarks> /// Размеры идут наборами страниц, а не слоями PDF: решение 10 в /// `design.md`. Непостроимые размеры перечисляются до вывода: обнаружить /// их на бумаге значит перепечатать. /// </remarks> private string ExportRange(string path) { var layout = BuildLayout(); var grading = new GradingService(_document); var nester = new AutoNester(Length.FromMillimetres(GapMillimetres)); var ready = _document.Sizes.Sizes .Where(size => grading.ReportFor(size).IsReady) .Select(size => { var content = _service.BuildContent(_document, size); nester.Arrange(content, layout); return (Size: size, Content: content); }) .ToArray(); if (ready.Length == 0) return "Ни один размер ряда не строится: выводить нечего"; var grids = new PdfExporter().ExportRange(ready, layout, path); var sheets = grids.Sum(item => item.Grid.Count); var skipped = Problems.Count == 0 ? string.Empty : $", пропущено размеров: {Problems.Count}"; return $"Сохранено: {path} — размеров {ready.Length}, листов {sheets}{skipped}"; } [RelayCommand] private void Print() { var sent = _service.Print(_document, BuildLayout()); Summary = sent ? "Документ отправлен на печать" : "Печать PDF не назначена в системе — файл открыт для печати вручную"; } }