@scenoco-three/core 0.1.0

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/dist/components/Follow.d.ts +15 -0
  4. package/dist/components/Follow.js +41 -0
  5. package/dist/components/LookAt.d.ts +11 -0
  6. package/dist/components/LookAt.js +30 -0
  7. package/dist/components/Oscillator.d.ts +13 -0
  8. package/dist/components/Oscillator.js +42 -0
  9. package/dist/components/Rotator.d.ts +5 -0
  10. package/dist/components/Rotator.js +21 -0
  11. package/dist/core/Component.d.ts +60 -0
  12. package/dist/core/Component.js +63 -0
  13. package/dist/core/Engine.d.ts +169 -0
  14. package/dist/core/Engine.js +468 -0
  15. package/dist/core/NodeRegistry.d.ts +99 -0
  16. package/dist/core/NodeRegistry.js +87 -0
  17. package/dist/core/Registry.d.ts +55 -0
  18. package/dist/core/Registry.js +28 -0
  19. package/dist/core/System.d.ts +33 -0
  20. package/dist/core/System.js +23 -0
  21. package/dist/core/decorators.d.ts +50 -0
  22. package/dist/core/decorators.js +142 -0
  23. package/dist/dsl/PrefabRef.d.ts +36 -0
  24. package/dist/dsl/PrefabRef.js +33 -0
  25. package/dist/dsl/bundle-format.d.ts +74 -0
  26. package/dist/dsl/bundle-format.js +41 -0
  27. package/dist/dsl/bundle-shared.d.ts +18 -0
  28. package/dist/dsl/bundle-shared.js +33 -0
  29. package/dist/dsl/compiled.d.ts +64 -0
  30. package/dist/dsl/compiled.js +1 -0
  31. package/dist/dsl/deserialize.d.ts +9 -0
  32. package/dist/dsl/deserialize.js +125 -0
  33. package/dist/dsl/instantiate.d.ts +40 -0
  34. package/dist/dsl/instantiate.js +111 -0
  35. package/dist/dsl/nodes.d.ts +1 -0
  36. package/dist/dsl/nodes.js +10 -0
  37. package/dist/dsl/types.d.ts +45 -0
  38. package/dist/dsl/types.js +120 -0
  39. package/dist/index.d.ts +13 -0
  40. package/dist/index.js +17 -0
  41. package/dist/internal.d.ts +7 -0
  42. package/dist/internal.js +7 -0
  43. package/dist/nodes/base.d.ts +31 -0
  44. package/dist/nodes/base.js +27 -0
  45. package/dist/nodes/geometry/BoxGeometry.d.ts +13 -0
  46. package/dist/nodes/geometry/BoxGeometry.js +44 -0
  47. package/dist/nodes/geometry/CylinderGeometry.d.ts +11 -0
  48. package/dist/nodes/geometry/CylinderGeometry.js +36 -0
  49. package/dist/nodes/geometry/PlaneGeometry.d.ts +11 -0
  50. package/dist/nodes/geometry/PlaneGeometry.js +36 -0
  51. package/dist/nodes/geometry/SphereGeometry.d.ts +10 -0
  52. package/dist/nodes/geometry/SphereGeometry.js +32 -0
  53. package/dist/nodes/geometry/TorusGeometry.d.ts +11 -0
  54. package/dist/nodes/geometry/TorusGeometry.js +36 -0
  55. package/dist/nodes/index.d.ts +36 -0
  56. package/dist/nodes/index.js +46 -0
  57. package/dist/nodes/manifest.d.ts +18 -0
  58. package/dist/nodes/manifest.js +64 -0
  59. package/dist/nodes/material/LineBasicMaterial.d.ts +11 -0
  60. package/dist/nodes/material/LineBasicMaterial.js +41 -0
  61. package/dist/nodes/material/MeshBasicMaterial.d.ts +11 -0
  62. package/dist/nodes/material/MeshBasicMaterial.js +41 -0
  63. package/dist/nodes/material/MeshStandardMaterial.d.ts +14 -0
  64. package/dist/nodes/material/MeshStandardMaterial.js +56 -0
  65. package/dist/nodes/material/PointsMaterial.d.ts +12 -0
  66. package/dist/nodes/material/PointsMaterial.js +46 -0
  67. package/dist/nodes/material/SpriteMaterial.d.ts +11 -0
  68. package/dist/nodes/material/SpriteMaterial.js +41 -0
  69. package/dist/nodes/object3d/AmbientLight.d.ts +7 -0
  70. package/dist/nodes/object3d/AmbientLight.js +26 -0
  71. package/dist/nodes/object3d/ArrayCamera.d.ts +5 -0
  72. package/dist/nodes/object3d/ArrayCamera.js +20 -0
  73. package/dist/nodes/object3d/BatchedMesh.d.ts +7 -0
  74. package/dist/nodes/object3d/BatchedMesh.js +31 -0
  75. package/dist/nodes/object3d/Bone.d.ts +5 -0
  76. package/dist/nodes/object3d/Bone.js +18 -0
  77. package/dist/nodes/object3d/DirectionalLight.d.ts +7 -0
  78. package/dist/nodes/object3d/DirectionalLight.js +29 -0
  79. package/dist/nodes/object3d/Group.d.ts +5 -0
  80. package/dist/nodes/object3d/Group.js +18 -0
  81. package/dist/nodes/object3d/HemisphereLight.d.ts +8 -0
  82. package/dist/nodes/object3d/HemisphereLight.js +30 -0
  83. package/dist/nodes/object3d/InstancedMesh.d.ts +6 -0
  84. package/dist/nodes/object3d/InstancedMesh.js +28 -0
  85. package/dist/nodes/object3d/LOD.d.ts +5 -0
  86. package/dist/nodes/object3d/LOD.js +20 -0
  87. package/dist/nodes/object3d/Line.d.ts +5 -0
  88. package/dist/nodes/object3d/Line.js +21 -0
  89. package/dist/nodes/object3d/LineLoop.d.ts +5 -0
  90. package/dist/nodes/object3d/LineLoop.js +21 -0
  91. package/dist/nodes/object3d/LineSegments.d.ts +5 -0
  92. package/dist/nodes/object3d/LineSegments.js +21 -0
  93. package/dist/nodes/object3d/Mesh.d.ts +5 -0
  94. package/dist/nodes/object3d/Mesh.js +21 -0
  95. package/dist/nodes/object3d/Object3D.d.ts +5 -0
  96. package/dist/nodes/object3d/Object3D.js +18 -0
  97. package/dist/nodes/object3d/OrthographicCamera.d.ts +11 -0
  98. package/dist/nodes/object3d/OrthographicCamera.js +42 -0
  99. package/dist/nodes/object3d/PerspectiveCamera.d.ts +8 -0
  100. package/dist/nodes/object3d/PerspectiveCamera.js +31 -0
  101. package/dist/nodes/object3d/PointLight.d.ts +9 -0
  102. package/dist/nodes/object3d/PointLight.js +34 -0
  103. package/dist/nodes/object3d/Points.d.ts +5 -0
  104. package/dist/nodes/object3d/Points.js +21 -0
  105. package/dist/nodes/object3d/RectAreaLight.d.ts +9 -0
  106. package/dist/nodes/object3d/RectAreaLight.js +34 -0
  107. package/dist/nodes/object3d/Scene.d.ts +5 -0
  108. package/dist/nodes/object3d/Scene.js +22 -0
  109. package/dist/nodes/object3d/SkinnedMesh.d.ts +5 -0
  110. package/dist/nodes/object3d/SkinnedMesh.js +23 -0
  111. package/dist/nodes/object3d/SpotLight.d.ts +11 -0
  112. package/dist/nodes/object3d/SpotLight.js +42 -0
  113. package/dist/nodes/object3d/Sprite.d.ts +5 -0
  114. package/dist/nodes/object3d/Sprite.js +21 -0
  115. package/dist/nodes/setting/Background.d.ts +6 -0
  116. package/dist/nodes/setting/Background.js +22 -0
  117. package/dist/nodes/setting/Fog.d.ts +8 -0
  118. package/dist/nodes/setting/Fog.js +30 -0
  119. package/dist/nodes/setting/FogExp2.d.ts +7 -0
  120. package/dist/nodes/setting/FogExp2.js +27 -0
  121. package/dist/nodes/targets.d.ts +5 -0
  122. package/dist/nodes/targets.js +5 -0
  123. package/package.json +42 -0
