mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-25 17:17:19 +00:00
Compare commits
1 Commits
drjkl/bran
...
DynamicGro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
842e3d7541 |
@@ -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
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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')!
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
:key="nodeData.id"
|
||||
:node-data="nodeData"
|
||||
:error="
|
||||
executionErrorStore.lastExecutionErrorNodeId === nodeData.id
|
||||
executionErrorStore.lastExecutionError?.node_id === nodeData.id
|
||||
? 'Execution error'
|
||||
: null
|
||||
"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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: '' })
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')"
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } }
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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++
|
||||
})
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ?? {}
|
||||
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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', () => ({}))
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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]))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user