@ifc-lite/renderer 1.6.1 → 1.7.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 +50 -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/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 +8 -1
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +59 -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
|
|
@@ -872,7 +581,7 @@ export class Renderer {
|
|
|
872
581
|
const meshKey = `${selId}:${selectedModelIndex ?? 'any'}`;
|
|
873
582
|
if (!existingMeshKeys.has(meshKey) && this.scene.hasMeshData(selId, selectedModelIndex)) {
|
|
874
583
|
const meshData = this.scene.getMeshData(selId, selectedModelIndex);
|
|
875
|
-
this.createMeshFromData(meshData);
|
|
584
|
+
this.geometryManager.createMeshFromData(meshData);
|
|
876
585
|
existingMeshKeys.add(meshKey);
|
|
877
586
|
}
|
|
878
587
|
}
|
|
@@ -1030,11 +739,12 @@ export class Renderer {
|
|
|
1030
739
|
}
|
|
1031
740
|
// Draw section plane visual BEFORE pass.end() (within same MSAA render pass)
|
|
1032
741
|
// Always show plane when sectionPlane options are provided (as preview or active)
|
|
1033
|
-
|
|
742
|
+
const modelBounds = this.geometryManager.getModelBounds();
|
|
743
|
+
if (options.sectionPlane && this.sectionPlaneRenderer && modelBounds) {
|
|
1034
744
|
this.sectionPlaneRenderer.draw(pass, {
|
|
1035
745
|
axis: options.sectionPlane.axis,
|
|
1036
746
|
position: options.sectionPlane.position,
|
|
1037
|
-
bounds:
|
|
747
|
+
bounds: modelBounds,
|
|
1038
748
|
viewProj,
|
|
1039
749
|
isPreview: !options.sectionPlane.enabled, // Preview mode when not enabled
|
|
1040
750
|
min: options.sectionPlane.min,
|
|
@@ -1045,7 +755,7 @@ export class Renderer {
|
|
|
1045
755
|
this.section2DOverlayRenderer.draw(pass, {
|
|
1046
756
|
axis: options.sectionPlane.axis,
|
|
1047
757
|
position: options.sectionPlane.position,
|
|
1048
|
-
bounds:
|
|
758
|
+
bounds: modelBounds,
|
|
1049
759
|
viewProj,
|
|
1050
760
|
min: options.sectionPlane.min,
|
|
1051
761
|
max: options.sectionPlane.max,
|
|
@@ -1076,112 +786,7 @@ export class Renderer {
|
|
|
1076
786
|
* These are scaled internally to match the actual canvas pixel dimensions.
|
|
1077
787
|
*/
|
|
1078
788
|
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;
|
|
789
|
+
return this.pickingManager.pick(x, y, options);
|
|
1185
790
|
}
|
|
1186
791
|
/**
|
|
1187
792
|
* Raycast into the scene to get precise 3D intersection point
|
|
@@ -1191,103 +796,7 @@ export class Renderer {
|
|
|
1191
796
|
* These are scaled internally to match the actual canvas pixel dimensions.
|
|
1192
797
|
*/
|
|
1193
798
|
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
|
-
}
|
|
799
|
+
return this.raycastEngine.raycastScene(x, y, options);
|
|
1291
800
|
}
|
|
1292
801
|
/**
|
|
1293
802
|
* Raycast with magnetic edge snapping behavior
|
|
@@ -1297,149 +806,31 @@ export class Renderer {
|
|
|
1297
806
|
* These are scaled internally to match the actual canvas pixel dimensions.
|
|
1298
807
|
*/
|
|
1299
808
|
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
|
-
}
|
|
809
|
+
return this.raycastEngine.raycastSceneMagnetic(x, y, currentEdgeLock, options);
|
|
1418
810
|
}
|
|
1419
811
|
/**
|
|
1420
812
|
* Invalidate BVH cache (call when geometry changes)
|
|
1421
813
|
*/
|
|
1422
814
|
invalidateBVHCache() {
|
|
1423
|
-
this.
|
|
815
|
+
this.raycastEngine.invalidateBVHCache();
|
|
1424
816
|
}
|
|
1425
817
|
/**
|
|
1426
818
|
* Get the raycaster instance (for advanced usage)
|
|
1427
819
|
*/
|
|
1428
820
|
getRaycaster() {
|
|
1429
|
-
return this.
|
|
821
|
+
return this.raycastEngine.getRaycaster();
|
|
1430
822
|
}
|
|
1431
823
|
/**
|
|
1432
824
|
* Get the snap detector instance (for advanced usage)
|
|
1433
825
|
*/
|
|
1434
826
|
getSnapDetector() {
|
|
1435
|
-
return this.
|
|
827
|
+
return this.raycastEngine.getSnapDetector();
|
|
1436
828
|
}
|
|
1437
829
|
/**
|
|
1438
830
|
* Clear all caches (call when geometry changes)
|
|
1439
831
|
*/
|
|
1440
832
|
clearCaches() {
|
|
1441
|
-
this.
|
|
1442
|
-
this.snapDetector.clearCache();
|
|
833
|
+
this.raycastEngine.clearCaches();
|
|
1443
834
|
}
|
|
1444
835
|
/**
|
|
1445
836
|
* Resize canvas
|
|
@@ -1469,12 +860,13 @@ export class Renderer {
|
|
|
1469
860
|
// minVal = options.sectionPlane.min ?? boundsMin[axisIdx]
|
|
1470
861
|
// maxVal = options.sectionPlane.max ?? boundsMax[axisIdx]
|
|
1471
862
|
const axisIdx = axis === 'side' ? 'x' : axis === 'down' ? 'y' : 'z';
|
|
863
|
+
const modelBounds = this.geometryManager.getModelBounds();
|
|
1472
864
|
// Allow upload if either sectionRange has both values, or modelBounds exists as fallback
|
|
1473
865
|
const hasFullRange = sectionRange?.min !== undefined && sectionRange?.max !== undefined;
|
|
1474
|
-
if (!hasFullRange && !
|
|
866
|
+
if (!hasFullRange && !modelBounds)
|
|
1475
867
|
return;
|
|
1476
|
-
const minVal = sectionRange?.min ??
|
|
1477
|
-
const maxVal = sectionRange?.max ??
|
|
868
|
+
const minVal = sectionRange?.min ?? modelBounds.min[axisIdx];
|
|
869
|
+
const maxVal = sectionRange?.max ?? modelBounds.max[axisIdx];
|
|
1478
870
|
const planePosition = minVal + (position / 100) * (maxVal - minVal);
|
|
1479
871
|
this.section2DOverlayRenderer.uploadDrawing(polygons, lines, axis, planePosition, flipped);
|
|
1480
872
|
}
|