@ifc-lite/renderer 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edl-pass.d.ts +52 -0
- package/dist/edl-pass.d.ts.map +1 -0
- package/dist/edl-pass.js +204 -0
- package/dist/edl-pass.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +314 -19
- package/dist/index.js.map +1 -1
- package/dist/picker.d.ts +22 -3
- package/dist/picker.d.ts.map +1 -1
- package/dist/picker.js +48 -8
- package/dist/picker.js.map +1 -1
- package/dist/picking-manager.d.ts +14 -1
- package/dist/picking-manager.d.ts.map +1 -1
- package/dist/picking-manager.js +13 -1
- package/dist/picking-manager.js.map +1 -1
- package/dist/point-picker.d.ts +61 -0
- package/dist/point-picker.d.ts.map +1 -0
- package/dist/point-picker.js +223 -0
- package/dist/point-picker.js.map +1 -0
- package/dist/pointcloud/point-cloud-node.d.ts +56 -0
- package/dist/pointcloud/point-cloud-node.d.ts.map +1 -0
- package/dist/pointcloud/point-cloud-node.js +112 -0
- package/dist/pointcloud/point-cloud-node.js.map +1 -0
- package/dist/pointcloud/point-cloud-renderer.d.ts +135 -0
- package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -0
- package/dist/pointcloud/point-cloud-renderer.js +296 -0
- package/dist/pointcloud/point-cloud-renderer.js.map +1 -0
- package/dist/pointcloud/point-pipeline.d.ts +25 -0
- package/dist/pointcloud/point-pipeline.d.ts.map +1 -0
- package/dist/pointcloud/point-pipeline.js +111 -0
- package/dist/pointcloud/point-pipeline.js.map +1 -0
- package/dist/pointcloud/point-shader.wgsl.d.ts +22 -0
- package/dist/pointcloud/point-shader.wgsl.d.ts.map +1 -0
- package/dist/pointcloud/point-shader.wgsl.js +212 -0
- package/dist/pointcloud/point-shader.wgsl.js.map +1 -0
- package/dist/types.d.ts +15 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -26,6 +26,10 @@ export { ZeroCopyGpuUploader, createZeroCopyUploader, } from './zero-copy-upload
|
|
|
26
26
|
// Extracted manager classes
|
|
27
27
|
export { PickingManager } from './picking-manager.js';
|
|
28
28
|
export { RaycastEngine } from './raycast-engine.js';
|
|
29
|
+
export { PointPicker, decodePickSample } from './point-picker.js';
|
|
30
|
+
// Point cloud rendering (Phase 0: IFCx inline; Phase 1+: streaming LAS/LAZ)
|
|
31
|
+
export { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
|
|
32
|
+
export { PointRenderPipeline } from './pointcloud/point-pipeline.js';
|
|
29
33
|
import { WebGPUDevice } from './device.js';
|
|
30
34
|
import { RenderPipeline, InstancedRenderPipeline } from './pipeline.js';
|
|
31
35
|
import { Camera } from './camera.js';
|
|
@@ -40,6 +44,8 @@ import { DEFAULT_CAP_STYLE, HATCH_PATTERN_IDS } from './section-cap-style.js';
|
|
|
40
44
|
import { PickingManager } from './picking-manager.js';
|
|
41
45
|
import { RaycastEngine } from './raycast-engine.js';
|
|
42
46
|
import { PostProcessor } from './post-processor.js';
|
|
47
|
+
import { EdlPass } from './edl-pass.js';
|
|
48
|
+
import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
|
|
43
49
|
const MAX_ENCODED_ENTITY_ID = 0xFFFFFF;
|
|
44
50
|
let warnedEntityIdRange = false;
|
|
45
51
|
/**
|
|
@@ -56,6 +62,14 @@ export class Renderer {
|
|
|
56
62
|
sectionPlaneRenderer = null;
|
|
57
63
|
section2DOverlayRenderer = null;
|
|
58
64
|
postProcessor = null;
|
|
65
|
+
edlPass = null;
|
|
66
|
+
edlOptions = {
|
|
67
|
+
enabled: false,
|
|
68
|
+
strength: 1,
|
|
69
|
+
radiusPx: 1,
|
|
70
|
+
highQuality: true,
|
|
71
|
+
};
|
|
72
|
+
pointCloudRenderer = null;
|
|
59
73
|
visualEnhancementState = {
|
|
60
74
|
enabled: true,
|
|
61
75
|
edgeContrast: { enabled: true, intensity: 1.0 },
|
|
@@ -113,9 +127,217 @@ export class Renderer {
|
|
|
113
127
|
contactRadius: 1.0,
|
|
114
128
|
contactIntensity: 0.3,
|
|
115
129
|
}, this.pipeline.getSampleCount());
|
|
130
|
+
this.pointCloudRenderer = new PointCloudRenderer(this.device.getDevice(), this.device.getFormat(), 'depth24plus-stencil8', this.pipeline.getSampleCount());
|
|
131
|
+
this.edlPass = new EdlPass(this.device, this.pipeline.getSampleCount());
|
|
116
132
|
this.camera.setAspect(width / height);
|
|
117
133
|
// Update picking manager with initialized picker
|
|
118
134
|
this.pickingManager.setPicker(this.picker);
|
|
135
|
+
// Provide a snapshot of pickable point nodes per pick. The
|
|
136
|
+
// sizing must mirror the live splat shader so click hit-testing
|
|
137
|
+
// matches what the user actually sees on screen.
|
|
138
|
+
this.pickingManager.setPointPickProvider(() => {
|
|
139
|
+
const pcr = this.pointCloudRenderer;
|
|
140
|
+
if (!pcr || !pcr.hasAssets())
|
|
141
|
+
return null;
|
|
142
|
+
const opts = pcr.getOptions();
|
|
143
|
+
const sizeMode = opts.sizeMode === 'fixed-px' ? 0 : opts.sizeMode === 'adaptive-world' ? 1 : 2;
|
|
144
|
+
return {
|
|
145
|
+
nodes: pcr.getPickNodes(),
|
|
146
|
+
sizing: {
|
|
147
|
+
sizeMode: sizeMode,
|
|
148
|
+
worldRadius: opts.worldRadius,
|
|
149
|
+
pointSizePx: opts.pointSize,
|
|
150
|
+
clickTolerancePx: 2,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Replace all loaded point clouds with `assets`.
|
|
157
|
+
*
|
|
158
|
+
* Phase 0 entry point — single-chunk inline assets from IFCx
|
|
159
|
+
* (`pcd::base64`, `points::array`, `points::base64`). Future phases
|
|
160
|
+
* accept streaming sources via a different overload.
|
|
161
|
+
*/
|
|
162
|
+
setPointClouds(assets) {
|
|
163
|
+
if (!this.pointCloudRenderer) {
|
|
164
|
+
throw new Error('Renderer not initialized. Call init() first.');
|
|
165
|
+
}
|
|
166
|
+
this.pointCloudRenderer.setAssets(assets);
|
|
167
|
+
// Replace, not append — bounds may have shrunk (e.g. an IFCx
|
|
168
|
+
// reload with a smaller scan). `expandModelBoundsForPointClouds`
|
|
169
|
+
// alone only grows; recompute from scratch to keep
|
|
170
|
+
// fit-to-view + section-plane sliders accurate.
|
|
171
|
+
this.recomputeModelBounds();
|
|
172
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
173
|
+
this.requestRender();
|
|
174
|
+
}
|
|
175
|
+
/** Append additional point clouds without clearing existing ones. */
|
|
176
|
+
addPointClouds(assets) {
|
|
177
|
+
if (!this.pointCloudRenderer) {
|
|
178
|
+
throw new Error('Renderer not initialized. Call init() first.');
|
|
179
|
+
}
|
|
180
|
+
for (const asset of assets) {
|
|
181
|
+
this.pointCloudRenderer.addAsset(asset);
|
|
182
|
+
}
|
|
183
|
+
this.expandModelBoundsForPointClouds();
|
|
184
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
185
|
+
this.requestRender();
|
|
186
|
+
}
|
|
187
|
+
/** Total number of point cloud assets currently uploaded. */
|
|
188
|
+
getPointCloudAssetCount() {
|
|
189
|
+
return this.pointCloudRenderer?.getNodeCount() ?? 0;
|
|
190
|
+
}
|
|
191
|
+
/** Total number of points across all point cloud assets. */
|
|
192
|
+
getPointCloudPointCount() {
|
|
193
|
+
return this.pointCloudRenderer?.getPointCount() ?? 0;
|
|
194
|
+
}
|
|
195
|
+
/** Drop all point cloud GPU resources. */
|
|
196
|
+
clearPointClouds() {
|
|
197
|
+
this.pointCloudRenderer?.clear();
|
|
198
|
+
this.recomputeModelBounds();
|
|
199
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
200
|
+
this.requestRender();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Streaming entry: open an empty asset that will receive chunks via
|
|
204
|
+
* `appendPointCloudChunk`. Call `endPointCloudStream` when no more
|
|
205
|
+
* chunks will arrive (currently a no-op but kept for symmetry).
|
|
206
|
+
*/
|
|
207
|
+
beginPointCloudStream(meta) {
|
|
208
|
+
if (!this.pointCloudRenderer) {
|
|
209
|
+
throw new Error('Renderer not initialized. Call init() first.');
|
|
210
|
+
}
|
|
211
|
+
return this.pointCloudRenderer.beginAsset(meta);
|
|
212
|
+
}
|
|
213
|
+
appendPointCloudChunk(handle, chunk) {
|
|
214
|
+
if (!this.pointCloudRenderer)
|
|
215
|
+
return;
|
|
216
|
+
this.pointCloudRenderer.appendChunk(handle, chunk);
|
|
217
|
+
this.expandModelBoundsForPointClouds();
|
|
218
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
219
|
+
this.requestRender();
|
|
220
|
+
}
|
|
221
|
+
endPointCloudStream(handle) {
|
|
222
|
+
this.pointCloudRenderer?.endAsset(handle);
|
|
223
|
+
this.requestRender();
|
|
224
|
+
}
|
|
225
|
+
removePointCloudAsset(handle) {
|
|
226
|
+
this.pointCloudRenderer?.removeAsset(handle);
|
|
227
|
+
// Bounds may have shrunk — recompute from scratch so fit-to-view
|
|
228
|
+
// and section-plane sliders see fresh extents.
|
|
229
|
+
this.recomputeModelBounds();
|
|
230
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
231
|
+
this.requestRender();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Reassign a streamed point-cloud's expressId after upload. Use
|
|
235
|
+
* this when the federation registry assigns a new model offset and
|
|
236
|
+
* the renderer needs to emit the post-offset globalId in picking
|
|
237
|
+
* outputs. The change takes effect on the next render — no GPU
|
|
238
|
+
* buffer rewrite needed.
|
|
239
|
+
*/
|
|
240
|
+
relabelPointCloudAsset(handle, newExpressId) {
|
|
241
|
+
this.pointCloudRenderer?.relabelAsset(handle, newExpressId);
|
|
242
|
+
this.requestRender();
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Compute model bounds from triangle meshes + remaining point clouds.
|
|
246
|
+
* Called from removeAsset / clear paths so bounds shrink correctly.
|
|
247
|
+
* Triangle meshes still drive the bounds when present (existing
|
|
248
|
+
* Scene-driven path), so this only re-folds in the point cloud
|
|
249
|
+
* extents over whatever the mesh path left.
|
|
250
|
+
*/
|
|
251
|
+
recomputeModelBounds() {
|
|
252
|
+
// Always recompute from scratch: take mesh bounds as the
|
|
253
|
+
// baseline, then fold in the CURRENT point-cloud bounds on
|
|
254
|
+
// top. Folding only-up via expandModelBoundsForPointClouds()
|
|
255
|
+
// is correct when pc bounds grow but never shrinks them when
|
|
256
|
+
// an asset is removed, leaving stale oversized extents until
|
|
257
|
+
// every point cloud is gone.
|
|
258
|
+
const meshBounds = this.computeMeshBounds();
|
|
259
|
+
const pcBounds = this.pointCloudRenderer?.getBounds() ?? null;
|
|
260
|
+
if (!meshBounds && !pcBounds) {
|
|
261
|
+
this.modelBounds = null;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.modelBounds = meshBounds ?? {
|
|
265
|
+
min: { x: pcBounds.min[0], y: pcBounds.min[1], z: pcBounds.min[2] },
|
|
266
|
+
max: { x: pcBounds.max[0], y: pcBounds.max[1], z: pcBounds.max[2] },
|
|
267
|
+
};
|
|
268
|
+
if (meshBounds && pcBounds) {
|
|
269
|
+
this.expandModelBoundsForPointClouds();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** Aggregate bounds across all batched + individual meshes. Returns
|
|
273
|
+
* null if the scene has no mesh geometry. */
|
|
274
|
+
computeMeshBounds() {
|
|
275
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
276
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
277
|
+
let any = false;
|
|
278
|
+
for (const batch of this.scene.getBatchedMeshes()) {
|
|
279
|
+
if (!batch.bounds)
|
|
280
|
+
continue;
|
|
281
|
+
any = true;
|
|
282
|
+
if (batch.bounds.min[0] < minX)
|
|
283
|
+
minX = batch.bounds.min[0];
|
|
284
|
+
if (batch.bounds.min[1] < minY)
|
|
285
|
+
minY = batch.bounds.min[1];
|
|
286
|
+
if (batch.bounds.min[2] < minZ)
|
|
287
|
+
minZ = batch.bounds.min[2];
|
|
288
|
+
if (batch.bounds.max[0] > maxX)
|
|
289
|
+
maxX = batch.bounds.max[0];
|
|
290
|
+
if (batch.bounds.max[1] > maxY)
|
|
291
|
+
maxY = batch.bounds.max[1];
|
|
292
|
+
if (batch.bounds.max[2] > maxZ)
|
|
293
|
+
maxZ = batch.bounds.max[2];
|
|
294
|
+
}
|
|
295
|
+
if (!any)
|
|
296
|
+
return null;
|
|
297
|
+
return { min: { x: minX, y: minY, z: minZ }, max: { x: maxX, y: maxY, z: maxZ } };
|
|
298
|
+
}
|
|
299
|
+
/** Apply rendering options (color mode, fixed override, point size). */
|
|
300
|
+
setPointCloudOptions(opts) {
|
|
301
|
+
this.pointCloudRenderer?.setOptions(opts);
|
|
302
|
+
this.requestRender();
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Toggle Eye-Dome Lighting and tune its strength.
|
|
306
|
+
*
|
|
307
|
+
* EDL adds depth perception to point clouds (and meshes) via screen-
|
|
308
|
+
* space depth gradient — silhouette pixels get a soft black halo.
|
|
309
|
+
* Cheap: ~9 texture taps per pixel. Only runs when point clouds are
|
|
310
|
+
* loaded.
|
|
311
|
+
*/
|
|
312
|
+
setEdlOptions(opts) {
|
|
313
|
+
if (opts.enabled !== undefined)
|
|
314
|
+
this.edlOptions.enabled = opts.enabled;
|
|
315
|
+
if (opts.strength !== undefined)
|
|
316
|
+
this.edlOptions.strength = Math.max(0, Math.min(3, opts.strength));
|
|
317
|
+
if (opts.radiusPx !== undefined)
|
|
318
|
+
this.edlOptions.radiusPx = Math.max(1, Math.min(4, opts.radiusPx));
|
|
319
|
+
if (opts.highQuality !== undefined)
|
|
320
|
+
this.edlOptions.highQuality = opts.highQuality;
|
|
321
|
+
this.requestRender();
|
|
322
|
+
}
|
|
323
|
+
expandModelBoundsForPointClouds() {
|
|
324
|
+
const pcBounds = this.pointCloudRenderer?.getBounds();
|
|
325
|
+
if (!pcBounds)
|
|
326
|
+
return;
|
|
327
|
+
if (!this.modelBounds) {
|
|
328
|
+
this.modelBounds = {
|
|
329
|
+
min: { x: pcBounds.min[0], y: pcBounds.min[1], z: pcBounds.min[2] },
|
|
330
|
+
max: { x: pcBounds.max[0], y: pcBounds.max[1], z: pcBounds.max[2] },
|
|
331
|
+
};
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const m = this.modelBounds;
|
|
335
|
+
m.min.x = Math.min(m.min.x, pcBounds.min[0]);
|
|
336
|
+
m.min.y = Math.min(m.min.y, pcBounds.min[1]);
|
|
337
|
+
m.min.z = Math.min(m.min.z, pcBounds.min[2]);
|
|
338
|
+
m.max.x = Math.max(m.max.x, pcBounds.max[0]);
|
|
339
|
+
m.max.y = Math.max(m.max.y, pcBounds.max[1]);
|
|
340
|
+
m.max.z = Math.max(m.max.z, pcBounds.max[2]);
|
|
119
341
|
}
|
|
120
342
|
/**
|
|
121
343
|
* Load geometry from GeometryResult or MeshData array
|
|
@@ -553,29 +775,66 @@ export class Renderer {
|
|
|
553
775
|
const hasHiddenFilter = options.hiddenIds && options.hiddenIds.size > 0;
|
|
554
776
|
const hasIsolatedFilter = options.isolatedIds !== null && options.isolatedIds !== undefined;
|
|
555
777
|
const hasVisibilityFiltering = hasHiddenFilter || hasIsolatedFilter;
|
|
778
|
+
// Build the selected-id set once per frame so the X-Ray override paths
|
|
779
|
+
// can keep highlighted entities at full alpha without per-site checks.
|
|
780
|
+
const selectedId = options.selectedId;
|
|
781
|
+
const selectedIds = options.selectedIds;
|
|
782
|
+
const selectedModelIndex = options.selectedModelIndex;
|
|
783
|
+
const selectedExpressIds = new Set();
|
|
784
|
+
if (selectedId !== undefined && selectedId !== null) {
|
|
785
|
+
selectedExpressIds.add(selectedId);
|
|
786
|
+
}
|
|
787
|
+
if (selectedIds) {
|
|
788
|
+
for (const id of selectedIds) {
|
|
789
|
+
selectedExpressIds.add(id);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const hasSelected = selectedExpressIds.size > 0;
|
|
556
793
|
// Per-frame alpha overrides for X-Ray mode. See RenderOptions.transparencyOverrides.
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
const
|
|
794
|
+
// Snapshot the caller's map so mid-frame mutation can't desync classification
|
|
795
|
+
// and uniform-write decisions for the same batch/mesh.
|
|
796
|
+
const txOverridesSrc = options.transparencyOverrides;
|
|
797
|
+
const hasTxOverrides = txOverridesSrc != null && txOverridesSrc.size > 0;
|
|
798
|
+
const txOverrides = hasTxOverrides ? new Map(txOverridesSrc) : null;
|
|
560
799
|
const alphaForMesh = (expressId, fallback) => {
|
|
561
800
|
if (!hasTxOverrides)
|
|
562
801
|
return fallback;
|
|
802
|
+
// Selected meshes are exempt — the highlight pass renders them last,
|
|
803
|
+
// but exempting here also keeps mesh classification + uniform writes
|
|
804
|
+
// consistent so a selected mesh never enters the transparent pipeline
|
|
805
|
+
// because of its own override entry.
|
|
806
|
+
if (hasSelected && selectedExpressIds.has(expressId))
|
|
807
|
+
return fallback;
|
|
563
808
|
const a = txOverrides.get(expressId);
|
|
564
809
|
return a !== undefined ? a : fallback;
|
|
565
810
|
};
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
811
|
+
// Cache resolved batch alpha for the frame: classification needs it
|
|
812
|
+
// (opaque vs transparent routing) and renderBatch needs it for the
|
|
813
|
+
// uniform write. Without the cache we'd walk batch.expressIds twice
|
|
814
|
+
// per batch per frame, which becomes the dominant JS cost in X-Ray.
|
|
815
|
+
const batchAlphaCache = hasTxOverrides
|
|
816
|
+
? new WeakMap()
|
|
817
|
+
: null;
|
|
569
818
|
const alphaForBatch = (batch, fallback) => {
|
|
570
819
|
if (!hasTxOverrides)
|
|
571
820
|
return fallback;
|
|
821
|
+
const cached = batchAlphaCache.get(batch);
|
|
822
|
+
if (cached !== undefined)
|
|
823
|
+
return cached;
|
|
572
824
|
let minAlpha = Infinity;
|
|
573
825
|
for (const eid of batch.expressIds) {
|
|
826
|
+
// Selected ids never drag down a batch's alpha — the highlight
|
|
827
|
+
// pass redraws them on top, but excluding here also means a
|
|
828
|
+
// batch made entirely of selected entities stays opaque.
|
|
829
|
+
if (hasSelected && selectedExpressIds.has(eid))
|
|
830
|
+
continue;
|
|
574
831
|
const a = txOverrides.get(eid);
|
|
575
832
|
if (a !== undefined && a < minAlpha)
|
|
576
833
|
minAlpha = a;
|
|
577
834
|
}
|
|
578
|
-
|
|
835
|
+
const resolved = minAlpha === Infinity ? fallback : minAlpha;
|
|
836
|
+
batchAlphaCache.set(batch, resolved);
|
|
837
|
+
return resolved;
|
|
579
838
|
};
|
|
580
839
|
// PERFORMANCE FIX: Use batch-level visibility filtering instead of creating individual meshes
|
|
581
840
|
// Only create individual meshes for selected elements (for highlighting)
|
|
@@ -645,9 +904,6 @@ export class Renderer {
|
|
|
645
904
|
// Write uniform data to each mesh's buffer BEFORE recording commands
|
|
646
905
|
// This ensures each mesh has its own color data
|
|
647
906
|
const allMeshes = [...opaqueMeshes, ...transparentMeshes];
|
|
648
|
-
const selectedId = options.selectedId;
|
|
649
|
-
const selectedIds = options.selectedIds;
|
|
650
|
-
const selectedModelIndex = options.selectedModelIndex;
|
|
651
907
|
// Calculate section plane parameters and model bounds
|
|
652
908
|
// Always calculate bounds when sectionPlane is provided (for preview and active mode)
|
|
653
909
|
let sectionPlaneData;
|
|
@@ -682,6 +938,21 @@ export class Renderer {
|
|
|
682
938
|
boundsMax.z = Math.max(boundsMax.z, batch.bounds.max[2]);
|
|
683
939
|
}
|
|
684
940
|
}
|
|
941
|
+
// Fold in point-cloud bounds too — without this, a
|
|
942
|
+
// pure point-cloud scene falls through to the default
|
|
943
|
+
// [-100,100], and a mixed scene clips against a
|
|
944
|
+
// smaller mesh-only range while the point pipeline
|
|
945
|
+
// (which honours the same sectionPlaneData) keeps
|
|
946
|
+
// drawing points outside the slider's reach.
|
|
947
|
+
const pcBoundsForSection = this.pointCloudRenderer?.getBounds();
|
|
948
|
+
if (pcBoundsForSection) {
|
|
949
|
+
boundsMin.x = Math.min(boundsMin.x, pcBoundsForSection.min[0]);
|
|
950
|
+
boundsMin.y = Math.min(boundsMin.y, pcBoundsForSection.min[1]);
|
|
951
|
+
boundsMin.z = Math.min(boundsMin.z, pcBoundsForSection.min[2]);
|
|
952
|
+
boundsMax.x = Math.max(boundsMax.x, pcBoundsForSection.max[0]);
|
|
953
|
+
boundsMax.y = Math.max(boundsMax.y, pcBoundsForSection.max[1]);
|
|
954
|
+
boundsMax.z = Math.max(boundsMax.z, pcBoundsForSection.max[2]);
|
|
955
|
+
}
|
|
685
956
|
// If no batched meshes have bounds yet (streaming, degenerate
|
|
686
957
|
// models), fall back to individual meshes so at least the
|
|
687
958
|
// slider has a workable range.
|
|
@@ -961,15 +1232,6 @@ export class Renderer {
|
|
|
961
1232
|
opaqueBatches.push(batch);
|
|
962
1233
|
}
|
|
963
1234
|
}
|
|
964
|
-
const selectedExpressIds = new Set();
|
|
965
|
-
if (selectedId !== undefined && selectedId !== null) {
|
|
966
|
-
selectedExpressIds.add(selectedId);
|
|
967
|
-
}
|
|
968
|
-
if (selectedIds) {
|
|
969
|
-
for (const id of selectedIds) {
|
|
970
|
-
selectedExpressIds.add(id);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
1235
|
// Build a uniform template ONCE per frame — shared across all batches.
|
|
974
1236
|
// Only the 4-float color (offset 32) differs per batch; everything else
|
|
975
1237
|
// (viewProj, identity model, material, section plane, flags) is identical.
|
|
@@ -1308,6 +1570,19 @@ export class Renderer {
|
|
|
1308
1570
|
}
|
|
1309
1571
|
}
|
|
1310
1572
|
}
|
|
1573
|
+
// Draw point clouds (IFCx inline + streamed LAS/LAZ).
|
|
1574
|
+
// Shares the depth buffer + section plane state with the mesh pipeline so
|
|
1575
|
+
// points occlude triangles and vice versa. The splat shader needs the
|
|
1576
|
+
// viewport size to convert pixel sizes into clip-space offsets.
|
|
1577
|
+
if (this.pointCloudRenderer && this.pointCloudRenderer.hasAssets()) {
|
|
1578
|
+
this.pointCloudRenderer.draw(pass, {
|
|
1579
|
+
viewProj,
|
|
1580
|
+
sectionPlane: sectionPlaneData
|
|
1581
|
+
? { ...sectionPlaneData, flipped: options.sectionPlane?.flipped === true }
|
|
1582
|
+
: null,
|
|
1583
|
+
viewport: { width: this.canvas.width, height: this.canvas.height },
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1311
1586
|
// Draw section plane visual BEFORE pass.end() (within same MSAA render pass)
|
|
1312
1587
|
// Always show plane when sectionPlane options are provided (as preview or active)
|
|
1313
1588
|
const modelBounds = this.getModelBounds();
|
|
@@ -1380,6 +1655,21 @@ export class Renderer {
|
|
|
1380
1655
|
enableSeparationLines: separationEnabled,
|
|
1381
1656
|
});
|
|
1382
1657
|
}
|
|
1658
|
+
// Eye-Dome Lighting — runs AFTER contact/separation so it darkens
|
|
1659
|
+
// every layer uniformly. Cheap (~9 depth taps), only active when
|
|
1660
|
+
// there are point clouds in the scene and the user has enabled it.
|
|
1661
|
+
if (this.edlPass
|
|
1662
|
+
&& this.edlOptions.enabled
|
|
1663
|
+
&& this.pointCloudRenderer?.hasAssets()) {
|
|
1664
|
+
this.edlPass.apply(encoder, {
|
|
1665
|
+
targetView: textureView,
|
|
1666
|
+
depthView: this.pipeline.getDepthOnlyTextureView(),
|
|
1667
|
+
}, {
|
|
1668
|
+
strength: this.edlOptions.strength,
|
|
1669
|
+
radiusPx: this.edlOptions.radiusPx,
|
|
1670
|
+
highQuality: this.edlOptions.highQuality,
|
|
1671
|
+
});
|
|
1672
|
+
}
|
|
1383
1673
|
device.queue.submit([encoder.finish()]);
|
|
1384
1674
|
}
|
|
1385
1675
|
catch (error) {
|
|
@@ -1601,11 +1891,16 @@ export class Renderer {
|
|
|
1601
1891
|
// Post-processor uniform buffer
|
|
1602
1892
|
this.postProcessor?.destroy();
|
|
1603
1893
|
this.postProcessor = null;
|
|
1894
|
+
this.edlPass?.destroy();
|
|
1895
|
+
this.edlPass = null;
|
|
1604
1896
|
// Section-plane renderers
|
|
1605
1897
|
this.sectionPlaneRenderer?.destroy();
|
|
1606
1898
|
this.sectionPlaneRenderer = null;
|
|
1607
1899
|
this.section2DOverlayRenderer?.dispose();
|
|
1608
1900
|
this.section2DOverlayRenderer = null;
|
|
1901
|
+
// Point cloud GPU resources
|
|
1902
|
+
this.pointCloudRenderer?.clear();
|
|
1903
|
+
this.pointCloudRenderer = null;
|
|
1609
1904
|
// Snap detector geometry cache
|
|
1610
1905
|
this.raycastEngine.clearCaches();
|
|
1611
1906
|
}
|