# Woodwerx design language Woodwerx designs are written in KDL: a `project` of parameters, materials, components and an assembly, which the compiler turns into solids, drawings, a cut list and a fabrication pack. This is the reference for that language. It is generated from the compiler's own tables, the example library and the language reference, and every code sample on it has been parsed or compiled when it was built. Three facts first: dimensions are floats (`19.0`, never `19`); a quoted string in a value position is an expression, a bare word is a name; and Z is negative going down from a part's top face. Plain-text versions for tools and agents: every page is available as Markdown at `/technology/language/.md`, and the whole reference as one file at `/llms.txt` (also `/technology/language.md`). ## Start here - [Quickstart](start.md): *A minimal valid `.kdl` skeleton* - [Task index](tasks.md): *"How do I..." jump-in points, each pointing at the file with the real syntax* - [Node router](nodes.md): *Which file documents which node* ## Language reference - [Banding & Stringing](banding-and-stringing.md): Covers `banding`, `stringing`, `lay`, `pattern`. - [Bevels & Rabbets](bevel-and-rabbet.md): Covers `bevel`, `rabbet`, `moulding`, `trim`. - [Components & Stock](components-and-stock.md): Covers `component`, `stock`, `role`, `grain`, `mark`, `normal`, `tilt`, `heading`, and more. - [Curved Members](curved-members.md): Covers `bend`, `arc`, `spline`. - [Cut-Path Outlines](cut-paths.md): Covers `cut_path`, `path`, `keep`, `line`, `arc`, `polyline`, `point`, `kerf`. - [Edge & Face Cuts](edge-and-face-cuts.md): Covers `dado`, `groove`, `rabbet`, `chamfer`, `roundover`, `moulding`, `taper`, `hole`, and more. - [Grain Axis](grain-axis.md): Covers `stock`, `along_x`, `along_y`, `along_z`. - [Hardware Mounting](hardware-mounting.md): Covers `hardware`, `hardware_mount`, `hardware_use`, `distribute`. - [Joinery Contract](joinery-contract.md): Covers `joinery`, `join`, `male`, `female`, `joint`, `clearance`, `fit`, `butt`, and more. - [Fastener Joints](joinery-fasteners.md): Covers `dowel`, `pocket_hole`, `biscuit`, `domino`, `edge_glue`, `screw_type`, `spacing`, `count`, and more. - [Frame Joinery](joinery-frames.md): Covers `miter`, `stile_and_rail`, `mitred_moulding`, `angle`, `groove_width`, `groove_depth`, `stub_tenon_length`, `profile`. - [Laps, Bridles & Dovetails](joinery-laps-and-boxes.md): Covers `half_lap`, `cross_lap`, `bridle`, `box_joint`, `dovetail_through`, `dovetail_half_blind`, `sliding_dovetail`, `depth_ratio`, and more. - [Tenon Joinery](joinery-tenons.md): Covers `mortise_and_tenon`, `round_tenon`, `tenon_thickness`, `tenon_width`, `tenon_length`, `haunch`, `centering`, `through`, and more. - [Loose Inserts](loose-inserts.md): Covers `dowel`, `biscuit`, `domino`. - [Materials & Hardware](materials-and-hardware.md): Covers `materials`, `material`, `type`, `color`, `finish`, `hardware`, `hardware_use`, `hardware_mount`, and more. - [Model Metrics](model-metrics.md): Covers `volume`, `no_join`, `a`, `b`, `reason`. - [Non-Wood Materials](non-wood-materials.md): Covers `stock`, `ring`, `constraint`, `to`, `substance`, `tempered`. - [Oblique & Skewed Joints](oblique-and-skewed-joints.md): Covers `cross_lap`, `tilt`, `heading`. - [Parts & Assemblies](parts-and-assemblies.md): Covers `assembly`, `part`, `instance`, `by`, `pos`, `rot`, `mirror`, `repeat`, and more. - [Paths & Splines](paths-and-splines.md): Covers `path`, `cut_path`, `spline`, `circle`, `line`, `arc`, `polyline`. - [Plans & Interfaces](plans-and-interfaces.md): Covers `plan`, `site`, `room`, `run`, `place`, `fill`, `interface`, `wall`, and more. - [Project & Params](project-and-params.md): Covers `project`, `units`, `meta`, `params`, `preset`, `label`, `category`, `unit_display`, and more. - [Reuse & Collections](reuse-and-collections.md): Covers `import`, `from`, `export`, `collection`, `extends`, `choice`, `slots`, `style`, and more. - [Round Stock & Sections](round-stock-sections.md): Covers `shape`, `rect`, `round`, `section`, `interp`, `circle`, `samples`, `axis`, and more. - [Sliding Sleeve](sliding-sleeve.md): Covers `sliding_sleeve`. - [Speaker Analysis](speaker-analysis.md): Covers `driver`, `analysis`, `ac_sealed_fc`, `ac_sealed_qtc`, `ac_port_length`, `ac_helmholtz_fb`, `fs`, `qts`, and more. - [Style & Finish Rule](style-finish-rule.md): Covers `finish`, `one_of`. - [Tenon Tips & Coped Rails](tenon-tips-wedges-and-coped-rails.md): Covers `tip`, `wedge`, `stile_and_rail`. - [Tongue & Groove](tongue-and-groove.md): Covers `tongue_and_groove`. - [Travel](travel.md): Covers `travel`, `body`, `pivot`. - [Turning & Sections](turning-and-sections.md): Covers `stock`, `section`, `turn`, `profile`. - [Veneer & Laminate](veneer-and-laminate.md): Covers `veneer`, `match`. ## Generated indexes - [Vocabulary](vocabulary.md): Every node, joinery kind, feature, anchor, face, grain and expression function the language accepts. - [Names](names.md): A keyword index: every name with its kind, the page that documents it and how often examples use it. - [Diagnostics](diagnostics.md): Every ww:: diagnostic code with its message and help text. - [Examples](examples.md): Complete designs, three in full, and the list of the whole example library. --- # Quickstart *A minimal valid `.kdl` skeleton* Get a first draft through `design_compile` (or `ww check`) with zero diagnostics before you add anything real. Then use [`features/INDEX.md`](names.md) for the syntax of whatever comes next, [`nodes.md`](nodes.md) to find the file for a node, and [`tasks.md`](tasks.md) for a "how do I..." jump-in. The language contract is `docs/KDL_AUTHORING.md`. Four rules cause most first-draft diagnostics: - Every dimension is a float (`19.0`, never `19`), and `units` must be `mm`. - Z is negative going down from a part's top face (`z=0.0` is the top). - A **quoted string is a CEL expression**; a bare word is a name or enum (`x="width / 2.0"`, `normal=pos_x`). - A joint is `join joint= male=Part@Mark female=Part@Mark`, unquoted `Part@Mark`; the word `connect` is dead. ```kdl // A minimal one-part project: a single board, no joinery. project name="Test Shelf" units=mm schema_version=1 { params { width 600.0 min=300.0 max=1200.0 step=10.0 label="Width" category="Envelope" depth 250.0 min=150.0 max=400.0 step=10.0 label="Depth" category="Envelope" thick 19.0 min=12.0 max=32.0 step=1.0 label="Thickness" category="Stock" } materials { material WhiteOak type=hardwood } assembly Root { part Board material=WhiteOak role=shelf { stock WhiteOak width depth thick by top_sw pos x=0.0 y=0.0 z=0.0 } } } ``` ## Adding a second part and a joint A real design uses `component` (a reusable template with `mark`s to join on) and `joinery` + `join` (the two-body contract). Here an apron's tenon enters a leg. The apron's mark sits on the **shoulder**, the end of its stock line, facing out (`neg_x`); the leg's mark sits on the face the tenon enters, facing the apron (`pos_x`). Their positions must line up when placed. ```kdl project name="Test Frame" units=mm schema_version=1 { params { leg_dim 45.0 min=30.0 max=64.0 step=1.0 label="Leg Section" category="Stock" leg_len 700.0 min=400.0 max=900.0 step=10.0 label="Leg Length" category="Stock" apron_len 400.0 min=200.0 max=1200.0 step=10.0 label="Apron Length" category="Stock" apron_h 100.0 min=60.0 max=140.0 step=5.0 label="Apron Height" category="Stock" apron_th 20.0 min=16.0 max=25.0 step=1.0 label="Apron Thickness" category="Stock" glue_clearance 0.2 } materials { material Ash type=hardwood } component Leg role=leg { stock Ash leg_dim leg_dim leg_len mark MortiseFace_X x=leg_dim y="leg_dim / 2.0" z=-55.0 normal=pos_x } component Apron role=apron { stock Ash apron_len apron_th apron_h mark Shoulder x=0.0 y="apron_th / 2.0" z="0.0 - apron_h / 2.0" normal=neg_x } joinery mortise_and_tenon name=ApronJoint { tenon_thickness 8.0 tenon_length 25.0 clearance glue_clearance centering axis=z } assembly Root { part Leg_FL instance=Leg { by top_sw; pos x=0.0 y=0.0 z=0.0 } part Apron_Front instance=Apron { by top_sw pos x=leg_dim y="(leg_dim - apron_th) / 2.0" z=-5.0 } join joint=ApronJoint male=Apron_Front@Shoulder female=Leg_FL@MortiseFace_X } } ``` ## Validate ```sh ww check your_file.kdl # clashes, joint contacts, and the joinery verdicts cargo run --manifest-path tools/kdl-check/Cargo.toml -- your_file.kdl # lint only ``` `kdl-check` is a linter: it cannot see geometry, so a green lint is not a joint that builds. Run `ww check` (or `design_compile` over MCP) and read its diagnostics. Joinery kinds that refuse an impossible configuration say why in the diagnostic; the family file in [`features/`](names.md) names the rules. --- # Task index *"How do I..." jump-in points, each pointing at the file with the real syntax* Task-shaped jump-in points. Each answer is one or two lines and a pointer: the syntax, a compiling example and the pitfalls are in the named file under [`features/`](names.md). Where a file marks something **not built**, it is also recorded by the compiler as skipped, not an error, so a design that uses it still compiles and silently lacks the cut: read the file's *Pitfalls* section. Before you rely on a "not built" line here, check `features/INDEX.md` for a newer file that builds it. ## Shape and material - **Taper a leg.** `taper start_z= end_z= target_dim= { faces ... }` in the leg's `component`. [`edge-and-face-cuts`](edge-and-face-cuts.md). - **Make a turned part (leg, spindle, chess piece).** `stock shape=round diameter= z=` plus a `section` of `circle` stations. A leg square at the top and turned below keeps square `stock` and a `section` that stops short of the top (the pommel). Not built: a non-circular varying section, and a mark on the curved surface (only the flat axis ends take marks). [`round-stock-sections`](round-stock-sections.md). - **Cut a curved outline (round top, felloe, bootjack notch).** `cut_path` with `line`, `arc`, `polyline`; `keep="inside"` makes the path the new footprint, `keep="outside"` pierces a cut-out. A smooth freeform edge (a cloud lift, a serpentine front, a vase splat) is a `spline` through points, sampled to a tolerance, and a whole round is a `circle`; a named `path { ... }` holds a compound path. [`cut-paths`](cut-paths.md), [`paths-and-splines`](paths-and-splines.md). - **Round or ease an edge, or rebate it.** `chamfer`, `roundover`, `rabbet` with an `edges` child list; selectors include `top_perimeter`, `bottom_perimeter`, `front_perimeter`, `top_n ... bottom_w` and the `upright_*` arrises. [`edge-and-face-cuts`](edge-and-face-cuts.md). - **Bevel an edge.** `bevel angle=35.0 { edges top_s }`, or `bevel face=pos_z edge=neg_y angle=35.0 [depth=]` for any edge. `chamfer size=` is the 45 degree equal-leg case. [`bevel-and-rabbet`](bevel-and-rabbet.md). - **Cut a moulding profile (ovolo, cove, ogee, bead).** `moulding section= size= { edges ... }`; `half_round` and `cockbead` are applied strips, their own narrow `component`. [`edge-and-face-cuts`](edge-and-face-cuts.md). - **Cut a slot, not a round hole.** `hole diameter= depth= length= axis=x|y`. [`edge-and-face-cuts`](edge-and-face-cuts.md). - **Level a raked leg's foot to the floor.** `trim reference=world normal=pos_z at=0.0 { faces ... }`; name a side face, not the end. [`edge-and-face-cuts`](edge-and-face-cuts.md). - **Glue up thick stock from boards.** `lamination` is not built as material removal. Model the finished thickness as plain `stock` and note it in a comment. - **Veneer or laminate a face.** `veneer face= material= ...`; a laminate is the same node with a `type=sheet` material. [`veneer-and-laminate`](veneer-and-laminate.md). - **Inlay a banding or stringing line round a panel.** `banding face= material= width= depth= offset=` or `stringing face= material= offset=`: one node cuts the groove and generates the flush filler, mitred at the corners; `pattern=herringbone`, `angle=90` for cross-banding. [`banding-and-stringing`](banding-and-stringing.md). - **Choose a material, a finish, a display colour.** `material ... finish="'danish oil'" color="#c8a06e"`; `color` is presentation only. [`materials-and-hardware`](materials-and-hardware.md). - **Add a hinge, pull or drawer slide.** A `hardware` catalogue item (a slide takes `travel_distance` `slide_depth` `load_kg`), then `hardware_use qty=` or a `hardware_mount` join. It reaches the hardware schedule, never the cut list. [`materials-and-hardware`](materials-and-hardware.md). ## Joining two parts Start at [`joinery-contract`](joinery-contract.md): its kinds table names the file for every joint, and its layout rule (marks facing each other, male mark on the male's end or cheek) applies to all of them. - **Prove a drawer, door or lid can move.** A `travel` child on the `join` that hangs it (`travel axis=-y body=assembly`, or `kind=angle` for a hinge). `ww check` sweeps it over its range against every other part and names the angle or distance at which anything fouls. [`travel`](travel.md). - **Rail into a leg.** `mortise_and_tenon` ([`joinery-tenons`](joinery-tenons.md)). - **Make a through tenon stand proud, drawbore, wedge or key it.** `through #true`, `proud`, `pin`, `wedge`, `key`: all in [`joinery-tenons`](joinery-tenons.md); each needs a square, axis-aligned frame. - **Join at an angle (splayed leg, dished spoke).** An oblique `mortise_and_tenon`: the tenon runs along the male's shoulder normal as placed and may meet the female face up to 45 degrees off square. Give the mark an oriented facing with `tilt=` and `heading=` (RFC-0022, built). [`joinery-tenons`](joinery-tenons.md), [`components-and-stock`](components-and-stock.md). A compound mitre (a leaning, turning corner) is still not synthesised. - **Join a turned rung into a leg.** `round_tenon diameter= length= clearance=`; it may be oblique (up to 45 degrees). [`joinery-tenons`](joinery-tenons.md). - **Cross two members.** `half_lap`, `cross_lap`, `bridle`; **corner a box or drawer:** `box_joint`, `dovetail_through`, `dovetail_half_blind`; **capture a shelf in a housing:** `sliding_dovetail`. [`joinery-laps-and-boxes`](joinery-laps-and-boxes.md). - **Size a dovetail or box joint's pins to the board.** `box_joint`: `pin_count` or `finger_width` (not both). Dovetails: `pin_count` or a graduated `repeat` of `pin index= width=`, not both. [`joinery-laps-and-boxes`](joinery-laps-and-boxes.md). - **Mitre a corner or frame a panel.** `miter`, `stile_and_rail`. [`joinery-frames`](joinery-frames.md). - **Use dowels, biscuits, dominos, pocket screws, or a glued seam.** [`joinery-fasteners`](joinery-fasteners.md). ## Placement, repetition and reuse - **Place four legs at four corners.** Anchor each by the corner that lands there (`by top_sw/top_se/top_ne/top_nw`, no `rot`), or one anchor with `rot z=0/90/180/270`. Never mix the two: it swings the part out of the assembly. [`parts-and-assemblies`](parts-and-assemblies.md). - **A true left/right pair.** `mirror x=|y=|z=` on the second `part`, not a `rot` plus hand-adjusted offsets. [`parts-and-assemblies`](parts-and-assemblies.md). - **A row of parts or features.** `repeat count= { ... }` with `id="Name_{var}"` wherever an id could collide. Legal in an assembly, a component and a joinery body. [`parts-and-assemblies`](parts-and-assemblies.md). - **Use the built-in drawer or my own library.** `import Alias from="std:drawer" export=DrawerBox` or `from="lib/x.kdl"`; override the assembly's params as child nodes on the `part`. [`reuse-and-collections`](reuse-and-collections.md). - **Offer a build variant (face frame or frameless).** `collection` with a `choice`; the params are `coll_` and `coll__is_`. [`reuse-and-collections`](reuse-and-collections.md). - **Use an expression or a function.** A quoted string is CEL: `sin cos tan asin acos atan atan2 floor ceil round abs min max sqrt pow inch`, in degrees. [`project-and-params`](project-and-params.md). - **Size a speaker port, or see a sealed or ported box's response.** `ac_port_length`, `ac_sealed_qtc` in a param; a `driver` and an `analysis` node, read with `ww analyze`. Litres, Hz, mm. [`speaker-analysis`](speaker-analysis.md). ## When none of this covers it If the furniture genuinely has no representation (steam bending, a working `lamination`), do not invent syntax. Model the closest buildable approximation, add a `// RFC-NNNN:` comment at the line, and file `docs/rfc/NNNN-short-name.md` per `docs/KDL_AUTHORING.md` §11. The template requires naming which existing examples you checked, because a past RFC claimed a gap that `desk.kdl` had already closed. --- # Node router *Which file documents which node* This page is a router, not a reference. Each language family has one file under [`features/`](names.md) with its syntax, a compiling example and its pitfalls. Grep `features/INDEX.md` (its **Name index** lists every node, joinery kind and property a feature file declares) or use the table below. The binding contract is `docs/KDL_AUTHORING.md`; where this directory and that document disagree, the document wins and the disagreement is a bug to report. **Facts that apply to every node** (KDL_AUTHORING §1, §4, §7b): - Dimensions are floats: `19.0`, never `19`. `units=mm` is the only unit; `(in)34.5` is an inch literal, `(in)"1-3/4"` (a string) is a fractional-inch literal (`34 1/2`, `3/4`, `1'-2"`), and `inch(x)` converts in an expression. `--set`/`overrides` also take length text (`"34 1/2"`, `"12.7mm"`). - Z is negative going down from a part's top face: `z=0.0` is the top, `z=-90.0` is 90 mm down. - A **quoted string is a CEL expression**; a **bare word is a name or enum**. `x="width / 2.0"` is an expression, `normal=pos_x` is a name. Never quote an enum value. - Features on one part are order-independent: every cutter is unioned and cut once. - Enum lists (`edges`, `faces`) are **child nodes**; KDL v2 has no array literal. - `join` (not `connect`) takes `male=Part@Mark female=Part@Mark`, unquoted. ## Node to file | You are writing | Nodes | File | | --- | --- | --- | | The root, parameters, expressions, limits | `project` `params` `meta` `unit_display`, CEL functions | [`project-and-params`](project-and-params.md) | | Materials, finish, colour, purchased hardware | `materials` `material` `hardware` `hardware_use` `hardware_mount` `finish` `color` | [`materials-and-hardware`](materials-and-hardware.md) | | A part template, its stock, grain and marks | `component` `stock` `mark` `tilt=` `heading=` | [`components-and-stock`](components-and-stock.md) | | A part that moves (drawer, door, lid, vise chop): checked over its travel | `travel` | [`travel`](travel.md) | | Placing parts | `assembly` `part` `by` `pos` `rot` `mirror` `repeat` `role=reference` | [`parts-and-assemblies`](parts-and-assemblies.md) | | Importing or choosing a build variant | `import` `collection` `choice` `slots` `construction` | [`reuse-and-collections`](reuse-and-collections.md) | | Cuts into a face or edge | `dado` `groove` `rabbet` `chamfer` `roundover` `moulding` `taper` `hole` `trim` | [`edge-and-face-cuts`](edge-and-face-cuts.md) | | A cut-out or curved outline | `cut_path` `line` `arc` `polyline` `path` | [`cut-paths`](cut-paths.md) | | A smooth spline edge, a named compound path | `spline` `circle` `path` `cut_path` | [`paths-and-splines`](paths-and-splines.md) | | Veneer or laminate faces | `veneer` `match` | [`veneer-and-laminate`](veneer-and-laminate.md) | | Turned parts | `shape=round` `section` `circle` `pommel` | [`round-stock-sections`](round-stock-sections.md) | | A member curved along its own length (a crest rail, a splat) | `bend` `arc` `spline` | [`curved-members`](curved-members.md) | | Any joint: marks, `join`, clearance, which kind | `joinery` `join` `butt` | [`joinery-contract`](joinery-contract.md) | | Dowels, pocket holes, biscuits, dominos, edge glue | `dowel` `pocket_hole` `biscuit` `domino` `edge_glue` | [`joinery-fasteners`](joinery-fasteners.md) | | Mortise and tenon, round tenon, pin, wedge, key | `mortise_and_tenon` `round_tenon` | [`joinery-tenons`](joinery-tenons.md) | | Laps, bridles, box joints, dovetails | `half_lap` `cross_lap` `bridle` `box_joint` `dovetail_*` `sliding_dovetail` | [`joinery-laps-and-boxes`](joinery-laps-and-boxes.md) | | Mitres, panel-frame corners | `miter` `stile_and_rail` | [`joinery-frames`](joinery-frames.md) | `features/INDEX.md` is generated from the files' front matter and also lists any family file added after this table was written. **Check it first.** ## Words that are accepted but cut nothing The parser knows these names; the build records them as **skipped** (scene JSON `"skipped"`, and a `ww check` warning) instead of failing, so a design with one still compiles and does not do what you meant. - `lamination`: glued-up stock, not material removal. Model the finished thickness as plain `stock`. - `pocket`, `mortise`, `tenon`, `dovetail` written as a **feature**: these are joints. Write a `joinery` and a `join`. ## Limits (compiler-enforced, the same in CLI, server and browser) Source 256 KiB; nesting depth 64; 2,000 parts and 1,000 features per part after every `repeat` expands; one `repeat` 4,096 iterations (nested ones multiply). The diagnostic names the limit it hit. Details: `features/project-and-params.md`. --- # Banding & Stringing *Inlaid border bands, strung lines, and sunburst veneer, as generated parts* **Names:** `banding` `stringing` `lay` `pattern` ## When to use Use `banding` or `stringing` when a design needs a strip of contrasting wood let flush into a face: a line of holly round a walnut top, a herringbone or cross-banded border on a Federal, Hepplewhite or Sheraton panel, a flush edge band. One node cuts the groove in the host and generates the filler part that fills it exactly, so the two cannot disagree. Use `veneer match=sunburst` for a fan of veneer leaves radiating from a point. Do not use any of these for a pictorial motif (patera, bellflower, marquetry): out of scope (RFC-0141). ## Syntax ```kdl grammar banding face=pos_z material= width= depth= offset= [lay=inlay|surface] [thickness=] [pattern=plain|herringbone] [angle=] [{ edges ... }] stringing face=pos_z material= offset= [width=] [depth=] [angle=] veneer face=pos_z material= match=sunburst leaves= [center_u= center_v=] [start_angle= end_angle=] ``` - `offset=` is the distance from the face's edge to the strip's near side. Required for `stringing`; `0` on a `banding` opens the groove through the side. - Defaults: banding 12mm wide, 3mm deep; stringing 1.5mm wide, 2mm deep. `lay=surface` bonds the strip on the face instead (`thickness=`, default 0.6; no groove, no `depth=`). - `pattern=herringbone` splits each strip into two halves at `+angle` and `-angle` (default 45). `angle=90` is cross-banding. The figure is a grain angle, never geometry. - `{ edges pos_x pos_y }` limits the banding to edges named by their neighbouring faces. Strips mitre only where two selected edges meet; the others end square. - Strips are mitred at 45 degrees where two neighbouring edges are both banded. - `repeat i count=3 { stringing ... offset="10.0 + i * 6.0" }` makes concentric lines. - `match=sunburst`: `leaves` wedges (default 8) tile the face from the centre point; each leaf must span under 180 degrees, so a full circle needs at least 3 leaves. ## Example ```kdl project name="Inlaid Panel" units=mm schema_version=1 { materials { material Poplar type=hardwood material Walnut type=hardwood material Holly type=hardwood } assembly InlaidPanel { part Panel role=panel { stock Poplar 300.0 200.0 22.0 lengthwise by top_sw pos x=0.0 y=0.0 z=0.0 banding face=pos_z material=Walnut width=10.0 depth=3.0 offset=10.0 pattern=herringbone repeat i count=2 { stringing face=pos_z material=Holly offset="6.0 + i * 16.0" } } part Fan role=panel { stock Poplar 160.0 160.0 16.0 lengthwise by top_sw pos x=330.0 y=0.0 z=0.0 veneer face=pos_z material=Walnut thickness=0.6 match=sunburst leaves=8 } } } ``` The full worked fixture is `examples/inlaid_tray.kdl`. ## Pitfalls and diagnostics - A bad property or an impossible section is a **named skip** on both the groove and the filler, never a silent no-op: `stringing` without `offset=`, `depth=` that reaches through the part, `width=0`, a missing `material=`, `lay=` other than `inlay`/`surface`, `pattern=` other than `plain`/`herringbone`, a strip wider than the face allows, `thickness=` on an inlay, `depth=` on a surface strip, an `edges` word that is not a neighbour of the face. - Strips must not meet their opposite strip: `2 * (offset + width)` must stay under the face's extent. - The filler sits flush with no gap, touching its groove: expected, never a clash. - Cut list: strips are ordinary strip stock, `L` the outer edge's length (the mitre is cut from it), the grain angle in the component name (`Walnut herringbone banding (+45°)`). A flush inlay does not change the host's thickness. - Do not put an inlay groove on a face that also carries `veneer`; surface banding likewise is not meant to share a face with a veneer leaf. - Ids: `Host@banding_/Strip_` and `Host@stringing_/Strip_`, `n` numbering the unrolled banding/stringing nodes of that host in document order. - Not built: curved banding, stopped bands, the pictorial motifs, the chevron figure itself. ## See also - `docs/KDL_AUTHORING.md` §7, *Banding and stringing* and *Veneer and laminate* - `docs/rfc/0141-banding-stringing-sunburst.md`, `docs/adr/0025-banding-stringing-inlay.md` - `examples/inlaid_tray.kdl` - `docs/agent-reference/nodes.md`, `docs/agent-reference/tasks.md` ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `banding` feature; node in 6 of 116 examples (12 uses). Shortest real use, from `serpentine_sideboard.kdl`: ```kdl banding face=neg_x material=Tulipwood width=band_w depth=3.0 offset=band_off ``` ### `stringing` feature; node in 12 of 116 examples (20 uses). Shortest real use, from `deco_console_table.kdl`: ```kdl stringing face=neg_y material=Ivory offset=8.0 ``` ### `lay` word-valued property; not used by any example yet. ### `pattern` word-valued property; property in 4 of 116 examples (6 uses). Shortest real use, from `william_and_mary_lowboy.kdl`: ```kdl banding face=pos_z material=Maple width=7.0 depth=3.0 offset=29.0 pattern=herringbone ``` ## Related terms - [Banding](/almanac/techniques#banding) - [Cross-banding](/almanac/techniques#cross-banding) - [Herringbone banding](/almanac/techniques#herringbone-banding) - [Stringing](/almanac/techniques#stringing) --- # Bevels & Rabbets *Stopped mouldings on any edge, straight or vertical* **Names:** `bevel` `rabbet` `moulding` `trim` ## When to use - A flat slope across an edge at a chosen angle (a bevelled shelf front, a hopper side's mitred edge): `bevel angle=`. For a 45° corner break use `chamfer`. - A step cut into an edge, including a **vertical** edge (the back rabbet on a standing carcass side, a notch at a shelf's front corner, a lipped drawer front's ends): `rabbet face= edge=`. - A profile (`ovolo`, `cove`, `ogee`, ...) that starts and ends part-way along an edge: `moulding ... stopped=#true`. - `trim` aligns a raked end to a plane fixed in **world** space (`reference=world`) or in the **nearest enclosing assembly**'s own frame (`reference=assembly`, #458). ## Syntax ```kdl grammar bevel angle= [depth=] { edges ... } // or: bevel face= edge= angle= [depth=] rabbet width= depth= { edges ... } // or: rabbet face= edge= width= depth= [stopped=#true start= run_length=] moulding section= size= [stopped=#true start= run_length= stop=square|ramp] { edges ... } ``` - `face=` is the face the cut goes into and the angle or width is measured on; `edge=` is an adjacent face (not `face`, not its opposite). Faces are `pos_x neg_x pos_y neg_y pos_z neg_z`. Use `face=`/`edge=` **or** `{ edges }`, never both. - Bevel `depth=` defaults to the whole thickness across `face`; the facet is `depth / tan(angle)` wide on `face`. - Rabbet `width` is measured on `face` from `edge`; `depth` goes into `face`. - Stops: `start=` defaults to 10 mm in; `start=0.0` reaches the end of the edge; omit `run_length=` to run out through the far end. `bevel` takes `stop=square|ramp|lambs_tongue`, `moulding` takes `square|ramp`, `rabbet` is square-ended only. ## Example ```kdl project name="Bevel and rabbet" units=mm schema_version=1 { materials { material Oak type=hardwood } component Side { stock Oak 20.0 300.0 600.0 // Back rabbet on the standing side: the vertical edge where pos_x meets pos_y. rabbet face=pos_x edge=pos_y width=18.0 depth=6.0 } component Shelf { stock Oak 400.0 250.0 20.0 // A 35 degree bevel across the front edge, the whole thickness. bevel angle=35.0 { edges top_s } // A cove that stops 50mm in from each end of the back edge. moulding section=cove size=5.0 stopped=#true start=50.0 run_length=300.0 { edges top_n } } assembly Piece { part Side instance=Side { pos x=0.0 y=0.0 z=0.0 } part Shelf instance=Shelf { pos x=40.0 y=0.0 z=0.0 } } } ``` ## Pitfalls and diagnostics - `angle` outside `[1, 90)` is `ww::build::bad_value`, a missing `angle` is a missing-property error. A facet wider than the face, a `depth` thicker than the board, `face=` without `edge=` (error) or with `{ edges }` (skip, "not both"), non-adjacent faces ("adjacent"), and `stop=` without `stopped=` are skips naming the cause: the built part simply has no such cut, so read the skipped list, not just the green tick. - `upright_*` in `{ edges }` is skipped for `bevel` and `rabbet`. Use `face=`/`edge=`. - `stop=ramp` on a `rabbet` is a skip ("square"); `stop=lambs_tongue` is for `chamfer` and `bevel` only. - A stopped `moulding` is a 32-layer staircase: it never removes more than the true section and falls under 7% short of its volume. Do not expect a smooth curve at the run's ends. - A through `rabbet face= edge=` that a dovetail socket or other join opens into is understood by the join (it is lowered as a housing on `face`); write it, rather than a groove at the edge of the face. - `trim reference=assembly` is built (#458): the nearest enclosing `instance=`-placed assembly's own frame, identity (same as `reference=world`) when there is none. A `reference=` naming neither `world` nor `assembly` is a named skip. ## See also - `docs/KDL_AUTHORING.md` section 7, "Bevel, and a rabbet on any edge" and "Moulding profiles" - `docs/rfc/0143-bevel-and-stopped-mouldings.md`, `docs/adr/0024-bevel-rabbet-stopped-mouldings.md` - RFC-0051 (rabbet on any edge), RFC-0126 (mouldings), RFC-0025 (stopped chamfers), RFC-0024 (`trim`) - Examples: `examples/bedside_cabinet.kdl`, `examples/welsh_dresser.kdl`, `examples/wall_shelf.kdl`, `examples/dining_chair.kdl`; `docs/styles/models/bevel.kdl` ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `bevel` feature; node in 6 of 116 examples (16 uses). Shortest real use, from `jacobean_joined_chest.kdl`: ```kdl bevel face=pos_y edge=pos_z angle=10.0 depth=bevel_depth ``` ### `rabbet` feature; node in 24 of 116 examples (56 uses). Shortest real use, from `tall_white_cutout_cabinet.kdl`: ```kdl rabbet face=pos_x edge=pos_y width=back_th depth=rd ``` ### `moulding` feature; node in 18 of 116 examples (36 uses); value in 2 of 116 examples (4 uses). Shortest real use, from `farmhouse_harvest_table.kdl`: ```kdl moulding section=thumbnail size=6.0 { edges top_s } ``` ### `trim` feature; node in 5 of 116 examples (7 uses); value in 1 of 116 examples (1 uses). Shortest real use, from `mid_century_credenza.kdl`: ```kdl trim reference=world normal=pos_z at=floor_z { faces pos_x } ``` ## Related terms - [Bevel](/almanac/techniques#bevel) - [Rabbet](/almanac/techniques#rabbet) --- # Components & Stock *Grain direction and marks for joining* **Names:** `component` `stock` `role` `grain` `mark` `normal` `tilt` `heading` `lengthwise` `crosswise` ## When to use Every distinct piece of timber or sheet is one `component` (a reusable template) with exactly one `stock` line, plus the `mark`s that joints attach to. Use a `part` with an inline `stock` (see `parts-and-assemblies`) for a one-off that nothing else reuses. Turned (round) stock and its `section` profile are in `round-stock-sections`. ## Syntax ```text component [role=] { [params { ... }] // defaults, overridable per instance stock [lengthwise|crosswise] // REQUIRED, exactly one mark x= y= z= normal= [tilt= heading=] // see edge-and-face-cuts, cut-paths } ``` - **`stock x y z`**: the material name, then three positional dimensions (floats or CEL), then an optional grain word. `x` is width, `y` is depth, `z` is length down the part (the part's own frame, ADR-0007). **Exactly one per component.** A `part` has either `instance=` or an inline `stock`, never both. - **Grain** (`lengthwise` default, along the longest dimension; or `crosswise`). State it where it decides a joint. A `type=sheet` material takes no grain (hard error). The grain word vocabulary and the style finish rule are a separate family; see `INDEX.md`. - **`role=`** is free text for BOM grouping (`leg`, `apron`, `rail`, `stile`, `panel`, `top`, `shelf`, `door`, `drawer_face`, ...). It has no compiler meaning, except that `role=reference` makes a part a reference part (see `parts-and-assemblies`). - **`mark`** is a named datum: a point plus a facing, attached to the part's own frame. Joints address it as `Part@Mark`. `normal` is one of `pos_x neg_x pos_y neg_y pos_z neg_z` and points **out of the material**, toward the mating part. Write coordinates in the component's own params (`x=leg_dim`, `y="apron_th / 2.0"`), never as bare constants. - **`tilt=` / `heading=`** (RFC-0022, built in M25) make a mark an *oriented reference*: the facing leans `tilt` degrees off the named face's outward direction, toward `heading` degrees counter-clockwise from that face's own `+u`. Both default to `0.0` and then the mark is exactly the named face. Use them for a mitred end, a compound-angle approach or a spoke at an azimuth. A mark that sits *set back* from a face but still faces squarely along an axis needs neither. Worked examples: `examples/wagon_wheel.kdl` (`Hub`, `Felloe`). - `params` inside a component are defaults an instance can override with child nodes on its `part` line. ## Example ```kdl project name="Leg Frame" units=mm schema_version=1 { params { leg_dim 45.0 min=30.0 max=70.0 step=1.0 label="Leg" category="Stock" leg_len 700.0 min=400.0 max=900.0 step=10.0 label="Leg length" category="Envelope" felloe_half_angle 18.0 } materials { material Ash type=hardwood } component Leg role=leg { stock Ash leg_dim leg_dim leg_len lengthwise mark MortiseFace_X x=leg_dim y="leg_dim / 2.0" z=-55.0 normal=pos_x // an oriented mark: leans off pos_x by the mitre's half angle mark Spoke_1 x=leg_dim y="leg_dim / 2.0" z=-300.0 normal=pos_x tilt=felloe_half_angle heading=0.0 } assembly Root { part Leg_A instance=Leg { by top_sw; pos x=0.0 y=0.0 z=0.0 } } } ``` ## Pitfalls and diagnostics - A component declared and never instantiated by a `part` is a `kdl-check` **warning**. It is a real bug that has happened: a Welsh dresser's centre support was declared with marks and never placed, so the shelf span was more than double what the file claimed. - A mark not on any real stock face fails **at `join` time**, not at the `mark` line. With `tilt=`/`heading=` this now covers a mitred end or compound corner that is *within* the stock. Still not built: a **mark on a curved turned surface** (only the flat axis ends, `normal=pos_z`/`neg_z`, are reachable) and a mortise cut into a curved face. Tenon into the square pommel instead (see `round-stock-sections`). A **compound mitre** joint (`miter` that leans and turns) is also not synthesised; see `joinery-frames`. - A mark's coordinates are in the component's frame *before* any `rot`; the placement is the part's `by`/`pos`/`rot`, not the mark's. - A `component` with no `stock` node is a `kdl-check` error ("has no `stock` node"). Write exactly one (KDL_AUTHORING §6); the position of the line among the features does not matter. - Mark names and `id=`s inside a `repeat` must interpolate the loop variable (`mark "Pin_{i}" ...`), or they collide (`kdl-check` enforces this). - `shape=round` takes no grain argument; naming one is rejected. ## See also - `docs/KDL_AUTHORING.md` §6 (`component`, `mark`), §7b (naming), ADR-0007 (the stock line). - `docs/rfc/0022-compound-angle-joint-frame.md` (resolved; the residual gaps above). - `parts-and-assemblies.md` (placing a component), `joinery-contract.md` (what marks are for), `round-stock-sections.md` (turned stock). ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `component` project-level node; node in 110 of 116 examples (819 uses); value in 1 of 116 examples (1 uses). Shortest real use, from `glazed_vitrine_whiplash_crest.kdl`: ```kdl component Pane role=glass { stock Glass dw 6.0 dh } ``` ### `stock` feature; node in 114 of 116 examples (910 uses). Shortest real use, from `glazed_vitrine_whiplash_crest.kdl`: ```kdl stock Glass dw 6.0 dh ``` ### `role` word-valued property; property in 114 of 116 examples (900 uses). Shortest real use, from `glazed_vitrine_whiplash_crest.kdl`: ```kdl component Pane role=glass { stock Glass dw 6.0 dh } ``` ### `grain` feature, word-valued property; property in 2 of 116 examples (3 uses). Shortest real use, from `biedermeier_pedestal_table.kdl`: ```kdl stock Cherry shape=round diameter=top_diameter z=top_thickness grain=across ``` ### `mark` feature; node in 110 of 116 examples (2193 uses). Shortest real use, from `windsor_fan_back_side_chair.kdl`: ```kdl mark Top x=7.5 y=7.5 z=0.0 normal=pos_z ``` ### `normal` word-valued property; property in 110 of 116 examples (2207 uses). Shortest real use, from `windsor_fan_back_side_chair.kdl`: ```kdl mark Top x=7.5 y=7.5 z=0.0 normal=pos_z ``` ### `tilt` property; property in 2 of 116 examples (6 uses). Shortest real use, from `primary_grid_lath_armchair.kdl`: ```kdl mark Seat x=0.0 y="lath / 2.0" z="0.0 - dm" normal=neg_x tilt=0.01 heading=0.0 ``` ### `heading` property; property in 2 of 116 examples (6 uses). Shortest real use, from `primary_grid_lath_armchair.kdl`: ```kdl mark Seat x=0.0 y="lath / 2.0" z="0.0 - dm" normal=neg_x tilt=0.01 heading=0.0 ``` ### `lengthwise` grain; value in 26 of 116 examples (84 uses). Shortest real use, from `ming_horseshoe_armchair.kdl`: ```kdl stock Huanghuali length th h lengthwise ``` ### `crosswise` grain; not used by any example yet. --- # Curved Members *A swept bend along a member's own length* **Names:** `bend` `arc` `spline` ## When to use - A member that curves along its own **length**, not its cross-section: a chair crest rail that bows in plan or in elevation, a splat that follows a lumbar curve — RFC-0040's own two motivating cases. - You want the cut list to say "follows spine," and the card to dimension the curve itself (an arc's radius, or a spline's keyframes), not the swept solid's bounding box. Not for a cross-section that varies along a *straight* length (that is `section`'s own `rect`/`interp=spline` job — [`turning-and-sections`](turning-and-sections.md)), a doubly curved surface such as a saddled seat (not built, RFC-0021), or a true 3D (non-planar) spine (not built: `plane=` is always one coordinate plane). ## Syntax ```text bend axis=z plane= [tolerance= | samples=] { arc radius= } bend axis=z plane= [tolerance= | samples=] { spline { point z= = // two or more, z running the whole part } } ``` - `axis=` — only `"z"`. A bent member's own length is always its local `z`; place it with `rot` like any other component whose length is not world `z`. - `plane=` — `"xz"` (bows local `x`, holds local `y` fixed) or `"yz"` (bows local `y`, holds local `x` fixed). The spine stays planar. - `tolerance=` or `samples=`, mutually exclusive, same meaning as the `rect`-section family's own knobs. - Exactly one of `arc` (closed-form, tangent at `z=0` only, asymmetric end to end) or `spline` (two or more `(z, offset)` control points, resampled through `ww_geom::spline::sample` — symmetric when both ends hold `offset=0`). A spline's points must run the whole part, `z=0` to `z = 0 - size.z`. - A radius (or local curvature) tighter than the cross-section's own half-depth is refused before any kernel call — it would self-intersect the sweep. **Available faces.** Only the two end `FaceFrame`s (and their standard anchors — `top_sw`, …, `top_center`, `bottom_center`, `center`) exist; the swept lateral surface is not a face. A `mark` with an end-facing `normal=` (`pos_z`/`neg_z`) resolves off the real, curved end frame; one with a lateral `normal=` is refused by name. `mortise_and_tenon` needs a `mark` with `normal=` (a standard anchor has no facing, D6) — a mark at the end face's own centre is exact whenever the curve holds `offset=0` at that end. ## Example ```kdl fragment=component bend axis=z plane=xz { spline { point z=0.0 x=0.0 point z="0.0 - 300.0" x="0.0 - 18.0" point z="0.0 - 600.0" x=0.0 } } mark CrestTenon_Start x=50.0 y=50.0 z=0.0 normal=pos_z mark CrestTenon_End x=50.0 y=50.0 z="0.0 - 600.0" normal=neg_z ``` `examples/arched_crest_side_chair.kdl` is the worked case: a steam-bent ash crest rail, mortise-and-tenoned into its two back posts' own end faces — the same "square tenon against a resolved `FaceFrame`" path a straight member's own end joinery uses, no synthesizer change. `crates/ww-build/tests/bend_joinery.rs`'s own fixture is the `arc`-bodied, tangent-at-one-end case. ## Pitfalls and diagnostics - `axis=x`/`axis=y`, a varying cross-section along the bend, a mid-curve feature or mark, a curved CNC toolpath, and turning a bend (round stock plus a curved spine) are all not built. - A spline whose points do not run the whole part (`z=0` to `z = 0 - size.z`) is refused: "a `bend`'s spline must run the whole part, from its top face (z=0) to its foot ...". - A `join` naming a lateral mark on a bent component is refused, naming the two valid end faces (`top_sw`/`bottom_sw`'s own families). - `mortise_and_tenon` against a standard anchor (`top_center`, …) is refused: "is a standard anchor, which has a point but no facing (D6)" — use a `mark` with `normal=` instead. - The built solid's real boundary can stand a millimetre or two past the exact plane the end `FaceFrame` reports, at a member's own free ends (an existing, documented margin, `crates/ww-build/tests/bend_joinery.rs`'s own header) — a mating part placed flush against that reported plane can show a small, bounded clash there; not a placement bug. ## See also `docs/KDL_AUTHORING.md` §6 ("`bend`"), RFC-0040, ADR-0016 (`arc`/`path2d` tessellation), ADR-0032 (`ww_geom::spline`), ADR-0012 (native/wasm determinism — a bend's own rotation uses `libm::atan2`/`libm::sincos`, never the inherent `f64` methods), ADR-0028 (grain axis — "follows spine"), [`turning-and-sections`](turning-and-sections.md) (the sibling cross-section-along-a-straight-length family), [`joinery-tenons`](joinery-tenons.md). ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `bend` feature; node in 1 of 116 examples (1 uses). Shortest real use, from `arched_crest_side_chair.kdl`: ```kdl bend axis=z plane=xz { // A `bend`'s spline must run the whole part from its own top face (z=0) to its // foot (z = 0 - open_w, D4's top-down local convention) — the same direction // `crates/ww-build/tests/bend.rs`'s own spline fixture uses, not the ascending // z this file's own first draft tried (kdl-check's own diagnostic caught it). spline { point z=0.0 x=0.0 point z="0.0 - (open_w / 2.0)" x="0.0 - crest_arch_rise" point z="0.0 - open_w" x=0.0 } } ``` ### `arc` node in 19 of 116 examples (65 uses). Shortest real use, from `windsor_fan_back_side_chair.kdl`: ```kdl arc center_x=rf center_y=cy radius=rf start_angle=0.0 end_angle=180.0 ``` ### `spline` node in 26 of 116 examples (110 uses); value in 28 of 116 examples (42 uses). Shortest real use, from `adirondack_plank_chair.kdl`: ```kdl spline { point x=0.0 y=60.0; point x="arm_w * 0.12" y=18.0; point x="arm_w * 0.5" y=0.0; point x="arm_w * 0.88" y=18.0; point x=arm_w y=60.0 } ``` ## Related terms - [Bent lamination](/almanac/techniques#bent-lamination) - [Steam bending](/almanac/techniques#steam-bending) --- # Cut-Path Outlines *Lines, arcs, polylines, and named paths for a pierced or shaped cut* **Names:** `cut_path` `path` `keep` `line` `arc` `polyline` `point` `kerf` ## When to use An outline cut through (or part-way into) a part, in the plane of one face: a round or curved tabletop, a curved felloe, a bootjack notch, a pierced window, a semicircular cut-out. A straight cut across a whole face is a `dado`/`groove` and a profile along an edge is `chamfer`/`roundover`/`moulding` (all in `edge-and-face-cuts`). ## Syntax ```text cut_path depth= [face=] [keep="outside"|"inside"] [tilt= heading=] [kerf=] { line from_x= from_y= to_x= to_y= arc center_x= center_y= radius= start_angle= end_angle= // degrees polyline { point x= y=; point x= y=; ... } // at least 2 points } cut_path depth= path= // a named single segment path arc center_x= center_y= radius= start_angle= end_angle= // declared in the component ``` - `depth=0.0` is through; a positive depth stops short of the far face. A depth that reaches the far face is refused (write `0.0`). `face=` defaults to the broad face (as for `dado`). - **`keep="outside"` (default):** the closed region is **removed** (a pierced cut-out). **`keep="inside"`:** everything *outside* the region is removed, so the path becomes the part's new footprint through its full thickness (a round top, a felloe's curved edge). - **Segments.** `line` never closes alone (reported as a skip if used by itself). `arc` is in degrees (the `sin`/`cos` convention); a 360 degree sweep is a full circle, less closes along the chord. `polyline` has the closing edge from last point to first **implied**: never repeat the first point. - **Compound path:** several segments in one inline `cut_path { ... }` chained in document order, each end meeting the next start (`arc`, `line`, `arc` is an annular wedge, the wagon-wheel felloe). The closing edge back to the start is implied; do not write a fourth. - **Named path:** `path props...` declares exactly **one** segment once; `cut_path path=` uses it. Naming a `path=` and also giving inline segments is refused. Only an inline `cut_path` chains several. - `tilt=`/`heading=` lean the cut (a raked wall); `kerf=` is inert on a closed path (kept for toolpath compensation). - `id=`/`label=` are accepted syntax (both parse as strings) but nothing in the compiler reads them back: no diagnostic, no cross-reference, no cut-list or drawing output resolves against them (issue #551, same gap as `dado`/`groove`, issue #549). They are for the author's own bookkeeping only, not yet a real reference mechanism another node can resolve against. ## Example ```kdl project name="Cut Paths" units=mm schema_version=1 { materials { material Oak type=hardwood } component Panel role=panel { stock Oak 200.0 150.0 20.0 // a pierced rectangular window (closing edge implied) cut_path depth=0.0 { polyline { point x=60.0 y=50.0 point x=140.0 y=50.0 point x=140.0 y=100.0 point x=60.0 y=100.0 } } // a semicircular notch, named once and referenced path Notch arc center_x=100.0 center_y=0.0 radius=30.0 start_angle=0.0 end_angle=180.0 cut_path depth=0.0 path=Notch } component Felloe role=felloe { stock Oak 200.0 150.0 20.0 // a compound annular wedge: arc, line, arc, chained end to start cut_path depth=0.0 keep="inside" { arc center_x=100.0 center_y=75.0 radius=60.0 start_angle=0.0 end_angle=90.0 line from_x=100.0 from_y=135.0 to_x=100.0 to_y=115.0 arc center_x=100.0 center_y=75.0 radius=40.0 start_angle=90.0 end_angle=0.0 } } assembly Root { part P instance=Panel { by top_sw; pos x=0.0 y=0.0 z=0.0 } part F instance=Felloe { by top_sw; pos x=300.0 y=0.0 z=0.0 } } } ``` ## Pitfalls and diagnostics - **`spline` and `circle` segments are built** (ADR-0032): see `paths-and-splines.md`. - Anything the build cannot read (an unknown `keep=` word, a missing `path=` target, a path with no geometry, a degenerate or self-meeting outline) is a **skip** in the scene JSON `"skipped"` list and a `ww check` warning, not a build error. Read the skip text. - `keep="inside"` on the wrong side of the stock leaves nothing: size the blank over-length and let the path trim it. - Coordinates are part-local in the face's own plane (`from_x`, `center_x` ...), not the part's `x`/`y`/`z`. ## See also - `docs/KDL_AUTHORING.md` §7 (`cut_path`); `docs/adr/0016-path-geometry.md`; `docs/rfc/0120-curved-path-geometry.md` (levels 1 to 2 and compound). - `examples/wagon_wheel.kdl` (compound felloe), `examples/three_legged_stool.kdl`, `examples/bed.kdl`, `examples/six_board_chest.kdl`, `examples/loom_shuttle.kdl`. - `edge-and-face-cuts.md`, `round-stock-sections.md` (turned outlines). ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `cut_path` feature; node in 50 of 116 examples (239 uses). Shortest real use, from `ming_waisted_corner_leg_table.kdl`: ```kdl cut_path depth=0.0 face=neg_y path=Hoof ``` ### `path` feature, word-valued property; node in 9 of 116 examples (15 uses); property in 9 of 116 examples (19 uses). Shortest real use, from `jacobean_joint_stool.kdl`: ```kdl path ArchLong { line from_x="span_w * 0.10" from_y=-5.0 to_x="span_w * 0.10" to_y=0.0 spline { point x="span_w * 0.10" y=0.0 point x="span_w * 0.30" y="arch_rise * 0.80" point x="span_w * 0.50" y=arch_rise point x="span_w * 0.70" y="arch_rise * 0.80" point x="span_w * 0.90" y=0.0 } line from_x="span_w * 0.90" from_y=0.0 to_x="span_w * 0.90" to_y=-5.0 } ``` ### `keep` word-valued property; property in 27 of 116 examples (61 uses). Shortest real use, from `postmodern_flat_silhouette_chair.kdl`: ```kdl cut_path depth=0.0 keep="inside" path=RearOutline ``` ### `line` node in 33 of 116 examples (256 uses). Shortest real use, from `postmodern_flat_silhouette_chair.kdl`: ```kdl line from_x=w from_y=0.0 to_x=w to_y=seat_h ``` ### `arc` node in 19 of 116 examples (65 uses). Shortest real use, from `windsor_fan_back_side_chair.kdl`: ```kdl arc center_x=rf center_y=cy radius=rf start_angle=0.0 end_angle=180.0 ``` ### `polyline` node in 21 of 116 examples (91 uses). Shortest real use, from `incised_walnut_washstand.kdl`: ```kdl polyline { point x="9.0 + (0.0000)" y="9.0 + (1.0000)"; point x="face_w - 9.0 + (0.0000)" y="9.0 + (1.0000)"; point x="face_w - 9.0 - (0.0000)" y="9.0 - (1.0000)"; point x="9.0 - (0.0000)" y="9.0 - (1.0000)" } ``` ### `point` node in 40 of 116 examples (1487 uses). Shortest real use, from `country_rush_seat_ladder_chair.kdl`: ```kdl point x=crx y=d ``` ### `kerf` property; property in 6 of 116 examples (8 uses). Shortest real use, from `wedged_post_and_rung_stool.kdl`: ```kdl wedge kerf=3.0 depth=18.0 flare=2.0 flare_depth=10.0 ``` ## Related terms - [Bootjack](/almanac/techniques#bootjack) - [Cut path](/almanac/techniques#cut-path) --- # Edge & Face Cuts *Dado, groove, rabbet, chamfer, roundover, moulding, taper, hole, and trim* **Names:** `dado` `groove` `rabbet` `chamfer` `roundover` `moulding` `taper` `hole` `trim` `edges` `faces` `face` `offset` `stopped` `start` `run_length` `stop` `size` `width` `depth` `target_dim` `start_z` `end_z` `target` `reference` `at` `length` `diameter` `ovolo` `cove` `ogee` `thumbnail` `half_round` `cockbead` `ramp` `lambs_tongue` `top_perimeter` `bottom_perimeter` `front_perimeter` ## When to use Material removal on one part, written inside its `component` (or an inline `part`) in the part's own frame. Every feature on a part lowers to a single cut: the compiler unions every cutter and cuts once, so **write order does not matter**. Use this file for housings, panel grooves, edge profiles, tapered legs, holes and slots, and levelling a raked leg to the floor. For an outline cut through the thickness use `cut-paths`; for joints between two parts use `joinery-contract`. A veneer face is `veneer-and-laminate`. ## Syntax Enum lists (`edges`, `faces`) are **child nodes** taking one or more arguments; KDL v2 has no array literals. | Node | Required | Notes | | --- | --- | --- | | `dado` | `width` `depth` `offset` | Runs **across** the grain. | | `groove` | `width` `depth` `offset` | Runs **with** the grain. A face the grain runs into (end grain) skips the cut at that face. | | `rabbet` | `width` `depth` + `{ edges ... }` or `face=` `edge=` | `face=`/`edge=` reaches any edge, vertical ones included. Stoppable, square-ended only. [`bevel-and-rabbet`](bevel-and-rabbet.md). | | `bevel` | `angle` + `{ edges ... }` or `face=` `edge=` | Built (RFC-0143). [`bevel-and-rabbet`](bevel-and-rabbet.md). | | `chamfer` | `size` + `{ edges ... }` | Stoppable (below). | | `roundover` | `size` + `{ edges ... }` | Stoppable; `stop=lambs_tongue` refused. | | `moulding` | `section=` `size=` + `{ edges ... }` | Stoppable (`stopped=` `start=` `run_length=` `stop=square\|ramp`). `section=` one of `ovolo cove ogee thumbnail half_round cockbead`; any other word is refused. | | `taper` | `start_z` `end_z` `target_dim` + `{ faces ... }` | Full section at `start_z`, `target_dim` at `end_z`, on the named faces. | | `hole` | `diameter` `depth` | `depth=0.0` is through. `x=` `y=` `z=` and `normal=` (the face it is drilled into) place it. `length=` + `axis=(x\|y)` make a slot. | | `trim` | `reference=world\|assembly` `normal=` `at=` + `{ faces ... }` | The one feature resolved in **world** or assembly space, after placement. | - **Edge selectors** (`edges` children): `top_perimeter`, `bottom_perimeter`, `front_perimeter`, `all`; named edges `top_n top_s top_e top_w` and `bottom_n bottom_s bottom_e bottom_w`; the four vertical arrises `upright_ne upright_nw upright_se upright_sw`; the four edges of the front face `front_top front_bottom front_e front_w`. - **Face words** (`face=`, `faces`): `pos_x neg_x pos_y neg_y pos_z neg_z`, part-local. - **Placing a `dado`/`groove` (ADR-0011).** `face=` is the face it is cut into (default: the broad face; **required** on a square section). `offset=` is the distance of the cut's *low edge* from the low end of its axis (the bottom face for z): always positive, "up from the bottom" or "in from the front or left". `stopped=#true` with `start=` (default 10 mm in) and `run_length=` (default: out through the far end) stops it. Which way it runs is not a property: it comes from the grain. The removed names `offset_bottom` and `offset_x` do not exist; the build reports them by name. - **`target=`** on a `dado`/`groove` (a bare word, e.g. `target=panel`) names the part the cut captures. It is documentation for the reader and the joint rules (a `stile_and_rail` frame's grooves, `joinery-frames`), not geometry: it moves nothing. - **`id=`/`label=`** on a `dado`/`groove` are accepted syntax (both parse as strings) but nothing in the compiler reads them back: no diagnostic, no cross-reference, no cut-list or drawing output resolves against them (issue #549). They are for the author's own bookkeeping or future-proofing only — a name you can grep your own source for — not yet a real reference mechanism another node can resolve against. - **Stopped `chamfer`/`roundover` (RFC-0025):** `stopped=#true start= run_length= stop=square|ramp` (`square` default). `chamfer` alone also takes `stop=lambs_tongue`. A chamfer or roundover on a vertical arris is refused if the run overlaps a `taper` on either of its faces. - **`moulding size=`** is the profile's overall projection, measured as `chamfer`/`roundover`'s `size`. Every section is a stepped polygon (16 chords per quarter circle). `half_round` and `cockbead` are *applied* mouldings: author them as their own narrow `component` (a strip whose stock is the bead's section, profile cut on the faces that show) and `join` it on; a cut on the host can never leave material standing proud. - **Most profiles do not follow a `cut_path` outline:** `rabbet`/`bevel`/`moulding` and any edge selector naming individual compass edges ease the blank's straight edges only. On a part with a through `cut_path` that changes the outline, a perimeter selector (`front_perimeter`, `all`) leaves the sawn edges plain and reports "shaped only along the blank's own edges" in the skips. A named edge (`top_n`) the saw has taken away entirely is skipped as "sawn away" (a profile there would cut air); a named edge the saw only shortened is still shaped along what is left. A true profile along a curved run is not built (RFC-0126 addendum). - **`chamfer`'s and `roundover`'s own `top_perimeter`/`bottom_perimeter` are the exception** (RFC-0021, issue #58 Part A): on a part whose outline came from a through, `keep="inside"` `cut_path`, these two sweep the real outline instead — arcs and spline-flattened runs included — rather than the box's four compass edges, which such a part does not have at all (`three_legged_stool.kdl`'s rounded-triangle `Seat`). `stopped=` is refused there (no single straight edge's own length to measure a run against). - **How big a profile may be:** a `chamfer`, `roundover`, `moulding` or `rabbet` is cut into the corner of two faces and must stop **strictly short** of the opposite face of each, otherwise it is skipped ("would remove the whole section", naming the short dimension). So `size` is *not* "the strip's thickness": a half-round strip needs thickness **greater than** `size` (and width greater than `size`); `size = width / 2` with both long edges moulded is the true semicircle. A strip 6 thick takes `size` up to just under 6, not 6. - **Stop a rabbet (or any stopped cut) at a height**: there is no "stop at extent" keyword. Measure on the edge's run axis from its **low end**: `stopped=#true start=0.0 run_length=` stops it `` up; for a run measured down from the high end, `start=` and leave `run_length` out. Omitting `start=` begins 10mm in. - **`hole`** with `length=`/`axis=` (RFC-0030) is a slot: `length` is the straight run between the two semicircular caps and `axis` (`x` or `y`) the part-local axis it runs along. Omit `length` (or `0.0`) for a round hole; `axis=` does nothing then, so a `repeat` can pass both and ternary only `length`. - **`id=`/`label=`** on a `hole` are accepted syntax (both parse as strings) but nothing in the compiler reads them back: no diagnostic, no cross-reference, no cut-list or drawing output resolves against them (issue #551, same gap as `dado`/`groove`, issue #549). They are for the author's own bookkeeping only — a name you can grep your own source for — not yet a real reference mechanism another node can resolve against. - **`trim` (RFC-0024; `reference=assembly`, #458)**: `normal=` is a direction in `reference=`'s own frame naming the side of the plane that is **kept**; `at=` is where the plane sits along that axis (a CEL expression, usually derived, `"0.0 - leg_drop"`). `faces` names the part's **own** face the cut is written from. Pick a *side* face nearly perpendicular to the plane, not the end the trim conceptually shortens: a face leaning past 60 degrees (`MAX_LEAN`) is refused. `reference=` is `world` (the document's own origin) or `assembly` (the nearest enclosing `instance=`-placed assembly's own frame; identity, same as `world`, when the part has no such ancestor). ## Example ```kdl fragment=component // housing across the grain, a panel groove in an edge, a stopped groove dado face=pos_y width=19.0 depth=8.0 offset=40.0 groove face=pos_y width=6.0 depth=10.0 offset="(100.0 - 6.0) / 2.0" groove face=pos_y width=10.0 depth=6.0 offset=40.0 stopped=#true start=50.0 run_length=400.0 // edge treatments chamfer size=3.0 { edges top_perimeter } roundover size=4.0 stopped=#true start=60.0 run_length=400.0 stop=ramp { edges upright_se upright_sw } moulding section=ogee size=4.0 { edges bottom_perimeter } rabbet width=10.0 depth=6.0 { edges front_top } ``` ```kdl fragment=component taper start_z=-200.0 end_z=-600.0 target_dim=60.0 { faces pos_x pos_y } // a row of holes where only the outer two are slots repeat i count=4 { hole id="Peg_{i}" diameter=10.0 depth=0.0 x="20.0 + (i * 20.0)" y=40.0 z=0.0 length="(i == 0.0 || i == 3.0) ? 16.0 : 0.0" axis=x } ``` ## Pitfalls and diagnostics - **Not built features are skipped, never silently ignored.** `lamination`, `pocket`, `spline` and the legacy component-level `mortise`, `tenon`, `dovetail`, `box_joint` (etc.) appear as a `skipped` entry in the scene JSON and as `ww check`'s warning "`lamination` not built: ...". Nothing is cut. - `bevel`, `rabbet face= edge=` and stopped `moulding` are built; see [`bevel-and-rabbet`](bevel-and-rabbet.md). `trim reference=assembly` is built (#458); only a `reference=` naming neither `world` nor `assembly` is refused. - `stop=lambs_tongue` on a `roundover` is refused, named. A `section=` outside the six words is refused with the reason. - A feature that never reaches the part (a `trim` whose face never crosses the plane) is a skip in the build report, not a silent no-op. - Padding stock only to give `trim` something to cut is real waste (`ww::lower::wasteful_stock`). Size the stock so the **shallowest** corner lands on the plane. - A `groove` or `dado` cut into a face the grain runs into (end grain) is skipped. - `offset` is always positive and measured from the low end; do not use negative z here. - Features in a `repeat` body need `id="Name_{i}"` wherever an id or mark would collide. ## See also - `docs/KDL_AUTHORING.md` §7; ADR-0011 (dado/groove placement); RFC-0024 (`trim`), RFC-0025 (stopped profiles), RFC-0030 (slots), RFC-0126 (mouldings). - `examples/three_legged_stool.kdl` (`trim`), `examples/desk.kdl` (dados), `examples/dining_table.kdl`. - `cut-paths.md`, `veneer-and-laminate.md`, `joinery-contract.md`, `components-and-stock.md`. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `dado` feature; node in 23 of 116 examples (63 uses). Shortest real use, from `tall_white_cutout_cabinet.kdl`: ```kdl dado face=pos_x width=shelf_th depth=dd offset=plinth ``` ### `groove` feature; node in 40 of 116 examples (145 uses). Shortest real use, from `kumiko_andon_lantern.kdl`: ```kdl groove face=pos_z width=gw depth=gd offset="(post_s - gw) / 2.0" ``` ### `rabbet` feature; node in 24 of 116 examples (56 uses). Shortest real use, from `tall_white_cutout_cabinet.kdl`: ```kdl rabbet face=pos_x edge=pos_y width=back_th depth=rd ``` ### `chamfer` feature; node in 30 of 116 examples (56 uses). Shortest real use, from `adirondack_plank_chair.kdl`: ```kdl chamfer size=3.0 { edges top_perimeter } ``` ### `roundover` feature; node in 42 of 116 examples (70 uses). Shortest real use, from `adirondack_plank_chair.kdl`: ```kdl roundover size=3.0 { edges top_perimeter } ``` ### `moulding` feature; node in 18 of 116 examples (36 uses); value in 2 of 116 examples (4 uses). Shortest real use, from `farmhouse_harvest_table.kdl`: ```kdl moulding section=thumbnail size=6.0 { edges top_s } ``` ### `taper` feature; node in 12 of 116 examples (27 uses); property in 4 of 116 examples (4 uses). Shortest real use, from `deco_side_chair.kdl`: ```kdl taper start_z=0.0 end_z=seat_z target_dim=leg_dim { faces pos_y } ``` ### `hole` feature; node in 23 of 116 examples (29 uses). Shortest real use, from `pennsylvania_schrank.kdl`: ```kdl hole ``` ### `trim` feature; node in 5 of 116 examples (7 uses); value in 1 of 116 examples (1 uses). Shortest real use, from `mid_century_credenza.kdl`: ```kdl trim reference=world normal=pos_z at=floor_z { faces pos_x } ``` ### `edges` word-valued property; node in 71 of 116 examples (189 uses). Shortest real use, from `dining_chair.kdl`: ```kdl edges top_n ``` ### `faces` word-valued property; node in 15 of 116 examples (34 uses). Shortest real use, from `deco_side_chair.kdl`: ```kdl faces neg_y ``` ### `face` property; node in 1 of 116 examples (1 uses); property in 77 of 116 examples (483 uses). Shortest real use, from `blanket_chest.kdl`: ```kdl face "'neg_y'" ``` ### `offset` property; property in 57 of 116 examples (240 uses). Shortest real use, from `deco_console_table.kdl`: ```kdl stringing face=neg_y material=Ivory offset=8.0 ``` ### `stopped` property; node in 7 of 116 examples (7 uses); property in 40 of 116 examples (135 uses). Shortest real use, from `biedermeier_chest_of_drawers.kdl`: ```kdl stopped #true ``` ### `start` property; property in 40 of 116 examples (136 uses). Shortest real use, from `blanket_chest.kdl`: ```kdl avoid start="carcass_height - till_lid_th - till_height" width=till_stock_th ``` ### `run_length` property; property in 36 of 116 examples (123 uses). Shortest real use, from `tansu_kiri_chest.kdl`: ```kdl groove face=neg_z width=door_groove_w depth=8.0 offset=track_y stopped=#true start=0.0 run_length=left_w ``` ### `stop` word-valued property; property in 5 of 116 examples (15 uses). Shortest real use, from `ebonised_incised_cabinet.kdl`: ```kdl chamfer size=chamfer_sz stopped=#true start=14.0 run_length="post_h - 28.0" stop=lambs_tongue { edges upright_sw } ``` ### `size` property; node in 1 of 116 examples (3 uses); property in 73 of 116 examples (198 uses). Shortest real use, from `bookcase.kdl`: ```kdl size domino_size ``` ### `width` property; node in 46 of 116 examples (66 uses); property in 56 of 116 examples (292 uses); value in 22 of 116 examples (38 uses). Shortest real use, from `appalachian_post_and_rung_bench.kdl`: ```kdl width 20.0 ``` ### `depth` property; node in 59 of 116 examples (122 uses); property in 95 of 116 examples (571 uses); value in 33 of 116 examples (74 uses). Shortest real use, from `glazed_vitrine_whiplash_crest.kdl`: ```kdl depth 8.0 ``` ### `target_dim` property; property in 12 of 116 examples (27 uses). Shortest real use, from `deco_side_chair.kdl`: ```kdl taper start_z=0.0 end_z=seat_z target_dim=leg_dim { faces pos_y } ``` ### `start_z` property; property in 12 of 116 examples (27 uses). Shortest real use, from `deco_side_chair.kdl`: ```kdl taper start_z=0.0 end_z=seat_z target_dim=leg_dim { faces pos_y } ``` ### `end_z` property; property in 12 of 116 examples (27 uses). Shortest real use, from `deco_side_chair.kdl`: ```kdl taper start_z=0.0 end_z=seat_z target_dim=leg_dim { faces pos_y } ``` ### `target` word-valued property; property in 33 of 116 examples (95 uses). Shortest real use, from `ebonised_incised_cabinet.kdl`: ```kdl groove target=panel face=neg_z width=panel_th depth=groove_d offset=panel_off ``` ### `reference` word-valued property; property in 5 of 116 examples (7 uses); value in 3 of 116 examples (3 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl part Fridge role=reference label="Refrigerator" { stock Appliance (in)36 (in)30 (in)68 by top_sw pos x=(in)42 y=0.0 z=0.0 } ``` ### `at` property; node in 47 of 116 examples (1024 uses); property in 10 of 116 examples (18 uses). Shortest real use, from `windsor_round_stool.kdl`: ```kdl at z=-4.0 { circle r=rf } ``` ### `length` property; node in 59 of 116 examples (154 uses); property in 50 of 116 examples (234 uses); value in 46 of 116 examples (203 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl length 26.0 ``` ### `diameter` property; node in 50 of 116 examples (94 uses); property in 65 of 116 examples (129 uses). Shortest real use, from `appalachian_post_and_rung_bench.kdl`: ```kdl diameter 9.0 ``` ### `ovolo` value in 2 of 116 examples (5 uses). Shortest real use, from `jacobean_joined_chest.kdl`: ```kdl moulding section=ovolo size=rail_ovolo { edges front_top } ``` ### `cove` value in 3 of 116 examples (3 uses). Shortest real use, from `pierced_splat_side_chair.kdl`: ```kdl moulding section=cove size=5.0 { edges upright_sw upright_nw upright_se } ``` ### `ogee` value in 9 of 116 examples (14 uses). Shortest real use, from `blanket_chest.kdl`: ```kdl moulding section=ogee size=4.0 { edges top_perimeter } ``` ### `thumbnail` value in 6 of 116 examples (9 uses). Shortest real use, from `farmhouse_harvest_table.kdl`: ```kdl moulding section=thumbnail size=6.0 { edges top_s } ``` ### `half_round` value in 2 of 116 examples (4 uses). Shortest real use, from `william_and_mary_chest_of_drawers.kdl`: ```kdl moulding section=half_round size=bead_size { edges front_e front_w } ``` ### `cockbead` value in 1 of 116 examples (1 uses). Shortest real use, from `federal_chest_of_drawers.kdl`: ```kdl moulding section=cockbead size=3.0 { edges front_perimeter } ``` ### `ramp` not used by any example yet. ### `lambs_tongue` value in 5 of 116 examples (15 uses). Shortest real use, from `ebonised_incised_cabinet.kdl`: ```kdl chamfer size=chamfer_sz stopped=#true start=14.0 run_length="post_h - 28.0" stop=lambs_tongue { edges upright_sw } ``` ### `top_perimeter` value in 49 of 116 examples (70 uses). Shortest real use, from `adirondack_plank_chair.kdl`: ```kdl edges top_perimeter ``` ### `bottom_perimeter` value in 19 of 116 examples (21 uses). Shortest real use, from `flat_pack_scale_test_table.kdl`: ```kdl edges bottom_perimeter ``` ### `front_perimeter` value in 11 of 116 examples (11 uses). Shortest real use, from `bedside_cabinet.kdl`: ```kdl edges front_perimeter ``` ## Related terms - [Bored hole](/almanac/techniques#bored-hole) - [Chamfer](/almanac/techniques#chamfer) - [Cockbead](/almanac/techniques#cockbead) - [Cove moulding](/almanac/techniques#cove-moulding) - [Dado](/almanac/techniques#dado) - [Groove](/almanac/techniques#groove) - [Ogee](/almanac/techniques#ogee) - [Ogee bracket foot](/almanac/techniques#ogee-bracket-foot) - [Ovolo](/almanac/techniques#ovolo) - [Pocket-hole joint](/almanac/techniques#pocket-hole) - [Rabbet](/almanac/techniques#rabbet) - [Roundover](/almanac/techniques#roundover) - [Stopped chamfer](/almanac/techniques#stopped-chamfer) - [Taper](/almanac/techniques#taper) - [Thumbnail edge](/almanac/techniques#thumbnail-edge) - [Tongue and groove](/almanac/techniques#tongue-and-groove) --- # Grain Axis *Pinning a part's grain direction to a named axis* **Names:** `stock` `along_x` `along_y` `along_z` ## When to use Use `along_x`, `along_y` or `along_z` on a rectangular `stock` line when you know which way the grain runs and the board's proportions can change. `lengthwise` and `crosswise` are re-derived from the board's *current* dimensions at every parameter value, so a parametric panel whose depth and height can cross over silently turns its grain 90 degrees, and every `dado` and `groove` on it turns with it. A drawer side or a chest end is the usual case: the grain runs front to back (or along the end's length) whether the part is wider than tall or not. Pin it and the grain stays put. Leave `lengthwise` (the default) on members and fixed-proportion parts. Sheet goods (`type=sheet`) have no grain; naming one is still an error. ## Syntax `stock x y z [grain]`, where `grain` is one of: | Word | Meaning | | --- | --- | | `lengthwise` (default) | Along the unique longest dimension | | `crosswise` | Across the shorter of the broad face's two axes | | `along_x` | Along the part-local x axis, at every parameter value | | `along_y` | Along the part-local y axis | | `along_z` | Along the part-local z axis (the thickness axis, when the board is flat in z) | The axes are the part's own D4 frame, before placement; a rotation or mirror in `pos` does not change which local axis is pinned. It is a bare word in the fifth positional slot, not a CEL expression and not a named property (`grain=y` is rejected, ADR-0007 rule 4). Effects of a pinned axis: - ADR-0011 rule 2 reads it directly: a `groove` runs along it and a `dado` across it. A cut into a face whose normal is the pinned axis is end grain and is skipped, with that reason shown. - The cut list and part cards put the pinned axis first: its length is L (runs with the grain), even when another dimension is larger. The grain line reads `along_y`. - There is no tie error: a board with two equal long dimensions is fine once pinned. ## Example ```kdl // A drawer side 278 mm deep and 312 mm tall: taller than it is deep, so the default // `lengthwise` would run the grain up the side and turn the bottom groove vertical. // Pinned along_y, the grain and the groove run front to back at every setting. project name="Pinned Grain Side" units=mm schema_version=1 { params { side_d 278.0 min=200.0 max=400.0 step=1.0 label="Side Depth" category="Envelope" side_h 312.0 min=100.0 max=400.0 step=1.0 label="Side Height" category="Envelope" } materials { material WhiteOak type=hardwood } assembly Root { part Side material=WhiteOak role=drawer_side { stock WhiteOak 15.0 side_d side_h along_y groove width=6.0 depth=6.0 offset=10.0 by top_sw pos x=0.0 y=0.0 z=0.0 } } } ``` ## Pitfalls and diagnostics - **It is one axis for the whole board, not grain per face.** A quadrilinear post (four mitred quartersawn boards round a core) still cannot say that each face shows the same figure; `docs/styles/models/quadrilinear-leg.kdl` stays approximate for that reason (per-face grain is not in the language; RFC-0113, grain on turned stock, is still open). - **Pinning the thickness axis makes the broad faces end grain** (a butcher-block end): every `dado`/`groove` on those faces is skipped as end grain, by design. - **Do not pin out of habit.** A leg or rail that never changes proportion gains nothing; the default already reads its longest dimension. Pin parametric panels whose depth and height ranges overlap. - **Round stock** (`stock shape=round ...`) takes no grain word at all: its grain is derived, along the turned axis. The pinned words are for rectangular stock. - `ww-expand` rejects any other word: ``grain is `lengthwise`, `crosswise`, `along_x`, `along_y` or `along_z` ``. A grain word of any kind on a `type=sheet` material is the error "`` is a sheet material and has no grain". ## See also - `docs/rfc/0072-grain-pinned-to-an-axis.md`, `docs/adr/0028-grain-pinned-to-an-axis.md` - `docs/adr/0011-dado-and-groove-placement.md` (rule 2), `docs/adr/0007-stock-line.md` - `docs/KDL_AUTHORING.md` section 10, house rule 4 - `examples/bedside_cabinet.kdl`, `examples/blanket_chest.kdl`, `examples/six_board_chest.kdl`, `examples/biedermeier_box.kdl` ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `stock` feature; node in 114 of 116 examples (910 uses). Shortest real use, from `glazed_vitrine_whiplash_crest.kdl`: ```kdl stock Glass dw 6.0 dh ``` ### `along_x` grain; value in 35 of 116 examples (133 uses). Shortest real use, from `kumiko_andon_lantern.kdl`: ```kdl stock Hinoki wo pan_th prh along_x ``` ### `along_y` grain; value in 25 of 116 examples (34 uses). Shortest real use, from `shaker_tall_chest_of_drawers.kdl`: ```kdl stock Poplar box_th box_d h along_y ``` ### `along_z` grain; value in 27 of 116 examples (44 uses). Shortest real use, from `kumiko_andon_lantern.kdl`: ```kdl stock Hinoki sw pan_th ph along_z ``` --- # Hardware Mounting *Placement, profile, recess cuts, and display-proxy geometry for catalogue hardware* **Names:** `hardware` `hardware_mount` `hardware_use` `distribute` ## When to use Use it when a purchased item has a position on the wood, or asks something of it: a knob or pull that is screwed through a door front, a hinge whose leaf is let into an edge, a bail handle you want to see in the viewer. If the item only needs counting (a box of screws, a slide, a hank of rush) use `hardware_use` and stop; nothing here is needed. Keywords: knob, pull, handle, hinge, leaf, recess, mortise, bore, screw hole, drawer pull, bail, proxy, display, `hardware_mount`, one-sided join. ## Syntax A catalogue item in `materials`. Only the numbers the cut or the picture needs: ```kdl grammar hardware supplier="…" part_no="…" price= \ screw_diameter= screw_length= \ // a `hole` takes its size from these leaf_length= leaf_width= leaf_thickness= \ // a recess takes its footprint from these shape=round|turned|bar|bail|custom|leaf|span color="#rrggbb" \ // optional display proxy diameter= projection= length= width= ``` A mount, and the `join` that places it. `female=` may be left off for a `hardware_mount` joint, and only for that kind: ```kdl grammar joinery hardware_mount name= { hardware [qty=] // required; qty is per join, for the schedule hole [diameter=] [depth=] [dx= dy= dz=] [normal=pos_x…neg_z] mortise_depth [mortise_length ] [mortise_width ] axis x|y|z // part-local axis the leaf's long side runs along distribute [from=] [to=] [count=] [setback=] // N items along the axis (ADR-0078) } join joint= male=Part@Mark [female=Part@Mark] ``` | Row | Does | Defaults | | --- | --- | --- | | `hole` | bores the male part's face at the mark | `diameter` from `screw_diameter`, `depth` from `screw_length`; `depth=0.0` is through | | `mortise_depth` | recesses a leaf on every end the join names | footprint from `leaf_length` by `leaf_width`; the long side runs along `axis` (the hinge axis) when the joinery names one, else along the face's longer in-face axis | | neither | counts the item and cuts nothing | the behaviour of every design written before ADR-0030 | `distribute` (ADR-0078) spreads N items along the mark's long axis instead of one at the mark: `from`/`to` are the zone the items must lie within, signed mm from the mark's point along the male face's axis (default: the whole run of the male face, so a bare `distribute` hangs a door along its full edge). The first and last item's near end sits `setback` inside the zone's ends (the catalogue item's `setback=`, or the row's); N is `ceil(D / max_pitch) + 1` for `D` the centre-to-centre run (the item's `max_pitch=`), or the row's `count=`; the items are evenly spaced. One join places every item on every end it names, each recess, `hole` and proxy included, and the schedule counts N. A zone too short for one item and its setbacks is `hardware-distribute`; so is a missing `setback` or `max_pitch` (never invented). A zone that keeps hinges clear of a till divider is just a `from`/`to` pair. Proxy shapes and the numbers each needs: `round`/`turned` need `diameter`, `projection`; `bar`/`bail` need `length`, `diameter`, `projection`; `custom` needs `length`, `width`, `projection`; `leaf` needs `leaf_length`, `leaf_width`, `leaf_thickness`; `span` needs only `diameter` (RFC-0145): a bar drawn from the male mark to the female mark, so a `join` with `shape=span` must name both ends (`hardware-span` otherwise), and its marks are apart by design (the join gap check skips it). A proxy is a display solid only: it is in no cut list, stock list, drawing or clash check, so it never clashes with the wood it is mounted on. An item with no `shape=` draws nothing. ## Example ```kdl project name="Cabinet door" units=mm schema_version=1 { materials { material Oak type=hardwood hardware Knob shape=round diameter=30.0 projection=25.0 \ screw_diameter=4.0 screw_length=12.0 color="#b08d57" \ supplier="Horton Brasses" part_no="K-30" price=4.0 hardware Hinge shape=leaf leaf_length=60.0 leaf_width=18.0 leaf_thickness=2.0 price=3.5 } component Door role=door { stock Oak 400.0 300.0 20.0 mark PullPoint x=200.0 y=150.0 z=0.0 normal=pos_z mark HingeEdge x=0.0 y=150.0 z=-10.0 normal=neg_x } component Post role=post { stock Oak 40.0 300.0 20.0 mark HingePost x=40.0 y=150.0 z=-10.0 normal=pos_x } joinery hardware_mount name=KnobMount { hardware Knob; hole } joinery hardware_mount name=HingeMount { hardware Hinge qty=2; mortise_depth 2.0 } assembly Root { part D instance=Door part P instance=Post { pos x=-40.0 } join joint=KnobMount male=D@PullPoint join joint=HingeMount male=D@HingeEdge female=P@HingePost } } ``` The knob is bored ⌀4 by 12 into the door's face and drawn as a ⌀30 by 25 cylinder standing on it. The hinge is let 2 mm into the door's edge and the post's edge, and a leaf plate is drawn on each. The hardware schedule says one knob and two hinges. ## Pitfalls and diagnostics - **A recess is a fit you size.** There is no clearance allowance. `recess-fit` (error) means the leaf is larger than the face it is let into; shrink `mortise_length`/`mortise_width` or choose a bigger edge. - **`remaining-wall`** (error): a blind bore or recess as deep as the part is thick. Write `depth=0.0` for a through bore. - **`hardware-size`** (error): a `hole` has no diameter or depth and the catalogue item has no `screw_diameter`/`screw_length`; or a `mortise_depth` has no leaf size. - **`hardware-face`** (error): the mark is a standard anchor, which has a point but no facing. Use a declared `mark … normal=`, or give the `hole` a `normal=`. - **`hardware-fit`** (error): a bore overruns the face. Move the mark or shrink the bore. - **`hardware-shape`** (warning): `shape=` is not one of the words above, or its numbers are missing or not positive. No proxy is drawn; the mount itself is unaffected. - **A one-sided `join` on any other joint is an error** (`ww::expand::one_sided_join`). - A proxy is an envelope, not a model of the product: a knob is a cylinder, a bail a box. Do not read it as a clearance check. A `join` that carries a `travel` is swept in motion, but only the wood is (ADR-0075, [`travel`](travel.md)). - The proxy count is one per mount mark. `qty=` is the schedule's count only. ## Reveals, orientation and `ww joins` - **Which way the leaf lies.** Without `axis` the recess (and the drawn leaf) follows the face's longer in-face axis, which is right for a door taller than it is wide. For a wide lid, or any part hung on its long edge, write `axis x|y|z`: the part-local axis the hinge runs along. It must be one of the face's two in-face axes; the face normal, or an axis the face does not carry, is a `recess-axis` error naming the axis and the face. - **A reveal is not a gap.** An inset door stands off the carcass by its reveal and the hinge spans it. `ww joins` reports a leaf-hinge `hardware_mount` join (the item gives `leaf_length` and `leaf_width`) whose parts stand apart by no more than the hinge reaches (its catalogue `knuckle_dia`, else `leaf_width`) as `bridged`, counted apart from `gap`s; a bolt, a pull or a hinge pulled further apart than its reach is still a `gap`. - **Veneer is not a gap.** A veneer leaf bonded to a marked face stands proud of the host by its own thickness; `ww joins` takes that thickness off the measured distance, so veneering a face a part is joined to neither opens a gap nor changes the reveal. ## See also - `docs/adr/0030-hardware-placement-profile-and-recess.md`, `docs/adr/0017-hardware-and-materials.md`, `docs/adr/0075-travel-primitive.md` - [`travel`](travel.md): sweeping a part that moves - `docs/rfc/0128-hardware-placement-and-profile.md`, `docs/rfc/0071-hardware-hinges.md` - `docs/KDL_AUTHORING.md` §5 (`hardware`) and the joinery table - `examples/blanket_chest.kdl`, `examples/six_board_chest.kdl` (hinged lids), `examples/welsh_dresser.kdl` (counted-only hinges) - `docs/styles/models/hardware-mount.kdl` ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `hardware` word-valued property; node in 36 of 116 examples (112 uses); value in 1 of 116 examples (1 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl hardware @pull ``` ### `hardware_mount` joinery kind; value in 35 of 116 examples (49 uses). Shortest real use, from `step_ladder.kdl`: ```kdl joinery hardware_mount name=TieRod { hardware TieRod_Steel } ``` ### `hardware_use` node in 5 of 116 examples (15 uses). Shortest real use, from `pennsylvania_schrank.kdl`: ```kdl hardware_use DoorLock qty=1 ``` ### `distribute` node in 2 of 116 examples (2 uses). Shortest real use, from `welsh_dresser.kdl`: ```kdl distribute ``` ## Related terms - [Gilt-bronze mount](/almanac/techniques#gilt-bronze-mount) - [Hardware mount](/almanac/techniques#hardware-mount) --- # Joinery Contract *The two-body contract between joinery and join: marks, clearance, and the kinds table* **Names:** `joinery` `join` `male` `female` `joint` `clearance` `fit` `butt` `name` `grain_check` ## When to use Every time two parts are joined. A joint is **declared by the author, never inferred** (D8): you name the joint type once with a `joinery` block, then apply it with `join` between two marks. The compiler cuts the male feature on one part and the female cavity on the other at the same time. Which kind to write is the next question: use the kinds table below to find the family file. ## Syntax ```text joinery name= { ... // kind-specific; see the family file clearance // female side only, grown by clearance / 2 per face fit slip|snug|press // instead of clearance=: resolve the female material's own grain_check off reason="..." // opt out of the grain-contact error/rating; reason required } join joint= male=@ female=@ ``` - `male=`/`female=` are **`Part@Mark`**, written **unquoted** (ADR-0002). The male part receives the protruding feature (tenon, tongue, pin); the female receives the cavity. - A reference whose part id contains a `repeat` interpolation must be **quoted** because `{` is structural in KDL: `male="TopBoard_{j}@EdgeGlue_N"`. A literal id stays bare. - `join` lives in an `assembly`. The `joinery` block is project-level and reusable by name. - `repeat` inside a `joinery` body writes a schedule (graduated dovetail `pin` rows). - `grain_check off reason="..."` opts a joint out of the grain-contact error and the banded strength rating (never the movement warning, a separate concern) — see `docs/KDL_AUTHORING.md` §8's "Grain contact and the joint-strength rating" and `docs/adr/0082-grain-contact-and-joint-strength-rating.md`. `reason=` is required. ### Kinds and the file that documents each | Kind | Key params | File | | --- | --- | --- | | `butt` | none (needs no marks) | here | | `dowel` `pocket_hole` `biscuit` `domino` `edge_glue` | fastener rows | `joinery-fasteners` | | `mortise_and_tenon` `round_tenon` | tenon family (pin, wedge, key, through, proud) | `joinery-tenons` | | `half_lap` `cross_lap` `bridle` `box_joint` `dovetail_through` `dovetail_half_blind` `sliding_dovetail` | laps, bridles, box and dovetail joints | `joinery-laps-and-boxes` | | `miter` `stile_and_rail` | mitres and frame-and-panel | `joinery-frames` | | `tongue_and_groove` | declared only; a newer file in `INDEX.md` may build it | see `INDEX.md` | | `hardware_mount` | `hardware `; cuts nothing | `materials-and-hardware` | A kind that is declared but not yet cut is resolved, drawn and reported as "not yet synthesised" by `ww joins`, `ww check` and the viewer. ## The layout rule every cut joint shares (M5, ADR-0013) A joint is **laid out from its male mark**, on the stock face the mark sits on and faces. - The two marks' `normal`s must face each other (opposite in world space) on faces that are in contact, and the male mark must lie where the faces overlap. Where the female mark sits along its face is not used. - A standard anchor (`top_sw`) has no facing, so a joint that cuts anything needs `mark`s on both parts. Only `butt` needs none. - **`clearance` goes on the female side only**: its hole, slot or mortise is the male one grown by `clearance / 2` on every face. - **Clearance resolution (issue #55).** `clearance=` always wins when given. With neither `clearance=` nor `fit=`, or `fit=` alone: the female material's `tolerance_slip=`/ `tolerance_snug=`/`tolerance_press=` (`materials-and-hardware`), for the named class or `snug` by default; else the project's `glue_clearance` param; else `0.2`. `clearance=` and `fit=` together is an error. See `docs/KDL_AUTHORING.md`'s "Clearance resolution" for the full chain and the draft per-material starting values. - **Refused** (nothing cut; `ww check` fails): marks that do not face each other, faces not in contact (an inset door's reveal), faces that do not overlap or are too short for the fastener, a fastener wider than the shared overlap or one that does not fit its stock, fasteners closer than their own size, a hole deeper than the board, holes from opposite faces that meet, and a parameter missing or out of range. - **Warned:** less wood left behind a blind cut than `max(3 mm, 15 %)` of the board, and a fractional `count`. - A mark on no real stock face fails at `join` time. Oblique shoulders and `tilt=`/`heading=` marks are handled by the tenon family; see `components-and-stock`. ## Example ```kdl project name="Joined Boards" units=mm schema_version=1 { params { glue_clearance 0.2 } materials { material Oak type=hardwood } component Leg role=leg { stock Oak 45.0 45.0 700.0 mark Face x=45.0 y=22.5 z=-100.0 normal=pos_x } component Rail role=rail { stock Oak 400.0 20.0 80.0 mark End x=0.0 y=10.0 z=-40.0 normal=neg_x } joinery mortise_and_tenon name=RailJoint { tenon_thickness 8.0 tenon_length 25.0 clearance glue_clearance } assembly Frame { part Leg_A instance=Leg { by top_sw; pos x=0.0 y=0.0 z=0.0 } part Rail_A instance=Rail { by top_sw; pos x=45.0 y=12.5 z=-60.0 } join joint=RailJoint male=Rail_A@End female=Leg_A@Face } } ``` ## Pitfalls and diagnostics - Write `male=Rail_A@End`, never `Rail_A:End` or a quoted string (unless interpolated). - `join` is a **mutual contract**, not an action on one body; do not order joins or features expecting a sequence. `connect` is the removed old spelling of `join`. - `ww joins` lists each join with its gap, whether the marks meet and the normals' dot product (-1.0 is facing squarely); `ww check` adds the clash and joinery verdicts. - A parameter on a `joinery` block may be a literal or a CEL expression over the project's `params`; enums (`slope "1:8"`, `face "'neg_y'"`) have per-kind quoting; see the family file. - Choose the joint a woodworker would actually use. The validator rejects what cannot be made; it cannot tell a poor choice (end-grain pocket screws into hardwood, a mortise into 6 mm ply) from a good one. - Marks that touch but are not `join`ed are not a joint; a `join` is what cuts. ## See also - `docs/KDL_AUTHORING.md` §8; ADR-0013 (joint framework), ADR-0014 (joint authoring edits), ADR-0002 (`Part@Mark`). - `components-and-stock.md` (marks), `parts-and-assemblies.md` (where `join` sits). - `joinery-fasteners.md`, `joinery-tenons.md`, `joinery-laps-and-boxes.md`, `joinery-frames.md`, `materials-and-hardware.md`. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `joinery` project-level node; node in 109 of 116 examples (440 uses). Shortest real use, from `neo_shaker_cherry_sideboard.kdl`: ```kdl joinery butt name=BackJoint { } ``` ### `join` node in 109 of 116 examples (1962 uses). Shortest real use, from `desk.kdl`: ```kdl join joint=PullMount male=Face@PullPoint ``` ### `male` word-valued property; property in 109 of 116 examples (1962 uses). Shortest real use, from `desk.kdl`: ```kdl join joint=PullMount male=Face@PullPoint ``` ### `female` word-valued property; property in 108 of 116 examples (1918 uses). Shortest real use, from `wedged_post_and_rung_stool.kdl`: ```kdl join joint=LegJ male=Leg_FL@Top female=Seat@LegFL ``` ### `joint` word-valued property; property in 109 of 116 examples (1962 uses). Shortest real use, from `desk.kdl`: ```kdl join joint=PullMount male=Face@PullPoint ``` ### `clearance` node in 102 of 116 examples (333 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl clearance 0.4 ``` ### `fit` word-valued property; not used by any example yet. ### `butt` joinery kind; value in 20 of 116 examples (21 uses). Shortest real use, from `neo_shaker_cherry_sideboard.kdl`: ```kdl joinery butt name=BackJoint { } ``` ### `name` word-valued property; property in 116 of 116 examples (556 uses). Shortest real use, from `neo_shaker_cherry_sideboard.kdl`: ```kdl joinery butt name=BackJoint { } ``` ### `grain_check` word-valued property; not used by any example yet. ## Related terms - [Butt joint](/almanac/techniques#butt-joint) --- # Fastener Joints *Dowel, pocket-hole, biscuit, domino, and edge-glue joinery* **Names:** `dowel` `pocket_hole` `biscuit` `domino` `edge_glue` `screw_type` `spacing` `count` `biscuit_spacing` `biscuit_size` ## When to use Joining boards with loose fasteners laid out in a row: dowels, pocket screws, biscuits, dominos, and glued edge seams (a tabletop of several boards, a carcass corner). They are the cheap, jig-made joints. For a joint a woodworker would cut by hand (tenon, lap, dovetail) see the other `joinery-*` files. The marks, `join` and the layout rule are in `joinery-contract`. ## Syntax | Kind | Params | Notes | | --- | --- | --- | | `dowel` | `diameter` `count` `depth` | `count` is floored to a whole number with a warning; `depth` is into **each** part | | `pocket_hole` | `screw_type` `spacing`, opt. `face` | `screw_type` is plain quoted text: `"1-1/4_coarse"`, `"2_fine"` (length in inches, thread); `face` is a CEL string `"'neg_y'"` | | `biscuit` | `size` `spacing` | `size` is `0`, `10` or `20` only | | `domino` | `size` `spacing`, opt. `depth` | `size` is the tenon thickness `4 5 6 8 10`; `depth` per part, default half the tenon length | | `edge_glue` | opt. `biscuit_spacing` `biscuit_size` | Biscuits only when `biscuit_spacing` is given; otherwise a plain glued seam | All of them take `clearance` (female side only). Fasteners run in a row along the **long axis of the faces' overlap**, centred on the male mark, each in an equal share of the longest span centred on the mark that stays on the overlap. - **`dowel`**: `count` of them. Make the count follow a board with an expression: `count "floor(1.0 + (shelf_depth / dowel_spacing))"`. A hole must stop at least `3 mm` short of the far face (the remaining-wall rule): `depth "carcass_th - 4.0"` on a carcass side. - **`pocket_hole`, `biscuit`, `domino`, `edge_glue`**: one fastener per whole `spacing` of that span, at least one, so the count follows the board as a slider moves. - **`pocket_hole`** cuts a 15 degree pocket and pilot in the male, drilled into one of the two faces across the joint: `face` names it (a male part-local face), else the one no other part covers, else the underside of a board lying flat and the `+` face of one standing. The female gets the matching pilot. The pockets are bench work (a jig) and the fabrication pack says so. - **`edge_glue`** joins board edges for a panel; the marks sit on the facing edges. ## Example ```kdl fragment=joinery joinery dowel name=J { diameter 8.0 count 2.0 depth 15.0 clearance glue_clearance } ``` ```kdl fragment=joinery joinery biscuit name=J { size 20.0 spacing 100.0 clearance glue_clearance } ``` A pocket hole needs a board with room behind the joint face for the 15 degree run (a thin rail is refused: "a pocket ... reaches N mm back from the joint face"). This block is linted only; the geometry that builds is `examples/filing_cabinet_lateral.kdl`'s `PlinthFastener`. ```kdl fragment=joinery nocompile joinery pocket_hole name=J { screw_type "1-1/4_coarse" spacing 100.0 } ``` ## Pitfalls and diagnostics - A fractional `count` warns and floors (ADR-0005): write a whole number. - A biscuit `size` other than `0`/`10`/`20`, or a domino `size` outside `4 5 6 8 10`, is out of range and refused. - A fastener wider than the overlap, closer than its own size to its neighbour, or deeper than the board is refused; less than `max(3 mm, 15 %)` left behind a blind hole warns. - `screw_type` is plain text, not an expression: do not wrap it in inner single quotes. `face` **is** a CEL string and does need them: `face "'neg_y'"`. - End-grain pocket screws into hardwood is a poor choice the validator cannot see. - The count of an `edge_glue` biscuit row follows `biscuit_spacing`; none given means no biscuits, not a default. ## See also - `docs/KDL_AUTHORING.md` §8 ("What the compiler cuts, and what it checks"); ADR-0013 (joint framework). - `examples/bookcase.kdl` (biscuit, dowel, domino), `examples/dining_table.kdl` (`edge_glue` with biscuits), `examples/filing_cabinet_lateral.kdl` (dowel, pocket hole), `examples/blanket_chest.kdl`. - `joinery-contract.md`, `joinery-laps-and-boxes.md`. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `dowel` joinery kind; value in 32 of 116 examples (54 uses). Shortest real use, from `tall_white_cutout_cabinet.kdl`: ```kdl joinery dowel name=TopDowel { diameter 8.0 count 3 depth 18.0 clearance glue_clearance } ``` ### `pocket_hole` joinery kind; value in 5 of 116 examples (7 uses). Shortest real use, from `pennsylvania_schrank.kdl`: ```kdl joinery pocket_hole name=PlinthFix { screw_type "2_coarse" spacing 100.0 } ``` ### `biscuit` joinery kind; value in 1 of 116 examples (2 uses). Shortest real use, from `bookcase.kdl`: ```kdl joinery biscuit name=TopEdgeGlue { size biscuit_size spacing biscuit_spacing clearance glue_clearance } ``` ### `domino` joinery kind; value in 1 of 116 examples (1 uses). Shortest real use, from `bookcase.kdl`: ```kdl joinery domino name=FrameCorner { size domino_size spacing domino_spacing depth domino_depth clearance glue_clearance } ``` ### `edge_glue` joinery kind; value in 8 of 116 examples (8 uses). Shortest real use, from `shaker_trestle_dining_table.kdl`: ```kdl joinery edge_glue name=PineSeam { } ``` ### `screw_type` word-valued property; node in 5 of 116 examples (7 uses). Shortest real use, from `blanket_chest.kdl`: ```kdl screw_type "2_coarse" ``` ### `spacing` property; node in 6 of 116 examples (10 uses); property in 1 of 116 examples (1 uses). Shortest real use, from `desk.kdl`: ```kdl spacing 32.0 ``` ### `count` property; node in 32 of 116 examples (54 uses); property in 70 of 116 examples (192 uses). Shortest real use, from `tall_white_cutout_cabinet.kdl`: ```kdl count 3 ``` ### `biscuit_spacing` node in 5 of 116 examples (5 uses); value in 1 of 116 examples (2 uses). Shortest real use, from `arts_crafts_hayrake_table.kdl`: ```kdl biscuit_spacing 200.0 ``` ### `biscuit_size` node in 1 of 116 examples (1 uses); value in 1 of 116 examples (2 uses). Shortest real use, from `bookcase.kdl`: ```kdl biscuit_size 10.0 min=10.0 max=20.0 step=10.0 label="Biscuit Size" category="Joinery" ``` ## Related terms - [Biscuit joint](/almanac/techniques#biscuit-joint) - [Bored hole](/almanac/techniques#bored-hole) - [Domino joint](/almanac/techniques#domino-joint) - [Dowel joint](/almanac/techniques#dowel-joint) - [Edge-glue joint](/almanac/techniques#edge-glue-joint) - [Glue block](/almanac/techniques#glue-block) - [Lamination (glue-up)](/almanac/techniques#glue-up-lamination) - [Pocket-hole joint](/almanac/techniques#pocket-hole) --- # Frame Joinery *Mitred corners and stile-and-rail panel frames* **Names:** `miter` `stile_and_rail` `mitred_moulding` `angle` `groove_width` `groove_depth` `stub_tenon_length` `profile` ## When to use - `miter`: two boards that meet at a corner, both cut back to the plane bisecting it (a plinth, a picture frame, a box lid rim). - `stile_and_rail`: the corner of a **panel frame** (a door, a lid frame, a cabinet back) where a rail meets a stile and both carry a panel groove. The rail gets a stub tenon that fills the stile's groove. - `mitred_moulding` (#460): a mitred corner — `miter`'s own two symmetric ends, no T, no cope — where **both** members also carry the same stuck profile the whole way to the corner, so the moulding runs on round it (a door's own moulded frame corner, not a panel-frame T-joint). For a rail into a leg without a panel groove, use `mortise_and_tenon` (`joinery-tenons`). The marks, `join` and the layout rule are in `joinery-contract`. ## Syntax ```text joinery miter name= { angle } joinery stile_and_rail name= { groove_width groove_depth stub_tenon_length [profile "square"] } ``` - **`miter`**: the marks sit on the two boards' **ends**, facing out along each board. Both boards run **full length to the corner's outer edge**, so the two end faces meet in that edge; each is cut back to the plane bisecting it. `angle` is each end's cut off square, as a mitre saw reads it: 45 for a square corner, 30 for a hexagon's. It is **checked against the placement**: the declaration and the corner disagree is an error. A mitre has no cavity, so `clearance` is unused. - **`stile_and_rail`**: both parts declare a `groove target=panel` (a part feature; see `edge-and-face-cuts`) with the same `groove_width` and `groove_depth` as the joint. The rail's male mark sits on its **end**, the stile's female mark on the face the rail meets. The stub tenon is `groove_width` thick, `stub_tenon_length` long, from the edge nearer the stile's end to the rail's own panel groove (cut back `groove_depth` there, so the panel runs on past it), into the stile's groove. - **`profile`**: only `"square"` (flat sticking), which is also the default. Any other value is reported unsupported. A stuck moulding with coped shoulders is not synthesised by this joint as built here; check `INDEX.md` for a newer file that adds it. - **`mitred_moulding`**: `angle` means the same thing `miter`'s own does, and the marks sit on the two members' ends the same way. `profile`/`profile_size`/`profile_face` are `stile_and_rail`'s own three (the six moulding sections, `tenon-tips-wedges-and-coped-rails` has the full syntax). Both members are stuck the whole length, then both are mitred — the mitre cuts through the sticking, not a plain box. `profile "square"` degenerates to exactly `joinery miter`'s own geometry. Which side face carries the moulding (its "inside" edge, opposite `profile_face`'s own show face) is **not** a parameter: it is derived from the corner itself (the width-axis face farther from the mitre's own outer edge) — place the components so it lands where intended, the same way placement already decides which face `profile_face` resolves to. ```text joinery mitred_moulding name= { angle profile "ogee" // ovolo cove ogee thumbnail half_round cockbead; "square" is flat profile_size profile_face "'neg_y'" // pos_x neg_x pos_y neg_y pos_z neg_z } ``` ## Example ```kdl project name="Frames" units=mm schema_version=1 { params { glue_clearance 0.2 } materials { material Oak type=hardwood } component Board role=board { stock Oak 300.0 20.0 60.0 mark End x=0.0 y=10.0 z=-30.0 normal=neg_x } component Stile role=stile { stock Oak 60.0 20.0 400.0 groove target=panel face=pos_x width=6.0 depth=10.0 offset=7.0 mark Seat x=60.0 y=10.0 z=-30.0 normal=pos_x } component Rail role=rail { stock Oak 300.0 20.0 60.0 groove target=panel face=neg_z width=6.0 depth=10.0 offset=7.0 mark End x=0.0 y=10.0 z=-30.0 normal=neg_x } joinery miter name=Corner { angle 45.0 } joinery stile_and_rail name=FrameCorner { groove_width 6.0 groove_depth 10.0 stub_tenon_length 10.0 profile "square" } assembly Frames { part Front instance=Board { by top_sw; pos x=0.0 y=1000.0 z=0.0 } part Side instance=Board { by top_nw; pos x=0.0 y=1000.0 z=0.0; rot z=90.0 } join joint=Corner male=Front@End female=Side@End part Stile_A instance=Stile { by top_sw; pos x=0.0 y=1500.0 z=0.0 } part Rail_A instance=Rail { by top_sw; pos x=60.0 y=1500.0 z=0.0 } join joint=FrameCorner male=Rail_A@End female=Stile_A@Seat } } ``` ## Pitfalls and diagnostics - **`angle` must match the corner**: two boards meeting at 90 degrees are each cut 45. A mismatch reads "`angle` is 30.0, but `Front` and `Side` meet at 90.0, which a mitre halves". `angle` is in the range 1 to 89. - **A compound mitre** (boards that lean as well as turn, a splayed box) is reported unsupported (`corner`, RFC-0022); so are ends that do not meet in a corner edge, or a plane that misses a board or cuts it through its length. - **A mitre's boards must both run to the outer edge.** Writing one short of the corner leaves the bisecting plane missing it. - **`stile_and_rail` without the parts' own grooves** has nothing for the stub tenon to fill: declare `groove target=panel ...` on both parts first, matching the joint's `groove_width` and `groove_depth`. A `profile` other than `"square"` is reported unsupported. - A muntin or mid-rail into the **middle** of a stile is a T, not a corner: use a plain `mortise_and_tenon` (`panel_door_and_window.kdl`'s `MuntinTenonJoint`), and `cross_lap` where two muntins cross. - **`mitred_moulding`'s own checks**: `params`/`cope-profile` (a stuck `profile` needs `profile_size` and `profile_face`; an unknown profile or face word is named; a sticking deeper than half the member's own width, on either end). `corner` — a stuck profile on a genuinely compound corner (a member leaning as well as turning) is refused: no side face of a leaning member lines up with any world direction for `profile_face` to resolve at all, so this combination never reaches the chord-by-chord mirror check `profile "square"` reuses from `miter`'s own compound-mitre handling unchanged. ## See also - `docs/KDL_AUTHORING.md` §8 (`stile_and_rail`, `miter`, `mitred_moulding`); ADR-0013; RFC-0022 (oriented marks, compound mitres). - `examples/blanket_chest.kdl` (`PlinthMiter`, `LidFrameJoint`), `examples/filing_cabinet_lateral.kdl` (`PlinthMiter`), `examples/welsh_dresser.kdl` (`DoorFrameJoint`), `examples/panel_door_and_window.kdl`, `docs/styles/models/mitred-moulding-corner.kdl` (#460). - `joinery-contract.md`, `joinery-tenons.md`, `joinery-laps-and-boxes.md`, `edge-and-face-cuts.md` (grooves, mouldings), `tenon-tips-wedges-and-coped-rails.md` (the stuck-profile syntax `mitred_moulding` reuses). ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `miter` joinery kind; value in 6 of 116 examples (6 uses). Shortest real use, from `serpentine_chest_bracket_feet.kdl`: ```kdl joinery miter name=FootMiter { angle 45.0 } ``` ### `stile_and_rail` joinery kind; value in 9 of 116 examples (9 uses). Shortest real use, from `ebonised_incised_cabinet.kdl`: ```kdl joinery stile_and_rail name=PanelFrame { groove_width panel_th groove_depth groove_d stub_tenon_length stub_l profile "square" } ``` ### `mitred_moulding` joinery kind; not used by any example yet. ### `angle` property; node in 6 of 116 examples (6 uses); property in 10 of 116 examples (22 uses); value in 2 of 116 examples (3 uses). Shortest real use, from `blanket_chest.kdl`: ```kdl angle 45.0 ``` ### `groove_width` node in 9 of 116 examples (9 uses). Shortest real use, from `arts_crafts_cabinet_on_stand.kdl`: ```kdl groove_width panel_th ``` ### `groove_depth` node in 16 of 116 examples (16 uses); value in 7 of 116 examples (23 uses). Shortest real use, from `acrylic_panel_frame.kdl`: ```kdl groove_depth 8.0 ``` ### `stub_tenon_length` node in 9 of 116 examples (9 uses). Shortest real use, from `arts_crafts_cabinet_on_stand.kdl`: ```kdl stub_tenon_length 10.0 ``` ### `profile` word-valued property; node in 11 of 116 examples (12 uses). Shortest real use, from `arts_crafts_cabinet_on_stand.kdl`: ```kdl profile "square" ``` ## Related terms - [Compound mitre](/almanac/techniques#compound-mitre) - [Groove](/almanac/techniques#groove) - [Mitre joint](/almanac/techniques#mitre-joint) - [Mitred moulding corner](/almanac/techniques#mitred-moulding-corner) - [Mortise and tenon](/almanac/techniques#mortise-and-tenon) - [Rail](/almanac/techniques#rail) - [Round tenon](/almanac/techniques#round-tenon) - [Stile](/almanac/techniques#stile) - [Stile-and-rail joint](/almanac/techniques#stile-and-rail) - [Tongue and groove](/almanac/techniques#tongue-and-groove) --- # Laps, Bridles & Dovetails *Half lap, cross lap, bridle, box joint, through and half-blind dovetail, and sliding dovetail* **Names:** `half_lap` `cross_lap` `bridle` `box_joint` `dovetail_through` `dovetail_half_blind` `sliding_dovetail` `depth_ratio` `cheek_thickness` `depth` `slope` `pin_count` `finger_width` `socket_depth` `housing_depth` `tongue_thickness` `stopped` `front_gap` `index` `pin` `avoid` `on` ## When to use - **Members that cross** (frames, lattices, a trestle's X): `half_lap` where at least one member ends at the crossing, `cross_lap` where both run on through it, `bridle` where the end of one member is forked over the other (an open mortise and tenon). - **A box or drawer corner** (two boards meeting end to face): `box_joint` for fingers, `dovetail_through` or `dovetail_half_blind` for a drawer front. - **A shelf or divider captured in a housing** (a T, not a corner): `sliding_dovetail`. The marks, `join` and the layout rule are in `joinery-contract`. A joint with a tenon is in `joinery-tenons`. ## Syntax ```text joinery half_lap name= { [depth_ratio <0..1>] clearance } joinery cross_lap name= { [depth_ratio <0..1>] clearance } joinery bridle name= { cheek_thickness depth clearance } joinery box_joint name= { pin_count | finger_width clearance } joinery dovetail_through name= { slope "1:N" pin_count clearance } joinery dovetail_half_blind name= { slope "1:N" pin_count socket_depth clearance } joinery sliding_dovetail name= { slope "1:N" housing_depth tongue_thickness clearance [stopped #true front_gap ] } ``` **Crossing members (`half_lap`, `cross_lap`, `bridle`).** The two boards are laid out **through one another** at full size; the shared box is the joint. The marks sit **on the cheek**, inside the stock, each facing the board that lies over it. - `half_lap`/`cross_lap`: each part loses what of the shared box lies on its mark's side, down to the cheek. `depth_ratio` (default `0.5`) is the share of the male's thickness it loses and **must agree with where its mark puts the cheek**: a disagreement is an error, not a guess. `clearance` widens the female's notch but never deepens it, so the cheeks stay flush. A `half_lap` reaches the end of at least one member; two boards crossing mid-length are a `cross_lap`. - `bridle`: the female is the fork, open at its end; the male keeps its middle as the tongue. `cheek_thickness` is each fork cheek, so the slot is `thickness - 2 * cheek_thickness` wide. `depth` is how far the slot runs in from the female's end and must reach across the male. **Both members are the same thickness, face for face, and both marks sit on the mid-plane** (half the thickness down). **Corners (`box_joint`, `dovetail_*`).** The male (finger or tail board) is written **shoulder to shoulder**, like a tenoned rail: its end stops at the female's inner face and its mark sits on that **end**, facing the female's mark on the **broad face** it bears on. The compiler adds the fingers or tails to the male's end and cuts the sockets from the female; the stock list adds the tails to the male's blank. - Write the female **full length**, running to the male's outer face (a dovetail must end there: the tails slide in along the tail board's thickness). - `box_joint`: `pin_count` is the male's fingers (`2 * pin_count - 1` equal positions, both outer ones the male's), **or** `finger_width`, the narrowest finger you want, and the count follows the board (an odd number). One of the two, not both. - `dovetail_*`: `slope "1:N"` is an enum written as a word (1:8 hardwood, 1:6 softwood; outside 1:4 to 1:12 warns). `pin_count` counts pins **with the half-pin at each edge**, so there is one tail fewer. In place of `pin_count`, a graduated schedule: one `pin index= width=` row per tail inside a `repeat`. `socket_depth` (half-blind) is how deep the sockets run into the front; the rest is the lap. A through dovetail's tails are as long as the pin board is thick. `avoid start= width= margin=` (RFC-0070) keeps every tail root clear of a feature cut elsewhere on the same, male board — `start=`/`width=` are offsets from the joint's own low end, the same zero a `pin index=` row's position already is, not the avoided feature's own `offset=`/`start=` frame (reuse a `dado`'s *run*-axis `start=` to land in the same axis). `on=female` (a zone on the pin board instead) is refused, not built. **`sliding_dovetail`** is a **T**: male mark on the shelf's end, female mark on the broad face it meets, square. The housing runs the male's whole depth, walls leaning outward as they go deeper; the male is shouldered down to a matching tapered tongue. `housing_depth` is how deep it goes and how far the tongue stands, `tongue_thickness` is the tongue's root width centred on the male mark. `stopped #true` with `front_gap` leaves that much uncut at the end nearer the project's front (`front=`, default `neg_y`) so the housing does not show. ## Example A half lap, a cross lap and a bridle. The half lap's marks are on the cheek at mid-thickness with opposite normals; the bridle's female member ends where the male crosses it. ```kdl project name="Laps" units=mm schema_version=1 { params { glue_clearance 0.2 } materials { material Oak type=hardwood } // Half lap: each board keeps one half of its thickness at the overlap. The mark of // each sits on the cheek (mid-thickness) and faces the other board. component LapA role=rail { stock Oak 400.0 40.0 20.0 mark Cheek x=380.0 y=20.0 z=-10.0 normal=neg_z } component LapB role=rail { stock Oak 400.0 40.0 20.0 mark Cheek x=20.0 y=20.0 z=-10.0 normal=pos_z } // Cross lap and bridle: members of one thickness, marks on the mid-plane. component Bar role=bar { stock Oak 300.0 60.0 30.0 mark Mid x=30.0 y=30.0 z=-15.0 normal=pos_z mark MidDown x=30.0 y=30.0 z=-15.0 normal=neg_z } component Slat role=slat { stock Oak 300.0 40.0 20.0 mark Top x=150.0 y=20.0 z=-10.0 normal=pos_z mark Bottom x=150.0 y=20.0 z=-10.0 normal=neg_z } joinery half_lap name=Lap { depth_ratio 0.5 clearance glue_clearance } joinery cross_lap name=Cross { depth_ratio 0.5 clearance glue_clearance } joinery bridle name=Fork { cheek_thickness 10.0 depth 60.0 clearance glue_clearance } assembly Frame { part Lap_A instance=LapA { by top_sw; pos x=0.0 y=0.0 z=0.0 } part Lap_B instance=LapB { by top_sw; pos x=360.0 y=0.0 z=0.0 } join joint=Lap male=Lap_A@Cheek female=Lap_B@Cheek part Cross_A instance=Slat { by top_sw; pos x=0.0 y=500.0 z=0.0 } part Cross_B instance=Slat { by center; pos x=150.0 y=520.0 z=-10.0; rot z=90.0 } join joint=Cross male=Cross_A@Top female=Cross_B@Bottom part Bridle_A instance=Bar { by top_sw; pos x=0.0 y=1000.0 z=0.0 } part Bridle_B instance=Bar { by top_sw; pos x=60.0 y=1000.0 z=0.0; rot z=90.0 } join joint=Fork male=Bridle_A@Mid female=Bridle_B@MidDown } } ``` Corners and the housing: the male's mark is on its end, the female's on a broad face. ```kdl project name="Corners" units=mm schema_version=1 { params { glue_clearance 0.2 } materials { material Oak type=hardwood } component Side role=side { stock Oak 20.0 300.0 100.0 mark Inner x=20.0 y=7.5 z=-50.0 normal=pos_x } component Front role=front { stock Oak 200.0 15.0 100.0 mark End x=0.0 y=7.5 z=-50.0 normal=neg_x } joinery box_joint name=Box { finger_width 20.0 clearance glue_clearance } joinery dovetail_through name=Through { slope "1:8" pin_count 3.0 clearance glue_clearance } joinery dovetail_half_blind name=HalfBlind { slope "1:8" pin_count 3.0 socket_depth 12.0 clearance glue_clearance } joinery sliding_dovetail name=Housing { slope "1:6" housing_depth 7.0 tongue_thickness 10.0 clearance glue_clearance stopped #true front_gap 12.0 } assembly Boxes { part S1 instance=Side { by top_sw; pos x=0.0 y=0.0 z=0.0 } part F1 instance=Front { by top_sw; pos x=20.0 y=0.0 z=0.0 } part S2 instance=Side { by top_sw; pos x=0.0 y=400.0 z=0.0 } part F2 instance=Front { by top_sw; pos x=20.0 y=400.0 z=0.0 } part S3 instance=Side { by top_sw; pos x=0.0 y=800.0 z=0.0 } part F3 instance=Front { by top_sw; pos x=20.0 y=800.0 z=0.0 } part S4 instance=Side { by top_sw; pos x=0.0 y=1200.0 z=0.0 } part F4 instance=Front { by top_sw; pos x=20.0 y=1200.0 z=0.0 } join joint=Box male=F1@End female=S1@Inner join joint=Through male=F2@End female=S2@Inner join joint=HalfBlind male=F3@End female=S3@Inner join joint=Housing male=F4@End female=S4@Inner } } ``` ## Pitfalls and diagnostics - **A lap whose `depth_ratio` disagrees with its mark** is refused; so is a mark off the shared cheek plane (`contact`), marks that do not face each other (`facing`), and members that cross skewed rather than square to one another (`faces-overlap`: skewed laps are unsupported, RFC-0022). - **A `half_lap` crossing mid-length of both members** is refused with "this is a `cross_lap`"; a `bridle` whose female does not end where the male crosses it is refused with "a member crossing in its middle is a `cross_lap`" (`kind`). - **A bridle** needs members the same thickness, face for face (`material`), marks half the thickness down (`contact`), `cheek_thickness` leaving a slot (`params`), and `depth` reaching across the male and not running the female's whole length (`remaining-wall`). - **Corners.** Refused: the male mark on a broad face, the female mark on an end or edge, the male's end not wholly on the face, fewer than three fingers, a finger, tail root or pin under 3 mm, a half-blind socket as deep as the pin board is thick, a box joint with both or neither of `pin_count` and `finger_width`, dovetail sockets closed on both sides. Warned: a lap thinner than `max(3 mm, 15 %)`, a fractional `pin_count`, a slope outside 1:4 to 1:12. - `slope "1:8"` is a word, never a parameter's name. Do not give a `dovetail_*` both `pin_count` and a `pin` schedule. - A lipped (overlay) drawer front that runs past the side: rabbet its back face back to the side at least as deep as the sockets (`bedside_cabinet.kdl`). - A groove through the male's end stops at the shoulder; run it on into the tails with `stopped=#true` where something (a drawer floor) sits in it (`blanket_chest.kdl`). - Choose the joint a woodworker would use: the validator cannot tell a poor choice (box joints on a corner that needs a mitre) from a good one. ## See also - `docs/KDL_AUTHORING.md` §8 ("Tenons, laps, bridles and mitres", "Box joints and dovetails", "Sliding dovetail housing"); ADR-0013 (and its lap addendum), ADR-0015 (graduated schedules); RFC-0124 (sliding dovetail). - `examples/desk.kdl` (`box_joint`), `examples/dining_chair.kdl` (`half_lap`), `examples/panel_door_and_window.kdl` (`cross_lap`), `examples/bedside_cabinet.kdl`, `examples/blanket_chest.kdl`, `examples/biedermeier_box.kdl`, `examples/welsh_dresser.kdl` (dovetails), `examples/bookcase.kdl` (`sliding_dovetail`). - `joinery-contract.md`, `joinery-tenons.md`, `joinery-frames.md`, `edge-and-face-cuts.md`. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `half_lap` joinery kind; value in 4 of 116 examples (4 uses). Shortest real use, from `pierced_splat_side_chair.kdl`: ```kdl joinery half_lap name=SlipSeatCornerJoint { depth_ratio 0.5 clearance glue_clearance } ``` ### `cross_lap` joinery kind; value in 3 of 116 examples (3 uses). Shortest real use, from `mid_century_slat_bench.kdl`: ```kdl joinery cross_lap name=SlatLap { depth_ratio 0.25 clearance glue_clearance } ``` ### `bridle` joinery kind; not used by any example yet. ### `box_joint` joinery kind; value in 6 of 116 examples (7 uses). Shortest real use, from `danish_teak_sideboard.kdl`: ```kdl joinery box_joint name=CaseCorner { finger_width finger_w clearance glue_clearance } ``` ### `dovetail_through` joinery kind; value in 10 of 116 examples (10 uses). Shortest real use, from `arts_crafts_cabinet_on_stand.kdl`: ```kdl joinery dovetail_through name=CarcaseDovetail { slope "1:8" pin_count 5 clearance glue_clearance } ``` ### `dovetail_half_blind` joinery kind; value in 17 of 116 examples (17 uses). Shortest real use, from `welsh_dresser.kdl`: ```kdl joinery dovetail_half_blind name=DrawerDovetail { slope "1:6" pin_count 3.0 socket_depth 10.0 clearance glue_clearance } ``` ### `sliding_dovetail` joinery kind; value in 8 of 116 examples (8 uses). Shortest real use, from `studio_cabinet_on_stand.kdl`: ```kdl joinery sliding_dovetail name=ShelfDovetail { slope "1:8" housing_depth "case_th * 0.4" tongue_thickness "shelf_th * 0.6" clearance glue_clear } ``` ### `depth_ratio` node in 7 of 116 examples (7 uses). Shortest real use, from `dining_chair.kdl`: ```kdl depth_ratio 0.5 ``` ### `cheek_thickness` not used by any example yet. ### `depth` property; node in 59 of 116 examples (122 uses); property in 95 of 116 examples (571 uses); value in 33 of 116 examples (74 uses). Shortest real use, from `glazed_vitrine_whiplash_crest.kdl`: ```kdl depth 8.0 ``` ### `slope` word-valued property; node in 23 of 116 examples (35 uses). Shortest real use, from `art_deco_cabinet_on_fuseau_legs.kdl`: ```kdl slope "1:8" ``` ### `pin_count` node in 21 of 116 examples (28 uses). Shortest real use, from `arts_crafts_cabinet_on_stand.kdl`: ```kdl pin_count 5 ``` ### `finger_width` node in 4 of 116 examples (5 uses). Shortest real use, from `danish_teak_sideboard.kdl`: ```kdl finger_width finger_w ``` ### `socket_depth` node in 17 of 116 examples (18 uses); value in 1 of 116 examples (1 uses). Shortest real use, from `welsh_dresser.kdl`: ```kdl socket_depth 10.0 ``` ### `housing_depth` node in 11 of 116 examples (15 uses); value in 7 of 116 examples (16 uses). Shortest real use, from `pennsylvania_schrank.kdl`: ```kdl housing_depth 8.0 ``` ### `tongue_thickness` node in 12 of 116 examples (18 uses); value in 3 of 116 examples (3 uses). Shortest real use, from `dining_table.kdl`: ```kdl tongue_thickness 10.0 ``` ### `stopped` property; node in 7 of 116 examples (7 uses); property in 40 of 116 examples (135 uses). Shortest real use, from `biedermeier_chest_of_drawers.kdl`: ```kdl stopped #true ``` ### `front_gap` node in 7 of 116 examples (8 uses); value in 1 of 116 examples (1 uses). Shortest real use, from `danish_teak_sideboard.kdl`: ```kdl front_gap 10.0 ``` ### `index` property; property in 1 of 116 examples (1 uses). Shortest real use, from `blanket_chest.kdl`: ```kdl pin index=i width="i == 2.0 ? 18.0 : (carcass_height - carcass_th) / 8.0" ``` ### `pin` node in 19 of 116 examples (35 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl pin shape=round size=3.0 ``` ### `avoid` node in 1 of 116 examples (1 uses). Shortest real use, from `blanket_chest.kdl`: ```kdl avoid start="carcass_height - till_lid_th - till_height" width=till_stock_th ``` ### `on` word-valued property; not used by any example yet. ## Related terms - [Box joint](/almanac/techniques#box-joint) - [Bridle joint](/almanac/techniques#bridle-joint) - [Cross-lap joint](/almanac/techniques#cross-lap-joint) - [Dado](/almanac/techniques#dado) - [Half-blind dovetail](/almanac/techniques#half-blind-dovetail) - [Half-lap joint](/almanac/techniques#half-lap-joint) - [Peg](/almanac/techniques#peg) - [Sliding dovetail](/almanac/techniques#sliding-dovetail) - [Swallowtail fingers](/almanac/techniques#swallowtail-fingers) - [Through dovetail](/almanac/techniques#through-dovetail) --- # Tenon Joinery *Mortise-and-tenon and round tenon: haunch, through, proud, pin, wedge, key, and oblique* **Names:** `mortise_and_tenon` `round_tenon` `tenon_thickness` `tenon_width` `tenon_length` `haunch` `centering` `through` `proud` `end_chamfer` `pin` `wedge` `key` `taper_to` `draw` `enter` `ends_proud` ## When to use A rail or rung that enters a leg or post: the strong, traditional frame joint. Use `mortise_and_tenon` for a rectangular tenon (aprons, rails, stretchers) and `round_tenon` for a turned member (a chair rung, a spindle) into a bored socket. The marks, `join` and the common layout rule are in `joinery-contract`. ## Syntax ```text joinery mortise_and_tenon name= { tenon_thickness [tenon_length ] clearance [tenon_width ] [haunch length= depth=] [centering axis=] [through #true [proud ] [end_chamfer ]] [pin shape=round|square size= draw= count= spacing= [taper= at= enter= ends_proud= material= label=]] [wedge count= kerf= depth= flare= flare_depth= [material= label=]] [key size= thickness= taper= [draw= ends_proud= material= label=]] } joinery round_tenon name= { diameter length [taper_to ] clearance } ``` - **Layout.** The male mark sits on the **shoulder**, the end of the male's stock line. The tenon is **added** outside it, `tenon_length` long (the stock line stays shoulder to shoulder; the stock list buys the blank longer). `tenon_thickness` runs across the shoulder's narrower side, centred on the mark. Along the wider side the tenon is `tenon_width` wide, or without it leaves **equal shoulders all round**. The female mark names the face the tenon enters; the mortise is the tenon grown by `clearance / 2` per face. - **Thickness axis.** Always the smaller of the shoulder's two extents (exactly square: the first axis), never chosen by `tenon_width` or `haunch`. Within 2 % of square but not exactly square, with a `tenon_width`/`haunch` the compiler warns `tenon-axis` and names the side it took; make the stock differ by more to get the other orientation. - **On a `cut_path keep="inside"` part** the tenon stands on the end face the outline leaves (stepped or short faces included); none left is a `material` error naming the outline. - **`clearance 0`** is fine; round sockets are floored just above tessellation so they still clear. - **Blind or through.** A tenon that reaches the far face is through; what would stand proud is cut flush by default. `through #true` asserts it and, with `tenon_length` omitted, computes the length to reach the far face exactly, so a resize cannot silently turn it blind. - **`proud`** moves the flush-trim plane out (needs a through tenon). **`end_chamfer`** needs `proud` greater than zero, less than `proud` and less than a quarter of `tenon_thickness`. - **`haunch length= depth=`**: the `length` of the tenon's width toward the nearer end of the mortised part stands only `depth` off the shoulder, and the mortise steps with it. - **`centering axis=`** names a male axis the mark must be centred on (checked). - **`pin`** (drawbore): a peg driven across the tenon's thickness through both parts, the tenon's hole displaced `draw` toward the shoulder. `shape` `round` (default) or `square`; `size` is the diameter (or square side, required); `count`/`spacing` spread pins across the tenon width. `at` is the pin's distance from the shoulder (default: half the female's thickness for a through tenon, else half the tenon's length). `taper` makes a square pin tapered, and `enter` (a male-local face word on the pin's axis, e.g. `pos_z`) names the cheek it is driven in from, so the small end leaves from the other. It makes its own **generated part** `@/Pin_`: no `part`, `mark` or `join` names it. Needs both parts square to one another (multiple of 90 degrees). - **`wedge`** (wari-kusabi): needs `through #true`. `count` saw kerfs `kerf` wide and `depth` deep from the tenon's end, a flared mouth on the female's exit face (`flare` at most a quarter of `flare_depth`). Generated parts `@/Wedge_`. A `pin` and a `wedge` on one joint must not cross (`pin-wedge-clash`). - **`key`** (tusk tenon): needs `through #true` with `proud` large enough (`key-relish`). A mortise `size` by `thickness` cut through the tenon's **width**; `size`, `thickness` and `taper` required, `draw` defaults to `1.5`. Generated part `@/Key_0`. - **Oblique shoulder.** The tenon runs along the male's shoulder normal as placed and may meet the female face up to 45 degrees off square (`MAX_OBLIQUE`): a raked leg, a dished spoke. The mortise **leans** with the tenon and the shoulder is scribed to the face all round. - **`round_tenon`**: a bored round socket and a cylindrical tenon. Clearance is on the socket only; the tenon is cut at nominal `diameter` even when `taper_to` (its tip diameter, less than `diameter`) makes it a cone. **An oblique round tenon is built**: the socket leans up to the same 45 degree limit (RFC-0127, closed 2026-09-28). `docs/KDL_AUTHORING.md` §8 still says it is "square-shouldered only"; that sentence is stale, and the source and the RFC status line are right. ## Example ```kdl project name="Tenons" units=mm schema_version=1 { params { glue_clearance 0.2 leg_dim 45.0 } materials { material Oak type=hardwood } component Leg role=leg { stock Oak leg_dim leg_dim 700.0 mark Face_A x=leg_dim y="leg_dim / 2.0" z=-100.0 normal=pos_x mark Face_B x=leg_dim y="leg_dim / 2.0" z=-300.0 normal=pos_x mark Face_C x=leg_dim y="leg_dim / 2.0" z=-500.0 normal=pos_x } component Rail role=rail { // runs along x; its end face at x=0 is the shoulder stock Oak 400.0 20.0 80.0 mark Shoulder x=0.0 y=10.0 z=-40.0 normal=neg_x } component Rung role=rung { stock Oak 300.0 30.0 30.0 mark Shoulder x=0.0 y=15.0 z=-15.0 normal=neg_x } joinery mortise_and_tenon name=HaunchedJoint { tenon_thickness 8.0 tenon_length 25.0 clearance glue_clearance haunch length=12.0 depth=10.0 centering axis=z } joinery mortise_and_tenon name=DrawboredJoint { tenon_thickness 8.0 through #true proud 4.0 end_chamfer 1.0 clearance glue_clearance pin shape=round size=2.5 draw=1.0 count=1 } joinery round_tenon name=RungJoint { diameter 16.0 length 30.0 taper_to 12.0 clearance 0.3 } assembly Frame { part Leg_A instance=Leg { by top_sw; pos x=0.0 y=0.0 z=0.0 } part Rail_A instance=Rail { by top_sw; pos x=45.0 y=12.5 z=-60.0 } part Rail_B instance=Rail { by top_sw; pos x=45.0 y=12.5 z=-260.0 } part Rung_A instance=Rung { by top_sw; pos x=45.0 y=7.5 z=-485.0 } join joint=HaunchedJoint male=Rail_A@Shoulder female=Leg_A@Face_A join joint=DrawboredJoint male=Rail_B@Shoulder female=Leg_A@Face_B join joint=RungJoint male=Rung_A@Shoulder female=Leg_A@Face_C } } ``` ## Pitfalls and diagnostics - **Refused:** a tenon as thick as its stock; a tenon or `tenon_width` off its shoulder; a mortise running off the female face; a blind mortise the clearance breaks through; a haunch as wide or long as the tenon or with no nearer end; a shoulder more than 45 degrees off square or off the face; and **two tenons meeting inside one part** (`tenons-meet`: two aprons into one leg at one height: stop each short of the other, or see `INDEX.md` for mitred tips). - `pin`, `wedge` and `key` each need the square, axis-aligned frame (`pin-frame`, `wedge-frame`, `key-frame`: unsupported otherwise). `wedge-grain`, `wedge-flare`, `wedge-kerf`, `key-relish` and `key-mortise` name the specific refusal. - A pin much thicker than a third of the tenon warns (`pin-size`): typical is a quarter to a third. - The mortise and tenon are modelled square-cornered; a router's corner radius is a fabrication question. - `round_tenon` refuses a `taper_to` not less than `diameter` and any socket or tenon that would run past its face's edge. Tenon the round part's flat end, not its curved surface. - A mark on a curved turned surface is not reachable; tenon into a square pommel (`round-stock-sections`). - **Stale elsewhere:** `docs/KDL_AUTHORING.md` §8 ("Square-shouldered only", "Refused: the shoulder off square by any amount" under `round_tenon`) predates the oblique build. Trust this file and `crates/ww-build/src/joinery/round_tenon.rs`. ## See also - `docs/KDL_AUTHORING.md` §8 ("Tenons, laps, bridles and mitres"); RFC-0090 (pin, wedge), RFC-0125 (key), RFC-0127 (round tenon), RFC-0022 (oriented marks); ADR-0013. - `examples/desk.kdl`, `examples/dining_table.kdl`, `examples/trestle_table.kdl`, `examples/wagon_wheel.kdl`, `examples/three_legged_stool.kdl`. - `joinery-contract.md`, `joinery-laps-and-boxes.md`, `joinery-frames.md`, `round-stock-sections.md`, `components-and-stock.md` (`tilt=`/`heading=` marks). ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `mortise_and_tenon` joinery kind; value in 80 of 116 examples (186 uses). Shortest real use, from `incised_walnut_washstand.kdl`: ```kdl joinery mortise_and_tenon name=TopTenon { tenon_thickness 12.0 tenon_length 10.0 clearance glue_clearance } ``` ### `round_tenon` joinery kind; value in 22 of 116 examples (40 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl joinery round_tenon name=RungJ { diameter 15.0 length 26.0 clearance 0.4 } ``` ### `tenon_thickness` node in 80 of 116 examples (190 uses); value in 4 of 116 examples (8 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl tenon_thickness 8.0 ``` ### `tenon_width` node in 54 of 116 examples (120 uses); value in 1 of 116 examples (1 uses). Shortest real use, from `pasadena_library_table.kdl`: ```kdl tenon_width tw ``` ### `tenon_length` node in 69 of 116 examples (152 uses); value in 3 of 116 examples (6 uses). Shortest real use, from `high_back_ladder_chair.kdl`: ```kdl tenon_length 8.0 ``` ### `haunch` node in 6 of 116 examples (9 uses). Shortest real use, from `filing_cabinet_vertical.kdl`: ```kdl haunch length=6.0 depth=5.0 ``` ### `centering` node in 36 of 116 examples (68 uses). Shortest real use, from `arched_crest_side_chair.kdl`: ```kdl centering axis=z ``` ### `through` node in 20 of 116 examples (38 uses). Shortest real use, from `danish_oak_dining_table.kdl`: ```kdl through #true ``` ### `proud` node in 14 of 116 examples (29 uses); value in 2 of 116 examples (2 uses). Shortest real use, from `pasadena_bedside_chest.kdl`: ```kdl proud tenon_proud ``` ### `end_chamfer` node in 11 of 116 examples (20 uses). Shortest real use, from `pasadena_bedside_chest.kdl`: ```kdl end_chamfer 1.0 ``` ### `pin` node in 19 of 116 examples (35 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl pin shape=round size=3.0 ``` ### `wedge` node in 6 of 116 examples (8 uses). Shortest real use, from `wedged_post_and_rung_stool.kdl`: ```kdl wedge kerf=3.0 depth=18.0 flare=2.0 flare_depth=10.0 ``` ### `key` node in 2 of 116 examples (2 uses); value in 1 of 116 examples (1 uses). Shortest real use, from `mission_keyed_bookcase.kdl`: ```kdl key size=key_size thickness=key_thickness taper=key_taper draw=key_draw \ ends_proud=key_ends_proud label="Key" ``` ### `taper_to` node in 2 of 116 examples (10 uses). Shortest real use, from `windsor_fan_back_side_chair.kdl`: ```kdl taper_to 9.0 ``` ### `draw` property; property in 18 of 116 examples (34 uses). Shortest real use, from `danish_oak_dining_table.kdl`: ```kdl pin shape=round size=peg_size draw=peg_draw count=1 ``` ### `enter` word-valued property; not used by any example yet. ### `ends_proud` property; property in 9 of 116 examples (18 uses). Shortest real use, from `arts_crafts_cabinet_on_stand.kdl`: ```kdl pin shape=round size=peg_size draw=1.0 count=1 ends_proud=0.0 label="Peg" ``` ## Related terms - [Bridle joint](/almanac/techniques#bridle-joint) - [Chamfer](/almanac/techniques#chamfer) - [Domino joint](/almanac/techniques#domino-joint) - [Drawbore](/almanac/techniques#drawbore) - [Haunch](/almanac/techniques#haunch) - [Kusabi](/almanac/techniques#kusabi) - [Mortise and tenon](/almanac/techniques#mortise-and-tenon) - [Peg](/almanac/techniques#peg) - [Round tenon](/almanac/techniques#round-tenon) - [Shoulder](/almanac/techniques#shoulder) - [Stopped chamfer](/almanac/techniques#stopped-chamfer) - [Taper](/almanac/techniques#taper) - [Through dovetail](/almanac/techniques#through-dovetail) - [Through tenon](/almanac/techniques#through-tenon) - [Tusk tenon](/almanac/techniques#tusk-tenon) - [Wedged tenon](/almanac/techniques#wedged-tenon) --- # Loose Inserts *Dowels, biscuits, & dominos as generated parts* **Names:** `dowel` `biscuit` `domino` ## When to use Use a `dowel`, `biscuit` or `domino` joinery (or an `edge_glue` with `biscuit_spacing`) when two parts are joined by a loose piece glued into holes, slots or mortises cut in both. The compiler cuts both cavities **and emits the loose piece itself**. Do not model the dowel, wafer or domino as a `stock` part; it would be counted twice and reach the cut list. ## Syntax No new syntax. The existing joinery nodes are unchanged: - `joinery dowel { diameter D; count N; depth H; clearance C }` gives N round dowels, each D across and `2 * H` long. - `joinery biscuit { size 0|10|20; spacing S }` gives one #0 (47 x 15), #10 (53 x 19) or #20 (56 x 23) wafer, 4 thick, per whole `S` of the joint, at least one. - `joinery domino { size 4|5|6|8|10; spacing S; depth H }` gives one loose tenon per whole `S` (size 8 is 8 x 22 x 40). `depth` defaults to half the tenon's length. - `edge_glue` with `biscuit_spacing` and `biscuit_size` emits biscuits; without `biscuit_spacing` it emits nothing. Each insert is a generated part, `role=insert`, in the male part's material, with id `@/Dowel_`, `/Biscuit_` or `/Domino_`. It is **bought, not cut** (ADR-0017): it appears in the scene, drawings, exploded views and clash check and in the hardware schedule (`Dowel 8 x 24`, `Biscuit #20`, `Domino 8 x 22 x 40`, quantity summed over every join), and in no cut list, stock list, cut card or toolpath. ## Example ```kdl project name="Doweled rail" units=mm schema_version=1 { materials { material Oak type=hardwood } component Post { stock Oak 38.0 38.0 130.0 mark Face x=38.0 y=19.0 z=-65.0 normal=pos_x } component Rail { stock Oak 110.0 19.0 38.0 mark End x=0.0 y=9.5 z=-19.0 normal=neg_x } joinery dowel name=Dowels { diameter 8.0 count 2.0 depth 20.0 clearance 0.2 } assembly Joint { part Post instance=Post { pos x=0.0 y=0.0 z=0.0 } part Rail instance=Rail { pos x=38.0 y=9.5 z=-46.0 } join joint=Dowels male=Rail@End female=Post@Face } } ``` Compiling this gives two parts, `Rail@End/Dowel_0` and `Rail@End/Dowel_1`, each a 8 x 40 round revolve straddling the joint plane, and two hardware rows' worth of purchased dowels (`Dowel 8 x 40`, quantity 2). ## Pitfalls and diagnostics - **Double counting.** A hand-modelled `stock` dowel, wafer or domino beside the join is now a second piece; delete it. - **`insert-fit` (domino).** `2 * depth` shorter than the tenon's length leaves the tenon standing out of the mortises. Raise `depth`, or pick a smaller `size`. - **Clash.** An insert overlapping its own joint's two parts is design interference, not a clash (RFC-0090 section 5). An insert that meets any third part, or overlaps its own parts beyond the tessellation budget, is reported as a clash. Two joints whose holes cross inside one part (a bottom rail's dominos and a stile's alignment biscuit slot, as in `examples/bookcase.kdl`) show up this way: it is a real modelling overlap. - **`clearance` is female-only** (ADR-0012): the insert is nominal, fills the male cut and sits shy in the female one. - Inserts are never in the cut list: a cut list is unchanged by adding or removing these joints. - The hardware schedule label is the nominal size, so a #20 biscuit prints `Biscuit #20`. ## See also - `docs/KDL_AUTHORING.md` section 8, "What the compiler cuts, and what it checks" - `docs/rfc/0140-loose-inserts.md`, `docs/adr/0023-loose-inserts.md` - `docs/adr/0017-hardware-and-materials.md` (bought, never cut), `docs/adr/0018-turned-stock.md` - `docs/styles/models/{dowel,biscuit,domino}-joint.kdl`: the glossary's real joints ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `dowel` joinery kind; value in 32 of 116 examples (54 uses). Shortest real use, from `tall_white_cutout_cabinet.kdl`: ```kdl joinery dowel name=TopDowel { diameter 8.0 count 3 depth 18.0 clearance glue_clearance } ``` ### `biscuit` joinery kind; value in 1 of 116 examples (2 uses). Shortest real use, from `bookcase.kdl`: ```kdl joinery biscuit name=TopEdgeGlue { size biscuit_size spacing biscuit_spacing clearance glue_clearance } ``` ### `domino` joinery kind; value in 1 of 116 examples (1 uses). Shortest real use, from `bookcase.kdl`: ```kdl joinery domino name=FrameCorner { size domino_size spacing domino_spacing depth domino_depth clearance glue_clearance } ``` ## Related terms - [Biscuit joint](/almanac/techniques#biscuit-joint) - [Domino joint](/almanac/techniques#domino-joint) - [Dowel joint](/almanac/techniques#dowel-joint) --- # Materials & Hardware *Material definitions, finish, colour, and purchased hardware catalogue items* **Names:** `materials` `material` `type` `color` `finish` `hardware` `hardware_use` `hardware_mount` `qty` ## When to use - **`material`** for anything you cut from stock: a timber species, a sheet good, solid metal. - **`hardware`** for anything you buy as-is and never machine: hinges, pulls, slides, screws, nails, casters, and non-wood fill sold by the hank or roll (rush, cane, webbing). - **`finish=`** to print a finishing note on the cut sheet. **`color=`** only to tint a material in the viewer. This file covers the *declarations* and how hardware reaches the hardware schedule. Recesses and proxy geometry for hardware are a separate family; see `INDEX.md`. ## Syntax ```text materials { material type=hardwood|softwood|sheet|metal [nominal_th= finished_th=] [mill=rough|s2s|s4s|dimensional] [finish="'...'"] [color="#rrggbb"] hardware [supplier="..." part_no="..."] [price=] [=] } hardware_use [qty=] [at=@] joinery hardware_mount name= { hardware qty= } ``` - `type=`: `hardwood`, `softwood`, `sheet` or `metal`. A `sheet` declares both `nominal_th=` and `finished_th=`; joinery uses the finished figure (real plywood is undersized). Glass and laminate are `sheet`. `metal` is solid stock with no grain. - Any `material` may add `tolerance_slip=`/`tolerance_snug=`/`tolerance_press=` (mm, issue #55): the female-side clearance a joint's `fit=` resolves (`joinery-contract`), read by `fit` class, female material wins. `movement_radial_pct=`/`movement_tangential_pct=` store a material's cited shrinkage for CEL and for reference; never applied to geometry automatically — see `docs/KDL_AUTHORING.md` §5 for the draft table and sourcing. - `mill=` (issue #56, `hardwood`/`softwood` only — an error on `sheet`): how far along the rough-to-S4S road the material's boards already are. Default `s4s` (already at finished size, zero allowance) when left off — every example that predates this issue omits it and keeps its exact stock-list/BOM output. `mill=rough`/`s2s` adds a milling allowance (by mill state, never by species) to the purchase size the stock list and BOM report alongside the finished size. - `finish=` (RFC-0122): optional on any `material` and on any `part`; a CEL expression to text, printed on the cut sheet. A part's own `finish=` overrides its material's. Write a real string literal: `finish="'milk paint, barn red'"`. - `color="#RRGGBB"` (or `#rgb`): presentation only (viewer and scene JSON). A value that is not a hex colour is a warning, `must be a hex string such as "#c8a06e"; ignored`. - `hardware ` takes free-form properties, kept verbatim: `supplier=` and `part_no=` are plain-text labels (never CEL); `price=` and every other property are CEL numbers, so a param can drive them. Typical: a hinge's `leaf_length` `leaf_width` `leaf_thickness` `knuckle_dia`, a slide's `travel_distance` `slide_depth` `load_kg`. - **Two ways to use hardware (ADR-0017).** *Mounted*: `joinery hardware_mount name=... { hardware qty= }` applied through an ordinary `join` on two marks. It **cuts nothing**, like `butt`: no leaf mortise and no swing envelope are modelled. *Unmounted*: a bare `hardware_use qty= [at=Part@Mark]` fact, for a slide screwed between two flat faces, a hank of rush, a box of screws. `qty` defaults to 1; `at=` is provenance only and is never checked against a mark. - Either way the item reaches the **hardware schedule** (`ww pack`'s `hardware-schedule.txt`/`.json`) with a summed quantity, never the cut list. - `@name` slot references work anywhere a material or hardware name goes (see `reuse-and-collections`). ## Example ```kdl project name="Hinged Lid" units=mm schema_version=1 { params { glue_clearance 0.2 } materials { material WhiteOak type=hardwood finish="'danish oil'" material Ply type=sheet nominal_th=18.0 finished_th=17.6 color="#c8a06e" hardware ButtHinge_75mm supplier="Horton Brasses" part_no="H-06-75AB" \ leaf_length=75.0 leaf_width=45.0 leaf_thickness=2.5 knuckle_dia=8.0 price=6.50 hardware BallBearingSlide_16x300 travel_distance=300.0 slide_depth=16.0 load_kg=30.0 price=14.0 } component Base role=carcass { stock Ply 400.0 300.0 18.0 mark Hinge_Top x=400.0 y=150.0 z=0.0 normal=pos_x } component Lid role=door { stock WhiteOak 400.0 300.0 18.0 mark Hinge_Top x=0.0 y=150.0 z=0.0 normal=neg_x } joinery hardware_mount name=LidHinge { hardware ButtHinge_75mm qty=2 } assembly Root { part Carcass instance=Base { by top_sw; pos x=0.0 y=0.0 z=0.0 } part Lid instance=Lid finish="'shellac'" { by top_sw; pos x=400.0 y=0.0 z=0.0 } join joint=LidHinge male=Lid@Hinge_Top female=Carcass@Hinge_Top hardware_use BallBearingSlide_16x300 qty=2 } } ``` ## Pitfalls and diagnostics - **Naming a grain on a `type=sheet` material is a hard error** (ADR-0007): sheet goods are cross-laminated and have no solid-timber grain. - Declare **both** `nominal_th` and `finished_th` on a `sheet` material (KDL_AUTHORING §5). The parser keeps them verbatim and does not refuse a missing one, so an omission is silent: joinery and the sheet packer then lack the real thickness. Using the nominal figure for a dado width is the classic error. - **Never model a purchased part as a `component` with fake `stock`.** A `component` needs exactly one `stock` line, and a hinge has no board to mill. `kdl-check` *warns* on a `hardware` item that is declared and never named by `hardware_mount` or `hardware_use`. - `finish=hardwood` (bare word) reads as an unknown identifier; quote it as a CEL string. - `hardware_mount` placement is not geometry: nothing is recessed or swung. Do not expect a clash or a recess from it. - `supplier=` / `part_no=` are labels, never expressions; `price=` is an expression. - Not covered here: hardware with a placed recess or proxy geometry. If a sibling file in `features/` (see `INDEX.md`) documents it, that file's `status:` says whether it is built. ## See also - `docs/KDL_AUTHORING.md` §5 (`materials`, `hardware`), §15 (material `color=`). - `docs/adr/0017-hardware-and-materials.md`, `docs/rfc/0122-finish-attribute.md`, `docs/rfc/0121-non-wood-materials-and-hardware.md`. - `components-and-stock.md` (grain), `reuse-and-collections.md` (slots), `examples/loom_shuttle.kdl` and `examples/filing_cabinet_vertical.kdl` (`hardware_use`). ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `materials` project-level node; node in 114 of 116 examples (114 uses). Shortest real use, from `spindle.kdl`: ```kdl materials { material Ash type=hardwood } ``` ### `material` word-valued property; node in 114 of 116 examples (309 uses); property in 34 of 116 examples (116 uses); value in 1 of 116 examples (3 uses). Shortest real use, from `spindle.kdl`: ```kdl material Ash type=hardwood ``` ### `type` word-valued property; property in 114 of 116 examples (309 uses). Shortest real use, from `spindle.kdl`: ```kdl material Ash type=hardwood ``` ### `color` property; property in 85 of 116 examples (234 uses). Shortest real use, from `ebonised_incised_cabinet.kdl`: ```kdl material Brass type=metal color="#b8923a" ``` ### `finish` property; property in 78 of 116 examples (147 uses). Shortest real use, from `farmhouse_harvest_table.kdl`: ```kdl material Pine type=softwood finish="'none'" ``` ### `hardware` word-valued property; node in 36 of 116 examples (112 uses); value in 1 of 116 examples (1 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl hardware @pull ``` ### `hardware_use` node in 5 of 116 examples (15 uses). Shortest real use, from `pennsylvania_schrank.kdl`: ```kdl hardware_use DoorLock qty=1 ``` ### `hardware_mount` joinery kind; value in 35 of 116 examples (49 uses). Shortest real use, from `step_ladder.kdl`: ```kdl joinery hardware_mount name=TieRod { hardware TieRod_Steel } ``` ### `qty` property; property in 19 of 116 examples (34 uses). Shortest real use, from `adirondack_plank_chair.kdl`: ```kdl hardware WoodScrew qty=2 ``` ## Related terms - [Gilt-bronze mount](/almanac/techniques#gilt-bronze-mount) - [Hardware mount](/almanac/techniques#hardware-mount) --- # Model Metrics *Volume, mass, area, enclosed volumes, and a bill of materials* **Names:** `volume` `no_join` `a` `b` `reason` ## When to use Use `ww metrics ` to ask what a design weighs, how much surface a finish must cover, what its box holds, and what to buy. The numbers are read from the built solids, so they follow every parameter. Add a `volume` node when you need an **enclosed volume**: the litres inside a speaker cabinet, a chest, a drawer, a sealed compartment. Metrics are a report: they never change the model. ## Syntax ```text volume Interior { bounded_by End_L End_R Front Back Bottom Lid subtract Shelf } ``` - `volume ` is a top-level node of the project. `Name` keys the result in `metrics.json`. - `bounded_by` lists the parts whose union closes the region (part ids, or any unique trailing run of one: `Lid`, `Chest/Lid`). At least one. - `subtract` lists parts standing in the air that displace it (a shelf, a brace, a driver). - Nothing else goes inside `volume`; it takes no properties. CLI: `ww metrics [--json] [--set name=value] [--waste F] [--coats N] [--end-grain-max X]`. The fabrication pack carries the same report as `metrics.json`. **Contact completeness** (issue #61 Stage 1). `ww metrics` finds every real planar contact between built parts (exact geometry, not bounding boxes) and warns (`ww::analysis::unaccounted_contact`) on any contact with no covering `join` and no covering `no_join`: ```text no_join a=Shelf b=Case reason="rests on cleats, lifts out, no joint by design" ``` - `no_join` is a top-level node of the project, like `volume`. - `a=` and `b=` name the two touching parts the same way `volume`'s `bounded_by` does: an exact id, or any unique trailing run of one. Both are required. - `reason=` is required and must not be empty — the escape hatch names *why* a real contact has no joint, it does not silence by merely existing. - This decides no joint type and changes no solid (D8 stands): it only asks whether the author said *something* about a touching pair. **Finish quantity.** Give a material or part a `finish="'danish oil'"`; `ww metrics` matches the text to `docs/finishes` and, where that entry has a sourced coverage, reports litres as a range (`finish.groups..litres`). Entries with no sourced figure (shellac, paints, milk paint) report no quantity and say why. **Bill of materials.** `bom` lists stock to buy per material and thickness (nominal, and with a waste factor: `--waste F`, default 0.15; board feet for timber), purchased items split into `fastener` (hardware with a `length`), `insert` (generated dowels, biscuits, dominos) and `hardware`, and consumables: glue (the area of the glue lines) and finish litres. Give hardware a `part_no=`, `supplier=` and `price=` and they appear in the list and the total. ## Example ```kdl project name="Box" units=mm schema_version=1 { materials { material Oak type=hardwood } component Plate { stock Oak 120.0 100.0 10.0 } component Side { stock Oak 10.0 100.0 60.0 } component Rail { stock Oak 100.0 10.0 60.0 } assembly Chest { part Bottom instance=Plate { pos x=0.0 y=0.0 z=-70.0 } part Top instance=Plate { pos x=0.0 y=0.0 z=0.0 } part Left instance=Side { pos x=0.0 y=0.0 z=-10.0 } part Right instance=Side { pos x=110.0 y=0.0 z=-10.0 } part Front instance=Rail { pos x=10.0 y=0.0 z=-10.0 } part Back instance=Rail { pos x=10.0 y=90.0 z=-10.0 } } volume Inside { bounded_by Bottom Top Left Right Front Back } } ``` `Inside` is 100 x 80 x 60 = 480 000 mm³ = 0.48 L (`volumes.Inside.net_l`). ## Pitfalls and diagnostics - **A leak is an answer, not a guess.** If the bounding parts do not close (a missing panel, an open driver cutout), `status` is `leak` and there is no volume. `leak_closes_at_mm` says how wide a seam would have to be thickened to close it; absent means a face is missing. - **Seams under 2 micrometres** are sealed and flagged: `sealed_gap_mm` and `seal_error_bound_mm3` say how much the volume might be short. - **Closed dowel sockets are not the enclosure**: chambers under 1 mL or under 1% of the bounding box are listed as `ignored_voids`, not counted. - **Mass is `null` where the material has no known density**, with a reason: never a default. Give the material `species="white-oak"` or `density_kg_m3=...`. Mass is air-dry, about 12% moisture content. - **Contact is planar only**: round tenons in round mortises are not found as contact, so free area there is a slight over-statement. - **Finish litres are a range, and the end-grain multiplier (default x2 high end) is an assumption**: no maker figure for end grain is recorded. Curved end grain is not detected. Never trust a finish total with `area_without_litres_m2` above zero as the whole job. - **Glue is an area, not a quantity.** No spread rate is recorded for any glue, so none is invented. Joined pairs that meet over a curved surface are counted in `pairs_without_contact` and left out of the area. - **Stock totals are blanks, not a board layout**: waste is a flat factor, and a ring blank (sized by its circumscribing square) over-counts. Only a hardware entry with a `length` is listed as a fastener. - **Unpriced items are counted, never priced at zero**: read `unpriced_items` beside `priced_total`. - A `role=reference` part is reported but is in no total and cannot bound a volume. - **A real contact with no `join` and no `no_join` is `ww::analysis::unaccounted_contact`.** This can flood a design that has many incidental true contacts by its own nature (a woven seat's warp/weft crossings, a veneer top's leaf-to-leaf edges) — each is a real finding, not a bug in the check, but excusing hundreds of such pairs one `no_join` at a time is often not the right fix; it is a sign the design's structure is not well served by a per-pair declaration and is worth raising, not silencing. - A `no_join` naming a part that does not resolve is reported, not silently ignored, and the contact it was meant to excuse is still flagged. ## See also - `docs/adr/0036-model-metrics.md`: the method, accuracy, and the JSON schema - `docs/KDL_AUTHORING.md`, the `volume` section - `docs/proposals/0132-derived-quantities-and-linked-parameters.md` section 3 ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `volume` project-level node; node in 2 of 116 examples (2 uses); property in 2 of 116 examples (2 uses). Shortest real use, from `six_board_chest.kdl`: ```kdl volume Interior { bounded_by End_L End_R Front Back Bottom Lid } ``` ### `no_join` project-level node; not used by any example yet. ### `a` word-valued property; not used by any example yet. ### `b` word-valued property; not used by any example yet. ### `reason` word-valued property; not used by any example yet. --- # Non-Wood Materials *Ring stock, shrink-fit pre-stress, and external, bought-in parts* **Names:** `stock` `ring` `constraint` `to` `substance` `tempered` `status: partial`. Built: `stock shape=ring`, `constraint shrink`, non-timber sheet goods (`type=sheet substance=acrylic|solid_surface|glass|laminate`, `tempered=#true`), the material-pair joint allow-list, and the SUPPLY section of the cut list (ADR-0033, ADR-0076). **`external_part` (RFC-0121 Level 3) is rejected permanently** — do not write it; it does not parse. For a bought assembly use a `hardware` catalogue entry and `hardware_mount`, or a proxy `component` (ADR-0017, ADR-0076, `docs/KDL_AUTHORING.md` §5). Stress, force and pull-out are not modelled; the shrink report is *geometric only, not a structural check*. ## When to use - A part that is a **rolled or forged band**, not a sawn or turned one: a wagon wheel's iron tyre, a barrel hoop, a steel collar on a post. Material is `type=metal`. - A band **fitted by shrinking**: made a little too small, heated, dropped on, left to cool. `constraint shrink` states the fit and the compiler checks it, so the deliberate overlap is not reported as a clash. - Any part that is **bought or made to order** rather than cut from timber: a `type=metal` part is listed under SUPPLY in the cut list and left out of timber volumes, boards and blank sizes. ## Syntax ```kdl grammar material Steel type=metal thermal_expansion=0.000012 // per kelvin stock shape=ring diameter= section_w= section_h= constraint shrink from= to= radial_interference= temperature= [ambient=] ``` - `diameter` is the **outer** diameter; the bore is `diameter / 2 - section_h`. The ring sits in its circumscribing square with its top face at `z = 0` (D4), like every other part. - `shape=ring` takes the material and **no other positional argument**: no `x y z`, no grain word. It needs all three of `diameter=`, `section_w=`, `section_h=`. Only a `type=metal` material may be a ring. - `constraint` is an assembly-scope node, may sit inside a `repeat`, and `to=` takes the same `{i}` interpolation as `part id=`. `ambient=` defaults to 20 degC. The material of `from` must declare `thermal_expansion=`. - What it checks: (1) the built overlap is the declared `radial_interference` (within 0.1 mm); (2) at `temperature=` the bore `r (1 + alpha (T - T0))` clears `to`; it reports the lowest temperature that does, and the hoop strain `interference / r`. ## Example ```kdl project name="Banded Plug" units=mm schema_version=1 { params { plug_diameter 300.0 min=200.0 max=500.0 label="Plug Diameter" plug_len 20.0 min=10.0 max=60.0 label="Plug Length" band_th 6.0 min=3.0 max=12.0 label="Band Thickness" interference 0.5 min=0.3 max=1.5 label="Band Interference (radial)" heat_c 400.0 min=150.0 max=600.0 label="Band Heat (degC)" // Outer diameter of the band: the plug's diameter plus the band's wall, less what the // band is made too small by. band_diameter "plug_diameter + 2.0 * (band_th - interference)" band_depth "band_th" plug_r "plug_diameter / 2.0" } materials { material Oak type=hardwood // `thermal_expansion` is per kelvin; the shrink fit's heating arithmetic needs it. material Steel type=metal thermal_expansion=0.000012 } component Plug role=plug { stock Oak shape=round diameter=plug_diameter z=plug_len // Round stock is only round once it is turned: a plain cylinder is two stations. section axis=z interp=linear { at z=0.0 { circle r=plug_r } at z="0.0 - plug_len" { circle r=plug_r } } } // A rolled band: outer diameter, axial width, radial thickness. No grain word. component Band role=band { stock Steel shape=ring diameter=band_diameter section_w=plug_len section_h=band_depth } assembly Banded { part Plug instance=Plug { by top_center pos x=0.0 y=0.0 z=0.0 } part Band instance=Band { by top_center pos x=0.0 y=0.0 z=0.0 } constraint shrink from=Band to=Plug radial_interference=interference temperature=heat_c } } ``` `ww clash` on it reports 0 clashes, the overlap as design interference, and the fit: 0.500 mm interference (bore r = 149.500, fit r = 150.000); at 400 degC the bore clears by 0.182 mm; it fits from 299 degC; the shrink asks 0.00334 of strain and heating to 400 degC supplies 0.00456 (labelled *geometric only, not a structural check*). A worked example with six felloes and six spokes is `examples/wagon_wheel.kdl`. ## Pitfalls and diagnostics - **Round stock without a `section` is a square block.** The plug above needs its two-station `section` to be a cylinder; without it the ring "reaches" the block's corners and the fit fails with an interference of several millimetres. - **`declared radial_interference=… but the built solids interfere by …`**: the ring's `stock` and the constraint disagree. The ring's bore is `diameter / 2 - section_h`; make the constraint say what the geometry says, or the geometry what the constraint says. - **`… does not clear … the ring cannot be fitted; heat it to at least N degC`**: a steel ring grows about 1.2e-5 per kelvin, so 1 mm on a 49 mm bore needs about 1,700 degC. Interference is small relative to the radius for a reason: raise the radius, lower the interference or raise `temperature=`. - **`… declares no thermal_expansion=`**: add `thermal_expansion=0.000012` (steel) to the `material`. - **`` `X` is a wood joint, but the male part `Y` is Steel ``**: wood joinery (`mortise_and_tenon`, `dowel`, `lap`, …) is refused on a non-wood part. Only `butt` and `hardware_mount` may touch one; hold a band with `constraint shrink`. - **A ring overlapping a part it has no constraint to** stays an ordinary clash. So does one whose constraint failed. - The tolerance is 0.1 mm. A flat tang end 0.083 mm proud of a curved tread passes, a 1 mm discrepancy does not. A constraint that only passes within the tolerance is a modelling artefact worth a comment, as in the wagon wheel. - **Not modelled, and not claimed**: hoop stress, clamping force on the joints, wood crushing, yield. A `constraint shrink` is geometry and linear thermal growth only. - `kdl-check` rejects a grain word on a `type=metal` material, an unknown `constraint` kind and a `shape=ring` stock missing a dimension. ## See also - `docs/adr/0033-non-wood-materials-levels-2-3.md` (decisions, the measured wagon wheel) - `docs/rfc/0121-non-wood-materials-and-hardware.md` (Level 3, `external_part`, not built) - `docs/adr/0017-hardware-and-materials.md` (Level 1: `type=metal`, `hardware`) - `docs/KDL_AUTHORING.md` §5 `materials` - `examples/wagon_wheel.kdl`, `examples/filing_cabinet_vertical.kdl` (metal parts, supplied) ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `stock` feature; node in 114 of 116 examples (910 uses). Shortest real use, from `glazed_vitrine_whiplash_crest.kdl`: ```kdl stock Glass dw 6.0 dh ``` ### `ring` stock shape; value in 1 of 116 examples (1 uses). Shortest real use, from `wagon_wheel.kdl`: ```kdl stock Steel shape=ring diameter=wheel_diameter section_w=rim_width section_h=tyre_band ``` ### `constraint` node in 1 of 116 examples (2 uses). Shortest real use, from `wagon_wheel.kdl`: ```kdl constraint shrink from=Tyre to="Spoke_{k}" radial_interference=tyre_interference temperature=tyre_heat_c ``` ### `to` word-valued property; property in 2 of 116 examples (3 uses). Shortest real use, from `blanket_chest.kdl`: ```kdl distribute from="0.0 - (hinge_zone / 2.0)" to="hinge_zone / 2.0" ``` ### `substance` property; property in 1 of 116 examples (1 uses). Shortest real use, from `acrylic_panel_frame.kdl`: ```kdl material Acrylic type=sheet substance=acrylic nominal_th=6.0 finished_th=6.0 color="#d8ecf2" ``` ### `tempered` not used by any example yet. --- # Oblique & Skewed Joints *Oblique tenons between splayed legs and skewed crossing members* **Names:** `cross_lap` `tilt` `heading` ## When to use - A diagonal member between two legs: a hayrake or splayed stretcher, a raked brace. Tenon each end into the leg face at the member's own slope. - Two members crossing at an angle in one plane (the X of a hayrake, a St Andrew's cross): halve them into each other with a `cross_lap`. - A compound mitre at the corner of a splayed or canted frame, when both boards are mirror images across the mitre plane. Not covered: a skewed `bridle` (unsupported), crossings shallower than 15° (error), and a coped shoulder to a curved surface (the shoulder is flat on the tangent plane, standing off the curve; ADR-0027). ## Syntax - **Oblique tenon:** an ordinary `mortise_and_tenon`. The male mark is on the member's end face; the member is placed with `rot y=` at its slope. The tenon runs along the member as placed, may meet the female face up to **45° off square**, and the shoulder is scribed flush to that face. - **Skewed crossing:** an ordinary `cross_lap` (or `half_lap`). Marks sit on the shared cheek and face each other. Each member is notched to the parallelogram the other casts on the cheek. Keep `depth_ratio` agreeing with the mark depth. - **Compound mitre:** an ordinary `miter`; `angle` is checked as for a plain mitre. - **`tilt=` / `heading=`** on a `mark` orient a mark that is on no stock face (a turned surface's tangent). A mark on a stock face needs neither. ## Example ```kdl project name="Hayrake" units=mm schema_version=1 { materials { material Oak type=hardwood } component LegLeft { stock Oak 34.0 34.0 230.0 mark RailHigh x=34.0 y=17.0 z=-40.0 normal=pos_x mark RailLow x=34.0 y=17.0 z=-190.0 normal=pos_x } component LegRight { stock Oak 34.0 34.0 230.0 mark RailHigh x=0.0 y=17.0 z=-40.0 normal=neg_x mark RailLow x=0.0 y=17.0 z=-190.0 normal=neg_x } component Brace { stock Oak 250.0 19.0 30.0 mark Lap x=125.0 y=9.5 z=-15.0 normal=pos_y mark TenonStart x=0.0 y=9.5 z=-15.0 normal=neg_x mark TenonEnd x=250.0 y=9.5 z=-15.0 normal=pos_x } component BraceBack { stock Oak 250.0 19.0 30.0 mark Lap x=125.0 y=9.5 z=-15.0 normal=neg_y mark TenonStart x=0.0 y=9.5 z=-15.0 normal=neg_x mark TenonEnd x=250.0 y=9.5 z=-15.0 normal=pos_x } joinery cross_lap name=Lap {} joinery mortise_and_tenon name=RailTenon { tenon_thickness 8.0 tenon_width 20.0 tenon_length 20.0 clearance 0.0 centering axis=z } assembly Frame { part LegL instance=LegLeft { by top_center pos x=17.0 y=17.0 z=0.0 } part LegR instance=LegRight { by top_center pos x=251.0 y=17.0 z=0.0 } part Lower instance=Brace { by center pos x=134.0 y=17.0 z=-115.0 rot y=36.8699 } part Upper instance=BraceBack { by center pos x=134.0 y=17.0 z=-115.0 rot y=-36.8699 } join joint=RailTenon male=Lower@TenonStart female=LegL@RailHigh join joint=RailTenon male=Lower@TenonEnd female=LegR@RailLow join joint=RailTenon male=Upper@TenonStart female=LegL@RailLow join joint=RailTenon male=Upper@TenonEnd female=LegR@RailHigh join joint=Lap male=Lower@Lap female=Upper@Lap } } ``` The inside faces of the legs are 200 apart and the tenon marks 150 apart in height, so each brace is exactly 250 on its centre line and leans 36.8699° (a 3-4-5 run). Compiled: five joins synthesised, no clashes (`docs/styles/models/hayrake-stretcher.kdl` is the same model). ## Pitfalls and diagnostics - **`faces-overlap`, "shallower than the 15°":** the members cross at less than 15°. Move them apart, or make the slope steeper. - **`unsupported` on a `bridle`:** a skewed bridle is not laid out; use a `cross_lap` or `half_lap`. - **`corner`, "a compound mitre, and the boards are not mirror images":** the two boards' sections on the mitre plane differ by the stated mm. Give both the same section and lean. - **Shoulder gap on a round leg:** a tenon into a turned surface has a flat shoulder on the tangent plane, so it stands off the curve. Put the tenon on a flat (the square pommel). - The tenon must lie within 45° of square to the female face, and `centering axis=` checks that the mark is centred on the member. - Both lap members' marks must lie on the cheek and face each other, or the join is rejected (`facing` / `contact`). ## See also - `docs/rfc/0142-oblique-tenon-between-legs.md`, `docs/rfc/0144-skewed-crossing-joint.md`, `docs/adr/0027-oblique-and-skewed-joints.md`, RFC-0022 (oriented marks). - `docs/KDL_AUTHORING.md` §7 (tenons, laps, bridles and mitres) and the `mark` section. - `crates/ww-build/tests/oblique_skewed.rs` for the measured geometry. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `cross_lap` joinery kind; value in 3 of 116 examples (3 uses). Shortest real use, from `mid_century_slat_bench.kdl`: ```kdl joinery cross_lap name=SlatLap { depth_ratio 0.25 clearance glue_clearance } ``` ### `tilt` property; property in 2 of 116 examples (6 uses). Shortest real use, from `primary_grid_lath_armchair.kdl`: ```kdl mark Seat x=0.0 y="lath / 2.0" z="0.0 - dm" normal=neg_x tilt=0.01 heading=0.0 ``` ### `heading` property; property in 2 of 116 examples (6 uses). Shortest real use, from `primary_grid_lath_armchair.kdl`: ```kdl mark Seat x=0.0 y="lath / 2.0" z="0.0 - dm" normal=neg_x tilt=0.01 heading=0.0 ``` ## Related terms - [Cross-lap joint](/almanac/techniques#cross-lap-joint) - [Half-lap joint](/almanac/techniques#half-lap-joint) --- # Parts & Assemblies *Placement, mirroring, repeats, and reference parts* **Names:** `assembly` `part` `instance` `by` `pos` `rot` `mirror` `repeat` `count` `id` `label` `role` `finish` ## When to use Placing things in space. A `component` is the template (see `components-and-stock`); a `part` in an `assembly` is one placed copy of it. Also here: `repeat` for rows of parts, `mirror` for left/right pairs, nested assemblies for pieces that come apart, and `role=reference` parts for context (a fridge, a wall) that must be seen and clash-checked but never built. ## Syntax ```text assembly [export=] { [params {...}] ... } part instance= [ ...] [finish=] [label="..."] { by // default top_sw pos x= y= z= // omitted axes are 0.0 rot x= y= z= // degrees, about the anchor mirror x= | y= | z= // exactly one axis } part material= role= { stock x y z [grain]; by ...; pos ... } // inline one-off repeat count= { ... } // is a float 0..n-1; id="Name_{var}" required ``` - A `part` has **either** `instance=` or an inline `stock`, never both. - **Placement** is `M = S . T(pos) . R(rot) . T(-anchor)` (D5, ADR-0009): put the anchor point at `pos`, having rotated about it. Anchors, on every part for free: top corners `top_sw top_se top_ne top_nw`; top edge midpoints `top_n top_s top_e top_w`; bottom corners and midpoints `bottom_*`; and `top_center bottom_center center`. `sw` is the part-local `(0,0)` corner. Default `top_sw`. - **The anchor is named in the part's own frame, before rotation.** `top_se` is always that part's `(x_max, 0, 0)` corner whatever `rot` says. Two correct ways to place four legs: do not rotate and anchor each leg by the corner that lands there (`top_sw`/`top_se`/`top_ne`/ `top_nw`), or keep `top_sw` and turn by 0/90/180/270 so the same faces point inward. **Never mix the two** (`by top_se; rot z=90.0` swings the leg out of the assembly). `examples/dining_table.kdl` uses the rotate form inside a `repeat`. - **`mirror`** takes exactly one of `x= y= z=`: a plane `axis = value` in the enclosing frame, applied last. A true left/right pair is one component written once, the right hand the same `by/pos/rot` plus `mirror`. Marks and their `normal=` reflect with the part; the cut list gives the pair two rows (left hand / right hand). Grain and asymmetric joinery are reflected geometrically, not re-cut. Legal on an assembly instance too. - **`repeat count=`** binds `` to `0..n-1`, a float (so `i / 2.0` works). Also legal in a `component` body (a schedule of features) and a `joinery` body (a `pin` schedule), lowered by what the body means. Nested repeats multiply (see the limits in `project-and-params`). - **`instance=`** may name a component **or** an assembly. An instance of an assembly may override that assembly's own `params` with child nodes (`width 800.0`); a name that is not a param of the instanced assembly is `ww::eval::unknown_override`. - **`finish=`** on any `part` overrides its material's finish for the cut sheet; not valid on an assembly instance. - **Reference parts (ADR-0021):** `role=reference` plus `label="Refrigerator"`. It is in the scene and **in the clash check**, and in **no list**: not the cut list, stock list, nest, labels, DXF, edge schedule, cost, hardware schedule or waste. `label=` on any part is the display name in the viewer; the part id is unchanged. ## Example ```kdl project name="Four Legs And A Fridge" units=mm schema_version=1 { params { width 800.0 depth 500.0 leg_dim 40.0 top_th 20.0 } materials { material Ash type=hardwood material Appliance type=sheet nominal_th=(in)1 finished_th=(in)1 color="#B0B4B8" } component Leg role=leg { stock Ash leg_dim leg_dim 700.0 } assembly Table { // do not rotate: anchor each leg by the corner that lands there part Leg_FL instance=Leg { by top_sw; pos x=0.0 y=0.0 z=0.0 } part Leg_FR instance=Leg { by top_se; pos x=width y=0.0 z=0.0 } part Leg_BR instance=Leg { by top_ne; pos x=width y=depth z=0.0 } part Leg_BL instance=Leg { by top_nw; pos x=0.0 y=depth z=0.0 } part Top material=Ash role=top { stock Ash width depth top_th by top_sw pos x=0.0 y=0.0 z=0.0 } // a mirror pair from one component, plane at the middle of the table part Leg_Mirrored instance=Leg { by top_sw; pos x=0.0 y=0.0 z=-100.0; mirror x="width / 2.0" } // a row, with an interpolated id repeat i count=3 { part Peg instance=Leg id="Peg_{i}" { by top_sw; pos x="100.0 + i * 50.0" y=100.0 z=-300.0 } } part Fridge role=reference label="Refrigerator" { stock Appliance (in)36 (in)30 (in)68 by top_sw pos x=(in)42 y=0.0 z=0.0 } } } ``` ## Pitfalls and diagnostics - A `repeat` body part or mark whose id does not interpolate the loop variable collides; `kdl-check` refuses it. A `repeat` loop variable is a float, never a CEL int. - Mixing "anchor by one corner" with "rotate about it" moves the part out of the assembly (the guide printed that mistake until 2026-09-24). - `instance=` naming neither a component nor an assembly is a `kdl-check` error. - `mirror` takes exactly one axis, and `finish=` is not valid on an assembly instance (an assembly is not one material) (KDL_AUTHORING §9). - An assembly's params see its own file's project scope only, never the enclosing assembly's. - Z is negative going **down**: `z=-100.0` is 100 mm below the assembly origin. - Reference parts still clash: a cabinet overlapping the fridge is reported. ## See also - `docs/KDL_AUTHORING.md` §2 (anchors), §9 (`assembly`, `part`, `repeat`), §15 (reference). - `docs/rfc/0012-mirrored-placement.md` (`mirror`); `docs/adr/0009-rotation-order.md` (anchors). - `components-and-stock.md`, `joinery-contract.md` (`join` lives in an assembly), `reuse-and-collections.md` (importing an assembly). ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `assembly` project-level node; node in 116 of 116 examples (159 uses); value in 2 of 116 examples (7 uses). Shortest real use, from `drawer_dividers.kdl`: ```kdl assembly Pair { part Left instance=Uniform { by top_sw; pos x=0.0 y=0.0 z=0.0 } part Right instance=Compact { by top_sw; pos x="400.0 + organizer_gap" y=0.0 z=0.0 } } ``` ### `part` node in 116 of 116 examples (1574 uses). Shortest real use, from `queen_anne_tea_table.kdl`: ```kdl part Top instance=Top { by top_sw; pos x=0.0 y=0.0 z=0.0 } ``` ### `instance` word-valued property; property in 112 of 116 examples (1483 uses). Shortest real use, from `queen_anne_tea_table.kdl`: ```kdl part Top instance=Top { by top_sw; pos x=0.0 y=0.0 z=0.0 } ``` ### `by` feature; node in 113 of 116 examples (1543 uses). Shortest real use, from `pop_laminate_totem_divider.kdl`: ```kdl by top_w ``` ### `pos` feature; node in 116 of 116 examples (1574 uses). Shortest real use, from `danish_teak_sideboard.kdl`: ```kdl pos x=ox y=oy z=oz ``` ### `rot` feature; node in 82 of 116 examples (439 uses). Shortest real use, from `adirondack_pole_side_table.kdl`: ```kdl rot x=90.0 ``` ### `mirror` node in 30 of 116 examples (64 uses); value in 1 of 116 examples (2 uses). Shortest real use, from `adirondack_plank_chair.kdl`: ```kdl mirror x=xc ``` ### `repeat` node in 65 of 116 examples (160 uses). Shortest real use, from `inlaid_tray.kdl`: ```kdl repeat i count=2 { stringing face=pos_z material=Holly offset="6.0 + i * string_gap" } ``` ### `count` property; node in 32 of 116 examples (54 uses); property in 70 of 116 examples (192 uses). Shortest real use, from `tall_white_cutout_cabinet.kdl`: ```kdl count 3 ``` ### `id` word-valued property; property in 43 of 116 examples (92 uses). Shortest real use, from `art_deco_skyscraper_bookcase.kdl`: ```kdl dado id="Housing_{i}" face=pos_x width=shelf_th depth=dado_depth offset="toe + (i * p)" stopped=#true start=dado_stop ``` ### `label` word-valued property; property in 116 of 116 examples (2144 uses). Shortest real use, from `drawer_stack.kdl`: ```kdl width 400.0 min=250.0 max=900.0 step=10.0 label="Drawer width" ``` ### `role` word-valued property; property in 114 of 116 examples (900 uses). Shortest real use, from `glazed_vitrine_whiplash_crest.kdl`: ```kdl component Pane role=glass { stock Glass dw 6.0 dh } ``` ### `finish` property; property in 78 of 116 examples (147 uses). Shortest real use, from `farmhouse_harvest_table.kdl`: ```kdl material Pine type=softwood finish="'none'" ``` ## Related terms - [Mirrored (handed) placement](/almanac/techniques#mirrored-placement) - [Peg schedule](/almanac/techniques#peg-schedule) --- # Paths & Splines *Spline segments, named compound paths, and derived circle paths* **Names:** `path` `cut_path` `spline` `circle` `line` `arc` `polyline` ## When to use A shaped edge or a pierced cut-out that is not a straight line or a round: a cloud lift on an apron, a serpentine rail, a vase-shaped splat, a scalloped edge, a freeform window. Use `spline` for a smooth curve through points you choose, `line`/`arc`/`polyline` for the straight and circular parts, and chain them in one `cut_path` (a compound path). Name a compound path once with `path { ... }` when several `cut_path`s reuse it. Use `circle` for a plain round hole of any size (a `hole` is simpler for a drilled one). ## Syntax ```kdl grammar cut_path depth= [face=] [keep="outside"|"inside"] [path=Name] { ... } path Name // one named segment path Name { ... } // a named compound path line from_x= from_y= to_x= to_y= arc center_x= center_y= radius= start_angle= end_angle= polyline { point x= y= ; ... } spline [closed=#true] [tolerance= | samples=] { point x= y= ; ... } circle center_x= center_y= radius= ``` - `spline` passes through every `point`. `tolerance=` (default `0.01` length units) is the largest gap between the cut edge and the true curve; `samples=` instead fixes the chords per span (whole number, at most 1024). Give one, not both. The curve is flattened to chords once, the same way every time, so the result is deterministic and native/wasm identical. - An open spline closes along its chord, like a lone `arc`; in a compound path it chains end to start (each segment's end must meet the next one's start). `closed=#true` is a whole loop and cannot chain. A `circle` is a whole loop too. - A `cut_path` removes the closed region (`keep="outside"`, the default); `keep="inside"` keeps only the region, making it the part's new footprint. ## Example ```kdl // A cloud lift cut into the lower edge of an apron: straight sides, a spline between them. project name="Cloud lift" units=mm schema_version=1 { materials { material Oak type=hardwood } component Apron { stock Oak 190.0 22.0 90.0 // Named once as a compound path, then cut. path Cloud { line from_x=30.0 from_y=-10.0 to_x=30.0 to_y=0.0 spline { point x=30.0 y=0.0 point x=75.0 y=34.0 point x=115.0 y=34.0 point x=160.0 y=0.0 } line from_x=160.0 from_y=0.0 to_x=160.0 to_y=-10.0 } cut_path depth=0.0 face=neg_y path=Cloud // A closed spline: a pierced, smooth-edged window, finer than the default. cut_path depth=0.0 face=neg_y { spline closed=#true tolerance=0.05 { point x=60.0 y=45.0 point x=80.0 y=65.0 point x=100.0 y=45.0 point x=80.0 y=25.0 } } // A round hole by centre and radius. cut_path depth=0.0 face=neg_y { circle center_x=20.0 center_y=65.0 radius=8.0 } } assembly Rail { part Apron instance=Apron { pos x=0.0 y=0.0 z=0.0 } } } ``` ## Pitfalls and diagnostics - The region must have an enclosed area and not cross itself. A wave that dips below its own chord is reported as a skip ("the closed path crosses itself"); lift the end points or close the loop yourself with a `line`. - Cut corners must lie *outside* the stock so the cut breaks cleanly through the edge: the cloud lift's low corners are at `y=-10`, below the true edge at `y=0`. - Consecutive spline points that coincide are refused. A curve of `closed=#true` needs at least three points, an open one at least two. - A spline is drawn through the points you give, so overshoot comes from the points, not the maths: space them evenly, and add a point where the curve bends sharply. - `closed=#true` combined with other segments, `tolerance=` together with `samples=`, a non-whole `samples=`, and a tolerance so fine that a span would need more than 1024 chords are all refused with the cause named. - `spline` and `circle` are path segments, not features: written outside a `cut_path`/`path` they are reported as such. - `kdl-check` passing says only that the file is well formed; it cannot see whether a path closes or is sensible. Run `ww check`. - Not built: a bent or curved *part* (RFC-0040), G02/G03 toolpath arcs, features placed along a path. ## See also - `docs/KDL_AUTHORING.md` §7, `cut_path` - `docs/adr/0032-spline-and-compound-paths.md`, `docs/adr/0016-path-geometry.md` - `docs/rfc/0120-curved-path-geometry.md` - `docs/agent-reference/nodes.md` (`cut_path`), `tasks.md` ("cut a curved outline") ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `path` feature, word-valued property; node in 9 of 116 examples (15 uses); property in 9 of 116 examples (19 uses). Shortest real use, from `jacobean_joint_stool.kdl`: ```kdl path ArchLong { line from_x="span_w * 0.10" from_y=-5.0 to_x="span_w * 0.10" to_y=0.0 spline { point x="span_w * 0.10" y=0.0 point x="span_w * 0.30" y="arch_rise * 0.80" point x="span_w * 0.50" y=arch_rise point x="span_w * 0.70" y="arch_rise * 0.80" point x="span_w * 0.90" y=0.0 } line from_x="span_w * 0.90" from_y=0.0 to_x="span_w * 0.90" to_y=-5.0 } ``` ### `cut_path` feature; node in 50 of 116 examples (239 uses). Shortest real use, from `ming_waisted_corner_leg_table.kdl`: ```kdl cut_path depth=0.0 face=neg_y path=Hoof ``` ### `spline` node in 26 of 116 examples (110 uses); value in 28 of 116 examples (42 uses). Shortest real use, from `adirondack_plank_chair.kdl`: ```kdl spline { point x=0.0 y=60.0; point x="arm_w * 0.12" y=18.0; point x="arm_w * 0.5" y=0.0; point x="arm_w * 0.88" y=18.0; point x=arm_w y=60.0 } ``` ### `circle` node in 40 of 116 examples (933 uses). Shortest real use, from `windsor_round_stool.kdl`: ```kdl circle r=rf ``` ### `line` node in 33 of 116 examples (256 uses). Shortest real use, from `postmodern_flat_silhouette_chair.kdl`: ```kdl line from_x=w from_y=0.0 to_x=w to_y=seat_h ``` ### `arc` node in 19 of 116 examples (65 uses). Shortest real use, from `windsor_fan_back_side_chair.kdl`: ```kdl arc center_x=rf center_y=cy radius=rf start_angle=0.0 end_angle=180.0 ``` ### `polyline` node in 21 of 116 examples (91 uses). Shortest real use, from `incised_walnut_washstand.kdl`: ```kdl polyline { point x="9.0 + (0.0000)" y="9.0 + (1.0000)"; point x="face_w - 9.0 + (0.0000)" y="9.0 + (1.0000)"; point x="face_w - 9.0 - (0.0000)" y="9.0 - (1.0000)"; point x="9.0 - (0.0000)" y="9.0 - (1.0000)" } ``` ## Related terms - [Bootjack](/almanac/techniques#bootjack) - [Cut path](/almanac/techniques#cut-path) --- # Plans & Interfaces *Placing several designs in a room, and the component interface that makes one placeable* **Names:** `plan` `site` `room` `run` `place` `fill` `interface` `wall` `edge` `opening` `feature` `obstacle` `footprint` `mount` `attach` `abut` `needs` `stretch` `expose` `design` `swing` `hinge` `against` `at` `left_of` `right_of` `centre_on` `align` `gap` `facing` `elevation` `pin` ## When to use A `plan` lays out several parametric designs (a bed, a wardrobe, a run of kitchen cabinets) in a room and compiles them as one project. A design becomes placeable by adding an `interface` block to its root `assembly`; nothing else about the design changes, and it still builds alone. This is Stream 1 of ADR-0060 (#463): the grammar and the static checks (`ww::plan::*`) are implemented and type-check; the positioning verbs (`against`, `centre_on`, `fill`, ...) parse and have their references checked, but have **no geometric effect yet** — nothing is placed into world space, so `status: partial`. Lowering a `Plan` into a built, pooled scene is ADR-0061 (a later stream). ## Syntax ```text project ... { plan { ... } // or `site `, an exact alias for outdoor work } ``` Inside a `plan`/`site`: ```text room [height=] [floor=] { start x= y= wall x= y= // or `edge` (bounds the room, is not a wall) opening wall= at= width= height= [sill=] kind=door|window|opening [swing=in|out] [hinge=left|right] feature wall= at= kind= [z=] obstacle x= y= w= d= [h=] } run wall= [from=left|right] [offset=] [elevation=] { place [overrides...] [label=] [finish=] [mirror=] [role=reference] { } fill [] } place ... { } // also legal directly inside a plan clearance min= [label=] // plan-level, check-only ``` Positioning verbs inside a `place` body (ADR-0060 §4.5; parsed and reference-checked, no geometric effect in this stream): `against `, `at `, `left_of [gap=]`, `right_of [gap=]`, `centre_on [with=]`, `align with=`, `gap `, `facing `, `elevation `, `pin x= y= [rot=]`, `clearance min=`. Inside a top-level `assembly`, the `interface` that makes it placeable: ```text assembly export= { interface [kind=] [front=pos_x|neg_x|pos_y|neg_y|pos_z|neg_z] { expose ... [{ standard ... }] footprint [x=] [y=] [z=] w= d= h= mount floor|free|wall [z=] attach ... // default: back abut ... // default: none (not abuttable) clearance depth= [width=] [kind=walk|use|swing] [hinge=left|right] [label=] needs design= at=exposed_end|wall_end [min=] stretch ... mark [x=] [y=] [z=] [normal=pos_x|neg_x|pos_y|neg_y|pos_z|neg_z] } ... } ``` Face words (`attach`/`abut`/`clearance `/`align `, and what a `mark` may not be named): `front back left right top bottom`. `expose` names either a param of this assembly or a project param of this file (ADR-0060 Q4) — most catalogue designs keep their envelope dimensions (`width`, `bench_length`, ...) at project scope, not assembly scope. ## Example ```kdl fragment=project materials { material Oak type=hardwood } assembly Board export=Board { params { w 100.0 min=50.0 max=200.0 label="Width" } interface kind=panel { expose w footprint w=w d=10.0 h=10.0 mount floor attach back abut left right clearance front depth=600.0 kind=walk label="Walkway" } part Panel { stock Oak w 10.0 10.0 by top_sw pos x=0.0 y=0.0 z=0.0 } } plan Room { room Main height=2400.0 { start x=0.0 y=0.0 wall S x=4000.0 y=0.0 wall E x=4000.0 y=3000.0 wall N x=0.0 y=3000.0 wall W x=0.0 y=0.0 } place B1 Board w=120.0 label="Board" { against S centre_on S } } ``` ## Pitfalls and diagnostics - **`ww::plan::duplicate_plan`** — at most one `plan`/`site` per project (ADR-0060 Q2). - **`ww::plan::plan_in_import`** — plans are not importable (Q15); an imported file declaring one is refused at the `import` line. - **`ww::plan::no_footprint`** / **`duplicate_interface`** — an `interface` needs exactly one `footprint`. - **`ww::plan::reserved_param`** — `expose` may not name `label`, `finish`, `mirror` or `role`: those are reserved `place` overrides. - **`ww::plan::mark_shadows_face`** — an interface `mark` may not be named `front`, `back`, `left`, `right`, `top` or `bottom`. - **`ww::plan::room_not_closed`** / **`room_self_intersecting`** / **`opening_off_wall`** — pure 2D geometry over literal room coordinates; a room whose walls depend on an unresolved CEL expression is checked at Stage 2 instead (not yet implemented). - **`ww::plan::unknown_design`** / **`no_interface`** — `place`/`fill`/`needs design=` names a top-level assembly of this file or an import, and that design has an `interface`. - **`ww::plan::not_exposed`** — a `place` override must name a param the design's `interface` exposes. - **`ww::plan::standard_out_of_range`** — an `expose ... { standard ... }` size must lie inside the param's own `min..max`. - **`ww::plan::fill_without_stretch`** — `fill ` needs a design with a `stretch` param. - **`ww::plan::not_abuttable`** — a design with no `abut` faces (and no `needs`, which the compiler would otherwise add fillers/end panels for) cannot sit mid-run. - **`ww::plan::duplicate_name`** / **`unknown_ref`** / **`ambiguous_ref`** — walls, openings, features, obstacles, runs and places share one namespace per plan. - **`ww::plan::mount_needs_wall`** — a `mount wall` design placed outside a `run` needs an explicit `against `. - A `place`'s overrides are evaluated like a `--set` on the design's own file (ADR-0060 §5.3): this stream checks the override resolves and lies in range, but does not yet build a specialised instance — that is ADR-0061's lowering. ## See also `parts-and-assemblies` (an assembly's own params and `part`/`instance=`), `reuse-and-collections` (`import`/`export=`, the mechanism `place`'s design names resolve through), `project-and-params` (`expose`'s project-scope half, ADR-0060 Q4). ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `plan` project-level node; not used by any example yet. ### `site` project-level node; not used by any example yet. ### `room` word-valued property; not used by any example yet. ### `run` node in 2 of 116 examples (2 uses). Shortest real use, from `postmodern_broken_pediment_cabinet.kdl`: ```kdl run "w / 2.0 - gap_w / 2.0" ``` ### `place` not used by any example yet. ### `fill` property; property in 10 of 116 examples (10 uses). Shortest real use, from `deco_side_chair.kdl`: ```kdl material Stuffing type=fabric weave=velvet color="#1f4d3c" fill="'webbing, springs and horsehair under a velvet cover (one solid stands for all of it)'" ``` ### `interface` not used by any example yet. ### `wall` word-valued property; not used by any example yet. ### `edge` property; property in 23 of 116 examples (45 uses). Shortest real use, from `tall_white_cutout_cabinet.kdl`: ```kdl rabbet face=pos_x edge=pos_y width=back_th depth=rd ``` ### `opening` word-valued property; not used by any example yet. ### `feature` not used by any example yet. ### `obstacle` not used by any example yet. ### `footprint` not used by any example yet. ### `mount` not used by any example yet. ### `attach` not used by any example yet. ### `abut` not used by any example yet. ### `needs` not used by any example yet. ### `stretch` not used by any example yet. ### `expose` not used by any example yet. ### `design` word-valued property; not used by any example yet. ### `swing` word-valued property; not used by any example yet. ### `hinge` word-valued property; value in 1 of 116 examples (2 uses). Shortest real use, from `ebonised_incised_cabinet.kdl`: ```kdl component StrapR role=hinge { stock Brass hinge_len 3.0 22.0 cut_path depth=0.0 face=neg_y keep="inside" { polyline { point x=hinge_len y=0.0 point x=18.0 y=0.0 point x=0.0 y=11.0 point x=18.0 y=22.0 point x=hinge_len y=22.0 } } } ``` ### `against` not used by any example yet. ### `at` property; node in 47 of 116 examples (1024 uses); property in 10 of 116 examples (18 uses). Shortest real use, from `windsor_round_stool.kdl`: ```kdl at z=-4.0 { circle r=rf } ``` ### `left_of` not used by any example yet. ### `right_of` not used by any example yet. ### `centre_on` not used by any example yet. ### `align` not used by any example yet. ### `gap` node in 1 of 116 examples (1 uses). Shortest real use, from `drawer_stack.kdl`: ```kdl gap 10.0 min=4.0 max=30.0 step=1.0 label="Gap between drawers" ``` ### `facing` not used by any example yet. ### `elevation` not used by any example yet. ### `pin` node in 19 of 116 examples (35 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl pin shape=round size=3.0 ``` ## Related terms - [Gilt-bronze mount](/almanac/techniques#gilt-bronze-mount) - [Hardware mount](/almanac/techniques#hardware-mount) - [Hinge](/almanac/techniques#hinge) - [Peg](/almanac/techniques#peg) - [Shadow gap](/almanac/techniques#shadow-gap) --- # Project & Params *The project root, parameters, and CEL expressions* **Names:** `project` `units` `meta` `params` `preset` `label` `category` `unit_display` `solves` `inverse` ## When to use Every file starts here: the `project` root, the `params` that make the design parametric, and the expression language that every quoted string value is written in. Read this before any other family, because the quoting rule below decides how every other node is spelled. ## Syntax ```text project name="..." units=mm [schema_version=1] [front=] [collection=] { ... } params { [min= max= step= label= category= unit_display=] ... } ``` - `project` is the one root node. `units=mm` is the only accepted value (`units=in` changes only the viewer's readout). `front=` says which world direction is the piece's front (default `neg_y`). `collection=` selects a collection (see `reuse-and-collections`). - `params` may appear at three scopes, and the inner shadows the outer: `project`, `component` (defaults, overridable per instance) and `assembly` (local working values). - A parameter is **one line**: `name min= max= step= label= category= unit_display=`. Give every user-facing dimension `min`/`max`/`step`/`label`/`category`; a derived value gets none (it is not a slider). `unit_display="in"` makes the viewer show fractional inches. To *write* a default in fractional inches use `(in)"1-3/4"` (ADR-0074), e.g. `w (in)"34 1/2" min=(in)12 max=(in)96`. - `meta` is a block of free-form nodes inside `project`, parsed and kept. It carries no geometry and nothing reads it at compile time. **The quoting rule (ADR-0004), which governs the whole language:** | You write | It means | | --- | --- | | `19.0`, `glue_clearance 0.2` | a literal number | | `"width / 2.0"` (a quoted string in a value position) | a **CEL expression** | | `normal=pos_x`, `edges top_perimeter`, `part Face` | a **name or enum word**, never quoted | | `"'danish oil'"` | a CEL expression that evaluates to a *string literal* | All dimensions are floats (`19.0`, never `19`). Z is **negative going down** from a part's top face. Unit literals: `(in)34.5` is 876.3 mm at parse time, `(mm)N` is the identity, anything else (`(ft)3`) is `ww::dsl::unknown_unit`; in a CEL string use `inch(0.75)`. **CEL (degrees, not radians).** Functions: `sin cos tan asin acos atan atan2`, `floor ceil round abs`, `min max`, `sqrt pow`, `inch`, and the loudspeaker set `ac_sealed_fc ac_sealed_qtc ac_port_length ac_helmholtz_fb` (litres, Hz, mm; [`speaker-analysis`](speaker-analysis.md)). Operators: `+ - * /`, unary `-`, comparisons, `&& || !`, `cond ? a : b`; `+` joins text. Nothing else of CEL: `map`, `filter`, `all`, `exists`, `has`, `size`, `matches`, the type-conversion functions, method calls, `in` and list literals are refused with `ww::eval::not_in_language`. Limits (the diagnostic names the one you hit): expression source 4 KiB, 128 operators/brackets per expression, text 4 KiB, integer arithmetic +-2^53; and the whole-file limits: source 256 KiB, nesting depth 64, 2,000 parts, 1,000 features per part, one `repeat` 4,096 iterations. ## Example ```kdl project name="Test Shelf" units=mm schema_version=1 { params { width 600.0 min=300.0 max=1200.0 step=10.0 label="Width" category="Envelope" depth 250.0 min=150.0 max=400.0 step=10.0 label="Depth" category="Envelope" thick 19.0 min=12.0 max=32.0 step=1.0 label="Thickness" category="Stock" half_w "width / 2.0" // derived: quoted, no slider angle_rise "atan2(100.0, width) + sin(30.0)" // degrees gap (in)0.125 // unit literal finish_name "'danish oil'" // a string literal, via CEL } materials { material WhiteOak type=hardwood } assembly Root { part Board material=WhiteOak role=shelf { stock WhiteOak width depth thick by top_sw pos x=0.0 y=0.0 z=0.0 } } } ``` **Linked parameters (ADR-0035) — a slider for a derived value.** A customer asks "how tall?", the model is driven by "how many shelves". A *derived* project param may name the literal param it **solves** and an **inverse** that turns a requested value into that param's input: ```text "" [min= max= step= label= category=] solves= inverse="" [inverse_for_="" ...] ``` - `inverse` is ordinary CEL, evaluated with the current params in scope plus the reserved name `v` (the requested value). The tie rule is yours to write: `floor(x + 0.5)` rounds ties up (the builtin `round` is ties-to-even). The result is clamped, then snapped to the target's own `min`/`max`/`step`, so `--set shelf_h=900` on a 5-shelf-max case lands on a real count. - The forward expression must depend on the target, the target must be a **literal** param (never derived), and the inverse must not read the derived param it sets. Cycles are errors. - Several inverses: `inverse_for_=`; the first declared is the default, and `ww build f.kdl --set total_h=900 --for total_h=other_param` picks another. - A request lands as the *target's* value; the compile reports `requested -> resolved`, and identity (share link, cache key) is the canonical target value, never the derived one. Setting the derived param and its target in one request is refused. ```kdl project name="Shelving" units=mm schema_version=1 { params { shelf_count 3.0 min=2.0 max=6.0 step=1.0 label="Shelves" category="Envelope" gap 300.0 min=250.0 max=350.0 step=10.0 label="Shelf Gap" category="Envelope" thick 19.0 min=12.0 max=32.0 step=1.0 label="Thickness" category="Stock" total_h "(shelf_count * gap) + thick" min=500.0 max=2200.0 step=10.0 \ label="Height" category="Envelope" \ solves=shelf_count inverse="floor((v - thick) / gap + 0.5)" } materials { material WhiteOak type=hardwood } assembly Root { part Board material=WhiteOak role=shelf { stock WhiteOak 600.0 250.0 thick by top_sw pos x=0.0 y=0.0 z=0.0 } } } ``` **Named parameter bundles — `preset` (#642, ADR-0084).** A `preset "Name" { … }` node, sibling to `params`, bundles a sparse set of literal-parameter overrides under a display name; the viewer shows each one as a clickable card: ```text preset "" { ... } ``` ```text params { case_w 800.0 min=420.0 max=1200.0 step=10.0 label="Width" category="Envelope" height 1700.0 min=500.0 max=1900.0 step=10.0 label="Height" category="Envelope" } preset "Tallboy" { case_w 800.0 height 1700.0 } preset "Bedside Chest" { case_w 500.0 height 620.0 } ``` - The node's own argument is the display name, same convention as `component`/`assembly`. The body holds bare `name value` lines only — no `min=`/`max=`/`step=`/`label=`, since an override is a value, not a second slider declaration. - Any subset of the project's declared parameters; a parameter the preset does not mention is left alone. A target must be a literal parameter, never derived (`ww::preset::derived`, same rule as `--set`). - Checked at compile time against the target's own `min=`/`max=`/`step=`, reusing the same validation `--set` already goes through: `ww::preset::unknown`/`::derived`/`::type`/ `::range`/`::step`, each naming which preset. A compile error, not a warning. Two presets may not share a name (`ww::preset::duplicate`). Declaration order is display order. ## Pitfalls and diagnostics - A bare integer dimension (`19`) and `units=` anything but `mm` are rejected by `kdl-check`. - A typo in an identifier (`"heigth - top_th"`) is a hard error, not a silent dead reference: `kdl-check` resolves every identifier in every expression and ignores only the contents of CEL string literals. - A literal word where an expression is expected (`finish="danish"`) reads as an unknown identifier. Write `"'danish'"`. - A bare argument outside `params` is a **name**: `edges top_perimeter` is never an expression. A named property whose key is a label/enum slot (`label`, `category`, `id`, `name`, `instance`, `role`, `material`, `type`, `slope`, `profile`, `section`, `normal`, `axis`, `joint`, `male`, `female`, `stop`, `keep`, `match`, ...) is also never an expression. - A parameter with children (`width 1.0 { ... }`) or more than one default is a parse error; an unknown key on a parameter lists the allowed ones (`min` `max` `step` `label` `category` `unit_display`). - Hitting a limit: split a deeply nested `repeat` (`repeat 64` inside `repeat 64` is 4,096 and is refused before the inner loop unrolls) or hoist an expression into a named param. ## See also - `docs/KDL_AUTHORING.md` §1-4 (units, Z, params, `preset`), §13 (limits), §16 (unit literals). - `docs/adr/0004` (string = expression), `docs/adr/0005` (CEL), `docs/adr/0021`, `docs/adr/0084` (`preset` blocks). - `quickstart.md` for a first compiling file; `components-and-stock.md` for what goes in a project next. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `project` node in 116 of 116 examples (116 uses). ### `units` word-valued property; property in 116 of 116 examples (116 uses). ### `meta` project-level node; not used by any example yet. ### `params` project-level node, feature; node in 116 of 116 examples (386 uses). Shortest real use, from `high_back_ladder_chair.kdl`: ```kdl params { h 30.0 } ``` ### `preset` project-level node; node in 4 of 116 examples (10 uses). Shortest real use, from `shaker_trestle_dining_table.kdl`: ```kdl preset "Dining Table" { top_len 1800.0 top_w 840.0 } ``` ### `label` word-valued property; property in 116 of 116 examples (2144 uses). Shortest real use, from `drawer_stack.kdl`: ```kdl width 400.0 min=250.0 max=900.0 step=10.0 label="Drawer width" ``` ### `category` word-valued property; property in 115 of 116 examples (2105 uses). Shortest real use, from `scandinavian_birch_shelving_unit.kdl`: ```kdl bays 3.0 min=3.0 max=5.0 step=1.0 label="Bays" category="Envelope" ``` ### `unit_display` word-valued property; not used by any example yet. ### `solves` word-valued property; property in 1 of 116 examples (1 uses). Shortest real use, from `filing_cabinet_vertical.kdl`: ```kdl overall_h "leg_len + top_th" min=650.0 max=2000.0 step=5.0 \ label="Overall Height" category="Envelope" \ solves=drawer_count \ inverse="floor((v - top_th - bank_floor_h - rail_h) / opening_pitch + 0.5)" ``` ### `inverse` property; property in 1 of 116 examples (1 uses). Shortest real use, from `filing_cabinet_vertical.kdl`: ```kdl overall_h "leg_len + top_th" min=650.0 max=2000.0 step=5.0 \ label="Overall Height" category="Envelope" \ solves=drawer_count \ inverse="floor((v - top_th - bank_floor_h - rail_h) / opening_pitch + 0.5)" ``` --- # Reuse & Collections *Import, collections, choices, and slots* **Names:** `import` `from` `export` `collection` `extends` `choice` `slots` `style` `requires` `kind` `construction` ## When to use - **`import`** to use another file's assembly or component, or the built-in drawer (`std:drawer`). - **`collection`** when one design must come in several "looks" or standards (a US base height vs a slab-front frameless kitchen) without forking the file: standards as `params`, word-valued settings as `choice`, and role names (`slots`) bound to materials, hardware, components or assemblies. ## Syntax ```text import from="lib/x.kdl" export=[,] // or from="std:drawer" project name="..." units=mm collection= { collection [extends=] { params { ... } // read as coll_ choice = { ... } // read as coll__is_ (0 or 1) slots { [requires="p q"] } // a derived collection rebinds } } ``` - **Import:** one line; both values plain text. `from=` is relative to the importing file's directory (CLI: inside the entry file's directory only, no absolute paths, no `..`, `.kdl` only). `export=` is a comma-separated list; omitted, it takes everything the file offers. The imported file marks what it offers with `assembly Name export=Name { ... }`. A file resolves in its **own** scope: an imported drawer sees the drawer file's params, never the importer's. Override at the instance: child nodes on the `part` (`width 800.0`) set the *assembly's own* params. Imported top-level assemblies are templates, never scene roots. - **`std:drawer`** is built in, works in the browser and on a server: `DrawerBox`, a box-jointed Baltic-birch drawer box; params `width` `depth` `height` (outside size; origin the top face's south-west corner). - **Collections:** `project ... collection=Name` selects the one in force; there is no default. `extends=Base` merges root-first, a derived collection overriding only keys the base declares. `choice n=word { words }` is project-level only; with `n` = `construction` and words `face_frame frameless` the params are `coll_construction_is_face_frame` and `coll_construction_is_frameless`; branch with `repeat _ count=coll_construction_is_face_frame {...}`. **Slots** are declared once in the base as `name kind Target [requires="p1 p2"]`; used as `@name` wherever a material, hardware, component or assembly name goes (`stock @carcass ...`, `part Door instance=@door {...}`, `hardware_use @pull qty=1`, joinery, `finish`). `requires=` lists the params a bound component must declare, checked for every collection's slots whether in force or not. - Materials, hardware and joinery are one global catalogue by name across imports, but an imported entry is evaluated in its **own file's** scope. ## Example ```kdl project name="Kitchen Cabinet" units=mm schema_version=1 collection=SlabFrameless { import DrawerBox from="std:drawer" export=DrawerBox collection UsBase { params { base_h (in)34.5 reveal (in)0.125 } choice construction=face_frame { face_frame frameless } slots { carcass material CarcassPly } } collection SlabFrameless extends=UsBase { params { base_h (in)36 } choice construction=frameless } materials { material CarcassPly type=sheet nominal_th=18.0 finished_th=17.6 } assembly Cabinet { part Side material=@carcass role=side { stock @carcass 560.0 "coll_base_h" 17.6 by top_sw pos x=0.0 y=0.0 z=0.0 } part Drawer instance=DrawerBox width=400.0 depth=450.0 height=120.0 { by top_sw pos x=20.0 y=0.0 z=-100.0 } } } ``` ## Pitfalls and diagnostics - Diagnostics: `ww::dsl::collection_unknown_key` (a derived collection sets a key the base lacks), `collection_cycle`, `unknown_collection`, `bad_choice`, `slot_contract` (a bound component lacks a `requires=` param, reported at the slot line), `unknown_slot` (`@x` with no collection in force or naming no slot). - Import diagnostics: `import_cycle`, `import_too_deep`, `too_many_imports`, `unknown_export`, `import_conflict` (a same-name material/hardware/joinery that differs, or reads a param so means something different in each file). Diagnostics inside an imported file name that file and count from its own line 1 (`lib/bad.kdl:7:15`). - Limits (documented, not defects): collections are local to the file (not importable); `coll_*` params are not visible inside imported components; **computed values cannot be exported** (only assemblies and components); `ww` author edits (`join`, marks, joinery) refuse targets inside an imported file. - Imported parts appear with mangled names (`DrawerBox.DrawerBoxEnd~Wide.Bottom.Front`). - `style` here is the project/collection look; the *style rules* a finish or style file applies are a separate family (see `INDEX.md`). ## See also - `docs/KDL_AUTHORING.md` §14 (`import`), §16 (collections); ADR-0020 (import), ADR-0021. - `examples/kitchen_collections.kdl` (a three-unit kitchen; the second collection is ten lines). - `parts-and-assemblies.md` (instance overrides), `materials-and-hardware.md`. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `import` project-level node; node in 5 of 116 examples (8 uses). Shortest real use, from `drawer_stack.kdl`: ```kdl import DrawerBox from="std:drawer" export=DrawerBox ``` ### `from` word-valued property; property in 7 of 116 examples (11 uses). Shortest real use, from `drawer_stack.kdl`: ```kdl import DrawerBox from="std:drawer" export=DrawerBox ``` ### `export` word-valued property; property in 5 of 116 examples (10 uses). Shortest real use, from `drawer_stack.kdl`: ```kdl import DrawerBox from="std:drawer" export=DrawerBox ``` ### `collection` project-level node, word-valued property; node in 1 of 116 examples (3 uses); property in 1 of 116 examples (1 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl collection ShakerFaceFrame extends=UsBase style=traditional-shaker { } ``` ### `extends` word-valued property; property in 1 of 116 examples (2 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl collection ShakerFaceFrame extends=UsBase style=traditional-shaker { } ``` ### `choice` project-level node; node in 1 of 116 examples (2 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl choice construction=frameless ``` ### `slots` node in 1 of 116 examples (2 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl slots { carcass CarcassPly door Door_Slab front WalnutPly pull Knob_1_25 } ``` ### `style` word-valued property; property in 1 of 116 examples (2 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl collection ShakerFaceFrame extends=UsBase style=traditional-shaker { } ``` ### `requires` word-valued property; property in 1 of 116 examples (1 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl door component Door_Shaker requires="dw dh pull_x pull_y" ``` ### `kind` word-valued property; property in 4 of 116 examples (5 uses). Shortest real use, from `welsh_dresser.kdl`: ```kdl travel kind=angle axis=z body=assembly pivot=Pin_Back ``` ### `construction` property; property in 1 of 116 examples (2 uses). Shortest real use, from `kitchen_collections.kdl`: ```kdl choice construction=frameless ``` ## Related terms - [Bridle joint](/almanac/techniques#bridle-joint) --- # Round Stock & Sections *Turned parts: round stock, circle sections, and the square pommel* **Names:** `shape` `rect` `round` `section` `interp` `circle` `samples` `axis` `pommel` ## When to use A lathe-turned part: a table leg, a spindle, a chess piece, a wagon-wheel hub. The blank is round or square stock, and a `section` of stations sets the turned profile. Turned parts are a bench operation in the fabrication pack (no CNC setup spins a spindle). ## Syntax ```text stock shape=round diameter= z= // no grain argument section axis=z [interp=linear|spline] [samples=] { at z= { circle r= } // two or more stations } ``` - **`shape=`** is `rect` (the default for every plain `stock x y z`) or `round`. Round stock takes named `diameter=` and `z=` (the length along the spindle axis), never a grain word: its grain is lengthwise by definition and naming one is rejected. - **`section`** stations are sorted by `z` at compile time, whatever the write order. Each is `at z= { circle r= }`. `interp=linear` (default) makes straight facets between stations; `interp=spline` resamples a centripetal Catmull-Rom curve through them (`samples=`, default 64). - **The declared `diameter` must be at least twice the widest `circle` station**, or the build refuses it (the blank is narrower than the part). - A turned part is a stock primitive, not a cutter: ordinary features (`hole`, a joint's mortise) still cut it. Anchors `top_center`, `bottom_center`, `center` are the spindle axis exactly. The corner anchors stay legal but sit in wood the lathe has removed. - **Square pommel (RFC-0127):** keep ordinary square `stock` (no `shape=round`) and give a `section` whose stations do **not** reach the part's top face. Above the topmost station the square stays untouched (the pommel); below it the part is turned. The last station must reach the part's own foot (`z = -leg_len`) exactly. The topmost radius may be up to half the stock width: equal for a flush transition, a little less for a small shoulder. A tenon goes into the **square** pommel, never the round part. ## Example ```kdl project name="Turned Leg" units=mm schema_version=1 { params { leg_dim 45.0 leg_len 700.0 pommel_h 90.0 foot_r 14.0 } materials { material Ash type=hardwood } component Leg role=leg { stock Ash leg_dim leg_dim leg_len section axis=z interp=linear { at z="0.0 - pommel_h" { circle r="leg_dim / 2.0" } at z="0.0 - leg_len" { circle r=foot_r } } mark MortiseFace_X x=leg_dim y="leg_dim / 2.0" z="0.0 - pommel_h / 2.0" normal=pos_x } component Pawn role=game_piece { stock Ash shape=round diameter=40.0 z=60.0 section axis=z interp=spline samples=48 { at z=0.0 { circle r=6.0 } at z=-30.0 { circle r=10.0 } at z=-60.0 { circle r=16.0 } } mark AxisTop x=20.0 y=20.0 z=0.0 normal=pos_z } assembly Root { part Leg_A instance=Leg { by top_sw; pos x=0.0 y=0.0 z=0.0 } part Pawn_A instance=Pawn { by top_sw; pos x=200.0 y=0.0 z=0.0 } } } ``` ## Pitfalls and diagnostics - **Not built (residual):** a `rect`-family `section` (a non-circular varying profile; only `circle` stations exist), and a mark on the curved turned surface (only the flat axis ends, `normal=pos_z`/`neg_z`, are reachable; a mortise cut into a curved face is not modelled). Another file in `INDEX.md` may lift these (faceplate and rectangular sections); the newer file wins. - A grain argument on `shape=round` stock is an error; so is a grain word on a `type=sheet` material. - A `section` that stops short of the foot leaves square material floating under a round run; the build refuses it. Make the last station land on `z = -leg_len`. - A station radius over half the square stock width is refused (the blank cannot hold it). - A tenon into the round part cannot be modelled: tenon into the pommel, or use a `round_tenon` (see `joinery-tenons`) on a turned member's flat end. ## See also - `docs/KDL_AUTHORING.md` §6 ("Turned stock"); ADR-0018; RFC-0020 (turned round stock), RFC-0127 (pommel). - `examples/chess_pawn.kdl`, `examples/spindle.kdl`, `examples/shaker_side_table.kdl`, `examples/wagon_wheel.kdl`, `examples/speaker_ported.kdl`. - `components-and-stock.md`, `joinery-tenons.md`. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `shape` word-valued property; property in 66 of 116 examples (133 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl pin shape=round size=3.0 ``` ### `rect` stock shape; node in 15 of 116 examples (101 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl rect x_min=0.0 x_max=pw y_min=0.0 y_max=pd ``` ### `round` stock shape, expression function; value in 44 of 116 examples (89 uses). Shortest real use, from `appalachian_ladder_back_side_chair.kdl`: ```kdl pin shape=round size=3.0 ``` ### `section` feature, word-valued property; node in 47 of 116 examples (93 uses); property in 18 of 116 examples (36 uses). Shortest real use, from `pop_laminate_stepped_sideboard.kdl`: ```kdl section axis=z interp=linear { at z=0.0 { circle r="drum_d / 2.0" } at z="0.0 - drum_h" { circle r="drum_d / 2.0" } } ``` ### `interp` word-valued property; property in 41 of 116 examples (80 uses). Shortest real use, from `pop_laminate_stepped_sideboard.kdl`: ```kdl section axis=z interp=linear { at z=0.0 { circle r="drum_d / 2.0" } at z="0.0 - drum_h" { circle r="drum_d / 2.0" } } ``` ### `circle` node in 40 of 116 examples (933 uses). Shortest real use, from `windsor_round_stool.kdl`: ```kdl circle r=rf ``` ### `samples` property; property in 23 of 116 examples (35 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl section axis=z interp=linear samples=2 { at z=0.0 { circle r=port_outer_radius } at z="0.0 - port_length" { circle r=port_outer_radius } } ``` ### `axis` word-valued property; property in 72 of 116 examples (173 uses). Shortest real use, from `arched_crest_side_chair.kdl`: ```kdl centering axis=z ``` ### `pommel` not used by any example yet. ## Related terms - [Pommel](/almanac/techniques#pommel) --- # Sliding Sleeve *A telescoping column joint, drawn parked* **Names:** `sliding_sleeve` ## When to use Use `sliding_sleeve` for a telescoping member: an inner post riding in a bored outer sleeve (a music-stand column, an adjustable leg), drawn **parked** at one declared height. It is a static pose, not a `travel`: nothing is swept (ADR-0075 does not apply) and each part keeps one transform. Keywords: telescope, sleeve, column, adjustable height, slip joint. ## Syntax ```kdl grammar joinery sliding_sleeve name= { extension // required: how far the inner member stands out of the mouth clearance // the bore is this much wider than the member (total, ADR-0012) depth // optional: a blind bore; absent or 0.0 bores the whole sleeve } join joint= male=@ female=@ ``` - `Foot` is a mark on the inner member's inserted end face, `Mouth` one on the sleeve's open end face, each normal pointing out of its own wood: they face each other. - The author places both parts; the joint **checks** the pose: the foot is on the mouth's axis (`sleeve-axis`), and the member is placed so it stands exactly `extension` out (`sleeve-pose`). `sleeve-extension`: nothing of the member is inside. `sleeve-depth`: the bore is shorter than the engagement or the sleeve. `sleeve-fit`: no wall is left. - The bore is the member's section plus `clearance`, so the member stands in air and there is no clash to exempt; a mis-posed member still clashes and is reported. - Both members stay in the cut list. The join gap check skips it (a clearance fit). ## Pitfalls and diagnostics - `extension` is declared and the placement is yours: change `stand_height` and the inner member's length and `pos` together, or `sleeve-pose` says they disagree. - The inner member's section is read off its `Foot` face; a member turned a non-right angle about the column's axis against the sleeve is not synthesised (`sleeve-axis`, unsupported). - A blind bore leaves wood at the sleeve's foot (`remaining-wall` warns when thin). ## Example `examples/music_stand.kdl`: `ColumnSleeve` on the spider, `ColumnInner` parked `stand_height - sleeve_len` out, carrying the head. ## See also - [`travel`](travel.md) — a swept motion; this joint is a parked pose. - [`hardware-mounting`](hardware-mounting.md) — the clamp knob, as a round proxy. - [`tongue-and-groove`](tongue-and-groove.md) — another one-off joinery kind's page. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `sliding_sleeve` joinery kind; value in 1 of 116 examples (1 uses). Shortest real use, from `music_stand.kdl`: ```kdl joinery sliding_sleeve name=Telescope { extension column_extension clearance glue_clearance depth bore_depth } ``` ## Related terms - [Sliding sleeve](/almanac/techniques#sliding-sleeve) --- # Speaker Analysis *Loudspeaker enclosure functions (ac_*) and the report-only analysis node* **Names:** `driver` `analysis` `ac_sealed_fc` `ac_sealed_qtc` `ac_port_length` `ac_helmholtz_fb` `fs` `qts` `vas` `sd` `xmax` `re` `sensitivity_db` `tolerance` `drive_watts` `port_diameter` `ports` `tuning` `range` `points` ## When to use You are modelling a speaker cabinet and want its tuning or its response to follow the box. Two separate things: - **`ac_*` functions** are CEL builtins. They run before geometry, so a result may feed a dimension: a port length for a target tuning, the Fc of a sealed box. - **`driver` and `analysis` nodes** are a report. They never change a solid, a cut list or a cache key; `ww analyze FILE` prints the response, the -3 dB point, port air speed and cone excursion, and `ww pack` writes the same into `analysis.json`. Units are fixed and never written next to the number: **volumes in litres, frequencies in Hz, port lengths and diameters in mm**. A bare number elsewhere in the file is still millimetres, so a volume in a litre argument is the one place to check twice. ## Syntax Functions (every argument finite and positive; `np` a whole number 1 to 16): | Function | Arguments | Returns | | --- | --- | --- | | `ac_sealed_fc(fs, vas, vb)` | Hz, litres, litres | sealed-box resonance, Hz | | `ac_sealed_qtc(qts, vas, vb)` | none, litres, litres | sealed-box total Q | | `ac_port_length(vb, fb, dv, np)` | litres, Hz, mm, count | length of each port, mm, end correction (0.732 d) included | | `ac_helmholtz_fb(vb, dv, lv, np)` | litres, mm, mm, count | tuning, Hz | `driver Name { ... }` (top level; values are children, a number or a CEL expression): `fs` Hz, `qts`, `vas` litres, `sd` cm2, `xmax` mm, `re` ohms, `sensitivity_db` dB at 1 W 1 m, `tolerance` a fraction 0 to under 1 (default none). There is no `unit=`. `analysis Name kind=sealed|ported driver=Name volume= [drive_watts=W] { range LO HI; points N }`. `kind=ported` also needs `port_diameter=` (mm) and exactly one of `port_length=` (mm, reports the tuning it makes) or `tuning=` (Hz, reports the length it needs), plus optional `ports=` (count, default 1). `points` is at most 512 (default 256); more is an error, not clamped. `range` defaults to 20 to 2000 Hz. ## Example ```kdl project name="Sealed monitor" units=mm schema_version=1 { params { net_litres 20.0 min=5.0 max=80.0 step=1.0 label="Net volume" category="Acoustics" qtc "ac_sealed_qtc(0.35, 45.0, net_litres)" } materials { material mdf type=sheet } driver Woofer { fs 32.0 qts 0.35 vas 45.0 sd 132.0 xmax 6.0 re 5.5 sensitivity_db 88.0 tolerance 0.15 } analysis Response kind=sealed driver=Woofer volume=net_litres { range 20.0 2000.0 points 128 } component Panel role=side { stock mdf 300.0 400.0 18.0 } assembly Root { part Side instance=Panel { by top_sw pos x=0.0 y=0.0 z=0.0 } } } ``` ## Pitfalls and diagnostics - **It is an estimate.** Every report has a `fidelity` object (`grade: "estimate"`) listing what is modelled and not: baffle step, diffraction, room, port compression, cabinet resonances, crossover, and leakage beyond an assumed Ql of 7. Say so when you quote a figure. The tolerance band is the min and max over the 2^3 corners of fs, qts and vas. - **f3 is the half-power point**, -3.0103 dB, not -3 dB. - **`port_length` cannot be derived from `ac_port_length`** when the net volume depends on the port's own displacement: that is a cycle and a hard error. Keep `port_length` authored and use the function in an advisory parameter, as `examples/speaker_ported.kdl` does. - **`kind=transmission_line` is not built** (`not yet` diagnostic), nor are horns, passive radiators and band-pass boxes: this is why the file is `partial`. `volume=` cannot name a metrics volume yet. - The inline `1.2 r` end correction in an older file differs from the library's `0.732 d` by about 1 Hz on a 33 Hz tuning. - A function name is `ac_sealed_fc`, never `ac.sealed_fc`: method calls are refused. ## See also - `docs/KDL_AUTHORING.md` section 18, `docs/adr/0037-acoustic-functions-and-analysis.md`. - [`project-and-params`](project-and-params.md) for CEL and the function table. - `examples/speaker_sealed.kdl`, `examples/speaker_ported.kdl`. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `driver` project-level node, word-valued property; node in 2 of 116 examples (2 uses); property in 2 of 116 examples (2 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl driver Woofer { fs "driver_fs" qts "driver_qts" vas "driver_vas" sd 132.0 // cm^2: a 6.5 in cone's effective piston area xmax 6.0 // mm, one-way linear excursion re 5.5 // ohms, DC resistance sensitivity_db 88.0 // dB SPL, 1 W / 1 m tolerance 0.15 // +/-15 % on Fs, Qts and Vas } ``` ### `analysis` project-level node; node in 2 of 116 examples (2 uses). Shortest real use, from `speaker_sealed.kdl`: ```kdl analysis Response kind=sealed driver=Woofer volume=vb_net_liters drive_watts=5.0 { range 20.0 2000.0 points 256 } ``` ### `ac_sealed_fc` expression function; not used by any example yet. ### `ac_sealed_qtc` expression function; not used by any example yet. ### `ac_port_length` expression function; not used by any example yet. ### `ac_helmholtz_fb` expression function; not used by any example yet. ### `fs` node in 4 of 116 examples (4 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl fs "driver_fs" ``` ### `qts` node in 2 of 116 examples (2 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl qts "driver_qts" ``` ### `vas` node in 2 of 116 examples (2 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl vas "driver_vas" ``` ### `sd` node in 2 of 116 examples (2 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl sd 132.0 ``` ### `xmax` node in 2 of 116 examples (2 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl xmax 6.0 ``` ### `re` node in 2 of 116 examples (2 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl re 5.5 ``` ### `sensitivity_db` node in 2 of 116 examples (2 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl sensitivity_db 88.0 ``` ### `tolerance` property; node in 2 of 116 examples (2 uses); property in 3 of 116 examples (36 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl tolerance 0.15 ``` ### `drive_watts` property; property in 2 of 116 examples (2 uses). Shortest real use, from `speaker_sealed.kdl`: ```kdl analysis Response kind=sealed driver=Woofer volume=vb_net_liters drive_watts=5.0 { range 20.0 2000.0 points 256 } ``` ### `port_diameter` property; property in 1 of 116 examples (1 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl analysis Response kind=ported driver=Woofer volume=vb_net_liters \ port_diameter="2.0 * port_radius" port_length=port_length drive_watts=5.0 { range 15.0 2000.0 points 256 } ``` ### `ports` not used by any example yet. ### `tuning` not used by any example yet. ### `range` node in 2 of 116 examples (2 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl range 15.0 2000.0 ``` ### `points` node in 2 of 116 examples (2 uses). Shortest real use, from `speaker_ported.kdl`: ```kdl points 256 ``` --- # Style & Finish Rule *A style rule keyed to the declared finish* **Names:** `finish` `one_of` ## When to use Use the `finish { one_of ... }` predicate when writing a style rule file (`styles/.kdl`) and the style guide names the surface treatments of its period: what the pieces were painted, stained, oiled or shellacked with. It grades the `finish=` text a project already declares (RFC-0122), so a design that says `finish="'milk paint, barn red'"` can be told it is inside, or outside, the finishes the guide records. It is advice, like every style rule. A part with no finish is not graded, and a project where no part declares one reports "not applicable", never a pass. ## Syntax In a rule file: ```text rule recorded-finishes level=may cite=36 { finish { one_of paint painted stain stained varnish oil } } ``` - `one_of` lists one or more words; at least one is required. - Each part that declares a finish (its own `finish=`, else its material's) is split into lower-cased alphanumeric words. The part passes when any `one_of` word equals one of them as a **whole word**: `paint` matches "milk paint, barn red" and does not match "repainted". List the inflections you mean (`stain stained`, `fumed fuming`). - Generated parts (dowels, loose tenons) are skipped. - `cite=` is a line of the guide that records the finish; `level=` is `may` unless the guide forbids the others. The two shipped rules are `recorded-finishes` in `styles/traditional-shaker.kdl` (guide line 36) and `fumed-shellac-wax` in `a style file under styles/` (guide line 28). ## Example A project the Shaker rule grades. The part inherits "milk paint" from its material and passes; the second part overrides it with a finish the guide does not record and is reported by name. ```kdl project name="Finished Pair" units=mm schema_version=1 { params { top_w 500.0 min=300.0 max=900.0 step=10.0 label="Top Width" category="Envelope" } materials { material Poplar type=hardwood finish="'milk paint, barn red'" } assembly Root { part Painted material=Poplar role=top { stock Poplar top_w 400.0 20.0 by top_sw pos x=0.0 y=0.0 z=0.0 } part Plasticised material=Poplar role=shelf finish="'catalysed lacquer'" { stock Poplar top_w 300.0 20.0 by top_sw pos x=0.0 y=0.0 z=100.0 } } } ``` Run it with `ww style finished-pair.kdl --style shaker`: `recorded-finishes` scores 1 of 2 and reports `Root/Plasticised` is finished "catalysed lacquer", not one of the listed words. ## Pitfalls and diagnostics - **A passing `finish` rule is weak evidence by itself.** It proves the words, not the work: a part can say "oil" and be unfinished. Pair it with rules about shape and joinery. - **Cite the guide, do not infer.** Take the words from the cited lines. Mission's rule omits `oil` because the guide never names it for that style; adding it would be inventing history. When a guide records no finish, write no rule. - **A bare word in `finish=` is a CEL identifier.** Write `finish="'milk paint'"` with the inner quotes (ADR-0004), as in the example. - **Not applicable is not a pass.** An example with no `finish=` anywhere reports "no part declares a `finish=`" and contributes nothing to the score; an example whose only finish is outside the set can score 0/100 (`welsh_dresser` against Mission). - `kdl-check` does not read rule files. A rule file error (an empty `one_of`) shows when you run `ww style --style `, which exits 2. ## See also - `docs/adr/0029-style-finish-rule.md` - `docs/rfc/0122-finish-attribute.md` (the `finish=` attribute this rule reads) - `styles/README.md` (the predicates), `docs/styles/traditional-shaker.md`, `a style guide under docs/styles/` - `docs/agent-reference/features/grain-axis.md` ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `finish` property; property in 78 of 116 examples (147 uses). Shortest real use, from `farmhouse_harvest_table.kdl`: ```kdl material Pine type=softwood finish="'none'" ``` ### `one_of` not used by any example yet. --- # Tenon Tips & Coped Rails *Mitred tenon tips, round-tenon wedges, and coped stile-and-rail joints* **Names:** `tip` `wedge` `stile_and_rail` Three gaps in the tenon family, closed by ADR-0026. Source of truth for the language is `docs/KDL_AUTHORING.md` §8 (the joinery section); this page is the agent's quick card. ## When to use - **`tip mitre=#true`** — two rails tenon into the *same* post from different faces (a desk's apron and side apron into one leg; a chair's seat rail and side rail). Their tenons would cross inside the leg; mitring the tips lets each be long. Without it the `tenons-meet` rule refuses the pair. - **`wedge` on a `round_tenon`** — a round tenon driven through its mortised part and wedged in a kerf (a stool leg through its seat, a chair stretcher through a leg). The rectangular `mortise_and_tenon` has had `wedge` since M15; this is the round one. - **`profile "ogee"` (or any moulding) on `stile_and_rail`** — a cope-and-stick door or frame: the inside edges are stuck with a moulding, and each rail end is coped to it. `profile "square"` (the default) is still the flat frame. ## Syntax Mitred tips — one line in the body of **both** joints that meet: ```text joinery mortise_and_tenon name=Apron_to_Leg { tenon_thickness 10.0 tenon_length 48.0 clearance 0.2 tip mitre=#true } ``` Round-tenon wedge — the tenon must be through (`through #true`, or `length` reaching the far face); the kerf is sawn across the grain of the mortised part: ```text joinery round_tenon name=Leg_to_Seat { diameter 22.0 length 40.0 through #true wedge kerf=3.0 depth=18.0 flare=2.0 flare_depth=10.0 } ``` Stuck profile — `profile` is a moulding name, `profile_size` its projection in mm, and `profile_face` the way the frame's show face looks **in the assembly's axes**, written as a CEL string (`"'neg_y'"`): ```text profile "ogee" // ovolo cove ogee thumbnail half_round cockbead; "square" is flat profile_size 5.0 profile_face "'neg_y'" // pos_x neg_x pos_y neg_y pos_z neg_z ``` ## Example A single door, stiles and rails 20 mm thick, the front (`neg_y`) stuck with a 5 mm ogee. The right stile is the left turned half round and the bottom rail is turned over; the sticking still lands on the front of all four members, because `profile_face` is a world direction. ```kdl project name="stuck_door" units=mm schema_version=1 { materials { material Softwood type=softwood } component DoorStile { stock Softwood 60.0 20.0 500.0 groove target=panel face=pos_x width=8.0 depth=10.0 offset=6.0 mark RailSeat_Top x=60.0 y=10.0 z=-30.0 normal=pos_x mark RailSeat_Bot x=60.0 y=10.0 z=-470.0 normal=pos_x } component DoorRail { stock Softwood 280.0 20.0 60.0 groove target=panel face=neg_z width=8.0 depth=10.0 offset=6.0 mark Stile_Left x=0.0 y=10.0 z=-30.0 normal=neg_x mark Stile_Right x=280.0 y=10.0 z=-30.0 normal=pos_x } joinery stile_and_rail name=DoorFrameJoint { groove_width 8.0 groove_depth 10.0 stub_tenon_length 12.0 profile "ogee" profile_size 5.0 profile_face "'neg_y'" } assembly Root { part DoorStile_L instance=DoorStile { pos x=0.0 y=0.0 z=0.0 } part DoorStile_R instance=DoorStile { pos x=400.0 y=20.0 z=0.0 rot z=180.0 } part DoorRail_Top instance=DoorRail { pos x=60.0 y=0.0 z=0.0 } part DoorRail_Bot instance=DoorRail { pos x=60.0 y=20.0 z=-500.0 rot x=180.0 } join joint=DoorFrameJoint male=DoorRail_Top@Stile_Left female=DoorStile_L@RailSeat_Top join joint=DoorFrameJoint male=DoorRail_Top@Stile_Right female=DoorStile_R@RailSeat_Top join joint=DoorFrameJoint male=DoorRail_Bot@Stile_Left female=DoorStile_L@RailSeat_Bot join joint=DoorFrameJoint male=DoorRail_Bot@Stile_Right female=DoorStile_R@RailSeat_Bot part DoorPanel role=panel { stock Softwood 296.0 8.0 396.0 pos x=52.0 y=6.0 z=-52.0 } } } ``` ## Pitfalls and diagnostics - `tenons-meet` — two tenons run into each other inside one part. Shorten them, or write `tip mitre=#true` on **both** joints. `tip-mitre` — the mitre needs tenons that meet at an angle (parallel axes have no bisector), and the bisector plane may not cut a tenon's own rail: use shorter tenons. The mortises are **not** clipped to the mitre (they stay the full rectangular pockets); only the male tips are. - `wedge-kerf` — the wedge needs a *through* round tenon (`through #true`); `depth` must be at least `flare_depth` and at most three quarters of the tenon's run through the mortised part; a kerf may be no closer than 1.5× its width to the tenon's surface or another kerf. `wedge-grain` — the wedges spread the tenon along the mortised part's **grain** (spreading across it splits the part): declare `grain` on it, otherwise the longer of its two faces square to the tenon is taken; a tenon in end grain is refused. `wedge-flare`, `wedge-frame` — see `docs/KDL_AUTHORING.md`. - `params` — a stuck `profile` needs both `profile_size` and `profile_face`; an unknown profile or face word is named. `profile_size`/`profile_face` with `profile "square"` is only a warning (they do nothing). - `cope-profile` — the sticking would cut into the panel groove (`profile_size` larger than the wood between the show face and the groove: for a 20 mm member with an 8 mm groove, 6 mm), is deeper than half the rail's width, `profile_face` names a narrow edge of the rail or a way no member's side face looks, or the shoulder is off square. - `profile_face` is a **world** direction, never part-local: a door's right stile and bottom rail are usually the same component turned. Check the face against the *assembly*. - The cope has no clearance: `clearance` belongs to the groove (D8, ADR-0012). The cope is the stile's sticking void exactly, to the chord of the 16-segment section. ## See also - `docs/KDL_AUTHORING.md` §8 — "Mitred tips", the round `wedge`, and the `stile_and_rail` stuck-profile paragraph. - `docs/adr/0026-tenon-tips-wedges-and-coped-rails.md` — the three decisions and their evidence; RFC-0041, RFC-0126, RFC-0127. - `docs/agent-reference/nodes.md` — the node index. - Examples: `examples/desk.kdl` and `examples/dining_chair.kdl` (`tip mitre=#true`); `docs/styles/models/stile-and-rail.kdl` (the glossary's frame joint). ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `tip` node in 15 of 116 examples (22 uses). Shortest real use, from `deco_console_table.kdl`: ```kdl tip mitre=#true ``` ### `wedge` node in 6 of 116 examples (8 uses). Shortest real use, from `wedged_post_and_rung_stool.kdl`: ```kdl wedge kerf=3.0 depth=18.0 flare=2.0 flare_depth=10.0 ``` ### `stile_and_rail` joinery kind; value in 9 of 116 examples (9 uses). Shortest real use, from `ebonised_incised_cabinet.kdl`: ```kdl joinery stile_and_rail name=PanelFrame { groove_width panel_th groove_depth groove_d stub_tenon_length stub_l profile "square" } ``` ## Related terms - [Kusabi](/almanac/techniques#kusabi) - [Rail](/almanac/techniques#rail) - [Stile](/almanac/techniques#stile) - [Stile-and-rail joint](/almanac/techniques#stile-and-rail) - [Wedged tenon](/almanac/techniques#wedged-tenon) --- # Tongue & Groove *A tongue cut from one board locking into a groove in another* **Names:** `tongue_and_groove` ## When to use Use `tongue_and_groove` when one board's edge or end must lock flat into another's: floor and panelling boards, and above all the **breadboard end** of a solid tabletop, where a cap board runs across the end grain of the field boards. The tongue is the male board's own wood (an `Add` on its face, like a tenon); the groove is a blind cut in the female. Nothing is a loose spline. For a tongue that locks by its shape use `sliding_dovetail`; for a stub that goes into a pocket rather than a full-length groove use `mortise_and_tenon`. ## Syntax ```kdl grammar joinery tongue_and_groove name= { tongue_thickness // across the male's thickness, centred on the male mark (default 10, 3 to 40) depth // how far the tongue stands out of the shoulder (default 12, 3 to 40) clearance // optional; widens the groove only, never the tongue (ADR-0013) slot diameter= length= at= // optional: elongated peg hole through the tongue } join joint= male=@ female=@ ``` - The male mark sits on the face the tongue stands out of, with its normal pointing out. The tongue spans the face's longer extent (the run) and is `tongue_thickness` across the shorter one (the flare), centred on the mark. - The groove is `tongue_thickness + clearance` wide and `depth + clearance / 2` deep, over the tongue's full run, so one groove per field board merges into the one continuous groove a breadboard end needs. - `slot` is a stadium cut through the tongue's thickness, long axis along the run. `length` is the overall length (`diameter` plus twice the travel); `at` is the distance out of the shoulder (default `depth / 2`). Glued versus sliding is two `joinery` declarations, one with `slot` and one without, each named by the `join`s that want it. ## Example ```kdl project name="Breadboard end" units=mm schema_version=1 { materials { material Oak type=hardwood } component FieldBoard { stock Oak 100.0 50.0 20.0 mark End x=50.0 y=50.0 z=-10.0 normal=pos_y } component Cap { stock Oak 100.0 50.0 20.0 mark Groove x=50.0 y=0.0 z=-10.0 normal=neg_y } joinery tongue_and_groove name=Sliding { tongue_thickness 7.0 depth 10.0 clearance 0.1 slot diameter=4.0 length=8.0 at=5.0 } assembly Top { part Field instance=FieldBoard { pos x=0.0 y=0.0 z=0.0 } part Breadboard instance=Cap { pos x=0.0 y=50.0 z=0.0 } join joint=Sliding male=Field@End female=Breadboard@Groove } } ``` `examples/dining_table.kdl` is the full case: eight field boards, `BreadboardEndGlued` for the centre boards and `BreadboardEndSliding` for the outer ones, with the cap's round peg holes cut as ordinary `hole`s. ## Pitfalls and diagnostics - The slot is a feature of the tongue, not a `hole` on the field board: a `hole` there would land in the visible tabletop, because the board as modelled has no tongue to carry it. - The peg is not generated. `slot` and the cap's `hole` are the two holes it passes through. - `params` (error): `tongue_thickness` or `depth` missing or out of range. - `material` (error): the tongue is as thick as its board, or the groove does not fit across the female. - `facing`, `contact` (error): the male's shoulder meets the female off square, or does not bear on its face. - `slot` (error): not `diameter=`/`length=`/`at=`; `length` below `diameter`; the slot leaves the tongue along its depth or run. - `remaining-wall` (warning): less than `max(3 mm, 15 %)` of the female is left beside the groove. `tongue-overhang` (warning): the tongue's run is longer than the face it goes into. - One slot per joint; a very wide board that needs two is two joints at two marks. ## See also - `docs/adr/0022-tongue-and-groove.md` and `docs/rfc/0030-breadboard-elongated-mortises.md` - `docs/KDL_AUTHORING.md` section 8 (joinery kinds) - `docs/agent-reference/nodes.md` (the node index) - `docs/styles/models/tongue-and-groove.kdl` and `breadboard-end.kdl` (the glossary models) ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `tongue_and_groove` joinery kind; value in 4 of 116 examples (7 uses). Shortest real use, from `farmhouse_harvest_table.kdl`: ```kdl joinery tongue_and_groove name=CapGlued { tongue_thickness 10.0 depth tongue_depth } ``` ## Related terms - [Groove](/almanac/techniques#groove) - [Tongue and groove](/almanac/techniques#tongue-and-groove) --- # Travel *Parts that move: slides, hinges, and screws swept as a clearance check* **Names:** `travel` `body` `pivot` ## When to use Use it when a part is meant to move and you want the compiler to prove it can: a drawer on its slides, a door or lid on its hinges, a vise chop on its screw. A `travel` does **not** move anything in the drawing or change a cut: the document is drawn at `rest` and every part keeps its one transform. The viewer shows a slider per travel that poses the parts for a look (a view, never the cut list). It adds a check, which carries the moving body through its range and fails if it meets any other part on the way. If you only need the hardware counted or recessed, use `hardware_mount` / `hardware_use` ([`hardware-mounting`](hardware-mounting.md)). A part that is *fixed* in a different position (a drawer drawn half open) is just placed there. Keywords: slide, drawer, runner, hinge, swing, door, lid, vise, screw, travel, sweep, clearance, open, extension, pivot, collision. ## Syntax A `travel` is the one child a `join` may have. The **male** end of the join moves; the female is the part it is carried on. ```text join joint= male=Part@Mark female=Part@Mark { travel [kind=slide|angle] axis=x|y|z|-x|-y|-z [min=] [max=] [rest=] [step=] [body=part|assembly] [pivot=] } ``` | Property | Meaning | Default | | --- | --- | --- | | `kind` | `slide` moves along the axis (mm); `angle` turns about it (degrees, right-handed) | `slide` | | `axis` | `x`, `y` or `z`, signed to reverse (`-y`), in the frame of the assembly the `join` is written in | required | | `min`, `max` | the limits, CEL numbers | `min` 0; `max` the item's `travel_distance` (slide) or `max_angle` (angle), else an error | | `rest` | the pose the document is drawn in; must lie in `[min, max]` | `min` | | `step` | sweep spacing | 10 mm for a slide, 5 degrees for an angle | | `body` | `part` moves the male part alone; `assembly` moves every part under its innermost enclosing assembly | `part` | | `pivot` | `angle` only: a mark **on the male part** the axis passes through | the male end's own mark | Catalogue numbers a `travel` reads, on the `hardware` item the joinery names: `travel_distance` (a slide's reach), `max_angle` (a hinge's swing), and `mount_gap`, the gap a slide fills between the two parts it joins. With a `mount_gap` the check also measures the real gap and fails (`ww::check::slide_clearance`) if it is more than 0.5 mm from the catalogue's; the margin is printed by `ww check` and is in its JSON (`invariants.travel.slide_margins`). ## Example ```kdl project name="Lidded box" units=mm schema_version=1 { materials { material Oak type=hardwood hardware Hinge leaf_length=50.0 leaf_width=16.0 leaf_thickness=2.0 max_angle=110.0 } joinery hardware_mount name=LidHinge { hardware Hinge } assembly Box { part Case { stock Oak 100.0 60.0 20.0 mark Seat x=0.0 y=60.0 z=0.0 normal=pos_z by top_sw pos x=0.0 y=0.0 z=0.0 } assembly Lid { part Panel { stock Oak 100.0 60.0 10.0 mark Seat x=0.0 y=60.0 z=-10.0 normal=neg_z // the hinge pin's line: the lid's back edge, on its underside mark Pin x=0.0 y=60.0 z=-10.0 normal=neg_z by top_sw pos x=0.0 y=0.0 z=10.0 } part Cleat { stock Oak 100.0 10.0 10.0 by top_sw pos x=0.0 y=0.0 z=0.0 } // Swings the whole lid (both parts) up to the hinge's 110 degrees. join joint=LidHinge male=Panel@Seat female=Case@Seat { travel kind=angle axis=x body=assembly pivot=Pin } } } } ``` A drawer on slides, from `examples/welsh_dresser.kdl`: the drawer is its own assembly, each side stands on a slide over its runner, and the catalogue gives the reach and the gap: ```text hardware UndermountSlide_350 travel_distance=300.0 mount_gap=12.0 load_kg=35.0 ... join joint=DrawerSlide male=DrawerSide_L@Slide female=Runner_L@SlideEdgeL { travel axis=-y body=assembly } ``` ## Pitfalls and diagnostics - **Which end moves.** The male end. `body=part` (the default) moves that one part, which is right for a vise chop and wrong for a door: write `body=assembly` and put the door's parts in an `assembly`. A drawer is the same: wrap the front, sides, back and bottom in one. - **A join can only name parts of its own assembly or one enclosing it**, so the slide joins of a drawer live in the drawer's assembly and reach out to the runner; the drawer must be a nested inline `assembly`, not `part X instance=`. - **Two joins, one body.** Two hinges on one door each carry the same `travel`; it is swept once. Declare it on both so either can be deleted. - **Where the pin is matters.** The axis passes through the `pivot` mark, or the male end's own mark. A hinge mark in the middle of a door's edge puts the pin in the middle of its thickness and the door's back corner swings into the carcass; mark the pin on the show face (`welsh_dresser.kdl`'s `Pin_Front` / `Pin_Back`). - **`ww::check::travel_foul`** (defect): a moving part overlaps a fixed one. The message names both parts, the first *sampled* travel that fouls, and the onset between that sample and the last clean one. A pair already overlapping at `rest` is `ww clash`'s finding and is skipped. - **`ww::check::travel_invalid`** (defect): the body moves nothing, `min` is above `max`, `rest` is outside the range, the pivot mark does not exist, or there is no `max` to default. - **`ww::check::slide_clearance`** (defect): the gap a slide fills is not its `mount_gap`. - **`ww::check::travel_unchecked`** (defect): the kernel could not intersect a pair; it is not reported clean. - **`ww::expand::bad_travel`** (error): an unknown property, axis, kind or body word, a missing `axis=`, or `pivot=` on a slide. - **Sampling is not a proof.** An obstacle thinner than `step` between two samples is missed; shrink `step=`. The default (10 mm, 5 degrees) is below every board in the library. - **Hardware is never swept.** A screw, garter, guide, hinge or slide is a catalogue item and a display proxy, never a part, so it cannot foul anything. Model what must be checked as a part. - **No dynamics.** No forces, wear, springs, door sag or thread geometry (ADR-0075). - The viewer does not yet have a slider per movable part (#225); the data (axis, limits, rest, moving part ids) is in `PlacedJoin::travel`. ## See also - `docs/adr/0075-travel-primitive.md`; `docs/rfc/0081-vise-and-moving-hardware.md`, `docs/rfc/0050-drawer-runners-and-slides.md`, `docs/rfc/0071-hardware-hinges.md` - `docs/KDL_AUTHORING.md` §5 (`hardware`) and §8 (`join`) - `examples/workbench.kdl` (a vise chop, 0 to 180 mm), `examples/welsh_dresser.kdl` (slides and door swings), `examples/blanket_chest.kdl` (a lid swung 100 degrees) - [`hardware-mounting`](hardware-mounting.md), [`joinery-contract`](joinery-contract.md) ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `travel` node in 3 of 116 examples (8 uses). Shortest real use, from `welsh_dresser.kdl`: ```kdl travel axis=-y body=assembly ``` ### `body` word-valued property; property in 2 of 116 examples (7 uses). Shortest real use, from `welsh_dresser.kdl`: ```kdl travel axis=-y body=assembly ``` ### `pivot` word-valued property; property in 2 of 116 examples (3 uses). Shortest real use, from `welsh_dresser.kdl`: ```kdl travel kind=angle axis=z body=assembly pivot=Pin_Back ``` --- # Turning & Sections *Faceplate turning, profile sections, and turned-part cards* **Names:** `stock` `section` `turn` `profile` ## When to use - A part with a **round** cross-section made on a lathe: a spindle, baluster, leg, bowl, platter, pedestal-table top or foot. - A **rectangular member that changes size along its length**: a tapered spoke or a leg narrowing to a foot. - You want the cut list to say how the grain runs on a disc (`across axis (faceplate)`). Not for a flat round outline with the faces left flat (use `cut_path`), nor for a curved (barrel or ogee) rectangular profile (not built: RFC-0101 stays partial for that alone). ## Syntax `section` defines the part's **base solid**, the role `stock`'s cube plays for every other part. `circle`/`profile` turned sections are `axis=z` only (one spindle axis). A `rect` section may sweep `axis=z` (default), `axis=x` or `axis=y` (#459, ADR-0034's "not done" bullet — now built): the physical bound names `rect` and `at` take follow the sweep axis. | `axis=` | `at` key | `rect` bounds | sweep runs | | --- | --- | --- | --- | | `z` (default) | `z=` | `x_min= x_max= y_min= y_max=` | `z = 0` to `z = -size.z` (top face to foot) | | `x` | `x=` | `y_min= y_max= z_min= z_max=` | `x = 0` to `x = size.x` | | `y` | `y=` | `x_min= x_max= z_min= z_max=` | `y = 0` to `y = size.y` | Stations and bounds are otherwise in the part's own frame (D4): for `axis=z`, `z = 0` is the top face and down is negative. ```kdl grammar stock shape=round diameter= z= [grain=across] // round blank stock // rect blank section axis=z [interp=linear|spline] [samples=] { at z= { circle r= } // spindle: radius at a station } section axis=z { profile { line … arc … polyline { point x= y= } } } // faceplate: drawn outline section axis=z { at z= { rect x_min= x_max= y_min= y_max= } } // tapered rect member ``` - **Spindle (`circle` stations).** Grain is derived lengthwise. `interp=linear` joins the stations; `interp=spline` (`samples=`, default 64) draws a centripetal curve through them. - **Faceplate (`grain=across`).** The blank is a disc cut from a board; the grain crosses the axis. Pair it with a `profile`: a closed outline in **(radius, z)** (`x` is the distance from the axis), written with the same `line` / `arc` / `polyline` children as `cut_path`, chained end to start. The **closing edge back to the first point is implied**, so write the outline from the axis at one face, out around the profile, and back to the axis. Winding is normalised; `interp=` and `samples=` do not apply; a `section` is stations or a `profile`, never both. - **Curved rect member (`interp=spline` on `rect` stations, ADR-0034).** Each of the four bounds is a smooth curve through its keyframes; `samples=` (chords per interval) or `tolerance=` (default 0.01) sets the fineness, one or the other. See the worked case in `examples/trestle_table.kdl`. - **Tapered rect member (`rect` stations).** Straight between keyframes. Stations must run the whole part (the sweep axis's own `0` to its far face; repeat a rectangle to hold a run constant), and no rectangle may leave the blank. `rect` bounds are corner-anchored: for a taper that keeps one face flush, hold the lower bound at `0.0` on every station; for a centred taper, inset both sides equally. - **`axis=x` / `axis=y` member (a curved apron or shaped rail, #459).** The apron's own length runs along its *local* sweep axis, whichever way it is placed in the assembly (a side apron turned 90 degrees still sweeps its own `axis=x`, never a second `axis=y` case). Worked case: `examples/curved_apron_side_table.kdl`'s `Apron` (`axis=x interp=spline`, full height where it tenons into a leg, waisted thinner at mid-span). - A square pommel over a turning (square `stock` plus `circle` stations that stop short of the top) is the RFC-0127 form in `docs/KDL_AUTHORING.md` §6. ## Example ```kdl project name="Turned And Tapered" units=mm schema_version=1 { materials { material Maple type=hardwood material Oak type=hardwood } // A faceplate bowl: a disc cut from a board, grain across the lathe axis. The // profile is a closed outline in (radius, z); the edge down the axis is implied. component Bowl role=bowl { stock Maple shape=round diameter=200.0 z=70.0 grain=across section axis=z { profile { polyline { point x=0.0 y="0.0 - 70.0" point x=60.0 y="0.0 - 70.0" point x=100.0 y="0.0 - 20.0" point x=100.0 y=0.0 point x=92.0 y=0.0 point x=0.0 y="0.0 - 60.0" } } } } // A spindle: stations of diameter, grain derived lengthwise. component Baluster role=baluster { stock Maple shape=round diameter=40.0 z=300.0 section axis=z interp=spline { at z=0.0 { circle r=14.0 } at z="0.0 - 60.0" { circle r=20.0 } at z="0.0 - 150.0" { circle r=10.0 } at z="0.0 - 300.0" { circle r=16.0 } } } // A tapered rectangular member: straight between keyframes, whole length. component Spoke role=spoke { stock Oak 44.0 44.0 420.0 section axis=z { at z=0.0 { rect x_min=0.0 x_max=44.0 y_min=0.0 y_max=44.0 } at z="0.0 - 40.0" { rect x_min=0.0 x_max=44.0 y_min=0.0 y_max=44.0 } at z="0.0 - 420.0" { rect x_min=7.0 x_max=37.0 y_min=7.0 y_max=37.0 } } } assembly Set { part Bowl1 instance=Bowl { by top_center pos x=0.0 y=0.0 z=0.0 } part Baluster1 instance=Baluster { by top_center pos x=300.0 y=0.0 z=0.0 } part Spoke1 instance=Spoke { by top_sw pos x=500.0 y=0.0 z=0.0 } } } ``` The compiled cut list prints `across axis (faceplate)` for the bowl, `along axis` for the baluster and `lengthwise` for the spoke. Each turned part's drawing card carries a `TURNED` line with one `depth below top → ⌀` pair per authored station (a tapered member prints `→ width × thickness`); on a `rect` section, "depth below top" is `axis=z`'s own wording — an `axis=x`/`axis=y` member's card instead reads "depth from `{entry face}`" (e.g. `depth from x=0`), since it has no top or foot. Full worked examples: `examples/pedestal_table.kdl` (turned), `examples/curved_apron_side_table.kdl` (`axis=x` `rect` section). ## Pitfalls and diagnostics - **`grain=across` on rect stock** is refused: rect stock states grain as its fifth positional argument. On round stock `across` is the only value. - **"declared blank is …mm across, but the `profile` reaches …"**: the `diameter` must cover the widest point. Likewise a profile taller than `z=`. - **"reaches x=-…"**: `x` is a distance from the axis and cannot be negative; the kernel would silently clip it, so it is refused. - **"does not close into a loop that encloses an area"**: segments must chain end to start, and the outline must not cross itself. Do not repeat the first point at the end. - **A `spline` child inside `profile`** is not a segment; use `line`, `arc` or `polyline`. - **"a `rect` section must run the whole part"**: add a station at `z=0.0` and one at `z="0.0 - "`. - **"leaves the blank"**: a section only removes material; widen the `stock`. - **`circle` beside `rect`**: one `section` is one shape family. - **"overshoots the blank"** (`interp=spline` on `rect` stations): the smooth curve bulged past the stock between keyframes. Add a keyframe where the curve turns, or declare the blank wider. A curve that "doubles back in `z`", or "pinches the member", is refused too. - **`samples=` / `tolerance=` without `interp=spline`**, or both together, are refused. - A `kdl-check` warning on `by top_sw` (or any corner/edge anchor) of a round part: use `top_center`, `bottom_center` or `center`, which are the axis. - Turning is a bench operation in the fabrication pack: no CNC setup is generated for it. - A mark on the curved surface of a turning is not reachable; only the flat ends (`normal=pos_z` / `neg_z`) are. ## See also - `docs/KDL_AUTHORING.md` §6 *Turned stock*, and §7 for `taper` (still supported; a `rect` section builds the same solid, and `examples/wagon_wheel.kdl`'s spoke now uses one) - `docs/adr/0018-turned-stock.md`, `docs/adr/0031-faceplate-turning-and-profile-sections.md`, `docs/adr/0034-curved-rect-sections.md` - `docs/rfc/0129-faceplate-and-profile-path-turning.md`, `docs/rfc/0101-functional-sections.md`, `docs/rfc/0127-round-tenon.md` - `docs/superpowers/specs/2026-10-11-curved-rect-section-axis-x-design.md` (#459, `axis=x`/ `axis=y` on a `rect` section) - `examples/pedestal_table.kdl`, `examples/chess_pawn.kdl`, `examples/spindle.kdl`, `examples/wagon_wheel.kdl`, `examples/curved_apron_side_table.kdl` ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `stock` feature; node in 114 of 116 examples (910 uses). Shortest real use, from `glazed_vitrine_whiplash_crest.kdl`: ```kdl stock Glass dw 6.0 dh ``` ### `section` feature, word-valued property; node in 47 of 116 examples (93 uses); property in 18 of 116 examples (36 uses). Shortest real use, from `pop_laminate_stepped_sideboard.kdl`: ```kdl section axis=z interp=linear { at z=0.0 { circle r="drum_d / 2.0" } at z="0.0 - drum_h" { circle r="drum_d / 2.0" } } ``` ### `turn` not used by any example yet. ### `profile` word-valued property; node in 11 of 116 examples (12 uses). Shortest real use, from `arts_crafts_cabinet_on_stand.kdl`: ```kdl profile "square" ``` ## Related terms - [Baluster turning](/almanac/techniques#baluster-turning) - [Bamboo turning](/almanac/techniques#bamboo-turning) - [Round tenon](/almanac/techniques#round-tenon) - [Spindle](/almanac/techniques#spindle) - [Turned leg](/almanac/techniques#turned-leg) - [Turning](/almanac/techniques#turning) --- # Veneer & Laminate *Laminate faces, book match, and slip match veneer* **Names:** `veneer` `match` ## When to use A show face covered in a thin leaf of another species (veneer) or a sheet good (a laminate), bonded to a solid substrate. The host keeps its declared dimensions; each leaf is its own generated part with its own cut-list row. Not for solid banding or stringing inlays: another file in `INDEX.md` may cover those, and it extends this one. ## Syntax ```text veneer face= material= [thickness=] [grain_angle=] [leaves=] [match=book|slip|sunburst] ``` | Property | Meaning | Default | | --- | --- | --- | | `face=` | Part-local face word (`pos_x neg_x pos_y neg_y pos_z neg_z`) the leaves bond to. | `pos_z` | | `material=` | The leaf's species, or a `type=sheet` material for a **laminate**. A bare word, never a CEL expression; must be declared in `materials`. | required | | `thickness=` | Finished leaf thickness, mm. Real veneer is about 0.6 (paper-backed) to 3 (hand-sawn). | 0.6 | | `grain_angle=` | Degrees off the host's length axis; recorded on the cut list. A book-matched mirrored half reads at the opposite angle. | 0.0 | | `leaves=` | Leaves laid side by side, edge to edge, no gap. | 1, or 2 once `match=` is given | | `match=` | `book` (alternate leaves mirror about each seam; exact mirror, ADR-0019) or `slip` (repeat, unmirrored), or `sunburst` (leaves laid as a fan, RFC-0141; see [`banding-and-stringing`](banding-and-stringing.md)). | none (single leaf), `slip` when `leaves` > 1 | - **A laminate is the same node**: give `material=` a `type=sheet` material. There is no separate `laminate` node. - `veneer` cuts nothing on the host. Each leaf is a generated part (never written back to the source), bonded not joined: no `join` names it and it is expected to touch the host, so it never reports a clash or gap. - **A leaf follows its host's finished outline** (RFC-0123): a `cut_path ... keep="inside"` outline or a pierced `cut_path` cut-out written on a face parallel to the veneered one, and a `trim` or scribed tenon shoulder written square to it, shape the leaf as they shape the host, on every leaf of a book, slip or sunburst match. Not followed: a leaning (`tilt=`) cut on a parallel face, an outline written on a face square to the veneered one, and blind cuts; the leaf keeps the blank rectangle there. A trim that removes a whole leaf leaves that leaf `not built: the features remove the entire stock`. - The cut list shows each leaf as its own row (species, thickness, grain angle, match), a book-match's mirrored half as a distinct handed row, and the host row **twice**: the declared substrate size unchanged plus a finished-thickness figure (substrate plus leaves). ## Example ```kdl project name="Veneered Lid" units=mm schema_version=1 { materials { material Pine type=softwood material Walnut type=hardwood } component Lid role=lid { stock Pine 280.0 180.0 18.0 // two leaves, opened like a book: the figure mirrors about the centre seam veneer face=pos_z material=Walnut thickness=3.0 grain_angle=0.0 match=book leaves=2 } assembly Box { part Lid instance=Lid { by top_sw; pos x=0.0 y=0.0 z=0.0 } } } ``` ## Pitfalls and diagnostics - Veneer cross-grain to a solid substrate cracks as the substrate moves; align `grain_angle=0.0` with the host's grain unless the piece is a stable sheet substrate. `examples/biedermeier_box.kdl` explains why it veneers the show faces and balances the back. - A one-sided veneer on a wide panel cups; veneer the balancing face too (a second `veneer`). - `material=` is a name, not an expression: `material=Walnut`, not `material="Walnut"`. - `veneer` is read by name; it is **not** an edge feature, so it takes no `edges` child. ## See also - `docs/KDL_AUTHORING.md` §7 ("Veneer and laminate"); `docs/rfc/0123-veneer-and-laminate.md`; ADR-0019 (the mirror used by book match). - `examples/biedermeier_box.kdl`; `docs/styles/biedermeier.md`. - `materials-and-hardware.md` (declaring the species), `edge-and-face-cuts.md`. ## Names Every name this page declares, how the example library uses it, and the shortest real use. ### `veneer` feature; node in 15 of 116 examples (30 uses). Shortest real use, from `empire_columned_chest.kdl`: ```kdl veneer face=neg_x material=Mahogany thickness=veneer_th ``` ### `match` word-valued property; property in 15 of 116 examples (26 uses). Shortest real use, from `adirondack_pole_side_table.kdl`: ```kdl veneer face=pos_z material=Willow thickness=0.6 match=sunburst leaves=8 ``` ## Related terms - [Book-matched veneer](/almanac/techniques#book-matched-veneer) - [Burl veneer](/almanac/techniques#burl-veneer) - [Oyster veneer](/almanac/techniques#oyster-veneer) - [Veneer](/almanac/techniques#veneer) --- # Vocabulary The complete set of names the compiler's schema linter accepts, read from the linter's own tables (`tools/kdl-check/src/main.rs`) every time this reference is built, so a name added to the language appears here without anyone editing this page. Syntax and examples for each are in the language reference pages; the [names](names.md) index says which page. ## Project-level nodes Nodes written directly inside `project`. `import` `params` `preset` `materials` `component` `joinery` `assembly` `meta` `collection` `choice` `driver` `analysis` `volume` `no_join` `plan` `site` ## Features Cuts and shaping operations written inside a `component` or `part`. `dado` `groove` `rabbet` `chamfer` `roundover` `moulding` `taper` `bevel` `cut_path` `hole` `pocket` `trim` `veneer` `banding` `stringing` `grain` `stock` `mark` `by` `pos` `rot` `params` `lamination` `path` `section` `bend` ## Joinery kinds The `kind` of a `joinery` block, named by a `join`. `mortise_and_tenon` `half_lap` `cross_lap` `bridle` `box_joint` `dovetail_through` `dovetail_half_blind` `dowel` `pocket_hole` `biscuit` `domino` `tongue_and_groove` `edge_glue` `stile_and_rail` `miter` `butt` `sliding_dovetail` `hardware_mount` `round_tenon` `sliding_sleeve` `mitred_moulding` ## Stock shapes `stock shape=` values; `rect` is the default. `rect` `round` `ring` ## Anchors Nine points on each of the top and bottom faces plus the centre, named by `by` when placing a part. `top_sw` `top_se` `top_ne` `top_nw` `top_n` `top_s` `top_e` `top_w` `top_center` `bottom_sw` `bottom_se` `bottom_ne` `bottom_nw` `bottom_n` `bottom_s` `bottom_e` `bottom_w` `bottom_center` `center` ## Faces World and part-local directions: `normal=`, `face=`, `front=`. `pos_x` `neg_x` `pos_y` `neg_y` `pos_z` `neg_z` ## Grain `grain=` values; grain runs along the length unless stated. `lengthwise` `crosswise` `along_x` `along_y` `along_z` ## Word-valued properties A property with one of these keys holds a name or an enum word, never an expression: do not quote the value as a computation. Every other quoted value is a CEL expression. `to` `from` `export` `label` `category` `id` `name` `instance` `role` `material` `type` `screw_type` `slope` `profile` `fit` `section` `direction` `unit_display` `edges` `faces` `sides` `axis` `target` `normal` `joint` `male` `female` `units` `kind` `shape` `enter` `stop` `hardware` `path` `driver` `reference` `interp` `grain` `match` `lay` `pattern` `keep` `collection` `extends` `style` `requires` `solves` `body` `pivot` `wall` `design` `swing` `hinge` `plane` `a` `b` `reason` `on` `grain_check` `room` `opening` `of` `ridge` `rotate` `corner` `within` `offset_pin` `path_start` `path_end` `between` `high_side` ## Expression language A quoted string in a value position is a CEL expression (degrees, not radians). These names are available besides the project's own parameters. Operators, quoting and the units rules are in [project-and-params](project-and-params.md). **Functions and constants:** `true` `false` `null` `sin` `cos` `tan` `asin` `acos` `atan` `atan2` `floor` `ceil` `round` `abs` `min` `max` `sqrt` `pow` `inch` `ac_sealed_fc` `ac_sealed_qtc` `ac_port_length` `ac_helmholtz_fb` **Refused** (valid CEL that this language rejects with `ww::eval::not_in_language`): `map` `filter` `all` `exists` `exists_one` `has` `size` `matches` `string` `int` `uint` `double` `bytes` `timestamp` `duration` `contains` `startsWith` `endsWith` **Limits:** an expression is at most 4096 bytes and 128 operators or brackets. --- # Names One row per name: what it is, the page that documents it (the link lands on its entry), and how many example files use it. Look a keyword up here first. `/technology/language/` on the site goes straight to the first page that documents it. | Name | Is a | Documented in | Examples | | --- | --- | --- | --- | | `a` | word-valued property | [model-metrics](model-metrics.md#a) | 0 | | `abs` | expression function | [vocabulary](vocabulary.md) | 0 | | `abut` | property | [plans-and-interfaces](plans-and-interfaces.md#abut) | 0 | | `ac_helmholtz_fb` | expression function | [speaker-analysis](speaker-analysis.md#ac_helmholtz_fb) | 0 | | `ac_port_length` | expression function | [speaker-analysis](speaker-analysis.md#ac_port_length) | 0 | | `ac_sealed_fc` | expression function | [speaker-analysis](speaker-analysis.md#ac_sealed_fc) | 0 | | `ac_sealed_qtc` | expression function | [speaker-analysis](speaker-analysis.md#ac_sealed_qtc) | 0 | | `acos` | expression function | [vocabulary](vocabulary.md) | 0 | | `against` | property | [plans-and-interfaces](plans-and-interfaces.md#against) | 0 | | `align` | property | [plans-and-interfaces](plans-and-interfaces.md#align) | 0 | | `along_x` | grain | [grain-axis](grain-axis.md#along_x) | 35 | | `along_y` | grain | [grain-axis](grain-axis.md#along_y) | 25 | | `along_z` | grain | [grain-axis](grain-axis.md#along_z) | 27 | | `analysis` | project-level node | [speaker-analysis](speaker-analysis.md#analysis) | 2 | | `angle` | property | [joinery-frames](joinery-frames.md#angle) | 10 | | `arc` | property | [curved-members](curved-members.md#arc), [cut-paths](cut-paths.md#arc), [paths-and-splines](paths-and-splines.md#arc) | 19 | | `asin` | expression function | [vocabulary](vocabulary.md) | 0 | | `assembly` | project-level node | [parts-and-assemblies](parts-and-assemblies.md#assembly) | 116 | | `at` | property | [edge-and-face-cuts](edge-and-face-cuts.md#at), [plans-and-interfaces](plans-and-interfaces.md#at) | 47 | | `atan` | expression function | [vocabulary](vocabulary.md) | 0 | | `atan2` | expression function | [vocabulary](vocabulary.md) | 0 | | `attach` | property | [plans-and-interfaces](plans-and-interfaces.md#attach) | 0 | | `avoid` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#avoid) | 1 | | `axis` | word-valued property | [round-stock-sections](round-stock-sections.md#axis) | 72 | | `b` | word-valued property | [model-metrics](model-metrics.md#b) | 0 | | `banding` | feature | [banding-and-stringing](banding-and-stringing.md#banding) | 6 | | `bend` | feature | [curved-members](curved-members.md#bend) | 1 | | `between` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `bevel` | feature | [bevel-and-rabbet](bevel-and-rabbet.md#bevel) | 6 | | `biscuit` | joinery kind | [joinery-fasteners](joinery-fasteners.md#biscuit), [loose-inserts](loose-inserts.md#biscuit) | 1 | | `biscuit_size` | property | [joinery-fasteners](joinery-fasteners.md#biscuit_size) | 1 | | `biscuit_spacing` | property | [joinery-fasteners](joinery-fasteners.md#biscuit_spacing) | 5 | | `body` | word-valued property | [travel](travel.md#body) | 2 | | `bottom_center` | anchor | [vocabulary](vocabulary.md) | 6 | | `bottom_e` | anchor | [vocabulary](vocabulary.md) | 5 | | `bottom_n` | anchor | [vocabulary](vocabulary.md) | 1 | | `bottom_ne` | anchor | [vocabulary](vocabulary.md) | 0 | | `bottom_nw` | anchor | [vocabulary](vocabulary.md) | 0 | | `bottom_perimeter` | property | [edge-and-face-cuts](edge-and-face-cuts.md#bottom_perimeter) | 19 | | `bottom_s` | anchor | [vocabulary](vocabulary.md) | 9 | | `bottom_se` | anchor | [vocabulary](vocabulary.md) | 0 | | `bottom_sw` | anchor | [vocabulary](vocabulary.md) | 14 | | `bottom_w` | anchor | [vocabulary](vocabulary.md) | 7 | | `box_joint` | joinery kind | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#box_joint) | 6 | | `bridle` | joinery kind | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#bridle) | 0 | | `butt` | joinery kind | [joinery-contract](joinery-contract.md#butt) | 20 | | `by` | feature | [parts-and-assemblies](parts-and-assemblies.md#by) | 113 | | `category` | word-valued property | [project-and-params](project-and-params.md#category) | 115 | | `ceil` | expression function | [vocabulary](vocabulary.md) | 0 | | `center` | anchor | [vocabulary](vocabulary.md) | 6 | | `centering` | property | [joinery-tenons](joinery-tenons.md#centering) | 36 | | `centre_on` | property | [plans-and-interfaces](plans-and-interfaces.md#centre_on) | 0 | | `chamfer` | feature | [edge-and-face-cuts](edge-and-face-cuts.md#chamfer) | 30 | | `cheek_thickness` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#cheek_thickness) | 0 | | `choice` | project-level node | [reuse-and-collections](reuse-and-collections.md#choice) | 1 | | `circle` | property | [paths-and-splines](paths-and-splines.md#circle), [round-stock-sections](round-stock-sections.md#circle) | 40 | | `clearance` | property | [joinery-contract](joinery-contract.md#clearance) | 102 | | `cockbead` | property | [edge-and-face-cuts](edge-and-face-cuts.md#cockbead) | 1 | | `collection` | project-level node, word-valued property | [reuse-and-collections](reuse-and-collections.md#collection) | 1 | | `color` | property | [materials-and-hardware](materials-and-hardware.md#color) | 85 | | `component` | project-level node | [components-and-stock](components-and-stock.md#component) | 110 | | `constraint` | property | [non-wood-materials](non-wood-materials.md#constraint) | 1 | | `construction` | property | [reuse-and-collections](reuse-and-collections.md#construction) | 1 | | `corner` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `cos` | expression function | [vocabulary](vocabulary.md) | 0 | | `count` | property | [joinery-fasteners](joinery-fasteners.md#count), [parts-and-assemblies](parts-and-assemblies.md#count) | 70 | | `cove` | property | [edge-and-face-cuts](edge-and-face-cuts.md#cove) | 3 | | `cross_lap` | joinery kind | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#cross_lap), [oblique-and-skewed-joints](oblique-and-skewed-joints.md#cross_lap) | 3 | | `crosswise` | grain | [components-and-stock](components-and-stock.md#crosswise) | 0 | | `cut_path` | feature | [cut-paths](cut-paths.md#cut_path), [paths-and-splines](paths-and-splines.md#cut_path) | 50 | | `dado` | feature | [edge-and-face-cuts](edge-and-face-cuts.md#dado) | 23 | | `depth` | property | [edge-and-face-cuts](edge-and-face-cuts.md#depth), [joinery-laps-and-boxes](joinery-laps-and-boxes.md#depth) | 95 | | `depth_ratio` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#depth_ratio) | 7 | | `design` | word-valued property | [plans-and-interfaces](plans-and-interfaces.md#design) | 0 | | `diameter` | property | [edge-and-face-cuts](edge-and-face-cuts.md#diameter) | 65 | | `direction` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `distribute` | property | [hardware-mounting](hardware-mounting.md#distribute) | 2 | | `domino` | joinery kind | [joinery-fasteners](joinery-fasteners.md#domino), [loose-inserts](loose-inserts.md#domino) | 1 | | `dovetail_half_blind` | joinery kind | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#dovetail_half_blind) | 17 | | `dovetail_through` | joinery kind | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#dovetail_through) | 10 | | `dowel` | joinery kind | [joinery-fasteners](joinery-fasteners.md#dowel), [loose-inserts](loose-inserts.md#dowel) | 32 | | `draw` | property | [joinery-tenons](joinery-tenons.md#draw) | 18 | | `drive_watts` | property | [speaker-analysis](speaker-analysis.md#drive_watts) | 2 | | `driver` | project-level node, word-valued property | [speaker-analysis](speaker-analysis.md#driver) | 2 | | `edge` | property | [plans-and-interfaces](plans-and-interfaces.md#edge) | 23 | | `edge_glue` | joinery kind | [joinery-fasteners](joinery-fasteners.md#edge_glue) | 8 | | `edges` | word-valued property | [edge-and-face-cuts](edge-and-face-cuts.md#edges) | 71 | | `elevation` | property | [plans-and-interfaces](plans-and-interfaces.md#elevation) | 0 | | `end_chamfer` | property | [joinery-tenons](joinery-tenons.md#end_chamfer) | 11 | | `end_z` | property | [edge-and-face-cuts](edge-and-face-cuts.md#end_z) | 12 | | `ends_proud` | property | [joinery-tenons](joinery-tenons.md#ends_proud) | 9 | | `enter` | word-valued property | [joinery-tenons](joinery-tenons.md#enter) | 0 | | `export` | word-valued property | [reuse-and-collections](reuse-and-collections.md#export) | 5 | | `expose` | property | [plans-and-interfaces](plans-and-interfaces.md#expose) | 0 | | `extends` | word-valued property | [reuse-and-collections](reuse-and-collections.md#extends) | 1 | | `face` | property | [edge-and-face-cuts](edge-and-face-cuts.md#face) | 77 | | `faces` | word-valued property | [edge-and-face-cuts](edge-and-face-cuts.md#faces) | 15 | | `facing` | property | [plans-and-interfaces](plans-and-interfaces.md#facing) | 0 | | `false` | expression function | [vocabulary](vocabulary.md) | 0 | | `feature` | property | [plans-and-interfaces](plans-and-interfaces.md#feature) | 0 | | `female` | word-valued property | [joinery-contract](joinery-contract.md#female) | 108 | | `fill` | property | [plans-and-interfaces](plans-and-interfaces.md#fill) | 10 | | `finger_width` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#finger_width) | 4 | | `finish` | property | [materials-and-hardware](materials-and-hardware.md#finish), [parts-and-assemblies](parts-and-assemblies.md#finish), [style-finish-rule](style-finish-rule.md#finish) | 78 | | `fit` | word-valued property | [joinery-contract](joinery-contract.md#fit) | 0 | | `floor` | expression function | [vocabulary](vocabulary.md) | 4 | | `footprint` | property | [plans-and-interfaces](plans-and-interfaces.md#footprint) | 0 | | `from` | word-valued property | [reuse-and-collections](reuse-and-collections.md#from) | 7 | | `front_gap` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#front_gap) | 7 | | `front_perimeter` | property | [edge-and-face-cuts](edge-and-face-cuts.md#front_perimeter) | 11 | | `fs` | property | [speaker-analysis](speaker-analysis.md#fs) | 4 | | `gap` | property | [plans-and-interfaces](plans-and-interfaces.md#gap) | 1 | | `grain` | feature, word-valued property | [components-and-stock](components-and-stock.md#grain) | 2 | | `grain_check` | word-valued property | [joinery-contract](joinery-contract.md#grain_check) | 0 | | `groove` | feature | [edge-and-face-cuts](edge-and-face-cuts.md#groove) | 40 | | `groove_depth` | property | [joinery-frames](joinery-frames.md#groove_depth) | 16 | | `groove_width` | property | [joinery-frames](joinery-frames.md#groove_width) | 9 | | `half_lap` | joinery kind | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#half_lap) | 4 | | `half_round` | property | [edge-and-face-cuts](edge-and-face-cuts.md#half_round) | 2 | | `hardware` | word-valued property | [hardware-mounting](hardware-mounting.md#hardware), [materials-and-hardware](materials-and-hardware.md#hardware) | 36 | | `hardware_mount` | joinery kind | [hardware-mounting](hardware-mounting.md#hardware_mount), [materials-and-hardware](materials-and-hardware.md#hardware_mount) | 35 | | `hardware_use` | property | [hardware-mounting](hardware-mounting.md#hardware_use), [materials-and-hardware](materials-and-hardware.md#hardware_use) | 5 | | `haunch` | property | [joinery-tenons](joinery-tenons.md#haunch) | 6 | | `heading` | property | [components-and-stock](components-and-stock.md#heading), [oblique-and-skewed-joints](oblique-and-skewed-joints.md#heading) | 2 | | `high_side` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `hinge` | word-valued property | [plans-and-interfaces](plans-and-interfaces.md#hinge) | 1 | | `hole` | feature | [edge-and-face-cuts](edge-and-face-cuts.md#hole) | 23 | | `housing_depth` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#housing_depth) | 11 | | `id` | word-valued property | [parts-and-assemblies](parts-and-assemblies.md#id) | 43 | | `import` | project-level node | [reuse-and-collections](reuse-and-collections.md#import) | 5 | | `inch` | expression function | [vocabulary](vocabulary.md) | 0 | | `index` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#index) | 1 | | `instance` | word-valued property | [parts-and-assemblies](parts-and-assemblies.md#instance) | 112 | | `interface` | property | [plans-and-interfaces](plans-and-interfaces.md#interface) | 0 | | `interp` | word-valued property | [round-stock-sections](round-stock-sections.md#interp) | 41 | | `inverse` | property | [project-and-params](project-and-params.md#inverse) | 1 | | `join` | property | [joinery-contract](joinery-contract.md#join) | 109 | | `joinery` | project-level node | [joinery-contract](joinery-contract.md#joinery) | 109 | | `joint` | word-valued property | [joinery-contract](joinery-contract.md#joint) | 109 | | `keep` | word-valued property | [cut-paths](cut-paths.md#keep) | 27 | | `kerf` | property | [cut-paths](cut-paths.md#kerf) | 6 | | `key` | property | [joinery-tenons](joinery-tenons.md#key) | 2 | | `kind` | word-valued property | [reuse-and-collections](reuse-and-collections.md#kind) | 4 | | `label` | word-valued property | [parts-and-assemblies](parts-and-assemblies.md#label), [project-and-params](project-and-params.md#label) | 116 | | `lambs_tongue` | property | [edge-and-face-cuts](edge-and-face-cuts.md#lambs_tongue) | 5 | | `lamination` | feature | [vocabulary](vocabulary.md) | 0 | | `lay` | word-valued property | [banding-and-stringing](banding-and-stringing.md#lay) | 0 | | `left_of` | property | [plans-and-interfaces](plans-and-interfaces.md#left_of) | 0 | | `length` | property | [edge-and-face-cuts](edge-and-face-cuts.md#length) | 59 | | `lengthwise` | grain | [components-and-stock](components-and-stock.md#lengthwise) | 26 | | `line` | property | [cut-paths](cut-paths.md#line), [paths-and-splines](paths-and-splines.md#line) | 33 | | `male` | word-valued property | [joinery-contract](joinery-contract.md#male) | 109 | | `mark` | feature | [components-and-stock](components-and-stock.md#mark) | 110 | | `match` | word-valued property | [veneer-and-laminate](veneer-and-laminate.md#match) | 15 | | `material` | word-valued property | [materials-and-hardware](materials-and-hardware.md#material) | 114 | | `materials` | project-level node | [materials-and-hardware](materials-and-hardware.md#materials) | 114 | | `max` | expression function | [vocabulary](vocabulary.md) | 116 | | `meta` | project-level node | [project-and-params](project-and-params.md#meta) | 0 | | `min` | expression function | [vocabulary](vocabulary.md) | 116 | | `mirror` | property | [parts-and-assemblies](parts-and-assemblies.md#mirror) | 30 | | `miter` | joinery kind | [joinery-frames](joinery-frames.md#miter) | 6 | | `mitred_moulding` | joinery kind | [joinery-frames](joinery-frames.md#mitred_moulding) | 0 | | `mortise_and_tenon` | joinery kind | [joinery-tenons](joinery-tenons.md#mortise_and_tenon) | 80 | | `moulding` | feature | [bevel-and-rabbet](bevel-and-rabbet.md#moulding), [edge-and-face-cuts](edge-and-face-cuts.md#moulding) | 18 | | `mount` | property | [plans-and-interfaces](plans-and-interfaces.md#mount) | 0 | | `name` | word-valued property | [joinery-contract](joinery-contract.md#name) | 116 | | `needs` | property | [plans-and-interfaces](plans-and-interfaces.md#needs) | 0 | | `neg_x` | face | [vocabulary](vocabulary.md) | 101 | | `neg_y` | face | [vocabulary](vocabulary.md) | 93 | | `neg_z` | face | [vocabulary](vocabulary.md) | 89 | | `no_join` | project-level node | [model-metrics](model-metrics.md#no_join) | 0 | | `normal` | word-valued property | [components-and-stock](components-and-stock.md#normal) | 110 | | `null` | expression function | [vocabulary](vocabulary.md) | 0 | | `obstacle` | property | [plans-and-interfaces](plans-and-interfaces.md#obstacle) | 0 | | `of` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `offset` | property | [edge-and-face-cuts](edge-and-face-cuts.md#offset) | 57 | | `offset_pin` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `ogee` | property | [edge-and-face-cuts](edge-and-face-cuts.md#ogee) | 9 | | `on` | word-valued property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#on) | 0 | | `one_of` | property | [style-finish-rule](style-finish-rule.md#one_of) | 0 | | `opening` | word-valued property | [plans-and-interfaces](plans-and-interfaces.md#opening) | 0 | | `ovolo` | property | [edge-and-face-cuts](edge-and-face-cuts.md#ovolo) | 2 | | `params` | project-level node, feature | [project-and-params](project-and-params.md#params) | 116 | | `part` | property | [parts-and-assemblies](parts-and-assemblies.md#part) | 116 | | `path` | feature, word-valued property | [cut-paths](cut-paths.md#path), [paths-and-splines](paths-and-splines.md#path) | 9 | | `path_end` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `path_start` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `pattern` | word-valued property | [banding-and-stringing](banding-and-stringing.md#pattern) | 4 | | `pin` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#pin), [joinery-tenons](joinery-tenons.md#pin), [plans-and-interfaces](plans-and-interfaces.md#pin) | 19 | | `pin_count` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#pin_count) | 21 | | `pivot` | word-valued property | [travel](travel.md#pivot) | 2 | | `place` | property | [plans-and-interfaces](plans-and-interfaces.md#place) | 0 | | `plan` | project-level node | [plans-and-interfaces](plans-and-interfaces.md#plan) | 0 | | `plane` | word-valued property | [vocabulary](vocabulary.md) | 1 | | `pocket` | feature | [vocabulary](vocabulary.md) | 0 | | `pocket_hole` | joinery kind | [joinery-fasteners](joinery-fasteners.md#pocket_hole) | 5 | | `point` | property | [cut-paths](cut-paths.md#point) | 40 | | `points` | property | [speaker-analysis](speaker-analysis.md#points) | 2 | | `polyline` | property | [cut-paths](cut-paths.md#polyline), [paths-and-splines](paths-and-splines.md#polyline) | 21 | | `pommel` | property | [round-stock-sections](round-stock-sections.md#pommel) | 0 | | `port_diameter` | property | [speaker-analysis](speaker-analysis.md#port_diameter) | 1 | | `ports` | property | [speaker-analysis](speaker-analysis.md#ports) | 0 | | `pos` | feature | [parts-and-assemblies](parts-and-assemblies.md#pos) | 116 | | `pos_x` | face | [vocabulary](vocabulary.md) | 103 | | `pos_y` | face | [vocabulary](vocabulary.md) | 88 | | `pos_z` | face | [vocabulary](vocabulary.md) | 97 | | `pow` | expression function | [vocabulary](vocabulary.md) | 0 | | `preset` | project-level node | [project-and-params](project-and-params.md#preset) | 4 | | `profile` | word-valued property | [joinery-frames](joinery-frames.md#profile), [turning-and-sections](turning-and-sections.md#profile) | 11 | | `project` | property | [project-and-params](project-and-params.md#project) | 116 | | `proud` | property | [joinery-tenons](joinery-tenons.md#proud) | 14 | | `qts` | property | [speaker-analysis](speaker-analysis.md#qts) | 2 | | `qty` | property | [materials-and-hardware](materials-and-hardware.md#qty) | 19 | | `rabbet` | feature | [bevel-and-rabbet](bevel-and-rabbet.md#rabbet), [edge-and-face-cuts](edge-and-face-cuts.md#rabbet) | 24 | | `ramp` | property | [edge-and-face-cuts](edge-and-face-cuts.md#ramp) | 0 | | `range` | property | [speaker-analysis](speaker-analysis.md#range) | 2 | | `re` | property | [speaker-analysis](speaker-analysis.md#re) | 2 | | `reason` | word-valued property | [model-metrics](model-metrics.md#reason) | 0 | | `rect` | stock shape | [round-stock-sections](round-stock-sections.md#rect) | 15 | | `reference` | word-valued property | [edge-and-face-cuts](edge-and-face-cuts.md#reference) | 5 | | `repeat` | property | [parts-and-assemblies](parts-and-assemblies.md#repeat) | 65 | | `requires` | word-valued property | [reuse-and-collections](reuse-and-collections.md#requires) | 1 | | `ridge` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `right_of` | property | [plans-and-interfaces](plans-and-interfaces.md#right_of) | 0 | | `ring` | stock shape | [non-wood-materials](non-wood-materials.md#ring) | 1 | | `role` | word-valued property | [components-and-stock](components-and-stock.md#role), [parts-and-assemblies](parts-and-assemblies.md#role) | 114 | | `room` | word-valued property | [plans-and-interfaces](plans-and-interfaces.md#room) | 0 | | `rot` | feature | [parts-and-assemblies](parts-and-assemblies.md#rot) | 82 | | `rotate` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `round` | stock shape, expression function | [round-stock-sections](round-stock-sections.md#round) | 44 | | `round_tenon` | joinery kind | [joinery-tenons](joinery-tenons.md#round_tenon) | 22 | | `roundover` | feature | [edge-and-face-cuts](edge-and-face-cuts.md#roundover) | 42 | | `run` | property | [plans-and-interfaces](plans-and-interfaces.md#run) | 2 | | `run_length` | property | [edge-and-face-cuts](edge-and-face-cuts.md#run_length) | 36 | | `samples` | property | [round-stock-sections](round-stock-sections.md#samples) | 23 | | `screw_type` | word-valued property | [joinery-fasteners](joinery-fasteners.md#screw_type) | 5 | | `sd` | property | [speaker-analysis](speaker-analysis.md#sd) | 2 | | `section` | feature, word-valued property | [round-stock-sections](round-stock-sections.md#section), [turning-and-sections](turning-and-sections.md#section) | 47 | | `sensitivity_db` | property | [speaker-analysis](speaker-analysis.md#sensitivity_db) | 2 | | `shape` | word-valued property | [round-stock-sections](round-stock-sections.md#shape) | 66 | | `sides` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `sin` | expression function | [vocabulary](vocabulary.md) | 0 | | `site` | project-level node | [plans-and-interfaces](plans-and-interfaces.md#site) | 0 | | `size` | property | [edge-and-face-cuts](edge-and-face-cuts.md#size) | 73 | | `sliding_dovetail` | joinery kind | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#sliding_dovetail) | 8 | | `sliding_sleeve` | joinery kind | [sliding-sleeve](sliding-sleeve.md#sliding_sleeve) | 1 | | `slope` | word-valued property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#slope) | 23 | | `slots` | property | [reuse-and-collections](reuse-and-collections.md#slots) | 1 | | `socket_depth` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#socket_depth) | 17 | | `solves` | word-valued property | [project-and-params](project-and-params.md#solves) | 1 | | `spacing` | property | [joinery-fasteners](joinery-fasteners.md#spacing) | 6 | | `spline` | property | [curved-members](curved-members.md#spline), [paths-and-splines](paths-and-splines.md#spline) | 28 | | `sqrt` | expression function | [vocabulary](vocabulary.md) | 0 | | `start` | property | [edge-and-face-cuts](edge-and-face-cuts.md#start) | 40 | | `start_z` | property | [edge-and-face-cuts](edge-and-face-cuts.md#start_z) | 12 | | `stile_and_rail` | joinery kind | [joinery-frames](joinery-frames.md#stile_and_rail), [tenon-tips-wedges-and-coped-rails](tenon-tips-wedges-and-coped-rails.md#stile_and_rail) | 9 | | `stock` | feature | [components-and-stock](components-and-stock.md#stock), [grain-axis](grain-axis.md#stock), [non-wood-materials](non-wood-materials.md#stock), [turning-and-sections](turning-and-sections.md#stock) | 114 | | `stop` | word-valued property | [edge-and-face-cuts](edge-and-face-cuts.md#stop) | 5 | | `stopped` | property | [edge-and-face-cuts](edge-and-face-cuts.md#stopped), [joinery-laps-and-boxes](joinery-laps-and-boxes.md#stopped) | 40 | | `stretch` | property | [plans-and-interfaces](plans-and-interfaces.md#stretch) | 0 | | `stringing` | feature | [banding-and-stringing](banding-and-stringing.md#stringing) | 12 | | `stub_tenon_length` | property | [joinery-frames](joinery-frames.md#stub_tenon_length) | 9 | | `style` | word-valued property | [reuse-and-collections](reuse-and-collections.md#style) | 1 | | `substance` | property | [non-wood-materials](non-wood-materials.md#substance) | 1 | | `swing` | word-valued property | [plans-and-interfaces](plans-and-interfaces.md#swing) | 0 | | `tan` | expression function | [vocabulary](vocabulary.md) | 0 | | `taper` | feature | [edge-and-face-cuts](edge-and-face-cuts.md#taper) | 12 | | `taper_to` | property | [joinery-tenons](joinery-tenons.md#taper_to) | 2 | | `target` | word-valued property | [edge-and-face-cuts](edge-and-face-cuts.md#target) | 33 | | `target_dim` | property | [edge-and-face-cuts](edge-and-face-cuts.md#target_dim) | 12 | | `tempered` | property | [non-wood-materials](non-wood-materials.md#tempered) | 0 | | `tenon_length` | property | [joinery-tenons](joinery-tenons.md#tenon_length) | 69 | | `tenon_thickness` | property | [joinery-tenons](joinery-tenons.md#tenon_thickness) | 80 | | `tenon_width` | property | [joinery-tenons](joinery-tenons.md#tenon_width) | 54 | | `through` | property | [joinery-tenons](joinery-tenons.md#through) | 20 | | `thumbnail` | property | [edge-and-face-cuts](edge-and-face-cuts.md#thumbnail) | 6 | | `tilt` | property | [components-and-stock](components-and-stock.md#tilt), [oblique-and-skewed-joints](oblique-and-skewed-joints.md#tilt) | 2 | | `tip` | property | [tenon-tips-wedges-and-coped-rails](tenon-tips-wedges-and-coped-rails.md#tip) | 15 | | `to` | word-valued property | [non-wood-materials](non-wood-materials.md#to) | 2 | | `tolerance` | property | [speaker-analysis](speaker-analysis.md#tolerance) | 3 | | `tongue_and_groove` | joinery kind | [tongue-and-groove](tongue-and-groove.md#tongue_and_groove) | 4 | | `tongue_thickness` | property | [joinery-laps-and-boxes](joinery-laps-and-boxes.md#tongue_thickness) | 12 | | `top_center` | anchor | [vocabulary](vocabulary.md) | 28 | | `top_e` | anchor | [vocabulary](vocabulary.md) | 17 | | `top_n` | anchor | [vocabulary](vocabulary.md) | 12 | | `top_ne` | anchor | [vocabulary](vocabulary.md) | 1 | | `top_nw` | anchor | [vocabulary](vocabulary.md) | 2 | | `top_perimeter` | property | [edge-and-face-cuts](edge-and-face-cuts.md#top_perimeter) | 49 | | `top_s` | anchor | [vocabulary](vocabulary.md) | 15 | | `top_se` | anchor | [vocabulary](vocabulary.md) | 6 | | `top_sw` | anchor | [vocabulary](vocabulary.md) | 105 | | `top_w` | anchor | [vocabulary](vocabulary.md) | 25 | | `travel` | property | [travel](travel.md#travel) | 3 | | `trim` | feature | [bevel-and-rabbet](bevel-and-rabbet.md#trim), [edge-and-face-cuts](edge-and-face-cuts.md#trim) | 5 | | `true` | expression function | [vocabulary](vocabulary.md) | 0 | | `tuning` | property | [speaker-analysis](speaker-analysis.md#tuning) | 0 | | `turn` | property | [turning-and-sections](turning-and-sections.md#turn) | 0 | | `type` | word-valued property | [materials-and-hardware](materials-and-hardware.md#type) | 114 | | `unit_display` | word-valued property | [project-and-params](project-and-params.md#unit_display) | 0 | | `units` | word-valued property | [project-and-params](project-and-params.md#units) | 116 | | `vas` | property | [speaker-analysis](speaker-analysis.md#vas) | 2 | | `veneer` | feature | [veneer-and-laminate](veneer-and-laminate.md#veneer) | 15 | | `volume` | project-level node | [model-metrics](model-metrics.md#volume) | 2 | | `wall` | word-valued property | [plans-and-interfaces](plans-and-interfaces.md#wall) | 0 | | `wedge` | property | [joinery-tenons](joinery-tenons.md#wedge), [tenon-tips-wedges-and-coped-rails](tenon-tips-wedges-and-coped-rails.md#wedge) | 6 | | `width` | property | [edge-and-face-cuts](edge-and-face-cuts.md#width) | 56 | | `within` | word-valued property | [vocabulary](vocabulary.md) | 0 | | `xmax` | property | [speaker-analysis](speaker-analysis.md#xmax) | 2 | --- # Diagnostics Every `ww::stage::name` code a compile can report, scanned from the compiler's sources. A scene carries them in `diagnostics` with a severity, the line, and the message. Where the stage declares a message template, it is shown (`{braces}` are filled in at run time) with its help line. ## Stage `analysis` ### `ww::analysis::unaccounted_contact` Reported by the compiler with a message built at run time. ## Stage `check` ### `ww::check::below_floor` Reported by the compiler with a message built at run time. ### `ww::check::empty_assembly` Reported by the compiler with a message built at run time. ### `ww::check::extra_root` Reported by the compiler with a message built at run time. ### `ww::check::floating` Reported by the compiler with a message built at run time. ### `ww::check::slide_clearance` Reported by the compiler with a message built at run time. ### `ww::check::travel_foul` Reported by the compiler with a message built at run time. ### `ww::check::travel_invalid` Reported by the compiler with a message built at run time. ### `ww::check::travel_unchecked` Reported by the compiler with a message built at run time. ## Stage `dsl` ### `ww::dsl::bad_choice` A `choice` set to a word its declaration does not allow, or declared without words. Message: ``choice `{choice}`: {problem}`` Help: `{allowed}` ### `ww::dsl::bad_import_path` Message: `` `{path}` is not a usable import path `` Help: `` `from=` is `std:name` for an embedded library, or a `/`-separated path relative to this file `` ### `ww::dsl::bad_reference` Message: `` `{raw}` is not a valid reference `` Help: ``a reference is `Part@Mark` (ADR-0002); either half may interpolate a name in braces (`Rail_{i}@Slot_{i}`), but braces must pair up around it`` ### `ww::dsl::bad_value` Message: `` `{node}` has a `{key}` of the wrong type `` Help: `expected {expected}` ### `ww::dsl::collection_cycle` Message: `collections extend each other in a cycle: {chain}` Help: `` `extends=` must lead back to a collection with no `extends=` `` ### `ww::dsl::collection_unknown_key` A derived collection setting a param, choice or slot its bases never declared. Message: ``collection `{collection}` sets {what} `{name}`, which `{base}` does not declare`` Help: `a derived collection lists only differences; declare it in the base collection first` ### `ww::dsl::duplicate` Message: `` `{name}` is declared more than once in this scope `` ### `ww::dsl::import_conflict` Message: `import conflict: {what}` Help: `{hint}` ### `ww::dsl::import_cycle` Message: `import cycle: {chain}` Help: `a file cannot import itself, directly or through others` ### `ww::dsl::import_load_failed` Message: ``cannot import `{path}`: {reason}`` ### `ww::dsl::import_too_deep` [`crate::Limits::max_import_depth`]. Message: `imports chain more than {limit} files deep` Help: `a chain of files importing files may be at most {limit} long (docs/KDL_AUTHORING.md §13)` ### `ww::dsl::kdl_syntax` Message: `file does not parse as KDL v2` Help: ``woodwerx uses KDL v2. Note that `(` after `=` starts a type annotation, not a tuple, and backtick is a legal identifier character (ADR-0002).`` ### `ww::dsl::missing_field` Message: `` `{node}` is missing {what} `` ### `ww::dsl::missing_project` Message: ``expected a single top-level `project` node`` ### `ww::dsl::nothing_exported` Message: `` `{path}` exports nothing `` Help: `` a file opts in to being imported by writing `export=Name` on a top-level `assembly` or `component` `` ### `ww::dsl::slot_contract` A slot bound to something that is missing, of the wrong kind, or lacks a param the slot's contract names. Points at the collection line that made the binding. Message: ``slot `{slot}` of collection `{collection}`: {problem}`` Help: `` a slot binds a name of its kind, and a component or assembly slot must declare every param in its `requires=` `` ### `ww::dsl::source_too_large` [`crate::Limits::max_source_bytes`]. Checked before the file is parsed at all. Message: `the file is {size} bytes; the limit is {limit}` Help: `a project file may be at most {limit} bytes (docs/KDL_AUTHORING.md §13). The largest example is about a sixth of that` ### `ww::dsl::too_deep` [`crate::Limits::max_depth`], counted in `{ … }` blocks. Checked before the file is parsed, because the KDL parser recurses once per level. Message: `blocks nest more than {limit} deep` Help: `` `{ … }` blocks may nest at most {limit} levels, counting `project` as the first (docs/KDL_AUTHORING.md §13) `` ### `ww::dsl::too_many_imports` [`crate::Limits::max_imports`]. Message: `more than {limit} distinct files imported` Help: `a project may pull in at most {limit} distinct files (docs/KDL_AUTHORING.md §13)` ### `ww::dsl::unexpected_entry` An argument or property in a slot the typed IR has nowhere to put. Stage 1 reports these rather than discarding them: silently dropping a property the author wrote is how a file compiles into something other than what it says. Message: `` `{node}` does not take {what} `` Help: `{expected}` ### `ww::dsl::unexpected_node` Message: `` `{node}` is not valid at this scope `` Help: `{expected}` ### `ww::dsl::unknown_collection` `project collection=X` or `extends=X` naming a collection that does not exist. Message: `` no collection named `{name}` `` Help: `{available}` ### `ww::dsl::unknown_export` Message: `` `{path}` does not export `{name}` `` Help: `it exports: {available}` ### `ww::dsl::unknown_slot` `@name` where no slot of that name is in force. Message: ``no slot `@{name}` in force`` Help: `{available}` ### `ww::dsl::unknown_unit` A `(unit)` annotation on a number that is not `mm` or `in` (ADR-0021). Message: `` `({unit})` is not a known unit `` Help: ``the unit annotations are `(mm)` and `(in)`; bare numbers are millimetres`` ## Stage `eval` ### `ww::eval::cel_runtime` Message: `expression could not be evaluated` ### `ww::eval::cel_syntax` Message: `expression is not valid CEL` Help: `{hint}` ### `ww::eval::cycle` Message: `` `{name}` is defined in terms of itself `` Help: `the cycle is: {cycle}` ### `ww::eval::expression_too_complex` An expression has more operators and brackets than [`crate::guard::MAX_EXPR_OPERATORS`] — a bound on how deep its tree can nest, which `cel` walks recursively. Checked before it is parsed. Message: `expression has {count} operators and brackets` Help: `at most {limit} in one expression; split it into named parameters` ### `ww::eval::expression_too_long` An expression's source text is longer than [`crate::guard::MAX_EXPR_BYTES`]. Checked before it is parsed. Message: `expression is {len} bytes long` Help: `at most {limit} bytes; split it into named parameters` ### `ww::eval::fractional_count` Message: `` `count` is {value}, so this `repeat` runs {iterations} time(s) `` Help: `` `repeat count=` is floored (ADR-0005). Write `ceil(…)` for {next_up}, or make the count come out whole `` ### `ww::eval::link` A linked parameter (`solves=` / `inverse=`, ADR-0035) that cannot work: the target is derived, the link loops, or the inverse reads what it is trying to set. Message: `` `{name}` is a badly linked parameter: {message} `` Help: ``a linked parameter is a derived one whose `solves=` names a literal parameter it depends on; see docs/adr/0035-linked-parameters.md`` ### `ww::eval::non_positive` Message: `` `{name}` is {value}, which is not a positive dimension `` ### `ww::eval::not_finite` Message: `` `{name}` evaluates to {value} `` Help: `a dimension must be a finite number; check for a division by zero` ### `ww::eval::not_in_language` Something CEL parses but woodwerx's expression language does not include: a comprehension macro, a method call, a list or map, field selection. See [`crate::guard`]. Message: `{construct} is not part of woodwerx's expression language` Help: `{hint}` ### `ww::eval::out_of_range` Message: `` `{name}` is {value}, outside its declared range {min}..={max} `` ### `ww::eval::repeat_count_too_large` `repeat count=` resolved to more iterations than Stage 2 will enumerate. Stage 2 binds the loop index for each iteration, so a count is a bound on work. A furniture project does not have ten thousand of anything; a count that large is a units mistake or a runaway expression, and diagnosing it beats hanging. Message: `` `repeat` asks for {count} iterations `` Help: `at most {limit} are resolved; check the units on this count` ### `ww::eval::text_too_long` An expression can build text longer than [`crate::guard::MAX_TEXT_BYTES`]. Usually found before evaluation, from the lengths of what the expression concatenates, in which case `len` is that upper bound. Message: `` `{name}` can build {len} bytes of text `` Help: `text in an expression is limited to {limit} bytes` ### `ww::eval::too_many_instances` `Limits::max_parts`, reached by nested `repeat`s: this one's count times every enclosing one's. Diagnosed before the loop is unrolled, not after. Message: ``nested `repeat`s unroll to {product} instances; the limit is {limit}`` Help: ``this count multiplied by every enclosing `repeat`'s is {product}; a project may expand to at most {limit} parts (docs/KDL_AUTHORING.md §13)`` ### `ww::eval::too_many_parts` `Limits::max_parts`, reached by the `part` nodes walked with every `repeat` unrolled. Resolution stops here. Message: ``more than {limit} `part`s, counting each `repeat` iteration`` Help: `a project may expand to at most {limit} parts (docs/KDL_AUTHORING.md §13)` ### `ww::eval::too_many_specialisations` `Limits::max_specialisations`: distinct override sets across instances of assemblies. Message: `more than {limit} distinct assembly specialisations` Help: ``each distinct set of overrides on an `instance=` of an assembly is one specialisation; at most {limit} (docs/KDL_AUTHORING.md §13). An assembly that instances itself with changing overrides never settles`` ### `ww::eval::unknown_name` Message: `` unknown name `{name}` `` ### `ww::eval::unknown_override` Message: `` `{key}` is not a parameter of `{component}` `` Help: ``{}`{component}` declares no parameters, so it has nothing to override`{component}` declares: {},`` ### `ww::eval::unsupported_operator` An operator CEL parses but woodwerx's numeric domain cannot apply. The one that matters is `%`: `cel` reports it as `UnsupportedBinaryOperator("rem", Float, Float)`, which tells an author writing `shelf_pitch % 2.0` nothing at all. No example in `examples/` uses it outside a comment, but an author will. Message: `` `{op}` cannot be used on dimensions `` Help: `{hint}` ## Stage `expand` ### `ww::expand::assembly_anchor` An assembly has no stock box, so of D6's anchors only the origin means anything. Message: `` an assembly instance cannot be anchored `by {anchor}` `` Help: ``an assembly has no board to take a bounding-box anchor from, so it is placed by its own origin: `by top_sw`, or no `by` at all. Move the parts inside the assembly instead`` ### `ww::expand::bad_dimension` Message: `` `{what}` is {value}, which cannot be a dimension `` Help: `a board has three finite, positive extents (D4)` ### `ww::expand::bad_interpolation` Message: `` `{text}` cannot be interpolated `` Help: `{help}` ### `ww::expand::bad_mark` Message: `` `mark` needs {what} `` ### `ww::expand::bad_mirror` RFC-0012 / proposal 0202: `mirror` reflects across exactly one axis. Message: `` `mirror` needs {what} `` Help: `` `mirror` reflects the part across the plane ` = ` in the parent frame: exactly one of `x=`, `y=` or `z=` (RFC-0012, proposal 0202) `` ### `ww::expand::bad_normal` Message: `` `{found}` is not a face direction `` Help: `` `normal=` is one of pos_x neg_x pos_y neg_y pos_z neg_z, pointing out of the material (D6) `` ### `ww::expand::bad_stock` Message: `` `stock` is malformed `` Help: `{help}` ### `ww::expand::bad_travel` A `travel` child of a `join` that cannot be read (ADR-0075). Message: ``bad `travel`: {detail}`` Help: `` `travel [kind=slide|angle] [axis=x|y|z, optionally -signed] [min=] [max=] [rest=] [step=] [body=part|assembly] [pivot=]` `` ### `ww::expand::duplicate_axis` `pos x=1 x=2`: KDL keeps the last, and the first would be lost without a word. Message: `` `{node}` gives `{key}=` twice `` Help: ``each of `x=`, `y=`, `z=` appears at most once on a `{node}`; the second would silently replace the first`` ### `ww::expand::duplicate_id` Message: `` `{id}` names two things in one assembly `` Help: `` ids are unique among the parts and sub-assemblies of one assembly. Inside a `repeat`, interpolate the loop variable: `id="Name_{i}"` `` ### `ww::expand::duplicate_mark` Message: ``mark `{name}` is declared more than once on one part`` ### `ww::expand::duplicate_node` Message: `` `{node}` is given more than once `` ### `ww::expand::inline_override` Message: `` `{key}` is not a parameter — part `{id}` instantiates no component `` Help: `only a component instance takes parameter overrides; an inline part's expressions see the enclosing scope directly` ### `ww::expand::instance_cycle` Message: ``assembly `{name}` contains itself`` Help: `the chain is: {cycle}` ### `ww::expand::instance_with_stock` Message: `` part `{id}` has both `instance=` and its own `stock` `` Help: ``a `part` either instantiates a component or carries an inline `stock`, never both (docs/KDL_AUTHORING.md §9). The component's `stock` is the board`` ### `ww::expand::material_refusal` ADR-0076: a `substance=` / `tempered=` that is malformed, or a feature the material cannot take (a cut in tempered glass). Message: `a material cannot be made or cut that way` ### `ww::expand::missing_stock` Message: `` {what} has no `stock` `` Help: ``every part is cut from a board: `stock [grain]` (ADR-0007)`` ### `ww::expand::not_a_number` Message: `` `{what}` is {found}, not a number `` ### `ww::expand::not_a_part` Message: `` `{part}` is a sub-assembly, not a part `` Help: `a join mates two boards; name a part inside the sub-assembly from within it` ### `ww::expand::not_text` RFC-0122's `finish=`: a genuine string literal needs the CEL single-quote escape hatch (ADR-0004) — `finish "'danish oil'"`, not `finish "danish oil"`, which CEL reads as an (unknown) identifier. Message: `` `{what}` is {found}, not text `` Help: ``a genuine string literal is a CEL expression that evaluates to one: `finish "'danish oil'"`, not `finish "danish oil"` (ADR-0004)`` ### `ww::expand::one_sided_join` `join joint=X male=Part@Mark` with no `female=`, for a joinery that joins two parts (RFC-0128, ADR-0030): only a `hardware_mount` mounts onto one. Message: `` `join joint={joint}` names no `female=`, but a `{kind}` joint joins two parts `` Help: ``only a `hardware_mount` joinery may be fired with `male=` alone (one item on one part)`` ### `ww::expand::skeleton` ADR-0081: a malformed `locus`, a `part … in=` naming an unresolved locus, a `trimmed_by { … }`/`outline` that cannot be reconciled with its carrier, a `joints { rule … }` that is malformed or ambiguous, or a structural contact `joints {}` does not cover. One catch-all variant, like `BadStock`: the message and help carry the specifics, which differ by case far more than they repeat. Message: `{message}` Help: `{help}` ### `ww::expand::stray_argument` A bare word where only `x=`/`y=`/`z=` belong: nearly always a second node written on the same line (`pos x=1 mirror x=2`), which KDL reads as one `pos` carrying the argument `mirror` and a repeated `x=`. Refused rather than silently misplaced. Message: `` `{node}` takes no bare argument, but `{found}` was written after it `` Help: `{help}` ### `ww::expand::too_deep` `Limits::max_depth`, counted in assemblies: a root, every `assembly` nested inside it, and every sub-assembly placed with `instance=`, all the way down. Diagnosed before anything is placed, and before the check itself recurses past the limit. Message: `assemblies nest more than {limit} deep` Help: ``counting nested `assembly` blocks and sub-assemblies placed with `instance=`, a part may sit at most {limit} assemblies deep (docs/KDL_AUTHORING.md §13)`` ### `ww::expand::too_many_parts` `Limits::max_parts`, counted with every sub-assembly instance multiplied out. Diagnosed from per-assembly counts, before any part is placed. Message: `the project places {total} parts; the limit is {limit}` Help: ``a project may expand to at most {limit} parts, counting every part each `instance=` of a sub-assembly places (docs/KDL_AUTHORING.md §13)`` ### `ww::expand::unexpected_node` Message: `` `{node}` is not valid here `` Help: `{expected}` ### `ww::expand::unexpected_property` Message: `` `{node}` does not take `{key}` `` Help: `{expected}` ### `ww::expand::unknown_anchor` Message: `` `{name}` is not one of the standard anchors `` Help: ``D6's anchors are top_/bottom_ followed by sw, se, ne, nw, n, s, e, w or center, plus `center` for the body centre`` ### `ww::expand::unknown_hardware` `hardware_use ` naming no `hardware` catalogue item (ADR-0017). Message: `` `{name}` is not a declared hardware item `` Help: `{help}` ### `ww::expand::unknown_instance` Message: `` `{name}` is not a component or a top-level assembly `` Help: `` `instance=` names a `component`, or an `assembly` declared at the top level `` ### `ww::expand::unknown_interpolation` Message: `` `{name}`, interpolated in `{text}`, names nothing in scope `` Help: `` `{…}`/`<…>` interpolates a name visible where the id is written — usually a `repeat` variable. It is a name, not an expression `` ### `ww::expand::unknown_joint` Message: `` `{name}` is not a declared joinery `` Help: `{help}` ### `ww::expand::unknown_mark` Message: `` part `{part}` has no mark `{mark}` `` Help: `{help}` ### `ww::expand::unknown_material` Message: `` `{name}` is not a declared material `` Help: `{help}` ### `ww::expand::unknown_part` Message: `` no part `{part}` for `{part}@{mark}` `` Help: `{help}` ## Stage `joinery` ### `ww::joinery::not_yet_synthesized` Reported by the compiler with a message built at run time. ## Stage `lattice` ### `ww::lattice::degenerate_grid` Message: `` `lattice rows={rows} cols={cols}` needs both at least 1 `` Help: `` `rows`/`cols` count compartments (§6.8): at least one of each `` ### `ww::lattice::missing_prop` Message: `` `lattice` has no `{key}=` `` Help: `` `lattice` needs `rows=`, `cols=`, `bar_th=`, `wall_height=`, `fit_clearance=`, `border_th=`, `inner_width=` and `inner_length=`, all literal numbers `` ### `ww::lattice::non_literal_prop` Message: `` `lattice`'s `{key}=` must be a literal number, not a CEL expression `` Help: `the grid's own shape (how many parts/joins exist) is decided before any CEL expression is evaluated (docs/superpowers/plans/2026-10-29-drawer-dividers-698-plan.md): write the resolved number directly` ### `ww::lattice::omit_out_of_range` Message: `` `omit {form}` names a grid line outside the lattice `` Help: `` a single-coordinate `omit x= y=` needs `x` in `1..cols-1` and `y` in `1..rows`; a region `omit from_x= from_y= to_x= to_y=` needs its bounds inside `0..cols` and `0..rows` `` ## Stage `length` ### `ww::length::ambiguous` Reported by the compiler with a message built at run time. ### `ww::length::bad_fraction` Reported by the compiler with a message built at run time. ### `ww::length::bad_number` Reported by the compiler with a message built at run time. ### `ww::length::bad_unit` Reported by the compiler with a message built at run time. ### `ww::length::empty` Reported by the compiler with a message built at run time. ### `ww::length::out_of_range` Reported by the compiler with a message built at run time. ### `ww::length::zero_denominator` Reported by the compiler with a message built at run time. ## Stage `link` ### `ww::link::inverse` Reported by the compiler with a message built at run time. ## Stage `lower` ### `ww::lower::wasteful_stock` Reported by the compiler with a message built at run time. ## Stage `override` ### `ww::override::conflict` Reported by the compiler with a message built at run time. ### `ww::override::derived` Reported by the compiler with a message built at run time. ### `ww::override::inverse` Reported by the compiler with a message built at run time. ### `ww::override::inverse_unknown` Reported by the compiler with a message built at run time. ### `ww::override::length` Reported by the compiler with a message built at run time. ### `ww::override::range` Reported by the compiler with a message built at run time. ### `ww::override::step` Reported by the compiler with a message built at run time. ### `ww::override::type` Reported by the compiler with a message built at run time. ### `ww::override::unknown` Reported by the compiler with a message built at run time. ### `ww::override::unresolved` Reported by the compiler with a message built at run time. ## Stage `plan` ### `ww::plan::ambiguous_ref` Message: `` `{name}` is ambiguous in this plan `` Help: `{detail}` ### `ww::plan::cladding_of_edge` Message: `` `of={name}` names an `edge`, not a `wall` `` Help: ``an `edge` bounds a room without being a wall and has no face to clad`` ### `ww::plan::cladding_of_shares_wall` Message: `` `of={name}` names a `shares=`'d wall, which has no face of its own `` Help: ``a `shares=`'d wall has no endpoints of its own to build a face from (RFC-0154's own adversarial review, item 5, left open) — clad the wall it shares, or the other room's own side of it, if that has an exterior face`` ### `ww::plan::cladding_of_unknown_wall` Message: `` `of={name}` names no wall of this room `` Help: `` `cladding`'s `of=` must name one of this room's own `wall` sides `` ### `ww::plan::clearance_outside_room` Reported by the compiler with a message built at run time. ### `ww::plan::clearance_overlap` Reported by the compiler with a message built at run time. ### `ww::plan::clearance_too_close` Reported by the compiler with a message built at run time. ### `ww::plan::cover_of_forward_layer` Message: `` `of={name}` names a layer declared later (or self) in this room `` Help: `` `cover`/`upstand`'s `of=` must name a layer already declared earlier in the same room (#656 §2.3) `` ### `ww::plan::cover_of_wrong_body` Message: `` `of={name}` names a `{kind}`-bodied layer, which `cover`/`upstand` cannot derive a footprint from `` Help: `` `cover`/`upstand` derive a footprint from a layer's placed geometry, which only a `path`-bodied layer has (#656 §2.3): `of=` must name a `path` layer, not another `cover`, `upstand` or `roof` layer `` ### `ww::plan::direction_on_freestanding_path` Message: `` `direction=` on a `path` whose `{node}` leaves the room perimeter `` Help: `` `direction=` has no meaning once a `path` leaves the perimeter — remove it, or address `{node}` with `corner=` instead (final consolidation §1/§6.1) `` ### `ww::plan::duplicate_interface` Message: ``a second `interface` on this assembly`` Help: `` an assembly may declare at most one `interface` `` ### `ww::plan::duplicate_name` Message: `` `{name}` is declared more than once in this plan `` Help: `walls, openings, features, obstacles, runs, places and marks share one namespace per plan` ### `ww::plan::duplicate_plan` Message: ``a second `plan`/`site` in one project`` Help: ``at most one `plan`/`site` per project (ADR-0060 Q2); a project needing two unrelated plans is two files`` ### `ww::plan::empty_layer` Message: `` layer `{layer}` has no `path` `` Help: ``a `layer` needs at least one `path` (or, once Task 5 lands, a `roof`/`cover`/`upstand`)`` ### `ww::plan::exclude_forward_instance` Message: `` `exclude {name}` names an instance placed in this layer or a later one `` Help: `` `exclude ` may only name an instance placed in an *earlier* layer of the same room (final consolidation §3.2) `` ### `ww::plan::expose_unknown` Message: `` `expose {name}` names neither a param of `{design}` nor a project param of its file `` ### `ww::plan::fill_without_stretch` Message: `` `fill` names `{design}`, which has no `stretch` param `` Help: ``a `fill` solves a `stretch` param to close the run; expose and `stretch` one on `{design}`'s interface`` ### `ww::plan::footprint_outside` Message: `` `{design}`'s built solids ({built}) reach outside its declared footprint ({footprint}) `` Help: `the footprint is an envelope (ADR-0060 §4.6, Q8): enlarge it, or bring the build inside it` ### `ww::plan::forward_layer_ref` `height=`/`offset=`/`anchor=` naming `_bottom`/`_top`/`_height` of a layer declared later (or of itself) in the same room (#653 §6.4, #647/#657 Task 3). Named `ww::plan::forward_layer_ref`, not `ww::eval::*`, deliberately: this diagnostic is part of the `ww::plan::*` family the rest of the layer/path grammar's diagnostics belong to (`crates/ww-dsl/src/error.rs`), even though the check itself can only run here — it needs `Program::references().variables()`, which only this crate's `cel` dependency provides (`ww-dsl` has none). Message: `` `{expr}` names a layer declared later in this room `` Help: ``a `height=`/`offset=`/`anchor=` expression may only reference `_bottom`/`_top`/`_height` of a layer declared earlier in the same room (#653 §6.4)`` ### `ww::plan::forward_path_ref` Message: `` `{name}` names a path declared later (or self) in this room `` Help: `` `path_start=`/`path_end=`/`between=` naming `{name}_start`/`{name}_end` need that path declared earlier in the same room (first synthesis §3.1) `` ### `ww::plan::layer_overflow` A sequentially-packed segment (module doc: the independently-packed run `exclude` splits a path into) sums to more than the path's own available length — a wall or room-perimeter run, for a segment this module fully resolved (module doc's own "stand-in, not final" discipline: a path mixing sequential packing with an unresolved mode such as `anchor=` never reaches this check at all, because `lower_path` bails the whole path to `None` before any segment is measured; this variant only ever sees a segment this module is fully confident in). Message: `{segment} packs to {packed_mm:.1}mm, {overshoot_mm:.1}mm past its {available_mm:.1}mm available length` ### `ww::plan::mark_shadows_face` Message: `` `mark {name}` shadows a face name `` Help: ``a mark may not be named `front`, `back`, `left`, `right`, `top` or `bottom`: those name faces`` ### `ww::plan::missing_roof` Message: `` room `{room}` has `layer`s but no `Roof` layer and no `roof_of` `` Help: ``say so explicitly: give this room its own `layer Roof` with a `roof` child (Task 5), or `roof_of room=` naming the room whose roof covers it (final consolidation §3.4) — never an implicit default`` ### `ww::plan::missing_wall_thickness` Message: `` wall `{wall}` bears room `{room}`'s shed roof but declares no `thickness=` `` Help: ``the roof's bearing/seat math (2026-10-25-roof-truss-lowering-683.md) needs this wall's thickness — add `thickness=` to `wall {wall}` (2026-10-25-wall-thickness-683.md §2); only a shed roof's `high_side=` is checked here, a gable roof's bearing walls are not yet determined structurally and so are not enforced by this diagnostic`` ### `ww::plan::mount_needs_wall` Message: `` `{design}` mounts to a wall but `{place}` has neither a `run` nor an `against` `` Help: `` a wall-mounted design needs a wall to hang from: put it in a `run`, or give it an `against ` `` ### `ww::plan::no_footprint` Message: `` `interface` has no `footprint` `` Help: ``a placeable design needs `footprint w= d= h=` (ADR-0060 §4.6)`` ### `ww::plan::no_interface` Message: `` `{design}` has no `interface` `` Help: ``only a design with an `interface` can be placed (ADR-0060 §4.6)`` ### `ww::plan::no_layer_height` Message: `` layer `{layer}` has no `height=` `` Help: `` `height=` is required on every `layer` (#653 §3): it is the layer's bottom elevation `` ### `ww::plan::not_abuttable` Message: `` `{design}` has no `abut` faces, so it cannot sit mid-run `` Help: ``declare `abut left right` (or whichever faces touch a neighbour) on `{design}`'s interface`` ### `ww::plan::not_exposed` Message: `` `{key}` overrides a param `{design}` does not expose `` Help: `` only params an `interface`'s `expose` lists may be overridden on a `place` `` ### `ww::plan::op` Reported by the compiler with a message built at run time. ### `ww::plan::opening_off_wall` Message: `` `{name}` sits off the end of wall `{wall}` `` Help: `` `at=` plus `width=` must fit between the wall's two ends `` ### `ww::plan::packing_prop_outside_path` Message: `` `{key}=` on a `place` that is not inside a `path` `` Help: `` `offset_pin=`, `offset=`, `corner=`, `between=` and `anchor=` only mean something on a `place` inside a `path` (#653 §5); a `run` has its own sequential/`fill` packing `` ### `ww::plan::path_endpoint_ambiguous` Message: `` more than one addressing form on this `{node}` `` Help: ``a `path`'s `start`/`end` takes exactly one of `corner=`, `x=`/`y=`, or `path_start=`/`path_end=` (final consolidation §6.1)`` ### `ww::plan::path_endpoint_incomplete_xy` Message: `` `{node}` has `{given}=` without its pair `` Help: `` `x=` and `y=` on a `path`'s `start`/`end` must both be given (final consolidation §1/§6.1) `` ### `ww::plan::path_endpoint_off_wall` Message: `` `{node}` is not on `wall={wall}` `` Help: ``on a `wall=`-scoped `path`, `start`/`end` are interpreted as the two ends of that one wall (final consolidation §4): a `corner=` must be one of that wall's own two ends, and an `x=`/`y=` point must lie on it`` ### `ww::plan::path_wall_unknown` Message: `` `wall={wall}` names no wall or edge of this room `` Help: ``a `path`'s `wall=` must name one of the room's own `wall`/`edge` sides (final consolidation §4)`` ### `ww::plan::plan_in_import` Message: `` `{path}` declares a `plan`, which an imported file cannot do `` Help: ``plans are not importable (ADR-0060 Q15); move the `place` of this design into the importing file's own plan`` ### `ww::plan::reserved_param` Message: `` `expose {name}` collides with the reserved override `{reserved}` `` Help: `` `label`, `finish`, `mirror` and `role` are reserved on `place`; expose a different name `` ### `ww::plan::rev` Reported by the compiler with a message built at run time. ### `ww::plan::roof_missing_high_side` Message: `` `roof kind=shed` has no `high_side=` `` Help: `` `high_side=` is required for `kind=shed` (#653 §8): it names the wall the ridge runs along `` ### `ww::plan::roof_missing_ridge_axis` Message: `` `roof kind=gable` has no `ridge=` `` Help: `` `ridge=along_x|along_y` is required for `kind=gable` (#653 §8) `` ### `ww::plan::roof_mixed_with_path` Message: `` a `layer` mixes `{a}` with `{b}` `` Help: ``a `layer` uses exactly one mechanism: `path`, `roof`, `cover` or `upstand` — never more than one`` ### `ww::plan::roof_pitch_conflict` Message: `` `roof` gives both or neither of `pitch=` and `rise_in_12=` `` Help: ``exactly one of `pitch=` or `rise_in_12=` is required`` ### `ww::plan::roof_ref_not_shared_room` Message: `` `roof_of room={name}` names a room this one does not share a wall with `` Help: `` `roof_of=` may only name a room this one is wall-connected to via `shares=` (final consolidation §3.4) `` ### `ww::plan::room_not_closed` Message: ``room `{room}` is not a closed polygon`` Help: `` the last `wall`/`edge` must return to `start x= y=` `` ### `ww::plan::room_self_intersecting` Message: ``room `{room}`'s walls cross themselves`` Help: `list a room's sides in order round the room, either direction, with no crossing` ### `ww::plan::shares_dimension_mismatch` Message: `` `shares` wall `{name}` is a different length than `{room}.{wall}` `` Help: ``without `span=`, a `shares`'d wall must be the same length as the wall it shares`` ### `ww::plan::shares_opening_mismatch` Message: `` `shared_with`'s restated `{key}=` disagrees with `{room}.{opening}`'s own value `` Help: `` `at=`/`width=` are optional on a `shared_with` opening (derived from the counterpart); restated anyway, they must agree (final consolidation §3.3) `` ### `ww::plan::shares_span_out_of_range` Message: `` `shares`'s `span` exceeds `{room}.{wall}`'s own length `` Help: `` `span`'s `at=`/`length=` must fit inside the named wall's own length `` ### `ww::plan::shares_wall_has_coordinates` Message: `` a `shares`'d wall also gives its own `x=`/`y=`/`thickness=` `` Help: `` a `shares`'d wall's position and thickness are both derived from the named wall, never independently stated (final consolidation §3.1; thickness per 2026-10-25-wall-thickness-683.md §3) — remove `x=`/`y=`/`thickness=`, or remove `shares` `` ### `ww::plan::span_self_reference` Message: `` `between=` names the same anchor twice `` Help: `` `between= ` needs two distinct anchors `` ### `ww::plan::standard_out_of_range` Message: `` `standard` value for `{name}` is outside `{min}..{max}` `` ### `ww::plan::unknown_design` Message: `` `{design}` names no top-level assembly in scope `` Help: ``a `place`/`fill`/`needs design=` names a top-level assembly of this file or an imported one`` ### `ww::plan::unknown_ref` Message: `` `{name}` names nothing in this plan `` Help: `{available}` ## Stage `preset` ### `ww::preset::derived` Reported by the compiler with a message built at run time. ### `ww::preset::duplicate` Message: ``preset `{name}` is declared more than once in this project`` Help: `two presets may not share a name; rename one of them` ### `ww::preset::range` Reported by the compiler with a message built at run time. ### `ww::preset::step` Reported by the compiler with a message built at run time. ### `ww::preset::type` Reported by the compiler with a message built at run time. ### `ww::preset::unknown` Reported by the compiler with a message built at run time. --- # Examples 116 complete designs live in `examples/`; each is built by the test suite. Three are shown in full below (and compiled again when this page is generated), then the library is listed. ## `drawer_stack.kdl` ```kdl // examples/drawer_stack.kdl // // A BANK OF DRAWERS, instanced at two widths — the worked example of `import` and of // overriding an assembly's params where it is instanced (proposal 0130, ADR-0020). // // - `import DrawerBox from="std:drawer"` brings in a built-in library (no file needed, so // this works in the browser too): a Baltic-birch drawer box with corner finger joints and // a captured bottom, parametric in `width`, `depth` and `height`. // - `DrawerBank` stacks three of them, each at its own height, from the bank's own params. // - `DrawerStack` instances `DrawerBank` twice. The narrow bank keeps its defaults; the // wide one overrides `width` (from this file's slider) and `h_low`. Each distinct set of // overrides is compiled once and placed wherever it is used, so the two banks are two // different sets of parts, not a resized copy. // // Part ids run instancing part, then the library's own part names: `Wide/Bottom/Front`. // For a library that lives in its own file see examples/imports/two_banks.kdl. project name="Drawer Stack" units=mm schema_version=1 { import DrawerBox from="std:drawer" export=DrawerBox params { bank_width 600.0 min=400.0 max=900.0 step=10.0 label="Wide bank width" category="Envelope" bank_gap 60.0 min=30.0 max=200.0 step=5.0 label="Space between banks" category="Envelope" } assembly DrawerBank { params { width 400.0 min=250.0 max=900.0 step=10.0 label="Drawer width" depth 450.0 min=250.0 max=700.0 step=10.0 label="Drawer depth" gap 10.0 min=4.0 max=30.0 step=1.0 label="Gap between drawers" h_top 90.0 min=60.0 max=300.0 step=5.0 label="Top drawer height" h_mid 120.0 min=60.0 max=300.0 step=5.0 label="Middle drawer height" h_low 180.0 min=60.0 max=300.0 step=5.0 label="Bottom drawer height" } part Top instance=DrawerBox { width width depth depth height h_top by top_sw pos x=0.0 y=0.0 z=0.0 } part Middle instance=DrawerBox { width width depth depth height h_mid by top_sw pos x=0.0 y=0.0 z="0.0 - h_top - gap" } part Bottom instance=DrawerBox { width width depth depth height h_low by top_sw pos x=0.0 y=0.0 z="0.0 - h_top - h_mid - (2.0 * gap)" } } assembly DrawerStack { part Narrow instance=DrawerBank { by top_sw pos x=0.0 y=0.0 z=0.0 } part Wide instance=DrawerBank { width bank_width h_low 260.0 by top_sw pos x="400.0 + bank_gap" y=0.0 z=0.0 } } } ``` ## `inlaid_tray.kdl` ```kdl // examples/inlaid_tray.kdl // // BANDING / STRINGING / SUNBURST FIXTURE (M30, docs/rfc/0141-banding-stringing-sunburst.md, // docs/adr/0025-banding-stringing-inlay.md) — the only piece in the library exercising // `banding`, `stringing` and `veneer match=sunburst`. Two small show panels of the kind // a Federal or Hepplewhite maker would inlay: a tray top and a round-fan lid panel. // // Tray 300 x 200 x 22 poplar. Its top carries, from the edge in: a 1.5mm holly // string at 6mm, a 10mm herringbone band in walnut at 10mm (two half-strips // cut on opposing 45 degree diagonals, glued edge to edge) and a second // holly string at 22mm — the two strings are one `repeat`, ADR-0015. Every // strip is let flush into its own groove (3mm for the band, 2mm for a // string), mitred at the four corners. // Fan 160 x 160 x 16 poplar, faced with 8 walnut veneer leaves laid as a // sunburst from the middle of the panel, 0.6mm thick (paper-backed). // // No existing example fits: the closest, biedermeier_box.kdl, is veneer only — a book // match, not a fan — and nothing in the library lets a contrasting strip into a face. // // Why a tray top and not a drawer front or a table: the inlay's whole claim is that // the groove and the filler agree, which is a property of one panel's face, not of a // carcase; a small flat panel isolates it. The strips do not touch the host's other // parts, because there are none. // // Expected clashes, checked with `ww clash`: none. Every filler sits flush in its groove // (a coplanar, complementary contact, exactly a flush veneer's) and the leaves are // bonded flush to their own face; no `join` connects any of them to its host. project name="Inlaid Tray" units=mm schema_version=1 { params { tray_width 300.0 min=240.0 max=400.0 step=10.0 label="Tray Width" category="Tray" tray_depth 200.0 min=160.0 max=300.0 step=10.0 label="Tray Depth" category="Tray" tray_th 22.0 min=18.0 max=30.0 step=1.0 label="Tray Thickness" category="Tray" band_width 10.0 min=6.0 max=14.0 step=1.0 label="Band Width" category="Inlay" band_offset 10.0 min=8.0 max=14.0 step=1.0 label="Band Offset" category="Inlay" string_gap 16.0 min=12.0 max=20.0 step=1.0 label="String Spacing" category="Inlay" fan_size 160.0 min=120.0 max=200.0 step=10.0 label="Fan Panel Size" category="Fan" fan_leaves 8.0 min=6.0 max=16.0 step=1.0 label="Fan Leaves" category="Fan" } materials { material Poplar type=hardwood // The band: dark against the pale ground. material Walnut type=hardwood // The strings: the traditional pale line, holly. material Holly type=hardwood } assembly InlaidTray { part Tray role=panel { stock Poplar tray_width tray_depth tray_th lengthwise by top_sw pos x=0.0 y=0.0 z=0.0 // The band, between the two strings. Herringbone: each strip is two half-width // pieces at +45 and -45 degrees, so the figure chevrons round the panel. banding face=pos_z material=Walnut width=band_width depth=3.0 \ offset=band_offset pattern=herringbone // Two fine lines, one either side of the band: offsets 6 and 22 at the default // spacing. `repeat` unrolls in both the groove and the filler generator. repeat i count=2 { stringing face=pos_z material=Holly offset="6.0 + i * string_gap" } } part Fan role=panel { stock Poplar fan_size fan_size 16.0 lengthwise by top_sw pos x="tray_width + 30.0" y=0.0 z=0.0 // A sunburst: leaves radiate from the middle of the panel, each clipped to the // face. Geometry only — the figure of each leaf is its recorded grain angle. veneer face=pos_z material=Walnut thickness=0.6 match=sunburst leaves=fan_leaves } } } ``` ## `chess_pawn.kdl` ```kdl // examples/chess_pawn.kdl // // A turned boxwood chess pawn: the simplest object that is a pure surface of revolution. // Real pawn: ~45mm tall; base ~12mm diameter; skirt (belly) ~9mm; ball on top ~8mm. // // `stock shape=round` declares the round blank (RFC-0110), and `section axis=z // interp=spline` (proposal 0101, RFC-0111) sweeps the profile through the ball, neck, // collar, stem, skirt and base in one `revolve()` call. `AxisTop`/`AxisBottom` (RFC-0112) // coincide exactly with `top_center`/`bottom_center`, and grain is derived as lengthwise, // along the axis (RFC-0113). // // THE PROFILE, TOP DOWN. A section's `z` runs 0 at the top to negative values below it // (D4), and a station's `circle r=` is a radius. The ball is sampled on a true sphere of // radius `ball_diameter / 2` (stations at 0, 45, 90 and 135 degrees round it, the // 90-degree one the equator), so the ball stays round at every `ball_diameter`. The neck, // the collar ring (`collar_height` is the whole collar zone, ring and undercut), the stem // and the skirt are fractions of `belly_diameter` and `total_height`, so every slider // moves the whole silhouette with it rather than one hard-coded station. // // PLACEMENT. The part's origin is its top (D4); `by top_center` with `pos z=total_height` // stands the pawn on the floor, base at z = 0, top at z = total_height. project name="Chess Pawn" units=mm schema_version=1 { params { // ---- Envelope (user-facing) ---- total_height 45.0 min=40.0 max=50.0 step=1.0 label="Total Height" category="Envelope" base_diameter 12.0 min=10.0 max=14.0 step=0.5 label="Base Diameter" category="Envelope" belly_diameter 9.0 min=8.0 max=10.0 step=0.5 label="Belly Diameter" category="Envelope" collar_height 5.0 min=3.0 max=7.0 step=0.5 label="Collar Height" category="Envelope" ball_diameter 8.0 min=6.0 max=9.0 step=0.5 label="Ball Diameter" category="Envelope" // ---- Stock ---- // RFC-0110: the blank is the round bar you buy, not the finished shape. It must // be at least as wide as the widest turned station, the base at `base_diameter` // (14.0 at its slider maximum), and `ww_build::turning` refuses a narrower one // (proposal 0101 section 3); `stock_size` therefore starts at that maximum. stock_size 14.0 min=14.0 max=16.0 step=0.5 label="Stock Blank" category="Stock" // ---- Derived ---- ball_r "ball_diameter / 2.0" belly_r "belly_diameter / 2.0" base_r "base_diameter / 2.0" // Where the collar zone starts, below the ball and a short neck. collar_top "ball_diameter + 1.2" } materials { material Boxwood type=hardwood } // ================================================================== // THE PAWN COMPONENT // ================================================================== component Pawn role=game_piece { // RFC-0110: a round blank — the square stock a lathe starts from, `stock_size` on // a side, `total_height` long down the spindle axis. stock Boxwood shape=round diameter=stock_size z=total_height // The turned profile (RFC-0111, proposal 0101 §1.3): a `circle` keyframe at every // station a caliper would check on the real pawn, `interp=spline` sweeping a // smooth curve between them — the base's cove, the stem's collar-belly-neck // curve, the ball's dome — in the one `revolve()` call ADR-0018 verifies. Radii, // not diameters, per the grammar; every station reachable from the envelope // params above. section axis=z interp=spline samples=64 { // Ball: four stations round a sphere centred `ball_r` below the top. at z=0.0 { circle r="ball_r * 0.25" } at z="0.0 - (ball_r * 0.293)" { circle r="ball_r * 0.707" } at z="0.0 - ball_r" { circle r="ball_r" } at z="0.0 - (ball_r * 1.707)" { circle r="ball_r * 0.707" } // Neck, then the collar ring: a short flare, the ring, and the undercut. at z="0.0 - (ball_diameter + 0.5)" { circle r="belly_r * 0.4" } at z="0.0 - collar_top" { circle r="belly_r * 0.44" } at z="0.0 - (collar_top + 0.5)" { circle r="belly_r * 0.8" } at z="0.0 - (collar_top + (collar_height * 0.4))" { circle r="belly_r * 0.8" } at z="0.0 - (collar_top + collar_height)" { circle r="belly_r * 0.6" } // Stem: a slim waist, then the concave flare out to the skirt. at z="0.0 - (total_height * 0.52)" { circle r="belly_r * 0.54" } at z="0.0 - (total_height * 0.72)" { circle r="belly_r * 0.68" } at z="0.0 - (total_height * 0.84)" { circle r="belly_r" } // Base: the skirt's foot swells to the base disc, which then runs straight. at z="0.0 - (total_height * 0.90)" { circle r="base_r * 0.96" } at z="0.0 - (total_height * 0.93)" { circle r="base_r" } at z="0.0 - (total_height * 0.97)" { circle r="base_r" } at z="0.0 - total_height" { circle r="base_r" } } // RFC-0112: `top_center`/`bottom_center` (D6, already free on every part) sit // exactly on the spindle axis for a `shape=round` part — the circumscribing // square's centre, by construction (ADR-0018 §4). These two marks name it for // the assembly's placement and any future joinery (a socket this pawn might sit // in), the same role `mark` always plays; they are not a stand-in for a part // origin the schema lacks. mark AxisTop x="stock_size / 2.0" y="stock_size / 2.0" z=0.0 normal=pos_z mark AxisBottom x="stock_size / 2.0" y="stock_size / 2.0" z="0.0 - total_height" normal=neg_z } // ================================================================== // ASSEMBLY // ================================================================== assembly ChessPawn { // A single pawn, standing on the floor: the top-centre anchor lifted to // `total_height`, so the base is at z = 0. Per D5, placement is // M = T(pos) * R(rot) * T(-anchor). part PawnPiece instance=Pawn { by top_center pos x=0.0 y=0.0 z=total_height } } } ``` ## The library | File | Lines | What it is | | --- | --- | --- | | `acrylic_panel_frame.kdl` | 91 | examples/acrylic_panel_frame.kdl | | `adirondack_plank_chair.kdl` | 337 | A Westport / Adirondack plank chair, ~720 W x 940 D x 940 H: everything is flat board stock, | | `adirondack_pole_side_table.kdl` | 268 | A Great-Camp rustic side table, ~560 W x 420 D x 600 H, built the way the style builds it | | `appalachian_ladder_back_side_chair.kdl` | 347 | Appalachian ladder-back side chair -- a post-and-rung chair in the mountain tradition. | | `appalachian_post_and_rung_bench.kdl` | 456 | Appalachian post-and-rung bench -- a two-seat hall or porch settle in the mountain tradition. | | `arched_crest_side_chair.kdl` | 337 | examples/arched_crest_side_chair.kdl | | `art_deco_cabinet_on_fuseau_legs.kdl` | 352 | examples/art_deco_cabinet_on_fuseau_legs.kdl | | `art_deco_skyscraper_bookcase.kdl` | 287 | examples/art_deco_skyscraper_bookcase.kdl | | `arts_crafts_cabinet_on_stand.kdl` | 358 | examples/arts_crafts_cabinet_on_stand.kdl | | `arts_crafts_hayrake_table.kdl` | 328 | examples/arts_crafts_hayrake_table.kdl | | `backyard_playset.kdl` | 963 | examples/backyard_playset.kdl | | `bed.kdl` | 347 | examples/bed.kdl | | `bedside_cabinet.kdl` | 588 | examples/bedside_cabinet.kdl | | `biedermeier_box.kdl` | 234 | examples/biedermeier_box.kdl | | `biedermeier_chest_of_drawers.kdl` | 526 | examples/biedermeier_chest_of_drawers.kdl | | `biedermeier_pedestal_table.kdl` | 269 | examples/biedermeier_pedestal_table.kdl | | `blanket_chest.kdl` | 703 | examples/blanket_chest.kdl | | `bookcase.kdl` | 547 | examples/bookcase.kdl | | `chess_pawn.kdl` | 114 | examples/chess_pawn.kdl | | `country_rush_seat_ladder_chair.kdl` | 422 | examples/country_rush_seat_ladder_chair.kdl | | `curved_apron_side_table.kdl` | 186 | examples/curved_apron_side_table.kdl | | `danish_oak_dining_table.kdl` | 248 | examples/danish_oak_dining_table.kdl | | `danish_slat_back_chair.kdl` | 399 | examples/danish_slat_back_chair.kdl | | `danish_teak_sideboard.kdl` | 481 | examples/danish_teak_sideboard.kdl | | `deco_console_table.kdl` | 272 | examples/deco_console_table.kdl | | `deco_side_chair.kdl` | 541 | examples/deco_side_chair.kdl | | `deco_veneered_tray.kdl` | 104 | examples/deco_veneered_tray.kdl | | `desk.kdl` | 449 | examples/desk.kdl | | `dining_chair.kdl` | 561 | examples/dining_chair.kdl | | `dining_table.kdl` | 381 | examples/dining_table.kdl | | `drawer_dividers.kdl` | 69 | examples/drawer_dividers.kdl (#698) | | `drawer_stack.kdl` | 72 | examples/drawer_stack.kdl | | `ebonised_incised_cabinet.kdl` | 1093 | examples/ebonised_incised_cabinet.kdl | | `empire_columned_chest.kdl` | 576 | examples/empire_columned_chest.kdl | | `empire_pier_table.kdl` | 356 | examples/empire_pier_table.kdl | | `farmhouse_harvest_table.kdl` | 590 | examples/farmhouse_harvest_table.kdl | | `federal_chest_of_drawers.kdl` | 592 | examples/federal_chest_of_drawers.kdl | | `federal_tapered_leg_sideboard.kdl` | 504 | examples/federal_tapered_leg_sideboard.kdl | | `filing_cabinet_lateral.kdl` | 512 | examples/filing_cabinet_lateral.kdl | | `filing_cabinet_vertical.kdl` | 594 | examples/filing_cabinet_vertical.kdl | | `flat_pack_scale_test_table.kdl` | 161 | examples/flat_pack_scale_test_table.kdl | | `glazed_vitrine_whiplash_crest.kdl` | 669 | examples/glazed_vitrine_whiplash_crest.kdl | | `high_back_ladder_chair.kdl` | 351 | examples/high_back_ladder_chair.kdl | | `incised_walnut_washstand.kdl` | 535 | examples/incised_walnut_washstand.kdl | | `inlaid_tray.kdl` | 79 | examples/inlaid_tray.kdl | | `jacobean_joined_chest.kdl` | 424 | examples/jacobean_joined_chest.kdl | | `jacobean_joint_stool.kdl` | 310 | examples/jacobean_joint_stool.kdl | | `japanese_bench.kdl` | 322 | examples/japanese_bench.kdl | | `kitchen_collections.kdl` | 204 | examples/kitchen_collections.kdl | | `kumiko_andon_lantern.kdl` | 421 | examples/kumiko_andon_lantern.kdl | | `loom_shuttle.kdl` | 162 | examples/loom_shuttle.kdl | | `mid_century_credenza.kdl` | 410 | examples/mid_century_credenza.kdl | | `mid_century_slat_bench.kdl` | 238 | examples/mid_century_slat_bench.kdl | | `ming_horseshoe_armchair.kdl` | 495 | examples/ming_horseshoe_armchair.kdl | | `ming_waisted_corner_leg_table.kdl` | 471 | examples/ming_waisted_corner_leg_table.kdl | | `minimal_slab_low_table.kdl` | 162 | examples/minimal_slab_low_table.kdl | | `minimal_wall_console.kdl` | 199 | examples/minimal_wall_console.kdl | | `mission_keyed_bookcase.kdl` | 213 | examples/mission_keyed_bookcase.kdl | | `mission_slatted_settle.kdl` | 357 | examples/mission_slatted_settle.kdl | | `music_stand.kdl` | 588 | examples/music_stand.kdl | | `neo_shaker_cherry_sideboard.kdl` | 531 | Neo-Shaker cherry sideboard, ~1500 x 480 x 880mm: the Shaker case vocabulary (plain | | `neo_shaker_tapered_leg_table.kdl` | 253 | Neo-Shaker cherry tapered-leg dining table, ~1600 x 850 x 740mm: the Shaker table | | `oak_floating_wall_shelf.kdl` | 229 | examples/oak_floating_wall_shelf.kdl | | `oak_koshikake_bench.kdl` | 311 | examples/oak_koshikake_bench.kdl | | `panel_door_and_window.kdl` | 400 | examples/panel_door_and_window.kdl | | `pasadena_bedside_chest.kdl` | 493 | examples/pasadena_bedside_chest.kdl | | `pasadena_library_table.kdl` | 330 | examples/pasadena_library_table.kdl | | `pedestal_table.kdl` | 178 | examples/pedestal_table.kdl | | `pennsylvania_dower_chest.kdl` | 502 | examples/pennsylvania_dower_chest.kdl | | `pennsylvania_schrank.kdl` | 757 | examples/pennsylvania_schrank.kdl | | `pierced_splat_side_chair.kdl` | 566 | examples/pierced_splat_side_chair.kdl | | `pop_laminate_stepped_sideboard.kdl` | 292 | examples/pop_laminate_stepped_sideboard.kdl | | `pop_laminate_totem_divider.kdl` | 331 | examples/pop_laminate_totem_divider.kdl | | `postmodern_broken_pediment_cabinet.kdl` | 331 | examples/postmodern_broken_pediment_cabinet.kdl | | `postmodern_flat_silhouette_chair.kdl` | 231 | examples/postmodern_flat_silhouette_chair.kdl | | `primary_grid_lath_armchair.kdl` | 327 | examples/primary_grid_lath_armchair.kdl | | `primary_grid_lath_side_table.kdl` | 232 | examples/primary_grid_lath_side_table.kdl | | `queen_anne_side_chair.kdl` | 553 | examples/queen_anne_side_chair.kdl | | `queen_anne_tea_table.kdl` | 272 | Queen Anne tea table: a walnut tray-top table on four cabriole legs with pad feet and | | `scandinavian_birch_shelving_unit.kdl` | 201 | examples/scandinavian_birch_shelving_unit.kdl | | `scandinavian_birch_trestle_desk.kdl` | 274 | examples/scandinavian_birch_trestle_desk.kdl | | `serpentine_chest_bracket_feet.kdl` | 508 | Serpentine chest of drawers on ogee bracket feet: mahogany with poplar secondaries, in the | | `serpentine_sideboard.kdl` | 390 | examples/serpentine_sideboard.kdl | | `shaker_cherry_side_chair.kdl` | 619 | examples/shaker_cherry_side_chair.kdl | | `shaker_side_table.kdl` | 469 | examples/shaker_side_table.kdl | | `shaker_tall_chest_of_drawers.kdl` | 452 | examples/shaker_tall_chest_of_drawers.kdl | | `shaker_trestle_dining_table.kdl` | 329 | Shaker trestle dining table, ~1830 x 840 x 740mm: a pine top with pinned breadboard | | `shield_back_side_chair.kdl` | 364 | examples/shield_back_side_chair.kdl | | `shoe_bench.kdl` | 1042 | examples/shoe_bench.kdl | | `six_board_chest.kdl` | 259 | examples/six_board_chest.kdl | | `speaker_ported.kdl` | 323 | examples/speaker_ported.kdl | | `speaker_sealed.kdl` | 323 | examples/speaker_sealed.kdl | | `speaker_transmission_line.kdl` | 247 | examples/speaker_transmission_line.kdl | | `spindle.kdl` | 170 | examples/spindle.kdl | | `square_back_side_chair.kdl` | 498 | examples/square_back_side_chair.kdl | | `step_ladder.kdl` | 351 | examples/step_ladder.kdl | | `studio_cabinet_on_stand.kdl` | 457 | A small studio-craft cabinet on a stand, ~480 W x 330 D x 1080 H: the form the style's own | | `studio_slab_dining_table.kdl` | 271 | A studio-craft slab dining table, ~1900 x 900 x 740 mm, in the form the style's own sketch | | `tall_white_cutout_cabinet.kdl` | 301 | examples/tall_white_cutout_cabinet.kdl | | `tansu_kiri_chest.kdl` | 445 | examples/tansu_kiri_chest.kdl | | `three_legged_stool.kdl` | 611 | examples/three_legged_stool.kdl | | `trestle_table.kdl` | 193 | | | `turned_leg_work_table.kdl` | 205 | examples/turned_leg_work_table.kdl | | `victorian_balloon_back_chair.kdl` | 544 | examples/victorian_balloon_back_chair.kdl | | `victorian_parlour_table.kdl` | 335 | examples/victorian_parlour_table.kdl | | `wagon_wheel.kdl` | 784 | examples/wagon_wheel.kdl | | `wall_shelf.kdl` | 253 | examples/wall_shelf.kdl | | `walnut_side_table.kdl` | 214 | examples/walnut_side_table.kdl | | `wedged_post_and_rung_stool.kdl` | 169 | examples/wedged_post_and_rung_stool.kdl | | `welsh_dresser.kdl` | 966 | examples/welsh_dresser.kdl | | `whiplash_leg_side_table.kdl` | 469 | examples/whiplash_leg_side_table.kdl | | `william_and_mary_chest_of_drawers.kdl` | 520 | examples/william_and_mary_chest_of_drawers.kdl | | `william_and_mary_lowboy.kdl` | 657 | examples/william_and_mary_lowboy.kdl | | `windsor_fan_back_side_chair.kdl` | 661 | examples/windsor_fan_back_side_chair.kdl | | `windsor_round_stool.kdl` | 428 | examples/windsor_round_stool.kdl | | `workbench.kdl` | 383 | examples/workbench.kdl |