mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 13:12:10 +00:00
## Summary
Architecture documentation proposing an Entity Component System for the
litegraph layer.
```mermaid
graph LR
subgraph Today["Today: Spaghetti"]
God["🍝 God Objects"]
Circ["🔄 Circular Deps"]
Mut["💥 Render Mutations"]
end
subgraph Tomorrow["Tomorrow: ECS"]
ID["🏷️ Branded IDs"]
Comp["📦 Components"]
Sys["⚙️ Systems"]
World["🌍 World"]
end
God -->|"decompose"| Comp
Circ -->|"flatten"| ID
Mut -->|"separate"| Sys
Comp --> World
ID --> World
Sys -->|"query"| World
```
## Changes
- **What**: ADR 0008 + 4 architecture docs (no code changes)
- `docs/adr/0008-entity-component-system.md` — entity taxonomy, branded
IDs, component decomposition, migration strategy
- `docs/architecture/entity-interactions.md` — as-is Mermaid diagrams of
all entity relationships
- `docs/architecture/entity-problems.md` — structural problems with
file:line evidence
- `docs/architecture/ecs-target-architecture.md` — target architecture
diagrams
- `docs/architecture/proto-ecs-stores.md` — analysis of existing Pinia
stores as proto-ECS patterns
## Review Focus
- Does the entity taxonomy (Node, Link, Subgraph, Widget, Slot, Reroute,
Group) cover all cases?
- Are the component decompositions reasonable starting points?
- Is the migration strategy (bridge layer, incremental extraction)
feasible?
- Are there entity interactions or problems we missed?
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10420-docs-ADR-0008-Entity-Component-System-32d6d73d365081feb048d16a5231d350)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
745 lines
27 KiB
Markdown
745 lines
27 KiB
Markdown
# ECS Lifecycle Scenarios
|
|
|
|
This document walks through the major entity lifecycle operations — showing the current imperative implementation and how each transforms under the ECS architecture from [ADR 0008](../adr/0008-entity-component-system.md).
|
|
|
|
Each scenario follows the same structure: **Current Flow** (what happens today), **ECS Flow** (what it looks like with the World), and a **Key Differences** table.
|
|
|
|
## 1. Node Removal
|
|
|
|
### Current Flow
|
|
|
|
`LGraph.remove(node)` — 107 lines, touches 6+ entity types and 4+ external systems:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant G as LGraph
|
|
participant N as LGraphNode
|
|
participant L as LLink
|
|
participant R as Reroute
|
|
participant C as LGraphCanvas
|
|
participant LS as LayoutStore
|
|
|
|
Caller->>G: remove(node)
|
|
G->>G: beforeChange() [undo checkpoint]
|
|
|
|
loop each input slot
|
|
G->>N: disconnectInput(i)
|
|
N->>L: link.disconnect(network)
|
|
L->>G: _links.delete(linkId)
|
|
L->>R: cleanup orphaned reroutes
|
|
N->>LS: layoutMutations.removeLink()
|
|
N->>G: _version++
|
|
end
|
|
|
|
loop each output slot
|
|
G->>N: disconnectOutput(i)
|
|
Note over N,R: same cascade as above
|
|
end
|
|
|
|
G->>G: scan floatingLinks for node refs
|
|
G->>G: if SubgraphNode: check refs, maybe delete subgraph def
|
|
G->>N: node.onRemoved?.()
|
|
G->>N: node.graph = null
|
|
G->>G: _version++
|
|
|
|
loop each canvas
|
|
G->>C: deselect(node)
|
|
G->>C: delete selected_nodes[id]
|
|
end
|
|
|
|
G->>G: splice from _nodes[], delete from _nodes_by_id
|
|
G->>G: onNodeRemoved?.(node)
|
|
G->>C: setDirtyCanvas(true, true)
|
|
G->>G: afterChange() [undo checkpoint]
|
|
G->>G: updateExecutionOrder()
|
|
```
|
|
|
|
Problems: the graph method manually disconnects every slot, cleans up reroutes, scans floating links, checks subgraph references, notifies canvases, and recomputes execution order — all in one method that knows about every entity type.
|
|
|
|
### ECS Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant CS as ConnectivitySystem
|
|
participant W as World
|
|
participant VS as VersionSystem
|
|
|
|
Caller->>CS: removeNode(world, nodeId)
|
|
|
|
CS->>W: getComponent(nodeId, Connectivity)
|
|
W-->>CS: { inputSlotIds, outputSlotIds }
|
|
|
|
loop each slotId
|
|
CS->>W: getComponent(slotId, SlotConnection)
|
|
W-->>CS: { linkIds }
|
|
loop each linkId
|
|
CS->>CS: removeLink(world, linkId)
|
|
Note over CS,W: removes Link entity + updates remote slots
|
|
end
|
|
CS->>W: deleteEntity(slotId)
|
|
end
|
|
|
|
CS->>W: getComponent(nodeId, WidgetContainer)
|
|
W-->>CS: { widgetIds }
|
|
loop each widgetId
|
|
CS->>W: deleteEntity(widgetId)
|
|
end
|
|
|
|
CS->>W: deleteEntity(nodeId)
|
|
Note over W: removes Position, NodeVisual, NodeType,<br/>Connectivity, Execution, Properties,<br/>WidgetContainer — all at once
|
|
|
|
CS->>VS: markChanged()
|
|
```
|
|
|
|
### Key Differences
|
|
|
|
| Aspect | Current | ECS |
|
|
| ------------------- | ------------------------------------------------ | ------------------------------------------------------ |
|
|
| Lines of code | ~107 in one method | ~30 in system function |
|
|
| Entity types known | Graph knows about all 6+ types | ConnectivitySystem knows Connectivity + SlotConnection |
|
|
| Cleanup | Manual per-slot, per-link, per-reroute | `deleteEntity()` removes all components atomically |
|
|
| Canvas notification | `setDirtyCanvas()` called explicitly | RenderSystem sees missing entity on next frame |
|
|
| Store cleanup | WidgetValueStore/LayoutStore NOT cleaned up | World deletion IS the cleanup |
|
|
| Undo/redo | `beforeChange()`/`afterChange()` manually placed | System snapshots affected components before deletion |
|
|
| Testability | Needs full LGraph + LGraphCanvas | Needs only World + ConnectivitySystem |
|
|
|
|
## 2. Serialization
|
|
|
|
### Current Flow
|
|
|
|
`LGraph.serialize()` → `asSerialisable()` — walks every collection manually:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant G as LGraph
|
|
participant N as LGraphNode
|
|
participant L as LLink
|
|
participant R as Reroute
|
|
participant Gr as LGraphGroup
|
|
participant SG as Subgraph
|
|
|
|
Caller->>G: serialize()
|
|
G->>G: asSerialisable()
|
|
|
|
loop each node
|
|
G->>N: node.serialize()
|
|
N->>N: snapshot inputs, outputs (with link IDs)
|
|
N->>N: snapshot properties
|
|
N->>N: collect widgets_values[]
|
|
N-->>G: ISerialisedNode
|
|
end
|
|
|
|
loop each link
|
|
G->>L: link.asSerialisable()
|
|
L-->>G: SerialisableLLink
|
|
end
|
|
|
|
loop each reroute
|
|
G->>R: reroute.asSerialisable()
|
|
R-->>G: SerialisableReroute
|
|
end
|
|
|
|
loop each group
|
|
G->>Gr: group.serialize()
|
|
Gr-->>G: ISerialisedGroup
|
|
end
|
|
|
|
G->>G: findUsedSubgraphIds()
|
|
loop each used subgraph
|
|
G->>SG: subgraph.asSerialisable()
|
|
Note over SG: recursively serializes internal nodes, links, etc.
|
|
SG-->>G: ExportedSubgraph
|
|
end
|
|
|
|
G-->>Caller: ISerialisedGraph
|
|
```
|
|
|
|
Problems: serialization logic lives in 6 different `serialize()` methods across 6 classes. Widget values are collected inline during node serialization. The graph walks its own collections — no separation of "what to serialize" from "how to serialize."
|
|
|
|
### ECS Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant SS as SerializationSystem
|
|
participant W as World
|
|
|
|
Caller->>SS: serialize(world)
|
|
|
|
SS->>W: queryAll(NodeType, Position, Properties, WidgetContainer, Connectivity)
|
|
W-->>SS: all node entities with their components
|
|
|
|
SS->>W: queryAll(LinkEndpoints)
|
|
W-->>SS: all link entities
|
|
|
|
SS->>W: queryAll(SlotIdentity, SlotConnection)
|
|
W-->>SS: all slot entities
|
|
|
|
SS->>W: queryAll(RerouteLinks, Position)
|
|
W-->>SS: all reroute entities
|
|
|
|
SS->>W: queryAll(GroupMeta, GroupChildren, Position)
|
|
W-->>SS: all group entities
|
|
|
|
SS->>W: queryAll(SubgraphStructure, SubgraphMeta)
|
|
W-->>SS: all subgraph entities
|
|
|
|
SS->>SS: assemble JSON from component data
|
|
SS-->>Caller: SerializedGraph
|
|
```
|
|
|
|
### Key Differences
|
|
|
|
| Aspect | Current | ECS |
|
|
| ---------------------- | ----------------------------------------------- | ---------------------------------------------- |
|
|
| Serialization logic | Spread across 6 classes (`serialize()` on each) | Single SerializationSystem |
|
|
| Widget values | Collected inline during `node.serialize()` | WidgetValue component queried directly |
|
|
| Subgraph recursion | `asSerialisable()` recursively calls itself | Flat query — SubgraphStructure has entity refs |
|
|
| Adding a new component | Modify the entity's `serialize()` method | Add component to query in SerializationSystem |
|
|
| Testing | Need full object graph to test serialization | Mock World with test components |
|
|
|
|
## 3. Deserialization
|
|
|
|
### Current Flow
|
|
|
|
`LGraph.configure(data)` — ~180 lines, two-phase node creation:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant G as LGraph
|
|
participant N as LGraphNode
|
|
participant L as LLink
|
|
participant WVS as WidgetValueStore
|
|
|
|
Caller->>G: configure(data)
|
|
G->>G: clear() [destroy all existing entities]
|
|
G->>G: _configureBase(data) [set id, extra]
|
|
|
|
loop each serialized link
|
|
G->>L: LLink.create(linkData)
|
|
G->>G: _links.set(link.id, link)
|
|
end
|
|
|
|
loop each serialized reroute
|
|
G->>G: setReroute(rerouteData)
|
|
end
|
|
|
|
opt has subgraph definitions
|
|
G->>G: deduplicateSubgraphNodeIds()
|
|
loop each subgraph (topological order)
|
|
G->>G: createSubgraph(data)
|
|
end
|
|
end
|
|
|
|
rect rgb(60, 40, 40)
|
|
Note over G,N: Phase 1: Create nodes (unlinked)
|
|
loop each serialized node
|
|
G->>N: LiteGraph.createNode(type)
|
|
G->>G: graph.add(node) [assigns ID]
|
|
end
|
|
end
|
|
|
|
rect rgb(40, 60, 40)
|
|
Note over G,N: Phase 2: Configure nodes (links now resolvable)
|
|
loop each node
|
|
G->>N: node.configure(nodeData)
|
|
N->>N: create slots, restore properties
|
|
N->>N: resolve links from graph._links
|
|
N->>N: restore widget values
|
|
N->>WVS: widget.setNodeId() → register in store
|
|
N->>N: fire onConnectionsChange per linked slot
|
|
end
|
|
end
|
|
|
|
G->>G: add floating links
|
|
G->>G: validate reroutes
|
|
G->>G: _removeDuplicateLinks()
|
|
|
|
loop each serialized group
|
|
G->>G: create + configure group
|
|
end
|
|
|
|
G->>G: updateExecutionOrder()
|
|
```
|
|
|
|
Problems: two-phase creation is necessary because nodes need to reference each other's links during configure. Widget value restoration happens deep inside `node.configure()`. Store population is a side effect of configuration. Subgraph creation requires topological sorting to handle nested subgraphs.
|
|
|
|
### ECS Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant SS as SerializationSystem
|
|
participant W as World
|
|
participant LS as LayoutSystem
|
|
participant ES as ExecutionSystem
|
|
|
|
Caller->>SS: deserialize(world, data)
|
|
|
|
SS->>W: clear() [remove all entities]
|
|
|
|
Note over SS,W: All entities created in one pass — no two-phase needed
|
|
|
|
loop each node in data
|
|
SS->>W: createEntity(NodeEntityId)
|
|
SS->>W: setComponent(id, Position, {...})
|
|
SS->>W: setComponent(id, NodeType, {...})
|
|
SS->>W: setComponent(id, NodeVisual, {...})
|
|
SS->>W: setComponent(id, Properties, {...})
|
|
SS->>W: setComponent(id, Execution, {...})
|
|
end
|
|
|
|
loop each slot in data
|
|
SS->>W: createEntity(SlotEntityId)
|
|
SS->>W: setComponent(id, SlotIdentity, {...})
|
|
SS->>W: setComponent(id, SlotConnection, {...})
|
|
end
|
|
|
|
Note over SS,W: Slots reference links by ID — no resolution needed yet
|
|
|
|
loop each link in data
|
|
SS->>W: createEntity(LinkEntityId)
|
|
SS->>W: setComponent(id, LinkEndpoints, {...})
|
|
end
|
|
|
|
Note over SS,W: Connectivity assembled from slot/link components
|
|
|
|
loop each widget in data
|
|
SS->>W: createEntity(WidgetEntityId)
|
|
SS->>W: setComponent(id, WidgetIdentity, {...})
|
|
SS->>W: setComponent(id, WidgetValue, {...})
|
|
end
|
|
|
|
SS->>SS: create reroutes, groups, subgraphs similarly
|
|
|
|
Note over SS,W: Systems react to populated World
|
|
|
|
SS->>LS: runLayout(world)
|
|
SS->>ES: computeExecutionOrder(world)
|
|
```
|
|
|
|
### Key Differences
|
|
|
|
| Aspect | Current | ECS |
|
|
| ------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
| Two-phase creation | Required (nodes must exist before link resolution) | Not needed — components reference IDs, not instances |
|
|
| Widget restoration | Hidden inside `node.configure()` line ~900 | Explicit: WidgetValue component written directly |
|
|
| Store population | Side effect of `widget.setNodeId()` | World IS the store — writing component IS population |
|
|
| Callback cascade | `onConnectionsChange`, `onInputAdded`, `onConfigure` fire during configure | No callbacks — systems query World after deserialization |
|
|
| Subgraph ordering | Topological sort required | Flat write — SubgraphStructure just holds entity IDs |
|
|
| Error handling | Failed node → placeholder with `has_errors=true` | Failed entity → skip; components that loaded are still valid |
|
|
|
|
## 4. Pack Subgraph
|
|
|
|
### Current Flow
|
|
|
|
`LGraph.convertToSubgraph(items)` — clones nodes, computes boundaries, creates SubgraphNode:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant G as LGraph
|
|
participant N as LGraphNode
|
|
participant SG as Subgraph
|
|
participant SGN as SubgraphNode
|
|
|
|
Caller->>G: convertToSubgraph(selectedItems)
|
|
G->>G: beforeChange()
|
|
|
|
G->>G: getBoundaryLinks(items)
|
|
Note over G: classify links as internal, boundary-in, boundary-out
|
|
|
|
G->>G: splitPositionables(items) → nodes, reroutes, groups
|
|
G->>N: multiClone(nodes) → cloned nodes (no links)
|
|
G->>G: serialize internal links, reroutes
|
|
|
|
G->>G: mapSubgraphInputsAndLinks(boundaryInputLinks)
|
|
G->>G: mapSubgraphOutputsAndLinks(boundaryOutputLinks)
|
|
|
|
G->>G: createSubgraph(exportedData)
|
|
G->>SG: subgraph.configure(data)
|
|
|
|
loop disconnect boundary links
|
|
G->>N: inputNode.disconnectInput()
|
|
G->>N: outputNode.disconnectOutput()
|
|
end
|
|
|
|
loop remove original items
|
|
G->>G: remove(node), remove(reroute), remove(group)
|
|
end
|
|
|
|
G->>SGN: LiteGraph.createNode(subgraph.id)
|
|
G->>G: graph.add(subgraphNode)
|
|
|
|
loop reconnect boundary inputs
|
|
G->>N: externalNode.connectSlots(output, subgraphNode, input)
|
|
end
|
|
|
|
loop reconnect boundary outputs
|
|
G->>SGN: subgraphNode.connectSlots(output, externalNode, input)
|
|
end
|
|
|
|
G->>G: afterChange()
|
|
```
|
|
|
|
Problems: 200+ lines in one method. Manual boundary link analysis. Clone-serialize-configure dance. Disconnect-remove-recreate-reconnect sequence with many intermediate states where the graph is inconsistent.
|
|
|
|
### ECS Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant CS as ConnectivitySystem
|
|
participant W as World
|
|
|
|
Caller->>CS: packSubgraph(world, selectedEntityIds)
|
|
|
|
CS->>W: query Connectivity + SlotConnection for selected nodes
|
|
CS->>CS: classify links as internal vs boundary
|
|
|
|
CS->>W: create new GraphId scope in scopes registry
|
|
|
|
Note over CS,W: Create SubgraphNode entity in parent scope
|
|
|
|
CS->>W: createEntity(NodeEntityId) [the SubgraphNode]
|
|
CS->>W: setComponent(nodeId, Position, { center of selection })
|
|
CS->>W: setComponent(nodeId, SubgraphStructure, { graphId, interface })
|
|
CS->>W: setComponent(nodeId, SubgraphMeta, { name: 'New Subgraph' })
|
|
|
|
Note over CS,W: Re-parent selected entities into new graph scope
|
|
|
|
loop each selected entity
|
|
CS->>W: update graphScope to new graphId
|
|
end
|
|
|
|
Note over CS,W: Create boundary slots on SubgraphNode
|
|
|
|
loop each boundary input link
|
|
CS->>W: create SlotEntity on SubgraphNode
|
|
CS->>W: update LinkEndpoints to target new slot
|
|
end
|
|
|
|
loop each boundary output link
|
|
CS->>W: create SlotEntity on SubgraphNode
|
|
CS->>W: update LinkEndpoints to source from new slot
|
|
end
|
|
```
|
|
|
|
### Key Differences
|
|
|
|
| Aspect | Current | ECS |
|
|
| -------------------------- | ------------------------------------------------- | ------------------------------------------------------- |
|
|
| Entity movement | Clone → serialize → configure → remove originals | Re-parent entities: update graphScope to new GraphId |
|
|
| Boundary links | Disconnect → remove → recreate → reconnect | Update LinkEndpoints to point at new SubgraphNode slots |
|
|
| Intermediate inconsistency | Graph is partially disconnected during operation | Atomic: all component writes happen together |
|
|
| Code size | 200+ lines | ~50 lines in system |
|
|
| Undo | `beforeChange()`/`afterChange()` wraps everything | Snapshot affected components before mutation |
|
|
|
|
## 5. Unpack Subgraph
|
|
|
|
### Current Flow
|
|
|
|
`LGraph.unpackSubgraph(subgraphNode)` — clones internal nodes, remaps IDs, reconnects boundary:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant G as LGraph
|
|
participant SGN as SubgraphNode
|
|
participant SG as Subgraph
|
|
participant N as LGraphNode
|
|
|
|
Caller->>G: unpackSubgraph(subgraphNode)
|
|
G->>G: beforeChange()
|
|
|
|
G->>SG: get internal nodes
|
|
G->>N: multiClone(internalNodes)
|
|
|
|
loop each cloned node
|
|
G->>G: assign new ID (++lastNodeId)
|
|
G->>G: nodeIdMap[oldId] = newId
|
|
G->>G: graph.add(node)
|
|
G->>N: node.configure(info)
|
|
G->>N: node.setPos(pos + offset)
|
|
end
|
|
|
|
G->>G: clone and add groups
|
|
|
|
Note over G,SG: Remap internal links
|
|
|
|
loop each internal link
|
|
G->>G: remap origin_id/target_id via nodeIdMap
|
|
opt origin is SUBGRAPH_INPUT_ID
|
|
G->>G: resolve to external source via subgraphNode.inputs
|
|
end
|
|
opt target is SUBGRAPH_OUTPUT_ID
|
|
G->>G: resolve to external target via subgraphNode.outputs
|
|
end
|
|
end
|
|
|
|
G->>G: remove(subgraphNode)
|
|
G->>G: deduplicate links
|
|
G->>G: create new LLink objects in parent graph
|
|
G->>G: remap reroute parentIds
|
|
G->>G: afterChange()
|
|
```
|
|
|
|
Problems: ID remapping is complex and error-prone. Magic IDs (SUBGRAPH_INPUT_ID = -10, SUBGRAPH_OUTPUT_ID = -20) require special-case handling. Boundary link resolution requires looking up the SubgraphNode's slots to find external connections.
|
|
|
|
### ECS Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant CS as ConnectivitySystem
|
|
participant W as World
|
|
|
|
Caller->>CS: unpackSubgraph(world, subgraphNodeId)
|
|
|
|
CS->>W: getComponent(subgraphNodeId, SubgraphStructure)
|
|
W-->>CS: { graphId, interface }
|
|
|
|
CS->>W: query entities where graphScope = graphId
|
|
W-->>CS: all child entities (nodes, links, reroutes, etc.)
|
|
|
|
Note over CS,W: Re-parent entities to containing graph scope
|
|
|
|
loop each child entity
|
|
CS->>W: update graphScope to parent scope
|
|
end
|
|
|
|
Note over CS,W: Reconnect boundary links
|
|
|
|
loop each boundary slot in interface
|
|
CS->>W: getComponent(slotId, SlotConnection)
|
|
CS->>W: update LinkEndpoints: SubgraphNode slot → internal node slot
|
|
end
|
|
|
|
CS->>W: deleteEntity(subgraphNodeId)
|
|
CS->>W: remove graphId from scopes registry
|
|
|
|
Note over CS,W: Offset positions
|
|
|
|
loop each moved entity
|
|
CS->>W: update Position component
|
|
end
|
|
```
|
|
|
|
### Key Differences
|
|
|
|
| Aspect | Current | ECS |
|
|
| ----------------- | --------------------------------------------------- | --------------------------------------------------------------- |
|
|
| ID remapping | `nodeIdMap[oldId] = newId` for every node and link | No remapping — entities keep their IDs, only graphScope changes |
|
|
| Magic IDs | SUBGRAPH_INPUT_ID = -10, SUBGRAPH_OUTPUT_ID = -20 | No magic IDs — boundary modeled as slot entities |
|
|
| Clone vs move | Clone nodes, assign new IDs, configure from scratch | Move entity references between scopes |
|
|
| Link reconnection | Remap origin_id/target_id, create new LLink objects | Update LinkEndpoints component in place |
|
|
| Complexity | ~200 lines with deduplication and reroute remapping | ~40 lines, no remapping needed |
|
|
|
|
## 6. Connect Slots
|
|
|
|
### Current Flow
|
|
|
|
`LGraphNode.connectSlots()` — creates link, updates both endpoints, handles reroutes:
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant N1 as SourceNode
|
|
participant N2 as TargetNode
|
|
participant G as LGraph
|
|
participant L as LLink
|
|
participant R as Reroute
|
|
participant LS as LayoutStore
|
|
|
|
Caller->>N1: connectSlots(output, targetNode, input)
|
|
|
|
N1->>N1: validate slot types
|
|
N1->>N2: onConnectInput?() → can reject
|
|
N1->>N1: onConnectOutput?() → can reject
|
|
|
|
opt input already connected
|
|
N1->>N2: disconnectInput(inputIndex)
|
|
end
|
|
|
|
N1->>L: new LLink(++lastLinkId, type, ...)
|
|
N1->>G: _links.set(link.id, link)
|
|
N1->>LS: layoutMutations.createLink()
|
|
|
|
N1->>N1: output.links.push(link.id)
|
|
N1->>N2: input.link = link.id
|
|
|
|
loop each reroute in path
|
|
N1->>R: reroute.linkIds.add(link.id)
|
|
end
|
|
|
|
N1->>G: _version++
|
|
N1->>N1: onConnectionsChange?(OUTPUT, ...)
|
|
N1->>N2: onConnectionsChange?(INPUT, ...)
|
|
N1->>G: setDirtyCanvas()
|
|
N1->>G: afterChange()
|
|
```
|
|
|
|
Problems: the source node orchestrates everything — it reaches into the graph's link map, the target node's slot, the layout store, the reroute chain, and the version counter. 19 steps in one method.
|
|
|
|
### ECS Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant Caller
|
|
participant CS as ConnectivitySystem
|
|
participant W as World
|
|
participant VS as VersionSystem
|
|
|
|
Caller->>CS: connect(world, outputSlotId, inputSlotId)
|
|
|
|
CS->>W: getComponent(inputSlotId, SlotConnection)
|
|
opt already connected
|
|
CS->>CS: removeLink(world, existingLinkId)
|
|
end
|
|
|
|
CS->>W: createEntity(LinkEntityId)
|
|
CS->>W: setComponent(linkId, LinkEndpoints, {<br/> originNodeId, originSlotIndex,<br/> targetNodeId, targetSlotIndex, type<br/>})
|
|
|
|
CS->>W: update SlotConnection on outputSlotId (add linkId)
|
|
CS->>W: update SlotConnection on inputSlotId (set linkId)
|
|
|
|
CS->>VS: markChanged()
|
|
```
|
|
|
|
### Key Differences
|
|
|
|
| Aspect | Current | ECS |
|
|
| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------- |
|
|
| Orchestrator | Source node (reaches into graph, target, reroutes) | ConnectivitySystem (queries World) |
|
|
| Side effects | `_version++`, `setDirtyCanvas()`, `afterChange()`, callbacks | `markChanged()` — one call |
|
|
| Reroute handling | Manual: iterate chain, add linkId to each | RerouteLinks component updated by system |
|
|
| Slot mutation | Direct: `output.links.push()`, `input.link = id` | Component update: `setComponent(slotId, SlotConnection, ...)` |
|
|
| Validation | `onConnectInput`/`onConnectOutput` callbacks on nodes | Validation system or guard function |
|
|
|
|
## 7. Copy / Paste
|
|
|
|
### Current Flow
|
|
|
|
Copy: serialize selected items → clipboard. Paste: deserialize with new IDs.
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant C as LGraphCanvas
|
|
participant G as LGraph
|
|
participant N as LGraphNode
|
|
participant CB as Clipboard
|
|
|
|
rect rgb(40, 40, 60)
|
|
Note over User,CB: Copy
|
|
User->>C: Ctrl+C
|
|
C->>C: _serializeItems(selectedItems)
|
|
loop each selected node
|
|
C->>N: node.clone().serialize()
|
|
C->>C: collect input links
|
|
end
|
|
C->>C: collect groups, reroutes
|
|
C->>C: recursively collect subgraph definitions
|
|
C->>CB: localStorage.setItem(JSON.stringify(data))
|
|
end
|
|
|
|
rect rgb(40, 60, 40)
|
|
Note over User,CB: Paste
|
|
User->>C: Ctrl+V
|
|
C->>CB: localStorage.getItem()
|
|
C->>C: _deserializeItems(parsed)
|
|
|
|
C->>C: remap subgraph IDs (new UUIDs)
|
|
C->>C: deduplicateSubgraphNodeIds()
|
|
C->>G: beforeChange()
|
|
|
|
loop each subgraph
|
|
C->>G: createSubgraph(data)
|
|
end
|
|
loop each node (id=-1 forces new ID)
|
|
C->>G: graph.add(node)
|
|
C->>N: node.configure(info)
|
|
end
|
|
loop each reroute
|
|
C->>G: setReroute(data)
|
|
C->>C: remap parentIds
|
|
end
|
|
loop each link
|
|
C->>N: outNode.connect(slot, inNode, slot)
|
|
end
|
|
|
|
C->>C: offset positions to cursor
|
|
C->>C: selectItems(created)
|
|
C->>G: afterChange()
|
|
end
|
|
```
|
|
|
|
Problems: clone-serialize-parse-remap-deserialize dance. Every entity type has
|
|
its own ID remapping logic. Subgraph IDs, node IDs, reroute IDs, and link
|
|
parent IDs all remapped independently. ~300 lines across multiple methods.
|
|
|
|
### ECS Flow
|
|
|
|
```mermaid
|
|
sequenceDiagram
|
|
participant User
|
|
participant CS as ClipboardSystem
|
|
participant W as World
|
|
participant CB as Clipboard
|
|
|
|
rect rgb(40, 40, 60)
|
|
Note over User,CB: Copy
|
|
User->>CS: copy(world, selectedEntityIds)
|
|
CS->>W: snapshot all components for selected entities
|
|
CS->>W: snapshot components for child entities (slots, widgets)
|
|
CS->>W: snapshot connected links (LinkEndpoints)
|
|
CS->>CB: store component snapshot
|
|
end
|
|
|
|
rect rgb(40, 60, 40)
|
|
Note over User,CB: Paste
|
|
User->>CS: paste(world, position)
|
|
CS->>CB: retrieve snapshot
|
|
|
|
CS->>CS: generate ID remap table (old → new branded IDs)
|
|
|
|
loop each entity in snapshot
|
|
CS->>W: createEntity(newId)
|
|
loop each component
|
|
CS->>W: setComponent(newId, type, remappedData)
|
|
Note over CS,W: entity ID refs in component data<br/>are remapped via table
|
|
end
|
|
end
|
|
|
|
CS->>CS: offset all Position components to cursor
|
|
end
|
|
```
|
|
|
|
### Key Differences
|
|
|
|
| Aspect | Current | ECS |
|
|
| -------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
|
|
| Copy format | Clone → serialize → JSON (format depends on class) | Component snapshot (uniform format for all entities) |
|
|
| ID remapping | Separate logic per entity type (nodes, reroutes, subgraphs, links) | Single remap table applied to all entity ID refs in all components |
|
|
| Paste reconstruction | `createNode()` → `add()` → `configure()` → `connect()` per node | `createEntity()` → `setComponent()` per entity (flat) |
|
|
| Subgraph handling | Recursive clone + UUID remap + deduplication | Snapshot includes SubgraphStructure component with entity refs |
|
|
| Code complexity | ~300 lines across 4 methods | ~60 lines in one system |
|
|
|
|
## Summary: Cross-Cutting Benefits
|
|
|
|
| Benefit | Scenarios Where It Applies |
|
|
| ----------------------------- | -------------------------------------------------------------------------- |
|
|
| **Atomic operations** | Node Removal, Pack/Unpack — no intermediate inconsistent state |
|
|
| **No scattered `_version++`** | All scenarios — VersionSystem handles change tracking |
|
|
| **No callback cascades** | Deserialization, Connect — systems query World instead of firing callbacks |
|
|
| **Uniform ID handling** | Copy/Paste, Unpack — one remap table instead of per-type logic |
|
|
| **Entity deletion = cleanup** | Node Removal — `deleteEntity()` removes all components |
|
|
| **No two-phase creation** | Deserialization — components reference IDs, not instances |
|
|
| **Move instead of clone** | Pack/Unpack — entities keep their IDs, just change scope |
|
|
| **Testable in isolation** | All scenarios — mock World, test one system |
|
|
| **Undo/redo for free** | All scenarios — snapshot components before mutation, restore on undo |
|