From d96f4317ac6fa7f3e6a2392ea0c2c294dd16ac20 Mon Sep 17 00:00:00 2001 From: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:02:05 -0400 Subject: [PATCH] fix: Keep one non-finite value from corrupting its compressed PLY chunk Co-Authored-By: Claude Sonnet 5.5 --- src/lib/writers/compressed-chunk.ts | 19 +++++++++++-------- test/formats.test.mjs | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/lib/writers/compressed-chunk.ts b/src/lib/writers/compressed-chunk.ts index 30e7d4b9..8ae8a2aa 100644 --- a/src/lib/writers/compressed-chunk.ts +++ b/src/lib/writers/compressed-chunk.ts @@ -53,20 +53,23 @@ class CompressedChunk { } pack() { + // Bounds over the finite values only: a single NaN/Infinity would otherwise + // make the whole chunk's bounds non-finite and corrupt all 256 splats. const calcMinMax = (data: Float32Array) => { - let min; - let max; - min = max = data[0]; - for (let i = 1; i < data.length; ++i) { + let min = Infinity; + let max = -Infinity; + for (let i = 0; i < data.length; ++i) { const v = data[i]; - min = Math.min(min, v); - max = Math.max(max, v); + if (Number.isFinite(v)) { + if (v < min) min = v; + if (v > max) max = v; + } } - return { min, max }; + return min <= max ? { min, max } : { min: 0, max: 0 }; }; const normalize = (x: number, min: number, max: number) => { - if (x <= min) return 0; + if (Number.isNaN(x) || x <= min) return 0; if (x >= max) return 1; return max - min < 0.00001 ? 0 : (x - min) / (max - min); }; diff --git a/test/formats.test.mjs b/test/formats.test.mjs index 71696df4..8a9474c3 100644 --- a/test/formats.test.mjs +++ b/test/formats.test.mjs @@ -270,6 +270,28 @@ describe('Compressed PLY Format', () => { compareSummaries(actualSummary, expectedSummary, { tolerance: 0.1 }); }); + it('a non-finite value must not corrupt the rest of its 256-splat chunk', async () => { + const dt = createTestDataTable(256); + dt.transform = Transform.PLY.clone(); + dt.getColumnByName('x').data[7] = NaN; + dt.getColumnByName('scale_0').data[9] = NaN; + dt.getColumnByName('f_dc_1').data[11] = NaN; + + const writeFs = new MemoryFileSystem(); + await writeCompressedPly({ filename: 'nan.compressed.ply', dataTable: dt }, writeFs); + + const pool = createChunkDataPool(); + const readBack = await materializeToDataTable( + await readPly(new BufferReadSource(writeFs.results.get('nan.compressed.ply')), pool), pool + ); + + // Before the fix all 256 splats decoded as NaN in each of these columns. + for (const name of ['x', 'scale_0', 'f_dc_1']) { + const nanCount = Array.from(readBack.getColumnByName(name).data).filter(Number.isNaN).length; + assert.strictEqual(nanCount, 0, `${name}: ${nanCount} NaN values after round-trip`); + } + }); + it('writeCompressedPlySource (ChunkSource adapter) == writeCompressedPly byte-for-byte', async () => { const dt = createTestDataTable(300, { includeSH: true, shBands: 1 }); dt.transform = Transform.PLY.clone();