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',