@galfus/engine 0.0.0-stage → 0.31.1-alpha

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.
Files changed (80) hide show
  1. package/README.md +190 -2
  2. package/package.json +59 -4
  3. package/src/anchors/cache.ts +101 -0
  4. package/src/anchors/component.ts +123 -0
  5. package/src/anchors/hierarchy.ts +192 -0
  6. package/src/anchors/index.ts +8 -0
  7. package/src/anchors/solver.ts +247 -0
  8. package/src/anchors/strategy.ts +142 -0
  9. package/src/animation/blending.ts +163 -0
  10. package/src/animation/clip.ts +132 -0
  11. package/src/animation/coordinator.ts +568 -0
  12. package/src/animation/index.ts +7 -0
  13. package/src/animation/native-targets.ts +37 -0
  14. package/src/animation/player.ts +287 -0
  15. package/src/audio/index.ts +4 -0
  16. package/src/audio/manager.ts +371 -0
  17. package/src/collision/components.ts +211 -0
  18. package/src/collision/index.ts +16 -0
  19. package/src/collision/query2d.ts +167 -0
  20. package/src/collision/query3d.ts +170 -0
  21. package/src/ecs/component.ts +130 -0
  22. package/src/ecs/entity.ts +75 -0
  23. package/src/ecs/index.ts +9 -0
  24. package/src/ecs/intent.ts +162 -0
  25. package/src/ecs/metadata.ts +44 -0
  26. package/src/ecs/system.ts +224 -0
  27. package/src/ecs/world.ts +291 -0
  28. package/src/index.ts +19 -0
  29. package/src/interaction/focus.ts +327 -0
  30. package/src/interaction/index.ts +9 -0
  31. package/src/interaction/listener.ts +58 -0
  32. package/src/interaction/snapshot2d.ts +210 -0
  33. package/src/interaction/snapshot3d.ts +214 -0
  34. package/src/interaction/system.ts +544 -0
  35. package/src/interaction/types.ts +27 -0
  36. package/src/math/common.ts +67 -0
  37. package/src/math/index.ts +17 -0
  38. package/src/math/mat2d.ts +320 -0
  39. package/src/math/mat4.ts +576 -0
  40. package/src/math/quat.ts +438 -0
  41. package/src/math/vec2.ts +161 -0
  42. package/src/math/vec3.ts +242 -0
  43. package/src/math/vec4.ts +171 -0
  44. package/src/multiverse/coordinator.ts +540 -0
  45. package/src/multiverse/index.ts +13 -0
  46. package/src/multiverse/presentation-graph.ts +306 -0
  47. package/src/multiverse/presentation-links.ts +134 -0
  48. package/src/multiverse/realm.ts +49 -0
  49. package/src/platform/index.ts +2 -0
  50. package/src/platform/platform.ts +149 -0
  51. package/src/platform/window.ts +181 -0
  52. package/src/realm2d/components.ts +254 -0
  53. package/src/realm2d/index.ts +12 -0
  54. package/src/realm2d/presets/index.ts +36 -0
  55. package/src/realm2d/realm.ts +522 -0
  56. package/src/realm2d/sync.ts +284 -0
  57. package/src/realm2d/utils/index.ts +32 -0
  58. package/src/realm3d/components.ts +341 -0
  59. package/src/realm3d/geometry.ts +224 -0
  60. package/src/realm3d/index.ts +12 -0
  61. package/src/realm3d/presets/index.ts +67 -0
  62. package/src/realm3d/realm.ts +588 -0
  63. package/src/realm3d/resource-ids.ts +62 -0
  64. package/src/realm3d/skinning.ts +128 -0
  65. package/src/realm3d/sync.ts +442 -0
  66. package/src/realm3d/utils/index.ts +26 -0
  67. package/src/resources/index.ts +4 -0
  68. package/src/resources/manager.ts +92 -0
  69. package/src/runtime.ts +588 -0
  70. package/src/spatial/dirty_registry.ts +217 -0
  71. package/src/spatial/flags.ts +14 -0
  72. package/src/spatial/index.ts +6 -0
  73. package/src/spatial/metrics.ts +124 -0
  74. package/src/text/editor.ts +224 -0
  75. package/src/text/encoding.ts +38 -0
  76. package/src/text/index.ts +13 -0
  77. package/src/text/input.ts +160 -0
  78. package/src/text/keyboard.ts +50 -0
  79. package/src/text/svg-decorations.ts +147 -0
  80. package/src/types.ts +85 -0
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Entity management and ID allocation with policy enforcement.
3
+ */
4
+
5
+ import { assertValidLogicalId } from '@galfus/transport-types';
6
+
7
+ export type EntityId = number;
8
+
9
+ export class EntityManager {
10
+ private _nextId = 1;
11
+ private readonly _alive = new Set<EntityId>();
12
+ private readonly _freeList: EntityId[] = [];
13
+
14
+ /**
15
+ * Allocates a new unique EntityId, reusing recycled IDs before incrementing,
16
+ * and asserting validity against the core reserved range.
17
+ */
18
+ create(): EntityId {
19
+ let id: EntityId;
20
+ if (this._freeList.length > 0) {
21
+ id = this._freeList.pop()!;
22
+ } else {
23
+ id = this._nextId++;
24
+ assertValidLogicalId(id, 'EntityId');
25
+ }
26
+
27
+ this._alive.add(id);
28
+ return id;
29
+ }
30
+
31
+ /**
32
+ * Destroys an entity, marking its ID as free for recycling.
33
+ *
34
+ * @param id The EntityId to destroy.
35
+ * @returns True if the entity was alive and destroyed, false otherwise.
36
+ */
37
+ destroy(id: EntityId): boolean {
38
+ if (!this._alive.has(id)) {
39
+ return false;
40
+ }
41
+ this._alive.delete(id);
42
+ this._freeList.push(id);
43
+ return true;
44
+ }
45
+
46
+ /**
47
+ * Checks whether the specified EntityId is currently alive.
48
+ */
49
+ isAlive(id: EntityId): boolean {
50
+ return this._alive.has(id);
51
+ }
52
+
53
+ /**
54
+ * Returns the count of currently alive entities.
55
+ */
56
+ aliveCount(): number {
57
+ return this._alive.size;
58
+ }
59
+
60
+ /**
61
+ * Returns an array of all currently active entity IDs.
62
+ */
63
+ getActiveEntities(): EntityId[] {
64
+ return Array.from(this._alive);
65
+ }
66
+
67
+ /**
68
+ * Clears all entities and resets the allocation sequence.
69
+ */
70
+ clear(): void {
71
+ this._alive.clear();
72
+ this._freeList.length = 0;
73
+ this._nextId = 1;
74
+ }
75
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Generic ECS subsystem.
3
+ */
4
+ export * from './component';
5
+ export * from './entity';
6
+ export * from './intent';
7
+ export * from './metadata';
8
+ export * from './system';
9
+ export * from './world';
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Intent store for atomic and deferred ECS state mutations.
3
+ * Built on a strictly FIFO linked list with isolated error recovery and recursion bounds.
4
+ */
5
+
6
+ import type { Component } from './component';
7
+ import type { EntityId } from './entity';
8
+
9
+ export type Intent =
10
+ | { kind: 'attach'; entityId: EntityId; component: Component }
11
+ | { kind: 'detach'; entityId: EntityId; componentType: string }
12
+ | { kind: 'destroy'; entityId: EntityId }
13
+ | { kind: 'custom'; action: () => void };
14
+
15
+ export interface IntentFlusher {
16
+ attachComponent(entityId: EntityId, component: Component): void;
17
+ detachComponent(entityId: EntityId, componentType: string): void;
18
+ destroyEntity(entityId: EntityId): void;
19
+ }
20
+
21
+ interface IntentNode {
22
+ intent: Intent;
23
+ next: IntentNode | null;
24
+ }
25
+
26
+ export class IntentStore {
27
+ private _head: IntentNode | null = null;
28
+ private _tail: IntentNode | null = null;
29
+ private _size = 0;
30
+ private _lastErrors: Error[] = [];
31
+
32
+ /**
33
+ * Enqueues an intention to attach a component to an entity on the next flush.
34
+ */
35
+ queueAttach(entityId: EntityId, component: Component): void {
36
+ this._enqueue({ kind: 'attach', entityId, component });
37
+ }
38
+
39
+ /**
40
+ * Enqueues an intention to detach a component from an entity on the next flush.
41
+ */
42
+ queueDetach(entityId: EntityId, componentType: string): void {
43
+ this._enqueue({ kind: 'detach', entityId, componentType });
44
+ }
45
+
46
+ /**
47
+ * Enqueues an intention to destroy an entity on the next flush.
48
+ */
49
+ queueDestroy(entityId: EntityId): void {
50
+ this._enqueue({ kind: 'destroy', entityId });
51
+ }
52
+
53
+ /**
54
+ * Enqueues a custom arbitrary atomic mutation callback.
55
+ */
56
+ queueCustom(action: () => void): void {
57
+ this._enqueue({ kind: 'custom', action });
58
+ }
59
+
60
+ /**
61
+ * Returns the count of pending queued intents.
62
+ */
63
+ get pendingCount(): number {
64
+ return this._size;
65
+ }
66
+
67
+ /**
68
+ * Returns errors captured during the last flush execution.
69
+ */
70
+ get lastErrors(): readonly Error[] {
71
+ return this._lastErrors;
72
+ }
73
+
74
+ /**
75
+ * Clears recorded execution errors.
76
+ */
77
+ clearErrors(): void {
78
+ this._lastErrors = [];
79
+ }
80
+
81
+ /**
82
+ * Flushes all queued intents strictly in FIFO order.
83
+ * If intent executions trigger further intents, they are flushed up to `maxFlushDepth`.
84
+ * Each intent is executed in an isolated try/catch so a failure does not break subsequent intents.
85
+ *
86
+ * @param flusher Target receiver executing the atomic state operations.
87
+ * @param maxFlushDepth Maximum number of recursive flush cycles before aborting (default 100).
88
+ * @returns Array of errors that occurred during this flush.
89
+ */
90
+ flush(flusher: IntentFlusher, maxFlushDepth = 100): readonly Error[] {
91
+ if (this._head === null && this._lastErrors.length === 0) return this._lastErrors;
92
+ this._lastErrors = [];
93
+ let depth = 0;
94
+
95
+ while (this._head !== null) {
96
+ if (++depth > maxFlushDepth) {
97
+ const overflow = new Error(
98
+ `[IntentStore] Max flush depth (${maxFlushDepth}) exceeded; dropping pending intents to prevent infinite recursion.`
99
+ );
100
+ this._lastErrors.push(overflow);
101
+ console.error(overflow.message);
102
+ this.clear();
103
+ break;
104
+ }
105
+
106
+ // Detach current chain for processing this cycle
107
+ let current: IntentNode | null = this._head;
108
+ this._head = null;
109
+ this._tail = null;
110
+ this._size = 0;
111
+
112
+ while (current !== null) {
113
+ const node = current;
114
+ current = current.next;
115
+
116
+ try {
117
+ switch (node.intent.kind) {
118
+ case 'attach':
119
+ flusher.attachComponent(node.intent.entityId, node.intent.component);
120
+ break;
121
+ case 'detach':
122
+ flusher.detachComponent(node.intent.entityId, node.intent.componentType);
123
+ break;
124
+ case 'destroy':
125
+ flusher.destroyEntity(node.intent.entityId);
126
+ break;
127
+ case 'custom':
128
+ node.intent.action();
129
+ break;
130
+ }
131
+ } catch (err) {
132
+ const error = err instanceof Error ? err : new Error(String(err));
133
+ this._lastErrors.push(error);
134
+ console.error('[IntentStore] Error executing intent:', error);
135
+ }
136
+ }
137
+ }
138
+
139
+ return this._lastErrors;
140
+ }
141
+
142
+ /**
143
+ * Discards all pending queued intents without executing them.
144
+ */
145
+ clear(): void {
146
+ this._head = null;
147
+ this._tail = null;
148
+ this._size = 0;
149
+ }
150
+
151
+ private _enqueue(intent: Intent): void {
152
+ const node: IntentNode = { intent, next: null };
153
+ if (this._tail === null) {
154
+ this._head = node;
155
+ this._tail = node;
156
+ } else {
157
+ this._tail.next = node;
158
+ this._tail = node;
159
+ }
160
+ this._size++;
161
+ }
162
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Name and Metadata components for ECS entities.
3
+ */
4
+
5
+ import type { Component } from './component';
6
+
7
+ export interface NameComponent extends Component {
8
+ readonly type: 'Name';
9
+ name: string;
10
+ }
11
+
12
+ export function createName(name: string): NameComponent {
13
+ return {
14
+ type: 'Name',
15
+ name
16
+ };
17
+ }
18
+
19
+ export interface MetadataComponent extends Component {
20
+ readonly type: 'Metadata';
21
+ readonly entries: Map<string, unknown>;
22
+ }
23
+
24
+ export function createMetadata(
25
+ initial?: Record<string, unknown> | Map<string, unknown>
26
+ ): MetadataComponent {
27
+ const entries = new Map<string, unknown>();
28
+ if (initial) {
29
+ if (initial instanceof Map) {
30
+ for (const [key, value] of initial.entries()) {
31
+ entries.set(key, value);
32
+ }
33
+ } else {
34
+ for (const [key, value] of Object.entries(initial)) {
35
+ entries.set(key, value);
36
+ }
37
+ }
38
+ }
39
+
40
+ return {
41
+ type: 'Metadata',
42
+ entries
43
+ };
44
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Execution pipeline and 4-phase system scheduler.
3
+ */
4
+
5
+ import type { GalfusEngine } from '@galfus/transport-types';
6
+
7
+ /**
8
+ * Standard execution stages executed in fixed sequence each frame:
9
+ * 1. `input`: Processes incoming platform events, gamepads, pointers, keyboards.
10
+ * 2. `update`: Main game logic, physics integration, AI behavior.
11
+ * 3. `preRender`: Anchor solving, spatial hierarchies, camera sync, scene graph updates.
12
+ * 4. `postRender`: Frame metrics, debug diagnostics, cleanups (executed after dispatch).
13
+ */
14
+ export type SystemStep = 'input' | 'update' | 'preRender' | 'postRender';
15
+
16
+ export const SYSTEM_STEPS: readonly SystemStep[] = [
17
+ 'input',
18
+ 'update',
19
+ 'preRender',
20
+ 'postRender'
21
+ ] as const;
22
+
23
+ /**
24
+ * Functional classification of a registered system.
25
+ */
26
+ export type SystemKind = 'ordinary' | 'anchor' | 'sync' | 'cleanup' | 'custom';
27
+
28
+ /**
29
+ * Reserved internal system execution priorities.
30
+ * User-registered ordinary systems are clamped to [MIN_USER_PRIORITY, MAX_USER_PRIORITY]
31
+ * to ensure that internal stages (anchor solving, skinning, realm sync) execute in strict order.
32
+ */
33
+ export const RESERVED_PRIORITY_ANCHOR_RESOLVE = -1_000_000;
34
+ export const RESERVED_PRIORITY_INTERNAL_RESOLVERS = -2_000_000;
35
+ export const RESERVED_PRIORITY_REALM_SYNC = -3_000_000;
36
+ export const MIN_USER_PRIORITY = -999_999;
37
+ export const MAX_USER_PRIORITY = 999_999;
38
+
39
+ /**
40
+ * Execution context passed to every system during frame execution.
41
+ */
42
+ export interface SystemContext {
43
+ /** Total elapsed simulation time in seconds. */
44
+ time: number;
45
+ /** Delta time of the current frame in seconds (`deltaMs / 1000`). */
46
+ dt: number;
47
+ /** Sequential frame identifier. */
48
+ frameId: number;
49
+ /** Direct access to the underlying native or WASM GalfusEngine ABI. */
50
+ engine?: GalfusEngine;
51
+ }
52
+
53
+ /**
54
+ * Pure function signature of an ECS system.
55
+ */
56
+ export type System<TWorld = unknown> = (world: TWorld, context: SystemContext) => void;
57
+
58
+ /**
59
+ * Options or configuration descriptor for registering an ECS system.
60
+ */
61
+ export interface SystemDescriptor<TWorld = unknown> {
62
+ step: SystemStep;
63
+ fn: System<TWorld>;
64
+ /**
65
+ * Execution priority within the stage.
66
+ * Higher priority systems execute earlier. Ties are resolved by stable insertion order.
67
+ * Defaults to 0.
68
+ */
69
+ priority?: number;
70
+ /** Functional category of the system. Defaults to 'ordinary'. */
71
+ kind?: SystemKind;
72
+ }
73
+
74
+ /**
75
+ * An immutable registration entry in the system scheduler.
76
+ */
77
+ export interface RegisteredSystem<TWorld = unknown> {
78
+ step: SystemStep;
79
+ fn: System<TWorld>;
80
+ priority: number;
81
+ insertionOrder: number;
82
+ kind: SystemKind;
83
+ }
84
+
85
+ /**
86
+ * Manages systems registered across the 4 stages with stable priority and insertion ordering.
87
+ */
88
+ export class SystemPipeline<TWorld = unknown> {
89
+ private readonly _systems: Record<SystemStep, RegisteredSystem<TWorld>[]> = {
90
+ input: [],
91
+ update: [],
92
+ preRender: [],
93
+ postRender: []
94
+ };
95
+ private _insertionCounter = 0;
96
+
97
+ /**
98
+ * Registers a system into a specified execution stage with stable ordering.
99
+ *
100
+ * @param stepOrDesc Stage name or full {@link SystemDescriptor}.
101
+ * @param systemFn The system function when passing step as string.
102
+ * @param options Optional priority and kind configuration.
103
+ */
104
+ add(
105
+ stepOrDesc: SystemStep | SystemDescriptor<TWorld>,
106
+ systemFn?: System<TWorld>,
107
+ options?: { priority?: number; kind?: SystemKind }
108
+ ): this {
109
+ let desc: SystemDescriptor<TWorld>;
110
+ if (typeof stepOrDesc === 'string') {
111
+ if (!systemFn) {
112
+ throw new Error('System function must be provided when step is passed as a string');
113
+ }
114
+ desc = {
115
+ step: stepOrDesc,
116
+ fn: systemFn,
117
+ priority: options?.priority,
118
+ kind: options?.kind ?? 'ordinary'
119
+ };
120
+ } else {
121
+ desc = stepOrDesc;
122
+ }
123
+
124
+ const kind = desc.kind ?? 'ordinary';
125
+ let priority: number;
126
+ if (kind === 'ordinary' || kind === 'custom') {
127
+ priority = Math.max(MIN_USER_PRIORITY, Math.min(MAX_USER_PRIORITY, desc.priority ?? 0));
128
+ } else if (desc.priority !== undefined) {
129
+ priority = desc.priority;
130
+ } else if (kind === 'anchor') {
131
+ priority = RESERVED_PRIORITY_ANCHOR_RESOLVE;
132
+ } else if (kind === 'sync') {
133
+ priority = RESERVED_PRIORITY_REALM_SYNC;
134
+ } else {
135
+ priority = RESERVED_PRIORITY_INTERNAL_RESOLVERS;
136
+ }
137
+
138
+ const entry: RegisteredSystem<TWorld> = {
139
+ step: desc.step,
140
+ fn: desc.fn,
141
+ priority,
142
+ insertionOrder: ++this._insertionCounter,
143
+ kind
144
+ };
145
+
146
+ const list = [...this._systems[desc.step]];
147
+ list.push(entry);
148
+
149
+ // Stably sort: higher priority first; equal priorities preserve ascending insertionOrder
150
+ list.sort((a, b) => {
151
+ if (b.priority !== a.priority) {
152
+ return b.priority - a.priority;
153
+ }
154
+ return a.insertionOrder - b.insertionOrder;
155
+ });
156
+
157
+ this._systems[desc.step] = list;
158
+ return this;
159
+ }
160
+
161
+ /**
162
+ * Removes a previously registered system from a stage.
163
+ */
164
+ remove(step: SystemStep, system: System<TWorld>): boolean {
165
+ const list = this._systems[step];
166
+ const index = list.findIndex((s) => s.fn === system);
167
+ if (index !== -1) {
168
+ this._systems[step] = [...list.slice(0, index), ...list.slice(index + 1)];
169
+ return true;
170
+ }
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Runs all systems registered in a specific stage in sorted order.
176
+ * Safe against removal during iteration and isolates individual system exceptions.
177
+ */
178
+ runStep(step: SystemStep, world: TWorld, context: SystemContext): void {
179
+ const list = this._systems[step];
180
+ for (const entry of list) {
181
+ try {
182
+ entry.fn(world, context);
183
+ } catch (err) {
184
+ console.error(`[SystemPipeline] Error executing system in stage '${step}':`, err);
185
+ }
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Runs all 4 stages in canonical sequence: input -> update -> preRender -> postRender.
191
+ */
192
+ runAll(world: TWorld, context: SystemContext): void {
193
+ this.runStep('input', world, context);
194
+ this.runStep('update', world, context);
195
+ this.runStep('preRender', world, context);
196
+ this.runStep('postRender', world, context);
197
+ }
198
+
199
+ /**
200
+ * Returns a copy of registered systems in the given stage or all stages.
201
+ */
202
+ getRegistered(step?: SystemStep): RegisteredSystem<TWorld>[] {
203
+ if (step) {
204
+ return [...this._systems[step]];
205
+ }
206
+ return [
207
+ ...this._systems.input,
208
+ ...this._systems.update,
209
+ ...this._systems.preRender,
210
+ ...this._systems.postRender
211
+ ];
212
+ }
213
+
214
+ /**
215
+ * Clears all registered systems.
216
+ */
217
+ clear(): void {
218
+ this._systems.input = [];
219
+ this._systems.update = [];
220
+ this._systems.preRender = [];
221
+ this._systems.postRender = [];
222
+ this._insertionCounter = 0;
223
+ }
224
+ }