Smith (Smith v0.1.0)

Copy Markdown View Source

CAD recipes built with Elixir functions and pipelines.

Constructors and modeling operations return immutable Smith.Model values. evaluate/1 executes the recipe through OCEx and returns native geometry. Use ordinary functions for features and comprehensions for patterns; no process or mutable modeling context is required.

iex> model =
...>   Smith.box(60, 40, 5)
...>   |> Smith.fillet(edges: {:parallel, :z}, radius: 2, count: 4)
...>   |> Smith.hole(on: :top, diameter: 8, through: :all)
iex> {:ok, part} = Smith.evaluate(model)
iex> {:ok, [solid]} = OCEx.solids(part.shape)
iex> OCEx.valid?(solid)
{:ok, true}

Units and coordinates

Dimensions are millimeters and modeling angles are degrees. Transforms use world coordinates; sketches use a Smith.Plane with local 2D coordinates. Mesh angular tolerance is in radians.

Boxes begin at the origin by default. Cylinders and cones are centered on world Z with their bottoms at Z=0. Spheres and tori are centered at the origin. All solid primitives support explicit placement with :at and per-axis :align.

Where to start

Recipe validation runs during evaluation. Use valid structs and documented argument types; arbitrary malformed Elixir terms and callback exceptions are not converted into modeling errors. See errors and limits.

Summary

Primitives

Describes a box with X, Y, and Z dimensions in millimeters.

Describes a cone or frustum along world +Z.

Describes a cylinder along world +Z, using radius and height in mm.

Describes a sphere by radius in mm, centered at the origin by default.

Describes a complete ring torus around world Z, centered at the origin.

Profiles

Describes a circular arc in a world-coordinate plane.

Describes an extrusion of a sketch, face recipe, or compound of planar faces.

Extrudes with symmetric extent or tapered walls.

Extrudes a profile until it meets an infinite target plane.

Fills one closed planar wire as a face recipe.

Describes a directed straight edge between two world points.

Describes a capped solid through two or more ordered sketches.

Describes a planar polygon face from world-coordinate points.

Describes a planar face bounded by an ordered list of edge recipes.

Projects a sketch, path, or edge/face recipe onto target surfaces.

Describes a solid formed by revolving a sketch or face about a world axis.

Takes a filled cross section of solid material on a world plane.

Describes an interpolated, nonperiodic B-spline edge in world coordinates.

Extracts selected faces and sews their shared edges into a surface recipe.

Modeling

Appends an equal-distance bevel on selected edges.

Appends same-domain face and edge simplification.

Retains the intersection with a tool recipe, then cleans its topology.

Groups model recipes without fusing their geometry.

Drills a hole with a cylindrical recess for a fastener head.

Drills a hole with a conical recess for a countersunk fastener.

Subtracts one tool recipe or an ordered list from the current body.

Tapers selected faces around a neutral plane.

Appends constant-radius rounding on selected edges.

Unites a model with one tool recipe or an ordered list of recipes.

Appends a circular through-all or flat-bottomed blind hole.

Reflects a recipe across a world plane.

Offsets a solid or surface by a signed normal distance in mm.

Appends a right-handed rotation in degrees about a world axis.

Hollows the current solid by removing selected faces and offsetting its walls.

Divides solid geometry with an infinite world plane.

Sweeps a placed sketch along an open Smith.Path.

Builds solid material between an open surface and its signed offset.

Appends a world-coordinate translation in millimeters.

Topology

Selects edges from an evaluated result using Smith.Selector.

Selects faces from an evaluated result using Smith.Selector.

Returns selected edges with their geometry metadata and native :shape handles.

Returns selected faces with their geometry metadata and native :shape handles.

Evaluation and export

Builds native geometry from a model, sketch, or assembly recipe.

Writes one geometry file, choosing its format from the path extension.

Writes a part or assembly bundle and updates the output manifest after checks pass.

Primitives

box(x, y, z, opts \\ [])

@spec box(number(), number(), number(), [primitive_option()]) :: Smith.Model.t()

Describes a box with X, Y, and Z dimensions in millimeters.

Returns a recipe. Geometry is built by evaluate/1; each dimension must exceed the native linear tolerance of 1.0e-7 mm.

Placement options

  • :at — world anchor {x, y, z}; defaults to {0, 0, 0}.
  • :align — one of :min, :center, or :max for each axis; defaults to {:min, :min, :min}.

Alignment chooses the point of the unrotated bounding box that lands at :at. For example, {:center, :center, :min} centers the footprint and places its bottom at the anchor's Z coordinate. Later transformations act on this placed geometry.

Unknown or duplicate options, malformed points, and invalid alignments produce a Smith.Error with reason :invalid_options at evaluation. Invalid native dimensions or numbers produce :invalid_argument.

Examples

