@ifc-lite/renderer 1.15.2 → 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 = [];
@@ -1029,89 +1028,11 @@ export class Scene {
1029
1028
  };
1030
1029
  }
1031
1030
  /**
1032
- * Merge multiple mesh geometries into single vertex/index buffers
1033
- *
1034
- * 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.
1035
1033
  */
1036
1034
  mergeGeometry(meshDataArray) {
1037
- let totalVertices = 0;
1038
- let totalIndices = 0;
1039
- // Calculate total sizes
1040
- for (const mesh of meshDataArray) {
1041
- totalVertices += mesh.positions.length / 3;
1042
- totalIndices += mesh.indices.length;
1043
- }
1044
- // Create merged buffers
1045
- const vertexBufferRaw = new ArrayBuffer(totalVertices * 7 * 4);
1046
- const vertexData = new Float32Array(vertexBufferRaw); // position + normal
1047
- const vertexDataU32 = new Uint32Array(vertexBufferRaw); // entityId lane
1048
- const indices = new Uint32Array(totalIndices);
1049
- // Track bounds during merge (avoids a second pass)
1050
- let minX = Infinity, minY = Infinity, minZ = Infinity;
1051
- let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
1052
- let indexOffset = 0;
1053
- let vertexBase = 0;
1054
- for (const mesh of meshDataArray) {
1055
- const positions = mesh.positions;
1056
- const normals = mesh.normals;
1057
- const vertexCount = positions.length / 3;
1058
- // Interleave vertex data (position + normal + entityId)
1059
- // This loop is O(n) per mesh and unavoidable for interleaving
1060
- let outIdx = vertexBase * 7;
1061
- const perVertexEntityIds = mesh.entityIds; // color-merged batches
1062
- let entityId = mesh.expressId >>> 0;
1063
- if (!perVertexEntityIds && entityId > MAX_ENCODED_ENTITY_ID) {
1064
- if (!warnedEntityIdRange) {
1065
- warnedEntityIdRange = true;
1066
- console.warn('[Renderer] expressId exceeds 24-bit seam-ID encoding range; seam lines may collide.');
1067
- }
1068
- entityId = entityId & MAX_ENCODED_ENTITY_ID;
1069
- }
1070
- const hasNormals = normals.length > 0;
1071
- for (let i = 0; i < vertexCount; i++) {
1072
- const srcIdx = i * 3;
1073
- const px = positions[srcIdx];
1074
- const py = positions[srcIdx + 1];
1075
- const pz = positions[srcIdx + 2];
1076
- vertexData[outIdx++] = px;
1077
- vertexData[outIdx++] = py;
1078
- vertexData[outIdx++] = pz;
1079
- vertexData[outIdx++] = hasNormals ? normals[srcIdx] : 0;
1080
- vertexData[outIdx++] = hasNormals ? normals[srcIdx + 1] : 0;
1081
- vertexData[outIdx++] = hasNormals ? normals[srcIdx + 2] : 0;
1082
- vertexDataU32[outIdx++] = perVertexEntityIds ? (perVertexEntityIds[i] >>> 0) : entityId;
1083
- // Update bounds
1084
- if (px < minX)
1085
- minX = px;
1086
- if (py < minY)
1087
- minY = py;
1088
- if (pz < minZ)
1089
- minZ = pz;
1090
- if (px > maxX)
1091
- maxX = px;
1092
- if (py > maxY)
1093
- maxY = py;
1094
- if (pz > maxZ)
1095
- maxZ = pz;
1096
- }
1097
- // Copy indices with vertex base offset
1098
- // Use subarray for slightly better cache locality
1099
- const meshIndices = mesh.indices;
1100
- const indexCount = meshIndices.length;
1101
- for (let i = 0; i < indexCount; i++) {
1102
- indices[indexOffset + i] = meshIndices[i] + vertexBase;
1103
- }
1104
- vertexBase += vertexCount;
1105
- indexOffset += indexCount;
1106
- }
1107
- return {
1108
- vertexData,
1109
- indices,
1110
- bounds: {
1111
- min: [minX, minY, minZ],
1112
- max: [maxX, maxY, maxZ],
1113
- },
1114
- };
1035
+ return mergeGeometry(meshDataArray);
1115
1036
  }
