diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index aaa2b590a..b8de43d05 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index b63ad44ee..ebf6a0470 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index 0b51591e7..9fe1a1e8b 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png index ff06bd6ad..4df1c65ac 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png index 088ec907c..7fe1f3aa3 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png index 91e59358a..46544631b 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png index 43e9ac170..251c07a08 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png index ce83405d2..5b37aad71 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png differ diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index fac938707..4f8b94f9c 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -2672,7 +2672,7 @@ export class LGraphCanvas ): boolean { const outputLinks = [ ...(output.links ?? []), - ...[...(output._floatingLinks ?? new Set())] + ...(output._floatingLinks ?? new Set()) ] return outputLinks.some( (linkId) => diff --git a/src/renderer/core/canvas/links/linkConnectorAdapter.ts b/src/renderer/core/canvas/links/linkConnectorAdapter.ts index 860d365f6..b7937faf2 100644 --- a/src/renderer/core/canvas/links/linkConnectorAdapter.ts +++ b/src/renderer/core/canvas/links/linkConnectorAdapter.ts @@ -154,10 +154,13 @@ export function createLinkConnectorAdapter(): LinkConnectorAdapter | null { const graph = app.canvas?.graph const connector = app.canvas?.linkConnector if (!graph || !connector) return null - let adapter = adapterByGraph.get(graph) - if (!adapter || adapter.linkConnector !== connector) { - adapter = new LinkConnectorAdapter(graph, connector) - adapterByGraph.set(graph, adapter) + + const adapter = adapterByGraph.get(graph) + if (adapter && adapter.linkConnector === connector) { + return adapter } - return adapter + + const newAdapter = new LinkConnectorAdapter(graph, connector) + adapterByGraph.set(graph, newAdapter) + return newAdapter } diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index e4f02cf5f..0a9d13b40 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -15,7 +15,7 @@ // hover (only when node should handle events) shouldHandleNodePointerEvents && 'hover:ring-7 ring-node-component-ring', - 'outline-transparent -outline-offset-2 outline-2', + 'outline-transparent outline-2', borderClass, outlineClass, { @@ -44,7 +44,20 @@ @wheel="handleWheel" @contextmenu="handleContextMenu" > -
+
+ { return globalOpacity }) +const hasInputs = computed(() => nonWidgetedInputs(nodeData).length > 0) +const hasOutputs = computed((): boolean => !!nodeData.outputs?.length) + // Use canvas interactions for proper wheel event handling and pointer event capture control const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions() diff --git a/src/renderer/extensions/vueNodes/components/NodeSlots.vue b/src/renderer/extensions/vueNodes/components/NodeSlots.vue index e341e50bb..31d00a92b 100644 --- a/src/renderer/extensions/vueNodes/components/NodeSlots.vue +++ b/src/renderer/extensions/vueNodes/components/NodeSlots.vue @@ -2,8 +2,11 @@
{{ $t('Node Slots Error') }}
-
-
+
+
-
+
() +const { nodeData, unified = false } = defineProps() -// Filter out input slots that have corresponding widgets -const filteredInputs = computed(() => { - if (!nodeData?.inputs) return [] +const linkedWidgetInputs = computed(() => + unified ? linkedWidgetedInputs(nodeData) : [] +) - return nodeData.inputs - .filter((input) => { - // Check if this slot has a widget property (indicating it has a corresponding widget) - if (isSlotObject(input) && 'widget' in input && input.widget) { - // This slot has a widget, so we should not display it separately - return false - } - return true - }) - .map((input) => - isSlotObject(input) - ? input - : ({ - name: typeof input === 'string' ? input : '', - type: 'any', - boundingRect: [0, 0, 0, 0] as [number, number, number, number] - } as INodeSlot) - ) -}) +const filteredInputs = computed(() => [ + ...nonWidgetedInputs(nodeData), + ...linkedWidgetInputs.value +]) + +const unifiedWrapperClass = computed((): string => + cn( + unified && + 'absolute inset-0 items-center pointer-events-none opacity-0 z-30' + ) +) +const unifiedDotsClass = computed((): string => + cn( + unified && + 'grid grid-cols-1 grid-rows-1 gap-0 [&>*]:row-span-full [&>*]:col-span-full place-items-center' + ) +) // Get the actual index of an input slot in the node's inputs array // (accounting for filtered widget slots) diff --git a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue index ddc35d5e1..18fe8cc56 100644 --- a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue +++ b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue @@ -24,7 +24,12 @@
@@ -140,9 +145,7 @@ const processedWidgets = computed((): ProcessedWidget[] => { // This prevents conflicting input sources - when a slot is linked to another // node's output, the widget should be read-only to avoid data conflicts if (slotMetadata?.linked) { - widgetOptions = widget.options - ? { ...widget.options, disabled: true } - : { disabled: true } + widgetOptions = { ...widget.options, disabled: true } } const simplified: SimplifiedWidget = { diff --git a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts index c5137081d..f93885e88 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts @@ -1,6 +1,5 @@ -import { useEventListener } from '@vueuse/core' +import { tryOnScopeDispose, useEventListener } from '@vueuse/core' import type { Fn } from '@vueuse/core' -import { onBeforeUnmount } from 'vue' import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion' import type { LGraph } from '@/lib/litegraph/src/LGraph' @@ -555,6 +554,8 @@ export function useSlotLinkInteraction({ if (event.button !== 0) return if (!nodeId) return if (pointerSession.isActive()) return + event.preventDefault() + event.stopPropagation() const canvas = app.canvas const graph = canvas?.graph @@ -613,7 +614,7 @@ export function useSlotLinkInteraction({ if (shouldBatchDisconnectOutputLinks && resolvedNode) { resolvedNode.disconnectOutput(index) - app.canvas?.setDirty(true, true) + canvas.setDirty(true, true) event.preventDefault() event.stopPropagation() return @@ -634,20 +635,18 @@ export function useSlotLinkInteraction({ const shouldMoveExistingInput = isInputSlot && !shouldBreakExistingInputLink && hasExistingInputLink - if (activeAdapter) { - if (isOutputSlot) { - activeAdapter.beginFromOutput(localNodeId, index, { - moveExisting: shouldMoveExistingOutput - }) - } else { - activeAdapter.beginFromInput(localNodeId, index, { - moveExisting: shouldMoveExistingInput - }) - } + if (isOutputSlot) { + activeAdapter.beginFromOutput(localNodeId, index, { + moveExisting: shouldMoveExistingOutput + }) + } else { + activeAdapter.beginFromInput(localNodeId, index, { + moveExisting: shouldMoveExistingInput + }) + } - if (shouldMoveExistingInput && existingInputLink) { - existingInputLink._dragging = true - } + if (shouldMoveExistingInput && existingInputLink) { + existingInputLink._dragging = true } syncRenderLinkOrigins() @@ -678,21 +677,19 @@ export function useSlotLinkInteraction({ toCanvasPointerEvent(event) updatePointerState(event) - if (activeAdapter) { - activeAdapter.linkConnector.state.snapLinksPos = [ - state.pointer.canvas.x, - state.pointer.canvas.y - ] - } + activeAdapter.linkConnector.state.snapLinksPos = [ + state.pointer.canvas.x, + state.pointer.canvas.y + ] pointerSession.register( - useEventListener(window, 'pointermove', handlePointerMove, { + useEventListener('pointermove', handlePointerMove, { capture: true }), - useEventListener(window, 'pointerup', handlePointerUp, { + useEventListener('pointerup', handlePointerUp, { capture: true }), - useEventListener(window, 'pointercancel', handlePointerCancel, { + useEventListener('pointercancel', handlePointerCancel, { capture: true }) ) @@ -710,12 +707,10 @@ export function useSlotLinkInteraction({ : activeAdapter.isOutputValidDrop(slotLayout.nodeId, idx) setCompatibleForKey(key, ok) } - app.canvas?.setDirty(true, true) - event.preventDefault() - event.stopPropagation() + canvas.setDirty(true, true) } - onBeforeUnmount(() => { + tryOnScopeDispose(() => { if (pointerSession.isActive()) { cleanupInteraction() } diff --git a/src/renderer/extensions/vueNodes/utils/__tests__/nodeDataUtils.test.ts b/src/renderer/extensions/vueNodes/utils/__tests__/nodeDataUtils.test.ts new file mode 100644 index 000000000..7a2247993 --- /dev/null +++ b/src/renderer/extensions/vueNodes/utils/__tests__/nodeDataUtils.test.ts @@ -0,0 +1,127 @@ +import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' +import type { + INodeInputSlot, + IWidgetLocator +} from '@/lib/litegraph/src/interfaces' +import type { LinkId } from '@/renderer/core/layout/types' +import { + linkedWidgetedInputs, + nonWidgetedInputs +} from '@/renderer/extensions/vueNodes/utils/nodeDataUtils' +import { describe, it } from 'vitest' + +function makeFakeInputSlot( + name: string, + withWidget = false, + link: LinkId | null = null +): INodeInputSlot { + const widget: IWidgetLocator | undefined = withWidget ? { name } : undefined + return { + name, + widget, + link, + boundingRect: [0, 0, 0, 0], + type: 'FAKE' + } +} + +function makeFakeNodeData(inputs: INodeInputSlot[]): VueNodeData { + const nodeData: Partial = { inputs } + return nodeData as VueNodeData +} + +describe('nodeDataUtils', () => { + describe('nonWidgetedInputs', () => { + it('should handle an empty inputs list', () => { + const inputs: INodeInputSlot[] = [] + const nodeData = makeFakeNodeData(inputs) + + const actual = nonWidgetedInputs(nodeData) + + expect(actual.length).toBe(0) + }) + + it('should handle a list of only widgeted inputs', () => { + const inputs: INodeInputSlot[] = [ + makeFakeInputSlot('first', true), + makeFakeInputSlot('second', true) + ] + const nodeData = makeFakeNodeData(inputs) + + const actual = nonWidgetedInputs(nodeData) + + expect(actual.length).toBe(0) + }) + + it('should handle a list of only slot inputs', () => { + const inputs: INodeInputSlot[] = [ + makeFakeInputSlot('first'), + makeFakeInputSlot('second') + ] + const nodeData = makeFakeNodeData(inputs) + + const actual = nonWidgetedInputs(nodeData) + + expect(actual.length).toBe(2) + }) + + it('should handle a list of mixed inputs', () => { + const inputs: INodeInputSlot[] = [ + makeFakeInputSlot('first'), + makeFakeInputSlot('second'), + makeFakeInputSlot('third', true), + makeFakeInputSlot('fourth', true) + ] + const nodeData = makeFakeNodeData(inputs) + + const actual = nonWidgetedInputs(nodeData) + + expect(actual.length).toBe(2) + }) + }) + + describe('linkedWidgetedInputs', () => { + it('should return input slots that are bound to widgets and are linked: none present', () => { + const inputs: INodeInputSlot[] = [ + makeFakeInputSlot('first'), + makeFakeInputSlot('second'), + makeFakeInputSlot('third', true), + makeFakeInputSlot('fourth', true) + ] + const nodeData = makeFakeNodeData(inputs) + + const actual = linkedWidgetedInputs(nodeData) + + expect(actual.length).toBe(0) + }) + + it('should return input slots that are bound to widgets and are linked: one present', () => { + const inputs: INodeInputSlot[] = [ + makeFakeInputSlot('first'), + makeFakeInputSlot('second'), + makeFakeInputSlot('third', true), + makeFakeInputSlot('fourth', true, 1) + ] + const nodeData = makeFakeNodeData(inputs) + + const actual = linkedWidgetedInputs(nodeData) + + expect(actual.length).toBe(1) + }) + + it('should return input slots that are bound to widgets and are linked: multiple present', () => { + const inputs: INodeInputSlot[] = [ + makeFakeInputSlot('first'), + makeFakeInputSlot('second'), + makeFakeInputSlot('third', true), + makeFakeInputSlot('fourth', true, 1), + makeFakeInputSlot('fifth', true, 2) + ] + const nodeData = makeFakeNodeData(inputs) + + const actual = linkedWidgetedInputs(nodeData) + + expect(actual.length).toBe(2) + }) + }) +}) diff --git a/src/renderer/extensions/vueNodes/utils/nodeDataUtils.ts b/src/renderer/extensions/vueNodes/utils/nodeDataUtils.ts new file mode 100644 index 000000000..1517b9fb9 --- /dev/null +++ b/src/renderer/extensions/vueNodes/utils/nodeDataUtils.ts @@ -0,0 +1,36 @@ +import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' +import type { INodeInputSlot, INodeSlot } from '@/lib/litegraph/src/interfaces' +import { isSlotObject } from '@/utils/typeGuardUtil' + +function coerceINodeSlot(input: INodeInputSlot): INodeSlot { + return isSlotObject(input) + ? input + : { + name: typeof input === 'string' ? input : '', + type: 'any', + boundingRect: [0, 0, 0, 0] + } +} + +function inputHasWidget(input: INodeInputSlot) { + return isSlotObject(input) && 'widget' in input && input.widget +} +export function nonWidgetedInputs( + nodeData: VueNodeData | undefined +): INodeSlot[] { + if (!nodeData?.inputs) return [] + + return nodeData.inputs + .filter((input) => !inputHasWidget(input)) + .map(coerceINodeSlot) +} + +export function linkedWidgetedInputs( + nodeData: VueNodeData | undefined +): INodeSlot[] { + if (!nodeData?.inputs) return [] + + return nodeData.inputs + .filter((input) => inputHasWidget(input) && !!input.link) + .map(coerceINodeSlot) +} diff --git a/src/renderer/extensions/vueNodes/widgets/components/layout/index.ts b/src/renderer/extensions/vueNodes/widgets/components/layout/index.ts index 372304b36..ba3b33c11 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/layout/index.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/layout/index.ts @@ -2,8 +2,8 @@ import { cn } from '@/utils/tailwindUtil' export const WidgetInputBaseClass = cn([ // Background - 'bg-node-component-widget-input-surface', - 'text-node-component-widget-input', + 'not-disabled:bg-node-component-widget-input-surface', + 'not-disabled:text-node-component-widget-input', // Outline 'border-none', 'outline outline-offset-[-1px] outline-node-stroke',