iex> model = Smith.box(20, 10, 4, at: {0, 0, 6}, align: {:center, :center, :min})
iex> {:ok, part} = Smith.evaluate(model)
iex> OCEx.bounds(part.shape)
{:ok, {{-10.0, -5.0, 6.0}, {10.0, 5.0, 10.0}}}

cone(bottom_radius, top_radius, height, opts \\ [])

@spec cone(number(), number(), number(), [primitive_option()]) :: Smith.Model.t()

Describes a cone or frustum along world +Z.

bottom_radius is at the bottom and top_radius is at the top. Radii must be nonnegative and differ by more than 1.0e-7 mm; one may be zero. Height must exceed 1.0e-7 mm. Use cylinder/3 for equal radii.

Supports the :at and :align options of box/4, defaulting to {:center, :center, :min}. Bounds include the larger radius. Center alignment uses the bounds midpoint, so centered Z is halfway up the height regardless of the cone's center of mass.

cylinder(radius, height, opts \\ [])

@spec cylinder(number(), number(), [primitive_option()]) :: Smith.Model.t()

Describes a cylinder along world +Z, using radius and height in mm.

Both dimensions must exceed 1.0e-7 mm at evaluation. Supports the :at and :align options of box/4, with default alignment {:center, :center, :min}: the axis passes through the anchor's XY coordinates and the bottom starts at its Z coordinate.

iex> {:ok, pin} = Smith.cylinder(2, 8, at: {10, 0, 3}) |> Smith.evaluate()
iex> OCEx.bounds(pin.shape)
{:ok, {{8.0, -2.0, 3.0}, {12.0, 2.0, 11.0}}}

sphere(radius, opts \\ [])

@spec sphere(number(), [primitive_option()]) :: Smith.Model.t()

Describes a sphere by radius in mm, centered at the origin by default.

Radius must exceed 1.0e-7 mm. Supports the :at and :align options of box/4, defaulting to {:center, :center, :center}.

torus(major_radius, minor_radius, opts \\ [])

@spec torus(number(), number(), [primitive_option()]) :: Smith.Model.t()

Describes a complete ring torus around world Z, centered at the origin.

major_radius measures from the axis to the tube center; minor_radius is the tube radius, both in mm. Both radii and their difference must exceed 1.0e-7 mm. Supports :at and :align as in box/4, defaulting to {:center, :center, :center}. Rotate the recipe for another axis. Horn and spindle tori fail with :invalid_argument at evaluation.

Profiles

arc(center, normal, x_direction, radius, start, sweep)

Describes a circular arc in a world-coordinate plane.

center is the circle center. normal and x_direction must be nonzero and nonparallel; the latter is projected into the plane to set zero degrees. Radius must exceed 1.0e-7 mm.

start and signed sweep use degrees. Positive sweep follows the right-hand rule around the normal. The absolute sweep must be greater than 1.0e-9 and at most 360. The recipe evaluates to an edge. For local 2D coordinates see Smith.Sketch.arc/4.

extrude(sketch, distance)

@spec extrude(Smith.Sketch.t(), number()) :: Smith.Model.t()
@spec extrude(Smith.Model.t(), {number(), number(), number()}) :: Smith.Model.t()

Describes an extrusion of a sketch, face recipe, or compound of planar faces.

With a Smith.Sketch, supply a signed distance in millimeters. Positive distance follows its plane normal; negative distance extends behind the plane. Zero fails with :invalid_extrusion. Sketch holes pass through the solid.

With a Smith.Model that evaluates to planar faces, supply a world vector {x, y, z}. It must have a nonzero normal component; extrusion within the face plane fails with :degenerate_extrusion. Native length tolerances also apply. Each face produces its own solid; results are not fused. This supports disconnected regions from section/2. A scalar distance is only supported for sketches.

iex> model = Smith.Sketch.rectangle(4, 6) |> Smith.extrude(-2)
iex> {:ok, part} = Smith.evaluate(model)
iex> OCEx.bounds(part.shape)
{:ok, {{-2.0, -3.0, -2.0}, {2.0, 3.0, 0.0}}}

extrude(sketch, distance, opts)

@spec extrude(Smith.Sketch.t(), number(), keyword()) :: Smith.Model.t()
@spec extrude(Smith.Model.t(), {number(), number(), number()}, keyword()) ::
  Smith.Model.t()

Extrudes with symmetric extent or tapered walls.

Accepts the same profile and distance/vector forms as extrude/2. Options default to both: false and taper: 0. With both: true, the supplied distance applies on each side of the profile, so a distance of 5 makes a total depth of 10. The sign selects the first direction; both halves have equal extent.

taper: is in degrees, strictly between −90 and 90. Positive values narrow outer walls and widen holes away from the starting profile; negative values widen outer walls and narrow holes. Each symmetric half tapers away from the shared starting plane. The profile is the neutral section, not a scaled copy of the end section.

