@ifc-lite/renderer 1.28.4 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -8
- 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.d.ts +143 -0
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +653 -4
- 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 +87 -3
- 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/dist/types.d.ts +19 -0
- package/dist/types.d.ts.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';
|
|
5
|
+
import { prepareRayDirInv, raycastBoundingBoxes, raycastTriangles, rayIntersectsBox, } from './scene-raycaster.js';
|
|
6
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"
|
|
@@ -608,6 +664,24 @@ export class Scene {
|
|
|
608
664
|
* to a full reload if needed.
|
|
609
665
|
*/
|
|
610
666
|
translateMeshesForEntity(expressId, delta) {
|
|
667
|
+
// An entity can have flat meshes, GPU-instanced occurrences, or both. The
|
|
668
|
+
// instanced occurrences live in the per-template instance buffers, NOT in
|
|
669
|
+
// meshDataMap, so the flat path below can't reach them — without this they
|
|
670
|
+
// are "left behind" when a storey lifts in Exploded mode (#1289).
|
|
671
|
+
//
|
|
672
|
+
// Flat runs FIRST because it deletes the entity's cached world AABB; the
|
|
673
|
+
// instanced pass runs last and rebuilds that AABB from the moved occurrence
|
|
674
|
+
// matrices, so a mixed flat+instanced entity never ends up with stranded
|
|
675
|
+
// (null) instanced bounds.
|
|
676
|
+
const flatMoved = this.translateFlatMeshesForEntity(expressId, delta);
|
|
677
|
+
const instancedMoved = this.translateInstancedEntity(expressId, delta);
|
|
678
|
+
return flatMoved || instancedMoved;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Translate every flat (non-instanced) mesh for `expressId` by `delta`. See
|
|
682
|
+
* {@link translateMeshesForEntity} for the full contract; this is the flat half.
|
|
683
|
+
*/
|
|
684
|
+
translateFlatMeshesForEntity(expressId, delta) {
|
|
611
685
|
const meshDataList = this.meshDataMap.get(expressId);
|
|
612
686
|
if (!meshDataList || meshDataList.length === 0)
|
|
613
687
|
return false;
|
|
@@ -676,6 +750,82 @@ export class Scene {
|
|
|
676
750
|
}
|
|
677
751
|
return true;
|
|
678
752
|
}
|
|
753
|
+
/**
|
|
754
|
+
* Translate every GPU-instanced occurrence of `expressId` by `delta` in the
|
|
755
|
+
* renderer world frame. Instanced occurrences live in the per-template instance
|
|
756
|
+
* buffers (NOT meshDataMap), so the flat translate path can't reach them — this
|
|
757
|
+
* is what keeps repeated geometry (e.g. windows / mullions emitted via
|
|
758
|
+
* IfcMappedItem) lifting with its storey in Exploded mode (#1289).
|
|
759
|
+
*
|
|
760
|
+
* Mutates BOTH halves so every consumer stays consistent:
|
|
761
|
+
* - the CPU instance record (so getInstancedMeshDataPieces / bounds / raycast
|
|
762
|
+
* / measure / section / export see the new position), and
|
|
763
|
+
* - the GPU instance buffer (so the occurrence renders at the new position).
|
|
764
|
+
* The cached world AABB is shifted by the same delta — every occurrence of the
|
|
765
|
+
* entity moves identically, so a recompute is unnecessary.
|
|
766
|
+
*
|
|
767
|
+
* Returns `true` when at least one occurrence moved. No-op (returns false) for
|
|
768
|
+
* a non-instanced id or a zero delta.
|
|
769
|
+
*/
|
|
770
|
+
translateInstancedEntity(expressId, delta) {
|
|
771
|
+
const occurrences = this.instancedEntityMap.get(expressId);
|
|
772
|
+
if (!occurrences || occurrences.length === 0)
|
|
773
|
+
return false;
|
|
774
|
+
const [dx, dy, dz] = delta;
|
|
775
|
+
if (dx === 0 && dy === 0 && dz === 0)
|
|
776
|
+
return false;
|
|
777
|
+
const device = this.instancedDevice;
|
|
778
|
+
let moved = false;
|
|
779
|
+
for (const occ of occurrences) {
|
|
780
|
+
const cpu = this.instancedTemplateCpu[occ.templateIndex];
|
|
781
|
+
if (!cpu)
|
|
782
|
+
continue;
|
|
783
|
+
// Column-major mat4: the translation column is floats 12,13,14, i.e. bytes
|
|
784
|
+
// +48/+52/+56 of the occurrence's record (matches unionInstancedWorldAabb).
|
|
785
|
+
const dv = new DataView(cpu.instanceData);
|
|
786
|
+
const b = occ.byteOffset;
|
|
787
|
+
const tx = dv.getFloat32(b + 48, true) + dx;
|
|
788
|
+
const ty = dv.getFloat32(b + 52, true) + dy;
|
|
789
|
+
const tz = dv.getFloat32(b + 56, true) + dz;
|
|
790
|
+
dv.setFloat32(b + 48, tx, true);
|
|
791
|
+
dv.setFloat32(b + 52, ty, true);
|
|
792
|
+
dv.setFloat32(b + 56, tz, true);
|
|
793
|
+
// Push only the 12 translation bytes to the GPU buffer (in place). Guarded
|
|
794
|
+
// on the cached device so CPU-only tests still exercise the matrix math.
|
|
795
|
+
if (device) {
|
|
796
|
+
const gpu = this.instancedTemplates[occ.templateIndex]?.instanceBuffer;
|
|
797
|
+
if (gpu)
|
|
798
|
+
device.queue.writeBuffer(gpu, b + 48, new Float32Array([tx, ty, tz]));
|
|
799
|
+
}
|
|
800
|
+
moved = true;
|
|
801
|
+
}
|
|
802
|
+
if (!moved)
|
|
803
|
+
return false;
|
|
804
|
+
// Rebuild the cached world AABB from the moved occurrence matrices so pick /
|
|
805
|
+
// measure / section bounds stay correct. A simple in-place shift is unsafe for
|
|
806
|
+
// an entity that has BOTH flat meshes and instanced occurrences: the flat
|
|
807
|
+
// translate path deletes this same cache entry, which would strand the shift
|
|
808
|
+
// (a later getInstancedEntityBounds would return null). Recomputing fresh is
|
|
809
|
+
// robust regardless of the flat path and the upload-time bounds.
|
|
810
|
+
this.recomputeInstancedBounds(expressId);
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
/** Recompute an instanced entity's cached world AABB by unioning the transformed
|
|
814
|
+
* template AABB across its occurrences (using their CURRENT matrices). Used after
|
|
815
|
+
* a translate so bounds reflect the moved geometry. No-op for a non-instanced id. */
|
|
816
|
+
recomputeInstancedBounds(expressId) {
|
|
817
|
+
const occurrences = this.instancedEntityMap.get(expressId);
|
|
818
|
+
if (!occurrences || occurrences.length === 0)
|
|
819
|
+
return;
|
|
820
|
+
this.boundingBoxes.delete(expressId);
|
|
821
|
+
for (const occ of occurrences) {
|
|
822
|
+
const cpu = this.instancedTemplateCpu[occ.templateIndex];
|
|
823
|
+
if (!cpu)
|
|
824
|
+
continue;
|
|
825
|
+
const dv = new DataView(cpu.instanceData);
|
|
826
|
+
this.unionInstancedWorldAabb(expressId, dv, occ.byteOffset, cpu.localMin[0], cpu.localMin[1], cpu.localMin[2], cpu.localMax[0], cpu.localMax[1], cpu.localMax[2]);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
679
829
|
/** Drop the per-entity selection-highlight meshes for `expressId` (frozen
|
|
680
830
|
* copies in `this.meshes`) + free their GPU buffers, so the highlight is
|
|
681
831
|
* rebuilt from the entity's current geometry on the next render. Used after a
|
|
@@ -1071,6 +1221,9 @@ export class Scene {
|
|
|
1071
1221
|
this.buckets.clear();
|
|
1072
1222
|
this.meshDataBucket = new Map();
|
|
1073
1223
|
this.meshDataMap.clear();
|
|
1224
|
+
// Free the compact instanced template geometry too; the per-occurrence world
|
|
1225
|
+
// AABBs already live in boundingBoxes, so bbox-raycast still finds instanced ids.
|
|
1226
|
+
this.instancedTemplateCpu = [];
|
|
1074
1227
|
this.activeBucketKey.clear();
|
|
1075
1228
|
this.pendingBatchKeys.clear();
|
|
1076
1229
|
for (const batch of this.partialBatchCache.values())
|
|
@@ -1142,6 +1295,11 @@ export class Scene {
|
|
|
1142
1295
|
}
|
|
1143
1296
|
// 2. Clear the heavy data structures — typed arrays become GC-eligible
|
|
1144
1297
|
this.meshDataMap.clear();
|
|
1298
|
+
// Free the compact instanced template geometry; per-occurrence world AABBs are
|
|
1299
|
+
// already cached in boundingBoxes, so the released-path bbox-raycast still
|
|
1300
|
+
// resolves instanced ids (exact-triangle measure/section is unavailable post-
|
|
1301
|
+
// release, same as the flat path).
|
|
1302
|
+
this.instancedTemplateCpu = [];
|
|
1145
1303
|
// Clear meshData arrays in each bucket (typed arrays become GC-eligible)
|
|
1146
1304
|
// but keep the bucket shells so batchedMesh references remain valid
|
|
1147
1305
|
for (const bucket of this.buckets.values()) {
|
|
@@ -1468,10 +1626,12 @@ export class Scene {
|
|
|
1468
1626
|
if (this.geometryReleased) {
|
|
1469
1627
|
console.warn('[Scene] setColorOverrides called after geometry data was released — skipping.');
|
|
1470
1628
|
this.colorOverrides = null;
|
|
1629
|
+
this.setInstancedColorOverrides(null);
|
|
1471
1630
|
return;
|
|
1472
1631
|
}
|
|
1473
1632
|
if (overrides.size === 0) {
|
|
1474
1633
|
this.colorOverrides = null;
|
|
1634
|
+
this.setInstancedColorOverrides(null);
|
|
1475
1635
|
return;
|
|
1476
1636
|
}
|
|
1477
1637
|
// Defensive copy so external callers can mutate or reuse `overrides`
|
|
@@ -1479,6 +1639,10 @@ export class Scene {
|
|
|
1479
1639
|
// frozen by the readonly type — we don't deep-clone the inner arrays
|
|
1480
1640
|
// because they're treated as immutable by every consumer.
|
|
1481
1641
|
this.colorOverrides = new Map(overrides);
|
|
1642
|
+
// Mirror the overlay onto the GPU-instanced occurrences: patch their colour
|
|
1643
|
+
// bytes in place (no separate overlay pass — the instanced records carry the
|
|
1644
|
+
// override colour directly). No-op when no instanced data is loaded.
|
|
1645
|
+
this.setInstancedColorOverrides(overrides);
|
|
1482
1646
|
// Group expressIds by override color
|
|
1483
1647
|
const colorGroups = new Map();
|
|
1484
1648
|
for (const [expressId, color] of overrides) {
|
|
@@ -1513,6 +1677,7 @@ export class Scene {
|
|
|
1513
1677
|
clearColorOverrides() {
|
|
1514
1678
|
this.destroyOverrideBatches();
|
|
1515
1679
|
this.colorOverrides = null;
|
|
1680
|
+
this.setInstancedColorOverrides(null);
|
|
1516
1681
|
}
|
|
1517
1682
|
/** Get overlay batches for rendering */
|
|
1518
1683
|
getOverrideBatches() {
|
|
@@ -1552,6 +1717,438 @@ export class Scene {
|
|
|
1552
1717
|
getTexturedMeshes() {
|
|
1553
1718
|
return this.texturedMeshes;
|
|
1554
1719
|
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Toggle the instanced draw pass. Instanced geometry is class-0 occurrences
|
|
1722
|
+
* (the Model view); hide it in the Types view mode, where the flat path shows
|
|
1723
|
+
* the class-1/2 type library instead. Buffers stay uploaded — just not drawn —
|
|
1724
|
+
* so toggling back is free.
|
|
1725
|
+
*/
|
|
1726
|
+
setInstancedVisible(visible) {
|
|
1727
|
+
this.instancedVisible = visible;
|
|
1728
|
+
}
|
|
1729
|
+
/** GPU-instancing templates for the renderer's instanced draw pass. Empty when
|
|
1730
|
+
* hidden (Types view mode) so the draw loop skips it. */
|
|
1731
|
+
getInstancedTemplates() {
|
|
1732
|
+
if (!this.instancedVisible)
|
|
1733
|
+
return EMPTY_INSTANCED_TEMPLATES;
|
|
1734
|
+
return this.instancedTemplates;
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Decode a per-batch IFNS instancing shard (`processGeometryBatchInstanced`)
|
|
1738
|
+
* and upload its templates for GPU-instanced drawing. Each unique geometry
|
|
1739
|
+
* becomes a slot-0 vertex buffer (28-byte pos+norm+entityId, matching the flat
|
|
1740
|
+
* layout — the per-vertex entityId is a 0 placeholder; vs_instanced reads the
|
|
1741
|
+
* per-occurrence id) + index buffer, plus a slot-1 per-instance buffer (mat4 +
|
|
1742
|
+
* entityId + rgba) already composed in the WebGL Y-up frame
|
|
1743
|
+
* (`prepareInstancedRender` folds the Z-up→Y-up swap). No-op on a non-shard or
|
|
1744
|
+
* empty payload. Idempotent only in the sense of "append" — call clear() to
|
|
1745
|
+
* reset on a new model.
|
|
1746
|
+
*
|
|
1747
|
+
* Takes an ALREADY-DECODED shard (the worker/main layer that receives the raw
|
|
1748
|
+
* IFNS bytes owns `decodeInstancedShard`), so this module keeps only a
|
|
1749
|
+
* type-only dependency on @ifc-lite/geometry and the Scene stays focused on
|
|
1750
|
+
* GPU upload.
|
|
1751
|
+
*/
|
|
1752
|
+
addInstancedShard(device, shard) {
|
|
1753
|
+
this.instancedDevice = device; // cached for per-instance selection/overlay writeBuffer
|
|
1754
|
+
const prepared = prepareInstancedRender(shard);
|
|
1755
|
+
for (const t of prepared) {
|
|
1756
|
+
const vcount = Math.floor(t.positions.length / 3);
|
|
1757
|
+
if (vcount === 0 || t.indices.length === 0 || t.instanceCount === 0)
|
|
1758
|
+
continue;
|
|
1759
|
+
// Interleave the template's local positions + normals into the 28-byte
|
|
1760
|
+
// (pos3f + norm3f + entityId u32) vertex layout the instanced pipeline's
|
|
1761
|
+
// slot 0 expects. entityId is a 0 placeholder (vs_instanced ignores it).
|
|
1762
|
+
const vtx = new ArrayBuffer(vcount * 28);
|
|
1763
|
+
const vf = new Float32Array(vtx);
|
|
1764
|
+
for (let i = 0; i < vcount; i++) {
|
|
1765
|
+
const o = i * 7;
|
|
1766
|
+
vf[o + 0] = t.positions[i * 3 + 0];
|
|
1767
|
+
vf[o + 1] = t.positions[i * 3 + 1];
|
|
1768
|
+
vf[o + 2] = t.positions[i * 3 + 2];
|
|
1769
|
+
// normals may be shorter/absent on degenerate meshes — default to 0.
|
|
1770
|
+
vf[o + 3] = i * 3 + 0 < t.normals.length ? t.normals[i * 3 + 0] : 0;
|
|
1771
|
+
vf[o + 4] = i * 3 + 1 < t.normals.length ? t.normals[i * 3 + 1] : 0;
|
|
1772
|
+
vf[o + 5] = i * 3 + 2 < t.normals.length ? t.normals[i * 3 + 2] : 0;
|
|
1773
|
+
// vf[o + 6] (entityId lane) stays 0.
|
|
1774
|
+
}
|
|
1775
|
+
const vertexBuffer = device.createBuffer({
|
|
1776
|
+
size: vtx.byteLength,
|
|
1777
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1778
|
+
mappedAtCreation: true,
|
|
1779
|
+
});
|
|
1780
|
+
new Uint8Array(vertexBuffer.getMappedRange()).set(new Uint8Array(vtx));
|
|
1781
|
+
vertexBuffer.unmap();
|
|
1782
|
+
const indexBuffer = device.createBuffer({
|
|
1783
|
+
size: t.indices.byteLength,
|
|
1784
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1785
|
+
mappedAtCreation: true,
|
|
1786
|
+
});
|
|
1787
|
+
new Uint32Array(indexBuffer.getMappedRange()).set(t.indices);
|
|
1788
|
+
indexBuffer.unmap();
|
|
1789
|
+
// instanceBuffer is already the interleaved mat4 + entityId + rgba block
|
|
1790
|
+
// (INSTANCE_STRIDE_BYTES per occurrence) from prepareInstancedRender.
|
|
1791
|
+
const instSize = t.instanceCount * INSTANCE_STRIDE_BYTES;
|
|
1792
|
+
const instanceBuffer = device.createBuffer({
|
|
1793
|
+
size: instSize,
|
|
1794
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
1795
|
+
mappedAtCreation: true,
|
|
1796
|
+
});
|
|
1797
|
+
new Uint8Array(instanceBuffer.getMappedRange()).set(new Uint8Array(t.instanceBuffer, 0, instSize));
|
|
1798
|
+
instanceBuffer.unmap();
|
|
1799
|
+
const templateIndex = this.instancedTemplates.length;
|
|
1800
|
+
this.instancedTemplates.push({
|
|
1801
|
+
vertexBuffer,
|
|
1802
|
+
indexBuffer,
|
|
1803
|
+
indexCount: t.indices.length,
|
|
1804
|
+
instanceBuffer,
|
|
1805
|
+
instanceCount: t.instanceCount,
|
|
1806
|
+
});
|
|
1807
|
+
// Template-local AABB (used to derive per-occurrence world AABBs cheaply).
|
|
1808
|
+
let lmnx = Infinity, lmny = Infinity, lmnz = Infinity;
|
|
1809
|
+
let lmxx = -Infinity, lmxy = -Infinity, lmxz = -Infinity;
|
|
1810
|
+
for (let i = 0; i < t.positions.length; i += 3) {
|
|
1811
|
+
const x = t.positions[i], y = t.positions[i + 1], z = t.positions[i + 2];
|
|
1812
|
+
if (x < lmnx)
|
|
1813
|
+
lmnx = x;
|
|
1814
|
+
if (y < lmny)
|
|
1815
|
+
lmny = y;
|
|
1816
|
+
if (z < lmnz)
|
|
1817
|
+
lmnz = z;
|
|
1818
|
+
if (x > lmxx)
|
|
1819
|
+
lmxx = x;
|
|
1820
|
+
if (y > lmxy)
|
|
1821
|
+
lmxy = y;
|
|
1822
|
+
if (z > lmxz)
|
|
1823
|
+
lmxz = z;
|
|
1824
|
+
}
|
|
1825
|
+
// Retain the compact CPU geometry + the packed instance records (mat4 per
|
|
1826
|
+
// occurrence) so CPU consumers can reach instanced geometry without a full
|
|
1827
|
+
// per-occurrence MeshData each. These are references into the decoded shard.
|
|
1828
|
+
this.instancedTemplateCpu.push({
|
|
1829
|
+
positions: t.positions,
|
|
1830
|
+
normals: t.normals,
|
|
1831
|
+
indices: t.indices,
|
|
1832
|
+
instanceData: t.instanceBuffer,
|
|
1833
|
+
localMin: [lmnx, lmny, lmnz],
|
|
1834
|
+
localMax: [lmxx, lmxy, lmxz],
|
|
1835
|
+
});
|
|
1836
|
+
// Map each occurrence's express_id -> (template, byte offset, original
|
|
1837
|
+
// colour) so selection (flag byte) + lens/IDS overlays (colour bytes) can
|
|
1838
|
+
// patch individual occurrences via writeBuffer without a re-upload. Also fold
|
|
1839
|
+
// each occurrence's world AABB (template local box × its mat4) into
|
|
1840
|
+
// boundingBoxes so getEntityBoundingBox + the CPU raycast-bounds path
|
|
1841
|
+
// (BCF anchors, large/released-model picking, frame-selection) see instanced
|
|
1842
|
+
// geometry. (Templates with no finite local box — empty geometry — are skipped.)
|
|
1843
|
+
const cdv = new DataView(t.instanceBuffer);
|
|
1844
|
+
const haveBox = Number.isFinite(lmnx);
|
|
1845
|
+
for (let i = 0; i < t.instanceCount; i++) {
|
|
1846
|
+
const byteOffset = i * INSTANCE_STRIDE_BYTES;
|
|
1847
|
+
const eid = t.entityIds[i];
|
|
1848
|
+
const originalColor = [
|
|
1849
|
+
cdv.getFloat32(byteOffset + INSTANCE_COLOR_OFFSET, true),
|
|
1850
|
+
cdv.getFloat32(byteOffset + INSTANCE_COLOR_OFFSET + 4, true),
|
|
1851
|
+
cdv.getFloat32(byteOffset + INSTANCE_COLOR_OFFSET + 8, true),
|
|
1852
|
+
cdv.getFloat32(byteOffset + INSTANCE_COLOR_OFFSET + 12, true),
|
|
1853
|
+
];
|
|
1854
|
+
let arr = this.instancedEntityMap.get(eid);
|
|
1855
|
+
if (!arr) {
|
|
1856
|
+
arr = [];
|
|
1857
|
+
this.instancedEntityMap.set(eid, arr);
|
|
1858
|
+
}
|
|
1859
|
+
arr.push({ templateIndex, byteOffset, originalColor });
|
|
1860
|
+
if (haveBox) {
|
|
1861
|
+
this.unionInstancedWorldAabb(eid, cdv, byteOffset, lmnx, lmny, lmnz, lmxx, lmxy, lmxz);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
// New occurrences default to flags=0 (visible). Force the next setInstancedVisibility
|
|
1866
|
+
// to recompute so an already-active isolate/hide also applies to geometry that
|
|
1867
|
+
// streamed in after the visibility was set.
|
|
1868
|
+
this.instancedVisibilityDirty = true;
|
|
1869
|
+
}
|
|
1870
|
+
/** Transform a template's local AABB by an occurrence's column-major mat4 (read
|
|
1871
|
+
* from the packed instance record at `matOffset`) and union the world box into
|
|
1872
|
+
* boundingBoxes[eid]. */
|
|
1873
|
+
unionInstancedWorldAabb(eid, dv, matOffset, lmnx, lmny, lmnz, lmxx, lmxy, lmxz) {
|
|
1874
|
+
const m0 = dv.getFloat32(matOffset + 0, true), m1 = dv.getFloat32(matOffset + 4, true), m2 = dv.getFloat32(matOffset + 8, true);
|
|
1875
|
+
const m4 = dv.getFloat32(matOffset + 16, true), m5 = dv.getFloat32(matOffset + 20, true), m6 = dv.getFloat32(matOffset + 24, true);
|
|
1876
|
+
const m8 = dv.getFloat32(matOffset + 32, true), m9 = dv.getFloat32(matOffset + 36, true), m10 = dv.getFloat32(matOffset + 40, true);
|
|
1877
|
+
const m12 = dv.getFloat32(matOffset + 48, true), m13 = dv.getFloat32(matOffset + 52, true), m14 = dv.getFloat32(matOffset + 56, true);
|
|
1878
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
1879
|
+
for (let c = 0; c < 8; c++) {
|
|
1880
|
+
const x = (c & 1) ? lmxx : lmnx, y = (c & 2) ? lmxy : lmny, z = (c & 4) ? lmxz : lmnz;
|
|
1881
|
+
const wx = m0 * x + m4 * y + m8 * z + m12;
|
|
1882
|
+
const wy = m1 * x + m5 * y + m9 * z + m13;
|
|
1883
|
+
const wz = m2 * x + m6 * y + m10 * z + m14;
|
|
1884
|
+
if (wx < minX)
|
|
1885
|
+
minX = wx;
|
|
1886
|
+
if (wy < minY)
|
|
1887
|
+
minY = wy;
|
|
1888
|
+
if (wz < minZ)
|
|
1889
|
+
minZ = wz;
|
|
1890
|
+
if (wx > maxX)
|
|
1891
|
+
maxX = wx;
|
|
1892
|
+
if (wy > maxY)
|
|
1893
|
+
maxY = wy;
|
|
1894
|
+
if (wz > maxZ)
|
|
1895
|
+
maxZ = wz;
|
|
1896
|
+
}
|
|
1897
|
+
const existing = this.boundingBoxes.get(eid);
|
|
1898
|
+
if (existing) {
|
|
1899
|
+
existing.min.x = Math.min(existing.min.x, minX);
|
|
1900
|
+
existing.min.y = Math.min(existing.min.y, minY);
|
|
1901
|
+
existing.min.z = Math.min(existing.min.z, minZ);
|
|
1902
|
+
existing.max.x = Math.max(existing.max.x, maxX);
|
|
1903
|
+
existing.max.y = Math.max(existing.max.y, maxY);
|
|
1904
|
+
existing.max.z = Math.max(existing.max.z, maxZ);
|
|
1905
|
+
}
|
|
1906
|
+
else {
|
|
1907
|
+
this.boundingBoxes.set(eid, { min: { x: minX, y: minY, z: minZ }, max: { x: maxX, y: maxY, z: maxZ } });
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
/** True if `expressId` is a GPU-instanced occurrence (lives only in the instanced
|
|
1911
|
+
* shard, not the flat meshDataMap). CPU consumers use this to decide whether to
|
|
1912
|
+
* fall back to the instanced accessors below. */
|
|
1913
|
+
isInstancedEntity(expressId) {
|
|
1914
|
+
return this.instancedEntityMap.has(expressId);
|
|
1915
|
+
}
|
|
1916
|
+
/** All instanced occurrence express_ids (for CPU consumers that enumerate geometry,
|
|
1917
|
+
* e.g. the raycast-engine and exporters). */
|
|
1918
|
+
getInstancedEntityIds() {
|
|
1919
|
+
return this.instancedEntityMap.keys();
|
|
1920
|
+
}
|
|
1921
|
+
/** Materialize EVERY instanced occurrence as world-space MeshData. Transient + not
|
|
1922
|
+
* retained — for one-shot full-geometry consumers (glTF / IFC5 export) that must
|
|
1923
|
+
* include the instanced occurrences absent from geometryResult.meshes. Returns []
|
|
1924
|
+
* when no instanced data is loaded or after geometry release (templates freed). */
|
|
1925
|
+
getAllInstancedMeshData() {
|
|
1926
|
+
const out = [];
|
|
1927
|
+
for (const eid of this.instancedEntityMap.keys()) {
|
|
1928
|
+
const pieces = this.getInstancedMeshDataPieces(eid);
|
|
1929
|
+
if (pieces)
|
|
1930
|
+
out.push(...pieces);
|
|
1931
|
+
}
|
|
1932
|
+
return out;
|
|
1933
|
+
}
|
|
1934
|
+
/** World-space AABB for an instanced occurrence (union over its occurrences),
|
|
1935
|
+
* or null if not instanced. Populated at upload time, so this is O(1). */
|
|
1936
|
+
getInstancedEntityBounds(expressId) {
|
|
1937
|
+
if (!this.instancedEntityMap.has(expressId))
|
|
1938
|
+
return null;
|
|
1939
|
+
return this.boundingBoxes.get(expressId) ?? null;
|
|
1940
|
+
}
|
|
1941
|
+
/** Lazily materialize per-occurrence world-space MeshData for an instanced entity
|
|
1942
|
+
* (template geometry × each occurrence's matrix). NOT retained — built on demand
|
|
1943
|
+
* for CPU consumers that need triangles (exact raycast / measure / section-face /
|
|
1944
|
+
* export). Returns undefined if the id is not instanced. */
|
|
1945
|
+
getInstancedMeshDataPieces(expressId) {
|
|
1946
|
+
const occ = this.instancedEntityMap.get(expressId);
|
|
1947
|
+
if (!occ || occ.length === 0)
|
|
1948
|
+
return undefined;
|
|
1949
|
+
const out = [];
|
|
1950
|
+
for (const o of occ) {
|
|
1951
|
+
const tpl = this.instancedTemplateCpu[o.templateIndex];
|
|
1952
|
+
if (!tpl || tpl.positions.length === 0)
|
|
1953
|
+
continue;
|
|
1954
|
+
const dv = new DataView(tpl.instanceData);
|
|
1955
|
+
const b = o.byteOffset;
|
|
1956
|
+
const m0 = dv.getFloat32(b + 0, true), m1 = dv.getFloat32(b + 4, true), m2 = dv.getFloat32(b + 8, true);
|
|
1957
|
+
const m4 = dv.getFloat32(b + 16, true), m5 = dv.getFloat32(b + 20, true), m6 = dv.getFloat32(b + 24, true);
|
|
1958
|
+
const m8 = dv.getFloat32(b + 32, true), m9 = dv.getFloat32(b + 36, true), m10 = dv.getFloat32(b + 40, true);
|
|
1959
|
+
const m12 = dv.getFloat32(b + 48, true), m13 = dv.getFloat32(b + 52, true), m14 = dv.getFloat32(b + 56, true);
|
|
1960
|
+
const n = tpl.positions.length;
|
|
1961
|
+
const positions = new Float32Array(n);
|
|
1962
|
+
const normals = new Float32Array(tpl.normals.length);
|
|
1963
|
+
for (let i = 0; i < n; i += 3) {
|
|
1964
|
+
const x = tpl.positions[i], y = tpl.positions[i + 1], z = tpl.positions[i + 2];
|
|
1965
|
+
positions[i] = m0 * x + m4 * y + m8 * z + m12;
|
|
1966
|
+
positions[i + 1] = m1 * x + m5 * y + m9 * z + m13;
|
|
1967
|
+
positions[i + 2] = m2 * x + m6 * y + m10 * z + m14;
|
|
1968
|
+
if (i + 2 < tpl.normals.length) {
|
|
1969
|
+
// Rotate normals by the upper-3×3 (instancing transforms are rigid +
|
|
1970
|
+
// uniform scale, so this is correct up to a renormalize).
|
|
1971
|
+
const nx = tpl.normals[i], ny = tpl.normals[i + 1], nz = tpl.normals[i + 2];
|
|
1972
|
+
let rx = m0 * nx + m4 * ny + m8 * nz;
|
|
1973
|
+
let ry = m1 * nx + m5 * ny + m9 * nz;
|
|
1974
|
+
let rz = m2 * nx + m6 * ny + m10 * nz;
|
|
1975
|
+
const len = Math.hypot(rx, ry, rz) || 1;
|
|
1976
|
+
rx /= len;
|
|
1977
|
+
ry /= len;
|
|
1978
|
+
rz /= len;
|
|
1979
|
+
normals[i] = rx;
|
|
1980
|
+
normals[i + 1] = ry;
|
|
1981
|
+
normals[i + 2] = rz;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
const color = [...o.originalColor];
|
|
1985
|
+
out.push({ expressId, positions, normals, indices: tpl.indices, color });
|
|
1986
|
+
}
|
|
1987
|
+
return out.length > 0 ? out : undefined;
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Per-instance SELECTION: highlight the occurrences of `expressIds` by setting
|
|
1991
|
+
* their flag byte (bit 0) and clearing the previously-selected ones. The shader
|
|
1992
|
+
* (vs_instanced -> fs_main) applies the blue highlight per occurrence, so no
|
|
1993
|
+
* re-draw is needed. No-op until a shard has been uploaded.
|
|
1994
|
+
*/
|
|
1995
|
+
setInstancedSelection(expressIds) {
|
|
1996
|
+
const device = this.instancedDevice;
|
|
1997
|
+
if (!device || this.instancedTemplates.length === 0)
|
|
1998
|
+
return;
|
|
1999
|
+
// Called every render frame from the renderer. Fast-path an UNCHANGED selection
|
|
2000
|
+
// (the common orbit case, especially the empty set) so we skip both the per-frame
|
|
2001
|
+
// writeBuffer loops AND the `new Set(...)` allocation — equal sizes + full
|
|
2002
|
+
// containment ⇒ set equality.
|
|
2003
|
+
let changed = expressIds.size !== this.instancedSelected.size;
|
|
2004
|
+
if (!changed) {
|
|
2005
|
+
for (const eid of expressIds) {
|
|
2006
|
+
if (!this.instancedSelected.has(eid)) {
|
|
2007
|
+
changed = true;
|
|
2008
|
+
break;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
if (!changed)
|
|
2013
|
+
return;
|
|
2014
|
+
// Re-derive the combined flag lane (selected | hidden) for every occurrence whose
|
|
2015
|
+
// selected-membership flips, so we never clobber the hidden bit.
|
|
2016
|
+
const prev = this.instancedSelected;
|
|
2017
|
+
this.instancedSelected = new Set(expressIds);
|
|
2018
|
+
for (const eid of prev) {
|
|
2019
|
+
if (!expressIds.has(eid))
|
|
2020
|
+
this.writeInstanceFlags(device, eid);
|
|
2021
|
+
}
|
|
2022
|
+
for (const eid of expressIds) {
|
|
2023
|
+
if (!prev.has(eid))
|
|
2024
|
+
this.writeInstanceFlags(device, eid);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Per-instance VISIBILITY (hide / isolate): set the hidden flag bit on occurrences
|
|
2029
|
+
* that should not render, mirroring the flat path's hiddenIds/isolatedIds filter.
|
|
2030
|
+
* The shader discards hidden occurrences in BOTH the render and pick passes, so they
|
|
2031
|
+
* neither draw nor are pickable. `isolatedIds != null` means "show only these"; any
|
|
2032
|
+
* occurrence not in the set is hidden. Diffed so an unchanged visibility set is a
|
|
2033
|
+
* no-op (no writeBuffer). No-op until a shard has been uploaded.
|
|
2034
|
+
*/
|
|
2035
|
+
setInstancedVisibility(hiddenIds, isolatedIds) {
|
|
2036
|
+
const device = this.instancedDevice;
|
|
2037
|
+
if (!device || this.instancedTemplates.length === 0)
|
|
2038
|
+
return;
|
|
2039
|
+
// Called every render frame. The viewer passes stable Set references that only
|
|
2040
|
+
// change when visibility changes, so a reference-equality guard skips the O(N)
|
|
2041
|
+
// set rebuild + allocation during orbit (the common, unchanged case). The dirty
|
|
2042
|
+
// flag forces a recompute after a new shard adds occurrences mid-stream, so an
|
|
2043
|
+
// active isolate/hide also applies to geometry that streams in afterwards.
|
|
2044
|
+
if (!this.instancedVisibilityDirty &&
|
|
2045
|
+
hiddenIds === this.lastInstancedHiddenIds &&
|
|
2046
|
+
isolatedIds === this.lastInstancedIsolatedIds) {
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
this.instancedVisibilityDirty = false;
|
|
2050
|
+
this.lastInstancedHiddenIds = hiddenIds ?? null;
|
|
2051
|
+
this.lastInstancedIsolatedIds = isolatedIds ?? null;
|
|
2052
|
+
const isHidden = (eid) => (hiddenIds != null && hiddenIds.has(eid)) ||
|
|
2053
|
+
(isolatedIds != null && !isolatedIds.has(eid));
|
|
2054
|
+
// Recompute the effective hidden set over all instanced occurrences and diff vs
|
|
2055
|
+
// the current one; only flips touch the GPU buffer.
|
|
2056
|
+
const next = new Set();
|
|
2057
|
+
for (const eid of this.instancedEntityMap.keys()) {
|
|
2058
|
+
if (isHidden(eid))
|
|
2059
|
+
next.add(eid);
|
|
2060
|
+
}
|
|
2061
|
+
// Fast-path: unchanged hidden set → nothing to write.
|
|
2062
|
+
let changed = next.size !== this.instancedHidden.size;
|
|
2063
|
+
if (!changed) {
|
|
2064
|
+
for (const eid of next) {
|
|
2065
|
+
if (!this.instancedHidden.has(eid)) {
|
|
2066
|
+
changed = true;
|
|
2067
|
+
break;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
if (!changed)
|
|
2072
|
+
return;
|
|
2073
|
+
const prev = this.instancedHidden;
|
|
2074
|
+
this.instancedHidden = next;
|
|
2075
|
+
for (const eid of prev) {
|
|
2076
|
+
if (!next.has(eid))
|
|
2077
|
+
this.writeInstanceFlags(device, eid);
|
|
2078
|
+
}
|
|
2079
|
+
for (const eid of next) {
|
|
2080
|
+
if (!prev.has(eid))
|
|
2081
|
+
this.writeInstanceFlags(device, eid);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Per-instance COLOUR OVERRIDE (lens / IDS / compare / 4D): patch the colour
|
|
2086
|
+
* bytes of the affected occurrences in place; occurrences dropped from the map
|
|
2087
|
+
* are restored to their original colour. Pass null/empty to clear all.
|
|
2088
|
+
*/
|
|
2089
|
+
setInstancedColorOverrides(overrides) {
|
|
2090
|
+
const device = this.instancedDevice;
|
|
2091
|
+
if (!device || this.instancedTemplates.length === 0)
|
|
2092
|
+
return;
|
|
2093
|
+
const next = overrides ?? new Map();
|
|
2094
|
+
for (const eid of this.instancedOverridden) {
|
|
2095
|
+
if (!next.has(eid))
|
|
2096
|
+
this.restoreInstanceColor(device, eid);
|
|
2097
|
+
}
|
|
2098
|
+
let hasTransparent = false;
|
|
2099
|
+
for (const [eid, rgba] of next) {
|
|
2100
|
+
this.writeInstanceColor(device, eid, rgba);
|
|
2101
|
+
// Instanced occurrences are opaque by partition; only an override can drop alpha
|
|
2102
|
+
// below the cutoff (lens-ghost / x-ray / compare). Track it so the renderer runs
|
|
2103
|
+
// the transparent instanced sub-pass only when something is actually translucent.
|
|
2104
|
+
if (rgba[3] < OPAQUE_ALPHA_CUTOFF)
|
|
2105
|
+
hasTransparent = true;
|
|
2106
|
+
}
|
|
2107
|
+
this.instancedOverridden = new Set(next.keys());
|
|
2108
|
+
this.instancedHasTransparent = hasTransparent;
|
|
2109
|
+
}
|
|
2110
|
+
/** True when an active colour override made some instanced occurrence translucent,
|
|
2111
|
+
* so the renderer should run the transparent instanced sub-pass. */
|
|
2112
|
+
hasTransparentInstances() {
|
|
2113
|
+
return this.instancedHasTransparent;
|
|
2114
|
+
}
|
|
2115
|
+
/** Write the combined flag lane (selected | hidden) for every occurrence of `eid`.
|
|
2116
|
+
* Folding both bits here means selection and visibility updates never clobber each
|
|
2117
|
+
* other (they share the one u32 flags lane at INSTANCE_FLAGS_OFFSET). */
|
|
2118
|
+
writeInstanceFlags(device, eid) {
|
|
2119
|
+
const locs = this.instancedEntityMap.get(eid);
|
|
2120
|
+
if (!locs)
|
|
2121
|
+
return;
|
|
2122
|
+
const flags = (this.instancedSelected.has(eid) ? INSTANCE_FLAG_SELECTED : 0) |
|
|
2123
|
+
(this.instancedHidden.has(eid) ? INSTANCE_FLAG_HIDDEN : 0);
|
|
2124
|
+
const data = new Uint32Array([flags >>> 0]);
|
|
2125
|
+
for (const loc of locs) {
|
|
2126
|
+
const buf = this.instancedTemplates[loc.templateIndex]?.instanceBuffer;
|
|
2127
|
+
if (buf)
|
|
2128
|
+
device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_FLAGS_OFFSET, data);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
writeInstanceColor(device, eid, rgba) {
|
|
2132
|
+
const locs = this.instancedEntityMap.get(eid);
|
|
2133
|
+
if (!locs)
|
|
2134
|
+
return;
|
|
2135
|
+
const data = new Float32Array([rgba[0], rgba[1], rgba[2], rgba[3]]);
|
|
2136
|
+
for (const loc of locs) {
|
|
2137
|
+
const buf = this.instancedTemplates[loc.templateIndex]?.instanceBuffer;
|
|
2138
|
+
if (buf)
|
|
2139
|
+
device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_COLOR_OFFSET, data);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
restoreInstanceColor(device, eid) {
|
|
2143
|
+
const locs = this.instancedEntityMap.get(eid);
|
|
2144
|
+
if (!locs)
|
|
2145
|
+
return;
|
|
2146
|
+
for (const loc of locs) {
|
|
2147
|
+
const buf = this.instancedTemplates[loc.templateIndex]?.instanceBuffer;
|
|
2148
|
+
if (buf)
|
|
2149
|
+
device.queue.writeBuffer(buf, loc.byteOffset + INSTANCE_COLOR_OFFSET, new Float32Array(loc.originalColor));
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
1555
2152
|
/**
|
|
1556
2153
|
* Build a textured mesh (#961): interleave position+normal+entityId+uv into one
|
|
1557
2154
|
* vertex buffer, upload the decoded RGBA8 texture, create a sampler honouring
|
|
@@ -1656,6 +2253,23 @@ export class Scene {
|
|
|
1656
2253
|
tm.texture.destroy();
|
|
1657
2254
|
}
|
|
1658
2255
|
this.texturedMeshes = [];
|
|
2256
|
+
// GPU-instancing templates own their vertex/index/instance buffers.
|
|
2257
|
+
for (const it of this.instancedTemplates) {
|
|
2258
|
+
it.vertexBuffer.destroy();
|
|
2259
|
+
it.indexBuffer.destroy();
|
|
2260
|
+
it.instanceBuffer.destroy();
|
|
2261
|
+
}
|
|
2262
|
+
this.instancedTemplates = [];
|
|
2263
|
+
this.instancedTemplateCpu = [];
|
|
2264
|
+
this.instancedEntityMap.clear();
|
|
2265
|
+
this.instancedSelected.clear();
|
|
2266
|
+
this.instancedHidden.clear();
|
|
2267
|
+
this.instancedOverridden.clear();
|
|
2268
|
+
this.instancedHasTransparent = false;
|
|
2269
|
+
this.lastInstancedHiddenIds = null;
|
|
2270
|
+
this.lastInstancedIsolatedIds = null;
|
|
2271
|
+
this.instancedVisibilityDirty = false;
|
|
2272
|
+
this.instancedDevice = undefined;
|
|
1659
2273
|
// Clear partial batch cache
|
|
1660
2274
|
for (const batch of this.partialBatchCache.values())
|
|
1661
2275
|
destroyGpuResources(batch);
|
|
@@ -1758,9 +2372,17 @@ export class Scene {
|
|
|
1758
2372
|
*/
|
|
1759
2373
|
getAllMeshDataExpressIds() {
|
|
1760
2374
|
if (this.geometryReleased) {
|
|
2375
|
+
// boundingBoxes already includes instanced occurrences (their AABBs are
|
|
2376
|
+
// stored there at upload), so the released path needs no extra union.
|
|
1761
2377
|
return Array.from(this.boundingBoxes.keys());
|
|
1762
2378
|
}
|
|
1763
|
-
|
|
2379
|
+
// Union instanced-only occurrences (absent from meshDataMap) so CPU consumers
|
|
2380
|
+
// enumerating geometry see them too. IDs only — no geometry materialized.
|
|
2381
|
+
// (#1238 review)
|
|
2382
|
+
const ids = new Set(this.meshDataMap.keys());
|
|
2383
|
+
for (const eid of this.instancedEntityMap.keys())
|
|
2384
|
+
ids.add(eid);
|
|
2385
|
+
return Array.from(ids);
|
|
1764
2386
|
}
|
|
1765
2387
|
/**
|
|
1766
2388
|
* Get or compute bounding box for an entity from its mesh vertex data.
|
|
@@ -1823,7 +2445,34 @@ export class Scene {
|
|
|
1823
2445
|
return raycastBoundingBoxes(rayOrigin, rayDirInv, rayDirSign, this.boundingBoxes, hiddenIds, isolatedIds);
|
|
1824
2446
|
}
|
|
1825
2447
|
// Full triangle-level raycast with bounding-box pre-filter
|
|
1826
|
-
|
|
2448
|
+
const flatHit = raycastTriangles(rayOrigin, rayDir, rayDirInv, rayDirSign, this.meshDataMap, (id) => this.getEntityBoundingBox(id), hiddenIds, isolatedIds);
|
|
2449
|
+
// Instanced-only occurrences live in the shard, not meshDataMap, so the CPU
|
|
2450
|
+
// pick fallback would miss them. Materialize triangles lazily ONLY for
|
|
2451
|
+
// entities whose world AABB the ray actually hits (never the whole instanced
|
|
2452
|
+
// population), then return whichever hit is closer. (#1238 review)
|
|
2453
|
+
let instancedHit = null;
|
|
2454
|
+
if (this.instancedEntityMap.size > 0) {
|
|
2455
|
+
const instancedMap = new Map();
|
|
2456
|
+
for (const eid of this.instancedEntityMap.keys()) {
|
|
2457
|
+
if (hiddenIds?.has(eid))
|
|
2458
|
+
continue;
|
|
2459
|
+
if (isolatedIds != null && !isolatedIds.has(eid))
|
|
2460
|
+
continue;
|
|
2461
|
+
const bounds = this.getInstancedEntityBounds(eid);
|
|
2462
|
+
if (!bounds || !rayIntersectsBox(rayOrigin, rayDirInv, rayDirSign, bounds))
|
|
2463
|
+
continue;
|
|
2464
|
+
const pieces = this.getInstancedMeshDataPieces(eid);
|
|
2465
|
+
if (pieces && pieces.length > 0)
|
|
2466
|
+
instancedMap.set(eid, pieces);
|
|
2467
|
+
}
|
|
2468
|
+
if (instancedMap.size > 0) {
|
|
2469
|
+
instancedHit = raycastTriangles(rayOrigin, rayDir, rayDirInv, rayDirSign, instancedMap, (id) => this.getInstancedEntityBounds(id), hiddenIds, isolatedIds);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
if (flatHit && instancedHit) {
|
|
2473
|
+
return instancedHit.distance < flatHit.distance ? instancedHit : flatHit;
|
|
2474
|
+
}
|
|
2475
|
+
return flatHit ?? instancedHit;
|
|
1827
2476
|
}
|
|
1828
2477
|
}
|
|
1829
2478
|
//# sourceMappingURL=scene.js.map
|