@ifc-lite/geometry 1.16.2 → 1.16.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coordinate-handler.d.ts +9 -0
- package/dist/coordinate-handler.d.ts.map +1 -1
- package/dist/coordinate-handler.js +32 -30
- package/dist/coordinate-handler.js.map +1 -1
- package/dist/ifc-lite-mesh-collector.js.map +1 -1
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +453 -34
- package/dist/index.js.map +1 -1
- package/dist/native-bridge.d.ts +9 -2
- package/dist/native-bridge.d.ts.map +1 -1
- package/dist/native-bridge.js +490 -16
- package/dist/native-bridge.js.map +1 -1
- package/dist/platform-bridge.d.ts +63 -4
- package/dist/platform-bridge.d.ts.map +1 -1
- package/dist/platform-bridge.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/wasm-bridge.d.ts +3 -2
- package/dist/wasm-bridge.d.ts.map +1 -1
- package/dist/wasm-bridge.js +9 -2
- package/dist/wasm-bridge.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -42,16 +42,32 @@ export function calculateDynamicBatchSize(batchNumber, initialBatchSize = 50, ma
|
|
|
42
42
|
return maxBatchSize; // Full throughput earlier
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
const MAX_NATIVE_STREAM_QUEUE_EVENTS = 8;
|
|
46
|
+
const MAX_NATIVE_STREAM_QUEUE_MESHES = 32768;
|
|
47
|
+
const MAX_NATIVE_STREAM_EVENTS_PER_TURN = 4;
|
|
48
|
+
const MAX_NATIVE_STREAM_MESHES_PER_TURN = 8192;
|
|
49
|
+
const MAX_NATIVE_STREAM_DRAIN_MS = 10;
|
|
50
|
+
function yieldToEventLoop() {
|
|
51
|
+
const maybeScheduler = globalThis.scheduler;
|
|
52
|
+
if (typeof maybeScheduler?.yield === 'function') {
|
|
53
|
+
return maybeScheduler.yield();
|
|
54
|
+
}
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
globalThis.setTimeout(resolve, 0);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
45
59
|
export class GeometryProcessor {
|
|
60
|
+
static largeFileByteStreamingThreshold = 256 * 1024 * 1024;
|
|
46
61
|
bridge = null;
|
|
47
62
|
platformBridge = null;
|
|
48
63
|
bufferBuilder;
|
|
49
64
|
coordinateHandler;
|
|
50
65
|
isNative = false;
|
|
66
|
+
lastNativeStats = null;
|
|
51
67
|
constructor(options = {}) {
|
|
52
68
|
this.bufferBuilder = new BufferBuilder();
|
|
53
69
|
this.coordinateHandler = new CoordinateHandler();
|
|
54
|
-
this.isNative = isTauri();
|
|
70
|
+
this.isNative = options.preferNative !== false && isTauri();
|
|
55
71
|
// Note: options accepted for API compatibility
|
|
56
72
|
void options.quality;
|
|
57
73
|
if (!this.isNative) {
|
|
@@ -88,9 +104,7 @@ export class GeometryProcessor {
|
|
|
88
104
|
if (this.isNative && this.platformBridge) {
|
|
89
105
|
// NATIVE PATH - Use Tauri commands
|
|
90
106
|
console.time('[GeometryProcessor] native-processing');
|
|
91
|
-
const
|
|
92
|
-
const content = decoder.decode(buffer);
|
|
93
|
-
const result = await this.platformBridge.processGeometry(content);
|
|
107
|
+
const result = await this.platformBridge.processGeometry(buffer);
|
|
94
108
|
meshes = result.meshes;
|
|
95
109
|
console.timeEnd('[GeometryProcessor] native-processing');
|
|
96
110
|
}
|
|
@@ -132,6 +146,30 @@ export class GeometryProcessor {
|
|
|
132
146
|
};
|
|
133
147
|
return result;
|
|
134
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Process IFC geometry directly from a filesystem path in native desktop
|
|
151
|
+
* hosts. This avoids copying IFC content through JS when the host already
|
|
152
|
+
* has the file path.
|
|
153
|
+
*/
|
|
154
|
+
async processPath(path) {
|
|
155
|
+
if (!this.isNative) {
|
|
156
|
+
throw new Error('Path-based geometry processing is only available in native desktop builds');
|
|
157
|
+
}
|
|
158
|
+
if (!this.platformBridge) {
|
|
159
|
+
await this.init();
|
|
160
|
+
}
|
|
161
|
+
if (!this.platformBridge?.processGeometryPath) {
|
|
162
|
+
throw new Error('Native platform bridge does not support file-path geometry processing');
|
|
163
|
+
}
|
|
164
|
+
const result = await this.platformBridge.processGeometryPath(path);
|
|
165
|
+
const coordinateInfo = this.coordinateHandler.processMeshes(result.meshes);
|
|
166
|
+
return {
|
|
167
|
+
meshes: result.meshes,
|
|
168
|
+
totalTriangles: result.totalTriangles,
|
|
169
|
+
totalVertices: result.totalVertices,
|
|
170
|
+
coordinateInfo,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
135
173
|
/**
|
|
136
174
|
* Collect meshes on main thread using IFC-Lite WASM
|
|
137
175
|
*/
|
|
@@ -147,6 +185,182 @@ export class GeometryProcessor {
|
|
|
147
185
|
const buildingRotation = collector.getBuildingRotation();
|
|
148
186
|
return { meshes, buildingRotation };
|
|
149
187
|
}
|
|
188
|
+
getStreamingBatchSize(buffer, batchConfig) {
|
|
189
|
+
if (typeof batchConfig === 'number') {
|
|
190
|
+
return batchConfig;
|
|
191
|
+
}
|
|
192
|
+
const fileSizeMB = batchConfig.fileSizeMB
|
|
193
|
+
? batchConfig.fileSizeMB
|
|
194
|
+
: buffer.length / (1024 * 1024);
|
|
195
|
+
return fileSizeMB < 10 ? 100
|
|
196
|
+
: fileSizeMB < 50 ? 200
|
|
197
|
+
: fileSizeMB < 100 ? 300
|
|
198
|
+
: fileSizeMB < 300 ? 500
|
|
199
|
+
: fileSizeMB < 500 ? 1500
|
|
200
|
+
: 3000;
|
|
201
|
+
}
|
|
202
|
+
convertMeshCollectionToBatch(collection) {
|
|
203
|
+
const batch = [];
|
|
204
|
+
try {
|
|
205
|
+
for (let i = 0; i < collection.length; i++) {
|
|
206
|
+
const mesh = collection.get(i);
|
|
207
|
+
if (!mesh)
|
|
208
|
+
continue;
|
|
209
|
+
try {
|
|
210
|
+
batch.push({
|
|
211
|
+
expressId: mesh.expressId,
|
|
212
|
+
ifcType: mesh.ifcType,
|
|
213
|
+
positions: mesh.positions,
|
|
214
|
+
normals: mesh.normals,
|
|
215
|
+
indices: mesh.indices,
|
|
216
|
+
color: [mesh.color[0], mesh.color[1], mesh.color[2], mesh.color[3]],
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
mesh.free();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
collection.free();
|
|
226
|
+
}
|
|
227
|
+
return batch;
|
|
228
|
+
}
|
|
229
|
+
withBuildingRotation(coordinateInfo, buildingRotation) {
|
|
230
|
+
return buildingRotation !== undefined
|
|
231
|
+
? { ...coordinateInfo, buildingRotation }
|
|
232
|
+
: coordinateInfo;
|
|
233
|
+
}
|
|
234
|
+
convertInstancedCollectionToBatch(collection) {
|
|
235
|
+
const batch = [];
|
|
236
|
+
try {
|
|
237
|
+
for (let i = 0; i < collection.length; i++) {
|
|
238
|
+
const geometry = collection.get(i);
|
|
239
|
+
if (geometry) {
|
|
240
|
+
batch.push(geometry);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
collection.free();
|
|
246
|
+
}
|
|
247
|
+
return batch;
|
|
248
|
+
}
|
|
249
|
+
async *processStreamingBytes(buffer, batchConfig) {
|
|
250
|
+
if (!this.bridge) {
|
|
251
|
+
throw new Error('WASM bridge not initialized');
|
|
252
|
+
}
|
|
253
|
+
const api = this.bridge.getApi();
|
|
254
|
+
const prePass = api.buildPrePassOnce(buffer);
|
|
255
|
+
yield { type: 'model-open', modelID: 0 };
|
|
256
|
+
if (prePass.rtcOffset) {
|
|
257
|
+
yield {
|
|
258
|
+
type: 'rtcOffset',
|
|
259
|
+
rtcOffset: {
|
|
260
|
+
x: prePass.rtcOffset[0] ?? 0,
|
|
261
|
+
y: prePass.rtcOffset[1] ?? 0,
|
|
262
|
+
z: prePass.rtcOffset[2] ?? 0,
|
|
263
|
+
},
|
|
264
|
+
hasRtc: Boolean(prePass.needsShift),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const buildingRotation = prePass.buildingRotation ?? undefined;
|
|
268
|
+
if (!prePass.jobs || prePass.totalJobs === 0) {
|
|
269
|
+
const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
|
|
270
|
+
yield { type: 'complete', totalMeshes: 0, coordinateInfo };
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const batchSize = this.getStreamingBatchSize(buffer, batchConfig);
|
|
274
|
+
let totalMeshes = 0;
|
|
275
|
+
for (let startJob = 0; startJob < prePass.totalJobs; startJob += batchSize) {
|
|
276
|
+
const endJob = Math.min(startJob + batchSize, prePass.totalJobs);
|
|
277
|
+
const jobSlice = prePass.jobs.slice(startJob * 3, endJob * 3);
|
|
278
|
+
const collection = api.processGeometryBatch(buffer, jobSlice, prePass.unitScale, prePass.rtcOffset?.[0] ?? 0, prePass.rtcOffset?.[1] ?? 0, prePass.rtcOffset?.[2] ?? 0, prePass.needsShift, prePass.voidKeys, prePass.voidCounts, prePass.voidValues, prePass.styleIds, prePass.styleColors);
|
|
279
|
+
const batch = this.convertMeshCollectionToBatch(collection);
|
|
280
|
+
if (batch.length === 0) {
|
|
281
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
this.coordinateHandler.processMeshesIncremental(batch);
|
|
285
|
+
totalMeshes += batch.length;
|
|
286
|
+
const currentCoordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
|
|
287
|
+
const coordinateInfo = currentCoordinateInfo
|
|
288
|
+
? this.withBuildingRotation(currentCoordinateInfo, buildingRotation)
|
|
289
|
+
: null;
|
|
290
|
+
yield {
|
|
291
|
+
type: 'batch',
|
|
292
|
+
meshes: batch,
|
|
293
|
+
totalSoFar: totalMeshes,
|
|
294
|
+
coordinateInfo: coordinateInfo || undefined,
|
|
295
|
+
};
|
|
296
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
297
|
+
}
|
|
298
|
+
const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
|
|
299
|
+
yield { type: 'complete', totalMeshes, coordinateInfo };
|
|
300
|
+
}
|
|
301
|
+
async *processInstancedStreamingBytes(buffer, batchSize) {
|
|
302
|
+
if (!this.bridge) {
|
|
303
|
+
throw new Error('WASM bridge not initialized');
|
|
304
|
+
}
|
|
305
|
+
const api = this.bridge.getApi();
|
|
306
|
+
const prePass = api.buildPrePassOnce(buffer);
|
|
307
|
+
const buildingRotation = prePass.buildingRotation ?? undefined;
|
|
308
|
+
yield { type: 'model-open', modelID: 0 };
|
|
309
|
+
if (!prePass.jobs || prePass.totalJobs === 0) {
|
|
310
|
+
const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
|
|
311
|
+
yield { type: 'complete', totalGeometries: 0, totalInstances: 0, coordinateInfo };
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
let totalGeometries = 0;
|
|
315
|
+
let totalInstances = 0;
|
|
316
|
+
for (let startJob = 0; startJob < prePass.totalJobs; startJob += batchSize) {
|
|
317
|
+
const endJob = Math.min(startJob + batchSize, prePass.totalJobs);
|
|
318
|
+
const jobSlice = prePass.jobs.slice(startJob * 3, endJob * 3);
|
|
319
|
+
const collection = api.processInstancedGeometryBatch(buffer, jobSlice, prePass.unitScale, prePass.rtcOffset?.[0] ?? 0, prePass.rtcOffset?.[1] ?? 0, prePass.rtcOffset?.[2] ?? 0, prePass.needsShift, prePass.styleIds, prePass.styleColors);
|
|
320
|
+
const batch = this.convertInstancedCollectionToBatch(collection);
|
|
321
|
+
if (batch.length === 0) {
|
|
322
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const meshDataBatch = [];
|
|
326
|
+
for (const geom of batch) {
|
|
327
|
+
const positions = geom.positions;
|
|
328
|
+
const normals = geom.normals;
|
|
329
|
+
const indices = geom.indices;
|
|
330
|
+
if (geom.instance_count > 0) {
|
|
331
|
+
const firstInstance = geom.get_instance(0);
|
|
332
|
+
if (firstInstance) {
|
|
333
|
+
const color = firstInstance.color;
|
|
334
|
+
meshDataBatch.push({
|
|
335
|
+
expressId: firstInstance.expressId,
|
|
336
|
+
positions,
|
|
337
|
+
normals,
|
|
338
|
+
indices,
|
|
339
|
+
color: [color[0], color[1], color[2], color[3]],
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (meshDataBatch.length > 0) {
|
|
345
|
+
this.coordinateHandler.processMeshesIncremental(meshDataBatch);
|
|
346
|
+
}
|
|
347
|
+
totalGeometries += batch.length;
|
|
348
|
+
totalInstances += batch.reduce((sum, geometry) => sum + geometry.instance_count, 0);
|
|
349
|
+
const currentCoordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
|
|
350
|
+
const coordinateInfo = currentCoordinateInfo
|
|
351
|
+
? this.withBuildingRotation(currentCoordinateInfo, buildingRotation)
|
|
352
|
+
: null;
|
|
353
|
+
yield {
|
|
354
|
+
type: 'batch',
|
|
355
|
+
geometries: batch,
|
|
356
|
+
totalSoFar: totalGeometries,
|
|
357
|
+
coordinateInfo: coordinateInfo || undefined,
|
|
358
|
+
};
|
|
359
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
360
|
+
}
|
|
361
|
+
const coordinateInfo = this.withBuildingRotation(this.coordinateHandler.getFinalCoordinateInfo(), buildingRotation);
|
|
362
|
+
yield { type: 'complete', totalGeometries, totalInstances, coordinateInfo };
|
|
363
|
+
}
|
|
150
364
|
/**
|
|
151
365
|
* Process IFC file with streaming output for progressive rendering
|
|
152
366
|
* Uses native Rust in Tauri, WASM in browser
|
|
@@ -168,23 +382,75 @@ export class GeometryProcessor {
|
|
|
168
382
|
this.coordinateHandler.reset();
|
|
169
383
|
// Yield start event FIRST so UI can update before heavy processing
|
|
170
384
|
yield { type: 'start', totalEstimate: buffer.length / 1000 };
|
|
171
|
-
// Yield to main thread before heavy
|
|
385
|
+
// Yield to main thread before heavy processing begins
|
|
172
386
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
173
|
-
// Convert buffer to string (IFC files are text)
|
|
174
|
-
const decoder = new TextDecoder();
|
|
175
|
-
const content = decoder.decode(buffer);
|
|
176
|
-
yield { type: 'model-open', modelID: 0 };
|
|
177
387
|
if (this.isNative && this.platformBridge) {
|
|
388
|
+
yield { type: 'model-open', modelID: 0 };
|
|
178
389
|
// NATIVE PATH - Use Tauri streaming
|
|
179
390
|
console.time('[GeometryProcessor] native-streaming');
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
391
|
+
const queuedEvents = [];
|
|
392
|
+
let resolvePending = null;
|
|
393
|
+
let completed = false;
|
|
394
|
+
let streamError = null;
|
|
395
|
+
let completedTotalMeshes;
|
|
396
|
+
let totalMeshes = 0;
|
|
397
|
+
const wake = () => {
|
|
398
|
+
if (resolvePending) {
|
|
399
|
+
resolvePending();
|
|
400
|
+
resolvePending = null;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
const streamingPromise = this.platformBridge.processGeometryStreaming(buffer, {
|
|
404
|
+
onBatch: (batch) => {
|
|
405
|
+
queuedEvents.push({ type: 'batch', meshes: batch.meshes, nativeTelemetry: batch.nativeTelemetry });
|
|
406
|
+
wake();
|
|
407
|
+
},
|
|
408
|
+
onColorUpdate: (updates) => {
|
|
409
|
+
queuedEvents.push({ type: 'colorUpdate', updates: new Map(updates) });
|
|
410
|
+
wake();
|
|
411
|
+
},
|
|
412
|
+
onComplete: (stats) => {
|
|
413
|
+
this.lastNativeStats = stats;
|
|
414
|
+
completedTotalMeshes = stats.totalMeshes;
|
|
415
|
+
completed = true;
|
|
416
|
+
wake();
|
|
417
|
+
},
|
|
418
|
+
onError: (error) => {
|
|
419
|
+
streamError = error;
|
|
420
|
+
completed = true;
|
|
421
|
+
wake();
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
while (!completed || queuedEvents.length > 0) {
|
|
425
|
+
while (queuedEvents.length > 0) {
|
|
426
|
+
const event = queuedEvents.shift();
|
|
427
|
+
if (event.type === 'colorUpdate') {
|
|
428
|
+
yield { type: 'colorUpdate', updates: event.updates };
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
this.coordinateHandler.processMeshesIncremental(event.meshes);
|
|
432
|
+
totalMeshes += event.meshes.length;
|
|
433
|
+
const coordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
|
|
434
|
+
yield {
|
|
435
|
+
type: 'batch',
|
|
436
|
+
meshes: event.meshes,
|
|
437
|
+
totalSoFar: totalMeshes,
|
|
438
|
+
coordinateInfo: coordinateInfo || undefined,
|
|
439
|
+
nativeTelemetry: event.nativeTelemetry,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (streamError) {
|
|
443
|
+
throw streamError;
|
|
444
|
+
}
|
|
445
|
+
if (!completed) {
|
|
446
|
+
await new Promise((resolve) => {
|
|
447
|
+
resolvePending = resolve;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
await streamingPromise;
|
|
185
452
|
const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
|
|
186
|
-
yield { type: '
|
|
187
|
-
yield { type: 'complete', totalMeshes, coordinateInfo };
|
|
453
|
+
yield { type: 'complete', totalMeshes: completedTotalMeshes ?? totalMeshes, coordinateInfo };
|
|
188
454
|
console.timeEnd('[GeometryProcessor] native-streaming');
|
|
189
455
|
}
|
|
190
456
|
else {
|
|
@@ -192,19 +458,18 @@ export class GeometryProcessor {
|
|
|
192
458
|
if (!this.bridge) {
|
|
193
459
|
throw new Error('WASM bridge not initialized');
|
|
194
460
|
}
|
|
461
|
+
if (buffer.length >= GeometryProcessor.largeFileByteStreamingThreshold) {
|
|
462
|
+
yield* this.processStreamingBytes(buffer, batchConfig);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// Convert buffer to string (IFC files are text)
|
|
466
|
+
const decoder = new TextDecoder();
|
|
467
|
+
const content = decoder.decode(buffer);
|
|
468
|
+
yield { type: 'model-open', modelID: 0 };
|
|
195
469
|
const collector = new IfcLiteMeshCollector(this.bridge.getApi(), content);
|
|
196
470
|
let totalMeshes = 0;
|
|
197
471
|
let extractedBuildingRotation = undefined;
|
|
198
|
-
|
|
199
|
-
// Larger batches = fewer callbacks = faster processing
|
|
200
|
-
const fileSizeMB = typeof batchConfig !== 'number' && batchConfig.fileSizeMB
|
|
201
|
-
? batchConfig.fileSizeMB
|
|
202
|
-
: buffer.length / (1024 * 1024);
|
|
203
|
-
// Use WASM batches directly - no JS accumulation layer
|
|
204
|
-
// WASM already prioritizes simple geometry (walls, slabs) for fast first frame
|
|
205
|
-
// PERF: Larger batches dramatically reduce WASM↔JS boundary crossing overhead.
|
|
206
|
-
// 487MB file: batch 500→1500 cut WASM wait from 79s to 39s.
|
|
207
|
-
const wasmBatchSize = fileSizeMB < 10 ? 100 : fileSizeMB < 50 ? 200 : fileSizeMB < 100 ? 300 : fileSizeMB < 300 ? 500 : fileSizeMB < 500 ? 1500 : 3000;
|
|
472
|
+
const wasmBatchSize = this.getStreamingBatchSize(buffer, batchConfig);
|
|
208
473
|
// Use WASM batches directly for maximum throughput
|
|
209
474
|
for await (const item of collector.collectMeshesStreaming(wasmBatchSize)) {
|
|
210
475
|
// Handle color update events
|
|
@@ -239,6 +504,34 @@ export class GeometryProcessor {
|
|
|
239
504
|
yield { type: 'complete', totalMeshes, coordinateInfo: finalCoordinateInfo };
|
|
240
505
|
}
|
|
241
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Stream geometry directly from a filesystem path in native desktop hosts.
|
|
509
|
+
* This avoids copying very large IFC files through JS and Tauri IPC.
|
|
510
|
+
*/
|
|
511
|
+
async *processStreamingPath(path, estimatedBytes = 0, cacheKey) {
|
|
512
|
+
if (!this.isNative) {
|
|
513
|
+
throw new Error('File-path geometry streaming is only available in native desktop builds');
|
|
514
|
+
}
|
|
515
|
+
if (!this.platformBridge) {
|
|
516
|
+
await this.init();
|
|
517
|
+
}
|
|
518
|
+
if (!this.platformBridge?.processGeometryStreamingPath) {
|
|
519
|
+
throw new Error('Native platform bridge does not support file-path streaming');
|
|
520
|
+
}
|
|
521
|
+
yield* this.streamNativeGeometry((options) => this.platformBridge.processGeometryStreamingPath(path, options, cacheKey), estimatedBytes > 0 ? estimatedBytes / 1000 : 0);
|
|
522
|
+
}
|
|
523
|
+
async *processStreamingCache(cacheKey) {
|
|
524
|
+
if (!this.isNative) {
|
|
525
|
+
throw new Error('Native cached geometry streaming is only available in native desktop builds');
|
|
526
|
+
}
|
|
527
|
+
if (!this.platformBridge) {
|
|
528
|
+
await this.init();
|
|
529
|
+
}
|
|
530
|
+
if (!this.platformBridge?.processGeometryStreamingCache) {
|
|
531
|
+
throw new Error('Native platform bridge does not support cached geometry streaming');
|
|
532
|
+
}
|
|
533
|
+
yield* this.streamNativeGeometry((options) => this.platformBridge.processGeometryStreamingCache(cacheKey, options), 0);
|
|
534
|
+
}
|
|
242
535
|
/**
|
|
243
536
|
* Process IFC file with streaming instanced geometry output for progressive rendering
|
|
244
537
|
* Groups identical geometries by hash (before transformation) for GPU instancing
|
|
@@ -261,6 +554,15 @@ export class GeometryProcessor {
|
|
|
261
554
|
// Reset coordinate handler for new file
|
|
262
555
|
this.coordinateHandler.reset();
|
|
263
556
|
yield { type: 'start', totalEstimate: buffer.length / 1000 };
|
|
557
|
+
// Adapt batch size for large files to reduce callback overhead
|
|
558
|
+
// Larger batches = fewer callbacks = less overhead for huge models
|
|
559
|
+
const fileSizeMB = buffer.length / (1024 * 1024);
|
|
560
|
+
const effectiveBatchSize = fileSizeMB < 50 ? batchSize : fileSizeMB < 200 ? Math.max(batchSize, 50) : fileSizeMB < 300 ? Math.max(batchSize, 100) : Math.max(batchSize, 200);
|
|
561
|
+
const byteBatchSize = Math.max(effectiveBatchSize, this.getStreamingBatchSize(buffer, batchSize));
|
|
562
|
+
if (buffer.length >= GeometryProcessor.largeFileByteStreamingThreshold) {
|
|
563
|
+
yield* this.processInstancedStreamingBytes(buffer, byteBatchSize);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
264
566
|
// Convert buffer to string (IFC files are text)
|
|
265
567
|
const decoder = new TextDecoder();
|
|
266
568
|
const content = decoder.decode(buffer);
|
|
@@ -269,10 +571,6 @@ export class GeometryProcessor {
|
|
|
269
571
|
const collector = new IfcLiteMeshCollector(this.bridge.getApi(), content);
|
|
270
572
|
let totalGeometries = 0;
|
|
271
573
|
let totalInstances = 0;
|
|
272
|
-
// Adapt batch size for large files to reduce callback overhead
|
|
273
|
-
// Larger batches = fewer callbacks = less overhead for huge models
|
|
274
|
-
const fileSizeMB = buffer.length / (1024 * 1024);
|
|
275
|
-
const effectiveBatchSize = fileSizeMB < 50 ? batchSize : fileSizeMB < 200 ? Math.max(batchSize, 50) : fileSizeMB < 300 ? Math.max(batchSize, 100) : Math.max(batchSize, 200);
|
|
276
574
|
for await (const batch of collector.collectInstancedGeometryStreaming(effectiveBatchSize)) {
|
|
277
575
|
// For instanced geometry, we need to extract mesh data from instances for coordinate handling
|
|
278
576
|
// Convert InstancedGeometry to MeshData[] for coordinate handler
|
|
@@ -526,20 +824,19 @@ export class GeometryProcessor {
|
|
|
526
824
|
// Small files: Load all at once (sync)
|
|
527
825
|
if (buffer.length < sizeThreshold) {
|
|
528
826
|
yield { type: 'start', totalEstimate: buffer.length / 1000 };
|
|
529
|
-
// Convert buffer to string (IFC files are text)
|
|
530
|
-
const decoder = new TextDecoder();
|
|
531
|
-
const content = decoder.decode(buffer);
|
|
532
827
|
yield { type: 'model-open', modelID: 0 };
|
|
533
828
|
let allMeshes;
|
|
534
829
|
if (this.isNative && this.platformBridge) {
|
|
535
830
|
// NATIVE PATH - single batch processing
|
|
536
831
|
console.time('[GeometryProcessor] native-adaptive-sync');
|
|
537
|
-
const result = await this.platformBridge.processGeometry(
|
|
832
|
+
const result = await this.platformBridge.processGeometry(buffer);
|
|
538
833
|
allMeshes = result.meshes;
|
|
539
834
|
console.timeEnd('[GeometryProcessor] native-adaptive-sync');
|
|
540
835
|
}
|
|
541
836
|
else {
|
|
542
837
|
// WASM PATH
|
|
838
|
+
const decoder = new TextDecoder();
|
|
839
|
+
const content = decoder.decode(buffer);
|
|
543
840
|
const collector = new IfcLiteMeshCollector(this.bridge.getApi(), content);
|
|
544
841
|
allMeshes = collector.collectMeshes();
|
|
545
842
|
}
|
|
@@ -578,6 +875,128 @@ export class GeometryProcessor {
|
|
|
578
875
|
}
|
|
579
876
|
return this.bridge.getApi();
|
|
580
877
|
}
|
|
878
|
+
getLastNativeStats() {
|
|
879
|
+
return this.lastNativeStats;
|
|
880
|
+
}
|
|
881
|
+
enqueueNativeStreamingEvent(queuedEvents, event, queueState) {
|
|
882
|
+
if (event.type === 'colorUpdate') {
|
|
883
|
+
const lastEvent = queuedEvents[queuedEvents.length - 1];
|
|
884
|
+
if (lastEvent?.type === 'colorUpdate') {
|
|
885
|
+
for (const [expressId, color] of event.updates) {
|
|
886
|
+
lastEvent.updates.set(expressId, color);
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
queuedEvents.push(event);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const lastEvent = queuedEvents[queuedEvents.length - 1];
|
|
894
|
+
const shouldCoalesce = lastEvent?.type === 'batch' &&
|
|
895
|
+
(queuedEvents.length >= MAX_NATIVE_STREAM_QUEUE_EVENTS || queueState.queuedMeshes >= MAX_NATIVE_STREAM_QUEUE_MESHES);
|
|
896
|
+
if (shouldCoalesce) {
|
|
897
|
+
for (let i = 0; i < event.meshes.length; i++) {
|
|
898
|
+
lastEvent.meshes.push(event.meshes[i]);
|
|
899
|
+
}
|
|
900
|
+
lastEvent.nativeTelemetry = event.nativeTelemetry;
|
|
901
|
+
queueState.coalescedBatchCount += 1;
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
queuedEvents.push(event);
|
|
905
|
+
}
|
|
906
|
+
queueState.queuedMeshes += event.meshes.length;
|
|
907
|
+
}
|
|
908
|
+
async *streamNativeGeometry(startStream, totalEstimate) {
|
|
909
|
+
this.coordinateHandler.reset();
|
|
910
|
+
yield { type: 'start', totalEstimate };
|
|
911
|
+
await yieldToEventLoop();
|
|
912
|
+
yield { type: 'model-open', modelID: 0 };
|
|
913
|
+
const queuedEvents = [];
|
|
914
|
+
const queueState = { queuedMeshes: 0, coalescedBatchCount: 0 };
|
|
915
|
+
let resolvePending = null;
|
|
916
|
+
let completed = false;
|
|
917
|
+
let streamError = null;
|
|
918
|
+
let completedTotalMeshes;
|
|
919
|
+
let totalMeshes = 0;
|
|
920
|
+
const wake = () => {
|
|
921
|
+
if (resolvePending) {
|
|
922
|
+
resolvePending();
|
|
923
|
+
resolvePending = null;
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
const streamingPromise = startStream({
|
|
927
|
+
onBatch: (batch) => {
|
|
928
|
+
this.enqueueNativeStreamingEvent(queuedEvents, { type: 'batch', meshes: batch.meshes, nativeTelemetry: batch.nativeTelemetry }, queueState);
|
|
929
|
+
wake();
|
|
930
|
+
},
|
|
931
|
+
onColorUpdate: (updates) => {
|
|
932
|
+
this.enqueueNativeStreamingEvent(queuedEvents, { type: 'colorUpdate', updates: new Map(updates) }, queueState);
|
|
933
|
+
wake();
|
|
934
|
+
},
|
|
935
|
+
onComplete: (stats) => {
|
|
936
|
+
this.lastNativeStats = stats;
|
|
937
|
+
completedTotalMeshes = stats.totalMeshes;
|
|
938
|
+
completed = true;
|
|
939
|
+
wake();
|
|
940
|
+
},
|
|
941
|
+
onError: (error) => {
|
|
942
|
+
streamError = error;
|
|
943
|
+
completed = true;
|
|
944
|
+
wake();
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
while (!completed || queuedEvents.length > 0) {
|
|
948
|
+
let drainedEventCount = 0;
|
|
949
|
+
let drainedMeshCount = 0;
|
|
950
|
+
let drainStartedAt = performance.now();
|
|
951
|
+
while (queuedEvents.length > 0) {
|
|
952
|
+
const event = queuedEvents.shift();
|
|
953
|
+
if (event.type === 'colorUpdate') {
|
|
954
|
+
yield { type: 'colorUpdate', updates: event.updates };
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
queueState.queuedMeshes = Math.max(0, queueState.queuedMeshes - event.meshes.length);
|
|
958
|
+
// Native desktop streaming already produces site-local geometry, so
|
|
959
|
+
// avoid the generic JS RTC/outlier scan on every streamed batch.
|
|
960
|
+
this.coordinateHandler.processTrustedMeshesIncremental(event.meshes);
|
|
961
|
+
totalMeshes += event.meshes.length;
|
|
962
|
+
const coordinateInfo = this.coordinateHandler.getCurrentCoordinateInfo();
|
|
963
|
+
yield {
|
|
964
|
+
type: 'batch',
|
|
965
|
+
meshes: event.meshes,
|
|
966
|
+
totalSoFar: totalMeshes,
|
|
967
|
+
coordinateInfo: coordinateInfo || undefined,
|
|
968
|
+
nativeTelemetry: event.nativeTelemetry,
|
|
969
|
+
};
|
|
970
|
+
drainedEventCount += 1;
|
|
971
|
+
drainedMeshCount += event.meshes.length;
|
|
972
|
+
if (queuedEvents.length > 0) {
|
|
973
|
+
const shouldYield = drainedEventCount >= MAX_NATIVE_STREAM_EVENTS_PER_TURN ||
|
|
974
|
+
drainedMeshCount >= MAX_NATIVE_STREAM_MESHES_PER_TURN ||
|
|
975
|
+
performance.now() - drainStartedAt >= MAX_NATIVE_STREAM_DRAIN_MS;
|
|
976
|
+
if (shouldYield) {
|
|
977
|
+
await yieldToEventLoop();
|
|
978
|
+
drainedEventCount = 0;
|
|
979
|
+
drainedMeshCount = 0;
|
|
980
|
+
drainStartedAt = performance.now();
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (streamError) {
|
|
985
|
+
throw streamError;
|
|
986
|
+
}
|
|
987
|
+
if (!completed) {
|
|
988
|
+
await new Promise((resolve) => {
|
|
989
|
+
resolvePending = resolve;
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
await streamingPromise;
|
|
994
|
+
if (queueState.coalescedBatchCount > 0) {
|
|
995
|
+
console.info(`[GeometryProcessor] Coalesced ${queueState.coalescedBatchCount} native batches while JS drained the queue`);
|
|
996
|
+
}
|
|
997
|
+
const coordinateInfo = this.coordinateHandler.getFinalCoordinateInfo();
|
|
998
|
+
yield { type: 'complete', totalMeshes: completedTotalMeshes ?? totalMeshes, coordinateInfo };
|
|
999
|
+
}
|
|
581
1000
|
/**
|
|
582
1001
|
* Parse symbolic representations (Plan, Annotation, FootPrint) from IFC content
|
|
583
1002
|
* These are pre-authored 2D curves for architectural drawings (door swings, window cuts, etc.)
|