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">
<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
View File
@@ -1,4 +1,3 @@
<Solution>
<Project Path="NetworkDiagram/NetworkDiagram.csproj" />
<Project Path="NetworkDiagramAvalonia/NetworkDiagramAvalonia.csproj" />
</Solution>
-3
View File
@@ -1,6 +1,3 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
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">
<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>
+287 -82
View File
@@ -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
+29 -8
View File
@@ -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
View File
@@ -1,5 +1,4 @@
using Avalonia;
using System;
namespace NetworkDiagram;
+2 -1
View File
@@ -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>