using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform.Storage; using TraceCad.App.Tools; using TraceCad.Core.Model; using TraceCad.Core.Serialization; using TraceCad.Dxf; using TraceCad.Vision.Calibration; namespace TraceCad.App; public sealed class MainWindow : Window { private static readonly SolidColorBrush AppBackground = Brush("#0b1020"); private static readonly SolidColorBrush ChromeBackground = Brush("#111827"); private static readonly SolidColorBrush GlassBackground = Brush("#0f172a"); private static readonly SolidColorBrush PanelBackground = Brush("#111827"); private static readonly SolidColorBrush RailBackground = Brush("#0f172a"); private static readonly SolidColorBrush RailButtonBackground = Brush("#1f2937"); private static readonly SolidColorBrush RailButtonSelectedBackground = Brush("#1d4ed8"); private static readonly SolidColorBrush StrokeBrush = Brush("#334155"); private static readonly SolidColorBrush GlassStroke = Brush("#475569"); private static readonly SolidColorBrush TextPrimary = Brush("#f8fafc"); private static readonly SolidColorBrush TextSecondary = Brush("#cbd5e1"); private static readonly SolidColorBrush TextMuted = Brush("#94a3b8"); private static readonly SolidColorBrush TextOnDark = Brush("#ffffff"); private static readonly SolidColorBrush Accent = Brush("#3b82f6"); private static readonly SolidColorBrush AccentSoft = Brush("#172554"); private readonly SketchCanvas _canvas = new(); private readonly TextBlock _status = Text("", TextSecondary, 12); private readonly TextBlock _toolState = Text("", TextSecondary, 12); private readonly TextBlock _referenceName = Text("", TextSecondary, 12); private readonly Slider _referenceOpacity = new() { Minimum = 0.0, Maximum = 1.0, Value = 0.55 }; private readonly CheckBox _referenceLocked = new() { Content = "Locked", Foreground = TextPrimary }; private readonly NumericUpDown _referenceX = NumberInput(1.0); private readonly NumericUpDown _referenceY = NumberInput(1.0); private readonly NumericUpDown _referenceScale = NumberInput(0.01, minimum: 0.0001); private readonly NumericUpDown _referenceRotation = NumberInput(1.0, minimum: -3600.0, maximum: 3600.0); private readonly IDxfExporter _dxfExporter = new NetDxfExporter(); private readonly SheetCalibrator _sheetCalibrator = new(); private readonly Dictionary _toolButtons = new(); private bool _syncingReferencePanel; public MainWindow() { Title = "easyTrace"; Width = 1360; Height = 860; MinWidth = 980; MinHeight = 640; Background = AppBackground; TransparencyLevelHint = new[] { WindowTransparencyLevel.None }; TransparencyBackgroundFallback = AppBackground; _referenceOpacity.PropertyChanged += (_, e) => { if (_syncingReferencePanel || e.Property != RangeBase.ValueProperty) { return; } _canvas.SetReferenceOpacity(_referenceOpacity.Value); UpdateStatus(); }; _referenceLocked.PropertyChanged += (_, e) => { if (_syncingReferencePanel || e.Property != ToggleButton.IsCheckedProperty) { return; } SetReferenceLockedFromPanel(); }; Content = BuildLayout(); _canvas.StatusChanged += (_, _) => UpdateStatus(); _canvas.Changed += (_, _) => { UpdateStatus(); SyncReferencePanel(); ApplyToolButtonState(); }; UpdateStatus(); SyncReferencePanel(); ApplyToolButtonState(); } private Control BuildLayout() { var root = new Grid { RowDefinitions = new RowDefinitions("76,*,34"), Background = AppSurfaceBrush() }; var topBar = AddTopBar(); Grid.SetRow(topBar, 0); root.Children.Add(topBar); var workArea = new Grid { ColumnDefinitions = new ColumnDefinitions("104,*,332"), Background = Brushes.Transparent }; Grid.SetRow(workArea, 1); root.Children.Add(workArea); var rail = BuildToolRail(); Grid.SetColumn(rail, 0); workArea.Children.Add(rail); var canvasHost = GlassPanel( _canvas, new Avalonia.Thickness(0, 10, 0, 14), new Avalonia.CornerRadius(12), GlassBackground); canvasHost.ClipToBounds = true; canvasHost.BorderThickness = new Avalonia.Thickness(1.4); Grid.SetColumn(canvasHost, 1); workArea.Children.Add(canvasHost); var inspector = BuildInspector(); Grid.SetColumn(inspector, 2); workArea.Children.Add(inspector); var statusBar = BuildStatusBar(); Grid.SetRow(statusBar, 2); root.Children.Add(statusBar); return root; } private Border GlassPanel(Control child, Avalonia.Thickness margin, Avalonia.CornerRadius cornerRadius, IBrush? background = null) { return new Border { Margin = margin, Background = background ?? GlassCardBrush(), BorderBrush = GlassStroke, BorderThickness = new Avalonia.Thickness(1), CornerRadius = cornerRadius, Child = child }; } private Control AddTopBar() { var dock = new DockPanel { Margin = new Avalonia.Thickness(18, 11), LastChildFill = true }; var brand = new StackPanel { Width = 205, Spacing = 0, VerticalAlignment = VerticalAlignment.Center }; brand.Children.Add(Text("easyTrace", TextPrimary, 21, FontWeight.SemiBold)); brand.Children.Add(Text("Calibrated 2D tracing CAD", TextMuted, 11)); DockPanel.SetDock(brand, Dock.Left); dock.Children.Add(brand); var actions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, VerticalAlignment = VerticalAlignment.Center }; DockPanel.SetDock(actions, Dock.Right); actions.Children.Add(CommandButton(UiIcon.New, "New", (_, _) => NewDocument())); actions.Children.Add(CommandButton(UiIcon.Open, "Open", async (_, _) => await OpenProject())); actions.Children.Add(CommandButton(UiIcon.Save, "Save", async (_, _) => await SaveProject())); actions.Children.Add(CommandButton(UiIcon.Image, "Reference", async (_, _) => await ImportReference())); actions.Children.Add(CommandButton(UiIcon.Calibrate, "Calibrate", async (_, _) => await CalibrateSheet())); actions.Children.Add(PrimaryButton(UiIcon.Export, "Export DXF", async (_, _) => await ExportDxf())); dock.Children.Add(actions); var center = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; center.Children.Add(CommandButton(UiIcon.Undo, "Undo", (_, _) => _canvas.Undo())); center.Children.Add(CommandButton(UiIcon.Redo, "Redo", (_, _) => _canvas.Redo())); center.Children.Add(CommandButton(UiIcon.Delete, "Delete", (_, _) => _canvas.DeleteSelected())); dock.Children.Add(center); return GlassPanel( dock, new Avalonia.Thickness(14, 12, 14, 6), new Avalonia.CornerRadius(14), ChromeBackground); } private Control BuildToolRail() { var stack = new StackPanel { Margin = new Avalonia.Thickness(12, 14), Spacing = 10 }; stack.Children.Add(Text("TOOLS", Brush("#94a3b8"), 10, FontWeight.SemiBold)); stack.Children.Add(ToolButton(UiIcon.Select, "Select", () => _canvas.SelectTool())); stack.Children.Add(ToolButton(UiIcon.Line, "Line", () => _canvas.SetTool(new LineTool()))); stack.Children.Add(ToolButton(UiIcon.Circle, "Circle 3P", () => _canvas.SetTool(new Circle3PointTool()))); stack.Children.Add(ToolButton(UiIcon.Arc, "Arc 3P", () => _canvas.SetTool(new Arc3PointTool()))); return GlassPanel( stack, new Avalonia.Thickness(14, 10, 12, 14), new Avalonia.CornerRadius(14), RailBackground); } private Control BuildInspector() { var panel = new StackPanel { Spacing = 14, Margin = new Avalonia.Thickness(18) }; panel.Children.Add(SectionHeader("Reference Image")); panel.Children.Add(_referenceName); panel.Children.Add(LabeledControl("Opacity", _referenceOpacity)); panel.Children.Add(_referenceLocked); panel.Children.Add(LabeledControl("X origin (mm)", _referenceX)); panel.Children.Add(LabeledControl("Y origin (mm)", _referenceY)); panel.Children.Add(LabeledControl("Scale (mm / image px)", _referenceScale)); panel.Children.Add(LabeledControl("Rotation (deg)", _referenceRotation)); panel.Children.Add(PrimaryButton(UiIcon.Apply, "Apply Transform", (_, _) => ApplyReferenceTransformFromPanel())); panel.Children.Add(CommandButton(UiIcon.Clear, "Clear Reference", (_, _) => _canvas.ClearReference())); panel.Children.Add(new Border { Height = 1, Background = StrokeBrush, Margin = new Avalonia.Thickness(0, 4) }); panel.Children.Add(SectionHeader("Document")); panel.Children.Add(DocumentMetric("Units", "mm")); return GlassPanel( panel, new Avalonia.Thickness(0, 10, 14, 14), new Avalonia.CornerRadius(14), PanelBackground); } private Control BuildStatusBar() { var dock = new DockPanel { Margin = new Avalonia.Thickness(14, 0), LastChildFill = true }; DockPanel.SetDock(_toolState, Dock.Right); dock.Children.Add(_toolState); dock.Children.Add(_status); return GlassPanel( dock, new Avalonia.Thickness(14, 0, 14, 8), new Avalonia.CornerRadius(10), ChromeBackground); } private Button ToolButton(string icon, string text, Action action) { var button = new Button { Content = ToolButtonContent(icon, text, selected: false), Background = RailButtonBackground, Foreground = TextPrimary, BorderBrush = StrokeBrush, BorderThickness = new Avalonia.Thickness(1), Padding = new Avalonia.Thickness(5, 9), HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalContentAlignment = HorizontalAlignment.Center, MinHeight = 66, CornerRadius = new Avalonia.CornerRadius(10) }; button.Click += (_, _) => { action(); ApplyToolButtonState(); }; _toolButtons[text] = (button, icon); return button; } private static Button CommandButton(string icon, string text, EventHandler handler) { var button = new Button { Content = CommandButtonContent(icon, text, TextPrimary), Background = Brush("#1f2937"), Foreground = TextPrimary, BorderBrush = StrokeBrush, BorderThickness = new Avalonia.Thickness(1), Padding = new Avalonia.Thickness(13, 8), MinHeight = 38, HorizontalContentAlignment = HorizontalAlignment.Center, CornerRadius = new Avalonia.CornerRadius(9) }; button.Click += handler; return button; } private static Button PrimaryButton(string icon, string text, EventHandler handler) { var button = CommandButton(icon, text, handler); button.Content = CommandButtonContent(icon, text, TextOnDark); button.Background = PrimaryAccentBrush(); button.Foreground = TextOnDark; button.BorderBrush = Brush("#1d4ed8"); return button; } private static Control LabeledControl(string label, Control input) { return new StackPanel { Spacing = 5, Children = { Text(label, TextSecondary, 11, FontWeight.SemiBold), input } }; } private static Control DocumentMetric(string label, string value) { var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto") }; var labelText = Text(label, TextSecondary, 12, FontWeight.SemiBold); var valueText = Text(value, TextPrimary, 12, FontWeight.SemiBold); Grid.SetColumn(valueText, 1); grid.Children.Add(labelText); grid.Children.Add(valueText); return new Border { Background = GlassCardBrush(), BorderBrush = Brush("#7fbfff44"), BorderThickness = new Avalonia.Thickness(1), CornerRadius = new Avalonia.CornerRadius(12), Padding = new Avalonia.Thickness(12, 9), Child = grid }; } private static StackPanel CommandButtonContent(string icon, string text, IBrush foreground) { return new StackPanel { Orientation = Orientation.Horizontal, Spacing = 7, VerticalAlignment = VerticalAlignment.Center, Children = { IconText(icon, foreground, 14), Text(text, foreground, 12, FontWeight.SemiBold) } }; } private static StackPanel ToolButtonContent(string icon, string text, bool selected) { var foreground = selected ? Accent : TextSecondary; return new StackPanel { Spacing = 5, HorizontalAlignment = HorizontalAlignment.Center, Children = { IconText(icon, foreground, 22), Text(text, foreground, 10, FontWeight.SemiBold) } }; } private static TextBlock IconText(string glyph, IBrush foreground, double size) => new() { Text = glyph, FontFamily = new FontFamily("Segoe MDL2 Assets"), Foreground = foreground, FontSize = size, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; private static TextBlock SectionHeader(string text) => Text(text, TextPrimary, 15, FontWeight.SemiBold); private static TextBlock Text(string text, IBrush foreground, double size, FontWeight weight = FontWeight.Normal) => new() { Text = text, Foreground = foreground, FontSize = size, FontWeight = weight, TextWrapping = TextWrapping.Wrap }; private void NewDocument() { _canvas.SetDocument(SketchDocument.CreateDefault()); UpdateStatus("New project"); } private async Task ImportReference() { var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { AllowMultiple = false, Title = "Import reference image", FileTypeFilter = new[] { ImageFileType } }); var path = files.FirstOrDefault()?.TryGetLocalPath(); if (path is null) { return; } _canvas.ImportReferenceImage(path); SyncReferencePanel(); } private async Task CalibrateSheet() { var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { AllowMultiple = false, Title = "Calibrate reference sheet photo", FileTypeFilter = new[] { ImageFileType } }); var path = files.FirstOrDefault()?.TryGetLocalPath(); if (path is null) { return; } UpdateStatus("Calibrating reference sheet..."); try { var outputDirectory = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "easyTrace", "calibrated-references"); var result = await Task.Run(() => _sheetCalibrator.Calibrate(path, outputDirectory)); if (!result.Success || result.CorrectedImagePath is null) { UpdateStatus(result.Message); return; } _canvas.SetReferenceImage(new ReferenceImage( result.CorrectedImagePath, 0.55, Locked: true, new ReferenceTransform( OriginX: 0.0, OriginY: 0.0, ScaleX: result.MmPerPixel, ScaleY: result.MmPerPixel, RotationDeg: 0.0))); SyncReferencePanel(); UpdateStatus(result.Message); } catch (Exception ex) { UpdateStatus($"Calibration failed: {ex.Message}"); } } private async Task OpenProject() { var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { AllowMultiple = false, Title = "Open easyTrace project", FileTypeFilter = new[] { ProjectFileType } }); var path = files.FirstOrDefault()?.TryGetLocalPath(); if (path is null) { return; } var json = await File.ReadAllTextAsync(path); _canvas.SetDocument(SketchDocumentSerializer.Deserialize(json)); UpdateStatus($"Opened {Path.GetFileName(path)}"); } private async Task SaveProject() { var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = "Save easyTrace project", SuggestedFileName = "drawing.easytrace.json", FileTypeChoices = new[] { ProjectFileType } }); var path = file?.TryGetLocalPath(); if (path is null) { return; } await File.WriteAllTextAsync(path, SketchDocumentSerializer.Serialize(_canvas.Document)); UpdateStatus($"Saved {Path.GetFileName(path)}"); } private async Task ExportDxf() { var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { Title = "Export DXF", SuggestedFileName = "drawing.dxf", FileTypeChoices = new[] { DxfFileType } }); var path = file?.TryGetLocalPath(); if (path is null) { return; } _dxfExporter.Export(_canvas.Document, path); UpdateStatus($"Exported {Path.GetFileName(path)}"); } private void UpdateStatus(string? prefix = null) { _status.Text = prefix ?? _canvas.StatusText; var reference = _canvas.Document.Reference is null ? "No reference" : "Reference loaded"; _toolState.Text = $"Tool: {_canvas.ActiveToolName} Entities: {_canvas.Document.Entities.Count} {reference}"; } private void SyncReferencePanel() { _syncingReferencePanel = true; var reference = _canvas.Document.Reference; var hasReference = reference is not null; _referenceName.Text = hasReference ? Path.GetFileName(reference!.ImagePath) : "No image imported"; _referenceOpacity.IsEnabled = hasReference; _referenceLocked.IsEnabled = hasReference; _referenceX.IsEnabled = hasReference && !reference!.Locked; _referenceY.IsEnabled = hasReference && !reference!.Locked; _referenceScale.IsEnabled = hasReference && !reference!.Locked; _referenceRotation.IsEnabled = hasReference && !reference!.Locked; if (reference is not null) { _referenceOpacity.Value = reference.Opacity; _referenceLocked.IsChecked = reference.Locked; _referenceX.Value = (decimal)reference.Transform.OriginX; _referenceY.Value = (decimal)reference.Transform.OriginY; _referenceScale.Value = (decimal)reference.Transform.ScaleX; _referenceRotation.Value = (decimal)reference.Transform.RotationDeg; } else { _referenceLocked.IsChecked = false; _referenceX.Value = 0m; _referenceY.Value = 0m; _referenceScale.Value = 1m; _referenceRotation.Value = 0m; } _syncingReferencePanel = false; } private void SetReferenceLockedFromPanel() { if (_syncingReferencePanel) { return; } _canvas.SetReferenceLocked(_referenceLocked.IsChecked == true); SyncReferencePanel(); } private void ApplyReferenceTransformFromPanel() { if (_canvas.Document.Reference is null || _canvas.Document.Reference.Locked) { return; } var scale = ToDouble(_referenceScale.Value, 1.0); _canvas.SetReferenceTransform(new ReferenceTransform( ToDouble(_referenceX.Value, 0.0), ToDouble(_referenceY.Value, 0.0), scale, scale, ToDouble(_referenceRotation.Value, 0.0))); SyncReferencePanel(); } private void ApplyToolButtonState() { foreach (var (name, entry) in _toolButtons) { var selected = string.Equals(name, _canvas.ActiveToolName, StringComparison.Ordinal); entry.Button.Background = selected ? RailButtonSelectedBackground : RailButtonBackground; entry.Button.BorderBrush = selected ? Brush("#93c5fd") : StrokeBrush; entry.Button.Content = ToolButtonContent(entry.Icon, name, selected); } } private static double ToDouble(decimal? value, double fallback) => value is null ? fallback : (double)value.Value; private static NumericUpDown NumberInput(double increment, double minimum = -100000.0, double maximum = 100000.0) => new() { Minimum = (decimal)minimum, Maximum = (decimal)maximum, Increment = (decimal)increment, FormatString = "0.####", HorizontalAlignment = HorizontalAlignment.Stretch, Background = Brush("#0f172a"), Foreground = TextPrimary, BorderBrush = StrokeBrush, BorderThickness = new Avalonia.Thickness(1), CornerRadius = new Avalonia.CornerRadius(7) }; private static IBrush AppSurfaceBrush() => new LinearGradientBrush { StartPoint = new Avalonia.RelativePoint(0.0, 0.0, Avalonia.RelativeUnit.Relative), EndPoint = new Avalonia.RelativePoint(1.0, 1.0, Avalonia.RelativeUnit.Relative), GradientStops = { new GradientStop(Color.Parse("#0b1020"), 0.0), new GradientStop(Color.Parse("#111827"), 0.52), new GradientStop(Color.Parse("#0f172a"), 1.0) } }; private static IBrush PrimaryAccentBrush() => new LinearGradientBrush { StartPoint = new Avalonia.RelativePoint(0.0, 0.0, Avalonia.RelativeUnit.Relative), EndPoint = new Avalonia.RelativePoint(1.0, 1.0, Avalonia.RelativeUnit.Relative), GradientStops = { new GradientStop(Color.Parse("#60a5fa"), 0.0), new GradientStop(Color.Parse("#3b82f6"), 0.5), new GradientStop(Color.Parse("#1d4ed8"), 1.0) } }; private static IBrush GlassCardBrush() => new LinearGradientBrush { StartPoint = new Avalonia.RelativePoint(0.0, 0.0, Avalonia.RelativeUnit.Relative), EndPoint = new Avalonia.RelativePoint(1.0, 1.0, Avalonia.RelativeUnit.Relative), GradientStops = { new GradientStop(Color.Parse("#1e293b"), 0.0), new GradientStop(Color.Parse("#111827"), 0.55), new GradientStop(Color.Parse("#0f172a"), 1.0) } }; private static SolidColorBrush Brush(string color) => new(Color.Parse(color)); private static class UiIcon { public const string New = "\uE710"; public const string Open = "\uE838"; public const string Save = "\uE74E"; public const string Image = "\uE91B"; public const string Calibrate = "\uE9F9"; public const string Export = "\uE898"; public const string Undo = "\uE7A7"; public const string Redo = "\uE7A6"; public const string Delete = "\uE74D"; public const string Select = "\uE7C9"; public const string Line = "\uE738"; public const string Circle = "\uEA3A"; public const string Arc = "\uE7C1"; public const string Apply = "\uE73E"; public const string Clear = "\uE711"; } private static FilePickerFileType ProjectFileType { get; } = new("easyTrace project") { Patterns = new[] { "*.easytrace.json", "*.tracecad.json", "*.json" } }; private static FilePickerFileType DxfFileType { get; } = new("DXF") { Patterns = new[] { "*.dxf" } }; private static FilePickerFileType ImageFileType { get; } = new("Images") { Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.bmp", "*.tif", "*.tiff", "*.webp" } }; protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); if (!e.KeyModifiers.HasFlag(KeyModifiers.Control)) { return; } if (e.Key == Key.S) { _ = SaveProject(); e.Handled = true; } } }