Converting Representations: What Survives and What Does Not
Overview
Rasmah keeps one parametric model and derives every other representation from it. This tutorial is a map of those representations, the file formats that serialize each one, and — most importantly — which conversions are lossless and which lose information and are controlled by a resolution parameter.
The representations, from most to least “exact”:
| Representation | Type | What it stores | Lossless? |
|---|---|---|---|
| Parametric model | Feature |
the feature recipe (CSG operations over values) | — (source of truth) |
| BRep | BRep |
exact topology + analytic surfaces | exact geometry |
| SDF | ImplicitGeometry |
signed-distance scalar field | analytic (per feature) |
| Surface mesh | TriangleMesh |
triangle soup / shared-vertex mesh | approximates a surface |
| Volume mesh | TetMesh |
tetrahedral mesh | approximates a solid |
| Scalar volume | DicomVolume |
a regular 3D grid of values | sampled field |
File formats ↔︎ representations
| Format | Representation it stores | Nature |
|---|---|---|
.rasmah |
parametric model | lossless (JSON) |
| STEP | BRep | lossless within the analytic subset |
STL / OBJ / PLY / .vtp / MSH (tris) |
surface mesh | lossless for the mesh |
.vtu / MSH (tets) / INP |
volume mesh | lossless for the mesh |
| DICOM / NIfTI | scalar volume | lossless for the grid (pixel depth) |
The conversion graph
using Rasmah
model = difference(box(4.0, 2.0, 0.5), cylinder(0.5, 0.5))Difference{Box{Float64, Float64, Float64}, Cylinder{Float64, Float64}}(Box{Float64, Float64, Float64}(4.0, 2.0, 0.5), Cylinder{Float64, Float64}(0.5, 0.5))
Feature ──brep()──► BRep ──write_step──► .step
│ │
field(x) surface_mesh(b) (tessellate)
│ │
▼ ▼
SDF ◄──── to_sdf(b) ── surface mesh ──► STL/OBJ/PLY/VTK
│ ▲
surface_mesh / tetrahedralize │ to_sdf(mesh)
│ │
├──────────────► volume mesh ──► VTU/MSH/INP
│
sample_grid
│
▼
scalar volume ──► DICOM / NIfTI
▲
│ segment(vol, threshold)
SDF
Feature → SDF and Feature → BRep
Both are lossless: the signed-distance field is evaluated analytically from the same features, and the BRep is the exact topological model. No resolution parameter exists because no sampling happens:
b = brep(model) # exact BRep
# numeric volumes via the divergence theorem (analytic `volume` is primitive-only)
println("SDF volume: ", round(volume(surface_mesh(model; n=64)); digits=3))
println("BRep volume: ", round(volume(surface_mesh(b; n=48)); digits=3))SDF volume: 3.607 m^3
BRep volume: 3.608 m^3
SDF / BRep → mesh (lossy, resolution-controlled)
Meshing is where information is lost: a smooth surface becomes flat triangles, and the error is set by a resolution parameter. Every mesher takes one:
# implicit → surface (n = grid resolution along each axis)
m1 = surface_mesh(model; n=32)
m2 = surface_mesh(model; n=96)
println("n=32: ", size(m1.faces, 2), " tris, area ", round(surface_area(m1); digits=4))
println("n=96: ", size(m2.faces, 2), " tris, area ", round(surface_area(m2); digits=4))
# implicit → volume (n, and optionally conform for a boundary-fitted mesh)
vol = tetrahedralize(model, 16)
volc = tetrahedralize(model, 16; conform=true)
println("marching tets: ", size(vol.elements, 2), " conforming: ", size(volc.elements, 2))
# exact BRep → surface (n = tessellation points per curve/loop)
mb = surface_mesh(b; n=24)
println("tessellated BRep: ", size(mb.faces, 2), " tris")
# exact BRep → volume (`volume_mesh` is the named alias of `tetrahedralize(b, n)`)
vb = volume_mesh(b, 12)
println("BRep volume mesh: ", size(vb.elements, 2), " tets")n=32: 12304 tris, area 21.1712 m^2
n=96: 110144 tris, area 21.7226 m^2
marching tets: 34936 conforming: 34936
tessellated BRep: 33706 tris
BRep volume mesh: 15892 tets
Options worth knowing:
surface_mesh(; adaptive=true, max_depth=6)— adaptive octree instead of a uniform grid (finer near the surface only).tetrahedralize(; conform=true)— snaps boundary vertices onto the surface and relaxes the interior (see Meshing Algorithms).
Mesh → SDF (lossy, but converges)
A mesh can be turned back into a signed-distance field via a bounding-volume hierarchy + winding-number sign. It is approximate but the underlying surface is recovered to the mesh’s own resolution:
sdf2 = to_sdf(m2) # surface mesh → SDF
println("recovered distance at (2.5,0,0): ", sdf2([2.5, 0.0, 0.0]))
println("analytic distance: ", field(model, [2.5, 0.0, 0.0]))recovered distance at (2.5,0,0): 0.5
analytic distance: 0.5
SDF ↔︎ scalar volume (lossy, grid-limited)
sample_grid evaluates an SDF on a regular grid (the DICOM/NIfTI export path), and segment thresholds a volume back into a level set (the import path). Both are limited by the grid spacing:
grid = sample_grid(x -> field(model, x), [-2.5, -1.5, -0.75], [2.5, 1.5, 0.75], (50, 30, 15))
println("sampled grid: ", size(grid.values), " (spacing ", round.(grid.spacing; digits=3), ")")
seg = segment(grid, 0.0) # volume → level set
println("inside the solid (1.5,0,0): ", seg([1.5, 0.0, 0.0]) > 0)
println("in the hole (0,0,0): ", seg([0.0, 0.0, 0.0]) < 0)
println("outside the box (2.4,0,0): ", seg([2.4, 0.0, 0.0]) < 0)sampled grid: (50, 30, 15) (spacing [0.102, 0.103, 0.107])
inside the solid (1.5,0,0): true
in the hole (0,0,0): true
outside the box (2.4,0,0): true
What cannot be converted
- Mesh → BRep (reverse engineering a surface mesh into exact topology/NURBS) is out of scope — a triangle mesh has lost the analytic surface.
- SDF → BRep in general: the implicit field has no topology to recover.
- BRep → SDF exists but is lossy (
to_sdf(b)tessellates first).
Summary
lossless: Feature ⇄ SDF ⇄ Feature, Feature → BRep, BRep → STEP,
mesh → STL/OBJ/PLY/VTK/MSH/INP (for the mesh itself),
grid → DICOM/NIfTI (to pixel depth)
lossy (resolution n): anything → mesh, mesh → SDF, SDF ⇄ grid,
BRep → SDF
impossible/out of scope: mesh → BRep, SDF → BRep
The parametric model is the only thing worth keeping around: it regenerates every other representation, exactly, at any resolution, and differentiably.
