@ifc-lite/renderer 1.16.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/README.md +73 -15
- 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 +348 -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/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +16 -1
- package/dist/pipeline.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/shaders/main.wgsl.d.ts +1 -1
- package/dist/shaders/main.wgsl.d.ts.map +1 -1
- package/dist/shaders/main.wgsl.js +15 -3
- package/dist/shaders/main.wgsl.js.map +1 -1
- package/dist/types.d.ts +20 -0
- 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,6 +775,67 @@ 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;
|
|
793
|
+
// Per-frame alpha overrides for X-Ray mode. See RenderOptions.transparencyOverrides.
|
|
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;
|
|
799
|
+
const alphaForMesh = (expressId, fallback) => {
|
|
800
|
+
if (!hasTxOverrides)
|
|
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;
|
|
808
|
+
const a = txOverrides.get(expressId);
|
|
809
|
+
return a !== undefined ? a : fallback;
|
|
810
|
+
};
|
|
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;
|
|
818
|
+
const alphaForBatch = (batch, fallback) => {
|
|
819
|
+
if (!hasTxOverrides)
|
|
820
|
+
return fallback;
|
|
821
|
+
const cached = batchAlphaCache.get(batch);
|
|
822
|
+
if (cached !== undefined)
|
|
823
|
+
return cached;
|
|
824
|
+
let minAlpha = Infinity;
|
|
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;
|
|
831
|
+
const a = txOverrides.get(eid);
|
|
832
|
+
if (a !== undefined && a < minAlpha)
|
|
833
|
+
minAlpha = a;
|
|
834
|
+
}
|
|
835
|
+
const resolved = minAlpha === Infinity ? fallback : minAlpha;
|
|
836
|
+
batchAlphaCache.set(batch, resolved);
|
|
837
|
+
return resolved;
|
|
838
|
+
};
|
|
556
839
|
// PERFORMANCE FIX: Use batch-level visibility filtering instead of creating individual meshes
|
|
557
840
|
// Only create individual meshes for selected elements (for highlighting)
|
|
558
841
|
// Batches are filtered at render time - fully visible batches render normally,
|
|
@@ -602,7 +885,7 @@ export class Renderer {
|
|
|
602
885
|
const opaqueMeshes = [];
|
|
603
886
|
const transparentMeshes = [];
|
|
604
887
|
for (const mesh of meshes) {
|
|
605
|
-
const alpha = mesh.color[3];
|
|
888
|
+
const alpha = alphaForMesh(mesh.expressId, mesh.color[3]);
|
|
606
889
|
const transparency = mesh.material?.transparency ?? 0.0;
|
|
607
890
|
const isTransparent = alpha < 0.99 || transparency > 0.01;
|
|
608
891
|
if (isTransparent) {
|
|
@@ -621,9 +904,6 @@ export class Renderer {
|
|
|
621
904
|
// Write uniform data to each mesh's buffer BEFORE recording commands
|
|
622
905
|
// This ensures each mesh has its own color data
|
|
623
906
|
const allMeshes = [...opaqueMeshes, ...transparentMeshes];
|
|
624
|
-
const selectedId = options.selectedId;
|
|
625
|
-
const selectedIds = options.selectedIds;
|
|
626
|
-
const selectedModelIndex = options.selectedModelIndex;
|
|
627
907
|
// Calculate section plane parameters and model bounds
|
|
628
908
|
// Always calculate bounds when sectionPlane is provided (for preview and active mode)
|
|
629
909
|
let sectionPlaneData;
|
|
@@ -658,6 +938,21 @@ export class Renderer {
|
|
|
658
938
|
boundsMax.z = Math.max(boundsMax.z, batch.bounds.max[2]);
|
|
659
939
|
}
|
|
660
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
|
+
}
|
|
661
956
|
// If no batched meshes have bounds yet (streaming, degenerate
|
|
662
957
|
// models), fall back to individual meshes so at least the
|
|
663
958
|
// slider has a workable range.
|
|
@@ -790,7 +1085,8 @@ export class Renderer {
|
|
|
790
1085
|
meshBuf[32] = mesh.color[0];
|
|
791
1086
|
meshBuf[33] = mesh.color[1];
|
|
792
1087
|
meshBuf[34] = mesh.color[2];
|
|
793
|
-
|
|
1088
|
+
// Selected meshes always keep their own alpha so highlights stay opaque
|
|
1089
|
+
meshBuf[35] = isSelected ? mesh.color[3] : alphaForMesh(mesh.expressId, mesh.color[3]);
|
|
794
1090
|
meshBuf[36] = mesh.material?.metallic ?? 0.0;
|
|
795
1091
|
meshBuf[37] = mesh.material?.roughness ?? 0.6;
|
|
796
1092
|
meshBuf[38] = 0;
|
|
@@ -928,7 +1224,7 @@ export class Renderer {
|
|
|
928
1224
|
continue; // Don't add batch to render list
|
|
929
1225
|
}
|
|
930
1226
|
}
|
|
931
|
-
const alpha = batch.color[3];
|
|
1227
|
+
const alpha = alphaForBatch(batch, batch.color[3]);
|
|
932
1228
|
if (alpha < 0.99) {
|
|
933
1229
|
transparentBatches.push(batch);
|
|
934
1230
|
}
|
|
@@ -936,15 +1232,6 @@ export class Renderer {
|
|
|
936
1232
|
opaqueBatches.push(batch);
|
|
937
1233
|
}
|
|
938
1234
|
}
|
|
939
|
-
const selectedExpressIds = new Set();
|
|
940
|
-
if (selectedId !== undefined && selectedId !== null) {
|
|
941
|
-
selectedExpressIds.add(selectedId);
|
|
942
|
-
}
|
|
943
|
-
if (selectedIds) {
|
|
944
|
-
for (const id of selectedIds) {
|
|
945
|
-
selectedExpressIds.add(id);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
1235
|
// Build a uniform template ONCE per frame — shared across all batches.
|
|
949
1236
|
// Only the 4-float color (offset 32) differs per batch; everything else
|
|
950
1237
|
// (viewProj, identity model, material, section plane, flags) is identical.
|
|
@@ -1006,7 +1293,7 @@ export class Renderer {
|
|
|
1006
1293
|
tpl[32] = batch.color[0];
|
|
1007
1294
|
tpl[33] = batch.color[1];
|
|
1008
1295
|
tpl[34] = batch.color[2];
|
|
1009
|
-
tpl[35] = batch.color[3];
|
|
1296
|
+
tpl[35] = alphaForBatch(batch, batch.color[3]);
|
|
1010
1297
|
device.queue.writeBuffer(batch.uniformBuffer, 0, tpl);
|
|
1011
1298
|
// Single draw call for entire batch!
|
|
1012
1299
|
pass.setBindGroup(0, batch.bindGroup);
|
|
@@ -1031,8 +1318,9 @@ export class Renderer {
|
|
|
1031
1318
|
// Get or create a cached sub-batch for this visibility state
|
|
1032
1319
|
const subBatch = this.scene.getOrCreatePartialBatch(sourceBatchKey, colorKey, visibleIds, device, this.pipeline);
|
|
1033
1320
|
if (subBatch) {
|
|
1034
|
-
// Use opaque or transparent pipeline based on alpha
|
|
1035
|
-
|
|
1321
|
+
// Use opaque or transparent pipeline based on resolved alpha
|
|
1322
|
+
// (not the parent batch's color[3] — that ignores transparencyOverrides)
|
|
1323
|
+
const isTransparent = alphaForBatch(subBatch, color[3]) < 0.99;
|
|
1036
1324
|
if (isTransparent) {
|
|
1037
1325
|
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
1038
1326
|
}
|
|
@@ -1051,12 +1339,20 @@ export class Renderer {
|
|
|
1051
1339
|
// Placed AFTER partial batches so depth buffer is complete for both full
|
|
1052
1340
|
// and partial batches. Uses 'equal' depth compare — only paints where
|
|
1053
1341
|
// original geometry wrote depth, so hidden entities never leak through.
|
|
1342
|
+
//
|
|
1343
|
+
// flags.x bit 1 = overlay: tells the shader to preserve baseColor.a
|
|
1344
|
+
// (the overlay pipeline now has src-alpha blending so low-alpha ghost
|
|
1345
|
+
// tints composite correctly against the opaque pass) AND skip the
|
|
1346
|
+
// glass-fresnel branch (which is meant for real glass materials and
|
|
1347
|
+
// would whiten low-alpha colour overrides at grazing angles).
|
|
1054
1348
|
const overrideBatches = this.scene.getOverrideBatches();
|
|
1055
1349
|
if (overrideBatches.length > 0) {
|
|
1056
1350
|
pass.setPipeline(this.pipeline.getOverlayPipeline());
|
|
1351
|
+
tplFlags[0] = 2; // set overlay bit for the duration of these draws
|
|
1057
1352
|
for (const batch of overrideBatches) {
|
|
1058
1353
|
renderBatch(batch);
|
|
1059
1354
|
}
|
|
1355
|
+
tplFlags[0] = 0; // restore for any downstream use of the template
|
|
1060
1356
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
1061
1357
|
}
|
|
1062
1358
|
// Filled, hatched 3D cut surfaces are now rendered by
|
|
@@ -1132,7 +1428,7 @@ export class Renderer {
|
|
|
1132
1428
|
tpl[32] = mesh.color[0];
|
|
1133
1429
|
tpl[33] = mesh.color[1];
|
|
1134
1430
|
tpl[34] = mesh.color[2];
|
|
1135
|
-
tpl[35] = mesh.color[3];
|
|
1431
|
+
tpl[35] = alphaForMesh(mesh.expressId, mesh.color[3]);
|
|
1136
1432
|
tpl[36] = mesh.material?.metallic ?? 0.0;
|
|
1137
1433
|
tpl[37] = mesh.material?.roughness ?? 0.6;
|
|
1138
1434
|
tpl[38] = 0;
|
|
@@ -1274,6 +1570,19 @@ export class Renderer {
|
|
|
1274
1570
|
}
|
|
1275
1571
|
}
|
|
1276
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
|
+
}
|
|
1277
1586
|
// Draw section plane visual BEFORE pass.end() (within same MSAA render pass)
|
|
1278
1587
|
// Always show plane when sectionPlane options are provided (as preview or active)
|
|
1279
1588
|
const modelBounds = this.getModelBounds();
|
|
@@ -1346,6 +1655,21 @@ export class Renderer {
|
|
|
1346
1655
|
enableSeparationLines: separationEnabled,
|
|
1347
1656
|
});
|
|
1348
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
|
+
}
|
|
1349
1673
|
device.queue.submit([encoder.finish()]);
|
|
1350
1674
|
}
|
|
1351
1675
|
catch (error) {
|
|
@@ -1567,11 +1891,16 @@ export class Renderer {
|
|
|
1567
1891
|
// Post-processor uniform buffer
|
|
1568
1892
|
this.postProcessor?.destroy();
|
|
1569
1893
|
this.postProcessor = null;
|
|
1894
|
+
this.edlPass?.destroy();
|
|
1895
|
+
this.edlPass = null;
|
|
1570
1896
|
// Section-plane renderers
|
|
1571
1897
|
this.sectionPlaneRenderer?.destroy();
|
|
1572
1898
|
this.sectionPlaneRenderer = null;
|
|
1573
1899
|
this.section2DOverlayRenderer?.dispose();
|
|
1574
1900
|
this.section2DOverlayRenderer = null;
|
|
1901
|
+
// Point cloud GPU resources
|
|
1902
|
+
this.pointCloudRenderer?.clear();
|
|
1903
|
+
this.pointCloudRenderer = null;
|
|
1575
1904
|
// Snap detector geometry cache
|
|
1576
1905
|
this.raycastEngine.clearCaches();
|
|
1577
1906
|
}
|