/
docNemo
/
clothes-graph
Обзор
Документация
Войти
/
docNemo
/
clothes-graph
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/ClothesGraph.App/ViewModels/UnderlayViewModel.cs
290 строк
10 KB
docNemo
Запрос на слияние 'interface-to-spec' (
#1
) из interface-to-spec в main
09 авг 2026, 20:28
Верифицирован
09 авг 2026, 20:28
1bbc436
Код
Авторство
О чём код?
using System; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Platform.Storage; using ClothesGraph.Geometry; using ClothesGraph.Model; using ClothesGraph.Model.Underlay; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ClothesGraph.App.ViewModels; /// <summary>Подложенное изображение в списке.</summary> public sealed partial class UnderlayRowViewModel : ObservableObject { private readonly UnderlayViewModel _owner; [ObservableProperty] private bool _isVisible; [ObservableProperty] private double _opacity; [ObservableProperty] private string _realWidth = string.Empty; [ObservableProperty] private string _x = string.Empty; [ObservableProperty] private string _y = string.Empty; [ObservableProperty] private double _rotation; public UnderlayRowViewModel(UnderlayViewModel owner, UnderlayImage image) { _owner = owner; Image = image; _isVisible = image.IsVisible; _opacity = image.Opacity; _rotation = image.RotationDegrees; _x = Format(image.Origin.X, owner.DisplayUnit); _y = Format(image.Origin.Y, owner.DisplayUnit); if (image.IsCalibrated) _realWidth = Format(image.Width, owner.DisplayUnit); } private static string Format(Length value, LengthUnit unit) => value.To(unit).ToString("0.#", CultureInfo.CurrentCulture); public UnderlayImage Image { get; } public string Name => Image.Name; public string Status => Image.IsCalibrated ? $"{Image.PixelWidth}×{Image.PixelHeight} точек" : "масштаб не задан"; public bool NeedsCalibration => !Image.IsCalibrated; partial void OnIsVisibleChanged(bool value) { Image.IsVisible = value; _owner.NotifyChanged(); } partial void OnOpacityChanged(double value) { Image.Opacity = value; _owner.NotifyChanged(); } partial void OnRotationChanged(double value) { Image.RotationDegrees = value; _owner.NotifyChanged(); } partial void OnXChanged(string value) => SetOrigin(value, Y); partial void OnYChanged(string value) => SetOrigin(X, value); /// <summary> /// Ставит левый верхний угол изображения в указанное место чертежа. /// </summary> /// <remarks> /// Положение задаётся числами, а не перетаскиванием: подложку двигают /// редко и точно — обычно совмещая её с уже построенной линией, — и ввести /// координату надёжнее, чем попадать мышью. /// </remarks> private void SetOrigin(string x, string y) { var unit = _owner.DisplayUnit; if (!Length.TryParse(x, unit, CultureInfo.CurrentCulture, out var left) || !Length.TryParse(y, unit, CultureInfo.CurrentCulture, out var top)) return; Image.Origin = new Point2(left, top); _owner.NotifyChanged(); } /// <summary> /// Задаёт масштаб по известной ширине изображения на чертеже. /// </summary> /// <remarks> /// Упрощённая калибровка: полная выполняется указанием двух точек на /// изображении, но задать ширину образца обычно проще и быстрее. /// </remarks> partial void OnRealWidthChanged(string value) { if (!Length.TryParse(value, _owner.DisplayUnit, CultureInfo.CurrentCulture, out var width)) return; if (width <= Length.Zero) return; Image.Calibrate((0, 0), (Image.PixelWidth, 0), width); OnPropertyChanged(nameof(Status)); OnPropertyChanged(nameof(NeedsCalibration)); _owner.NotifyChanged(); } } /// <summary>Подложки под чертежом: добавление, видимость, калибровка.</summary> public sealed partial class UnderlayViewModel : ObservableObject { private readonly PatternDocument _document; private readonly Func<Window?> _window; [ObservableProperty] private string? _warning; public UnderlayViewModel(PatternDocument document, Func<Window?> window) { _document = document; _window = window; Rows = []; // Список идёт в порядке вывода: нижняя подложка первой, как её видит // пользователь на чертеже. foreach (var image in document.Underlays.OrderBy(image => image.Order)) Rows.Add(new UnderlayRowViewModel(this, image)); UpdateWarning(); } public ObservableCollection<UnderlayRowViewModel> Rows { get; } public LengthUnit DisplayUnit => _document.DisplayUnit; public event EventHandler? Changed; /// <summary> /// Состав или вид подложек изменился. /// </summary> /// <remarks> /// Единственный путь правки подложек, поэтому пометка о несохранённых /// изменениях ставится здесь: видимость, прозрачность и калибровка уходят /// в файл наравне с самим изображением. /// </remarks> internal void NotifyChanged() { _document.MarkModified(); UpdateWarning(); Changed?.Invoke(this, EventArgs.Empty); } [RelayCommand] private async Task AddAsync() { if (_window() is not { } window) return; var files = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = "Подложить изображение", AllowMultiple = false, FileTypeFilter = [FilePickerFileTypes.ImageAll] }); if (files.FirstOrDefault()?.TryGetLocalPath() is not { } path) return; if (UnderlayLoader.Add(_document, path) is not { } image) { Warning = "Не удалось прочитать изображение"; return; } // Первое размещение — прикидкой на ширину чертежа: без неё подложка // легла бы в масштабе «точка к сотой миллиметра» и оказалась бы // размером с ноготь. Калиброванной подложка при этом не считается: // пятьсот миллиметров взяты с потолка, и снимать с них размеры нельзя. UnderlayLoader.FitToWidth(image, Point2.Origin, Length.FromMillimetres(500)); Rows.Add(new UnderlayRowViewModel(this, image)); NotifyChanged(); } [RelayCommand] private void Remove(UnderlayRowViewModel? row) { if (row is null) return; _document.Underlays.Remove(row.Image); Rows.Remove(row); Renumber(); NotifyChanged(); } /// <summary>Поднимает подложку выше по порядку вывода.</summary> [RelayCommand] private void MoveUp(UnderlayRowViewModel? row) => Move(row, -1); [RelayCommand] private void MoveDown(UnderlayRowViewModel? row) => Move(row, +1); /// <summary> /// Переставляет подложку в порядке вывода. /// </summary> /// <remarks> /// Порядок задаётся списком, а не числом в поле: пользователь видит, что /// лежит выше, и переставляет соседей, не считая номера. /// </remarks> private void Move(UnderlayRowViewModel? row, int step) { if (row is null) return; var from = Rows.IndexOf(row); var to = from + step; if (from < 0 || to < 0 || to >= Rows.Count) return; Rows.Move(from, to); Renumber(); NotifyChanged(); } /// <summary>Приводит номера порядка к положению строк в списке.</summary> private void Renumber() { for (var index = 0; index < Rows.Count; index++) Rows[index].Image.Order = index; } /// <summary>Пересобирает предупреждения: состав обводок изменился.</summary> internal void Refresh() => UpdateWarning(); /// <summary> /// Предупреждает о начатой обводке по некалиброванной подложке. /// </summary> /// <remarks> /// Момент начала обводки — последний, когда предупреждение ещё полезно: /// снятые с непроверенной подложки размеры произвольны, и узнать об этом /// после обводки значит обвести заново. /// </remarks> internal void NoteUnanchoredTracing() => UpdateWarning(); private void UpdateWarning() { var uncalibrated = _document.Underlays.Where(image => !image.IsCalibrated).ToArray(); var ungraded = _document.UngradedTracings(); var problems = new System.Collections.Generic.List<string>(); if (uncalibrated.Length > 0) problems.Add( "масштаб не задан: " + string.Join(", ", uncalibrated.Select(image => image.Name)) + ". Снимать размеры с такой подложки нельзя"); if (ungraded.Count > 0) problems.Add( "не привязаны к каркасу и не будут градироваться: " + string.Join(", ", ungraded.Select(node => node.Name))); Warning = problems.Count == 0 ? null : string.Join("\n", problems); } }