@ifc-lite/geometry 1.16.4 → 1.16.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ifc-lite/geometry
2
2
 
3
- Geometry processing bridge for IFClite. Connects the WASM-based geometry engine to the TypeScript pipeline with streaming mesh processing.
3
+ Streaming geometry processor for IFClite. Converts IFC bytes to renderable mesh batches via Rust + WASM (or Tauri native), with first triangles in 300–500ms and progressive streaming for large models.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,7 +8,7 @@ Geometry processing bridge for IFClite. Connects the WASM-based geometry engine
8
8
  npm install @ifc-lite/geometry
9
9
  ```
10
10
 
11
- ## Quick Start
11
+ ## Process geometry from IFC bytes
12
12
 
13
13
  ```typescript
14
14
  import { GeometryProcessor } from '@ifc-lite/geometry';
@@ -16,22 +16,53 @@ import { GeometryProcessor } from '@ifc-lite/geometry';
16
16
  const processor = new GeometryProcessor();
17
17
  await processor.init();
18
18
 
19
- const result = await processor.process(ifcBuffer, {
20
- onBatch: (batch) => renderer.addMeshes(batch),
21
- });
19
+ const buffer = new Uint8Array(await file.arrayBuffer());
20
+ const result = await processor.process(buffer);
21
+
22
+ console.log(`${result.meshes.length} meshes, ${result.totalTriangles} triangles`);
23
+ // Each mesh: { expressId, positions: Float32Array, normals, indices: Uint32Array, color, ifcType }
24
+ ```
25
+
26
+ ## Stream geometry (recommended for large models)
27
+
28
+ ```typescript
29
+ for await (const event of processor.processStreaming(buffer)) {
30
+ if (event.type === 'batch') {
31
+ renderer.appendMeshes(event.meshes);
32
+ console.log(`Loaded ${event.totalSoFar} meshes so far`);
33
+ } else if (event.type === 'complete') {
34
+ console.log(`Done: ${event.totalMeshes} meshes`);
35
+ }
36
+ }
37
+ ```
38
+
39
+ The streaming path emits batches every ~100 meshes so the renderer can paint progressively — first triangles typically arrive 300–500ms after `processStreaming()` returns.
40
+
41
+ ## Coordinate handling
42
+
43
+ ```typescript
44
+ import { CoordinateHandler } from '@ifc-lite/geometry';
45
+
46
+ const result = await processor.process(buffer);
47
+
48
+ // Models with large world coordinates (geo-referenced) get auto-shifted
49
+ // to keep float precision inside the renderer.
50
+ if (result.coordinateInfo?.hasLargeCoordinates) {
51
+ const { x, y, z } = result.coordinateInfo.originShift;
52
+ console.log(`Origin shifted by [${x}, ${y}, ${z}] for renderer precision`);
53
+ }
22
54
  ```
23
55
 
24
- ## Features
56
+ ## Performance
25
57
 
26
- - Streaming geometry processing (100 meshes/batch)
27
- - First triangles in 300-500ms
28
- - Up to 5x faster than web-ifc
29
- - Web Worker support for large files (>50MB)
30
- - Coordinate system handling and origin shift
58
+ - **First triangles:** 300–500ms (streaming path)
59
+ - **Throughput:** up to 5× faster than `web-ifc` on the same model
60
+ - **Worker support:** files > 50 MB process off-main-thread automatically
61
+ - **Native (Tauri):** `preferNative: true` constructor option enables the native Rust pipeline for desktop builds
31
62
 
32
63
  ## API
33
64
 
34
- See the [Geometry Guide](../../docs/guide/geometry.md) and [API Reference](../../docs/api/typescript.md#ifc-litegeometry).
65
+ See the [Geometry Guide](https://louistrue.github.io/ifc-lite/guide/geometry/) and [API Reference](https://louistrue.github.io/ifc-lite/api/typescript/#ifc-litegeometry).
35
66
 
36
67
  ## License
37
68
 
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Coordinate transform utilities and batch-conversion helpers.
3
+ *
4
+ * Pure functions that convert WASM mesh/instanced-geometry collections
5
+ * into plain MeshData arrays, compute streaming batch sizes, and merge
6
+ * building rotation into coordinate info.
7
+ */
8
+ import type { MeshData, CoordinateInfo } from './types.js';
9
+ import type { DynamicBatchConfig } from './index.js';
10
+ /**
11
+ * Return a fixed or heuristic batch size for streaming, given the file
12
+ * buffer and the caller-supplied config.
13
+ */
14
+ export declare function getStreamingBatchSize(buffer: Uint8Array, batchConfig: number | DynamicBatchConfig): number;
15
+ /**
16
+ * Convert a WASM MeshCollection into a plain MeshData array, freeing
17
+ * each mesh and the collection itself.
18
+ */
19
+ export declare function convertMeshCollectionToBatch(collection: import('@ifc-lite/wasm').MeshCollection): MeshData[];
20
+ /**
21
+ * Convert a WASM InstancedMeshCollection into a plain array of
22
+ * InstancedGeometry objects, freeing the collection wrapper.
23
+ */
24
+ export declare function convertInstancedCollectionToBatch(collection: import('@ifc-lite/wasm').InstancedMeshCollection): import('@ifc-lite/wasm').InstancedGeometry[];
25
+ /**
26
+ * Merge an optional building rotation value into a CoordinateInfo object.
27
+ */
28
+ export declare function withBuildingRotation(coordinateInfo: CoordinateInfo, buildingRotation?: number): CoordinateInfo;
29
+ //# sourceMappingURL=geometry-coordinate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry-coordinate.d.ts","sourceRoot":"","sources":["../src/geometry-coordinate.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIrD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,MAAM,GAAG,kBAAkB,GACvC,MAAM,CAeR;AAID;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,OAAO,gBAAgB,EAAE,cAAc,GAClD,QAAQ,EAAE,CA0BZ;AAED;;;GAGG;AACH,wBAAgB,iCAAiC,CAC/C,UAAU,EAAE,OAAO,gBAAgB,EAAE,uBAAuB,GAC3D,OAAO,gBAAgB,EAAE,iBAAiB,EAAE,CAe9C;AAID;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,cAAc,EAC9B,gBAAgB,CAAC,EAAE,MAAM,GACxB,cAAc,CAIhB"}
@@ -0,0 +1,83 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ // ── Batch-size heuristics ──
5
+ /**
6
+ * Return a fixed or heuristic batch size for streaming, given the file
7
+ * buffer and the caller-supplied config.
8
+ */
9
+ export function getStreamingBatchSize(buffer, batchConfig) {
10
+ if (typeof batchConfig === 'number') {
11
+ return batchConfig;
12
+ }
13
+ const fileSizeMB = batchConfig.fileSizeMB
14
+ ? batchConfig.fileSizeMB
15
+ : buffer.length / (1024 * 1024);
16
+ return fileSizeMB < 10 ? 100
17
+ : fileSizeMB < 50 ? 200
18
+ : fileSizeMB < 100 ? 300
19
+ : fileSizeMB < 300 ? 500
20
+ : fileSizeMB < 500 ? 1500
21
+ : 3000;
22
+ }
23
+ // ── WASM collection → MeshData[] conversion ──
24
+ /**
25
+ * Convert a WASM MeshCollection into a plain MeshData array, freeing
26
+ * each mesh and the collection itself.
27
+ */
28
+ export function convertMeshCollectionToBatch(collection) {
29
+ const batch = [];
30
+ try {
31
+ for (let i = 0; i < collection.length; i++) {
32
+ const mesh = collection.get(i);
33
+ if (!mesh)
34
+ continue;
35
+ try {
36
+ batch.push({
37
+ expressId: mesh.expressId,
38
+ ifcType: mesh.ifcType,
39
+ positions: mesh.positions,
40
+ normals: mesh.normals,
41
+ indices: mesh.indices,
42
+ color: [mesh.color[0], mesh.color[1], mesh.color[2], mesh.color[3]],
43
+ });
44
+ }
45
+ finally {
46
+ mesh.free();
47
+ }
48
+ }
49
+ }
50
+ finally {
51
+ collection.free();
52
+ }
53
+ return batch;
54
+ }
55
+ /**
56
+ * Convert a WASM InstancedMeshCollection into a plain array of
57
+ * InstancedGeometry objects, freeing the collection wrapper.
58
+ */
59
+ export function convertInstancedCollectionToBatch(collection) {
60
+ const batch = [];
61
+ try {
62
+ for (let i = 0; i < collection.length; i++) {
63
+ const geometry = collection.get(i);
64
+ if (geometry) {
65
+ batch.push(geometry);
66
+ }
67
+ }
68
+ }
69
+ finally {
70
+ collection.free();
71
+ }
72
+ return batch;
73
+ }
74
+ // ── Coordinate-info helpers ──
75
+ /**
76
+ * Merge an optional building rotation value into a CoordinateInfo object.
77
+ */
78
+ export function withBuildingRotation(coordinateInfo, buildingRotation) {
79
+ return buildingRotation !== undefined
80
+ ? { ...coordinateInfo, buildingRotation }
81
+ : coordinateInfo;
82
+ }
83
+ //# sourceMappingURL=geometry-coordinate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry-coordinate.js","sourceRoot":"","sources":["../src/geometry-coordinate.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAa/D,8BAA8B;AAE9B;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAkB,EAClB,WAAwC;IAExC,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACpC,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,MAAM,UAAU,GAAG,WAAW,CAAC,UAAU;QACvC,CAAC,CAAC,WAAW,CAAC,UAAU;QACxB,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAElC,OAAO,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG;QAC1B,CAAC,CAAC,UAAU,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG;YACvB,CAAC,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG;gBACxB,CAAC,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG;oBACxB,CAAC,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI;wBACzB,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED,gDAAgD;AAEhD;;;GAGG;AACH,MAAM,UAAU,4BAA4B,CAC1C,UAAmD;IAEnD,MAAM,KAAK,GAAe,EAAE,CAAC;IAE7B,IAAI,CAAC;QACH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3C,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC/B,IAAI,CAAC,IAAI;gBAAE,SAAS;YAEpB,IAAI,CAAC;gBACH,KAAK,CAAC,IAAI,CAAC;oBACT,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,SAAS,EAAE,IAAI,CAAC,SAAS;oBACzB,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,KAAK,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;iBACpE,CAAC,CAAC;YACL,CAAC;oBAAS,CAAC;gBACT,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iCAAiC,CAC/C,UAA4D;IAE5D,MAAM,KAAK,GAAiD,EAAE,CAAC;IAE/D,IAAI,CAAC;QACH,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3C,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACnC,IAAI,QAAQ,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;IACH,CAAC;YAAS,CAAC;QACT,UAAU,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gCAAgC;AAEhC;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAClC,cAA8B,EAC9B,gBAAyB;IAEzB,OAAO,gBAAgB,KAAK,SAAS;QACnC,CAAC,CAAC,EAAE,GAAG,cAAc,EAAE,gBAAgB,EAAE;QACzC,CAAC,CAAC,cAAc,CAAC;AACrB,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Native Tauri bridge streaming helpers.
3
+ *
4
+ * Handles queue coalescing, back-pressure, and event-loop yielding for
5
+ * the native desktop geometry streaming path. These are platform-specific
6
+ * utilities isolated from the main GeometryProcessor.
7
+ */
8
+ import type { CoordinateHandler } from './coordinate-handler.js';
9
+ import type { MeshData } from './types.js';
10
+ import type { GeometryStats as PlatformGeometryStats, GeometryBatch, NativeBatchTelemetry } from './platform-bridge.js';
11
+ import type { StreamingGeometryEvent } from './index.js';
12
+ export declare const MAX_NATIVE_STREAM_QUEUE_EVENTS = 8;
13
+ export declare const MAX_NATIVE_STREAM_QUEUE_MESHES = 32768;
14
+ export declare const MAX_NATIVE_STREAM_EVENTS_PER_TURN = 4;
15
+ export declare const MAX_NATIVE_STREAM_MESHES_PER_TURN = 8192;
16
+ export declare const MAX_NATIVE_STREAM_DRAIN_MS = 10;
17
+ export type QueuedNativeStreamingEvent = {
18
+ type: 'batch';
19
+ meshes: MeshData[];
20
+ nativeTelemetry?: NativeBatchTelemetry;
21
+ } | {
22
+ type: 'colorUpdate';
23
+ updates: Map<number, [number, number, number, number]>;
24
+ };
25
+ export declare function yieldToEventLoop(): Promise<void>;
26
+ /**
27
+ * Coalesce incoming native events into the queue to reduce per-yield
28
+ * overhead when the JS main thread cannot keep up with Rust production.
29
+ */
30
+ export declare function enqueueNativeStreamingEvent(queuedEvents: QueuedNativeStreamingEvent[], event: QueuedNativeStreamingEvent, queueState: {
31
+ queuedMeshes: number;
32
+ coalescedBatchCount: number;
33
+ }): void;
34
+ /**
35
+ * Shared native streaming generator used by both buffer-based and
36
+ * path-based native geometry streaming.
37
+ *
38
+ * @param startStream Callback that kicks off the native stream and
39
+ * returns a promise resolving when it finishes.
40
+ * @param totalEstimate Estimated total for the 'start' event.
41
+ * @param coordinator CoordinateHandler for incremental bounds.
42
+ * @param setLastNativeStats Callback to persist the latest stats on
43
+ * the owning GeometryProcessor instance.
44
+ */
45
+ export declare function streamNativeGeometry(startStream: (options: {
46
+ onBatch: (batch: GeometryBatch) => void;
47
+ onColorUpdate: (updates: Map<number, [number, number, number, number]>) => void;
48
+ onComplete: (stats: PlatformGeometryStats) => void;
49
+ onError: (error: Error) => void;
50
+ }) => Promise<PlatformGeometryStats>, totalEstimate: number, coordinator: CoordinateHandler, setLastNativeStats: (stats: PlatformGeometryStats) => void): AsyncGenerator<StreamingGeometryEvent>;
51
+ //# sourceMappingURL=geometry-native.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry-native.d.ts","sourceRoot":"","sources":["../src/geometry-native.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,IAAI,qBAAqB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACxH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAIzD,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAChD,eAAO,MAAM,8BAA8B,QAAQ,CAAC;AACpD,eAAO,MAAM,iCAAiC,IAAI,CAAC;AACnD,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAI7C,MAAM,MAAM,0BAA0B,GAClC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,QAAQ,EAAE,CAAC;IAAC,eAAe,CAAC,EAAE,oBAAoB,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAC;AAIpF,wBAAgB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAUhD;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,0BAA0B,EAAE,EAC1C,KAAK,EAAE,0BAA0B,EACjC,UAAU,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,mBAAmB,EAAE,MAAM,CAAA;CAAE,GAChE,IAAI,CA6BN;AAED;;;;;;;;;;GAUG;AACH,wBAAuB,oBAAoB,CACzC,WAAW,EAAE,CAAC,OAAO,EAAE;IACrB,OAAO,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,IAAI,CAAC;IAChF,UAAU,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IACnD,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACjC,KAAK,OAAO,CAAC,qBAAqB,CAAC,EACpC,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,iBAAiB,EAC9B,kBAAkB,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,GACzD,cAAc,CAAC,sBAAsB,CAAC,CA8GxC"}
@@ -0,0 +1,154 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ // ── Queue tuning constants ──
5
+ export const MAX_NATIVE_STREAM_QUEUE_EVENTS = 8;
6
+ export const MAX_NATIVE_STREAM_QUEUE_MESHES = 32768;
7
+ export const MAX_NATIVE_STREAM_EVENTS_PER_TURN = 4;
8
+ export const MAX_NATIVE_STREAM_MESHES_PER_TURN = 8192;
9
+ export const MAX_NATIVE_STREAM_DRAIN_MS = 10;
10
+ // ── Helpers ──
11
+ export function yieldToEventLoop() {
12
+ const maybeScheduler = globalThis.scheduler;
13
+ if (typeof maybeScheduler?.yield === 'function') {
14
+ return maybeScheduler.yield();
15
+ }
16
+ return new Promise((resolve) => {
17
+ globalThis.setTimeout(resolve, 0);
18
+ });
19
+ }
20
+ /**
21
+ * Coalesce incoming native events into the queue to reduce per-yield
22
+ * overhead when the JS main thread cannot keep up with Rust production.
23
+ */
24
+ export function enqueueNativeStreamingEvent(queuedEvents, event, queueState) {
25
+ if (event.type === 'colorUpdate') {
26
+ const lastEvent = queuedEvents[queuedEvents.length - 1];
27
+ if (lastEvent?.type === 'colorUpdate') {
28
+ for (const [expressId, color] of event.updates) {
29
+ lastEvent.updates.set(expressId, color);
30
+ }
31
+ return;
32
+ }
33
+ queuedEvents.push(event);
34
+ return;
35
+ }
36
+ const lastEvent = queuedEvents[queuedEvents.length - 1];
37
+ const shouldCoalesce = lastEvent?.type === 'batch' &&
38
+ (queuedEvents.length >= MAX_NATIVE_STREAM_QUEUE_EVENTS || queueState.queuedMeshes >= MAX_NATIVE_STREAM_QUEUE_MESHES);
39
+ if (shouldCoalesce) {
40
+ for (let i = 0; i < event.meshes.length; i++) {
41
+ lastEvent.meshes.push(event.meshes[i]);
42
+ }
43
+ lastEvent.nativeTelemetry = event.nativeTelemetry;
44
+ queueState.coalescedBatchCount += 1;
45
+ }
46
+ else {
47
+ queuedEvents.push(event);
48
+ }
49
+ queueState.queuedMeshes += event.meshes.length;
50
+ }
51
+ /**
52
+ * Shared native streaming generator used by both buffer-based and
53
+ * path-based native geometry streaming.
54
+ *
55
+ * @param startStream Callback that kicks off the native stream and
56
+ * returns a promise resolving when it finishes.
57
+ * @param totalEstimate Estimated total for the 'start' event.
58
+ * @param coordinator CoordinateHandler for incremental bounds.
59
+ * @param setLastNativeStats Callback to persist the latest stats on
60
+ * the owning GeometryProcessor instance.
61
+ */
62
+ export async function* streamNativeGeometry(startStream, totalEstimate, coordinator, setLastNativeStats) {
63
+ coordinator.reset();
64
+ yield { type: 'start', totalEstimate };
65
+ await yieldToEventLoop();
66
+ yield { type: 'model-open', modelID: 0 };
67
+ const queuedEvents = [];
68
+ const queueState = { queuedMeshes: 0, coalescedBatchCount: 0 };
69
+ let resolvePending = null;
70
+ let completed = false;
71
+ let streamError = null;
72
+ let completedTotalMeshes;
73
+ let totalMeshes = 0;
74
+ const wake = () => {
75
+ if (resolvePending) {
76
+ resolvePending();
77
+ resolvePending = null;
78
+ }
79
+ };
80
+ const streamingPromise = startStream({
81
+ onBatch: (batch) => {
82
+ enqueueNativeStreamingEvent(queuedEvents, { type: 'batch', meshes: batch.meshes, nativeTelemetry: batch.nativeTelemetry }, queueState);
83
+ wake();
84
+ },
85
+ onColorUpdate: (updates) => {
86
+ enqueueNativeStreamingEvent(queuedEvents, { type: 'colorUpdate', updates: new Map(updates) }, queueState);
87
+ wake();
88
+ },
89
+ onComplete: (stats) => {
90
+ setLastNativeStats(stats);
91
+ completedTotalMeshes = stats.totalMeshes;
92
+ completed = true;
93
+ wake();
94
+ },
95
+ onError: (error) => {
96
+ streamError = error;
97
+ completed = true;
98
+ wake();
99
+ },
100
+ });
101
+ while (!completed || queuedEvents.length > 0) {
102
+ let drainedEventCount = 0;
103
+ let drainedMeshCount = 0;
104
+ let drainStartedAt = performance.now();
105
+ while (queuedEvents.length > 0) {
106
+ const event = queuedEvents.shift();
107
+ if (event.type === 'colorUpdate') {
108
+ yield { type: 'colorUpdate', updates: event.updates };
109
+ continue;
110
+ }
111
+ queueState.queuedMeshes = Math.max(0, queueState.queuedMeshes - event.meshes.length);
112
+ // Native desktop streaming already produces site-local geometry, so
113
+ // avoid the generic JS RTC/outlier scan on every streamed batch.
114
+ coordinator.processTrustedMeshesIncremental(event.meshes);
115
+ totalMeshes += event.meshes.length;
116
+ const coordinateInfo = coordinator.getCurrentCoordinateInfo();
117
+ yield {
118
+ type: 'batch',
119
+ meshes: event.meshes,
120
+ totalSoFar: totalMeshes,
121
+ coordinateInfo: coordinateInfo || undefined,
122
+ nativeTelemetry: event.nativeTelemetry,
123
+ };
124
+ drainedEventCount += 1;
125
+ drainedMeshCount += event.meshes.length;
126
+ if (queuedEvents.length > 0) {
127
+ const shouldYield = drainedEventCount >= MAX_NATIVE_STREAM_EVENTS_PER_TURN ||
128
+ drainedMeshCount >= MAX_NATIVE_STREAM_MESHES_PER_TURN ||
129
+ performance.now() - drainStartedAt >= MAX_NATIVE_STREAM_DRAIN_MS;
130
+ if (shouldYield) {
131
+ await yieldToEventLoop();
132
+ drainedEventCount = 0;
133
+ drainedMeshCount = 0;
134
+ drainStartedAt = performance.now();
135
+ }
136
+ }
137
+ }
138
+ if (streamError) {
139
+ throw streamError;
140
+ }
141
+ if (!completed) {
142
+ await new Promise((resolve) => {
143
+ resolvePending = resolve;
144
+ });
145
+ }
146
+ }
147
+ await streamingPromise;
148
+ if (queueState.coalescedBatchCount > 0) {
149
+ console.info(`[GeometryProcessor] Coalesced ${queueState.coalescedBatchCount} native batches while JS drained the queue`);
150
+ }
151
+ const coordinateInfo = coordinator.getFinalCoordinateInfo();
152
+ yield { type: 'complete', totalMeshes: completedTotalMeshes ?? totalMeshes, coordinateInfo };
153
+ }
154
+ //# sourceMappingURL=geometry-native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry-native.js","sourceRoot":"","sources":["../src/geometry-native.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Multi-worker parallel geometry processing.
3
+ *
4
+ * Spawns Web Workers that each get their own WASM instance and process
5
+ * disjoint slices of the geometry entity list. Batches are yielded as
6
+ * they arrive from any worker, enabling progressive rendering while
7
+ * utilizing multiple cores.
8
+ */
9
+ import type { CoordinateHandler } from './coordinate-handler.js';
10
+ import type { StreamingGeometryEvent } from './index.js';
11
+ /**
12
+ * Run the full pre-pass in a dedicated worker, then fan geometry jobs
13
+ * out to N workers and yield batches as they complete.
14
+ *
15
+ * @param buffer Raw IFC file bytes
16
+ * @param coordinator CoordinateHandler used to accumulate bounds
17
+ */
18
+ export declare function processParallel(buffer: Uint8Array, coordinator: CoordinateHandler, sharedRtcOffset?: {
19
+ x: number;
20
+ y: number;
21
+ z: number;
22
+ }): AsyncGenerator<StreamingGeometryEvent>;
23
+ //# sourceMappingURL=geometry-parallel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry-parallel.d.ts","sourceRoot":"","sources":["../src/geometry-parallel.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAuB,eAAe,CACpC,MAAM,EAAE,UAAU,EAClB,WAAW,EAAE,iBAAiB,EAC9B,eAAe,CAAC,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,GACpD,cAAc,CAAC,sBAAsB,CAAC,CA+MxC"}
@@ -0,0 +1,193 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Run the full pre-pass in a dedicated worker, then fan geometry jobs
6
+ * out to N workers and yield batches as they complete.
7
+ *
8
+ * @param buffer Raw IFC file bytes
9
+ * @param coordinator CoordinateHandler used to accumulate bounds
10
+ */
11
+ export async function* processParallel(buffer, coordinator, sharedRtcOffset) {
12
+ coordinator.reset();
13
+ yield { type: 'start', totalEstimate: buffer.length / 1000 };
14
+ yield { type: 'model-open', modelID: 0 };
15
+ // Copy file bytes into SharedArrayBuffer for zero-copy sharing with workers
16
+ const sharedBuffer = new SharedArrayBuffer(buffer.byteLength);
17
+ new Uint8Array(sharedBuffer).set(buffer);
18
+ // ── PHASE 1: Full pre-pass in worker ──
19
+ const makeWorker = () => new Worker(new URL('./geometry.worker.ts', import.meta.url), { type: 'module' });
20
+ const prePassResult = await new Promise((resolve, reject) => {
21
+ const w = makeWorker();
22
+ w.onmessage = (e) => {
23
+ if (e.data.type === 'prepass-result') {
24
+ w.terminate();
25
+ resolve(e.data.result);
26
+ }
27
+ else if (e.data.type === 'error') {
28
+ w.terminate();
29
+ reject(new Error(e.data.message));
30
+ }
31
+ };
32
+ w.onerror = (e) => { w.terminate(); reject(new Error(e.message)); };
33
+ w.postMessage({ type: 'prepass', sharedBuffer });
34
+ });
35
+ if (!prePassResult || !prePassResult.jobs || prePassResult.totalJobs === 0) {
36
+ const coordinateInfo = coordinator.getFinalCoordinateInfo();
37
+ yield { type: 'complete', totalMeshes: 0, coordinateInfo };
38
+ return;
39
+ }
40
+ const { jobs: jobsFlat, totalJobs, unitScale, rtcOffset, needsShift, voidKeys, voidCounts, voidValues, styleIds, styleColors } = prePassResult;
41
+ // When a shared RTC offset is provided (2nd+ federated model), use it
42
+ // instead of the per-model RTC. This ensures all models share the same
43
+ // coordinate origin, giving pixel-perfect federation alignment.
44
+ const useSharedRtc = sharedRtcOffset != null;
45
+ const rtcX = useSharedRtc ? sharedRtcOffset.x : (rtcOffset?.[0] ?? 0);
46
+ const rtcY = useSharedRtc ? sharedRtcOffset.y : (rtcOffset?.[1] ?? 0);
47
+ const rtcZ = useSharedRtc ? sharedRtcOffset.z : (rtcOffset?.[2] ?? 0);
48
+ const effectiveNeedsShift = useSharedRtc ? true : needsShift;
49
+ yield {
50
+ type: 'rtcOffset',
51
+ rtcOffset: { x: rtcX, y: rtcY, z: rtcZ },
52
+ hasRtc: effectiveNeedsShift,
53
+ };
54
+ // ── PHASE 2: Dynamic worker provisioning based on device capability ──
55
+ const cores = typeof navigator !== 'undefined' ? (navigator.hardwareConcurrency ?? 2) : 2;
56
+ const deviceMemoryGB = typeof navigator !== 'undefined' ? (navigator.deviceMemory ?? 8) : 8;
57
+ const fileSizeGB = buffer.byteLength / (1024 * 1024 * 1024);
58
+ // Determine optimal workers:
59
+ // - Desktop (16+ cores, 16+ GB): up to 8 workers
60
+ // - Laptop (8 cores, 8 GB): 2-4 workers (avoid thermal throttling on fanless)
61
+ // - Low-end (4 cores, 4 GB): 1-2 workers
62
+ // - Large files need more memory per worker, so fewer workers
63
+ let maxWorkers;
64
+ if (cores >= 16 && deviceMemoryGB >= 16) {
65
+ maxWorkers = Math.min(8, Math.floor(cores / 2));
66
+ }
67
+ else if (cores >= 8 && deviceMemoryGB >= 8) {
68
+ // MacBook Air M-series: 8 cores but fanless → throttles with too many workers
69
+ // Use 3 workers: enough parallelism without severe throttling
70
+ maxWorkers = fileSizeGB > 0.5 ? 2 : 3;
71
+ }
72
+ else {
73
+ maxWorkers = Math.max(1, Math.min(2, Math.floor(cores / 2)));
74
+ }
75
+ const workerCount = Math.min(maxWorkers, totalJobs);
76
+ const jobsPerWorker = Math.ceil(totalJobs / workerCount);
77
+ const chunks = [];
78
+ for (let i = 0; i < workerCount; i++) {
79
+ const start = i * jobsPerWorker;
80
+ const end = Math.min(start + jobsPerWorker, totalJobs);
81
+ if (start < end)
82
+ chunks.push([start, end]);
83
+ }
84
+ // Queue-based async generator: workers push batches, generator yields them
85
+ const batchQueue = [];
86
+ let resolveWaiting = null;
87
+ let workersCompleted = 0;
88
+ let totalMeshes = 0;
89
+ let workerError = null;
90
+ const workers = [];
91
+ for (let i = 0; i < chunks.length; i++) {
92
+ const [jobStart, jobEnd] = chunks[i];
93
+ if (jobStart >= jobEnd) {
94
+ workersCompleted++;
95
+ continue;
96
+ }
97
+ const workerJobs = jobsFlat.slice(jobStart * 3, jobEnd * 3);
98
+ const worker = new Worker(new URL('./geometry.worker.ts', import.meta.url), { type: 'module' });
99
+ workers.push(worker);
100
+ worker.onmessage = (e) => {
101
+ const msg = e.data;
102
+ if (msg.type === 'batch') {
103
+ // Convert transferable data back to MeshData[]
104
+ const meshes = msg.meshes.map((m) => ({
105
+ expressId: m.expressId,
106
+ ifcType: m.ifcType,
107
+ positions: m.positions instanceof Float32Array ? m.positions : new Float32Array(m.positions),
108
+ normals: m.normals instanceof Float32Array ? m.normals : new Float32Array(m.normals),
109
+ indices: m.indices instanceof Uint32Array ? m.indices : new Uint32Array(m.indices),
110
+ color: m.color,
111
+ }));
112
+ if (meshes.length > 0) {
113
+ batchQueue.push(meshes);
114
+ if (resolveWaiting) {
115
+ resolveWaiting();
116
+ resolveWaiting = null;
117
+ }
118
+ }
119
+ }
120
+ else if (msg.type === 'complete') {
121
+ totalMeshes += msg.totalMeshes;
122
+ workersCompleted++;
123
+ worker.terminate();
124
+ if (resolveWaiting) {
125
+ resolveWaiting();
126
+ resolveWaiting = null;
127
+ }
128
+ }
129
+ else if (msg.type === 'error') {
130
+ workerError = new Error(`Geometry worker error: ${msg.message}`);
131
+ workersCompleted++;
132
+ worker.terminate();
133
+ if (resolveWaiting) {
134
+ resolveWaiting();
135
+ resolveWaiting = null;
136
+ }
137
+ }
138
+ };
139
+ worker.onerror = (e) => {
140
+ workerError = new Error(`Geometry worker failed: ${e.message}`);
141
+ workersCompleted++;
142
+ worker.terminate();
143
+ if (resolveWaiting) {
144
+ resolveWaiting();
145
+ resolveWaiting = null;
146
+ }
147
+ };
148
+ // Send work — sharedBuffer is zero-copy, typed arrays are transferred
149
+ worker.postMessage({
150
+ type: 'process',
151
+ sharedBuffer,
152
+ jobsFlat: workerJobs,
153
+ unitScale,
154
+ rtcX, rtcY, rtcZ,
155
+ needsShift: effectiveNeedsShift,
156
+ voidKeys, voidCounts, voidValues,
157
+ styleIds, styleColors,
158
+ });
159
+ }
160
+ // Yield batches as they arrive from any worker
161
+ while (true) {
162
+ while (batchQueue.length > 0) {
163
+ const batch = batchQueue.shift();
164
+ coordinator.processMeshesIncremental(batch);
165
+ const coordinateInfo = coordinator.getCurrentCoordinateInfo();
166
+ yield {
167
+ type: 'batch',
168
+ meshes: batch,
169
+ totalSoFar: totalMeshes,
170
+ coordinateInfo: coordinateInfo || undefined,
171
+ };
172
+ }
173
+ if (workerError) {
174
+ // Terminate remaining workers
175
+ for (const w of workers) {
176
+ try {
177
+ w.terminate();
178
+ }
179
+ catch { /* cleanup — safe to ignore */ }
180
+ }
181
+ throw workerError;
182
+ }
183
+ if (workersCompleted >= chunks.length && batchQueue.length === 0) {
184
+ break;
185
+ }
186
+ await new Promise((resolve) => {
187
+ resolveWaiting = resolve;
188
+ });
189
+ }
190
+ const coordinateInfo = coordinator.getFinalCoordinateInfo();
191
+ yield { type: 'complete', totalMeshes, coordinateInfo };
192
+ }
193
+ //# sourceMappingURL=geometry-parallel.js.map