mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
docs: draft ECS component interfaces and World type
Example interfaces for all 7 entity kinds (Node, Link, Slot, Widget, Reroute, Group, Subgraph), branded entity ID types with cast helpers, and a Map-backed World implementation. Reuses existing litegraph types (Point, Size, INodeFlags, ISlotType, etc.) for migration compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
32
src/ecs/components/group.ts
Normal file
32
src/ecs/components/group.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Group components.
|
||||
*
|
||||
* Groups are visual containers that hold nodes and reroutes.
|
||||
* Currently no state has been extracted from LGraphGroup — these
|
||||
* components represent the full extraction target.
|
||||
*/
|
||||
|
||||
import type { NodeEntityId, RerouteEntityId } from '../entityId'
|
||||
|
||||
/** Metadata for a group. */
|
||||
export interface GroupMeta {
|
||||
title: string
|
||||
font?: string
|
||||
fontSize: number
|
||||
}
|
||||
|
||||
/** Visual properties for group rendering. */
|
||||
export interface GroupVisual {
|
||||
color?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Entities contained within a group.
|
||||
*
|
||||
* Replaces LGraphGroup._children (Set<Positionable>) and
|
||||
* LGraphGroup._nodes (LGraphNode[]).
|
||||
*/
|
||||
export interface GroupChildren {
|
||||
nodeIds: ReadonlySet<NodeEntityId>
|
||||
rerouteIds: ReadonlySet<RerouteEntityId>
|
||||
}
|
||||
51
src/ecs/components/link.ts
Normal file
51
src/ecs/components/link.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Link components.
|
||||
*
|
||||
* Decomposes LLink into endpoint topology, visual state, and
|
||||
* transient interaction state.
|
||||
*/
|
||||
|
||||
import type {
|
||||
CanvasColour,
|
||||
ISlotType,
|
||||
Point
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
import type { NodeEntityId, RerouteEntityId } from '../entityId'
|
||||
|
||||
/**
|
||||
* The topological endpoints of a link.
|
||||
*
|
||||
* Replaces origin_id/origin_slot/target_id/target_slot/type on LLink.
|
||||
* Slot indices will migrate to SlotEntityId references once slots
|
||||
* have independent IDs.
|
||||
*/
|
||||
export interface LinkEndpoints {
|
||||
originNodeId: NodeEntityId
|
||||
originSlotIndex: number
|
||||
targetNodeId: NodeEntityId
|
||||
targetSlotIndex: number
|
||||
/** Data type flowing through this link (e.g., 'IMAGE', 'MODEL'). */
|
||||
type: ISlotType
|
||||
/** Reroute that owns this link segment, if any. */
|
||||
parentRerouteId?: RerouteEntityId
|
||||
}
|
||||
|
||||
/** Visual properties for link rendering. */
|
||||
export interface LinkVisual {
|
||||
color?: CanvasColour
|
||||
/** Cached rendered path (invalidated on position change). */
|
||||
path?: Path2D
|
||||
/** Cached center point of the link curve. */
|
||||
centerPos?: Point
|
||||
/** Cached angle at the center point. */
|
||||
centerAngle?: number
|
||||
}
|
||||
|
||||
/** Transient interaction state for a link. */
|
||||
export interface LinkState {
|
||||
/** True while the user is dragging this link. */
|
||||
dragging: boolean
|
||||
/** Arbitrary data payload flowing through the link. */
|
||||
data?: unknown
|
||||
}
|
||||
87
src/ecs/components/node.ts
Normal file
87
src/ecs/components/node.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Node-specific components.
|
||||
*
|
||||
* These decompose the ~4,300-line LGraphNode class into focused data
|
||||
* objects. Each component captures one concern; systems provide behavior.
|
||||
*
|
||||
* Reuses existing types from litegraph where possible to ease migration.
|
||||
*/
|
||||
|
||||
import type { Dictionary, INodeFlags } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type {
|
||||
LGraphEventMode,
|
||||
RenderShape
|
||||
} from '@/lib/litegraph/src/types/globalEnums'
|
||||
|
||||
import type { SlotEntityId, WidgetEntityId } from '../entityId'
|
||||
|
||||
/** Static identity and classification of a node. */
|
||||
export interface NodeType {
|
||||
/** Registered node type string (e.g., 'KSampler', 'CLIPTextEncode'). */
|
||||
type: string
|
||||
/** Display title. */
|
||||
title: string
|
||||
/** Category path for the node menu (e.g., 'sampling'). */
|
||||
category?: string
|
||||
/** Backend node definition data, if resolved. */
|
||||
nodeData?: unknown
|
||||
/** Optional description shown in tooltips/docs. */
|
||||
description?: string
|
||||
}
|
||||
|
||||
/** Visual / rendering properties of a node. */
|
||||
export interface NodeVisual {
|
||||
color?: string
|
||||
bgcolor?: string
|
||||
boxcolor?: string
|
||||
shape?: RenderShape
|
||||
}
|
||||
|
||||
/**
|
||||
* Connectivity — references to this node's slot entities.
|
||||
*
|
||||
* Replaces the `inputs[]` and `outputs[]` arrays on LGraphNode.
|
||||
* Actual slot data lives on SlotIdentity / SlotConnection components
|
||||
* keyed by SlotEntityId.
|
||||
*/
|
||||
export interface Connectivity {
|
||||
inputSlotIds: readonly SlotEntityId[]
|
||||
outputSlotIds: readonly SlotEntityId[]
|
||||
}
|
||||
|
||||
/** Execution scheduling state. */
|
||||
export interface Execution {
|
||||
/** Computed execution order (topological sort index). */
|
||||
order: number
|
||||
/** How this node participates in execution. */
|
||||
mode: LGraphEventMode
|
||||
/** Behavioral flags (pinned, collapsed, ghost, etc.). */
|
||||
flags: INodeFlags
|
||||
}
|
||||
|
||||
/** User-defined key-value properties on a node. */
|
||||
export interface Properties {
|
||||
properties: Dictionary<NodeProperty | undefined>
|
||||
propertiesInfo: readonly PropertyInfo[]
|
||||
}
|
||||
|
||||
export interface PropertyInfo {
|
||||
name?: string
|
||||
type?: string
|
||||
default_value?: NodeProperty
|
||||
widget?: string
|
||||
label?: string
|
||||
values?: unknown[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for widget entities owned by this node.
|
||||
*
|
||||
* Replaces the `widgets[]` array on LGraphNode.
|
||||
* Actual widget data lives on WidgetIdentity / WidgetValue components
|
||||
* keyed by WidgetEntityId.
|
||||
*/
|
||||
export interface WidgetContainer {
|
||||
widgetIds: readonly WidgetEntityId[]
|
||||
}
|
||||
24
src/ecs/components/position.ts
Normal file
24
src/ecs/components/position.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Position component — shared by Node, Reroute, and Group entities.
|
||||
*
|
||||
* Plain data object. No methods, no back-references.
|
||||
* Corresponds to the spatial data currently on LGraphNode.pos/size,
|
||||
* Reroute.pos, and LGraphGroup._bounding.
|
||||
*
|
||||
* During the bridge phase, this mirrors data from the LayoutStore
|
||||
* (Y.js CRDTs). See migration plan Phase 2a.
|
||||
*/
|
||||
|
||||
import type { Point, Size } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
export interface Position {
|
||||
/** Position in graph coordinates (top-left for nodes/groups, center for reroutes). */
|
||||
pos: Point
|
||||
/** Width and height. Undefined for point-like entities (reroutes). */
|
||||
size?: Size
|
||||
/**
|
||||
* Bounding rectangle as [x, y, width, height].
|
||||
* May extend beyond pos/size (e.g., nodes with title overhang).
|
||||
*/
|
||||
bounding: readonly [x: number, y: number, w: number, h: number]
|
||||
}
|
||||
35
src/ecs/components/reroute.ts
Normal file
35
src/ecs/components/reroute.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Reroute components.
|
||||
*
|
||||
* Reroutes are waypoints on link paths. Position is shared via the
|
||||
* Position component. These components capture the link topology
|
||||
* and visual state specific to reroutes.
|
||||
*/
|
||||
|
||||
import type { CanvasColour } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
import type { LinkEntityId, RerouteEntityId } from '../entityId'
|
||||
|
||||
/**
|
||||
* Link topology for a reroute.
|
||||
*
|
||||
* A reroute can be chained (parentId) and carries a set of links
|
||||
* that pass through it.
|
||||
*/
|
||||
export interface RerouteLinks {
|
||||
/** Parent reroute in the chain, if any. */
|
||||
parentId?: RerouteEntityId
|
||||
/** Links that pass through this reroute. */
|
||||
linkIds: ReadonlySet<LinkEntityId>
|
||||
/** Floating (in-progress) links passing through this reroute. */
|
||||
floatingLinkIds: ReadonlySet<LinkEntityId>
|
||||
}
|
||||
|
||||
/** Visual state specific to reroute rendering. */
|
||||
export interface RerouteVisual {
|
||||
color?: CanvasColour
|
||||
/** Cached path for the link segment. */
|
||||
path?: Path2D
|
||||
/** Angle at the reroute center (for directional rendering). */
|
||||
centerAngle?: number
|
||||
}
|
||||
76
src/ecs/components/slot.ts
Normal file
76
src/ecs/components/slot.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Slot components.
|
||||
*
|
||||
* Slots currently lack independent IDs — they're identified by their
|
||||
* index on a parent node's inputs/outputs array. The ECS assigns each
|
||||
* slot a synthetic SlotEntityId, making them first-class entities.
|
||||
*
|
||||
* Decomposes SlotBase / INodeInputSlot / INodeOutputSlot into identity,
|
||||
* connection topology, and visual state.
|
||||
*/
|
||||
|
||||
import type {
|
||||
CanvasColour,
|
||||
ISlotType,
|
||||
Point
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
LinkDirection,
|
||||
RenderShape
|
||||
} from '@/lib/litegraph/src/types/globalEnums'
|
||||
|
||||
import type { LinkEntityId, NodeEntityId } from '../entityId'
|
||||
|
||||
/** Immutable identity of a slot. */
|
||||
export interface SlotIdentity {
|
||||
/** Display name (e.g., 'model', 'positive'). */
|
||||
name: string
|
||||
/** Localized display name, if available. */
|
||||
localizedName?: string
|
||||
/** Optional label override. */
|
||||
label?: string
|
||||
/** Data type accepted/produced by this slot. */
|
||||
type: ISlotType
|
||||
/** Whether this is an input or output slot. */
|
||||
direction: 'input' | 'output'
|
||||
/** The node that owns this slot. */
|
||||
parentNodeId: NodeEntityId
|
||||
/** Position index on the parent node (0-based). */
|
||||
index: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection state of a slot.
|
||||
*
|
||||
* Input slots have at most one link. Output slots can have many.
|
||||
*/
|
||||
export interface SlotConnection {
|
||||
/**
|
||||
* For input slots: the single connected link, or null.
|
||||
* For output slots: all connected links.
|
||||
*/
|
||||
linkIds: readonly LinkEntityId[]
|
||||
/** Widget locator, if this slot backs a promoted widget. */
|
||||
widgetLocator?: SlotWidgetLocator
|
||||
}
|
||||
|
||||
export interface SlotWidgetLocator {
|
||||
name: string
|
||||
nodeId: NodeEntityId
|
||||
}
|
||||
|
||||
/** Visual / rendering properties of a slot. */
|
||||
export interface SlotVisual {
|
||||
/** Computed position relative to the node. */
|
||||
pos?: Point
|
||||
/** Bounding rectangle for hit testing. */
|
||||
boundingRect: readonly [x: number, y: number, w: number, h: number]
|
||||
/** Color when connected. */
|
||||
colorOn?: CanvasColour
|
||||
/** Color when disconnected. */
|
||||
colorOff?: CanvasColour
|
||||
/** Render shape (circle, arrow, grid, etc.). */
|
||||
shape?: RenderShape
|
||||
/** Flow direction for link rendering. */
|
||||
dir?: LinkDirection
|
||||
}
|
||||
34
src/ecs/components/subgraph.ts
Normal file
34
src/ecs/components/subgraph.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Subgraph components.
|
||||
*
|
||||
* A subgraph is a graph that lives inside a SubgraphNode. It inherits
|
||||
* all node components (via its SubgraphNode entity) and adds structural
|
||||
* and metadata components for its interior.
|
||||
*
|
||||
* This is the most complex entity kind — it depends on Node and Link
|
||||
* extraction being complete first. See migration plan Phase 2.
|
||||
*/
|
||||
|
||||
import type { LinkEntityId, NodeEntityId, RerouteEntityId } from '../entityId'
|
||||
|
||||
/**
|
||||
* The interior structure of a subgraph.
|
||||
*
|
||||
* Replaces the recursive LGraph container that Subgraph inherits.
|
||||
* Entity IDs reference entities that live in the World — not in a
|
||||
* nested graph instance.
|
||||
*/
|
||||
export interface SubgraphStructure {
|
||||
/** Nodes contained within the subgraph. */
|
||||
nodeIds: readonly NodeEntityId[]
|
||||
/** Internal links (both endpoints inside the subgraph). */
|
||||
linkIds: readonly LinkEntityId[]
|
||||
/** Internal reroutes. */
|
||||
rerouteIds: readonly RerouteEntityId[]
|
||||
}
|
||||
|
||||
/** Descriptive metadata for a subgraph definition. */
|
||||
export interface SubgraphMeta {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
75
src/ecs/components/widget.ts
Normal file
75
src/ecs/components/widget.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Widget components.
|
||||
*
|
||||
* Widget value extraction is already complete (WidgetValueStore).
|
||||
* These interfaces formalize the target shape and add the layout
|
||||
* component that remains on the BaseWidget class.
|
||||
*
|
||||
* The 23+ widget subclasses (NumberWidget, ComboWidget, etc.) become
|
||||
* configuration data here. Widget-type-specific rendering behavior
|
||||
* will live in the RenderSystem.
|
||||
*/
|
||||
|
||||
import type {
|
||||
IWidgetOptions,
|
||||
TWidgetValue
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
import type { NodeEntityId } from '../entityId'
|
||||
|
||||
/** Immutable identity of a widget within its parent node. */
|
||||
export interface WidgetIdentity {
|
||||
/** Widget name (unique within a node). */
|
||||
name: string
|
||||
/**
|
||||
* Widget type string (e.g., 'number', 'combo', 'toggle', 'text').
|
||||
* Determines which system handles rendering and interaction.
|
||||
*/
|
||||
widgetType: string
|
||||
/** The node that owns this widget. */
|
||||
parentNodeId: NodeEntityId
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget value and configuration.
|
||||
*
|
||||
* Structurally equivalent to the existing WidgetState in
|
||||
* WidgetValueStore — the bridge layer can share the same objects.
|
||||
*/
|
||||
export interface WidgetValue {
|
||||
/** Current value (type depends on widgetType). */
|
||||
value: TWidgetValue
|
||||
/** Configuration options (min, max, step, values, etc.). */
|
||||
options: IWidgetOptions
|
||||
/** Display label override. */
|
||||
label?: string
|
||||
/** Whether the widget is disabled. */
|
||||
disabled?: boolean
|
||||
/** Whether to include this widget's value in serialized workflow JSON. */
|
||||
serialize?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout metrics computed during the arrange phase.
|
||||
*
|
||||
* Currently lives as mutable properties on BaseWidget (y,
|
||||
* computedHeight, width). The LayoutSystem will own these writes;
|
||||
* the RenderSystem reads them.
|
||||
*/
|
||||
export interface WidgetLayout {
|
||||
/** Vertical position relative to the node body. */
|
||||
y: number
|
||||
/** Computed height after layout distribution. */
|
||||
computedHeight: number
|
||||
/** Width override (undefined = use node width). */
|
||||
width?: number
|
||||
/** Layout size constraints from computeLayoutSize(). */
|
||||
constraints?: WidgetLayoutConstraints
|
||||
}
|
||||
|
||||
export interface WidgetLayoutConstraints {
|
||||
minHeight: number
|
||||
maxHeight?: number
|
||||
minWidth?: number
|
||||
maxWidth?: number
|
||||
}
|
||||
64
src/ecs/entityId.ts
Normal file
64
src/ecs/entityId.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Branded entity ID types for compile-time cross-kind safety.
|
||||
*
|
||||
* Each entity kind gets a nominal type wrapping its underlying primitive.
|
||||
* The brand prevents accidentally passing a LinkEntityId where a
|
||||
* NodeEntityId is expected — a class of bugs that plain `number` allows.
|
||||
*
|
||||
* At runtime these are just numbers (or strings for subgraphs). The brand
|
||||
* is erased by TypeScript and has zero runtime cost.
|
||||
*
|
||||
* @see {@link ../../../docs/adr/0008-entity-component-system.md}
|
||||
*/
|
||||
|
||||
// -- Branded ID types -------------------------------------------------------
|
||||
|
||||
type Brand<T, B extends string> = T & { readonly __brand: B }
|
||||
|
||||
export type NodeEntityId = Brand<number, 'NodeEntityId'>
|
||||
export type LinkEntityId = Brand<number, 'LinkEntityId'>
|
||||
export type SubgraphEntityId = Brand<string, 'SubgraphEntityId'>
|
||||
export type WidgetEntityId = Brand<number, 'WidgetEntityId'>
|
||||
export type SlotEntityId = Brand<number, 'SlotEntityId'>
|
||||
export type RerouteEntityId = Brand<number, 'RerouteEntityId'>
|
||||
export type GroupEntityId = Brand<number, 'GroupEntityId'>
|
||||
|
||||
/** Union of all entity ID types. */
|
||||
export type EntityId =
|
||||
| NodeEntityId
|
||||
| LinkEntityId
|
||||
| SubgraphEntityId
|
||||
| WidgetEntityId
|
||||
| SlotEntityId
|
||||
| RerouteEntityId
|
||||
| GroupEntityId
|
||||
|
||||
// -- Cast helpers (for use at system boundaries) ----------------------------
|
||||
|
||||
export function asNodeEntityId(id: number): NodeEntityId {
|
||||
return id as NodeEntityId
|
||||
}
|
||||
|
||||
export function asLinkEntityId(id: number): LinkEntityId {
|
||||
return id as LinkEntityId
|
||||
}
|
||||
|
||||
export function asSubgraphEntityId(id: string): SubgraphEntityId {
|
||||
return id as SubgraphEntityId
|
||||
}
|
||||
|
||||
export function asWidgetEntityId(id: number): WidgetEntityId {
|
||||
return id as WidgetEntityId
|
||||
}
|
||||
|
||||
export function asSlotEntityId(id: number): SlotEntityId {
|
||||
return id as SlotEntityId
|
||||
}
|
||||
|
||||
export function asRerouteEntityId(id: number): RerouteEntityId {
|
||||
return id as RerouteEntityId
|
||||
}
|
||||
|
||||
export function asGroupEntityId(id: number): GroupEntityId {
|
||||
return id as GroupEntityId
|
||||
}
|
||||
242
src/ecs/world.ts
Normal file
242
src/ecs/world.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* World — the central registry for all entity state.
|
||||
*
|
||||
* The World is a typed container mapping branded entity IDs to their
|
||||
* component sets. Systems query the World to read/write components;
|
||||
* entities never reference each other directly.
|
||||
*
|
||||
* This is the initial type definition (Phase 1c of the migration plan).
|
||||
* The implementation starts as plain Maps. CRDT backing, transactions,
|
||||
* and reactivity are future concerns.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Connectivity,
|
||||
Execution,
|
||||
NodeType,
|
||||
NodeVisual,
|
||||
Properties,
|
||||
WidgetContainer
|
||||
} from './components/node'
|
||||
import type { GroupChildren, GroupMeta, GroupVisual } from './components/group'
|
||||
import type { LinkEndpoints, LinkState, LinkVisual } from './components/link'
|
||||
import type { Position } from './components/position'
|
||||
import type { RerouteLinks, RerouteVisual } from './components/reroute'
|
||||
import type {
|
||||
SlotConnection,
|
||||
SlotIdentity,
|
||||
SlotVisual
|
||||
} from './components/slot'
|
||||
import type { SubgraphMeta, SubgraphStructure } from './components/subgraph'
|
||||
import type {
|
||||
WidgetIdentity,
|
||||
WidgetLayout,
|
||||
WidgetValue
|
||||
} from './components/widget'
|
||||
import type {
|
||||
GroupEntityId,
|
||||
LinkEntityId,
|
||||
NodeEntityId,
|
||||
RerouteEntityId,
|
||||
SlotEntityId,
|
||||
SubgraphEntityId,
|
||||
WidgetEntityId
|
||||
} from './entityId'
|
||||
|
||||
// -- Component bundles per entity kind --------------------------------------
|
||||
|
||||
export interface NodeComponents {
|
||||
position: Position
|
||||
nodeType: NodeType
|
||||
visual: NodeVisual
|
||||
connectivity: Connectivity
|
||||
execution: Execution
|
||||
properties: Properties
|
||||
widgetContainer: WidgetContainer
|
||||
}
|
||||
|
||||
export interface LinkComponents {
|
||||
endpoints: LinkEndpoints
|
||||
visual: LinkVisual
|
||||
state: LinkState
|
||||
}
|
||||
|
||||
export interface SlotComponents {
|
||||
identity: SlotIdentity
|
||||
connection: SlotConnection
|
||||
visual: SlotVisual
|
||||
}
|
||||
|
||||
export interface WidgetComponents {
|
||||
identity: WidgetIdentity
|
||||
value: WidgetValue
|
||||
layout: WidgetLayout
|
||||
}
|
||||
|
||||
export interface RerouteComponents {
|
||||
position: Position
|
||||
links: RerouteLinks
|
||||
visual: RerouteVisual
|
||||
}
|
||||
|
||||
export interface GroupComponents {
|
||||
position: Position
|
||||
meta: GroupMeta
|
||||
visual: GroupVisual
|
||||
children: GroupChildren
|
||||
}
|
||||
|
||||
export interface SubgraphComponents {
|
||||
structure: SubgraphStructure
|
||||
meta: SubgraphMeta
|
||||
}
|
||||
|
||||
// -- Entity kind registry ---------------------------------------------------
|
||||
|
||||
export interface EntityKindMap {
|
||||
node: { id: NodeEntityId; components: NodeComponents }
|
||||
link: { id: LinkEntityId; components: LinkComponents }
|
||||
slot: { id: SlotEntityId; components: SlotComponents }
|
||||
widget: { id: WidgetEntityId; components: WidgetComponents }
|
||||
reroute: { id: RerouteEntityId; components: RerouteComponents }
|
||||
group: { id: GroupEntityId; components: GroupComponents }
|
||||
subgraph: { id: SubgraphEntityId; components: SubgraphComponents }
|
||||
}
|
||||
|
||||
export type EntityKind = keyof EntityKindMap
|
||||
|
||||
// -- World interface --------------------------------------------------------
|
||||
|
||||
export interface World {
|
||||
/** Per-kind entity stores. */
|
||||
nodes: Map<NodeEntityId, NodeComponents>
|
||||
links: Map<LinkEntityId, LinkComponents>
|
||||
slots: Map<SlotEntityId, SlotComponents>
|
||||
widgets: Map<WidgetEntityId, WidgetComponents>
|
||||
reroutes: Map<RerouteEntityId, RerouteComponents>
|
||||
groups: Map<GroupEntityId, GroupComponents>
|
||||
subgraphs: Map<SubgraphEntityId, SubgraphComponents>
|
||||
|
||||
/**
|
||||
* Create a new entity of the given kind, returning its branded ID.
|
||||
* The entity starts with no components — call setComponent() to populate.
|
||||
*/
|
||||
createEntity<K extends EntityKind>(kind: K): EntityKindMap[K]['id']
|
||||
|
||||
/**
|
||||
* Remove an entity and all its components.
|
||||
* Returns true if the entity existed, false otherwise.
|
||||
*/
|
||||
deleteEntity<K extends EntityKind>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id']
|
||||
): boolean
|
||||
|
||||
/**
|
||||
* Get a single component from an entity.
|
||||
* Returns undefined if the entity or component doesn't exist.
|
||||
*/
|
||||
getComponent<
|
||||
K extends EntityKind,
|
||||
C extends keyof EntityKindMap[K]['components']
|
||||
>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id'],
|
||||
component: C
|
||||
): EntityKindMap[K]['components'][C] | undefined
|
||||
|
||||
/**
|
||||
* Set a single component on an entity.
|
||||
* Creates the component if it doesn't exist, overwrites if it does.
|
||||
*/
|
||||
setComponent<
|
||||
K extends EntityKind,
|
||||
C extends keyof EntityKindMap[K]['components']
|
||||
>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id'],
|
||||
component: C,
|
||||
data: EntityKindMap[K]['components'][C]
|
||||
): void
|
||||
}
|
||||
|
||||
// -- Factory ----------------------------------------------------------------
|
||||
|
||||
export function createWorld(): World {
|
||||
const counters = {
|
||||
node: 0,
|
||||
link: 0,
|
||||
slot: 0,
|
||||
widget: 0,
|
||||
reroute: 0,
|
||||
group: 0,
|
||||
subgraph: 0
|
||||
}
|
||||
|
||||
const stores = {
|
||||
nodes: new Map<NodeEntityId, NodeComponents>(),
|
||||
links: new Map<LinkEntityId, LinkComponents>(),
|
||||
slots: new Map<SlotEntityId, SlotComponents>(),
|
||||
widgets: new Map<WidgetEntityId, WidgetComponents>(),
|
||||
reroutes: new Map<RerouteEntityId, RerouteComponents>(),
|
||||
groups: new Map<GroupEntityId, GroupComponents>(),
|
||||
subgraphs: new Map<SubgraphEntityId, SubgraphComponents>()
|
||||
}
|
||||
|
||||
const storeForKind: Record<EntityKind, Map<unknown, unknown>> = {
|
||||
node: stores.nodes,
|
||||
link: stores.links,
|
||||
slot: stores.slots,
|
||||
widget: stores.widgets,
|
||||
reroute: stores.reroutes,
|
||||
group: stores.groups,
|
||||
subgraph: stores.subgraphs
|
||||
}
|
||||
|
||||
return {
|
||||
...stores,
|
||||
|
||||
createEntity<K extends EntityKind>(kind: K): EntityKindMap[K]['id'] {
|
||||
const id = ++counters[kind]
|
||||
const store = storeForKind[kind]
|
||||
store.set(id, {} as never)
|
||||
return id as EntityKindMap[K]['id']
|
||||
},
|
||||
|
||||
deleteEntity<K extends EntityKind>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id']
|
||||
): boolean {
|
||||
return storeForKind[kind].delete(id)
|
||||
},
|
||||
|
||||
getComponent<
|
||||
K extends EntityKind,
|
||||
C extends keyof EntityKindMap[K]['components']
|
||||
>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id'],
|
||||
component: C
|
||||
): EntityKindMap[K]['components'][C] | undefined {
|
||||
const entity = storeForKind[kind].get(id) as
|
||||
| EntityKindMap[K]['components']
|
||||
| undefined
|
||||
return entity?.[component]
|
||||
},
|
||||
|
||||
setComponent<
|
||||
K extends EntityKind,
|
||||
C extends keyof EntityKindMap[K]['components']
|
||||
>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id'],
|
||||
component: C,
|
||||
data: EntityKindMap[K]['components'][C]
|
||||
): void {
|
||||
const entity = storeForKind[kind].get(id)
|
||||
if (entity) {
|
||||
Object.assign(entity, { [component]: data })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user