@adobe/ecs 0.2.0 → 0.2.3
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 +2 -2
- package/dist/implementation/compiler/describe.d.ts +1 -1
- package/dist/implementation/compiler/describe.js +3 -3
- package/dist/implementation/compiler/describe.js.map +1 -1
- package/dist/implementation/compiler/signature.js +1 -1
- package/dist/implementation/compiler/transform.js +3 -3
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/world/commands.js +50 -1
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/entities.d.ts +2 -0
- package/dist/implementation/world/entities.js +6 -0
- package/dist/implementation/world/entities.js.map +1 -1
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +10 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/register.js +2 -2
- package/dist/implementation/world/rows.d.ts +1 -1
- package/dist/implementation/world/rows.js +5 -0
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/runner.js +3 -3
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/sharing.js +1 -1
- package/dist/implementation/world/staging.d.ts +4 -0
- package/dist/implementation/world/staging.js +15 -3
- package/dist/implementation/world/staging.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +7 -0
- package/dist/implementation/world/tables-read.js +60 -0
- package/dist/implementation/world/tables-read.js.map +1 -0
- package/dist/implementation/world/tables.d.ts +9 -0
- package/dist/implementation/world/tables.js +355 -0
- package/dist/implementation/world/tables.js.map +1 -0
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +1 -0
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/interface/component/component.d.ts +7 -1
- package/dist/interface/component/component.js +0 -1
- package/dist/interface/component/component.js.map +1 -1
- package/dist/interface/component/declarations.d.ts +4 -0
- package/dist/interface/component/declarations.js +8 -0
- package/dist/interface/component/declarations.js.map +1 -0
- package/dist/interface/component/declared.d.ts +12 -0
- package/dist/interface/component/declared.js +2 -0
- package/dist/interface/component/declared.js.map +1 -0
- package/dist/interface/component/index.d.ts +3 -1
- package/dist/interface/component/index.js +1 -1
- package/dist/interface/component/index.js.map +1 -1
- package/dist/interface/component/value.d.ts +5 -5
- package/dist/interface/component/values.d.ts +2 -2
- package/dist/interface/finalizer/body.d.ts +5 -5
- package/dist/interface/registry/names.d.ts +2 -0
- package/dist/interface/registry/names.js +9 -0
- package/dist/interface/registry/names.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +3 -3
- package/dist/interface/registry/schedule.js +3 -3
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/resource/condition.d.ts +3 -3
- package/dist/interface/resource/declarations.d.ts +4 -0
- package/dist/interface/resource/declarations.js +8 -0
- package/dist/interface/resource/declarations.js.map +1 -0
- package/dist/interface/resource/index.d.ts +2 -1
- package/dist/interface/resource/index.js +1 -1
- package/dist/interface/resource/index.js.map +1 -1
- package/dist/interface/resource/resource.d.ts +4 -1
- package/dist/interface/resource/resource.js +0 -1
- package/dist/interface/resource/resource.js.map +1 -1
- package/dist/interface/resource/value.d.ts +5 -5
- package/dist/interface/resources/cadence.d.ts +1 -1
- package/dist/interface/system/access.d.ts +1 -1
- package/dist/interface/system/body.d.ts +13 -8
- package/dist/interface/system/check.d.ts +1 -1
- package/dist/interface/system/check.js +25 -9
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +20 -4
- package/dist/interface/system/create.d.ts +21 -9
- package/dist/interface/system/create.js +3 -3
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/declaration.d.ts +9 -2
- package/dist/interface/system/struct.d.ts +2 -2
- package/dist/interface/system/tables.d.ts +20 -0
- package/dist/interface/system/tables.js +2 -0
- package/dist/interface/system/tables.js.map +1 -0
- package/dist/interface/system/values-of.d.ts +4 -4
- package/dist/interface/system/values.d.ts +5 -5
- package/dist/interface/trait/types.d.ts +4 -4
- package/dist/interface/world/create.d.ts +3 -7
- package/dist/interface/world/declarations.d.ts +2 -2
- package/dist/interface/world/index.d.ts +1 -1
- package/dist/interface/world/join.d.ts +2 -10
- package/dist/interface/world/table/index.d.ts +26 -0
- package/dist/interface/world/table/index.js +3 -0
- package/dist/interface/world/table/index.js.map +1 -0
- package/dist/interface/world/table/table.d.ts +11 -0
- package/dist/interface/world/table/table.js +2 -0
- package/dist/interface/world/table/table.js.map +1 -0
- package/dist/interface/world/world.d.ts +10 -10
- package/dist/world.d.ts +27 -0
- package/dist/world.js +27 -0
- package/dist/world.js.map +1 -0
- package/package.json +1 -1
- package/src/exports.test.ts +3 -3
- package/src/guide/00-keys.ts +1 -1
- package/src/guide/01-resources.md +1 -1
- package/src/guide/01-resources.ts +1 -1
- package/src/guide/02-entities.ts +2 -2
- package/src/guide/03-systems.md +2 -2
- package/src/guide/03-systems.out +1 -1
- package/src/guide/03-systems.ts +3 -3
- package/src/guide/04-ticks.md +1 -1
- package/src/guide/04-ticks.ts +3 -3
- package/src/guide/05-scheduling.ts +1 -1
- package/src/guide/06-sync-points.md +9 -2
- package/src/guide/06-sync-points.ts +2 -2
- package/src/guide/07-change-filters.ts +2 -2
- package/src/guide/08-input.ts +1 -1
- package/src/guide/09-rules.ts +1 -1
- package/src/guide/11-traits.ts +3 -3
- package/src/guide/11-traits.type-test.ts +1 -1
- package/src/guide/12-lookups.ts +2 -2
- package/src/guide/13-indexes.ts +4 -4
- package/src/guide/14-kernels.ts +1 -1
- package/src/guide/15-reductions.ts +1 -1
- package/src/guide/16-gpu.ts +1 -1
- package/src/guide/17-hierarchies.ts +1 -1
- package/src/guide/18-identity.out +1 -1
- package/src/guide/19-threads.md +4 -2
- package/src/guide/19-threads.ts +2 -2
- package/src/guide/20-shared.ts +3 -3
- package/src/guide/21-dom.md +2 -2
- package/src/guide/21-dom.ts +1 -1
- package/src/guide/22-canvas.md +1 -1
- package/src/guide/22-canvas.ts +1 -1
- package/src/guide/CLAUDE.md +1 -1
- package/src/guide/guide.test.ts +7 -5
- package/src/implementation/compiler/describe.test-helper.ts +1 -1
- package/src/implementation/compiler/describe.test.ts +1 -1
- package/src/implementation/compiler/describe.ts +3 -3
- package/src/implementation/compiler/hygiene.test.ts +1 -1
- package/src/implementation/compiler/plugin.test.ts +2 -2
- package/src/implementation/compiler/signature.ts +1 -1
- package/src/implementation/compiler/transform.test.ts +3 -3
- package/src/implementation/compiler/transform.ts +3 -3
- package/src/implementation/world/change-blocks.test.ts +1 -1
- package/src/implementation/world/commands.ts +44 -1
- package/src/implementation/world/create.ts +3 -3
- package/src/implementation/world/defaults.test.ts +1 -1
- package/src/implementation/world/entities.ts +6 -0
- package/src/implementation/world/finalizers.test.ts +1 -1
- package/src/implementation/world/finalizers.ts +3 -3
- package/src/implementation/world/index-changed.test.ts +2 -2
- package/src/implementation/world/index-joined.test.ts +5 -5
- package/src/implementation/world/index-order.test.ts +1 -1
- package/src/implementation/world/join.test.ts +4 -4
- package/src/implementation/world/join.ts +10 -2
- package/src/implementation/world/joined-observer.test.ts +1 -1
- package/src/implementation/world/joining-worker.fixture.ts +1 -1
- package/src/implementation/world/joining.fixture.ts +1 -1
- package/src/implementation/world/joining.test-helper.ts +1 -1
- package/src/implementation/world/kernel-spawns.test.ts +1 -1
- package/src/implementation/world/observe-factories.test.ts +2 -2
- package/src/implementation/world/observe-ticks-cases.test.ts +1 -1
- package/src/implementation/world/observe.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained-worker.fixture.ts +1 -1
- package/src/implementation/world/observer-contained.test-helper.ts +1 -1
- package/src/implementation/world/observer-contained.test.ts +2 -2
- package/src/implementation/world/observer-joined.test-helper.ts +4 -4
- package/src/implementation/world/observer-joined.test.ts +23 -15
- package/src/implementation/world/observer.browser.test.ts +2 -2
- package/src/implementation/world/observer.test.ts +8 -8
- package/src/implementation/world/optional-workers.fixture.ts +1 -1
- package/src/implementation/world/optional.test.ts +7 -7
- package/src/implementation/world/pool-size.cases.ts +3 -1
- package/src/implementation/world/reads.test-helper.ts +6 -6
- package/src/implementation/world/reclaim.test.ts +3 -3
- package/src/implementation/world/register.ts +2 -2
- package/src/implementation/world/remote-objects.test-helper.ts +1 -1
- package/src/implementation/world/rows.ts +5 -1
- package/src/implementation/world/run-results.test.ts +10 -10
- package/src/implementation/world/runner.ts +3 -3
- package/src/implementation/world/shared-gpu.browser.test.ts +2 -2
- package/src/implementation/world/shared-kernels.test.ts +2 -2
- package/src/implementation/world/shared-workers.fixture.ts +2 -2
- package/src/implementation/world/shared.test.ts +8 -8
- package/src/implementation/world/sharing.ts +1 -1
- package/src/implementation/world/staging.ts +16 -3
- package/src/implementation/world/tables-read.ts +59 -0
- package/src/implementation/world/tables.test.ts +267 -0
- package/src/implementation/world/tables.ts +330 -0
- package/src/implementation/world/tails.test.ts +1 -1
- package/src/implementation/world/target.test.ts +2 -2
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.test-helper.ts +3 -3
- package/src/implementation/world/thread-systems.ts +1 -0
- package/src/implementation/world/where.test.ts +4 -4
- package/src/implementation/world/workers.fixture.ts +2 -2
- package/src/index.ts +4 -6
- package/src/interface/component/component.ts +7 -1
- package/src/interface/component/declarations.ts +11 -0
- package/src/interface/component/declared.ts +9 -0
- package/src/interface/component/index.ts +3 -1
- package/src/interface/component/value.ts +5 -5
- package/src/interface/component/values.ts +2 -2
- package/src/interface/finalizer/body.ts +5 -5
- package/src/interface/index.type-test.ts +3 -3
- package/src/interface/indexes/index.type-test.ts +1 -1
- package/src/interface/registry/names.ts +8 -0
- package/src/interface/registry/schedule.ts +4 -4
- package/src/interface/resource/condition.ts +3 -3
- package/src/interface/resource/declarations.ts +11 -0
- package/src/interface/resource/index.ts +2 -1
- package/src/interface/resource/resource.ts +4 -1
- package/src/interface/resource/value.ts +5 -5
- package/src/interface/resources/cadence.ts +1 -1
- package/src/interface/system/access.ts +1 -1
- package/src/interface/system/body.ts +9 -7
- package/src/interface/system/check.ts +22 -8
- package/src/interface/system/commands.ts +18 -4
- package/src/interface/system/create.test.ts +13 -13
- package/src/interface/system/create.ts +15 -10
- package/src/interface/system/declaration.ts +9 -2
- package/src/interface/system/lists.type-test.ts +1 -1
- package/src/interface/system/optional.type-test.ts +1 -1
- package/src/interface/system/shared.type-test.ts +1 -1
- package/src/interface/system/struct.ts +2 -2
- package/src/interface/system/system.type-test.ts +26 -20
- package/src/interface/system/tables.ts +21 -0
- package/src/interface/system/up.type-test.ts +3 -3
- package/src/interface/system/values-of.ts +4 -4
- package/src/interface/system/values.ts +5 -5
- package/src/interface/thread/thread.type-test.ts +1 -1
- package/src/interface/trait/trait.type-test.ts +4 -4
- package/src/interface/trait/types.ts +4 -4
- package/src/interface/world/create.ts +3 -7
- package/src/interface/world/declarations.ts +2 -2
- package/src/interface/world/index.ts +2 -2
- package/src/interface/world/join.ts +2 -10
- package/src/interface/world/loop.type-test.ts +6 -6
- package/src/interface/world/run.type-test.ts +3 -3
- package/src/interface/world/table/index.ts +19 -0
- package/src/interface/world/table/table.ts +12 -0
- package/src/interface/world/world.ts +10 -10
- package/src/observe/entity.ts +2 -2
- package/src/observe/members.ts +3 -3
- package/src/observe/query.ts +1 -1
- package/src/observe/resource.ts +2 -2
- package/src/observe/select.ts +5 -5
- package/src/observe/values.ts +2 -2
- package/src/observe/world-state.ts +3 -3
- package/src/perftest/benchmarks.ts +1 -1
- package/src/perftest/indexes.ts +3 -3
- package/src/perftest/loop.ts +1 -1
- package/src/perftest/observer.ts +4 -4
- package/src/perftest/reads.ts +2 -2
- package/src/perftest/remote.ts +5 -5
- package/src/perftest/strings.ts +1 -1
- package/src/perftest/suite.ts +1 -1
- package/src/world.test.ts +2 -2
- package/src/world.ts +32 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Layout, Path } from '../../interface/registry/layout.js';
|
|
3
|
+
import { declarationOf, labelOf } from '../../interface/registry/names.js';
|
|
4
|
+
import { isEntity } from '../../interface/registry/schemas.js';
|
|
5
|
+
import type { Schema } from '../../interface/schema/schema.js';
|
|
6
|
+
import type { Table } from '../../interface/world/table/table.js';
|
|
7
|
+
import { batchOf } from './batches.js';
|
|
8
|
+
import { indexOf, reserveRun } from './entities.js';
|
|
9
|
+
import { internShared } from './shared.js';
|
|
10
|
+
import { claimRows, openRows, writerOf, type Column as BatchColumn } from './staging.js';
|
|
11
|
+
import { commandIdOf, type Key, type State } from './state.js';
|
|
12
|
+
|
|
13
|
+
/** Whether a value of `schema` may hold an entity anywhere in it. */
|
|
14
|
+
const holdsEntity = (schema: Schema): boolean => {
|
|
15
|
+
if (isEntity(schema)) return true;
|
|
16
|
+
if ("anyOf" in schema) return schema.anyOf.some(holdsEntity);
|
|
17
|
+
if ("allOf" in schema) return schema.allOf.some(holdsEntity);
|
|
18
|
+
if (!("type" in schema)) return false;
|
|
19
|
+
switch (schema.type) {
|
|
20
|
+
case "array": case "set": return holdsEntity(schema.items);
|
|
21
|
+
case "map": return holdsEntity(schema.keys) || holdsEntity(schema.values);
|
|
22
|
+
case "object": return Object.values(schema.properties).some(holdsEntity);
|
|
23
|
+
default: return false;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The schema at `path` inside `schema`, through struct fields and tuple items. */
|
|
28
|
+
const schemaAt = (schema: Schema, path: Path): Schema | undefined => {
|
|
29
|
+
let at: Schema | undefined = schema;
|
|
30
|
+
for (const step of path) {
|
|
31
|
+
if (at === undefined || !("type" in at)) return undefined;
|
|
32
|
+
if (at.type === "object" && typeof step === "string") at = at.properties[step];
|
|
33
|
+
else if (at.type === "array" && typeof step === "number") at = at.items;
|
|
34
|
+
else return undefined;
|
|
35
|
+
}
|
|
36
|
+
return at;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** The f64 leaves of a linear layout holding entities: where, in f64 elements, each row's is. */
|
|
40
|
+
const entityLeavesOf = (layout: Layout, schema: Schema | undefined): readonly { readonly unitStride: number; readonly unitOffset: number }[] => {
|
|
41
|
+
if (layout.kind !== "linear" || schema === undefined) return [];
|
|
42
|
+
return layout.leaves.filter(leaf => {
|
|
43
|
+
const leafSchema = schemaAt(schema, leaf.path);
|
|
44
|
+
return leaf.view === "f64" && leafSchema !== undefined && isEntity(leafSchema);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** The entity each entity of the tables given is spawned as, and what a value naming none of them becomes. */
|
|
49
|
+
type Remap = (entity: number) => number;
|
|
50
|
+
|
|
51
|
+
/** `value` of `schema` with each entity in it replaced by `remap`'s. */
|
|
52
|
+
const remapValue = (schema: Schema, value: unknown, remap: Remap): unknown => {
|
|
53
|
+
if (isEntity(schema)) return typeof value === "number" ? remap(value) : value;
|
|
54
|
+
if (!holdsEntity(schema)) return value;
|
|
55
|
+
if ("anyOf" in schema) {
|
|
56
|
+
for (const member of schema.anyOf) {
|
|
57
|
+
if (isEntity(member) && typeof value === "number") return remap(value);
|
|
58
|
+
if (!("type" in member) || !holdsEntity(member)) continue;
|
|
59
|
+
if ((member.type === "array" && Array.isArray(value)) || (member.type === "set" && value instanceof Set) || (member.type === "map" && value instanceof Map)
|
|
60
|
+
|| (member.type === "object" && typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Set) && !(value instanceof Map))) return remapValue(member, value, remap);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if ("allOf" in schema) return schema.allOf.reduce<unknown>((next, member) => remapValue(member, next, remap), value);
|
|
65
|
+
if (!("type" in schema)) return value;
|
|
66
|
+
switch (schema.type) {
|
|
67
|
+
case "array": return Array.isArray(value) ? value.map(item => remapValue(schema.items, item, remap)) : value;
|
|
68
|
+
case "set": return value instanceof Set ? new Set(Array.from(value, item => remapValue(schema.items, item, remap))) : value;
|
|
69
|
+
case "map": return value instanceof Map ? new Map(Array.from(value, ([key, item]) => [remapValue(schema.keys, key, remap), remapValue(schema.values, item, remap)])) : value;
|
|
70
|
+
case "object": {
|
|
71
|
+
if (typeof value !== "object" || value === null) return value;
|
|
72
|
+
const copy: { [name: string]: unknown } = {};
|
|
73
|
+
for (const name of Object.keys(value)) {
|
|
74
|
+
const field = schema.properties[name];
|
|
75
|
+
const item: unknown = Reflect.get(value, name);
|
|
76
|
+
copy[name] = field === undefined ? item : remapValue(field, item, remap);
|
|
77
|
+
}
|
|
78
|
+
return copy;
|
|
79
|
+
}
|
|
80
|
+
default: return value;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** A table's components with their ids, the columns given for them, and their layouts. */
|
|
85
|
+
type Planned = {
|
|
86
|
+
readonly table: Table;
|
|
87
|
+
readonly keys: readonly Key[];
|
|
88
|
+
readonly ids: readonly number[];
|
|
89
|
+
readonly shared: (number | undefined)[];
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Throws, naming the component, for a column that doesn't fit its component's layout or its table's count. */
|
|
93
|
+
const checkColumn = (state: State, key: Key, id: number, column: Table.Column, count: number): void => {
|
|
94
|
+
const layout = state.layouts[id];
|
|
95
|
+
const fail = (message: string): never => { throw new Error(`commands.spawnTables: "${labelOf(key)}" ${message}.`); };
|
|
96
|
+
if (state.shared[id] !== undefined) {
|
|
97
|
+
if (column.kind !== "value") fail("is a shared component, so its column is one value");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (layout.kind === "constant" || column.kind === "value") return;
|
|
101
|
+
if (layout.kind === "linear") {
|
|
102
|
+
if (column.kind !== "bytes") fail("is stored as bytes, so its column gives bytes");
|
|
103
|
+
else if (column.stride !== layout.stride) fail(`has a stride of ${layout.stride} bytes in this world, not ${column.stride}`);
|
|
104
|
+
else if (column.bytes.length !== count * layout.stride) fail(`gives ${column.bytes.length} bytes for ${count} rows of ${layout.stride}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (column.kind !== "values") fail("is stored as values, so its column gives values");
|
|
108
|
+
else if (column.values.length !== count) fail(`gives ${column.values.length} values for ${count} rows`);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** The plan of each table: its keys, their command ids, and the numbers of its shared values. Throws for one that won't spawn. */
|
|
112
|
+
const planOf = (state: State, table: Table): Planned => {
|
|
113
|
+
const { count, entities, columns } = table;
|
|
114
|
+
if (!Number.isInteger(count) || count < 0 || entities.length !== count) throw new Error(`commands.spawnTables: a table of ${count} rows gives ${entities.length} entities.`);
|
|
115
|
+
const keys = Object.getOwnPropertySymbols(columns).filter(key => Reflect.get(columns, key) !== undefined);
|
|
116
|
+
const ids = keys.map(key => commandIdOf(state, key));
|
|
117
|
+
const shared: (number | undefined)[] = [];
|
|
118
|
+
keys.forEach((key, k) => {
|
|
119
|
+
const column: Table.Column = Reflect.get(columns, key);
|
|
120
|
+
checkColumn(state, key, ids[k], column, count);
|
|
121
|
+
const values = state.shared[ids[k]];
|
|
122
|
+
if (values !== undefined && column.kind === "value") {
|
|
123
|
+
const number = internShared(values, column.value);
|
|
124
|
+
if (number < 0) throw new Error(`commands.spawnTables: "${labelOf(key)}", a shared component, isn't given one of its type's values: ${labelOf(column.value)}.`);
|
|
125
|
+
shared[ids[k]] = number;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return { table, keys, ids, shared };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Where each entity of the tables is, by its index: the entity itself, to tell it from another of that index, and its
|
|
133
|
+
* position among all the tables' rows. Throws for an entity two rows name.
|
|
134
|
+
*/
|
|
135
|
+
const placesOf = (plans: readonly Planned[]): { readonly entities: Float64Array; readonly positions: Int32Array } => {
|
|
136
|
+
let largest = 0;
|
|
137
|
+
for (const { table } of plans) for (let row = 0; row < table.count; row++) largest = Math.max(largest, indexOf(table.entities[row]));
|
|
138
|
+
const entities = new Float64Array(largest + 1).fill(-1);
|
|
139
|
+
const positions = new Int32Array(largest + 1);
|
|
140
|
+
let position = 0;
|
|
141
|
+
for (const { table } of plans) {
|
|
142
|
+
for (let row = 0; row < table.count; row++) {
|
|
143
|
+
const entity = table.entities[row];
|
|
144
|
+
const index = indexOf(entity);
|
|
145
|
+
if (entities[index] === entity) throw new Error(`commands.spawnTables: entity ${entity} is in two rows.`);
|
|
146
|
+
entities[index] = entity;
|
|
147
|
+
positions[index] = position++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { entities, positions };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** A reference column's entity value in each row: the f64 at the row's start, a reference's whole value. */
|
|
154
|
+
const referencesOf = (column: Table.Column): ((row: number) => number) => {
|
|
155
|
+
if (column.kind !== "bytes") return () => 0;
|
|
156
|
+
const view = new DataView(column.bytes.buffer, column.bytes.byteOffset, column.bytes.byteLength);
|
|
157
|
+
const { stride } = column;
|
|
158
|
+
return row => view.getFloat64(row * stride, true);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Copies a table's `rows`, or all of them, of one column into rows `start` on of a batch's column, remapping entities. */
|
|
162
|
+
const copyColumn = (target: BatchColumn, column: Table.Column, schema: Schema | undefined, rows: readonly number[] | null, count: number, start: number, remap: Remap | null): void => {
|
|
163
|
+
const rowAt = (i: number): number => rows === null ? i : rows[i];
|
|
164
|
+
const remapped = remap !== null && schema !== undefined && holdsEntity(schema);
|
|
165
|
+
if (column.kind === "value") {
|
|
166
|
+
const write = writerOf(target, column.value);
|
|
167
|
+
const value = remapped ? remapValue(schema, column.value, remap) : column.value;
|
|
168
|
+
for (let i = 0; i < count; i++) write(start + i, value);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (column.kind === "bytes") {
|
|
172
|
+
const { stride } = target;
|
|
173
|
+
if (rows === null) target.views.u8.set(column.bytes, start * stride);
|
|
174
|
+
else for (let i = 0; i < count; i++) target.views.u8.set(column.bytes.subarray(rows[i] * stride, (rows[i] + 1) * stride), (start + i) * stride);
|
|
175
|
+
if (!remapped) return;
|
|
176
|
+
const { f64 } = target.views;
|
|
177
|
+
for (const leaf of entityLeavesOf(target.layout, schema)) {
|
|
178
|
+
for (let row = start, end = start + count; row < end; row++) {
|
|
179
|
+
const index = row * leaf.unitStride + leaf.unitOffset;
|
|
180
|
+
f64[index] = remap(f64[index]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (target.remote !== null && column.values.some(value => value !== undefined && value !== null)) throw new Error(target.remote);
|
|
186
|
+
for (let i = 0; i < count; i++) {
|
|
187
|
+
const value = column.values[rowAt(i)];
|
|
188
|
+
target.values[start + i] = remapped ? remapValue(schema, value, remap) : value;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** Spawns `rows` of a plan's table, or all of them, with the components it holds but `removed`, as `entities`. */
|
|
193
|
+
const spawnRows = (state: State, { table, keys, ids, shared }: Planned, rows: readonly number[] | null, removed: ReadonlySet<Key>, entities: Float64Array, remap: Remap | null): void => {
|
|
194
|
+
const kept = keys.flatMap((key, k) => removed.has(key) ? [] : [k]);
|
|
195
|
+
const batch = batchOf(state, kept.map(k => ids[k]), shared.length === 0 ? undefined : shared);
|
|
196
|
+
const start = openRows(batch, entities.length);
|
|
197
|
+
for (const k of kept) {
|
|
198
|
+
const target = batch.columns.find(column => column.id === ids[k]);
|
|
199
|
+
if (target === undefined || target.layout.kind === "constant") continue;
|
|
200
|
+
const column: Table.Column = Reflect.get(table.columns, keys[k]);
|
|
201
|
+
copyColumn(target, column, declarationOf(keys[k])?.type, rows, entities.length, start, remap);
|
|
202
|
+
}
|
|
203
|
+
claimRows(batch, entities);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Spawns an entity for each row of `tables` into the batches of their components, as references would leave them had
|
|
208
|
+
* the entities outside the tables despawned: a row owned, directly or through others, by an entity outside them isn't
|
|
209
|
+
* spawned; a weak reference to one is removed; and any other entity value naming one is `Entity.none`. Every entity
|
|
210
|
+
* value naming an entity spawned names the one spawned for it.
|
|
211
|
+
*/
|
|
212
|
+
export const spawnTables = (state: State, tables: readonly Table[]): Table.Spawned => {
|
|
213
|
+
const plans = tables.map(table => planOf(state, table));
|
|
214
|
+
// Entities are mapped only where a value of the tables may hold one.
|
|
215
|
+
const remapping = plans.some(({ keys }) => keys.some(key => {
|
|
216
|
+
const schema = declarationOf(key)?.type;
|
|
217
|
+
return schema !== undefined && holdsEntity(schema);
|
|
218
|
+
}));
|
|
219
|
+
const none: ReadonlySet<Key> = new Set();
|
|
220
|
+
if (!remapping) {
|
|
221
|
+
const entities = plans.map(({ table }) => {
|
|
222
|
+
const spawned = new Float64Array(table.count);
|
|
223
|
+
reserveRun(state.entities, spawned);
|
|
224
|
+
return spawned;
|
|
225
|
+
});
|
|
226
|
+
plans.forEach((plan, t) => spawnRows(state, plan, null, none, entities[t], null));
|
|
227
|
+
return { entities, dropped: 0 };
|
|
228
|
+
}
|
|
229
|
+
const places = placesOf(plans);
|
|
230
|
+
const positionOf = (entity: number): number => {
|
|
231
|
+
const index = indexOf(entity);
|
|
232
|
+
return entity !== 0 && index < places.entities.length && places.entities[index] === entity ? places.positions[index] : -1;
|
|
233
|
+
};
|
|
234
|
+
const firsts: number[] = [];
|
|
235
|
+
let total = 0;
|
|
236
|
+
for (const { table } of plans) {
|
|
237
|
+
firsts.push(total);
|
|
238
|
+
total += table.count;
|
|
239
|
+
}
|
|
240
|
+
// A row owned by an entity outside the tables isn't spawned, nor anything it owns, as despawning its owner would.
|
|
241
|
+
const dropped = new Uint8Array(total);
|
|
242
|
+
/** Each owner reference between rows of the tables: the owned row's position, and its owner's. */
|
|
243
|
+
const ownedRows: number[] = [];
|
|
244
|
+
const ownerRows: number[] = [];
|
|
245
|
+
const waiting: number[] = [];
|
|
246
|
+
plans.forEach(({ table, keys }, t) => {
|
|
247
|
+
for (const key of keys) {
|
|
248
|
+
if (declarationOf(key)?.reference !== "owner") continue;
|
|
249
|
+
const ownerAt = referencesOf(Reflect.get(table.columns, key));
|
|
250
|
+
for (let row = 0; row < table.count; row++) {
|
|
251
|
+
const owner = ownerAt(row);
|
|
252
|
+
if (owner === 0) continue;
|
|
253
|
+
const at = positionOf(owner);
|
|
254
|
+
const position = firsts[t] + row;
|
|
255
|
+
if (at >= 0) {
|
|
256
|
+
ownedRows.push(position);
|
|
257
|
+
ownerRows.push(at);
|
|
258
|
+
}
|
|
259
|
+
else if (dropped[position] === 0) {
|
|
260
|
+
dropped[position] = 1;
|
|
261
|
+
waiting.push(position);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
if (waiting.length > 0) {
|
|
267
|
+
const owned = new Map<number, number[]>();
|
|
268
|
+
for (let i = 0; i < ownedRows.length; i++) {
|
|
269
|
+
const list = owned.get(ownerRows[i]);
|
|
270
|
+
if (list === undefined) owned.set(ownerRows[i], [ownedRows[i]]);
|
|
271
|
+
else list.push(ownedRows[i]);
|
|
272
|
+
}
|
|
273
|
+
for (let next = waiting.pop(); next !== undefined; next = waiting.pop()) {
|
|
274
|
+
for (const child of owned.get(next) ?? []) {
|
|
275
|
+
if (dropped[child] !== 0) continue;
|
|
276
|
+
dropped[child] = 1;
|
|
277
|
+
waiting.push(child);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const spawnedAt = new Float64Array(total);
|
|
282
|
+
const entities = plans.map(({ table }, t) => {
|
|
283
|
+
const alive = [];
|
|
284
|
+
for (let row = 0; row < table.count; row++) if (dropped[firsts[t] + row] === 0) alive.push(row);
|
|
285
|
+
const reserved = new Float64Array(alive.length);
|
|
286
|
+
reserveRun(state.entities, reserved);
|
|
287
|
+
const spawned = new Float64Array(table.count);
|
|
288
|
+
alive.forEach((row, i) => {
|
|
289
|
+
spawned[row] = reserved[i];
|
|
290
|
+
spawnedAt[firsts[t] + row] = reserved[i];
|
|
291
|
+
});
|
|
292
|
+
return spawned;
|
|
293
|
+
});
|
|
294
|
+
const isSpawned = (entity: number): boolean => {
|
|
295
|
+
const at = positionOf(entity);
|
|
296
|
+
return at >= 0 && dropped[at] === 0;
|
|
297
|
+
};
|
|
298
|
+
const remap: Remap = entity => isSpawned(entity) ? spawnedAt[positionOf(entity)] : 0;
|
|
299
|
+
let droppedCount = 0;
|
|
300
|
+
plans.forEach((plan, t) => {
|
|
301
|
+
const { table, keys } = plan;
|
|
302
|
+
const first = firsts[t];
|
|
303
|
+
const weak = keys.filter(key => declarationOf(key)?.reference === "weak").map(key => ({ key, targetAt: referencesOf(Reflect.get(table.columns, key)) }));
|
|
304
|
+
const loses = (row: number): boolean => weak.some(({ targetAt }) => {
|
|
305
|
+
const target = targetAt(row);
|
|
306
|
+
return target !== 0 && !isSpawned(target);
|
|
307
|
+
});
|
|
308
|
+
let whole = true;
|
|
309
|
+
for (let row = 0; row < table.count && whole; row++) if (dropped[first + row] !== 0 || loses(row)) whole = false;
|
|
310
|
+
if (whole) return spawnRows(state, plan, null, none, entities[t], remap);
|
|
311
|
+
// Rows by the weak references they lose, whose targets aren't spawned: each set of components a batch of its own.
|
|
312
|
+
const groups = new Map<string, { readonly removed: Set<Key>; readonly rows: number[] }>();
|
|
313
|
+
for (let row = 0; row < table.count; row++) {
|
|
314
|
+
if (dropped[first + row] !== 0) {
|
|
315
|
+
droppedCount++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const removed = weak.filter(({ targetAt }) => {
|
|
319
|
+
const target = targetAt(row);
|
|
320
|
+
return target !== 0 && !isSpawned(target);
|
|
321
|
+
}).map(({ key }) => key);
|
|
322
|
+
const signature = removed.map(key => weak.findIndex(entry => entry.key === key)).join(",");
|
|
323
|
+
let group = groups.get(signature);
|
|
324
|
+
if (group === undefined) groups.set(signature, group = { removed: new Set(removed), rows: [] });
|
|
325
|
+
group.rows.push(row);
|
|
326
|
+
}
|
|
327
|
+
for (const { removed, rows } of groups.values()) spawnRows(state, plan, rows, removed, Float64Array.from(rows, row => entities[t][row]), remap);
|
|
328
|
+
});
|
|
329
|
+
return { entities, dropped: droppedCount };
|
|
330
|
+
};
|
|
@@ -75,7 +75,7 @@ describe("tails", () => {
|
|
|
75
75
|
const { world, remote } = await joined({ loop: driver }, { [always]: true });
|
|
76
76
|
await world.run(seed, { count: 10 });
|
|
77
77
|
const counters: number[] = [];
|
|
78
|
-
const values = System.create({
|
|
78
|
+
const values = System.create({ readonly: true }, ({ resources }) => { counters.push(resources[counter]); });
|
|
79
79
|
world.add(ticked);
|
|
80
80
|
world.add(values);
|
|
81
81
|
await until(Observe.resource(world, counter), value => value >= 5);
|
|
@@ -24,8 +24,8 @@ const remove = System.create({ target: "entity", components: { with: [tagged] }
|
|
|
24
24
|
});
|
|
25
25
|
let amounts: number[] = [];
|
|
26
26
|
let changed: number[] = [];
|
|
27
|
-
const readAmounts = System.create({
|
|
28
|
-
const watchChanged = System.create({
|
|
27
|
+
const readAmounts = System.create({ readonly: true, components: { with: [amount] } }, row => { amounts.push(row[amount]); });
|
|
28
|
+
const watchChanged = System.create({ readonly: true, components: { with: [amount], changed: [amount] } }, row => { changed.push(row[amount]); });
|
|
29
29
|
|
|
30
30
|
const inline = () => createWorld({ [workers]: 0 });
|
|
31
31
|
const amountsOf = async (world: ReturnType<typeof inline>): Promise<number[]> => {
|
|
@@ -69,7 +69,7 @@ const errorOf = (error: unknown): WorkerError =>
|
|
|
69
69
|
|
|
70
70
|
const refuse = (): never => { throw new Error("An observer system issues no commands."); };
|
|
71
71
|
/** An observer system's context has no commands, so these are never called. */
|
|
72
|
-
const noCommands: Commands = { spawn: refuse, despawn: refuse, add: refuse, remove: refuse };
|
|
72
|
+
const noCommands: Commands = { spawn: refuse, despawn: refuse, add: refuse, remove: refuse, spawnTables: refuse };
|
|
73
73
|
const noStamps: number[] = [];
|
|
74
74
|
|
|
75
75
|
export const createThreadObserver = (host: ThreadObserverHost) => {
|
|
@@ -172,7 +172,7 @@ const reading: { entities: readonly Entity[]; objects: readonly { readonly [name
|
|
|
172
172
|
|
|
173
173
|
/** Each entity's objects of the thread, as data, a note by its text: none for an entity that's dead or has no uid. */
|
|
174
174
|
const readObjects = System.create({
|
|
175
|
-
|
|
175
|
+
readonly: true,
|
|
176
176
|
lookup: { ids: { with: [uid] }, captions: { with: [caption] }, notes: { with: [note] } },
|
|
177
177
|
}, ({ lookup }) => {
|
|
178
178
|
reading.objects = reading.entities.map(entity => {
|
|
@@ -322,9 +322,9 @@ export const paintReader = System.create(({ resources }) => () => resources[pain
|
|
|
322
322
|
/** A composite of systems on the thread returning values, which no run resolves to. */
|
|
323
323
|
export const paintsTogether = System.create([paintReader, paintLengthThere]);
|
|
324
324
|
/** An observer system on the thread, as it reads the thread's paint: what a run of it resolves to. */
|
|
325
|
-
export const paintObserved = System.create({
|
|
325
|
+
export const paintObserved = System.create({ readonly: true }, ({ resources }) => resources[paint].text);
|
|
326
326
|
/** An observer system on the thread, throwing there. */
|
|
327
|
-
export const paintObservedFails = System.create({
|
|
327
|
+
export const paintObservedFails = System.create({ readonly: true }, ({ resources }) => {
|
|
328
328
|
if (resources[paint].text !== "never") throw new Error("threadSystemsTest observer failure there");
|
|
329
329
|
return 0;
|
|
330
330
|
});
|
|
@@ -279,6 +279,7 @@ export const createThreadSystems = (host: ThreadSystemsHost) => {
|
|
|
279
279
|
return typeof preset === "object" && preset !== null ? presetSpawnerOf(components, preset) : spawnerOf(components);
|
|
280
280
|
},
|
|
281
281
|
despawn: entity => beginCommand(writer, despawnCommand, entity),
|
|
282
|
+
spawnTables: () => { throw new Error("commands.spawnTables runs on the world's thread: run the system there."); },
|
|
282
283
|
add: (entity: number, components: symbol | readonly symbol[], values?: unknown) => {
|
|
283
284
|
if (typeof components === "symbol") {
|
|
284
285
|
const key = givenKeyOf(components);
|
|
@@ -62,10 +62,10 @@ export const noteInView = System.create({ components: { with: [position, note],
|
|
|
62
62
|
row[note] = { text: `${row[position]}` };
|
|
63
63
|
});
|
|
64
64
|
const heard: number[] = [];
|
|
65
|
-
export const hearInView = System.create({
|
|
65
|
+
export const hearInView = System.create({ readonly: true, components: { with: [position], changed: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, row => { heard.push(row[position]); });
|
|
66
66
|
// Filtered by a resource the predicate doesn't read: a culled archetype coming into view catches up on its change.
|
|
67
67
|
const signalled: number[] = [];
|
|
68
|
-
export const signalInView = System.create({
|
|
68
|
+
export const signalInView = System.create({ readonly: true, components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } }, resources: { changed: [other] } }, row => { signalled.push(row[position]); });
|
|
69
69
|
// A system moving the camera its own predicate reads: its next run sees the new view.
|
|
70
70
|
const followed: number[] = [];
|
|
71
71
|
export const follow = System.create({ components: { with: [position], where: { [cell]: (at, { resources }) => visible(at, resources[camera].x, resources[camera].radius) } } }, (row, { resources }) => {
|
|
@@ -79,7 +79,7 @@ export const noteChanged = System.create({ components: { with: [position, note],
|
|
|
79
79
|
row[note] = { text: `${row[position]}` };
|
|
80
80
|
});
|
|
81
81
|
const notes: string[] = [];
|
|
82
|
-
export const readNotes = System.create({
|
|
82
|
+
export const readNotes = System.create({ readonly: true, components: { with: [note] } }, row => { if (row[note] !== null) notes.push(row[note].text); });
|
|
83
83
|
export const nudgeNoted = System.create({ components: { with: [position, note] } }, row => { row[position] += 0.5; });
|
|
84
84
|
|
|
85
85
|
/** Waits a turn at a time until `done`, for two seconds at most. */
|
|
@@ -91,7 +91,7 @@ const until = async (done: () => boolean): Promise<void> => {
|
|
|
91
91
|
/** Every row's position and how often it was seen, by position. */
|
|
92
92
|
const seenOf = async (world: ReturnType<typeof createWorld>): Promise<readonly string[]> => {
|
|
93
93
|
const read: [number, string][] = [];
|
|
94
|
-
await world.run(System.create({
|
|
94
|
+
await world.run(System.create({ readonly: true, components: { with: [position, seen] } }, row => { read.push([row[position], `${row[position]}:${row[seen]}`]); }));
|
|
95
95
|
return read.sort((a, b) => a[0] - b[0]).map(([, text]) => text).filter(text => !text.endsWith(":0"));
|
|
96
96
|
};
|
|
97
97
|
|
|
@@ -149,8 +149,8 @@ const emitted = { seen: 0 };
|
|
|
149
149
|
const countEmitted = System.create({ components: { added: [position], with: [spawned] } }, () => { emitted.seen++; });
|
|
150
150
|
|
|
151
151
|
const gathered: Entity[] = [];
|
|
152
|
-
const gatherPositioned = System.create({
|
|
153
|
-
const gatherSpawned = System.create({
|
|
152
|
+
const gatherPositioned = System.create({ readonly: true, components: { with: [position] } }, row => { gathered.push(row.entity); });
|
|
153
|
+
const gatherSpawned = System.create({ readonly: true, components: { with: [spawned] } }, row => { gathered.push(row.entity); });
|
|
154
154
|
|
|
155
155
|
/** The entities an observer row system passes, in storage order. */
|
|
156
156
|
const storageOrder = async (world: World, gather: System<void>): Promise<Entity[]> => {
|
package/src/index.ts
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
* A high-performance, multithreaded Entity Component System.
|
|
4
4
|
* @module @adobe/ecs
|
|
5
5
|
*/
|
|
6
|
-
import { create, join } from './implementation/index.js';
|
|
7
|
-
import type { Create } from './interface/world/create.js';
|
|
8
|
-
import type { Join } from './interface/world/join.js';
|
|
9
6
|
import type { World as Created } from './interface/world/world.js';
|
|
10
7
|
|
|
11
8
|
export * from './interface/index.js';
|
|
@@ -15,7 +12,8 @@ export type World = Created;
|
|
|
15
12
|
|
|
16
13
|
/**
|
|
17
14
|
* How a world is made, `World.create(resources?)`, on the thread that will run it, with resources starting at the given
|
|
18
|
-
* values in place of their defaults;
|
|
19
|
-
* `World.join(port, { threads })
|
|
15
|
+
* values in place of their defaults; how another thread joins one that shared itself with it,
|
|
16
|
+
* `World.join(port, { threads })`; and `World.Table`, an archetype's rows as a readonly system's `tables` copies them
|
|
17
|
+
* and `commands.spawnTables` spawns them.
|
|
20
18
|
*/
|
|
21
|
-
export
|
|
19
|
+
export * as World from './world.js';
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Key } from './key.js';
|
|
3
|
+
import type { Type } from './value.js';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The type of component `K`'s value, as its record in `Components` declares it: `Component<typeof position>`. The
|
|
7
|
+
* namespace's `Key` and `With` name components' keys.
|
|
8
|
+
*/
|
|
9
|
+
export type Component<K extends Key> = Type<K>;
|
|
4
10
|
|
|
5
11
|
export * as Component from './index.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Declared } from './declared.js';
|
|
3
|
+
import { declaredOf } from '../registry/names.js';
|
|
4
|
+
import type { Key } from './key.js';
|
|
5
|
+
|
|
6
|
+
/** Every component its modules registered so far, with its declaration. */
|
|
7
|
+
export const declarations = (): ReadonlyMap<Key, Declared> => {
|
|
8
|
+
// Registration takes only keys declared in `Components`, so each symbol registered as a component is one of its keys.
|
|
9
|
+
const found = declaredOf("component") as ReadonlyMap<Key, Declared>;
|
|
10
|
+
return found;
|
|
11
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import type { Schema } from '../schema/schema.js';
|
|
3
|
+
import type { Key as TraitKey } from '../trait/key.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A component's or resource's declaration as registered: its schema as `type` where its value is data, what it
|
|
7
|
+
* starts at as `default`, and its other traits, the built-in ones by name and those of your own by their keys.
|
|
8
|
+
*/
|
|
9
|
+
export type Declared = { readonly type?: Schema; readonly default?: unknown } & { readonly [T in TraitKey]?: unknown };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
export type { Key } from './key.js';
|
|
3
|
-
export type {
|
|
3
|
+
export type { With } from './value.js';
|
|
4
|
+
export { declarations } from './declarations.js';
|
|
5
|
+
export type { Declared } from './declared.js';
|
|
@@ -4,16 +4,16 @@ import type { Key as TraitKey } from "../trait/key.js";
|
|
|
4
4
|
import type { Key } from "./key.js";
|
|
5
5
|
import type { Components } from "./types.js";
|
|
6
6
|
|
|
7
|
-
/** Every component's value type, computed once so `
|
|
8
|
-
type
|
|
7
|
+
/** Every component's value type, computed once so `Type` is a lookup. */
|
|
8
|
+
type Types = { readonly [K in Key]: Components[K] extends { readonly type: infer T } ? T : never };
|
|
9
9
|
|
|
10
10
|
/** The value type of component `K`. */
|
|
11
|
-
export type
|
|
11
|
+
export type Type<K extends Key> = Types[K];
|
|
12
12
|
|
|
13
13
|
type Primitive = string | number | boolean | bigint | symbol | null | undefined;
|
|
14
14
|
|
|
15
15
|
/** Component `K`'s value as reads return it: read-only, with branded primitives such as `Entity` as they are. */
|
|
16
|
-
export type
|
|
16
|
+
export type ReadonlyType<K extends Key> = Type<K> extends Primitive ? Type<K> : Readonly<Type<K>>;
|
|
17
17
|
|
|
18
18
|
/** Components that declare trait `T`, with a value assignable to `V`. */
|
|
19
19
|
export type With<T extends TraitKey, V = unknown> = { [K in Key]: Components[K] extends { readonly [P in T]: V } ? K : never }[Key];
|
|
@@ -35,7 +35,7 @@ export type Shared = With<"shared">;
|
|
|
35
35
|
export type Writable = Exclude<Key, With<"reference"> | Shared>;
|
|
36
36
|
|
|
37
37
|
/** Every component's thread, computed once so `ThreadOf` is a lookup. */
|
|
38
|
-
type Threads = { readonly [K in Key]: ThreadOf<Components[K],
|
|
38
|
+
type Threads = { readonly [K in Key]: ThreadOf<Components[K], Type<K>> };
|
|
39
39
|
|
|
40
40
|
/** The thread component `K` lives on, or `never` for a struct, which lives in shared memory. */
|
|
41
41
|
export type ThreadOfComponent<K extends Key> = Threads[K];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
2
|
import type { Key } from "./key.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Type } from './value.js';
|
|
4
4
|
|
|
5
5
|
/** Values for any subset of components, keyed by component. */
|
|
6
|
-
export type Values = { readonly [K in Key]?:
|
|
6
|
+
export type Values = { readonly [K in Key]?: Type<K> };
|
|
@@ -6,7 +6,7 @@ import type { Listed, Property } from '../system/property.js';
|
|
|
6
6
|
import type { LookupFunction } from '../system/values.js';
|
|
7
7
|
import type { WriteResources } from '../system/body.js';
|
|
8
8
|
import type { ChangesDeclaration, Declaration } from './declaration.js';
|
|
9
|
-
import type { Essential,
|
|
9
|
+
import type { Essential, ReadonlyType } from '../component/value.js';
|
|
10
10
|
import type { Elsewhere, ThreadsOf } from '../system/threads.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -14,8 +14,8 @@ import type { Elsewhere, ThreadsOf } from '../system/threads.js';
|
|
|
14
14
|
* its `optional` where it had them, `undefined` where it didn't.
|
|
15
15
|
*/
|
|
16
16
|
export type Row<D extends Declaration> =
|
|
17
|
-
{ readonly entity: Entity } & { readonly [K in Listed<Property<D, "components">, "with"> & Component.Key]: Component
|
|
18
|
-
& { readonly [K in Exclude<Listed<Property<D, "components">, "optional">, Listed<Property<D, "components">, "with">> & Component.Key]: Component
|
|
17
|
+
{ readonly entity: Entity } & { readonly [K in Listed<Property<D, "components">, "with"> & Component.Key]: Component<K> }
|
|
18
|
+
& { readonly [K in Exclude<Listed<Property<D, "components">, "optional">, Listed<Property<D, "components">, "with">> & Component.Key]: Component<K> | undefined };
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Every resource, its lookups when declared, and the commands it issues for the next sync point: none on another
|
|
@@ -46,7 +46,7 @@ export type Group<C extends Component.Key> = {
|
|
|
46
46
|
export type ChangeSet<C extends Component.Key> = {
|
|
47
47
|
readonly groups: readonly Group<C>[];
|
|
48
48
|
/** The value before the sync point, or null if the entity didn't have it. */
|
|
49
|
-
previous<K extends C>(entity: Entity, component: K):
|
|
49
|
+
previous<K extends C>(entity: Entity, component: K): ReadonlyType<K> | null;
|
|
50
50
|
/** The value after it, or null if the entity won't have it. */
|
|
51
|
-
next<K extends C>(entity: Entity, component: K):
|
|
51
|
+
next<K extends C>(entity: Entity, component: K): ReadonlyType<K> | null;
|
|
52
52
|
};
|
|
@@ -27,8 +27,8 @@ Rule.create({ components: { with: [position] }, derive: [slot] });
|
|
|
27
27
|
declare const create: Create;
|
|
28
28
|
declare const wgsl: typeof Gpu.wgsl;
|
|
29
29
|
export const world = create({ [workers]: 4, [gpuDevice]: null });
|
|
30
|
-
export const read = System.create({
|
|
31
|
-
export const readOne = System.create({
|
|
30
|
+
export const read = System.create({ readonly: true, components: { with: [position, slot] } }, () => {});
|
|
31
|
+
export const readOne = System.create({ readonly: true, lookup: { at: { with: [position, slot] } } }, () => {});
|
|
32
32
|
export const report = world.run(all);
|
|
33
33
|
export const shader = wgsl(draw);
|
|
34
34
|
export const added = world.add(read);
|
|
@@ -36,7 +36,7 @@ export const added = world.add(read);
|
|
|
36
36
|
// Only types a consumer declares with are named; the rest are inferred where wanted.
|
|
37
37
|
// @ts-expect-error a thread's name is keyof Threads
|
|
38
38
|
export type NoThread = Ecs.Thread;
|
|
39
|
-
// @ts-expect-error the cadence's value is Resource
|
|
39
|
+
// @ts-expect-error the cadence's value is Resource<typeof cadence>
|
|
40
40
|
export type NoCadence = Ecs.Cadence;
|
|
41
41
|
// @ts-expect-error a rule is the value Rule.create returns: ReturnType<typeof Rule.create>
|
|
42
42
|
export const noRule: Rule = Rule.create({ components: { with: [position] }, derive: [slot] });
|
|
@@ -219,7 +219,7 @@ System.create({
|
|
|
219
219
|
|
|
220
220
|
// An index lookup's `changed: true` runs its system only when the index's entries changed since it last ran: its
|
|
221
221
|
// lists' `changed` then reports entries alone. Observer systems and CPU kernels take it; it's not a list of components.
|
|
222
|
-
System.create({
|
|
222
|
+
System.create({ readonly: true, lookup: { living: { index: living, changed: true } } }, ({ lookup }) => {
|
|
223
223
|
const rows = lookup.living();
|
|
224
224
|
check<Equal<typeof rows.changed, boolean>>();
|
|
225
225
|
check<Equal<typeof rows.length, number>>();
|
|
@@ -61,6 +61,14 @@ const entryOf = (key: symbol): Entry | undefined => {
|
|
|
61
61
|
return entries.get(key);
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
/** Every key registered as `kind`, each by the symbol a world gives its values by, with its declaration. */
|
|
65
|
+
export const declaredOf = (kind: "component" | "resource"): ReadonlyMap<symbol, AnyDeclaration> => {
|
|
66
|
+
catchUp();
|
|
67
|
+
const found = new Map<symbol, AnyDeclaration>();
|
|
68
|
+
for (const entry of entries.values()) if (entry.kind === kind && entry.declaration !== undefined) found.set(entry.newest, entry.declaration);
|
|
69
|
+
return found;
|
|
70
|
+
};
|
|
71
|
+
|
|
64
72
|
/** Whether a key is a component, a resource or a trait. */
|
|
65
73
|
export const kindOf = (key: unknown): Kind | undefined => typeof key === "symbol" ? entryOf(key)?.kind : undefined;
|
|
66
74
|
|