@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/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
- // When using batched rendering with visibility filtering, we need individual meshes
377
- // Create them lazily from stored MeshData for visible elements only
378
- const batchedMeshes = this.scene.getBatchedMeshes();
379
- if (hasVisibilityFiltering && batchedMeshes.length > 0 && meshes.length === 0) {
380
- // Collect all expressIds from batched meshes
381
- const allExpressIds = new Set();
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
- view: textureView,
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: 1.0,
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
- if (allBatchedMeshes.length > 0 && !hasVisibilityFiltering) {
576
- // Separate batches into selected and non-selected
577
- const nonSelectedBatches = [];
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
- // Render ALL batches normally (non-selected meshes will render normally)
588
- // Selected meshes will be rendered individually on top with highlight
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
- continue;
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
- const allMeshes = this.scene.getMeshes();
629
- const existingMeshIds = new Set(allMeshes.map(m => m.expressId));
630
- // Create GPU resources lazily for selected meshes that don't have them yet
631
- for (const selectedId of selectedExpressIds) {
632
- if (!existingMeshIds.has(selectedId) && this.scene.hasMeshData(selectedId)) {
633
- const meshData = this.scene.getMeshData(selectedId);
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 (includes newly created ones)
638
- const selectedMeshes = this.scene.getMeshes().filter(mesh => selectedExpressIds.has(mesh.expressId));
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 (slower but works)
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 per-mesh bind groups
705
- for (const mesh of transparentMeshes) {
706
- if (mesh.bindGroup) {
707
- pass.setBindGroup(0, mesh.bindGroup);
708
- }
709
- else {
710
- pass.setBindGroup(0, this.pipeline.getBindGroup());
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
- // If we have batched meshes but no regular meshes, create picking meshes from stored MeshData
774
- // This implements lazy loading for picking - meshes are created on-demand from MeshData
775
- if (meshes.length === 0) {
776
- const batchedMeshes = this.scene.getBatchedMeshes();
777
- if (batchedMeshes.length > 0) {
778
- // Collect all expressIds from batched meshes
779
- const expressIds = new Set();
780
- for (const batch of batchedMeshes) {
781
- for (const expressId of batch.expressIds) {
782
- expressIds.add(expressId);
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
- // Track existing expressIds to avoid duplicates (using Set for O(1) lookup)
786
- const existingExpressIds = new Set(meshes.map(m => m.expressId));
787
- // Create picking meshes lazily from stored MeshData
788
- for (const expressId of expressIds) {
789
- if (!existingExpressIds.has(expressId) && this.scene.hasMeshData(expressId)) {
790
- const meshData = this.scene.getMeshData(expressId);
791
- if (meshData) {
792
- this.createMeshFromData(meshData);
793
- existingExpressIds.add(expressId); // Track newly created mesh
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
  */