@holoscript/core 1.0.0-alpha.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/HoloScript2DParser.js +227 -0
- package/src/HoloScript2DParser.ts +5 -0
- package/src/HoloScriptCodeParser.js +1102 -0
- package/src/HoloScriptCodeParser.ts +145 -20
- package/src/HoloScriptDebugger.js +458 -0
- package/src/HoloScriptParser.js +338 -0
- package/src/HoloScriptPlusParser.js +371 -0
- package/src/HoloScriptPlusParser.ts +543 -0
- package/src/HoloScriptRuntime.js +1399 -0
- package/src/HoloScriptRuntime.test.js +351 -0
- package/src/HoloScriptRuntime.ts +17 -3
- package/src/HoloScriptTypeChecker.js +356 -0
- package/src/__tests__/GraphicsServices.test.js +357 -0
- package/src/__tests__/GraphicsServices.test.ts +427 -0
- package/src/__tests__/HoloScriptPlusParser.test.js +317 -0
- package/src/__tests__/HoloScriptPlusParser.test.ts +392 -0
- package/src/__tests__/integration.test.js +336 -0
- package/src/__tests__/performance.bench.js +218 -0
- package/src/__tests__/type-checker.test.js +60 -0
- package/src/__tests__/type-checker.test.ts +73 -0
- package/src/index.js +217 -0
- package/src/index.ts +158 -18
- package/src/interop/Interoperability.js +413 -0
- package/src/interop/Interoperability.ts +494 -0
- package/src/logger.js +42 -0
- package/src/parser/EnhancedParser.js +205 -0
- package/src/parser/EnhancedParser.ts +251 -0
- package/src/parser/HoloScriptPlusParser.js +928 -0
- package/src/parser/HoloScriptPlusParser.ts +1089 -0
- package/src/runtime/HoloScriptPlusRuntime.js +674 -0
- package/src/runtime/HoloScriptPlusRuntime.ts +861 -0
- package/src/runtime/PerformanceTelemetry.js +323 -0
- package/src/runtime/PerformanceTelemetry.ts +467 -0
- package/src/runtime/RuntimeOptimization.js +361 -0
- package/src/runtime/RuntimeOptimization.ts +416 -0
- package/src/services/HololandGraphicsPipelineService.js +506 -0
- package/src/services/HololandGraphicsPipelineService.ts +662 -0
- package/src/services/PlatformPerformanceOptimizer.js +356 -0
- package/src/services/PlatformPerformanceOptimizer.ts +503 -0
- package/src/state/ReactiveState.js +427 -0
- package/src/state/ReactiveState.ts +572 -0
- package/src/tools/DeveloperExperience.js +376 -0
- package/src/tools/DeveloperExperience.ts +438 -0
- package/src/traits/AIDriverTrait.js +322 -0
- package/src/traits/AIDriverTrait.test.js +329 -0
- package/src/traits/AIDriverTrait.test.ts +357 -0
- package/src/traits/AIDriverTrait.ts +474 -0
- package/src/traits/LightingTrait.js +313 -0
- package/src/traits/LightingTrait.test.js +410 -0
- package/src/traits/LightingTrait.test.ts +462 -0
- package/src/traits/LightingTrait.ts +505 -0
- package/src/traits/MaterialTrait.js +194 -0
- package/src/traits/MaterialTrait.test.js +286 -0
- package/src/traits/MaterialTrait.test.ts +329 -0
- package/src/traits/MaterialTrait.ts +324 -0
- package/src/traits/RenderingTrait.js +356 -0
- package/src/traits/RenderingTrait.test.js +363 -0
- package/src/traits/RenderingTrait.test.ts +427 -0
- package/src/traits/RenderingTrait.ts +555 -0
- package/src/traits/VRTraitSystem.js +740 -0
- package/src/traits/VRTraitSystem.ts +1040 -0
- package/src/traits/VoiceInputTrait.js +284 -0
- package/src/traits/VoiceInputTrait.test.js +226 -0
- package/src/traits/VoiceInputTrait.test.ts +252 -0
- package/src/traits/VoiceInputTrait.ts +401 -0
- package/src/types/AdvancedTypeSystem.js +226 -0
- package/src/types/AdvancedTypeSystem.ts +494 -0
- package/src/types/HoloScriptPlus.d.ts +853 -0
- package/src/types.js +6 -0
- package/src/types.ts +96 -1
- package/tsconfig.json +1 -1
- package/tsup.config.d.ts +2 -0
- package/tsup.config.js +18 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HoloScript+ Runtime Engine
|
|
3
|
+
*
|
|
4
|
+
* Executes parsed HoloScript+ AST with:
|
|
5
|
+
* - Control flow (@for, @if) evaluation
|
|
6
|
+
* - Lifecycle hook management
|
|
7
|
+
* - VR trait integration
|
|
8
|
+
* - Reactive state binding
|
|
9
|
+
* - TypeScript companion integration
|
|
10
|
+
*
|
|
11
|
+
* @version 1.0.0
|
|
12
|
+
*/
|
|
13
|
+
import { createState, ExpressionEvaluator } from '../state/ReactiveState';
|
|
14
|
+
import { vrTraitRegistry } from '../traits/VRTraitSystem';
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// BUILT-IN FUNCTIONS
|
|
17
|
+
// =============================================================================
|
|
18
|
+
function createBuiltins(runtime) {
|
|
19
|
+
return {
|
|
20
|
+
Math,
|
|
21
|
+
range: (start, end, step = 1) => {
|
|
22
|
+
const result = [];
|
|
23
|
+
if (step > 0) {
|
|
24
|
+
for (let i = start; i < end; i += step) {
|
|
25
|
+
result.push(i);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (step < 0) {
|
|
29
|
+
for (let i = start; i > end; i += step) {
|
|
30
|
+
result.push(i);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
},
|
|
35
|
+
interpolate_color: (t, from, to) => {
|
|
36
|
+
// Parse hex colors
|
|
37
|
+
const parseHex = (hex) => {
|
|
38
|
+
const clean = hex.replace('#', '');
|
|
39
|
+
return [
|
|
40
|
+
parseInt(clean.substring(0, 2), 16),
|
|
41
|
+
parseInt(clean.substring(2, 4), 16),
|
|
42
|
+
parseInt(clean.substring(4, 6), 16),
|
|
43
|
+
];
|
|
44
|
+
};
|
|
45
|
+
const toHex = (r, g, b) => {
|
|
46
|
+
const clamp = (v) => Math.max(0, Math.min(255, Math.round(v)));
|
|
47
|
+
return `#${clamp(r).toString(16).padStart(2, '0')}${clamp(g).toString(16).padStart(2, '0')}${clamp(b).toString(16).padStart(2, '0')}`;
|
|
48
|
+
};
|
|
49
|
+
const [r1, g1, b1] = parseHex(from);
|
|
50
|
+
const [r2, g2, b2] = parseHex(to);
|
|
51
|
+
return toHex(r1 + (r2 - r1) * t, g1 + (g2 - g1) * t, b1 + (b2 - b1) * t);
|
|
52
|
+
},
|
|
53
|
+
distance_to: (point) => {
|
|
54
|
+
const viewer = runtime.vrContext.headset.position;
|
|
55
|
+
return Math.sqrt(Math.pow(point[0] - viewer[0], 2) +
|
|
56
|
+
Math.pow(point[1] - viewer[1], 2) +
|
|
57
|
+
Math.pow(point[2] - viewer[2], 2));
|
|
58
|
+
},
|
|
59
|
+
distance_to_viewer: () => {
|
|
60
|
+
return 0; // Override in node context
|
|
61
|
+
},
|
|
62
|
+
hand_position: (handId) => {
|
|
63
|
+
const hand = handId === 'left' ? runtime.vrContext.hands.left : runtime.vrContext.hands.right;
|
|
64
|
+
return hand?.position || [0, 0, 0];
|
|
65
|
+
},
|
|
66
|
+
hand_velocity: (handId) => {
|
|
67
|
+
const hand = handId === 'left' ? runtime.vrContext.hands.left : runtime.vrContext.hands.right;
|
|
68
|
+
return hand?.velocity || [0, 0, 0];
|
|
69
|
+
},
|
|
70
|
+
dominant_hand: () => {
|
|
71
|
+
// Default to right hand
|
|
72
|
+
return runtime.vrContext.hands.right || runtime.vrContext.hands.left || {
|
|
73
|
+
id: 'right',
|
|
74
|
+
position: [0, 0, 0],
|
|
75
|
+
rotation: [0, 0, 0],
|
|
76
|
+
velocity: [0, 0, 0],
|
|
77
|
+
grip: 0,
|
|
78
|
+
trigger: 0,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
play_sound: (source, options) => {
|
|
82
|
+
runtime.emit('play_sound', { source, ...options });
|
|
83
|
+
},
|
|
84
|
+
haptic_feedback: (hand, intensity) => {
|
|
85
|
+
const handId = typeof hand === 'string' ? hand : hand.id;
|
|
86
|
+
runtime.emit('haptic', { hand: handId, intensity });
|
|
87
|
+
},
|
|
88
|
+
haptic_pulse: (intensity) => {
|
|
89
|
+
runtime.emit('haptic', { hand: 'both', intensity });
|
|
90
|
+
},
|
|
91
|
+
apply_velocity: (node, velocity) => {
|
|
92
|
+
runtime.emit('apply_velocity', { node, velocity });
|
|
93
|
+
},
|
|
94
|
+
spawn: (template, position) => {
|
|
95
|
+
return runtime.spawnTemplate(template, position);
|
|
96
|
+
},
|
|
97
|
+
destroy: (node) => {
|
|
98
|
+
runtime.destroyNode(node);
|
|
99
|
+
},
|
|
100
|
+
api_call: async (url, method, body) => {
|
|
101
|
+
const response = await fetch(url, {
|
|
102
|
+
method,
|
|
103
|
+
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
104
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
105
|
+
});
|
|
106
|
+
return response.json();
|
|
107
|
+
},
|
|
108
|
+
open_modal: (modalId) => {
|
|
109
|
+
runtime.emit('open_modal', { id: modalId });
|
|
110
|
+
},
|
|
111
|
+
close_modal: (modalId) => {
|
|
112
|
+
runtime.emit('close_modal', { id: modalId });
|
|
113
|
+
},
|
|
114
|
+
setTimeout: (callback, delay) => {
|
|
115
|
+
return window.setTimeout(callback, delay);
|
|
116
|
+
},
|
|
117
|
+
clearTimeout: (id) => {
|
|
118
|
+
window.clearTimeout(id);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// RUNTIME IMPLEMENTATION
|
|
124
|
+
// =============================================================================
|
|
125
|
+
class HoloScriptPlusRuntimeImpl {
|
|
126
|
+
constructor(ast, options = {}) {
|
|
127
|
+
this.rootInstance = null;
|
|
128
|
+
this.eventHandlers = new Map();
|
|
129
|
+
this.templates = new Map();
|
|
130
|
+
this.updateLoopId = null;
|
|
131
|
+
this.lastUpdateTime = 0;
|
|
132
|
+
this.mounted = false;
|
|
133
|
+
// VR context
|
|
134
|
+
this.vrContext = {
|
|
135
|
+
hands: {
|
|
136
|
+
left: null,
|
|
137
|
+
right: null,
|
|
138
|
+
},
|
|
139
|
+
headset: {
|
|
140
|
+
position: [0, 1.6, 0],
|
|
141
|
+
rotation: [0, 0, 0],
|
|
142
|
+
},
|
|
143
|
+
controllers: {
|
|
144
|
+
left: null,
|
|
145
|
+
right: null,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
this.ast = ast;
|
|
149
|
+
this.options = options;
|
|
150
|
+
this.state = createState({});
|
|
151
|
+
this.traitRegistry = vrTraitRegistry;
|
|
152
|
+
this.companions = options.companions || {};
|
|
153
|
+
this.builtins = createBuiltins(this);
|
|
154
|
+
// Create expression evaluator with context
|
|
155
|
+
this.evaluator = new ExpressionEvaluator(this.state.getSnapshot(), this.builtins);
|
|
156
|
+
// Initialize state from AST
|
|
157
|
+
this.initializeState();
|
|
158
|
+
// Load imports
|
|
159
|
+
this.loadImports();
|
|
160
|
+
}
|
|
161
|
+
// ==========================================================================
|
|
162
|
+
// INITIALIZATION
|
|
163
|
+
// ==========================================================================
|
|
164
|
+
initializeState() {
|
|
165
|
+
const stateDirective = this.ast.root.directives.find((d) => d.type === 'state');
|
|
166
|
+
if (stateDirective && stateDirective.type === 'state') {
|
|
167
|
+
this.state.update(stateDirective.body);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
loadImports() {
|
|
171
|
+
for (const imp of this.ast.imports) {
|
|
172
|
+
// Companions should be provided via options
|
|
173
|
+
if (this.companions[imp.alias]) {
|
|
174
|
+
// Already loaded
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
console.warn(`Import ${imp.path} not found. Provide via companions option.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ==========================================================================
|
|
181
|
+
// MOUNTING
|
|
182
|
+
// ==========================================================================
|
|
183
|
+
mount(container) {
|
|
184
|
+
if (this.mounted) {
|
|
185
|
+
console.warn('Runtime already mounted');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.mounted = true;
|
|
189
|
+
// Build node tree
|
|
190
|
+
this.rootInstance = this.instantiateNode(this.ast.root, null);
|
|
191
|
+
// Mount to container
|
|
192
|
+
if (this.options.renderer && this.rootInstance) {
|
|
193
|
+
this.options.renderer.appendChild(container, this.rootInstance.renderedNode);
|
|
194
|
+
}
|
|
195
|
+
// Call mount lifecycle
|
|
196
|
+
this.callLifecycle(this.rootInstance, 'on_mount');
|
|
197
|
+
// Start update loop
|
|
198
|
+
this.startUpdateLoop();
|
|
199
|
+
}
|
|
200
|
+
unmount() {
|
|
201
|
+
if (!this.mounted)
|
|
202
|
+
return;
|
|
203
|
+
// Stop update loop
|
|
204
|
+
this.stopUpdateLoop();
|
|
205
|
+
// Call unmount lifecycle
|
|
206
|
+
if (this.rootInstance) {
|
|
207
|
+
this.callLifecycle(this.rootInstance, 'on_unmount');
|
|
208
|
+
this.destroyInstance(this.rootInstance);
|
|
209
|
+
}
|
|
210
|
+
this.rootInstance = null;
|
|
211
|
+
this.mounted = false;
|
|
212
|
+
}
|
|
213
|
+
// ==========================================================================
|
|
214
|
+
// NODE INSTANTIATION
|
|
215
|
+
// ==========================================================================
|
|
216
|
+
instantiateNode(node, parent) {
|
|
217
|
+
const instance = {
|
|
218
|
+
node,
|
|
219
|
+
renderedNode: null,
|
|
220
|
+
lifecycleHandlers: new Map(),
|
|
221
|
+
children: [],
|
|
222
|
+
parent,
|
|
223
|
+
destroyed: false,
|
|
224
|
+
};
|
|
225
|
+
// Process directives
|
|
226
|
+
this.processDirectives(instance);
|
|
227
|
+
// Create rendered element
|
|
228
|
+
if (this.options.renderer) {
|
|
229
|
+
const properties = this.evaluateProperties(node.properties);
|
|
230
|
+
instance.renderedNode = this.options.renderer.createElement(node.type, properties);
|
|
231
|
+
}
|
|
232
|
+
// Attach VR traits
|
|
233
|
+
for (const [traitName, config] of node.traits) {
|
|
234
|
+
this.traitRegistry.attachTrait(node, traitName, config, this.createTraitContext(instance));
|
|
235
|
+
}
|
|
236
|
+
// Process children with control flow
|
|
237
|
+
const children = this.processControlFlow(node.children, node.directives);
|
|
238
|
+
for (const childNode of children) {
|
|
239
|
+
const childInstance = this.instantiateNode(childNode, instance);
|
|
240
|
+
instance.children.push(childInstance);
|
|
241
|
+
if (this.options.renderer && instance.renderedNode) {
|
|
242
|
+
this.options.renderer.appendChild(instance.renderedNode, childInstance.renderedNode);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return instance;
|
|
246
|
+
}
|
|
247
|
+
processDirectives(instance) {
|
|
248
|
+
for (const directive of instance.node.directives) {
|
|
249
|
+
if (directive.type === 'lifecycle') {
|
|
250
|
+
this.registerLifecycleHandler(instance, directive);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
registerLifecycleHandler(instance, directive) {
|
|
255
|
+
const { hook, params, body } = directive;
|
|
256
|
+
// Create handler function
|
|
257
|
+
const handler = (...args) => {
|
|
258
|
+
// Build parameter context
|
|
259
|
+
const paramContext = {};
|
|
260
|
+
if (params) {
|
|
261
|
+
params.forEach((param, i) => {
|
|
262
|
+
paramContext[param] = args[i];
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
// Evaluate body
|
|
266
|
+
this.evaluator.updateContext({
|
|
267
|
+
...this.state.getSnapshot(),
|
|
268
|
+
...paramContext,
|
|
269
|
+
node: instance.node,
|
|
270
|
+
self: instance.node,
|
|
271
|
+
});
|
|
272
|
+
try {
|
|
273
|
+
// Check if body looks like code or expression
|
|
274
|
+
if (body.includes(';') || body.includes('{')) {
|
|
275
|
+
// Execute as code block
|
|
276
|
+
new Function(...Object.keys(this.builtins), ...Object.keys(paramContext), 'state', 'node', body)(...Object.values(this.builtins), ...Object.values(paramContext), this.state, instance.node);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Evaluate as expression
|
|
280
|
+
this.evaluator.evaluate(body);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
console.error(`Error in lifecycle handler ${hook}:`, error);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
// Register handler
|
|
288
|
+
if (!instance.lifecycleHandlers.has(hook)) {
|
|
289
|
+
instance.lifecycleHandlers.set(hook, []);
|
|
290
|
+
}
|
|
291
|
+
instance.lifecycleHandlers.get(hook).push(handler);
|
|
292
|
+
}
|
|
293
|
+
// ==========================================================================
|
|
294
|
+
// CONTROL FLOW
|
|
295
|
+
// ==========================================================================
|
|
296
|
+
processControlFlow(children, directives) {
|
|
297
|
+
const result = [];
|
|
298
|
+
// Process @for directives
|
|
299
|
+
for (const directive of directives) {
|
|
300
|
+
if (directive.type === 'for') {
|
|
301
|
+
const items = this.evaluateExpression(directive.iterable);
|
|
302
|
+
if (Array.isArray(items)) {
|
|
303
|
+
items.forEach((item, index) => {
|
|
304
|
+
// Create context for each iteration
|
|
305
|
+
const iterContext = {
|
|
306
|
+
[directive.variable]: item,
|
|
307
|
+
index,
|
|
308
|
+
first: index === 0,
|
|
309
|
+
last: index === items.length - 1,
|
|
310
|
+
even: index % 2 === 0,
|
|
311
|
+
odd: index % 2 !== 0,
|
|
312
|
+
};
|
|
313
|
+
// Clone and process body nodes
|
|
314
|
+
for (const bodyNode of directive.body) {
|
|
315
|
+
const cloned = this.cloneNodeWithContext(bodyNode, iterContext);
|
|
316
|
+
result.push(cloned);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else if (directive.type === 'if') {
|
|
322
|
+
const condition = this.evaluateExpression(directive.condition);
|
|
323
|
+
if (condition) {
|
|
324
|
+
result.push(...directive.body);
|
|
325
|
+
}
|
|
326
|
+
else if (directive.else) {
|
|
327
|
+
result.push(...directive.else);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// Add regular children
|
|
332
|
+
result.push(...children);
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
cloneNodeWithContext(node, context) {
|
|
336
|
+
// Deep clone the node
|
|
337
|
+
const cloned = {
|
|
338
|
+
type: node.type,
|
|
339
|
+
id: node.id ? this.interpolateString(node.id, context) : undefined,
|
|
340
|
+
properties: this.interpolateProperties(node.properties, context),
|
|
341
|
+
directives: [...node.directives],
|
|
342
|
+
children: node.children.map((child) => this.cloneNodeWithContext(child, context)),
|
|
343
|
+
traits: new Map(node.traits),
|
|
344
|
+
loc: node.loc,
|
|
345
|
+
};
|
|
346
|
+
return cloned;
|
|
347
|
+
}
|
|
348
|
+
interpolateString(str, context) {
|
|
349
|
+
return str.replace(/\$\{([^}]+)\}/g, (_match, expr) => {
|
|
350
|
+
this.evaluator.updateContext(context);
|
|
351
|
+
const value = this.evaluator.evaluate(expr);
|
|
352
|
+
return String(value ?? '');
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
interpolateProperties(properties, context) {
|
|
356
|
+
const result = {};
|
|
357
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
358
|
+
if (typeof value === 'string') {
|
|
359
|
+
result[key] = this.interpolateString(value, context);
|
|
360
|
+
}
|
|
361
|
+
else if (value && typeof value === 'object' && '__expr' in value) {
|
|
362
|
+
this.evaluator.updateContext(context);
|
|
363
|
+
result[key] = this.evaluator.evaluate(value.__raw);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
result[key] = value;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
// ==========================================================================
|
|
372
|
+
// EXPRESSION EVALUATION
|
|
373
|
+
// ==========================================================================
|
|
374
|
+
evaluateExpression(expr) {
|
|
375
|
+
this.evaluator.updateContext(this.state.getSnapshot());
|
|
376
|
+
return this.evaluator.evaluate(expr);
|
|
377
|
+
}
|
|
378
|
+
evaluateProperties(properties) {
|
|
379
|
+
const result = {};
|
|
380
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
381
|
+
if (value && typeof value === 'object' && '__expr' in value) {
|
|
382
|
+
result[key] = this.evaluateExpression(value.__raw);
|
|
383
|
+
}
|
|
384
|
+
else if (value && typeof value === 'object' && '__ref' in value) {
|
|
385
|
+
// Reference to state or companion
|
|
386
|
+
const ref = value.__ref;
|
|
387
|
+
result[key] = this.state.get(ref) ?? ref;
|
|
388
|
+
}
|
|
389
|
+
else if (typeof value === 'string' && value.includes('${')) {
|
|
390
|
+
// String interpolation
|
|
391
|
+
result[key] = this.interpolateString(value, this.state.getSnapshot());
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
result[key] = value;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
399
|
+
// ==========================================================================
|
|
400
|
+
// LIFECYCLE
|
|
401
|
+
// ==========================================================================
|
|
402
|
+
callLifecycle(instance, hook, ...args) {
|
|
403
|
+
if (!instance || instance.destroyed)
|
|
404
|
+
return;
|
|
405
|
+
const handlers = instance.lifecycleHandlers.get(hook);
|
|
406
|
+
if (handlers) {
|
|
407
|
+
handlers.forEach((handler) => {
|
|
408
|
+
try {
|
|
409
|
+
handler(...args);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
console.error(`Error in lifecycle ${hook}:`, error);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
// Recurse to children
|
|
417
|
+
for (const child of instance.children) {
|
|
418
|
+
this.callLifecycle(child, hook, ...args);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ==========================================================================
|
|
422
|
+
// UPDATE LOOP
|
|
423
|
+
// ==========================================================================
|
|
424
|
+
startUpdateLoop() {
|
|
425
|
+
this.lastUpdateTime = performance.now();
|
|
426
|
+
const update = () => {
|
|
427
|
+
const now = performance.now();
|
|
428
|
+
const delta = (now - this.lastUpdateTime) / 1000; // Convert to seconds
|
|
429
|
+
this.lastUpdateTime = now;
|
|
430
|
+
this.update(delta);
|
|
431
|
+
this.updateLoopId = requestAnimationFrame(update);
|
|
432
|
+
};
|
|
433
|
+
this.updateLoopId = requestAnimationFrame(update);
|
|
434
|
+
}
|
|
435
|
+
stopUpdateLoop() {
|
|
436
|
+
if (this.updateLoopId !== null) {
|
|
437
|
+
cancelAnimationFrame(this.updateLoopId);
|
|
438
|
+
this.updateLoopId = null;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
update(delta) {
|
|
442
|
+
if (!this.rootInstance)
|
|
443
|
+
return;
|
|
444
|
+
// Update all instances
|
|
445
|
+
this.updateInstance(this.rootInstance, delta);
|
|
446
|
+
// Call update lifecycle
|
|
447
|
+
this.callLifecycle(this.rootInstance, 'on_update', delta);
|
|
448
|
+
}
|
|
449
|
+
updateInstance(instance, delta) {
|
|
450
|
+
if (instance.destroyed)
|
|
451
|
+
return;
|
|
452
|
+
// Update VR traits
|
|
453
|
+
const traitContext = this.createTraitContext(instance);
|
|
454
|
+
this.traitRegistry.updateAllTraits(instance.node, traitContext, delta);
|
|
455
|
+
// Update rendered element if properties changed
|
|
456
|
+
if (this.options.renderer && instance.renderedNode) {
|
|
457
|
+
const properties = this.evaluateProperties(instance.node.properties);
|
|
458
|
+
this.options.renderer.updateElement(instance.renderedNode, properties);
|
|
459
|
+
}
|
|
460
|
+
// Update children
|
|
461
|
+
for (const child of instance.children) {
|
|
462
|
+
this.updateInstance(child, delta);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// ==========================================================================
|
|
466
|
+
// TRAIT CONTEXT
|
|
467
|
+
// ==========================================================================
|
|
468
|
+
createTraitContext(_instance) {
|
|
469
|
+
return {
|
|
470
|
+
vr: {
|
|
471
|
+
hands: this.vrContext.hands,
|
|
472
|
+
headset: this.vrContext.headset,
|
|
473
|
+
getPointerRay: (hand) => {
|
|
474
|
+
const vrHand = hand === 'left' ? this.vrContext.hands.left : this.vrContext.hands.right;
|
|
475
|
+
if (!vrHand)
|
|
476
|
+
return null;
|
|
477
|
+
return {
|
|
478
|
+
origin: vrHand.position,
|
|
479
|
+
direction: [0, 0, -1], // Forward direction - should be calculated from rotation
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
getDominantHand: () => this.vrContext.hands.right || this.vrContext.hands.left,
|
|
483
|
+
},
|
|
484
|
+
physics: {
|
|
485
|
+
applyVelocity: (node, velocity) => {
|
|
486
|
+
this.emit('apply_velocity', { node, velocity });
|
|
487
|
+
},
|
|
488
|
+
applyAngularVelocity: (node, angularVelocity) => {
|
|
489
|
+
this.emit('apply_angular_velocity', { node, angularVelocity });
|
|
490
|
+
},
|
|
491
|
+
setKinematic: (node, kinematic) => {
|
|
492
|
+
this.emit('set_kinematic', { node, kinematic });
|
|
493
|
+
},
|
|
494
|
+
raycast: (_origin, _direction, _maxDistance) => {
|
|
495
|
+
// Would need physics engine integration
|
|
496
|
+
return null;
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
audio: {
|
|
500
|
+
playSound: (source, options) => {
|
|
501
|
+
this.emit('play_sound', { source, ...options });
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
haptics: {
|
|
505
|
+
pulse: (hand, intensity, duration) => {
|
|
506
|
+
this.emit('haptic', { hand, intensity, duration, type: 'pulse' });
|
|
507
|
+
},
|
|
508
|
+
rumble: (hand, intensity) => {
|
|
509
|
+
this.emit('haptic', { hand, intensity, type: 'rumble' });
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
emit: this.emit.bind(this),
|
|
513
|
+
getState: () => this.state.getSnapshot(),
|
|
514
|
+
setState: (updates) => this.state.update(updates),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
// ==========================================================================
|
|
518
|
+
// NODE DESTRUCTION
|
|
519
|
+
// ==========================================================================
|
|
520
|
+
destroyInstance(instance) {
|
|
521
|
+
if (instance.destroyed)
|
|
522
|
+
return;
|
|
523
|
+
instance.destroyed = true;
|
|
524
|
+
// Destroy children first
|
|
525
|
+
for (const child of instance.children) {
|
|
526
|
+
this.destroyInstance(child);
|
|
527
|
+
}
|
|
528
|
+
// Detach traits
|
|
529
|
+
const traitContext = this.createTraitContext(instance);
|
|
530
|
+
for (const traitName of instance.node.traits.keys()) {
|
|
531
|
+
this.traitRegistry.detachTrait(instance.node, traitName, traitContext);
|
|
532
|
+
}
|
|
533
|
+
// Destroy rendered element
|
|
534
|
+
if (this.options.renderer && instance.renderedNode) {
|
|
535
|
+
this.options.renderer.destroy(instance.renderedNode);
|
|
536
|
+
}
|
|
537
|
+
// Clear handlers
|
|
538
|
+
instance.lifecycleHandlers.clear();
|
|
539
|
+
instance.children = [];
|
|
540
|
+
}
|
|
541
|
+
// ==========================================================================
|
|
542
|
+
// PUBLIC API
|
|
543
|
+
// ==========================================================================
|
|
544
|
+
updateData(data) {
|
|
545
|
+
this.state.set('data', data);
|
|
546
|
+
this.callLifecycle(this.rootInstance, 'on_data_update', data);
|
|
547
|
+
}
|
|
548
|
+
getState() {
|
|
549
|
+
return this.state.getSnapshot();
|
|
550
|
+
}
|
|
551
|
+
setState(updates) {
|
|
552
|
+
this.state.update(updates);
|
|
553
|
+
}
|
|
554
|
+
emit(event, payload) {
|
|
555
|
+
const handlers = this.eventHandlers.get(event);
|
|
556
|
+
if (handlers) {
|
|
557
|
+
handlers.forEach((handler) => {
|
|
558
|
+
try {
|
|
559
|
+
handler(payload);
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
console.error(`Error in event handler for ${event}:`, error);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
on(event, handler) {
|
|
568
|
+
if (!this.eventHandlers.has(event)) {
|
|
569
|
+
this.eventHandlers.set(event, new Set());
|
|
570
|
+
}
|
|
571
|
+
this.eventHandlers.get(event).add(handler);
|
|
572
|
+
return () => {
|
|
573
|
+
this.eventHandlers.get(event)?.delete(handler);
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
// ==========================================================================
|
|
577
|
+
// VR INTEGRATION
|
|
578
|
+
// ==========================================================================
|
|
579
|
+
updateVRContext(context) {
|
|
580
|
+
this.vrContext = context;
|
|
581
|
+
}
|
|
582
|
+
handleVREvent(event, node) {
|
|
583
|
+
// Find instance for node
|
|
584
|
+
const instance = this.findInstance(node);
|
|
585
|
+
if (!instance)
|
|
586
|
+
return;
|
|
587
|
+
// Dispatch to traits
|
|
588
|
+
const traitContext = this.createTraitContext(instance);
|
|
589
|
+
this.traitRegistry.handleEventForAllTraits(node, traitContext, event);
|
|
590
|
+
// Call lifecycle hooks based on event type
|
|
591
|
+
const hookMapping = {
|
|
592
|
+
grab_start: 'on_grab',
|
|
593
|
+
grab_end: 'on_release',
|
|
594
|
+
hover_enter: 'on_hover_enter',
|
|
595
|
+
hover_exit: 'on_hover_exit',
|
|
596
|
+
point_enter: 'on_point_enter',
|
|
597
|
+
point_exit: 'on_point_exit',
|
|
598
|
+
collision: 'on_collision',
|
|
599
|
+
trigger_enter: 'on_trigger_enter',
|
|
600
|
+
trigger_exit: 'on_trigger_exit',
|
|
601
|
+
click: 'on_click',
|
|
602
|
+
};
|
|
603
|
+
const hook = hookMapping[event.type];
|
|
604
|
+
if (hook) {
|
|
605
|
+
this.callLifecycle(instance, hook, event);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
findInstance(node, root = this.rootInstance) {
|
|
609
|
+
if (!root)
|
|
610
|
+
return null;
|
|
611
|
+
if (root.node === node)
|
|
612
|
+
return root;
|
|
613
|
+
for (const child of root.children) {
|
|
614
|
+
const found = this.findInstance(node, child);
|
|
615
|
+
if (found)
|
|
616
|
+
return found;
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
// ==========================================================================
|
|
621
|
+
// TEMPLATES & SPAWNING
|
|
622
|
+
// ==========================================================================
|
|
623
|
+
registerTemplate(name, node) {
|
|
624
|
+
this.templates.set(name, node);
|
|
625
|
+
}
|
|
626
|
+
spawnTemplate(name, position) {
|
|
627
|
+
const template = this.templates.get(name);
|
|
628
|
+
if (!template) {
|
|
629
|
+
throw new Error(`Template "${name}" not found`);
|
|
630
|
+
}
|
|
631
|
+
// Clone template
|
|
632
|
+
const cloned = this.cloneNodeWithContext(template, { position });
|
|
633
|
+
cloned.properties.position = position;
|
|
634
|
+
// Instantiate
|
|
635
|
+
if (this.rootInstance) {
|
|
636
|
+
const instance = this.instantiateNode(cloned, this.rootInstance);
|
|
637
|
+
this.rootInstance.children.push(instance);
|
|
638
|
+
if (this.options.renderer && this.rootInstance.renderedNode) {
|
|
639
|
+
this.options.renderer.appendChild(this.rootInstance.renderedNode, instance.renderedNode);
|
|
640
|
+
}
|
|
641
|
+
this.callLifecycle(instance, 'on_mount');
|
|
642
|
+
}
|
|
643
|
+
return cloned;
|
|
644
|
+
}
|
|
645
|
+
destroyNode(node) {
|
|
646
|
+
const instance = this.findInstance(node);
|
|
647
|
+
if (!instance)
|
|
648
|
+
return;
|
|
649
|
+
// Call unmount
|
|
650
|
+
this.callLifecycle(instance, 'on_unmount');
|
|
651
|
+
// Remove from parent
|
|
652
|
+
if (instance.parent) {
|
|
653
|
+
const index = instance.parent.children.indexOf(instance);
|
|
654
|
+
if (index > -1) {
|
|
655
|
+
instance.parent.children.splice(index, 1);
|
|
656
|
+
}
|
|
657
|
+
if (this.options.renderer && instance.parent.renderedNode && instance.renderedNode) {
|
|
658
|
+
this.options.renderer.removeChild(instance.parent.renderedNode, instance.renderedNode);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Destroy
|
|
662
|
+
this.destroyInstance(instance);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// =============================================================================
|
|
666
|
+
// FACTORY FUNCTION
|
|
667
|
+
// =============================================================================
|
|
668
|
+
export function createRuntime(ast, options) {
|
|
669
|
+
return new HoloScriptPlusRuntimeImpl(ast, options);
|
|
670
|
+
}
|
|
671
|
+
// =============================================================================
|
|
672
|
+
// EXPORTS
|
|
673
|
+
// =============================================================================
|
|
674
|
+
export { HoloScriptPlusRuntimeImpl };
|