Skip to content

Commit 8ffe63f

Browse files
authored
Layoutstore Minimap calculation (#5547)
This pull request refactors the minimap rendering system to use a unified, extensible data source abstraction for all minimap operations. By introducing a data source interface and factory, the minimap can now seamlessly support multiple sources of node layout (such as the `LayoutStore` or the underlying `LiteGraph`), improving maintainability and future extensibility. Rendering logic and change detection throughout the minimap have been updated to use this new abstraction, resulting in cleaner code and easier support for new data models. **Core architecture improvements:** * Introduced a new `IMinimapDataSource` interface and related data types (`MinimapNodeData`, `MinimapLinkData`, `MinimapGroupData`) to standardize node, link, and group data for minimap rendering. * Added an abstract base class `AbstractMinimapDataSource` that provides shared logic for bounds and group/link extraction, and implemented two concrete data sources: `LiteGraphDataSource` (for classic graph data) and `LayoutStoreDataSource` (for layout store data). [[1]](diffhunk://#diff-ea46218fc9ffced84168a5ff975e4a30e43f7bf134ee8f02ed2eae66efbb729dR1-R95) [[2]](diffhunk://#diff-9a6b7c6be25b4dbeb358fea18f3a21e78797058ccc86c818ed1e5f69c7355273R1-R30) [[3]](diffhunk://#diff-f200ba9495a03157198abff808ed6c3761746071404a52adbad98f6a9d01249bR1-R42) * Created a `MinimapDataSourceFactory` that selects the appropriate data source based on the presence of layout store data, enabling seamless switching between data models. **Minimap rendering and logic refactoring:** * Updated all minimap rendering functions (`renderGroups`, `renderNodes`, `renderConnections`) and the main `renderMinimapToCanvas` entry point to use the unified data source interface, significantly simplifying the rendering code and decoupling it from the underlying graph structure. [[1]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L1-R11) [[2]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121R33-R75) [[3]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L66-R124) [[4]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L134-R161) [[5]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L153-R187) [[6]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L187-L188) [[7]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121R227-R231) [[8]](diffhunk://#diff-3670f99330b2e24aca3cffeeac6600adf8abadd6dd585f596d60fde1dd093121L230-R248) * Refactored minimap viewport and graph change detection logic to use the data source abstraction for bounds, node, and link change detection, and to respond to layout store version changes. [[1]](diffhunk://#diff-d92e448dee5e30782a66b9e66d8c8b05626dffd0b2ff1032f2612b9a9b9c51f6L2-R10) [[2]](diffhunk://#diff-d92e448dee5e30782a66b9e66d8c8b05626dffd0b2ff1032f2612b9a9b9c51f6R33-R35) [[3]](diffhunk://#diff-d92e448dee5e30782a66b9e66d8c8b05626dffd0b2ff1032f2612b9a9b9c51f6L99-R141) [[4]](diffhunk://#diff-d92e448dee5e30782a66b9e66d8c8b05626dffd0b2ff1032f2612b9a9b9c51f6R157-R160) [[5]](diffhunk://#diff-338d14c67dabffaf6f68fbf09b16e8d67bead2b9df340e46601b2fbd57331521L8-R11) [[6]](diffhunk://#diff-338d14c67dabffaf6f68fbf09b16e8d67bead2b9df340e46601b2fbd57331521L56-R64) These changes make the minimap codebase more modular and robust, and lay the groundwork for supporting additional node layout strategies in the future. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5547-Layoutstore-Minimap-calculation-26e6d73d3650813e9457c051dff41ca1) by [Unito](https://www.unito.io)
1 parent 893409d commit 8ffe63f

File tree

9 files changed

+530
-93
lines changed

9 files changed

+530
-93
lines changed

src/renderer/extensions/minimap/composables/useMinimapGraph.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useThrottleFn } from '@vueuse/core'
2-
import { ref } from 'vue'
2+
import { ref, watch } from 'vue'
33
import type { Ref } from 'vue'
44

55
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
66
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
7+
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
78
import { api } from '@/scripts/api'
89

10+
import { MinimapDataSourceFactory } from '../data/MinimapDataSourceFactory'
911
import type { UpdateFlags } from '../types'
1012

1113
interface GraphCallbacks {
@@ -28,6 +30,9 @@ export function useMinimapGraph(
2830
viewport: false
2931
})
3032

33+
// Track LayoutStore version for change detection
34+
const layoutStoreVersion = layoutStore.getVersion()
35+
3136
// Map to store original callbacks per graph ID
3237
const originalCallbacksMap = new Map<string, GraphCallbacks>()
3338

@@ -96,35 +101,44 @@ export function useMinimapGraph(
96101
let positionChanged = false
97102
let connectionChanged = false
98103

99-
if (g._nodes.length !== lastNodeCount.value) {
104+
// Use unified data source for change detection
105+
const dataSource = MinimapDataSourceFactory.create(g)
106+
107+
// Check for node count changes
108+
const currentNodeCount = dataSource.getNodeCount()
109+
if (currentNodeCount !== lastNodeCount.value) {
100110
structureChanged = true
101-
lastNodeCount.value = g._nodes.length
111+
lastNodeCount.value = currentNodeCount
102112
}
103113

104-
for (const node of g._nodes) {
105-
const key = node.id
106-
const currentState = `${node.pos[0]},${node.pos[1]},${node.size[0]},${node.size[1]}`
114+
// Check for node position/size changes
115+
const nodes = dataSource.getNodes()
116+
for (const node of nodes) {
117+
const nodeId = node.id
118+
const currentState = `${node.x},${node.y},${node.width},${node.height}`
107119

108-
if (nodeStatesCache.get(key) !== currentState) {
120+
if (nodeStatesCache.get(nodeId) !== currentState) {
109121
positionChanged = true
110-
nodeStatesCache.set(key, currentState)
122+
nodeStatesCache.set(nodeId, currentState)
111123
}
112124
}
113125

114-
const currentLinks = JSON.stringify(g.links || {})
115-
if (currentLinks !== linksCache.value) {
116-
connectionChanged = true
117-
linksCache.value = currentLinks
118-
}
119-
120-
const currentNodeIds = new Set(g._nodes.map((n: LGraphNode) => n.id))
126+
// Clean up removed nodes from cache
127+
const currentNodeIds = new Set(nodes.map((n) => n.id))
121128
for (const [nodeId] of nodeStatesCache) {
122129
if (!currentNodeIds.has(nodeId)) {
123130
nodeStatesCache.delete(nodeId)
124131
structureChanged = true
125132
}
126133
}
127134

135+
// TODO: update when Layoutstore tracks links
136+
const currentLinks = JSON.stringify(g.links || {})
137+
if (currentLinks !== linksCache.value) {
138+
connectionChanged = true
139+
linksCache.value = currentLinks
140+
}
141+
128142
if (structureChanged || positionChanged) {
129143
updateFlags.value.bounds = true
130144
updateFlags.value.nodes = true
@@ -140,6 +154,10 @@ export function useMinimapGraph(
140154
const init = () => {
141155
setupEventListeners()
142156
api.addEventListener('graphChanged', handleGraphChangedThrottled)
157+
158+
watch(layoutStoreVersion, () => {
159+
void handleGraphChangedThrottled()
160+
})
143161
}
144162

145163
const destroy = () => {

src/renderer/extensions/minimap/composables/useMinimapViewport.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformS
55
import type { LGraph } from '@/lib/litegraph/src/litegraph'
66
import {
77
calculateMinimapScale,
8-
calculateNodeBounds,
98
enforceMinimumBounds
109
} from '@/renderer/core/spatial/boundsCalculator'
10+
import { MinimapDataSourceFactory } from '@/renderer/extensions/minimap/data/MinimapDataSourceFactory'
1111

1212
import type { MinimapBounds, MinimapCanvas, ViewportTransform } from '../types'
1313

@@ -53,17 +53,15 @@ export function useMinimapViewport(
5353
}
5454

5555
const calculateGraphBounds = (): MinimapBounds => {
56-
const g = graph.value
57-
if (!g || !g._nodes || g._nodes.length === 0) {
58-
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
59-
}
56+
// Use unified data source
57+
const dataSource = MinimapDataSourceFactory.create(graph.value)
6058

61-
const bounds = calculateNodeBounds(g._nodes)
62-
if (!bounds) {
59+
if (!dataSource.hasData()) {
6360
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
6461
}
6562

66-
return enforceMinimumBounds(bounds)
63+
const sourceBounds = dataSource.getBounds()
64+
return enforceMinimumBounds(sourceBounds)
6765
}
6866

6967
const calculateScale = () => {
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { LGraph } from '@/lib/litegraph/src/litegraph'
2+
import { calculateNodeBounds } from '@/renderer/core/spatial/boundsCalculator'
3+
4+
import type {
5+
IMinimapDataSource,
6+
MinimapBounds,
7+
MinimapGroupData,
8+
MinimapLinkData,
9+
MinimapNodeData
10+
} from '../types'
11+
12+
/**
13+
* Abstract base class for minimap data sources
14+
* Provides common functionality and shared implementation
15+
*/
16+
export abstract class AbstractMinimapDataSource implements IMinimapDataSource {
17+
constructor(protected graph: LGraph | null) {}
18+
19+
// Abstract methods that must be implemented by subclasses
20+
abstract getNodes(): MinimapNodeData[]
21+
abstract getNodeCount(): number
22+
abstract hasData(): boolean
23+
24+
// Shared implementation using calculateNodeBounds
25+
getBounds(): MinimapBounds {
26+
const nodes = this.getNodes()
27+
if (nodes.length === 0) {
28+
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
29+
}
30+
31+
// Convert MinimapNodeData to the format expected by calculateNodeBounds
32+
const compatibleNodes = nodes.map((node) => ({
33+
pos: [node.x, node.y],
34+
size: [node.width, node.height]
35+
}))
36+
37+
const bounds = calculateNodeBounds(compatibleNodes)
38+
if (!bounds) {
39+
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
40+
}
41+
42+
return bounds
43+
}
44+
45+
// Shared implementation for groups
46+
getGroups(): MinimapGroupData[] {
47+
if (!this.graph?._groups) return []
48+
return this.graph._groups.map((group) => ({
49+
x: group.pos[0],
50+
y: group.pos[1],
51+
width: group.size[0],
52+
height: group.size[1],
53+
color: group.color
54+
}))
55+
}
56+
57+
// TODO: update when Layoutstore supports links
58+
getLinks(): MinimapLinkData[] {
59+
if (!this.graph) return []
60+
return this.extractLinksFromGraph(this.graph)
61+
}
62+
63+
protected extractLinksFromGraph(graph: LGraph): MinimapLinkData[] {
64+
const links: MinimapLinkData[] = []
65+
const nodeMap = new Map(this.getNodes().map((n) => [n.id, n]))
66+
67+
for (const node of graph._nodes) {
68+
if (!node.outputs) continue
69+
70+
const sourceNodeData = nodeMap.get(String(node.id))
71+
if (!sourceNodeData) continue
72+
73+
for (const output of node.outputs) {
74+
if (!output.links) continue
75+
76+
for (const linkId of output.links) {
77+
const link = graph.links[linkId]
78+
if (!link) continue
79+
80+
const targetNodeData = nodeMap.get(String(link.target_id))
81+
if (!targetNodeData) continue
82+
83+
links.push({
84+
sourceNode: sourceNodeData,
85+
targetNode: targetNodeData,
86+
sourceSlot: link.origin_slot,
87+
targetSlot: link.target_slot
88+
})
89+
}
90+
}
91+
}
92+
93+
return links
94+
}
95+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
2+
3+
import type { MinimapNodeData } from '../types'
4+
import { AbstractMinimapDataSource } from './AbstractMinimapDataSource'
5+
6+
/**
7+
* Layout Store data source implementation
8+
*/
9+
export class LayoutStoreDataSource extends AbstractMinimapDataSource {
10+
getNodes(): MinimapNodeData[] {
11+
const allNodes = layoutStore.getAllNodes().value
12+
if (allNodes.size === 0) return []
13+
14+
const nodes: MinimapNodeData[] = []
15+
16+
for (const [nodeId, layout] of allNodes) {
17+
// Find corresponding LiteGraph node for additional properties
18+
const graphNode = this.graph?._nodes?.find((n) => String(n.id) === nodeId)
19+
20+
nodes.push({
21+
id: nodeId,
22+
x: layout.position.x,
23+
y: layout.position.y,
24+
width: layout.size.width,
25+
height: layout.size.height,
26+
bgcolor: graphNode?.bgcolor,
27+
mode: graphNode?.mode,
28+
hasErrors: graphNode?.has_errors
29+
})
30+
}
31+
32+
return nodes
33+
}
34+
35+
getNodeCount(): number {
36+
return layoutStore.getAllNodes().value.size
37+
}
38+
39+
hasData(): boolean {
40+
return this.getNodeCount() > 0
41+
}
42+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { MinimapNodeData } from '../types'
2+
import { AbstractMinimapDataSource } from './AbstractMinimapDataSource'
3+
4+
/**
5+
* LiteGraph data source implementation
6+
*/
7+
export class LiteGraphDataSource extends AbstractMinimapDataSource {
8+
getNodes(): MinimapNodeData[] {
9+
if (!this.graph?._nodes) return []
10+
11+
return this.graph._nodes.map((node) => ({
12+
id: String(node.id),
13+
x: node.pos[0],
14+
y: node.pos[1],
15+
width: node.size[0],
16+
height: node.size[1],
17+
bgcolor: node.bgcolor,
18+
mode: node.mode,
19+
hasErrors: node.has_errors
20+
}))
21+
}
22+
23+
getNodeCount(): number {
24+
return this.graph?._nodes?.length ?? 0
25+
}
26+
27+
hasData(): boolean {
28+
return this.getNodeCount() > 0
29+
}
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { LGraph } from '@/lib/litegraph/src/litegraph'
2+
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
3+
4+
import type { IMinimapDataSource } from '../types'
5+
import { LayoutStoreDataSource } from './LayoutStoreDataSource'
6+
import { LiteGraphDataSource } from './LiteGraphDataSource'
7+
8+
/**
9+
* Factory for creating the appropriate data source
10+
*/
11+
export class MinimapDataSourceFactory {
12+
static create(graph: LGraph | null): IMinimapDataSource {
13+
// Check if LayoutStore has data
14+
const layoutStoreHasData = layoutStore.getAllNodes().value.size > 0
15+
16+
if (layoutStoreHasData) {
17+
return new LayoutStoreDataSource(graph)
18+
}
19+
20+
return new LiteGraphDataSource(graph)
21+
}
22+
}

0 commit comments

Comments
 (0)