From 32cc53bc5deb71be1bf08c26cc42c918cb627495 Mon Sep 17 00:00:00 2001 From: wnj00524 <68168066+wnj00524@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:36:56 +0100 Subject: [PATCH] Document editor and simulation code Refresh the README to match the current in-app editing workflow and add explanatory comments and XML documentation across the public models, services, and key view-model logic. --- README.md | 26 ++++++++++ src/MedWNetworkSim.App/MainWindow.xaml.cs | 1 + src/MedWNetworkSim.App/Models/EdgeModel.cs | 24 ++++++++++ src/MedWNetworkSim.App/Models/NetworkModel.cs | 18 +++++++ src/MedWNetworkSim.App/Models/NodeModel.cs | 18 +++++++ .../Models/NodeTrafficProfile.cs | 15 ++++++ .../Models/RoutingPreference.cs | 14 ++++++ .../Models/TrafficTypeDefinition.cs | 15 ++++++ .../NodeEditorWindow.xaml.cs | 1 + .../Services/ConsumerCostSummary.cs | 30 ++++++++++++ .../Services/NetworkFileService.cs | 29 +++++++++++ .../Services/NetworkSimulationEngine.cs | 20 ++++++++ .../Services/RouteAllocation.cs | 48 +++++++++++++++++++ .../Services/TrafficSimulationOutcome.cs | 30 ++++++++++++ .../ViewModels/EdgeViewModel.cs | 2 + .../ViewModels/MainWindowViewModel.cs | 4 ++ .../ViewModels/NodeViewModel.cs | 2 + 17 files changed, 297 insertions(+) diff --git a/README.md b/README.md index ab84cba..d2e66f0 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ WPF network simulator for modelling multi-traffic movement across producer, cons ## What It Does - Loads a JSON network file. +- Lets users create a new network and edit traffic types, nodes, node roles, and edges directly in the app. - Draws the network on a draggable canvas. - Auto-positions nodes when `x` and `y` are omitted from the input file. - Includes an `Auto Arrange` action to regenerate node positions for the whole network. @@ -18,6 +19,20 @@ WPF network simulator for modelling multi-traffic movement across producer, cons - Simulates routed movements from producers to consumers through valid transhipment nodes. - Saves the current network, including updated node positions, back to JSON. +## Editing In App + +- Use `New Network` to start from an empty model. +- Maintain traffic types in the `Network Editor` tab, including routing preference and optional `capacityBidPerUnit`. +- Add and remove nodes in the main editor grid. +- Open `Open Node Editor...` to edit one node in a dedicated window. +- In the node editor, choose the node, then choose one of its traffic-role entries, then set: + - `Traffic Type` + - `Role` + - `Production` + - `Consumption` +- Add and remove edges in the `Edges` grid. `From` and `To` are chosen from the existing node list rather than typed freehand. +- Drag nodes on the canvas to refine the layout visually, or use `Auto Arrange` to regenerate positions. + ## Run It ```powershell @@ -95,3 +110,14 @@ The app uses a simple custom JSON format: - Omit `capacity` on an edge when you want it to behave as unlimited. - The consumer-cost view shows local and imported movement costs separately, plus the blended movement cost seen at each consumer node. - Routing is path-based and allocates producer supply to consumer demand using the best available routes under the chosen traffic preference and capacity bidding. +- `Auto Arrange` only updates node positions. It does not throw away in-memory edits to nodes, roles, or traffic types. + +## Code Structure + +- [MainWindow.xaml](/C:/Users/jdwil/source/repos/Codex/MedWNetworkSim/src/MedWNetworkSim.App/MainWindow.xaml) defines the main shell: canvas, summary panes, simulation results, and the in-app editor grids. +- [MainWindowViewModel.cs](/C:/Users/jdwil/source/repos/Codex/MedWNetworkSim/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs) is the application coordinator. It loads/saves networks, keeps editor selections in sync, and triggers simulation. +- [NodeEditorWindow.xaml](/C:/Users/jdwil/source/repos/Codex/MedWNetworkSim/src/MedWNetworkSim.App/NodeEditorWindow.xaml) provides the dedicated dropdown-driven node editing workflow. +- [NetworkFileService.cs](/C:/Users/jdwil/source/repos/Codex/MedWNetworkSim/src/MedWNetworkSim.App/Services/NetworkFileService.cs) normalizes and validates JSON data and applies automatic layout. +- [NetworkSimulationEngine.cs](/C:/Users/jdwil/source/repos/Codex/MedWNetworkSim/src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs) performs routing, capacity competition, bid-cost calculation, and consumer-cost summarization. +- The `Models` folder contains the persisted JSON shape. +- The `ViewModels` folder contains the editable UI state and display helpers used by WPF binding. diff --git a/src/MedWNetworkSim.App/MainWindow.xaml.cs b/src/MedWNetworkSim.App/MainWindow.xaml.cs index 15607ed..7945f9a 100644 --- a/src/MedWNetworkSim.App/MainWindow.xaml.cs +++ b/src/MedWNetworkSim.App/MainWindow.xaml.cs @@ -90,6 +90,7 @@ private void AddNode_Click(object sender, RoutedEventArgs e) private void EditSelectedNode_Click(object sender, RoutedEventArgs e) { + // The dedicated window keeps node-role editing simpler than trying to fit every selector into the main pane. var window = new NodeEditorWindow(ViewModel) { Owner = this diff --git a/src/MedWNetworkSim.App/Models/EdgeModel.cs b/src/MedWNetworkSim.App/Models/EdgeModel.cs index 6a95515..58a700e 100644 --- a/src/MedWNetworkSim.App/Models/EdgeModel.cs +++ b/src/MedWNetworkSim.App/Models/EdgeModel.cs @@ -1,18 +1,42 @@ namespace MedWNetworkSim.App.Models; +/// +/// Represents a connection between two nodes, including routing attributes and optional capacity. +/// public sealed class EdgeModel { + /// + /// Gets or sets the unique identifier for the edge. + /// public string Id { get; set; } = string.Empty; + /// + /// Gets or sets the source node identifier. + /// public string FromNodeId { get; set; } = string.Empty; + /// + /// Gets or sets the destination node identifier. + /// public string ToNodeId { get; set; } = string.Empty; + /// + /// Gets or sets the time cost used when traffic prioritizes speed or total cost. + /// public double Time { get; set; } + /// + /// Gets or sets the monetary or general routing cost used when traffic prioritizes cost or total cost. + /// public double Cost { get; set; } + /// + /// Gets or sets the optional shared capacity of the edge. Null means unlimited capacity. + /// public double? Capacity { get; set; } + /// + /// Gets or sets a value indicating whether traffic can travel in both directions on this edge. + /// public bool IsBidirectional { get; set; } = true; } diff --git a/src/MedWNetworkSim.App/Models/NetworkModel.cs b/src/MedWNetworkSim.App/Models/NetworkModel.cs index 1268dc2..019e631 100644 --- a/src/MedWNetworkSim.App/Models/NetworkModel.cs +++ b/src/MedWNetworkSim.App/Models/NetworkModel.cs @@ -2,15 +2,33 @@ namespace MedWNetworkSim.App.Models; +/// +/// Represents a complete persisted network, including traffic-type definitions, nodes, and edges. +/// public sealed class NetworkModel { + /// + /// Gets or sets the display name of the network. + /// public string Name { get; set; } = "Untitled Network"; + /// + /// Gets or sets an optional free-text description shown in the editor. + /// public string Description { get; set; } = string.Empty; + /// + /// Gets or sets the declared traffic types that can move through the network. + /// public List TrafficTypes { get; set; } = []; + /// + /// Gets or sets the nodes that can produce, consume, or transship traffic. + /// public List Nodes { get; set; } = []; + /// + /// Gets or sets the edges that connect nodes and define time, cost, and optional capacity. + /// public List Edges { get; set; } = []; } diff --git a/src/MedWNetworkSim.App/Models/NodeModel.cs b/src/MedWNetworkSim.App/Models/NodeModel.cs index 1e7e5ef..76751e8 100644 --- a/src/MedWNetworkSim.App/Models/NodeModel.cs +++ b/src/MedWNetworkSim.App/Models/NodeModel.cs @@ -2,15 +2,33 @@ namespace MedWNetworkSim.App.Models; +/// +/// Represents a node in the network graph. +/// public sealed class NodeModel { + /// + /// Gets or sets the unique identifier used by edges to reference this node. + /// public string Id { get; set; } = string.Empty; + /// + /// Gets or sets the user-facing name shown on the canvas and in editors. + /// public string Name { get; set; } = string.Empty; + /// + /// Gets or sets the optional horizontal canvas position of the node center. + /// public double? X { get; set; } + /// + /// Gets or sets the optional vertical canvas position of the node center. + /// public double? Y { get; set; } + /// + /// Gets or sets the per-traffic roles and quantities for this node. + /// public List TrafficProfiles { get; set; } = []; } diff --git a/src/MedWNetworkSim.App/Models/NodeTrafficProfile.cs b/src/MedWNetworkSim.App/Models/NodeTrafficProfile.cs index bc57d9c..e36d532 100644 --- a/src/MedWNetworkSim.App/Models/NodeTrafficProfile.cs +++ b/src/MedWNetworkSim.App/Models/NodeTrafficProfile.cs @@ -1,12 +1,27 @@ namespace MedWNetworkSim.App.Models; +/// +/// Describes how a single node participates in one traffic type. +/// public sealed class NodeTrafficProfile { + /// + /// Gets or sets the traffic type this profile applies to. + /// public string TrafficType { get; set; } = string.Empty; + /// + /// Gets or sets the amount of this traffic type produced at the node. + /// public double Production { get; set; } + /// + /// Gets or sets the amount of this traffic type consumed at the node. + /// public double Consumption { get; set; } + /// + /// Gets or sets a value indicating whether this node may be used as an intermediate transhipment point. + /// public bool CanTransship { get; set; } } diff --git a/src/MedWNetworkSim.App/Models/RoutingPreference.cs b/src/MedWNetworkSim.App/Models/RoutingPreference.cs index bae7912..27f35dd 100644 --- a/src/MedWNetworkSim.App/Models/RoutingPreference.cs +++ b/src/MedWNetworkSim.App/Models/RoutingPreference.cs @@ -1,8 +1,22 @@ namespace MedWNetworkSim.App.Models; +/// +/// Describes how a traffic type scores alternative routes. +/// public enum RoutingPreference { + /// + /// Prefer routes with the lowest total edge time. + /// Speed, + + /// + /// Prefer routes with the lowest total edge cost. + /// Cost, + + /// + /// Prefer routes with the lowest combined time plus cost score. + /// TotalCost } diff --git a/src/MedWNetworkSim.App/Models/TrafficTypeDefinition.cs b/src/MedWNetworkSim.App/Models/TrafficTypeDefinition.cs index b9fe794..b85a825 100644 --- a/src/MedWNetworkSim.App/Models/TrafficTypeDefinition.cs +++ b/src/MedWNetworkSim.App/Models/TrafficTypeDefinition.cs @@ -1,12 +1,27 @@ namespace MedWNetworkSim.App.Models; +/// +/// Defines how one traffic type behaves when the simulator routes it through the network. +/// public sealed class TrafficTypeDefinition { + /// + /// Gets or sets the name of the traffic type. + /// public string Name { get; set; } = string.Empty; + /// + /// Gets or sets an optional description shown in the editor. + /// public string Description { get; set; } = string.Empty; + /// + /// Gets or sets the route-scoring preference for this traffic type. + /// public RoutingPreference RoutingPreference { get; set; } = RoutingPreference.TotalCost; + /// + /// Gets or sets the optional per-unit bid used when competing for constrained edge capacity. + /// public double? CapacityBidPerUnit { get; set; } } diff --git a/src/MedWNetworkSim.App/NodeEditorWindow.xaml.cs b/src/MedWNetworkSim.App/NodeEditorWindow.xaml.cs index d6b51bd..8cb2373 100644 --- a/src/MedWNetworkSim.App/NodeEditorWindow.xaml.cs +++ b/src/MedWNetworkSim.App/NodeEditorWindow.xaml.cs @@ -9,6 +9,7 @@ public NodeEditorWindow(MainWindowViewModel viewModel) { InitializeComponent(); ViewModel = viewModel; + // The window edits the same shared view model as the main screen, so changes are live immediately. DataContext = ViewModel; } diff --git a/src/MedWNetworkSim.App/Services/ConsumerCostSummary.cs b/src/MedWNetworkSim.App/Services/ConsumerCostSummary.cs index e0fd133..433bfde 100644 --- a/src/MedWNetworkSim.App/Services/ConsumerCostSummary.cs +++ b/src/MedWNetworkSim.App/Services/ConsumerCostSummary.cs @@ -1,22 +1,52 @@ namespace MedWNetworkSim.App.Services; +/// +/// Summarizes the delivered movement cost seen by a consumer node for one traffic type. +/// public sealed class ConsumerCostSummary { + /// + /// Gets the traffic type summarized by this row. + /// public string TrafficType { get; init; } = string.Empty; + /// + /// Gets the consumer node identifier. + /// public string ConsumerNodeId { get; init; } = string.Empty; + /// + /// Gets the consumer node name. + /// public string ConsumerName { get; init; } = string.Empty; + /// + /// Gets the quantity satisfied by same-node local production. + /// public double LocalQuantity { get; init; } + /// + /// Gets the average movement cost per unit for locally satisfied demand. + /// public double LocalUnitCost { get; init; } + /// + /// Gets the quantity satisfied by imported flow from other nodes. + /// public double ImportedQuantity { get; init; } + /// + /// Gets the average movement cost per unit for imported flow. + /// public double ImportedUnitCost { get; init; } + /// + /// Gets the average movement cost per unit across local and imported supply together. + /// public double BlendedUnitCost { get; init; } + /// + /// Gets the total movement cost accumulated by all delivered supply in this summary row. + /// public double TotalMovementCost { get; init; } } diff --git a/src/MedWNetworkSim.App/Services/NetworkFileService.cs b/src/MedWNetworkSim.App/Services/NetworkFileService.cs index ebb83f5..e38c213 100644 --- a/src/MedWNetworkSim.App/Services/NetworkFileService.cs +++ b/src/MedWNetworkSim.App/Services/NetworkFileService.cs @@ -5,6 +5,9 @@ namespace MedWNetworkSim.App.Services; +/// +/// Loads, saves, normalizes, validates, and optionally auto-lays out network files. +/// public sealed class NetworkFileService { private static readonly StringComparer Comparer = StringComparer.OrdinalIgnoreCase; @@ -16,6 +19,11 @@ public sealed class NetworkFileService Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } }; + /// + /// Loads a network file from disk and returns a normalized, validated model. + /// + /// The JSON file to load. + /// The normalized network model. public NetworkModel Load(string path) { var json = File.ReadAllText(path); @@ -25,6 +33,11 @@ public NetworkModel Load(string path) return NormalizeAndValidate(model); } + /// + /// Saves a network model to disk after normalizing and validating it. + /// + /// The network model to persist. + /// The destination JSON file path. public void Save(NetworkModel model, string path) { var normalized = NormalizeAndValidate(model); @@ -32,11 +45,21 @@ public void Save(NetworkModel model, string path) File.WriteAllText(path, json); } + /// + /// Recomputes coordinates for every node in the supplied model. + /// + /// The network model to arrange. + /// A normalized model with fresh node coordinates. public NetworkModel AutoArrange(NetworkModel model) { return NormalizeAndValidate(model, forceLayoutAllNodes: true); } + /// + /// Normalizes and validates a network model without forcing a full re-layout of every node. + /// + /// The network model to check. + /// The normalized network model. public NetworkModel NormalizeAndValidate(NetworkModel model) { return NormalizeAndValidate(model, forceLayoutAllNodes: false); @@ -46,6 +69,7 @@ private NetworkModel NormalizeAndValidate(NetworkModel model, bool forceLayoutAl { ArgumentNullException.ThrowIfNull(model); + // Rebuild the model into a predictable, validated shape before either rendering or saving it. var normalizedNodes = new List(); var nodeIds = new HashSet(Comparer); @@ -137,6 +161,7 @@ private NetworkModel NormalizeAndValidate(NetworkModel model, bool forceLayoutAl private static List NormalizeProfiles(IEnumerable? profiles) { + // Duplicate traffic rows on the same node are collapsed into one persisted profile per traffic type. return (profiles ?? []) .Where(profile => !string.IsNullOrWhiteSpace(profile.TrafficType)) .GroupBy(profile => profile.TrafficType.Trim(), Comparer) @@ -181,6 +206,7 @@ private static List NormalizeTrafficDefinitions( }; } + // Traffic types referenced by nodes are back-filled even if the file omits an explicit definition. foreach (var trafficName in nodes .SelectMany(node => node.TrafficProfiles) .Select(profile => profile.TrafficType) @@ -213,6 +239,7 @@ private static void ApplyAutomaticLayout( if (forceLayoutAllNodes) { + // Auto Arrange deliberately relays out every node, even if it already has saved coordinates. ApplyRoleBasedLayout(nodes, edges); return; } @@ -237,6 +264,7 @@ private static void ApplyAutomaticLayout( private static void ApplyRoleBasedLayout(IList nodes, IReadOnlyList edges) { + // Producers trend left, consumers trend right, and transhipment-capable nodes sit in the middle layer. var degreeByNodeId = BuildDegreeLookup(edges); var layers = nodes .GroupBy(GetLayoutLayer) @@ -272,6 +300,7 @@ private static void ApplySupplementalLayout( IReadOnlyList edges, IReadOnlyList nodesMissingCoordinates) { + // When only some nodes are missing positions, preserve the explicit coordinates and append the rest nearby. var explicitNodes = nodes .Where(node => node.X.HasValue && node.Y.HasValue) .ToList(); diff --git a/src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs b/src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs index 8b2128d..d6d46fa 100644 --- a/src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs +++ b/src/MedWNetworkSim.App/Services/NetworkSimulationEngine.cs @@ -2,15 +2,24 @@ namespace MedWNetworkSim.App.Services; +/// +/// Simulates movement through a network, including route scoring, capacity sharing, and bid competition. +/// public sealed class NetworkSimulationEngine { private const double Epsilon = 0.000001d; private static readonly StringComparer Comparer = StringComparer.OrdinalIgnoreCase; + /// + /// Runs the routing simulation for every traffic type present in the network. + /// + /// The network to simulate. + /// The per-traffic routing outcomes. public IReadOnlyList Simulate(NetworkModel network) { ArgumentNullException.ThrowIfNull(network); + // Build shared graph data once, then route every traffic type against the same pool of edge capacity. var adjacency = BuildAdjacency(network); var definitionsByTraffic = network.TrafficTypes .ToDictionary(definition => definition.Name, definition => definition, Comparer); @@ -45,11 +54,13 @@ public IReadOnlyList Simulate(NetworkModel network) context.Notes.Add("No consumer nodes were defined for this traffic type."); } + // Same-node supply satisfies same-node demand before any network routing is attempted. ApplyLocalAllocations(context); } while (true) { + // Capacity bidding is resolved globally: every traffic type competes for the next best route. var nextCandidate = contexts .SelectMany(context => BuildCandidateRoutes(context, adjacency, remainingCapacityByEdgeId)) .OrderByDescending(candidate => candidate.CapacityBidPerUnit) @@ -161,6 +172,11 @@ public IReadOnlyList Simulate(NetworkModel network) .ToList(); } + /// + /// Aggregates route allocations into landed-cost summaries for each consumer node and traffic type. + /// + /// The traffic outcomes produced by . + /// The consumer cost summaries. public IReadOnlyList SummarizeConsumerCosts(IEnumerable outcomes) { return outcomes @@ -206,6 +222,7 @@ private static double CalculateAverageUnitCost(IReadOnlyCollection node.Id, node => node.TrafficProfiles.FirstOrDefault(profile => Comparer.Equals(profile.TrafficType, definition.Name)), @@ -307,6 +324,7 @@ private static List BuildCandidateRoutes( IReadOnlyDictionary> adjacency, IDictionary remainingCapacityByEdgeId) { + // A Dijkstra pass finds the best currently-feasible route under this traffic type's scoring rule. var distances = new Dictionary(Comparer) { [producerNodeId] = 0d @@ -402,6 +420,7 @@ private static bool CanTraverseNode( return true; } + // Intermediate nodes must explicitly allow transhipment for the current traffic type. return profilesByNodeId.TryGetValue(nodeId, out var profile) && profile?.CanTransship == true; } @@ -454,6 +473,7 @@ private static double CalculateBidCostPerUnit( double quantity, double routeCapacity) { + // Bid cost is only charged when the chosen movement fully consumes a finite bottleneck on the route. if (capacityBidPerUnit <= Epsilon || quantity <= Epsilon || double.IsPositiveInfinity(routeCapacity) || diff --git a/src/MedWNetworkSim.App/Services/RouteAllocation.cs b/src/MedWNetworkSim.App/Services/RouteAllocation.cs index 888c675..c088c9d 100644 --- a/src/MedWNetworkSim.App/Services/RouteAllocation.cs +++ b/src/MedWNetworkSim.App/Services/RouteAllocation.cs @@ -3,35 +3,83 @@ namespace MedWNetworkSim.App.Services; +/// +/// Describes one simulated movement allocation from a producer node to a consumer node. +/// public sealed class RouteAllocation { + /// + /// Gets the traffic type moved by this allocation. + /// public string TrafficType { get; init; } = string.Empty; + /// + /// Gets the route-scoring preference that selected this path. + /// public RoutingPreference RoutingPreference { get; init; } + /// + /// Gets the producing node identifier. + /// public string ProducerNodeId { get; init; } = string.Empty; + /// + /// Gets the producing node name. + /// public string ProducerName { get; init; } = string.Empty; + /// + /// Gets the consuming node identifier. + /// public string ConsumerNodeId { get; init; } = string.Empty; + /// + /// Gets the consuming node name. + /// public string ConsumerName { get; init; } = string.Empty; + /// + /// Gets the quantity delivered by this allocation. + /// public double Quantity { get; init; } + /// + /// Gets a value indicating whether the quantity was satisfied locally on the same node. + /// public bool IsLocalSupply { get; init; } + /// + /// Gets the total edge time across the chosen route. + /// public double TotalTime { get; init; } + /// + /// Gets the transit-only cost per unit across the chosen route. + /// public double TotalCost { get; init; } + /// + /// Gets the additional per-unit cost caused by capacity bidding. + /// public double BidCostPerUnit { get; init; } + /// + /// Gets the full per-unit delivered movement cost, including any bid premium. + /// public double DeliveredCostPerUnit { get; init; } + /// + /// Gets the total movement cost for the full delivered quantity. + /// public double TotalMovementCost { get; init; } + /// + /// Gets the route score used for path comparison under the active routing preference. + /// public double TotalScore { get; init; } + /// + /// Gets the ordered node names visited by the movement path. + /// public IReadOnlyList PathNodeNames { get; init; } = []; } diff --git a/src/MedWNetworkSim.App/Services/TrafficSimulationOutcome.cs b/src/MedWNetworkSim.App/Services/TrafficSimulationOutcome.cs index 407a9c3..fbfccdd 100644 --- a/src/MedWNetworkSim.App/Services/TrafficSimulationOutcome.cs +++ b/src/MedWNetworkSim.App/Services/TrafficSimulationOutcome.cs @@ -3,23 +3,53 @@ namespace MedWNetworkSim.App.Services; +/// +/// Captures the full simulation result for one traffic type. +/// public sealed class TrafficSimulationOutcome { + /// + /// Gets the traffic type summarized by this outcome. + /// public string TrafficType { get; init; } = string.Empty; + /// + /// Gets the route-scoring preference used for this traffic type. + /// public RoutingPreference RoutingPreference { get; init; } + /// + /// Gets the total available production for this traffic type. + /// public double TotalProduction { get; init; } + /// + /// Gets the total requested consumption for this traffic type. + /// public double TotalConsumption { get; init; } + /// + /// Gets the total quantity successfully delivered. + /// public double TotalDelivered { get; init; } + /// + /// Gets the production quantity left unused after routing. + /// public double UnusedSupply { get; init; } + /// + /// Gets the demand quantity left unmet after routing. + /// public double UnmetDemand { get; init; } + /// + /// Gets the detailed local and routed allocations that make up the outcome. + /// public IReadOnlyList Allocations { get; init; } = []; + /// + /// Gets informational notes describing notable routing conditions or constraints. + /// public IReadOnlyList Notes { get; init; } = []; } diff --git a/src/MedWNetworkSim.App/ViewModels/EdgeViewModel.cs b/src/MedWNetworkSim.App/ViewModels/EdgeViewModel.cs index e9f0e38..61f6dac 100644 --- a/src/MedWNetworkSim.App/ViewModels/EdgeViewModel.cs +++ b/src/MedWNetworkSim.App/ViewModels/EdgeViewModel.cs @@ -191,6 +191,7 @@ public string ArrowPoints const double arrowLength = 18d; const double arrowWidth = 8d; + // Build the arrowhead from the edge direction vector so it tracks node dragging automatically. var baseX = end.X - (ux * arrowLength); var baseY = end.Y - (uy * arrowLength); @@ -273,6 +274,7 @@ private void UpdateResolvedNodes(NodeViewModel? newSourceNode, NodeViewModel? ne var source = new Point(sourceNode.CenterX, sourceNode.CenterY); var target = new Point(targetNode.CenterX, targetNode.CenterY); + // Edges connect to the border of each node card rather than the center to keep the canvas readable. var outbound = FindRectangleIntersection(source, target, sourceNode.Width / 2d, sourceNode.Height / 2d); var inbound = FindRectangleIntersection(target, source, targetNode.Width / 2d, targetNode.Height / 2d); return (outbound, inbound); diff --git a/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs b/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs index 4e13c96..1f01b55 100644 --- a/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs +++ b/src/MedWNetworkSim.App/ViewModels/MainWindowViewModel.cs @@ -172,6 +172,7 @@ public NodeTrafficProfileViewModel? SelectedNodeTrafficProfile selectedNodeTrafficProfile.PropertyChanged += HandleSelectedNodeTrafficProfilePropertyChanged; } + // The node editor binds through these proxy properties so profile swaps do not confuse nested WPF bindings. RaiseSelectedNodeTrafficEditorPropertiesChanged(); } } @@ -382,6 +383,7 @@ public void AutoArrangeNodes() var arranged = fileService.AutoArrange(current); var arrangedNodesById = arranged.Nodes.ToDictionary(node => node.Id, Comparer); + // Keep the existing node view models and only update coordinates so in-memory edits are preserved. foreach (var node in Nodes) { if (!arrangedNodesById.TryGetValue(node.Id, out var arrangedNode)) @@ -769,6 +771,7 @@ private void HandleNodeDefinitionChanged(object? sender, EventArgs e) if (!isNormalizingNodeTrafficProfiles && sender is NodeViewModel node) { + // Editing can temporarily create duplicate traffic rows; fold them back into one profile per traffic type. NormalizeNodeTrafficProfiles(node); } @@ -829,6 +832,7 @@ private void HandleTrafficDefinitionNameChanged(object? sender, ValueChangedEven private void RefreshDerivedStateAfterStructureChange(string message) { + // Centralize all the "network shape changed" refresh work so the UI stays consistent after edits. RefreshNodeIdOptions(); RefreshTrafficTypeNameOptions(); RefreshEdgeBindings(); diff --git a/src/MedWNetworkSim.App/ViewModels/NodeViewModel.cs b/src/MedWNetworkSim.App/ViewModels/NodeViewModel.cs index 7af7450..46b079e 100644 --- a/src/MedWNetworkSim.App/ViewModels/NodeViewModel.cs +++ b/src/MedWNetworkSim.App/ViewModels/NodeViewModel.cs @@ -24,6 +24,7 @@ public NodeViewModel(NodeModel model) model.TrafficProfiles.Select(profile => new NodeTrafficProfileViewModel(profile))); TrafficProfiles.CollectionChanged += HandleTrafficProfilesChanged; + // Bubble traffic-profile edits up as node-definition changes so the rest of the UI can refresh once. foreach (var profile in TrafficProfiles) { profile.PropertyChanged += HandleTrafficProfilePropertyChanged; @@ -134,6 +135,7 @@ public void RemoveTrafficProfile(NodeTrafficProfileViewModel profile) public void MoveBy(double deltaX, double deltaY) { + // Keep the node on the positive canvas while preserving drag semantics from the node center. X = Math.Max(Width / 2d, X + deltaX); Y = Math.Max(Height / 2d, Y + deltaY); }