plato-421.md
August 3, 2026 · View on GitHub
What and why
Plato has no vocabulary for periodic lattice structures — the strut-and-node
scaffolds used in additive manufacturing, metamaterials and infill. The pieces are
all present separately (Grid3D, IntegerVector3, Line3D, Bounds3D,
PolygonMesh3D, the implicit-SDF collection) and nothing names a unit cell or a
lattice operator over one.
Scope: stdlib/geometry (shipping tier — must pass lint strict, the checker
ratchet and index freshness). New lattices.types.plato +
lattices.library.plato, plus lattices.concepts.plato if an interface earns
its place.
Subject matter, roughly:
- Strut lattices: a unit cell as a set of nodes plus a connectivity list in normalized cell coordinates; the standard cells (simple/body-centred/face-centred cubic, octet truss, Kelvin/tetrakaidecahedron, diamond, re-entrant auxetic).
- Tiling an instance: a cell repeated over an
IntegerVector3count inside aBounds3D, producing struts asArray<Line3D>with the shared nodes welded, plus the derived readings (relative density, strut length, node valence, cell count). - Operators over a lattice: uniform and graded scaling of strut radius or cell
size from a field (
IScalarField3D), trimming to a bounding solid or an SDF, conforming a lattice to aBounds3Dversus a deformation, and the dual lattice. - Triply-periodic minimal surfaces as the implicit counterpart — gyroid,
Schwarz P, Schwarz D, Neovius, primitive — as
IScalarField3D/ISignedDistanceField3Dimplementations so the existing marching-cubes and SDF paths consume them with no new plumbing.
Reuse what exists. Struts become geometry through the existing Line3D /
PolygonMesh3D vocabulary; TPMS fields go through implicit-sdf and
voxels.library.plato's marching cubes rather than a second extraction path.
Design decisions
-
Two new files, no new interface.
stdlib/geometry/lattices.types.plato(nine declarations) andstdlib/geometry/lattices.library.plato(library Lattices). Nolattices.concepts.plato: the only candidate was anILatticeCellabstraction overLatticeUnitCell, which would have exactly one implementer. STYLE_GUIDE's "add a type only when two real uses exist" rules that out, and LINT013 punishes an interface nothing implements. -
A unit cell is a graph in normalized cell coordinates, not a world-sized object:
LatticeUnitCell { Nodes: Array<Point3D>; Struts: Array<LatticeStrut> }with node positions in the unit cube. One cell description therefore serves every cell size, and the cell tables are pure data. Connectivity uses the existingItemIndex(CONVENTIONS.md — typed indices) rather than a new index type; the precedent isSdfNode2D.Leaf(Primitive: ItemIndex). -
Welding is a local ownership rule, not a vertex search. A strut with BOTH endpoints on the far face of an axis lies in the face shared with the next cell, which describes the same segment on its near face; the near-face copy wins, and the far-face copy is kept only when there is no next cell. That is one predicate over one strut — no hash grid, no tolerance on world positions, no second pass. Node positions weld by construction because every one is spelled
Min + (cellIndex + normalizedCoordinate) * cellSize, so the far node of cell i and the near node of cell i+1 evaluate the identical expression(i+1)*cellSizeand land bit-identically. Rejected: emitting every cell's struts and deduplicating afterwards, which needs a spatial hash and a tolerance the rest of the tree does not have. This makes periodicity a stated precondition on the cell (a node at coordinate 1 has a twin at 0), recorded inlattices.types.plato. All seven shipped cells satisfy it. -
Seven named cells, each literal data:
SimpleCubic,BodyCenteredCubic,FaceCenteredCubic,OctetTruss,DiamondCubic,TruncatedOctahedron(the Kelvin cell) andReentrantHoneycomb(reentrancy)/ReentrantAuxetic. Rejected: deriving struts by a nearest-neighbour distance threshold, which would have made the FCC cell inexpressible (the octet truss is exactly FCC's node set under that rule) and put a magic distance in every table. Two renames away from the obvious spelling, both to avoid joining an unrelated overload group:TruncatedOctahedronrather thanKelvin(Temperature.Kelvinexists) andDiamondCubicrather thanDiamond(aTpmsFamilycase). The re-entrant cell is parameterized, because re-entrancy is a family and the auxetic behaviour is a function of the fold depth;ReentrantAuxeticis the quarter-fold constant for callers that just want a named cell. -
The graded, trimmed and warped variants consume the welded strut list, not the lattice:
Trimmed(struts, …),StrutRadii(struts, …),Deformed(struts, mapping)all takeArray<Line3D>, so they compose in any order and read left to right (lattice.Struts.Trimmed(solid).StrutRadii(field, range)). There is exactly one tiling path,Struts. Graded cell size is a deformation, not a second tiling:Deformedwarps the tiled struts, which is what conforming a lattice to something other than a box means. Trimming keeps or drops whole struts by their midpoint and never clips — clipping would need root finding against the trimming solid and would break the welding at the cut. -
Relative density is declared as a first-order OVERESTIMATE. Sum of
pi * r^2 * lengthover the envelope volume double-counts material at the nodes and omits the joint fillets; it converges on the truth in the slender regime lattices are actually used in. Saying so is better than shipping a number that looks exact. -
Node valence is the coordination number in the INFINITE tiling, computed from the cell alone by identifying nodes modulo the cell period and counting endpoints of owned struts. That is the number an engineer means; the count of struts touching a node inside one cell is an artifact of where the cube was cut.
-
TPMS: one sum-type family, three types.
TpmsFamily(Gyroid, SchwarzPrimitive, SchwarzDiamond, Neovius, IwpSurface) mirrors theNoiseBasisprecedent instead of five near-identical types.TpmsField3Dis the raw nodal implicit and implementsIScalarField3Donly — a nodal implicit is NOT a distance and claiming otherwise would be the "not a bound at all" failureimplicit-sdf.concepts.platowarns about. The two solids,TpmsNetwork3D(one labyrinth) andTpmsSheet3D(a wall of finite thickness), implementISignedDistanceField3Dby dividing the implicit by a per-family Lipschitz bound, which makes them an honest LOWER BOUND. Bounds are the summed per-term coefficients times the angular frequency; verified numerically to be upper bounds (see Verification). -
Struts reach geometry through the existing SDF path.
StrutSdf3D/GradedStrutSdf3Dare unions of capsules overDistanceToCapsule, already inimplicit-sdf.library.plato, and are EXACT signed distance fields. Marching cubes and sphere tracing therefore consume a lattice with no new plumbing, which is what the issue asked for. Evaluation is linear in strut count with no acceleration structure, and the type comment says so. -
Dropped: the dual lattice. A correct periodic dual (the line graph of the cell, one node per strut midpoint) needs adjacencies that cross the cell boundary, and the normalized-unit-cube cell cannot name a node in the neighbouring cell. Identifying nodes modulo the period instead is too coarse — it would declare the simple cubic cell's twelve edges mutually adjacent, since all eight corners are one periodic node. Left out rather than shipped wrong; it is not one of the boxes below.
Verification
Lint (strict) and the checker ratchet pass. No gate currently executes geometry
bodies (plato-308), so the algorithm was verified out-of-band, against the same
tables and index arithmetic the Plato source uses:
- For all seven cells, tiled at 3x3x3 and at a deliberately lopsided 3x1x2, the ownership rule emits every distinct world-space strut exactly once: no duplicates and nothing missing against the full undeduplicated set. The same holds for nodes. The lopsided count is the case that matters — it is where a cell has a successor on one axis and not another.
- The periodic valences the cell tables produce are the textbook coordination numbers: simple cubic 6, body-centred cubic 8, face-centred cubic 12 at a corner and 4 at a face node, octet truss 12 everywhere, diamond 4 everywhere, Kelvin 4 (the truncated octahedron is 3-valent; the fourth strut is the neighbour's across the shared square face). Every cell's struts come out a single length, as each structure requires.
- Each TPMS family's Lipschitz constant was checked against 200k sampled gradient magnitudes and bounds them all, with slack (the gyroid's true maximum is half its bound). Every family is at volume fraction 0.5 at level 0 except I-WP, which the doc comments do not claim.
Done means
- A unit-cell type and the named standard cells, in normalized cell coordinates
- A lattice instance tiles a cell over a count inside a bounds and yields welded struts
- Graded / field-driven and trimmed variants, without a second tiling path
- The TPMS family implements the existing scalar-field and SDF interfaces
- Derived readings: relative density, strut count, total strut length, node valence
-
.\tools\check-stdlib-fast.ps1 -SkipIndexgreen (lint strict + checker ratchet) - Design decisions recorded above
-
stdlib/types-and-concepts.txtregenerated — nine new types make it stale, and index freshness is the third gate incheck-stdlib-fast.ps1 - A browser demo drives it:
demos/webgl/lattices.html+src/demos/lattices.ts, green undernpm run typecheckandnpm run scenes