# Tenon reference card (language 0.3) Tenon is a small, declarative, text-based language for parametric physical designs: 3D-printed parts, furniture from dimensional lumber and sheet goods, welded steel frames. A `.tenon` file declares parts (one manufactured thing each) and assemblies (placed parts), with units on every quantity, relational placement ("this bottom sits on that top"), `check` rules that state design intent, and `output`s (STEP/STL/3MF exports, cut lists, BOMs). Each definition has a constructor `fn new()` that returns its geometry, and is built with `Name::new(...)`. There is no I/O, mutation, recursion or lambdas: a file is a pure description that always evaluates the same way. This card is the whole core language. The packages card adds `print3d` and `stock` (lumber, sheet, steel). ## Conventions (stated once, never vary) - **Z is up. Front is −Y.** +X is right, +Y is back (away from the viewer). Built-in face anchors: `right` (+X), `left` (−X), `back` (+Y), `front` (−Y), `top` (+Z), `bottom` (−Z), plus point `center`. - **Primitives are centred on the origin**: `box`, `cylinder`, `sphere`, `cone`, `torus`, `wedge`, `rect`, `circle`, `slot`. Cylinders and cones have their axis along Z. Stock members are centred, lying flat: length along X, width along Y, thickness along Z. - **Every quantity needs a unit**: `40mm`, `1.5in`, `8ft`, `45deg`. No space between number and unit. Fractions are one literal: `3/4in`, `1/8in` (no spaces). Mixed numbers are sums: `1in + 1/2in`. A bare number is dimensionless. - **Only the first argument may be positional; every other argument is named**: `cylinder(d: 10mm, height: 20mm)`, `subtract(body, remove: hole)`. Constructors name every argument: `Leg::new(height: 30in)`. - **Import every package you use, explicitly.** `use stock::lumber` binds only `lumber` (for `lumber::board(...)`); `stock::cutlist(...)` and stock board methods like `.miter(...)` need their own `use stock`. Missing imports are `E0151` (the fix adds the `use` line). - **`::` for packages, `.` for values.** `use stock::lumber` then `lumber::board(...)`, `print3d::PETG`, `Leg::new(...)`. Fields, anchors, members and methods use `.`: `s.width`, `base.top`, `main.legs[0]`, `shape.fillet(...)`. - **Chain methods; never `|>`.** `a.f(k: v)` is exactly `f(a, k: v)`. Every method returns a new value; nothing is modified in place. - **Every part and assembly has `fn new()` that returns its result**: `fn new() -> Shape { … return shape }` for a part, `fn new() -> Assembly { … return Assembly { legs, top } }` for an assembly. `new` takes no parameters: declare inputs with `param` in the body; inside `new` use params, lets and components by bare name. - **Build a definition with `Name::new(arg: value)`**; never `Name(...)`, `Name::create(...)`, `.new()` on a value, or `self`. The file builds `main Name::new(...)`. - **Every statement ends with `;`** (`use`, `let`, `param`, `check`, `return`, `main`, `output`, `part x = …`, properties, and the header `tenon 0.3;`). Definitions that end with `}` — `fn`, `part Name { }`, `assembly Name { }` — don't. Newlines are just whitespace, so long chains and argument lists can wrap anywhere. - **Tenon is not Rust**: no `mut`, no `&`, no macros (`vec![]`), no `Some(…)`/`.unwrap()`, and functions always end with `return expr;`. The no-value literal is `None` (capitalised); `true`/`false` are lowercase. - **Position things with `.place()` and anchors, not `.move()`.** `place` changes translation only; `.orient()`/`.rotate()` first to turn something. - **Built-in anchors (`top bottom left right front back center`) are world-aligned**: computed from the axis-aligned bounding box in the frame where they are read, so `top` always faces +Z even after rotating. **Custom anchors and package anchors** (a stock member's `start`, `end`, `face`, `edge`, a cylinder's `axis`) **rotate with the part.** - **Rotation is right-handed** (thumb along the axis, positive angle turns the fingers' way): `rotate(about: X, angle: 15deg)` tips the top toward the front (−Y); `about: Y` tips the top toward +X (right); `about: Z` turns counter-clockwise seen from above (+X toward +Y). - **Stock dimensions are actual, not nominal**: a 2x4 is 1.5in × 3.5in; "3/4" plywood is 23/32in. Get sizes from the catalog (`lumber::section("2x4")`), never hard-code them. - **No recursion, no lambdas, no mutation.** Build lists in one of two ways — **brackets take one expression, braces take statements**: - `[for x in xs => expr]` — one expression per item; - `for x in xs { let a = …; let b = …; value }` — statements per item; the last line (no `;`) is the item. `if cond { continue; }` skips an item. Both give a `List`; an item that is itself a list gives a list of lists, so use `flatten(…)` when you want one list. ## File structure ```tenon tenon 0.3; // required header, first statement use print3d; // package: print3d::PETG use stock; // package: stock::cutlist(...), board.miter(...) use stock::lumber; // sub-module: lumber::board(...) — binds only `lumber` use "./hinge.tenon" as hinge; // another file: hinge::Hinge::new() param …; let …; fn … { } part … { } assembly … { } check …; // any order; order-independent main Name::new(arg: value); // the design to build (at most one) output name: export(main, format: "stl"); ``` Comments: `// comment`, `/// doc comment` (attaches to the next declaration, param, component or property; shown in UI and BOM). No block comments. Names: `snake_case` for values/params/functions/components/anchors, `PascalCase` for parts and assemblies. `;` ends a statement; newlines carry no meaning (write one call per line in long chains). ## Syntax cheat-sheet | Construct | Example | |---|---| | Quantity | `40mm` `1.5in` `3/4in` `45deg` `2kg` `50N` | | Arithmetic | `width + 2 * wall` `length / 2` `x % 2` `side ^ 2` (Length*Length → Area) | | Compare / logic | `== != < <= > >=` (Lengths compare with 1e-6 m tolerance), `and or not` | | Conditional | `if usb then [usb_cut] else []` | | List / index | `[1mm, 2mm]` `xs[0]` | | Record / field | `{ width: 3.5in, thickness: 1.5in }` `s.width` `p.x` | | Range | `0..4` (end excluded) `0..=4` (included) `20mm..80mm` | | Comprehension | `[for i in 0..4 => i * 25mm]` `[for p in pts if p.z > 0mm => f(p)]` (one expression) | | `for` block | `for z in levels { let r = rail.place(z: at(z)); if z > 2m { continue; } [r, r.mirror(across: YZ)] }` (statements; last line is the item) | | Call | `box(x: 10mm, y: 20mm, z: 5mm)` `union(a, b, c)` | | Method chain | `box(…).fillet(edges: edges(parallel: Z), r: 1mm).place(bottom: on(ground))` | | Path | `lumber::board(…)` `print3d::PETG` `stock::lumber::section("2x4")` `Leg::new(height: 1m)` | | Build | `Leg::new(height: 30in)` → a `Part`; `Table::new()` → an `Assembly` | | Members | `Assembly { legs, top: panel }` (only in `return` of an assembly's `new`) | | String | `"text"`, escapes `\" \\ \n \t`; no interpolation: `"w=" + str(width)` | | Optional | `None`, type `Length?` (`param fillet: Length? = None`) | Variadic first parameters (`union(...shapes)`, `min(...xs)`) take all positional arguments; a list argument is flattened in, so `union(body, bosses)` works when `bosses` is a list. ## Types and units | Type | Units / values | |---|---| | `Length` | `mm` `cm` `m` `in` `ft` `yd` | | `Angle` | `deg` `rad` `turn` | | `Mass` | `g` `kg` `lb` `oz` | | `Force` | `N` `kN` `lbf` | | `Pressure` | `Pa` `kPa` `MPa` `GPa` `psi` `ksi` | | `Time` | `s` `min` `h` | | `Money` | `USD` `EUR` `GBP` `CAD` (no conversion between currencies) | | Derived | `Area` `Volume` `Density` `Moment` `Stiffness` `LinearDensity` come from arithmetic | | Other | `Bool` `Number` `Int` `String` `List[T]` `Range[T]` records `{a: T}` enums `"a" \| "b"` optional `T?` | | Geometry | `Vec3` `Point` `Direction` `Frame` `Plane` `Profile` `Shape` `Anchor` `Part` `Assembly` `Selection[Face]` `Selection[Edge]` | | Domain | `Material` `Process` `Stock` `Export` `Report` `Color` | A list of three Lengths converts to `Point`/`Vec3`: `[0mm, 5mm, 2mm]`. Adding or comparing different dimensions is error `E0301`. ## Declarations ```tenon param width: Length = 40mm { range: 20mm..80mm, step: 1mm, doc: "Outside width", group: "Size" }; param finish: "raw" | "painted" = "raw"; param screw_d: Length = 4mm { options: [3mm, 4mm, 5mm] }; param usb: Bool = true { doc: "Cut a USB opening" }; ``` `param` needs a type and a default. Metadata keys: `range`, `step`, `options`, `doc`, `group`, `unit`. Values outside `range`/`options` are `E0310`. ```tenon let clearance = 0.2mm; let hole_d: Length = screw_d + 2 * clearance; // single assignment; annotation optional fn rounded_plate(size: List[Length], r: Length, t: Length) -> Shape { let profile = rect(x: size[0], y: size[1]).round_corners(r: r); return profile.extrude(height: t); // `let`/`check` lines, then exactly one `return` } // call: rounded_plate([60mm, 40mm], r: 4mm, t: 3mm) ``` ```tenon /// Doc comment becomes the part description. part Name { param … // inputs, set by Name::new(width: 50mm) let … // private values (params in scope) part insert = … // component: a template value this part is made from (private) anchor mount = face(at: [0mm, 0mm, -2mm], normal: -Z); // public custom anchor check name: expr { message: "…", severity: "warning" }; // rule on params/lets material: print3d::PETG; // optional properties: material process color tags quantity process: print3d::fdm(up: "top"); color: color("#3366cc"); fn new() -> Shape { // REQUIRED, exactly once, no parameters let body = box(x: 10mm, y: 10mm, z: 2mm); return body; // the part's geometry } } ``` `Name::new(width: 50mm)` (every argument named; others take defaults) returns a `Part` that exposes its params (`p.width`), custom anchors and built-in anchors; `let`s and components are private. ```tenon assembly Table { param height: Length = 720mm; part leg = Leg::new(height: height); // components: built once, placed in new() part slab = TableTop::new(); fn new() -> Assembly { let legs = [for c in corners(rect(x: 1100mm, y: 600mm)) => leg.place(bottom: on(ground), x: at(c.x), y: at(c.y))]; let top = slab.place(bottom: on(legs[0].top), x: centered(legs), y: centered(legs)); check level: world_box(top).max.z > height; // geometry checks go inside new() return Assembly { legs, top }; // fields become members: main.legs[0], main.top } } ``` Each `Assembly { … }` field is a **member** (a part, an assembly, a plain shape, or a list of them), shown in the tree and BOM in field order. `Assembly { legs }` is shorthand for `Assembly { legs: legs }`. Members may not overlap in volume (`E0510`); intended overlaps: `return Assembly { a, b }.allow_overlap(pairs: [[a, b]])` (pairs of member values; list items like `legs[0]` work too). Assembly properties: `tags`, `quantity` only. ```tenon check fits: width >= 3 * screw_d { message: "Too narrow for the screw head" }; // severity "error" (default) | "warning" | "info" main ShelfBracket::new(width: 50mm); output print: export(main, format: "3mf"); output body: export(main.body, format: "stl"); ``` A failing `error` check fails the build (exit 2) but geometry is still produced. ## Core prelude (in scope without `use`, no path) Notation: first parameter may be positional or the method receiver; the rest are named. `Many[T]` = one `T` or a list. **Math & lists** - `abs(x)` · `min(...xs)` · `max(...xs)` · `clamp(x, lo:, hi:)` — any quantity, same dimension - `sqrt(x)` (`sqrt(Area) -> Length`) · `round(x, to: step)` · `floor(x, to:)` · `ceil(x, to:)` (`to` required for quantities) - `sin(a)` `cos(a)` `tan(a)` (Angle → Number) · `asin(n)` `acos(n)` `atan(n)` `atan2(y, x:)` (→ Angle) · `pi` - `len(list)` · `sum(list)` · `all(list)` · `any(list)` · `reverse(list)` · `zip(a, with:)` · `flatten(list)` - `range(from, to:, step:)` — like `from..to` with a step; works with quantities - `str(x)` — text for messages · `to(x, unit: "in")` — plain number in a unit (messages/reports only) **Geometry values**: constants `X` `Y` `Z` (directions; `-Z` works), `origin`, `ground` (face z = 0, normal +Z), planes `XY` `YZ` `XZ` - `vec(x:, y:, z:)` · `direction(x:, y:, z:)` (numbers) · `plane(through: Point, normal: Direction)` - `face(at: Point, normal: Direction)` · `point(at: Point)` · `axis(at: Point, direction: Direction)` — for `anchor` declarations - `facing(dir)` — target for `orient` **2D profiles** (on `XY` unless `on:` gives a plane or face anchor; centred) - `rect(x:, y:, on:)` · `circle(d:, on:)` (diameter only, no `r:`) · `slot(length:, width:, on:)` (long axis X) - `polygon(points: List[Point], on:)` · `text(content, size:, font: "tenon-sans", on:)` - `round_corners(profile, r:)` · `offset(profile, by:)` · `union` / `subtract` / `intersect` also work on profiles - `corners(profile)` → vertices counter-clockwise from front-left: front-left, front-right, back-right, back-left **Solids** - `box(x:, y:, z:)` · `cylinder(d:, height:)` (anchor `axis`) · `cone(d1:, d2:, height:)` (`d1` at bottom) · `sphere(d:)` · `torus(d_major:, d_minor:)` · `wedge(x:, y:, z:, top_x:)` - `extrude(profile, height:, symmetric: false, taper: 0deg)` — from the profile's plane along its normal (+Z for XY), not centred unless `symmetric: true` - `revolve(profile, about: Anchor, angle: 360deg)` · `sweep(profile, path:)` · `loft(...profiles)` - Paths: `line(from:, to:)` · `arc(from:, through:, to:)` · `polyline(points:)` · `helix(d:, pitch:, height:)` · `join(...paths)` **Operations** - `union(...shapes)` · `subtract(target, remove: Many[Shape])` · `intersect(...shapes)` - `fillet(shape, edges: Selection[Edge], r:)` · `chamfer(shape, edges:, d:)` · `shell(shape, open: Selection[Face], thickness:)` (hollows inward) - `hole(shape, at: Point | Anchor, d:, depth: Length | "through", direction: -Z)` — starts at `at` and cuts along `direction`; `"through"` cuts to the far side, so put `at` on the entry face - `linear_array(item, count:, step: Vec3)` → list, first copy at the original position · `polar_array(item, count:, about: Anchor, angle: 360deg)` - `place(item, key: relation, …)` · `orient(item, key: facing(dir), …)` · `rotate(item, about: Direction, angle:, through: = item.center)` · `mirror(item, across: Plane)` · `move(item, by: Vec3)` (escape hatch) **Selectors**: `faces(in:, facing:, created_by:, role:, of:)` · `edges(in:, of:, between:, and:, parallel:, created_by:)` · narrow with `topmost()` `bottommost()` `leftmost()` `rightmost()` `frontmost()` `backmost()` `largest()` `smallest()` · `count(sel)` **Measurements** (usable in `check` and `tenon query`) - `volume(x)` · `area(x)` · `mass(x)` (needs `material`) · `center_of_mass(x)` - `box_of(x)` → `{min, max, size, center}` in the object's own frame · `world_box(x)` → same, axis-aligned in the current frame; `x` may be a list - `size(x)` → `Vec3` = `box_of(x).size` (own dimensions, unaffected by rotation: an upright board's `size(b).x` is still its length) - `distance(a, b)` / `gap(a, b)` (0 if touching) · `touches(a, b)` · `interferes(a, b)` · `fits_within(x, envelope: Vec3)` · `min_thickness(x)` **Materials & outputs** - `material(name:, density:, properties: {})` · `color("#RRGGBB")` - `export(design, format: "step" | "stl" | "3mf" | "gltf" | "brep" | "obj", tolerance: 0.01mm, file:)` - `report(title:, rows: List[Record], columns:, format: "md" | "csv" | "json")` · `bom(design)` ## Methods by type Any function whose first parameter accepts a value is a method on it: `a.f(k: v)` = `f(a, k: v)`. Documented methods: | Receiver | Methods | |---|---| | `Shape` | `union` `subtract(remove:)` `intersect` `fillet` `chamfer` `shell` `hole` · `place` `orient` `rotate` `mirror` `move` `linear_array` `polar_array` · `volume()` `area()` `size()` `box_of()` `world_box()` `center_of_mass()` `min_thickness()` `fits_within` `gap` `distance` `touches` `interferes` | | `Profile` | `extrude(height:)` `revolve(about:)` `sweep(path:)` `round_corners(r:)` `offset(by:)` `corners()` `union` `subtract` `intersect`; field `p.area` | | `Part` | `place` `orient` `rotate` `mirror` `move` `linear_array` `polar_array` · measurements (incl. `mass()`) · `bom()`; stock members add the shop methods (packages card) | | `Assembly` | `place` `orient` `rotate` `mirror` `move` · measurements · `bom()` · `allow_overlap(pairs: [[a, b]])` (on the returned `Assembly { … }`) | | `Selection` | `topmost()` `bottommost()` `leftmost()` `rightmost()` `frontmost()` `backmost()` `largest()` `smallest()` `count()` | | `List` | `len()` `sum()` `all()` `any()` `reverse()` `zip(with:)` `flatten()` `min()` `max()`; lists of parts/shapes: `world_box()` `size()` `union()` | | quantity | `abs()` `round(to:)` `floor(to:)` `ceil(to:)` `clamp(lo:, hi:)` `sqrt()` `str()` `to(unit:)`; Angle: `sin()` `cos()` `tan()` | Inside a method call, selectors without `in:` refer to the receiver: `body.fillet(edges: edges(parallel: Z), r: 2mm)`. Elsewhere pass `in: shape` (else `E0410`). ## Placement ```tenon item.place(: , …) ``` Keys are the **item's** anchors (built-in or custom) or the axis keys `x`, `y`, `z`. Targets are anchors of other things (`base.top`, `legs[0].front`, `ground`). | Relation | Key kind | Meaning | |---|---|---| | `on(target_face, gap: 0mm)` | face | Item's face lies on the target face, facing it (normals opposite). Stacking and side contact. | | `flush(target_face, inset: 0mm)` | face | Coplanar, same normal direction. `inset` moves it inward. | | `at(value)` | axis key: `Length`; face: `Length` (plane coordinate); point (`center`, custom): `Point` | Absolute position in the current frame. For `x:`/`y:`/`z:` the item's bounding-box centre goes there. | | `centered(target)` | axis key | Item's centre on that axis equals the target's (shape, instance, list, or anchor). | **Solve rule:** each face relation fixes one direction (along its normal), an axis key fixes one, a point relation fixes three. `place` applies the *smallest* translation that satisfies them, so **any direction you don't constrain stays where it was** (for a new primitive: centred on 0). Errors: `E0520` `on` normals not opposite / `flush` not equal; `E0521` conflicting relations; `E0523` unknown key; `E0524` wrong relation for key (e.g. `x: on(…)`). ```tenon let base = box(x: 80mm, y: 60mm, z: 4mm); // z from -2mm to 2mm let post = cylinder(d: 10mm, height: 30mm) .place(bottom: on(base.top), x: at(20mm), y: centered(base)); // post z: 2mm..32mm let wall = box(x: 80mm, y: 4mm, z: 40mm) .place(bottom: on(base.top), back: flush(base.back)); // x unconstrained: stays centred let pin = sphere(d: 6mm).place(center: at([0mm, 0mm, 50mm])); ``` **Orientation** (always before `place`): `orient` rotates about the item's centre so object-attached axes (`x`, `y`, `z`, or custom/package face anchors) point where you say. One pair = the smallest rotation (the axis perpendicular to both stays put); two perpendicular pairs fix it fully. For a stock board (length X, width Y, thickness Z): | Call | length | width | thickness | Use | |---|---|---|---|---| | (none) | X | Y | Z | flat on the bench | | `.orient(x: facing(Z))` | Z | Y | X | upright leg | | `.orient(z: facing(-Y))` | X | Z | Y | on edge, runs left-right | | `.orient(x: facing(Y), y: facing(Z))` | Y | Z | X | on edge, runs front-back | | `.orient(x: facing(Y))` | Y | −X | Z | flat, runs front-back | `part.rotate(about: Z, angle: 15deg)` turns about the part's own centre by default (`through:` moves the pivot). ## Selectors (for fillet, chamfer, shell) Faces/edges are chosen by provenance and geometry, never by index. ```tenon faces(facing: Z) // planar faces whose outward normal is +Z faces(facing: Z).topmost() // only the highest of them faces(created_by: wall, role: "top") // the top face that `wall` created edges(of: faces(facing: Z)) // edges bounding those faces edges(parallel: Z) // vertical edges edges(between: base, and: wall) // where base meets wall ``` An empty selection consumed by fillet/chamfer/shell is `E0411`. **Current tools** build a mesh preview: fillets, chamfers and shells are **not drawn** (info `I0601`; the shape is shown and measured without them), measurements are mesh-accurate, selectors are approximate. Write them anyway when the design needs them. ## Common mistakes | # | Wrong | Right | Code | |---|---|---|---| | 1 | `box(x: 40, y: 20, z: 3)` / `thickness >= 1.2` | `box(x: 40mm, y: 20mm, z: 3mm)` / `thickness >= 1.2mm` | E0301 | | 2 | `cylinder(10mm, 20mm)`, `subtract(body, hole)` | `cylinder(d: 10mm, height: 20mm)`, `body.subtract(remove: hole)` | E0201 | | 3 | `cylinder(r: 5mm, h: 20mm)`, `circle(r: 3mm)` | `cylinder(d: 10mm, height: 20mm)`, `circle(d: 6mm)` | unknown argument | | 4 | `3 / 4in`, `1 1/2in`, `1.5"` | `3/4in`, `1in + 1/2in`, `1.5in` | E0301 / syntax | | 5 | `box(...).move(by: [0mm, 0mm, 10mm])` to "sit it on the floor" | `box(...).place(bottom: on(ground))` (boxes are centred) | — | | 6 | `lid.place(top: on(body.top))` | `lid.place(bottom: on(body.top))` — `on` joins opposite-facing faces; `flush` aligns same-facing | E0520 | | 7 | `box(x: 48in, y: 4in, z: 2in)` for a 2x4 | `lumber::board("2x4", length: 48in)`; sizes from `lumber::section("2x4")` | — | | 8 | Members placed into each other (both centred at the origin) | Stack with `on`, align with `flush`; or `.allow_overlap(pairs: [[a, b]])` if intended | E0510 | | 9 | `import stock.lumber as lumber`, `lumber.board(…)`, `print3d.PETG` | `use stock::lumber`, `lumber::board(…)`, `print3d::PETG` | E0030 / E0028 | | 10 | `main::legs`, `base::top` | `main.legs`, `base.top` (`::` is only for packages and `Name::new`) | E0029 | | 11 | `body \|> fillet(…)` | `body.fillet(…)` | E0022 | | 12 | `shape: union(a, b)` / `add top = …` / `allow_overlap: …` | `fn new() -> Shape { return union(a, b) }` / `let top = …` + `return Assembly { top }` / `.allow_overlap(…)` | E0143 / E0144 / E0145 | | 13 | `main Bench()`, `Leg(height: 1m)`, `Leg::create(…)` | `main Bench::new()`, `Leg::new(height: 1m)` | E0140 / E0148 | | 14 | `fn new(width: Length) -> Shape`, `self.width` | `param width: Length = 40mm` + `fn new() -> Shape`; just `width` | E0149 / E0150 | | 15 | `let x = 5mm` (no `;`) / `let mut n = 0` / `n = n + 1` | `let x = 5mm;` / a new `let` or a `for` block | E0032 / E0024 | | 16 | `fn f(x: Length) -> Length { x * 2 }` | `fn f(x: Length) -> Length { return x * 2; }` | E0027 | | 17 | `vec![a, b]`, `&shape`, `Some(x)`, `.unwrap()` | `[a, b]`, `shape`, `x` | E0026 / E0025 / E0008 | | 18 | Only `use stock::lumber`, then `stock::cutlist(…)` or `board.miter(…)` | add `use stock` too | E0151 | | 19 | `= none`, `null`, `nil` | `= None` | E0031 | | 20 | `parts.push(leg)`, `for c in cs { parts.push(…) }`, `f` calling itself, lambdas | `let parts = for c in cs { leg.place(…) };`, `flatten(…)` | E0142 / E0039 / E0120 | | 21 | `[for z in zs => let a = …; [a]]` (statements after `=>`) / `[for z in zs { … }]` | `for z in zs { let a = …; [a] }` (braces, no brackets) | E0033 / E0034 | | 22 | `return a;` or `a;` as the last line of a `for` block; `break` | `a` (no `return`, no `;`); filter with `if` or `if c { continue; }` | E0035 / W0036 / E0040 | | 23 | `part Leg { … };` | `part Leg { … }` (no `;` after a definition's `}`) | W0038 | | 24 | `part Board { part b = lumber::board(…); fn new() -> Part { return b; } }` | a part's `new` is always `-> Shape`; for a single stock member as the design just write `main lumber::board(…);` | E0303 | Also: start every file with `tenon 0.3;`; `rotate`/`orient` before `place`, because `place` never rotates; a definition needs exactly one `fn new()` (`E0147` / `E0131`). ## Example 1: printed part ```tenon tenon 0.3; use print3d; /// L-shaped shelf bracket, printed lying on its side. part ShelfBracket { param width: Length = 40mm { range: 20mm..80mm, doc: "Bracket width along the shelf edge" }; param arm: Length = 60mm { range: 30mm..150mm, doc: "Length of each arm" }; param thickness: Length = 4mm { range: 2mm..10mm }; param screw_d: Length = 4mm { options: [3mm, 4mm, 5mm], doc: "Screw shank diameter" }; let hole_d = screw_d + print3d::allowance("clearance"); material: print3d::PETG; process: print3d::fdm(up: "right"); check room_for_screw: width >= 3 * screw_d { message: "Bracket is too narrow for the screw head" }; fn new() -> Shape { let base = box(x: width, y: arm, z: thickness); let wall = box(x: width, y: thickness, z: arm) .place(bottom: on(base.top), back: flush(base.back), x: centered(base)); return union(base, wall) .fillet(edges: edges(between: faces(created_by: base, role: "top"), and: faces(created_by: wall, role: "front")), r: thickness) .hole(at: [0mm, -arm / 4, thickness / 2], d: hole_d, depth: "through") .hole(at: [0mm, arm / 2 - thickness, thickness / 2 + arm * 0.6], d: hole_d, depth: "through", direction: Y); } } main ShelfBracket::new(); output print: export(main, format: "3mf"); ``` ## Example 2: assembly with a custom anchor ```tenon tenon 0.3; use print3d; /// Screw-boss positions shared by body and lid so they always line up. fn boss_points(inner_x: Length, inner_y: Length) -> List[Point] { return corners(rect(x: inner_x - 8mm, y: inner_y - 8mm)); } /// Open-top box with corner screw bosses. part Body { param inner_x: Length = 80mm { range: 30mm..200mm }; param inner_y: Length = 50mm { range: 30mm..200mm }; param inner_z: Length = 30mm { range: 15mm..100mm }; param wall: Length = 2mm { range: 1.2mm..4mm }; material: print3d::PETG; process: print3d::fdm(up: "top"); fn new() -> Shape { let outer = box(x: inner_x + 2 * wall, y: inner_y + 2 * wall, z: inner_z + wall) .shell(open: faces(facing: Z).topmost(), thickness: wall); let bosses = [for p in boss_points(inner_x, inner_y: inner_y) => cylinder(d: 7mm, height: inner_z - 3mm) .place(bottom: flush(outer.bottom, inset: wall), x: at(p.x), y: at(p.y))]; return union(outer, bosses); } } /// Flat lid with screw holes. part Lid { param inner_x: Length = 80mm; param inner_y: Length = 50mm; param wall: Length = 2mm; /// Underside of the plate: rests on the body's rim. anchor seat = face(at: [0mm, 0mm, -wall / 2], normal: -Z); material: print3d::PETG; process: print3d::fdm(up: "bottom"); fn new() -> Shape { let plate = box(x: inner_x + 2 * wall, y: inner_y + 2 * wall, z: wall); let screw_holes = [for p in boss_points(inner_x, inner_y: inner_y) => cylinder(d: 3.4mm, height: 3 * wall).place(x: at(p.x), y: at(p.y), z: centered(plate))]; return plate.subtract(remove: screw_holes); } } /// Body and lid, closed. assembly Enclosure { param inner_x: Length = 80mm { range: 30mm..200mm }; param inner_y: Length = 50mm { range: 30mm..200mm }; param inner_z: Length = 30mm { range: 15mm..100mm }; fn new() -> Assembly { let body = Body::new(inner_x: inner_x, inner_y: inner_y, inner_z: inner_z); let lid = Lid::new(inner_x: inner_x, inner_y: inner_y) .place(seat: on(body.top), x: centered(body), y: centered(body)); check closed: touches(body, lid) and not interferes(body, lid); return Assembly { body, lid }; } } main Enclosure::new(); output body: export(main.body, format: "3mf"); output lid: export(main.lid, format: "3mf"); ``` ## Example 3: lumber project with a cut list ```tenon tenon 0.3; use stock; use stock::lumber; use stock::sheet; /// A 2x4 on edge, running left to right. fn rail(label: String, length: Length) -> Part { return lumber::board("2x4", length: length, label: label).orient(z: facing(-Y)); } /// Bench: four 2x4 legs, front and back rails, plywood top. assembly Bench { param length: Length = 48in { range: 24in..96in, step: 1in, group: "Size" }; param depth: Length = 16in { range: 12in..30in, group: "Size" }; param height: Length = 18in { range: 12in..36in, doc: "Floor to seat" }; let s = lumber::section("2x4"); // actual { width: 3.5in, thickness: 1.5in } let top_t = sheet::thickness("plywood_3/4"); // actual 23/32in part leg = lumber::board("2x4", length: height - top_t, label: "Leg"); part rail_2x4 = rail("Rail", length: length - 2 * s.thickness); part seat = sheet::panel("plywood_3/4", x: length, y: depth, label: "Top"); fn new() -> Assembly { // upright legs: thickness along X, width along Y; corners(): front-left, front-right, back-right, back-left let legs = [for c in corners(rect(x: length - s.thickness, y: depth - s.width)) => leg .orient(x: facing(Z)) .place(bottom: on(ground), x: at(c.x), y: at(c.y))]; let front_rail = rail_2x4.place(top: flush(legs[0].top), front: flush(legs[0].front), x: at(0mm)); let back_rail = rail_2x4.place(top: flush(legs[3].top), back: flush(legs[3].back), x: at(0mm)); let top = seat.place(bottom: on(legs[0].top), x: centered(legs), y: centered(legs)); check seat_height: world_box(top).max.z == height { message: "Seat must be at the requested height" }; return Assembly { legs, front_rail, back_rail, top }; } } main Bench::new(); output cuts: stock::cutlist(main, kerf: 1/8in, buy: [lumber::stock("2x4", length: 8ft)], optimize: "min_waste"); output parts: bom(main); ``` --- # Task make me a round spacer, 12mm outside diameter, 5.2mm hole through the middle, 8mm tall --- Reply with a single ```tenon code block containing one complete Tenon file (starting with `tenon 0.3;`). Put any explanation before the code block, keep it short, and do not include other code blocks.