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
- Getting started — a complete script and first export.
Smith.Sketch— 2D outlines, cutouts, and planes.Smith.Path— open paths for placed sweep profiles.Smith.Selector— composable edge and face queries.Smith.Assembly— named parts, references, and print placement.Smith.Export— files, mesh checks, and export records.Smith.Kino— interactive Livebook previews.
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
@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:maxfor 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}}}
@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.
@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}}}
@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}.
@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
@spec arc(OCEx.point3(), OCEx.point3(), OCEx.point3(), number(), number(), number()) :: Smith.Model.t()
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.
@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}}}
@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.
@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.
@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.
@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.
@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
@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.
@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}
@spec project( Smith.Model.t() | Smith.Sketch.t() | Smith.Path.t(), Smith.Model.t() | Smith.Sketch.t(), keyword() ) :: Smith.Model.t()
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.
@spec revolve( Smith.Sketch.t() | Smith.Model.t(), OCEx.point3(), number(), OCEx.point3() ) :: Smith.Model.t()
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
@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
@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.
@spec surface(Smith.Model.t(), Smith.Selector.input()) :: Smith.Model.t()
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
@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
@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.
@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.
@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.
@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.
@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.
@spec cut(Smith.Model.t(), Smith.Model.t() | [Smith.Model.t()]) :: Smith.Model.t()
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.
@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.
@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
:allselects 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.Selectorfilters by type, direction, extrema, or predicates. - A one-argument function receives the map from
OCEx.edge_info/1plus:boundsand:midpoint. It must returntrueorfalse.
: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}}
@spec fuse(Smith.Model.t(), Smith.Model.t() | [Smith.Model.t()]) :: Smith.Model.t()
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
@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: :topselects the unique highest planar face with an outward +Z normal.:atis an XY offset from that face's area centroid. Earlier cuts can move this centroid.on: planeuses plane-local:atcoordinates 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}
@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
@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.
@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.
@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
@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.
@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
@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
@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
@spec edges(Smith.Result.t(), Smith.Selector.input()) :: OCEx.result([OCEx.Shape.t()])
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.
@spec faces(Smith.Result.t(), Smith.Selector.input()) :: OCEx.result([OCEx.Shape.t()])
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.
@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.
@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
@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}
@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.
@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
@type alignment() ::
{:min | :center | :max, :min | :center | :max, :min | :center | :max}
@type primitive_option() :: {:at, OCEx.point3()} | {:align, alignment()}