Project an outline onto a surface

Copy Markdown View Source
Mix.install([{:smith, "~> 0.2.0"}, {:kino, "~> 0.19.0"}])

Before you start

After workplanes and sketches, transfer outlines onto surfaces. This focused lesson compares a flat target with a cylinder and turns one projected outline into a printable part.

Run cells in order. Lengths are millimeters and modeling angles are degrees. Each 3D preview supports orbit, zoom, and fullscreen.

Define the source and target

The source is a circle above the target plane z = -x. Projecting along world Z produces an ellipse on that inclined plane. Preview each surface separately.

alias Smith.{Assembly, Plane, Selector, Sketch}
source = Sketch.circle(4, on: Plane.xy(z: 12))
inclined = Plane.new(normal: {1, 0, 1}, x_direction: {1, 0, -1})
target = Sketch.rectangle(30, 30, on: inclined)

for {label, recipe} <- [{"1 · Source circle", source}, {"2 · Inclined target", target}] do
  Smith.Kino.render(recipe, label: label)
end |> Kino.Layout.grid(columns: 2)

Project and fill the outline

Parallel projection is bidirectional and keeps all surface hits. Target a single face when you want one outline. face/1 fills one closed planar wire; it does not infer holes or choose between multiple hits. The ellipse area is π * 4² * √2.

outline = Smith.project(source, target, direction: {0, 0, -1})
profile = Smith.face(outline)
{:ok, face_result} = Smith.evaluate(profile)
{:ok, area} = OCEx.area(face_result.shape)
true = abs(area - 16 * :math.pi() * :math.sqrt(2)) < 1.0e-6
Smith.Kino.render(face_result, label: "3 · Projected ellipse")

Extrude the projected face

Face recipes use a world displacement vector. Moving the ellipse 3 mm along Z makes this oblique solid. Its volume is the XY footprint area times 3 mm.

cap = face_result |> Smith.from_result() |> Smith.extrude({0, 0, 3})
{:ok, cap_result} = Smith.evaluate(cap)
{:ok, volume} = OCEx.volume(cap_result.shape)
true = abs(volume - 48 * :math.pi()) < 1.0e-6
Smith.Kino.render(cap_result, label: "4 · Extruded projected face")

Project through a point

Conical projection follows half-rays from the chosen point through the source. The target here is twice as far from that point as the source plane, so the circle radius doubles from 2 to 4 mm. Surfaces behind the point are excluded.

enlarged = Sketch.circle(2, on: Plane.xy(z: 5))
  |> Smith.project(Sketch.rectangle(20, 20), from: {0, 0, 10})
  |> Smith.face()
  |> Smith.extrude({0, 0, 3})
{:ok, enlarged_result} = Smith.evaluate(enlarged)
{:ok, volume} = OCEx.volume(enlarged_result.shape)
true = abs(volume - 48 * :math.pi()) < 1.0e-6
Smith.Kino.render(enlarged_result, label: "5 · Conical projection")

Measure a projection on a curved surface

This line projects onto both sides of a cylinder. The two resulting arcs remain curves; Kino can display these directly. Inspect sample coordinates as well. Every sample must lie on x² + y² = 25 and z = 5. Projection uses exact native curves.

wall = Smith.cylinder(5, 10) |> Smith.surface(Selector.type(:cylinder))
curves = Smith.line({-3, -10, 5}, {3, -10, 5})
  |> Smith.project(wall, direction: {0, 1, 0})
{:ok, curves_result} = Smith.evaluate(curves)
{:ok, edges} = Smith.edges(curves_result)
{:ok, length} = OCEx.length(curves_result.shape)
true = abs(length - 20 * :math.asin(3 / 5)) < 1.0e-5

for {edge, index} <- Enum.with_index(edges, 1), fraction <- [0, 0.25, 0.5, 0.75, 1] do
  {:ok, %{point: {x, y, z}}} = OCEx.edge_sample(edge, fraction)
  true = abs(x*x + y*y - 25) < 1.0e-6 and abs(z - 5) < 1.0e-6
  %{edge: index, fraction: fraction, x_mm: x, y_mm: y, z_mm: z}
end |> Kino.DataTable.new()

Target face boundaries and holes clip the curves. Successful partial hits can produce open wires. A missed or failed source boundary returns :projection_failed. To project onto one side of a solid, select the target with Smith.surface/2 first.

Smith.Kino.render(curves_result, label: "Projected arcs", view: :top)

Export two printable parts

The inclined cap is rotated −45° about Y for printing so its lower face lies flat. Its print footprint is elliptical; the enlarged disk's footprint is circular. The print pack contains two STL files and two 3MF files; use one format per part.

sample = Assembly.new(:projection_samples)
  |> Assembly.part(:inclined_cap, cap, position: {-12, 0, 4},
    print: [rotation: {{0, 1, 0}, -45}, on_bed: true])
  |> Assembly.part(:enlarged_disk, enlarged, position: {12, 0, 0}, print: [on_bed: true])
{:ok, assembly} = Smith.evaluate(sample)
Smith.Kino.render(assembly, label: "6 · Projection samples")
{:ok, files} = Smith.export(assembly, "output/models", name: "projection-samples",
  angular_tolerance: 0.1)
2 = length(files.parts)
true = Enum.all?(files.parts, & &1.verification.mesh.watertight)
{:ok, archive} = :zip.extract(File.read!(files.print_pack), [:memory])
4 = length(archive)
Kino.Text.new(files.print_pack)