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">
|
<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,4 +1,3 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="NetworkDiagram/NetworkDiagram.csproj" />
|
<Project Path="NetworkDiagram/NetworkDiagram.csproj" />
|
||||||
<Project Path="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
|
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
<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
@@ -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
|
||||||
|
|||||||
@@ -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,5 +1,4 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace NetworkDiagram;
|
namespace NetworkDiagram;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user