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 +2 -2
- package/build/addons/simulator/utils/PlaneExtractor.d.ts +3 -0
- package/build/addons/simulator/utils/PlaneExtractor.js +456 -0
- package/build/addons/ui/LongSelectHandler.d.ts +37 -0
- package/build/addons/ui/LongSelectHandler.js +155 -0
- package/build/constants.d.ts +1 -1
- package/build/core/components/ScriptsManager.d.ts +8 -1
- package/build/depth/Depth.d.ts +8 -5
- package/build/depth/DepthMesh.d.ts +5 -9
- package/build/depth/DepthTextures.d.ts +4 -4
- package/build/simulator/Simulator.d.ts +6 -1
- package/build/simulator/SimulatorOptions.d.ts +2 -1
- package/build/simulator/SimulatorWorld.d.ts +8 -0
- package/build/singletons.d.ts +4 -0
- package/build/ui/layouts/Orbiter.d.ts +16 -1
- package/build/world/World.d.ts +6 -4
- package/build/world/WorldOptions.d.ts +7 -0
- package/build/world/mesh/DetectedMesh.d.ts +12 -0
- package/build/world/mesh/MeshDetectionOptions.d.ts +10 -0
- package/build/world/mesh/MeshDetector.d.ts +24 -0
- package/build/world/planes/DetectedPlane.d.ts +7 -9
- package/build/world/planes/PlaneDetector.d.ts +4 -0
- package/build/world/planes/SimulatorPlane.d.ts +20 -0
- package/build/xrblocks.d.ts +1 -0
- package/build/xrblocks.js +4298 -4024
- package/build/xrblocks.js.map +1 -1
- package/build/xrblocks.min.js +1 -1
- package/build/xrblocks.min.js.map +1 -1
- package/package.json +4 -3
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.
|
|
78
|
-
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.
|
|
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,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 };
|
package/build/constants.d.ts
CHANGED
|
@@ -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@
|
|
52
|
+
export declare const XR_BLOCKS_ASSETS_PATH = "https://cdn.jsdelivr.net/gh/xrblocks/assets@a500427f2dfc12312df1a75860460244bab3a146/";
|