@vworlds/vecs 1.0.12 → 1.0.14

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/dist/entity.js CHANGED
@@ -14,18 +14,20 @@ import { Bitset } from "./util/bitset.js";
14
14
  * e.set(Position, { x: 100 });
15
15
  * ```
16
16
  *
17
- * Entities support a parentchild hierarchy. `parent` and `children` form a
17
+ * Entities support a parent-child hierarchy. `parent` and `children` form a
18
18
  * bidirectional link maintained by {@link setParent}; the children set is
19
19
  * created lazily. Destroying a parent recursively destroys its children.
20
20
  *
21
21
  * ## Deferred semantics
22
22
  *
23
23
  * Inside a system body or `forEach` iteration the world is in **deferred
24
- * mode**: `add` / `set` / `modified` / `remove` / `destroy` / `setParent` only
25
- * enqueue commands. The data layer (the components map and `componentBitmask`)
26
- * is mutated when the world drains its queue. Concretely, while deferred:
24
+ * mode**: `add` / `attach` / `set` / `modified` / `remove` / `destroy` /
25
+ * `setParent` only enqueue commands. The data layer (the components map and
26
+ * `componentBitmask`) is mutated when the world drains its queue. Concretely,
27
+ * while deferred:
27
28
  *
28
29
  * - `entity.get(C)` returns `undefined` after `entity.add(C)` (no instance yet).
30
+ * - `entity.get(C)` returns `undefined` after `entity.attach(instance)` if C was absent.
29
31
  * - `entity.get(C)` returns the previous value after `entity.set(C, props)`.
30
32
  * - `entity.get(C)` still returns the component after `entity.remove(C)`.
31
33
  *
@@ -42,6 +44,8 @@ export class Entity {
42
44
  this.eid = eid;
43
45
  /** @internal Maps numeric component type id to component instance. */
44
46
  this._components = new ArrayMap();
47
+ /** @internal Component types with pending modified delivery. */
48
+ this._dirtyComponentBitmask = new Bitset();
45
49
  /** @internal Set of queries this entity currently belongs to. */
46
50
  this._queries = new Set();
47
51
  /**
@@ -57,6 +61,14 @@ export class Entity {
57
61
  /** @internal Set to `true` after the world has fully torn down this entity. */
58
62
  this._destroyed = false;
59
63
  }
64
+ /** Fire every `onAdd` hook registered for `meta`. */
65
+ _runOnAddHandlers(meta, component) {
66
+ meta._onAddHandlers?.forEach((handler) => handler(this, component));
67
+ }
68
+ /** Fire every `onSet` hook registered for `meta`. */
69
+ _runOnSetHandlers(meta, component) {
70
+ meta._onSetHandlers?.forEach((handler) => handler(this, component));
71
+ }
60
72
  /**
61
73
  * Re-evaluate every world query for this entity, firing `_enter` / `_exit`
62
74
  * routing whenever membership flipped.
@@ -64,145 +76,70 @@ export class Entity {
64
76
  _updateQueries() {
65
77
  this.world.queries.forEach((q) => {
66
78
  const belongs = q.belongs(this);
67
- const isIn = this._isInQuery(q);
79
+ const isIn = this._queries.has(q);
68
80
  if (belongs !== isIn) {
69
81
  belongs ? q._enter(this) : q._exit(this);
70
82
  }
71
83
  });
72
84
  }
73
- /**
74
- * Construct a fresh component of `type`, apply `props`, store it on this
75
- * entity, fire the `onAdd` hook, and route query updates.
76
- *
77
- * If the component type belongs to an exclusivity group, any conflicting
78
- * component already on this entity is removed first.
79
- */
80
- _new(type, props) {
81
- const meta = this.world.getComponentMeta(type);
82
- if (meta.exclusive) {
83
- for (const exclusiveType of meta.exclusive) {
84
- if (this._components.has(exclusiveType)) {
85
- this._remove(exclusiveType);
85
+ /** Store a component instance and perform the shared add-side bookkeeping. */
86
+ _storeComponent(meta, component, isAdd) {
87
+ if (meta._exclusive) {
88
+ for (const exclusiveMeta of meta._exclusive) {
89
+ if (this._components.has(exclusiveMeta.type)) {
90
+ this._remove(exclusiveMeta);
86
91
  }
87
92
  }
88
93
  }
89
- const c = new meta.Class(this, meta);
90
- if (props !== undefined) {
91
- Object.assign(c, props);
92
- }
93
- this._components.set(type, c);
94
- this.componentBitmask.add(type);
95
- if (meta._onAddHandlers) {
96
- meta._onAddHandlers.forEach((handler) => handler(c));
94
+ this._components.set(meta.type, component);
95
+ this.componentBitmask.addBit(meta.bitPtr);
96
+ if (isAdd) {
97
+ this._runOnAddHandlers(meta, component);
97
98
  }
98
99
  this._updateQueries();
99
- return c;
100
- }
101
- /**
102
- * @internal Return `true` when this entity is currently tracked by `q`.
103
- */
104
- _isInQuery(q) {
105
- return this._queries.has(q);
106
100
  }
107
- /**
108
- * @internal Look up a component instance by numeric type id.
109
- *
110
- * Faster than {@link get} because no class → type id resolution is needed.
111
- */
112
- _get(type) {
113
- return this._components.get(type);
114
- }
115
- /**
116
- * @internal Reparent this entity in place, maintaining the bidirectional
117
- * link. Throws if `newParent` is a descendant of this entity.
118
- *
119
- * Called by the world either inline (outside deferred mode) or while
120
- * routing a queued `SetParent` command.
121
- */
122
- _setParent(newParent) {
123
- if (this._destroyed) {
124
- return;
125
- }
126
- if (newParent !== undefined) {
127
- let ancestor = newParent;
128
- while (ancestor !== undefined) {
129
- if (ancestor === this) {
130
- throw new Error(`Circular parent reference: entity ${this.eid} is already an ancestor of entity ${newParent.eid}`);
131
- }
132
- ancestor = ancestor._parent;
133
- }
134
- }
135
- if (this._parent) {
136
- this._parent._children?.delete(this);
137
- }
138
- this._parent = newParent;
139
- if (newParent) {
140
- (newParent._children ?? (newParent._children = new Set())).add(this);
101
+ /** Perform the shared set-side hook and query update routing. */
102
+ _notifyComponentSet(meta, component, isUpdate) {
103
+ this._runOnSetHandlers(meta, component);
104
+ this._dirtyComponentBitmask.deleteBit(meta.bitPtr);
105
+ if (isUpdate) {
106
+ this._queries.forEach((q) => q._notifyModified(this, meta, component));
141
107
  }
142
108
  }
143
109
  /**
144
- * @internal Apply a `Set` command: create the component if missing, assign
145
- * `props` if provided, fire `onSet`, and route modified events to every
146
- * query that watches the component type.
110
+ * Construct a fresh component of `type`, apply `props`, store it on this
111
+ * entity, fire the `onAdd` hook, and route query updates.
112
+ *
113
+ * If the component type belongs to an exclusivity group, any conflicting
114
+ * component already on this entity is removed first.
147
115
  */
148
- _set(type, props) {
149
- if (this._destroyed) {
150
- return;
151
- }
152
- const existing = this._components.get(type);
153
- const c = existing ?? this._new(type, props);
116
+ _new(meta, props) {
117
+ const c = new meta.Class();
154
118
  if (props !== undefined) {
155
- if (existing) {
156
- Object.assign(c, props);
157
- }
158
- const setHandlers = c.meta._onSetHandlers;
159
- if (setHandlers) {
160
- setHandlers.forEach((handler) => handler(c));
161
- }
162
- c._dirty = false;
163
- if (existing) {
164
- this._queries.forEach((q) => q._notifyModified(c));
165
- }
119
+ Object.assign(c, props);
166
120
  }
121
+ this._storeComponent(meta, c, true);
122
+ return c;
167
123
  }
168
124
  /**
169
- * @internal Apply a `Modified` command: fire `onSet` and route modified
170
- * events to every query that watches the component type.
125
+ * @internal Record that this entity is now tracked by `q`. Called by
126
+ * `Query._enter`.
171
127
  */
172
- _modified(type) {
173
- if (this._destroyed) {
174
- return;
175
- }
176
- const c = this._components.get(type);
177
- if (!c) {
178
- return;
179
- }
180
- const setHandlers = c.meta._onSetHandlers;
181
- if (setHandlers) {
182
- setHandlers.forEach((handler) => handler(c));
183
- }
184
- c._dirty = false;
185
- this._queries.forEach((q) => q._notifyModified(c));
128
+ _addQueryMembership(q) {
129
+ this._queries.add(q);
186
130
  }
187
131
  /**
188
- * @internal Apply a `Remove` command: clear the type bit, route exits,
189
- * detach the component, and fire `onRemove`.
132
+ * @internal Apply an `Attach` command: store the provided component instance,
133
+ * replacing any previous instance for this type. Events are fired as if the
134
+ * caller had performed a `set` operation.
190
135
  */
191
- _remove(type) {
136
+ _attach(meta, component) {
192
137
  if (this._destroyed) {
193
138
  return;
194
139
  }
195
- const c = this._components.get(type);
196
- if (!c) {
197
- return;
198
- }
199
- this.componentBitmask.delete(type);
200
- this._updateQueries();
201
- this._components.delete(type);
202
- const removeHandlers = c.meta._onRemoveHandlers;
203
- if (removeHandlers) {
204
- removeHandlers.forEach((handler) => handler(c));
205
- }
140
+ const existing = this._components.get(meta.type);
141
+ this._storeComponent(meta, component, existing === undefined);
142
+ this._notifyComponentSet(meta, component, existing !== undefined);
206
143
  }
207
144
  /**
208
145
  * @internal Apply a `Destroy` command: fire `_exit` on every query, then
@@ -221,12 +158,14 @@ export class Entity {
221
158
  }
222
159
  });
223
160
  toExit.forEach((q) => q._exit(this));
224
- this._components.forEach((c) => {
225
- const removeHandlers = c.meta._onRemoveHandlers;
161
+ this._components.forEach((c, type) => {
162
+ const meta = this.world.getComponentMeta(type);
163
+ const removeHandlers = meta._onRemoveHandlers;
226
164
  if (removeHandlers) {
227
- removeHandlers.forEach((handler) => handler(c));
165
+ removeHandlers.forEach((handler) => handler(this, c));
228
166
  }
229
167
  });
168
+ this._dirtyComponentBitmask.clear();
230
169
  if (this._events) {
231
170
  this._events.emit("destroy");
232
171
  this._events.removeAllListeners("destroy");
@@ -237,6 +176,34 @@ export class Entity {
237
176
  this._parent = undefined;
238
177
  }
239
178
  }
179
+ /**
180
+ * @internal Look up a component instance by numeric type id.
181
+ *
182
+ * Faster than {@link get} because no class to type id resolution is needed.
183
+ */
184
+ _get(type) {
185
+ return this._components.get(type);
186
+ }
187
+ /**
188
+ * @internal Return `true` when this entity is currently tracked by `q`.
189
+ */
190
+ _isInQuery(q) {
191
+ return this._queries.has(q);
192
+ }
193
+ /**
194
+ * @internal Apply a `Modified` command: fire `onSet` and route modified
195
+ * events to every query that watches the component type.
196
+ */
197
+ _modified(meta) {
198
+ if (this._destroyed) {
199
+ return;
200
+ }
201
+ const c = this._components.get(meta.type);
202
+ if (!c) {
203
+ return;
204
+ }
205
+ this._notifyComponentSet(meta, c, true);
206
+ }
240
207
  /**
241
208
  * @internal Forget query `q` without firing exit callbacks. Called by
242
209
  * {@link World} when a {@link Query.destroy} sweeps every entity.
@@ -245,11 +212,25 @@ export class Entity {
245
212
  this._queries.delete(q);
246
213
  }
247
214
  /**
248
- * @internal Record that this entity is now tracked by `q`. Called by
249
- * `Query._enter`.
215
+ * @internal Apply a `Remove` command: clear the type bit, route exits,
216
+ * detach the component, and fire `onRemove`.
250
217
  */
251
- _addQueryMembership(q) {
252
- this._queries.add(q);
218
+ _remove(meta) {
219
+ if (this._destroyed) {
220
+ return;
221
+ }
222
+ const c = this._components.get(meta.type);
223
+ if (!c) {
224
+ return;
225
+ }
226
+ this._dirtyComponentBitmask.deleteBit(meta.bitPtr);
227
+ this.componentBitmask.deleteBit(meta.bitPtr);
228
+ this._updateQueries();
229
+ this._components.delete(meta.type);
230
+ const removeHandlers = meta._onRemoveHandlers;
231
+ if (removeHandlers) {
232
+ removeHandlers.forEach((handler) => handler(this, c));
233
+ }
253
234
  }
254
235
  /**
255
236
  * @internal Record that this entity is no longer tracked by `q`. Called by
@@ -258,40 +239,66 @@ export class Entity {
258
239
  _removeQueryMembership(q) {
259
240
  this._queries.delete(q);
260
241
  }
261
- /** Parent entity in the scene hierarchy, or `undefined` for a root entity. */
262
- get parent() {
263
- return this._parent;
264
- }
265
242
  /**
266
- * Read-only view of direct child entities. The backing set is created lazily
267
- * on the first child link; before that this getter returns a shared empty set.
243
+ * @internal Apply a `Set` command: create the component if missing, assign
244
+ * `props` if provided, fire `onSet`, and route modified events to every
245
+ * query that watches the component type.
268
246
  */
269
- get children() {
270
- return this._children ?? Entity._emptyChildren;
247
+ _set(meta, props) {
248
+ if (this._destroyed) {
249
+ return;
250
+ }
251
+ const existing = this._components.get(meta.type);
252
+ const c = existing ?? this._new(meta, props);
253
+ if (props !== undefined) {
254
+ if (existing) {
255
+ Object.assign(c, props);
256
+ }
257
+ this._notifyComponentSet(meta, c, existing !== undefined);
258
+ }
271
259
  }
272
260
  /**
273
- * Typed event emitter for entity-level lifecycle events. Currently only the
274
- * `"destroy"` event is emitted, just before the entity is fully torn down.
261
+ * @internal Reparent this entity in place, maintaining the bidirectional
262
+ * link. Throws if `newParent` is a descendant of this entity.
275
263
  *
276
- * The emitter is created lazily on first access.
264
+ * Called by the world either inline (outside deferred mode) or while
265
+ * routing a queued `SetParent` command.
277
266
  */
278
- get events() {
279
- if (!this._events) {
280
- this._events = new Events();
267
+ _setParent(newParent) {
268
+ if (this._destroyed) {
269
+ return;
270
+ }
271
+ if (newParent !== undefined) {
272
+ let ancestor = newParent;
273
+ while (ancestor !== undefined) {
274
+ if (ancestor === this) {
275
+ throw new Error(`Circular parent reference: entity ${this.eid} is already an ancestor of entity ${newParent.eid}`);
276
+ }
277
+ ancestor = ancestor._parent;
278
+ }
279
+ }
280
+ if (this._parent) {
281
+ this._parent._children?.delete(this);
282
+ }
283
+ this._parent = newParent;
284
+ if (newParent) {
285
+ (newParent._children ?? (newParent._children = new Set())).add(this);
281
286
  }
282
- return this._events;
283
287
  }
284
- /** `true` when no components are currently attached to this entity. */
285
- get empty() {
286
- return this._components.size == 0;
288
+ /**
289
+ * Read-only view of direct child entities. The backing set is created lazily
290
+ * on the first child link; before that this getter returns a shared empty set.
291
+ */
292
+ get children() {
293
+ return this._children ?? Entity._emptyChildren;
287
294
  }
288
295
  /**
289
296
  * Read-only view of all components currently attached to this entity, keyed
290
297
  * by numeric component type id.
291
298
  *
292
299
  * The mutating methods (`set`, `delete`, `clear`) are not exposed. Use
293
- * `entity.add`, `entity.set`, and `entity.remove` to change the component
294
- * set.
300
+ * `entity.add`, `entity.attach`, `entity.set`, and `entity.remove` to change
301
+ * the component set.
295
302
  *
296
303
  * ```ts
