Files
ComfyUI_frontend/src/renderer/extensions/minimap/minimapCanvasRenderer.ts
Johnpaul Chiwetelu 8ffe63f54e 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)
2025-09-19 13:52:57 -07:00

250 lines
6.7 KiB
TypeScript

import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { adjustColor } from '@/utils/colorUtil'
import { MinimapDataSourceFactory } from './data/MinimapDataSourceFactory'
import type {
IMinimapDataSource,
MinimapNodeData,
MinimapRenderContext
} from './types'
/**
* Get theme-aware colors for the minimap
*/
function getMinimapColors() {
const colorPaletteStore = useColorPaletteStore()
const isLightTheme = colorPaletteStore.completedActivePalette.light_theme
return {
nodeColor: isLightTheme ? '#3DA8E099' : '#0B8CE999',
nodeColorDefault: isLightTheme ? '#D9D9D9' : '#353535',
linkColor: isLightTheme ? '#616161' : '#B3B3B3',
slotColor: isLightTheme ? '#616161' : '#B3B3B3',
groupColor: isLightTheme ? '#A2D3EC' : '#1F547A',
groupColorDefault: isLightTheme ? '#283640' : '#B3C1CB',
bypassColor: isLightTheme ? '#DBDBDB' : '#4B184B',
errorColor: '#FF0000',
isLightTheme
}
}
/**
* 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,
dataSource: IMinimapDataSource,
offsetX: number,
offsetY: number,
context: MinimapRenderContext,
colors: ReturnType<typeof getMinimapColors>
) {
const groups = dataSource.getGroups()
if (groups.length === 0) return
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
if (context.settings.nodeColors) {
color = group.color ?? colors.groupColorDefault
if (colors.isLightTheme) {
color = adjustColor(color, { opacity: 0.5 })
}
}
ctx.fillStyle = color
ctx.fillRect(x, y, w, h)
}
}
/**
* Render nodes on the minimap with performance optimizations
*/
function renderNodes(
ctx: CanvasRenderingContext2D,
dataSource: IMinimapDataSource,
offsetX: number,
offsetY: number,
context: MinimapRenderContext,
colors: ReturnType<typeof getMinimapColors>
) {
const nodes = dataSource.getNodes()
if (nodes.length === 0) return
// 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 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
const color = getNodeColor(node, context.settings, colors)
if (!nodesByColor.has(color)) {
nodesByColor.set(color, [])
}
nodesByColor.get(color)!.push({ x, y, w, h, hasErrors: node.hasErrors })
}
// Batch render nodes by color
for (const [color, nodes] of nodesByColor) {
ctx.fillStyle = color
for (const node of nodes) {
ctx.fillRect(node.x, node.y, node.w, node.h)
}
}
// Render error outlines if needed
if (context.settings.renderError) {
ctx.strokeStyle = colors.errorColor
ctx.lineWidth = 0.3
for (const nodes of nodesByColor.values()) {
for (const node of nodes) {
if (node.hasErrors) {
ctx.strokeRect(node.x, node.y, node.w, node.h)
}
}
}
}
}
/**
* Render connections on the minimap
*/
function renderConnections(
ctx: CanvasRenderingContext2D,
dataSource: IMinimapDataSource,
offsetX: number,
offsetY: number,
context: MinimapRenderContext,
colors: ReturnType<typeof getMinimapColors>
) {
const links = dataSource.getLinks()
if (links.length === 0) return
ctx.strokeStyle = colors.linkColor
ctx.lineWidth = 0.3
const slotRadius = Math.max(context.scale, 0.5)
const connections: Array<{
x1: number
y1: number
x2: number
y2: number
}> = []
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 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
// 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 })
}
// Render connection slots on top
ctx.fillStyle = colors.slotColor
for (const conn of connections) {
// Output slot
ctx.beginPath()
ctx.arc(conn.x1, conn.y1, slotRadius, 0, Math.PI * 2)
ctx.fill()
// Input slot
ctx.beginPath()
ctx.arc(conn.x2, conn.y2, slotRadius, 0, Math.PI * 2)
ctx.fill()
}
}
/**
* Render a graph to a minimap canvas
*/
export function renderMinimapToCanvas(
canvas: HTMLCanvasElement,
graph: LGraph,
context: MinimapRenderContext
) {
const ctx = canvas.getContext('2d')
if (!ctx) return
// 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 (!dataSource.hasData()) {
return
}
const colors = getMinimapColors()
const offsetX = (context.width - context.bounds.width * context.scale) / 2
const offsetY = (context.height - context.bounds.height * context.scale) / 2
// Render in correct order: groups -> links -> nodes
if (context.settings.showGroups) {
renderGroups(ctx, dataSource, offsetX, offsetY, context, colors)
}
if (context.settings.showLinks) {
renderConnections(ctx, dataSource, offsetX, offsetY, context, colors)
}
renderNodes(ctx, dataSource, offsetX, offsetY, context, colors)
}