Compare commits

...

22 Commits

Author SHA1 Message Date
DrJKL
28f39e2672 Merge remote-tracking branch 'origin/main' into drjkl/now-for-nodes
# Conflicts:
#	src/platform/workflow/management/stores/comfyWorkflow.ts
#	src/stores/executionStore.ts
2026-06-22 17:33:57 -07:00
DrJKL
b058be561c refactor: brand NodeLocatorId and NodeExecutionId as distinct types
Amp-Thread-ID: https://ampcode.com/threads/T-019edd66-1adb-72ac-9404-a95f11ec5a42
Co-authored-by: Amp <amp@ampcode.com>
2026-06-18 18:27:18 -07:00
DrJKL
cac8db09cc Merge remote-tracking branch 'origin/main' into drjkl/now-for-nodes
Amp-Thread-ID: https://ampcode.com/threads/T-019edd49-585e-726c-9606-b18da6d3b68a
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	browser_tests/fixtures/utils/litegraphUtils.ts
#	src/extensions/core/groupNode.ts
#	src/utils/executableGroupNodeChildDTO.test.ts
#	src/utils/executableGroupNodeChildDTO.ts
2026-06-18 17:41:40 -07:00
DrJKL
1b815ab260 Merge remote-tracking branch 'origin/main' into drjkl/now-for-nodes
Amp-Thread-ID: https://ampcode.com/threads/T-019ed8d4-c55a-7683-b7df-f4fbaf177fc3
Co-authored-by: Amp <amp@ampcode.com>

# Conflicts:
#	src/components/error/ErrorOverlay.test.ts
#	src/components/error/useErrorOverlayState.test.ts
#	src/components/rightSidePanel/errors/useErrorGroups.test.ts
#	src/lib/litegraph/src/LGraph.ts
#	src/lib/litegraph/src/LGraphNode.ts
2026-06-17 21:00:57 -07:00
DrJKL
0ef3ba2d20 refactor: tighten NodeId typing and collapse sentinels
- Remove asNodeId laundering on string-keyed execution-id records
- Key ComfyApiWorkflow and ws/task records by string to keep them total
- Drop LinkEndpointNodeId alias; consumers use NodeId
- Collapse FLOATING_LINK_NODE_ID into UNASSIGNED_NODE_ID
- Make SpatialIndexManager generic over key type
- Keep boundary normalization in layoutStore for legacy numeric ids

Amp-Thread-ID: https://ampcode.com/threads/T-019ed371-abc5-72bb-aba2-749eca29ef23
Co-authored-by: Amp <amp@ampcode.com>
2026-06-17 12:11:32 -07:00
DrJKL
0e6c1bdc9e test: brand missing-model candidate ids in TabErrors after rebase
origin/main added error-tab tests using plain string nodeIds; brand them with
asNodeId so they satisfy the consolidated NodeId type.

Amp-Thread-ID: https://ampcode.com/threads/T-019ed1de-8ee2-7418-850c-76eb6b11e0c0
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 17:02:45 -07:00
DrJKL
7090f4b6d0 docs: note groupNode event cast hides type mismatch, not worth fixing
GroupNodes are slated for removal soon, so document why the EventDetail/NodeId
cast is left in place rather than refactoring the event contract.

Amp-Thread-ID: https://ampcode.com/threads/T-019ed1de-8ee2-7418-850c-76eb6b11e0c0
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:19 -07:00
DrJKL
1547db1580 fix: harden NodeId numeric semantics and legacy clipboard remap
- isNumericNodeId: number and string forms now agree; negative sentinels
  (-1, -10, -20) are non-numeric, matching the counter-id intent
- nodeIdToNumber: throw TypeError instead of silently returning NaN
- Normalize remap keys with asNodeId in clipboard subgraph dedup so legacy
  numeric ids match normalized lookups
- Accept legacy numeric ids in proxyWidgets/previewExposures remapping and
  centralize the sentinel skip in remapNodeId
- Add nodeId unit tests and a numeric-legacy clipboard remap regression test

Amp-Thread-ID: https://ampcode.com/threads/T-019ed1de-8ee2-7418-850c-76eb6b11e0c0
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:18 -07:00
DrJKL
a588de6b92 fix: address NodeId normalization edge cases and remove brand casts
- Drop Number() coercion in nodeOutputStore so string/UUID ids survive
- Skip floating links when writing link layout state
- Reject empty execution-id segments in isNodeExecutionId
- Normalize endpoints with asNodeId in patchLinkNodeIds
- Replace direct `as NodeId` casts with asNodeId (the brand fn is the only cast)
- Use UNASSIGNED_NODE_ID in SubgraphEditor.hideAll
- Fix vacuous .not.toBe(<number>) NodeId assertions in tests
- Restore numeric legacy id in oldSchemaGraph fixture and refresh snapshot

Amp-Thread-ID: https://ampcode.com/threads/T-019ed1de-8ee2-7418-850c-76eb6b11e0c0
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:17 -07:00
DrJKL
67442d8180 fix: include node header in Vue-node selection toolbox bounds
The toolbox position used layout-store bounds whose y is the node body
top (header excluded), placing the toolbox over the title bar. Now that
node ids are branded strings, all Vue nodes take this path instead of
the LiteGraph fallback, so the regression became visible.

Expand the Vue-node bounds upward by NODE_TITLE_HEIGHT to reach the
visual node top, matching the LiteGraph fallback and restoring the
toolbox position above the header.

Amp-Thread-ID: https://ampcode.com/threads/T-019ecdfd-9b11-7788-a778-c3172460fb4b
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:16 -07:00
DrJKL
6fa43949ec test: align browser tests with string NodeId edges
Update e2e specs and helpers to treat node ids as branded strings:
normalize ids at helper boundaries via asNodeId/NodeIdInput, return
branded ids from selection helpers, and drop numeric-id assumptions.

Amp-Thread-ID: https://ampcode.com/threads/T-019ecdfd-9b11-7788-a778-c3172460fb4b
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:16 -07:00
DrJKL
b27c2cfca2 refactor: serialize link endpoints as branded string NodeId
Emit branded string NodeId for link-endpoint sentinels on the wire
instead of down-converting to numeric -1/-10/-20. Removes the
SerialisedLinkEndpointNodeId union and serialiseLinkEndpointNodeId,
making the serialized form uniformly string-typed.

Runtime subgraph IO node ids are now the branded string sentinels too,
eliminating the last numeric ids. Read paths remain tolerant of legacy
numeric values via asNodeId and the sentinel predicates, so existing
workflows still load.

Amp-Thread-ID: https://ampcode.com/threads/T-019ecdfd-9b11-7788-a778-c3172460fb4b
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:12 -07:00
DrJKL
bb013a5e74 refactor: tighten node-id edge types and lock invariants with tests
- LLink constructor accepts NodeIdInput (handles legacy numeric node ids,
  not just sentinels)
- MovingLinkBase endpoint fields are NodeId (sourced from branded LLink)
- LinkEndpointNodeId is a deprecated alias of SerialisedLinkEndpointNodeId
- export branded sentinels + tolerant predicates from the litegraph barrel
- add LLink regression tests for sentinel branding and wire round-trip