Nonzero taper requires travel normal to the profile and straight or circular boundary edges. Collapsing walls, unsupported side surfaces, and topology changes fail through OCEx with this recipe step's context. See OCEx.extrude/3 for native limits and error reasons. Options are validated at evaluation. Earlier recipes remain reusable.

extrude_until(profile, plane, opts \\ [])

@spec extrude_until(
  Smith.Model.t() | Smith.Sketch.t(),
  Smith.Plane.t() | :xy | :xz | :yz,
  keyword()
) ::
  Smith.Model.t()

Extrudes a profile until it meets an infinite target plane.

The target is a Smith.Plane or :xy, :xz, or :yz. Sketches travel along their plane normal by default. Supply direction: {x, y, z} for an oblique or reversed world direction; the vector is normalized. Face recipes require this option explicitly.

The entire profile must lie behind the target in the travel direction. A target crossing or touching the starting profile returns :target_not_ahead. The target may be tilted relative to the profile; its normal's sign does not affect the result. Holes and disconnected face regions remain intact, with a separate solid for each face.

Walls are straight and untapered. :direction is the only option; :both and :taper are not accepted here. This stops at a plane, not the nearest face of another body. See OCEx.extrude_until/4 for native tolerances and failure reasons. Geometry errors retain recipe step context, and input recipes remain unchanged.

face(model)

@spec face(Smith.Model.t()) :: Smith.Model.t()

Fills one closed planar wire as a face recipe.

Use after project/3 when its result is a single closed outline. The face retains the wire's world placement. It can then be extruded, revolved, or used in face Boolean operations. Input recipes remain reusable.

This does not infer holes or choose a wire from multiple projection hits. A compound of wires returns :wrong_shape_type, an open wire returns :open_wire, and nonplanar or invalid boundaries fail in OCEx. Select a single target surface before projection when only one outline is wanted. To construct a face from edge recipes directly, use profile/1.

line(from, to)

@spec line(OCEx.point3(), OCEx.point3()) :: Smith.Model.t()

Describes a directed straight edge between two world points.

The points must be more than 1.0e-7 mm apart. Use ordered edge recipes with profile/1 to make a face. For plane-local 2D edges use Smith.Sketch.line/2.

loft(sketches, opts \\ [])

@spec loft([Smith.Sketch.t()], keyword()) :: Smith.Model.t()

Describes a capped solid through two or more ordered sketches.

Each sketch uses its own plane and must have exactly one boundary wire. Sections with holes fail with :loft_profile_has_holes. A cut touching the outer edge is allowed if it leaves one boundary. OCCT chooses edge correspondence; there are no guide rails or seam controls.

ruled: true (default) joins adjacent sections with straight generators. Use ruled: false for a smooth interpolating loft. Smooth interpolation can overshoot between sections; inspect the result for your dimensions. Smith requires one solid with volume greater than 1.0e-9 mm³.

iex> sections = [
...>   Smith.Sketch.rectangle(20, 10),
...>   Smith.Sketch.rectangle(10, 5, on: Smith.Plane.xy(z: 12))
...> ]
iex> {:ok, transition} = Smith.loft(sections) |> Smith.evaluate()
iex> {:ok, solids} = OCEx.solids(transition.shape)
iex> length(solids)
1

polygon(points)

@spec polygon([OCEx.point3()]) :: Smith.Model.t()

Describes a planar polygon face from world-coordinate points.

Supply at least three points in boundary order. Closure is implicit; do not repeat the first point. The outline must form a valid closed planar face. For 2D points and workplane placement use Smith.Sketch.polygon/2.

profile(edges)

@spec profile([Smith.Model.t()]) :: Smith.Model.t()

Describes a planar face bounded by an ordered list of edge recipes.

The list must be nonempty, connected, and closed. Open wires fail with :open_wire; disconnected edges fail with :disconnected_wire. Nonplanar or invalid boundaries fail in the native kernel. Inner loops are not accepted here; use Smith.Sketch.cut/2 for sketch holes.

iex> outline = [
...>   Smith.line({0, 0, 0}, {4, 0, 0}),
...>   Smith.line({4, 0, 0}, {0, 3, 0}),
...>   Smith.line({0, 3, 0}, {0, 0, 0})
...> ]
iex> {:ok, face} = Smith.profile(outline) |> Smith.evaluate()
iex> OCEx.area(face.shape)
{:ok, 6.0}

project(sketch, target, opts)

Projects a sketch, path, or edge/face recipe onto target surfaces.

The target is a model or sketch recipe. Supply exactly one option: direction: {x, y, z} for parallel projection or from: {x, y, z} for projection through a world point. Parallel directions are normalized. Coordinates remain in world space.

