Mesh authoring
Aether's native authoring representation is a small s-expression DSL, not a
triangle-editing model. aether-mesh parses the text into a typed tree,
evaluates primitives and transforms into fixed-point polygons, cleans the
polygon stream, and tessellates only where a triangle consumer needs it. The
aether.kit.mesh guest actor is the file-backed viewer: it reads a .dsl or
minimal .obj, atomically replaces its cached filled DrawTriangles and DSL
outline loops on success, and renders them on every Render stage.
Decision map
| ADR | Status | What still governs |
|---|---|---|
| ADR-0026 | Accepted | primitive-composition, palette indices, s-expression data |
| ADR-0051 | Accepted | structural syntax plus torus and sweep |
| ADR-0052 | Accepted | edit text and reload; no retained vertex/face editor state |
| ADR-0053 | Accepted | library-only mesher boundary |
| ADR-0054 | Superseded by ADR-0062 | historical BSP/CSG rationale only |
| ADR-0055 | Superseded by ADR-0062 | historical origin; reusable cleanup code was retained |
| ADR-0056 | Accepted | constrained Delaunay tessellation for non-convex faces and holes |
| ADR-0057 | Accepted | n-gon polygons, not triangles, are the canonical face form |
| ADR-0062 | Accepted | no union, intersection, or difference in v1 |
ADR-0026 said conventional imports would not be supported. Current code is
narrower in ambition but broader in practice: MeshViewer accepts a minimal
Wavefront OBJ subset in addition to the native DSL. Treat that as a viewer
compatibility path, not a second structural authoring model; OBJ becomes a
flat triangle cache and cannot round-trip through the DSL AST.
Mental model
Keep the layers distinct:
- Text data is parsed by
aether_mesh::parseintoNode. It is Lisp-like syntax but never evaluated as code. - The AST retains primitives, transforms, structural repetition, and a
palette index per primitive.
serializeemits a canonical s-expression; original whitespace and comments are not retained. - Canonical geometry is
Polygon: a fixed-point CCW outer loop, zero or more CW hole loops, a face-normal hint, and oneu32color index. - Wire/render geometry is triangles.
tessellate_polygonconverts a canonical face at the upload boundary;meshoffers a direct triangle convenience path. - The viewer actor maps color indices through its eight-entry RGB palette
and caches filled
DrawTriangles separately from DSL outer and hole loops. It resends the faces directly and rebuilds the loops as eye-facing slate stroke ribbons for the active camera on everyRender.
The library has no renderer or mailbox dependency. The viewer is guest code in
the multi-actor aether-kit wasm module. aether-kit exports several types,
so a load that names no export is refused with the module's export list
(ADR-0241 §9); the mesh actor must be selected as
aether_kit@aether.kit.mesh. Export membership is in
aether-kit/src/lib.rs.
The viewer's config names the camera whose eye its outlines face
({"camera": "aether.kit.camera:main"}, which is also the default): load
aether_kit@aether.kit.camera under that key first, and send
aether.render.view_from naming it so the renderer draws through the same
camera. A viewer loaded before its camera is refused at load, naming the
path.
DSL vocabulary
Every primitive has exact positional arity and requires :color <u32>.
Structural nodes do not take color; child primitives carry it.
| Family | Forms |
|---|---|
| solids | box, cylinder, cone, wedge, sphere |
| profile/path | lathe, extrude, torus, sweep |
| structure | composition, translate, rotate, scale, mirror, array |
The typed variants and their fields are the authority in
ast.rs; exact arity, keyword, and
error behavior is in
parse.rs. A representative model
is:
(composition
(box 2 0.5 1 :color 5)
(translate (0 0.75 0)
(array 3 (0.6 0 0)
(cylinder 0.15 1 8 :color 3))))
One input contains exactly one top-level form. Unknown heads and keywords,
wrong arity, dotted lists, missing color, malformed vectors/profiles, and
trailing forms are parse errors. composition may contain any number of
children. Vectors are ordered (x y z). Rotation uses an axis vector and an
angle passed to the math layer in radians. mirror accepts only the symbols
x, y, or z, and emits its child plus the child's reflection across that
axis-plane through the local origin.
sphere takes a radius and an icosphere refinement level: level 0 is the
20-face icosahedron and each level quadruples the faces, up to a cap of 4
(5120 faces); a higher level is a mesh error.
sweep takes a 2D profile and a 3D waypoint path. Optional :scales must have
exactly one scalar per waypoint. It is capped by default; :open true omits
the end caps. The mesher transports a frame along the path and stitches
adjacent rings. Boolean-looking heads are not parked syntax: they are unknown
nodes and fail parsing.
Public library operations
The crate-root exports are intentionally small:
| Rust operation | Purpose |
|---|---|
parse(&str) | DSL text to typed Node, or ParseError |
serialize(&Node) | canonical DSL text; parse/serialize/parse preserves the tree |
mesh_polygons(&Node) | evaluate to canonical n-gon Polygons |
tessellate_polygon(&Polygon) | display-time fixed-point triangles |
mesh(&Node) | convenience pipeline directly to colored float Triangles |
to_obj(&[Triangle]) | inspection export grouped by palette index |
surface_net(...) | separate dense-scalar-volume boundary extractor |
See lib.rs,
polygon.rs, and
surface_net.rs. Node,
Polygon, Point3, and Triangle are library types, not mail kinds.
The geometry pipeline simplifies identity transforms, emits primitive n-gons,
then runs exact-grid cleanup: weld vertices, repair T-junctions to a fixed
point, merge coplanar same-color boundaries, and remove short-edge slivers.
The public polygon path groups boundary loops deterministically into outers and
holes. The triangle path runs cleanup followed by CDT. The implementation is in
mesh.rs,
cleanup/, and
tessellate/.
Convex faces without holes use a fan fast path. Concave faces or faces with holes use integer constrained Delaunay triangulation. If CDT fails, the public display helper logs a warning and fan-triangulates the outer and each hole independently. That keeps geometry visible but fills holes incorrectly; a CDT warning is a correctness signal, not harmless noise.
Viewer mail surface
The request and result intentionally use different kind prefixes:
| Mail kind | Rust payload | Meaning |
|---|---|---|
aether.kit.mesh.load | aether_kit::mesh::LoadMesh | read and load namespace + relative path |
aether.mesh.load_result | aether_kinds::MeshLoadResult | echoes path, ok, optional error, and warnings |
Send the request to the loaded actor mailbox aether.kit.mesh. Although an
older kind comment calls it fire-and-forget, the current handler preserves an
available reply target and returns MeshLoadResult after fs read and parse
settle. Await the reply when automation needs a definitive outcome.
Extension matching is case-insensitive. DSL input must be UTF-8; OBJ is read directly from bytes and validates the recognized numeric tokens:
.dslusesparse→mesh_polygons→tessellate_polygon. Filled faces usecolor % 8in the viewer palette. Every outer and hole loop is retained alongside those faces, lifted slightly along its polygon normal, and solved into a slate eye-facing stroke when the active camera eye replies. Its width is angular, so the raster weight does not collapse edge-on or thin with distance..objaccepts positive one-based and relative negativevposition indices inffaces, including slash-form references. Faces are fan-triangulated. Normals, UVs, groups, materials, smoothing, and other directives are ignored; every face is soft blue and receives no outline. Malformed coordinates or face indices and references outside the positions defined so far are errors.
Any read, DSL UTF-8, extension, parse, mesh, or OBJ-index failure leaves the
entire prior face-and-outline cache untouched and returns ok: false. The current loader does
not produce non-fatal warnings, though the reply reserves that vector. The
actor and request kind are in
aether-kit/src/mesh/mod.rs,
mesh/kinds.rs.
The shared importer is in
aether-mesh/src/obj.rs;
the shared reply type is in
aether-kinds/src/lib.rs.
Geometry invariants and failure modes
- Every meshed coordinate is snapped to a signed 16:16 grid. Inputs must be
finite and remain within inclusive
±256after transforms. Anything outside that asset-local range returnsMeshError::OutOfRange; put world-scale placement outside the authored mesh. Seefixed.rs. - Primitive faces are wound CCW from outside. Mirroring reverses the reflected copy's vertices to preserve outward winding. Degenerate faces can collapse and be skipped rather than producing an error.
- N-gon outer loops are CCW and holes are CW around
plane_normal. ConvertPoint3to float only at the render/upload boundary when topology matters. - Cleanup stage invariant checks currently log warnings rather than aborting. A non-simple loop, surviving twin edge, or unrepaired T-junction in logs means the canonical output may be damaged.
- The parser and evaluator have no authored-complexity budget or nesting-depth
cap. The one bounded count is the
spherelevel, capped at 4 because its face count grows exponentially. Large segment/array counts or deeply nested trees can spend substantial CPU, memory, or stack. Validate or bound generated input at the producer when it is not trusted. serializeassumes a valid finite in-memory AST and panics if asked to print a non-finite float. Parsed non-finite geometry is rejected later at the fixed-point boundary.
Lifecycle and chassis caveats
MeshViewer subscribes only to the Render lifecycle stage. It performs file
I/O and atomically replaces the cache when a load reply arrives. On each render
it sends cached faces immediately. At wire it subscribes to the view of the
camera its config names (aether.render.view_subscribe) and keeps the eye of
each ViewProjection the camera sends; a DSL cache with outline loops rebuilds
and submits them for that eye on each render. A camera that has not published
a view yet leaves the filled mesh visible and omits only the outlines. OBJ caches contain no outline loops. A chassis without
Render, such as the production headless Tick-only graph, rejects the
subscription; the actor can load but never submits geometry. The minimal hub
does not host guest gameplay actors. Use desktop or the render-capable
SubstrateHarness for visual verification. See Lifecycle and
Rendering & camera.
Where to change or extend it
- A new DSL form must update
Node, parser, serializer, mesher, round-trip tests, and reference docs together. If it changes the v1 vocabulary, amend ADR-0051 or add a superseding decision. - Reintroducing booleans is not a parser-only change. It must explicitly supersede ADR-0062 and supply topology, resource bounds, native/wasm parity, and adversarial geometry coverage.
- Change canonical topology or tessellation in
aether-mesh, keeping n-gons authoritative per ADR-0057. Do not bake viewer palette or render types into the library. - Add a viewer file format in
aether-kit/src/mesh, preserving whole-cache atomic replacement and a structuredMeshLoadResult. A compatibility importer should not silently become a new native authoring representation. - The editable loop remains file-first: write DSL through
aether.fs, sendaether.kit.mesh.load, inspect the result, and capture a frame. There is no currentset_texteditor mail or retained face/vertex mutation API.