@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.
- 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,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
|
-
};
|