@ifc-lite/renderer 2.0.0 → 2.2.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/README.md +10 -0
- package/dist/appearance-batch-cohorts.d.ts +33 -0
- package/dist/appearance-batch-cohorts.d.ts.map +1 -0
- package/dist/appearance-batch-cohorts.js +126 -0
- package/dist/appearance-batch-cohorts.js.map +1 -0
- package/dist/appearance-companions.d.ts +30 -0
- package/dist/appearance-companions.d.ts.map +1 -0
- package/dist/appearance-companions.js +138 -0
- package/dist/appearance-companions.js.map +1 -0
- package/dist/appearance-preview.d.ts +93 -0
- package/dist/appearance-preview.d.ts.map +1 -0
- package/dist/appearance-preview.js +246 -0
- package/dist/appearance-preview.js.map +1 -0
- package/dist/appearance-uvs.d.ts +10 -0
- package/dist/appearance-uvs.d.ts.map +1 -0
- package/dist/appearance-uvs.js +112 -0
- package/dist/appearance-uvs.js.map +1 -0
- package/dist/bvh.d.ts.map +1 -1
- package/dist/bvh.js +3 -0
- package/dist/bvh.js.map +1 -1
- package/dist/deviation/deviation-computer.js +1 -1
- package/dist/deviation/deviation-computer.js.map +1 -1
- package/dist/index.d.ts +60 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +184 -232
- package/dist/index.js.map +1 -1
- package/dist/lod-simplify.d.ts.map +1 -1
- package/dist/lod-simplify.js +24 -2
- package/dist/lod-simplify.js.map +1 -1
- package/dist/merged-mesh-extract.d.ts +18 -0
- package/dist/merged-mesh-extract.d.ts.map +1 -0
- package/dist/merged-mesh-extract.js +73 -0
- package/dist/merged-mesh-extract.js.map +1 -0
- package/dist/model-bounds-tracker.d.ts +1 -1
- package/dist/model-bounds-tracker.d.ts.map +1 -1
- package/dist/model-bounds-tracker.js +3 -2
- package/dist/model-bounds-tracker.js.map +1 -1
- package/dist/model-placement-bounds.d.ts +29 -0
- package/dist/model-placement-bounds.d.ts.map +1 -0
- package/dist/model-placement-bounds.js +55 -0
- package/dist/model-placement-bounds.js.map +1 -0
- package/dist/model-translation.d.ts +57 -0
- package/dist/model-translation.d.ts.map +1 -0
- package/dist/model-translation.js +223 -0
- package/dist/model-translation.js.map +1 -0
- package/dist/partial-batch-cache.d.ts +65 -0
- package/dist/partial-batch-cache.d.ts.map +1 -0
- package/dist/partial-batch-cache.js +88 -0
- package/dist/partial-batch-cache.js.map +1 -0
- package/dist/partial-batch-requests.d.ts +77 -0
- package/dist/partial-batch-requests.d.ts.map +1 -0
- package/dist/partial-batch-requests.js +97 -0
- package/dist/partial-batch-requests.js.map +1 -0
- package/dist/picking-manager.js +2 -2
- package/dist/picking-manager.js.map +1 -1
- package/dist/point-picker.d.ts +2 -2
- package/dist/point-picker.d.ts.map +1 -1
- package/dist/point-picker.js +27 -21
- package/dist/point-picker.js.map +1 -1
- package/dist/pointcloud/point-cloud-node.d.ts +2 -0
- package/dist/pointcloud/point-cloud-node.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-node.js +12 -0
- package/dist/pointcloud/point-cloud-node.js.map +1 -1
- package/dist/pointcloud/point-cloud-placement.d.ts +20 -0
- package/dist/pointcloud/point-cloud-placement.d.ts.map +1 -0
- package/dist/pointcloud/point-cloud-placement.js +69 -0
- package/dist/pointcloud/point-cloud-placement.js.map +1 -0
- package/dist/pointcloud/point-cloud-renderer.d.ts +14 -14
- package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
- package/dist/pointcloud/point-cloud-renderer.js +47 -63
- package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
- package/dist/raycast-engine.d.ts +9 -11
- package/dist/raycast-engine.d.ts.map +1 -1
- package/dist/raycast-engine.js +6 -11
- package/dist/raycast-engine.js.map +1 -1
- package/dist/raycaster.d.ts.map +1 -1
- package/dist/raycaster.js +3 -0
- package/dist/raycaster.js.map +1 -1
- package/dist/reference-image-hit.d.ts +5 -0
- package/dist/reference-image-hit.d.ts.map +1 -0
- package/dist/reference-image-hit.js +28 -0
- package/dist/reference-image-hit.js.map +1 -0
- package/dist/reference-image-host.d.ts +5 -0
- package/dist/reference-image-host.d.ts.map +1 -0
- package/dist/reference-image-host.js +26 -0
- package/dist/reference-image-host.js.map +1 -0
- package/dist/reference-image-pipeline.d.ts +15 -0
- package/dist/reference-image-pipeline.d.ts.map +1 -0
- package/dist/reference-image-pipeline.js +104 -0
- package/dist/reference-image-pipeline.js.map +1 -0
- package/dist/reference-image-types.d.ts +27 -0
- package/dist/reference-image-types.d.ts.map +1 -0
- package/dist/reference-image-types.js +5 -0
- package/dist/reference-image-types.js.map +1 -0
- package/dist/reference-images.d.ts +27 -0
- package/dist/reference-images.d.ts.map +1 -0
- package/dist/reference-images.js +134 -0
- package/dist/reference-images.js.map +1 -0
- package/dist/renderer-viewport.d.ts +3 -0
- package/dist/renderer-viewport.d.ts.map +1 -0
- package/dist/renderer-viewport.js +20 -0
- package/dist/renderer-viewport.js.map +1 -0
- package/dist/rgba-texture-pool.d.ts +12 -0
- package/dist/rgba-texture-pool.d.ts.map +1 -0
- package/dist/rgba-texture-pool.js +51 -0
- package/dist/rgba-texture-pool.js.map +1 -0
- package/dist/scene-appearance-buckets.d.ts +41 -0
- package/dist/scene-appearance-buckets.d.ts.map +1 -0
- package/dist/scene-appearance-buckets.js +164 -0
- package/dist/scene-appearance-buckets.js.map +1 -0
- package/dist/scene-appearance-instances.d.ts +42 -0
- package/dist/scene-appearance-instances.d.ts.map +1 -0
- package/dist/scene-appearance-instances.js +143 -0
- package/dist/scene-appearance-instances.js.map +1 -0
- package/dist/scene-appearance-preview.d.ts +30 -0
- package/dist/scene-appearance-preview.d.ts.map +1 -0
- package/dist/scene-appearance-preview.js +149 -0
- package/dist/scene-appearance-preview.js.map +1 -0
- package/dist/scene-appearance-resources.d.ts +16 -0
- package/dist/scene-appearance-resources.d.ts.map +1 -0
- package/dist/scene-appearance-resources.js +41 -0
- package/dist/scene-appearance-resources.js.map +1 -0
- package/dist/scene-authored-owner.d.ts +9 -0
- package/dist/scene-authored-owner.d.ts.map +1 -0
- package/dist/scene-authored-owner.js +73 -0
- package/dist/scene-authored-owner.js.map +1 -0
- package/dist/scene-batch-upload.d.ts +12 -0
- package/dist/scene-batch-upload.d.ts.map +1 -0
- package/dist/scene-batch-upload.js +147 -0
- package/dist/scene-batch-upload.js.map +1 -0
- package/dist/scene-contents.d.ts +41 -0
- package/dist/scene-contents.d.ts.map +1 -1
- package/dist/scene-geometry.d.ts +16 -0
- package/dist/scene-geometry.d.ts.map +1 -1
- package/dist/scene-geometry.js +26 -0
- package/dist/scene-geometry.js.map +1 -1
- package/dist/scene-instance-materialization.d.ts +17 -0
- package/dist/scene-instance-materialization.d.ts.map +1 -0
- package/dist/scene-instance-materialization.js +52 -0
- package/dist/scene-instance-materialization.js.map +1 -0
- package/dist/scene-instance-suppression.d.ts +22 -0
- package/dist/scene-instance-suppression.d.ts.map +1 -0
- package/dist/scene-instance-suppression.js +100 -0
- package/dist/scene-instance-suppression.js.map +1 -0
- package/dist/scene-instance-types.d.ts +55 -0
- package/dist/scene-instance-types.d.ts.map +1 -0
- package/dist/scene-instance-types.js +2 -0
- package/dist/scene-instance-types.js.map +1 -0
- package/dist/scene-model-translation.d.ts +49 -0
- package/dist/scene-model-translation.d.ts.map +1 -0
- package/dist/scene-model-translation.js +100 -0
- package/dist/scene-model-translation.js.map +1 -0
- package/dist/scene-stream-split.d.ts +4 -0
- package/dist/scene-stream-split.d.ts.map +1 -0
- package/dist/scene-stream-split.js +54 -0
- package/dist/scene-stream-split.js.map +1 -0
- package/dist/scene.d.ts +105 -85
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +445 -581
- package/dist/scene.js.map +1 -1
- package/dist/shadow-occluders.d.ts.map +1 -1
- package/dist/shadow-occluders.js +13 -9
- package/dist/shadow-occluders.js.map +1 -1
- package/dist/snap-detector.d.ts.map +1 -1
- package/dist/snap-detector.js +3 -0
- package/dist/snap-detector.js.map +1 -1
- package/dist/textured-vertices.d.ts +9 -0
- package/dist/textured-vertices.d.ts.map +1 -0
- package/dist/textured-vertices.js +40 -0
- package/dist/textured-vertices.js.map +1 -0
- package/dist/types.d.ts +11 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/xray-alpha.d.ts +131 -0
- package/dist/xray-alpha.d.ts.map +1 -0
- package/dist/xray-alpha.js +266 -0
- package/dist/xray-alpha.js.map +1 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
export { WebGPUDevice } from './device.js';
|
|
8
8
|
export { RenderPipeline } from './pipeline.js';
|
|
9
9
|
export { Camera } from './camera.js';
|
|
10
|
+
export { expandAppearanceCorners, equivalentAppearanceGeometry } from './appearance-uvs.js';
|
|
11
|
+
export { sameCompanionParts } from './appearance-companions.js';
|
|
12
|
+
import { createReferenceImageManager } from './reference-image-host.js';
|
|
13
|
+
import { resizeRendererViewport } from './renderer-viewport.js';
|
|
10
14
|
export { pickFitPolicy } from './camera-fit-policy.js';
|
|
11
15
|
// `Scene` is NOT exported: an exported class republishes every method `SceneContents` above just froze.
|
|
12
16
|
export { Picker } from './picker.js';
|
|
@@ -65,6 +69,7 @@ export { PointPicker, decodePickSample } from './point-picker.js';
|
|
|
65
69
|
// Point cloud rendering (Phase 0: IFCx inline; Phase 1+: streaming LAS/LAZ)
|
|
66
70
|
export { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
|
|
67
71
|
export { PointRenderPipeline } from './pointcloud/point-pipeline.js';
|
|
72
|
+
import { modelPlacementBounds, sceneMeshBounds } from './model-placement-bounds.js';
|
|
68
73
|
import { WebGPUDevice } from './device.js';
|
|
69
74
|
import { RenderPipeline } from './pipeline.js';
|
|
70
75
|
import { Camera } from './camera.js';
|
|
@@ -94,6 +99,8 @@ import { ShadowPass, resolveShadowMapResolution } from './shadow-pass.js';
|
|
|
94
99
|
import { fitSunLightMatrix, cameraFrustumFocusCorners } from './shadow-light-matrix.js';
|
|
95
100
|
import { collectShadowOccluders, classifyBatchVisibility, DEFAULT_MIN_CAST_ALPHA } from './shadow-occluders.js';
|
|
96
101
|
import { shouldRouteMeshTransparent, shouldRouteBatchTransparent, splitVisibleIdsByPromotion, DEFAULT_GHOST_ALPHA } from './overlay-routing.js';
|
|
102
|
+
import { XRayAlpha, XRayEpochTracker } from './xray-alpha.js';
|
|
103
|
+
import { PartialBatchRequests } from './partial-batch-requests.js';
|
|
97
104
|
import { colorSaltByte, packEntityLane } from './scene-geometry.js';
|
|
98
105
|
import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
|
|
99
106
|
import { DeviationComputer } from './deviation/deviation-computer.js';
|
|
@@ -187,6 +194,8 @@ export class Renderer {
|
|
|
187
194
|
},
|
|
188
195
|
requestRender: () => this.requestRender(),
|
|
189
196
|
});
|
|
197
|
+
referenceImages = createReferenceImageManager(this);
|
|
198
|
+
getReferenceImages() { return this.referenceImages; }
|
|
190
199
|
postProcessor = null;
|
|
191
200
|
interactionEffects = new InteractionEffectsGovernor();
|
|
192
201
|
edlPass = null;
|
|
@@ -383,13 +392,15 @@ export class Renderer {
|
|
|
383
392
|
// tracker), so callers may either mutate the same Set in place or pass a
|
|
384
393
|
// fresh Set per frame — see the RenderOptions.hiddenIds contract.
|
|
385
394
|
// `_visibilityVersion` drives the per-batch visibility cache;
|
|
386
|
-
// `_partialBatchEpoch` additionally folds colour-override
|
|
387
|
-
// partial sub-batch cache fast path stays correct.
|
|
395
|
+
// `_partialBatchEpoch` additionally folds colour-override and X-Ray changes
|
|
396
|
+
// so the partial sub-batch cache fast path stays correct.
|
|
388
397
|
_visibilityEpochs = new VisibilityEpochTracker();
|
|
398
|
+
_xrayEpochs = new XRayEpochTracker();
|
|
389
399
|
_visibilityVersion = 0;
|
|
390
400
|
_partialBatchEpoch = 0;
|
|
391
401
|
_lastColorOverrideGen = -1;
|
|
392
|
-
|
|
402
|
+
_xrayVersion = 0;
|
|
403
|
+
_lastHadPartialSources = false;
|
|
393
404
|
// Cached per-batch visibility, valid only while `_batchVisibilityEpoch`
|
|
394
405
|
// matches `_visibilityVersion`. Avoids the O(total element count) recompute
|
|
395
406
|
// (+ per-batch visible-id Set allocation) every frame while hide/isolate
|
|
@@ -407,6 +418,8 @@ export class Renderer {
|
|
|
407
418
|
// old model's hydrated mesh.
|
|
408
419
|
_prevHydratedSelection = new Set();
|
|
409
420
|
_prevHydratedSelectionModelIndex = undefined;
|
|
421
|
+
_prevHydratedSelectionItemExpressId;
|
|
422
|
+
_prevHydratedSelectionItemId;
|
|
410
423
|
// One-shot log guard — prints Y-up clip bounds on first section-enable so
|
|
411
424
|
// users can confirm the slider is operating on the intended range.
|
|
412
425
|
_loggedSectionBounds = false;
|
|
@@ -525,6 +538,7 @@ export class Renderer {
|
|
|
525
538
|
this.pipeline = new RenderPipeline(this.device, width, height);
|
|
526
539
|
this.picker = new Picker(this.device, width, height);
|
|
527
540
|
this.overlays.init(this.device.getDevice(), this.device.getFormat(), this.pipeline.getSampleCount());
|
|
541
|
+
this.referenceImages.init(this.device.getDevice(), this.device.getFormat(), this.pipeline.getSampleCount());
|
|
528
542
|
// PostProcessor is optional — if it fails (e.g. mobile GPU lacking
|
|
529
543
|
// depth TEXTURE_BINDING), rendering still works without post-processing.
|
|
530
544
|
try {
|
|
@@ -707,6 +721,7 @@ export class Renderer {
|
|
|
707
721
|
if (this.deviceLost)
|
|
708
722
|
return;
|
|
709
723
|
this.deviceLost = true;
|
|
724
|
+
this.referenceImages.destroy();
|
|
710
725
|
this.deviceLostGeneration = this.initGeneration;
|
|
711
726
|
this.deviceLostInfo = info;
|
|
712
727
|
console.warn('[Renderer] GPU device lost — halting rendering until re-init:', info.message);
|
|
@@ -840,14 +855,10 @@ export class Renderer {
|
|
|
840
855
|
if (!this.pointCloudRenderer) {
|
|
841
856
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
842
857
|
}
|
|
858
|
+
for (const asset of assets)
|
|
859
|
+
this.pointCloudRenderer.setModelTranslation(asset.modelIndex ?? 0, this.scene.getModelTranslation(asset.modelIndex ?? 0));
|
|
843
860
|
this.pointCloudRenderer.setAssets(assets);
|
|
844
|
-
|
|
845
|
-
// reload with a smaller scan). `expandForPointClouds`
|
|
846
|
-
// alone only grows; recompute from scratch to keep
|
|
847
|
-
// fit-to-view + section-plane sliders accurate.
|
|
848
|
-
this.modelBoundsTracker.recompute();
|
|
849
|
-
this.camera.setSceneBounds(this.modelBounds);
|
|
850
|
-
this.requestRender();
|
|
861
|
+
this.refreshPlacementBounds();
|
|
851
862
|
}
|
|
852
863
|
/** Append additional point clouds without clearing existing ones. */
|
|
853
864
|
addPointClouds(assets) {
|
|
@@ -855,6 +866,7 @@ export class Renderer {
|
|
|
855
866
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
856
867
|
}
|
|
857
868
|
for (const asset of assets) {
|
|
869
|
+
this.pointCloudRenderer.setModelTranslation(asset.modelIndex ?? 0, this.scene.getModelTranslation(asset.modelIndex ?? 0));
|
|
858
870
|
this.pointCloudRenderer.addAsset(asset);
|
|
859
871
|
}
|
|
860
872
|
this.modelBoundsTracker.expandForPointClouds();
|
|
@@ -872,9 +884,7 @@ export class Renderer {
|
|
|
872
884
|
/** Drop all point cloud GPU resources. */
|
|
873
885
|
clearPointClouds() {
|
|
874
886
|
this.pointCloudRenderer?.clear();
|
|
875
|
-
this.
|
|
876
|
-
this.camera.setSceneBounds(this.modelBounds);
|
|
877
|
-
this.requestRender();
|
|
887
|
+
this.refreshPlacementBounds();
|
|
878
888
|
}
|
|
879
889
|
/**
|
|
880
890
|
* Streaming entry: open an empty asset that will receive chunks via
|
|
@@ -903,9 +913,7 @@ export class Renderer {
|
|
|
903
913
|
this.pointCloudRenderer?.removeAsset(handle);
|
|
904
914
|
// Bounds may have shrunk — recompute from scratch so fit-to-view
|
|
905
915
|
// and section-plane sliders see fresh extents.
|
|
906
|
-
this.
|
|
907
|
-
this.camera.setSceneBounds(this.modelBounds);
|
|
908
|
-
this.requestRender();
|
|
916
|
+
this.refreshPlacementBounds();
|
|
909
917
|
}
|
|
910
918
|
/**
|
|
911
919
|
* Reassign a streamed point-cloud's expressId after upload. Use
|
|
@@ -918,54 +926,48 @@ export class Renderer {
|
|
|
918
926
|
this.pointCloudRenderer?.relabelAsset(handle, newExpressId);
|
|
919
927
|
this.requestRender();
|
|
920
928
|
}
|
|
921
|
-
/**
|
|
922
|
-
* null if the scene has no mesh geometry. */
|
|
929
|
+
/** Bounds across flat draw batches. */
|
|
923
930
|
computeMeshBounds() {
|
|
924
|
-
|
|
925
|
-
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
926
|
-
let any = false;
|
|
927
|
-
for (const batch of this.scene.getBatchedMeshes()) {
|
|
928
|
-
if (!batch.bounds)
|
|
929
|
-
continue;
|
|
930
|
-
any = true;
|
|
931
|
-
if (batch.bounds.min[0] < minX)
|
|
932
|
-
minX = batch.bounds.min[0];
|
|
933
|
-
if (batch.bounds.min[1] < minY)
|
|
934
|
-
minY = batch.bounds.min[1];
|
|
935
|
-
if (batch.bounds.min[2] < minZ)
|
|
936
|
-
minZ = batch.bounds.min[2];
|
|
937
|
-
if (batch.bounds.max[0] > maxX)
|
|
938
|
-
maxX = batch.bounds.max[0];
|
|
939
|
-
if (batch.bounds.max[1] > maxY)
|
|
940
|
-
maxY = batch.bounds.max[1];
|
|
941
|
-
if (batch.bounds.max[2] > maxZ)
|
|
942
|
-
maxZ = batch.bounds.max[2];
|
|
943
|
-
}
|
|
944
|
-
if (!any)
|
|
945
|
-
return null;
|
|
946
|
-
return { min: { x: minX, y: minY, z: minZ }, max: { x: maxX, y: maxY, z: maxZ } };
|
|
931
|
+
return sceneMeshBounds(this.scene);
|
|
947
932
|
}
|
|
948
933
|
/** Apply rendering options (color mode, fixed override, point size). */
|
|
949
934
|
setPointCloudOptions(opts) {
|
|
950
935
|
this.pointCloudRenderer?.setOptions(opts);
|
|
951
936
|
this.requestRender();
|
|
952
937
|
}
|
|
938
|
+
getModelPlacementBounds(modelIndex, pointCloudHandle) {
|
|
939
|
+
return modelPlacementBounds(this.scene, this.pointCloudRenderer, modelIndex, pointCloudHandle);
|
|
940
|
+
}
|
|
941
|
+
/** Absolute workspace translation in renderer Y-up metres (#4226). */
|
|
942
|
+
setModelTranslation(modelIndex, translation) {
|
|
943
|
+
this.pointCloudRenderer?.validateModelTranslation(modelIndex, translation);
|
|
944
|
+
this.scene.setModelTranslation(modelIndex, translation);
|
|
945
|
+
this.pointCloudRenderer?.setModelTranslation(modelIndex, translation);
|
|
946
|
+
this.clearCaches();
|
|
947
|
+
this.refreshPlacementBounds();
|
|
948
|
+
}
|
|
949
|
+
/** Streamed clouds are addressed by durable asset handle. */
|
|
950
|
+
setPointCloudTranslation(handle, translation) {
|
|
951
|
+
this.pointCloudRenderer?.setAssetTranslation(handle, translation);
|
|
952
|
+
this.clearCaches();
|
|
953
|
+
this.refreshPlacementBounds();
|
|
954
|
+
}
|
|
955
|
+
refreshPlacementBounds() {
|
|
956
|
+
this.modelBoundsTracker.recompute();
|
|
957
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
958
|
+
this.requestRender();
|
|
959
|
+
}
|
|
960
|
+
getPointCloudTransform(handle) {
|
|
961
|
+
return this.pointCloudRenderer?.getAssetTransform(handle);
|
|
962
|
+
}
|
|
953
963
|
/**
|
|
954
|
-
* Set
|
|
955
|
-
*
|
|
956
|
-
*
|
|
957
|
-
* frame's uniform write, no GPU buffer rewrite.
|
|
964
|
+
* Set/clear a streamed cloud's column-major model matrix (16 floats) for
|
|
965
|
+
* IfcMapConversion alignment (#1804). Applied on the next frame's uniform
|
|
966
|
+
* write without rewriting vertex buffers.
|
|
958
967
|
*/
|
|
959
968
|
setPointCloudTransform(handle, matrix) {
|
|
960
969
|
this.pointCloudRenderer?.setAssetTransform(handle, matrix);
|
|
961
|
-
|
|
962
|
-
// matrix-aware) point-cloud bounds into the scene bounds and push
|
|
963
|
-
// them to the camera (matching every other bounds-mutating
|
|
964
|
-
// point-cloud method) so framing / zoom-to-fit targets where the
|
|
965
|
-
// points actually render.
|
|
966
|
-
this.modelBoundsTracker.recompute();
|
|
967
|
-
this.camera.setSceneBounds(this.modelBounds);
|
|
968
|
-
this.requestRender();
|
|
970
|
+
this.refreshPlacementBounds();
|
|
969
971
|
}
|
|
970
972
|
/**
|
|
971
973
|
* Compute BIM ↔ scan deviation for every loaded point cloud asset.
|
|
@@ -1020,7 +1022,7 @@ export class Renderer {
|
|
|
1020
1022
|
const device = this.device.getDevice();
|
|
1021
1023
|
this.scene.appendToBatches(meshes, device, this.pipeline, false);
|
|
1022
1024
|
// Calculate and store model bounds for fitToView
|
|
1023
|
-
this.modelBoundsTracker.updateFromMeshes(meshes);
|
|
1025
|
+
this.modelBoundsTracker.updateFromMeshes(meshes, (index) => this.scene.getModelTranslation(index));
|
|
1024
1026
|
console.log(`[Renderer] Loaded ${meshes.length} meshes`);
|
|
1025
1027
|
// Update camera scene bounds for tight orthographic near/far planes
|
|
1026
1028
|
this.camera.setSceneBounds(this.modelBounds);
|
|
@@ -1040,7 +1042,7 @@ export class Renderer {
|
|
|
1040
1042
|
const device = this.device.getDevice();
|
|
1041
1043
|
this.scene.appendToBatches(meshes, device, this.pipeline, isStreaming);
|
|
1042
1044
|
// Update model bounds incrementally
|
|
1043
|
-
this.modelBoundsTracker.updateFromMeshes(meshes);
|
|
1045
|
+
this.modelBoundsTracker.updateFromMeshes(meshes, (index) => this.scene.getModelTranslation(index));
|
|
1044
1046
|
// Update camera scene bounds for tight orthographic near/far planes
|
|
1045
1047
|
this.camera.setSceneBounds(this.modelBounds);
|
|
1046
1048
|
}
|
|
@@ -1134,7 +1136,10 @@ export class Renderer {
|
|
|
1134
1136
|
* partially-hidden OPAQUE batch casts only its visible subset via the SAME
|
|
1135
1137
|
* cached partial sub-batch the colour pass renders (shared cache key
|
|
1136
1138
|
* `${colorKey}:${id}` + `_partialBatchEpoch`), so no extra clone memory and no
|
|
1137
|
-
* phantom shadow from an individually-hidden element in a shared batch.
|
|
1139
|
+
* phantom shadow from an individually-hidden element in a shared batch. That
|
|
1140
|
+
* sharing lapses while X-Ray splits the same batch further (#4129): the colour
|
|
1141
|
+
* pass then draws `:x0`/`:x1` slots and this slot owns its own clone — right,
|
|
1142
|
+
* since a ghosted element still casts its real shadow.
|
|
1138
1143
|
*
|
|
1139
1144
|
* Transparent (glass-like) partially-hidden parents are left to the collector's
|
|
1140
1145
|
* material-alpha filter — they don't cast at all, so building a visible subset
|
|
@@ -1224,7 +1229,7 @@ export class Renderer {
|
|
|
1224
1229
|
// We compute the same `world` here. When there's no shared origin yet
|
|
1225
1230
|
// (legacy / pre-batch), fall back to a plain f64 fold (local + origin).
|
|
1226
1231
|
const o = meshData.origin;
|
|
1227
|
-
const so = this.scene.getSharedFrameOrigin();
|
|
1232
|
+
const so = this.scene.getSharedFrameOrigin(meshData.modelIndex);
|
|
1228
1233
|
const ox = o ? o[0] : 0, oy = o ? o[1] : 0, oz = o ? o[2] : 0;
|
|
1229
1234
|
const fr = Math.fround;
|
|
1230
1235
|
const sox = so ? fr(so[0]) : null, soy = so ? fr(so[1]) : 0, soz = so ? fr(so[2]) : 0;
|
|
@@ -1310,10 +1315,10 @@ export class Renderer {
|
|
|
1310
1315
|
* must still free the previous model's hydrated mesh (it would otherwise
|
|
1311
1316
|
* stay resident and keep drawing unhighlighted).
|
|
1312
1317
|
*/
|
|
1313
|
-
syncHydratedSelectionMeshes(selected, selectedModelIndex) {
|
|
1318
|
+
syncHydratedSelectionMeshes(selected, selectedModelIndex, itemFilterExpressId, itemFilterItemId) {
|
|
1314
1319
|
const prev = this._prevHydratedSelection;
|
|
1315
|
-
let changed = selected.size !== prev.size
|
|
1316
|
-
||
|
|
1320
|
+
let changed = selected.size !== prev.size || selectedModelIndex !== this._prevHydratedSelectionModelIndex
|
|
1321
|
+
|| itemFilterExpressId !== this._prevHydratedSelectionItemExpressId || itemFilterItemId !== this._prevHydratedSelectionItemId;
|
|
1317
1322
|
if (!changed) {
|
|
1318
1323
|
for (const id of selected) {
|
|
1319
1324
|
if (!prev.has(id)) {
|
|
@@ -1326,7 +1331,9 @@ export class Renderer {
|
|
|
1326
1331
|
return;
|
|
1327
1332
|
this._prevHydratedSelection = new Set(selected);
|
|
1328
1333
|
this._prevHydratedSelectionModelIndex = selectedModelIndex;
|
|
1329
|
-
this.
|
|
1334
|
+
this._prevHydratedSelectionItemExpressId = itemFilterExpressId;
|
|
1335
|
+
this._prevHydratedSelectionItemId = itemFilterItemId;
|
|
1336
|
+
this.scene.disposeHydratedMeshesExcept(selected, selectedModelIndex, itemFilterExpressId, itemFilterItemId);
|
|
1330
1337
|
}
|
|
1331
1338
|
/**
|
|
1332
1339
|
* Pop the frame's validation error scope, recording any captured validation
|
|
@@ -1573,33 +1580,11 @@ export class Renderer {
|
|
|
1573
1580
|
const hasHiddenFilter = options.hiddenIds && options.hiddenIds.size > 0;
|
|
1574
1581
|
const hasIsolatedFilter = options.isolatedIds !== null && options.isolatedIds !== undefined;
|
|
1575
1582
|
const hasVisibilityFiltering = hasHiddenFilter || hasIsolatedFilter;
|
|
1576
|
-
// ─── Visibility / override epoch bookkeeping ────────────────────────
|
|
1577
|
-
// The tracker compares hide/isolate CONTENT against a snapshot, so both
|
|
1578
|
-
// in-place mutation of the caller's Set and a fresh identical Set per
|
|
1579
|
-
// frame behave correctly (see RenderOptions.hiddenIds). Bumping
|
|
1580
|
-
// `_visibilityVersion` invalidates the per-batch visibility cache; the
|
|
1581
|
-
// partial sub-batch cache additionally depends on colour-override
|
|
1582
|
-
// promotion, so its epoch bumps on either.
|
|
1583
|
-
const newVisibilityVersion = this._visibilityEpochs.update(options.hiddenIds, options.isolatedIds);
|
|
1584
|
-
const visibilityChanged = newVisibilityVersion !== this._visibilityVersion;
|
|
1585
|
-
this._visibilityVersion = newVisibilityVersion;
|
|
1586
|
-
const colorOverrideGen = this.scene.getColorOverrideGeneration();
|
|
1587
|
-
if (visibilityChanged || colorOverrideGen !== this._lastColorOverrideGen) {
|
|
1588
|
-
this._lastColorOverrideGen = colorOverrideGen;
|
|
1589
|
-
this._partialBatchEpoch++;
|
|
1590
|
-
}
|
|
1591
|
-
// When hide/isolate turns fully OFF (back to all-visible), release the
|
|
1592
|
-
// partial sub-batch clones built while filtering. They are excluded from
|
|
1593
|
-
// the GPU residency budget and are otherwise only freed on clear()/
|
|
1594
|
-
// finalize/evict — never here — so ~model-sized clone VRAM would stay
|
|
1595
|
-
// pinned until the next model reload. Any override-promotion sub-batches
|
|
1596
|
-
// dropped alongside are rebuilt on demand next frame (cache miss).
|
|
1597
|
-
if (this._lastHadVisibilityFiltering && !hasVisibilityFiltering) {
|
|
1598
|
-
this.scene.dropAllPartialCaches();
|
|
1599
|
-
}
|
|
1600
|
-
this._lastHadVisibilityFiltering = hasVisibilityFiltering;
|
|
1601
1583
|
// Build the selected-id set once per frame so the X-Ray override paths
|
|
1602
1584
|
// can keep highlighted entities at full alpha without per-site checks.
|
|
1585
|
+
// Built BEFORE the epoch bookkeeping below, which consumes it: selection
|
|
1586
|
+
// decides which entities are exempt from fading, so it is an input to the
|
|
1587
|
+
// X-Ray split and must reach the sub-batch cache epoch.
|
|
1603
1588
|
const selectedId = options.selectedId;
|
|
1604
1589
|
const selectedIds = options.selectedIds;
|
|
1605
1590
|
const selectedModelIndex = options.selectedModelIndex;
|
|
@@ -1612,95 +1597,75 @@ export class Renderer {
|
|
|
1612
1597
|
selectedExpressIds.add(id);
|
|
1613
1598
|
}
|
|
1614
1599
|
}
|
|
1615
|
-
const
|
|
1600
|
+
const itemFilterExpressId = (options.selectedItemId !== undefined && selectedId !== undefined && selectedId !== null) ? selectedId : undefined;
|
|
1601
|
+
const itemFilterItemId = itemFilterExpressId !== undefined ? options.selectedItemId : undefined; // #4382
|
|
1602
|
+
// ─── Visibility / override epoch bookkeeping ────────────────────────
|
|
1603
|
+
// The tracker compares hide/isolate CONTENT against a snapshot, so both
|
|
1604
|
+
// in-place mutation of the caller's Set and a fresh identical Set per
|
|
1605
|
+
// frame behave correctly (see RenderOptions.hiddenIds). Bumping
|
|
1606
|
+
// `_visibilityVersion` invalidates the per-batch visibility cache.
|
|
1607
|
+
//
|
|
1608
|
+
// The partial sub-batch epoch must carry EVERYTHING that decides a
|
|
1609
|
+
// sub-batch's membership — hide/isolate, colour-override promotion, and
|
|
1610
|
+
// the X-Ray split incl. selection (#4129) — because
|
|
1611
|
+
// `getOrCreatePartialBatch`'s fast path returns the cached clone without
|
|
1612
|
+
// ever inspecting the id set it was handed. A missing input therefore
|
|
1613
|
+
// shows up as a stale subset on screen, not as an extra rebuild.
|
|
1614
|
+
const newVisibilityVersion = this._visibilityEpochs.update(options.hiddenIds, options.isolatedIds);
|
|
1615
|
+
const visibilityChanged = newVisibilityVersion !== this._visibilityVersion;
|
|
1616
|
+
this._visibilityVersion = newVisibilityVersion;
|
|
1617
|
+
const colorOverrideGen = this.scene.getColorOverrideGeneration();
|
|
1618
|
+
const xrayVersion = this._xrayEpochs.update(options, selectedExpressIds);
|
|
1619
|
+
const partialEpochChanged = visibilityChanged || colorOverrideGen !== this._lastColorOverrideGen || xrayVersion !== this._xrayVersion;
|
|
1620
|
+
if (partialEpochChanged) {
|
|
1621
|
+
this._lastColorOverrideGen = colorOverrideGen;
|
|
1622
|
+
this._xrayVersion = xrayVersion;
|
|
1623
|
+
this._partialBatchEpoch++;
|
|
1624
|
+
}
|
|
1625
|
+
// Every source of partial sub-batches is off (all-visible AND no X-Ray)
|
|
1626
|
+
// — release the clones wholesale, or ~model-sized VRAM stays pinned till
|
|
1627
|
+
// the next model reload (see `partial-batch-cache.ts`). Slots orphaned
|
|
1628
|
+
// WHILE X-Ray is still on are the narrower `retireUnusedAlphaSlots` case.
|
|
1629
|
+
const xrayActive = (options.transparencyOverrides?.size ?? 0) > 0 || options.ghostExceptIds != null;
|
|
1630
|
+
const hasPartialSources = hasVisibilityFiltering || xrayActive;
|
|
1631
|
+
if (this._lastHadPartialSources && !hasPartialSources) {
|
|
1632
|
+
this.scene.dropAllPartialCaches();
|
|
1633
|
+
}
|
|
1634
|
+
this._lastHadPartialSources = hasPartialSources;
|
|
1616
1635
|
// Free hydrated (pick/selection) individual meshes whose entity is no
|
|
1617
1636
|
// longer selected BEFORE we snapshot the mesh list, so stale glass
|
|
1618
1637
|
// doesn't double-draw over its batch copy or accumulate until clear().
|
|
1619
1638
|
// Only acts on a selection change (avoids per-frame buffer churn) and
|
|
1620
1639
|
// never touches authored (non-hydrated) or batch geometry.
|
|
1621
|
-
this.syncHydratedSelectionMeshes(selectedExpressIds, selectedModelIndex);
|
|
1640
|
+
this.syncHydratedSelectionMeshes(selectedExpressIds, selectedModelIndex, itemFilterExpressId, itemFilterItemId);
|
|
1622
1641
|
let meshes = this.scene.getMeshes();
|
|
1623
1642
|
// Keep the GPU-instanced occurrences' per-instance selected flag in sync.
|
|
1624
1643
|
// The Scene diff makes this a no-op (no writeBuffer) when the set is
|
|
1625
1644
|
// unchanged, so calling it every frame is cheap; it no-ops entirely when
|
|
1626
1645
|
// no instanced data is loaded. The flat path handles selection inline
|
|
1627
1646
|
// below via `selectedExpressIds`.
|
|
1628
|
-
this.scene.setInstancedSelection(selectedExpressIds);
|
|
1647
|
+
this.scene.setInstancedSelection(selectedExpressIds, itemFilterExpressId, itemFilterItemId);
|
|
1629
1648
|
// Mirror hide/isolate onto the instanced occurrences (the flat path filters
|
|
1630
1649
|
// its mesh list by hiddenIds/isolatedIds below; the instanced pass can't, so
|
|
1631
1650
|
// it carries a per-instance hidden flag the shader discards on). Diffed → a
|
|
1632
1651
|
// no-op when visibility is unchanged.
|
|
1633
1652
|
this.scene.setInstancedVisibility(options.hiddenIds, options.isolatedIds);
|
|
1634
1653
|
// Per-frame alpha overrides for X-Ray mode. See RenderOptions.transparencyOverrides.
|
|
1635
|
-
//
|
|
1636
|
-
// and uniform-write decisions for the same batch/mesh
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
//
|
|
1641
|
-
// fades to ghostAlpha. It feeds the same alpha-override machinery as
|
|
1642
|
-
// transparencyOverrides (explicit per-id entries win), so it routes
|
|
1643
|
-
// through the transparent pipeline with no extra call sites — and avoids
|
|
1644
|
-
// building a Map over every element just to fade "the rest".
|
|
1645
|
-
const ghostExceptIds = options.ghostExceptIds ?? null;
|
|
1654
|
+
// XRayAlpha snapshots the caller's map so mid-frame mutation can't desync
|
|
1655
|
+
// classification and uniform-write decisions for the same batch/mesh, and
|
|
1656
|
+
// owns the per-entity resolution + the mixed-batch partition (#4129).
|
|
1657
|
+
// X-Ray *context* mode (`ghostExceptIds`) feeds the same machinery, so it
|
|
1658
|
+
// routes through the transparent pipeline with no extra call sites — and
|
|
1659
|
+
// avoids building a Map over every element just to fade "the rest".
|
|
1646
1660
|
const ghostAlpha = options.ghostAlpha ?? DEFAULT_GHOST_ALPHA;
|
|
1647
1661
|
// X-Ray reaches the instanced pass too (#2606). Without this, ghosting
|
|
1648
1662
|
// stopped at the flat geometry: on a model whose facade is instanced,
|
|
1649
1663
|
// the user asked to fade the building and got a solid facade standing
|
|
1650
1664
|
// in front of a ghosted interior.
|
|
1651
|
-
this.scene.setInstancedGhosting(ghostExceptIds, selectedExpressIds, ghostAlpha);
|
|
1652
|
-
const
|
|
1653
|
-
const
|
|
1654
|
-
const
|
|
1655
|
-
if (!hasTxOverrides)
|
|
1656
|
-
return fallback;
|
|
1657
|
-
// Selected meshes are exempt — the highlight pass renders them last,
|
|
1658
|
-
// but exempting here also keeps mesh classification + uniform writes
|
|
1659
|
-
// consistent so a selected mesh never enters the transparent pipeline
|
|
1660
|
-
// because of its own override entry.
|
|
1661
|
-
if (hasSelected && selectedExpressIds.has(expressId))
|
|
1662
|
-
return fallback;
|
|
1663
|
-
const a = txOverrides?.get(expressId);
|
|
1664
|
-
if (a !== undefined)
|
|
1665
|
-
return a;
|
|
1666
|
-
if (hasGhost && !ghostExceptIds.has(expressId))
|
|
1667
|
-
return ghostAlpha;
|
|
1668
|
-
return fallback;
|
|
1669
|
-
};
|
|
1670
|
-
// Cache resolved batch alpha for the frame: classification needs it
|
|
1671
|
-
// (opaque vs transparent routing) and renderBatch needs it for the
|
|
1672
|
-
// uniform write. Without the cache we'd walk batch.expressIds twice
|
|
1673
|
-
// per batch per frame, which becomes the dominant JS cost in X-Ray.
|
|
1674
|
-
const batchAlphaCache = hasTxOverrides
|
|
1675
|
-
? new WeakMap()
|
|
1676
|
-
: null;
|
|
1677
|
-
const alphaForBatch = (batch, fallback) => {
|
|
1678
|
-
if (!hasTxOverrides)
|
|
1679
|
-
return fallback;
|
|
1680
|
-
const cached = batchAlphaCache.get(batch);
|
|
1681
|
-
if (cached !== undefined)
|
|
1682
|
-
return cached;
|
|
1683
|
-
let minAlpha = Infinity;
|
|
1684
|
-
for (const eid of batch.expressIds) {
|
|
1685
|
-
// Selected ids never drag down a batch's alpha — the highlight
|
|
1686
|
-
// pass redraws them on top, but excluding here also means a
|
|
1687
|
-
// batch made entirely of selected entities stays opaque.
|
|
1688
|
-
if (hasSelected && selectedExpressIds.has(eid))
|
|
1689
|
-
continue;
|
|
1690
|
-
const a = txOverrides?.get(eid);
|
|
1691
|
-
if (a !== undefined) {
|
|
1692
|
-
if (a < minAlpha)
|
|
1693
|
-
minAlpha = a;
|
|
1694
|
-
}
|
|
1695
|
-
else if (hasGhost && !ghostExceptIds.has(eid)) {
|
|
1696
|
-
if (ghostAlpha < minAlpha)
|
|
1697
|
-
minAlpha = ghostAlpha;
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
const resolved = minAlpha === Infinity ? fallback : minAlpha;
|
|
1701
|
-
batchAlphaCache.set(batch, resolved);
|
|
1702
|
-
return resolved;
|
|
1703
|
-
};
|
|
1665
|
+
this.scene.setInstancedGhosting(options.ghostExceptIds ?? null, selectedExpressIds, ghostAlpha);
|
|
1666
|
+
const xray = new XRayAlpha(options, selectedExpressIds);
|
|
1667
|
+
const alphaForMesh = (expressId, fallback) => xray.forEntity(expressId, fallback);
|
|
1668
|
+
const alphaForBatch = (batch, fallback) => xray.forBatch(batch, fallback);
|
|
1704
1669
|
// Lens / Pset color overrides: when an entity has an override, force
|
|
1705
1670
|
// its base draw through the opaque pipeline so it writes depth. The
|
|
1706
1671
|
// overlay paint pass uses depthCompare 'equal' and otherwise silently
|
|
@@ -2186,52 +2151,13 @@ export class Renderer {
|
|
|
2186
2151
|
const opaqueBatches = [];
|
|
2187
2152
|
const transparentBatches = [];
|
|
2188
2153
|
// PERFORMANCE FIX: Track partially visible batches for sub-batch rendering
|
|
2189
|
-
// Instead of creating 10,000+ individual meshes, we create cached sub-batches
|
|
2190
|
-
|
|
2191
|
-
//
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
const pushVisibleAsPartial = (sourceBatch, visibleIds, isTransparent) => {
|
|
2197
|
-
const baseKey = `${sourceBatch.colorKey}:${sourceBatch.id}`;
|
|
2198
|
-
if (!isTransparent) {
|
|
2199
|
-
partiallyVisibleBatches.push({
|
|
2200
|
-
sourceBatchKey: baseKey,
|
|
2201
|
-
colorKey: sourceBatch.colorKey,
|
|
2202
|
-
visibleIds,
|
|
2203
|
-
color: sourceBatch.color,
|
|
2204
|
-
});
|
|
2205
|
-
return;
|
|
2206
|
-
}
|
|
2207
|
-
const split = splitVisibleIdsByPromotion(visibleIds, colorOverrides);
|
|
2208
|
-
// No promotion or every visible id promoted → single sub-batch,
|
|
2209
|
-
// classifier downstream routes via shouldRouteBatchTransparent.
|
|
2210
|
-
if (split == null || split.remaining.size === 0) {
|
|
2211
|
-
partiallyVisibleBatches.push({
|
|
2212
|
-
sourceBatchKey: baseKey,
|
|
2213
|
-
colorKey: sourceBatch.colorKey,
|
|
2214
|
-
visibleIds,
|
|
2215
|
-
color: sourceBatch.color,
|
|
2216
|
-
});
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
// Mixed — emit one promoted (opaque-routed) and one remaining
|
|
2220
|
-
// (transparent-routed) sub-batch. Distinct sourceBatchKeys so the
|
|
2221
|
-
// partial-batch cache can hold both simultaneously.
|
|
2222
|
-
partiallyVisibleBatches.push({
|
|
2223
|
-
sourceBatchKey: `${baseKey}:promoted`,
|
|
2224
|
-
colorKey: sourceBatch.colorKey,
|
|
2225
|
-
visibleIds: split.promoted,
|
|
2226
|
-
color: sourceBatch.color,
|
|
2227
|
-
});
|
|
2228
|
-
partiallyVisibleBatches.push({
|
|
2229
|
-
sourceBatchKey: `${baseKey}:remaining`,
|
|
2230
|
-
colorKey: sourceBatch.colorKey,
|
|
2231
|
-
visibleIds: split.remaining,
|
|
2232
|
-
color: sourceBatch.color,
|
|
2233
|
-
});
|
|
2234
|
-
};
|
|
2154
|
+
// Instead of creating 10,000+ individual meshes, we create cached sub-batches.
|
|
2155
|
+
// The collector also owns the override-promotion split (#677) and the
|
|
2156
|
+
// per-entity X-Ray alpha split (#4129) — see partial-batch-requests.ts.
|
|
2157
|
+
const partialRequests = new PartialBatchRequests(colorOverrides, xray, (b) => this.scene.canPartitionBatch(b));
|
|
2158
|
+
const partiallyVisibleBatches = partialRequests.items;
|
|
2159
|
+
const pushVisibleAsPartial = partialRequests.pushVisible.bind(partialRequests);
|
|
2160
|
+
const pushAlphaSplit = partialRequests.pushAlphaSplit.bind(partialRequests);
|
|
2235
2161
|
for (const batch of allBatchedMeshes) {
|
|
2236
2162
|
// Frustum culling: skip batches entirely outside the camera view
|
|
2237
2163
|
if (batch.bounds) {
|
|
@@ -2269,7 +2195,7 @@ export class Renderer {
|
|
|
2269
2195
|
// The visible subset was computed once for this
|
|
2270
2196
|
// visibility epoch (cached) — reuse it, don't rebuild.
|
|
2271
2197
|
const visibleIds = vis.visibleIds;
|
|
2272
|
-
if (visibleIds && visibleIds.size > 0) {
|
|
2198
|
+
if (visibleIds && visibleIds.size > 0 && !pushAlphaSplit(batch, visibleIds)) {
|
|
2273
2199
|
pushVisibleAsPartial(batch, visibleIds, nativelyTransparent);
|
|
2274
2200
|
}
|
|
2275
2201
|
// A COLD parent has no CPU meshData, so the partial
|
|
@@ -2293,6 +2219,11 @@ export class Renderer {
|
|
|
2293
2219
|
continue;
|
|
2294
2220
|
}
|
|
2295
2221
|
this.scene.recordBatchDrawn(batch);
|
|
2222
|
+
// X-Ray names entities, not batches: a batch whose entities
|
|
2223
|
+
// resolve to different alphas splits per alpha (#4129) rather
|
|
2224
|
+
// than fading whole to the minimum.
|
|
2225
|
+
if (pushAlphaSplit(batch, null))
|
|
2226
|
+
continue;
|
|
2296
2227
|
// Transparent batches with mixed
|
|
2297
2228
|
// override membership must be split so non-overridden batchmates
|
|
2298
2229
|
// stay transparent — see splitVisibleIdsByPromotion / issue #677.
|
|
@@ -2312,6 +2243,15 @@ export class Renderer {
|
|
|
2312
2243
|
opaqueBatches.push(batch);
|
|
2313
2244
|
}
|
|
2314
2245
|
}
|
|
2246
|
+
// Retire the alpha-split slots this frame's classification did NOT
|
|
2247
|
+
// ask for (#4129 review): an X-Ray edit that makes a batch uniform
|
|
2248
|
+
// again, or that shrinks its group count, orphans slots the draw
|
|
2249
|
+
// loop below never revisits. Only on an epoch change — while the
|
|
2250
|
+
// state holds, the split shape is stable and nothing can be
|
|
2251
|
+
// orphaned, so the steady-state cost is zero.
|
|
2252
|
+
if (partialEpochChanged) {
|
|
2253
|
+
this.scene.retireUnusedAlphaSlots(partialRequests.requestedKeys());
|
|
2254
|
+
}
|
|
2315
2255
|
// Build a uniform template ONCE per frame — shared across all batches.
|
|
2316
2256
|
// Only the 4-float color (offset 32) differs per batch; everything else
|
|
2317
2257
|
// (viewProj, identity model, material, section plane, flags) is identical.
|
|
@@ -2570,6 +2510,11 @@ export class Renderer {
|
|
|
2570
2510
|
// would show open, un-capped cut holes.
|
|
2571
2511
|
const opaqueSubBatches = [];
|
|
2572
2512
|
if (partiallyVisibleBatches.length > 0) {
|
|
2513
|
+
// Transparent sub-batches are deferred to a second pass below:
|
|
2514
|
+
// they write no depth, so an opaque sub-batch drawn after one
|
|
2515
|
+
// paints straight over it. That is routine since #4129 — an
|
|
2516
|
+
// X-Rayed batch emits a faded group AND a solid one.
|
|
2517
|
+
const transparentSubBatches = [];
|
|
2573
2518
|
for (const { sourceBatchKey, colorKey, visibleIds, color } of partiallyVisibleBatches) {
|
|
2574
2519
|
// Get or create a cached sub-batch for this visibility state
|
|
2575
2520
|
const subBatch = this.scene.getOrCreatePartialBatch(sourceBatchKey, colorKey, visibleIds, device, this.pipeline, this._partialBatchEpoch);
|
|
@@ -2580,21 +2525,24 @@ export class Renderer {
|
|
|
2580
2525
|
// lens/Pset colour override, so the overlay paint pass finds depth.
|
|
2581
2526
|
const isTransparent = shouldRouteBatchTransparent(alphaForBatch(subBatch, color[3]), subBatch.expressIds, colorOverrides);
|
|
2582
2527
|
if (isTransparent) {
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
else {
|
|
2586
|
-
// Opaque (incl. material-layer slices): double-sided.
|
|
2587
|
-
// Layer slices are NOT culled — since #1311 they are
|
|
2588
|
-
// open watertight-skin bands with unreliable winding,
|
|
2589
|
-
// so culling punched holes (wall read hollow). See the
|
|
2590
|
-
// full-batch path above.
|
|
2591
|
-
pass.setPipeline(pipeFor(subBatch, 'opaque'));
|
|
2592
|
-
opaqueSubBatches.push(subBatch);
|
|
2528
|
+
transparentSubBatches.push(subBatch);
|
|
2529
|
+
continue;
|
|
2593
2530
|
}
|
|
2531
|
+
// Opaque (incl. material-layer slices): double-sided.
|
|
2532
|
+
// Layer slices are NOT culled — since #1311 they are
|
|
2533
|
+
// open watertight-skin bands with unreliable winding,
|
|
2534
|
+
// so culling punched holes (wall read hollow). See the
|
|
2535
|
+
// full-batch path above.
|
|
2536
|
+
pass.setPipeline(pipeFor(subBatch, 'opaque'));
|
|
2537
|
+
opaqueSubBatches.push(subBatch);
|
|
2594
2538
|
// Render the sub-batch as a single draw call
|
|
2595
2539
|
renderBatch(subBatch);
|
|
2596
2540
|
}
|
|
2597
2541
|
}
|
|
2542
|
+
for (const subBatch of transparentSubBatches) {
|
|
2543
|
+
pass.setPipeline(pipeFor(subBatch, 'transparent'));
|
|
2544
|
+
renderBatch(subBatch);
|
|
2545
|
+
}
|
|
2598
2546
|
// Reset to opaque pipeline for subsequent rendering
|
|
2599
2547
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
2600
2548
|
}
|
|
@@ -2650,7 +2598,8 @@ export class Renderer {
|
|
|
2650
2598
|
existingPieceCounts.set(key, (existingPieceCounts.get(key) ?? 0) + 1);
|
|
2651
2599
|
}
|
|
2652
2600
|
for (const selId of visibleSelectedIds) {
|
|
2653
|
-
const
|
|
2601
|
+
const pieceItemId = selId === itemFilterExpressId ? itemFilterItemId : undefined; // #4382
|
|
2602
|
+
const pieces = this.scene.getMeshDataPieces(selId, selectedModelIndex, pieceItemId);
|
|
2654
2603
|
if (!pieces || pieces.length === 0)
|
|
2655
2604
|
continue;
|
|
2656
2605
|
const seenOrdinalsByKey = new Map();
|
|
@@ -2671,6 +2620,8 @@ export class Renderer {
|
|
|
2671
2620
|
return false;
|
|
2672
2621
|
if (selectedModelIndex !== undefined && mesh.modelIndex !== selectedModelIndex)
|
|
2673
2622
|
return false;
|
|
2623
|
+
if (mesh.expressId === itemFilterExpressId && mesh.geometryItemId !== itemFilterItemId)
|
|
2624
|
+
return false; // #4382
|
|
2674
2625
|
return true;
|
|
2675
2626
|
})
|
|
2676
2627
|
: [];
|
|
@@ -2857,6 +2808,7 @@ export class Renderer {
|
|
|
2857
2808
|
// Section-plane gizmo, 2D section cap and every standalone 3D
|
|
2858
2809
|
// overlay (annotation / alignment / grid / DXF / clash / symbolic
|
|
2859
2810
|
// text). One draw call into the pass — see RendererOverlays.draw().
|
|
2811
|
+
this.referenceImages.draw(pass, viewProj);
|
|
2860
2812
|
this.overlays.draw(pass, {
|
|
2861
2813
|
options,
|
|
2862
2814
|
viewProj,
|
|
@@ -3021,13 +2973,11 @@ export class Renderer {
|
|
|
3021
2973
|
return new Set();
|
|
3022
2974
|
return this.pickingManager.pickRect(x0, y0, x1, y1, options, this.activePickClip());
|
|
3023
2975
|
}
|
|
3024
|
-
/**
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
* These are scaled internally to match the actual canvas pixel dimensions.
|
|
3030
|
-
*/
|
|
2976
|
+
/** Whether the last rendered frame clipped surfaces (section, terrain or box). */
|
|
2977
|
+
hasActiveClipping() {
|
|
2978
|
+
return this._activePickSection !== null || this._activePickClipBox !== null;
|
|
2979
|
+
}
|
|
2980
|
+
/** Exact surface raycast in CSS canvas coordinates; does not apply clipping. */
|
|
3031
2981
|
raycastScene(x, y, options) {
|
|
3032
2982
|
return this.raycastEngine.raycastScene(x, y, options);
|
|
3033
2983
|
}
|
|
@@ -3088,20 +3038,21 @@ export class Renderer {
|
|
|
3088
3038
|
* Resize canvas
|
|
3089
3039
|
*/
|
|
3090
3040
|
resize(width, height) {
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
this.
|
|
3104
|
-
|
|
3041
|
+
resizeRendererViewport(this.canvas, this.camera, width, height);
|
|
3042
|
+
}
|
|
3043
|
+
/** Stage one new owner; borrowed mesh buffers must remain immutable until disposal. */
|
|
3044
|
+
prepareAuthoredOwner(parts) {
|
|
3045
|
+
if (!this.device.isInitialized() || !this.pipeline)
|
|
3046
|
+
throw new Error('Renderer is not initialized.');
|
|
3047
|
+
const prepared = this.scene.prepareAuthoredOwner(parts, this.device.getDevice(), this.pipeline);
|
|
3048
|
+
return { commit: () => { prepared.commit(); this.refreshPlacementBounds(); this.invalidateBVHCache(); this.requestRender(); }, dispose: prepared.dispose };
|
|
3049
|
+
}
|
|
3050
|
+
prepareTexturedOwner(mesh) { if (!mesh.uvs || !(mesh.texture || (mesh.textureRef && mesh.textureBitmap)))
|
|
3051
|
+
throw new Error('A new textured owner requires an image and UVs.'); return this.prepareAuthoredOwner([mesh]); }
|
|
3052
|
+
getAppearancePreview() {
|
|
3053
|
+
if (!this.pipeline)
|
|
3054
|
+
throw new Error('Renderer must be initialized before previewing appearance');
|
|
3055
|
+
return this.scene.appearancePreview(this.device.getDevice(), this.pipeline);
|
|
3105
3056
|
}
|
|
3106
3057
|
getCamera() {
|
|
3107
3058
|
return this.camera;
|
|
@@ -3384,6 +3335,7 @@ export class Renderer {
|
|
|
3384
3335
|
// Section-plane gizmo, 2D section overlay and the symbolic annotation
|
|
3385
3336
|
// pipelines — see RendererOverlays.destroy().
|
|
3386
3337
|
this.overlays.destroy();
|
|
3338
|
+
this.referenceImages.destroy();
|
|
3387
3339
|
// Point cloud GPU resources
|
|
3388
3340
|
this.pointCloudRenderer?.clear();
|
|
3389
3341
|
this.pointCloudRenderer = null;
|