@ifc-lite/renderer 1.1.7 → 1.2.1
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 +51 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +587 -125
- 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 +351 -49
- 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/section-plane.d.ts +14 -4
- package/dist/section-plane.d.ts.map +1 -1
- package/dist/section-plane.js +129 -53
- package/dist/section-plane.js.map +1 -1
- package/dist/snap-detector.d.ts +119 -0
- package/dist/snap-detector.d.ts.map +1 -0
- package/dist/snap-detector.js +706 -0
- package/dist/snap-detector.js.map +1 -0
- package/dist/types.d.ts +14 -1
- 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
|
|
@@ -57,7 +76,7 @@ export class Renderer {
|
|
|
57
76
|
this.pipeline = new RenderPipeline(this.device, width, height);
|
|
58
77
|
this.instancedPipeline = new InstancedRenderPipeline(this.device, width, height);
|
|
59
78
|
this.picker = new Picker(this.device, width, height);
|
|
60
|
-
this.sectionPlaneRenderer = new SectionPlaneRenderer(this.device.getDevice(), this.device.getFormat());
|
|
79
|
+
this.sectionPlaneRenderer = new SectionPlaneRenderer(this.device.getDevice(), this.device.getFormat(), this.pipeline.getSampleCount());
|
|
61
80
|
this.camera.setAspect(width / height);
|
|
62
81
|
}
|
|
63
82
|
/**
|
|
@@ -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 {
|
|
@@ -467,50 +459,64 @@ export class Renderer {
|
|
|
467
459
|
const allMeshes = [...opaqueMeshes, ...transparentMeshes];
|
|
468
460
|
const selectedId = options.selectedId;
|
|
469
461
|
const selectedIds = options.selectedIds;
|
|
470
|
-
// Calculate section plane parameters
|
|
462
|
+
// Calculate section plane parameters and model bounds
|
|
463
|
+
// Always calculate bounds when sectionPlane is provided (for preview and active mode)
|
|
471
464
|
let sectionPlaneData;
|
|
472
|
-
if (options.sectionPlane
|
|
473
|
-
//
|
|
474
|
-
const normal = [0, 0, 0];
|
|
475
|
-
if (options.sectionPlane.axis === 'x')
|
|
476
|
-
normal[0] = 1;
|
|
477
|
-
else if (options.sectionPlane.axis === 'y')
|
|
478
|
-
normal[1] = 1;
|
|
479
|
-
else
|
|
480
|
-
normal[2] = 1;
|
|
481
|
-
// Get model bounds for calculating plane position and visual
|
|
465
|
+
if (options.sectionPlane) {
|
|
466
|
+
// Get model bounds from ALL geometry sources: individual meshes AND batched meshes
|
|
482
467
|
const boundsMin = { x: Infinity, y: Infinity, z: Infinity };
|
|
483
468
|
const boundsMax = { x: -Infinity, y: -Infinity, z: -Infinity };
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
469
|
+
// Check individual meshes
|
|
470
|
+
for (const mesh of meshes) {
|
|
471
|
+
if (mesh.bounds) {
|
|
472
|
+
boundsMin.x = Math.min(boundsMin.x, mesh.bounds.min[0]);
|
|
473
|
+
boundsMin.y = Math.min(boundsMin.y, mesh.bounds.min[1]);
|
|
474
|
+
boundsMin.z = Math.min(boundsMin.z, mesh.bounds.min[2]);
|
|
475
|
+
boundsMax.x = Math.max(boundsMax.x, mesh.bounds.max[0]);
|
|
476
|
+
boundsMax.y = Math.max(boundsMax.y, mesh.bounds.max[1]);
|
|
477
|
+
boundsMax.z = Math.max(boundsMax.z, mesh.bounds.max[2]);
|
|
494
478
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
479
|
+
}
|
|
480
|
+
// Check batched meshes (most geometry is here!)
|
|
481
|
+
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
482
|
+
for (const batch of batchedMeshes) {
|
|
483
|
+
if (batch.bounds) {
|
|
484
|
+
boundsMin.x = Math.min(boundsMin.x, batch.bounds.min[0]);
|
|
485
|
+
boundsMin.y = Math.min(boundsMin.y, batch.bounds.min[1]);
|
|
486
|
+
boundsMin.z = Math.min(boundsMin.z, batch.bounds.min[2]);
|
|
487
|
+
boundsMax.x = Math.max(boundsMax.x, batch.bounds.max[0]);
|
|
488
|
+
boundsMax.y = Math.max(boundsMax.y, batch.bounds.max[1]);
|
|
489
|
+
boundsMax.z = Math.max(boundsMax.z, batch.bounds.max[2]);
|
|
498
490
|
}
|
|
499
491
|
}
|
|
500
|
-
|
|
492
|
+
// Fallback if no bounds found
|
|
493
|
+
if (!Number.isFinite(boundsMin.x)) {
|
|
501
494
|
boundsMin.x = boundsMin.y = boundsMin.z = -100;
|
|
502
495
|
boundsMax.x = boundsMax.y = boundsMax.z = 100;
|
|
503
496
|
}
|
|
504
497
|
// Store bounds for section plane visual
|
|
505
498
|
this.modelBounds = { min: boundsMin, max: boundsMax };
|
|
506
|
-
//
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
499
|
+
// Only calculate clipping data if section is enabled
|
|
500
|
+
if (options.sectionPlane.enabled) {
|
|
501
|
+
// Calculate plane normal based on semantic axis
|
|
502
|
+
// down = Y axis (horizontal cut), front = Z axis, side = X axis
|
|
503
|
+
const normal = [0, 0, 0];
|
|
504
|
+
if (options.sectionPlane.axis === 'side')
|
|
505
|
+
normal[0] = 1; // X axis
|
|
506
|
+
else if (options.sectionPlane.axis === 'down')
|
|
507
|
+
normal[1] = 1; // Y axis (horizontal)
|
|
508
|
+
else
|
|
509
|
+
normal[2] = 1; // Z axis (front)
|
|
510
|
+
// Get axis-specific range based on semantic axis
|
|
511
|
+
// Use min/max overrides from sectionPlane if provided (storey-based range)
|
|
512
|
+
const axisIdx = options.sectionPlane.axis === 'side' ? 'x' : options.sectionPlane.axis === 'down' ? 'y' : 'z';
|
|
513
|
+
const minVal = options.sectionPlane.min ?? boundsMin[axisIdx];
|
|
514
|
+
const maxVal = options.sectionPlane.max ?? boundsMax[axisIdx];
|
|
515
|
+
// Calculate plane distance from position percentage
|
|
516
|
+
const range = maxVal - minVal;
|
|
517
|
+
const distance = minVal + (options.sectionPlane.position / 100) * range;
|
|
518
|
+
sectionPlaneData = { normal, distance, enabled: true };
|
|
519
|
+
}
|
|
514
520
|
}
|
|
515
521
|
for (const mesh of allMeshes) {
|
|
516
522
|
if (mesh.uniformBuffer) {
|
|
@@ -551,30 +557,92 @@ export class Renderer {
|
|
|
551
557
|
}
|
|
552
558
|
// Now record draw commands
|
|
553
559
|
const encoder = device.createCommandEncoder();
|
|
560
|
+
// Set up MSAA rendering if enabled
|
|
561
|
+
const msaaView = this.pipeline.getMultisampleTextureView();
|
|
562
|
+
const useMSAA = msaaView !== null && this.pipeline.getSampleCount() > 1;
|
|
554
563
|
const pass = encoder.beginRenderPass({
|
|
555
564
|
colorAttachments: [
|
|
556
565
|
{
|
|
557
|
-
|
|
566
|
+
// If MSAA enabled: render to multisample texture, resolve to swap chain
|
|
567
|
+
// If MSAA disabled: render directly to swap chain
|
|
568
|
+
view: useMSAA ? msaaView : textureView,
|
|
569
|
+
resolveTarget: useMSAA ? textureView : undefined,
|
|
558
570
|
loadOp: 'clear',
|
|
559
571
|
clearValue: clearColor,
|
|
560
|
-
storeOp: 'store',
|
|
572
|
+
storeOp: useMSAA ? 'discard' : 'store', // Discard MSAA buffer after resolve
|
|
561
573
|
},
|
|
562
574
|
],
|
|
563
575
|
depthStencilAttachment: {
|
|
564
576
|
view: this.pipeline.getDepthTextureView(),
|
|
565
|
-
depthClearValue:
|
|
577
|
+
depthClearValue: 0.0, // Reverse-Z: clear to 0.0 (far plane)
|
|
566
578
|
depthLoadOp: 'clear',
|
|
567
579
|
depthStoreOp: 'store',
|
|
568
580
|
},
|
|
569
581
|
});
|
|
570
582
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
571
583
|
// 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
584
|
const allBatchedMeshes = this.scene.getBatchedMeshes();
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
585
|
+
// PERFORMANCE FIX: Always use batch rendering when we have batches
|
|
586
|
+
// Apply visibility filtering at the BATCH level instead of creating individual meshes
|
|
587
|
+
// This keeps draw calls at ~50-200 instead of 60K+
|
|
588
|
+
if (allBatchedMeshes.length > 0) {
|
|
589
|
+
// Pre-compute visibility for each batch (only when filtering is active)
|
|
590
|
+
// A batch is visible if ANY of its elements are visible
|
|
591
|
+
// A batch is fully visible if ALL of its elements are visible
|
|
592
|
+
const batchVisibility = new Map();
|
|
593
|
+
if (hasVisibilityFiltering) {
|
|
594
|
+
for (const batch of allBatchedMeshes) {
|
|
595
|
+
let visibleCount = 0;
|
|
596
|
+
const total = batch.expressIds.length;
|
|
597
|
+
for (const expressId of batch.expressIds) {
|
|
598
|
+
const isHidden = options.hiddenIds?.has(expressId) ?? false;
|
|
599
|
+
const isIsolated = !hasIsolatedFilter || options.isolatedIds.has(expressId);
|
|
600
|
+
if (!isHidden && isIsolated) {
|
|
601
|
+
visibleCount++;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
batchVisibility.set(batch.colorKey, {
|
|
605
|
+
visible: visibleCount > 0,
|
|
606
|
+
fullyVisible: visibleCount === total,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Separate batches into opaque and transparent, filtering by visibility
|
|
611
|
+
// IMPORTANT: Only render FULLY visible batches - partially visible batches
|
|
612
|
+
// need individual mesh rendering to show only the visible elements
|
|
613
|
+
const opaqueBatches = [];
|
|
614
|
+
const transparentBatches = [];
|
|
615
|
+
// Collect visible expressIds from partially visible batches
|
|
616
|
+
// These need individual mesh rendering instead of batch rendering
|
|
617
|
+
const partiallyVisibleExpressIds = new Set();
|
|
618
|
+
for (const batch of allBatchedMeshes) {
|
|
619
|
+
// Check visibility
|
|
620
|
+
if (hasVisibilityFiltering) {
|
|
621
|
+
const vis = batchVisibility.get(batch.colorKey);
|
|
622
|
+
if (!vis || !vis.visible)
|
|
623
|
+
continue; // Skip completely hidden batches
|
|
624
|
+
// FIX: Skip partially visible batches - their visible elements
|
|
625
|
+
// will be rendered individually below
|
|
626
|
+
if (!vis.fullyVisible) {
|
|
627
|
+
// Collect the visible expressIds from this batch
|
|
628
|
+
for (const expressId of batch.expressIds) {
|
|
629
|
+
const isHidden = options.hiddenIds?.has(expressId) ?? false;
|
|
630
|
+
const isIsolated = !hasIsolatedFilter || options.isolatedIds.has(expressId);
|
|
631
|
+
if (!isHidden && isIsolated) {
|
|
632
|
+
partiallyVisibleExpressIds.add(expressId);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
continue; // Don't add batch to render list
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const alpha = batch.color[3];
|
|
639
|
+
if (alpha < 0.99) {
|
|
640
|
+
transparentBatches.push(batch);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
opaqueBatches.push(batch);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
578
646
|
const selectedExpressIds = new Set();
|
|
579
647
|
if (selectedId !== undefined && selectedId !== null) {
|
|
580
648
|
selectedExpressIds.add(selectedId);
|
|
@@ -584,11 +652,10 @@ export class Renderer {
|
|
|
584
652
|
selectedExpressIds.add(id);
|
|
585
653
|
}
|
|
586
654
|
}
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
for (const batch of allBatchedMeshes) {
|
|
655
|
+
// Helper function to render a batch
|
|
656
|
+
const renderBatch = (batch) => {
|
|
590
657
|
if (!batch.bindGroup || !batch.uniformBuffer)
|
|
591
|
-
|
|
658
|
+
return;
|
|
592
659
|
// Update uniform buffer for this batch
|
|
593
660
|
const buffer = new Float32Array(48);
|
|
594
661
|
const flagBuffer = new Uint32Array(buffer.buffer, 176, 4);
|
|
@@ -621,21 +688,109 @@ export class Renderer {
|
|
|
621
688
|
pass.setVertexBuffer(0, batch.vertexBuffer);
|
|
622
689
|
pass.setIndexBuffer(batch.indexBuffer, 'uint32');
|
|
623
690
|
pass.drawIndexed(batch.indexCount);
|
|
691
|
+
};
|
|
692
|
+
// Render opaque batches first with opaque pipeline
|
|
693
|
+
pass.setPipeline(this.pipeline.getPipeline());
|
|
694
|
+
for (const batch of opaqueBatches) {
|
|
695
|
+
renderBatch(batch);
|
|
696
|
+
}
|
|
697
|
+
// Render partially visible elements individually (from batches that had mixed visibility)
|
|
698
|
+
// This is the FIX for hide/isolate showing wrong batches
|
|
699
|
+
const allMeshes = this.scene.getMeshes();
|
|
700
|
+
const existingMeshIds = new Set(allMeshes.map(m => m.expressId));
|
|
701
|
+
if (partiallyVisibleExpressIds.size > 0) {
|
|
702
|
+
// Create GPU resources lazily for partially visible meshes
|
|
703
|
+
for (const pvId of partiallyVisibleExpressIds) {
|
|
704
|
+
if (!existingMeshIds.has(pvId) && this.scene.hasMeshData(pvId)) {
|
|
705
|
+
const meshData = this.scene.getMeshData(pvId);
|
|
706
|
+
this.createMeshFromData(meshData);
|
|
707
|
+
existingMeshIds.add(pvId);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// Get partially visible meshes and render them
|
|
711
|
+
const partialMeshes = this.scene.getMeshes().filter(mesh => partiallyVisibleExpressIds.has(mesh.expressId));
|
|
712
|
+
// Ensure partial meshes have uniform buffers and bind groups
|
|
713
|
+
for (const mesh of partialMeshes) {
|
|
714
|
+
if (!mesh.uniformBuffer && this.pipeline) {
|
|
715
|
+
mesh.uniformBuffer = device.createBuffer({
|
|
716
|
+
size: this.pipeline.getUniformBufferSize(),
|
|
717
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
718
|
+
});
|
|
719
|
+
mesh.bindGroup = device.createBindGroup({
|
|
720
|
+
layout: this.pipeline.getBindGroupLayout(),
|
|
721
|
+
entries: [
|
|
722
|
+
{
|
|
723
|
+
binding: 0,
|
|
724
|
+
resource: { buffer: mesh.uniformBuffer },
|
|
725
|
+
},
|
|
726
|
+
],
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// Render partially visible meshes (not selected, normal rendering)
|
|
731
|
+
for (const mesh of partialMeshes) {
|
|
732
|
+
if (!mesh.bindGroup || !mesh.uniformBuffer) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const buffer = new Float32Array(48);
|
|
736
|
+
const flagBuffer = new Uint32Array(buffer.buffer, 176, 4);
|
|
737
|
+
buffer.set(viewProj, 0);
|
|
738
|
+
buffer.set(mesh.transform.m, 16);
|
|
739
|
+
buffer.set(mesh.color, 32);
|
|
740
|
+
buffer[36] = mesh.material?.metallic ?? 0.0;
|
|
741
|
+
buffer[37] = mesh.material?.roughness ?? 0.6;
|
|
742
|
+
// Section plane data
|
|
743
|
+
if (sectionPlaneData) {
|
|
744
|
+
buffer[40] = sectionPlaneData.normal[0];
|
|
745
|
+
buffer[41] = sectionPlaneData.normal[1];
|
|
746
|
+
buffer[42] = sectionPlaneData.normal[2];
|
|
747
|
+
buffer[43] = sectionPlaneData.distance;
|
|
748
|
+
}
|
|
749
|
+
// Flags (not selected)
|
|
750
|
+
flagBuffer[0] = 0;
|
|
751
|
+
flagBuffer[1] = sectionPlaneData?.enabled ? 1 : 0;
|
|
752
|
+
flagBuffer[2] = 0;
|
|
753
|
+
flagBuffer[3] = 0;
|
|
754
|
+
device.queue.writeBuffer(mesh.uniformBuffer, 0, buffer);
|
|
755
|
+
// Use opaque or transparent pipeline based on alpha
|
|
756
|
+
const isTransparent = mesh.color[3] < 0.99;
|
|
757
|
+
if (isTransparent) {
|
|
758
|
+
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
pass.setPipeline(this.pipeline.getPipeline());
|
|
762
|
+
}
|
|
763
|
+
pass.setBindGroup(0, mesh.bindGroup);
|
|
764
|
+
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
765
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
766
|
+
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
767
|
+
}
|
|
624
768
|
}
|
|
625
769
|
// Render selected meshes individually for proper highlighting
|
|
626
770
|
// First, check if we have Mesh objects for selected IDs
|
|
627
771
|
// If not, create them lazily from stored MeshData
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
for (const
|
|
632
|
-
|
|
633
|
-
|
|
772
|
+
// FIX: Filter selected IDs by visibility BEFORE creating GPU resources
|
|
773
|
+
// This ensures highlights don't appear for hidden elements
|
|
774
|
+
const visibleSelectedIds = new Set();
|
|
775
|
+
for (const selId of selectedExpressIds) {
|
|
776
|
+
// Skip if hidden
|
|
777
|
+
if (options.hiddenIds?.has(selId))
|
|
778
|
+
continue;
|
|
779
|
+
// Skip if isolation is active and this entity is not isolated
|
|
780
|
+
if (hasIsolatedFilter && !options.isolatedIds.has(selId))
|
|
781
|
+
continue;
|
|
782
|
+
visibleSelectedIds.add(selId);
|
|
783
|
+
}
|
|
784
|
+
// Create GPU resources lazily for visible selected meshes that don't have them yet
|
|
785
|
+
for (const selId of visibleSelectedIds) {
|
|
786
|
+
if (!existingMeshIds.has(selId) && this.scene.hasMeshData(selId)) {
|
|
787
|
+
const meshData = this.scene.getMeshData(selId);
|
|
634
788
|
this.createMeshFromData(meshData);
|
|
789
|
+
existingMeshIds.add(selId);
|
|
635
790
|
}
|
|
636
791
|
}
|
|
637
|
-
// Now get selected meshes (
|
|
638
|
-
const selectedMeshes = this.scene.getMeshes().filter(mesh =>
|
|
792
|
+
// Now get selected meshes (only visible ones)
|
|
793
|
+
const selectedMeshes = this.scene.getMeshes().filter(mesh => visibleSelectedIds.has(mesh.expressId));
|
|
639
794
|
// Ensure selected meshes have uniform buffers and bind groups
|
|
640
795
|
for (const mesh of selectedMeshes) {
|
|
641
796
|
if (!mesh.uniformBuffer && this.pipeline) {
|
|
@@ -686,9 +841,49 @@ export class Renderer {
|
|
|
686
841
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
687
842
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
688
843
|
}
|
|
844
|
+
// Render transparent BATCHED meshes with transparent pipeline (after opaque batches and selections)
|
|
845
|
+
if (transparentBatches.length > 0) {
|
|
846
|
+
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
847
|
+
for (const batch of transparentBatches) {
|
|
848
|
+
renderBatch(batch);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
// Render transparent individual meshes with transparent pipeline
|
|
852
|
+
if (transparentMeshes.length > 0) {
|
|
853
|
+
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
854
|
+
for (const mesh of transparentMeshes) {
|
|
855
|
+
if (!mesh.bindGroup || !mesh.uniformBuffer) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const buffer = new Float32Array(48);
|
|
859
|
+
const flagBuffer = new Uint32Array(buffer.buffer, 176, 4);
|
|
860
|
+
buffer.set(viewProj, 0);
|
|
861
|
+
buffer.set(mesh.transform.m, 16);
|
|
862
|
+
buffer.set(mesh.color, 32);
|
|
863
|
+
buffer[36] = mesh.material?.metallic ?? 0.0;
|
|
864
|
+
buffer[37] = mesh.material?.roughness ?? 0.6;
|
|
865
|
+
// Section plane data
|
|
866
|
+
if (sectionPlaneData) {
|
|
867
|
+
buffer[40] = sectionPlaneData.normal[0];
|
|
868
|
+
buffer[41] = sectionPlaneData.normal[1];
|
|
869
|
+
buffer[42] = sectionPlaneData.normal[2];
|
|
870
|
+
buffer[43] = sectionPlaneData.distance;
|
|
871
|
+
}
|
|
872
|
+
// Flags (not selected, transparent)
|
|
873
|
+
flagBuffer[0] = 0;
|
|
874
|
+
flagBuffer[1] = sectionPlaneData?.enabled ? 1 : 0;
|
|
875
|
+
flagBuffer[2] = 0;
|
|
876
|
+
flagBuffer[3] = 0;
|
|
877
|
+
device.queue.writeBuffer(mesh.uniformBuffer, 0, buffer);
|
|
878
|
+
pass.setBindGroup(0, mesh.bindGroup);
|
|
879
|
+
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
880
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
881
|
+
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
689
884
|
}
|
|
690
885
|
else {
|
|
691
|
-
// Fallback: render individual meshes (
|
|
886
|
+
// Fallback: render individual meshes (only when no batches exist)
|
|
692
887
|
// Render opaque meshes with per-mesh bind groups
|
|
693
888
|
for (const mesh of opaqueMeshes) {
|
|
694
889
|
if (mesh.bindGroup) {
|
|
@@ -701,17 +896,20 @@ export class Renderer {
|
|
|
701
896
|
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
702
897
|
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
703
898
|
}
|
|
704
|
-
// Render transparent meshes with
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
899
|
+
// Render transparent meshes with transparent pipeline (alpha blending)
|
|
900
|
+
if (transparentMeshes.length > 0) {
|
|
901
|
+
pass.setPipeline(this.pipeline.getTransparentPipeline());
|
|
902
|
+
for (const mesh of transparentMeshes) {
|
|
903
|
+
if (mesh.bindGroup) {
|
|
904
|
+
pass.setBindGroup(0, mesh.bindGroup);
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
pass.setBindGroup(0, this.pipeline.getBindGroup());
|
|
908
|
+
}
|
|
909
|
+
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
910
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
911
|
+
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
711
912
|
}
|
|
712
|
-
pass.setVertexBuffer(0, mesh.vertexBuffer);
|
|
713
|
-
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
714
|
-
pass.drawIndexed(mesh.indexCount, 1, 0, 0, 0);
|
|
715
913
|
}
|
|
716
914
|
}
|
|
717
915
|
// Render instanced meshes (much more efficient for repeated geometry)
|
|
@@ -735,16 +933,20 @@ export class Renderer {
|
|
|
735
933
|
}
|
|
736
934
|
}
|
|
737
935
|
}
|
|
738
|
-
pass.end()
|
|
739
|
-
//
|
|
740
|
-
if (
|
|
741
|
-
this.sectionPlaneRenderer.
|
|
936
|
+
// Draw section plane visual BEFORE pass.end() (within same MSAA render pass)
|
|
937
|
+
// Always show plane when sectionPlane options are provided (as preview or active)
|
|
938
|
+
if (options.sectionPlane && this.sectionPlaneRenderer && this.modelBounds) {
|
|
939
|
+
this.sectionPlaneRenderer.draw(pass, {
|
|
742
940
|
axis: options.sectionPlane.axis,
|
|
743
941
|
position: options.sectionPlane.position,
|
|
744
942
|
bounds: this.modelBounds,
|
|
745
943
|
viewProj,
|
|
944
|
+
isPreview: !options.sectionPlane.enabled, // Preview mode when not enabled
|
|
945
|
+
min: options.sectionPlane.min,
|
|
946
|
+
max: options.sectionPlane.max,
|
|
746
947
|
});
|
|
747
948
|
}
|
|
949
|
+
pass.end();
|
|
748
950
|
device.queue.submit([encoder.finish()]);
|
|
749
951
|
}
|
|
750
952
|
catch (error) {
|
|
@@ -759,6 +961,7 @@ export class Renderer {
|
|
|
759
961
|
}
|
|
760
962
|
/**
|
|
761
963
|
* Pick object at screen coordinates
|
|
964
|
+
* Respects visibility filtering so users can only select visible elements
|
|
762
965
|
*/
|
|
763
966
|
async pick(x, y, options) {
|
|
764
967
|
if (!this.picker) {
|
|
@@ -770,38 +973,297 @@ export class Renderer {
|
|
|
770
973
|
return null;
|
|
771
974
|
}
|
|
772
975
|
let meshes = this.scene.getMeshes();
|
|
773
|
-
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
976
|
+
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
977
|
+
// If we have batched meshes, check if we need CPU raycasting
|
|
978
|
+
// This handles the case where we have SOME individual meshes (e.g., from highlighting)
|
|
979
|
+
// but not enough for full GPU picking coverage
|
|
980
|
+
if (batchedMeshes.length > 0) {
|
|
981
|
+
// Collect all expressIds from batched meshes
|
|
982
|
+
const expressIds = new Set();
|
|
983
|
+
for (const batch of batchedMeshes) {
|
|
984
|
+
for (const expressId of batch.expressIds) {
|
|
985
|
+
expressIds.add(expressId);
|
|
784
986
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
987
|
+
}
|
|
988
|
+
// Track existing expressIds to avoid duplicates (using Set for O(1) lookup)
|
|
989
|
+
const existingExpressIds = new Set(meshes.map(m => m.expressId));
|
|
990
|
+
// Count how many meshes we'd need to create for full GPU picking
|
|
991
|
+
let toCreate = 0;
|
|
992
|
+
for (const expressId of expressIds) {
|
|
993
|
+
if (existingExpressIds.has(expressId))
|
|
994
|
+
continue;
|
|
995
|
+
if (options?.hiddenIds?.has(expressId))
|
|
996
|
+
continue;
|
|
997
|
+
if (options?.isolatedIds !== null && options?.isolatedIds !== undefined && !options.isolatedIds.has(expressId))
|
|
998
|
+
continue;
|
|
999
|
+
if (this.scene.hasMeshData(expressId))
|
|
1000
|
+
toCreate++;
|
|
1001
|
+
}
|
|
1002
|
+
// PERFORMANCE FIX: Use CPU raycasting for large models instead of creating GPU meshes
|
|
1003
|
+
// GPU picking requires individual mesh buffers; for 60K+ elements this is too slow
|
|
1004
|
+
// CPU raycasting uses bounding box filtering + triangle tests - no GPU buffers needed
|
|
1005
|
+
const MAX_PICK_MESH_CREATION = 500;
|
|
1006
|
+
if (toCreate > MAX_PICK_MESH_CREATION) {
|
|
1007
|
+
// Use CPU raycasting fallback - works regardless of how many individual meshes exist
|
|
1008
|
+
const ray = this.camera.unprojectToRay(x, y, this.canvas.width, this.canvas.height);
|
|
1009
|
+
const hit = this.scene.raycast(ray.origin, ray.direction, options?.hiddenIds, options?.isolatedIds);
|
|
1010
|
+
return hit ? hit.expressId : null;
|
|
1011
|
+
}
|
|
1012
|
+
// For smaller models, create GPU meshes for picking
|
|
1013
|
+
// Only create meshes for VISIBLE elements (not hidden, and either no isolation or in isolated set)
|
|
1014
|
+
for (const expressId of expressIds) {
|
|
1015
|
+
// Skip if already exists
|
|
1016
|
+
if (existingExpressIds.has(expressId))
|
|
1017
|
+
continue;
|
|
1018
|
+
// Skip if hidden
|
|
1019
|
+
if (options?.hiddenIds?.has(expressId))
|
|
1020
|
+
continue;
|
|
1021
|
+
// Skip if isolation is active and this entity is not isolated
|
|
1022
|
+
if (options?.isolatedIds !== null && options?.isolatedIds !== undefined && !options.isolatedIds.has(expressId))
|
|
1023
|
+
continue;
|
|
1024
|
+
if (this.scene.hasMeshData(expressId)) {
|
|
1025
|
+
const meshData = this.scene.getMeshData(expressId);
|
|
1026
|
+
if (meshData) {
|
|
1027
|
+
this.createMeshFromData(meshData);
|
|
1028
|
+
existingExpressIds.add(expressId); // Track newly created mesh
|
|
795
1029
|
}
|
|
796
1030
|
}
|
|
797
|
-
// Get updated meshes list (includes newly created ones)
|
|
798
|
-
meshes = this.scene.getMeshes();
|
|
799
1031
|
}
|
|
1032
|
+
// Get updated meshes list (includes newly created ones)
|
|
1033
|
+
meshes = this.scene.getMeshes();
|
|
1034
|
+
}
|
|
1035
|
+
// Apply visibility filtering to meshes before picking
|
|
1036
|
+
// This ensures users can only select elements that are actually visible
|
|
1037
|
+
if (options?.hiddenIds && options.hiddenIds.size > 0) {
|
|
1038
|
+
meshes = meshes.filter(mesh => !options.hiddenIds.has(mesh.expressId));
|
|
1039
|
+
}
|
|
1040
|
+
if (options?.isolatedIds !== null && options?.isolatedIds !== undefined) {
|
|
1041
|
+
meshes = meshes.filter(mesh => options.isolatedIds.has(mesh.expressId));
|
|
800
1042
|
}
|
|
801
1043
|
const viewProj = this.camera.getViewProjMatrix().m;
|
|
802
1044
|
const result = await this.picker.pick(x, y, this.canvas.width, this.canvas.height, meshes, viewProj);
|
|
803
1045
|
return result;
|
|
804
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Raycast into the scene to get precise 3D intersection point
|
|
1049
|
+
* This is more accurate than pick() as it returns the exact surface point
|
|
1050
|
+
*/
|
|
1051
|
+
raycastScene(x, y, options) {
|
|
1052
|
+
try {
|
|
1053
|
+
// Create ray from screen coordinates
|
|
1054
|
+
const ray = this.camera.unprojectToRay(x, y, this.canvas.width, this.canvas.height);
|
|
1055
|
+
// Get all mesh data from scene
|
|
1056
|
+
const allMeshData = [];
|
|
1057
|
+
const meshes = this.scene.getMeshes();
|
|
1058
|
+
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
1059
|
+
// Collect mesh data from regular meshes
|
|
1060
|
+
for (const mesh of meshes) {
|
|
1061
|
+
const meshData = this.scene.getMeshData(mesh.expressId);
|
|
1062
|
+
if (meshData) {
|
|
1063
|
+
// Apply visibility filtering
|
|
1064
|
+
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1065
|
+
continue;
|
|
1066
|
+
if (options?.isolatedIds !== null &&
|
|
1067
|
+
options?.isolatedIds !== undefined &&
|
|
1068
|
+
!options.isolatedIds.has(meshData.expressId)) {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
allMeshData.push(meshData);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
// Collect mesh data from batched meshes
|
|
1075
|
+
for (const batch of batchedMeshes) {
|
|
1076
|
+
for (const expressId of batch.expressIds) {
|
|
1077
|
+
const meshData = this.scene.getMeshData(expressId);
|
|
1078
|
+
if (meshData) {
|
|
1079
|
+
// Apply visibility filtering
|
|
1080
|
+
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1081
|
+
continue;
|
|
1082
|
+
if (options?.isolatedIds !== null &&
|
|
1083
|
+
options?.isolatedIds !== undefined &&
|
|
1084
|
+
!options.isolatedIds.has(meshData.expressId)) {
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
allMeshData.push(meshData);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (allMeshData.length === 0) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
// Use BVH for performance if we have many meshes
|
|
1095
|
+
let meshesToTest = allMeshData;
|
|
1096
|
+
if (allMeshData.length > this.BVH_THRESHOLD) {
|
|
1097
|
+
// Check if BVH needs rebuilding
|
|
1098
|
+
const needsRebuild = !this.bvhCache ||
|
|
1099
|
+
!this.bvhCache.isBuilt ||
|
|
1100
|
+
this.bvhCache.meshCount !== allMeshData.length;
|
|
1101
|
+
if (needsRebuild) {
|
|
1102
|
+
// Build BVH only when needed
|
|
1103
|
+
this.bvh.build(allMeshData);
|
|
1104
|
+
this.bvhCache = {
|
|
1105
|
+
meshCount: allMeshData.length,
|
|
1106
|
+
meshData: allMeshData,
|
|
1107
|
+
isBuilt: true,
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
// Use BVH to filter meshes
|
|
1111
|
+
const meshIndices = this.bvh.getMeshesForRay(ray, allMeshData);
|
|
1112
|
+
meshesToTest = meshIndices.map(i => allMeshData[i]);
|
|
1113
|
+
}
|
|
1114
|
+
// Perform raycasting
|
|
1115
|
+
const intersection = this.raycaster.raycast(ray, meshesToTest);
|
|
1116
|
+
if (!intersection) {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
// Detect snap targets if requested
|
|
1120
|
+
// Pass meshes near the ray to detect edges even when partially occluded
|
|
1121
|
+
let snapTarget;
|
|
1122
|
+
if (options?.snapOptions) {
|
|
1123
|
+
const cameraPos = this.camera.getPosition();
|
|
1124
|
+
const cameraFov = this.camera.getFOV();
|
|
1125
|
+
// Pass meshes that are near the ray (from BVH or all meshes if BVH not used)
|
|
1126
|
+
// This allows detecting edges even when they're behind other objects
|
|
1127
|
+
snapTarget = this.snapDetector.detectSnapTarget(ray, meshesToTest, // Pass all meshes near the ray
|
|
1128
|
+
intersection, { position: cameraPos, fov: cameraFov }, this.canvas.height, options.snapOptions) || undefined;
|
|
1129
|
+
}
|
|
1130
|
+
return {
|
|
1131
|
+
intersection,
|
|
1132
|
+
snap: snapTarget,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
catch (error) {
|
|
1136
|
+
console.error('Raycast error:', error);
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Raycast with magnetic edge snapping behavior
|
|
1142
|
+
* This provides the "stick and slide along edges" experience
|
|
1143
|
+
*/
|
|
1144
|
+
raycastSceneMagnetic(x, y, currentEdgeLock, options) {
|
|
1145
|
+
try {
|
|
1146
|
+
// Create ray from screen coordinates
|
|
1147
|
+
const ray = this.camera.unprojectToRay(x, y, this.canvas.width, this.canvas.height);
|
|
1148
|
+
// Get all mesh data from scene
|
|
1149
|
+
const allMeshData = [];
|
|
1150
|
+
const meshes = this.scene.getMeshes();
|
|
1151
|
+
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
1152
|
+
// Collect mesh data from regular meshes
|
|
1153
|
+
for (const mesh of meshes) {
|
|
1154
|
+
const meshData = this.scene.getMeshData(mesh.expressId);
|
|
1155
|
+
if (meshData) {
|
|
1156
|
+
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1157
|
+
continue;
|
|
1158
|
+
if (options?.isolatedIds !== null &&
|
|
1159
|
+
options?.isolatedIds !== undefined &&
|
|
1160
|
+
!options.isolatedIds.has(meshData.expressId)) {
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
allMeshData.push(meshData);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
// Collect mesh data from batched meshes
|
|
1167
|
+
for (const batch of batchedMeshes) {
|
|
1168
|
+
for (const expressId of batch.expressIds) {
|
|
1169
|
+
const meshData = this.scene.getMeshData(expressId);
|
|
1170
|
+
if (meshData) {
|
|
1171
|
+
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1172
|
+
continue;
|
|
1173
|
+
if (options?.isolatedIds !== null &&
|
|
1174
|
+
options?.isolatedIds !== undefined &&
|
|
1175
|
+
!options.isolatedIds.has(meshData.expressId)) {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
allMeshData.push(meshData);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (allMeshData.length === 0) {
|
|
1183
|
+
return {
|
|
1184
|
+
intersection: null,
|
|
1185
|
+
snapTarget: null,
|
|
1186
|
+
edgeLock: {
|
|
1187
|
+
edge: null,
|
|
1188
|
+
meshExpressId: null,
|
|
1189
|
+
edgeT: 0,
|
|
1190
|
+
shouldLock: false,
|
|
1191
|
+
shouldRelease: true,
|
|
1192
|
+
isCorner: false,
|
|
1193
|
+
cornerValence: 0,
|
|
1194
|
+
},
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
// Use BVH for performance if we have many meshes
|
|
1198
|
+
let meshesToTest = allMeshData;
|
|
1199
|
+
if (allMeshData.length > this.BVH_THRESHOLD) {
|
|
1200
|
+
const needsRebuild = !this.bvhCache ||
|
|
1201
|
+
!this.bvhCache.isBuilt ||
|
|
1202
|
+
this.bvhCache.meshCount !== allMeshData.length;
|
|
1203
|
+
if (needsRebuild) {
|
|
1204
|
+
this.bvh.build(allMeshData);
|
|
1205
|
+
this.bvhCache = {
|
|
1206
|
+
meshCount: allMeshData.length,
|
|
1207
|
+
meshData: allMeshData,
|
|
1208
|
+
isBuilt: true,
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
const meshIndices = this.bvh.getMeshesForRay(ray, allMeshData);
|
|
1212
|
+
meshesToTest = meshIndices.map(i => allMeshData[i]);
|
|
1213
|
+
}
|
|
1214
|
+
// Perform raycasting
|
|
1215
|
+
const intersection = this.raycaster.raycast(ray, meshesToTest);
|
|
1216
|
+
// Use magnetic snap detection
|
|
1217
|
+
const cameraPos = this.camera.getPosition();
|
|
1218
|
+
const cameraFov = this.camera.getFOV();
|
|
1219
|
+
const magneticResult = this.snapDetector.detectMagneticSnap(ray, meshesToTest, intersection, { position: cameraPos, fov: cameraFov }, this.canvas.height, currentEdgeLock, options?.snapOptions || {});
|
|
1220
|
+
return {
|
|
1221
|
+
intersection,
|
|
1222
|
+
...magneticResult,
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
catch (error) {
|
|
1226
|
+
console.error('Magnetic raycast error:', error);
|
|
1227
|
+
return {
|
|
1228
|
+
intersection: null,
|
|
1229
|
+
snapTarget: null,
|
|
1230
|
+
edgeLock: {
|
|
1231
|
+
edge: null,
|
|
1232
|
+
meshExpressId: null,
|
|
1233
|
+
edgeT: 0,
|
|
1234
|
+
shouldLock: false,
|
|
1235
|
+
shouldRelease: true,
|
|
1236
|
+
isCorner: false,
|
|
1237
|
+
cornerValence: 0,
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Invalidate BVH cache (call when geometry changes)
|
|
1244
|
+
*/
|
|
1245
|
+
invalidateBVHCache() {
|
|
1246
|
+
this.bvhCache = null;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Get the raycaster instance (for advanced usage)
|
|
1250
|
+
*/
|
|
1251
|
+
getRaycaster() {
|
|
1252
|
+
return this.raycaster;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Get the snap detector instance (for advanced usage)
|
|
1256
|
+
*/
|
|
1257
|
+
getSnapDetector() {
|
|
1258
|
+
return this.snapDetector;
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Clear all caches (call when geometry changes)
|
|
1262
|
+
*/
|
|
1263
|
+
clearCaches() {
|
|
1264
|
+
this.invalidateBVHCache();
|
|
1265
|
+
this.snapDetector.clearCache();
|
|
1266
|
+
}
|
|
805
1267
|
/**
|
|
806
1268
|
* Resize canvas
|
|
807
1269
|
*/
|