@ifc-lite/renderer 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/dist/bvh.d.ts +50 -0
- package/dist/bvh.d.ts.map +1 -0
- package/dist/bvh.js +177 -0
- package/dist/bvh.js.map +1 -0
- package/dist/camera.d.ts +17 -0
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +72 -6
- package/dist/camera.js.map +1 -1
- package/dist/index.d.ts +42 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +429 -87
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +30 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +103 -0
- package/dist/math.js.map +1 -1
- package/dist/picker.js +2 -2
- package/dist/picker.js.map +1 -1
- package/dist/pipeline.d.ts +17 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +347 -47
- package/dist/pipeline.js.map +1 -1
- package/dist/raycaster.d.ts +67 -0
- package/dist/raycaster.d.ts.map +1 -0
- package/dist/raycaster.js +192 -0
- package/dist/raycaster.js.map +1 -0
- package/dist/scene.d.ts +56 -2
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +362 -26
- package/dist/scene.js.map +1 -1
- package/dist/snap-detector.d.ts +73 -0
- package/dist/snap-detector.d.ts.map +1 -0
- package/dist/snap-detector.js +291 -0
- package/dist/snap-detector.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/zero-copy-uploader.d.ts +145 -0
- package/dist/zero-copy-uploader.d.ts.map +1 -0
- package/dist/zero-copy-uploader.js +146 -0
- package/dist/zero-copy-uploader.js.map +1 -0
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -11,7 +11,12 @@ export { Scene } from './scene.js';
|
|
|
11
11
|
export { Picker } from './picker.js';
|
|
12
12
|
export { MathUtils } from './math.js';
|
|
13
13
|
export { SectionPlaneRenderer } from './section-plane.js';
|
|
14
|
+
export { Raycaster } from './raycaster.js';
|
|
15
|
+
export { SnapDetector, SnapType } from './snap-detector.js';
|
|
16
|
+
export { BVH } from './bvh.js';
|
|
14
17
|
export * from './types.js';
|
|
18
|
+
// Zero-copy GPU upload (new - faster, less memory)
|
|
19
|
+
export { ZeroCopyGpuUploader, createZeroCopyUploader, } from './zero-copy-uploader.js';
|
|
15
20
|
import { WebGPUDevice } from './device.js';
|
|
16
21
|
import { RenderPipeline, InstancedRenderPipeline } from './pipeline.js';
|
|
17
22
|
import { Camera } from './camera.js';
|
|
@@ -21,6 +26,9 @@ import { FrustumUtils } from '@ifc-lite/spatial';
|
|
|
21
26
|
import { SectionPlaneRenderer } from './section-plane.js';
|
|
22
27
|
import { deduplicateMeshes } from '@ifc-lite/geometry';
|
|
23
28
|
import { MathUtils } from './math.js';
|
|
29
|
+
import { Raycaster } from './raycaster.js';
|
|
30
|
+
import { SnapDetector } from './snap-detector.js';
|
|
31
|
+
import { BVH } from './bvh.js';
|
|
24
32
|
/**
|
|
25
33
|
* Main renderer class
|
|
26
34
|
*/
|
|
@@ -34,11 +42,22 @@ export class Renderer {
|
|
|
34
42
|
canvas;
|
|
35
43
|
sectionPlaneRenderer = null;
|
|
36
44
|
modelBounds = null;
|
|
45
|
+
raycaster;
|
|
46
|
+
snapDetector;
|
|
47
|
+
bvh;
|
|
48
|
+
// BVH cache
|
|
49
|
+
bvhCache = null;
|
|
50
|
+
// Performance constants
|
|
51
|
+
BVH_THRESHOLD = 100;
|
|
37
52
|
constructor(canvas) {
|
|
38
53
|
this.canvas = canvas;
|
|
39
54
|
this.device = new WebGPUDevice();
|
|
40
55
|
this.camera = new Camera();
|
|
41
56
|
this.scene = new Scene();
|
|
57
|
+
this.raycaster = new Raycaster();
|
|
58
|
+
this.snapDetector = new SnapDetector();
|
|
59
|
+
this.bvh = new BVH();
|
|
60
|
+
this.bvhCache = null;
|
|
42
61
|
}
|
|
43
62
|
/**
|
|
44
63
|
* Initialize renderer
|
|
@@ -366,44 +385,17 @@ export class Renderer {
|
|
|
366
385
|
}
|
|
367
386
|
const device = this.device.getDevice();
|
|
368
387
|
const viewProj = this.camera.getViewProjMatrix().m;
|
|
369
|
-
// Ensure all meshes have GPU resources (in case they were added before pipeline was ready)
|
|
370
|
-
this.ensureMeshResources();
|
|
371
388
|
let meshes = this.scene.getMeshes();
|
|
372
389
|
// Check if visibility filtering is active
|
|
373
390
|
const hasHiddenFilter = options.hiddenIds && options.hiddenIds.size > 0;
|
|
374
391
|
const hasIsolatedFilter = options.isolatedIds !== null && options.isolatedIds !== undefined;
|
|
375
392
|
const hasVisibilityFiltering = hasHiddenFilter || hasIsolatedFilter;
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
for (const batch of batchedMeshes) {
|
|
383
|
-
for (const expressId of batch.expressIds) {
|
|
384
|
-
allExpressIds.add(expressId);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
// Filter to get visible expressIds
|
|
388
|
-
const visibleExpressIds = [];
|
|
389
|
-
for (const expressId of allExpressIds) {
|
|
390
|
-
const isHidden = options.hiddenIds?.has(expressId) ?? false;
|
|
391
|
-
const isIsolated = !hasIsolatedFilter || options.isolatedIds.has(expressId);
|
|
392
|
-
if (!isHidden && isIsolated) {
|
|
393
|
-
visibleExpressIds.push(expressId);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
// Create individual meshes for visible elements only
|
|
397
|
-
const existingMeshIds = new Set(meshes.map(m => m.expressId));
|
|
398
|
-
for (const expressId of visibleExpressIds) {
|
|
399
|
-
if (!existingMeshIds.has(expressId) && this.scene.hasMeshData(expressId)) {
|
|
400
|
-
const meshData = this.scene.getMeshData(expressId);
|
|
401
|
-
this.createMeshFromData(meshData);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
// Get updated meshes list
|
|
405
|
-
meshes = this.scene.getMeshes();
|
|
406
|
-
}
|
|
393
|
+
// PERFORMANCE FIX: Use batch-level visibility filtering instead of creating individual meshes
|
|
394
|
+
// Only create individual meshes for selected elements (for highlighting)
|
|
395
|
+
// Batches are filtered at render time - fully visible batches render normally,
|
|
396
|
+
// partially visible batches are skipped (their visible elements will be in other batches or individual meshes)
|
|
397
|
+
// Ensure all existing meshes have GPU resources
|
|
398
|
+
this.ensureMeshResources();
|
|
407
399
|
// Frustum culling (if enabled and spatial index available)
|
|
408
400
|
if (options.enableFrustumCulling && options.spatialIndex) {
|
|
409
401
|
try {
|
|
@@ -551,30 +543,92 @@ export class Renderer {
|
|
|
551
543
|
}
|
|
552
544
|
// Now record draw commands
|
|
553
545
|
const encoder = device.createCommandEncoder();
|
|
546
|
+
// Set up MSAA rendering if enabled
|
|
547
|
+
const msaaView = this.pipeline.getMultisampleTextureView();
|
|
548
|
+
const useMSAA = msaaView !== null && this.pipeline.getSampleCount() > 1;
|
|
554
549
|
const pass = encoder.beginRenderPass({
|
|
555
550
|
colorAttachments: [
|
|
556
551
|
{
|
|
557
|
-
|
|
552
|
+
// If MSAA enabled: render to multisample texture, resolve to swap chain
|
|
553
|
+
// If MSAA disabled: render directly to swap chain
|
|
554
|
+
view: useMSAA ? msaaView : textureView,
|
|
555
|
+
resolveTarget: useMSAA ? textureView : undefined,
|
|
558
556
|
loadOp: 'clear',
|
|
559
557
|
clearValue: clearColor,
|
|
560
|
-
storeOp: 'store',
|
|
558
|
+
storeOp: useMSAA ? 'discard' : 'store', // Discard MSAA buffer after resolve
|
|
561
559
|
},
|
|
562
560
|
],
|
|
563
561
|
depthStencilAttachment: {
|
|
564
562
|
view: this.pipeline.getDepthTextureView(),
|
|
565
|
-
depthClearValue:
|
|
563
|
+
depthClearValue: 0.0, // Reverse-Z: clear to 0.0 (far plane)
|
|
566
564
|
depthLoadOp: 'clear',
|
|
567
565
|
depthStoreOp: 'store',
|
|
568
566
|
},
|
|
569
567
|
});
|
|
570
568
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
571
569
|
// Check if we have batched meshes (preferred for performance)
|
|
572
|
-
// When visibility filtering is active, we need to render individual meshes instead of batches
|
|
573
|
-
// because batches merge geometry by color and can't be partially rendered
|
|
574
570
|
const allBatchedMeshes = this.scene.getBatchedMeshes();
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
571
|
+
// PERFORMANCE FIX: Always use batch rendering when we have batches
|
|
572
|
+
// Apply visibility filtering at the BATCH level instead of creating individual meshes
|
|
573
|
+
// This keeps draw calls at ~50-200 instead of 60K+
|
|
574
|
+
if (allBatchedMeshes.length > 0) {
|
|
575
|
+
// Pre-compute visibility for each batch (only when filtering is active)
|
|
576
|
+
// A batch is visible if ANY of its elements are visible
|
|
577
|
+
// A batch is fully visible if ALL of its elements are visible
|
|
578
|
+
const batchVisibility = new Map();
|
|
579
|
+
if (hasVisibilityFiltering) {
|
|
580
|
+
for (const batch of allBatchedMeshes) {
|
|
581
|
+
let visibleCount = 0;
|
|
582
|
+
const total = batch.expressIds.length;
|
|
583
|
+
for (const expressId of batch.expressIds) {
|
|
584
|
+
const isHidden = options.hiddenIds?.has(expressId) ?? false;
|
|
585
|
+
const isIsolated = !hasIsolatedFilter || options.isolatedIds.has(expressId);
|
|
586
|
+
if (!isHidden && isIsolated) {
|
|
587
|
+
visibleCount++;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
batchVisibility.set(batch.colorKey, {
|
|
591
|
+
visible: visibleCount > 0,
|
|
592
|
+
fullyVisible: visibleCount === total,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Separate batches into opaque and transparent, filtering by visibility
|
|
597
|
+
// IMPORTANT: Only render FULLY visible batches - partially visible batches
|
|
598
|
+
// need individual mesh rendering to show only the visible elements
|
|
599
|
+
const opaqueBatches = [];
|
|
600
|
+
const transparentBatches = [];
|
|
601
|
+
// Collect visible expressIds from partially visible batches
|
|
602
|
+
// These need individual mesh rendering instead of batch rendering
|
|
603
|
+
const partiallyVisibleExpressIds = new Set();
|
|
604
|
+
for (const batch of allBatchedMeshes) {
|
|
605
|
+
// Check visibility
|
|
606
|
+
if (hasVisibilityFiltering) {
|
|
607
|
+
const vis = batchVisibility.get(batch.colorKey);
|
|
608
|
+
if (!vis || !vis.visible)
|
|
609
|
+
continue; // Skip completely hidden batches
|
|
610
|
+
// FIX: Skip partially visible batches - their visible elements
|
|
611
|
+
// will be rendered individually below
|
|
612
|
+
if (!vis.fullyVisible) {
|
|
613
|
+
// Collect the visible expressIds from this batch
|
|
614
|
+
for (const expressId of batch.expressIds) {
|
|
615
|
+
const isHidden = options.hiddenIds?.has(expressId) ?? false;
|
|
616
|
+
const isIsolated = !hasIsolatedFilter || options.isolatedIds.has(expressId);
|
|
617
|
+
if (!isHidden && isIsolated) {
|
|
618
|
+
partiallyVisibleExpressIds.add(expressId);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
continue; // Don't add batch to render list
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const alpha = batch.color[3];
|
|
625
|
+
if (alpha < 0.99) {
|
|
626
|
+
transparentBatches.push(batch);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
opaqueBatches.push(batch);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
578
632
|
const selectedExpressIds = new Set();
|
|
579
633
|
if (selectedId !== undefined && selectedId !== null) {
|
|
580
634
|
selectedExpressIds.add(selectedId);
|
|
@@ -584,11 +638,10 @@ export class Renderer {
|
|
|
584
638
|
selectedExpressIds.add(id);
|
|
585
639
|
}
|
|
586
640
|
}
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
for (const batch of allBatchedMeshes) {
|
|
641
|
+
// Helper function to render a batch
|
|
642
|
+
const renderBatch = (batch) => {
|
|
590
643
|
if (!batch.bindGroup || !batch.uniformBuffer)
|
|
591
|
-
|
|
644
|
+
return;
|
|
592
645
|
// Update uniform buffer for this batch
|
|
593
646
|
const buffer = new Float32Array(48);
|
|
594
647
|
const flagBuffer = new Uint32Array(buffer.buffer, 176, 4);
|
|
@@ -621,21 +674,109 @@ export class Renderer {
|
|
|
621
674
|
pass.setVertexBuffer(0, batch.vertexBuffer);
|
|
622
675
|
pass.setIndexBuffer(batch.indexBuffer, 'uint32');
|
|
623
676
|
pass.drawIndexed(batch.indexCount);
|
|
677
|
+
};
|
|
678
|
+
// Render opaque batches first with opaque pipeline
|
|
679
|
+
pass.setPipeline(this.pipeline.getPipeline());
|
|
680
|
+
for (const batch of opaqueBatches) {
|
|
681
|
+
renderBatch(batch);
|
|
682
|
+
}
|
|
683
|
+
// Render partially visible elements individually (from batches that had mixed visibility)
|
|
684
|
+
// This is the FIX for hide/isolate showing wrong batches
|
|
685
|
+
const allMeshes = this.scene.getMeshes();
|
|
686
|
+
const existingMeshIds = new Set(allMeshes.map(m => m.expressId));
|
|
687
|
+
if (partiallyVisibleExpressIds.size > 0) {
|
|
688
|
+
// Create GPU resources lazily for partially visible meshes
|
|
689
|
+
for (const pvId of partiallyVisibleExpressIds) {
|
|
690
|
+
if (!existingMeshIds.has(pvId) && this.scene.hasMeshData(pvId)) {
|
|
691
|
+
const meshData = this.scene.getMeshData(pvId);
|
|
692
|
+
this.createMeshFromData(meshData);
|
|
693
|
+
existingMeshIds.add(pvId);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Get partially visible meshes and render them
|
|
697
|
+
const partialMeshes = this.scene.getMeshes().filter(mesh => partiallyVisibleExpressIds.has(mesh.expressId));
|
|
698
|
+
// Ensure partial meshes have uniform buffers and bind groups
|
|
699
|
+
for (const mesh of partialMeshes) {
|
|
700
|
+
if (!mesh.uniformBuffer && this.pipeline) {
|
|
701
|
+
mesh.uniformBuffer = device.createBuffer({
|
|
702
|
+
size: this.pipeline.getUniformBufferSize(),
|
|
703
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
704
|
+
});
|
|
705
|
+
mesh.bindGroup = device.createBindGroup({
|
|
706
|
+
layout: this.pipeline.getBindGroupLayout(),
|
|
707
|
+
entries: [
|
|
708
|
+
{
|
|
709
|
+
binding: 0,
|
|
710
|
+
resource: { buffer: mesh.uniformBuffer },
|
|
711
|
+
},
|
|
712
|
+
],
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// Render partially visible meshes (not selected, normal rendering)
|
|
717
|
+
for (const mesh of partialMeshes) {
|
|
718
|
+
if (!mesh.bindGroup || !mesh.uniformBuffer) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
const buffer = new Float32Array(48);
|
|
722
|
+
const flagBuffer = new Uint32Array(buffer.buffer, 176, 4);
|
|
723
|
+
buffer.set(viewProj, 0);
|
|
724
|
+
buffer.set(mesh.transform.m, 16);
|
|
725
|
+
buffer.set(mesh.color, 32);
|
|
726
|
+
buffer[36] = mesh.material?.metallic ?? 0.0;
|
|
727
|
+
buffer[37] = mesh.material?.roughness ?? 0.6;
|
|
728
|
+
// Section plane data
|
|
729
|
+
if (sectionPlaneData) {
|
|
730
|
+
buffer[40] = sectionPlaneData.normal[0];
|
|
731
|
+
buffer[41] = sectionPlaneData.normal[1];
|
|
732
|
+
buffer[42] = sectionPlaneData.normal[2];
|
|
733
|
+
buffer[43] = sectionPlaneData.distance;
|
|
734
|
+
}
|
|
735
|
+
// Flags (not selected)
|
|
736
|
+
flagBuffer[0] = 0;
|
|
737
|
+
flagBuffer[1] = sectionPlaneData?.enabled ? 1 : 0;
|
|
738
|
+
flagBuffer[2] = 0;
|
|
739
|
+
flagBuffer[3] = 0;
|
|
740
|
+
device.queue.writeBuffer(mesh.uniformBuffer, 0, buffer);
|
|
741
|
+
// Use opaque or transparent pipeline based on alpha
|
|
742
|
+
const isTransparent = mesh.color[3] < 0.99;
|
|
743
|
+
if (isTransparent) {
|
|
744
|
+
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
pass.setPipeline(this.pipeline.getPipeline());
|
|
748
|
+
}
|
|
749
|
+
pass.setBindGroup(0, mesh.bindGroup);
|
|
750
|
+
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
751
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
752
|
+
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
753
|
+
}
|
|
624
754
|
}
|
|
625
755
|
// Render selected meshes individually for proper highlighting
|
|
626
756
|
// First, check if we have Mesh objects for selected IDs
|
|
627
757
|
// If not, create them lazily from stored MeshData
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
for (const
|
|
632
|
-
|
|
633
|
-
|
|
758
|
+
// FIX: Filter selected IDs by visibility BEFORE creating GPU resources
|
|
759
|
+
// This ensures highlights don't appear for hidden elements
|
|
760
|
+
const visibleSelectedIds = new Set();
|
|
761
|
+
for (const selId of selectedExpressIds) {
|
|
762
|
+
// Skip if hidden
|
|
763
|
+
if (options.hiddenIds?.has(selId))
|
|
764
|
+
continue;
|
|
765
|
+
// Skip if isolation is active and this entity is not isolated
|
|
766
|
+
if (hasIsolatedFilter && !options.isolatedIds.has(selId))
|
|
767
|
+
continue;
|
|
768
|
+
visibleSelectedIds.add(selId);
|
|
769
|
+
}
|
|
770
|
+
// Create GPU resources lazily for visible selected meshes that don't have them yet
|
|
771
|
+
for (const selId of visibleSelectedIds) {
|
|
772
|
+
if (!existingMeshIds.has(selId) && this.scene.hasMeshData(selId)) {
|
|
773
|
+
const meshData = this.scene.getMeshData(selId);
|
|
634
774
|
this.createMeshFromData(meshData);
|
|
775
|
+
existingMeshIds.add(selId);
|
|
635
776
|
}
|
|
636
777
|
}
|
|
637
|
-
// Now get selected meshes (
|
|
638
|
-
const selectedMeshes = this.scene.getMeshes().filter(mesh =>
|
|
778
|
+
// Now get selected meshes (only visible ones)
|
|
779
|
+
const selectedMeshes = this.scene.getMeshes().filter(mesh => visibleSelectedIds.has(mesh.expressId));
|
|
639
780
|
// Ensure selected meshes have uniform buffers and bind groups
|
|
640
781
|
for (const mesh of selectedMeshes) {
|
|
641
782
|
if (!mesh.uniformBuffer && this.pipeline) {
|
|
@@ -686,9 +827,49 @@ export class Renderer {
|
|
|
686
827
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
687
828
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
688
829
|
}
|
|
830
|
+
// Render transparent BATCHED meshes with transparent pipeline (after opaque batches and selections)
|
|
831
|
+
if (transparentBatches.length > 0) {
|
|
832
|
+
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
833
|
+
for (const batch of transparentBatches) {
|
|
834
|
+
renderBatch(batch);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
// Render transparent individual meshes with transparent pipeline
|
|
838
|
+
if (transparentMeshes.length > 0) {
|
|
839
|
+
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
840
|
+
for (const mesh of transparentMeshes) {
|
|
841
|
+
if (!mesh.bindGroup || !mesh.uniformBuffer) {
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const buffer = new Float32Array(48);
|
|
845
|
+
const flagBuffer = new Uint32Array(buffer.buffer, 176, 4);
|
|
846
|
+
buffer.set(viewProj, 0);
|
|
847
|
+
buffer.set(mesh.transform.m, 16);
|
|
848
|
+
buffer.set(mesh.color, 32);
|
|
849
|
+
buffer[36] = mesh.material?.metallic ?? 0.0;
|
|
850
|
+
buffer[37] = mesh.material?.roughness ?? 0.6;
|
|
851
|
+
// Section plane data
|
|
852
|
+
if (sectionPlaneData) {
|
|
853
|
+
buffer[40] = sectionPlaneData.normal[0];
|
|
854
|
+
buffer[41] = sectionPlaneData.normal[1];
|
|
855
|
+
buffer[42] = sectionPlaneData.normal[2];
|
|
856
|
+
buffer[43] = sectionPlaneData.distance;
|
|
857
|
+
}
|
|
858
|
+
// Flags (not selected, transparent)
|
|
859
|
+
flagBuffer[0] = 0;
|
|
860
|
+
flagBuffer[1] = sectionPlaneData?.enabled ? 1 : 0;
|
|
861
|
+
flagBuffer[2] = 0;
|
|
862
|
+
flagBuffer[3] = 0;
|
|
863
|
+
device.queue.writeBuffer(mesh.uniformBuffer, 0, buffer);
|
|
864
|
+
pass.setBindGroup(0, mesh.bindGroup);
|
|
865
|
+
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
866
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
867
|
+
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
689
870
|
}
|
|
690
871
|
else {
|
|
691
|
-
// Fallback: render individual meshes (
|
|
872
|
+
// Fallback: render individual meshes (only when no batches exist)
|
|
692
873
|
// Render opaque meshes with per-mesh bind groups
|
|
693
874
|
for (const mesh of opaqueMeshes) {
|
|
694
875
|
if (mesh.bindGroup) {
|
|
@@ -701,17 +882,20 @@ export class Renderer {
|
|
|
701
882
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
702
883
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
703
884
|
}
|
|
704
|
-
// Render transparent meshes with
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
885
|
+
// Render transparent meshes with transparent pipeline (alpha blending)
|
|
886
|
+
if (transparentMeshes.length > 0) {
|
|
887
|
+
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
888
|
+
for (const mesh of transparentMeshes) {
|
|
889
|
+
if (mesh.bindGroup) {
|
|
890
|
+
pass.setBindGroup(0, mesh.bindGroup);
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
pass.setBindGroup(0, this.pipeline.getBindGroup());
|
|
894
|
+
}
|
|
895
|
+
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
896
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
897
|
+
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
711
898
|
}
|
|
712
|
-
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
713
|
-
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
714
|
-
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
715
899
|
}
|
|
716
900
|
}
|
|
717
901
|
// Render instanced meshes (much more efficient for repeated geometry)
|
|
@@ -759,6 +943,7 @@ export class Renderer {
|
|
|
759
943
|
}
|
|
760
944
|
/**
|
|
761
945
|
* Pick object at screen coordinates
|
|
946
|
+
* Respects visibility filtering so users can only select visible elements
|
|
762
947
|
*/
|
|
763
948
|
async pick(x, y, options) {
|
|
764
949
|
if (!this.picker) {
|
|
@@ -770,38 +955,195 @@ export class Renderer {
|
|
|
770
955
|
return null;
|
|
771
956
|
}
|
|
772
957
|
let meshes = this.scene.getMeshes();
|
|
773
|
-
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
958
|
+
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
959
|
+
// If we have batched meshes, check if we need CPU raycasting
|
|
960
|
+
// This handles the case where we have SOME individual meshes (e.g., from highlighting)
|
|
961
|
+
// but not enough for full GPU picking coverage
|
|
962
|
+
if (batchedMeshes.length > 0) {
|
|
963
|
+
// Collect all expressIds from batched meshes
|
|
964
|
+
const expressIds = new Set();
|
|
965
|
+
for (const batch of batchedMeshes) {
|
|
966
|
+
for (const expressId of batch.expressIds) {
|
|
967
|
+
expressIds.add(expressId);
|
|
784
968
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
969
|
+
}
|
|
970
|
+
// Track existing expressIds to avoid duplicates (using Set for O(1) lookup)
|
|
971
|
+
const existingExpressIds = new Set(meshes.map(m => m.expressId));
|
|
972
|
+
// Count how many meshes we'd need to create for full GPU picking
|
|
973
|
+
let toCreate = 0;
|
|
974
|
+
for (const expressId of expressIds) {
|
|
975
|
+
if (existingExpressIds.has(expressId))
|
|
976
|
+
continue;
|
|
977
|
+
if (options?.hiddenIds?.has(expressId))
|
|
978
|
+
continue;
|
|
979
|
+
if (options?.isolatedIds !== null && options?.isolatedIds !== undefined && !options.isolatedIds.has(expressId))
|
|
980
|
+
continue;
|
|
981
|
+
if (this.scene.hasMeshData(expressId))
|
|
982
|
+
toCreate++;
|
|
983
|
+
}
|
|
984
|
+
// PERFORMANCE FIX: Use CPU raycasting for large models instead of creating GPU meshes
|
|
985
|
+
// GPU picking requires individual mesh buffers; for 60K+ elements this is too slow
|
|
986
|
+
// CPU raycasting uses bounding box filtering + triangle tests - no GPU buffers needed
|
|
987
|
+
const MAX_PICK_MESH_CREATION = 500;
|
|
988
|
+
if (toCreate > MAX_PICK_MESH_CREATION) {
|
|
989
|
+
// Use CPU raycasting fallback - works regardless of how many individual meshes exist
|
|
990
|
+
const ray = this.camera.unprojectToRay(x, y, this.canvas.width, this.canvas.height);
|
|
991
|
+
const hit = this.scene.raycast(ray.origin, ray.direction, options?.hiddenIds, options?.isolatedIds);
|
|
992
|
+
return hit ? hit.expressId : null;
|
|
993
|
+
}
|
|
994
|
+
// For smaller models, create GPU meshes for picking
|
|
995
|
+
// Only create meshes for VISIBLE elements (not hidden, and either no isolation or in isolated set)
|
|
996
|
+
for (const expressId of expressIds) {
|
|
997
|
+
// Skip if already exists
|
|
998
|
+
if (existingExpressIds.has(expressId))
|
|
999
|
+
continue;
|
|
1000
|
+
// Skip if hidden
|
|
1001
|
+
if (options?.hiddenIds?.has(expressId))
|
|
1002
|
+
continue;
|
|
1003
|
+
// Skip if isolation is active and this entity is not isolated
|
|
1004
|
+
if (options?.isolatedIds !== null && options?.isolatedIds !== undefined && !options.isolatedIds.has(expressId))
|
|
1005
|
+
continue;
|
|
1006
|
+
if (this.scene.hasMeshData(expressId)) {
|
|
1007
|
+
const meshData = this.scene.getMeshData(expressId);
|
|
1008
|
+
if (meshData) {
|
|
1009
|
+
this.createMeshFromData(meshData);
|
|
1010
|
+
existingExpressIds.add(expressId); // Track newly created mesh
|
|
795
1011
|
}
|
|
796
1012
|
}
|
|
797
|
-
// Get updated meshes list (includes newly created ones)
|
|
798
|
-
meshes = this.scene.getMeshes();
|
|
799
1013
|
}
|
|
1014
|
+
// Get updated meshes list (includes newly created ones)
|
|
1015
|
+
meshes = this.scene.getMeshes();
|
|
1016
|
+
}
|
|
1017
|
+
// Apply visibility filtering to meshes before picking
|
|
1018
|
+
// This ensures users can only select elements that are actually visible
|
|
1019
|
+
if (options?.hiddenIds && options.hiddenIds.size > 0) {
|
|
1020
|
+
meshes = meshes.filter(mesh => !options.hiddenIds.has(mesh.expressId));
|
|
1021
|
+
}
|
|
1022
|
+
if (options?.isolatedIds !== null && options?.isolatedIds !== undefined) {
|
|
1023
|
+
meshes = meshes.filter(mesh => options.isolatedIds.has(mesh.expressId));
|
|
800
1024
|
}
|
|
801
1025
|
const viewProj = this.camera.getViewProjMatrix().m;
|
|
802
1026
|
const result = await this.picker.pick(x, y, this.canvas.width, this.canvas.height, meshes, viewProj);
|
|
803
1027
|
return result;
|
|
804
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Raycast into the scene to get precise 3D intersection point
|
|
1031
|
+
* This is more accurate than pick() as it returns the exact surface point
|
|
1032
|
+
*/
|
|
1033
|
+
raycastScene(x, y, options) {
|
|
1034
|
+
try {
|
|
1035
|
+
// Create ray from screen coordinates
|
|
1036
|
+
const ray = this.camera.unprojectToRay(x, y, this.canvas.width, this.canvas.height);
|
|
1037
|
+
// Get all mesh data from scene
|
|
1038
|
+
const allMeshData = [];
|
|
1039
|
+
const meshes = this.scene.getMeshes();
|
|
1040
|
+
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
1041
|
+
// Collect mesh data from regular meshes
|
|
1042
|
+
for (const mesh of meshes) {
|
|
1043
|
+
const meshData = this.scene.getMeshData(mesh.expressId);
|
|
1044
|
+
if (meshData) {
|
|
1045
|
+
// Apply visibility filtering
|
|
1046
|
+
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1047
|
+
continue;
|
|
1048
|
+
if (options?.isolatedIds !== null &&
|
|
1049
|
+
options?.isolatedIds !== undefined &&
|
|
1050
|
+
!options.isolatedIds.has(meshData.expressId)) {
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
allMeshData.push(meshData);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
// Collect mesh data from batched meshes
|
|
1057
|
+
for (const batch of batchedMeshes) {
|
|
1058
|
+
for (const expressId of batch.expressIds) {
|
|
1059
|
+
const meshData = this.scene.getMeshData(expressId);
|
|
1060
|
+
if (meshData) {
|
|
1061
|
+
// Apply visibility filtering
|
|
1062
|
+
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1063
|
+
continue;
|
|
1064
|
+
if (options?.isolatedIds !== null &&
|
|
1065
|
+
options?.isolatedIds !== undefined &&
|
|
1066
|
+
!options.isolatedIds.has(meshData.expressId)) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
allMeshData.push(meshData);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
if (allMeshData.length === 0) {
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
// Use BVH for performance if we have many meshes
|
|
1077
|
+
let meshesToTest = allMeshData;
|
|
1078
|
+
if (allMeshData.length > this.BVH_THRESHOLD) {
|
|
1079
|
+
// Check if BVH needs rebuilding
|
|
1080
|
+
const needsRebuild = !this.bvhCache ||
|
|
1081
|
+
!this.bvhCache.isBuilt ||
|
|
1082
|
+
this.bvhCache.meshCount !== allMeshData.length;
|
|
1083
|
+
if (needsRebuild) {
|
|
1084
|
+
// Build BVH only when needed
|
|
1085
|
+
this.bvh.build(allMeshData);
|
|
1086
|
+
this.bvhCache = {
|
|
1087
|
+
meshCount: allMeshData.length,
|
|
1088
|
+
meshData: allMeshData,
|
|
1089
|
+
isBuilt: true,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
// Use BVH to filter meshes
|
|
1093
|
+
const meshIndices = this.bvh.getMeshesForRay(ray, allMeshData);
|
|
1094
|
+
meshesToTest = meshIndices.map(i => allMeshData[i]);
|
|
1095
|
+
}
|
|
1096
|
+
// Perform raycasting
|
|
1097
|
+
const intersection = this.raycaster.raycast(ray, meshesToTest);
|
|
1098
|
+
if (!intersection) {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
// Detect snap targets if requested
|
|
1102
|
+
// Pass meshes near the ray to detect edges even when partially occluded
|
|
1103
|
+
let snapTarget;
|
|
1104
|
+
if (options?.snapOptions) {
|
|
1105
|
+
const cameraPos = this.camera.getPosition();
|
|
1106
|
+
const cameraFov = this.camera.getFOV();
|
|
1107
|
+
// Pass meshes that are near the ray (from BVH or all meshes if BVH not used)
|
|
1108
|
+
// This allows detecting edges even when they're behind other objects
|
|
1109
|
+
snapTarget = this.snapDetector.detectSnapTarget(ray, meshesToTest, // Pass all meshes near the ray
|
|
1110
|
+
intersection, { position: cameraPos, fov: cameraFov }, this.canvas.height, options.snapOptions) || undefined;
|
|
1111
|
+
}
|
|
1112
|
+
return {
|
|
1113
|
+
intersection,
|
|
1114
|
+
snap: snapTarget,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
console.error('Raycast error:', error);
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Invalidate BVH cache (call when geometry changes)
|
|
1124
|
+
*/
|
|
1125
|
+
invalidateBVHCache() {
|
|
1126
|
+
this.bvhCache = null;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Get the raycaster instance (for advanced usage)
|
|
1130
|
+
*/
|
|
1131
|
+
getRaycaster() {
|
|
1132
|
+
return this.raycaster;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Get the snap detector instance (for advanced usage)
|
|
1136
|
+
*/
|
|
1137
|
+
getSnapDetector() {
|
|
1138
|
+
return this.snapDetector;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Clear all caches (call when geometry changes)
|
|
1142
|
+
*/
|
|
1143
|
+
clearCaches() {
|
|
1144
|
+
this.invalidateBVHCache();
|
|
1145
|
+
this.snapDetector.clearCache();
|
|
1146
|
+
}
|
|
805
1147
|
/**
|
|
806
1148
|
* Resize canvas
|
|
807
1149
|
*/
|