diff --git a/src/composables/useUpstreamValue.test.ts b/src/composables/useUpstreamValue.test.ts index c1ff9b17ba..8d886ad3df 100644 --- a/src/composables/useUpstreamValue.test.ts +++ b/src/composables/useUpstreamValue.test.ts @@ -1,11 +1,12 @@ -import { describe, expect, it, vi } from 'vitest' -import { reactive } from 'vue' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { NodeId } from '@/lib/litegraph/src/LGraphNode' +import type { UUID } from '@/lib/litegraph/src/utils/uuid' +import { useWidgetValueStore } from '@/stores/widgetValueStore' import type { WidgetState } from '@/stores/widgetValueStore' -import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' -import { asGraphId } from '@/world/entityIds' -import { registerWidgetInWorld } from '@/world/widgetWorldBridge' -import { getWorld, resetWorldInstance } from '@/world/worldInstance' +import { resetWorldInstance } from '@/world/worldInstance' import { boundsExtractor, @@ -133,18 +134,21 @@ describe('boundsExtractor', () => { }) }) -describe('useUpstreamValue (World-backed read path)', () => { - it('reads upstream node widgets via the World, not the Pinia store', () => { +describe('useUpstreamValue (store-backed read path)', () => { + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) resetWorldInstance() - const graphId = asGraphId('00000000-0000-0000-0000-000000000001') - const state = reactive({ + }) + + it('reads upstream node widgets via the widget value store', () => { + const graphId = '00000000-0000-0000-0000-000000000001' as UUID + const state = useWidgetValueStore().registerWidget(graphId, { nodeId: 'upstream-1' as NodeId, name: 'value', type: 'number', value: 7, options: {} }) - registerWidgetInWorld(getWorld(), graphId, state) const upstreamValue = useUpstreamValue( () => ({ nodeId: 'upstream-1', outputName: 'value' }), @@ -157,7 +161,6 @@ describe('useUpstreamValue (World-backed read path)', () => { }) it('returns undefined when no upstream linkage is provided', () => { - resetWorldInstance() const upstreamValue = useUpstreamValue( () => undefined, singleValueExtractor((v): v is number => typeof v === 'number') diff --git a/src/composables/useUpstreamValue.ts b/src/composables/useUpstreamValue.ts index da3e93159e..bdb51f2f00 100644 --- a/src/composables/useUpstreamValue.ts +++ b/src/composables/useUpstreamValue.ts @@ -1,12 +1,10 @@ import { computed } from 'vue' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' -import type { WidgetState } from '@/stores/widgetValueStore' import type { Bounds } from '@/renderer/core/layout/types' +import type { WidgetState } from '@/stores/widgetValueStore' +import { useWidgetValueStore } from '@/stores/widgetValueStore' import type { LinkedUpstreamInfo } from '@/types/simplifiedWidget' -import { asGraphId } from '@/world/entityIds' -import { getNodeWidgetsThroughWorld } from '@/world/widgetWorldBridge' -import { getWorld } from '@/world/worldInstance' type ValueExtractor = ( widgets: WidgetState[], @@ -18,17 +16,14 @@ export function useUpstreamValue( extractValue: ValueExtractor ) { const canvasStore = useCanvasStore() + const widgetValueStore = useWidgetValueStore() return computed(() => { const upstream = getLinkedUpstream() if (!upstream) return undefined const graphId = canvasStore.canvas?.graph?.rootGraph.id if (!graphId) return undefined - const widgets = getNodeWidgetsThroughWorld( - getWorld(), - asGraphId(graphId), - upstream.nodeId - ) + const widgets = widgetValueStore.getNodeWidgets(graphId, upstream.nodeId) return extractValue(widgets, upstream.outputName) }) } diff --git a/src/lib/litegraph/src/widgets/BaseWidget.ts b/src/lib/litegraph/src/widgets/BaseWidget.ts index c59f73f7a0..b87d4aad18 100644 --- a/src/lib/litegraph/src/widgets/BaseWidget.ts +++ b/src/lib/litegraph/src/widgets/BaseWidget.ts @@ -20,9 +20,6 @@ import type { import { usePromotionStore } from '@/stores/promotionStore' import type { WidgetState } from '@/stores/widgetValueStore' import { useWidgetValueStore } from '@/stores/widgetValueStore' -import { asGraphId } from '@/world/entityIds' -import { registerWidgetInWorld } from '@/world/widgetWorldBridge' -import { getWorld } from '@/world/worldInstance' export interface DrawWidgetOptions { /** The width of the node where this widget will be displayed. */ @@ -152,11 +149,6 @@ export abstract class BaseWidget value: this.value, nodeId }) - registerWidgetInWorld( - getWorld(), - asGraphId(graphId), - this._state as WidgetState - ) } constructor(widget: TWidget & { node: LGraphNode }) diff --git a/src/stores/widgetComponents.test.ts b/src/stores/widgetComponents.test.ts index ed6445fb60..d651125cc0 100644 --- a/src/stores/widgetComponents.test.ts +++ b/src/stores/widgetComponents.test.ts @@ -2,10 +2,14 @@ import { describe, expect, it } from 'vitest' import type { WidgetState } from '@/stores/widgetValueStore' import { asGraphId, nodeEntityId, widgetEntityId } from '@/world/entityIds' -import { registerWidgetInWorld } from '@/world/widgetWorldBridge' import { createWorld } from '@/world/world' -import { widgetParent } from './widgetComponents' +import type { WidgetValue } from './widgetComponents' +import { + WidgetContainerComponent, + WidgetValueComponent, + widgetParent +} from './widgetComponents' const graphId = asGraphId('00000000-0000-0000-0000-000000000001') @@ -16,9 +20,14 @@ function makeState(nodeId: string, name: string, value: unknown): WidgetState { describe('widgetParent', () => { it('returns the owning node entity for a widget', () => { const world = createWorld() - registerWidgetInWorld(world, graphId, makeState('node-1', 'seed', 1)) - const widgetId = widgetEntityId(graphId, 'node-1', 'seed') - expect(widgetParent(world, widgetId)).toBe(nodeEntityId(graphId, 'node-1')) + const state = makeState('node-1', 'seed', 1) + const widgetId = widgetEntityId(graphId, state.nodeId, state.name) + const ownerId = nodeEntityId(graphId, state.nodeId) + world.setComponent(widgetId, WidgetValueComponent, state as WidgetValue) + world.setComponent(ownerId, WidgetContainerComponent, { + widgetIds: [widgetId] + }) + expect(widgetParent(world, widgetId)).toBe(ownerId) }) it('returns undefined when no container references the widget', () => { diff --git a/src/world/entityIds.ts b/src/world/entityIds.ts index 659940f290..ee87e40b2e 100644 --- a/src/world/entityIds.ts +++ b/src/world/entityIds.ts @@ -13,7 +13,7 @@ import type { UUID } from '@/lib/litegraph/src/utils/uuid' import type { Brand } from './brand' -export type GraphId = Brand +type GraphId = Brand export type NodeEntityId = Brand export type WidgetEntityId = Brand diff --git a/src/world/widgetWorldBridge.test.ts b/src/world/widgetWorldBridge.test.ts deleted file mode 100644 index 5898a5c2e8..0000000000 --- a/src/world/widgetWorldBridge.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, expect, it } from 'vitest' - -import { - WidgetContainerComponent, - WidgetValueComponent -} from '@/stores/widgetComponents' -import type { WidgetState } from '@/stores/widgetValueStore' - -import { asGraphId, nodeEntityId, widgetEntityId } from './entityIds' -import { - getNodeWidgetsThroughWorld, - registerWidgetInWorld -} from './widgetWorldBridge' -import { createWorld } from './world' - -function makeState(nodeId: string, name: string, value: unknown): WidgetState { - return { nodeId, name, type: 'number', value, options: {} } -} - -const graphId = asGraphId('00000000-0000-0000-0000-000000000001') - -describe('registerWidgetInWorld', () => { - it('writes WidgetValue and updates WidgetContainer on the node', () => { - const world = createWorld() - const state = makeState('node-1', 'seed', 100) - - registerWidgetInWorld(world, graphId, state) - - const widgetId = widgetEntityId(graphId, 'node-1', 'seed') - const nodeId = nodeEntityId(graphId, 'node-1') - expect(world.getComponent(widgetId, WidgetValueComponent)?.value).toBe(100) - expect( - world.getComponent(nodeId, WidgetContainerComponent)?.widgetIds - ).toEqual([widgetId]) - }) - - it('shares object identity with the registered state (reactive bridge)', () => { - const world = createWorld() - const state = makeState('node-1', 'seed', 100) - registerWidgetInWorld(world, graphId, state) - - state.value = 200 - const widgetId = widgetEntityId(graphId, 'node-1', 'seed') - expect(world.getComponent(widgetId, WidgetValueComponent)?.value).toBe(200) - }) - - it('appends additional widgets to the same node container', () => { - const world = createWorld() - registerWidgetInWorld(world, graphId, makeState('node-1', 'seed', 1)) - registerWidgetInWorld(world, graphId, makeState('node-1', 'cfg', 7)) - - const nodeId = nodeEntityId(graphId, 'node-1') - const ids = world.getComponent(nodeId, WidgetContainerComponent)?.widgetIds - expect(ids).toEqual([ - widgetEntityId(graphId, 'node-1', 'seed'), - widgetEntityId(graphId, 'node-1', 'cfg') - ]) - }) - - it('does not duplicate widgetIds when the same widget re-registers', () => { - const world = createWorld() - const state = makeState('node-1', 'seed', 1) - registerWidgetInWorld(world, graphId, state) - registerWidgetInWorld(world, graphId, state) - - const nodeId = nodeEntityId(graphId, 'node-1') - expect( - world.getComponent(nodeId, WidgetContainerComponent)?.widgetIds - ).toHaveLength(1) - }) -}) - -describe('getNodeWidgetsThroughWorld', () => { - it('returns all widget states attached to a node', () => { - const world = createWorld() - registerWidgetInWorld(world, graphId, makeState('node-1', 'seed', 1)) - registerWidgetInWorld(world, graphId, makeState('node-1', 'cfg', 7)) - registerWidgetInWorld(world, graphId, makeState('node-2', 'seed', 99)) - - const widgets = getNodeWidgetsThroughWorld(world, graphId, 'node-1') - expect(widgets.map((w) => w.name).sort()).toEqual(['cfg', 'seed']) - }) - - it('returns an empty array for unknown nodes', () => { - const world = createWorld() - expect(getNodeWidgetsThroughWorld(world, graphId, 'missing')).toEqual([]) - }) -}) diff --git a/src/world/widgetWorldBridge.ts b/src/world/widgetWorldBridge.ts deleted file mode 100644 index 242422ca6e..0000000000 --- a/src/world/widgetWorldBridge.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { NodeId } from '@/lib/litegraph/src/LGraphNode' -import type { WidgetValue } from '@/stores/widgetComponents' -import { - WidgetContainerComponent, - WidgetValueComponent -} from '@/stores/widgetComponents' -import type { WidgetState } from '@/stores/widgetValueStore' - -import type { GraphId } from './entityIds' -import { nodeEntityId, widgetEntityId } from './entityIds' -import type { World } from './world' - -/** - * Slice 1 bridge: writes widget entities into the World whenever - * `WidgetValueStore.registerWidget` runs. The `state` argument is the - * SAME reactive object the store holds — sharing identity preserves Vue - * tracking across both read paths. - */ -export function registerWidgetInWorld( - world: World, - graphId: GraphId, - state: WidgetState -): void { - const widgetId = widgetEntityId(graphId, state.nodeId, state.name) - // `state` IS the reactive object owned by `WidgetValueStore`; sharing the - // reference is intentional — Vue tracking flows through both read paths - // during the slice-1 bridge window. The wider WidgetState shape collapses - // to `WidgetValue` at the component-key boundary. - world.setComponent(widgetId, WidgetValueComponent, state as WidgetValue) - - const nodeId = nodeEntityId(graphId, state.nodeId) - const container = world.getComponent(nodeId, WidgetContainerComponent) - if (!container) { - world.setComponent(nodeId, WidgetContainerComponent, { - widgetIds: [widgetId] - }) - return - } - if (!container.widgetIds.includes(widgetId)) { - container.widgetIds.push(widgetId) - } -} - -/** - * Look up all widget value states attached to a node, going through the - * World rather than the Pinia store. Used by `useUpstreamValue`. - */ -export function getNodeWidgetsThroughWorld( - world: World, - graphId: GraphId, - nodeId: NodeId -): WidgetState[] { - const owner = nodeEntityId(graphId, nodeId) - const container = world.getComponent(owner, WidgetContainerComponent) - if (!container) return [] - const widgets: WidgetState[] = [] - for (const widgetId of container.widgetIds) { - const value = world.getComponent(widgetId, WidgetValueComponent) - if (value) widgets.push(value as unknown as WidgetState) - } - return widgets -}