@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/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
- interface BoundingBox {
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 where each chunk's largest buffer
210
- * (vertex or index) stays within maxBufferSize.
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
- * Ray-box intersection test (slab method).
298
- * Handles zero ray direction components (axis-aligned rays) safely.
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
- private rayBoxDistance;
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
@@ -1 +1 @@
1
- {"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"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"}
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
- const MAX_ENCODED_ENTITY_ID = 0xFFFFFF;
7
- let warnedEntityIdRange = false;
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 = 128;
473
- const FLUSH_BUDGET_MS = 10;
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
- device.queue.writeBuffer(vertexBuffer, 0, merged.vertexData);
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
- device.queue.writeBuffer(indexBuffer, 0, merged.indices);
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
- let totalVertices = 0;
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 where each chunk's largest buffer
1119
- * (vertex or index) stays within maxBufferSize.
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
- // Fast path: estimate total size — if it fits, no splitting needed
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
- * Ray-box intersection test (slab method).
1541
- * Handles zero ray direction components (axis-aligned rays) safely.
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
- // Precompute ray direction inverse and signs for box tests
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
- for (const [expressId, bbox] of this.boundingBoxes) {
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
- return closestHit;
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