@@ -0,0 +1,468 @@
1
+ import { PerspectiveCamera, Scene, Vector2, WebGLRenderer } from 'three';
2
+ import { Registry } from './Registry.js';
3
+ import { bundleKind } from '../dsl/bundle-format.js';
4
+ import { deserializeBundle } from '../dsl/deserialize.js';
5
+ import { allSystemDefs } from '../dsl/nodes.js';
6
+ import { instantiateScene } from '../dsl/instantiate.js';
7
+ /**
8
+ * Core runtime: owns the Three.js scene graph, the render loop, and the
9
+ * lifecycles of all attached components.
10
+ *
11
+ * Without a canvas the engine runs headless — `tick(dt)` advances the
12
+ * simulation manually, which is how tests and CLI tooling drive it.
13
+ */
14
+ export class Engine {
15
+ scene = new Scene();
16
+ /** The engine's built-in fallback camera, used until a scene supplies one. */
17
+ camera;
18
+ renderer;
19
+ /** Camera the renderer currently draws through (defaults to `camera`). */
20
+ activeCamera;
21
+ components = [];
22
+ byNode = new Map();
23
+ byId = new Map();
24
+ systems = [];
25
+ /** Each running system → the component type its `components` query is kept in sync with. */
26
+ systemQueries = new Map();
27
+ rendererSize = new Vector2();
28
+ /** Seconds per fixed step; accumulator carries leftover frame time. */
29
+ fixedTimeStep;
30
+ fixedAccumulator = 0;
31
+ rafId;
32
+ lastFrameTime;
33
+ /** Root object of the currently loaded scene (from loadScene), if any. */
34
+ sceneRoot;
35
+ /** Components owned by the loaded scene — the only ones disposeScene removes. */
36
+ sceneComponents = [];
37
+ /** Systems auto-run for the loaded scene — removed on unload. */
38
+ sceneSystems = [];
39
+ /** Live attachment instances of the loaded scene (retained for findAttachment). */
40
+ sceneAttachments = [];
41
+ /** Live spawned subtrees (from instantiate), so dispose() can clean them up. */
42
+ instances = new Set();
43
+ constructor(options = {}) {
44
+ this.fixedTimeStep = options.fixedTimeStep ?? 1 / 60;
45
+ this.camera = new PerspectiveCamera(60, 1, 0.1, 1000);
46
+ this.camera.position.set(0, 2, 6);
47
+ this.camera.lookAt(0, 0, 0);
48
+ this.activeCamera = this.camera;
49
+ if (options.canvas) {
50
+ this.renderer = new WebGLRenderer({ canvas: options.canvas, antialias: true });
51
+ this.renderer.setPixelRatio(Math.min(globalThis.devicePixelRatio ?? 1, 2));
52
+ if (options.autoStart !== false)
53
+ this.start();
54
+ }
55
+ }
56
+ addComponent(node, type, props) {
57
+ let ctor;
58
+ if (typeof type === 'string') {
59
+ const entry = Registry.getComponent(type);
60
+ if (!entry) {
61
+ const known = Registry.componentNames().join(', ') || '(none)';
62
+ throw new Error(`Unknown component "${type}". Registered components: ${known}`);
63
+ }
64
+ ctor = entry.ctor;
65
+ }
66
+ else {
67
+ ctor = type;
68
+ }
69
+ const instance = new ctor();
70
+ instance.node = node;
71
+ if (props)
72
+ Object.assign(instance, props);
73
+ this.registerComponent(node, instance);
74
+ this.initComponent(instance);
75
+ return instance;
76
+ }
77
+ /** Bookkeeping only: track the instance without running any lifecycle hook. */
78
+ registerComponent(node, instance) {
79
+ instance.engine = this;
80
+ this.components.push(instance);
81
+ let list = this.byNode.get(node);
82
+ if (!list)
83
+ this.byNode.set(node, (list = []));
84
+ list.push(instance);
85
+ // Index by id for findComponent. First registration wins on a duplicate
86
+ // (dynamically spawned prefab instances may reuse internal ids).
87
+ if (instance.id !== undefined && !this.byId.has(instance.id))
88
+ this.byId.set(instance.id, instance);
89
+ // Keep each system's live query in sync.
90
+ for (const [system, type] of this.systemQueries) {
91
+ if (instance instanceof type)
92
+ system.components.push(instance);
93
+ }
94
+ }
95
+ /** Run onLoad + onEnable for an already-registered instance. */
96
+ initComponent(instance) {
97
+ instance.onLoad?.();
98
+ instance._loaded = true;
99
+ if (instance.enabled)
100
+ instance.onEnable?.();
101
+ }
102
+ /** Detach a component, running onDisable (if enabled) and onDestroy. */
103
+ removeComponent(instance) {
104
+ const index = this.components.indexOf(instance);
105
+ if (index === -1)
106
+ return;
107
+ this.components.splice(index, 1);
108
+ const list = this.byNode.get(instance.node);
109
+ if (list) {
110
+ const i = list.indexOf(instance);
111
+ if (i !== -1)
112
+ list.splice(i, 1);
113
+ if (list.length === 0)
114
+ this.byNode.delete(instance.node);
115
+ }
116
+ if (instance.id !== undefined && this.byId.get(instance.id) === instance)
117
+ this.byId.delete(instance.id);
118
+ // Drop it from any system query it belonged to.
119
+ for (const [system, type] of this.systemQueries) {
120
+ if (!(instance instanceof type))
121
+ continue;
122
+ const q = system.components;
123
+ const qi = q.indexOf(instance);
124
+ if (qi !== -1)
125
+ q.splice(qi, 1);
126
+ }
127
+ if (instance.enabled && instance._loaded)
128
+ instance.onDisable?.();
129
+ instance.onDestroy?.();
130
+ }
131
+ getComponents(node) {
132
+ return this.byNode.get(node) ?? [];
133
+ }
134
+ /**
135
+ * Register an engine subsystem instance. Injects `system.engine`, seeds its
136
+ * `components` query (from its `@system(Type)` registration), and runs
137
+ * `onAttach`. Usually the engine does this for you (a registered `@system`
138
+ * auto-runs when its component type appears); call it directly for manual use.
139
+ */
140
+ addSystem(system) {
141
+ system.engine = this;
142
+ const type = allSystemDefs().find((d) => system instanceof d.ctor)?.componentType;
143
+ if (type) {
144
+ this.systemQueries.set(system, type);
145
+ system.components.push(...this.components.filter((c) => c instanceof type));
146
+ }
147
+ this.systems.push(system);
148
+ system.onAttach?.();
149
+ return system;
150
+ }
151
+ /** The first running system assignable to `type` (Unity GetComponent style). */
152
+ getSystem(type) {
153
+ for (const s of this.systems)
154
+ if (s instanceof type)
155
+ return s;
156
+ return undefined;
157
+ }
158
+ /** Remove a subsystem, running its `onDetach`. */
159
+ removeSystem(system) {
160
+ const i = this.systems.indexOf(system);
161
+ if (i === -1)
162
+ return;
163
+ this.systems.splice(i, 1);
164
+ this.systemQueries.delete(system);
165
+ system.onDetach?.();
166
+ }
167
+ /**
168
+ * The live instance of an attachment type in the loaded scene (e.g. a system
169
+ * reads `engine.findAttachment(Physics)?.gravity`). Singleton config attachments
170
+ * are the typical use.
171
+ */
172
+ findAttachment(type) {
173
+ for (const a of this.sceneAttachments)
174
+ if (a instanceof type)
175
+ return a;
176
+ return undefined;
177
+ }
178
+ /**
179
+ * Find an Object3D by its scene `id` (e.g. `findObject('Turbine_01')`, or a
180
+ * scoped prefab-instance internal `findObject('t1/blade')`). Searches the whole
181
+ * loaded scene and any spawned instances; returns the first match.
182
+ */
183
+ findObject(id) {
184
+ return this.scene.getObjectByName(id);
185
+ }
186
+ /**
187
+ * Find a component by id — mirroring the `#id` references used in XML. Resolves
188
+ * a component's own id first; failing that, treats `id` as a node id and returns
189
+ * the (first) component on that node, optionally narrowed to `type`.
190
+ *
191
+ * `type` may be a class reference OR a registered component name string (e.g.
192
+ * `'GameManager'`). The string form avoids the `import type` footgun: a type-only
193
+ * import is erased at runtime, making the class reference undefined.
194
+ */
195
+ findComponent(id, type) {
196
+ const ctor = typeof type === 'string'
197
+ ? Registry.getComponent(type)?.ctor
198
+ : type;
199
+ const direct = this.byId.get(id);
200
+ if (direct && (!ctor || direct instanceof ctor))
201
+ return direct;
202
+ const node = this.scene.getObjectByName(id);
203
+ if (node) {
204
+ for (const c of this.getComponents(node))
205
+ if (!ctor || c instanceof ctor)
206
+ return c;
207
+ }
208
+ return undefined;
209
+ }
210
+ /** The camera the renderer draws through (a scene camera if one is active). */
211
+ get renderCamera() {
212
+ return this.activeCamera;
213
+ }
214
+ /**
215
+ * Render through a specific camera — e.g. one defined in the scene
216
+ * (`engine.setActiveCamera(engine.findObject('Main')!)`). `loadScene` already
217
+ * auto-selects the first camera it finds; this is for switching afterwards.
218
+ * Pass nothing to revert to the engine's built-in camera.
219
+ */
220
+ setActiveCamera(camera = this.camera) {
221
+ this.activeCamera = camera;
222
+ }
223
+ /**
224
+ * Load a scene from a pre-built bundle (or a CompiledScene), replacing any
225
+ * current one. Bundles are produced at build time by `scenoco/compiler`; the
226
+ * runtime never parses or validates XML. All components are instantiated and
227
+ * their references wired before any onLoad runs, so cross-component refs are
228
+ * valid from the first lifecycle hook.
229
+ *
230
+ * If the scene defines a camera, it becomes the active render camera; the
231
+ * engine's built-in camera is the fallback when it doesn't.
232
+ */
233
+ loadScene(source) {
234
+ if (Array.isArray(source) && bundleKind(source) === 'prefab') {
235
+ throw new Error('loadScene() received a prefab bundle — use engine.instantiate() to spawn a prefab.');
236
+ }
237
+ const compiled = Array.isArray(source) ? deserializeBundle(source) : source;
238
+ this.disposeScene();
239
+ // Attachments (geometry/material on nodes, fog/background on the scene) are
240
+ // applied during instantiation against the live scene/renderer.
241
+ const live = instantiateScene(compiled, this.instantiateContext());
242
+ this.sceneRoot = live.root;
243
+ this.sceneAttachments = live.attachments;
244
+ this.scene.add(live.root);
245
+ this.sceneComponents = this.initLive(live);
246
+ // Auto-run registered systems whose component type the scene contains.
247
+ this.sceneSystems = this.runSceneSystems();
248
+ const sceneCamera = findCamera(live.root);
249
+ if (sceneCamera)
250
+ this.setActiveCamera(sceneCamera);
251
+ }
252
+ /** Instantiate + run each registered @system whose component type is present. */
253
+ runSceneSystems() {
254
+ const started = [];
255
+ for (const def of allSystemDefs()) {
256
+ const alreadyRunning = this.systems.some((s) => s instanceof def.ctor);
257
+ const present = this.components.some((c) => c instanceof def.componentType);
258
+ if (alreadyRunning || !present)
259
+ continue;
260
+ started.push(this.addSystem(new def.ctor()));
261
+ }
262
+ return started;
263
+ }
264
+ /**
265
+ * Spawn a prefab (or any bundle/CompiledScene) as a subtree under `parent`
266
+ * (the scene root by default), at runtime — Unity/Cocos `Instantiate`.
267
+ * Returns a handle whose `destroy()` removes just this subtree; the loaded
268
+ * scene and other instances are untouched. References inside the prefab are
269
+ * already wired by index, so spawning the same prefab many times is safe.
270
+ */
271
+ instantiate(source, parent = this.scene) {
272
+ const compiled = Array.isArray(source) ? deserializeBundle(source) : source;
273
+ const live = instantiateScene(compiled, this.instantiateContext());
274
+ parent.add(live.root);
275
+ const components = this.initLive(live);
276
+ const instance = {
277
+ root: live.root,
278
+ destroy: () => {
279
+ if (!this.instances.delete(instance))
280
+ return;
281
+ for (const c of components)
282
+ this.removeComponent(c);
283
+ live.root.removeFromParent();
284
+ disposeObject(live.root);
285
+ },
286
+ };
287
+ this.instances.add(instance);
288
+ return instance;
289
+ }
290
+ /**
291
+ * Context for `instantiateScene`: the live scene/renderer (for attachments)
292
+ * and how a `prefab` prop spawns — `engine.instantiate`, so spawned subtrees
293
+ * are tracked and torn down by `dispose()`.
294
+ */
295
+ instantiateContext() {
296
+ return {
297
+ scene: this.scene,
298
+ ...(this.renderer ? { renderer: this.renderer } : {}),
299
+ spawn: (compiled, parent) => this.instantiate(compiled, parent),
300
+ };
301
+ }
302
+ /** Register + run onLoad/onEnable for a freshly instantiated subtree. */
303
+ initLive(live) {
304
+ const components = live.components.map(({ instance }) => instance);
305
+ for (const { instance, node } of live.components)
306
+ this.registerComponent(node, instance);
307
+ for (const { instance } of live.components)
308
+ this.initComponent(instance);
309
+ return components;
310
+ }
311
+ /**
312
+ * Remove the loaded scene: destroy its components (only those owned by the
313
+ * scene — manually attached ones and spawned instances survive) and release
314
+ * its GPU resources.
315
+ */
316
+ disposeScene() {
317
+ if (!this.sceneRoot)
318
+ return;
319
+ // Components first (a RigidBody.onDisable removes its body), then the
320
+ // subsystems they used (PhysicsSystem.onDetach frees the world).
321
+ for (const c of this.sceneComponents)
322
+ this.removeComponent(c);
323
+ this.sceneComponents = [];
324
+ for (const s of this.sceneSystems)
325
+ this.removeSystem(s);
326
+ this.sceneSystems = [];
327
+ this.sceneAttachments = [];
328
+ this.scene.remove(this.sceneRoot);
329
+ disposeObject(this.sceneRoot);
330
+ this.sceneRoot = undefined;
331
+ // Reset the built-in scene settings so they don't leak into the next scene.
332
+ this.scene.fog = null;
333
+ this.scene.background = null;
334
+ // The scene's camera is gone; fall back to the built-in one.
335
+ this.activeCamera = this.camera;
336
+ }
337
+ /**
338
+ * Advance the simulation by dt seconds, then render if a renderer exists.
339
+ * Phases, in order: fixed steps (`fixedUpdate` + subsystem `fixedStep`,
340
+ * repeated to drain the accumulator) → `start`/`update` → `lateUpdate` →
341
+ * subsystem `step` → render. Variable `update` still runs exactly once per
342
+ * tick, so existing components are unaffected.
343
+ */
344
+ tick(dt) {
345
+ // Fixed-timestep phase (deterministic; physics + fixedUpdate). The substep
346
+ // cap prevents a "spiral of death" after a long pause.
347
+ this.fixedAccumulator += dt;
348
+ let steps = 0;
349
+ while (this.fixedAccumulator >= this.fixedTimeStep && steps < MAX_FIXED_SUBSTEPS) {
350
+ this.fixedStep(this.fixedTimeStep);
351
+ this.fixedAccumulator -= this.fixedTimeStep;
352
+ steps++;
353
+ }
354
+ if (steps === MAX_FIXED_SUBSTEPS)
355
+ this.fixedAccumulator = 0; // drop the backlog
356
+ // Variable-rate phase: start (once) then update.
357
+ for (let i = 0; i < this.components.length; i++) {
358
+ const c = this.components[i];
359
+ if (!c.enabled || !c._loaded)
360
+ continue;
361
+ if (!c._started) {
362
+ c._started = true;
363
+ c.start?.();
364
+ }
365
+ c.update?.(dt);
366
+ }
367
+ // Late phase: after every update this frame.
368
+ for (let i = 0; i < this.components.length; i++) {
369
+ const c = this.components[i];
370
+ if (c.enabled && c._loaded)
371
+ c.lateUpdate?.(dt);
372
+ }
373
+ for (const s of this.systems)
374
+ s.step?.(dt);
375
+ if (this.renderer) {
376
+ this.resizeToCanvas();
377
+ this.renderer.render(this.scene, this.activeCamera);
378
+ }
379
+ }
380
+ /** One fixed step: components' fixedUpdate (apply forces/read), then subsystems step. */
381
+ fixedStep(fixedDt) {
382
+ for (let i = 0; i < this.components.length; i++) {
383
+ const c = this.components[i];
384
+ if (c.enabled && c._loaded)
385
+ c.fixedUpdate?.(fixedDt);
386
+ }
387
+ for (const s of this.systems)
388
+ s.fixedStep?.(fixedDt);
389
+ }
390
+ /** Start the requestAnimationFrame loop (no-op when already running or headless). */
391
+ start() {
392
+ if (this.rafId !== undefined || !this.renderer)
393
+ return;
394
+ this.lastFrameTime = undefined;
395
+ const loop = (time) => {
396
+ this.rafId = requestAnimationFrame(loop);
397
+ const dt = this.lastFrameTime === undefined ? 0 : (time - this.lastFrameTime) / 1000;
398
+ this.lastFrameTime = time;
399
+ // Clamp dt so background-tab pauses don't produce giant simulation steps.
400
+ this.tick(Math.min(dt, 0.1));
401
+ };
402
+ this.rafId = requestAnimationFrame(loop);
403
+ }
404
+ stop() {
405
+ if (this.rafId !== undefined) {
406
+ cancelAnimationFrame(this.rafId);
407
+ this.rafId = undefined;
408
+ }
409
+ }
410
+ /** Stop the loop, destroy all components/instances/subsystems, free GPU resources. */
411
+ dispose() {
412
+ this.stop();
413
+ for (const instance of [...this.instances])
414
+ instance.destroy();
415
+ this.disposeScene();
416
+ for (const c of [...this.components])
417
+ this.removeComponent(c);
418
+ for (const s of [...this.systems])
419
+ this.removeSystem(s);
420
+ this.renderer?.dispose();
421
+ }
422
+ resizeToCanvas() {
423
+ const renderer = this.renderer;
424
+ const canvas = renderer.domElement;
425
+ const width = canvas.clientWidth;
426
+ const height = canvas.clientHeight;
427
+ if (width === 0 || height === 0)
428
+ return;
429
+ renderer.getSize(this.rendererSize);
430
+ if (this.rendererSize.x !== width || this.rendererSize.y !== height) {
431
+ renderer.setSize(width, height, false);
432
+ // Keep the built-in camera's aspect current; update the active scene
433
+ // camera too when it's a perspective camera.
434
+ const aspect = width / height;
435
+ this.camera.aspect = aspect;
436
+ this.camera.updateProjectionMatrix();
437
+ const active = this.activeCamera;
438
+ if (active !== this.camera && active instanceof PerspectiveCamera) {
439
+ active.aspect = aspect;
440
+ active.updateProjectionMatrix();
441
+ }
442
+ }
443
+ }
444
+ }
445
+ /** Max fixed substeps per frame — caps catch-up work after a long pause. */
446
+ const MAX_FIXED_SUBSTEPS = 5;
447
+ /** Depth-first search for the first camera in a subtree (Object3D.isCamera). */
448
+ function findCamera(root) {
449
+ let found;
450
+ root.traverse((o) => {
451
+ if (!found && o.isCamera)
452
+ found = o;
453
+ });
454
+ return found;
455
+ }
456
+ /** Release geometries and materials of every mesh in a subtree. */
457
+ function disposeObject(root) {
458
+ root.traverse((object) => {
459
+ const mesh = object;
460
+ mesh.geometry?.dispose();
461
+ const material = mesh.material;
462
+ if (Array.isArray(material))
463
+ for (const m of material)
464
+ m.dispose();
465
+ else
466
+ material?.dispose?.();
467
+ });
468
+ }
@@ -0,0 +1,99 @@
1
+ import type { Object3D, Scene, WebGLRenderer } from 'three';
2
+ import type { CompiledScalar, ScalarType } from '../dsl/types.js';
3
+ /**
4
+ * The tag registry — the extensible counterpart to the component `Registry`.
5
+ *
6
+ * Two kinds of tag live here:
7
+ * - **Nodes** (`@node`): scene-graph `Object3D` tags (`<Mesh>`, `<SpotLight>`, …).
8
+ * - **Attachments** (`@attachment`): leaf config that attaches to and configures a
9
+ * node — geometry/material on a mesh, fog/background on the scene. One concept
10
+ * replaces the old geometry/material/setting kinds.
11
+ *
12
+ * Built-ins live one-per-file under `src/nodes/**` and self-register on import via
13
+ * the class decorators (see core/decorators.ts); projects register their own the
14
+ * same way (the compiler discovers those decorators like it discovers `@component`).
15
+ */
16
+ export type NodeKind = 'scene' | 'object3d';
17
+ export interface NodeAttr {
18
+ type: ScalarType;
19
+ /** Default in compiled form (number, boolean, number[], 0xRRGGBB). */
20
+ default?: CompiledScalar;
21
+ required?: boolean;
22
+ description?: string;
23
+ }
24
+ /** What a node may contain (attachments are governed separately, by target). */
25
+ export interface ChildRules {
26
+ /** May contain child Object3D node tags. */
27
+ object3d: boolean;
28
+ /** May contain a single <Components> block. */
29
+ components: boolean;
30
+ }
31
+ export interface NodeDef {
32
+ tag: string;
33
+ kind: NodeKind;
34
+ description?: string;
35
+ /** Type-specific attributes (transform attrs are shared, see TRANSFORM_ATTRS). */
36
+ attrs: Record<string, NodeAttr>;
37
+ children: ChildRules;
38
+ /** Build the bare Three.js object (transforms are applied separately). */
39
+ build(attrs: Record<string, CompiledScalar>): Object3D;
40
+ /** The Object3D subclass this node builds — used to match attachment targets. */
41
+ produces?: AttachClass;
42
+ }
43
+ /** A node's runtime Object3D constructor (for attachment target matching). */
44
+ export type AttachClass = abstract new (...args: never[]) => Object3D;
45
+ /** Where an attachment may attach: an Object3D class (subclass-aware) or a node tag. */
46
+ export type AttachTarget = AttachClass | AttachClass[] | string;
47
+ /** What an attachment is given when the scene loads. `node` is the host. */
48
+ export interface AttachContext<TNode extends Object3D = Object3D> {
49
+ node: TNode;
50
+ scene: Scene;
51
+ renderer?: WebGLRenderer;
52
+ }
53
+ /** A live attachment instance (created by the def, holds @property fields). */
54
+ export interface AttachmentInstance {
55
+ attach(ctx: AttachContext): void;
56
+ }
57
+ export interface AttachmentDef {
58
+ tag: string;
59
+ description?: string;
60
+ attrs: Record<string, NodeAttr>;
61
+ /** The node(s) this attaches to. */
62
+ target: AttachTarget;
63
+ /** Cardinality bucket on the target (e.g. 'geometry', 'material', 'fog'). */
64
+ group: string;
65
+ /** The target needs at least one attachment of this group. */
66
+ required: boolean;
67
+ /** At most one attachment of this group per target (default true). */
68
+ unique: boolean;
69
+ /** Make a fresh instance and assign its attrs. */
70
+ make(attrs: Record<string, CompiledScalar>): AttachmentInstance;
71
+ }
72
+ export interface SystemDef {
73
+ ctor: new () => unknown;
74
+ /** The component type the engine feeds this system (single-type query, v1). */
75
+ componentType: abstract new (...args: never[]) => object;
76
+ }
77
+ /** Transform attributes shared by every Object3D-family node (degrees for rotation). */
78
+ export declare const TRANSFORM_ATTRS: Record<string, NodeAttr>;
79
+ export declare function registerNode(def: NodeDef): void;
80
+ export declare function unregisterNode(tag: string): boolean;
81
+ export declare function getNodeDef(tag: string): NodeDef | undefined;
82
+ export declare function nodeTags(): string[];
83
+ export declare function allNodeDefs(): NodeDef[];
84
+ /** True for tags that carry a transform (Scene + Object3D family). All nodes do. */
85
+ export declare function isSpatial(tag: string): boolean;
86
+ export declare function registerAttachment(def: AttachmentDef): void;
87
+ export declare function unregisterAttachment(tag: string): boolean;
88
+ export declare function getAttachmentDef(tag: string): AttachmentDef | undefined;
89
+ export declare function attachmentTags(): string[];
90
+ export declare function allAttachmentDefs(): AttachmentDef[];
91
+ /** Does an attachment's target accept this node? (class-assignable or tag match) */
92
+ export declare function attachmentAcceptsNode(def: AttachmentDef, node: NodeDef): boolean;
93
+ /** Attachment tags a node may contain (for the XSD/docs and validator). */
94
+ export declare function attachmentsForNode(node: NodeDef): AttachmentDef[];
95
+ export declare function registerSystem(def: SystemDef): void;
96
+ export declare function unregisterSystem(ctor: new () => unknown): boolean;
97
+ export declare function allSystemDefs(): readonly SystemDef[];
98
+ /** Default child rules for a node: holds child nodes + a <Components> block. */
99
+ export declare const spatialChildren: (overrides?: Partial<ChildRules>) => ChildRules;
@@ -0,0 +1,87 @@
1
+ /** Transform attributes shared by every Object3D-family node (degrees for rotation). */
2
+ export const TRANSFORM_ATTRS = {
3
+ position: { type: 'vec3', default: [0, 0, 0], description: 'Local position x y z' },
4
+ rotation: { type: 'euler', default: [0, 0, 0], description: 'Local rotation in degrees x y z' },
5
+ scale: { type: 'vec3', default: [1, 1, 1], description: 'Local scale x y z' },
6
+ visible: { type: 'bool', default: true, description: 'Whether the object renders' },
7
+ };
8
+ // ---- node registry ----------------------------------------------------------
9
+ const nodeDefs = new Map();
10
+ export function registerNode(def) {
11
+ if (nodeDefs.has(def.tag))
12
+ throw new Error(`Node tag <${def.tag}> is already registered`);
13
+ nodeDefs.set(def.tag, def);
14
+ }
15
+ export function unregisterNode(tag) {
16
+ return nodeDefs.delete(tag);
17
+ }
18
+ export function getNodeDef(tag) {
19
+ return nodeDefs.get(tag);
20
+ }
21
+ export function nodeTags() {
22
+ return [...nodeDefs.keys()];
23
+ }
24
+ export function allNodeDefs() {
25
+ return [...nodeDefs.values()];
26
+ }
27
+ /** True for tags that carry a transform (Scene + Object3D family). All nodes do. */
28
+ export function isSpatial(tag) {
29
+ return nodeDefs.has(tag);
30
+ }
31
+ // ---- attachment registry ----------------------------------------------------
32
+ const attachmentDefs = new Map();
33
+ export function registerAttachment(def) {
34
+ if (attachmentDefs.has(def.tag))
35
+ throw new Error(`Attachment tag <${def.tag}> is already registered`);
36
+ attachmentDefs.set(def.tag, def);
37
+ }
38
+ export function unregisterAttachment(tag) {
39
+ return attachmentDefs.delete(tag);
40
+ }
41
+ export function getAttachmentDef(tag) {
42
+ return attachmentDefs.get(tag);
43
+ }
44
+ export function attachmentTags() {
45
+ return [...attachmentDefs.keys()];
46
+ }
47
+ export function allAttachmentDefs() {
48
+ return [...attachmentDefs.values()];
49
+ }
50
+ /** Does an attachment's target accept this node? (class-assignable or tag match) */
51
+ export function attachmentAcceptsNode(def, node) {
52
+ const t = def.target;
53
+ if (typeof t === 'string')
54
+ return node.tag === t;
55
+ const classes = Array.isArray(t) ? t : [t];
56
+ return node.produces !== undefined && classes.some((c) => isProducedBy(node.produces, c));
57
+ }
58
+ /** `produces` is `target` or a subclass of it. */
59
+ function isProducedBy(produces, target) {
60
+ return produces === target || produces.prototype instanceof target;
61
+ }
62
+ /** Attachment tags a node may contain (for the XSD/docs and validator). */
63
+ export function attachmentsForNode(node) {
64
+ return allAttachmentDefs().filter((d) => attachmentAcceptsNode(d, node));
65
+ }
66
+ // ---- system registry --------------------------------------------------------
67
+ const systemDefs = [];
68
+ export function registerSystem(def) {
69
+ systemDefs.push(def);
70
+ }
71
+ export function unregisterSystem(ctor) {
72
+ const i = systemDefs.findIndex((d) => d.ctor === ctor);
73
+ if (i === -1)
74
+ return false;
75
+ systemDefs.splice(i, 1);
76
+ return true;
77
+ }
78
+ export function allSystemDefs() {
79
+ return systemDefs;
80
+ }
81
+ // ---- shared helper ----------------------------------------------------------
82
+ /** Default child rules for a node: holds child nodes + a <Components> block. */
83
+ export const spatialChildren = (overrides = {}) => ({
84
+ object3d: true,
85
+ components: true,
86
+ ...overrides,
87
+ });