Amp-Thread-ID: https://ampcode.com/threads/T-019ecdfd-9b11-7788-a778-c3172460fb4b
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:08 -07:00
DrJKL
043fd943f7 refactor: brand link endpoints as NodeId at edges (phase 2)
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019ecdfd-9b11-7788-a778-c3172460fb4b
2026-06-16 16:58:08 -07:00
DrJKL
ddda65a39d refactor: add branded node-id sentinels and tolerant predicates (phase 1)
Amp-Thread-ID: https://ampcode.com/threads/T-019ecdfd-9b11-7788-a778-c3172460fb4b
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:07 -07:00
DrJKL
7992177927 fix: normalize legacy numeric node ids in vue layout rendering layer
Amp-Thread-ID: https://ampcode.com/threads/T-019ecdfd-9b11-7788-a778-c3172460fb4b
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:04 -07:00
DrJKL
e9c61493b3 fix: normalize node id in batchUpdateNodeBounds for legacy numeric ids
Amp-Thread-ID: https://ampcode.com/threads/T-019ecdfd-9b11-7788-a778-c3172460fb4b
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:58:03 -07:00
DrJKL
bae159cd1d refactor: Move NodeId to a top level types module 2026-06-16 16:58:00 -07:00
DrJKL
62284c9abc docs: Update intended Entity ID names to remove redundant infix 2026-06-16 16:57:58 -07:00
DrJKL
b179a3f4f3 refactor: move nodeId to src/types alongside widgetId
Relocate nodeId.ts from litegraph utils to src/types and update all
importers to @/types/nodeId. Trim verbose JSDoc in favor of
self-documenting code.

Amp-Thread-ID: https://ampcode.com/threads/T-019ecd36-e558-711c-92e2-1c3fc0a5b00b
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:57:56 -07:00
DrJKL
62f58354b4 refactor: brand and consolidate NodeId as string
Consolidate the four NodeId definitions into a single canonical branded
type (string & { __brand: 'NodeId' }) in litegraph's nodeId util module,
re-exported everywhere. Normalize raw boundary values via asNodeId(); add
LinkEndpointNodeId for floating/subgraph-IO numeric sentinels. Runtime and
on-disk node ids are now strings; legacy numeric workflows still load via
the zNodeId transform.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019ecd17-c144-774d-b574-e70db46245cc
2026-06-16 16:57:46 -07:00
DrJKL
c847cf06c1 refactor: stringify node id generation and serialization
Always assign string node ids at runtime and on disk. Add nodeId helpers
(UNASSIGNED_NODE_ID, isUnassignedNodeId, isNumericNodeId, nodeIdToNumber)
and route id generation, load, dedup, and clipboard remap through them.
Legacy numeric workflows still load; ids become strings once re-saved.

Precursor to branding NodeId.

Amp-Thread-ID: https://ampcode.com/threads/T-019eae9f-0c9f-75fd-827c-eb8f2fa85485
Co-authored-by: Amp <amp@ampcode.com>
2026-06-16 16:57:44 -07:00
260 changed files with 2509 additions and 1620 deletions

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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']

View File

