Files
zui/frontend/src/lib/graph/graphologyPath.ts
2026-03-14 07:52:34 +01:00

202 lines
6.3 KiB
TypeScript

/**
* Path derivation for connection status using graphology.
* Builds a DirectedGraph from canvas edges (with optional node attributes),
* uses BFS (outbound/inbound) to compute path sets for propagation animation.
*
* @see https://graphology.github.io/
* @see https://graphology.github.io/standard-library/traversal.html
*/
import DirectedGraph from 'graphology'
import { bfsFromNode } from 'graphology-traversal'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export type GraphEdge = { source: string; target: string; id?: string }
/** Optional attributes stored on graphology nodes (e.g. for sink detection). */
export type GraphologyNodeAttributes = {
nodeType?: string
updateMode?: 'auto' | 'manual'
}
/** Optional: pass when building so graph has node type/updateMode for future use. */
export type NodeAttributesMap = Record<string, GraphologyNodeAttributes>
// ---------------------------------------------------------------------------
// Graph build (with cache)
// ---------------------------------------------------------------------------
let cachedEdgesRef: GraphEdge[] | null = null
let cachedGraph: DirectedGraph<GraphologyNodeAttributes, { source: string; target: string; edgeId?: string }> | null =
null
function buildGraph(
edges: GraphEdge[],
nodeAttributes?: NodeAttributesMap
): DirectedGraph<GraphologyNodeAttributes, { source: string; target: string; edgeId?: string }> {
if (edges === cachedEdgesRef && cachedGraph !== null) return cachedGraph
const graph = new DirectedGraph<GraphologyNodeAttributes, { source: string; target: string; edgeId?: string }>()
const nodeIds = new Set<string>()
for (const e of edges) {
nodeIds.add(e.source)
nodeIds.add(e.target)
}
for (const id of nodeIds) {
const attrs: GraphologyNodeAttributes = { ...nodeAttributes?.[id] }
graph.mergeNode(id, attrs)
}
for (const e of edges) {
const key = e.id ?? `${e.source}->${e.target}`
if (!graph.hasEdge(e.source, e.target)) {
graph.addEdgeWithKey(key, e.source, e.target, {
source: e.source,
target: e.target,
edgeId: e.id,
})
}
}
cachedEdgesRef = edges
cachedGraph = graph
return graph
}
/** Call when graph structure changes from outside (e.g. store reset) to clear cache. */
export function clearGraphologyPathCache(): void {
cachedEdgesRef = null
cachedGraph = null
}
// ---------------------------------------------------------------------------
// Traversal helpers (BFS via graphology-traversal)
// ---------------------------------------------------------------------------
/** Outbound BFS: collect all node ids reachable from seeds following edges forward (source → target). */
function getDownstreamNodeIds(
graph: DirectedGraph,
seedIds: string[]
): Set<string> {
const out = new Set<string>()
for (const id of seedIds) {
if (!graph.hasNode(id)) continue
bfsFromNode(
graph,
id,
(node) => {
out.add(node)
return false
},
{ mode: 'outbound' }
)
}
return out
}
/** Inbound BFS: collect all node ids that can reach any seed (following edges backward). */
function getUpstreamNodeIds(
graph: DirectedGraph,
seedIds: string[]
): Set<string> {
const out = new Set<string>()
for (const id of seedIds) {
if (!graph.hasNode(id)) continue
bfsFromNode(
graph,
id,
(node) => {
out.add(node)
return false
},
{ mode: 'inbound' }
)
}
return out
}
// ---------------------------------------------------------------------------
// Path-to-updating / path-to-paused (same semantics as graphPath.ts)
// ---------------------------------------------------------------------------
function getPathToUpdatingNodeIds(
graph: DirectedGraph,
triggerNodeIds: string[],
updatingNodeIds: string[]
): Set<string> {
const downstreamOfTrigger = getDownstreamNodeIds(graph, triggerNodeIds)
const upstreamOfUpdating = getUpstreamNodeIds(graph, updatingNodeIds)
const downstreamOfUpdating = getDownstreamNodeIds(graph, updatingNodeIds)
const path = new Set<string>()
downstreamOfTrigger.forEach((id) => {
if (upstreamOfUpdating.has(id) || downstreamOfUpdating.has(id)) path.add(id)
})
return path
}
function getPathToPausedNodeIds(
graph: DirectedGraph,
triggerNodeIds: string[],
pausedNodeIds: string[]
): Set<string> {
const upstreamOfPaused = getUpstreamNodeIds(graph, pausedNodeIds)
const downstreamOfTrigger = getDownstreamNodeIds(graph, triggerNodeIds)
const path = new Set<string>()
upstreamOfPaused.forEach((id) => {
if (downstreamOfTrigger.has(id)) path.add(id)
})
return path
}
// ---------------------------------------------------------------------------
// Public API (same signatures as graphPath.ts for drop-in replacement)
// ---------------------------------------------------------------------------
/**
* All node ids on the path of an update (downstream of trigger, or path to updating nodes).
*/
export function getPathNodeIds(
edges: GraphEdge[],
updatingNodeIds: string[],
triggerNodeIds?: string[],
_pausedNodeIds?: string[],
nodeAttributes?: NodeAttributesMap
): Set<string> {
const hasUpdating = updatingNodeIds.length > 0
const hasTrigger = triggerNodeIds != null && triggerNodeIds.length > 0
const graph = buildGraph(edges, nodeAttributes)
if (hasTrigger && hasUpdating) {
return getPathToUpdatingNodeIds(graph, triggerNodeIds!, updatingNodeIds)
}
if (hasUpdating) {
const upstream = getUpstreamNodeIds(graph, updatingNodeIds)
const downstream = getDownstreamNodeIds(graph, updatingNodeIds)
const path = new Set<string>(upstream)
downstream.forEach((id) => path.add(id))
return path
}
if (hasTrigger) {
return getDownstreamNodeIds(graph, triggerNodeIds!)
}
return new Set()
}
/**
* Path nodes that show "updating" during the time-bound pulse.
* When pulseActive is true, returns downstream(trigger); otherwise empty.
*/
export function getPathToUpdatingSegmentNodeIds(
edges: GraphEdge[],
triggerNodeIds: string[],
pulseActive: boolean,
nodeAttributes?: NodeAttributesMap
): Set<string> {
if (!pulseActive || triggerNodeIds.length === 0) return new Set()
const graph = buildGraph(edges, nodeAttributes)
return getDownstreamNodeIds(graph, triggerNodeIds)
}