table-layout
The pure geometry behind a data table — column widths, and which rows are worth drawing. No DOM, no Babylon, no SVG: it turns a column spec plus a scroll offset into rectangles, so the view is a straight paint and the arithmetic is unit-tested.
Two problems, both of which are only annoying when you get them wrong:
- Columns — fixed widths must be honoured exactly,
flexcolumns share what's left, and the total must land on the available width to the pixel or the header stops lining up with the body. - Rows — a table you can scroll should only build the rows you can see.
visibleRowsis the window: the slice to draw, plus the offset to draw it at.
import { resolveColumns, visibleRows, rowAt } from 'tosijs-3d'
const cols = resolveColumns(
[{ key: 'name', flex: 1 }, { key: 'qty', width: 60 }, { key: 'price', width: 80 }],
{ width: 400, gap: 8 }
)
// → [{key:'name', x:0, width:236}, {key:'qty', x:244, width:60}, {key:'price', x:312, width:80}]
visibleRows({ scroll: 120, rowHeight: 28, viewportHeight: 100, count: 500 })
// → { start: 4, end: 12, offsetY: -8 } ← build 8 rows, not 500
Why virtualize at all
A table on a 3D texture pays twice for rows you can't see: once building SVG nodes,
and again rasterizing the whole sheet to a DynamicTexture every time it changes. A
500-row inventory becomes 500 <g>s in a texture that shows twelve of them. The window
keeps it proportional to the viewport instead of the data.
overscan draws a row or two beyond each edge so a fast scroll doesn't flash empty
bands before the next paint.