@@ -74,7 +74,7 @@ test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => {
// Count links from Switch(CFG) to node 85 cfg (should be 1, not 2)
let cfgLinkToNode85Count = 0
for (const link of subgraph.links.values()) {
if (link.origin_id === 120 && link.target_id === 85)
if (link.origin_id === '120' && link.target_id === '85')
cfgLinkToNode85Count++
}

View File

@@ -99,10 +99,10 @@ test.describe('Node replacement', { tag: ['@node', '@ui'] }, () => {
expect(
ksampler?.id,
'Replaced node should keep the original id'
).toBe(1)
).toBe('1')
const linkFromReplacedToDecode = workflow.links?.find(
(l) => l[1] === 1 && l[3] === 2
(l) => l[1] === '1' && l[3] === '2'
)
expect(
linkFromReplacedToDecode,

View File

@@ -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()

View File

@@ -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)

View File

@@ -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 { asNodeId } from '@/types/nodeId'
/**
* 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 = asNodeId('9')
const COVER_FILENAME = 'cover_00001_.png'
const DUPLICATE_FILENAME = 'duplicate_00002_.png'
const DISTINCT_FILENAMES = ['distinct_00003_.png', 'distinct_00004_.png']

View File

@@ -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',

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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,11 +586,10 @@ 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])
expect(rootIds).toEqual(['1', '2', '5'])
})
test('Promoted widget tuples are stable after full page reload boot path', async ({
@@ -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

View File

@@ -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

View File

@@ -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))
}
@@ -197,9 +195,7 @@ test.describe('Workflow settings', { tag: '@canvas' }, () => {
// must enumerate the same node set regardless of the sort flag.
const apiPrompt: ComfyApiWorkflow =
await comfyPage.workflow.getExportedWorkflow({ api: true })
expect(ascendingById(Object.keys(apiPrompt).map(Number))).toEqual(
expectedIds
)
expect(ascendingById(Object.keys(apiPrompt))).toEqual(expectedIds)
})
})
})

View File

@@ -13,12 +13,13 @@ import type {
RawJobListItem,
zJobsListResponse
} from '@/platform/remote/comfyui/jobs/jobTypes'
import { asNodeId } from '@/types/nodeId'
type JobsListResponse = z.infer<typeof zJobsListResponse>
const test = mergeTests(comfyPageFixture, webSocketFixture)
const KSAMPLER_NODE = '3'
const KSAMPLER_NODE = asNodeId('3')
const EXECUTING_CLASS = /outline-node-stroke-executing/
const QUEUE_ROUTE = /\/api\/jobs\?[^/]*status=in_progress,pending/

View File

@@ -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` and `WidgetId` are branded `string`: node ids are canonicalized from legacy `number | string` values to `string` at load/serialization boundaries (via `asNodeId`), and widget ids are composite path strings. 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 = string & { 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

View File

@@ -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,

View File

@@ -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 })

View File

@@ -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()

View File

@@ -5,6 +5,7 @@ import { defineComponent, nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import { useErrorOverlayState } from './useErrorOverlayState'
import { asNodeId } from '@/lib/litegraph/src/litegraph'
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: asNodeId(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: asNodeId('1'),
nodeType: 'LoadImage',
widgetName: 'image',
mediaType: 'image',
@@ -285,7 +286,7 @@ describe('useErrorOverlayState', () => {
},
referencingNodes: [
{
nodeId: '1',
nodeId: asNodeId('1'),
nodeType: 'LoadImage',
widgetName: 'image'
}
@@ -312,7 +313,7 @@ describe('useErrorOverlayState', () => {
const missingMediaStore = useMissingMediaStore()
missingMediaStore.setMissingMedia([
{
nodeId: '1',
nodeId: asNodeId('1'),
nodeType: 'LoadImage',
widgetName: 'image',
mediaType: 'image',
@@ -338,7 +339,7 @@ describe('useErrorOverlayState', () => {
{
name: 'missing.safetensors',
representative: {
nodeId: '1',
nodeId: asNodeId('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: asNodeId('1'), widgetName: 'ckpt_name' },
{ nodeId: asNodeId('2'), widgetName: 'ckpt_name' }
]
}
]
@@ -420,7 +421,7 @@ describe('useErrorOverlayState', () => {
{
name: 'first.safetensors',
representative: {
nodeId: '1',
nodeId: asNodeId('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: asNodeId('1'), widgetName: 'ckpt_name' }
]
},
{
name: 'second.safetensors',
representative: {
nodeId: '2',
nodeId: asNodeId('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: asNodeId('2'), widgetName: 'ckpt_name' }
]
}
]
}

View File

@@ -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)

View File

@@ -16,7 +16,7 @@ import { default as DOMPurify } from 'dompurify'
import Skeleton from 'primevue/skeleton'
import { computed, onMounted, watch } from 'vue'
import type { NodeId } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/types/nodeId'
import { useExecutionStore } from '@/stores/executionStore'
import { linkifyHtml, nl2br } from '@/utils/formatUtil'

View File

@@ -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'

View File

@@ -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(() => ({
@@ -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],
@@ -168,7 +170,7 @@ describe('Load3D', () => {
resolveNodeMock.mockReturnValue(MOCK_NODE)
renderLoad3D({ widget: {}, nodeId: 42 })
expect(resolveNodeMock).toHaveBeenCalledWith(42)
expect(resolveNodeMock).toHaveBeenCalledWith('42')
expect(await screen.findByTestId('load3d-scene')).toBeInTheDocument()
})

View File

@@ -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'

View File

@@ -22,6 +22,7 @@ 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', () => {
@@ -40,7 +41,9 @@ 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' } })
render(Load3DAdvanced, {
props: { widget: widget as never, nodeId: asNodeId('a') }
})
expect(lastProps.value?.widget).toEqual(widget)
expect(lastProps.value?.nodeId).toBe('a')
})

View File

@@ -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'

View File

@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { asNodeId } from '@/types/nodeId'
import type {
JobListItem,
JobStatus
@@ -154,7 +155,7 @@ export const Queued: Story = {
value: 1,
max: 1,
state: 'running',
node_id: '1',
node_id: asNodeId('1'),
prompt_id: 'p1'
}
}
@@ -204,7 +205,7 @@ export const QueuedParallel: Story = {
value: 1,
max: 2,
state: 'running',
node_id: '1',
node_id: asNodeId('1'),
prompt_id: 'p1'
}
},
@@ -213,7 +214,7 @@ export const QueuedParallel: Story = {
value: 1,
max: 2,
state: 'running',
node_id: '2',
node_id: asNodeId('2'),
prompt_id: 'p2'
}
}
@@ -254,7 +255,7 @@ export const Running: Story = {
value: 5,
max: 10,
state: 'running',
node_id: '1',
node_id: asNodeId('1'),
prompt_id: 'p1'
}
}
@@ -299,7 +300,7 @@ export const QueuedZeroAheadSingleRunning: Story = {
value: 1,
max: 3,
state: 'running',
node_id: '1',
node_id: asNodeId('1'),
prompt_id: 'p1'
}
}
@@ -347,7 +348,7 @@ export const QueuedZeroAheadMultiRunning: Story = {
value: 2,
max: 5,
state: 'running',
node_id: '1',
node_id: asNodeId('1'),
prompt_id: 'p1'
}
},
@@ -356,7 +357,7 @@ export const QueuedZeroAheadMultiRunning: Story = {
value: 3,
max: 5,
state: 'running',
node_id: '2',
node_id: asNodeId('2'),
prompt_id: 'p2'
}
}

View File

@@ -5,6 +5,7 @@ import PrimeVue from 'primevue/config'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import TabErrors from './TabErrors.vue'
import { asNodeId } from '@/lib/litegraph/src/litegraph'
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
import type { MissingMediaCandidate } from '@/platform/missingMedia/types'
import type { MissingModelCandidate } from '@/platform/missingModel/types'
@@ -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: asNodeId('1'),
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
name: 'local-only.safetensors',
@@ -429,7 +430,7 @@ describe('TabErrors.vue', () => {
missingModel: {
missingModelCandidates: [
{
nodeId: '1',
nodeId: asNodeId('1'),
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
name: 'model-a.safetensors',
@@ -438,7 +439,7 @@ describe('TabErrors.vue', () => {
isAssetSupported: true
},
{
nodeId: '2',
nodeId: asNodeId('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: asNodeId('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: asNodeId('3'),
nodeType: 'LoadImage',
widgetName: 'image',
mediaType: 'image',
@@ -519,7 +520,7 @@ describe('TabErrors.vue', () => {
missingMedia: {
missingMediaCandidates: [
{
nodeId: '3',
nodeId: asNodeId('3'),
nodeType: 'LoadImage',
widgetName: 'image',
mediaType: 'image',
@@ -527,7 +528,7 @@ describe('TabErrors.vue', () => {
isMissing: true
},
{
nodeId: '4',
nodeId: asNodeId('4'),
nodeType: 'PreviewImage',
widgetName: 'image',
mediaType: 'image',
@@ -595,7 +596,7 @@ describe('TabErrors.vue', () => {
missingMedia: {
missingMediaCandidates: [
{
nodeId: '3',
nodeId: asNodeId('3'),
nodeType: 'LoadImage',
widgetName: 'image',
mediaType: 'image',
@@ -603,7 +604,7 @@ describe('TabErrors.vue', () => {
isMissing: true
},
{
nodeId: '4',
nodeId: asNodeId('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: asNodeId('1'),
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
name: 'downloadable.safetensors',

View File

@@ -3,6 +3,7 @@ import { createPinia, setActivePinia } from 'pinia'
import { nextTick, ref } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { asNodeId } from '@/lib/litegraph/src/litegraph'
import type { MissingNodeType } from '@/types/comfy'
vi.mock('@/scripts/app', () => ({
@@ -166,7 +167,7 @@ function makeModel(
) {
return {
name,
nodeId: opts.nodeId ?? '1',
nodeId: asNodeId(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: asNodeId(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: asNodeId(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: asNodeId(5),
node_type: 'KSampler',
executed: [],
exception_type: 'torch.OutOfMemoryError',

View File

@@ -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,

View File

@@ -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'

View File

@@ -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'

View File

@@ -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',

View File

@@ -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'

View File

@@ -24,6 +24,7 @@ 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'
@@ -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,8 @@ 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._nodes_by_id[asNodeId(exposure.sourceNodeId)]
if (!sourceNode) return []
const realWidget = getPromotableWidgets(sourceNode).find(
(candidate) => candidate.name === exposure.sourcePreviewName
@@ -248,7 +250,7 @@ function rowDisplayName(row: ActiveRow): string {
function isRowLinked(row: ActiveRow): boolean {
if (row.kind !== 'promoted') return false
if (row.node.id === -1) return true
if (row.node.id === UNASSIGNED_NODE_ID) return true
const source = promotedRowSource(row)
return (
!!activeNode.value &&
@@ -321,7 +323,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)
}
}

View File

@@ -225,6 +225,7 @@ import {
} from 'vue'
import { useI18n } from 'vue-i18n'
import { asNodeId } from '@/types/nodeId'
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: asNodeId('0'),
mediaType: mediaType === 'image' ? 'images' : mediaType
})

View File

@@ -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 { asNodeId } from '@/types/nodeId'
import type { NodeId } from '@/types/nodeId'
import type { ResultItemImpl } from '@/stores/queueStore'
import MediaLightbox from './MediaLightbox.vue'
@@ -63,7 +64,7 @@ describe('MediaLightbox', () => {
filename: 'image1.jpg',
subfolder: 'outputs',
type: 'output',
nodeId: '123' as NodeId,
nodeId: asNodeId('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: asNodeId('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: asNodeId('789'),
mediaType: 'images',
isImage: true,
isVideo: false,

View File

@@ -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('mock-node')
this.boundingRect = [0, 0, 0, 0]
}

View File

@@ -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 {

View File

@@ -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'

View File

@@ -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
@@ -232,7 +232,7 @@ describe('Widget change error clearing via onWidgetChanged', () => {
seedRequiredInputMissingNodeError(store, String(node.id), 'image')
mediaStore.setMissingMedia([
{
nodeId: String(node.id),
nodeId: node.id,
nodeType: 'LoadImage',
widgetName: 'image',
mediaType: 'image',
@@ -371,7 +371,7 @@ describe('installErrorClearingHooks lifecycle', () => {
.spyOn(missingModelScan, 'scanNodeModelCandidates')
.mockImplementation((_rootGraph, node) => [
{
nodeId: String(node.id),
nodeId: node.id,
nodeType: node.type,
widgetName: 'ckpt_name',
isAssetSupported: false,
@@ -448,7 +448,7 @@ describe('onNodeRemoved clears missing asset errors by execution ID', () => {
Parameters<typeof modelStore.setMissingModels>[0][number],
unknown
>({
nodeId: String(node.id),
nodeId: node.id,
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
isAssetSupported: false,
@@ -471,7 +471,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)
@@ -507,7 +507,7 @@ 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)
@@ -565,7 +565,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: node.id,
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
isAssetSupported: true,
@@ -613,7 +613,7 @@ describe('realtime scan verifies pending cloud candidates', () => {
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([])
vi.spyOn(missingMediaScan, 'scanNodeMediaCandidates').mockReturnValue([
{
nodeId: String(node.id),
nodeId: node.id,
nodeType: 'LoadImage',
widgetName: 'image',
mediaType: 'image',
@@ -656,7 +656,7 @@ describe('realtime scan verifies pending cloud candidates', () => {
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([
{
nodeId: String(node.id),
nodeId: node.id,
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
isAssetSupported: true,
@@ -702,7 +702,7 @@ describe('realtime verification staleness guards', () => {
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([
{
nodeId: String(node.id),
nodeId: node.id,
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
isAssetSupported: true,
@@ -753,7 +753,7 @@ describe('realtime verification staleness guards', () => {
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([])
vi.spyOn(missingMediaScan, 'scanNodeMediaCandidates').mockReturnValue([
{
nodeId: String(node.id),
nodeId: node.id,
nodeType: 'LoadImage',
widgetName: 'image',
mediaType: 'image',
@@ -802,7 +802,7 @@ describe('realtime verification staleness guards', () => {
vi.spyOn(missingModelScan, 'scanNodeModelCandidates').mockReturnValue([
{
nodeId: String(nodeA.id),
nodeId: nodeA.id,
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
isAssetSupported: true,
@@ -863,7 +863,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 +873,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: asNodeId(`${subgraphNode.id}:${interiorNode.id}`),
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
isAssetSupported: false,
@@ -902,13 +902,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 +982,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 +1007,7 @@ describe('clearWidgetRelatedErrors parameter routing', () => {
const missingModelStore = useMissingModelStore()
missingModelStore.setMissingModels([
{
nodeId: '2:1',
nodeId: asNodeId('2:1'),
nodeType: 'CheckpointLoaderSimple',
widgetName: 'ckpt_name',
isAssetSupported: false,

View File

@@ -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
)

View File

@@ -4,7 +4,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { computed, nextTick, watch } from 'vue'
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,
@@ -224,7 +229,7 @@ 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)
@@ -355,7 +360,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,7 +371,9 @@ 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)
@@ -401,7 +410,7 @@ 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)
@@ -444,7 +453,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)
@@ -591,7 +600,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)
@@ -632,7 +641,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
missingModelStore.setMissingModels([
{
nodeId: String(nodeA.id),
nodeId: nodeA.id,
nodeType: 'CheckpointLoader',
widgetName: 'ckpt_name',
isAssetSupported: false,
@@ -652,7 +661,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
missingModelStore.setMissingModels([
{
nodeId: String(nodeA.id),
nodeId: nodeA.id,
nodeType: 'CheckpointLoader',
widgetName: 'ckpt_name',
isAssetSupported: false,
@@ -673,7 +682,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 +698,7 @@ describe('reconcileNodeErrorFlags (via lastNodeErrors watcher)', () => {
missingModelStore.setMissingModels([
{
nodeId: `${subgraphNode.id}:${interiorNode.id}`,
nodeId: asNodeId(`${subgraphNode.id}:${interiorNode.id}`),
nodeType: 'CheckpointLoader',
widgetName: 'ckpt_name',
isAssetSupported: false,

View File

@@ -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,
@@ -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)

View File

@@ -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'

View File

@@ -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
}

View File

@@ -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
)
}

View File

@@ -3,7 +3,7 @@ import { setActivePinia } from 'pinia'
import { reactive } from 'vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, asNodeId } from '@/lib/litegraph/src/litegraph'
import {
createTestSubgraph,
createTestSubgraphNode
@@ -58,7 +58,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 +69,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,7 +82,7 @@ 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
}
}
@@ -281,7 +281,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()
@@ -329,10 +331,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}`

View File

@@ -1,6 +1,7 @@
import type { MaybeRefOrGetter } from 'vue'
import { computed, toValue } from 'vue'
import { asNodeId } from '@/types/nodeId'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { UUID } from '@/utils/uuid'
@@ -43,7 +44,7 @@ export function usePromotedPreviews(
): string[] | undefined {
const locatorId = createNodeLocatorId(
leafHost.subgraph.id,
leafSourceNodeId
asNodeId(leafSourceNodeId)
)
const reactiveOutputs = nodeOutputStore.nodeOutputs[locatorId]
const reactivePreviews = nodeOutputStore.nodePreviewImages[locatorId]

View File

@@ -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 { asNodeId } from '@/lib/litegraph/src/litegraph'
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: asNodeId('node-1'),
mediaType: supportsPreview ? 'images' : 'unknown'
})
// Override url getter for test matching

View File

@@ -9,7 +9,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import WidgetImageCrop from '@/components/imagecrop/WidgetImageCrop.vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
import { 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: {

View File

@@ -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'

View File

@@ -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
}

View File

@@ -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)
})
}

View File

@@ -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'
@@ -262,7 +264,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,7 +280,12 @@ 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
}
]
: []
})
}
@@ -603,7 +610,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
}))

View File

@@ -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 {
@@ -71,7 +71,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,7 +82,9 @@ 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,
@@ -129,13 +133,13 @@ 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')
expect(result).toEqual({

View File

@@ -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 }
}

View File

@@ -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', '*')

View File

@@ -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])

View File

@@ -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 }

View File

@@ -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,
@@ -297,7 +298,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,
@@ -483,7 +484,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 +520,7 @@ describe('ensureGlobalIdUniqueness', () => {
rootGraph.ensureGlobalIdUniqueness()
expect(rootGraph.state.lastNodeId).toBeGreaterThanOrEqual(
subNode.id as number
Number(subNode.id)
)
})
@@ -536,7 +537,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 +556,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 +869,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 +917,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 +929,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 +953,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 +978,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[][]) {
@@ -1016,7 +1018,7 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
expect(migrationCall).toBeDefined()
expect(migrationCall![1]).toEqual(
expect.objectContaining({
hostNodeId: expect.any(Number),
hostNodeId: expect.any(String),
proxyWidgets: expect.anything()
})
)
@@ -1038,8 +1040,12 @@ describe('deduplicateSubgraphNodeIds (via configure)', () => {
const graph = new LGraph()
graph.configure(structuredClone(uniqueSubgraphNodeIds))
expect(nodeIdSet(graph, SUBGRAPH_A)).toEqual(new Set([10, 11, 12]))
expect(nodeIdSet(graph, SUBGRAPH_B)).toEqual(new Set([20, 21, 22]))
expect(nodeIdSet(graph, SUBGRAPH_A)).toEqual(
new Set([asNodeId(10), asNodeId(11), asNodeId(12)])
)
expect(nodeIdSet(graph, SUBGRAPH_B)).toEqual(
new Set([asNodeId(20), asNodeId(21), asNodeId(22)])
)
})
})

View File

@@ -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'
@@ -87,6 +83,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 +640,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 +957,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 +969,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
@@ -1128,8 +1139,8 @@ export class LGraph
/**
* Returns a node by its id.
*/
getNodeById(id: NodeId | null | undefined): LGraphNode | null {
return id != null ? this._nodes_by_id[id] : null
getNodeById(id: NodeIdInput | null | undefined): LGraphNode | null {
return id != null ? this._nodes_by_id[asNodeId(id)] : null
}
/**
@@ -1389,10 +1400,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 +1422,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 +1724,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 +1825,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 +1866,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 +1971,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 +2039,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 +2057,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 +2108,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 +2118,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
@@ -2499,15 +2509,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 +2567,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 +2689,24 @@ 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 = asNodeId(state.lastNodeId)
delete graph._nodes_by_id[oldId]
node.id = newId
graph._nodes_by_id[newId] = node
usedNodeIds.add(newId)
usedNodeIds.add(state.lastNodeId)
remappedIds.set(oldId, newId)
console.warn(
`LiteGraph: duplicate node ID ${oldId} reassigned to ${newId} in graph ${graph.id}`
)
} else {
usedNodeIds.add(node.id as number)
if ((node.id as number) > state.lastNodeId)
state.lastNodeId = node.id as number
usedNodeIds.add(numericId)
if (numericId > state.lastNodeId) state.lastNodeId = numericId
}
}
@@ -2877,16 +2890,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 +2913,7 @@ export class Subgraph
}
private _findLinkBySlot(
nodeId: number,
nodeId: NodeId,
slotIndex: number
): LLink | undefined {
for (const link of this._links.values()) {
@@ -3104,10 +3117,10 @@ function patchLinkNodeIds(
remappedIds: Map<NodeId, NodeId>
): void {
for (const link of links.values()) {
const newOrigin = remappedIds.get(link.origin_id)
const newOrigin = remappedIds.get(asNodeId(link.origin_id))
if (newOrigin !== undefined) link.origin_id = newOrigin
const newTarget = remappedIds.get(link.target_id)
const newTarget = remappedIds.get(asNodeId(link.target_id))
if (newTarget !== undefined) link.target_id = newTarget
}
}

View File

@@ -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: [],
@@ -209,7 +287,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 +342,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 +366,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 +418,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 +442,7 @@ describe('_deserializeItems paste-time migration & auto-expose', () => {
}
const hostInfo: ISerialisedNode = {
id: 99,
id: asNodeId(99),
type: subgraphId,
pos: [0, 0],
size: [140, 80],

View File

@@ -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)

View File

@@ -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 },

View File

@@ -112,6 +112,13 @@ 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,
UNASSIGNED_NODE_ID
} from '@/types/nodeId'
import { resolveConnectingLinkColor } from './utils/linkColors'
import { createUuidv4 } from '@/utils/uuid'
import { BaseWidget } from './widgets/BaseWidget'
@@ -4216,7 +4223,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 +4316,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 +8987,26 @@ 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. */
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 = asNodeId(nodeId)
if (normalized === UNASSIGNED_NODE_ID) return undefined
return remappedIds.get(normalized)
}
function remapProxyWidgets(
@@ -9015,21 +9022,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,7 +9050,7 @@ 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)
@@ -9057,17 +9064,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 +9084,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) {

View File

@@ -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],

View File

@@ -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
}
@@ -1999,9 +2008,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 +3171,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 +3281,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
}

