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)
This commit is contained in:
Johnpaul Chiwetelu
2025-09-19 21:52:57 +01:00
committed by GitHub
parent 893409dfc8
commit 8ffe63f54e
9 changed files with 529 additions and 92 deletions

View File

@@ -1,11 +1,13 @@
import { useThrottleFn } from '@vueuse/core'
import { ref } from 'vue'
import { ref, watch } from 'vue'
import type { Ref } from 'vue'
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { api } from '@/scripts/api'
import { MinimapDataSourceFactory } from '../data/MinimapDataSourceFactory'
import type { UpdateFlags } from '../types'
interface GraphCallbacks {
@@ -28,6 +30,9 @@ export function useMinimapGraph(
viewport: false
})
// Track LayoutStore version for change detection
const layoutStoreVersion = layoutStore.getVersion()
// Map to store original callbacks per graph ID
const originalCallbacksMap = new Map<string, GraphCallbacks>()
@@ -96,28 +101,30 @@ export function useMinimapGraph(
let positionChanged = false
let connectionChanged = false
if (g._nodes.length !== lastNodeCount.value) {
// Use unified data source for change detection
const dataSource = MinimapDataSourceFactory.create(g)
// Check for node count changes
const currentNodeCount = dataSource.getNodeCount()
if (currentNodeCount !== lastNodeCount.value) {
structureChanged = true
lastNodeCount.value = g._nodes.length
lastNodeCount.value = currentNodeCount
}
for (const node of g._nodes) {
const key = node.id
const currentState = `${node.pos[0]},${node.pos[1]},${node.size[0]},${node.size[1]}`
// Check for node position/size changes
const nodes = dataSource.getNodes()
for (const node of nodes) {
const nodeId = node.id
const currentState = `${node.x},${node.y},${node.width},${node.height}`
if (nodeStatesCache.get(key) !== currentState) {
if (nodeStatesCache.get(nodeId) !== currentState) {
positionChanged = true
nodeStatesCache.set(key, currentState)
nodeStatesCache.set(nodeId, currentState)
}
}
const currentLinks = JSON.stringify(g.links || {})
if (currentLinks !== linksCache.value) {
connectionChanged = true
linksCache.value = currentLinks
}
const currentNodeIds = new Set(g._nodes.map((n: LGraphNode) => n.id))
// Clean up removed nodes from cache
const currentNodeIds = new Set(nodes.map((n) => n.id))
for (const [nodeId] of nodeStatesCache) {
if (!currentNodeIds.has(nodeId)) {
nodeStatesCache.delete(nodeId)
@@ -125,6 +132,13 @@ export function useMinimapGraph(
}
}
// TODO: update when Layoutstore tracks links
const currentLinks = JSON.stringify(g.links || {})
if (currentLinks !== linksCache.value) {
connectionChanged = true
linksCache.value = currentLinks
}
if (structureChanged || positionChanged) {
updateFlags.value.bounds = true
updateFlags.value.nodes = true
@@ -140,6 +154,10 @@ export function useMinimapGraph(
const init = () => {
setupEventListeners()
api.addEventListener('graphChanged', handleGraphChangedThrottled)
watch(layoutStoreVersion, () => {
void handleGraphChangedThrottled()
})
}
const destroy = () => {

View File

@@ -5,9 +5,9 @@ import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformS
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import {
calculateMinimapScale,
calculateNodeBounds,
enforceMinimumBounds
} from '@/renderer/core/spatial/boundsCalculator'
import { MinimapDataSourceFactory } from '@/renderer/extensions/minimap/data/MinimapDataSourceFactory'
import type { MinimapBounds, MinimapCanvas, ViewportTransform } from '../types'
@@ -53,17 +53,15 @@ export function useMinimapViewport(
}
const calculateGraphBounds = (): MinimapBounds => {
const g = graph.value
if (!g || !g._nodes || g._nodes.length === 0) {
// Use unified data source
const dataSource = MinimapDataSourceFactory.create(graph.value)
if (!dataSource.hasData()) {
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
}
const bounds = calculateNodeBounds(g._nodes)
if (!bounds) {
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
}
return enforceMinimumBounds(bounds)
const sourceBounds = dataSource.getBounds()
return enforceMinimumBounds(sourceBounds)
}
const calculateScale = () => {

View File

@@ -0,0 +1,95 @@
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { calculateNodeBounds } from '@/renderer/core/spatial/boundsCalculator'
import type {
IMinimapDataSource,
MinimapBounds,
MinimapGroupData,
MinimapLinkData,
MinimapNodeData
} from '../types'
/**
* Abstract base class for minimap data sources
* Provides common functionality and shared implementation
*/
export abstract class AbstractMinimapDataSource implements IMinimapDataSource {
constructor(protected graph: LGraph | null) {}
// Abstract methods that must be implemented by subclasses
abstract getNodes(): MinimapNodeData[]
abstract getNodeCount(): number
abstract hasData(): boolean
// Shared implementation using calculateNodeBounds
getBounds(): MinimapBounds {
const nodes = this.getNodes()
if (nodes.length === 0) {
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
}
// Convert MinimapNodeData to the format expected by calculateNodeBounds
const compatibleNodes = nodes.map((node) => ({
pos: [node.x, node.y],
size: [node.width, node.height]
}))
const bounds = calculateNodeBounds(compatibleNodes)
if (!bounds) {
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
}
return bounds
}
// Shared implementation for groups
getGroups(): MinimapGroupData[] {
if (!this.graph?._groups) return []
return this.graph._groups.map((group) => ({
x: group.pos[0],
y: group.pos[1],
width: group.size[0],
height: group.size[1],
color: group.color
}))
}
// TODO: update when Layoutstore supports links
getLinks(): MinimapLinkData[] {
if (!this.graph) return []
return this.extractLinksFromGraph(this.graph)
}
protected extractLinksFromGraph(graph: LGraph): MinimapLinkData[] {
const links: MinimapLinkData[] = []
const nodeMap = new Map(this.getNodes().map((n) => [n.id, n]))
for (const node of graph._nodes) {
if (!node.outputs) continue
const sourceNodeData = nodeMap.get(String(node.id))
if (!sourceNodeData) continue
for (const output of node.outputs) {
if (!output.links) continue
for (const linkId of output.links) {
const link = graph.links[linkId]
if (!link) continue
const targetNodeData = nodeMap.get(String(link.target_id))
if (!targetNodeData) continue
links.push({
sourceNode: sourceNodeData,
targetNode: targetNodeData,
sourceSlot: link.origin_slot,
targetSlot: link.target_slot
})
}
}
}
return links
}
}

View File

@@ -0,0 +1,42 @@
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { MinimapNodeData } from '../types'
import { AbstractMinimapDataSource } from './AbstractMinimapDataSource'
/**
* Layout Store data source implementation
*/
export class LayoutStoreDataSource extends AbstractMinimapDataSource {
getNodes(): MinimapNodeData[] {
const allNodes = layoutStore.getAllNodes().value
if (allNodes.size === 0) return []
const nodes: MinimapNodeData[] = []
for (const [nodeId, layout] of allNodes) {
// Find corresponding LiteGraph node for additional properties
const graphNode = this.graph?._nodes?.find((n) => String(n.id) === nodeId)
nodes.push({
id: nodeId,
x: layout.position.x,
y: layout.position.y,
width: layout.size.width,
height: layout.size.height,
bgcolor: graphNode?.bgcolor,
mode: graphNode?.mode,
hasErrors: graphNode?.has_errors
})
}
return nodes
}
getNodeCount(): number {
return layoutStore.getAllNodes().value.size
}
hasData(): boolean {
return this.getNodeCount() > 0
}
}

View File

@@ -0,0 +1,30 @@
import type { MinimapNodeData } from '../types'
import { AbstractMinimapDataSource } from './AbstractMinimapDataSource'
/**
* LiteGraph data source implementation
*/
export class LiteGraphDataSource extends AbstractMinimapDataSource {
getNodes(): MinimapNodeData[] {
if (!this.graph?._nodes) return []
return this.graph._nodes.map((node) => ({
id: String(node.id),
x: node.pos[0],
y: node.pos[1],
width: node.size[0],
height: node.size[1],
bgcolor: node.bgcolor,
mode: node.mode,
hasErrors: node.has_errors
}))
}
getNodeCount(): number {
return this.graph?._nodes?.length ?? 0
}
hasData(): boolean {
return this.getNodeCount() > 0
}
}

View File

@@ -0,0 +1,22 @@
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { IMinimapDataSource } from '../types'
import { LayoutStoreDataSource } from './LayoutStoreDataSource'
import { LiteGraphDataSource } from './LiteGraphDataSource'
/**
* Factory for creating the appropriate data source
*/
export class MinimapDataSourceFactory {
static create(graph: LGraph | null): IMinimapDataSource {
// Check if LayoutStore has data
const layoutStoreHasData = layoutStore.getAllNodes().value.size > 0
if (layoutStoreHasData) {
return new LayoutStoreDataSource(graph)
}
return new LiteGraphDataSource(graph)
}
}

View File

@@ -3,7 +3,12 @@ import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'
import type { MinimapRenderContext } from './types'
import { MinimapDataSourceFactory } from './data/MinimapDataSourceFactory'
import type {
IMinimapDataSource,
MinimapNodeData,
MinimapRenderContext
} from './types'
/**
* Get theme-aware colors for the minimap
@@ -25,24 +30,49 @@ function getMinimapColors() {
}
}
/**
* Get node color based on settings and node properties (Single Responsibility)
*/
function getNodeColor(
node: MinimapNodeData,
settings: MinimapRenderContext['settings'],
colors: ReturnType<typeof getMinimapColors>
): string {
if (settings.renderBypass && node.mode === LGraphEventMode.BYPASS) {
return colors.bypassColor
}
if (settings.nodeColors) {
if (node.bgcolor) {
return colors.isLightTheme
? adjustColor(node.bgcolor, { lightness: 0.5 })
: node.bgcolor
}
return colors.nodeColorDefault
}
return colors.nodeColor
}
/**
* Render groups on the minimap
*/
function renderGroups(
ctx: CanvasRenderingContext2D,
graph: LGraph,
dataSource: IMinimapDataSource,
offsetX: number,
offsetY: number,
context: MinimapRenderContext,
colors: ReturnType<typeof getMinimapColors>
) {
if (!graph._groups || graph._groups.length === 0) return
const groups = dataSource.getGroups()
if (groups.length === 0) return
for (const group of graph._groups) {
const x = (group.pos[0] - context.bounds.minX) * context.scale + offsetX
const y = (group.pos[1] - context.bounds.minY) * context.scale + offsetY
const w = group.size[0] * context.scale
const h = group.size[1] * context.scale
for (const group of groups) {
const x = (group.x - context.bounds.minX) * context.scale + offsetX
const y = (group.y - context.bounds.minY) * context.scale + offsetY
const w = group.width * context.scale
const h = group.height * context.scale
let color = colors.groupColor
@@ -64,45 +94,34 @@ function renderGroups(
*/
function renderNodes(
ctx: CanvasRenderingContext2D,
graph: LGraph,
dataSource: IMinimapDataSource,
offsetX: number,
offsetY: number,
context: MinimapRenderContext,
colors: ReturnType<typeof getMinimapColors>
) {
if (!graph._nodes || graph._nodes.length === 0) return
const nodes = dataSource.getNodes()
if (nodes.length === 0) return
// Group nodes by color for batch rendering
// Group nodes by color for batch rendering (performance optimization)
const nodesByColor = new Map<
string,
Array<{ x: number; y: number; w: number; h: number; hasErrors?: boolean }>
>()
for (const node of graph._nodes) {
const x = (node.pos[0] - context.bounds.minX) * context.scale + offsetX
const y = (node.pos[1] - context.bounds.minY) * context.scale + offsetY
const w = node.size[0] * context.scale
const h = node.size[1] * context.scale
for (const node of nodes) {
const x = (node.x - context.bounds.minX) * context.scale + offsetX
const y = (node.y - context.bounds.minY) * context.scale + offsetY
const w = node.width * context.scale
const h = node.height * context.scale
let color = colors.nodeColor
if (context.settings.renderBypass && node.mode === LGraphEventMode.BYPASS) {
color = colors.bypassColor
} else if (context.settings.nodeColors) {
color = colors.nodeColorDefault
if (node.bgcolor) {
color = colors.isLightTheme
? adjustColor(node.bgcolor, { lightness: 0.5 })
: node.bgcolor
}
}
const color = getNodeColor(node, context.settings, colors)
if (!nodesByColor.has(color)) {
nodesByColor.set(color, [])
}
nodesByColor.get(color)!.push({ x, y, w, h, hasErrors: node.has_errors })
nodesByColor.get(color)!.push({ x, y, w, h, hasErrors: node.hasErrors })
}
// Batch render nodes by color
@@ -132,13 +151,14 @@ function renderNodes(
*/
function renderConnections(
ctx: CanvasRenderingContext2D,
graph: LGraph,
dataSource: IMinimapDataSource,
offsetX: number,
offsetY: number,
context: MinimapRenderContext,
colors: ReturnType<typeof getMinimapColors>
) {
if (!graph || !graph._nodes) return
const links = dataSource.getLinks()
if (links.length === 0) return
ctx.strokeStyle = colors.linkColor
ctx.lineWidth = 0.3
@@ -151,41 +171,28 @@ function renderConnections(
y2: number
}> = []
for (const node of graph._nodes) {
if (!node.outputs) continue
for (const link of links) {
const x1 =
(link.sourceNode.x - context.bounds.minX) * context.scale + offsetX
const y1 =
(link.sourceNode.y - context.bounds.minY) * context.scale + offsetY
const x2 =
(link.targetNode.x - context.bounds.minX) * context.scale + offsetX
const y2 =
(link.targetNode.y - context.bounds.minY) * context.scale + offsetY
const x1 = (node.pos[0] - context.bounds.minX) * context.scale + offsetX
const y1 = (node.pos[1] - context.bounds.minY) * context.scale + offsetY
const outputX = x1 + link.sourceNode.width * context.scale
const outputY = y1 + link.sourceNode.height * context.scale * 0.2
const inputX = x2
const inputY = y2 + link.targetNode.height * context.scale * 0.2
for (const output of node.outputs) {
if (!output.links) continue
// Draw connection line
ctx.beginPath()
ctx.moveTo(outputX, outputY)
ctx.lineTo(inputX, inputY)
ctx.stroke()
for (const linkId of output.links) {
const link = graph.links[linkId]
if (!link) continue
const targetNode = graph.getNodeById(link.target_id)
if (!targetNode) continue
const x2 =
(targetNode.pos[0] - context.bounds.minX) * context.scale + offsetX
const y2 =
(targetNode.pos[1] - context.bounds.minY) * context.scale + offsetY
const outputX = x1 + node.size[0] * context.scale
const outputY = y1 + node.size[1] * context.scale * 0.2
const inputX = x2
const inputY = y2 + targetNode.size[1] * context.scale * 0.2
// Draw connection line
ctx.beginPath()
ctx.moveTo(outputX, outputY)
ctx.lineTo(inputX, inputY)
ctx.stroke()
connections.push({ x1: outputX, y1: outputY, x2: inputX, y2: inputY })
}
}
connections.push({ x1: outputX, y1: outputY, x2: inputX, y2: inputY })
}
// Render connection slots on top
@@ -217,8 +224,11 @@ export function renderMinimapToCanvas(
// Clear canvas
ctx.clearRect(0, 0, context.width, context.height)
// Create unified data source (Dependency Inversion)
const dataSource = MinimapDataSourceFactory.create(graph)
// Fast path for empty graph
if (!graph || !graph._nodes || graph._nodes.length === 0) {
if (!dataSource.hasData()) {
return
}
@@ -228,12 +238,12 @@ export function renderMinimapToCanvas(
// Render in correct order: groups -> links -> nodes
if (context.settings.showGroups) {
renderGroups(ctx, graph, offsetX, offsetY, context, colors)
renderGroups(ctx, dataSource, offsetX, offsetY, context, colors)
}
if (context.settings.showLinks) {
renderConnections(ctx, graph, offsetX, offsetY, context, colors)
renderConnections(ctx, dataSource, offsetX, offsetY, context, colors)
}
renderNodes(ctx, graph, offsetX, offsetY, context, colors)
renderNodes(ctx, dataSource, offsetX, offsetY, context, colors)
}

View File

@@ -2,6 +2,7 @@
* Minimap-specific type definitions
*/
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
/**
* Minimal interface for what the minimap needs from the canvas
@@ -66,3 +67,50 @@ export type MinimapSettingsKey =
| 'Comfy.Minimap.ShowGroups'
| 'Comfy.Minimap.RenderBypassState'
| 'Comfy.Minimap.RenderErrorState'
/**
* Node data required for minimap rendering
*/
export interface MinimapNodeData {
id: NodeId
x: number
y: number
width: number
height: number
bgcolor?: string
mode?: number
hasErrors?: boolean
}
/**
* Link data required for minimap rendering
*/
export interface MinimapLinkData {
sourceNode: MinimapNodeData
targetNode: MinimapNodeData
sourceSlot: number
targetSlot: number
}
/**
* Group data required for minimap rendering
*/
export interface MinimapGroupData {
x: number
y: number
width: number
height: number
color?: string
}
/**
* Interface for minimap data sources (Dependency Inversion Principle)
*/
export interface IMinimapDataSource {
getNodes(): MinimapNodeData[]
getLinks(): MinimapLinkData[]
getGroups(): MinimapGroupData[]
getBounds(): MinimapBounds
getNodeCount(): number
hasData(): boolean
}

View File

@@ -0,0 +1,174 @@
import { describe, expect, it, vi } from 'vitest'
import { type ComputedRef, computed } from 'vue'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraph, LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { NodeLayout } from '@/renderer/core/layout/types'
import { MinimapDataSourceFactory } from '@/renderer/extensions/minimap/data/MinimapDataSourceFactory'
// Mock layoutStore
vi.mock('@/renderer/core/layout/store/layoutStore', () => ({
layoutStore: {
getAllNodes: vi.fn()
}
}))
// Helper to create mock links that satisfy LGraph['links'] type
function createMockLinks(): LGraph['links'] {
const map = new Map<number, LLink>()
return Object.assign(map, {}) as LGraph['links']
}
describe('MinimapDataSource', () => {
describe('MinimapDataSourceFactory', () => {
it('should create LayoutStoreDataSource when LayoutStore has data', () => {
// Arrange
const mockNodes = new Map<string, NodeLayout>([
[
'node1',
{
id: 'node1',
position: { x: 0, y: 0 },
size: { width: 100, height: 50 },
zIndex: 0,
visible: true,
bounds: { x: 0, y: 0, width: 100, height: 50 }
}
]
])
// Create a computed ref that returns the map
const computedNodes: ComputedRef<ReadonlyMap<string, NodeLayout>> =
computed(() => mockNodes)
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedNodes)
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
_nodes: [],
_groups: [],
links: createMockLinks()
}
// Act
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
// Assert
expect(dataSource).toBeDefined()
expect(dataSource.hasData()).toBe(true)
expect(dataSource.getNodeCount()).toBe(1)
})
it('should create LiteGraphDataSource when LayoutStore is empty', () => {
// Arrange
const emptyMap = new Map<string, NodeLayout>()
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
computed(() => emptyMap)
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
const mockNode: Pick<
LGraphNode,
'id' | 'pos' | 'size' | 'bgcolor' | 'mode' | 'has_errors' | 'outputs'
> = {
id: 'node1' as NodeId,
pos: [0, 0],
size: [100, 50],
bgcolor: '#fff',
mode: 0,
has_errors: false,
outputs: []
}
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
_nodes: [mockNode as LGraphNode],
_groups: [],
links: createMockLinks()
}
// Act
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
// Assert
expect(dataSource).toBeDefined()
expect(dataSource.hasData()).toBe(true)
expect(dataSource.getNodeCount()).toBe(1)
const nodes = dataSource.getNodes()
expect(nodes).toHaveLength(1)
expect(nodes[0]).toMatchObject({
id: 'node1',
x: 0,
y: 0,
width: 100,
height: 50
})
})
it('should handle empty graph correctly', () => {
// Arrange
const emptyMap = new Map<string, NodeLayout>()
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
computed(() => emptyMap)
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
_nodes: [],
_groups: [],
links: createMockLinks()
}
// Act
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
// Assert
expect(dataSource.hasData()).toBe(false)
expect(dataSource.getNodeCount()).toBe(0)
expect(dataSource.getNodes()).toEqual([])
expect(dataSource.getLinks()).toEqual([])
expect(dataSource.getGroups()).toEqual([])
})
})
describe('Bounds calculation', () => {
it('should calculate correct bounds from nodes', () => {
// Arrange
const emptyMap = new Map<string, NodeLayout>()
const computedEmpty: ComputedRef<ReadonlyMap<string, NodeLayout>> =
computed(() => emptyMap)
vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty)
const mockNode1: Pick<LGraphNode, 'id' | 'pos' | 'size' | 'outputs'> = {
id: 'node1' as NodeId,
pos: [0, 0],
size: [100, 50],
outputs: []
}
const mockNode2: Pick<LGraphNode, 'id' | 'pos' | 'size' | 'outputs'> = {
id: 'node2' as NodeId,
pos: [200, 100],
size: [150, 75],
outputs: []
}
const mockGraph: Pick<LGraph, '_nodes' | '_groups' | 'links'> = {
_nodes: [mockNode1 as LGraphNode, mockNode2 as LGraphNode],
_groups: [],
links: createMockLinks()
}
// Act
const dataSource = MinimapDataSourceFactory.create(mockGraph as LGraph)
const bounds = dataSource.getBounds()
// Assert
expect(bounds).toEqual({
minX: 0,
minY: 0,
maxX: 350,
maxY: 175,
width: 350,
height: 175
})
})
})
})