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