1116
1037
  /**
1117
1038
  * Get the effective max buffer size for this GPU device, with a safety margin.
@@ -1121,50 +1042,11 @@ export class Scene {
1121
1042
  return Math.floor(deviceMax * BATCH_CONSTANTS.BUFFER_SIZE_SAFETY_FACTOR);
1122
1043
  }
1123
1044
  /**
1124
- * Split a meshDataArray into chunks where each chunk's largest buffer
1125
- * (vertex or index) stays within maxBufferSize.
1126
- *
1127
- * Each mesh is kept intact — we never split a single element's geometry.
1128
- * If a single mesh exceeds the limit on its own it is placed in a solo chunk
1129
- * (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.
1130
1047
  */
1131
1048
  splitMeshDataForBufferLimit(meshDataArray, maxBufferSize) {
1132
- // Fast path: estimate total size — if it fits, no splitting needed
1133
- let totalVertexBytes = 0;
1134
- let totalIndexBytes = 0;
1135
- for (const mesh of meshDataArray) {
1136
- totalVertexBytes += (mesh.positions.length / 3) * BATCH_CONSTANTS.BYTES_PER_VERTEX;
1137
- totalIndexBytes += mesh.indices.length * BATCH_CONSTANTS.BYTES_PER_INDEX;
1138
- }
1139
- if (totalVertexBytes <= maxBufferSize && totalIndexBytes <= maxBufferSize) {
1140
- return [meshDataArray];
1141
- }
1142
- // Slow path: partition into chunks
1143
- const chunks = [];
1144
- let currentChunk = [];
1145
- let currentVertexBytes = 0;
1146
- let currentIndexBytes = 0;
1147
- for (const mesh of meshDataArray) {
1148
- const meshVertexBytes = (mesh.positions.length / 3) * BATCH_CONSTANTS.BYTES_PER_VERTEX;
1149
- const meshIndexBytes = mesh.indices.length * BATCH_CONSTANTS.BYTES_PER_INDEX;
1150
- // Would adding this mesh exceed the limit? Start a new chunk.
1151
- // (Skip check when chunk is empty — a single mesh must always be included.)
1152
- if (currentChunk.length > 0 &&
1153
- (currentVertexBytes + meshVertexBytes > maxBufferSize ||
1154
- currentIndexBytes + meshIndexBytes > maxBufferSize)) {
1155
- chunks.push(currentChunk);
1156
- currentChunk = [];
1157
- currentVertexBytes = 0;
1158
- currentIndexBytes = 0;
1159
- }
1160
- currentChunk.push(mesh);
1161
- currentVertexBytes += meshVertexBytes;
1162
- currentIndexBytes += meshIndexBytes;
1163
- }
1164
- if (currentChunk.length > 0) {
1165
- chunks.push(currentChunk);
1166
- }
1167
- return chunks;
1049
+ return splitMeshDataForBufferLimit(meshDataArray, maxBufferSize);
1168
1050
  }
