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