feat(model): project the model into a Vue Flow graph
This commit is contained in:
98
src/model/projection.ts
Normal file
98
src/model/projection.ts
Normal 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) }
|
||||
}
|
||||
Reference in New Issue
Block a user