# 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); ``` --- # Tenon packages card: `print3d` and `stock` (language 0.3) Addendum to the reference card. Same rules: units everywhere, first argument positional, the rest named; `::` for package paths, `.` for values and methods. `use` what you need: ```tenon use print3d; // print3d::PETG, print3d::fdm(...) use stock; // stock::cutlist, stock::deflection, ... and the board methods below use stock::lumber; // lumber::board, lumber::section, lumber::stock use stock::sheet; // sheet::panel, sheet::thickness, sheet::stock use stock::steel; // steel::member, steel::section, steel::stock ``` ## print3d (3D printing, usually in mm) **Processes** (a part's `process:` property): - `print3d::fdm(nozzle: 0.4mm, layer: 0.2mm, up: "top", supports: false, allow: [])` - `print3d::sla(layer: 0.05mm, up: "top", allow: [])` · `print3d::sls(layer: 0.1mm, allow: [])` `up` names the part's face anchor that points away from the build plate (`"top"` = printed as modelled; `"right"` = lying on its left side). It never moves the part in assemblies. `allow: ["overhang"]` disables a named process check for that part. **Materials** (`material:`): `print3d::PLA` `print3d::PETG` `print3d::ABS` `print3d::ASA` `print3d::TPU` `print3d::PA` `print3d::RESIN_STANDARD` `print3d::PA12_SLS`. They have density, so `mass(part)` works. **Fits**: printed holes come out small and pegs large; state intent, not magic numbers. - `print3d::allowance(fit: "press" | "snug" | "slide" | "clearance", process: = )` → diametral `Length`. FDM 0.4 mm: press 0mm, snug 0.1mm, slide 0.25mm, clearance 0.4mm. ```tenon let hole_d = 3mm + print3d::allowance("clearance"); // M3 screw passes through let peg_d = 6mm - print3d::allowance("slide"); // peg slides in a 6mm hole ``` - `print3d::insert_hole(size: "M3", length: = )` → cutting `Shape` for a heat-set insert: a cylinder (M3: d 4.0mm, default length 5.7mm) **centred on the origin, axis Z**, like `cylinder`. Sizes `M2` `M2.5` `M3` `M4` `M5`. - `print3d::nut_trap(size: "M3", depth:)` → hexagonal prism of height `depth`, centred on the origin, axis Z. - Place either before subtracting: `body.subtract(remove: print3d::insert_hole(size: "M3").place(top: flush(body.top), x: at(10mm)))` (insert pressed in from the top face). **Automatic checks** (reported as `print3d.`; FDM defaults): `min_wall` min thickness ≥ 2 × nozzle (error) · `overhang` downward faces steeper than 45° with `supports: false` (warning) · `bridge` unsupported spans > 10mm (warning) · `min_feature` holes/pins < 2 × nozzle (warning) · `first_layer` plate contact < 5% of footprint (warning) · `fits_bed` (error, when a printer is set) · `closed_solid` watertight (error). **Printers**: `print3d::printer(name: "…")` or `print3d::printer(volume: [250mm, 210mm, 220mm])` → build volume. Check bed fit yourself: `check fits_bed: fits_within(main, envelope: [250mm, 210mm, 220mm])`. ## stock (lumber, sheet goods, steel; usually in inches) Constructors return ready-to-use parts (no `::new`), created **centred, lying flat: length along X, width along Y, thickness along Z**. Their process is set for you; don't set `process:`. Dimensions are **actual** — read them from the catalog. **Create** - `lumber::board(id, length:, label:, grain: "x", region: "US")` - `sheet::panel(id, x:, y:, grain: "x" | "y" | "any", label:)` — x × y panel, thickness from the catalog - `steel::member(id, length:, label:)` · `stock::member(catalog:, id:, length:, label:)` (generic) Give every member a `label` (it is the cut-list name). Declare members as components (`part leg = lumber::board(…)`) and place copies in `new()`. **Catalog sizes** - `lumber::section(id)` / `steel::section(id)` → `{ width, thickness }` (width along Y, thickness along Z of the unrotated member) - `sheet::thickness(id)` → `Length` **Member anchors** (object-attached, rotate with the member): `start` (−X end face), `end` (+X end face), `face` (+Z wide face), `edge` (+Y narrow face). Built-in `top`/`bottom`/… are world-aligned bounding-box faces as usual. **Turning boards** (before `place`; see the reference card's orient table): ```tenon lumber::board("2x4", length: 30in, label: "Leg").orient(x: facing(Z)); // upright: width Y, thickness X lumber::board("2x4", length: 45in, label: "Rail").orient(z: facing(-Y)); // on edge left-right: width Z, thickness Y lumber::board("2x4", length: 20in, label: "Side").orient(x: facing(Y), y: facing(Z)); // on edge front-back: width Z, thickness X ``` ### Catalog ids US softwood lumber (`stock::lumber`, material `softwood_spf`, stock lengths 8ft 10ft 12ft 16ft). Actual thickness × width: | id | actual | id | actual | id | actual | |---|---|---|---|---|---| | `1x2` | 0.75 × 1.5in | `1x10` | 0.75 × 9.25in | `2x6` | 1.5 × 5.5in | | `1x3` | 0.75 × 2.5in | `1x12` | 0.75 × 11.25in | `2x8` | 1.5 × 7.25in | | `1x4` | 0.75 × 3.5in | `2x2` | 1.5 × 1.5in | `2x10` | 1.5 × 9.25in | | `1x6` | 0.75 × 5.5in | `2x3` | 1.5 × 2.5in | `2x12` | 1.5 × 11.25in | | `1x8` | 0.75 × 7.25in | `2x4` | 1.5 × 3.5in | `4x4` / `6x6` | 3.5 × 3.5 / 5.5 × 5.5in | Sheet goods (`stock::sheet`, 4ft × 8ft): `plywood_1/4` (7/32in actual) · `plywood_1/2` (15/32in) · `plywood_3/4` (23/32in) · `mdf_1/2` (1/2in) · `mdf_3/4` (3/4in). Steel (`stock::steel`, ids in inches, `WxHxWALL`; 20ft sticks): `square_tube_1x1x0.065` · `square_tube_1.5x1.5x0.083` · `square_tube_2x2x0.12` · `rect_tube_2x1x0.083` (2in wide, 1in high) · `angle_1.5x1.5x0.125` · `flat_bar_1x0.25` (1in × 1/4in) · `round_tube_1x0.065` (1in OD). Unknown ids are `E0701` with suggestions. Never model `"2 x 4"` as a box. ### Shop operations (methods on stock members; return a new member; recorded in the cut list) All work in the member's **own, unrotated frame** (length along X, centred), so apply them before `orient`/`place`. Each is also callable as `stock::op(m, …)`; on a non-stock part they are `E0702`. **They require `use stock`** (`use stock::lumber` alone is not enough; `E0151`). - `m.miter(end: "left" | "right", angle:, across: "width" | "thickness" = "width", long_side: "+" | "-" = "+")` — `angle` from square (0deg = square cut); `long_side` = which side (+Y/−Y for width, +Z/−Z for thickness) keeps full length. Cut lists report long-point lengths. - `m.bevel(end:, angle:)` — miter across thickness · `m.rip(width:)` - `m.notch(at:, width:, depth:, face: "top" | "bottom" | "front" | "back")` · `m.dado(at:, width:, depth:, face:)` — `at` = distance from the left (−X, `start`) end to the near side of the cut - `m.rabbet(edge: "front_top" | "front_bottom" | "back_top" | "back_bottom", width:, depth:)` - `m.drill(at: Point | Anchor, d:, depth: Length | "through", direction: -Z)` (`depth` required) — a Point `at` is measured from the member's **centre** (x from −length/2 to +length/2); the hole starts at `at` and cuts along `direction`, so put `at` on the entry face: `m.drill(at: [-20in, 0mm, s.thickness / 2], d: 3/8in, depth: "through")` or `at: m.face` - `m.cut_to_fit(between: Anchor, and: Anchor)` — length = distance between two face anchors ### Purchasable stock (for `buy:`) `lumber::stock(id, length:, price: = None)` · `steel::stock(id, length:, price:)` · `sheet::stock(id, x:, y:, price:)` — e.g. `lumber::stock("2x4", length: 8ft)`. `buy: "catalog"` uses catalog lengths/sizes. ### Reports (use in `output`) - `stock::cutlist(design, kerf:, buy: "catalog" | List[Stock], optimize: "min_waste" | "min_count" | "none", min_offcut:)` — parts table, stock plan, totals. A piece longer than any stock is `E0710`. - `stock::sheet_layout(design, kerf:, buy:, respect_grain: true)` · `stock::shopping_list(design, prices: "catalog")` · `bom(design)` (core) - Accessors (for `check` and `tenon query`): `stock::purchase_count(report, id: "2x4")` → `Int` · `stock::waste(report)` → fraction · `stock::total_cost(report)`. Refer to outputs as `outputs.`: `stock::purchase_count(outputs.cuts, id: "2x4")`. ### Structural sanity checks (closed-form, not engineering review) - `stock::deflection(member, load: Force, span: Length, support: "simple" | "cantilever" | "fixed", load_type: "point_center" | "uniform" = "uniform", axis: "strong" | "weak" = "strong")` → `Length` · `stock::stress(member, …)` → max bending stress ```tenon check shelf_stiff: stock::deflection(front_low, load: 450N, span: 44in, support: "simple") <= 44in / 240 { severity: "warning" }; ``` ### Example: welded steel frame with mitered corners ```tenon tenon 0.3; use stock; use stock::steel; /// Welded table frame: mitered top ring on four square-cut legs. assembly Frame { param length: Length = 36in { range: 18in..72in }; param width: Length = 20in { range: 12in..36in }; param height: Length = 30in { range: 12in..40in }; let tube = "square_tube_1x1x0.065"; let t = steel::section(tube).width; part leg = steel::member(tube, length: height - t, label: "Leg"); part long_tube = steel::member(tube, length: length, label: "Long side"); part short_tube = steel::member(tube, length: width, label: "Short side"); fn new() -> Assembly { let legs = [for c in corners(rect(x: length - t, y: width - t)) => leg.orient(x: facing(Z)).place(bottom: on(ground), x: at(c.x), y: at(c.y))]; let front = long_tube .miter(end: "left", angle: 45deg, long_side: "-") .miter(end: "right", angle: 45deg, long_side: "-") .place(bottom: on(legs[0].top), front: flush(legs[0].front), x: at(0mm)); let back = long_tube .miter(end: "left", angle: 45deg, long_side: "+") .miter(end: "right", angle: 45deg, long_side: "+") .place(bottom: on(legs[3].top), back: flush(legs[3].back), x: at(0mm)); // Sides: orient(x: facing(Y)) turns local +Y toward -X. let left = short_tube .miter(end: "left", angle: 45deg, long_side: "+") .miter(end: "right", angle: 45deg, long_side: "+") .orient(x: facing(Y)) .place(bottom: on(legs[0].top), left: flush(legs[0].left), y: at(0mm)); let right = short_tube .miter(end: "left", angle: 45deg, long_side: "-") .miter(end: "right", angle: 45deg, long_side: "-") .orient(x: facing(Y)) .place(bottom: on(legs[1].top), right: flush(legs[1].right), y: at(0mm)); check frame_height: world_box(front).max.z == height; return Assembly { legs, front, back, left, right }; } } main Frame::new(); output cuts: stock::cutlist(main, kerf: 1/16in, buy: [steel::stock("square_tube_1x1x0.065", length: 20ft)], optimize: "min_waste"); ``` --- # Task Build a freestanding shelving unit as an assembly, from stock lumber and plywood. The footprint is 36 inches wide (left to right) by 12 inches deep, and it's 60 inches tall: four 2x2 legs, 60 inches long, standing upright at the corners. There are three shelf levels. The top surface of each shelf is at 14 in, 34 in and 54 in above the floor. Each level has a front rail and a back rail made of 1x3, running left to right between the legs, standing on edge (the 3/4 in thickness facing front/back), with its outside face flush with the outside faces of the legs. A 1/2 in plywood shelf rests on the two rails of its level; it spans the full 12 in depth and fits between the legs left to right. Name the members `legs` (4), `rails` (6, listed from the bottom level up, front then back on each level) and `shelves` (3, bottom first). --- 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.