@holoscript/core 2.0.1 → 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.
- package/LICENSE +21 -0
- package/dist/chunk-3N67RLQP.cjs +1298 -0
- package/dist/chunk-3N67RLQP.cjs.map +1 -0
- package/dist/chunk-3X2EGU7Z.cjs +52 -0
- package/dist/chunk-3X2EGU7Z.cjs.map +1 -0
- package/dist/chunk-4CV4JOE5.js +24 -0
- package/dist/chunk-4CV4JOE5.js.map +1 -0
- package/dist/chunk-4OHVW4XR.cjs +1027 -0
- package/dist/chunk-4OHVW4XR.cjs.map +1 -0
- package/dist/chunk-CZLDE2OZ.cjs +28 -0
- package/dist/chunk-CZLDE2OZ.cjs.map +1 -0
- package/{src/HoloScriptRuntime.ts → dist/chunk-EU6CZMGJ.js} +437 -794
- package/dist/chunk-EU6CZMGJ.js.map +1 -0
- package/dist/chunk-KWYIVRIH.js +344 -0
- package/dist/chunk-KWYIVRIH.js.map +1 -0
- package/dist/chunk-MCP6D4LT.js +1025 -0
- package/dist/chunk-MCP6D4LT.js.map +1 -0
- package/dist/chunk-SATNCODL.js +45 -0
- package/dist/chunk-SATNCODL.js.map +1 -0
- package/dist/chunk-VMZN4EVR.cjs +347 -0
- package/dist/chunk-VMZN4EVR.cjs.map +1 -0
- package/{src/HoloScriptDebugger.ts → dist/chunk-VYIDLUCV.js} +118 -257
- package/dist/chunk-VYIDLUCV.js.map +1 -0
- package/dist/chunk-WFI4T3XB.cjs +424 -0
- package/dist/chunk-WFI4T3XB.cjs.map +1 -0
- package/dist/debugger.cjs +20 -0
- package/dist/debugger.cjs.map +1 -0
- package/dist/debugger.d.cts +171 -0
- package/dist/debugger.d.ts +171 -0
- package/dist/debugger.js +7 -0
- package/dist/debugger.js.map +1 -0
- package/dist/index.cjs +6006 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2482 -0
- package/dist/index.d.ts +2482 -0
- package/dist/index.js +5926 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.cjs +14 -0
- package/dist/parser.cjs.map +1 -0
- package/dist/parser.d.cts +139 -0
- package/dist/parser.d.ts +139 -0
- package/dist/parser.js +5 -0
- package/dist/parser.js.map +1 -0
- package/dist/runtime.cjs +14 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.cts +180 -0
- package/dist/runtime.d.ts +180 -0
- package/dist/runtime.js +5 -0
- package/dist/runtime.js.map +1 -0
- package/dist/type-checker.cjs +17 -0
- package/dist/type-checker.cjs.map +1 -0
- package/dist/type-checker.d.cts +105 -0
- package/dist/type-checker.d.ts +105 -0
- package/dist/type-checker.js +4 -0
- package/dist/type-checker.js.map +1 -0
- package/dist/types-D6g4ACjP.d.cts +262 -0
- package/dist/types-D6g4ACjP.d.ts +262 -0
- package/package.json +11 -8
- package/src/HoloScript2DParser.js +0 -227
- package/src/HoloScript2DParser.ts +0 -261
- package/src/HoloScriptCodeParser.js +0 -1102
- package/src/HoloScriptCodeParser.ts +0 -1188
- package/src/HoloScriptDebugger.js +0 -458
- package/src/HoloScriptParser.js +0 -338
- package/src/HoloScriptParser.ts +0 -397
- package/src/HoloScriptPlusParser.js +0 -371
- package/src/HoloScriptPlusParser.ts +0 -543
- package/src/HoloScriptRuntime.js +0 -1399
- package/src/HoloScriptRuntime.test.js +0 -351
- package/src/HoloScriptRuntime.test.ts +0 -436
- package/src/HoloScriptTypeChecker.js +0 -356
- package/src/HoloScriptTypeChecker.ts +0 -475
- package/src/__tests__/GraphicsServices.test.js +0 -357
- package/src/__tests__/GraphicsServices.test.ts +0 -427
- package/src/__tests__/HoloScriptPlusParser.test.js +0 -317
- package/src/__tests__/HoloScriptPlusParser.test.ts +0 -392
- package/src/__tests__/integration.test.js +0 -336
- package/src/__tests__/integration.test.ts +0 -416
- package/src/__tests__/performance.bench.js +0 -218
- package/src/__tests__/performance.bench.ts +0 -262
- package/src/__tests__/type-checker.test.js +0 -60
- package/src/__tests__/type-checker.test.ts +0 -73
- package/src/index.js +0 -217
- package/src/index.ts +0 -426
- package/src/interop/Interoperability.js +0 -413
- package/src/interop/Interoperability.ts +0 -494
- package/src/logger.js +0 -42
- package/src/logger.ts +0 -57
- package/src/parser/EnhancedParser.js +0 -205
- package/src/parser/EnhancedParser.ts +0 -251
- package/src/parser/HoloScriptPlusParser.js +0 -928
- package/src/parser/HoloScriptPlusParser.ts +0 -1089
- package/src/runtime/HoloScriptPlusRuntime.js +0 -674
- package/src/runtime/HoloScriptPlusRuntime.ts +0 -861
- package/src/runtime/PerformanceTelemetry.js +0 -323
- package/src/runtime/PerformanceTelemetry.ts +0 -467
- package/src/runtime/RuntimeOptimization.js +0 -361
- package/src/runtime/RuntimeOptimization.ts +0 -416
- package/src/services/HololandGraphicsPipelineService.js +0 -506
- package/src/services/HololandGraphicsPipelineService.ts +0 -662
- package/src/services/PlatformPerformanceOptimizer.js +0 -356
- package/src/services/PlatformPerformanceOptimizer.ts +0 -503
- package/src/state/ReactiveState.js +0 -427
- package/src/state/ReactiveState.ts +0 -572
- package/src/tools/DeveloperExperience.js +0 -376
- package/src/tools/DeveloperExperience.ts +0 -438
- package/src/traits/AIDriverTrait.js +0 -322
- package/src/traits/AIDriverTrait.test.js +0 -329
- package/src/traits/AIDriverTrait.test.ts +0 -357
- package/src/traits/AIDriverTrait.ts +0 -474
- package/src/traits/LightingTrait.js +0 -313
- package/src/traits/LightingTrait.test.js +0 -410
- package/src/traits/LightingTrait.test.ts +0 -462
- package/src/traits/LightingTrait.ts +0 -505
- package/src/traits/MaterialTrait.js +0 -194
- package/src/traits/MaterialTrait.test.js +0 -286
- package/src/traits/MaterialTrait.test.ts +0 -329
- package/src/traits/MaterialTrait.ts +0 -324
- package/src/traits/RenderingTrait.js +0 -356
- package/src/traits/RenderingTrait.test.js +0 -363
- package/src/traits/RenderingTrait.test.ts +0 -427
- package/src/traits/RenderingTrait.ts +0 -555
- package/src/traits/VRTraitSystem.js +0 -740
- package/src/traits/VRTraitSystem.ts +0 -1040
- package/src/traits/VoiceInputTrait.js +0 -284
- package/src/traits/VoiceInputTrait.test.js +0 -226
- package/src/traits/VoiceInputTrait.test.ts +0 -252
- package/src/traits/VoiceInputTrait.ts +0 -401
- package/src/types/AdvancedTypeSystem.js +0 -226
- package/src/types/AdvancedTypeSystem.ts +0 -494
- package/src/types/HoloScriptPlus.d.ts +0 -853
- package/src/types.js +0 -6
- package/src/types.ts +0 -369
- package/tsconfig.json +0 -23
- package/tsup.config.d.ts +0 -2
- package/tsup.config.js +0 -18
- package/tsup.config.ts +0 -19
|
@@ -1,740 +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
|
-
// GRABBABLE TRAIT
|
|
19
|
-
// =============================================================================
|
|
20
|
-
const grabbableHandler = {
|
|
21
|
-
name: 'grabbable',
|
|
22
|
-
defaultConfig: {
|
|
23
|
-
snap_to_hand: true,
|
|
24
|
-
two_handed: false,
|
|
25
|
-
haptic_on_grab: 0.5,
|
|
26
|
-
grab_points: [],
|
|
27
|
-
preserve_rotation: false,
|
|
28
|
-
distance_grab: false,
|
|
29
|
-
max_grab_distance: 3,
|
|
30
|
-
},
|
|
31
|
-
onAttach(node, config, context) {
|
|
32
|
-
// Initialize grab state
|
|
33
|
-
const state = {
|
|
34
|
-
isGrabbed: false,
|
|
35
|
-
grabbingHand: null,
|
|
36
|
-
grabOffset: [0, 0, 0],
|
|
37
|
-
grabRotationOffset: [0, 0, 0],
|
|
38
|
-
previousHandPositions: [],
|
|
39
|
-
previousHandTimes: [],
|
|
40
|
-
};
|
|
41
|
-
node.__grabState = state;
|
|
42
|
-
},
|
|
43
|
-
onDetach(node) {
|
|
44
|
-
delete node.__grabState;
|
|
45
|
-
},
|
|
46
|
-
onUpdate(node, config, context, delta) {
|
|
47
|
-
const state = node.__grabState;
|
|
48
|
-
if (!state?.isGrabbed || !state.grabbingHand)
|
|
49
|
-
return;
|
|
50
|
-
// Follow hand position
|
|
51
|
-
const hand = state.grabbingHand;
|
|
52
|
-
const newPosition = config.snap_to_hand
|
|
53
|
-
? hand.position
|
|
54
|
-
: [
|
|
55
|
-
hand.position[0] + state.grabOffset[0],
|
|
56
|
-
hand.position[1] + state.grabOffset[1],
|
|
57
|
-
hand.position[2] + state.grabOffset[2],
|
|
58
|
-
];
|
|
59
|
-
// Update position
|
|
60
|
-
node.properties.position = newPosition;
|
|
61
|
-
// Track velocity for throw
|
|
62
|
-
state.previousHandPositions.push([...hand.position]);
|
|
63
|
-
state.previousHandTimes.push(Date.now());
|
|
64
|
-
// Keep last 10 frames
|
|
65
|
-
if (state.previousHandPositions.length > 10) {
|
|
66
|
-
state.previousHandPositions.shift();
|
|
67
|
-
state.previousHandTimes.shift();
|
|
68
|
-
}
|
|
69
|
-
// Update rotation if not preserving
|
|
70
|
-
if (!config.preserve_rotation) {
|
|
71
|
-
node.properties.rotation = hand.rotation;
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
onEvent(node, config, context, event) {
|
|
75
|
-
const state = node.__grabState;
|
|
76
|
-
if (event.type === 'grab_start') {
|
|
77
|
-
// Check distance for distance grab
|
|
78
|
-
if (!config.distance_grab) {
|
|
79
|
-
const handPos = event.hand.position;
|
|
80
|
-
const nodePos = node.properties.position || [0, 0, 0];
|
|
81
|
-
const distance = Math.sqrt(Math.pow(handPos[0] - nodePos[0], 2) +
|
|
82
|
-
Math.pow(handPos[1] - nodePos[1], 2) +
|
|
83
|
-
Math.pow(handPos[2] - nodePos[2], 2));
|
|
84
|
-
if (distance > (config.max_grab_distance || 3))
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
state.isGrabbed = true;
|
|
88
|
-
state.grabbingHand = event.hand;
|
|
89
|
-
// Calculate grab offset
|
|
90
|
-
const nodePos = node.properties.position || [0, 0, 0];
|
|
91
|
-
state.grabOffset = [
|
|
92
|
-
nodePos[0] - event.hand.position[0],
|
|
93
|
-
nodePos[1] - event.hand.position[1],
|
|
94
|
-
nodePos[2] - event.hand.position[2],
|
|
95
|
-
];
|
|
96
|
-
// Haptic feedback
|
|
97
|
-
if (config.haptic_on_grab) {
|
|
98
|
-
context.haptics.pulse(event.hand.id, config.haptic_on_grab);
|
|
99
|
-
}
|
|
100
|
-
// Make kinematic while grabbed
|
|
101
|
-
context.physics.setKinematic(node, true);
|
|
102
|
-
// Emit grab event
|
|
103
|
-
context.emit('grab', { node, hand: event.hand });
|
|
104
|
-
}
|
|
105
|
-
if (event.type === 'grab_end') {
|
|
106
|
-
state.isGrabbed = false;
|
|
107
|
-
state.grabbingHand = null;
|
|
108
|
-
// Re-enable physics
|
|
109
|
-
context.physics.setKinematic(node, false);
|
|
110
|
-
// Calculate throw velocity from tracked positions
|
|
111
|
-
if (state.previousHandPositions.length >= 2) {
|
|
112
|
-
const len = state.previousHandPositions.length;
|
|
113
|
-
const dt = (state.previousHandTimes[len - 1] - state.previousHandTimes[0]) / 1000;
|
|
114
|
-
if (dt > 0) {
|
|
115
|
-
const velocity = [
|
|
116
|
-
(state.previousHandPositions[len - 1][0] - state.previousHandPositions[0][0]) / dt,
|
|
117
|
-
(state.previousHandPositions[len - 1][1] - state.previousHandPositions[0][1]) / dt,
|
|
118
|
-
(state.previousHandPositions[len - 1][2] - state.previousHandPositions[0][2]) / dt,
|
|
119
|
-
];
|
|
120
|
-
// Apply velocity if throwable trait exists
|
|
121
|
-
if (node.traits.has('throwable')) {
|
|
122
|
-
const throwConfig = node.traits.get('throwable');
|
|
123
|
-
const multiplier = throwConfig.velocity_multiplier || 1;
|
|
124
|
-
context.physics.applyVelocity(node, [
|
|
125
|
-
velocity[0] * multiplier,
|
|
126
|
-
velocity[1] * multiplier,
|
|
127
|
-
velocity[2] * multiplier,
|
|
128
|
-
]);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// Clear tracking
|
|
133
|
-
state.previousHandPositions = [];
|
|
134
|
-
state.previousHandTimes = [];
|
|
135
|
-
// Emit release event
|
|
136
|
-
context.emit('release', { node, velocity: event.velocity });
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
// =============================================================================
|
|
141
|
-
// THROWABLE TRAIT
|
|
142
|
-
// =============================================================================
|
|
143
|
-
const throwableHandler = {
|
|
144
|
-
name: 'throwable',
|
|
145
|
-
defaultConfig: {
|
|
146
|
-
velocity_multiplier: 1,
|
|
147
|
-
gravity: true,
|
|
148
|
-
max_velocity: 50,
|
|
149
|
-
spin: true,
|
|
150
|
-
bounce: false,
|
|
151
|
-
bounce_factor: 0.5,
|
|
152
|
-
},
|
|
153
|
-
onAttach(node, config, context) {
|
|
154
|
-
// Throwable works with grabbable - just configures throw behavior
|
|
155
|
-
},
|
|
156
|
-
onEvent(node, config, context, event) {
|
|
157
|
-
if (event.type === 'collision' && config.bounce) {
|
|
158
|
-
const collision = event.data;
|
|
159
|
-
const bounceFactor = config.bounce_factor || 0.5;
|
|
160
|
-
// Reflect velocity
|
|
161
|
-
const velocity = collision.relativeVelocity;
|
|
162
|
-
const normal = collision.normal;
|
|
163
|
-
const dot = velocity[0] * normal[0] + velocity[1] * normal[1] + velocity[2] * normal[2];
|
|
164
|
-
const reflected = [
|
|
165
|
-
(velocity[0] - 2 * dot * normal[0]) * bounceFactor,
|
|
166
|
-
(velocity[1] - 2 * dot * normal[1]) * bounceFactor,
|
|
167
|
-
(velocity[2] - 2 * dot * normal[2]) * bounceFactor,
|
|
168
|
-
];
|
|
169
|
-
context.physics.applyVelocity(node, reflected);
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
// =============================================================================
|
|
174
|
-
// POINTABLE TRAIT
|
|
175
|
-
// =============================================================================
|
|
176
|
-
const pointableHandler = {
|
|
177
|
-
name: 'pointable',
|
|
178
|
-
defaultConfig: {
|
|
179
|
-
highlight_on_point: true,
|
|
180
|
-
highlight_color: '#00ff00',
|
|
181
|
-
cursor_style: 'pointer',
|
|
182
|
-
},
|
|
183
|
-
onAttach(node, config, context) {
|
|
184
|
-
const state = {
|
|
185
|
-
isPointed: false,
|
|
186
|
-
pointingHand: null,
|
|
187
|
-
};
|
|
188
|
-
node.__pointState = state;
|
|
189
|
-
},
|
|
190
|
-
onDetach(node) {
|
|
191
|
-
delete node.__pointState;
|
|
192
|
-
},
|
|
193
|
-
onEvent(node, config, context, event) {
|
|
194
|
-
const state = node.__pointState;
|
|
195
|
-
if (event.type === 'point_enter') {
|
|
196
|
-
state.isPointed = true;
|
|
197
|
-
state.pointingHand = event.hand;
|
|
198
|
-
if (config.highlight_on_point) {
|
|
199
|
-
node.properties.__originalEmissive = node.properties.emissive;
|
|
200
|
-
node.properties.emissive = config.highlight_color;
|
|
201
|
-
}
|
|
202
|
-
context.emit('point_enter', { node, hand: event.hand });
|
|
203
|
-
}
|
|
204
|
-
if (event.type === 'point_exit') {
|
|
205
|
-
state.isPointed = false;
|
|
206
|
-
state.pointingHand = null;
|
|
207
|
-
if (config.highlight_on_point) {
|
|
208
|
-
node.properties.emissive = node.properties.__originalEmissive || null;
|
|
209
|
-
delete node.properties.__originalEmissive;
|
|
210
|
-
}
|
|
211
|
-
context.emit('point_exit', { node });
|
|
212
|
-
}
|
|
213
|
-
if (event.type === 'click') {
|
|
214
|
-
context.emit('click', { node, hand: event.hand });
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
// =============================================================================
|
|
219
|
-
// HOVERABLE TRAIT
|
|
220
|
-
// =============================================================================
|
|
221
|
-
const hoverableHandler = {
|
|
222
|
-
name: 'hoverable',
|
|
223
|
-
defaultConfig: {
|
|
224
|
-
highlight_color: '#ffffff',
|
|
225
|
-
scale_on_hover: 1.1,
|
|
226
|
-
show_tooltip: false,
|
|
227
|
-
tooltip_offset: [0, 0.2, 0],
|
|
228
|
-
glow: false,
|
|
229
|
-
glow_intensity: 0.5,
|
|
230
|
-
},
|
|
231
|
-
onAttach(node, config, context) {
|
|
232
|
-
const state = {
|
|
233
|
-
isHovered: false,
|
|
234
|
-
hoveringHand: null,
|
|
235
|
-
originalScale: typeof node.properties.scale === 'number' ? node.properties.scale : 1,
|
|
236
|
-
originalColor: null,
|
|
237
|
-
};
|
|
238
|
-
node.__hoverState = state;
|
|
239
|
-
},
|
|
240
|
-
onDetach(node) {
|
|
241
|
-
delete node.__hoverState;
|
|
242
|
-
},
|
|
243
|
-
onEvent(node, config, context, event) {
|
|
244
|
-
const state = node.__hoverState;
|
|
245
|
-
if (event.type === 'hover_enter') {
|
|
246
|
-
state.isHovered = true;
|
|
247
|
-
state.hoveringHand = event.hand;
|
|
248
|
-
// Scale up
|
|
249
|
-
if (config.scale_on_hover && config.scale_on_hover !== 1) {
|
|
250
|
-
state.originalScale = typeof node.properties.scale === 'number' ? node.properties.scale : 1;
|
|
251
|
-
node.properties.scale = state.originalScale * config.scale_on_hover;
|
|
252
|
-
}
|
|
253
|
-
// Glow effect
|
|
254
|
-
if (config.glow) {
|
|
255
|
-
state.originalColor = node.properties.emissive || null;
|
|
256
|
-
node.properties.emissive = config.highlight_color;
|
|
257
|
-
node.properties.emissiveIntensity = config.glow_intensity;
|
|
258
|
-
}
|
|
259
|
-
// Tooltip
|
|
260
|
-
if (config.show_tooltip) {
|
|
261
|
-
const tooltipText = typeof config.show_tooltip === 'string'
|
|
262
|
-
? config.show_tooltip
|
|
263
|
-
: node.properties.tooltip || node.id || node.type;
|
|
264
|
-
context.emit('show_tooltip', {
|
|
265
|
-
node,
|
|
266
|
-
text: tooltipText,
|
|
267
|
-
offset: config.tooltip_offset,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
context.emit('hover_enter', { node, hand: event.hand });
|
|
271
|
-
}
|
|
272
|
-
if (event.type === 'hover_exit') {
|
|
273
|
-
state.isHovered = false;
|
|
274
|
-
state.hoveringHand = null;
|
|
275
|
-
// Restore scale
|
|
276
|
-
if (config.scale_on_hover && config.scale_on_hover !== 1) {
|
|
277
|
-
node.properties.scale = state.originalScale;
|
|
278
|
-
}
|
|
279
|
-
// Remove glow
|
|
280
|
-
if (config.glow) {
|
|
281
|
-
node.properties.emissive = state.originalColor;
|
|
282
|
-
delete node.properties.emissiveIntensity;
|
|
283
|
-
}
|
|
284
|
-
// Hide tooltip
|
|
285
|
-
if (config.show_tooltip) {
|
|
286
|
-
context.emit('hide_tooltip', { node });
|
|
287
|
-
}
|
|
288
|
-
context.emit('hover_exit', { node });
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
};
|
|
292
|
-
// =============================================================================
|
|
293
|
-
// SCALABLE TRAIT
|
|
294
|
-
// =============================================================================
|
|
295
|
-
const scalableHandler = {
|
|
296
|
-
name: 'scalable',
|
|
297
|
-
defaultConfig: {
|
|
298
|
-
min_scale: 0.1,
|
|
299
|
-
max_scale: 10,
|
|
300
|
-
uniform: true,
|
|
301
|
-
pivot: [0, 0, 0],
|
|
302
|
-
},
|
|
303
|
-
onAttach(node, config, context) {
|
|
304
|
-
const state = {
|
|
305
|
-
isScaling: false,
|
|
306
|
-
initialDistance: 0,
|
|
307
|
-
initialScale: 1,
|
|
308
|
-
};
|
|
309
|
-
node.__scaleState = state;
|
|
310
|
-
},
|
|
311
|
-
onDetach(node) {
|
|
312
|
-
delete node.__scaleState;
|
|
313
|
-
},
|
|
314
|
-
onUpdate(node, config, context, delta) {
|
|
315
|
-
const state = node.__scaleState;
|
|
316
|
-
if (!state?.isScaling)
|
|
317
|
-
return;
|
|
318
|
-
const { hands } = context.vr;
|
|
319
|
-
if (!hands.left || !hands.right)
|
|
320
|
-
return;
|
|
321
|
-
// Calculate current distance between hands
|
|
322
|
-
const currentDistance = Math.sqrt(Math.pow(hands.right.position[0] - hands.left.position[0], 2) +
|
|
323
|
-
Math.pow(hands.right.position[1] - hands.left.position[1], 2) +
|
|
324
|
-
Math.pow(hands.right.position[2] - hands.left.position[2], 2));
|
|
325
|
-
// Calculate scale factor
|
|
326
|
-
const scaleFactor = currentDistance / state.initialDistance;
|
|
327
|
-
let newScale = state.initialScale * scaleFactor;
|
|
328
|
-
// Clamp scale
|
|
329
|
-
newScale = Math.max(config.min_scale || 0.1, Math.min(config.max_scale || 10, newScale));
|
|
330
|
-
node.properties.scale = newScale;
|
|
331
|
-
context.emit('scale_update', { node, scale: newScale });
|
|
332
|
-
},
|
|
333
|
-
onEvent(node, config, context, event) {
|
|
334
|
-
const state = node.__scaleState;
|
|
335
|
-
if (event.type === 'scale_start') {
|
|
336
|
-
state.isScaling = true;
|
|
337
|
-
state.initialScale = typeof node.properties.scale === 'number' ? node.properties.scale : 1;
|
|
338
|
-
// Calculate initial distance between hands
|
|
339
|
-
const { left, right } = event.hands;
|
|
340
|
-
state.initialDistance = Math.sqrt(Math.pow(right.position[0] - left.position[0], 2) +
|
|
341
|
-
Math.pow(right.position[1] - left.position[1], 2) +
|
|
342
|
-
Math.pow(right.position[2] - left.position[2], 2));
|
|
343
|
-
context.emit('scale_start', { node });
|
|
344
|
-
}
|
|
345
|
-
if (event.type === 'scale_end') {
|
|
346
|
-
state.isScaling = false;
|
|
347
|
-
context.emit('scale_end', { node, finalScale: node.properties.scale });
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
};
|
|
351
|
-
// =============================================================================
|
|
352
|
-
// ROTATABLE TRAIT
|
|
353
|
-
// =============================================================================
|
|
354
|
-
const rotatableHandler = {
|
|
355
|
-
name: 'rotatable',
|
|
356
|
-
defaultConfig: {
|
|
357
|
-
axis: 'all',
|
|
358
|
-
snap_angles: [],
|
|
359
|
-
speed: 1,
|
|
360
|
-
},
|
|
361
|
-
onAttach(node, config, context) {
|
|
362
|
-
const state = {
|
|
363
|
-
isRotating: false,
|
|
364
|
-
initialHandRotation: [0, 0, 0],
|
|
365
|
-
initialObjectRotation: [0, 0, 0],
|
|
366
|
-
};
|
|
367
|
-
node.__rotateState = state;
|
|
368
|
-
},
|
|
369
|
-
onDetach(node) {
|
|
370
|
-
delete node.__rotateState;
|
|
371
|
-
},
|
|
372
|
-
onUpdate(node, config, context, delta) {
|
|
373
|
-
const state = node.__rotateState;
|
|
374
|
-
if (!state?.isRotating)
|
|
375
|
-
return;
|
|
376
|
-
const hand = context.vr.getDominantHand();
|
|
377
|
-
if (!hand)
|
|
378
|
-
return;
|
|
379
|
-
// Calculate rotation delta
|
|
380
|
-
const deltaRotation = [
|
|
381
|
-
(hand.rotation[0] - state.initialHandRotation[0]) * (config.speed || 1),
|
|
382
|
-
(hand.rotation[1] - state.initialHandRotation[1]) * (config.speed || 1),
|
|
383
|
-
(hand.rotation[2] - state.initialHandRotation[2]) * (config.speed || 1),
|
|
384
|
-
];
|
|
385
|
-
// Apply axis constraint
|
|
386
|
-
let newRotation;
|
|
387
|
-
switch (config.axis) {
|
|
388
|
-
case 'x':
|
|
389
|
-
newRotation = [
|
|
390
|
-
state.initialObjectRotation[0] + deltaRotation[0],
|
|
391
|
-
state.initialObjectRotation[1],
|
|
392
|
-
state.initialObjectRotation[2],
|
|
393
|
-
];
|
|
394
|
-
break;
|
|
395
|
-
case 'y':
|
|
396
|
-
newRotation = [
|
|
397
|
-
state.initialObjectRotation[0],
|
|
398
|
-
state.initialObjectRotation[1] + deltaRotation[1],
|
|
399
|
-
state.initialObjectRotation[2],
|
|
400
|
-
];
|
|
401
|
-
break;
|
|
402
|
-
case 'z':
|
|
403
|
-
newRotation = [
|
|
404
|
-
state.initialObjectRotation[0],
|
|
405
|
-
state.initialObjectRotation[1],
|
|
406
|
-
state.initialObjectRotation[2] + deltaRotation[2],
|
|
407
|
-
];
|
|
408
|
-
break;
|
|
409
|
-
default:
|
|
410
|
-
newRotation = [
|
|
411
|
-
state.initialObjectRotation[0] + deltaRotation[0],
|
|
412
|
-
state.initialObjectRotation[1] + deltaRotation[1],
|
|
413
|
-
state.initialObjectRotation[2] + deltaRotation[2],
|
|
414
|
-
];
|
|
415
|
-
}
|
|
416
|
-
// Snap to angles if configured
|
|
417
|
-
if (config.snap_angles && config.snap_angles.length > 0) {
|
|
418
|
-
newRotation = newRotation.map((angle) => {
|
|
419
|
-
let closest = config.snap_angles[0];
|
|
420
|
-
let minDiff = Math.abs(angle - closest);
|
|
421
|
-
for (const snapAngle of config.snap_angles) {
|
|
422
|
-
const diff = Math.abs(angle - snapAngle);
|
|
423
|
-
if (diff < minDiff) {
|
|
424
|
-
minDiff = diff;
|
|
425
|
-
closest = snapAngle;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
// Only snap if close enough
|
|
429
|
-
return minDiff < 10 ? closest : angle;
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
node.properties.rotation = newRotation;
|
|
433
|
-
context.emit('rotate_update', { node, rotation: newRotation });
|
|
434
|
-
},
|
|
435
|
-
onEvent(node, config, context, event) {
|
|
436
|
-
const state = node.__rotateState;
|
|
437
|
-
if (event.type === 'rotate_start') {
|
|
438
|
-
state.isRotating = true;
|
|
439
|
-
state.initialHandRotation = [...event.hand.rotation];
|
|
440
|
-
state.initialObjectRotation = node.properties.rotation || [0, 0, 0];
|
|
441
|
-
context.emit('rotate_start', { node });
|
|
442
|
-
}
|
|
443
|
-
if (event.type === 'rotate_end') {
|
|
444
|
-
state.isRotating = false;
|
|
445
|
-
context.emit('rotate_end', { node, finalRotation: node.properties.rotation });
|
|
446
|
-
}
|
|
447
|
-
},
|
|
448
|
-
};
|
|
449
|
-
// =============================================================================
|
|
450
|
-
// STACKABLE TRAIT
|
|
451
|
-
// =============================================================================
|
|
452
|
-
const stackableHandler = {
|
|
453
|
-
name: 'stackable',
|
|
454
|
-
defaultConfig: {
|
|
455
|
-
stack_axis: 'y',
|
|
456
|
-
stack_offset: 0,
|
|
457
|
-
max_stack: 10,
|
|
458
|
-
snap_distance: 0.5,
|
|
459
|
-
},
|
|
460
|
-
onAttach(node, config, context) {
|
|
461
|
-
const state = {
|
|
462
|
-
stackedItems: [],
|
|
463
|
-
stackParent: null,
|
|
464
|
-
};
|
|
465
|
-
node.__stackState = state;
|
|
466
|
-
},
|
|
467
|
-
onDetach(node) {
|
|
468
|
-
const state = node.__stackState;
|
|
469
|
-
// Remove from parent stack
|
|
470
|
-
if (state.stackParent) {
|
|
471
|
-
const parentState = state.stackParent.__stackState;
|
|
472
|
-
const index = parentState.stackedItems.indexOf(node);
|
|
473
|
-
if (index > -1) {
|
|
474
|
-
parentState.stackedItems.splice(index, 1);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
// Clear children
|
|
478
|
-
state.stackedItems = [];
|
|
479
|
-
delete node.__stackState;
|
|
480
|
-
},
|
|
481
|
-
onEvent(node, config, context, event) {
|
|
482
|
-
const state = node.__stackState;
|
|
483
|
-
if (event.type === 'collision' || event.type === 'trigger_enter') {
|
|
484
|
-
const other = event.type === 'collision' ? event.data.target : event.other;
|
|
485
|
-
// Check if other is stackable
|
|
486
|
-
if (!other.traits.has('stackable'))
|
|
487
|
-
return;
|
|
488
|
-
const otherState = other.__stackState;
|
|
489
|
-
if (!otherState)
|
|
490
|
-
return;
|
|
491
|
-
// Check stack limit
|
|
492
|
-
if (state.stackedItems.length >= (config.max_stack || 10))
|
|
493
|
-
return;
|
|
494
|
-
// Check if close enough
|
|
495
|
-
const nodePos = node.properties.position || [0, 0, 0];
|
|
496
|
-
const otherPos = other.properties.position || [0, 0, 0];
|
|
497
|
-
const axisIndex = config.stack_axis === 'x' ? 0 : config.stack_axis === 'z' ? 2 : 1;
|
|
498
|
-
const otherAxes = [0, 1, 2].filter((i) => i !== axisIndex);
|
|
499
|
-
// Check alignment on other axes
|
|
500
|
-
let aligned = true;
|
|
501
|
-
for (const axis of otherAxes) {
|
|
502
|
-
if (Math.abs(nodePos[axis] - otherPos[axis]) > (config.snap_distance || 0.5)) {
|
|
503
|
-
aligned = false;
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (aligned && otherPos[axisIndex] > nodePos[axisIndex]) {
|
|
508
|
-
// Other is above - add to stack
|
|
509
|
-
state.stackedItems.push(other);
|
|
510
|
-
otherState.stackParent = node;
|
|
511
|
-
// Snap position
|
|
512
|
-
const stackOffset = config.stack_offset || 0;
|
|
513
|
-
const newPos = [...nodePos];
|
|
514
|
-
newPos[axisIndex] = nodePos[axisIndex] + stackOffset;
|
|
515
|
-
other.properties.position = newPos;
|
|
516
|
-
context.emit('stack', { parent: node, child: other });
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
},
|
|
520
|
-
};
|
|
521
|
-
// =============================================================================
|
|
522
|
-
// SNAPPABLE TRAIT
|
|
523
|
-
// =============================================================================
|
|
524
|
-
const snappableHandler = {
|
|
525
|
-
name: 'snappable',
|
|
526
|
-
defaultConfig: {
|
|
527
|
-
snap_points: [],
|
|
528
|
-
snap_distance: 0.3,
|
|
529
|
-
snap_rotation: false,
|
|
530
|
-
magnetic: false,
|
|
531
|
-
},
|
|
532
|
-
onUpdate(node, config, context, delta) {
|
|
533
|
-
if (!config.snap_points || config.snap_points.length === 0)
|
|
534
|
-
return;
|
|
535
|
-
if (!config.magnetic)
|
|
536
|
-
return;
|
|
537
|
-
const nodePos = node.properties.position || [0, 0, 0];
|
|
538
|
-
// Find closest snap point
|
|
539
|
-
let closestPoint = null;
|
|
540
|
-
let closestDistance = config.snap_distance || 0.3;
|
|
541
|
-
for (const snapPoint of config.snap_points) {
|
|
542
|
-
const distance = Math.sqrt(Math.pow(nodePos[0] - snapPoint[0], 2) +
|
|
543
|
-
Math.pow(nodePos[1] - snapPoint[1], 2) +
|
|
544
|
-
Math.pow(nodePos[2] - snapPoint[2], 2));
|
|
545
|
-
if (distance < closestDistance) {
|
|
546
|
-
closestDistance = distance;
|
|
547
|
-
closestPoint = snapPoint;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
// Apply magnetic pull
|
|
551
|
-
if (closestPoint) {
|
|
552
|
-
const pullStrength = 0.1;
|
|
553
|
-
node.properties.position = [
|
|
554
|
-
nodePos[0] + (closestPoint[0] - nodePos[0]) * pullStrength,
|
|
555
|
-
nodePos[1] + (closestPoint[1] - nodePos[1]) * pullStrength,
|
|
556
|
-
nodePos[2] + (closestPoint[2] - nodePos[2]) * pullStrength,
|
|
557
|
-
];
|
|
558
|
-
}
|
|
559
|
-
},
|
|
560
|
-
onEvent(node, config, context, event) {
|
|
561
|
-
if (event.type !== 'grab_end')
|
|
562
|
-
return;
|
|
563
|
-
if (!config.snap_points || config.snap_points.length === 0)
|
|
564
|
-
return;
|
|
565
|
-
const nodePos = node.properties.position || [0, 0, 0];
|
|
566
|
-
// Find closest snap point
|
|
567
|
-
let closestPoint = null;
|
|
568
|
-
let closestDistance = config.snap_distance || 0.3;
|
|
569
|
-
for (const snapPoint of config.snap_points) {
|
|
570
|
-
const distance = Math.sqrt(Math.pow(nodePos[0] - snapPoint[0], 2) +
|
|
571
|
-
Math.pow(nodePos[1] - snapPoint[1], 2) +
|
|
572
|
-
Math.pow(nodePos[2] - snapPoint[2], 2));
|
|
573
|
-
if (distance < closestDistance) {
|
|
574
|
-
closestDistance = distance;
|
|
575
|
-
closestPoint = snapPoint;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
// Snap to closest point
|
|
579
|
-
if (closestPoint) {
|
|
580
|
-
node.properties.position = closestPoint;
|
|
581
|
-
context.emit('snap', { node, point: closestPoint });
|
|
582
|
-
// Haptic feedback
|
|
583
|
-
context.haptics.pulse(event.hand.id, 0.3);
|
|
584
|
-
}
|
|
585
|
-
},
|
|
586
|
-
};
|
|
587
|
-
// =============================================================================
|
|
588
|
-
// BREAKABLE TRAIT
|
|
589
|
-
// =============================================================================
|
|
590
|
-
const breakableHandler = {
|
|
591
|
-
name: 'breakable',
|
|
592
|
-
defaultConfig: {
|
|
593
|
-
break_velocity: 5,
|
|
594
|
-
fragments: 8,
|
|
595
|
-
fragment_mesh: undefined,
|
|
596
|
-
sound_on_break: undefined,
|
|
597
|
-
respawn: false,
|
|
598
|
-
respawn_delay: '5s',
|
|
599
|
-
},
|
|
600
|
-
onEvent(node, config, context, event) {
|
|
601
|
-
if (event.type !== 'collision')
|
|
602
|
-
return;
|
|
603
|
-
const collision = event.data;
|
|
604
|
-
const impactVelocity = Math.sqrt(Math.pow(collision.relativeVelocity[0], 2) +
|
|
605
|
-
Math.pow(collision.relativeVelocity[1], 2) +
|
|
606
|
-
Math.pow(collision.relativeVelocity[2], 2));
|
|
607
|
-
if (impactVelocity < (config.break_velocity || 5))
|
|
608
|
-
return;
|
|
609
|
-
// Play break sound
|
|
610
|
-
if (config.sound_on_break) {
|
|
611
|
-
context.audio.playSound(config.sound_on_break, {
|
|
612
|
-
position: collision.point,
|
|
613
|
-
spatial: true,
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
// Spawn fragments
|
|
617
|
-
const fragmentCount = config.fragments || 8;
|
|
618
|
-
for (let i = 0; i < fragmentCount; i++) {
|
|
619
|
-
const angle = (i / fragmentCount) * Math.PI * 2;
|
|
620
|
-
const velocity = [
|
|
621
|
-
Math.cos(angle) * 2,
|
|
622
|
-
Math.random() * 3,
|
|
623
|
-
Math.sin(angle) * 2,
|
|
624
|
-
];
|
|
625
|
-
context.emit('spawn_fragment', {
|
|
626
|
-
position: collision.point,
|
|
627
|
-
velocity,
|
|
628
|
-
mesh: config.fragment_mesh,
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
// Emit break event
|
|
632
|
-
context.emit('break', { node, impactVelocity, collision });
|
|
633
|
-
// Handle respawn
|
|
634
|
-
if (config.respawn) {
|
|
635
|
-
const delay = parseDuration(config.respawn_delay || '5s');
|
|
636
|
-
setTimeout(() => {
|
|
637
|
-
context.emit('respawn', { node });
|
|
638
|
-
}, delay);
|
|
639
|
-
}
|
|
640
|
-
// Mark for destruction
|
|
641
|
-
node.properties.__destroyed = true;
|
|
642
|
-
},
|
|
643
|
-
};
|
|
644
|
-
// =============================================================================
|
|
645
|
-
// UTILITIES
|
|
646
|
-
// =============================================================================
|
|
647
|
-
function parseDuration(duration) {
|
|
648
|
-
const match = duration.match(/^(\d+(?:\.\d+)?)(ms|s|m)$/);
|
|
649
|
-
if (!match)
|
|
650
|
-
return 0;
|
|
651
|
-
const value = parseFloat(match[1]);
|
|
652
|
-
const unit = match[2];
|
|
653
|
-
switch (unit) {
|
|
654
|
-
case 'ms':
|
|
655
|
-
return value;
|
|
656
|
-
case 's':
|
|
657
|
-
return value * 1000;
|
|
658
|
-
case 'm':
|
|
659
|
-
return value * 60 * 1000;
|
|
660
|
-
default:
|
|
661
|
-
return value;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
// =============================================================================
|
|
665
|
-
// TRAIT REGISTRY
|
|
666
|
-
// =============================================================================
|
|
667
|
-
export class VRTraitRegistry {
|
|
668
|
-
constructor() {
|
|
669
|
-
this.handlers = new Map();
|
|
670
|
-
// Register all built-in handlers
|
|
671
|
-
this.register(grabbableHandler);
|
|
672
|
-
this.register(throwableHandler);
|
|
673
|
-
this.register(pointableHandler);
|
|
674
|
-
this.register(hoverableHandler);
|
|
675
|
-
this.register(scalableHandler);
|
|
676
|
-
this.register(rotatableHandler);
|
|
677
|
-
this.register(stackableHandler);
|
|
678
|
-
this.register(snappableHandler);
|
|
679
|
-
this.register(breakableHandler);
|
|
680
|
-
}
|
|
681
|
-
register(handler) {
|
|
682
|
-
this.handlers.set(handler.name, handler);
|
|
683
|
-
}
|
|
684
|
-
getHandler(name) {
|
|
685
|
-
return this.handlers.get(name);
|
|
686
|
-
}
|
|
687
|
-
attachTrait(node, traitName, config, context) {
|
|
688
|
-
const handler = this.handlers.get(traitName);
|
|
689
|
-
if (!handler)
|
|
690
|
-
return;
|
|
691
|
-
const mergedConfig = { ...handler.defaultConfig, ...config };
|
|
692
|
-
node.traits.set(traitName, mergedConfig);
|
|
693
|
-
if (handler.onAttach) {
|
|
694
|
-
handler.onAttach(node, mergedConfig, context);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
detachTrait(node, traitName, context) {
|
|
698
|
-
const handler = this.handlers.get(traitName);
|
|
699
|
-
if (!handler)
|
|
700
|
-
return;
|
|
701
|
-
const config = node.traits.get(traitName);
|
|
702
|
-
if (config && handler.onDetach) {
|
|
703
|
-
handler.onDetach(node, config, context);
|
|
704
|
-
}
|
|
705
|
-
node.traits.delete(traitName);
|
|
706
|
-
}
|
|
707
|
-
updateTrait(node, traitName, context, delta) {
|
|
708
|
-
const handler = this.handlers.get(traitName);
|
|
709
|
-
if (!handler || !handler.onUpdate)
|
|
710
|
-
return;
|
|
711
|
-
const config = node.traits.get(traitName);
|
|
712
|
-
if (config) {
|
|
713
|
-
handler.onUpdate(node, config, context, delta);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
handleEvent(node, traitName, context, event) {
|
|
717
|
-
const handler = this.handlers.get(traitName);
|
|
718
|
-
if (!handler || !handler.onEvent)
|
|
719
|
-
return;
|
|
720
|
-
const config = node.traits.get(traitName);
|
|
721
|
-
if (config) {
|
|
722
|
-
handler.onEvent(node, config, context, event);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
updateAllTraits(node, context, delta) {
|
|
726
|
-
for (const traitName of node.traits.keys()) {
|
|
727
|
-
this.updateTrait(node, traitName, context, delta);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
handleEventForAllTraits(node, context, event) {
|
|
731
|
-
for (const traitName of node.traits.keys()) {
|
|
732
|
-
this.handleEvent(node, traitName, context, event);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
// =============================================================================
|
|
737
|
-
// EXPORTS
|
|
738
|
-
// =============================================================================
|
|
739
|
-
export const vrTraitRegistry = new VRTraitRegistry();
|
|
740
|
-
export { grabbableHandler, throwableHandler, pointableHandler, hoverableHandler, scalableHandler, rotatableHandler, stackableHandler, snappableHandler, breakableHandler, };
|