xrblocks 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,8 +74,8 @@ code below:
74
74
  <script type="importmap">
75
75
  {
76
76
  "imports": {
77
- "three": "https://cdn.jsdelivr.net/npm/three@0.181.0/build/three.module.js",
78
- "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.181.0/examples/jsm/",
77
+ "three": "https://cdn.jsdelivr.net/npm/three@0.182.0/build/three.module.js",
78
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.182.0/examples/jsm/",
79
79
  "xrblocks": "https://cdn.jsdelivr.net/gh/google/xrblocks@build/xrblocks.js",
80
80
  "xrblocks/addons/": "https://cdn.jsdelivr.net/gh/google/xrblocks@build/addons/"
81
81
  }
@@ -0,0 +1,3 @@
1
+ import * as THREE from 'three';
2
+ import { SimulatorPlane } from 'xrblocks';
3
+ export declare function findPlanesInScene(root: THREE.Object3D, minArea?: number): SimulatorPlane[];
@@ -0,0 +1,456 @@
1
+ import * as THREE from 'three';
2
+
3
+ // ------------------------------------------------------------------
4
+ // Main Function
5
+ // ------------------------------------------------------------------
6
+ function findPlanesInScene(root, minArea = 0.1) {
7
+ const planesMap = new Map();
8
+ const upVector = new THREE.Vector3(0, 1, 0);
9
+ // Temps
10
+ const _vA = new THREE.Vector3();
11
+ const _vB = new THREE.Vector3();
12
+ const _vC = new THREE.Vector3();
13
+ const _edge1 = new THREE.Vector3();
14
+ const _edge2 = new THREE.Vector3();
15
+ const _normal = new THREE.Vector3();
16
+ root.updateMatrixWorld(true);
17
+ // 1. Clustering Phase
18
+ root.traverse((obj) => {
19
+ if (!obj.isMesh)
20
+ return;
21
+ const mesh = obj;
22
+ const geometry = mesh.geometry;
23
+ const posAttr = geometry.attributes.position;
24
+ const indexAttr = geometry.index;
25
+ if (!posAttr)
26
+ return;
27
+ const getVertexWorld = (idx, target) => {
28
+ target.fromBufferAttribute(posAttr, idx);
29
+ target.applyMatrix4(mesh.matrixWorld);
30
+ };
31
+ const count = indexAttr ? indexAttr.count / 3 : posAttr.count / 3;
32
+ for (let i = 0; i < count; i++) {
33
+ let a, b, c;
34
+ if (indexAttr) {
35
+ a = indexAttr.getX(i * 3);
36
+ b = indexAttr.getX(i * 3 + 1);
37
+ c = indexAttr.getX(i * 3 + 2);
38
+ }
39
+ else {
40
+ a = i * 3;
41
+ b = i * 3 + 1;
42
+ c = i * 3 + 2;
43
+ }
44
+ getVertexWorld(a, _vA);
45
+ getVertexWorld(b, _vB);
46
+ getVertexWorld(c, _vC);
47
+ // Compute Normal & Area
48
+ _edge1.subVectors(_vB, _vA);
49
+ _edge2.subVectors(_vC, _vA);
50
+ _normal.crossVectors(_edge1, _edge2);
51
+ const combinedLen = _normal.length();
52
+ const area = combinedLen * 0.5;
53
+ if (area < 1e-6)
54
+ continue;
55
+ _normal.divideScalar(combinedLen);
56
+ // Classify
57
+ const absDot = Math.abs(_normal.dot(upVector));
58
+ let type = null;
59
+ if (absDot >= 0.9)
60
+ type = 'horizontal';
61
+ else if (absDot <= 0.1)
62
+ type = 'vertical';
63
+ if (!type)
64
+ continue;
65
+ // Plane Constant D
66
+ const d = -_normal.dot(_vA);
67
+ // Hash Key
68
+ const precision = 2;
69
+ const nx = (Math.round(_normal.x * 100) / 100).toFixed(precision);
70
+ const ny = (Math.round(_normal.y * 100) / 100).toFixed(precision);
71
+ const nz = (Math.round(_normal.z * 100) / 100).toFixed(precision);
72
+ const dist = d.toFixed(precision);
73
+ const key = `${type}_${nx}_${ny}_${nz}_${dist}`;
74
+ let acc = planesMap.get(key);
75
+ if (!acc) {
76
+ acc = {
77
+ type,
78
+ normal: _normal.clone(),
79
+ constant: d,
80
+ totalArea: 0,
81
+ vertices: [],
82
+ };
83
+ planesMap.set(key, acc);
84
+ }
85
+ acc.totalArea += area;
86
+ // We store all vertices to calculate the exact bounds later
87
+ acc.vertices.push(_vA.clone(), _vB.clone(), _vC.clone());
88
+ }
89
+ });
90
+ // 2. Geometry Projection & Clustering Phase
91
+ const results = [];
92
+ for (const [_, data] of planesMap) {
93
+ if (data.totalArea < minArea)
94
+ continue;
95
+ // A. Define Local Coordinate System
96
+ const quaternion = new THREE.Quaternion().setFromUnitVectors(upVector, data.normal);
97
+ const inverseRotation = quaternion.clone().invert();
98
+ // B. Project all vertices to Local 2D Space
99
+ // We'll store them as {x, y, originalIndex}
100
+ // Note: Local Y is constant (plane constant), we care about X and Z (mapped to x,y for 2D clustering)
101
+ const localPoints = [];
102
+ // Global center sum unused now as we calculate per cluster
103
+ // We need a reference point for projection to avoid large number precision issues?
104
+ // Actually just projecting relative to the first vertex is fine for local coords.
105
+ const origin = data.vertices[0].clone();
106
+ for (const v of data.vertices) {
107
+ const tempVec = new THREE.Vector3().subVectors(v, origin);
108
+ tempVec.applyQuaternion(inverseRotation);
109
+ // In local space: x=x, y=0 (approx), z=z.
110
+ // We map 3D (x,y,z) -> 2D (x, z)
111
+ localPoints.push({ x: tempVec.x, y: tempVec.z, vec: v });
112
+ }
113
+ // C. Cluster Triangles
114
+ // We treat each triangle as an atomic unit.
115
+ // We merge triangles if they share vertices (or have vertices very close to each other).
116
+ const clusteringThreshold = 0.2; // 20cm gap allowed for continuity
117
+ const thresholdSq = clusteringThreshold * clusteringThreshold;
118
+ // We'll using a simple spatial acceleration for vertex matching
119
+ // 1. Identify unique spatial vertices (within threshold) using a Grid
120
+ // 2. Build connectivity graph of triangles
121
+ const numTriangles = Math.floor(localPoints.length / 3);
122
+ const parent = new Int32Array(numTriangles);
123
+ for (let i = 0; i < numTriangles; i++)
124
+ parent[i] = i;
125
+ const find = (i) => {
126
+ if (parent[i] === i)
127
+ return i;
128
+ parent[i] = find(parent[i]);
129
+ return parent[i];
130
+ };
131
+ const union = (i, j) => {
132
+ const rootI = find(i);
133
+ const rootJ = find(j);
134
+ if (rootI !== rootJ)
135
+ parent[rootI] = rootJ;
136
+ };
137
+ // Grid for vertices: key -> list of {triangleIndex, vertexIndexInTriangle} (actually just triangleIndex is enough)
138
+ // We only need to know "which triangles possess a vertex in this cell"
139
+ const cellSize = clusteringThreshold;
140
+ const grid = new Map(); // key -> triangle indices
141
+ const getKey = (x, y) => {
142
+ const kx = Math.floor(x / cellSize);
143
+ const ky = Math.floor(y / cellSize);
144
+ return `${kx},${ky}`;
145
+ };
146
+ // Pre-populate grid
147
+ for (let t = 0; t < numTriangles; t++) {
148
+ // For each vertex in triangle
149
+ for (let k = 0; k < 3; k++) {
150
+ const idx = t * 3 + k;
151
+ const p = localPoints[idx];
152
+ const key = getKey(p.x, p.y);
153
+ if (!grid.has(key))
154
+ grid.set(key, []);
155
+ grid.get(key).push(t);
156
+ }
157
+ }
158
+ // Connect triangles
159
+ // For each triangle, look at its vertices. Check neighbors in grid.
160
+ // If a neighbor vertex is close, union the two triangles.
161
+ const neighborOffsets = [
162
+ [0, 0],
163
+ [1, 0],
164
+ [-1, 0],
165
+ [0, 1],
166
+ [0, -1],
167
+ [1, 1],
168
+ [1, -1],
169
+ [-1, 1],
170
+ [-1, -1],
171
+ ];
172
+ for (let t = 0; t < numTriangles; t++) {
173
+ for (let k = 0; k < 3; k++) {
174
+ const idx = t * 3 + k;
175
+ const p = localPoints[idx];
176
+ const kx = Math.floor(p.x / cellSize);
177
+ const ky = Math.floor(p.y / cellSize);
178
+ for (const offset of neighborOffsets) {
179
+ const nx = kx + offset[0];
180
+ const ny = ky + offset[1];
181
+ const nKey = `${nx},${ny}`;
182
+ const neighbors = grid.get(nKey);
183
+ if (!neighbors)
184
+ continue;
185
+ for (const otherT of neighbors) {
186
+ if (otherT === t)
187
+ continue; // Same triangle
188
+ // We found a triangle 'otherT' that has a vertex in this cell (or neighbor cell)
189
+ // We need to verify distance to 'p'
190
+ // Check all 3 vertices of otherT?
191
+ // Optimization: We could store exact vertex index in grid to compare point-to-point.
192
+ // But iterating 3 vertices of otherT is cheap.
193
+ if (find(t) === find(otherT))
194
+ continue; // Already merged
195
+ let connected = false;
196
+ for (let j = 0; j < 3; j++) {
197
+ const otherP = localPoints[otherT * 3 + j];
198
+ const dx = p.x - otherP.x;
199
+ const dy = p.y - otherP.y;
200
+ if (dx * dx + dy * dy <= thresholdSq) {
201
+ connected = true;
202
+ break;
203
+ }
204
+ }
205
+ if (connected) {
206
+ union(t, otherT);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ // Group by root
213
+ const clustersMap = new Map(); // root -> triangle indices
214
+ for (let t = 0; t < numTriangles; t++) {
215
+ const root = find(t);
216
+ if (!clustersMap.has(root))
217
+ clustersMap.set(root, []);
218
+ clustersMap.get(root).push(t);
219
+ }
220
+ const clusters = [];
221
+ for (const bin of clustersMap.values()) {
222
+ const clusterPoints = [];
223
+ for (const tIdx of bin) {
224
+ clusterPoints.push(localPoints[tIdx * 3], localPoints[tIdx * 3 + 1], localPoints[tIdx * 3 + 2]);
225
+ }
226
+ clusters.push(clusterPoints);
227
+ }
228
+ // D. Process Clusters
229
+ for (const cluster of clusters) {
230
+ if (cluster.length < 3)
231
+ continue; // Need at least a triangle
232
+ // Calculate Cluster Center for local projection (to avoid large coordinates)
233
+ const clusterCenterSum = new THREE.Vector3();
234
+ for (const p of cluster) {
235
+ clusterCenterSum.add(p.vec);
236
+ }
237
+ const clusterCenter = clusterCenterSum
238
+ .clone()
239
+ .divideScalar(cluster.length);
240
+ // Project all points to local 2D space relative to clusterCenter
241
+ // We need 2D points for boundary tracing
242
+ const clusterPoints2D = [];
243
+ for (let i = 0; i < cluster.length; i++) {
244
+ // Re-project relative to clusterCenter
245
+ const v = cluster[i].vec;
246
+ const diff = new THREE.Vector3().subVectors(v, clusterCenter);
247
+ diff.applyQuaternion(inverseRotation);
248
+ clusterPoints2D.push(new THREE.Vector2(diff.x, diff.z));
249
+ }
250
+ // Trace Boundary
251
+ // 1. Quantize vertices to identifying shared edges
252
+ // We assume index i in clusterPoints2D corresponds to vertex i in cluster
253
+ // The cluster array is flat version of triangles (every 3 points = 1 triangle)
254
+ const numTri = Math.floor(cluster.length / 3);
255
+ const edges = new Map(); // edgeKey -> count
256
+ // We need robust vertex matching.
257
+ // Let's use a quantized key map.
258
+ const quantization = 1000; // 1mm precision
259
+ const getId = (p) => {
260
+ const ix = Math.round(p.x * quantization);
261
+ const iy = Math.round(p.y * quantization);
262
+ return `${ix},${iy}`;
263
+ };
264
+ // Map original index -> unique quantized ID
265
+ // Determine unique IDs
266
+ const uniqueIds = [];
267
+ const indexToUniqueId = [];
268
+ const uniqueIdToIndexMap = new Map(); // uniqueIdString -> uniqueIndex (0..N)
269
+ for (let i = 0; i < clusterPoints2D.length; i++) {
270
+ const key = getId(clusterPoints2D[i]);
271
+ let uid = uniqueIdToIndexMap.get(key);
272
+ if (uid === undefined) {
273
+ uid = uniqueIds.length;
274
+ uniqueIds.push(key);
275
+ uniqueIdToIndexMap.set(key, uid);
276
+ }
277
+ indexToUniqueId.push(uid);
278
+ }
279
+ // Count edges
280
+ for (let t = 0; t < numTri; t++) {
281
+ const i0 = indexToUniqueId[t * 3];
282
+ const i1 = indexToUniqueId[t * 3 + 1];
283
+ const i2 = indexToUniqueId[t * 3 + 2];
284
+ // Skip degenerate triangles
285
+ if (i0 === i1 || i1 === i2 || i2 === i0)
286
+ continue;
287
+ const addEdge = (u, v) => {
288
+ const k = u < v ? `${u}:${v}` : `${v}:${u}`;
289
+ edges.set(k, (edges.get(k) || 0) + 1);
290
+ };
291
+ addEdge(i0, i1);
292
+ addEdge(i1, i2);
293
+ addEdge(i2, i0);
294
+ }
295
+ // Find boundary edges (count === 1)
296
+ const boundaryEdges = [];
297
+ for (const [k, count] of edges) {
298
+ if (count === 1) {
299
+ const [u, v] = k.split(':').map(Number);
300
+ boundaryEdges.push({ u, v });
301
+ }
302
+ }
303
+ if (boundaryEdges.length < 3)
304
+ continue;
305
+ // Build adjacency graph for boundary
306
+ const adj = new Map();
307
+ for (const e of boundaryEdges) {
308
+ if (!adj.has(e.u))
309
+ adj.set(e.u, []);
310
+ if (!adj.has(e.v))
311
+ adj.set(e.v, []);
312
+ adj.get(e.u).push(e.v);
313
+ adj.get(e.v).push(e.u);
314
+ }
315
+ // Trace loops
316
+ // We pick an arbitrary start node
317
+ const visitedEdges = new Set();
318
+ const loops = [];
319
+ // This greedy approach might find multiple loops if there are holes or disjoint islands
320
+ // (though we clustered by connectivity, holes are possible)
321
+ for (const startNode of adj.keys()) {
322
+ if (adj.get(startNode).length === 0)
323
+ continue; // Should not happen
324
+ // Try to find a loop starting here
325
+ // We need to consume edges to avoid re-visiting
326
+ // DFS/Walk
327
+ const path = [startNode];
328
+ let curr = startNode;
329
+ let foundLoop = false;
330
+ // We only want to start if we haven't visited incident edges?
331
+ // Actually, let's just pick an unvisited edge from startNode
332
+ let next = -1;
333
+ const neighbors = adj.get(curr);
334
+ for (const n of neighbors) {
335
+ const k = curr < n ? `${curr}:${n}` : `${n}:${curr}`;
336
+ if (!visitedEdges.has(k)) {
337
+ next = n;
338
+ visitedEdges.add(k);
339
+ break;
340
+ }
341
+ }
342
+ if (next === -1)
343
+ continue; // All edges visited
344
+ curr = next;
345
+ path.push(curr);
346
+ while (true) {
347
+ if (curr === startNode) {
348
+ foundLoop = true;
349
+ break;
350
+ }
351
+ // Find next unvisited edge
352
+ const nbors = adj.get(curr);
353
+ let nextNode = -1;
354
+ for (const n of nbors) {
355
+ // Don't go back immediately unless it's the only way (isolated line) - but count=1 means boundary loop, so every node degree should be 2 ideally for simple loop.
356
+ // If degree > 2, it's touching boundaries (hourglass).
357
+ const k = curr < n ? `${curr}:${n}` : `${n}:${curr}`;
358
+ // We must not reuse edges
359
+ if (visitedEdges.has(k))
360
+ continue;
361
+ nextNode = n;
362
+ visitedEdges.add(k);
363
+ break;
364
+ }
365
+ if (nextNode === -1) {
366
+ // Dead end? In a proper mesh boundary, this shouldn't happen unless open geometry.
367
+ break;
368
+ }
369
+ curr = nextNode;
370
+ path.push(curr);
371
+ }
372
+ if (foundLoop) {
373
+ loops.push(path.slice(0, path.length - 1)); // Remove closing duplicate
374
+ }
375
+ }
376
+ // Pick largest loop by bbox or length?
377
+ // Let's pick the one with most vertices for now
378
+ let bestLoop = null;
379
+ let maxLen = -1;
380
+ for (const loop of loops) {
381
+ if (loop.length > maxLen) {
382
+ maxLen = loop.length;
383
+ bestLoop = loop;
384
+ }
385
+ }
386
+ if (!bestLoop || bestLoop.length < 3)
387
+ continue;
388
+ // Reconstruct Polygon
389
+ // The loop contains Unique IDs. We need coordinates.
390
+ // We can recover coordinates from uniqueIdString or by finding one vertex with that ID.
391
+ // Let's decode uniqueIdString
392
+ const finalPolygon = [];
393
+ for (const uid of bestLoop) {
394
+ const key = uniqueIds[uid];
395
+ const [ix, iy] = key.split(',').map(Number);
396
+ finalPolygon.push(new THREE.Vector2(ix / quantization, iy / quantization));
397
+ }
398
+ // Simplify Polygon (Collinear)
399
+ const simplified = [];
400
+ if (finalPolygon.length > 0) {
401
+ simplified.push(finalPolygon[0]);
402
+ for (let i = 1; i < finalPolygon.length; i++) {
403
+ const prev = simplified[simplified.length - 1];
404
+ const curr = finalPolygon[i];
405
+ const next = finalPolygon[(i + 1) % finalPolygon.length];
406
+ // Check if curr is collinear with prev and next
407
+ const v1 = new THREE.Vector2().subVectors(curr, prev).normalize();
408
+ const v2 = new THREE.Vector2().subVectors(next, curr).normalize();
409
+ // Dot product ~ 1 or -1 means collinear
410
+ // We care about direction preservation, so dot ~ 1 (same direction) means we can skip curr.
411
+ // Wait, if v1 and v2 are same dir, we can skip curr.
412
+ if (v1.dot(v2) > 0.999) {
413
+ // Skip curr
414
+ continue;
415
+ }
416
+ simplified.push(curr);
417
+ }
418
+ // Check last closing alignment
419
+ if (simplified.length > 2) {
420
+ const first = simplified[0];
421
+ const last = simplified[simplified.length - 1];
422
+ const secondLast = simplified[simplified.length - 2];
423
+ const v1 = new THREE.Vector2()
424
+ .subVectors(last, secondLast)
425
+ .normalize();
426
+ const v2 = new THREE.Vector2().subVectors(first, last).normalize();
427
+ if (v1.dot(v2) > 0.999) {
428
+ simplified.pop(); // Remove last if it's collinear with wrap-around
429
+ }
430
+ }
431
+ }
432
+ if (simplified.length < 3)
433
+ continue;
434
+ // Calculate Area from Polygon
435
+ let polyArea = 0;
436
+ for (let i = 0; i < simplified.length; i++) {
437
+ const j = (i + 1) % simplified.length;
438
+ polyArea +=
439
+ simplified[i].x * simplified[j].y - simplified[j].x * simplified[i].y;
440
+ }
441
+ polyArea = Math.abs(polyArea / 2);
442
+ if (polyArea < minArea)
443
+ continue;
444
+ results.push({
445
+ type: data.type,
446
+ area: polyArea,
447
+ position: clusterCenter,
448
+ quaternion: quaternion,
449
+ polygon: simplified,
450
+ });
451
+ }
452
+ }
453
+ return results;
454
+ }
455
+
456
+ export { findPlanesInScene };
@@ -0,0 +1,37 @@
1
+ import * as xb from 'xrblocks';
2
+ export declare class LongSelectHandler extends xb.Script {
3
+ protected onTrigger: () => void;
4
+ protected triggerDelay: number;
5
+ protected triggerCooldownDuration: number;
6
+ protected pulseAnimationDuration: number;
7
+ protected visualizerColor: number;
8
+ protected visualizerRadius: number;
9
+ private triggerTimeout;
10
+ private lastTriggerTime;
11
+ private isTriggerOnCooldown;
12
+ private activeHandedness;
13
+ private triggerStartTime;
14
+ private isPulsing;
15
+ private pulseStartTime;
16
+ private outerVisualizer;
17
+ private innerVisualizer;
18
+ private outerMaterialOpacity;
19
+ private innerMaterialOpacity;
20
+ private sphereGeometry;
21
+ private outerMaterial;
22
+ private innerMaterial;
23
+ constructor(onTrigger: () => void, { triggerDelay, triggerCooldownDuration, pulseAnimationDuration, visualizerColor, visualizerRadius, }?: {
24
+ triggerDelay?: number | undefined;
25
+ triggerCooldownDuration?: number | undefined;
26
+ pulseAnimationDuration?: number | undefined;
27
+ visualizerColor?: number | undefined;
28
+ visualizerRadius?: number | undefined;
29
+ });
30
+ onSelectStart(event: xb.SelectEvent): void;
31
+ onSelecting(): void;
32
+ onSelectEnd(): void;
33
+ private _triggerSelection;
34
+ private createVisualizers;
35
+ updateVisualizers(): void;
36
+ removeVisualizers(): void;
37
+ }
@@ -0,0 +1,155 @@
1
+ import * as THREE from 'three';
2
+ import * as xb from 'xrblocks';
3
+
4
+ function easeInQuad(x) {
5
+ return x * x;
6
+ }
7
+ function easeOutQuint(x) {
8
+ return 1 - Math.pow(1 - x, 5);
9
+ }
10
+ class LongSelectHandler extends xb.Script {
11
+ constructor(onTrigger, { triggerDelay = 1000, triggerCooldownDuration = 5000, pulseAnimationDuration = 400, visualizerColor = 0x4970ff, visualizerRadius = 0.028, } = {}) {
12
+ super();
13
+ this.onTrigger = onTrigger;
14
+ this.triggerTimeout = null;
15
+ this.lastTriggerTime = 0;
16
+ this.isTriggerOnCooldown = false;
17
+ this.activeHandedness = null;
18
+ this.triggerStartTime = 0;
19
+ this.isPulsing = false;
20
+ this.pulseStartTime = 0;
21
+ this.outerVisualizer = null;
22
+ this.innerVisualizer = null;
23
+ this.outerMaterialOpacity = 0.3;
24
+ this.innerMaterialOpacity = 0.6;
25
+ this.triggerDelay = triggerDelay;
26
+ this.triggerCooldownDuration = triggerCooldownDuration;
27
+ this.pulseAnimationDuration = pulseAnimationDuration;
28
+ this.visualizerColor = visualizerColor;
29
+ this.visualizerRadius = visualizerRadius;
30
+ this.sphereGeometry = new THREE.SphereGeometry(this.visualizerRadius, 32, 32);
31
+ this.outerMaterial = new THREE.MeshBasicMaterial({
32
+ color: this.visualizerColor,
33
+ transparent: true,
34
+ opacity: this.outerMaterialOpacity,
35
+ depthWrite: false,
36
+ });
37
+ this.innerMaterial = new THREE.MeshBasicMaterial({
38
+ color: this.visualizerColor,
39
+ transparent: true,
40
+ opacity: this.innerMaterialOpacity,
41
+ depthWrite: false,
42
+ });
43
+ }
44
+ onSelectStart(event) {
45
+ const inputSource = event.target.inputSource;
46
+ if (inputSource && inputSource.handedness) {
47
+ this.activeHandedness = inputSource.handedness;
48
+ }
49
+ else {
50
+ console.warn('Could not determine handedness from onSelectStart event.');
51
+ this.activeHandedness = null;
52
+ }
53
+ }
54
+ onSelecting() {
55
+ if (this.isPulsing) {
56
+ this.updateVisualizers();
57
+ return;
58
+ }
59
+ if (this.triggerTimeout === null) {
60
+ if (this.isTriggerOnCooldown)
61
+ return;
62
+ if (!this.activeHandedness || !xb.core.input || !xb.core.input.hands)
63
+ return;
64
+ const handIndex = this.activeHandedness === 'right' ? 1 : 0;
65
+ const hand = xb.core.input.hands[handIndex];
66
+ if (hand && hand.joints && hand.joints['index-finger-tip']) {
67
+ const indexTip = hand.joints['index-finger-tip'];
68
+ this.createVisualizers(indexTip);
69
+ this.triggerStartTime = Date.now();
70
+ this.triggerTimeout = setTimeout(() => {
71
+ this._triggerSelection();
72
+ }, this.triggerDelay);
73
+ }
74
+ }
75
+ this.updateVisualizers();
76
+ }
77
+ onSelectEnd() {
78
+ this.removeVisualizers();
79
+ if (this.triggerTimeout) {
80
+ clearTimeout(this.triggerTimeout);
81
+ this.triggerTimeout = null;
82
+ }
83
+ this.activeHandedness = null;
84
+ }
85
+ _triggerSelection() {
86
+ const currentTime = Date.now();
87
+ if (currentTime - this.lastTriggerTime < this.triggerCooldownDuration) {
88
+ return;
89
+ }
90
+ this.lastTriggerTime = currentTime;
91
+ this.isTriggerOnCooldown = true;
92
+ setTimeout(() => {
93
+ this.isTriggerOnCooldown = false;
94
+ }, this.triggerCooldownDuration);
95
+ setTimeout(() => {
96
+ if (!this.outerVisualizer)
97
+ return;
98
+ xb.core.sound.soundSynthesizer.playPresetTone('ACTIVATE');
99
+ this.isPulsing = true;
100
+ this.pulseStartTime = Date.now();
101
+ setTimeout(() => {
102
+ this.removeVisualizers();
103
+ }, this.pulseAnimationDuration);
104
+ }, 75);
105
+ if (this.onTrigger) {
106
+ this.onTrigger();
107
+ }
108
+ }
109
+ createVisualizers(parent) {
110
+ this.removeVisualizers();
111
+ this.outerMaterial.opacity = this.outerMaterialOpacity;
112
+ this.innerMaterial.opacity = this.innerMaterialOpacity;
113
+ this.outerVisualizer = new THREE.Mesh(this.sphereGeometry, this.outerMaterial);
114
+ parent.add(this.outerVisualizer);
115
+ this.innerVisualizer = new THREE.Mesh(this.sphereGeometry, this.innerMaterial);
116
+ this.innerVisualizer.scale.setScalar(0.01);
117
+ parent.add(this.innerVisualizer);
118
+ }
119
+ updateVisualizers() {
120
+ if (!this.innerVisualizer || this.triggerStartTime <= 0)
121
+ return;
122
+ const currentTime = Date.now();
123
+ if (this.isPulsing) {
124
+ const pulseElapsed = currentTime - this.pulseStartTime;
125
+ const pulseProgress = Math.min(pulseElapsed / this.pulseAnimationDuration, 1.0);
126
+ const scaleProgress = easeOutQuint(pulseProgress);
127
+ const pulseScale = 1.0 + scaleProgress * 0.2;
128
+ this.innerVisualizer.scale.setScalar(pulseScale);
129
+ const fadeProgress = pulseProgress;
130
+ this.innerMaterial.opacity = 0.5 * (1 - fadeProgress);
131
+ this.outerMaterial.opacity = 0.2 * (1 - fadeProgress);
132
+ }
133
+ else {
134
+ const elapsed = currentTime - this.triggerStartTime;
135
+ const progress = Math.min(elapsed / this.triggerDelay, 1.0);
136
+ const easedProgress = easeInQuad(progress);
137
+ this.innerVisualizer.scale.setScalar(easedProgress);
138
+ }
139
+ }
140
+ removeVisualizers() {
141
+ if (this.outerVisualizer) {
142
+ this.outerVisualizer.removeFromParent();
143
+ this.outerVisualizer = null;
144
+ }
145
+ if (this.innerVisualizer) {
146
+ this.innerVisualizer.removeFromParent();
147
+ this.innerVisualizer = null;
148
+ }
149
+ this.triggerStartTime = 0;
150
+ this.isPulsing = false;
151
+ this.pulseStartTime = 0;
152
+ }
153
+ }
154
+
155
+ export { LongSelectHandler };
@@ -49,4 +49,4 @@ export declare const DEFAULT_DEVICE_CAMERA_WIDTH = 1280;
49
49
  * Corresponds to a 720p resolution.
50
50
  */
51
51
  export declare const DEFAULT_DEVICE_CAMERA_HEIGHT = 720;
52
- export declare const XR_BLOCKS_ASSETS_PATH = "https://cdn.jsdelivr.net/gh/xrblocks/assets@34228db7ec7cef66fd65ef3250ef6f4a930fe373/";
52
+ export declare const XR_BLOCKS_ASSETS_PATH = "https://cdn.jsdelivr.net/gh/xrblocks/assets@a500427f2dfc12312df1a75860460244bab3a146/";