@holoscript/engine 6.0.3 → 6.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AutoMesher-CK47F6AV.js +17 -0
- package/dist/GPUBuffers-2LHBCD7X.js +9 -0
- package/dist/WebGPUContext-TNEUYU2Y.js +11 -0
- package/dist/animation/index.cjs +38 -38
- package/dist/animation/index.d.cts +1 -1
- package/dist/animation/index.d.ts +1 -1
- package/dist/animation/index.js +1 -1
- package/dist/audio/index.cjs +16 -6
- package/dist/audio/index.d.cts +1 -1
- package/dist/audio/index.d.ts +1 -1
- package/dist/audio/index.js +1 -1
- package/dist/camera/index.cjs +23 -23
- package/dist/camera/index.d.cts +1 -1
- package/dist/camera/index.d.ts +1 -1
- package/dist/camera/index.js +1 -1
- package/dist/character/index.cjs +6 -4
- package/dist/character/index.js +1 -1
- package/dist/choreography/index.cjs +1194 -0
- package/dist/choreography/index.d.cts +687 -0
- package/dist/choreography/index.d.ts +687 -0
- package/dist/choreography/index.js +1156 -0
- package/dist/chunk-2CSNRI2N.js +217 -0
- package/dist/chunk-33T2WINR.js +266 -0
- package/dist/chunk-35R73OFM.js +1257 -0
- package/dist/chunk-4MMDSUNP.js +1256 -0
- package/dist/chunk-5V6HOU72.js +319 -0
- package/dist/chunk-6QOP6PYF.js +1038 -0
- package/dist/chunk-7KMJVHIL.js +8944 -0
- package/dist/chunk-7VPUC62U.js +1106 -0
- package/dist/chunk-A2Y6RCAT.js +1878 -0
- package/dist/chunk-AHM42MK6.js +8944 -0
- package/dist/chunk-BL7IDTHE.js +218 -0
- package/dist/chunk-CITOMSWL.js +10462 -0
- package/dist/chunk-CXDPKW2K.js +8944 -0
- package/dist/chunk-CXZPLD4S.js +223 -0
- package/dist/chunk-CZYJE7IH.js +5169 -0
- package/dist/chunk-D2OP7YC7.js +6325 -0
- package/dist/chunk-EDRVQHUU.js +1544 -0
- package/dist/chunk-EJSLOOW2.js +3589 -0
- package/dist/chunk-F53SFGW5.js +1878 -0
- package/dist/chunk-HCFPELPY.js +919 -0
- package/dist/chunk-HNEE36PY.js +93 -0
- package/dist/chunk-HYXNV36F.js +1256 -0
- package/dist/chunk-IB7KHVFY.js +821 -0
- package/dist/chunk-IBBO7YYG.js +690 -0
- package/dist/chunk-ILIBGINU.js +5470 -0
- package/dist/chunk-IS4MHLKN.js +5479 -0
- package/dist/chunk-JT2PFKWD.js +5479 -0
- package/dist/chunk-K4CUB4NY.js +1038 -0
- package/dist/chunk-KATDQXRJ.js +10462 -0
- package/dist/chunk-KBQE6ZFJ.js +8944 -0
- package/dist/chunk-KBVD5K7E.js +560 -0
- package/dist/chunk-KCDPVQRY.js +4088 -0
- package/dist/chunk-KN4QJPKN.js +8944 -0
- package/dist/chunk-KWJ3ROSI.js +8944 -0
- package/dist/chunk-L45VF6DD.js +919 -0
- package/dist/chunk-LY4T37YK.js +307 -0
- package/dist/chunk-MDN5WZXA.js +1544 -0
- package/dist/chunk-MGCDP6VU.js +928 -0
- package/dist/chunk-NCX7X6G2.js +8681 -0
- package/dist/chunk-OF54BPVD.js +913 -0
- package/dist/chunk-OWSN2Q3Q.js +690 -0
- package/dist/chunk-PRRB5TTA.js +406 -0
- package/dist/chunk-PXWVQF76.js +4086 -0
- package/dist/chunk-PYCOIDT2.js +812 -0
- package/dist/chunk-PZCSADOV.js +928 -0
- package/dist/chunk-Q2XBVS2K.js +1038 -0
- package/dist/chunk-QDZRXWN5.js +1776 -0
- package/dist/chunk-RNWOZ6WQ.js +913 -0
- package/dist/chunk-ROLFT4CJ.js +1693 -0
- package/dist/chunk-SLTJRZ2N.js +266 -0
- package/dist/chunk-SRUS5XSU.js +4088 -0
- package/dist/chunk-TKCA3WZ5.js +5409 -0
- package/dist/chunk-TNRMXYI2.js +1650 -0
- package/dist/chunk-TQB3GJGM.js +9763 -0
- package/dist/chunk-TUFGXG6K.js +510 -0
- package/dist/chunk-U6KMTGQJ.js +632 -0
- package/dist/chunk-VMGJQST6.js +8681 -0
- package/dist/chunk-X4F4TCG4.js +5470 -0
- package/dist/chunk-ZIFROE75.js +1544 -0
- package/dist/chunk-ZIJQYHSQ.js +1204 -0
- package/dist/combat/index.cjs +4 -4
- package/dist/combat/index.d.cts +1 -1
- package/dist/combat/index.d.ts +1 -1
- package/dist/combat/index.js +1 -1
- package/dist/ecs/index.cjs +1 -1
- package/dist/ecs/index.js +1 -1
- package/dist/environment/index.cjs +14 -14
- package/dist/environment/index.d.cts +1 -1
- package/dist/environment/index.d.ts +1 -1
- package/dist/environment/index.js +1 -1
- package/dist/gpu/index.cjs +4810 -0
- package/dist/gpu/index.js +3714 -0
- package/dist/hologram/index.cjs +27 -1
- package/dist/hologram/index.js +1 -1
- package/dist/index-B2PIsAmR.d.cts +2180 -0
- package/dist/index-B2PIsAmR.d.ts +2180 -0
- package/dist/index-BHySEPX7.d.cts +2921 -0
- package/dist/index-BJV21zuy.d.cts +341 -0
- package/dist/index-BJV21zuy.d.ts +341 -0
- package/dist/index-BQutTphC.d.cts +790 -0
- package/dist/index-ByIq2XrS.d.cts +3910 -0
- package/dist/index-BysHjDSO.d.cts +224 -0
- package/dist/index-BysHjDSO.d.ts +224 -0
- package/dist/index-CKwAJGck.d.ts +455 -0
- package/dist/index-CUl3QstQ.d.cts +3006 -0
- package/dist/index-CUl3QstQ.d.ts +3006 -0
- package/dist/index-CmYtNiI-.d.cts +953 -0
- package/dist/index-CmYtNiI-.d.ts +953 -0
- package/dist/index-CnRzWxi_.d.cts +522 -0
- package/dist/index-CnRzWxi_.d.ts +522 -0
- package/dist/index-CwRWbSC7.d.ts +2921 -0
- package/dist/index-CxKIBstO.d.ts +790 -0
- package/dist/index-DJ6-R8vh.d.cts +455 -0
- package/dist/index-DQKisbcI.d.cts +4968 -0
- package/dist/index-DQKisbcI.d.ts +4968 -0
- package/dist/index-DRT2zJez.d.ts +3910 -0
- package/dist/index-DfNLiAka.d.cts +192 -0
- package/dist/index-DfNLiAka.d.ts +192 -0
- package/dist/index-nMvkoRm8.d.cts +405 -0
- package/dist/index-nMvkoRm8.d.ts +405 -0
- package/dist/index-s9yOFU37.d.cts +604 -0
- package/dist/index-s9yOFU37.d.ts +604 -0
- package/dist/index.cjs +22966 -6960
- package/dist/index.d.cts +864 -20
- package/dist/index.d.ts +864 -20
- package/dist/index.js +3062 -48
- package/dist/input/index.cjs +1 -1
- package/dist/input/index.js +1 -1
- package/dist/orbital/index.cjs +3 -3
- package/dist/orbital/index.d.cts +1 -1
- package/dist/orbital/index.d.ts +1 -1
- package/dist/orbital/index.js +1 -1
- package/dist/particles/index.cjs +16 -16
- package/dist/particles/index.d.cts +1 -1
- package/dist/particles/index.d.ts +1 -1
- package/dist/particles/index.js +1 -1
- package/dist/physics/index.cjs +2377 -21
- package/dist/physics/index.d.cts +1 -1
- package/dist/physics/index.d.ts +1 -1
- package/dist/physics/index.js +35 -1
- package/dist/postfx/index.cjs +3491 -0
- package/dist/postfx/index.js +93 -0
- package/dist/procedural/index.cjs +1 -1
- package/dist/procedural/index.js +1 -1
- package/dist/puppeteer-5VF6KDVO.js +52197 -0
- package/dist/puppeteer-IZVZ3SG4.js +52197 -0
- package/dist/rendering/index.cjs +33 -32
- package/dist/rendering/index.d.cts +1 -1
- package/dist/rendering/index.d.ts +1 -1
- package/dist/rendering/index.js +8 -6
- package/dist/runtime/index.cjs +23 -13
- package/dist/runtime/index.d.cts +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +8 -6
- package/dist/runtime/protocols/index.cjs +349 -0
- package/dist/runtime/protocols/index.js +15 -0
- package/dist/scene/index.cjs +8 -8
- package/dist/scene/index.d.cts +1 -1
- package/dist/scene/index.d.ts +1 -1
- package/dist/scene/index.js +1 -1
- package/dist/shader/index.cjs +3087 -0
- package/dist/shader/index.js +3044 -0
- package/dist/simulation/index.cjs +10680 -0
- package/dist/simulation/index.d.cts +3 -0
- package/dist/simulation/index.d.ts +3 -0
- package/dist/simulation/index.js +307 -0
- package/dist/spatial/index.cjs +2443 -0
- package/dist/spatial/index.d.cts +1545 -0
- package/dist/spatial/index.d.ts +1545 -0
- package/dist/spatial/index.js +2400 -0
- package/dist/terrain/index.cjs +1 -1
- package/dist/terrain/index.d.cts +1 -1
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/transformers.node-4NKAPD5U.js +45620 -0
- package/dist/vm/index.cjs +7 -8
- package/dist/vm/index.d.cts +1 -1
- package/dist/vm/index.d.ts +1 -1
- package/dist/vm/index.js +1 -1
- package/dist/vm-bridge/index.cjs +2 -2
- package/dist/vm-bridge/index.d.cts +2 -2
- package/dist/vm-bridge/index.d.ts +2 -2
- package/dist/vm-bridge/index.js +1 -1
- package/dist/vr/index.cjs +6 -6
- package/dist/vr/index.js +1 -1
- package/dist/world/index.cjs +3 -3
- package/dist/world/index.d.cts +1 -1
- package/dist/world/index.d.ts +1 -1
- package/dist/world/index.js +1 -1
- package/package.json +53 -21
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -1,14 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
simulation_exports
|
|
3
|
+
} from "./chunk-TQB3GJGM.js";
|
|
4
|
+
import "./chunk-BL7IDTHE.js";
|
|
5
|
+
import {
|
|
6
|
+
terrain_exports
|
|
7
|
+
} from "./chunk-PYCOIDT2.js";
|
|
8
|
+
import {
|
|
9
|
+
tilemap_exports
|
|
10
|
+
} from "./chunk-3JORF6V4.js";
|
|
11
|
+
import {
|
|
12
|
+
vm_exports
|
|
13
|
+
} from "./chunk-HYXNV36F.js";
|
|
14
|
+
import {
|
|
15
|
+
vm_bridge_exports
|
|
16
|
+
} from "./chunk-SLTJRZ2N.js";
|
|
1
17
|
import {
|
|
2
18
|
vr_exports
|
|
3
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-KBVD5K7E.js";
|
|
4
20
|
import {
|
|
5
21
|
world_exports
|
|
6
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-PRRB5TTA.js";
|
|
23
|
+
import {
|
|
24
|
+
PhysicsWorldImpl,
|
|
25
|
+
VRPhysicsBridge,
|
|
26
|
+
physics_exports
|
|
27
|
+
} from "./chunk-VMGJQST6.js";
|
|
28
|
+
import {
|
|
29
|
+
particles_exports
|
|
30
|
+
} from "./chunk-Q2XBVS2K.js";
|
|
31
|
+
import {
|
|
32
|
+
procedural_exports
|
|
33
|
+
} from "./chunk-HCFPELPY.js";
|
|
34
|
+
import {
|
|
35
|
+
scene_exports
|
|
36
|
+
} from "./chunk-RNWOZ6WQ.js";
|
|
7
37
|
import {
|
|
8
38
|
AdvancedLightingManager,
|
|
9
39
|
AdvancedPBRMaterial,
|
|
10
40
|
BVH,
|
|
11
|
-
BloomEffect,
|
|
12
41
|
CloudRenderer,
|
|
13
42
|
ColorGrading,
|
|
14
43
|
DecalBatcher,
|
|
@@ -20,10 +49,7 @@ import {
|
|
|
20
49
|
MATERIAL_PRESETS2,
|
|
21
50
|
MaterialLibrary,
|
|
22
51
|
MaterialSystem,
|
|
23
|
-
PP_PRESETS,
|
|
24
52
|
PhysicsDebugDrawer,
|
|
25
|
-
PostProcessStack,
|
|
26
|
-
PostProcessingStack,
|
|
27
53
|
ProjectorLight,
|
|
28
54
|
PuppeteerRenderer,
|
|
29
55
|
RayTracer,
|
|
@@ -93,7 +119,6 @@ import {
|
|
|
93
119
|
packRect,
|
|
94
120
|
parseIESProfile,
|
|
95
121
|
pathTrace,
|
|
96
|
-
postprocess_exports,
|
|
97
122
|
prerenderHTML,
|
|
98
123
|
rectSolidAngle,
|
|
99
124
|
renderPDF,
|
|
@@ -110,73 +135,65 @@ import {
|
|
|
110
135
|
sheenVisibility,
|
|
111
136
|
thinSlabTransmission,
|
|
112
137
|
triplanarWeights
|
|
113
|
-
} from "./chunk-
|
|
138
|
+
} from "./chunk-TKCA3WZ5.js";
|
|
114
139
|
import {
|
|
115
|
-
|
|
116
|
-
|
|
140
|
+
BloomEffect,
|
|
141
|
+
PP_PRESETS,
|
|
142
|
+
PostProcessStack,
|
|
143
|
+
PostProcessingStack,
|
|
144
|
+
postprocess_exports
|
|
145
|
+
} from "./chunk-EJSLOOW2.js";
|
|
117
146
|
import {
|
|
147
|
+
eventBus,
|
|
118
148
|
runtime_exports
|
|
119
|
-
} from "./chunk-
|
|
120
|
-
import
|
|
121
|
-
scene_exports
|
|
122
|
-
} from "./chunk-HWEANAXF.js";
|
|
123
|
-
import {
|
|
124
|
-
terrain_exports
|
|
125
|
-
} from "./chunk-HLZJ37AZ.js";
|
|
126
|
-
import {
|
|
127
|
-
tilemap_exports
|
|
128
|
-
} from "./chunk-3JORF6V4.js";
|
|
129
|
-
import {
|
|
130
|
-
vm_bridge_exports
|
|
131
|
-
} from "./chunk-RN5CTZ6Q.js";
|
|
132
|
-
import {
|
|
133
|
-
vm_exports
|
|
134
|
-
} from "./chunk-PXUX4KT5.js";
|
|
149
|
+
} from "./chunk-CZYJE7IH.js";
|
|
150
|
+
import "./chunk-5V6HOU72.js";
|
|
135
151
|
import {
|
|
136
152
|
ecs_exports
|
|
137
|
-
} from "./chunk-
|
|
153
|
+
} from "./chunk-IB7KHVFY.js";
|
|
154
|
+
import {
|
|
155
|
+
environment_exports
|
|
156
|
+
} from "./chunk-ZIFROE75.js";
|
|
138
157
|
import {
|
|
139
158
|
gameplay_exports
|
|
140
159
|
} from "./chunk-RCQHJUSC.js";
|
|
160
|
+
import {
|
|
161
|
+
GaussianSplatExtractor
|
|
162
|
+
} from "./chunk-HNEE36PY.js";
|
|
163
|
+
import "./chunk-TUFGXG6K.js";
|
|
164
|
+
import "./chunk-2CSNRI2N.js";
|
|
141
165
|
import {
|
|
142
166
|
hologram_exports
|
|
143
|
-
} from "./chunk-
|
|
167
|
+
} from "./chunk-7VPUC62U.js";
|
|
144
168
|
import {
|
|
145
169
|
input_exports
|
|
146
|
-
} from "./chunk-
|
|
170
|
+
} from "./chunk-U6KMTGQJ.js";
|
|
147
171
|
import {
|
|
148
172
|
navigation_exports
|
|
149
173
|
} from "./chunk-4YSUBXFR.js";
|
|
150
174
|
import {
|
|
151
175
|
orbital_exports
|
|
152
|
-
} from "./chunk-
|
|
153
|
-
import {
|
|
154
|
-
particles_exports
|
|
155
|
-
} from "./chunk-DWP6F6V4.js";
|
|
156
|
-
import {
|
|
157
|
-
physics_exports
|
|
158
|
-
} from "./chunk-JQIUSARK.js";
|
|
176
|
+
} from "./chunk-LY4T37YK.js";
|
|
159
177
|
import {
|
|
178
|
+
AnimationEngine,
|
|
179
|
+
TransitionSystem,
|
|
160
180
|
animation_exports
|
|
161
|
-
} from "./chunk-
|
|
181
|
+
} from "./chunk-F53SFGW5.js";
|
|
162
182
|
import {
|
|
163
183
|
audio_exports
|
|
164
|
-
} from "./chunk-
|
|
184
|
+
} from "./chunk-SRUS5XSU.js";
|
|
165
185
|
import {
|
|
166
186
|
camera_exports
|
|
167
|
-
} from "./chunk-
|
|
187
|
+
} from "./chunk-MGCDP6VU.js";
|
|
168
188
|
import {
|
|
169
189
|
character_exports
|
|
170
|
-
} from "./chunk-
|
|
190
|
+
} from "./chunk-ZIJQYHSQ.js";
|
|
171
191
|
import {
|
|
172
192
|
combat_exports
|
|
173
|
-
} from "./chunk-
|
|
193
|
+
} from "./chunk-OWSN2Q3Q.js";
|
|
174
194
|
import {
|
|
175
195
|
dialogue_exports
|
|
176
196
|
} from "./chunk-2MUNCP4Y.js";
|
|
177
|
-
import {
|
|
178
|
-
environment_exports
|
|
179
|
-
} from "./chunk-CRVDNMZI.js";
|
|
180
197
|
import {
|
|
181
198
|
__export
|
|
182
199
|
} from "./chunk-AKLW2MUS.js";
|
|
@@ -1465,7 +1482,7 @@ var LODManager = class {
|
|
|
1465
1482
|
/**
|
|
1466
1483
|
* Process transition queue (max transitions per frame to prevent stuttering)
|
|
1467
1484
|
*/
|
|
1468
|
-
processTransitionQueue(
|
|
1485
|
+
processTransitionQueue(_deltaTime) {
|
|
1469
1486
|
this.transitionQueue.sort((a, b) => b.priority - a.priority);
|
|
1470
1487
|
const transitionsToProcess = this.transitionQueue.slice(0, this.maxTransitionsPerFrame);
|
|
1471
1488
|
for (const { objectId, level } of transitionsToProcess) {
|
|
@@ -2644,7 +2661,7 @@ var TransitionScheduler = class {
|
|
|
2644
2661
|
/**
|
|
2645
2662
|
* Record actual GPU cost for a transition (for budget tuning)
|
|
2646
2663
|
*/
|
|
2647
|
-
recordCost(entityId,
|
|
2664
|
+
recordCost(entityId, _actualCostMs) {
|
|
2648
2665
|
if (this.activatedThisFrame.has(entityId)) {
|
|
2649
2666
|
}
|
|
2650
2667
|
}
|
|
@@ -2809,7 +2826,7 @@ var LODMemoryPool = class {
|
|
|
2809
2826
|
}
|
|
2810
2827
|
this.stats.defragmentationCount++;
|
|
2811
2828
|
this.updateStatistics();
|
|
2812
|
-
const
|
|
2829
|
+
const _duration = performance.now() - startTime;
|
|
2813
2830
|
}
|
|
2814
2831
|
/**
|
|
2815
2832
|
* Check memory pressure and trigger callbacks
|
|
@@ -3163,7 +3180,7 @@ var LODPerformanceMetrics = class {
|
|
|
3163
3180
|
/**
|
|
3164
3181
|
* Start profiling a code section
|
|
3165
3182
|
*/
|
|
3166
|
-
startProfile(
|
|
3183
|
+
startProfile(_name) {
|
|
3167
3184
|
if (!this.enabled) return 0;
|
|
3168
3185
|
return performance.now();
|
|
3169
3186
|
}
|
|
@@ -3690,6 +3707,2993 @@ function createMeshletGenerator(options) {
|
|
|
3690
3707
|
function generateMeshlets(mesh, options) {
|
|
3691
3708
|
return new MeshletGenerator(options).generate(mesh);
|
|
3692
3709
|
}
|
|
3710
|
+
|
|
3711
|
+
// src/simulation/TetGenWasmMesher.ts
|
|
3712
|
+
var TetGenWasmMesher = class {
|
|
3713
|
+
wasmModule = null;
|
|
3714
|
+
constructor() {
|
|
3715
|
+
}
|
|
3716
|
+
async initialize() {
|
|
3717
|
+
if (this.wasmModule) return;
|
|
3718
|
+
console.log("TetGenWasmMesher: Initializing WASM binary...");
|
|
3719
|
+
}
|
|
3720
|
+
async tetrahedralize(surface, options) {
|
|
3721
|
+
await this.initialize();
|
|
3722
|
+
console.log(`TetGenWasmMesher: Tetrahedralizing surface with ${surface.triangles.length / 3} faces...`);
|
|
3723
|
+
const flags = options?.quality ? `-pq${options.quality}` : "-pq";
|
|
3724
|
+
console.log(`TetGenWasmMesher: Using flags ${flags}`);
|
|
3725
|
+
const { meshBox } = await import("./AutoMesher-CK47F6AV.js");
|
|
3726
|
+
return meshBox({
|
|
3727
|
+
size: [1, 1, 1],
|
|
3728
|
+
// Placeholder
|
|
3729
|
+
divisions: [10, 10, 10]
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3732
|
+
};
|
|
3733
|
+
|
|
3734
|
+
// src/runtime/HoloScriptPlusRuntime.ts
|
|
3735
|
+
import { createState, ExpressionEvaluator } from "@holoscript/core";
|
|
3736
|
+
import { vrTraitRegistry } from "@holoscript/core";
|
|
3737
|
+
|
|
3738
|
+
// src/runtime/loader.ts
|
|
3739
|
+
var ChunkLoader = class {
|
|
3740
|
+
runtime;
|
|
3741
|
+
manifest = null;
|
|
3742
|
+
loadedChunks = /* @__PURE__ */ new Set();
|
|
3743
|
+
loadingChunks = /* @__PURE__ */ new Set();
|
|
3744
|
+
options;
|
|
3745
|
+
constructor(runtime, options) {
|
|
3746
|
+
this.runtime = runtime;
|
|
3747
|
+
this.options = options;
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3750
|
+
* Initialize the loader by fetching the manifest
|
|
3751
|
+
*/
|
|
3752
|
+
async init() {
|
|
3753
|
+
try {
|
|
3754
|
+
const response = await fetch(this.options.manifestUrl);
|
|
3755
|
+
this.manifest = await response.json();
|
|
3756
|
+
} catch (e) {
|
|
3757
|
+
console.error("[ChunkLoader] Failed to load manifest", e);
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
/**
|
|
3761
|
+
* Update the loader (check for spatial triggers)
|
|
3762
|
+
*/
|
|
3763
|
+
update() {
|
|
3764
|
+
if (!this.manifest) return;
|
|
3765
|
+
const playerPos = this.runtime.vrContext.headset.position;
|
|
3766
|
+
if (!playerPos) return;
|
|
3767
|
+
for (const [chunkId, info] of Object.entries(this.manifest.chunks)) {
|
|
3768
|
+
if (this.loadedChunks.has(chunkId) || this.loadingChunks.has(chunkId)) continue;
|
|
3769
|
+
if (info.bounds && this.isPointInBounds(playerPos, info.bounds)) {
|
|
3770
|
+
this.loadChunk(chunkId);
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
/**
|
|
3775
|
+
* Manually load a chunk
|
|
3776
|
+
*/
|
|
3777
|
+
async loadChunk(chunkId) {
|
|
3778
|
+
if (!this.manifest || !this.manifest.chunks[chunkId]) return;
|
|
3779
|
+
if (this.loadedChunks.has(chunkId) || this.loadingChunks.has(chunkId)) return;
|
|
3780
|
+
this.loadingChunks.add(chunkId);
|
|
3781
|
+
try {
|
|
3782
|
+
const info = this.manifest.chunks[chunkId];
|
|
3783
|
+
const url = this.options.baseUrl ? `${this.options.baseUrl}/${info.file}` : info.file;
|
|
3784
|
+
const response = await fetch(url);
|
|
3785
|
+
const chunkData = await response.json();
|
|
3786
|
+
await this.integrateChunk(chunkData);
|
|
3787
|
+
this.loadedChunks.add(chunkId);
|
|
3788
|
+
} catch (e) {
|
|
3789
|
+
console.error(`[ChunkLoader] Failed to load chunk ${chunkId}`, e);
|
|
3790
|
+
} finally {
|
|
3791
|
+
this.loadingChunks.delete(chunkId);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
async integrateChunk(chunk) {
|
|
3795
|
+
const objects = chunk.objects;
|
|
3796
|
+
if (!objects) return;
|
|
3797
|
+
const extRuntime = this.runtime;
|
|
3798
|
+
for (const obj of objects) {
|
|
3799
|
+
if (extRuntime.mountObject) {
|
|
3800
|
+
extRuntime.mountObject(obj);
|
|
3801
|
+
} else if (extRuntime.instantiateNode) {
|
|
3802
|
+
extRuntime.instantiateNode(obj, extRuntime.rootInstance);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
isPointInBounds(point, bounds) {
|
|
3807
|
+
if (bounds.length < 2) return false;
|
|
3808
|
+
const [min, max] = bounds;
|
|
3809
|
+
return point[0] >= min[0] && point[0] <= max[0] && point[1] >= min[1] && point[1] <= max[1] && point[2] >= min[2] && point[2] <= max[2];
|
|
3810
|
+
}
|
|
3811
|
+
};
|
|
3812
|
+
|
|
3813
|
+
// src/runtime/HotReloader.ts
|
|
3814
|
+
import {
|
|
3815
|
+
diffState,
|
|
3816
|
+
buildMigrationChain,
|
|
3817
|
+
snapshotState,
|
|
3818
|
+
applyAutoMigration
|
|
3819
|
+
} from "@holoscript/core";
|
|
3820
|
+
var HotReloader = class {
|
|
3821
|
+
templates = /* @__PURE__ */ new Map();
|
|
3822
|
+
instances = /* @__PURE__ */ new Map();
|
|
3823
|
+
devMode;
|
|
3824
|
+
onWarning;
|
|
3825
|
+
onReload;
|
|
3826
|
+
onRollback;
|
|
3827
|
+
migrationExecutor = null;
|
|
3828
|
+
constructor(options = {}) {
|
|
3829
|
+
this.devMode = options.devMode ?? false;
|
|
3830
|
+
this.onWarning = options.onWarning ?? (() => {
|
|
3831
|
+
});
|
|
3832
|
+
this.onReload = options.onReload ?? (() => {
|
|
3833
|
+
});
|
|
3834
|
+
this.onRollback = options.onRollback ?? (() => {
|
|
3835
|
+
});
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Set the migration executor — the function that runs migration statement bodies.
|
|
3839
|
+
* This decouples the HotReloader from the runtime's statement interpreter.
|
|
3840
|
+
*/
|
|
3841
|
+
setMigrationExecutor(executor) {
|
|
3842
|
+
this.migrationExecutor = executor;
|
|
3843
|
+
}
|
|
3844
|
+
/**
|
|
3845
|
+
* Register a template. Called on initial load.
|
|
3846
|
+
*/
|
|
3847
|
+
registerTemplate(template) {
|
|
3848
|
+
this.templates.set(template.name, template);
|
|
3849
|
+
if (!this.instances.has(template.name)) {
|
|
3850
|
+
this.instances.set(template.name, []);
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
/**
|
|
3854
|
+
* Register a live instance of a template.
|
|
3855
|
+
*/
|
|
3856
|
+
registerInstance(instance) {
|
|
3857
|
+
const list = this.instances.get(instance.templateName);
|
|
3858
|
+
if (list) {
|
|
3859
|
+
list.push(instance);
|
|
3860
|
+
} else {
|
|
3861
|
+
this.instances.set(instance.templateName, [instance]);
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Unregister an instance (e.g., on destroy).
|
|
3866
|
+
*/
|
|
3867
|
+
unregisterInstance(holoId) {
|
|
3868
|
+
for (const [, list] of this.instances) {
|
|
3869
|
+
const idx = list.findIndex((i) => i.__holo_id === holoId);
|
|
3870
|
+
if (idx !== -1) {
|
|
3871
|
+
list.splice(idx, 1);
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
/**
|
|
3877
|
+
* Hot-reload a template. This is the main entry point.
|
|
3878
|
+
*
|
|
3879
|
+
* Steps:
|
|
3880
|
+
* 1. Snapshot all instances of the template
|
|
3881
|
+
* 2. Diff old vs new state schema
|
|
3882
|
+
* 3. Build migration chain if needed
|
|
3883
|
+
* 4. Apply migrations to all instances
|
|
3884
|
+
* 5. Validate and swap
|
|
3885
|
+
* 6. Rollback on failure
|
|
3886
|
+
*/
|
|
3887
|
+
async reload(newTemplate) {
|
|
3888
|
+
const warnings = [];
|
|
3889
|
+
const templateName = newTemplate.name;
|
|
3890
|
+
const oldTemplate = this.templates.get(templateName);
|
|
3891
|
+
const oldVersion = oldTemplate?.version ?? 0;
|
|
3892
|
+
const newVersion = newTemplate.version ?? 0;
|
|
3893
|
+
const instanceList = this.instances.get(templateName) ?? [];
|
|
3894
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
3895
|
+
for (const instance of instanceList) {
|
|
3896
|
+
snapshots.set(instance.__holo_id, snapshotState(instance.state));
|
|
3897
|
+
}
|
|
3898
|
+
const diff = diffState(oldTemplate?.state, newTemplate.state);
|
|
3899
|
+
if (!diff.hasChanges && oldVersion === newVersion) {
|
|
3900
|
+
this.templates.set(templateName, newTemplate);
|
|
3901
|
+
const result = {
|
|
3902
|
+
success: true,
|
|
3903
|
+
templateName,
|
|
3904
|
+
oldVersion,
|
|
3905
|
+
newVersion,
|
|
3906
|
+
diff,
|
|
3907
|
+
migrationChain: null,
|
|
3908
|
+
instancesMigrated: 0,
|
|
3909
|
+
rollback: false,
|
|
3910
|
+
warnings
|
|
3911
|
+
};
|
|
3912
|
+
this.onReload(result);
|
|
3913
|
+
return result;
|
|
3914
|
+
}
|
|
3915
|
+
let migrationChain = null;
|
|
3916
|
+
if (newVersion > oldVersion || diff.requiresMigration) {
|
|
3917
|
+
migrationChain = buildMigrationChain(newTemplate, oldVersion, newVersion);
|
|
3918
|
+
if (!migrationChain) {
|
|
3919
|
+
if (diff.typeChanged.length > 0) {
|
|
3920
|
+
const error = `Missing migration chain from v${oldVersion} to v${newVersion} for template "${templateName}". ${diff.typeChanged.length} field(s) changed type: ${diff.typeChanged.map((c) => c.key).join(", ")}`;
|
|
3921
|
+
const result = {
|
|
3922
|
+
success: false,
|
|
3923
|
+
templateName,
|
|
3924
|
+
oldVersion,
|
|
3925
|
+
newVersion,
|
|
3926
|
+
diff,
|
|
3927
|
+
migrationChain: null,
|
|
3928
|
+
instancesMigrated: 0,
|
|
3929
|
+
rollback: true,
|
|
3930
|
+
error,
|
|
3931
|
+
warnings
|
|
3932
|
+
};
|
|
3933
|
+
this.onRollback(result);
|
|
3934
|
+
return result;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
const oldDefaults = /* @__PURE__ */ new Map();
|
|
3939
|
+
if (oldTemplate?.state?.properties) {
|
|
3940
|
+
for (const prop of oldTemplate.state.properties) {
|
|
3941
|
+
oldDefaults.set(prop.key, prop.value);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
let migrated = 0;
|
|
3945
|
+
try {
|
|
3946
|
+
for (const instance of instanceList) {
|
|
3947
|
+
applyAutoMigration(instance.state, diff, oldDefaults);
|
|
3948
|
+
if (migrationChain && this.migrationExecutor) {
|
|
3949
|
+
for (const step of migrationChain.steps) {
|
|
3950
|
+
await this.migrationExecutor(instance, step.body);
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
instance.version = newVersion;
|
|
3954
|
+
migrated++;
|
|
3955
|
+
}
|
|
3956
|
+
if (this.devMode) {
|
|
3957
|
+
for (const change of diff.removed) {
|
|
3958
|
+
warnings.push(`Field "${change.key}" removed from template "${templateName}"`);
|
|
3959
|
+
}
|
|
3960
|
+
for (const change of diff.reactiveChanged) {
|
|
3961
|
+
warnings.push(
|
|
3962
|
+
`Reactivity changed for field "${change.key}" in template "${templateName}"`
|
|
3963
|
+
);
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
if (newTemplate.state?.properties) {
|
|
3967
|
+
for (const instance of instanceList) {
|
|
3968
|
+
for (const prop of newTemplate.state.properties) {
|
|
3969
|
+
if (!instance.state.has(prop.key)) {
|
|
3970
|
+
warnings.push(
|
|
3971
|
+
`Instance ${instance.__holo_id}: missing field "${prop.key}" after migration`
|
|
3972
|
+
);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
this.templates.set(templateName, newTemplate);
|
|
3978
|
+
const result = {
|
|
3979
|
+
success: true,
|
|
3980
|
+
templateName,
|
|
3981
|
+
oldVersion,
|
|
3982
|
+
newVersion,
|
|
3983
|
+
diff,
|
|
3984
|
+
migrationChain,
|
|
3985
|
+
instancesMigrated: migrated,
|
|
3986
|
+
rollback: false,
|
|
3987
|
+
warnings
|
|
3988
|
+
};
|
|
3989
|
+
this.onReload(result);
|
|
3990
|
+
return result;
|
|
3991
|
+
} catch (err) {
|
|
3992
|
+
for (const instance of instanceList) {
|
|
3993
|
+
const snapshot = snapshots.get(instance.__holo_id);
|
|
3994
|
+
if (snapshot) {
|
|
3995
|
+
instance.state = snapshot;
|
|
3996
|
+
instance.version = oldVersion;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
const result = {
|
|
4000
|
+
success: false,
|
|
4001
|
+
templateName,
|
|
4002
|
+
oldVersion,
|
|
4003
|
+
newVersion,
|
|
4004
|
+
diff,
|
|
4005
|
+
migrationChain,
|
|
4006
|
+
instancesMigrated: 0,
|
|
4007
|
+
rollback: true,
|
|
4008
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4009
|
+
warnings
|
|
4010
|
+
};
|
|
4011
|
+
this.onRollback(result);
|
|
4012
|
+
return result;
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
/**
|
|
4016
|
+
* Get all registered templates.
|
|
4017
|
+
*/
|
|
4018
|
+
getTemplates() {
|
|
4019
|
+
return this.templates;
|
|
4020
|
+
}
|
|
4021
|
+
/**
|
|
4022
|
+
* Get all instances of a template.
|
|
4023
|
+
*/
|
|
4024
|
+
getInstances(templateName) {
|
|
4025
|
+
return this.instances.get(templateName) ?? [];
|
|
4026
|
+
}
|
|
4027
|
+
};
|
|
4028
|
+
|
|
4029
|
+
// src/runtime/NetworkPredictor.ts
|
|
4030
|
+
var NetworkPredictor = class {
|
|
4031
|
+
inputBuffer = [];
|
|
4032
|
+
stateBuffer = [];
|
|
4033
|
+
lastConfirmedSequence = -1;
|
|
4034
|
+
predictedState;
|
|
4035
|
+
confirmedState;
|
|
4036
|
+
rtt = 0;
|
|
4037
|
+
jitter = 0;
|
|
4038
|
+
lastPingTime = 0;
|
|
4039
|
+
constructor(initialState) {
|
|
4040
|
+
this.predictedState = { ...initialState };
|
|
4041
|
+
this.confirmedState = { ...initialState };
|
|
4042
|
+
this.stateBuffer.push({ sequenceNumber: 0, state: { ...initialState } });
|
|
4043
|
+
}
|
|
4044
|
+
/**
|
|
4045
|
+
* Add a local input and predict the resulting state immediately.
|
|
4046
|
+
*/
|
|
4047
|
+
predict(input, applyFn) {
|
|
4048
|
+
const seq = ++this.lastConfirmedSequence;
|
|
4049
|
+
const networkInput = {
|
|
4050
|
+
sequenceNumber: seq,
|
|
4051
|
+
timestamp: performance.now(),
|
|
4052
|
+
data: input
|
|
4053
|
+
};
|
|
4054
|
+
this.inputBuffer.push(networkInput);
|
|
4055
|
+
applyFn(this.predictedState, input);
|
|
4056
|
+
this.stateBuffer.push({
|
|
4057
|
+
sequenceNumber: seq,
|
|
4058
|
+
state: JSON.parse(JSON.stringify(this.predictedState))
|
|
4059
|
+
});
|
|
4060
|
+
if (this.stateBuffer.length > 120) {
|
|
4061
|
+
this.stateBuffer.shift();
|
|
4062
|
+
}
|
|
4063
|
+
return this.predictedState;
|
|
4064
|
+
}
|
|
4065
|
+
/**
|
|
4066
|
+
* Reconcile local state with authoritative server state.
|
|
4067
|
+
*/
|
|
4068
|
+
reconcile(serverState, applyFn) {
|
|
4069
|
+
this.confirmedState = JSON.parse(JSON.stringify(serverState.state));
|
|
4070
|
+
this.inputBuffer = this.inputBuffer.filter(
|
|
4071
|
+
(i) => i.sequenceNumber > serverState.sequenceNumber
|
|
4072
|
+
);
|
|
4073
|
+
const newState = JSON.parse(JSON.stringify(this.confirmedState));
|
|
4074
|
+
for (const input of this.inputBuffer) {
|
|
4075
|
+
applyFn(newState, input.data);
|
|
4076
|
+
}
|
|
4077
|
+
this.predictedState = newState;
|
|
4078
|
+
return this.predictedState;
|
|
4079
|
+
}
|
|
4080
|
+
/**
|
|
4081
|
+
* Update network metrics for adaptive prediction.
|
|
4082
|
+
*/
|
|
4083
|
+
updateMetrics(serverTimestamp) {
|
|
4084
|
+
const now = performance.now();
|
|
4085
|
+
const currentRtt = now - serverTimestamp;
|
|
4086
|
+
this.jitter = Math.abs(currentRtt - this.rtt) * 0.1 + this.jitter * 0.9;
|
|
4087
|
+
this.rtt = currentRtt * 0.1 + this.rtt * 0.9;
|
|
4088
|
+
}
|
|
4089
|
+
getPredictionHorizon() {
|
|
4090
|
+
return this.rtt / 2 + this.jitter * 2;
|
|
4091
|
+
}
|
|
4092
|
+
getPredictedState() {
|
|
4093
|
+
return this.predictedState;
|
|
4094
|
+
}
|
|
4095
|
+
};
|
|
4096
|
+
|
|
4097
|
+
// src/runtime/MovementPredictor.ts
|
|
4098
|
+
var MovementPredictor = class {
|
|
4099
|
+
lastPosition = { x: 0, y: 0, z: 0 };
|
|
4100
|
+
velocity = { x: 0, y: 0, z: 0 };
|
|
4101
|
+
history = [];
|
|
4102
|
+
maxHistory = 60;
|
|
4103
|
+
// 1 second at 60fps
|
|
4104
|
+
intent = null;
|
|
4105
|
+
/**
|
|
4106
|
+
* Convert Vector3 to tuple for consistent handling
|
|
4107
|
+
*/
|
|
4108
|
+
toTuple(v) {
|
|
4109
|
+
return Array.isArray(v) ? [v[0], v[1], v[2]] : [v.x, v.y, v.z];
|
|
4110
|
+
}
|
|
4111
|
+
/**
|
|
4112
|
+
* Update internal state with current player transform
|
|
4113
|
+
*/
|
|
4114
|
+
update(position, dt) {
|
|
4115
|
+
const currentPos = this.toTuple(position);
|
|
4116
|
+
if (dt > 0) {
|
|
4117
|
+
const lastPos = this.toTuple(this.lastPosition);
|
|
4118
|
+
this.velocity = {
|
|
4119
|
+
x: (currentPos[0] - lastPos[0]) / dt,
|
|
4120
|
+
y: (currentPos[1] - lastPos[1]) / dt,
|
|
4121
|
+
z: (currentPos[2] - lastPos[2]) / dt
|
|
4122
|
+
};
|
|
4123
|
+
}
|
|
4124
|
+
this.lastPosition = { x: currentPos[0], y: currentPos[1], z: currentPos[2] };
|
|
4125
|
+
this.history.push({ x: currentPos[0], y: currentPos[1], z: currentPos[2] });
|
|
4126
|
+
if (this.history.length > this.maxHistory) {
|
|
4127
|
+
this.history.shift();
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
/**
|
|
4131
|
+
* Set the player's current intent signal (Tier 3)
|
|
4132
|
+
*/
|
|
4133
|
+
setIntent(intent) {
|
|
4134
|
+
this.intent = intent;
|
|
4135
|
+
}
|
|
4136
|
+
/**
|
|
4137
|
+
* Tier 1: Linear Extrapolation
|
|
4138
|
+
*/
|
|
4139
|
+
predictLinear(lookahead) {
|
|
4140
|
+
const lastPosTuple = this.toTuple(this.lastPosition);
|
|
4141
|
+
const velTuple = this.toTuple(this.velocity);
|
|
4142
|
+
return [
|
|
4143
|
+
lastPosTuple[0] + velTuple[0] * lookahead,
|
|
4144
|
+
lastPosTuple[1] + velTuple[1] * lookahead,
|
|
4145
|
+
lastPosTuple[2] + velTuple[2] * lookahead
|
|
4146
|
+
];
|
|
4147
|
+
}
|
|
4148
|
+
/**
|
|
4149
|
+
* Tier 2: Non-linear pathing (Bezier-based extrapolation)
|
|
4150
|
+
* Analyzes path curvature to predict smooth turns.
|
|
4151
|
+
*/
|
|
4152
|
+
predictRecurrent(lookahead) {
|
|
4153
|
+
if (this.history.length < 5) return this.predictLinear(lookahead);
|
|
4154
|
+
const p2 = this.toTuple(this.history[this.history.length - 1]);
|
|
4155
|
+
const p1 = this.toTuple(this.history[this.history.length - 3]);
|
|
4156
|
+
const p0 = this.toTuple(this.history[this.history.length - 5]);
|
|
4157
|
+
const v1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
|
|
4158
|
+
const v0 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
|
|
4159
|
+
const acc = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
|
|
4160
|
+
return [
|
|
4161
|
+
p2[0] + v1[0] * lookahead + 0.5 * acc[0] * lookahead ** 2,
|
|
4162
|
+
p2[1] + v1[1] * lookahead + 0.5 * acc[1] * lookahead ** 2,
|
|
4163
|
+
p2[2] + v1[2] * lookahead + 0.5 * acc[2] * lookahead ** 2
|
|
4164
|
+
];
|
|
4165
|
+
}
|
|
4166
|
+
/**
|
|
4167
|
+
* Tier 3: Intent-based biasing
|
|
4168
|
+
*/
|
|
4169
|
+
predictIntent(lookahead) {
|
|
4170
|
+
const recurrent = this.predictRecurrent(lookahead);
|
|
4171
|
+
if (!this.intent) return recurrent;
|
|
4172
|
+
const targetPos = this.intent.target;
|
|
4173
|
+
const weight = this.intent.weight;
|
|
4174
|
+
const targetPosTuple = this.toTuple(targetPos);
|
|
4175
|
+
return [
|
|
4176
|
+
recurrent[0] * (1 - weight) + targetPosTuple[0] * weight,
|
|
4177
|
+
recurrent[1] * (1 - weight) + targetPosTuple[1] * weight,
|
|
4178
|
+
recurrent[2] * (1 - weight) + targetPosTuple[2] * weight
|
|
4179
|
+
];
|
|
4180
|
+
}
|
|
4181
|
+
/**
|
|
4182
|
+
* Get predictive windows for asset pre-fetching.
|
|
4183
|
+
* Combines all tiers into a prioritized set of windows.
|
|
4184
|
+
*/
|
|
4185
|
+
getPredictiveWindows(lookaheadSeconds) {
|
|
4186
|
+
const windows = [];
|
|
4187
|
+
windows.push({
|
|
4188
|
+
center: this.toTuple(this.lastPosition),
|
|
4189
|
+
radius: 10,
|
|
4190
|
+
likelihood: 1
|
|
4191
|
+
});
|
|
4192
|
+
const speed = Math.sqrt(
|
|
4193
|
+
(this.velocity.x ?? 0) ** 2 + (this.velocity.y ?? 0) ** 2 + (this.velocity.z ?? 0) ** 2
|
|
4194
|
+
);
|
|
4195
|
+
if (speed > 0.5) {
|
|
4196
|
+
windows.push({
|
|
4197
|
+
center: this.predictLinear(lookaheadSeconds),
|
|
4198
|
+
radius: speed * lookaheadSeconds * 0.3 + 5,
|
|
4199
|
+
likelihood: 0.9
|
|
4200
|
+
});
|
|
4201
|
+
const ensembleCenter = this.predictIntent(lookaheadSeconds);
|
|
4202
|
+
windows.push({
|
|
4203
|
+
center: ensembleCenter,
|
|
4204
|
+
radius: speed * lookaheadSeconds * 0.6 + 8,
|
|
4205
|
+
likelihood: 0.7
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4208
|
+
return windows;
|
|
4209
|
+
}
|
|
4210
|
+
};
|
|
4211
|
+
|
|
4212
|
+
// src/runtime/WebXRManager.ts
|
|
4213
|
+
var WebXRManager = class {
|
|
4214
|
+
session = null;
|
|
4215
|
+
referenceSpace = null;
|
|
4216
|
+
glBinding = null;
|
|
4217
|
+
projectionLayer = null;
|
|
4218
|
+
context;
|
|
4219
|
+
onSessionStart = null;
|
|
4220
|
+
onSessionEnd = null;
|
|
4221
|
+
onInputSourcesChange = null;
|
|
4222
|
+
constructor(context) {
|
|
4223
|
+
this.context = context;
|
|
4224
|
+
}
|
|
4225
|
+
/**
|
|
4226
|
+
* Check if WebXR is supported
|
|
4227
|
+
*/
|
|
4228
|
+
static async isSupported() {
|
|
4229
|
+
if (typeof navigator !== "undefined" && "xr" in navigator) {
|
|
4230
|
+
return await navigator.xr.isSessionSupported("immersive-vr");
|
|
4231
|
+
}
|
|
4232
|
+
return false;
|
|
4233
|
+
}
|
|
4234
|
+
/**
|
|
4235
|
+
* Instance method to check if a specific session mode is supported.
|
|
4236
|
+
*/
|
|
4237
|
+
async isSessionSupported(mode = "immersive-vr") {
|
|
4238
|
+
if (typeof navigator !== "undefined" && "xr" in navigator) {
|
|
4239
|
+
return await navigator.xr.isSessionSupported(mode);
|
|
4240
|
+
}
|
|
4241
|
+
return false;
|
|
4242
|
+
}
|
|
4243
|
+
/**
|
|
4244
|
+
* Trigger haptic pulse on a controller
|
|
4245
|
+
*/
|
|
4246
|
+
triggerHaptic(hand, intensity, duration) {
|
|
4247
|
+
if (!this.session) return;
|
|
4248
|
+
for (const source of this.session.inputSources) {
|
|
4249
|
+
if (source.handedness === hand && source.gamepad) {
|
|
4250
|
+
const actuators = source.gamepad.hapticActuators;
|
|
4251
|
+
if (actuators && actuators.length > 0) {
|
|
4252
|
+
actuators[0].pulse(intensity, duration);
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* Request an immersive VR session
|
|
4259
|
+
*/
|
|
4260
|
+
async requestSession(config = {}) {
|
|
4261
|
+
if (this.session) {
|
|
4262
|
+
console.warn("WebXR session already active");
|
|
4263
|
+
return this.session;
|
|
4264
|
+
}
|
|
4265
|
+
const sessionInit = {
|
|
4266
|
+
requiredFeatures: ["local-floor"],
|
|
4267
|
+
optionalFeatures: ["hand-tracking", "layers", ...config.features || []]
|
|
4268
|
+
};
|
|
4269
|
+
try {
|
|
4270
|
+
this.session = await navigator.xr.requestSession(
|
|
4271
|
+
"immersive-vr",
|
|
4272
|
+
sessionInit
|
|
4273
|
+
);
|
|
4274
|
+
this.session.addEventListener("end", this.handleSessionEnd);
|
|
4275
|
+
this.session.addEventListener(
|
|
4276
|
+
"inputsourceschange",
|
|
4277
|
+
this.handleInputSourcesChange
|
|
4278
|
+
);
|
|
4279
|
+
const g = globalThis;
|
|
4280
|
+
if (typeof g.XRWebGPUBinding !== "undefined") {
|
|
4281
|
+
this.glBinding = new g.XRWebGPUBinding(this.session, this.context.device);
|
|
4282
|
+
} else {
|
|
4283
|
+
console.warn("XRWebGPUBinding not found. Rendering may fail.");
|
|
4284
|
+
}
|
|
4285
|
+
this.referenceSpace = await this.session.requestReferenceSpace("local-floor");
|
|
4286
|
+
if (this.glBinding) {
|
|
4287
|
+
this.projectionLayer = this.glBinding.createProjectionLayer({
|
|
4288
|
+
colorFormat: this.context.format,
|
|
4289
|
+
depthStencilFormat: "depth24plus"
|
|
4290
|
+
});
|
|
4291
|
+
this.session.updateRenderState({ layers: [this.projectionLayer] });
|
|
4292
|
+
}
|
|
4293
|
+
this.onSessionStart?.(this.session);
|
|
4294
|
+
return this.session;
|
|
4295
|
+
} catch (error) {
|
|
4296
|
+
console.error("Failed to start WebXR session:", error);
|
|
4297
|
+
throw error;
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
/**
|
|
4301
|
+
* Set a callback to be invoked each XR frame.
|
|
4302
|
+
*/
|
|
4303
|
+
setAnimationLoop(callback) {
|
|
4304
|
+
this.animationLoopCallback = callback;
|
|
4305
|
+
if (this.session && callback) {
|
|
4306
|
+
const loop = (time, frame) => {
|
|
4307
|
+
if (this.animationLoopCallback) {
|
|
4308
|
+
this.animationLoopCallback(time, frame);
|
|
4309
|
+
this.session?.requestAnimationFrame?.(loop);
|
|
4310
|
+
}
|
|
4311
|
+
};
|
|
4312
|
+
this.session.requestAnimationFrame?.(loop);
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
animationLoopCallback = null;
|
|
4316
|
+
/**
|
|
4317
|
+
* End the current session
|
|
4318
|
+
*/
|
|
4319
|
+
async endSession() {
|
|
4320
|
+
if (this.session) {
|
|
4321
|
+
this.animationLoopCallback = null;
|
|
4322
|
+
await this.session.end();
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
/**
|
|
4326
|
+
* Get the current XRSession
|
|
4327
|
+
*/
|
|
4328
|
+
getSession() {
|
|
4329
|
+
return this.session;
|
|
4330
|
+
}
|
|
4331
|
+
/**
|
|
4332
|
+
* Get the current Reference Space
|
|
4333
|
+
*/
|
|
4334
|
+
getReferenceSpace() {
|
|
4335
|
+
return this.referenceSpace;
|
|
4336
|
+
}
|
|
4337
|
+
/**
|
|
4338
|
+
* Get the WebGPU Binding
|
|
4339
|
+
*/
|
|
4340
|
+
getBinding() {
|
|
4341
|
+
return this.glBinding;
|
|
4342
|
+
}
|
|
4343
|
+
/**
|
|
4344
|
+
* Get the active Projection Layer
|
|
4345
|
+
*/
|
|
4346
|
+
getProjectionLayer() {
|
|
4347
|
+
return this.projectionLayer;
|
|
4348
|
+
}
|
|
4349
|
+
// ==========================================================================
|
|
4350
|
+
// HANDLERS
|
|
4351
|
+
// ==========================================================================
|
|
4352
|
+
handleSessionEnd = () => {
|
|
4353
|
+
this.session = null;
|
|
4354
|
+
this.referenceSpace = null;
|
|
4355
|
+
this.glBinding = null;
|
|
4356
|
+
this.projectionLayer = null;
|
|
4357
|
+
this.onSessionEnd?.();
|
|
4358
|
+
};
|
|
4359
|
+
handleInputSourcesChange = (event) => {
|
|
4360
|
+
this.onInputSourcesChange?.(event.added, event.removed);
|
|
4361
|
+
};
|
|
4362
|
+
};
|
|
4363
|
+
|
|
4364
|
+
// src/runtime/KeyboardSystem.ts
|
|
4365
|
+
var KeyboardSystem = class _KeyboardSystem {
|
|
4366
|
+
runtime;
|
|
4367
|
+
focusedInputId = null;
|
|
4368
|
+
cursorIndex = 0;
|
|
4369
|
+
static CHAR_WIDTH = 0.048;
|
|
4370
|
+
// Monospace approximation for fontSize 0.08
|
|
4371
|
+
static START_X = -0.2;
|
|
4372
|
+
constructor(runtime) {
|
|
4373
|
+
this.runtime = runtime;
|
|
4374
|
+
this.setupListeners();
|
|
4375
|
+
}
|
|
4376
|
+
setupListeners() {
|
|
4377
|
+
}
|
|
4378
|
+
handleEvent(event, payload) {
|
|
4379
|
+
if (event === "ui_press_end") {
|
|
4380
|
+
const nodeId = payload.nodeId;
|
|
4381
|
+
if (nodeId.includes("_key_")) {
|
|
4382
|
+
const key = nodeId.split("_key_")[1];
|
|
4383
|
+
this.handleKeyPress(key);
|
|
4384
|
+
} else if (nodeId.includes("input")) {
|
|
4385
|
+
if (this.focusedInputId && this.focusedInputId !== nodeId) {
|
|
4386
|
+
this.setCursorVisible(this.focusedInputId, false);
|
|
4387
|
+
}
|
|
4388
|
+
this.focusedInputId = nodeId;
|
|
4389
|
+
const text = this.getText(nodeId);
|
|
4390
|
+
this.cursorIndex = text.length;
|
|
4391
|
+
this.setCursorVisible(nodeId, true);
|
|
4392
|
+
this.updateCursorVisuals(nodeId);
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
handleKeyPress(key) {
|
|
4397
|
+
if (!this.focusedInputId) return;
|
|
4398
|
+
let currentText = this.getText(this.focusedInputId);
|
|
4399
|
+
switch (key) {
|
|
4400
|
+
case "LEFT":
|
|
4401
|
+
this.cursorIndex = Math.max(0, this.cursorIndex - 1);
|
|
4402
|
+
break;
|
|
4403
|
+
case "RIGHT":
|
|
4404
|
+
this.cursorIndex = Math.min(currentText.length, this.cursorIndex + 1);
|
|
4405
|
+
break;
|
|
4406
|
+
case "BACKSPACE":
|
|
4407
|
+
if (this.cursorIndex > 0) {
|
|
4408
|
+
const before = currentText.slice(0, this.cursorIndex - 1);
|
|
4409
|
+
const after = currentText.slice(this.cursorIndex);
|
|
4410
|
+
currentText = before + after;
|
|
4411
|
+
this.cursorIndex--;
|
|
4412
|
+
}
|
|
4413
|
+
break;
|
|
4414
|
+
case "SPACE":
|
|
4415
|
+
currentText = this.insertAtCursor(currentText, " ");
|
|
4416
|
+
this.cursorIndex++;
|
|
4417
|
+
break;
|
|
4418
|
+
default:
|
|
4419
|
+
if (key.length === 1) {
|
|
4420
|
+
currentText = this.insertAtCursor(currentText, key);
|
|
4421
|
+
this.cursorIndex++;
|
|
4422
|
+
}
|
|
4423
|
+
break;
|
|
4424
|
+
}
|
|
4425
|
+
this.updateInputState(this.focusedInputId, currentText);
|
|
4426
|
+
}
|
|
4427
|
+
insertAtCursor(text, char) {
|
|
4428
|
+
const before = text.slice(0, this.cursorIndex);
|
|
4429
|
+
const after = text.slice(this.cursorIndex);
|
|
4430
|
+
return before + char + after;
|
|
4431
|
+
}
|
|
4432
|
+
getText(nodeId) {
|
|
4433
|
+
const instance = this.runtime.findInstanceById(nodeId);
|
|
4434
|
+
return String(instance && instance.node.properties.text || "");
|
|
4435
|
+
}
|
|
4436
|
+
updateInputState(nodeId, text) {
|
|
4437
|
+
this.runtime.updateNodeProperty(nodeId, "text", text);
|
|
4438
|
+
const instance = this.runtime.findInstanceById(nodeId);
|
|
4439
|
+
if (instance && instance.children) {
|
|
4440
|
+
const textChild = instance.children.find(
|
|
4441
|
+
(c) => c.node.type === "text"
|
|
4442
|
+
);
|
|
4443
|
+
if (textChild) {
|
|
4444
|
+
this.runtime.updateNodeProperty(textChild.node.id, "text", text);
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
this.updateCursorVisuals(nodeId);
|
|
4448
|
+
}
|
|
4449
|
+
setCursorVisible(nodeId, visible) {
|
|
4450
|
+
const instance = this.runtime.findInstanceById(nodeId);
|
|
4451
|
+
if (instance && instance.children) {
|
|
4452
|
+
const cursorChild = instance.children.find(
|
|
4453
|
+
(c) => c.node.properties.tag === "cursor"
|
|
4454
|
+
);
|
|
4455
|
+
if (cursorChild) {
|
|
4456
|
+
this.runtime.updateNodeProperty(cursorChild.node.id, "visible", visible);
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
updateCursorVisuals(nodeId) {
|
|
4461
|
+
const instance = this.runtime.findInstanceById(nodeId);
|
|
4462
|
+
if (!instance || !instance.children) return;
|
|
4463
|
+
const cursorChild = instance.children.find(
|
|
4464
|
+
(c) => c.node.properties.tag === "cursor"
|
|
4465
|
+
);
|
|
4466
|
+
if (cursorChild) {
|
|
4467
|
+
const newX = _KeyboardSystem.START_X + this.cursorIndex * _KeyboardSystem.CHAR_WIDTH;
|
|
4468
|
+
const currentPos = cursorChild.node.properties.position || { x: 0, y: 0, z: 0 };
|
|
4469
|
+
const newPos = { ...currentPos, x: newX };
|
|
4470
|
+
this.runtime.updateNodeProperty(cursorChild.node.id, "position", newPos);
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
};
|
|
4474
|
+
|
|
4475
|
+
// src/runtime/HandMenuSystem.ts
|
|
4476
|
+
var createUIButton = (id, props) => ({ id, type: "button", properties: props });
|
|
4477
|
+
var createUIPanel = (id, props, children) => ({ id, type: "panel", properties: props, children });
|
|
4478
|
+
var HandMenuSystem = class {
|
|
4479
|
+
constructor(runtime, engine) {
|
|
4480
|
+
this.runtime = runtime;
|
|
4481
|
+
this.transitions = new TransitionSystem(engine || new AnimationEngine());
|
|
4482
|
+
}
|
|
4483
|
+
runtime;
|
|
4484
|
+
menuNodeId = null;
|
|
4485
|
+
menuNode = null;
|
|
4486
|
+
isMenuVisible = false;
|
|
4487
|
+
isTransitioning = false;
|
|
4488
|
+
lastToggleTime = 0;
|
|
4489
|
+
transitions;
|
|
4490
|
+
update(delta) {
|
|
4491
|
+
this.transitions.update(delta);
|
|
4492
|
+
const leftHand = this.runtime.vrContext?.hands?.left;
|
|
4493
|
+
if (!leftHand) return;
|
|
4494
|
+
if (this.checkPalmFacingUser(leftHand)) {
|
|
4495
|
+
if (!this.isMenuVisible && !this.isTransitioning && Date.now() - this.lastToggleTime > 1e3) {
|
|
4496
|
+
this.showMenu(leftHand);
|
|
4497
|
+
}
|
|
4498
|
+
} else {
|
|
4499
|
+
if (this.isMenuVisible && !this.isTransitioning) {
|
|
4500
|
+
this.hideMenu();
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
// Check if palm normal points to headset
|
|
4505
|
+
checkPalmFacingUser(_hand) {
|
|
4506
|
+
return false;
|
|
4507
|
+
}
|
|
4508
|
+
showMenu(hand) {
|
|
4509
|
+
if (this.menuNodeId) return;
|
|
4510
|
+
const menuId = "hand_menu_" + Date.now();
|
|
4511
|
+
const menu = createUIPanel(
|
|
4512
|
+
menuId,
|
|
4513
|
+
{
|
|
4514
|
+
width: 0.2,
|
|
4515
|
+
height: 0.15,
|
|
4516
|
+
color: "#1a1a1a"
|
|
4517
|
+
},
|
|
4518
|
+
[
|
|
4519
|
+
createUIButton(`${menuId}_btn1`, {
|
|
4520
|
+
text: "Home",
|
|
4521
|
+
position: [0, 0.03, 0.01],
|
|
4522
|
+
width: 0.18,
|
|
4523
|
+
height: 0.04,
|
|
4524
|
+
data: { action: "home" }
|
|
4525
|
+
}),
|
|
4526
|
+
createUIButton(`${menuId}_btn2`, {
|
|
4527
|
+
text: "Settings",
|
|
4528
|
+
position: [0, -0.03, 0.01],
|
|
4529
|
+
width: 0.18,
|
|
4530
|
+
height: 0.04,
|
|
4531
|
+
data: { action: "settings" }
|
|
4532
|
+
})
|
|
4533
|
+
]
|
|
4534
|
+
);
|
|
4535
|
+
menu.properties.position = {
|
|
4536
|
+
x: hand.position.x,
|
|
4537
|
+
y: hand.position.y + 0.1,
|
|
4538
|
+
z: hand.position.z
|
|
4539
|
+
};
|
|
4540
|
+
menu.properties.opacity = 0;
|
|
4541
|
+
menu.properties.scale = 0;
|
|
4542
|
+
this.runtime.mountObject(menu);
|
|
4543
|
+
this.menuNodeId = menuId;
|
|
4544
|
+
this.menuNode = menu;
|
|
4545
|
+
this.isMenuVisible = true;
|
|
4546
|
+
this.isTransitioning = true;
|
|
4547
|
+
this.lastToggleTime = Date.now();
|
|
4548
|
+
this.transitions.popIn(
|
|
4549
|
+
menuId,
|
|
4550
|
+
(s) => {
|
|
4551
|
+
if (this.menuNode?.properties) this.menuNode.properties.scale = s;
|
|
4552
|
+
},
|
|
4553
|
+
(o) => {
|
|
4554
|
+
if (this.menuNode?.properties) this.menuNode.properties.opacity = o;
|
|
4555
|
+
},
|
|
4556
|
+
{
|
|
4557
|
+
duration: 0.35,
|
|
4558
|
+
onComplete: () => {
|
|
4559
|
+
this.isTransitioning = false;
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
);
|
|
4563
|
+
}
|
|
4564
|
+
hideMenu() {
|
|
4565
|
+
if (!this.menuNodeId || !this.menuNode) return;
|
|
4566
|
+
this.isTransitioning = true;
|
|
4567
|
+
const nodeIdToRemove = this.menuNodeId;
|
|
4568
|
+
this.transitions.popOut(
|
|
4569
|
+
nodeIdToRemove,
|
|
4570
|
+
(s) => {
|
|
4571
|
+
if (this.menuNode?.properties) this.menuNode.properties.scale = s;
|
|
4572
|
+
},
|
|
4573
|
+
(o) => {
|
|
4574
|
+
if (this.menuNode?.properties) this.menuNode.properties.opacity = o;
|
|
4575
|
+
},
|
|
4576
|
+
{
|
|
4577
|
+
duration: 0.25,
|
|
4578
|
+
onComplete: () => {
|
|
4579
|
+
this.runtime.unmountObject(nodeIdToRemove);
|
|
4580
|
+
this.isTransitioning = false;
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
);
|
|
4584
|
+
this.menuNodeId = null;
|
|
4585
|
+
this.menuNode = null;
|
|
4586
|
+
this.isMenuVisible = false;
|
|
4587
|
+
this.lastToggleTime = Date.now();
|
|
4588
|
+
}
|
|
4589
|
+
};
|
|
4590
|
+
|
|
4591
|
+
// src/runtime/HoloScriptPlusRuntime.ts
|
|
4592
|
+
var StateSync = class {
|
|
4593
|
+
constructor(_options) {
|
|
4594
|
+
}
|
|
4595
|
+
getInterpolatedState(_id) {
|
|
4596
|
+
return null;
|
|
4597
|
+
}
|
|
4598
|
+
};
|
|
4599
|
+
var HoloScriptPlusRuntimeImpl = class {
|
|
4600
|
+
ast;
|
|
4601
|
+
options;
|
|
4602
|
+
state;
|
|
4603
|
+
evaluator;
|
|
4604
|
+
builtins;
|
|
4605
|
+
traitRegistry;
|
|
4606
|
+
rootInstance = null;
|
|
4607
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
4608
|
+
templates = /* @__PURE__ */ new Map();
|
|
4609
|
+
updateLoopId = null;
|
|
4610
|
+
lastUpdateTime = 0;
|
|
4611
|
+
companions;
|
|
4612
|
+
networkSync;
|
|
4613
|
+
mounted = false;
|
|
4614
|
+
scaleMultiplier = 1;
|
|
4615
|
+
chunkLoader = null;
|
|
4616
|
+
hotReloader;
|
|
4617
|
+
networkPredictor = null;
|
|
4618
|
+
movementPredictor;
|
|
4619
|
+
// Optimization: Flat list for iteration
|
|
4620
|
+
_flatEntities = [];
|
|
4621
|
+
// AI Copilot integration
|
|
4622
|
+
_copilot = null;
|
|
4623
|
+
/**
|
|
4624
|
+
* Set the AI Copilot for generate directive processing.
|
|
4625
|
+
*/
|
|
4626
|
+
setCopilot(copilot) {
|
|
4627
|
+
this._copilot = copilot;
|
|
4628
|
+
}
|
|
4629
|
+
// VR context
|
|
4630
|
+
vrContext = {
|
|
4631
|
+
hands: {
|
|
4632
|
+
left: null,
|
|
4633
|
+
right: null
|
|
4634
|
+
},
|
|
4635
|
+
headset: {
|
|
4636
|
+
position: { x: 0, y: 1.6, z: 0 },
|
|
4637
|
+
rotation: { x: 0, y: 0, z: 0 }
|
|
4638
|
+
},
|
|
4639
|
+
controllers: {
|
|
4640
|
+
left: null,
|
|
4641
|
+
right: null
|
|
4642
|
+
}
|
|
4643
|
+
};
|
|
4644
|
+
// WebXR Manager
|
|
4645
|
+
webXrManager = null;
|
|
4646
|
+
isXRSessionActive = false;
|
|
4647
|
+
// Physics World
|
|
4648
|
+
physicsWorld;
|
|
4649
|
+
vrPhysicsBridge;
|
|
4650
|
+
debugDrawer = null;
|
|
4651
|
+
keyboardSystem;
|
|
4652
|
+
handMenuSystem;
|
|
4653
|
+
constructor(ast, options = {}) {
|
|
4654
|
+
this.ast = ast;
|
|
4655
|
+
this.options = options;
|
|
4656
|
+
console.log("[[TYPE OF PW]]", typeof PhysicsWorldImpl, PhysicsWorldImpl);
|
|
4657
|
+
this.physicsWorld = new PhysicsWorldImpl({
|
|
4658
|
+
gravity: { x: 0, y: -9.81, z: 0 },
|
|
4659
|
+
maxSubsteps: 2
|
|
4660
|
+
});
|
|
4661
|
+
this.vrPhysicsBridge = new VRPhysicsBridge(this.physicsWorld, (hand, intensity, duration) => {
|
|
4662
|
+
if (this.webXrManager) {
|
|
4663
|
+
this.webXrManager.triggerHaptic(hand, intensity, duration);
|
|
4664
|
+
}
|
|
4665
|
+
});
|
|
4666
|
+
if (options.renderer instanceof WebGPURenderer) {
|
|
4667
|
+
this.debugDrawer = new PhysicsDebugDrawer(this.physicsWorld, options.renderer);
|
|
4668
|
+
}
|
|
4669
|
+
this.keyboardSystem = new KeyboardSystem(this);
|
|
4670
|
+
this.handMenuSystem = new HandMenuSystem(this);
|
|
4671
|
+
this.on("physics_grab", (payload) => {
|
|
4672
|
+
const { nodeId, hand } = payload;
|
|
4673
|
+
const handBodyId = this.vrPhysicsBridge.getHandBodyId(hand);
|
|
4674
|
+
const objectBody = this.physicsWorld.getBody(nodeId);
|
|
4675
|
+
if (handBodyId && objectBody) {
|
|
4676
|
+
this.physicsWorld.createConstraint({
|
|
4677
|
+
type: "fixed",
|
|
4678
|
+
id: `grab_${handBodyId}_${nodeId}`,
|
|
4679
|
+
bodyA: handBodyId,
|
|
4680
|
+
bodyB: nodeId,
|
|
4681
|
+
pivotA: { x: 0, y: 0, z: 0 },
|
|
4682
|
+
// Center of hand
|
|
4683
|
+
pivotB: { x: 0, y: 0, z: 0 }
|
|
4684
|
+
// Should be relative offset
|
|
4685
|
+
// For simplicity, we snap center-to-center or need to calculate offset based on current positions
|
|
4686
|
+
});
|
|
4687
|
+
}
|
|
4688
|
+
});
|
|
4689
|
+
this.on("physics_release", (payload) => {
|
|
4690
|
+
const { nodeId, velocity } = payload;
|
|
4691
|
+
this.physicsWorld.removeConstraint(`grab_${nodeId}`);
|
|
4692
|
+
if (velocity) {
|
|
4693
|
+
const body = this.physicsWorld.getBody(nodeId);
|
|
4694
|
+
if (body) {
|
|
4695
|
+
body.velocity = { x: velocity[0], y: velocity[1], z: velocity[2] };
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
});
|
|
4699
|
+
this.on("ui_press_end", (payload) => {
|
|
4700
|
+
this.keyboardSystem.handleEvent("ui_press_end", payload);
|
|
4701
|
+
});
|
|
4702
|
+
this.on("physics_add_constraint", (payload) => {
|
|
4703
|
+
const { type, nodeId, axis, min, max, spring } = payload;
|
|
4704
|
+
this.physicsWorld.createConstraint({
|
|
4705
|
+
type: "slider",
|
|
4706
|
+
// Mapped to slider for prismatic capability
|
|
4707
|
+
id: `constraint_slider_${nodeId}`,
|
|
4708
|
+
bodyA: "WORLD_ANCHOR",
|
|
4709
|
+
// Placeholder for "Static Anchor at start pos"
|
|
4710
|
+
bodyB: nodeId,
|
|
4711
|
+
pivotA: { x: 0, y: 0, z: 0 },
|
|
4712
|
+
axisA: axis || { x: 0, y: 1, z: 0 },
|
|
4713
|
+
limits: { low: min || 0, high: max || 0 }
|
|
4714
|
+
});
|
|
4715
|
+
});
|
|
4716
|
+
if (options.renderer && options.renderer.context && options.vrEnabled) {
|
|
4717
|
+
}
|
|
4718
|
+
const isNetworked = ast.root.traits?.has("networked") || ast.root.directives?.some(
|
|
4719
|
+
(d) => d.type === "sync" || d.type === "networked"
|
|
4720
|
+
);
|
|
4721
|
+
const _syncId = isNetworked ? ast.root.id || "global_session" : void 0;
|
|
4722
|
+
this.state = createState({});
|
|
4723
|
+
this.traitRegistry = vrTraitRegistry;
|
|
4724
|
+
this.companions = options.companions || {};
|
|
4725
|
+
this.builtins = createBuiltins(this);
|
|
4726
|
+
this.networkSync = new StateSync({ interpolation: true });
|
|
4727
|
+
this.evaluator = new ExpressionEvaluator(
|
|
4728
|
+
// @ts-expect-error - TS2554 structural type mismatch
|
|
4729
|
+
this.state.getSnapshot(),
|
|
4730
|
+
this.builtins
|
|
4731
|
+
);
|
|
4732
|
+
this.initializeState();
|
|
4733
|
+
this.loadImports();
|
|
4734
|
+
this.movementPredictor = new MovementPredictor();
|
|
4735
|
+
if (isNetworked) {
|
|
4736
|
+
this.networkPredictor = new NetworkPredictor(this.state.getSnapshot());
|
|
4737
|
+
}
|
|
4738
|
+
this.hotReloader = new HotReloader({ devMode: true });
|
|
4739
|
+
this.hotReloader.setMigrationExecutor(async (instance, body) => {
|
|
4740
|
+
let nodeInstance = this.findInstanceById(instance.__holo_id);
|
|
4741
|
+
if (!nodeInstance && instance.templateName === "@program" && this.rootInstance) {
|
|
4742
|
+
nodeInstance = this.rootInstance;
|
|
4743
|
+
}
|
|
4744
|
+
if (nodeInstance) {
|
|
4745
|
+
this.executeMigrationCode(nodeInstance, body);
|
|
4746
|
+
}
|
|
4747
|
+
});
|
|
4748
|
+
const self = this;
|
|
4749
|
+
if (this.ast.version !== void 0) {
|
|
4750
|
+
this.hotReloader.registerTemplate({
|
|
4751
|
+
type: "template",
|
|
4752
|
+
name: "@program",
|
|
4753
|
+
version: this.ast.version || 0,
|
|
4754
|
+
migrations: this.ast.migrations || [],
|
|
4755
|
+
state: { properties: [] }
|
|
4756
|
+
});
|
|
4757
|
+
const stateBridge = this.createStateMapProxy();
|
|
4758
|
+
this.hotReloader.registerInstance({
|
|
4759
|
+
__holo_id: "root",
|
|
4760
|
+
templateName: "@program",
|
|
4761
|
+
get version() {
|
|
4762
|
+
return self.ast.version || 0;
|
|
4763
|
+
},
|
|
4764
|
+
set version(v) {
|
|
4765
|
+
self.ast.version = v;
|
|
4766
|
+
},
|
|
4767
|
+
state: stateBridge
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
const initialTemplates = this.findAllTemplates(this.ast.root);
|
|
4771
|
+
for (const [name, node] of initialTemplates) {
|
|
4772
|
+
this.templates.set(name, node);
|
|
4773
|
+
this.hotReloader.registerTemplate(node);
|
|
4774
|
+
}
|
|
4775
|
+
if (this.options.manifestUrl) {
|
|
4776
|
+
this.chunkLoader = new ChunkLoader(this, {
|
|
4777
|
+
manifestUrl: this.options.manifestUrl,
|
|
4778
|
+
baseUrl: this.options.baseUrl
|
|
4779
|
+
});
|
|
4780
|
+
this.chunkLoader.init();
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
// ==========================================================================
|
|
4784
|
+
// WEBXR INTEGRATION
|
|
4785
|
+
// ==========================================================================
|
|
4786
|
+
async enterVR() {
|
|
4787
|
+
if (!this.options.vrEnabled) {
|
|
4788
|
+
console.warn("VR is not enabled in runtime options");
|
|
4789
|
+
return;
|
|
4790
|
+
}
|
|
4791
|
+
const renderer = this.options.renderer;
|
|
4792
|
+
if (!renderer) {
|
|
4793
|
+
console.error("Cannot enter VR: No renderer found");
|
|
4794
|
+
return;
|
|
4795
|
+
}
|
|
4796
|
+
if (!this.webXrManager) {
|
|
4797
|
+
const context = renderer.getContext ? renderer.getContext() : renderer.context;
|
|
4798
|
+
if (!context) {
|
|
4799
|
+
console.error("WebGPU context not found on renderer");
|
|
4800
|
+
return;
|
|
4801
|
+
}
|
|
4802
|
+
const ManagerClass = this.options.webXrManagerClass || WebXRManager;
|
|
4803
|
+
this.webXrManager = new ManagerClass(context);
|
|
4804
|
+
this.webXrManager.onSessionStart = (session) => {
|
|
4805
|
+
this.isXRSessionActive = true;
|
|
4806
|
+
this.stopUpdateLoop();
|
|
4807
|
+
if (renderer.setXRSession) {
|
|
4808
|
+
renderer.setXRSession(
|
|
4809
|
+
session,
|
|
4810
|
+
this.webXrManager.getBinding(),
|
|
4811
|
+
this.webXrManager.getProjectionLayer()
|
|
4812
|
+
);
|
|
4813
|
+
}
|
|
4814
|
+
this.webXrManager.setAnimationLoop(this.xrLoop.bind(this));
|
|
4815
|
+
};
|
|
4816
|
+
this.webXrManager.onSessionEnd = () => {
|
|
4817
|
+
this.isXRSessionActive = false;
|
|
4818
|
+
if (renderer.setXRSession) {
|
|
4819
|
+
renderer.setXRSession(null, null, null);
|
|
4820
|
+
}
|
|
4821
|
+
this.startUpdateLoop();
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4824
|
+
if (await this.webXrManager.isSessionSupported("immersive-vr")) {
|
|
4825
|
+
await this.webXrManager.requestSession();
|
|
4826
|
+
} else {
|
|
4827
|
+
console.warn("WebXR not supported in this environment");
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* Main XR Loop - called by WebXRManager
|
|
4832
|
+
*/
|
|
4833
|
+
xrLoop(time, frame) {
|
|
4834
|
+
const now = performance.now();
|
|
4835
|
+
const delta = (now - this.lastUpdateTime) / 1e3;
|
|
4836
|
+
this.lastUpdateTime = now;
|
|
4837
|
+
this.updateVRInput(frame);
|
|
4838
|
+
this.update(delta);
|
|
4839
|
+
if (this.handMenuSystem) this.handMenuSystem.update(delta);
|
|
4840
|
+
const renderer = this.options.renderer;
|
|
4841
|
+
if (renderer?.renderXR) {
|
|
4842
|
+
renderer.renderXR(frame);
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
async exitVR() {
|
|
4846
|
+
if (this.webXrManager) {
|
|
4847
|
+
await this.webXrManager.endSession();
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
/**
|
|
4851
|
+
* Convert Quaternion to Euler Angles (YXZ sequence)
|
|
4852
|
+
*/
|
|
4853
|
+
quaternionToEuler(q) {
|
|
4854
|
+
const x = q[0], y = q[1], z = q[2], w = q[3];
|
|
4855
|
+
const t0 = 2 * (w * y - z * x);
|
|
4856
|
+
const ry = Math.asin(Math.max(-1, Math.min(1, t0)));
|
|
4857
|
+
const t1 = 2 * (w * x + y * z);
|
|
4858
|
+
const t2 = 1 - 2 * (x * x + y * y);
|
|
4859
|
+
const rx = Math.atan2(t1, t2);
|
|
4860
|
+
const t3 = 2 * (w * z + x * y);
|
|
4861
|
+
const t4 = 1 - 2 * (y * y + z * z);
|
|
4862
|
+
const rz = Math.atan2(t3, t4);
|
|
4863
|
+
return { x: rx, y: ry, z: rz };
|
|
4864
|
+
}
|
|
4865
|
+
updateVRInput(frame) {
|
|
4866
|
+
if (!this.isXRSessionActive || !this.webXrManager) return;
|
|
4867
|
+
const session = frame ? frame.session : this.webXrManager.getSession();
|
|
4868
|
+
const refSpace = this.webXrManager.getReferenceSpace();
|
|
4869
|
+
if (!session || !refSpace) return;
|
|
4870
|
+
if (frame) {
|
|
4871
|
+
const viewerPose = frame.getViewerPose(refSpace);
|
|
4872
|
+
if (viewerPose) {
|
|
4873
|
+
const { position, orientation } = viewerPose.transform;
|
|
4874
|
+
this.vrContext.headset.position = { x: position.x, y: position.y, z: position.z };
|
|
4875
|
+
const eulerH = this.quaternionToEuler([
|
|
4876
|
+
orientation.x,
|
|
4877
|
+
orientation.y,
|
|
4878
|
+
orientation.z,
|
|
4879
|
+
orientation.w
|
|
4880
|
+
]);
|
|
4881
|
+
this.vrContext.headset.rotation = { x: eulerH[0], y: eulerH[1], z: eulerH[2] };
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
for (const source of session.inputSources) {
|
|
4885
|
+
if (!source.gripSpace) continue;
|
|
4886
|
+
let pose;
|
|
4887
|
+
if (frame) {
|
|
4888
|
+
pose = frame.getPose(source.gripSpace, refSpace) ?? void 0;
|
|
4889
|
+
}
|
|
4890
|
+
if (pose) {
|
|
4891
|
+
const { position, orientation } = pose.transform;
|
|
4892
|
+
const handSide = source.handedness;
|
|
4893
|
+
const handData = {
|
|
4894
|
+
id: `${handSide}_hand`,
|
|
4895
|
+
grip: 0,
|
|
4896
|
+
trigger: 0,
|
|
4897
|
+
position: [position.x, position.y, position.z],
|
|
4898
|
+
rotation: this.quaternionToEuler([
|
|
4899
|
+
orientation.x,
|
|
4900
|
+
orientation.y,
|
|
4901
|
+
orientation.z,
|
|
4902
|
+
orientation.w
|
|
4903
|
+
]),
|
|
4904
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
4905
|
+
// Not provided by WebXR directly without previous frame diff
|
|
4906
|
+
pinchStrength: 0,
|
|
4907
|
+
gripStrength: 0
|
|
4908
|
+
};
|
|
4909
|
+
if (source.gamepad) {
|
|
4910
|
+
if (source.gamepad.buttons.length > 0) {
|
|
4911
|
+
handData.pinchStrength = source.gamepad.buttons[0].value;
|
|
4912
|
+
}
|
|
4913
|
+
if (source.gamepad.buttons.length > 1) {
|
|
4914
|
+
handData.gripStrength = source.gamepad.buttons[1].value;
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
const prevHand = handSide === "left" ? this.vrContext.hands.left : this.vrContext.hands.right;
|
|
4918
|
+
if (prevHand) {
|
|
4919
|
+
}
|
|
4920
|
+
if (handSide === "left") {
|
|
4921
|
+
this.vrContext.hands.left = handData;
|
|
4922
|
+
} else if (handSide === "right") {
|
|
4923
|
+
this.vrContext.hands.right = handData;
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
// ==========================================================================
|
|
4929
|
+
// INITIALIZATION
|
|
4930
|
+
// ==========================================================================
|
|
4931
|
+
initializeState() {
|
|
4932
|
+
const stateDirective = (this.ast.root.directives || []).find(
|
|
4933
|
+
(d) => d.type === "state"
|
|
4934
|
+
);
|
|
4935
|
+
if (stateDirective && stateDirective.type === "state") {
|
|
4936
|
+
this.state.update(stateDirective.body);
|
|
4937
|
+
}
|
|
4938
|
+
}
|
|
4939
|
+
loadImports() {
|
|
4940
|
+
for (const imp of this.ast.imports || []) {
|
|
4941
|
+
const alias = imp.alias || imp.source || imp.path;
|
|
4942
|
+
if (this.companions[alias]) {
|
|
4943
|
+
continue;
|
|
4944
|
+
}
|
|
4945
|
+
console.warn(
|
|
4946
|
+
`Import ${imp.path || imp.source} not found. Provide via companions option.`
|
|
4947
|
+
);
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
// ==========================================================================
|
|
4951
|
+
// MOUNTING
|
|
4952
|
+
// ==========================================================================
|
|
4953
|
+
mount(container) {
|
|
4954
|
+
if (this.mounted) {
|
|
4955
|
+
console.warn("Runtime already mounted");
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
this.mounted = true;
|
|
4959
|
+
this.rootInstance = this.instantiateNode(this.ast.root, null);
|
|
4960
|
+
if (this.options.renderer && this.rootInstance) {
|
|
4961
|
+
this.options.renderer.appendChild(container, this.rootInstance.renderedNode);
|
|
4962
|
+
}
|
|
4963
|
+
this.callLifecycle(this.rootInstance, "on_mount");
|
|
4964
|
+
this.startUpdateLoop();
|
|
4965
|
+
}
|
|
4966
|
+
unmount() {
|
|
4967
|
+
if (!this.mounted) return;
|
|
4968
|
+
this.stopUpdateLoop();
|
|
4969
|
+
if (this.rootInstance) {
|
|
4970
|
+
this.callLifecycle(this.rootInstance, "on_unmount");
|
|
4971
|
+
this.destroyInstance(this.rootInstance);
|
|
4972
|
+
}
|
|
4973
|
+
this.rootInstance = null;
|
|
4974
|
+
this.mounted = false;
|
|
4975
|
+
}
|
|
4976
|
+
/**
|
|
4977
|
+
* Scans the AST for all template definitions, including nested ones.
|
|
4978
|
+
*/
|
|
4979
|
+
findAllTemplates(node, templates = /* @__PURE__ */ new Map()) {
|
|
4980
|
+
if (node.type === "template" && node.name) {
|
|
4981
|
+
templates.set(node.name, node);
|
|
4982
|
+
}
|
|
4983
|
+
if (node.children) {
|
|
4984
|
+
for (const child of node.children) {
|
|
4985
|
+
this.findAllTemplates(child, templates);
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
if (node.type === "composition") {
|
|
4989
|
+
const comp = node;
|
|
4990
|
+
if (comp.children) {
|
|
4991
|
+
for (const child of comp.children) {
|
|
4992
|
+
this.findAllTemplates(child, templates);
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
}
|
|
4996
|
+
return templates;
|
|
4997
|
+
}
|
|
4998
|
+
/**
|
|
4999
|
+
* Dynamically mount a new object into the scene (e.g. from a lazy-loaded chunk)
|
|
5000
|
+
*/
|
|
5001
|
+
mountObject(node, parent = null) {
|
|
5002
|
+
const targetParent = parent || this.rootInstance;
|
|
5003
|
+
const instance = this.instantiateNode(node, targetParent);
|
|
5004
|
+
if (targetParent) {
|
|
5005
|
+
targetParent.children.push(instance);
|
|
5006
|
+
if (this.options.renderer && targetParent.renderedNode && instance.renderedNode) {
|
|
5007
|
+
this.options.renderer.appendChild(targetParent.renderedNode, instance.renderedNode);
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
this.callLifecycle(instance, "on_mount");
|
|
5011
|
+
return instance;
|
|
5012
|
+
}
|
|
5013
|
+
unmountObject(idOrInstance) {
|
|
5014
|
+
let instance;
|
|
5015
|
+
if (typeof idOrInstance === "string") {
|
|
5016
|
+
instance = this._flatEntities.find(
|
|
5017
|
+
(n) => n.node.id === idOrInstance || n.__holo_id === idOrInstance
|
|
5018
|
+
);
|
|
5019
|
+
} else {
|
|
5020
|
+
instance = idOrInstance;
|
|
5021
|
+
}
|
|
5022
|
+
if (!instance) return;
|
|
5023
|
+
if (instance.parent) {
|
|
5024
|
+
const idx = instance.parent.children.indexOf(instance);
|
|
5025
|
+
if (idx > -1) instance.parent.children.splice(idx, 1);
|
|
5026
|
+
}
|
|
5027
|
+
const flatIdx = this._flatEntities.indexOf(instance);
|
|
5028
|
+
if (flatIdx > -1) this._flatEntities.splice(flatIdx, 1);
|
|
5029
|
+
if (this.options.renderer && instance.renderedNode) {
|
|
5030
|
+
this.options.renderer.destroy(instance.renderedNode);
|
|
5031
|
+
}
|
|
5032
|
+
this.callLifecycle(instance, "on_unmount");
|
|
5033
|
+
}
|
|
5034
|
+
// ==========================================================================
|
|
5035
|
+
// NODE INSTANTIATION
|
|
5036
|
+
// ==========================================================================
|
|
5037
|
+
_holoIdCounter = 0;
|
|
5038
|
+
generateHoloId(node) {
|
|
5039
|
+
const name = node.name || node.type || "obj";
|
|
5040
|
+
return `${name}_${++this._holoIdCounter}_${Date.now().toString(36)}`;
|
|
5041
|
+
}
|
|
5042
|
+
instantiateNode(node, parent) {
|
|
5043
|
+
const instance = {
|
|
5044
|
+
__holo_id: this.generateHoloId(node),
|
|
5045
|
+
node,
|
|
5046
|
+
get properties() {
|
|
5047
|
+
return this.node.properties || {};
|
|
5048
|
+
},
|
|
5049
|
+
renderedNode: null,
|
|
5050
|
+
lifecycleHandlers: /* @__PURE__ */ new Map(),
|
|
5051
|
+
children: [],
|
|
5052
|
+
parent,
|
|
5053
|
+
destroyed: false,
|
|
5054
|
+
dirty: true,
|
|
5055
|
+
hasTraits: !!(node.traits && node.traits.size > 0)
|
|
5056
|
+
};
|
|
5057
|
+
this._flatEntities.push(instance);
|
|
5058
|
+
let templateName = node.template || node.properties && node.properties.__templateRef;
|
|
5059
|
+
if (!templateName && this.templates.has(node.type)) {
|
|
5060
|
+
templateName = node.type;
|
|
5061
|
+
}
|
|
5062
|
+
if (templateName) {
|
|
5063
|
+
instance.templateName = templateName;
|
|
5064
|
+
const templateNode = this.templates.get(templateName);
|
|
5065
|
+
instance.templateVersion = templateNode?.version || node.version || 0;
|
|
5066
|
+
const stateBridge = this.createStateMapProxy();
|
|
5067
|
+
const _self = this;
|
|
5068
|
+
this.hotReloader.registerInstance({
|
|
5069
|
+
__holo_id: instance.__holo_id,
|
|
5070
|
+
templateName: instance.templateName,
|
|
5071
|
+
get version() {
|
|
5072
|
+
return instance.templateVersion || 0;
|
|
5073
|
+
},
|
|
5074
|
+
set version(v) {
|
|
5075
|
+
instance.templateVersion = v;
|
|
5076
|
+
},
|
|
5077
|
+
state: stateBridge
|
|
5078
|
+
});
|
|
5079
|
+
}
|
|
5080
|
+
const templateNodeForDirectives = templateName ? this.templates.get(templateName) : null;
|
|
5081
|
+
this.processDirectives(instance, templateNodeForDirectives?.directives);
|
|
5082
|
+
if (this.options.renderer) {
|
|
5083
|
+
const properties = node.properties ? this.evaluateProperties(node.properties) : {};
|
|
5084
|
+
instance.renderedNode = this.options.renderer.createElement(node.type, properties);
|
|
5085
|
+
}
|
|
5086
|
+
if (node.traits) {
|
|
5087
|
+
for (const [traitName, config] of node.traits) {
|
|
5088
|
+
this.traitRegistry.attachTrait(node, traitName, config, this.createTraitContext(instance));
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
const childrenNodes = node.children || node.body || [];
|
|
5092
|
+
const children = this.processControlFlow(childrenNodes, node.directives || []);
|
|
5093
|
+
for (const childNode of children) {
|
|
5094
|
+
const childInstance = this.instantiateNode(childNode, instance);
|
|
5095
|
+
instance.children.push(childInstance);
|
|
5096
|
+
if (this.options.renderer && instance.renderedNode) {
|
|
5097
|
+
this.options.renderer.appendChild(instance.renderedNode, childInstance.renderedNode);
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
return instance;
|
|
5101
|
+
}
|
|
5102
|
+
getNode(id) {
|
|
5103
|
+
const instance = this._flatEntities.find((n) => n.node.id === id || n.__holo_id === id);
|
|
5104
|
+
return instance ? instance.node : void 0;
|
|
5105
|
+
}
|
|
5106
|
+
processDirectives(instance, extraDirectives) {
|
|
5107
|
+
const directives = [...instance.node.directives || [], ...extraDirectives || []];
|
|
5108
|
+
for (const directive of directives) {
|
|
5109
|
+
if (directive.type === "lifecycle") {
|
|
5110
|
+
this.registerLifecycleHandler(instance, directive);
|
|
5111
|
+
} else if (directive.type === "state") {
|
|
5112
|
+
const stateBody = directive.body || {};
|
|
5113
|
+
for (const [key, value] of Object.entries(stateBody)) {
|
|
5114
|
+
if (!this.state.has(key)) {
|
|
5115
|
+
this.state.set(key, value);
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
registerLifecycleHandler(instance, directive) {
|
|
5122
|
+
const { hook, params, body } = directive;
|
|
5123
|
+
const handler = (...args) => {
|
|
5124
|
+
const paramContext = {};
|
|
5125
|
+
if (params) {
|
|
5126
|
+
params.forEach((param, i) => {
|
|
5127
|
+
paramContext[param] = args[i];
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
this.evaluator.updateContext({
|
|
5131
|
+
...this.state.getSnapshot(),
|
|
5132
|
+
...paramContext,
|
|
5133
|
+
node: instance.node,
|
|
5134
|
+
self: instance.node
|
|
5135
|
+
});
|
|
5136
|
+
try {
|
|
5137
|
+
if (body.includes(";") || body.includes("{")) {
|
|
5138
|
+
new Function(
|
|
5139
|
+
...Object.keys(this.builtins),
|
|
5140
|
+
...Object.keys(paramContext),
|
|
5141
|
+
"state",
|
|
5142
|
+
"node",
|
|
5143
|
+
body
|
|
5144
|
+
)(
|
|
5145
|
+
...Object.values(this.builtins),
|
|
5146
|
+
...Object.values(paramContext),
|
|
5147
|
+
this.state,
|
|
5148
|
+
instance.node
|
|
5149
|
+
);
|
|
5150
|
+
} else {
|
|
5151
|
+
this.evaluator.evaluate(body);
|
|
5152
|
+
}
|
|
5153
|
+
} catch (error) {
|
|
5154
|
+
console.error(`Error in lifecycle handler ${hook}:`, error);
|
|
5155
|
+
}
|
|
5156
|
+
};
|
|
5157
|
+
if (!instance.lifecycleHandlers.has(hook)) {
|
|
5158
|
+
instance.lifecycleHandlers.set(hook, []);
|
|
5159
|
+
}
|
|
5160
|
+
instance.lifecycleHandlers.get(hook).push(handler);
|
|
5161
|
+
}
|
|
5162
|
+
// ==========================================================================
|
|
5163
|
+
// CONTROL FLOW
|
|
5164
|
+
// ==========================================================================
|
|
5165
|
+
processControlFlow(children, directives) {
|
|
5166
|
+
const result = [];
|
|
5167
|
+
for (const directive of directives) {
|
|
5168
|
+
if (directive.type === "for") {
|
|
5169
|
+
const items = this.evaluateExpression(directive.iterable);
|
|
5170
|
+
if (Array.isArray(items)) {
|
|
5171
|
+
items.forEach((item, index) => {
|
|
5172
|
+
const iterContext = {
|
|
5173
|
+
[directive.variable]: item,
|
|
5174
|
+
index,
|
|
5175
|
+
first: index === 0,
|
|
5176
|
+
last: index === items.length - 1,
|
|
5177
|
+
even: index % 2 === 0,
|
|
5178
|
+
odd: index % 2 !== 0
|
|
5179
|
+
};
|
|
5180
|
+
for (const bodyNode of directive.body) {
|
|
5181
|
+
const cloned = this.cloneNodeWithContext(bodyNode, iterContext);
|
|
5182
|
+
result.push(cloned);
|
|
5183
|
+
}
|
|
5184
|
+
});
|
|
5185
|
+
}
|
|
5186
|
+
} else if (directive.type === "forEach") {
|
|
5187
|
+
const items = this.evaluateExpression(
|
|
5188
|
+
directive.collection
|
|
5189
|
+
);
|
|
5190
|
+
if (Array.isArray(items)) {
|
|
5191
|
+
items.forEach((item, index) => {
|
|
5192
|
+
const iterContext = {
|
|
5193
|
+
[directive.variable]: item,
|
|
5194
|
+
index,
|
|
5195
|
+
first: index === 0,
|
|
5196
|
+
last: index === items.length - 1,
|
|
5197
|
+
even: index % 2 === 0,
|
|
5198
|
+
odd: index % 2 !== 0
|
|
5199
|
+
};
|
|
5200
|
+
for (const bodyNode of directive.body) {
|
|
5201
|
+
const cloned = this.cloneNodeWithContext(bodyNode, iterContext);
|
|
5202
|
+
result.push(cloned);
|
|
5203
|
+
}
|
|
5204
|
+
});
|
|
5205
|
+
}
|
|
5206
|
+
} else if (directive.type === "while") {
|
|
5207
|
+
const MAX_ITERATIONS = 1e3;
|
|
5208
|
+
let iterations = 0;
|
|
5209
|
+
while (iterations < MAX_ITERATIONS) {
|
|
5210
|
+
const condition = this.evaluateExpression(
|
|
5211
|
+
directive.condition
|
|
5212
|
+
);
|
|
5213
|
+
if (!condition) break;
|
|
5214
|
+
const iterContext = {
|
|
5215
|
+
iteration: iterations,
|
|
5216
|
+
index: iterations
|
|
5217
|
+
};
|
|
5218
|
+
for (const bodyNode of directive.body) {
|
|
5219
|
+
const cloned = this.cloneNodeWithContext(bodyNode, iterContext);
|
|
5220
|
+
result.push(cloned);
|
|
5221
|
+
}
|
|
5222
|
+
iterations++;
|
|
5223
|
+
}
|
|
5224
|
+
if (iterations >= MAX_ITERATIONS) {
|
|
5225
|
+
console.warn("@while loop hit maximum iteration limit (1000)");
|
|
5226
|
+
}
|
|
5227
|
+
} else if (directive.type === "if") {
|
|
5228
|
+
const condition = this.evaluateExpression(
|
|
5229
|
+
directive.condition
|
|
5230
|
+
);
|
|
5231
|
+
if (condition) {
|
|
5232
|
+
result.push(...directive.body);
|
|
5233
|
+
} else if (directive.else) {
|
|
5234
|
+
result.push(...directive.else);
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
result.push(...children);
|
|
5239
|
+
return result;
|
|
5240
|
+
}
|
|
5241
|
+
cloneNodeWithContext(node, context) {
|
|
5242
|
+
const cloned = {
|
|
5243
|
+
type: node.type,
|
|
5244
|
+
id: node.id ? this.interpolateString(node.id, context) : void 0,
|
|
5245
|
+
properties: node.properties ? this.interpolateProperties(node.properties, context) : {},
|
|
5246
|
+
directives: node.directives ? [...node.directives] : [],
|
|
5247
|
+
children: (node.children || []).map(
|
|
5248
|
+
(child) => this.cloneNodeWithContext(child, context)
|
|
5249
|
+
),
|
|
5250
|
+
traits: node.traits ? new Map(node.traits) : /* @__PURE__ */ new Map(),
|
|
5251
|
+
loc: node.loc
|
|
5252
|
+
};
|
|
5253
|
+
return cloned;
|
|
5254
|
+
}
|
|
5255
|
+
interpolateString(str, context) {
|
|
5256
|
+
return str.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
|
|
5257
|
+
this.evaluator.updateContext(context);
|
|
5258
|
+
const value = this.evaluator.evaluate(expr);
|
|
5259
|
+
return String(value ?? "");
|
|
5260
|
+
});
|
|
5261
|
+
}
|
|
5262
|
+
interpolateProperties(properties, context) {
|
|
5263
|
+
const result = {};
|
|
5264
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
5265
|
+
if (typeof value === "string") {
|
|
5266
|
+
result[key] = this.interpolateString(value, context);
|
|
5267
|
+
} else if (value && typeof value === "object" && "__expr" in value) {
|
|
5268
|
+
this.evaluator.updateContext(context);
|
|
5269
|
+
result[key] = this.evaluator.evaluate(value.__raw);
|
|
5270
|
+
} else {
|
|
5271
|
+
result[key] = value;
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
return result;
|
|
5275
|
+
}
|
|
5276
|
+
// ==========================================================================
|
|
5277
|
+
// EXPRESSION EVALUATION
|
|
5278
|
+
// ==========================================================================
|
|
5279
|
+
evaluateExpression(expr) {
|
|
5280
|
+
this.evaluator.updateContext(this.state.getSnapshot());
|
|
5281
|
+
return this.evaluator.evaluate(expr);
|
|
5282
|
+
}
|
|
5283
|
+
evaluateProperties(properties) {
|
|
5284
|
+
const expandedProperties = this.expandPropertySpreads(properties);
|
|
5285
|
+
const result = {};
|
|
5286
|
+
for (const [key, value] of Object.entries(expandedProperties)) {
|
|
5287
|
+
if (value && typeof value === "object" && "__expr" in value) {
|
|
5288
|
+
result[key] = this.evaluateExpression(value.__raw);
|
|
5289
|
+
} else if (value && typeof value === "object" && "__ref" in value) {
|
|
5290
|
+
const ref = value.__ref;
|
|
5291
|
+
result[key] = this.state.get(ref) ?? ref;
|
|
5292
|
+
} else if (typeof value === "string" && value.includes("${")) {
|
|
5293
|
+
result[key] = this.interpolateString(value, this.state.getSnapshot());
|
|
5294
|
+
} else {
|
|
5295
|
+
result[key] = value;
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5298
|
+
return result;
|
|
5299
|
+
}
|
|
5300
|
+
/**
|
|
5301
|
+
* Expands spread expressions in a properties object.
|
|
5302
|
+
* Spread keys are formatted as __spread_N with value { type: 'spread', argument: ... }
|
|
5303
|
+
*/
|
|
5304
|
+
expandPropertySpreads(properties) {
|
|
5305
|
+
const result = {};
|
|
5306
|
+
const spreadKeys = [];
|
|
5307
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
5308
|
+
if (key.startsWith("__spread_")) {
|
|
5309
|
+
spreadKeys.push(key);
|
|
5310
|
+
} else if (value && typeof value === "object" && "type" in value && value.type === "spread") {
|
|
5311
|
+
spreadKeys.push(key);
|
|
5312
|
+
} else {
|
|
5313
|
+
if (value && typeof value === "object" && !Array.isArray(value) && !("__ref" in value) && !("__expr" in value)) {
|
|
5314
|
+
result[key] = this.expandPropertySpreads(value);
|
|
5315
|
+
} else if (Array.isArray(value)) {
|
|
5316
|
+
result[key] = this.expandArraySpreads(value);
|
|
5317
|
+
} else {
|
|
5318
|
+
result[key] = value;
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
for (const spreadKey of spreadKeys) {
|
|
5323
|
+
const spreadValue = properties[spreadKey];
|
|
5324
|
+
if (spreadValue && typeof spreadValue === "object" && "type" in spreadValue && spreadValue.type === "spread") {
|
|
5325
|
+
const resolved = this.resolveSpreadArgument(
|
|
5326
|
+
spreadValue.argument
|
|
5327
|
+
);
|
|
5328
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
5329
|
+
Object.assign(result, this.expandPropertySpreads(resolved));
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
}
|
|
5333
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
5334
|
+
if (!key.startsWith("__spread_") && !(value && typeof value === "object" && "type" in value && value.type === "spread")) {
|
|
5335
|
+
if (value && typeof value === "object" && !Array.isArray(value) && !("__ref" in value) && !("__expr" in value)) {
|
|
5336
|
+
result[key] = this.expandPropertySpreads(value);
|
|
5337
|
+
} else if (Array.isArray(value)) {
|
|
5338
|
+
result[key] = this.expandArraySpreads(value);
|
|
5339
|
+
} else {
|
|
5340
|
+
result[key] = value;
|
|
5341
|
+
}
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
return result;
|
|
5345
|
+
}
|
|
5346
|
+
/**
|
|
5347
|
+
* Expands spread expressions within an array.
|
|
5348
|
+
*/
|
|
5349
|
+
expandArraySpreads(arr) {
|
|
5350
|
+
const result = [];
|
|
5351
|
+
for (const item of arr) {
|
|
5352
|
+
if (item && typeof item === "object" && "type" in item && item.type === "spread") {
|
|
5353
|
+
const resolved = this.resolveSpreadArgument(item.argument);
|
|
5354
|
+
if (Array.isArray(resolved)) {
|
|
5355
|
+
result.push(...resolved);
|
|
5356
|
+
} else if (resolved !== void 0 && resolved !== null) {
|
|
5357
|
+
result.push(resolved);
|
|
5358
|
+
}
|
|
5359
|
+
} else if (item && typeof item === "object" && !Array.isArray(item)) {
|
|
5360
|
+
result.push(this.expandPropertySpreads(item));
|
|
5361
|
+
} else {
|
|
5362
|
+
result.push(item);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
return result;
|
|
5366
|
+
}
|
|
5367
|
+
/**
|
|
5368
|
+
* Resolves a spread argument to its runtime value.
|
|
5369
|
+
*/
|
|
5370
|
+
resolveSpreadArgument(argument) {
|
|
5371
|
+
if (argument === null || argument === void 0) {
|
|
5372
|
+
return void 0;
|
|
5373
|
+
}
|
|
5374
|
+
if (typeof argument === "object" && !("__ref" in argument)) {
|
|
5375
|
+
return argument;
|
|
5376
|
+
}
|
|
5377
|
+
if (typeof argument === "object" && "__ref" in argument) {
|
|
5378
|
+
const ref = argument.__ref;
|
|
5379
|
+
const stateValue = this.state.get(ref);
|
|
5380
|
+
if (stateValue !== void 0) {
|
|
5381
|
+
return stateValue;
|
|
5382
|
+
}
|
|
5383
|
+
if (ref.includes(".")) {
|
|
5384
|
+
const snapshot = this.state.getSnapshot();
|
|
5385
|
+
const parts = ref.split(".");
|
|
5386
|
+
let value = snapshot;
|
|
5387
|
+
for (const part of parts) {
|
|
5388
|
+
if (value && typeof value === "object" && part in value) {
|
|
5389
|
+
value = value[part];
|
|
5390
|
+
} else {
|
|
5391
|
+
return void 0;
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
return value;
|
|
5395
|
+
}
|
|
5396
|
+
return void 0;
|
|
5397
|
+
}
|
|
5398
|
+
if (typeof argument === "string") {
|
|
5399
|
+
return this.state.get(argument);
|
|
5400
|
+
}
|
|
5401
|
+
return argument;
|
|
5402
|
+
}
|
|
5403
|
+
// ==========================================================================
|
|
5404
|
+
// LIFECYCLE
|
|
5405
|
+
// ==========================================================================
|
|
5406
|
+
callLifecycle(instance, hook, ...args) {
|
|
5407
|
+
if (!instance || instance.destroyed) return;
|
|
5408
|
+
const handlers = instance.lifecycleHandlers.get(hook);
|
|
5409
|
+
if (handlers) {
|
|
5410
|
+
handlers.forEach((handler) => {
|
|
5411
|
+
try {
|
|
5412
|
+
handler(...args);
|
|
5413
|
+
} catch (error) {
|
|
5414
|
+
console.error(`Error in lifecycle ${hook}:`, error);
|
|
5415
|
+
}
|
|
5416
|
+
});
|
|
5417
|
+
}
|
|
5418
|
+
for (const child of instance.children) {
|
|
5419
|
+
this.callLifecycle(child, hook, ...args);
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
// ==========================================================================
|
|
5423
|
+
// UPDATE LOOP
|
|
5424
|
+
// ==========================================================================
|
|
5425
|
+
startUpdateLoop() {
|
|
5426
|
+
const raf = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(() => cb(performance.now()), 16);
|
|
5427
|
+
this.lastUpdateTime = performance.now();
|
|
5428
|
+
const update = () => {
|
|
5429
|
+
const now = performance.now();
|
|
5430
|
+
const delta = (now - this.lastUpdateTime) / 1e3;
|
|
5431
|
+
this.lastUpdateTime = now;
|
|
5432
|
+
this.updateVRInput();
|
|
5433
|
+
this.update(delta);
|
|
5434
|
+
this.updateLoopId = raf(update);
|
|
5435
|
+
};
|
|
5436
|
+
this.updateLoopId = raf(update);
|
|
5437
|
+
}
|
|
5438
|
+
stopUpdateLoop() {
|
|
5439
|
+
if (this.updateLoopId !== null) {
|
|
5440
|
+
if (typeof cancelAnimationFrame !== "undefined") {
|
|
5441
|
+
cancelAnimationFrame(this.updateLoopId);
|
|
5442
|
+
} else {
|
|
5443
|
+
clearTimeout(this.updateLoopId);
|
|
5444
|
+
}
|
|
5445
|
+
this.updateLoopId = null;
|
|
5446
|
+
}
|
|
5447
|
+
}
|
|
5448
|
+
update(delta) {
|
|
5449
|
+
if (!this.rootInstance) return;
|
|
5450
|
+
try {
|
|
5451
|
+
this.vrPhysicsBridge.update(this.vrContext, delta);
|
|
5452
|
+
} catch (e) {
|
|
5453
|
+
console.error("[Runtime] Error in vrPhysicsBridge.update:", e);
|
|
5454
|
+
}
|
|
5455
|
+
this.physicsWorld.step(delta);
|
|
5456
|
+
if (this.debugDrawer) {
|
|
5457
|
+
this.debugDrawer.update();
|
|
5458
|
+
}
|
|
5459
|
+
const count = this._flatEntities.length;
|
|
5460
|
+
for (let i = 0; i < count; i++) {
|
|
5461
|
+
this.updateInstance(this._flatEntities[i], delta);
|
|
5462
|
+
}
|
|
5463
|
+
this.callLifecycle(this.rootInstance, "on_update", delta);
|
|
5464
|
+
if (this.chunkLoader) {
|
|
5465
|
+
if (this.chunkLoader) this.chunkLoader.update();
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
/**
|
|
5469
|
+
* Authoritative server sync for networked state.
|
|
5470
|
+
*/
|
|
5471
|
+
onNetworkStateUpdate(serverState) {
|
|
5472
|
+
if (this.networkPredictor) {
|
|
5473
|
+
const reconciled = this.networkPredictor.reconcile(serverState, (state, input) => {
|
|
5474
|
+
if (input.type === "state_update") {
|
|
5475
|
+
Object.assign(state, input.payload);
|
|
5476
|
+
}
|
|
5477
|
+
});
|
|
5478
|
+
this.state.update(reconciled);
|
|
5479
|
+
this.networkPredictor.updateMetrics(performance.now() - 100);
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
updateInstance(instance, delta) {
|
|
5483
|
+
if (instance.destroyed) return;
|
|
5484
|
+
if (instance.hasTraits) {
|
|
5485
|
+
const traitContext = this.createTraitContext(instance);
|
|
5486
|
+
this.traitRegistry.updateAllTraits(instance.node, traitContext, delta);
|
|
5487
|
+
}
|
|
5488
|
+
if (instance.node.type === "avatar") {
|
|
5489
|
+
this.syncAvatarParts(instance);
|
|
5490
|
+
}
|
|
5491
|
+
if (instance.node.traits?.has("networked")) {
|
|
5492
|
+
const interpolated = this.networkSync.getInterpolatedState(
|
|
5493
|
+
instance.node.id || ""
|
|
5494
|
+
);
|
|
5495
|
+
const body = this.physicsWorld.getBody(instance.node.id || "");
|
|
5496
|
+
if (interpolated && instance.node.properties) {
|
|
5497
|
+
instance.dirty = true;
|
|
5498
|
+
if (interpolated.position) {
|
|
5499
|
+
instance.node.properties.position = [
|
|
5500
|
+
interpolated.position.x,
|
|
5501
|
+
interpolated.position.y,
|
|
5502
|
+
interpolated.position.z
|
|
5503
|
+
];
|
|
5504
|
+
}
|
|
5505
|
+
if (interpolated.rotation) {
|
|
5506
|
+
instance.node.properties.rotation = [
|
|
5507
|
+
interpolated.rotation.x,
|
|
5508
|
+
interpolated.rotation.y,
|
|
5509
|
+
interpolated.rotation.z
|
|
5510
|
+
];
|
|
5511
|
+
}
|
|
5512
|
+
if (body) {
|
|
5513
|
+
if (body.type !== "kinematic") {
|
|
5514
|
+
if (!instance.node.__originalPhysicsType) {
|
|
5515
|
+
instance.node.__originalPhysicsType = body.type;
|
|
5516
|
+
}
|
|
5517
|
+
body.type = "kinematic";
|
|
5518
|
+
}
|
|
5519
|
+
if (interpolated.position) {
|
|
5520
|
+
const pos = interpolated.position;
|
|
5521
|
+
body.position = {
|
|
5522
|
+
x: pos.x,
|
|
5523
|
+
y: pos.y,
|
|
5524
|
+
z: pos.z
|
|
5525
|
+
};
|
|
5526
|
+
body.velocity = { x: 0, y: 0, z: 0 };
|
|
5527
|
+
}
|
|
5528
|
+
if (interpolated.rotation) {
|
|
5529
|
+
const rot = interpolated.rotation;
|
|
5530
|
+
body.rotation = {
|
|
5531
|
+
x: rot.x,
|
|
5532
|
+
y: rot.y,
|
|
5533
|
+
z: rot.z,
|
|
5534
|
+
w: rot.w || 1
|
|
5535
|
+
};
|
|
5536
|
+
body.angularVelocity = { x: 0, y: 0, z: 0 };
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
} else if (!interpolated && body && instance.node.__originalPhysicsType) {
|
|
5540
|
+
if (body.type === "kinematic" && instance.node.__originalPhysicsType !== "kinematic") {
|
|
5541
|
+
body.type = instance.node.__originalPhysicsType;
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
if (instance.dirty && this.options.renderer && instance.renderedNode) {
|
|
5546
|
+
const properties = this.evaluateProperties(instance.node.properties || {});
|
|
5547
|
+
this.options.renderer.updateElement(instance.renderedNode, properties);
|
|
5548
|
+
instance.dirty = false;
|
|
5549
|
+
}
|
|
5550
|
+
this.updateExternalApis(instance, delta);
|
|
5551
|
+
this.processGenerateDirectives(instance);
|
|
5552
|
+
}
|
|
5553
|
+
syncAvatarParts(instance) {
|
|
5554
|
+
const vrHands = this.vrContext.hands;
|
|
5555
|
+
const vrHead = this.vrContext.headset;
|
|
5556
|
+
if (instance.node.id === "local_player" && instance.node.properties) {
|
|
5557
|
+
instance.node.properties.position = vrHead.position;
|
|
5558
|
+
instance.node.properties.rotation = vrHead.rotation;
|
|
5559
|
+
instance.children.forEach((child) => {
|
|
5560
|
+
if (child.node.id === "left_hand" && vrHands.left && child.node.properties) {
|
|
5561
|
+
child.node.properties.position = vrHands.left.position;
|
|
5562
|
+
child.node.properties.rotation = vrHands.left.rotation;
|
|
5563
|
+
} else if (child.node.id === "right_hand" && vrHands.right && child.node.properties) {
|
|
5564
|
+
child.node.properties.position = vrHands.right.position;
|
|
5565
|
+
child.node.properties.rotation = vrHands.right.rotation;
|
|
5566
|
+
}
|
|
5567
|
+
});
|
|
5568
|
+
if (instance.node.traits?.has("networked")) {
|
|
5569
|
+
this.emit("network_snapshot", {
|
|
5570
|
+
objectId: instance.node.id,
|
|
5571
|
+
position: [
|
|
5572
|
+
instance.node.properties.position[0],
|
|
5573
|
+
instance.node.properties.position[1],
|
|
5574
|
+
instance.node.properties.position[2]
|
|
5575
|
+
],
|
|
5576
|
+
rotation: [
|
|
5577
|
+
instance.node.properties.rotation[0],
|
|
5578
|
+
instance.node.properties.rotation[1],
|
|
5579
|
+
instance.node.properties.rotation[2]
|
|
5580
|
+
]
|
|
5581
|
+
});
|
|
5582
|
+
}
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
generatedNodes = /* @__PURE__ */ new Set();
|
|
5586
|
+
processGenerateDirectives(instance) {
|
|
5587
|
+
if (!instance.node.directives) return;
|
|
5588
|
+
const generateDirectives = instance.node.directives.filter((d) => d.type === "generate");
|
|
5589
|
+
for (const d of generateDirectives) {
|
|
5590
|
+
const directive = d;
|
|
5591
|
+
const genId = `${instance.node.id || "node"}_${directive.prompt.substring(0, 10)}`;
|
|
5592
|
+
if (this.generatedNodes.has(genId)) continue;
|
|
5593
|
+
if (this._copilot && this._copilot.isReady()) {
|
|
5594
|
+
this._copilot.generateFromPrompt(directive.prompt, {
|
|
5595
|
+
context: directive.context
|
|
5596
|
+
}).then((response) => {
|
|
5597
|
+
this.emit("generate_complete", {
|
|
5598
|
+
id: genId,
|
|
5599
|
+
nodeId: instance.node.id,
|
|
5600
|
+
result: response
|
|
5601
|
+
});
|
|
5602
|
+
}).catch(() => {
|
|
5603
|
+
});
|
|
5604
|
+
} else {
|
|
5605
|
+
this.emit("generate_request", {
|
|
5606
|
+
id: genId,
|
|
5607
|
+
nodeId: instance.node.id,
|
|
5608
|
+
prompt: directive.prompt,
|
|
5609
|
+
context: directive.context,
|
|
5610
|
+
target: directive.target || "children"
|
|
5611
|
+
});
|
|
5612
|
+
}
|
|
5613
|
+
this.generatedNodes.add(genId);
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5616
|
+
apiPollingTimers = /* @__PURE__ */ new Map();
|
|
5617
|
+
updateExternalApis(instance, _delta) {
|
|
5618
|
+
if (!instance.node.directives) return;
|
|
5619
|
+
const apiDirectives = instance.node.directives.filter((d) => d.type === "external_api");
|
|
5620
|
+
for (const d of apiDirectives) {
|
|
5621
|
+
const directive = d;
|
|
5622
|
+
if (directive.type !== "external_api") continue;
|
|
5623
|
+
const intervalStr = directive.interval || "0s";
|
|
5624
|
+
const intervalMs = this.parseDurationToMs(String(intervalStr));
|
|
5625
|
+
if (intervalMs <= 0) continue;
|
|
5626
|
+
const lastTime = this.apiPollingTimers.get(instance) || 0;
|
|
5627
|
+
const now = performance.now();
|
|
5628
|
+
if (now - lastTime >= intervalMs) {
|
|
5629
|
+
this.apiPollingTimers.set(instance, now);
|
|
5630
|
+
this.executeExternalApi(instance, directive);
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
async executeExternalApi(instance, directive) {
|
|
5635
|
+
try {
|
|
5636
|
+
const apiCall = this.builtins["api_call"];
|
|
5637
|
+
const data = apiCall ? await apiCall(String(directive.url), String(directive.method || "GET")) : void 0;
|
|
5638
|
+
this.state.set("api_data", data);
|
|
5639
|
+
this.updateData(data);
|
|
5640
|
+
} catch (error) {
|
|
5641
|
+
console.error(`External API error for ${directive.url}:`, error);
|
|
5642
|
+
}
|
|
5643
|
+
}
|
|
5644
|
+
parseDurationToMs(duration) {
|
|
5645
|
+
const match = duration.match(/^(\d+)(ms|s|m)$/);
|
|
5646
|
+
if (!match) return 0;
|
|
5647
|
+
const value = parseInt(match[1], 10);
|
|
5648
|
+
const unit = match[2];
|
|
5649
|
+
switch (unit) {
|
|
5650
|
+
case "ms":
|
|
5651
|
+
return value;
|
|
5652
|
+
case "s":
|
|
5653
|
+
return value * 1e3;
|
|
5654
|
+
case "m":
|
|
5655
|
+
return value * 6e4;
|
|
5656
|
+
default:
|
|
5657
|
+
return 0;
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
// ==========================================================================
|
|
5661
|
+
// TRAIT CONTEXT
|
|
5662
|
+
// ==========================================================================
|
|
5663
|
+
createTraitContext(_instance) {
|
|
5664
|
+
return {
|
|
5665
|
+
vr: {
|
|
5666
|
+
hands: this.vrContext.hands,
|
|
5667
|
+
headset: this.vrContext.headset,
|
|
5668
|
+
getPointerRay: (hand) => {
|
|
5669
|
+
const vrHand = hand === "left" ? this.vrContext.hands.left : this.vrContext.hands.right;
|
|
5670
|
+
if (!vrHand) return null;
|
|
5671
|
+
return {
|
|
5672
|
+
origin: vrHand.position,
|
|
5673
|
+
direction: { x: 0, y: 0, z: -1 }
|
|
5674
|
+
// Forward direction - should be calculated from rotation
|
|
5675
|
+
};
|
|
5676
|
+
},
|
|
5677
|
+
getDominantHand: () => this.vrContext.hands.right || this.vrContext.hands.left
|
|
5678
|
+
},
|
|
5679
|
+
physics: {
|
|
5680
|
+
applyVelocity: (node, velocity) => {
|
|
5681
|
+
const body = this.physicsWorld.getBody(node.id || "");
|
|
5682
|
+
if (body) {
|
|
5683
|
+
body.velocity = {
|
|
5684
|
+
x: velocity[0],
|
|
5685
|
+
y: velocity[1],
|
|
5686
|
+
z: velocity[2]
|
|
5687
|
+
};
|
|
5688
|
+
}
|
|
5689
|
+
},
|
|
5690
|
+
applyAngularVelocity: (node, angularVelocity) => {
|
|
5691
|
+
const body = this.physicsWorld.getBody(node.id || "");
|
|
5692
|
+
if (body) {
|
|
5693
|
+
body.angularVelocity = {
|
|
5694
|
+
x: angularVelocity[0],
|
|
5695
|
+
y: angularVelocity[1],
|
|
5696
|
+
z: angularVelocity[2]
|
|
5697
|
+
};
|
|
5698
|
+
}
|
|
5699
|
+
},
|
|
5700
|
+
setKinematic: (node, kinematic) => {
|
|
5701
|
+
const body = this.physicsWorld.getBody(node.id || "");
|
|
5702
|
+
if (body) {
|
|
5703
|
+
body.type = kinematic ? "kinematic" : "dynamic";
|
|
5704
|
+
}
|
|
5705
|
+
},
|
|
5706
|
+
raycast: (origin, direction, maxDistance) => {
|
|
5707
|
+
const hit = this.physicsWorld.raycastClosest({
|
|
5708
|
+
origin: { x: origin[0], y: origin[1], z: origin[2] },
|
|
5709
|
+
direction: {
|
|
5710
|
+
x: direction[0],
|
|
5711
|
+
y: direction[1],
|
|
5712
|
+
z: direction[2]
|
|
5713
|
+
},
|
|
5714
|
+
maxDistance
|
|
5715
|
+
});
|
|
5716
|
+
if (hit) {
|
|
5717
|
+
const h = hit;
|
|
5718
|
+
return {
|
|
5719
|
+
point: { x: h.point.x || 0, y: h.point.y || 0, z: h.point.z || 0 },
|
|
5720
|
+
normal: { x: h.normal.x || 0, y: h.normal.y || 0, z: h.normal.z || 0 },
|
|
5721
|
+
distance: h.distance,
|
|
5722
|
+
bodyId: h.bodyId,
|
|
5723
|
+
nodeId: h.bodyId,
|
|
5724
|
+
node: h.bodyId
|
|
5725
|
+
};
|
|
5726
|
+
}
|
|
5727
|
+
return null;
|
|
5728
|
+
},
|
|
5729
|
+
getBodyPosition: (nodeId) => {
|
|
5730
|
+
const body = this.physicsWorld.getBody(nodeId);
|
|
5731
|
+
if (body && body.position) return { x: body.position.x || 0, y: body.position.y || 0, z: body.position.z || 0 };
|
|
5732
|
+
return null;
|
|
5733
|
+
},
|
|
5734
|
+
getBodyVelocity: (nodeId) => {
|
|
5735
|
+
const body = this.physicsWorld.getBody(nodeId);
|
|
5736
|
+
if (body && body.velocity) return { x: body.velocity.x || 0, y: body.velocity.y || 0, z: body.velocity.z || 0 };
|
|
5737
|
+
return null;
|
|
5738
|
+
}
|
|
5739
|
+
},
|
|
5740
|
+
audio: {
|
|
5741
|
+
playSound: (source, options) => {
|
|
5742
|
+
this.emit("play_sound", { source, ...options });
|
|
5743
|
+
}
|
|
5744
|
+
},
|
|
5745
|
+
haptics: {
|
|
5746
|
+
pulse: (hand, intensity, duration) => {
|
|
5747
|
+
this.emit("haptic", { hand, intensity, duration, type: "pulse" });
|
|
5748
|
+
},
|
|
5749
|
+
rumble: (hand, intensity) => {
|
|
5750
|
+
this.emit("haptic", { hand, intensity, type: "rumble" });
|
|
5751
|
+
}
|
|
5752
|
+
},
|
|
5753
|
+
emit: this.emit.bind(this),
|
|
5754
|
+
getState: () => this.state.getSnapshot(),
|
|
5755
|
+
setState: (updates) => this.state.update(updates),
|
|
5756
|
+
getScaleMultiplier: () => this.scaleMultiplier,
|
|
5757
|
+
setScaleContext: (magnitude) => {
|
|
5758
|
+
const multipliers = {
|
|
5759
|
+
galactic: 1e6,
|
|
5760
|
+
macro: 1e3,
|
|
5761
|
+
standard: 1,
|
|
5762
|
+
micro: 1e-3,
|
|
5763
|
+
atomic: 1e-6
|
|
5764
|
+
};
|
|
5765
|
+
const newMultiplier = multipliers[magnitude] || 1;
|
|
5766
|
+
if (this.scaleMultiplier !== newMultiplier) {
|
|
5767
|
+
this.scaleMultiplier = newMultiplier;
|
|
5768
|
+
this.emit("scale_change", { magnitude, multiplier: newMultiplier });
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
};
|
|
5772
|
+
}
|
|
5773
|
+
// ==========================================================================
|
|
5774
|
+
// NODE DESTRUCTION
|
|
5775
|
+
// ==========================================================================
|
|
5776
|
+
destroyInstance(instance) {
|
|
5777
|
+
if (instance.destroyed) return;
|
|
5778
|
+
instance.destroyed = true;
|
|
5779
|
+
for (const child of instance.children) {
|
|
5780
|
+
this.destroyInstance(child);
|
|
5781
|
+
}
|
|
5782
|
+
const traitContext = this.createTraitContext(instance);
|
|
5783
|
+
if (instance.node.traits) {
|
|
5784
|
+
for (const traitName of instance.node.traits.keys()) {
|
|
5785
|
+
this.traitRegistry.detachTrait(instance.node, traitName, traitContext);
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
if (this.options.renderer && instance.renderedNode) {
|
|
5789
|
+
this.options.renderer.destroy(instance.renderedNode);
|
|
5790
|
+
}
|
|
5791
|
+
instance.lifecycleHandlers.clear();
|
|
5792
|
+
instance.children = [];
|
|
5793
|
+
const index = this._flatEntities.indexOf(instance);
|
|
5794
|
+
if (index !== -1) {
|
|
5795
|
+
this._flatEntities.splice(index, 1);
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
// ==========================================================================
|
|
5799
|
+
// PUBLIC API
|
|
5800
|
+
// ==========================================================================
|
|
5801
|
+
togglePhysicsDebug(enabled) {
|
|
5802
|
+
if (this.debugDrawer) {
|
|
5803
|
+
this.debugDrawer.setEnabled(enabled);
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
updateData(data) {
|
|
5807
|
+
this.state.set("data", data);
|
|
5808
|
+
this.callLifecycle(this.rootInstance, "on_data_update", data);
|
|
5809
|
+
}
|
|
5810
|
+
updateNodeProperty(nodeId, property, value) {
|
|
5811
|
+
const instance = this.findInstanceById(nodeId);
|
|
5812
|
+
if (instance && instance.node.properties) {
|
|
5813
|
+
instance.node.properties[property] = value;
|
|
5814
|
+
if (this.options.renderer && instance.renderedNode) {
|
|
5815
|
+
instance.dirty = true;
|
|
5816
|
+
}
|
|
5817
|
+
this.emit("property_update", { nodeId, property, value });
|
|
5818
|
+
}
|
|
5819
|
+
}
|
|
5820
|
+
getState() {
|
|
5821
|
+
return this.state.getSnapshot();
|
|
5822
|
+
}
|
|
5823
|
+
// ==========================================================================
|
|
5824
|
+
// COMPATIBILITY METHODS
|
|
5825
|
+
// ==========================================================================
|
|
5826
|
+
getVariable(name) {
|
|
5827
|
+
return this.state.get(name);
|
|
5828
|
+
}
|
|
5829
|
+
setVariable(name, value) {
|
|
5830
|
+
this.state.set(name, value);
|
|
5831
|
+
}
|
|
5832
|
+
getContext() {
|
|
5833
|
+
const spatialMemory = /* @__PURE__ */ new Map();
|
|
5834
|
+
const hologramState = /* @__PURE__ */ new Map();
|
|
5835
|
+
const traverse = (instance) => {
|
|
5836
|
+
if (instance.node.id && instance.node.properties) {
|
|
5837
|
+
spatialMemory.set(
|
|
5838
|
+
instance.node.id,
|
|
5839
|
+
instance.node.properties.position || { x: 0, y: 0, z: 0 }
|
|
5840
|
+
);
|
|
5841
|
+
hologramState.set(instance.node.id, {
|
|
5842
|
+
shape: instance.node.properties.shape || instance.node.type,
|
|
5843
|
+
color: instance.node.properties.color,
|
|
5844
|
+
size: instance.node.properties.size,
|
|
5845
|
+
glow: instance.node.properties.glow,
|
|
5846
|
+
interactive: instance.node.properties.interactive
|
|
5847
|
+
});
|
|
5848
|
+
}
|
|
5849
|
+
instance.children.forEach(traverse);
|
|
5850
|
+
};
|
|
5851
|
+
if (this.rootInstance) traverse(this.rootInstance);
|
|
5852
|
+
return {
|
|
5853
|
+
spatialMemory,
|
|
5854
|
+
hologramState,
|
|
5855
|
+
state: this.state,
|
|
5856
|
+
builtins: this.builtins,
|
|
5857
|
+
vr: this.vrContext
|
|
5858
|
+
};
|
|
5859
|
+
}
|
|
5860
|
+
reset() {
|
|
5861
|
+
this.unmount();
|
|
5862
|
+
this.state = createState({});
|
|
5863
|
+
this.mounted = false;
|
|
5864
|
+
}
|
|
5865
|
+
updateAnimations() {
|
|
5866
|
+
this.update(1 / 60);
|
|
5867
|
+
}
|
|
5868
|
+
updateParticles(delta) {
|
|
5869
|
+
this.update(delta);
|
|
5870
|
+
}
|
|
5871
|
+
getHologramStates() {
|
|
5872
|
+
return this.getContext().hologramState;
|
|
5873
|
+
}
|
|
5874
|
+
setState(updates) {
|
|
5875
|
+
this.state.update(updates);
|
|
5876
|
+
}
|
|
5877
|
+
emit(event, payload) {
|
|
5878
|
+
const handlers = this.eventHandlers.get(event);
|
|
5879
|
+
if (handlers) {
|
|
5880
|
+
handlers.forEach((handler) => {
|
|
5881
|
+
try {
|
|
5882
|
+
handler(payload);
|
|
5883
|
+
} catch (error) {
|
|
5884
|
+
console.error(`Error in event handler for ${event}:`, error);
|
|
5885
|
+
}
|
|
5886
|
+
});
|
|
5887
|
+
}
|
|
5888
|
+
eventBus.emit(event, payload);
|
|
5889
|
+
}
|
|
5890
|
+
updateEntity(id, properties) {
|
|
5891
|
+
if (!this.rootInstance) return false;
|
|
5892
|
+
let found = false;
|
|
5893
|
+
const traverse = (instance) => {
|
|
5894
|
+
if (instance.node.id === id) {
|
|
5895
|
+
instance.node.properties = { ...instance.node.properties, ...properties };
|
|
5896
|
+
if (this.options.renderer && instance.renderedNode) {
|
|
5897
|
+
this.options.renderer.updateElement(instance.renderedNode, properties);
|
|
5898
|
+
}
|
|
5899
|
+
found = true;
|
|
5900
|
+
}
|
|
5901
|
+
instance.children.forEach(traverse);
|
|
5902
|
+
};
|
|
5903
|
+
traverse(this.rootInstance);
|
|
5904
|
+
return found;
|
|
5905
|
+
}
|
|
5906
|
+
on(event, handler) {
|
|
5907
|
+
if (!this.eventHandlers.has(event)) {
|
|
5908
|
+
this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
5909
|
+
}
|
|
5910
|
+
this.eventHandlers.get(event).add(handler);
|
|
5911
|
+
return () => {
|
|
5912
|
+
this.eventHandlers.get(event)?.delete(handler);
|
|
5913
|
+
};
|
|
5914
|
+
}
|
|
5915
|
+
// ==========================================================================
|
|
5916
|
+
// HOT-RELOAD & MIGRATION
|
|
5917
|
+
// ==========================================================================
|
|
5918
|
+
async hotReload(newAst) {
|
|
5919
|
+
const newTemplates = this.findAllTemplates(newAst.root);
|
|
5920
|
+
for (const [name, newNode] of newTemplates) {
|
|
5921
|
+
const oldNode = this.templates.get(name);
|
|
5922
|
+
if (oldNode && newNode.version !== oldNode.version) {
|
|
5923
|
+
const result = await this.hotReloader.reload(newNode);
|
|
5924
|
+
if (result.success) {
|
|
5925
|
+
this.templates.set(name, newNode);
|
|
5926
|
+
} else {
|
|
5927
|
+
console.error(`[Hot-Reload] Failed for template "${name}":`, result.error);
|
|
5928
|
+
}
|
|
5929
|
+
} else {
|
|
5930
|
+
this.templates.set(name, newNode);
|
|
5931
|
+
this.hotReloader.registerTemplate(newNode);
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
if (newAst.version !== void 0 && newAst.version !== this.ast.version) {
|
|
5935
|
+
const result = await this.hotReloader.reload({
|
|
5936
|
+
type: "Template",
|
|
5937
|
+
name: "@program",
|
|
5938
|
+
version: newAst.version,
|
|
5939
|
+
migrations: newAst.migrations,
|
|
5940
|
+
state: { type: "State", properties: [] },
|
|
5941
|
+
properties: [],
|
|
5942
|
+
actions: [],
|
|
5943
|
+
traits: []
|
|
5944
|
+
});
|
|
5945
|
+
if (!result.success) {
|
|
5946
|
+
console.error(`[Hot-Reload] Global program migration failed:`, result.error);
|
|
5947
|
+
} else {
|
|
5948
|
+
}
|
|
5949
|
+
}
|
|
5950
|
+
this.ast = newAst;
|
|
5951
|
+
}
|
|
5952
|
+
/**
|
|
5953
|
+
* Creates a Map proxy that reflects the reactive state.
|
|
5954
|
+
* This allowed the HotReloader (designed for Map-based state) to work with
|
|
5955
|
+
* the runtime's Record-based reactive state.
|
|
5956
|
+
*/
|
|
5957
|
+
createStateMapProxy() {
|
|
5958
|
+
const runtime = this;
|
|
5959
|
+
return {
|
|
5960
|
+
get(key) {
|
|
5961
|
+
return runtime.state.get(key);
|
|
5962
|
+
},
|
|
5963
|
+
set(key, value) {
|
|
5964
|
+
runtime.state.set(key, value);
|
|
5965
|
+
return this;
|
|
5966
|
+
},
|
|
5967
|
+
has(key) {
|
|
5968
|
+
return runtime.state.get(key) !== void 0;
|
|
5969
|
+
},
|
|
5970
|
+
delete(key) {
|
|
5971
|
+
runtime.state.set(key, void 0);
|
|
5972
|
+
return true;
|
|
5973
|
+
},
|
|
5974
|
+
clear() {
|
|
5975
|
+
},
|
|
5976
|
+
get size() {
|
|
5977
|
+
return Object.keys(runtime.state.getSnapshot()).length;
|
|
5978
|
+
},
|
|
5979
|
+
forEach(cb) {
|
|
5980
|
+
const snap = runtime.state.getSnapshot();
|
|
5981
|
+
Object.entries(snap).forEach(([k, v]) => cb(v, k, this));
|
|
5982
|
+
},
|
|
5983
|
+
[Symbol.iterator]() {
|
|
5984
|
+
const snap = runtime.state.getSnapshot();
|
|
5985
|
+
return Object.entries(snap)[Symbol.iterator]();
|
|
5986
|
+
},
|
|
5987
|
+
entries() {
|
|
5988
|
+
const snap = runtime.state.getSnapshot();
|
|
5989
|
+
return Object.entries(snap)[Symbol.iterator]();
|
|
5990
|
+
},
|
|
5991
|
+
keys() {
|
|
5992
|
+
const snap = runtime.state.getSnapshot();
|
|
5993
|
+
return Object.keys(snap)[Symbol.iterator]();
|
|
5994
|
+
},
|
|
5995
|
+
values() {
|
|
5996
|
+
const snap = runtime.state.getSnapshot();
|
|
5997
|
+
return Object.values(snap)[Symbol.iterator]();
|
|
5998
|
+
}
|
|
5999
|
+
};
|
|
6000
|
+
}
|
|
6001
|
+
findInstanceById(id, root = this.rootInstance) {
|
|
6002
|
+
if (!root) return null;
|
|
6003
|
+
if (root.__holo_id === id || root.node.id === id) return root;
|
|
6004
|
+
for (const child of root.children) {
|
|
6005
|
+
const found = this.findInstanceById(id, child);
|
|
6006
|
+
if (found) return found;
|
|
6007
|
+
}
|
|
6008
|
+
return null;
|
|
6009
|
+
}
|
|
6010
|
+
/**
|
|
6011
|
+
* Executes a block of HoloScript+ statements
|
|
6012
|
+
*/
|
|
6013
|
+
async executeStatementBlock(instance, body) {
|
|
6014
|
+
for (const stmt of body) {
|
|
6015
|
+
await this.executeStatement(instance, stmt);
|
|
6016
|
+
}
|
|
6017
|
+
}
|
|
6018
|
+
/**
|
|
6019
|
+
* Executes a single HoloScript+ statement
|
|
6020
|
+
*/
|
|
6021
|
+
async executeStatement(instance, stmt) {
|
|
6022
|
+
const context = {
|
|
6023
|
+
...this.state.getSnapshot(),
|
|
6024
|
+
node: instance.node,
|
|
6025
|
+
self: instance.node,
|
|
6026
|
+
props: instance.node.properties || {}
|
|
6027
|
+
};
|
|
6028
|
+
this.evaluator.updateContext(context);
|
|
6029
|
+
try {
|
|
6030
|
+
switch (stmt.type) {
|
|
6031
|
+
case "Assignment": {
|
|
6032
|
+
const value = this.evaluator.evaluate(String(stmt.value));
|
|
6033
|
+
const target = stmt.target;
|
|
6034
|
+
if (target.startsWith("props.")) {
|
|
6035
|
+
const propName = target.split(".")[1];
|
|
6036
|
+
if (instance.node.properties) {
|
|
6037
|
+
instance.node.properties[propName] = value;
|
|
6038
|
+
}
|
|
6039
|
+
} else if (target.startsWith("state.")) {
|
|
6040
|
+
const stateKey = target.split(".")[1];
|
|
6041
|
+
this.state.set(stateKey, value);
|
|
6042
|
+
} else {
|
|
6043
|
+
context[target] = value;
|
|
6044
|
+
}
|
|
6045
|
+
break;
|
|
6046
|
+
}
|
|
6047
|
+
case "MethodCall": {
|
|
6048
|
+
const args = (stmt.arguments || []).map((arg) => this.evaluator.evaluate(String(arg)));
|
|
6049
|
+
const method = this.builtins[stmt.method];
|
|
6050
|
+
if (typeof method === "function") {
|
|
6051
|
+
await method(...args);
|
|
6052
|
+
}
|
|
6053
|
+
break;
|
|
6054
|
+
}
|
|
6055
|
+
case "IfStatement": {
|
|
6056
|
+
const condition = this.evaluator.evaluate(String(stmt.condition));
|
|
6057
|
+
if (condition) {
|
|
6058
|
+
await this.executeStatementBlock(instance, stmt.consequent);
|
|
6059
|
+
} else if (stmt.alternate) {
|
|
6060
|
+
await this.executeStatementBlock(instance, stmt.alternate);
|
|
6061
|
+
}
|
|
6062
|
+
break;
|
|
6063
|
+
}
|
|
6064
|
+
case "EmitStatement": {
|
|
6065
|
+
const data = stmt.data ? this.evaluator.evaluate(String(stmt.data)) : void 0;
|
|
6066
|
+
this.emit(stmt.event, data);
|
|
6067
|
+
break;
|
|
6068
|
+
}
|
|
6069
|
+
// Add more statement types as needed
|
|
6070
|
+
default:
|
|
6071
|
+
console.warn(`[Runtime] Unsupported statement type: ${stmt.type}`);
|
|
6072
|
+
}
|
|
6073
|
+
} catch (error) {
|
|
6074
|
+
console.error(`[Runtime] Execution error in statement ${stmt.type}:`, error);
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
migrateInstancesOfTemplate(name, oldVersion, newTemplate) {
|
|
6078
|
+
const instances = this.findAllInstancesOfTemplate(name);
|
|
6079
|
+
for (const instance of instances) {
|
|
6080
|
+
this.migrateInstance(instance, oldVersion, newTemplate);
|
|
6081
|
+
}
|
|
6082
|
+
}
|
|
6083
|
+
findAllInstancesOfTemplate(name, root = this.rootInstance) {
|
|
6084
|
+
if (!root) return [];
|
|
6085
|
+
const results = [];
|
|
6086
|
+
if (root.templateName === name) {
|
|
6087
|
+
results.push(root);
|
|
6088
|
+
}
|
|
6089
|
+
for (const child of root.children) {
|
|
6090
|
+
results.push(...this.findAllInstancesOfTemplate(name, child));
|
|
6091
|
+
}
|
|
6092
|
+
return results;
|
|
6093
|
+
}
|
|
6094
|
+
migrateInstance(instance, oldVersion, newTemplate) {
|
|
6095
|
+
const _context = this.createTraitContext(instance);
|
|
6096
|
+
const currentProperties = { ...instance.node.properties || {} };
|
|
6097
|
+
const newNode = this.cloneNodeWithContext(newTemplate, {
|
|
6098
|
+
position: currentProperties.position
|
|
6099
|
+
});
|
|
6100
|
+
const oldNode = instance.node;
|
|
6101
|
+
Object.keys(oldNode).forEach(
|
|
6102
|
+
(key) => delete oldNode[key]
|
|
6103
|
+
);
|
|
6104
|
+
Object.assign(oldNode, newNode);
|
|
6105
|
+
oldNode.properties = { ...oldNode.properties || {}, ...currentProperties };
|
|
6106
|
+
instance.templateVersion = newTemplate.version;
|
|
6107
|
+
const migrations = newTemplate.migrations || [];
|
|
6108
|
+
const migration = migrations.find((m) => m.fromVersion === oldVersion);
|
|
6109
|
+
if (migration && migration.body) {
|
|
6110
|
+
this.executeMigrationCode(instance, migration.body);
|
|
6111
|
+
}
|
|
6112
|
+
if (this.options.renderer && instance.renderedNode) {
|
|
6113
|
+
const properties = this.evaluateProperties(instance.node.properties || {});
|
|
6114
|
+
this.options.renderer.updateElement(instance.renderedNode, properties);
|
|
6115
|
+
}
|
|
6116
|
+
}
|
|
6117
|
+
executeMigrationCode(instance, code) {
|
|
6118
|
+
const stateProxy = new Proxy(this.state, {
|
|
6119
|
+
get: (target, prop) => {
|
|
6120
|
+
if (typeof prop === "string" && prop in target && typeof target[prop] === "function") {
|
|
6121
|
+
return target[prop].bind(target);
|
|
6122
|
+
}
|
|
6123
|
+
return target.get(String(prop));
|
|
6124
|
+
},
|
|
6125
|
+
set: (target, prop, value) => {
|
|
6126
|
+
target.set(String(prop), value);
|
|
6127
|
+
return true;
|
|
6128
|
+
}
|
|
6129
|
+
});
|
|
6130
|
+
const sandbox = {
|
|
6131
|
+
...this.builtins,
|
|
6132
|
+
state: stateProxy,
|
|
6133
|
+
node: instance.node,
|
|
6134
|
+
self: instance.node,
|
|
6135
|
+
props: instance.node.properties || {},
|
|
6136
|
+
renameProperty: (oldName, newName) => {
|
|
6137
|
+
if (instance.node.properties && instance.node.properties[oldName] !== void 0) {
|
|
6138
|
+
instance.node.properties[newName] = instance.node.properties[oldName];
|
|
6139
|
+
delete instance.node.properties[oldName];
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
};
|
|
6143
|
+
try {
|
|
6144
|
+
const fn = new Function(...Object.keys(sandbox), code);
|
|
6145
|
+
fn(...Object.values(sandbox));
|
|
6146
|
+
} catch (error) {
|
|
6147
|
+
console.error(`[Runtime] Migration execution failed in "${instance.templateName}":`, error);
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
// ==========================================================================
|
|
6151
|
+
// VR INTEGRATION
|
|
6152
|
+
// ==========================================================================
|
|
6153
|
+
updateVRContext(context) {
|
|
6154
|
+
this.vrContext = context;
|
|
6155
|
+
}
|
|
6156
|
+
handleVREvent(event, node) {
|
|
6157
|
+
const instance = this.findInstance(node);
|
|
6158
|
+
if (!instance) return;
|
|
6159
|
+
const traitContext = this.createTraitContext(instance);
|
|
6160
|
+
this.traitRegistry.handleEventForAllTraits(node, traitContext, event);
|
|
6161
|
+
const hookMapping = {
|
|
6162
|
+
grab_start: "on_grab",
|
|
6163
|
+
grab_end: "on_release",
|
|
6164
|
+
hover_enter: "on_hover_enter",
|
|
6165
|
+
hover_exit: "on_hover_exit",
|
|
6166
|
+
point_enter: "on_point_enter",
|
|
6167
|
+
point_exit: "on_point_exit",
|
|
6168
|
+
collision: "on_collision",
|
|
6169
|
+
trigger_enter: "on_trigger_enter",
|
|
6170
|
+
trigger_exit: "on_trigger_exit",
|
|
6171
|
+
click: "on_click"
|
|
6172
|
+
};
|
|
6173
|
+
const hook = hookMapping[event.type];
|
|
6174
|
+
if (hook) {
|
|
6175
|
+
this.callLifecycle(instance, hook, event);
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
findInstance(node, root = this.rootInstance) {
|
|
6179
|
+
if (!root) return null;
|
|
6180
|
+
if (root.node === node) return root;
|
|
6181
|
+
for (const child of root.children) {
|
|
6182
|
+
const found = this.findInstance(node, child);
|
|
6183
|
+
if (found) return found;
|
|
6184
|
+
}
|
|
6185
|
+
return null;
|
|
6186
|
+
}
|
|
6187
|
+
// ==========================================================================
|
|
6188
|
+
// TEMPLATES & SPAWNING
|
|
6189
|
+
// ==========================================================================
|
|
6190
|
+
registerTemplate(name, node) {
|
|
6191
|
+
this.templates.set(name, node);
|
|
6192
|
+
}
|
|
6193
|
+
spawnTemplate(name, position) {
|
|
6194
|
+
const template = this.templates.get(name);
|
|
6195
|
+
if (!template) {
|
|
6196
|
+
throw new Error(`Template "${name}" not found`);
|
|
6197
|
+
}
|
|
6198
|
+
const cloned = this.cloneNodeWithContext(template, { position });
|
|
6199
|
+
if (!cloned.properties) cloned.properties = {};
|
|
6200
|
+
cloned.properties.position = position;
|
|
6201
|
+
if (this.rootInstance) {
|
|
6202
|
+
const instance = this.instantiateNode(cloned, this.rootInstance);
|
|
6203
|
+
instance.templateName = name;
|
|
6204
|
+
instance.templateVersion = typeof template.version === "number" ? template.version : parseInt(template.version || "0");
|
|
6205
|
+
this.rootInstance.children.push(instance);
|
|
6206
|
+
if (this.options.renderer && this.rootInstance.renderedNode) {
|
|
6207
|
+
this.options.renderer.appendChild(this.rootInstance.renderedNode, instance.renderedNode);
|
|
6208
|
+
}
|
|
6209
|
+
this.callLifecycle(instance, "on_mount");
|
|
6210
|
+
}
|
|
6211
|
+
return cloned;
|
|
6212
|
+
}
|
|
6213
|
+
destroyNode(node) {
|
|
6214
|
+
const instance = this.findInstance(node);
|
|
6215
|
+
if (!instance) return;
|
|
6216
|
+
this.callLifecycle(instance, "on_unmount");
|
|
6217
|
+
if (instance.parent) {
|
|
6218
|
+
const index = instance.parent.children.indexOf(instance);
|
|
6219
|
+
if (index > -1) {
|
|
6220
|
+
instance.parent.children.splice(index, 1);
|
|
6221
|
+
}
|
|
6222
|
+
if (this.options.renderer && instance.parent.renderedNode && instance.renderedNode) {
|
|
6223
|
+
this.options.renderer.removeChild(instance.parent.renderedNode, instance.renderedNode);
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
this.destroyInstance(instance);
|
|
6227
|
+
}
|
|
6228
|
+
};
|
|
6229
|
+
function createBuiltins(runtime) {
|
|
6230
|
+
return {
|
|
6231
|
+
log: (...args) => console.log("[HoloScript]", ...args),
|
|
6232
|
+
warn: (...args) => console.warn("[HoloScript]", ...args),
|
|
6233
|
+
error: (...args) => console.error("[HoloScript]", ...args),
|
|
6234
|
+
Math,
|
|
6235
|
+
range: (start, end, step = 1) => {
|
|
6236
|
+
const result = [];
|
|
6237
|
+
if (step > 0) {
|
|
6238
|
+
for (let i = start; i < end; i += step) {
|
|
6239
|
+
result.push(i);
|
|
6240
|
+
}
|
|
6241
|
+
} else if (step < 0) {
|
|
6242
|
+
for (let i = start; i > end; i += step) {
|
|
6243
|
+
result.push(i);
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
return result;
|
|
6247
|
+
},
|
|
6248
|
+
interpolate_color: (t, from, to) => {
|
|
6249
|
+
const parseHex = (hex) => {
|
|
6250
|
+
const clean = hex.replace("#", "");
|
|
6251
|
+
return [
|
|
6252
|
+
parseInt(clean.substring(0, 2), 16),
|
|
6253
|
+
parseInt(clean.substring(2, 4), 16),
|
|
6254
|
+
parseInt(clean.substring(4, 6), 16)
|
|
6255
|
+
];
|
|
6256
|
+
};
|
|
6257
|
+
const toHex = (r, g, b) => {
|
|
6258
|
+
const clamp = (v) => Math.max(0, Math.min(255, Math.round(v)));
|
|
6259
|
+
return `#${clamp(r).toString(16).padStart(2, "0")}${clamp(g).toString(16).padStart(2, "0")}${clamp(b).toString(16).padStart(2, "0")}`;
|
|
6260
|
+
};
|
|
6261
|
+
const [r1, g1, b1] = typeof from === "string" ? parseHex(from) : [from.r || 0, from.g || 0, from.b || 0];
|
|
6262
|
+
const [r2, g2, b2] = typeof to === "string" ? parseHex(to) : [to.r || 0, to.g || 0, to.b || 0];
|
|
6263
|
+
return toHex(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t);
|
|
6264
|
+
},
|
|
6265
|
+
distance_to: (point) => {
|
|
6266
|
+
const viewer = runtime.vrContext.headset.position;
|
|
6267
|
+
return Math.sqrt(
|
|
6268
|
+
Math.pow(point.x - viewer.x, 2) + Math.pow(point.y - viewer.y, 2) + Math.pow(point.z - viewer.z, 2)
|
|
6269
|
+
);
|
|
6270
|
+
},
|
|
6271
|
+
distance_to_viewer: () => {
|
|
6272
|
+
return 0;
|
|
6273
|
+
},
|
|
6274
|
+
hand_position: (handId) => {
|
|
6275
|
+
const hand = handId === "left" ? runtime.vrContext.hands.left : runtime.vrContext.hands.right;
|
|
6276
|
+
return hand?.position || { x: 0, y: 0, z: 0 };
|
|
6277
|
+
},
|
|
6278
|
+
hand_velocity: (handId) => {
|
|
6279
|
+
const hand = handId === "left" ? runtime.vrContext.hands.left : runtime.vrContext.hands.right;
|
|
6280
|
+
return hand?.velocity || { x: 0, y: 0, z: 0 };
|
|
6281
|
+
},
|
|
6282
|
+
dominant_hand: () => {
|
|
6283
|
+
return runtime.vrContext.hands.right || runtime.vrContext.hands.left || {
|
|
6284
|
+
id: "right",
|
|
6285
|
+
position: [0, 0, 0],
|
|
6286
|
+
rotation: { x: 0, y: 0, z: 0 },
|
|
6287
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
6288
|
+
gripStrength: 0,
|
|
6289
|
+
pinchStrength: 0
|
|
6290
|
+
};
|
|
6291
|
+
},
|
|
6292
|
+
play_sound: (source, options) => {
|
|
6293
|
+
runtime.emit("play_sound", { source, ...options });
|
|
6294
|
+
},
|
|
6295
|
+
haptic_feedback: (hand, intensity) => {
|
|
6296
|
+
const handId = typeof hand === "string" ? hand : hand.id;
|
|
6297
|
+
runtime.emit("haptic", { hand: handId, intensity });
|
|
6298
|
+
},
|
|
6299
|
+
haptic_pulse: (intensity) => {
|
|
6300
|
+
runtime.emit("haptic", { hand: "both", intensity });
|
|
6301
|
+
},
|
|
6302
|
+
apply_velocity: (node, velocity) => {
|
|
6303
|
+
runtime.emit("apply_velocity", { node, velocity });
|
|
6304
|
+
},
|
|
6305
|
+
spawn: (template, position) => {
|
|
6306
|
+
return runtime.spawnTemplate(template, position);
|
|
6307
|
+
},
|
|
6308
|
+
assistant_generate: (prompt, context) => {
|
|
6309
|
+
runtime.emit("assistant_generate", { prompt, context });
|
|
6310
|
+
},
|
|
6311
|
+
destroy: (node) => {
|
|
6312
|
+
runtime.destroyNode(node);
|
|
6313
|
+
},
|
|
6314
|
+
api_call: async (url, method, body) => {
|
|
6315
|
+
const response = await fetch(url, {
|
|
6316
|
+
method,
|
|
6317
|
+
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
6318
|
+
body: body ? JSON.stringify(body) : void 0
|
|
6319
|
+
});
|
|
6320
|
+
return response.json();
|
|
6321
|
+
},
|
|
6322
|
+
open_modal: (modalId) => {
|
|
6323
|
+
runtime.emit("open_modal", { id: modalId });
|
|
6324
|
+
},
|
|
6325
|
+
close_modal: (modalId) => {
|
|
6326
|
+
runtime.emit("close_modal", { id: modalId });
|
|
6327
|
+
},
|
|
6328
|
+
setTimeout: (callback, delay) => {
|
|
6329
|
+
return window.setTimeout(callback, delay);
|
|
6330
|
+
},
|
|
6331
|
+
clearTimeout: (id) => {
|
|
6332
|
+
window.clearTimeout(id);
|
|
6333
|
+
},
|
|
6334
|
+
animate: (node, properties, options = {}) => {
|
|
6335
|
+
if (options.sync) {
|
|
6336
|
+
runtime.emit("network_animation", {
|
|
6337
|
+
objectId: node.id,
|
|
6338
|
+
properties,
|
|
6339
|
+
options,
|
|
6340
|
+
timestamp: Date.now()
|
|
6341
|
+
});
|
|
6342
|
+
}
|
|
6343
|
+
if (node.properties) {
|
|
6344
|
+
Object.assign(node.properties, properties);
|
|
6345
|
+
}
|
|
6346
|
+
runtime.emit("animate", { node, properties, options });
|
|
6347
|
+
},
|
|
6348
|
+
transition: (targetScene, options = {}) => {
|
|
6349
|
+
if (options.audio) {
|
|
6350
|
+
runtime.emit("play_sound", { source: options.audio });
|
|
6351
|
+
}
|
|
6352
|
+
runtime.emit("scene_transition", { target: targetScene, options });
|
|
6353
|
+
}
|
|
6354
|
+
};
|
|
6355
|
+
}
|
|
6356
|
+
function createRuntime(ast, options = {}) {
|
|
6357
|
+
return new HoloScriptPlusRuntimeImpl(ast, options);
|
|
6358
|
+
}
|
|
6359
|
+
|
|
6360
|
+
// src/runtime/InstancedRenderer.ts
|
|
6361
|
+
function getThree() {
|
|
6362
|
+
return globalThis.THREE;
|
|
6363
|
+
}
|
|
6364
|
+
var InstancedRenderer = class {
|
|
6365
|
+
batches = /* @__PURE__ */ new Map();
|
|
6366
|
+
objects = /* @__PURE__ */ new Map();
|
|
6367
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6368
|
+
scene;
|
|
6369
|
+
// THREE.Scene
|
|
6370
|
+
maxInstancesPerBatch = 1e3;
|
|
6371
|
+
// Configurable
|
|
6372
|
+
enabled = true;
|
|
6373
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6374
|
+
constructor(scene, maxInstancesPerBatch = 1e3) {
|
|
6375
|
+
this.scene = scene;
|
|
6376
|
+
this.maxInstancesPerBatch = maxInstancesPerBatch;
|
|
6377
|
+
}
|
|
6378
|
+
/**
|
|
6379
|
+
* Add object to instanced rendering
|
|
6380
|
+
*/
|
|
6381
|
+
addInstance(id, geometryType, materialType, position, rotation = [0, 0, 0], scale = [1, 1, 1], color) {
|
|
6382
|
+
if (!this.enabled) return false;
|
|
6383
|
+
const batchKey = `${geometryType}_${materialType}`;
|
|
6384
|
+
let batch = this.batches.get(batchKey);
|
|
6385
|
+
if (!batch) {
|
|
6386
|
+
const newBatch = this.createBatch(batchKey, geometryType, materialType);
|
|
6387
|
+
if (!newBatch) return false;
|
|
6388
|
+
batch = newBatch;
|
|
6389
|
+
}
|
|
6390
|
+
if (batch.count >= batch.maxInstances && batch.freeIndices.length === 0) {
|
|
6391
|
+
console.warn(
|
|
6392
|
+
`[InstancedRenderer] Batch ${batchKey} is full (${batch.maxInstances} instances)`
|
|
6393
|
+
);
|
|
6394
|
+
return false;
|
|
6395
|
+
}
|
|
6396
|
+
const instanceIndex = batch.freeIndices.length > 0 ? batch.freeIndices.pop() : batch.count++;
|
|
6397
|
+
const matrix = this.createTransformMatrix(position, rotation, scale);
|
|
6398
|
+
const instance = {
|
|
6399
|
+
id,
|
|
6400
|
+
batchKey,
|
|
6401
|
+
instanceIndex,
|
|
6402
|
+
matrix,
|
|
6403
|
+
color,
|
|
6404
|
+
visible: true
|
|
6405
|
+
};
|
|
6406
|
+
this.objects.set(id, instance);
|
|
6407
|
+
batch.instanceMap.set(id, instanceIndex);
|
|
6408
|
+
batch.instancedMesh.setMatrixAt(instanceIndex, this.arrayToMatrix4(matrix));
|
|
6409
|
+
if (color && batch.instancedMesh.instanceColor) {
|
|
6410
|
+
const THREE = getThree();
|
|
6411
|
+
batch.instancedMesh.setColorAt(instanceIndex, new THREE.Color(color[0], color[1], color[2]));
|
|
6412
|
+
}
|
|
6413
|
+
batch.needsUpdate = true;
|
|
6414
|
+
return true;
|
|
6415
|
+
}
|
|
6416
|
+
/**
|
|
6417
|
+
* Remove instance from rendering
|
|
6418
|
+
*/
|
|
6419
|
+
removeInstance(id) {
|
|
6420
|
+
const instance = this.objects.get(id);
|
|
6421
|
+
if (!instance) return false;
|
|
6422
|
+
const batch = this.batches.get(instance.batchKey);
|
|
6423
|
+
if (!batch) return false;
|
|
6424
|
+
const matrix = this.createTransformMatrix([999999, 999999, 999999], [0, 0, 0], [0, 0, 0]);
|
|
6425
|
+
batch.instancedMesh.setMatrixAt(instance.instanceIndex, this.arrayToMatrix4(matrix));
|
|
6426
|
+
batch.needsUpdate = true;
|
|
6427
|
+
batch.freeIndices.push(instance.instanceIndex);
|
|
6428
|
+
batch.instanceMap.delete(id);
|
|
6429
|
+
this.objects.delete(id);
|
|
6430
|
+
return true;
|
|
6431
|
+
}
|
|
6432
|
+
/**
|
|
6433
|
+
* Update instance transform
|
|
6434
|
+
*/
|
|
6435
|
+
updateInstance(id, position, rotation, scale) {
|
|
6436
|
+
const instance = this.objects.get(id);
|
|
6437
|
+
if (!instance) return false;
|
|
6438
|
+
const batch = this.batches.get(instance.batchKey);
|
|
6439
|
+
if (!batch) return false;
|
|
6440
|
+
const current = this.matrixToTransform(instance.matrix);
|
|
6441
|
+
const newPosition = position || current.position;
|
|
6442
|
+
const newRotation = rotation || current.rotation;
|
|
6443
|
+
const newScale = scale || current.scale;
|
|
6444
|
+
const matrix = this.createTransformMatrix(newPosition, newRotation, newScale);
|
|
6445
|
+
instance.matrix = matrix;
|
|
6446
|
+
batch.instancedMesh.setMatrixAt(instance.instanceIndex, this.arrayToMatrix4(matrix));
|
|
6447
|
+
batch.needsUpdate = true;
|
|
6448
|
+
return true;
|
|
6449
|
+
}
|
|
6450
|
+
/**
|
|
6451
|
+
* Update instance color
|
|
6452
|
+
*/
|
|
6453
|
+
updateInstanceColor(id, color) {
|
|
6454
|
+
const instance = this.objects.get(id);
|
|
6455
|
+
if (!instance) return false;
|
|
6456
|
+
const batch = this.batches.get(instance.batchKey);
|
|
6457
|
+
if (!batch || !batch.instancedMesh.instanceColor) return false;
|
|
6458
|
+
const THREE = getThree();
|
|
6459
|
+
batch.instancedMesh.setColorAt(
|
|
6460
|
+
instance.instanceIndex,
|
|
6461
|
+
new THREE.Color(color[0], color[1], color[2])
|
|
6462
|
+
);
|
|
6463
|
+
instance.color = color;
|
|
6464
|
+
batch.needsUpdate = true;
|
|
6465
|
+
return true;
|
|
6466
|
+
}
|
|
6467
|
+
/**
|
|
6468
|
+
* Update all batches (call once per frame)
|
|
6469
|
+
*/
|
|
6470
|
+
update() {
|
|
6471
|
+
for (const batch of this.batches.values()) {
|
|
6472
|
+
if (batch.needsUpdate) {
|
|
6473
|
+
batch.instancedMesh.instanceMatrix.needsUpdate = true;
|
|
6474
|
+
if (batch.instancedMesh.instanceColor) {
|
|
6475
|
+
batch.instancedMesh.instanceColor.needsUpdate = true;
|
|
6476
|
+
}
|
|
6477
|
+
batch.needsUpdate = false;
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6481
|
+
/**
|
|
6482
|
+
* Create a new batch
|
|
6483
|
+
*/
|
|
6484
|
+
createBatch(key, geometryType, materialType) {
|
|
6485
|
+
if (!getThree()) return null;
|
|
6486
|
+
const THREE = getThree();
|
|
6487
|
+
const geometry = this.createGeometry(geometryType);
|
|
6488
|
+
if (!geometry) return null;
|
|
6489
|
+
const material = this.createMaterial(materialType);
|
|
6490
|
+
if (!material) return null;
|
|
6491
|
+
const instancedMesh = new THREE.InstancedMesh(geometry, material, this.maxInstancesPerBatch);
|
|
6492
|
+
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
|
6493
|
+
instancedMesh.castShadow = true;
|
|
6494
|
+
instancedMesh.receiveShadow = true;
|
|
6495
|
+
instancedMesh.instanceColor = new THREE.InstancedBufferAttribute(
|
|
6496
|
+
new Float32Array(this.maxInstancesPerBatch * 3),
|
|
6497
|
+
3
|
|
6498
|
+
);
|
|
6499
|
+
this.scene.add(instancedMesh);
|
|
6500
|
+
const batch = {
|
|
6501
|
+
key,
|
|
6502
|
+
geometryType,
|
|
6503
|
+
materialType,
|
|
6504
|
+
instancedMesh,
|
|
6505
|
+
maxInstances: this.maxInstancesPerBatch,
|
|
6506
|
+
count: 0,
|
|
6507
|
+
instanceMap: /* @__PURE__ */ new Map(),
|
|
6508
|
+
freeIndices: [],
|
|
6509
|
+
needsUpdate: false
|
|
6510
|
+
};
|
|
6511
|
+
this.batches.set(key, batch);
|
|
6512
|
+
return batch;
|
|
6513
|
+
}
|
|
6514
|
+
/**
|
|
6515
|
+
* Create geometry by type
|
|
6516
|
+
*/
|
|
6517
|
+
createGeometry(type) {
|
|
6518
|
+
const THREE = getThree();
|
|
6519
|
+
const geometryMap = {
|
|
6520
|
+
// ===== CORE PRIMITIVES =====
|
|
6521
|
+
box: () => new THREE.BoxGeometry(1, 1, 1),
|
|
6522
|
+
sphere: () => new THREE.SphereGeometry(0.5, 16, 16),
|
|
6523
|
+
cylinder: () => new THREE.CylinderGeometry(0.5, 0.5, 1, 16),
|
|
6524
|
+
cone: () => new THREE.ConeGeometry(0.5, 1, 16),
|
|
6525
|
+
plane: () => new THREE.PlaneGeometry(1, 1),
|
|
6526
|
+
torus: () => new THREE.TorusGeometry(0.5, 0.2, 16, 50),
|
|
6527
|
+
ring: () => new THREE.RingGeometry(0.25, 0.5, 16),
|
|
6528
|
+
circle: () => new THREE.CircleGeometry(0.5, 16),
|
|
6529
|
+
// ===== ADVANCED GEOMETRIES =====
|
|
6530
|
+
capsule: () => new THREE.CapsuleGeometry(0.25, 0.5, 4, 8),
|
|
6531
|
+
torusknot: () => new THREE.TorusKnotGeometry(0.5, 0.15, 64, 8, 2, 3),
|
|
6532
|
+
// ===== POLYHEDRONS =====
|
|
6533
|
+
dodecahedron: () => new THREE.DodecahedronGeometry(0.5, 0),
|
|
6534
|
+
icosahedron: () => new THREE.IcosahedronGeometry(0.5, 0),
|
|
6535
|
+
octahedron: () => new THREE.OctahedronGeometry(0.5, 0),
|
|
6536
|
+
tetrahedron: () => new THREE.TetrahedronGeometry(0.5, 0)
|
|
6537
|
+
};
|
|
6538
|
+
return geometryMap[type]?.() || new THREE.BoxGeometry(1, 1, 1);
|
|
6539
|
+
}
|
|
6540
|
+
/**
|
|
6541
|
+
* Create material by type
|
|
6542
|
+
*/
|
|
6543
|
+
createMaterial(_type) {
|
|
6544
|
+
const THREE = getThree();
|
|
6545
|
+
return new THREE.MeshStandardMaterial({
|
|
6546
|
+
color: 16777215,
|
|
6547
|
+
roughness: 0.7,
|
|
6548
|
+
metalness: 0.3,
|
|
6549
|
+
vertexColors: true
|
|
6550
|
+
// Enable per-instance colors
|
|
6551
|
+
});
|
|
6552
|
+
}
|
|
6553
|
+
/**
|
|
6554
|
+
* Create transform matrix from position, rotation, scale
|
|
6555
|
+
*/
|
|
6556
|
+
createTransformMatrix(position, rotation, scale) {
|
|
6557
|
+
const matrix = new Float32Array(16);
|
|
6558
|
+
const cx = Math.cos(rotation[0]);
|
|
6559
|
+
const sx = Math.sin(rotation[0]);
|
|
6560
|
+
const cy = Math.cos(rotation[1]);
|
|
6561
|
+
const sy = Math.sin(rotation[1]);
|
|
6562
|
+
const cz = Math.cos(rotation[2]);
|
|
6563
|
+
const sz = Math.sin(rotation[2]);
|
|
6564
|
+
matrix[0] = cy * cz * scale[0];
|
|
6565
|
+
matrix[1] = cy * sz * scale[0];
|
|
6566
|
+
matrix[2] = -sy * scale[0];
|
|
6567
|
+
matrix[3] = 0;
|
|
6568
|
+
matrix[4] = (sx * sy * cz - cx * sz) * scale[1];
|
|
6569
|
+
matrix[5] = (sx * sy * sz + cx * cz) * scale[1];
|
|
6570
|
+
matrix[6] = sx * cy * scale[1];
|
|
6571
|
+
matrix[7] = 0;
|
|
6572
|
+
matrix[8] = (cx * sy * cz + sx * sz) * scale[2];
|
|
6573
|
+
matrix[9] = (cx * sy * sz - sx * cz) * scale[2];
|
|
6574
|
+
matrix[10] = cx * cy * scale[2];
|
|
6575
|
+
matrix[11] = 0;
|
|
6576
|
+
matrix[12] = position[0];
|
|
6577
|
+
matrix[13] = position[1];
|
|
6578
|
+
matrix[14] = position[2];
|
|
6579
|
+
matrix[15] = 1;
|
|
6580
|
+
return matrix;
|
|
6581
|
+
}
|
|
6582
|
+
/**
|
|
6583
|
+
* Convert Float32Array to THREE.Matrix4
|
|
6584
|
+
*/
|
|
6585
|
+
arrayToMatrix4(array) {
|
|
6586
|
+
const THREE = getThree();
|
|
6587
|
+
const matrix = new THREE.Matrix4();
|
|
6588
|
+
matrix.fromArray(array);
|
|
6589
|
+
return matrix;
|
|
6590
|
+
}
|
|
6591
|
+
/**
|
|
6592
|
+
* Extract transform from matrix
|
|
6593
|
+
*/
|
|
6594
|
+
matrixToTransform(matrix) {
|
|
6595
|
+
const THREE = getThree();
|
|
6596
|
+
if (THREE) {
|
|
6597
|
+
const m = new THREE.Matrix4().fromArray(matrix);
|
|
6598
|
+
const pos = new THREE.Vector3();
|
|
6599
|
+
const quat = new THREE.Quaternion();
|
|
6600
|
+
const scl = new THREE.Vector3();
|
|
6601
|
+
m.decompose(pos, quat, scl);
|
|
6602
|
+
const euler = new THREE.Euler().setFromQuaternion(quat, "XYZ");
|
|
6603
|
+
return {
|
|
6604
|
+
position: [pos.x, pos.y, pos.z],
|
|
6605
|
+
rotation: [euler.x, euler.y, euler.z],
|
|
6606
|
+
scale: [scl.x, scl.y, scl.z]
|
|
6607
|
+
};
|
|
6608
|
+
}
|
|
6609
|
+
const position = [matrix[12], matrix[13], matrix[14]];
|
|
6610
|
+
const sx = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1] + matrix[2] * matrix[2]);
|
|
6611
|
+
const sy = Math.sqrt(matrix[4] * matrix[4] + matrix[5] * matrix[5] + matrix[6] * matrix[6]);
|
|
6612
|
+
const sz = Math.sqrt(matrix[8] * matrix[8] + matrix[9] * matrix[9] + matrix[10] * matrix[10]);
|
|
6613
|
+
const scale = [sx, sy, sz];
|
|
6614
|
+
let rx = 0, ry = 0, rz = 0;
|
|
6615
|
+
if (sx !== 0 && sy !== 0 && sz !== 0) {
|
|
6616
|
+
const m13 = matrix[8] / sz;
|
|
6617
|
+
ry = Math.asin(Math.max(-1, Math.min(1, m13)));
|
|
6618
|
+
if (Math.abs(m13) < 0.999999) {
|
|
6619
|
+
rx = Math.atan2(-matrix[9] / sz, matrix[10] / sz);
|
|
6620
|
+
rz = Math.atan2(-matrix[4] / sy, matrix[0] / sx);
|
|
6621
|
+
} else {
|
|
6622
|
+
rx = Math.atan2(matrix[6] / sy, matrix[5] / sy);
|
|
6623
|
+
rz = 0;
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
const rotation = [rx, ry, rz];
|
|
6627
|
+
return { position, rotation, scale };
|
|
6628
|
+
}
|
|
6629
|
+
/**
|
|
6630
|
+
* Get statistics
|
|
6631
|
+
*/
|
|
6632
|
+
getStatistics() {
|
|
6633
|
+
let totalInstances = 0;
|
|
6634
|
+
let memoryUsage = 0;
|
|
6635
|
+
for (const batch of this.batches.values()) {
|
|
6636
|
+
totalInstances += batch.count - batch.freeIndices.length;
|
|
6637
|
+
memoryUsage += batch.count * 76 / (1024 * 1024);
|
|
6638
|
+
}
|
|
6639
|
+
const drawCalls = this.batches.size;
|
|
6640
|
+
const nonInstancedDrawCalls = totalInstances;
|
|
6641
|
+
const improvement = nonInstancedDrawCalls > 0 ? `${((1 - drawCalls / nonInstancedDrawCalls) * 100).toFixed(1)}% fewer draw calls` : "N/A";
|
|
6642
|
+
return {
|
|
6643
|
+
batchCount: this.batches.size,
|
|
6644
|
+
totalInstances,
|
|
6645
|
+
drawCalls,
|
|
6646
|
+
memoryUsage: parseFloat(memoryUsage.toFixed(2)),
|
|
6647
|
+
improvement
|
|
6648
|
+
};
|
|
6649
|
+
}
|
|
6650
|
+
/**
|
|
6651
|
+
* Enable/disable instanced rendering
|
|
6652
|
+
*/
|
|
6653
|
+
setEnabled(enabled) {
|
|
6654
|
+
this.enabled = enabled;
|
|
6655
|
+
}
|
|
6656
|
+
/**
|
|
6657
|
+
* Clear all batches
|
|
6658
|
+
*/
|
|
6659
|
+
clear() {
|
|
6660
|
+
for (const batch of this.batches.values()) {
|
|
6661
|
+
this.scene.remove(batch.instancedMesh);
|
|
6662
|
+
batch.instancedMesh.geometry.dispose();
|
|
6663
|
+
batch.instancedMesh.material.dispose();
|
|
6664
|
+
}
|
|
6665
|
+
this.batches.clear();
|
|
6666
|
+
this.objects.clear();
|
|
6667
|
+
}
|
|
6668
|
+
/**
|
|
6669
|
+
* Get batch for inspection
|
|
6670
|
+
*/
|
|
6671
|
+
getBatch(key) {
|
|
6672
|
+
return this.batches.get(key);
|
|
6673
|
+
}
|
|
6674
|
+
/**
|
|
6675
|
+
* Get all batch keys
|
|
6676
|
+
*/
|
|
6677
|
+
getBatchKeys() {
|
|
6678
|
+
return Array.from(this.batches.keys());
|
|
6679
|
+
}
|
|
6680
|
+
};
|
|
6681
|
+
|
|
6682
|
+
// src/runtime/AssetStreamer.ts
|
|
6683
|
+
var StreamPriority = /* @__PURE__ */ ((StreamPriority2) => {
|
|
6684
|
+
StreamPriority2[StreamPriority2["IMMEDIATE"] = 0] = "IMMEDIATE";
|
|
6685
|
+
StreamPriority2[StreamPriority2["PREDICTIVE_HIGH"] = 1] = "PREDICTIVE_HIGH";
|
|
6686
|
+
StreamPriority2[StreamPriority2["PREDICTIVE_LOW"] = 2] = "PREDICTIVE_LOW";
|
|
6687
|
+
StreamPriority2[StreamPriority2["BACKGROUND"] = 3] = "BACKGROUND";
|
|
6688
|
+
return StreamPriority2;
|
|
6689
|
+
})(StreamPriority || {});
|
|
6690
|
+
var assetStreamerRegistry = /* @__PURE__ */ new Map();
|
|
6691
|
+
function registerAssetStreamer(name, streamer) {
|
|
6692
|
+
assetStreamerRegistry.set(name, streamer);
|
|
6693
|
+
}
|
|
6694
|
+
function getAssetStreamer(name) {
|
|
6695
|
+
return assetStreamerRegistry.get(name);
|
|
6696
|
+
}
|
|
3693
6697
|
export {
|
|
3694
6698
|
MATERIAL_PRESETS as ADVANCED_PBR_PRESETS,
|
|
3695
6699
|
AdvancedLightingManager,
|
|
@@ -3716,8 +6720,11 @@ export {
|
|
|
3716
6720
|
FogSystem,
|
|
3717
6721
|
GIProbeGrid,
|
|
3718
6722
|
gameplay_exports as Gameplay,
|
|
6723
|
+
GaussianSplatExtractor,
|
|
6724
|
+
HoloScriptPlusRuntimeImpl,
|
|
3719
6725
|
hologram_exports as Hologram,
|
|
3720
6726
|
input_exports as Input,
|
|
6727
|
+
InstancedRenderer,
|
|
3721
6728
|
lod_exports as LOD,
|
|
3722
6729
|
LODBridge,
|
|
3723
6730
|
LODGenerator,
|
|
@@ -3757,8 +6764,11 @@ export {
|
|
|
3757
6764
|
STANDARD_VERTEX_SHADER,
|
|
3758
6765
|
scene_exports as Scene,
|
|
3759
6766
|
ShaderGraph,
|
|
6767
|
+
simulation_exports as Simulation,
|
|
3760
6768
|
SpatialEngine,
|
|
6769
|
+
StreamPriority,
|
|
3761
6770
|
terrain_exports as Terrain,
|
|
6771
|
+
TetGenWasmMesher,
|
|
3762
6772
|
tilemap_exports as Tilemap,
|
|
3763
6773
|
TransitionScheduler,
|
|
3764
6774
|
UNLIT_FRAGMENT_SHADER,
|
|
@@ -3779,6 +6789,7 @@ export {
|
|
|
3779
6789
|
applyFilmGrain,
|
|
3780
6790
|
applyMotionBlur,
|
|
3781
6791
|
applyVignette,
|
|
6792
|
+
assetStreamerRegistry,
|
|
3782
6793
|
blendDetailNormal,
|
|
3783
6794
|
blendTAA,
|
|
3784
6795
|
buildCircleCookie,
|
|
@@ -3812,6 +6823,7 @@ export {
|
|
|
3812
6823
|
createMeshletGenerator,
|
|
3813
6824
|
createMobileLODManager,
|
|
3814
6825
|
createMobileMemoryPool,
|
|
6826
|
+
createRuntime,
|
|
3815
6827
|
createSH9,
|
|
3816
6828
|
createSolidTexture,
|
|
3817
6829
|
createStandardLODConfig,
|
|
@@ -3832,6 +6844,7 @@ export {
|
|
|
3832
6844
|
generateLODs,
|
|
3833
6845
|
generateMeshlets,
|
|
3834
6846
|
geometrySmith,
|
|
6847
|
+
getAssetStreamer,
|
|
3835
6848
|
getAtlasEfficiency,
|
|
3836
6849
|
getRecommendedLODCount,
|
|
3837
6850
|
getRectUV,
|
|
@@ -3848,6 +6861,7 @@ export {
|
|
|
3848
6861
|
pathTrace,
|
|
3849
6862
|
prerenderHTML,
|
|
3850
6863
|
rectSolidAngle,
|
|
6864
|
+
registerAssetStreamer,
|
|
3851
6865
|
renderPDF,
|
|
3852
6866
|
renderScreenshot,
|
|
3853
6867
|
rgbaToHex,
|