Modeling with functions and pipelines

Copy Markdown View Source

A Smith model records operations. Every operation returns a new recipe, so feature functions can accept a body, add a feature, and return the next body. Smith.evaluate/1 executes the ordered operations and returns a native shape.

Primitives and placement

Boxes, cylinders, and cones accept the same placement options:

  • at: {x, y, z} anchors the primitive in world coordinates; default {0, 0, 0}.
  • align: {x, y, z} selects :min, :center, or :max independently for each axis. The selected point of the primitive's bounds lands at at:.
PrimitiveDefault alignmentMeaning of dimensions
Smith.box(width, depth, height, opts){:min, :min, :min}Positive X, Y, Z extents
Smith.cylinder(radius, height, opts){:center, :center, :min}Radius and positive Z height
Smith.cone(bottom_radius, top_radius, height, opts){:center, :center, :min}Bottom/top radii and positive Z height

All dimensions are millimeters. Dimensions must exceed the native length tolerance of 1.0e-7 mm; a cone may have one zero end radius. Alignment uses bounds before any subsequent rotations. A cone's X/Y bounds include the larger radius, and its centered Z is halfway up its height, regardless of its center of mass. Unsupported options, malformed points, and invalid alignment return a Smith.Error with reason :invalid_options during evaluation. Invalid dimensions retain the native :invalid_argument error.

boss = Smith.cylinder(5, 8, at: {15, 0, 4})
plate = Smith.box(40, 20, 4, align: {:center, :center, :min})
model = plate |> Smith.fuse(boss)

To describe a centered footprint between two elevations, keep the height explicit:

bottom = 6
top = 14
body = Smith.box(30, 20, top - bottom,
  at: {12, -8, bottom}, align: {:center, :center, :min})

Use sketches for local 2D outlines and arbitrary planes; their at: uses two local coordinates, while solid primitive at: uses three world coordinates.

Smith.rotate(model, axis, degrees, origin) uses a world axis vector and optional world origin (default zero). Rotation is right-handed. Transform order matters: translating then rotating moves the translated position around the rotation axis. Use Smith.Plane and local sketches for side-mounted profiles rather than manually rotating every point.

Boolean composition

fuse/2 adds material, cut/2 subtracts a tool, and common/2 retains the intersection. Fuse and cut also accept ordered lists. An empty list leaves the recipe alone. Each operation resolves its tool recipe and cleans same-domain topology afterward.

bores = for x <- [-12, 12], do: Smith.cylinder(2, 6, at: {x, 0, -1})
model = plate |> Smith.cut(bores)

Extend cutting tools past the surface when the intended feature is through-all; this avoids relying on coincident faces. Alternatively use Smith.hole/2 with through: :all, which sizes its cutter across the complete body.

A missed Boolean tool can be a no-op; a missed hole/2 fails explicitly. Disjoint unions can contain multiple solids. Smith.compound/1 groups shapes without fusing and preserves separate boundaries. For separately named and printable parts, use assemblies.

Finish the current geometry

model =
  Smith.box(60, 40, 5)
  |> Smith.fillet(edges: {:parallel, :z}, radius: 2, count: 4)
  |> Smith.hole(on: :top, diameter: 8, through: :all)

Fillet/chamfer selectors run against the body at that evaluation step. {:parallel, axis} selects straight edges parallel to the world axis; curved edges are not included. edges: :all selects all edges. A predicate receives native edge information together with its world bounds and a point at the middle of its parameter interval. For a spline, that point need not divide its length in half:

round_vertical = fn edge ->
  edge.type == :line and abs(elem(edge.direction, 2)) > 0.99
end

model = Smith.box(20, 10, 4) |> Smith.fillet(edges: round_vertical, radius: 1, count: 4)

For reusable selections, compose Smith.Selector filters such as Selector.type(:line) |> Selector.parallel(:z). The same selector machinery supports face queries and shell openings. Smith.edges(result, selector) and Smith.faces(result, selector) inspect evaluated geometry.

Use count: when the design expects a particular number of edges. Unexpected counts fail rather than rounding unintended geometry. Avoid relying on enumeration order; topology can change after booleans and cleanup. Predicates must return booleans, and exceptions in your own callbacks propagate.

Finishing order is part of design intent. Filleting an outside edge before drilling can differ from filleting all edges after drilling. Keep those choices in named feature functions.

Place holes deliberately

on: :top selects the highest outward +Z planar face. Its default hole position is the area centroid of that face, which can move after a cut. Two successive top holes with the same offset need not share the same XY position. If the design specifies fixed hole coordinates, use a plane:

model =
  Smith.box(60, 40, 5)
  |> Smith.hole(on: Smith.Plane.xy(), at: {10, 20}, diameter: 4, through: :all)
  |> Smith.hole(on: Smith.Plane.xy(), at: {50, 20}, diameter: 4, through: :all)

The plane supplies a frame and drill direction; it does not need to coincide with a face. The cutter spans the body's projected bounds. If several top faces share the highest elevation, on: :top fails with :ambiguous_top_face instead of choosing one.

Curves and solid generation

Smith.line/2, arc/6, and spline/2 describe world-coordinate edges. Smith.profile/1 joins ordered edges into a closed planar face. Smith.polygon/1 creates a planar face from world-coordinate points. Extrude a face recipe with a world vector.

Prefer Smith.Sketch for local 2D outlines. Its signed scalar extrusion, full/partial revolve, and loft are covered in Sketches and planes. See paths, lofts, and shells for open paths, smooth interpolation, sweeps, and hollowing.

Evaluation and branching

base = Smith.box(20, 10, 4)
rounded = base |> Smith.fillet(edges: {:parallel, :z}, radius: 1)
drilled = base |> Smith.hole(on: :top, diameter: 3, through: :all)

All three recipes remain independent. Native geometry is immutable too. The evaluated revision is a SHA-256 hash of serialized BREP, useful for export identity and detecting stale results. It is not promised to remain identical across kernel versions or platforms, and it is not a parametric editing format. Keep the Elixir recipe as the design source.

For measured filters, sorting, topology metadata, mirrored parts, blind holes, and recessed fasteners, see mechanical parts.