Merge remote-tracking branch 'origin/main' into bl-more-slots
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
comfyExpect as expect,
|
comfyExpect as expect,
|
||||||
comfyPageFixture as test
|
comfyPageFixture as test
|
||||||
} from '../../../fixtures/ComfyPage'
|
} from '../../../../fixtures/ComfyPage'
|
||||||
|
|
||||||
test.describe('Vue Nodes Zoom', () => {
|
test.describe('Vue Nodes Zoom', () => {
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
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 { Locator, Page } from '@playwright/test'
|
||||||
|
|
||||||
import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema'
|
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||||
import { getSlotKey } from '../../../src/renderer/core/layout/slots/slotIdentifier'
|
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
|
||||||
import {
|
import {
|
||||||
comfyExpect as expect,
|
comfyExpect as expect,
|
||||||
comfyPageFixture as test
|
comfyPageFixture as test
|
||||||
} from '../../fixtures/ComfyPage'
|
} from '../../../../fixtures/ComfyPage'
|
||||||
import { getMiddlePoint } from '../../fixtures/utils/litegraphUtils'
|
import { getMiddlePoint } from '../../../../fixtures/utils/litegraphUtils'
|
||||||
import { fitToViewInstant } from '../../helpers/fitToView'
|
import { fitToViewInstant } from '../../../../helpers/fitToView'
|
||||||
|
|
||||||
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
|
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
|
||||||
const box = await locator.boundingBox()
|
const box = await locator.boundingBox()
|
||||||
@@ -189,6 +189,13 @@ test.describe('Vue Node Link Interaction', () => {
|
|||||||
|
|
||||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||||
expect(await clipInput.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 ({
|
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 samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||||
expect(samplerNode && vaeNode).toBeTruthy()
|
expect(samplerNode && vaeNode).toBeTruthy()
|
||||||
|
|
||||||
const samplerOutputCenter = await getSlotCenter(
|
const samplerOutputCenter = await getSlotCenter(
|
||||||
comfyPage.page,
|
comfyPage.page,
|
||||||
samplerNode.id,
|
samplerNode.id,
|
||||||
|
After Width: | Height: | Size: 54 KiB |
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from '@playwright/test'
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage'
|
||||||
|
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
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 {
|
import {
|
||||||
comfyExpect as expect,
|
comfyExpect as expect,
|
||||||
comfyPageFixture as test
|
comfyPageFixture as test
|
||||||
} from '../../../fixtures/ComfyPage'
|
} from '../../../../fixtures/ComfyPage'
|
||||||
|
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||||
@@ -15,7 +15,8 @@ test.describe('Vue Node Selection', () => {
|
|||||||
|
|
||||||
const modifiers = [
|
const modifiers = [
|
||||||
{ key: 'Control', name: 'ctrl' },
|
{ key: 'Control', name: 'ctrl' },
|
||||||
{ key: 'Shift', name: 'shift' }
|
{ key: 'Shift', name: 'shift' },
|
||||||
|
{ key: 'Meta', name: 'meta' }
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
for (const { key: modifier, name } of modifiers) {
|
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 {
|
import {
|
||||||
comfyExpect as expect,
|
comfyExpect as expect,
|
||||||
comfyPageFixture as test
|
comfyPageFixture as test
|
||||||
} from '../../fixtures/ComfyPage'
|
} from '../../../fixtures/ComfyPage'
|
||||||
import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
|
import { VueNodeFixture } from '../../../fixtures/utils/vueNodeFixtures'
|
||||||
|
|
||||||
test.describe('NodeHeader', () => {
|
test.describe('Vue Node Collapse', () => {
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
|
||||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||||
await comfyPage.setup()
|
await comfyPage.setup()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('displays node title', async ({ comfyPage }) => {
|
test('should allow collapsing node with collapse icon', async ({
|
||||||
// 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 ({
|
|
||||||
comfyPage
|
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
|
// Get the KSampler node from the default workflow
|
||||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||||
const node = nodes[0]
|
const node = nodes[0]
|
||||||
@@ -103,7 +43,7 @@ test.describe('NodeHeader', () => {
|
|||||||
expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height)
|
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 nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||||
const node = nodes[0]
|
const node = nodes[0]
|
||||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||||
@@ -123,7 +63,9 @@ test.describe('NodeHeader', () => {
|
|||||||
expect(iconClass).toContain('pi-chevron-down')
|
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 nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||||
const node = nodes[0]
|
const node = nodes[0]
|
||||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
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 { expect } from '@playwright/test'
|
||||||
|
|
||||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
import { comfyPageFixture as test } from '../../../fixtures/ComfyPage'
|
||||||
|
|
||||||
test.beforeEach(async ({ comfyPage }) => {
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
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",
|
"name": "@comfyorg/comfyui-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.28.2",
|
"version": "1.28.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||||
"homepage": "https://comfy.org",
|
"homepage": "https://comfy.org",
|
||||||
@@ -158,4 +158,4 @@
|
|||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zod-validation-error": "^3.3.0"
|
"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 SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
|
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
|
||||||
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
|
||||||
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
import { useNodeBadge } from '@/composables/node/useNodeBadge'
|
||||||
@@ -103,7 +104,6 @@ import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
|||||||
import { usePaste } from '@/composables/usePaste'
|
import { usePaste } from '@/composables/usePaste'
|
||||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||||
import { i18n, t } from '@/i18n'
|
import { i18n, t } from '@/i18n'
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
||||||
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
|
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
|
||||||
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
|
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
@@ -189,8 +189,8 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const allNodes = computed(() =>
|
const allNodes = computed((): VueNodeData[] =>
|
||||||
Array.from(vueNodeLifecycle.vueNodeData.value.values())
|
Array.from(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])
|
||||||
)
|
)
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
@@ -225,7 +225,6 @@ watch(
|
|||||||
for (const n of comfyApp.graph.nodes) {
|
for (const n of comfyApp.graph.nodes) {
|
||||||
if (!n.widgets) continue
|
if (!n.widgets) continue
|
||||||
for (const w of n.widgets) {
|
for (const w of n.widgets) {
|
||||||
// @ts-expect-error fixme ts strict error
|
|
||||||
if (w[IS_CONTROL_WIDGET]) {
|
if (w[IS_CONTROL_WIDGET]) {
|
||||||
updateControlWidgetLabel(w)
|
updateControlWidgetLabel(w)
|
||||||
if (w.linkedWidgets) {
|
if (w.linkedWidgets) {
|
||||||
@@ -293,45 +292,36 @@ watch(
|
|||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update node slot errors
|
// Update node slot errors for LiteGraph nodes
|
||||||
|
// (Vue nodes read from store directly)
|
||||||
watch(
|
watch(
|
||||||
() => executionStore.lastNodeErrors,
|
() => executionStore.lastNodeErrors,
|
||||||
(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) {
|
for (const slot of node.inputs) {
|
||||||
delete slot.hasErrors
|
delete slot.hasErrors
|
||||||
}
|
}
|
||||||
for (const slot of node.outputs) {
|
for (const slot of node.outputs) {
|
||||||
delete slot.hasErrors
|
delete slot.hasErrors
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (const node of comfyApp.graph.nodes) {
|
|
||||||
removeSlotError(node)
|
|
||||||
const nodeErrors = lastNodeErrors?.[node.id]
|
const nodeErrors = lastNodeErrors?.[node.id]
|
||||||
if (!nodeErrors) continue
|
if (!nodeErrors) continue
|
||||||
|
|
||||||
const validErrors = nodeErrors.errors.filter(
|
const validErrors = nodeErrors.errors.filter(
|
||||||
(error) => error.extra_info?.input_name !== undefined
|
(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
|
validErrors.forEach((error) => {
|
||||||
if (slotErrorsChanged && comfyApp.graph.onTrigger) {
|
const inputName = error.extra_info!.input_name!
|
||||||
comfyApp.graph.onTrigger('node:slot-errors:changed', {
|
const inputIndex = node.findInputSlot(inputName)
|
||||||
nodeId: node.id
|
if (inputIndex !== -1) {
|
||||||
})
|
node.inputs[inputIndex].hasErrors = true
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
comfyApp.canvas.draw(true, true)
|
comfyApp.canvas.draw(true, true)
|
||||||
@@ -364,7 +354,6 @@ const loadCustomNodesI18n = async () => {
|
|||||||
|
|
||||||
const comfyAppReady = ref(false)
|
const comfyAppReady = ref(false)
|
||||||
const workflowPersistence = useWorkflowPersistence()
|
const workflowPersistence = useWorkflowPersistence()
|
||||||
// @ts-expect-error fixme ts strict error
|
|
||||||
useCanvasDrop(canvasRef)
|
useCanvasDrop(canvasRef)
|
||||||
useLitegraphSettings()
|
useLitegraphSettings()
|
||||||
useNodeBadge()
|
useNodeBadge()
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export interface VueNodeData {
|
|||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
pinned?: boolean
|
pinned?: boolean
|
||||||
}
|
}
|
||||||
|
color?: string
|
||||||
|
bgcolor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GraphNodeManager {
|
export interface GraphNodeManager {
|
||||||
@@ -126,7 +128,9 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
|||||||
widgets: safeWidgets,
|
widgets: safeWidgets,
|
||||||
inputs: node.inputs ? [...node.inputs] : undefined,
|
inputs: node.inputs ? [...node.inputs] : undefined,
|
||||||
outputs: node.outputs ? [...node.outputs] : 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,
|
...currentData,
|
||||||
mode: typeof event.newValue === 'number' ? event.newValue : 0
|
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 (
|
} else if (
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import { createSharedComposable } from '@vueuse/core'
|
import { createSharedComposable } from '@vueuse/core'
|
||||||
import { readonly, ref, shallowRef, watch } from 'vue'
|
import { shallowRef, watch } from 'vue'
|
||||||
|
|
||||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||||
import type {
|
import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||||
GraphNodeManager,
|
|
||||||
VueNodeData
|
|
||||||
} from '@/composables/graph/useGraphNodeManager'
|
|
||||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
@@ -22,31 +19,19 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||||
|
|
||||||
const nodeManager = shallowRef<GraphNodeManager | null>(null)
|
const nodeManager = shallowRef<GraphNodeManager | null>(null)
|
||||||
const cleanupNodeManager = shallowRef<(() => void) | null>(null)
|
|
||||||
|
|
||||||
// Sync management
|
const { startSync } = useLayoutSync()
|
||||||
const slotSync = shallowRef<ReturnType<typeof useSlotLayoutSync> | null>(null)
|
const linkSyncManager = useLinkLayoutSync()
|
||||||
const slotSyncStarted = ref(false)
|
const slotSyncManager = useSlotLayoutSync()
|
||||||
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 initializeNodeManager = () => {
|
const initializeNodeManager = () => {
|
||||||
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
|
// 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
|
if (!activeGraph || nodeManager.value) return
|
||||||
|
|
||||||
// Initialize the core node manager
|
// Initialize the core node manager
|
||||||
const manager = useGraphNodeManager(activeGraph)
|
const manager = useGraphNodeManager(activeGraph)
|
||||||
nodeManager.value = manager
|
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
|
// Initialize layout system with existing nodes from active graph
|
||||||
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
|
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
|
||||||
@@ -76,46 +61,29 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize layout sync (one-way: Layout Store → LiteGraph)
|
// Initialize layout sync (one-way: Layout Store → LiteGraph)
|
||||||
const { startSync } = useLayoutSync()
|
|
||||||
startSync(canvasStore.canvas)
|
startSync(canvasStore.canvas)
|
||||||
|
|
||||||
// Initialize link layout sync for event-driven updates
|
|
||||||
const linkSyncManager = useLinkLayoutSync()
|
|
||||||
linkSync.value = linkSyncManager
|
|
||||||
if (comfyApp.canvas) {
|
if (comfyApp.canvas) {
|
||||||
linkSyncManager.start(comfyApp.canvas)
|
linkSyncManager.start(comfyApp.canvas)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force computed properties to re-evaluate
|
|
||||||
nodeDataTrigger.value++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const disposeNodeManagerAndSyncs = () => {
|
const disposeNodeManagerAndSyncs = () => {
|
||||||
if (!nodeManager.value) return
|
if (!nodeManager.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
cleanupNodeManager.value?.()
|
nodeManager.value.cleanup()
|
||||||
} catch {
|
} catch {
|
||||||
/* empty */
|
/* empty */
|
||||||
}
|
}
|
||||||
nodeManager.value = null
|
nodeManager.value = null
|
||||||
cleanupNodeManager.value = null
|
|
||||||
|
|
||||||
// Clean up link layout sync
|
linkSyncManager.stop()
|
||||||
if (linkSync.value) {
|
|
||||||
linkSync.value.stop()
|
|
||||||
linkSync.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset reactive maps to clean state
|
|
||||||
vueNodeData.value = new Map()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for Vue nodes enabled state changes
|
// Watch for Vue nodes enabled state changes
|
||||||
watch(
|
watch(
|
||||||
() =>
|
() => shouldRenderVueNodes.value && Boolean(comfyApp.canvas?.graph),
|
||||||
shouldRenderVueNodes.value &&
|
|
||||||
Boolean(comfyApp.canvas?.graph || comfyApp.graph),
|
|
||||||
(enabled) => {
|
(enabled) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
initializeNodeManager()
|
initializeNodeManager()
|
||||||
@@ -138,20 +106,14 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Switching to Vue
|
// Switching to Vue
|
||||||
if (vueMode && slotSyncStarted.value) {
|
if (vueMode) {
|
||||||
slotSync.value?.stop()
|
slotSyncManager.stop()
|
||||||
slotSyncStarted.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Switching to LG
|
// Switching to LG
|
||||||
const shouldRun = Boolean(canvas?.graph) && !vueMode
|
const shouldRun = Boolean(canvas?.graph) && !vueMode
|
||||||
if (shouldRun && !slotSyncStarted.value && canvas) {
|
if (shouldRun && canvas) {
|
||||||
// Initialize slot sync if not already created
|
slotSyncManager.attemptStart(canvas as LGraphCanvas)
|
||||||
if (!slotSync.value) {
|
|
||||||
slotSync.value = useSlotLayoutSync()
|
|
||||||
}
|
|
||||||
const started = slotSync.value.attemptStart(canvas as LGraphCanvas)
|
|
||||||
slotSyncStarted.value = started
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -159,26 +121,27 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
|
|
||||||
// Handle case where Vue nodes are enabled but graph starts empty
|
// Handle case where Vue nodes are enabled but graph starts empty
|
||||||
const setupEmptyGraphListener = () => {
|
const setupEmptyGraphListener = () => {
|
||||||
|
const activeGraph = comfyApp.canvas?.graph
|
||||||
if (
|
if (
|
||||||
shouldRenderVueNodes.value &&
|
!shouldRenderVueNodes.value ||
|
||||||
comfyApp.graph &&
|
nodeManager.value ||
|
||||||
!nodeManager.value &&
|
activeGraph?._nodes.length !== 0
|
||||||
comfyApp.graph._nodes.length === 0
|
|
||||||
) {
|
) {
|
||||||
const originalOnNodeAdded = comfyApp.graph.onNodeAdded
|
return
|
||||||
comfyApp.graph.onNodeAdded = function (node: LGraphNode) {
|
}
|
||||||
// Restore original handler
|
const originalOnNodeAdded = activeGraph.onNodeAdded
|
||||||
comfyApp.graph.onNodeAdded = originalOnNodeAdded
|
activeGraph.onNodeAdded = function (node: LGraphNode) {
|
||||||
|
// Restore original handler
|
||||||
|
activeGraph.onNodeAdded = originalOnNodeAdded
|
||||||
|
|
||||||
// Initialize node manager if needed
|
// Initialize node manager if needed
|
||||||
if (shouldRenderVueNodes.value && !nodeManager.value) {
|
if (shouldRenderVueNodes.value && !nodeManager.value) {
|
||||||
initializeNodeManager()
|
initializeNodeManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call original handler
|
// Call original handler
|
||||||
if (originalOnNodeAdded) {
|
if (originalOnNodeAdded) {
|
||||||
originalOnNodeAdded.call(this, node)
|
originalOnNodeAdded.call(this, node)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,20 +152,12 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
nodeManager.value.cleanup()
|
nodeManager.value.cleanup()
|
||||||
nodeManager.value = null
|
nodeManager.value = null
|
||||||
}
|
}
|
||||||
if (slotSyncStarted.value) {
|
slotSyncManager.stop()
|
||||||
slotSync.value?.stop()
|
linkSyncManager.stop()
|
||||||
slotSyncStarted.value = false
|
|
||||||
}
|
|
||||||
slotSync.value = null
|
|
||||||
if (linkSync.value) {
|
|
||||||
linkSync.value.stop()
|
|
||||||
linkSync.value = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
vueNodeData,
|
nodeManager,
|
||||||
nodeManager: readonly(nodeManager),
|
|
||||||
|
|
||||||
// Lifecycle methods
|
// Lifecycle methods
|
||||||
initializeNodeManager,
|
initializeNodeManager,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
|||||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||||
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||||
|
|
||||||
export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement>) => {
|
export const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement | null>) => {
|
||||||
const modelToNodeStore = useModelToNodeStore()
|
const modelToNodeStore = useModelToNodeStore()
|
||||||
const litegraphService = useLitegraphService()
|
const litegraphService = useLitegraphService()
|
||||||
const workflowService = useWorkflowService()
|
const workflowService = useWorkflowService()
|
||||||
|
|||||||
@@ -2,19 +2,17 @@ import {
|
|||||||
draggable,
|
draggable,
|
||||||
dropTargetForElements
|
dropTargetForElements
|
||||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
|
} 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(
|
export function usePragmaticDroppable(
|
||||||
dropTargetElement: HTMLElement | (() => HTMLElement),
|
dropTargetElement: MaybeRefOrGetter<HTMLElement | null>,
|
||||||
options: Omit<Parameters<typeof dropTargetForElements>[0], 'element'>
|
options: Omit<Parameters<typeof dropTargetForElements>[0], 'element'>
|
||||||
) {
|
) {
|
||||||
let cleanup = () => {}
|
let cleanup = () => {}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const element =
|
const element = toValue(dropTargetElement)
|
||||||
typeof dropTargetElement === 'function'
|
|
||||||
? dropTargetElement()
|
|
||||||
: dropTargetElement
|
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return
|
return
|
||||||
@@ -32,16 +30,13 @@ export function usePragmaticDroppable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function usePragmaticDraggable(
|
export function usePragmaticDraggable(
|
||||||
draggableElement: HTMLElement | (() => HTMLElement),
|
draggableElement: MaybeRefOrGetter<HTMLElement | null>,
|
||||||
options: Omit<Parameters<typeof draggable>[0], 'element'>
|
options: Omit<Parameters<typeof draggable>[0], 'element'>
|
||||||
) {
|
) {
|
||||||
let cleanup = () => {}
|
let cleanup = () => {}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const element =
|
const element = toValue(draggableElement)
|
||||||
typeof draggableElement === 'function'
|
|
||||||
? draggableElement()
|
|
||||||
: draggableElement
|
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return
|
return
|
||||||
@@ -51,6 +46,7 @@ export function usePragmaticDraggable(
|
|||||||
element,
|
element,
|
||||||
...options
|
...options
|
||||||
})
|
})
|
||||||
|
// TODO: Change to onScopeDispose
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ class GroupNodeBuilder {
|
|||||||
// Use the built in copyToClipboard function to generate the node data we need
|
// Use the built in copyToClipboard function to generate the node data we need
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error fixme ts strict error
|
// @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)
|
const config = JSON.parse(serialised)
|
||||||
|
|
||||||
storeLinkTypes(config)
|
storeLinkTypes(config)
|
||||||
|
|||||||
@@ -757,9 +757,7 @@ export class LGraphCanvas
|
|||||||
|
|
||||||
// Initialize link renderer if graph is available
|
// Initialize link renderer if graph is available
|
||||||
if (graph) {
|
if (graph) {
|
||||||
this.linkRenderer = new LitegraphLinkAdapter(graph)
|
this.linkRenderer = new LitegraphLinkAdapter(false)
|
||||||
// Disable layout writes during render
|
|
||||||
this.linkRenderer.enableLayoutStoreWrites = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.linkConnector.events.addEventListener('link-created', () =>
|
this.linkConnector.events.addEventListener('link-created', () =>
|
||||||
@@ -1858,9 +1856,7 @@ export class LGraphCanvas
|
|||||||
newGraph.attachCanvas(this)
|
newGraph.attachCanvas(this)
|
||||||
|
|
||||||
// Re-initialize link renderer with new graph
|
// Re-initialize link renderer with new graph
|
||||||
this.linkRenderer = new LitegraphLinkAdapter(newGraph)
|
this.linkRenderer = new LitegraphLinkAdapter(false)
|
||||||
// Disable layout writes during render
|
|
||||||
this.linkRenderer.enableLayoutStoreWrites = false
|
|
||||||
|
|
||||||
this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph })
|
this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph })
|
||||||
this.#dirty()
|
this.#dirty()
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ const DEFAULT_TRACKED_PROPERTIES: string[] = [
|
|||||||
'title',
|
'title',
|
||||||
'flags.collapsed',
|
'flags.collapsed',
|
||||||
'flags.pinned',
|
'flags.pinned',
|
||||||
'mode'
|
'mode',
|
||||||
|
'color',
|
||||||
|
'bgcolor'
|
||||||
]
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ export interface IBaseWidget<
|
|||||||
TType extends string = string,
|
TType extends string = string,
|
||||||
TOptions extends IWidgetOptions<unknown> = IWidgetOptions<unknown>
|
TOptions extends IWidgetOptions<unknown> = IWidgetOptions<unknown>
|
||||||
> {
|
> {
|
||||||
|
[symbol: symbol]: boolean
|
||||||
|
|
||||||
linkedWidgets?: IBaseWidget[]
|
linkedWidgets?: IBaseWidget[]
|
||||||
|
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export const useCanvasStore = defineStore('canvas', () => {
|
|||||||
newCanvas.canvas,
|
newCanvas.canvas,
|
||||||
'litegraph:set-graph',
|
'litegraph:set-graph',
|
||||||
(event: CustomEvent<{ newGraph: LGraph; oldGraph: LGraph }>) => {
|
(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
|
currentGraph.value = newGraph
|
||||||
isInSubgraph.value = Boolean(app.canvas?.subgraph)
|
isInSubgraph.value = Boolean(app.canvas?.subgraph)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
* rendering data that can be consumed by the PathRenderer.
|
* rendering data that can be consumed by the PathRenderer.
|
||||||
* Maintains backward compatibility with existing litegraph integration.
|
* 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 { LLink } from '@/lib/litegraph/src/LLink'
|
||||||
import type { Reroute } from '@/lib/litegraph/src/Reroute'
|
import type { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||||
import type {
|
import type {
|
||||||
@@ -19,7 +18,6 @@ import {
|
|||||||
LinkMarkerShape,
|
LinkMarkerShape,
|
||||||
LinkRenderType
|
LinkRenderType
|
||||||
} from '@/lib/litegraph/src/types/globalEnums'
|
} from '@/lib/litegraph/src/types/globalEnums'
|
||||||
import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations'
|
|
||||||
import {
|
import {
|
||||||
type ArrowShape,
|
type ArrowShape,
|
||||||
CanvasPathRenderer,
|
CanvasPathRenderer,
|
||||||
@@ -54,142 +52,10 @@ export interface LinkRenderContext {
|
|||||||
disabledPattern?: CanvasPattern | null
|
disabledPattern?: CanvasPattern | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LinkRenderOptions {
|
|
||||||
color?: CanvasColour
|
|
||||||
flow?: boolean
|
|
||||||
skipBorder?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LitegraphLinkAdapter {
|
export class LitegraphLinkAdapter {
|
||||||
private graph: LGraph
|
private readonly pathRenderer = new CanvasPathRenderer()
|
||||||
private pathRenderer: CanvasPathRenderer
|
|
||||||
public enableLayoutStoreWrites = true
|
|
||||||
|
|
||||||
constructor(graph: LGraph) {
|
constructor(public readonly enableLayoutStoreWrites = true) {}
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert LinkDirection enum to Direction string
|
* Convert LinkDirection enum to Direction string
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
* The layout store is the single source of truth.
|
* The layout store is the single source of truth.
|
||||||
*/
|
*/
|
||||||
import { onUnmounted } from 'vue'
|
import { onUnmounted } from 'vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import type { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
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
|
* This replaces the bidirectional sync with a one-way sync
|
||||||
*/
|
*/
|
||||||
export function useLayoutSync() {
|
export function useLayoutSync() {
|
||||||
let unsubscribe: (() => void) | null = null
|
const unsubscribe = ref<() => void>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start syncing from Layout system to LiteGraph
|
* Start syncing from Layout → LiteGraph
|
||||||
* This is one-way: Layout → LiteGraph only
|
|
||||||
*/
|
*/
|
||||||
function startSync(canvas: any) {
|
function startSync(canvas: ReturnType<typeof useCanvasStore>['canvas']) {
|
||||||
if (!canvas?.graph) return
|
if (!canvas?.graph) return
|
||||||
|
|
||||||
|
// Cancel last subscription
|
||||||
|
stopSync()
|
||||||
// Subscribe to layout changes
|
// Subscribe to layout changes
|
||||||
unsubscribe = layoutStore.onChange((change) => {
|
unsubscribe.value = layoutStore.onChange((change) => {
|
||||||
// Apply changes to LiteGraph regardless of source
|
// Apply changes to LiteGraph regardless of source
|
||||||
// The layout store is the single source of truth
|
// The layout store is the single source of truth
|
||||||
for (const nodeId of change.nodeIds) {
|
for (const nodeId of change.nodeIds) {
|
||||||
const layout = layoutStore.getNodeLayoutRef(nodeId).value
|
const layout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||||
if (!layout) continue
|
if (!layout) continue
|
||||||
|
|
||||||
const liteNode = canvas.graph.getNodeById(parseInt(nodeId))
|
const liteNode = canvas.graph?.getNodeById(parseInt(nodeId))
|
||||||
if (!liteNode) continue
|
if (!liteNode) continue
|
||||||
|
|
||||||
// Update position if changed
|
|
||||||
if (
|
if (
|
||||||
liteNode.pos[0] !== layout.position.x ||
|
liteNode.pos[0] !== layout.position.x ||
|
||||||
liteNode.pos[1] !== layout.position.y
|
liteNode.pos[1] !== layout.position.y
|
||||||
@@ -42,7 +44,6 @@ export function useLayoutSync() {
|
|||||||
liteNode.pos[1] = layout.position.y
|
liteNode.pos[1] = layout.position.y
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update size if changed
|
|
||||||
if (
|
if (
|
||||||
liteNode.size[0] !== layout.size.width ||
|
liteNode.size[0] !== layout.size.width ||
|
||||||
liteNode.size[1] !== layout.size.height
|
liteNode.size[1] !== layout.size.height
|
||||||
@@ -57,20 +58,12 @@ export function useLayoutSync() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop syncing
|
|
||||||
*/
|
|
||||||
function stopSync() {
|
function stopSync() {
|
||||||
if (unsubscribe) {
|
unsubscribe.value?.()
|
||||||
unsubscribe()
|
unsubscribe.value = undefined
|
||||||
unsubscribe = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-cleanup on unmount
|
onUnmounted(stopSync)
|
||||||
onUnmounted(() => {
|
|
||||||
stopSync()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startSync,
|
startSync,
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
/**
|
import { tryOnScopeDispose } from '@vueuse/core'
|
||||||
* Composable for event-driven link layout synchronization
|
import { computed, ref, toValue } from 'vue'
|
||||||
*
|
|
||||||
* 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 type { LGraph } from '@/lib/litegraph/src/LGraph'
|
|
||||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||||
import { Reroute } from '@/lib/litegraph/src/Reroute'
|
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 { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
import type { LayoutChange } from '@/renderer/core/layout/types'
|
import type { LayoutChange } from '@/renderer/core/layout/types'
|
||||||
|
|
||||||
const logger = log.getLogger('useLinkLayoutSync')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable for managing link layout synchronization
|
|
||||||
*/
|
|
||||||
export function useLinkLayoutSync() {
|
export function useLinkLayoutSync() {
|
||||||
let canvas: LGraphCanvas | null = null
|
const canvasRef = ref<LGraphCanvas>()
|
||||||
let graph: LGraph | null = null
|
const graphRef = computed(() => canvasRef.value?.graph)
|
||||||
let offscreenCtx: CanvasRenderingContext2D | null = null
|
const unsubscribeLayoutChange = ref<() => void>()
|
||||||
let adapter: LitegraphLinkAdapter | null = null
|
const adapter = new LitegraphLinkAdapter()
|
||||||
let unsubscribeLayoutChange: (() => void) | null = null
|
|
||||||
let restoreHandlers: (() => void) | null = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build link render context from canvas properties
|
* Build link render context from canvas properties
|
||||||
*/
|
*/
|
||||||
function buildLinkRenderContext(): LinkRenderContext {
|
function buildLinkRenderContext(): LinkRenderContext {
|
||||||
|
const canvas = toValue(canvasRef)
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
throw new Error('Canvas not initialized')
|
throw new Error('Canvas not initialized')
|
||||||
}
|
}
|
||||||
@@ -73,7 +59,9 @@ export function useLinkLayoutSync() {
|
|||||||
* - No dragging state handling (pure geometry computation)
|
* - No dragging state handling (pure geometry computation)
|
||||||
*/
|
*/
|
||||||
function recomputeLinkById(linkId: number): void {
|
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)
|
const link = graph.links.get(linkId)
|
||||||
if (!link || link.id === -1) return // Skip floating/temp links
|
if (!link || link.id === -1) return // Skip floating/temp links
|
||||||
@@ -131,7 +119,7 @@ export function useLinkLayoutSync() {
|
|||||||
|
|
||||||
// Render segment to this reroute
|
// Render segment to this reroute
|
||||||
adapter.renderLinkDirect(
|
adapter.renderLinkDirect(
|
||||||
offscreenCtx,
|
canvas.ctx,
|
||||||
segmentStartPos,
|
segmentStartPos,
|
||||||
reroute.pos,
|
reroute.pos,
|
||||||
link,
|
link,
|
||||||
@@ -167,7 +155,7 @@ export function useLinkLayoutSync() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
adapter.renderLinkDirect(
|
adapter.renderLinkDirect(
|
||||||
offscreenCtx,
|
canvas.ctx,
|
||||||
lastReroute.pos,
|
lastReroute.pos,
|
||||||
endPos,
|
endPos,
|
||||||
link,
|
link,
|
||||||
@@ -185,7 +173,7 @@ export function useLinkLayoutSync() {
|
|||||||
} else {
|
} else {
|
||||||
// No reroutes - render direct link
|
// No reroutes - render direct link
|
||||||
adapter.renderLinkDirect(
|
adapter.renderLinkDirect(
|
||||||
offscreenCtx,
|
canvas.ctx,
|
||||||
startPos,
|
startPos,
|
||||||
endPos,
|
endPos,
|
||||||
link,
|
link,
|
||||||
@@ -206,6 +194,7 @@ export function useLinkLayoutSync() {
|
|||||||
* Recompute all links connected to a node
|
* Recompute all links connected to a node
|
||||||
*/
|
*/
|
||||||
function recomputeLinksForNode(nodeId: number): void {
|
function recomputeLinksForNode(nodeId: number): void {
|
||||||
|
const graph = toValue(graphRef)
|
||||||
if (!graph) return
|
if (!graph) return
|
||||||
|
|
||||||
const node = graph.getNodeById(nodeId)
|
const node = graph.getNodeById(nodeId)
|
||||||
@@ -243,6 +232,7 @@ export function useLinkLayoutSync() {
|
|||||||
* Recompute all links associated with a reroute
|
* Recompute all links associated with a reroute
|
||||||
*/
|
*/
|
||||||
function recomputeLinksForReroute(rerouteId: number): void {
|
function recomputeLinksForReroute(rerouteId: number): void {
|
||||||
|
const graph = toValue(graphRef)
|
||||||
if (!graph) return
|
if (!graph) return
|
||||||
|
|
||||||
const reroute = graph.reroutes.get(rerouteId)
|
const reroute = graph.reroutes.get(rerouteId)
|
||||||
@@ -258,105 +248,55 @@ export function useLinkLayoutSync() {
|
|||||||
* Start link layout sync with event-driven functionality
|
* Start link layout sync with event-driven functionality
|
||||||
*/
|
*/
|
||||||
function start(canvasInstance: LGraphCanvas): void {
|
function start(canvasInstance: LGraphCanvas): void {
|
||||||
canvas = canvasInstance
|
canvasRef.value = canvasInstance
|
||||||
graph = canvas.graph
|
if (!canvasInstance.graph) return
|
||||||
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
|
|
||||||
|
|
||||||
// Initial computation for all existing links
|
// 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) {
|
if (link.id !== -1) {
|
||||||
recomputeLinkById(link.id)
|
recomputeLinkById(link.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe to layout store changes
|
// Subscribe to layout store changes
|
||||||
unsubscribeLayoutChange = layoutStore.onChange((change: LayoutChange) => {
|
unsubscribeLayoutChange.value?.()
|
||||||
switch (change.operation.type) {
|
unsubscribeLayoutChange.value = layoutStore.onChange(
|
||||||
case 'moveNode':
|
(change: LayoutChange) => {
|
||||||
case 'resizeNode':
|
switch (change.operation.type) {
|
||||||
recomputeLinksForNode(parseInt(change.operation.nodeId))
|
case 'moveNode':
|
||||||
break
|
case 'resizeNode':
|
||||||
case 'createLink':
|
recomputeLinksForNode(parseInt(change.operation.nodeId))
|
||||||
recomputeLinkById(change.operation.linkId)
|
break
|
||||||
break
|
case 'createLink':
|
||||||
case 'deleteLink':
|
recomputeLinkById(change.operation.linkId)
|
||||||
// No-op - store already cleaned by existing code
|
break
|
||||||
break
|
case 'deleteLink':
|
||||||
case 'createReroute':
|
// No-op - store already cleaned by existing code
|
||||||
case 'deleteReroute':
|
break
|
||||||
// Recompute all affected links
|
case 'createReroute':
|
||||||
if ('linkIds' in change.operation) {
|
case 'deleteReroute':
|
||||||
for (const linkId of change.operation.linkIds) {
|
// Recompute all affected links
|
||||||
recomputeLinkById(linkId)
|
if ('linkIds' in change.operation) {
|
||||||
|
for (const linkId of change.operation.linkIds) {
|
||||||
|
recomputeLinkById(linkId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
break
|
||||||
break
|
case 'moveReroute':
|
||||||
case 'moveReroute':
|
recomputeLinksForReroute(change.operation.rerouteId)
|
||||||
recomputeLinksForReroute(change.operation.rerouteId)
|
break
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
function stop(): void {
|
||||||
if (unsubscribeLayoutChange) {
|
unsubscribeLayoutChange.value?.()
|
||||||
unsubscribeLayoutChange()
|
unsubscribeLayoutChange.value = undefined
|
||||||
unsubscribeLayoutChange = null
|
canvasRef.value = undefined
|
||||||
}
|
|
||||||
if (restoreHandlers) {
|
|
||||||
restoreHandlers()
|
|
||||||
restoreHandlers = null
|
|
||||||
}
|
|
||||||
canvas = null
|
|
||||||
graph = null
|
|
||||||
offscreenCtx = null
|
|
||||||
adapter = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-cleanup on unmount
|
tryOnScopeDispose(stop)
|
||||||
onUnmounted(() => {
|
|
||||||
stop()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
/**
|
import { tryOnScopeDispose } from '@vueuse/core'
|
||||||
* Composable for managing slot layout registration
|
import { ref } from 'vue'
|
||||||
*
|
|
||||||
* 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 type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
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 { registerNodeSlots } from '@/renderer/core/layout/slots/register'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
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 {
|
function computeAndRegisterSlots(node: LGraphNode): void {
|
||||||
const nodeId = String(node.id)
|
const nodeId = String(node.id)
|
||||||
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
|
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
|
||||||
@@ -50,12 +41,9 @@ function computeAndRegisterSlots(node: LGraphNode): void {
|
|||||||
registerNodeSlots(nodeId, context)
|
registerNodeSlots(nodeId, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Composable for managing slot layout registration
|
|
||||||
*/
|
|
||||||
export function useSlotLayoutSync() {
|
export function useSlotLayoutSync() {
|
||||||
let unsubscribeLayoutChange: (() => void) | null = null
|
const unsubscribeLayoutChange = ref<() => void>()
|
||||||
let restoreHandlers: (() => void) | null = null
|
const restoreHandlers = ref<() => void>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to start slot layout sync with full event-driven functionality
|
* 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
|
// Layout changes → recompute slots for changed nodes
|
||||||
unsubscribeLayoutChange = layoutStore.onChange((change) => {
|
unsubscribeLayoutChange.value?.()
|
||||||
|
unsubscribeLayoutChange.value = layoutStore.onChange((change) => {
|
||||||
for (const nodeId of change.nodeIds) {
|
for (const nodeId of change.nodeIds) {
|
||||||
const node = graph.getNodeById(parseInt(nodeId))
|
const node = graph.getNodeById(parseInt(nodeId))
|
||||||
if (node) {
|
if (node) {
|
||||||
@@ -131,7 +120,7 @@ export function useSlotLayoutSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store cleanup function
|
// Store cleanup function
|
||||||
restoreHandlers = () => {
|
restoreHandlers.value = () => {
|
||||||
graph.onNodeAdded = origNodeAdded || undefined
|
graph.onNodeAdded = origNodeAdded || undefined
|
||||||
graph.onNodeRemoved = origNodeRemoved || undefined
|
graph.onNodeRemoved = origNodeRemoved || undefined
|
||||||
// Only restore onTrigger if Vue nodes are not active
|
// Only restore onTrigger if Vue nodes are not active
|
||||||
@@ -145,24 +134,14 @@ export function useSlotLayoutSync() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop slot layout sync and cleanup all subscriptions
|
|
||||||
*/
|
|
||||||
function stop(): void {
|
function stop(): void {
|
||||||
if (unsubscribeLayoutChange) {
|
unsubscribeLayoutChange.value?.()
|
||||||
unsubscribeLayoutChange()
|
unsubscribeLayoutChange.value = undefined
|
||||||
unsubscribeLayoutChange = null
|
restoreHandlers.value?.()
|
||||||
}
|
restoreHandlers.value = undefined
|
||||||
if (restoreHandlers) {
|
|
||||||
restoreHandlers()
|
|
||||||
restoreHandlers = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-cleanup on unmount
|
tryOnScopeDispose(stop)
|
||||||
onUnmounted(() => {
|
|
||||||
stop()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attemptStart,
|
attemptStart,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<SlotConnectionDot
|
<SlotConnectionDot
|
||||||
ref="connectionDotRef"
|
ref="connectionDotRef"
|
||||||
:color="slotColor"
|
:color="slotColor"
|
||||||
class="-translate-x-1/2"
|
:class="cn('-translate-x-1/2', errorClassesDot)"
|
||||||
v-on="readonly ? {} : { pointerdown: onPointerDown }"
|
v-on="readonly ? {} : { pointerdown: onPointerDown }"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -13,7 +13,9 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<span
|
<span
|
||||||
v-if="!dotOnly"
|
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}` }}
|
{{ slotData.localized_name || slotData.name || `Input ${index}` }}
|
||||||
</span>
|
</span>
|
||||||
@@ -39,6 +41,7 @@ import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
|
|||||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||||
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
|
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
|
||||||
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
|
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
|
||||||
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import LODFallback from './LODFallback.vue'
|
import LODFallback from './LODFallback.vue'
|
||||||
@@ -57,7 +60,30 @@ interface InputSlotProps {
|
|||||||
|
|
||||||
const props = defineProps<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 renderError = ref<string | null>(null)
|
||||||
const { toastErrorHandler } = useErrorHandling()
|
const { toastErrorHandler } = useErrorHandling()
|
||||||
|
|
||||||
@@ -81,8 +107,12 @@ onErrorCaptured((error) => {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
// Get slot color based on type
|
const slotColor = computed(() => {
|
||||||
const slotColor = computed(() => getSlotColor(props.slotData.type))
|
if (hasSlotError.value) {
|
||||||
|
return 'var(--color-error)'
|
||||||
|
}
|
||||||
|
return getSlotColor(props.slotData.type)
|
||||||
|
})
|
||||||
|
|
||||||
const slotWrapperClass = computed(() =>
|
const slotWrapperClass = computed(() =>
|
||||||
cn(
|
cn(
|
||||||
@@ -103,8 +133,6 @@ const connectionDotRef = ref<ComponentPublicInstance<{
|
|||||||
}> | null>(null)
|
}> | null>(null)
|
||||||
const slotElRef = ref<HTMLElement | 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(() => {
|
watchEffect(() => {
|
||||||
const el = connectionDotRef.value?.slotElRef
|
const el = connectionDotRef.value?.slotElRef
|
||||||
slotElRef.value = el || null
|
slotElRef.value = el || null
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
cn(
|
cn(
|
||||||
'bg-white dark-theme:bg-charcoal-800',
|
'bg-white dark-theme:bg-charcoal-800',
|
||||||
'lg-node absolute rounded-2xl',
|
'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)
|
// hover (only when node should handle events)
|
||||||
shouldHandleNodePointerEvents &&
|
shouldHandleNodePointerEvents &&
|
||||||
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
|
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
|
||||||
@@ -34,7 +34,9 @@
|
|||||||
:style="[
|
:style="[
|
||||||
{
|
{
|
||||||
transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||||
zIndex: zIndex
|
zIndex: zIndex,
|
||||||
|
backgroundColor: nodeBodyBackgroundColor,
|
||||||
|
opacity: nodeOpacity
|
||||||
},
|
},
|
||||||
dragStyle
|
dragStyle
|
||||||
]"
|
]"
|
||||||
@@ -47,9 +49,14 @@
|
|||||||
<SlotConnectionDot multi class="absolute left-0 -translate-x-1/2" />
|
<SlotConnectionDot multi class="absolute left-0 -translate-x-1/2" />
|
||||||
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
|
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
|
||||||
</template>
|
</template>
|
||||||
<!-- Header only updates on title/color changes -->
|
|
||||||
<NodeHeader
|
<NodeHeader
|
||||||
v-memo="[nodeData.title, isCollapsed, nodeData.flags?.pinned]"
|
v-memo="[
|
||||||
|
nodeData.title,
|
||||||
|
nodeData.color,
|
||||||
|
nodeData.bgcolor,
|
||||||
|
isCollapsed,
|
||||||
|
nodeData.flags?.pinned
|
||||||
|
]"
|
||||||
:node-data="nodeData"
|
:node-data="nodeData"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:collapsed="isCollapsed"
|
:collapsed="isCollapsed"
|
||||||
@@ -94,7 +101,11 @@
|
|||||||
>
|
>
|
||||||
<!-- Slots only rendered at full detail -->
|
<!-- Slots only rendered at full detail -->
|
||||||
<NodeSlots
|
<NodeSlots
|
||||||
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length]"
|
v-memo="[
|
||||||
|
nodeData.inputs?.length,
|
||||||
|
nodeData.outputs?.length,
|
||||||
|
executionStore.lastNodeErrors
|
||||||
|
]"
|
||||||
:node-data="nodeData"
|
:node-data="nodeData"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
/>
|
/>
|
||||||
@@ -139,6 +150,7 @@ import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
|||||||
import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
|
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 { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
|
||||||
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
|
||||||
import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
|
import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
|
||||||
|
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
import { useExecutionStore } from '@/stores/executionStore'
|
import { useExecutionStore } from '@/stores/executionStore'
|
||||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||||
|
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||||
import {
|
import {
|
||||||
getLocatorIdFromNodeData,
|
getLocatorIdFromNodeData,
|
||||||
getNodeByLocatorId
|
getNodeByLocatorId
|
||||||
@@ -205,22 +219,35 @@ const hasExecutionError = computed(
|
|||||||
() => executionStore.lastExecutionErrorNodeId === nodeData.id
|
() => executionStore.lastExecutionErrorNodeId === nodeData.id
|
||||||
)
|
)
|
||||||
|
|
||||||
// Computed error states for styling
|
|
||||||
const hasAnyError = computed((): boolean => {
|
const hasAnyError = computed((): boolean => {
|
||||||
return !!(
|
return !!(
|
||||||
hasExecutionError.value ||
|
hasExecutionError.value ||
|
||||||
nodeData.hasErrors ||
|
nodeData.hasErrors ||
|
||||||
error ||
|
error ||
|
||||||
// Type assertions needed because VueNodeData.inputs/outputs are typed as unknown[]
|
(executionStore.lastNodeErrors?.[nodeData.id]?.errors.length ?? 0) > 0
|
||||||
// but at runtime they contain INodeInputSlot/INodeOutputSlot objects
|
|
||||||
nodeData.inputs?.some((slot) => slot?.hasErrors) ||
|
|
||||||
nodeData.outputs?.some((slot) => slot?.hasErrors)
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const bypassed = computed((): boolean => nodeData.mode === 4)
|
const bypassed = computed((): boolean => nodeData.mode === 4)
|
||||||
const muted = computed((): boolean => nodeData.mode === 2) // NEVER mode
|
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
|
// Use canvas interactions for proper wheel event handling and pointer event capture control
|
||||||
const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()
|
const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()
|
||||||
|
|
||||||
@@ -289,26 +316,19 @@ const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const borderClass = computed(() => {
|
const borderClass = computed(() => {
|
||||||
if (hasAnyError.value) {
|
return (
|
||||||
return 'border-error dark-theme:border-error'
|
(hasAnyError.value && 'border-error dark-theme:border-error') ||
|
||||||
}
|
(executing.value && 'border-blue-500')
|
||||||
if (executing.value) {
|
)
|
||||||
return 'border-blue-500'
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const outlineClass = computed(() => {
|
const outlineClass = computed(() => {
|
||||||
if (!isSelected.value) {
|
return (
|
||||||
return undefined
|
isSelected.value &&
|
||||||
}
|
((hasAnyError.value && 'outline-error dark-theme:outline-error') ||
|
||||||
if (hasAnyError.value) {
|
(executing.value && 'outline-blue-500 dark-theme:outline-blue-500') ||
|
||||||
return 'outline-error dark-theme:outline-error'
|
'outline-black dark-theme:outline-white')
|
||||||
}
|
)
|
||||||
if (executing.value) {
|
|
||||||
return 'outline-blue-500 dark-theme:outline-blue-500'
|
|
||||||
}
|
|
||||||
return 'outline-black dark-theme:outline-white'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
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 || ''}`"
|
:data-testid="`node-header-${nodeData?.id || ''}`"
|
||||||
@dblclick="handleDoubleClick"
|
@dblclick="handleDoubleClick"
|
||||||
>
|
>
|
||||||
@@ -71,8 +72,11 @@ import EditableText from '@/components/common/EditableText.vue'
|
|||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
|
||||||
|
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
|
||||||
import { app } from '@/scripts/app'
|
import { app } from '@/scripts/app'
|
||||||
|
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||||
import {
|
import {
|
||||||
getLocatorIdFromNodeData,
|
getLocatorIdFromNodeData,
|
||||||
@@ -123,6 +127,23 @@ const tooltipConfig = computed(() => {
|
|||||||
return createTooltipConfig(description)
|
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 resolveTitle = (info: VueNodeData | undefined) => {
|
||||||
const title = (info?.title ?? '').trim()
|
const title = (info?.title ?? '').trim()
|
||||||
if (title.length > 0) return title
|
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
|
// @ts-expect-error fixme ts strict error
|
||||||
_nodeOutputs: Record<string, any>
|
_nodeOutputs: Record<string, any>
|
||||||
nodePreviewImages: Record<string, string[]>
|
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() {
|
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
|
// @ts-expect-error fixme ts strict error
|
||||||
canvas: LGraphCanvas
|
canvas: LGraphCanvas
|
||||||
dragOverNode: LGraphNode | null = null
|
dragOverNode: LGraphNode | null = null
|
||||||
@@ -765,8 +776,7 @@ export class ComfyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#addAfterConfigureHandler() {
|
private addAfterConfigureHandler(graph: LGraph) {
|
||||||
const { graph } = this
|
|
||||||
const { onConfigure } = graph
|
const { onConfigure } = graph
|
||||||
graph.onConfigure = function (...args) {
|
graph.onConfigure = function (...args) {
|
||||||
fixLinkInputSlots(this)
|
fixLinkInputSlots(this)
|
||||||
@@ -809,10 +819,10 @@ export class ComfyApp {
|
|||||||
this.#addConfigureHandler()
|
this.#addConfigureHandler()
|
||||||
this.#addApiUpdateHandlers()
|
this.#addApiUpdateHandlers()
|
||||||
|
|
||||||
this.#graph = new LGraph()
|
const graph = new LGraph()
|
||||||
|
|
||||||
// Register the subgraph - adds type wrapper for Litegraph's `createNode` factory
|
// 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 {
|
try {
|
||||||
const { subgraph, data } = e.detail
|
const { subgraph, data } = e.detail
|
||||||
useSubgraphService().registerNewSubgraph(subgraph, data)
|
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.
|
// Make canvas states reactive so we can observe changes on them.
|
||||||
this.canvas.state = reactive(this.canvas.state)
|
this.canvas.state = reactive(this.canvas.state)
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ export function addValueControlWidgets(
|
|||||||
|
|
||||||
valueControl.tooltip =
|
valueControl.tooltip =
|
||||||
'Allows the linked widget to be changed automatically, for example randomizing the noise seed.'
|
'Allows the linked widget to be changed automatically, for example randomizing the noise seed.'
|
||||||
// @ts-ignore index with symbol
|
|
||||||
valueControl[IS_CONTROL_WIDGET] = true
|
valueControl[IS_CONTROL_WIDGET] = true
|
||||||
updateControlWidgetLabel(valueControl)
|
updateControlWidgetLabel(valueControl)
|
||||||
const widgets: [IComboWidget, ...IStringWidget[]] = [valueControl]
|
const widgets: [IComboWidget, ...IStringWidget[]] = [valueControl]
|
||||||
@@ -273,12 +272,10 @@ export function addValueControlWidgets(
|
|||||||
valueControl.beforeQueued = () => {
|
valueControl.beforeQueued = () => {
|
||||||
if (controlValueRunBefore()) {
|
if (controlValueRunBefore()) {
|
||||||
// Don't run on first execution
|
// Don't run on first execution
|
||||||
// @ts-ignore index with symbol
|
|
||||||
if (valueControl[HAS_EXECUTED]) {
|
if (valueControl[HAS_EXECUTED]) {
|
||||||
applyWidgetControl()
|
applyWidgetControl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// @ts-ignore index with symbol
|
|
||||||
valueControl[HAS_EXECUTED] = true
|
valueControl[HAS_EXECUTED] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||