@holoscript/core 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-3N67RLQP.cjs +1298 -0
  3. package/dist/chunk-3N67RLQP.cjs.map +1 -0
  4. package/dist/chunk-3X2EGU7Z.cjs +52 -0
  5. package/dist/chunk-3X2EGU7Z.cjs.map +1 -0
  6. package/dist/chunk-4CV4JOE5.js +24 -0
  7. package/dist/chunk-4CV4JOE5.js.map +1 -0
  8. package/dist/chunk-4OHVW4XR.cjs +1027 -0
  9. package/dist/chunk-4OHVW4XR.cjs.map +1 -0
  10. package/dist/chunk-CZLDE2OZ.cjs +28 -0
  11. package/dist/chunk-CZLDE2OZ.cjs.map +1 -0
  12. package/{src/HoloScriptRuntime.ts → dist/chunk-EU6CZMGJ.js} +437 -794
  13. package/dist/chunk-EU6CZMGJ.js.map +1 -0
  14. package/dist/chunk-KWYIVRIH.js +344 -0
  15. package/dist/chunk-KWYIVRIH.js.map +1 -0
  16. package/dist/chunk-MCP6D4LT.js +1025 -0
  17. package/dist/chunk-MCP6D4LT.js.map +1 -0
  18. package/dist/chunk-SATNCODL.js +45 -0
  19. package/dist/chunk-SATNCODL.js.map +1 -0
  20. package/dist/chunk-VMZN4EVR.cjs +347 -0
  21. package/dist/chunk-VMZN4EVR.cjs.map +1 -0
  22. package/{src/HoloScriptDebugger.ts → dist/chunk-VYIDLUCV.js} +118 -257
  23. package/dist/chunk-VYIDLUCV.js.map +1 -0
  24. package/dist/chunk-WFI4T3XB.cjs +424 -0
  25. package/dist/chunk-WFI4T3XB.cjs.map +1 -0
  26. package/dist/debugger.cjs +20 -0
  27. package/dist/debugger.cjs.map +1 -0
  28. package/dist/debugger.d.cts +171 -0
  29. package/dist/debugger.d.ts +171 -0
  30. package/dist/debugger.js +7 -0
  31. package/dist/debugger.js.map +1 -0
  32. package/dist/index.cjs +6006 -0
  33. package/dist/index.cjs.map +1 -0
  34. package/dist/index.d.cts +2482 -0
  35. package/dist/index.d.ts +2482 -0
  36. package/dist/index.js +5926 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/parser.cjs +14 -0
  39. package/dist/parser.cjs.map +1 -0
  40. package/dist/parser.d.cts +139 -0
  41. package/dist/parser.d.ts +139 -0
  42. package/dist/parser.js +5 -0
  43. package/dist/parser.js.map +1 -0
  44. package/dist/runtime.cjs +14 -0
  45. package/dist/runtime.cjs.map +1 -0
  46. package/dist/runtime.d.cts +180 -0
  47. package/dist/runtime.d.ts +180 -0
  48. package/dist/runtime.js +5 -0
  49. package/dist/runtime.js.map +1 -0
  50. package/dist/type-checker.cjs +17 -0
  51. package/dist/type-checker.cjs.map +1 -0
  52. package/dist/type-checker.d.cts +105 -0
  53. package/dist/type-checker.d.ts +105 -0
  54. package/dist/type-checker.js +4 -0
  55. package/dist/type-checker.js.map +1 -0
  56. package/dist/types-D6g4ACjP.d.cts +262 -0
  57. package/dist/types-D6g4ACjP.d.ts +262 -0
  58. package/package.json +11 -8
  59. package/src/HoloScript2DParser.js +0 -227
  60. package/src/HoloScript2DParser.ts +0 -261
  61. package/src/HoloScriptCodeParser.js +0 -1102
  62. package/src/HoloScriptCodeParser.ts +0 -1188
  63. package/src/HoloScriptDebugger.js +0 -458
  64. package/src/HoloScriptParser.js +0 -338
  65. package/src/HoloScriptParser.ts +0 -397
  66. package/src/HoloScriptPlusParser.js +0 -371
  67. package/src/HoloScriptPlusParser.ts +0 -543
  68. package/src/HoloScriptRuntime.js +0 -1399
  69. package/src/HoloScriptRuntime.test.js +0 -351
  70. package/src/HoloScriptRuntime.test.ts +0 -436
  71. package/src/HoloScriptTypeChecker.js +0 -356
  72. package/src/HoloScriptTypeChecker.ts +0 -475
  73. package/src/__tests__/GraphicsServices.test.js +0 -357
  74. package/src/__tests__/GraphicsServices.test.ts +0 -427
  75. package/src/__tests__/HoloScriptPlusParser.test.js +0 -317
  76. package/src/__tests__/HoloScriptPlusParser.test.ts +0 -392
  77. package/src/__tests__/integration.test.js +0 -336
  78. package/src/__tests__/integration.test.ts +0 -416
  79. package/src/__tests__/performance.bench.js +0 -218
  80. package/src/__tests__/performance.bench.ts +0 -262
  81. package/src/__tests__/type-checker.test.js +0 -60
  82. package/src/__tests__/type-checker.test.ts +0 -73
  83. package/src/index.js +0 -217
  84. package/src/index.ts +0 -426
  85. package/src/interop/Interoperability.js +0 -413
  86. package/src/interop/Interoperability.ts +0 -494
  87. package/src/logger.js +0 -42
  88. package/src/logger.ts +0 -57
  89. package/src/parser/EnhancedParser.js +0 -205
  90. package/src/parser/EnhancedParser.ts +0 -251
  91. package/src/parser/HoloScriptPlusParser.js +0 -928
  92. package/src/parser/HoloScriptPlusParser.ts +0 -1089
  93. package/src/runtime/HoloScriptPlusRuntime.js +0 -674
  94. package/src/runtime/HoloScriptPlusRuntime.ts +0 -861
  95. package/src/runtime/PerformanceTelemetry.js +0 -323
  96. package/src/runtime/PerformanceTelemetry.ts +0 -467
  97. package/src/runtime/RuntimeOptimization.js +0 -361
  98. package/src/runtime/RuntimeOptimization.ts +0 -416
  99. package/src/services/HololandGraphicsPipelineService.js +0 -506
  100. package/src/services/HololandGraphicsPipelineService.ts +0 -662
  101. package/src/services/PlatformPerformanceOptimizer.js +0 -356
  102. package/src/services/PlatformPerformanceOptimizer.ts +0 -503
  103. package/src/state/ReactiveState.js +0 -427
  104. package/src/state/ReactiveState.ts +0 -572
  105. package/src/tools/DeveloperExperience.js +0 -376
  106. package/src/tools/DeveloperExperience.ts +0 -438
  107. package/src/traits/AIDriverTrait.js +0 -322
  108. package/src/traits/AIDriverTrait.test.js +0 -329
  109. package/src/traits/AIDriverTrait.test.ts +0 -357
  110. package/src/traits/AIDriverTrait.ts +0 -474
  111. package/src/traits/LightingTrait.js +0 -313
  112. package/src/traits/LightingTrait.test.js +0 -410
  113. package/src/traits/LightingTrait.test.ts +0 -462
  114. package/src/traits/LightingTrait.ts +0 -505
  115. package/src/traits/MaterialTrait.js +0 -194
  116. package/src/traits/MaterialTrait.test.js +0 -286
  117. package/src/traits/MaterialTrait.test.ts +0 -329
  118. package/src/traits/MaterialTrait.ts +0 -324
  119. package/src/traits/RenderingTrait.js +0 -356
  120. package/src/traits/RenderingTrait.test.js +0 -363
  121. package/src/traits/RenderingTrait.test.ts +0 -427
  122. package/src/traits/RenderingTrait.ts +0 -555
  123. package/src/traits/VRTraitSystem.js +0 -740
  124. package/src/traits/VRTraitSystem.ts +0 -1040
  125. package/src/traits/VoiceInputTrait.js +0 -284
  126. package/src/traits/VoiceInputTrait.test.js +0 -226
  127. package/src/traits/VoiceInputTrait.test.ts +0 -252
  128. package/src/traits/VoiceInputTrait.ts +0 -401
  129. package/src/types/AdvancedTypeSystem.js +0 -226
  130. package/src/types/AdvancedTypeSystem.ts +0 -494
  131. package/src/types/HoloScriptPlus.d.ts +0 -853
  132. package/src/types.js +0 -6
  133. package/src/types.ts +0 -369
  134. package/tsconfig.json +0 -23
  135. package/tsup.config.d.ts +0 -2
  136. package/tsup.config.js +0 -18
  137. package/tsup.config.ts +0 -19
