@ifc-lite/renderer 1.28.3 → 1.28.5
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/index.d.ts.map +1 -1
- package/dist/index.js +70 -2
- package/dist/index.js.map +1 -1
- package/dist/instanced-render.d.ts +73 -0
- package/dist/instanced-render.d.ts.map +1 -0
- package/dist/instanced-render.js +163 -0
- package/dist/instanced-render.js.map +1 -0
- package/dist/overlay-routing.d.ts +1 -0
- package/dist/overlay-routing.d.ts.map +1 -1
- package/dist/overlay-routing.js +1 -1
- package/dist/overlay-routing.js.map +1 -1
- package/dist/picker.d.ts +5 -2
- package/dist/picker.d.ts.map +1 -1
- package/dist/picker.js +123 -5
- package/dist/picker.js.map +1 -1
- package/dist/picking-manager.d.ts.map +1 -1
- package/dist/picking-manager.js +2 -2
- package/dist/picking-manager.js.map +1 -1
- package/dist/pipeline.d.ts +15 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +119 -1
- package/dist/pipeline.js.map +1 -1
- package/dist/point-picker.d.ts +10 -2
- package/dist/point-picker.d.ts.map +1 -1
- package/dist/point-picker.js +17 -2
- package/dist/point-picker.js.map +1 -1
- package/dist/raycast-engine.d.ts +3 -0
- package/dist/raycast-engine.d.ts.map +1 -1
- package/dist/raycast-engine.js +92 -5
- package/dist/raycast-engine.js.map +1 -1
- package/dist/scene-geometry.d.ts +21 -0
- package/dist/scene-geometry.d.ts.map +1 -1
- package/dist/scene-geometry.js +38 -1
- package/dist/scene-geometry.js.map +1 -1
- package/dist/scene.d.ts +116 -0
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +565 -6
- 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 +102 -16
- package/dist/shaders/main.wgsl.js.map +1 -1
- package/dist/shaders/textured.wgsl.d.ts.map +1 -1
- package/dist/shaders/textured.wgsl.js +17 -6
- package/dist/shaders/textured.wgsl.js.map +1 -1
- package/package.json +2 -2
package/dist/scene.js
CHANGED
|
@@ -2,14 +2,18 @@
|
|
|
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
4
|
import { BATCH_CONSTANTS } from './constants.js';
|
|
5
|
-
import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, } from './scene-raycaster.js';
|
|
6
|
-
import { mergeGeometry, splitMeshDataForBufferLimit } from './scene-geometry.js';
|
|
5
|
+
import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, rayIntersectsBox, } from './scene-raycaster.js';
|
|
6
|
+
import { mergeGeometry, splitMeshDataForBufferLimit, colorSaltByte, packEntityLane } from './scene-geometry.js';
|
|
7
|
+
import { OPAQUE_ALPHA_CUTOFF } from './overlay-routing.js';
|
|
8
|
+
import { prepareInstancedRender, INSTANCE_STRIDE_BYTES, INSTANCE_COLOR_OFFSET, INSTANCE_FLAGS_OFFSET, INSTANCE_FLAG_SELECTED, INSTANCE_FLAG_HIDDEN, } from './instanced-render.js';
|
|
7
9
|
function destroyGpuResources(m) {
|
|
8
10
|
m.vertexBuffer.destroy();
|
|
9
11
|
m.indexBuffer.destroy();
|
|
10
12
|
if (m.uniformBuffer)
|
|
11
13
|
m.uniformBuffer.destroy();
|
|
12
14
|
}
|
|
15
|
+
/** Shared empty result for getInstancedTemplates() when the instanced pass is hidden. */
|
|
16
|
+
const EMPTY_INSTANCED_TEMPLATES = [];
|
|
13
17
|
export class Scene {
|
|
14
18
|
meshes = [];
|
|
15
19
|
batchedMeshes = []; // flat render array (rebuilt from buckets)
|
|
@@ -19,6 +23,18 @@ export class Scene {
|
|
|
19
23
|
boundingBoxes = new Map(); // Map expressId -> bounding box (computed lazily)
|
|
20
24
|
texturedMeshes = []; // #961: IFC surface-textured meshes (own buffers/texture/bindGroup)
|
|
21
25
|
texturedDevice; // #961: cached for textured-mesh re-upload on translate
|
|
26
|
+
instancedTemplates = []; // GPU-instancing: unique templates + per-occurrence buffers (fed by addInstancedShard)
|
|
27
|
+
instancedVisible = true; // GPU-instancing: hidden in Types view mode (instanced geometry is class-0 occurrences)
|
|
28
|
+
instancedEntityMap = new Map(); // express_id -> occurrences, for per-instance selection/overlay patching
|
|
29
|
+
instancedTemplateCpu = []; // compact CPU geometry per template (index-aligned with instancedTemplates) for CPU consumers
|
|
30
|
+
instancedDevice; // cached for per-instance flag/colour writeBuffer updates
|
|
31
|
+
instancedSelected = new Set(); // currently flag-selected instanced express_ids
|
|
32
|
+
instancedHidden = new Set(); // currently hidden instanced express_ids (hide/isolate)
|
|
33
|
+
instancedOverridden = new Set(); // currently colour-overridden instanced express_ids
|
|
34
|
+
instancedHasTransparent = false; // an override made some instanced occurrence translucent
|
|
35
|
+
lastInstancedHiddenIds = null; // ref-equality guard for setInstancedVisibility
|
|
36
|
+
lastInstancedIsolatedIds = null;
|
|
37
|
+
instancedVisibilityDirty = false; // set when a new shard adds occurrences → re-apply visibility
|
|
22
38
|
// Buffer-size-aware bucket splitting: when a single color group's geometry
|
|
23
39
|
// would exceed the GPU maxBufferSize, overflow is directed to a new
|
|
24
40
|
// sub-bucket with a suffixed key (e.g. "500|500|500|1000#1"). This keeps
|
|
@@ -329,6 +345,16 @@ export class Scene {
|
|
|
329
345
|
visit(piece);
|
|
330
346
|
}
|
|
331
347
|
}
|
|
348
|
+
// Instanced-only occurrences live in the shard, not meshDataMap, so full-
|
|
349
|
+
// geometry CPU consumers (e.g. the deviation BVH) would miss them. Materialize
|
|
350
|
+
// them lazily here — these copies are transient (the caller builds its BVH and
|
|
351
|
+
// discards them), so this does NOT retain the N full copies instancing avoids.
|
|
352
|
+
// Skipped after geometry release (templates freed). (#1238 review)
|
|
353
|
+
if (!this.geometryReleased) {
|
|
354
|
+
for (const piece of this.getAllInstancedMeshData()) {
|
|
355
|
+
visit(piece);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
332
358
|
}
|
|
333
359
|
getMeshDataPieces(expressId, modelIndex) {
|
|
334
360
|
let pieces = this.meshDataMap.get(expressId);
|
|
@@ -508,7 +534,10 @@ export class Scene {
|
|
|
508
534
|
const meshDataList = this.meshDataMap.get(expressId);
|
|
509
535
|
if (!meshDataList || meshDataList.length === 0) {
|
|
510
536
|
this.boundingBoxes.delete(expressId);
|
|
511
|
-
|
|
537
|
+
// Instanced-only entity (lives in the shard, not meshDataMap): without this
|
|
538
|
+
// the GPU occurrence keeps rendering AND picking after delete/split. Tombstone
|
|
539
|
+
// it on the GPU + drop its instanced state. (#1238 review)
|
|
540
|
+
return this.removeInstancedEntity(expressId);
|
|
512
541
|
}
|
|
513
542
|
// Track which buckets need re-batching so we don't repeatedly
|
|
514
543
|
// mark the same key.
|
|
@@ -568,10 +597,37 @@ export class Scene {
|
|
|
568
597
|
// in the buckets and would otherwise linger after a delete/split (same ghost
|
|
569
598
|
// class as a move).
|
|
570
599
|
this.evictHighlightMeshes(expressId);
|
|
600
|
+
// An entity can have BOTH flat meshes and instanced occurrences; clean up the
|
|
601
|
+
// instanced side here too so a mixed entity doesn't keep ghost instances.
|
|
602
|
+
if (this.removeInstancedEntity(expressId))
|
|
603
|
+
removedDedicated = true;
|
|
571
604
|
// True when at least one dedicated mesh was removed — covers
|
|
572
605
|
// the case where a mesh was queued but not yet bucketed.
|
|
573
606
|
return removedDedicated;
|
|
574
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Tombstone a GPU-instanced entity on delete/split: set its HIDDEN flag on the
|
|
610
|
+
* GPU (both render + pick shaders discard it) before forgetting its occurrence
|
|
611
|
+
* locations, then drop all instanced state for it. The occurrence's buffer slots
|
|
612
|
+
* are not reclaimed (delete/split is rare) — hiding them is sufficient and never
|
|
613
|
+
* touches other entities' slots. Returns true if the id was instanced. (#1238)
|
|
614
|
+
*/
|
|
615
|
+
removeInstancedEntity(expressId) {
|
|
616
|
+
if (!this.instancedEntityMap.has(expressId))
|
|
617
|
+
return false;
|
|
618
|
+
const device = this.instancedDevice;
|
|
619
|
+
if (device) {
|
|
620
|
+
// Must set the flag while the occurrence locations are still in the map.
|
|
621
|
+
this.instancedHidden.add(expressId);
|
|
622
|
+
this.writeInstanceFlags(device, expressId);
|
|
623
|
+
}
|
|
624
|
+
this.instancedEntityMap.delete(expressId);
|
|
625
|
+
this.instancedSelected.delete(expressId);
|
|
626
|
+
this.instancedHidden.delete(expressId);
|
|
627
|
+
this.instancedOverridden.delete(expressId);
|
|
628
|
+
this.boundingBoxes.delete(expressId);
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
575
631
|
/**
|
|
576
632
|
* Bulk variant of `removeMeshesForEntity`. Avoids re-marking the
|
|
577
633
|
* same bucket key once per entity in the common "split N walls"
|
|
@@ -1071,6 +1127,9 @@ export class Scene {
|
|
|
1071
1127
|
this.buckets.clear();
|
|
1072
1128
|
this.meshDataBucket = new Map();
|
|
1073
1129
|
this.meshDataMap.clear();
|
|
1130
|
+
// Free the compact instanced template geometry too; the per-occurrence world
|
|
1131
|
+
// AABBs already live in boundingBoxes, so bbox-raycast still finds instanced ids.
|
|
1132
|
+
this.instancedTemplateCpu = [];
|
|
1074
1133
|
this.activeBucketKey.clear();
|
|
1075
1134
|
this.pendingBatchKeys.clear();
|
|
1076
1135
|
for (const batch of this.partialBatchCache.values())
|
|
@@ -1142,6 +1201,11 @@ export class Scene {
|
|
|
1142
1201
|
}
|
|
1143
1202
|
// 2. Clear the heavy data structures — typed arrays become GC-eligible
|
|
1144
1203
|
this.meshDataMap.clear();
|
|
1204
|
+
// Free the compact instanced template geometry; per-occurrence world AABBs are
|
|
1205
|
+
// already cached in boundingBoxes, so the released-path bbox-raycast still
|
|
1206
|
+
// resolves instanced ids (exact-triangle measure/section is unavailable post-
|
|
1207
|
+
// release, same as the flat path).
|
|
1208
|
+
this.instancedTemplateCpu = [];
|
|
1145
1209
|
// Clear meshData arrays in each bucket (typed arrays become GC-eligible)
|
|
1146
1210
|
// but keep the bucket shells so batchedMesh references remain valid
|
|
1147
1211
|
for (const bucket of this.buckets.values()) {
|
|
@@ -1468,10 +1532,12 @@ export class Scene {
|
|
|
1468
1532
|
if (this.geometryReleased) {
|
|
1469
1533
|
console.warn('[Scene] setColorOverrides called after geometry data was released — skipping.');
|
|
1470
1534
|
this.colorOverrides = null;
|
|
1535
|
+
this.setInstancedColorOverrides(null);
|
|
1471
1536
|
return;
|
|
1472
1537
|
}
|
|
1473
1538
|
if (overrides.size === 0) {
|
|
1474
1539
|
this.colorOverrides = null;
|
|
1540
|
+
this.setInstancedColorOverrides(null);
|
|
1475
1541
|
return;
|
|
1476
1542
|
}
|
|
1477
1543
|
// Defensive copy so external callers can mutate or reuse `overrides`
|
|
@@ -1479,6 +1545,10 @@ export class Scene {
|
|
|
1479
1545
|
// frozen by the readonly type — we don't deep-clone the inner arrays
|
|
1480
1546
|
// because they're treated as immutable by every consumer.
|
|
1481
1547
|
this.colorOverrides = new Map(overrides);
|
|
1548
|
+
// Mirror the overlay onto the GPU-instanced occurrences: patch their colour
|
|
1549
|
+
// bytes in place (no separate overlay pass — the instanced records carry the
|
|
1550
|
+
// override colour directly). No-op when no instanced data is loaded.
|
|
1551
|
+
this.setInstancedColorOverrides(overrides);
|
|
1482
1552
|
// Group expressIds by override color
|
|
1483
1553
|
const colorGroups = new Map();
|
|
1484
1554
|
for (const [expressId, color] of overrides) {
|
|
@@ -1513,6 +1583,7 @@ export class Scene {
|
|
|
1513
1583
|
clearColorOverrides() {
|
|
1514
1584
|
this.destroyOverrideBatches();
|
|
1515
1585
|
this.colorOverrides = null;
|
|
1586
|
+
this.setInstancedColorOverrides(null);
|
|
1516
1587
|
}
|
|
1517
1588
|
/** Get overlay batches for rendering */
|
|
1518
1589
|
getOverrideBatches() {
|
|
@@ -1552,6 +1623,438 @@ export class Scene {
|
|
|
1552
1623
|
getTexturedMeshes() {
|
|
1553
1624
|
return this.texturedMeshes;
|
|
1554
1625
|
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Toggle the instanced draw pass. Instanced geometry is class-0 occurrences
|
|
1628
|
+
* (the Model view); hide it in the Types view mode, where the flat path shows
|
|
1629
|
+
* the class-1/2 type library instead. Buffers stay uploaded — just not drawn —
|
|
1630
|
+
* so toggling back is free.
|
|
1631
|
+
*/
|
|
1632
|
+
setInstancedVisible(visible) {
|
|
1633
|
+
this.instancedVisible = visible;
|
|
1634
|
+
}
|
|
1635
|
+
/** GPU-instancing templates for the renderer's instanced draw pass. Empty when
|
|
1636
|
+
* hidden (Types view mode) so the draw loop skips it. */
|
|
1637
|
+
getInstancedTemplates() {
|
|
1638
|
+
if (!this.instancedVisible)
|
|
1639
|
+
return EMPTY_INSTANCED_TEMPLATES;
|
|
1640
|
+
return this.instancedTemplates;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Decode a per-batch IFNS instancing shard (`processGeometryBatchInstanced`)
|
|
1644
|
+
* and upload its templates for GPU-instanced drawing. Each unique geometry
|
|
1645
|
+
* becomes a slot-0 vertex buffer (28-byte pos+norm+entityId, matching the flat
|
|
1646
|
+
* layout — the per-vertex entityId is a 0 placeholder; vs_instanced reads the
|
|
1647
|
+
* per-occurrence id) + index buffer, plus a slot-1 per-instance buffer (mat4 +
|
|
1648
|
+
* entityId + rgba) already composed in the WebGL Y-up frame
|
|
1649
|
+
* (`prepareInstancedRender` folds the Z-up→Y-up swap). No-op on a non-shard or
|
|
1650
|
+
* empty payload. Idempotent only in the sense of "append" — call clear() to
|
|
1651
|
+
* reset on a new model.
|
|
1652
|
+
*
|
|
1653
|
+
* Takes an ALREADY-DECODED shard (the worker/main layer that receives the raw
|
|
1654
|
+
* IFNS bytes owns `decodeInstancedShard`), so this module keeps only a
|
|
1655
|
+
* type-only dependency on @ifc-lite/geometry and the Scene stays focused on
|
|
1656
|
+
* GPU upload.
|
|
1657
|
+
*/
|
|
1658
|
+
addInstancedShard(device, shard) {
|
|
1659
|
+
this.instancedDevice = device; // cached for per-instance selection/overlay writeBuffer
|
|
1660
|
+
const prepared = prepareInstancedRender(shard);
|
|
1661
|
+
for (const t of prepared) {
|
|
1662
|
+
const vcount = Math.floor(t.positions.length / 3);
|
|
1663
|
+
if (vcount === 0 || t.indices.length === 0 || t.instanceCount === 0)
|
|
1664
|
+
continue;
|
|
1665
|
+
// Interleave the template's local positions + normals into the 28-byte
|
|
1666
|
+
// (pos3f + norm3f + entityId u32) vertex layout the instanced pipeline's
|
|
1667
|
+
// slot 0 expects. entityId is a 0 placeholder (vs_instanced ignores it).
|
|
1668
|
+
const vtx = new ArrayBuffer(vcount * 28);
|
|
1669
|
+
const vf = new Float32Array(vtx);
|
|
1670
|
+
for (let i = 0; i < vcount; i++) {
|
|
1671
|
+
const o = i * 7;
|
|
1672
|
+
vf[o + 0] = t.positions[i * 3 + 0];
|
|
1673
|
+
vf[o + 1] = t.positions[i * 3 + 1];
|
|
1674
|
+
vf[o + 2] = t.positions[i * 3 + 2];
|
|
1675
|
+
// normals may be shorter/absent on degenerate meshes — default to 0.
|
|
1676
|
+
vf[o + 3] = i * 3 + 0 < t.normals.length ? t.normals[i * 3 + 0] : 0;
|
|
1677
|
+
vf[o + 4] = i * 3 + 1 < t.normals.length ? t.normals[i * 3 + 1] : 0;
|
|
1678
|
+
vf[o + 5] = i * 3 + 2 < t.normals.length ? t.normals[i * 3 + 2] : 0;
|
|
1679
|
+
// vf[o + 6] (entityId lane) stays 0.
|
|
1680
|
+
}
|
|
1681
|
+
const vertexBuffer = device.createBuffer({
|
|
1682
|
+
size: vtx.byteLength,
|
|
1683
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1684
|
+
mappedAtCreation: true,
|
|
1685
|
+
});
|
|
1686
|
+
new Uint8Array(vertexBuffer.getMappedRange()).set(new Uint8Array(vtx));
|
|
1687
|
+
vertexBuffer.unmap();
|
|
1688
|
+
const indexBuffer = device.createBuffer({
|
|
1689
|
+
size: t.indices.byteLength,
|
|
1690
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1691
|
+
mappedAtCreation: true,
|
|
1692
|
+
});
|
|
1693
|
+
new Uint32Array(indexBuffer.getMappedRange()).set(t.indices);
|
|
1694
|
+
indexBuffer.unmap();
|
|
1695
|
+
// instanceBuffer is already the interleaved mat4 + entityId + rgba block
|
|
1696
|
+
// (INSTANCE_STRIDE_BYTES per occurrence) from prepareInstancedRender.
|
|
1697
|
+
const instSize = t.instanceCount * INSTANCE_STRIDE_BYTES;
|
|
1698
|
+
const instanceBuffer = device.createBuffer({
|
|
1699
|
+
size: instSize,
|
|
1700
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1701
|
+
mappedAtCreation: true,
|
|
1702
|
+
});
|
|
1703
|
+
new Uint8Array(instanceBuffer.getMappedRange()).set(new Uint8Array(t.instanceBuffer, 0, instSize));
|
|
1704
|
+
instanceBuffer.unmap();
|
|
1705
|
+
const templateIndex = this.instancedTemplates.length;
|
|
1706
|
+
this.instancedTemplates.push({
|
|
1707
|
+
vertexBuffer,
|
|
1708
|
+
indexBuffer,
|
|
1709
|
+
indexCount: t.indices.length,
|
|
1710
|
+
instanceBuffer,
|
|
1711
|
+
instanceCount: t.instanceCount,
|
|
1712
|
+
});
|
|
1713
|
+
// Template-local AABB (used to derive per-occurrence world AABBs cheaply).
|
|
1714
|
+
let lmnx = Infinity, lmny = Infinity, lmnz = Infinity;
|
|
1715
|
+
let lmxx = -Infinity, lmxy = -Infinity, lmxz = -Infinity;
|
|
1716
|
+
for (let i = 0; i < t.positions.length; i += 3) {
|
|
1717
|
+
const x = t.positions[i], y = t.positions[i + 1], z = t.positions[i + 2];
|
|
1718
|
+
if (x < lmnx)
|
|
1719
|
+
lmnx = x;
|
|
1720
|
+
if (y < lmny)
|
|
1721
|
+
lmny = y;
|
|
1722
|
+
if (z < lmnz)
|
|
1723
|
+
lmnz = z;
|
|
1724
|
+
if (x > lmxx)
|
|
1725
|
+
lmxx = x;
|
|
1726
|
+
if (y > lmxy)
|
|
1727
|
+
lmxy = y;
|
|
1728
|
+
if (z > lmxz)
|
|
1729
|
+
lmxz = z;
|
|
1730
|
+
}
|
|
1731
|
+
// Retain the compact CPU geometry + the packed instance records (mat4 per
|
|
1732
|
+
// occurrence) so CPU consumers can reach instanced geometry without a full
|
|
1733
|
+
// per-occurrence MeshData each. These are references into the decoded shard.
|
|
1734
|
+
this.instancedTemplateCpu.push({
|
|
1735
|
+
positions: t.positions,
|
|
1736
|
+
normals: t.normals,
|
|
1737
|
+
indices: t.indices,
|
|
1738
|
+
instanceData: t.instanceBuffer,
|
|
1739
|
+
localMin: [lmnx, lmny, lmnz],
|
|
1740
|
+
localMax: [lmxx, lmxy, lmxz],
|
|
1741
|
+
});
|
|
1742
|
+
// Map each occurrence's express_id -> (template, byte offset, original
|
|
1743
|
+
// colour) so selection (flag byte) + lens/IDS overlays (colour bytes) can
|
|
1744
|
+
// patch individual occurrences via writeBuffer without a re-upload. Also fold
|
|
1745
|
+
// each occurrence's world AABB (template local box × its mat4) into
|
|
1746
|
+
// boundingBoxes so getEntityBoundingBox + the CPU raycast-bounds path
|
|
1747
|
+
// (BCF anchors, large/released-model picking, frame-selection) see instanced
|
|
1748
|
+
// geometry. (Templates with no finite local box — empty geometry — are skipped.)
|
|
1749
|
+
const cdv = new DataView(t.instanceBuffer);
|
|
1750
|
+
const haveBox = Number.isFinite(lmnx);
|
|
1751
|
+
for (let i = 0; i < t.instanceCount; i++) {
|
|
1752
|
+
const byteOffset = i * INSTANCE_STRIDE_BYTES;
|
|
1753
|
+
const eid = t.entityIds[i];
|
|
1754
|
+
const originalColor = [
|
|
1755
|
+
cdv.getFloat32(byteOffset + INSTANCE_COLOR_OFFSET, true),
|
|
1756
|
+
cdv.getFloat32(byteOffset + INSTANCE_COLOR_OFFSET + 4, true),
|
|
1757
|
+
cdv.getFloat32(byteOffset + INSTANCE_COLOR_OFFSET + 8, true),
|
|
1758
|
+
cdv.getFloat32(byteOffset + INSTANCE_COLOR_OFFSET + 12, true),
|
|
1759
|
+
];
|
|
1760
|
+
let arr = this.instancedEntityMap.get(eid);
|
|
1761
|
+
if (!arr) {
|
|
1762
|
+
arr = [];
|
|
1763
|
+
this.instancedEntityMap.set(eid, arr);
|
|
1764
|
+
}
|
|
1765
|
+
arr.push({ templateIndex, byteOffset, originalColor });
|
|
1766
|
+
if (haveBox) {
|
|
1767
|
+
this.unionInstancedWorldAabb(eid, cdv, byteOffset, lmnx, lmny, lmnz, lmxx, lmxy, lmxz);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
// New occurrences default to flags=0 (visible). Force the next setInstancedVisibility
|
|
1772
|
+
// to recompute so an already-active isolate/hide also applies to geometry that
|
|
1773
|
+
// streamed in after the visibility was set.
|
|
1774
|
+
this.instancedVisibilityDirty = true;
|
|
1775
|
+
}
|
|
1776
|
+
/** Transform a template's local AABB by an occurrence's column-major mat4 (read
|
|
1777
|
+
* from the packed instance record at `matOffset`) and union the world box into
|
|
1778
|
+
* boundingBoxes[eid]. */
|
|
1779
|
+
unionInstancedWorldAabb(eid, dv, matOffset, lmnx, lmny, lmnz, lmxx, lmxy, lmxz) {
|
|
1780
|
+
const m0 = dv.getFloat32(matOffset + 0, true), m1 = dv.getFloat32(matOffset + 4, true), m2 = dv.getFloat32(matOffset + 8, true);
|
|
1781
|
+
const m4 = dv.getFloat32(matOffset + 16, true), m5 = dv.getFloat32(matOffset + 20, true), m6 = dv.getFloat32(matOffset + 24, true);
|
|
1782
|
+
const m8 = dv.getFloat32(matOffset + 32, true), m9 = dv.getFloat32(matOffset + 36, true), m10 = dv.getFloat32(matOffset + 40, true);
|
|
1783
|
+
const m12 = dv.getFloat32(matOffset + 48, true), m13 = dv.getFloat32(matOffset + 52, true), m14 = dv.getFloat32(matOffset + 56, true);
|
|
1784
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
1785
|
+
for (let c = 0; c < 8; c++) {
|
|
1786
|
+
const x = (c & 1) ? lmxx : lmnx, y = (c & 2) ? lmxy : lmny, z = (c & 4) ? lmxz : lmnz;
|
|
1787
|
+
const wx = m0 * x + m4 * y + m8 * z + m12;
|
|
1788
|
+
const wy = m1 * x + m5 * y + m9 * z + m13;
|
|
1789
|
+
const wz = m2 * x + m6 * y + m10 * z + m14;
|
|
1790
|
+
if (wx < minX)
|
|
1791
|
+
minX = wx;
|
|
1792
|
+
if (wy < minY)
|
|
1793
|
+
minY = wy;
|
|
1794
|
+
if (wz < minZ)
|
|
1795
|
+
minZ = wz;
|
|
1796
|
+
if (wx > maxX)
|
|
1797
|
+
maxX = wx;
|
|
1798
|
+
if (wy > maxY)
|
|
1799
|
+
maxY = wy;
|
|
1800
|
+
if (wz > maxZ)
|
|
1801
|
+
maxZ = wz;
|
|
1802
|
+
}
|
|
1803
|
+
const existing = this.boundingBoxes.get(eid);
|
|
1804
|
+
if (existing) {
|
|
1805
|
+
existing.min.x = Math.min(existing.min.x, minX);
|
|
1806
|
+
existing.min.y = Math.min(existing.min.y, minY);
|
|
1807
|
+
existing.min.z = Math.min(existing.min.z, minZ);
|
|
1808
|
+
existing.max.x = Math.max(existing.max.x, maxX);
|
|
1809
|
+
existing.max.y = Math.max(existing.max.y, maxY);
|
|
1810
|
+
existing.max.z = Math.max(existing.max.z, maxZ);
|
|
1811
|
+
}
|
|
1812
|
+
else {
|
|
1813
|
+
this.boundingBoxes.set(eid, { min: { x: minX, y: minY, z: minZ }, max: { x: maxX, y: maxY, z: maxZ } });
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
/** True if `expressId` is a GPU-instanced occurrence (lives only in the instanced
|
|
1817
|
+
* shard, not the flat meshDataMap). CPU consumers use this to decide whether to
|
|
1818
|
+
* fall back to the instanced accessors below. */
|
|
1819
|
+
isInstancedEntity(expressId) {
|
|
1820
|
+
return this.instancedEntityMap.has(expressId);
|
|
1821
|
+
}
|
|
1822
|
+
/** All instanced occurrence express_ids (for CPU consumers that enumerate geometry,
|
|
1823
|
+
* e.g. the raycast-engine and exporters). */
|
|
1824
|
+
getInstancedEntityIds() {
|
|
1825
|
+
return this.instancedEntityMap.keys();
|
|
1826
|
+
}
|
|
1827
|
+
/** Materialize EVERY instanced occurrence as world-space MeshData. Transient + not
|
|
1828
|
+
* retained — for one-shot full-geometry consumers (glTF / IFC5 export) that must
|
|
1829
|
+
* include the instanced occurrences absent from geometryResult.meshes. Returns []
|
|
1830
|
+
* when no instanced data is loaded or after geometry release (templates freed). */
|
|
1831
|
+
getAllInstancedMeshData() {
|
|
1832
|
+
const out = [];
|
|
1833
|
+
for (const eid of this.instancedEntityMap.keys()) {
|
|
1834
|
+
const pieces = this.getInstancedMeshDataPieces(eid);
|
|
1835
|
+
if (pieces)
|
|
1836
|
+
out.push(...pieces);
|
|
1837
|
+
}
|
|
1838
|
+
return out;
|
|
1839
|
+
}
|
|
1840
|
+
/** World-space AABB for an instanced occurrence (union over its occurrences),
|
|
1841
|
+
* or null if not instanced. Populated at upload time, so this is O(1). */
|
|
1842
|
+
getInstancedEntityBounds(expressId) {
|
|
1843
|
+
if (!this.instancedEntityMap.has(expressId))
|
|
1844
|
+
return null;
|
|
1845
|
+
return this.boundingBoxes.get(expressId) ?? null;
|
|
1846
|
+
}
|
|
1847
|
+
/** Lazily materialize per-occurrence world-space MeshData for an instanced entity
|
|
1848
|
+
* (template geometry × each occurrence's matrix). NOT retained — built on demand
|
|
1849
|
+
* for CPU consumers that need triangles (exact raycast / measure / section-face /
|
|
1850
|
+
* export). Returns undefined if the id is not instanced. */
|
|
1851
|
+
getInstancedMeshDataPieces(expressId) {
|
|
1852
|
+
const occ = this.instancedEntityMap.get(expressId);
|
|
1853
|
+
if (!occ || occ.length === 0)
|
|
1854
|
+
return undefined;
|
|
1855
|
+
const out = [];
|
|
1856
|
+
for (const o of occ) {
|
|
1857
|
+
const tpl = this.instancedTemplateCpu[o.templateIndex];
|
|
1858
|
+
if (!tpl || tpl.positions.length === 0)
|
|
1859
|
+
continue;
|
|
1860
|
+
const dv = new DataView(tpl.instanceData);
|
|
1861
|
+
const b = o.byteOffset;
|
|
1862
|
+
const m0 = dv.getFloat32(b + 0, true), m1 = dv.getFloat32(b + 4, true), m2 = dv.getFloat32(b + 8, true);
|
|
1863
|
+
const m4 = dv.getFloat32(b + 16, true), m5 = dv.getFloat32(b + 20, true), m6 = dv.getFloat32(b + 24, true);
|
|
1864
|
+
const m8 = dv.getFloat32(b + 32, true), m9 = dv.getFloat32(b + 36, true), m10 = dv.getFloat32(b + 40, true);
|
|
1865
|
+
const m12 = dv.getFloat32(b + 48, true), m13 = dv.getFloat32(b + 52, true), m14 = dv.getFloat32(b + 56, true);
|
|
1866
|
+
const n = tpl.positions.length;
|
|
1867
|
+
const positions = new Float32Array(n);
|
|
1868
|
+
const normals = new Float32Array(tpl.normals.length);
|
|
1869
|
+
for (let i = 0; i < n; i += 3) {
|
|
1870
|
+
const x = tpl.positions[i], y = tpl.positions[i + 1], z = tpl.positions[i + 2];
|
|
1871
|
+
positions[i] = m0 * x + m4 * y + m8 * z + m12;
|
|
1872
|
+
positions[i + 1] = m1 * x + m5 * y + m9 * z + m13;
|
|
1873
|
+
positions[i + 2] = m2 * x + m6 * y + m10 * z + m14;
|
|
1874
|
+
if (i + 2 < tpl.normals.length) {
|
|
1875
|
+
// Rotate normals by the upper-3×3 (instancing transforms are rigid +
|
|
1876
|
+
// uniform scale, so this is correct up to a renormalize).
|
|
1877
|
+
const nx = tpl.normals[i], ny = tpl.normals[i + 1], nz = tpl.normals[i + 2];
|
|
1878
|
+
let rx = m0 * nx + m4 * ny + m8 * nz;
|
|
1879
|
+
let ry = m1 * nx + m5 * ny + m9 * nz;
|
|
1880
|
+
let rz = m2 * nx + m6 * ny + m10 * nz;
|
|
1881
|
+
const len = Math.hypot(rx, ry, rz) || 1;
|
|
1882
|
+
rx /= len;
|
|
1883
|
+
ry /= len;
|
|
1884
|
+
rz /= len;
|
|
1885
|
+
normals[i] = rx;
|
|
1886
|
+
normals[i + 1] = ry;
|
|
1887
|
+
normals[i + 2] = rz;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
const color = [...o.originalColor];
|
|
1891
|
+
out.push({ expressId, positions, normals, indices: tpl.indices, color });
|
|
1892
|
+
}
|
|
1893
|
+
return out.length > 0 ? out : undefined;
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* Per-instance SELECTION: highlight the occurrences of `expressIds` by setting
|
|
1897
|
+
* their flag byte (bit 0) and clearing the previously-selected ones. The shader
|
|
1898
|
+
* (vs_instanced -> fs_main) applies the blue highlight per occurrence, so no
|
|
1899
|
+
* re-draw is needed. No-op until a shard has been uploaded.
|
|
1900
|
+
*/
|
|
1901
|
+
setInstancedSelection(expressIds) {
|
|
1902
|
+
const device = this.instancedDevice;
|
|
1903
|
+
if (!device || this.instancedTemplates.length === 0)
|
|
1904
|
+
return;
|
|
1905
|
+
// Called every render frame from the renderer. Fast-path an UNCHANGED selection
|
|
1906
|
+
// (the common orbit case, especially the empty set) so we skip both the per-frame
|
|
1907
|
+
// writeBuffer loops AND the `new Set(...)` allocation — equal sizes + full
|
|
1908
|
+
// containment ⇒ set equality.
|
|
1909
|
+
let changed = expressIds.size !== this.instancedSelected.size;
|
|
1910
|
+
if (!changed) {
|
|
1911
|
+
for (const eid of expressIds) {
|
|
1912
|
+
if (!this.instancedSelected.has(eid)) {
|
|
1913
|
+
changed = true;
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (!changed)
|
|
1919
|
+
return;
|
|
1920
|
+
// Re-derive the combined flag lane (selected | hidden) for every occurrence whose
|
|
1921
|
+
// selected-membership flips, so we never clobber the hidden bit.
|
|
1922
|
+
const prev = this.instancedSelected;
|
|
1923
|
+
this.instancedSelected = new Set(expressIds);
|
|
1924
|
+
for (const eid of prev) {
|
|
1925
|
+
if (!expressIds.has(eid))
|
|
1926
|
+
this.writeInstanceFlags(device, eid);
|
|
1927
|
+
}
|
|
1928
|
+
for (const eid of expressIds) {
|
|
1929
|
+
if (!prev.has(eid))
|
|
1930
|
+
this.writeInstanceFlags(device, eid);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Per-instance VISIBILITY (hide / isolate): set the hidden flag bit on occurrences
|
|
1935
|
+
* that should not render, mirroring the flat path's hiddenIds/isolatedIds filter.
|
|
1936
|
+
* The shader discards hidden occurrences in BOTH the render and pick passes, so they
|
|
1937
|
+
* neither draw nor are pickable. `isolatedIds != null` means "show only these"; any
|
|
1938
|
+
* occurrence not in the set is hidden. Diffed so an unchanged visibility set is a
|
|
1939
|
+
* no-op (no writeBuffer). No-op until a shard has been uploaded.
|
|
1940
|
+
*/
|
|
1941
|
+
setInstancedVisibility(hiddenIds, isolatedIds) {
|
|
1942
|
+
const device = this.instancedDevice;
|
|
1943
|
+
if (!device || this.instancedTemplates.length === 0)
|
|
1944
|
+
return;
|
|
1945
|
+
// Called every render frame. The viewer passes stable Set references that only
|
|
1946
|
+
// change when visibility changes, so a reference-equality guard skips the O(N)
|
|
1947
|
+
// set rebuild + allocation during orbit (the common, unchanged case). The dirty
|
|
1948
|
+
// flag forces a recompute after a new shard adds occurrences mid-stream, so an
|
|
1949
|
+
// active isolate/hide also applies to geometry that streams in afterwards.
|
|
1950
|
+
if (!this.instancedVisibilityDirty &&
|
|
1951
|
+
hiddenIds === this.lastInstancedHiddenIds &&
|
|
1952
|
+
isolatedIds === this.lastInstancedIsolatedIds) {
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
this.instancedVisibilityDirty = false;
|
|
1956
|
+
this.lastInstancedHiddenIds = hiddenIds ?? null;
|
|
1957
|
+
this.lastInstancedIsolatedIds = isolatedIds ?? null;
|
|
1958
|
+
const isHidden = (eid) => (hiddenIds != null && hiddenIds.has(eid)) ||
|
|
1959
|
+
(isolatedIds != null && !isolatedIds.has(eid));
|
|
1960
|
+
// Recompute the effective hidden set over all instanced occurrences and diff vs
|
|
1961
|
+
// the current one; only flips touch the GPU buffer.
|
|
1962
|
+
const next = new Set();
|
|
1963
|
+
for (const eid of this.instancedEntityMap.keys()) {
|
|
1964
|
+
if (isHidden(eid))
|
|
1965
|
+
next.add(eid);
|
|
1966
|
+
}
|
|
1967
|
+
// Fast-path: unchanged hidden set → nothing to write.
|
|
1968
|
+
let changed = next.size !== this.instancedHidden.size;
|
|
1969
|
+
if (!changed) {
|
|
1970
|
+
for (const eid of next) {
|
|
1971
|
+
if (!this.instancedHidden.has(eid)) {
|
|
1972
|
+
changed = true;
|
|
1973
|
+
break;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (!changed)
|
|
1978
|
+
return;
|
|
1979
|
+
const prev = this.instancedHidden;
|
|
1980
|
+
this.instancedHidden = next;
|
|
1981
|
+
for (const eid of prev) {
|
|
1982
|
+
if (!next.has(eid))
|
|
1983
|
+
this.writeInstanceFlags(device, eid);
|
|
1984
|
+
}
|
|
1985
|
+
for (const eid of next) {
|
|
1986
|
+
if (!prev.has(eid))
|
|
1987
|
+
this.writeInstanceFlags(device, eid);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Per-instance COLOUR OVERRIDE (lens / IDS / compare / 4D): patch the colour
|
|
1992
|
+
* bytes of the affected occurrences in place; occurrences dropped from the map
|
|
1993
|
+
* are restored to their original colour. Pass null/empty to clear all.
|
|
1994
|
+
*/
|
|
1995
|
+
setInstancedColorOverrides(overrides) {
|
|
1996
|
+
const device = this.instancedDevice;
|
|
1997
|
+
if (!device || this.instancedTemplates.length === 0)
|
|
1998
|
+
return;
|
|
1999
|
+
const next = overrides ?? new Map();
|
|
2000
|
+
for (const eid of this.instancedOverridden) {
|
|
2001
|
+
if (!next.has(eid))
|
|
2002
|
+
this.restoreInstanceColor(device, eid);
|
|
2003
|
+
}
|
|
2004
|
+
let hasTransparent = false;
|
|
2005
|
+
for (const [eid, rgba] of next) {
|
|
2006
|
+
this.writeInstanceColor(device, eid, rgba);
|
|
2007
|
+
// Instanced occurrences are opaque by partition; only an override can drop alpha
|
|
2008
|
+
// below the cutoff (lens-ghost / x-ray / compare). Track it so the renderer runs
|
|
2009
|
+
// the transparent instanced sub-pass only when something is actually translucent.
|
|
2010
|
+
if (rgba[3] < OPAQUE_ALPHA_CUTOFF)
|
|
2011
|
+
hasTransparent = true;
|
|
2012
|
+
}
|
|
2013
|
+
this.instancedOverridden = new Set(next.keys());
|
|
2014
|
+
this.instancedHasTransparent = hasTransparent;
|
|
2015
|
+
}
|
|
2016
|
+
/** True when an active colour override made some instanced occurrence translucent,
|
|
2017
|
+
* so the renderer should run the transparent instanced sub-pass. */
|
|
2018
|
+
hasTransparentInstances() {
|
|
2019
|
+
return this.instancedHasTransparent;
|
|
2020
|
+
}
|
|
2021
|
+
/** Write the combined flag lane (selected | hidden) for every occurrence of `eid`.
|
|
2022
|
+
* Folding both bits here means selection and visibility updates never clobber each
|
|
2023
|
+
* other (they share the one u32 flags lane at INSTANCE_FLAGS_OFFSET). */
|
|
2024
|
+
writeInstanceFlags(device, eid) {
|
|
2025
|
+
const locs = this.instancedEntityMap.get(eid);
|
|
2026
|
+
if (!locs)
|
|
2027
|
+
return;
|
|
2028
|
+
const flags = (this.instancedSelected.has(eid) ? INSTANCE_FLAG_SELECTED : 0) |
|
|
2029
|
+
(this.instancedHidden.has(eid) ? INSTANCE_FLAG_HIDDEN : 0);
|
|
2030
|
+
const data = new Uint32Array([flags >>> 0]);
|
|
2031
|
+
for (const loc of locs) {
|
|
2032
|
+
const buf = this.instancedTemplates[loc.templateIndex]?.instanceBuffer;
|
|
2033
|
+
if (buf)
|
|
2034
|
+
device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_FLAGS_OFFSET, data);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
writeInstanceColor(device, eid, rgba) {
|
|
2038
|
+
const locs = this.instancedEntityMap.get(eid);
|
|
2039
|
+
if (!locs)
|
|
2040
|
+
return;
|
|
2041
|
+
const data = new Float32Array([rgba[0], rgba[1], rgba[2], rgba[3]]);
|
|
2042
|
+
for (const loc of locs) {
|
|
2043
|
+
const buf = this.instancedTemplates[loc.templateIndex]?.instanceBuffer;
|
|
2044
|
+
if (buf)
|
|
2045
|
+
device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_COLOR_OFFSET, data);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
restoreInstanceColor(device, eid) {
|
|
2049
|
+
const locs = this.instancedEntityMap.get(eid);
|
|
2050
|
+
if (!locs)
|
|
2051
|
+
return;
|
|
2052
|
+
for (const loc of locs) {
|
|
2053
|
+
const buf = this.instancedTemplates[loc.templateIndex]?.instanceBuffer;
|
|
2054
|
+
if (buf)
|
|
2055
|
+
device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_COLOR_OFFSET, new Float32Array(loc.originalColor));
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
1555
2058
|
/**
|
|
1556
2059
|
* Build a textured mesh (#961): interleave position+normal+entityId+uv into one
|
|
1557
2060
|
* vertex buffer, upload the decoded RGBA8 texture, create a sampler honouring
|
|
@@ -1578,6 +2081,10 @@ export class Scene {
|
|
|
1578
2081
|
const f = new Float32Array(interleaved);
|
|
1579
2082
|
const u = new Uint32Array(interleaved);
|
|
1580
2083
|
const entityIds = meshData.entityIds;
|
|
2084
|
+
// Match mergeGeometry's entityId-lane packing so an overlay (lens/IDS/...)
|
|
2085
|
+
// drawn over a textured mesh computes the same z-nudge → depthCompare:'equal'
|
|
2086
|
+
// matches. High 8 bits = colour salt, low 24 = picking id.
|
|
2087
|
+
const saltByte = colorSaltByte(meshData.color);
|
|
1581
2088
|
for (let i = 0; i < vertexCount; i++) {
|
|
1582
2089
|
const o = i * 9;
|
|
1583
2090
|
f[o] = positions[i * 3];
|
|
@@ -1586,7 +2093,7 @@ export class Scene {
|
|
|
1586
2093
|
f[o + 3] = normals[i * 3] ?? 0;
|
|
1587
2094
|
f[o + 4] = normals[i * 3 + 1] ?? 0;
|
|
1588
2095
|
f[o + 5] = normals[i * 3 + 2] ?? 0;
|
|
1589
|
-
u[o + 6] = entityIds ? entityIds[i] : meshData.expressId;
|
|
2096
|
+
u[o + 6] = packEntityLane(entityIds ? entityIds[i] : meshData.expressId, saltByte);
|
|
1590
2097
|
f[o + 7] = uvs[i * 2] ?? 0;
|
|
1591
2098
|
f[o + 8] = uvs[i * 2 + 1] ?? 0;
|
|
1592
2099
|
}
|
|
@@ -1652,6 +2159,23 @@ export class Scene {
|
|
|
1652
2159
|
tm.texture.destroy();
|
|
1653
2160
|
}
|
|
1654
2161
|
this.texturedMeshes = [];
|
|
2162
|
+
// GPU-instancing templates own their vertex/index/instance buffers.
|
|
2163
|
+
for (const it of this.instancedTemplates) {
|
|
2164
|
+
it.vertexBuffer.destroy();
|
|
2165
|
+
it.indexBuffer.destroy();
|
|
2166
|
+
it.instanceBuffer.destroy();
|
|
2167
|
+
}
|
|
2168
|
+
this.instancedTemplates = [];
|
|
2169
|
+
this.instancedTemplateCpu = [];
|
|
2170
|
+
this.instancedEntityMap.clear();
|
|
2171
|
+
this.instancedSelected.clear();
|
|
2172
|
+
this.instancedHidden.clear();
|
|
2173
|
+
this.instancedOverridden.clear();
|
|
2174
|
+
this.instancedHasTransparent = false;
|
|
2175
|
+
this.lastInstancedHiddenIds = null;
|
|
2176
|
+
this.lastInstancedIsolatedIds = null;
|
|
2177
|
+
this.instancedVisibilityDirty = false;
|
|
2178
|
+
this.instancedDevice = undefined;
|
|
1655
2179
|
// Clear partial batch cache
|
|
1656
2180
|
for (const batch of this.partialBatchCache.values())
|
|
1657
2181
|
destroyGpuResources(batch);
|
|
@@ -1754,9 +2278,17 @@ export class Scene {
|
|
|
1754
2278
|
*/
|
|
1755
2279
|
getAllMeshDataExpressIds() {
|
|
1756
2280
|
if (this.geometryReleased) {
|
|
2281
|
+
// boundingBoxes already includes instanced occurrences (their AABBs are
|
|
2282
|
+
// stored there at upload), so the released path needs no extra union.
|
|
1757
2283
|
return Array.from(this.boundingBoxes.keys());
|
|
1758
2284
|
}
|
|
1759
|
-
|
|
2285
|
+
// Union instanced-only occurrences (absent from meshDataMap) so CPU consumers
|
|
2286
|
+
// enumerating geometry see them too. IDs only — no geometry materialized.
|
|
2287
|
+
// (#1238 review)
|
|
2288
|
+
const ids = new Set(this.meshDataMap.keys());
|
|
2289
|
+
for (const eid of this.instancedEntityMap.keys())
|
|
2290
|
+
ids.add(eid);
|
|
2291
|
+
return Array.from(ids);
|
|
1760
2292
|
}
|
|
1761
2293
|
/**
|
|
1762
2294
|
* Get or compute bounding box for an entity from its mesh vertex data.
|
|
@@ -1819,7 +2351,34 @@ export class Scene {
|
|
|
1819
2351
|
return raycastBoundingBoxes(rayOrigin, rayDirInv, rayDirSign, this.boundingBoxes, hiddenIds, isolatedIds);
|
|
1820
2352
|
}
|
|
1821
2353
|
// Full triangle-level raycast with bounding-box pre-filter
|
|
1822
|
-
|
|
2354
|
+
const flatHit = raycastTriangles(rayOrigin, rayDir, rayDirInv, rayDirSign, this.meshDataMap, (id) => this.getEntityBoundingBox(id), hiddenIds, isolatedIds);
|
|
2355
|
+
// Instanced-only occurrences live in the shard, not meshDataMap, so the CPU
|
|
2356
|
+
// pick fallback would miss them. Materialize triangles lazily ONLY for
|
|
2357
|
+
// entities whose world AABB the ray actually hits (never the whole instanced
|
|
2358
|
+
// population), then return whichever hit is closer. (#1238 review)
|
|
2359
|
+
let instancedHit = null;
|
|
2360
|
+
if (this.instancedEntityMap.size > 0) {
|
|
2361
|
+
const instancedMap = new Map();
|
|
2362
|
+
for (const eid of this.instancedEntityMap.keys()) {
|
|
2363
|
+
if (hiddenIds?.has(eid))
|
|
2364
|
+
continue;
|
|
2365
|
+
if (isolatedIds != null && !isolatedIds.has(eid))
|
|
2366
|
+
continue;
|
|
2367
|
+
const bounds = this.getInstancedEntityBounds(eid);
|
|
2368
|
+
if (!bounds || !rayIntersectsBox(rayOrigin, rayDirInv, rayDirSign, bounds))
|
|
2369
|
+
continue;
|
|
2370
|
+
const pieces = this.getInstancedMeshDataPieces(eid);
|
|
2371
|
+
if (pieces && pieces.length > 0)
|
|
2372
|
+
instancedMap.set(eid, pieces);
|
|
2373
|
+
}
|
|
2374
|
+
if (instancedMap.size > 0) {
|
|
2375
|
+
instancedHit = raycastTriangles(rayOrigin, rayDir, rayDirInv, rayDirSign, instancedMap, (id) => this.getInstancedEntityBounds(id), hiddenIds, isolatedIds);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
if (flatHit && instancedHit) {
|
|
2379
|
+
return instancedHit.distance < flatHit.distance ? instancedHit : flatHit;
|
|
2380
|
+
}
|
|
2381
|
+
return flatHit ?? instancedHit;
|
|
1823
2382
|
}
|
|
1824
2383
|
}
|
|
1825
2384
|
//# sourceMappingURL=scene.js.map
|