feat(model): project the model into a Vue Flow graph

This commit is contained in:
Julien Calixte
2026-06-20 09:37:19 +02:00
parent 95990b0d1f
commit 8fac6f4694

98
src/model/projection.ts Normal file
View File

@@ -0,0 +1,98 @@
/**
* Model → Vue Flow projection (C6, ADR-0002).
*
* One-directional and pure: the domain Model in, the `{ nodes, edges }` Vue Flow
* renders out. Writes never flow this way — Vue Flow events call store actions
* that mutate the Model, and this re-derives. Keeping it pure means it is cheap
* to memoise (the F3 escape hatch) and trivial to reason about.
*
* Edges are derived, never stored. A Flow node contributes two "pipe" edges
* (source→valve, valve→target) so the classic pipe-with-valve renders from the
* Flow's references (ADR-0003); each Information Link contributes one "info"
* edge carrying its polarity.
*/
import type { Edge, Node } from "@vue-flow/core"
import type { Model, ModelNode, Polarity } from "./types"
/** Payload carried on every projected Vue Flow node: the domain node itself. */
export interface NodeData {
node: ModelNode
}
/** Payload on a projected edge — which kind, and (for info links) its polarity. */
export interface EdgeData {
kind: "pipe" | "info"
polarity?: Polarity
}
export type FlowGraphNode = Node<NodeData>
export type FlowGraphEdge = Edge<EdgeData>
/**
* Stable handle ids every node renders (target on the left, source on the
* right). Edges name them explicitly so Vue Flow always resolves the right
* anchor — without ids, an edge whose source is the small two-handle valve fails
* to render (the "pipe-out" bug).
*/
export const HANDLE_IN = "in"
export const HANDLE_OUT = "out"
export function projectNodes(model: Model): FlowGraphNode[] {
return model.nodes.map((node) => ({
id: node.id,
type: node.kind,
position: node.position,
data: { node },
}))
}
export function projectEdges(model: Model): FlowGraphEdge[] {
const edges: FlowGraphEdge[] = []
for (const node of model.nodes) {
if (node.kind !== "flow") continue
// Pipe in: source → valve. No arrowhead — the valve is the visual midpoint.
edges.push({
id: `${node.id}::in`,
source: node.source,
sourceHandle: HANDLE_OUT,
target: node.id,
targetHandle: HANDLE_IN,
data: { kind: "pipe" },
style: { strokeWidth: "2.5px" },
})
// Pipe out: valve → target. Arrowhead carries the flow direction.
edges.push({
id: `${node.id}::out`,
source: node.id,
sourceHandle: HANDLE_OUT,
target: node.target,
targetHandle: HANDLE_IN,
data: { kind: "pipe" },
style: { strokeWidth: "2.5px" },
markerEnd: "arrowclosed",
})
}
// Information Links: thin, dashed, arrow at the influenced end. The custom
// "info" edge type renders the dashed pipe plus the clickable polarity badge.
for (const link of model.infoLinks) {
edges.push({
id: link.id,
type: "info",
source: link.source,
sourceHandle: HANDLE_OUT,
target: link.target,
targetHandle: HANDLE_IN,
data: { kind: "info", polarity: link.polarity },
style: { strokeDasharray: "5 4" },
markerEnd: "arrowclosed",
})
}
return edges
}
export function project(model: Model): { nodes: FlowGraphNode[]; edges: FlowGraphEdge[] } {
return { nodes: projectNodes(model), edges: projectEdges(model) }
}