@stuly/anode 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
- # anode
1
+ # @stuly/anode
2
2
 
3
- The core headless engine for Anode. It manages graph state, connectivity,
4
- spatial indexing, and history independently of any UI framework.
3
+ The high-performance, headless core engine for Anode. It manages graph topology, spatial indexing, transactional history, and reactive data flow independently of any UI framework.
5
4
 
6
5
  ## Installation
7
6
 
@@ -9,11 +8,29 @@ spatial indexing, and history independently of any UI framework.
9
8
  npm install @stuly/anode
10
9
  ```
11
10
 
12
- ## Features
11
+ ## Quick Start
13
12
 
14
- - **Headless Context:** Centralized state management for nodes, links, and sockets.
15
- - **QuadTree Indexing:** High-performance spatial querying and culling.
16
- - **Transactional History:** Built-in undo/redo with support for batched actions.
17
- - **Data Flow:** Automated value propagation between linked sockets.
13
+ ```typescript
14
+ import { Context, SocketKind } from '@stuly/anode';
18
15
 
19
- For detailed documentation and usage examples, see the [root README](../../README.md).
16
+ const ctx = new Context();
17
+
18
+ const nodeA = ctx.newEntity({ label: 'Source' });
19
+ const outA = ctx.newSocket(nodeA, SocketKind.OUTPUT, 'out');
20
+
21
+ const nodeB = ctx.newEntity({ label: 'Sink' });
22
+ const inB = ctx.newSocket(nodeB, SocketKind.INPUT, 'in');
23
+
24
+ ctx.newLink(outA, inB);
25
+ ctx.setSocketValue(outA.id, 'Data');
26
+ ```
27
+
28
+ ## Core Elements
29
+
30
+ - **`Context`**: Central state manager.
31
+ - **`Entity`**: Graph node with `inner` data.
32
+ - **`Socket`**: Reactive connection point (`INPUT`/`OUTPUT`).
33
+ - **`Link`**: Connection between sockets.
34
+ - **`Group`**: Hierarchical container.
35
+
36
+ For comprehensive documentation on architecture, spatial indexing, and history management, see the [Full README](https://github.com/stulyproject/anode?tab=readme-ov-file).
@@ -3,13 +3,29 @@ import { QuadTree } from "./layout.js";
3
3
  import { HistoryAction, HistoryManager } from "./history.js";
4
4
 
5
5
  //#region src/core/context.d.ts
6
+ /** Callback triggered when an entity is created or dropped */
6
7
  type EntityCallback<T> = (entity: Entity<T>) => void;
8
+ /** Callback triggered when a link is created, dropped, or updated */
7
9
  type LinkCallback = (link: Link) => void;
10
+ /** Callback triggered when a socket is created, dropped, or moved */
8
11
  type SocketCallback = (socket: Socket) => void;
12
+ /** Callback triggered when an entity moves, providing its new world position */
9
13
  type EntityMoveCallback<T> = (entity: Entity<T>, pos: Vec2) => void;
14
+ /** Callback triggered when a group is created or dropped */
10
15
  type GroupCallback = (group: Group) => void;
16
+ /** Callback triggered when a socket value is updated */
11
17
  type SocketValueCallback = (socket: Socket, value: any) => void;
18
+ /** A unique handle used to unregister a listener */
12
19
  type CallbackHandle = number;
20
+ /**
21
+ * The central engine for Anode.
22
+ *
23
+ * Context manages the lifecycle of entities, sockets, links, and groups.
24
+ * It handles reactive data propagation, spatial indexing (QuadTree),
25
+ * and transactional history (undo/redo).
26
+ *
27
+ * @template T The type of the custom data associated with entities.
28
+ */
13
29
  declare class Context<T = any> {
14
30
  private eid;
15
31
  private lid;
@@ -34,15 +50,20 @@ declare class Context<T = any> {
34
50
  private groupCreateCallbacks;
35
51
  private groupDropCallbacks;
36
52
  private bulkChangeCallbacks;
53
+ /** Map of all entities indexed by their unique ID */
37
54
  entities: Map<number, Entity<T>>;
55
+ /** Map of all links indexed by their unique ID */
38
56
  links: Map<number, Link>;
57
+ /** Map of all sockets indexed by their unique ID */
39
58
  sockets: Map<number, Socket>;
59
+ /** Map of all groups indexed by their unique ID */
40
60
  groups: Map<number, Group>;
61
+ /** Spatial index for efficient querying and culling */
41
62
  quadTree: QuadTree<number>;
63
+ /** Manager for undo/redo history */
42
64
  history: HistoryManager;
43
65
  private isApplyingHistory;
44
66
  private currentBatch;
45
- private currentUndoBatch;
46
67
  private isBatchingQuadTree;
47
68
  private getNextEid;
48
69
  private getNextLid;
@@ -50,48 +71,146 @@ declare class Context<T = any> {
50
71
  private getNextGid;
51
72
  private getNextCallbackHandle;
52
73
  private setupEntity;
74
+ /** Triggers all bulk change listeners. Usually called after undo/redo or batch operations. */
53
75
  notifyBulkChange(): void;
76
+ /**
77
+ * Registers a callback that is triggered when multiple changes occur at once.
78
+ * Useful for syncing UI state that doesn't need to respond to every individual mutation.
79
+ */
54
80
  registerBulkChangeListener(cb: () => void): CallbackHandle;
81
+ /**
82
+ * Sets the value of a specific socket and triggers reactive propagation.
83
+ *
84
+ * **Side Effects:**
85
+ * 1. Updates the `socket.value`.
86
+ * 2. Triggers `SocketValueListener` for the socket.
87
+ * 3. If the socket is an `OUTPUT`, pushes the value to all linked `INPUT` sockets recursively.
88
+ *
89
+ * @param socketId The unique ID of the socket.
90
+ * @param value The new value to assign.
91
+ */
55
92
  setSocketValue(socketId: number, value: any): void;
93
+ /** Registers a listener for socket value changes. */
56
94
  registerSocketValueListener(cb: SocketValueCallback): CallbackHandle;
95
+ /**
96
+ * Records a custom set of actions to the history stack.
97
+ * Internally used by all mutation methods.
98
+ */
57
99
  record(doActions: HistoryAction | HistoryAction[], undoActions: HistoryAction | HistoryAction[], label?: string): void;
100
+ /**
101
+ * Executes a function as a single atomic transaction in the history stack.
102
+ *
103
+ * During the batch:
104
+ * 1. Individual operations do not record separate history entries.
105
+ * 2. QuadTree updates are suspended until the end of the batch.
106
+ * 3. A single snapshot-based history entry is created for the entire operation.
107
+ *
108
+ * @param fn The function containing multiple mutations.
109
+ * @param label A human-readable label for the history entry (e.g., "Layout Graph").
110
+ */
58
111
  batch(fn: () => void, label?: string): void;
112
+ /** Reverts the last recorded transaction. */
59
113
  undo(): void;
114
+ /** Re-applies the last undone transaction. */
60
115
  redo(): void;
61
116
  private applyAction;
117
+ /** Registers a listener for entity creation. */
62
118
  registerEntityCreateListener(cb: EntityCallback<T>): CallbackHandle;
119
+ /** Registers a listener for entity deletion. */
63
120
  registerEntityDropListener(cb: EntityCallback<T>): CallbackHandle;
121
+ /** Registers a listener for entity movements (absolute position). */
64
122
  registerEntityMoveListener(cb: EntityMoveCallback<T>): CallbackHandle;
123
+ /** Registers a listener for socket movements (relative offset changes). */
65
124
  registerSocketMoveListener(cb: SocketCallback): CallbackHandle;
125
+ /** Triggers all socket move listeners. */
66
126
  notifySocketMove(socket: Socket): void;
127
+ /** Registers a listener for link creation. */
67
128
  registerLinkCreateListener(cb: LinkCallback): CallbackHandle;
129
+ /** Registers a listener for link deletion. */
68
130
  registerLinkDropListener(cb: LinkCallback): CallbackHandle;
131
+ /** Registers a listener for link updates (reconnections, waypoints). */
69
132
  registerLinkUpdateListener(cb: LinkCallback): CallbackHandle;
133
+ /** Registers a listener for socket creation. */
70
134
  registerSocketCreateListener(cb: SocketCallback): CallbackHandle;
135
+ /** Registers a listener for socket deletion. */
71
136
  registerSocketDropListener(cb: SocketCallback): CallbackHandle;
137
+ /** Registers a listener for group creation. */
72
138
  registerGroupCreateListener(cb: GroupCallback): CallbackHandle;
139
+ /** Registers a listener for group deletion. */
73
140
  registerGroupDropListener(cb: GroupCallback): CallbackHandle;
141
+ /**
142
+ * Unregisters a listener using the handle returned by the registration method.
143
+ * @returns true if the listener was successfully removed.
144
+ */
74
145
  unregisterListener(handle: CallbackHandle): boolean;
146
+ /** Creates and returns a new group. */
75
147
  newGroup(name?: string): Group;
148
+ /**
149
+ * Drops a group.
150
+ *
151
+ * **Side Effects:**
152
+ * 1. Detaches all child entities and groups (they remain in the context).
153
+ * 2. Removes the group from its parent group if applicable.
154
+ * 3. Triggers `GroupDropListener`.
155
+ */
76
156
  dropGroup(group: Group): void;
157
+ /** Calculates the absolute world position of an entity by traversing its parent group hierarchy. */
77
158
  getWorldPosition(entityId: number): Vec2;
159
+ /** Calculates the absolute world position of a group by traversing its parent group hierarchy. */
78
160
  getGroupWorldPosition(groupId: number): Vec2;
161
+ /**
162
+ * Moves a group and triggers move notifications for all nested entities recursively.
163
+ * This handles the complex coordinate system updates during group drags.
164
+ */
79
165
  moveGroup(group: Group, dx: number, dy: number): void;
166
+ /** Adds an entity to a group. Automatically removes it from its previous group if necessary. */
80
167
  addToGroup(groupId: number, entityId: number): void;
168
+ /** Removes an entity from its parent group. */
81
169
  removeFromGroup(groupId: number, entityId: number): void;
170
+ /** Adds a group to a parent group, creating a nested hierarchy. */
82
171
  addGroupToGroup(parentGroupId: number, childGroupId: number): void;
172
+ /** Removes a group from its parent group. */
83
173
  removeGroupFromGroup(parentGroupId: number, childGroupId: number): void;
174
+ /**
175
+ * Rebuilds the QuadTree spatial index.
176
+ * Suspended if `isBatchingQuadTree` is true.
177
+ */
84
178
  updateQuadTree(): void;
179
+ /**
180
+ * Creates a new entity.
181
+ * @param inner Custom data associated with the entity.
182
+ * @param forcedId Optional forced ID (useful for synchronization/deserialization).
183
+ */
85
184
  newEntity(inner: T, forcedId?: number): Entity<T>;
185
+ /**
186
+ * Drops an entity and all its associated sockets and links.
187
+ * This is performed as a batched operation to ensure consistent history.
188
+ */
86
189
  dropEntity(entity: Entity<T>): void;
190
+ /** Adds a new socket to an entity. */
87
191
  newSocket(entity: Entity<T>, kind: SocketKind, name?: string, forcedId?: number): Socket;
192
+ /** Drops a socket and all links connected to it. */
88
193
  dropSocket(socket: Socket): void;
194
+ /**
195
+ * Creates a new link between two sockets.
196
+ * Fails if the connection is invalid (e.g., creating a cycle, connecting same entity, etc.).
197
+ */
89
198
  newLink(from: Socket, to: Socket, kind?: LinkKind, forcedId?: number, inner?: any): Link<any> | null;
199
+ /** Updates an existing link's source or target. */
90
200
  updateLink(link: Link, fromId?: number, toId?: number): void;
201
+ /** Sets custom routing waypoints for a link. */
91
202
  setLinkWaypoints(link: Link, waypoints: Vec2[]): void;
203
+ /**
204
+ * Validation logic for connections.
205
+ * Prevents self-loops, same-entity links, identical kind connections,
206
+ * duplicate links, and cycles.
207
+ */
92
208
  canLink(from: Socket, to: Socket): boolean;
209
+ /** Performs a cycle detection search in the graph. */
93
210
  detectCycle(from: Socket, to: Socket): boolean;
211
+ /** Deletes a link from the context. */
94
212
  dropLink(link: Link): void;
213
+ /** Serializes the entire context state into a JSON-compatible object. */
95
214
  toJSON(): {
96
215
  entities: {
97
216
  id: number;
@@ -134,6 +253,10 @@ declare class Context<T = any> {
134
253
  parentId: number | null;
135
254
  }[];
136
255
  };
256
+ /**
257
+ * Restores the context state from a serialized JSON object.
258
+ * **Note:** This clears the current state completely.
259
+ */
137
260
  fromJSON(data: any): void;
138
261
  }
139
262
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","names":[],"sources":["../../src/core/context.ts"],"mappings":"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"}
1
+ {"version":3,"file":"context.d.ts","names":[],"sources":["../../src/core/context.ts"],"mappings":"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"}
@@ -3,6 +3,15 @@ import { QuadTree, Rect } from "./layout.js";
3
3
  import { HistoryManager } from "./history.js";
4
4
 
5
5
  //#region src/core/context.ts
6
+ /**
7
+ * The central engine for Anode.
8
+ *
9
+ * Context manages the lifecycle of entities, sockets, links, and groups.
10
+ * It handles reactive data propagation, spatial indexing (QuadTree),
11
+ * and transactional history (undo/redo).
12
+ *
13
+ * @template T The type of the custom data associated with entities.
14
+ */
6
15
  var Context = class {
7
16
  eid = 0;
8
17
  lid = 0;
@@ -27,15 +36,20 @@ var Context = class {
27
36
  groupCreateCallbacks = /* @__PURE__ */ new Map();
28
37
  groupDropCallbacks = /* @__PURE__ */ new Map();
29
38
  bulkChangeCallbacks = /* @__PURE__ */ new Map();
39
+ /** Map of all entities indexed by their unique ID */
30
40
  entities = /* @__PURE__ */ new Map();
41
+ /** Map of all links indexed by their unique ID */
31
42
  links = /* @__PURE__ */ new Map();
43
+ /** Map of all sockets indexed by their unique ID */
32
44
  sockets = /* @__PURE__ */ new Map();
45
+ /** Map of all groups indexed by their unique ID */
33
46
  groups = /* @__PURE__ */ new Map();
47
+ /** Spatial index for efficient querying and culling */
34
48
  quadTree = new QuadTree(new Rect(-1e5, -1e5, 2e5, 2e5));
49
+ /** Manager for undo/redo history */
35
50
  history = new HistoryManager();
36
51
  isApplyingHistory = false;
37
52
  currentBatch = null;
38
- currentUndoBatch = null;
39
53
  isBatchingQuadTree = false;
40
54
  getNextEid() {
41
55
  return this.freeEids.pop() ?? this.eid++;
@@ -53,7 +67,7 @@ var Context = class {
53
67
  return this.freeCallbackIds.pop() ?? this.callbackIds++;
54
68
  }
55
69
  setupEntity(entity) {
56
- entity.onMove((pos) => {
70
+ entity.onMove((_pos) => {
57
71
  this.updateQuadTree();
58
72
  for (const cb of this.entityMoveCallbacks.values()) try {
59
73
  cb(entity, this.getWorldPosition(entity.id));
@@ -62,6 +76,7 @@ var Context = class {
62
76
  }
63
77
  });
64
78
  }
79
+ /** Triggers all bulk change listeners. Usually called after undo/redo or batch operations. */
65
80
  notifyBulkChange() {
66
81
  for (const cb of this.bulkChangeCallbacks.values()) try {
67
82
  cb();
@@ -69,11 +84,26 @@ var Context = class {
69
84
  console.error(err);
70
85
  }
71
86
  }
87
+ /**
88
+ * Registers a callback that is triggered when multiple changes occur at once.
89
+ * Useful for syncing UI state that doesn't need to respond to every individual mutation.
90
+ */
72
91
  registerBulkChangeListener(cb) {
73
92
  const handle = this.getNextCallbackHandle();
74
93
  this.bulkChangeCallbacks.set(handle, cb);
75
94
  return handle;
76
95
  }
96
+ /**
97
+ * Sets the value of a specific socket and triggers reactive propagation.
98
+ *
99
+ * **Side Effects:**
100
+ * 1. Updates the `socket.value`.
101
+ * 2. Triggers `SocketValueListener` for the socket.
102
+ * 3. If the socket is an `OUTPUT`, pushes the value to all linked `INPUT` sockets recursively.
103
+ *
104
+ * @param socketId The unique ID of the socket.
105
+ * @param value The new value to assign.
106
+ */
77
107
  setSocketValue(socketId, value) {
78
108
  const socket = this.sockets.get(socketId);
79
109
  if (!socket) return;
@@ -83,11 +113,16 @@ var Context = class {
83
113
  for (const link of this.links.values()) if (link.from === socketId) this.setSocketValue(link.to, value);
84
114
  }
85
115
  }
116
+ /** Registers a listener for socket value changes. */
86
117
  registerSocketValueListener(cb) {
87
118
  const handle = this.getNextCallbackHandle();
88
119
  this.socketValueCallbacks.set(handle, cb);
89
120
  return handle;
90
121
  }
122
+ /**
123
+ * Records a custom set of actions to the history stack.
124
+ * Internally used by all mutation methods.
125
+ */
91
126
  record(doActions, undoActions, label) {
92
127
  if (this.isApplyingHistory) return;
93
128
  if (this.currentBatch) return;
@@ -100,6 +135,17 @@ var Context = class {
100
135
  timestamp: Date.now()
101
136
  });
102
137
  }
138
+ /**
139
+ * Executes a function as a single atomic transaction in the history stack.
140
+ *
141
+ * During the batch:
142
+ * 1. Individual operations do not record separate history entries.
143
+ * 2. QuadTree updates are suspended until the end of the batch.
144
+ * 3. A single snapshot-based history entry is created for the entire operation.
145
+ *
146
+ * @param fn The function containing multiple mutations.
147
+ * @param label A human-readable label for the history entry (e.g., "Layout Graph").
148
+ */
103
149
  batch(fn, label) {
104
150
  if (this.currentBatch) {
105
151
  fn();
@@ -132,6 +178,7 @@ var Context = class {
132
178
  this.isBatchingQuadTree = oldBatchingQT;
133
179
  }
134
180
  }
181
+ /** Reverts the last recorded transaction. */
135
182
  undo() {
136
183
  const cmd = this.history.undoStack.pop();
137
184
  if (!cmd) return;
@@ -148,6 +195,7 @@ var Context = class {
148
195
  this.isBatchingQuadTree = false;
149
196
  }
150
197
  }
198
+ /** Re-applies the last undone transaction. */
151
199
  redo() {
152
200
  const cmd = this.history.redoStack.pop();
153
201
  if (!cmd) return;
@@ -284,26 +332,31 @@ var Context = class {
284
332
  }
285
333
  }
286
334
  }
335
+ /** Registers a listener for entity creation. */
287
336
  registerEntityCreateListener(cb) {
288
337
  const handle = this.getNextCallbackHandle();
289
338
  this.entityCreateCallbacks.set(handle, cb);
290
339
  return handle;
291
340
  }
341
+ /** Registers a listener for entity deletion. */
292
342
  registerEntityDropListener(cb) {
293
343
  const handle = this.getNextCallbackHandle();
294
344
  this.entityDropCallbacks.set(handle, cb);
295
345
  return handle;
296
346
  }
347
+ /** Registers a listener for entity movements (absolute position). */
297
348
  registerEntityMoveListener(cb) {
298
349
  const handle = this.getNextCallbackHandle();
299
350
  this.entityMoveCallbacks.set(handle, cb);
300
351
  return handle;
301
352
  }
353
+ /** Registers a listener for socket movements (relative offset changes). */
302
354
  registerSocketMoveListener(cb) {
303
355
  const handle = this.getNextCallbackHandle();
304
356
  this.socketMoveCallbacks.set(handle, cb);
305
357
  return handle;
306
358
  }
359
+ /** Triggers all socket move listeners. */
307
360
  notifySocketMove(socket) {
308
361
  for (const cb of this.socketMoveCallbacks.values()) try {
309
362
  cb(socket);
@@ -311,41 +364,52 @@ var Context = class {
311
364
  console.error(err);
312
365
  }
313
366
  }
367
+ /** Registers a listener for link creation. */
314
368
  registerLinkCreateListener(cb) {
315
369
  const handle = this.getNextCallbackHandle();
316
370
  this.linkCreateCallbacks.set(handle, cb);
317
371
  return handle;
318
372
  }
373
+ /** Registers a listener for link deletion. */
319
374
  registerLinkDropListener(cb) {
320
375
  const handle = this.getNextCallbackHandle();
321
376
  this.linkDropCallbacks.set(handle, cb);
322
377
  return handle;
323
378
  }
379
+ /** Registers a listener for link updates (reconnections, waypoints). */
324
380
  registerLinkUpdateListener(cb) {
325
381
  const handle = this.getNextCallbackHandle();
326
382
  this.linkUpdateCallbacks.set(handle, cb);
327
383
  return handle;
328
384
  }
385
+ /** Registers a listener for socket creation. */
329
386
  registerSocketCreateListener(cb) {
330
387
  const handle = this.getNextCallbackHandle();
331
388
  this.socketCreateCallbacks.set(handle, cb);
332
389
  return handle;
333
390
  }
391
+ /** Registers a listener for socket deletion. */
334
392
  registerSocketDropListener(cb) {
335
393
  const handle = this.getNextCallbackHandle();
336
394
  this.socketDropCallbacks.set(handle, cb);
337
395
  return handle;
338
396
  }
397
+ /** Registers a listener for group creation. */
339
398
  registerGroupCreateListener(cb) {
340
399
  const handle = this.getNextCallbackHandle();
341
400
  this.groupCreateCallbacks.set(handle, cb);
342
401
  return handle;
343
402
  }
403
+ /** Registers a listener for group deletion. */
344
404
  registerGroupDropListener(cb) {
345
405
  const handle = this.getNextCallbackHandle();
346
406
  this.groupDropCallbacks.set(handle, cb);
347
407
  return handle;
348
408
  }
409
+ /**
410
+ * Unregisters a listener using the handle returned by the registration method.
411
+ * @returns true if the listener was successfully removed.
412
+ */
349
413
  unregisterListener(handle) {
350
414
  if (this.linkCreateCallbacks.delete(handle) || this.linkDropCallbacks.delete(handle) || this.linkUpdateCallbacks.delete(handle) || this.entityCreateCallbacks.delete(handle) || this.entityDropCallbacks.delete(handle) || this.entityMoveCallbacks.delete(handle) || this.socketCreateCallbacks.delete(handle) || this.socketDropCallbacks.delete(handle) || this.socketMoveCallbacks.delete(handle) || this.groupCreateCallbacks.delete(handle) || this.groupDropCallbacks.delete(handle) || this.bulkChangeCallbacks.delete(handle)) {
351
415
  this.freeCallbackIds.push(handle);
@@ -353,6 +417,7 @@ var Context = class {
353
417
  }
354
418
  return false;
355
419
  }
420
+ /** Creates and returns a new group. */
356
421
  newGroup(name = "") {
357
422
  const group = new Group(this.getNextGid(), name);
358
423
  this.groups.set(group.id, group);
@@ -363,6 +428,14 @@ var Context = class {
363
428
  }
364
429
  return group;
365
430
  }
431
+ /**
432
+ * Drops a group.
433
+ *
434
+ * **Side Effects:**
435
+ * 1. Detaches all child entities and groups (they remain in the context).
436
+ * 2. Removes the group from its parent group if applicable.
437
+ * 3. Triggers `GroupDropListener`.
438
+ */
366
439
  dropGroup(group) {
367
440
  if (this.groups.delete(group.id)) {
368
441
  if (group.parentId !== null) this.removeGroupFromGroup(group.parentId, group.id);
@@ -383,6 +456,7 @@ var Context = class {
383
456
  this.updateQuadTree();
384
457
  }
385
458
  }
459
+ /** Calculates the absolute world position of an entity by traversing its parent group hierarchy. */
386
460
  getWorldPosition(entityId) {
387
461
  const entity = this.entities.get(entityId);
388
462
  if (!entity) return new Vec2();
@@ -397,6 +471,7 @@ var Context = class {
397
471
  }
398
472
  return pos;
399
473
  }
474
+ /** Calculates the absolute world position of a group by traversing its parent group hierarchy. */
400
475
  getGroupWorldPosition(groupId) {
401
476
  const group = this.groups.get(groupId);
402
477
  if (!group) return new Vec2();
@@ -411,6 +486,10 @@ var Context = class {
411
486
  }
412
487
  return pos;
413
488
  }
489
+ /**
490
+ * Moves a group and triggers move notifications for all nested entities recursively.
491
+ * This handles the complex coordinate system updates during group drags.
492
+ */
414
493
  moveGroup(group, dx, dy) {
415
494
  const oldBatching = this.isBatchingQuadTree;
416
495
  this.isBatchingQuadTree = true;
@@ -433,6 +512,7 @@ var Context = class {
433
512
  this.updateQuadTree();
434
513
  }
435
514
  }
515
+ /** Adds an entity to a group. Automatically removes it from its previous group if necessary. */
436
516
  addToGroup(groupId, entityId) {
437
517
  const group = this.groups.get(groupId);
438
518
  const entity = this.entities.get(entityId);
@@ -443,6 +523,7 @@ var Context = class {
443
523
  this.updateQuadTree();
444
524
  }
445
525
  }
526
+ /** Removes an entity from its parent group. */
446
527
  removeFromGroup(groupId, entityId) {
447
528
  const group = this.groups.get(groupId);
448
529
  const entity = this.entities.get(entityId);
@@ -452,6 +533,7 @@ var Context = class {
452
533
  this.updateQuadTree();
453
534
  }
454
535
  }
536
+ /** Adds a group to a parent group, creating a nested hierarchy. */
455
537
  addGroupToGroup(parentGroupId, childGroupId) {
456
538
  const parent = this.groups.get(parentGroupId);
457
539
  const child = this.groups.get(childGroupId);
@@ -462,6 +544,7 @@ var Context = class {
462
544
  this.updateQuadTree();
463
545
  }
464
546
  }
547
+ /** Removes a group from its parent group. */
465
548
  removeGroupFromGroup(parentGroupId, childGroupId) {
466
549
  const parent = this.groups.get(parentGroupId);
467
550
  const child = this.groups.get(childGroupId);
@@ -471,6 +554,10 @@ var Context = class {
471
554
  this.updateQuadTree();
472
555
  }
473
556
  }
557
+ /**
558
+ * Rebuilds the QuadTree spatial index.
559
+ * Suspended if `isBatchingQuadTree` is true.
560
+ */
474
561
  updateQuadTree() {
475
562
  if (this.isBatchingQuadTree) return;
476
563
  this.quadTree.clear();
@@ -480,6 +567,11 @@ var Context = class {
480
567
  for (const socket of entity.sockets.values()) this.quadTree.insert(new Vec2(entityWorldPos.x + socket.offset.x, entityWorldPos.y + socket.offset.y), entity.id);
481
568
  }
482
569
  }
570
+ /**
571
+ * Creates a new entity.
572
+ * @param inner Custom data associated with the entity.
573
+ * @param forcedId Optional forced ID (useful for synchronization/deserialization).
574
+ */
483
575
  newEntity(inner, forcedId) {
484
576
  const ett = new Entity(forcedId ?? this.getNextEid(), inner);
485
577
  this.entities.set(ett.id, ett);
@@ -506,6 +598,10 @@ var Context = class {
506
598
  this.updateQuadTree();
507
599
  return ett;
508
600
  }
601
+ /**
602
+ * Drops an entity and all its associated sockets and links.
603
+ * This is performed as a batched operation to ensure consistent history.
604
+ */
509
605
  dropEntity(entity) {
510
606
  if (this.entities.has(entity.id)) this.batch(() => {
511
607
  this.record({
@@ -533,6 +629,7 @@ var Context = class {
533
629
  this.updateQuadTree();
534
630
  }, "Drop Entity");
535
631
  }
632
+ /** Adds a new socket to an entity. */
536
633
  newSocket(entity, kind, name = "", forcedId) {
537
634
  const socket = new Socket(forcedId ?? this.getNextSid(), entity.id, kind, name);
538
635
  this.sockets.set(socket.id, socket);
@@ -560,6 +657,7 @@ var Context = class {
560
657
  }
561
658
  return socket;
562
659
  }
660
+ /** Drops a socket and all links connected to it. */
563
661
  dropSocket(socket) {
564
662
  if (this.sockets.delete(socket.id)) {
565
663
  if (!this.isApplyingHistory) this.record({
@@ -588,6 +686,10 @@ var Context = class {
588
686
  }
589
687
  }
590
688
  }
689
+ /**
690
+ * Creates a new link between two sockets.
691
+ * Fails if the connection is invalid (e.g., creating a cycle, connecting same entity, etc.).
692
+ */
591
693
  newLink(from, to, kind = LinkKind.BEZIER, forcedId, inner = {}) {
592
694
  if (!this.canLink(from, to)) return null;
593
695
  const link = new Link(forcedId ?? this.getNextLid(), from.id, to.id, kind, inner);
@@ -616,6 +718,7 @@ var Context = class {
616
718
  }
617
719
  return link;
618
720
  }
721
+ /** Updates an existing link's source or target. */
619
722
  updateLink(link, fromId, toId) {
620
723
  const oldFrom = link.from;
621
724
  const oldTo = link.to;
@@ -668,6 +771,7 @@ var Context = class {
668
771
  console.error(err);
669
772
  }
670
773
  }
774
+ /** Sets custom routing waypoints for a link. */
671
775
  setLinkWaypoints(link, waypoints) {
672
776
  const oldWaypoints = link.waypoints.map((p) => ({
673
777
  x: p.x,
@@ -715,6 +819,11 @@ var Context = class {
715
819
  console.error(err);
716
820
  }
717
821
  }
822
+ /**
823
+ * Validation logic for connections.
824
+ * Prevents self-loops, same-entity links, identical kind connections,
825
+ * duplicate links, and cycles.
826
+ */
718
827
  canLink(from, to) {
719
828
  if (from.id === to.id) return false;
720
829
  if (from.entityId === to.entityId) return false;
@@ -723,6 +832,7 @@ var Context = class {
723
832
  if (this.detectCycle(from, to)) return false;
724
833
  return true;
725
834
  }
835
+ /** Performs a cycle detection search in the graph. */
726
836
  detectCycle(from, to) {
727
837
  const visited = /* @__PURE__ */ new Set();
728
838
  const stack = [to.entityId];
@@ -742,6 +852,7 @@ var Context = class {
742
852
  }
743
853
  return false;
744
854
  }
855
+ /** Deletes a link from the context. */
745
856
  dropLink(link) {
746
857
  if (this.links.delete(link.id)) {
747
858
  if (!this.isApplyingHistory) this.record({
@@ -767,6 +878,7 @@ var Context = class {
767
878
  }
768
879
  }
769
880
  }
881
+ /** Serializes the entire context state into a JSON-compatible object. */
770
882
  toJSON() {
771
883
  return {
772
884
  entities: Array.from(this.entities.values()).map((e) => ({
@@ -811,6 +923,10 @@ var Context = class {
811
923
  }))
812
924
  };
813
925
  }
926
+ /**
927
+ * Restores the context state from a serialized JSON object.
928
+ * **Note:** This clears the current state completely.
929
+ */
814
930
  fromJSON(data) {
815
931
  this.entities.clear();
816
932
  this.links.clear();