SVG import
Read vector artwork into the same Shapes and Contours the rest of the framework uses. SVG is the W3C's plain-text vector format, published in 2001, and every design tool exports it. A logo traced in a design tool arrives as geometry. So does a scanned drawing auto-traced to paths, or a file exported from another sketch. Once it is loaded you can draw it as authored, respace it into dots, offset it, or hatch it. You can also run it through the shape booleans, or send it back out through the SVG export.
@main
final class Badge: Sketch {
var art: SVG?
override func setup() {
art = try? loadSVG("rocket.svg")
}
override func draw() {
background(.white)
guard let art else { return }
drawSVG(art, in: Rectangle(x: 100, y: 100, width: 880, height: 880))
}
}
The importer covers the subset of SVG that generative work meets in practice:
<path>, with the full path grammar of lines, cubic and quadratic Béziers, smooth shorthands, and elliptical arcs.- The basic shapes
rectincluding rounded corners,circle,ellipse,line,polyline, andpolygon. <g>groups,transformlists, presentation attributes, and inlinestyle.- Both fill rules, per-element opacity, and stroke width, join, and cap.
Curves are flattened to points at import, after the transforms are applied, so the sampling density matches the final size. The Path builder works the same way.
The importer deliberately skips gradients and patterns, CSS <style> blocks, <use>/<symbol> references, clipping, masks, filters, and text. A url(#…) paint falls back to mid-gray, so the form stays visible. Convert text to outlines when you export from a design tool. Content inside <defs> and similar elements is ignored as a whole, and the plain geometry in the file still imports.
Contents
Loading
try loadSVG(_ path: String) -> SVG // Sketch sugar, by file path
try loadSVG(_ url: URL) -> SVG
try SVG(contentsOf: "art/crest.svg") // the same, as initializers
try SVG(url: fileURL)
try SVG(data: data) // raw bytes (an inline string, a download)
try SVG(resource: "crest", in: .module) // bundled beside the sketch
All of these throw a FileError when the file cannot be read (missing) or holds no importable geometry (unreadable). The resource:in: form follows the font and image loaders, so you pass the caller's bundle explicitly, such as .module from inside a package target. Pass it every time, because a default would resolve to Ollin's own bundle rather than yours.
Drawing: drawSVG
drawSVG(_ svg: SVG) // in the document's own coordinates
drawSVG(_ svg: SVG, in rect: Rectangle) // scaled uniformly to fit rect
drawSVG draws every element in document order, with the fill, stroke, stroke width, join, and cap it was authored with. The current transform applies, so translate, rotate, and scale move the whole artwork. Your sketch's own fill and stroke state is untouched afterward, and elements with neither a fill nor a stroke are skipped.
drawSVG(_:in:) letterboxes the artwork. It keeps the aspect ratio, scales the artwork to fit, and centers it in rect. That is the same rule as Rectangle(fitting:in:).
The SVG type
struct SVG {
var elements: [Element] // document order, back to front
var bounds: Rectangle // the viewBox (or the geometry's bounds)
var shapes: [Shape] // just the geometry
var contours: [Contour] // every contour, as plain line-work
func element(named: String) -> Element? // find by id="…"
func fitted(in: Rectangle) -> SVG // a scaled copy (strokes scale too)
}
struct SVG.Element {
var shape: Shape // closed contours fill; open ones stroke
var fill: Color? // nil = fill="none"
var stroke: Color? // nil = unstroked (the SVG default)
var strokeWidth: Double // in document units, transform-scaled
var join: StrokeJoin // stroke-linejoin (miter default)
var cap: StrokeCap // stroke-linecap (butt default)
var name: String? // the id attribute
}
Coordinates come back in the document's user space, which is the viewBox. They run y-down from the top-left, the same as the canvas, so nothing needs flipping. fitted(in:) returns a copy remapped into a canvas rectangle. Use it when you would rather hold the scaled geometry than wrap the drawing in a transform.
An element's id survives as name, so you can pull one part of the artwork out and treat it on its own:
if let window = art.element(named: "window") {
drawShape(window.shape.resampled(spacing: 4))
}
Mining the geometry
Once a file is imported it is data rather than a picture, so the rest of the framework can work on it directly. Here are some of the things a loaded file feeds:
// Even dots along every outline (see Contour.resampled).
for contour in art.fitted(in: frame).contours {
for p in contour.resampled(spacing: 6).points {
drawCircle(center: p, radius: 2)
}
}
// Booleans against drawn geometry (see Geometry.md).
let cut = art.shapes[0].subtracting(stamp)
// Nested outlines, each a step inside the last (see Shape.offset).
let inner = art.shapes[0].offset(by: -6)
// Pen hatching, at a different angle per part (see Hatching).
for (i, shape) in art.fitted(in: frame).shapes.enumerated() {
let hatch = Hatching(spacing: 4.5, angle: 0.5 + Double(i) * 0.7)
for line in hatch.lines(filling: shape) {
drawPolyline(line)
}
}
// Plotter output: import, transform, re-export as vectors.
// swift run MySketch --export-svg out.svg
Fit the artwork before you resample or hatch it, so a spacing is measured in canvas pixels rather than document units. The offset is on the geometry page, and Hatching is with the plotter export. A reworked file leaves again through --export-svg, so a sketch can import a drawing, rework it, and hand the result to a plotter.
A <path> with several contours stays one Shape, so holes keep cutting. Its fill-rule maps onto the shape's winding, which is nonzero by default and evenodd when the file was authored that way.
Gotchas
- An element with no fill and no stroke is invisible in SVG terms, but still parsed. The default fill is black. That is the SVG initial value, so most artwork shows up as it is.
drawSVGskips only the elements that setfill="none"and have no stroke, and their geometry is still there inshapesandcontours. - Gradient fills fall back to mid-gray. The importer resolves flat colors, which covers hex,
rgb(), the CSS named set, and opacity folded into alpha. A gradient or pattern reference keeps the element visible in gray instead of dropping it. Repaint that element yourself when the color matters. - Sizes are document units. Nothing is rescaled at load. A 100-unit viewBox draws 100 pixels wide until you use
drawSVG(_:in:)orfitted(in:). - Text does not import. Convert text to outlines when you export from the design tool. It then arrives as paths, like everything else.
Example: Examples/Shapes/SVGImport. The vector half of export is in Output/Export.md.