Skip to content

Commit 50895fb

Browse files
authored
feat(geometry,wasm,cache): representation-item identity crosses the boundary, disjoint from material ids (#3199) (#3210)
* feat(geometry,wasm,cache): representation-item identity crosses the boundary, disjoint from material ids (#3199) The router already kept each representation item's STEP id, and it already reached the server REST payload. `MeshDataJs::from_mesh_data` did not copy it, so the browser never saw it and a host could not drill from a rendered piece to the entity that produced it -- an `IfcWindow`'s pane or frame -- and navigate there in source (#2985). AND IT WAS WRONG FOR LAYERED WALLS. `router/layers.rs` pushes `SubMesh::new(layer.material_id, slab)`, so for material-layered walls and slabs the same field carried an `IfcMaterial` id. Following it to source landed on the wrong entity, with nothing to tell the caller which kind of id it had. TWO DISJOINT FIELDS, never both: `geometry_item_id` is always a representation item, `material_layer_id` is always an `IfcMaterial`. `with_style_metadata` takes the DISCRIMINATOR rather than the destination field, so a caller cannot put a material id in the wrong one by picking the wrong setter -- a two-setter API would leave the confusion one typo away. THE DISCRIMINATOR LIVES ON `SubMeshCollection`, set where the layered slabs are built, because it is a property of how the collection was built and is uniform across it. `geometry_class == GEOM_CLASS_LAYER_SLICE` cannot answer it, and this was checked rather than assumed: that class is stamped in `element.rs` from `is_material_layer_sliceable`, a static material-index check made BEFORE the geometry runs, while `try_layered_sub_meshes` can still return `None` at several points and fall through to the representation-item path. Rep-item ids can carry class 3. The voided rebuild in `voids/mod.rs` INHERITS the flag rather than hard-coding rep-item. It is false there today, but hard-coding would let a future layered producer be silently relabelled -- the exact confusion this removes. CACHE FORMAT v14, because `sections/geometry.ts` writes `MeshData` field by field and a cache-restored session would otherwise lose the identity. Bumped rather than read leniently: the record is positional, so a v13 reader handed a v14 record would take the origin's first f64 out of the two new u32 and every field after it would be garbage. A v14 reader still reads v13 records -- the fields are version-gated and degrade to `undefined`, the same state the runtime uses where identity is genuinely merged away, so an old cache degrades to "unknown" and never to a WRONG id. THE ABSENT SENTINEL IS 0xFFFFFFFF, NOT 0, and a test caught me getting this wrong. `layers.rs` uses `0` for a layer with no material -- an air gap -- so 0 is a REAL value that must round trip. The first version treated it as absent, which is the valid-but-falsy trap and would have dropped exactly the layers hardest to notice missing. Every read now compares against the sentinel or `undefined`; nothing tests these ids for truthiness. MODULE SIZE. Five Rust rows are raised with a per-row justification and the digest re-pinned, because adding a field to a boundary struct necessarily grows the boundary file and `zero_copy/mesh.rs` cannot absorb 2 fields + 2 getters + a paired setter + propagation at three clone sites within its budget. The TS allowlist forbids raises outright, so `types.ts` was SPLIT instead: the self-contained symbolic-representation family moves to `symbolic-types.ts` (it is re-exported, so the public surface is unchanged), taking types.ts from 459 to 381 and letting its row be DELETED -- which is what the gate asks for. The worker was brought back to exactly its budget. VERIFIED AT THE REAL BOUNDARY, not by reading: duplex.ifc through processGeometryBatch: 436 meshes with geometryItemId, 186 with materialLayerId, BOTH = 0, neither = 0 Paired probe: conflating the two fields (setting both, as before the fix) turns the new contract test red with "186 meshes carried BOTH". The contract test asserts non-vacuity in both directions -- some mesh must carry each field -- because "never both" is satisfied trivially by never setting either. One instrument bug found on the way: `scripts/lib/mesh-via-prepass.mjs` mirrors `convertMeshCollectionToBatch` field by field, so it silently dropped the new ids and my first probe read zeros through it. A field the real converter carries and that facade drops is invisible to every script that reads through it. 2162 Rust tests, clippy -D warnings clean, 621 script tests, 106 cache tests, 73 wasm-contract assertions, typecheck clean. Closes #3199 * fix(3199): never emit a source id of 0, mirror the ids on the REST wire, and rename material_layer_id (#3210 review) Review round on #3210. The three defects below were raised by the peer session that filed and claimed #3199 and had built its own branch for it; the rest came from Codex, CodeRabbit and a /simplify + /code-review pass. Three of these are defects in the change as first written. A SOURCE ID OF 0 IS NOT AN ENTITY, and the first version shipped one. `IfcMaterialLayer.Material` is OPTIONAL, so `material_layer_index.rs` reads it as `get_ref(0).unwrap_or(0)` and an air gap arrives with `material_id == 0`. That is the DECODER'S ABSENCE SENTINEL, not an id -- STEP instance names start at #1. Storing it made the slab claim to be a slice of `IfcMaterial #0`, and a host following it landed on nothing, which is the exact defect #3199 exists to remove, one field over. MEASURED at the real boundary: 12 slabs of duplex.ifc reported `IfcMaterial #0` before the filter. `with_style_metadata` now maps 0 to `None` on BOTH fields, at the setter rather than at each producer, so the next producer does not have to remember. This makes my earlier reasoning WRONG, not just incomplete: I had argued 0 was a real value that must round trip, and built the cache's 0xFFFFFFFF sentinel on that. The sentinel is still right -- an absence marker the domain can produce is one upstream change from being wrong again -- but its stated REASON was false in three places (the writer, the v14 ledger, and a cache test citing a measurement the same PR had invalidated). All three now tell one story. THE REST MIRROR was untouched. `apps/server/src/types/mesh.rs` re-exports the very struct that changed, so the server had started sending `material_id` while `packages/server-client/src/types.ts` declared no such field and `convertServerMesh` dropped it -- the REST path's identity died at the converter. Both are fixed, with a Rust test pinning the wire NAMES and the ABSENT ENCODING: `skip_serializing_if` makes `None` a MISSING KEY, and a TS optional means absent rather than nullable, so the two agree only while that attribute stays on. RENAMED `material_layer_id` -> `material_id`. It holds an `IfcMaterial` express id, not an `IfcMaterialLayer` one, and `IfcMaterialLayer` is a real and different entity -- so the old name did not merely abbreviate, it pointed at the wrong schema type. AGENTS.md: "New surface still gets the EXPRESS name, once." Cheaper now than after it ships as public API on four packages. TESTS THAT COULD NOT FAIL, found by mutation rather than by reading: - Deleting BOTH spreads from `convertMeshCollectionToBatch` left packages/geometry at 353/353 green. That converter is the only path by which the viewer's main thread sees the ids, and every other #3199 test reads the raw MeshCollection, which is upstream of it. Three tests added; they fail with the spreads removed. - The block header claimed "one test below reads through the facade on purpose" and none did, so the `mesh-via-prepass.mjs` fix shipped with no coverage -- the facade being the thing that actually went wrong. Added, and it cross-checks against the raw collection rather than a fixed count, so it cannot go vacuous when the fixture changes. - `voided_submesh_entry_reports_layer_ids_as_material_ids` claimed to cover the sub-mesh-by-sub-mesh rebuild. It does not: a layered wall returns at the `try_layered_sub_meshes` early exit and never reaches it, and replacing the inheritance with a hard-coded `false` leaves everything green. The comment now says so. The code stays an inheritance deliberately; it is defensive and honestly untested rather than dishonestly claimed. MODULE SIZE, redone: the TS allowlist now has NO raises at all. Splitting the self-contained symbolic wire shapes out of `server-client/src/types.ts` (the same split already made in `@ifc-lite/geometry`, re-exported so the surface is unchanged) ratchets that row 772 -> 627, and `packages/geometry/src/types.ts`'s row is deleted. Both directions the gate asks for, neither a raise. STATED, NOT SILENTLY SHIPPED -- three things this PR does not fix: #3211 five style lookups in element.rs still read a sub-mesh id as a representation item on the layered path; safe only because express ids are unique per file, which is an accident doing load-bearing work #3215 the Parquet transport carries neither id, so drill-to-source works on JSON and not on the binary path #3216 npm drives the Cargo version, so this change -- breaking for the published `ifc-lite-processing` crate -- ships as a minor bump, with no cargo-semver-checks anywhere to catch it BYTE IDENTITY, verified independently rather than inherited: duplex.ifc through scripts/perf/ab.sh gives fingerprintDrift=false, 486/486 meshes, 52053/52053 vertices, 30264/30264 triangles. motif-ifc-ad measured the same on schependomlaan (6057/423949/257790). Neither of us has a trustworthy TIMING verdict: ab.sh refused one both times with tooNoisy, this machine running five Claude sessions at load ~10. 2164 Rust tests, clippy -D warnings clean, 73 wasm-contract assertions, 106 cache tests, 356 geometry tests, viewer shard 2 at 1374, typecheck and lint clean, module-size gate green.
1 parent 0e22478 commit 50895fb

34 files changed

Lines changed: 1767 additions & 306 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@ifc-lite/geometry": minor
3+
"@ifc-lite/wasm": minor
4+
"@ifc-lite/cache": minor
5+
"@ifc-lite/server-client": minor
6+
---
7+
8+
Carry representation-item identity across the wasm boundary, and stop delivering material ids in the same field.
9+
10+
`MeshData` gains two DISJOINT fields. `geometryItemId` is always the `IfcRepresentationItem` a mesh was tessellated from, so a host can drill from a rendered piece into an `IfcWindow`'s pane or frame and navigate to that entity in source. `materialId` is always the `IfcMaterial` whose layer a mesh slices. Never both — a consumer that ignores the distinction still cannot read one as the other.
11+
12+
The router already kept each item's STEP id and it already reached the server REST payload; `MeshDataJs::from_mesh_data` did not copy it, so the browser never saw it. And for material-layered walls and slabs the same field carried the layer's `IfcMaterial` id, so following it to source landed on the wrong entity with nothing to warn the caller.
13+
14+
`geometryClass === 3` cannot discriminate the two: it is stamped from a static material-index check made before the geometry runs, while the layered path can bail at runtime and emit representation-item submeshes under that class. The discriminator therefore lives on `SubMeshCollection`, set where the layered slabs are built.
15+
16+
Neither field is ever `0`. `IfcMaterialLayer.Material` is optional, so an air gap reaches the mesher as `material_id 0` — that is the decoder's "no reference" sentinel, not an entity, and STEP instance names start at `#1`. Twelve slabs of `duplex.ifc` reported `IfcMaterial #0` before this was filtered at the setter. An air-gap slab is still meshed; it simply reports no material.
17+
18+
Both fields cross the boundary, both wasm converters carry them, the REST wire shape and `convertServerMesh` carry them, and the cache format gains them at v14 — without that, a cache-restored session silently lost the identity.
19+
20+
BREAKING FOR THE RUST CRATE, and this changeset cannot express it. `ifc-lite-processing` is published to crates.io (`scripts/release-crates.mjs`), `MeshData` gains a public field, and `with_style_metadata` goes from two arguments to three — both break a struct literal or a call downstream, which `rust/export/src/usd/tests.rs` demonstrates in-repo. `scripts/sync-versions.js` derives the Cargo workspace version from the highest npm package version, so a `minor` here ships 6.0.0 → 6.1.0 and a consumer pinned to `ifc-lite-processing = "6"` breaks on `cargo update`. Nothing gates this: there is no `cargo-semver-checks` anywhere in the repo.

apps/viewer/src/utils/coldGeometryProvider.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { describe, it } from 'node:test';
66
import assert from 'node:assert';
77
import type { CoordinateInfo, MeshData } from '@ifc-lite/geometry';
8-
import { buildGeometrySectionV13, openGeometryChunksV13 } from '@ifc-lite/cache';
8+
import { buildGeometrySectionV13, openGeometryChunksV13, FORMAT_VERSION } from '@ifc-lite/cache';
99
import { makeColdGeometryProvider } from './coldGeometryProvider.js';
1010

1111
const coordInfo: CoordinateInfo = {
@@ -32,7 +32,15 @@ async function makeFixture() {
3232
// Two far-apart cells → at least two chunks with disjoint AABBs.
3333
const meshes = [mesh(1, [1, 1, 1]), mesh(2, [2, 2, 2]), mesh(3, [1000, 0, 0])];
3434
const section = await buildGeometrySectionV13(meshes, coordInfo);
35-
const open = openGeometryChunksV13(section, 0, 13);
35+
// The reader version is "whatever the writer just emitted", not a literal.
36+
// It was pinned to 13 and went stale at the #3199 bump: the writer gained 8
37+
// bytes per mesh record, a v13 reader skipped them, and every record after
38+
// the first was misaligned -- which `decodeGeometryChunk`'s exact-consumption
39+
// check caught, but only here, because the cache package's own suite does not
40+
// run this file. `buildGeometrySectionV13` takes NO version argument: it
41+
// always writes current, so any literal paired with it is a bet on the
42+
// format never moving.
43+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
3644
return { section, chunks: open.chunks };
3745
}
3846

@@ -43,7 +51,7 @@ describe('makeColdGeometryProvider', () => {
4351
source: section,
4452
geometrySectionOffset: 0,
4553
chunks,
46-
version: 13,
54+
version: FORMAT_VERSION,
4755
});
4856
const near = await provider.loadMeshesInBounds([0, 0, 0], [10, 10, 10]);
4957
assert.deepStrictEqual(near.map((m) => m.expressId).sort(), [1, 2]);
@@ -62,7 +70,7 @@ describe('makeColdGeometryProvider', () => {
6270
source: blob,
6371
geometrySectionOffset: 128,
6472
chunks,
65-
version: 13,
73+
version: FORMAT_VERSION,
6674
});
6775
const near = await provider.loadMeshesInBounds([0, 0, 0], [10, 10, 10]);
6876
assert.deepStrictEqual(near.map((m) => m.expressId).sort(), [1, 2]);

apps/viewer/src/utils/serverMesh.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,33 @@ test('keeps an origin that is zero on only some axes', () => {
5050
const out = convertServerMesh({ ...base, origin: [0, 0, -20] });
5151
assert.deepEqual(out.origin, [0, 0, -20]);
5252
});
53+
54+
test('forwards the representation-item id, so a REST-loaded pick can reach source (#3199)', () => {
55+
const out = convertServerMesh({ ...base, geometry_item_id: 1234 });
56+
assert.equal(out.geometryItemId, 1234);
57+
assert.equal('materialId' in out, false);
58+
});
59+
60+
test('forwards the material id, disjoint from the representation-item id (#3199)', () => {
61+
const out = convertServerMesh({ ...base, material_id: 3876 });
62+
assert.equal(out.materialId, 3876);
63+
assert.equal('geometryItemId' in out, false);
64+
});
65+
66+
test('omits both source ids when the server sent neither', () => {
67+
const out = convertServerMesh(base);
68+
assert.equal('geometryItemId' in out, false);
69+
assert.equal('materialId' in out, false);
70+
});
71+
72+
// The other two optionals in this converter are truthiness-gated on purpose: a
73+
// zero origin and class 0 are both the default, so carrying them would make the
74+
// same mesh decode differently per transport. A source id is not like that --
75+
// it is an express instance name, and this converter must not invent a rule
76+
// about which values are "real". The server already drops 0 (`#0` is not an
77+
// entity), so if one ever arrives the honest thing is to pass it through and
78+
// let the id-provenance tests catch it, not to silently swallow it here.
79+
test('does not truthiness-gate the source ids the way it gates origin and class', () => {
80+
const out = convertServerMesh({ ...base, geometry_item_id: 0 });
81+
assert.equal(out.geometryItemId, 0, 'a 0 id was dropped by a truthiness check');
82+
});

apps/viewer/src/utils/serverMesh.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,18 @@ export function convertServerMesh(m: ServerMeshData): MeshData {
3434
// make the same mesh decode to a different shape depending on transport.
3535
...(m.origin?.some((v) => v !== 0) ? { origin: m.origin } : {}),
3636
...(m.geometry_class ? { geometryClass: m.geometry_class } : {}),
37+
// The two DISJOINT source ids (#3199). Tested against `undefined`, never for
38+
// truthiness -- unlike the two optionals above, which are correctly
39+
// truthiness-gated because a zero origin and class 0 are both the default.
40+
//
41+
// The server never sends `0` for either id (`#0` is not a STEP instance
42+
// name; an air-gap layer is sent as absent), so this converter would be
43+
// gating on a value it can never see. That is exactly why it must NOT gate:
44+
// a converter enforcing a producer's invariant turns a future producer bug
45+
// into silent data loss here instead of a visible wrong id upstream, and
46+
// dropping an id is how the REST path keeps rendering identically while a
47+
// host loses the ability to drill from a picked piece to its source.
48+
...(m.geometry_item_id !== undefined ? { geometryItemId: m.geometry_item_id } : {}),
49+
...(m.material_id !== undefined ? { materialId: m.material_id } : {}),
3750
};
3851
}

packages/cache/src/sections/geometry-chunks.test.ts

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ import {
1313
inflateRaw,
1414
decodeGeometryChunk,
1515
} from './geometry-chunks.js';
16-
import { GeometryChunkFlags, type GeometryChunkInfo } from '../types.js';
16+
import { FORMAT_VERSION, GeometryChunkFlags, type GeometryChunkInfo } from '../types.js';
17+
18+
// These suites all read back what `buildGeometrySectionV13` just wrote, so the
19+
// reader version is "whatever the writer emits", not a literal. It was pinned
20+
// to 13 and went stale at the #3199 bump: the writer added 8 bytes per mesh
21+
// record, a v13 reader skipped them, and every record after the first was
22+
// misaligned. The on-disk version literal is pinned in header.test.ts, which
23+
// is where a silent bump should be caught.
1724

1825
const coordInfo = (overrides: Partial<CoordinateInfo> = {}): CoordinateInfo => ({
1926
originShift: { x: 1.5, y: -2.5, z: 1e6 },
@@ -131,7 +138,7 @@ describe('v13 geometry section round-trip', () => {
131138
it('round-trips meshes, counts and coordinateInfo (compressed)', async () => {
132139
const info = coordInfo({ wasmRtcOffset: { x: 1, y: 2, z: 3 }, buildingRotation: 0.25 });
133140
const section = await buildGeometrySectionV13(meshes, info);
134-
const result = await readGeometryV13(section, 0, 13);
141+
const result = await readGeometryV13(section, 0, FORMAT_VERSION);
135142
expectMeshesEqual(result.meshes, meshes);
136143
expect(result.totalVertices).toBe(12);
137144
expect(result.totalTriangles).toBe(4);
@@ -140,13 +147,13 @@ describe('v13 geometry section round-trip', () => {
140147

141148
it('round-trips with compression disabled', async () => {
142149
const section = await buildGeometrySectionV13(meshes, coordInfo(), { compress: false });
143-
const result = await readGeometryV13(section, 0, 13);
150+
const result = await readGeometryV13(section, 0, FORMAT_VERSION);
144151
expectMeshesEqual(result.meshes, meshes);
145152
});
146153

147154
it('exposes a directory with valid AABBs and per-chunk decode', async () => {
148155
const section = await buildGeometrySectionV13(meshes, coordInfo());
149-
const open = openGeometryChunksV13(section, 0, 13);
156+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
150157
expect(open.chunks.length).toBeGreaterThanOrEqual(2);
151158
let total = 0;
152159
for (let i = 0; i < open.chunks.length; i++) {
@@ -171,7 +178,7 @@ describe('v13 geometry section round-trip', () => {
171178
it('small chunks skip compression; a large repetitive chunk compresses', async () => {
172179
// Small: below the 64KiB floor → raw.
173180
const small = await buildGeometrySectionV13([mesh(1, [0, 0, 0])], coordInfo());
174-
const openSmall = openGeometryChunksV13(small, 0, 13);
181+
const openSmall = openGeometryChunksV13(small, 0, FORMAT_VERSION);
175182
expect(openSmall.chunks[0].flags & GeometryChunkFlags.DeflateRaw).toBe(0);
176183

177184
// Large + repetitive: one 100k-vertex mesh of zeros → compresses well.
@@ -181,7 +188,7 @@ describe('v13 geometry section round-trip', () => {
181188
indices: new Uint32Array(300_000),
182189
});
183190
const section = await buildGeometrySectionV13([big], coordInfo());
184-
const open = openGeometryChunksV13(section, 0, 13);
191+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
185192
expect(open.chunks[0].flags & GeometryChunkFlags.DeflateRaw).toBe(GeometryChunkFlags.DeflateRaw);
186193
expect(open.chunks[0].byteLength).toBeLessThan(open.chunks[0].uncompressedLength / 4);
187194
const decoded = await open.readChunk(0);
@@ -190,7 +197,7 @@ describe('v13 geometry section round-trip', () => {
190197

191198
it('handles empty mesh lists', async () => {
192199
const section = await buildGeometrySectionV13([], coordInfo());
193-
const result = await readGeometryV13(section, 0, 13);
200+
const result = await readGeometryV13(section, 0, FORMAT_VERSION);
194201
expect(result.meshes).toEqual([]);
195202
expect(result.totalVertices).toBe(0);
196203
});
@@ -223,19 +230,19 @@ describe('corrupt geometry chunk directory', () => {
223230
const meshes = [mesh(1, [0, 0, 0]), mesh(2, [5000, 5000, 5000])]; // forced into separate chunks
224231
const section = await buildGeometrySectionV13(meshes, coordInfo(), { compress: false });
225232
const bytes = new Uint8Array(section);
226-
const info0 = openGeometryChunksV13(section, 0, 13).chunks[0];
233+
const info0 = openGeometryChunksV13(section, 0, FORMAT_VERSION).chunks[0];
227234
const entry = findDirectoryEntry(bytes, info0.byteOffset, info0.byteLength);
228235
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
229236
dv.setUint32(entry + 28, info0.byteLength + 10_000, true); // byteLength: reach past the buffer end
230237

231-
expect(() => openGeometryChunksV13(section, 0, 13)).toThrow(/not contiguous/);
238+
expect(() => openGeometryChunksV13(section, 0, FORMAT_VERSION)).toThrow(/not contiguous/);
232239
});
233240

234241
it('rejects a chunk whose inflated byteLength/meshCount/uncompressedLength consistently absorb the next chunk', async () => {
235242
const meshes = [mesh(1, [0, 0, 0]), mesh(2, [5000, 5000, 5000])];
236243
const section = await buildGeometrySectionV13(meshes, coordInfo(), { compress: false });
237244
const bytes = new Uint8Array(section);
238-
const open = openGeometryChunksV13(section, 0, 13);
245+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
239246
expect(open.chunks.length).toBe(2); // control: the fixture really did split into 2 chunks
240247
const info0 = open.chunks[0];
241248
const info1 = open.chunks[1];
@@ -250,13 +257,13 @@ describe('corrupt geometry chunk directory', () => {
250257
dv.setUint32(entry + 32, absorbedLength, true); // uncompressedLength
251258
dv.setUint32(entry + 36, info0.meshCount + info1.meshCount, true); // meshCount
252259

253-
expect(() => openGeometryChunksV13(section, 0, 13)).toThrow(/not contiguous/);
260+
expect(() => openGeometryChunksV13(section, 0, FORMAT_VERSION)).toThrow(/not contiguous/);
254261
});
255262

256263
it('bounding control: a valid multi-chunk directory (ranges reaching exactly to the next chunk) still decodes', async () => {
257264
const meshes = [mesh(1, [0, 0, 0]), mesh(2, [5000, 5000, 5000])];
258265
const section = await buildGeometrySectionV13(meshes, coordInfo(), { compress: false });
259-
const open = openGeometryChunksV13(section, 0, 13);
266+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
260267
expect(open.chunks.length).toBe(2);
261268
const decoded0 = await open.readChunk(0);
262269
const decoded1 = await open.readChunk(1);
@@ -271,7 +278,7 @@ describe('corrupt geometry chunk directory', () => {
271278
const meshes = [mesh(1, [0, 0, 0]), mesh(2, [5000, 5000, 5000])];
272279
const section = await buildGeometrySectionV13(meshes, coordInfo(), { compress: false });
273280
const bytes = new Uint8Array(section);
274-
const open = openGeometryChunksV13(section, 0, 13);
281+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
275282
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
276283

277284
// Shift EVERY chunk's byteOffset by the same delta. Relative spacing —
@@ -282,7 +289,7 @@ describe('corrupt geometry chunk directory', () => {
282289
dv.setUint32(entry + 24, info.byteOffset + SHIFT, true);
283290
}
284291

285-
expect(() => openGeometryChunksV13(section, 0, 13)).toThrow(
292+
expect(() => openGeometryChunksV13(section, 0, FORMAT_VERSION)).toThrow(
286293
/chunk 0 byteOffset .* does not start at the end of the head/,
287294
);
288295
});
@@ -299,7 +306,7 @@ describe('corrupt geometry chunk directory', () => {
299306
const section = await buildGeometrySectionV13(meshes, coordInfo(), { compress: false });
300307
const bytes = new Uint8Array(section);
301308
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
302-
const open = openGeometryChunksV13(section, 0, 13);
309+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
303310
const info0 = open.chunks[0];
304311
const trueHeadLength = dv.getUint32(0, true);
305312
const entry = findDirectoryEntry(bytes, info0.byteOffset, info0.byteLength);
@@ -310,7 +317,7 @@ describe('corrupt geometry chunk directory', () => {
310317
dv.setUint32(0, forgedHeadLength, true);
311318
dv.setUint32(entry + 24, 4 + forgedHeadLength, true);
312319

313-
expect(() => openGeometryChunksV13(section, 0, 13)).toThrow(/headLength/);
320+
expect(() => openGeometryChunksV13(section, 0, FORMAT_VERSION)).toThrow(/headLength/);
314321
});
315322

316323
// `readChunk`'s own end-of-buffer check was unreachable from the tests
@@ -322,15 +329,15 @@ describe('corrupt geometry chunk directory', () => {
322329
const meshes = [mesh(1, [0, 0, 0]), mesh(2, [5000, 5000, 5000])];
323330
const section = await buildGeometrySectionV13(meshes, coordInfo(), { compress: false });
324331
const bytes = new Uint8Array(section);
325-
const open = openGeometryChunksV13(section, 0, 13);
332+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
326333
const last = open.chunks[open.chunks.length - 1];
327334
const entry = findDirectoryEntry(bytes, last.byteOffset, last.byteLength);
328335
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
329336
dv.setUint32(entry + 28, last.byteLength + 10_000, true);
330337

331338
// The directory still validates: chunk 0 is anchored, and the last chunk
332339
// has no successor for the contiguity loop to compare against.
333-
const reopened = openGeometryChunksV13(section, 0, 13);
340+
const reopened = openGeometryChunksV13(section, 0, FORMAT_VERSION);
334341
await expect(reopened.readChunk(open.chunks.length - 1)).rejects.toThrow(
335342
/exceeds buffer length/,
336343
);
@@ -348,7 +355,7 @@ describe('decodeGeometryChunk length verification', () => {
348355
it('throws when the stored bytes decode to a length that disagrees with the directory', async () => {
349356
const meshes = [mesh(1, [0, 0, 0])];
350357
const section = await buildGeometrySectionV13(meshes, coordInfo(), { compress: false });
351-
const open = openGeometryChunksV13(section, 0, 13);
358+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
352359
const realInfo = open.chunks[0];
353360
expect(realInfo.flags & GeometryChunkFlags.DeflateRaw).toBe(0); // uncompressed: raw === stored
354361

@@ -357,7 +364,7 @@ describe('decodeGeometryChunk length verification', () => {
357364

358365
// Directory claims one more byte than the record actually decodes to.
359366
const lyingInfo: GeometryChunkInfo = { ...realInfo, uncompressedLength: realInfo.uncompressedLength + 1 };
360-
await expect(decodeGeometryChunk(stored, lyingInfo, 13)).rejects.toThrow(
367+
await expect(decodeGeometryChunk(stored, lyingInfo, FORMAT_VERSION)).rejects.toThrow(
361368
/Invalid cache: chunk decoded to \d+ bytes, directory says \d+/,
362369
);
363370

@@ -366,7 +373,7 @@ describe('decodeGeometryChunk length verification', () => {
366373
// the decoded content itself (mesh count + expressId), not mere
367374
// truthiness: an empty array is also truthy and would satisfy a
368375
// `resolves.toBeTruthy()` check whether or not any mesh actually decoded.
369-
const decoded = await decodeGeometryChunk(stored, realInfo, 13);
376+
const decoded = await decodeGeometryChunk(stored, realInfo, FORMAT_VERSION);
370377
expectMeshesEqual(decoded, meshes);
371378
});
372379

@@ -383,7 +390,7 @@ describe('decodeGeometryChunk length verification', () => {
383390
it('throws when a mesh record under-consumes the declared chunk length (meshCount and uncompressedLength are both truthful)', async () => {
384391
const meshes = [mesh(1, [0, 0, 0])];
385392
const section = await buildGeometrySectionV13(meshes, coordInfo(), { compress: false });
386-
const open = openGeometryChunksV13(section, 0, 13);
393+
const open = openGeometryChunksV13(section, 0, FORMAT_VERSION);
387394
const realInfo = open.chunks[0];
388395
expect(realInfo.flags & GeometryChunkFlags.DeflateRaw).toBe(0); // uncompressed: raw === stored
389396
expect(realInfo.meshCount).toBe(1);
@@ -402,14 +409,14 @@ describe('decodeGeometryChunk length verification', () => {
402409
expect(realVertexCount).toBe(3);
403410
dv.setUint32(4, realVertexCount - 1, true);
404411

405-
await expect(decodeGeometryChunk(stored, realInfo, 13)).rejects.toThrow(
412+
await expect(decodeGeometryChunk(stored, realInfo, FORMAT_VERSION)).rejects.toThrow(
406413
/Invalid cache: chunk claims \d+ mesh record\(s\) but consumed \d+ of \d+ bytes/,
407414
);
408415

409416
// Control: restoring the true vertexCount round-trips fine — the throw
410417
// above is caused by the corruption, not some unrelated fixture bug.
411418
dv.setUint32(4, realVertexCount, true);
412-
const decoded = await decodeGeometryChunk(stored, realInfo, 13);
419+
const decoded = await decodeGeometryChunk(stored, realInfo, FORMAT_VERSION);
413420
expectMeshesEqual(decoded, meshes);
414421
});
415422
});

0 commit comments

Comments
 (0)