Import SVG line art or filled outlines as measurable planar CAD geometry. Fit it in millimeters, place it on any plane, then extrude and fuse or cut it like text. The interactive notebook includes a local file upload, selection, sizing, stroke width, raised/engraved controls, and printable exports.

Snapshot and inspect

alias Smith.{SVG, Plane, Sketch, Font, Text}
{:ok, artwork} = SVG.load(Path.join(:code.priv_dir(:smith), "svg/volleyball.svg"))
IO.inspect(SVG.elements(artwork), label: "Selectable SVG elements")
motif = SVG.new(artwork, width: 18, align: {:center, :center})
{:ok, layout} = SVG.layout(motif)
IO.inspect(Map.take(layout.report, [:width, :height, :area, :regions, :source_sha256]))
{:ok, previews} = SVG.write(layout, "output/svg-inspection")

Interactive preview of the converted SVG regions.

SVG.from_binary/1 accepts uploaded bytes. Loading parses the document and saves its SHA-256; it does not build native geometry. Editing the source file cannot change an existing asset. SVG.new/2 records a deferred recipe; Smith.svg/2 is a shorthand. Evaluation and reports use the same geometry snapshot.

Fills, strokes and selection

ModeResult
:painted (default)Union of selected filled areas and expanded strokes
:fillFilled contours, with holes and disconnected islands
:strokesVisible stroke areas, including open centerlines, caps and joins

Source colors are metadata. A white background is material in painted mode; it is not automatically subtracted. Select the intended group or use stroke mode for line art. Overlapping paint colors are not browser-style compositing.

seams = SVG.new(artwork, select: "seams", mode: :strokes,
  width: 18, stroke_width: 0.6, align: {:center, :center})
{:ok, seam_layout} = SVG.layout(seams)

Selection accepts IDs, group IDs/labels, lists, stable element indices, and {:fill, "black"} or {:stroke, "black"}. Paint selection uses the resolved source string, not a color-distance calculation. An unmatched selection fails. reference: :document keeps selected pieces registered to the same document bounds and size. Use reference: :selection to size/align only selected artwork.

Source stroke widths follow SVG affine transforms and uniform CAD scaling. stroke_width: overrides them in final millimeters, after all transforms. This is useful for adjusting groove width independently of motif size. An unreachable size, such as fitting a fixed 2 mm round stroke into 1 mm, fails with :size_unreachable rather than distorting the geometry.

Size and placement

width: sets artwork width; height: sets height. Specifying both contains the artwork within the box without stretching. SVG.fit/3 also accepts a margin. Artwork bounds exclude blank document margins; bounds: :viewport explicitly uses the document viewport instead. Measurements use native kernel tolerances.

Without an explicit size, physical units follow the document (96 px per inch). A viewBox-only SVG uses its viewBox dimensions as the viewport in CSS pixels. The importer resolves viewBox and transforms, then flips SVG Y into CAD Y. Default alignment preserves the document origin. Use :min, :center, :max, or :origin for each axis, at: {x,y} for the anchor and on: for the plane. Scalar extrusion follows the plane's normal.

Engraved and raised volleyball tags

base = Sketch.slot(80, 24)
  |> Sketch.cut(Sketch.circle(2, at: {-34, 0}))
  |> Smith.extrude(3)
{:ok, font} = Font.load(Path.join(:code.priv_dir(:smith), "fonts/Graduate-Regular.ttf"))
text = Text.new("ALEX", font: font, size: 10, align: {:center, :center},
  at: {10, 0}, on: Plane.xy(z: 2.8))
{:ok, text} = Text.fit(text, {38, 15}, margin: 1, min_size: 5)
base = Smith.fuse(base, Smith.extrude(text, 1))
engraving = SVG.new(artwork, width: 18, align: {:center, :center},
  at: {-20, 0}, on: Plane.xy(z: 3))
raised = SVG.new(artwork, width: 18, align: {:center, :center},
  at: {-20, 0}, on: Plane.xy(z: 2.8))
engraved_tag = Smith.cut(base, Smith.extrude(engraving, -0.6))
raised_tag = Smith.fuse(base, Smith.extrude(raised, 0.8))
{:ok, engraved_result} = Smith.evaluate(engraved_tag)
{:ok, raised_result} = Smith.evaluate(raised_tag)
{:ok, files} = Smith.export(engraved_result, "output/svg-keychains", name: "engraved", on_bed: true)

Interactive preview of the engraved volleyball keychain.

Both versions should remain one printable solid. Raised artwork overlaps the base by 0.2 mm. The engraving leaves 2.4 mm of base beneath it. Region count, width and height are not printer-resolution or minimum-wall guarantees.

The bundled MIT-licensed original is stroked. volleyball-outlined.svg contains filled outlines of the same motif, generated with 0.001 mm sampling. Tests compare their symmetric difference against boundary length times that tolerance. User artwork and team assets belong in private consumer projects.

Reuse centerlines

SVG.paths/1 returns positioned wire models with element IDs and closed/open metadata. Open paths also expose a Smith.Path for the existing sweep API; closed loops expose path: nil because Smith sweeps require open paths.

{:ok, line_asset} = SVG.from_binary("<svg viewBox='0 0 20 20'><path fill='none' stroke='black' d='M0 0H20'/></svg>")
{:ok, [%{path: spine}]} = SVG.paths(SVG.new(line_asset, width: 20))
swept = Smith.Sketch.circle(0.3, on: Plane.yz()) |> Smith.sweep(spine)
{:ok, swept_result} = Smith.evaluate(swept)

Supported content and failures

Paths support absolute/relative M/L/H/V/C/S/Q/T/A/Z, implicit repetition, compact arc flags, and SVG's implicit closure for fills. Quadratic/cubic curves and elliptical arcs remain native curves. Native stroke expansion samples curved centerlines using tolerance: 0.01 in final mm; straight edges and round caps are analytic. SVG.outline_svg/2 exports sampled filled boundaries for reuse.

Basic shapes, rounded rectangles, nested group transforms, local <use> references, inline styles and inherited presentation attributes are supported. The nonzero and even-odd fill rules preserve nested holes, intersections, and separate regions. Hidden content and editor metadata are excluded.

Stylesheets, text, raster images, external references, gradients/patterns, clipping/masks, filters, dashes, markers, alpha color syntax, non-scaling strokes and partial opacity return errors identifying the affected element. Convert text to paths or use Smith.Text; use a vector editor to expand unsupported effects. Nested SVG viewports and preserveAspectRatio slice/clipping are currently unsupported. No unsupported visible geometry is silently omitted.

XML has a 4 MiB limit, bounded nesting, node and expansion counts. DTDs/entities and processing instructions (except the initial XML declaration) are rejected. No network or script execution occurs. Native complexity limits return tagged errors. Like other OCEx operations, geometry runs synchronously on serialized dirty schedulers; it cannot be forcibly cancelled inside the VM.