diff --git a/src/renderer/extensions/minimap/composables/useMinimapGraph.ts b/src/renderer/extensions/minimap/composables/useMinimapGraph.ts index 4f1c0dd8e..c5bf9aa6c 100644 --- a/src/renderer/extensions/minimap/composables/useMinimapGraph.ts +++ b/src/renderer/extensions/minimap/composables/useMinimapGraph.ts @@ -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() @@ -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 = () => { diff --git a/src/renderer/extensions/minimap/composables/useMinimapViewport.ts b/src/renderer/extensions/minimap/composables/useMinimapViewport.ts index da67e5a7c..6f947a307 100644 --- a/src/renderer/extensions/minimap/composables/useMinimapViewport.ts +++ b/src/renderer/extensions/minimap/composables/useMinimapViewport.ts @@ -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 = () => { diff --git a/src/renderer/extensions/minimap/data/AbstractMinimapDataSource.ts b/src/renderer/extensions/minimap/data/AbstractMinimapDataSource.ts new file mode 100644 index 000000000..4aae340b4 --- /dev/null +++ b/src/renderer/extensions/minimap/data/AbstractMinimapDataSource.ts @@ -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 + } +} diff --git a/src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts b/src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts new file mode 100644 index 000000000..c0daf7030 --- /dev/null +++ b/src/renderer/extensions/minimap/data/LayoutStoreDataSource.ts @@ -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 + } +} diff --git a/src/renderer/extensions/minimap/data/LiteGraphDataSource.ts b/src/renderer/extensions/minimap/data/LiteGraphDataSource.ts new file mode 100644 index 000000000..8e1048e75 --- /dev/null +++ b/src/renderer/extensions/minimap/data/LiteGraphDataSource.ts @@ -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 + } +} diff --git a/src/renderer/extensions/minimap/data/MinimapDataSourceFactory.ts b/src/renderer/extensions/minimap/data/MinimapDataSourceFactory.ts new file mode 100644 index 000000000..49b15ed9e --- /dev/null +++ b/src/renderer/extensions/minimap/data/MinimapDataSourceFactory.ts @@ -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) + } +} diff --git a/src/renderer/extensions/minimap/minimapCanvasRenderer.ts b/src/renderer/extensions/minimap/minimapCanvasRenderer.ts index 2e0790ca9..3e547ce68 100644 --- a/src/renderer/extensions/minimap/minimapCanvasRenderer.ts +++ b/src/renderer/extensions/minimap/minimapCanvasRenderer.ts @@ -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 +): 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 ) { - 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 ) { - 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 ) { - 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) } diff --git a/src/renderer/extensions/minimap/types.ts b/src/renderer/extensions/minimap/types.ts index fbea21c83..b458718ea 100644 --- a/src/renderer/extensions/minimap/types.ts +++ b/src/renderer/extensions/minimap/types.ts @@ -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 +} diff --git a/tests-ui/tests/minimap/MinimapDataSource.test.ts b/tests-ui/tests/minimap/MinimapDataSource.test.ts new file mode 100644 index 000000000..eff6ba771 --- /dev/null +++ b/tests-ui/tests/minimap/MinimapDataSource.test.ts @@ -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() + return Object.assign(map, {}) as LGraph['links'] +} + +describe('MinimapDataSource', () => { + describe('MinimapDataSourceFactory', () => { + it('should create LayoutStoreDataSource when LayoutStore has data', () => { + // Arrange + const mockNodes = new Map([ + [ + '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> = + computed(() => mockNodes) + vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedNodes) + + const mockGraph: Pick = { + _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() + const computedEmpty: ComputedRef> = + 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 = { + _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() + const computedEmpty: ComputedRef> = + computed(() => emptyMap) + vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty) + + const mockGraph: Pick = { + _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() + const computedEmpty: ComputedRef> = + computed(() => emptyMap) + vi.mocked(layoutStore.getAllNodes).mockReturnValue(computedEmpty) + + const mockNode1: Pick = { + id: 'node1' as NodeId, + pos: [0, 0], + size: [100, 50], + outputs: [] + } + + const mockNode2: Pick = { + id: 'node2' as NodeId, + pos: [200, 100], + size: [150, 75], + outputs: [] + } + + const mockGraph: Pick = { + _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 + }) + }) + }) +})