mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-23 07:50:15 +00:00
Merge remote-tracking branch 'origin/main' into bl-more-slots
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef } from 'vue'
|
||||
|
||||
import type { Point, Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
LGraphGroup,
|
||||
LGraphNode
|
||||
@@ -94,9 +96,43 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
appScalePercentage.value = Math.round(newScale * 100)
|
||||
}
|
||||
|
||||
const currentGraph = shallowRef<LGraph | null>(null)
|
||||
const isInSubgraph = ref(false)
|
||||
|
||||
// Provide selection state to all Vue nodes
|
||||
const selectedNodeIds = computed(
|
||||
() =>
|
||||
new Set(
|
||||
selectedItems.value
|
||||
.filter((item) => item.id !== undefined)
|
||||
.map((item) => String(item.id))
|
||||
)
|
||||
)
|
||||
|
||||
whenever(
|
||||
() => canvas.value,
|
||||
(newCanvas) => {
|
||||
useEventListener(
|
||||
newCanvas.canvas,
|
||||
'litegraph:set-graph',
|
||||
(event: CustomEvent<{ newGraph: LGraph; oldGraph: LGraph }>) => {
|
||||
const newGraph = event.detail?.newGraph || app.canvas?.graph
|
||||
currentGraph.value = newGraph
|
||||
isInSubgraph.value = Boolean(app.canvas?.subgraph)
|
||||
}
|
||||
)
|
||||
|
||||
useEventListener(newCanvas.canvas, 'subgraph-opened', () => {
|
||||
isInSubgraph.value = true
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
return {
|
||||
canvas,
|
||||
selectedItems,
|
||||
selectedNodeIds,
|
||||
nodeSelected,
|
||||
groupSelected,
|
||||
rerouteSelected,
|
||||
@@ -105,6 +141,8 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
getCanvas,
|
||||
setAppZoomFromPercentage,
|
||||
initScaleSync,
|
||||
cleanupScaleSync
|
||||
cleanupScaleSync,
|
||||
currentGraph,
|
||||
isInSubgraph
|
||||
}
|
||||
})
|
||||
|
||||
@@ -2,13 +2,6 @@ import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
import type { NodeProgressState } from '@/schemas/apiSchema'
|
||||
|
||||
/**
|
||||
* Injection key for providing selected node IDs to Vue node components.
|
||||
* Contains a reactive Set of selected node IDs (as strings).
|
||||
*/
|
||||
export const SelectedNodeIdsKey: InjectionKey<Ref<Set<string>>> =
|
||||
Symbol('selectedNodeIds')
|
||||
|
||||
/**
|
||||
* Injection key for providing executing node IDs to Vue node components.
|
||||
* Contains a reactive Set of currently executing node IDs (as strings).
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
30
src/renderer/extensions/minimap/data/LiteGraphDataSource.ts
Normal file
30
src/renderer/extensions/minimap/data/LiteGraphDataSource.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
:collapsed="isCollapsed"
|
||||
@collapse="handleCollapse"
|
||||
@update:title="handleTitleUpdate"
|
||||
@enter-subgraph="handleEnterSubgraph"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -138,12 +139,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
toRef,
|
||||
watch
|
||||
@@ -152,10 +153,11 @@ import {
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'
|
||||
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
|
||||
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
|
||||
@@ -164,10 +166,12 @@ import type { ExecutedWsMessage } from '@/schemas/apiSchema'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { getNodeByLocatorId } from '@/utils/graphTraversalUtil'
|
||||
import {
|
||||
getLocatorIdFromNodeData,
|
||||
getNodeByLocatorId
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import { useVueElementTracking } from '../composables/useVueNodeResizeTracking'
|
||||
import NodeContent from './NodeContent.vue'
|
||||
import NodeHeader from './NodeHeader.vue'
|
||||
import NodeSlots from './NodeSlots.vue'
|
||||
@@ -186,8 +190,8 @@ interface LGraphNodeProps {
|
||||
|
||||
const {
|
||||
nodeData,
|
||||
position,
|
||||
size,
|
||||
position = { x: 0, y: 0 },
|
||||
size = { width: 100, height: 50 },
|
||||
error = null,
|
||||
readonly = false,
|
||||
zoomLevel = 1
|
||||
@@ -205,20 +209,13 @@ const emit = defineEmits<{
|
||||
slotIndex: number,
|
||||
isInput: boolean
|
||||
]
|
||||
dragStart: [event: DragEvent, nodeData: VueNodeData]
|
||||
'update:collapsed': [nodeId: string, collapsed: boolean]
|
||||
'update:title': [nodeId: string, newTitle: string]
|
||||
}>()
|
||||
|
||||
useVueElementTracking(nodeData.id, 'node')
|
||||
|
||||
// Inject selection state from parent
|
||||
const selectedNodeIds = inject(SelectedNodeIdsKey)
|
||||
if (!selectedNodeIds) {
|
||||
throw new Error(
|
||||
'SelectedNodeIds not provided - LGraphNode must be used within a component that provides selection state'
|
||||
)
|
||||
}
|
||||
const { selectedNodeIds } = storeToRefs(useCanvasStore())
|
||||
|
||||
// Inject transform state for coordinate conversion
|
||||
const transformState = inject(TransformStateKey)
|
||||
@@ -245,12 +242,7 @@ const hasAnyError = computed(
|
||||
const bypassed = computed((): boolean => nodeData.mode === 4)
|
||||
|
||||
// Use canvas interactions for proper wheel event handling and pointer event capture control
|
||||
const {
|
||||
handleWheel,
|
||||
handlePointer,
|
||||
forwardEventToCanvas,
|
||||
shouldHandleNodePointerEvents
|
||||
} = useCanvasInteractions()
|
||||
const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()
|
||||
|
||||
// LOD (Level of Detail) system based on zoom level
|
||||
const zoomRef = toRef(() => zoomLevel)
|
||||
@@ -276,14 +268,16 @@ onErrorCaptured((error) => {
|
||||
})
|
||||
|
||||
// Use layout system for node position and dragging
|
||||
const { position: layoutPosition, zIndex, resize } = useNodeLayout(nodeData.id)
|
||||
const {
|
||||
position: layoutPosition,
|
||||
zIndex,
|
||||
startDrag,
|
||||
handleDrag: handleLayoutDrag,
|
||||
endDrag,
|
||||
resize
|
||||
} = useNodeLayout(nodeData.id)
|
||||
handlePointerDown,
|
||||
handlePointerUp,
|
||||
handlePointerMove,
|
||||
isDragging,
|
||||
dragStyle
|
||||
} = useNodePointerInteractions(nodeData, (event, nodeData, wasDragging) => {
|
||||
emit('node-click', event, nodeData, wasDragging)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (size && transformState?.camera) {
|
||||
@@ -296,16 +290,6 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Drag state for styling
|
||||
const isDragging = ref(false)
|
||||
const dragStyle = computed(() => ({
|
||||
cursor: isDragging.value ? 'grabbing' : 'grab'
|
||||
}))
|
||||
const lastY = ref(0)
|
||||
const lastX = ref(0)
|
||||
// Treat tiny pointer jitter as a click, not a drag
|
||||
const DRAG_THRESHOLD_PX = 4
|
||||
|
||||
// Track collapsed state
|
||||
const isCollapsed = ref(nodeData.flags?.collapsed ?? false)
|
||||
|
||||
@@ -371,60 +355,6 @@ const outlineClass = computed(() => {
|
||||
})
|
||||
|
||||
// Event handlers
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (!nodeData) {
|
||||
console.warn('LGraphNode: nodeData is null/undefined in handlePointerDown')
|
||||
return
|
||||
}
|
||||
|
||||
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
|
||||
if (!shouldHandleNodePointerEvents.value) {
|
||||
forwardEventToCanvas(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Start drag using layout system
|
||||
isDragging.value = true
|
||||
|
||||
// Set Vue node dragging state for selection toolbox
|
||||
layoutStore.isDraggingVueNodes.value = true
|
||||
|
||||
startDrag(event)
|
||||
lastY.value = event.clientY
|
||||
lastX.value = event.clientX
|
||||
}
|
||||
|
||||
const handlePointerMove = (event: PointerEvent) => {
|
||||
// Check if this should be forwarded to canvas (e.g., space panning, middle mouse)
|
||||
handlePointer(event)
|
||||
|
||||
if (isDragging.value) {
|
||||
void handleLayoutDrag(event)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerUp = (event: PointerEvent) => {
|
||||
if (isDragging.value) {
|
||||
isDragging.value = false
|
||||
void endDrag(event)
|
||||
|
||||
// Clear Vue node dragging state for selection toolbox
|
||||
layoutStore.isDraggingVueNodes.value = false
|
||||
}
|
||||
|
||||
// Don't emit node-click when canvas is in panning mode - forward to canvas instead
|
||||
if (!shouldHandleNodePointerEvents.value) {
|
||||
forwardEventToCanvas(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Emit node-click for selection handling in GraphCanvas
|
||||
const dx = event.clientX - lastX.value
|
||||
const dy = event.clientY - lastY.value
|
||||
const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX
|
||||
emit('node-click', event, nodeData, wasDragging)
|
||||
}
|
||||
|
||||
const handleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value
|
||||
// Emit event so parent can sync with LiteGraph if needed
|
||||
@@ -453,14 +383,36 @@ const handleTitleUpdate = (newTitle: string) => {
|
||||
emit('update:title', nodeData.id, newTitle)
|
||||
}
|
||||
|
||||
const handleEnterSubgraph = () => {
|
||||
const graph = app.graph?.rootGraph || app.graph
|
||||
if (!graph) {
|
||||
console.warn('LGraphNode: No graph available for subgraph navigation')
|
||||
return
|
||||
}
|
||||
|
||||
const locatorId = getLocatorIdFromNodeData(nodeData)
|
||||
|
||||
const litegraphNode = getNodeByLocatorId(graph, locatorId)
|
||||
|
||||
if (!litegraphNode?.isSubgraphNode() || !('subgraph' in litegraphNode)) {
|
||||
console.warn('LGraphNode: Node is not a valid subgraph node', litegraphNode)
|
||||
return
|
||||
}
|
||||
|
||||
const canvas = app.canvas
|
||||
if (!canvas || typeof canvas.openSubgraph !== 'function') {
|
||||
console.warn('LGraphNode: Canvas or openSubgraph method not available')
|
||||
return
|
||||
}
|
||||
|
||||
canvas.openSubgraph(litegraphNode.subgraph)
|
||||
}
|
||||
|
||||
const nodeOutputs = useNodeOutputStore()
|
||||
|
||||
const nodeImageUrls = ref<string[]>([])
|
||||
const onNodeOutputsUpdate = (newOutputs: ExecutedWsMessage['output']) => {
|
||||
// Construct proper locator ID using subgraph ID from VueNodeData
|
||||
const locatorId = nodeData.subgraphId
|
||||
? `${nodeData.subgraphId}:${nodeData.id}`
|
||||
: nodeData.id
|
||||
const locatorId = getLocatorIdFromNodeData(nodeData)
|
||||
|
||||
// Use root graph for getNodeByLocatorId since it needs to traverse from root
|
||||
const rootGraph = app.graph?.rootGraph || app.graph
|
||||
@@ -493,11 +445,4 @@ watch(
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Template ref for tooltip positioning
|
||||
const nodeContainerRef = ref<HTMLElement>()
|
||||
|
||||
// Provide nodeImageUrls and tooltip container to child components
|
||||
provide('nodeImageUrls', nodeImageUrls)
|
||||
provide('tooltipContainer', nodeContainerRef)
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="lg-node-header flex items-center justify-between p-4 rounded-t-2xl cursor-move"
|
||||
class="lg-node-header flex items-center justify-between p-4 rounded-t-2xl cursor-move w-full"
|
||||
:data-testid="`node-header-${nodeData?.id || ''}`"
|
||||
@dblclick="handleDoubleClick"
|
||||
>
|
||||
@@ -36,17 +36,39 @@
|
||||
@cancel="handleTitleCancel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Title Buttons -->
|
||||
<div v-if="!readonly" class="flex items-center">
|
||||
<IconButton
|
||||
v-if="isSubgraphNode"
|
||||
size="sm"
|
||||
type="transparent"
|
||||
class="text-stone-200 dark-theme:text-slate-300"
|
||||
data-testid="subgraph-enter-button"
|
||||
title="Enter Subgraph"
|
||||
@click.stop="handleEnterSubgraph"
|
||||
@dblclick.stop
|
||||
>
|
||||
<i class="pi pi-external-link"></i>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Ref, computed, inject, onErrorCaptured, ref, watch } from 'vue'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
|
||||
import { app } from '@/scripts/app'
|
||||
import {
|
||||
getLocatorIdFromNodeData,
|
||||
getNodeByLocatorId
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
|
||||
interface NodeHeaderProps {
|
||||
nodeData?: VueNodeData
|
||||
@@ -60,6 +82,7 @@ const { nodeData, readonly, collapsed } = defineProps<NodeHeaderProps>()
|
||||
const emit = defineEmits<{
|
||||
collapse: []
|
||||
'update:title': [newTitle: string]
|
||||
'enter-subgraph': []
|
||||
}>()
|
||||
|
||||
// Error boundary implementation
|
||||
@@ -111,6 +134,22 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// Subgraph detection
|
||||
const isSubgraphNode = computed(() => {
|
||||
if (!nodeData?.id) return false
|
||||
|
||||
// Get the underlying LiteGraph node
|
||||
const graph = app.graph?.rootGraph || app.graph
|
||||
if (!graph) return false
|
||||
|
||||
const locatorId = getLocatorIdFromNodeData(nodeData)
|
||||
|
||||
const litegraphNode = getNodeByLocatorId(graph, locatorId)
|
||||
|
||||
// Use the official type guard method
|
||||
return litegraphNode?.isSubgraphNode() ?? false
|
||||
})
|
||||
|
||||
// Event handlers
|
||||
const handleCollapse = () => {
|
||||
emit('collapse')
|
||||
@@ -134,4 +173,8 @@ const handleTitleEdit = (newTitle: string) => {
|
||||
const handleTitleCancel = () => {
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const handleEnterSubgraph = () => {
|
||||
emit('enter-subgraph')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,19 +8,17 @@
|
||||
* - Layout mutations for visual feedback
|
||||
* - Integration with LiteGraph canvas selection system
|
||||
*/
|
||||
import type { Ref } from 'vue'
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
|
||||
|
||||
interface NodeManager {
|
||||
getNode: (id: string) => any
|
||||
}
|
||||
|
||||
export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
|
||||
function useNodeEventHandlersIndividual() {
|
||||
const canvasStore = useCanvasStore()
|
||||
const { nodeManager } = useVueNodeLifecycle()
|
||||
const { bringNodeToFront } = useNodeZIndex()
|
||||
const { shouldHandleNodePointerEvents } = useCanvasInteractions()
|
||||
|
||||
@@ -237,3 +235,7 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
|
||||
deselectNodes
|
||||
}
|
||||
}
|
||||
|
||||
export const useNodeEventHandlers = createSharedComposable(
|
||||
useNodeEventHandlersIndividual
|
||||
)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { type MaybeRefOrGetter, computed, ref, toValue } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||
|
||||
// Treat tiny pointer jitter as a click, not a drag
|
||||
const DRAG_THRESHOLD_PX = 4
|
||||
|
||||
export function useNodePointerInteractions(
|
||||
nodeDataMaybe: MaybeRefOrGetter<VueNodeData>,
|
||||
onPointerUp: (
|
||||
event: PointerEvent,
|
||||
nodeData: VueNodeData,
|
||||
wasDragging: boolean
|
||||
) => void
|
||||
) {
|
||||
const nodeData = toValue(nodeDataMaybe)
|
||||
|
||||
const { startDrag, endDrag, handleDrag } = useNodeLayout(nodeData.id)
|
||||
// Use canvas interactions for proper wheel event handling and pointer event capture control
|
||||
const { forwardEventToCanvas, shouldHandleNodePointerEvents } =
|
||||
useCanvasInteractions()
|
||||
|
||||
// Drag state for styling
|
||||
const isDragging = ref(false)
|
||||
const dragStyle = computed(() => ({
|
||||
cursor: isDragging.value ? 'grabbing' : 'grab'
|
||||
}))
|
||||
const lastX = ref(0)
|
||||
const lastY = ref(0)
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (!nodeData) {
|
||||
console.warn(
|
||||
'LGraphNode: nodeData is null/undefined in handlePointerDown'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
|
||||
if (!shouldHandleNodePointerEvents.value) {
|
||||
forwardEventToCanvas(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Start drag using layout system
|
||||
isDragging.value = true
|
||||
|
||||
// Set Vue node dragging state for selection toolbox
|
||||
layoutStore.isDraggingVueNodes.value = true
|
||||
|
||||
startDrag(event)
|
||||
lastY.value = event.clientY
|
||||
lastX.value = event.clientX
|
||||
}
|
||||
|
||||
const handlePointerMove = (event: PointerEvent) => {
|
||||
if (isDragging.value) {
|
||||
void handleDrag(event)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerUp = (event: PointerEvent) => {
|
||||
if (isDragging.value) {
|
||||
isDragging.value = false
|
||||
void endDrag(event)
|
||||
|
||||
// Clear Vue node dragging state for selection toolbox
|
||||
layoutStore.isDraggingVueNodes.value = false
|
||||
}
|
||||
|
||||
// Don't emit node-click when canvas is in panning mode - forward to canvas instead
|
||||
if (!shouldHandleNodePointerEvents.value) {
|
||||
forwardEventToCanvas(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Emit node-click for selection handling in GraphCanvas
|
||||
const dx = event.clientX - lastX.value
|
||||
const dy = event.clientY - lastY.value
|
||||
const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX
|
||||
onPointerUp(event, nodeData, wasDragging)
|
||||
}
|
||||
return {
|
||||
isDragging,
|
||||
dragStyle,
|
||||
handlePointerMove,
|
||||
handlePointerDown,
|
||||
handlePointerUp
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
/**
|
||||
* Composable for individual Vue node components
|
||||
*
|
||||
@@ -6,7 +7,7 @@
|
||||
*/
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
@@ -17,14 +18,14 @@ import { LayoutSource, type Point } from '@/renderer/core/layout/types'
|
||||
* Uses customRef for shared write access with Canvas renderer
|
||||
*/
|
||||
export function useNodeLayout(nodeId: string) {
|
||||
const store = layoutStore
|
||||
const mutations = useLayoutMutations()
|
||||
const { selectedNodeIds } = storeToRefs(useCanvasStore())
|
||||
|
||||
// Get transform utilities from TransformPane if available
|
||||
const transformState = inject(TransformStateKey)
|
||||
|
||||
// Get the customRef for this node (shared write access)
|
||||
const layoutRef = store.getNodeLayoutRef(nodeId)
|
||||
const layoutRef = layoutStore.getNodeLayoutRef(nodeId)
|
||||
|
||||
// Computed properties for easy access
|
||||
const position = computed(() => {
|
||||
@@ -53,8 +54,6 @@ export function useNodeLayout(nodeId: string) {
|
||||
let dragStartMouse: Point | null = null
|
||||
let otherSelectedNodesStartPositions: Map<string, Point> | null = null
|
||||
|
||||
const selectedNodeIds = inject(SelectedNodeIdsKey, null)
|
||||
|
||||
/**
|
||||
* Start dragging the node
|
||||
*/
|
||||
|
||||
@@ -3,10 +3,9 @@ import { ref } from 'vue'
|
||||
import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
IBaseWidget,
|
||||
IComboWidget
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import { isAssetWidget, isComboWidget } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration'
|
||||
@@ -73,11 +72,29 @@ const addComboWidget = (
|
||||
const currentValue = getDefaultValue(inputSpec)
|
||||
const displayLabel = currentValue ?? t('widgets.selectModel')
|
||||
|
||||
const widget = node.addWidget('asset', inputSpec.name, displayLabel, () => {
|
||||
console.log(
|
||||
`Asset Browser would open here for:\nNode: ${node.type}\nWidget: ${inputSpec.name}\nCurrent Value:${currentValue}`
|
||||
)
|
||||
})
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
|
||||
const widget = node.addWidget(
|
||||
'asset',
|
||||
inputSpec.name,
|
||||
displayLabel,
|
||||
async () => {
|
||||
if (!isAssetWidget(widget)) {
|
||||
throw new Error(`Expected asset widget but received ${widget.type}`)
|
||||
}
|
||||
await assetBrowserDialog.show({
|
||||
nodeType: node.comfyClass || '',
|
||||
inputName: inputSpec.name,
|
||||
currentValue: widget.value,
|
||||
onAssetSelected: (filename: string) => {
|
||||
const oldValue = widget.value
|
||||
widget.value = filename
|
||||
// Using onWidgetChanged prevents a callback race where asset selection could reopen the dialog
|
||||
node.onWidgetChanged?.(widget.name, filename, oldValue, widget)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return widget
|
||||
}
|
||||
@@ -96,11 +113,14 @@ const addComboWidget = (
|
||||
)
|
||||
|
||||
if (inputSpec.remote) {
|
||||
if (!isComboWidget(widget)) {
|
||||
throw new Error(`Expected combo widget but received ${widget.type}`)
|
||||
}
|
||||
const remoteWidget = useRemoteWidget({
|
||||
remoteConfig: inputSpec.remote,
|
||||
defaultValue,
|
||||
node,
|
||||
widget: widget as IComboWidget
|
||||
widget
|
||||
})
|
||||
if (inputSpec.remote.refresh_button) remoteWidget.addRefreshButton()
|
||||
|
||||
@@ -116,16 +136,19 @@ const addComboWidget = (
|
||||
}
|
||||
|
||||
if (inputSpec.control_after_generate) {
|
||||
if (!isComboWidget(widget)) {
|
||||
throw new Error(`Expected combo widget but received ${widget.type}`)
|
||||
}
|
||||
widget.linkedWidgets = addValueControlWidgets(
|
||||
node,
|
||||
widget as IComboWidget,
|
||||
widget,
|
||||
undefined,
|
||||
undefined,
|
||||
transformInputSpecV2ToV1(inputSpec)
|
||||
)
|
||||
}
|
||||
|
||||
return widget as IBaseWidget
|
||||
return widget
|
||||
}
|
||||
|
||||
export const useComboWidget = () => {
|
||||
|
||||
Reference in New Issue
Block a user