Sketches contribute their boundary wires, including holes. Results are wires, which may be open when clipped by the target. They are not filled faces or printable solids. Multiple target hits are retained, and parallel projection is bidirectional. Use surface/2 to select target faces before projecting when only one side of a body is wanted. Conical projection follows half-rays from the point through the source, excluding the opposite side of that point.

Source collections fail if a boundary misses or projection fails. The result can be inspected with edges/2 or native wire/curve queries. Use face/1 to fill a single closed planar outline before extrusion. Solid source recipes are not accepted: explicitly select their surfaces first. See OCEx.project/3 for topology and failure details. Inputs stay reusable; target failures retain their nested recipe context.

revolve(profile, axis, degrees \\ 360, origin \\ {0, 0, 0})

Describes a solid formed by revolving a sketch or face about a world axis.

axis is a nonzero direction vector and origin is a point on that axis (default {0, 0, 0}). degrees defaults to 360 and must be greater than 1.0e-7 and at most 360. Reverse the axis for the opposite turn. Partial revolutions include end faces.

Place the profile so that its sweep forms valid geometry. Smith checks for one solid and volume greater than 1.0e-9 mm³; otherwise evaluation returns :invalid_solid or a native construction error.

iex> profile =
...>   Smith.Sketch.rectangle(2, 10,
...>     align: {:min, :min},
...>     at: {4, 0},
...>     on: Smith.Plane.xz()
...>   )
iex> {:ok, sleeve} = Smith.revolve(profile, {0, 0, 1}) |> Smith.evaluate()
iex> {:ok, volume} = OCEx.volume(sleeve.shape)
iex> abs(volume - :math.pi() * (6 * 6 - 4 * 4) * 10) < 1.0e-6
true

section(model, plane)

@spec section(Smith.Model.t(), :xy | :xz | :yz | Smith.Plane.t()) :: Smith.Model.t()

Takes a filled cross section of solid material on a world plane.

Accepts :xy, :xz, :yz, or a positioned Smith.Plane. Returns a deferred model whose result contains planar faces, retaining inner holes and disconnected regions. One region is a face; zero or several regions form a compound. An outside or point/edge-tangent plane gives no faces. A coincident boundary face remains in the section.

Coordinates stay in world space and face normals follow the plane's normal. Inspect with faces/2, inspect_faces/2, or OCEx.area/1. Extrude with a world vector to turn the section into solids. Bare section faces cannot be exported as printable bundles. This is an intersection, not a projection, and its input must contain only solid geometry.

iex> section = Smith.box(4, 6, 8) |> Smith.section(Smith.Plane.xy(z: 3))
iex> {:ok, part} = section |> Smith.extrude({0, 0, 2}) |> Smith.evaluate()
iex> {:ok, volume} = OCEx.volume(part.shape)
iex> abs(volume - 48) < 1.0e-6
true

spline(points, tangents \\ nil)

@spec spline([OCEx.point3()], {OCEx.point3(), OCEx.point3()} | nil) :: Smith.Model.t()

Describes an interpolated, nonperiodic B-spline edge in world coordinates.

Supply 2 to 100,000 points, with consecutive points more than 1.0e-6 mm apart. tangents is nil or {start_vector, end_vector}; vectors must be nonzero, and OCCT scales their magnitudes. The interpolation tolerance is 1.0e-6 mm. These are points on the curve, not control points. See OCEx.spline/2 for the native contract.

surface(model, selector \\ :all)

Extracts selected faces and sews their shared edges into a surface recipe.

The selector defaults to :all and resolves against the geometry at this step. Connected faces form shells; a single face remains a face; disconnected surfaces form a compound. A closed shell remains a surface, not a filled solid. Coordinates and source face orientations are retained.

Use this to extract a curved wall for offset/3 or thicken/3. Empty selections return :empty_selection. Sewing uses 1.0e-7 mm tolerance and rejects non-manifold joins. The original model is unchanged. Bare surfaces are not printable bundles; thicken them first.

Modeling

chamfer(model, opts)

@spec chamfer(Smith.Model.t(), keyword()) :: Smith.Model.t()

Appends an equal-distance bevel on selected edges.

Requires edges: and distance:, with optional positive integer count:. Selectors and their failure behavior match fillet/2. Distance is in millimeters and must exceed 1.0e-7. The native kernel can reject a distance that does not fit the surrounding geometry.

iex> recipe =
...>   Smith.box(20, 10, 4)
...>   |> Smith.chamfer(edges: {:parallel, :z}, distance: 1, count: 4)
iex> {:ok, part} = Smith.evaluate(recipe)
iex> {:ok, volume} = OCEx.volume(part.shape)
iex> abs(volume - 792.0) < 1.0e-6
true

clean(model)

@spec clean(Smith.Model.t()) :: Smith.Model.t()