1169
1051
  /**
1170
1052
  * Resolve which bucket a mesh should be added to.
@@ -1543,224 +1425,18 @@ export class Scene {
1543
1425
  return bbox;
1544
1426
  }
1545
1427
  /**
1546
- * Ray-box intersection test (slab method).
1547
- * Handles zero ray direction components (axis-aligned rays) safely.
1548
- */
1549
- rayIntersectsBox(rayOrigin, rayDirInv, // 1/rayDir for efficiency
1550
- rayDirSign, box) {
1551
- const bounds = [box.min, box.max];
1552
- let tmin = -Infinity;
1553
- let tmax = Infinity;
1554
- // X axis
1555
- if (!isFinite(rayDirInv.x)) {
1556
- if (rayOrigin.x < box.min.x || rayOrigin.x > box.max.x)
1557
- return false;
1558
- }
1559
- else {
1560
- tmin = (bounds[rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
1561
- tmax = (bounds[1 - rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
1562
- }
1563
- // Y axis
1564
- if (!isFinite(rayDirInv.y)) {
1565
- if (rayOrigin.y < box.min.y || rayOrigin.y > box.max.y)
1566
- return false;
1567
- }
1568
- else {
1569
- const tymin = (bounds[rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
1570
- const tymax = (bounds[1 - rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
1571
- if (tmin > tymax || tymin > tmax)
1572
- return false;
1573
- if (tymin > tmin)
1574
- tmin = tymin;
1575
- if (tymax < tmax)
1576
- tmax = tymax;
1577
- }
1578
- // Z axis
1579
- if (!isFinite(rayDirInv.z)) {
1580
- if (rayOrigin.z < box.min.z || rayOrigin.z > box.max.z)
1581
- return false;
1582
- }
1583
- else {
1584
- const tzmin = (bounds[rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
1585
- const tzmax = (bounds[1 - rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
1586
- if (tmin > tzmax || tzmin > tmax)
1587
- return false;
1588
- if (tzmin > tmin)
1589
- tmin = tzmin;
1590
- if (tzmax < tmax)
1591
- tmax = tzmax;
1592
- }
1593
- return tmax >= 0;
1594
- }
1595
- /**
1596
- * Ray-box intersection returning entry distance (tNear).
1597
- * Returns null if no intersection, otherwise the distance along the ray
1598
- * to the entry point (clamped to 0 if the ray originates inside the box).
1599
- * Handles zero ray direction components (axis-aligned rays) safely.
1600
- */
1601
- rayBoxDistance(rayOrigin, rayDirInv, rayDirSign, box) {
1602
- const bounds = [box.min, box.max];
1603
- let tmin = -Infinity;
1604
- let tmax = Infinity;
1605
- // X axis
1606
- if (!isFinite(rayDirInv.x)) {
1607
- // Ray parallel to X: miss if origin outside X slab
1608
- if (rayOrigin.x < box.min.x || rayOrigin.x > box.max.x)
1609
- return null;
1610
- }
1611
- else {
1612
- const t1 = (bounds[rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
1613
- const t2 = (bounds[1 - rayDirSign[0]].x - rayOrigin.x) * rayDirInv.x;
1614
- tmin = t1;
1615
- tmax = t2;
1616
- }
1617
- // Y axis
1618
- if (!isFinite(rayDirInv.y)) {
1619
- if (rayOrigin.y < box.min.y || rayOrigin.y > box.max.y)
1620
- return null;
1621
- }
1622
- else {
1623
- const tymin = (bounds[rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
1624
- const tymax = (bounds[1 - rayDirSign[1]].y - rayOrigin.y) * rayDirInv.y;
1625
- if (tmin > tymax || tymin > tmax)
1626
- return null;
1627
- if (tymin > tmin)
1628
- tmin = tymin;
1629
- if (tymax < tmax)
1630
- tmax = tymax;
1631
- }
1632
- // Z axis
1633
- if (!isFinite(rayDirInv.z)) {
1634
- if (rayOrigin.z < box.min.z || rayOrigin.z > box.max.z)
1635
- return null;
1636
- }
1637
- else {
1638
- const tzmin = (bounds[rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
1639
- const tzmax = (bounds[1 - rayDirSign[2]].z - rayOrigin.z) * rayDirInv.z;
1640
- if (tmin > tzmax || tzmin > tmax)
1641
- return null;
1642
- if (tzmin > tmin)
1643
- tmin = tzmin;
1644
- if (tzmax < tmax)
1645
- tmax = tzmax;
1646
- }
1647
- if (tmax < 0)
1648
- return null;
1649
- return tmin < 0 ? 0 : tmin;
1650
- }
1651
- /**
1652
- * Möller–Trumbore ray-triangle intersection
1653
- * Returns distance to intersection or null if no hit
1654
- */
1655
- rayTriangleIntersect(rayOrigin, rayDir, v0, v1, v2) {
1656
- const EPSILON = 1e-7;
1657
- const edge1 = MathUtils.subtract(v1, v0);
1658
- const edge2 = MathUtils.subtract(v2, v0);
1659
- const h = MathUtils.cross(rayDir, edge2);
1660
- const a = MathUtils.dot(edge1, h);
1661
- if (a > -EPSILON && a < EPSILON)
1662
- return null; // Ray parallel to triangle
1663
- const f = 1.0 / a;
1664
- const s = MathUtils.subtract(rayOrigin, v0);
1665
- const u = f * MathUtils.dot(s, h);
1666
- if (u < 0.0 || u > 1.0)
1667
- return null;
1668
- const q = MathUtils.cross(s, edge1);
1669
- const v = f * MathUtils.dot(rayDir, q);
1670
- if (v < 0.0 || u + v > 1.0)
1671
- return null;
1672
- const t = f * MathUtils.dot(edge2, q);
1673
- if (t > EPSILON)
1674
- return t; // Ray intersection
1675
- return null;
1676
- }
1677
- /**
1678
- * CPU raycast against all mesh data
1679
- * Returns expressId and modelIndex of closest hit, or null
1680
- * 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.
1681
1431
  */
1682
1432
  raycast(rayOrigin, rayDir, hiddenIds, isolatedIds) {
1683
- // Precompute ray direction inverse and signs for box tests
1684
- const rayDirInv = {
1685
- x: rayDir.x !== 0 ? 1.0 / rayDir.x : Infinity,
1686
- y: rayDir.y !== 0 ? 1.0 / rayDir.y : Infinity,
1687
- z: rayDir.z !== 0 ? 1.0 / rayDir.z : Infinity,
1688
- };
1689
- const rayDirSign = [
1690
- rayDirInv.x < 0 ? 1 : 0,
1691
- rayDirInv.y < 0 ? 1 : 0,
1692
- rayDirInv.z < 0 ? 1 : 0,
1693
- ];
1694
- let closestHit = null;
1695
- let closestDistance = Infinity;
1433
+ const { rayDirInv, rayDirSign } = prepareRayDirInv(rayDir);
1696
1434
  // When geometry data has been released, use bounding-box-only raycast.
1697
- // This is less accurate but uses only the precomputed bounding boxes.
1698
- // For precise picking, callers should use GPU picking instead.
1699
1435
  if (this.geometryReleased) {
1700
- for (const [expressId, bbox] of this.boundingBoxes) {
1701
- if (hiddenIds?.has(expressId))
1702
- continue;
1703
- if (isolatedIds !== null && isolatedIds !== undefined && !isolatedIds.has(expressId))
1704
- continue;
1705
- const tNear = this.rayBoxDistance(rayOrigin, rayDirInv, rayDirSign, bbox);
1706
- if (tNear !== null && tNear < closestDistance) {
1707
- closestDistance = tNear;
1708
- closestHit = { expressId, distance: tNear };
1709
- }
1710
- }
1711
- return closestHit;
1712
- }
1713
- // First pass: filter by bounding box (fast)
1714
- const candidates = [];
1715
- for (const expressId of this.meshDataMap.keys()) {
1716
- // Skip hidden elements
1717
- if (hiddenIds?.has(expressId))
1718
- continue;
1719
- // Skip non-isolated elements if isolation is active
1720
- if (isolatedIds !== null && isolatedIds !== undefined && !isolatedIds.has(expressId))
1721
- continue;
1722
- const bbox = this.getEntityBoundingBox(expressId);
1723
- if (!bbox)
1724
- continue;
1725
- if (this.rayIntersectsBox(rayOrigin, rayDirInv, rayDirSign, bbox)) {
1726
- candidates.push(expressId);
1727
- }
1728
- }
1729
- // Second pass: test triangles for candidates (accurate)
1730
- for (const expressId of candidates) {
1731
- const pieces = this.meshDataMap.get(expressId);
1732
- if (!pieces)
1733
- continue;
1734
- for (const piece of pieces) {
1735
- const positions = piece.positions;
1736
- const indices = piece.indices;
1737
- const pieceEntityIds = piece.entityIds; // per-vertex IDs for merged meshes
1738
- // Test each triangle
1739
- for (let i = 0; i < indices.length; i += 3) {
1740
- // For color-merged meshes, skip triangles that don't belong to
1741
- // this entity. Without this check, hitting ANY triangle in the
1742
- // merged batch would attribute it to the candidate expressId.
1743
- if (pieceEntityIds) {
1744
- const vertIdx = indices[i];
1745
- if (pieceEntityIds[vertIdx] !== expressId)
1746
- continue;
1747
- }
1748
- const i0 = indices[i] * 3;
1749
- const i1 = indices[i + 1] * 3;
1750
- const i2 = indices[i + 2] * 3;
1751
- const v0 = { x: positions[i0], y: positions[i0 + 1], z: positions[i0 + 2] };
1752
- const v1 = { x: positions[i1], y: positions[i1 + 1], z: positions[i1 + 2] };
1753
- const v2 = { x: positions[i2], y: positions[i2 + 1], z: positions[i2 + 2] };
1754
- const t = this.rayTriangleIntersect(rayOrigin, rayDir, v0, v1, v2);
1755
- if (t !== null && t < closestDistance) {
1756
- closestDistance = t;
1757
- // Track modelIndex from the piece that was actually hit
1758
- closestHit = { expressId, distance: t, modelIndex: piece.modelIndex };
1759
- }
1760
- }
1761
- }
1436
+ return raycastBoundingBoxes(rayOrigin, rayDirInv, rayDirSign, this.boundingBoxes, hiddenIds, isolatedIds);
1762
1437
  }
1763
- 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);
1764
1440
  }
1765
1441
  }
1766
1442
  //# sourceMappingURL=scene.js.map