Compare commits

...

23 Commits

Author SHA1 Message Date
DrJKL
85008b7c0b refactor: move NodeId normalization to boundaries 2026-06-24 16:26:45 -07:00
DrJKL
b7cb8cb0ec fix: require branded node ids for pricing refs 2026-06-24 14:53:04 -07:00
DrJKL
1e45c0a405 fix: require branded node ids in vue node surfaces 2026-06-24 14:48:59 -07:00
DrJKL
a9dfcfa445 test: expect string-backed node ids 2026-06-24 14:34:52 -07:00
DrJKL
82108ea87e fix: align node ids after main rebase 2026-06-24 12:18:52 -07:00
DrJKL
0cb521ae06 fix: address node id review feedback 2026-06-24 12:14:06 -07:00
DrJKL
1cb6fb6c23 fix: preserve promoted preview node ids 2026-06-24 12:14:06 -07:00
DrJKL
6c4cf12a04 fix: consolidate serialized node id types 2026-06-24 12:14:06 -07:00
DrJKL
dc74b0edc2 fix: keep linear input id internal 2026-06-24 12:13:23 -07:00
DrJKL
b7c262d327 fix: standardize node ids in layout and subgraphs 2026-06-24 12:13:23 -07:00
DrJKL
2fb4e24894 fix: normalize node ids at frontend boundaries 2026-06-24 12:13:23 -07:00
DrJKL
a79b78deef refactor: strengthen widget host node ids 2026-06-24 12:12:55 -07:00
DrJKL
cd94b01d9f refactor: strengthen load3d node ids 2026-06-24 12:12:55 -07:00
DrJKL
797d9c83c7 refactor: strengthen image crop node ids 2026-06-24 12:12:54 -07:00
DrJKL
dfafeae897 refactor: strengthen graph node ids 2026-06-24 12:12:54 -07:00
DrJKL
c79aeaf16d refactor: strengthen canvas node ids 2026-06-24 12:11:47 -07:00
DrJKL
7c717e2570 refactor: strengthen vue node ids 2026-06-24 12:11:47 -07:00
DrJKL
57f891836f fix: preserve branded layout operation ids 2026-06-24 12:11:47 -07:00
DrJKL
7b57fb0bb9 fix: preserve branded layout node ids 2026-06-24 12:11:47 -07:00
DrJKL
c4ac059027 fix: brand layout node ids 2026-06-24 12:08:21 -07:00
DrJKL
f79ab0399d fix: use serialized node ids for queue results 2026-06-24 12:08:21 -07:00
DrJKL
c53ee739b1 fix: type execution error maps as strings 2026-06-24 12:08:21 -07:00
DrJKL
3ba1b05893 feat: brand local node ids 2026-06-24 12:08:21 -07:00
223 changed files with 1646 additions and 1184 deletions

View File

