i cannot for the life of me fix the dang camera shooting off to god knows where

This commit is contained in:
Matěj Kubíček
2026-04-21 21:19:44 +02:00
parent 8a57680b0e
commit 70ca51d9ef
11 changed files with 395 additions and 134 deletions
+3
View File
@@ -3,7 +3,10 @@
<component name="AvaloniaProject"> <component name="AvaloniaProject">
<option name="projectPerEditor"> <option name="projectPerEditor">
<map> <map>
<entry key="NetworkDiagram/ExportOptionsWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagram/MainWindow.axaml" value="NetworkDiagram/NetworkDiagram.csproj" />
<entry key="NetworkDiagramAvalonia/App.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" /> <entry key="NetworkDiagramAvalonia/App.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
<entry key="NetworkDiagramAvalonia/Views/MainWindow.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
</map> </map>
</option> </option>
</component> </component>
-1
View File
@@ -1,4 +1,3 @@
<Solution> <Solution>
<Project Path="NetworkDiagram/NetworkDiagram.csproj" /> <Project Path="NetworkDiagram/NetworkDiagram.csproj" />
<Project Path="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
</Solution> </Solution>
-3
View File
@@ -1,6 +1,3 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using NetworkDiagram.Models; using NetworkDiagram.Models;
+14
View File
@@ -0,0 +1,14 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NetworkDiagram.ExportOptionsWindow"
Title="{DynamicResource ExportOptionsTitle}" Width="400" Height="180"
WindowStartupLocation="CenterOwner" CanResize="False"
Background="{DynamicResource BgWhite}">
<StackPanel Margin="20" Spacing="20">
<TextBlock Text="{DynamicResource ExportWhiteBgMsg}" TextWrapping="Wrap" HorizontalAlignment="Center" TextAlignment="Center"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="20">
<Button Content="Yes (White)" Click="White_Click" Width="100" Classes="ModernButtonStyle"/>
<Button Content="No (Transparent)" Click="Transparent_Click" Width="140" Classes="SecondaryButtonStyle"/>
</StackPanel>
</StackPanel>
</Window>
@@ -0,0 +1,23 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace NetworkDiagram
{
public partial class ExportOptionsWindow : Window
{
public ExportOptionsWindow()
{
InitializeComponent();
}
private void White_Click(object? sender, RoutedEventArgs e)
{
Close(true);
}
private void Transparent_Click(object? sender, RoutedEventArgs e)
{
Close(false);
}
}
}
+4 -4
View File
@@ -55,7 +55,7 @@
<DockPanel Margin="10,15"> <DockPanel Margin="10,15">
<TextBlock Text="{DynamicResource DevicesHeader}" DockPanel.Dock="Top" FontSize="18" FontWeight="Bold" Margin="10,0,10,15" Foreground="{DynamicResource PrimaryBlue}"/> <TextBlock Text="{DynamicResource DevicesHeader}" DockPanel.Dock="Top" FontSize="18" FontWeight="Bold" Margin="10,0,10,15" Foreground="{DynamicResource PrimaryBlue}"/>
<ListBox x:Name="ToolboxList" Classes="SidebarListBoxStyle" <ListBox x:Name="ToolboxList" Classes="SidebarListBoxStyle"
PointerPressed="Toolbox_PointerPressed"> PointerMoved="Toolbox_PointerMoved">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="models:DeviceTemplate"> <DataTemplate x:DataType="models:DeviceTemplate">
<Border Background="White" CornerRadius="8" Padding="12" BorderBrush="{DynamicResource GridBlue}" BorderThickness="1"> <Border Background="White" CornerRadius="8" Padding="12" BorderBrush="{DynamicResource GridBlue}" BorderThickness="1">
@@ -72,24 +72,24 @@
<!-- Canvas Area --> <!-- Canvas Area -->
<Panel Grid.Row="1" Grid.Column="1" x:Name="CanvasViewport" ClipToBounds="True" Background="Transparent" <Panel Grid.Row="1" Grid.Column="1" x:Name="CanvasViewport" ClipToBounds="True" Background="Transparent"
DragDrop.AllowDrop="True"
PointerPressed="Viewport_PointerPressed" PointerPressed="Viewport_PointerPressed"
PointerMoved="Viewport_PointerMoved" PointerMoved="Viewport_PointerMoved"
PointerReleased="Viewport_PointerReleased" PointerReleased="Viewport_PointerReleased"
PointerWheelChanged="Viewport_PointerWheelChanged"> PointerWheelChanged="Viewport_PointerWheelChanged">
<Canvas x:Name="DiagramCanvas" Width="40000" Height="40000" <Canvas x:Name="DiagramCanvas" Width="40000" Height="40000"
DragDrop.AllowDrop="True"
PointerPressed="DiagramCanvas_PointerPressed" PointerPressed="DiagramCanvas_PointerPressed"
PointerMoved="DiagramCanvas_PointerMoved" PointerMoved="DiagramCanvas_PointerMoved"
PointerReleased="DiagramCanvas_PointerReleased"> PointerReleased="DiagramCanvas_PointerReleased">
<Canvas.RenderTransform> <Canvas.RenderTransform>
<TransformGroup> <TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/> <ScaleTransform ScaleX="1" ScaleY="1"/>
<TranslateTransform X="-20000" Y="-20000"/> <TranslateTransform X="0" Y="0"/>
</TransformGroup> </TransformGroup>
</Canvas.RenderTransform> </Canvas.RenderTransform>
<Canvas.Background> <Canvas.Background>
<DrawingBrush TileMode="Tile" DestinationRect="0,0,40,40"> <DrawingBrush TileMode="Tile" SourceRect="0,0,40,40" DestinationRect="0,0,40,40">
<DrawingBrush.Drawing> <DrawingBrush.Drawing>
<GeometryDrawing> <GeometryDrawing>
<GeometryDrawing.Geometry> <GeometryDrawing.Geometry>
+319 -114
View File
@@ -16,6 +16,7 @@ using Avalonia.Markup.Xaml.Styling;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using NetworkDiagram.Models; using NetworkDiagram.Models;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@@ -26,7 +27,8 @@ namespace NetworkDiagram
{ {
private List<DeviceTemplate> _deviceTemplates = []; private List<DeviceTemplate> _deviceTemplates = [];
private Diagram _currentDiagram = new(); private Diagram _currentDiagram = new();
private bool _langComboReady;
// Selection & Dragging // Selection & Dragging
private readonly HashSet<Control> _selectedElements = []; private readonly HashSet<Control> _selectedElements = [];
private Point _dragStartPoint; private Point _dragStartPoint;
@@ -50,6 +52,8 @@ namespace NetworkDiagram
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
LangCombo.SelectedIndex = 0;
_langComboReady = true;
LoadTemplates(); LoadTemplates();
this.Opened += (s, e) => { this.Opened += (s, e) => {
if (DiagramCanvas.RenderTransform is TransformGroup group) if (DiagramCanvas.RenderTransform is TransformGroup group)
@@ -59,29 +63,41 @@ namespace NetworkDiagram
} }
CenterView(); CenterView();
}; };
AddHandler(DragDrop.DragOverEvent, DiagramCanvas_DragOver); AddHandler(DragDrop.DragOverEvent, Viewport_DragOver);
AddHandler(DragDrop.DropEvent, DiagramCanvas_Drop); AddHandler(DragDrop.DropEvent, Viewport_Drop);
// Use Tunneling for drag start to avoid ListBoxItem selection interference
ToolboxList.AddHandler(PointerPressedEvent, Toolbox_PointerPressed, RoutingStrategies.Tunnel);
} }
// The logical center of the 40000x40000 canvas
private const double CanvasCenterX = 20000;
private const double CanvasCenterY = 20000;
private void CenterView() private void CenterView()
{ {
if (CanvasTranslate == null || CanvasScale == null) return; if (CanvasTranslate == null || CanvasScale == null) return;
CanvasTranslate.X = -20000 + (CanvasViewport.Bounds.Width / 2); double vw = CanvasViewport.Bounds.Width > 0 ? CanvasViewport.Bounds.Width : 1000;
CanvasTranslate.Y = -20000 + (CanvasViewport.Bounds.Height / 2); double vh = CanvasViewport.Bounds.Height > 0 ? CanvasViewport.Bounds.Height : 700;
CanvasScale.ScaleX = 1.0; CanvasScale.ScaleX = 1.0;
CanvasScale.ScaleY = 1.0; CanvasScale.ScaleY = 1.0;
// Place canvas center (20000,20000) in viewport center
CanvasTranslate.X = vw / 2 - CanvasCenterX;
CanvasTranslate.Y = vh / 2 - CanvasCenterY;
} }
#region Panning and Zooming #region Panning and Zooming
private void Viewport_PointerPressed(object? sender, PointerPressedEventArgs e) private void Viewport_PointerPressed(object? sender, PointerPressedEventArgs e)
{ {
var properties = e.GetCurrentPoint(CanvasViewport).Properties; var properties = e.GetCurrentPoint(CanvasViewport).Properties;
if (properties.IsMiddleButtonPressed) if (properties.IsMiddleButtonPressed || properties.IsRightButtonPressed)
{ {
_isPanning = true; _isPanning = true;
_lastPanPoint = e.GetPosition(CanvasViewport); _lastPanPoint = e.GetPosition(CanvasViewport);
e.Pointer.Capture(CanvasViewport); e.Pointer.Capture(CanvasViewport);
e.Handled = true;
} }
} }
@@ -102,7 +118,8 @@ namespace NetworkDiagram
private void Viewport_PointerReleased(object? sender, PointerReleasedEventArgs e) private void Viewport_PointerReleased(object? sender, PointerReleasedEventArgs e)
{ {
if (e.InitialPressMouseButton == MouseButton.Middle) var props = e.GetCurrentPoint(CanvasViewport).Properties;
if (!props.IsMiddleButtonPressed && !props.IsRightButtonPressed)
{ {
_isPanning = false; _isPanning = false;
e.Pointer.Capture(null); e.Pointer.Capture(null);
@@ -113,9 +130,9 @@ namespace NetworkDiagram
{ {
if (CanvasScale == null || CanvasTranslate == null) return; if (CanvasScale == null || CanvasTranslate == null) return;
double zoom = e.Delta.Y > 0 ? 1.1 : 0.9; double zoom = e.Delta.Y > 0 ? 1.1 : 0.9;
double newScale = CanvasScale.ScaleX * zoom; double newScale = CanvasScale.ScaleX * zoom;
if (newScale < 0.1 || newScale > 5) return; if (newScale < 0.05 || newScale > 10) return;
Point mousePos = e.GetPosition(DiagramCanvas); Point mousePos = e.GetPosition(DiagramCanvas);
@@ -133,10 +150,8 @@ namespace NetworkDiagram
try try
{ {
if (!File.Exists("devices.json")) return; if (!File.Exists("devices.json")) return;
var json = File.ReadAllText("devices.json"); var json = File.ReadAllText("devices.json");
_deviceTemplates = JsonSerializer.Deserialize<List<DeviceTemplate>>(json) ?? []; _deviceTemplates = JsonSerializer.Deserialize<List<DeviceTemplate>>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? [];
ToolboxList.ItemsSource = _deviceTemplates; ToolboxList.ItemsSource = _deviceTemplates;
} }
catch (Exception ex) { Console.WriteLine($"Error loading templates: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"Error loading templates: {ex.Message}"); }
@@ -144,6 +159,7 @@ namespace NetworkDiagram
private void LangCombo_SelectionChanged(object? sender, SelectionChangedEventArgs e) private void LangCombo_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{ {
if (!_langComboReady) return;
if (LangCombo.SelectedItem is ComboBoxItem { Tag: string lang }) if (LangCombo.SelectedItem is ComboBoxItem { Tag: string lang })
{ {
var app = Application.Current; var app = Application.Current;
@@ -152,34 +168,72 @@ namespace NetworkDiagram
var mergedDicts = app.Resources.MergedDictionaries; var mergedDicts = app.Resources.MergedDictionaries;
var oldDict = mergedDicts.OfType<ResourceInclude>().FirstOrDefault(ri => ri.Source?.OriginalString.Contains("Localization/Strings.") == true); var oldDict = mergedDicts.OfType<ResourceInclude>().FirstOrDefault(ri => ri.Source?.OriginalString.Contains("Localization/Strings.") == true);
if (oldDict != null) mergedDicts.Remove(oldDict); if (oldDict != null) mergedDicts.Remove(oldDict);
mergedDicts.Add(new ResourceInclude(new Uri("avares://NetworkDiagram/App.axaml")) mergedDicts.Add(new ResourceInclude(new Uri("avares://NetworkDiagram/App.axaml"))
{ {
Source = new Uri($"avares://NetworkDiagram/Localization/Strings.{lang}.axaml") Source = new Uri($"avares://NetworkDiagram/Localization/Strings.{lang}.axaml")
}); });
} }
} }
#region Drag and Drop (Toolbox to Canvas) #region Drag and Drop (Toolbox to Canvas)
private async void Toolbox_PointerPressed(object? sender, PointerPressedEventArgs e) private Point _toolboxDragStart;
private DeviceTemplate? _toolboxPendingTemplate;
private void Toolbox_PointerPressed(object? sender, PointerPressedEventArgs e)
{ {
var item = (e.Source as Control)?.FindAncestorOfType<ListBoxItem>(); var item = (e.Source as Control)?.FindAncestorOfType<ListBoxItem>();
if (item?.Content is DeviceTemplate template) if (item?.Content is DeviceTemplate template)
{ {
var data = new DataObject(); _toolboxDragStart = e.GetPosition(null);
data.Set("DeviceTemplate", template); _toolboxPendingTemplate = template;
await DragDrop.DoDragDrop(e, data, DragDropEffects.Copy); }
}
private async void Toolbox_PointerMoved(object? sender, PointerEventArgs e)
{
if (_toolboxPendingTemplate != null && e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
{
var pos = e.GetPosition(null);
var diff = _toolboxDragStart - pos;
if (Math.Abs(diff.X) > 5 || Math.Abs(diff.Y) > 5)
{
var template = _toolboxPendingTemplate;
_toolboxPendingTemplate = null;
var data = new DataObject();
data.Set("DeviceTemplate", template);
await DragDrop.DoDragDrop(e, data, DragDropEffects.Copy);
}
}
}
private void Viewport_DragOver(object? sender, DragEventArgs e)
{
if (e.Data.Contains("DeviceTemplate"))
e.DragEffects = DragDropEffects.Copy;
else
e.DragEffects = DragDropEffects.None;
}
private void Viewport_Drop(object? sender, DragEventArgs e)
{
if (e.Data.Get("DeviceTemplate") is DeviceTemplate template)
{
var dropPoint = e.GetPosition(DiagramCanvas);
AddDeviceToCanvas(template, dropPoint.X, dropPoint.Y);
} }
} }
private void DiagramCanvas_PointerPressed(object? sender, PointerPressedEventArgs e) private void DiagramCanvas_PointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (e.Source == DiagramCanvas) var props = e.GetCurrentPoint(DiagramCanvas).Properties;
if (e.Source == DiagramCanvas && props.IsLeftButtonPressed)
{ {
ClearSelection(); ClearSelection();
_isSelectingArea = true; _isSelectingArea = true;
_selectionStartPoint = e.GetPosition(DiagramCanvas); _selectionStartPoint = e.GetPosition(DiagramCanvas);
Canvas.SetLeft(SelectionRect, _selectionStartPoint.X); Canvas.SetLeft(SelectionRect, _selectionStartPoint.X);
Canvas.SetTop(SelectionRect, _selectionStartPoint.Y); Canvas.SetTop(SelectionRect, _selectionStartPoint.Y);
SelectionRect.Width = 0; SelectionRect.Width = 0;
@@ -242,26 +296,19 @@ namespace NetworkDiagram
StopDragging(); StopDragging();
e.Pointer.Capture(null); e.Pointer.Capture(null);
} }
// Implementation of DragDrop for Canvas
private void DiagramCanvas_DragOver(object? sender, DragEventArgs e)
{
if (e.Data.Contains("DeviceTemplate"))
e.DragEffects = DragDropEffects.Copy;
else
e.DragEffects = DragDropEffects.None;
}
private void DiagramCanvas_Drop(object? sender, DragEventArgs e)
{
if (e.Data.Get("DeviceTemplate") is DeviceTemplate template)
{
var dropPoint = e.GetPosition(DiagramCanvas);
AddDeviceToCanvas(template, dropPoint.X, dropPoint.Y);
}
}
#endregion #endregion
private Point GetVisibleCanvasCenter()
{
if (CanvasTranslate == null || CanvasScale == null) return new Point(CanvasCenterX, CanvasCenterY);
double vw = CanvasViewport.Bounds.Width > 0 ? CanvasViewport.Bounds.Width : 1000;
double vh = CanvasViewport.Bounds.Height > 0 ? CanvasViewport.Bounds.Height : 700;
return new Point(
(vw / 2 - CanvasTranslate.X) / CanvasScale.ScaleX,
(vh / 2 - CanvasTranslate.Y) / CanvasScale.ScaleY
);
}
private void AddDeviceToCanvas(DeviceTemplate template, double x, double y) private void AddDeviceToCanvas(DeviceTemplate template, double x, double y)
{ {
var placed = new PlacedDevice { Name = template.Name, TemplateName = template.Name, IconPath = template.IconPath, X = x, Y = y }; var placed = new PlacedDevice { Name = template.Name, TemplateName = template.Name, IconPath = template.IconPath, X = x, Y = y };
@@ -274,36 +321,40 @@ namespace NetworkDiagram
var container = new Border var container = new Border
{ {
CornerRadius = new CornerRadius(8), CornerRadius = new CornerRadius(8),
Padding = new Thickness(8), Padding = new Thickness(12),
Background = Brushes.Transparent, Background = Brushes.White,
BorderBrush = Brushes.Transparent, BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!,
BorderThickness = new Thickness(2), BorderThickness = new Thickness(1),
MaxWidth = 160, MinWidth = 100,
Tag = device MinHeight = 80,
MaxWidth = 180,
Tag = device,
ZIndex = 10,
DataContext = device
}; };
var stack = new StackPanel(); var stack = new StackPanel { Spacing = 4 };
var image = new Image { MaxWidth = 64, MaxHeight = 64, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center }; var image = new Image { Width = 64, Height = 64, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
image.Bind(Image.SourceProperty, new Avalonia.Data.Binding(nameof(device.IconPath)) { Converter = ImageConverter.Instance }); image.Bind(Image.SourceProperty, new Avalonia.Data.Binding("IconPath") { Converter = ImageConverter.Instance });
stack.Children.Add(image); stack.Children.Add(image);
var primaryBlue = Application.Current!.FindResource("PrimaryBlue"); var primaryBlue = Application.Current!.FindResource("PrimaryBlue");
var nameText = new TextBlock { FontWeight = FontWeight.Bold, FontSize = 12, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, TextAlignment = TextAlignment.Center, Margin = new Thickness(0,4,0,0) }; var nameText = new TextBlock { FontWeight = FontWeight.Bold, FontSize = 12, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, TextAlignment = TextAlignment.Center };
if (primaryBlue is IBrush brush) nameText.Foreground = brush; if (primaryBlue is IBrush brush) nameText.Foreground = brush;
nameText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding(nameof(device.Name))); nameText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("Name"));
stack.Children.Add(nameText); stack.Children.Add(nameText);
var ips = new ItemsControl(); var ips = new ItemsControl();
ips.Bind(ItemsControl.ItemsSourceProperty, new Avalonia.Data.Binding(nameof(device.IpAddresses))); ips.Bind(ItemsControl.ItemsSourceProperty, new Avalonia.Data.Binding("IpAddresses"));
var accentBlue = Application.Current!.FindResource("AccentBlue"); var accentBlue = Application.Current!.FindResource("AccentBlue");
ips.ItemTemplate = new FuncDataTemplate<string>((val, _) => { ips.ItemTemplate = new FuncDataTemplate<string>((val, _) => {
var tb = new TextBlock { Text = val, FontSize = 10, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, Opacity = 0.8 }; var tb = new TextBlock { Text = val, FontSize = 10, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextWrapping = TextWrapping.Wrap, Opacity = 0.8 };
if (accentBlue is IBrush b) tb.Foreground = b; if (accentBlue is IBrush b) tb.Foreground = b;
return tb; return tb;
}); });
stack.Children.Add(ips); stack.Children.Add(ips);
container.Child = stack; container.Child = stack;
@@ -370,7 +421,8 @@ namespace NetworkDiagram
{ {
var accentBlue = Application.Current!.FindResource("AccentBlue"); var accentBlue = Application.Current!.FindResource("AccentBlue");
if (accentBlue is IBrush brush) b.BorderBrush = brush; if (accentBlue is IBrush brush) b.BorderBrush = brush;
b.Background = new SolidColorBrush(Color.FromArgb(30, 0, 120, 215)); b.Background = new SolidColorBrush(Color.FromArgb(50, 0, 120, 215));
b.BorderThickness = new Thickness(2);
} }
else if (element is Line l) else if (element is Line l)
{ {
@@ -384,8 +436,9 @@ namespace NetworkDiagram
_selectedElements.Remove(element); _selectedElements.Remove(element);
if (element is Border b) if (element is Border b)
{ {
b.BorderBrush = Brushes.Transparent; b.BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!;
b.Background = Brushes.Transparent; b.Background = Brushes.White;
b.BorderThickness = new Thickness(1);
} }
else if (element is Line l) else if (element is Line l)
{ {
@@ -410,7 +463,7 @@ namespace NetworkDiagram
{ {
if (_firstDeviceForConnection != device) if (_firstDeviceForConnection != device)
{ {
var conn = new Connection { StartDevice = _firstDeviceForConnection, EndDevice = device, Type = _activeTool.Value }; var conn = new Connection { StartDevice = _firstDeviceForConnection, EndDevice = device, Type = _activeTool!.Value };
_currentDiagram.Connections.Add(conn); _currentDiagram.Connections.Add(conn);
RenderConnection(conn); RenderConnection(conn);
} }
@@ -438,15 +491,35 @@ namespace NetworkDiagram
void UpdatePos() void UpdatePos()
{ {
if (!_deviceElements.TryGetValue(conn.StartDevice, out var s) || !_deviceElements.TryGetValue(conn.EndDevice, out var e)) return; if (!_deviceElements.TryGetValue(conn.StartDevice, out var startEl) || !_deviceElements.TryGetValue(conn.EndDevice, out var endEl)) return;
line.StartPoint = new Point(conn.StartDevice.X + s.Bounds.Width / 2, conn.StartDevice.Y + s.Bounds.Height / 2); double sw = startEl.Bounds.Width > 0 ? startEl.Bounds.Width : 120;
line.EndPoint = new Point(conn.EndDevice.X + e.Bounds.Width / 2, conn.EndDevice.Y + e.Bounds.Height / 2); double sh = startEl.Bounds.Height > 0 ? startEl.Bounds.Height : 100;
double ew = endEl.Bounds.Width > 0 ? endEl.Bounds.Width : 120;
double eh = endEl.Bounds.Height > 0 ? endEl.Bounds.Height : 100;
line.StartPoint = new Point(conn.StartDevice.X + sw / 2, conn.StartDevice.Y + sh / 2);
line.EndPoint = new Point(conn.EndDevice.X + ew / 2, conn.EndDevice.Y + eh / 2);
} }
UpdatePos(); UpdatePos();
conn.StartDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdatePos(); }; conn.StartDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdatePos(); };
conn.EndDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdatePos(); }; conn.EndDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdatePos(); };
// Re-calculate line endpoints once device bounds are known after first layout pass
// Unsubscribe immediately after first call to avoid repeated triggers
if (_deviceElements.TryGetValue(conn.StartDevice, out var startElem))
{
EventHandler? handler = null;
handler = (s, e) => { startElem.LayoutUpdated -= handler; UpdatePos(); };
startElem.LayoutUpdated += handler;
}
if (_deviceElements.TryGetValue(conn.EndDevice, out var endElem) && endElem != startElem)
{
EventHandler? handler = null;
handler = (s, e) => { endElem.LayoutUpdated -= handler; UpdatePos(); };
endElem.LayoutUpdated += handler;
}
line.PointerPressed += (s, e) => { line.PointerPressed += (s, e) => {
if (!e.KeyModifiers.HasFlag(KeyModifiers.Control)) ClearSelection(); if (!e.KeyModifiers.HasFlag(KeyModifiers.Control)) ClearSelection();
SelectElement(line); SelectElement(line);
@@ -455,24 +528,24 @@ namespace NetworkDiagram
_connectionLines[conn] = line; _connectionLines[conn] = line;
DiagramCanvas.Children.Insert(0, line); DiagramCanvas.Children.Insert(0, line);
// Initial position update after layout
LayoutUpdated += (s, e) => UpdatePos();
} }
private async void EditDevice(PlacedDevice device) private async void EditDevice(PlacedDevice device)
{ {
var dialog = new EditDeviceWindow(device); var dialog = new EditDeviceWindow(device);
await dialog.ShowDialog(this); await dialog.ShowDialog(this);
if (_deviceElements.TryGetValue(device, out var elem)) if (_deviceElements.TryGetValue(device, out var elem))
{ {
// Refresh layout and connections
var attached = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList(); var attached = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
foreach(var c in attached) if (_connectionLines.TryGetValue(c, out var l)) foreach(var c in attached) if (_connectionLines.TryGetValue(c, out var l))
{ {
l.StartPoint = new Point(c.StartDevice.X + _deviceElements[c.StartDevice].Bounds.Width / 2, c.StartDevice.Y + _deviceElements[c.StartDevice].Bounds.Height / 2); double sw = _deviceElements[c.StartDevice].Bounds.Width > 0 ? _deviceElements[c.StartDevice].Bounds.Width : 120;
l.EndPoint = new Point(c.EndDevice.X + _deviceElements[c.EndDevice].Bounds.Width / 2, c.EndDevice.Y + _deviceElements[c.EndDevice].Bounds.Height / 2); double sh = _deviceElements[c.StartDevice].Bounds.Height > 0 ? _deviceElements[c.StartDevice].Bounds.Height : 100;
double ew = _deviceElements[c.EndDevice].Bounds.Width > 0 ? _deviceElements[c.EndDevice].Bounds.Width : 120;
double eh = _deviceElements[c.EndDevice].Bounds.Height > 0 ? _deviceElements[c.EndDevice].Bounds.Height : 100;
l.StartPoint = new Point(c.StartDevice.X + sw / 2, c.StartDevice.Y + sh / 2);
l.EndPoint = new Point(c.EndDevice.X + ew / 2, c.EndDevice.Y + eh / 2);
} }
} }
} }
@@ -509,6 +582,11 @@ namespace NetworkDiagram
{ {
ResetConnectionTool(); StopDragging(); ClearSelection(); ResetConnectionTool(); StopDragging(); ClearSelection();
} }
else if (e.Key == Key.Home)
{
if (_currentDiagram.Devices.Count > 0) CenterOnContent();
else CenterView();
}
} }
#region Toolbar Events #region Toolbar Events
@@ -544,12 +622,41 @@ namespace NetworkDiagram
Type = c.Type Type = c.Type
}).ToList() }).ToList()
}; };
using var stream = await file.OpenWriteAsync(); using var stream = await file.OpenWriteAsync();
await JsonSerializer.SerializeAsync(stream, model, new JsonSerializerOptions { WriteIndented = true }); await JsonSerializer.SerializeAsync(stream, model, new JsonSerializerOptions { WriteIndented = true });
} }
} }
private void CenterOnContent()
{
if (CanvasTranslate == null || CanvasScale == null || _currentDiagram.Devices.Count == 0) return;
var devices = _currentDiagram.Devices;
double minX = devices.Min(d => d.X);
double minY = devices.Min(d => d.Y);
double maxX = devices.Max(d => d.X) + 160;
double maxY = devices.Max(d => d.Y) + 120;
double contentWidth = maxX - minX;
double contentHeight = maxY - minY;
double contentCenterX = (minX + maxX) / 2.0;
double contentCenterY = (minY + maxY) / 2.0;
double vw = CanvasViewport.Bounds.Width > 0 ? CanvasViewport.Bounds.Width : (this.Bounds.Width > 240 ? this.Bounds.Width - 240 : 860);
double vh = CanvasViewport.Bounds.Height > 0 ? CanvasViewport.Bounds.Height : (this.Bounds.Height > 60 ? this.Bounds.Height - 60 : 600);
// Zoom-to-fit: scale content to fill 85% of viewport, capped between 0.05 and 2.0
double scaleToFit = Math.Min((vw * 0.85) / contentWidth, (vh * 0.85) / contentHeight);
double newScale = Math.Clamp(scaleToFit, 0.05, 2.0);
CanvasScale.ScaleX = newScale;
CanvasScale.ScaleY = newScale;
CanvasTranslate.X = vw / 2.0 - contentCenterX * newScale;
CanvasTranslate.Y = vh / 2.0 - contentCenterY * newScale;
}
private async void LoadDiagram_Click(object? sender, RoutedEventArgs e) private async void LoadDiagram_Click(object? sender, RoutedEventArgs e)
{ {
var topLevel = GetTopLevel(this); var topLevel = GetTopLevel(this);
@@ -563,25 +670,55 @@ namespace NetworkDiagram
if (files.Count > 0) if (files.Count > 0)
{ {
using var stream = await files[0].OpenReadAsync(); try {
var model = await JsonSerializer.DeserializeAsync<DiagramSaveModel>(stream); using var stream = await files[0].OpenReadAsync();
if (model == null) return; var model = await JsonSerializer.DeserializeAsync<DiagramSaveModel>(stream, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
NewDiagram_Click(null, new RoutedEventArgs()); if (model == null || model.Devices == null) return;
_currentDiagram = new Diagram { Devices = model.Devices };
foreach (var device in _currentDiagram.Devices) // Clear canvas without calling CenterView (we'll center on content after load)
{ _currentDiagram = new Diagram();
var template = _deviceTemplates.FirstOrDefault(t => t.Name == device.TemplateName); DiagramCanvas.Children.Clear();
if (template != null) device.IconPath = template.IconPath; DiagramCanvas.Children.Add(SelectionRect);
RenderDevice(device); _connectionLines.Clear();
} _deviceElements.Clear();
ClearSelection();
foreach (var cModel in model.Connections) // Load devices exactly as saved — no normalization
{ _currentDiagram = new Diagram { Devices = model.Devices };
var conn = new Connection { StartDevice = _currentDiagram.Devices[cModel.StartIndex], EndDevice = _currentDiagram.Devices[cModel.EndIndex], Type = cModel.Type };
_currentDiagram.Connections.Add(conn); foreach (var device in _currentDiagram.Devices)
RenderConnection(conn); {
var template = _deviceTemplates.FirstOrDefault(t => t.Name == device.TemplateName);
if (template != null) device.IconPath = template.IconPath;
else if (device.TemplateName == "Note") device.IconPath = "";
RenderDevice(device);
}
if (model.Connections != null)
{
foreach (var cModel in model.Connections)
{
if (cModel.StartIndex >= 0 && cModel.StartIndex < _currentDiagram.Devices.Count &&
cModel.EndIndex >= 0 && cModel.EndIndex < _currentDiagram.Devices.Count)
{
var conn = new Connection {
StartDevice = _currentDiagram.Devices[cModel.StartIndex],
EndDevice = _currentDiagram.Devices[cModel.EndIndex],
Type = cModel.Type
};
_currentDiagram.Connections.Add(conn);
RenderConnection(conn);
}
}
}
// Wait for Avalonia to measure/arrange the newly added controls, then center
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Loaded);
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Render);
CenterOnContent();
} catch (Exception ex) {
Console.WriteLine("Load failed: " + ex.Message);
} }
} }
} }
@@ -590,16 +727,18 @@ namespace NetworkDiagram
private void WifiTool_Click(object? sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wifi; private void WifiTool_Click(object? sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wifi;
private void AddText_Click(object? sender, RoutedEventArgs e) private void AddText_Click(object? sender, RoutedEventArgs e)
{ {
if (CanvasTranslate == null || CanvasScale == null) return; var center = GetVisibleCanvasCenter();
double centerX = (CanvasViewport.Bounds.Width / 2 - CanvasTranslate.X) / CanvasScale.ScaleX; AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, center.X - 60, center.Y - 40);
double centerY = (CanvasViewport.Bounds.Height / 2 - CanvasTranslate.Y) / CanvasScale.ScaleY;
AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, centerX, centerY);
} }
private async void ExportPng_Click(object? sender, RoutedEventArgs e) private async void ExportPng_Click(object? sender, RoutedEventArgs e)
{ {
if (_currentDiagram.Devices.Count == 0) return; if (_currentDiagram.Devices.Count == 0) return;
var prompt = new ExportOptionsWindow();
var result = await prompt.ShowDialog<bool?>(this);
if (result == null) return;
var topLevel = GetTopLevel(this); var topLevel = GetTopLevel(this);
if (topLevel == null) return; if (topLevel == null) return;
@@ -611,32 +750,98 @@ namespace NetworkDiagram
if (file != null) if (file != null)
{ {
var oldTranslate = new Point(CanvasTranslate.X, CanvasTranslate.Y); double minX = _currentDiagram.Devices.Min(d => d.X) - 50;
var oldScale = new Point(CanvasScale.ScaleX, CanvasScale.ScaleY); double minY = _currentDiagram.Devices.Min(d => d.Y) - 50;
double maxX = _currentDiagram.Devices.Max(d => d.X) + 210;
double maxY = _currentDiagram.Devices.Max(d => d.Y) + 160;
double minX = _currentDiagram.Devices.Min(d => d.X) - 20; int width = (int)(maxX - minX);
double minY = _currentDiagram.Devices.Min(d => d.Y) - 20; int height = (int)(maxY - minY);
double maxX = _currentDiagram.Devices.Max(d => d.X) + 180; if (width <= 0 || height <= 0) return;
double maxY = _currentDiagram.Devices.Max(d => d.Y) + 120;
// Create a temporary canvas for rendering to avoid live UI transform issues
var tempCanvas = new Canvas {
Width = width,
Height = height,
Background = result.Value ? Brushes.White : Brushes.Transparent
};
// Create a container to host temp canvas for rendering
var container = new Panel();
container.Children.Add(tempCanvas);
var rtb = new RenderTargetBitmap(new PixelSize(width, height), new Vector(96, 96));
// 1. Draw connections
foreach (var conn in _currentDiagram.Connections)
{
if (_connectionLines.TryGetValue(conn, out var line))
{
var tempLine = new Line {
Stroke = line.Stroke,
StrokeThickness = line.StrokeThickness,
StrokeDashArray = line.StrokeDashArray,
StartPoint = new Point(line.StartPoint.X - minX, line.StartPoint.Y - minY),
EndPoint = new Point(line.EndPoint.X - minX, line.EndPoint.Y - minY)
};
tempCanvas.Children.Add(tempLine);
}
}
// 2. Draw devices
foreach (var device in _currentDiagram.Devices)
{
if (_deviceElements.TryGetValue(device, out var original))
{
// Create a clone-like visual
var clone = new Border {
CornerRadius = new CornerRadius(8),
Padding = new Thickness(12),
Background = Brushes.White,
BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!,
BorderThickness = new Thickness(1),
Width = original.Bounds.Width > 0 ? original.Bounds.Width : 120,
Height = original.Bounds.Height > 0 ? original.Bounds.Height : 100,
DataContext = device
};
var stack = new StackPanel { Spacing = 4 };
var img = new Image { Width = 64, Height = 64 };
img.Bind(Image.SourceProperty, new Avalonia.Data.Binding("IconPath") { Converter = ImageConverter.Instance });
var txt = new TextBlock {
Text = device.Name,
FontWeight = FontWeight.Bold,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
Foreground = (IBrush)Application.Current!.FindResource("PrimaryBlue")!
};
stack.Children.Add(img);
stack.Children.Add(txt);
var ips = new ItemsControl { ItemsSource = device.IpAddresses };
ips.ItemTemplate = new FuncDataTemplate<string>((val, _) => new TextBlock {
Text = val, FontSize = 10, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
Foreground = (IBrush)Application.Current!.FindResource("AccentBlue")!
});
stack.Children.Add(ips);
clone.Child = stack;
Canvas.SetLeft(clone, device.X - minX);
Canvas.SetTop(clone, device.Y - minY);
tempCanvas.Children.Add(clone);
}
}
// Important: Ensure layout of temp visual
container.Measure(new Size(width, height));
container.Arrange(new Rect(0, 0, width, height));
await Task.Delay(100);
rtb.Render(container);
var pixelSize = new PixelSize((int)(maxX - minX), (int)(maxY - minY));
var rtb = new RenderTargetBitmap(pixelSize, new Vector(96, 96));
CanvasTranslate.X = -minX;
CanvasTranslate.Y = -minY;
CanvasScale.ScaleX = 1.0;
CanvasScale.ScaleY = 1.0;
await Task.Delay(50);
rtb.Render(DiagramCanvas);
using var stream = await file.OpenWriteAsync(); using var stream = await file.OpenWriteAsync();
rtb.Save(stream); rtb.Save(stream);
CanvasTranslate.X = oldTranslate.X;
CanvasTranslate.Y = oldTranslate.Y;
CanvasScale.ScaleX = oldScale.X;
CanvasScale.ScaleY = oldScale.Y;
} }
} }
#endregion #endregion
+29 -8
View File
@@ -1,11 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Avalonia;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
@@ -20,13 +16,18 @@ namespace NetworkDiagram.Models
public class DeviceTemplate public class DeviceTemplate
{ {
[JsonPropertyName("Name")]
public string Name { get; init; } = string.Empty; public string Name { get; init; } = string.Empty;
[JsonPropertyName("IconPath")]
public string IconPath { get; set; } = string.Empty; public string IconPath { get; set; } = string.Empty;
} }
public class PlacedDevice : INotifyPropertyChanged public class PlacedDevice : INotifyPropertyChanged
{ {
private string _name = string.Empty; private string _name = string.Empty;
[JsonPropertyName("Name")]
public string Name public string Name
{ {
get => _name; get => _name;
@@ -37,12 +38,14 @@ namespace NetworkDiagram.Models
} }
} }
[JsonPropertyName("TemplateName")]
public string TemplateName { get; set; } = string.Empty; public string TemplateName { get; set; } = string.Empty;
[JsonIgnore] [JsonIgnore]
public string IconPath { get; set; } = string.Empty; public string IconPath { get; set; } = string.Empty;
private double _x; private double _x;
[JsonPropertyName("X")]
public double X public double X
{ {
get => _x; get => _x;
@@ -55,6 +58,7 @@ namespace NetworkDiagram.Models
} }
private double _y; private double _y;
[JsonPropertyName("Y")]
public double Y public double Y
{ {
get => _y; get => _y;
@@ -67,6 +71,7 @@ namespace NetworkDiagram.Models
} }
private List<string> _ipAddresses = []; private List<string> _ipAddresses = [];
[JsonPropertyName("IpAddresses")]
public List<string> IpAddresses public List<string> IpAddresses
{ {
get => _ipAddresses; get => _ipAddresses;
@@ -77,7 +82,9 @@ namespace NetworkDiagram.Models
} }
} }
[JsonIgnore]
public double CenterX => X + 50; public double CenterX => X + 50;
[JsonIgnore]
public double CenterY => Y + 40; public double CenterY => Y + 40;
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
@@ -107,7 +114,7 @@ namespace NetworkDiagram.Models
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
if (value is string path && !string.IsNullOrEmpty(path)) if (value is string path && !string.IsNullOrWhiteSpace(path))
{ {
try try
{ {
@@ -117,12 +124,17 @@ namespace NetworkDiagram.Models
if (File.Exists(path)) if (File.Exists(path))
return new Bitmap(path); return new Bitmap(path);
// Try as relative avares string uriPath = path.Replace("\\", "/");
var uri = new Uri($"avares://NetworkDiagram/{path.Replace("\\", "/")}"); if (!uriPath.StartsWith("Assets/")) uriPath = "Assets/" + uriPath.TrimStart('/');
string escapedPath = uriPath.Replace(" ", "%20");
var uri = new Uri($"avares://NetworkDiagram/{escapedPath}");
return new Bitmap(AssetLoader.Open(uri)); return new Bitmap(AssetLoader.Open(uri));
} }
catch catch (Exception ex)
{ {
Console.WriteLine($"[ImageConverter] Error loading '{path}': {ex.Message}");
return null; return null;
} }
} }
@@ -133,12 +145,21 @@ namespace NetworkDiagram.Models
} }
public class DiagramSaveModel { public class DiagramSaveModel {
[JsonPropertyName("Devices")]
public List<PlacedDevice> Devices { get; set; } = []; public List<PlacedDevice> Devices { get; set; } = [];
[JsonPropertyName("Connections")]
public List<ConnectionSaveModel> Connections { get; set; } = []; public List<ConnectionSaveModel> Connections { get; set; } = [];
} }
public class ConnectionSaveModel { public class ConnectionSaveModel {
[JsonPropertyName("StartIndex")]
public int StartIndex { get; set; } public int StartIndex { get; set; }
[JsonPropertyName("EndIndex")]
public int EndIndex { get; set; } public int EndIndex { get; set; }
[JsonPropertyName("Type")]
public ConnectionType Type { get; set; } public ConnectionType Type { get; set; }
} }
} }
-1
View File
@@ -1,5 +1,4 @@
using Avalonia; using Avalonia;
using System;
namespace NetworkDiagram; namespace NetworkDiagram;
+2 -1
View File
@@ -1,12 +1,13 @@
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using System.Net.Mime; using System.Net.Mime;
using Avalonia;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using NetworkDiagramAvalonia.ViewModels; using NetworkDiagramAvalonia.ViewModels;
using NetworkDiagramAvalonia.Views; using NetworkDiagramAvalonia.Views;
namespace NetworkDiagramAvalonia; namespace NetworkDiagramAvalonia;
public partial class App : MediaTypeNames.Application public class App : Application
{ {
public override void Initialize() public override void Initialize()
{ {
@@ -1,6 +1,5 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:NetworkDiagramAvalonia.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:NetworkDiagramAvalonia.ViewModels" xmlns:viewModels="clr-namespace:NetworkDiagramAvalonia.ViewModels"
@@ -16,6 +15,6 @@
<viewModels:MainWindowViewModel/> <viewModels:MainWindowViewModel/>
</Design.DataContext> </Design.DataContext>
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Window> </Window>