Appends same-domain face and edge simplification.

Adjacent faces or edges on compatible underlying geometry may be merged. This can change topology and edge counts. Later selectors run against the new body. Smith already cleans the results of fuse/2, cut/2, common/2, fillet/2, and chamfer/2.

common(model, tool)

@spec common(Smith.Model.t(), Smith.Model.t()) :: Smith.Model.t()

Retains the intersection with a tool recipe, then cleans its topology.

A disjoint intersection can evaluate successfully to an empty compound. Query OCEx.solids/1 and OCEx.volume/1 on the result when the design requires a solid. Both source recipes remain reusable.

compound(models)

@spec compound([Smith.Model.t()]) :: Smith.Model.t()

Groups model recipes without fusing their geometry.

Members can be edges, faces, or solids. Separate boundaries are retained, including where members overlap. An empty list evaluates to an empty compound. Use fuse/2 to unite material, or Smith.Assembly to name parts and export them separately.

counterbore(model, opts)

@spec counterbore(Smith.Model.t(), keyword()) :: Smith.Model.t()

Drills a hole with a cylindrical recess for a fastener head.

Uses hole/2 options for :on, :at, :diameter, and exactly one of :depth or through: :all. Also requires :bore_diameter, larger than the hole diameter, and positive :bore_depth, both in mm.

The recess runs from the entry plane into its negative normal. Total blind depth includes the recess and must be at least bore depth. Both cutters use the original entry location even if the first cut moves the face centroid. A recess removing no additional material fails with :recess_misses_body; malformed options fail with :invalid_options. Dimensions must also satisfy native modeling tolerance.

countersink(model, opts)

@spec countersink(Smith.Model.t(), keyword()) :: Smith.Model.t()

Drills a hole with a conical recess for a countersunk fastener.

Uses hole/2 placement and extent options. Requires :sink_diameter, larger than :diameter. Optional :angle is the included cone angle in degrees, default 90, strictly between 0 and 180.

The recess narrows from sink diameter at the entry plane to hole diameter at depth (sink_diameter - diameter) / (2 * tan(angle / 2)). It runs into the negative plane normal. Total blind depth includes this recess and must reach its bottom. Uses the same entry-location and error rules as counterbore/2. The remaining blind hole has a flat floor.

cut(model, tools)

Subtracts one tool recipe or an ordered list from the current body.

Each subtraction is followed by same-domain cleanup. An empty list returns the original recipe. A missed tool can leave the geometry unchanged, and removing all material can produce an empty compound. Use hole/2 when a missed circular through-cut should fail explicitly.

Tool evaluation errors appear as a nested Smith.Error in the cut step's :reason. Neither recipe is mutated.

draft(model, opts)

@spec draft(Smith.Model.t(), keyword()) :: Smith.Model.t()

Tapers selected faces around a neutral plane.

Requires faces:, neutral:, and angle:. Faces use the usual Smith.Selector inputs. The neutral plane accepts :xy, :xz, :yz, or a positioned Smith.Plane; the surface intersections with that plane stay fixed. Angles are degrees, strictly between -90 and 90.

Optional direction: is the pull vector and defaults to the neutral plane normal. It must be nonzero and not lie in that plane. Positive angles remove material on the pull side; negative angles add material. Zero retains the geometry. Optional positive count: guards the number of explicitly selected faces, before tangent propagation.

Requires one solid, including a solid wrapped by an earlier Boolean operation. Selected faces must be planar, cylindrical, or conical. OCCT also tapers tangent-connected faces. The requested taper must not collapse edges or otherwise require a topology change. Those cases may return :draft_failed or a native geometry error. Empty selections, incorrect counts, invalid planes and options retain their usual tagged errors at this step. The source recipe remains reusable.

fillet(model, opts)

@spec fillet(Smith.Model.t(), keyword()) :: Smith.Model.t()

Appends constant-radius rounding on selected edges.

Requires edges: and radius:. Radius is in millimeters and must exceed 1.0e-7. The kernel may reject a radius that does not fit.

Selectors

  • :all selects every edge.
  • {:parallel, :x | :y | :z} selects straight edges parallel to a world axis, in either direction. Curved edges do not match.

  • A composed Smith.Selector filters by type, direction, extrema, or predicates.
  • A one-argument function receives the map from OCEx.edge_info/1 plus :bounds and :midpoint. It must return true or false.

:bounds is {minimum, maximum} in world coordinates. :midpoint is the point halfway through the edge's parameter interval; it need not be halfway along its length. Selection runs against the current body at this step, including earlier transforms and cuts.

Optional count: asserts a positive number of matches. A mismatch gives :selection_count_mismatch. An empty selection without count: fails with :invalid_argument. Invalid or duplicate options produce :invalid_options; nonboolean predicate results produce :invalid_selector_result. Exceptions in your predicate propagate.

