# kaicad — the complete documentation > Compile a YAML furniture definition into 3D models, shop drawings, a cut list and a BOM — in your browser, with no account and no upload. Schema version: 0.1 Generated from https://kaicad.barpom.xyz/ Each page below is delimited by its own front matter, and is byte-identical to the `.md` twin served at that page's URL. --- title: "What kaicad is" description: "A client-side compiler that turns a YAML furniture definition into a 3D assembly, shop drawings, a cut list and a BOM — with no agent, no account and no backend." url: https://kaicad.barpom.xyz/docs/start-here/what-kaicad-is schema: "0.1" --- # What kaicad is kaicad compiles a **furniture definition written as YAML** into the things you need to actually build the piece: a 3D assembly you can spin, shop drawings, a cut list, a bill of materials, and a validation report that says whether the design will survive contact with timber. All of it runs in your browser. There is no server, no account, and nothing is uploaded — the CAD kernel is a WebAssembly module that loads once and is then cached. ## There is no AI in the product That is the part people find surprising. kaicad does not draw your furniture for you, and it does not wrap a model in a chat window. Instead, the model you already use writes the YAML — these docs are what it writes against — and kaicad compiles and checks what it wrote. When a check fails you copy the report back into the same chat and it repairs the file. The reason is not modesty about what models can do. Text-to-CAD fails in a cascade: executable code, then valid geometry, then a design that a person could actually build. Most of the first loss is invented API calls. Asking for *data against a published schema* rather than *code* deletes that failure class outright — a wrong fixture ID is caught in milliseconds by a validator, not discovered after a kernel call throws. ## The validator is the product Anyone can arrange boxes in space. What is missing from every open woodworking tool is the layer that knows: - whether two parts occupy the same cubic millimetre, and by how much - whether a screw has enough thread engagement, and whether it is too near an edge - whether a 900 mm shelf in 18 mm pine will sag under books - whether a frame-and-panel top will split when the timber moves 4 mm across the grain in winter - whether the slat gap under a mattress meets the standard - whether the parts can be cut from stock anyone actually sells Every one of those produces a stable code, a JSON Pointer into your document, and a **computed suggestion** — "use `fixtures.wood_screw.4x50`, which gives 30 mm engagement", not "insufficient engagement". ## What it does not do, yet v1 is beds, tables, benches, shelving and open case goods, plus cable management. Doors, hinges, drawers and runners are v2. Metric only. Parts are placed by **anchors** — named points and faces on other parts — and never by absolute coordinates. ## Where to go next - [The loop, end to end](/docs/start-here/the-loop) — the round trip, in detail - [Your first design](/docs/start-here/your-first-design) — from nothing to a compiled bed - [How checks run](/docs/validation/how-checks-run) — what the validator actually does - [For your LLM](/docs/reference/for-your-llm) — the Markdown twins, `llms.txt` and the rest --- --- title: "The loop, end to end" description: "The round trip — write the YAML with an LLM, compile it in the browser, read the report, paste it back — and what has to be true at each step." url: https://kaicad.barpom.xyz/docs/start-here/the-loop schema: "0.1" --- # The loop, end to end ``` ① describe your piece to Claude or ChatGPT ② it emits design.yaml one fenced block, complete ③ paste it into kaicad compiles in your browser, no backend ④ 3D · cut list · BOM · validation report └─ errors? copy the report, paste it back, return to ② ``` Only one of those steps is geometry. The rest is the handover between a chat window and a compiler, which is where this kind of tool usually breaks. ## ① — asking There is no agent in the product, so the model you already use is the one that writes the document. A good brief is short: what you are building, what drives its size, where it goes, what timber, what tools you have, and whether it has to come apart. A model asked for "a queen platform bed in walnut that comes apart" will under-specify around forty parameters. Most of them have defaults, and the ones that do not are the ones worth asking you about. It should never ask you about pilot-hole diameters — that is the validator's job, and asking implies you are responsible for something you are not. ## ② — the file One `design.yaml` per piece, complete and self-contained, in a single fenced block with nothing but YAML inside it. Commentary — the assumptions it made, what to check, what to say if you want it changed — belongs outside the fence, so that selecting the block and copying it gives you a file that works. YAML comments are welcome. They are part of the document, they survive into the app, and they are how the reasoning behind a dimension stays attached to it. ## ③ — getting it in Paste is the primary input, not a fallback. Selecting a code block and hitting copy is easy; extracting a file from a chat is not. kaicad also accepts a dropped `.yaml` file, and a shared `#d=` link — a design travels in the URL fragment, which is never sent to any server. The compile happens in a Web Worker in your browser. Nothing is uploaded, and there is nothing to sign into. ## ④ — the report The compile produces the model and the takeoff, and the validation report alongside them. The report is written for the reader who will repair it: stable codes, JSON Pointer paths, computed suggestions, errors before warnings, in a deterministic order so that two runs are diffable. Copy it, paste it back into the same chat, and ask for the whole file again with the same `meta.id`. That is the loop. [Reading a report](/docs/validation/reading-a-report) · [Repairing from one](/docs/validation/repairing-from-a-report). --- --- title: "Your first design" description: "The shortest path from nothing to a compiled assembly — open the worked example, then paste a document of your own." url: https://kaicad.barpom.xyz/docs/start-here/your-first-design schema: "0.1" --- # Your first design ## 1. Open the example The fastest way to see what kaicad produces is the worked example: a queen platform bed in walnut, 22 parts, knock-down. It opens in the workbench with the part tree, the axonometric view, the checks and the cut list already populated. It is also the design the docs quote from, so a number you see on that screen is a number you can look up here. ## 2. Ask for something ordinary > Just make me a queen platform bed. Walnut. It has to come apart to get up the stairs. A model writing against this schema should state its assumptions — mattress size, slat count, rail section, knock-down fixture, height to the top of the slats — rather than asking you about them, and emit one fenced block. ## 3. Copy the block, paste it in Select the block, copy, and paste it into the front door or onto the library screen. Nothing is uploaded; the compile happens in your browser. ## 4. Read the report before the picture The 3D view is the reassuring part and the report is the useful part. It tells you which rules ran, which found something, and — this matters — **which could not run and why**. A rule that needed geometry and did not get it is reported as *unverified*, never as *passed*. ## 5. Repair, if it needs it Copy the report and paste it back into the same chat. Errors are not negotiable; warnings are judgement calls that the model should raise with you rather than quietly design around. Ask for the whole file back, with the same `meta.id`, and paste it in again. [How checks run](/docs/validation/how-checks-run) · [Repairing from a report](/docs/validation/repairing-from-a-report). --- --- title: "The document" description: "The top-level shape of a design.yaml — the schema line, meta, units, catalogue pins, and the order the ten sections appear in." url: https://kaicad.barpom.xyz/docs/schema/the-document schema: "0.1" source: packages/spec/src/design.ts --- # The document A design is one YAML file. It is **pure data** — there is no `kind: custom`, no inline code and nothing that executes, so a `design.yaml` is always safe to open and safe to share. ```yaml schema: furniture-cad/v0.1 meta: { ... } # who and what this is units: mm # metric only catalogs: { ... } # which catalogue versions to compile against parameters: [ ... ] # the numbers a person would change requirements: { ... } # design intent the validator can check materials: [ ... ] # the stock this piece is made from parts: [ ... ] # blanks, and what is cut into them joints: [ ... ] # what touches what, and with which hardware assembly: { ... } # which part is held still, and how it comes apart outputs: { ... } # what to draw and export ``` `materials` and `parts` are the only sections that are required. A document with no `joints` compiles — you get a pile of parts at the origin — which is occasionally what you want while you are working out blank sizes. ## The order does not matter, but write it this way anyway YAML mappings are unordered and the resolver does not care. References are resolved **lazily**: a parameter may reference a material, a material may reference a parameter, and a part may reference another part. There is no phase ordering to obey, and a genuine loop is reported as [`RES-003`](/docs/validation/error-codes#res-003) naming the actual cycle. Write the sections in the order above regardless. It is the order the pages here follow, the order the authoring pack emits, and it reads top-down: the numbers, then the stock, then the parts, then what holds them together. ## `schema:` ```yaml schema: furniture-cad/v0.1 ``` Required, and the first line. It is how a file written against an older version of this reference is recognised as such rather than mis-parsed. ## `meta:` ```yaml meta: id: walnut-platform-bed-queen title: Walnut slatted platform bed — Queen description: > A knock-down platform bed in solid walnut with a panelled headboard. Rails bolt to the posts so the whole thing comes apart to get up a staircase. author: claude-cad tags: [bed, knock-down, slatted, walnut] ``` | Key | | | | --- | --- | --- | | `id` | required | Stable identifier. **Keep it across repairs** — it is how the app recognises a re-emitted file as the same design rather than a new one. | | `title` | required | What a person calls the piece. | | `description` | | Prose. Reaches the drawing's title block. | | `author` | | Free text. | | `created` | | ISO date. | | `tags` | | Free-text list, for the design library. | ## `units:` ```yaml units: mm ``` Optional, and `mm` is the only accepted value. It exists so that a file written in inches is **rejected with a clear message** rather than compiled into a piece of furniture forty times too large. Every number in the document is millimetres, except angles, which are degrees. See [Units and precision](/docs/schema/units-and-precision). ## `catalogs:` ```yaml catalogs: materials: std/lumber@1 fixtures: std/fixtures@1 standards: std/standards@1 ``` Pins which catalogue the ids in this document resolve against. This is the closest thing the format has to an import: nothing is copied into your file, and the catalogue is not something you author — you name a version of it and then reference entries from `materials`, `joints` and `parameters`. All three keys are optional and default to the bundled `std/…@1`. Unless you have a reason, leave them at the defaults or omit the section. What each family contains, and how a reference resolves, is [How the catalogue works](/docs/catalogue/how-the-catalogue-works). ## The rest Each of the remaining sections has its own page: - [Parameters and expressions](/docs/schema/parameters-and-expressions) — the numbers, and the only computation in the format - [Requirements](/docs/schema/requirements) — intent the validator can check - [Materials](/docs/schema/materials) — narrowing a catalogue family to a real board - [Parts](/docs/schema/parts) — blanks, grain and instances - [Features](/docs/schema/features) — what is cut into a blank - [Anchors](/docs/schema/anchors) — the placement model, and the concept to read first - [Joints](/docs/schema/joints) — what touches what - [Arrays](/docs/schema/arrays) — slats, shelves, pickets - [Assembly and outputs](/docs/schema/assembly-and-outputs) — the ground part, groups, exports ## A complete, minimal document Two parts, one screw, nothing derived: ```yaml schema: furniture-cad/v0.1 meta: id: shelf-and-cleat title: One shelf on one cleat units: mm materials: - id: pine from: lumber.softwood.s4s species: pine thickness: 25 width: 195 parts: - id: shelf material: pine grain: length blank: { length: 800, width: 195, thickness: 25 } - id: cleat material: pine grain: length blank: { length: 800, width: 40, thickness: 25 } joints: - id: shelf_to_cleat kind: screwed fixture: fixtures.wood_screw.4x50 through: shelf mate: a: shelf:face.bottom b: cleat:face.top align: { contact: true } fasteners: pattern: { along: length, count: 4, spacing: auto, margin: 60 } assembly: ground: cleat ``` That compiles, and the validator has something to say about it — which is the point. --- --- title: "Parameters and expressions" description: "The five parameter types, and the restricted expression language — what it can compute, what names it can see, and the one hyphen rule that trips models up." url: https://kaicad.barpom.xyz/docs/schema/parameters-and-expressions schema: "0.1" source: packages/resolver/src/expression.ts --- # Parameters and expressions `parameters` are the numbers a person would want to change: the mattress size, the timber, the deck height. Everything else in the document should be **derived** from them, so that changing one moves the whole piece rather than leaving half of it behind. ```yaml parameters: - id: deck_height label: Height of the slat top above the floor type: length value: 300 min: 180 max: 500 - id: deck_width label: Clear width between the rails type: length derived: mattress.width + 2 * mattress_clearance ``` Every parameter has an `id` and a `type`, and optionally a `label` and a `description`. The label is what a person sees; write it as a sentence, not a variable name. ## `value` or `derived`, never both `value` is entered. `derived` is computed, and is always an expression string. A parameter with neither is [`SCH-002`](/docs/validation/error-codes#sch-002). The distinction is the difference between a piece that can be resized and one that cannot. If you find yourself typing the same arithmetic into two `blank:` entries, that number wanted to be a derived parameter. ## The five types ### `length` Millimetres. `min` and `max` bound it, and are themselves expressions. ```yaml - id: headboard_height type: length value: 900 min: 600 max: 1400 ``` ### `number`, `count`, `angle` The same shape as `length` — `value` or `derived`, with optional `min` and `max`. `count` is a whole number of things; `angle` is degrees. ### `enum` A closed list, and a chosen value. Referencing it yields the **string**, so it is normally fed into a material's `species` rather than into arithmetic. ```yaml - id: species type: enum values: [walnut, white_oak, ash, maple, cherry] value: walnut ``` ### `boolean` ```yaml - id: add_centre_rail type: boolean value: true ``` ### `catalog_ref` Names a catalogue family and one entry in it. This is the useful one: the parameter then **behaves like the entry**, so its fields read as sub-references. ```yaml - id: mattress type: catalog_ref catalog: standards.mattress value: us-queen ``` `mattress.width` is then 1524 and `mattress.length` is 2032, and changing `value` to `uk-double` moves every part derived from them. A value that is not in the family is [`RES-005`](/docs/validation/error-codes#res-005), and the report lists the ones that are. ## The expression language Expressions are the only computation in the format, and the evaluator is a hand-written recursive-descent parser. It never calls `eval`, never builds a function and never reaches a global. The only names it can see are the ones listed below. **Operators**, in the usual precedence: `^` · `* / %` · `+ -` · `< <= > >=` · `== !=` · `&&` · `||` · `? :`, with unary `-` and `!`, and parentheses. **Functions**: `min`, `max` (both variadic), `round`, `ceil`, `floor`, `abs`, `sqrt`, `clamp(value, low, high)`. ```yaml count: ceil((slat_cleat.blank.length - slat.blank.width) / (slat.blank.width + max_slat_gap)) + 1 ``` There are no user-defined functions, no loops and no conditionals beyond the ternary. If a computation does not fit, the answer is another derived parameter, not a longer expression. ## What an expression can see | Reference | Yields | | --- | --- | | `deck_height` | A parameter's value | | `mattress.width` | A field of a `catalog_ref` parameter's entry | | `rail_stock.thickness`, `.width` | A resolved material's actual milled dimension | | `rail_stock.density`, `.modulus_of_elasticity` | That material's species properties | | `side_rail.blank.length`, `.width`, `.thickness` | Another part's resolved blank | | `standards.bed.slat_gap_max` | A catalogue standard | That table is the whole reach of the language. A bare part or material name is not a value — `side_rail` on its own resolves to nothing. Local names win over catalogue names, and nothing local may be called `standards`. Resolution is lazy and memoised, so these may depend on each other in any order. A real loop is [`RES-003`](/docs/validation/error-codes#res-003), and the finding names the cycle. ## Where expressions are allowed Anywhere the reference says `Expr`: blank dimensions, feature sizes and positions, offsets, pattern counts and spacings, `min`/`max` bounds, and array counts. **In a number-typed field, a string is always an expression** — no marker needed: ```yaml blank: length: deck_length # expression width: rail_stock.width # expression thickness: 38 # number ``` **In a text-typed field, an expression needs a leading `=`**, because the literal text is usually what you meant: ```yaml species: walnut # the literal string "walnut" species: "=species" # the value of the `species` parameter ``` ## The hyphen rule Catalogue keys contain hyphens — `standards.mattress.us-queen.width` — and so does subtraction. The rule the tokeniser applies: > A hyphen continues an identifier **only** when it sits directly between an identifier > character and a *letter or underscore*, with no whitespace. | Written | Read as | | --- | --- | | `us-queen` | one name | | `deck_height - rail_stock.width` | subtraction | | `width-10` | subtraction — a digit follows the hyphen | | `a-b` | **one name** | So if you mean subtraction between two single-word names, put spaces around the minus. Getting it wrong is a clear [`RES-002`](/docs/validation/error-codes#res-002) — an unknown name — not a wrong number quietly compiled. ## Worked: sizing a bed from its mattress ```yaml parameters: - { id: mattress, type: catalog_ref, catalog: standards.mattress, value: us-queen } - { id: mattress_clearance, type: length, value: 6, min: 3, max: 15 } - { id: deck_height, type: length, value: 300 } - id: deck_width type: length derived: mattress.width + 2 * mattress_clearance - id: max_slat_gap type: length derived: standards.bed.slat_gap_max - id: post_centre_offset type: length derived: deck_width / 2 + rail_stock.thickness / 2 ``` Nothing in that block is a measurement of this particular bed. Change `us-queen` to `uk-double` and the rails, the cleats, the slat count and the gap between slats all follow. --- --- title: "Materials" description: "Naming the stock a design is made from — pointing at a catalogue family and narrowing it to a board that is actually milled." url: https://kaicad.barpom.xyz/docs/schema/materials schema: "0.1" source: packages/spec/src/design.ts --- # Materials A `materials` entry is a **named stock**, not a species and not a board. `from` points at a catalogue family and the remaining keys narrow it to one real, purchasable size. ```yaml materials: - id: rail_stock from: lumber.hardwood.s4s species: "=species" thickness: 38 width: 190 ``` Parts then say `material: rail_stock`, and expressions can read `rail_stock.thickness`, `rail_stock.width`, `rail_stock.density` and `rail_stock.modulus_of_elasticity`. | Key | | | | --- | --- | --- | | `id` | required | The name parts and expressions use. | | `from` | required | A catalogue family id — see [the catalogue](/docs/catalogue/how-the-catalogue-works). | | `species` | | Required for families that are milled in more than one species. | | `thickness` | | Millimetres, **actual**, never nominal. | | `width` | | Millimetres, actual. Omit for sheet goods. | | `label` | | Overrides the id on the cut list. | ## Declare a stock per role, not per species The mistake to avoid is one `walnut` material used for everything. Posts, rails and cleats are different boards; giving each its own entry is what makes the cut list group correctly, lets the rails change thickness without moving the posts, and gives expressions something specific to reference. ```yaml materials: - { id: post_stock, from: lumber.hardwood.s4s, species: "=species", thickness: 75, width: 75 } - { id: rail_stock, from: lumber.hardwood.s4s, species: "=species", thickness: 38, width: 190 } - { id: cleat_stock, from: lumber.softwood.s4s, species: pine, thickness: 25, width: 40 } - { id: panel_stock, from: sheet.plywood.birch, thickness: 18 } ``` Note `species: "=species"` — a **text** field, so an expression needs the leading `=`. Three of those stocks follow the design's species parameter; the cleats are pine because nobody sees them. ## The size must be one that is milled The catalogue lists the thicknesses and widths a family is actually available in. A combination that is not on the list is [`RES-009`](/docs/validation/error-codes#res-009), and the finding names the nearest sizes that are. ``` RES-009 lumber.hardwood.s4s is not milled 35 mm thick. Nearest available: 32 mm, 38 mm. ``` This is a design rule masquerading as a lookup failure. A 35 mm rail is a rail you cannot buy, and finding that out at the timber yard is worse than finding it out here. ## What a resolved material carries Narrowing a family gives the rest of the pipeline more than two numbers. The species properties come with it, and the validator reads them: - **density** — for the weight on the BOM and the load checks - **modulus of elasticity** — for shelf and rail deflection - **movement, tangential and radial** — for the wood-movement checks on panels and tops - **screw withdrawal, face and end** — for the fastener checks - **rough allowance** — extra on each dimension for the cut list's rough sizes A pine cleat and a walnut rail take the same screw and hold it differently, and the report says so because the material knows which it is. ## Sheet goods Sheet materials have a thickness and a sheet size, not a width: ```yaml - id: panel_stock from: sheet.plywood.birch thickness: 18 ``` They also carry their own properties rather than inheriting from a species, because a panel behaves the way it does because of how it is built, not what tree it came from. Plywood barely moves in plane; MDF holds a screw poorly in the edge and very poorly in the end. `sheet.veneered.mdf` is the exception that takes a `species`, because the veneer is directional and the sheet has a show face. ## Sheet goods still need a grain direction Plywood is stiff along the face grain and noticeably less so across it, and veneered panels have a visible direction. Set `grain:` on the [part](/docs/schema/parts), not the material — two parts cut from the same sheet can run different ways, and the nesting is worked out from the parts. ## Rough sawn versus surfaced `lumber.hardwood.rough` states the size you **start** with; `lumber.hardwood.s4s` states the size you **finish** at. The rough family carries larger allowances — 6 mm on thickness against 3 mm — so the cut list asks for enough board to flatten. If you are unsure, use the surfaced family. It is what a merchant hands over. ## The families available | Family | Class | | | --- | --- | --- | | [`lumber.hardwood.s4s`](/docs/catalogue/solid-lumber) | solid | Nine species, 19–75 mm, 50–290 mm wide | | [`lumber.softwood.s4s`](/docs/catalogue/solid-lumber) | solid | Five species, 18–75 mm, 40–245 mm wide | | [`lumber.hardwood.rough`](/docs/catalogue/solid-lumber) | solid | Rough sawn, larger allowances | | [`sheet.plywood.birch`](/docs/catalogue/sheet-goods) | sheet | 4–30 mm, three sheet sizes | | [`sheet.plywood.poplar`](/docs/catalogue/sheet-goods) | sheet | 6–25 mm, cheaper and softer | | [`sheet.mdf`](/docs/catalogue/sheet-goods) | engineered | 6–30 mm | | [`sheet.veneered.mdf`](/docs/catalogue/sheet-goods) | engineered | 18–25 mm, five veneers | Full specification tables, including every milled size and the per-species properties, are in the [catalogue section](/docs/catalogue/how-the-catalogue-works). --- --- title: "Parts" description: "Blanks, grain direction and named instances — the board a part is cut from, before anything is cut into it." url: https://kaicad.barpom.xyz/docs/schema/parts schema: "0.1" source: packages/spec/src/design.ts --- # Parts A part is a **blank** — a rectangular board — plus the features cut into it. Nothing about where it sits in the piece appears here; that is what [joints](/docs/schema/joints) are for. ```yaml parts: - id: side_rail label: Side rail instances: [left, right] material: rail_stock grain: length blank: length: deck_length width: rail_stock.width thickness: rail_stock.thickness features: - kind: roundover edges: [top_left, top_right] radius: 3 ``` | Key | | | | --- | --- | --- | | `id` | required | Referenced by joints, expressions and groups. | | `material` | required | A `materials` id. | | `blank` | required | `length`, `width`, `thickness` — all expressions. | | `label` | | What the cut list and the drawing call it. | | `instances` | | Named copies — see below. | | `grain` | | `length` (default), `width` or `thickness`. | | `features` | | What is cut into it — see [Features](/docs/schema/features). | | `note` | | Free text, reaches the cut list and the drawing. | ## The blank is the board, not the finished part Give the size **before** the joinery. A tenon does not lengthen the blank and a dado does not shorten it; the resolver subtracts the features itself. Blank dimensions are what the cut list asks the timber yard for, with the material's rough allowance added on top. Size them from parameters rather than typing measurements: ```yaml blank: length: deck_length # a derived parameter width: rail_stock.width # the full width of the stock thickness: rail_stock.thickness ``` ```yaml blank: length: side_rail.blank.length - 150 # another part's blank ``` The second form is how parts stay in step. The cleat is always 150 mm shorter than the rail, whatever the rail becomes. ## `length` is the long axis, and it decides everything else The three names are not interchangeable labels — they are the part's own coordinate axes, and every face, edge and feature is named against them: ``` +X = length start ←→ end +Y = width left ←→ right +Z = thickness bottom ←→ top ``` A board 800 × 195 × 25 is `length: 800, width: 195, thickness: 25`. Writing it as `length: 25` is legal and will produce a part whose `top` face is 800 × 195 — which is not what any of your joints expect. The full coordinate model is [Anchors](/docs/schema/anchors). ## `grain` Which blank axis the long grain runs along. Defaults to `length`. ```yaml grain: length # the usual case grain: width # a panel whose grain runs across its length ``` It drives the wood-movement checks, the end-grain fastener checks and the nesting on the cut list. A panel captured in a groove on all four sides will move across its grain and split the frame if it was not allowed room, and the validator can only tell you which way it will move if you have said which way the grain runs. ```yaml - id: headboard_panel material: panel_stock grain: width blank: { length: 1600, width: 560, thickness: 18 } ``` That panel is wider than it is tall in the piece, and its grain runs vertically — the `width` axis. Say so. ## `instances` Named copies of one part, cut from one cut-list row. ```yaml - id: head_post instances: [left, right] ``` You then have `head_post@left` and `head_post@right`, and anchors address them with the `@`: ```yaml mate: a: side_rail@left:face.start b: head_post@left:face.top ``` **`left` and `right` are special.** A part with exactly those two instances is **mirrored** about the design's YZ plane. Any other set of instance names is copied unmirrored, and each copy is positioned by its own joints. ### Mirrored instances are the sharpest edge in the model Mirroring flips the part, and it takes the part's faces and features with it. The consequences are worth reading twice: - **The inner face of the left post is its `right` face; the inner face of the right post is its `left` face.** Any joint that talks about "the inside" has to name a different face for each side, which is why rail-to-post joints are usually written out twice rather than with `for_each`. - **A feature arrives upside down on the mirrored copy.** A groove on the `right` face of the left post is on the `right` face of the right post too — rotated 180°. A panel mating into both will go in backwards on one side unless you correct it with `align.roll: 180`. Both of these are visible in the [worked example](/app/d/queen-platform-bed): the headboard panel seats into the left post's groove normally and into the right post's with `roll: 180`. If a part has three or more copies that are genuinely identical and evenly spaced — slats, shelves, pickets — do not use `instances`. Use an [array](/docs/schema/arrays). ## `note` ```yaml - id: centre_leg note: Trim to fit on site — floors are rarely flat. ``` Free text that travels to the cut list and the drawing. Use it for the thing a person needs to know that no number captures. --- --- title: "Anchors" description: "The placement model — part-local coordinates, the six faces, the twelve edges, face (u, v) positions, anchor path syntax, and why an anchor carries a secondary axis." url: https://kaicad.barpom.xyz/docs/schema/anchors schema: "0.1" source: packages/spec/src/anchors.ts --- # Anchors **Parts are never placed by absolute transform.** Every part exposes named anchors, and a joint mates anchor A to anchor B. There is no escape hatch, and no `at: [x, y, z]` on a part. Read this page before writing a joint. It is the concept that a model composing YAML gets wrong most often, and the failure is the quiet kind: the file validates, the geometry builds, and the piece of furniture is simply incorrect. ## Why there is no coordinate field Absolute coordinates encode an *answer* — where the rail ended up — instead of the *reason*: the rail's end sits against the post's inner face. When the mattress size changes, the reason still holds and the answer does not. A design written in coordinates has to be recomputed everywhere; a design written in anchors recomputes itself. It also removes an entire failure class. A wrong anchor path is caught in milliseconds with a JSON Pointer at the line that wrote it. A plausible-looking wrong coordinate is caught by someone in a workshop. ## Part-local coordinates Every blank is a box centred on its own origin: ``` +Z thickness length +X ±length/2 │ ┌───────────┐ width +Y ±width/2 │ ╱ ╱│ thickness +Z ±thickness/2 │ ┌───────────┐ │ │ │ │ │ The origin is the centre of the ───────┼─│─────── ───│─┼── +X box, not a corner. ╱│ │ │ ╱ length ╱ │ └───────────┘ +Y ╱ width ``` ## The six faces | Face | Direction | On a board | | --- | --- | --- | | `start` | −X | One end, along the length | | `end` | +X | The other end | | `left` | −Y | One long edge | | `right` | +Y | The other long edge | | `bottom` | −Z | One broad face | | `top` | +Z | The other broad face | `start` and `end` are the **ends** of a board — the end-grain faces on a solid part. `top` and `bottom` are the broad faces. These names are about the part's own axes, not about how it ends up sitting in the room: a post standing vertically still has its `start` at the bottom, because that is the low end of its `length`. ## The twelve edges An edge is named for the two faces that meet at it, and the order does not matter — `top_left` and `left_top` are the same edge, and both resolve to the canonical `top_left`. ``` top_left top_right top_start top_end bottom_left bottom_right bottom_start bottom_end left_start left_end right_start right_end ``` Opposite faces never meet, so there is no `top_bottom`. **Wildcards** work in edge selectors, which is how chamfers and roundovers are usually written: | Selector | Selects | | --- | --- | | `top_*` | The four edges of the top face | | `*_start` | The four edges around the `start` end | | `end_*` | The four edges around the `end` end | | `*` | All twelve | ```yaml features: - kind: chamfer edges: [end_*] size: 3 ``` ## Anchor paths ``` [@]:.[.] ``` ```yaml side_rail:face.start # a part with no instances side_rail@left:face.start # one named instance head_post@left:feature.panel_slot # a named feature on an instance slat_cleat@left:edge.top_start centre_rail:point.center centre_rail:center # shorthand — a bare name means point.center ``` | Kind | Names | What the frame is | | --- | --- | --- | | `face` | `top` `bottom` `left` `right` `start` `end` | The centre of that face, normal pointing out | | `edge` | `top_left`, … | The midpoint of the edge, normal bisecting the two faces | | `feature` | a feature's `id` | The feature's own frame — see below | | `point` | `center` (or `centre`) | The part's origin | | `axis` | `length` `width` `thickness` | The origin, normal along that axis | A malformed or unknown path is [`RES-007`](/docs/validation/error-codes#res-007), and the finding lists what the part does have. ### Feature anchors are the ones that matter A named feature exposes an anchor at its own frame, sitting in the recess with its normal pointing out of the part. Mating to it seats the neighbour **in** the recess rather than on the surface: ```yaml parts: - id: head_post features: - kind: groove id: panel_slot # ← the id is what makes it mateable on: right along: length width: 18 depth: 10 center: true stopped: 40 joints: - id: panel_to_head_post_left kind: dado mate: a: head_post@left:feature.panel_slot b: headboard_panel:face.start ``` The point of that indirection: the groove and the panel that lives in it **cannot drift apart**. Move the groove and the panel follows. Positioning the panel with an offset and cutting the groove independently is exactly the quiet error this model exists to prevent. Grooves, dados, rabbets, mortises, pockets and holes can be mated to. A chamfer cannot — it has no frame to sit in. ## Why an anchor carries a secondary axis A point and a normal leave the **roll** about that normal free. One unconstrained degree of freedom is precisely what an author would otherwise have to pin down with a coordinate, which would put the escape hatch back. So every anchor carries a secondary axis as well, and a single mate is a complete placement. The convention is mechanical: > The secondary axis is **+Z**, except on the faces whose normal is already ±Z — `top` and > `bottom` — where it is **+X**. When a part arrives rotated the wrong way about the mate normal, that is the secondary axis disagreeing with you, and [`align.roll`](/docs/schema/joints#align) overrides it. `roll` in 90° steps is normal and expected — it is not a sign you have done something wrong. ## Face coordinates (u, v) Holes and pockets are positioned in a face's own 2D frame, measured **from the face's minimum corner**, so both numbers are positive and read the way a person would measure them with a tape. | Face | `u` runs along | `v` runs along | | --- | --- | --- | | `top`, `bottom` | length | width | | `left`, `right` | length | thickness | | `start`, `end` | width | thickness | ```yaml - kind: hole on: top diameter: 8 depth: 12 at: { u: 100, v: center } ``` `u` and `v` always run along the **positive** part axis. That means the triad (u, v, normal) is right-handed on some faces and left-handed on others. It is a deliberate trade: predictable authored numbers matter more here than a consistent handedness nobody writing a design would notice. ## Mirrored instances A part with instances named exactly `left` and `right` is mirrored about the design's YZ plane. Mirroring carries the faces and features with it, and there are two consequences to hold onto. **The inner face swaps sides.** The inner face of the left post is its `right` face; the inner face of the right post is its `left` face. No `{i}` substitution can express that, which is why rail-to-post joints are normally written out twice rather than with `for_each`: ```yaml - id: rail_to_head_post_left mate: { a: side_rail@left:face.start, b: head_post@left:face.top } - id: rail_to_head_post_right mate: { a: side_rail@right:face.start, b: head_post@right:face.bottom } ``` **Features arrive flipped.** The groove cut on the `right` face of the left post is on the `right` face of the right post too — but upside down. A panel mating into both goes in backwards on one side until you turn it over: ```yaml - id: panel_to_head_post_right mate: a: head_post@right:feature.panel_slot b: headboard_panel:face.end align: roll: 180 # ← the mirror, undone ``` If a mirrored part comes out inverted, `roll: 180` is almost always the fix. ## Checking your work `fcad inspect ` prints where every part ended up — world bounds and transform, per instance. It is the fastest way to find out whether an anchor said what you meant, and it needs no CAD kernel: ``` fcad inspect fixtures/bed-queen/design.yaml ``` In the app, the workbench's **isolate** and **dimensions** controls do the same job visually. --- --- title: "Features" description: "The ten things that can be cut into a blank — dados, grooves, rabbets, mortises, holes, pockets, notches, chamfers, roundovers and tapers — and how each one is positioned." url: https://kaicad.barpom.xyz/docs/schema/features schema: "0.1" source: packages/spec/src/design.ts --- # Features Features are cut into a part's blank. They do not change the blank's size: a dado does not shorten a board and a tenon does not lengthen one. The blank is what you buy; the features are what you do to it. ```yaml parts: - id: head_post blank: { length: 900, width: 75, thickness: 75 } features: - kind: groove id: panel_slot on: right along: length width: 18 depth: 10 center: true stopped: 40 - kind: chamfer edges: [end_*] size: 3 ``` Every feature takes an optional `id`. **Give it one if a joint needs to mate to it** — that is what turns the feature into an [anchor](/docs/schema/anchors#feature-anchors-are-the-ones-that-matter), and it is how a panel and the groove it sits in stay in step. Most features also take an `on:` face. Faces, edges and the (u, v) frames they are measured in are all on the [Anchors](/docs/schema/anchors) page. ## Positioning along an axis Dados, grooves, mortises and notches sit somewhere along an axis, and there are four ways to say where. **Give exactly one.** | Form | Means | | --- | --- | | `from: 120` | 120 mm from the `start` end to the feature's **near edge** | | `to: 300` | 300 mm from the `start` end to the feature's **far edge** | | `at: 210` | 210 mm from the `start` end to the feature's **centre** | | `center: true` | Centred on the axis | Giving two, or none, is a [`RES-`](/docs/validation/error-codes) finding rather than a guess. The resolver will not pick for you, because picking wrong puts a trench in the wrong place and nothing downstream would notice. `from` and `to` may be expressions, so a feature can follow a parameter: ```yaml - kind: dado on: right from: deck_height - shelf_stock.thickness width: 18 depth: 9 ``` ## The trenches ### `dado` A square-bottomed trench **across** the grain, running the full width of the face unless stopped. ```yaml - kind: dado id: shelf_seat on: right along: width # optional — defaults to the short axis of `on` at: 400 width: 18 # the width of the trench, i.e. the thickness of what sits in it depth: 9 stopped: 12 # optional: stop 12 mm short of the far edge ``` `width` is the trench's width, not the part's. Size it to the **thickness** of the part that seats in it — or better, reference that part directly: ```yaml width: shelf.blank.thickness ``` ### `groove` The same cut, **with** the grain. It is a separate kind because the validation differs: a groove along the grain and a dado across it fail in different ways, and a groove is what captures a panel that will move seasonally. Same keys as `dado`. `stopped:` leaves a blind end, which is what keeps a groove from showing on the front edge of a post. ### `rabbet` An L-shaped cut along one edge of a face. ```yaml - kind: rabbet id: back_seat on: right # one of the six faces edge: end # which edge of that face it runs along width: 12 depth: 9 from: 0 # optional — run only part of the edge to: 400 ``` Note `edge:` here names an edge of the face, not one of the twelve blank edges. ### `mortise` A blind or through rectangular slot. ```yaml - kind: mortise id: tenon_seat on: right along: length # the slot's long axis at: 300 length: 60 # along `along` width: 12 # across it depth: 30 # omit and set `through: true` for a through mortise ends: round # `round` (routed) or `square` (chiselled) ``` `ends` matters to whoever cuts it: a router leaves round ends and a chisel leaves square ones, and a tenon has to match whichever you chose. ### `notch` A rectangle removed from a corner or an edge — the classic leg-and-apron cut-out. ```yaml - kind: notch on: top edge: start from: 0 length: 40 depth: 20 ``` ## Holes and pockets ### `hole` ```yaml - kind: hole on: top diameter: 8 depth: 12 # or: through: true at: { u: 100, v: center } counterbore: { diameter: 14, depth: 7 } countersink: { diameter: 8.4, angle: 90 } ``` `at` is a position in the face's own (u, v) frame, measured from its minimum corner. Either value may be `center`. For more than one hole, use `pattern:` instead of `at:` — see below. > **Most holes should not be authored.** Every clearance hole, pilot hole, counterbore and > countersink that belongs to a fastener comes from the fixture's machining recipe when you > declare a [joint](/docs/schema/joints). Hand-placing a screw hole means the validator cannot > check its edge distance or thread engagement, because as far as it knows there is no screw > there. Author a hole for a cable exit, a bolt you are supplying yourself, or a shelf-pin > array — not for a screw. ### `pocket` A flat-bottomed rectangular recess that reaches no edge. ```yaml - kind: pocket on: bottom length: 120 width: 40 depth: 6 at: { u: center, v: center } ``` ## Patterns A repeated position. This is the shape behind every run of screw holes and every shelf-pin line. ```yaml pattern: along: length # required: which axis the run follows count: 10 spacing: auto # centre-to-centre, or `auto` to divide the span evenly margin: 80 # keep this clear at each end ``` | Key | | | --- | --- | | `along` | Required. `length`, `width` or `thickness`. | | `count` | How many. An expression, so it can be computed. | | `spacing` | Centre-to-centre, or `auto` to divide the available span evenly. | | `margin` | Clear space to leave at each end of the span. | | `from` / `to` | Bound the run explicitly instead of using the whole axis. | | `center` | Centre the run on the axis. | | `offset` | Position on the face's *other* axis. Defaults to centred. | | `at` | Shorthand instead of count and spacing: any of `start`, `end`, `centre`, `ends`. | ```yaml # Ten screws down a cleat, evenly spread, 80 mm clear at each end. pattern: { along: length, count: 10, spacing: auto, margin: 80 } # Two bolts, 100 mm apart, positioned at a computed height. pattern: along: length count: 2 spacing: bolt_pitch from: rail_centre_height - bolt_pitch / 2 to: rail_centre_height + bolt_pitch / 2 # Shelf pins on the 32 mm system. pattern: { along: length, count: 12, spacing: 32, from: 200 } ``` The same `pattern` shape is used by `joints[].fasteners.pattern`, which is where you will write it most of the time. ## Edge treatments ### `chamfer` and `roundover` Both take edge selectors, and wildcards work: ```yaml - kind: chamfer edges: [top_end, bottom_end, left_end, right_end] size: 3 - kind: chamfer edges: [end_*] # the same four, shorter size: 3 - kind: roundover edges: [top_left, top_right] radius: 3 ``` Edge names and the wildcard rules are on the [Anchors](/docs/schema/anchors#the-twelve-edges) page. A chamfer or roundover has no frame, so it cannot be mated to. A 2–3 mm break on every edge a hand will touch is worth authoring. It is also the cheapest thing in the file to change later. ### `taper` A linear taper of one dimension along another axis — table and chair legs. ```yaml - kind: taper along: length # the axis the taper runs down reduce: width # the dimension that shrinks to: 30 # its size at the far end of `along` from: 150 # where the taper begins, from the `start` end sides: both # `both` (default), `near` or `far` ``` `sides: far` tapers only the outward face, which is the usual choice for a leg that must stay square where it meets an apron. ## The ten kinds, at a glance | Kind | Positioned by | Mateable | | --- | --- | --- | | `dado` | `on` + along-axis form | yes | | `groove` | `on` + along-axis form | yes | | `rabbet` | `on` + `edge` + `from`/`to` | yes | | `mortise` | `on` + along-axis form | yes | | `hole` | `on` + `at` or `pattern` | yes | | `pocket` | `on` + `at` | yes | | `notch` | `on` + `edge` + along-axis form | no | | `chamfer` | `edges` | no | | `roundover` | `edges` | no | | `taper` | `along` + `reduce` | no | --- --- title: "Joints" description: "What touches what — the twelve joint kinds, the mate that positions two parts, align, the `through` part that decides which way hardware goes in, and fastener patterns." url: https://kaicad.barpom.xyz/docs/schema/joints schema: "0.1" source: packages/spec/src/design.ts --- # Joints A joint does three things from one declaration: 1. **It positions parts** — the mate places one anchor against another. 2. **It creates hardware** — the fixture becomes real items on the BOM. 3. **It cuts machining into both parts** — every hole, counterbore and countersink in a compiled design comes from a fixture's recipe. Hardware can never be decorative. If there is a screw in the model, there is a pilot hole in the receiving part and a clearance hole in the through part, and the validator has checked its engagement and its edge distance. ```yaml joints: - id: cleat_to_rail_left kind: screwed fixture: fixtures.wood_screw.4x50 mate: a: slat_cleat@left:face.top b: side_rail@left:face.bottom align: contact: true offset: [0, -cleat_drop, 0] fasteners: pattern: { along: length, count: 10, spacing: auto, margin: 80 } ``` | Key | | | | --- | --- | --- | | `id` | required | Referenced by `outputs.drawings.details` and by findings. | | `kind` | required | One of the twelve below. | | `mate` | required | The two anchors, and how they align. | | `fixture` | | A catalogue fixture id. Required for hardware kinds. | | `through` | | Which part the fastener passes through. | | `fasteners` | | `{ pattern: … }` — where the fasteners go. | | `for_each` | | Expand once per named instance. | | `array` | | Expand into a distributed run — see [Arrays](/docs/schema/arrays). | | `glue` | | Whether the joint is glued. | | `removable` | | Whether it comes apart. | | `fit` | | `{ clearance: … }` — overrides the fixture's default. | ## The twelve kinds | Kind | Fixture | | | --- | --- | --- | | `butt` | optional | Two parts simply touching | | `glue_only` | no | Held by glue alone | | `screwed` | required | A wood screw through one part into another | | `doweled` | required | Fluted dowels, glued | | `mortise_tenon` | no | Traditional joinery; the mortise is a feature | | `dado` | no | One part seated in the other's trench | | `rabbet` | no | One part seated in the other's L-cut | | `bed_bolt` | required | Bolt into a cross dowel — knock-down | | `cam_lock` | required | Flat-pack cam and dowel bolt | | `threaded_insert` | required | Machine screw into an insert — knock-down | | `pocket_screw` | required | Angled screw from a pocket | | `bracket` | required | Metal bracket | `kind` is what the assembly steps and the drawing call the joint. It does not change the geometry — the **fixture** does that. ## `mate` ```yaml mate: a: side_rail@left:face.start b: head_post@left:face.top align: contact: true roll: -90 offset: [rail_drop_on_head_post, 0, 0] ``` `a` **moves**; `b` **is the target**. Both are [anchor paths](/docs/schema/anchors#anchor-paths). The part on the `a` side is placed so that its anchor meets the anchor on the `b` side. Placement propagates outwards from the [ground part](/docs/schema/assembly-and-outputs). The chain matters: ground the headboard panel, hang the posts off its ends, the rails off the posts and the foot posts off the rails, and the whole piece stays symmetric about its centre line for free. ### `align` | Key | | | --- | --- | | `contact: true` | Make the two faces coincident and opposed. **The normal case.** | | `offset: [x, y, z]` | A nudge, in the **target** anchor's local frame. | | `roll: ` | Rotation about the mate normal, when the default is wrong. | | `flip: true` | Flip the moving part end-for-end about the mate normal. | **`offset` is local, not a position.** It is expressed in the frame of the anchor named in `b`, which is what keeps it small and meaningful — "35 mm down the post's inner face" rather than "at z = 265". If an offset in your file is a large number that looks like a coordinate, the mate is probably attached to the wrong anchor. ```yaml # Slide the cleat down the rail's inner face. align: contact: true offset: [0, -cleat_drop, 0] ``` **`roll` in 90° steps is normal.** Each anchor carries a secondary axis so that one mate is a complete placement, and the convention that sets it is mechanical rather than clever — see [why an anchor carries a secondary axis](/docs/schema/anchors#why-an-anchor-carries-a-secondary-axis). When a part arrives rotated about the mate normal, `roll` is the correction, and `roll: 180` is the usual fix for a mirrored instance arriving upside down. ## `through` — which way the hardware goes in ```yaml - id: rail_to_head_post_left kind: bed_bolt fixture: fixtures.bed_bolt.m8x150 through: head_post@left mate: a: side_rail@left:face.start b: head_post@left:face.top ``` `through` names the part the fastener **passes through**. It defaults to the `a` side of the mate, and it is stated explicitly whenever that is wrong. These are two different questions. The mate says where the parts sit. `through` says which way the hardware is driven in, and there is no reason the two should agree: a bed bolt goes through the post from the outside into the end of the rail, whichever order the mate happened to be written in. Tying them together would mean rewriting a working mate in order to fix a hole. The fixture then supplies the rest. Each entry declares a `through_from` — `mating` for a screw that enters on the face touching the other part, `opposite` for a bed bolt whose counterbore is on the outside face. Get `through` wrong and you put an 18 mm counterbore in the wrong surface and run the clearance hole down the length of the board instead of across it, which is exactly the kind of unbuildable design that still compiles and exports cleanly. ## `fasteners` Where the fasteners go, as a [pattern](/docs/schema/features#patterns): ```yaml fasteners: pattern: { along: length, count: 10, spacing: auto, margin: 80 } ``` ```yaml fasteners: pattern: along: length count: 2 spacing: bolt_pitch from: rail_centre_height - bolt_pitch / 2 to: rail_centre_height + bolt_pitch / 2 ``` Each position in the pattern becomes one fixture on the BOM and one full set of machining operations on both parts. The fixture's recipe decides what those are; you never write a pilot hole yourself. ## `glue`, `removable` and `fit` ```yaml glue: false removable: true fit: { clearance: 0.3 } ``` `removable: true` is how a joint declares itself knock-down, and it is what `requirements.knock_down` is checked against. `fit.clearance` overrides the fixture's catalogue default for this joint only. ## `for_each`, and when not to use it ```yaml - id: shelf_pins kind: butt for_each: [a, b, c] mate: a: shelf@{i}:face.bottom b: side@left:face.right ``` `{i}` is substituted with each name in turn. It is a genuine saving when the instances are interchangeable. It is **not** usable for mirrored `left`/`right` pairs. The inner face of the left post is its `right` face and the inner face of the right post is its `left` face, and no substitution can express that difference. Write those two joints out: ```yaml - id: rail_to_head_post_left mate: { a: side_rail@left:face.start, b: head_post@left:face.top } - id: rail_to_head_post_right mate: { a: side_rail@right:face.start, b: head_post@right:face.bottom } ``` The duplication is honest. A `for_each` that quietly places one rail on the wrong face is not. ## `array` A joint may expand into a distributed run of one part instead — slats, shelves, pickets. That has its own page: [Arrays](/docs/schema/arrays). ## Worked: a bed bolt ```yaml - id: rail_to_head_post_left kind: bed_bolt fixture: fixtures.bed_bolt.m8x150 removable: true through: head_post@left mate: a: side_rail@left:face.start b: head_post@left:face.top align: contact: true roll: -90 offset: [rail_drop_on_head_post, 0, 0] fasteners: pattern: along: length count: 2 spacing: bolt_pitch from: rail_centre_height - bolt_pitch / 2 to: rail_centre_height + bolt_pitch / 2 ``` That one declaration produces, per bolt: a 9 mm clearance hole and an 18 mm × 8 mm counterbore through the post from its **outside** face, a 9 mm bore 100 mm into the end of the rail, and a 12.5 mm cross bore through the rail's width at 80 mm depth for the dowel to capture the bolt. Ten operations across two parts from two lines of pattern, and every one of them checked. The cross-dowel bore is the one that ruins knock-down beds when it lands in the wrong place, and it is not a number anyone should be typing. --- --- title: "Arrays" description: "One declaration for a run of identical parts — slats, shelves and pickets — with a count that can be computed from a standard." url: https://kaicad.barpom.xyz/docs/schema/arrays schema: "0.1" source: packages/spec/src/design.ts --- # Arrays An array expands a joint into a distributed run of one part. It is how you get eighteen slats from one declaration, with the count computed rather than counted. ```yaml - id: slats_to_cleats kind: butt removable: true array: of: slat count: ceil((slat_cleat.blank.length - slat.blank.width) / (slat.blank.width + max_slat_gap)) + 1 distribute: even span: [slat_cleat@left:face.start, slat_cleat@left:face.end] mate: a: slat:face.bottom b: slat_cleat@left:face.left align: contact: true roll: 180 offset: [-slat_centre_offset, 0, 0] ``` | Key | | | | --- | --- | --- | | `of` | required | The part id to repeat. | | `count` | required | How many. An expression. | | `span` | | Two anchor paths bounding the run. | | `distribute` | | `even` or `even_gaps`. | | `spacing` | | A fixed centre-to-centre distance, instead of a span. | ## `span` plus `distribute`, or `spacing` **`span` + `distribute`** fills a run between two anchors. Use it when the ends are fixed and the spacing should fall out — slats between the ends of a cleat, shelves between the top and bottom of a carcass. - `even` spaces the parts' **centres** evenly across the span. - `even_gaps` makes the clear **gaps between** them equal, which is what you want when the parts have width and the gap is the thing being checked. **`spacing`** sets a fixed centre-to-centre distance and lets the run end where it ends. Use it when the pitch is what matters — shelf pins on the 32 mm system, balusters at a required maximum gap. ## Compute the count; do not count it ```yaml count: ceil((slat_cleat.blank.length - slat.blank.width) / (slat.blank.width + max_slat_gap)) + 1 ``` That expression reads: divide the run by one slat plus one maximum gap, round up, add the slat at the far end. Because `max_slat_gap` is derived from `standards.bed.slat_gap_max`, and the cleat length is derived from the mattress, changing `us-queen` to `uk-super-king` adds slats by itself and the gap stays legal. Typing `count: 18` is the version that is wrong the moment anything else moves. It is also the version that quietly stops satisfying the standard it was sized against, with nothing to say so. ## The mate is written once, for one part Inside an array, the mate is written as though there were a single part. The expansion applies it to each copy in turn and slides them along the span. ```yaml mate: a: slat:face.bottom b: slat_cleat@left:face.left align: contact: true roll: 180 offset: [-slat_centre_offset, 0, 0] ``` That offset slides each slat from the left cleat across to the bed's centre line, so it lands on **both** cleats rather than hanging off one. It is derived, like everything else, so it follows the mattress size. ## Arrays or instances? | Use | When | | --- | --- | | [`instances`](/docs/schema/parts#instances) | Two or three copies that sit in different, named places — a left post and a right post. | | `array` | Many interchangeable copies in a regular run — slats, shelves, pickets. | Instances are placed by their own joints, one per copy. An array is placed by one joint, and its count can be an expression. If you are about to write `instances: [a, b, c, d, e, f]`, write an array instead. ## What the validator does with it Two things worth knowing. **Findings are collapsed.** If every slat in a run has the same problem, the report says so once with the count and the full subject list, rather than eighteen times. Twelve copies of one problem crowd out the other eleven problems in a context window. **The gap is checked against the standard, not against your arithmetic.** `DOM-` findings read `standards.bed.slat_gap_max` directly. If the computed count leaves a gap of 78 mm, the report gives you the count that would not — with the number in it, ready to paste back. --- --- title: "Requirements" description: "Design intent the machine can check — knock-down, the longest part that fits up your stairs, load, deflection, tools, and the standards a design promises to satisfy." url: https://kaicad.barpom.xyz/docs/schema/requirements schema: "0.1" source: packages/spec/src/design.ts --- # Requirements `requirements` is where the design says what it is *for*. It is the direct output of the authoring pack's interview and the direct input to the validator, and it is what makes "why did you do it that way?" answerable months later. ```yaml requirements: knock_down: true max_part_length: 2100 max_assembled_load: 300 deflection_limit: span/360 tools: [table_saw, drill, router, circular_saw] finish: oil budget_hint: mid must_satisfy: - standards.bed.slat_gap_max - standards.bed.centre_rail_required notes: - The rails must come off without disturbing the headboard panel. ``` Every key is optional. Each one that is present is something the report can check and, when it fails, name. | Key | | | | --- | --- | --- | | `knock_down` | boolean | The piece must come apart without breaking a glue joint. | | `max_part_length` | mm | The longest part that can be carried in. | | `max_assembled_load` | kg | Distributed load the piece must carry. | | `deflection_limit` | | `span/360` style, or an absolute mm figure. | | `max_envelope` | | `{ length, width, height }` the finished piece must fit inside. | | `tools` | list | What the builder actually has. | | `finish` | | Free text — `oil`, `lacquer`, `none`. | | `budget_hint` | | `low`, `mid` or `high`. | | `must_satisfy` | list | Catalogue standard ids this design promises to meet. | | `notes` | list | Prose constraints, for the humans. | ## `knock_down` ```yaml knock_down: true ``` Checked against the joints: every joint on the disassembly path has to be `removable: true`, with hardware that can be undone. A glued dowel joint in a piece that claims to be knock-down is a finding, not an oversight to be noticed later. This is normally the constraint that picks the hardware. A bed that has to come apart wants bed bolts and threaded inserts; one that does not can be glued and will be stiffer for it. ## `max_part_length` ```yaml max_part_length: 2100 ``` "Has to get up my stairs." Compared against every part's blank length — the size the timber yard cuts, not the finished dimension — because that is the board that has to come through the door. It is worth stating even when you think nothing is close to it. A 2032 mm queen rail and a 2100 mm stairwell is a fact you want checked, not remembered. ## `max_assembled_load` and `deflection_limit` ```yaml max_assembled_load: 300 # kg, distributed deflection_limit: span/360 # or an absolute mm figure ``` These drive the deflection checks. The span divisor is the convention: `span/360` is structural practice and stricter than furniture needs; `span/200` is where sag becomes visible and is the usual figure for a shelf. The check reads the **resolved material's** modulus of elasticity, so a pine shelf and a maple one of the same size get different answers, and the finding carries a computed suggestion — a thickness, or a span, that would pass. ## `max_envelope` ```yaml max_envelope: length: 2200 width: 1700 height: 1000 ``` The box the finished piece must fit inside. Different from `max_part_length`, which is about getting the parts in; this is about the room it ends up in. ## `tools` ```yaml tools: [table_saw, drill, router, circular_saw] ``` What the builder has. A design that needs a mortiser when the list says otherwise is worth saying out loud, and it changes which joinery is a reasonable suggestion in a repair. Common values: `table_saw`, `circular_saw`, `mitre_saw`, `bandsaw`, `router`, `drill`, `drill_press`, `planer`, `jointer`, `domino`, `pocket_hole_jig`, `chisel`, `hand_tools`. ## `must_satisfy` ```yaml must_satisfy: - standards.bed.slat_gap_max - standards.bed.centre_rail_required ``` Catalogue standard ids the design promises to meet. Listing one turns its check from advisory into a **requirement of this design**, and the finding cites the source — who says so, not just that something is wrong: ``` DOM-002 A 1636 mm bed needs a centre support rail with at least one leg to the floor. Mattress warranty conditions for queen and larger sizes require one above 1370 mm width. ``` Every standard carries a citation, because an uncited number is an opinion and the value of this validator is that it is not giving you opinions. The full list is in [Standards](/docs/catalogue/standards). ## `notes` ```yaml notes: - The rails must come off without disturbing the headboard panel. - Matching the existing bedside tables — walnut, oiled. ``` Prose, for the people. Nothing checks these, and they are still worth writing: they are the constraints that explain the design to whoever reads it next, including the model asked to change it. ## Why this section pays for itself Requirements are what a repair reads. When the validator raises a finding and you paste the report back, the model fixing it can see that the piece has to come apart, that nothing may exceed 2100 mm, and that the slat gap is a promise rather than a preference. Without that, a plausible fix — glue the cleat, lengthen the rail, spread the slats — can satisfy the finding and break the design. --- --- title: "Assembly and outputs" description: "The part held still, how the piece is grouped and taken apart, and what gets drawn, exported and tabulated." url: https://kaicad.barpom.xyz/docs/schema/assembly-and-outputs schema: "0.1" source: packages/spec/src/design.ts --- # Assembly and outputs ## `assembly` ```yaml assembly: ground: part: headboard_panel at: [0, 0, (headboard_height - 40) / 2] orient: { length: x, width: -z, thickness: y } groups: - { id: head_end, label: Head end, members: [head_post@left, head_post@right, headboard_panel] } - { id: base, label: Base, members: [side_rail@*, slat_cleat@*] } disassembly: - { id: slats, label: Lift out the slats, removes: [slat@*] } - { id: rails, label: Unbolt the rails, removes: [side_rail@*] } - { id: ends, label: Separate the ends, removes: [foot_end, head_end] } ``` ### `ground` The part held still. Everything else is positioned relative to it through the chain of joints. ```yaml ground: cleat # shorthand, when the defaults are right ``` ```yaml ground: part: headboard_panel orient: { length: x, width: -z, thickness: y } at: [0, 0, (headboard_height - 40) / 2] ``` **`orient`** maps the ground part's own axes onto world axes. Without it, a part's `length` runs along world X, its `width` along Y and its `thickness` along Z — which lies a headboard panel flat on the floor. The declaration above stands it up: its length spans the bed's width along X, its width runs *up* (`-z` because the panel's `start` is its top edge in this design), and its 18 mm thickness runs front-to-back along Y. **`at`** places the ground part's origin in world space. This is the one place in the whole format where a coordinate is legitimate, and it is doing one job: putting the piece at a sensible height so that `z` means *height above the floor* and the ergonomic checks have a datum. Get that wrong and every height-based finding is measured from the wrong place. **Choose the ground part for symmetry.** Grounding the headboard panel rather than a post makes the queen bed symmetric about its centre line for free: the two posts hang off the panel's ends, the rails off the posts, the foot posts off the rails. Grounding a post instead would make every dimension relative to one corner, and half the offsets would become coordinates in disguise. ### `groups` Named collections, used by the exploded view, the isolate control in the workbench and the disassembly steps. ```yaml groups: - id: base label: Base members: [side_rail@*, slat_cleat@*] ``` `@*` matches every instance of a part. ### `disassembly` How the piece comes apart, in order. It is **reversed** to produce the assembly steps, because taking something apart is the direction people can actually describe. ```yaml disassembly: - { id: slats, label: Lift out the slats, removes: [slat@*] } - { id: rails, label: Unbolt the rails, removes: [side_rail@*] } - { id: ends, label: Separate the ends, removes: [foot_end, head_end] } ``` `removes` takes part references and group ids. This is also what `requirements.knock_down` is checked against: every joint crossed by a disassembly step has to be one that actually undoes. ## `outputs` ```yaml outputs: drawings: assembly: { views: [iso, front, side, plan], sheet: a3, scale: auto } parts: { all: true, sheet: a4, dimension: envelope_and_features } details: [rail_to_head_post_left, cleat_to_rail_left] exploded: true exports: [step, glb, svg] data: [cutlist, bom, assembly_steps, validation] ``` Optional in full. Omit the section and you get the defaults — a compile, a cut list, a BOM and a validation report. ### `drawings` | Key | | | --- | --- | | `assembly.views` | `iso`, `front`, `side`, `plan`, `back`, `bottom` | | `assembly.sheet` | `a4`, `a3`, `a2`, `a1` | | `assembly.scale` | `auto`, or a number | | `parts.all` | One sheet per part | | `parts.only` | A list of part ids instead | | `parts.dimension` | `envelope`, `features`, `envelope_and_features` | | `details` | Joint ids to draw as detail views | | `exploded` | Add an exploded assembly view | > The `drawing` lane is not built yet. These keys are part of the schema and are accepted; > the sheets themselves arrive with that lane. Everything else on this page compiles today. ### `exports` `step` · `glb` · `stl` · `3mf` · `svg` · `dxf` · `pdf` STEP is the one to hand to another CAD package; GLB is the one the viewer uses; STL is for printing a test fit of a joint before cutting timber. ### `data` `cutlist` · `bom` · `assembly_steps` · `validation` | | | | --- | --- | | `cutlist` | Every part, grouped by stock, with rough sizes including the material's allowance | | `bom` | Every fixture, with pack sizes and counts | | `assembly_steps` | `disassembly`, reversed | | `validation` | The report — [reading one](/docs/validation/reading-a-report) | ## From the command line ``` fcad compile design.yaml -o out/ everything fcad check design.yaml validation only, no kernel fcad cutlist design.yaml the cut list fcad inspect design.yaml where every part ended up fcad check design.yaml --md the report, ready to paste back ``` `--no-geometry` skips the CAD kernel. The rules that need solids are then reported as **unverified** rather than passed, and the report says which and why — a clean bill of health you did not earn is the most misleading thing this tool could give you. --- --- title: "Units and precision" description: "Millimetres everywhere, degrees for angles, exact arithmetic internally, 0.1 mm on display — and the rule that stops a rounded cut list breaking a clearance that passed." url: https://kaicad.barpom.xyz/docs/schema/units-and-precision schema: "0.1" source: packages/spec/src/units.ts --- # Units and precision ## Metric only Every length in a design is **millimetres**. Every angle is **degrees**. There is no alternative unit, no per-field unit suffix and no conversion layer. ```yaml units: mm ``` The `units` key accepts only `mm`. It exists so that a document written in inches is rejected with a clear message rather than compiled into a piece of furniture forty times too large. This is not a preference. Mixed units are one of the classic ways a parametric model goes quietly wrong, and the cost of supporting them — every expression, every catalogue entry and every finding carrying a unit — buys nothing that a converted number at the point of authoring does not. **If you think in inches**, convert once, when you write the file: 3/4″ is 19 mm, 1½″ is 38 mm, 4′ × 8′ is 1220 × 2440. The catalogue's milled sizes are metric actuals, so 19 mm and 25 mm are real thicknesses you can buy. Degrees rather than radians for angles, because a human writes this file. ## Exact inside, rounded only to show you Internally every length is a floating-point millimetre and stays that way. Nothing is rounded in transit — not between the resolver and the assembler, not between the assembler and the validator, not on the way into an export. Rounding happens at **presentation only**: 0.1 mm on the cut list, in the inspector and on a drawing's dimensions. A part 1636.0000001 mm long reads as 1636 mm, and the validator still saw the number it actually was. Two lengths closer than 10⁻⁶ mm are the same length as far as validation is concerned. That tolerance sits well below the display precision on purpose: anything you can see in the UI is also distinguishable to the validator. ## The rule that matters > A value rounded for a cut list must never silently violate a clearance that passed on the > exact value. Consider a clearance that requires at least 6 mm and comes out at 5.96 mm. It fails, and you are told. Now consider one that comes out at 5.9999 mm against a 6 mm minimum: it fails on the exact number, but a cut list printed at 0.1 mm would say **6.0**, and a person reading that sheet would have no way of knowing the design did not pass. The compiler detects exactly that case — where the exact value is on one side of a limit and the rounded value is on the other — and does not let the rounded figure stand alone. What you read on a sheet and what the validator checked cannot disagree about which side of a limit a number is on. ## What this means when you author **Do not pre-round.** Write the expression, not the answer: ```yaml derived: mattress.width + 2 * mattress_clearance # yes value: 1536 # no ``` The second form is correct until something upstream moves, and then it is wrong with nothing to say so. **Do not chase the last 0.1 mm.** If a finding says a shelf sags 4.6 mm against a 4.5 mm limit, the fix is a thicker shelf or a shorter span, not a slightly different number. The suggestion in the finding is already computed — it will tell you which. **Trust the material, not the nominal size.** The catalogue's thicknesses are what the board actually measures, not what it is sold as. A "1 inch" board is 19 mm or 25 mm depending on the family, and the resolver will reject a thickness that nothing is milled at rather than let you design around a board that does not exist. ## Display, in practice | Where | Precision | | --- | --- | | Cut list, BOM | 0.1 mm | | Inspector, part properties | 0.1 mm | | Drawing dimensions | 0.1 mm | | Exports (STEP, GLB, STL) | Full precision | | Validation findings | 0.1 mm, computed from the exact value | Angles display as whole degrees unless they are not whole. --- --- title: "How the catalogue works" description: "Materials, fixtures and standards — how an id resolves, why a fixture is a machining recipe rather than a mesh, and the licence." url: https://kaicad.barpom.xyz/docs/catalogue/how-the-catalogue-works schema: "0.1" source: packages/catalogue/data --- # How the catalogue works The catalogue is the shared data a design references. You do not author it and nothing is copied into your file — you name entries from it, and the compiler resolves them. ```yaml catalogs: materials: std/lumber@1 fixtures: std/fixtures@1 standards: std/standards@1 ``` All three default to the bundled `std/…@1`, so this section is normally omitted. ## Three families | Family | Ids look like | What an entry is | | --- | --- | --- | | **Materials** | `lumber.hardwood.s4s`, `sheet.plywood.birch` | A stock family and the sizes it is actually milled in | | **Fixtures** | `fixtures.wood_screw.4x50` | A solid, **plus a machining recipe, plus design rules** | | **Standards** | `standards.bed.slat_gap_max` | A published number, with its citation | Species sit alongside materials: `walnut`, `pine`, `white_oak` and the rest carry the physical properties that solid-lumber families inherit. ## Where each one is referenced from ```yaml materials: - id: rail_stock from: lumber.hardwood.s4s # ← a material family species: walnut # ← a species thickness: 38 width: 190 parameters: - id: mattress type: catalog_ref catalog: standards.mattress # ← a standard, as a parameter value: us-queen - id: max_slat_gap type: length derived: standards.bed.slat_gap_max # ← a standard, in an expression joints: - id: cleat_to_rail_left fixture: fixtures.wood_screw.4x50 # ← a fixture ``` An id that does not exist is [`RES-005`](/docs/validation/error-codes#res-005) or [`RES-009`](/docs/validation/error-codes#res-009), and the finding lists what does — near misses first. **Invented ids are the single most common way an LLM-authored design fails**, so the failure is fast, specific and repairable rather than mysterious. ## A material family is not a board `lumber.hardwood.s4s` carries nine species, seven thicknesses and eleven widths. A design narrows it to one real board: ```yaml - id: post_stock from: lumber.hardwood.s4s species: walnut thickness: 75 width: 75 ``` A combination that is not milled is rejected, and the finding names the nearest two that are. The sizes are **actual**, never nominal — see [Solid lumber](/docs/catalogue/solid-lumber). ## A fixture is a machining recipe This is the part that has no equivalent in any open woodworking library, and it is the reason hardware in a kaicad design is structural rather than decorative. A fixture entry is: 1. **A solid** — a mesh or a couple of primitives, for the 3D view. Threads are never modelled; a smooth shank with a head is all a preview needs and a fraction of the triangles. 2. **A machining recipe for its neighbours** — what to cut in the part the fastener passes through, and what to cut in the part it goes into. Every hole in a compiled design comes from one of these. 3. **The design rules it imposes** — minimum edge distance, end distance, engagement, spacing, and whether it may go into end grain. 4. **The dimensions it determines** — a counterbore diameter, a cross-dowel depth. ```yaml machining: through_from: opposite through_part: - { kind: hole, diameter: 9, through: true } - { kind: counterbore, diameter: 18, depth: 8 } receiving_part: - { kind: hole, diameter: 9, depth: 100, axis: from_mating_face } - { kind: cross_hole, diameter: 12.5, at_depth: 80, depth: through_width } ``` The **joint** supplies the position; the **fixture** supplies the operations. You never write a pilot hole. ### `through_from` is the field that catches people | Value | Means | | --- | --- | | `mating` | The fastener enters on the face that **touches** the other part — a screw through a cleat into a rail. | | `opposite` | It enters on the **far** face — a bed bolt through a post from the outside, counterbore and head away from the rail. | Getting this wrong puts an 18 mm counterbore in the wrong surface and runs the clearance hole down the length of the board instead of across it. That design compiles and exports cleanly and cannot be built, which is why the fixture declares the direction rather than the joint guessing it. Your side of it is the joint's [`through:`](/docs/schema/joints#through--which-way-the-hardware-goes-in) part. ### `N * d` rules Some rules are written against the fixture's nominal diameter: ```yaml rules: min_edge_distance: "3 * d" min_end_distance: "6 * d" min_spacing: "8 * d" ``` `d` is `nominalDiameter`. For a 4 mm screw that is 12 mm from an edge, 24 mm from an end and 32 mm between screws — the usual shop rules, applied by something that does not forget them. ## A standard carries its source ```yaml - id: standards.bed.slat_gap_max kind: limit source: > Consensus of mattress-maker warranty conditions (Tempur, Sealy, IKEA and others all specify 75–80 mm); 76 mm is the most commonly stated figure. limit: { value: 76, direction: max, unit: mm } ``` **`source` is required.** An uncited number is an opinion, and the whole value of this validator is that it is not giving you opinions — when a report says a slat gap is too wide, it can say who says so. Four kinds: | Kind | Shape | Example | | --- | --- | --- | | `dimension_set` | Named sizes | `standards.mattress.us-queen` | | `limit` | One bound, `max` or `min` | `standards.bed.slat_gap_max` | | `range` | `min`, `max`, `typical` | `standards.table.dining_height` | | `rule` | A requirement in prose, with a condition | `standards.bed.centre_rail_required` | A `dimension_set` used as a `catalog_ref` parameter behaves like the entry it names, so `mattress.width` reads naturally. A `limit` or a `range` field can be referenced directly in an expression. ## What is in it | Page | | | --- | --- | | [Species](/docs/catalogue/species) | Fourteen timbers, with density, stiffness, movement and screw holding | | [Solid lumber](/docs/catalogue/solid-lumber) | Three families, and every milled thickness and width | | [Sheet goods](/docs/catalogue/sheet-goods) | Plywood, MDF and veneered MDF, with sheet sizes | | [Wood screws](/docs/catalogue/wood-screws) | Six sizes, with pilot, clearance and countersink | | [Bed bolts](/docs/catalogue/bed-bolts) | Four sizes, and the five holes each one cuts | | [Knock-down hardware](/docs/catalogue/knock-down-hardware) | Threaded inserts, cam locks, dowels, shelf pins | | [Cable management](/docs/catalogue/cable-management) | Grommets and tie mounts | | [Standards](/docs/catalogue/standards) | Mattress sizes, slat gaps, ergonomic ranges — each cited | Coming in v2: concealed hinges, side-mount and undermount slides, pulls and knobs, levelling feet, and figure-8s, Z-clips and buttons. ## Licence The catalogue is **CC0-1.0** — public domain, and meant to be adopted elsewhere. It encodes published standard dimensions and cites vendor part numbers; it never redistributes vendor CAD files. See [Licences](/docs/reference/licences). If you build something else that needs to know that an M8 bed bolt wants a 12.5 mm cross bore at 80 mm, take the data. That is what it is for. --- --- title: "Species" description: "Fourteen timbers with density, stiffness, seasonal movement and screw-holding — the properties the deflection, wood-movement and fastener checks read." url: https://kaicad.barpom.xyz/docs/catalogue/species schema: "0.1" source: packages/catalogue/data/species --- # Species Species carry the physical properties that solid-lumber families inherit. Naming one in a material is what gives the validator something to reason with: a pine cleat and a maple rail of identical size take the same screw and hold it differently, and the report says so. ```yaml materials: - id: rail_stock from: lumber.hardwood.s4s species: walnut thickness: 38 width: 190 ``` ## Hardwoods Available in `lumber.hardwood.s4s` and `lumber.hardwood.rough`. | Id | | Density | E | MOR | Movement T / R | Screw face / end | Hardness | | --- | --- | --- | --- | --- | --- | --- | --- | | `walnut` | American black walnut | 610 | 11 600 | 101 | 2.57 / 1.82 | high / low | 4490 | | `white_oak` | White oak | 755 | 12 300 | 105 | 3.50 / 1.80 | high / low | 6000 | | `red_oak` | Red oak | 700 | 12 500 | 99 | 3.47 / 1.37 | high / low | 5700 | | `ash` | White ash | 670 | 12 000 | 103 | 2.60 / 1.60 | high / low | 5870 | | `maple` | Hard maple | 705 | 12 600 | 109 | 3.23 / 1.60 | high / low | 6450 | | `cherry` | Black cherry | 560 | 10 300 | 85 | 2.53 / 1.17 | medium / low | 4230 | | `beech` | European beech | 720 | 14 300 | 103 | 3.87 / 1.87 | high / low | 5800 | | `birch` | Yellow birch | 670 | 13 900 | 114 | 3.00 / 2.43 | high / low | 5580 | | `poplar` | Yellow poplar | 455 | 10 900 | 70 | 2.73 / 1.40 | medium / poor | 2420 | ## Softwoods Available in `lumber.softwood.s4s`. | Id | | Density | E | MOR | Movement T / R | Screw face / end | Hardness | | --- | --- | --- | --- | --- | --- | --- | --- | | `pine` | Scots pine / European redwood | 510 | 10 100 | 83 | 2.47 / 1.33 | medium / poor | 2420 | | `spruce` | European spruce / whitewood | 450 | 10 200 | 70 | 2.57 / 1.27 | low / poor | 1800 | | `douglas_fir` | Douglas fir | 530 | 13 400 | 85 | 2.43 / 1.60 | medium / poor | 2900 | | `western_red_cedar` | Western red cedar | 350 | 7700 | 51 | 1.67 / 0.80 | low / poor | 1560 | | `larch` | European larch | 590 | 12 500 | 92 | 2.50 / 1.43 | medium / poor | 3200 | Units: density kg/m³ · **E** (modulus of elasticity) MPa · **MOR** (modulus of rupture) MPa · movement % per 10% change in moisture content, tangential / radial · hardness N (Janka). ## What each figure is used for **Density** sets the weight on the BOM and feeds the load checks. A queen bed in white oak is noticeably heavier than the same bed in pine, which matters if it has to be carried upstairs assembled. **Modulus of elasticity** drives every deflection check — shelf sag, rail bending under `max_assembled_load`. It is the number that decides whether an 900 mm shelf needs to be 19 mm or 25 mm, and the finding tells you which. **Modulus of rupture** is the breaking stress, used for the margin against outright failure rather than visible sag. **Movement, tangential and radial**, is seasonal shrinkage per 10% change in moisture content. Tangential movement is roughly twice radial in most timbers, which is why a flat-sawn panel moves more than a quarter-sawn one. The wood-movement checks read these together with the part's [`grain`](/docs/schema/parts#grain) to work out whether a captured panel will split its frame. White oak at 3.50% tangential is the mover in this list; western red cedar at 1.67% is the most stable. The arithmetic is *width × coefficient × (MC swing ÷ 10)*: a 560 mm flat-sawn white-oak panel through a 4% seasonal swing moves 560 × 0.035 × 0.4 ≈ **7.8 mm**. Indoor timber typically swings 4–5% between winter and summer, so substitute your own figure — the point is that it is millimetres, not fractions of one, and a frame that did not allow for it splits. **Screw withdrawal**, face and end, is graded `high` · `medium` · `low` · `poor`. The fastener checks use it, and note that **every timber here rates worse in end grain than in face grain** — which is why `forbid_into_end_grain` exists on the fixtures and why bed bolts, which thread into a steel cross dowel rather than into wood, are the right answer for a rail end. **Hardness** is Janka, in newtons. It does not drive a check; it is there for choosing a timber for a surface that gets used. ## Sources Figures are at approximately 12% moisture content, from the **USDA Wood Handbook (FPL-GTR-282, 2021)**, Tables 5-3 and 5-5, converted to SI. Movement coefficients are total green-to-ovendry shrinkage divided by 30% MC, the standard linear approximation for seasonal movement in service. These are published material properties, not anyone's dataset. The catalogue is **CC0-1.0**. ## Sheet goods do not have a species A panel behaves the way it does because of how it is built, not what tree it came from, so sheet materials carry their own properties — see [Sheet goods](/docs/catalogue/sheet-goods). The one exception is `sheet.veneered.mdf`, which takes a species for the veneer, because the veneer is directional and the sheet has a show face. --- --- title: "Solid lumber" description: "Three stock families with every thickness and width they are actually milled in — metric actuals, never nominal — and the rough allowances the cut list adds." url: https://kaicad.barpom.xyz/docs/catalogue/solid-lumber schema: "0.1" source: packages/catalogue/data/materials/solid-lumber.yaml --- # Solid lumber ```yaml materials: - id: post_stock from: lumber.hardwood.s4s species: walnut thickness: 75 width: 75 ``` **Every size on this page is actual, not nominal.** These are what the board measures when a merchant hands it over, which is the only size worth designing to. A thickness that is not on the list is [`RES-009`](/docs/validation/error-codes#res-009), and the finding names the nearest two that are — a 35 mm rail is a rail you cannot buy, and finding that out at the timber yard is worse than finding it out here. ## `lumber.hardwood.s4s` Hardwood, surfaced four sides. | | | | --- | --- | | **Species** | `walnut` `white_oak` `red_oak` `ash` `maple` `cherry` `beech` `birch` `poplar` | | **Thicknesses** | 19 · 25 · 32 · 38 · 50 · 63 · 75 mm | | **Widths** | 50 · 63 · 75 · 100 · 125 · 150 · 175 · 190 · 215 · 240 · 290 mm | | **Lengths** | 1800 · 2100 · 2400 · 3000 · 3600 mm | | **Rough allowance** | length 50 · width 6 · thickness 3 mm | | **Cost** | ≈ 4200 per m³ | Boards above about 240 mm wide are increasingly hard to find in one piece and usually mean a glue-up. The width check **warns** rather than errors — it is a procurement problem, not a design error. ## `lumber.softwood.s4s` Softwood, planed all round. | | | | --- | --- | | **Species** | `pine` `spruce` `douglas_fir` `western_red_cedar` `larch` | | **Thicknesses** | 18 · 20 · 25 · 32 · 38 · 45 · 50 · 63 · 75 mm | | **Widths** | 40 · 45 · 70 · 95 · 120 · 145 · 170 · 195 · 220 · 245 mm | | **Lengths** | 2400 · 3000 · 3600 · 4200 · 4800 mm | | **Rough allowance** | length 50 · width 5 · thickness 2 mm | | **Cost** | ≈ 900 per m³ | These are **planed** sizes. The sawn size the board is sold under is larger and is not modelled — a "2 × 4" is 45 × 95 here, because that is what you can measure. ## `lumber.hardwood.rough` Hardwood, rough sawn. Same species as the surfaced family. | | | | --- | --- | | **Thicknesses** | 25 · 32 · 38 · 50 · 63 · 75 · 100 mm | | **Widths** | 100 · 125 · 150 · 175 · 200 · 225 · 250 · 300 mm | | **Lengths** | 1800 · 2100 · 2400 · 3000 · 3600 mm | | **Rough allowance** | length 75 · width 10 · thickness 6 mm | | **Cost** | ≈ 3100 per m³ | Sold rough, so the stated thickness is what you **start** with, not what you finish at. The allowances are correspondingly larger — 6 mm on thickness, against 3 mm for surfaced stock, because a rough board has to be flattened before it is anything. Use this family when you are milling your own. Otherwise use `lumber.hardwood.s4s`. ## Rough allowances and the cut list The allowance is the extra on each dimension the cut list asks for, so there is enough board to trim both ends square, joint an edge and flatten a face that has moved. A part with a finished blank of 2032 × 190 × 38 in `lumber.hardwood.s4s` appears on the cut list as **2082 × 196 × 41** rough. That is what you buy; the blank is what you cut to. Sheet goods carry much smaller allowances — 10 mm on length and width, nothing on thickness — because a sheet arrives flat and stays flat. ## Choosing a size Two habits worth having. **Drive the width from the stock, not the other way round.** ```yaml blank: length: deck_length width: rail_stock.width # the full width of the board thickness: rail_stock.thickness ``` A rail that uses the full 190 mm of a 190 mm board wastes nothing and does not need ripping. **Check the length against the available ones.** A 2400 mm part cut from a 2400 mm board leaves no allowance for trimming. The next length up is the one you want, and the cut list's rough length will tell you which that is. --- --- title: "Sheet goods" description: "Plywood, MDF and veneered MDF — thicknesses, sheet sizes and the properties that make a panel behave differently from a board." url: https://kaicad.barpom.xyz/docs/catalogue/sheet-goods schema: "0.1" source: packages/catalogue/data/materials/sheet-goods.yaml --- # Sheet goods ```yaml materials: - id: panel_stock from: sheet.plywood.birch thickness: 18 ``` Sheet materials take a thickness and no width — the width comes from the sheet. They also carry **their own properties** rather than inheriting from a species, because a panel behaves the way it does because of how it is built, not what tree it came from. Set [`grain`](/docs/schema/parts#grain) on the part anyway. Plywood is stiff along the face grain and noticeably less so across it, and a veneered sheet has a visible direction. ## `sheet.plywood.birch` Birch plywood. The default choice for a structural panel. | | | | --- | --- | | **Thicknesses** | 4 · 6 · 9 · 12 · 15 · 18 · 21 · 24 · 30 mm | | **Sheet sizes** | 2440 × 1220 · 2500 × 1250 · 3050 × 1525 mm | | **Rough allowance** | length 10 · width 10 · thickness 0 mm | | **Cost** | ≈ 95 per sheet | | Density | E | MOR | Movement | Screw face / end | | --- | --- | --- | --- | --- | | 680 | 9500 | 60 | 0.15 / 0.15 | medium / low | ## `sheet.plywood.poplar` Poplar-core plywood. Lighter, cheaper and softer; fine for a back or a bottom, marginal for a shelf. | | | | --- | --- | | **Thicknesses** | 6 · 9 · 12 · 15 · 18 · 22 · 25 mm | | **Sheet sizes** | 2440 × 1220 · 2500 × 1250 mm | | **Cost** | ≈ 55 per sheet | | Density | E | MOR | Movement | Screw face / end | | --- | --- | --- | --- | --- | | 470 | 6500 | 40 | 0.15 / 0.15 | low / **poor** | ## `sheet.mdf` Medium-density fibreboard. | | | | --- | --- | | **Thicknesses** | 6 · 9 · 12 · 15 · 18 · 22 · 25 · 30 mm | | **Sheet sizes** | 2440 × 1220 · 3050 × 1220 mm | | **Cost** | ≈ 38 per sheet | | Density | E | MOR | Movement | Screw face / end | | --- | --- | --- | --- | --- | | 750 | 3600 | 32 | 0.25 / 0.25 | low / **poor** | **Very poor edge screw holding.** Anything structural wants a [threaded insert](/docs/catalogue/knock-down-hardware) or a through bolt rather than a screw into the edge. It is also the least stiff material here — at E 3600 it is a third of birch ply, so an MDF shelf of the same span sags roughly three times as far. ## `sheet.veneered.mdf` Veneered MDF. The one sheet material that takes a species. | | | | --- | --- | | **Species** | `walnut` `white_oak` `ash` `maple` `birch` | | **Thicknesses** | 18 · 22 · 25 mm | | **Sheet sizes** | 2440 × 1220 · 2800 × 2070 mm | | **Cost** | ≈ 130 per sheet | | Density | E | MOR | Movement | Screw face / end | | --- | --- | --- | --- | --- | | 760 | 3800 | 34 | 0.25 / 0.25 | low / **poor** | The veneer is directional and the sheet has a show face, so parts cut from it are grain-constrained in a way plain MDF is not. Set `grain:` and mean it. Units: density kg/m³ · **E** MPa · **MOR** MPa · movement % per 10% change in moisture content, tangential / radial. ## What to notice **Movement is tiny, and equal both ways.** Plywood at 0.15% barely moves in plane, against 2.5–3.5% for solid timber. That is the whole reason a panel goes in a groove and a solid top does not: a plywood panel can be captured on all four sides and glued, and a solid one cannot. **Stiffness is the deciding number.** Birch ply at E 9500 is close to softwood; MDF at 3600 is not close to anything. For a shelf, the deflection check will make the choice for you and give you the thickness that passes. **Panel stiffness figures are along the face grain.** Across it, plywood is considerably less stiff. The shelf-sag rule accounts for that separately, using the part's `grain`. **Edge screw holding is the failure mode.** Every sheet material here rates `low` or `poor` in the edge, which is where people put screws when assembling a carcass. The fastener checks will raise it; the fix is an insert, a dowel, or a through bolt. ## Sheet sizes and nesting The cut list nests parts into sheets and tells you how many you need. Two things follow: - **A part longer than the sheet is an error, not a warning.** Sheets have sheet sizes rather than a list of lengths, so the check is against both dimensions of the sheet — and it is the compiler's job, not yours. - **Sheet choice matters.** `sheet.plywood.birch` at 3050 × 1525 will yield a 1600 mm panel in one piece where the 2440 × 1220 sheet will not. --- --- title: "Wood screws" description: "Six countersunk sizes, each with the pilot hole, clearance hole and countersink it cuts, and the edge, end, engagement and spacing rules it imposes." url: https://kaicad.barpom.xyz/docs/catalogue/wood-screws schema: "0.1" source: packages/catalogue/data/fixtures/wood-screw.yaml --- # Wood screws Countersunk wood screws to **DIN 7997**. The everyday fixture: cleats to rails, backs to carcasses, anything screwed rather than joined. ```yaml joints: - id: cleat_to_rail_left kind: screwed fixture: fixtures.wood_screw.4x50 mate: a: slat_cleat@left:face.top b: side_rail@left:face.bottom align: { contact: true } fasteners: pattern: { along: length, count: 10, spacing: auto, margin: 80 } ``` That declaration puts ten screws down the cleat, and with each one: a 4.5 mm clearance hole through the cleat, an 8.4 mm × 90° countersink, and a 2.5 mm pilot 35 mm into the rail. You write none of those. ## The six sizes | Id | d | Length | Drive | Head Ø | | --- | --- | --- | --- | --- | | `fixtures.wood_screw.3x30` | 3 | 30 | Pozi 1 | 6.0 | | `fixtures.wood_screw.4x30` | 4 | 30 | Torx 20 | 8.4 | | `fixtures.wood_screw.4x40` | 4 | 40 | Torx 20 | 8.4 | | `fixtures.wood_screw.4x50` | 4 | 50 | Torx 20 | 8.4 | | `fixtures.wood_screw.5x60` | 5 | 60 | Torx 25 | 10.0 | | `fixtures.wood_screw.5x80` | 5 | 80 | Torx 25 | 10.0 | ## What each one cuts **Through part** — the board the screw passes through. **Receiving part** — the one it threads into. The screw enters on the **mating** face, the one touching the other part. | Id | Clearance | Countersink | Pilot | Pilot depth | | --- | --- | --- | --- | --- | | `3x30` | 3.4 | 6.0 × 90° | 1.9 | 22 | | `4x30` | 4.5 | 8.4 × 90° | 2.5 | 20 | | `4x40` | 4.5 | 8.4 × 90° | 2.5 | 28 | | `4x50` | 4.5 | 8.4 × 90° | 2.5 | 35 | | `5x60` | 5.5 | 10.0 × 90° | 3.2 | 42 | | `5x80` | 5.5 | 10.0 × 90° | 3.2 | 55 | All in millimetres. Pilot diameters are for hardwood; softwood tolerates the same pilot and simply holds a little less well, which the species' withdrawal rating already captures — so the catalogue does not fork per species. Threads are never modelled. The mesh is a smooth shank with a head, which is all a 3D preview needs and a fraction of the triangles. ## The rules each one imposes | Id | Min edge | Min end | Min engagement | Min spacing | Max through | | --- | --- | --- | --- | --- | --- | | `3x30` | 9 | 18 | 14 | 24 | 14 | | `4x30` | 12 | 24 | 15 | 32 | 14 | | `4x40` | 12 | 24 | 18 | 32 | 22 | | `4x50` | 12 | 24 | 20 | 32 | 26 | | `5x60` | 15 | 30 | 25 | 40 | 32 | | `5x80` | 15 | 30 | 30 | 40 | 48 | Edge, end and spacing follow the usual shop rules of **3d**, **6d** and **8d**, resolved against the nominal diameter. Every one of these is checked, and every failure comes back with a computed suggestion rather than an observation. **Min engagement** is how much thread has to be in the receiving part. This is the check that catches the classic error: ``` FAS-003 Screws in `cleat_to_rail_left` have 0 mm of engagement — the screw passes through 40 mm of a 40 mm cleat. Use `fixtures.wood_screw.4x50`, which gives 20 mm. ``` **Max through thickness** is the most board the screw can pass through and still reach. A 4×40 through a 25 mm cleat has 15 mm left, which is under its 18 mm minimum engagement — so the compiler tells you to go to a 4×50 before you have drilled anything. ## End grain Every size carries `forbid_into_end_grain: warn`. A screw into end grain holds roughly half as well as one into face grain, and every timber in the catalogue rates worse in the end than the face. It is a **warning**, not an error, because it is sometimes the right answer — a screw into the end of a cleat that is also glued and carries no load is fine. When it is carrying load, the answer is usually a [threaded insert](/docs/catalogue/knock-down-hardware) or a [bed bolt](/docs/catalogue/bed-bolts), which threads into steel rather than into wood. ## Choosing a size Work from the engagement, not the total length: > **screw length − thickness of the through part ≥ min engagement** A 25 mm cleat onto a 38 mm rail: a 4×40 leaves 15 mm (short of 18), a 4×50 leaves 25 mm. Take the 4×50. Then check the receiving part is thick enough that the pilot does not come out the far side. A 5×80 pilots 55 mm deep, which is more than a 38 mm rail has. The compiler does both of these. The point of knowing the rule is being able to pick correctly the first time. ## Procurement | Id | Pack sizes | Unit cost | | --- | --- | --- | | `3x30` | 200 · 500 | 0.04 | | `4x30` | 100 · 200 · 500 | 0.05 | | `4x40` | 100 · 200 · 500 | 0.06 | | `4x50` | 100 · 200 | 0.07 | | `5x60` | 100 · 200 | 0.11 | | `5x80` | 50 · 100 | 0.15 | The BOM rounds up to whole packs, so eleven screws is one box of 100. --- --- title: "Bed bolts" description: "The knock-down rail-to-post joint — four sizes, and the five holes across two parts that each declaration cuts." url: https://kaicad.barpom.xyz/docs/catalogue/bed-bolts schema: "0.1" source: packages/catalogue/data/fixtures/bed-bolt.yaml --- # Bed bolts A bed bolt passes through the post from the outside and threads into a **steel cross dowel** buried in the end of the rail. It is the joint that lets a bed come apart to get up a staircase, and it is the clearest illustration of why a fixture in this catalogue is a machining recipe rather than a mesh. One declaration cuts **five holes across two parts**, one of them perpendicular to the other four and at a specific depth. Getting the cross-dowel bore wrong is the classic way a knock-down bed fails. ```yaml - id: rail_to_head_post_left kind: bed_bolt fixture: fixtures.bed_bolt.m8x150 removable: true through: head_post@left mate: a: side_rail@left:face.start b: head_post@left:face.top align: { contact: true, roll: -90, offset: [rail_drop_on_head_post, 0, 0] } fasteners: pattern: { along: length, count: 2, spacing: 100, from: ..., to: ... } ``` ## The four sizes | Id | d | Length | Drive | Dowel | Washer | | --- | --- | --- | --- | --- | --- | | `fixtures.bed_bolt.m6x80` | 6 | 80 | Hex 4 | 10 × 25, M6 | 12 | | `fixtures.bed_bolt.m8x100` | 8 | 100 | Hex 5 | 12 × 30, M8 | 16 | | `fixtures.bed_bolt.m8x130` | 8 | 130 | Hex 5 | 12 × 30, M8 | 16 | | `fixtures.bed_bolt.m8x150` | 8 | 150 | Hex 5 | 12 × 30, M8 | 16 | ## What each one cuts All four use `through_from: opposite` — **the bolt is driven through the post from the outside**, so its head and counterbore are on the face *away* from the rail. This is the field that catches people; see [`through`](/docs/schema/joints#through--which-way-the-hardware-goes-in). ### In the post (the through part) | Id | Clearance | Counterbore | | --- | --- | --- | | `m6x80` | 7 mm, through | 14 × 7 | | `m8x100` | 9 mm, through | 18 × 8 | | `m8x130` | 9 mm, through | 18 × 8 | | `m8x150` | 9 mm, through | 18 × 8 | ### In the rail (the receiving part) | Id | Bolt bore | Cross bore | At depth | | --- | --- | --- | --- | | `m6x80` | 7 mm × 50 deep | 10.5 mm, through the width | 35 | | `m8x100` | 9 mm × 60 deep | 12.5 mm, through the width | 40 | | `m8x130` | 9 mm × 85 deep | 12.5 mm, through the width | 65 | | `m8x150` | 9 mm × 100 deep | 12.5 mm, through the width | 80 | The bolt bore runs in from the mating face — the end of the rail. The cross bore is drilled perpendicular to it, at `at_depth` from that same face, and **must intersect the bolt bore** for the dowel to capture the bolt. Those two numbers have to agree with each other and with the bolt's length, which is precisely why nobody should be typing them. ## The rules | Id | Min edge | Min end | Min engagement | Min spacing | | --- | --- | --- | --- | --- | | `m6x80` | 20 | 30 | 40 | 60 | | `m8x100` | 18 | 38 | 50 | 70 | | `m8x130` | 18 | 38 | 70 | 70 | | `m8x150` | 18 | 38 | 70 | 70 | All require a **square shoulder** — the rail end has to meet the post flat, because the joint is a clamp and a gappy shoulder makes it a hinge. **The edge distance is governed by the cross dowel, not the bolt.** A 12.5 mm bore wants about 1.4 diameters of wood on each side, so an M8 bed bolt needs a rail of roughly 35 mm or more. An M8 through a 25 mm rail leaves 3.5 mm of timber around an 18 mm counterbore, and the validator says so: ``` FAS-001 M8 bed bolts through a 25 mm rail leave 3.5 mm of wood around the counterbore. Use `fixtures.bed_bolt.m6x80`, or increase the rail thickness to 38 mm. ``` ## End grain is fine here All four carry `forbid_into_end_grain: allow`, which is the opposite of every other fixture in the catalogue. The bolt threads into a **steel cross dowel**, not into the wood. That is the entire reason this joint exists: the end of a rail is the worst place in a piece of furniture to put a screw, and a bed bolt does not put one there. ## Choosing a size The bolt has to pass through the post and still reach a dowel far enough into the rail: > **post thickness − counterbore depth + cross-dowel depth ≤ bolt length** The counterbore buries the head, so it does not count against the length. A 75 mm post with the M8 × 150 works out at 75 − 8 + 80 = **147**, inside the bolt's 150 — which is why that entry notes it suits a 75 mm post. A thinner post takes a shorter bolt. Two bolts per corner, 100 mm apart, is the normal arrangement. One bolt is a pivot. ## Assembly Hex key, removable, and the step hint is `final_assembly` — the bolts go in last, after the ends are stood up and the rails are offered in. The assembly steps generated from `assembly.disassembly` put them in that order. ## Procurement | Id | Pack sizes | Unit cost | | --- | --- | --- | | `m6x80` | 4 · 8 | 1.80 | | `m8x100` | 4 · 8 | 2.40 | | `m8x130` | 4 · 8 | 2.90 | | `m8x150` | 4 · 8 | 3.40 | A bed with two bolts at each of four corners needs eight, which is one pack. --- --- title: "Knock-down hardware" description: "Threaded inserts, cam locks, dowels and shelf pins — what each cuts, what it demands of the panel, and which to reach for." url: https://kaicad.barpom.xyz/docs/catalogue/knock-down-hardware schema: "0.1" source: packages/catalogue/data/fixtures/knock-down.yaml --- # Knock-down hardware Hardware other than [bed bolts](/docs/catalogue/bed-bolts) for pieces that come apart — plus the two entries that are not knock-down at all but live in the same file: the glued dowel and the shelf pin. These are what let a design satisfy `requirements.knock_down` without glue. ## Threaded inserts The one to reach for in sheet goods. A wood screw into the edge of MDF or plywood holds badly — every sheet material in the catalogue rates `low` or `poor` there. An insert converts a poor screw joint into a good machine-screw joint. | Id | Insert | Machine screw | Drive | | --- | --- | --- | --- | | `fixtures.threaded_insert.m6` | 10 Ø × 13, M6 | M6 × 40 | Hex 4 | | `fixtures.threaded_insert.m8` | 13 Ø × 15, M8 | M8 × 50 | Hex 5 | **Through part** | **Receiving part** | Id | Clearance | Counterbore | Insert bore | | --- | --- | --- | --- | | `m6` | 6.5, through | 11 × 6 | 8.5 × 15 deep | | `m8` | 8.5, through | 14 × 8 | 11 × 17 deep | | Id | Min edge | Min end | Min engagement | Min spacing | | --- | --- | --- | --- | --- | | `m6` | 15 | 20 | 13 | 32 | | `m8` | 20 | 26 | 15 | 40 | End grain is `allow` — like a bed bolt, the load goes into steel rather than into wood. **The insert goes in before assembly and stays in.** Only the machine screw comes out when the piece is taken apart, which is what makes this joint survive being assembled and disassembled repeatedly where a wood screw into the same hole would not. ## Cam locks Flat-pack hardware: a cam buried in one panel, a dowel bolt threaded into the other, and a quarter turn pulls them together. | Id | Cam | Bolt | Drive | | --- | --- | --- | --- | | `fixtures.cam_lock.15` | 15 Ø × 12.5 | 7 Ø × 34 | Pozi 2 | | Through part | Receiving part | | --- | --- | | 15 mm bore, 12.5 deep · 8 mm cross bore at 9.5 depth, 24 long | 5 mm bore, 12 deep | | Min edge | Min end | Min engagement | Min spacing | Min panel thickness | | --- | --- | --- | --- | --- | | 12 | 24 | 11 | 64 | **15** | **Needs at least 15 mm of panel to bury the cam**, which is why it does not belong in 12 mm ply. That minimum is enforced, and the finding names it. End grain is `warn`: the dowel bolt threads into wood, so it is a real concern here in a way it is not for an insert. ## Dowels | Id | | | | --- | --- | --- | | `fixtures.dowel.8x40` | 8 × 40 fluted beech | Both holes 8 Ø × 21 deep | | Min edge | Min end | Min engagement | Min spacing | | --- | --- | --- | --- | | 16 | 24 | 19 | 32 | Both holes are **1 mm deeper than half the dowel** so glue has somewhere to go and the joint actually closes. Two 20 mm holes for a 40 mm dowel is how you end up with a joint that will not seat. **A doweled joint is glued, so it is not knock-down.** If the design declares `requirements.knock_down: true` and a dowel sits on the disassembly path, that is a finding. Dowels are here for alignment and for glued joinery, not for a piece that comes apart. ## Shelf pins | Id | | | | --- | --- | --- | | `fixtures.shelf_pin.5` | 5 mm pin, 16 long | 5 Ø × 10 deep, receiving part only | | Min edge | Min end | Min engagement | Min spacing | | --- | --- | --- | --- | | 10 | 20 | 9 | 32 | Adjustable shelving is drilled on the **32 mm system**, so a spacing that is a multiple of 32 keeps the whole drilling plan on one jig setting. Write it that way: ```yaml pattern: { along: length, count: 12, spacing: 32, from: 200 } ``` End grain is `warn`. The pin carries the shelf in shear, so the hole wants face grain around it. ## Which to use | Situation | Reach for | | --- | --- | | Rail into the end of a post, must come apart | [Bed bolt](/docs/catalogue/bed-bolts) | | Panel edge in sheet goods, must come apart | Threaded insert | | Flat-pack carcass, 15 mm panels or thicker | Cam lock | | Alignment in a glued joint | Dowel | | Adjustable shelf | Shelf pin | | Everything else, glued or not structural | [Wood screw](/docs/catalogue/wood-screws) | ## Procurement | Id | Pack sizes | Unit cost | | --- | --- | --- | | `threaded_insert.m6` | 10 · 25 · 50 | 0.55 | | `threaded_insert.m8` | 10 · 25 | 0.75 | | `cam_lock.15` | 10 · 20 | 0.35 | | `dowel.8x40` | 50 · 100 | 0.06 | | `shelf_pin.5` | 20 · 100 | 0.09 | --- --- title: "Cable management" description: "Grommets and cable tie mounts — the clear bore that limits the bundle, the panel thickness each needs, and what the path checks read." url: https://kaicad.barpom.xyz/docs/catalogue/cable-management schema: "0.1" source: packages/catalogue/data/fixtures/cable.yaml --- # Cable management Cable management is in v1 scope deliberately. A desk or a media unit that cannot get a cable from the back of a device to a socket is a design failure that no other woodworking tool checks for, and the geometry to check it — a panel with a hole in it — is already in the model. > **The `PTH-` path rules are not built yet.** The codes are reserved in the registry and no > rule implements them, so nothing currently checks that a cable route is continuous. These > entries are the numbers to design to in the meantime, and the edge-distance and > feature-within-blank checks do run against the holes you cut for them. ## Grommets | Id | Nominal Ø | Depth | Ring Ø | **Clear bore** | | --- | --- | --- | --- | --- | | `fixtures.grommet.40` | 40 | 22 | 46 | **34** | | `fixtures.grommet.60` | 60 | 24 | 68 | **52** | The 40 has a cap; the 60 has a brush insert, which keeps dust out and still passes a plug. ### `clearBore` is not the hole you drill This is the field that exists to prevent one specific mistake. The drilled hole for a `grommet.40` is 40 mm. The **usable** opening once the trim ring is fitted is 34 mm — the cap costs about 6 mm. Size a bundle against the drilled hole and it will not fit — a 40 mm grommet passes a 34 mm bundle, not a 40 mm one. ### Machining | Id | Through part | Receiving part | | --- | --- | --- | | `grommet.40` | 40 mm hole, through | — | | `grommet.60` | 60 mm hole, through | — | ### Rules | Id | Min edge | Min end | Min spacing | Panel thickness | | --- | --- | --- | --- | --- | | `grommet.40` | 28 | 28 | 60 | 12 – 22 | | `grommet.60` | 40 | 40 | 90 | 12 – 24 | **Both a minimum and a maximum panel thickness.** The maximum is real: the body is 22 mm long, so it will not reach through a 25 mm top, and the grommet simply will not seat. The minimum is what the clip needs to grab. A 40 mm grommet also needs 28 mm of panel around it. Putting one 20 mm from a back edge splits the panel, and the edge-distance check catches it. ## Cable tie mounts | Id | Base | Screw bore | | --- | --- | --- | | `fixtures.cable_tie_mount.20` | 20 mm square × 8 | 2.5 mm × 12 deep | | Min edge | Min end | Min engagement | Min spacing | | --- | --- | --- | --- | | 14 | 20 | 10 | 100 | Screwed or adhesive. End grain is `warn` — the mount is carrying a cable bundle in shear and wants face grain under it. 100 mm minimum spacing is about the bundle, not the wood: tie mounts closer than that do not let the cable bend between them. ## Using them A grommet does not join two parts, so there is no second part to mate to. Cut its hole as a [`hole` feature](/docs/schema/features#hole) on the panel, sized from the table above: ```yaml parts: - id: desk_top material: top_stock grain: length blank: { length: 1600, width: 700, thickness: 22 } features: - kind: hole id: cable_exit on: top diameter: 60 # fixtures.grommet.60 — drilled size, not clear bore through: true at: { u: 1400, v: 120 } ``` This is the one place the catalogue's numbers are yours to transcribe rather than the compiler's to apply, because a grommet is not a fastener between two parts. Check three things against the table: - **Panel thickness** is inside the entry's range — a 22 mm top is at the limit of the `grommet.40` and fine for the `grommet.60`. - **The hole is at least the minimum edge distance from every edge.** 120 mm from the back edge of a 700 mm top clears the `grommet.60`'s 40 mm easily; 20 mm would not, and [`GEO-003`](/docs/validation/error-codes#geo-003) measures a hole at its **rim**, not its centre. - **The bundle fits the clear bore**, not the drilled diameter. The hole itself is checked — that it lands on the board and keeps its edge distance. What is not yet checked is whether the route it belongs to actually goes anywhere, which is what the `PTH-` rules will do. ## Assembly Both grommets fit with no tool, and the step hint is `after_finish` — they go in once the panel is oiled or lacquered, because finishing around a fitted grommet is miserable. The tie mounts are `after_finish` too. Those hints reach the assembly steps when a fixture is declared on a joint. Since a grommet is authored as a feature today, put it in the part's `note:` instead. ## Procurement | Id | Pack sizes | Unit cost | | --- | --- | --- | | `grommet.40` | 1 · 10 | 2.10 | | `grommet.60` | 1 · 10 | 3.40 | | `cable_tie_mount.20` | 20 · 50 | 0.18 | --- --- title: "Standards" description: "Mattress sizes, slat gaps and ergonomic ranges — every number with the source it came from, because an uncited number is an opinion." url: https://kaicad.barpom.xyz/docs/catalogue/standards schema: "0.1" source: packages/catalogue/data/standards --- # Standards A standard is a published number a design can cite. **Every entry carries a `source`**, and that is not decoration: the whole value of this validator is that it is not giving you opinions. When a report says a slat gap is too wide, it can say who says so. ```yaml parameters: - id: mattress type: catalog_ref catalog: standards.mattress value: us-queen - id: max_slat_gap type: length derived: standards.bed.slat_gap_max requirements: must_satisfy: - standards.bed.slat_gap_max - standards.bed.centre_rail_required ``` Listing an id under `must_satisfy` turns its check from advisory into a **requirement of this design**. ## Four kinds | Kind | Shape | Read as | | --- | --- | --- | | `dimension_set` | Named sizes | `mattress.width` via a `catalog_ref` parameter | | `limit` | One bound, `max` or `min` | `standards.bed.slat_gap_max` | | `range` | `min`, `max`, `typical` | `standards.table.dining_height.typical` | | `rule` | A requirement in prose, with a condition | Checked, not referenced | --- ## Beds ### `standards.mattress` — mattress sizes `dimension_set`. **Source:** ISPA standard sizes for the US series; EN 1334 and common European trade sizes for the eu- and uk- series. | Value | Width | Length | | --- | --- | --- | | `us-twin` | 991 | 1880 | | `us-twin-xl` | 991 | 2032 | | `us-full` | 1372 | 1880 | | `us-queen` | 1524 | 2032 | | `us-king` | 1930 | 2032 | | `us-cal-king` | 1829 | 2134 | | `uk-single` | 900 | 1900 | | `uk-double` | 1350 | 1900 | | `uk-king` | 1500 | 2000 | | `uk-super-king` | 1800 | 2000 | | `eu-single` | 900 | 2000 | | `eu-double` | 1400 | 2000 | | `eu-queen` | 1600 | 2000 | | `eu-king` | 1800 | 2000 | These are **nominal finished sizes**. Real mattresses vary by ±10 mm or so between makers, which is exactly why `mattress_clearance` should be a parameter with a sane default rather than a number baked into a rail length. ### `standards.bed.slat_gap_max` — maximum clear gap between slats `limit`: **76 mm max**. **Source:** consensus of mattress-maker warranty conditions — Tempur, Sealy, IKEA and others all specify 75–80 mm; 76 mm is the most commonly stated figure and the most conservative in wide use. Foam and hybrid mattresses sag between slats further apart than this, and most warranties are void above it. Sprung mattresses tolerate more, but there is no reason to design to a looser number. This is what an [array count](/docs/schema/arrays#compute-the-count-do-not-count-it) should be derived from, so that changing the mattress size adds slats by itself. ### `standards.bed.slat_thickness_min` — minimum slat thickness in softwood `limit`: **18 mm min**. **Source:** common trade practice for slatted bases at 900–1600 mm spans. ### `standards.bed.centre_rail_required` — centre rail above a threshold width `rule`. Applies when `mattress.width > 1370`. > A bed wider than 1370 mm needs a centre support rail with at least one leg to the floor, so > the slats do not span the full width unsupported. **Source:** mattress warranty conditions for queen and larger sizes; universally required by US makers above 1370 mm width. This is [`DOM-002`](/docs/validation/error-codes#dom-002), and it is one of the four things the validator found wrong with the golden queen bed the first time it ran. ### `standards.bed.deck_height` — comfortable mattress-top height `range`: 450 – 650 mm, typical **560**. **Source:** anthropometric guidance for sit-to-stand transfer; matches the range used in accessible-design guidance for bed height. This is the top of the **mattress**, not the deck. Subtract the mattress thickness to get the slat height the design actually controls — a 300 mm deck under a 260 mm mattress gives 560. ### `standards.bed.mattress_clearance` — clearance per side `range`: 3 – 15 mm, typical **6**. **Source:** trade practice; accounts for maker-to-maker size variation. Too tight and the mattress will not drop in; too loose and it slides and opens a gap at the head. --- ## Tables and desks | Id | Kind | Figures | Source | | --- | --- | --- | --- | | `standards.table.dining_height` | range | 710 – 760, typical 750 | BS EN 1730, BIFMA G1, trade practice | | `standards.table.desk_height` | range | 720 – 750, typical 730 | BS EN 527-1, fixed-height category | | `standards.table.counter_height` | range | 890 – 950, typical 910 | Trade practice — European carcass plus worktop | | `standards.table.place_setting_width` | range | 550 – 700, typical 600 | Trade practice for dining layout | ### `standards.table.knee_clearance` `limit`: **685 mm min** clear height under an apron. **Source:** ADA Standards §306.3 and BS 8300-2, which both require at least 685 mm clear for a seated user. This is the one ergonomic figure that is a **hard accessibility requirement** rather than a comfort range, which is why it is a limit and not a range. A 750 mm dining table with a 90 mm apron leaves 660 and fails it. ### `standards.table.legroom_depth` `limit`: **480 mm min** clear depth under a table. **Source:** ADA Standards §306.3. --- ## Seating | Id | Kind | Figures | Source | | --- | --- | --- | --- | | `standards.seat.height` | range | 400 – 460, typical 450 | BS EN 1729-1 size mark 6 | | `standards.seat.depth` | range | 380 – 450, typical 420 | Buttock-to-popliteal length, 5th percentile | --- ## Shelving and casework ### `standards.shelf.clear_height` `range`: 220 – 400 mm, typical **300**. **Source:** trade practice — paperbacks to large-format art books. ### `standards.shelf.deflection_limit` `limit`: **span/200 max**, expressed as a span divisor. **Source:** woodworking convention — sag becomes visible at roughly span/200 and is widely treated as unacceptable beyond it. Structural practice for floors uses span/360, which is stricter than a shelf needs. A 900 mm shelf may sag 4.5 mm. Use `deflection_limit: span/360` in [`requirements`](/docs/schema/requirements) if the shelf sits next to a fixed reference line where sag shows up. ### `standards.case.toe_kick` `range`: 75 – 100 mm, typical **90**. **Source:** trade practice for standing-height casework. --- ## Errors and warnings The ergonomic ranges drive [`DOM-003`](/docs/validation/error-codes#dom-003), and they are **warnings**. A 780 mm table is unusual, not wrong, and the report's job is to say "this is outside the normal range, and here is the range" rather than refuse to compile it. `standards.table.knee_clearance` is the exception, as noted above — an accessibility requirement, checked as a limit. Anything you list under `must_satisfy` is checked as a requirement of the design regardless of its default severity. --- --- title: "How checks run" description: "What runs when, why a rule that could not run is reported as unverified rather than passed, and what the runner does when a rule throws." url: https://kaicad.barpom.xyz/docs/validation/how-checks-run schema: "0.1" source: packages/validate/src/index.ts --- # How checks run A compile is a pipeline, and validation is not a step at the end of it — findings are raised at every stage and folded into one report. ``` design.yaml │ parse + schema SCH- is this a valid document? ▼ resolve RES- do the parameters, expressions and │ catalogue IDs resolve? ▼ assemble GEO- where does every part actually end up? │ ├──▶ takeoff MFG- can these parts be cut from real stock? │ ├──▶ geometry (OCCT) GEO- do any two solids overlap, and by how much? │ ▼ validate FAS- STR- DOM- PTH- │ ▼ one report ``` ## A rule is one file Every rule is a single pure function in its own file. It never imports another rule, never mutates the design, and never reaches for the CAD kernel unless it declares that it needs one. That is what makes the rule set something several people can extend at once without collisions. A rule that says "fastener too close to edge" has done half the job. Every finding carries: - a **path** — a JSON Pointer into the document *you* wrote, not into some intermediate form - a **suggestion** — computed: "move it to at least 12 mm from the edge" - **detail** — the numbers behind the claim, so the report shows its working ## Severity is the rule's decision The same condition is an error in one design and fine in another. A screw into end grain is a warning in softwood and unremarkable in a bed bolt, because the bolt threads into steel. Rules read the fixture's own constraints rather than hard-coding a severity. ## What did not run is part of the report This is the part worth knowing before you cut anything. - **`rulesPassed`** names every rule that ran and found nothing. That is evidence of coverage, and it is why a clean report is worth something. - **Skipped rules are named, with a reason.** A rule that needs built geometry and did not get it is reported as **unverified**, never as passed. The difference matters more than any single finding. - **A rule that throws does not take the report with it.** The other findings still arrive, and the crash is reported as a finding of its own. ## The codes are permanent The code registry is append-only. A code that has been seen by someone's LLM, or counted in an evaluation run, keeps meaning the same thing forever. Retiring a rule marks its code retired; it never reuses the number. [Every code, grouped by prefix](/docs/validation/error-codes). --- --- title: "Error codes" description: "Every code the pipeline can emit, grouped by prefix, with the explanation written for a model repairing a document rather than a developer reading a stack trace." url: https://kaicad.barpom.xyz/docs/validation/error-codes schema: "0.1" source: packages/spec/src/codes.ts generated: true --- # Error codes Every code kaicad can emit. The registry is **append-only**: a code that has been seen by someone's LLM, or counted in an evaluation run, keeps meaning the same thing forever. Retiring a rule marks its code retired and never reuses the number. The explanations below are written for a model repairing a document. That is a docs decision that lives in code — they are stored beside the registry, in `packages/spec/src/codes.ts`, so the report, the authoring pack and this page cannot disagree. | Prefix | Owns | Codes | | --- | --- | --- | | `SCH-` | Schema and document structure | 6 | | `RES-` | Resolution — parameters, expressions, catalogue lookups | 9 | | `GEO-` | Geometry and assembly | 8 | | `FAS-` | Fasteners and fixtures | 8 | | `MFG-` | Manufacturing and stock | 6 | | `STR-` | Structural | 4 | | `DOM-` | Domain rules and standards | 4 | | `PTH-` | Paths and service voids | 3 | ## SCH — Schema and document structure ### SCH-001 **The document is not valid YAML** · default severity: error The file could not be parsed. Most often an indentation error, or a value containing a colon that needs quoting. ### SCH-002 **The document does not match the schema** · default severity: error A required key is missing, or a value has the wrong type. The path points at the exact location. ### SCH-003 **Unknown schema version** · default severity: error The `schema:` line names a version this build does not know. Get the current authoring pack from the app. ### SCH-004 **The document was written for an older schema** · default severity: warning The design was migrated forward automatically. It will keep compiling, but regenerate it from the current pack when convenient. ### SCH-005 **Units other than millimetres** · default severity: error This format is metric only. Every length is a number of millimetres. ### SCH-006 **Duplicate identifier** · default severity: error Two parts, materials, parameters or joints share an id. Ids must be unique. ## RES — Resolution — parameters, expressions, catalogue lookups ### RES-001 **Expression could not be parsed** · default severity: error The expression language supports arithmetic, comparison, min, max, round, ceil, floor, abs, and references to parameters, materials, parts and catalogue values. Nothing else. ### RES-002 **Reference to something that does not exist** · default severity: error An expression names a parameter, material, part or catalogue entry that is not in the document. ### RES-003 **Circular reference between derived values** · default severity: error Two or more derived parameters depend on each other, so neither can be computed. ### RES-004 **Parameter is outside its declared range** · default severity: error The value given falls outside the `min`/`max` the parameter itself declares. ### RES-005 **Catalogue entry not found** · default severity: error A material, fixture or standard id does not exist in the pinned catalogue. Check the catalogue reference in the authoring pack. ### RES-006 **Feature position is over- or under-specified** · default severity: error Give exactly one of `at`, `center`, or a `from`/`to` pair. Giving several is ambiguous; giving none leaves the feature unplaced. ### RES-007 **Anchor path does not resolve** · default severity: error A mate names a part, instance or anchor that does not exist. Anchor paths look like `side_rail@left:face.start`. ### RES-008 **Expression produced a value of the wrong kind** · default severity: error A length expression returned a string or a boolean, or a count returned a fraction. ### RES-009 **Material narrowed to a stock size that is not made** · default severity: error The thickness or width asked for is not available in that species and class. The suggestion lists the nearest real sizes. ## GEO — Geometry and assembly ### GEO-001 **Parts overlap** · default severity: error Two solids occupy the same space. Usually a mate that needs an offset, or a missing joint. ### GEO-002 **Part is not connected to anything** · default severity: error The part has no joint to any other part, so it would fall on the floor. Every part needs at least one joint, directly or through a group. ### GEO-003 **Feature falls outside the blank** · default severity: error A feature is positioned past the edge of the board it is cut into. ### GEO-004 **Feature removes too much material** · default severity: warning A cut leaves less than a third of the board thickness behind, which is fragile in solid wood. ### GEO-005 **Features intersect each other** · default severity: warning Two cuts in the same part overlap. Sometimes intentional, often a sign of a position error. ### GEO-006 **The assembled piece exceeds its declared envelope** · default severity: error The finished dimensions are larger than `requirements.max_envelope` allows. ### GEO-007 **No ground part** · default severity: error Nothing is held still, so the assembly has no fixed reference. Name one part in `assembly.ground`. ### GEO-008 **Mate leaves the roll undetermined** · default severity: warning The default roll was used because the two anchors do not fix it. Add `align.roll` if the part is rotated the wrong way. ## FAS — Fasteners and fixtures ### FAS-001 **Fastener is too close to an edge** · default severity: error Driving a screw nearer the edge than the fixture allows splits the board. The suggestion gives the minimum distance for this fixture and species. ### FAS-002 **Fastener is too close to an end** · default severity: error End grain splits more readily than edge grain, so the end distance is larger. ### FAS-003 **Insufficient engagement in the receiving part** · default severity: error The screw does not reach far enough into the second board to hold. Either use a longer fastener or a thinner first board. ### FAS-004 **Fastener would break through the far face** · default severity: error The fastener is longer than the material behind it. ### FAS-005 **Fastener driven into end grain** · default severity: warning End-grain screw holding is roughly half of face-grain holding. Consider a dowel, an insert or a different joint. ### FAS-006 **Fasteners are spaced too closely** · default severity: warning Closely spaced fasteners along the grain can split a board. ### FAS-007 **Joint declared removable but uses glue** · default severity: error `requirements.knock_down` is set, but this joint is glued, so the piece cannot be taken apart. ### FAS-008 **No fixture given for a hardware joint** · default severity: error Joint kinds other than `butt` and `glue_only` need a `fixture:`. ## MFG — Manufacturing and stock ### MFG-001 **Part is longer than any available board** · default severity: error No stock length in the catalogue can produce this part. ### MFG-002 **Part is wider than the stock it is cut from** · default severity: error The part needs a glue-up, or a wider board. Say which in the design rather than leaving it implied. ### MFG-003 **Part exceeds the declared maximum part length** · default severity: error `requirements.max_part_length` exists because the piece has to get into the room. This part will not. ### MFG-004 **Feature needs a tool that is not available** · default severity: warning The design uses a cut the declared `requirements.tools` cannot make. The suggestion names an alternative joint. ### MFG-005 **Sheet yield is poor** · default severity: info The parts nest badly on the sheet. A small dimension change may save a sheet. ### MFG-006 **Rounded dimension breaks a clearance that the exact value passed** · default severity: warning The cut list shows a rounded number that would fail a check the model passed. D13: cut to the exact figure, or change the design so the rounded one works. ## STR — Structural ### STR-001 **Deflection exceeds the limit** · default severity: error Under the declared load the member sags more than `requirements.deflection_limit`. The suggestion gives the depth that would satisfy it. ### STR-002 **Shelf sag exceeds the limit** · default severity: warning The shelf span is too long for its material and thickness. Add a support, thicken it, or add a stiffening edge. ### STR-003 **Wood movement is not accommodated** · default severity: warning A wide solid-wood panel is fixed across its width, so seasonal movement will split it or push the joint apart. ### STR-004 **Racking resistance is inadequate** · default severity: warning The frame has no panel, brace or fixed back to resist being pushed sideways. ## DOM — Domain rules and standards ### DOM-001 **Slat gap exceeds the standard maximum** · default severity: error Mattress warranties and support both depend on the gap between slats. The suggestion gives the slat count that closes it. ### DOM-002 **A required standard is not satisfied** · default severity: error The design lists a standard in `requirements.must_satisfy` and does not meet it. ### DOM-003 **Dimension is outside the ergonomic range** · default severity: warning Seat, table and counter heights have narrow comfortable ranges. The suggestion gives the range for this item type. ### DOM-004 **Mattress clearance is too tight or too loose** · default severity: warning Too tight and the mattress will not drop in; too loose and it slides around. ## PTH — Paths and service voids ### PTH-001 **Cable path is not continuous** · default severity: error A declared path has a gap: no grommet, notch or void connects two segments. ### PTH-002 **Service void is unreachable** · default severity: warning There is no access to the void once the piece is assembled. ### PTH-003 **Path is too tight for the declared cable bundle** · default severity: warning The smallest cross-section on the path is smaller than the bundle it must carry. --- --- title: "Reading a report" description: "The report format — stable codes, JSON Pointer paths, computed suggestions, errors before warnings, in an order that makes two runs diffable." url: https://kaicad.barpom.xyz/docs/validation/reading-a-report schema: "0.1" --- # Reading a report ``` KAICAD VALIDATION — walnut-platform-bed-queen — schema 0.1 4 errors, 2 warnings ERROR RES-005 /joints/2/fixture Catalogue entry not found: 'fixtures.wood_screw.5x60'. Nearest valid: fixtures.wood_screw.5x50, fixtures.wood_screw.5x70, fixtures.wood_screw.4x60 ERROR GEO-001 /parts/side_rail side_rail@left intersects head_post@left by 1820 mm³. Both are mated to the same anchor; one offset is probably missing. ERROR FAS-003 /joints/4 fixtures.wood_screw.4x40 into slat_cleat: 20 mm through + 20 mm engagement, but this fixture requires >= 25 mm engagement. Suggestion: fixtures.wood_screw.4x50 gives 30 mm. ERROR DOM-001 /parts/slat Slat gap is 81 mm; standards.bed.slat_gap_max is 76 mm. Increase count to 15 or widen slats to 95 mm. WARN STR-003 /parts/headboard_panel A 420 mm wide solid panel is fixed at both ends; seasonal movement of ±3.8 mm has nowhere to go. Use a slotted screw hole or a floating panel. WARN STR-004 /parts/frame Nothing resists racking along the long axis. A fixed back, a brace or a panel would. ``` ## How to read it **The code** is stable and documented — [every one of them](/docs/validation/error-codes) has an entry written for a model repairing a document, not for a developer reading a stack trace. **The path** is a JSON Pointer into the document you pasted. `/joints/4` is the fifth entry in your `joints:` list. Repairs are therefore targeted: there is no searching for which of twenty screws the finding is about. **The suggestion** is computed wherever one exists. That is the difference between a report you can hand to a model and one you have to think about first. **Errors come before warnings**, and the order is deterministic. Two runs of the same design produce the same report, so you can diff them and see what your repair actually changed. ## Errors and warnings are different kinds of thing An **error** means the design does not compile or is physically wrong: an unknown ID, two parts in the same space, a screw that cannot hold. A **warning** is a judgement call with a number attached. Shelf sag over span/360 is a warning because whether it matters depends on what is going on the shelf. Warnings are for you to decide about — so ask your LLM to raise them with you rather than quietly designing around them. ## What is not in the report Rules that could not run are listed separately, by name, with the reason. A design that compiles without geometry has not been checked for interference, and the report says so rather than implying a clean bill of health. --- --- title: "Repairing from a report" description: "Errors first, warnings are judgement calls, re-emit the whole file, keep meta.id. What to paste back, and what to expect in return." url: https://kaicad.barpom.xyz/docs/validation/repairing-from-a-report schema: "0.1" --- # Repairing from a report The report stands alone on purpose: the model that wrote your design never sees kaicad, so everything it needs to repair the file has to be in the text you paste. ## What to paste back The report, and the document it is about. By the time you are repairing, the chat may well have lost the file from its context, so send both — and say which schema version it is written against. ## What to ask for 1. **Fix the errors.** They are not negotiable — the design does not compile, or it is physically wrong. 2. **Treat warnings as judgement calls and raise them with you.** A deflection warning on a shelf might be fine for photographs and wrong for books. The model should say what the trade-off is rather than silently changing your design. 3. **The whole file back**, one fenced block, same `meta.id`. A diff has to be applied by hand, and a hand-applied diff to a file you did not write is how a design quietly stops matching its report. 4. **No invented fix for a code it does not recognise.** An unknown code means it is working from something older than this build; the right move is to say so. ## A worked example ``` ERROR FAS-003 /joints/4 fixtures.wood_screw.4x40 into slat_cleat: 20 mm through + 20 mm engagement, but this fixture requires >= 25 mm engagement. Suggestion: fixtures.wood_screw.4x50 gives 30 mm. ``` Everything needed for the repair is in those four lines: which joint, which fixture, what the rule requires, what the actual numbers are, and a specific fixture that satisfies it. The model changes one field and re-emits. Compare the version of that finding that most tools would produce — "insufficient screw engagement" — which is a research task, not a repair. ## When a repair does not converge If two rounds do not clear the errors, the usual cause is that the design wants something the v1 vocabulary cannot express — doors, hinges, drawers and runners are v2 — and the model is working around it. Say what you are actually trying to build; a schema gap is worth knowing about and is a better answer than a workaround. --- --- title: "For your LLM" description: "Every page here is published twice — rendered, and as plain Markdown at the same URL with .md appended — plus llms.txt and llms-full.txt for agents that would rather fetch the lot." url: https://kaicad.barpom.xyz/docs/reference/for-your-llm schema: "0.1" --- # For your LLM Half the readership of this site is a language model writing YAML on someone's behalf. So every page is published twice, from one source. | URL | What it is | | --- | --- | | `/docs/…` | The rendered page — navigation, search, table of contents | | `/docs/….md` | The same content as plain Markdown, with front matter and no chrome | | [`/llms.txt`](/llms.txt) | An index: one line per page, pointing at its `.md` twin | | [`/llms-full.txt`](/llms-full.txt) | Every page concatenated, for a model with room for the lot | Add `.md` to any docs URL in this site and you get the Markdown. Every page also carries a **Copy for chat** button that puts exactly that text on your clipboard. ## The front matter is the useful part ```yaml --- title: "How checks run" url: https://example.com/docs/validation/how-checks-run schema: "0.1" source: packages/validate/src/index.ts --- ``` `schema` is what makes a stale copy **detectable** rather than mysterious: if the material your model is working from names an older schema than the page it is reading, that is a fact either of you can act on. `source` names the file in the codebase a page describes — `packages/spec/src/anchors.ts` for the anchors page, `packages/catalogue/data/fixtures/bed-bolt.yaml` for the bed bolts. It is a pointer to the authoritative copy of the numbers on the page, and to the file whose change would make the page stale. `generated: true` is the stronger claim: that page is **written by a script** from its `source` and is never edited by hand, so fixing one means fixing the data. Only the [error-code reference](/docs/validation/error-codes) carries it today. ## If you are an agent reading this The static half of this site is prerendered HTML: it needs no JavaScript, and a plain HTTP `GET` returns the whole page. `/app/` is the opposite — a client-rendered compiler whose HTML is an empty element — so there is nothing there for you, and `robots.txt` says so. The [error-code reference](/docs/validation/error-codes) is generated from the registry the validator itself reads, so a code that exists and a code that is documented cannot diverge. The rest of the reference — [schema](/docs/schema/the-document) and [catalogue](/docs/catalogue/how-the-catalogue-works) — is written by hand, because most of what makes those pages useful is not in the data. The authoritative copy of any number on them is the one the validator reads, and a finding will always carry it. If you want the numbers without the prose, take [`catalogue.json`](/docs/catalogue/how-the-catalogue-works) — the same machine-readable file the authoring pack ships. --- --- title: "Licences" description: "MIT for the app, CC0 for the catalogue data, and what that means for the LGPL wasm kernel underneath." url: https://kaicad.barpom.xyz/docs/reference/licences schema: "0.1" --- # Licences | Part | Licence | | --- | --- | | Application code | **MIT** | | Catalogue data — materials, fixtures, standards | **CC0-1.0** | | `replicad-opencascadejs` (the CAD kernel, as wasm) | LGPL-2.1, loaded unmodified | ## Why the catalogue is CC0 The catalogue is deliberately the most permissive thing here, and that is a decision rather than an oversight. A fixture entry in this format is not a solid. It is a solid, **plus a machining recipe for the parts it joins, plus the dimensions it determines, plus the design rules the validator then enforces**. That combination does not exist anywhere open, and its value is as a *standard* rather than as an asset. The more tools that adopt the format, the better it gets. CC0 removes even attribution friction. ## The discipline behind the data - Encode **published standard dimensions**. Cite the standard on the page. - **Never redistribute vendor CAD files.** Reference part numbers instead. Every generated catalogue page names its source file and its licence in the footer, because saying so on the page is half the point of publishing it. ## The kernel The OCCT kernel is loaded as an unmodified WebAssembly module — the dynamic-linking case under LGPL-2.1 — which imposes no obligation beyond leaving it replaceable. It is not statically linked into the app, and it is not modified.