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);
}