feat: render cross-reference edges as dashed purple arrows on canvas
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,46 @@ function estimateNodeHeight(node: TreeStructure): number {
|
||||
return height
|
||||
}
|
||||
|
||||
/** Collect all node IDs in the tree. */
|
||||
function collectAllIds(root: TreeStructure): Set<string> {
|
||||
const ids = new Set<string>()
|
||||
function walk(node: TreeStructure) {
|
||||
ids.add(node.id)
|
||||
node.children?.forEach(walk)
|
||||
}
|
||||
walk(root)
|
||||
return ids
|
||||
}
|
||||
|
||||
/** Find all cross-reference edges (next_node_id pointing outside children). */
|
||||
function collectCrossRefEdges(root: TreeStructure): Array<{ source: string; target: string; label?: string }> {
|
||||
const refs: Array<{ source: string; target: string; label?: string }> = []
|
||||
const allIds = collectAllIds(root)
|
||||
|
||||
function walk(node: TreeStructure) {
|
||||
const childIds = new Set(node.children?.map(c => c.id) ?? [])
|
||||
|
||||
// Decision options pointing outside children
|
||||
if (node.type === 'decision' && node.options) {
|
||||
for (const opt of node.options) {
|
||||
if (opt.next_node_id && !childIds.has(opt.next_node_id) && allIds.has(opt.next_node_id)) {
|
||||
refs.push({ source: node.id, target: opt.next_node_id, label: opt.label })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action next_node_id pointing to non-child (always a cross-ref since actions use next_node_id not children)
|
||||
if (node.type === 'action' && node.next_node_id && allIds.has(node.next_node_id) && !childIds.has(node.next_node_id)) {
|
||||
refs.push({ source: node.id, target: node.next_node_id, label: 'loops back' })
|
||||
}
|
||||
|
||||
node.children?.forEach(walk)
|
||||
}
|
||||
|
||||
walk(root)
|
||||
return refs
|
||||
}
|
||||
|
||||
interface UseTreeLayoutResult {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
@@ -128,6 +168,40 @@ export function useTreeLayout(): UseTreeLayoutResult {
|
||||
|
||||
walk(treeStructure, null)
|
||||
|
||||
// Add cross-reference edges (dashed, purple)
|
||||
if (treeStructure) {
|
||||
const crossRefs = collectCrossRefEdges(treeStructure)
|
||||
for (const ref of crossRefs) {
|
||||
// Only add if both source and target nodes are visible (not collapsed away)
|
||||
const sourceVisible = nodes.some(n => n.id === ref.source)
|
||||
const targetVisible = nodes.some(n => n.id === ref.target)
|
||||
if (sourceVisible && targetVisible) {
|
||||
edges.push({
|
||||
id: `xref-${ref.source}->${ref.target}`,
|
||||
source: ref.source,
|
||||
target: ref.target,
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
label: ref.label ? truncateLabel(ref.label) : undefined,
|
||||
labelStyle: { fill: 'hsl(var(--primary))', fontSize: 10, fontWeight: 500 },
|
||||
labelBgStyle: { fill: 'hsl(var(--card))', fillOpacity: 0.95 },
|
||||
labelBgPadding: [4, 2] as [number, number],
|
||||
style: {
|
||||
stroke: 'hsl(var(--primary))',
|
||||
strokeWidth: 2,
|
||||
strokeDasharray: '6 3',
|
||||
},
|
||||
markerEnd: {
|
||||
type: 'arrowclosed' as const,
|
||||
color: 'hsl(var(--primary))',
|
||||
width: 16,
|
||||
height: 16,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { rawNodes: nodes, rawEdges: edges }
|
||||
}, [treeStructure, collapsedNodeIds, validationErrors, measuredHeights])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user