Merge remote-tracking branch 'origin/main' into bl-more-slots
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Nodes Zoom', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,13 +1,13 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { getSlotKey } from '../../../src/renderer/core/layout/slots/slotIdentifier'
|
||||
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../fixtures/ComfyPage'
|
||||
import { getMiddlePoint } from '../../fixtures/utils/litegraphUtils'
|
||||
import { fitToViewInstant } from '../../helpers/fitToView'
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import { getMiddlePoint } from '../../../../fixtures/utils/litegraphUtils'
|
||||
import { fitToViewInstant } from '../../../../helpers/fitToView'
|
||||
|
||||
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
|
||||
const box = await locator.boundingBox()
|
||||
@@ -189,6 +189,13 @@ test.describe('Vue Node Link Interaction', () => {
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||
expect(await clipInput.getLinkCount()).toBe(0)
|
||||
|
||||
const graphLinkDetails = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
clipNode.id,
|
||||
0
|
||||
)
|
||||
expect(graphLinkDetails).toBeNull()
|
||||
})
|
||||
|
||||
test('should not create a link when dropping onto a slot on the same node', async ({
|
||||
@@ -218,7 +225,6 @@ test.describe('Vue Node Link Interaction', () => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
|
||||
const samplerOutputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
|
After Width: | Height: | Size: 54 KiB |
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import { VueNodeFixture } from '../../../../fixtures/utils/vueNodeFixtures'
|
||||
|
||||
test.describe('Vue Nodes Renaming', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('should display node title', async ({ comfyPage }) => {
|
||||
// Get the KSampler node from the default workflow
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(nodes.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
const title = await vueNode.getTitle()
|
||||
expect(title).toBe('KSampler')
|
||||
|
||||
// Verify title is visible in the header
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('KSampler')
|
||||
})
|
||||
|
||||
test('should allow title renaming by double clicking on the node header', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
// Test renaming with Enter
|
||||
await vueNode.setTitle('My Custom Sampler')
|
||||
const newTitle = await vueNode.getTitle()
|
||||
expect(newTitle).toBe('My Custom Sampler')
|
||||
|
||||
// Verify the title is displayed
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('My Custom Sampler')
|
||||
|
||||
// Test cancel with Escape
|
||||
const titleElement = await vueNode.getTitleElement()
|
||||
await titleElement.dblclick()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Type a different value but cancel
|
||||
const input = (await vueNode.getHeader()).locator(
|
||||
'[data-testid="node-title-input"]'
|
||||
)
|
||||
await input.fill('This Should Be Cancelled')
|
||||
await input.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Title should remain as the previously saved value
|
||||
const titleAfterCancel = await vueNode.getTitle()
|
||||
expect(titleAfterCancel).toBe('My Custom Sampler')
|
||||
})
|
||||
|
||||
test('Double click node body does not trigger edit', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const nodeBbox = await loadCheckpointNode.boundingBox()
|
||||
if (!nodeBbox) throw new Error('Node not found')
|
||||
await loadCheckpointNode.dblclick()
|
||||
|
||||
const editingTitleInput = comfyPage.page.getByTestId('node-title-input')
|
||||
await expect(editingTitleInput).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
@@ -15,7 +15,8 @@ test.describe('Vue Node Selection', () => {
|
||||
|
||||
const modifiers = [
|
||||
{ key: 'Control', name: 'ctrl' },
|
||||
{ key: 'Shift', name: 'shift' }
|
||||
{ key: 'Shift', name: 'shift' },
|
||||
{ key: 'Meta', name: 'meta' }
|
||||
] as const
|
||||
|
||||
for (const { key: modifier, name } of modifiers) {
|
||||
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 107 KiB |
@@ -1,80 +1,20 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../fixtures/ComfyPage'
|
||||
import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
import { VueNodeFixture } from '../../../fixtures/utils/vueNodeFixtures'
|
||||
|
||||
test.describe('NodeHeader', () => {
|
||||
test.describe('Vue Node Collapse', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('displays node title', async ({ comfyPage }) => {
|
||||
// Get the KSampler node from the default workflow
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(nodes.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
const title = await vueNode.getTitle()
|
||||
expect(title).toBe('KSampler')
|
||||
|
||||
// Verify title is visible in the header
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('KSampler')
|
||||
})
|
||||
|
||||
test('allows title renaming', async ({ comfyPage }) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
// Test renaming with Enter
|
||||
await vueNode.setTitle('My Custom Sampler')
|
||||
const newTitle = await vueNode.getTitle()
|
||||
expect(newTitle).toBe('My Custom Sampler')
|
||||
|
||||
// Verify the title is displayed
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('My Custom Sampler')
|
||||
|
||||
// Test cancel with Escape
|
||||
const titleElement = await vueNode.getTitleElement()
|
||||
await titleElement.dblclick()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Type a different value but cancel
|
||||
const input = (await vueNode.getHeader()).locator(
|
||||
'[data-testid="node-title-input"]'
|
||||
)
|
||||
await input.fill('This Should Be Cancelled')
|
||||
await input.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Title should remain as the previously saved value
|
||||
const titleAfterCancel = await vueNode.getTitle()
|
||||
expect(titleAfterCancel).toBe('My Custom Sampler')
|
||||
})
|
||||
|
||||
test('Double click node body does not trigger edit', async ({
|
||||
test('should allow collapsing node with collapse icon', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const nodeBbox = await loadCheckpointNode.boundingBox()
|
||||
if (!nodeBbox) throw new Error('Node not found')
|
||||
await loadCheckpointNode.dblclick()
|
||||
|
||||
const editingTitleInput = comfyPage.page.getByTestId('node-title-input')
|
||||
await expect(editingTitleInput).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('handles node collapsing', async ({ comfyPage }) => {
|
||||
// Get the KSampler node from the default workflow
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
@@ -103,7 +43,7 @@ test.describe('NodeHeader', () => {
|
||||
expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height)
|
||||
})
|
||||
|
||||
test('shows collapse/expand icon state', async ({ comfyPage }) => {
|
||||
test('should show collapse/expand icon state', async ({ comfyPage }) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
@@ -123,7 +63,9 @@ test.describe('NodeHeader', () => {
|
||||
expect(iconClass).toContain('pi-chevron-down')
|
||||
})
|
||||
|
||||
test('preserves title when collapsing/expanding', async ({ comfyPage }) => {
|
||||
test('should preserve title when collapsing/expanding', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
49
browser_tests/tests/vueNodes/nodeStates/colors.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Node Custom Colors', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('displays color picker button and allows color selection', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode = comfyPage.page.locator('[data-node-id]').filter({
|
||||
hasText: 'Load Checkpoint'
|
||||
})
|
||||
await loadCheckpointNode.getByText('Load Checkpoint').click()
|
||||
|
||||
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
|
||||
await comfyPage.page
|
||||
.locator('.color-picker-container')
|
||||
.locator('i[data-testid="blue"]')
|
||||
.click()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-custom-color-blue.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should load node colors from workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/every_node_color')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-custom-colors-dark-all-colors.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should show brightened node colors on light theme', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.loadWorkflow('nodes/every_node_color')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-custom-colors-light-all-colors.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 91 KiB |
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 107 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.28.2",
|
||||
"version": "1.28.3",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -158,4 +158,4 @@
|
||||
"zod": "^3.23.8",
|
||||
"zod-validation-error": "^3.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
|
||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||
@@ -103,7 +104,6 @@ import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||
import { usePaste } from '@/composables/usePaste'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import { i18n, t } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
|
||||
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -189,8 +189,8 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const allNodes = computed(() =>
|
||||
Array.from(vueNodeLifecycle.vueNodeData.value.values())
|
||||
const allNodes = computed((): VueNodeData[] =>
|
||||
Array.from(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])
|
||||
)
|
||||
|
||||
watchEffect(() => {
|
||||
@@ -225,7 +225,6 @@ watch(
|
||||
for (const n of comfyApp.graph.nodes) {
|
||||
if (!n.widgets) continue
|
||||
for (const w of n.widgets) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
if (w[IS_CONTROL_WIDGET]) {
|
||||
updateControlWidgetLabel(w)
|
||||
if (w.linkedWidgets) {
|
||||
@@ -293,45 +292,36 @@ watch(
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Update node slot errors
|
||||
// Update node slot errors for LiteGraph nodes
|
||||
// (Vue nodes read from store directly)
|
||||
watch(
|
||||
() => executionStore.lastNodeErrors,
|
||||
(lastNodeErrors) => {
|
||||
const removeSlotError = (node: LGraphNode) => {
|
||||
if (!comfyApp.graph) return
|
||||
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
// Clear existing errors
|
||||
for (const slot of node.inputs) {
|
||||
delete slot.hasErrors
|
||||
}
|
||||
for (const slot of node.outputs) {
|
||||
delete slot.hasErrors
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of comfyApp.graph.nodes) {
|
||||
removeSlotError(node)
|
||||
const nodeErrors = lastNodeErrors?.[node.id]
|
||||
if (!nodeErrors) continue
|
||||
|
||||
const validErrors = nodeErrors.errors.filter(
|
||||
(error) => error.extra_info?.input_name !== undefined
|
||||
)
|
||||
const slotErrorsChanged =
|
||||
validErrors.length > 0 &&
|
||||
validErrors.some((error) => {
|
||||
const inputName = error.extra_info!.input_name!
|
||||
const inputIndex = node.findInputSlot(inputName)
|
||||
if (inputIndex !== -1) {
|
||||
node.inputs[inputIndex].hasErrors = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Trigger Vue node data update if slot errors changed
|
||||
if (slotErrorsChanged && comfyApp.graph.onTrigger) {
|
||||
comfyApp.graph.onTrigger('node:slot-errors:changed', {
|
||||
nodeId: node.id
|
||||
})
|
||||
}
|
||||
validErrors.forEach((error) => {
|
||||
const inputName = error.extra_info!.input_name!
|
||||
const inputIndex = node.findInputSlot(inputName)
|
||||
if (inputIndex !== -1) {
|
||||
node.inputs[inputIndex].hasErrors = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
comfyApp.canvas.draw(true, true)
|
||||
@@ -364,7 +354,6 @@ const loadCustomNodesI18n = async () => {
|
||||
|
||||
const comfyAppReady = ref(false)
|
||||
const workflowPersistence = useWorkflowPersistence()
|
||||
// @ts-expect-error fixme ts strict error
|
||||
useCanvasDrop(canvasRef)
|
||||
useLitegraphSettings()
|
||||
useNodeBadge()
|
||||
|
||||
@@ -41,6 +41,8 @@ export interface VueNodeData {
|
||||
collapsed?: boolean
|
||||
pinned?: boolean
|
||||
}
|
||||
color?: string
|
||||
bgcolor?: string
|
||||
}
|
||||
|
||||
export interface GraphNodeManager {
|
||||
@@ -126,7 +128,9 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
widgets: safeWidgets,
|
||||
inputs: node.inputs ? [...node.inputs] : undefined,
|
||||
outputs: node.outputs ? [...node.outputs] : undefined,
|
||||
flags: node.flags ? { ...node.flags } : undefined
|
||||
flags: node.flags ? { ...node.flags } : undefined,
|
||||
color: node.color || undefined,
|
||||
bgcolor: node.bgcolor || undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,6 +453,24 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
...currentData,
|
||||
mode: typeof event.newValue === 'number' ? event.newValue : 0
|
||||
})
|
||||
break
|
||||
case 'color':
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
color:
|
||||
typeof event.newValue === 'string'
|
||||
? event.newValue
|
||||
: undefined
|
||||
})
|
||||
break
|
||||
case 'bgcolor':
|
||||
vueNodeData.set(nodeId, {
|
||||
...currentData,
|
||||
bgcolor:
|
||||
typeof event.newValue === 'string'
|
||||
? event.newValue
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
import { readonly, ref, shallowRef, watch } from 'vue'
|
||||
import { shallowRef, watch } from 'vue'
|
||||
|
||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import type {
|
||||
GraphNodeManager,
|
||||
VueNodeData
|
||||
} from '@/composables/graph/useGraphNodeManager'
|
||||
import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
@@ -22,31 +19,19 @@ function useVueNodeLifecycleIndividual() {
|
||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||
|
||||
const nodeManager = shallowRef<GraphNodeManager | null>(null)
|
||||
const cleanupNodeManager = shallowRef<(() => void) | null>(null)
|
||||
|
||||
// Sync management
|
||||
const slotSync = shallowRef<ReturnType<typeof useSlotLayoutSync> | null>(null)
|
||||
const slotSyncStarted = ref(false)
|
||||
const linkSync = shallowRef<ReturnType<typeof useLinkLayoutSync> | null>(null)
|
||||
|
||||
// Vue node data state
|
||||
const vueNodeData = ref<ReadonlyMap<string, VueNodeData>>(new Map())
|
||||
|
||||
// Trigger for forcing computed re-evaluation
|
||||
const nodeDataTrigger = ref(0)
|
||||
const { startSync } = useLayoutSync()
|
||||
const linkSyncManager = useLinkLayoutSync()
|
||||
const slotSyncManager = useSlotLayoutSync()
|
||||
|
||||
const initializeNodeManager = () => {
|
||||
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
|
||||
const activeGraph = comfyApp.canvas?.graph || comfyApp.graph
|
||||
const activeGraph = comfyApp.canvas?.graph
|
||||
if (!activeGraph || nodeManager.value) return
|
||||
|
||||
// Initialize the core node manager
|
||||
const manager = useGraphNodeManager(activeGraph)
|
||||
nodeManager.value = manager
|
||||
cleanupNodeManager.value = manager.cleanup
|
||||
|
||||
// Use the manager's data maps
|
||||
vueNodeData.value = manager.vueNodeData
|
||||
|
||||
// Initialize layout system with existing nodes from active graph
|
||||
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
|
||||
@@ -76,46 +61,29 @@ function useVueNodeLifecycleIndividual() {
|
||||
}
|
||||
|
||||
// Initialize layout sync (one-way: Layout Store → LiteGraph)
|
||||
const { startSync } = useLayoutSync()
|
||||
startSync(canvasStore.canvas)
|
||||
|
||||
// Initialize link layout sync for event-driven updates
|
||||
const linkSyncManager = useLinkLayoutSync()
|
||||
linkSync.value = linkSyncManager
|
||||
if (comfyApp.canvas) {
|
||||
linkSyncManager.start(comfyApp.canvas)
|
||||
}
|
||||
|
||||
// Force computed properties to re-evaluate
|
||||
nodeDataTrigger.value++
|
||||
}
|
||||
|
||||
const disposeNodeManagerAndSyncs = () => {
|
||||
if (!nodeManager.value) return
|
||||
|
||||
try {
|
||||
cleanupNodeManager.value?.()
|
||||
nodeManager.value.cleanup()
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
nodeManager.value = null
|
||||
cleanupNodeManager.value = null
|
||||
|
||||
// Clean up link layout sync
|
||||
if (linkSync.value) {
|
||||
linkSync.value.stop()
|
||||
linkSync.value = null
|
||||
}
|
||||
|
||||
// Reset reactive maps to clean state
|
||||
vueNodeData.value = new Map()
|
||||
linkSyncManager.stop()
|
||||
}
|
||||
|
||||
// Watch for Vue nodes enabled state changes
|
||||
watch(
|
||||
() =>
|
||||
shouldRenderVueNodes.value &&
|
||||
Boolean(comfyApp.canvas?.graph || comfyApp.graph),
|
||||
() => shouldRenderVueNodes.value && Boolean(comfyApp.canvas?.graph),
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
initializeNodeManager()
|
||||
@@ -138,20 +106,14 @@ function useVueNodeLifecycleIndividual() {
|
||||
}
|
||||
|
||||
// Switching to Vue
|
||||
if (vueMode && slotSyncStarted.value) {
|
||||
slotSync.value?.stop()
|
||||
slotSyncStarted.value = false
|
||||
if (vueMode) {
|
||||
slotSyncManager.stop()
|
||||
}
|
||||
|
||||
// Switching to LG
|
||||
const shouldRun = Boolean(canvas?.graph) && !vueMode
|
||||
if (shouldRun && !slotSyncStarted.value && canvas) {
|
||||
// Initialize slot sync if not already created
|
||||
if (!slotSync.value) {
|
||||
slotSync.value = useSlotLayoutSync()
|
||||
}
|
||||
const started = slotSync.value.attemptStart(canvas as LGraphCanvas)
|
||||
slotSyncStarted.value = started
|
||||
if (shouldRun && canvas) {
|
||||
slotSyncManager.attemptStart(canvas as LGraphCanvas)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -159,26 +121,27 @@ function useVueNodeLifecycleIndividual() {
|
||||
|
||||
// Handle case where Vue nodes are enabled but graph starts empty
|
||||
const setupEmptyGraphListener = () => {
|
||||
const activeGraph = comfyApp.canvas?.graph
|
||||
if (
|
||||
shouldRenderVueNodes.value &&
|
||||
comfyApp.graph &&
|
||||
!nodeManager.value &&
|
||||
comfyApp.graph._nodes.length === 0
|
||||
!shouldRenderVueNodes.value ||
|
||||
nodeManager.value ||
|
||||
activeGraph?._nodes.length !== 0
|
||||
) {
|
||||
const originalOnNodeAdded = comfyApp.graph.onNodeAdded
|
||||
comfyApp.graph.onNodeAdded = function (node: LGraphNode) {
|
||||
// Restore original handler
|
||||
comfyApp.graph.onNodeAdded = originalOnNodeAdded
|
||||
return
|
||||
}
|
||||
const originalOnNodeAdded = activeGraph.onNodeAdded
|
||||
activeGraph.onNodeAdded = function (node: LGraphNode) {
|
||||
// Restore original handler
|
||||
activeGraph.onNodeAdded = originalOnNodeAdded
|
||||
|
||||
// Initialize node manager if needed
|
||||
if (shouldRenderVueNodes.value && !nodeManager.value) {
|
||||
initializeNodeManager()
|
||||
}
|
||||
// Initialize node manager if needed
|
||||
if (shouldRenderVueNodes.value && !nodeManager.value) {
|
||||
initializeNodeManager()
|
||||
}
|
||||
|
||||
// Call original handler
|
||||
if (originalOnNodeAdded) {
|
||||
originalOnNodeAdded.call(this, node)
|
||||
}
|
||||
// Call original handler
|
||||
if (originalOnNodeAdded) {
|
||||
originalOnNodeAdded.call(this, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,20 +152,12 @@ function useVueNodeLifecycleIndividual() {
|
||||
nodeManager.value.cleanup()
|
||||
nodeManager.value = null
|
||||
}
|
||||
if (slotSyncStarted.value) {
|
||||
slotSync.value?.stop()
|
||||
slotSyncStarted.value = false
|
||||
}
|
||||
slotSync.value = null
|
||||
if (linkSync.value) {
|
||||
linkSync.value.stop()
|
||||
linkSync.value = null
|
||||
}
|
||||
slotSyncManager.stop()
|
||||
linkSyncManager.stop()
|
||||
}
|
||||
|
||||
return {
|
||||
vueNodeData,
|
||||
nodeManager: readonly(nodeManager),
|
||||
nodeManager,
|
||||
|
||||
// Lifecycle methods
|
||||
initializeNodeManager,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
|
||||
export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement>) => {
|
||||
export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement | null>) => {
|
||||
const modelToNodeStore = useModelToNodeStore()
|
||||
const litegraphService = useLitegraphService()
|
||||
const workflowService = useWorkflowService()
|
||||
|
||||
@@ -2,19 +2,17 @@ import {
|
||||
draggable,
|
||||
dropTargetForElements
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { toValue } from 'vue'
|
||||
import { type MaybeRefOrGetter, onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
export function usePragmaticDroppable(
|
||||
dropTargetElement: HTMLElement | (() => HTMLElement),
|
||||
dropTargetElement: MaybeRefOrGetter<HTMLElement | null>,
|
||||
options: Omit<Parameters<typeof dropTargetForElements>[0], 'element'>
|
||||
) {
|
||||
let cleanup = () => {}
|
||||
|
||||
onMounted(() => {
|
||||
const element =
|
||||
typeof dropTargetElement === 'function'
|
||||
? dropTargetElement()
|
||||
: dropTargetElement
|
||||
const element = toValue(dropTargetElement)
|
||||
|
||||
if (!element) {
|
||||
return
|
||||
@@ -32,16 +30,13 @@ export function usePragmaticDroppable(
|
||||
}
|
||||
|
||||
export function usePragmaticDraggable(
|
||||
draggableElement: HTMLElement | (() => HTMLElement),
|
||||
draggableElement: MaybeRefOrGetter<HTMLElement | null>,
|
||||
options: Omit<Parameters<typeof draggable>[0], 'element'>
|
||||
) {
|
||||
let cleanup = () => {}
|
||||
|
||||
onMounted(() => {
|
||||
const element =
|
||||
typeof draggableElement === 'function'
|
||||
? draggableElement()
|
||||
: draggableElement
|
||||
const element = toValue(draggableElement)
|
||||
|
||||
if (!element) {
|
||||
return
|
||||
@@ -51,6 +46,7 @@ export function usePragmaticDraggable(
|
||||
element,
|
||||
...options
|
||||
})
|
||||
// TODO: Change to onScopeDispose
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -170,7 +170,7 @@ class GroupNodeBuilder {
|
||||
// Use the built in copyToClipboard function to generate the node data we need
|
||||
try {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const serialised = serialise(this.nodes, app.canvas.graph)
|
||||
const serialised = serialise(this.nodes, app.canvas?.graph)
|
||||
const config = JSON.parse(serialised)
|
||||
|
||||
storeLinkTypes(config)
|
||||
|
||||
@@ -757,9 +757,7 @@ export class LGraphCanvas
|
||||
|
||||
// Initialize link renderer if graph is available
|
||||
if (graph) {
|
||||
this.linkRenderer = new LitegraphLinkAdapter(graph)
|
||||
// Disable layout writes during render
|
||||
this.linkRenderer.enableLayoutStoreWrites = false
|
||||
this.linkRenderer = new LitegraphLinkAdapter(false)
|
||||
}
|
||||
|
||||
this.linkConnector.events.addEventListener('link-created', () =>
|
||||
@@ -1858,9 +1856,7 @@ export class LGraphCanvas
|
||||
newGraph.attachCanvas(this)
|
||||
|
||||
// Re-initialize link renderer with new graph
|
||||
this.linkRenderer = new LitegraphLinkAdapter(newGraph)
|
||||
// Disable layout writes during render
|
||||
this.linkRenderer.enableLayoutStoreWrites = false
|
||||
this.linkRenderer = new LitegraphLinkAdapter(false)
|
||||
|
||||
this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph })
|
||||
this.#dirty()
|
||||
|
||||
@@ -7,7 +7,9 @@ const DEFAULT_TRACKED_PROPERTIES: string[] = [
|
||||
'title',
|
||||
'flags.collapsed',
|
||||
'flags.pinned',
|
||||
'mode'
|
||||
'mode',
|
||||
'color',
|
||||
'bgcolor'
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -251,6 +251,8 @@ export interface IBaseWidget<
|
||||
TType extends string = string,
|
||||
TOptions extends IWidgetOptions<unknown> = IWidgetOptions<unknown>
|
||||
> {
|
||||
[symbol: symbol]: boolean
|
||||
|
||||
linkedWidgets?: IBaseWidget[]
|
||||
|
||||
name: string
|
||||
|
||||
@@ -116,7 +116,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||
newCanvas.canvas,
|
||||
'litegraph:set-graph',
|
||||
(event: CustomEvent<{ newGraph: LGraph; oldGraph: LGraph }>) => {
|
||||
const newGraph = event.detail?.newGraph || app.canvas?.graph
|
||||
const newGraph = event.detail?.newGraph ?? app.canvas?.graph // TODO: Ambiguous Graph
|
||||
currentGraph.value = newGraph
|
||||
isInSubgraph.value = Boolean(app.canvas?.subgraph)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* rendering data that can be consumed by the PathRenderer.
|
||||
* Maintains backward compatibility with existing litegraph integration.
|
||||
*/
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||
import type {
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
LinkMarkerShape,
|
||||
LinkRenderType
|
||||
} from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations'
|
||||
import {
|
||||
type ArrowShape,
|
||||
CanvasPathRenderer,
|
||||
@@ -54,142 +52,10 @@ export interface LinkRenderContext {
|
||||
disabledPattern?: CanvasPattern | null
|
||||
}
|
||||
|
||||
interface LinkRenderOptions {
|
||||
color?: CanvasColour
|
||||
flow?: boolean
|
||||
skipBorder?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export class LitegraphLinkAdapter {
|
||||
private graph: LGraph
|
||||
private pathRenderer: CanvasPathRenderer
|
||||
public enableLayoutStoreWrites = true
|
||||
private readonly pathRenderer = new CanvasPathRenderer()
|
||||
|
||||
constructor(graph: LGraph) {
|
||||
this.graph = graph
|
||||
this.pathRenderer = new CanvasPathRenderer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single link with all necessary data properly fetched
|
||||
* Populates link.path for hit detection
|
||||
*/
|
||||
renderLink(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
link: LLink,
|
||||
context: LinkRenderContext,
|
||||
options: LinkRenderOptions = {}
|
||||
): void {
|
||||
// Get nodes from graph
|
||||
const sourceNode = this.graph.getNodeById(link.origin_id)
|
||||
const targetNode = this.graph.getNodeById(link.target_id)
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
console.warn(`Cannot render link ${link.id}: missing nodes`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get slots from nodes
|
||||
const sourceSlot = sourceNode.outputs?.[link.origin_slot]
|
||||
const targetSlot = targetNode.inputs?.[link.target_slot]
|
||||
|
||||
if (!sourceSlot || !targetSlot) {
|
||||
console.warn(`Cannot render link ${link.id}: missing slots`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get positions using layout tree data if available
|
||||
const startPos = getSlotPosition(
|
||||
sourceNode,
|
||||
link.origin_slot,
|
||||
false // output
|
||||
)
|
||||
const endPos = getSlotPosition(
|
||||
targetNode,
|
||||
link.target_slot,
|
||||
true // input
|
||||
)
|
||||
|
||||
// Get directions from slots
|
||||
const startDir = sourceSlot.dir || LinkDirection.RIGHT
|
||||
const endDir = targetSlot.dir || LinkDirection.LEFT
|
||||
|
||||
// Convert to pure render data
|
||||
const linkData = this.convertToLinkRenderData(
|
||||
link,
|
||||
{ x: startPos[0], y: startPos[1] },
|
||||
{ x: endPos[0], y: endPos[1] },
|
||||
startDir,
|
||||
endDir,
|
||||
options
|
||||
)
|
||||
|
||||
// Convert context
|
||||
const pathContext = this.convertToPathRenderContext(context)
|
||||
|
||||
// Render using pure renderer
|
||||
const path = this.pathRenderer.drawLink(ctx, linkData, pathContext)
|
||||
|
||||
// Store path for hit detection
|
||||
link.path = path
|
||||
|
||||
// Update layout store when writes are enabled (event-driven path)
|
||||
if (this.enableLayoutStoreWrites && link.id !== -1) {
|
||||
// Calculate bounds and center only when writing
|
||||
const bounds = this.calculateLinkBounds(startPos, endPos, linkData)
|
||||
const centerPos = linkData.centerPos || {
|
||||
x: (startPos[0] + endPos[0]) / 2,
|
||||
y: (startPos[1] + endPos[1]) / 2
|
||||
}
|
||||
|
||||
layoutStore.updateLinkLayout(link.id, {
|
||||
id: link.id,
|
||||
path: path,
|
||||
bounds: bounds,
|
||||
centerPos: centerPos,
|
||||
sourceNodeId: String(link.origin_id),
|
||||
targetNodeId: String(link.target_id),
|
||||
sourceSlot: link.origin_slot,
|
||||
targetSlot: link.target_slot
|
||||
})
|
||||
|
||||
// Also update segment layout for the whole link (null rerouteId means final segment)
|
||||
layoutStore.updateLinkSegmentLayout(link.id, null, {
|
||||
path: path,
|
||||
bounds: bounds,
|
||||
centerPos: centerPos
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert litegraph link data to pure render format
|
||||
*/
|
||||
private convertToLinkRenderData(
|
||||
link: LLink,
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
startDir: LinkDirection,
|
||||
endDir: LinkDirection,
|
||||
options: LinkRenderOptions
|
||||
): LinkRenderData {
|
||||
return {
|
||||
id: String(link.id),
|
||||
startPoint,
|
||||
endPoint,
|
||||
startDirection: this.convertDirection(startDir),
|
||||
endDirection: this.convertDirection(endDir),
|
||||
color: options.color
|
||||
? String(options.color)
|
||||
: link.color
|
||||
? String(link.color)
|
||||
: undefined,
|
||||
type: link.type !== undefined ? String(link.type) : undefined,
|
||||
flow: options.flow || false,
|
||||
disabled: options.disabled || false
|
||||
}
|
||||
}
|
||||
constructor(public readonly enableLayoutStoreWrites = true) {}
|
||||
|
||||
/**
|
||||
* Convert LinkDirection enum to Direction string
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
* The layout store is the single source of truth.
|
||||
*/
|
||||
import { onUnmounted } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
|
||||
/**
|
||||
@@ -13,27 +15,27 @@ import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
* This replaces the bidirectional sync with a one-way sync
|
||||
*/
|
||||
export function useLayoutSync() {
|
||||
let unsubscribe: (() => void) | null = null
|
||||
const unsubscribe = ref<() => void>()
|
||||
|
||||
/**
|
||||
* Start syncing from Layout system to LiteGraph
|
||||
* This is one-way: Layout → LiteGraph only
|
||||
* Start syncing from Layout → LiteGraph
|
||||
*/
|
||||
function startSync(canvas: any) {
|
||||
function startSync(canvas: ReturnType<typeof useCanvasStore>['canvas']) {
|
||||
if (!canvas?.graph) return
|
||||
|
||||
// Cancel last subscription
|
||||
stopSync()
|
||||
// Subscribe to layout changes
|
||||
unsubscribe = layoutStore.onChange((change) => {
|
||||
unsubscribe.value = layoutStore.onChange((change) => {
|
||||
// Apply changes to LiteGraph regardless of source
|
||||
// The layout store is the single source of truth
|
||||
for (const nodeId of change.nodeIds) {
|
||||
const layout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||
if (!layout) continue
|
||||
|
||||
const liteNode = canvas.graph.getNodeById(parseInt(nodeId))
|
||||
const liteNode = canvas.graph?.getNodeById(parseInt(nodeId))
|
||||
if (!liteNode) continue
|
||||
|
||||
// Update position if changed
|
||||
if (
|
||||
liteNode.pos[0] !== layout.position.x ||
|
||||
liteNode.pos[1] !== layout.position.y
|
||||
@@ -42,7 +44,6 @@ export function useLayoutSync() {
|
||||
liteNode.pos[1] = layout.position.y
|
||||
}
|
||||
|
||||
// Update size if changed
|
||||
if (
|
||||
liteNode.size[0] !== layout.size.width ||
|
||||
liteNode.size[1] !== layout.size.height
|
||||
@@ -57,20 +58,12 @@ export function useLayoutSync() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop syncing
|
||||
*/
|
||||
function stopSync() {
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
unsubscribe = null
|
||||
}
|
||||
unsubscribe.value?.()
|
||||
unsubscribe.value = undefined
|
||||
}
|
||||
|
||||
// Auto-cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
stopSync()
|
||||
})
|
||||
onUnmounted(stopSync)
|
||||
|
||||
return {
|
||||
startSync,
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
/**
|
||||
* Composable for event-driven link layout synchronization
|
||||
*
|
||||
* Implements event-driven link layout updates decoupled from the render cycle.
|
||||
* Updates link geometry only when it actually changes (node move/resize, link create/delete,
|
||||
* reroute create/delete/move, collapse toggles).
|
||||
*/
|
||||
import log from 'loglevel'
|
||||
import { onUnmounted } from 'vue'
|
||||
import { tryOnScopeDispose } from '@vueuse/core'
|
||||
import { computed, ref, toValue } from 'vue'
|
||||
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||
@@ -20,23 +12,17 @@ import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculatio
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import type { LayoutChange } from '@/renderer/core/layout/types'
|
||||
|
||||
const logger = log.getLogger('useLinkLayoutSync')
|
||||
|
||||
/**
|
||||
* Composable for managing link layout synchronization
|
||||
*/
|
||||
export function useLinkLayoutSync() {
|
||||
let canvas: LGraphCanvas | null = null
|
||||
let graph: LGraph | null = null
|
||||
let offscreenCtx: CanvasRenderingContext2D | null = null
|
||||
let adapter: LitegraphLinkAdapter | null = null
|
||||
let unsubscribeLayoutChange: (() => void) | null = null
|
||||
let restoreHandlers: (() => void) | null = null
|
||||
const canvasRef = ref<LGraphCanvas>()
|
||||
const graphRef = computed(() => canvasRef.value?.graph)
|
||||
const unsubscribeLayoutChange = ref<() => void>()
|
||||
const adapter = new LitegraphLinkAdapter()
|
||||
|
||||
/**
|
||||
* Build link render context from canvas properties
|
||||
*/
|
||||
function buildLinkRenderContext(): LinkRenderContext {
|
||||
const canvas = toValue(canvasRef)
|
||||
if (!canvas) {
|
||||
throw new Error('Canvas not initialized')
|
||||
}
|
||||
@@ -73,7 +59,9 @@ export function useLinkLayoutSync() {
|
||||
* - No dragging state handling (pure geometry computation)
|
||||
*/
|
||||
function recomputeLinkById(linkId: number): void {
|
||||
if (!graph || !adapter || !offscreenCtx || !canvas) return
|
||||
const canvas = toValue(canvasRef)
|
||||
const graph = toValue(graphRef)
|
||||
if (!graph || !canvas) return
|
||||
|
||||
const link = graph.links.get(linkId)
|
||||
if (!link || link.id === -1) return // Skip floating/temp links
|
||||
@@ -131,7 +119,7 @@ export function useLinkLayoutSync() {
|
||||
|
||||
// Render segment to this reroute
|
||||
adapter.renderLinkDirect(
|
||||
offscreenCtx,
|
||||
canvas.ctx,
|
||||
segmentStartPos,
|
||||
reroute.pos,
|
||||
link,
|
||||
@@ -167,7 +155,7 @@ export function useLinkLayoutSync() {
|
||||
]
|
||||
|
||||
adapter.renderLinkDirect(
|
||||
offscreenCtx,
|
||||
canvas.ctx,
|
||||
lastReroute.pos,
|
||||
endPos,
|
||||
link,
|
||||
@@ -185,7 +173,7 @@ export function useLinkLayoutSync() {
|
||||
} else {
|
||||
// No reroutes - render direct link
|
||||
adapter.renderLinkDirect(
|
||||
offscreenCtx,
|
||||
canvas.ctx,
|
||||
startPos,
|
||||
endPos,
|
||||
link,
|
||||
@@ -206,6 +194,7 @@ export function useLinkLayoutSync() {
|
||||
* Recompute all links connected to a node
|
||||
*/
|
||||
function recomputeLinksForNode(nodeId: number): void {
|
||||
const graph = toValue(graphRef)
|
||||
if (!graph) return
|
||||
|
||||
const node = graph.getNodeById(nodeId)
|
||||
@@ -243,6 +232,7 @@ export function useLinkLayoutSync() {
|
||||
* Recompute all links associated with a reroute
|
||||
*/
|
||||
function recomputeLinksForReroute(rerouteId: number): void {
|
||||
const graph = toValue(graphRef)
|
||||
if (!graph) return
|
||||
|
||||
const reroute = graph.reroutes.get(rerouteId)
|
||||
@@ -258,105 +248,55 @@ export function useLinkLayoutSync() {
|
||||
* Start link layout sync with event-driven functionality
|
||||
*/
|
||||
function start(canvasInstance: LGraphCanvas): void {
|
||||
canvas = canvasInstance
|
||||
graph = canvas.graph
|
||||
if (!graph) return
|
||||
|
||||
// Create offscreen canvas context
|
||||
const offscreenCanvas = document.createElement('canvas')
|
||||
offscreenCtx = offscreenCanvas.getContext('2d')
|
||||
if (!offscreenCtx) {
|
||||
logger.error('Failed to create offscreen canvas context')
|
||||
return
|
||||
}
|
||||
|
||||
// Create dedicated adapter with layout writes enabled
|
||||
adapter = new LitegraphLinkAdapter(graph)
|
||||
adapter.enableLayoutStoreWrites = true
|
||||
canvasRef.value = canvasInstance
|
||||
if (!canvasInstance.graph) return
|
||||
|
||||
// Initial computation for all existing links
|
||||
for (const link of graph._links.values()) {
|
||||
for (const link of canvasInstance.graph._links.values()) {
|
||||
if (link.id !== -1) {
|
||||
recomputeLinkById(link.id)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe to layout store changes
|
||||
unsubscribeLayoutChange = layoutStore.onChange((change: LayoutChange) => {
|
||||
switch (change.operation.type) {
|
||||
case 'moveNode':
|
||||
case 'resizeNode':
|
||||
recomputeLinksForNode(parseInt(change.operation.nodeId))
|
||||
break
|
||||
case 'createLink':
|
||||
recomputeLinkById(change.operation.linkId)
|
||||
break
|
||||
case 'deleteLink':
|
||||
// No-op - store already cleaned by existing code
|
||||
break
|
||||
case 'createReroute':
|
||||
case 'deleteReroute':
|
||||
// Recompute all affected links
|
||||
if ('linkIds' in change.operation) {
|
||||
for (const linkId of change.operation.linkIds) {
|
||||
recomputeLinkById(linkId)
|
||||
unsubscribeLayoutChange.value?.()
|
||||
unsubscribeLayoutChange.value = layoutStore.onChange(
|
||||
(change: LayoutChange) => {
|
||||
switch (change.operation.type) {
|
||||
case 'moveNode':
|
||||
case 'resizeNode':
|
||||
recomputeLinksForNode(parseInt(change.operation.nodeId))
|
||||
break
|
||||
case 'createLink':
|
||||
recomputeLinkById(change.operation.linkId)
|
||||
break
|
||||
case 'deleteLink':
|
||||
// No-op - store already cleaned by existing code
|
||||
break
|
||||
case 'createReroute':
|
||||
case 'deleteReroute':
|
||||
// Recompute all affected links
|
||||
if ('linkIds' in change.operation) {
|
||||
for (const linkId of change.operation.linkIds) {
|
||||
recomputeLinkById(linkId)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'moveReroute':
|
||||
recomputeLinksForReroute(change.operation.rerouteId)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// Hook collapse events
|
||||
const origTrigger = graph.onTrigger
|
||||
|
||||
graph.onTrigger = (action: string, param: any) => {
|
||||
if (
|
||||
action === 'node:property:changed' &&
|
||||
param?.property === 'flags.collapsed'
|
||||
) {
|
||||
const nodeId = parseInt(String(param.nodeId))
|
||||
if (!isNaN(nodeId)) {
|
||||
recomputeLinksForNode(nodeId)
|
||||
break
|
||||
case 'moveReroute':
|
||||
recomputeLinksForReroute(change.operation.rerouteId)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (origTrigger) {
|
||||
origTrigger.call(graph, action, param)
|
||||
}
|
||||
}
|
||||
|
||||
// Store cleanup function
|
||||
restoreHandlers = () => {
|
||||
if (graph) {
|
||||
graph.onTrigger = origTrigger || undefined
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop link layout sync and cleanup all resources
|
||||
*/
|
||||
function stop(): void {
|
||||
if (unsubscribeLayoutChange) {
|
||||
unsubscribeLayoutChange()
|
||||
unsubscribeLayoutChange = null
|
||||
}
|
||||
if (restoreHandlers) {
|
||||
restoreHandlers()
|
||||
restoreHandlers = null
|
||||
}
|
||||
canvas = null
|
||||
graph = null
|
||||
offscreenCtx = null
|
||||
adapter = null
|
||||
unsubscribeLayoutChange.value?.()
|
||||
unsubscribeLayoutChange.value = undefined
|
||||
canvasRef.value = undefined
|
||||
}
|
||||
|
||||
// Auto-cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
stop()
|
||||
})
|
||||
tryOnScopeDispose(stop)
|
||||
|
||||
return {
|
||||
start,
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
/**
|
||||
* Composable for managing slot layout registration
|
||||
*
|
||||
* Implements event-driven slot registration decoupled from the draw cycle.
|
||||
* Registers slots once on initial load and keeps them updated when necessary.
|
||||
*/
|
||||
import { onUnmounted } from 'vue'
|
||||
import { tryOnScopeDispose } from '@vueuse/core'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
@@ -13,10 +8,6 @@ import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotC
|
||||
import { registerNodeSlots } from '@/renderer/core/layout/slots/register'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
|
||||
/**
|
||||
* Compute and register slot layouts for a node
|
||||
* @param node LiteGraph node to process
|
||||
*/
|
||||
function computeAndRegisterSlots(node: LGraphNode): void {
|
||||
const nodeId = String(node.id)
|
||||
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||
@@ -50,12 +41,9 @@ function computeAndRegisterSlots(node: LGraphNode): void {
|
||||
registerNodeSlots(nodeId, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for managing slot layout registration
|
||||
*/
|
||||
export function useSlotLayoutSync() {
|
||||
let unsubscribeLayoutChange: (() => void) | null = null
|
||||
let restoreHandlers: (() => void) | null = null
|
||||
const unsubscribeLayoutChange = ref<() => void>()
|
||||
const restoreHandlers = ref<() => void>()
|
||||
|
||||
/**
|
||||
* Attempt to start slot layout sync with full event-driven functionality
|
||||
@@ -77,7 +65,8 @@ export function useSlotLayoutSync() {
|
||||
}
|
||||
|
||||
// Layout changes → recompute slots for changed nodes
|
||||
unsubscribeLayoutChange = layoutStore.onChange((change) => {
|
||||
unsubscribeLayoutChange.value?.()
|
||||
unsubscribeLayoutChange.value = layoutStore.onChange((change) => {
|
||||
for (const nodeId of change.nodeIds) {
|
||||
const node = graph.getNodeById(parseInt(nodeId))
|
||||
if (node) {
|
||||
@@ -131,7 +120,7 @@ export function useSlotLayoutSync() {
|
||||
}
|
||||
|
||||
// Store cleanup function
|
||||
restoreHandlers = () => {
|
||||
restoreHandlers.value = () => {
|
||||
graph.onNodeAdded = origNodeAdded || undefined
|
||||
graph.onNodeRemoved = origNodeRemoved || undefined
|
||||
// Only restore onTrigger if Vue nodes are not active
|
||||
@@ -145,24 +134,14 @@ export function useSlotLayoutSync() {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop slot layout sync and cleanup all subscriptions
|
||||
*/
|
||||
function stop(): void {
|
||||
if (unsubscribeLayoutChange) {
|
||||
unsubscribeLayoutChange()
|
||||
unsubscribeLayoutChange = null
|
||||
}
|
||||
if (restoreHandlers) {
|
||||
restoreHandlers()
|
||||
restoreHandlers = null
|
||||
}
|
||||
unsubscribeLayoutChange.value?.()
|
||||
unsubscribeLayoutChange.value = undefined
|
||||
restoreHandlers.value?.()
|
||||
restoreHandlers.value = undefined
|
||||
}
|
||||
|
||||
// Auto-cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
stop()
|
||||
})
|
||||
tryOnScopeDispose(stop)
|
||||
|
||||
return {
|
||||
attemptStart,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<SlotConnectionDot
|
||||
ref="connectionDotRef"
|
||||
:color="slotColor"
|
||||
class="-translate-x-1/2"
|
||||
:class="cn('-translate-x-1/2', errorClassesDot)"
|
||||
v-on="readonly ? {} : { pointerdown: onPointerDown }"
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
<div class="relative">
|
||||
<span
|
||||
v-if="!dotOnly"
|
||||
class="whitespace-nowrap text-sm font-normal dark-theme:text-slate-200 text-stone-200 lod-toggle"
|
||||
:class="
|
||||
cn('whitespace-nowrap text-sm font-normal lod-toggle', labelClasses)
|
||||
"
|
||||
>
|
||||
{{ slotData.localized_name || slotData.name || `Input ${index}` }}
|
||||
</span>
|
||||
@@ -39,6 +41,7 @@ import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
|
||||
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import LODFallback from './LODFallback.vue'
|
||||
@@ -57,7 +60,30 @@ interface InputSlotProps {
|
||||
|
||||
const props = defineProps<InputSlotProps>()
|
||||
|
||||
// Error boundary implementation
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
const hasSlotError = computed(() => {
|
||||
const nodeErrors = executionStore.lastNodeErrors?.[props.nodeId ?? '']
|
||||
if (!nodeErrors) return false
|
||||
|
||||
const slotName = props.slotData.name
|
||||
return nodeErrors.errors.some(
|
||||
(error) => error.extra_info?.input_name === slotName
|
||||
)
|
||||
})
|
||||
|
||||
const errorClassesDot = computed(() => {
|
||||
return hasSlotError.value
|
||||
? 'ring-2 ring-error dark-theme:ring-error ring-offset-0 rounded-full'
|
||||
: ''
|
||||
})
|
||||
|
||||
const labelClasses = computed(() =>
|
||||
hasSlotError.value
|
||||
? 'text-error dark-theme:text-error font-medium'
|
||||
: 'dark-theme:text-slate-200 text-stone-200'
|
||||
)
|
||||
|
||||
const renderError = ref<string | null>(null)
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
|
||||
@@ -81,8 +107,12 @@ onErrorCaptured((error) => {
|
||||
return false
|
||||
})
|
||||
|
||||
// Get slot color based on type
|
||||
const slotColor = computed(() => getSlotColor(props.slotData.type))
|
||||
const slotColor = computed(() => {
|
||||
if (hasSlotError.value) {
|
||||
return 'var(--color-error)'
|
||||
}
|
||||
return getSlotColor(props.slotData.type)
|
||||
})
|
||||
|
||||
const slotWrapperClass = computed(() =>
|
||||
cn(
|
||||
@@ -103,8 +133,6 @@ const connectionDotRef = ref<ComponentPublicInstance<{
|
||||
}> | null>(null)
|
||||
const slotElRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Watch for when the child component's ref becomes available
|
||||
// Vue automatically unwraps the Ref when exposing it
|
||||
watchEffect(() => {
|
||||
const el = connectionDotRef.value?.slotElRef
|
||||
slotElRef.value = el || null
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
cn(
|
||||
'bg-white dark-theme:bg-charcoal-800',
|
||||
'lg-node absolute rounded-2xl',
|
||||
'border border-solid border-sand-100 dark-theme:border-charcoal-600',
|
||||
'border-2 border-solid border-sand-100 dark-theme:border-charcoal-600',
|
||||
// hover (only when node should handle events)
|
||||
shouldHandleNodePointerEvents &&
|
||||
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
|
||||
@@ -34,7 +34,9 @@
|
||||
:style="[
|
||||
{
|
||||
transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||
zIndex: zIndex
|
||||
zIndex: zIndex,
|
||||
backgroundColor: nodeBodyBackgroundColor,
|
||||
opacity: nodeOpacity
|
||||
},
|
||||
dragStyle
|
||||
]"
|
||||
@@ -47,9 +49,14 @@
|
||||
<SlotConnectionDot multi class="absolute left-0 -translate-x-1/2" />
|
||||
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
|
||||
</template>
|
||||
<!-- Header only updates on title/color changes -->
|
||||
<NodeHeader
|
||||
v-memo="[nodeData.title, isCollapsed, nodeData.flags?.pinned]"
|
||||
v-memo="[
|
||||
nodeData.title,
|
||||
nodeData.color,
|
||||
nodeData.bgcolor,
|
||||
isCollapsed,
|
||||
nodeData.flags?.pinned
|
||||
]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:collapsed="isCollapsed"
|
||||
@@ -94,7 +101,11 @@
|
||||
>
|
||||
<!-- Slots only rendered at full detail -->
|
||||
<NodeSlots
|
||||
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length]"
|
||||
v-memo="[
|
||||
nodeData.inputs?.length,
|
||||
nodeData.outputs?.length,
|
||||
executionStore.lastNodeErrors
|
||||
]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
@@ -139,6 +150,7 @@ import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
||||
@@ -148,9 +160,11 @@ import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composable
|
||||
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||
import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
|
||||
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import {
|
||||
getLocatorIdFromNodeData,
|
||||
getNodeByLocatorId
|
||||
@@ -205,22 +219,35 @@ const hasExecutionError = computed(
|
||||
() => executionStore.lastExecutionErrorNodeId === nodeData.id
|
||||
)
|
||||
|
||||
// Computed error states for styling
|
||||
const hasAnyError = computed((): boolean => {
|
||||
return !!(
|
||||
hasExecutionError.value ||
|
||||
nodeData.hasErrors ||
|
||||
error ||
|
||||
// Type assertions needed because VueNodeData.inputs/outputs are typed as unknown[]
|
||||
// but at runtime they contain INodeInputSlot/INodeOutputSlot objects
|
||||
nodeData.inputs?.some((slot) => slot?.hasErrors) ||
|
||||
nodeData.outputs?.some((slot) => slot?.hasErrors)
|
||||
(executionStore.lastNodeErrors?.[nodeData.id]?.errors.length ?? 0) > 0
|
||||
)
|
||||
})
|
||||
|
||||
const bypassed = computed((): boolean => nodeData.mode === 4)
|
||||
const muted = computed((): boolean => nodeData.mode === 2) // NEVER mode
|
||||
|
||||
const nodeBodyBackgroundColor = computed(() => {
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
if (!nodeData.bgcolor) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return applyLightThemeColor(
|
||||
nodeData.bgcolor,
|
||||
Boolean(colorPaletteStore.completedActivePalette.light_theme)
|
||||
)
|
||||
})
|
||||
|
||||
const nodeOpacity = computed(
|
||||
() => useSettingStore().get('Comfy.Node.Opacity') ?? 1
|
||||
)
|
||||
|
||||
// Use canvas interactions for proper wheel event handling and pointer event capture control
|
||||
const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()
|
||||
|
||||
@@ -289,26 +316,19 @@ const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
||||
)
|
||||
|
||||
const borderClass = computed(() => {
|
||||
if (hasAnyError.value) {
|
||||
return 'border-error dark-theme:border-error'
|
||||
}
|
||||
if (executing.value) {
|
||||
return 'border-blue-500'
|
||||
}
|
||||
return undefined
|
||||
return (
|
||||
(hasAnyError.value && 'border-error dark-theme:border-error') ||
|
||||
(executing.value && 'border-blue-500')
|
||||
)
|
||||
})
|
||||
|
||||
const outlineClass = computed(() => {
|
||||
if (!isSelected.value) {
|
||||
return undefined
|
||||
}
|
||||
if (hasAnyError.value) {
|
||||
return 'outline-error dark-theme:outline-error'
|
||||
}
|
||||
if (executing.value) {
|
||||
return 'outline-blue-500 dark-theme:outline-blue-500'
|
||||
}
|
||||
return 'outline-black dark-theme:outline-white'
|
||||
return (
|
||||
isSelected.value &&
|
||||
((hasAnyError.value && 'outline-error dark-theme:outline-error') ||
|
||||
(executing.value && 'outline-blue-500 dark-theme:outline-blue-500') ||
|
||||
'outline-black dark-theme:outline-white')
|
||||
)
|
||||
})
|
||||
|
||||
// Event handlers
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="lg-node-header p-4 rounded-t-2xl cursor-move"
|
||||
class="lg-node-header p-4 rounded-t-2xl w-full cursor-move"
|
||||
:style="headerStyle"
|
||||
:data-testid="`node-header-${nodeData?.id || ''}`"
|
||||
@dblclick="handleDoubleClick"
|
||||
>
|
||||
@@ -71,8 +72,11 @@ import EditableText from '@/components/common/EditableText.vue'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { st } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import {
|
||||
getLocatorIdFromNodeData,
|
||||
@@ -123,6 +127,23 @@ const tooltipConfig = computed(() => {
|
||||
return createTooltipConfig(description)
|
||||
})
|
||||
|
||||
const headerStyle = computed(() => {
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const opacity = useSettingStore().get('Comfy.Node.Opacity') ?? 1
|
||||
|
||||
if (!nodeData?.color) {
|
||||
return { backgroundColor: '', opacity }
|
||||
}
|
||||
|
||||
const headerColor = applyLightThemeColor(
|
||||
nodeData.color,
|
||||
Boolean(colorPaletteStore.completedActivePalette.light_theme)
|
||||
)
|
||||
|
||||
return { backgroundColor: headerColor, opacity }
|
||||
})
|
||||
|
||||
const resolveTitle = (info: VueNodeData | undefined) => {
|
||||
const title = (info?.title ?? '').trim()
|
||||
if (title.length > 0) return title
|
||||
|
||||
14
src/renderer/extensions/vueNodes/utils/nodeStyleUtils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
/**
|
||||
* Applies light theme color adjustments to a color
|
||||
*/
|
||||
export function applyLightThemeColor(
|
||||
color: string,
|
||||
isLightTheme: boolean
|
||||
): string {
|
||||
if (!color || !isLightTheme) {
|
||||
return color
|
||||
}
|
||||
return adjustColor(color, { lightness: 0.5 })
|
||||
}
|
||||
@@ -157,11 +157,22 @@ export class ComfyApp {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
_nodeOutputs: Record<string, any>
|
||||
nodePreviewImages: Record<string, string[]>
|
||||
// @ts-expect-error fixme ts strict error
|
||||
#graph: LGraph
|
||||
|
||||
private rootGraphInternal: LGraph | undefined
|
||||
|
||||
// TODO: Migrate internal usage to the
|
||||
/** @deprecated Use {@link rootGraph} instead */
|
||||
get graph() {
|
||||
return this.#graph
|
||||
return this.rootGraphInternal!
|
||||
}
|
||||
|
||||
get rootGraph(): LGraph | undefined {
|
||||
if (!this.rootGraphInternal) {
|
||||
console.error('ComfyApp graph accessed before initialization')
|
||||
}
|
||||
return this.rootGraphInternal
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
canvas: LGraphCanvas
|
||||
dragOverNode: LGraphNode | null = null
|
||||
@@ -765,8 +776,7 @@ export class ComfyApp {
|
||||
}
|
||||
}
|
||||
|
||||
#addAfterConfigureHandler() {
|
||||
const { graph } = this
|
||||
private addAfterConfigureHandler(graph: LGraph) {
|
||||
const { onConfigure } = graph
|
||||
graph.onConfigure = function (...args) {
|
||||
fixLinkInputSlots(this)
|
||||
@@ -809,10 +819,10 @@ export class ComfyApp {
|
||||
this.#addConfigureHandler()
|
||||
this.#addApiUpdateHandlers()
|
||||
|
||||
this.#graph = new LGraph()
|
||||
const graph = new LGraph()
|
||||
|
||||
// Register the subgraph - adds type wrapper for Litegraph's `createNode` factory
|
||||
this.graph.events.addEventListener('subgraph-created', (e) => {
|
||||
graph.events.addEventListener('subgraph-created', (e) => {
|
||||
try {
|
||||
const { subgraph, data } = e.detail
|
||||
useSubgraphService().registerNewSubgraph(subgraph, data)
|
||||
@@ -826,9 +836,10 @@ export class ComfyApp {
|
||||
}
|
||||
})
|
||||
|
||||
this.#addAfterConfigureHandler()
|
||||
this.addAfterConfigureHandler(graph)
|
||||
|
||||
this.canvas = new LGraphCanvas(canvasEl, this.graph)
|
||||
this.rootGraphInternal = graph
|
||||
this.canvas = new LGraphCanvas(canvasEl, graph)
|
||||
// Make canvas states reactive so we can observe changes on them.
|
||||
this.canvas.state = reactive(this.canvas.state)
|
||||
|
||||
|
||||
@@ -140,7 +140,6 @@ export function addValueControlWidgets(
|
||||
|
||||
valueControl.tooltip =
|
||||
'Allows the linked widget to be changed automatically, for example randomizing the noise seed.'
|
||||
// @ts-ignore index with symbol
|
||||
valueControl[IS_CONTROL_WIDGET] = true
|
||||
updateControlWidgetLabel(valueControl)
|
||||
const widgets: [IComboWidget, ...IStringWidget[]] = [valueControl]
|
||||
@@ -273,12 +272,10 @@ export function addValueControlWidgets(
|
||||
valueControl.beforeQueued = () => {
|
||||
if (controlValueRunBefore()) {
|
||||
// Don't run on first execution
|
||||
// @ts-ignore index with symbol
|
||||
if (valueControl[HAS_EXECUTED]) {
|
||||
applyWidgetControl()
|
||||
}
|
||||
}
|
||||
// @ts-ignore index with symbol
|
||||
valueControl[HAS_EXECUTED] = true
|
||||
}
|
||||
|
||||
|
||||