mirror of
https://github.com/egmont11/NetworkDiagram.git
synced 2026-07-24 07:09:43 +02:00
i cannot for the life of me fix the dang camera shooting off to god knows where
This commit is contained in:
+3
@@ -3,7 +3,10 @@
|
||||
<component name="AvaloniaProject">
|
||||
<option name="projectPerEditor">
|
||||
<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/Views/MainWindow.axaml" value="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<Solution>
|
||||
<Project Path="NetworkDiagram/NetworkDiagram.csproj" />
|
||||
<Project Path="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
|
||||
</Solution>
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using NetworkDiagram.Models;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
<DockPanel Margin="10,15">
|
||||
<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"
|
||||
PointerPressed="Toolbox_PointerPressed">
|
||||
PointerMoved="Toolbox_PointerMoved">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:DeviceTemplate">
|
||||
<Border Background="White" CornerRadius="8" Padding="12" BorderBrush="{DynamicResource GridBlue}" BorderThickness="1">
|
||||
@@ -72,24 +72,24 @@
|
||||
|
||||
<!-- Canvas Area -->
|
||||
<Panel Grid.Row="1" Grid.Column="1" x:Name="CanvasViewport" ClipToBounds="True" Background="Transparent"
|
||||
DragDrop.AllowDrop="True"
|
||||
PointerPressed="Viewport_PointerPressed"
|
||||
PointerMoved="Viewport_PointerMoved"
|
||||
PointerReleased="Viewport_PointerReleased"
|
||||
PointerWheelChanged="Viewport_PointerWheelChanged">
|
||||
|
||||
<Canvas x:Name="DiagramCanvas" Width="40000" Height="40000"
|
||||
DragDrop.AllowDrop="True"
|
||||
PointerPressed="DiagramCanvas_PointerPressed"
|
||||
PointerMoved="DiagramCanvas_PointerMoved"
|
||||
PointerReleased="DiagramCanvas_PointerReleased">
|
||||
<Canvas.RenderTransform>
|
||||
<TransformGroup>
|
||||
<ScaleTransform ScaleX="1" ScaleY="1"/>
|
||||
<TranslateTransform X="-20000" Y="-20000"/>
|
||||
<TranslateTransform X="0" Y="0"/>
|
||||
</TransformGroup>
|
||||
</Canvas.RenderTransform>
|
||||
<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>
|
||||
<GeometryDrawing>
|
||||
<GeometryDrawing.Geometry>
|
||||
|
||||
@@ -16,6 +16,7 @@ using Avalonia.Markup.Xaml.Styling;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using NetworkDiagram.Models;
|
||||
using Avalonia.Controls.Templates;
|
||||
@@ -26,6 +27,7 @@ namespace NetworkDiagram
|
||||
{
|
||||
private List<DeviceTemplate> _deviceTemplates = [];
|
||||
private Diagram _currentDiagram = new();
|
||||
private bool _langComboReady;
|
||||
|
||||
// Selection & Dragging
|
||||
private readonly HashSet<Control> _selectedElements = [];
|
||||
@@ -50,6 +52,8 @@ namespace NetworkDiagram
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
LangCombo.SelectedIndex = 0;
|
||||
_langComboReady = true;
|
||||
LoadTemplates();
|
||||
this.Opened += (s, e) => {
|
||||
if (DiagramCanvas.RenderTransform is TransformGroup group)
|
||||
@@ -60,28 +64,40 @@ namespace NetworkDiagram
|
||||
CenterView();
|
||||
};
|
||||
|
||||
AddHandler(DragDrop.DragOverEvent, DiagramCanvas_DragOver);
|
||||
AddHandler(DragDrop.DropEvent, DiagramCanvas_Drop);
|
||||
AddHandler(DragDrop.DragOverEvent, Viewport_DragOver);
|
||||
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()
|
||||
{
|
||||
if (CanvasTranslate == null || CanvasScale == null) return;
|
||||
CanvasTranslate.X = -20000 + (CanvasViewport.Bounds.Width / 2);
|
||||
CanvasTranslate.Y = -20000 + (CanvasViewport.Bounds.Height / 2);
|
||||
double vw = CanvasViewport.Bounds.Width > 0 ? CanvasViewport.Bounds.Width : 1000;
|
||||
double vh = CanvasViewport.Bounds.Height > 0 ? CanvasViewport.Bounds.Height : 700;
|
||||
|
||||
CanvasScale.ScaleX = 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
|
||||
private void Viewport_PointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
var properties = e.GetCurrentPoint(CanvasViewport).Properties;
|
||||
if (properties.IsMiddleButtonPressed)
|
||||
if (properties.IsMiddleButtonPressed || properties.IsRightButtonPressed)
|
||||
{
|
||||
_isPanning = true;
|
||||
_lastPanPoint = e.GetPosition(CanvasViewport);
|
||||
e.Pointer.Capture(CanvasViewport);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +118,8 @@ namespace NetworkDiagram
|
||||
|
||||
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;
|
||||
e.Pointer.Capture(null);
|
||||
@@ -115,7 +132,7 @@ namespace NetworkDiagram
|
||||
double zoom = e.Delta.Y > 0 ? 1.1 : 0.9;
|
||||
|
||||
double newScale = CanvasScale.ScaleX * zoom;
|
||||
if (newScale < 0.1 || newScale > 5) return;
|
||||
if (newScale < 0.05 || newScale > 10) return;
|
||||
|
||||
Point mousePos = e.GetPosition(DiagramCanvas);
|
||||
|
||||
@@ -133,10 +150,8 @@ namespace NetworkDiagram
|
||||
try
|
||||
{
|
||||
if (!File.Exists("devices.json")) return;
|
||||
|
||||
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;
|
||||
}
|
||||
catch (Exception ex) { Console.WriteLine($"Error loading templates: {ex.Message}"); }
|
||||
@@ -144,6 +159,7 @@ namespace NetworkDiagram
|
||||
|
||||
private void LangCombo_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (!_langComboReady) return;
|
||||
if (LangCombo.SelectedItem is ComboBoxItem { Tag: string lang })
|
||||
{
|
||||
var app = Application.Current;
|
||||
@@ -161,20 +177,58 @@ namespace NetworkDiagram
|
||||
}
|
||||
|
||||
#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>();
|
||||
if (item?.Content is DeviceTemplate template)
|
||||
{
|
||||
_toolboxDragStart = e.GetPosition(null);
|
||||
_toolboxPendingTemplate = template;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (e.Source == DiagramCanvas)
|
||||
var props = e.GetCurrentPoint(DiagramCanvas).Properties;
|
||||
if (e.Source == DiagramCanvas && props.IsLeftButtonPressed)
|
||||
{
|
||||
ClearSelection();
|
||||
_isSelectingArea = true;
|
||||
@@ -242,26 +296,19 @@ namespace NetworkDiagram
|
||||
StopDragging();
|
||||
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
|
||||
|
||||
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)
|
||||
{
|
||||
var placed = new PlacedDevice { Name = template.Name, TemplateName = template.Name, IconPath = template.IconPath, X = x, Y = y };
|
||||
@@ -274,28 +321,32 @@ namespace NetworkDiagram
|
||||
var container = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(8),
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(2),
|
||||
MaxWidth = 160,
|
||||
Tag = device
|
||||
Padding = new Thickness(12),
|
||||
Background = Brushes.White,
|
||||
BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!,
|
||||
BorderThickness = new Thickness(1),
|
||||
MinWidth = 100,
|
||||
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 };
|
||||
image.Bind(Image.SourceProperty, new Avalonia.Data.Binding(nameof(device.IconPath)) { Converter = ImageConverter.Instance });
|
||||
var image = new Image { Width = 64, Height = 64, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center };
|
||||
image.Bind(Image.SourceProperty, new Avalonia.Data.Binding("IconPath") { Converter = ImageConverter.Instance });
|
||||
stack.Children.Add(image);
|
||||
|
||||
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;
|
||||
nameText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding(nameof(device.Name)));
|
||||
nameText.Bind(TextBlock.TextProperty, new Avalonia.Data.Binding("Name"));
|
||||
stack.Children.Add(nameText);
|
||||
|
||||
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");
|
||||
ips.ItemTemplate = new FuncDataTemplate<string>((val, _) => {
|
||||
@@ -370,7 +421,8 @@ namespace NetworkDiagram
|
||||
{
|
||||
var accentBlue = Application.Current!.FindResource("AccentBlue");
|
||||
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)
|
||||
{
|
||||
@@ -384,8 +436,9 @@ namespace NetworkDiagram
|
||||
_selectedElements.Remove(element);
|
||||
if (element is Border b)
|
||||
{
|
||||
b.BorderBrush = Brushes.Transparent;
|
||||
b.Background = Brushes.Transparent;
|
||||
b.BorderBrush = (IBrush)Application.Current!.FindResource("GridBlue")!;
|
||||
b.Background = Brushes.White;
|
||||
b.BorderThickness = new Thickness(1);
|
||||
}
|
||||
else if (element is Line l)
|
||||
{
|
||||
@@ -410,7 +463,7 @@ namespace NetworkDiagram
|
||||
{
|
||||
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);
|
||||
RenderConnection(conn);
|
||||
}
|
||||
@@ -438,15 +491,35 @@ namespace NetworkDiagram
|
||||
|
||||
void UpdatePos()
|
||||
{
|
||||
if (!_deviceElements.TryGetValue(conn.StartDevice, out var s) || !_deviceElements.TryGetValue(conn.EndDevice, out var e)) return;
|
||||
line.StartPoint = new Point(conn.StartDevice.X + s.Bounds.Width / 2, conn.StartDevice.Y + s.Bounds.Height / 2);
|
||||
line.EndPoint = new Point(conn.EndDevice.X + e.Bounds.Width / 2, conn.EndDevice.Y + e.Bounds.Height / 2);
|
||||
if (!_deviceElements.TryGetValue(conn.StartDevice, out var startEl) || !_deviceElements.TryGetValue(conn.EndDevice, out var endEl)) return;
|
||||
double sw = startEl.Bounds.Width > 0 ? startEl.Bounds.Width : 120;
|
||||
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();
|
||||
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(); };
|
||||
|
||||
// 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) => {
|
||||
if (!e.KeyModifiers.HasFlag(KeyModifiers.Control)) ClearSelection();
|
||||
SelectElement(line);
|
||||
@@ -455,9 +528,6 @@ namespace NetworkDiagram
|
||||
|
||||
_connectionLines[conn] = line;
|
||||
DiagramCanvas.Children.Insert(0, line);
|
||||
|
||||
// Initial position update after layout
|
||||
LayoutUpdated += (s, e) => UpdatePos();
|
||||
}
|
||||
|
||||
private async void EditDevice(PlacedDevice device)
|
||||
@@ -467,12 +537,15 @@ namespace NetworkDiagram
|
||||
|
||||
if (_deviceElements.TryGetValue(device, out var elem))
|
||||
{
|
||||
// Refresh layout and connections
|
||||
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))
|
||||
{
|
||||
l.StartPoint = new Point(c.StartDevice.X + _deviceElements[c.StartDevice].Bounds.Width / 2, c.StartDevice.Y + _deviceElements[c.StartDevice].Bounds.Height / 2);
|
||||
l.EndPoint = new Point(c.EndDevice.X + _deviceElements[c.EndDevice].Bounds.Width / 2, c.EndDevice.Y + _deviceElements[c.EndDevice].Bounds.Height / 2);
|
||||
double sw = _deviceElements[c.StartDevice].Bounds.Width > 0 ? _deviceElements[c.StartDevice].Bounds.Width : 120;
|
||||
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();
|
||||
}
|
||||
else if (e.Key == Key.Home)
|
||||
{
|
||||
if (_currentDiagram.Devices.Count > 0) CenterOnContent();
|
||||
else CenterView();
|
||||
}
|
||||
}
|
||||
|
||||
#region Toolbar Events
|
||||
@@ -550,6 +628,35 @@ namespace NetworkDiagram
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var topLevel = GetTopLevel(this);
|
||||
@@ -563,43 +670,75 @@ namespace NetworkDiagram
|
||||
|
||||
if (files.Count > 0)
|
||||
{
|
||||
try {
|
||||
using var stream = await files[0].OpenReadAsync();
|
||||
var model = await JsonSerializer.DeserializeAsync<DiagramSaveModel>(stream);
|
||||
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;
|
||||
|
||||
// Clear canvas without calling CenterView (we'll center on content after load)
|
||||
_currentDiagram = new Diagram();
|
||||
DiagramCanvas.Children.Clear();
|
||||
DiagramCanvas.Children.Add(SelectionRect);
|
||||
_connectionLines.Clear();
|
||||
_deviceElements.Clear();
|
||||
ClearSelection();
|
||||
|
||||
// Load devices exactly as saved — no normalization
|
||||
_currentDiagram = new Diagram { Devices = model.Devices };
|
||||
|
||||
foreach (var device in _currentDiagram.Devices)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var conn = new Connection { StartDevice = _currentDiagram.Devices[cModel.StartIndex], EndDevice = _currentDiagram.Devices[cModel.EndIndex], Type = cModel.Type };
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WireTool_Click(object? sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wire;
|
||||
private void WifiTool_Click(object? sender, RoutedEventArgs e) => _activeTool = ConnectionType.Wifi;
|
||||
private void AddText_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (CanvasTranslate == null || CanvasScale == null) return;
|
||||
double centerX = (CanvasViewport.Bounds.Width / 2 - CanvasTranslate.X) / CanvasScale.ScaleX;
|
||||
double centerY = (CanvasViewport.Bounds.Height / 2 - CanvasTranslate.Y) / CanvasScale.ScaleY;
|
||||
AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, centerX, centerY);
|
||||
var center = GetVisibleCanvasCenter();
|
||||
AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, center.X - 60, center.Y - 40);
|
||||
}
|
||||
|
||||
private async void ExportPng_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
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);
|
||||
if (topLevel == null) return;
|
||||
|
||||
@@ -611,32 +750,98 @@ namespace NetworkDiagram
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
var oldTranslate = new Point(CanvasTranslate.X, CanvasTranslate.Y);
|
||||
var oldScale = new Point(CanvasScale.ScaleX, CanvasScale.ScaleY);
|
||||
double minX = _currentDiagram.Devices.Min(d => d.X) - 50;
|
||||
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;
|
||||
double minY = _currentDiagram.Devices.Min(d => d.Y) - 20;
|
||||
double maxX = _currentDiagram.Devices.Max(d => d.X) + 180;
|
||||
double maxY = _currentDiagram.Devices.Max(d => d.Y) + 120;
|
||||
int width = (int)(maxX - minX);
|
||||
int height = (int)(maxY - minY);
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
var pixelSize = new PixelSize((int)(maxX - minX), (int)(maxY - minY));
|
||||
var rtb = new RenderTargetBitmap(pixelSize, new Vector(96, 96));
|
||||
// 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
|
||||
};
|
||||
|
||||
CanvasTranslate.X = -minX;
|
||||
CanvasTranslate.Y = -minY;
|
||||
CanvasScale.ScaleX = 1.0;
|
||||
CanvasScale.ScaleY = 1.0;
|
||||
// Create a container to host temp canvas for rendering
|
||||
var container = new Panel();
|
||||
container.Children.Add(tempCanvas);
|
||||
|
||||
await Task.Delay(50);
|
||||
rtb.Render(DiagramCanvas);
|
||||
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);
|
||||
|
||||
using var stream = await file.OpenWriteAsync();
|
||||
rtb.Save(stream);
|
||||
|
||||
CanvasTranslate.X = oldTranslate.X;
|
||||
CanvasTranslate.Y = oldTranslate.Y;
|
||||
CanvasScale.ScaleX = oldScale.X;
|
||||
CanvasScale.ScaleY = oldScale.Y;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using Avalonia;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Platform;
|
||||
@@ -20,13 +16,18 @@ namespace NetworkDiagram.Models
|
||||
|
||||
public class DeviceTemplate
|
||||
{
|
||||
[JsonPropertyName("Name")]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("IconPath")]
|
||||
public string IconPath { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class PlacedDevice : INotifyPropertyChanged
|
||||
{
|
||||
private string _name = string.Empty;
|
||||
|
||||
[JsonPropertyName("Name")]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
@@ -37,12 +38,14 @@ namespace NetworkDiagram.Models
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("TemplateName")]
|
||||
public string TemplateName { get; set; } = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public string IconPath { get; set; } = string.Empty;
|
||||
|
||||
private double _x;
|
||||
[JsonPropertyName("X")]
|
||||
public double X
|
||||
{
|
||||
get => _x;
|
||||
@@ -55,6 +58,7 @@ namespace NetworkDiagram.Models
|
||||
}
|
||||
|
||||
private double _y;
|
||||
[JsonPropertyName("Y")]
|
||||
public double Y
|
||||
{
|
||||
get => _y;
|
||||
@@ -67,6 +71,7 @@ namespace NetworkDiagram.Models
|
||||
}
|
||||
|
||||
private List<string> _ipAddresses = [];
|
||||
[JsonPropertyName("IpAddresses")]
|
||||
public List<string> IpAddresses
|
||||
{
|
||||
get => _ipAddresses;
|
||||
@@ -77,7 +82,9 @@ namespace NetworkDiagram.Models
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public double CenterX => X + 50;
|
||||
[JsonIgnore]
|
||||
public double CenterY => Y + 40;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
@@ -107,7 +114,7 @@ namespace NetworkDiagram.Models
|
||||
|
||||
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
|
||||
{
|
||||
@@ -117,12 +124,17 @@ namespace NetworkDiagram.Models
|
||||
if (File.Exists(path))
|
||||
return new Bitmap(path);
|
||||
|
||||
// Try as relative avares
|
||||
var uri = new Uri($"avares://NetworkDiagram/{path.Replace("\\", "/")}");
|
||||
string uriPath = 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));
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[ImageConverter] Error loading '{path}': {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -133,12 +145,21 @@ namespace NetworkDiagram.Models
|
||||
}
|
||||
|
||||
public class DiagramSaveModel {
|
||||
[JsonPropertyName("Devices")]
|
||||
public List<PlacedDevice> Devices { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("Connections")]
|
||||
public List<ConnectionSaveModel> Connections { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ConnectionSaveModel {
|
||||
[JsonPropertyName("StartIndex")]
|
||||
public int StartIndex { get; set; }
|
||||
|
||||
[JsonPropertyName("EndIndex")]
|
||||
public int EndIndex { get; set; }
|
||||
|
||||
[JsonPropertyName("Type")]
|
||||
public ConnectionType Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Avalonia;
|
||||
using System;
|
||||
|
||||
namespace NetworkDiagram;
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using System.Net.Mime;
|
||||
using Avalonia;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using NetworkDiagramAvalonia.ViewModels;
|
||||
using NetworkDiagramAvalonia.Views;
|
||||
|
||||
namespace NetworkDiagramAvalonia;
|
||||
|
||||
public partial class App : MediaTypeNames.Application
|
||||
public class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:NetworkDiagramAvalonia.ViewModels"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:viewModels="clr-namespace:NetworkDiagramAvalonia.ViewModels"
|
||||
@@ -16,6 +15,6 @@
|
||||
<viewModels:MainWindowViewModel/>
|
||||
</Design.DataContext>
|
||||
|
||||
<TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
|
||||
</Window>
|
||||
|
||||
Reference in New Issue
Block a user