Primitives, SDFs, and CSG
Overview
Every shape in Rasmah is built by starting from a small set of primitives — sphere, box, cylinder, and so on — and then combining them with constructive solid geometry (CSG) and transforms. This chapter introduces each primitive, shows how its signed distance field represents it exactly, and then shows how to cut, merge, move, scale, and rotate shapes.
The big idea is that a whole solid can be described by a single scalar function $s(\mathbf{x})$ (its SDF). A boolean operation between two shapes is then nothing more than a min or a max of their two fields. The catch is that a min/max is only piecewise smooth: it is continuous, but it has a kink — a sharp crease — exactly where the two surfaces cross, where $s_a = s_b$. So the combined field is differentiable almost everywhere, not everywhere. That is usually enough to keep the CAD → mesh → simulation chain differentiable in practice; where a rounded transition is wanted instead of a crease, the soft booleans smooth_union / smooth_intersection / smooth_difference replace the hard min/max with a smooth blend.
The primitives
Each primitive has an exact signed distance field and an analytic (closed form) volume, so the geometry never depends on how finely you sample it.
Sphere
Rasmah.sphere — Function
sphere(r)Construct a solid sphere of radius r, centred at the origin.
A sphere is the set of points whose distance from its centre is at most r. As a signed distance field (SDF) it is the exact field
\[s(\mathbf{x}) = \|\mathbf{x}\| - r,\]
which is negative inside, zero on the surface, and positive outside. Because $|s(\mathbf{x})|$ is the shortest distance to the surface, the gradient $\nabla s = \mathbf{x}/\|\mathbf{x}\|$ has unit length almost everywhere — this is the defining property of an exact SDF, and it is what keeps the field well-behaved when it is fed to meshing, ray casting, or gradient.
Arguments
r::Real(or a unit-awareQuantitylength): the sphere radius.
Returns
A Sphere feature node. Feed it to brep for the exact topology, to evaluate(…, MeshBackend(…)) for a mesh, or read its analytic volume / bounds.
Example
A bare number is interpreted in SI base units, and an explicit Quantity gives the same result. A quantity is written by appending the u"…" string macro to a number — 2u"m" is 2 metres, 10u"mm" is 10 millimetres, and so on; the notation is covered in the Units chapter:
julia> volume(sphere(2))
33.510321638291124 m^3
julia> volume(sphere(2u"m"))
33.510321638291124 m^3
julia> bounds(evaluate(sphere(2), SDFBackend())) == ([-2, -2, -2], [2, 2, 2])
true
julia> vtk(sphere(2.0)) # interactive VTK.js viewer (inline in Jupyter/Quarto)
VTKView(640×480, inline)The analytic volume is $4\pi r^{3}/3$, so its derivative with respect to the radius is the surface area $4\pi r^{2}$. With a unitless radius the derivative keeps the output's volume units, while an explicit length gives area:
julia> derivative(r -> volume(sphere(r)), 2.0)
50.26548245743669 m^3
julia> derivative(r -> volume(sphere(r)), 2.0u"m")
50.26548245743669 m^2vtk(sphere(2.0))The view is live — drag to rotate, scroll to zoom. Every 3D view in the Handbook is interactive like this.
Box
Rasmah.box — Function
box(x, y, z)Construct an axis-aligned box with full side lengths x, y, and z, centred at the origin.
The box occupies [-x/2, x/2] × [-y/2, y/2] × [-z/2, z/2]. Its signed distance field is built from the component-wise outside distances q_i = max(|p_i| − s_i/2, 0) for the three axes:
\[s(\mathbf{p}) = \left\|\,(q_x, q_y, q_z)\,\right\| + \min\left(\max(q_x, q_y, q_z),\, 0\right),\]
Outside the box the first term is the straight-line distance to the nearest face, edge, or corner; inside, the second (negative) term is the distance to the nearest face. The two combine into the exact Euclidean distance, so box is a true SDF — which is what keeps gradient, meshing, and ray casting well-behaved.
Arguments
x,y,z: full side lengths along the three axes (unit-aware lengths).
Returns
A Box feature node (see sphere for how to evaluate it).
Example
julia> using Rasmah
julia> volume(box(2, 3, 4))
24.0 m^3
julia> vtk(box(2, 3, 4))
VTKView(640×480, inline)vtk(box(2, 3, 4))Cylinder, cone, torus
Rasmah.cylinder — Function
cylinder(r, h)Construct a solid cylinder of radius r and height h, aligned with the z-axis and centred at the origin.
The cylinder is the set of points whose distance from the z-axis is at most r and whose z-coordinate is within [-h/2, h/2]. Its signed distance field is the 2D box SDF of the radial distance from the axis and the axial distance from the mid-plane, with the axis-adjacent point used to form an exact distance:
\[s(\mathbf{x}) = \left\|\,\left(\sqrt{x^2 + y^2} - r,\; |z| - h/2\right)_+\,\right\| + \min\left(\max(\sqrt{x^2 + y^2} - r,\; |z| - h/2),\, 0\right),\]
where (v)_+ = max(v, 0).
Arguments
r: the cylinder radius.h: the cylinder height.
Returns
A Cylinder feature node.
Example
julia> using Rasmah
julia> volume(cylinder(1, 2))
6.283185307179586 m^3
julia> vtk(cylinder(1.0, 2.0))
VTKView(640×480, inline)vtk(cylinder(1.0, 2.0))Rasmah.cone — Function
cone(r, h)Construct a solid cone of base radius r and height h, aligned with the z-axis.
The cone's apex is at z = -h/2 and its base disk (radius r) is at z = +h/2, so it is a right circular cone. Its signed distance field is the exact distance to the (truncated) cone surface, computed from the closest point along the generating line and the closest point on the base circle, with the sign fixed by which side of the cone the point lies on.
Arguments
r: the base radius.h: the cone height.
Returns
A Cone feature node. The analytic volume is $\pi r^{2} h/3$.
Example
julia> using Rasmah
julia> volume(cone(1, 2))
2.0943951023931953 m^3
julia> vtk(cone(1.0, 2.0))
VTKView(640×480, inline)vtk(cone(1.0, 2.0))Rasmah.torus — Function
torus(major, minor)Construct a solid torus with major (ring) radius major and minor (tube) radius minor, lying in the xy-plane and centred at the origin.
The torus is the set of points whose distance from the circle of radius major in the xy-plane is at most minor. Its signed distance field is
\[s(\mathbf{x}) = \sqrt{\left(\sqrt{x^2 + y^2} - \text{major}\right)^2 + z^2} - \text{minor},\]
which is an exact SDF: the two square roots give the distance to the central circle, and subtracting minor gives the distance to the tube's surface.
Arguments
major: the major (ring-centre) radius.minor: the minor (tube) radius.
Returns
A Torus feature node. The analytic volume is $2\pi^{2}\,\mathrm{major}\,\mathrm{minor}^{2}$ (Pappus's second theorem).
Example
julia> using Rasmah
julia> volume(torus(2, 0.5))
9.869604401089358 m^3
julia> vtk(torus(2, 0.5))
VTKView(640×480, inline)vtk(torus(2, 0.5))Wedge, prism, tube
Rasmah.wedge — Function
wedge(w, h, d)Construct a solid wedge (a right-triangular prism) of width w, height h, and depth d.
The wedge is the box [±w/2, ±h/2, ±d/2] cut by the diagonal plane $x/w + y/h \le 0$, so its cross-section in the xy-plane is a right triangle with legs w and h. Its signed distance field is the maximum of the box field and the half-plane field.
Arguments
w: width along x.h: height along y.d: depth along z.
Returns
A Wedge feature node. The analytic volume is $w\cdot h\cdot d/2$ (half the enclosing box).
Example
julia> using Rasmah
julia> volume(wedge(2, 3, 4))
12.0 m^3
julia> vtk(wedge(2, 3, 4))
VTKView(640×480, inline)vtk(wedge(2, 3, 4))Rasmah.prism — Function
prism(n, circumradius, height)Construct a regular n-gon prism with circumradius circumradius, extruded along z by height.
The cross-section is a regular polygon with n sides inscribed in a circle of radius circumradius, with its first vertex on the +x axis. The signed distance field is the exact distance to the regular polygon in the xy-plane, extruded along z (the polygon distance uses the periodic angular symmetry).
Arguments
n: number of sides (an integer,n ≥ 3).circumradius: the polygon circumradius.height: the prism height.
Returns
A Prism feature node. The analytic volume is $(n/2)\cdot \mathrm{circumradius}^{2}\cdot \sin(2\pi/n)\cdot \mathrm{height}$.
Example
julia> using Rasmah
julia> volume(prism(6, 1, 2))
5.196152422706632 m^3
julia> vtk(prism(6, 1, 2))
VTKView(640×480, inline)vtk(prism(6, 1, 2))Rasmah.tube — Function
tube(ro, ri, height)Construct a hollow cylinder (an annular tube) with outer radius ro, inner radius ri, and height height, aligned with the z-axis.
The tube is the region between two coaxial cylinders of radii ri and ro. Its signed distance field is the 2D box SDF of the radial distance from the annular mid-line (ro + ri)/2 and the axial distance from the mid-plane.
Arguments
ro: outer radius.ri: inner radius (must be $r_i \le r_o$).height: tube height.
Returns
A Tube feature node. The analytic volume is $\pi(r_o^{2} - r_i^{2})\cdot \mathrm{height}$.
Example
julia> using Rasmah
julia> volume(tube(2, 1, 3))
28.274333882308138 m^3
julia> vtk(tube(2, 1, 3))
VTKView(640×480, inline)vtk(tube(2, 1, 3))The half-space plane
Rasmah.plane — Function
plane(n, d)Construct an infinite half-space plane with unit normal n and signed offset d, satisfying $\mathbf{n}\cdot \mathbf{x} = d$.
A plane is not a solid: it is an unbounded cutting surface used to slice other solids in boolean operations (e.g. difference(box(...), plane(n, d))). Because it is infinite, it has no bounds and no volume; meshing a plane alone requires explicit bounds.
Arguments
n: the plane normal (a 3-vector).d: the signed offset along the normal (the half-space keeps $\mathbf{n}\cdot \mathbf{x} \le d$).
Returns
A Plane feature node.
Example
julia> using Rasmah
julia> half = difference(box(2, 2, 2), plane([0, 0, 1], 0.0));
julia> bounds(evaluate(half, SDFBackend())) == ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0])
true
julia> vtk(difference(box(2, 2, 2), plane([0, 0, 1], 0.0)))
VTKView(640×480, inline)vtk(difference(box(2, 2, 2), plane([0, 0, 1], 0.0)))Springs and threads
Two more solids are built from primitives plus a sweep: a helix spring (a circular wire swept along a helix) and a threaded rod (a cylinder with a helical ridge wound around it):
Rasmah.helix_solid — Function
helix_solid(radius, pitch, turns, wire)Construct a helix spring solid: a circular wire of radius wire swept along a helix of radius radius, pitch pitch, and turns turns.
Rasmah.thread — Function
thread(radius, height, pitch; depth=nothing)Construct a threaded rod (cosmetic thread): a cylinder of radius and height with a helical ridge of circular cross-section wound around it.
Keyword arguments
depth: ridge depth; defaults topitch * 0.2.
volume(helix_solid(1.0, 0.5, 3, 0.1))0.5918489312887331 m^3A spring's volume is Pappus's theorem: the wire's cross-section area times the helix's arc length. The helix path itself is introduced with the other wireframe curves in the curves chapter.
Constructive solid geometry (CSG)
CSG combines solids with the set operations difference, union, and intersection. In SDF terms the three operations are beautifully simple:
\[\begin{aligned} s_{a \cup b} &= \min(s_a, s_b) && \text{union},\\ s_{a \cap b} &= \max(s_a, s_b) && \text{intersection},\\ s_{a \setminus b} &= \max(s_a, -s_b) && \text{difference}. \end{aligned}\]
The union keeps the nearer surface of the two shapes (so a point is inside when it is inside either); the intersection keeps the farther (inside both); the difference flips b's sign so that b carves a cavity out of a.
Each of these is a hard min or max, so the combined field is continuous but has a kink wherever the two surfaces cross ($s_a = s_b$): it is differentiable almost everywhere, with its derivative undefined along that crease. That is normally fine — a gradient step away from the crease is well-defined — but when a rounded, fillet-like transition is wanted instead of a sharp edge, the soft booleans smooth_union / smooth_intersection / smooth_difference blend the two fields over a radius instead of taking the hard min/max.
Rasmah.difference — Function
difference(a, b)Boolean set difference a ∖ b: the solid a with the solid b cut away.
Constructive solid geometry (CSG) combines solids with set operations. The difference is the set of points inside a but outside b. As a signed distance field it is max(s_a, -s_b) — the maximum keeps the farther-in region of a and flips b's field so that b's interior becomes exterior.
Arguments
a: the base feature.b: the feature subtracted froma.
Returns
A Difference feature node.
Example
julia> using Rasmah
julia> d = difference(box(2, 2, 2), sphere(0.5));
julia> bounds(evaluate(d, SDFBackend())) == ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0])
true
julia> vtk(difference(box(2, 2, 2), sphere(0.5)))
VTKView(640×480, inline)difference(a::BRep, b::BRep; sdf_n::Int=8) -> BRepBoolean difference a ∖ b of two already-evaluated BReps. Every operand face is clipped to the region kept by the other solid's signed-distance field and the surviving faces are stitched into a single shell.
Keyword arguments
sdf_n: tessellation resolution used to build the operand SDFs when an exact analytic field is unavailable.
Rasmah.intersection — Function
intersection(a, b)Boolean set intersection a ∩ b: the solid region common to both a and b.
The intersection's signed distance field is max(s_a, s_b) — the farther-in of the two solids wins, so a point is inside the intersection only when it is inside both operands.
Arguments
a,b: the two features to intersect.
Returns
An Intersection feature node.
Example
julia> vtk(intersection(sphere(1.5), box(2, 2, 2)))
VTKView(640×480, inline)intersection(a::BRep, b::BRep; sdf_n::Int=8) -> BRepBoolean intersection a ∩ b of two already-evaluated BReps, computed by clipping each operand's faces to the region inside the other solid and stitching the survivors into a single shell.
Keyword arguments
sdf_n: tessellation resolution used to build the operand SDFs when an exact analytic field is unavailable.
vtk(difference(box(2, 2, 2), sphere(0.5)))vtk(intersection(sphere(1.5), box(2, 2, 2)))The union is available through union (Rasmah extends Base.union for features), e.g. union(sphere(1), sphere(1)). Note that the Union type is deliberately not exported — it would clash with Core.Union — so use the union function instead.
Smooth (blended) CSG
A hard boolean joins two shapes at a sharp crease. Real parts often want a filleted transition instead, so Rasmah provides smoothed versions that round the crease over a band of radius $k$:
Rasmah.smooth_union — Function
smooth_union(a, b, k)Smooth (blended) union of two implicit geometries, with blend radius k.
Theory
The plain boolean operations in difference / intersection / union join two surfaces with a sharp crease — the field is a hard min or max. Real parts often want a filleted transition, so Rasmah also provides smoothed versions that round the crease. They replace the min/max with a polynomial smooth-minimum/maximum of width k: the two fields meet over a blend band of radius $k$ instead of a knife edge.
Arguments
a,b— two implicit geometries (FeatureorImplicitGeometry).k— the blend radius: the width of the rounded transition ($k = 0$ recovers the hard boolean).
Returns
A new ImplicitGeometry whose surface is the blended result. Because the blend is a smooth function of both fields, it stays differentiable. The smooth booleans work at the SDF level, so wrap features in to_sdf(...) (or use the plain difference / union / intersection, which take features directly).
Example
julia> s = smooth_union(to_sdf(sphere(1.0)), to_sdf(translate(sphere(1.0), 1.5, 0.0, 0.0)), 0.3);
julia> s isa ImplicitGeometry
trueSee also
Rasmah.smooth_intersection — Function
smooth_intersection(a, b, k)Smooth (blended) intersection of two implicit geometries with blend radius k. The smoothed counterpart of intersection; see smooth_union for the blend.
Rasmah.smooth_difference — Function
smooth_difference(a, b, k)Smooth (blended) difference a \ b of two implicit geometries with blend radius k. The smoothed counterpart of difference; see smooth_union for the blend.
s = smooth_union(to_sdf(sphere(1.0)), to_sdf(translate(sphere(1.0), 1.5, 0.0, 0.0)), 0.3)
bounds(s)([-1.0, -1.0, -1.0], [2.5, 1.0, 1.0])Two spheres still merge into one solid, but the neck where they meet is rounded rather than a knife-edge min. $k = 0$ recovers the hard boolean. The smooth operations act on the SDF level, so wrap features in to_sdf(...).
Standard engineering shapes
The primitives are the raw building blocks, but real parts rarely start from a bare sphere or box. They start from the standard engineering shapes — the profiles and parts that recur across mechanical, structural, and thermal design. Rasmah ships a small library of them, each built from the primitives with CSG and transforms, so they stay parametric and differentiable like any other shape:
Rasmah.angle_bar — Function
angle_bar(a, b, t, length) -> FeatureAn L-shaped angle (angle bar): two perpendicular rectangular legs of thickness t, with leg lengths a (along x) and b (along y), extruded length along z. The corner sits at the origin and the legs extend along +x and +y. Asymmetric, so it is the go-to shape for showing transforms, booleans, and mirroring.
Arguments
a,b: the two leg lengths.t: the leg (wall) thickness.length: the extrusion depth along z.
Rasmah.i_beam — Function
i_beam(h, b, tf, tw, length) -> FeatureA structural I-beam (universal beam): a web of thickness tw joining two flanges of width b and thickness tf, over an overall height h and length length.
Arguments
h: overall depth (flange-to-flange).b: flange width.tf: flange thickness.tw: web thickness.length: the beam length along z.
Rasmah.channel — Function
channel(h, b, tf, tw, length) -> FeatureA structural channel (C-section): an I-beam with the flanges on one side only, so the cross-section is a "C".
Arguments
h: overall depth.b: flange width.tf: flange thickness.tw: web thickness.length: the length along z.
Rasmah.plate_with_hole — Function
plate_with_hole(w, h, t, r) -> FeatureA flat rectangular plate of width w, height h and thickness t, with a circular hole of radius r through its centre. The classic stress-concentration benchmark for linear-elasticity.
Arguments
w,h: plate width and height (in the x-y plane).t: plate thickness (along z).r: hole radius.
Rasmah.heat_sink — Function
heat_sink(w, h, t, nfins, fin_h, fin_t) -> FeatureA finned heat sink: a base plate of width w, depth h and thickness t, with nfins evenly spaced rectangular fins of height fin_h and thickness fin_t standing on it. The thermal benchmark for conduction/convection.
Arguments
w,h: base footprint (x and z); the fin extrusion depth ish.t: base plate thickness.nfins: number of fins.fin_h: fin height (in y, above the base).fin_t: fin thickness (in x).
Rasmah.gear — Function
gear(teeth, m, width) -> FeatureA spur gear: a root cylinder with teeth rectangular teeth spaced around its circumference at the given gear module m (pitch diameter = teeth * m). A mechanical/assembly benchmark shape.
Arguments
teeth: number of teeth.m: the gear module (pitch diameter =teeth * m).width: the gear face width (along z).
vtk(angle_bar(3, 4, 0.5, 2))The library is deliberately asymmetric — unlike a sphere or cube, a rotated or mirrored angle_bar looks different — which is exactly what the next section needs to make transforms visible.
Transforms
Transforms reposition a shape without changing it. Translation and rotation are rigid (they preserve volume); scaling multiplies every length and so scales volume by the cube of the factor.
Rasmah.translate — Function
translate(g, x, y, z)Rigidly translate the feature g by the vector (x, y, z).
Translation does not change a shape's volume; it only moves the bounds by the same vector. In the SDF it maps the query point by $x \mapsto x - t$, so the child field is evaluated in the un-translated frame.
Arguments
g: the feature to translate.x,y,z: translation offsets along each axis.
Returns
A Translate feature node.
Example
julia> using Rasmah
julia> bounds(evaluate(translate(sphere(1), 1, 2, 3), SDFBackend())) == ([0, 1, 2], [2, 3, 4])
true
julia> vtk(translate(sphere(1.0), 1.0, 2.0, 3.0))
VTKView(640×480, inline)translate(c::Component, t::AbstractVector) -> ComponentReturn a new component translated by t (orientation preserved).
Rasmah.scale — Function
scale(g, s)Uniformly scale the feature g by the factor s.
Scaling multiplies every length by |s|, so a shape's volume scales by $|s|^{3}$. A negative s additionally reflects the shape (mirrors it through the origin).
Arguments
g: the feature to scale.s: the scale factor (a positiveReal; negative to also reflect).
Returns
A Scale feature node.
Example
julia> using Rasmah
julia> volume(scale(sphere(1), 2.0))
33.510321638291124 m^3
julia> vtk(scale(sphere(1.0), 2.0))
VTKView(640×480, inline)Rasmah.rotate — Function
rotate(g, R)Rotate the feature g by the 3×3 rotation matrix R.
Rotation is rigid: it preserves volume. R must be an orthonormal matrix (its transpose is its inverse). The convenience constructors rotation_x, rotation_y, and rotation_z build R for a rotation about a coordinate axis.
Arguments
g: the feature to rotate.R: a 3×3 rotation matrix.
Returns
A Rotate feature node.
Example
julia> using Rasmah
julia> bounds(evaluate(rotate(sphere(1), rotation_z(0.0)), SDFBackend())) == ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0])
true
julia> vtk(rotate(sphere(1.0), rotation_z(0.3)))
VTKView(640×480, inline)rotate(c::Component, R::AbstractMatrix) -> ComponentReturn a new component rotated by R (about the origin).
vtk(translate(angle_bar(3, 4, 0.5, 2), 1.0, 2.0, 3.0))vtk(scale(angle_bar(3, 4, 0.5, 2), 2.0))vtk(rotate(angle_bar(3, 4, 0.5, 2), rotation_z(0.6)))A worked example
Let's build a simple part: a box with a cylindrical hole, moved to one side.
using Rasmah
part = translate(
difference(box(4, 2, 2), cylinder(0.5, 2.2)),
0, 0, 0,
)Translate{Difference{Box{Int64, Int64, Int64}, Cylinder{Float64, Float64}}, Int64, Int64, Int64}(Difference{Box{Int64, Int64, Int64}, Cylinder{Float64, Float64}}(Box{Int64, Int64, Int64}(4, 2, 2), Cylinder{Float64, Float64}(0.5, 2.2)), 0, 0, 0)bounds(evaluate(part, SDFBackend())) == ([-2.0, -1.0, -1.0], [2.0, 1.0, 1.0])trueThe bounding box is the box's box (the hole only removes material, so it does not change the extents). Now measure how much material is left: a 4 × 2 × 2 box with a cylinder of radius $0.5$ cut out.
volume(box(4, 2, 2)) - volume(cylinder(0.5, 2))14.429203673205103 m^3Next steps
With primitives and booleans in hand, the natural next step is to sweep 2D sketches into 3D — Sketches, extrusion, and revolution — and then to reshape solids with the Modeling operations (shell, fillet, chamfer, patterns, loft, sweep).
