RasmahRasmah

Additional Primitives: Torus and Wedge

using Rasmah

# Geometry is stored in SI (metres); these helpers render millimetres.
mm(v)  = round(uconvert(u"mm", v); digits = 1)
mm3(v) = round(uconvert(u"mm^3", v); digits = 1)
mm3 (generic function with 1 method)

Overview

Beyond the classic four (sphere, box, cylinder, cone), Rasmah ships two more analytic primitives with exact volume, bounds, and BRep: the torus (torus(major, minor)) and the wedge (wedge(w, h, d)). Like the others, each carries an exact analytic signed distance field and an exact seam-vertexed BRep, so both work in the signed distance field, the exact BRep, and the mesh — and both are differentiable.

These two shapes cover a surprising amount of everyday hardware: a torus is an O-ring or a circlip (snap ring), a wedge is a doorstop, and the catalog’s prism and tube round it out with the hex nut and the pipe section. Each example below is a real part, dimensioned in millimetres (the code writes SI metres).

The torus

A torus is parameterized by its major radius R (from the centre to the tube centre) and its minor (tube) radius r. Its signed distance field is

\[ \mathrm{torus}(x) = \sqrt{\big(\sqrt{x^2 + y^2} - R\big)^2 + z^2} - r, \]

with analytic volume 2π² R r² (Pappus’s theorem). An O-ring with a 12 mm mean diameter and a 2 mm cross-section is torus(R = 6 mm, r = 1 mm):

oring = torus(0.006, 0.001)                  # R = 6 mm, r = 1 mm

println("torus SDF at origin: ", field(oring, [0.0, 0.0, 0.0]))
println("analytic volume:     ", mm3(volume(oring)))   # 2π²·6·1² = 118.4 mm³
lo, hi = bounds(oring)
println("bounds:              ", mm.(lo), " → ", mm.(hi))
torus SDF at origin: 0.005
analytic volume:     118.4
bounds:              [-7.0, -7.0, -1.0] → [7.0, 7.0, 1.0]

The field is negative inside the tube, zero on the surface, and positive elsewhere — the same convention as every other primitive, so the torus composes with CSG, transforms, and dilate/fillet exactly like a box or sphere.

vtk(oring; n = 48)

Torus BRep

The torus’s BRep is a single closed toroidal surface — a periodic (u, v) ∈ [0, 2π]² surface with no seam and no boundary, so it has one face and a placeholder self-loop edge:

b = brep(torus(0.006, 0.001))

println("faces:   ", length(b.faces))
faces:   1

Because the torus has no real edges (it is closed in both parameters), the edge-usage predicate is_watertight reports false — but the surface is a closed 2-manifold by construction.

A circlip: cutting the tube

Because the torus is a first-class BRep primitive, the boolean kernel clips it against any other solid exactly. A circlip — the C-shaped retaining ring that holds a bearing in its seat — is a torus with a small rectangular gap cut through the wire:

gap = translate(box(0.004, 0.004, 0.004), 0.006, 0.0, 0.0)   # 4 mm gap over the wire
circlip = difference(torus(0.006, 0.001), gap)

println("cut faces: ", length(brep(circlip).faces))
println("cut volume: ", round(uconvert(u"mm^3", volume(surface_mesh(brep(circlip); n=64))); digits = 2), " mm³")
println("analytic  : ", round(2π^2 * 6 - π * 4; digits = 2), " mm³ (torus minus the gap)")
cut faces: 3
cut volume: 105.56 mm³
analytic  : 105.87 mm³ (torus minus the gap)

The signed volume of the tessellated result is the boolean-orientation oracle: it matches the analytic torus volume minus the volume of the part of the box inside the tube (here a π r²-area wire times the 4 mm gap).

vtk(circlip; n = 48)

The wedge

The wedge is a right-triangular prism: the box [±w/2, ±h/2, ±d/2] cut by the diagonal plane x/w + y/h ≤ 0. Its SDF is exact, with analytic volume w·h·d/2 (half the enclosing box). A 40 × 20 × 60 mm doorstop is the canonical example:

doorstop = wedge(0.040, 0.020, 0.060)         # 40 × 20 × 60 mm

println("wedge volume: ", mm3(volume(doorstop)))   # 40·20·60/2 = 24000 mm³
wedge volume: 24000.0

Its BRep is the five faces of the prism (two triangular end faces, two rectangular sides, and the slanted rectangular face):

bw = brep(wedge(0.040, 0.020, 0.060))
println("wedge faces: ", length(bw.faces), ", watertight ", is_watertight(bw))
wedge faces: 5, watertight true
vtk(doorstop; n = 48)

Wedge booleans

The wedge composes too — for example a sphere punched through its slanted face makes the finger hole of a doorstop:

poke = difference(wedge(0.040, 0.020, 0.060), sphere(0.008))
println("wedge − sphere faces: ", length(brep(poke).faces))
println("wedge − sphere volume: ", mm3(volume(surface_mesh(brep(poke); n=64))))
wedge − sphere faces: 7
wedge − sphere volume: 22925.9
vtk(poke; n = 48)

The hex nut and the pipe

Two more analytic primitives round out the catalog. prism(n, circumradius, height) is a regular n-gon extruded along z; tube(ro, ri, height) is a hollow (annular) cylinder:

p = prism(6, 0.0104, 0.020)
println(volume(p))                       # (n/2) R² sin(2π/n) h

t = tube(0.010, 0.005, 0.020)
println(volume(t))                       # π (ro² − ri²) h
println(isapprox(volume(t), π * (0.010^2 - 0.005^2) * 0.020))
5.620158460399493e-6 m^3
4.71238898038469e-6 m^3
true

Both are exact in the BRep too — the prism is a regular hexagonal polyhedron, the tube an annular cylinder — and both compose with the boolean kernel.

An M8 hex nut is a hexagonal prism with a tapped hole: across flats 13 mm (circumradius 13/√3), 6.8 mm tall, and an Ø8 hole. The machined volume is the hexagon area times height minus the hole — checked against the mesh:

nut = difference(prism(6, 0.013 / √3, 0.0068), cylinder(0.004, 0.0068))

println("nut bounds   : ", mm.(bounds(nut)[2]))   # ±7.5 in xy, ±3.4 in z
println("nut volume   : ", mm3(volume(surface_mesh(nut; n=48))), " mm³ (mesh)")
println("analytic     : ",
        round(3 * (13 / √3)^2 * sin(π / 3) * 6.8 - π * 4^2 * 6.8; digits = 1), " mm³")
nut bounds   : [7.5, 7.5, 3.4]
nut volume   : 651.3 mm³ (mesh)
analytic     : 653.4 mm³
vtk(nut; n = 48)

A 1-inch schedule-40 pipe section — OD 33.4 mm, wall 3.38 mm, 50 mm long — is a tube with the ID as the outer minus twice the wall:

pipe = tube(0.0167, 0.0134, 0.050)              # OD 33.4, ID 26.8, 50 mm long

println("pipe volume: ", mm3(volume(pipe)), " mm³")
println("analytic   : ", round(π * (16.7^2 - 13.4^2) * 50.0; digits = 1), " mm³")
pipe volume: 15602.7 mm³
analytic   : 15602.7 mm³

Next steps