Editor/Prism/Model/Edge.cs

Defines immutable records for graph connections used by the editor: Edge (with ids, endpoints, optional via points and fill) and BrokenEdge with a reason enum for unresolved connections. Provides helpers to create edges, compute PortRef properties, validity checks, touch test, string descriptions and user-facing messages for broken edges.

using Editor.Prism.Core;

namespace Editor.Prism.Model;

/// <summary>
/// A connection between two ports.
/// <para>
/// Prism stores connections as an explicit, id-bearing edge list rather than as a property on the
/// consuming node. That is what makes diffs small, undo cheap, per-edge metadata possible, and
/// dangling connections <em>visible</em> instead of silently dropped on load.
/// </para>
/// </summary>
public sealed record Edge(
	EdgeId Id, NodeId FromNode, PortId FromPort, NodeId ToNode, PortId ToPort, Vector2[] Via )
{
	/// <summary>Build an edge between two port references, minting a fresh id.</summary>
	public static Edge Between( PortRef from, PortRef to ) =>
		new( EdgeId.New(), from.Node, from.Port, to.Node, to.Port, null );

	/// <summary>Build an edge between two port references with an explicit id.</summary>
	public static Edge Between( EdgeId id, PortRef from, PortRef to ) =>
		new( id, from.Node, from.Port, to.Node, to.Port, null );

	/// <summary>The producing end.</summary>
	public PortRef From => new( FromNode, FromPort );

	/// <summary>The consuming end.</summary>
	public PortRef To => new( ToNode, ToPort );

	/// <summary>
	/// Per-edge override for the value written into components invented by a padding conversion.
	/// Null means use <see cref="TypeRules.DefaultFill"/>.
	/// </summary>
	public float? Fill { get; init; }

	/// <summary>True when both ends reference something.</summary>
	public bool IsValid => FromNode.IsValid && FromPort.IsValid && ToNode.IsValid && ToPort.IsValid;

	/// <summary>True when this edge touches the given node at either end.</summary>
	public bool Touches( NodeId node ) => FromNode == node || ToNode == node;

	/// <inheritdoc/>
	public override string ToString() => $"{FromNode}.{FromPort} -> {ToNode}.{ToPort}";
}

/// <summary>Why an edge could not be resolved against the loaded graph.</summary>
public enum BrokenEdgeReason
{
	/// <summary>The producing node is not in the document.</summary>
	MissingFromNode,
	/// <summary>The consuming node is not in the document.</summary>
	MissingToNode,
	/// <summary>The producing node exists but no longer declares that port.</summary>
	MissingFromPort,
	/// <summary>The consuming node exists but no longer declares that port.</summary>
	MissingToPort,
	/// <summary>Both ends exist but the types cannot be connected.</summary>
	TypeMismatch,
	/// <summary>The edge was part of a cycle and had to be cut to compile.</summary>
	Cycle,
	/// <summary>The edge could not be deserialized at all.</summary>
	Malformed
}

/// <summary>
/// A connection that could not be resolved. Prism keeps these — they are drawn as dashed red ghosts
/// with a context menu offering reconnect or delete, and they round-trip through a save. The built-in
/// editor deletes such connections silently, which is how graphs quietly lose work.
/// </summary>
public sealed record BrokenEdge( Edge Edge, BrokenEdgeReason Reason, string Detail )
{
	/// <summary>The edge id, for diagnostics and UI selection.</summary>
	public EdgeId Id => Edge?.Id ?? EdgeId.None;

	/// <summary>A short user-facing explanation.</summary>
	public string Describe() => Reason switch
	{
		BrokenEdgeReason.MissingFromNode => "Source node is missing",
		BrokenEdgeReason.MissingToNode => "Target node is missing",
		BrokenEdgeReason.MissingFromPort => "Source port no longer exists",
		BrokenEdgeReason.MissingToPort => "Target port no longer exists",
		BrokenEdgeReason.TypeMismatch => "Types are not connectable",
		BrokenEdgeReason.Cycle => "Connection would create a cycle",
		_ => "Connection could not be read"
	};

	/// <inheritdoc/>
	public override string ToString() => $"{Edge} ({Reason})";
}