mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-06-25 01:04:58 +00:00
Compare commits
25 Commits
codex/cove
...
drjkl/now-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
488406846d | ||
|
|
bfc21fbb72 | ||
|
|
cb53ea1e6a | ||
|
|
7cdbd5af6e | ||
|
|
53b08c5027 | ||
|
|
1336524260 | ||
|
|
77351e6bcd | ||
|
|
d9a9fb009c | ||
|
|
7aed2c9e32 | ||
|
|
0eb948ec00 | ||
|
|
159815d37e | ||
|
|
938584122c | ||
|
|
e39f93d22b | ||
|
|
dc9b1db3e7 | ||
|
|
dcea64e420 | ||
|
|
aa25c3f92e | ||
|
|
4dcd0fc7ff | ||
|
|
974975c722 | ||
|
|
c567acbcb2 | ||
|
|
9581c75680 | ||
|
|
8e49de8398 | ||
|
|
51759157d8 | ||
|
|
d7435f038d | ||
|
|
153a790781 | ||
|
|
d23a594de1 |
@@ -5,10 +5,9 @@ import type {
|
||||
LGraph,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
ComfyWorkflowJSON,
|
||||
NodeId
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId, NodeIdInput } from '@/types/nodeId'
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import { DefaultGraphPositions } from '@e2e/fixtures/constants/defaultGraphPositions'
|
||||
import type { Position, Size } from '@e2e/fixtures/types'
|
||||
@@ -42,11 +41,12 @@ export class NodeOperationsHelper {
|
||||
}
|
||||
|
||||
async getSelectedNodeIds(): Promise<NodeId[]> {
|
||||
return await this.page.evaluate(() => {
|
||||
const ids = await this.page.evaluate(() => {
|
||||
const selected = window.app?.canvas?.selected_nodes
|
||||
if (!selected) return []
|
||||
return Object.keys(selected).map(Number)
|
||||
return Object.keys(selected)
|
||||
})
|
||||
return ids.map(asNodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,7 +114,7 @@ export class NodeOperationsHelper {
|
||||
return this.getNodeRefById(id)
|
||||
}
|
||||
|
||||
async getNodeRefById(id: NodeId): Promise<NodeReference> {
|
||||
async getNodeRefById(id: NodeIdInput): Promise<NodeReference> {
|
||||
return new NodeReference(id, this.comfyPage)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { SerialisableLLink } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId, NodeIdInput } from '@/types/nodeId'
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import type { Position, Size } from '@e2e/fixtures/types'
|
||||
import { VueNodeFixture } from '@e2e/fixtures/utils/vueNodeFixtures'
|
||||
@@ -298,10 +299,13 @@ class NodeWidgetReference {
|
||||
}
|
||||
}
|
||||
export class NodeReference {
|
||||
readonly id: NodeId
|
||||
constructor(
|
||||
readonly id: NodeId,
|
||||
id: NodeIdInput,
|
||||
readonly comfyPage: ComfyPage
|
||||
) {}
|
||||
) {
|
||||
this.id = asNodeId(id)
|
||||
}
|
||||
async exists(): Promise<boolean> {
|
||||
return await this.comfyPage.page.evaluate((id) => {
|
||||
const node = window.app!.canvas.graph!.getNodeById(id)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mergeTests } from '@playwright/test'
|
||||
|
||||
import type { NodeError } from '@/schemas/apiSchema'
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture
|
||||
@@ -12,7 +13,7 @@ import { webSocketFixture } from '@e2e/fixtures/ws'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
const VALIDATION_ERROR_NODE_ID = '1'
|
||||
const VALIDATION_ERROR_NODE_ID = asNodeId('1')
|
||||
const VALIDATION_ERROR_MESSAGE = 'Required input is missing: source'
|
||||
const PARTIAL_EXECUTION_ROOT_NODE_IDS = ['1', '4']
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ test.describe('Preview as Text node', () => {
|
||||
})
|
||||
|
||||
const previewEntry = Object.values(apiWorkflow).find(
|
||||
(n) => n.class_type === 'PreviewAny'
|
||||
(n) => n?.class_type === 'PreviewAny'
|
||||
)
|
||||
expect(previewEntry).toBeDefined()
|
||||
|
||||
|
||||
@@ -3,16 +3,17 @@ import {
|
||||
comfyPageFixture as test
|
||||
} from '@e2e/fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { Position } from '@e2e/fixtures/types'
|
||||
|
||||
type NodeSnapshot = { id: number } & Position
|
||||
type NodeSnapshot = { id: NodeId } & Position
|
||||
|
||||
async function getAllNodePositions(
|
||||
comfyPage: ComfyPage
|
||||
): Promise<NodeSnapshot[]> {
|
||||
return comfyPage.page.evaluate(() =>
|
||||
window.app!.graph.nodes.map((n) => ({
|
||||
id: n.id as number,
|
||||
id: n.id,
|
||||
x: n.pos[0],
|
||||
y: n.pos[1]
|
||||
}))
|
||||
@@ -21,7 +22,7 @@ async function getAllNodePositions(
|
||||
|
||||
async function getNodePosition(
|
||||
comfyPage: ComfyPage,
|
||||
nodeId: number
|
||||
nodeId: NodeId
|
||||
): Promise<Position | undefined> {
|
||||
return comfyPage.page.evaluate((targetNodeId) => {
|
||||
const node = window.app!.graph.nodes.find((n) => n.id === targetNodeId)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
import { createMockJob } from '@e2e/fixtures/helpers/AssetsHelper'
|
||||
import type { JobDetail } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
|
||||
/**
|
||||
* Expanded folder view must drop output records that resolve to the same
|
||||
@@ -12,7 +13,7 @@ import type { JobDetail } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
*/
|
||||
|
||||
const STACK_JOB_ID = 'job-output-dedupe'
|
||||
const COVER_NODE_ID = '9'
|
||||
const COVER_NODE_ID = asNodeExecutionId('9')
|
||||
const COVER_FILENAME = 'cover_00001_.png'
|
||||
const DUPLICATE_FILENAME = 'duplicate_00002_.png'
|
||||
const DISTINCT_FILENAMES = ['distinct_00003_.png', 'distinct_00004_.png']
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
JobDetail,
|
||||
RawJobListItem
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
|
||||
// Legacy coverage backed by AssetsHelper's shadow backend. New assets-sidebar
|
||||
// browser coverage should use typed route mocks in assetsSidebarTab.spec.ts.
|
||||
@@ -71,7 +72,7 @@ const SAMPLE_IMPORTED_FILES = [
|
||||
const JOB_GAMMA_DETAIL: JobDetail = {
|
||||
...SAMPLE_JOBS[2],
|
||||
outputs: {
|
||||
'3': {
|
||||
[asNodeId('3')]: {
|
||||
images: [
|
||||
{
|
||||
filename: 'abstract_art.png',
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
JobDetail,
|
||||
RawJobListItem
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
|
||||
const test = mergeTests(comfyPageFixture, jobsRouteFixture)
|
||||
|
||||
@@ -72,7 +73,7 @@ const multiOutputJob = createRouteMockJob({
|
||||
const multiOutputJobDetail: JobDetail = {
|
||||
...multiOutputJob,
|
||||
outputs: {
|
||||
'3': {
|
||||
[asNodeId('3')]: {
|
||||
images: [
|
||||
{
|
||||
filename: 'multi-output-a.png',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
interface SubgraphNodePosition {
|
||||
|
||||
@@ -568,10 +568,7 @@ test.describe('Subgraph Serialization', { tag: ['@subgraph'] }, () => {
|
||||
const result = await comfyPage.page.evaluate(() => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
const allGraphs = [graph, ...graph.subgraphs.values()]
|
||||
const allIds = allGraphs
|
||||
.flatMap((g) => g._nodes)
|
||||
.map((n) => n.id)
|
||||
.filter((id): id is number => typeof id === 'number')
|
||||
const allIds = allGraphs.flatMap((g) => g._nodes).map((n) => n.id)
|
||||
|
||||
return { allIds, uniqueCount: new Set(allIds).size }
|
||||
})
|
||||
@@ -589,8 +586,7 @@ test.describe('Subgraph Serialization', { tag: ['@subgraph'] }, () => {
|
||||
const graph = window.app!.canvas.graph!
|
||||
return graph._nodes
|
||||
.map((n) => n.id)
|
||||
.filter((id): id is number => typeof id === 'number')
|
||||
.sort((a, b) => a - b)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
})
|
||||
|
||||
expect(rootIds).toEqual([1, 2, 5])
|
||||
@@ -633,9 +629,9 @@ test.describe('Subgraph Serialization', { tag: ['@subgraph'] }, () => {
|
||||
)
|
||||
]
|
||||
|
||||
const SENTINEL_IDS = new Set([-1, -10, -20])
|
||||
const isSentinelNodeId = (id: number | string): id is number =>
|
||||
typeof id === 'number' && SENTINEL_IDS.has(id)
|
||||
const SENTINEL_IDS = new Set(['-1', '-10', '-20'])
|
||||
const isSentinelNodeId = (id: number | string): boolean =>
|
||||
SENTINEL_IDS.has(String(id))
|
||||
|
||||
const checkEndpoint = (
|
||||
label: string,
|
||||
@@ -644,7 +640,7 @@ test.describe('Subgraph Serialization', { tag: ['@subgraph'] }, () => {
|
||||
g: typeof graph
|
||||
): string | null => {
|
||||
if (isSentinelNodeId(id)) return null
|
||||
if (typeof id !== 'number' || !g._nodes_by_id[id]) {
|
||||
if (!g.getNodeById(id)) {
|
||||
return `${label}: ${kind} ${id} invalid or not found`
|
||||
}
|
||||
return null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { Page, Request } from '@playwright/test'
|
||||
|
||||
import type {
|
||||
ComfyApiWorkflow,
|
||||
NodeId
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ComfyApiWorkflow } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
@@ -146,7 +144,7 @@ test.describe('Workflow settings', { tag: '@canvas' }, () => {
|
||||
)
|
||||
}
|
||||
|
||||
function ascendingById(ids: NodeId[]): NodeId[] {
|
||||
function ascendingById<T extends string | number>(ids: T[]): T[] {
|
||||
return [...ids].sort((a, b) => Number(a) - Number(b))
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,13 @@ import type {
|
||||
RawJobListItem,
|
||||
zJobsListResponse
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
|
||||
type JobsListResponse = z.infer<typeof zJobsListResponse>
|
||||
|
||||
const test = mergeTests(comfyPageFixture, webSocketFixture)
|
||||
|
||||
const KSAMPLER_NODE = '3'
|
||||
const KSAMPLER_NODE = asNodeExecutionId('3')
|
||||
const EXECUTING_CLASS = /outline-node-stroke-executing/
|
||||
|
||||
const QUEUE_ROUTE = /\/api\/jobs\?[^/]*status=in_progress,pending/
|
||||
|
||||
@@ -36,34 +36,34 @@ Adopt an Entity Component System architecture for the graph domain model. This A
|
||||
|
||||
Six entity kinds, each with a branded ID type:
|
||||
|
||||
| Entity Kind | Current Class(es) | Current ID | Branded ID |
|
||||
| ----------- | ------------------------------------------------- | --------------------------- | ----------------- |
|
||||
| Node | `LGraphNode` | `NodeId = number \| string` | `NodeEntityId` |
|
||||
| Link | `LLink` | `LinkId = number` | `LinkEntityId` |
|
||||
| Widget | `BaseWidget` subclasses (25+) | name + parent node | `WidgetEntityId` |
|
||||
| Slot | `SlotBase` / `INodeInputSlot` / `INodeOutputSlot` | index on parent node | `SlotEntityId` |
|
||||
| Reroute | `Reroute` | `RerouteId = number` | `RerouteEntityId` |
|
||||
| Group | `LGraphGroup` | `number` | `GroupEntityId` |
|
||||
| Entity Kind | Current Class(es) | Current ID | Branded ID |
|
||||
| ----------- | ------------------------------------------------- | --------------------------- | ----------- |
|
||||
| Node | `LGraphNode` | `NodeId = number \| string` | `NodeId` |
|
||||
| Link | `LLink` | `LinkId = number` | `LinkId` |
|
||||
| Widget | `BaseWidget` subclasses (25+) | name + parent node | `WidgetId` |
|
||||
| Slot | `SlotBase` / `INodeInputSlot` / `INodeOutputSlot` | index on parent node | `SlotId` |
|
||||
| Reroute | `Reroute` | `RerouteId = number` | `RerouteId` |
|
||||
| Group | `LGraphGroup` | `number` | `GroupId` |
|
||||
|
||||
Subgraphs are not a separate entity kind. A subgraph is a node with a `SubgraphStructure` component. See [Subgraph Boundaries and Widget Promotion](../architecture/subgraph-boundaries-and-promotion.md) for the full design rationale.
|
||||
|
||||
### Branded ID Design
|
||||
|
||||
Each entity kind gets a nominal/branded type wrapping its underlying primitive. The brand prevents accidental cross-kind usage at compile time while remaining structurally compatible with existing ID types:
|
||||
Each entity kind gets a nominal/branded type wrapping its underlying primitive, and the brand prevents accidental cross-kind usage at compile time. `NodeId` is a branded `number`: node ids are normalized from legacy `number | string` values to `number` at load/serialization boundaries (via `asNodeId`, which rejects non-decimal-integer strings). `WidgetId` is a branded `string` composite path. The remaining ids keep `number` as their underlying primitive:
|
||||
|
||||
```ts
|
||||
type NodeEntityId = number & { readonly __brand: 'NodeEntityId' }
|
||||
type LinkEntityId = number & { readonly __brand: 'LinkEntityId' }
|
||||
type WidgetEntityId = number & { readonly __brand: 'WidgetEntityId' }
|
||||
type SlotEntityId = number & { readonly __brand: 'SlotEntityId' }
|
||||
type RerouteEntityId = number & { readonly __brand: 'RerouteEntityId' }
|
||||
type GroupEntityId = number & { readonly __brand: 'GroupEntityId' }
|
||||
type NodeId = number & { readonly __brand: 'NodeId' }
|
||||
type LinkId = number & { readonly __brand: 'LinkId' }
|
||||
type WidgetId = string & { readonly __brand: 'WidgetId' }
|
||||
type SlotId = number & { readonly __brand: 'SlotId' }
|
||||
type RerouteId = number & { readonly __brand: 'RerouteId' }
|
||||
type GroupId = number & { readonly __brand: 'GroupId' }
|
||||
|
||||
// Scope identifier, not an entity ID
|
||||
type GraphId = string & { readonly __brand: 'GraphId' }
|
||||
```
|
||||
|
||||
Widgets and Slots currently lack independent IDs. The ECS will assign synthetic IDs at entity creation time via an auto-incrementing counter (matching the pattern used by `lastNodeId`, `lastLinkId`, etc. in `LGraphState`).
|
||||
Widgets and Slots currently lack independent IDs. A `WidgetId` is a composite path string derived from its `graphId`, parent `NodeId`, and widget name (stable across instances of the same node). Slots get synthetic IDs assigned at entity creation time via an auto-incrementing counter (matching the pattern used by `lastNodeId`, `lastLinkId`, etc. in `LGraphState`).
|
||||
|
||||
### Component Decomposition
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import { useResolvedSelectedInputs } from '@/components/builder/useResolvedSelec
|
||||
import type { ResolvedSelection } from '@/components/builder/useResolvedSelectedInputs'
|
||||
import type { WidgetId } from '@/types/widgetId'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { UNASSIGNED_NODE_ID } from '@/types/nodeId'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
@@ -75,7 +76,7 @@ const mappedSelections = computed((): WidgetEntry[] => {
|
||||
if (!matchingWidget) return []
|
||||
|
||||
matchingWidget.slotMetadata = undefined
|
||||
matchingWidget.nodeId = String(node.id)
|
||||
matchingWidget.nodeId = node.id
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -139,7 +140,7 @@ async function handleDragDrop() {
|
||||
return false
|
||||
}
|
||||
|
||||
app.dragOverNode = { id: -1, onDragDrop }
|
||||
app.dragOverNode = { id: UNASSIGNED_NODE_ID, onDragDrop }
|
||||
}
|
||||
|
||||
defineExpose({ handleDragDrop })
|
||||
|
||||
@@ -5,6 +5,7 @@ import { nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import ErrorOverlay from './ErrorOverlay.vue'
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import type { NodeError } from '@/schemas/apiSchema'
|
||||
import type {
|
||||
@@ -153,7 +154,7 @@ describe('ErrorOverlay', () => {
|
||||
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
'1': makeNodeError(['Only error'])
|
||||
[asNodeId('1')]: makeNodeError(['Only error'])
|
||||
}
|
||||
executionErrorStore.showErrorOverlay()
|
||||
await nextTick()
|
||||
@@ -190,7 +191,7 @@ describe('ErrorOverlay', () => {
|
||||
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastNodeErrors = {
|
||||
'1': makeNodeError(['Only error'])
|
||||
[asNodeId('1')]: makeNodeError(['Only error'])
|
||||
}
|
||||
executionErrorStore.showErrorOverlay()
|
||||
await nextTick()
|
||||
|
||||
@@ -5,6 +5,7 @@ import { defineComponent, nextTick } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { useErrorOverlayState } from './useErrorOverlayState'
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useMissingMediaStore } from '@/platform/missingMedia/missingMediaStore'
|
||||
import type { NodeError } from '@/schemas/apiSchema'
|
||||
@@ -250,7 +251,7 @@ describe('useErrorOverlayState', () => {
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
executionErrorStore.lastExecutionError = {
|
||||
prompt_id: 'prompt',
|
||||
node_id: 1,
|
||||
node_id: asNodeExecutionId(1),
|
||||
node_type: 'KSampler',
|
||||
executed: [],
|
||||
exception_message: 'CUDA out of memory',
|
||||
@@ -276,7 +277,7 @@ describe('useErrorOverlayState', () => {
|
||||
name: 'image.png',
|
||||
mediaType: 'image',
|
||||
representative: {
|
||||
nodeId: '1',
|
||||
nodeId: asNodeExecutionId('1'),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -285,7 +286,7 @@ describe('useErrorOverlayState', () => {
|
||||
},
|
||||
referencingNodes: [
|
||||
{
|
||||
nodeId: '1',
|
||||
nodeId: asNodeExecutionId('1'),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image'
|
||||
}
|
||||
@@ -312,7 +313,7 @@ describe('useErrorOverlayState', () => {
|
||||
const missingMediaStore = useMissingMediaStore()
|
||||
missingMediaStore.setMissingMedia([
|
||||
{
|
||||
nodeId: '1',
|
||||
nodeId: asNodeExecutionId('1'),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -338,7 +339,7 @@ describe('useErrorOverlayState', () => {
|
||||
{
|
||||
name: 'missing.safetensors',
|
||||
representative: {
|
||||
nodeId: '1',
|
||||
nodeId: asNodeExecutionId('1'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'missing.safetensors',
|
||||
@@ -347,8 +348,8 @@ describe('useErrorOverlayState', () => {
|
||||
isMissing: true
|
||||
},
|
||||
referencingNodes: [
|
||||
{ nodeId: '1', widgetName: 'ckpt_name' },
|
||||
{ nodeId: '2', widgetName: 'ckpt_name' }
|
||||
{ nodeId: asNodeExecutionId('1'), widgetName: 'ckpt_name' },
|
||||
{ nodeId: asNodeExecutionId('2'), widgetName: 'ckpt_name' }
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -420,7 +421,7 @@ describe('useErrorOverlayState', () => {
|
||||
{
|
||||
name: 'first.safetensors',
|
||||
representative: {
|
||||
nodeId: '1',
|
||||
nodeId: asNodeExecutionId('1'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'first.safetensors',
|
||||
@@ -428,12 +429,14 @@ describe('useErrorOverlayState', () => {
|
||||
isAssetSupported: true,
|
||||
isMissing: true
|
||||
},
|
||||
referencingNodes: [{ nodeId: '1', widgetName: 'ckpt_name' }]
|
||||
referencingNodes: [
|
||||
{ nodeId: asNodeExecutionId('1'), widgetName: 'ckpt_name' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'second.safetensors',
|
||||
representative: {
|
||||
nodeId: '2',
|
||||
nodeId: asNodeExecutionId('2'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'second.safetensors',
|
||||
@@ -441,7 +444,9 @@ describe('useErrorOverlayState', () => {
|
||||
isAssetSupported: true,
|
||||
isMissing: true
|
||||
},
|
||||
referencingNodes: [{ nodeId: '2', widgetName: 'ckpt_name' }]
|
||||
referencingNodes: [
|
||||
{ nodeId: asNodeExecutionId('2'), widgetName: 'ckpt_name' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
||||
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraph, LGraphNode, asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
@@ -21,7 +21,7 @@ function createNode(
|
||||
pos: [number, number]
|
||||
) {
|
||||
const node = new LGraphNode(title)
|
||||
node.id = id
|
||||
node.id = asNodeId(id)
|
||||
node.pos = [...pos]
|
||||
node.size = [240, 120]
|
||||
graph.add(node)
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
:key="nodeData.id"
|
||||
:node-data="nodeData"
|
||||
:error="
|
||||
executionErrorStore.lastExecutionError?.node_id === nodeData.id
|
||||
executionErrorStore.lastExecutionErrorNodeLocatorId ===
|
||||
getLocatorIdFromNodeData(nodeData)
|
||||
? 'Execution error'
|
||||
: null
|
||||
"
|
||||
@@ -189,7 +190,10 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||
import {
|
||||
forEachNode,
|
||||
getLocatorIdFromNodeData
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
|
||||
import SelectionRectangle from './SelectionRectangle.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
@@ -35,13 +35,14 @@ const SkeletonStub = defineComponent({
|
||||
|
||||
function renderPreview(
|
||||
text: string,
|
||||
{ nodeId = 'node-1' }: { nodeId?: string | number } = {}
|
||||
{ executionId = null }: { executionId?: string | null } = {}
|
||||
) {
|
||||
const value = ref(text)
|
||||
const Harness = defineComponent({
|
||||
components: { TextPreviewWidget },
|
||||
setup: () => ({ value, nodeId }),
|
||||
template: '<TextPreviewWidget v-model="value" :node-id="nodeId" />'
|
||||
setup: () => ({ value, executionId }),
|
||||
template:
|
||||
'<TextPreviewWidget v-model="value" :execution-id="executionId" />'
|
||||
})
|
||||
return render(Harness, {
|
||||
global: {
|
||||
@@ -167,21 +168,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', { executionId: 'n1' })
|
||||
expect(screen.queryByTestId('skeleton')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows a Skeleton on mount when the parent node is executing', () => {
|
||||
it('shows a Skeleton on mount when the node is executing', () => {
|
||||
execState().executingNodeIds = ['n1']
|
||||
execState().isIdle = false
|
||||
renderPreview('text', { nodeId: 'n1' })
|
||||
renderPreview('text', { executionId: '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', { executionId: 'n1' })
|
||||
expect(screen.getByTestId('skeleton')).toBeInTheDocument()
|
||||
|
||||
execState().executingNodeIds = []
|
||||
@@ -191,15 +192,36 @@ describe('TextPreviewWidget', () => {
|
||||
expect(screen.queryByTestId('skeleton')).toBeNull()
|
||||
})
|
||||
|
||||
it('hides the Skeleton when the parent node leaves executingNodeIds', async () => {
|
||||
it('hides the Skeleton when the node leaves executingNodeIds', async () => {
|
||||
execState().executingNodeIds = ['n1']
|
||||
execState().isIdle = false
|
||||
renderPreview('text', { nodeId: 'n1' })
|
||||
renderPreview('text', { executionId: 'n1' })
|
||||
|
||||
execState().executingNodeIds = ['other']
|
||||
await nextTick()
|
||||
|
||||
expect(screen.queryByTestId('skeleton')).toBeNull()
|
||||
})
|
||||
|
||||
it('matches subgraph execution ids by full path, not local id', () => {
|
||||
execState().executingNodeIds = ['65:18']
|
||||
execState().isIdle = false
|
||||
renderPreview('text', { executionId: '65:18' })
|
||||
expect(screen.getByTestId('skeleton')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not match when only the local id collides across subgraphs', () => {
|
||||
execState().executingNodeIds = ['18']
|
||||
execState().isIdle = false
|
||||
renderPreview('text', { executionId: '65:18' })
|
||||
expect(screen.queryByTestId('skeleton')).toBeNull()
|
||||
})
|
||||
|
||||
it('falls back to any executing node when executionId is unknown', () => {
|
||||
execState().executingNodeIds = ['anything']
|
||||
execState().isIdle = false
|
||||
renderPreview('text', { executionId: null })
|
||||
expect(screen.getByTestId('skeleton')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,22 +14,22 @@
|
||||
<script setup lang="ts">
|
||||
import { default as DOMPurify } from 'dompurify'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { NodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { linkifyHtml, nl2br } from '@/utils/formatUtil'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
const props = defineProps<{
|
||||
nodeId: NodeId
|
||||
const { executionId } = defineProps<{
|
||||
executionId: NodeExecutionId | null
|
||||
}>()
|
||||
|
||||
const executionStore = useExecutionStore()
|
||||
const isParentNodeExecuting = computed(() => {
|
||||
if (executionStore.isIdle) return false
|
||||
if (!parentNodeId) return executionStore.executingNodeIds.length > 0
|
||||
return executionStore.executingNodeIds.includes(parentNodeId)
|
||||
if (!executionId) return executionStore.executingNodeIds.length > 0
|
||||
return executionStore.executingNodeIds.includes(executionId)
|
||||
})
|
||||
const formattedText = computed(() => {
|
||||
const src = modelValue.value
|
||||
@@ -64,19 +64,4 @@ const formattedText = computed(() => {
|
||||
ALLOWED_ATTR: ['href', 'target', 'rel']
|
||||
})
|
||||
})
|
||||
|
||||
let parentNodeId: NodeId | null = null
|
||||
onMounted(() => {
|
||||
// Get the parent node ID from props if provided
|
||||
// For backward compatibility, fall back to the first executing node
|
||||
parentNodeId = props.nodeId ?? parentNodeId
|
||||
})
|
||||
|
||||
// Lazily adopt the first executing node as the parent when no nodeId is known.
|
||||
watch(
|
||||
() => executionStore.executingNodeIds,
|
||||
(ids) => {
|
||||
if (!parentNodeId && ids.length > 0) parentNodeId = ids[0]
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -135,7 +135,7 @@ import {
|
||||
boundsExtractor,
|
||||
useUpstreamValue
|
||||
} from '@/composables/useUpstreamValue'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
|
||||
const { load3dState, resolveNodeMock, settingGetMock } = vi.hoisted(() => ({
|
||||
@@ -88,7 +89,7 @@ type RenderOptions = {
|
||||
enable3DViewer?: boolean
|
||||
}
|
||||
|
||||
const MOCK_NODE = { id: 'node', type: 'Load3D' }
|
||||
const MOCK_NODE = { id: 1, type: 'Load3D' }
|
||||
|
||||
function renderLoad3D(options: RenderOptions = {}) {
|
||||
const stub = buildLoad3dStub()
|
||||
@@ -109,7 +110,8 @@ function renderLoad3D(options: RenderOptions = {}) {
|
||||
widget: (options.widget ?? {
|
||||
node: MOCK_NODE
|
||||
}) as unknown as ComponentWidget<string[]>,
|
||||
nodeId: options.nodeId
|
||||
nodeId:
|
||||
options.nodeId === undefined ? undefined : asNodeId(options.nodeId)
|
||||
},
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
|
||||
@@ -115,7 +115,7 @@ 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 type { NodeId } from '@/types/nodeId'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
@@ -144,7 +144,7 @@ const node = ref<LGraphNode | null>(null)
|
||||
|
||||
if (isComponentWidget(widget)) {
|
||||
node.value = widget.node
|
||||
} else if (nodeId) {
|
||||
} else if (nodeId != null) {
|
||||
onMounted(() => {
|
||||
node.value = resolveNode(nodeId) ?? null
|
||||
})
|
||||
|
||||
@@ -22,10 +22,11 @@ vi.mock('@/components/load3d/Load3D.vue', () => ({
|
||||
}))
|
||||
|
||||
import Load3DAdvanced from '@/components/load3d/Load3DAdvanced.vue'
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
describe('Load3DAdvanced', () => {
|
||||
it('renders the inner Load3D with all expressive features disabled', () => {
|
||||
const MOCK_NODE = { id: 'node', type: 'Load3DAdvanced' }
|
||||
const MOCK_NODE = { id: 1, type: 'Load3DAdvanced' }
|
||||
render(Load3DAdvanced, {
|
||||
props: {
|
||||
widget: { node: MOCK_NODE } as never
|
||||
@@ -39,9 +40,11 @@ describe('Load3DAdvanced', () => {
|
||||
})
|
||||
|
||||
it('forwards widget and nodeId to the inner Load3D', () => {
|
||||
const widget = { node: { id: 'a', type: 'Load3DAdvanced' } }
|
||||
render(Load3DAdvanced, { props: { widget: widget as never, nodeId: 'a' } })
|
||||
const widget = { node: { id: 1, type: 'Load3DAdvanced' } }
|
||||
render(Load3DAdvanced, {
|
||||
props: { widget: widget as never, nodeId: asNodeId(1) }
|
||||
})
|
||||
expect(lastProps.value?.widget).toEqual(widget)
|
||||
expect(lastProps.value?.nodeId).toBe('a')
|
||||
expect(lastProps.value?.nodeId).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { ComponentWidget } from '@/scripts/domWidget'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
import type {
|
||||
JobListItem,
|
||||
JobStatus
|
||||
@@ -154,7 +155,7 @@ export const Queued: Story = {
|
||||
value: 1,
|
||||
max: 1,
|
||||
state: 'running',
|
||||
node_id: '1',
|
||||
node_id: asNodeExecutionId('1'),
|
||||
prompt_id: 'p1'
|
||||
}
|
||||
}
|
||||
@@ -204,7 +205,7 @@ export const QueuedParallel: Story = {
|
||||
value: 1,
|
||||
max: 2,
|
||||
state: 'running',
|
||||
node_id: '1',
|
||||
node_id: asNodeExecutionId('1'),
|
||||
prompt_id: 'p1'
|
||||
}
|
||||
},
|
||||
@@ -213,7 +214,7 @@ export const QueuedParallel: Story = {
|
||||
value: 1,
|
||||
max: 2,
|
||||
state: 'running',
|
||||
node_id: '2',
|
||||
node_id: asNodeExecutionId('2'),
|
||||
prompt_id: 'p2'
|
||||
}
|
||||
}
|
||||
@@ -254,7 +255,7 @@ export const Running: Story = {
|
||||
value: 5,
|
||||
max: 10,
|
||||
state: 'running',
|
||||
node_id: '1',
|
||||
node_id: asNodeExecutionId('1'),
|
||||
prompt_id: 'p1'
|
||||
}
|
||||
}
|
||||
@@ -299,7 +300,7 @@ export const QueuedZeroAheadSingleRunning: Story = {
|
||||
value: 1,
|
||||
max: 3,
|
||||
state: 'running',
|
||||
node_id: '1',
|
||||
node_id: asNodeExecutionId('1'),
|
||||
prompt_id: 'p1'
|
||||
}
|
||||
}
|
||||
@@ -347,7 +348,7 @@ export const QueuedZeroAheadMultiRunning: Story = {
|
||||
value: 2,
|
||||
max: 5,
|
||||
state: 'running',
|
||||
node_id: '1',
|
||||
node_id: asNodeExecutionId('1'),
|
||||
prompt_id: 'p1'
|
||||
}
|
||||
},
|
||||
@@ -356,7 +357,7 @@ export const QueuedZeroAheadMultiRunning: Story = {
|
||||
value: 3,
|
||||
max: 5,
|
||||
state: 'running',
|
||||
node_id: '2',
|
||||
node_id: asNodeExecutionId('2'),
|
||||
prompt_id: 'p2'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import type { MissingMediaCandidate } from '@/platform/missingMedia/types'
|
||||
import type { MissingModelCandidate } from '@/platform/missingModel/types'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
|
||||
const mockFocusNode = vi.hoisted(() => vi.fn())
|
||||
const mockEnterSubgraph = vi.hoisted(() => vi.fn())
|
||||
@@ -398,7 +399,7 @@ describe('TabErrors.vue', () => {
|
||||
|
||||
it('shows missing model Refresh in the section header when no model is downloadable', async () => {
|
||||
const missingModel = {
|
||||
nodeId: '1',
|
||||
nodeId: asNodeExecutionId('1'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'local-only.safetensors',
|
||||
@@ -429,7 +430,7 @@ describe('TabErrors.vue', () => {
|
||||
missingModel: {
|
||||
missingModelCandidates: [
|
||||
{
|
||||
nodeId: '1',
|
||||
nodeId: asNodeExecutionId('1'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'model-a.safetensors',
|
||||
@@ -438,7 +439,7 @@ describe('TabErrors.vue', () => {
|
||||
isAssetSupported: true
|
||||
},
|
||||
{
|
||||
nodeId: '2',
|
||||
nodeId: asNodeExecutionId('2'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'model-b.safetensors',
|
||||
@@ -460,7 +461,7 @@ describe('TabErrors.vue', () => {
|
||||
|
||||
it('renders missing model display message below the section title', () => {
|
||||
const missingModel = {
|
||||
nodeId: '1',
|
||||
nodeId: asNodeExecutionId('1'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'local-only.safetensors',
|
||||
@@ -483,7 +484,7 @@ describe('TabErrors.vue', () => {
|
||||
|
||||
it('renders missing media display message below the section title', () => {
|
||||
const missingMedia = {
|
||||
nodeId: '3',
|
||||
nodeId: asNodeExecutionId('3'),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -519,7 +520,7 @@ describe('TabErrors.vue', () => {
|
||||
missingMedia: {
|
||||
missingMediaCandidates: [
|
||||
{
|
||||
nodeId: '3',
|
||||
nodeId: asNodeExecutionId('3'),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -527,7 +528,7 @@ describe('TabErrors.vue', () => {
|
||||
isMissing: true
|
||||
},
|
||||
{
|
||||
nodeId: '4',
|
||||
nodeId: asNodeExecutionId('4'),
|
||||
nodeType: 'PreviewImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -595,7 +596,7 @@ describe('TabErrors.vue', () => {
|
||||
missingMedia: {
|
||||
missingMediaCandidates: [
|
||||
{
|
||||
nodeId: '3',
|
||||
nodeId: asNodeExecutionId('3'),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -603,7 +604,7 @@ describe('TabErrors.vue', () => {
|
||||
isMissing: true
|
||||
},
|
||||
{
|
||||
nodeId: '4',
|
||||
nodeId: asNodeExecutionId('4'),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -658,7 +659,7 @@ describe('TabErrors.vue', () => {
|
||||
|
||||
it('renders missing model Refresh in the header and Download all in the card when models are downloadable', () => {
|
||||
const missingModel = {
|
||||
nodeId: '1',
|
||||
nodeId: asNodeExecutionId('1'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
name: 'downloadable.safetensors',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
import type { MissingNodeType } from '@/types/comfy'
|
||||
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
@@ -166,7 +167,7 @@ function makeModel(
|
||||
) {
|
||||
return {
|
||||
name,
|
||||
nodeId: opts.nodeId ?? '1',
|
||||
nodeId: asNodeExecutionId(opts.nodeId ?? '1'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: opts.widgetName ?? 'ckpt_name',
|
||||
isAssetSupported: opts.isAssetSupported ?? false,
|
||||
@@ -185,7 +186,7 @@ function makeMedia(
|
||||
): MissingMediaCandidate {
|
||||
return {
|
||||
name,
|
||||
nodeId: opts.nodeId,
|
||||
nodeId: asNodeExecutionId(opts.nodeId),
|
||||
nodeType: opts.nodeType ?? 'LoadImage',
|
||||
widgetName: opts.widgetName ?? 'image',
|
||||
mediaType: 'image',
|
||||
@@ -540,7 +541,7 @@ describe('useErrorGroups', () => {
|
||||
store.lastExecutionError = {
|
||||
prompt_id: 'test-prompt',
|
||||
timestamp: Date.now(),
|
||||
node_id: 5,
|
||||
node_id: asNodeExecutionId(5),
|
||||
node_type: 'KSampler',
|
||||
executed: [],
|
||||
exception_type: 'RuntimeError',
|
||||
@@ -577,7 +578,7 @@ describe('useErrorGroups', () => {
|
||||
store.lastExecutionError = {
|
||||
prompt_id: 'test-prompt',
|
||||
timestamp: Date.now(),
|
||||
node_id: 5,
|
||||
node_id: asNodeExecutionId(5),
|
||||
node_type: 'KSampler',
|
||||
executed: [],
|
||||
exception_type: 'torch.OutOfMemoryError',
|
||||
|
||||
@@ -181,7 +181,7 @@ describe('useErrorReport', () => {
|
||||
exceptionType: 'RuntimeError',
|
||||
exceptionMessage: 'CUDA oom',
|
||||
traceback: 'trace-0',
|
||||
nodeId: '42',
|
||||
nodeId: 42,
|
||||
nodeType: 'KSampler',
|
||||
systemStats: sampleSystemStats,
|
||||
serverLogs: 'server logs',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import { until } from '@vueuse/core'
|
||||
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSystemStatsStore } from '@/stores/systemStatsStore'
|
||||
@@ -81,7 +82,8 @@ export function useErrorReport(cardSource: MaybeRefOrGetter<ErrorCardData>) {
|
||||
exceptionType: error.exceptionType ?? FALLBACK_EXCEPTION_TYPE,
|
||||
exceptionMessage: error.message,
|
||||
traceback: error.details,
|
||||
nodeId: card.nodeId,
|
||||
nodeId:
|
||||
card.nodeId === undefined ? undefined : asNodeId(card.nodeId),
|
||||
nodeType: card.title,
|
||||
systemStats: systemStatsStore.systemStats,
|
||||
serverLogs: logs,
|
||||
|
||||
@@ -152,7 +152,7 @@ function isWidgetShownOnParents(
|
||||
const interiorNodeId =
|
||||
String(widgetNode.id) === String(parent.id)
|
||||
? source.nodeId
|
||||
: String(widgetNode.id)
|
||||
: widgetNode.id
|
||||
|
||||
return isWidgetPromotedOnSubgraphNode(parent, {
|
||||
sourceNodeId: interiorNodeId,
|
||||
@@ -160,7 +160,7 @@ function isWidgetShownOnParents(
|
||||
})
|
||||
}
|
||||
return isWidgetPromotedOnSubgraphNode(parent, {
|
||||
sourceNodeId: String(widgetNode.id),
|
||||
sourceNodeId: widgetNode.id,
|
||||
sourceWidgetName: widget.name
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,7 +4,8 @@ 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 type { NodeId } from '@/types/nodeId'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
|
||||
@@ -3,7 +3,8 @@ 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 type { NodeId } from '@/types/nodeId'
|
||||
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'
|
||||
|
||||
@@ -82,7 +82,7 @@ const advancedInputsWidgets = computed((): NodeWidgetsList => {
|
||||
return allInteriorWidgets.filter(
|
||||
({ node: interiorNode, widget }) =>
|
||||
!isWidgetPromotedOnSubgraphNode(node, {
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceNodeId: interiorNode.id,
|
||||
sourceWidgetName: getWidgetName(widget)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -92,7 +92,7 @@ function handleHideInput() {
|
||||
if (source) {
|
||||
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 : node.id
|
||||
demotePromotedInput(parent, {
|
||||
sourceNodeId,
|
||||
sourceWidgetName: source.widgetName
|
||||
|
||||
@@ -5,6 +5,7 @@ import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
@@ -145,7 +146,7 @@ describe('WidgetItem', () => {
|
||||
const expectedOptions = {
|
||||
values: ['model_a.safetensors', 'model_b.safetensors']
|
||||
}
|
||||
const id = widgetId('test-graph-id', 1, 'ckpt_name')
|
||||
const id = widgetId('test-graph-id', asNodeId(1), 'ckpt_name')
|
||||
const widget = createMockWidget({ widgetId: id, name: 'ckpt_name' })
|
||||
useWidgetValueStore().registerWidget(id, {
|
||||
type: 'combo',
|
||||
@@ -160,7 +161,7 @@ describe('WidgetItem', () => {
|
||||
})
|
||||
|
||||
it('passes type from widget state to the widget component', () => {
|
||||
const id = widgetId('test-graph-id', 1, 'ckpt_name')
|
||||
const id = widgetId('test-graph-id', asNodeId(1), 'ckpt_name')
|
||||
const widget = createMockWidget({ widgetId: id, type: 'string' })
|
||||
useWidgetValueStore().registerWidget(id, {
|
||||
type: 'combo',
|
||||
@@ -175,7 +176,7 @@ describe('WidgetItem', () => {
|
||||
})
|
||||
|
||||
it('passes name from widget state to the widget component', () => {
|
||||
const id = widgetId('test-graph-id', 1, 'ckpt_name')
|
||||
const id = widgetId('test-graph-id', asNodeId(1), 'ckpt_name')
|
||||
const widget = createMockWidget({ widgetId: id, name: 'source_name' })
|
||||
useWidgetValueStore().registerWidget(id, {
|
||||
type: 'combo',
|
||||
@@ -190,7 +191,7 @@ describe('WidgetItem', () => {
|
||||
})
|
||||
|
||||
it('passes value from widget state to the widget component', () => {
|
||||
const id = widgetId('test-graph-id', 1, 'ckpt_name')
|
||||
const id = widgetId('test-graph-id', asNodeId(1), 'ckpt_name')
|
||||
const widget = createMockWidget({ widgetId: id, value: 'source value' })
|
||||
useWidgetValueStore().registerWidget(id, {
|
||||
type: 'combo',
|
||||
|
||||
@@ -70,7 +70,7 @@ const widgetComponent = computed(() => {
|
||||
|
||||
const isLinked = computed(() => {
|
||||
const safeWidget = useVueNodeLifecycle()
|
||||
.nodeManager.value?.vueNodeData.get(String(node.id))
|
||||
.nodeManager.value?.vueNodeData.get(node.id)
|
||||
?.widgets?.find((w) => w.name === widget.name)
|
||||
return safeWidget?.slotMetadata
|
||||
? !!safeWidget.slotMetadata.linked
|
||||
|
||||
@@ -5,7 +5,8 @@ import type { IFuseOptions } from 'fuse.js'
|
||||
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('SubgraphEditor', () => {
|
||||
subgraph.rootGraph.id,
|
||||
String(host.id),
|
||||
{
|
||||
sourceNodeId: String(previewNode.id),
|
||||
sourceNodeId: previewNode.id,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
)
|
||||
@@ -172,7 +172,7 @@ describe('SubgraphEditor', () => {
|
||||
const input = host.inputs.find((input) => {
|
||||
if (!input.widgetId) return false
|
||||
const target = resolveSubgraphInputTarget(host, input.name)
|
||||
return target?.nodeId === String(sourceNode.id)
|
||||
return target?.nodeId === sourceNode.id
|
||||
})!
|
||||
return {
|
||||
kind: 'promoted',
|
||||
@@ -277,7 +277,7 @@ describe('SubgraphEditor', () => {
|
||||
|
||||
const previewStore = usePreviewExposureStore()
|
||||
previewStore.addExposure(subgraph.rootGraph.id, String(host.id), {
|
||||
sourceNodeId: String(orphanedSourceNode.id),
|
||||
sourceNodeId: orphanedSourceNode.id,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
} from '@/core/graph/subgraph/promotedInputWidget'
|
||||
import type { PromotedSource } from '@/core/graph/subgraph/promotedInputWidget'
|
||||
import type { WidgetItem } from '@/core/graph/subgraph/promotionUtils'
|
||||
import type { PreviewExposure } from '@/core/schemas/previewExposureSchema'
|
||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
||||
import { asNodeId, UNASSIGNED_NODE_ID } from '@/types/nodeId'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
@@ -31,6 +31,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import type { NormalizedPreviewExposure } from '@/stores/previewExposureStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
@@ -45,7 +46,7 @@ type PromotedRow = {
|
||||
type PreviewRow = {
|
||||
kind: 'preview'
|
||||
node: LGraphNode
|
||||
exposure: PreviewExposure
|
||||
exposure: NormalizedPreviewExposure
|
||||
realWidget?: IBaseWidget
|
||||
}
|
||||
type ActiveRow = PromotedRow | PreviewRow
|
||||
@@ -69,7 +70,7 @@ function buildPromotedRows(node: SubgraphNode): PromotedRow[] {
|
||||
if (!widget) return []
|
||||
const source = promotedInputSource(node, input)
|
||||
if (!source) return []
|
||||
const sourceNode = node.subgraph._nodes_by_id[source.nodeId]
|
||||
const sourceNode = node.subgraph._nodes_by_id[asNodeId(source.nodeId)]
|
||||
if (!sourceNode) return []
|
||||
return [{ kind: 'promoted', node: sourceNode, input, widget }]
|
||||
})
|
||||
@@ -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,12 +249,12 @@ 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 === UNASSIGNED_NODE_ID) return true
|
||||
const source = promotedRowSource(row)
|
||||
return (
|
||||
!!activeNode.value &&
|
||||
!!source &&
|
||||
isLinkedPromotion(activeNode.value, String(row.node.id), source.widgetName)
|
||||
isLinkedPromotion(activeNode.value, row.node.id, source.widgetName)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -321,7 +322,7 @@ function showAll() {
|
||||
}
|
||||
function hideAll() {
|
||||
for (const row of filteredActive.value) {
|
||||
if (String(row.node.id) === '-1') continue
|
||||
if (row.node.id === UNASSIGNED_NODE_ID) continue
|
||||
demoteRow(row)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +225,7 @@ import {
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import AssetsSidebarGridView from '@/components/sidebar/tabs/AssetsSidebarGridView.vue'
|
||||
import AssetsSidebarListView from '@/components/sidebar/tabs/AssetsSidebarListView.vue'
|
||||
@@ -472,7 +473,7 @@ const galleryItems = computed(() => {
|
||||
filename: asset.name,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '0',
|
||||
nodeId: asNodeExecutionId('0'),
|
||||
mediaType: mediaType === 'image' ? 'images' : mediaType
|
||||
})
|
||||
|
||||
|
||||
@@ -4,7 +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 { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
|
||||
import MediaLightbox from './MediaLightbox.vue'
|
||||
@@ -28,7 +29,7 @@ type MockResultItem = Partial<ResultItemImpl> & {
|
||||
filename: string
|
||||
subfolder: string
|
||||
type: string
|
||||
nodeId: NodeId
|
||||
nodeId: NodeExecutionId
|
||||
mediaType: string
|
||||
id?: string
|
||||
url?: string
|
||||
@@ -63,7 +64,7 @@ describe('MediaLightbox', () => {
|
||||
filename: 'image1.jpg',
|
||||
subfolder: 'outputs',
|
||||
type: 'output',
|
||||
nodeId: '123' as NodeId,
|
||||
nodeId: asNodeExecutionId('123'),
|
||||
mediaType: 'images',
|
||||
isImage: true,
|
||||
isVideo: false,
|
||||
@@ -75,7 +76,7 @@ describe('MediaLightbox', () => {
|
||||
filename: 'image2.jpg',
|
||||
subfolder: 'outputs',
|
||||
type: 'output',
|
||||
nodeId: '456' as NodeId,
|
||||
nodeId: asNodeExecutionId('456'),
|
||||
mediaType: 'images',
|
||||
isImage: true,
|
||||
isVideo: false,
|
||||
@@ -87,7 +88,7 @@ describe('MediaLightbox', () => {
|
||||
filename: 'image3.jpg',
|
||||
subfolder: 'outputs',
|
||||
type: 'output',
|
||||
nodeId: '789' as NodeId,
|
||||
nodeId: asNodeExecutionId('789'),
|
||||
mediaType: 'images',
|
||||
isImage: true,
|
||||
isVideo: false,
|
||||
|
||||
@@ -7,7 +7,8 @@ import type { Positionable } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
LGraphNode,
|
||||
Reroute
|
||||
Reroute,
|
||||
asNodeId
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import type { NodeId } from '@/renderer/core/layout/types'
|
||||
@@ -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 = asNodeId(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 = asNodeId(100)
|
||||
this.boundingRect = [0, 0, 0, 0]
|
||||
}
|
||||
|
||||
|
||||
@@ -100,14 +100,16 @@ 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)
|
||||
// Use layout store for Vue nodes (only works with string IDs).
|
||||
// Stored bounds.y excludes the header, so expand upward by the title
|
||||
// height to reach the visual node top (matching the fallback below).
|
||||
const layout = layoutStore.getNodeLayoutRef(item.id).value
|
||||
if (layout) {
|
||||
allBounds.push([
|
||||
layout.bounds.x,
|
||||
layout.bounds.y,
|
||||
layout.bounds.y - LiteGraph.NODE_TITLE_HEIGHT,
|
||||
layout.bounds.width,
|
||||
layout.bounds.height
|
||||
layout.bounds.height + LiteGraph.NODE_TITLE_HEIGHT
|
||||
])
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 type { NodeId } from '@/types/nodeId'
|
||||
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { installErrorClearingHooks } from '@/composables/graph/useErrorClearingHooks'
|
||||
import { promoteValueWidgetViaSubgraphInput } from '@/core/graph/subgraph/promotionUtils'
|
||||
import { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraph, LGraphNode, asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
@@ -21,6 +21,7 @@ import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNodesErrorStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
import { seedRequiredInputMissingNodeError } from '@/utils/__tests__/executionErrorTestUtils'
|
||||
import type { MissingMediaCandidate } from '@/platform/missingMedia/types'
|
||||
import type { MissingModelCandidate } from '@/platform/missingModel/types'
|
||||
@@ -50,7 +51,7 @@ describe('Connection error clearing via onConnectionsChange', () => {
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
|
||||
seedRequiredInputMissingNodeError(store, String(node.id), 'clip')
|
||||
seedRequiredInputMissingNodeError(store, asNodeExecutionId(node.id), 'clip')
|
||||
|
||||
node.onConnectionsChange!(NodeSlotType.INPUT, 0, true, null, node.inputs[0])
|
||||
|
||||
@@ -62,7 +63,7 @@ describe('Connection error clearing via onConnectionsChange', () => {
|
||||
installErrorClearingHooks(graph)
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
seedRequiredInputMissingNodeError(store, String(node.id), 'clip')
|
||||
seedRequiredInputMissingNodeError(store, asNodeExecutionId(node.id), 'clip')
|
||||
|
||||
node.onConnectionsChange!(
|
||||
NodeSlotType.INPUT,
|
||||
@@ -81,7 +82,7 @@ describe('Connection error clearing via onConnectionsChange', () => {
|
||||
installErrorClearingHooks(graph)
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
seedRequiredInputMissingNodeError(store, String(node.id), 'clip')
|
||||
seedRequiredInputMissingNodeError(store, asNodeExecutionId(node.id), 'clip')
|
||||
|
||||
node.onConnectionsChange!(
|
||||
NodeSlotType.OUTPUT,
|
||||
@@ -103,7 +104,11 @@ describe('Connection error clearing via onConnectionsChange', () => {
|
||||
|
||||
const store = useExecutionErrorStore()
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
|
||||
seedRequiredInputMissingNodeError(store, String(node.id), 'model')
|
||||
seedRequiredInputMissingNodeError(
|
||||
store,
|
||||
asNodeExecutionId(node.id),
|
||||
'model'
|
||||
)
|
||||
|
||||
node.onConnectionsChange!(NodeSlotType.INPUT, 0, true, null, node.inputs[0])
|
||||
|
||||
@@ -229,10 +234,14 @@ describe('Widget change error clearing via onWidgetChanged', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
const mediaStore = useMissingMediaStore()
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
|
||||
seedRequiredInputMissingNodeError(store, String(node.id), 'image')
|
||||
seedRequiredInputMissingNodeError(
|
||||
store,
|
||||
asNodeExecutionId(node.id),
|
||||
'image'
|
||||
)
|
||||
mediaStore.setMissingMedia([
|
||||
{
|
||||
nodeId: String(node.id),
|
||||
nodeId: asNodeExecutionId(node.id),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -279,7 +288,11 @@ describe('installErrorClearingHooks lifecycle', () => {
|
||||
// Verify the hooks actually work
|
||||
const store = useExecutionErrorStore()
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(graph)
|
||||
seedRequiredInputMissingNodeError(store, String(lateNode.id), 'value')
|
||||
seedRequiredInputMissingNodeError(
|
||||
store,
|
||||
asNodeExecutionId(lateNode.id),
|
||||
'value'
|
||||
)
|
||||
|
||||
lateNode.onConnectionsChange!(
|
||||
NodeSlotType.INPUT,
|
||||
@@ -371,7 +384,7 @@ describe('installErrorClearingHooks lifecycle', () => {
|
||||
.spyOn(missingModelScan, 'scanNodeModelCandidates')
|
||||
.mockImplementation((_rootGraph, node) => [
|
||||
{
|
||||
nodeId: String(node.id),
|
||||
nodeId: asNodeExecutionId(node.id),
|
||||
nodeType: node.type,
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: false,
|
||||
@@ -448,7 +461,7 @@ describe('onNodeRemoved clears missing asset errors by execution ID', () => {
|
||||
Parameters<typeof modelStore.setMissingModels>[0][number],
|
||||
unknown
|
||||
>({
|
||||
nodeId: String(node.id),
|
||||
nodeId: asNodeExecutionId(node.id),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: false,
|
||||
@@ -471,7 +484,7 @@ describe('onNodeRemoved clears missing asset errors by execution ID', () => {
|
||||
const interiorNode = new LGraphNode('CheckpointLoaderSimple')
|
||||
subgraph.add(interiorNode)
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 65 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(65) })
|
||||
const rootGraph = subgraphNode.graph as LGraph
|
||||
rootGraph.add(subgraphNode)
|
||||
|
||||
@@ -481,7 +494,9 @@ describe('onNodeRemoved clears missing asset errors by execution ID', () => {
|
||||
// graph whose onNodeRemoved fires for interior deletions.
|
||||
installErrorClearingHooks(subgraph)
|
||||
|
||||
const interiorExecId = `${subgraphNode.id}:${interiorNode.id}`
|
||||
const interiorExecId = asNodeExecutionId(
|
||||
`${subgraphNode.id}:${interiorNode.id}`
|
||||
)
|
||||
const modelStore = useMissingModelStore()
|
||||
modelStore.setMissingModels([
|
||||
fromAny<
|
||||
@@ -507,14 +522,16 @@ describe('onNodeRemoved clears missing asset errors by execution ID', () => {
|
||||
const interiorNode = new LGraphNode('LoadImage')
|
||||
subgraph.add(interiorNode)
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 65 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(65) })
|
||||
const rootGraph = subgraphNode.graph as LGraph
|
||||
rootGraph.add(subgraphNode)
|
||||
|
||||
vi.spyOn(app, 'rootGraph', 'get').mockReturnValue(rootGraph)
|
||||
installErrorClearingHooks(subgraph)
|
||||
|
||||
const interiorExecId = `${subgraphNode.id}:${interiorNode.id}`
|
||||
const interiorExecId = asNodeExecutionId(
|
||||
`${subgraphNode.id}:${interiorNode.id}`
|
||||
)
|
||||
|
||||
const mediaStore = useMissingMediaStore()
|
||||
mediaStore.setMissingMedia([
|
||||
@@ -565,7 +582,7 @@ describe('realtime scan verifies pending cloud candidates', () => {
|
||||
// verifyAssetSupportedCandidates resolves them against the assets store.
|
||||
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([
|
||||
{
|
||||
nodeId: String(node.id),
|
||||
nodeId: asNodeExecutionId(node.id),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: true,
|
||||
@@ -613,7 +630,7 @@ describe('realtime scan verifies pending cloud candidates', () => {
|
||||
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([])
|
||||
vi.spyOn(missingMediaScan, 'scanNodeMediaCandidates').mockReturnValue([
|
||||
{
|
||||
nodeId: String(node.id),
|
||||
nodeId: asNodeExecutionId(node.id),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -656,7 +673,7 @@ describe('realtime scan verifies pending cloud candidates', () => {
|
||||
|
||||
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([
|
||||
{
|
||||
nodeId: String(node.id),
|
||||
nodeId: asNodeExecutionId(node.id),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: true,
|
||||
@@ -702,7 +719,7 @@ describe('realtime verification staleness guards', () => {
|
||||
|
||||
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([
|
||||
{
|
||||
nodeId: String(node.id),
|
||||
nodeId: asNodeExecutionId(node.id),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: true,
|
||||
@@ -753,7 +770,7 @@ describe('realtime verification staleness guards', () => {
|
||||
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([])
|
||||
vi.spyOn(missingMediaScan, 'scanNodeMediaCandidates').mockReturnValue([
|
||||
{
|
||||
nodeId: String(node.id),
|
||||
nodeId: asNodeExecutionId(node.id),
|
||||
nodeType: 'LoadImage',
|
||||
widgetName: 'image',
|
||||
mediaType: 'image',
|
||||
@@ -802,7 +819,7 @@ describe('realtime verification staleness guards', () => {
|
||||
|
||||
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([
|
||||
{
|
||||
nodeId: String(nodeA.id),
|
||||
nodeId: asNodeExecutionId(nodeA.id),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: true,
|
||||
@@ -863,7 +880,7 @@ describe('scan skips interior of bypassed subgraph containers', () => {
|
||||
const interiorNode = new LGraphNode('CheckpointLoaderSimple')
|
||||
subgraph.add(interiorNode)
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 65 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(65) })
|
||||
subgraphNode.mode = LGraphEventMode.BYPASS
|
||||
const rootGraph = subgraphNode.graph as LGraph
|
||||
rootGraph.add(subgraphNode)
|
||||
@@ -873,7 +890,7 @@ describe('scan skips interior of bypassed subgraph containers', () => {
|
||||
// didn't short-circuit first — return a concrete missing candidate.
|
||||
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([
|
||||
{
|
||||
nodeId: `${subgraphNode.id}:${interiorNode.id}`,
|
||||
nodeId: asNodeExecutionId(`${subgraphNode.id}:${interiorNode.id}`),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: false,
|
||||
@@ -902,13 +919,13 @@ describe('scan skips interior of bypassed subgraph containers', () => {
|
||||
|
||||
const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, {
|
||||
parentGraph: outerSubgraph,
|
||||
id: 76
|
||||
id: asNodeId(76)
|
||||
})
|
||||
outerSubgraph.add(innerSubgraphNode)
|
||||
|
||||
const outerSubgraphNode = createTestSubgraphNode(outerSubgraph, {
|
||||
parentGraph: rootGraph,
|
||||
id: 205
|
||||
id: asNodeId(205)
|
||||
})
|
||||
rootGraph.add(outerSubgraphNode)
|
||||
|
||||
@@ -982,11 +999,11 @@ describe('clearWidgetRelatedErrors parameter routing', () => {
|
||||
it('clears promoted widget errors by interior execution id', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const graph = subgraph.rootGraph
|
||||
const host = createTestSubgraphNode(subgraph, { id: 2 })
|
||||
const host = createTestSubgraphNode(subgraph, { id: asNodeId(2) })
|
||||
graph.add(host)
|
||||
|
||||
const interiorNode = new LGraphNode('CheckpointLoaderSimple')
|
||||
interiorNode.id = 1
|
||||
interiorNode.id = asNodeId(1)
|
||||
subgraph.add(interiorNode)
|
||||
const input = interiorNode.addInput('ckpt_name', 'COMBO')
|
||||
const widget = interiorNode.addWidget(
|
||||
@@ -1007,7 +1024,7 @@ describe('clearWidgetRelatedErrors parameter routing', () => {
|
||||
const missingModelStore = useMissingModelStore()
|
||||
missingModelStore.setMissingModels([
|
||||
{
|
||||
nodeId: '2:1',
|
||||
nodeId: asNodeExecutionId('2:1'),
|
||||
nodeType: 'CheckpointLoaderSimple',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: false,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import { widgetPromotedSource } from '@/core/graph/subgraph/promotedInputWidget'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
NodeSlotType
|
||||
@@ -308,7 +309,7 @@ function scheduleAddedNodeScan(node: LGraphNode): void {
|
||||
|
||||
function handleNodeModeChange(
|
||||
localGraph: LGraph,
|
||||
nodeId: number,
|
||||
nodeId: NodeId,
|
||||
oldMode: number,
|
||||
newMode: number
|
||||
): void {
|
||||
@@ -407,7 +408,7 @@ export function installErrorClearingHooks(graph: LGraph): () => void {
|
||||
if (event.type === 'node:property:changed' && event.property === 'mode') {
|
||||
handleNodeModeChange(
|
||||
graph,
|
||||
event.nodeId as number,
|
||||
event.nodeId,
|
||||
event.oldValue as number,
|
||||
event.newValue as number
|
||||
)
|
||||
|
||||
@@ -2,9 +2,15 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { computed, nextTick, watch } from 'vue'
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
|
||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import { BaseWidget, LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
BaseWidget,
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
asNodeId
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { widgetId } from '@/types/widgetId'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
@@ -116,7 +122,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(asNodeId(node.id))
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
|
||||
|
||||
expect(widgetData?.slotMetadata).toBeDefined()
|
||||
@@ -127,7 +133,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(asNodeId(node.id))
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
|
||||
|
||||
// Verify initially linked
|
||||
@@ -138,7 +144,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
|
||||
// Fire the trigger event that LiteGraph fires on disconnect
|
||||
graph.trigger('node:slot-links:changed', {
|
||||
nodeId: node.id,
|
||||
nodeId: asNodeId(node.id),
|
||||
slotType: NodeSlotType.INPUT,
|
||||
slotIndex: 0,
|
||||
connected: false,
|
||||
@@ -155,7 +161,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(asNodeId(node.id))!
|
||||
|
||||
// Mimic what processedWidgets does in NodeWidgets.vue:
|
||||
// derive disabled from slotMetadata.linked
|
||||
@@ -175,7 +181,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
// Simulate disconnect
|
||||
node.inputs[0].link = null
|
||||
graph.trigger('node:slot-links:changed', {
|
||||
nodeId: node.id,
|
||||
nodeId: asNodeId(node.id),
|
||||
slotType: NodeSlotType.INPUT,
|
||||
slotIndex: 0,
|
||||
connected: false,
|
||||
@@ -204,7 +210,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(asNodeId(node.id))
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'prompt')
|
||||
|
||||
expect(widgetData?.slotMetadata?.linked).toBe(true)
|
||||
@@ -224,13 +230,13 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
subgraph.add(interiorNode)
|
||||
subgraph.inputNode.slots[0].connect(interiorInput, interiorNode)
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 123 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(123) })
|
||||
subgraphNode._internalConfigureAfterSlots()
|
||||
const graph = subgraphNode.graph as LGraph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeData = vueNodeData.get(String(subgraphNode.id))
|
||||
const nodeData = vueNodeData.get(asNodeId(subgraphNode.id))
|
||||
|
||||
const widgetData = nodeData?.widgets?.find((w) => w.name === 'value')
|
||||
expect(widgetData).toBeDefined()
|
||||
@@ -242,7 +248,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(asNodeId(node.id))!
|
||||
const widgetData = nodeData.widgets!.find((w) => w.name === 'prompt')!
|
||||
|
||||
expect(widgetData.slotMetadata?.linked).toBe(true)
|
||||
@@ -252,7 +258,7 @@ describe('Widget slotMetadata reactivity on link disconnect', () => {
|
||||
node.inputs[0].link = null
|
||||
|
||||
graph.trigger('node:slot-links:changed', {
|
||||
nodeId: node.id,
|
||||
nodeId: asNodeId(node.id),
|
||||
slotType: NodeSlotType.INPUT,
|
||||
slotIndex: 0,
|
||||
connected: false,
|
||||
@@ -278,7 +284,7 @@ describe('Subgraph output slot label reactivity', () => {
|
||||
graph.add(node)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeId = String(node.id)
|
||||
const nodeId = asNodeId(node.id)
|
||||
const nodeData = vueNodeData.get(nodeId)
|
||||
if (!nodeData?.outputs) throw new Error('Expected output data to exist')
|
||||
|
||||
@@ -288,7 +294,7 @@ describe('Subgraph output slot label reactivity', () => {
|
||||
// Simulate what SubgraphNode does: set the label, then fire the trigger
|
||||
node.outputs[0].label = 'custom_label'
|
||||
graph.trigger('node:slot-label:changed', {
|
||||
nodeId: node.id,
|
||||
nodeId: asNodeId(node.id),
|
||||
slotType: NodeSlotType.OUTPUT
|
||||
})
|
||||
|
||||
@@ -306,7 +312,7 @@ describe('Subgraph output slot label reactivity', () => {
|
||||
graph.add(node)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeId = String(node.id)
|
||||
const nodeId = asNodeId(node.id)
|
||||
const nodeData = vueNodeData.get(nodeId)
|
||||
if (!nodeData?.inputs) throw new Error('Expected input data to exist')
|
||||
|
||||
@@ -314,7 +320,7 @@ describe('Subgraph output slot label reactivity', () => {
|
||||
|
||||
node.inputs[0].label = 'custom_label'
|
||||
graph.trigger('node:slot-label:changed', {
|
||||
nodeId: node.id,
|
||||
nodeId: asNodeId(node.id),
|
||||
slotType: NodeSlotType.INPUT
|
||||
})
|
||||
|
||||
@@ -330,7 +336,7 @@ describe('Subgraph output slot label reactivity', () => {
|
||||
|
||||
expect(() =>
|
||||
graph.trigger('node:slot-label:changed', {
|
||||
nodeId: 'missing-node',
|
||||
nodeId: asNodeId(999_999),
|
||||
slotType: NodeSlotType.OUTPUT
|
||||
})
|
||||
).not.toThrow()
|
||||
@@ -355,7 +361,9 @@ describe('Nested promoted widget mapping', () => {
|
||||
subgraphA.add(innerNode)
|
||||
subgraphA.inputNode.slots[0].connect(innerInput, innerNode)
|
||||
|
||||
const subgraphNodeA = createTestSubgraphNode(subgraphA, { id: 11 })
|
||||
const subgraphNodeA = createTestSubgraphNode(subgraphA, {
|
||||
id: asNodeId(11)
|
||||
})
|
||||
|
||||
const subgraphB = createTestSubgraph({
|
||||
inputs: [{ name: 'b_input', type: '*' }]
|
||||
@@ -364,12 +372,14 @@ describe('Nested promoted widget mapping', () => {
|
||||
subgraphNodeA._internalConfigureAfterSlots()
|
||||
subgraphB.inputNode.slots[0].connect(subgraphNodeA.inputs[0], subgraphNodeA)
|
||||
|
||||
const subgraphNodeB = createTestSubgraphNode(subgraphB, { id: 22 })
|
||||
const subgraphNodeB = createTestSubgraphNode(subgraphB, {
|
||||
id: asNodeId(22)
|
||||
})
|
||||
const graph = subgraphNodeB.graph as LGraph
|
||||
graph.add(subgraphNodeB)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeData = vueNodeData.get(String(subgraphNodeB.id))
|
||||
const nodeData = vueNodeData.get(asNodeId(subgraphNodeB.id))
|
||||
const mappedWidget = nodeData?.widgets?.[0]
|
||||
|
||||
expect(mappedWidget).toBeDefined()
|
||||
@@ -401,12 +411,12 @@ describe('Nested promoted widget mapping', () => {
|
||||
subgraph.add(secondNode)
|
||||
subgraph.inputNode.slots[1].connect(secondInput, secondNode)
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 100 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(100) })
|
||||
const graph = subgraphNode.graph as LGraph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
const { vueNodeData } = useGraphNodeManager(graph)
|
||||
const nodeData = vueNodeData.get(String(subgraphNode.id))
|
||||
const nodeData = vueNodeData.get(asNodeId(subgraphNode.id))
|
||||
const widgets = nodeData?.widgets
|
||||
|
||||
expect(widgets).toHaveLength(2)
|
||||
@@ -444,7 +454,7 @@ describe('Promoted widget sourceExecutionId', () => {
|
||||
subgraph.add(interiorNode)
|
||||
subgraph.inputNode.slots[0].connect(interiorInput, interiorNode)
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 65 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(65) })
|
||||
subgraphNode._internalConfigureAfterSlots()
|
||||
const graph = subgraphNode.graph as LGraph
|
||||
graph.add(subgraphNode)
|
||||
@@ -452,7 +462,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(asNodeId(subgraphNode.id))
|
||||
const promotedWidget = nodeData?.widgets?.find(
|
||||
(w) => w.name === 'ckpt_input'
|
||||
)
|
||||
@@ -475,7 +485,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(asNodeId(node.id))
|
||||
const widget = nodeData?.widgets?.find((w) => w.name === 'steps')
|
||||
|
||||
expect(widget).toBeDefined()
|
||||
@@ -591,7 +601,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
interiorNode.addInput('value', 'INT')
|
||||
subgraph.add(interiorNode)
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 50 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(50) })
|
||||
const graph = subgraphNode.graph as LGraph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
@@ -602,7 +612,9 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
const store = useExecutionErrorStore()
|
||||
|
||||
// Error on interior node: execution ID = "50:<interiorNodeId>"
|
||||
const interiorExecId = `${subgraphNode.id}:${interiorNode.id}`
|
||||
const interiorExecId = asNodeExecutionId(
|
||||
`${subgraphNode.id}:${interiorNode.id}`
|
||||
)
|
||||
store.lastNodeErrors = {
|
||||
[interiorExecId]: {
|
||||
errors: [
|
||||
@@ -632,7 +644,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
|
||||
missingModelStore.setMissingModels([
|
||||
{
|
||||
nodeId: String(nodeA.id),
|
||||
nodeId: asNodeExecutionId(nodeA.id),
|
||||
nodeType: 'CheckpointLoader',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: false,
|
||||
@@ -652,7 +664,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
|
||||
missingModelStore.setMissingModels([
|
||||
{
|
||||
nodeId: String(nodeA.id),
|
||||
nodeId: asNodeExecutionId(nodeA.id),
|
||||
nodeType: 'CheckpointLoader',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: false,
|
||||
@@ -673,7 +685,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
const interiorNode = new LGraphNode('CheckpointLoader')
|
||||
subgraph.add(interiorNode)
|
||||
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 50 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(50) })
|
||||
const graph = subgraphNode.graph as LGraph
|
||||
graph.add(subgraphNode)
|
||||
|
||||
@@ -689,7 +701,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
|
||||
|
||||
missingModelStore.setMissingModels([
|
||||
{
|
||||
nodeId: `${subgraphNode.id}:${interiorNode.id}`,
|
||||
nodeId: asNodeExecutionId(`${subgraphNode.id}:${interiorNode.id}`),
|
||||
nodeType: 'CheckpointLoader',
|
||||
widgetName: 'ckpt_name',
|
||||
isAssetSupported: false,
|
||||
|
||||
@@ -134,10 +134,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: string): LGraphNode | undefined
|
||||
getNode(id: NodeId): LGraphNode | undefined
|
||||
|
||||
// Lifecycle methods
|
||||
cleanup(): void
|
||||
@@ -469,7 +469,7 @@ export function extractVueNodeData(node: LGraphNode): VueNodeData {
|
||||
const badges = node.badges
|
||||
|
||||
return {
|
||||
id: String(node.id),
|
||||
id: node.id,
|
||||
title: typeof node.title === 'string' ? node.title : '',
|
||||
type: nodeType,
|
||||
mode: node.mode || 0,
|
||||
@@ -496,12 +496,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)
|
||||
|
||||
@@ -516,14 +516,14 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
}
|
||||
|
||||
// Get access to original LiteGraph node (non-reactive)
|
||||
const getNode = (id: string): LGraphNode | undefined => {
|
||||
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) => n.id))
|
||||
|
||||
// Remove deleted nodes
|
||||
for (const id of Array.from(vueNodeData.keys())) {
|
||||
@@ -535,7 +535,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
|
||||
// Add/update existing nodes
|
||||
graph._nodes.forEach((node) => {
|
||||
const id = String(node.id)
|
||||
const id = node.id
|
||||
|
||||
// Store non-reactive reference
|
||||
nodeRefs.set(id, node)
|
||||
@@ -553,7 +553,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
node: LGraphNode,
|
||||
originalCallback?: (node: LGraphNode) => void
|
||||
) => {
|
||||
const id = String(node.id)
|
||||
const id = node.id
|
||||
|
||||
// Store non-reactive reference to original node
|
||||
nodeRefs.set(id, node)
|
||||
@@ -615,7 +615,7 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
node: LGraphNode,
|
||||
originalCallback?: (node: LGraphNode) => void
|
||||
) => {
|
||||
const id = String(node.id)
|
||||
const id = node.id
|
||||
|
||||
// Remove node from layout store
|
||||
setSource(LayoutSource.Canvas)
|
||||
@@ -673,7 +673,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 = propertyEvent.nodeId
|
||||
const currentData = vueNodeData.get(nodeId)
|
||||
|
||||
if (currentData) {
|
||||
@@ -769,15 +769,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
|
||||
}
|
||||
},
|
||||
'node:slot-errors:changed': (slotErrorsEvent) => {
|
||||
refreshNodeSlots(String(slotErrorsEvent.nodeId))
|
||||
refreshNodeSlots(slotErrorsEvent.nodeId)
|
||||
},
|
||||
'node:slot-links:changed': (slotLinksEvent) => {
|
||||
if (slotLinksEvent.slotType === NodeSlotType.INPUT) {
|
||||
refreshNodeSlots(String(slotLinksEvent.nodeId))
|
||||
refreshNodeSlots(slotLinksEvent.nodeId)
|
||||
}
|
||||
},
|
||||
'node:slot-label:changed': (slotLabelEvent) => {
|
||||
const nodeId = String(slotLabelEvent.nodeId)
|
||||
const nodeId = slotLabelEvent.nodeId
|
||||
const nodeRef = nodeRefs.get(nodeId)
|
||||
if (!nodeRef) return
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
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 type { NodeId } from '@/types/nodeId'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { getExtraOptionsForWidget } from '@/services/litegraphService'
|
||||
import { isLGraphGroup } from '@/utils/litegraphUtil'
|
||||
|
||||
@@ -6,7 +6,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useNodeMenuOptions } from '@/composables/graph/useNodeMenuOptions'
|
||||
import type { Positionable } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
LGraphNode,
|
||||
asNodeId
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
|
||||
// canvasStore transitively imports the app singleton; stub it so the real
|
||||
@@ -45,7 +49,7 @@ const i18n = createI18n({
|
||||
|
||||
const nodeWithMode = (mode: LGraphEventMode, id = 1): LGraphNode => {
|
||||
const node = new LGraphNode('Test')
|
||||
node.id = id
|
||||
node.id = asNodeId(id)
|
||||
node.mode = mode
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { shallowRef, watch } from 'vue'
|
||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
@@ -29,7 +30,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]
|
||||
}))
|
||||
@@ -47,9 +48,9 @@ function useVueNodeLifecycleIndividual() {
|
||||
for (const link of activeGraph._links.values()) {
|
||||
layoutMutations.createLink(
|
||||
link.id,
|
||||
link.origin_id,
|
||||
asNodeId(link.origin_id),
|
||||
link.origin_slot,
|
||||
link.target_id,
|
||||
asNodeId(link.target_id),
|
||||
link.target_slot
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { reactive } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import type { 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 = asNodeId(options.id)
|
||||
if (options.previewMediaType) {
|
||||
node.previewMediaType = options.previewMediaType
|
||||
}
|
||||
@@ -69,7 +70,7 @@ function addInteriorNode(
|
||||
function seedOutputs(subgraphId: string, nodeIds: Array<number | string>) {
|
||||
const store = useNodeOutputStore()
|
||||
for (const nodeId of nodeIds) {
|
||||
const locatorId = createNodeLocatorId(subgraphId, nodeId)
|
||||
const locatorId = createNodeLocatorId(subgraphId, asNodeId(nodeId))
|
||||
store.nodeOutputs[locatorId] = {
|
||||
images: [{ filename: 'output.png' }]
|
||||
}
|
||||
@@ -82,20 +83,20 @@ function seedPreviewImages(
|
||||
) {
|
||||
const store = useNodeOutputStore()
|
||||
for (const { nodeId, urls } of entries) {
|
||||
const locatorId = createNodeLocatorId(subgraphId, nodeId)
|
||||
const locatorId = createNodeLocatorId(subgraphId, asNodeId(nodeId))
|
||||
store.nodePreviewImages[locatorId] = urls
|
||||
}
|
||||
}
|
||||
|
||||
function exposePreview(
|
||||
setup: ReturnType<typeof createSetup>,
|
||||
sourceNodeId: string,
|
||||
sourceNodeId: number | string,
|
||||
sourcePreviewName = CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
) {
|
||||
usePreviewExposureStore().addExposure(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
String(setup.subgraphNode.id),
|
||||
{ sourceNodeId, sourcePreviewName }
|
||||
{ sourceNodeId: asNodeId(sourceNodeId), sourcePreviewName }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -153,7 +154,7 @@ describe(usePromotedPreviews, () => {
|
||||
const { promotedPreviews } = usePromotedPreviews(() => setup.subgraphNode)
|
||||
expect(promotedPreviews.value).toEqual([
|
||||
{
|
||||
sourceNodeId: '10',
|
||||
sourceNodeId: asNodeId(10),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
type: 'image',
|
||||
urls
|
||||
@@ -161,6 +162,30 @@ describe(usePromotedPreviews, () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('types preview exposure source node ids as NodeId', () => {
|
||||
const setup = createSetup()
|
||||
const sourceNodeId = asNodeId(10)
|
||||
|
||||
usePreviewExposureStore().addExposure(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
String(setup.subgraphNode.id),
|
||||
{
|
||||
sourceNodeId,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
)
|
||||
|
||||
const exposure = usePreviewExposureStore().getExposures(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
String(setup.subgraphNode.id)
|
||||
)[0]
|
||||
const { promotedPreviews } = usePromotedPreviews(() => setup.subgraphNode)
|
||||
|
||||
expectTypeOf(exposure.sourceNodeId).toEqualTypeOf<NodeId>()
|
||||
type PromotedPreview = (typeof promotedPreviews.value)[number]
|
||||
expectTypeOf<PromotedPreview['sourceNodeId']>().toEqualTypeOf<NodeId>()
|
||||
})
|
||||
|
||||
it.for([
|
||||
['video', '/view?filename=output.webm'],
|
||||
['audio', '/view?filename=output.mp3']
|
||||
@@ -226,7 +251,7 @@ describe(usePromotedPreviews, () => {
|
||||
const { promotedPreviews } = usePromotedPreviews(() => setup.subgraphNode)
|
||||
expect(promotedPreviews.value).toEqual([
|
||||
{
|
||||
sourceNodeId: '10',
|
||||
sourceNodeId: asNodeId(10),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
type: 'image',
|
||||
urls: [blobUrl]
|
||||
@@ -248,7 +273,7 @@ describe(usePromotedPreviews, () => {
|
||||
|
||||
expect(promotedPreviews.value).toEqual([
|
||||
{
|
||||
sourceNodeId: '10',
|
||||
sourceNodeId: asNodeId(10),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
type: 'image',
|
||||
urls: [blobUrl]
|
||||
@@ -281,7 +306,9 @@ describe(usePromotedPreviews, () => {
|
||||
})
|
||||
|
||||
const outerSetup = createSetup()
|
||||
const innerHost = createTestSubgraphNode(innerSetup.subgraph, { id: 20 })
|
||||
const innerHost = createTestSubgraphNode(innerSetup.subgraph, {
|
||||
id: asNodeId(20)
|
||||
})
|
||||
outerSetup.subgraph.add(innerHost)
|
||||
|
||||
const store = usePreviewExposureStore()
|
||||
@@ -289,7 +316,7 @@ describe(usePromotedPreviews, () => {
|
||||
outerSetup.subgraphNode.rootGraph.id,
|
||||
String(innerHost.id),
|
||||
{
|
||||
sourceNodeId: String(leafNode.id),
|
||||
sourceNodeId: leafNode.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
)
|
||||
@@ -297,7 +324,7 @@ describe(usePromotedPreviews, () => {
|
||||
outerSetup.subgraphNode.rootGraph.id,
|
||||
String(outerSetup.subgraphNode.id),
|
||||
{
|
||||
sourceNodeId: String(innerHost.id),
|
||||
sourceNodeId: innerHost.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
)
|
||||
@@ -313,7 +340,7 @@ describe(usePromotedPreviews, () => {
|
||||
)
|
||||
expect(promotedPreviews.value).toEqual([
|
||||
{
|
||||
sourceNodeId: '10',
|
||||
sourceNodeId: asNodeId(10),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
type: 'image',
|
||||
urls: mockUrls
|
||||
@@ -329,10 +356,16 @@ describe(usePromotedPreviews, () => {
|
||||
})
|
||||
|
||||
const outerSetup = createSetup()
|
||||
const innerHost = createTestSubgraphNode(innerSetup.subgraph, { id: 20 })
|
||||
const innerHost = createTestSubgraphNode(innerSetup.subgraph, {
|
||||
id: asNodeId(20)
|
||||
})
|
||||
outerSetup.subgraph.add(innerHost)
|
||||
const firstHost = createTestSubgraphNode(outerSetup.subgraph, { id: 11 })
|
||||
const secondHost = createTestSubgraphNode(outerSetup.subgraph, { id: 12 })
|
||||
const firstHost = createTestSubgraphNode(outerSetup.subgraph, {
|
||||
id: asNodeId(11)
|
||||
})
|
||||
const secondHost = createTestSubgraphNode(outerSetup.subgraph, {
|
||||
id: asNodeId(12)
|
||||
})
|
||||
const firstHostLocator = String(firstHost.id)
|
||||
const secondHostLocator = String(secondHost.id)
|
||||
const firstNestedLocator = `${firstHostLocator}:${innerHost.id}`
|
||||
@@ -342,19 +375,19 @@ describe(usePromotedPreviews, () => {
|
||||
|
||||
const store = usePreviewExposureStore()
|
||||
store.addExposure(firstHost.rootGraph.id, firstHostLocator, {
|
||||
sourceNodeId: String(innerHost.id),
|
||||
sourceNodeId: innerHost.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
store.addExposure(firstHost.rootGraph.id, secondHostLocator, {
|
||||
sourceNodeId: String(innerHost.id),
|
||||
sourceNodeId: innerHost.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
store.addExposure(firstHost.rootGraph.id, firstNestedLocator, {
|
||||
sourceNodeId: String(leafNode.id),
|
||||
sourceNodeId: leafNode.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
store.addExposure(firstHost.rootGraph.id, secondNestedLocator, {
|
||||
sourceNodeId: String(leafNode.id),
|
||||
sourceNodeId: leafNode.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
|
||||
@@ -377,7 +410,7 @@ describe(usePromotedPreviews, () => {
|
||||
expect(usePromotedPreviews(() => firstHost).promotedPreviews.value).toEqual(
|
||||
[
|
||||
{
|
||||
sourceNodeId: '10',
|
||||
sourceNodeId: asNodeId(10),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
type: 'image',
|
||||
urls: ['blob:first']
|
||||
@@ -388,7 +421,7 @@ describe(usePromotedPreviews, () => {
|
||||
usePromotedPreviews(() => secondHost).promotedPreviews.value
|
||||
).toEqual([
|
||||
{
|
||||
sourceNodeId: '10',
|
||||
sourceNodeId: asNodeId(10),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
type: 'image',
|
||||
urls: ['blob:second']
|
||||
|
||||
@@ -7,9 +7,10 @@ import type { UUID } from '@/utils/uuid'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
interface PromotedPreview {
|
||||
sourceNodeId: string
|
||||
sourceNodeId: NodeId
|
||||
sourceWidgetName: string
|
||||
type: 'image' | 'video' | 'audio'
|
||||
urls: string[]
|
||||
@@ -37,7 +38,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: string,
|
||||
interiorNode: LGraphNode
|
||||
): string[] | undefined {
|
||||
@@ -84,7 +85,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)
|
||||
|
||||
@@ -80,7 +80,7 @@ export function usePainter(nodeId: string, options: UsePainterOptions) {
|
||||
|
||||
const litegraphNode = computed(() => {
|
||||
if (!nodeId || !app.canvas.graph) return null
|
||||
return app.canvas.graph.getNodeById(nodeId) as LGraphNode | null
|
||||
return app.canvas.graph.getNodeByRawId(nodeId) as LGraphNode | null
|
||||
})
|
||||
|
||||
function getWidgetByName(name: string): IBaseWidget | undefined {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { useResultGallery } from '@/composables/queue/useResultGallery'
|
||||
import type { JobListItem as JobListViewItem } from '@/composables/queue/useJobList'
|
||||
import { asNodeExecutionId } from '@/types/nodeIdentification'
|
||||
import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'
|
||||
|
||||
@@ -14,7 +15,7 @@ const createResultItem = (
|
||||
filename: url,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: 'node-1',
|
||||
nodeId: asNodeExecutionId('node-1'),
|
||||
mediaType: supportsPreview ? 'images' : 'unknown'
|
||||
})
|
||||
// Override url getter for test matching
|
||||
|
||||
@@ -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 { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
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(asNodeId(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 = asNodeId(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: asNodeId(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: asNodeId(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: asNodeId(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: asNodeId(2),
|
||||
modelValue: { x: 0, y: 0, width: 100, height: 100 }
|
||||
},
|
||||
global: {
|
||||
|
||||
@@ -2,7 +2,8 @@ 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 { NodeId } from '@/types/nodeId'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { resolveNode } from '@/utils/litegraphUtil'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { asNodeId } 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: asNodeId('1'),
|
||||
options: {},
|
||||
y: 0
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
@@ -23,7 +24,10 @@ export function useUpstreamValue<T>(
|
||||
if (!upstream) return undefined
|
||||
const graphId = canvasStore.canvas?.graph?.rootGraph.id
|
||||
if (!graphId) return undefined
|
||||
const widgets = widgetValueStore.getNodeWidgets(graphId, upstream.nodeId)
|
||||
const widgets = widgetValueStore.getNodeWidgets(
|
||||
graphId,
|
||||
asNodeId(upstream.nodeId)
|
||||
)
|
||||
return extractValue(widgets, upstream.outputName)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -445,7 +445,7 @@ describe('flushProxyWidgetMigration', () => {
|
||||
)
|
||||
expect(exposures).toHaveLength(1)
|
||||
expect(exposures[0].sourcePreviewName).toBe('$$canvas-image-preview')
|
||||
expect(exposures[0].sourceNodeId).toBe(String(inner.id))
|
||||
expect(exposures[0].sourceNodeId).toBe(inner.id)
|
||||
})
|
||||
|
||||
it('classifies type:preview serialize:false widgets as preview exposure', () => {
|
||||
@@ -465,7 +465,7 @@ describe('flushProxyWidgetMigration', () => {
|
||||
)
|
||||
expect(exposures).toEqual([
|
||||
expect.objectContaining({
|
||||
sourceNodeId: String(inner.id),
|
||||
sourceNodeId: inner.id,
|
||||
sourcePreviewName: 'videopreview'
|
||||
})
|
||||
])
|
||||
@@ -483,7 +483,7 @@ describe('flushProxyWidgetMigration', () => {
|
||||
const store = usePreviewExposureStore()
|
||||
const locator = String(host.id)
|
||||
store.addExposure(host.rootGraph.id, locator, {
|
||||
sourceNodeId: String(innerA.id),
|
||||
sourceNodeId: innerA.id,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
|
||||
@@ -494,9 +494,7 @@ describe('flushProxyWidgetMigration', () => {
|
||||
|
||||
const exposures = store.getExposures(host.rootGraph.id, locator)
|
||||
expect(exposures).toHaveLength(2)
|
||||
const newExposure = exposures.find(
|
||||
(e) => e.sourceNodeId === String(innerB.id)
|
||||
)
|
||||
const newExposure = exposures.find((e) => e.sourceNodeId === innerB.id)
|
||||
expect(newExposure?.name).toBe('$$canvas-image-preview_1')
|
||||
})
|
||||
|
||||
@@ -509,7 +507,7 @@ describe('flushProxyWidgetMigration', () => {
|
||||
const store = usePreviewExposureStore()
|
||||
const locator = String(host.id)
|
||||
store.addExposure(host.rootGraph.id, locator, {
|
||||
sourceNodeId: String(inner.id),
|
||||
sourceNodeId: inner.id,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
|
||||
@@ -746,7 +744,7 @@ describe('flushProxyWidgetMigration', () => {
|
||||
)
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
sourceNodeId: String(inner.id),
|
||||
sourceNodeId: inner.id,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
])
|
||||
@@ -779,7 +777,7 @@ describe('normalizeLegacyProxyWidgetEntry', () => {
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceNodeId: innerNode.id,
|
||||
sourceWidgetName: 'seed'
|
||||
})
|
||||
})
|
||||
@@ -795,7 +793,7 @@ describe('normalizeLegacyProxyWidgetEntry', () => {
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceNodeId: innerNode.id,
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: String(innerNode.id)
|
||||
})
|
||||
@@ -833,7 +831,7 @@ describe('normalizeLegacyProxyWidgetEntry', () => {
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceNodeId: innerNode.id,
|
||||
sourceWidgetName: 'nonexistent_widget',
|
||||
disambiguatingSourceNodeId: '999'
|
||||
})
|
||||
|
||||
@@ -17,7 +17,9 @@ 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 { asNodeId } from '@/types/nodeId'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
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'
|
||||
@@ -30,7 +32,11 @@ import { isWidgetValue } from '@/lib/litegraph/src/types/widgets'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
interface LegacyProxyEntrySource extends PromotedWidgetSource {
|
||||
interface LegacyProxyEntrySource extends Omit<
|
||||
PromotedWidgetSource,
|
||||
'sourceNodeId'
|
||||
> {
|
||||
sourceNodeId: NodeId
|
||||
disambiguatingSourceNodeId?: string
|
||||
}
|
||||
|
||||
@@ -54,7 +60,7 @@ function stripLegacyPrefixes(sourceWidgetName: string): StrippedPrefix {
|
||||
|
||||
function canResolveLegacyProxy(
|
||||
hostNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceNodeId: NodeId,
|
||||
widgetName: string
|
||||
): boolean {
|
||||
return (
|
||||
@@ -69,9 +75,12 @@ export function normalizeLegacyProxyWidgetEntry(
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
): LegacyProxyEntrySource {
|
||||
if (canResolveLegacyProxy(hostNode, sourceNodeId, sourceWidgetName)) {
|
||||
const normalizedSourceNodeId = asNodeId(sourceNodeId)
|
||||
if (
|
||||
canResolveLegacyProxy(hostNode, normalizedSourceNodeId, sourceWidgetName)
|
||||
) {
|
||||
return {
|
||||
sourceNodeId,
|
||||
sourceNodeId: normalizedSourceNodeId,
|
||||
sourceWidgetName,
|
||||
...(disambiguatingSourceNodeId && { disambiguatingSourceNodeId })
|
||||
}
|
||||
@@ -82,7 +91,7 @@ export function normalizeLegacyProxyWidgetEntry(
|
||||
stripped.deepestPrefixId ?? disambiguatingSourceNodeId
|
||||
|
||||
return {
|
||||
sourceNodeId,
|
||||
sourceNodeId: normalizedSourceNodeId,
|
||||
sourceWidgetName: stripped.sourceWidgetName,
|
||||
...(patchDisambiguatingSourceNodeId && {
|
||||
disambiguatingSourceNodeId: patchDisambiguatingSourceNodeId
|
||||
@@ -101,7 +110,7 @@ function resolveSourceWidget(
|
||||
if (disambiguatingSourceNodeId) {
|
||||
return (
|
||||
target?.widgetName === sourceWidgetName &&
|
||||
target.nodeId === disambiguatingSourceNodeId
|
||||
String(target.nodeId) === disambiguatingSourceNodeId
|
||||
)
|
||||
}
|
||||
if (input.name === sourceWidgetName) return true
|
||||
@@ -262,7 +271,7 @@ function collectTargetsStrict(
|
||||
const link = subgraph.links.get(linkId)
|
||||
if (!link) return undefined
|
||||
targets.push({
|
||||
targetNodeId: link.target_id,
|
||||
targetNodeId: asNodeId(link.target_id),
|
||||
targetSlot: link.target_slot
|
||||
})
|
||||
}
|
||||
@@ -278,14 +287,19 @@ function collectTargetsSkippingDangling(
|
||||
return linkIds.flatMap((linkId) => {
|
||||
const link = subgraph.links.get(linkId)
|
||||
return link
|
||||
? [{ targetNodeId: link.target_id, targetSlot: link.target_slot }]
|
||||
? [
|
||||
{
|
||||
targetNodeId: asNodeId(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
|
||||
@@ -306,7 +320,7 @@ function classify(
|
||||
return { kind: 'alreadyLinked', subgraphInputName: linkedInput.name }
|
||||
}
|
||||
|
||||
const sourceNode = hostNode.subgraph.getNodeById(normalized.sourceNodeId)
|
||||
const sourceNode = hostNode.subgraph.getNodeByRawId(normalized.sourceNodeId)
|
||||
if (!sourceNode) {
|
||||
return { kind: 'quarantine', reason: 'missingSourceNode' }
|
||||
}
|
||||
@@ -428,7 +442,7 @@ function repairCreateSubgraphInput(
|
||||
sourceWidgetName: string
|
||||
): RepairValueResult {
|
||||
const subgraph = hostNode.subgraph
|
||||
const sourceNode: LGraphNode | null = subgraph.getNodeById(
|
||||
const sourceNode: LGraphNode | null = subgraph.getNodeByRawId(
|
||||
entry.normalized.sourceNodeId
|
||||
)
|
||||
if (!sourceNode) {
|
||||
@@ -549,7 +563,7 @@ function rollback(
|
||||
}
|
||||
}
|
||||
for (const link of snapshot) {
|
||||
const targetNode = hostNode.subgraph.getNodeById(link.targetNodeId)
|
||||
const targetNode = hostNode.subgraph.getNodeByRawId(link.targetNodeId)
|
||||
if (!targetNode) continue
|
||||
primitiveNode.connect(link.primitiveSlot, targetNode, link.targetSlot)
|
||||
}
|
||||
@@ -564,7 +578,7 @@ function repairPrimitive(
|
||||
return failPrimitive('cohort validation failed', { cohort })
|
||||
|
||||
const subgraph = hostNode.subgraph
|
||||
const primitiveNode = subgraph.getNodeById(validated.primitiveNodeId)
|
||||
const primitiveNode = subgraph.getNodeByRawId(validated.primitiveNodeId)
|
||||
if (!primitiveNode) return failPrimitive('primitive node missing', validated)
|
||||
if (primitiveNode.type !== PRIMITIVE_NODE_TYPE) {
|
||||
return failPrimitive('node is not a PrimitiveNode', primitiveNode.type)
|
||||
@@ -579,7 +593,7 @@ function repairPrimitive(
|
||||
const primitiveOutputType = String(primitiveOutput.type ?? '*')
|
||||
|
||||
for (const target of targets) {
|
||||
const targetNode = subgraph.getNodeById(target.targetNodeId)
|
||||
const targetNode = subgraph.getNodeByRawId(target.targetNodeId)
|
||||
if (!targetNode) return failPrimitive('target node missing', target)
|
||||
const targetSlot = targetNode.inputs?.[target.targetSlot]
|
||||
if (!targetSlot) return failPrimitive('target slot missing', target)
|
||||
@@ -603,7 +617,7 @@ function repairPrimitive(
|
||||
.filter((l): l is NonNullable<typeof l> => l !== undefined)
|
||||
.map((l) => ({
|
||||
primitiveSlot: l.origin_slot,
|
||||
targetNodeId: l.target_id,
|
||||
targetNodeId: asNodeId(l.target_id),
|
||||
targetSlot: l.target_slot
|
||||
}))
|
||||
|
||||
@@ -616,7 +630,7 @@ function repairPrimitive(
|
||||
)
|
||||
|
||||
for (const snap of snapshot) {
|
||||
const targetNode = subgraph.getNodeById(snap.targetNodeId)
|
||||
const targetNode = subgraph.getNodeByRawId(snap.targetNodeId)
|
||||
if (!targetNode)
|
||||
throw new Error(
|
||||
`target node ${snap.targetNodeId} disappeared mid-mutation`
|
||||
@@ -625,7 +639,7 @@ function repairPrimitive(
|
||||
}
|
||||
|
||||
for (const target of targets) {
|
||||
const targetNode = subgraph.getNodeById(target.targetNodeId)
|
||||
const targetNode = subgraph.getNodeByRawId(target.targetNodeId)
|
||||
if (!targetNode)
|
||||
throw new Error(`target node ${target.targetNodeId} disappeared`)
|
||||
const targetSlot = targetNode.inputs?.[target.targetSlot]
|
||||
@@ -683,7 +697,7 @@ function migratePreview(
|
||||
store: ReturnType<typeof usePreviewExposureStore>,
|
||||
plan: { kind: 'previewExposure'; sourcePreviewName: string }
|
||||
): MigratePreviewResult {
|
||||
const sourceNode = hostNode.subgraph.getNodeById(
|
||||
const sourceNode = hostNode.subgraph.getNodeByRawId(
|
||||
entry.normalized.sourceNodeId
|
||||
)
|
||||
if (!sourceNode) {
|
||||
@@ -725,8 +739,8 @@ function quarantineFor(
|
||||
const { sourceNodeId, sourceWidgetName, disambiguatingSourceNodeId } =
|
||||
entry.normalized
|
||||
const originalEntry: SerializedProxyWidgetTuple = disambiguatingSourceNodeId
|
||||
? [sourceNodeId, sourceWidgetName, disambiguatingSourceNodeId]
|
||||
: [sourceNodeId, sourceWidgetName]
|
||||
? [String(sourceNodeId), sourceWidgetName, disambiguatingSourceNodeId]
|
||||
: [String(sourceNodeId), sourceWidgetName]
|
||||
return makeQuarantineEntry({
|
||||
originalEntry,
|
||||
reason,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { PreviewExposure } from '@/core/schemas/previewExposureSchema'
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
|
||||
import type { PreviewExposureChainContext } from './previewExposureChain'
|
||||
@@ -9,11 +11,13 @@ import { resolvePreviewExposureChain } from './previewExposureChain'
|
||||
const rootGraphA = 'root-a' as UUID
|
||||
const rootGraphB = 'root-b' as UUID
|
||||
|
||||
interface FixtureExposure extends PreviewExposure {}
|
||||
type FixtureExposure = Omit<PreviewExposure, 'sourceNodeId'> & {
|
||||
sourceNodeId: NodeId
|
||||
}
|
||||
|
||||
interface NestedHostMapping {
|
||||
fromHostLocator: string
|
||||
fromSourceNodeId: string
|
||||
fromSourceNodeId: NodeId
|
||||
toRootGraphId: UUID
|
||||
toHostLocator: string
|
||||
}
|
||||
@@ -66,7 +70,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'preview',
|
||||
sourceNodeId: '42',
|
||||
sourceNodeId: asNodeId(42),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
]
|
||||
@@ -88,14 +92,14 @@ describe(resolvePreviewExposureChain, () => {
|
||||
hostNodeLocator: 'host-a',
|
||||
exposure: {
|
||||
name: 'preview',
|
||||
sourceNodeId: '42',
|
||||
sourceNodeId: asNodeId(42),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
}
|
||||
],
|
||||
leaf: {
|
||||
rootGraphId: rootGraphA,
|
||||
sourceNodeId: '42',
|
||||
sourceNodeId: asNodeId(42),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
})
|
||||
@@ -108,7 +112,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'outer-preview',
|
||||
sourceNodeId: '99',
|
||||
sourceNodeId: asNodeId(99),
|
||||
sourcePreviewName: 'inner-preview'
|
||||
}
|
||||
]
|
||||
@@ -118,7 +122,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'inner-preview',
|
||||
sourceNodeId: 'leaf-node',
|
||||
sourceNodeId: asNodeId(77),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
]
|
||||
@@ -127,7 +131,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
const ctx = makeContext(exposureMap, [
|
||||
{
|
||||
fromHostLocator: 'host-outer',
|
||||
fromSourceNodeId: '99',
|
||||
fromSourceNodeId: asNodeId(99),
|
||||
toRootGraphId: rootGraphA,
|
||||
toHostLocator: 'host-inner'
|
||||
}
|
||||
@@ -145,7 +149,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
expect(result?.steps[1].hostNodeLocator).toBe('host-inner')
|
||||
expect(result?.leaf).toEqual({
|
||||
rootGraphId: rootGraphA,
|
||||
sourceNodeId: 'leaf-node',
|
||||
sourceNodeId: asNodeId(77),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
})
|
||||
@@ -157,7 +161,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'p1',
|
||||
sourceNodeId: 'sub-a',
|
||||
sourceNodeId: asNodeId(10),
|
||||
sourcePreviewName: 'p2'
|
||||
}
|
||||
]
|
||||
@@ -167,7 +171,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'p2',
|
||||
sourceNodeId: 'sub-b',
|
||||
sourceNodeId: asNodeId(20),
|
||||
sourcePreviewName: 'p3'
|
||||
}
|
||||
]
|
||||
@@ -177,7 +181,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'p3',
|
||||
sourceNodeId: 'leaf',
|
||||
sourceNodeId: asNodeId(30),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
]
|
||||
@@ -186,13 +190,13 @@ describe(resolvePreviewExposureChain, () => {
|
||||
const ctx = makeContext(exposureMap, [
|
||||
{
|
||||
fromHostLocator: 'host-1',
|
||||
fromSourceNodeId: 'sub-a',
|
||||
fromSourceNodeId: asNodeId(10),
|
||||
toRootGraphId: rootGraphA,
|
||||
toHostLocator: 'host-2'
|
||||
},
|
||||
{
|
||||
fromHostLocator: 'host-2',
|
||||
fromSourceNodeId: 'sub-b',
|
||||
fromSourceNodeId: asNodeId(20),
|
||||
toRootGraphId: rootGraphB,
|
||||
toHostLocator: 'host-3'
|
||||
}
|
||||
@@ -208,7 +212,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
])
|
||||
expect(result?.leaf).toEqual({
|
||||
rootGraphId: rootGraphB,
|
||||
sourceNodeId: 'leaf',
|
||||
sourceNodeId: asNodeId(30),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
})
|
||||
@@ -220,7 +224,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
[
|
||||
{
|
||||
name: 'outer',
|
||||
sourceNodeId: '99',
|
||||
sourceNodeId: asNodeId(99),
|
||||
sourcePreviewName: 'missing-on-inner'
|
||||
}
|
||||
]
|
||||
@@ -230,7 +234,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
const ctx = makeContext(exposureMap, [
|
||||
{
|
||||
fromHostLocator: 'host-outer',
|
||||
fromSourceNodeId: '99',
|
||||
fromSourceNodeId: asNodeId(99),
|
||||
toRootGraphId: rootGraphA,
|
||||
toHostLocator: 'host-inner'
|
||||
}
|
||||
@@ -246,7 +250,7 @@ describe(resolvePreviewExposureChain, () => {
|
||||
expect(result?.steps).toHaveLength(1)
|
||||
expect(result?.leaf).toEqual({
|
||||
rootGraphId: rootGraphA,
|
||||
sourceNodeId: '99',
|
||||
sourceNodeId: asNodeId(99),
|
||||
sourcePreviewName: 'missing-on-inner'
|
||||
})
|
||||
})
|
||||
@@ -255,13 +259,19 @@ describe(resolvePreviewExposureChain, () => {
|
||||
const exposureMap = new Map<string, FixtureExposure[]>([
|
||||
[
|
||||
`${rootGraphA}|host-a`,
|
||||
[{ name: 'cyclic', sourceNodeId: 'sub', sourcePreviewName: 'cyclic' }]
|
||||
[
|
||||
{
|
||||
name: 'cyclic',
|
||||
sourceNodeId: asNodeId(40),
|
||||
sourcePreviewName: 'cyclic'
|
||||
}
|
||||
]
|
||||
]
|
||||
])
|
||||
const ctx = makeContext(exposureMap, [
|
||||
{
|
||||
fromHostLocator: 'host-a',
|
||||
fromSourceNodeId: 'sub',
|
||||
fromSourceNodeId: asNodeId(40),
|
||||
toRootGraphId: rootGraphA,
|
||||
toHostLocator: 'host-a'
|
||||
}
|
||||
@@ -278,6 +288,6 @@ describe(resolvePreviewExposureChain, () => {
|
||||
expect.stringContaining('cycle detected')
|
||||
)
|
||||
expect(result?.steps).toHaveLength(1)
|
||||
expect(result?.leaf.sourceNodeId).toBe('sub')
|
||||
expect(result?.leaf.sourceNodeId).toBe(asNodeId(40))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import type { PreviewExposure } from '@/core/schemas/previewExposureSchema'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
|
||||
type ChainPreviewExposure = Omit<PreviewExposure, 'sourceNodeId'> & {
|
||||
sourceNodeId: NodeId
|
||||
}
|
||||
|
||||
interface ResolvedPreviewChainStep {
|
||||
rootGraphId: UUID
|
||||
hostNodeLocator: string
|
||||
exposure: PreviewExposure
|
||||
exposure: ChainPreviewExposure
|
||||
}
|
||||
|
||||
export interface ResolvedPreviewChain {
|
||||
steps: readonly ResolvedPreviewChainStep[]
|
||||
leaf: {
|
||||
rootGraphId: UUID
|
||||
sourceNodeId: string
|
||||
sourceNodeId: NodeId
|
||||
sourcePreviewName: string
|
||||
}
|
||||
}
|
||||
@@ -20,11 +25,11 @@ export interface PreviewExposureChainContext {
|
||||
getExposures(
|
||||
rootGraphId: UUID,
|
||||
hostNodeLocator: string
|
||||
): readonly PreviewExposure[]
|
||||
): readonly ChainPreviewExposure[]
|
||||
resolveNestedHost(
|
||||
rootGraphId: UUID,
|
||||
hostNodeLocator: string,
|
||||
sourceNodeId: string
|
||||
sourceNodeId: NodeId
|
||||
): { rootGraphId: UUID; hostNodeLocator: string } | undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
import type { WidgetId } from '@/types/widgetId'
|
||||
|
||||
function promotedInputNames(host: {
|
||||
@@ -223,7 +224,7 @@ describe('pruneDisconnected', () => {
|
||||
subgraphNode.rootGraph.id,
|
||||
hostLocator,
|
||||
{
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceNodeId: interiorNode.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
)
|
||||
@@ -238,7 +239,7 @@ describe('pruneDisconnected', () => {
|
||||
).toEqual([
|
||||
{
|
||||
name: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceNodeId: interiorNode.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
])
|
||||
@@ -363,7 +364,7 @@ describe('promoteRecommendedWidgets', () => {
|
||||
).toEqual([
|
||||
{
|
||||
name: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
sourceNodeId: String(glslNode.id),
|
||||
sourceNodeId: glslNode.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
])
|
||||
@@ -408,7 +409,7 @@ describe('promoteRecommendedWidgets', () => {
|
||||
)
|
||||
).toContainEqual({
|
||||
name: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
sourceNodeId: String(glslNode.id),
|
||||
sourceNodeId: glslNode.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
expect(updatePreviewsMock).not.toHaveBeenCalled()
|
||||
@@ -468,7 +469,7 @@ describe('autoExposeKnownPreviewNodes', () => {
|
||||
subgraphNode.properties.previewExposures = [
|
||||
{
|
||||
name: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
sourceNodeId: String(otherNode.id),
|
||||
sourceNodeId: otherNode.id,
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
]
|
||||
@@ -479,7 +480,7 @@ describe('autoExposeKnownPreviewNodes', () => {
|
||||
usePreviewExposureStore()
|
||||
.getExposures(subgraphNode.rootGraph.id, String(subgraphNode.id))
|
||||
.map((e) => e.sourceNodeId)
|
||||
).not.toContain(String(glslNode.id))
|
||||
).not.toContain(glslNode.id)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -543,20 +544,20 @@ describe('isLinkedPromotion', () => {
|
||||
const host = createTestSubgraphNode(createTestSubgraph())
|
||||
const node = promoteSource(host, 'text')
|
||||
|
||||
expect(isLinkedPromotion(host, String(node.id), 'text')).toBe(true)
|
||||
expect(isLinkedPromotion(host, node.id, 'text')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when no promotion exists', () => {
|
||||
const host = createTestSubgraphNode(createTestSubgraph())
|
||||
|
||||
expect(isLinkedPromotion(host, '999', 'nonexistent')).toBe(false)
|
||||
expect(isLinkedPromotion(host, asNodeId(999), 'nonexistent')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when sourceWidgetName does not match', () => {
|
||||
const host = createTestSubgraphNode(createTestSubgraph())
|
||||
const node = promoteSource(host, 'text')
|
||||
|
||||
expect(isLinkedPromotion(host, String(node.id), 'wrong_name')).toBe(false)
|
||||
expect(isLinkedPromotion(host, node.id, 'wrong_name')).toBe(false)
|
||||
})
|
||||
|
||||
it('identifies linked widgets across different inputs', () => {
|
||||
@@ -564,10 +565,10 @@ describe('isLinkedPromotion', () => {
|
||||
const nodeA = promoteSource(host, 'string_a')
|
||||
const nodeB = promoteSource(host, 'value')
|
||||
|
||||
expect(isLinkedPromotion(host, String(nodeA.id), 'string_a')).toBe(true)
|
||||
expect(isLinkedPromotion(host, String(nodeB.id), 'value')).toBe(true)
|
||||
expect(isLinkedPromotion(host, String(nodeA.id), 'value')).toBe(false)
|
||||
expect(isLinkedPromotion(host, '5', 'string_a')).toBe(false)
|
||||
expect(isLinkedPromotion(host, nodeA.id, 'string_a')).toBe(true)
|
||||
expect(isLinkedPromotion(host, nodeB.id, 'value')).toBe(true)
|
||||
expect(isLinkedPromotion(host, nodeA.id, 'value')).toBe(false)
|
||||
expect(isLinkedPromotion(host, asNodeId(5), 'string_a')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -795,9 +796,9 @@ describe('demoteWidget — axiomatic projection retraction', () => {
|
||||
expect(host.subgraph.inputs).toHaveLength(1)
|
||||
expect(host.inputs[0]?.link).toBe(9999)
|
||||
expect(host.inputs[0]?._widget).toBeUndefined()
|
||||
expect(
|
||||
isLinkedPromotion(host, String(interiorNode.id), interiorWidget.name)
|
||||
).toBe(false)
|
||||
expect(isLinkedPromotion(host, interiorNode.id, interiorWidget.name)).toBe(
|
||||
false
|
||||
)
|
||||
expect(host.widgets).toHaveLength(0)
|
||||
if (!promotedInputId) throw new Error('Missing promoted input widgetId')
|
||||
expect(useWidgetValueStore().getWidget(promotedInputId)).toBeUndefined()
|
||||
@@ -821,8 +822,8 @@ describe('demoteWidget — axiomatic projection retraction', () => {
|
||||
demoteWidget(nodeB, widgetB, [host])
|
||||
|
||||
expect(host.subgraph.inputs.map((i) => i.name)).toEqual(['text'])
|
||||
expect(isLinkedPromotion(host, String(nodeB.id), widgetB.name)).toBe(false)
|
||||
expect(isLinkedPromotion(host, String(nodeA.id), widgetA.name)).toBe(true)
|
||||
expect(isLinkedPromotion(host, nodeB.id, widgetB.name)).toBe(false)
|
||||
expect(isLinkedPromotion(host, nodeA.id, widgetA.name)).toBe(true)
|
||||
})
|
||||
|
||||
it('demotes the correct slot when widget lives on a nested SubgraphNode with same-named deep sources', () => {
|
||||
@@ -849,12 +850,8 @@ describe('demoteWidget — axiomatic projection retraction', () => {
|
||||
demoteWidget(innerHost, promotedWidgetRef(innerHost, 'text_1'), [outerHost])
|
||||
|
||||
expect(outerHost.subgraph.inputs.map((i) => i.name)).toEqual(['text'])
|
||||
expect(isLinkedPromotion(outerHost, String(innerHost.id), 'text_1')).toBe(
|
||||
false
|
||||
)
|
||||
expect(isLinkedPromotion(outerHost, String(innerHost.id), 'text')).toBe(
|
||||
true
|
||||
)
|
||||
expect(isLinkedPromotion(outerHost, innerHost.id, 'text_1')).toBe(false)
|
||||
expect(isLinkedPromotion(outerHost, innerHost.id, 'text')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import type { WidgetId } from '@/types/widgetId'
|
||||
import { widgetId } from '@/types/widgetId'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
type PartialNode = Pick<LGraphNode, 'title' | 'id' | 'type'>
|
||||
|
||||
@@ -33,7 +34,7 @@ export function getWidgetName(w: IBaseWidget): string {
|
||||
|
||||
export function isLinkedPromotion(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceNodeId: NodeId,
|
||||
sourceWidgetName: string
|
||||
): boolean {
|
||||
return (
|
||||
@@ -44,7 +45,7 @@ export function isLinkedPromotion(
|
||||
|
||||
export function findHostInputForPromotion(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceNodeId: NodeId,
|
||||
sourceWidgetName: string
|
||||
) {
|
||||
return subgraphNode.inputs.find((input) => {
|
||||
@@ -74,7 +75,7 @@ function resolvePromotionSource(
|
||||
|
||||
if (inputNode.isSubgraphNode()) {
|
||||
return {
|
||||
sourceNodeId: String(inputNode.id),
|
||||
sourceNodeId: inputNode.id,
|
||||
sourceWidgetName: targetInput.name
|
||||
}
|
||||
}
|
||||
@@ -83,7 +84,7 @@ function resolvePromotionSource(
|
||||
if (!targetWidget) continue
|
||||
|
||||
return {
|
||||
sourceNodeId: String(inputNode.id),
|
||||
sourceNodeId: inputNode.id,
|
||||
sourceWidgetName: targetWidget.name
|
||||
}
|
||||
}
|
||||
@@ -212,7 +213,7 @@ function toPromotionSource(
|
||||
widget: IBaseWidget
|
||||
): PromotedWidgetSource {
|
||||
return {
|
||||
sourceNodeId: String(node.id),
|
||||
sourceNodeId: node.id,
|
||||
sourceWidgetName: getWidgetName(widget)
|
||||
}
|
||||
}
|
||||
@@ -235,9 +236,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 }
|
||||
}
|
||||
|
||||
@@ -309,13 +308,13 @@ function promotePreviewViaExposure(
|
||||
.getExposures(rootGraphId, hostLocator)
|
||||
.some(
|
||||
(exposure) =>
|
||||
exposure.sourceNodeId === String(sourceNode.id) &&
|
||||
exposure.sourceNodeId === sourceNode.id &&
|
||||
exposure.sourcePreviewName === sourcePreviewName
|
||||
)
|
||||
if (existing) return
|
||||
|
||||
store.addExposure(rootGraphId, hostLocator, {
|
||||
sourceNodeId: String(sourceNode.id),
|
||||
sourceNodeId: sourceNode.id,
|
||||
sourcePreviewName
|
||||
})
|
||||
}
|
||||
@@ -603,7 +602,7 @@ export function pruneDisconnected(subgraphNode: SubgraphNode) {
|
||||
if (!hostInput?.widgetId && !hostInput?._widget) return false
|
||||
|
||||
removedEntries.push({
|
||||
sourceNodeId: String(subgraphNode.id),
|
||||
sourceNodeId: subgraphNode.id,
|
||||
sourceWidgetName: input.name
|
||||
})
|
||||
return true
|
||||
@@ -642,7 +641,7 @@ export function hasUnpromotedWidgets(subgraphNode: SubgraphNode): boolean {
|
||||
!isWidgetPromotedOnSubgraphNode(
|
||||
subgraphNode,
|
||||
{
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceNodeId: interiorNode.id,
|
||||
sourceWidgetName: widget.name
|
||||
},
|
||||
widget
|
||||
|
||||
@@ -4,7 +4,7 @@ import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { resolveConcretePromotedWidget } from '@/core/graph/subgraph/resolveConcretePromotedWidget'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
@@ -23,7 +23,7 @@ vi.mock('@/services/litegraphService', () => ({
|
||||
}))
|
||||
|
||||
function createHostNode(id: number): SubgraphNode {
|
||||
return createTestSubgraphNode(createTestSubgraph(), { id })
|
||||
return createTestSubgraphNode(createTestSubgraph(), { id: asNodeId(id) })
|
||||
}
|
||||
|
||||
function addNodeToHost(host: SubgraphNode, title: string): LGraphNode {
|
||||
@@ -46,11 +46,7 @@ describe('resolveConcretePromotedWidget', () => {
|
||||
const concreteNode = addNodeToHost(host, 'leaf')
|
||||
addConcreteWidget(concreteNode, 'seed')
|
||||
|
||||
const result = resolveConcretePromotedWidget(
|
||||
host,
|
||||
String(concreteNode.id),
|
||||
'seed'
|
||||
)
|
||||
const result = resolveConcretePromotedWidget(host, concreteNode.id, 'seed')
|
||||
|
||||
expect(result.status).toBe('resolved')
|
||||
if (result.status !== 'resolved') return
|
||||
@@ -71,7 +67,9 @@ describe('resolveConcretePromotedWidget', () => {
|
||||
innerSubgraph.add(leaf)
|
||||
innerSubgraph.inputNode.slots[0].connect(leafInput, leaf)
|
||||
|
||||
const innerNode = createTestSubgraphNode(innerSubgraph, { id: 11 })
|
||||
const innerNode = createTestSubgraphNode(innerSubgraph, {
|
||||
id: asNodeId(11)
|
||||
})
|
||||
|
||||
const outerSubgraph = createTestSubgraph({
|
||||
inputs: [{ name: 'y', type: '*' }]
|
||||
@@ -80,13 +78,11 @@ describe('resolveConcretePromotedWidget', () => {
|
||||
innerNode._internalConfigureAfterSlots()
|
||||
outerSubgraph.inputNode.slots[0].connect(innerNode.inputs[0], innerNode)
|
||||
|
||||
const outerNode = createTestSubgraphNode(outerSubgraph, { id: 22 })
|
||||
const outerNode = createTestSubgraphNode(outerSubgraph, {
|
||||
id: asNodeId(22)
|
||||
})
|
||||
|
||||
const result = resolveConcretePromotedWidget(
|
||||
outerNode,
|
||||
String(innerNode.id),
|
||||
'x'
|
||||
)
|
||||
const result = resolveConcretePromotedWidget(outerNode, innerNode.id, 'x')
|
||||
|
||||
expect(result.status).toBe('resolved')
|
||||
if (result.status !== 'resolved') return
|
||||
@@ -98,7 +94,7 @@ describe('resolveConcretePromotedWidget', () => {
|
||||
test('returns cycle when nested promoted widget traversal revisits the same input', () => {
|
||||
const recursiveInput = { name: 'x', link: 1 }
|
||||
const recursiveNode = fromAny<LGraphNode, unknown>({
|
||||
id: 11,
|
||||
id: asNodeId(11),
|
||||
inputs: [recursiveInput],
|
||||
isSubgraphNode: () => true,
|
||||
subgraph: {
|
||||
@@ -106,17 +102,17 @@ describe('resolveConcretePromotedWidget', () => {
|
||||
getLink: () => ({
|
||||
resolve: () => ({ inputNode: recursiveNode })
|
||||
}),
|
||||
getNodeById: () => recursiveNode
|
||||
getNodeByRawId: () => recursiveNode
|
||||
}
|
||||
})
|
||||
const host = fromAny<SubgraphNode, unknown>({
|
||||
isSubgraphNode: () => true,
|
||||
subgraph: {
|
||||
getNodeById: () => recursiveNode
|
||||
getNodeByRawId: () => recursiveNode
|
||||
}
|
||||
})
|
||||
|
||||
const result = resolveConcretePromotedWidget(host, '11', 'x')
|
||||
const result = resolveConcretePromotedWidget(host, asNodeId(11), 'x')
|
||||
|
||||
expect(result).toEqual({ status: 'failure', failure: 'cycle' })
|
||||
})
|
||||
@@ -129,15 +125,15 @@ describe('resolveConcretePromotedWidget', () => {
|
||||
for (let index = 0; index < subgraphs.length - 1; index++) {
|
||||
const current = subgraphs[index]
|
||||
const next = subgraphs[index + 1]
|
||||
const nextNode = createTestSubgraphNode(next, { id: index + 1 })
|
||||
const nextNode = createTestSubgraphNode(next, { id: asNodeId(index + 1) })
|
||||
current.add(nextNode)
|
||||
nextNode._internalConfigureAfterSlots()
|
||||
current.inputNode.slots[0].connect(nextNode.inputs[0], nextNode)
|
||||
}
|
||||
|
||||
const host = createTestSubgraphNode(subgraphs[0], { id: 200 })
|
||||
const host = createTestSubgraphNode(subgraphs[0], { id: asNodeId(200) })
|
||||
|
||||
const result = resolveConcretePromotedWidget(host, '1', 'x')
|
||||
const result = resolveConcretePromotedWidget(host, asNodeId(1), 'x')
|
||||
expect(result).toEqual({
|
||||
status: 'failure',
|
||||
failure: 'max-depth-exceeded'
|
||||
@@ -147,7 +143,7 @@ describe('resolveConcretePromotedWidget', () => {
|
||||
test('returns invalid-host for non-subgraph host node', () => {
|
||||
const host = new LGraphNode('plain-host')
|
||||
|
||||
const result = resolveConcretePromotedWidget(host, 'x', 'y')
|
||||
const result = resolveConcretePromotedWidget(host, asNodeId(1), 'y')
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'failure',
|
||||
@@ -158,7 +154,7 @@ describe('resolveConcretePromotedWidget', () => {
|
||||
test('returns missing-node when source node does not exist in host subgraph', () => {
|
||||
const host = createHostNode(100)
|
||||
|
||||
const result = resolveConcretePromotedWidget(host, 'missing-node', 'seed')
|
||||
const result = resolveConcretePromotedWidget(host, asNodeId(999), 'seed')
|
||||
|
||||
expect(result).toEqual({
|
||||
status: 'failure',
|
||||
@@ -173,7 +169,7 @@ describe('resolveConcretePromotedWidget', () => {
|
||||
|
||||
const result = resolveConcretePromotedWidget(
|
||||
host,
|
||||
String(sourceNode.id),
|
||||
sourceNode.id,
|
||||
'missing-widget'
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 type { NodeId } from '@/types/nodeId'
|
||||
|
||||
type PromotedWidgetResolutionFailure =
|
||||
| 'invalid-host'
|
||||
@@ -18,7 +19,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>>()
|
||||
@@ -35,7 +36,7 @@ function traversePromotedWidgetChain(
|
||||
visited.add(key)
|
||||
visitedByHost.set(currentHost, visited)
|
||||
|
||||
const sourceNode = currentHost.subgraph.getNodeById(currentNodeId)
|
||||
const sourceNode = currentHost.subgraph.getNodeByRawId(currentNodeId)
|
||||
if (!sourceNode) {
|
||||
return { status: 'failure', failure: 'missing-node' }
|
||||
}
|
||||
@@ -69,7 +70,7 @@ function traversePromotedWidgetChain(
|
||||
|
||||
export function resolveConcretePromotedWidget(
|
||||
hostNode: LGraphNode,
|
||||
nodeId: string,
|
||||
nodeId: NodeId,
|
||||
widgetName: string
|
||||
): PromotedWidgetResolutionResult {
|
||||
if (!hostNode.isSubgraphNode()) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { resolveSubgraphInputLink } from '@/core/graph/subgraph/resolveSubgraphInputLink'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode,
|
||||
@@ -30,7 +30,7 @@ function createSubgraphSetup(inputName: string): {
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [{ name: inputName, type: '*' }]
|
||||
})
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: 1 })
|
||||
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(1) })
|
||||
return { subgraph, subgraphNode }
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
@@ -28,7 +28,9 @@ function createOuterSubgraphSetup(inputNames: string[]): {
|
||||
const outerSubgraph = createTestSubgraph({
|
||||
inputs: inputNames.map((name) => ({ name, type: '*' }))
|
||||
})
|
||||
const outerSubgraphNode = createTestSubgraphNode(outerSubgraph, { id: 1 })
|
||||
const outerSubgraphNode = createTestSubgraphNode(outerSubgraph, {
|
||||
id: asNodeId(1)
|
||||
})
|
||||
return { outerSubgraph, outerSubgraphNode }
|
||||
}
|
||||
|
||||
@@ -41,7 +43,9 @@ function addLinkedNestedSubgraphNode(
|
||||
const innerSubgraph = createTestSubgraph({
|
||||
inputs: [{ name: linkedInputName, type: '*' }]
|
||||
})
|
||||
const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, { id: 819 })
|
||||
const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, {
|
||||
id: asNodeId(819)
|
||||
})
|
||||
outerSubgraph.add(innerSubgraphNode)
|
||||
|
||||
const inputSlot = outerSubgraph.inputNode.slots.find(
|
||||
@@ -139,7 +143,7 @@ describe('resolveSubgraphInputTarget', () => {
|
||||
(slot) => slot.name === 'seed'
|
||||
)!
|
||||
const node = new LGraphNode('Interior-seed')
|
||||
node.id = 42
|
||||
node.id = asNodeId(42)
|
||||
const input = node.addInput('seed_input', '*')
|
||||
node.addWidget('number', 'seed', 0, () => undefined)
|
||||
input.widget = { name: 'seed' }
|
||||
@@ -185,7 +189,7 @@ describe('resolveSubgraphInputTarget', () => {
|
||||
]
|
||||
})
|
||||
const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, {
|
||||
id: 820
|
||||
id: asNodeId(820)
|
||||
})
|
||||
outerSubgraph.add(innerSubgraphNode)
|
||||
|
||||
@@ -224,7 +228,7 @@ describe('resolveSubgraphInputTarget', () => {
|
||||
inputs: [{ name: 'seed', type: '*' }]
|
||||
})
|
||||
const concreteNode = new LGraphNode('ConcreteNode')
|
||||
concreteNode.id = 900
|
||||
concreteNode.id = asNodeId(900)
|
||||
const concreteInput = concreteNode.addInput('seed_input', '*')
|
||||
concreteNode.addWidget('number', 'seed', 0, () => undefined)
|
||||
concreteInput.widget = { name: 'seed' }
|
||||
@@ -235,7 +239,7 @@ describe('resolveSubgraphInputTarget', () => {
|
||||
inputs: [{ name: 'seed', type: '*' }]
|
||||
})
|
||||
const innerSubgraphNode = createTestSubgraphNode(innerSubgraph, {
|
||||
id: 901
|
||||
id: asNodeId(901)
|
||||
})
|
||||
middleSubgraph.add(innerSubgraphNode)
|
||||
const middleInput = innerSubgraphNode.addInput('seed', '*')
|
||||
@@ -247,7 +251,7 @@ describe('resolveSubgraphInputTarget', () => {
|
||||
'seed'
|
||||
])
|
||||
const middleSubgraphNode = createTestSubgraphNode(middleSubgraph, {
|
||||
id: 902
|
||||
id: asNodeId(902)
|
||||
})
|
||||
outerSubgraph.add(middleSubgraphNode)
|
||||
const outerInput = middleSubgraphNode.addInput('seed', '*')
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
import { resolveSubgraphInputLink } from './resolveSubgraphInputLink'
|
||||
|
||||
type ResolvedSubgraphInputTarget = {
|
||||
nodeId: string
|
||||
nodeId: NodeId
|
||||
widgetName: string
|
||||
}
|
||||
|
||||
@@ -17,7 +18,7 @@ export function resolveSubgraphInputTarget(
|
||||
({ inputNode, targetInput, getTargetWidget }) => {
|
||||
if (inputNode.isSubgraphNode()) {
|
||||
return {
|
||||
nodeId: String(inputNode.id),
|
||||
nodeId: inputNode.id,
|
||||
widgetName: targetInput.name
|
||||
}
|
||||
}
|
||||
@@ -26,7 +27,7 @@ export function resolveSubgraphInputTarget(
|
||||
if (!targetWidget) return undefined
|
||||
|
||||
return {
|
||||
nodeId: String(inputNode.id),
|
||||
nodeId: inputNode.id,
|
||||
widgetName: targetWidget.name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,18 @@ describe(parsePreviewExposures, () => {
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
]
|
||||
expect(parsePreviewExposures(input)).toEqual(input)
|
||||
expect(parsePreviewExposures(input)).toEqual([
|
||||
{
|
||||
name: 'preview',
|
||||
sourceNodeId: 5,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
},
|
||||
{
|
||||
name: 'preview2',
|
||||
sourceNodeId: 7,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('parses JSON-string input', () => {
|
||||
@@ -27,7 +38,13 @@ describe(parsePreviewExposures, () => {
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
]
|
||||
expect(parsePreviewExposures(JSON.stringify(input))).toEqual(input)
|
||||
expect(parsePreviewExposures(JSON.stringify(input))).toEqual([
|
||||
{
|
||||
name: 'preview',
|
||||
sourceNodeId: 5,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('returns empty array for undefined', () => {
|
||||
@@ -65,7 +82,7 @@ describe(parsePreviewExposures, () => {
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('returns empty array when entries have wrong types', () => {
|
||||
it('returns empty array when entries have wrong types or invalid node ids', () => {
|
||||
expect(
|
||||
parsePreviewExposures([
|
||||
{
|
||||
@@ -79,7 +96,7 @@ describe(parsePreviewExposures, () => {
|
||||
parsePreviewExposures([
|
||||
{
|
||||
name: 'preview',
|
||||
sourceNodeId: 5,
|
||||
sourceNodeId: 'not-a-node-id',
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
])
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { z } from 'zod'
|
||||
import { fromZodError } from 'zod-validation-error'
|
||||
|
||||
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
|
||||
import { parseNodePropertyArray } from './parseNodePropertyArray'
|
||||
import { tryAsNodeId } from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
|
||||
const previewExposureSchema = z.object({
|
||||
name: z.string(),
|
||||
sourceNodeId: z.string(),
|
||||
sourceNodeId: z
|
||||
.union([z.number(), z.string()])
|
||||
.transform(tryAsNodeId)
|
||||
.refine((value): value is NodeId => value !== null, 'Invalid NodeId'),
|
||||
sourcePreviewName: z.string()
|
||||
})
|
||||
export type PreviewExposure = z.infer<typeof previewExposureSchema>
|
||||
@@ -16,9 +20,20 @@ const previewExposuresPropertySchema = z.array(previewExposureSchema)
|
||||
export function parsePreviewExposures(
|
||||
property: NodeProperty | undefined
|
||||
): PreviewExposure[] {
|
||||
return parseNodePropertyArray(
|
||||
property,
|
||||
previewExposuresPropertySchema,
|
||||
'properties.previewExposures'
|
||||
)
|
||||
if (property === undefined) return []
|
||||
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = typeof property === 'string' ? JSON.parse(property) : property
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse properties.previewExposures:', e)
|
||||
return []
|
||||
}
|
||||
|
||||
const result = previewExposuresPropertySchema.safeParse(parsed)
|
||||
if (result.success) return result.data
|
||||
|
||||
const error = fromZodError(result.error)
|
||||
console.warn(`Invalid assignment for properties.previewExposures:\n${error}`)
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink'
|
||||
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { asNodeId } from '@/types/nodeId'
|
||||
|
||||
import type { GroupNodeWorkflowData } from './groupNode'
|
||||
|
||||
@@ -15,7 +16,7 @@ import { GroupNodeConfig, replaceLegacySeparators } from './groupNode'
|
||||
|
||||
function makeNode(type: string): ComfyNode {
|
||||
return {
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
type,
|
||||
pos: [0, 0],
|
||||
size: [1, 1],
|
||||
@@ -66,8 +67,22 @@ describe('GroupNodeConfig.getLinks', () => {
|
||||
}
|
||||
|
||||
it('indexes outgoing links by [origin index][origin slot]', () => {
|
||||
const clip = [1, 1, 2, 0, 4, 'CLIP'] satisfies SerialisedLLinkArray
|
||||
const model = [1, 0, 4, 0, 4, 'MODEL'] satisfies SerialisedLLinkArray
|
||||
const clip = [
|
||||
1,
|
||||
asNodeId(1),
|
||||
2,
|
||||
asNodeId(0),
|
||||
4,
|
||||
'CLIP'
|
||||
] satisfies SerialisedLLinkArray
|
||||
const model = [
|
||||
1,
|
||||
asNodeId(0),
|
||||
4,
|
||||
asNodeId(0),
|
||||
4,
|
||||
'MODEL'
|
||||
] satisfies SerialisedLLinkArray
|
||||
const config = configFrom([clip, model])
|
||||
|
||||
expect(config.linksFrom[1][1]).toEqual([clip])
|
||||
@@ -75,8 +90,22 @@ describe('GroupNodeConfig.getLinks', () => {
|
||||
})
|
||||
|
||||
it('indexes incoming links by [target index][target slot]', () => {
|
||||
const clip = [1, 1, 2, 0, 4, 'CLIP'] satisfies SerialisedLLinkArray
|
||||
const cond = [2, 0, 4, 1, 6, 'CONDITIONING'] satisfies SerialisedLLinkArray
|
||||
const clip = [
|
||||
1,
|
||||
asNodeId(1),
|
||||
2,
|
||||
asNodeId(0),
|
||||
4,
|
||||
'CLIP'
|
||||
] satisfies SerialisedLLinkArray
|
||||
const cond = [
|
||||
2,
|
||||
asNodeId(0),
|
||||
4,
|
||||
asNodeId(1),
|
||||
6,
|
||||
'CONDITIONING'
|
||||
] satisfies SerialisedLLinkArray
|
||||
const config = configFrom([clip, cond])
|
||||
|
||||
expect(config.linksTo[2][0]).toEqual(clip)
|
||||
@@ -84,15 +113,36 @@ describe('GroupNodeConfig.getLinks', () => {
|
||||
})
|
||||
|
||||
it('accumulates multiple fan-out links from the same origin slot', () => {
|
||||
const toPos = [1, 1, 2, 0, 4, 'CLIP'] satisfies SerialisedLLinkArray
|
||||
const toNeg = [1, 1, 3, 0, 5, 'CLIP'] satisfies SerialisedLLinkArray
|
||||
const toPos = [
|
||||
1,
|
||||
asNodeId(1),
|
||||
2,
|
||||
asNodeId(0),
|
||||
4,
|
||||
'CLIP'
|
||||
] satisfies SerialisedLLinkArray
|
||||
const toNeg = [
|
||||
1,
|
||||
asNodeId(1),
|
||||
3,
|
||||
asNodeId(0),
|
||||
5,
|
||||
'CLIP'
|
||||
] satisfies SerialisedLLinkArray
|
||||
const config = configFrom([toPos, toNeg])
|
||||
|
||||
expect(config.linksFrom[1][1]).toEqual([toPos, toNeg])
|
||||
})
|
||||
|
||||
it('skips links that have a null endpoint', () => {
|
||||
const valid = [1, 1, 2, 0, 4, 'CLIP'] satisfies SerialisedLLinkArray
|
||||
const valid = [
|
||||
1,
|
||||
asNodeId(1),
|
||||
2,
|
||||
asNodeId(0),
|
||||
4,
|
||||
'CLIP'
|
||||
] satisfies SerialisedLLinkArray
|
||||
const broken = [null, 1, 2, 0, 4, 'CLIP'] as unknown as SerialisedLLinkArray
|
||||
const config = configFrom([valid, broken])
|
||||
|
||||
|
||||
@@ -851,7 +851,7 @@ class GroupNodeHandler {
|
||||
const selectedIds = Object.keys(app.canvas.selected_nodes)
|
||||
const newNodes: LGraphNode[] = []
|
||||
for (let i = 0; i < selectedIds.length; i++) {
|
||||
const newNode = app.rootGraph.getNodeById(selectedIds[i])
|
||||
const newNode = app.rootGraph.getNodeByRawId(selectedIds[i])
|
||||
const innerNodeData = nodeData.nodes[i]
|
||||
if (!newNode) continue
|
||||
newNodes.push(newNode)
|
||||
@@ -905,7 +905,7 @@ class GroupNodeHandler {
|
||||
|
||||
const reconnectInputs = (selectedIds: (string | number)[]) => {
|
||||
for (const innerNodeIndex in oldToNewInputMap) {
|
||||
const newNode = app.rootGraph.getNodeById(
|
||||
const newNode = app.rootGraph.getNodeByRawId(
|
||||
selectedIds[Number(innerNodeIndex)]
|
||||
)
|
||||
if (!newNode) continue
|
||||
@@ -917,7 +917,7 @@ class GroupNodeHandler {
|
||||
if (slot.link == null) continue
|
||||
const link = app.rootGraph.links[slot.link]
|
||||
if (!link) continue
|
||||
const originNode = app.rootGraph.getNodeById(link.origin_id)
|
||||
const originNode = app.rootGraph.getNodeByRawId(link.origin_id)
|
||||
originNode?.connect(link.origin_slot, newNode, +innerInputId)
|
||||
}
|
||||
}
|
||||
@@ -937,8 +937,8 @@ class GroupNodeHandler {
|
||||
if (!slot) continue
|
||||
const link = app.rootGraph.links[l]
|
||||
if (!link) continue
|
||||
const targetNode = app.rootGraph.getNodeById(link.target_id)
|
||||
const newNode = app.rootGraph.getNodeById(
|
||||
const targetNode = app.rootGraph.getNodeByRawId(link.target_id)
|
||||
const newNode = app.rootGraph.getNodeByRawId(
|
||||
selectedIds[slot.node.index ?? 0]
|
||||
)
|
||||
if (targetNode) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
INodeOutputSlot,
|
||||
LLink
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { createMockLLink } from '@/utils/__tests__/litegraphTestUtils'
|
||||
@@ -39,7 +40,7 @@ function createTargetNode(
|
||||
id = 7
|
||||
): Pick<LGraphNode, 'id' | 'inputs' | 'widgets'> {
|
||||
return fromPartial<Pick<LGraphNode, 'id' | 'inputs' | 'widgets'>>({
|
||||
id,
|
||||
id: asNodeId(id),
|
||||
inputs: [
|
||||
fromPartial<INodeInputSlot>({
|
||||
widget: { name: widget.name }
|
||||
|
||||
@@ -2,8 +2,9 @@ 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 {
|
||||
asNodeId,
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
@@ -142,6 +143,25 @@ describe('LGraph', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('NodeId lookup typing', () => {
|
||||
it('requires branded NodeId for runtime node lookup', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('Test')
|
||||
graph.add(node)
|
||||
|
||||
expect(graph.getNodeById(node.id)).toBe(node)
|
||||
expect(graph.getNodeById(asNodeId(node.id))).toBe(node)
|
||||
expect(graph.getNodeById(asNodeId(999))).toBeNull()
|
||||
expect(graph.getNodeByRawId(999)).toBeNull()
|
||||
|
||||
// @ts-expect-error raw numeric ids must be normalized at boundaries first
|
||||
graph.getNodeById(1)
|
||||
|
||||
// @ts-expect-error raw string ids must be normalized at boundaries first
|
||||
graph.getNodeById('1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Floating Links / Reroutes', () => {
|
||||
test('Floating reroute should be removed when node and link are removed', ({
|
||||
expect,
|
||||
@@ -297,7 +317,7 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
})
|
||||
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
const seedWidgetId = widgetId(graphId, '10' as NodeId, 'seed')
|
||||
const seedWidgetId = widgetId(graphId, asNodeId('10'), 'seed')
|
||||
widgetValueStore.registerWidget(seedWidgetId, {
|
||||
type: 'number',
|
||||
value: 1,
|
||||
@@ -461,6 +481,47 @@ describe('Shared LGraphState', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('configure with arbitrary string node ids', () => {
|
||||
class StringIdNode extends LGraphNode {
|
||||
constructor() {
|
||||
super('StringIdNode')
|
||||
this.addOutput('out', 'STRING')
|
||||
this.addInput('in', 'STRING')
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
LiteGraph.registerNodeType('test/string-id', StringIdNode)
|
||||
})
|
||||
|
||||
it('remaps string ids to numeric and preserves link connectivity', () => {
|
||||
const graph = new LGraph()
|
||||
graph.configure({
|
||||
id: zeroUuid,
|
||||
revision: 0,
|
||||
version: 0.4,
|
||||
config: {},
|
||||
last_node_id: 0,
|
||||
last_link_id: 18,
|
||||
groups: [],
|
||||
nodes: [
|
||||
{ id: 'Source.0', type: 'test/string-id' },
|
||||
{ id: 'Sink.0', type: 'test/string-id' }
|
||||
],
|
||||
links: [[10, 'Source.0', 0, 'Sink.0', 0, 'STRING']]
|
||||
} as unknown as SerialisableGraph)
|
||||
|
||||
expect(graph._nodes).toHaveLength(2)
|
||||
expect(graph._nodes.every((n) => typeof n.id === 'number')).toBe(true)
|
||||
|
||||
const [source, sink] = graph._nodes
|
||||
const link = [...graph._links.values()][0]
|
||||
expect(link.origin_id).toBe(source.id)
|
||||
expect(link.target_id).toBe(sink.id)
|
||||
expect(graph.getNodeById(source.id)).toBe(source)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensureGlobalIdUniqueness', () => {
|
||||
function createSubgraphOnGraph(rootGraph: LGraph): Subgraph {
|
||||
const data = createTestSubgraphData()
|
||||
@@ -483,7 +544,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', () => {
|
||||
@@ -519,7 +580,7 @@ describe('ensureGlobalIdUniqueness', () => {
|
||||
rootGraph.ensureGlobalIdUniqueness()
|
||||
|
||||
expect(rootGraph.state.lastNodeId).toBeGreaterThanOrEqual(
|
||||
subNode.id as number
|
||||
Number(subNode.id)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -536,7 +597,7 @@ describe('ensureGlobalIdUniqueness', () => {
|
||||
subgraph._nodes_by_id[subNodeA.id] = subNodeA
|
||||
|
||||
const subNodeB = new DummyNode()
|
||||
subNodeB.id = 999
|
||||
subNodeB.id = asNodeId(999)
|
||||
subgraph._nodes.push(subNodeB)
|
||||
subgraph._nodes_by_id[subNodeB.id] = subNodeB
|
||||
|
||||
@@ -555,13 +616,14 @@ describe('ensureGlobalIdUniqueness', () => {
|
||||
const subgraph = createSubgraphOnGraph(rootGraph)
|
||||
|
||||
const subNode = new DummyNode()
|
||||
subNode.id = 42
|
||||
subNode.id = asNodeId(42)
|
||||
subgraph._nodes.push(subNode)
|
||||
subgraph._nodes_by_id[subNode.id] = subNode
|
||||
|
||||
rootGraph.ensureGlobalIdUniqueness([42])
|
||||
|
||||
expect(subNode.id).not.toBe(42)
|
||||
expect(subNode.id).not.toBe(asNodeId(42))
|
||||
expect(subgraph._nodes_by_id[asNodeId(42)]).toBeUndefined()
|
||||
expect(subgraph._nodes_by_id[subNode.id]).toBe(subNode)
|
||||
})
|
||||
|
||||
@@ -867,7 +929,7 @@ describe('Subgraph Unpacking', () => {
|
||||
|
||||
const firstInstance = createTestSubgraphNode(subgraph, { pos: [100, 100] })
|
||||
const secondInstance = createTestSubgraphNode(subgraph, { pos: [300, 100] })
|
||||
secondInstance.id = 2
|
||||
secondInstance.id = asNodeId(2)
|
||||
rootGraph.add(firstInstance)
|
||||
rootGraph.add(secondInstance)
|
||||
|
||||
@@ -915,7 +977,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(asNodeId(id))).toBe(true)
|
||||
}
|
||||
for (const id of idsA) {
|
||||
expect(idsB.has(id)).toBe(false)
|
||||
@@ -927,8 +989,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)
|
||||
expect(idsB.has(asNodeId(link.origin_id))).toBe(true)
|
||||
expect(idsB.has(asNodeId(link.target_id))).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -951,14 +1013,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(asNodeId(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(asNodeId(103))?.properties?.proxyWidgets
|
||||
expect(Array.isArray(pw103)).toBe(true)
|
||||
for (const entry of pw103 as unknown[][]) {
|
||||
expect(Array.isArray(entry)).toBe(true)
|
||||
@@ -976,7 +1038,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 === asNodeId(50))
|
||||
const pw = innerNode?.properties?.proxyWidgets
|
||||
expect(Array.isArray(pw)).toBe(true)
|
||||
for (const entry of pw as unknown[][]) {
|
||||
@@ -1031,15 +1093,19 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
|
||||
expect(() => {
|
||||
const graph = new LGraph()
|
||||
graph.configure(structuredClone(nodeIdSpaceExhausted))
|
||||
}).toThrow('Node ID space exhausted')
|
||||
}).toThrow('LiteGraph: node ID space exhausted')
|
||||
})
|
||||
|
||||
it('is a no-op when subgraph node IDs are already unique', () => {
|
||||
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([asNodeId(10), asNodeId(11), asNodeId(12)])
|
||||
)
|
||||
expect(nodeIdSet(graph, SUBGRAPH_B)).toEqual(
|
||||
new Set([asNodeId(20), asNodeId(21), asNodeId(22)])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { toString } from 'es-toolkit/compat'
|
||||
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
} from '@/lib/litegraph/src/constants'
|
||||
import { isNodeBindable } from '@/lib/litegraph/src/utils/type'
|
||||
import type { UUID } from '@/utils/uuid'
|
||||
import { createUuidv4, zeroUuid } from '@/utils/uuid'
|
||||
@@ -19,6 +15,8 @@ import {
|
||||
repairInputLinks,
|
||||
selectSurvivorLink
|
||||
} from './linkDeduplication'
|
||||
import { nextFreeNodeId } from './nodeIdAllocation'
|
||||
import { normalizeStringNodeIds } from './nodeIdNormalization'
|
||||
|
||||
import type { DragAndScaleState } from './DragAndScale'
|
||||
import { LGraphCanvas } from './LGraphCanvas'
|
||||
@@ -87,6 +85,18 @@ import type {
|
||||
SerialisableReroute
|
||||
} from './types/serialisation'
|
||||
import { getAllNestedItems } from './utils/collections'
|
||||
import {
|
||||
SUBGRAPH_INPUT_NODE_ID,
|
||||
SUBGRAPH_OUTPUT_NODE_ID,
|
||||
asNodeId,
|
||||
isFloatingNodeId,
|
||||
isNumericNodeId,
|
||||
isSubgraphInputNodeId,
|
||||
isSubgraphOutputNodeId,
|
||||
isUnassignedNodeId,
|
||||
nodeIdToNumber
|
||||
} from '@/types/nodeId'
|
||||
import type { NodeIdInput } from '@/types/nodeId'
|
||||
import {
|
||||
deduplicateSubgraphNodeIds,
|
||||
topologicalSortSubgraphs
|
||||
@@ -632,7 +642,7 @@ export class LGraph
|
||||
const S: LGraphNode[] = []
|
||||
const M: Dictionary<LGraphNode> = {}
|
||||
// to avoid repeating links
|
||||
const visited_links: Record<NodeId, boolean> = {}
|
||||
const visited_links: Record<LinkId, boolean> = {}
|
||||
const remaining_links: Record<NodeId, number> = {}
|
||||
|
||||
// search for the nodes without inputs (starting nodes)
|
||||
@@ -949,11 +959,11 @@ export class LGraph
|
||||
}
|
||||
|
||||
// nodes
|
||||
if (node.id != -1 && this._nodes_by_id[node.id] != null) {
|
||||
if (!isUnassignedNodeId(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 = asNodeId(++state.lastNodeId)
|
||||
}
|
||||
|
||||
if (this._nodes.length >= LiteGraph.MAX_NUMBER_OF_NODES) {
|
||||
@@ -961,10 +971,13 @@ 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 (isUnassignedNodeId(node.id)) {
|
||||
node.id = asNodeId(++state.lastNodeId)
|
||||
} else if (
|
||||
isNumericNodeId(node.id) &&
|
||||
state.lastNodeId < nodeIdToNumber(node.id)
|
||||
) {
|
||||
state.lastNodeId = nodeIdToNumber(node.id)
|
||||
}
|
||||
|
||||
// Set ghost flag before registration so VueNodeData picks it up
|
||||
@@ -1129,7 +1142,11 @@ 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
|
||||
return id != null ? (this._nodes_by_id[id] ?? null) : null
|
||||
}
|
||||
|
||||
getNodeByRawId(id: NodeIdInput | null | undefined): LGraphNode | null {
|
||||
return id != null ? this.getNodeById(asNodeId(id)) : null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1389,10 +1406,9 @@ export class LGraph
|
||||
}
|
||||
this.floatingLinksInternal.set(link.id, link)
|
||||
|
||||
const slot =
|
||||
link.target_id !== -1
|
||||
? this.getNodeById(link.target_id)?.inputs?.[link.target_slot]
|
||||
: this.getNodeById(link.origin_id)?.outputs?.[link.origin_slot]
|
||||
const slot = !isFloatingNodeId(link.target_id)
|
||||
? this.getNodeById(link.target_id)?.inputs?.[link.target_slot]
|
||||
: this.getNodeById(link.origin_id)?.outputs?.[link.origin_slot]
|
||||
if (slot) {
|
||||
slot._floatingLinks ??= new Set()
|
||||
slot._floatingLinks.add(link)
|
||||
@@ -1412,10 +1428,9 @@ export class LGraph
|
||||
removeFloatingLink(link: LLink): void {
|
||||
this.floatingLinksInternal.delete(link.id)
|
||||
|
||||
const slot =
|
||||
link.target_id !== -1
|
||||
? this.getNodeById(link.target_id)?.inputs?.[link.target_slot]
|
||||
: this.getNodeById(link.origin_id)?.outputs?.[link.origin_slot]
|
||||
const slot = !isFloatingNodeId(link.target_id)
|
||||
? this.getNodeById(link.target_id)?.inputs?.[link.target_slot]
|
||||
: this.getNodeById(link.origin_id)?.outputs?.[link.origin_slot]
|
||||
if (slot) {
|
||||
slot._floatingLinks?.delete(link)
|
||||
}
|
||||
@@ -1715,11 +1730,11 @@ export class LGraph
|
||||
id: createUuidv4(),
|
||||
name: 'New Subgraph',
|
||||
inputNode: {
|
||||
id: SUBGRAPH_INPUT_ID,
|
||||
id: SUBGRAPH_INPUT_NODE_ID,
|
||||
bounding: [0, 0, 75, 100]
|
||||
},
|
||||
outputNode: {
|
||||
id: SUBGRAPH_OUTPUT_ID,
|
||||
id: SUBGRAPH_OUTPUT_NODE_ID,
|
||||
bounding: [0, 0, 75, 100]
|
||||
},
|
||||
inputs,
|
||||
@@ -1816,7 +1831,7 @@ export class LGraph
|
||||
|
||||
// Special handling: Subgraph input node
|
||||
i++
|
||||
if (link.origin_id === SUBGRAPH_INPUT_ID) {
|
||||
if (isSubgraphInputNodeId(link.origin_id)) {
|
||||
link.target_id = subgraphNode.id
|
||||
link.target_slot = i - 1
|
||||
if (subgraphInput instanceof SubgraphInput) {
|
||||
@@ -1857,7 +1872,7 @@ export class LGraph
|
||||
for (const connection of connections) {
|
||||
const { input, inputNode, link, subgraphOutput } = connection
|
||||
// Special handling: Subgraph output node
|
||||
if (link.target_id === SUBGRAPH_OUTPUT_ID) {
|
||||
if (isSubgraphOutputNodeId(link.target_id)) {
|
||||
link.origin_id = subgraphNode.id
|
||||
link.origin_slot = i - 1
|
||||
this.links.set(link.id, link)
|
||||
@@ -1962,9 +1977,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 newId = asNodeId(++this.last_node_id)
|
||||
nodeIdMap.set(n_info.id, newId)
|
||||
node.id = newId
|
||||
n_info.id = newId
|
||||
|
||||
// Strip links from serialized data before configure to prevent
|
||||
// onConnectionsChange from resolving subgraph-internal link IDs
|
||||
@@ -2029,7 +2045,7 @@ export class LGraph
|
||||
}[] = []
|
||||
for (const [, link] of subgraphNode.subgraph._links) {
|
||||
let externalParentId: RerouteId | undefined
|
||||
if (link.origin_id === SUBGRAPH_INPUT_ID) {
|
||||
if (isSubgraphInputNodeId(link.origin_id)) {
|
||||
const outerLinkId = subgraphNode.inputs[link.origin_slot].link
|
||||
if (!outerLinkId) {
|
||||
console.error('Missing Link ID when unpacking')
|
||||
@@ -2047,7 +2063,7 @@ export class LGraph
|
||||
}
|
||||
link.origin_id = origin_id
|
||||
}
|
||||
if (link.target_id === SUBGRAPH_OUTPUT_ID) {
|
||||
if (isSubgraphOutputNodeId(link.target_id)) {
|
||||
for (const linkId of subgraphNode.outputs[link.target_slot].links ??
|
||||
[]) {
|
||||
const sublink = this.links[linkId]
|
||||
@@ -2098,7 +2114,7 @@ export class LGraph
|
||||
const linkIdMap = new Map<LinkId, LinkId[]>()
|
||||
for (const newLink of dedupedNewLinks) {
|
||||
let created: LLink | null | undefined
|
||||
if (newLink.oid == SUBGRAPH_INPUT_ID) {
|
||||
if (isSubgraphInputNodeId(newLink.oid)) {
|
||||
if (!(this instanceof Subgraph)) {
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
@@ -2108,7 +2124,7 @@ export class LGraph
|
||||
tnode.inputs[newLink.tslot],
|
||||
tnode
|
||||
)
|
||||
} else if (newLink.tid == SUBGRAPH_OUTPUT_ID) {
|
||||
} else if (isSubgraphOutputNodeId(newLink.tid)) {
|
||||
if (!(this instanceof Subgraph)) {
|
||||
console.error('Ignoring link to subgraph outside subgraph')
|
||||
continue
|
||||
@@ -2417,6 +2433,8 @@ export class LGraph
|
||||
if (!data) return
|
||||
if (options.clearGraph) this.clear()
|
||||
|
||||
data = normalizeStringNodeIds(data, this.state)
|
||||
|
||||
this._configureBase(data)
|
||||
|
||||
let reroutes: SerialisableReroute[] | undefined
|
||||
@@ -2499,15 +2517,18 @@ 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)
|
||||
if (isNumericNodeId(node.id))
|
||||
reservedNodeIds.add(nodeIdToNumber(node.id))
|
||||
}
|
||||
for (const sg of this.subgraphs.values()) {
|
||||
for (const node of sg.nodes) {
|
||||
if (typeof node.id === 'number') reservedNodeIds.add(node.id)
|
||||
if (isNumericNodeId(node.id))
|
||||
reservedNodeIds.add(nodeIdToNumber(node.id))
|
||||
}
|
||||
}
|
||||
for (const n of nodesData ?? []) {
|
||||
if (typeof n.id === 'number') reservedNodeIds.add(n.id)
|
||||
if (n.id != null && isNumericNodeId(n.id))
|
||||
reservedNodeIds.add(nodeIdToNumber(n.id))
|
||||
}
|
||||
|
||||
const deduplicated = this.isRootGraph
|
||||
@@ -2554,7 +2575,7 @@ export class LGraph
|
||||
}
|
||||
|
||||
// id it or it will create a new id
|
||||
node.id = n_info.id
|
||||
if (n_info.id != null) node.id = asNodeId(n_info.id)
|
||||
// add before configure, otherwise configure cannot create links
|
||||
this.add(node, true)
|
||||
nodeDataMap.set(node.id, n_info)
|
||||
@@ -2676,24 +2697,22 @@ export class LGraph
|
||||
const remappedIds = new Map<NodeId, NodeId>()
|
||||
|
||||
for (const node of graph._nodes) {
|
||||
if (typeof node.id !== 'number') continue
|
||||
if (!isNumericNodeId(node.id)) continue
|
||||
|
||||
if (usedNodeIds.has(node.id)) {
|
||||
const numericId = nodeIdToNumber(node.id)
|
||||
if (usedNodeIds.has(numericId)) {
|
||||
const oldId = node.id
|
||||
while (usedNodeIds.has(++state.lastNodeId));
|
||||
const newId = state.lastNodeId
|
||||
const newId = nextFreeNodeId(usedNodeIds, state)
|
||||
delete graph._nodes_by_id[oldId]
|
||||
node.id = newId
|
||||
graph._nodes_by_id[newId] = node
|
||||
usedNodeIds.add(newId)
|
||||
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(numericId)
|
||||
if (numericId > state.lastNodeId) state.lastNodeId = numericId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2877,16 +2896,16 @@ export class Subgraph
|
||||
*/
|
||||
private _repairIOSlotLinkIds(): void {
|
||||
for (const [slotIndex, slot] of this.inputs.entries()) {
|
||||
this._repairSlotLinkIds(slot.linkIds, SUBGRAPH_INPUT_ID, slotIndex)
|
||||
this._repairSlotLinkIds(slot.linkIds, SUBGRAPH_INPUT_NODE_ID, slotIndex)
|
||||
}
|
||||
for (const [slotIndex, slot] of this.outputs.entries()) {
|
||||
this._repairSlotLinkIds(slot.linkIds, SUBGRAPH_OUTPUT_ID, slotIndex)
|
||||
this._repairSlotLinkIds(slot.linkIds, SUBGRAPH_OUTPUT_NODE_ID, slotIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private _repairSlotLinkIds(
|
||||
linkIds: LinkId[],
|
||||
ioNodeId: number,
|
||||
ioNodeId: NodeId,
|
||||
slotIndex: number
|
||||
): void {
|
||||
const repaired = linkIds.map((id) =>
|
||||
@@ -2900,7 +2919,7 @@ export class Subgraph
|
||||
}
|
||||
|
||||
private _findLinkBySlot(
|
||||
nodeId: number,
|
||||
nodeId: NodeId,
|
||||
slotIndex: number
|
||||
): LLink | undefined {
|
||||
for (const link of this._links.values()) {
|
||||
|
||||
@@ -6,11 +6,13 @@ import { flushProxyWidgetMigration } from '@/core/graph/subgraph/migration/proxy
|
||||
import { autoExposeKnownPreviewNodes } from '@/core/graph/subgraph/promotionUtils'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
asNodeId,
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
SubgraphNode,
|
||||
UNASSIGNED_NODE_ID,
|
||||
createUuidv4
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { remapClipboardSubgraphNodeIds } from '@/lib/litegraph/src/LGraphCanvas'
|
||||
@@ -19,6 +21,7 @@ import type {
|
||||
ExportedSubgraph,
|
||||
ISerialisedNode
|
||||
} from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { createMockCanvasRenderingContext2D } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
@@ -35,7 +38,7 @@ function createSerialisedNode(
|
||||
proxyWidgets?: Array<[string, string]>
|
||||
): ISerialisedNode {
|
||||
return {
|
||||
id,
|
||||
id: asNodeId(id),
|
||||
type,
|
||||
pos: [0, 0],
|
||||
size: [140, 80],
|
||||
@@ -52,7 +55,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 = asNodeId(1)
|
||||
rootGraph.add(existingNode)
|
||||
|
||||
const subgraphId = createUuidv4()
|
||||
@@ -69,11 +72,11 @@ describe('remapClipboardSubgraphNodeIds', () => {
|
||||
config: {},
|
||||
name: 'Pasted Subgraph',
|
||||
inputNode: {
|
||||
id: -10,
|
||||
id: asNodeId(-10),
|
||||
bounding: [0, 0, 10, 10]
|
||||
},
|
||||
outputNode: {
|
||||
id: -20,
|
||||
id: asNodeId(-20),
|
||||
bounding: [0, 0, 10, 10]
|
||||
},
|
||||
inputs: [],
|
||||
@@ -84,9 +87,9 @@ describe('remapClipboardSubgraphNodeIds', () => {
|
||||
{
|
||||
id: 1,
|
||||
type: '*',
|
||||
origin_id: 1,
|
||||
origin_id: asNodeId(1),
|
||||
origin_slot: 0,
|
||||
target_id: 1,
|
||||
target_id: asNodeId(1),
|
||||
target_slot: 0
|
||||
}
|
||||
],
|
||||
@@ -121,10 +124,85 @@ describe('remapClipboardSubgraphNodeIds', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('remaps legacy numeric interior IDs, links, and proxyWidgets references', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const existingNode = new LGraphNode('existing')
|
||||
existingNode.id = asNodeId(1)
|
||||
rootGraph.add(existingNode)
|
||||
|
||||
const subgraphId = createUuidv4()
|
||||
// Legacy clipboard JSON written by a pre-branding frontend: numeric ids.
|
||||
const legacyNumericId = 1 as unknown as NodeId
|
||||
const pastedSubgraph: ExportedSubgraph = {
|
||||
id: subgraphId,
|
||||
version: 1,
|
||||
revision: 0,
|
||||
state: { lastNodeId: 0, lastLinkId: 0, lastGroupId: 0, lastRerouteId: 0 },
|
||||
config: {},
|
||||
name: 'Pasted Subgraph',
|
||||
inputNode: { id: asNodeId(-10), bounding: [0, 0, 10, 10] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [0, 0, 10, 10] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
nodes: [
|
||||
{
|
||||
id: legacyNumericId,
|
||||
type: 'test/node',
|
||||
pos: [0, 0],
|
||||
size: [140, 80],
|
||||
flags: {},
|
||||
order: 0,
|
||||
mode: 0,
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
properties: {}
|
||||
}
|
||||
],
|
||||
links: [
|
||||
{
|
||||
id: 1,
|
||||
type: '*',
|
||||
origin_id: 1 as unknown as NodeId,
|
||||
origin_slot: 0,
|
||||
target_id: 1 as unknown as NodeId,
|
||||
target_slot: 0
|
||||
}
|
||||
],
|
||||
groups: []
|
||||
}
|
||||
|
||||
const hostInfo = createSerialisedNode(99, subgraphId, [
|
||||
[1 as unknown as string, 'seed']
|
||||
])
|
||||
|
||||
const parsed: ClipboardItems = {
|
||||
nodes: [hostInfo],
|
||||
groups: [],
|
||||
reroutes: [],
|
||||
links: [],
|
||||
subgraphs: [pastedSubgraph]
|
||||
}
|
||||
|
||||
remapClipboardSubgraphNodeIds(parsed, rootGraph)
|
||||
|
||||
const remappedInteriorId = parsed.subgraphs?.[0]?.nodes?.[0]?.id
|
||||
expect(remappedInteriorId).not.toBe(1)
|
||||
expect(remappedInteriorId).not.toBe('1')
|
||||
|
||||
const remappedLink = parsed.subgraphs?.[0]?.links?.[0]
|
||||
expect(remappedLink?.origin_id).toBe(remappedInteriorId)
|
||||
expect(remappedLink?.target_id).toBe(remappedInteriorId)
|
||||
|
||||
expect(parsed.nodes?.[0]?.properties?.proxyWidgets).toStrictEqual([
|
||||
[String(remappedInteriorId), 'seed']
|
||||
])
|
||||
})
|
||||
|
||||
it('remaps pasted SubgraphNode previewExposures sourceNodeId references', () => {
|
||||
const rootGraph = new LGraph()
|
||||
const existingNode = new LGraphNode('existing')
|
||||
existingNode.id = 1
|
||||
existingNode.id = asNodeId(1)
|
||||
rootGraph.add(existingNode)
|
||||
|
||||
const subgraphId = createUuidv4()
|
||||
@@ -140,8 +218,8 @@ describe('remapClipboardSubgraphNodeIds', () => {
|
||||
},
|
||||
config: {},
|
||||
name: 'Pasted Subgraph',
|
||||
inputNode: { id: -10, bounding: [0, 0, 10, 10] },
|
||||
outputNode: { id: -20, bounding: [0, 0, 10, 10] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [0, 0, 10, 10] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [0, 0, 10, 10] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
@@ -173,13 +251,18 @@ describe('remapClipboardSubgraphNodeIds', () => {
|
||||
|
||||
const remappedInteriorId = parsed.subgraphs?.[0]?.nodes?.[0]?.id
|
||||
expect(remappedInteriorId).not.toBe(1)
|
||||
expect(parsed.nodes?.[0]?.properties?.previewExposures).toStrictEqual([
|
||||
expect(typeof remappedInteriorId).toBe('number')
|
||||
const previewExposures = parsed.nodes?.[0]?.properties?.previewExposures
|
||||
expect(previewExposures).toStrictEqual([
|
||||
{
|
||||
name: '$$canvas-image-preview',
|
||||
sourceNodeId: String(remappedInteriorId),
|
||||
sourceNodeId: remappedInteriorId,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
])
|
||||
if (!Array.isArray(previewExposures))
|
||||
throw new Error('Expected previewExposures')
|
||||
expect(typeof previewExposures?.[0]?.sourceNodeId).toBe('number')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -209,7 +292,7 @@ describe('_deserializeItems paste-time migration & auto-expose', () => {
|
||||
class TestSubgraphNode extends SubgraphNode {
|
||||
constructor() {
|
||||
super(rootGraph, subgraph as Subgraph, {
|
||||
id: -1,
|
||||
id: UNASSIGNED_NODE_ID,
|
||||
type: subgraph.id,
|
||||
pos: [0, 0],
|
||||
size: [100, 100],
|
||||
@@ -264,14 +347,14 @@ describe('_deserializeItems paste-time migration & auto-expose', () => {
|
||||
},
|
||||
config: {},
|
||||
name: 'Pasted Subgraph',
|
||||
inputNode: { id: -10, bounding: [0, 0, 10, 10] },
|
||||
outputNode: { id: -20, bounding: [0, 0, 10, 10] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [0, 0, 10, 10] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [0, 0, 10, 10] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
nodes: [
|
||||
{
|
||||
id: interiorId,
|
||||
id: asNodeId(interiorId),
|
||||
type: 'test/inner',
|
||||
pos: [0, 0],
|
||||
size: [140, 80],
|
||||
@@ -288,7 +371,7 @@ describe('_deserializeItems paste-time migration & auto-expose', () => {
|
||||
}
|
||||
|
||||
const hostInfo: ISerialisedNode = {
|
||||
id: 99,
|
||||
id: asNodeId(99),
|
||||
type: subgraphId,
|
||||
pos: [0, 0],
|
||||
size: [140, 80],
|
||||
@@ -340,14 +423,14 @@ describe('_deserializeItems paste-time migration & auto-expose', () => {
|
||||
},
|
||||
config: {},
|
||||
name: 'Pasted Subgraph',
|
||||
inputNode: { id: -10, bounding: [0, 0, 10, 10] },
|
||||
outputNode: { id: -20, bounding: [0, 0, 10, 10] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [0, 0, 10, 10] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [0, 0, 10, 10] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
nodes: [
|
||||
{
|
||||
id: interiorPreviewId,
|
||||
id: asNodeId(interiorPreviewId),
|
||||
type: 'PreviewImage',
|
||||
pos: [0, 0],
|
||||
size: [140, 80],
|
||||
@@ -364,7 +447,7 @@ describe('_deserializeItems paste-time migration & auto-expose', () => {
|
||||
}
|
||||
|
||||
const hostInfo: ISerialisedNode = {
|
||||
id: 99,
|
||||
id: asNodeId(99),
|
||||
type: subgraphId,
|
||||
pos: [0, 0],
|
||||
size: [140, 80],
|
||||
@@ -398,7 +481,7 @@ describe('_deserializeItems paste-time migration & auto-expose', () => {
|
||||
const interiorIdAfterRemap = pastedHost!.subgraph.nodes[0].id
|
||||
expect(exposures).toEqual([
|
||||
expect.objectContaining({
|
||||
sourceNodeId: String(interiorIdAfterRemap),
|
||||
sourceNodeId: interiorIdAfterRemap,
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
])
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import type { NodeLayout } from '@/renderer/core/layout/types'
|
||||
import type { NodeId, NodeLayout } from '@/renderer/core/layout/types'
|
||||
|
||||
import {
|
||||
LGraph,
|
||||
@@ -74,7 +74,7 @@ function createCanvas(graph: LGraph): LGraphCanvas {
|
||||
}
|
||||
|
||||
function createLayoutEntry(node: LGraphNode, zIndex: number) {
|
||||
const nodeId = String(node.id)
|
||||
const nodeId = node.id
|
||||
const layout: NodeLayout = {
|
||||
id: nodeId,
|
||||
position: { x: node.pos[0], y: node.pos[1] },
|
||||
@@ -99,7 +99,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 +145,7 @@ describe('cloned node z-index in Vue renderer', () => {
|
||||
originalNode.size = [200, 100]
|
||||
graph.add(originalNode)
|
||||
|
||||
const originalNodeId = String(originalNode.id)
|
||||
const originalNodeId = originalNode.id
|
||||
|
||||
setZIndex(originalNodeId, 5, 0)
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('cloned node z-index in Vue renderer', () => {
|
||||
expect(result!.created.length).toBe(1)
|
||||
|
||||
const clonedNode = result!.created[0] as LGraphNode
|
||||
const clonedNodeId = String(clonedNode.id)
|
||||
const clonedNodeId = clonedNode.id
|
||||
|
||||
// The cloned node should have a z-index higher than the original
|
||||
const clonedLayout = layoutStore.getNodeLayoutRef(clonedNodeId).value
|
||||
@@ -171,13 +171,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(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(nodeB.id, 7, 0)
|
||||
|
||||
const result = LGraphCanvas.cloneNodes([nodeA, nodeB])
|
||||
expect(result).toBeDefined()
|
||||
@@ -185,8 +185,8 @@ describe('cloned node z-index in Vue renderer', () => {
|
||||
|
||||
const clonedA = result!.created[0] as LGraphNode
|
||||
const clonedB = result!.created[1] as LGraphNode
|
||||
const layoutA = layoutStore.getNodeLayoutRef(String(clonedA.id)).value!
|
||||
const layoutB = layoutStore.getNodeLayoutRef(String(clonedB.id)).value!
|
||||
const layoutA = layoutStore.getNodeLayoutRef(clonedA.id).value!
|
||||
const layoutB = layoutStore.getNodeLayoutRef(clonedB.id).value!
|
||||
|
||||
// Both cloned nodes should be above the highest original (z-index 7)
|
||||
expect(layoutA.zIndex).toBeGreaterThan(7)
|
||||
|
||||
@@ -107,7 +107,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: node.id,
|
||||
index: 0,
|
||||
type: 'output',
|
||||
position: { x: 252, y: 120 },
|
||||
@@ -188,7 +188,7 @@ describe('LGraphCanvas slot hit detection', () => {
|
||||
expect(node.isPointInside(clickX, clickY)).toBe(false)
|
||||
|
||||
vi.mocked(layoutStore.querySlotAtPoint).mockReturnValue({
|
||||
nodeId: String(node.id),
|
||||
nodeId: node.id,
|
||||
index: 0,
|
||||
type: 'input',
|
||||
position: { x: 98, y: 140 },
|
||||
|
||||
@@ -112,6 +112,14 @@ import type { NeverNever, PickNevers } from './types/utility'
|
||||
import type { IBaseWidget, TWidgetValue } from './types/widgets'
|
||||
import { alignNodes, distributeNodes, getBoundaryNodes } from './utils/arrange'
|
||||
import { findFirstNode, getAllNestedItems } from './utils/collections'
|
||||
import type { NodeIdInput } from '@/types/nodeId'
|
||||
import {
|
||||
asNodeId,
|
||||
isNumericNodeId,
|
||||
nodeIdToNumber,
|
||||
tryAsNodeId,
|
||||
UNASSIGNED_NODE_ID
|
||||
} from '@/types/nodeId'
|
||||
import { resolveConnectingLinkColor } from './utils/linkColors'
|
||||
import { createUuidv4 } from '@/utils/uuid'
|
||||
import { BaseWidget } from './widgets/BaseWidget'
|
||||
@@ -3359,7 +3367,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
let outputId: number = -1
|
||||
|
||||
const slotLayout = layoutStore.querySlotAtPoint({ x, y })
|
||||
if (slotLayout && slotLayout.nodeId === String(node.id)) {
|
||||
if (slotLayout && slotLayout.nodeId === node.id) {
|
||||
if (slotLayout.type === 'input') {
|
||||
inputId = slotLayout.index
|
||||
pos[0] = slotLayout.position.x
|
||||
@@ -4216,7 +4224,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
}
|
||||
|
||||
nodes.set(info.id, node)
|
||||
info.id = -1
|
||||
info.id = UNASSIGNED_NODE_ID
|
||||
|
||||
graph.add(node)
|
||||
node.configure(info)
|
||||
@@ -4309,7 +4317,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],
|
||||
@@ -8980,26 +8988,30 @@ function patchLinkNodeIds(
|
||||
if (!links?.length) return
|
||||
|
||||
for (const link of links) {
|
||||
const newOriginId = remappedIds.get(link.origin_id)
|
||||
const newOriginId = remappedIds.get(asNodeId(link.origin_id))
|
||||
if (newOriginId !== undefined) link.origin_id = newOriginId
|
||||
|
||||
const newTargetId = remappedIds.get(link.target_id)
|
||||
const newTargetId = remappedIds.get(asNodeId(link.target_id))
|
||||
if (newTargetId !== undefined) link.target_id = newTargetId
|
||||
}
|
||||
}
|
||||
|
||||
function isNodeIdValue(value: unknown): value is NodeIdInput {
|
||||
return typeof value === 'string' || typeof value === 'number'
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a remap, tolerating legacy numeric ids and skipping sentinels.
|
||||
* Invalid metadata ids (e.g. non-decimal strings from legacy/extension
|
||||
* `proxyWidgets` or `previewExposures`) are skipped rather than aborting paste.
|
||||
*/
|
||||
function remapNodeId(
|
||||
nodeId: string,
|
||||
nodeId: NodeIdInput,
|
||||
remappedIds: Map<NodeId, NodeId>
|
||||
): NodeId | undefined {
|
||||
const directMatch = remappedIds.get(nodeId)
|
||||
if (directMatch !== undefined) return directMatch
|
||||
if (!/^-?\d+$/.test(nodeId)) return undefined
|
||||
|
||||
const numericId = Number(nodeId)
|
||||
if (!Number.isSafeInteger(numericId)) return undefined
|
||||
|
||||
return remappedIds.get(numericId)
|
||||
const normalized = tryAsNodeId(nodeId)
|
||||
if (normalized === null || normalized === UNASSIGNED_NODE_ID) return undefined
|
||||
return remappedIds.get(normalized)
|
||||
}
|
||||
|
||||
function remapProxyWidgets(
|
||||
@@ -9015,21 +9027,21 @@ function remapProxyWidgets(
|
||||
if (!Array.isArray(entry)) continue
|
||||
|
||||
const [nodeId] = entry
|
||||
if (typeof nodeId !== 'string' || nodeId === '-1') continue
|
||||
if (!isNodeIdValue(nodeId)) continue
|
||||
|
||||
const remappedNodeId = remapNodeId(nodeId, remappedIds)
|
||||
if (remappedNodeId !== undefined) entry[0] = String(remappedNodeId)
|
||||
}
|
||||
}
|
||||
|
||||
function hasStringSourceNodeId(
|
||||
function hasSourceNodeId(
|
||||
value: unknown
|
||||
): value is { sourceNodeId: string } {
|
||||
): value is { sourceNodeId: NodeIdInput } {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'sourceNodeId' in value &&
|
||||
typeof value.sourceNodeId === 'string'
|
||||
isNodeIdValue(value.sourceNodeId)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9043,11 +9055,10 @@ function remapPreviewExposures(
|
||||
if (!Array.isArray(previewExposures)) return
|
||||
|
||||
for (const entry of previewExposures) {
|
||||
if (!hasStringSourceNodeId(entry) || entry.sourceNodeId === '-1') continue
|
||||
if (!hasSourceNodeId(entry)) continue
|
||||
|
||||
const remappedNodeId = remapNodeId(entry.sourceNodeId, remappedIds)
|
||||
if (remappedNodeId !== undefined)
|
||||
entry.sourceNodeId = String(remappedNodeId)
|
||||
if (remappedNodeId !== undefined) entry.sourceNodeId = remappedNodeId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9057,17 +9068,18 @@ 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
|
||||
if (!isNumericNodeId(node.id)) return
|
||||
const numericId = nodeIdToNumber(node.id)
|
||||
usedNodeIds.add(numericId)
|
||||
if (rootGraph.state.lastNodeId < numericId)
|
||||
rootGraph.state.lastNodeId = numericId
|
||||
})
|
||||
|
||||
function nextUniqueNodeId() {
|
||||
function nextUniqueNodeId(): NodeId {
|
||||
while (usedNodeIds.has(++rootGraph.state.lastNodeId));
|
||||
const nextId = rootGraph.state.lastNodeId
|
||||
usedNodeIds.add(nextId)
|
||||
return nextId
|
||||
return asNodeId(nextId)
|
||||
}
|
||||
|
||||
const subgraphNodeIdMap = new Map<SubgraphId, Map<NodeId, NodeId>>()
|
||||
@@ -9076,19 +9088,20 @@ export function remapClipboardSubgraphNodeIds(
|
||||
const interiorNodes = subgraphInfo.nodes ?? []
|
||||
|
||||
for (const nodeInfo of interiorNodes) {
|
||||
if (typeof nodeInfo.id !== 'number') continue
|
||||
if (!isNumericNodeId(nodeInfo.id)) continue
|
||||
|
||||
if (usedNodeIds.has(nodeInfo.id)) {
|
||||
const oldId = nodeInfo.id
|
||||
const numericId = nodeIdToNumber(nodeInfo.id)
|
||||
if (usedNodeIds.has(numericId)) {
|
||||
const oldId = asNodeId(nodeInfo.id)
|
||||
const newId = nextUniqueNodeId()
|
||||
remappedIds.set(oldId, newId)
|
||||
nodeInfo.id = newId
|
||||
continue
|
||||
}
|
||||
|
||||
usedNodeIds.add(nodeInfo.id)
|
||||
if (rootGraph.state.lastNodeId < nodeInfo.id)
|
||||
rootGraph.state.lastNodeId = nodeInfo.id
|
||||
usedNodeIds.add(numericId)
|
||||
if (rootGraph.state.lastNodeId < numericId)
|
||||
rootGraph.state.lastNodeId = numericId
|
||||
}
|
||||
|
||||
if (remappedIds.size > 0) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { Rect } from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
asNodeId,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
LGraph,
|
||||
@@ -107,7 +108,7 @@ describe('LGraphNode', () => {
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 0,
|
||||
id: asNodeId(0),
|
||||
inputs: [{ name: 'TestInput', type: 'number', link: null }]
|
||||
})
|
||||
)
|
||||
@@ -117,8 +118,8 @@ describe('LGraphNode', () => {
|
||||
expect(node.inputs[0]).instanceOf(NodeInputSlot)
|
||||
|
||||
// Should not override existing inputs
|
||||
node.configure(getMockISerialisedNode({ id: 1 }))
|
||||
expect(node.id).toEqual(1)
|
||||
node.configure(getMockISerialisedNode({ id: asNodeId(1) }))
|
||||
expect(node.id).toEqual(asNodeId(1))
|
||||
expect(node.inputs.length).toEqual(1)
|
||||
})
|
||||
|
||||
@@ -126,7 +127,7 @@ describe('LGraphNode', () => {
|
||||
const node = new LGraphNode('TestNode')
|
||||
node.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 0,
|
||||
id: asNodeId(0),
|
||||
outputs: [{ name: 'TestOutput', type: 'number', links: [] }]
|
||||
})
|
||||
)
|
||||
@@ -137,16 +138,16 @@ describe('LGraphNode', () => {
|
||||
expect(node.outputs[0]).instanceOf(NodeOutputSlot)
|
||||
|
||||
// Should not override existing outputs
|
||||
node.configure(getMockISerialisedNode({ id: 1 }))
|
||||
expect(node.id).toEqual(1)
|
||||
node.configure(getMockISerialisedNode({ id: asNodeId(1) }))
|
||||
expect(node.id).toEqual(asNodeId(1))
|
||||
expect(node.outputs.length).toEqual(1)
|
||||
})
|
||||
test('should not allow configuring id to -1', () => {
|
||||
const graph = new LGraph()
|
||||
const node = new LGraphNode('TestNode')
|
||||
graph.add(node)
|
||||
node.configure(getMockISerialisedNode({ id: -1 }))
|
||||
expect(node.id).not.toBe(-1)
|
||||
node.configure(getMockISerialisedNode({ id: asNodeId(-1) }))
|
||||
expect(node.id).not.toBe(asNodeId(-1))
|
||||
})
|
||||
|
||||
describe('Disconnect I/O Slots', () => {
|
||||
@@ -157,13 +158,13 @@ describe('LGraphNode', () => {
|
||||
// Configure nodes with input/output slots
|
||||
node1.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
outputs: [{ name: 'Output1', type: 'number', links: [] }]
|
||||
})
|
||||
)
|
||||
node2.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 2,
|
||||
id: asNodeId(2),
|
||||
inputs: [{ name: 'Input1', type: 'number', link: null }]
|
||||
})
|
||||
)
|
||||
@@ -209,7 +210,7 @@ describe('LGraphNode', () => {
|
||||
// Configure nodes with input/output slots
|
||||
sourceNode.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
outputs: [
|
||||
{ name: 'Output1', type: 'number', links: [] },
|
||||
{ name: 'Output2', type: 'number', links: [] }
|
||||
@@ -218,13 +219,13 @@ describe('LGraphNode', () => {
|
||||
)
|
||||
targetNode1.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 2,
|
||||
id: asNodeId(2),
|
||||
inputs: [{ name: 'Input1', type: 'number', link: null }]
|
||||
})
|
||||
)
|
||||
targetNode2.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 3,
|
||||
id: asNodeId(3),
|
||||
inputs: [{ name: 'Input1', type: 'number', link: null }]
|
||||
})
|
||||
)
|
||||
@@ -319,7 +320,7 @@ describe('LGraphNode', () => {
|
||||
node.updateArea()
|
||||
node.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
inputs: [{ name: 'Input1', type: 'number', link: null }],
|
||||
outputs: [{ name: 'Output1', type: 'number', links: [] }]
|
||||
})
|
||||
@@ -342,7 +343,7 @@ describe('LGraphNode', () => {
|
||||
node.size = [100, 100]
|
||||
node.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
inputs: [{ name: 'Input1', type: 'number', link: null }],
|
||||
outputs: [{ name: 'Output1', type: 'number', links: [] }]
|
||||
})
|
||||
@@ -363,7 +364,7 @@ describe('LGraphNode', () => {
|
||||
node.size = [100, 100]
|
||||
node.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
inputs: [{ name: 'Input1', type: 'number', link: null }],
|
||||
outputs: [{ name: 'Output1', type: 'number', links: [] }]
|
||||
})
|
||||
@@ -382,7 +383,7 @@ describe('LGraphNode', () => {
|
||||
node.updateArea()
|
||||
node.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
inputs: [
|
||||
{ name: 'Input1', type: 'number', link: null },
|
||||
{ name: 'Input2', type: 'string', link: null }
|
||||
@@ -408,7 +409,7 @@ describe('LGraphNode', () => {
|
||||
node.updateArea()
|
||||
node.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
outputs: [
|
||||
{ name: 'Output1', type: 'number', links: [] },
|
||||
{ name: 'Output2', type: 'string', links: [] }
|
||||
@@ -435,7 +436,7 @@ describe('LGraphNode', () => {
|
||||
node.updateArea()
|
||||
node.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
inputs: [{ name: 'Input1', type: 'number', link: null }],
|
||||
outputs: [{ name: 'Output1', type: 'number', links: [] }]
|
||||
})
|
||||
@@ -575,7 +576,7 @@ describe('LGraphNode', () => {
|
||||
node.widgets![0].serialize = false
|
||||
node.configure(
|
||||
getMockISerialisedNode({
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
type: 'TestNode',
|
||||
pos: [100, 100],
|
||||
size: [100, 100],
|
||||
|
||||
@@ -16,8 +16,15 @@ import {
|
||||
toClass
|
||||
} from '@/lib/litegraph/src/utils/type'
|
||||
|
||||
import { SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants'
|
||||
import { cachedMeasureText } from '@/lib/litegraph/src/utils/textMeasureCache'
|
||||
import {
|
||||
UNASSIGNED_NODE_ID,
|
||||
asNodeId,
|
||||
isSubgraphInputNodeId,
|
||||
isSubgraphOutputNodeId,
|
||||
isUnassignedNodeId
|
||||
} from '@/types/nodeId'
|
||||
import type { NodeId } from '@/types/nodeId'
|
||||
import type { DragAndScale } from './DragAndScale'
|
||||
import type { LGraph } from './LGraph'
|
||||
import { BadgePosition, LGraphBadge } from './LGraphBadge'
|
||||
@@ -98,7 +105,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
|
||||
|
||||
@@ -501,7 +508,7 @@ export class LGraphNode
|
||||
|
||||
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] })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -523,7 +530,7 @@ export class LGraphNode
|
||||
|
||||
const mutations = useLayoutMutations()
|
||||
mutations.setSource(LayoutSource.Canvas)
|
||||
mutations.resizeNode(String(this.id), {
|
||||
mutations.resizeNode(this.id, {
|
||||
width: value[0],
|
||||
height: value[1]
|
||||
})
|
||||
@@ -810,7 +817,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]
|
||||
@@ -833,7 +840,7 @@ export class LGraphNode
|
||||
if (this.graph) {
|
||||
this.graph.incrementVersion()
|
||||
}
|
||||
if (info.id === -1) info.id = this.id
|
||||
if (isUnassignedNodeId(info.id)) info.id = this.id
|
||||
for (const j in info) {
|
||||
if (j == 'properties') {
|
||||
// i don't want to clone properties, I want to reuse the old container
|
||||
@@ -864,6 +871,8 @@ export class LGraphNode
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUnassignedNodeId(this.id)) this.id = asNodeId(this.id)
|
||||
|
||||
if (!info.title) {
|
||||
this.title = this.constructor.title
|
||||
}
|
||||
@@ -1413,17 +1422,14 @@ export class LGraphNode
|
||||
options.action_call ||= `${this.id}_exec_${Math.floor(Math.random() * 9999)}`
|
||||
if (!this.graph) throw new NullGraphError()
|
||||
|
||||
// @ts-expect-error Technically it works when id is a string. Array gets props.
|
||||
this.graph.nodes_executing[this.id] = true
|
||||
this.onExecute(param, options)
|
||||
// @ts-expect-error deprecated
|
||||
this.graph.nodes_executing[this.id] = false
|
||||
|
||||
// save execution/action ref
|
||||
this.exec_version = this.graph.iteration
|
||||
if (options?.action_call) {
|
||||
this.action_call = options.action_call
|
||||
// @ts-expect-error deprecated
|
||||
this.graph.nodes_executedAction[this.id] = options.action_call
|
||||
}
|
||||
}
|
||||
@@ -1447,16 +1453,13 @@ export class LGraphNode
|
||||
options.action_call ||= `${this.id}_${action || 'action'}_${Math.floor(Math.random() * 9999)}`
|
||||
if (!this.graph) throw new NullGraphError()
|
||||
|
||||
// @ts-expect-error deprecated
|
||||
this.graph.nodes_actioning[this.id] = action || 'actioning'
|
||||
this.onAction(action, param, options)
|
||||
// @ts-expect-error deprecated
|
||||
this.graph.nodes_actioning[this.id] = false
|
||||
|
||||
// save execution/action ref
|
||||
if (options?.action_call) {
|
||||
this.action_call = options.action_call
|
||||
// @ts-expect-error deprecated
|
||||
this.graph.nodes_executedAction[this.id] = options.action_call
|
||||
}
|
||||
}
|
||||
@@ -1999,9 +2002,9 @@ export class LGraphNode
|
||||
this._widgetSlotsDirty = true
|
||||
|
||||
// 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
|
||||
// If the node isn't in a graph yet, registration happens
|
||||
// when the node is added via LGraph.add() -> node.onAdded.
|
||||
if (this.id !== -1 && isNodeBindable(widget)) {
|
||||
if (!isUnassignedNodeId(this.id) && isNodeBindable(widget)) {
|
||||
widget.setNodeId(this.id)
|
||||
}
|
||||
|
||||
@@ -3162,7 +3165,7 @@ export class LGraphNode
|
||||
const link_info = graph._links.get(link_id)
|
||||
if (!link_info) continue
|
||||
if (
|
||||
link_info.target_id === SUBGRAPH_OUTPUT_ID &&
|
||||
isSubgraphOutputNodeId(link_info.target_id) &&
|
||||
graph instanceof Subgraph
|
||||
) {
|
||||
const targetSlot = graph.outputNode.slots[link_info.target_slot]
|
||||
@@ -3272,7 +3275,10 @@ 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 (
|
||||
isSubgraphInputNodeId(link_info.origin_id) &&
|
||||
'inputNode' in graph
|
||||
) {
|
||||
graph.inputNode._disconnectNodeInput(this, input, link_info)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { asNodeId, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
|
||||
import { sortWidgetValuesByInputOrder } from '@/workbench/utils/nodeDefOrderingUtil'
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('LGraphNode widget ordering', () => {
|
||||
|
||||
// Configure with widget values
|
||||
const info: ISerialisedNode = {
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
type: 'TestNode',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
@@ -50,7 +50,7 @@ describe('LGraphNode widget ordering', () => {
|
||||
|
||||
// Widget values are in input_order: [steps, seed, prompt]
|
||||
const info: ISerialisedNode = {
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
type: 'TestNode',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
@@ -81,7 +81,7 @@ describe('LGraphNode widget ordering', () => {
|
||||
node.addWidget('number', 'seed', 0, null, {})
|
||||
|
||||
const info: ISerialisedNode = {
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
type: 'TestNode',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
|
||||
@@ -1,17 +1,80 @@
|
||||
import { describe, expect } from 'vitest'
|
||||
|
||||
import { LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
asNodeId,
|
||||
UNASSIGNED_NODE_ID,
|
||||
LLink,
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_INPUT_NODE_ID,
|
||||
SUBGRAPH_OUTPUT_ID,
|
||||
SUBGRAPH_OUTPUT_NODE_ID
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './__fixtures__/testExtensions'
|
||||
|
||||
describe('LLink', () => {
|
||||
test('matches previous snapshot', () => {
|
||||
const link = new LLink(1, 'float', 4, 2, 5, 3)
|
||||
const link = new LLink(1, 'float', asNodeId(4), 2, asNodeId(5), 3)
|
||||
expect(link.serialize()).toMatchSnapshot('Basic')
|
||||
})
|
||||
|
||||
test('serializes to the previous snapshot', () => {
|
||||
const link = new LLink(1, 'float', 4, 2, 5, 3)
|
||||
const link = new LLink(1, 'float', asNodeId(4), 2, asNodeId(5), 3)
|
||||
expect(link.serialize()).toMatchSnapshot('Basic')
|
||||
})
|
||||
|
||||
describe('node-id branding at the edges', () => {
|
||||
test('brands numeric endpoints into NodeId at construction', () => {
|
||||
const link = new LLink(1, 'float', 4, 2, 5, 3)
|
||||
expect(link.origin_id).toBe(asNodeId(4))
|
||||
expect(link.target_id).toBe(asNodeId(5))
|
||||
expect(typeof link.origin_id).toBe('number')
|
||||
})
|
||||
|
||||
test('brands the floating sentinel into UNASSIGNED_NODE_ID', () => {
|
||||
const link = new LLink(1, 'float', -1, -1, asNodeId(5), 3)
|
||||
expect(link.origin_id).toBe(UNASSIGNED_NODE_ID)
|
||||
expect(link.isFloatingOutput).toBe(true)
|
||||
})
|
||||
|
||||
test('brands subgraph IO sentinels into branded sentinels', () => {
|
||||
const link = new LLink(
|
||||
1,
|
||||
'float',
|
||||
SUBGRAPH_INPUT_ID,
|
||||
0,
|
||||
SUBGRAPH_OUTPUT_ID,
|
||||
0
|
||||
)
|
||||
expect(link.origin_id).toBe(SUBGRAPH_INPUT_NODE_ID)
|
||||
expect(link.target_id).toBe(SUBGRAPH_OUTPUT_NODE_ID)
|
||||
expect(link.originIsIoNode).toBe(true)
|
||||
expect(link.targetIsIoNode).toBe(true)
|
||||
})
|
||||
|
||||
test('serializes sentinels as branded string ids', () => {
|
||||
const floating = new LLink(1, 'float', -1, -1, asNodeId(5), 3)
|
||||
expect(floating.asSerialisable().origin_id).toBe(UNASSIGNED_NODE_ID)
|
||||
expect(floating.serialize()[1]).toBe(UNASSIGNED_NODE_ID)
|
||||
|
||||
const io = new LLink(
|
||||
2,
|
||||
'float',
|
||||
SUBGRAPH_INPUT_ID,
|
||||
0,
|
||||
SUBGRAPH_OUTPUT_ID,
|
||||
0
|
||||
)
|
||||
const serialised = io.asSerialisable()
|
||||
expect(serialised.origin_id).toBe(SUBGRAPH_INPUT_NODE_ID)
|
||||
expect(serialised.target_id).toBe(SUBGRAPH_OUTPUT_NODE_ID)
|
||||
})
|
||||
|
||||
test('serializes real node ids as branded strings (round-trips)', () => {
|
||||
const link = new LLink(1, 'float', 4, 2, 5, 3)
|
||||
const restored = LLink.create(link.asSerialisable())
|
||||
expect(restored.origin_id).toBe(asNodeId(4))
|
||||
expect(restored.target_id).toBe(asNodeId(5))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
} from '@/lib/litegraph/src/constants'
|
||||
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 type { LGraphNode, NodeId } from './LGraphNode'
|
||||
import type { NodeIdInput } from '@/types/nodeId'
|
||||
import {
|
||||
UNASSIGNED_NODE_ID,
|
||||
asNodeId,
|
||||
isFloatingNodeId,
|
||||
isSubgraphInputNodeId,
|
||||
isSubgraphOutputNodeId
|
||||
} from '@/types/nodeId'
|
||||
import type { Reroute, RerouteId } from './Reroute'
|
||||
import type {
|
||||
CanvasColour,
|
||||
@@ -130,11 +134,11 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
}
|
||||
|
||||
public get isFloatingOutput(): boolean {
|
||||
return this.origin_id === -1 && this.origin_slot === -1
|
||||
return isFloatingNodeId(this.origin_id) && this.origin_slot === -1
|
||||
}
|
||||
|
||||
public get isFloatingInput(): boolean {
|
||||
return this.target_id === -1 && this.target_slot === -1
|
||||
return isFloatingNodeId(this.target_id) && this.target_slot === -1
|
||||
}
|
||||
|
||||
public get isFloating(): boolean {
|
||||
@@ -143,28 +147,28 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
|
||||
/** `true` if this link is connected to a subgraph input node (the actual origin is in a different graph). */
|
||||
get originIsIoNode(): boolean {
|
||||
return this.origin_id === SUBGRAPH_INPUT_ID
|
||||
return isSubgraphInputNodeId(this.origin_id)
|
||||
}
|
||||
|
||||
/** `true` if this link is connected to a subgraph output node (the actual target is in a different graph). */
|
||||
get targetIsIoNode(): boolean {
|
||||
return this.target_id === SUBGRAPH_OUTPUT_ID
|
||||
return isSubgraphOutputNodeId(this.target_id)
|
||||
}
|
||||
|
||||
constructor(
|
||||
id: LinkId,
|
||||
type: ISlotType,
|
||||
origin_id: NodeId,
|
||||
origin_id: NodeIdInput,
|
||||
origin_slot: number,
|
||||
target_id: NodeId,
|
||||
target_id: NodeIdInput,
|
||||
target_slot: number,
|
||||
parentId?: RerouteId
|
||||
) {
|
||||
this.id = id
|
||||
this.type = type
|
||||
this.origin_id = origin_id
|
||||
this.origin_id = asNodeId(origin_id)
|
||||
this.origin_slot = origin_slot
|
||||
this.target_id = target_id
|
||||
this.target_id = asNodeId(target_id)
|
||||
this.target_slot = target_slot
|
||||
this.parentId = parentId
|
||||
|
||||
@@ -307,10 +311,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
* it is recommended to use simpler methods where appropriate.
|
||||
*/
|
||||
resolve(network: BasicReadonlyNetwork): ResolvedConnection {
|
||||
const inputNode =
|
||||
this.target_id === -1
|
||||
? undefined
|
||||
: (network.getNodeById(this.target_id) ?? undefined)
|
||||
const inputNode = isFloatingNodeId(this.target_id)
|
||||
? undefined
|
||||
: (network.getNodeById(this.target_id) ?? undefined)
|
||||
const input = inputNode?.inputs[this.target_slot]
|
||||
const subgraphInput = this.originIsIoNode
|
||||
? network.inputNode?.slots[this.origin_slot]
|
||||
@@ -319,10 +322,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
return { inputNode, input, subgraphInput, link: this }
|
||||
}
|
||||
|
||||
const outputNode =
|
||||
this.origin_id === -1
|
||||
? undefined
|
||||
: (network.getNodeById(this.origin_id) ?? undefined)
|
||||
const outputNode = isFloatingNodeId(this.origin_id)
|
||||
? undefined
|
||||
: (network.getNodeById(this.origin_id) ?? undefined)
|
||||
const output = outputNode?.outputs[this.origin_slot]
|
||||
const subgraphOutput = this.targetIsIoNode
|
||||
? network.outputNode?.slots[this.target_slot]
|
||||
@@ -351,17 +353,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 = asNodeId(o[1])
|
||||
this.origin_slot = o[2]
|
||||
this.target_id = o[3]
|
||||
this.target_id = asNodeId(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 = asNodeId(o.origin_id)
|
||||
this.origin_slot = o.origin_slot
|
||||
this.target_id = o.target_id
|
||||
this.target_id = asNodeId(o.target_id)
|
||||
this.target_slot = o.target_slot
|
||||
this.parentId = o.parentId
|
||||
}
|
||||
@@ -399,10 +401,10 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
exported.parentId = parentId
|
||||
|
||||
if (slotType === 'input') {
|
||||
exported.origin_id = -1
|
||||
exported.origin_id = UNASSIGNED_NODE_ID
|
||||
exported.origin_slot = -1
|
||||
} else {
|
||||
exported.target_id = -1
|
||||
exported.target_id = UNASSIGNED_NODE_ID
|
||||
exported.target_slot = -1
|
||||
}
|
||||
|
||||
@@ -432,12 +434,12 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
newLink.id = -1
|
||||
|
||||
if (keepReroutes === 'input') {
|
||||
newLink.origin_id = -1
|
||||
newLink.origin_id = UNASSIGNED_NODE_ID
|
||||
newLink.origin_slot = -1
|
||||
|
||||
lastReroute.floating = { slotType: 'input' }
|
||||
} else {
|
||||
newLink.target_id = -1
|
||||
newLink.target_id = UNASSIGNED_NODE_ID
|
||||
newLink.target_slot = -1
|
||||
|
||||
lastReroute.floating = { slotType: 'output' }
|
||||
|
||||
@@ -3,6 +3,7 @@ import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
|
||||
import { LGraphBadge } from './LGraphBadge'
|
||||
import type { LGraphNode, NodeId } from './LGraphNode'
|
||||
import { isFloatingNodeId } from '@/types/nodeId'
|
||||
import { LLink } from './LLink'
|
||||
import type { LinkId } from './LLink'
|
||||
import type {
|
||||
@@ -372,7 +373,7 @@ export class Reroute
|
||||
|
||||
for (const linkId of this.floatingLinkIds) {
|
||||
const link = floatingLinks.get(linkId)
|
||||
if (link?.[idProp] === -1) out.push(link)
|
||||
if (link && isFloatingNodeId(link[idProp])) out.push(link)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -807,7 +808,7 @@ function getNextPos(
|
||||
if (linkPos) return linkPos
|
||||
|
||||
// Floating link with no input to find
|
||||
if (link.target_id === -1 || link.target_slot === -1) return
|
||||
if (isFloatingNodeId(link.target_id) || link.target_slot === -1) return
|
||||
|
||||
return network.getNodeById(link.target_id)?.getInputPos(link.target_slot)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const oldSchemaGraph: ISerialisedGraph = {
|
||||
title: 'A group to test with'
|
||||
}
|
||||
],
|
||||
nodes: [{ id: 1 } as Partial<ISerialisedNode> as ISerialisedNode],
|
||||
nodes: [{ id: 1 } as unknown as ISerialisedNode],
|
||||
links: []
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SerialisableGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
/**
|
||||
* Root graph with two nodes (Source, Target) connected by one valid link
|
||||
@@ -17,7 +18,7 @@ export const duplicateLinksRoot: SerialisableGraph = {
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
type: 'test/DupTestNode',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
@@ -29,7 +30,7 @@ export const duplicateLinksRoot: SerialisableGraph = {
|
||||
properties: {}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: asNodeId(2),
|
||||
type: 'test/DupTestNode',
|
||||
pos: [300, 0],
|
||||
size: [200, 100],
|
||||
@@ -44,25 +45,25 @@ export const duplicateLinksRoot: SerialisableGraph = {
|
||||
links: [
|
||||
{
|
||||
id: 1,
|
||||
origin_id: 1,
|
||||
origin_id: asNodeId(1),
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_id: asNodeId(2),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
origin_id: 1,
|
||||
origin_id: asNodeId(1),
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_id: asNodeId(2),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
origin_id: 1,
|
||||
origin_id: asNodeId(1),
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_id: asNodeId(2),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
}
|
||||
@@ -87,7 +88,7 @@ export const duplicateLinksSlotShift: SerialisableGraph = {
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
type: 'test/DupTestNode',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
@@ -99,7 +100,7 @@ export const duplicateLinksSlotShift: SerialisableGraph = {
|
||||
properties: {}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: asNodeId(2),
|
||||
type: 'test/DupTestNode',
|
||||
pos: [300, 0],
|
||||
size: [200, 100],
|
||||
@@ -117,17 +118,17 @@ export const duplicateLinksSlotShift: SerialisableGraph = {
|
||||
links: [
|
||||
{
|
||||
id: 1,
|
||||
origin_id: 1,
|
||||
origin_id: asNodeId(1),
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_id: asNodeId(2),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
origin_id: 1,
|
||||
origin_id: asNodeId(1),
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_id: asNodeId(2),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
}
|
||||
@@ -151,7 +152,7 @@ export const duplicateLinksSubgraph: SerialisableGraph = {
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
type: 'dd111111-1111-4111-8111-111111111111',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
@@ -175,14 +176,14 @@ export const duplicateLinksSubgraph: SerialisableGraph = {
|
||||
},
|
||||
name: 'Subgraph With Duplicates',
|
||||
config: {},
|
||||
inputNode: { id: -10, bounding: [0, 100, 120, 60] },
|
||||
outputNode: { id: -20, bounding: [500, 100, 120, 60] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [0, 100, 120, 60] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [500, 100, 120, 60] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [],
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
id: asNodeId(1),
|
||||
type: 'test/Source',
|
||||
pos: [100, 100],
|
||||
size: [200, 100],
|
||||
@@ -194,7 +195,7 @@ export const duplicateLinksSubgraph: SerialisableGraph = {
|
||||
properties: {}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
id: asNodeId(2),
|
||||
type: 'test/Target',
|
||||
pos: [400, 100],
|
||||
size: [200, 100],
|
||||
@@ -210,25 +211,25 @@ export const duplicateLinksSubgraph: SerialisableGraph = {
|
||||
links: [
|
||||
{
|
||||
id: 1,
|
||||
origin_id: 1,
|
||||
origin_id: asNodeId(1),
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_id: asNodeId(2),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
origin_id: 1,
|
||||
origin_id: asNodeId(1),
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_id: asNodeId(2),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
origin_id: 1,
|
||||
origin_id: asNodeId(1),
|
||||
origin_slot: 0,
|
||||
target_id: 2,
|
||||
target_id: asNodeId(2),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SerialisableGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
/**
|
||||
* Workflow with two subgraph definitions whose internal nodes share
|
||||
@@ -20,7 +21,7 @@ export const duplicateSubgraphNodeIds = {
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 102,
|
||||
id: asNodeId(102),
|
||||
type: '11111111-1111-4111-8111-111111111111',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
@@ -30,7 +31,7 @@ export const duplicateSubgraphNodeIds = {
|
||||
properties: { proxyWidgets: [['3', 'seed']] }
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
id: asNodeId(103),
|
||||
type: '22222222-2222-4222-8222-222222222222',
|
||||
pos: [300, 0],
|
||||
size: [200, 100],
|
||||
@@ -54,14 +55,14 @@ export const duplicateSubgraphNodeIds = {
|
||||
},
|
||||
name: 'SubgraphA',
|
||||
config: {},
|
||||
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [400, 100, 140, 126] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [{ id: 3, name: 'seed' }],
|
||||
widgets: [{ id: asNodeId(3), name: 'seed' }],
|
||||
nodes: [
|
||||
{
|
||||
id: 3,
|
||||
id: asNodeId(3),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -70,7 +71,7 @@ export const duplicateSubgraphNodeIds = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: asNodeId(8),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -79,7 +80,7 @@ export const duplicateSubgraphNodeIds = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 37,
|
||||
id: asNodeId(37),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -91,9 +92,9 @@ export const duplicateSubgraphNodeIds = {
|
||||
links: [
|
||||
{
|
||||
id: 1,
|
||||
origin_id: 3,
|
||||
origin_id: asNodeId(3),
|
||||
origin_slot: 0,
|
||||
target_id: 8,
|
||||
target_id: asNodeId(8),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
}
|
||||
@@ -112,14 +113,14 @@ export const duplicateSubgraphNodeIds = {
|
||||
},
|
||||
name: 'SubgraphB',
|
||||
config: {},
|
||||
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [400, 100, 140, 126] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [{ id: 8, name: 'prompt' }],
|
||||
widgets: [{ id: asNodeId(8), name: 'prompt' }],
|
||||
nodes: [
|
||||
{
|
||||
id: 3,
|
||||
id: asNodeId(3),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -128,7 +129,7 @@ export const duplicateSubgraphNodeIds = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: asNodeId(8),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -137,7 +138,7 @@ export const duplicateSubgraphNodeIds = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 37,
|
||||
id: asNodeId(37),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -149,9 +150,9 @@ export const duplicateSubgraphNodeIds = {
|
||||
links: [
|
||||
{
|
||||
id: 2,
|
||||
origin_id: 3,
|
||||
origin_id: asNodeId(3),
|
||||
origin_slot: 0,
|
||||
target_id: 37,
|
||||
target_id: asNodeId(37),
|
||||
target_slot: 0,
|
||||
type: 'string'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SerialisableGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
/**
|
||||
* Workflow where SubgraphA contains a nested SubgraphNode referencing
|
||||
@@ -24,7 +25,7 @@ export const nestedSubgraphProxyWidgets = {
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 102,
|
||||
id: asNodeId(102),
|
||||
type: '11111111-1111-4111-8111-111111111111',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
@@ -34,7 +35,7 @@ export const nestedSubgraphProxyWidgets = {
|
||||
properties: { proxyWidgets: [['3', 'seed']] }
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
id: asNodeId(103),
|
||||
type: '22222222-2222-4222-8222-222222222222',
|
||||
pos: [300, 0],
|
||||
size: [200, 100],
|
||||
@@ -58,14 +59,14 @@ export const nestedSubgraphProxyWidgets = {
|
||||
},
|
||||
name: 'SubgraphA',
|
||||
config: {},
|
||||
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [400, 100, 140, 126] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [{ id: 3, name: 'seed' }],
|
||||
widgets: [{ id: asNodeId(3), name: 'seed' }],
|
||||
nodes: [
|
||||
{
|
||||
id: 3,
|
||||
id: asNodeId(3),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -74,7 +75,7 @@ export const nestedSubgraphProxyWidgets = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: asNodeId(8),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -83,7 +84,7 @@ export const nestedSubgraphProxyWidgets = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 37,
|
||||
id: asNodeId(37),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -92,7 +93,7 @@ export const nestedSubgraphProxyWidgets = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 50,
|
||||
id: asNodeId(50),
|
||||
type: '22222222-2222-4222-8222-222222222222',
|
||||
pos: [200, 0],
|
||||
size: [100, 50],
|
||||
@@ -105,9 +106,9 @@ export const nestedSubgraphProxyWidgets = {
|
||||
links: [
|
||||
{
|
||||
id: 1,
|
||||
origin_id: 3,
|
||||
origin_id: asNodeId(3),
|
||||
origin_slot: 0,
|
||||
target_id: 8,
|
||||
target_id: asNodeId(8),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
}
|
||||
@@ -126,14 +127,14 @@ export const nestedSubgraphProxyWidgets = {
|
||||
},
|
||||
name: 'SubgraphB',
|
||||
config: {},
|
||||
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [400, 100, 140, 126] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [{ id: 8, name: 'prompt' }],
|
||||
widgets: [{ id: asNodeId(8), name: 'prompt' }],
|
||||
nodes: [
|
||||
{
|
||||
id: 3,
|
||||
id: asNodeId(3),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -142,7 +143,7 @@ export const nestedSubgraphProxyWidgets = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: asNodeId(8),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -151,7 +152,7 @@ export const nestedSubgraphProxyWidgets = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 37,
|
||||
id: asNodeId(37),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -163,9 +164,9 @@ export const nestedSubgraphProxyWidgets = {
|
||||
links: [
|
||||
{
|
||||
id: 2,
|
||||
origin_id: 3,
|
||||
origin_id: asNodeId(3),
|
||||
origin_slot: 0,
|
||||
target_id: 37,
|
||||
target_id: asNodeId(37),
|
||||
target_slot: 0,
|
||||
type: 'string'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SerialisableGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
/**
|
||||
* Workflow where lastNodeId is near the MAX_NODE_ID ceiling (100_000_000)
|
||||
@@ -20,7 +21,7 @@ export const nodeIdSpaceExhausted = {
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 102,
|
||||
id: asNodeId(102),
|
||||
type: '11111111-1111-4111-8111-111111111111',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
@@ -30,7 +31,7 @@ export const nodeIdSpaceExhausted = {
|
||||
properties: { proxyWidgets: [['3', 'seed']] }
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
id: asNodeId(103),
|
||||
type: '22222222-2222-4222-8222-222222222222',
|
||||
pos: [300, 0],
|
||||
size: [200, 100],
|
||||
@@ -40,7 +41,7 @@ export const nodeIdSpaceExhausted = {
|
||||
properties: { proxyWidgets: [['8', 'prompt']] }
|
||||
},
|
||||
{
|
||||
id: 100_000_000,
|
||||
id: asNodeId(100_000_000),
|
||||
type: 'dummy',
|
||||
pos: [600, 0],
|
||||
size: [100, 50],
|
||||
@@ -63,14 +64,14 @@ export const nodeIdSpaceExhausted = {
|
||||
},
|
||||
name: 'SubgraphA',
|
||||
config: {},
|
||||
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [400, 100, 140, 126] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [{ id: 3, name: 'seed' }],
|
||||
widgets: [{ id: asNodeId(3), name: 'seed' }],
|
||||
nodes: [
|
||||
{
|
||||
id: 3,
|
||||
id: asNodeId(3),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -79,7 +80,7 @@ export const nodeIdSpaceExhausted = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: asNodeId(8),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -88,7 +89,7 @@ export const nodeIdSpaceExhausted = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 37,
|
||||
id: asNodeId(37),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -100,9 +101,9 @@ export const nodeIdSpaceExhausted = {
|
||||
links: [
|
||||
{
|
||||
id: 1,
|
||||
origin_id: 3,
|
||||
origin_id: asNodeId(3),
|
||||
origin_slot: 0,
|
||||
target_id: 8,
|
||||
target_id: asNodeId(8),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
}
|
||||
@@ -121,14 +122,14 @@ export const nodeIdSpaceExhausted = {
|
||||
},
|
||||
name: 'SubgraphB',
|
||||
config: {},
|
||||
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [400, 100, 140, 126] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [{ id: 8, name: 'prompt' }],
|
||||
widgets: [{ id: asNodeId(8), name: 'prompt' }],
|
||||
nodes: [
|
||||
{
|
||||
id: 3,
|
||||
id: asNodeId(3),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -137,7 +138,7 @@ export const nodeIdSpaceExhausted = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
id: asNodeId(8),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -146,7 +147,7 @@ export const nodeIdSpaceExhausted = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 37,
|
||||
id: asNodeId(37),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -158,9 +159,9 @@ export const nodeIdSpaceExhausted = {
|
||||
links: [
|
||||
{
|
||||
id: 2,
|
||||
origin_id: 3,
|
||||
origin_id: asNodeId(3),
|
||||
origin_slot: 0,
|
||||
target_id: 37,
|
||||
target_id: asNodeId(37),
|
||||
target_slot: 0,
|
||||
type: 'string'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { SerialisableGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import { asNodeId } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
/**
|
||||
* Workflow with two subgraph definitions whose internal nodes already
|
||||
@@ -20,7 +21,7 @@ export const uniqueSubgraphNodeIds = {
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: 102,
|
||||
id: asNodeId(102),
|
||||
type: '11111111-1111-4111-8111-111111111111',
|
||||
pos: [0, 0],
|
||||
size: [200, 100],
|
||||
@@ -30,7 +31,7 @@ export const uniqueSubgraphNodeIds = {
|
||||
properties: { proxyWidgets: [['10', 'seed']] }
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
id: asNodeId(103),
|
||||
type: '22222222-2222-4222-8222-222222222222',
|
||||
pos: [300, 0],
|
||||
size: [200, 100],
|
||||
@@ -54,14 +55,14 @@ export const uniqueSubgraphNodeIds = {
|
||||
},
|
||||
name: 'SubgraphA',
|
||||
config: {},
|
||||
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [400, 100, 140, 126] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [{ id: 10, name: 'seed' }],
|
||||
widgets: [{ id: asNodeId(10), name: 'seed' }],
|
||||
nodes: [
|
||||
{
|
||||
id: 10,
|
||||
id: asNodeId(10),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -70,7 +71,7 @@ export const uniqueSubgraphNodeIds = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
id: asNodeId(11),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -79,7 +80,7 @@ export const uniqueSubgraphNodeIds = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
id: asNodeId(12),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -91,9 +92,9 @@ export const uniqueSubgraphNodeIds = {
|
||||
links: [
|
||||
{
|
||||
id: 1,
|
||||
origin_id: 10,
|
||||
origin_id: asNodeId(10),
|
||||
origin_slot: 0,
|
||||
target_id: 11,
|
||||
target_id: asNodeId(11),
|
||||
target_slot: 0,
|
||||
type: 'number'
|
||||
}
|
||||
@@ -112,14 +113,14 @@ export const uniqueSubgraphNodeIds = {
|
||||
},
|
||||
name: 'SubgraphB',
|
||||
config: {},
|
||||
inputNode: { id: -10, bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: -20, bounding: [400, 100, 140, 126] },
|
||||
inputNode: { id: asNodeId(-10), bounding: [10, 100, 150, 126] },
|
||||
outputNode: { id: asNodeId(-20), bounding: [400, 100, 140, 126] },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
widgets: [{ id: 21, name: 'prompt' }],
|
||||
widgets: [{ id: asNodeId(21), name: 'prompt' }],
|
||||
nodes: [
|
||||
{
|
||||
id: 20,
|
||||
id: asNodeId(20),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -128,7 +129,7 @@ export const uniqueSubgraphNodeIds = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
id: asNodeId(21),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -137,7 +138,7 @@ export const uniqueSubgraphNodeIds = {
|
||||
mode: 0
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
id: asNodeId(22),
|
||||
type: 'dummy',
|
||||
pos: [0, 0],
|
||||
size: [100, 50],
|
||||
@@ -149,9 +150,9 @@ export const uniqueSubgraphNodeIds = {
|
||||
links: [
|
||||
{
|
||||
id: 2,
|
||||
origin_id: 20,
|
||||
origin_id: asNodeId(20),
|
||||
origin_slot: 0,
|
||||
target_id: 22,
|
||||
target_id: asNodeId(22),
|
||||
target_slot: 0,
|
||||
type: 'string'
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import {
|
||||
UNASSIGNED_NODE_ID,
|
||||
SUBGRAPH_INPUT_NODE_ID,
|
||||
SUBGRAPH_OUTPUT_NODE_ID,
|
||||
isFloatingNodeId
|
||||
} from '@/types/nodeId'
|
||||
import type { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
} from '@/lib/litegraph/src/constants'
|
||||
import type { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget'
|
||||
import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap'
|
||||
import type {
|
||||
@@ -37,13 +39,13 @@ export class FloatingRenderLink implements RenderLink {
|
||||
readonly fromDirection: LinkDirection
|
||||
readonly fromSlotIndex: SlotIndex
|
||||
|
||||
readonly outputNodeId: NodeId = -1
|
||||
readonly outputNodeId: NodeId = UNASSIGNED_NODE_ID
|
||||
readonly outputNode?: LGraphNode
|
||||
readonly outputSlot?: INodeOutputSlot
|
||||
readonly outputIndex: number = -1
|
||||
readonly outputPos?: Point
|
||||
|
||||
readonly inputNodeId: NodeId = -1
|
||||
readonly inputNodeId: NodeId = UNASSIGNED_NODE_ID
|
||||
readonly inputNode?: LGraphNode
|
||||
readonly inputSlot?: INodeInputSlot
|
||||
readonly inputIndex: number = -1
|
||||
@@ -63,7 +65,7 @@ export class FloatingRenderLink implements RenderLink {
|
||||
target_slot: inputIndex
|
||||
} = link
|
||||
|
||||
if (outputNodeId !== -1) {
|
||||
if (!isFloatingNodeId(outputNodeId)) {
|
||||
// Output connected
|
||||
const outputNode = network.getNodeById(outputNodeId) ?? undefined
|
||||
if (!outputNode)
|
||||
@@ -175,7 +177,7 @@ export class FloatingRenderLink implements RenderLink {
|
||||
_events?: CustomEventTarget<LinkConnectorEventMap>
|
||||
): void {
|
||||
const floatingLink = this.link
|
||||
floatingLink.origin_id = SUBGRAPH_INPUT_ID
|
||||
floatingLink.origin_id = SUBGRAPH_INPUT_NODE_ID
|
||||
floatingLink.origin_slot = input.parent.slots.indexOf(input)
|
||||
|
||||
this.fromSlot._floatingLinks?.delete(floatingLink)
|
||||
@@ -188,7 +190,7 @@ export class FloatingRenderLink implements RenderLink {
|
||||
_events?: CustomEventTarget<LinkConnectorEventMap>
|
||||
): void {
|
||||
const floatingLink = this.link
|
||||
floatingLink.origin_id = SUBGRAPH_OUTPUT_ID
|
||||
floatingLink.origin_id = SUBGRAPH_OUTPUT_NODE_ID
|
||||
floatingLink.origin_slot = output.parent.slots.indexOf(output)
|
||||
|
||||
this.fromSlot._floatingLinks?.delete(floatingLink)
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ISlotType
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
asNodeId,
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
LLink,
|
||||
@@ -46,7 +47,8 @@ const test = baseTest.extend<TestContext>({
|
||||
reroutes,
|
||||
floatingLinks,
|
||||
getLink: graph.getLink.bind(graph),
|
||||
getNodeById: (id: number) => graph.getNodeById(id),
|
||||
getNodeById: graph.getNodeById.bind(graph),
|
||||
getNodeByRawId: graph.getNodeByRawId.bind(graph),
|
||||
addFloatingLink: (link: LLink) => {
|
||||
floatingLinks.set(link.id, link)
|
||||
return link
|
||||
@@ -74,7 +76,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 = asNodeId(id)
|
||||
network.add(node)
|
||||
return node
|
||||
})
|
||||
@@ -87,7 +89,14 @@ const test = baseTest.extend<TestContext>({
|
||||
targetId: number,
|
||||
slotType: ISlotType = 'number'
|
||||
): LLink => {
|
||||
const link = new LLink(id, slotType, sourceId, 0, targetId, 0)
|
||||
const link = new LLink(
|
||||
id,
|
||||
slotType,
|
||||
asNodeId(sourceId),
|
||||
0,
|
||||
asNodeId(targetId),
|
||||
0
|
||||
)
|
||||
network.links.set(link.id, link)
|
||||
return link
|
||||
}
|
||||
@@ -121,7 +130,7 @@ describe('LinkConnector', () => {
|
||||
sourceNode.addOutput('out', slotType)
|
||||
targetNode.addInput('in', slotType)
|
||||
|
||||
const link = new LLink(1, slotType, 1, 0, 2, 0)
|
||||
const link = new LLink(1, slotType, asNodeId(1), 0, asNodeId(2), 0)
|
||||
network.links.set(link.id, link)
|
||||
targetNode.inputs[0].link = link.id
|
||||
|
||||
@@ -158,7 +167,7 @@ describe('LinkConnector', () => {
|
||||
sourceNode.addOutput('out', slotType)
|
||||
targetNode.addInput('in', slotType)
|
||||
|
||||
const link = new LLink(1, slotType, 1, 0, 2, 0)
|
||||
const link = new LLink(1, slotType, asNodeId(1), 0, asNodeId(2), 0)
|
||||
network.links.set(link.id, link)
|
||||
sourceNode.outputs[0].links = [link.id]
|
||||
|
||||
@@ -261,7 +270,7 @@ describe('LinkConnector', () => {
|
||||
connector.state.multi = true
|
||||
connector.state.draggingExistingLinks = true
|
||||
|
||||
const link = new LLink(1, 'number', 1, 0, 2, 0)
|
||||
const link = new LLink(1, 'number', asNodeId(1), 0, asNodeId(2), 0)
|
||||
link._dragging = true
|
||||
connector.inputLinks.push(link)
|
||||
|
||||
@@ -302,7 +311,7 @@ describe('LinkConnector', () => {
|
||||
fromPos: [0, 0],
|
||||
fromDirection: LinkDirection.RIGHT,
|
||||
toType: 'input',
|
||||
link: new LLink(1, 'number', 1, 0, 2, 0)
|
||||
link: new LLink(1, 'number', asNodeId(1), 0, asNodeId(2), 0)
|
||||
} as MovingInputLink
|
||||
|
||||
connector.events.dispatch('input-moved', mockRenderLink)
|
||||
@@ -319,7 +328,7 @@ describe('LinkConnector', () => {
|
||||
connector.state.connectingTo = 'input'
|
||||
connector.state.multi = true
|
||||
|
||||
const link = new LLink(1, 'number', 1, 0, 2, 0)
|
||||
const link = new LLink(1, 'number', asNodeId(1), 0, asNodeId(2), 0)
|
||||
connector.inputLinks.push(link)
|
||||
|
||||
const exported = connector.export(network)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user