diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts new file mode 100644 index 0000000000..cc42e8b51e --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts @@ -0,0 +1,52 @@ +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' + ) + }) + + // TODO: implement loading node colors from workflow in Vue system + test.fail('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' + ) + }) + + // TODO: implement loading node colors from workflow in Vue system + test.fail( + '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' + ) + } + ) +}) diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts index beaca8d4c7..55b06cabd4 100644 --- a/src/composables/graph/useGraphNodeManager.ts +++ b/src/composables/graph/useGraphNodeManager.ts @@ -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 ( diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index a1eeb8ae27..f01e4f7610 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -332,6 +332,9 @@ export class LGraphNode /** @inheritdoc {@link IColorable.setColorOption} */ setColorOption(colorOption: ColorOption | null): void { + const oldColor = this.color + const oldBgcolor = this.bgcolor + if (colorOption == null) { delete this.color delete this.bgcolor @@ -339,6 +342,29 @@ export class LGraphNode this.color = colorOption.color this.bgcolor = colorOption.bgcolor } + + // Trigger property change events for Vue node synchronization + if (this.graph) { + const newColor = this.color // undefined if deleted + const newBgcolor = this.bgcolor // undefined if deleted + + if (oldColor !== newColor) { + this.graph.trigger('node:property:changed', { + nodeId: this.id, + property: 'color', + oldValue: oldColor, + newValue: newColor + }) + } + if (oldBgcolor !== newBgcolor) { + this.graph.trigger('node:property:changed', { + nodeId: this.id, + property: 'bgcolor', + oldValue: oldBgcolor, + newValue: newBgcolor + }) + } + } } /** @inheritdoc {@link IColorable.getColorOption} */ diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index a86d8de3a2..b6f0454ba3 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -34,7 +34,8 @@ :style="[ { transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`, - zIndex: zIndex + zIndex: zIndex, + backgroundColor: nodeData.bgcolor || '' }, dragStyle ]" @@ -49,7 +50,13 @@ { return createTooltipConfig(description) }) +// Header style that replicates LiteGraph's ColorOption and drawNode logic +const headerStyle = computed(() => { + if (!nodeData?.color) { + return { backgroundColor: '' } // Explicitly clear background color + } + + const colorPaletteStore = useColorPaletteStore() + let headerColor = nodeData.color + + // Apply base header darkening to replicate LiteGraph's ColorOption system + // When header and body colors are the same/similar, darken the header + if (nodeData.bgcolor && nodeData.color === nodeData.bgcolor) { + // Darken header relative to body (opposite of light theme adjustment) + headerColor = adjustColor(nodeData.color, { lightness: -0.15 }) + } + + // Apply light theme lightening on top of base darkening (same as drawNode monkey patch) + if (colorPaletteStore.completedActivePalette.light_theme) { + headerColor = adjustColor(headerColor, { lightness: 0.5 }) + } + + return { backgroundColor: headerColor } +}) + const resolveTitle = (info: VueNodeData | undefined) => { const title = (info?.title ?? '').trim() if (title.length > 0) return title