RasmahRasmah

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.sphereFunction
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-aware Quantity length): 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^2
source
vtk(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.boxFunction
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)
source
vtk(box(2, 3, 4))

Cylinder, cone, torus

Rasmah.cylinderFunction
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)
source
vtk(cylinder(1.0, 2.0))
Rasmah.coneFunction
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)
source
vtk(cone(1.0, 2.0))
Rasmah.torusFunction
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)
source
vtk(torus(2, 0.5))

Wedge, prism, tube

Rasmah.wedgeFunction
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)
source
vtk(wedge(2, 3, 4))
Rasmah.prismFunction
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)
source
vtk(prism(6, 1, 2))
Rasmah.tubeFunction
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)
source
vtk(tube(2, 1, 3))

The half-space plane

Rasmah.planeFunction
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)
source
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_solidFunction
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.

source
Rasmah.threadFunction
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 to pitch * 0.2.
source
volume(helix_solid(1.0, 0.5, 3, 0.1))
0.5918489312887331 m^3

A 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.differenceFunction
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 from a.

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)
source
difference(a::BRep, b::BRep; sdf_n::Int=8) -> BRep

Boolean 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.
source
Rasmah.intersectionFunction
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)
source
intersection(a::BRep, b::BRep; sdf_n::Int=8) -> BRep

Boolean 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.
source
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_unionFunction
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 (Feature or ImplicitGeometry).
  • 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
true

See also

smooth_intersection, smooth_difference.

source
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_barFunction
angle_bar(a, b, t, length) -> Feature

An 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.
source
Rasmah.i_beamFunction
i_beam(h, b, tf, tw, length) -> Feature

A 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.
source
Rasmah.channelFunction
channel(h, b, tf, tw, length) -> Feature

A 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.
source
Rasmah.plate_with_holeFunction
plate_with_hole(w, h, t, r) -> Feature

A 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.
source
Rasmah.heat_sinkFunction
heat_sink(w, h, t, nfins, fin_h, fin_t) -> Feature

A 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 is h.
  • t: base plate thickness.
  • nfins: number of fins.
  • fin_h: fin height (in y, above the base).
  • fin_t: fin thickness (in x).
source
Rasmah.gearFunction
gear(teeth, m, width) -> Feature

A 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).
source
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.translateFunction
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)
source
translate(c::Component, t::AbstractVector) -> Component

Return a new component translated by t (orientation preserved).

source
Rasmah.scaleFunction
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 positive Real; 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)
source
Rasmah.rotateFunction
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)
source
rotate(c::Component, R::AbstractMatrix) -> Component

Return a new component rotated by R (about the origin).

source
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])
true

The 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^3

Next 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).