Added functional project (created repo after getting it to a functional state)
@@ -0,0 +1,15 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Rider ignored files
|
||||||
|
/.idea.NetworkDiagram.iml
|
||||||
|
/contentModel.xml
|
||||||
|
/modules.xml
|
||||||
|
/projectSettingsUpdater.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="UserContentModel">
|
||||||
|
<attachedFolders />
|
||||||
|
<explicitIncludes />
|
||||||
|
<explicitExcludes />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="NetworkDiagram/NetworkDiagram.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Application x:Class="NetworkDiagram.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="clr-namespace:NetworkDiagram"
|
||||||
|
StartupUri="MainWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="Localization/Strings.en.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Configuration;
|
||||||
|
using System.Data;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace NetworkDiagram;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for App.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
[assembly: ThemeInfo(
|
||||||
|
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// or application resource dictionaries)
|
||||||
|
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||||
|
//(used if a resource is not found in the page,
|
||||||
|
// app, or any theme specific resource dictionaries)
|
||||||
|
)]
|
||||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
<Window x:Class="NetworkDiagram.EditDeviceWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="{DynamicResource EditTitle}" Height="350" Width="400"
|
||||||
|
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
||||||
|
Background="#F5F9FF">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="{DynamicResource DeviceNameLabel}" Margin="0,0,0,5"/>
|
||||||
|
<TextBox x:Name="NameBox" Grid.Row="1" Margin="0,0,0,15" Padding="5"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Text="{DynamicResource IpLabel}" Margin="0,0,0,5"/>
|
||||||
|
<TextBox x:Name="IpBox" Grid.Row="3" Margin="0,0,0,15" Padding="5"
|
||||||
|
AcceptsReturn="True" VerticalScrollBarVisibility="Auto"/>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="{DynamicResource CancelBtn}" Click="Cancel_Click" Width="80" Margin="0,0,10,0" Padding="5"/>
|
||||||
|
<Button Content="{DynamicResource SaveDialogBtn}" Click="Save_Click" Width="80" Padding="5" Background="#0078D7" Foreground="White"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using NetworkDiagram.Models;
|
||||||
|
|
||||||
|
namespace NetworkDiagram
|
||||||
|
{
|
||||||
|
public partial class EditDeviceWindow : Window
|
||||||
|
{
|
||||||
|
private PlacedDevice _device;
|
||||||
|
|
||||||
|
public EditDeviceWindow(PlacedDevice device)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_device = device;
|
||||||
|
NameBox.Text = device.Name;
|
||||||
|
IpBox.Text = string.Join(Environment.NewLine, device.IpAddresses);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_device.Name = NameBox.Text;
|
||||||
|
_device.IpAddresses = IpBox.Text.Split(new[] { Environment.NewLine, "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||||
|
DialogResult = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DialogResult = false;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||||
|
<system:String x:Key="AppTitle">Editor síťových diagramů</system:String>
|
||||||
|
<system:String x:Key="NewBtn">Nový</system:String>
|
||||||
|
<system:String x:Key="SaveBtn">Uložit</system:String>
|
||||||
|
<system:String x:Key="LoadBtn">Načíst</system:String>
|
||||||
|
<system:String x:Key="ExportBtn">Exportovat PNG</system:String>
|
||||||
|
<system:String x:Key="WireBtn">Kabelové připojení</system:String>
|
||||||
|
<system:String x:Key="WifiBtn">Bezdrátové (WiFi)</system:String>
|
||||||
|
<system:String x:Key="AddTextBtn">Přidat text</system:String>
|
||||||
|
<system:String x:Key="DevicesHeader">Zařízení</system:String>
|
||||||
|
|
||||||
|
<!-- Edit Dialog -->
|
||||||
|
<system:String x:Key="EditTitle">Upravit zařízení</system:String>
|
||||||
|
<system:String x:Key="DeviceNameLabel">Název zařízení:</system:String>
|
||||||
|
<system:String x:Key="IpLabel">IP adresy (jedna na řádek):</system:String>
|
||||||
|
<system:String x:Key="CancelBtn">Zrušit</system:String>
|
||||||
|
<system:String x:Key="SaveDialogBtn">Uložit</system:String>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<system:String x:Key="ExportWhiteBgMsg">Chcete bílé pozadí? (Ne = průhlednost)</system:String>
|
||||||
|
<system:String x:Key="ExportOptionsTitle">Možnosti exportu</system:String>
|
||||||
|
<system:String x:Key="ExportSuccess">Export byl úspěšný!</system:String>
|
||||||
|
<system:String x:Key="EmptyDiagramMsg">Diagram je prázdný.</system:String>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:system="clr-namespace:System;assembly=mscorlib">
|
||||||
|
<system:String x:Key="AppTitle">Network Diagram Studio</system:String>
|
||||||
|
<system:String x:Key="NewBtn">New</system:String>
|
||||||
|
<system:String x:Key="SaveBtn">Save</system:String>
|
||||||
|
<system:String x:Key="LoadBtn">Load</system:String>
|
||||||
|
<system:String x:Key="ExportBtn">Export PNG</system:String>
|
||||||
|
<system:String x:Key="WireBtn">Wire Connection</system:String>
|
||||||
|
<system:String x:Key="WifiBtn">Wifi Connection</system:String>
|
||||||
|
<system:String x:Key="AddTextBtn">Add Text</system:String>
|
||||||
|
<system:String x:Key="DevicesHeader">Devices</system:String>
|
||||||
|
|
||||||
|
<!-- Edit Dialog -->
|
||||||
|
<system:String x:Key="EditTitle">Edit Device</system:String>
|
||||||
|
<system:String x:Key="DeviceNameLabel">Device Name:</system:String>
|
||||||
|
<system:String x:Key="IpLabel">IP Addresses (one per line):</system:String>
|
||||||
|
<system:String x:Key="CancelBtn">Cancel</system:String>
|
||||||
|
<system:String x:Key="SaveDialogBtn">Save</system:String>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<system:String x:Key="ExportWhiteBgMsg">Do you want a white background? (No will result in transparency)</system:String>
|
||||||
|
<system:String x:Key="ExportOptionsTitle">Export Options</system:String>
|
||||||
|
<system:String x:Key="ExportSuccess">Export successful!</system:String>
|
||||||
|
<system:String x:Key="EmptyDiagramMsg">Diagram is empty.</system:String>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<Window x:Class="NetworkDiagram.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:NetworkDiagram"
|
||||||
|
xmlns:models="clr-namespace:NetworkDiagram.Models"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="{DynamicResource AppTitle}" Height="700" Width="1080"
|
||||||
|
Background="#F5F9FF"
|
||||||
|
KeyDown="Window_KeyDown">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<SolidColorBrush x:Key="HeaderBlue" Color="#004578"/>
|
||||||
|
<SolidColorBrush x:Key="AccentBlue" Color="#0078D7"/>
|
||||||
|
<SolidColorBrush x:Key="SidebarBlue" Color="#E1EEFA"/>
|
||||||
|
<SolidColorBrush x:Key="CanvasGridBlue" Color="#D0E2F2"/>
|
||||||
|
|
||||||
|
<Style x:Key="ModernButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource AccentBlue}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="Padding" Value="10,5"/>
|
||||||
|
<Setter Property="Margin" Value="5"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="220"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource HeaderBlue}" Padding="10">
|
||||||
|
<DockPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
|
||||||
|
<TextBlock Text="{DynamicResource AppTitle}" Foreground="White" FontSize="18" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,20,0"/>
|
||||||
|
<Button Content="{DynamicResource NewBtn}" Style="{StaticResource ModernButton}" Click="NewDiagram_Click"/>
|
||||||
|
<Button Content="{DynamicResource SaveBtn}" Style="{StaticResource ModernButton}" Click="SaveDiagram_Click"/>
|
||||||
|
<Button Content="{DynamicResource LoadBtn}" Style="{StaticResource ModernButton}" Click="LoadDiagram_Click"/>
|
||||||
|
<Button Content="{DynamicResource ExportBtn}" Style="{StaticResource ModernButton}" Click="ExportPng_Click"/>
|
||||||
|
<Separator Margin="10,0" Background="White" Opacity="0.5"/>
|
||||||
|
<Button Content="{DynamicResource WireBtn}" Style="{StaticResource ModernButton}" Click="WireTool_Click"/>
|
||||||
|
<Button Content="{DynamicResource WifiBtn}" Style="{StaticResource ModernButton}" Click="WifiTool_Click"/>
|
||||||
|
<Button Content="{DynamicResource AddTextBtn}" Style="{StaticResource ModernButton}" Click="AddText_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<ComboBox x:Name="LangCombo" VerticalAlignment="Center" Width="80" SelectionChanged="LangCombo_SelectionChanged" Margin="10,0,0,0">
|
||||||
|
<ComboBoxItem Tag="en">EN</ComboBoxItem>
|
||||||
|
<ComboBoxItem Tag="cs" IsSelected="True">CZ</ComboBoxItem>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Grid.Column="0" Background="{StaticResource SidebarBlue}" BorderBrush="{StaticResource CanvasGridBlue}" BorderThickness="0,0,1,0">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock Text="{DynamicResource DevicesHeader}" DockPanel.Dock="Top" FontSize="16" FontWeight="SemiBold" Margin="10" Foreground="{StaticResource HeaderBlue}"/>
|
||||||
|
<ListBox x:Name="ToolboxList" Background="Transparent" BorderThickness="0" Margin="5"
|
||||||
|
PreviewMouseDown="Toolbox_PreviewMouseDown">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border BorderBrush="{StaticResource AccentBlue}" BorderThickness="1" CornerRadius="4" Margin="5" Padding="10" Background="White">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Image Source="{Binding IconPath}" Width="32" Height="32" Margin="0,0,10,0"/>
|
||||||
|
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontWeight="Medium"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1" Grid.Column="1" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
|
||||||
|
<Canvas x:Name="DiagramCanvas" Width="2000" Height="2000" AllowDrop="True"
|
||||||
|
Drop="DiagramCanvas_Drop" DragOver="DiagramCanvas_DragOver"
|
||||||
|
MouseDown="DiagramCanvas_MouseDown"
|
||||||
|
MouseMove="DiagramCanvas_MouseMove"
|
||||||
|
MouseUp="DiagramCanvas_MouseUp">
|
||||||
|
<Canvas.Background>
|
||||||
|
<VisualBrush TileMode="Tile" Viewport="0,0,40,40" ViewportUnits="Absolute">
|
||||||
|
<VisualBrush.Visual>
|
||||||
|
<Path Data="M 0 40 L 0 0 L 40 0" Stroke="{StaticResource CanvasGridBlue}" StrokeThickness="0.5"/>
|
||||||
|
</VisualBrush.Visual>
|
||||||
|
</VisualBrush>
|
||||||
|
</Canvas.Background>
|
||||||
|
|
||||||
|
<Canvas.Resources>
|
||||||
|
<DataTemplate x:Key="DeviceTemplate">
|
||||||
|
<Border x:Name="DeviceBorder" BorderBrush="Transparent" BorderThickness="2" CornerRadius="4"
|
||||||
|
Background="Transparent" Padding="5" MaxWidth="150">
|
||||||
|
<StackPanel>
|
||||||
|
<Image Source="{Binding IconPath}" Width="48" Height="48" HorizontalAlignment="Center">
|
||||||
|
<Image.Style>
|
||||||
|
<Style TargetType="Image">
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="Source" Value="{x:Null}">
|
||||||
|
<Setter Property="Height" Value="0"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Image.Style>
|
||||||
|
</Image>
|
||||||
|
<TextBlock Text="{Binding Name}" FontWeight="Bold" HorizontalAlignment="Center"
|
||||||
|
Foreground="#004578" TextWrapping="Wrap" TextAlignment="Center"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding IpAddresses}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding}" FontSize="10" Foreground="#0078D7"
|
||||||
|
HorizontalAlignment="Center" TextWrapping="Wrap"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</Canvas.Resources>
|
||||||
|
|
||||||
|
<!-- Selection rectangle visual -->
|
||||||
|
<Rectangle x:Name="SelectionRect" Stroke="{StaticResource AccentBlue}" StrokeDashArray="2,2"
|
||||||
|
Fill="#330078D7" Visibility="Collapsed" Panel.ZIndex="9999"/>
|
||||||
|
</Canvas>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,554 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
using NetworkDiagram.Models;
|
||||||
|
using Path = System.IO.Path;
|
||||||
|
|
||||||
|
namespace NetworkDiagram
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private List<DeviceTemplate> _deviceTemplates = new List<DeviceTemplate>();
|
||||||
|
private Diagram _currentDiagram = new Diagram();
|
||||||
|
|
||||||
|
// Selection & Dragging
|
||||||
|
private HashSet<FrameworkElement> _selectedElements = new HashSet<FrameworkElement>();
|
||||||
|
private Point _dragStartPoint;
|
||||||
|
private bool _isDraggingDevices;
|
||||||
|
private bool _isSelectingArea;
|
||||||
|
private Point _selectionStartPoint;
|
||||||
|
|
||||||
|
private ConnectionType? _activeTool;
|
||||||
|
private PlacedDevice? _firstDeviceForConnection;
|
||||||
|
|
||||||
|
private Dictionary<Connection, Line> _connectionLines = new Dictionary<Connection, Line>();
|
||||||
|
private Dictionary<PlacedDevice, FrameworkElement> _deviceElements = new Dictionary<PlacedDevice, FrameworkElement>();
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
LoadTemplates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadTemplates()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists("devices.json"))
|
||||||
|
{
|
||||||
|
string json = File.ReadAllText("devices.json");
|
||||||
|
_deviceTemplates = JsonSerializer.Deserialize<List<DeviceTemplate>>(json) ?? new List<DeviceTemplate>();
|
||||||
|
foreach(var t in _deviceTemplates)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(t.IconPath)) t.IconPath = Path.GetFullPath(t.IconPath);
|
||||||
|
}
|
||||||
|
ToolboxList.ItemsSource = _deviceTemplates;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) { MessageBox.Show($"Error loading templates: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLocalizedString(string key) => Application.Current.Resources[key] as string ?? key;
|
||||||
|
|
||||||
|
private void LangCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (LangCombo.SelectedItem is ComboBoxItem item && item.Tag is string lang)
|
||||||
|
{
|
||||||
|
var dict = new ResourceDictionary();
|
||||||
|
dict.Source = new Uri($"Localization/Strings.{lang}.xaml", UriKind.Relative);
|
||||||
|
|
||||||
|
// Replace the existing localization dictionary
|
||||||
|
var oldDict = Application.Current.Resources.MergedDictionaries.FirstOrDefault(d => d.Source != null && d.Source.OriginalString.Contains("Localization/Strings."));
|
||||||
|
if (oldDict != null) Application.Current.Resources.MergedDictionaries.Remove(oldDict);
|
||||||
|
Application.Current.Resources.MergedDictionaries.Add(dict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Drag and Drop (Toolbox to Canvas)
|
||||||
|
private void Toolbox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is ListBox listBox && listBox.SelectedItem is DeviceTemplate template)
|
||||||
|
DragDrop.DoDragDrop(listBox, template, DragDropEffects.Copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DiagramCanvas_DragOver(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
e.Effects = e.Data.GetDataPresent(typeof(DeviceTemplate)) ? DragDropEffects.Copy : DragDropEffects.None;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DiagramCanvas_Drop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data.GetDataPresent(typeof(DeviceTemplate)))
|
||||||
|
{
|
||||||
|
var template = (DeviceTemplate)e.Data.GetData(typeof(DeviceTemplate));
|
||||||
|
Point dropPoint = e.GetPosition(DiagramCanvas);
|
||||||
|
AddDeviceToCanvas(template, dropPoint.X, dropPoint.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void AddDeviceToCanvas(DeviceTemplate template, double x, double y)
|
||||||
|
{
|
||||||
|
var placed = new PlacedDevice { Name = template.Name, IconPath = template.IconPath, X = x, Y = y };
|
||||||
|
_currentDiagram.Devices.Add(placed);
|
||||||
|
RenderDevice(placed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderDevice(PlacedDevice device)
|
||||||
|
{
|
||||||
|
var template = (DataTemplate)DiagramCanvas.Resources["DeviceTemplate"];
|
||||||
|
var contentPresenter = new ContentPresenter { Content = device, ContentTemplate = template };
|
||||||
|
var container = new Border { Child = contentPresenter, Tag = device, Background = Brushes.Transparent };
|
||||||
|
|
||||||
|
Canvas.SetLeft(container, device.X);
|
||||||
|
Canvas.SetTop(container, device.Y);
|
||||||
|
|
||||||
|
device.PropertyChanged += (s, e) => {
|
||||||
|
if (e.PropertyName == nameof(PlacedDevice.X)) Canvas.SetLeft(container, device.X);
|
||||||
|
if (e.PropertyName == nameof(PlacedDevice.Y)) Canvas.SetTop(container, device.Y);
|
||||||
|
};
|
||||||
|
|
||||||
|
container.MouseDown += Device_MouseDown;
|
||||||
|
container.MouseLeftButtonDown += (s, e) => {
|
||||||
|
if (e.ClickCount == 2) {
|
||||||
|
StopDragging();
|
||||||
|
EditDevice(device);
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_deviceElements[device] = container;
|
||||||
|
DiagramCanvas.Children.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopDragging()
|
||||||
|
{
|
||||||
|
_isDraggingDevices = false;
|
||||||
|
_isSelectingArea = false;
|
||||||
|
SelectionRect.Visibility = Visibility.Collapsed;
|
||||||
|
DiagramCanvas.ReleaseMouseCapture();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Device_MouseDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is FrameworkElement element && element.Tag is PlacedDevice device)
|
||||||
|
{
|
||||||
|
if (_activeTool != null)
|
||||||
|
{
|
||||||
|
HandleConnectionTool(device, element);
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Selection
|
||||||
|
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
|
||||||
|
{
|
||||||
|
if (!_selectedElements.Contains(element))
|
||||||
|
{
|
||||||
|
ClearSelection();
|
||||||
|
SelectElement(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_selectedElements.Contains(element)) DeselectElement(element);
|
||||||
|
else SelectElement(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Dragging
|
||||||
|
_isDraggingDevices = true;
|
||||||
|
_dragStartPoint = e.GetPosition(DiagramCanvas);
|
||||||
|
DiagramCanvas.CaptureMouse();
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectElement(FrameworkElement element)
|
||||||
|
{
|
||||||
|
_selectedElements.Add(element);
|
||||||
|
element.Opacity = 0.7;
|
||||||
|
if (element is Line l) l.StrokeThickness = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeselectElement(FrameworkElement element)
|
||||||
|
{
|
||||||
|
_selectedElements.Remove(element);
|
||||||
|
element.Opacity = 1.0;
|
||||||
|
if (element is Line l) l.StrokeThickness = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearSelection()
|
||||||
|
{
|
||||||
|
foreach (var el in _selectedElements.ToList()) DeselectElement(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Canvas Interaction (Selection Area & Dragging)
|
||||||
|
private void DiagramCanvas_MouseDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.OriginalSource == DiagramCanvas)
|
||||||
|
{
|
||||||
|
ClearSelection();
|
||||||
|
_isSelectingArea = true;
|
||||||
|
_selectionStartPoint = e.GetPosition(DiagramCanvas);
|
||||||
|
|
||||||
|
Canvas.SetLeft(SelectionRect, _selectionStartPoint.X);
|
||||||
|
Canvas.SetTop(SelectionRect, _selectionStartPoint.Y);
|
||||||
|
SelectionRect.Width = 0;
|
||||||
|
SelectionRect.Height = 0;
|
||||||
|
SelectionRect.Visibility = Visibility.Visible;
|
||||||
|
DiagramCanvas.CaptureMouse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DiagramCanvas_MouseMove(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
Point currentPoint = e.GetPosition(DiagramCanvas);
|
||||||
|
|
||||||
|
if (_isSelectingArea)
|
||||||
|
{
|
||||||
|
double x = Math.Min(_selectionStartPoint.X, currentPoint.X);
|
||||||
|
double y = Math.Min(_selectionStartPoint.Y, currentPoint.Y);
|
||||||
|
double w = Math.Abs(_selectionStartPoint.X - currentPoint.X);
|
||||||
|
double h = Math.Abs(_selectionStartPoint.Y - currentPoint.Y);
|
||||||
|
|
||||||
|
Canvas.SetLeft(SelectionRect, x);
|
||||||
|
Canvas.SetTop(SelectionRect, y);
|
||||||
|
SelectionRect.Width = w;
|
||||||
|
SelectionRect.Height = h;
|
||||||
|
|
||||||
|
// Real-time selection preview
|
||||||
|
Rect selectionBounds = new Rect(x, y, w, h);
|
||||||
|
foreach (var border in _deviceElements.Values)
|
||||||
|
{
|
||||||
|
Rect elementBounds = new Rect(Canvas.GetLeft(border), Canvas.GetTop(border), border.ActualWidth, border.ActualHeight);
|
||||||
|
if (selectionBounds.IntersectsWith(elementBounds))
|
||||||
|
{
|
||||||
|
if (!_selectedElements.Contains(border)) SelectElement(border);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Only deselect if we are in the middle of a selection area drag
|
||||||
|
if (_selectedElements.Contains(border)) DeselectElement(border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also preview for lines
|
||||||
|
foreach (var line in _connectionLines.Values)
|
||||||
|
{
|
||||||
|
Rect lineBounds = new Rect(
|
||||||
|
Math.Min(line.X1, line.X2),
|
||||||
|
Math.Min(line.Y1, line.Y2),
|
||||||
|
Math.Abs(line.X1 - line.X2),
|
||||||
|
Math.Abs(line.Y1 - line.Y2));
|
||||||
|
|
||||||
|
if (selectionBounds.IntersectsWith(lineBounds))
|
||||||
|
{
|
||||||
|
if (!_selectedElements.Contains(line)) SelectElement(line);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_selectedElements.Contains(line)) DeselectElement(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_isDraggingDevices && e.LeftButton == MouseButtonState.Pressed)
|
||||||
|
{
|
||||||
|
double deltaX = currentPoint.X - _dragStartPoint.X;
|
||||||
|
double deltaY = currentPoint.Y - _dragStartPoint.Y;
|
||||||
|
|
||||||
|
foreach (var element in _selectedElements)
|
||||||
|
{
|
||||||
|
if (element.Tag is PlacedDevice device)
|
||||||
|
{
|
||||||
|
device.X += deltaX;
|
||||||
|
device.Y += deltaY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_dragStartPoint = currentPoint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DiagramCanvas_MouseUp(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isSelectingArea)
|
||||||
|
{
|
||||||
|
Rect selectionBounds = new Rect(
|
||||||
|
Canvas.GetLeft(SelectionRect),
|
||||||
|
Canvas.GetTop(SelectionRect),
|
||||||
|
SelectionRect.Width,
|
||||||
|
SelectionRect.Height);
|
||||||
|
|
||||||
|
foreach (var border in _deviceElements.Values)
|
||||||
|
{
|
||||||
|
Rect elementBounds = new Rect(Canvas.GetLeft(border), Canvas.GetTop(border), border.ActualWidth, border.ActualHeight);
|
||||||
|
if (selectionBounds.IntersectsWith(elementBounds)) SelectElement(border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StopDragging();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void HandleConnectionTool(PlacedDevice device, FrameworkElement element)
|
||||||
|
{
|
||||||
|
if (_firstDeviceForConnection == null)
|
||||||
|
{
|
||||||
|
_firstDeviceForConnection = device;
|
||||||
|
element.Opacity = 0.5;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_firstDeviceForConnection != device)
|
||||||
|
{
|
||||||
|
var conn = new Connection { StartDevice = _firstDeviceForConnection, EndDevice = device, Type = _activeTool.Value };
|
||||||
|
_currentDiagram.Connections.Add(conn);
|
||||||
|
RenderConnection(conn);
|
||||||
|
}
|
||||||
|
ResetConnectionTool();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetConnectionTool()
|
||||||
|
{
|
||||||
|
_firstDeviceForConnection = null;
|
||||||
|
_activeTool = null;
|
||||||
|
foreach (var child in DiagramCanvas.Children.OfType<Border>()) child.Opacity = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenderConnection(Connection conn)
|
||||||
|
{
|
||||||
|
var line = new Line
|
||||||
|
{
|
||||||
|
Stroke = conn.Type == ConnectionType.Wifi ? Brushes.DeepSkyBlue : Brushes.SlateGray,
|
||||||
|
StrokeThickness = 3,
|
||||||
|
Tag = conn
|
||||||
|
};
|
||||||
|
if (conn.Type == ConnectionType.Wifi) line.StrokeDashArray = new DoubleCollection { 2, 2 };
|
||||||
|
|
||||||
|
UpdateLinePosition(conn, line);
|
||||||
|
|
||||||
|
conn.StartDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdateLinePosition(conn, line); };
|
||||||
|
conn.EndDevice.PropertyChanged += (s, e) => { if (e.PropertyName == "X" || e.PropertyName == "Y") UpdateLinePosition(conn, line); };
|
||||||
|
|
||||||
|
line.MouseDown += (s, e) => {
|
||||||
|
if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl)) ClearSelection();
|
||||||
|
SelectElement(line);
|
||||||
|
e.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
_connectionLines[conn] = line;
|
||||||
|
DiagramCanvas.Children.Insert(0, line);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLinePosition(Connection conn, Line line)
|
||||||
|
{
|
||||||
|
if (_deviceElements.TryGetValue(conn.StartDevice, out var startElem) &&
|
||||||
|
_deviceElements.TryGetValue(conn.EndDevice, out var endElem))
|
||||||
|
{
|
||||||
|
if (startElem.ActualWidth == 0) startElem.UpdateLayout();
|
||||||
|
if (endElem.ActualWidth == 0) endElem.UpdateLayout();
|
||||||
|
|
||||||
|
line.X1 = conn.StartDevice.X + (startElem.ActualWidth / 2);
|
||||||
|
line.Y1 = conn.StartDevice.Y + 24;
|
||||||
|
line.X2 = conn.EndDevice.X + (endElem.ActualWidth / 2);
|
||||||
|
line.Y2 = conn.EndDevice.Y + 24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Delete && _selectedElements.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var el in _selectedElements.ToList())
|
||||||
|
{
|
||||||
|
if (el is Line line && el.Tag is Connection conn)
|
||||||
|
{
|
||||||
|
_currentDiagram.Connections.Remove(conn);
|
||||||
|
_connectionLines.Remove(conn);
|
||||||
|
DiagramCanvas.Children.Remove(line);
|
||||||
|
}
|
||||||
|
else if (el is Border border && border.Tag is PlacedDevice device)
|
||||||
|
{
|
||||||
|
_currentDiagram.Devices.Remove(device);
|
||||||
|
_deviceElements.Remove(device);
|
||||||
|
var toRemove = _currentDiagram.Connections.Where(c => c.StartDevice == device || c.EndDevice == device).ToList();
|
||||||
|
foreach (var c in toRemove)
|
||||||
|
{
|
||||||
|
if (_connectionLines.TryGetValue(c, out var l)) DiagramCanvas.Children.Remove(l);
|
||||||
|
_currentDiagram.Connections.Remove(c);
|
||||||
|
_connectionLines.Remove(c);
|
||||||
|
}
|
||||||
|
DiagramCanvas.Children.Remove(border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_selectedElements.Clear();
|
||||||
|
}
|
||||||
|
else if (e.Key == Key.Escape) { ResetConnectionTool(); StopDragging(); ClearSelection(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditDevice(PlacedDevice device)
|
||||||
|
{
|
||||||
|
var dialog = new EditDeviceWindow(device);
|
||||||
|
dialog.ShowDialog();
|
||||||
|
if (_deviceElements.TryGetValue(device, out var elem))
|
||||||
|
{
|
||||||
|
elem.UpdateLayout();
|
||||||
|
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)) UpdateLinePosition(c, l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Toolbar Events
|
||||||
|
private void NewDiagram_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_currentDiagram = new Diagram();
|
||||||
|
DiagramCanvas.Children.Clear();
|
||||||
|
_connectionLines.Clear();
|
||||||
|
_deviceElements.Clear();
|
||||||
|
ClearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveDiagram_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var sfd = new SaveFileDialog { Filter = "Network Diagram (*.ndjson)|*.ndjson" };
|
||||||
|
if (sfd.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
var model = new DiagramSaveModel {
|
||||||
|
Devices = _currentDiagram.Devices,
|
||||||
|
Connections = _currentDiagram.Connections.Select(c => new ConnectionSaveModel {
|
||||||
|
StartIndex = _currentDiagram.Devices.IndexOf(c.StartDevice),
|
||||||
|
EndIndex = _currentDiagram.Devices.IndexOf(c.EndDevice),
|
||||||
|
Type = c.Type
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
string json = JsonSerializer.Serialize(model, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(sfd.FileName, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadDiagram_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var ofd = new OpenFileDialog { Filter = "Network Diagram (*.ndjson)|*.ndjson" };
|
||||||
|
if (ofd.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
string json = File.ReadAllText(ofd.FileName);
|
||||||
|
var model = JsonSerializer.Deserialize<DiagramSaveModel>(json);
|
||||||
|
if (model == null) return;
|
||||||
|
_currentDiagram = new Diagram { Devices = model.Devices };
|
||||||
|
DiagramCanvas.Children.Clear();
|
||||||
|
_connectionLines.Clear();
|
||||||
|
_deviceElements.Clear();
|
||||||
|
ClearSelection();
|
||||||
|
foreach (var device in _currentDiagram.Devices) RenderDevice(device);
|
||||||
|
foreach (var cModel in model.Connections)
|
||||||
|
{
|
||||||
|
var conn = new Connection { StartDevice = _currentDiagram.Devices[cModel.StartIndex], EndDevice = _currentDiagram.Devices[cModel.EndIndex], Type = cModel.Type };
|
||||||
|
_currentDiagram.Connections.Add(conn);
|
||||||
|
RenderConnection(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => AddDeviceToCanvas(new DeviceTemplate { Name = "Note", IconPath = "" }, 100, 100);
|
||||||
|
|
||||||
|
private void ExportPng_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_currentDiagram.Devices.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show(GetLocalizedString("EmptyDiagramMsg"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sfd = new SaveFileDialog { Filter = "PNG Image (*.png)|*.png", Title = GetLocalizedString("ExportOptionsTitle") };
|
||||||
|
if (sfd.ShowDialog() != true) return;
|
||||||
|
|
||||||
|
var result = MessageBox.Show(GetLocalizedString("ExportWhiteBgMsg"), GetLocalizedString("ExportOptionsTitle"), MessageBoxButton.YesNoCancel);
|
||||||
|
if (result == MessageBoxResult.Cancel) return;
|
||||||
|
|
||||||
|
bool whiteBg = result == MessageBoxResult.Yes;
|
||||||
|
|
||||||
|
// 1. Find content bounds
|
||||||
|
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
|
||||||
|
foreach (var border in _deviceElements.Values)
|
||||||
|
{
|
||||||
|
double x = Canvas.GetLeft(border);
|
||||||
|
double y = Canvas.GetTop(border);
|
||||||
|
minX = Math.Min(minX, x);
|
||||||
|
minY = Math.Min(minY, y);
|
||||||
|
maxX = Math.Max(maxX, x + border.ActualWidth);
|
||||||
|
maxY = Math.Max(maxY, y + border.ActualHeight);
|
||||||
|
}
|
||||||
|
foreach (var line in _connectionLines.Values)
|
||||||
|
{
|
||||||
|
minX = Math.Min(minX, Math.Min(line.X1, line.X2));
|
||||||
|
minY = Math.Min(minY, Math.Min(line.Y1, line.Y2));
|
||||||
|
maxX = Math.Max(maxX, Math.Max(line.X1, line.X2));
|
||||||
|
maxY = Math.Max(maxY, Math.Max(line.Y1, line.Y2));
|
||||||
|
}
|
||||||
|
|
||||||
|
double margin = 20;
|
||||||
|
minX -= margin; minY -= margin; maxX += margin; maxY += margin;
|
||||||
|
double width = Math.Max(1, maxX - minX);
|
||||||
|
double height = Math.Max(1, maxY - minY);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RenderTargetBitmap rtb = new RenderTargetBitmap((int)width, (int)height, 96, 96, PixelFormats.Pbgra32);
|
||||||
|
DrawingVisual dv = new DrawingVisual();
|
||||||
|
using (DrawingContext dc = dv.RenderOpen())
|
||||||
|
{
|
||||||
|
if (whiteBg) dc.DrawRectangle(Brushes.White, null, new Rect(0, 0, width, height));
|
||||||
|
dc.PushTransform(new TranslateTransform(-minX, -minY));
|
||||||
|
|
||||||
|
foreach (var child in DiagramCanvas.Children)
|
||||||
|
{
|
||||||
|
if (child == SelectionRect || !(child is Visual v) || ((UIElement)child).Visibility != Visibility.Visible) continue;
|
||||||
|
|
||||||
|
double left = Canvas.GetLeft((UIElement)child);
|
||||||
|
double top = Canvas.GetTop((UIElement)child);
|
||||||
|
|
||||||
|
if (child is Line line)
|
||||||
|
{
|
||||||
|
dc.DrawLine(new Pen(line.Stroke, line.StrokeThickness) { DashStyle = new DashStyle(line.StrokeDashArray, 0) },
|
||||||
|
new Point(line.X1, line.Y1), new Point(line.X2, line.Y2));
|
||||||
|
}
|
||||||
|
else if (child is FrameworkElement fe)
|
||||||
|
{
|
||||||
|
VisualBrush vb = new VisualBrush(fe) { Stretch = Stretch.None };
|
||||||
|
dc.DrawRectangle(vb, null, new Rect(left, top, fe.ActualWidth, fe.ActualHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rtb.Render(dv);
|
||||||
|
PngBitmapEncoder encoder = new PngBitmapEncoder();
|
||||||
|
encoder.Frames.Add(BitmapFrame.Create(rtb));
|
||||||
|
using (var stream = File.Create(sfd.FileName)) encoder.Save(stream);
|
||||||
|
MessageBox.Show(GetLocalizedString("ExportSuccess"));
|
||||||
|
}
|
||||||
|
catch (Exception ex) { MessageBox.Show($"Export failed: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DiagramSaveModel {
|
||||||
|
public List<PlacedDevice> Devices { get; set; } = new();
|
||||||
|
public List<ConnectionSaveModel> Connections { get; set; } = new();
|
||||||
|
}
|
||||||
|
public class ConnectionSaveModel {
|
||||||
|
public int StartIndex { get; set; }
|
||||||
|
public int EndIndex { get; set; }
|
||||||
|
public ConnectionType Type { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace NetworkDiagram.Models
|
||||||
|
{
|
||||||
|
public enum ConnectionType
|
||||||
|
{
|
||||||
|
Wire,
|
||||||
|
Wifi
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeviceTemplate
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string IconPath { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlacedDevice : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private string _name = string.Empty;
|
||||||
|
private List<string> _ipAddresses = new List<string>();
|
||||||
|
private string _annotation = string.Empty;
|
||||||
|
private double _x;
|
||||||
|
private double _y;
|
||||||
|
|
||||||
|
public string Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set { _name = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string IconPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public double X
|
||||||
|
{
|
||||||
|
get => _x;
|
||||||
|
set { _x = value; OnPropertyChanged(); OnPropertyChanged(nameof(CenterX)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Y
|
||||||
|
{
|
||||||
|
get => _y;
|
||||||
|
set { _y = value; OnPropertyChanged(); OnPropertyChanged(nameof(CenterY)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> IpAddresses
|
||||||
|
{
|
||||||
|
get => _ipAddresses;
|
||||||
|
set { _ipAddresses = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Annotation
|
||||||
|
{
|
||||||
|
get => _annotation;
|
||||||
|
set { _annotation = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public double CenterX => X + 50;
|
||||||
|
public double CenterY => Y + 40;
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Connection
|
||||||
|
{
|
||||||
|
public PlacedDevice StartDevice { get; set; } = null!;
|
||||||
|
public PlacedDevice EndDevice { get; set; } = null!;
|
||||||
|
public ConnectionType Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Diagram
|
||||||
|
{
|
||||||
|
public List<PlacedDevice> Devices { get; set; } = new List<PlacedDevice>();
|
||||||
|
public List<Connection> Connections { get; set; } = new List<Connection>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="devices.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Assets\**">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Name": "Router",
|
||||||
|
"IconPath": "Assets/router.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Switch",
|
||||||
|
"IconPath": "Assets/workgroup switch.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Server",
|
||||||
|
"IconPath": "Assets/fileserver.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Storage server",
|
||||||
|
"IconPath": "Assets/storage server.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Personal Computer",
|
||||||
|
"IconPath": "Assets/workstation.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Access Point",
|
||||||
|
"IconPath": "Assets/accesspoint.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Antenna",
|
||||||
|
"IconPath": "Assets/antenna.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name:": "Cloud",
|
||||||
|
"IconPath": "Assets/cloud.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Dual-Mode AP",
|
||||||
|
"IconPath": "Assets/dual mode ap.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "IP Phone",
|
||||||
|
"IconPath": "Assets/ip phone.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Modem",
|
||||||
|
"IconPath": "Assets/modem.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Printer",
|
||||||
|
"IconPath": "Assets/printer.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Wireless Bridge",
|
||||||
|
"IconPath": "Assets/wireless bridge.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Wireless Router",
|
||||||
|
"IconPath": "Assets/wireless router.jpg"
|
||||||
|
}
|
||||||
|
]
|
||||||