@@ -1,1040 +0,0 @@
1
- /**
2
- * VR Trait System
3
- *
4
- * Implements VR interaction traits for HoloScript+ objects:
5
- * - @grabbable - Hand grab interactions
6
- * - @throwable - Physics-based throwing
7
- * - @pointable - Laser pointer interactions
8
- * - @hoverable - Hover state and highlights
9
- * - @scalable - Two-handed scaling
10
- * - @rotatable - Rotation interactions
11
- * - @stackable - Stacking behavior
12
- * - @snappable - Snap-to-point behavior
13
- * - @breakable - Destruction on impact
14
- *
15
- * @version 1.0.0
16
- */
17
-
18
- import type {
19
- VRTraitName,
20
- VRHand,
21
- ThrowVelocity,
22
- CollisionEvent,
23
- Vector3,
24
- GrabbableTrait,
25
- ThrowableTrait,
26
- PointableTrait,
27
- HoverableTrait,
28
- ScalableTrait,
29
- RotatableTrait,
30
- StackableTrait,
31
- SnappableTrait,
32
- BreakableTrait,
33
- HSPlusNode,
34
- } from '../types/HoloScriptPlus';
35
-
36
- // =============================================================================
37
- // TRAIT HANDLER TYPES
38
- // =============================================================================
39
-
40
- export interface TraitHandler<TConfig = unknown> {
41
- name: VRTraitName;
42
- defaultConfig: TConfig;
43
- onAttach?: (node: HSPlusNode, config: TConfig, context: TraitContext) => void;
44
- onDetach?: (node: HSPlusNode, config: TConfig, context: TraitContext) => void;
45
- onUpdate?: (node: HSPlusNode, config: TConfig, context: TraitContext, delta: number) => void;
46
- onEvent?: (node: HSPlusNode, config: TConfig, context: TraitContext, event: TraitEvent) => void;
47
- }
48
-
49
- export interface TraitContext {
50
- vr: VRContext;
51
- physics: PhysicsContext;
52
- audio: AudioContext;
53
- haptics: HapticsContext;
54
- emit: (event: string, payload?: unknown) => void;
55
- getState: () => Record<string, unknown>;
56
- setState: (updates: Record<string, unknown>) => void;
57
- }
58
-
59
- export interface VRContext {
60
- hands: {
61
- left: VRHand | null;
62
- right: VRHand | null;
63
- };
64
- headset: {
65
- position: Vector3;
66
- rotation: Vector3;
67
- };
68
- getPointerRay: (hand: 'left' | 'right') => { origin: Vector3; direction: Vector3 } | null;
69
- getDominantHand: () => VRHand | null;
70
- }
71
-
72
- export interface PhysicsContext {
73
- applyVelocity: (node: HSPlusNode, velocity: Vector3) => void;
74
- applyAngularVelocity: (node: HSPlusNode, angularVelocity: Vector3) => void;
75
- setKinematic: (node: HSPlusNode, kinematic: boolean) => void;
76
- raycast: (origin: Vector3, direction: Vector3, maxDistance: number) => RaycastHit | null;
77
- }
78
-
79
- export interface RaycastHit {
80
- node: HSPlusNode;
81
- point: Vector3;
82
- normal: Vector3;
83
- distance: number;
84
- }
85
-
86
- export interface AudioContext {
87
- playSound: (source: string, options?: { position?: Vector3; volume?: number; spatial?: boolean }) => void;
88
- }
89
-
90
- export interface HapticsContext {
91
- pulse: (hand: 'left' | 'right', intensity: number, duration?: number) => void;
92
- rumble: (hand: 'left' | 'right', intensity: number) => void;
93
- }
94
-
95
- export type TraitEvent =
96
- | { type: 'grab_start'; hand: VRHand }
97
- | { type: 'grab_end'; hand: VRHand; velocity: ThrowVelocity }
98
- | { type: 'hover_enter'; hand: VRHand }
99
- | { type: 'hover_exit'; hand: VRHand }
100
- | { type: 'point_enter'; hand: VRHand }
101
- | { type: 'point_exit'; hand: VRHand }
102
- | { type: 'collision'; data: CollisionEvent }
103
- | { type: 'trigger_enter'; other: HSPlusNode }
104
- | { type: 'trigger_exit'; other: HSPlusNode }
105
- | { type: 'click'; hand: VRHand }
106
- | { type: 'scale_start'; hands: { left: VRHand; right: VRHand } }
107
- | { type: 'scale_update'; scale: number }
108
- | { type: 'scale_end'; finalScale: number }
109
- | { type: 'rotate_start'; hand: VRHand }
110
- | { type: 'rotate_update'; rotation: Vector3 }
111
- | { type: 'rotate_end'; finalRotation: Vector3 };
112
-
113
- // =============================================================================
114
- // TRAIT STATE
115
- // =============================================================================
116
-
117
- interface GrabState {
118
- isGrabbed: boolean;
119
- grabbingHand: VRHand | null;
120
- grabOffset: Vector3;
121
- grabRotationOffset: Vector3;
122
- previousHandPositions: Vector3[];
123
- previousHandTimes: number[];
124
- }
125
-
126
- interface HoverState {
127
- isHovered: boolean;
128
- hoveringHand: VRHand | null;
129
- originalScale: number;
130
- originalColor: string | null;
131
- }
132
-
133
- interface PointState {
134
- isPointed: boolean;
135
- pointingHand: VRHand | null;
136
- }
137
-
138
- interface ScaleState {
139
- isScaling: boolean;
140
- initialDistance: number;
141
- initialScale: number;
142
- }
143
-
144
- interface RotateState {
145
- isRotating: boolean;
146
- initialHandRotation: Vector3;
147
- initialObjectRotation: Vector3;
148
- }
149
-
150
- interface StackState {
151
- stackedItems: HSPlusNode[];
152
- stackParent: HSPlusNode | null;
153
- }
154
-
155
- // =============================================================================
156
- // GRABBABLE TRAIT
157
- // =============================================================================
158
-
159
- const grabbableHandler: TraitHandler<GrabbableTrait> = {
160
- name: 'grabbable',
161
-
162
- defaultConfig: {
163
- snap_to_hand: true,
164
- two_handed: false,
165
- haptic_on_grab: 0.5,
166
- grab_points: [],
167
- preserve_rotation: false,
168
- distance_grab: false,
169
- max_grab_distance: 3,
170
- },
171
-
172
- onAttach(node, config, context) {
173
- // Initialize grab state
174
- const state: GrabState = {
175
- isGrabbed: false,
176
- grabbingHand: null,
177
- grabOffset: [0, 0, 0],
178
- grabRotationOffset: [0, 0, 0],
179
- previousHandPositions: [],
180
- previousHandTimes: [],
181
- };
182
- (node as unknown as { __grabState: GrabState }).__grabState = state;
183
- },
184
-
185
- onDetach(node) {
186
- delete (node as unknown as { __grabState?: GrabState }).__grabState;
187
- },
188
-
189
- onUpdate(node, config, context, delta) {
190
- const state = (node as unknown as { __grabState: GrabState }).__grabState;
191
- if (!state?.isGrabbed || !state.grabbingHand) return;
192
-
193
- // Follow hand position
194
- const hand = state.grabbingHand;
195
- const newPosition: Vector3 = config.snap_to_hand
196
- ? hand.position
197
- : [
198
- hand.position[0] + state.grabOffset[0],
199
- hand.position[1] + state.grabOffset[1],
200
- hand.position[2] + state.grabOffset[2],
201
- ];
202
-
203
- // Update position
204
- node.properties.position = newPosition;
205
-
206
- // Track velocity for throw
207
- state.previousHandPositions.push([...hand.position]);
208
- state.previousHandTimes.push(Date.now());
209
-
210
- // Keep last 10 frames
211
- if (state.previousHandPositions.length > 10) {
212
- state.previousHandPositions.shift();
213
- state.previousHandTimes.shift();
214
- }
215
-
216
- // Update rotation if not preserving
217
- if (!config.preserve_rotation) {
218
- node.properties.rotation = hand.rotation;
219
- }
220
- },
221
-
222
- onEvent(node, config, context, event) {
223
- const state = (node as unknown as { __grabState: GrabState }).__grabState;
224
-
225
- if (event.type === 'grab_start') {
226
- // Check distance for distance grab
227
- if (!config.distance_grab) {
228
- const handPos = event.hand.position;
229
- const nodePos = node.properties.position as Vector3 || [0, 0, 0];
230
- const distance = Math.sqrt(
231
- Math.pow(handPos[0] - nodePos[0], 2) +
232
- Math.pow(handPos[1] - nodePos[1], 2) +
233
- Math.pow(handPos[2] - nodePos[2], 2)
234
- );
235
- if (distance > (config.max_grab_distance || 3)) return;
236
- }
237
-
238
- state.isGrabbed = true;
239
- state.grabbingHand = event.hand;
240
-
241
- // Calculate grab offset
242
- const nodePos = node.properties.position as Vector3 || [0, 0, 0];
243
- state.grabOffset = [
244
- nodePos[0] - event.hand.position[0],
245
- nodePos[1] - event.hand.position[1],
246
- nodePos[2] - event.hand.position[2],
247
- ];
248
-
249
- // Haptic feedback
250
- if (config.haptic_on_grab) {
251
- context.haptics.pulse(event.hand.id, config.haptic_on_grab);
252
- }
253
-
254
- // Make kinematic while grabbed
255
- context.physics.setKinematic(node, true);
256
-
257
- // Emit grab event
258
- context.emit('grab', { node, hand: event.hand });
259
- }
260
-
261
- if (event.type === 'grab_end') {
262
- state.isGrabbed = false;
263
- state.grabbingHand = null;
264
-
265
- // Re-enable physics
266
- context.physics.setKinematic(node, false);
267
-
268
- // Calculate throw velocity from tracked positions
269
- if (state.previousHandPositions.length >= 2) {
270
- const len = state.previousHandPositions.length;
271
- const dt = (state.previousHandTimes[len - 1] - state.previousHandTimes[0]) / 1000;
272
- if (dt > 0) {
273
- const velocity: Vector3 = [
274
- (state.previousHandPositions[len - 1][0] - state.previousHandPositions[0][0]) / dt,
275
- (state.previousHandPositions[len - 1][1] - state.previousHandPositions[0][1]) / dt,
276
- (state.previousHandPositions[len - 1][2] - state.previousHandPositions[0][2]) / dt,
277
- ];
278
-
279
- // Apply velocity if throwable trait exists
280
- if (node.traits.has('throwable')) {
281
- const throwConfig = node.traits.get('throwable') as ThrowableTrait;
282
- const multiplier = throwConfig.velocity_multiplier || 1;
283
- context.physics.applyVelocity(node, [
284
- velocity[0] * multiplier,
285
- velocity[1] * multiplier,
286
- velocity[2] * multiplier,
287
- ]);
288
- }
289
- }
290
- }
291
-
292
- // Clear tracking
293
- state.previousHandPositions = [];
294
- state.previousHandTimes = [];
295
-
296
- // Emit release event
297
- context.emit('release', { node, velocity: event.velocity });
298
- }
299
- },
300
- };
301
-
302
- // =============================================================================
303
- // THROWABLE TRAIT
304
- // =============================================================================
305
-
306
- const throwableHandler: TraitHandler<ThrowableTrait> = {
307
- name: 'throwable',
308
-
309
- defaultConfig: {
310
- velocity_multiplier: 1,
311
- gravity: true,
312
- max_velocity: 50,
313
- spin: true,
314
- bounce: false,
315
- bounce_factor: 0.5,
316
- },
317
-
318
- onAttach(node, config, context) {
319
- // Throwable works with grabbable - just configures throw behavior
320
- },
321
-
322
- onEvent(node, config, context, event) {
323
- if (event.type === 'collision' && config.bounce) {
324
- const collision = event.data;
325
- const bounceFactor = config.bounce_factor || 0.5;
326
-
327
- // Reflect velocity
328
- const velocity = collision.relativeVelocity;
329
- const normal = collision.normal;
330
- const dot = velocity[0] * normal[0] + velocity[1] * normal[1] + velocity[2] * normal[2];
331
- const reflected: Vector3 = [
332
- (velocity[0] - 2 * dot * normal[0]) * bounceFactor,
333
- (velocity[1] - 2 * dot * normal[1]) * bounceFactor,
334
- (velocity[2] - 2 * dot * normal[2]) * bounceFactor,
335
- ];
336
-
337
- context.physics.applyVelocity(node, reflected);
338
- }
339
- },
340
- };
341
-
342
- // =============================================================================
343
- // POINTABLE TRAIT
344
- // =============================================================================
345
-
346
- const pointableHandler: TraitHandler<PointableTrait> = {
347
- name: 'pointable',
348
-
349
- defaultConfig: {
350
- highlight_on_point: true,
351
- highlight_color: '#00ff00',
352
- cursor_style: 'pointer',
353
- },
354
-
355
- onAttach(node, config, context) {
356
- const state: PointState = {
357
- isPointed: false,
358
- pointingHand: null,
359
- };
360
- (node as unknown as { __pointState: PointState }).__pointState = state;
361
- },
362
-
363
- onDetach(node) {
364
- delete (node as unknown as { __pointState?: PointState }).__pointState;
365
- },
366
-
367
- onEvent(node, config, context, event) {
368
- const state = (node as unknown as { __pointState: PointState }).__pointState;
369
-
370
- if (event.type === 'point_enter') {
371
- state.isPointed = true;
372
- state.pointingHand = event.hand;
373
-
374
- if (config.highlight_on_point) {
375
- node.properties.__originalEmissive = node.properties.emissive;
376
- node.properties.emissive = config.highlight_color;
377
- }
378
-
379
- context.emit('point_enter', { node, hand: event.hand });
380
- }
381
-
382
- if (event.type === 'point_exit') {
383
- state.isPointed = false;
384
- state.pointingHand = null;
385
-
386
- if (config.highlight_on_point) {
387
- node.properties.emissive = node.properties.__originalEmissive || null;
388
- delete node.properties.__originalEmissive;
389
- }
390
-
391
- context.emit('point_exit', { node });
392
- }
393
-
394
- if (event.type === 'click') {
395
- context.emit('click', { node, hand: event.hand });
396
- }
397
- },
398
- };
399
-
400
- // =============================================================================
401
- // HOVERABLE TRAIT
402
- // =============================================================================
403
-
404
- const hoverableHandler: TraitHandler<HoverableTrait> = {
405
- name: 'hoverable',
406
-
407
- defaultConfig: {
408
- highlight_color: '#ffffff',
409
- scale_on_hover: 1.1,
410
- show_tooltip: false,
411
- tooltip_offset: [0, 0.2, 0],
412
- glow: false,
413
- glow_intensity: 0.5,
414
- },
415
-
416
- onAttach(node, config, context) {
417
- const state: HoverState = {
418
- isHovered: false,
419
- hoveringHand: null,
420
- originalScale: typeof node.properties.scale === 'number' ? node.properties.scale : 1,
421
- originalColor: null,
422
- };
423
- (node as unknown as { __hoverState: HoverState }).__hoverState = state;
424
- },
425
-
426
- onDetach(node) {
427
- delete (node as unknown as { __hoverState?: HoverState }).__hoverState;
428
- },
429
-
430
- onEvent(node, config, context, event) {
431
- const state = (node as unknown as { __hoverState: HoverState }).__hoverState;
432
-
433
- if (event.type === 'hover_enter') {
434
- state.isHovered = true;
435
- state.hoveringHand = event.hand;
436
-
437
- // Scale up
438
- if (config.scale_on_hover && config.scale_on_hover !== 1) {
439
- state.originalScale = typeof node.properties.scale === 'number' ? node.properties.scale : 1;
440
- node.properties.scale = state.originalScale * config.scale_on_hover;
441
- }
442
-
443
- // Glow effect
444
- if (config.glow) {
445
- state.originalColor = (node.properties.emissive as string) || null;
446
- node.properties.emissive = config.highlight_color;
447
- node.properties.emissiveIntensity = config.glow_intensity;
448
- }
449
-
450
- // Tooltip
451
- if (config.show_tooltip) {
452
- const tooltipText = typeof config.show_tooltip === 'string'
453
- ? config.show_tooltip
454
- : node.properties.tooltip || node.id || node.type;
455
- context.emit('show_tooltip', {
456
- node,
457
- text: tooltipText,
458
- offset: config.tooltip_offset,
459
- });
460
- }
461
-
462
- context.emit('hover_enter', { node, hand: event.hand });
463
- }
464
-
465
- if (event.type === 'hover_exit') {
466
- state.isHovered = false;
467
- state.hoveringHand = null;
468
-
469
- // Restore scale
470
- if (config.scale_on_hover && config.scale_on_hover !== 1) {
471
- node.properties.scale = state.originalScale;
472
- }
473
-
474
- // Remove glow
475
- if (config.glow) {
476
- node.properties.emissive = state.originalColor;
477
- delete node.properties.emissiveIntensity;
478
- }
479
-
480
- // Hide tooltip
481
- if (config.show_tooltip) {
482
- context.emit('hide_tooltip', { node });
483
- }
484
-
485
- context.emit('hover_exit', { node });
486
- }
487
- },
488
- };
489
-
490
- // =============================================================================
491
- // SCALABLE TRAIT
492
- // =============================================================================
493
-
494
- const scalableHandler: TraitHandler<ScalableTrait> = {
495
- name: 'scalable',
496
-
497
- defaultConfig: {
498
- min_scale: 0.1,
499
- max_scale: 10,
500
- uniform: true,
501
- pivot: [0, 0, 0],
502
- },
503
-
504
- onAttach(node, config, context) {
505
- const state: ScaleState = {
506
- isScaling: false,
507
- initialDistance: 0,
508
- initialScale: 1,
509
- };
510
- (node as unknown as { __scaleState: ScaleState }).__scaleState = state;
511
- },
512
-
513
- onDetach(node) {
514
- delete (node as unknown as { __scaleState?: ScaleState }).__scaleState;
515
- },
516
-
517
- onUpdate(node, config, context, delta) {
518
- const state = (node as unknown as { __scaleState: ScaleState }).__scaleState;
519
- if (!state?.isScaling) return;
520
-
521
- const { hands } = context.vr;
522
- if (!hands.left || !hands.right) return;
523
-
524
- // Calculate current distance between hands
525
- const currentDistance = Math.sqrt(
526
- Math.pow(hands.right.position[0] - hands.left.position[0], 2) +
527
- Math.pow(hands.right.position[1] - hands.left.position[1], 2) +
528
- Math.pow(hands.right.position[2] - hands.left.position[2], 2)
529
- );
530
-
531
- // Calculate scale factor
532
- const scaleFactor = currentDistance / state.initialDistance;
533
- let newScale = state.initialScale * scaleFactor;
534
-
535
- // Clamp scale
536
- newScale = Math.max(config.min_scale || 0.1, Math.min(config.max_scale || 10, newScale));
537
-
538
- node.properties.scale = newScale;
539
-
540
- context.emit('scale_update', { node, scale: newScale });
541
- },
542
-
543
- onEvent(node, config, context, event) {
544
- const state = (node as unknown as { __scaleState: ScaleState }).__scaleState;
545
-
546
- if (event.type === 'scale_start') {
547
- state.isScaling = true;
548
- state.initialScale = typeof node.properties.scale === 'number' ? node.properties.scale : 1;
549
-
550
- // Calculate initial distance between hands
551
- const { left, right } = event.hands;
552
- state.initialDistance = Math.sqrt(
553
- Math.pow(right.position[0] - left.position[0], 2) +
554
- Math.pow(right.position[1] - left.position[1], 2) +
555
- Math.pow(right.position[2] - left.position[2], 2)
556
- );
557
-
558
- context.emit('scale_start', { node });
559
- }
560
-
561
- if (event.type === 'scale_end') {
562
- state.isScaling = false;
563
- context.emit('scale_end', { node, finalScale: node.properties.scale });
564
- }
565
- },
566
- };
567
-
568
- // =============================================================================
569
- // ROTATABLE TRAIT
570
- // =============================================================================
571
-
572
- const rotatableHandler: TraitHandler<RotatableTrait> = {
573
- name: 'rotatable',
574
-
575
- defaultConfig: {
576
- axis: 'all',
577
- snap_angles: [],
578
- speed: 1,
579
- },
580
-
581
- onAttach(node, _config, _context) {
582
- const state: RotateState = {
583
- isRotating: false,
584
- initialHandRotation: [0, 0, 0],
585
- initialObjectRotation: [0, 0, 0],
586
- };
587
- (node as unknown as { __rotateState: RotateState }).__rotateState = state;
588
- },
589
-
590
- onDetach(node) {
591
- delete (node as unknown as { __rotateState?: RotateState }).__rotateState;
592
- },
593
-
594
- onUpdate(node, config, context, _delta) {
595
- const state = (node as unknown as { __rotateState: RotateState }).__rotateState;
596
- if (!state?.isRotating) return;
597
-
598
- const hand = context.vr.getDominantHand();
599
- if (!hand) return;
600
-
601
- // Calculate rotation delta
602
- const deltaRotation: Vector3 = [
603
- (hand.rotation[0] - state.initialHandRotation[0]) * (config.speed || 1),
604
- (hand.rotation[1] - state.initialHandRotation[1]) * (config.speed || 1),
605
- (hand.rotation[2] - state.initialHandRotation[2]) * (config.speed || 1),
606
- ];
607
-
608
- // Apply axis constraint
609
- let newRotation: Vector3;
610
- switch (config.axis) {
611
- case 'x':
612
- newRotation = [
613
- state.initialObjectRotation[0] + deltaRotation[0],
614
- state.initialObjectRotation[1],
615
- state.initialObjectRotation[2],
616
- ];
617
- break;
618
- case 'y':
619
- newRotation = [
620
- state.initialObjectRotation[0],
621
- state.initialObjectRotation[1] + deltaRotation[1],
622
- state.initialObjectRotation[2],
623
- ];
624
- break;
625
- case 'z':
626
- newRotation = [
627
- state.initialObjectRotation[0],
628
- state.initialObjectRotation[1],
629
- state.initialObjectRotation[2] + deltaRotation[2],
630
- ];
631
- break;
632
- default:
633
- newRotation = [
634
- state.initialObjectRotation[0] + deltaRotation[0],
635
- state.initialObjectRotation[1] + deltaRotation[1],
636
- state.initialObjectRotation[2] + deltaRotation[2],
637
- ];
638
- }
639
-
640
- // Snap to angles if configured
641
- if (config.snap_angles && config.snap_angles.length > 0) {
642
- newRotation = newRotation.map((angle) => {
643
- let closest = config.snap_angles![0];
644
- let minDiff = Math.abs(angle - closest);
645
- for (const snapAngle of config.snap_angles!) {
646
- const diff = Math.abs(angle - snapAngle);
647
- if (diff < minDiff) {
648
- minDiff = diff;
649
- closest = snapAngle;
650
- }
651
- }
652
- // Only snap if close enough
653
- return minDiff < 10 ? closest : angle;
654
- }) as Vector3;
655
- }
656
-
657
- node.properties.rotation = newRotation;
658
- context.emit('rotate_update', { node, rotation: newRotation });
659
- },
660
-
661
- onEvent(node, _config, context, event) {
662
- const state = (node as unknown as { __rotateState: RotateState }).__rotateState;
663
-
664
- if (event.type === 'rotate_start') {
665
- state.isRotating = true;
666
- state.initialHandRotation = [...event.hand.rotation];
667
- state.initialObjectRotation = (node.properties.rotation as Vector3) || [0, 0, 0];
668
-
669
- context.emit('rotate_start', { node });
670
- }
671
-
672
- if (event.type === 'rotate_end') {
673
- state.isRotating = false;
674
- context.emit('rotate_end', { node, finalRotation: node.properties.rotation });
675
- }
676
- },
677
- };
678
-
679
- // =============================================================================
680
- // STACKABLE TRAIT
681
- // =============================================================================
682
-
683
- const stackableHandler: TraitHandler<StackableTrait> = {
684
- name: 'stackable',
685
-
686
- defaultConfig: {
687
- stack_axis: 'y',
688
- stack_offset: 0,
689
- max_stack: 10,
690
- snap_distance: 0.5,
691
- },
692
-
693
- onAttach(node, _config, _context) {
694
- const state: StackState = {
695
- stackedItems: [],
696
- stackParent: null,
697
- };
698
- (node as unknown as { __stackState: StackState }).__stackState = state;
699
- },
700
-
701
- onDetach(node) {
702
- const state = (node as unknown as { __stackState: StackState }).__stackState;
703
- // Remove from parent stack
704
- if (state.stackParent) {
705
- const parentState = (state.stackParent as unknown as { __stackState: StackState }).__stackState;
706
- const index = parentState.stackedItems.indexOf(node);
707
- if (index > -1) {
708
- parentState.stackedItems.splice(index, 1);
709
- }
710
- }
711
- // Clear children
712
- state.stackedItems = [];
713
- delete (node as unknown as { __stackState?: StackState }).__stackState;
714
- },
715
-
716
- onEvent(node, config, context, event) {
717
- const state = (node as unknown as { __stackState: StackState }).__stackState;
718
-
719
- if (event.type === 'collision' || event.type === 'trigger_enter') {
720
- const other = event.type === 'collision' ? event.data.target : (event as { other: HSPlusNode }).other;
721
-
722
- // Check if other is stackable
723
- if (!other.traits.has('stackable')) return;
724
-
725
- const otherState = (other as unknown as { __stackState: StackState }).__stackState;
726
- if (!otherState) return;
727
-
728
- // Check stack limit
729
- if (state.stackedItems.length >= (config.max_stack || 10)) return;
730
-
731
- // Check if close enough
732
- const nodePos = node.properties.position as Vector3 || [0, 0, 0];
733
- const otherPos = other.properties.position as Vector3 || [0, 0, 0];
734
-
735
- const axisIndex = config.stack_axis === 'x' ? 0 : config.stack_axis === 'z' ? 2 : 1;
736
- const otherAxes = [0, 1, 2].filter((i) => i !== axisIndex);
737
-
738
- // Check alignment on other axes
739
- let aligned = true;
740
- for (const axis of otherAxes) {
741
- if (Math.abs(nodePos[axis] - otherPos[axis]) > (config.snap_distance || 0.5)) {
742
- aligned = false;
743
- break;
744
- }
745
- }
746
-
747
- if (aligned && otherPos[axisIndex] > nodePos[axisIndex]) {
748
- // Other is above - add to stack
749
- state.stackedItems.push(other);
750
- otherState.stackParent = node;
751
-
752
- // Snap position
753
- const stackOffset = config.stack_offset || 0;
754
- const newPos: Vector3 = [...nodePos];
755
- newPos[axisIndex] = nodePos[axisIndex] + stackOffset;
756
-
757
- other.properties.position = newPos;
758
-
759
- context.emit('stack', { parent: node, child: other });
760
- }
761
- }
762
- },
763
- };
764
-
765
- // =============================================================================
766
- // SNAPPABLE TRAIT
767
- // =============================================================================
768
-
769
- const snappableHandler: TraitHandler<SnappableTrait> = {
770
- name: 'snappable',
771
-
772
- defaultConfig: {
773
- snap_points: [],
774
- snap_distance: 0.3,
775
- snap_rotation: false,
776
- magnetic: false,
777
- },
778
-
779
- onUpdate(node, config, _context, _delta) {
780
- if (!config.snap_points || config.snap_points.length === 0) return;
781
- if (!config.magnetic) return;
782
-
783
- const nodePos = node.properties.position as Vector3 || [0, 0, 0];
784
-
785
- // Find closest snap point
786
- let closestPoint: Vector3 | null = null;
787
- let closestDistance = config.snap_distance || 0.3;
788
-
789
- for (const snapPoint of config.snap_points) {
790
- const distance = Math.sqrt(
791
- Math.pow(nodePos[0] - snapPoint[0], 2) +
792
- Math.pow(nodePos[1] - snapPoint[1], 2) +
793
- Math.pow(nodePos[2] - snapPoint[2], 2)
794
- );
795
-
796
- if (distance < closestDistance) {
797
- closestDistance = distance;
798
- closestPoint = snapPoint;
799
- }
800
- }
801
-
802
- // Apply magnetic pull
803
- if (closestPoint) {
804
- const pullStrength = 0.1;
805
- node.properties.position = [
806
- nodePos[0] + (closestPoint[0] - nodePos[0]) * pullStrength,
807
- nodePos[1] + (closestPoint[1] - nodePos[1]) * pullStrength,
808
- nodePos[2] + (closestPoint[2] - nodePos[2]) * pullStrength,
809
- ];
810
- }
811
- },
812
-
813
- onEvent(node, config, context, event) {
814
- if (event.type !== 'grab_end') return;
815
- if (!config.snap_points || config.snap_points.length === 0) return;
816
-
817
- const nodePos = node.properties.position as Vector3 || [0, 0, 0];
818
-
819
- // Find closest snap point
820
- let closestPoint: Vector3 | null = null;
821
- let closestDistance = config.snap_distance || 0.3;
822
-
823
- for (const snapPoint of config.snap_points) {
824
- const distance = Math.sqrt(
825
- Math.pow(nodePos[0] - snapPoint[0], 2) +
826
- Math.pow(nodePos[1] - snapPoint[1], 2) +
827
- Math.pow(nodePos[2] - snapPoint[2], 2)
828
- );
829
-
830
- if (distance < closestDistance) {
831
- closestDistance = distance;
832
- closestPoint = snapPoint;
833
- }
834
- }
835
-
836
- // Snap to closest point
837
- if (closestPoint) {
838
- node.properties.position = closestPoint;
839
- context.emit('snap', { node, point: closestPoint });
840
-
841
- // Haptic feedback
842
- context.haptics.pulse(event.hand.id, 0.3);
843
- }
844
- },
845
- };
846
-
847
- // =============================================================================
848
- // BREAKABLE TRAIT
849
- // =============================================================================
850
-
851
- const breakableHandler: TraitHandler<BreakableTrait> = {
852
- name: 'breakable',
853
-
854
- defaultConfig: {
855
- break_velocity: 5,
856
- fragments: 8,
857
- fragment_mesh: undefined,
858
- sound_on_break: undefined,
859
- respawn: false,
860
- respawn_delay: '5s',
861
- },
862
-
863
- onEvent(node, config, context, event) {
864
- if (event.type !== 'collision') return;
865
-
866
- const collision = event.data;
867
- const impactVelocity = Math.sqrt(
868
- Math.pow(collision.relativeVelocity[0], 2) +
869
- Math.pow(collision.relativeVelocity[1], 2) +
870
- Math.pow(collision.relativeVelocity[2], 2)
871
- );
872
-
873
- if (impactVelocity < (config.break_velocity || 5)) return;
874
-
875
- // Play break sound
876
- if (config.sound_on_break) {
877
- context.audio.playSound(config.sound_on_break, {
878
- position: collision.point,
879
- spatial: true,
880
- });
881
- }
882
-
883
- // Spawn fragments
884
- const fragmentCount = config.fragments || 8;
885
- for (let i = 0; i < fragmentCount; i++) {
886
- const angle = (i / fragmentCount) * Math.PI * 2;
887
- const velocity: Vector3 = [
888
- Math.cos(angle) * 2,
889
- Math.random() * 3,
890
- Math.sin(angle) * 2,
891
- ];
892
-
893
- context.emit('spawn_fragment', {
894
- position: collision.point,
895
- velocity,
896
- mesh: config.fragment_mesh,
897
- });
898
- }
899
-
900
- // Emit break event
901
- context.emit('break', { node, impactVelocity, collision });
902
-
903
- // Handle respawn
904
- if (config.respawn) {
905
- const delay = parseDuration(config.respawn_delay || '5s');
906
- setTimeout(() => {
907
- context.emit('respawn', { node });
908
- }, delay);
909
- }
910
-
911
- // Mark for destruction
912
- node.properties.__destroyed = true;
913
- },
914
- };
915
-
916
- // =============================================================================
917
- // UTILITIES
918
- // =============================================================================
919
-
920
- function parseDuration(duration: string): number {
921
- const match = duration.match(/^(\d+(?:\.\d+)?)(ms|s|m)$/);
922
- if (!match) return 0;
923
-
924
- const value = parseFloat(match[1]);
925
- const unit = match[2];
926
-
927
- switch (unit) {
928
- case 'ms':
929
- return value;
930
- case 's':
931
- return value * 1000;
932
- case 'm':
933
- return value * 60 * 1000;
934
- default:
935
- return value;
936
- }
937
- }
938
-
939
- // =============================================================================
940
- // TRAIT REGISTRY
941
- // =============================================================================
942
-
943
- export class VRTraitRegistry {
944
- private handlers: Map<VRTraitName, TraitHandler> = new Map();
945
-
946
- constructor() {
947
- // Register all built-in handlers
948
- this.register(grabbableHandler);
949
- this.register(throwableHandler);
950
- this.register(pointableHandler);
951
- this.register(hoverableHandler);
952
- this.register(scalableHandler);
953
- this.register(rotatableHandler);
954
- this.register(stackableHandler);
955
- this.register(snappableHandler);
956
- this.register(breakableHandler);
957
- }
958
-
959
- register<T>(handler: TraitHandler<T>): void {
960
- this.handlers.set(handler.name, handler as TraitHandler);
961
- }
962
-
963
- getHandler(name: VRTraitName): TraitHandler | undefined {
964
- return this.handlers.get(name);
965
- }
966
-
967
- attachTrait(node: HSPlusNode, traitName: VRTraitName, config: unknown, context: TraitContext): void {
968
- const handler = this.handlers.get(traitName);
969
- if (!handler) return;
970
-
971
- const mergedConfig = { ...(handler.defaultConfig as object), ...(config as object) };
972
- node.traits.set(traitName, mergedConfig);
973
-
974
- if (handler.onAttach) {
975
- handler.onAttach(node, mergedConfig, context);
976
- }
977
- }
978
-
979
- detachTrait(node: HSPlusNode, traitName: VRTraitName, context: TraitContext): void {
980
- const handler = this.handlers.get(traitName);
981
- if (!handler) return;
982
-
983
- const config = node.traits.get(traitName);
984
- if (config && handler.onDetach) {
985
- handler.onDetach(node, config, context);
986
- }
987
-
988
- node.traits.delete(traitName);
989
- }
990
-
991
- updateTrait(node: HSPlusNode, traitName: VRTraitName, context: TraitContext, delta: number): void {
992
- const handler = this.handlers.get(traitName);
993
- if (!handler || !handler.onUpdate) return;
994
-
995
- const config = node.traits.get(traitName);
996
- if (config) {
997
- handler.onUpdate(node, config, context, delta);
998
- }
999
- }
1000
-
1001
- handleEvent(node: HSPlusNode, traitName: VRTraitName, context: TraitContext, event: TraitEvent): void {
1002
- const handler = this.handlers.get(traitName);
1003
- if (!handler || !handler.onEvent) return;
1004
-
1005
- const config = node.traits.get(traitName);
1006
- if (config) {
1007
- handler.onEvent(node, config, context, event);
1008
- }
1009
- }
1010
-
1011
- updateAllTraits(node: HSPlusNode, context: TraitContext, delta: number): void {
1012
- for (const traitName of node.traits.keys()) {
1013
- this.updateTrait(node, traitName, context, delta);
1014
- }
1015
- }
1016
-
1017
- handleEventForAllTraits(node: HSPlusNode, context: TraitContext, event: TraitEvent): void {
1018
- for (const traitName of node.traits.keys()) {
1019
- this.handleEvent(node, traitName, context, event);
1020
- }
1021
- }
1022
- }
1023
-
1024
- // =============================================================================
1025
- // EXPORTS
1026
- // =============================================================================
1027
-
1028
- export const vrTraitRegistry = new VRTraitRegistry();
1029
-
1030
- export {
1031
- grabbableHandler,
1032
- throwableHandler,
1033
- pointableHandler,
1034
- hoverableHandler,
1035
- scalableHandler,
1036
- rotatableHandler,
1037
- stackableHandler,
1038
- snappableHandler,
1039
- breakableHandler,
1040
- };