Compare commits

..

1 Commits

Author SHA1 Message Date
Talmaj Marinc
842e3d7541 Initial commit for DynamiGroupSupport. 2026-06-25 00:14:28 +02:00
245 changed files with 2274 additions and 2425 deletions

View File

@@ -179,9 +179,6 @@ 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

@@ -344,6 +344,15 @@ export const zDynamicComboInputSpec = z.tuple([
})
])
export const zDynamicGroupInputSpec = z.tuple([
z.literal('COMFY_DYNAMICGROUP_V3'),
zBaseInputOptions.extend({
template: zComfyInputsSpec,
min: z.number().int().nonnegative().optional().default(0),
max: z.number().int().positive().max(100).optional().default(50)
})
])
export const zMatchTypeOptions = z.object({
...zBaseInputOptions.shape,
type: z.literal('COMFY_MATCHTYPE_V3'),

View File

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

View File

@@ -145,9 +145,8 @@ 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: NodeId }>()
const { nodeId } = defineProps<{ nodeId: string }>()
const modelValue = defineModel<BoundingBox[]>({ default: () => [] })
const canvasEl = useTemplateRef<HTMLCanvasElement>('canvasEl')

View File

@@ -12,9 +12,8 @@ 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 } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import type { NodeId } from '@/types/nodeId'
import {
LGraphEventMode,
TitleMode
@@ -133,7 +132,7 @@ function handleClick(e: MouseEvent) {
if (!isSelectOutputsMode.value) return
if (!node.constructor.nodeData?.output_node)
return canvasInteractions.forwardEventToCanvas(e)
const index = appModeStore.selectedOutputs.findIndex((id) => id === node.id)
const index = appModeStore.selectedOutputs.findIndex((id) => id == node.id)
if (index === -1) appModeStore.selectedOutputs.push(node.id)
else appModeStore.selectedOutputs.splice(index, 1)
return
@@ -288,7 +287,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
:title
:sub-title="String(key)"
:remove="
() => remove(appModeStore.selectedOutputs, (k) => k === key)
() => remove(appModeStore.selectedOutputs, (k) => k == key)
"
/>
</DraggableList>
@@ -348,7 +347,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
v-if="isSelected"
class="pointer-events-auto absolute -top-1/2 -right-1/2 size-full cursor-pointer rounded-lg bg-warning-background p-2"
@click.stop="
remove(appModeStore.selectedOutputs, (k) => k === key)
remove(appModeStore.selectedOutputs, (k) => k == key)
"
@pointerdown.stop
>

View File

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

View File

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

View File

@@ -11,7 +11,6 @@ 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 { toNodeId } from '@/types/nodeId'
type TestWidget = BaseDOMWidget<object | string>
@@ -22,7 +21,7 @@ function createNode(
pos: [number, number]
) {
const node = new LGraphNode(title)
node.id = toNodeId(id)
node.id = 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.lastExecutionErrorNodeId === nodeData.id
executionErrorStore.lastExecutionError?.node_id === nodeData.id
? 'Execution error'
: null
"

View File

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

View File

@@ -14,10 +14,10 @@
<script setup lang="ts">
import { default as DOMPurify } from 'dompurify'
import Skeleton from 'primevue/skeleton'
import { computed } from 'vue'
import { computed, onMounted, watch } from 'vue'
import type { NodeId } from '@/lib/litegraph/src/litegraph'
import { useExecutionStore } from '@/stores/executionStore'
import type { NodeId } from '@/types/nodeId'
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
const modelValue = defineModel<string>({ required: true })
@@ -28,7 +28,8 @@ const props = defineProps<{
const executionStore = useExecutionStore()
const isParentNodeExecuting = computed(() => {
if (executionStore.isIdle) return false
return executionStore.executingNodeIds.includes(props.nodeId)
if (!parentNodeId) return executionStore.executingNodeIds.length > 0
return executionStore.executingNodeIds.includes(parentNodeId)
})
const formattedText = computed(() => {
const src = modelValue.value
@@ -63,4 +64,19 @@ const formattedText = computed(() => {
ALLOWED_ATTR: ['href', 'target', 'rel']
})
})
let parentNodeId: NodeId | null = null
onMounted(() => {
// Get the parent node ID from props if provided
// For backward compatibility, fall back to the first executing node
parentNodeId = props.nodeId ?? parentNodeId
})
// Lazily adopt the first executing node as the parent when no nodeId is known.
watch(
() => executionStore.executingNodeIds,
(ids) => {
if (!parentNodeId && ids.length > 0) parentNodeId = ids[0]
}
)
</script>

View File

@@ -5,7 +5,6 @@ import { defineComponent, ref } from 'vue'
import { createI18n } from 'vue-i18n'
import type { Bounds } from '@/renderer/core/layout/types'
import { toNodeId } from '@/types/nodeId'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import type { Ref } from 'vue'
@@ -133,12 +132,11 @@ 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, nodeId }),
setup: () => ({ value, widget }),
template:
'<WidgetImageCrop v-model="value" :widget="widget" :node-id="nodeId" />'
'<WidgetImageCrop v-model="value" :widget="widget" :node-id="1" />'
})
const utils = render(Harness, {
global: {
@@ -235,7 +233,7 @@ describe('WidgetImageCrop', () => {
renderWidget(
makeWidget({
options: { disabled: true },
linkedUpstream: { nodeId: toNodeId('n1') }
linkedUpstream: { nodeId: '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,8 +6,6 @@ import { createI18n } from 'vue-i18n'
import Load3D from '@/components/load3d/Load3D.vue'
import type { ComponentWidget } from '@/scripts/domWidget'
import { toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
const { load3dState, resolveNodeMock, settingGetMock } = vi.hoisted(() => ({
load3dState: {
@@ -85,7 +83,7 @@ const i18n = createI18n({
type RenderOptions = {
widget?: unknown
nodeId?: NodeId
nodeId?: number | string
stateOverrides?: Partial<ReturnType<typeof buildLoad3dStub>>
enable3DViewer?: boolean
}
@@ -167,17 +165,16 @@ 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 })
renderLoad3D({ widget: {}, nodeId: 42 })
expect(resolveNodeMock).toHaveBeenCalledWith(nodeId)
expect(resolveNodeMock).toHaveBeenCalledWith(42)
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: toNodeId(99) })
renderLoad3D({ widget: {}, nodeId: 99 })
await Promise.resolve()
expect(screen.queryByTestId('load3d-scene')).not.toBeInTheDocument()
@@ -222,11 +219,7 @@ describe('Load3D', () => {
it('hides ViewerControls when there is no node even if the setting is on', () => {
resolveNodeMock.mockReturnValue(null)
renderLoad3D({
widget: {},
nodeId: toNodeId(1),
enable3DViewer: true
})
renderLoad3D({ widget: {}, nodeId: 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 { ComponentWidget } from '@/scripts/domWidget'
import type { NodeId } from '@/types/nodeId'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import { resolveNode } from '@/utils/litegraphUtil'
import type { ComponentWidget } from '@/scripts/domWidget'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
const {
widget,

View File

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

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,12 +289,11 @@ 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: NodeId
nodeId: string
}>()
const modelValue = defineModel<string>({ default: '' })

View File

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

View File

@@ -2,7 +2,6 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import ErrorNodeCard from './ErrorNodeCard.vue'
import type { ErrorCardData } from './types'
import { createNodeExecutionId } from '@/types/nodeIdentification'
import { toNodeId } from '@/types/nodeId'
const meta: Meta<typeof ErrorNodeCard> = {
title: 'RightSidePanel/Errors/ErrorNodeCard',
@@ -25,7 +24,7 @@ type Story = StoryObj<typeof meta>
const singleErrorCard: ErrorCardData = {
id: 'node-10',
title: 'CLIPTextEncode',
nodeId: createNodeExecutionId([toNodeId(10)]),
nodeId: createNodeExecutionId([10]),
nodeTitle: 'CLIP Text Encode (Prompt)',
isSubgraphNode: false,
errors: [
@@ -39,7 +38,7 @@ const singleErrorCard: ErrorCardData = {
const multipleErrorsCard: ErrorCardData = {
id: 'node-24',
title: 'VAEDecode',
nodeId: createNodeExecutionId([toNodeId(24)]),
nodeId: createNodeExecutionId([24]),
nodeTitle: 'VAE Decode',
isSubgraphNode: false,
errors: [
@@ -57,7 +56,7 @@ const multipleErrorsCard: ErrorCardData = {
const runtimeErrorCard: ErrorCardData = {
id: 'exec-45',
title: 'KSampler',
nodeId: createNodeExecutionId([toNodeId(45)]),
nodeId: createNodeExecutionId([45]),
nodeTitle: 'KSampler',
isSubgraphNode: false,
errors: [
@@ -77,7 +76,7 @@ const runtimeErrorCard: ErrorCardData = {
const subgraphErrorCard: ErrorCardData = {
id: 'node-3:15',
title: 'KSampler',
nodeId: createNodeExecutionId([toNodeId(3), toNodeId(15)]),
nodeId: createNodeExecutionId([3, 15]),
nodeTitle: 'Nested KSampler',
isSubgraphNode: true,
errors: [

View File

@@ -7,7 +7,6 @@ import { createI18n } from 'vue-i18n'
import ErrorNodeCard from './ErrorNodeCard.vue'
import type { ErrorCardData } from './types'
import { createNodeExecutionId } from '@/types/nodeIdentification'
import { toNodeId } from '@/types/nodeId'
const mockGetLogs = vi.fn(() => Promise.resolve('mock server logs'))
const mockSerialize = vi.fn(() => ({ nodes: [] }))
@@ -158,7 +157,7 @@ describe('ErrorNodeCard.vue', () => {
return {
id: `exec-${++cardIdCounter}`,
title: 'KSampler',
nodeId: createNodeExecutionId([toNodeId(10)]),
nodeId: createNodeExecutionId([10]),
nodeTitle: 'KSampler',
errors: [
{
@@ -251,7 +250,7 @@ describe('ErrorNodeCard.vue', () => {
renderCard({
id: `node-${++cardIdCounter}`,
title: 'KSampler',
nodeId: createNodeExecutionId([toNodeId(10)]),
nodeId: createNodeExecutionId([10]),
nodeTitle: 'KSampler',
errors: [
{
@@ -389,7 +388,7 @@ describe('ErrorNodeCard.vue', () => {
const card: ErrorCardData = {
id: `exec-${++cardIdCounter}`,
title: 'KSampler',
nodeId: createNodeExecutionId([toNodeId(10)]),
nodeId: createNodeExecutionId([10]),
nodeTitle: 'KSampler',
errors: [
{

View File

@@ -6,7 +6,6 @@ import type { useSystemStatsStore } from '@/stores/systemStatsStore'
import type { ErrorCardData } from './types'
import { createNodeExecutionId } from '@/types/nodeIdentification'
import { useErrorReport } from './useErrorReport'
import { toNodeId } from '@/types/nodeId'
async function flushPromises() {
await new Promise((resolve) => setTimeout(resolve, 0))
@@ -105,7 +104,7 @@ function makeCard(overrides: Partial<ErrorCardData> = {}): ErrorCardData {
return {
id: 'card-1',
title: 'KSampler',
nodeId: createNodeExecutionId([toNodeId(42)]),
nodeId: createNodeExecutionId([42]),
errors: [],
...overrides
}
@@ -183,7 +182,7 @@ describe('useErrorReport', () => {
exceptionType: 'RuntimeError',
exceptionMessage: 'CUDA oom',
traceback: 'trace-0',
nodeId: createNodeExecutionId([toNodeId(42)]),
nodeId: createNodeExecutionId([42]),
nodeType: 'KSampler',
systemStats: sampleSystemStats,
serverLogs: 'server logs',

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,6 @@ 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 { toNodeId } from '@/types/nodeId'
import { searchWidgets } from '../shared'
import type { NodeWidgetsList } from '../shared'
@@ -83,7 +82,7 @@ const advancedInputsWidgets = computed((): NodeWidgetsList => {
return allInteriorWidgets.filter(
({ node: interiorNode, widget }) =>
!isWidgetPromotedOnSubgraphNode(node, {
sourceNodeId: toNodeId(interiorNode.id),
sourceNodeId: String(interiorNode.id),
sourceWidgetName: getWidgetName(widget)
})
)

View File

@@ -19,7 +19,6 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
import { toNodeId } from '@/types/nodeId'
import { getWidgetDefaultValue, promptWidgetLabel } from '@/utils/widgetUtil'
import type { WidgetValue } from '@/utils/widgetUtil'
@@ -91,10 +90,9 @@ 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 : currentNodeId
String(node.id) === String(parent.id) ? source.nodeId : String(node.id)
demotePromotedInput(parent, {
sourceNodeId,
sourceWidgetName: source.widgetName

View File

@@ -10,7 +10,6 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { widgetId } from '@/types/widgetId'
import WidgetItem from './WidgetItem.vue'
import { toNodeId } from '@/types/nodeId'
const { mockGetInputSpecForWidget, StubWidgetComponent } = vi.hoisted(() => ({
mockGetInputSpecForWidget: vi.fn(),
@@ -146,7 +145,7 @@ describe('WidgetItem', () => {
const expectedOptions = {
values: ['model_a.safetensors', 'model_b.safetensors']
}
const id = widgetId('test-graph-id', toNodeId(1), 'ckpt_name')
const id = widgetId('test-graph-id', 1, 'ckpt_name')
const widget = createMockWidget({ widgetId: id, name: 'ckpt_name' })
useWidgetValueStore().registerWidget(id, {
type: 'combo',
@@ -161,7 +160,7 @@ describe('WidgetItem', () => {
})
it('passes type from widget state to the widget component', () => {
const id = widgetId('test-graph-id', toNodeId(1), 'ckpt_name')
const id = widgetId('test-graph-id', 1, 'ckpt_name')
const widget = createMockWidget({ widgetId: id, type: 'string' })
useWidgetValueStore().registerWidget(id, {
type: 'combo',
@@ -176,7 +175,7 @@ describe('WidgetItem', () => {
})
it('passes name from widget state to the widget component', () => {
const id = widgetId('test-graph-id', toNodeId(1), 'ckpt_name')
const id = widgetId('test-graph-id', 1, 'ckpt_name')
const widget = createMockWidget({ widgetId: id, name: 'source_name' })
useWidgetValueStore().registerWidget(id, {
type: 'combo',
@@ -191,7 +190,7 @@ describe('WidgetItem', () => {
})
it('passes value from widget state to the widget component', () => {
const id = widgetId('test-graph-id', toNodeId(1), 'ckpt_name')
const id = widgetId('test-graph-id', 1, 'ckpt_name')
const widget = createMockWidget({ widgetId: id, value: 'source value' })
useWidgetValueStore().registerWidget(id, {
type: 'combo',

View File

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

View File

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

View File

@@ -32,7 +32,6 @@ 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 { toNodeId } from '@/types/nodeId'
import { cn } from '@comfyorg/tailwind-utils'
import SubgraphNodeWidget from './SubgraphNodeWidget.vue'
@@ -117,7 +116,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.getNodeById(exposure.sourceNodeId)
const sourceNode = node.subgraph._nodes_by_id[exposure.sourceNodeId]
if (!sourceNode) return []
const realWidget = getPromotableWidgets(sourceNode).find(
(candidate) => candidate.name === exposure.sourcePreviewName
@@ -249,7 +248,7 @@ function rowDisplayName(row: ActiveRow): string {
function isRowLinked(row: ActiveRow): boolean {
if (row.kind !== 'promoted') return false
if (row.node.id === toNodeId(-1)) return true
if (row.node.id === -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: SerializedNodeId
nodeId: NodeId
mediaType: string
id?: string
url?: string
@@ -63,7 +63,7 @@ describe('MediaLightbox', () => {
filename: 'image1.jpg',
subfolder: 'outputs',
type: 'output',
nodeId: '123',
nodeId: '123' as NodeId,
mediaType: 'images',
isImage: true,
isVideo: false,
@@ -75,7 +75,7 @@ describe('MediaLightbox', () => {
filename: 'image2.jpg',
subfolder: 'outputs',
type: 'output',
nodeId: '456',
nodeId: '456' as NodeId,
mediaType: 'images',
isImage: true,
isVideo: false,
@@ -87,7 +87,7 @@ describe('MediaLightbox', () => {
filename: 'image3.jpg',
subfolder: 'outputs',
type: 'output',
nodeId: '789',
nodeId: '789' as NodeId,
mediaType: 'images',
isImage: true,
isVideo: false,

View File

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

View File

@@ -18,7 +18,6 @@ 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
@@ -40,7 +39,7 @@ interface UseBoundingBoxesOptions {
}
export function useBoundingBoxes(
nodeId: NodeId,
nodeId: string,
{
canvasEl,
canvasContainer,
@@ -64,7 +63,9 @@ export function useBoundingBoxes(
nodeId && app.canvas?.graph ? app.canvas.graph.getNodeById(nodeId) : null
)
const { selectedNodeIds } = storeToRefs(useCanvasStore())
const isNodeSelected = computed(() => selectedNodeIds.value.has(nodeId))
const isNodeSelected = computed(() =>
selectedNodeIds.value.has(String(nodeId))
)
function dimWidget(name: 'width' | 'height'): number | undefined {
const v = litegraphNode.value?.widgets?.find((w) => w.name === name)?.value

View File

@@ -11,7 +11,6 @@ import {
} from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { NodeId } from '@/renderer/core/layout/types'
import { toNodeId } from '@/types/nodeId'
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
const mockApp = vi.hoisted(() => ({
@@ -39,7 +38,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 = toNodeId(id)
node.id = id
node.mode = mode
return node
}
@@ -70,7 +69,7 @@ class MockNode implements Positionable {
) {
this.pos = pos
this.size = size
this.id = toNodeId('mock-node')
this.id = 'mock-node'
this.boundingRect = [0, 0, 0, 0]
}

View File

@@ -12,7 +12,6 @@ import {
} from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { toNodeId } from '@/types/nodeId'
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
import { computeUnionBounds } from '@/utils/mathUtil'
@@ -101,7 +100,8 @@ export function useSelectionToolboxPosition(
if (item.id == null) continue
if (shouldRenderVueNodes.value && typeof item.id === 'string') {
const layout = layoutStore.getNodeLayoutRef(toNodeId(item.id)).value
// Use layout store for Vue nodes (only works with string IDs)
const layout = layoutStore.getNodeLayoutRef(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,14 +1,12 @@
import { useSelectionState } from '@/composables/graph/useSelectionState'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, NodeId } 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 { toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
export type ArrangeLayout = 'vertical' | 'horizontal' | 'grid'
@@ -41,7 +39,7 @@ const titleHeightOf = (node: LGraphNode): number => {
const toBox = (node: LGraphNode): NodeBox => {
const titleHeight = titleHeightOf(node)
return {
id: toNodeId(node.id),
id: node.id,
posX: node.pos[0],
posY: node.pos[1],
visualWidth: node.size[0],

View File

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

View File

@@ -35,7 +35,6 @@ import { getCnrIdFromNode } from '@/platform/nodeReplacement/cnrIdUtil'
import { app } from '@/scripts/app'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { appendNodeExecutionId } from '@/types/nodeIdentification'
import { toNodeId } from '@/types/nodeId'
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
import {
collectAllNodes,
@@ -323,7 +322,7 @@ function handleNodeModeChange(
// Find the node by local ID in the graph that fired the event,
// then compute its execution ID relative to the root graph.
const node = localGraph.getNodeById(nodeId === -1 ? null : toNodeId(nodeId))
const node = localGraph.getNodeById(nodeId)
if (!node) return
const execId = getExecutionIdByNode(app.rootGraph, node)

View File

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

View File

@@ -21,8 +21,7 @@ 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 { toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import type { NodeId } from '@/renderer/core/layout/types'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { isDOMWidget } from '@/scripts/domWidget'
import { IS_CONTROL_WIDGET } from '@/scripts/widgets'
@@ -31,6 +30,7 @@ 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?: NodeId
originNodeId?: string
originOutputName?: string
type: string
}
@@ -84,6 +84,7 @@ export interface SafeWidgetData {
advanced?: boolean
hidden?: boolean
read_only?: boolean
removable?: boolean
values?: unknown
}
/** Input specification from node definition */
@@ -136,10 +137,10 @@ export interface VueNodeData {
export interface GraphNodeManager {
// Reactive state - safe data extracted from LiteGraph nodes
vueNodeData: ReadonlyMap<NodeId, VueNodeData>
vueNodeData: ReadonlyMap<string, VueNodeData>
// Access to original LiteGraph nodes (non-reactive)
getNode(id: NodeId): LGraphNode | undefined
getNode(id: WorkflowNodeId): LGraphNode | undefined
// Lifecycle methods
cleanup(): void
@@ -213,7 +214,8 @@ function extractWidgetDisplayOptions(
canvasOnly: widget.options.canvasOnly,
advanced: widget.options?.advanced ?? widget.advanced,
hidden: widget.options.hidden,
read_only: widget.options.read_only
read_only: widget.options.read_only,
removable: widget.options.removable
}
}
@@ -353,14 +355,14 @@ function buildSlotMetadata(
): Map<string, WidgetSlotMetadata> {
const metadata = new Map<string, WidgetSlotMetadata>()
inputs?.forEach((input, index) => {
let originNodeId: NodeId | undefined
let originNodeId: string | 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 = toNodeId(link.origin_id)
originNodeId = String(link.origin_id)
originOutputName = originNode.outputs?.[link.origin_slot]?.name
}
}
@@ -471,7 +473,7 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
const badges = node.badges
return {
id: toNodeId(node.id),
id: String(node.id),
title: typeof node.title === 'string' ? node.title : '',
type: nodeType,
mode: node.mode || 0,
@@ -498,12 +500,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<NodeId, VueNodeData>())
const vueNodeData = reactive(new Map<string, VueNodeData>())
// Non-reactive storage for original LiteGraph nodes
const nodeRefs = new Map<NodeId, LGraphNode>()
const nodeRefs = new Map<string, LGraphNode>()
const refreshNodeSlots = (nodeId: NodeId) => {
const refreshNodeSlots = (nodeId: string) => {
const nodeRef = nodeRefs.get(nodeId)
const currentData = vueNodeData.get(nodeId)
@@ -518,14 +520,14 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
}
// Get access to original LiteGraph node (non-reactive)
const getNode = (id: NodeId): LGraphNode | undefined => {
return nodeRefs.get(id)
const getNode = (id: WorkflowNodeId): LGraphNode | undefined => {
return nodeRefs.get(String(id))
}
const syncWithGraph = () => {
if (!graph?._nodes) return
const currentNodes = new Set(graph._nodes.map((n) => toNodeId(n.id)))
const currentNodes = new Set(graph._nodes.map((n) => String(n.id)))
// Remove deleted nodes
for (const id of Array.from(vueNodeData.keys())) {
@@ -537,7 +539,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Add/update existing nodes
graph._nodes.forEach((node) => {
const id = toNodeId(node.id)
const id = String(node.id)
// Store non-reactive reference
nodeRefs.set(id, node)
@@ -555,7 +557,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
node: LGraphNode,
originalCallback?: (node: LGraphNode) => void
) => {
const id = toNodeId(node.id)
const id = String(node.id)
// Store non-reactive reference to original node
nodeRefs.set(id, node)
@@ -610,7 +612,8 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
}
}
const dropNodeReferences = (id: NodeId) => {
const dropNodeReferences = (node: LGraphNode) => {
const id = String(node.id)
nodeRefs.delete(id)
vueNodeData.delete(id)
}
@@ -619,12 +622,9 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
node: LGraphNode,
originalCallback?: (node: LGraphNode) => void
) => {
const id = toNodeId(node.id)
// Remove node from layout store
const id = String(node.id)
setSource(LayoutSource.Canvas)
void deleteNode(id)
dropNodeReferences(id)
originalCallback?.(node)
}
@@ -672,7 +672,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
const beforeNodeRemovedListener = (
e: CustomEvent<{ node: LGraphNode }>
) => {
dropNodeReferences(toNodeId(e.detail.node.id))
dropNodeReferences(e.detail.node)
}
graph.events.addEventListener(
'node:before-removed',
@@ -683,7 +683,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
[K in LGraphTriggerAction]: (event: LGraphTriggerParam<K>) => void
} = {
'node:property:changed': (propertyEvent) => {
const nodeId = toNodeId(propertyEvent.nodeId)
const nodeId = String(propertyEvent.nodeId)
const currentData = vueNodeData.get(nodeId)
if (currentData) {
@@ -779,15 +779,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
}
},
'node:slot-errors:changed': (slotErrorsEvent) => {
refreshNodeSlots(toNodeId(slotErrorsEvent.nodeId))
refreshNodeSlots(String(slotErrorsEvent.nodeId))
},
'node:slot-links:changed': (slotLinksEvent) => {
if (slotLinksEvent.slotType === NodeSlotType.INPUT) {
refreshNodeSlots(toNodeId(slotLinksEvent.nodeId))
refreshNodeSlots(String(slotLinksEvent.nodeId))
}
},
'node:slot-label:changed': (slotLabelEvent) => {
const nodeId = toNodeId(slotLabelEvent.nodeId)
const nodeId = String(slotLabelEvent.nodeId)
const nodeRef = nodeRefs.get(nodeId)
if (!nodeRef) return

View File

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

View File

@@ -8,7 +8,6 @@ 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 { toNodeId } from '@/types/nodeId'
// canvasStore transitively imports the app singleton; stub it so the real
// ComfyApp module never loads during these unit tests.
@@ -46,7 +45,7 @@ const i18n = createI18n({
const nodeWithMode = (mode: LGraphEventMode, id = 1): LGraphNode => {
const node = new LGraphNode('Test')
node.id = toNodeId(id)
node.id = 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,
id: node.id.toString(),
pos: [node.pos[0], node.pos[1]] as [number, number],
size: [node.size[0], node.size[1]] as [number, number]
}))
@@ -45,7 +45,6 @@ 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,5 +1,4 @@
import { useMaskEditorDataStore } from '@/stores/maskEditorDataStore'
import { toNodeId } from '@/types/nodeId'
import type { ImageRef, ImageLayer } from '@/stores/maskEditorDataStore'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
@@ -179,7 +178,7 @@ export function useMaskEditorLoader() {
maskLayer,
paintLayer,
sourceRef,
nodeId: toNodeId(node.id)
nodeId: node.id
}
dataStore.sourceNode = node

View File

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

View File

@@ -24,8 +24,6 @@ import type {
WidgetDependency
} from '@/schemas/nodeDefSchema'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import type { Expression } from 'jsonata'
import jsonata from 'jsonata'
@@ -454,17 +452,18 @@ 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<NodeId, Ref<number>>()
const nodeRevisions = new Map<string, 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: NodeId): Ref<number> => {
let rev = nodeRevisions.get(nodeId)
const getNodeRevisionRef = (nodeId: string | number): Ref<number> => {
const key = String(nodeId)
let rev = nodeRevisions.get(key)
if (!rev) {
rev = ref(0)
nodeRevisions.set(nodeId, rev)
nodeRevisions.set(key, rev)
}
return rev
}
@@ -512,7 +511,7 @@ const scheduleEvaluation = (
if (LiteGraph.vueNodesMode) {
// VueNodes mode: bump per-node revision (only this node re-renders)
getNodeRevisionRef(toNodeId(node.id)).value++
getNodeRevisionRef(node.id).value++
}
pricingTick.value++
})

View File

@@ -11,7 +11,6 @@ import {
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { createNodeLocatorId } from '@/types/nodeIdentification'
import { toNodeId } from '@/types/nodeId'
import { CANVAS_IMAGE_PREVIEW_WIDGET } from './canvasImagePreviewTypes'
import { usePromotedPreviews } from './usePromotedPreviews'
@@ -59,7 +58,7 @@ function addInteriorNode(
} = { id: 10 }
): LGraphNode {
const node = new LGraphNode('test')
node.id = toNodeId(options.id)
node.id = options.id
if (options.previewMediaType) {
node.previewMediaType = options.previewMediaType
}
@@ -70,7 +69,7 @@ function addInteriorNode(
function seedOutputs(subgraphId: string, nodeIds: Array<number | string>) {
const store = useNodeOutputStore()
for (const nodeId of nodeIds) {
const locatorId = createNodeLocatorId(subgraphId, toNodeId(nodeId))
const locatorId = createNodeLocatorId(subgraphId, nodeId)
store.nodeOutputs[locatorId] = {
images: [{ filename: 'output.png' }]
}
@@ -83,7 +82,7 @@ function seedPreviewImages(
) {
const store = useNodeOutputStore()
for (const { nodeId, urls } of entries) {
const locatorId = createNodeLocatorId(subgraphId, toNodeId(nodeId))
const locatorId = createNodeLocatorId(subgraphId, nodeId)
store.nodePreviewImages[locatorId] = urls
}
}
@@ -233,9 +232,7 @@ describe(usePromotedPreviews, () => {
exposePreview(setup, '10')
const blobUrl = 'blob:http://localhost/glsl-preview'
seedPreviewImages(setup.subgraph.id, [
{ nodeId: toNodeId(10), urls: [blobUrl] }
])
seedPreviewImages(setup.subgraph.id, [{ nodeId: 10, urls: [blobUrl] }])
vi.mocked(useNodeOutputStore().getNodeImageUrls).mockReturnValue([blobUrl])
const { promotedPreviews } = usePromotedPreviews(() => setup.subgraphNode)
@@ -258,9 +255,7 @@ describe(usePromotedPreviews, () => {
expect(promotedPreviews.value).toEqual([])
const blobUrl = 'blob:http://localhost/glsl-preview'
seedPreviewImages(setup.subgraph.id, [
{ nodeId: toNodeId(10), urls: [blobUrl] }
])
seedPreviewImages(setup.subgraph.id, [{ nodeId: 10, urls: [blobUrl] }])
vi.mocked(useNodeOutputStore().getNodeImageUrls).mockReturnValue([blobUrl])
expect(promotedPreviews.value).toEqual([

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ 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'
@@ -32,7 +31,7 @@ interface UsePainterOptions {
modelValue: Ref<string>
}
export function usePainter(nodeId: NodeId, options: UsePainterOptions) {
export function usePainter(nodeId: string, options: UsePainterOptions) {
const { canvasEl, cursorEl, modelValue } = options
const { t } = useI18n()
const nodeOutputStore = useNodeOutputStore()

View File

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

View File

@@ -2,10 +2,9 @@ import { useResizeObserver } from '@vueuse/core'
import type { Ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode, NodeId } 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 { toNodeId } from '@/types/nodeId'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
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: toNodeId(1),
nodeId: '1' as NodeId,
options: {},
y: 0
}

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ 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'
@@ -14,7 +13,7 @@ import { resolveSubgraphInputTarget } from './resolveSubgraphInputTarget'
* on the projected widget.
*/
export interface PromotedSource {
nodeId: NodeId
nodeId: string
widgetName: string
}
@@ -110,6 +109,7 @@ 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,6 +1,5 @@
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
@@ -13,6 +12,6 @@ export interface ResolvedPromotedWidget {
* the source is a stored tuple rather than something link-derivable.
*/
export interface PromotedWidgetSource {
sourceNodeId: NodeId
sourceNodeId: string
sourceWidgetName: string
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,9 @@
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import { zAutogrowOptions, zMatchTypeOptions } from '@/schemas/nodeDefSchema'
import {
zAutogrowOptions,
zDynamicGroupInputSpec,
zMatchTypeOptions
} from '@/schemas/nodeDefSchema'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -8,6 +12,7 @@ const dynamicTypeResolvers: Record<
(inputSpec: InputSpecV2) => string[]
> = {
COMFY_AUTOGROW_V3: resolveAutogrowType,
COMFY_DYNAMICGROUP_V3: resolveDynamicGroupType,
COMFY_MATCHTYPE_V3: (input) =>
zMatchTypeOptions
.safeParse(input)
@@ -20,6 +25,21 @@ export function resolveInputType(input: InputSpecV2): string[] {
: input.type.split(',')
}
function resolveDynamicGroupType(rawSpec: InputSpecV2): string[] {
const parsed = zDynamicGroupInputSpec.safeParse([rawSpec.type, rawSpec])
const template = parsed.data?.[1]?.template
if (!template) return []
const inputTypes: (Record<string, InputSpec> | undefined)[] = [
template.required,
template.optional
]
return inputTypes.flatMap((inputType) =>
Object.entries(inputType ?? {}).flatMap(([name, v]) =>
resolveInputType(transformInputSpecV1ToV2(v, { name }))
)
)
}
function resolveAutogrowType(rawSpec: InputSpecV2): string[] {
const { input } = zAutogrowOptions.safeParse(rawSpec).data?.template ?? {}

View File

@@ -1,7 +1,9 @@
import { setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { describe, expect, test, vi } from 'vitest'
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { Point } from '@/lib/litegraph/src/interfaces'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import { useLitegraphService } from '@/services/litegraphService'
@@ -47,6 +49,22 @@ function addDynamicCombo(node: LGraphNode, inputs: DynamicInputs) {
transformInputSpecV1ToV2(inputSpec, { name: namePrefix, isOptional: false })
)
}
function addDynamicGroup(
node: LGraphNode,
template: object,
{ min, max, name = 'g' }: { min?: number; max?: number; name?: string } = {}
) {
const options: Record<string, unknown> = { template }
if (min !== undefined) options.min = min
if (max !== undefined) options.max = max
addNodeInput(
node,
transformInputSpecV1ToV2(['COMFY_DYNAMICGROUP_V3', options] as InputSpec, {
name,
isOptional: false
})
)
}
function addAutogrow(node: LGraphNode, template: unknown) {
addNodeInput(
node,
@@ -287,3 +305,101 @@ describe('Autogrow', () => {
])
})
})
describe('Dynamic Groups', () => {
const stringTemplate = { required: { a: ['STRING', {}] } }
const widgetNames = (node: LGraphNode) => node.widgets!.map((w) => w.name)
const inputNames = (node: LGraphNode) => node.inputs.map((i) => i.name)
const widgetNamed = (node: LGraphNode, name: string) =>
node.widgets!.find((w) => w.name === name)!
test('renders min rows on creation', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 2, max: 5 })
expect(widgetNames(node)).toStrictEqual([
'g',
'g.__row__0',
'g.0.a',
'g.__row__1',
'g.1.a'
])
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
})
test('add row appends a new row up to max', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 0, max: 2 })
expect(widgetNames(node)).toStrictEqual(['g'])
widgetNamed(node, 'g').callback?.(undefined)
expect(inputNames(node)).toStrictEqual(['g.0.a'])
widgetNamed(node, 'g').callback?.(undefined)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
// At max, further adds are ignored.
widgetNamed(node, 'g').callback?.(undefined)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
})
test('remove row renumbers later rows', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 0, max: 5 })
widgetNamed(node, 'g').callback?.(undefined)
widgetNamed(node, 'g').callback?.(undefined)
widgetNamed(node, 'g').callback?.(undefined)
const row0Field = widgetNamed(node, 'g.0.a')
const row2Field = widgetNamed(node, 'g.2.a')
widgetNamed(node, 'g.__row__1').callback?.(undefined)
expect(widgetNames(node)).toStrictEqual([
'g',
'g.__row__0',
'g.0.a',
'g.__row__1',
'g.1.a'
])
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
// Row 0 is untouched; the former row 2 shifts down into row 1.
expect(widgetNamed(node, 'g.0.a')).toBe(row0Field)
expect(widgetNamed(node, 'g.1.a')).toBe(row2Field)
})
test('rows below min are not removable', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 1, max: 5 })
widgetNamed(node, 'g').callback?.(undefined)
expect(widgetNamed(node, 'g.__row__0').options?.removable).toBe(false)
expect(widgetNamed(node, 'g.__row__1').options?.removable).toBe(true)
// Attempting to remove a protected row is a no-op.
widgetNamed(node, 'g.__row__0').callback?.(undefined)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
})
test('canvas click removes a row only on the remove hit target', () => {
const node = testNode()
addDynamicGroup(node, stringTemplate, { min: 0, max: 5 })
widgetNamed(node, 'g').callback?.(undefined)
widgetNamed(node, 'g').callback?.(undefined)
const header = widgetNamed(node, 'g.__row__1')
const up = { type: 'pointerup' } as CanvasPointerEvent
const down = { type: 'pointerdown' } as CanvasPointerEvent
const xCenter = node.size[0] - 15 - LiteGraph.NODE_WIDGET_HEIGHT * 0.5
// Releasing away from the remove target does nothing.
header.mouse?.(up, [0, 0] as Point, node)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
// A pointerdown on the target does nothing (only release acts).
header.mouse?.(down, [xCenter, 0] as Point, node)
expect(inputNames(node)).toStrictEqual(['g.0.a', 'g.1.a'])
// Releasing on the target removes the row.
header.mouse?.(up, [xCenter, 0] as Point, node)
expect(inputNames(node)).toStrictEqual(['g.0.a'])
})
})

View File

@@ -2,10 +2,12 @@ import { remove } from 'es-toolkit'
import { shallowReactive } from 'vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { t } from '@/i18n'
import type {
ISlotType,
INodeInputSlot,
INodeOutputSlot
INodeOutputSlot,
Point
} from '@/lib/litegraph/src/interfaces'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
@@ -13,11 +15,14 @@ import type { LLink } from '@/lib/litegraph/src/LLink'
import { commonType } from '@/lib/litegraph/src/utils/type'
import { resolveNodeRootGraphId } from '@/lib/litegraph/src/utils/widget'
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type { ComboInputSpec, InputSpec } from '@/schemas/nodeDefSchema'
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
import {
zAutogrowOptions,
zDynamicComboInputSpec,
zDynamicGroupInputSpec,
zMatchTypeOptions
} from '@/schemas/nodeDefSchema'
import { useLitegraphService } from '@/services/litegraphService'
@@ -28,6 +33,15 @@ import { widgetId } from '@/types/widgetId'
const INLINE_INPUTS = false
type DynamicGroupState = {
min: number
max: number
inputSpecs: InputSpecV2[]
}
type DynamicGroupNode = LGraphNode & {
comfyDynamic: { dynamicGroup: Record<string, DynamicGroupState> }
}
type MatchTypeNode = LGraphNode &
Pick<Required<LGraphNode>, 'onConnectionsChange'> & {
comfyDynamic: { matchType: Record<string, Record<string, string>> }
@@ -210,7 +224,321 @@ function dynamicComboWidget(
return { widget, minWidth, minHeight }
}
export const dynamicWidgets = { COMFY_DYNAMICCOMBO_V3: dynamicComboWidget }
function withComfyDynamicGroup(
node: LGraphNode
): asserts node is DynamicGroupNode {
if (node.comfyDynamic?.dynamicGroup) return
node.comfyDynamic ??= {}
node.comfyDynamic.dynamicGroup = {}
}
const ROW_MARKER = '__row__'
const rowHeaderName = (group: string, row: number) =>
`${group}.${ROW_MARKER}${row}`
const fieldName = (group: string, row: number, field: string) =>
`${group}.${row}.${field}`
/** Extract the row index from a header widget name, or `undefined`. */
function headerRowIndex(group: string, name: string): number | undefined {
const prefix = `${group}.${ROW_MARKER}`
if (!name.startsWith(prefix)) return undefined
const row = Number(name.slice(prefix.length))
return Number.isInteger(row) ? row : undefined
}
/** Rename a field that sits above the removed row, shifting its index down. */
function shiftedFieldName(
group: string,
name: string,
removedRow: number
): string | undefined {
const prefix = `${group}.`
if (!name.startsWith(prefix)) return undefined
const rest = name.slice(prefix.length)
const dot = rest.indexOf('.')
if (dot === -1) return undefined
const row = Number(rest.slice(0, dot))
if (!Number.isInteger(row) || row <= removedRow) return undefined
return fieldName(group, row - 1, rest.slice(dot + 1))
}
const belongsToRow = (group: string, name: string, row: number): boolean =>
name === rowHeaderName(group, row) || name.startsWith(`${group}.${row}.`)
const CANVAS_MARGIN = 15
/** Draw the "Add row" capsule button on the LiteGraph canvas. */
function drawGroupButton(
ctx: CanvasRenderingContext2D,
width: number,
y: number,
label: string,
disabled: boolean
): void {
const height = LiteGraph.NODE_WIDGET_HEIGHT
ctx.save()
if (disabled) ctx.globalAlpha *= 0.5
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR
ctx.strokeStyle = LiteGraph.WIDGET_OUTLINE_COLOR
ctx.beginPath()
ctx.roundRect(CANVAS_MARGIN, y, width - CANVAS_MARGIN * 2, height, [
height * 0.5
])
ctx.fill()
if (!disabled) ctx.stroke()
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR
ctx.font = `${LiteGraph.NODE_TEXT_SIZE}px ${LiteGraph.NODE_FONT}`
ctx.textAlign = 'center'
ctx.fillText(label, width * 0.5, y + height * 0.7)
ctx.restore()
}
/** Horizontal centre of a row header's remove (✕) hit target. */
const removeButtonCenterX = (width: number) =>
width - CANVAS_MARGIN - LiteGraph.NODE_WIDGET_HEIGHT * 0.5
/** Draw a row header (label on the left, ✕ on the right) on the canvas. */
function drawGroupRowHeader(
ctx: CanvasRenderingContext2D,
width: number,
y: number,
label: string,
removable: boolean
): void {
const height = LiteGraph.NODE_WIDGET_HEIGHT
ctx.save()
ctx.font = `${LiteGraph.NODE_TEXT_SIZE}px ${LiteGraph.NODE_FONT}`
ctx.fillStyle = LiteGraph.WIDGET_SECONDARY_TEXT_COLOR
ctx.textAlign = 'left'
ctx.fillText(label, CANVAS_MARGIN, y + height * 0.7)
if (removable) {
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR
ctx.textAlign = 'center'
ctx.fillText('\u2715', removeButtonCenterX(width), y + height * 0.7)
}
ctx.restore()
}
const countGroupRows = (group: string, node: LGraphNode): number =>
(node.widgets ?? []).reduce(
(count, w) =>
headerRowIndex(group, w.name) !== undefined ? count + 1 : count,
0
)
/** Build a row's header + field widgets, returning them detached from the node. */
function createRow(
group: string,
row: number,
state: DynamicGroupState,
node: DynamicGroupNode
): IBaseWidget[] {
const { addNodeInput } = useLitegraphService()
const startLen = node.widgets!.length
const header = node.addCustomWidget({
name: rowHeaderName(group, row),
type: 'dynamic_group_row',
value: row,
y: 0,
serialize: false,
callback: undefined as IBaseWidget['callback'],
draw(
this: IBaseWidget,
ctx: CanvasRenderingContext2D,
_node: LGraphNode,
width: number,
y: number
) {
const idx = headerRowIndex(group, this.name) ?? 0
const label = t('dynamicGroup.row', { index: idx + 1 })
drawGroupRowHeader(ctx, width, y, label, !!this.options?.removable)
},
mouse(this: IBaseWidget, event: CanvasPointerEvent, pos: Point) {
if (event.type !== 'pointerup' || !this.options?.removable) return false
const half = LiteGraph.NODE_WIDGET_HEIGHT * 0.5
if (Math.abs(pos[0] - removeButtonCenterX(node.size[0])) > half)
return false
const idx = headerRowIndex(group, this.name)
if (idx !== undefined) removeRow(group, idx, node)
return true
},
options: { serialize: false, socketless: true, removable: row >= state.min }
})
header.callback = function (this: IBaseWidget) {
const idx = headerRowIndex(group, this.name)
if (idx !== undefined) removeRow(group, idx, node)
}
for (const spec of state.inputSpecs)
addNodeInput(node, {
...spec,
name: fieldName(group, row, spec.name),
display_name: spec.display_name ?? spec.name
})
return node.widgets!.splice(startLen)
}
function insertRowAfterGroup(
group: string,
node: LGraphNode,
rowWidgets: IBaseWidget[]
): void {
const lastIdx = node.widgets!.findLastIndex(
(w) => w.name === group || w.name.startsWith(`${group}.`)
)
node.widgets!.splice(lastIdx + 1, 0, ...rowWidgets)
}
function syncController(group: string, node: DynamicGroupNode): void {
const state = node.comfyDynamic.dynamicGroup[group]
const controller = node.widgets?.find((w) => w.name === group)
if (!state || !controller) return
controller.options ??= {}
controller.options.disabled = countGroupRows(group, node) >= state.max
node.size[1] = node.computeSize([...node.size])[1]
}
function addRow(group: string, node: DynamicGroupNode): void {
const state = node.comfyDynamic.dynamicGroup[group]
if (!state) return
node.widgets ??= []
const row = countGroupRows(group, node)
if (row >= state.max) return
insertRowAfterGroup(group, node, createRow(group, row, state, node))
syncController(group, node)
app.canvas?.setDirty(true, true)
}
function removeRow(group: string, row: number, node: DynamicGroupNode): void {
const state = node.comfyDynamic.dynamicGroup[group]
if (!state || row < state.min) return
for (const w of remove(node.widgets!, (w) =>
belongsToRow(group, w.name, row)
))
w.onRemove?.()
remove(node.inputs, (inp) => belongsToRow(group, inp.name, row))
for (const w of node.widgets ?? []) {
const headerRow = headerRowIndex(group, w.name)
if (headerRow !== undefined && headerRow > row) {
w.name = rowHeaderName(group, headerRow - 1)
w.options ??= {}
w.options.removable = headerRow - 1 >= state.min
continue
}
const shifted = shiftedFieldName(group, w.name, row)
if (shifted !== undefined) w.name = shifted
}
for (const inp of node.inputs) {
const shifted = shiftedFieldName(group, inp.name, row)
if (shifted === undefined) continue
inp.name = shifted
if (inp.widget) inp.widget.name = shifted
}
syncController(group, node)
app.canvas?.setDirty(true, true)
}
/** Rebuild the group from scratch to hold exactly `count` rows. */
function rebuildRows(group: string, count: number, node: DynamicGroupNode) {
const state = node.comfyDynamic.dynamicGroup[group]
if (!state) return
node.widgets ??= []
const isRowMember = (name: string) => name.startsWith(`${group}.`)
for (const w of remove(node.widgets, (w) => isRowMember(w.name)))
w.onRemove?.()
remove(node.inputs, (inp) => isRowMember(inp.name))
const insertAt = node.widgets.findIndex((w) => w.name === group) + 1
const rowWidgets: IBaseWidget[] = []
for (let row = 0; row < count; row++)
rowWidgets.push(...createRow(group, row, state, node))
node.widgets.splice(insertAt, 0, ...rowWidgets)
}
function dynamicGroupWidget(
node: LGraphNode,
inputName: string,
untypedInputData: InputSpec,
_appArg: ComfyApp
) {
const parseResult = zDynamicGroupInputSpec.safeParse(untypedInputData)
if (!parseResult.success) throw new Error('invalid DynamicGroup spec')
const [, { template, min, max }] = parseResult.data
const toSpecs = (
inputs: Record<string, InputSpec> | undefined,
isOptional: boolean
) =>
Object.entries(inputs ?? {}).map(([name, spec]) =>
transformInputSpecV1ToV2(spec, { name, isOptional })
)
const inputSpecs = [
...toSpecs(template.required, false),
...toSpecs(template.optional, true)
]
withComfyDynamicGroup(node)
const typedNode = node as DynamicGroupNode
typedNode.comfyDynamic.dynamicGroup[inputName] = { min, max, inputSpecs }
node.widgets ??= []
const controller = node.addCustomWidget({
name: inputName,
type: 'dynamic_group_add',
value: min,
y: 0,
serialize: true,
callback: () => addRow(inputName, typedNode),
draw(
this: IBaseWidget,
ctx: CanvasRenderingContext2D,
_node: LGraphNode,
width: number,
y: number
) {
drawGroupButton(
ctx,
width,
y,
t('dynamicGroup.addRow'),
!!this.options?.disabled
)
},
mouse(this: IBaseWidget, event: CanvasPointerEvent) {
if (event.type !== 'pointerup' || this.options?.disabled) return false
addRow(inputName, typedNode)
return true
},
options: { serialize: false, socketless: true, disabled: false }
})
Object.defineProperty(controller, 'value', {
get() {
return countGroupRows(inputName, typedNode)
},
set(count: unknown) {
if (typeof count !== 'number') return
rebuildRows(inputName, count, typedNode)
syncController(inputName, typedNode)
},
configurable: true
})
controller.value = min
return { widget: controller }
}
export const dynamicWidgets = {
COMFY_DYNAMICCOMBO_V3: dynamicComboWidget,
COMFY_DYNAMICGROUP_V3: dynamicGroupWidget
}
const dynamicInputs: Record<
string,
(node: LGraphNode, inputSpec: InputSpecV2) => void

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[], z.ZodTypeDef, unknown>,
schema: z.ZodType<T[]>,
contextName: string
): T[] {
if (property === undefined) return []

View File

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

View File

@@ -2,7 +2,6 @@ import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink'
import type { LGraphNodeConstructor } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import { parseNodeId } from '@/types/nodeId'
import type {
ComfyNode,
ComfyWorkflowJSON
@@ -852,10 +851,7 @@ class GroupNodeHandler {
const selectedIds = Object.keys(app.canvas.selected_nodes)
const newNodes: LGraphNode[] = []
for (let i = 0; i < selectedIds.length; i++) {
const selectedId = parseNodeId(selectedIds[i])
const newNode = selectedId
? app.rootGraph.getNodeById(selectedId)
: null
const newNode = app.rootGraph.getNodeById(selectedIds[i])
const innerNodeData = nodeData.nodes[i]
if (!newNode) continue
newNodes.push(newNode)
@@ -909,10 +905,9 @@ class GroupNodeHandler {
const reconnectInputs = (selectedIds: (string | number)[]) => {
for (const innerNodeIndex in oldToNewInputMap) {
const selectedId = parseNodeId(selectedIds[Number(innerNodeIndex)])
const newNode = selectedId
? app.rootGraph.getNodeById(selectedId)
: null
const newNode = app.rootGraph.getNodeById(
selectedIds[Number(innerNodeIndex)]
)
if (!newNode) continue
const map = oldToNewInputMap[Number(innerNodeIndex)]
for (const innerInputId in map) {
@@ -943,10 +938,9 @@ class GroupNodeHandler {
const link = app.rootGraph.links[l]
if (!link) continue
const targetNode = app.rootGraph.getNodeById(link.target_id)
const selectedId = parseNodeId(selectedIds[slot.node.index ?? 0])
const newNode = selectedId
? app.rootGraph.getNodeById(selectedId)
: null
const newNode = app.rootGraph.getNodeById(
selectedIds[slot.node.index ?? 0]
)
if (targetNode) {
newNode?.connect(slot.slot, targetNode, link.target_slot)
}

View File

@@ -26,7 +26,6 @@ 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,7 +8,6 @@ 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 { toNodeId } from '@/types/nodeId'
import { createMockLLink } from '@/utils/__tests__/litegraphTestUtils'
vi.mock('@/scripts/app', () => ({
@@ -40,7 +39,7 @@ function createTargetNode(
id = 7
): Pick<LGraphNode, 'id' | 'inputs' | 'widgets'> {
return fromPartial<Pick<LGraphNode, 'id' | 'inputs' | 'widgets'>>({
id: toNodeId(id),
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 { Subgraph } from '@/lib/litegraph/src/litegraph'
import type { NodeId, Subgraph } from '@/lib/litegraph/src/litegraph'
import {
LGraph,
LGraphGroup,
@@ -17,7 +17,6 @@ import type { UUID } from '@/utils/uuid'
import { zeroUuid } from '@/utils/uuid'
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { toNodeId } from '@/types/nodeId'
import { widgetId } from '@/types/widgetId'
import {
createTestSubgraph,
@@ -299,7 +298,7 @@ describe('Graph Clearing and Callbacks', () => {
})
const widgetValueStore = useWidgetValueStore()
const seedWidgetId = widgetId(graphId, toNodeId('10'), 'seed')
const seedWidgetId = widgetId(graphId, '10' as NodeId, 'seed')
widgetValueStore.registerWidget(seedWidgetId, {
type: 'number',
value: 1,
@@ -622,7 +621,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]).toBeUndefined()
expect(subgraph._nodes_by_id[rootNode.id as number]).toBeUndefined()
})
it('preserves root graph node IDs as canonical', () => {
@@ -658,7 +657,7 @@ describe('ensureGlobalIdUniqueness', () => {
rootGraph.ensureGlobalIdUniqueness()
expect(rootGraph.state.lastNodeId).toBeGreaterThanOrEqual(
Number(subNode.id)
subNode.id as number
)
})
@@ -675,7 +674,7 @@ describe('ensureGlobalIdUniqueness', () => {
subgraph._nodes_by_id[subNodeA.id] = subNodeA
const subNodeB = new DummyNode()
subNodeB.id = toNodeId(999)
subNodeB.id = 999
subgraph._nodes.push(subNodeB)
subgraph._nodes_by_id[subNodeB.id] = subNodeB
@@ -694,7 +693,7 @@ describe('ensureGlobalIdUniqueness', () => {
const subgraph = createSubgraphOnGraph(rootGraph)
const subNode = new DummyNode()
subNode.id = toNodeId(42)
subNode.id = 42
subgraph._nodes.push(subNode)
subgraph._nodes_by_id[subNode.id] = subNode
@@ -872,9 +871,9 @@ describe('_removeDuplicateLinks', () => {
expect(graph._links.size).toBe(1)
const survivingLink = graph._links.values().next().value!
const targetNode = graph.getNodeById(toNodeId(survivingLink.target_id))!
const targetNode = graph.getNodeById(survivingLink.target_id)!
expect(targetNode.inputs[0].link).toBe(survivingLink.id)
const sourceNode = graph.getNodeById(toNodeId(survivingLink.origin_id))!
const sourceNode = graph.getNodeById(survivingLink.origin_id)!
expect(sourceNode.outputs[0].links).toEqual([survivingLink.id])
})
@@ -886,11 +885,11 @@ describe('_removeDuplicateLinks', () => {
expect(graph._links.size).toBe(1)
const link = graph._links.values().next().value!
const target = graph.getNodeById(toNodeId(link.target_id))!
const target = graph.getNodeById(link.target_id)!
const linkedInput = target.inputs.find((inp) => inp.link === link.id)
expect(linkedInput).toBeDefined()
const source = graph.getNodeById(toNodeId(link.origin_id))!
const source = graph.getNodeById(link.origin_id)!
expect(source.outputs[link.origin_slot].links).toContain(link.id)
})
@@ -902,7 +901,7 @@ describe('_removeDuplicateLinks', () => {
expect(subgraph._links.size).toBe(1)
const link = subgraph._links.values().next().value!
const target = subgraph.getNodeById(toNodeId(link.target_id))!
const target = subgraph.getNodeById(link.target_id)!
expect(target.inputs[0].link).toBe(link.id)
})
})
@@ -1006,7 +1005,7 @@ describe('Subgraph Unpacking', () => {
const firstInstance = createTestSubgraphNode(subgraph, { pos: [100, 100] })
const secondInstance = createTestSubgraphNode(subgraph, { pos: [300, 100] })
secondInstance.id = toNodeId(2)
secondInstance.id = 2
rootGraph.add(firstInstance)
rootGraph.add(secondInstance)
@@ -1054,7 +1053,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const idsB = nodeIdSet(graph, SUBGRAPH_B)
for (const id of SHARED_NODE_IDS) {
expect(idsA.has(toNodeId(id))).toBe(true)
expect(idsA.has(id as NodeId)).toBe(true)
}
for (const id of idsA) {
expect(idsB.has(id)).toBe(false)
@@ -1066,8 +1065,8 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const idsB = nodeIdSet(graph, SUBGRAPH_B)
for (const link of graph.subgraphs.get(SUBGRAPH_B)!.links.values()) {
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)
expect(idsB.has(link.origin_id)).toBe(true)
expect(idsB.has(link.target_id)).toBe(true)
}
})
@@ -1076,7 +1075,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const idsB = nodeIdSet(graph, SUBGRAPH_B)
for (const widget of graph.subgraphs.get(SUBGRAPH_B)!.widgets) {
expect(idsB.has(toNodeId(widget.id))).toBe(true)
expect(idsB.has(widget.id)).toBe(true)
}
})
@@ -1090,14 +1089,14 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
graph.subgraphs.get(SUBGRAPH_B)!.nodes.map((n) => String(n.id))
)
const pw102 = graph.getNodeById(toNodeId(102))?.properties?.proxyWidgets
const pw102 = graph.getNodeById(102 as NodeId)?.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(toNodeId(103))?.properties?.proxyWidgets
const pw103 = graph.getNodeById(103 as NodeId)?.properties?.proxyWidgets
expect(Array.isArray(pw103)).toBe(true)
for (const entry of pw103 as unknown[][]) {
expect(Array.isArray(entry)).toBe(true)
@@ -1115,7 +1114,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const innerNode = graph.subgraphs
.get(SUBGRAPH_A)!
.nodes.find((n) => n.id === toNodeId(50))
.nodes.find((n) => n.id === (50 as NodeId))
const pw = innerNode?.properties?.proxyWidgets
expect(Array.isArray(pw)).toBe(true)
for (const entry of pw as unknown[][]) {
@@ -1155,7 +1154,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
expect(migrationCall).toBeDefined()
expect(migrationCall![1]).toEqual(
expect.objectContaining({
hostNodeId: expect.any(String),
hostNodeId: expect.any(Number),
proxyWidgets: expect.anything()
})
)
@@ -1177,12 +1176,8 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const graph = new LGraph()
graph.configure(structuredClone(uniqueSubgraphNodeIds))
expect(nodeIdSet(graph, SUBGRAPH_A)).toEqual(
new Set([toNodeId(10), toNodeId(11), toNodeId(12)])
)
expect(nodeIdSet(graph, SUBGRAPH_B)).toEqual(
new Set([toNodeId(20), toNodeId(21), toNodeId(22)])
)
expect(nodeIdSet(graph, SUBGRAPH_A)).toEqual(new Set([10, 11, 12]))
expect(nodeIdSet(graph, SUBGRAPH_B)).toEqual(new Set([20, 21, 22]))
})
})

View File

@@ -11,8 +11,6 @@ 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, toNodeId } from '@/types/nodeId'
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
import { forEachNode } from '@/utils/graphTraversalUtil'
import {
@@ -27,8 +25,9 @@ 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 { LinkEndpointNodeId, LinkId } from './LLink'
import type { LinkId } from './LLink'
import { MapProxyHandler } from './MapProxyHandler'
import { Reroute } from './Reroute'
import type { RerouteId } from './Reroute'
@@ -104,22 +103,6 @@ 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'
/**
@@ -655,8 +638,8 @@ export class LGraph
const S: LGraphNode[] = []
const M: Dictionary<LGraphNode> = {}
// to avoid repeating links
const visited_links: Record<SerializedNodeId, boolean> = {}
const remaining_links: Record<SerializedNodeId, number> = {}
const visited_links: Record<NodeId, boolean> = {}
const remaining_links: Record<NodeId, number> = {}
// search for the nodes without inputs (starting nodes)
for (const node of this._nodes) {
@@ -972,11 +955,11 @@ export class LGraph
}
// nodes
if (node.id !== UNASSIGNED_NODE_ID && this._nodes_by_id[node.id] != null) {
if (node.id != -1 && this._nodes_by_id[node.id] != null) {
console.warn(
'LiteGraph: there is already a node with this ID, changing it'
)
node.id = nextNodeId(state)
node.id = ++state.lastNodeId
}
if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
@@ -984,10 +967,10 @@ export class LGraph
}
// give him an id
if (node.id == null || node.id === UNASSIGNED_NODE_ID) {
node.id = nextNodeId(state)
} else {
syncLastNodeId(state, node.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
}
// Set ghost flag before registration so VueNodeData picks it up
@@ -1150,8 +1133,8 @@ export class LGraph
/**
* Returns a node by its id.
*/
getNodeById(id: LinkEndpointNodeId | null | undefined): LGraphNode | null {
return id != null && id !== -1 ? this._nodes_by_id[id] : null
getNodeById(id: NodeId | null | undefined): LGraphNode | null {
return id != null ? this._nodes_by_id[id] : null
}
/**
@@ -1643,9 +1626,7 @@ export class LGraph
const node = this.getNodeById(sampleLink.target_id)
const keepId = selectSurvivorLink(ids, node)
purgeOrphanedLinks(ids, keepId, this._links, (id) =>
this.getNodeById(toNodeId(id))
)
purgeOrphanedLinks(ids, keepId, this._links, (id) => this.getNodeById(id))
repairInputLinks(ids, keepId, node)
}
}
@@ -1986,10 +1967,9 @@ export class LGraph
}
}
const newNodeId = nextNodeId(this.state)
nodeIdMap.set(toNodeId(n_info.id), newNodeId)
node.id = newNodeId
n_info.id = newNodeId
nodeIdMap.set(n_info.id, ++this.last_node_id)
node.id = this.last_node_id
n_info.id = this.last_node_id
// Strip links from serialized data before configure to prevent
// onConnectionsChange from resolving subgraph-internal link IDs
@@ -2043,9 +2023,9 @@ export class LGraph
}
}
const newLinks: {
oid: LinkEndpointNodeId
oid: NodeId
oslot: number
tid: LinkEndpointNodeId
tid: NodeId
tslot: number
id: LinkId
iparent?: RerouteId
@@ -2065,8 +2045,7 @@ export class LGraph
link.origin_slot = outerLink.origin_slot
externalParentId = outerLink.parentId
} else {
const origin_id =
link.origin_id === -1 ? undefined : nodeIdMap.get(link.origin_id)
const origin_id = nodeIdMap.get(link.origin_id)
if (!origin_id) {
console.error('Missing Link ID when unpacking')
continue
@@ -2091,8 +2070,7 @@ export class LGraph
}
continue
} else {
const target_id =
link.target_id === -1 ? undefined : nodeIdMap.get(link.target_id)
const target_id = nodeIdMap.get(link.target_id)
if (!target_id) {
console.error('Missing Link ID when unpacking')
continue
@@ -2130,9 +2108,7 @@ export class LGraph
console.error('Ignoring link to subgraph outside subgraph')
continue
}
if (newLink.tid === -1) continue
const tnode = this.getNodeById(newLink.tid)
if (!tnode) continue
const tnode = this._nodes_by_id[newLink.tid]
created = this.inputNode.slots[newLink.oslot].connect(
tnode.inputs[newLink.tslot],
tnode
@@ -2142,19 +2118,17 @@ export class LGraph
console.error('Ignoring link to subgraph outside subgraph')
continue
}
if (newLink.oid === -1) continue
const tnode = this.getNodeById(newLink.oid)
if (!tnode) continue
const tnode = this._nodes_by_id[newLink.oid]
created = this.outputNode.slots[newLink.tslot].connect(
tnode.outputs[newLink.oslot],
tnode
)
} else {
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)
created = this._nodes_by_id[newLink.oid].connect(
newLink.oslot,
this._nodes_by_id[newLink.tid],
newLink.tslot
)
}
if (!created) {
console.error('Failed to create link')
@@ -2530,13 +2504,11 @@ export class LGraph
if (subgraphs) {
const reservedNodeIds = new Set<number>()
for (const node of this._nodes) {
const id = numericNodeId(node.id)
if (id !== null) reservedNodeIds.add(id)
if (typeof node.id === 'number') reservedNodeIds.add(node.id)
}
for (const sg of this.subgraphs.values()) {
for (const node of sg.nodes) {
const id = numericNodeId(node.id)
if (id !== null) reservedNodeIds.add(id)
if (typeof node.id === 'number') reservedNodeIds.add(node.id)
}
}
for (const n of nodesData ?? []) {
@@ -2566,7 +2538,7 @@ export class LGraph
}
let error = false
const nodeDataMap = new Map<SerializedNodeId, ISerialisedNode>()
const nodeDataMap = new Map<NodeId, ISerialisedNode>()
// create nodes
this._nodes = []
@@ -2587,7 +2559,7 @@ export class LGraph
}
// id it or it will create a new id
node.id = toNodeId(n_info.id)
node.id = n_info.id
// add before configure, otherwise configure cannot create links
this.add(node, true)
nodeDataMap.set(node.id, n_info)
@@ -2595,7 +2567,7 @@ export class LGraph
// configure nodes afterwards so they can reach each other
for (const [id, nodeData] of nodeDataMap) {
const node = this.getNodeById(toNodeId(id))
const node = this.getNodeById(id)
node?.configure(nodeData)
if (LiteGraph.alwaysSnapToGrid && node) {
@@ -2709,24 +2681,24 @@ export class LGraph
const remappedIds = new Map<NodeId, NodeId>()
for (const node of graph._nodes) {
const currentId = numericNodeId(node.id)
if (currentId === null) continue
if (typeof node.id !== 'number') continue
if (usedNodeIds.has(currentId)) {
if (usedNodeIds.has(node.id)) {
const oldId = node.id
while (usedNodeIds.has(++state.lastNodeId));
const newId = toNodeId(state.lastNodeId)
const newId = state.lastNodeId
delete graph._nodes_by_id[oldId]
node.id = newId
graph._nodes_by_id[newId] = node
usedNodeIds.add(state.lastNodeId)
usedNodeIds.add(newId)
remappedIds.set(oldId, newId)
console.warn(
`LiteGraph: duplicate node ID ${oldId} reassigned to ${newId} in graph ${graph.id}`
)
} else {
usedNodeIds.add(currentId)
if (currentId > state.lastNodeId) state.lastNodeId = currentId
usedNodeIds.add(node.id as number)
if ((node.id as number) > state.lastNodeId)
state.lastNodeId = node.id as number
}
}
@@ -2919,7 +2891,7 @@ export class Subgraph
private _repairSlotLinkIds(
linkIds: LinkId[],
ioNodeId: NodeId,
ioNodeId: number,
slotIndex: number
): void {
const repaired = linkIds.map((id) =>
@@ -2933,7 +2905,7 @@ export class Subgraph
}
private _findLinkBySlot(
nodeId: NodeId,
nodeId: number,
slotIndex: number
): LLink | undefined {
for (const link of this._links.values()) {
@@ -3137,12 +3109,10 @@ function patchLinkNodeIds(
remappedIds: Map<NodeId, NodeId>
): void {
for (const link of links.values()) {
const newOrigin =
link.origin_id === -1 ? undefined : remappedIds.get(link.origin_id)
const newOrigin = remappedIds.get(link.origin_id)
if (newOrigin !== undefined) link.origin_id = newOrigin
const newTarget =
link.target_id === -1 ? undefined : remappedIds.get(link.target_id)
const newTarget = remappedIds.get(link.target_id)
if (newTarget !== undefined) link.target_id = newTarget
}
}

View File

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

View File

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

View File

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

View File

@@ -21,9 +21,7 @@ import type { AnimationOptions } from './DragAndScale'
import type { LGraph, SubgraphId } from './LGraph'
import { LGraphGroup } from './LGraphGroup'
import { LGraphNode } from './LGraphNode'
import type { NodeProperty } from './LGraphNode'
import { parseNodeId } from '@/types/nodeId'
import type { SerializedNodeId } from '@/types/nodeId'
import type { NodeId, NodeProperty } from './LGraphNode'
import { LLink } from './LLink'
import type { LinkId } from './LLink'
import { Reroute } from './Reroute'
@@ -215,7 +213,7 @@ interface LGraphCanvasState {
selectionChanged: boolean
/** ID of node currently in ghost placement mode (semi-transparent, following cursor). */
ghostNodeId: SerializedNodeId | null
ghostNodeId: NodeId | null
}
/**
@@ -226,7 +224,7 @@ interface ClipboardPasteResult {
/** All successfully created items */
created: Positionable[]
/** Map: original node IDs to newly created nodes */
nodes: Map<SerializedNodeId, LGraphNode>
nodes: Map<NodeId, LGraphNode>
/** Map: original link IDs to new link IDs */
links: Map<LinkId, LLink>
/** Map: original reroute IDs to newly created reroutes */
@@ -680,7 +678,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<SerializedNodeId> = new Set()
private _visible_node_ids: Set<NodeId> = new Set()
node_over?: LGraphNode
node_capturing_input?: LGraphNode | null
highlighted_links: Dictionary<boolean> = {}
@@ -693,7 +691,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<SerializedNodeId, LGraphNode>()
dirty_nodes = new Map<NodeId, LGraphNode>()
dirty_area?: Rect | null
/** @deprecated Unused */
node_in_panel?: LGraphNode | null
@@ -3775,8 +3773,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this._ghostKeyHandler = null
}
const parsedNodeId = parseNodeId(nodeId)
const node = parsedNodeId ? this.graph?.getNodeById(parsedNodeId) : null
const node = this.graph?.getNodeById(nodeId)
if (!node) return
if (cancelled) {
@@ -4171,7 +4168,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const results: ClipboardPasteResult = {
created: [],
nodes: new Map<SerializedNodeId, LGraphNode>(),
nodes: new Map<NodeId, LGraphNode>(),
links: new Map<LinkId, LLink>(),
reroutes: new Map<RerouteId, Reroute>(),
subgraphs: new Map<SubgraphId, Subgraph>()
@@ -4269,8 +4266,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
connectInputs &&
LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs
) {
const originNodeId = parseNodeId(info.origin_id)
outNode ??= originNodeId ? graph.getNodeById(originNodeId) : null
outNode ??= graph.getNodeById(info.origin_id)
afterRerouteId ??= info.parentId
}
@@ -4315,7 +4311,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const newPositions = created
.filter((item): item is LGraphNode => item instanceof LGraphNode)
.map((node) => ({
nodeId: node.id,
nodeId: String(node.id),
bounds: {
x: node.pos[0],
y: node.pos[1],
@@ -8963,10 +8959,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
function patchLinkNodeIds(
links:
| { origin_id: SerializedNodeId; target_id: SerializedNodeId }[]
| undefined,
remappedIds: Map<SerializedNodeId, SerializedNodeId>
links: { origin_id: NodeId; target_id: NodeId }[] | undefined,
remappedIds: Map<NodeId, NodeId>
) {
if (!links?.length) return
@@ -8981,8 +8975,8 @@ function patchLinkNodeIds(
function remapNodeId(
nodeId: string,
remappedIds: Map<SerializedNodeId, SerializedNodeId>
): SerializedNodeId | undefined {
remappedIds: Map<NodeId, NodeId>
): NodeId | undefined {
const directMatch = remappedIds.get(nodeId)
if (directMatch !== undefined) return directMatch
if (!/^-?\d+$/.test(nodeId)) return undefined
@@ -8995,7 +8989,7 @@ function remapNodeId(
function remapProxyWidgets(
info: ISerialisedNode,
remappedIds: Map<SerializedNodeId, SerializedNodeId> | undefined
remappedIds: Map<NodeId, NodeId> | undefined
) {
if (!remappedIds || remappedIds.size === 0) return
@@ -9026,7 +9020,7 @@ function hasStringSourceNodeId(
function remapPreviewExposures(
info: ISerialisedNode,
remappedIds: Map<SerializedNodeId, SerializedNodeId> | undefined
remappedIds: Map<NodeId, NodeId> | undefined
) {
if (!remappedIds || remappedIds.size === 0) return
@@ -9048,11 +9042,10 @@ export function remapClipboardSubgraphNodeIds(
): void {
const usedNodeIds = new Set<number>()
forEachNode(rootGraph, (node) => {
const numericId = Number(node.id)
if (!Number.isInteger(numericId)) return
usedNodeIds.add(numericId)
if (rootGraph.state.lastNodeId < numericId)
rootGraph.state.lastNodeId = numericId
if (typeof node.id !== 'number') return
usedNodeIds.add(node.id)
if (rootGraph.state.lastNodeId < node.id)
rootGraph.state.lastNodeId = node.id
})
function nextUniqueNodeId() {
@@ -9062,12 +9055,9 @@ export function remapClipboardSubgraphNodeIds(
return nextId
}
const subgraphNodeIdMap = new Map<
SubgraphId,
Map<SerializedNodeId, SerializedNodeId>
>()
const subgraphNodeIdMap = new Map<SubgraphId, Map<NodeId, NodeId>>()
for (const subgraphInfo of parsed.subgraphs ?? []) {
const remappedIds = new Map<SerializedNodeId, SerializedNodeId>()
const remappedIds = new Map<NodeId, NodeId>()
const interiorNodes = subgraphInfo.nodes ?? []
for (const nodeInfo of interiorNodes) {

View File

@@ -8,8 +8,6 @@ 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, toNodeId, serializeNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import { adjustColor } from '@/utils/colorUtil'
import type { ColorAdjustOptions } from '@/utils/colorUtil'
import {
@@ -18,10 +16,7 @@ import {
toClass
} from '@/lib/litegraph/src/utils/type'
import {
SUBGRAPH_INPUT_ID,
SUBGRAPH_OUTPUT_ID
} from '@/lib/litegraph/src/constants'
import { 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'
@@ -103,7 +98,7 @@ import type { WidgetTypeMap } from './widgets/widgetMap'
// #region Types
export type { NodeId } from '@/types/nodeId'
export type NodeId = number | string
export type NodeProperty = string | number | boolean | object
@@ -503,11 +498,10 @@ 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(this.id, { x: value[0], y: value[1] })
mutations.moveNode(String(this.id), { x: value[0], y: value[1] })
}
/**
@@ -526,11 +520,10 @@ 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(this.id, {
mutations.resizeNode(String(this.id), {
width: value[0],
height: value[1]
})
@@ -817,7 +810,7 @@ export class LGraphNode
}
constructor(title: string, type?: string) {
this.id = UNASSIGNED_NODE_ID
this.id = -1
this.title = title || 'Unnamed'
this.type = type ?? ''
this.size = [LiteGraph.NODE_WIDTH, 60]
@@ -840,37 +833,34 @@ export class LGraphNode
if (this.graph) {
this.graph.incrementVersion()
}
const configuredInfo: ISerialisedNode = {
...info,
id: info.id === -1 ? this.id : toNodeId(info.id)
}
for (const j in configuredInfo) {
if (info.id === -1) info.id = this.id
for (const j in info) {
if (j == 'properties') {
// i don't want to clone properties, I want to reuse the old container
for (const k in configuredInfo.properties) {
this.properties[k] = configuredInfo.properties[k]
this.onPropertyChanged?.(k, configuredInfo.properties[k])
for (const k in info.properties) {
this.properties[k] = info.properties[k]
this.onPropertyChanged?.(k, info.properties[k])
}
continue
}
// @ts-expect-error #594
if (configuredInfo[j] == null) {
if (info[j] == null) {
continue
// @ts-expect-error #594
} else if (typeof configuredInfo[j] == 'object') {
} else if (typeof info[j] == 'object') {
// @ts-expect-error #594
if (this[j]?.configure) {
// @ts-expect-error #594
this[j]?.configure(configuredInfo[j])
this[j]?.configure(info[j])
} else {
// @ts-expect-error #594
this[j] = LiteGraph.cloneObject(configuredInfo[j], this[j])
this[j] = LiteGraph.cloneObject(info[j], this[j])
}
} else {
// value
// @ts-expect-error #594
this[j] = configuredInfo[j]
this[j] = info[j]
}
}
@@ -954,7 +944,7 @@ export class LGraphNode
serialize(): ISerialisedNode {
// create serialization object
const o: ISerialisedNode = {
id: serializeNodeId(this.id),
id: this.id,
type: this.type,
pos: [this.pos[0], this.pos[1]],
size: [this.size[0], this.size[1]],
@@ -2011,7 +2001,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 !== UNASSIGNED_NODE_ID && isNodeBindable(widget)) {
if (this.id !== -1 && isNodeBindable(widget)) {
widget.setNodeId(this.id)
}
@@ -3282,7 +3272,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 === SUBGRAPH_INPUT_ID && 'inputNode' in graph) {
if (link_info.origin_id === -10 && 'inputNode' in graph) {
graph.inputNode._disconnectNodeInput(this, input, link_info)
return true
}

View File

@@ -6,10 +6,8 @@ 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 { toNodeId, serializeNodeId } from '@/types/nodeId'
import type { LGraphNode } from './LGraphNode'
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
import type { LGraphNode, NodeId } from './LGraphNode'
import type { Reroute, RerouteId } from './Reroute'
import type {
CanvasColour,
@@ -26,21 +24,16 @@ 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: SerializedNodeId,
origin_id: NodeId,
origin_slot: number,
target_id: SerializedNodeId,
target_id: NodeId,
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 &
(
@@ -104,11 +97,11 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
parentId?: RerouteId
type: ISlotType
/** Output node ID */
origin_id: LinkEndpointNodeId
origin_id: NodeId
/** Output slot index */
origin_slot: number
/** Input node ID */
target_id: LinkEndpointNodeId
target_id: NodeId
/** Input slot index */
target_slot: number
@@ -161,17 +154,17 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
constructor(
id: LinkId,
type: ISlotType,
origin_id: SerializedNodeId,
origin_id: NodeId,
origin_slot: number,
target_id: SerializedNodeId,
target_id: NodeId,
target_slot: number,
parentId?: RerouteId
) {
this.id = id
this.type = type
this.origin_id = toLinkEndpointNodeId(origin_id)
this.origin_id = origin_id
this.origin_slot = origin_slot
this.target_id = toLinkEndpointNodeId(target_id)
this.target_id = target_id
this.target_slot = target_slot
this.parentId = parentId
@@ -358,17 +351,17 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
configure(o: LLink | SerialisedLLinkArray) {
if (Array.isArray(o)) {
this.id = o[0]
this.origin_id = toLinkEndpointNodeId(o[1])
this.origin_id = o[1]
this.origin_slot = o[2]
this.target_id = toLinkEndpointNodeId(o[3])
this.target_id = o[3]
this.target_slot = o[4]
this.type = o[5]
} else {
this.id = o.id
this.type = o.type
this.origin_id = toLinkEndpointNodeId(o.origin_id)
this.origin_id = o.origin_id
this.origin_slot = o.origin_slot
this.target_id = toLinkEndpointNodeId(o.target_id)
this.target_id = o.target_id
this.target_slot = o.target_slot
this.parentId = o.parentId
}
@@ -380,7 +373,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: LinkEndpointNodeId, outputIndex: number): boolean {
hasOrigin(nodeId: NodeId, outputIndex: number): boolean {
return this.origin_id === nodeId && this.origin_slot === outputIndex
}
@@ -390,7 +383,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: LinkEndpointNodeId, inputIndex: number): boolean {
hasTarget(nodeId: NodeId, inputIndex: number): boolean {
return this.target_id === nodeId && this.target_slot === inputIndex
}
@@ -475,9 +468,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
serialize(): SerialisedLLinkArray {
return [
this.id,
serializeNodeId(this.origin_id),
this.origin_id,
this.origin_slot,
serializeNodeId(this.target_id),
this.target_id,
this.target_slot,
this.type
]
@@ -486,9 +479,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
asSerialisable(): SerialisableLLink {
const copy: SerialisableLLink = {
id: this.id,
origin_id: serializeNodeId(this.origin_id),
origin_id: this.origin_id,
origin_slot: this.origin_slot,
target_id: serializeNodeId(this.target_id),
target_id: 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 } from './LGraphNode'
import type { LGraphNode, NodeId } from './LGraphNode'
import { LLink } from './LLink'
import type { LinkEndpointNodeId, LinkId } from './LLink'
import type { LinkId } from './LLink'
import type {
CanvasColour,
INodeInputSlot,
@@ -182,7 +182,7 @@ export class Reroute
}
/** @inheritdoc */
get origin_id(): LinkEndpointNodeId | undefined {
get origin_id(): NodeId | undefined {
return this.firstLink?.origin_id
}

View File

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

View File

@@ -17,7 +17,6 @@ import {
LinkDirection
} from '@/lib/litegraph/src/litegraph'
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
import { toNodeId } from '@/types/nodeId'
import {
createMockNodeInputSlot,
createMockNodeOutputSlot
@@ -47,7 +46,7 @@ const test = baseTest.extend<TestContext>({
reroutes,
floatingLinks,
getLink: graph.getLink.bind(graph),
getNodeById: (id) => graph.getNodeById(id),
getNodeById: (id: number) => graph.getNodeById(id),
addFloatingLink: (link: LLink) => {
floatingLinks.set(link.id, link)
return link
@@ -75,7 +74,7 @@ const test = baseTest.extend<TestContext>({
createTestNode: async ({ network }, use) => {
await use((id: number): LGraphNode => {
const node = new LGraphNode('test')
node.id = toNodeId(id)
node.id = id
network.add(node)
return node
})

View File

@@ -12,7 +12,6 @@ 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 { toNodeId } from '@/types/nodeId'
import {
createMockCanvasPointerEvent,
createMockCanvasRenderingContext2D
@@ -60,7 +59,7 @@ const test = baseTest.extend<TestContext>({
createTestNode: async ({ graph }, use) => {
await use((id): LGraphNode => {
const node = new LGraphNode('test')
node.id = toNodeId(id)
node.id = id
graph.add(node)
return node
})
@@ -155,17 +154,17 @@ const test = baseTest.extend<TestContext>({
expect(link.origin_id).not.toBe(-1)
expect(link.origin_slot).not.toBe(-1)
expect(link.target_slot).toBe(-1)
const outputFloatingLinks = graph.getNodeById(
toNodeId(link.origin_id)
)?.outputs[link.origin_slot]._floatingLinks
const outputFloatingLinks = graph.getNodeById(link.origin_id)
?.outputs[link.origin_slot]._floatingLinks
expect(outputFloatingLinks).toBeDefined()
expect(outputFloatingLinks).toContain(link)
} else {
expect(link.origin_id).toBe(-1)
expect(link.origin_slot).toBe(-1)
expect(link.target_slot).not.toBe(-1)
const inputFloatingLinks = graph.getNodeById(toNodeId(link.target_id))
?.inputs[link.target_slot]._floatingLinks
const inputFloatingLinks = graph.getNodeById(link.target_id)?.inputs[
link.target_slot
]._floatingLinks
expect(inputFloatingLinks).toBeDefined()
expect(inputFloatingLinks).toContain(link)
}
@@ -219,8 +218,8 @@ describe('LinkConnector Integration', () => {
test('Should move input links', ({ graph, connector }) => {
const nextLinkId = graph.last_link_id + 1
const hasInputNode = graph.getNodeById(toNodeId(2))!
const disconnectedNode = graph.getNodeById(toNodeId(9))!
const hasInputNode = graph.getNodeById(2)!
const disconnectedNode = graph.getNodeById(9)!
const reroutesBefore = LLink.getReroutes(
graph,
@@ -267,7 +266,7 @@ describe('LinkConnector Integration', () => {
expect(floatingLink).toBeInstanceOf(LLink)
const floatingReroute = graph.reroutes.get(floatingLink.parentId!)!
const disconnectedNode = graph.getNodeById(toNodeId(9))!
const disconnectedNode = graph.getNodeById(9)!
connector.dragFromReroute(graph, floatingReroute)
expect(connector.state.connectingTo).toBe('input')
@@ -303,7 +302,7 @@ describe('LinkConnector Integration', () => {
}) => {
expect(graph.floatingLinks.size).toBe(1)
const floatingOutNode = graph.getNodeById(toNodeId(1))!
const floatingOutNode = graph.getNodeById(1)!
floatingOutNode.disconnectOutput(0)
// Should have lost one reroute
@@ -313,10 +312,10 @@ describe('LinkConnector Integration', () => {
// The two normal links should now be floating
expect(graph.floatingLinks.size).toBe(2)
graph.getNodeById(toNodeId(2))!.disconnectInput(0, true)
graph.getNodeById(2)!.disconnectInput(0, true)
expect(graph.floatingLinks.size).toBe(1)
graph.getNodeById(toNodeId(3))!.disconnectInput(0, false)
graph.getNodeById(3)!.disconnectInput(0, false)
expect(graph.floatingLinks.size).toBe(0)
// Removed 4 reroutes
@@ -327,7 +326,7 @@ describe('LinkConnector Integration', () => {
const {
inputs: [input],
outputs: [output]
} = graph.getNodeById(toNodeId(nodeId))!
} = graph.getNodeById(nodeId)!
expect(input.link).toBeNull()
@@ -343,9 +342,9 @@ describe('LinkConnector Integration', () => {
graph,
connector
}) => {
const hasOutputNode = graph.getNodeById(toNodeId(1))!
const hasInputNode = graph.getNodeById(toNodeId(2))!
const hasInputNode2 = graph.getNodeById(toNodeId(3))!
const hasOutputNode = graph.getNodeById(1)!
const hasInputNode = graph.getNodeById(2)!
const hasInputNode2 = graph.getNodeById(3)!
const reroutesBefore = LLink.getReroutes(
graph,
@@ -372,8 +371,8 @@ describe('LinkConnector Integration', () => {
graph,
connector
}) => {
const hasOutputNode = graph.getNodeById(toNodeId(1))!
const hasInputNode = graph.getNodeById(toNodeId(2))!
const hasOutputNode = graph.getNodeById(1)!
const hasInputNode = graph.getNodeById(2)!
const originalOutputNodes = hasOutputNode.getOutputNodes(0)
const reroutesBefore = LLink.getReroutes(
@@ -402,8 +401,8 @@ describe('LinkConnector Integration', () => {
test('Should move output links', ({ graph, connector }) => {
const nextLinkIds = [graph.last_link_id + 1, graph.last_link_id + 2]
const hasOutputNode = graph.getNodeById(toNodeId(1))!
const disconnectedNode = graph.getNodeById(toNodeId(9))!
const hasOutputNode = graph.getNodeById(1)!
const disconnectedNode = graph.getNodeById(9)!
const reroutesBefore = hasOutputNode.outputs[0].links
?.map((linkId) => graph.links.get(linkId)!)
@@ -442,7 +441,7 @@ describe('LinkConnector Integration', () => {
}) => {
const nextLinkIds = [graph.last_link_id + 1, graph.last_link_id + 2]
const floatingOutNode = graph.getNodeById(toNodeId(1))!
const floatingOutNode = graph.getNodeById(1)!
floatingOutNode.disconnectOutput(0)
// Should have lost one reroute
@@ -452,7 +451,7 @@ describe('LinkConnector Integration', () => {
// The two normal links should now be floating
expect(graph.floatingLinks.size).toBe(2)
const disconnectedNode = graph.getNodeById(toNodeId(9))!
const disconnectedNode = graph.getNodeById(9)!
connector.dragNewFromOutput(
graph,
disconnectedNode,
@@ -497,7 +496,7 @@ describe('LinkConnector Integration', () => {
}) => {
expect(graph.floatingLinks.size).toBe(1)
graph.getNodeById(toNodeId(2))!.disconnectInput(0, true)
graph.getNodeById(2)!.disconnectInput(0, true)
expect(graph.floatingLinks.size).toBe(1)
// Only the original reroute should be floating
@@ -508,14 +507,14 @@ describe('LinkConnector Integration', () => {
expect(reroute.floating).toBeUndefined()
}
graph.getNodeById(toNodeId(3))!.disconnectInput(0, true)
graph.getNodeById(3)!.disconnectInput(0, true)
expect([...graph.reroutes]).toEqual(reroutesBeforeTest)
// The normal link should now be floating
expect(graph.floatingLinks.size).toBe(2)
expect(graph.reroutes.get(3)!.floating).toEqual({ slotType: 'output' })
const floatingOutNode = graph.getNodeById(toNodeId(1))!
const floatingOutNode = graph.getNodeById(1)!
floatingOutNode.disconnectOutput(0)
// Should have lost one reroute
@@ -530,7 +529,7 @@ describe('LinkConnector Integration', () => {
const {
inputs: [input],
outputs: [output]
} = graph.getNodeById(toNodeId(nodeId))!
} = graph.getNodeById(nodeId)!
expect(input.link).toBeNull()
@@ -548,7 +547,7 @@ describe('LinkConnector Integration', () => {
floatingReroute,
validateIntegrityFloatingRemoved
}) => {
const manyOutputsNode = graph.getNodeById(toNodeId(4))!
const manyOutputsNode = graph.getNodeById(4)!
const canvasX = floatingReroute.pos[0]
const canvasY = floatingReroute.pos[1]
const floatingRerouteEvent = createMockCanvasPointerEvent(
@@ -571,7 +570,7 @@ describe('LinkConnector Integration', () => {
connector,
floatingReroute
}) => {
const manyOutputsNode = graph.getNodeById(toNodeId(4))!
const manyOutputsNode = graph.getNodeById(4)!
const reroute7 = graph.reroutes.get(7)!
const reroute10 = graph.reroutes.get(10)!
@@ -618,8 +617,8 @@ describe('LinkConnector Integration', () => {
graph,
connector
}) => {
const hasOutputNode = graph.getNodeById(toNodeId(1))!
const hasInputNode = graph.getNodeById(toNodeId(2))!
const hasOutputNode = graph.getNodeById(1)!
const hasInputNode = graph.getNodeById(2)!
const reroutesBefore = LLink.getReroutes(
graph,
@@ -633,9 +632,7 @@ describe('LinkConnector Integration', () => {
connector.reset()
expect(hasOutputNode.getOutputNodes(0)).toEqual([hasInputNode])
expect(hasInputNode.getOutputNodes(0)).toEqual([
graph.getNodeById(toNodeId(3))
])
expect(hasInputNode.getOutputNodes(0)).toEqual([graph.getNodeById(3)])
// Moved link should have the same reroutes
const reroutesAfter = LLink.getReroutes(
@@ -656,8 +653,8 @@ describe('LinkConnector Integration', () => {
graph,
connector
}) => {
const hasOutputNode = graph.getNodeById(toNodeId(1))!
const hasInputNode = graph.getNodeById(toNodeId(2))!
const hasOutputNode = graph.getNodeById(1)!
const hasInputNode = graph.getNodeById(2)!
const reroutesBefore = LLink.getReroutes(
graph,
@@ -671,9 +668,7 @@ describe('LinkConnector Integration', () => {
connector.reset()
expect(hasOutputNode.getOutputNodes(0)).toEqual([hasInputNode])
expect(hasInputNode.getOutputNodes(0)).toEqual([
graph.getNodeById(toNodeId(3))
])
expect(hasInputNode.getOutputNodes(0)).toEqual([graph.getNodeById(3)])
// Moved link should have the same reroutes
const reroutesAfter = LLink.getReroutes(
@@ -697,7 +692,7 @@ describe('LinkConnector Integration', () => {
connector,
floatingReroute
}) => {
const disconnectedNode = graph.getNodeById(toNodeId(9))!
const disconnectedNode = graph.getNodeById(9)!
const canvasX = disconnectedNode.pos[0]
const canvasY = disconnectedNode.pos[1]
@@ -734,13 +729,13 @@ describe('LinkConnector Integration', () => {
graph,
connector
}) => {
const manyOutputsNode = graph.getNodeById(toNodeId(4))!
const manyOutputsNode = graph.getNodeById(4)!
manyOutputsNode.disconnectOutput(0)
const floatingInputNode = graph.getNodeById(toNodeId(6))!
const floatingInputNode = graph.getNodeById(6)!
const fromFloatingInput = floatingInputNode.inputs[0]
const hasInputNode = graph.getNodeById(toNodeId(2))!
const hasInputNode = graph.getNodeById(2)!
const toInput = hasInputNode.inputs[0]
connector.moveInputLink(graph, fromFloatingInput)
@@ -762,7 +757,7 @@ describe('LinkConnector Integration', () => {
validateIntegrityNoChanges
}) => {
const rerouteWithTwoLinks = graph.reroutes.get(3)!
const targetNode = graph.getNodeById(toNodeId(2))!
const targetNode = graph.getNodeById(2)!
const targetDropEvent = mockedInputDropEvent(targetNode, 0)
@@ -801,10 +796,10 @@ describe('LinkConnector Integration', () => {
reroutesBeforeTest,
validateIntegrityNoChanges
}) => {
const floatingOutNode = graph.getNodeById(toNodeId(1))!
const floatingOutNode = graph.getNodeById(1)!
connector.moveOutputLink(graph, floatingOutNode.outputs[0])
const manyOutputsNode = graph.getNodeById(toNodeId(4))!
const manyOutputsNode = graph.getNodeById(4)!
const dropEvent = createMockCanvasPointerEvent(
manyOutputsNode.pos[0],
manyOutputsNode.pos[1]
@@ -821,7 +816,7 @@ describe('LinkConnector Integration', () => {
// Move again
connector.moveOutputLink(graph, manyOutputsNode.outputs[0])
const disconnectedNode = graph.getNodeById(toNodeId(9))!
const disconnectedNode = graph.getNodeById(9)!
const dropEvent2 = createMockCanvasPointerEvent(
disconnectedNode.pos[0],
disconnectedNode.pos[1]
@@ -857,7 +852,7 @@ describe('LinkConnector Integration', () => {
const {
inputs: [input],
outputs: [output]
} = graph.getNodeById(toNodeId(nodeId))!
} = graph.getNodeById(nodeId)!
expect(input.link).toBeNull()
@@ -947,7 +942,7 @@ describe('LinkConnector Integration', () => {
const linkCreatedCallback = vi.fn()
connector.listenUntilReset('link-created', linkCreatedCallback)
const disconnectedNode = graph.getNodeById(toNodeId(9))!
const disconnectedNode = graph.getNodeById(9)!
// Parent reroutes of the target reroute
for (const [index, parentId] of parentIds.entries()) {
@@ -1074,7 +1069,7 @@ describe('LinkConnector Integration', () => {
) => {
if (testFloatingInputs) {
// Start by disconnecting the output of the 3x3 array of reroutes
graph.getNodeById(toNodeId(4))!.disconnectOutput(0)
graph.getNodeById(4)!.disconnectOutput(0)
}
const fromReroute = graph.reroutes.get(fromRerouteId)!
@@ -1188,15 +1183,15 @@ describe('LinkConnector Integration', () => {
)
const nodeReroutePairs = [
{ nodeId: toNodeId(1), rerouteId: 1 },
{ nodeId: toNodeId(1), rerouteId: 3 },
{ nodeId: toNodeId(1), rerouteId: 4 },
{ nodeId: toNodeId(1), rerouteId: 2 },
{ nodeId: toNodeId(4), rerouteId: 7 },
{ nodeId: toNodeId(4), rerouteId: 6 },
{ nodeId: toNodeId(4), rerouteId: 8 },
{ nodeId: toNodeId(4), rerouteId: 10 },
{ nodeId: toNodeId(4), rerouteId: 12 }
{ nodeId: 1, rerouteId: 1 },
{ nodeId: 1, rerouteId: 3 },
{ nodeId: 1, rerouteId: 4 },
{ nodeId: 1, rerouteId: 2 },
{ nodeId: 4, rerouteId: 7 },
{ nodeId: 4, rerouteId: 6 },
{ nodeId: 4, rerouteId: 8 },
{ nodeId: 4, rerouteId: 10 },
{ nodeId: 4, rerouteId: 12 }
]
test.for(nodeReroutePairs)(
'Should ignore connections from input to same node via reroutes',
@@ -1207,7 +1202,7 @@ describe('LinkConnector Integration', () => {
const listener = vi.fn()
connector.listenUntilReset('link-created', listener)
const node = graph.getNodeById(toNodeId(nodeId))!
const node = graph.getNodeById(nodeId)!
const input = node.inputs[0]
const reroute = graph.getReroute(rerouteId)!
const dropEvent = createMockCanvasPointerEvent(
@@ -1238,7 +1233,7 @@ describe('LinkConnector Integration', () => {
const listener = vi.fn()
connector.listenUntilReset('link-created', listener)
const node = graph.getNodeById(toNodeId(nodeId))!
const node = graph.getNodeById(nodeId)!
const reroute = graph.getReroute(rerouteId)!
const dropEvent = createMockCanvasPointerEvent(node.pos[0], node.pos[1])
@@ -1265,7 +1260,7 @@ describe('LinkConnector Integration', () => {
const listener = vi.fn()
connector.listenUntilReset('link-created', listener)
const node = graph.getNodeById(toNodeId(nodeId))!
const node = graph.getNodeById(nodeId)!
const reroute = graph.getReroute(rerouteId)!
const inputPos = node.getInputPos(0)
const dropOnInputEvent = createMockCanvasPointerEvent(

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
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 } from '@/lib/litegraph/src/LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
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'
@@ -58,6 +57,6 @@ export interface LGraphCanvasEventMap {
/** Ghost placement mode has started or ended. */
'litegraph:ghost-placement': {
active: boolean
nodeId: SerializedNodeId
nodeId: NodeId
}
}

View File

@@ -1,6 +1,5 @@
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { SerializedNodeId } from '@/types/nodeId'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode, NodeId } 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'
@@ -60,21 +59,21 @@ export interface LGraphEventMap {
}
'node:property:changed': {
nodeId: SerializedNodeId
nodeId: NodeId
property: string
oldValue: unknown
newValue: unknown
}
'node:slot-errors:changed': { nodeId: SerializedNodeId }
'node:slot-errors:changed': { nodeId: NodeId }
'node:slot-links:changed': {
nodeId: SerializedNodeId
nodeId: NodeId
slotType: NodeSlotType
slotIndex: number
connected: boolean
linkId: number
}
'node:slot-label:changed': {
nodeId: SerializedNodeId
nodeId: NodeId
slotType?: NodeSlotType
}
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import type { LGraph } from '@/lib/litegraph/src/LGraph'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { SerializedNodeId } from '@/types/nodeId'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LinkId } from '@/lib/litegraph/src/LLink'
import { InvalidLinkError } from '@/lib/litegraph/src/infrastructure/InvalidLinkError'
import { useWidgetValueStore } from '@/stores/widgetValueStore'
@@ -100,7 +99,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 SerializedNodeId[],
readonly subgraphNodePath: readonly NodeId[],
/** 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,5 +1,4 @@
import { serializeNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type {
@@ -369,7 +368,7 @@ export abstract class SubgraphIONodeBase<
asSerialisable(): ExportedSubgraphIONode {
return {
id: serializeNodeId(this.id),
id: this.id,
bounding: this.boundingRect.export(),
pinned: this.pinned ? true : undefined
}

View File

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

View File

@@ -1,6 +1,5 @@
import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { NodeId } from '@/types/nodeId'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
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

@@ -17,7 +17,6 @@ import {
} from '@/lib/litegraph/src/litegraph'
import type { ISlotType } from '@/lib/litegraph/src/litegraph'
import { toNodeId } from '@/types/nodeId'
import {
createTestSubgraph,
createTestSubgraphNode,
@@ -477,8 +476,8 @@ describe('SubgraphSerialization - Data Integrity', () => {
expect(restored.links.size).toBe(2)
for (const [, link] of restored.links) {
const originNode = restored.getNodeById(toNodeId(link.origin_id))
const targetNode = restored.getNodeById(toNodeId(link.target_id))
const originNode = restored.getNodeById(link.origin_id)
const targetNode = restored.getNodeById(link.target_id)
expect(originNode).toBeDefined()
expect(targetNode).toBeDefined()
expect(link.origin_slot).toBeGreaterThanOrEqual(0)
@@ -493,19 +492,16 @@ describe('SubgraphSerialization - Data Integrity', () => {
graph.configure(structuredClone(duplicateSubgraphNodeIds))
const rootIds = graph.nodes
.map((node) => Number(node.id))
.map((node) => node.id)
.filter((id): id is number => typeof id === 'number')
.sort((a, b) => a - b)
expect(rootIds).toEqual([102, 103])
const subgraphAIds = new Set(
graph.subgraphs
.get(DUPLICATE_ID_SUBGRAPH_A)!
.nodes.map((node) => Number(node.id))
graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_A)!.nodes.map((node) => node.id)
)
const subgraphBIds = new Set(
graph.subgraphs
.get(DUPLICATE_ID_SUBGRAPH_B)!
.nodes.map((node) => Number(node.id))
graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_B)!.nodes.map((node) => node.id)
)
expect(subgraphAIds).toEqual(new Set([3, 8, 37]))
@@ -526,15 +522,13 @@ describe('SubgraphSerialization - Data Integrity', () => {
const subgraphB = graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_B)!
const subgraphBIds = new Set(subgraphB.nodes.map((node) => String(node.id)))
const rootProxyWidgetsA = graph.getNodeById(toNodeId(102))?.properties
?.proxyWidgets
const rootProxyWidgetsA = graph.getNodeById(102)?.properties?.proxyWidgets
expect(Array.isArray(rootProxyWidgetsA)).toBe(true)
for (const entry of rootProxyWidgetsA as string[][]) {
expect(subgraphAIds.has(String(entry[0]))).toBe(true)
}
const rootProxyWidgetsB = graph.getNodeById(toNodeId(103))?.properties
?.proxyWidgets
const rootProxyWidgetsB = graph.getNodeById(103)?.properties?.proxyWidgets
expect(Array.isArray(rootProxyWidgetsB)).toBe(true)
for (const entry of rootProxyWidgetsB as string[][]) {
expect(subgraphBIds.has(String(entry[0]))).toBe(true)

View File

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

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