iex> model =
...>   Smith.box(20, 10, 4)
...>   |> Smith.fillet(edges: {:parallel, :z}, radius: 1, count: 3)
iex> Smith.evaluate(model)
{:error, %Smith.Error{step: 2, operation: :fillet, reason: :selection_count_mismatch}}

fuse(model, tools)

Unites a model with one tool recipe or an ordered list of recipes.

Each tool is evaluated, united with the current body, and followed by same-domain cleanup. An empty list returns the original recipe. Disjoint inputs can leave multiple solids; this does not fail evaluation.

iex> base = Smith.box(10, 10, 2)
iex> boss = Smith.cylinder(2, 4, at: {5, 5, 2})
iex> {:ok, part} = Smith.fuse(base, boss) |> Smith.evaluate()
iex> {:ok, solids} = OCEx.solids(part.shape)
iex> length(solids)
1

hole(model, opts)

@spec hole(Smith.Model.t(), keyword()) :: Smith.Model.t()

Appends a circular through-all or flat-bottomed blind hole.

Requires on:, diameter:, and exactly one of through: :all or positive depth: in mm. Diameter is positive and in millimeters; its half-radius must also satisfy OCEx's native tolerance. Optional at: {u, v} defaults to {0, 0}.

  • on: :top selects the unique highest planar face with an outward +Z normal. :at is an XY offset from that face's area centroid. Earlier cuts can move this centroid.
  • on: plane uses plane-local :at coordinates and drills along its normal. The plane is independent of the body and may lie outside it.

Through-all extends through the body's full projected bounds, including disconnected solids. Blind depth starts at the entry plane and runs along its negative normal, leaving a flat floor when contained in the body. It is not measured from the first intersected surface; an outside plane consumes part of that distance before reaching material. A cut removing no more than 1.0e-9 mm³ fails with :hole_misses_body. Top selection can fail with :no_top_face or :ambiguous_top_face. Conflicting extent options fail with :invalid_options. Use counterbore/2 or countersink/2 for a recessed entry.

iex> model =
...>   Smith.box(20, 10, 4)
...>   |> Smith.hole(
...>     on: Smith.Plane.xy(),
...>     at: {5, 5},
...>     diameter: 2,
...>     through: :all
...>   )
iex> {:ok, part} = Smith.evaluate(model)
iex> OCEx.distance_to_point(part.shape, {5, 5, 2})
{:ok, 1.0}

mirror(model, plane)

@spec mirror(Smith.Model.t(), :xy | :xz | :yz | Smith.Plane.t()) :: Smith.Model.t()

Reflects a recipe across a world plane.

Accepts :xy, :xz, :yz through the origin, or a Smith.Plane for a positioned or oblique mirror. Returns only the reflected geometry; use compound/1 or fuse/2 to retain both copies. Supports face and edge recipes as well as solids. Invalid planes produce :invalid_plane at this recipe step. The source recipe remains reusable.

iex> {:ok, part} = Smith.box(2, 3, 4) |> Smith.mirror(Smith.Plane.yz(x: 5)) |> Smith.evaluate()
iex> {:ok, bounds} = OCEx.bounds(part.shape)
iex> bounds == {{8.0, 0.0, 0.0}, {10.0, 3.0, 4.0}}
true

offset(model, distance, opts \\ [])

@spec offset(Smith.Model.t() | Smith.Sketch.t(), number(), keyword()) ::
  Smith.Model.t()

Offsets a solid or surface by a signed normal distance in mm.

Positive distances expand oriented solids or follow surface normals; negative distances contract solids or oppose the normals. Magnitude must exceed 1.0e-7 mm. Faces, shells, solids, and their compounds are supported. Sketch inputs become face recipes before offsetting.

This is a 3D surface offset: offsetting a planar sketch moves its plane and does not grow its outline. join: :arc (default) rounds convex gaps; :intersection extends adjacent surfaces until they meet. Compound members are offset independently. Use surface/2 to sew selected connected faces before offsetting them as a shell.

Solid results must expand/contract with directional containment, checked at the volume tolerance documented in OCEx.offset/3. Complete collapse or inversion is an error. Curved surfaces require sufficiently smooth geometry and a small enough offset to avoid self-intersection. Global self-intersection repair is not provided. Native and option failures retain this recipe step's context.

rotate(model, axis, degrees, origin \\ {0, 0, 0})

@spec rotate(Smith.Model.t(), OCEx.point3(), number(), OCEx.point3()) ::
  Smith.Model.t()

Appends a right-handed rotation in degrees about a world axis.

axis is a nonzero vector. origin is a point on the axis and defaults to {0, 0, 0}; it is not automatically the body's center. Negative and zero angles are allowed. Rotation applies to the already placed geometry.