297
304
  * entity.components.forEach((c) => console.log(c.constructor.name));
@@ -300,103 +307,145 @@ export class Entity {
300
307
  get components() {
301
308
  return this._components;
302
309
  }
310
+ /** `true` when no components are currently attached to this entity. */
311
+ get empty() {
312
+ return this._components.size == 0;
313
+ }
303
314
  /**
304
- * Reparent this entity. In deferred mode the change is queued; outside
305
- * deferred mode it executes inline.
315
+ * Typed event emitter for entity-level lifecycle events. Currently only the
316
+ * `"destroy"` event is emitted, just before the entity is fully torn down.
306
317
  *
307
- * @param newParent - New parent, or `undefined` to make this a root entity.
318
+ * The emitter is created lazily on first access.
308
319
  */
309
- setParent(newParent) {
320
+ get events() {
321
+ if (!this._events) {
322
+ this._events = new Events();
323
+ }
324
+ return this._events;
325
+ }
326
+ /** Parent entity in the scene hierarchy, or `undefined` for a root entity. */
327
+ get parent() {
328
+ return this._parent;
329
+ }
330
+ add(typeOrClass) {
331
+ const meta = this.world.getComponentMeta(typeOrClass);
310
332
  if (this.world.deferred) {
311
- this.world._enqueue({ kind: 5 /* CommandKind.SetParent */, entity: this, parent: newParent });
333
+ this.world._enqueue({ kind: 1 /* CommandKind.Set */, entity: this, meta, props: undefined });
312
334
  }
313
335
  else {
314
- this._setParent(newParent);
336
+ this._set(meta, undefined);
315
337
  }
338
+ return this;
316
339
  }
317
340
  /**
318
- * Mark `c` as having changed, queueing the corresponding `onSet` / `update`
319
- * notifications.
341
+ * Attach an existing component instance to this entity and store that exact
342
+ * object. If a component of the same registered class already exists, it is
343
+ * replaced rather than assigned into.
320
344
  *
321
- * Equivalent to `c.modified()` but returns the entity for chaining. Repeated
322
- * calls before the world routes the modified command are coalesced via the
323
- * component's dirty flag.
345
+ * `attach` uses the instance constructor to resolve component metadata, so
346
+ * the constructor must already be registered in this world. The operation
347
+ * fires hooks and query updates like a `set` operation.
324
348
  *
325
- * @param c - Component instance whose data changed.
349
+ * @param component - Existing component instance to store on the entity.
326
350
  * @returns This entity, for chaining.
327
351
  */
328
- modified(c) {
329
- if (c._dirty) {
330
- return this;
331
- }
332
- c._dirty = true;
352
+ attach(component) {
353
+ const meta = this.world.getComponentMeta(component.constructor);
333
354
  if (this.world.deferred) {
334
- this.world._enqueue({ kind: 2 /* CommandKind.Modified */, entity: this, type: c.type });
355
+ this.world._enqueue({ kind: 6 /* CommandKind.Attach */, entity: this, meta, component });
335
356
  }
336
357
  else {
337
- this._modified(c.type);
358
+ this._attach(meta, component);
338
359
  }
339
360
  return this;
340
361
  }
341
- add(typeOrClass) {
342
- const type = this.world.getComponentType(typeOrClass);
362
+ /**
363
+ * Destroy this entity and recursively destroy its children.
364
+ *
365
+ * Each component fires its `onRemove` hook, the `"destroy"` event is emitted
366
+ * just before teardown, and the entity is unregistered from the world.
367
+ * After destruction the entity must not be used.
368
+ */
369
+ destroy() {
343
370
  if (this.world.deferred) {
344
- this.world._enqueue({ kind: 1 /* CommandKind.Set */, entity: this, type, props: undefined });
371
+ this.world._enqueue({ kind: 4 /* CommandKind.Destroy */, entity: this });
345
372
  }
346
373
  else {
347
- this._set(type, undefined);
374
+ this._destroy();
375
+ }
376
+ if (this._children) {
377
+ this._children.forEach((child) => {
378
+ child.destroy();
379
+ });
380
+ this._children.clear();
348
381
  }
349
- return this;
350
382
  }
351
- set(typeOrClass, props) {
383
+ /**
384
+ * Look up a component on this entity.
385
+ *
386
+ * @param typeOrClass - Component class or numeric type id.
387
+ * @returns The component instance, or `undefined` when it is not attached.
388
+ */
389
+ get(typeOrClass) {
352
390
  const type = this.world.getComponentType(typeOrClass);
391
+ return this._get(type);
392
+ }
393
+ /**
394
+ * Mark a component type as having changed, queueing the corresponding `onSet` / `update`
395
+ * notifications.
396
+ *
397
+ * Repeated calls before the world routes the modified command are coalesced via
398
+ * the entity's dirty component bitset.
399
+ *
400
+ * @param typeOrClass - Component class or numeric type id whose data changed.
401
+ * @returns This entity, for chaining.
402
+ */
403
+ modified(typeOrClass) {
404
+ const meta = this.world.getComponentMeta(typeOrClass);
405
+ if (this._dirtyComponentBitmask.hasBit(meta.bitPtr)) {
406
+ return this;
407
+ }
408
+ this._dirtyComponentBitmask.addBit(meta.bitPtr);
353
409
  if (this.world.deferred) {
354
- this.world._enqueue({ kind: 1 /* CommandKind.Set */, entity: this, type, props });
410
+ this.world._enqueue({ kind: 2 /* CommandKind.Modified */, entity: this, meta });
355
411
  }
356
412
  else {
357
- this._set(type, props);
413
+ this._modified(meta);
358
414
  }
359
415
  return this;
360
416
  }
361
417
  remove(typeOrClass) {
362
- const type = this.world.getComponentType(typeOrClass);
418
+ const meta = this.world.getComponentMeta(typeOrClass);
363
419
  if (this.world.deferred) {
364
- this.world._enqueue({ kind: 3 /* CommandKind.Remove */, entity: this, type });
420
+ this.world._enqueue({ kind: 3 /* CommandKind.Remove */, entity: this, meta });
365
421
  }
366
422
  else {
367
- this._remove(type);
423
+ this._remove(meta);
368
424
  }
369
425
  }
370
426
  /**
371
- * Look up a component on this entity.
372
- *
373
- * @param typeOrClass - Component class or numeric type id.
374
- * @returns The component instance, or `undefined` when it is not attached.
375
- */
376
- get(typeOrClass) {
377
- const type = this.world.getComponentType(typeOrClass);
378
- return this._get(type);
379
- }
380
- /**
381
- * Destroy this entity and recursively destroy its children.
427
+ * Reparent this entity. In deferred mode the change is queued; outside
428
+ * deferred mode it executes inline.
382
429
  *
383
- * Each component fires its `onRemove` hook, the `"destroy"` event is emitted
384
- * just before teardown, and the entity is unregistered from the world.
385
- * After destruction the entity must not be used.
430
+ * @param newParent - New parent, or `undefined` to make this a root entity.
386
431
  */
387
- destroy() {
432
+ setParent(newParent) {
388
433
  if (this.world.deferred) {
389
- this.world._enqueue({ kind: 4 /* CommandKind.Destroy */, entity: this });
434
+ this.world._enqueue({ kind: 5 /* CommandKind.SetParent */, entity: this, parent: newParent });
390
435
  }
391
436
  else {
392
- this._destroy();
437
+ this._setParent(newParent);
393
438
  }
394
- if (this._children) {
395
- this._children.forEach((child) => {
396
- child.destroy();
397
- });
398
- this._children.clear();
439
+ }
440
+ set(typeOrClass, props) {
441
+ const meta = this.world.getComponentMeta(typeOrClass);
442
+ if (this.world.deferred) {
443
+ this.world._enqueue({ kind: 1 /* CommandKind.Set */, entity: this, meta, props });
444
+ }
445
+ else {
446
+ this._set(meta, props);
399
447
  }
448
+ return this;
400
449
  }
401
450
  /** Returns `"EntityN"` where N is the entity id. */
402
451
  toString() {