Modeling operations
Overview
Once you have a basic solid, you usually want to modify it: hollow it out, round its edges, cut a chamfer, mirror it, repeat it in a pattern, or sweep a profile along a curve. These are the modeling operations.
Each operation is a small, differentiable recipe that builds a new signed distance field from an existing one. A hollow shell is a subtraction of two offsets; a fillet is a dilation; a mirror is a reflection of the query point. Because each step is differentiable, the whole CAD → simulation → loss pipeline remains differentiable through any number of them.
Hollowing and offset
Rasmah.shell — Function
shell(g, t)Hollow the feature g into a thin-walled shell of wall thickness 2t.
Shelling is difference(offset(g, t), offset(g, -t)): it grows the solid by t and erodes it by t, so what remains is a skin of thickness 2t following the original surface. The SDF is $|s_g(x)| - t$. For a sphere the volume is closed-form ($4\pi/3\cdot[(R + t)^{3} - (R - t)^{3}]$); other shapes shell exactly wherever the boolean kernel handles the two offsets.
Arguments
g: the solid to hollow.t: half the wall thickness (the wall is2tthick).
Example
julia> volume(shell(sphere(1.0), 0.2))
5.09356888902025 m^3
julia> vtk(shell(sphere(2.0), 0.2))
VTKView(640×480, inline)vtk(shell(sphere(2.0), 0.2))Rasmah.offset — Function
offset(g, d; join=:rounded)Offset the boundary of feature g by the signed distance d.
Positive d grows the solid (a Minkowski dilation), negative d erodes it. The SDF is simply $s_g(x) - d$, so it stays exact for primitives of the same family: a sphere of radius R becomes R + d, a box of side s becomes s + 2d, a cylinder (r, h) becomes (r + d, h + 2d). The general case is a faceted BRep of the offset field.
Arguments
g: the feature to offset.d: the offset distance (positive grows, negative shrinks).
Keyword arguments
join = :rounded: the corner treatment —:rounded(circular arcs),:miter(sharp, extended), or:bevel(flat 45° cuts).
Example
julia> volume(offset(sphere(1.0), 0.5))
14.137166941154067 m^3
julia> vtk(offset(sphere(2.0), 0.3))
VTKView(640×480, inline)vtk(offset(sphere(2.0), 0.3))Rasmah.dilate — Function
dilate(g, r)Grow a solid outward by a distance r (a Minkowski dilation).
Theory
Dilating a solid by $r$ means "take every point within $r$ of the surface". For a signed distance field this is simply $s(x) - r$: subtracting a constant from the field moves the zero level set outward by exactly that constant. A sphere grows $R \to R + r$, and a box grows a rounded-corner box of the same half-thickness $h + r$ (the corners round because they are exactly the points $r$ from the original corner). It is the same "offset outward" operation as offset, exposed under the mathematical name.
Arguments
g— an implicit geometry (FeatureorImplicitGeometry).r— the dilation distance (a length).
Returns
A new ImplicitGeometry offset outward by r. The inverse operation is a negative r, which shrinks the solid. dilate works at the SDF level, so wrap features in to_sdf(...).
Example
julia> bounds(dilate(to_sdf(box(2.0, 2.0, 2.0)), 0.5))
([-1.5, -1.5, -1.5], [1.5, 1.5, 1.5])dilate is the one-sided form of offset: it grows a solid outward by a fixed distance (or shrinks it for a negative distance), which is exactly what offset does outward — and what a fillet does to the edges.
Lofting and sweeping
Rasmah.loft — Function
loft(bottom, top, height)
loft(profiles::AbstractVector, heights::AbstractVector)Loft a solid between two cross-sections (or skin it through many).
A loft morphs one 2D profile into another as z sweeps from $-\mathrm{height}/2$ (bottom) to +height/2 (top). For circle→circle the result is an exact cone or frustum with an analytic volume; other pairs morph through rounded intermediates. The multi-profile form skins a solid through N ≥ 2 closed cross-sections at the given heights.
Arguments
bottom/top: the two cross-section sketches.height: the loft height.profiles/heights: (multi-profile form) the cross-sections and their heights.
Example
julia> volume(loft(circle_2d(1.0), circle_2d(2.0), 3.0))
21.991148575128552 m^3
julia> vtk(loft(circle_2d(1.0), circle_2d(2.0), 3.0))
VTKView(640×480, inline)vtk(loft(circle_2d(1.0), circle_2d(2.0), 3.0))Rasmah.sweep — Function
sweep(profile, path)Sweep a 2D profile along a path curve, holding it perpendicular to the path.
Sweeping is extrusion along a curve instead of a straight line. The profile is carried along path (a Curve3D such as a spline_path or helix) and the ends are capped with flat disks. The volume is Pappus's area(profile) · path length.
Arguments
profile: the 2D profile to sweep.path: the sweep path (aCurve3D).
Example
julia> volume(sweep(circle_2d(0.5), spline_path([[0.0, 0, 0], [2.0, 0, 0]])))
1.5707963267948966 m^3
julia> vtk(sweep(circle_2d(0.5), spline_path([[0.0, 0, 0], [2.0, 0, 0]])))
VTKView(640×480, inline)vtk(sweep(circle_2d(0.5), spline_path([[0.0, 0, 0], [2.0, 0, 0]])))Mirroring and patterns
Rasmah.mirror — Function
mirror(g, axis)Mirror the feature g across the plane perpendicular to axis.
axis is a coordinate axis (1 = x, 2 = y, 3 = z) or a datum_plane. The mirror is volume-preserving (it only reflects the shape). In the SDF the query point is reflected before evaluating the child field.
Arguments
g: the feature to mirror.axis: the coordinate axis whose normal plane is the mirror (1,2, or3), or adatum_plane.
Example
julia> bounds(evaluate(mirror(sphere(1.0), 1), SDFBackend())) == ([-1.0, -1.0, -1.0], [1.0, 1.0, 1.0])
true
julia> vtk(mirror(sphere(1.0), 1))
VTKView(640×480, inline)vtk(mirror(angle_bar(3, 4, 0.5, 2), 1))Mirroring the asymmetric angle_bar across the $x$ axis flips its handedness; a symmetric shape would look unchanged.
Rasmah.linear_pattern — Function
linear_pattern(g, axis, spacing, n)Repeat the feature g n times along a coordinate axis, spacing apart.
This is the "rectangular pattern" of a part — n equally-spaced copies of g translated along the axis (an integer 1/2/3, or a datum_axis). The SDF is the minimum over the copies, so the copies stay differentiable.
Arguments
g: the feature to pattern.axis: the pattern axis (1 = x,2 = y,3 = z) or adatum_axis.spacing: the distance between adjacent copies.n: the number of copies.
Example
julia> bounds(evaluate(linear_pattern(sphere(0.5), 1, 2.0, 3), SDFBackend())) == ([-0.5, -0.5, -0.5], [4.5, 0.5, 0.5])
true
julia> vtk(linear_pattern(sphere(0.5), 1, 2.0, 3))
VTKView(640×480, inline)linear_pattern(a::Assembly, name, dir, spacing, n) -> AssemblyReturn the assembly with n copies of the named component translated by k·spacing·dir. The original keeps its name; copies are named <name>_2 … <name>_n.
vtk(linear_pattern(angle_bar(1, 1, 0.3, 2), 1, 1.0, 3))Rasmah.circular_pattern — Function
circular_pattern(g, n; axis=3)Repeat the feature g n times around a rotation axis.
This is the "polar pattern" of a part — n equally-spaced copies of g rotated about the axis (an integer 1/2/3, or a datum_axis). The SDF is the minimum over the rotated copies.
A copy centred on the axis is unchanged by the rotation, so a visible ring needs the copy translated off the axis first.
Arguments
g: the feature to pattern.n: the number of copies.
Keyword arguments
axis = 3: the rotation axis (1 = x,2 = y,3 = z) or adatum_axis.
Example
julia> ring = circular_pattern(translate(sphere(0.5), 1.0, 0, 0), 4);
julia> field(evaluate(ring, SDFBackend()), [0.0, 0.0, 0.0])
0.5
julia> vtk(ring)
VTKView(640×480, inline)circular_pattern(a::Assembly, name, axis; n) -> AssemblyReturn the assembly with n copies of the named component rotated by 2πk/n about the axis (a DatumAxis). Copies are named <name>_2 … <name>_n.
vtk(circular_pattern(translate(angle_bar(1, 1, 0.3, 1), 1.0, 0, 0), 6))Edge treatments
Rasmah.fillet — Function
fillet(g, radius; edges=nothing)Round the convex edges and corners of g by dilating it with a ball of radius.
The whole-body form is a Minkowski dilation ($s_g(x) - r$): it both rounds convex corners and grows the shape by r (exact for spheres and boxes). When edges is given, only those named edges are blended by the exact rolling-ball fillet, which removes material.
Arguments
g: the feature to fillet.radius: the fillet radius.
Keyword arguments
edges = nothing: a topological edge name (or vector of names, e.g."Edge3") to fillet selectively, ornothingfor the whole body.
Example
julia> volume(fillet(sphere(1.0), 0.3)) == volume(sphere(1.3))
true
julia> vtk(fillet(sphere(1.0), 0.3))
VTKView(640×480, inline)vtk(fillet(sphere(1.0), 0.3))Rasmah.chamfer — Function
chamfer(g, distance; edges=nothing)Cut the convex edges of g with a flat 45° plane of setback distance.
The whole-body form is the L1-metric (Manhattan) analogue of fillet: a dilation by distance in the L1 metric, so it grows the shape with flat 45° edges (exact for an axis-aligned box). When edges is given, only those named edges are cut, which removes material.
Arguments
g: the feature to chamfer.distance: the chamfer setback.
Keyword arguments
edges = nothing: a topological edge name (or vector of names) to chamfer selectively, ornothingfor the whole body.
Example
julia> volume(chamfer(box(2.0, 2.0, 2.0), 0.2))
13.290666666666668 m^3
julia> vtk(chamfer(box(2.0, 2.0, 2.0), 0.2))
VTKView(640×480, inline)vtk(chamfer(box(2.0, 2.0, 2.0), 0.2))Drafting
Rasmah.draft — Function
draft(g::Feature, angle; axis::Int=3, ref::Real=0.0)Taper the cross-section of g along the pull axis by angle (radians).
Drafting is the machining operation that gives molded parts their release angle: each cross-section is scaled by a factor that grows linearly along the pull axis, narrowing (positive angle) toward +axis. ref is the height at which the cross-section is left undistorted (the mold's neutral plane).
Arguments
g: the feature to draft.angle: the taper angle in radians.
Keyword arguments
axis = 3: the pull axis (1 = x,2 = y,3 = z).ref = 0.0: the height where the cross-section is undistorted.
Example
julia> bounds(evaluate(draft(box(2.0, 2.0, 2.0), 0.2), SDFBackend())) == ([-1.2027100355086726, -1.2027100355086726, -1.0], [1.2027100355086726, 1.2027100355086726, 1.0])
true
julia> vtk(draft(box(2.0, 2.0, 2.0), 0.2))
VTKView(640×480, inline)vtk(draft(box(2.0, 2.0, 2.0), 0.2))A worked example: a mirrored, hollowed bracket
Combine several operations into one part — a box with rounded edges, hollowed out, then mirrored across the x = 0 plane:
part = mirror(shell(fillet(box(4.0, 2.0, 1.0), 0.2), 0.1), 1)Mirror{Hollow{Fillet{Box{Float64, Float64, Float64}, Float64}, Float64}, Int64}(Hollow{Fillet{Box{Float64, Float64, Float64}, Float64}, Float64}(Fillet{Box{Float64, Float64, Float64}, Float64}(Box{Float64, Float64, Float64}(4.0, 2.0, 1.0), 0.2), 0.1), 1)Each operation stayed differentiable, so the whole part can still feed a gradient-based optimizer. The bounding box is the outer extent of the rounded, hollowed box:
bounds(evaluate(part, SDFBackend()))([-2.3000000000000003, -1.3, -0.7999999999999999], [2.3000000000000003, 1.3, 0.7999999999999999])Because the box is symmetric, the mirror leaves the bounding box unchanged while doubling the part.
Next steps
The geometry chapters end with NURBS and the exact boundary representation in NURBS and the exact BRep. After that, the natural next stop is Meshing: from shapes to triangles, which turns any of these shapes into a mesh for simulation.