shell(model, opts)

@spec shell(Smith.Model.t(), keyword()) :: Smith.Model.t()

Hollows the current solid by removing selected faces and offsetting its walls.

Required options are :openings (a Smith.Selector or face predicate) and signed :thickness in millimeters. Negative thickness builds inward; positive builds outward. :join is :arc (default) or :intersection. Optional :count requires exactly that many opening faces.

Selectors run against the body at this recipe step. An empty selection returns :empty_selection; an unexpected count returns :selection_count_mismatch. Thickness must have magnitude greater than 1.0e-7 mm. OCCT can reject thicknesses that cannot fit the source geometry. This operation requires a single solid and at least one opening; it does not create a sealed cavity or thicken an open surface.

iex> {:ok, tray} = Smith.box(20, 16, 10)
...>   |> Smith.shell(openings: Smith.Selector.facing(:z), thickness: -2, count: 1)
...>   |> Smith.evaluate()
iex> {:ok, volume} = OCEx.volume(tray.shape)
iex> abs(volume - 1664) < 1.0e-6
true

split(model, plane, opts \\ [])

@spec split(Smith.Model.t(), :xy | :xz | :yz | Smith.Plane.t(), keyword()) ::
  Smith.Model.t()

Divides solid geometry with an infinite world plane.

Accepts :xy, :xz, :yz, or a positioned Smith.Plane. The only option is keep:: :both (default), :positive, or :negative. Positive follows the plane normal, so the positive side of an XZ plane is world -Y. Both retains separate solids at the cut.

Accepts a solid or a collection containing only solids. A plane outside the body retains the material on its side; the opposite side evaluates to an empty compound. One remaining piece is a solid, multiple pieces form a compound. Query OCEx.solids/1 on the result to inspect pieces, or use separate recipes with keep: to name and export each side.

Invalid planes fail with :invalid_plane; invalid options with :invalid_options; unsupported topology with :wrong_shape_type. Errors identify this recipe step. The source recipe remains reusable.

sweep(sketch, path, opts \\ [])

@spec sweep(Smith.Sketch.t(), Smith.Path.t(), keyword()) :: Smith.Model.t()

Sweeps a placed sketch along an open Smith.Path.

The sketch must have one closed boundary and lie in the plane through the start of the path, perpendicular to its starting tangent. Its local offset is retained. Smith does not move or rotate it onto the path. Profiles with holes return :sweep_profile_has_holes.

Options match OCEx.sweep/3: frame: :corrected (default) or :frenet, and transition: :transformed (default), :right, or :round. Tangent-continuous paths avoid sharp-corner transition ambiguity. Construction and validation are deferred until evaluate/1.

iex> path = Smith.Path.new([Smith.line({0, 0, 0}, {0, 0, 10})])
iex> {:ok, rod} = Smith.Sketch.circle(2) |> Smith.sweep(path) |> Smith.evaluate()
iex> {:ok, volume} = OCEx.volume(rod.shape)
iex> abs(volume - 40 * :math.pi()) < 1.0e-6
true

thicken(model, thickness, opts \\ [])

@spec thicken(Smith.Model.t() | Smith.Sketch.t(), number(), keyword()) ::
  Smith.Model.t()

Builds solid material between an open surface and its signed offset.

Accepts a sketch or a model containing faces/open shells. Magnitude must exceed 1.0e-7 mm. Positive thickness follows oriented normals; negative thickness goes against them. The original surface forms one boundary, free edges receive connecting walls, and holes remain open.

join: :intersection (default) extends adjacent surfaces; :arc uses rounded transitions where applicable. Use surface/2 to extract and sew faces from a solid. Disconnected surfaces produce separate solids without fusing. A solid input fails with :wrong_shape_type; a closed shell fails with :closed_shell. Use shell/2 to hollow an existing solid instead.

Results pass native shape and positive-volume checks. Smoothness, inversion, and self-intersection limits follow OCEx.thicken/3. Excessive thickness is a modeling failure, not an instruction to repair or delete intersecting features. Thickness is uniform along the normals.

iex> wall = Smith.cylinder(10, 12) |> Smith.surface(Smith.Selector.type(:cylinder))
iex> {:ok, tube} = wall |> Smith.thicken(-2) |> Smith.evaluate()
iex> {:ok, volume} = OCEx.volume(tube.shape)
iex> abs(volume - 432 * :math.pi()) < 1.0e-5
true

translate(model, vector)

@spec translate(Smith.Model.t(), OCEx.point3()) :: Smith.Model.t()

Appends a world-coordinate translation in millimeters.

The source recipe remains unchanged. Transformation order matters: translating before rotating also rotates the translated position. The vector may be zero.

Topology

edges(result, selector \\ :all)

Selects edges from an evaluated result using Smith.Selector.

