@ifc-lite/renderer 1.15.1 → 1.15.3
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/dist/bvh.d.ts +1 -1
- package/dist/bvh.d.ts.map +1 -1
- package/dist/federation-registry.d.ts.map +1 -1
- package/dist/federation-registry.js +0 -1
- package/dist/federation-registry.js.map +1 -1
- package/dist/pipeline.js.map +1 -1
- package/dist/scene-geometry.d.ts +31 -0
- package/dist/scene-geometry.d.ts.map +1 -0
- package/dist/scene-geometry.js +137 -0
- package/dist/scene-geometry.js.map +1 -0
- package/dist/scene-raycaster.d.ts +65 -0
- package/dist/scene-raycaster.d.ts.map +1 -0
- package/dist/scene-raycaster.js +234 -0
- package/dist/scene-raycaster.js.map +1 -0
- package/dist/scene.d.ts +9 -39
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +25 -343
- package/dist/scene.js.map +1 -1
- package/dist/shaders/main.wgsl.d.ts +1 -1
- package/dist/shaders/main.wgsl.d.ts.map +1 -1
- package/dist/shaders/main.wgsl.js +4 -2
- package/dist/shaders/main.wgsl.js.map +1 -1
- package/dist/snap-detector.d.ts +1 -17
- package/dist/snap-detector.d.ts.map +1 -1
- package/dist/snap-detector.js +16 -213
- package/dist/snap-detector.js.map +1 -1
- package/dist/snap-geometry-cache.d.ts +24 -0
- package/dist/snap-geometry-cache.d.ts.map +1 -0
- package/dist/snap-geometry-cache.js +155 -0
- package/dist/snap-geometry-cache.js.map +1 -0
- package/dist/snap-geometry-utils.d.ts +27 -0
- package/dist/snap-geometry-utils.d.ts.map +1 -0
- package/dist/snap-geometry-utils.js +56 -0
- package/dist/snap-geometry-utils.js.map +1 -0
- package/package.json +3 -3
package/dist/scene.d.ts
CHANGED
|
@@ -4,10 +4,7 @@
|
|
|
4
4
|
import type { Mesh, InstancedMesh, BatchedMesh, Vec3 } from './types.js';
|
|
5
5
|
import type { MeshData } from '@ifc-lite/geometry';
|
|
6
6
|
import type { RenderPipeline } from './pipeline.js';
|
|
7
|
-
|
|
8
|
-
min: Vec3;
|
|
9
|
-
max: Vec3;
|
|
10
|
-
}
|
|
7
|
+
import { type BoundingBox, type RaycastHit } from './scene-raycaster.js';
|
|
11
8
|
export declare class Scene {
|
|
12
9
|
private meshes;
|
|
13
10
|
private instancedMeshes;
|
|
@@ -196,9 +193,8 @@ export declare class Scene {
|
|
|
196
193
|
*/
|
|
197
194
|
private createBatchedMesh;
|
|
198
195
|
/**
|
|
199
|
-
* Merge multiple mesh geometries into single vertex/index buffers
|
|
200
|
-
*
|
|
201
|
-
* OPTIMIZATION: Uses efficient loops and bulk index adjustment
|
|
196
|
+
* Merge multiple mesh geometries into single vertex/index buffers.
|
|
197
|
+
* Delegates to the extracted mergeGeometry() utility.
|
|
202
198
|
*/
|
|
203
199
|
private mergeGeometry;
|
|
204
200
|
/**
|
|
@@ -206,12 +202,8 @@ export declare class Scene {
|
|
|
206
202
|
*/
|
|
207
203
|
private getMaxBufferSize;
|
|
208
204
|
/**
|
|
209
|
-
* Split a meshDataArray into chunks
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* Each mesh is kept intact — we never split a single element's geometry.
|
|
213
|
-
* If a single mesh exceeds the limit on its own it is placed in a solo chunk
|
|
214
|
-
* (WebGPU will clamp or error, but we don't silently drop geometry).
|
|
205
|
+
* Split a meshDataArray into chunks that fit within GPU buffer limits.
|
|
206
|
+
* Delegates to the extracted splitMeshDataForBufferLimit() utility.
|
|
215
207
|
*/
|
|
216
208
|
private splitMeshDataForBufferLimit;
|
|
217
209
|
/**
|
|
@@ -294,32 +286,10 @@ export declare class Scene {
|
|
|
294
286
|
*/
|
|
295
287
|
getEntityBoundingBox(expressId: number): BoundingBox | null;
|
|
296
288
|
/**
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
|
|
300
|
-
private rayIntersectsBox;
|
|
301
|
-
/**
|
|
302
|
-
* Ray-box intersection returning entry distance (tNear).
|
|
303
|
-
* Returns null if no intersection, otherwise the distance along the ray
|
|
304
|
-
* to the entry point (clamped to 0 if the ray originates inside the box).
|
|
305
|
-
* Handles zero ray direction components (axis-aligned rays) safely.
|
|
289
|
+
* CPU raycast against all mesh data.
|
|
290
|
+
* Returns expressId and modelIndex of closest hit, or null.
|
|
291
|
+
* Delegates to extracted raycaster utilities.
|
|
306
292
|
*/
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Möller–Trumbore ray-triangle intersection
|
|
310
|
-
* Returns distance to intersection or null if no hit
|
|
311
|
-
*/
|
|
312
|
-
private rayTriangleIntersect;
|
|
313
|
-
/**
|
|
314
|
-
* CPU raycast against all mesh data
|
|
315
|
-
* Returns expressId and modelIndex of closest hit, or null
|
|
316
|
-
* For multi-model support: tracks which model's geometry was hit
|
|
317
|
-
*/
|
|
318
|
-
raycast(rayOrigin: Vec3, rayDir: Vec3, hiddenIds?: Set<number>, isolatedIds?: Set<number> | null): {
|
|
319
|
-
expressId: number;
|
|
320
|
-
distance: number;
|
|
321
|
-
modelIndex?: number;
|
|
322
|
-
} | null;
|
|
293
|
+
raycast(rayOrigin: Vec3, rayDir: Vec3, hiddenIds?: Set<number>, isolatedIds?: Set<number> | null): RaycastHit | null;
|
|
323
294
|
}
|
|
324
|
-
export {};
|
|
325
295
|
//# sourceMappingURL=scene.d.ts.map
|
package/dist/scene.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"AAIA;;GAEG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"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"}
|
package/dist/scene.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
2
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
-
import { MathUtils } from './math.js';
|
|
5
4
|
import { BATCH_CONSTANTS } from './constants.js';
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, } from './scene-raycaster.js';
|
|
6
|
+
import { mergeGeometry, splitMeshDataForBufferLimit } from './scene-geometry.js';
|
|
8
7
|
export class Scene {
|
|
9
8
|
meshes = [];
|
|
10
9
|
instancedMeshes = [];
|
|
@@ -469,8 +468,8 @@ export class Scene {
|
|
|
469
468
|
// This preserves the per-frame time budget while cutting appendToBatches()
|
|
470
469
|
// overhead and front-of-array churn during huge desktop streams.
|
|
471
470
|
const MAX_MESHES_PER_FLUSH = 4096;
|
|
472
|
-
const MESHES_PER_APPEND =
|
|
473
|
-
const FLUSH_BUDGET_MS =
|
|
471
|
+
const MESHES_PER_APPEND = 512;
|
|
472
|
+
const FLUSH_BUDGET_MS = 12;
|
|
474
473
|
const start = performance.now();
|
|
475
474
|
let processed = 0;
|
|
476
475
|
while (this.meshQueueReadIndex < this.meshQueue.length && processed < MAX_MESHES_PER_FLUSH) {
|
|
@@ -983,17 +982,23 @@ export class Scene {
|
|
|
983
982
|
const merged = this.mergeGeometry(meshDataArray);
|
|
984
983
|
const expressIds = meshDataArray.map(m => m.expressId);
|
|
985
984
|
// Create vertex buffer (interleaved positions + normals)
|
|
985
|
+
// Use mappedAtCreation to avoid a separate writeBuffer IPC round-trip
|
|
986
|
+
// (significant win on Chrome/Dawn where each writeBuffer is a Mojo IPC call)
|
|
986
987
|
const vertexBuffer = device.createBuffer({
|
|
987
988
|
size: merged.vertexData.byteLength,
|
|
988
989
|
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
990
|
+
mappedAtCreation: true,
|
|
989
991
|
});
|
|
990
|
-
|
|
992
|
+
new Float32Array(vertexBuffer.getMappedRange()).set(merged.vertexData);
|
|
993
|
+
vertexBuffer.unmap();
|
|
991
994
|
// Create index buffer
|
|
992
995
|
const indexBuffer = device.createBuffer({
|
|
993
996
|
size: merged.indices.byteLength,
|
|
994
997
|
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
998
|
+
mappedAtCreation: true,
|
|
995
999
|
});
|
|
996
|
-
|
|
1000
|
+
new Uint32Array(indexBuffer.getMappedRange()).set(merged.indices);
|
|
1001
|
+
indexBuffer.unmap();
|
|
997
1002
|
// Create uniform buffer for this batch
|
|
998
1003
|
const uniformBuffer = device.createBuffer({
|
|
999
1004
|
size: pipeline.getUniformBufferSize(),
|
|
@@ -1023,89 +1028,11 @@ export class Scene {
|
|
|
1023
1028
|
};
|
|
1024
1029
|
}
|
|
1025
1030
|
/**
|
|
1026
|
-
* Merge multiple mesh geometries into single vertex/index buffers
|
|
1027
|
-
*
|
|
1028
|
-
* OPTIMIZATION: Uses efficient loops and bulk index adjustment
|
|
1031
|
+
* Merge multiple mesh geometries into single vertex/index buffers.
|
|
1032
|
+
* Delegates to the extracted mergeGeometry() utility.
|
|
1029
1033
|
*/
|
|
1030
1034
|
mergeGeometry(meshDataArray) {
|
|
1031
|
-
|
|
1032
|
-
let totalIndices = 0;
|
|
1033
|
-
// Calculate total sizes
|
|
1034
|
-
for (const mesh of meshDataArray) {
|
|
1035
|
-
totalVertices += mesh.positions.length / 3;
|
|
1036
|
-
totalIndices += mesh.indices.length;
|
|
1037
|
-
}
|
|
1038
|
-
// Create merged buffers
|
|
1039
|
-
const vertexBufferRaw = new ArrayBuffer(totalVertices * 7 * 4);
|
|
1040
|
-
const vertexData = new Float32Array(vertexBufferRaw); // position + normal
|
|
1041
|
-
const vertexDataU32 = new Uint32Array(vertexBufferRaw); // entityId lane
|
|
1042
|
-
const indices = new Uint32Array(totalIndices);
|
|
1043
|
-
// Track bounds during merge (avoids a second pass)
|
|
1044
|
-
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
1045
|
-
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
1046
|
-
let indexOffset = 0;
|
|
1047
|
-
let vertexBase = 0;
|
|
1048
|
-
for (const mesh of meshDataArray) {
|
|
1049
|
-
const positions = mesh.positions;
|
|
1050
|
-
const normals = mesh.normals;
|
|
1051
|
-
const vertexCount = positions.length / 3;
|
|
1052
|
-
// Interleave vertex data (position + normal + entityId)
|
|
1053
|
-
// This loop is O(n) per mesh and unavoidable for interleaving
|
|
1054
|
-
let outIdx = vertexBase * 7;
|
|
1055
|
-
const perVertexEntityIds = mesh.entityIds; // color-merged batches
|
|
1056
|
-
let entityId = mesh.expressId >>> 0;
|
|
1057
|
-
if (!perVertexEntityIds && entityId > MAX_ENCODED_ENTITY_ID) {
|
|
1058
|
-
if (!warnedEntityIdRange) {
|
|
1059
|
-
warnedEntityIdRange = true;
|
|
1060
|
-
console.warn('[Renderer] expressId exceeds 24-bit seam-ID encoding range; seam lines may collide.');
|
|
1061
|
-
}
|
|
1062
|
-
entityId = entityId & MAX_ENCODED_ENTITY_ID;
|
|
1063
|
-
}
|
|
1064
|
-
const hasNormals = normals.length > 0;
|
|
1065
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
1066
|
-
const srcIdx = i * 3;
|
|
1067
|
-
const px = positions[srcIdx];
|
|
1068
|
-
const py = positions[srcIdx + 1];
|
|
1069
|
-
const pz = positions[srcIdx + 2];
|
|
1070
|
-
vertexData[outIdx++] = px;
|
|
1071
|
-
vertexData[outIdx++] = py;
|
|
1072
|
-
vertexData[outIdx++] = pz;
|
|
1073
|
-
vertexData[outIdx++] = hasNormals ? normals[srcIdx] : 0;
|
|
1074
|
-
vertexData[outIdx++] = hasNormals ? normals[srcIdx + 1] : 0;
|
|
1075
|
-
vertexData[outIdx++] = hasNormals ? normals[srcIdx + 2] : 0;
|
|
1076
|
-
vertexDataU32[outIdx++] = perVertexEntityIds ? (perVertexEntityIds[i] >>> 0) : entityId;
|
|
1077
|
-
// Update bounds
|
|
1078
|
-
if (px < minX)
|
|
1079
|
-
minX = px;
|
|
1080
|
-
if (py < minY)
|
|
1081
|
-
minY = py;
|
|
1082
|
-
if (pz < minZ)
|
|
1083
|
-
minZ = pz;
|
|
1084
|
-
if (px > maxX)
|
|
1085
|
-
maxX = px;
|
|
1086
|
-
if (py > maxY)
|
|
1087
|
-
maxY = py;
|
|
1088
|
-
if (pz > maxZ)
|
|
1089
|
-
maxZ = pz;
|
|
1090
|
-
}
|
|
1091
|
-
// Copy indices with vertex base offset
|
|
1092
|
-
// Use subarray for slightly better cache locality
|
|
1093
|
-
const meshIndices = mesh.indices;
|
|
1094
|
-
const indexCount = meshIndices.length;
|
|
1095
|
-
for (let i = 0; i < indexCount; i++) {
|
|
1096
|
-
indices[indexOffset + i] = meshIndices[i] + vertexBase;
|
|
1097
|
-
}
|
|
1098
|
-
vertexBase += vertexCount;
|
|
1099
|
-
indexOffset += indexCount;
|
|
1100
|
-
}
|
|
1101
|
-
return {
|
|
1102
|
-
vertexData,
|
|
1103
|
-
indices,
|
|
1104
|
-
bounds: {
|
|
1105
|
-
min: [minX, minY, minZ],
|
|
1106
|
-
max: [maxX, maxY, maxZ],
|
|
1107
|
-
},
|
|
1108
|
-
};
|
|
1035
|
+
return mergeGeometry(meshDataArray);
|
|
1109
1036
|
}
|
|
1110
1037
|
/**
|
|
1111
1038
|
* Get the effective max buffer size for this GPU device, with a safety margin.
|
|
@@ -1115,50 +1042,11 @@ export class Scene {
|
|
|
1115
1042
|
return Math.floor(deviceMax * BATCH_CONSTANTS.BUFFER_SIZE_SAFETY_FACTOR);
|
|
1116
1043
|
}
|
|
1117
1044
|
/**
|
|
1118
|
-
* Split a meshDataArray into chunks
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
* Each mesh is kept intact — we never split a single element's geometry.
|
|
1122
|
-
* If a single mesh exceeds the limit on its own it is placed in a solo chunk
|
|
1123
|
-
* (WebGPU will clamp or error, but we don't silently drop geometry).
|
|
1045
|
+
* Split a meshDataArray into chunks that fit within GPU buffer limits.
|
|
1046
|
+
* Delegates to the extracted splitMeshDataForBufferLimit() utility.
|
|
1124
1047
|
*/
|
|
1125
1048
|
splitMeshDataForBufferLimit(meshDataArray, maxBufferSize) {
|
|
1126
|
-
|
|
1127
|
-
let totalVertexBytes = 0;
|
|
1128
|
-
let totalIndexBytes = 0;
|
|
1129
|
-
for (const mesh of meshDataArray) {
|
|
1130
|
-
totalVertexBytes += (mesh.positions.length / 3) * BATCH_CONSTANTS.BYTES_PER_VERTEX;
|
|
1131
|
-
totalIndexBytes += mesh.indices.length * BATCH_CONSTANTS.BYTES_PER_INDEX;
|
|
1132
|
-
}
|
|
1133
|
-
if (totalVertexBytes <= maxBufferSize && totalIndexBytes <= maxBufferSize) {
|
|
1134
|
-
return [meshDataArray];
|
|
1135
|
-
}
|
|
1136
|
-
// Slow path: partition into chunks
|
|
1137
|
-
const chunks = [];
|
|
1138
|
-
let currentChunk = [];
|
|
1139
|
-
let currentVertexBytes = 0;
|
|
1140
|
-
let currentIndexBytes = 0;
|
|
1141
|
-
for (const mesh of meshDataArray) {
|
|
1142
|
-
const meshVertexBytes = (mesh.positions.length / 3) * BATCH_CONSTANTS.BYTES_PER_VERTEX;
|
|
1143
|
-
const meshIndexBytes = mesh.indices.length * BATCH_CONSTANTS.BYTES_PER_INDEX;
|
|
1144
|
-
// Would adding this mesh exceed the limit? Start a new chunk.
|
|
1145
|
-
// (Skip check when chunk is empty — a single mesh must always be included.)
|
|
1146
|
-
if (currentChunk.length > 0 &&
|
|
1147
|
-
(currentVertexBytes + meshVertexBytes > maxBufferSize ||
|
|
1148
|
-
currentIndexBytes + meshIndexBytes > maxBufferSize)) {
|
|
1149
|
-
chunks.push(currentChunk);
|
|
1150
|
-
currentChunk = [];
|
|
1151
|
-
currentVertexBytes = 0;
|
|
1152
|
-
currentIndexBytes = 0;
|
|
1153
|
-
}
|
|
1154
|
-
currentChunk.push(mesh);
|
|
1155
|
-
currentVertexBytes += meshVertexBytes;
|
|
1156
|
-
currentIndexBytes += meshIndexBytes;
|
|
1157
|
-
}
|
|
1158
|
-
if (currentChunk.length > 0) {
|
|
1159
|
-
chunks.push(currentChunk);
|
|
1160
|
-
}
|
|
1161
|
-
return chunks;
|
|
1049
|
+
return splitMeshDataForBufferLimit(meshDataArray, maxBufferSize);
|
|
1162
1050
|
}
|
|
1163
1051
|
/**
|
|
1164
1052
|
* Resolve which bucket a mesh should be added to.
|
|
@@ -1537,224 +1425,18 @@ export class Scene {
|
|
|
1537
1425
|
return bbox;
|
|
1538
1426
|
}
|
|
1539
1427
|
/**
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1542
|
-
|
|
1543
|
-
rayIntersectsBox(rayOrigin, rayDirInv, // 1/rayDir for efficiency
|
|
1544
|
-
rayDirSign, box) {
|
|
1545
|
-
const bounds = [box.min, box.max];
|
|
1546
|
-
let tmin = -Infinity;
|
|
1547
|
-
let tmax = Infinity;
|
|
1548
|
-
// X axis
|
|
1549
|
-
if (!isFinite(rayDirInv.x)) {
|
|
1550
|
-
if (rayOrigin.x < box.min.x || rayOrigin.x > box.max.x)
|
|
1551
|
-
return false;
|
|
1552
|
-
}
|
|
1553
|
-
else {
|
|
1554
|
-
tmin = (bounds[rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
|
|
1555
|
-
tmax = (bounds[1 - rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
|
|
1556
|
-
}
|
|
1557
|
-
// Y axis
|
|
1558
|
-
if (!isFinite(rayDirInv.y)) {
|
|
1559
|
-
if (rayOrigin.y < box.min.y || rayOrigin.y > box.max.y)
|
|
1560
|
-
return false;
|
|
1561
|
-
}
|
|
1562
|
-
else {
|
|
1563
|
-
const tymin = (bounds[rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
|
|
1564
|
-
const tymax = (bounds[1 - rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
|
|
1565
|
-
if (tmin > tymax || tymin > tmax)
|
|
1566
|
-
return false;
|
|
1567
|
-
if (tymin > tmin)
|
|
1568
|
-
tmin = tymin;
|
|
1569
|
-
if (tymax < tmax)
|
|
1570
|
-
tmax = tymax;
|
|
1571
|
-
}
|
|
1572
|
-
// Z axis
|
|
1573
|
-
if (!isFinite(rayDirInv.z)) {
|
|
1574
|
-
if (rayOrigin.z < box.min.z || rayOrigin.z > box.max.z)
|
|
1575
|
-
return false;
|
|
1576
|
-
}
|
|
1577
|
-
else {
|
|
1578
|
-
const tzmin = (bounds[rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
|
|
1579
|
-
const tzmax = (bounds[1 - rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
|
|
1580
|
-
if (tmin > tzmax || tzmin > tmax)
|
|
1581
|
-
return false;
|
|
1582
|
-
if (tzmin > tmin)
|
|
1583
|
-
tmin = tzmin;
|
|
1584
|
-
if (tzmax < tmax)
|
|
1585
|
-
tmax = tzmax;
|
|
1586
|
-
}
|
|
1587
|
-
return tmax >= 0;
|
|
1588
|
-
}
|
|
1589
|
-
/**
|
|
1590
|
-
* Ray-box intersection returning entry distance (tNear).
|
|
1591
|
-
* Returns null if no intersection, otherwise the distance along the ray
|
|
1592
|
-
* to the entry point (clamped to 0 if the ray originates inside the box).
|
|
1593
|
-
* Handles zero ray direction components (axis-aligned rays) safely.
|
|
1594
|
-
*/
|
|
1595
|
-
rayBoxDistance(rayOrigin, rayDirInv, rayDirSign, box) {
|
|
1596
|
-
const bounds = [box.min, box.max];
|
|
1597
|
-
let tmin = -Infinity;
|
|
1598
|
-
let tmax = Infinity;
|
|
1599
|
-
// X axis
|
|
1600
|
-
if (!isFinite(rayDirInv.x)) {
|
|
1601
|
-
// Ray parallel to X: miss if origin outside X slab
|
|
1602
|
-
if (rayOrigin.x < box.min.x || rayOrigin.x > box.max.x)
|
|
1603
|
-
return null;
|
|
1604
|
-
}
|
|
1605
|
-
else {
|
|
1606
|
-
const t1 = (bounds[rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
|
|
1607
|
-
const t2 = (bounds[1 - rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
|
|
1608
|
-
tmin = t1;
|
|
1609
|
-
tmax = t2;
|
|
1610
|
-
}
|
|
1611
|
-
// Y axis
|
|
1612
|
-
if (!isFinite(rayDirInv.y)) {
|
|
1613
|
-
if (rayOrigin.y < box.min.y || rayOrigin.y > box.max.y)
|
|
1614
|
-
return null;
|
|
1615
|
-
}
|
|
1616
|
-
else {
|
|
1617
|
-
const tymin = (bounds[rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
|
|
1618
|
-
const tymax = (bounds[1 - rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
|
|
1619
|
-
if (tmin > tymax || tymin > tmax)
|
|
1620
|
-
return null;
|
|
1621
|
-
if (tymin > tmin)
|
|
1622
|
-
tmin = tymin;
|
|
1623
|
-
if (tymax < tmax)
|
|
1624
|
-
tmax = tymax;
|
|
1625
|
-
}
|
|
1626
|
-
// Z axis
|
|
1627
|
-
if (!isFinite(rayDirInv.z)) {
|
|
1628
|
-
if (rayOrigin.z < box.min.z || rayOrigin.z > box.max.z)
|
|
1629
|
-
return null;
|
|
1630
|
-
}
|
|
1631
|
-
else {
|
|
1632
|
-
const tzmin = (bounds[rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
|
|
1633
|
-
const tzmax = (bounds[1 - rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
|
|
1634
|
-
if (tmin > tzmax || tzmin > tmax)
|
|
1635
|
-
return null;
|
|
1636
|
-
if (tzmin > tmin)
|
|
1637
|
-
tmin = tzmin;
|
|
1638
|
-
if (tzmax < tmax)
|
|
1639
|
-
tmax = tzmax;
|
|
1640
|
-
}
|
|
1641
|
-
if (tmax < 0)
|
|
1642
|
-
return null;
|
|
1643
|
-
return tmin < 0 ? 0 : tmin;
|
|
1644
|
-
}
|
|
1645
|
-
/**
|
|
1646
|
-
* Möller–Trumbore ray-triangle intersection
|
|
1647
|
-
* Returns distance to intersection or null if no hit
|
|
1648
|
-
*/
|
|
1649
|
-
rayTriangleIntersect(rayOrigin, rayDir, v0, v1, v2) {
|
|
1650
|
-
const EPSILON = 1e-7;
|
|
1651
|
-
const edge1 = MathUtils.subtract(v1, v0);
|
|
1652
|
-
const edge2 = MathUtils.subtract(v2, v0);
|
|
1653
|
-
const h = MathUtils.cross(rayDir, edge2);
|
|
1654
|
-
const a = MathUtils.dot(edge1, h);
|
|
1655
|
-
if (a > -EPSILON && a < EPSILON)
|
|
1656
|
-
return null; // Ray parallel to triangle
|
|
1657
|
-
const f = 1.0 / a;
|
|
1658
|
-
const s = MathUtils.subtract(rayOrigin, v0);
|
|
1659
|
-
const u = f * MathUtils.dot(s, h);
|
|
1660
|
-
if (u < 0.0 || u > 1.0)
|
|
1661
|
-
return null;
|
|
1662
|
-
const q = MathUtils.cross(s, edge1);
|
|
1663
|
-
const v = f * MathUtils.dot(rayDir, q);
|
|
1664
|
-
if (v < 0.0 || u + v > 1.0)
|
|
1665
|
-
return null;
|
|
1666
|
-
const t = f * MathUtils.dot(edge2, q);
|
|
1667
|
-
if (t > EPSILON)
|
|
1668
|
-
return t; // Ray intersection
|
|
1669
|
-
return null;
|
|
1670
|
-
}
|
|
1671
|
-
/**
|
|
1672
|
-
* CPU raycast against all mesh data
|
|
1673
|
-
* Returns expressId and modelIndex of closest hit, or null
|
|
1674
|
-
* For multi-model support: tracks which model's geometry was hit
|
|
1428
|
+
* CPU raycast against all mesh data.
|
|
1429
|
+
* Returns expressId and modelIndex of closest hit, or null.
|
|
1430
|
+
* Delegates to extracted raycaster utilities.
|
|
1675
1431
|
*/
|
|
1676
1432
|
raycast(rayOrigin, rayDir, hiddenIds, isolatedIds) {
|
|
1677
|
-
|
|
1678
|
-
const rayDirInv = {
|
|
1679
|
-
x: rayDir.x !== 0 ? 1.0 / rayDir.x : Infinity,
|
|
1680
|
-
y: rayDir.y !== 0 ? 1.0 / rayDir.y : Infinity,
|
|
1681
|
-
z: rayDir.z !== 0 ? 1.0 / rayDir.z : Infinity,
|
|
1682
|
-
};
|
|
1683
|
-
const rayDirSign = [
|
|
1684
|
-
rayDirInv.x < 0 ? 1 : 0,
|
|
1685
|
-
rayDirInv.y < 0 ? 1 : 0,
|
|
1686
|
-
rayDirInv.z < 0 ? 1 : 0,
|
|
1687
|
-
];
|
|
1688
|
-
let closestHit = null;
|
|
1689
|
-
let closestDistance = Infinity;
|
|
1433
|
+
const { rayDirInv, rayDirSign } = prepareRayDirInv(rayDir);
|
|
1690
1434
|
// When geometry data has been released, use bounding-box-only raycast.
|
|
1691
|
-
// This is less accurate but uses only the precomputed bounding boxes.
|
|
1692
|
-
// For precise picking, callers should use GPU picking instead.
|
|
1693
1435
|
if (this.geometryReleased) {
|
|
1694
|
-
|
|
1695
|
-
if (hiddenIds?.has(expressId))
|
|
1696
|
-
continue;
|
|
1697
|
-
if (isolatedIds !== null && isolatedIds !== undefined && !isolatedIds.has(expressId))
|
|
1698
|
-
continue;
|
|
1699
|
-
const tNear = this.rayBoxDistance(rayOrigin, rayDirInv, rayDirSign, bbox);
|
|
1700
|
-
if (tNear !== null && tNear < closestDistance) {
|
|
1701
|
-
closestDistance = tNear;
|
|
1702
|
-
closestHit = { expressId, distance: tNear };
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
return closestHit;
|
|
1706
|
-
}
|
|
1707
|
-
// First pass: filter by bounding box (fast)
|
|
1708
|
-
const candidates = [];
|
|
1709
|
-
for (const expressId of this.meshDataMap.keys()) {
|
|
1710
|
-
// Skip hidden elements
|
|
1711
|
-
if (hiddenIds?.has(expressId))
|
|
1712
|
-
continue;
|
|
1713
|
-
// Skip non-isolated elements if isolation is active
|
|
1714
|
-
if (isolatedIds !== null && isolatedIds !== undefined && !isolatedIds.has(expressId))
|
|
1715
|
-
continue;
|
|
1716
|
-
const bbox = this.getEntityBoundingBox(expressId);
|
|
1717
|
-
if (!bbox)
|
|
1718
|
-
continue;
|
|
1719
|
-
if (this.rayIntersectsBox(rayOrigin, rayDirInv, rayDirSign, bbox)) {
|
|
1720
|
-
candidates.push(expressId);
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
// Second pass: test triangles for candidates (accurate)
|
|
1724
|
-
for (const expressId of candidates) {
|
|
1725
|
-
const pieces = this.meshDataMap.get(expressId);
|
|
1726
|
-
if (!pieces)
|
|
1727
|
-
continue;
|
|
1728
|
-
for (const piece of pieces) {
|
|
1729
|
-
const positions = piece.positions;
|
|
1730
|
-
const indices = piece.indices;
|
|
1731
|
-
const pieceEntityIds = piece.entityIds; // per-vertex IDs for merged meshes
|
|
1732
|
-
// Test each triangle
|
|
1733
|
-
for (let i = 0; i < indices.length; i += 3) {
|
|
1734
|
-
// For color-merged meshes, skip triangles that don't belong to
|
|
1735
|
-
// this entity. Without this check, hitting ANY triangle in the
|
|
1736
|
-
// merged batch would attribute it to the candidate expressId.
|
|
1737
|
-
if (pieceEntityIds) {
|
|
1738
|
-
const vertIdx = indices[i];
|
|
1739
|
-
if (pieceEntityIds[vertIdx] !== expressId)
|
|
1740
|
-
continue;
|
|
1741
|
-
}
|
|
1742
|
-
const i0 = indices[i] * 3;
|
|
1743
|
-
const i1 = indices[i + 1] * 3;
|
|
1744
|
-
const i2 = indices[i + 2] * 3;
|
|
1745
|
-
const v0 = { x: positions[i0], y: positions[i0 + 1], z: positions[i0 + 2] };
|
|
1746
|
-
const v1 = { x: positions[i1], y: positions[i1 + 1], z: positions[i1 + 2] };
|
|
1747
|
-
const v2 = { x: positions[i2], y: positions[i2 + 1], z: positions[i2 + 2] };
|
|
1748
|
-
const t = this.rayTriangleIntersect(rayOrigin, rayDir, v0, v1, v2);
|
|
1749
|
-
if (t !== null && t < closestDistance) {
|
|
1750
|
-
closestDistance = t;
|
|
1751
|
-
// Track modelIndex from the piece that was actually hit
|
|
1752
|
-
closestHit = { expressId, distance: t, modelIndex: piece.modelIndex };
|
|
1753
|
-
}
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1436
|
+
return raycastBoundingBoxes(rayOrigin, rayDirInv, rayDirSign, this.boundingBoxes, hiddenIds, isolatedIds);
|
|
1756
1437
|
}
|
|
1757
|
-
|
|
1438
|
+
// Full triangle-level raycast with bounding-box pre-filter
|
|
1439
|
+
return raycastTriangles(rayOrigin, rayDir, rayDirInv, rayDirSign, this.meshDataMap, (id) => this.getEntityBoundingBox(id), hiddenIds, isolatedIds);
|
|
1758
1440
|
}
|
|
1759
1441
|
}
|
|
1760
1442
|
//# sourceMappingURL=scene.js.map
|