View File

@@ -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],

View File

@@ -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('string')
})
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))
})
})
})

View File

@@ -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' }

View File

@@ -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)
}

View File

@@ -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: []
}

View File

@@ -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'
}

View File

@@ -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'
}

View File

@@ -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'
}

View File

@@ -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'
}

View File

@@ -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'
}

View File

@@ -3,9 +3,9 @@
exports[`LLink > matches previous snapshot > Basic 1`] = `
[
1,
4,
"4",
2,
5,
"5",
3,
"float",
]
@@ -14,9 +14,9 @@ exports[`LLink > matches previous snapshot > Basic 1`] = `
exports[`LLink > serializes to the previous snapshot > Basic 1`] = `
[
1,
4,
"4",
2,
5,
"5",
3,
"float",
]

View File

@@ -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)

View File

@@ -8,6 +8,7 @@ import type {
ISlotType
} from '@/lib/litegraph/src/litegraph'
import {
asNodeId,
LGraph,
LGraphNode,
LLink,
@@ -74,7 +75,7 @@ const test = baseTest.extend<TestContext>({
createTestNode: async ({ network }, use) => {
await use((id: number): LGraphNode => {
const node = new LGraphNode('test')
node.id = id
node.id = asNodeId(id)
network.add(node)
return node
})
@@ -87,7 +88,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 +129,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 +166,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 +269,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 +310,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 +327,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)

View File

@@ -8,10 +8,16 @@ import type {
CanvasPointerEvent,
RerouteId
} from '@/lib/litegraph/src/litegraph'
import { LGraphNode, LLink, LinkConnector } from '@/lib/litegraph/src/litegraph'
import {
asNodeId,
LGraphNode,
LLink,
LinkConnector
} from '@/lib/litegraph/src/litegraph'
import { test as baseTest } from '../__fixtures__/testExtensions'
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
import { UNASSIGNED_NODE_ID, isFloatingNodeId } from '@/types/nodeId'
import {
createMockCanvasPointerEvent,
createMockCanvasRenderingContext2D
@@ -59,7 +65,7 @@ const test = baseTest.extend<TestContext>({
createTestNode: async ({ graph }, use) => {
await use((id): LGraphNode => {
const node = new LGraphNode('test')
node.id = id
node.id = asNodeId(id)
graph.add(node)
return node
})
@@ -112,12 +118,12 @@ const test = baseTest.extend<TestContext>({
const link = graph.floatingLinks.get(linkId)
expect(link).toBeDefined()
if (link!.target_id === -1) {
expect(link!.origin_id).not.toBe(-1)
if (isFloatingNodeId(link!.target_id)) {
expect(link!.origin_id).not.toBe(UNASSIGNED_NODE_ID)
expect(link!.origin_slot).not.toBe(-1)
expect(link!.target_slot).toBe(-1)
} else {
expect(link!.origin_id).toBe(-1)
expect(link!.origin_id).toBe(UNASSIGNED_NODE_ID)
expect(link!.origin_slot).toBe(-1)
expect(link!.target_slot).not.toBe(-1)
}
@@ -150,8 +156,8 @@ const test = baseTest.extend<TestContext>({
}
for (const link of graph.floatingLinks.values()) {
if (link.target_id === -1) {
expect(link.origin_id).not.toBe(-1)
if (isFloatingNodeId(link.target_id)) {
expect(link.origin_id).not.toBe(UNASSIGNED_NODE_ID)
expect(link.origin_slot).not.toBe(-1)
expect(link.target_slot).toBe(-1)
const outputFloatingLinks = graph.getNodeById(link.origin_id)
@@ -159,7 +165,7 @@ const test = baseTest.extend<TestContext>({
expect(outputFloatingLinks).toBeDefined()
expect(outputFloatingLinks).toContain(link)
} else {
expect(link.origin_id).toBe(-1)
expect(link.origin_id).toBe(UNASSIGNED_NODE_ID)
expect(link.origin_slot).toBe(-1)
expect(link.target_slot).not.toBe(-1)
const inputFloatingLinks = graph.getNodeById(link.target_id)?.inputs[

View File

@@ -3,10 +3,7 @@ import { remove } from 'es-toolkit'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { 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 { isSubgraphInputNodeId, isSubgraphOutputNodeId } from '@/types/nodeId'
import { CustomEventTarget } from '@/lib/litegraph/src/infrastructure/CustomEventTarget'
import type { LinkConnectorEventMap } from '@/lib/litegraph/src/infrastructure/LinkConnectorEventMap'
import type {
@@ -183,7 +180,7 @@ export class LinkConnector {
if (!link) return
// Special handling for links from subgraph input nodes
if (link.origin_id === SUBGRAPH_INPUT_ID) {
if (isSubgraphInputNodeId(link.origin_id)) {
// For subgraph input links, we need to handle them differently
// since they don't have a regular output node
const subgraphInput = network.inputNode?.slots[link.origin_slot]
@@ -325,7 +322,7 @@ export class LinkConnector {
this.outputLinks.push(link)
try {
if (link.target_id === SUBGRAPH_OUTPUT_ID) {
if (isSubgraphOutputNodeId(link.target_id)) {
if (!(network instanceof Subgraph)) {
console.warn(
'Subgraph output link found in non-subgraph network.'
@@ -482,7 +479,7 @@ export class LinkConnector {
return
}
if (link.origin_id === SUBGRAPH_INPUT_ID) {
if (isSubgraphInputNodeId(link.origin_id)) {
if (!(network instanceof Subgraph)) {
console.warn('Subgraph input link found in non-subgraph network.')
return
@@ -546,7 +543,7 @@ export class LinkConnector {
return
}
if (link.target_id === SUBGRAPH_OUTPUT_ID) {
if (isSubgraphOutputNodeId(link.target_id)) {
if (!(network instanceof Subgraph)) {
console.warn('Subgraph output link found in non-subgraph network.')
return

View File

@@ -2,6 +2,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
asNodeId,
LinkConnector,
MovingOutputLink,
ToOutputRenderLink,
@@ -197,7 +198,7 @@ describe('LinkConnector SubgraphInput connection validation', () => {
// Create a minimal valid setup
const subgraph = createTestSubgraph()
const node = new LGraphNode('TestNode')
node.id = 1
node.id = asNodeId(1)
node.addInput('test_in', 'number')
subgraph.add(node)

View File

@@ -14,6 +14,8 @@ import type { NodeLike } from '@/lib/litegraph/src/types/NodeLike'
import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
import type { SubgraphIO } from '@/lib/litegraph/src/types/serialisation'
import { isSubgraphOutputNodeId } from '@/types/nodeId'
import type { RenderLink } from './RenderLink'
/** Connecting TO an output slot. */
@@ -55,7 +57,7 @@ export class ToOutputFromIoNodeLink implements RenderLink {
}
canConnectToReroute(reroute: Reroute): boolean {
if (reroute.origin_id === this.node.id) return false
if (isSubgraphOutputNodeId(reroute.origin_id)) return false
return true
}

View File

@@ -5,6 +5,7 @@ import type { TWidgetValue } from '@/lib/litegraph/src/types/widgets'
import type { ContextMenu } from './ContextMenu'
import type { LGraphNode, NodeId, NodeProperty } from './LGraphNode'
import type { NodeIdInput } from '@/types/nodeId'
import type { LLink, LinkId } from './LLink'
import type { Reroute, RerouteId } from './Reroute'
import type { SubgraphInput } from './subgraph/SubgraphInput'
@@ -162,7 +163,7 @@ export interface ReadonlyLinkNetwork {
readonly links: ReadonlyMap<LinkId, LLink>
readonly reroutes: ReadonlyMap<RerouteId, Reroute>
readonly floatingLinks: ReadonlyMap<LinkId, LLink>
getNodeById(id: NodeId | null | undefined): LGraphNode | null
getNodeById(id: NodeIdInput | null | undefined): LGraphNode | null
getLink(id: null | undefined): undefined
getLink(id: LinkId | null | undefined): LLink | undefined
getReroute(parentId: null | undefined): undefined

View File

@@ -1,5 +1,6 @@
import type { LGraphNode, NodeId } from './LGraphNode'
import type { LLink, LinkId } from './LLink'
import { asNodeId } from '@/types/nodeId'
/** Generates a unique string key for a link's connection tuple. */
function linkTupleKey(link: LLink): string {
@@ -51,7 +52,7 @@ export function purgeOrphanedLinks(
const link = links.get(id)
if (!link) continue
const originNode = getNodeById(link.origin_id)
const originNode = getNodeById(asNodeId(link.origin_id))
const output = originNode?.outputs?.[link.origin_slot]
if (output?.links) {
for (let i = output.links.length - 1; i >= 0; i--) {

View File

@@ -83,7 +83,7 @@ export { LinkConnector } from './canvas/LinkConnector'
export { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
export { CanvasPointer } from './CanvasPointer'
export * as Constants from './constants'
export { SUBGRAPH_INPUT_ID } from './constants'
export { SUBGRAPH_INPUT_ID, SUBGRAPH_OUTPUT_ID } from './constants'
export { ContextMenu } from './ContextMenu'
export { DragAndScale } from './DragAndScale'
@@ -118,6 +118,19 @@ export { BadgePosition, LGraphBadge } from './LGraphBadge'
export { LGraphCanvas } from './LGraphCanvas'
export { LGraphGroup, type GroupId } from './LGraphGroup'
export { LGraphNode, type NodeId } from './LGraphNode'
export {
asNodeId,
isFloatingNodeId,
isNumericNodeId,
isSubgraphInputNodeId,
isSubgraphOutputNodeId,
isUnassignedNodeId,
nodeIdToNumber,
SUBGRAPH_INPUT_NODE_ID,
SUBGRAPH_OUTPUT_NODE_ID,
UNASSIGNED_NODE_ID
} from '@/types/nodeId'
export type { NodeIdInput } from '@/types/nodeId'
export { LLink } from './LLink'
export { createBounds } from './measure'
export { Reroute, type RerouteId } from './Reroute'

View File

@@ -3,6 +3,7 @@ import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
asNodeId,
LGraph,
LGraphNode,
LGraphEventMode,
@@ -41,9 +42,9 @@ describe('ExecutableNodeDTO Creation', () => {
it('should create DTO with subgraph path', () => {
const graph = new LGraph()
const node = new LGraphNode('Inner Node')
node.id = 42
node.id = asNodeId(42)
graph.add(node)
const subgraphPath = ['10', '20'] as const
const subgraphPath = [asNodeId('10'), asNodeId('20')] as const
const dto = new ExecutableNodeDTO(node, subgraphPath, new Map(), undefined)
@@ -115,7 +116,7 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
it('should generate simple ID for root node', () => {
const graph = new LGraph()
const node = new LGraphNode('Root Node')
node.id = 5
node.id = asNodeId(5)
graph.add(node)
const dto = new ExecutableNodeDTO(node, [], new Map(), undefined)
@@ -126,9 +127,9 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
it('should generate path-based ID for nested node', () => {
const graph = new LGraph()
const node = new LGraphNode('Nested Node')
node.id = 3
node.id = asNodeId(3)
graph.add(node)
const path = ['1', '2'] as const
const path = [asNodeId('1'), asNodeId('2')] as const
const dto = new ExecutableNodeDTO(node, path, new Map(), undefined)
@@ -138,9 +139,15 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
it('should handle deep nesting paths', () => {
const graph = new LGraph()
const node = new LGraphNode('Deep Node')
node.id = 99
node.id = asNodeId(99)
graph.add(node)
const path = ['1', '2', '3', '4', '5'] as const
const path = [
asNodeId('1'),
asNodeId('2'),
asNodeId('3'),
asNodeId('4'),
asNodeId('5')
] as const
const dto = new ExecutableNodeDTO(node, path, new Map(), undefined)
@@ -150,15 +157,25 @@ describe('ExecutableNodeDTO Path-Based IDs', () => {
it('should handle string and number IDs consistently', () => {
const graph = new LGraph()
const node1 = new LGraphNode('Node 1')
node1.id = 10
node1.id = asNodeId(10)
graph.add(node1)
const node2 = new LGraphNode('Node 2')
node2.id = 20
node2.id = asNodeId(20)
graph.add(node2)
const dto1 = new ExecutableNodeDTO(node1, ['5'], new Map(), undefined)
const dto2 = new ExecutableNodeDTO(node2, ['5'], new Map(), undefined)
const dto1 = new ExecutableNodeDTO(
node1,
[asNodeId('5')],
new Map(),
undefined
)
const dto2 = new ExecutableNodeDTO(
node2,
[asNodeId('5')],
new Map(),
undefined
)
expect(dto1.id).toBe('5:10')
expect(dto2.id).toBe('5:20')
@@ -199,7 +216,12 @@ describe('ExecutableNodeDTO Input Resolution', () => {
// Get the inner node and create DTO
const innerNode = subgraph.nodes[0]
const dto = new ExecutableNodeDTO(innerNode, ['1'], new Map(), subgraphNode)
const dto = new ExecutableNodeDTO(
innerNode,
[asNodeId('1')],
new Map(),
subgraphNode
)
// Should return undefined for unconnected input
const resolved = dto.resolveInput(0)
@@ -234,7 +256,12 @@ describe('ExecutableNodeDTO Output Resolution', () => {
// Get the inner node and create DTO
const innerNode = subgraph.nodes[0]
const dto = new ExecutableNodeDTO(innerNode, ['1'], new Map(), subgraphNode)
const dto = new ExecutableNodeDTO(
innerNode,
[asNodeId('1')],
new Map(),
subgraphNode
)
const resolved = dto.resolveOutput(0, 'string', new Set())
@@ -487,12 +514,17 @@ describe('ExecutableNodeDTO Properties', () => {
it('should provide access to basic properties', () => {
const graph = new LGraph()
const node = new LGraphNode('Test Node')
node.id = 42
node.id = asNodeId(42)
node.addInput('input', 'number')
node.addOutput('output', 'string')
graph.add(node)
const dto = new ExecutableNodeDTO(node, ['1', '2'], new Map(), undefined)
const dto = new ExecutableNodeDTO(
node,
[asNodeId('1'), asNodeId('2')],
new Map(),
undefined
)
expect(dto.id).toBe('1:2:42')
expect(dto.type).toBe(node.type)
@@ -528,7 +560,12 @@ describe('ExecutableNodeDTO Memory Efficiency', () => {
node.addOutput('out2', 'string')
graph.add(node)
const dto = new ExecutableNodeDTO(node, ['1'], new Map(), undefined)
const dto = new ExecutableNodeDTO(
node,
[asNodeId('1')],
new Map(),
undefined
)
// DTO should be lightweight - only essential properties
expect(dto.node).toBe(node) // Reference, not copy
@@ -549,9 +586,14 @@ describe('ExecutableNodeDTO Memory Efficiency', () => {
// Create DTOs
for (let i = 0; i < 100; i++) {
const node = new LGraphNode(`Node ${i}`)
node.id = i
node.id = asNodeId(i)
graph.add(node)
const dto = new ExecutableNodeDTO(node, ['parent'], new Map(), undefined)
const dto = new ExecutableNodeDTO(
node,
[asNodeId('parent')],
new Map(),
undefined
)
nodes.push(dto)
}
@@ -570,7 +612,12 @@ describe('ExecutableNodeDTO Memory Efficiency', () => {
const subgraphNode = createTestSubgraphNode(subgraph)
const innerNode = subgraph.nodes[0]
const dto = new ExecutableNodeDTO(innerNode, ['1'], new Map(), subgraphNode)
const dto = new ExecutableNodeDTO(
innerNode,
[asNodeId('1')],
new Map(),
subgraphNode
)
// Should hold necessary references
expect(dto.node).toBe(innerNode)
@@ -619,20 +666,20 @@ describe('ExecutableNodeDTO Integration', () => {
it('should preserve original node properties through DTO', () => {
const graph = new LGraph()
const originalNode = new LGraphNode('Original')
originalNode.id = 123
originalNode.id = asNodeId(123)
originalNode.addInput('test', 'number')
originalNode.properties = { value: 42 }
graph.add(originalNode)
const dto = new ExecutableNodeDTO(
originalNode,
['parent'],
[asNodeId('parent')],
new Map(),
undefined
)
// DTO should provide access to original node properties
expect(dto.node.id).toBe(123)
expect(dto.node.id).toBe(asNodeId(123))
expect(dto.node.inputs).toHaveLength(1)
expect(dto.node.properties.value).toBe(42)
@@ -642,21 +689,21 @@ describe('ExecutableNodeDTO Integration', () => {
it('should handle execution context correctly', () => {
const subgraph = createTestSubgraph({ nodeCount: 1 })
const subgraphNode = createTestSubgraphNode(subgraph, { id: 99 })
const subgraphNode = createTestSubgraphNode(subgraph, { id: asNodeId(99) })
const innerNode = subgraph.nodes[0]
innerNode.id = 55
innerNode.id = asNodeId(55)
const dto = new ExecutableNodeDTO(
innerNode,
['99'],
[asNodeId('99')],
new Map(),
subgraphNode
)
// DTO provides execution context
expect(dto.id).toBe('99:55') // Path-based execution ID
expect(dto.node.id).toBe(55) // Original node ID preserved
expect(dto.subgraphNode?.id).toBe(99) // Subgraph context
expect(dto.node.id).toBe(asNodeId(55)) // Original node ID preserved
expect(dto.subgraphNode?.id).toBe(asNodeId(99)) // Subgraph context
})
})
@@ -669,11 +716,16 @@ describe('ExecutableNodeDTO Scale Testing', () => {
// Create DTOs to test performance
for (let i = 0; i < 1000; i++) {
const node = new LGraphNode(`Node ${i}`)
node.id = i
node.id = asNodeId(i)
node.addInput('in', 'number')
graph.add(node)
const dto = new ExecutableNodeDTO(node, ['parent'], new Map(), undefined)
const dto = new ExecutableNodeDTO(
node,
[asNodeId('parent')],
new Map(),
undefined
)
dtos.push(dto)
}
@@ -692,7 +744,7 @@ describe('ExecutableNodeDTO Scale Testing', () => {
it('should handle complex path generation correctly', () => {
const graph = new LGraph()
const node = new LGraphNode('Deep Node')
node.id = 999
node.id = asNodeId(999)
graph.add(node)
// Test deterministic path generation behavior
@@ -705,7 +757,7 @@ describe('ExecutableNodeDTO Scale Testing', () => {
for (const testCase of testCases) {
const path = Array.from({ length: testCase.depth }, (_, i) =>
(i + 1).toString()
asNodeId(i + 1)
)
const dto = new ExecutableNodeDTO(node, path, new Map(), undefined)
expect(dto.id).toBe(testCase.expectedId)

View File

@@ -11,7 +11,12 @@ import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { createUuidv4, Subgraph } from '@/lib/litegraph/src/litegraph'
import {
createUuidv4,
Subgraph,
SUBGRAPH_INPUT_NODE_ID,
SUBGRAPH_OUTPUT_NODE_ID
} from '@/lib/litegraph/src/litegraph'
import { subgraphTest } from './__fixtures__/subgraphFixtures'
import {
assertSubgraphStructure,
@@ -41,8 +46,8 @@ describe('Subgraph Construction', () => {
)
expect(subgraph.inputNode).toBeDefined()
expect(subgraph.outputNode).toBeDefined()
expect(subgraph.inputNode.id).toBe(-10)
expect(subgraph.outputNode.id).toBe(-20)
expect(subgraph.inputNode.id).toBe(SUBGRAPH_INPUT_NODE_ID)
expect(subgraph.outputNode.id).toBe(SUBGRAPH_OUTPUT_NODE_ID)
})
it('should require a root graph', () => {

View File

@@ -8,7 +8,12 @@ import { beforeEach, describe, expect, it } from 'vitest'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { LGraph, LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph'
import {
asNodeId,
LGraph,
LGraphNode,
Subgraph
} from '@/lib/litegraph/src/litegraph'
import {
createNestedSubgraphs,
@@ -28,8 +33,8 @@ describe('SubgraphEdgeCases - Recursion Detection', () => {
const sub2 = createTestSubgraph({ name: 'Sub2' })
// Create circular reference
const node1 = createTestSubgraphNode(sub1, { id: 1 })
const node2 = createTestSubgraphNode(sub2, { id: 2 })
const node1 = createTestSubgraphNode(sub1, { id: asNodeId(1) })
const node2 = createTestSubgraphNode(sub2, { id: asNodeId(2) })
// Current limitation: adding a circular reference overflows recursion depth.
sub1.add(node2)

View File

@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { ToInputFromIoNodeLink } from '@/lib/litegraph/src/canvas/ToInputFromIoNodeLink'
import { LinkDirection } from '@/lib/litegraph/src//types/globalEnums'
import { isSubgraphInputNodeId } from '@/types/nodeId'
import { subgraphTest } from './__fixtures__/subgraphFixtures'
import {
@@ -451,7 +452,7 @@ describe('SubgraphIO - Empty Slot Connection', () => {
expect(link).toBeDefined()
expect(link.target_id).toBe(internalNode.id)
expect(link.target_slot).toBe(0)
expect(link.origin_id).toBe(subgraph.inputNode.id)
expect(isSubgraphInputNodeId(link.origin_id)).toBe(true)
expect(link.origin_slot).toBe(1) // Should be the second slot
}
)

View File

@@ -1,4 +1,4 @@
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { NodeId } from '@/types/nodeId'
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
import type {

View File

@@ -1,9 +1,9 @@
import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LLink } from '@/lib/litegraph/src/LLink'
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/constants'
import { SUBGRAPH_INPUT_NODE_ID } from '@/types/nodeId'
import type {
DefaultConnectionColors,
INodeInputSlot,
@@ -26,7 +26,7 @@ export class SubgraphInputNode
extends SubgraphIONodeBase<SubgraphInput>
implements Positionable
{
readonly id: NodeId = SUBGRAPH_INPUT_ID
readonly id = SUBGRAPH_INPUT_NODE_ID
readonly emptySlot: EmptySubgraphInput = new EmptySubgraphInput(this)

View File

@@ -9,6 +9,7 @@ import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
asNodeId,
BaseWidget,
LGraph,
LGraphNode,
@@ -55,12 +56,12 @@ describe('SubgraphNode Construction', () => {
})
const subgraphNode = createTestSubgraphNode(subgraph, {
id: 42,
id: asNodeId(42),
pos: [300, 150],
size: [180, 80]
})
expect(subgraphNode.id).toBe(42)
expect(subgraphNode.id).toBe(asNodeId(42))
expect(Array.from(subgraphNode.pos)).toEqual([300, 150])
expect(Array.from(subgraphNode.size)).toEqual([180, 80])
})
@@ -705,13 +706,13 @@ describe('SubgraphNode Execution', () => {
})
const childSubgraphNode = createTestSubgraphNode(childSubgraph, {
id: 42,
id: asNodeId(42),
parentGraph: parentSubgraph
})
parentSubgraph.add(childSubgraphNode)
const parentSubgraphNode = createTestSubgraphNode(parentSubgraph, {
id: 10,
id: asNodeId(10),
parentGraph: rootGraph
})
rootGraph.add(parentSubgraphNode)
@@ -951,7 +952,7 @@ describe('SubgraphNode duplicate input pruning (#9977)', () => {
const parentGraph = new LGraph()
const instanceData = {
id: 1 as const,
id: asNodeId(1),
type: subgraph.id,
pos: [0, 0] as [number, number],
size: [200, 100] as [number, number],

View File

@@ -43,6 +43,7 @@ import { useWidgetValueStore } from '@/stores/widgetValueStore'
import { createNodeLocatorId } from '@/types/nodeIdentification'
import type { WidgetId } from '@/types/widgetId'
import { widgetId } from '@/types/widgetId'
import { asNodeId } from '@/types/nodeId'
import { ExecutableNodeDTO } from './ExecutableNodeDTO'
import type { ExecutableLGraphNode, ExecutionId } from './ExecutableNodeDTO'
@@ -733,7 +734,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
}
const newLink = LLink.create(innerLink)
newLink.origin_id = `${this.id}:${innerLink.origin_id}`
newLink.origin_id = asNodeId(`${this.id}:${innerLink.origin_id}`)
newLink.origin_slot = innerLink.origin_slot
return newLink

View File

@@ -1,9 +1,9 @@
import type { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { LLink } from '@/lib/litegraph/src/LLink'
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
import type { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
import { SUBGRAPH_OUTPUT_ID } from '@/lib/litegraph/src/constants'
import { SUBGRAPH_OUTPUT_NODE_ID } from '@/types/nodeId'
import type {
DefaultConnectionColors,
INodeInputSlot,
@@ -25,7 +25,7 @@ export class SubgraphOutputNode
extends SubgraphIONodeBase<SubgraphOutput>
implements Positionable
{
readonly id: NodeId = SUBGRAPH_OUTPUT_ID
readonly id = SUBGRAPH_OUTPUT_NODE_ID
readonly emptySlot: EmptySubgraphOutput = new EmptySubgraphOutput(this)

View File

@@ -10,9 +10,12 @@ import { setActivePinia } from 'pinia'
import { duplicateSubgraphNodeIds } from '@/lib/litegraph/src/__fixtures__/duplicateSubgraphNodeIds'
import {
asNodeId,
isNumericNodeId,
LGraph,
LGraphNode,
LiteGraph,
nodeIdToNumber,
Subgraph
} from '@/lib/litegraph/src/litegraph'
import type { ISlotType } from '@/lib/litegraph/src/litegraph'
@@ -189,7 +192,9 @@ describe('SubgraphSerialization - Complex Serialization', () => {
})
// Add child to parent
const childInstance = createTestSubgraphNode(childSubgraph, { id: 100 })
const childInstance = createTestSubgraphNode(childSubgraph, {
id: asNodeId(100)
})
parentSubgraph.add(childInstance)
// Serialize both
@@ -282,11 +287,11 @@ describe('SubgraphSerialization - Version Compatibility', () => {
inputs: [{ id: 'input-id', name: 'modern_input', type: 'number' }],
outputs: [{ id: 'output-id', name: 'modern_output', type: 'string' }],
inputNode: {
id: -10,
id: asNodeId(-10),
bounding: [0, 0, 120, 60]
},
outputNode: {
id: -20,
id: asNodeId(-20),
bounding: [300, 0, 120, 60]
},
widgets: []
@@ -312,11 +317,11 @@ describe('SubgraphSerialization - Version Compatibility', () => {
config: {},
definitions: { subgraphs: [] },
inputNode: {
id: -10,
id: asNodeId(-10),
bounding: [0, 0, 120, 60]
},
outputNode: {
id: -20,
id: asNodeId(-20),
bounding: [300, 0, 120, 60]
}
// Missing optional: inputs, outputs, widgets
@@ -345,11 +350,11 @@ describe('SubgraphSerialization - Version Compatibility', () => {
inputs: [],
outputs: [],
inputNode: {
id: -10,
id: asNodeId(-10),
bounding: [0, 0, 120, 60]
},
outputNode: {
id: -20,
id: asNodeId(-20),
bounding: [300, 0, 120, 60]
},
widgets: [],
@@ -493,9 +498,9 @@ describe('SubgraphSerialization - Data Integrity', () => {
const rootIds = graph.nodes
.map((node) => node.id)
.filter((id): id is number => typeof id === 'number')
.sort((a, b) => a - b)
expect(rootIds).toEqual([102, 103])
.filter((id) => isNumericNodeId(id))
.sort((a, b) => nodeIdToNumber(a) - nodeIdToNumber(b))
expect(rootIds).toEqual([asNodeId(102), asNodeId(103)])
const subgraphAIds = new Set(
graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_A)!.nodes.map((node) => node.id)
@@ -504,7 +509,9 @@ describe('SubgraphSerialization - Data Integrity', () => {
graph.subgraphs.get(DUPLICATE_ID_SUBGRAPH_B)!.nodes.map((node) => node.id)
)
expect(subgraphAIds).toEqual(new Set([3, 8, 37]))
expect(subgraphAIds).toEqual(
new Set([asNodeId(3), asNodeId(8), asNodeId(37)])
)
for (const id of subgraphAIds) {
expect(subgraphBIds.has(id)).toBe(false)
}

View File

@@ -3,13 +3,14 @@ import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import {
SUBGRAPH_INPUT_ID,
asNodeId,
LinkConnector,
ToInputFromIoNodeLink,
LGraphNode,
isSubgraphInput,
isSubgraphOutput
} from '@/lib/litegraph/src/litegraph'
import { isSubgraphInputNodeId } from '@/types/nodeId'
import type {
LinkNetwork,
NodeInputSlot,
@@ -132,7 +133,7 @@ describe('Subgraph slot connections', () => {
// Create a node inside the subgraph
const internalNode = new LGraphNode('InternalNode')
internalNode.id = 100
internalNode.id = asNodeId(100)
internalNode.addInput('in', 'number')
subgraph.add(internalNode)
@@ -142,7 +143,7 @@ describe('Subgraph slot connections', () => {
internalNode
)
expect(link).toBeDefined()
expect(link!.origin_id).toBe(SUBGRAPH_INPUT_ID)
expect(isSubgraphInputNodeId(link!.origin_id)).toBe(true)
expect(link!.target_id).toBe(internalNode.id)
// Verify the input slot has the link

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