@ifc-lite/server-client 1.24.0 → 2.1.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.
Files changed (48) hide show
  1. package/README.md +28 -1
  2. package/dist/client.d.ts +29 -3
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +89 -162
  5. package/dist/client.js.map +1 -1
  6. package/dist/data-model-decoder.d.ts +4 -0
  7. package/dist/data-model-decoder.d.ts.map +1 -1
  8. package/dist/data-model-decoder.js +14 -8
  9. package/dist/data-model-decoder.js.map +1 -1
  10. package/dist/geometry-diagnostics-types.d.ts +124 -0
  11. package/dist/geometry-diagnostics-types.d.ts.map +1 -0
  12. package/dist/geometry-diagnostics-types.js +5 -0
  13. package/dist/geometry-diagnostics-types.js.map +1 -0
  14. package/dist/parquet-columns.d.ts +22 -4
  15. package/dist/parquet-columns.d.ts.map +1 -1
  16. package/dist/parquet-columns.js +32 -0
  17. package/dist/parquet-columns.js.map +1 -1
  18. package/dist/parquet-decoder.d.ts +3 -1
  19. package/dist/parquet-decoder.d.ts.map +1 -1
  20. package/dist/parquet-decoder.js +40 -70
  21. package/dist/parquet-decoder.js.map +1 -1
  22. package/dist/parquet-nullable.d.ts +26 -0
  23. package/dist/parquet-nullable.d.ts.map +1 -0
  24. package/dist/parquet-nullable.js +36 -0
  25. package/dist/parquet-nullable.js.map +1 -0
  26. package/dist/parquet-rotation.d.ts +70 -0
  27. package/dist/parquet-rotation.d.ts.map +1 -0
  28. package/dist/parquet-rotation.js +96 -0
  29. package/dist/parquet-rotation.js.map +1 -0
  30. package/dist/parquet-stream-events.d.ts +23 -0
  31. package/dist/parquet-stream-events.d.ts.map +1 -0
  32. package/dist/parquet-stream-events.js +133 -0
  33. package/dist/parquet-stream-events.js.map +1 -0
  34. package/dist/parquet-tables.d.ts +11 -1
  35. package/dist/parquet-tables.d.ts.map +1 -1
  36. package/dist/parquet-tables.js +45 -59
  37. package/dist/parquet-tables.js.map +1 -1
  38. package/dist/parse-query.d.ts +18 -0
  39. package/dist/parse-query.d.ts.map +1 -0
  40. package/dist/parse-query.js +21 -0
  41. package/dist/parse-query.js.map +1 -0
  42. package/dist/symbolic-types.d.ts +2 -0
  43. package/dist/symbolic-types.d.ts.map +1 -1
  44. package/dist/types.d.ts +8 -46
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/types.js +1 -0
  47. package/dist/types.js.map +1 -1
  48. package/package.json +4 -3
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Null-safe bulk extraction for a nullable numeric Arrow column.
3
+ *
4
+ * `Vector.toArray()` returns the column's raw values buffer directly (its
5
+ * fast path for a primitive type), and that buffer carries whatever bits sit
6
+ * at a NULL row's slot — 0 in practice for a Float64/Int column written by
7
+ * every writer this project has observed (DuckDB, parquet-wasm), not a
8
+ * detectable sentinel. `data-model-decoder.ts`'s own module doc cites
9
+ * `toArray()` as "10-20x faster" than per-row `.get(i)` for BULK STRING
10
+ * extraction; that justification does not carry over to a NULLABLE numeric
11
+ * column, where `.get(i)` is the only call that reports a null row as `null`
12
+ * instead of a real-looking `0`.
13
+ *
14
+ * Two server-emitted columns are nullable `Float64` today: spatial-node
15
+ * `elevation` (a storey whose elevation cannot be resolved, see
16
+ * `apps/server/src/services/data_model/spatial.rs`) and material
17
+ * `thickness` (every non-layer material association — single material,
18
+ * material list, material constituent — always has `thickness: None`, see
19
+ * `apps/server/src/services/data_model/materials.rs`). Both used to decode
20
+ * here as `0` instead of `undefined` — a legitimate "no elevation" storey or
21
+ * "not a layer" material silently reading as a real, wrong measurement.
22
+ */
23
+ export declare function nullableFloat64Column(table: {
24
+ getChild(name: string): unknown;
25
+ }, name: string): (number | null)[] | undefined;
26
+ //# sourceMappingURL=parquet-nullable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parquet-nullable.d.ts","sourceRoot":"","sources":["../src/parquet-nullable.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAC1C,IAAI,EAAE,MAAM,GACX,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,GAAG,SAAS,CAU/B"}
@@ -0,0 +1,36 @@
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
+ * Null-safe bulk extraction for a nullable numeric Arrow column.
6
+ *
7
+ * `Vector.toArray()` returns the column's raw values buffer directly (its
8
+ * fast path for a primitive type), and that buffer carries whatever bits sit
9
+ * at a NULL row's slot — 0 in practice for a Float64/Int column written by
10
+ * every writer this project has observed (DuckDB, parquet-wasm), not a
11
+ * detectable sentinel. `data-model-decoder.ts`'s own module doc cites
12
+ * `toArray()` as "10-20x faster" than per-row `.get(i)` for BULK STRING
13
+ * extraction; that justification does not carry over to a NULLABLE numeric
14
+ * column, where `.get(i)` is the only call that reports a null row as `null`
15
+ * instead of a real-looking `0`.
16
+ *
17
+ * Two server-emitted columns are nullable `Float64` today: spatial-node
18
+ * `elevation` (a storey whose elevation cannot be resolved, see
19
+ * `apps/server/src/services/data_model/spatial.rs`) and material
20
+ * `thickness` (every non-layer material association — single material,
21
+ * material list, material constituent — always has `thickness: None`, see
22
+ * `apps/server/src/services/data_model/materials.rs`). Both used to decode
23
+ * here as `0` instead of `undefined` — a legitimate "no elevation" storey or
24
+ * "not a layer" material silently reading as a real, wrong measurement.
25
+ */
26
+ export function nullableFloat64Column(table, name) {
27
+ const col = table.getChild(name);
28
+ if (!col)
29
+ return undefined;
30
+ const raw = col.toArray();
31
+ // The common case (no nulls in this column/payload) keeps the fast bulk path.
32
+ if (col.nullCount === 0)
33
+ return Array.from(raw);
34
+ return Array.from(raw, (value, i) => (col.get(i) === null ? null : value));
35
+ }
36
+ //# sourceMappingURL=parquet-nullable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parquet-nullable.js","sourceRoot":"","sources":["../src/parquet-nullable.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,sEAAsE;AACtE,4DAA4D;AAE5D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAA0C,EAC1C,IAAY;IAEZ,MAAM,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAGlB,CAAC;IACd,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;IAC1B,8EAA8E;IAC9E,IAAI,GAAG,CAAC,SAAS,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7E,CAAC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Per-instance rotation, shared by both Parquet transports (issues #3575, #3888).
3
+ *
4
+ * Split out of `parquet-tables.ts` to stay under that file's module-size
5
+ * budget (see `parquet-columns.ts` for the same reasoning applied to the
6
+ * shared trailing columns).
7
+ *
8
+ * The server dedupes rotated `IfcMappedItem` / shared-`IfcRepresentationMap`
9
+ * occurrences by storing ONE template mesh in a canonical/local frame and
10
+ * carrying each instance's rotation (row-major 3x3, `rot0..rot8`) alongside
11
+ * its `origin_x/y/z`. The optimized transport has done this since #3575 and the
12
+ * flat one since `-parquet-v6` (#3888); the columns, the frame and the
13
+ * reconstruction are identical, which is why this module is shared rather than
14
+ * copied. Reconstruction contract:
15
+ * `world = origin + R * template_position` — the SAME `origin`-only contract
16
+ * as before (#1841) when `R` is the identity, which is what the server emits
17
+ * for every instance it did not verify a rotation-aware placement for.
18
+ */
19
+ import type { ArrowTableLike } from './parquet-tables.js';
20
+ /** The nine `rot0..rot8` columns, present together or not at all. */
21
+ export type RotationColumns = ArrayLike<number>[];
22
+ /**
23
+ * Read the rotation columns, or `undefined` when the payload does not carry
24
+ * them — an optimized wire-version-2 payload (#3575), or a flat `-parquet-v5`
25
+ * blob (#3888). Callers then fall back to the identity rotation, which is
26
+ * exactly the behaviour those payloads were written for.
27
+ *
28
+ * `onAbsent` says what a MISSING or short block means to the caller, in the
29
+ * caller's own terms rather than in one transport's version numbering.
30
+ * `'identity'` is the reading above. `'throw'` is for a payload whose own
31
+ * header states the columns are there: the optimized format's version 3 is
32
+ * only emitted once a non-identity rotation has actually been written
33
+ * (`optimized_wire_version` in `parquet_instancing.rs`), so absence there is
34
+ * truncated wire data, and decoding it as identity would place a genuinely
35
+ * rotated occurrence at the wrong orientation with no signal why.
36
+ *
37
+ * The flat transport has no such header — it cannot tell a truncated v6 mesh
38
+ * table from a genuine v5 one — so it passes `'identity'`, which is right for
39
+ * the v5 case and no worse than throwing for the other.
40
+ */
41
+ export declare function readRotationColumns(instanceArrow: ArrowTableLike, rowCount: number, onAbsent?: 'identity' | 'throw'): RotationColumns | undefined;
42
+ /**
43
+ * Rotate `positions` (and, when present, `normals`) IN PLACE by row `index`'s
44
+ * 3x3 rotation. A no-op when the row is identity (translation-only or
45
+ * unverified placements — the overwhelming majority of rows on most models),
46
+ * so this costs nothing beyond the identity check for meshes #3575 did not
47
+ * touch.
48
+ *
49
+ * Normals get the SAME rotation matrix as positions, not its inverse
50
+ * transpose — correct for a rigid (orthogonal) rotation, which is what the
51
+ * server's per-vertex residual check (`RECOMPOSITION_TOLERANCE_M`) verifies
52
+ * before emitting a non-identity row. A non-uniform scale baked into the
53
+ * source `IfcCartesianTransformationOperator` would need the inverse
54
+ * transpose for normals specifically; this matches the convention the
55
+ * existing GPU-instancing wire format (`ifc_lite_geometry::instancing`)
56
+ * already uses, so it is not a new approximation this fix introduces.
57
+ *
58
+ * `isIdentityRow` already refuses to call a row with a NaN cell "identity"
59
+ * (`NaN !== 1` is true, so the row fails the comparison and falls through
60
+ * here) — a NaN can never silently render as an unrotated placement. But
61
+ * multiplying it through `rotateTriplets` would still corrupt every vertex
62
+ * of THIS instance to NaN, uncontrolled: no error, a mesh that may vanish
63
+ * or wreck a shared bounding-box computation with no signal why. A row that
64
+ * isn't finite is malformed wire data — the same class of fault
65
+ * `buildMeshesFromOptimizedTables` already fails loudly on for a bad
66
+ * mesh/material index — so it throws here too, instead of writing NaN
67
+ * silently into the buffer.
68
+ */
69
+ export declare function applyInstanceRotation(positions: Float32Array, normals: Float32Array | undefined, rot: RotationColumns, index: number): void;
70
+ //# sourceMappingURL=parquet-rotation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parquet-rotation.d.ts","sourceRoot":"","sources":["../src/parquet-rotation.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAG1D,qEAAqE;AACrE,MAAM,MAAM,eAAe,GAAG,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;AAKlD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,cAAc,EAC7B,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,UAAU,GAAG,OAAoB,GAC1C,eAAe,GAAG,SAAS,CAe7B;AAUD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,YAAY,GAAG,SAAS,EACjC,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,MAAM,GACZ,IAAI,CAUN"}
@@ -0,0 +1,96 @@
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
+ import { numericColumn } from './parquet-columns.js';
5
+ /** Row-major identity, hoisted so the per-mesh identity check allocates nothing. */
6
+ const IDENTITY = [1, 0, 0, 0, 1, 0, 0, 0, 1];
7
+ /**
8
+ * Read the rotation columns, or `undefined` when the payload does not carry
9
+ * them — an optimized wire-version-2 payload (#3575), or a flat `-parquet-v5`
10
+ * blob (#3888). Callers then fall back to the identity rotation, which is
11
+ * exactly the behaviour those payloads were written for.
12
+ *
13
+ * `onAbsent` says what a MISSING or short block means to the caller, in the
14
+ * caller's own terms rather than in one transport's version numbering.
15
+ * `'identity'` is the reading above. `'throw'` is for a payload whose own
16
+ * header states the columns are there: the optimized format's version 3 is
17
+ * only emitted once a non-identity rotation has actually been written
18
+ * (`optimized_wire_version` in `parquet_instancing.rs`), so absence there is
19
+ * truncated wire data, and decoding it as identity would place a genuinely
20
+ * rotated occurrence at the wrong orientation with no signal why.
21
+ *
22
+ * The flat transport has no such header — it cannot tell a truncated v6 mesh
23
+ * table from a genuine v5 one — so it passes `'identity'`, which is right for
24
+ * the v5 case and no worse than throwing for the other.
25
+ */
26
+ export function readRotationColumns(instanceArrow, rowCount, onAbsent = 'identity') {
27
+ const cols = [];
28
+ for (let i = 0; i < 9; i++) {
29
+ const col = numericColumn(instanceArrow, `rot${i}`);
30
+ if (!col || col.length < rowCount) {
31
+ if (onAbsent === 'throw') {
32
+ throw new Error('Malformed optimized Parquet geometry: format version 3 requires rot0..rot8 instance columns');
33
+ }
34
+ return undefined;
35
+ }
36
+ cols.push(col);
37
+ }
38
+ return cols;
39
+ }
40
+ /** `true` when row `index`'s rotation is exactly identity (the common case). */
41
+ function isIdentityRow(rot, index) {
42
+ for (let i = 0; i < 9; i++) {
43
+ if (rot[i][index] !== IDENTITY[i])
44
+ return false;
45
+ }
46
+ return true;
47
+ }
48
+ /**
49
+ * Rotate `positions` (and, when present, `normals`) IN PLACE by row `index`'s
50
+ * 3x3 rotation. A no-op when the row is identity (translation-only or
51
+ * unverified placements — the overwhelming majority of rows on most models),
52
+ * so this costs nothing beyond the identity check for meshes #3575 did not
53
+ * touch.
54
+ *
55
+ * Normals get the SAME rotation matrix as positions, not its inverse
56
+ * transpose — correct for a rigid (orthogonal) rotation, which is what the
57
+ * server's per-vertex residual check (`RECOMPOSITION_TOLERANCE_M`) verifies
58
+ * before emitting a non-identity row. A non-uniform scale baked into the
59
+ * source `IfcCartesianTransformationOperator` would need the inverse
60
+ * transpose for normals specifically; this matches the convention the
61
+ * existing GPU-instancing wire format (`ifc_lite_geometry::instancing`)
62
+ * already uses, so it is not a new approximation this fix introduces.
63
+ *
64
+ * `isIdentityRow` already refuses to call a row with a NaN cell "identity"
65
+ * (`NaN !== 1` is true, so the row fails the comparison and falls through
66
+ * here) — a NaN can never silently render as an unrotated placement. But
67
+ * multiplying it through `rotateTriplets` would still corrupt every vertex
68
+ * of THIS instance to NaN, uncontrolled: no error, a mesh that may vanish
69
+ * or wreck a shared bounding-box computation with no signal why. A row that
70
+ * isn't finite is malformed wire data — the same class of fault
71
+ * `buildMeshesFromOptimizedTables` already fails loudly on for a bad
72
+ * mesh/material index — so it throws here too, instead of writing NaN
73
+ * silently into the buffer.
74
+ */
75
+ export function applyInstanceRotation(positions, normals, rot, index) {
76
+ if (isIdentityRow(rot, index))
77
+ return;
78
+ const r = rot.map((col) => col[index]);
79
+ if (!r.every(Number.isFinite)) {
80
+ throw new Error(`Malformed Parquet geometry: non-finite rotation value for row ${index} (rot=[${r.join(', ')}])`);
81
+ }
82
+ rotateTriplets(positions, r);
83
+ if (normals)
84
+ rotateTriplets(normals, r);
85
+ }
86
+ function rotateTriplets(buf, r) {
87
+ for (let v = 0; v < buf.length; v += 3) {
88
+ const x = buf[v];
89
+ const y = buf[v + 1];
90
+ const z = buf[v + 2];
91
+ buf[v] = r[0] * x + r[1] * y + r[2] * z;
92
+ buf[v + 1] = r[3] * x + r[4] * y + r[5] * z;
93
+ buf[v + 2] = r[6] * x + r[7] * y + r[8] * z;
94
+ }
95
+ }
96
+ //# sourceMappingURL=parquet-rotation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parquet-rotation.js","sourceRoot":"","sources":["../src/parquet-rotation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { ParquetBatch, ParquetStreamResult } from './types.js';
2
+ /**
3
+ * Raised when an SSE parse stream ends with no terminal event.
4
+ *
5
+ * Shared by `parseStream` and the Parquet stream reader below so the two
6
+ * cannot drift apart by an article. The wording has to hold on BOTH paths,
7
+ * which it does: this reader throws on an `error` event at its own
8
+ * `case 'error'`, so it can only reach its `!stats || !metadata` check in the
9
+ * same state `parseStream`'s `!terminated` describes, the stream stopped
10
+ * without ever saying why. Not re-exported from the package index: it is an
11
+ * internal guarantee about two call sites, not part of the published surface.
12
+ */
13
+ export declare const STREAM_ENDED_WITHOUT_TERMINAL_EVENT = "Stream ended without a complete event (connection dropped or the server failed mid-parse)";
14
+ /**
15
+ * Drain a `parquet-stream` SSE response, decoding each batch and handing it to
16
+ * `onBatch` as it arrives.
17
+ *
18
+ * @param response - An OK response whose body is the SSE stream.
19
+ * @param onBatch - Called once per decoded batch, for immediate rendering.
20
+ * @param startedAt - `performance.now()` at request time, for the timing log.
21
+ */
22
+ export declare function consumeParquetStream(response: Response, onBatch: (batch: ParquetBatch) => void, startedAt: number): Promise<ParquetStreamResult>;
23
+ //# sourceMappingURL=parquet-stream-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parquet-stream-events.d.ts","sourceRoot":"","sources":["../src/parquet-stream-events.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAEV,YAAY,EAEZ,mBAAmB,EAGpB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mCAAmC,8FAC6C,CAAC;AAE9F;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,EACtC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,mBAAmB,CAAC,CA4G9B"}
@@ -0,0 +1,133 @@
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
+ * Reading the SSE body of `POST /api/v1/parse/parquet-stream`.
6
+ *
7
+ * Its own module because there are now TWO requests that answer with that
8
+ * stream and they must be consumed identically (issue #3901): the hash-only
9
+ * probe, which sends no file at all and replays from cache, and the multipart
10
+ * upload it falls back to. The server guarantees a hit replays the same events
11
+ * a live parse emits; duplicating this loop per call site is how that
12
+ * guarantee stops being observable on the client.
13
+ */
14
+ import { decodeParquetGeometry } from './parquet-decoder.js';
15
+ /**
16
+ * Raised when an SSE parse stream ends with no terminal event.
17
+ *
18
+ * Shared by `parseStream` and the Parquet stream reader below so the two
19
+ * cannot drift apart by an article. The wording has to hold on BOTH paths,
20
+ * which it does: this reader throws on an `error` event at its own
21
+ * `case 'error'`, so it can only reach its `!stats || !metadata` check in the
22
+ * same state `parseStream`'s `!terminated` describes, the stream stopped
23
+ * without ever saying why. Not re-exported from the package index: it is an
24
+ * internal guarantee about two call sites, not part of the published surface.
25
+ */
26
+ export const STREAM_ENDED_WITHOUT_TERMINAL_EVENT = 'Stream ended without a complete event (connection dropped or the server failed mid-parse)';
27
+ /**
28
+ * Drain a `parquet-stream` SSE response, decoding each batch and handing it to
29
+ * `onBatch` as it arrives.
30
+ *
31
+ * @param response - An OK response whose body is the SSE stream.
32
+ * @param onBatch - Called once per decoded batch, for immediate rendering.
33
+ * @param startedAt - `performance.now()` at request time, for the timing log.
34
+ */
35
+ export async function consumeParquetStream(response, onBatch, startedAt) {
36
+ if (!response.body) {
37
+ throw new Error('No response body for streaming');
38
+ }
39
+ const reader = response.body.getReader();
40
+ const decoder = new TextDecoder();
41
+ let buffer = '';
42
+ let cache_key = '';
43
+ let total_meshes = 0;
44
+ let stats = null;
45
+ let metadata = null;
46
+ let symbolic_data;
47
+ // A thrown 'error' event (or any other exception mid-loop) must not
48
+ // leave the reader locked on `response.body` — mirrors the try/finally
49
+ // `parseStream` already uses for the same SSE-reading shape.
50
+ try {
51
+ while (true) {
52
+ const { done, value } = await reader.read();
53
+ if (done)
54
+ break;
55
+ buffer += decoder.decode(value, { stream: true });
56
+ // Process complete SSE events
57
+ const lines = buffer.split('\n');
58
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
59
+ for (const line of lines) {
60
+ if (!line.startsWith('data:'))
61
+ continue;
62
+ const jsonStr = line.slice(5).trim();
63
+ if (!jsonStr)
64
+ continue;
65
+ try {
66
+ const event = JSON.parse(jsonStr);
67
+ switch (event.type) {
68
+ case 'start':
69
+ cache_key = event.cache_key;
70
+ console.log(`[client] Stream started: ${event.total_estimate} entities, cache_key: ${cache_key.substring(0, 16)}...`);
71
+ break;
72
+ case 'progress':
73
+ // Progress events can be used for UI feedback
74
+ break;
75
+ case 'batch': {
76
+ const decodeStart = performance.now();
77
+ // Decode base64 Parquet data
78
+ const binaryStr = atob(event.data);
79
+ const bytes = new Uint8Array(binaryStr.length);
80
+ for (let i = 0; i < binaryStr.length; i++) {
81
+ bytes[i] = binaryStr.charCodeAt(i);
82
+ }
83
+ // Decode Parquet to meshes
84
+ const meshes = await decodeParquetGeometry(bytes.buffer);
85
+ const decodeTime = performance.now() - decodeStart;
86
+ total_meshes += meshes.length;
87
+ console.log(`[client] Batch #${event.batch_number}: ${meshes.length} meshes, decode: ${decodeTime.toFixed(0)}ms`);
88
+ // Call the batch callback for immediate rendering
89
+ onBatch({
90
+ meshes,
91
+ batch_number: event.batch_number,
92
+ decode_time_ms: decodeTime,
93
+ });
94
+ break;
95
+ }
96
+ case 'complete': {
97
+ stats = event.stats;
98
+ metadata = event.metadata;
99
+ symbolic_data = event.symbolic_data;
100
+ const totalTime = performance.now() - startedAt;
101
+ console.log(`[client] Stream complete: ${total_meshes} meshes in ${totalTime.toFixed(0)}ms`);
102
+ break;
103
+ }
104
+ case 'error':
105
+ throw new Error(`Stream error: ${event.message}`);
106
+ }
107
+ }
108
+ catch (e) {
109
+ if (e instanceof SyntaxError) {
110
+ console.warn('[client] Failed to parse SSE event:', jsonStr);
111
+ }
112
+ else {
113
+ throw e;
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ finally {
120
+ reader.releaseLock();
121
+ }
122
+ if (!stats || !metadata) {
123
+ throw new Error(STREAM_ENDED_WITHOUT_TERMINAL_EVENT);
124
+ }
125
+ return {
126
+ cache_key,
127
+ total_meshes,
128
+ stats,
129
+ metadata,
130
+ symbolic_data,
131
+ };
132
+ }
133
+ //# sourceMappingURL=parquet-stream-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parquet-stream-events.js","sourceRoot":"","sources":["../src/parquet-stream-events.ts"],"names":[],"mappings":"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"}
@@ -24,10 +24,19 @@ export interface ArrowTableLike {
24
24
  getChild(name: string): ArrowColumnLike | null | undefined;
25
25
  }
26
26
  /**
27
- * Rebuild `MeshData[]` from the standard (non-instanced) three-table layout.
27
+ * Rebuild `MeshData[]` from the standard three-table layout.
28
28
  *
29
29
  * Positions/normals are already Y-up metres — the server applies the axis swap
30
30
  * once, in `services::axis` (issue #1841), so every transport agrees.
31
+ *
32
+ * "Non-instanced" until `-parquet-v6` (issue #3888): several mesh rows can now
33
+ * name the SAME `vertex_start`/`index_start` block, each placed by its own
34
+ * `origin_x/y/z` plus a `rot0..rot8` rotation (`world = origin + R * p`) — the
35
+ * rotation-aware sharing the optimized transport has carried since #3575. Two
36
+ * things follow, and both are what keeps a `-parquet-v5` blob decoding here
37
+ * unchanged: the rotation columns are ABSENT on v5, and absent means identity;
38
+ * and `origin` was zero on every v5 flat row, so folding it in was a no-op
39
+ * there and is load-bearing here.
31
40
  */
32
41
  export declare function buildMeshesFromTables(meshArrow: ArrowTableLike, vertexArrow: ArrowTableLike, indexArrow: ArrowTableLike): MeshData[];
33
42
  /** Tables + header flags of the optimized (instanced, quantized) layout. */
@@ -41,6 +50,7 @@ export interface OptimizedTables {
41
50
  hasNormals: boolean;
42
51
  /** Quantization divisor: metres = quantized / vertexMultiplier. */
43
52
  vertexMultiplier: number;
53
+ wireVersion: 2 | 3;
44
54
  }
45
55
  /**
46
56
  * Rebuild `MeshData[]` from the optimized instanced layout.
@@ -1 +1 @@
1
- {"version":3,"file":"parquet-tables.d.ts","sourceRoot":"","sources":["../src/parquet-tables.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;CAC5D;AAgDD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,cAAc,EACzB,WAAW,EAAE,cAAc,EAC3B,UAAU,EAAE,cAAc,GACzB,QAAQ,EAAE,CAmIZ;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,cAAc,CAAC;IAC9B,SAAS,EAAE,cAAc,CAAC;IAC1B,aAAa,EAAE,cAAc,CAAC;IAC9B,WAAW,EAAE,cAAc,CAAC;IAC5B,UAAU,EAAE,cAAc,CAAC;IAC3B,+EAA+E;IAC/E,UAAU,EAAE,OAAO,CAAC;IACpB,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,eAAe,GAAG,QAAQ,EAAE,CAyKlF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,GAC9B,YAAY,CA0Cd"}
1
+ {"version":3,"file":"parquet-tables.d.ts","sourceRoot":"","sources":["../src/parquet-tables.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;IAC7B,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;CAC5D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,cAAc,EACzB,WAAW,EAAE,cAAc,EAC3B,UAAU,EAAE,cAAc,GACzB,QAAQ,EAAE,CAyIZ;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,cAAc,CAAC;IAC9B,SAAS,EAAE,cAAc,CAAC;IAC1B,aAAa,EAAE,cAAc,CAAC;IAC9B,WAAW,EAAE,cAAc,CAAC;IAC5B,UAAU,EAAE,cAAc,CAAC;IAC3B,+EAA+E;IAC/E,UAAU,EAAE,OAAO,CAAC;IACpB,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,eAAe,GAAG,QAAQ,EAAE,CAiLlF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,YAAY,EACvB,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,GAC9B,YAAY,CA0Cd"}
@@ -1,44 +1,22 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { meshColumns, numericColumn, readSourceId } from './parquet-columns.js';
4
+ import { meshColumns, numericColumn, transformFields } from './parquet-columns.js';
5
+ import { applyInstanceRotation, readRotationColumns } from './parquet-rotation.js';
5
6
  /**
6
- * The canonical per-mesh transform metadata, spread into the `MeshData` literal.
7
- *
8
- * `origin` is omitted when the frame IS the world origin and `geometry_class`
9
- * when it is 0 (occurrence), so a world-baked mesh decodes to exactly the shape
10
- * it had before these columns existed — and to the same shape on every
11
- * transport (JSON, standard Parquet, optimized Parquet).
12
- */
13
- function transformFields(index, cols) {
14
- // A usable column can still carry a non-finite VALUE at this row. `||` alone
15
- // misses it: an all-NaN triplet is already dropped, but a PARTIAL one
16
- // (`[NaN, 5, 0]`) is truthy and the NaN would poison bounds math. Not a
17
- // throw -- this file throws only for STRUCTURAL malformation.
18
- const ox = cols.originX?.[index];
19
- const oy = cols.originY?.[index];
20
- const oz = cols.originZ?.[index];
21
- const originFinite = Number.isFinite(ox) && Number.isFinite(oy) && Number.isFinite(oz);
22
- const origin = originFinite && (ox || oy || oz) ? [ox, oy, oz] : undefined;
23
- const geometry_class = cols.geometryClass?.[index] || undefined;
24
- // Sentinel, not null (#3215): a nullable column's values buffer is undefined
25
- // at null rows and parquet-wasm 0.7.x leaks the NEIGHBOURING row's id into it
26
- // -- a material-less mesh decoded as `material_id: 902`, a real-looking id
27
- // for another entity. Non-nullable means no validity bitmap to leak.
28
- const geometry_item_id = readSourceId(cols.geometryItemId, index);
29
- const material_id = readSourceId(cols.materialId, index);
30
- return {
31
- ...(origin ? { origin } : {}),
32
- ...(geometry_class ? { geometry_class } : {}),
33
- ...(geometry_item_id ? { geometry_item_id } : {}),
34
- ...(material_id ? { material_id } : {}),
35
- };
36
- }
37
- /**
38
- * Rebuild `MeshData[]` from the standard (non-instanced) three-table layout.
7
+ * Rebuild `MeshData[]` from the standard three-table layout.
39
8
  *
40
9
  * Positions/normals are already Y-up metres — the server applies the axis swap
41
10
  * once, in `services::axis` (issue #1841), so every transport agrees.
11
+ *
12
+ * "Non-instanced" until `-parquet-v6` (issue #3888): several mesh rows can now
13
+ * name the SAME `vertex_start`/`index_start` block, each placed by its own
14
+ * `origin_x/y/z` plus a `rot0..rot8` rotation (`world = origin + R * p`) — the
15
+ * rotation-aware sharing the optimized transport has carried since #3575. Two
16
+ * things follow, and both are what keeps a `-parquet-v5` blob decoding here
17
+ * unchanged: the rotation columns are ABSENT on v5, and absent means identity;
18
+ * and `origin` was zero on every v5 flat row, so folding it in was a no-op
19
+ * there and is load-bearing here.
42
20
  */
43
21
  export function buildMeshesFromTables(meshArrow, vertexArrow, indexArrow) {
44
22
  // Extract columns from mesh table
@@ -91,6 +69,10 @@ export function buildMeshesFromTables(meshArrow, vertexArrow, indexArrow) {
91
69
  // from servers predating them, where origin defaults to [0,0,0] and the
92
70
  // source ids simply do not appear.
93
71
  const cols = meshColumns(meshArrow, meshCount);
72
+ // Absent on every pre-#3888 payload. The flat transport has no version byte
73
+ // to tell a truncated v6 from a genuine v5, so absence reads as the identity
74
+ // it is on a v5 blob (see `readRotationColumns`).
75
+ const rotationCols = readRotationColumns(meshArrow, meshCount, 'identity');
94
76
  const meshes = new Array(meshCount);
95
77
  // Only consume the additive origin/geometry_class columns when all three
96
78
  // origin components are present AND parallel to the mesh rows — a short or
@@ -112,10 +94,8 @@ export function buildMeshesFromTables(meshArrow, vertexArrow, indexArrow) {
112
94
  `(vertexStart=${vertexStart}, vertexCount=${vertexCount}, vertices=${posX.length}; ` +
113
95
  `indexStart=${indexStart}, indexCount=${indexCount}, triangles=${idx0.length})`);
114
96
  }
115
- // Reconstruct interleaved positions from columnar format
116
- // OPTIMIZATION: Z-up to Y-up transform is now done server-side
117
- // Server already transforms: X stays same, new Y = old Z, new Z = -old Y
118
- // So we just copy directly without per-vertex transformation
97
+ // Reconstruct interleaved positions from columnar format. The server
98
+ // already applies the Z-up -> Y-up swap, so this just copies through.
119
99
  const positions = new Float32Array(vertexCount * 3);
120
100
  for (let v = 0; v < vertexCount; v++) {
121
101
  const srcIdx = vertexStart + v;
@@ -123,7 +103,7 @@ export function buildMeshesFromTables(meshArrow, vertexArrow, indexArrow) {
123
103
  positions[v * 3 + 1] = posY[srcIdx];
124
104
  positions[v * 3 + 2] = posZ[srcIdx];
125
105
  }
126
- // Reconstruct interleaved normals (also pre-transformed server-side)
106
+ // Reconstruct interleaved normals (also pre-transformed server-side).
127
107
  const normals = new Float32Array(vertexCount * 3);
128
108
  for (let v = 0; v < vertexCount; v++) {
129
109
  const srcIdx = vertexStart + v;
@@ -131,9 +111,13 @@ export function buildMeshesFromTables(meshArrow, vertexArrow, indexArrow) {
131
111
  normals[v * 3 + 1] = normY[srcIdx];
132
112
  normals[v * 3 + 2] = normZ[srcIdx];
133
113
  }
134
- // Reconstruct triangle indices from columnar format
135
- const triangleCount = indexCount / 3;
136
- const triangleStart = indexStart / 3;
114
+ // Rotate the shared block onto THIS occurrence before `origin` translates
115
+ // it (`world = origin + R * p`). A no-op on an identity row, which is every
116
+ // row of a v5 payload and every unshared row of a v6 one.
117
+ if (rotationCols)
118
+ applyInstanceRotation(positions, normals, rotationCols, i);
119
+ // Reconstruct triangle indices from columnar format.
120
+ const triangleCount = indexCount / 3, triangleStart = indexStart / 3;
137
121
  const indices = new Uint32Array(indexCount);
138
122
  for (let t = 0; t < triangleCount; t++) {
139
123
  const srcIdx = triangleStart + t;
@@ -222,6 +206,9 @@ export function buildMeshesFromOptimizedTables(tables) {
222
206
  const cols = meshColumns(instanceArrow, instanceCount);
223
207
  const meshes = new Array(instanceCount);
224
208
  const dequantMultiplier = 1.0 / vertexMultiplier;
209
+ // Wire version 3 states the columns are there (#3575), so absence is
210
+ // truncated data, not an older payload; version 2 predates them.
211
+ const rotationCols = readRotationColumns(instanceArrow, instanceCount, tables.wireVersion === 3 ? 'throw' : 'identity');
225
212
  // Additive per-instance origin/geometry_class columns (issue #1841): consume
226
213
  // only when present AND parallel to the instance rows.
227
214
  for (let i = 0; i < instanceCount; i++) {
@@ -249,9 +236,7 @@ export function buildMeshesFromOptimizedTables(tables) {
249
236
  `vertices=${vertexX.length}; indexOffset=${indexOffset}, indexCount=${indexCount}, ` +
250
237
  `indices=${indices.length})`);
251
238
  }
252
- // Dequantize and reconstruct positions
253
- // OPTIMIZATION: Z-up to Y-up transform is now done server-side for optimized format too
254
- // Server already transforms before quantization, so we just dequantize directly
239
+ // Dequantize (the server already applied Z-up -> Y-up before quantizing).
255
240
  const positions = new Float32Array(vertexCount * 3);
256
241
  for (let v = 0; v < vertexCount; v++) {
257
242
  const srcIdx = vertexOffset + v;
@@ -264,25 +249,26 @@ export function buildMeshesFromOptimizedTables(tables) {
264
249
  for (let j = 0; j < indexCount; j++) {
265
250
  meshIndicesArray[j] = indices[indexOffset + j];
266
251
  }
267
- // Reconstruct normals (pre-transformed server-side, or compute if not present)
268
- let normals;
252
+ // Server-provided normals, read BEFORE rotating (#3575) so a flat
253
+ // recompute below, when absent, starts from already-rotated positions.
254
+ let providedNormals;
269
255
  if (hasNormals && normalX && normalY && normalZ) {
270
- normals = new Float32Array(vertexCount * 3);
256
+ providedNormals = new Float32Array(vertexCount * 3);
271
257
  for (let v = 0; v < vertexCount; v++) {
272
258
  const srcIdx = vertexOffset + v;
273
- normals[v * 3] = normalX[srcIdx];
274
- normals[v * 3 + 1] = normalY[srcIdx];
275
- normals[v * 3 + 2] = normalZ[srcIdx];
259
+ providedNormals[v * 3] = normalX[srcIdx];
260
+ providedNormals[v * 3 + 1] = normalY[srcIdx];
261
+ providedNormals[v * 3 + 2] = normalZ[srcIdx];
276
262
  }
277
263
  }
278
- else {
279
- // Compute flat normals from triangle faces
280
- normals = computeFlatNormals(positions, meshIndicesArray);
281
- }
282
- // Convert byte colors to float [0-1]. `positions` are the TEMPLATE-local
283
- // vertices; `origin` (not baked in, to keep f32 precision at building scale)
284
- // carries the per-instance placement so the renderer reconstructs
285
- // world = origin + position — the same contract as the standard path.
264
+ // Rotate into this instance's own frame (#3575); no-op for identity rows.
265
+ if (rotationCols)
266
+ applyInstanceRotation(positions, providedNormals, rotationCols, i);
267
+ const normals = providedNormals ?? computeFlatNormals(positions, meshIndicesArray);
268
+ // Convert byte colors to float [0-1]. `origin` (not baked in, for f32
269
+ // precision at building scale) carries the remaining translation: world
270
+ // = origin + position, same contract as the standard path (#1841),
271
+ // extended with the rotation already applied to `positions` above.
286
272
  meshes[i] = {
287
273
  express_id: entityIds[i],
288
274
  ifc_type: ifcTypes?.get(i) ?? 'Unknown',