Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions src/lib/compat/data-table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ const GEOMETRIC_COLS = [
] as const;
const COLOR_DC_COLS = ['f_dc_0', 'f_dc_1', 'f_dc_2'] as const;

const standardColumnSet = new Set<string>([
...POSITION_COLS,
...GEOMETRIC_COLS,
...COLOR_DC_COLS
]);

/**
* Enumerate the canonical column names a source exposes, in the same order
* {@link materializeToDataTable} produces its columns — derived purely from
Expand Down Expand Up @@ -91,11 +85,15 @@ const detectShBands = (dataTable: DataTable): SHBands => {
throw new Error(`dataTableToChunkSource: unrecognized f_rest_* count: ${count}`);
};

const detectExtras = (dataTable: DataTable): ExtraColumn[] => {
const detectExtras = (
dataTable: DataTable,
standardColumns: ReadonlySet<string>,
hasColor: boolean
): ExtraColumn[] => {
const extras: ExtraColumn[] = [];
for (const c of dataTable.columns) {
if (standardColumnSet.has(c.name)) continue;
if (/^f_rest_\d+$/.test(c.name)) continue;
if (standardColumns.has(c.name)) continue;
if (hasColor && /^f_rest_\d+$/.test(c.name)) continue;
const type: 'float32' | 'uint32' = (
c.dataType === 'float32' || c.dataType === 'float64'
) ? 'float32' : 'uint32';
Expand Down Expand Up @@ -171,12 +169,17 @@ const dataTableToChunkSource = (
const count = indices ? indices.length : dataTable.numRows;
const shBands = detectShBands(dataTable);
const numRest = SH_REST_COUNTS[shBands];
const extras = detectExtras(dataTable);
const transform: Transform = dataTable.transform;

const hasPosition = POSITION_COLS.every(c => dataTable.hasColumn(c));
const hasGeometric = GEOMETRIC_COLS.every(c => dataTable.hasColumn(c));
const hasColor = COLOR_DC_COLS.every(c => dataTable.hasColumn(c));
const standardColumns = new Set<string>([
...(hasPosition ? POSITION_COLS : []),
...(hasGeometric ? GEOMETRIC_COLS : []),
...(hasColor ? COLOR_DC_COLS : [])
]);
const extras = detectExtras(dataTable, standardColumns, hasColor);
const hasOther = extras.length > 0;

const col = (name: string): Float32Array => dataTable.getColumnByName(name)!.data as Float32Array;
Expand Down
11 changes: 8 additions & 3 deletions src/lib/readers/read-ply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,10 +632,15 @@ const readPly = async (source: ReadSource, pool: ChunkDataPool): Promise<ChunkSo
throw new Error(`readPly: unrecognized f_rest_* count ${restCount}`);
}

// Non-standard columns become `other` extras (in file order).
const standard = new Set<string>(['x', 'y', 'z', ...GEOMETRIC_COLS, ...COLOR_DC_COLS]);
// Complete canonical layers use their fixed layouts. Properties from an
// incomplete layer remain as `other` extras instead of being discarded.
const standard = new Set<string>([
...(hasPosition ? ['x', 'y', 'z'] : []),
...(hasGeometric ? GEOMETRIC_COLS : []),
...(hasColor ? COLOR_DC_COLS : [])
]);
const extras: ExtraColumn[] = properties
.filter(p => !standard.has(p.name) && !/^f_rest_\d+$/.test(p.name))
.filter(p => !standard.has(p.name) && !(hasColor && /^f_rest_\d+$/.test(p.name)))
.map((p) => {
const type: 'float32' | 'uint32' = p.type === 'float' || p.type === 'double' ? 'float32' : 'uint32';
return { name: p.name, type };
Expand Down
22 changes: 22 additions & 0 deletions test/ply-streaming.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import { mapSource } from '../src/lib/ops/index.js';
import { decodePlyToDataTable, readPly } from '../src/lib/readers/read-ply.js';
import { createChunkDataPool } from '../src/lib/chunk/index.js';
import { materializeToDataTable } from '../src/lib/compat/data-table.js';
import { writePlyStreaming } from '../src/lib/writers/write-ply-streaming.js';

const SH_COEFFS = [0, 3, 8, 15];
Expand Down Expand Up @@ -159,6 +160,27 @@ describe('streaming PLY pipeline (chunked read -> transform -> streaming write)'
}
});

it('preserves 2DGS geometric properties when scale_2 is absent', async () => {
const dt = makeCanonicalDataTable(5);
dt.removeColumn('scale_2');
const pool = createChunkDataPool({ chunkSize: 2 });

const src = await readPly(await sourceFromBytes(encodePlyBinary(dt)), pool);
assert.ok(!src.meta.availableLayers.has('geometric'));
assert.ok(src.meta.availableLayers.has('other'));
assert.deepStrictEqual(
src.meta.extraColumns.map(column => column.name),
['rot_0', 'rot_1', 'rot_2', 'rot_3', 'scale_0', 'scale_1', 'opacity']
);

const out = await materializeToDataTable(src, pool);
assert.deepStrictEqual([...out.columnNames].sort(), [...dt.columnNames].sort());
for (const name of dt.columnNames) {
assert.deepStrictEqual(out.getColumnByName(name).data, dt.getColumnByName(name).data, `column '${name}' value mismatch`);
}
await src.close();
});

it('streams from disk with a bounded, scene-size-independent memory footprint', async () => {
const dir = await mkdtemp(join(tmpdir(), 'ply-streaming-'));
const inPath = join(dir, 'in.ply');
Expand Down
18 changes: 18 additions & 0 deletions test/source.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ describe('ChunkSource data model', () => {
assertTablesEqual(out, dt, 'partial round-trip');
});

it('preserves columns from an incomplete canonical layer as extras', async () => {
const dt = createTestDataTable(5);
dt.removeColumn('scale_2');
const chunkSize = 2;

const src = dataTableToChunkSource(dt, chunkSize);
assert.ok(!src.meta.availableLayers.has('geometric'));
assert.ok(src.meta.availableLayers.has('other'));
assert.deepStrictEqual(
src.meta.extraColumns.map(column => column.name),
['scale_0', 'scale_1', 'opacity', 'rot_0', 'rot_1', 'rot_2', 'rot_3']
);

const pool = createChunkDataPool({ chunkSize });
const out = await materializeToDataTable(src, pool);
assertTablesEqual(out, dt, 'incomplete canonical layer round-trip');
});

it('compact() materializes a source identically', async () => {
const dt = createTestDataTable(7, { includeSH: true, shBands: 1 });
const chunkSize = 3; // -> chunks of 3, 3, 1
Expand Down