LineDrawing
a 3D scene as line work a machine can follow: lineDrawing(of:) takes the meshes and the camera and gives back paths with what the surfaces hide taken out, with the crease angle, the test spacing, and the hidden stretches on parameters (--export-svg, --export-gcode)
- Run
swift run --package-path Examples Example-Export-LineDrawing- Source
- Examples/Export/LineDrawing/Sketch.swift
import Ollin
/// **A 3D scene as line work a machine can follow.**
///
/// `lineDrawing(of:)` takes meshes and the camera looking at them and gives
/// back 2D paths with everything the surfaces hide taken out: the silhouette
/// where a form turns away, the creases where faces meet at an angle, and the
/// boundary where a surface ends. Those paths are ordinary line work, so this
/// sketch draws them with `drawPolyline` and a pen plotter or a laser can have
/// them exactly as they are:
///
/// ```sh
/// ollin Sketch.swift --export-svg scene.svg
/// ollin Sketch.swift --export-gcode scene.gcode --gcode-width 180
/// ```
///
/// `Crease` is the angle two faces must meet at before the edge between them is
/// drawn, so a low one finds the ridges of the ball and a high one leaves it as
/// its outline alone; at 0 every edge is drawn, which is a wireframe rather than
/// a drawing. `Spacing` is how finely each edge is tested for whether it is
/// hidden, and `Shaded` shows the same scene lit, for the comparison.
@main
final class LineDrawing: Sketch {
@Param("Turn", 0 ... 1, icon: "rotate.3d", group: "Camera") var turn = 0.12
@Param("Height", 0.4 ... 4, icon: "arrow.up.and.down", group: "Camera") var eyeHeight = 1.9
@Param("Crease", 0 ... 1.2, icon: "angle", group: "Drawing") var crease = 0.5
@Param("Spacing", 0.5 ... 8, icon: "ruler", group: "Drawing") var spacing = 2.0
@Param("Weight", 0.5 ... 6, icon: "pencil.tip", group: "Drawing") var weight = 2.0
@Param(icon: "cube.transparent", group: "View") var shaded = false
@Param("Show hidden", icon: "eye.slash", group: "View") var dashed = false
override var canvasSize: CanvasSize { .square(900) }
/// A line of text along the bottom, in the ink the drawing is in.
private func note(_ text: String) {
noStroke()
fill(Color(hex: 0x1A1A1A))
textSize(20)
textAlign(.center, .bottom)
drawText(text, width / 2, height - 28)
}
/// The scene: a slab with three solids standing on it, each placed where it
/// belongs so they can all be handed over together and occlude each other.
private var scene: [Mesh] {
[
Mesh.box(width: 6, height: 0.5, depth: 6)
.transformed(by: MeshInstance(position: Vector3(0, -0.25, 0))),
Mesh.cylinder(radius: 0.7, height: 2.2, segments: 24)
.transformed(by: MeshInstance(position: Vector3(-1.5, 1.1, 0.6))),
Mesh.sphere(radius: 0.95, segments: 32, rings: 16)
.transformed(by: MeshInstance(position: Vector3(1.2, 0.95, -0.4))),
Mesh.box(size: 1.1)
.transformed(by: MeshInstance(position: Vector3(0.4, 0.55, 1.8),
rotation: Vector3(0, 0.6, 0))),
]
}
override func draw() {
background(Color(hex: 0xF4F1E8))
let angle = (turn + time * 0.03) * .pi * 2
camera(Camera3D(eye: Vector3(cos(angle) * 7.5, eyeHeight * 2.2, sin(angle) * 7.5),
target: Vector3(0, 0.9, 0)))
if shaded {
light(.directional(.white, direction: Vector3(0.5, -1, -0.4)))
fill(Color(hex: 0xBFB6A4))
for mesh in scene { drawMesh(mesh) }
note("The same scene lit, for the comparison")
return
}
stroke(Color(hex: 0x1A1A1A))
strokeWeight(weight)
strokeJoin(.round)
strokeCap(.round)
noFill()
let drawing = lineDrawing(of: scene, creaseAngle: crease, spacing: spacing)
if dashed {
stroke(Color(hex: 0x1A1A1A, alpha: 0.28))
strokeWeight(weight * 0.6)
for line in drawing.hidden { drawPolyline(line.points, closed: line.isClosed) }
stroke(Color(hex: 0x1A1A1A))
strokeWeight(weight)
}
for line in drawing.paths {
drawPolyline(line.points, closed: line.isClosed)
}
note("\(drawing.paths.count) paths, ready for a pen")
}
}