740 lines
26 KiB
C#
740 lines
26 KiB
C#
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<string, (Button Button, string Icon)> _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<RoutedEventArgs> 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<RoutedEventArgs> 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;
|
|
}
|
|
}
|
|
}
|