@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 +26 -9
- package/dist/core/context.d.ts +124 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/context.js +118 -2
- package/dist/core/context.js.map +1 -1
- package/dist/core/elements.d.ts +67 -4
- package/dist/core/elements.d.ts.map +1 -1
- package/dist/core/elements.js +67 -4
- package/dist/core/elements.js.map +1 -1
- package/dist/core/history.d.ts +25 -0
- package/dist/core/history.d.ts.map +1 -1
- package/dist/core/history.js +12 -0
- package/dist/core/history.js.map +1 -1
- package/dist/core/layout.d.ts +28 -0
- package/dist/core/layout.d.ts.map +1 -1
- package/dist/core/layout.js +28 -0
- package/dist/core/layout.js.map +1 -1
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
# anode
|
|
1
|
+
# @stuly/anode
|
|
2
2
|
|
|
3
|
-
The
|
|
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
|
-
##
|
|
11
|
+
## Quick Start
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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).
|
package/dist/core/context.d.ts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"context.d.ts","names":[],"sources":["../../src/core/context.ts"],"mappings":"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"}
|
package/dist/core/context.js
CHANGED
|
@@ -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((
|
|
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();
|