@ifc-lite/renderer 1.6.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/dist/camera-animation.d.ts +108 -0
- package/dist/camera-animation.d.ts.map +1 -0
- package/dist/camera-animation.js +606 -0
- package/dist/camera-animation.js.map +1 -0
- package/dist/camera-controls.d.ts +75 -0
- package/dist/camera-controls.d.ts.map +1 -0
- package/dist/camera-controls.js +239 -0
- package/dist/camera-controls.js.map +1 -0
- package/dist/camera-projection.d.ts +51 -0
- package/dist/camera-projection.d.ts.map +1 -0
- package/dist/camera-projection.js +147 -0
- package/dist/camera-projection.js.map +1 -0
- package/dist/camera.d.ts +33 -45
- package/dist/camera.d.ts.map +1 -1
- package/dist/camera.js +128 -815
- package/dist/camera.js.map +1 -1
- package/dist/geometry-manager.d.ts +99 -0
- package/dist/geometry-manager.d.ts.map +1 -0
- package/dist/geometry-manager.js +387 -0
- package/dist/geometry-manager.js.map +1 -0
- package/dist/index.d.ts +7 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +62 -658
- package/dist/index.js.map +1 -1
- package/dist/math.d.ts +6 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/math.js +20 -0
- package/dist/math.js.map +1 -1
- package/dist/picking-manager.d.ts +31 -0
- package/dist/picking-manager.d.ts.map +1 -0
- package/dist/picking-manager.js +140 -0
- package/dist/picking-manager.js.map +1 -0
- package/dist/pipeline.d.ts +2 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +42 -0
- package/dist/pipeline.js.map +1 -1
- package/dist/raycast-engine.d.ts +76 -0
- package/dist/raycast-engine.d.ts.map +1 -0
- package/dist/raycast-engine.js +255 -0
- package/dist/raycast-engine.js.map +1 -0
- package/dist/scene.d.ts +26 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +134 -25
- package/dist/scene.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -19,6 +19,10 @@ export { FederationRegistry, federationRegistry } from './federation-registry.js
|
|
|
19
19
|
export * from './types.js';
|
|
20
20
|
// Zero-copy GPU upload (new - faster, less memory)
|
|
21
21
|
export { ZeroCopyGpuUploader, createZeroCopyUploader, } from './zero-copy-uploader.js';
|
|
22
|
+
// Extracted manager classes
|
|
23
|
+
export { GeometryManager } from './geometry-manager.js';
|
|
24
|
+
export { PickingManager } from './picking-manager.js';
|
|
25
|
+
export { RaycastEngine } from './raycast-engine.js';
|
|
22
26
|
import { WebGPUDevice } from './device.js';
|
|
23
27
|
import { RenderPipeline, InstancedRenderPipeline } from './pipeline.js';
|
|
24
28
|
import { Camera } from './camera.js';
|
|
@@ -27,11 +31,9 @@ import { Picker } from './picker.js';
|
|
|
27
31
|
import { FrustumUtils } from '@ifc-lite/spatial';
|
|
28
32
|
import { SectionPlaneRenderer } from './section-plane.js';
|
|
29
33
|
import { Section2DOverlayRenderer } from './section-2d-overlay.js';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { SnapDetector } from './snap-detector.js';
|
|
34
|
-
import { BVH } from './bvh.js';
|
|
34
|
+
import { GeometryManager } from './geometry-manager.js';
|
|
35
|
+
import { PickingManager } from './picking-manager.js';
|
|
36
|
+
import { RaycastEngine } from './raycast-engine.js';
|
|
35
37
|
/**
|
|
36
38
|
* Main renderer class
|
|
37
39
|
*/
|
|
@@ -45,14 +47,10 @@ export class Renderer {
|
|
|
45
47
|
canvas;
|
|
46
48
|
sectionPlaneRenderer = null;
|
|
47
49
|
section2DOverlayRenderer = null;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// BVH cache
|
|
53
|
-
bvhCache = null;
|
|
54
|
-
// Performance constants
|
|
55
|
-
BVH_THRESHOLD = 100;
|
|
50
|
+
// Composition: delegate to extracted managers
|
|
51
|
+
geometryManager;
|
|
52
|
+
pickingManager;
|
|
53
|
+
raycastEngine;
|
|
56
54
|
// Error rate limiting (log at most once per second)
|
|
57
55
|
lastRenderErrorTime = 0;
|
|
58
56
|
RENDER_ERROR_THROTTLE_MS = 1000;
|
|
@@ -61,10 +59,10 @@ export class Renderer {
|
|
|
61
59
|
this.device = new WebGPUDevice();
|
|
62
60
|
this.camera = new Camera();
|
|
63
61
|
this.scene = new Scene();
|
|
64
|
-
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
62
|
+
// Create composition managers
|
|
63
|
+
this.geometryManager = new GeometryManager(this.device, this.scene);
|
|
64
|
+
this.pickingManager = new PickingManager(this.camera, this.scene, null, this.canvas, this.geometryManager);
|
|
65
|
+
this.raycastEngine = new RaycastEngine(this.camera, this.scene, this.canvas);
|
|
68
66
|
}
|
|
69
67
|
/**
|
|
70
68
|
* Initialize renderer
|
|
@@ -86,6 +84,8 @@ export class Renderer {
|
|
|
86
84
|
this.sectionPlaneRenderer = new SectionPlaneRenderer(this.device.getDevice(), this.device.getFormat(), this.pipeline.getSampleCount());
|
|
87
85
|
this.section2DOverlayRenderer = new Section2DOverlayRenderer(this.device.getDevice(), this.device.getFormat(), this.pipeline.getSampleCount());
|
|
88
86
|
this.camera.setAspect(width / height);
|
|
87
|
+
// Update picking manager with initialized picker
|
|
88
|
+
this.pickingManager.setPicker(this.picker);
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Load geometry from GeometryResult or MeshData array
|
|
@@ -94,20 +94,10 @@ export class Renderer {
|
|
|
94
94
|
* @param geometry - Either a GeometryResult from geometry.process() or an array of MeshData
|
|
95
95
|
*/
|
|
96
96
|
loadGeometry(geometry) {
|
|
97
|
-
if (!this.
|
|
97
|
+
if (!this.pipeline) {
|
|
98
98
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
if (meshes.length === 0) {
|
|
102
|
-
console.warn('[Renderer] loadGeometry called with empty mesh array');
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// Use batched rendering for optimal performance
|
|
106
|
-
const device = this.device.getDevice();
|
|
107
|
-
this.scene.appendToBatches(meshes, device, this.pipeline, false);
|
|
108
|
-
// Calculate and store model bounds for fitToView
|
|
109
|
-
this.updateModelBounds(meshes);
|
|
110
|
-
console.log(`[Renderer] Loaded ${meshes.length} meshes`);
|
|
100
|
+
this.geometryManager.loadGeometry(geometry, this.pipeline);
|
|
111
101
|
}
|
|
112
102
|
/**
|
|
113
103
|
* Add multiple meshes to the scene (convenience method for streaming)
|
|
@@ -116,158 +106,34 @@ export class Renderer {
|
|
|
116
106
|
* @param isStreaming - If true, throttles batch rebuilding for better streaming performance
|
|
117
107
|
*/
|
|
118
108
|
addMeshes(meshes, isStreaming = false) {
|
|
119
|
-
if (!this.
|
|
109
|
+
if (!this.pipeline) {
|
|
120
110
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
121
111
|
}
|
|
122
|
-
|
|
123
|
-
return;
|
|
124
|
-
const device = this.device.getDevice();
|
|
125
|
-
this.scene.appendToBatches(meshes, device, this.pipeline, isStreaming);
|
|
126
|
-
// Update model bounds incrementally
|
|
127
|
-
this.updateModelBounds(meshes);
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Update model bounds from mesh data
|
|
131
|
-
*/
|
|
132
|
-
updateModelBounds(meshes) {
|
|
133
|
-
if (!this.modelBounds) {
|
|
134
|
-
this.modelBounds = {
|
|
135
|
-
min: { x: Infinity, y: Infinity, z: Infinity },
|
|
136
|
-
max: { x: -Infinity, y: -Infinity, z: -Infinity }
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
for (const mesh of meshes) {
|
|
140
|
-
const positions = mesh.positions;
|
|
141
|
-
for (let i = 0; i < positions.length; i += 3) {
|
|
142
|
-
const x = positions[i];
|
|
143
|
-
const y = positions[i + 1];
|
|
144
|
-
const z = positions[i + 2];
|
|
145
|
-
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
|
|
146
|
-
this.modelBounds.min.x = Math.min(this.modelBounds.min.x, x);
|
|
147
|
-
this.modelBounds.min.y = Math.min(this.modelBounds.min.y, y);
|
|
148
|
-
this.modelBounds.min.z = Math.min(this.modelBounds.min.z, z);
|
|
149
|
-
this.modelBounds.max.x = Math.max(this.modelBounds.max.x, x);
|
|
150
|
-
this.modelBounds.max.y = Math.max(this.modelBounds.max.y, y);
|
|
151
|
-
this.modelBounds.max.z = Math.max(this.modelBounds.max.z, z);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
112
|
+
this.geometryManager.addMeshes(meshes, this.pipeline, isStreaming);
|
|
155
113
|
}
|
|
156
114
|
/**
|
|
157
115
|
* Fit camera to view all loaded geometry
|
|
158
116
|
*/
|
|
159
117
|
fitToView() {
|
|
160
|
-
|
|
161
|
-
console.warn('[Renderer] fitToView called but no geometry loaded');
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const { min, max } = this.modelBounds;
|
|
165
|
-
// Calculate center and size
|
|
166
|
-
const center = {
|
|
167
|
-
x: (min.x + max.x) / 2,
|
|
168
|
-
y: (min.y + max.y) / 2,
|
|
169
|
-
z: (min.z + max.z) / 2
|
|
170
|
-
};
|
|
171
|
-
const size = Math.max(max.x - min.x, max.y - min.y, max.z - min.z);
|
|
172
|
-
// Position camera to see entire model
|
|
173
|
-
const distance = size * 1.5;
|
|
174
|
-
this.camera.setPosition(center.x + distance * 0.5, center.y + distance * 0.5, center.z + distance);
|
|
175
|
-
this.camera.setTarget(center.x, center.y, center.z);
|
|
118
|
+
this.geometryManager.fitToView(this.camera);
|
|
176
119
|
}
|
|
177
120
|
/**
|
|
178
121
|
* Add mesh to scene with per-mesh GPU resources for unique colors
|
|
179
122
|
*/
|
|
180
123
|
addMesh(mesh) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// Create uniform buffer for this mesh
|
|
185
|
-
mesh.uniformBuffer = device.createBuffer({
|
|
186
|
-
size: this.pipeline.getUniformBufferSize(),
|
|
187
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
188
|
-
});
|
|
189
|
-
// Create bind group for this mesh
|
|
190
|
-
mesh.bindGroup = device.createBindGroup({
|
|
191
|
-
layout: this.pipeline.getBindGroupLayout(),
|
|
192
|
-
entries: [
|
|
193
|
-
{
|
|
194
|
-
binding: 0,
|
|
195
|
-
resource: { buffer: mesh.uniformBuffer },
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
this.scene.addMesh(mesh);
|
|
124
|
+
if (!this.pipeline)
|
|
125
|
+
return;
|
|
126
|
+
this.geometryManager.addMesh(mesh, this.pipeline);
|
|
201
127
|
}
|
|
202
128
|
/**
|
|
203
129
|
* Add instanced geometry to scene
|
|
204
130
|
* Converts InstancedGeometry from geometry package to InstancedMesh for rendering
|
|
205
131
|
*/
|
|
206
132
|
addInstancedGeometry(geometry) {
|
|
207
|
-
if (!this.instancedPipeline
|
|
133
|
+
if (!this.instancedPipeline) {
|
|
208
134
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
209
135
|
}
|
|
210
|
-
|
|
211
|
-
// Upload positions and normals interleaved
|
|
212
|
-
const vertexCount = geometry.positions.length / 3;
|
|
213
|
-
const vertexData = new Float32Array(vertexCount * 6);
|
|
214
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
215
|
-
vertexData[i * 6 + 0] = geometry.positions[i * 3 + 0];
|
|
216
|
-
vertexData[i * 6 + 1] = geometry.positions[i * 3 + 1];
|
|
217
|
-
vertexData[i * 6 + 2] = geometry.positions[i * 3 + 2];
|
|
218
|
-
vertexData[i * 6 + 3] = geometry.normals[i * 3 + 0];
|
|
219
|
-
vertexData[i * 6 + 4] = geometry.normals[i * 3 + 1];
|
|
220
|
-
vertexData[i * 6 + 5] = geometry.normals[i * 3 + 2];
|
|
221
|
-
}
|
|
222
|
-
// Create vertex buffer with exact size needed (ensure it matches data size)
|
|
223
|
-
const vertexBufferSize = vertexData.byteLength;
|
|
224
|
-
const vertexBuffer = device.createBuffer({
|
|
225
|
-
size: vertexBufferSize,
|
|
226
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
227
|
-
});
|
|
228
|
-
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
|
|
229
|
-
// Create index buffer
|
|
230
|
-
const indexBuffer = device.createBuffer({
|
|
231
|
-
size: geometry.indices.byteLength,
|
|
232
|
-
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
233
|
-
});
|
|
234
|
-
device.queue.writeBuffer(indexBuffer, 0, geometry.indices);
|
|
235
|
-
// Create instance buffer: each instance is 80 bytes (20 floats: 16 for transform + 4 for color)
|
|
236
|
-
const instanceCount = geometry.instance_count;
|
|
237
|
-
const instanceData = new Float32Array(instanceCount * 20);
|
|
238
|
-
const expressIdToInstanceIndex = new Map();
|
|
239
|
-
for (let i = 0; i < instanceCount; i++) {
|
|
240
|
-
const instance = geometry.get_instance(i);
|
|
241
|
-
if (!instance)
|
|
242
|
-
continue;
|
|
243
|
-
const baseIdx = i * 20;
|
|
244
|
-
// Copy transform (16 floats)
|
|
245
|
-
instanceData.set(instance.transform, baseIdx);
|
|
246
|
-
// Copy color (4 floats)
|
|
247
|
-
instanceData[baseIdx + 16] = instance.color[0];
|
|
248
|
-
instanceData[baseIdx + 17] = instance.color[1];
|
|
249
|
-
instanceData[baseIdx + 18] = instance.color[2];
|
|
250
|
-
instanceData[baseIdx + 19] = instance.color[3];
|
|
251
|
-
expressIdToInstanceIndex.set(instance.expressId, i);
|
|
252
|
-
}
|
|
253
|
-
const instanceBuffer = device.createBuffer({
|
|
254
|
-
size: instanceData.byteLength,
|
|
255
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
256
|
-
});
|
|
257
|
-
device.queue.writeBuffer(instanceBuffer, 0, instanceData);
|
|
258
|
-
// Create and cache bind group to avoid per-frame allocation
|
|
259
|
-
const bindGroup = this.instancedPipeline.createInstanceBindGroup(instanceBuffer);
|
|
260
|
-
const instancedMesh = {
|
|
261
|
-
geometryId: Number(geometry.geometryId),
|
|
262
|
-
vertexBuffer,
|
|
263
|
-
indexBuffer,
|
|
264
|
-
indexCount: geometry.indices.length,
|
|
265
|
-
instanceBuffer,
|
|
266
|
-
instanceCount: instanceCount,
|
|
267
|
-
expressIdToInstanceIndex,
|
|
268
|
-
bindGroup,
|
|
269
|
-
};
|
|
270
|
-
this.scene.addInstancedMesh(instancedMesh);
|
|
136
|
+
this.geometryManager.addInstancedGeometry(geometry, this.instancedPipeline);
|
|
271
137
|
}
|
|
272
138
|
/**
|
|
273
139
|
* Convert MeshData array to instanced meshes for optimized rendering
|
|
@@ -275,175 +141,19 @@ export class Renderer {
|
|
|
275
141
|
* Call this in background after initial streaming completes
|
|
276
142
|
*/
|
|
277
143
|
convertToInstanced(meshDataArray) {
|
|
278
|
-
if (!this.instancedPipeline
|
|
144
|
+
if (!this.instancedPipeline) {
|
|
279
145
|
console.warn('[Renderer] Cannot convert to instanced: renderer not initialized');
|
|
280
146
|
return;
|
|
281
147
|
}
|
|
282
|
-
|
|
283
|
-
const instancedData = deduplicateMeshes(meshDataArray);
|
|
284
|
-
const device = this.device.getDevice();
|
|
285
|
-
let totalInstances = 0;
|
|
286
|
-
for (const group of instancedData) {
|
|
287
|
-
// Create vertex buffer (interleaved positions + normals)
|
|
288
|
-
const vertexCount = group.positions.length / 3;
|
|
289
|
-
const vertexData = new Float32Array(vertexCount * 6);
|
|
290
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
291
|
-
vertexData[i * 6 + 0] = group.positions[i * 3 + 0];
|
|
292
|
-
vertexData[i * 6 + 1] = group.positions[i * 3 + 1];
|
|
293
|
-
vertexData[i * 6 + 2] = group.positions[i * 3 + 2];
|
|
294
|
-
vertexData[i * 6 + 3] = group.normals[i * 3 + 0];
|
|
295
|
-
vertexData[i * 6 + 4] = group.normals[i * 3 + 1];
|
|
296
|
-
vertexData[i * 6 + 5] = group.normals[i * 3 + 2];
|
|
297
|
-
}
|
|
298
|
-
const vertexBuffer = device.createBuffer({
|
|
299
|
-
size: vertexData.byteLength,
|
|
300
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
301
|
-
});
|
|
302
|
-
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
|
|
303
|
-
// Create index buffer
|
|
304
|
-
const indexBuffer = device.createBuffer({
|
|
305
|
-
size: group.indices.byteLength,
|
|
306
|
-
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
307
|
-
});
|
|
308
|
-
device.queue.writeBuffer(indexBuffer, 0, group.indices);
|
|
309
|
-
// Create instance buffer: each instance is 80 bytes (20 floats: 16 for transform + 4 for color)
|
|
310
|
-
const instanceCount = group.instances.length;
|
|
311
|
-
const instanceData = new Float32Array(instanceCount * 20);
|
|
312
|
-
const expressIdToInstanceIndex = new Map();
|
|
313
|
-
// Identity matrix for now (instances use same geometry, different colors)
|
|
314
|
-
const identityTransform = new Float32Array([
|
|
315
|
-
1, 0, 0, 0,
|
|
316
|
-
0, 1, 0, 0,
|
|
317
|
-
0, 0, 1, 0,
|
|
318
|
-
0, 0, 0, 1,
|
|
319
|
-
]);
|
|
320
|
-
for (let i = 0; i < instanceCount; i++) {
|
|
321
|
-
const instance = group.instances[i];
|
|
322
|
-
const baseIdx = i * 20;
|
|
323
|
-
// Copy identity transform (16 floats)
|
|
324
|
-
instanceData.set(identityTransform, baseIdx);
|
|
325
|
-
// Copy color (4 floats)
|
|
326
|
-
instanceData[baseIdx + 16] = instance.color[0];
|
|
327
|
-
instanceData[baseIdx + 17] = instance.color[1];
|
|
328
|
-
instanceData[baseIdx + 18] = instance.color[2];
|
|
329
|
-
instanceData[baseIdx + 19] = instance.color[3];
|
|
330
|
-
expressIdToInstanceIndex.set(instance.expressId, i);
|
|
331
|
-
}
|
|
332
|
-
const instanceBuffer = device.createBuffer({
|
|
333
|
-
size: instanceData.byteLength,
|
|
334
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
335
|
-
});
|
|
336
|
-
device.queue.writeBuffer(instanceBuffer, 0, instanceData);
|
|
337
|
-
// Create and cache bind group to avoid per-frame allocation
|
|
338
|
-
const bindGroup = this.instancedPipeline.createInstanceBindGroup(instanceBuffer);
|
|
339
|
-
// Convert hash string to number for geometryId
|
|
340
|
-
const geometryId = this.hashStringToNumber(group.geometryHash);
|
|
341
|
-
const instancedMesh = {
|
|
342
|
-
geometryId,
|
|
343
|
-
vertexBuffer,
|
|
344
|
-
indexBuffer,
|
|
345
|
-
indexCount: group.indices.length,
|
|
346
|
-
instanceBuffer,
|
|
347
|
-
instanceCount: instanceCount,
|
|
348
|
-
expressIdToInstanceIndex,
|
|
349
|
-
bindGroup,
|
|
350
|
-
};
|
|
351
|
-
this.scene.addInstancedMesh(instancedMesh);
|
|
352
|
-
totalInstances += instanceCount;
|
|
353
|
-
}
|
|
354
|
-
// Clear regular meshes after conversion to avoid double rendering
|
|
355
|
-
const regularMeshCount = this.scene.getMeshes().length;
|
|
356
|
-
this.scene.clearRegularMeshes();
|
|
357
|
-
console.log(`[Renderer] Converted ${meshDataArray.length} meshes to ${instancedData.length} instanced geometries ` +
|
|
358
|
-
`(${totalInstances} total instances, ${(totalInstances / instancedData.length).toFixed(1)}x deduplication). ` +
|
|
359
|
-
`Cleared ${regularMeshCount} regular meshes.`);
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Hash string to number for geometryId
|
|
363
|
-
*/
|
|
364
|
-
hashStringToNumber(str) {
|
|
365
|
-
let hash = 0;
|
|
366
|
-
for (let i = 0; i < str.length; i++) {
|
|
367
|
-
const char = str.charCodeAt(i);
|
|
368
|
-
hash = ((hash << 5) - hash) + char;
|
|
369
|
-
hash = hash & hash; // Convert to 32-bit integer
|
|
370
|
-
}
|
|
371
|
-
return Math.abs(hash);
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Create a GPU Mesh from MeshData (lazy creation for selection highlighting)
|
|
375
|
-
* This is called on-demand when a mesh is selected, avoiding 2x buffer creation during streaming
|
|
376
|
-
*/
|
|
377
|
-
createMeshFromData(meshData) {
|
|
378
|
-
if (!this.device.isInitialized())
|
|
379
|
-
return;
|
|
380
|
-
const device = this.device.getDevice();
|
|
381
|
-
const vertexCount = meshData.positions.length / 3;
|
|
382
|
-
const interleaved = new Float32Array(vertexCount * 6);
|
|
383
|
-
for (let i = 0; i < vertexCount; i++) {
|
|
384
|
-
const base = i * 6;
|
|
385
|
-
const posBase = i * 3;
|
|
386
|
-
interleaved[base] = meshData.positions[posBase];
|
|
387
|
-
interleaved[base + 1] = meshData.positions[posBase + 1];
|
|
388
|
-
interleaved[base + 2] = meshData.positions[posBase + 2];
|
|
389
|
-
interleaved[base + 3] = meshData.normals[posBase];
|
|
390
|
-
interleaved[base + 4] = meshData.normals[posBase + 1];
|
|
391
|
-
interleaved[base + 5] = meshData.normals[posBase + 2];
|
|
392
|
-
}
|
|
393
|
-
const vertexBuffer = device.createBuffer({
|
|
394
|
-
size: interleaved.byteLength,
|
|
395
|
-
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
396
|
-
});
|
|
397
|
-
device.queue.writeBuffer(vertexBuffer, 0, interleaved);
|
|
398
|
-
const indexBuffer = device.createBuffer({
|
|
399
|
-
size: meshData.indices.byteLength,
|
|
400
|
-
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
401
|
-
});
|
|
402
|
-
device.queue.writeBuffer(indexBuffer, 0, meshData.indices);
|
|
403
|
-
// Add to scene with identity transform (positions already in world space)
|
|
404
|
-
this.scene.addMesh({
|
|
405
|
-
expressId: meshData.expressId,
|
|
406
|
-
modelIndex: meshData.modelIndex, // Preserve modelIndex for multi-model selection
|
|
407
|
-
vertexBuffer,
|
|
408
|
-
indexBuffer,
|
|
409
|
-
indexCount: meshData.indices.length,
|
|
410
|
-
transform: MathUtils.identity(),
|
|
411
|
-
color: meshData.color,
|
|
412
|
-
});
|
|
148
|
+
this.geometryManager.convertToInstanced(meshDataArray, this.instancedPipeline);
|
|
413
149
|
}
|
|
414
150
|
/**
|
|
415
151
|
* Ensure all meshes have GPU resources (call after adding meshes if pipeline wasn't ready)
|
|
416
152
|
*/
|
|
417
153
|
ensureMeshResources() {
|
|
418
|
-
if (!this.pipeline
|
|
154
|
+
if (!this.pipeline)
|
|
419
155
|
return;
|
|
420
|
-
|
|
421
|
-
let created = 0;
|
|
422
|
-
for (const mesh of this.scene.getMeshes()) {
|
|
423
|
-
if (!mesh.uniformBuffer) {
|
|
424
|
-
mesh.uniformBuffer = device.createBuffer({
|
|
425
|
-
size: this.pipeline.getUniformBufferSize(),
|
|
426
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
427
|
-
});
|
|
428
|
-
mesh.bindGroup = device.createBindGroup({
|
|
429
|
-
layout: this.pipeline.getBindGroupLayout(),
|
|
430
|
-
entries: [
|
|
431
|
-
{
|
|
432
|
-
binding: 0,
|
|
433
|
-
resource: { buffer: mesh.uniformBuffer },
|
|
434
|
-
},
|
|
435
|
-
],
|
|
436
|
-
});
|
|
437
|
-
created++;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
if (created > 0) {
|
|
441
|
-
const totalMeshCount = this.scene.getMeshes().length;
|
|
442
|
-
// Only log every 250 meshes or when creating many at once to reduce noise
|
|
443
|
-
if (totalMeshCount % 250 === 0 || created > 100) {
|
|
444
|
-
console.log(`[Renderer] Created GPU resources for ${created} new meshes (${totalMeshCount} total)`);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
156
|
+
this.geometryManager.ensureMeshResources(this.pipeline);
|
|
447
157
|
}
|
|
448
158
|
/**
|
|
449
159
|
* Render frame
|
|
@@ -594,7 +304,7 @@ export class Renderer {
|
|
|
594
304
|
boundsMax.x = boundsMax.y = boundsMax.z = 100;
|
|
595
305
|
}
|
|
596
306
|
// Store bounds for section plane visual
|
|
597
|
-
this.
|
|
307
|
+
this.geometryManager.setModelBounds({ min: boundsMin, max: boundsMax });
|
|
598
308
|
// Only calculate clipping data if section is enabled
|
|
599
309
|
if (options.sectionPlane.enabled) {
|
|
600
310
|
// Calculate plane normal based on semantic axis
|
|
@@ -609,7 +319,6 @@ export class Renderer {
|
|
|
609
319
|
// Apply building rotation if present (rotate normal around Y axis)
|
|
610
320
|
// Building rotation is in X-Y plane (Z is up in IFC, Y is up in WebGL)
|
|
611
321
|
if (options.buildingRotation !== undefined && options.buildingRotation !== 0) {
|
|
612
|
-
const originalNormal = [...normal];
|
|
613
322
|
const cosR = Math.cos(options.buildingRotation);
|
|
614
323
|
const sinR = Math.sin(options.buildingRotation);
|
|
615
324
|
// Rotate normal vector around Y axis (vertical)
|
|
@@ -826,10 +535,10 @@ export class Renderer {
|
|
|
826
535
|
// PERFORMANCE FIX: Render partially visible batches as sub-batches (not individual meshes!)
|
|
827
536
|
// This is the key optimization: instead of 10,000+ individual draw calls,
|
|
828
537
|
// we create cached sub-batches with only visible elements and render them as single draw calls
|
|
829
|
-
const
|
|
538
|
+
const allMeshesFromScene = this.scene.getMeshes();
|
|
830
539
|
// Track existing meshes by (expressId:modelIndex) to handle multi-model expressId collisions
|
|
831
540
|
// E.g., door #535 in model 0 vs beam #535 in model 1 need separate tracking
|
|
832
|
-
const existingMeshKeys = new Set(
|
|
541
|
+
const existingMeshKeys = new Set(allMeshesFromScene.map(m => `${m.expressId}:${m.modelIndex ?? 'any'}`));
|
|
833
542
|
if (partiallyVisibleBatches.length > 0) {
|
|
834
543
|
for (const { colorKey, visibleIds, color } of partiallyVisibleBatches) {
|
|
835
544
|
// Get or create a cached sub-batch for this visibility state
|
|
@@ -850,6 +559,18 @@ export class Renderer {
|
|
|
850
559
|
// Reset to opaque pipeline for subsequent rendering
|
|
851
560
|
pass.setPipeline(this.pipeline.getPipeline());
|
|
852
561
|
}
|
|
562
|
+
// Render color overlay batches (lens coloring) on top of ALL opaque geometry.
|
|
563
|
+
// Placed AFTER partial batches so depth buffer is complete for both full
|
|
564
|
+
// and partial batches. Uses 'equal' depth compare — only paints where
|
|
565
|
+
// original geometry wrote depth, so hidden entities never leak through.
|
|
566
|
+
const overrideBatches = this.scene.getOverrideBatches();
|
|
567
|
+
if (overrideBatches.length > 0) {
|
|
568
|
+
pass.setPipeline(this.pipeline.getOverlayPipeline());
|
|
569
|
+
for (const batch of overrideBatches) {
|
|
570
|
+
renderBatch(batch);
|
|
571
|
+
}
|
|
572
|
+
pass.setPipeline(this.pipeline.getPipeline());
|
|
573
|
+
}
|
|
853
574
|
// Render selected meshes individually for proper highlighting
|
|
854
575
|
// First, check if we have Mesh objects for selected IDs
|
|
855
576
|
// If not, create them lazily from stored MeshData
|
|
@@ -872,7 +593,7 @@ export class Renderer {
|
|
|
872
593
|
const meshKey = `${selId}:${selectedModelIndex ?? 'any'}`;
|
|
873
594
|
if (!existingMeshKeys.has(meshKey) && this.scene.hasMeshData(selId, selectedModelIndex)) {
|
|
874
595
|
const meshData = this.scene.getMeshData(selId, selectedModelIndex);
|
|
875
|
-
this.createMeshFromData(meshData);
|
|
596
|
+
this.geometryManager.createMeshFromData(meshData);
|
|
876
597
|
existingMeshKeys.add(meshKey);
|
|
877
598
|
}
|
|
878
599
|
}
|
|
@@ -1030,11 +751,12 @@ export class Renderer {
|
|
|
1030
751
|
}
|
|
1031
752
|
// Draw section plane visual BEFORE pass.end() (within same MSAA render pass)
|
|
1032
753
|
// Always show plane when sectionPlane options are provided (as preview or active)
|
|
1033
|
-
|
|
754
|
+
const modelBounds = this.geometryManager.getModelBounds();
|
|
755
|
+
if (options.sectionPlane && this.sectionPlaneRenderer && modelBounds) {
|
|
1034
756
|
this.sectionPlaneRenderer.draw(pass, {
|
|
1035
757
|
axis: options.sectionPlane.axis,
|
|
1036
758
|
position: options.sectionPlane.position,
|
|
1037
|
-
bounds:
|
|
759
|
+
bounds: modelBounds,
|
|
1038
760
|
viewProj,
|
|
1039
761
|
isPreview: !options.sectionPlane.enabled, // Preview mode when not enabled
|
|
1040
762
|
min: options.sectionPlane.min,
|
|
@@ -1045,7 +767,7 @@ export class Renderer {
|
|
|
1045
767
|
this.section2DOverlayRenderer.draw(pass, {
|
|
1046
768
|
axis: options.sectionPlane.axis,
|
|
1047
769
|
position: options.sectionPlane.position,
|
|
1048
|
-
bounds:
|
|
770
|
+
bounds: modelBounds,
|
|
1049
771
|
viewProj,
|
|
1050
772
|
min: options.sectionPlane.min,
|
|
1051
773
|
max: options.sectionPlane.max,
|
|
@@ -1076,112 +798,7 @@ export class Renderer {
|
|
|
1076
798
|
* These are scaled internally to match the actual canvas pixel dimensions.
|
|
1077
799
|
*/
|
|
1078
800
|
async pick(x, y, options) {
|
|
1079
|
-
|
|
1080
|
-
return null;
|
|
1081
|
-
}
|
|
1082
|
-
// Scale CSS pixel coordinates to canvas pixel coordinates
|
|
1083
|
-
// The canvas.width may differ from CSS width due to 64-pixel alignment for WebGPU
|
|
1084
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
1085
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
1086
|
-
return null;
|
|
1087
|
-
}
|
|
1088
|
-
const scaleX = this.canvas.width / rect.width;
|
|
1089
|
-
const scaleY = this.canvas.height / rect.height;
|
|
1090
|
-
const scaledX = x * scaleX;
|
|
1091
|
-
const scaledY = y * scaleY;
|
|
1092
|
-
// Skip picker during streaming for consistent performance
|
|
1093
|
-
// Picking during streaming would be slow and incomplete anyway
|
|
1094
|
-
if (options?.isStreaming) {
|
|
1095
|
-
return null;
|
|
1096
|
-
}
|
|
1097
|
-
let meshes = this.scene.getMeshes();
|
|
1098
|
-
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
1099
|
-
// If we have batched meshes, check if we need CPU raycasting
|
|
1100
|
-
// This handles the case where we have SOME individual meshes (e.g., from highlighting)
|
|
1101
|
-
// but not enough for full GPU picking coverage
|
|
1102
|
-
if (batchedMeshes.length > 0) {
|
|
1103
|
-
// Collect all expressIds from batched meshes
|
|
1104
|
-
const expressIds = new Set();
|
|
1105
|
-
for (const batch of batchedMeshes) {
|
|
1106
|
-
for (const expressId of batch.expressIds) {
|
|
1107
|
-
expressIds.add(expressId);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
// Track existing meshes by (expressId:modelIndex) for multi-model support
|
|
1111
|
-
// This handles expressId collisions (e.g., door #535 in model 0 vs beam #535 in model 1)
|
|
1112
|
-
const existingMeshKeys = new Set(meshes.map(m => `${m.expressId}:${m.modelIndex ?? 'any'}`));
|
|
1113
|
-
// Count how many meshes we'd need to create for full GPU picking
|
|
1114
|
-
// For multi-model, count all pieces with unique (expressId, modelIndex) pairs
|
|
1115
|
-
let toCreate = 0;
|
|
1116
|
-
for (const expressId of expressIds) {
|
|
1117
|
-
if (options?.hiddenIds?.has(expressId))
|
|
1118
|
-
continue;
|
|
1119
|
-
if (options?.isolatedIds !== null && options?.isolatedIds !== undefined && !options.isolatedIds.has(expressId))
|
|
1120
|
-
continue;
|
|
1121
|
-
// Get all pieces for this expressId (handles multi-model)
|
|
1122
|
-
const pieces = this.scene.getMeshDataPieces(expressId);
|
|
1123
|
-
if (pieces) {
|
|
1124
|
-
for (const piece of pieces) {
|
|
1125
|
-
const meshKey = `${expressId}:${piece.modelIndex ?? 'any'}`;
|
|
1126
|
-
if (!existingMeshKeys.has(meshKey)) {
|
|
1127
|
-
toCreate++;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
// PERFORMANCE FIX: Use CPU raycasting for large models instead of creating GPU meshes
|
|
1133
|
-
// GPU picking requires individual mesh buffers; for 60K+ elements this is too slow
|
|
1134
|
-
// CPU raycasting uses bounding box filtering + triangle tests - no GPU buffers needed
|
|
1135
|
-
const MAX_PICK_MESH_CREATION = 500;
|
|
1136
|
-
if (toCreate > MAX_PICK_MESH_CREATION) {
|
|
1137
|
-
// Use CPU raycasting fallback - works regardless of how many individual meshes exist
|
|
1138
|
-
const ray = this.camera.unprojectToRay(scaledX, scaledY, this.canvas.width, this.canvas.height);
|
|
1139
|
-
const hit = this.scene.raycast(ray.origin, ray.direction, options?.hiddenIds, options?.isolatedIds);
|
|
1140
|
-
if (!hit)
|
|
1141
|
-
return null;
|
|
1142
|
-
// CPU raycasting returns expressId and modelIndex
|
|
1143
|
-
return {
|
|
1144
|
-
expressId: hit.expressId,
|
|
1145
|
-
modelIndex: hit.modelIndex,
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
// For smaller models, create GPU meshes for picking
|
|
1149
|
-
// Only create meshes for VISIBLE elements (not hidden, and either no isolation or in isolated set)
|
|
1150
|
-
// For multi-model support: create meshes for ALL (expressId, modelIndex) pairs
|
|
1151
|
-
for (const expressId of expressIds) {
|
|
1152
|
-
// Skip if hidden
|
|
1153
|
-
if (options?.hiddenIds?.has(expressId))
|
|
1154
|
-
continue;
|
|
1155
|
-
// Skip if isolation is active and this entity is not isolated
|
|
1156
|
-
if (options?.isolatedIds !== null && options?.isolatedIds !== undefined && !options.isolatedIds.has(expressId))
|
|
1157
|
-
continue;
|
|
1158
|
-
// Get all pieces for this expressId (handles multi-model)
|
|
1159
|
-
const pieces = this.scene.getMeshDataPieces(expressId);
|
|
1160
|
-
if (pieces) {
|
|
1161
|
-
for (const piece of pieces) {
|
|
1162
|
-
const meshKey = `${piece.expressId}:${piece.modelIndex ?? 'any'}`;
|
|
1163
|
-
// Skip if mesh already exists for this (expressId, modelIndex) pair
|
|
1164
|
-
if (existingMeshKeys.has(meshKey))
|
|
1165
|
-
continue;
|
|
1166
|
-
this.createMeshFromData(piece);
|
|
1167
|
-
existingMeshKeys.add(meshKey);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
// Get updated meshes list (includes newly created ones)
|
|
1172
|
-
meshes = this.scene.getMeshes();
|
|
1173
|
-
}
|
|
1174
|
-
// Apply visibility filtering to meshes before picking
|
|
1175
|
-
// This ensures users can only select elements that are actually visible
|
|
1176
|
-
if (options?.hiddenIds && options.hiddenIds.size > 0) {
|
|
1177
|
-
meshes = meshes.filter(mesh => !options.hiddenIds.has(mesh.expressId));
|
|
1178
|
-
}
|
|
1179
|
-
if (options?.isolatedIds !== null && options?.isolatedIds !== undefined) {
|
|
1180
|
-
meshes = meshes.filter(mesh => options.isolatedIds.has(mesh.expressId));
|
|
1181
|
-
}
|
|
1182
|
-
const viewProj = this.camera.getViewProjMatrix().m;
|
|
1183
|
-
const result = await this.picker.pick(scaledX, scaledY, this.canvas.width, this.canvas.height, meshes, viewProj);
|
|
1184
|
-
return result;
|
|
801
|
+
return this.pickingManager.pick(x, y, options);
|
|
1185
802
|
}
|
|
1186
803
|
/**
|
|
1187
804
|
* Raycast into the scene to get precise 3D intersection point
|
|
@@ -1191,103 +808,7 @@ export class Renderer {
|
|
|
1191
808
|
* These are scaled internally to match the actual canvas pixel dimensions.
|
|
1192
809
|
*/
|
|
1193
810
|
raycastScene(x, y, options) {
|
|
1194
|
-
|
|
1195
|
-
// Scale CSS pixel coordinates to canvas pixel coordinates
|
|
1196
|
-
// The canvas.width may differ from CSS width due to 64-pixel alignment for WebGPU
|
|
1197
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
1198
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
1199
|
-
return null;
|
|
1200
|
-
}
|
|
1201
|
-
const scaleX = this.canvas.width / rect.width;
|
|
1202
|
-
const scaleY = this.canvas.height / rect.height;
|
|
1203
|
-
const scaledX = x * scaleX;
|
|
1204
|
-
const scaledY = y * scaleY;
|
|
1205
|
-
// Create ray from screen coordinates
|
|
1206
|
-
const ray = this.camera.unprojectToRay(scaledX, scaledY, this.canvas.width, this.canvas.height);
|
|
1207
|
-
// Get all mesh data from scene
|
|
1208
|
-
const allMeshData = [];
|
|
1209
|
-
const meshes = this.scene.getMeshes();
|
|
1210
|
-
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
1211
|
-
// Collect mesh data from regular meshes
|
|
1212
|
-
for (const mesh of meshes) {
|
|
1213
|
-
const meshData = this.scene.getMeshData(mesh.expressId);
|
|
1214
|
-
if (meshData) {
|
|
1215
|
-
// Apply visibility filtering
|
|
1216
|
-
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1217
|
-
continue;
|
|
1218
|
-
if (options?.isolatedIds !== null &&
|
|
1219
|
-
options?.isolatedIds !== undefined &&
|
|
1220
|
-
!options.isolatedIds.has(meshData.expressId)) {
|
|
1221
|
-
continue;
|
|
1222
|
-
}
|
|
1223
|
-
allMeshData.push(meshData);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
// Collect mesh data from batched meshes
|
|
1227
|
-
for (const batch of batchedMeshes) {
|
|
1228
|
-
for (const expressId of batch.expressIds) {
|
|
1229
|
-
const meshData = this.scene.getMeshData(expressId);
|
|
1230
|
-
if (meshData) {
|
|
1231
|
-
// Apply visibility filtering
|
|
1232
|
-
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1233
|
-
continue;
|
|
1234
|
-
if (options?.isolatedIds !== null &&
|
|
1235
|
-
options?.isolatedIds !== undefined &&
|
|
1236
|
-
!options.isolatedIds.has(meshData.expressId)) {
|
|
1237
|
-
continue;
|
|
1238
|
-
}
|
|
1239
|
-
allMeshData.push(meshData);
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
if (allMeshData.length === 0) {
|
|
1244
|
-
return null;
|
|
1245
|
-
}
|
|
1246
|
-
// Use BVH for performance if we have many meshes
|
|
1247
|
-
let meshesToTest = allMeshData;
|
|
1248
|
-
if (allMeshData.length > this.BVH_THRESHOLD) {
|
|
1249
|
-
// Check if BVH needs rebuilding
|
|
1250
|
-
const needsRebuild = !this.bvhCache ||
|
|
1251
|
-
!this.bvhCache.isBuilt ||
|
|
1252
|
-
this.bvhCache.meshCount !== allMeshData.length;
|
|
1253
|
-
if (needsRebuild) {
|
|
1254
|
-
// Build BVH only when needed
|
|
1255
|
-
this.bvh.build(allMeshData);
|
|
1256
|
-
this.bvhCache = {
|
|
1257
|
-
meshCount: allMeshData.length,
|
|
1258
|
-
meshData: allMeshData,
|
|
1259
|
-
isBuilt: true,
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
// Use BVH to filter meshes
|
|
1263
|
-
const meshIndices = this.bvh.getMeshesForRay(ray, allMeshData);
|
|
1264
|
-
meshesToTest = meshIndices.map(i => allMeshData[i]);
|
|
1265
|
-
}
|
|
1266
|
-
// Perform raycasting
|
|
1267
|
-
const intersection = this.raycaster.raycast(ray, meshesToTest);
|
|
1268
|
-
if (!intersection) {
|
|
1269
|
-
return null;
|
|
1270
|
-
}
|
|
1271
|
-
// Detect snap targets if requested
|
|
1272
|
-
// Pass meshes near the ray to detect edges even when partially occluded
|
|
1273
|
-
let snapTarget;
|
|
1274
|
-
if (options?.snapOptions) {
|
|
1275
|
-
const cameraPos = this.camera.getPosition();
|
|
1276
|
-
const cameraFov = this.camera.getFOV();
|
|
1277
|
-
// Pass meshes that are near the ray (from BVH or all meshes if BVH not used)
|
|
1278
|
-
// This allows detecting edges even when they're behind other objects
|
|
1279
|
-
snapTarget = this.snapDetector.detectSnapTarget(ray, meshesToTest, // Pass all meshes near the ray
|
|
1280
|
-
intersection, { position: cameraPos, fov: cameraFov }, this.canvas.height, options.snapOptions) || undefined;
|
|
1281
|
-
}
|
|
1282
|
-
return {
|
|
1283
|
-
intersection,
|
|
1284
|
-
snap: snapTarget,
|
|
1285
|
-
};
|
|
1286
|
-
}
|
|
1287
|
-
catch (error) {
|
|
1288
|
-
console.error('Raycast error:', error);
|
|
1289
|
-
return null;
|
|
1290
|
-
}
|
|
811
|
+
return this.raycastEngine.raycastScene(x, y, options);
|
|
1291
812
|
}
|
|
1292
813
|
/**
|
|
1293
814
|
* Raycast with magnetic edge snapping behavior
|
|
@@ -1297,149 +818,31 @@ export class Renderer {
|
|
|
1297
818
|
* These are scaled internally to match the actual canvas pixel dimensions.
|
|
1298
819
|
*/
|
|
1299
820
|
raycastSceneMagnetic(x, y, currentEdgeLock, options) {
|
|
1300
|
-
|
|
1301
|
-
// Scale CSS pixel coordinates to canvas pixel coordinates
|
|
1302
|
-
// The canvas.width may differ from CSS width due to 64-pixel alignment for WebGPU
|
|
1303
|
-
const rect = this.canvas.getBoundingClientRect();
|
|
1304
|
-
if (rect.width === 0 || rect.height === 0) {
|
|
1305
|
-
return {
|
|
1306
|
-
intersection: null,
|
|
1307
|
-
snapTarget: null,
|
|
1308
|
-
edgeLock: {
|
|
1309
|
-
edge: null,
|
|
1310
|
-
meshExpressId: null,
|
|
1311
|
-
edgeT: 0,
|
|
1312
|
-
shouldLock: false,
|
|
1313
|
-
shouldRelease: true,
|
|
1314
|
-
isCorner: false,
|
|
1315
|
-
cornerValence: 0,
|
|
1316
|
-
},
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
const scaleX = this.canvas.width / rect.width;
|
|
1320
|
-
const scaleY = this.canvas.height / rect.height;
|
|
1321
|
-
const scaledX = x * scaleX;
|
|
1322
|
-
const scaledY = y * scaleY;
|
|
1323
|
-
// Create ray from screen coordinates
|
|
1324
|
-
const ray = this.camera.unprojectToRay(scaledX, scaledY, this.canvas.width, this.canvas.height);
|
|
1325
|
-
// Get all mesh data from scene
|
|
1326
|
-
const allMeshData = [];
|
|
1327
|
-
const meshes = this.scene.getMeshes();
|
|
1328
|
-
const batchedMeshes = this.scene.getBatchedMeshes();
|
|
1329
|
-
// Collect mesh data from regular meshes
|
|
1330
|
-
for (const mesh of meshes) {
|
|
1331
|
-
const meshData = this.scene.getMeshData(mesh.expressId);
|
|
1332
|
-
if (meshData) {
|
|
1333
|
-
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1334
|
-
continue;
|
|
1335
|
-
if (options?.isolatedIds !== null &&
|
|
1336
|
-
options?.isolatedIds !== undefined &&
|
|
1337
|
-
!options.isolatedIds.has(meshData.expressId)) {
|
|
1338
|
-
continue;
|
|
1339
|
-
}
|
|
1340
|
-
allMeshData.push(meshData);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
// Collect mesh data from batched meshes
|
|
1344
|
-
for (const batch of batchedMeshes) {
|
|
1345
|
-
for (const expressId of batch.expressIds) {
|
|
1346
|
-
const meshData = this.scene.getMeshData(expressId);
|
|
1347
|
-
if (meshData) {
|
|
1348
|
-
if (options?.hiddenIds?.has(meshData.expressId))
|
|
1349
|
-
continue;
|
|
1350
|
-
if (options?.isolatedIds !== null &&
|
|
1351
|
-
options?.isolatedIds !== undefined &&
|
|
1352
|
-
!options.isolatedIds.has(meshData.expressId)) {
|
|
1353
|
-
continue;
|
|
1354
|
-
}
|
|
1355
|
-
allMeshData.push(meshData);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
if (allMeshData.length === 0) {
|
|
1360
|
-
return {
|
|
1361
|
-
intersection: null,
|
|
1362
|
-
snapTarget: null,
|
|
1363
|
-
edgeLock: {
|
|
1364
|
-
edge: null,
|
|
1365
|
-
meshExpressId: null,
|
|
1366
|
-
edgeT: 0,
|
|
1367
|
-
shouldLock: false,
|
|
1368
|
-
shouldRelease: true,
|
|
1369
|
-
isCorner: false,
|
|
1370
|
-
cornerValence: 0,
|
|
1371
|
-
},
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
// Use BVH for performance if we have many meshes
|
|
1375
|
-
let meshesToTest = allMeshData;
|
|
1376
|
-
if (allMeshData.length > this.BVH_THRESHOLD) {
|
|
1377
|
-
const needsRebuild = !this.bvhCache ||
|
|
1378
|
-
!this.bvhCache.isBuilt ||
|
|
1379
|
-
this.bvhCache.meshCount !== allMeshData.length;
|
|
1380
|
-
if (needsRebuild) {
|
|
1381
|
-
this.bvh.build(allMeshData);
|
|
1382
|
-
this.bvhCache = {
|
|
1383
|
-
meshCount: allMeshData.length,
|
|
1384
|
-
meshData: allMeshData,
|
|
1385
|
-
isBuilt: true,
|
|
1386
|
-
};
|
|
1387
|
-
}
|
|
1388
|
-
const meshIndices = this.bvh.getMeshesForRay(ray, allMeshData);
|
|
1389
|
-
meshesToTest = meshIndices.map(i => allMeshData[i]);
|
|
1390
|
-
}
|
|
1391
|
-
// Perform raycasting
|
|
1392
|
-
const intersection = this.raycaster.raycast(ray, meshesToTest);
|
|
1393
|
-
// Use magnetic snap detection
|
|
1394
|
-
const cameraPos = this.camera.getPosition();
|
|
1395
|
-
const cameraFov = this.camera.getFOV();
|
|
1396
|
-
const magneticResult = this.snapDetector.detectMagneticSnap(ray, meshesToTest, intersection, { position: cameraPos, fov: cameraFov }, this.canvas.height, currentEdgeLock, options?.snapOptions || {});
|
|
1397
|
-
return {
|
|
1398
|
-
intersection,
|
|
1399
|
-
...magneticResult,
|
|
1400
|
-
};
|
|
1401
|
-
}
|
|
1402
|
-
catch (error) {
|
|
1403
|
-
console.error('Magnetic raycast error:', error);
|
|
1404
|
-
return {
|
|
1405
|
-
intersection: null,
|
|
1406
|
-
snapTarget: null,
|
|
1407
|
-
edgeLock: {
|
|
1408
|
-
edge: null,
|
|
1409
|
-
meshExpressId: null,
|
|
1410
|
-
edgeT: 0,
|
|
1411
|
-
shouldLock: false,
|
|
1412
|
-
shouldRelease: true,
|
|
1413
|
-
isCorner: false,
|
|
1414
|
-
cornerValence: 0,
|
|
1415
|
-
},
|
|
1416
|
-
};
|
|
1417
|
-
}
|
|
821
|
+
return this.raycastEngine.raycastSceneMagnetic(x, y, currentEdgeLock, options);
|
|
1418
822
|
}
|
|
1419
823
|
/**
|
|
1420
824
|
* Invalidate BVH cache (call when geometry changes)
|
|
1421
825
|
*/
|
|
1422
826
|
invalidateBVHCache() {
|
|
1423
|
-
this.
|
|
827
|
+
this.raycastEngine.invalidateBVHCache();
|
|
1424
828
|
}
|
|
1425
829
|
/**
|
|
1426
830
|
* Get the raycaster instance (for advanced usage)
|
|
1427
831
|
*/
|
|
1428
832
|
getRaycaster() {
|
|
1429
|
-
return this.
|
|
833
|
+
return this.raycastEngine.getRaycaster();
|
|
1430
834
|
}
|
|
1431
835
|
/**
|
|
1432
836
|
* Get the snap detector instance (for advanced usage)
|
|
1433
837
|
*/
|
|
1434
838
|
getSnapDetector() {
|
|
1435
|
-
return this.
|
|
839
|
+
return this.raycastEngine.getSnapDetector();
|
|
1436
840
|
}
|
|
1437
841
|
/**
|
|
1438
842
|
* Clear all caches (call when geometry changes)
|
|
1439
843
|
*/
|
|
1440
844
|
clearCaches() {
|
|
1441
|
-
this.
|
|
1442
|
-
this.snapDetector.clearCache();
|
|
845
|
+
this.raycastEngine.clearCaches();
|
|
1443
846
|
}
|
|
1444
847
|
/**
|
|
1445
848
|
* Resize canvas
|
|
@@ -1469,12 +872,13 @@ export class Renderer {
|
|
|
1469
872
|
// minVal = options.sectionPlane.min ?? boundsMin[axisIdx]
|
|
1470
873
|
// maxVal = options.sectionPlane.max ?? boundsMax[axisIdx]
|
|
1471
874
|
const axisIdx = axis === 'side' ? 'x' : axis === 'down' ? 'y' : 'z';
|
|
875
|
+
const modelBounds = this.geometryManager.getModelBounds();
|
|
1472
876
|
// Allow upload if either sectionRange has both values, or modelBounds exists as fallback
|
|
1473
877
|
const hasFullRange = sectionRange?.min !== undefined && sectionRange?.max !== undefined;
|
|
1474
|
-
if (!hasFullRange && !
|
|
878
|
+
if (!hasFullRange && !modelBounds)
|
|
1475
879
|
return;
|
|
1476
|
-
const minVal = sectionRange?.min ??
|
|
1477
|
-
const maxVal = sectionRange?.max ??
|
|
880
|
+
const minVal = sectionRange?.min ?? modelBounds.min[axisIdx];
|
|
881
|
+
const maxVal = sectionRange?.max ?? modelBounds.max[axisIdx];
|
|
1478
882
|
const planePosition = minVal + (position / 100) * (maxVal - minVal);
|
|
1479
883
|
this.section2DOverlayRenderer.uploadDrawing(polygons, lines, axis, planePosition, flipped);
|
|
1480
884
|
}
|