202 lines
6.3 KiB
TypeScript
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)
|
|
}
|