@@ -179,6 +179,9 @@ This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g
23. Favor pure functions (especially testable ones)
24. Do not use function expressions if it's possible to use function declarations instead
25. Watch out for [Code Smells](https://wiki.c2.com/?CodeSmell) and refactor to avoid them
26. Do not add alias helpers whose implementation is just a single-line call to another function
- Bad: `function id(value) { return nodeId(value) }`
- Use the real function directly, or introduce a named helper only when it adds validation, branching, domain meaning, or shared behavior beyond renaming
## Design Standards

View File

@@ -8,6 +8,7 @@ import { createI18n } from 'vue-i18n'
import WidgetBoundingBoxes from './WidgetBoundingBoxes.vue'
import boundingBoxes from '@/locales/en/main.json'
import type { BoundingBox } from '@/types/boundingBoxes'
import { nodeId as toNodeId } from '@/types/nodeId'
const { appState } = vi.hoisted(() => ({ appState: { node: null as unknown } }))
@@ -83,7 +84,7 @@ function prepCanvas(canvas: HTMLCanvasElement) {
function renderWidget(modelValue: BoundingBox[]) {
const result = render(WidgetBoundingBoxes, {
props: { nodeId: '1', modelValue },
props: { nodeId: toNodeId('1'), modelValue },
global: { plugins: [i18n] }
})
const canvas = screen.getByTestId('bounding-boxes').querySelector('canvas')!

View File

@@ -145,8 +145,9 @@ import Button from '@/components/ui/button/Button.vue'
import Textarea from '@/components/ui/textarea/Textarea.vue'
import { useBoundingBoxes } from '@/composables/boundingBoxes/useBoundingBoxes'
import type { BoundingBox } from '@/types/boundingBoxes'
import type { NodeId } from '@/types/nodeId'
const { nodeId } = defineProps<{ nodeId: string }>()
const { nodeId } = defineProps<{ nodeId: NodeId }>()
const modelValue = defineModel<BoundingBox[]>({ default: () => [] })
const canvasEl = useTemplateRef<HTMLCanvasElement>('canvasEl')

View File

@@ -12,8 +12,9 @@ import { useResolvedSelectedInputs } from '@/components/builder/useResolvedSelec
import type { ResolvedSelection } from '@/components/builder/useResolvedSelectedInputs'
import type { WidgetId } from '@/types/widgetId'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import type { SerializedNodeId } from '@/types/nodeId'
import {
LGraphEventMode,
TitleMode
@@ -50,7 +51,7 @@ workflowStore.activeWorkflow?.changeTracker?.reset()
const resolvedInputs = useResolvedSelectedInputs()
const outputsWithState = computed<[NodeId, string][]>(() =>
const outputsWithState = computed<[SerializedNodeId, string][]>(() =>
appModeStore.selectedOutputs.map((nodeId) => [
nodeId,
app.rootGraph.getNodeById(nodeId)?.title ?? String(nodeId)
@@ -75,7 +76,7 @@ function getHovered(
if (widget || node.constructor.nodeData?.output_node) return [node, widget]
}
function getNodeBounding(nodeId: NodeId) {
function getNodeBounding(nodeId: SerializedNodeId) {
if (settingStore.get('Comfy.VueNodes.Enabled')) return undefined
const node = app.rootGraph.getNodeById(nodeId)
if (!node) return
@@ -149,7 +150,7 @@ function handleClick(e: MouseEvent) {
function nodeToDisplayTuple(
n: LGraphNode
): [NodeId, MaybeRef<BoundStyle> | undefined, boolean] {
): [SerializedNodeId, MaybeRef<BoundStyle> | undefined, boolean] {
return [
n.id,
getNodeBounding(n.id),

View File

@@ -23,6 +23,7 @@ import { useAppModeStore } from '@/stores/appModeStore'
import { parseImageWidgetValue } from '@/utils/imageUtil'
import { cn } from '@comfyorg/tailwind-utils'
import { HideLayoutFieldKey } from '@/types/widgetTypes'
import { nodeId as toNodeId } from '@/types/nodeId'
import { promptRenameWidget } from '@/utils/widgetUtil'
interface WidgetEntry {
@@ -75,7 +76,7 @@ const mappedSelections = computed((): WidgetEntry[] => {
if (!matchingWidget) return []
matchingWidget.slotMetadata = undefined
matchingWidget.nodeId = String(node.id)
matchingWidget.nodeId = toNodeId(node.id)
return [
{
@@ -139,7 +140,7 @@ async function handleDragDrop() {
return false
}
app.dragOverNode = { id: -1, onDragDrop }
app.dragOverNode = { id: toNodeId(-1), onDragDrop }
}
defineExpose({ handleDragDrop })

View File

@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, ref } from 'vue'
import { createI18n } from 'vue-i18n'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
const i18n = createI18n({
@@ -239,7 +240,7 @@ describe('WidgetCurve', () => {
renderWidget(
makeWidget({
options: { disabled: true },
linkedUpstream: { nodeId: 'n1' }
linkedUpstream: { nodeId: toNodeId('n1') }
})
)
const parsed = JSON.parse(

View File

@@ -11,6 +11,7 @@ import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { BaseDOMWidget } from '@/scripts/domWidget'
import { useDomWidgetStore } from '@/stores/domWidgetStore'
import { nodeId } from '@/types/nodeId'
type TestWidget = BaseDOMWidget<object | string>
@@ -21,7 +22,7 @@ function createNode(
pos: [number, number]
) {
const node = new LGraphNode(title)
node.id = id
node.id = nodeId(id)
node.pos = [...pos]
node.size = [240, 120]
graph.add(node)

View File

@@ -73,7 +73,7 @@
:key="nodeData.id"
:node-data="nodeData"
:error="
executionErrorStore.lastExecutionError?.node_id === nodeData.id
executionErrorStore.lastExecutionErrorNodeId === nodeData.id
? 'Execution error'
: null
"

View File

@@ -3,6 +3,9 @@ import PrimeVue from 'primevue/config'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, nextTick, ref } from 'vue'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
const execHolder = vi.hoisted(() => ({
state: null as {
executingNodeIds: Array<string | number>
@@ -35,7 +38,7 @@ const SkeletonStub = defineComponent({
function renderPreview(
text: string,
{ nodeId = 'node-1' }: { nodeId?: string | number } = {}
{ nodeId = toNodeId('node-1') }: { nodeId?: NodeId } = {}
) {
const value = ref(text)
const Harness = defineComponent({
@@ -167,21 +170,21 @@ describe('TextPreviewWidget', () => {
it('hides the Skeleton on mount when execution is already idle', () => {
execState().executingNodeIds = []
execState().isIdle = true
renderPreview('text', { nodeId: 'n1' })
renderPreview('text', { nodeId: toNodeId('n1') })
expect(screen.queryByTestId('skeleton')).toBeNull()
})
it('shows a Skeleton on mount when the parent node is executing', () => {
execState().executingNodeIds = ['n1']
execState().isIdle = false
renderPreview('text', { nodeId: 'n1' })
renderPreview('text', { nodeId: toNodeId('n1') })
expect(screen.getByTestId('skeleton')).toBeInTheDocument()
})
it('hides the Skeleton when execution transitions to idle', async () => {
execState().executingNodeIds = ['n1']
execState().isIdle = false
renderPreview('text', { nodeId: 'n1' })
renderPreview('text', { nodeId: toNodeId('n1') })
expect(screen.getByTestId('skeleton')).toBeInTheDocument()
execState().executingNodeIds = []
@@ -194,7 +197,7 @@ describe('TextPreviewWidget', () => {
it('hides the Skeleton when the parent node leaves executingNodeIds', async () => {
execState().executingNodeIds = ['n1']
execState().isIdle = false
renderPreview('text', { nodeId: 'n1' })
renderPreview('text', { nodeId: toNodeId('n1') })
execState().executingNodeIds = ['other']
await nextTick()

View File

@@ -16,8 +16,9 @@ import { default as DOMPurify } from 'dompurify'
import Skeleton from 'primevue/skeleton'
import { computed, onMounted, watch } from 'vue'
import type { NodeId } from '@/lib/litegraph/src/litegraph'
import { useExecutionStore } from '@/stores/executionStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
const modelValue = defineModel<string>({ required: true })
@@ -76,7 +77,7 @@ onMounted(() => {
watch(
() => executionStore.executingNodeIds,
(ids) => {
if (!parentNodeId && ids.length > 0) parentNodeId = ids[0]
if (!parentNodeId && ids.length > 0) parentNodeId = toNodeId(ids[0])
}
)
</script>

View File

@@ -5,6 +5,7 @@ import { defineComponent, ref } from 'vue'
import { createI18n } from 'vue-i18n'
import type { Bounds } from '@/renderer/core/layout/types'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import type { Ref } from 'vue'
@@ -132,11 +133,12 @@ function renderWidget(
initialModel: Bounds = { x: 0, y: 0, width: 512, height: 512 }
) {
const value = ref<Bounds>(initialModel)
const nodeId = toNodeId(1)
const Harness = defineComponent({
components: { WidgetImageCrop },
setup: () => ({ value, widget }),
setup: () => ({ value, widget, nodeId }),
template:
'<WidgetImageCrop v-model="value" :widget="widget" :node-id="1" />'
'<WidgetImageCrop v-model="value" :widget="widget" :node-id="nodeId" />'
})
const utils = render(Harness, {
global: {
@@ -233,7 +235,7 @@ describe('WidgetImageCrop', () => {
renderWidget(
makeWidget({
options: { disabled: true },
linkedUpstream: { nodeId: 'n1' }
linkedUpstream: { nodeId: toNodeId('n1') }
}),
{ x: 0, y: 0, width: 512, height: 512 }
)

View File

@@ -135,8 +135,8 @@ import {
boundsExtractor,
useUpstreamValue
} from '@/composables/useUpstreamValue'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { Bounds } from '@/renderer/core/layout/types'
import type { NodeId } from '@/types/nodeId'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { cn } from '@comfyorg/tailwind-utils'

View File

@@ -6,6 +6,8 @@ import { createI18n } from 'vue-i18n'
import Load3D from '@/components/load3d/Load3D.vue'
import type { ComponentWidget } from '@/scripts/domWidget'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
const { load3dState, resolveNodeMock, settingGetMock } = vi.hoisted(() => ({
load3dState: {
@@ -83,7 +85,7 @@ const i18n = createI18n({
type RenderOptions = {
widget?: unknown
nodeId?: number | string
nodeId?: NodeId
stateOverrides?: Partial<ReturnType<typeof buildLoad3dStub>>
enable3DViewer?: boolean
}
@@ -165,16 +167,17 @@ describe('Load3D', () => {
})
it('falls back to resolveNode(nodeId) when the widget lacks a node', async () => {
const nodeId = toNodeId(42)
resolveNodeMock.mockReturnValue(MOCK_NODE)
renderLoad3D({ widget: {}, nodeId: 42 })
renderLoad3D({ widget: {}, nodeId })
expect(resolveNodeMock).toHaveBeenCalledWith(42)
expect(resolveNodeMock).toHaveBeenCalledWith(nodeId)
expect(await screen.findByTestId('load3d-scene')).toBeInTheDocument()
})
it('does not render Load3DScene when no node can be resolved', async () => {
resolveNodeMock.mockReturnValue(null)
renderLoad3D({ widget: {}, nodeId: 99 })
renderLoad3D({ widget: {}, nodeId: toNodeId(99) })
await Promise.resolve()
expect(screen.queryByTestId('load3d-scene')).not.toBeInTheDocument()
@@ -219,7 +222,11 @@ describe('Load3D', () => {
it('hides ViewerControls when there is no node even if the setting is on', () => {
resolveNodeMock.mockReturnValue(null)
renderLoad3D({ widget: {}, nodeId: 1, enable3DViewer: true })
renderLoad3D({
widget: {},
nodeId: toNodeId(1),
enable3DViewer: true
})
expect(screen.queryByTestId('viewer-controls')).not.toBeInTheDocument()
})
})

View File

@@ -115,10 +115,10 @@ import Button from '@/components/ui/button/Button.vue'
import { useLoad3d } from '@/composables/useLoad3d'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import { resolveNode } from '@/utils/litegraphUtil'
import type { ComponentWidget } from '@/scripts/domWidget'
import type { NodeId } from '@/types/nodeId'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { resolveNode } from '@/utils/litegraphUtil'
const {
widget,

View File

@@ -2,6 +2,8 @@ import { render } from '@testing-library/vue'
import { describe, expect, it, vi } from 'vitest'
import { defineComponent, h, ref } from 'vue'
import { nodeId as toNodeId } from '@/types/nodeId'
const lastProps = ref<Record<string, unknown> | null>(null)
vi.mock('@/components/load3d/Load3D.vue', () => ({
@@ -39,9 +41,10 @@ describe('Load3DAdvanced', () => {
})
it('forwards widget and nodeId to the inner Load3D', () => {
const nodeId = toNodeId('a')
const widget = { node: { id: 'a', type: 'Load3DAdvanced' } }
render(Load3DAdvanced, { props: { widget: widget as never, nodeId: 'a' } })
render(Load3DAdvanced, { props: { widget: widget as never, nodeId } })
expect(lastProps.value?.widget).toEqual(widget)
expect(lastProps.value?.nodeId).toBe('a')
expect(lastProps.value?.nodeId).toBe(nodeId)
})
})

View File

@@ -10,8 +10,8 @@
<script setup lang="ts">
import Load3D from '@/components/load3d/Load3D.vue'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ComponentWidget } from '@/scripts/domWidget'
import type { NodeId } from '@/types/nodeId'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
defineProps<{

View File

@@ -289,11 +289,12 @@ import { computed, useTemplateRef } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import Slider from '@/components/ui/slider/Slider.vue'
import { PAINTER_TOOLS, usePainter } from '@/composables/painter/usePainter'
import type { NodeId } from '@/types/nodeId'
import { toHexFromFormat } from '@/utils/colorUtil'
import { cn } from '@comfyorg/tailwind-utils'
const { nodeId } = defineProps<{
nodeId: string
nodeId: NodeId
}>()
const modelValue = defineModel<string>({ default: '' })

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/vue'
import { createNodeLocatorId } from '@/types/nodeIdentification'
import { nodeId as toNodeId } from '@/types/nodeId'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, ref } from 'vue'
@@ -135,7 +136,7 @@ describe('WidgetRange', () => {
setUpstream({ min: 0.3, max: 0.7 })
renderWidget(
makeWidget({ disabled: true } as IWidgetRangeOptions, {
linkedUpstream: { nodeId: 'n1' }
linkedUpstream: { nodeId: toNodeId('n1') }
}),
{ min: 0, max: 1 }
)
@@ -145,10 +146,13 @@ describe('WidgetRange', () => {
it('ignores upstream value when not disabled', () => {
setUpstream({ min: 0.3, max: 0.7 })
renderWidget(makeWidget({}, { linkedUpstream: { nodeId: 'n1' } }), {
min: 0,
max: 1
})
renderWidget(
makeWidget({}, { linkedUpstream: { nodeId: toNodeId('n1') } }),
{
min: 0,
max: 1
}
)
const el = screen.getByTestId('range-editor')
expect(JSON.parse(el.dataset.model!)).toEqual({ min: 0, max: 1 })
})

View File

@@ -22,6 +22,7 @@ import { DraggableList } from '@/scripts/ui/draggableList'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import { useSettingStore } from '@/platform/settings/settingStore'
import { cn } from '@comfyorg/tailwind-utils'
import { useNodeDefStore } from '@/stores/nodeDefStore'
@@ -149,10 +150,11 @@ function isWidgetShownOnParents(
const source = widgetPromotedSource(widgetNode, widget)
return parents.some((parent) => {
if (source) {
const widgetNodeId = toNodeId(widgetNode.id)
const interiorNodeId =
String(widgetNode.id) === String(parent.id)
? source.nodeId
: String(widgetNode.id)
: widgetNodeId
return isWidgetPromotedOnSubgraphNode(parent, {
sourceNodeId: interiorNodeId,
@@ -160,7 +162,7 @@ function isWidgetShownOnParents(
})
}
return isWidgetPromotedOnSubgraphNode(parent, {
sourceNodeId: String(widgetNode.id),
sourceNodeId: toNodeId(widgetNode.id),
sourceWidgetName: widget.name
})
})

View File

@@ -4,11 +4,13 @@ import { computed, reactive, ref, shallowRef, watch } from 'vue'
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
import type { NodeWidgetsListList } from '../shared'
@@ -56,12 +58,12 @@ function setSectionCollapsed(nodeId: NodeId, collapsed: boolean) {
const isAllCollapsed = computed({
get() {
return searchedWidgetsSectionDataList.value.every(({ node }) =>
isSectionCollapsed(node.id)
isSectionCollapsed(toNodeId(node.id))
)
},
set(collapse: boolean) {
for (const { node } of widgetsSectionDataList.value) {
setSectionCollapsed(node.id, collapse)
setSectionCollapsed(toNodeId(node.id), collapse)
}
}
})
@@ -101,7 +103,7 @@ async function searcher(query: string) {
:key="node.id"
:node
:widgets
:collapse="isSectionCollapsed(node.id) && !isSearching"
:collapse="isSectionCollapsed(toNodeId(node.id)) && !isSearching"
:tooltip="
isSearching || widgets.length
? ''
@@ -109,7 +111,7 @@ async function searcher(query: string) {
"
show-locate-button
class="border-b border-interface-stroke"
@update:collapse="setSectionCollapsed(node.id, $event)"
@update:collapse="setSectionCollapsed(toNodeId(node.id), $event)"
/>
</TransitionGroup>
</template>

View File

@@ -3,11 +3,13 @@ import { storeToRefs } from 'pinia'
import { computed, reactive, ref, shallowRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
import type { NodeWidgetsListList } from '../shared'
@@ -80,7 +82,7 @@ function setSectionCollapsed(nodeId: NodeId, collapsed: boolean) {
const isAllCollapsed = computed({
get() {
const normalAllCollapsed = searchedWidgetsSectionDataList.value.every(
({ node }) => isSectionCollapsed(node.id)
({ node }) => isSectionCollapsed(toNodeId(node.id))
)
const hasAdvanced = advancedWidgetsSectionDataList.value.length > 0
return hasAdvanced
@@ -89,7 +91,7 @@ const isAllCollapsed = computed({
},
set(collapse: boolean) {
for (const { node } of widgetsSectionDataList.value) {
setSectionCollapsed(node.id, collapse)
setSectionCollapsed(toNodeId(node.id), collapse)
}
advancedCollapsed.value = collapse
}
@@ -154,7 +156,7 @@ const advancedLabel = computed(() => {
:node
:label
:widgets
:collapse="isSectionCollapsed(node.id) && !isSearching"
:collapse="isSectionCollapsed(toNodeId(node.id)) && !isSearching"
:show-locate-button="isMultipleNodesSelected"
:tooltip="
isSearching || widgets.length
@@ -162,7 +164,7 @@ const advancedLabel = computed(() => {
: t('rightSidePanel.inputsNoneTooltip')
"
class="border-b border-interface-stroke"
@update:collapse="setSectionCollapsed(node.id, $event)"
@update:collapse="setSectionCollapsed(toNodeId(node.id), $event)"
/>
</TransitionGroup>
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">

View File

@@ -14,6 +14,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import { searchWidgets } from '../shared'
import type { NodeWidgetsList } from '../shared'
@@ -82,7 +83,7 @@ const advancedInputsWidgets = computed((): NodeWidgetsList => {
return allInteriorWidgets.filter(
({ node: interiorNode, widget }) =>
!isWidgetPromotedOnSubgraphNode(node, {
sourceNodeId: String(interiorNode.id),
sourceNodeId: toNodeId(interiorNode.id),
sourceWidgetName: getWidgetName(widget)
})
)

View File

@@ -19,6 +19,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import { getWidgetDefaultValue, promptWidgetLabel } from '@/utils/widgetUtil'
import type { WidgetValue } from '@/utils/widgetUtil'
@@ -90,9 +91,10 @@ function handleHideInput() {
const source = widgetPromotedSource(node, widget)
if (source) {
const currentNodeId = toNodeId(node.id)
for (const parent of parents) {
const sourceNodeId =
String(node.id) === String(parent.id) ? source.nodeId : String(node.id)
String(node.id) === String(parent.id) ? source.nodeId : currentNodeId
demotePromotedInput(parent, {
sourceNodeId,
sourceWidgetName: source.widgetName

View File

@@ -21,6 +21,7 @@ import {
useWidgetValueStore
} from '@/stores/widgetValueStore'
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { widgetId } from '@/types/widgetId'
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
@@ -70,7 +71,7 @@ const widgetComponent = computed(() => {
const isLinked = computed(() => {
const safeWidget = useVueNodeLifecycle()
.nodeManager.value?.vueNodeData.get(String(node.id))
.nodeManager.value?.vueNodeData.get(toNodeId(node.id))
?.widgets?.find((w) => w.name === widget.name)
return safeWidget?.slotMetadata
? !!safeWidget.slotMetadata.linked
@@ -212,7 +213,7 @@ const displayLabel = customRef((track, trigger) => {
:is="widgetComponent"
v-model="widgetValue"
:widget="simplifiedWidget"
:node-id="String(node.id)"
:node-id="toNodeId(node.id)"
:node-type="node.type"
:class="cn('col-span-1', shouldExpand(widget.type) && 'min-h-36')"
/>

View File

@@ -5,8 +5,10 @@ import type { IFuseOptions } from 'fuse.js'
import type { Positionable } from '@/lib/litegraph/src/interfaces'
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -92,7 +94,7 @@ export function searchWidgetsAndNodes(
}
const searchableList: NodeSearchItem[] = list.map((item) => ({
nodeId: item.node.id,
nodeId: toNodeId(item.node.id),
searchableTitle: (item.node.getTitle() ?? '').toLowerCase()
}))
@@ -108,8 +110,8 @@ export function searchWidgetsAndNodes(
)
return list
.map((item) => {
if (matchedNodeIds.has(item.node.id)) {
.map((item, index) => {
if (matchedNodeIds.has(searchableList[index].nodeId)) {
return { ...item, keep: true }
}
return {

View File

@@ -32,6 +32,7 @@ import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import { useLitegraphService } from '@/services/litegraphService'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import { cn } from '@comfyorg/tailwind-utils'
import SubgraphNodeWidget from './SubgraphNodeWidget.vue'
@@ -116,7 +117,7 @@ function getActivePreviewRows(node: SubgraphNode): PreviewRow[] {
const rootGraphId = node.rootGraph.id
const exposures = previewExposureStore.getExposures(rootGraphId, hostLocator)
return exposures.flatMap((exposure): PreviewRow[] => {
const sourceNode = node.subgraph._nodes_by_id[exposure.sourceNodeId]
const sourceNode = node.subgraph.getNodeById(exposure.sourceNodeId)
if (!sourceNode) return []
const realWidget = getPromotableWidgets(sourceNode).find(
(candidate) => candidate.name === exposure.sourcePreviewName
@@ -248,7 +249,7 @@ function rowDisplayName(row: ActiveRow): string {
function isRowLinked(row: ActiveRow): boolean {
if (row.kind !== 'promoted') return false
if (row.node.id === -1) return true
if (row.node.id === toNodeId(-1)) return true
const source = promotedRowSource(row)
return (
!!activeNode.value &&

View File

@@ -4,8 +4,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ResultItemImpl } from '@/stores/queueStore'
import type { SerializedNodeId } from '@/types/nodeId'
import MediaLightbox from './MediaLightbox.vue'
@@ -28,7 +28,7 @@ type MockResultItem = Partial<ResultItemImpl> & {
filename: string
subfolder: string
type: string
nodeId: NodeId
nodeId: SerializedNodeId
mediaType: string
id?: string
url?: string
@@ -63,7 +63,7 @@ describe('MediaLightbox', () => {
filename: 'image1.jpg',
subfolder: 'outputs',
type: 'output',
nodeId: '123' as NodeId,
nodeId: '123',
mediaType: 'images',
isImage: true,
isVideo: false,
@@ -75,7 +75,7 @@ describe('MediaLightbox', () => {
filename: 'image2.jpg',
subfolder: 'outputs',
type: 'output',
nodeId: '456' as NodeId,
nodeId: '456',
mediaType: 'images',
isImage: true,
isVideo: false,
@@ -87,7 +87,7 @@ describe('MediaLightbox', () => {
filename: 'image3.jpg',
subfolder: 'outputs',
type: 'output',
nodeId: '789' as NodeId,
nodeId: '789',
mediaType: 'images',
isImage: true,
isVideo: false,

View File

@@ -6,6 +6,7 @@ import { defineComponent, h, nextTick, ref, shallowRef } from 'vue'
import { useBoundingBoxes } from './useBoundingBoxes'
import type { BoundingBox } from '@/types/boundingBoxes'
import { nodeId as toNodeId } from '@/types/nodeId'
const { appState } = vi.hoisted(() => ({
appState: { node: null as unknown }
@@ -103,7 +104,7 @@ function setup(initial: BoundingBox[] = []) {
const canvasContainer = shallowRef<HTMLDivElement | null>(null)
const inlineEditorEl = shallowRef<HTMLTextAreaElement | null>(null)
const modelValue = ref(initial)
const api = useBoundingBoxes('1', {
const api = useBoundingBoxes(toNodeId('1'), {
canvasEl,
canvasContainer,
inlineEditorEl,

View File

@@ -18,6 +18,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
import type { BoundingBox } from '@/types/boundingBoxes'
import type { NodeId } from '@/types/nodeId'
import { readableTextColor, textOnColor } from '@/utils/colorUtil'
const HANDLE_PX = 8
@@ -39,7 +40,7 @@ interface UseBoundingBoxesOptions {
}
export function useBoundingBoxes(
nodeId: string,
nodeId: NodeId,
{
canvasEl,
canvasContainer,
@@ -63,9 +64,7 @@ export function useBoundingBoxes(
nodeId && app.canvas?.graph ? app.canvas.graph.getNodeById(nodeId) : null
)
const { selectedNodeIds } = storeToRefs(useCanvasStore())
const isNodeSelected = computed(() =>
selectedNodeIds.value.has(String(nodeId))
)
const isNodeSelected = computed(() => selectedNodeIds.value.has(nodeId))
function dimWidget(name: 'width' | 'height'): number | undefined {
const v = litegraphNode.value?.widgets?.find((w) => w.name === name)?.value

View File

@@ -11,6 +11,7 @@ import {
} from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { NodeId } from '@/renderer/core/layout/types'
import { nodeId } from '@/types/nodeId'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
const mockApp = vi.hoisted(() => ({
@@ -38,7 +39,7 @@ vi.mock('@/lib/litegraph/src/litegraph', async (importOriginal) => {
// unmodified — the node accessors filter selectedItems with the real predicate.
const makeNode = (mode: LGraphEventMode, id = 1): LGraphNode => {
const node = new LGraphNode('Test')
node.id = id
node.id = nodeId(id)
node.mode = mode
return node
}
@@ -69,7 +70,7 @@ class MockNode implements Positionable {
) {
this.pos = pos
this.size = size
this.id = 'mock-node'
this.id = nodeId('mock-node')
this.boundingRect = [0, 0, 0, 0]
}

View File

@@ -12,6 +12,7 @@ import {
} from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
import { computeUnionBounds } from '@/utils/mathUtil'
@@ -100,8 +101,7 @@ export function useSelectionToolboxPosition(
if (item.id == null) continue
if (shouldRenderVueNodes.value && typeof item.id === 'string') {
// Use layout store for Vue nodes (only works with string IDs)
const layout = layoutStore.getNodeLayoutRef(item.id).value
const layout = layoutStore.getNodeLayoutRef(toNodeId(item.id)).value
if (layout) {
allBounds.push([
layout.bounds.x,

View File

@@ -74,8 +74,8 @@ describe('computeArrangement', () => {
// 2: pos.y = 112.
const result = computeArrangement(nodes, 'vertical')
expect(result).toEqual([
{ nodeId: 1, position: { x: 0, y: 0 } },
{ nodeId: 2, position: { x: 0, y: 100 + GAP } }
{ nodeId: '1', position: { x: 0, y: 0 } },
{ nodeId: '2', position: { x: 0, y: 100 + GAP } }
])
})
@@ -88,8 +88,8 @@ describe('computeArrangement', () => {
// 2: pos.y = 212+30 = 242.
const result = computeArrangement(nodes, 'vertical')
expect(result).toEqual([
{ nodeId: 1, position: { x: 0, y: 0 } },
{ nodeId: 2, position: { x: 0, y: 200 + TITLE + GAP } }
{ nodeId: '1', position: { x: 0, y: 0 } },
{ nodeId: '2', position: { x: 0, y: 200 + TITLE + GAP } }
])
})
})
@@ -131,10 +131,10 @@ describe('computeArrangement', () => {
// pos.y = rowVisualTop + 30 (titleHeight).
const result = computeArrangement(nodes, 'grid')
expect(result).toEqual([
{ nodeId: 1, position: { x: 0, y: 0 } },
{ nodeId: 2, position: { x: 132, y: 0 } },
{ nodeId: 3, position: { x: 0, y: 102 } },
{ nodeId: 4, position: { x: 132, y: 102 } }
{ nodeId: '1', position: { x: 0, y: 0 } },
{ nodeId: '2', position: { x: 132, y: 0 } },
{ nodeId: '3', position: { x: 0, y: 102 } },
{ nodeId: '4', position: { x: 132, y: 102 } }
])
})

View File

@@ -1,12 +1,14 @@
import { useSelectionState } from '@/composables/graph/useSelectionState'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { Point } from '@/renderer/core/layout/types'
import { app } from '@/scripts/app'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
export type ArrangeLayout = 'vertical' | 'horizontal' | 'grid'
@@ -39,7 +41,7 @@ const titleHeightOf = (node: LGraphNode): number => {
const toBox = (node: LGraphNode): NodeBox => {
const titleHeight = titleHeightOf(node)
return {
id: node.id,
id: toNodeId(node.id),
posX: node.pos[0],
posY: node.pos[1],
visualWidth: node.size[0],

View File

@@ -22,6 +22,7 @@ import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNod
import { app } from '@/scripts/app'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { createNodeExecutionId } from '@/types/nodeIdentification'
import { nodeId } from '@/types/nodeId'
import { seedRequiredInputMissingNodeError } from '@/utils/__tests__/executionErrorTestUtils'
import type { MissingMediaCandidate } from '@/platform/missingMedia/types'
import type { MissingModelCandidate } from '@/platform/missingModel/types'
@@ -1011,7 +1012,7 @@ describe('clearWidgetRelatedErrors parameter routing', () => {
graph.add(host)
const interiorNode = new LGraphNode('CheckpointLoaderSimple')
interiorNode.id = 1
interiorNode.id = nodeId(1)
subgraph.add(interiorNode)
const input = interiorNode.addInput('ckpt_name', 'COMBO')
const widget = interiorNode.addWidget(

View File

@@ -16,6 +16,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { nodeId as toNodeId } from '@/types/nodeId'
describe('Node Reactivity', () => {
beforeEach(() => {
@@ -68,7 +69,7 @@ describe('Node Reactivity', () => {
const onValueChange = vi.fn()
graph.trigger('node:slot-links:changed', {
nodeId: String(node.id),
nodeId: toNodeId(node.id),
slotType: NodeSlotType.INPUT
})
await nextTick()
@@ -116,7 +117,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
const { graph, node } = createWidgetInputGraph()
const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(node.id))
const nodeData = vueNodeData.get(toNodeId(node.id))
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
expect(widgetData?.slotMetadata).toBeDefined()
@@ -127,7 +128,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
const { graph, node } = createWidgetInputGraph()
const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(node.id))
const nodeData = vueNodeData.get(toNodeId(node.id))
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
// Verify initially linked
@@ -155,7 +156,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
const { graph, node } = createWidgetInputGraph()
const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(node.id))!
const nodeData = vueNodeData.get(toNodeId(node.id))!
// Mimic what processedWidgets does in NodeWidgets.vue:
// derive disabled from slotMetadata.linked
@@ -204,7 +205,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
throw new Error('Expected SubgraphInput.connect to produce a link')
const { vueNodeData } = useGraphNodeManager(subgraph)
const nodeData = vueNodeData.get(String(node.id))
const nodeData = vueNodeData.get(toNodeId(node.id))
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
expect(widgetData?.slotMetadata?.linked).toBe(true)
@@ -230,7 +231,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
graph.add(subgraphNode)
const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(subgraphNode.id))
const nodeData = vueNodeData.get(toNodeId(subgraphNode.id))
const widgetData = nodeData?.widgets?.find((w) => w.name === 'value')
expect(widgetData).toBeDefined()
@@ -242,7 +243,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
const { graph, node } = createWidgetInputGraph()
const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(node.id))!
const nodeData = vueNodeData.get(toNodeId(node.id))!
const widgetData = nodeData.widgets!.find((w) => w.name === 'prompt')!
expect(widgetData.slotMetadata?.linked).toBe(true)
@@ -278,7 +279,7 @@ describe('Subgraph output slot label reactivity', () => {
graph.add(node)
const { vueNodeData } = useGraphNodeManager(graph)
const nodeId = String(node.id)
const nodeId = toNodeId(node.id)
const nodeData = vueNodeData.get(nodeId)
if (!nodeData?.outputs) throw new Error('Expected output data to exist')
@@ -306,7 +307,7 @@ describe('Subgraph output slot label reactivity', () => {
graph.add(node)
const { vueNodeData } = useGraphNodeManager(graph)
const nodeId = String(node.id)
const nodeId = toNodeId(node.id)
const nodeData = vueNodeData.get(nodeId)
if (!nodeData?.inputs) throw new Error('Expected input data to exist')
@@ -369,7 +370,7 @@ describe('Nested promoted widget mapping', () => {
graph.add(subgraphNodeB)
const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(subgraphNodeB.id))
const nodeData = vueNodeData.get(toNodeId(subgraphNodeB.id))
const mappedWidget = nodeData?.widgets?.[0]
expect(mappedWidget).toBeDefined()
@@ -406,7 +407,7 @@ describe('Nested promoted widget mapping', () => {
graph.add(subgraphNode)
const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(subgraphNode.id))
const nodeData = vueNodeData.get(toNodeId(subgraphNode.id))
const widgets = nodeData?.widgets
expect(widgets).toHaveLength(2)
@@ -452,7 +453,7 @@ describe('Promoted widget sourceExecutionId', () => {
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(subgraphNode.id))
const nodeData = vueNodeData.get(toNodeId(subgraphNode.id))
const promotedWidget = nodeData?.widgets?.find(
(w) => w.name === 'ckpt_input'
)
@@ -475,7 +476,7 @@ describe('Promoted widget sourceExecutionId', () => {
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
const { vueNodeData } = useGraphNodeManager(graph)
const nodeData = vueNodeData.get(String(node.id))
const nodeData = vueNodeData.get(toNodeId(node.id))
const widget = nodeData?.widgets?.find((w) => w.name === 'steps')
expect(widget).toBeDefined()
@@ -714,12 +715,13 @@ describe('Pre-remove vueNodeData drain', () => {
const node = new LGraphNode('test')
graph.add(node)
const { vueNodeData } = useGraphNodeManager(graph)
const id = toNodeId(node.id)
expect(vueNodeData.has(String(node.id))).toBe(true)
expect(vueNodeData.has(id)).toBe(true)
let dataPresentInOnRemoved: boolean | undefined
node.onRemoved = () => {
dataPresentInOnRemoved = vueNodeData.has(String(node.id))
dataPresentInOnRemoved = vueNodeData.has(id)
}
graph.remove(node)

View File

@@ -21,7 +21,8 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { NodeId } from '@/renderer/core/layout/types'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { isDOMWidget } from '@/scripts/domWidget'
import { IS_CONTROL_WIDGET } from '@/scripts/widgets'
@@ -30,7 +31,6 @@ import { useWidgetValueStore } from '@/stores/widgetValueStore'
import type { WidgetValue, SafeControlWidget } from '@/types/simplifiedWidget'
import { normalizeControlOption } from '@/types/simplifiedWidget'
import { getWidgetIdForNode } from '@/utils/litegraphUtil'
import type { NodeId as WorkflowNodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import type { WidgetId } from '@/types/widgetId'
@@ -51,7 +51,7 @@ import { getExecutionIdByNode } from '@/utils/graphTraversalUtil'
export interface WidgetSlotMetadata {
index: number
linked: boolean
originNodeId?: string
originNodeId?: NodeId
originOutputName?: string
type: string
}
@@ -136,10 +136,10 @@ export interface VueNodeData {
export interface GraphNodeManager {
// Reactive state - safe data extracted from LiteGraph nodes
vueNodeData: ReadonlyMap<string, VueNodeData>
vueNodeData: ReadonlyMap<NodeId, VueNodeData>
// Access to original LiteGraph nodes (non-reactive)
getNode(id: WorkflowNodeId): LGraphNode | undefined
getNode(id: NodeId): LGraphNode | undefined
// Lifecycle methods
cleanup(): void
@@ -353,14 +353,14 @@ function buildSlotMetadata(
): Map<string, WidgetSlotMetadata> {
const metadata = new Map<string, WidgetSlotMetadata>()
inputs?.forEach((input, index) => {
let originNodeId: string | undefined
let originNodeId: NodeId | undefined
let originOutputName: string | undefined
if (input.link != null && graphRef) {
const link = graphRef.getLink(input.link)
const originNode = link ? graphRef.getNodeById(link.origin_id) : null
if (link && originNode) {
originNodeId = String(link.origin_id)
originNodeId = toNodeId(link.origin_id)
originOutputName = originNode.outputs?.[link.origin_slot]?.name
}
}
@@ -471,7 +471,7 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
const badges = node.badges
return {
id: String(node.id),
id: toNodeId(node.id),
title: typeof node.title === 'string' ? node.title : '',
type: nodeType,
mode: node.mode || 0,
@@ -498,12 +498,12 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Get layout mutations composable
const { createNode, deleteNode, setSource } = useLayoutMutations()
// Safe reactive data extracted from LiteGraph nodes
const vueNodeData = reactive(new Map<string, VueNodeData>())
const vueNodeData = reactive(new Map<NodeId, VueNodeData>())
// Non-reactive storage for original LiteGraph nodes
const nodeRefs = new Map<string, LGraphNode>()
const nodeRefs = new Map<NodeId, LGraphNode>()
const refreshNodeSlots = (nodeId: string) => {
const refreshNodeSlots = (nodeId: NodeId) => {
const nodeRef = nodeRefs.get(nodeId)
const currentData = vueNodeData.get(nodeId)
@@ -518,14 +518,14 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
}
// Get access to original LiteGraph node (non-reactive)
const getNode = (id: WorkflowNodeId): LGraphNode | undefined => {
return nodeRefs.get(String(id))
const getNode = (id: NodeId): LGraphNode | undefined => {
return nodeRefs.get(id)
}
const syncWithGraph = () => {
if (!graph?._nodes) return
const currentNodes = new Set(graph._nodes.map((n) => String(n.id)))
const currentNodes = new Set(graph._nodes.map((n) => toNodeId(n.id)))
// Remove deleted nodes
for (const id of Array.from(vueNodeData.keys())) {
@@ -537,7 +537,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Add/update existing nodes
graph._nodes.forEach((node) => {
const id = String(node.id)
const id = toNodeId(node.id)
// Store non-reactive reference
nodeRefs.set(id, node)
@@ -555,7 +555,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
node: LGraphNode,
originalCallback?: (node: LGraphNode) => void
) => {
const id = String(node.id)
const id = toNodeId(node.id)
// Store non-reactive reference to original node
nodeRefs.set(id, node)
@@ -610,8 +610,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
}
}
const dropNodeReferences = (node: LGraphNode) => {
const id = String(node.id)
const dropNodeReferences = (id: NodeId) => {
nodeRefs.delete(id)
vueNodeData.delete(id)
}
@@ -620,9 +619,12 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
node: LGraphNode,
originalCallback?: (node: LGraphNode) => void
) => {
const id = String(node.id)
const id = toNodeId(node.id)
// Remove node from layout store
setSource(LayoutSource.Canvas)
void deleteNode(id)
dropNodeReferences(id)
originalCallback?.(node)
}
@@ -670,7 +672,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
const beforeNodeRemovedListener = (
e: CustomEvent<{ node: LGraphNode }>
) => {
dropNodeReferences(e.detail.node)
dropNodeReferences(toNodeId(e.detail.node.id))
}
graph.events.addEventListener(
'node:before-removed',
@@ -681,7 +683,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
[K in LGraphTriggerAction]: (event: LGraphTriggerParam<K>) => void
} = {
'node:property:changed': (propertyEvent) => {
const nodeId = String(propertyEvent.nodeId)
const nodeId = toNodeId(propertyEvent.nodeId)
const currentData = vueNodeData.get(nodeId)
if (currentData) {
@@ -777,15 +779,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
}
},
'node:slot-errors:changed': (slotErrorsEvent) => {
refreshNodeSlots(String(slotErrorsEvent.nodeId))
refreshNodeSlots(toNodeId(slotErrorsEvent.nodeId))
},
'node:slot-links:changed': (slotLinksEvent) => {
if (slotLinksEvent.slotType === NodeSlotType.INPUT) {
refreshNodeSlots(String(slotLinksEvent.nodeId))
refreshNodeSlots(toNodeId(slotLinksEvent.nodeId))
}
},
'node:slot-label:changed': (slotLabelEvent) => {
const nodeId = String(slotLabelEvent.nodeId)
const nodeId = toNodeId(slotLabelEvent.nodeId)
const nodeRef = nodeRefs.get(nodeId)
if (!nodeRef) return

View File

@@ -1,13 +1,10 @@
import { computed, ref } from 'vue'
import type { Ref } from 'vue'
import type {
LGraphGroup,
LGraphNode,
NodeId
} from '@/lib/litegraph/src/litegraph'
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { getExtraOptionsForWidget } from '@/services/litegraphService'
import type { SerializedNodeId } from '@/types/nodeId'
import { isLGraphGroup } from '@/utils/litegraphUtil'
import {
@@ -50,7 +47,7 @@ export enum BadgeVariant {
// Global singleton for NodeOptions component reference
let nodeOptionsInstance: null | NodeOptionsInstance = null
const hoveredWidget = ref<[string, NodeId | undefined]>()
const hoveredWidget = ref<[string, SerializedNodeId | undefined]>()
/**
* Toggle the node options popover
@@ -70,7 +67,7 @@ export function toggleNodeOptions(event: Event) {
export function showNodeOptions(
event: MouseEvent,
widgetName?: string,
nodeId?: NodeId
nodeId?: SerializedNodeId
) {
hoveredWidget.value = widgetName ? [widgetName, nodeId] : undefined
if (nodeOptionsInstance?.show) {

View File

@@ -8,6 +8,7 @@ import { useNodeMenuOptions } from '@/composables/graph/useNodeMenuOptions'
import type { Positionable } from '@/lib/litegraph/src/litegraph'
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { nodeId } from '@/types/nodeId'
// canvasStore transitively imports the app singleton; stub it so the real
// ComfyApp module never loads during these unit tests.
@@ -45,7 +46,7 @@ const i18n = createI18n({
const nodeWithMode = (mode: LGraphEventMode, id = 1): LGraphNode => {
const node = new LGraphNode('Test')
node.id = id
node.id = nodeId(id)
node.mode = mode
return node
}

View File

@@ -29,7 +29,7 @@ function useVueNodeLifecycleIndividual() {
// Initialize layout system with existing nodes from active graph
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
id: node.id.toString(),
id: node.id,
pos: [node.pos[0], node.pos[1]] as [number, number],
size: [node.size[0], node.size[1]] as [number, number]
}))
@@ -45,6 +45,7 @@ function useVueNodeLifecycleIndividual() {
// Seed existing links into the Layout Store (topology only)
for (const link of activeGraph._links.values()) {
if (link.origin_id === -1 || link.target_id === -1) continue
layoutMutations.createLink(
link.id,
link.origin_id,

View File

@@ -1,4 +1,5 @@
import { useMaskEditorDataStore } from '@/stores/maskEditorDataStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { ImageRef, ImageLayer } from '@/stores/maskEditorDataStore'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
@@ -178,7 +179,7 @@ export function useMaskEditorLoader() {
maskLayer,
paintLayer,
sourceRef,
nodeId: node.id
nodeId: toNodeId(node.id)
}
dataStore.sourceNode = node

View File

@@ -12,6 +12,7 @@ import {
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { ComfyNodeDef, PriceBadge } from '@/schemas/nodeDefSchema'
import { nodeId as toNodeId } from '@/types/nodeId'
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
// -----------------------------------------------------------------------------
@@ -619,14 +620,15 @@ describe('useNodePricing', () => {
LiteGraph.vueNodesMode = true
try {
const revBefore = getNodeRevisionRef(node.id).value
const nodeId = toNodeId(node.id)
const revBefore = getNodeRevisionRef(nodeId).value
const tickBefore = pricingRevision.value
getNodeDisplayPrice(node)
await new Promise((r) => setTimeout(r, 50))
// VueNodes path bumps per-node ref and the global tick.
expect(getNodeRevisionRef(node.id).value).toBeGreaterThan(revBefore)
expect(getNodeRevisionRef(nodeId).value).toBeGreaterThan(revBefore)
expect(pricingRevision.value).toBeGreaterThan(tickBefore)
} finally {
LiteGraph.vueNodesMode = false
@@ -658,7 +660,7 @@ describe('useNodePricing', () => {
describe('getNodeRevisionRef', () => {
it('should return a ref for a node ID', () => {
const { getNodeRevisionRef } = useNodePricing()
const ref = getNodeRevisionRef('node-1')
const ref = getNodeRevisionRef(toNodeId('node-1'))
expect(ref).toBeDefined()
expect(ref.value).toBe(0)
@@ -666,25 +668,24 @@ describe('useNodePricing', () => {
it('should return the same ref for the same node ID', () => {
const { getNodeRevisionRef } = useNodePricing()
const ref1 = getNodeRevisionRef('node-same')
const ref2 = getNodeRevisionRef('node-same')
const ref1 = getNodeRevisionRef(toNodeId('node-same'))
const ref2 = getNodeRevisionRef(toNodeId('node-same'))
expect(ref1).toBe(ref2)
})
it('should return different refs for different node IDs', () => {
const { getNodeRevisionRef } = useNodePricing()
const ref1 = getNodeRevisionRef('node-a')
const ref2 = getNodeRevisionRef('node-b')
const ref1 = getNodeRevisionRef(toNodeId('node-a'))
const ref2 = getNodeRevisionRef(toNodeId('node-b'))
expect(ref1).not.toBe(ref2)
})
it('should handle both string and number node IDs', () => {
const { getNodeRevisionRef } = useNodePricing()
// Number ID gets stringified, so '123' and 123 should return the same ref
const refFromNumber = getNodeRevisionRef(123)
const refFromString = getNodeRevisionRef('123')
const refFromNumber = getNodeRevisionRef(toNodeId(123))
const refFromString = getNodeRevisionRef(toNodeId('123'))
expect(refFromNumber).toBe(refFromString)
})

View File

@@ -24,6 +24,8 @@ import type {
WidgetDependency
} from '@/schemas/nodeDefSchema'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import type { Expression } from 'jsonata'
import jsonata from 'jsonata'
@@ -452,18 +454,17 @@ const pricingTick = ref(0)
// Per-node revision tracking for VueNodes mode (more efficient than global tick)
// Uses plain Map with individual refs per node for fine-grained reactivity
// Keys are stringified node IDs to handle both string and number ID types
const nodeRevisions = new Map<string, Ref<number>>()
const nodeRevisions = new Map<NodeId, Ref<number>>()
/**
* Get or create a revision ref for a specific node.
* Each node has its own independent ref, so updates to one won't trigger others.
*/
const getNodeRevisionRef = (nodeId: string | number): Ref<number> => {
const key = String(nodeId)
let rev = nodeRevisions.get(key)
const getNodeRevisionRef = (nodeId: NodeId): Ref<number> => {
let rev = nodeRevisions.get(nodeId)
if (!rev) {
rev = ref(0)
nodeRevisions.set(key, rev)
nodeRevisions.set(nodeId, rev)
}
return rev
}
@@ -511,7 +512,7 @@ const scheduleEvaluation = (
if (LiteGraph.vueNodesMode) {
// VueNodes mode: bump per-node revision (only this node re-renders)
getNodeRevisionRef(node.id).value++
getNodeRevisionRef(toNodeId(node.id)).value++
}
pricingTick.value++
})

View File

@@ -11,6 +11,7 @@ import {
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { createNodeLocatorId } from '@/types/nodeIdentification'
import { nodeId } from '@/types/nodeId'
import { CANVAS_IMAGE_PREVIEW_WIDGET } from './canvasImagePreviewTypes'
import { usePromotedPreviews } from './usePromotedPreviews'
@@ -58,7 +59,7 @@ function addInteriorNode(
} = { id: 10 }
): LGraphNode {
const node = new LGraphNode('test')
node.id = options.id
node.id = nodeId(options.id)
if (options.previewMediaType) {
node.previewMediaType = options.previewMediaType
}

View File

@@ -6,6 +6,8 @@ import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { UUID } from '@/utils/uuid'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import {
appendNodeExecutionId,
createNodeLocatorId
@@ -13,7 +15,7 @@ import {
import type { NodeExecutionId } from '@/types/nodeIdentification'
interface PromotedPreview {
sourceNodeId: string
sourceNodeId: NodeId
sourceWidgetName: string
type: 'image' | 'video' | 'audio'
urls: string[]
@@ -41,7 +43,7 @@ export function usePromotedPreviews(
/** Touches reactive sources for Vue tracking; `getNodeImageUrls` reads non-reactive app state. */
function readReactivePreviewUrls(
leafHost: SubgraphNode,
leafSourceNodeId: string,
leafSourceNodeId: NodeId,
leafExecutionId: NodeExecutionId,
interiorNode: LGraphNode
): string[] | undefined {
@@ -89,7 +91,7 @@ export function usePromotedPreviews(
function resolveNestedHost(
rootGraphId: UUID,
currentHostLocator: string,
sourceNodeId: string
sourceNodeId: NodeId
) {
const currentHost = hostNodesByLocator.get(currentHostLocator)
const sourceNode = currentHost?.subgraph.getNodeById(sourceNodeId)
@@ -114,7 +116,7 @@ export function usePromotedPreviews(
resolveNestedHost
)
const leaf = resolved?.leaf ?? {
sourceNodeId: exposure.sourceNodeId,
sourceNodeId: toNodeId(exposure.sourceNodeId),
sourcePreviewName: exposure.sourcePreviewName
}
const leafHostLocator =

View File

@@ -7,6 +7,8 @@ import { defineComponent, nextTick, ref } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { api } from '@/scripts/api'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import { usePainter } from './usePainter'
@@ -94,7 +96,10 @@ function makeWidget(name: string, value: unknown = null): IBaseWidget {
/**
* Mounts a thin wrapper component so Vue lifecycle hooks fire.
*/
function mountPainter(nodeId = 'test-node', initialModelValue = '') {
function mountPainter(
nodeId: NodeId = toNodeId('test-node'),
initialModelValue = ''
) {
let painter!: PainterResult
const canvasEl = ref<HTMLCanvasElement | null>(null)
const cursorEl = ref<HTMLElement | null>(null)
@@ -353,7 +358,7 @@ describe('usePainter', () => {
const maskWidget = makeWidget('mask', '')
mockWidgets.push(maskWidget)
mountPainter('test-node', 'painter/existing.png [temp]')
mountPainter(toNodeId('test-node'), 'painter/existing.png [temp]')
const result = await maskWidget.serializeValue!({} as LGraphNode, 0)
expect(result).toBe('painter/existing.png [temp]')
@@ -375,7 +380,7 @@ describe('usePainter', () => {
toBlob: (cb: BlobCallback) => cb(new Blob(['x']))
} as unknown as HTMLCanvasElement
const { canvasEl } = mountPainter('test-node', '')
const { canvasEl } = mountPainter(toNodeId('test-node'), '')
canvasEl.value = fakeCanvas
await nextTick()
@@ -408,7 +413,7 @@ describe('usePainter', () => {
toBlob: (cb: BlobCallback) => cb(new Blob(['x']))
} as unknown as HTMLCanvasElement
const { canvasEl } = mountPainter('test-node', '')
const { canvasEl } = mountPainter(toNodeId('test-node'), '')
canvasEl.value = fakeCanvas
await nextTick()
@@ -434,7 +439,7 @@ describe('usePainter', () => {
toBlob: (cb: BlobCallback) => cb(new Blob(['x']))
} as unknown as HTMLCanvasElement
const { canvasEl } = mountPainter('test-node', '')
const { canvasEl } = mountPainter(toNodeId('test-node'), '')
canvasEl.value = fakeCanvas
await nextTick()
@@ -447,7 +452,7 @@ describe('usePainter', () => {
const maskWidget = makeWidget('mask', '')
mockWidgets.push(maskWidget)
mountPainter('test-node', 'painter/cached.png [temp]')
mountPainter(toNodeId('test-node'), 'painter/cached.png [temp]')
const result = await maskWidget.serializeValue!({} as LGraphNode, 0)
expect(result).toBe('painter/cached.png [temp]')
@@ -466,7 +471,7 @@ describe('usePainter', () => {
} as unknown as HTMLCanvasElement
const { painter, canvasEl, modelValue } = mountPainter(
'test-node',
toNodeId('test-node'),
'painter/old-upload.png [temp]'
)
canvasEl.value = fakeCanvas
@@ -481,7 +486,7 @@ describe('usePainter', () => {
it('calls api.apiURL with parsed filename params when modelValue is set', () => {
vi.mocked(api.apiURL).mockClear()
mountPainter('test-node', 'painter/my-image.png [temp]')
mountPainter(toNodeId('test-node'), 'painter/my-image.png [temp]')
expect(api.apiURL).toHaveBeenCalledWith(
expect.stringContaining('filename=my-image.png')

View File

@@ -17,6 +17,7 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
import type { NodeId } from '@/types/nodeId'
type PainterTool = 'brush' | 'eraser'
@@ -31,7 +32,7 @@ interface UsePainterOptions {
modelValue: Ref<string>
}
export function usePainter(nodeId: string, options: UsePainterOptions) {
export function usePainter(nodeId: NodeId, options: UsePainterOptions) {
const { canvasEl, cursorEl, modelValue } = options
const { t } = useI18n()
const nodeOutputStore = useNodeOutputStore()

View File

@@ -9,7 +9,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import WidgetImageCrop from '@/components/imagecrop/WidgetImageCrop.vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import {
createMockLGraphNode,
@@ -83,7 +84,7 @@ const ImageCropHarness = defineComponent({
modelValue,
imageEl,
containerEl,
...useImageCrop(props.nodeId as NodeId, {
...useImageCrop(toNodeId(props.nodeId), {
imageEl,
containerEl,
modelValue
@@ -183,7 +184,7 @@ function setupImageLayout(vm: CropVm, nw: number, nh: number) {
const harnessCleanups: Array<() => void> = []
async function mountHarness(nodeId: NodeId = 2 as NodeId) {
async function mountHarness(nodeId: NodeId = toNodeId(2)) {
const el = document.createElement('div')
document.body.appendChild(el)
const app = createApp(ImageCropHarness, { nodeId: Number(nodeId) })
@@ -657,7 +658,7 @@ describe('WidgetImageCrop', () => {
container: attach,
props: {
widget,
nodeId: 2 as NodeId,
nodeId: toNodeId(2),
modelValue: { x: 0, y: 0, width: 100, height: 100 }
},
global: {
@@ -689,7 +690,7 @@ describe('WidgetImageCrop', () => {
container: attach,
props: {
widget,
nodeId: 2 as NodeId,
nodeId: toNodeId(2),
modelValue: { x: 0, y: 0, width: 200, height: 200 }
},
global: {
@@ -733,7 +734,7 @@ describe('WidgetImageCrop', () => {
container: attach,
props: {
widget,
nodeId: 2 as NodeId,
nodeId: toNodeId(2),
modelValue: { x: 0, y: 0, width: 200, height: 200 }
},
global: {
@@ -779,7 +780,7 @@ describe('WidgetImageCrop', () => {
container: attach,
props: {
widget,
nodeId: 2 as NodeId,
nodeId: toNodeId(2),
modelValue: { x: 0, y: 0, width: 100, height: 100 }
},
global: {

View File

@@ -2,9 +2,10 @@ import { useResizeObserver } from '@vueuse/core'
import type { Ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { Bounds } from '@/renderer/core/layout/types'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
import type { NodeId } from '@/types/nodeId'
import { resolveNode } from '@/utils/litegraphUtil'
export type ResizeDirection =

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import { nodeId } from '@/types/nodeId'
import type { WidgetState } from '@/types/widgetState'
import { boundsExtractor, singleValueExtractor } from './useUpstreamValue'
@@ -10,7 +10,7 @@ function widget(name: string, value: unknown): WidgetState {
name,
type: 'INPUT',
value,
nodeId: '1' as NodeId,
nodeId: nodeId(1),
options: {},
y: 0
}

View File

@@ -8,12 +8,17 @@ export function useWorkflowStatusDismissal() {
const executionStore = useExecutionStore()
watch(
() => workflowStore.activeWorkflow,
(workflow) => {
if (
workflow &&
executionStore.getWorkflowStatus(workflow) !== 'running'
) {
() => {
const workflow = workflowStore.activeWorkflow
return {
workflow,
status: workflow
? executionStore.getWorkflowStatus(workflow)
: undefined
}
},
({ workflow, status }) => {
if (workflow && status && status !== 'running') {
executionStore.clearWorkflowStatus(workflow)
}
},

View File

@@ -17,7 +17,7 @@ import type {
} from '@/core/schemas/proxyWidgetQuarantineSchema'
import { parseProxyWidgetErrorQuarantine } from '@/core/schemas/proxyWidgetQuarantineSchema'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { nextUniqueName } from '@/lib/litegraph/src/strings'
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
@@ -29,32 +29,34 @@ import type {
import { isWidgetValue } from '@/lib/litegraph/src/types/widgets'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
interface LegacyProxyEntrySource extends PromotedWidgetSource {
disambiguatingSourceNodeId?: string
disambiguatingSourceNodeId?: NodeId
}
const LEGACY_PROXY_WIDGET_PREFIX_PATTERN = /^\s*(\d+)\s*:\s*(.+)$/
interface StrippedPrefix {
sourceWidgetName: string
deepestPrefixId?: string
deepestPrefixId?: NodeId
}
function stripLegacyPrefixes(sourceWidgetName: string): StrippedPrefix {
let remaining = sourceWidgetName
let deepestPrefixId: string | undefined
let deepestPrefixId: NodeId | undefined
while (true) {
const match = LEGACY_PROXY_WIDGET_PREFIX_PATTERN.exec(remaining)
if (!match) return { sourceWidgetName: remaining, deepestPrefixId }
deepestPrefixId = match[1]
deepestPrefixId = toNodeId(match[1])
remaining = match[2]
}
}
function canResolveLegacyProxy(
hostNode: SubgraphNode,
sourceNodeId: string,
sourceNodeId: SerializedNodeId,
widgetName: string
): boolean {
return (
@@ -65,24 +67,32 @@ function canResolveLegacyProxy(
export function normalizeLegacyProxyWidgetEntry(
hostNode: SubgraphNode,
sourceNodeId: string,
sourceNodeId: SerializedNodeId,
sourceWidgetName: string,
disambiguatingSourceNodeId?: string
disambiguatingSourceNodeId?: SerializedNodeId
): LegacyProxyEntrySource {
const normalizedSourceNodeId = toNodeId(sourceNodeId)
const normalizedDisambiguatingSourceNodeId =
disambiguatingSourceNodeId === undefined
? undefined
: toNodeId(disambiguatingSourceNodeId)
if (canResolveLegacyProxy(hostNode, sourceNodeId, sourceWidgetName)) {
return {
sourceNodeId,
sourceNodeId: normalizedSourceNodeId,
sourceWidgetName,
...(disambiguatingSourceNodeId && { disambiguatingSourceNodeId })
...(normalizedDisambiguatingSourceNodeId && {
disambiguatingSourceNodeId: normalizedDisambiguatingSourceNodeId
})
}
}
const stripped = stripLegacyPrefixes(sourceWidgetName)
const patchDisambiguatingSourceNodeId =
stripped.deepestPrefixId ?? disambiguatingSourceNodeId
stripped.deepestPrefixId ?? normalizedDisambiguatingSourceNodeId
return {
sourceNodeId,
sourceNodeId: normalizedSourceNodeId,
sourceWidgetName: stripped.sourceWidgetName,
...(patchDisambiguatingSourceNodeId && {
disambiguatingSourceNodeId: patchDisambiguatingSourceNodeId
@@ -142,6 +152,7 @@ type Plan =
| { kind: 'quarantine'; reason: ProxyWidgetQuarantineReason }
interface PendingEntry {
originalEntry: SerializedProxyWidgetTuple
normalized: LegacyProxyEntrySource
hostValue: TWidgetValue | undefined
isHole: boolean
@@ -159,23 +170,27 @@ export function flushProxyWidgetMigration(args: FlushArgs): void {
const tuples = parseProxyWidgets(hostNode.properties.proxyWidgets)
if (tuples.length === 0) return
const cohort: LegacyProxyEntrySource[] = tuples.map(
([sourceNodeId, sourceWidgetName, disambiguator]) =>
normalizeLegacyProxyWidgetEntry(
const normalizedEntries = tuples.map((originalEntry) => {
const [sourceNodeId, sourceWidgetName, disambiguator] = originalEntry
return {
originalEntry,
normalized: normalizeLegacyProxyWidgetEntry(
hostNode,
sourceNodeId,
sourceWidgetName,
disambiguator
)
)
}
})
const cohort = normalizedEntries.map((entry) => entry.normalized)
const pending: PendingEntry[] = cohort.map((normalized, index) => {
const pending: PendingEntry[] = normalizedEntries.map((entry, index) => {
const { value, isHole } = pickHostValue(hostWidgetValues, index)
return {
normalized,
...entry,
hostValue: value,
isHole,
plan: classify(hostNode, normalized, cohort)
plan: classify(hostNode, entry.normalized, cohort)
}
})
@@ -261,6 +276,7 @@ function collectTargetsStrict(
for (const linkId of linkIds) {
const link = subgraph.links.get(linkId)
if (!link) return undefined
if (link.target_id === -1) return undefined
targets.push({
targetNodeId: link.target_id,
targetSlot: link.target_slot
@@ -277,15 +293,20 @@ function collectTargetsSkippingDangling(
const linkIds = primitiveNode.outputs?.[0]?.links ?? []
return linkIds.flatMap((linkId) => {
const link = subgraph.links.get(linkId)
return link
? [{ targetNodeId: link.target_id, targetSlot: link.target_slot }]
return link && link.target_id !== -1
? [
{
targetNodeId: link.target_id,
targetSlot: link.target_slot
}
]
: []
})
}
function cohortDuplicatesPrimitive(
cohort: readonly LegacyProxyEntrySource[],
primitiveNodeId: string
primitiveNodeId: NodeId
): boolean {
return (
cohort.filter((entry) => entry.sourceNodeId === primitiveNodeId).length >= 2
@@ -600,7 +621,13 @@ function repairPrimitive(
const baseName = userRenamedTitle(primitiveNode) ?? validated.sourceWidgetName
const snapshot: SnapshotLink[] = (primitiveOutput.links ?? [])
.map((id) => subgraph.links.get(id))
.filter((l): l is NonNullable<typeof l> => l !== undefined)
.filter(
(
l
): l is NonNullable<typeof l> & {
target_id: NodeId
} => l !== undefined && l.target_id !== -1
)
.map((l) => ({
primitiveSlot: l.origin_slot,
targetNodeId: l.target_id,
@@ -722,13 +749,8 @@ function quarantineFor(
entry: PendingEntry,
reason: ProxyWidgetQuarantineReason
): ProxyWidgetErrorQuarantineEntry {
const { sourceNodeId, sourceWidgetName, disambiguatingSourceNodeId } =
entry.normalized
const originalEntry: SerializedProxyWidgetTuple = disambiguatingSourceNodeId
? [sourceNodeId, sourceWidgetName, disambiguatingSourceNodeId]
: [sourceNodeId, sourceWidgetName]
return makeQuarantineEntry({
originalEntry,
originalEntry: entry.originalEntry,
reason,
hostValue: entry.isHole ? undefined : entry.hostValue
})

View File

@@ -1,6 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { PreviewExposure } from '@/core/schemas/previewExposureSchema'
import { nodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import type { UUID } from '@/utils/uuid'
import type { PreviewExposureChainContext } from './previewExposureChain'
@@ -13,7 +15,7 @@ interface FixtureExposure extends PreviewExposure {}
interface NestedHostMapping {
fromHostLocator: string
fromSourceNodeId: string
fromSourceNodeId: NodeId
toRootGraphId: UUID
toHostLocator: string
}
@@ -66,7 +68,7 @@ describe(resolvePreviewExposureChain, () => {
[
{
name: 'preview',
sourceNodeId: '42',
sourceNodeId: nodeId('42'),
sourcePreviewName: '$$canvas-image-preview'
}
]
@@ -88,14 +90,14 @@ describe(resolvePreviewExposureChain, () => {
hostNodeLocator: 'host-a',
exposure: {
name: 'preview',
sourceNodeId: '42',
sourceNodeId: nodeId('42'),
sourcePreviewName: '$$canvas-image-preview'
}
}
],
leaf: {
rootGraphId: rootGraphA,
sourceNodeId: '42',
sourceNodeId: nodeId('42'),
sourcePreviewName: '$$canvas-image-preview'
}
})
@@ -108,7 +110,7 @@ describe(resolvePreviewExposureChain, () => {
[
{
name: 'outer-preview',
sourceNodeId: '99',
sourceNodeId: nodeId('99'),
sourcePreviewName: 'inner-preview'
}
]
@@ -118,7 +120,7 @@ describe(resolvePreviewExposureChain, () => {
[
{
name: 'inner-preview',
sourceNodeId: 'leaf-node',
sourceNodeId: nodeId('leaf-node'),
sourcePreviewName: '$$canvas-image-preview'
}
]
@@ -127,7 +129,7 @@ describe(resolvePreviewExposureChain, () => {
const ctx = makeContext(exposureMap, [
{
fromHostLocator: 'host-outer',
fromSourceNodeId: '99',
fromSourceNodeId: nodeId('99'),
toRootGraphId: rootGraphA,
toHostLocator: 'host-inner'
}
@@ -145,7 +147,7 @@ describe(resolvePreviewExposureChain, () => {
expect(result?.steps[1].hostNodeLocator).toBe('host-inner')
expect(result?.leaf).toEqual({
rootGraphId: rootGraphA,
sourceNodeId: 'leaf-node',
sourceNodeId: nodeId('leaf-node'),
sourcePreviewName: '$$canvas-image-preview'
})
})
@@ -157,7 +159,7 @@ describe(resolvePreviewExposureChain, () => {
[
{
name: 'p1',
sourceNodeId: 'sub-a',
sourceNodeId: nodeId('sub-a'),
sourcePreviewName: 'p2'
}
]
@@ -167,7 +169,7 @@ describe(resolvePreviewExposureChain, () => {
[
{
name: 'p2',
sourceNodeId: 'sub-b',
sourceNodeId: nodeId('sub-b'),
sourcePreviewName: 'p3'
}
]
@@ -177,7 +179,7 @@ describe(resolvePreviewExposureChain, () => {
[
{
name: 'p3',
sourceNodeId: 'leaf',
sourceNodeId: nodeId('leaf'),
sourcePreviewName: '$$canvas-image-preview'
}
]
@@ -186,13 +188,13 @@ describe(resolvePreviewExposureChain, () => {
const ctx = makeContext(exposureMap, [
{
fromHostLocator: 'host-1',
fromSourceNodeId: 'sub-a',
fromSourceNodeId: nodeId('sub-a'),
toRootGraphId: rootGraphA,
toHostLocator: 'host-2'
},
{
fromHostLocator: 'host-2',
fromSourceNodeId: 'sub-b',
fromSourceNodeId: nodeId('sub-b'),
toRootGraphId: rootGraphB,
toHostLocator: 'host-3'
}
@@ -208,7 +210,7 @@ describe(resolvePreviewExposureChain, () => {
])
expect(result?.leaf).toEqual({
rootGraphId: rootGraphB,
sourceNodeId: 'leaf',
sourceNodeId: nodeId('leaf'),
sourcePreviewName: '$$canvas-image-preview'
})
})
@@ -220,7 +222,7 @@ describe(resolvePreviewExposureChain, () => {
[
{
name: 'outer',
sourceNodeId: '99',
sourceNodeId: nodeId('99'),
sourcePreviewName: 'missing-on-inner'
}
]
@@ -230,7 +232,7 @@ describe(resolvePreviewExposureChain, () => {
const ctx = makeContext(exposureMap, [
{
fromHostLocator: 'host-outer',
fromSourceNodeId: '99',
fromSourceNodeId: nodeId('99'),
toRootGraphId: rootGraphA,
toHostLocator: 'host-inner'
}
@@ -246,7 +248,7 @@ describe(resolvePreviewExposureChain, () => {
expect(result?.steps).toHaveLength(1)
expect(result?.leaf).toEqual({
rootGraphId: rootGraphA,
sourceNodeId: '99',
sourceNodeId: nodeId('99'),
sourcePreviewName: 'missing-on-inner'
})
})
@@ -255,13 +257,19 @@ describe(resolvePreviewExposureChain, () => {
const exposureMap = new Map<string, FixtureExposure[]>([
[
`${rootGraphA}|host-a`,
[{ name: 'cyclic', sourceNodeId: 'sub', sourcePreviewName: 'cyclic' }]
[
{
name: 'cyclic',
sourceNodeId: nodeId('sub'),
sourcePreviewName: 'cyclic'
}
]
]
])
const ctx = makeContext(exposureMap, [
{
fromHostLocator: 'host-a',
fromSourceNodeId: 'sub',
fromSourceNodeId: nodeId('sub'),
toRootGraphId: rootGraphA,
toHostLocator: 'host-a'
}
@@ -278,6 +286,6 @@ describe(resolvePreviewExposureChain, () => {
expect.stringContaining('cycle detected')
)
expect(result?.steps).toHaveLength(1)
expect(result?.leaf.sourceNodeId).toBe('sub')
expect(result?.leaf.sourceNodeId).toBe(nodeId('sub'))
})
})

View File

@@ -1,4 +1,5 @@
import type { PreviewExposure } from '@/core/schemas/previewExposureSchema'
import type { NodeId } from '@/types/nodeId'
import type { UUID } from '@/utils/uuid'
interface ResolvedPreviewChainStep {
@@ -11,7 +12,7 @@ export interface ResolvedPreviewChain {
steps: readonly ResolvedPreviewChainStep[]
leaf: {
rootGraphId: UUID
sourceNodeId: string
sourceNodeId: NodeId
sourcePreviewName: string
}
}
@@ -24,12 +25,16 @@ export interface PreviewExposureChainContext {
resolveNestedHost(
rootGraphId: UUID,
hostNodeLocator: string,
sourceNodeId: string
sourceNodeId: NodeId
): { rootGraphId: UUID; hostNodeLocator: string } | undefined
}
const MAX_CHAIN_DEPTH = 32
function exposureSourceNodeId(exposure: PreviewExposure): NodeId {
return exposure.sourceNodeId
}
export function resolvePreviewExposureChain(
rootGraphId: UUID,
hostNodeLocator: string,
@@ -50,7 +55,7 @@ export function resolvePreviewExposureChain(
steps,
leaf: {
rootGraphId: lastStep.rootGraphId,
sourceNodeId: lastStep.exposure.sourceNodeId,
sourceNodeId: exposureSourceNodeId(lastStep.exposure),
sourcePreviewName: lastStep.exposure.sourcePreviewName
}
}
@@ -80,14 +85,14 @@ export function resolvePreviewExposureChain(
const nested = ctx.resolveNestedHost(
currentRootGraphId,
currentHost,
exposure.sourceNodeId
exposureSourceNodeId(exposure)
)
if (!nested) {
return {
steps,
leaf: {
rootGraphId: currentRootGraphId,
sourceNodeId: exposure.sourceNodeId,
sourceNodeId: exposureSourceNodeId(exposure),
sourcePreviewName: exposure.sourcePreviewName
}
}

View File

@@ -3,6 +3,7 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { isWidgetValue } from '@/lib/litegraph/src/types/widgets'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import type { NodeId } from '@/types/nodeId'
import { resolveSubgraphInputTarget } from './resolveSubgraphInputTarget'
@@ -13,7 +14,7 @@ import { resolveSubgraphInputTarget } from './resolveSubgraphInputTarget'
* on the projected widget.
*/
export interface PromotedSource {
nodeId: string
nodeId: NodeId
widgetName: string
}
@@ -109,7 +110,6 @@ export function promotedInputWidget(input: INodeInputSlot): IBaseWidget | null {
}
}
/** Every promoted subgraph input on a node, projected to ordinary widgets. */
export function promotedInputWidgets(node: LGraphNode): IBaseWidget[] {
return node.inputs.flatMap((input) => {
const widget = promotedInputWidget(input)

View File

@@ -1,5 +1,6 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type { NodeId } from '@/types/nodeId'
export interface ResolvedPromotedWidget {
node: LGraphNode
@@ -12,6 +13,6 @@ export interface ResolvedPromotedWidget {
* the source is a stored tuple rather than something link-derivable.
*/
export interface PromotedWidgetSource {
sourceNodeId: string
sourceNodeId: NodeId
sourceWidgetName: string
}

View File

@@ -18,6 +18,8 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLitegraphService } from '@/services/litegraphService'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { SerializedNodeId } from '@/types/nodeId'
import type { WidgetId } from '@/types/widgetId'
import { widgetId } from '@/types/widgetId'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
@@ -33,7 +35,7 @@ export function getWidgetName(w: IBaseWidget): string {
export function isLinkedPromotion(
subgraphNode: SubgraphNode,
sourceNodeId: string,
sourceNodeId: SerializedNodeId,
sourceWidgetName: string
): boolean {
return (
@@ -44,9 +46,10 @@ export function isLinkedPromotion(
export function findHostInputForPromotion(
subgraphNode: SubgraphNode,
sourceNodeId: string,
rawSourceNodeId: SerializedNodeId,
sourceWidgetName: string
) {
const sourceNodeId = toNodeId(rawSourceNodeId)
return subgraphNode.inputs.find((input) => {
const source = input._subgraphSlot
? resolvePromotionSource(subgraphNode, input._subgraphSlot)
@@ -74,7 +77,7 @@ function resolvePromotionSource(
if (inputNode.isSubgraphNode()) {
return {
sourceNodeId: String(inputNode.id),
sourceNodeId: toNodeId(inputNode.id),
sourceWidgetName: targetInput.name
}
}
@@ -83,7 +86,7 @@ function resolvePromotionSource(
if (!targetWidget) continue
return {
sourceNodeId: String(inputNode.id),
sourceNodeId: toNodeId(inputNode.id),
sourceWidgetName: targetWidget.name
}
}
@@ -212,7 +215,7 @@ function toPromotionSource(
widget: IBaseWidget
): PromotedWidgetSource {
return {
sourceNodeId: String(node.id),
sourceNodeId: toNodeId(node.id),
sourceWidgetName: getWidgetName(widget)
}
}
@@ -235,9 +238,7 @@ export function promoteValueWidgetViaSubgraphInput(
sourceWidget: IBaseWidget
): CanonicalPromotionResult {
const sourceWidgetName = getWidgetName(sourceWidget)
if (
isLinkedPromotion(subgraphNode, String(sourceNode.id), sourceWidgetName)
) {
if (isLinkedPromotion(subgraphNode, sourceNode.id, sourceWidgetName)) {
return { ok: true }
}
@@ -315,7 +316,7 @@ function promotePreviewViaExposure(
if (existing) return
store.addExposure(rootGraphId, hostLocator, {
sourceNodeId: String(sourceNode.id),
sourceNodeId: sourceNode.id,
sourcePreviewName
})
}
@@ -603,7 +604,7 @@ export function pruneDisconnected(subgraphNode: SubgraphNode) {
if (!hostInput?.widgetId && !hostInput?._widget) return false
removedEntries.push({
sourceNodeId: String(subgraphNode.id),
sourceNodeId: toNodeId(subgraphNode.id),
sourceWidgetName: input.name
})
return true
@@ -643,7 +644,7 @@ export function hasUnpromotedWidgets(subgraphNode: SubgraphNode): boolean {
!isWidgetPromotedOnSubgraphNode(
subgraphNode,
{
sourceNodeId: String(interiorNode.id),
sourceNodeId: toNodeId(interiorNode.id),
sourceWidgetName: widget.name
},
widget

View File

@@ -2,6 +2,8 @@ import type { ResolvedPromotedWidget } from '@/core/graph/subgraph/promotedWidge
import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
type PromotedWidgetResolutionFailure =
| 'invalid-host'
@@ -18,7 +20,7 @@ const MAX_PROMOTED_WIDGET_CHAIN_DEPTH = 100
function traversePromotedWidgetChain(
hostNode: SubgraphNode,
nodeId: string,
nodeId: NodeId,
widgetName: string
): PromotedWidgetResolutionResult {
const visitedByHost = new WeakMap<SubgraphNode, Set<string>>()
@@ -69,11 +71,12 @@ function traversePromotedWidgetChain(
export function resolveConcretePromotedWidget(
hostNode: LGraphNode,
nodeId: string,
rawNodeId: SerializedNodeId,
widgetName: string
): PromotedWidgetResolutionResult {
if (!hostNode.isSubgraphNode()) {
return { status: 'failure', failure: 'invalid-host' }
}
const nodeId = toNodeId(rawNodeId)
return traversePromotedWidgetChain(hostNode, nodeId, widgetName)
}

View File

@@ -10,6 +10,7 @@ import {
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { nodeId } from '@/types/nodeId'
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: () => ({})
@@ -139,7 +140,7 @@ describe('resolveSubgraphInputTarget', () => {
(slot) => slot.name === 'seed'
)!
const node = new LGraphNode('Interior-seed')
node.id = 42
node.id = nodeId(42)
const input = node.addInput('seed_input', '*')
node.addWidget('number', 'seed', 0, () => undefined)
input.widget = { name: 'seed' }
@@ -224,7 +225,7 @@ describe('resolveSubgraphInputTarget', () => {
inputs: [{ name: 'seed', type: '*' }]
})
const concreteNode = new LGraphNode('ConcreteNode')
concreteNode.id = 900
concreteNode.id = nodeId(900)
const concreteInput = concreteNode.addInput('seed_input', '*')
concreteNode.addWidget('number', 'seed', 0, () => undefined)
concreteInput.widget = { name: 'seed' }

View File

@@ -1,9 +1,11 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import { resolveSubgraphInputLink } from './resolveSubgraphInputLink'
type ResolvedSubgraphInputTarget = {
nodeId: string
nodeId: NodeId
widgetName: string
}
@@ -17,7 +19,7 @@ export function resolveSubgraphInputTarget(
({ inputNode, targetInput, getTargetWidget }) => {
if (inputNode.isSubgraphNode()) {
return {
nodeId: String(inputNode.id),
nodeId: toNodeId(inputNode.id),
widgetName: targetInput.name
}
}
@@ -26,7 +28,7 @@ export function resolveSubgraphInputTarget(
if (!targetWidget) return undefined
return {
nodeId: String(inputNode.id),
nodeId: toNodeId(inputNode.id),
widgetName: targetWidget.name
}
}

View File

@@ -10,7 +10,7 @@ import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
*/
export function parseNodePropertyArray<T>(
property: NodeProperty | undefined,
schema: z.ZodType<T[]>,
schema: z.ZodType<T[], z.ZodTypeDef, unknown>,
contextName: string
): T[] {
if (property === undefined) return []

View File

@@ -1,12 +1,13 @@
import { z } from 'zod'
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
import { nodeId } from '@/types/nodeId'
import { parseNodePropertyArray } from './parseNodePropertyArray'
const previewExposureSchema = z.object({
name: z.string(),
sourceNodeId: z.string(),
sourceNodeId: z.string().transform(nodeId),
sourcePreviewName: z.string()
})
export type PreviewExposure = z.infer<typeof previewExposureSchema>
@@ -16,7 +17,7 @@ const previewExposuresPropertySchema = z.array(previewExposureSchema)
export function parsePreviewExposures(
property: NodeProperty | undefined
): PreviewExposure[] {
return parseNodePropertyArray(
return parseNodePropertyArray<PreviewExposure>(
property,
previewExposuresPropertySchema,
'properties.previewExposures'

View File

@@ -26,6 +26,7 @@ vi.mock('@/scripts/app', () => ({
}))
vi.mock('@/extensions/core/load3d', () => ({}))
vi.mock('@/extensions/core/load3dAdvanced', () => ({}))
vi.mock('@/extensions/core/load3dPreviewExtensions', () => ({}))
vi.mock('@/extensions/core/saveMesh', () => ({}))

View File

@@ -8,6 +8,7 @@ import type {
} from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { nodeId } from '@/types/nodeId'
import { createMockLLink } from '@/utils/__tests__/litegraphTestUtils'
vi.mock('@/scripts/app', () => ({
@@ -39,7 +40,7 @@ function createTargetNode(
id = 7
): Pick<LGraphNode, 'id' | 'inputs' | 'widgets'> {
return fromPartial<Pick<LGraphNode, 'id' | 'inputs' | 'widgets'>>({
id,
id: nodeId(id),
inputs: [
fromPartial<INodeInputSlot>({
widget: { name: widget.name }

View File

@@ -2,7 +2,7 @@ import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { NodeId, Subgraph } from '@/lib/litegraph/src/litegraph'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import {
LGraph,
LGraphGroup,
@@ -17,6 +17,7 @@ import type { UUID } from '@/utils/uuid'
import { zeroUuid } from '@/utils/uuid'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { nodeId } from '@/types/nodeId'
import { widgetId } from '@/types/widgetId'
import {
createTestSubgraph,
@@ -298,7 +299,7 @@ describe('Graph Clearing and Callbacks', () => {
})
const widgetValueStore = useWidgetValueStore()
const seedWidgetId = widgetId(graphId, '10' as NodeId, 'seed')
const seedWidgetId = widgetId(graphId, '10', 'seed')
widgetValueStore.registerWidget(seedWidgetId, {
type: 'number',
value: 1,
@@ -621,7 +622,7 @@ describe('ensureGlobalIdUniqueness', () => {
expect(subNode.id).not.toBe(rootNode.id)
expect(subgraph._nodes_by_id[subNode.id]).toBe(subNode)
expect(subgraph._nodes_by_id[rootNode.id as number]).toBeUndefined()
expect(subgraph._nodes_by_id[rootNode.id]).toBeUndefined()
})
it('preserves root graph node IDs as canonical', () => {
@@ -657,7 +658,7 @@ describe('ensureGlobalIdUniqueness', () => {
rootGraph.ensureGlobalIdUniqueness()
expect(rootGraph.state.lastNodeId).toBeGreaterThanOrEqual(
subNode.id as number
Number(subNode.id)
)
})
@@ -674,7 +675,7 @@ describe('ensureGlobalIdUniqueness', () => {
subgraph._nodes_by_id[subNodeA.id] = subNodeA
const subNodeB = new DummyNode()
subNodeB.id = 999
subNodeB.id = nodeId(999)
subgraph._nodes.push(subNodeB)
subgraph._nodes_by_id[subNodeB.id] = subNodeB
@@ -693,7 +694,7 @@ describe('ensureGlobalIdUniqueness', () => {
const subgraph = createSubgraphOnGraph(rootGraph)
const subNode = new DummyNode()
subNode.id = 42
subNode.id = nodeId(42)
subgraph._nodes.push(subNode)
subgraph._nodes_by_id[subNode.id] = subNode
@@ -1005,7 +1006,7 @@ describe('Subgraph Unpacking', () => {
const firstInstance = createTestSubgraphNode(subgraph, { pos: [100, 100] })
const secondInstance = createTestSubgraphNode(subgraph, { pos: [300, 100] })
secondInstance.id = 2
secondInstance.id = nodeId(2)
rootGraph.add(firstInstance)
rootGraph.add(secondInstance)
@@ -1053,7 +1054,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const idsB = nodeIdSet(graph, SUBGRAPH_B)
for (const id of SHARED_NODE_IDS) {
expect(idsA.has(id as NodeId)).toBe(true)
expect(idsA.has(nodeId(id))).toBe(true)
}
for (const id of idsA) {
expect(idsB.has(id)).toBe(false)
@@ -1065,8 +1066,8 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const idsB = nodeIdSet(graph, SUBGRAPH_B)
for (const link of graph.subgraphs.get(SUBGRAPH_B)!.links.values()) {
expect(idsB.has(link.origin_id)).toBe(true)
expect(idsB.has(link.target_id)).toBe(true)
if (link.origin_id !== -1) expect(idsB.has(link.origin_id)).toBe(true)
if (link.target_id !== -1) expect(idsB.has(link.target_id)).toBe(true)
}
})
@@ -1075,7 +1076,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const idsB = nodeIdSet(graph, SUBGRAPH_B)
for (const widget of graph.subgraphs.get(SUBGRAPH_B)!.widgets) {
expect(idsB.has(widget.id)).toBe(true)
expect(idsB.has(nodeId(widget.id))).toBe(true)
}
})
@@ -1089,14 +1090,14 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
graph.subgraphs.get(SUBGRAPH_B)!.nodes.map((n) => String(n.id))
)
const pw102 = graph.getNodeById(102 as NodeId)?.properties?.proxyWidgets
const pw102 = graph.getNodeById(102)?.properties?.proxyWidgets
expect(Array.isArray(pw102)).toBe(true)
for (const entry of pw102 as unknown[][]) {
expect(Array.isArray(entry)).toBe(true)
expect(idsA.has(String(entry[0]))).toBe(true)
}
const pw103 = graph.getNodeById(103 as NodeId)?.properties?.proxyWidgets
const pw103 = graph.getNodeById(103)?.properties?.proxyWidgets
expect(Array.isArray(pw103)).toBe(true)
for (const entry of pw103 as unknown[][]) {
expect(Array.isArray(entry)).toBe(true)
@@ -1114,7 +1115,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const innerNode = graph.subgraphs
.get(SUBGRAPH_A)!
.nodes.find((n) => n.id === (50 as NodeId))
.nodes.find((n) => n.id === nodeId(50))
const pw = innerNode?.properties?.proxyWidgets
expect(Array.isArray(pw)).toBe(true)
for (const entry of pw as unknown[][]) {
@@ -1154,7 +1155,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
expect(migrationCall).toBeDefined()
expect(migrationCall![1]).toEqual(
expect.objectContaining({
hostNodeId: expect.any(Number),
hostNodeId: expect.any(String),
proxyWidgets: expect.anything()
})
)
@@ -1176,8 +1177,12 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const graph = new LGraph()
graph.configure(structuredClone(uniqueSubgraphNodeIds))
expect(nodeIdSet(graph, SUBGRAPH_A)).toEqual(new Set([10, 11, 12]))
expect(nodeIdSet(graph, SUBGRAPH_B)).toEqual(new Set([20, 21, 22]))
expect(nodeIdSet(graph, SUBGRAPH_A)).toEqual(
new Set([nodeId(10), nodeId(11), nodeId(12)])
)
expect(nodeIdSet(graph, SUBGRAPH_B)).toEqual(
new Set([nodeId(20), nodeId(21), nodeId(22)])
)
})
})

View File

@@ -11,6 +11,8 @@ import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMuta
import { LayoutSource } from '@/renderer/core/layout/types'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { UNASSIGNED_NODE_ID, nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
import { forEachNode } from '@/utils/graphTraversalUtil'
import {
@@ -25,9 +27,8 @@ import { LGraphCanvas } from './LGraphCanvas'
import { LGraphGroup } from './LGraphGroup'
import type { GroupId } from './LGraphGroup'
import { LGraphNode } from './LGraphNode'
import type { NodeId } from './LGraphNode'
import { LLink } from './LLink'
import type { LinkId } from './LLink'
import type { LinkEndpointNodeId, LinkId } from './LLink'
import { MapProxyHandler } from './MapProxyHandler'
import { Reroute } from './Reroute'
import type { RerouteId } from './Reroute'
@@ -103,6 +104,22 @@ function isLGraphTriggerAction(action: string): action is LGraphTriggerAction {
return validTriggerActions.has(action as LGraphTriggerAction)
}
function nextNodeId(state: LGraphState): NodeId {
return toNodeId(++state.lastNodeId)
}
function numericNodeId(id: NodeId): number | null {
const numericId = Number(id)
return Number.isInteger(numericId) ? numericId : null
}
function syncLastNodeId(state: LGraphState, id: NodeId): void {
const numericId = numericNodeId(id)
if (numericId !== null && state.lastNodeId < numericId) {
state.lastNodeId = numericId
}
}
export type RendererType = 'LG' | 'Vue' | 'Vue-corrected'
/**
@@ -638,8 +655,8 @@ export class LGraph
const S: LGraphNode[] = []
const M: Dictionary<LGraphNode> = {}
// to avoid repeating links
const visited_links: Record<NodeId, boolean> = {}
const remaining_links: Record<NodeId, number> = {}
const visited_links: Record<SerializedNodeId, boolean> = {}
const remaining_links: Record<SerializedNodeId, number> = {}
// search for the nodes without inputs (starting nodes)
for (const node of this._nodes) {
@@ -955,11 +972,11 @@ export class LGraph
}
// nodes
if (node.id != -1 && this._nodes_by_id[node.id] != null) {
if (node.id !== UNASSIGNED_NODE_ID && this._nodes_by_id[node.id] != null) {
console.warn(
'LiteGraph: there is already a node with this ID, changing it'
)
node.id = ++state.lastNodeId
node.id = nextNodeId(state)
}
if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
@@ -967,10 +984,10 @@ export class LGraph
}
// give him an id
if (node.id == null || node.id == -1) {
node.id = ++state.lastNodeId
} else if (typeof node.id === 'number' && state.lastNodeId < node.id) {
state.lastNodeId = node.id
if (node.id == null || node.id === UNASSIGNED_NODE_ID) {
node.id = nextNodeId(state)
} else {
syncLastNodeId(state, node.id)
}
// Set ghost flag before registration so VueNodeData picks it up
@@ -1133,8 +1150,8 @@ export class LGraph
/**
* Returns a node by its id.
*/
getNodeById(id: NodeId | null | undefined): LGraphNode | null {
return id != null ? this._nodes_by_id[id] : null
getNodeById(id: SerializedNodeId | null | undefined): LGraphNode | null {
return id != null ? this._nodes_by_id[toNodeId(id)] : null
}
/**
@@ -1967,9 +1984,10 @@ export class LGraph
}
}
nodeIdMap.set(n_info.id, ++this.last_node_id)
node.id = this.last_node_id
n_info.id = this.last_node_id
const newNodeId = nextNodeId(this.state)
nodeIdMap.set(toNodeId(n_info.id), newNodeId)
node.id = newNodeId
n_info.id = newNodeId
// Strip links from serialized data before configure to prevent
// onConnectionsChange from resolving subgraph-internal link IDs
@@ -2023,9 +2041,9 @@ export class LGraph
}
}
const newLinks: {
oid: NodeId
oid: LinkEndpointNodeId
oslot: number
tid: NodeId
tid: LinkEndpointNodeId
tslot: number
id: LinkId
iparent?: RerouteId
@@ -2045,7 +2063,8 @@ export class LGraph
link.origin_slot = outerLink.origin_slot
externalParentId = outerLink.parentId
} else {
const origin_id = nodeIdMap.get(link.origin_id)
const origin_id =
link.origin_id === -1 ? undefined : nodeIdMap.get(link.origin_id)
if (!origin_id) {
console.error('Missing Link ID when unpacking')
continue
@@ -2070,7 +2089,8 @@ export class LGraph
}
continue
} else {
const target_id = nodeIdMap.get(link.target_id)
const target_id =
link.target_id === -1 ? undefined : nodeIdMap.get(link.target_id)
if (!target_id) {
console.error('Missing Link ID when unpacking')
continue
@@ -2108,7 +2128,9 @@ export class LGraph
console.error('Ignoring link to subgraph outside subgraph')
continue
}
const tnode = this._nodes_by_id[newLink.tid]
if (newLink.tid === -1) continue
const tnode = this.getNodeById(newLink.tid)
if (!tnode) continue
created = this.inputNode.slots[newLink.oslot].connect(
tnode.inputs[newLink.tslot],
tnode
@@ -2118,17 +2140,19 @@ export class LGraph
console.error('Ignoring link to subgraph outside subgraph')
continue
}
const tnode = this._nodes_by_id[newLink.oid]
if (newLink.oid === -1) continue
const tnode = this.getNodeById(newLink.oid)
if (!tnode) continue
created = this.outputNode.slots[newLink.tslot].connect(
tnode.outputs[newLink.oslot],
tnode
)
} else {
created = this._nodes_by_id[newLink.oid].connect(
newLink.oslot,
this._nodes_by_id[newLink.tid],
newLink.tslot
)
if (newLink.oid === -1 || newLink.tid === -1) continue
const originNode = this.getNodeById(newLink.oid)
const targetNode = this.getNodeById(newLink.tid)
if (!originNode || !targetNode) continue
created = originNode.connect(newLink.oslot, targetNode, newLink.tslot)
}
if (!created) {
console.error('Failed to create link')
@@ -2239,7 +2263,7 @@ export class LGraph
* @param nodeIds An ordered list of node IDs, from the root graph to the most nested subgraph node
* @returns An ordered list of nested subgraph nodes.
*/
resolveSubgraphIdPath(nodeIds: readonly NodeId[]): SubgraphNode[] {
resolveSubgraphIdPath(nodeIds: readonly SerializedNodeId[]): SubgraphNode[] {
const result: SubgraphNode[] = []
let currentGraph: GraphOrSubgraph = this.rootGraph
@@ -2504,11 +2528,13 @@ export class LGraph
if (subgraphs) {
const reservedNodeIds = new Set<number>()
for (const node of this._nodes) {
if (typeof node.id === 'number') reservedNodeIds.add(node.id)
const id = numericNodeId(node.id)
if (id !== null) reservedNodeIds.add(id)
}
for (const sg of this.subgraphs.values()) {
for (const node of sg.nodes) {
if (typeof node.id === 'number') reservedNodeIds.add(node.id)
const id = numericNodeId(node.id)
if (id !== null) reservedNodeIds.add(id)
}
}
for (const n of nodesData ?? []) {
@@ -2538,7 +2564,7 @@ export class LGraph
}
let error = false
const nodeDataMap = new Map<NodeId, ISerialisedNode>()
const nodeDataMap = new Map<SerializedNodeId, ISerialisedNode>()
// create nodes
this._nodes = []
@@ -2559,7 +2585,7 @@ export class LGraph
}
// id it or it will create a new id
node.id = n_info.id
node.id = toNodeId(n_info.id)
// add before configure, otherwise configure cannot create links
this.add(node, true)
nodeDataMap.set(node.id, n_info)
@@ -2681,24 +2707,24 @@ export class LGraph
const remappedIds = new Map<NodeId, NodeId>()
for (const node of graph._nodes) {
if (typeof node.id !== 'number') continue
const currentId = numericNodeId(node.id)
if (currentId === null) continue
if (usedNodeIds.has(node.id)) {
if (usedNodeIds.has(currentId)) {
const oldId = node.id
while (usedNodeIds.has(++state.lastNodeId));
const newId = state.lastNodeId
const newId = toNodeId(state.lastNodeId)
delete graph._nodes_by_id[oldId]
node.id = newId
graph._nodes_by_id[newId] = node
usedNodeIds.add(newId)
usedNodeIds.add(state.lastNodeId)
remappedIds.set(oldId, newId)
console.warn(
`LiteGraph: duplicate node ID ${oldId} reassigned to ${newId} in graph ${graph.id}`
)
} else {
usedNodeIds.add(node.id as number)
if ((node.id as number) > state.lastNodeId)
state.lastNodeId = node.id as number
usedNodeIds.add(currentId)
if (currentId > state.lastNodeId) state.lastNodeId = currentId
}
}
@@ -2891,7 +2917,7 @@ export class Subgraph
private _repairSlotLinkIds(
linkIds: LinkId[],
ioNodeId: number,
ioNodeId: NodeId,
slotIndex: number
): void {
const repaired = linkIds.map((id) =>
@@ -2905,7 +2931,7 @@ export class Subgraph
}
private _findLinkBySlot(
nodeId: number,
nodeId: NodeId,
slotIndex: number
): LLink | undefined {
for (const link of this._links.values()) {
@@ -3109,10 +3135,12 @@ function patchLinkNodeIds(
remappedIds: Map<NodeId, NodeId>
): void {
for (const link of links.values()) {
const newOrigin = remappedIds.get(link.origin_id)
const newOrigin =
link.origin_id === -1 ? undefined : remappedIds.get(link.origin_id)
if (newOrigin !== undefined) link.origin_id = newOrigin
const newTarget = remappedIds.get(link.target_id)
const newTarget =
link.target_id === -1 ? undefined : remappedIds.get(link.target_id)
if (newTarget !== undefined) link.target_id = newTarget
}
}

View File

@@ -14,6 +14,7 @@ import {
createUuidv4
} from '@/lib/litegraph/src/litegraph'
import { remapClipboardSubgraphNodeIds } from '@/lib/litegraph/src/LGraphCanvas'
import { nodeId } from '@/types/nodeId'
import type {
ClipboardItems,
ExportedSubgraph,
@@ -52,7 +53,7 @@ describe('remapClipboardSubgraphNodeIds', () => {
it('remaps pasted subgraph interior IDs and proxyWidgets references', () => {
const rootGraph = new LGraph()
const existingNode = new LGraphNode('existing')
existingNode.id = 1
existingNode.id = nodeId(1)
rootGraph.add(existingNode)
const subgraphId = createUuidv4()
@@ -124,7 +125,7 @@ describe('remapClipboardSubgraphNodeIds', () => {
it('remaps pasted SubgraphNode previewExposures sourceNodeId references', () => {
const rootGraph = new LGraph()
const existingNode = new LGraphNode('existing')
existingNode.id = 1
existingNode.id = nodeId(1)
rootGraph.add(existingNode)
const subgraphId = createUuidv4()

View File

@@ -1,5 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { NodeLayout } from '@/renderer/core/layout/types'
@@ -74,7 +77,7 @@ function createCanvas(graph: LGraph): LGraphCanvas {
}
function createLayoutEntry(node: LGraphNode, zIndex: number) {
const nodeId = String(node.id)
const nodeId = toNodeId(node.id)
const layout: NodeLayout = {
id: nodeId,
position: { x: node.pos[0], y: node.pos[1] },
@@ -99,7 +102,7 @@ function createLayoutEntry(node: LGraphNode, zIndex: number) {
})
}
function setZIndex(nodeId: string, zIndex: number, previousZIndex: number) {
function setZIndex(nodeId: NodeId, zIndex: number, previousZIndex: number) {
layoutStore.applyOperation({
type: 'setNodeZIndex',
entity: 'node',
@@ -145,7 +148,7 @@ describe('cloned node z-index in Vue renderer', () => {
originalNode.size = [200, 100]
graph.add(originalNode)
const originalNodeId = String(originalNode.id)
const originalNodeId = toNodeId(originalNode.id)
setZIndex(originalNodeId, 5, 0)
@@ -158,7 +161,7 @@ describe('cloned node z-index in Vue renderer', () => {
expect(result!.created.length).toBe(1)
const clonedNode = result!.created[0] as LGraphNode
const clonedNodeId = String(clonedNode.id)
const clonedNodeId = toNodeId(clonedNode.id)
// The cloned node should have a z-index higher than the original
const clonedLayout = layoutStore.getNodeLayoutRef(clonedNodeId).value
@@ -171,13 +174,13 @@ describe('cloned node z-index in Vue renderer', () => {
nodeA.pos = [100, 100]
nodeA.size = [200, 100]
graph.add(nodeA)
setZIndex(String(nodeA.id), 3, 0)
setZIndex(toNodeId(nodeA.id), 3, 0)
const nodeB = new TestNode()
nodeB.pos = [400, 100]
nodeB.size = [200, 100]
graph.add(nodeB)
setZIndex(String(nodeB.id), 7, 0)
setZIndex(toNodeId(nodeB.id), 7, 0)
const result = LGraphCanvas.cloneNodes([nodeA, nodeB])
expect(result).toBeDefined()
@@ -185,8 +188,8 @@ describe('cloned node z-index in Vue renderer', () => {
const clonedA = result!.created[0] as LGraphNode
const clonedB = result!.created[1] as LGraphNode
const layoutA = layoutStore.getNodeLayoutRef(String(clonedA.id)).value!
const layoutB = layoutStore.getNodeLayoutRef(String(clonedB.id)).value!
const layoutA = layoutStore.getNodeLayoutRef(toNodeId(clonedA.id)).value!
const layoutB = layoutStore.getNodeLayoutRef(toNodeId(clonedB.id)).value!
// Both cloned nodes should be above the highest original (z-index 7)
expect(layoutA.zIndex).toBeGreaterThan(7)

View File

@@ -1,5 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nodeId as toNodeId } from '@/types/nodeId'
import {
LGraph,
LGraphCanvas,
@@ -107,7 +109,7 @@ describe('LGraphCanvas slot hit detection', () => {
// Mock the slot query to return our node's slot
vi.mocked(layoutStore.querySlotAtPoint).mockReturnValue({
nodeId: String(node.id),
nodeId: toNodeId(node.id),
index: 0,
type: 'output',
position: { x: 252, y: 120 },
@@ -188,7 +190,7 @@ describe('LGraphCanvas slot hit detection', () => {
expect(node.isPointInside(clickX, clickY)).toBe(false)
vi.mocked(layoutStore.querySlotAtPoint).mockReturnValue({
nodeId: String(node.id),
nodeId: toNodeId(node.id),
index: 0,
type: 'input',
position: { x: 98, y: 140 },

View File

@@ -21,7 +21,8 @@ import type { AnimationOptions } from './DragAndScale'
import type { LGraph, SubgraphId } from './LGraph'
import { LGraphGroup } from './LGraphGroup'
import { LGraphNode } from './LGraphNode'
import type { NodeId, NodeProperty } from './LGraphNode'
import type { NodeProperty } from './LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import { LLink } from './LLink'
import type { LinkId } from './LLink'
import { Reroute } from './Reroute'
@@ -213,7 +214,7 @@ interface LGraphCanvasState {
selectionChanged: boolean
/** ID of node currently in ghost placement mode (semi-transparent, following cursor). */
ghostNodeId: NodeId | null
ghostNodeId: SerializedNodeId | null
}
/**
@@ -224,7 +225,7 @@ interface ClipboardPasteResult {
/** All successfully created items */
created: Positionable[]
/** Map: original node IDs to newly created nodes */
nodes: Map<NodeId, LGraphNode>
nodes: Map<SerializedNodeId, LGraphNode>
/** Map: original link IDs to new link IDs */
links: Map<LinkId, LLink>
/** Map: original reroute IDs to newly created reroutes */
@@ -678,7 +679,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* The IDs of the nodes that are currently visible on the canvas. More
* performant than {@link visible_nodes} for visibility checks.
*/
private _visible_node_ids: Set<NodeId> = new Set()
private _visible_node_ids: Set<SerializedNodeId> = new Set()
node_over?: LGraphNode
node_capturing_input?: LGraphNode | null
highlighted_links: Dictionary<boolean> = {}
@@ -691,7 +692,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
dirty_canvas: boolean = true
dirty_bgcanvas: boolean = true
/** A map of nodes that require selective-redraw */
dirty_nodes = new Map<NodeId, LGraphNode>()
dirty_nodes = new Map<SerializedNodeId, LGraphNode>()
dirty_area?: Rect | null
/** @deprecated Unused */
node_in_panel?: LGraphNode | null
@@ -4168,7 +4169,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const results: ClipboardPasteResult = {
created: [],
nodes: new Map<NodeId, LGraphNode>(),
nodes: new Map<SerializedNodeId, LGraphNode>(),
links: new Map<LinkId, LLink>(),
reroutes: new Map<RerouteId, Reroute>(),
subgraphs: new Map<SubgraphId, Subgraph>()
@@ -4311,7 +4312,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const newPositions = created
.filter((item): item is LGraphNode => item instanceof LGraphNode)
.map((node) => ({
nodeId: String(node.id),
nodeId: node.id,
bounds: {
x: node.pos[0],
y: node.pos[1],
@@ -8959,8 +8960,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
function patchLinkNodeIds(
links: { origin_id: NodeId; target_id: NodeId }[] | undefined,
remappedIds: Map<NodeId, NodeId>
links:
| { origin_id: SerializedNodeId; target_id: SerializedNodeId }[]
| undefined,
remappedIds: Map<SerializedNodeId, SerializedNodeId>
) {
if (!links?.length) return
@@ -8975,8 +8978,8 @@ function patchLinkNodeIds(
function remapNodeId(
nodeId: string,
remappedIds: Map<NodeId, NodeId>
): NodeId | undefined {
remappedIds: Map<SerializedNodeId, SerializedNodeId>
): SerializedNodeId | undefined {
const directMatch = remappedIds.get(nodeId)
if (directMatch !== undefined) return directMatch
if (!/^-?\d+$/.test(nodeId)) return undefined
@@ -8989,7 +8992,7 @@ function remapNodeId(
function remapProxyWidgets(
info: ISerialisedNode,
remappedIds: Map<NodeId, NodeId> | undefined
remappedIds: Map<SerializedNodeId, SerializedNodeId> | undefined
) {
if (!remappedIds || remappedIds.size === 0) return
@@ -9020,7 +9023,7 @@ function hasStringSourceNodeId(
function remapPreviewExposures(
info: ISerialisedNode,
remappedIds: Map<NodeId, NodeId> | undefined
remappedIds: Map<SerializedNodeId, SerializedNodeId> | undefined
) {
if (!remappedIds || remappedIds.size === 0) return
@@ -9042,10 +9045,11 @@ export function remapClipboardSubgraphNodeIds(
): void {
const usedNodeIds = new Set<number>()
forEachNode(rootGraph, (node) => {
if (typeof node.id !== 'number') return
usedNodeIds.add(node.id)
if (rootGraph.state.lastNodeId < node.id)
rootGraph.state.lastNodeId = node.id
const numericId = Number(node.id)
if (!Number.isInteger(numericId)) return
usedNodeIds.add(numericId)
if (rootGraph.state.lastNodeId < numericId)
rootGraph.state.lastNodeId = numericId
})
function nextUniqueNodeId() {
@@ -9055,9 +9059,12 @@ export function remapClipboardSubgraphNodeIds(
return nextId
}
const subgraphNodeIdMap = new Map<SubgraphId, Map<NodeId, NodeId>>()
const subgraphNodeIdMap = new Map<
SubgraphId,
Map<SerializedNodeId, SerializedNodeId>
>()
for (const subgraphInfo of parsed.subgraphs ?? []) {
const remappedIds = new Map<NodeId, NodeId>()
const remappedIds = new Map<SerializedNodeId, SerializedNodeId>()
const interiorNodes = subgraphInfo.nodes ?? []
for (const nodeInfo of interiorNodes) {

View File

@@ -8,6 +8,12 @@ import {
import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { LayoutSource } from '@/renderer/core/layout/types'
import {
UNASSIGNED_NODE_ID,
nodeId as toNodeId,
serializeNodeId
} from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import { adjustColor } from '@/utils/colorUtil'
import type { ColorAdjustOptions } from '@/utils/colorUtil'
import {
@@ -16,7 +22,10 @@ import {
toClass
} from '@/lib/litegraph/src/utils/type'
import { SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants'
import {
SUBGRAPH_INPUT_ID,
SUBGRAPH_OUTPUT_ID
} from '@/lib/litegraph/src/constants'
import { cachedMeasureText } from '@/lib/litegraph/src/utils/textMeasureCache'
import type { DragAndScale } from './DragAndScale'
import type { LGraph } from './LGraph'
@@ -98,7 +107,7 @@ import type { WidgetTypeMap } from './widgets/widgetMap'
// #region Types
export type NodeId = number | string
export type { NodeId } from '@/types/nodeId'
export type NodeProperty = string | number | boolean | object
@@ -498,10 +507,11 @@ export class LGraphNode
this._pos[0] = value[0]
this._pos[1] = value[1]
if (this.id === UNASSIGNED_NODE_ID || !this.graph) return
const mutations = useLayoutMutations()
mutations.setSource(LayoutSource.Canvas)
mutations.moveNode(String(this.id), { x: value[0], y: value[1] })
mutations.moveNode(this.id, { x: value[0], y: value[1] })
}
/**
@@ -520,10 +530,11 @@ export class LGraphNode
this._size[0] = value[0]
this._size[1] = value[1]
if (this.id === UNASSIGNED_NODE_ID || !this.graph) return
const mutations = useLayoutMutations()
mutations.setSource(LayoutSource.Canvas)
mutations.resizeNode(String(this.id), {
mutations.resizeNode(this.id, {
width: value[0],
height: value[1]
})
@@ -810,7 +821,7 @@ export class LGraphNode
}
constructor(title: string, type?: string) {
this.id = -1
this.id = UNASSIGNED_NODE_ID
this.title = title || 'Unnamed'
this.type = type ?? ''
this.size = [LiteGraph.NODE_WIDTH, 60]
@@ -834,6 +845,7 @@ export class LGraphNode
this.graph.incrementVersion()
}
if (info.id === -1) info.id = this.id
else info.id = toNodeId(info.id)
for (const j in info) {
if (j == 'properties') {
// i don't want to clone properties, I want to reuse the old container
@@ -944,7 +956,7 @@ export class LGraphNode
serialize(): ISerialisedNode {
// create serialization object
const o: ISerialisedNode = {
id: this.id,
id: serializeNodeId(this.id),
type: this.type,
pos: [this.pos[0], this.pos[1]],
size: [this.size[0], this.size[1]],
@@ -2001,7 +2013,7 @@ export class LGraphNode
// Only register with store if node has a valid ID (is already in a graph).
// If the node isn't in a graph yet (id === -1), registration happens
// when the node is added via LGraph.add() -> node.onAdded.
if (this.id !== -1 && isNodeBindable(widget)) {
if (this.id !== UNASSIGNED_NODE_ID && isNodeBindable(widget)) {
widget.setNodeId(this.id)
}
@@ -3272,7 +3284,7 @@ export class LGraphNode
const link_info = graph._links.get(link_id)
if (link_info) {
// Let SubgraphInput do the disconnect.
if (link_info.origin_id === -10 && 'inputNode' in graph) {
if (link_info.origin_id === SUBGRAPH_INPUT_ID && 'inputNode' in graph) {
graph.inputNode._disconnectNodeInput(this, input, link_info)
return true
}

View File

@@ -6,8 +6,10 @@ import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { LayoutSource } from '@/renderer/core/layout/types'
import { nodeId as toNodeId, serializeNodeId } from '@/types/nodeId'
import type { LGraphNode, NodeId } from './LGraphNode'
import type { LGraphNode } from './LGraphNode'
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
import type { Reroute, RerouteId } from './Reroute'
import type {
CanvasColour,
@@ -24,16 +26,21 @@ import type { Serialisable, SerialisableLLink } from './types/serialisation'
const layoutMutations = useLayoutMutations()
export type LinkId = number
export type LinkEndpointNodeId = NodeId | -1
export type SerialisedLLinkArray = [
id: LinkId,
origin_id: NodeId,
origin_id: SerializedNodeId,
origin_slot: number,
target_id: NodeId,
target_id: SerializedNodeId,
target_slot: number,
type: ISlotType
]
function toLinkEndpointNodeId(id: SerializedNodeId): LinkEndpointNodeId {
return id === -1 ? -1 : toNodeId(id)
}
// Resolved connection union; eliminates subgraph in/out as a possibility
export type ResolvedConnection = BaseResolvedConnection &
(
@@ -97,11 +104,11 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
parentId?: RerouteId
type: ISlotType
/** Output node ID */
origin_id: NodeId
origin_id: LinkEndpointNodeId
/** Output slot index */
origin_slot: number
/** Input node ID */
target_id: NodeId
target_id: LinkEndpointNodeId
/** Input slot index */
target_slot: number
@@ -154,17 +161,17 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
constructor(
id: LinkId,
type: ISlotType,
origin_id: NodeId,
origin_id: SerializedNodeId,
origin_slot: number,
target_id: NodeId,
target_id: SerializedNodeId,
target_slot: number,
parentId?: RerouteId
) {
this.id = id
this.type = type
this.origin_id = origin_id
this.origin_id = toLinkEndpointNodeId(origin_id)
this.origin_slot = origin_slot
this.target_id = target_id
this.target_id = toLinkEndpointNodeId(target_id)
this.target_slot = target_slot
this.parentId = parentId
@@ -351,17 +358,17 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
configure(o: LLink | SerialisedLLinkArray) {
if (Array.isArray(o)) {
this.id = o[0]
this.origin_id = o[1]
this.origin_id = toLinkEndpointNodeId(o[1])
this.origin_slot = o[2]
this.target_id = o[3]
this.target_id = toLinkEndpointNodeId(o[3])
this.target_slot = o[4]
this.type = o[5]
} else {
this.id = o.id
this.type = o.type
this.origin_id = o.origin_id
this.origin_id = toLinkEndpointNodeId(o.origin_id)
this.origin_slot = o.origin_slot
this.target_id = o.target_id
this.target_id = toLinkEndpointNodeId(o.target_id)
this.target_slot = o.target_slot
this.parentId = o.parentId
}
@@ -373,7 +380,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
* @param outputIndex The array index of the node output
* @returns `true` if the origin matches, otherwise `false`.
*/
hasOrigin(nodeId: NodeId, outputIndex: number): boolean {
hasOrigin(nodeId: LinkEndpointNodeId, outputIndex: number): boolean {
return this.origin_id === nodeId && this.origin_slot === outputIndex
}
@@ -383,7 +390,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
* @param inputIndex The array index of the node input
* @returns `true` if the target matches, otherwise `false`.
*/
hasTarget(nodeId: NodeId, inputIndex: number): boolean {
hasTarget(nodeId: SerializedNodeId, inputIndex: number): boolean {
return this.target_id === nodeId && this.target_slot === inputIndex
}
@@ -468,9 +475,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
serialize(): SerialisedLLinkArray {
return [
this.id,
this.origin_id,
serializeNodeId(this.origin_id),
this.origin_slot,
this.target_id,
serializeNodeId(this.target_id),
this.target_slot,
this.type
]
@@ -479,9 +486,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
asSerialisable(): SerialisableLLink {
const copy: SerialisableLLink = {
id: this.id,
origin_id: this.origin_id,
origin_id: serializeNodeId(this.origin_id),
origin_slot: this.origin_slot,
target_id: this.target_id,
target_id: serializeNodeId(this.target_id),
target_slot: this.target_slot,
type: this.type
}

View File

@@ -2,9 +2,9 @@ import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMuta
import { LayoutSource } from '@/renderer/core/layout/types'
import { LGraphBadge } from './LGraphBadge'
import type { LGraphNode, NodeId } from './LGraphNode'
import type { LGraphNode } from './LGraphNode'
import { LLink } from './LLink'
import type { LinkId } from './LLink'
import type { LinkEndpointNodeId, LinkId } from './LLink'
import type {
CanvasColour,
INodeInputSlot,
@@ -182,7 +182,7 @@ export class Reroute
}
/** @inheritdoc */
get origin_id(): NodeId | undefined {
get origin_id(): LinkEndpointNodeId | undefined {
return this.firstLink?.origin_id
}

View File

@@ -28,7 +28,7 @@ exports[`LGraph > supports schema v0.4 graphs > oldSchemaGraph 1`] = `
"links": [],
"nodes": [
{
"id": 1,
"id": "1",
"mode": 0,
"pos": [
10,

View File

@@ -1,4 +1,5 @@
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { LLink } from '@/lib/litegraph/src/LLink'
import type { Reroute } from '@/lib/litegraph/src/Reroute'
import {
@@ -37,13 +38,13 @@ export class FloatingRenderLink implements RenderLink {
readonly fromDirection: LinkDirection
readonly fromSlotIndex: SlotIndex
readonly outputNodeId: NodeId = -1
readonly outputNodeId: SerializedNodeId = -1
readonly outputNode?: LGraphNode
readonly outputSlot?: INodeOutputSlot
readonly outputIndex: number = -1
readonly outputPos?: Point
readonly inputNodeId: NodeId = -1
readonly inputNodeId: SerializedNodeId = -1
readonly inputNode?: LGraphNode
readonly inputSlot?: INodeInputSlot
readonly inputIndex: number = -1

View File

@@ -17,6 +17,7 @@ import {
LinkDirection
} from '@/lib/litegraph/src/litegraph'
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
import { nodeId } from '@/types/nodeId'
import {
createMockNodeInputSlot,
createMockNodeOutputSlot
@@ -74,7 +75,7 @@ const test = baseTest.extend<TestContext>({
createTestNode: async ({ network }, use) => {
await use((id: number): LGraphNode => {
const node = new LGraphNode('test')
node.id = id
node.id = nodeId(id)
network.add(node)
return node
})

View File

@@ -12,6 +12,7 @@ import { LGraphNode, LLink, LinkConnector } from '@/lib/litegraph/src/litegraph'
import { test as baseTest } from '../__fixtures__/testExtensions'
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
import { nodeId } from '@/types/nodeId'
import {
createMockCanvasPointerEvent,
createMockCanvasRenderingContext2D
@@ -59,7 +60,7 @@ const test = baseTest.extend<TestContext>({
createTestNode: async ({ graph }, use) => {
await use((id): LGraphNode => {
const node = new LGraphNode('test')
node.id = id
node.id = nodeId(id)
graph.add(node)
return node
})

View File

@@ -14,6 +14,7 @@ import type {
NodeInputSlot
} from '@/lib/litegraph/src/litegraph'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import { nodeId } from '@/types/nodeId'
import { createTestSubgraph } from '../subgraph/__fixtures__/subgraphHelpers'
import {
@@ -197,7 +198,7 @@ describe('LinkConnector SubgraphInput connection validation', () => {
// Create a minimal valid setup
const subgraph = createTestSubgraph()
const node = new LGraphNode('TestNode')
node.id = 1
node.id = nodeId(1)
node.addInput('test_in', 'number')
subgraph.add(node)

View File

@@ -1,4 +1,5 @@
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { LLink } from '@/lib/litegraph/src/LLink'
import type { Reroute } from '@/lib/litegraph/src/Reroute'
import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget'
@@ -36,13 +37,13 @@ export abstract class MovingLinkBase implements RenderLink {
abstract readonly fromDirection: LinkDirection
abstract readonly fromSlotIndex: SlotIndex
readonly outputNodeId: NodeId
readonly outputNodeId: SerializedNodeId
readonly outputNode: LGraphNode
readonly outputSlot: INodeOutputSlot
readonly outputIndex: number
readonly outputPos: Point
readonly inputNodeId: NodeId
readonly inputNodeId: SerializedNodeId
readonly inputNode: LGraphNode
readonly inputSlot: INodeInputSlot
readonly inputIndex: number

View File

@@ -1,3 +1,5 @@
import { nodeId } from '@/types/nodeId'
/**
* Subgraph constants
*
@@ -5,7 +7,7 @@
*/
/** ID of the virtual input node of a subgraph. */
export const SUBGRAPH_INPUT_ID = -10
export const SUBGRAPH_INPUT_ID = nodeId(-10)
/** ID of the virtual output node of a subgraph. */
export const SUBGRAPH_OUTPUT_ID = -20
export const SUBGRAPH_OUTPUT_ID = nodeId(-20)

View File

@@ -1,7 +1,8 @@
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LGraphButton } from '@/lib/litegraph/src/LGraphButton'
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
@@ -57,6 +58,6 @@ export interface LGraphCanvasEventMap {
/** Ghost placement mode has started or ended. */
'litegraph:ghost-placement': {
active: boolean
nodeId: NodeId
nodeId: SerializedNodeId
}
}

View File

@@ -1,5 +1,6 @@
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { LLink, ResolvedConnection } from '@/lib/litegraph/src/LLink'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
@@ -59,21 +60,21 @@ export interface LGraphEventMap {
}
'node:property:changed': {
nodeId: NodeId
nodeId: SerializedNodeId
property: string
oldValue: unknown
newValue: unknown
}
'node:slot-errors:changed': { nodeId: NodeId }
'node:slot-errors:changed': { nodeId: SerializedNodeId }
'node:slot-links:changed': {
nodeId: NodeId
nodeId: SerializedNodeId
slotType: NodeSlotType
slotIndex: number
connected: boolean
linkId: number
}
'node:slot-label:changed': {
nodeId: NodeId
nodeId: SerializedNodeId
slotType?: NodeSlotType
}
}

View File

@@ -4,8 +4,9 @@ import type { WidgetId } from '@/types/widgetId'
import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets'
import type { ContextMenu } from './ContextMenu'
import type { LGraphNode, NodeId, NodeProperty } from './LGraphNode'
import type { LLink, LinkId } from './LLink'
import type { LGraphNode, NodeProperty } from './LGraphNode'
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
import type { LinkEndpointNodeId, LLink, LinkId } from './LLink'
import type { Reroute, RerouteId } from './Reroute'
import type { SubgraphInput } from './subgraph/SubgraphInput'
import type { SubgraphInputNode } from './subgraph/SubgraphInputNode'
@@ -162,7 +163,7 @@ export interface ReadonlyLinkNetwork {
readonly links: ReadonlyMap<LinkId, LLink>
readonly reroutes: ReadonlyMap<RerouteId, Reroute>
readonly floatingLinks: ReadonlyMap<LinkId, LLink>
getNodeById(id: NodeId | null | undefined): LGraphNode | null
getNodeById(id: SerializedNodeId | null | undefined): LGraphNode | null
getLink(id: null | undefined): undefined
getLink(id: LinkId | null | undefined): LLink | undefined
getReroute(parentId: null | undefined): undefined
@@ -217,7 +218,7 @@ export interface LinkSegment {
_dragging?: boolean
/** Output node ID */
readonly origin_id: NodeId | undefined
readonly origin_id: LinkEndpointNodeId | undefined
/** Output slot index */
readonly origin_slot: SlotIndex | undefined
}

View File

@@ -1,4 +1,5 @@
import type { LGraphNode, NodeId } from './LGraphNode'
import type { LGraphNode } from './LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { LLink, LinkId } from './LLink'
/** Generates a unique string key for a link's connection tuple. */
@@ -43,7 +44,7 @@ export function purgeOrphanedLinks(
ids: LinkId[],
keepId: LinkId,
links: Map<LinkId, LLink>,
getNodeById: (id: NodeId) => LGraphNode | null
getNodeById: (id: SerializedNodeId) => LGraphNode | null
): void {
for (const id of ids) {
if (id === keepId) continue

View File

@@ -3,11 +3,12 @@ import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
ExecutableNodeDTO,
LGraph,
LGraphNode,
LGraphEventMode,
ExecutableNodeDTO
LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { nodeId } from '@/types/nodeId'
import {
createNestedSubgraphs,
@@ -41,7 +42,7 @@ describe('ExecutableNodeDTO Creation', () => {
it('should create DTO with subgraph path', () => {
const graph = new LGraph()
const node = new LGraphNode('Inner Node')
node.id = 42
node.id = nodeId(42)
graph.add(node)
const subgraphPath = ['10', '20'] as const
@@ -115,7 +116,7 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
it('should generate simple ID for root node', () => {
const graph = new LGraph()
const node = new LGraphNode('Root Node')
node.id = 5
node.id = nodeId(5)
graph.add(node)
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
@@ -126,7 +127,7 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
it('should generate path-based ID for nested node', () => {
const graph = new LGraph()
const node = new LGraphNode('Nested Node')
node.id = 3
node.id = nodeId(3)
graph.add(node)
const path = ['1', '2'] as const
@@ -138,7 +139,7 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
it('should handle deep nesting paths', () => {
const graph = new LGraph()
const node = new LGraphNode('Deep Node')
node.id = 99
node.id = nodeId(99)
graph.add(node)
const path = ['1', '2', '3', '4', '5'] as const
@@ -150,11 +151,11 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
it('should handle string and number IDs consistently', () => {
const graph = new LGraph()
const node1 = new LGraphNode('Node 1')
node1.id = 10
node1.id = nodeId(10)
graph.add(node1)
const node2 = new LGraphNode('Node 2')
node2.id = 20
node2.id = nodeId(20)
graph.add(node2)
const dto1 = new ExecutableNodeDTO(node1, ['5'], new Map(), undefined)
@@ -487,7 +488,7 @@ describe('ExecutableNodeDTO Properties', () => {
it('should provide access to basic properties', () => {
const graph = new LGraph()
const node = new LGraphNode('Test Node')
node.id = 42
node.id = nodeId(42)
node.addInput('input', 'number')
node.addOutput('output', 'string')
graph.add(node)
@@ -549,7 +550,7 @@ describe('ExecutableNodeDTO Memory Efficiency', () => {
// Create DTOs
for (let i = 0; i < 100; i++) {
const node = new LGraphNode(`Node ${i}`)
node.id = i
node.id = nodeId(i)
graph.add(node)
const dto = new ExecutableNodeDTO(node, ['parent'], new Map(), undefined)
nodes.push(dto)
@@ -619,7 +620,7 @@ describe('ExecutableNodeDTO Integration', () => {
it('should preserve original node properties through DTO', () => {
const graph = new LGraph()
const originalNode = new LGraphNode('Original')
originalNode.id = 123
originalNode.id = nodeId(123)
originalNode.addInput('test', 'number')
originalNode.properties = { value: 42 }
graph.add(originalNode)
@@ -632,7 +633,7 @@ describe('ExecutableNodeDTO Integration', () => {
)
// DTO should provide access to original node properties
expect(dto.node.id).toBe(123)
expect(Number(dto.node.id)).toBe(123)
expect(dto.node.inputs).toHaveLength(1)
expect(dto.node.properties.value).toBe(42)
@@ -644,7 +645,7 @@ describe('ExecutableNodeDTO Integration', () => {
const subgraph = createTestSubgraph({ nodeCount: 1 })
const subgraphNode = createTestSubgraphNode(subgraph, { id: 99 })
const innerNode = subgraph.nodes[0]
innerNode.id = 55
innerNode.id = nodeId(55)
const dto = new ExecutableNodeDTO(
innerNode,
@@ -655,8 +656,8 @@ describe('ExecutableNodeDTO Integration', () => {
// DTO provides execution context
expect(dto.id).toBe('99:55') // Path-based execution ID
expect(dto.node.id).toBe(55) // Original node ID preserved
expect(dto.subgraphNode?.id).toBe(99) // Subgraph context
expect(Number(dto.node.id)).toBe(55) // Original node ID preserved
expect(Number(dto.subgraphNode?.id)).toBe(99) // Subgraph context
})
})
@@ -669,7 +670,7 @@ describe('ExecutableNodeDTO Scale Testing', () => {
// Create DTOs to test performance
for (let i = 0; i < 1000; i++) {
const node = new LGraphNode(`Node ${i}`)
node.id = i
node.id = nodeId(i)
node.addInput('in', 'number')
graph.add(node)
@@ -692,7 +693,7 @@ describe('ExecutableNodeDTO Scale Testing', () => {
it('should handle complex path generation correctly', () => {
const graph = new LGraph()
const node = new LGraphNode('Deep Node')
node.id = 999
node.id = nodeId(999)
graph.add(node)
// Test deterministic path generation behavior

View File

@@ -1,5 +1,6 @@
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { LinkId } from '@/lib/litegraph/src/LLink'
import { InvalidLinkError } from '@/lib/litegraph/src/infrastructure/InvalidLinkError'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
@@ -99,7 +100,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
/** The actual node that this DTO wraps. */
readonly node: LGraphNode | SubgraphNode,
/** A list of subgraph instance node IDs from the root graph to the containing instance. @see {@link id} */
readonly subgraphNodePath: readonly NodeId[],
readonly subgraphNodePath: readonly SerializedNodeId[],
/** A flattened map of all DTOs in this node network. Subgraph instances have been expanded into their inner nodes. */
readonly nodesByExecutionId: Map<ExecutionId, ExecutableLGraphNode>,
/** The actual subgraph instance that contains this node, otherwise undefined. */

View File

@@ -1,4 +1,5 @@
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import { serializeNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type {
@@ -368,7 +369,7 @@ export abstract class SubgraphIONodeBase<
asSerialisable(): ExportedSubgraphIONode {
return {
id: this.id,
id: serializeNodeId(this.id),
bounding: this.boundingRect.export(),
pinned: this.pinned ? true : undefined
}

View File

@@ -1,5 +1,6 @@
import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { NodeId } from '@/types/nodeId'
import { LLink } from '@/lib/litegraph/src/LLink'
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'

View File

@@ -13,13 +13,11 @@ import type {
IWidgetLocator
} from '@/lib/litegraph/src/interfaces'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type {
INodeInputSlot,
ISlotType,
NodeId
} from '@/lib/litegraph/src/litegraph'
import type { INodeInputSlot, ISlotType } from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot'
import { NodeOutputSlot } from '@/lib/litegraph/src/node/NodeOutputSlot'
import { nodeId as toNodeId } from '@/types/nodeId'
import type { SerializedNodeId } from '@/types/nodeId'
import type {
GraphOrSubgraph,
Subgraph
@@ -737,7 +735,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
}
const newLink = LLink.create(innerLink)
newLink.origin_id = `${this.id}:${innerLink.origin_id}`
newLink.origin_id = toNodeId(`${this.id}:${innerLink.origin_id}`)
newLink.origin_slot = innerLink.origin_slot
return newLink
@@ -778,7 +776,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
override getInnerNodes(
executableNodes: Map<ExecutionId, ExecutableLGraphNode>,
subgraphNodePath: readonly NodeId[] = [],
subgraphNodePath: readonly SerializedNodeId[] = [],
nodes: ExecutableLGraphNode[] = [],
visited = new Set<SubgraphNode>()
): ExecutableLGraphNode[] {

View File

@@ -1,5 +1,6 @@
import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { NodeId } from '@/types/nodeId'
import type { LLink } from '@/lib/litegraph/src/LLink'
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'

View File

@@ -492,16 +492,19 @@ describe('SubgraphSerialization - Data Integrity', () => {
graph.configure(structuredClone(duplicateSubgraphNodeIds))
const rootIds = graph.nodes
.map((node) => node.id)
.filter((id): id is number => typeof id === 'number')
.map((node) => Number(node.id))
.sort((a, b) => a - b)
expect(rootIds).toEqual([102, 103])
const subgraphAIds = new Set(
graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_A)!.nodes.map((node) => node.id)
graph.subgraphs
.get(DUPLICATE_ID_SUBGRAPH_A)!
.nodes.map((node) => Number(node.id))
)
const subgraphBIds = new Set(
graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_B)!.nodes.map((node) => node.id)
graph.subgraphs
.get(DUPLICATE_ID_SUBGRAPH_B)!
.nodes.map((node) => Number(node.id))
)
expect(subgraphAIds).toEqual(new Set([3, 8, 37]))

View File

@@ -21,6 +21,7 @@ import {
createTestSubgraphNode,
resetSubgraphFixtureState
} from './__fixtures__/subgraphHelpers'
import { nodeId } from '@/types/nodeId'
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
@@ -132,7 +133,7 @@ describe('Subgraph slot connections', () => {
// Create a node inside the subgraph
const internalNode = new LGraphNode('InternalNode')
internalNode.id = 100
internalNode.id = nodeId(100)
internalNode.addInput('in', 'number')
subgraph.add(internalNode)

View File

@@ -21,6 +21,7 @@ import type { SerializedProxyWidgetTuple } from '@/core/schemas/promotionSchema'
import { IS_CONTROL_WIDGET } from '@/scripts/controlWidgetMarker'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { nodeId } from '@/types/nodeId'
import type { WidgetId } from '@/types/widgetId'
import { widgetId } from '@/types/widgetId'
import { createNodeLocatorId } from '@/types/nodeIdentification'
@@ -1130,13 +1131,24 @@ describe('SubgraphWidgetPromotion', () => {
describe('previewExposures round-trip', () => {
const CANVAS = '$$canvas-image-preview'
const exposure12 = { sourceNodeId: '12', sourcePreviewName: CANVAS }
const exposure12 = {
sourceNodeId: nodeId('12'),
sourcePreviewName: CANVAS
}
const exposure14 = {
sourceNodeId: nodeId('14'),
sourcePreviewName: 'videopreview'
}
const serializedExposure12 = {
sourceNodeId: '12',
sourcePreviewName: CANVAS
}
const serializedExposure14 = {
sourceNodeId: '14',
sourcePreviewName: 'videopreview'
}
const named12 = { name: CANVAS, ...exposure12 }
const named14 = { name: 'videopreview', ...exposure14 }
const named12 = { name: CANVAS, ...serializedExposure12 }
const named14 = { name: 'videopreview', ...serializedExposure14 }
it('hydrates previewExposures into the store during configure', () => {
const hostNode = createTestSubgraphNode(createTestSubgraph())

View File

@@ -11,9 +11,9 @@ import type {
ExportedSubgraph,
ExportedSubgraphInstance,
ISlotType,
NodeId,
UUID
} from '@/lib/litegraph/src/litegraph'
import type { SerializedNodeId } from '@/types/nodeId'
import {
LGraph,
LGraphNode,
@@ -77,7 +77,7 @@ interface TestSubgraphOptions {
interface TestSubgraphNodeOptions {
parentGraph?: LGraph | Subgraph
id?: NodeId
id?: SerializedNodeId
pos?: [number, number]
size?: [number, number]
}

View File

@@ -1,5 +1,5 @@
import type { LGraphState } from '../LGraph'
import type { NodeId } from '../LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type {
ExportedSubgraph,
ExposedWidget,
@@ -31,7 +31,10 @@ export function deduplicateSubgraphNodeIds(
const usedNodeIds = new Set(reservedNodeIds)
const subgraphIdSet = new Set(clonedSubgraphs.map((sg) => sg.id))
const remapBySubgraph = new Map<string, Map<NodeId, NodeId>>()
const remapBySubgraph = new Map<
string,
Map<SerializedNodeId, SerializedNodeId>
>()
for (const subgraph of clonedSubgraphs) {
const remappedIds = remapNodeIds(subgraph.nodes ?? [], usedNodeIds, state)
@@ -64,8 +67,8 @@ function remapNodeIds(
nodes: ISerialisedNode[],
usedNodeIds: Set<number>,
state: LGraphState
): Map<NodeId, NodeId> {
const remappedIds = new Map<NodeId, NodeId>()
): Map<SerializedNodeId, SerializedNodeId> {
const remappedIds = new Map<SerializedNodeId, SerializedNodeId>()
for (const node of nodes) {
const id = node.id
@@ -95,21 +98,21 @@ function remapNodeIds(
function findNextAvailableId(
usedNodeIds: Set<number>,
state: LGraphState
): NodeId {
): SerializedNodeId {
while (true) {
const nextId = state.lastNodeId + 1
if (nextId > MAX_NODE_ID) {
throw new Error('Node ID space exhausted')
}
state.lastNodeId = nextId
if (!usedNodeIds.has(nextId)) return nextId as NodeId
if (!usedNodeIds.has(nextId)) return nextId
}
}
/** Patches origin_id / target_id in serialized links. */
function patchSerialisedLinks(
links: SerialisableLLink[],
remappedIds: Map<NodeId, NodeId>
remappedIds: Map<SerializedNodeId, SerializedNodeId>
): void {
for (const link of links) {
const newOrigin = remappedIds.get(link.origin_id)
@@ -123,7 +126,7 @@ function patchSerialisedLinks(
/** Patches promoted widget node references. */
function patchPromotedWidgets(
widgets: ExposedWidget[],
remappedIds: Map<NodeId, NodeId>
remappedIds: Map<SerializedNodeId, SerializedNodeId>
): void {
for (const widget of widgets) {
const newId = remappedIds.get(widget.id)
@@ -192,7 +195,7 @@ export function topologicalSortSubgraphs(
function patchProxyWidgets(
rootNodes: ISerialisedNode[],
subgraphIdSet: Set<string>,
remapBySubgraph: Map<string, Map<NodeId, NodeId>>
remapBySubgraph: Map<string, Map<SerializedNodeId, SerializedNodeId>>
): void {
for (const node of rootNodes) {
if (!subgraphIdSet.has(node.type)) continue
@@ -204,7 +207,7 @@ function patchProxyWidgets(
for (const entry of proxyWidgets) {
if (!Array.isArray(entry)) continue
const oldId = Number(entry[0]) as NodeId
const oldId = Number(entry[0])
const newId = remappedIds.get(oldId)
if (newId !== undefined) entry[0] = String(newId)
}

View File

@@ -1,4 +1,4 @@
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type {
INodeInputSlot,
INodeOutputSlot
@@ -6,7 +6,7 @@ import type {
import type { SubgraphIO } from '@/lib/litegraph/src/types/serialisation'
export interface NodeLike {
id: NodeId
id: SerializedNodeId
canConnectTo(
node: NodeLike,

View File

@@ -1,9 +1,9 @@
import type { NodeId } from '../LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { NodeSlotType } from './globalEnums'
interface NodePropertyChangedEvent {
type: 'node:property:changed'
nodeId: NodeId
nodeId: SerializedNodeId
property: string
oldValue: unknown
newValue: unknown
@@ -11,12 +11,12 @@ interface NodePropertyChangedEvent {
interface NodeSlotErrorsChangedEvent {
type: 'node:slot-errors:changed'
nodeId: NodeId
nodeId: SerializedNodeId
}
interface NodeSlotLinksChangedEvent {
type: 'node:slot-links:changed'
nodeId: NodeId
nodeId: SerializedNodeId
slotType: NodeSlotType
slotIndex: number
connected: boolean
@@ -25,7 +25,7 @@ interface NodeSlotLinksChangedEvent {
interface NodeSlotLabelChangedEvent {
type: 'node:slot-label:changed'
nodeId: NodeId
nodeId: SerializedNodeId
slotType?: NodeSlotType
}

View File

@@ -7,7 +7,8 @@ import type {
SubgraphId
} from '../LGraph'
import type { GroupId, IGraphGroupFlags } from '../LGraphGroup'
import type { NodeId, NodeProperty } from '../LGraphNode'
import type { NodeProperty } from '../LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { LinkId, SerialisedLLinkArray } from '../LLink'
import type { FloatingRerouteSlot, RerouteId } from '../Reroute'
import type {
@@ -79,7 +80,7 @@ export type ISerialisableNodeOutput = Omit<
/** Serialised LGraphNode */
export interface ISerialisedNode {
title?: string
id: NodeId
id: SerializedNodeId
type: string
pos: Point
size: Size
@@ -125,7 +126,7 @@ export interface ExportedSubgraphInstance extends NodeSubgraphSharedProps {
* Maintained for backwards compat
*/
export interface ISerialisedGraph extends BaseExportedGraph {
last_node_id: NodeId
last_node_id: SerializedNodeId
last_link_id: LinkId
nodes: ISerialisedNode[]
links: SerialisedLLinkArray[]
@@ -175,7 +176,7 @@ export interface SubgraphIO extends SubgraphIOShared {
/** A reference to a node widget shown in the parent graph */
export interface ExposedWidget {
/** The ID of the node (inside the subgraph) that the widget belongs to. */
id: NodeId
id: SerializedNodeId
/** The name of the widget to show in the parent graph. */
name: string
}
@@ -211,11 +212,11 @@ export interface SerialisableLLink {
/** Link ID */
id: LinkId
/** Output node ID */
origin_id: NodeId
origin_id: SerializedNodeId
/** Output slot index */
origin_slot: number
/** Input node ID */
target_id: NodeId
target_id: SerializedNodeId
/** Input slot index */
target_slot: number
/** Data type of the link */
@@ -225,7 +226,7 @@ export interface SerialisableLLink {
}
export interface ExportedSubgraphIONode {
id: NodeId
id: SerializedNodeId
bounding: [number, number, number, number]
pinned?: boolean
}

View File

@@ -1,6 +1,7 @@
import type { Bounds } from '@/renderer/core/layout/types'
import type { CurveData } from '@/components/curve/types'
import type { BoundingBox } from '@/types/boundingBoxes'
import type { SerializedNodeId } from '@/types/nodeId'
import type { WidgetId } from '@/types/widgetId'
import type {
@@ -10,16 +11,11 @@ import type {
RequiredProps,
Size
} from '../interfaces'
import type {
CanvasPointer,
LGraphCanvas,
LGraphNode,
NodeId
} from '../litegraph'
import type { CanvasPointer, LGraphCanvas, LGraphNode } from '../litegraph'
import type { CanvasPointerEvent } from './events'
export interface NodeBindable {
setNodeId(nodeId: NodeId): void
setNodeId(nodeId: SerializedNodeId): void
}
export interface IWidgetOptions<TValues = unknown> {

View File

@@ -7,6 +7,7 @@ import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
import { NumberWidget } from '@/lib/litegraph/src/widgets/NumberWidget'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { nodeId } from '@/types/nodeId'
import { widgetId } from '@/types/widgetId'
function createTestWidget(
@@ -36,7 +37,7 @@ describe('BaseWidget store integration', () => {
store = useWidgetValueStore()
graph = new LGraph()
node = new LGraphNode('TestNode')
node.id = 1
node.id = nodeId(1)
graph.add(node)
})

View File

@@ -3,7 +3,7 @@ import { drawTextInArea } from '@/lib/litegraph/src/draw'
import { cachedMeasureText } from '@/lib/litegraph/src/utils/textMeasureCache'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type { Point } from '@/lib/litegraph/src/interfaces'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type {
CanvasPointer,
LGraphCanvas,
@@ -143,7 +143,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
* Associates this widget with a node ID and registers it in the WidgetValueStore.
* Once set, value reads/writes will be delegated to the store.
*/
setNodeId(nodeId: NodeId): void {
setNodeId(nodeId: SerializedNodeId): void {
const graphId = this.node.graph?.rootGraph.id
if (!graphId) return

Some files were not shown because too many files have changed in this diff Show More