Accepts :all (default), {:parallel, axis}, a metadata predicate, or a composed selector. Returns {:ok, [OCEx.Shape.t()]}, including an empty list when nothing matches. Handles belong to this result revision. Unsupported selectors return :invalid_options. Predicates must return booleans; their exceptions propagate.

faces(result, selector \\ :all)

Selects faces from an evaluated result using Smith.Selector.

Returns native handles with the same result and error rules as edges/2. Face predicates receive OCEx.face_info/1 metadata plus world :bounds. Empty and tied selections are retained; this query does not silently pick a single face. Use a feature's :count option to enforce its expectation.

inspect_edges(result, selector \\ :all)

@spec inspect_edges(Smith.Result.t(), Smith.Selector.input()) ::
  {:ok, [map()]} | {:error, atom()}

Returns selected edges with their geometry metadata and native :shape handles.

Each map contains OCEx.edge_info/1 fields plus world :bounds and :midpoint, using Smith.Selector.where/2 conventions. Query order is preserved. Handles belong to this result's revision. Returns a tagged list; selection and native query failures propagate as tagged errors.

inspect_faces(result, selector \\ :all)

@spec inspect_faces(Smith.Result.t(), Smith.Selector.input()) ::
  {:ok, [map()]} | {:error, atom()}

Returns selected faces with their geometry metadata and native :shape handles.

Each map contains OCEx.face_info/1 fields plus world :bounds. Uses the ordering, revision, and tagged-result rules of inspect_edges/2.

Evaluation and export

evaluate(assembly)

@spec evaluate(
  Smith.Model.t()
  | Smith.Assembly.t()
  | Smith.Sketch.t()
  | Smith.Path.t()
) ::
  {:ok, Smith.Result.t() | Smith.Assembly.Result.t()}
  | {:error, Smith.Error.t() | atom()}

Builds native geometry from a model, sketch, or assembly recipe.

Returns {:ok, %Smith.Result{}} for models and sketches, or {:ok, %Smith.Assembly.Result{}} for assemblies. A bare sketch becomes a face; paths evaluate to wires. Edge recipes and empty compounds can also evaluate successfully. Successful evaluation alone does not establish printability.

Model operations execute in construction order. Failures return {:error, %Smith.Error{step: index, operation: name, reason: reason}}; the index starts at 1. Assemblies also identify the failed part where available. Final shape checks and top-level validation can return a bare error atom. Empty models return :empty_model; unsupported top-level terms return :invalid_recipe.

Evaluation is synchronous. It rebuilds a recipe on each call, except that equal member recipes within one assembly evaluation share their base evaluation. User callback exceptions are not caught. See the error guide for details.

iex> Smith.box(0, 10, 4) |> Smith.evaluate()
{:error, %Smith.Error{step: 1, operation: :box, reason: :invalid_argument}}
iex> Smith.evaluate(nil)
{:error, :invalid_recipe}

export(result, path)

@spec export(Smith.Result.t(), String.t()) :: {:ok, :ok} | {:error, atom()}

Writes one geometry file, choosing its format from the path extension.

Accepts an evaluated Smith.Result. Extensions are case-insensitive: .step and .stp write STEP; .stl writes binary STL. Other extensions, including .3mf, return {:error, :unsupported_format}.

Returns {:ok, :ok} on success. Existing files are overwritten and the parent directory must already exist. STL uses OCEx defaults of 0.1 mm linear and 0.5 rad angular deflection. This function does not perform the bundle's mesh/STEP checks or update a manifest.

For 3MF, configurable mesh settings, and print placement, use export/3.

export(result, root, opts)

@spec export(Smith.Result.t() | Smith.Assembly.Result.t(), String.t(), keyword()) ::
  {:ok, map()} | {:error, term()}

Writes a part or assembly bundle and updates the output manifest after checks pass.

The second argument is an output directory; it is created as needed. Requires a string name:. Default formats are [:step, :stl, :three_mf]; BREP snapshots and reports are always included. All bundles require printable solids and run mesh checks, even when only STEP is requested.

For an individual result, options and returned record fields are documented in Smith.Export.write/3. For an assembly, see the assembly export options. Assembly print placement belongs on its members, not in this option list.

Returns {:ok, record} for a part or {:ok, report} for an assembly. A failure leaves the previous manifest in place but can leave unpublished files in the new export directory. Run writers to one root sequentially.

{:ok, part} = Smith.box(20, 10, 4) |> Smith.evaluate()
{:ok, files} = Smith.export(part, "output", name: "block", on_bed: true)
IO.puts(files.three_mf)

Types

alignment()

@type alignment() ::
  {:min | :center | :max, :min | :center | :max, :min | :center | :max}

primitive_option()

@type primitive_option() :: {:at, OCEx.point3()} | {:align, alignment()}