mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-25 09:09:14 +00:00
Compare commits
23 Commits
codex/fix-
...
drjkl/bran
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85008b7c0b | ||
|
|
b7cb8cb0ec | ||
|
|
1e45c0a405 | ||
|
|
a9dfcfa445 | ||
|
|
82108ea87e | ||
|
|
0cb521ae06 | ||
|
|
1cb6fb6c23 | ||
|
|
6c4cf12a04 | ||
|
|
dc74b0edc2 | ||
|
|
b7c262d327 | ||
|
|
2fb4e24894 | ||
|
|
a79b78deef | ||
|
|
cd94b01d9f | ||
|
|
797d9c83c7 | ||
|
|
dfafeae897 | ||
|
|
c79aeaf16d | ||
|
|
7c717e2570 | ||
|
|
57f891836f | ||
|
|
7b57fb0bb9 | ||
|
|
c4ac059027 | ||
|
|
f79ab0399d | ||
|
|
c53ee739b1 | ||
|
|
3ba1b05893 |
@@ -179,6 +179,9 @@ This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g
|
||||
23. Favor pure functions (especially testable ones)
|
||||
24. Do not use function expressions if it's possible to use function declarations instead
|
||||
25. Watch out for [Code Smells](https://wiki.c2.com/?CodeSmell) and refactor to avoid them
|
||||
26. Do not add alias helpers whose implementation is just a single-line call to another function
|
||||
- Bad: `function id(value) { return nodeId(value) }`
|
||||
- Use the real function directly, or introduce a named helper only when it adds validation, branching, domain meaning, or shared behavior beyond renaming
|
||||
|
||||
## Design Standards
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import WidgetBoundingBoxes from './WidgetBoundingBoxes.vue'
|
||||
import boundingBoxes from '@/locales/en/main.json'
|
||||
import type { BoundingBox } from '@/types/boundingBoxes'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
|
||||
const { appState } = vi.hoisted(() => ({ appState: { node: null as unknown } }))
|
||||
|
||||
@@ -83,7 +84,7 @@ function prepCanvas(canvas: HTMLCanvasElement) {
|
||||
|
||||
function renderWidget(modelValue: BoundingBox[]) {
|
||||
const result = render(WidgetBoundingBoxes, {
|
||||
props: { nodeId: '1', modelValue },
|
||||
props: { nodeId: toNodeId('1'), modelValue },
|
||||
global: { plugins: [i18n] }
|
||||
})
|
||||
const canvas = screen.getByTestId('bounding-boxes').querySelector('canvas')!
|
||||
|
||||
@@ -145,8 +145,9 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import Textarea from '@/components/ui/textarea/Textarea.vue'
|
||||
import { useBoundingBoxes } from '@/composables/boundingBoxes/useBoundingBoxes'
|
||||
import type { BoundingBox } from '@/types/boundingBoxes'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
const { nodeId } = defineProps<{ nodeId: string }>()
|
||||
const { nodeId } = defineProps<{ nodeId: NodeId }>()
|
||||
const modelValue = defineModel<BoundingBox[]>({ default: () => [] })
|
||||
|
||||
const canvasEl = useTemplateRef<HTMLCanvasElement>('canvasEl')
|
||||
|
||||
@@ -12,8 +12,9 @@ import { useResolvedSelectedInputs } from '@/components/builder/useResolvedSelec
|
||||
import type { ResolvedSelection } from '@/components/builder/useResolvedSelectedInputs'
|
||||
import type { WidgetId } from '@/types/widgetId'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
TitleMode
|
||||
@@ -50,7 +51,7 @@ workflowStore.activeWorkflow?.changeTracker?.reset()
|
||||
|
||||
const resolvedInputs = useResolvedSelectedInputs()
|
||||
|
||||
const outputsWithState = computed<[NodeId, string][]>(() =>
|
||||
const outputsWithState = computed<[SerializedNodeId, string][]>(() =>
|
||||
appModeStore.selectedOutputs.map((nodeId) => [
|
||||
nodeId,
|
||||
app.rootGraph.getNodeById(nodeId)?.title ?? String(nodeId)
|
||||
@@ -75,7 +76,7 @@ function getHovered(
|
||||
if (widget || node.constructor.nodeData?.output_node) return [node, widget]
|
||||
}
|
||||
|
||||
function getNodeBounding(nodeId: NodeId) {
|
||||
function getNodeBounding(nodeId: SerializedNodeId) {
|
||||
if (settingStore.get('Comfy.VueNodes.Enabled')) return undefined
|
||||
const node = app.rootGraph.getNodeById(nodeId)
|
||||
if (!node) return
|
||||
@@ -149,7 +150,7 @@ function handleClick(e: MouseEvent) {
|
||||
|
||||
function nodeToDisplayTuple(
|
||||
n: LGraphNode
|
||||
): [NodeId, MaybeRef<BoundStyle> | undefined, boolean] {
|
||||
): [SerializedNodeId, MaybeRef<BoundStyle> | undefined, boolean] {
|
||||
return [
|
||||
n.id,
|
||||
getNodeBounding(n.id),
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { parseImageWidgetValue } from '@/utils/imageUtil'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { HideLayoutFieldKey } from '@/types/widgetTypes'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import { promptRenameWidget } from '@/utils/widgetUtil'
|
||||
|
||||
interface WidgetEntry {
|
||||
@@ -75,7 +76,7 @@ const mappedSelections = computed((): WidgetEntry[] => {
|
||||
if (!matchingWidget) return []
|
||||
|
||||
matchingWidget.slotMetadata = undefined
|
||||
matchingWidget.nodeId = String(node.id)
|
||||
matchingWidget.nodeId = toNodeId(node.id)
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -139,7 +140,7 @@ async function handleDragDrop() {
|
||||
return false
|
||||
}
|
||||
|
||||
app.dragOverNode = { id: -1, onDragDrop }
|
||||
app.dragOverNode = { id: toNodeId(-1), onDragDrop }
|
||||
}
|
||||
|
||||
defineExpose({ handleDragDrop })
|
||||
|
||||
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
const i18n = createI18n({
|
||||
@@ -239,7 +240,7 @@ describe('WidgetCurve', () => {
|
||||
renderWidget(
|
||||
makeWidget({
|
||||
options: { disabled: true },
|
||||
linkedUpstream: { nodeId: 'n1' }
|
||||
linkedUpstream: { nodeId: toNodeId('n1') }
|
||||
})
|
||||
)
|
||||
const parsed = JSON.parse(
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
|
||||
type TestWidget = BaseDOMWidget<object | string>
|
||||
|
||||
@@ -21,7 +22,7 @@ function createNode(
|
||||
pos: [number, number]
|
||||
) {
|
||||
const node = new LGraphNode(title)
|
||||
node.id = id
|
||||
node.id = nodeId(id)
|
||||
node.pos = [...pos]
|
||||
node.size = [240, 120]
|
||||
graph.add(node)
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
:key="nodeData.id"
|
||||
:node-data="nodeData"
|
||||
:error="
|
||||
executionErrorStore.lastExecutionError?.node_id === nodeData.id
|
||||
executionErrorStore.lastExecutionErrorNodeId === nodeData.id
|
||||
? 'Execution error'
|
||||
: null
|
||||
"
|
||||
|
||||
@@ -3,6 +3,9 @@ import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, nextTick, ref } from 'vue'
|
||||
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
const execHolder = vi.hoisted(() => ({
|
||||
state: null as {
|
||||
executingNodeIds: Array<string | number>
|
||||
@@ -35,7 +38,7 @@ const SkeletonStub = defineComponent({
|
||||
|
||||
function renderPreview(
|
||||
text: string,
|
||||
{ nodeId = 'node-1' }: { nodeId?: string | number } = {}
|
||||
{ nodeId = toNodeId('node-1') }: { nodeId?: NodeId } = {}
|
||||
) {
|
||||
const value = ref(text)
|
||||
const Harness = defineComponent({
|
||||
@@ -167,21 +170,21 @@ describe('TextPreviewWidget', () => {
|
||||
it('hides the Skeleton on mount when execution is already idle', () => {
|
||||
execState().executingNodeIds = []
|
||||
execState().isIdle = true
|
||||
renderPreview('text', { nodeId: 'n1' })
|
||||
renderPreview('text', { nodeId: toNodeId('n1') })
|
||||
expect(screen.queryByTestId('skeleton')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows a Skeleton on mount when the parent node is executing', () => {
|
||||
execState().executingNodeIds = ['n1']
|
||||
execState().isIdle = false
|
||||
renderPreview('text', { nodeId: 'n1' })
|
||||
renderPreview('text', { nodeId: toNodeId('n1') })
|
||||
expect(screen.getByTestId('skeleton')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides the Skeleton when execution transitions to idle', async () => {
|
||||
execState().executingNodeIds = ['n1']
|
||||
execState().isIdle = false
|
||||
renderPreview('text', { nodeId: 'n1' })
|
||||
renderPreview('text', { nodeId: toNodeId('n1') })
|
||||
expect(screen.getByTestId('skeleton')).toBeInTheDocument()
|
||||
|
||||
execState().executingNodeIds = []
|
||||
@@ -194,7 +197,7 @@ describe('TextPreviewWidget', () => {
|
||||
it('hides the Skeleton when the parent node leaves executingNodeIds', async () => {
|
||||
execState().executingNodeIds = ['n1']
|
||||
execState().isIdle = false
|
||||
renderPreview('text', { nodeId: 'n1' })
|
||||
renderPreview('text', { nodeId: toNodeId('n1') })
|
||||
|
||||
execState().executingNodeIds = ['other']
|
||||
await nextTick()
|
||||
|
||||
@@ -16,8 +16,9 @@ import { default as DOMPurify } from 'dompurify'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
@@ -76,7 +77,7 @@ onMounted(() => {
|
||||
watch(
|
||||
() => executionStore.executingNodeIds,
|
||||
(ids) => {
|
||||
if (!parentNodeId && ids.length > 0) parentNodeId = ids[0]
|
||||
if (!parentNodeId && ids.length > 0) parentNodeId = toNodeId(ids[0])
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { defineComponent, ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
@@ -132,11 +133,12 @@ function renderWidget(
|
||||
initialModel: Bounds = { x: 0, y: 0, width: 512, height: 512 }
|
||||
) {
|
||||
const value = ref<Bounds>(initialModel)
|
||||
const nodeId = toNodeId(1)
|
||||
const Harness = defineComponent({
|
||||
components: { WidgetImageCrop },
|
||||
setup: () => ({ value, widget }),
|
||||
setup: () => ({ value, widget, nodeId }),
|
||||
template:
|
||||
'<WidgetImageCrop v-model="value" :widget="widget" :node-id="1" />'
|
||||
'<WidgetImageCrop v-model="value" :widget="widget" :node-id="nodeId" />'
|
||||
})
|
||||
const utils = render(Harness, {
|
||||
global: {
|
||||
@@ -233,7 +235,7 @@ describe('WidgetImageCrop', () => {
|
||||
renderWidget(
|
||||
makeWidget({
|
||||
options: { disabled: true },
|
||||
linkedUpstream: { nodeId: 'n1' }
|
||||
linkedUpstream: { nodeId: toNodeId('n1') }
|
||||
}),
|
||||
{ x: 0, y: 0, width: 512, height: 512 }
|
||||
)
|
||||
|
||||
@@ -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,6 +6,8 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
const { load3dState, resolveNodeMock, settingGetMock } = vi.hoisted(() => ({
|
||||
load3dState: {
|
||||
@@ -83,7 +85,7 @@ const i18n = createI18n({
|
||||
|
||||
type RenderOptions = {
|
||||
widget?: unknown
|
||||
nodeId?: number | string
|
||||
nodeId?: NodeId
|
||||
stateOverrides?: Partial<ReturnType<typeof buildLoad3dStub>>
|
||||
enable3DViewer?: boolean
|
||||
}
|
||||
@@ -165,16 +167,17 @@ describe('Load3D', () => {
|
||||
})
|
||||
|
||||
it('falls back to resolveNode(nodeId) when the widget lacks a node', async () => {
|
||||
const nodeId = toNodeId(42)
|
||||
resolveNodeMock.mockReturnValue(MOCK_NODE)
|
||||
renderLoad3D({ widget: {}, nodeId: 42 })
|
||||
renderLoad3D({ widget: {}, nodeId })
|
||||
|
||||
expect(resolveNodeMock).toHaveBeenCalledWith(42)
|
||||
expect(resolveNodeMock).toHaveBeenCalledWith(nodeId)
|
||||
expect(await screen.findByTestId('load3d-scene')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not render Load3DScene when no node can be resolved', async () => {
|
||||
resolveNodeMock.mockReturnValue(null)
|
||||
renderLoad3D({ widget: {}, nodeId: 99 })
|
||||
renderLoad3D({ widget: {}, nodeId: toNodeId(99) })
|
||||
|
||||
await Promise.resolve()
|
||||
expect(screen.queryByTestId('load3d-scene')).not.toBeInTheDocument()
|
||||
@@ -219,7 +222,11 @@ describe('Load3D', () => {
|
||||
|
||||
it('hides ViewerControls when there is no node even if the setting is on', () => {
|
||||
resolveNodeMock.mockReturnValue(null)
|
||||
renderLoad3D({ widget: {}, nodeId: 1, enable3DViewer: true })
|
||||
renderLoad3D({
|
||||
widget: {},
|
||||
nodeId: toNodeId(1),
|
||||
enable3DViewer: true
|
||||
})
|
||||
expect(screen.queryByTestId('viewer-controls')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -115,10 +115,10 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { useLoad3d } from '@/composables/useLoad3d'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
|
||||
const {
|
||||
widget,
|
||||
|
||||
@@ -2,6 +2,8 @@ import { render } from '@testing-library/vue'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, h, ref } from 'vue'
|
||||
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
|
||||
const lastProps = ref<Record<string, unknown> | null>(null)
|
||||
|
||||
vi.mock('@/components/load3d/Load3D.vue', () => ({
|
||||
@@ -39,9 +41,10 @@ describe('Load3DAdvanced', () => {
|
||||
})
|
||||
|
||||
it('forwards widget and nodeId to the inner Load3D', () => {
|
||||
const nodeId = toNodeId('a')
|
||||
const widget = { node: { id: 'a', type: 'Load3DAdvanced' } }
|
||||
render(Load3DAdvanced, { props: { widget: widget as never, nodeId: 'a' } })
|
||||
render(Load3DAdvanced, { props: { widget: widget as never, nodeId } })
|
||||
expect(lastProps.value?.widget).toEqual(widget)
|
||||
expect(lastProps.value?.nodeId).toBe('a')
|
||||
expect(lastProps.value?.nodeId).toBe(nodeId)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,11 +289,12 @@ import { computed, useTemplateRef } from 'vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import Slider from '@/components/ui/slider/Slider.vue'
|
||||
import { PAINTER_TOOLS, usePainter } from '@/composables/painter/usePainter'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { toHexFromFormat } from '@/utils/colorUtil'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { nodeId } = defineProps<{
|
||||
nodeId: string
|
||||
nodeId: NodeId
|
||||
}>()
|
||||
|
||||
const modelValue = defineModel<string>({ default: '' })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineComponent, ref } from 'vue'
|
||||
@@ -135,7 +136,7 @@ describe('WidgetRange', () => {
|
||||
setUpstream({ min: 0.3, max: 0.7 })
|
||||
renderWidget(
|
||||
makeWidget({ disabled: true } as IWidgetRangeOptions, {
|
||||
linkedUpstream: { nodeId: 'n1' }
|
||||
linkedUpstream: { nodeId: toNodeId('n1') }
|
||||
}),
|
||||
{ min: 0, max: 1 }
|
||||
)
|
||||
@@ -145,10 +146,13 @@ describe('WidgetRange', () => {
|
||||
|
||||
it('ignores upstream value when not disabled', () => {
|
||||
setUpstream({ min: 0.3, max: 0.7 })
|
||||
renderWidget(makeWidget({}, { linkedUpstream: { nodeId: 'n1' } }), {
|
||||
min: 0,
|
||||
max: 1
|
||||
})
|
||||
renderWidget(
|
||||
makeWidget({}, { linkedUpstream: { nodeId: toNodeId('n1') } }),
|
||||
{
|
||||
min: 0,
|
||||
max: 1
|
||||
}
|
||||
)
|
||||
const el = screen.getByTestId('range-editor')
|
||||
expect(JSON.parse(el.dataset.model!)).toEqual({ min: 0, max: 1 })
|
||||
})
|
||||
|
||||
@@ -22,6 +22,7 @@ import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
@@ -149,10 +150,11 @@ function isWidgetShownOnParents(
|
||||
const source = widgetPromotedSource(widgetNode, widget)
|
||||
return parents.some((parent) => {
|
||||
if (source) {
|
||||
const widgetNodeId = toNodeId(widgetNode.id)
|
||||
const interiorNodeId =
|
||||
String(widgetNode.id) === String(parent.id)
|
||||
? source.nodeId
|
||||
: String(widgetNode.id)
|
||||
: widgetNodeId
|
||||
|
||||
return isWidgetPromotedOnSubgraphNode(parent, {
|
||||
sourceNodeId: interiorNodeId,
|
||||
@@ -160,7 +162,7 @@ function isWidgetShownOnParents(
|
||||
})
|
||||
}
|
||||
return isWidgetPromotedOnSubgraphNode(parent, {
|
||||
sourceNodeId: String(widgetNode.id),
|
||||
sourceNodeId: toNodeId(widgetNode.id),
|
||||
sourceWidgetName: widget.name
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,11 +4,13 @@ import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
||||
import type { NodeWidgetsListList } from '../shared'
|
||||
@@ -56,12 +58,12 @@ function setSectionCollapsed(nodeId: NodeId, collapsed: boolean) {
|
||||
const isAllCollapsed = computed({
|
||||
get() {
|
||||
return searchedWidgetsSectionDataList.value.every(({ node }) =>
|
||||
isSectionCollapsed(node.id)
|
||||
isSectionCollapsed(toNodeId(node.id))
|
||||
)
|
||||
},
|
||||
set(collapse: boolean) {
|
||||
for (const { node } of widgetsSectionDataList.value) {
|
||||
setSectionCollapsed(node.id, collapse)
|
||||
setSectionCollapsed(toNodeId(node.id), collapse)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -101,7 +103,7 @@ async function searcher(query: string) {
|
||||
:key="node.id"
|
||||
:node
|
||||
:widgets
|
||||
:collapse="isSectionCollapsed(node.id) && !isSearching"
|
||||
:collapse="isSectionCollapsed(toNodeId(node.id)) && !isSearching"
|
||||
:tooltip="
|
||||
isSearching || widgets.length
|
||||
? ''
|
||||
@@ -109,7 +111,7 @@ async function searcher(query: string) {
|
||||
"
|
||||
show-locate-button
|
||||
class="border-b border-interface-stroke"
|
||||
@update:collapse="setSectionCollapsed(node.id, $event)"
|
||||
@update:collapse="setSectionCollapsed(toNodeId(node.id), $event)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
@@ -3,11 +3,13 @@ import { storeToRefs } from 'pinia'
|
||||
import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
|
||||
import type { NodeWidgetsListList } from '../shared'
|
||||
@@ -80,7 +82,7 @@ function setSectionCollapsed(nodeId: NodeId, collapsed: boolean) {
|
||||
const isAllCollapsed = computed({
|
||||
get() {
|
||||
const normalAllCollapsed = searchedWidgetsSectionDataList.value.every(
|
||||
({ node }) => isSectionCollapsed(node.id)
|
||||
({ node }) => isSectionCollapsed(toNodeId(node.id))
|
||||
)
|
||||
const hasAdvanced = advancedWidgetsSectionDataList.value.length > 0
|
||||
return hasAdvanced
|
||||
@@ -89,7 +91,7 @@ const isAllCollapsed = computed({
|
||||
},
|
||||
set(collapse: boolean) {
|
||||
for (const { node } of widgetsSectionDataList.value) {
|
||||
setSectionCollapsed(node.id, collapse)
|
||||
setSectionCollapsed(toNodeId(node.id), collapse)
|
||||
}
|
||||
advancedCollapsed.value = collapse
|
||||
}
|
||||
@@ -154,7 +156,7 @@ const advancedLabel = computed(() => {
|
||||
:node
|
||||
:label
|
||||
:widgets
|
||||
:collapse="isSectionCollapsed(node.id) && !isSearching"
|
||||
:collapse="isSectionCollapsed(toNodeId(node.id)) && !isSearching"
|
||||
:show-locate-button="isMultipleNodesSelected"
|
||||
:tooltip="
|
||||
isSearching || widgets.length
|
||||
@@ -162,7 +164,7 @@ const advancedLabel = computed(() => {
|
||||
: t('rightSidePanel.inputsNoneTooltip')
|
||||
"
|
||||
class="border-b border-interface-stroke"
|
||||
@update:collapse="setSectionCollapsed(node.id, $event)"
|
||||
@update:collapse="setSectionCollapsed(toNodeId(node.id), $event)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
<template v-if="advancedWidgetsSectionDataList.length > 0 && !isSearching">
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
|
||||
import { searchWidgets } from '../shared'
|
||||
import type { NodeWidgetsList } from '../shared'
|
||||
@@ -82,7 +83,7 @@ const advancedInputsWidgets = computed((): NodeWidgetsList => {
|
||||
return allInteriorWidgets.filter(
|
||||
({ node: interiorNode, widget }) =>
|
||||
!isWidgetPromotedOnSubgraphNode(node, {
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceNodeId: toNodeId(interiorNode.id),
|
||||
sourceWidgetName: getWidgetName(widget)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import { getWidgetDefaultValue, promptWidgetLabel } from '@/utils/widgetUtil'
|
||||
import type { WidgetValue } from '@/utils/widgetUtil'
|
||||
|
||||
@@ -90,9 +91,10 @@ function handleHideInput() {
|
||||
|
||||
const source = widgetPromotedSource(node, widget)
|
||||
if (source) {
|
||||
const currentNodeId = toNodeId(node.id)
|
||||
for (const parent of parents) {
|
||||
const sourceNodeId =
|
||||
String(node.id) === String(parent.id) ? source.nodeId : String(node.id)
|
||||
String(node.id) === String(parent.id) ? source.nodeId : currentNodeId
|
||||
demotePromotedInput(parent, {
|
||||
sourceNodeId,
|
||||
sourceWidgetName: source.widgetName
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
useWidgetValueStore
|
||||
} from '@/stores/widgetValueStore'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { widgetId } from '@/types/widgetId'
|
||||
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
|
||||
@@ -70,7 +71,7 @@ const widgetComponent = computed(() => {
|
||||
|
||||
const isLinked = computed(() => {
|
||||
const safeWidget = useVueNodeLifecycle()
|
||||
.nodeManager.value?.vueNodeData.get(String(node.id))
|
||||
.nodeManager.value?.vueNodeData.get(toNodeId(node.id))
|
||||
?.widgets?.find((w) => w.name === widget.name)
|
||||
return safeWidget?.slotMetadata
|
||||
? !!safeWidget.slotMetadata.linked
|
||||
@@ -212,7 +213,7 @@ const displayLabel = customRef((track, trigger) => {
|
||||
:is="widgetComponent"
|
||||
v-model="widgetValue"
|
||||
:widget="simplifiedWidget"
|
||||
:node-id="String(node.id)"
|
||||
:node-id="toNodeId(node.id)"
|
||||
:node-type="node.type"
|
||||
:class="cn('col-span-1', shouldExpand(widget.type) && 'min-h-36')"
|
||||
/>
|
||||
|
||||
@@ -5,8 +5,10 @@ import type { IFuseOptions } from 'fuse.js'
|
||||
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
@@ -92,7 +94,7 @@ export function searchWidgetsAndNodes(
|
||||
}
|
||||
|
||||
const searchableList: NodeSearchItem[] = list.map((item) => ({
|
||||
nodeId: item.node.id,
|
||||
nodeId: toNodeId(item.node.id),
|
||||
searchableTitle: (item.node.getTitle() ?? '').toLowerCase()
|
||||
}))
|
||||
|
||||
@@ -108,8 +110,8 @@ export function searchWidgetsAndNodes(
|
||||
)
|
||||
|
||||
return list
|
||||
.map((item) => {
|
||||
if (matchedNodeIds.has(item.node.id)) {
|
||||
.map((item, index) => {
|
||||
if (matchedNodeIds.has(searchableList[index].nodeId)) {
|
||||
return { ...item, keep: true }
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -32,6 +32,7 @@ import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
import SubgraphNodeWidget from './SubgraphNodeWidget.vue'
|
||||
@@ -116,7 +117,7 @@ function getActivePreviewRows(node: SubgraphNode): PreviewRow[] {
|
||||
const rootGraphId = node.rootGraph.id
|
||||
const exposures = previewExposureStore.getExposures(rootGraphId, hostLocator)
|
||||
return exposures.flatMap((exposure): PreviewRow[] => {
|
||||
const sourceNode = node.subgraph._nodes_by_id[exposure.sourceNodeId]
|
||||
const sourceNode = node.subgraph.getNodeById(exposure.sourceNodeId)
|
||||
if (!sourceNode) return []
|
||||
const realWidget = getPromotableWidgets(sourceNode).find(
|
||||
(candidate) => candidate.name === exposure.sourcePreviewName
|
||||
@@ -248,7 +249,7 @@ function rowDisplayName(row: ActiveRow): string {
|
||||
|
||||
function isRowLinked(row: ActiveRow): boolean {
|
||||
if (row.kind !== 'promoted') return false
|
||||
if (row.node.id === -1) return true
|
||||
if (row.node.id === toNodeId(-1)) return true
|
||||
const source = promotedRowSource(row)
|
||||
return (
|
||||
!!activeNode.value &&
|
||||
|
||||
@@ -4,8 +4,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
|
||||
import MediaLightbox from './MediaLightbox.vue'
|
||||
|
||||
@@ -28,7 +28,7 @@ type MockResultItem = Partial<ResultItemImpl> & {
|
||||
filename: string
|
||||
subfolder: string
|
||||
type: string
|
||||
nodeId: NodeId
|
||||
nodeId: SerializedNodeId
|
||||
mediaType: string
|
||||
id?: string
|
||||
url?: string
|
||||
@@ -63,7 +63,7 @@ describe('MediaLightbox', () => {
|
||||
filename: 'image1.jpg',
|
||||
subfolder: 'outputs',
|
||||
type: 'output',
|
||||
nodeId: '123' as NodeId,
|
||||
nodeId: '123',
|
||||
mediaType: 'images',
|
||||
isImage: true,
|
||||
isVideo: false,
|
||||
@@ -75,7 +75,7 @@ describe('MediaLightbox', () => {
|
||||
filename: 'image2.jpg',
|
||||
subfolder: 'outputs',
|
||||
type: 'output',
|
||||
nodeId: '456' as NodeId,
|
||||
nodeId: '456',
|
||||
mediaType: 'images',
|
||||
isImage: true,
|
||||
isVideo: false,
|
||||
@@ -87,7 +87,7 @@ describe('MediaLightbox', () => {
|
||||
filename: 'image3.jpg',
|
||||
subfolder: 'outputs',
|
||||
type: 'output',
|
||||
nodeId: '789' as NodeId,
|
||||
nodeId: '789',
|
||||
mediaType: 'images',
|
||||
isImage: true,
|
||||
isVideo: false,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { defineComponent, h, nextTick, ref, shallowRef } from 'vue'
|
||||
|
||||
import { useBoundingBoxes } from './useBoundingBoxes'
|
||||
import type { BoundingBox } from '@/types/boundingBoxes'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
|
||||
const { appState } = vi.hoisted(() => ({
|
||||
appState: { node: null as unknown }
|
||||
@@ -103,7 +104,7 @@ function setup(initial: BoundingBox[] = []) {
|
||||
const canvasContainer = shallowRef<HTMLDivElement | null>(null)
|
||||
const inlineEditorEl = shallowRef<HTMLTextAreaElement | null>(null)
|
||||
const modelValue = ref(initial)
|
||||
const api = useBoundingBoxes('1', {
|
||||
const api = useBoundingBoxes(toNodeId('1'), {
|
||||
canvasEl,
|
||||
canvasContainer,
|
||||
inlineEditorEl,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import type { BoundingBox } from '@/types/boundingBoxes'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { readableTextColor, textOnColor } from '@/utils/colorUtil'
|
||||
|
||||
const HANDLE_PX = 8
|
||||
@@ -39,7 +40,7 @@ interface UseBoundingBoxesOptions {
|
||||
}
|
||||
|
||||
export function useBoundingBoxes(
|
||||
nodeId: string,
|
||||
nodeId: NodeId,
|
||||
{
|
||||
canvasEl,
|
||||
canvasContainer,
|
||||
@@ -63,9 +64,7 @@ export function useBoundingBoxes(
|
||||
nodeId && app.canvas?.graph ? app.canvas.graph.getNodeById(nodeId) : null
|
||||
)
|
||||
const { selectedNodeIds } = storeToRefs(useCanvasStore())
|
||||
const isNodeSelected = computed(() =>
|
||||
selectedNodeIds.value.has(String(nodeId))
|
||||
)
|
||||
const isNodeSelected = computed(() => selectedNodeIds.value.has(nodeId))
|
||||
|
||||
function dimWidget(name: 'width' | 'height'): number | undefined {
|
||||
const v = litegraphNode.value?.widgets?.find((w) => w.name === name)?.value
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type { NodeId } from '@/renderer/core/layout/types'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
const mockApp = vi.hoisted(() => ({
|
||||
@@ -38,7 +39,7 @@ vi.mock('@/lib/litegraph/src/litegraph', async (importOriginal) => {
|
||||
// unmodified — the node accessors filter selectedItems with the real predicate.
|
||||
const makeNode = (mode: LGraphEventMode, id = 1): LGraphNode => {
|
||||
const node = new LGraphNode('Test')
|
||||
node.id = id
|
||||
node.id = nodeId(id)
|
||||
node.mode = mode
|
||||
return node
|
||||
}
|
||||
@@ -69,7 +70,7 @@ class MockNode implements Positionable {
|
||||
) {
|
||||
this.pos = pos
|
||||
this.size = size
|
||||
this.id = 'mock-node'
|
||||
this.id = nodeId('mock-node')
|
||||
this.boundingRect = [0, 0, 0, 0]
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
import { computeUnionBounds } from '@/utils/mathUtil'
|
||||
|
||||
@@ -100,8 +101,7 @@ export function useSelectionToolboxPosition(
|
||||
if (item.id == null) continue
|
||||
|
||||
if (shouldRenderVueNodes.value && typeof item.id === 'string') {
|
||||
// Use layout store for Vue nodes (only works with string IDs)
|
||||
const layout = layoutStore.getNodeLayoutRef(item.id).value
|
||||
const layout = layoutStore.getNodeLayoutRef(toNodeId(item.id)).value
|
||||
if (layout) {
|
||||
allBounds.push([
|
||||
layout.bounds.x,
|
||||
|
||||
@@ -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,12 +1,14 @@
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import type { Point } from '@/renderer/core/layout/types'
|
||||
import { app } from '@/scripts/app'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
export type ArrangeLayout = 'vertical' | 'horizontal' | 'grid'
|
||||
|
||||
@@ -39,7 +41,7 @@ const titleHeightOf = (node: LGraphNode): number => {
|
||||
const toBox = (node: LGraphNode): NodeBox => {
|
||||
const titleHeight = titleHeightOf(node)
|
||||
return {
|
||||
id: node.id,
|
||||
id: toNodeId(node.id),
|
||||
posX: node.pos[0],
|
||||
posY: node.pos[1],
|
||||
visualWidth: node.size[0],
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNod
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { createNodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import { seedRequiredInputMissingNodeError } from '@/utils/__tests__/executionErrorTestUtils'
|
||||
import type { MissingMediaCandidate } from '@/platform/missingMedia/types'
|
||||
import type { MissingModelCandidate } from '@/platform/missingModel/types'
|
||||
@@ -1011,7 +1012,7 @@ describe('clearWidgetRelatedErrors parameter routing', () => {
|
||||
graph.add(host)
|
||||
|
||||
const interiorNode = new LGraphNode('CheckpointLoaderSimple')
|
||||
interiorNode.id = 1
|
||||
interiorNode.id = nodeId(1)
|
||||
subgraph.add(interiorNode)
|
||||
const input = interiorNode.addInput('ckpt_name', 'COMBO')
|
||||
const widget = interiorNode.addWidget(
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
|
||||
describe('Node Reactivity', () => {
|
||||
beforeEach(() => {
|
||||
@@ -68,7 +69,7 @@ describe('Node Reactivity', () => {
|
||||
const onValueChange = vi.fn()
|
||||
|
||||
graph.trigger('node:slot-links:changed', {
|
||||
nodeId: String(node.id),
|
||||
nodeId: toNodeId(node.id),
|
||||
slotType: NodeSlotType.INPUT
|
||||
})
|
||||
await nextTick()
|
||||
@@ -116,7 +117,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
const { graph, node } = createWidgetInputGraph()
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
|
||||
const nodeData = vueNodeData.get(String(node.id))
|
||||
const nodeData = vueNodeData.get(toNodeId(node.id))
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
|
||||
|
||||
expect(widgetData?.slotMetadata).toBeDefined()
|
||||
@@ -127,7 +128,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
const { graph, node } = createWidgetInputGraph()
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
|
||||
const nodeData = vueNodeData.get(String(node.id))
|
||||
const nodeData = vueNodeData.get(toNodeId(node.id))
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
|
||||
|
||||
// Verify initially linked
|
||||
@@ -155,7 +156,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
const { graph, node } = createWidgetInputGraph()
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
|
||||
const nodeData = vueNodeData.get(String(node.id))!
|
||||
const nodeData = vueNodeData.get(toNodeId(node.id))!
|
||||
|
||||
// Mimic what processedWidgets does in NodeWidgets.vue:
|
||||
// derive disabled from slotMetadata.linked
|
||||
@@ -204,7 +205,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
throw new Error('Expected SubgraphInput.connect to produce a link')
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(subgraph)
|
||||
const nodeData = vueNodeData.get(String(node.id))
|
||||
const nodeData = vueNodeData.get(toNodeId(node.id))
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
|
||||
|
||||
expect(widgetData?.slotMetadata?.linked).toBe(true)
|
||||
@@ -230,7 +231,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeData = vueNodeData.get(String(subgraphNode.id))
|
||||
const nodeData = vueNodeData.get(toNodeId(subgraphNode.id))
|
||||
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'value')
|
||||
expect(widgetData).toBeDefined()
|
||||
@@ -242,7 +243,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
const { graph, node } = createWidgetInputGraph()
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
|
||||
const nodeData = vueNodeData.get(String(node.id))!
|
||||
const nodeData = vueNodeData.get(toNodeId(node.id))!
|
||||
const widgetData = nodeData.widgets!.find((w) => w.name === 'prompt')!
|
||||
|
||||
expect(widgetData.slotMetadata?.linked).toBe(true)
|
||||
@@ -278,7 +279,7 @@ describe('Subgraph output slot label reactivity', () => {
|
||||
graph.add(node)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeId = String(node.id)
|
||||
const nodeId = toNodeId(node.id)
|
||||
const nodeData = vueNodeData.get(nodeId)
|
||||
if (!nodeData?.outputs) throw new Error('Expected output data to exist')
|
||||
|
||||
@@ -306,7 +307,7 @@ describe('Subgraph output slot label reactivity', () => {
|
||||
graph.add(node)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeId = String(node.id)
|
||||
const nodeId = toNodeId(node.id)
|
||||
const nodeData = vueNodeData.get(nodeId)
|
||||
if (!nodeData?.inputs) throw new Error('Expected input data to exist')
|
||||
|
||||
@@ -369,7 +370,7 @@ describe('Nested promoted widget mapping', () => {
|
||||
graph.add(subgraphNodeB)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeData = vueNodeData.get(String(subgraphNodeB.id))
|
||||
const nodeData = vueNodeData.get(toNodeId(subgraphNodeB.id))
|
||||
const mappedWidget = nodeData?.widgets?.[0]
|
||||
|
||||
expect(mappedWidget).toBeDefined()
|
||||
@@ -406,7 +407,7 @@ describe('Nested promoted widget mapping', () => {
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeData = vueNodeData.get(String(subgraphNode.id))
|
||||
const nodeData = vueNodeData.get(toNodeId(subgraphNode.id))
|
||||
const widgets = nodeData?.widgets
|
||||
|
||||
expect(widgets).toHaveLength(2)
|
||||
@@ -452,7 +453,7 @@ describe('Promoted widget sourceExecutionId', () => {
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeData = vueNodeData.get(String(subgraphNode.id))
|
||||
const nodeData = vueNodeData.get(toNodeId(subgraphNode.id))
|
||||
const promotedWidget = nodeData?.widgets?.find(
|
||||
(w) => w.name === 'ckpt_input'
|
||||
)
|
||||
@@ -475,7 +476,7 @@ describe('Promoted widget sourceExecutionId', () => {
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeData = vueNodeData.get(String(node.id))
|
||||
const nodeData = vueNodeData.get(toNodeId(node.id))
|
||||
const widget = nodeData?.widgets?.find((w) => w.name === 'steps')
|
||||
|
||||
expect(widget).toBeDefined()
|
||||
@@ -714,12 +715,13 @@ describe('Pre-remove vueNodeData drain', () => {
|
||||
const node = new LGraphNode('test')
|
||||
graph.add(node)
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const id = toNodeId(node.id)
|
||||
|
||||
expect(vueNodeData.has(String(node.id))).toBe(true)
|
||||
expect(vueNodeData.has(id)).toBe(true)
|
||||
|
||||
let dataPresentInOnRemoved: boolean | undefined
|
||||
node.onRemoved = () => {
|
||||
dataPresentInOnRemoved = vueNodeData.has(String(node.id))
|
||||
dataPresentInOnRemoved = vueNodeData.has(id)
|
||||
}
|
||||
|
||||
graph.remove(node)
|
||||
|
||||
@@ -21,7 +21,8 @@ import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import type { NodeId } from '@/renderer/core/layout/types'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { isDOMWidget } from '@/scripts/domWidget'
|
||||
import { IS_CONTROL_WIDGET } from '@/scripts/widgets'
|
||||
@@ -30,7 +31,6 @@ import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { WidgetValue, SafeControlWidget } from '@/types/simplifiedWidget'
|
||||
import { normalizeControlOption } from '@/types/simplifiedWidget'
|
||||
import { getWidgetIdForNode } from '@/utils/litegraphUtil'
|
||||
import type { NodeId as WorkflowNodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import type { WidgetId } from '@/types/widgetId'
|
||||
|
||||
@@ -51,7 +51,7 @@ import { getExecutionIdByNode } from '@/utils/graphTraversalUtil'
|
||||
export interface WidgetSlotMetadata {
|
||||
index: number
|
||||
linked: boolean
|
||||
originNodeId?: string
|
||||
originNodeId?: NodeId
|
||||
originOutputName?: string
|
||||
type: string
|
||||
}
|
||||
@@ -136,10 +136,10 @@ export interface VueNodeData {
|
||||
|
||||
export interface GraphNodeManager {
|
||||
// Reactive state - safe data extracted from LiteGraph nodes
|
||||
vueNodeData: ReadonlyMap<string, VueNodeData>
|
||||
vueNodeData: ReadonlyMap<NodeId, VueNodeData>
|
||||
|
||||
// Access to original LiteGraph nodes (non-reactive)
|
||||
getNode(id: WorkflowNodeId): LGraphNode | undefined
|
||||
getNode(id: NodeId): LGraphNode | undefined
|
||||
|
||||
// Lifecycle methods
|
||||
cleanup(): void
|
||||
@@ -353,14 +353,14 @@ function buildSlotMetadata(
|
||||
): Map<string, WidgetSlotMetadata> {
|
||||
const metadata = new Map<string, WidgetSlotMetadata>()
|
||||
inputs?.forEach((input, index) => {
|
||||
let originNodeId: string | undefined
|
||||
let originNodeId: NodeId | undefined
|
||||
let originOutputName: string | undefined
|
||||
|
||||
if (input.link != null && graphRef) {
|
||||
const link = graphRef.getLink(input.link)
|
||||
const originNode = link ? graphRef.getNodeById(link.origin_id) : null
|
||||
if (link && originNode) {
|
||||
originNodeId = String(link.origin_id)
|
||||
originNodeId = toNodeId(link.origin_id)
|
||||
originOutputName = originNode.outputs?.[link.origin_slot]?.name
|
||||
}
|
||||
}
|
||||
@@ -471,7 +471,7 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
|
||||
const badges = node.badges
|
||||
|
||||
return {
|
||||
id: String(node.id),
|
||||
id: toNodeId(node.id),
|
||||
title: typeof node.title === 'string' ? node.title : '',
|
||||
type: nodeType,
|
||||
mode: node.mode || 0,
|
||||
@@ -498,12 +498,12 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
// Get layout mutations composable
|
||||
const { createNode, deleteNode, setSource } = useLayoutMutations()
|
||||
// Safe reactive data extracted from LiteGraph nodes
|
||||
const vueNodeData = reactive(new Map<string, VueNodeData>())
|
||||
const vueNodeData = reactive(new Map<NodeId, VueNodeData>())
|
||||
|
||||
// Non-reactive storage for original LiteGraph nodes
|
||||
const nodeRefs = new Map<string, LGraphNode>()
|
||||
const nodeRefs = new Map<NodeId, LGraphNode>()
|
||||
|
||||
const refreshNodeSlots = (nodeId: string) => {
|
||||
const refreshNodeSlots = (nodeId: NodeId) => {
|
||||
const nodeRef = nodeRefs.get(nodeId)
|
||||
const currentData = vueNodeData.get(nodeId)
|
||||
|
||||
@@ -518,14 +518,14 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
}
|
||||
|
||||
// Get access to original LiteGraph node (non-reactive)
|
||||
const getNode = (id: WorkflowNodeId): LGraphNode | undefined => {
|
||||
return nodeRefs.get(String(id))
|
||||
const getNode = (id: NodeId): LGraphNode | undefined => {
|
||||
return nodeRefs.get(id)
|
||||
}
|
||||
|
||||
const syncWithGraph = () => {
|
||||
if (!graph?._nodes) return
|
||||
|
||||
const currentNodes = new Set(graph._nodes.map((n) => String(n.id)))
|
||||
const currentNodes = new Set(graph._nodes.map((n) => toNodeId(n.id)))
|
||||
|
||||
// Remove deleted nodes
|
||||
for (const id of Array.from(vueNodeData.keys())) {
|
||||
@@ -537,7 +537,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
|
||||
// Add/update existing nodes
|
||||
graph._nodes.forEach((node) => {
|
||||
const id = String(node.id)
|
||||
const id = toNodeId(node.id)
|
||||
|
||||
// Store non-reactive reference
|
||||
nodeRefs.set(id, node)
|
||||
@@ -555,7 +555,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
node: LGraphNode,
|
||||
originalCallback?: (node: LGraphNode) => void
|
||||
) => {
|
||||
const id = String(node.id)
|
||||
const id = toNodeId(node.id)
|
||||
|
||||
// Store non-reactive reference to original node
|
||||
nodeRefs.set(id, node)
|
||||
@@ -610,8 +610,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
}
|
||||
}
|
||||
|
||||
const dropNodeReferences = (node: LGraphNode) => {
|
||||
const id = String(node.id)
|
||||
const dropNodeReferences = (id: NodeId) => {
|
||||
nodeRefs.delete(id)
|
||||
vueNodeData.delete(id)
|
||||
}
|
||||
@@ -620,9 +619,12 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
node: LGraphNode,
|
||||
originalCallback?: (node: LGraphNode) => void
|
||||
) => {
|
||||
const id = String(node.id)
|
||||
const id = toNodeId(node.id)
|
||||
|
||||
// Remove node from layout store
|
||||
setSource(LayoutSource.Canvas)
|
||||
void deleteNode(id)
|
||||
dropNodeReferences(id)
|
||||
originalCallback?.(node)
|
||||
}
|
||||
|
||||
@@ -670,7 +672,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
const beforeNodeRemovedListener = (
|
||||
e: CustomEvent<{ node: LGraphNode }>
|
||||
) => {
|
||||
dropNodeReferences(e.detail.node)
|
||||
dropNodeReferences(toNodeId(e.detail.node.id))
|
||||
}
|
||||
graph.events.addEventListener(
|
||||
'node:before-removed',
|
||||
@@ -681,7 +683,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
[K in LGraphTriggerAction]: (event: LGraphTriggerParam<K>) => void
|
||||
} = {
|
||||
'node:property:changed': (propertyEvent) => {
|
||||
const nodeId = String(propertyEvent.nodeId)
|
||||
const nodeId = toNodeId(propertyEvent.nodeId)
|
||||
const currentData = vueNodeData.get(nodeId)
|
||||
|
||||
if (currentData) {
|
||||
@@ -777,15 +779,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
}
|
||||
},
|
||||
'node:slot-errors:changed': (slotErrorsEvent) => {
|
||||
refreshNodeSlots(String(slotErrorsEvent.nodeId))
|
||||
refreshNodeSlots(toNodeId(slotErrorsEvent.nodeId))
|
||||
},
|
||||
'node:slot-links:changed': (slotLinksEvent) => {
|
||||
if (slotLinksEvent.slotType === NodeSlotType.INPUT) {
|
||||
refreshNodeSlots(String(slotLinksEvent.nodeId))
|
||||
refreshNodeSlots(toNodeId(slotLinksEvent.nodeId))
|
||||
}
|
||||
},
|
||||
'node:slot-label:changed': (slotLabelEvent) => {
|
||||
const nodeId = String(slotLabelEvent.nodeId)
|
||||
const nodeId = toNodeId(slotLabelEvent.nodeId)
|
||||
const nodeRef = nodeRefs.get(nodeId)
|
||||
if (!nodeRef) return
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type {
|
||||
LGraphGroup,
|
||||
LGraphNode,
|
||||
NodeId
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { getExtraOptionsForWidget } from '@/services/litegraphService'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import { isLGraphGroup } from '@/utils/litegraphUtil'
|
||||
|
||||
import {
|
||||
@@ -50,7 +47,7 @@ export enum BadgeVariant {
|
||||
// Global singleton for NodeOptions component reference
|
||||
let nodeOptionsInstance: null | NodeOptionsInstance = null
|
||||
|
||||
const hoveredWidget = ref<[string, NodeId | undefined]>()
|
||||
const hoveredWidget = ref<[string, SerializedNodeId | undefined]>()
|
||||
|
||||
/**
|
||||
* Toggle the node options popover
|
||||
@@ -70,7 +67,7 @@ export function toggleNodeOptions(event: Event) {
|
||||
export function showNodeOptions(
|
||||
event: MouseEvent,
|
||||
widgetName?: string,
|
||||
nodeId?: NodeId
|
||||
nodeId?: SerializedNodeId
|
||||
) {
|
||||
hoveredWidget.value = widgetName ? [widgetName, nodeId] : undefined
|
||||
if (nodeOptionsInstance?.show) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useNodeMenuOptions } from '@/composables/graph/useNodeMenuOptions'
|
||||
import type { Positionable } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
|
||||
// canvasStore transitively imports the app singleton; stub it so the real
|
||||
// ComfyApp module never loads during these unit tests.
|
||||
@@ -45,7 +46,7 @@ const i18n = createI18n({
|
||||
|
||||
const nodeWithMode = (mode: LGraphEventMode, id = 1): LGraphNode => {
|
||||
const node = new LGraphNode('Test')
|
||||
node.id = id
|
||||
node.id = nodeId(id)
|
||||
node.mode = mode
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ function useVueNodeLifecycleIndividual() {
|
||||
|
||||
// Initialize layout system with existing nodes from active graph
|
||||
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
|
||||
id: node.id.toString(),
|
||||
id: node.id,
|
||||
pos: [node.pos[0], node.pos[1]] as [number, number],
|
||||
size: [node.size[0], node.size[1]] as [number, number]
|
||||
}))
|
||||
@@ -45,6 +45,7 @@ function useVueNodeLifecycleIndividual() {
|
||||
|
||||
// Seed existing links into the Layout Store (topology only)
|
||||
for (const link of activeGraph._links.values()) {
|
||||
if (link.origin_id === -1 || link.target_id === -1) continue
|
||||
layoutMutations.createLink(
|
||||
link.id,
|
||||
link.origin_id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMaskEditorDataStore } from '@/stores/maskEditorDataStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { ImageRef, ImageLayer } from '@/stores/maskEditorDataStore'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
@@ -178,7 +179,7 @@ export function useMaskEditorLoader() {
|
||||
maskLayer,
|
||||
paintLayer,
|
||||
sourceRef,
|
||||
nodeId: node.id
|
||||
nodeId: toNodeId(node.id)
|
||||
}
|
||||
|
||||
dataStore.sourceNode = node
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyNodeDef, PriceBadge } from '@/schemas/nodeDefSchema'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -619,14 +620,15 @@ describe('useNodePricing', () => {
|
||||
|
||||
LiteGraph.vueNodesMode = true
|
||||
try {
|
||||
const revBefore = getNodeRevisionRef(node.id).value
|
||||
const nodeId = toNodeId(node.id)
|
||||
const revBefore = getNodeRevisionRef(nodeId).value
|
||||
const tickBefore = pricingRevision.value
|
||||
|
||||
getNodeDisplayPrice(node)
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
|
||||
// VueNodes path bumps per-node ref and the global tick.
|
||||
expect(getNodeRevisionRef(node.id).value).toBeGreaterThan(revBefore)
|
||||
expect(getNodeRevisionRef(nodeId).value).toBeGreaterThan(revBefore)
|
||||
expect(pricingRevision.value).toBeGreaterThan(tickBefore)
|
||||
} finally {
|
||||
LiteGraph.vueNodesMode = false
|
||||
@@ -658,7 +660,7 @@ describe('useNodePricing', () => {
|
||||
describe('getNodeRevisionRef', () => {
|
||||
it('should return a ref for a node ID', () => {
|
||||
const { getNodeRevisionRef } = useNodePricing()
|
||||
const ref = getNodeRevisionRef('node-1')
|
||||
const ref = getNodeRevisionRef(toNodeId('node-1'))
|
||||
|
||||
expect(ref).toBeDefined()
|
||||
expect(ref.value).toBe(0)
|
||||
@@ -666,25 +668,24 @@ describe('useNodePricing', () => {
|
||||
|
||||
it('should return the same ref for the same node ID', () => {
|
||||
const { getNodeRevisionRef } = useNodePricing()
|
||||
const ref1 = getNodeRevisionRef('node-same')
|
||||
const ref2 = getNodeRevisionRef('node-same')
|
||||
const ref1 = getNodeRevisionRef(toNodeId('node-same'))
|
||||
const ref2 = getNodeRevisionRef(toNodeId('node-same'))
|
||||
|
||||
expect(ref1).toBe(ref2)
|
||||
})
|
||||
|
||||
it('should return different refs for different node IDs', () => {
|
||||
const { getNodeRevisionRef } = useNodePricing()
|
||||
const ref1 = getNodeRevisionRef('node-a')
|
||||
const ref2 = getNodeRevisionRef('node-b')
|
||||
const ref1 = getNodeRevisionRef(toNodeId('node-a'))
|
||||
const ref2 = getNodeRevisionRef(toNodeId('node-b'))
|
||||
|
||||
expect(ref1).not.toBe(ref2)
|
||||
})
|
||||
|
||||
it('should handle both string and number node IDs', () => {
|
||||
const { getNodeRevisionRef } = useNodePricing()
|
||||
// Number ID gets stringified, so '123' and 123 should return the same ref
|
||||
const refFromNumber = getNodeRevisionRef(123)
|
||||
const refFromString = getNodeRevisionRef('123')
|
||||
const refFromNumber = getNodeRevisionRef(toNodeId(123))
|
||||
const refFromString = getNodeRevisionRef(toNodeId('123'))
|
||||
|
||||
expect(refFromNumber).toBe(refFromString)
|
||||
})
|
||||
|
||||
@@ -24,6 +24,8 @@ import type {
|
||||
WidgetDependency
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { Expression } from 'jsonata'
|
||||
import jsonata from 'jsonata'
|
||||
|
||||
@@ -452,18 +454,17 @@ const pricingTick = ref(0)
|
||||
// Per-node revision tracking for VueNodes mode (more efficient than global tick)
|
||||
// Uses plain Map with individual refs per node for fine-grained reactivity
|
||||
// Keys are stringified node IDs to handle both string and number ID types
|
||||
const nodeRevisions = new Map<string, Ref<number>>()
|
||||
const nodeRevisions = new Map<NodeId, Ref<number>>()
|
||||
|
||||
/**
|
||||
* Get or create a revision ref for a specific node.
|
||||
* Each node has its own independent ref, so updates to one won't trigger others.
|
||||
*/
|
||||
const getNodeRevisionRef = (nodeId: string | number): Ref<number> => {
|
||||
const key = String(nodeId)
|
||||
let rev = nodeRevisions.get(key)
|
||||
const getNodeRevisionRef = (nodeId: NodeId): Ref<number> => {
|
||||
let rev = nodeRevisions.get(nodeId)
|
||||
if (!rev) {
|
||||
rev = ref(0)
|
||||
nodeRevisions.set(key, rev)
|
||||
nodeRevisions.set(nodeId, rev)
|
||||
}
|
||||
return rev
|
||||
}
|
||||
@@ -511,7 +512,7 @@ const scheduleEvaluation = (
|
||||
|
||||
if (LiteGraph.vueNodesMode) {
|
||||
// VueNodes mode: bump per-node revision (only this node re-renders)
|
||||
getNodeRevisionRef(node.id).value++
|
||||
getNodeRevisionRef(toNodeId(node.id)).value++
|
||||
}
|
||||
pricingTick.value++
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
|
||||
import { CANVAS_IMAGE_PREVIEW_WIDGET } from './canvasImagePreviewTypes'
|
||||
import { usePromotedPreviews } from './usePromotedPreviews'
|
||||
@@ -58,7 +59,7 @@ function addInteriorNode(
|
||||
} = { id: 10 }
|
||||
): LGraphNode {
|
||||
const node = new LGraphNode('test')
|
||||
node.id = options.id
|
||||
node.id = nodeId(options.id)
|
||||
if (options.previewMediaType) {
|
||||
node.previewMediaType = options.previewMediaType
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import {
|
||||
appendNodeExecutionId,
|
||||
createNodeLocatorId
|
||||
@@ -13,7 +15,7 @@ import {
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
|
||||
interface PromotedPreview {
|
||||
sourceNodeId: string
|
||||
sourceNodeId: NodeId
|
||||
sourceWidgetName: string
|
||||
type: 'image' | 'video' | 'audio'
|
||||
urls: string[]
|
||||
@@ -41,7 +43,7 @@ export function usePromotedPreviews(
|
||||
/** Touches reactive sources for Vue tracking; `getNodeImageUrls` reads non-reactive app state. */
|
||||
function readReactivePreviewUrls(
|
||||
leafHost: SubgraphNode,
|
||||
leafSourceNodeId: string,
|
||||
leafSourceNodeId: NodeId,
|
||||
leafExecutionId: NodeExecutionId,
|
||||
interiorNode: LGraphNode
|
||||
): string[] | undefined {
|
||||
@@ -89,7 +91,7 @@ export function usePromotedPreviews(
|
||||
function resolveNestedHost(
|
||||
rootGraphId: UUID,
|
||||
currentHostLocator: string,
|
||||
sourceNodeId: string
|
||||
sourceNodeId: NodeId
|
||||
) {
|
||||
const currentHost = hostNodesByLocator.get(currentHostLocator)
|
||||
const sourceNode = currentHost?.subgraph.getNodeById(sourceNodeId)
|
||||
@@ -114,7 +116,7 @@ export function usePromotedPreviews(
|
||||
resolveNestedHost
|
||||
)
|
||||
const leaf = resolved?.leaf ?? {
|
||||
sourceNodeId: exposure.sourceNodeId,
|
||||
sourceNodeId: toNodeId(exposure.sourceNodeId),
|
||||
sourcePreviewName: exposure.sourcePreviewName
|
||||
}
|
||||
const leafHostLocator =
|
||||
|
||||
@@ -7,6 +7,8 @@ import { defineComponent, nextTick, ref } from 'vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { api } from '@/scripts/api'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
import { usePainter } from './usePainter'
|
||||
|
||||
@@ -94,7 +96,10 @@ function makeWidget(name: string, value: unknown = null): IBaseWidget {
|
||||
/**
|
||||
* Mounts a thin wrapper component so Vue lifecycle hooks fire.
|
||||
*/
|
||||
function mountPainter(nodeId = 'test-node', initialModelValue = '') {
|
||||
function mountPainter(
|
||||
nodeId: NodeId = toNodeId('test-node'),
|
||||
initialModelValue = ''
|
||||
) {
|
||||
let painter!: PainterResult
|
||||
const canvasEl = ref<HTMLCanvasElement | null>(null)
|
||||
const cursorEl = ref<HTMLElement | null>(null)
|
||||
@@ -353,7 +358,7 @@ describe('usePainter', () => {
|
||||
const maskWidget = makeWidget('mask', '')
|
||||
mockWidgets.push(maskWidget)
|
||||
|
||||
mountPainter('test-node', 'painter/existing.png [temp]')
|
||||
mountPainter(toNodeId('test-node'), 'painter/existing.png [temp]')
|
||||
|
||||
const result = await maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
expect(result).toBe('painter/existing.png [temp]')
|
||||
@@ -375,7 +380,7 @@ describe('usePainter', () => {
|
||||
toBlob: (cb: BlobCallback) => cb(new Blob(['x']))
|
||||
} as unknown as HTMLCanvasElement
|
||||
|
||||
const { canvasEl } = mountPainter('test-node', '')
|
||||
const { canvasEl } = mountPainter(toNodeId('test-node'), '')
|
||||
canvasEl.value = fakeCanvas
|
||||
await nextTick()
|
||||
|
||||
@@ -408,7 +413,7 @@ describe('usePainter', () => {
|
||||
toBlob: (cb: BlobCallback) => cb(new Blob(['x']))
|
||||
} as unknown as HTMLCanvasElement
|
||||
|
||||
const { canvasEl } = mountPainter('test-node', '')
|
||||
const { canvasEl } = mountPainter(toNodeId('test-node'), '')
|
||||
canvasEl.value = fakeCanvas
|
||||
await nextTick()
|
||||
|
||||
@@ -434,7 +439,7 @@ describe('usePainter', () => {
|
||||
toBlob: (cb: BlobCallback) => cb(new Blob(['x']))
|
||||
} as unknown as HTMLCanvasElement
|
||||
|
||||
const { canvasEl } = mountPainter('test-node', '')
|
||||
const { canvasEl } = mountPainter(toNodeId('test-node'), '')
|
||||
canvasEl.value = fakeCanvas
|
||||
await nextTick()
|
||||
|
||||
@@ -447,7 +452,7 @@ describe('usePainter', () => {
|
||||
const maskWidget = makeWidget('mask', '')
|
||||
mockWidgets.push(maskWidget)
|
||||
|
||||
mountPainter('test-node', 'painter/cached.png [temp]')
|
||||
mountPainter(toNodeId('test-node'), 'painter/cached.png [temp]')
|
||||
|
||||
const result = await maskWidget.serializeValue!({} as LGraphNode, 0)
|
||||
expect(result).toBe('painter/cached.png [temp]')
|
||||
@@ -466,7 +471,7 @@ describe('usePainter', () => {
|
||||
} as unknown as HTMLCanvasElement
|
||||
|
||||
const { painter, canvasEl, modelValue } = mountPainter(
|
||||
'test-node',
|
||||
toNodeId('test-node'),
|
||||
'painter/old-upload.png [temp]'
|
||||
)
|
||||
canvasEl.value = fakeCanvas
|
||||
@@ -481,7 +486,7 @@ describe('usePainter', () => {
|
||||
it('calls api.apiURL with parsed filename params when modelValue is set', () => {
|
||||
vi.mocked(api.apiURL).mockClear()
|
||||
|
||||
mountPainter('test-node', 'painter/my-image.png [temp]')
|
||||
mountPainter(toNodeId('test-node'), 'painter/my-image.png [temp]')
|
||||
|
||||
expect(api.apiURL).toHaveBeenCalledWith(
|
||||
expect.stringContaining('filename=my-image.png')
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
type PainterTool = 'brush' | 'eraser'
|
||||
|
||||
@@ -31,7 +32,7 @@ interface UsePainterOptions {
|
||||
modelValue: Ref<string>
|
||||
}
|
||||
|
||||
export function usePainter(nodeId: string, options: UsePainterOptions) {
|
||||
export function usePainter(nodeId: NodeId, options: UsePainterOptions) {
|
||||
const { canvasEl, cursorEl, modelValue } = options
|
||||
const { t } = useI18n()
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
@@ -9,7 +9,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import WidgetImageCrop from '@/components/imagecrop/WidgetImageCrop.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import {
|
||||
createMockLGraphNode,
|
||||
@@ -83,7 +84,7 @@ const ImageCropHarness = defineComponent({
|
||||
modelValue,
|
||||
imageEl,
|
||||
containerEl,
|
||||
...useImageCrop(props.nodeId as NodeId, {
|
||||
...useImageCrop(toNodeId(props.nodeId), {
|
||||
imageEl,
|
||||
containerEl,
|
||||
modelValue
|
||||
@@ -183,7 +184,7 @@ function setupImageLayout(vm: CropVm, nw: number, nh: number) {
|
||||
|
||||
const harnessCleanups: Array<() => void> = []
|
||||
|
||||
async function mountHarness(nodeId: NodeId = 2 as NodeId) {
|
||||
async function mountHarness(nodeId: NodeId = toNodeId(2)) {
|
||||
const el = document.createElement('div')
|
||||
document.body.appendChild(el)
|
||||
const app = createApp(ImageCropHarness, { nodeId: Number(nodeId) })
|
||||
@@ -657,7 +658,7 @@ describe('WidgetImageCrop', () => {
|
||||
container: attach,
|
||||
props: {
|
||||
widget,
|
||||
nodeId: 2 as NodeId,
|
||||
nodeId: toNodeId(2),
|
||||
modelValue: { x: 0, y: 0, width: 100, height: 100 }
|
||||
},
|
||||
global: {
|
||||
@@ -689,7 +690,7 @@ describe('WidgetImageCrop', () => {
|
||||
container: attach,
|
||||
props: {
|
||||
widget,
|
||||
nodeId: 2 as NodeId,
|
||||
nodeId: toNodeId(2),
|
||||
modelValue: { x: 0, y: 0, width: 200, height: 200 }
|
||||
},
|
||||
global: {
|
||||
@@ -733,7 +734,7 @@ describe('WidgetImageCrop', () => {
|
||||
container: attach,
|
||||
props: {
|
||||
widget,
|
||||
nodeId: 2 as NodeId,
|
||||
nodeId: toNodeId(2),
|
||||
modelValue: { x: 0, y: 0, width: 200, height: 200 }
|
||||
},
|
||||
global: {
|
||||
@@ -779,7 +780,7 @@ describe('WidgetImageCrop', () => {
|
||||
container: attach,
|
||||
props: {
|
||||
widget,
|
||||
nodeId: 2 as NodeId,
|
||||
nodeId: toNodeId(2),
|
||||
modelValue: { x: 0, y: 0, width: 100, height: 100 }
|
||||
},
|
||||
global: {
|
||||
|
||||
@@ -2,9 +2,10 @@ import { useResizeObserver } from '@vueuse/core'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
|
||||
export type ResizeDirection =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import type { WidgetState } from '@/types/widgetState'
|
||||
|
||||
import { boundsExtractor, singleValueExtractor } from './useUpstreamValue'
|
||||
@@ -10,7 +10,7 @@ function widget(name: string, value: unknown): WidgetState {
|
||||
name,
|
||||
type: 'INPUT',
|
||||
value,
|
||||
nodeId: '1' as NodeId,
|
||||
nodeId: nodeId(1),
|
||||
options: {},
|
||||
y: 0
|
||||
}
|
||||
|
||||
@@ -8,12 +8,17 @@ export function useWorkflowStatusDismissal() {
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
watch(
|
||||
() => workflowStore.activeWorkflow,
|
||||
(workflow) => {
|
||||
if (
|
||||
workflow &&
|
||||
executionStore.getWorkflowStatus(workflow) !== 'running'
|
||||
) {
|
||||
() => {
|
||||
const workflow = workflowStore.activeWorkflow
|
||||
return {
|
||||
workflow,
|
||||
status: workflow
|
||||
? executionStore.getWorkflowStatus(workflow)
|
||||
: undefined
|
||||
}
|
||||
},
|
||||
({ workflow, status }) => {
|
||||
if (workflow && status && status !== 'running') {
|
||||
executionStore.clearWorkflowStatus(workflow)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
} from '@/core/schemas/proxyWidgetQuarantineSchema'
|
||||
import { parseProxyWidgetErrorQuarantine } from '@/core/schemas/proxyWidgetQuarantineSchema'
|
||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { nextUniqueName } from '@/lib/litegraph/src/strings'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
|
||||
@@ -29,32 +29,34 @@ import type {
|
||||
import { isWidgetValue } from '@/lib/litegraph/src/types/widgets'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
|
||||
|
||||
interface LegacyProxyEntrySource extends PromotedWidgetSource {
|
||||
disambiguatingSourceNodeId?: string
|
||||
disambiguatingSourceNodeId?: NodeId
|
||||
}
|
||||
|
||||
const LEGACY_PROXY_WIDGET_PREFIX_PATTERN = /^\s*(\d+)\s*:\s*(.+)$/
|
||||
|
||||
interface StrippedPrefix {
|
||||
sourceWidgetName: string
|
||||
deepestPrefixId?: string
|
||||
deepestPrefixId?: NodeId
|
||||
}
|
||||
|
||||
function stripLegacyPrefixes(sourceWidgetName: string): StrippedPrefix {
|
||||
let remaining = sourceWidgetName
|
||||
let deepestPrefixId: string | undefined
|
||||
let deepestPrefixId: NodeId | undefined
|
||||
while (true) {
|
||||
const match = LEGACY_PROXY_WIDGET_PREFIX_PATTERN.exec(remaining)
|
||||
if (!match) return { sourceWidgetName: remaining, deepestPrefixId }
|
||||
deepestPrefixId = match[1]
|
||||
deepestPrefixId = toNodeId(match[1])
|
||||
remaining = match[2]
|
||||
}
|
||||
}
|
||||
|
||||
function canResolveLegacyProxy(
|
||||
hostNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceNodeId: SerializedNodeId,
|
||||
widgetName: string
|
||||
): boolean {
|
||||
return (
|
||||
@@ -65,24 +67,32 @@ function canResolveLegacyProxy(
|
||||
|
||||
export function normalizeLegacyProxyWidgetEntry(
|
||||
hostNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceNodeId: SerializedNodeId,
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
disambiguatingSourceNodeId?: SerializedNodeId
|
||||
): LegacyProxyEntrySource {
|
||||
const normalizedSourceNodeId = toNodeId(sourceNodeId)
|
||||
const normalizedDisambiguatingSourceNodeId =
|
||||
disambiguatingSourceNodeId === undefined
|
||||
? undefined
|
||||
: toNodeId(disambiguatingSourceNodeId)
|
||||
|
||||
if (canResolveLegacyProxy(hostNode, sourceNodeId, sourceWidgetName)) {
|
||||
return {
|
||||
sourceNodeId,
|
||||
sourceNodeId: normalizedSourceNodeId,
|
||||
sourceWidgetName,
|
||||
...(disambiguatingSourceNodeId && { disambiguatingSourceNodeId })
|
||||
...(normalizedDisambiguatingSourceNodeId && {
|
||||
disambiguatingSourceNodeId: normalizedDisambiguatingSourceNodeId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const stripped = stripLegacyPrefixes(sourceWidgetName)
|
||||
const patchDisambiguatingSourceNodeId =
|
||||
stripped.deepestPrefixId ?? disambiguatingSourceNodeId
|
||||
stripped.deepestPrefixId ?? normalizedDisambiguatingSourceNodeId
|
||||
|
||||
return {
|
||||
sourceNodeId,
|
||||
sourceNodeId: normalizedSourceNodeId,
|
||||
sourceWidgetName: stripped.sourceWidgetName,
|
||||
...(patchDisambiguatingSourceNodeId && {
|
||||
disambiguatingSourceNodeId: patchDisambiguatingSourceNodeId
|
||||
@@ -142,6 +152,7 @@ type Plan =
|
||||
| { kind: 'quarantine'; reason: ProxyWidgetQuarantineReason }
|
||||
|
||||
interface PendingEntry {
|
||||
originalEntry: SerializedProxyWidgetTuple
|
||||
normalized: LegacyProxyEntrySource
|
||||
hostValue: TWidgetValue | undefined
|
||||
isHole: boolean
|
||||
@@ -159,23 +170,27 @@ export function flushProxyWidgetMigration(args: FlushArgs): void {
|
||||
const tuples = parseProxyWidgets(hostNode.properties.proxyWidgets)
|
||||
if (tuples.length === 0) return
|
||||
|
||||
const cohort: LegacyProxyEntrySource[] = tuples.map(
|
||||
([sourceNodeId, sourceWidgetName, disambiguator]) =>
|
||||
normalizeLegacyProxyWidgetEntry(
|
||||
const normalizedEntries = tuples.map((originalEntry) => {
|
||||
const [sourceNodeId, sourceWidgetName, disambiguator] = originalEntry
|
||||
return {
|
||||
originalEntry,
|
||||
normalized: normalizeLegacyProxyWidgetEntry(
|
||||
hostNode,
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguator
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
const cohort = normalizedEntries.map((entry) => entry.normalized)
|
||||
|
||||
const pending: PendingEntry[] = cohort.map((normalized, index) => {
|
||||
const pending: PendingEntry[] = normalizedEntries.map((entry, index) => {
|
||||
const { value, isHole } = pickHostValue(hostWidgetValues, index)
|
||||
return {
|
||||
normalized,
|
||||
...entry,
|
||||
hostValue: value,
|
||||
isHole,
|
||||
plan: classify(hostNode, normalized, cohort)
|
||||
plan: classify(hostNode, entry.normalized, cohort)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -261,6 +276,7 @@ function collectTargetsStrict(
|
||||
for (const linkId of linkIds) {
|
||||
const link = subgraph.links.get(linkId)
|
||||
if (!link) return undefined
|
||||
if (link.target_id === -1) return undefined
|
||||
targets.push({
|
||||
targetNodeId: link.target_id,
|
||||
targetSlot: link.target_slot
|
||||
@@ -277,15 +293,20 @@ function collectTargetsSkippingDangling(
|
||||
const linkIds = primitiveNode.outputs?.[0]?.links ?? []
|
||||
return linkIds.flatMap((linkId) => {
|
||||
const link = subgraph.links.get(linkId)
|
||||
return link
|
||||
? [{ targetNodeId: link.target_id, targetSlot: link.target_slot }]
|
||||
return link && link.target_id !== -1
|
||||
? [
|
||||
{
|
||||
targetNodeId: link.target_id,
|
||||
targetSlot: link.target_slot
|
||||
}
|
||||
]
|
||||
: []
|
||||
})
|
||||
}
|
||||
|
||||
function cohortDuplicatesPrimitive(
|
||||
cohort: readonly LegacyProxyEntrySource[],
|
||||
primitiveNodeId: string
|
||||
primitiveNodeId: NodeId
|
||||
): boolean {
|
||||
return (
|
||||
cohort.filter((entry) => entry.sourceNodeId === primitiveNodeId).length >= 2
|
||||
@@ -600,7 +621,13 @@ function repairPrimitive(
|
||||
const baseName = userRenamedTitle(primitiveNode) ?? validated.sourceWidgetName
|
||||
const snapshot: SnapshotLink[] = (primitiveOutput.links ?? [])
|
||||
.map((id) => subgraph.links.get(id))
|
||||
.filter((l): l is NonNullable<typeof l> => l !== undefined)
|
||||
.filter(
|
||||
(
|
||||
l
|
||||
): l is NonNullable<typeof l> & {
|
||||
target_id: NodeId
|
||||
} => l !== undefined && l.target_id !== -1
|
||||
)
|
||||
.map((l) => ({
|
||||
primitiveSlot: l.origin_slot,
|
||||
targetNodeId: l.target_id,
|
||||
@@ -722,13 +749,8 @@ function quarantineFor(
|
||||
entry: PendingEntry,
|
||||
reason: ProxyWidgetQuarantineReason
|
||||
): ProxyWidgetErrorQuarantineEntry {
|
||||
const { sourceNodeId, sourceWidgetName, disambiguatingSourceNodeId } =
|
||||
entry.normalized
|
||||
const originalEntry: SerializedProxyWidgetTuple = disambiguatingSourceNodeId
|
||||
? [sourceNodeId, sourceWidgetName, disambiguatingSourceNodeId]
|
||||
: [sourceNodeId, sourceWidgetName]
|
||||
return makeQuarantineEntry({
|
||||
originalEntry,
|
||||
originalEntry: entry.originalEntry,
|
||||
reason,
|
||||
hostValue: entry.isHole ? undefined : entry.hostValue
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { PreviewExposure } from '@/core/schemas/previewExposureSchema'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
|
||||
import type { PreviewExposureChainContext } from './previewExposureChain'
|
||||
@@ -13,7 +15,7 @@ interface FixtureExposure extends PreviewExposure {}
|
||||
|
||||
interface NestedHostMapping {
|
||||
fromHostLocator: string
|
||||
fromSourceNodeId: string
|
||||
fromSourceNodeId: NodeId
|
||||
toRootGraphId: UUID
|
||||
toHostLocator: string
|
||||
}
|
||||
@@ -66,7 +68,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'preview',
|
||||
sourceNodeId: '42',
|
||||
sourceNodeId: nodeId('42'),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
]
|
||||
@@ -88,14 +90,14 @@ describe(resolvePreviewExposureChain, () => {
|
||||
hostNodeLocator: 'host-a',
|
||||
exposure: {
|
||||
name: 'preview',
|
||||
sourceNodeId: '42',
|
||||
sourceNodeId: nodeId('42'),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
}
|
||||
],
|
||||
leaf: {
|
||||
rootGraphId: rootGraphA,
|
||||
sourceNodeId: '42',
|
||||
sourceNodeId: nodeId('42'),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
})
|
||||
@@ -108,7 +110,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'outer-preview',
|
||||
sourceNodeId: '99',
|
||||
sourceNodeId: nodeId('99'),
|
||||
sourcePreviewName: 'inner-preview'
|
||||
}
|
||||
]
|
||||
@@ -118,7 +120,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'inner-preview',
|
||||
sourceNodeId: 'leaf-node',
|
||||
sourceNodeId: nodeId('leaf-node'),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
]
|
||||
@@ -127,7 +129,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
const ctx = makeContext(exposureMap, [
|
||||
{
|
||||
fromHostLocator: 'host-outer',
|
||||
fromSourceNodeId: '99',
|
||||
fromSourceNodeId: nodeId('99'),
|
||||
toRootGraphId: rootGraphA,
|
||||
toHostLocator: 'host-inner'
|
||||
}
|
||||
@@ -145,7 +147,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
expect(result?.steps[1].hostNodeLocator).toBe('host-inner')
|
||||
expect(result?.leaf).toEqual({
|
||||
rootGraphId: rootGraphA,
|
||||
sourceNodeId: 'leaf-node',
|
||||
sourceNodeId: nodeId('leaf-node'),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
})
|
||||
@@ -157,7 +159,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'p1',
|
||||
sourceNodeId: 'sub-a',
|
||||
sourceNodeId: nodeId('sub-a'),
|
||||
sourcePreviewName: 'p2'
|
||||
}
|
||||
]
|
||||
@@ -167,7 +169,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'p2',
|
||||
sourceNodeId: 'sub-b',
|
||||
sourceNodeId: nodeId('sub-b'),
|
||||
sourcePreviewName: 'p3'
|
||||
}
|
||||
]
|
||||
@@ -177,7 +179,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'p3',
|
||||
sourceNodeId: 'leaf',
|
||||
sourceNodeId: nodeId('leaf'),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
]
|
||||
@@ -186,13 +188,13 @@ describe(resolvePreviewExposureChain, () => {
|
||||
const ctx = makeContext(exposureMap, [
|
||||
{
|
||||
fromHostLocator: 'host-1',
|
||||
fromSourceNodeId: 'sub-a',
|
||||
fromSourceNodeId: nodeId('sub-a'),
|
||||
toRootGraphId: rootGraphA,
|
||||
toHostLocator: 'host-2'
|
||||
},
|
||||
{
|
||||
fromHostLocator: 'host-2',
|
||||
fromSourceNodeId: 'sub-b',
|
||||
fromSourceNodeId: nodeId('sub-b'),
|
||||
toRootGraphId: rootGraphB,
|
||||
toHostLocator: 'host-3'
|
||||
}
|
||||
@@ -208,7 +210,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
])
|
||||
expect(result?.leaf).toEqual({
|
||||
rootGraphId: rootGraphB,
|
||||
sourceNodeId: 'leaf',
|
||||
sourceNodeId: nodeId('leaf'),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
})
|
||||
@@ -220,7 +222,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'outer',
|
||||
sourceNodeId: '99',
|
||||
sourceNodeId: nodeId('99'),
|
||||
sourcePreviewName: 'missing-on-inner'
|
||||
}
|
||||
]
|
||||
@@ -230,7 +232,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
const ctx = makeContext(exposureMap, [
|
||||
{
|
||||
fromHostLocator: 'host-outer',
|
||||
fromSourceNodeId: '99',
|
||||
fromSourceNodeId: nodeId('99'),
|
||||
toRootGraphId: rootGraphA,
|
||||
toHostLocator: 'host-inner'
|
||||
}
|
||||
@@ -246,7 +248,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
expect(result?.steps).toHaveLength(1)
|
||||
expect(result?.leaf).toEqual({
|
||||
rootGraphId: rootGraphA,
|
||||
sourceNodeId: '99',
|
||||
sourceNodeId: nodeId('99'),
|
||||
sourcePreviewName: 'missing-on-inner'
|
||||
})
|
||||
})
|
||||
@@ -255,13 +257,19 @@ describe(resolvePreviewExposureChain, () => {
|
||||
const exposureMap = new Map<string, FixtureExposure[]>([
|
||||
[
|
||||
`${rootGraphA}|host-a`,
|
||||
[{ name: 'cyclic', sourceNodeId: 'sub', sourcePreviewName: 'cyclic' }]
|
||||
[
|
||||
{
|
||||
name: 'cyclic',
|
||||
sourceNodeId: nodeId('sub'),
|
||||
sourcePreviewName: 'cyclic'
|
||||
}
|
||||
]
|
||||
]
|
||||
])
|
||||
const ctx = makeContext(exposureMap, [
|
||||
{
|
||||
fromHostLocator: 'host-a',
|
||||
fromSourceNodeId: 'sub',
|
||||
fromSourceNodeId: nodeId('sub'),
|
||||
toRootGraphId: rootGraphA,
|
||||
toHostLocator: 'host-a'
|
||||
}
|
||||
@@ -278,6 +286,6 @@ describe(resolvePreviewExposureChain, () => {
|
||||
expect.stringContaining('cycle detected')
|
||||
)
|
||||
expect(result?.steps).toHaveLength(1)
|
||||
expect(result?.leaf.sourceNodeId).toBe('sub')
|
||||
expect(result?.leaf.sourceNodeId).toBe(nodeId('sub'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PreviewExposure } from '@/core/schemas/previewExposureSchema'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
|
||||
interface ResolvedPreviewChainStep {
|
||||
@@ -11,7 +12,7 @@ export interface ResolvedPreviewChain {
|
||||
steps: readonly ResolvedPreviewChainStep[]
|
||||
leaf: {
|
||||
rootGraphId: UUID
|
||||
sourceNodeId: string
|
||||
sourceNodeId: NodeId
|
||||
sourcePreviewName: string
|
||||
}
|
||||
}
|
||||
@@ -24,12 +25,16 @@ export interface PreviewExposureChainContext {
|
||||
resolveNestedHost(
|
||||
rootGraphId: UUID,
|
||||
hostNodeLocator: string,
|
||||
sourceNodeId: string
|
||||
sourceNodeId: NodeId
|
||||
): { rootGraphId: UUID; hostNodeLocator: string } | undefined
|
||||
}
|
||||
|
||||
const MAX_CHAIN_DEPTH = 32
|
||||
|
||||
function exposureSourceNodeId(exposure: PreviewExposure): NodeId {
|
||||
return exposure.sourceNodeId
|
||||
}
|
||||
|
||||
export function resolvePreviewExposureChain(
|
||||
rootGraphId: UUID,
|
||||
hostNodeLocator: string,
|
||||
@@ -50,7 +55,7 @@ export function resolvePreviewExposureChain(
|
||||
steps,
|
||||
leaf: {
|
||||
rootGraphId: lastStep.rootGraphId,
|
||||
sourceNodeId: lastStep.exposure.sourceNodeId,
|
||||
sourceNodeId: exposureSourceNodeId(lastStep.exposure),
|
||||
sourcePreviewName: lastStep.exposure.sourcePreviewName
|
||||
}
|
||||
}
|
||||
@@ -80,14 +85,14 @@ export function resolvePreviewExposureChain(
|
||||
const nested = ctx.resolveNestedHost(
|
||||
currentRootGraphId,
|
||||
currentHost,
|
||||
exposure.sourceNodeId
|
||||
exposureSourceNodeId(exposure)
|
||||
)
|
||||
if (!nested) {
|
||||
return {
|
||||
steps,
|
||||
leaf: {
|
||||
rootGraphId: currentRootGraphId,
|
||||
sourceNodeId: exposure.sourceNodeId,
|
||||
sourceNodeId: exposureSourceNodeId(exposure),
|
||||
sourcePreviewName: exposure.sourcePreviewName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { isWidgetValue } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
import { resolveSubgraphInputTarget } from './resolveSubgraphInputTarget'
|
||||
|
||||
@@ -13,7 +14,7 @@ import { resolveSubgraphInputTarget } from './resolveSubgraphInputTarget'
|
||||
* on the projected widget.
|
||||
*/
|
||||
export interface PromotedSource {
|
||||
nodeId: string
|
||||
nodeId: NodeId
|
||||
widgetName: string
|
||||
}
|
||||
|
||||
@@ -109,7 +110,6 @@ export function promotedInputWidget(input: INodeInputSlot): IBaseWidget | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** Every promoted subgraph input on a node, projected to ordinary widgets. */
|
||||
export function promotedInputWidgets(node: LGraphNode): IBaseWidget[] {
|
||||
return node.inputs.flatMap((input) => {
|
||||
const widget = promotedInputWidget(input)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
export interface ResolvedPromotedWidget {
|
||||
node: LGraphNode
|
||||
@@ -12,6 +13,6 @@ export interface ResolvedPromotedWidget {
|
||||
* the source is a stored tuple rather than something link-derivable.
|
||||
*/
|
||||
export interface PromotedWidgetSource {
|
||||
sourceNodeId: string
|
||||
sourceNodeId: NodeId
|
||||
sourceWidgetName: string
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { WidgetId } from '@/types/widgetId'
|
||||
import { widgetId } from '@/types/widgetId'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
@@ -33,7 +35,7 @@ export function getWidgetName(w: IBaseWidget): string {
|
||||
|
||||
export function isLinkedPromotion(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceNodeId: SerializedNodeId,
|
||||
sourceWidgetName: string
|
||||
): boolean {
|
||||
return (
|
||||
@@ -44,9 +46,10 @@ export function isLinkedPromotion(
|
||||
|
||||
export function findHostInputForPromotion(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
rawSourceNodeId: SerializedNodeId,
|
||||
sourceWidgetName: string
|
||||
) {
|
||||
const sourceNodeId = toNodeId(rawSourceNodeId)
|
||||
return subgraphNode.inputs.find((input) => {
|
||||
const source = input._subgraphSlot
|
||||
? resolvePromotionSource(subgraphNode, input._subgraphSlot)
|
||||
@@ -74,7 +77,7 @@ function resolvePromotionSource(
|
||||
|
||||
if (inputNode.isSubgraphNode()) {
|
||||
return {
|
||||
sourceNodeId: String(inputNode.id),
|
||||
sourceNodeId: toNodeId(inputNode.id),
|
||||
sourceWidgetName: targetInput.name
|
||||
}
|
||||
}
|
||||
@@ -83,7 +86,7 @@ function resolvePromotionSource(
|
||||
if (!targetWidget) continue
|
||||
|
||||
return {
|
||||
sourceNodeId: String(inputNode.id),
|
||||
sourceNodeId: toNodeId(inputNode.id),
|
||||
sourceWidgetName: targetWidget.name
|
||||
}
|
||||
}
|
||||
@@ -212,7 +215,7 @@ function toPromotionSource(
|
||||
widget: IBaseWidget
|
||||
): PromotedWidgetSource {
|
||||
return {
|
||||
sourceNodeId: String(node.id),
|
||||
sourceNodeId: toNodeId(node.id),
|
||||
sourceWidgetName: getWidgetName(widget)
|
||||
}
|
||||
}
|
||||
@@ -235,9 +238,7 @@ export function promoteValueWidgetViaSubgraphInput(
|
||||
sourceWidget: IBaseWidget
|
||||
): CanonicalPromotionResult {
|
||||
const sourceWidgetName = getWidgetName(sourceWidget)
|
||||
if (
|
||||
isLinkedPromotion(subgraphNode, String(sourceNode.id), sourceWidgetName)
|
||||
) {
|
||||
if (isLinkedPromotion(subgraphNode, sourceNode.id, sourceWidgetName)) {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
@@ -315,7 +316,7 @@ function promotePreviewViaExposure(
|
||||
if (existing) return
|
||||
|
||||
store.addExposure(rootGraphId, hostLocator, {
|
||||
sourceNodeId: String(sourceNode.id),
|
||||
sourceNodeId: sourceNode.id,
|
||||
sourcePreviewName
|
||||
})
|
||||
}
|
||||
@@ -603,7 +604,7 @@ export function pruneDisconnected(subgraphNode: SubgraphNode) {
|
||||
if (!hostInput?.widgetId && !hostInput?._widget) return false
|
||||
|
||||
removedEntries.push({
|
||||
sourceNodeId: String(subgraphNode.id),
|
||||
sourceNodeId: toNodeId(subgraphNode.id),
|
||||
sourceWidgetName: input.name
|
||||
})
|
||||
return true
|
||||
@@ -643,7 +644,7 @@ export function hasUnpromotedWidgets(subgraphNode: SubgraphNode): boolean {
|
||||
!isWidgetPromotedOnSubgraphNode(
|
||||
subgraphNode,
|
||||
{
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceNodeId: toNodeId(interiorNode.id),
|
||||
sourceWidgetName: widget.name
|
||||
},
|
||||
widget
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { ResolvedPromotedWidget } from '@/core/graph/subgraph/promotedWidge
|
||||
import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
|
||||
|
||||
type PromotedWidgetResolutionFailure =
|
||||
| 'invalid-host'
|
||||
@@ -18,7 +20,7 @@ const MAX_PROMOTED_WIDGET_CHAIN_DEPTH = 100
|
||||
|
||||
function traversePromotedWidgetChain(
|
||||
hostNode: SubgraphNode,
|
||||
nodeId: string,
|
||||
nodeId: NodeId,
|
||||
widgetName: string
|
||||
): PromotedWidgetResolutionResult {
|
||||
const visitedByHost = new WeakMap<SubgraphNode, Set<string>>()
|
||||
@@ -69,11 +71,12 @@ function traversePromotedWidgetChain(
|
||||
|
||||
export function resolveConcretePromotedWidget(
|
||||
hostNode: LGraphNode,
|
||||
nodeId: string,
|
||||
rawNodeId: SerializedNodeId,
|
||||
widgetName: string
|
||||
): PromotedWidgetResolutionResult {
|
||||
if (!hostNode.isSubgraphNode()) {
|
||||
return { status: 'failure', failure: 'invalid-host' }
|
||||
}
|
||||
const nodeId = toNodeId(rawNodeId)
|
||||
return traversePromotedWidgetChain(hostNode, nodeId, widgetName)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({})
|
||||
@@ -139,7 +140,7 @@ describe('resolveSubgraphInputTarget', () => {
|
||||
(slot) => slot.name === 'seed'
|
||||
)!
|
||||
const node = new LGraphNode('Interior-seed')
|
||||
node.id = 42
|
||||
node.id = nodeId(42)
|
||||
const input = node.addInput('seed_input', '*')
|
||||
node.addWidget('number', 'seed', 0, () => undefined)
|
||||
input.widget = { name: 'seed' }
|
||||
@@ -224,7 +225,7 @@ describe('resolveSubgraphInputTarget', () => {
|
||||
inputs: [{ name: 'seed', type: '*' }]
|
||||
})
|
||||
const concreteNode = new LGraphNode('ConcreteNode')
|
||||
concreteNode.id = 900
|
||||
concreteNode.id = nodeId(900)
|
||||
const concreteInput = concreteNode.addInput('seed_input', '*')
|
||||
concreteNode.addWidget('number', 'seed', 0, () => undefined)
|
||||
concreteInput.widget = { name: 'seed' }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
import { resolveSubgraphInputLink } from './resolveSubgraphInputLink'
|
||||
|
||||
type ResolvedSubgraphInputTarget = {
|
||||
nodeId: string
|
||||
nodeId: NodeId
|
||||
widgetName: string
|
||||
}
|
||||
|
||||
@@ -17,7 +19,7 @@ export function resolveSubgraphInputTarget(
|
||||
({ inputNode, targetInput, getTargetWidget }) => {
|
||||
if (inputNode.isSubgraphNode()) {
|
||||
return {
|
||||
nodeId: String(inputNode.id),
|
||||
nodeId: toNodeId(inputNode.id),
|
||||
widgetName: targetInput.name
|
||||
}
|
||||
}
|
||||
@@ -26,7 +28,7 @@ export function resolveSubgraphInputTarget(
|
||||
if (!targetWidget) return undefined
|
||||
|
||||
return {
|
||||
nodeId: String(inputNode.id),
|
||||
nodeId: toNodeId(inputNode.id),
|
||||
widgetName: targetWidget.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
*/
|
||||
export function parseNodePropertyArray<T>(
|
||||
property: NodeProperty | undefined,
|
||||
schema: z.ZodType<T[]>,
|
||||
schema: z.ZodType<T[], z.ZodTypeDef, unknown>,
|
||||
contextName: string
|
||||
): T[] {
|
||||
if (property === undefined) return []
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
|
||||
import { parseNodePropertyArray } from './parseNodePropertyArray'
|
||||
|
||||
const previewExposureSchema = z.object({
|
||||
name: z.string(),
|
||||
sourceNodeId: z.string(),
|
||||
sourceNodeId: z.string().transform(nodeId),
|
||||
sourcePreviewName: z.string()
|
||||
})
|
||||
export type PreviewExposure = z.infer<typeof previewExposureSchema>
|
||||
@@ -16,7 +17,7 @@ const previewExposuresPropertySchema = z.array(previewExposureSchema)
|
||||
export function parsePreviewExposures(
|
||||
property: NodeProperty | undefined
|
||||
): PreviewExposure[] {
|
||||
return parseNodePropertyArray(
|
||||
return parseNodePropertyArray<PreviewExposure>(
|
||||
property,
|
||||
previewExposuresPropertySchema,
|
||||
'properties.previewExposures'
|
||||
|
||||
@@ -26,6 +26,7 @@ vi.mock('@/scripts/app', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/extensions/core/load3d', () => ({}))
|
||||
vi.mock('@/extensions/core/load3dAdvanced', () => ({}))
|
||||
vi.mock('@/extensions/core/load3dPreviewExtensions', () => ({}))
|
||||
vi.mock('@/extensions/core/saveMesh', () => ({}))
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import { createMockLLink } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
@@ -39,7 +40,7 @@ function createTargetNode(
|
||||
id = 7
|
||||
): Pick<LGraphNode, 'id' | 'inputs' | 'widgets'> {
|
||||
return fromPartial<Pick<LGraphNode, 'id' | 'inputs' | 'widgets'>>({
|
||||
id,
|
||||
id: nodeId(id),
|
||||
inputs: [
|
||||
fromPartial<INodeInputSlot>({
|
||||
widget: { name: widget.name }
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { NodeId, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraph,
|
||||
LGraphGroup,
|
||||
@@ -17,6 +17,7 @@ import type { UUID } from '@/utils/uuid'
|
||||
import { zeroUuid } from '@/utils/uuid'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import { widgetId } from '@/types/widgetId'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
@@ -298,7 +299,7 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
})
|
||||
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const seedWidgetId = widgetId(graphId, '10' as NodeId, 'seed')
|
||||
const seedWidgetId = widgetId(graphId, '10', 'seed')
|
||||
widgetValueStore.registerWidget(seedWidgetId, {
|
||||
type: 'number',
|
||||
value: 1,
|
||||
@@ -621,7 +622,7 @@ describe('ensureGlobalIdUniqueness', () => {
|
||||
|
||||
expect(subNode.id).not.toBe(rootNode.id)
|
||||
expect(subgraph._nodes_by_id[subNode.id]).toBe(subNode)
|
||||
expect(subgraph._nodes_by_id[rootNode.id as number]).toBeUndefined()
|
||||
expect(subgraph._nodes_by_id[rootNode.id]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves root graph node IDs as canonical', () => {
|
||||
@@ -657,7 +658,7 @@ describe('ensureGlobalIdUniqueness', () => {
|
||||
rootGraph.ensureGlobalIdUniqueness()
|
||||
|
||||
expect(rootGraph.state.lastNodeId).toBeGreaterThanOrEqual(
|
||||
subNode.id as number
|
||||
Number(subNode.id)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -674,7 +675,7 @@ describe('ensureGlobalIdUniqueness', () => {
|
||||
subgraph._nodes_by_id[subNodeA.id] = subNodeA
|
||||
|
||||
const subNodeB = new DummyNode()
|
||||
subNodeB.id = 999
|
||||
subNodeB.id = nodeId(999)
|
||||
subgraph._nodes.push(subNodeB)
|
||||
subgraph._nodes_by_id[subNodeB.id] = subNodeB
|
||||
|
||||
@@ -693,7 +694,7 @@ describe('ensureGlobalIdUniqueness', () => {
|
||||
const subgraph = createSubgraphOnGraph(rootGraph)
|
||||
|
||||
const subNode = new DummyNode()
|
||||
subNode.id = 42
|
||||
subNode.id = nodeId(42)
|
||||
subgraph._nodes.push(subNode)
|
||||
subgraph._nodes_by_id[subNode.id] = subNode
|
||||
|
||||
@@ -1005,7 +1006,7 @@ describe('Subgraph Unpacking', () => {
|
||||
|
||||
const firstInstance = createTestSubgraphNode(subgraph, { pos: [100, 100] })
|
||||
const secondInstance = createTestSubgraphNode(subgraph, { pos: [300, 100] })
|
||||
secondInstance.id = 2
|
||||
secondInstance.id = nodeId(2)
|
||||
rootGraph.add(firstInstance)
|
||||
rootGraph.add(secondInstance)
|
||||
|
||||
@@ -1053,7 +1054,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
|
||||
const idsB = nodeIdSet(graph, SUBGRAPH_B)
|
||||
|
||||
for (const id of SHARED_NODE_IDS) {
|
||||
expect(idsA.has(id as NodeId)).toBe(true)
|
||||
expect(idsA.has(nodeId(id))).toBe(true)
|
||||
}
|
||||
for (const id of idsA) {
|
||||
expect(idsB.has(id)).toBe(false)
|
||||
@@ -1065,8 +1066,8 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
|
||||
const idsB = nodeIdSet(graph, SUBGRAPH_B)
|
||||
|
||||
for (const link of graph.subgraphs.get(SUBGRAPH_B)!.links.values()) {
|
||||
expect(idsB.has(link.origin_id)).toBe(true)
|
||||
expect(idsB.has(link.target_id)).toBe(true)
|
||||
if (link.origin_id !== -1) expect(idsB.has(link.origin_id)).toBe(true)
|
||||
if (link.target_id !== -1) expect(idsB.has(link.target_id)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1075,7 +1076,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
|
||||
const idsB = nodeIdSet(graph, SUBGRAPH_B)
|
||||
|
||||
for (const widget of graph.subgraphs.get(SUBGRAPH_B)!.widgets) {
|
||||
expect(idsB.has(widget.id)).toBe(true)
|
||||
expect(idsB.has(nodeId(widget.id))).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1089,14 +1090,14 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
|
||||
graph.subgraphs.get(SUBGRAPH_B)!.nodes.map((n) => String(n.id))
|
||||
)
|
||||
|
||||
const pw102 = graph.getNodeById(102 as NodeId)?.properties?.proxyWidgets
|
||||
const pw102 = graph.getNodeById(102)?.properties?.proxyWidgets
|
||||
expect(Array.isArray(pw102)).toBe(true)
|
||||
for (const entry of pw102 as unknown[][]) {
|
||||
expect(Array.isArray(entry)).toBe(true)
|
||||
expect(idsA.has(String(entry[0]))).toBe(true)
|
||||
}
|
||||
|
||||
const pw103 = graph.getNodeById(103 as NodeId)?.properties?.proxyWidgets
|
||||
const pw103 = graph.getNodeById(103)?.properties?.proxyWidgets
|
||||
expect(Array.isArray(pw103)).toBe(true)
|
||||
for (const entry of pw103 as unknown[][]) {
|
||||
expect(Array.isArray(entry)).toBe(true)
|
||||
@@ -1114,7 +1115,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
|
||||
|
||||
const innerNode = graph.subgraphs
|
||||
.get(SUBGRAPH_A)!
|
||||
.nodes.find((n) => n.id === (50 as NodeId))
|
||||
.nodes.find((n) => n.id === nodeId(50))
|
||||
const pw = innerNode?.properties?.proxyWidgets
|
||||
expect(Array.isArray(pw)).toBe(true)
|
||||
for (const entry of pw as unknown[][]) {
|
||||
@@ -1154,7 +1155,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
|
||||
expect(migrationCall).toBeDefined()
|
||||
expect(migrationCall![1]).toEqual(
|
||||
expect.objectContaining({
|
||||
hostNodeId: expect.any(Number),
|
||||
hostNodeId: expect.any(String),
|
||||
proxyWidgets: expect.anything()
|
||||
})
|
||||
)
|
||||
@@ -1176,8 +1177,12 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
|
||||
const graph = new LGraph()
|
||||
graph.configure(structuredClone(uniqueSubgraphNodeIds))
|
||||
|
||||
expect(nodeIdSet(graph, SUBGRAPH_A)).toEqual(new Set([10, 11, 12]))
|
||||
expect(nodeIdSet(graph, SUBGRAPH_B)).toEqual(new Set([20, 21, 22]))
|
||||
expect(nodeIdSet(graph, SUBGRAPH_A)).toEqual(
|
||||
new Set([nodeId(10), nodeId(11), nodeId(12)])
|
||||
)
|
||||
expect(nodeIdSet(graph, SUBGRAPH_B)).toEqual(
|
||||
new Set([nodeId(20), nodeId(21), nodeId(22)])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMuta
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { UNASSIGNED_NODE_ID, nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
|
||||
import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||
|
||||
import {
|
||||
@@ -25,9 +27,8 @@ import { LGraphCanvas } from './LGraphCanvas'
|
||||
import { LGraphGroup } from './LGraphGroup'
|
||||
import type { GroupId } from './LGraphGroup'
|
||||
import { LGraphNode } from './LGraphNode'
|
||||
import type { NodeId } from './LGraphNode'
|
||||
import { LLink } from './LLink'
|
||||
import type { LinkId } from './LLink'
|
||||
import type { LinkEndpointNodeId, LinkId } from './LLink'
|
||||
import { MapProxyHandler } from './MapProxyHandler'
|
||||
import { Reroute } from './Reroute'
|
||||
import type { RerouteId } from './Reroute'
|
||||
@@ -103,6 +104,22 @@ function isLGraphTriggerAction(action: string): action is LGraphTriggerAction {
|
||||
return validTriggerActions.has(action as LGraphTriggerAction)
|
||||
}
|
||||
|
||||
function nextNodeId(state: LGraphState): NodeId {
|
||||
return toNodeId(++state.lastNodeId)
|
||||
}
|
||||
|
||||
function numericNodeId(id: NodeId): number | null {
|
||||
const numericId = Number(id)
|
||||
return Number.isInteger(numericId) ? numericId : null
|
||||
}
|
||||
|
||||
function syncLastNodeId(state: LGraphState, id: NodeId): void {
|
||||
const numericId = numericNodeId(id)
|
||||
if (numericId !== null && state.lastNodeId < numericId) {
|
||||
state.lastNodeId = numericId
|
||||
}
|
||||
}
|
||||
|
||||
export type RendererType = 'LG' | 'Vue' | 'Vue-corrected'
|
||||
|
||||
/**
|
||||
@@ -638,8 +655,8 @@ export class LGraph
|
||||
const S: LGraphNode[] = []
|
||||
const M: Dictionary<LGraphNode> = {}
|
||||
// to avoid repeating links
|
||||
const visited_links: Record<NodeId, boolean> = {}
|
||||
const remaining_links: Record<NodeId, number> = {}
|
||||
const visited_links: Record<SerializedNodeId, boolean> = {}
|
||||
const remaining_links: Record<SerializedNodeId, number> = {}
|
||||
|
||||
// search for the nodes without inputs (starting nodes)
|
||||
for (const node of this._nodes) {
|
||||
@@ -955,11 +972,11 @@ export class LGraph
|
||||
}
|
||||
|
||||
// nodes
|
||||
if (node.id != -1 && this._nodes_by_id[node.id] != null) {
|
||||
if (node.id !== UNASSIGNED_NODE_ID && this._nodes_by_id[node.id] != null) {
|
||||
console.warn(
|
||||
'LiteGraph: there is already a node with this ID, changing it'
|
||||
)
|
||||
node.id = ++state.lastNodeId
|
||||
node.id = nextNodeId(state)
|
||||
}
|
||||
|
||||
if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
|
||||
@@ -967,10 +984,10 @@ export class LGraph
|
||||
}
|
||||
|
||||
// give him an id
|
||||
if (node.id == null || node.id == -1) {
|
||||
node.id = ++state.lastNodeId
|
||||
} else if (typeof node.id === 'number' && state.lastNodeId < node.id) {
|
||||
state.lastNodeId = node.id
|
||||
if (node.id == null || node.id === UNASSIGNED_NODE_ID) {
|
||||
node.id = nextNodeId(state)
|
||||
} else {
|
||||
syncLastNodeId(state, node.id)
|
||||
}
|
||||
|
||||
// Set ghost flag before registration so VueNodeData picks it up
|
||||
@@ -1133,8 +1150,8 @@ export class LGraph
|
||||
/**
|
||||
* Returns a node by its id.
|
||||
*/
|
||||
getNodeById(id: NodeId | null | undefined): LGraphNode | null {
|
||||
return id != null ? this._nodes_by_id[id] : null
|
||||
getNodeById(id: SerializedNodeId | null | undefined): LGraphNode | null {
|
||||
return id != null ? this._nodes_by_id[toNodeId(id)] : null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1967,9 +1984,10 @@ export class LGraph
|
||||
}
|
||||
}
|
||||
|
||||
nodeIdMap.set(n_info.id, ++this.last_node_id)
|
||||
node.id = this.last_node_id
|
||||
n_info.id = this.last_node_id
|
||||
const newNodeId = nextNodeId(this.state)
|
||||
nodeIdMap.set(toNodeId(n_info.id), newNodeId)
|
||||
node.id = newNodeId
|
||||
n_info.id = newNodeId
|
||||
|
||||
// Strip links from serialized data before configure to prevent
|
||||
// onConnectionsChange from resolving subgraph-internal link IDs
|
||||
@@ -2023,9 +2041,9 @@ export class LGraph
|
||||
}
|
||||
}
|
||||
const newLinks: {
|
||||
oid: NodeId
|
||||
oid: LinkEndpointNodeId
|
||||
oslot: number
|
||||
tid: NodeId
|
||||
tid: LinkEndpointNodeId
|
||||
tslot: number
|
||||
id: LinkId
|
||||
iparent?: RerouteId
|
||||
@@ -2045,7 +2063,8 @@ export class LGraph
|
||||
link.origin_slot = outerLink.origin_slot
|
||||
externalParentId = outerLink.parentId
|
||||
} else {
|
||||
const origin_id = nodeIdMap.get(link.origin_id)
|
||||
const origin_id =
|
||||
link.origin_id === -1 ? undefined : nodeIdMap.get(link.origin_id)
|
||||
if (!origin_id) {
|
||||
console.error('Missing Link ID when unpacking')
|
||||
continue
|
||||
@@ -2070,7 +2089,8 @@ export class LGraph
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
const target_id = nodeIdMap.get(link.target_id)
|
||||
const target_id =
|
||||
link.target_id === -1 ? undefined : nodeIdMap.get(link.target_id)
|
||||
if (!target_id) {
|
||||
console.error('Missing Link ID when unpacking')
|
||||
continue
|
||||
@@ -2108,7 +2128,9 @@ export class LGraph
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
}
|
||||
const tnode = this._nodes_by_id[newLink.tid]
|
||||
if (newLink.tid === -1) continue
|
||||
const tnode = this.getNodeById(newLink.tid)
|
||||
if (!tnode) continue
|
||||
created = this.inputNode.slots[newLink.oslot].connect(
|
||||
tnode.inputs[newLink.tslot],
|
||||
tnode
|
||||
@@ -2118,17 +2140,19 @@ export class LGraph
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
}
|
||||
const tnode = this._nodes_by_id[newLink.oid]
|
||||
if (newLink.oid === -1) continue
|
||||
const tnode = this.getNodeById(newLink.oid)
|
||||
if (!tnode) continue
|
||||
created = this.outputNode.slots[newLink.tslot].connect(
|
||||
tnode.outputs[newLink.oslot],
|
||||
tnode
|
||||
)
|
||||
} else {
|
||||
created = this._nodes_by_id[newLink.oid].connect(
|
||||
newLink.oslot,
|
||||
this._nodes_by_id[newLink.tid],
|
||||
newLink.tslot
|
||||
)
|
||||
if (newLink.oid === -1 || newLink.tid === -1) continue
|
||||
const originNode = this.getNodeById(newLink.oid)
|
||||
const targetNode = this.getNodeById(newLink.tid)
|
||||
if (!originNode || !targetNode) continue
|
||||
created = originNode.connect(newLink.oslot, targetNode, newLink.tslot)
|
||||
}
|
||||
if (!created) {
|
||||
console.error('Failed to create link')
|
||||
@@ -2239,7 +2263,7 @@ export class LGraph
|
||||
* @param nodeIds An ordered list of node IDs, from the root graph to the most nested subgraph node
|
||||
* @returns An ordered list of nested subgraph nodes.
|
||||
*/
|
||||
resolveSubgraphIdPath(nodeIds: readonly NodeId[]): SubgraphNode[] {
|
||||
resolveSubgraphIdPath(nodeIds: readonly SerializedNodeId[]): SubgraphNode[] {
|
||||
const result: SubgraphNode[] = []
|
||||
let currentGraph: GraphOrSubgraph = this.rootGraph
|
||||
|
||||
@@ -2504,11 +2528,13 @@ export class LGraph
|
||||
if (subgraphs) {
|
||||
const reservedNodeIds = new Set<number>()
|
||||
for (const node of this._nodes) {
|
||||
if (typeof node.id === 'number') reservedNodeIds.add(node.id)
|
||||
const id = numericNodeId(node.id)
|
||||
if (id !== null) reservedNodeIds.add(id)
|
||||
}
|
||||
for (const sg of this.subgraphs.values()) {
|
||||
for (const node of sg.nodes) {
|
||||
if (typeof node.id === 'number') reservedNodeIds.add(node.id)
|
||||
const id = numericNodeId(node.id)
|
||||
if (id !== null) reservedNodeIds.add(id)
|
||||
}
|
||||
}
|
||||
for (const n of nodesData ?? []) {
|
||||
@@ -2538,7 +2564,7 @@ export class LGraph
|
||||
}
|
||||
|
||||
let error = false
|
||||
const nodeDataMap = new Map<NodeId, ISerialisedNode>()
|
||||
const nodeDataMap = new Map<SerializedNodeId, ISerialisedNode>()
|
||||
|
||||
// create nodes
|
||||
this._nodes = []
|
||||
@@ -2559,7 +2585,7 @@ export class LGraph
|
||||
}
|
||||
|
||||
// id it or it will create a new id
|
||||
node.id = n_info.id
|
||||
node.id = toNodeId(n_info.id)
|
||||
// add before configure, otherwise configure cannot create links
|
||||
this.add(node, true)
|
||||
nodeDataMap.set(node.id, n_info)
|
||||
@@ -2681,24 +2707,24 @@ export class LGraph
|
||||
const remappedIds = new Map<NodeId, NodeId>()
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
if (typeof node.id !== 'number') continue
|
||||
const currentId = numericNodeId(node.id)
|
||||
if (currentId === null) continue
|
||||
|
||||
if (usedNodeIds.has(node.id)) {
|
||||
if (usedNodeIds.has(currentId)) {
|
||||
const oldId = node.id
|
||||
while (usedNodeIds.has(++state.lastNodeId));
|
||||
const newId = state.lastNodeId
|
||||
const newId = toNodeId(state.lastNodeId)
|
||||
delete graph._nodes_by_id[oldId]
|
||||
node.id = newId
|
||||
graph._nodes_by_id[newId] = node
|
||||
usedNodeIds.add(newId)
|
||||
usedNodeIds.add(state.lastNodeId)
|
||||
remappedIds.set(oldId, newId)
|
||||
console.warn(
|
||||
`LiteGraph: duplicate node ID ${oldId} reassigned to ${newId} in graph ${graph.id}`
|
||||
)
|
||||
} else {
|
||||
usedNodeIds.add(node.id as number)
|
||||
if ((node.id as number) > state.lastNodeId)
|
||||
state.lastNodeId = node.id as number
|
||||
usedNodeIds.add(currentId)
|
||||
if (currentId > state.lastNodeId) state.lastNodeId = currentId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2891,7 +2917,7 @@ export class Subgraph
|
||||
|
||||
private _repairSlotLinkIds(
|
||||
linkIds: LinkId[],
|
||||
ioNodeId: number,
|
||||
ioNodeId: NodeId,
|
||||
slotIndex: number
|
||||
): void {
|
||||
const repaired = linkIds.map((id) =>
|
||||
@@ -2905,7 +2931,7 @@ export class Subgraph
|
||||
}
|
||||
|
||||
private _findLinkBySlot(
|
||||
nodeId: number,
|
||||
nodeId: NodeId,
|
||||
slotIndex: number
|
||||
): LLink | undefined {
|
||||
for (const link of this._links.values()) {
|
||||
@@ -3109,10 +3135,12 @@ function patchLinkNodeIds(
|
||||
remappedIds: Map<NodeId, NodeId>
|
||||
): void {
|
||||
for (const link of links.values()) {
|
||||
const newOrigin = remappedIds.get(link.origin_id)
|
||||
const newOrigin =
|
||||
link.origin_id === -1 ? undefined : remappedIds.get(link.origin_id)
|
||||
if (newOrigin !== undefined) link.origin_id = newOrigin
|
||||
|
||||
const newTarget = remappedIds.get(link.target_id)
|
||||
const newTarget =
|
||||
link.target_id === -1 ? undefined : remappedIds.get(link.target_id)
|
||||
if (newTarget !== undefined) link.target_id = newTarget
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
createUuidv4
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { remapClipboardSubgraphNodeIds } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import type {
|
||||
ClipboardItems,
|
||||
ExportedSubgraph,
|
||||
@@ -52,7 +53,7 @@ describe('remapClipboardSubgraphNodeIds', () => {
|
||||
it('remaps pasted subgraph interior IDs and proxyWidgets references', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const existingNode = new LGraphNode('existing')
|
||||
existingNode.id = 1
|
||||
existingNode.id = nodeId(1)
|
||||
rootGraph.add(existingNode)
|
||||
|
||||
const subgraphId = createUuidv4()
|
||||
@@ -124,7 +125,7 @@ describe('remapClipboardSubgraphNodeIds', () => {
|
||||
it('remaps pasted SubgraphNode previewExposures sourceNodeId references', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const existingNode = new LGraphNode('existing')
|
||||
existingNode.id = 1
|
||||
existingNode.id = nodeId(1)
|
||||
rootGraph.add(existingNode)
|
||||
|
||||
const subgraphId = createUuidv4()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import type { NodeLayout } from '@/renderer/core/layout/types'
|
||||
@@ -74,7 +77,7 @@ function createCanvas(graph: LGraph): LGraphCanvas {
|
||||
}
|
||||
|
||||
function createLayoutEntry(node: LGraphNode, zIndex: number) {
|
||||
const nodeId = String(node.id)
|
||||
const nodeId = toNodeId(node.id)
|
||||
const layout: NodeLayout = {
|
||||
id: nodeId,
|
||||
position: { x: node.pos[0], y: node.pos[1] },
|
||||
@@ -99,7 +102,7 @@ function createLayoutEntry(node: LGraphNode, zIndex: number) {
|
||||
})
|
||||
}
|
||||
|
||||
function setZIndex(nodeId: string, zIndex: number, previousZIndex: number) {
|
||||
function setZIndex(nodeId: NodeId, zIndex: number, previousZIndex: number) {
|
||||
layoutStore.applyOperation({
|
||||
type: 'setNodeZIndex',
|
||||
entity: 'node',
|
||||
@@ -145,7 +148,7 @@ describe('cloned node z-index in Vue renderer', () => {
|
||||
originalNode.size = [200, 100]
|
||||
graph.add(originalNode)
|
||||
|
||||
const originalNodeId = String(originalNode.id)
|
||||
const originalNodeId = toNodeId(originalNode.id)
|
||||
|
||||
setZIndex(originalNodeId, 5, 0)
|
||||
|
||||
@@ -158,7 +161,7 @@ describe('cloned node z-index in Vue renderer', () => {
|
||||
expect(result!.created.length).toBe(1)
|
||||
|
||||
const clonedNode = result!.created[0] as LGraphNode
|
||||
const clonedNodeId = String(clonedNode.id)
|
||||
const clonedNodeId = toNodeId(clonedNode.id)
|
||||
|
||||
// The cloned node should have a z-index higher than the original
|
||||
const clonedLayout = layoutStore.getNodeLayoutRef(clonedNodeId).value
|
||||
@@ -171,13 +174,13 @@ describe('cloned node z-index in Vue renderer', () => {
|
||||
nodeA.pos = [100, 100]
|
||||
nodeA.size = [200, 100]
|
||||
graph.add(nodeA)
|
||||
setZIndex(String(nodeA.id), 3, 0)
|
||||
setZIndex(toNodeId(nodeA.id), 3, 0)
|
||||
|
||||
const nodeB = new TestNode()
|
||||
nodeB.pos = [400, 100]
|
||||
nodeB.size = [200, 100]
|
||||
graph.add(nodeB)
|
||||
setZIndex(String(nodeB.id), 7, 0)
|
||||
setZIndex(toNodeId(nodeB.id), 7, 0)
|
||||
|
||||
const result = LGraphCanvas.cloneNodes([nodeA, nodeB])
|
||||
expect(result).toBeDefined()
|
||||
@@ -185,8 +188,8 @@ describe('cloned node z-index in Vue renderer', () => {
|
||||
|
||||
const clonedA = result!.created[0] as LGraphNode
|
||||
const clonedB = result!.created[1] as LGraphNode
|
||||
const layoutA = layoutStore.getNodeLayoutRef(String(clonedA.id)).value!
|
||||
const layoutB = layoutStore.getNodeLayoutRef(String(clonedB.id)).value!
|
||||
const layoutA = layoutStore.getNodeLayoutRef(toNodeId(clonedA.id)).value!
|
||||
const layoutB = layoutStore.getNodeLayoutRef(toNodeId(clonedB.id)).value!
|
||||
|
||||
// Both cloned nodes should be above the highest original (z-index 7)
|
||||
expect(layoutA.zIndex).toBeGreaterThan(7)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
|
||||
import {
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
@@ -107,7 +109,7 @@ describe('LGraphCanvas slot hit detection', () => {
|
||||
|
||||
// Mock the slot query to return our node's slot
|
||||
vi.mocked(layoutStore.querySlotAtPoint).mockReturnValue({
|
||||
nodeId: String(node.id),
|
||||
nodeId: toNodeId(node.id),
|
||||
index: 0,
|
||||
type: 'output',
|
||||
position: { x: 252, y: 120 },
|
||||
@@ -188,7 +190,7 @@ describe('LGraphCanvas slot hit detection', () => {
|
||||
expect(node.isPointInside(clickX, clickY)).toBe(false)
|
||||
|
||||
vi.mocked(layoutStore.querySlotAtPoint).mockReturnValue({
|
||||
nodeId: String(node.id),
|
||||
nodeId: toNodeId(node.id),
|
||||
index: 0,
|
||||
type: 'input',
|
||||
position: { x: 98, y: 140 },
|
||||
|
||||
@@ -21,7 +21,8 @@ import type { AnimationOptions } from './DragAndScale'
|
||||
import type { LGraph, SubgraphId } from './LGraph'
|
||||
import { LGraphGroup } from './LGraphGroup'
|
||||
import { LGraphNode } from './LGraphNode'
|
||||
import type { NodeId, NodeProperty } from './LGraphNode'
|
||||
import type { NodeProperty } from './LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import { LLink } from './LLink'
|
||||
import type { LinkId } from './LLink'
|
||||
import { Reroute } from './Reroute'
|
||||
@@ -213,7 +214,7 @@ interface LGraphCanvasState {
|
||||
selectionChanged: boolean
|
||||
|
||||
/** ID of node currently in ghost placement mode (semi-transparent, following cursor). */
|
||||
ghostNodeId: NodeId | null
|
||||
ghostNodeId: SerializedNodeId | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,7 +225,7 @@ interface ClipboardPasteResult {
|
||||
/** All successfully created items */
|
||||
created: Positionable[]
|
||||
/** Map: original node IDs to newly created nodes */
|
||||
nodes: Map<NodeId, LGraphNode>
|
||||
nodes: Map<SerializedNodeId, LGraphNode>
|
||||
/** Map: original link IDs to new link IDs */
|
||||
links: Map<LinkId, LLink>
|
||||
/** Map: original reroute IDs to newly created reroutes */
|
||||
@@ -678,7 +679,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* The IDs of the nodes that are currently visible on the canvas. More
|
||||
* performant than {@link visible_nodes} for visibility checks.
|
||||
*/
|
||||
private _visible_node_ids: Set<NodeId> = new Set()
|
||||
private _visible_node_ids: Set<SerializedNodeId> = new Set()
|
||||
node_over?: LGraphNode
|
||||
node_capturing_input?: LGraphNode | null
|
||||
highlighted_links: Dictionary<boolean> = {}
|
||||
@@ -691,7 +692,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
dirty_canvas: boolean = true
|
||||
dirty_bgcanvas: boolean = true
|
||||
/** A map of nodes that require selective-redraw */
|
||||
dirty_nodes = new Map<NodeId, LGraphNode>()
|
||||
dirty_nodes = new Map<SerializedNodeId, LGraphNode>()
|
||||
dirty_area?: Rect | null
|
||||
/** @deprecated Unused */
|
||||
node_in_panel?: LGraphNode | null
|
||||
@@ -4168,7 +4169,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
const results: ClipboardPasteResult = {
|
||||
created: [],
|
||||
nodes: new Map<NodeId, LGraphNode>(),
|
||||
nodes: new Map<SerializedNodeId, LGraphNode>(),
|
||||
links: new Map<LinkId, LLink>(),
|
||||
reroutes: new Map<RerouteId, Reroute>(),
|
||||
subgraphs: new Map<SubgraphId, Subgraph>()
|
||||
@@ -4311,7 +4312,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
const newPositions = created
|
||||
.filter((item): item is LGraphNode => item instanceof LGraphNode)
|
||||
.map((node) => ({
|
||||
nodeId: String(node.id),
|
||||
nodeId: node.id,
|
||||
bounds: {
|
||||
x: node.pos[0],
|
||||
y: node.pos[1],
|
||||
@@ -8959,8 +8960,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
function patchLinkNodeIds(
|
||||
links: { origin_id: NodeId; target_id: NodeId }[] | undefined,
|
||||
remappedIds: Map<NodeId, NodeId>
|
||||
links:
|
||||
| { origin_id: SerializedNodeId; target_id: SerializedNodeId }[]
|
||||
| undefined,
|
||||
remappedIds: Map<SerializedNodeId, SerializedNodeId>
|
||||
) {
|
||||
if (!links?.length) return
|
||||
|
||||
@@ -8975,8 +8978,8 @@ function patchLinkNodeIds(
|
||||
|
||||
function remapNodeId(
|
||||
nodeId: string,
|
||||
remappedIds: Map<NodeId, NodeId>
|
||||
): NodeId | undefined {
|
||||
remappedIds: Map<SerializedNodeId, SerializedNodeId>
|
||||
): SerializedNodeId | undefined {
|
||||
const directMatch = remappedIds.get(nodeId)
|
||||
if (directMatch !== undefined) return directMatch
|
||||
if (!/^-?\d+$/.test(nodeId)) return undefined
|
||||
@@ -8989,7 +8992,7 @@ function remapNodeId(
|
||||
|
||||
function remapProxyWidgets(
|
||||
info: ISerialisedNode,
|
||||
remappedIds: Map<NodeId, NodeId> | undefined
|
||||
remappedIds: Map<SerializedNodeId, SerializedNodeId> | undefined
|
||||
) {
|
||||
if (!remappedIds || remappedIds.size === 0) return
|
||||
|
||||
@@ -9020,7 +9023,7 @@ function hasStringSourceNodeId(
|
||||
|
||||
function remapPreviewExposures(
|
||||
info: ISerialisedNode,
|
||||
remappedIds: Map<NodeId, NodeId> | undefined
|
||||
remappedIds: Map<SerializedNodeId, SerializedNodeId> | undefined
|
||||
) {
|
||||
if (!remappedIds || remappedIds.size === 0) return
|
||||
|
||||
@@ -9042,10 +9045,11 @@ export function remapClipboardSubgraphNodeIds(
|
||||
): void {
|
||||
const usedNodeIds = new Set<number>()
|
||||
forEachNode(rootGraph, (node) => {
|
||||
if (typeof node.id !== 'number') return
|
||||
usedNodeIds.add(node.id)
|
||||
if (rootGraph.state.lastNodeId < node.id)
|
||||
rootGraph.state.lastNodeId = node.id
|
||||
const numericId = Number(node.id)
|
||||
if (!Number.isInteger(numericId)) return
|
||||
usedNodeIds.add(numericId)
|
||||
if (rootGraph.state.lastNodeId < numericId)
|
||||
rootGraph.state.lastNodeId = numericId
|
||||
})
|
||||
|
||||
function nextUniqueNodeId() {
|
||||
@@ -9055,9 +9059,12 @@ export function remapClipboardSubgraphNodeIds(
|
||||
return nextId
|
||||
}
|
||||
|
||||
const subgraphNodeIdMap = new Map<SubgraphId, Map<NodeId, NodeId>>()
|
||||
const subgraphNodeIdMap = new Map<
|
||||
SubgraphId,
|
||||
Map<SerializedNodeId, SerializedNodeId>
|
||||
>()
|
||||
for (const subgraphInfo of parsed.subgraphs ?? []) {
|
||||
const remappedIds = new Map<NodeId, NodeId>()
|
||||
const remappedIds = new Map<SerializedNodeId, SerializedNodeId>()
|
||||
const interiorNodes = subgraphInfo.nodes ?? []
|
||||
|
||||
for (const nodeInfo of interiorNodes) {
|
||||
|
||||
@@ -8,6 +8,12 @@ import {
|
||||
import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import {
|
||||
UNASSIGNED_NODE_ID,
|
||||
nodeId as toNodeId,
|
||||
serializeNodeId
|
||||
} from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import type { ColorAdjustOptions } from '@/utils/colorUtil'
|
||||
import {
|
||||
@@ -16,7 +22,10 @@ import {
|
||||
toClass
|
||||
} from '@/lib/litegraph/src/utils/type'
|
||||
|
||||
import { SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants'
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
} from '@/lib/litegraph/src/constants'
|
||||
import { cachedMeasureText } from '@/lib/litegraph/src/utils/textMeasureCache'
|
||||
import type { DragAndScale } from './DragAndScale'
|
||||
import type { LGraph } from './LGraph'
|
||||
@@ -98,7 +107,7 @@ import type { WidgetTypeMap } from './widgets/widgetMap'
|
||||
|
||||
// #region Types
|
||||
|
||||
export type NodeId = number | string
|
||||
export type { NodeId } from '@/types/nodeId'
|
||||
|
||||
export type NodeProperty = string | number | boolean | object
|
||||
|
||||
@@ -498,10 +507,11 @@ export class LGraphNode
|
||||
|
||||
this._pos[0] = value[0]
|
||||
this._pos[1] = value[1]
|
||||
if (this.id === UNASSIGNED_NODE_ID || !this.graph) return
|
||||
|
||||
const mutations = useLayoutMutations()
|
||||
mutations.setSource(LayoutSource.Canvas)
|
||||
mutations.moveNode(String(this.id), { x: value[0], y: value[1] })
|
||||
mutations.moveNode(this.id, { x: value[0], y: value[1] })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -520,10 +530,11 @@ export class LGraphNode
|
||||
|
||||
this._size[0] = value[0]
|
||||
this._size[1] = value[1]
|
||||
if (this.id === UNASSIGNED_NODE_ID || !this.graph) return
|
||||
|
||||
const mutations = useLayoutMutations()
|
||||
mutations.setSource(LayoutSource.Canvas)
|
||||
mutations.resizeNode(String(this.id), {
|
||||
mutations.resizeNode(this.id, {
|
||||
width: value[0],
|
||||
height: value[1]
|
||||
})
|
||||
@@ -810,7 +821,7 @@ export class LGraphNode
|
||||
}
|
||||
|
||||
constructor(title: string, type?: string) {
|
||||
this.id = -1
|
||||
this.id = UNASSIGNED_NODE_ID
|
||||
this.title = title || 'Unnamed'
|
||||
this.type = type ?? ''
|
||||
this.size = [LiteGraph.NODE_WIDTH, 60]
|
||||
@@ -834,6 +845,7 @@ export class LGraphNode
|
||||
this.graph.incrementVersion()
|
||||
}
|
||||
if (info.id === -1) info.id = this.id
|
||||
else info.id = toNodeId(info.id)
|
||||
for (const j in info) {
|
||||
if (j == 'properties') {
|
||||
// i don't want to clone properties, I want to reuse the old container
|
||||
@@ -944,7 +956,7 @@ export class LGraphNode
|
||||
serialize(): ISerialisedNode {
|
||||
// create serialization object
|
||||
const o: ISerialisedNode = {
|
||||
id: this.id,
|
||||
id: serializeNodeId(this.id),
|
||||
type: this.type,
|
||||
pos: [this.pos[0], this.pos[1]],
|
||||
size: [this.size[0], this.size[1]],
|
||||
@@ -2001,7 +2013,7 @@ export class LGraphNode
|
||||
// Only register with store if node has a valid ID (is already in a graph).
|
||||
// If the node isn't in a graph yet (id === -1), registration happens
|
||||
// when the node is added via LGraph.add() -> node.onAdded.
|
||||
if (this.id !== -1 && isNodeBindable(widget)) {
|
||||
if (this.id !== UNASSIGNED_NODE_ID && isNodeBindable(widget)) {
|
||||
widget.setNodeId(this.id)
|
||||
}
|
||||
|
||||
@@ -3272,7 +3284,7 @@ export class LGraphNode
|
||||
const link_info = graph._links.get(link_id)
|
||||
if (link_info) {
|
||||
// Let SubgraphInput do the disconnect.
|
||||
if (link_info.origin_id === -10 && 'inputNode' in graph) {
|
||||
if (link_info.origin_id === SUBGRAPH_INPUT_ID && 'inputNode' in graph) {
|
||||
graph.inputNode._disconnectNodeInput(this, input, link_info)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
|
||||
import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import { nodeId as toNodeId, serializeNodeId } from '@/types/nodeId'
|
||||
|
||||
import type { LGraphNode, NodeId } from './LGraphNode'
|
||||
import type { LGraphNode } from './LGraphNode'
|
||||
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
|
||||
import type { Reroute, RerouteId } from './Reroute'
|
||||
import type {
|
||||
CanvasColour,
|
||||
@@ -24,16 +26,21 @@ import type { Serialisable, SerialisableLLink } from './types/serialisation'
|
||||
const layoutMutations = useLayoutMutations()
|
||||
|
||||
export type LinkId = number
|
||||
export type LinkEndpointNodeId = NodeId | -1
|
||||
|
||||
export type SerialisedLLinkArray = [
|
||||
id: LinkId,
|
||||
origin_id: NodeId,
|
||||
origin_id: SerializedNodeId,
|
||||
origin_slot: number,
|
||||
target_id: NodeId,
|
||||
target_id: SerializedNodeId,
|
||||
target_slot: number,
|
||||
type: ISlotType
|
||||
]
|
||||
|
||||
function toLinkEndpointNodeId(id: SerializedNodeId): LinkEndpointNodeId {
|
||||
return id === -1 ? -1 : toNodeId(id)
|
||||
}
|
||||
|
||||
// Resolved connection union; eliminates subgraph in/out as a possibility
|
||||
export type ResolvedConnection = BaseResolvedConnection &
|
||||
(
|
||||
@@ -97,11 +104,11 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
parentId?: RerouteId
|
||||
type: ISlotType
|
||||
/** Output node ID */
|
||||
origin_id: NodeId
|
||||
origin_id: LinkEndpointNodeId
|
||||
/** Output slot index */
|
||||
origin_slot: number
|
||||
/** Input node ID */
|
||||
target_id: NodeId
|
||||
target_id: LinkEndpointNodeId
|
||||
/** Input slot index */
|
||||
target_slot: number
|
||||
|
||||
@@ -154,17 +161,17 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
constructor(
|
||||
id: LinkId,
|
||||
type: ISlotType,
|
||||
origin_id: NodeId,
|
||||
origin_id: SerializedNodeId,
|
||||
origin_slot: number,
|
||||
target_id: NodeId,
|
||||
target_id: SerializedNodeId,
|
||||
target_slot: number,
|
||||
parentId?: RerouteId
|
||||
) {
|
||||
this.id = id
|
||||
this.type = type
|
||||
this.origin_id = origin_id
|
||||
this.origin_id = toLinkEndpointNodeId(origin_id)
|
||||
this.origin_slot = origin_slot
|
||||
this.target_id = target_id
|
||||
this.target_id = toLinkEndpointNodeId(target_id)
|
||||
this.target_slot = target_slot
|
||||
this.parentId = parentId
|
||||
|
||||
@@ -351,17 +358,17 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
configure(o: LLink | SerialisedLLinkArray) {
|
||||
if (Array.isArray(o)) {
|
||||
this.id = o[0]
|
||||
this.origin_id = o[1]
|
||||
this.origin_id = toLinkEndpointNodeId(o[1])
|
||||
this.origin_slot = o[2]
|
||||
this.target_id = o[3]
|
||||
this.target_id = toLinkEndpointNodeId(o[3])
|
||||
this.target_slot = o[4]
|
||||
this.type = o[5]
|
||||
} else {
|
||||
this.id = o.id
|
||||
this.type = o.type
|
||||
this.origin_id = o.origin_id
|
||||
this.origin_id = toLinkEndpointNodeId(o.origin_id)
|
||||
this.origin_slot = o.origin_slot
|
||||
this.target_id = o.target_id
|
||||
this.target_id = toLinkEndpointNodeId(o.target_id)
|
||||
this.target_slot = o.target_slot
|
||||
this.parentId = o.parentId
|
||||
}
|
||||
@@ -373,7 +380,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
* @param outputIndex The array index of the node output
|
||||
* @returns `true` if the origin matches, otherwise `false`.
|
||||
*/
|
||||
hasOrigin(nodeId: NodeId, outputIndex: number): boolean {
|
||||
hasOrigin(nodeId: LinkEndpointNodeId, outputIndex: number): boolean {
|
||||
return this.origin_id === nodeId && this.origin_slot === outputIndex
|
||||
}
|
||||
|
||||
@@ -383,7 +390,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
* @param inputIndex The array index of the node input
|
||||
* @returns `true` if the target matches, otherwise `false`.
|
||||
*/
|
||||
hasTarget(nodeId: NodeId, inputIndex: number): boolean {
|
||||
hasTarget(nodeId: SerializedNodeId, inputIndex: number): boolean {
|
||||
return this.target_id === nodeId && this.target_slot === inputIndex
|
||||
}
|
||||
|
||||
@@ -468,9 +475,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
serialize(): SerialisedLLinkArray {
|
||||
return [
|
||||
this.id,
|
||||
this.origin_id,
|
||||
serializeNodeId(this.origin_id),
|
||||
this.origin_slot,
|
||||
this.target_id,
|
||||
serializeNodeId(this.target_id),
|
||||
this.target_slot,
|
||||
this.type
|
||||
]
|
||||
@@ -479,9 +486,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
asSerialisable(): SerialisableLLink {
|
||||
const copy: SerialisableLLink = {
|
||||
id: this.id,
|
||||
origin_id: this.origin_id,
|
||||
origin_id: serializeNodeId(this.origin_id),
|
||||
origin_slot: this.origin_slot,
|
||||
target_id: this.target_id,
|
||||
target_id: serializeNodeId(this.target_id),
|
||||
target_slot: this.target_slot,
|
||||
type: this.type
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMuta
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
|
||||
import { LGraphBadge } from './LGraphBadge'
|
||||
import type { LGraphNode, NodeId } from './LGraphNode'
|
||||
import type { LGraphNode } from './LGraphNode'
|
||||
import { LLink } from './LLink'
|
||||
import type { LinkId } from './LLink'
|
||||
import type { LinkEndpointNodeId, LinkId } from './LLink'
|
||||
import type {
|
||||
CanvasColour,
|
||||
INodeInputSlot,
|
||||
@@ -182,7 +182,7 @@ export class Reroute
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get origin_id(): NodeId | undefined {
|
||||
get origin_id(): LinkEndpointNodeId | undefined {
|
||||
return this.firstLink?.origin_id
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ exports[`LGraph > supports schema v0.4 graphs > oldSchemaGraph 1`] = `
|
||||
"links": [],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"id": "1",
|
||||
"mode": 0,
|
||||
"pos": [
|
||||
10,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||
import {
|
||||
@@ -37,13 +38,13 @@ export class FloatingRenderLink implements RenderLink {
|
||||
readonly fromDirection: LinkDirection
|
||||
readonly fromSlotIndex: SlotIndex
|
||||
|
||||
readonly outputNodeId: NodeId = -1
|
||||
readonly outputNodeId: SerializedNodeId = -1
|
||||
readonly outputNode?: LGraphNode
|
||||
readonly outputSlot?: INodeOutputSlot
|
||||
readonly outputIndex: number = -1
|
||||
readonly outputPos?: Point
|
||||
|
||||
readonly inputNodeId: NodeId = -1
|
||||
readonly inputNodeId: SerializedNodeId = -1
|
||||
readonly inputNode?: LGraphNode
|
||||
readonly inputSlot?: INodeInputSlot
|
||||
readonly inputIndex: number = -1
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
LinkDirection
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import {
|
||||
createMockNodeInputSlot,
|
||||
createMockNodeOutputSlot
|
||||
@@ -74,7 +75,7 @@ const test = baseTest.extend<TestContext>({
|
||||
createTestNode: async ({ network }, use) => {
|
||||
await use((id: number): LGraphNode => {
|
||||
const node = new LGraphNode('test')
|
||||
node.id = id
|
||||
node.id = nodeId(id)
|
||||
network.add(node)
|
||||
return node
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ import { LGraphNode, LLink, LinkConnector } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test as baseTest } from '../__fixtures__/testExtensions'
|
||||
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import {
|
||||
createMockCanvasPointerEvent,
|
||||
createMockCanvasRenderingContext2D
|
||||
@@ -59,7 +60,7 @@ const test = baseTest.extend<TestContext>({
|
||||
createTestNode: async ({ graph }, use) => {
|
||||
await use((id): LGraphNode => {
|
||||
const node = new LGraphNode('test')
|
||||
node.id = id
|
||||
node.id = nodeId(id)
|
||||
graph.add(node)
|
||||
return node
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
NodeInputSlot
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
|
||||
import { createTestSubgraph } from '../subgraph/__fixtures__/subgraphHelpers'
|
||||
import {
|
||||
@@ -197,7 +198,7 @@ describe('LinkConnector SubgraphInput connection validation', () => {
|
||||
// Create a minimal valid setup
|
||||
const subgraph = createTestSubgraph()
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.id = 1
|
||||
node.id = nodeId(1)
|
||||
node.addInput('test_in', 'number')
|
||||
subgraph.add(node)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||
import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget'
|
||||
@@ -36,13 +37,13 @@ export abstract class MovingLinkBase implements RenderLink {
|
||||
abstract readonly fromDirection: LinkDirection
|
||||
abstract readonly fromSlotIndex: SlotIndex
|
||||
|
||||
readonly outputNodeId: NodeId
|
||||
readonly outputNodeId: SerializedNodeId
|
||||
readonly outputNode: LGraphNode
|
||||
readonly outputSlot: INodeOutputSlot
|
||||
readonly outputIndex: number
|
||||
readonly outputPos: Point
|
||||
|
||||
readonly inputNodeId: NodeId
|
||||
readonly inputNodeId: SerializedNodeId
|
||||
readonly inputNode: LGraphNode
|
||||
readonly inputSlot: INodeInputSlot
|
||||
readonly inputIndex: number
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
|
||||
/**
|
||||
* Subgraph constants
|
||||
*
|
||||
@@ -5,7 +7,7 @@
|
||||
*/
|
||||
|
||||
/** ID of the virtual input node of a subgraph. */
|
||||
export const SUBGRAPH_INPUT_ID = -10
|
||||
export const SUBGRAPH_INPUT_ID = nodeId(-10)
|
||||
|
||||
/** ID of the virtual output node of a subgraph. */
|
||||
export const SUBGRAPH_OUTPUT_ID = -20
|
||||
export const SUBGRAPH_OUTPUT_ID = nodeId(-20)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphButton } from '@/lib/litegraph/src/LGraphButton'
|
||||
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
@@ -57,6 +58,6 @@ export interface LGraphCanvasEventMap {
|
||||
/** Ghost placement mode has started or ended. */
|
||||
'litegraph:ghost-placement': {
|
||||
active: boolean
|
||||
nodeId: NodeId
|
||||
nodeId: SerializedNodeId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LLink, ResolvedConnection } from '@/lib/litegraph/src/LLink'
|
||||
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
@@ -59,21 +60,21 @@ export interface LGraphEventMap {
|
||||
}
|
||||
|
||||
'node:property:changed': {
|
||||
nodeId: NodeId
|
||||
nodeId: SerializedNodeId
|
||||
property: string
|
||||
oldValue: unknown
|
||||
newValue: unknown
|
||||
}
|
||||
'node:slot-errors:changed': { nodeId: NodeId }
|
||||
'node:slot-errors:changed': { nodeId: SerializedNodeId }
|
||||
'node:slot-links:changed': {
|
||||
nodeId: NodeId
|
||||
nodeId: SerializedNodeId
|
||||
slotType: NodeSlotType
|
||||
slotIndex: number
|
||||
connected: boolean
|
||||
linkId: number
|
||||
}
|
||||
'node:slot-label:changed': {
|
||||
nodeId: NodeId
|
||||
nodeId: SerializedNodeId
|
||||
slotType?: NodeSlotType
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import type { WidgetId } from '@/types/widgetId'
|
||||
import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
import type { ContextMenu } from './ContextMenu'
|
||||
import type { LGraphNode, NodeId, NodeProperty } from './LGraphNode'
|
||||
import type { LLink, LinkId } from './LLink'
|
||||
import type { LGraphNode, NodeProperty } from './LGraphNode'
|
||||
import type { NodeId, SerializedNodeId } from '@/types/nodeId'
|
||||
import type { LinkEndpointNodeId, LLink, LinkId } from './LLink'
|
||||
import type { Reroute, RerouteId } from './Reroute'
|
||||
import type { SubgraphInput } from './subgraph/SubgraphInput'
|
||||
import type { SubgraphInputNode } from './subgraph/SubgraphInputNode'
|
||||
@@ -162,7 +163,7 @@ export interface ReadonlyLinkNetwork {
|
||||
readonly links: ReadonlyMap<LinkId, LLink>
|
||||
readonly reroutes: ReadonlyMap<RerouteId, Reroute>
|
||||
readonly floatingLinks: ReadonlyMap<LinkId, LLink>
|
||||
getNodeById(id: NodeId | null | undefined): LGraphNode | null
|
||||
getNodeById(id: SerializedNodeId | null | undefined): LGraphNode | null
|
||||
getLink(id: null | undefined): undefined
|
||||
getLink(id: LinkId | null | undefined): LLink | undefined
|
||||
getReroute(parentId: null | undefined): undefined
|
||||
@@ -217,7 +218,7 @@ export interface LinkSegment {
|
||||
_dragging?: boolean
|
||||
|
||||
/** Output node ID */
|
||||
readonly origin_id: NodeId | undefined
|
||||
readonly origin_id: LinkEndpointNodeId | undefined
|
||||
/** Output slot index */
|
||||
readonly origin_slot: SlotIndex | undefined
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LGraphNode, NodeId } from './LGraphNode'
|
||||
import type { LGraphNode } from './LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { LLink, LinkId } from './LLink'
|
||||
|
||||
/** Generates a unique string key for a link's connection tuple. */
|
||||
@@ -43,7 +44,7 @@ export function purgeOrphanedLinks(
|
||||
ids: LinkId[],
|
||||
keepId: LinkId,
|
||||
links: Map<LinkId, LLink>,
|
||||
getNodeById: (id: NodeId) => LGraphNode | null
|
||||
getNodeById: (id: SerializedNodeId) => LGraphNode | null
|
||||
): void {
|
||||
for (const id of ids) {
|
||||
if (id === keepId) continue
|
||||
|
||||
@@ -3,11 +3,12 @@ import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import {
|
||||
ExecutableNodeDTO,
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LGraphEventMode,
|
||||
ExecutableNodeDTO
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
|
||||
import {
|
||||
createNestedSubgraphs,
|
||||
@@ -41,7 +42,7 @@ describe('ExecutableNodeDTO Creation', () => {
|
||||
it('should create DTO with subgraph path', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('Inner Node')
|
||||
node.id = 42
|
||||
node.id = nodeId(42)
|
||||
graph.add(node)
|
||||
const subgraphPath = ['10', '20'] as const
|
||||
|
||||
@@ -115,7 +116,7 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
|
||||
it('should generate simple ID for root node', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('Root Node')
|
||||
node.id = 5
|
||||
node.id = nodeId(5)
|
||||
graph.add(node)
|
||||
|
||||
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
|
||||
@@ -126,7 +127,7 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
|
||||
it('should generate path-based ID for nested node', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('Nested Node')
|
||||
node.id = 3
|
||||
node.id = nodeId(3)
|
||||
graph.add(node)
|
||||
const path = ['1', '2'] as const
|
||||
|
||||
@@ -138,7 +139,7 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
|
||||
it('should handle deep nesting paths', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('Deep Node')
|
||||
node.id = 99
|
||||
node.id = nodeId(99)
|
||||
graph.add(node)
|
||||
const path = ['1', '2', '3', '4', '5'] as const
|
||||
|
||||
@@ -150,11 +151,11 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
|
||||
it('should handle string and number IDs consistently', () => {
|
||||
const graph = new LGraph()
|
||||
const node1 = new LGraphNode('Node 1')
|
||||
node1.id = 10
|
||||
node1.id = nodeId(10)
|
||||
graph.add(node1)
|
||||
|
||||
const node2 = new LGraphNode('Node 2')
|
||||
node2.id = 20
|
||||
node2.id = nodeId(20)
|
||||
graph.add(node2)
|
||||
|
||||
const dto1 = new ExecutableNodeDTO(node1, ['5'], new Map(), undefined)
|
||||
@@ -487,7 +488,7 @@ describe('ExecutableNodeDTO Properties', () => {
|
||||
it('should provide access to basic properties', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('Test Node')
|
||||
node.id = 42
|
||||
node.id = nodeId(42)
|
||||
node.addInput('input', 'number')
|
||||
node.addOutput('output', 'string')
|
||||
graph.add(node)
|
||||
@@ -549,7 +550,7 @@ describe('ExecutableNodeDTO Memory Efficiency', () => {
|
||||
// Create DTOs
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const node = new LGraphNode(`Node ${i}`)
|
||||
node.id = i
|
||||
node.id = nodeId(i)
|
||||
graph.add(node)
|
||||
const dto = new ExecutableNodeDTO(node, ['parent'], new Map(), undefined)
|
||||
nodes.push(dto)
|
||||
@@ -619,7 +620,7 @@ describe('ExecutableNodeDTO Integration', () => {
|
||||
it('should preserve original node properties through DTO', () => {
|
||||
const graph = new LGraph()
|
||||
const originalNode = new LGraphNode('Original')
|
||||
originalNode.id = 123
|
||||
originalNode.id = nodeId(123)
|
||||
originalNode.addInput('test', 'number')
|
||||
originalNode.properties = { value: 42 }
|
||||
graph.add(originalNode)
|
||||
@@ -632,7 +633,7 @@ describe('ExecutableNodeDTO Integration', () => {
|
||||
)
|
||||
|
||||
// DTO should provide access to original node properties
|
||||
expect(dto.node.id).toBe(123)
|
||||
expect(Number(dto.node.id)).toBe(123)
|
||||
expect(dto.node.inputs).toHaveLength(1)
|
||||
expect(dto.node.properties.value).toBe(42)
|
||||
|
||||
@@ -644,7 +645,7 @@ describe('ExecutableNodeDTO Integration', () => {
|
||||
const subgraph = createTestSubgraph({ nodeCount: 1 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 99 })
|
||||
const innerNode = subgraph.nodes[0]
|
||||
innerNode.id = 55
|
||||
innerNode.id = nodeId(55)
|
||||
|
||||
const dto = new ExecutableNodeDTO(
|
||||
innerNode,
|
||||
@@ -655,8 +656,8 @@ describe('ExecutableNodeDTO Integration', () => {
|
||||
|
||||
// DTO provides execution context
|
||||
expect(dto.id).toBe('99:55') // Path-based execution ID
|
||||
expect(dto.node.id).toBe(55) // Original node ID preserved
|
||||
expect(dto.subgraphNode?.id).toBe(99) // Subgraph context
|
||||
expect(Number(dto.node.id)).toBe(55) // Original node ID preserved
|
||||
expect(Number(dto.subgraphNode?.id)).toBe(99) // Subgraph context
|
||||
})
|
||||
})
|
||||
|
||||
@@ -669,7 +670,7 @@ describe('ExecutableNodeDTO Scale Testing', () => {
|
||||
// Create DTOs to test performance
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const node = new LGraphNode(`Node ${i}`)
|
||||
node.id = i
|
||||
node.id = nodeId(i)
|
||||
node.addInput('in', 'number')
|
||||
graph.add(node)
|
||||
|
||||
@@ -692,7 +693,7 @@ describe('ExecutableNodeDTO Scale Testing', () => {
|
||||
it('should handle complex path generation correctly', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('Deep Node')
|
||||
node.id = 999
|
||||
node.id = nodeId(999)
|
||||
graph.add(node)
|
||||
|
||||
// Test deterministic path generation behavior
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { LinkId } from '@/lib/litegraph/src/LLink'
|
||||
import { InvalidLinkError } from '@/lib/litegraph/src/infrastructure/InvalidLinkError'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
@@ -99,7 +100,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
|
||||
/** The actual node that this DTO wraps. */
|
||||
readonly node: LGraphNode | SubgraphNode,
|
||||
/** A list of subgraph instance node IDs from the root graph to the containing instance. @see {@link id} */
|
||||
readonly subgraphNodePath: readonly NodeId[],
|
||||
readonly subgraphNodePath: readonly SerializedNodeId[],
|
||||
/** A flattened map of all DTOs in this node network. Subgraph instances have been expanded into their inner nodes. */
|
||||
readonly nodesByExecutionId: Map<ExecutionId, ExecutableLGraphNode>,
|
||||
/** The actual subgraph instance that contains this node, otherwise undefined. */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { serializeNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
||||
import type {
|
||||
@@ -368,7 +369,7 @@ export abstract class SubgraphIONodeBase<
|
||||
|
||||
asSerialisable(): ExportedSubgraphIONode {
|
||||
return {
|
||||
id: this.id,
|
||||
id: serializeNodeId(this.id),
|
||||
bounding: this.boundingRect.export(),
|
||||
pinned: this.pinned ? true : undefined
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
||||
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
|
||||
@@ -13,13 +13,11 @@ import type {
|
||||
IWidgetLocator
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
ISlotType,
|
||||
NodeId
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { INodeInputSlot, ISlotType } from '@/lib/litegraph/src/litegraph'
|
||||
import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot'
|
||||
import { NodeOutputSlot } from '@/lib/litegraph/src/node/NodeOutputSlot'
|
||||
import { nodeId as toNodeId } from '@/types/nodeId'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type {
|
||||
GraphOrSubgraph,
|
||||
Subgraph
|
||||
@@ -737,7 +735,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
|
||||
const newLink = LLink.create(innerLink)
|
||||
newLink.origin_id = `${this.id}:${innerLink.origin_id}`
|
||||
newLink.origin_id = toNodeId(`${this.id}:${innerLink.origin_id}`)
|
||||
newLink.origin_slot = innerLink.origin_slot
|
||||
|
||||
return newLink
|
||||
@@ -778,7 +776,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
override getInnerNodes(
|
||||
executableNodes: Map<ExecutionId, ExecutableLGraphNode>,
|
||||
subgraphNodePath: readonly NodeId[] = [],
|
||||
subgraphNodePath: readonly SerializedNodeId[] = [],
|
||||
nodes: ExecutableLGraphNode[] = [],
|
||||
visited = new Set<SubgraphNode>()
|
||||
): ExecutableLGraphNode[] {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
||||
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
|
||||
@@ -492,16 +492,19 @@ describe('SubgraphSerialization - Data Integrity', () => {
|
||||
graph.configure(structuredClone(duplicateSubgraphNodeIds))
|
||||
|
||||
const rootIds = graph.nodes
|
||||
.map((node) => node.id)
|
||||
.filter((id): id is number => typeof id === 'number')
|
||||
.map((node) => Number(node.id))
|
||||
.sort((a, b) => a - b)
|
||||
expect(rootIds).toEqual([102, 103])
|
||||
|
||||
const subgraphAIds = new Set(
|
||||
graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_A)!.nodes.map((node) => node.id)
|
||||
graph.subgraphs
|
||||
.get(DUPLICATE_ID_SUBGRAPH_A)!
|
||||
.nodes.map((node) => Number(node.id))
|
||||
)
|
||||
const subgraphBIds = new Set(
|
||||
graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_B)!.nodes.map((node) => node.id)
|
||||
graph.subgraphs
|
||||
.get(DUPLICATE_ID_SUBGRAPH_B)!
|
||||
.nodes.map((node) => Number(node.id))
|
||||
)
|
||||
|
||||
expect(subgraphAIds).toEqual(new Set([3, 8, 37]))
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
createTestSubgraphNode,
|
||||
resetSubgraphFixtureState
|
||||
} from './__fixtures__/subgraphHelpers'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
@@ -132,7 +133,7 @@ describe('Subgraph slot connections', () => {
|
||||
|
||||
// Create a node inside the subgraph
|
||||
const internalNode = new LGraphNode('InternalNode')
|
||||
internalNode.id = 100
|
||||
internalNode.id = nodeId(100)
|
||||
internalNode.addInput('in', 'number')
|
||||
subgraph.add(internalNode)
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { SerializedProxyWidgetTuple } from '@/core/schemas/promotionSchema'
|
||||
import { IS_CONTROL_WIDGET } from '@/scripts/controlWidgetMarker'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import type { WidgetId } from '@/types/widgetId'
|
||||
import { widgetId } from '@/types/widgetId'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
@@ -1130,13 +1131,24 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
|
||||
describe('previewExposures round-trip', () => {
|
||||
const CANVAS = '$$canvas-image-preview'
|
||||
const exposure12 = { sourceNodeId: '12', sourcePreviewName: CANVAS }
|
||||
const exposure12 = {
|
||||
sourceNodeId: nodeId('12'),
|
||||
sourcePreviewName: CANVAS
|
||||
}
|
||||
const exposure14 = {
|
||||
sourceNodeId: nodeId('14'),
|
||||
sourcePreviewName: 'videopreview'
|
||||
}
|
||||
const serializedExposure12 = {
|
||||
sourceNodeId: '12',
|
||||
sourcePreviewName: CANVAS
|
||||
}
|
||||
const serializedExposure14 = {
|
||||
sourceNodeId: '14',
|
||||
sourcePreviewName: 'videopreview'
|
||||
}
|
||||
const named12 = { name: CANVAS, ...exposure12 }
|
||||
const named14 = { name: 'videopreview', ...exposure14 }
|
||||
const named12 = { name: CANVAS, ...serializedExposure12 }
|
||||
const named14 = { name: 'videopreview', ...serializedExposure14 }
|
||||
|
||||
it('hydrates previewExposures into the store during configure', () => {
|
||||
const hostNode = createTestSubgraphNode(createTestSubgraph())
|
||||
|
||||
@@ -11,9 +11,9 @@ import type {
|
||||
ExportedSubgraph,
|
||||
ExportedSubgraphInstance,
|
||||
ISlotType,
|
||||
NodeId,
|
||||
UUID
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import {
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
@@ -77,7 +77,7 @@ interface TestSubgraphOptions {
|
||||
|
||||
interface TestSubgraphNodeOptions {
|
||||
parentGraph?: LGraph | Subgraph
|
||||
id?: NodeId
|
||||
id?: SerializedNodeId
|
||||
pos?: [number, number]
|
||||
size?: [number, number]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LGraphState } from '../LGraph'
|
||||
import type { NodeId } from '../LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type {
|
||||
ExportedSubgraph,
|
||||
ExposedWidget,
|
||||
@@ -31,7 +31,10 @@ export function deduplicateSubgraphNodeIds(
|
||||
|
||||
const usedNodeIds = new Set(reservedNodeIds)
|
||||
const subgraphIdSet = new Set(clonedSubgraphs.map((sg) => sg.id))
|
||||
const remapBySubgraph = new Map<string, Map<NodeId, NodeId>>()
|
||||
const remapBySubgraph = new Map<
|
||||
string,
|
||||
Map<SerializedNodeId, SerializedNodeId>
|
||||
>()
|
||||
|
||||
for (const subgraph of clonedSubgraphs) {
|
||||
const remappedIds = remapNodeIds(subgraph.nodes ?? [], usedNodeIds, state)
|
||||
@@ -64,8 +67,8 @@ function remapNodeIds(
|
||||
nodes: ISerialisedNode[],
|
||||
usedNodeIds: Set<number>,
|
||||
state: LGraphState
|
||||
): Map<NodeId, NodeId> {
|
||||
const remappedIds = new Map<NodeId, NodeId>()
|
||||
): Map<SerializedNodeId, SerializedNodeId> {
|
||||
const remappedIds = new Map<SerializedNodeId, SerializedNodeId>()
|
||||
|
||||
for (const node of nodes) {
|
||||
const id = node.id
|
||||
@@ -95,21 +98,21 @@ function remapNodeIds(
|
||||
function findNextAvailableId(
|
||||
usedNodeIds: Set<number>,
|
||||
state: LGraphState
|
||||
): NodeId {
|
||||
): SerializedNodeId {
|
||||
while (true) {
|
||||
const nextId = state.lastNodeId + 1
|
||||
if (nextId > MAX_NODE_ID) {
|
||||
throw new Error('Node ID space exhausted')
|
||||
}
|
||||
state.lastNodeId = nextId
|
||||
if (!usedNodeIds.has(nextId)) return nextId as NodeId
|
||||
if (!usedNodeIds.has(nextId)) return nextId
|
||||
}
|
||||
}
|
||||
|
||||
/** Patches origin_id / target_id in serialized links. */
|
||||
function patchSerialisedLinks(
|
||||
links: SerialisableLLink[],
|
||||
remappedIds: Map<NodeId, NodeId>
|
||||
remappedIds: Map<SerializedNodeId, SerializedNodeId>
|
||||
): void {
|
||||
for (const link of links) {
|
||||
const newOrigin = remappedIds.get(link.origin_id)
|
||||
@@ -123,7 +126,7 @@ function patchSerialisedLinks(
|
||||
/** Patches promoted widget node references. */
|
||||
function patchPromotedWidgets(
|
||||
widgets: ExposedWidget[],
|
||||
remappedIds: Map<NodeId, NodeId>
|
||||
remappedIds: Map<SerializedNodeId, SerializedNodeId>
|
||||
): void {
|
||||
for (const widget of widgets) {
|
||||
const newId = remappedIds.get(widget.id)
|
||||
@@ -192,7 +195,7 @@ export function topologicalSortSubgraphs(
|
||||
function patchProxyWidgets(
|
||||
rootNodes: ISerialisedNode[],
|
||||
subgraphIdSet: Set<string>,
|
||||
remapBySubgraph: Map<string, Map<NodeId, NodeId>>
|
||||
remapBySubgraph: Map<string, Map<SerializedNodeId, SerializedNodeId>>
|
||||
): void {
|
||||
for (const node of rootNodes) {
|
||||
if (!subgraphIdSet.has(node.type)) continue
|
||||
@@ -204,7 +207,7 @@ function patchProxyWidgets(
|
||||
|
||||
for (const entry of proxyWidgets) {
|
||||
if (!Array.isArray(entry)) continue
|
||||
const oldId = Number(entry[0]) as NodeId
|
||||
const oldId = Number(entry[0])
|
||||
const newId = remappedIds.get(oldId)
|
||||
if (newId !== undefined) entry[0] = String(newId)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
import type { SubgraphIO } from '@/lib/litegraph/src/types/serialisation'
|
||||
|
||||
export interface NodeLike {
|
||||
id: NodeId
|
||||
id: SerializedNodeId
|
||||
|
||||
canConnectTo(
|
||||
node: NodeLike,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { NodeId } from '../LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { NodeSlotType } from './globalEnums'
|
||||
|
||||
interface NodePropertyChangedEvent {
|
||||
type: 'node:property:changed'
|
||||
nodeId: NodeId
|
||||
nodeId: SerializedNodeId
|
||||
property: string
|
||||
oldValue: unknown
|
||||
newValue: unknown
|
||||
@@ -11,12 +11,12 @@ interface NodePropertyChangedEvent {
|
||||
|
||||
interface NodeSlotErrorsChangedEvent {
|
||||
type: 'node:slot-errors:changed'
|
||||
nodeId: NodeId
|
||||
nodeId: SerializedNodeId
|
||||
}
|
||||
|
||||
interface NodeSlotLinksChangedEvent {
|
||||
type: 'node:slot-links:changed'
|
||||
nodeId: NodeId
|
||||
nodeId: SerializedNodeId
|
||||
slotType: NodeSlotType
|
||||
slotIndex: number
|
||||
connected: boolean
|
||||
@@ -25,7 +25,7 @@ interface NodeSlotLinksChangedEvent {
|
||||
|
||||
interface NodeSlotLabelChangedEvent {
|
||||
type: 'node:slot-label:changed'
|
||||
nodeId: NodeId
|
||||
nodeId: SerializedNodeId
|
||||
slotType?: NodeSlotType
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import type {
|
||||
SubgraphId
|
||||
} from '../LGraph'
|
||||
import type { GroupId, IGraphGroupFlags } from '../LGraphGroup'
|
||||
import type { NodeId, NodeProperty } from '../LGraphNode'
|
||||
import type { NodeProperty } from '../LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { LinkId, SerialisedLLinkArray } from '../LLink'
|
||||
import type { FloatingRerouteSlot, RerouteId } from '../Reroute'
|
||||
import type {
|
||||
@@ -79,7 +80,7 @@ export type ISerialisableNodeOutput = Omit<
|
||||
/** Serialised LGraphNode */
|
||||
export interface ISerialisedNode {
|
||||
title?: string
|
||||
id: NodeId
|
||||
id: SerializedNodeId
|
||||
type: string
|
||||
pos: Point
|
||||
size: Size
|
||||
@@ -125,7 +126,7 @@ export interface ExportedSubgraphInstance extends NodeSubgraphSharedProps {
|
||||
* Maintained for backwards compat
|
||||
*/
|
||||
export interface ISerialisedGraph extends BaseExportedGraph {
|
||||
last_node_id: NodeId
|
||||
last_node_id: SerializedNodeId
|
||||
last_link_id: LinkId
|
||||
nodes: ISerialisedNode[]
|
||||
links: SerialisedLLinkArray[]
|
||||
@@ -175,7 +176,7 @@ export interface SubgraphIO extends SubgraphIOShared {
|
||||
/** A reference to a node widget shown in the parent graph */
|
||||
export interface ExposedWidget {
|
||||
/** The ID of the node (inside the subgraph) that the widget belongs to. */
|
||||
id: NodeId
|
||||
id: SerializedNodeId
|
||||
/** The name of the widget to show in the parent graph. */
|
||||
name: string
|
||||
}
|
||||
@@ -211,11 +212,11 @@ export interface SerialisableLLink {
|
||||
/** Link ID */
|
||||
id: LinkId
|
||||
/** Output node ID */
|
||||
origin_id: NodeId
|
||||
origin_id: SerializedNodeId
|
||||
/** Output slot index */
|
||||
origin_slot: number
|
||||
/** Input node ID */
|
||||
target_id: NodeId
|
||||
target_id: SerializedNodeId
|
||||
/** Input slot index */
|
||||
target_slot: number
|
||||
/** Data type of the link */
|
||||
@@ -225,7 +226,7 @@ export interface SerialisableLLink {
|
||||
}
|
||||
|
||||
export interface ExportedSubgraphIONode {
|
||||
id: NodeId
|
||||
id: SerializedNodeId
|
||||
bounding: [number, number, number, number]
|
||||
pinned?: boolean
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
import type { CurveData } from '@/components/curve/types'
|
||||
import type { BoundingBox } from '@/types/boundingBoxes'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type { WidgetId } from '@/types/widgetId'
|
||||
|
||||
import type {
|
||||
@@ -10,16 +11,11 @@ import type {
|
||||
RequiredProps,
|
||||
Size
|
||||
} from '../interfaces'
|
||||
import type {
|
||||
CanvasPointer,
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
NodeId
|
||||
} from '../litegraph'
|
||||
import type { CanvasPointer, LGraphCanvas, LGraphNode } from '../litegraph'
|
||||
import type { CanvasPointerEvent } from './events'
|
||||
|
||||
export interface NodeBindable {
|
||||
setNodeId(nodeId: NodeId): void
|
||||
setNodeId(nodeId: SerializedNodeId): void
|
||||
}
|
||||
|
||||
export interface IWidgetOptions<TValues = unknown> {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { NumberWidget } from '@/lib/litegraph/src/widgets/NumberWidget'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { nodeId } from '@/types/nodeId'
|
||||
import { widgetId } from '@/types/widgetId'
|
||||
|
||||
function createTestWidget(
|
||||
@@ -36,7 +37,7 @@ describe('BaseWidget store integration', () => {
|
||||
store = useWidgetValueStore()
|
||||
graph = new LGraph()
|
||||
node = new LGraphNode('TestNode')
|
||||
node.id = 1
|
||||
node.id = nodeId(1)
|
||||
graph.add(node)
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { drawTextInArea } from '@/lib/litegraph/src/draw'
|
||||
import { cachedMeasureText } from '@/lib/litegraph/src/utils/textMeasureCache'
|
||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
||||
import type { Point } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { SerializedNodeId } from '@/types/nodeId'
|
||||
import type {
|
||||
CanvasPointer,
|
||||
LGraphCanvas,
|
||||
@@ -143,7 +143,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
* Associates this widget with a node ID and registers it in the WidgetValueStore.
|
||||
* Once set, value reads/writes will be delegated to the store.
|
||||
*/
|
||||
setNodeId(nodeId: NodeId): void {
|
||||
setNodeId(nodeId: SerializedNodeId): void {
|
||||
const graphId = this.node.graph?.rootGraph.id
|
||||
if (!graphId) return
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user