# Materials, Finish, Batch Builds & Shop Inventory — Plan (v2, reconciled) Design plan for four related features, reconciled after a Codex review. **STATUS: all 7 phases implemented (193 tests).** v2 changes vs v1 are marked **[v2]**. Settled the two trailing questions: offcut reuse is **opt-in** (a toggle), and recording a purchase **opt-in** saves prices to the price book. ## What changed in v2 (after Codex review) - **[v2] Sanding never shrinks the design model.** Part dims = final/intended. Sanding becomes a *manufacturing allowance* in the CutPlan (rough vs final), not a scene mutation. (Codex #1.) - **[v2] Rob's stock-reality refinement:** the allowance applies to dimensions we actually **cut** — length always, width only for rips/sheet goods. Dimensional lumber's fixed section (a 2x4 is 1.5×3.5 as delivered) is NOT padded. Final dims remain the truth for fit in v1. - **[v2] Finish is first-class** (`material` + `finish` enum + `finish_color`), layered: stock shape → material/species → finish → visual → cost. A painted pine board is still pine for buying/cutting. (Codex #2, #9.) Single enum now, sheen later (simpler than Codex's kind+sheen split). - **[v2] Inventory is an append-only event ledger**; current state is derived. (Codex #5.) Kept deliberately lean — minimal events, no undo/branching, raw events never shown to the user. - **[v2] Offcuts are real stock pieces** behind one `AvailableStock` interface the planner consumes (reusing the seeded-packing from lock-aware reopt). (Codex #6.) - **[v2] Sequence reordered** per Codex: materials/finish first; allowance and batch as CutPlan features; inventory last (model → workflows → window). ## Guiding principles - Deterministic math, AI only for narrative. Easy, intuitive, useful — no complexity for its own sake. - Additive, backward-compatible serialization (loader filters by known fields). - Physical state (stock, offcuts, builds) is **shop-wide**, in the data dir. - Reuse existing machinery (`_pack_lumber_seeded`, `_pack_plywood_seeded`, `_free_rects_sheet`). ## Data locations - Scenes: `$XDG_DATA_HOME/woodshop/` (per project). - Prices + estimate rates: `$XDG_CONFIG_HOME/woodshop/{prices,estimate}.json`. - **New** ledger: `$XDG_DATA_HOME/woodshop/inventory.json` (shop-wide, append-only). --- ## Phase 1 — Material + finish fields + color resolver **Goal:** parts carry a material and a finish; the viewer colors by them; sanded raw wood reads lighter, painted boards show their color. Flat colors v1 (PyVista textures are a future option). **Data model (additive on `Part`):** - `material: str = ""` — species/sheet: spruce, pine, oak, walnut, maple, birch, mdf, spruce-ply. Default derived from stock (`lumber.py`). - `finish: str = "raw"` — one of `raw | sanded | clear | stain | paint`. - `finish_color: str = ""` — hex, for paint/stain. - **[v2] Migration:** old scenes store `finishes: ["sanded"]`; `from_dict` maps a non-empty list → `finish="sanded"` (or "paint" if a color is present). **Color resolver (viewer), layered stock→material→finish→visual:** - base = `MATERIAL_COLORS[material]` if set else positional palette (fallback). - `paint` → `finish_color`; `stain` → blend base toward `finish_color`/darker; `clear` → base slightly richer; `sanded` → base ~15% lighter; `raw` → base. - **Per-part subtle tint** (±~5% lightness keyed by `_stable_hash(id)`) so same-material boards stay distinguishable. Selection highlight unchanged. **Scene ops / UI:** `scene.set_material(ref, m)`, `scene.set_finish(ref, kind, color="")`. GUI: Paint button (QColorDialog) + Material & Finish dropdowns in the inspector; works on selection/group. CLI/voice: `wood-material`, `wood-finish`/ `wood-paint`; voice color words → hex via a small name table. **Tests:** resolver priority; sanded lightening; tint determinism; finish/material persist + migrate; group ops. --- ## Phase 2 — Finish costs in the estimate **Goal:** finish/paint cost folds into the project estimate by **finish kind × surface area**. **Design:** `EstimateRates` gains per-kind $/sq ft (`sanded` = abrasives only, `clear`, `stain`, `paint`). `project_estimate` adds a finish line per part using its `finish` + finished surface area. Labor already has a sanding/finishing line; extend its time to vary by finish kind (paint/stain take longer) — editable. **Tests:** paint vs clear vs raw produce expected finish $; editable rates flow through; zero for raw. --- ## Phase 3 — Manufacturing allowance in the CutPlan (rough vs final) **Goal:** the cut plan distinguishes the size you **cut** from the **final** size, so sanded/finished parts are cut slightly oversize where applicable. **Design:** - `CutItem` gains `final_length_in`, `final_width_in`; existing `length_in`/ `width_in` become the **rough cut** size. - For a part whose `finish != raw`: `rough_length = final + sanding_allowance`; `rough_width = final + sanding_allowance` **only** for sheet goods / ripped widths. **[v2]** Dimensional lumber at full nominal width: rough_width = stock width (no add); fixed section thickness unchanged. - `ShopSettings.sanding_allowance_in` (already exists) default `1/32"`, editable. - Cut list shows both: `Cut 24 1/16" × 3 9/16" → final 24" × 3 1/2"`. - Nesting/packing uses the **rough** size (that's what you cut from stock). **Tests:** finished part's rough length = final + allowance; lumber width not padded; raw parts unchanged; cut list shows rough→final; packing uses rough. **Open Q (deferred):** compensating joinery fit for material lost to sanding (e.g. tenon thickness). v1 keeps final dims as the fit truth. Note as future. --- ## Phase 4 — Batch builds (quantity N) **Goal:** estimate N identical units, nesting all units together so offcuts carry across units → real per-unit cost. **Design:** - `build_cut_plan(..., quantity=1)` **[v2 confirmed]**: replicate **CutItems**, not Parts — `ci_p3` → `ci_p3_u1, ci_p3_u2, …`. Label placements with unit # ("Unit 2 – left leg") in the layout. - `project_estimate(..., quantity=N)`: materials from the N-unit plan; **setup labor once per batch**, per-operation time and consumables ×N. Report **total + per-unit** (total / N). - UI: a quantity spinner in the BOM header; every tab reflects it. **Tests:** qty=2 uses ≤ 2× sticks (often fewer via shared offcuts); per-unit = total/2; consumables/labor scale; setup once. --- ## Phase 5 — Inventory ledger model (event-sourced) **Goal:** the source of truth for shop stock/offcuts/builds. **Design [v2]:** append-only event log; derive current state (cache totals for speed). Lean event set: - `purchase {stock, qty, unit, price?, date}` - `consume {stock, qty, build_id}` - `create_offcut {offcut, build_id}` - `discard {offcut_id, fate: burned|trashed}` - `adjustment {…, reason}` (manual correction) - `build_recorded {project, units, cost_snapshot, date}` **`AvailableStock` interface [v2]:** standard stock and offcuts share one shape — `{stock, material, length_in, width_in, is_sheet, source, bin?, usable, reserved/consumed}`. The planner consumes both through this interface. **Tests:** fold events → correct on-hand & offcut bin; cache matches fold; adjustments apply; derived stats (units built, $ spent, waste split) correct. --- ## Phase 6 — Inventory workflows (the UX that matters) **Workflow-first [v2], not spreadsheet-first:** 1. **Mark Purchased** — Shopping tab → "Add these to shop inventory?" → `purchase` events. Optionally record price paid. 2. **Record Build** — confirmation FIRST: shows *Consumed* (e.g. 2 × 2x4 8') and *Offcuts created* (18", 26", 12×24 ply); each offcut → **Keep / Trash / Burn / Ignore**. On confirm: emit consume/create_offcut/discard/build_recorded. 3. **Use Shop Inventory** toggle in BOM — "Use available offcuts first" → planner seeds from `AvailableStock` (reuses seeded-packing). **Tests:** purchase adds on-hand; record-build deducts + creates offcuts + logs; dispositions recorded; planner-with-offcuts uses ≤ as many new sticks. --- ## Phase 7 — Inventory window + stats (management view) A top-level **Inventory** window (menu: *Shop → Inventory*) as a management/review view, not the primary workflow. Tabs: On-hand, Offcut bin, Build history, Stats (units built per project, $ spent, material used, waste % kept/burned/trashed). Shop-wide. --- ## Locked sequence 1. Material + finish fields + color resolver 2. Finish costs in the estimate 3. Manufacturing allowance in CutPlan (rough vs final) — with the lumber-section caveat 4. Batch quantity in CutPlan 5. Inventory ledger model (event-sourced) 6. Purchase / Record-build / Use-offcuts workflows 7. Inventory window + stats Each phase committed separately; phases 5–7 are the big block, built model-first. ## Settled decisions (formerly open questions) - **Sanding:** rough-vs-final allowance on cut dims only; design dims stay final; lumber section not padded. (Rob + Codex.) - **Finish:** single `finish` enum + color now; sheen later. - **Pricing:** ~~stock-based in v1~~ **now material-aware** — the planner groups stock by (stock, species) so each bought piece is one material, and the price book applies a per-species multiplier (SPF 1.0, oak 3.0, walnut 5.5, …), editable in the price dialog. (B1 resolved.) - **Batch labor:** setup once per batch, per-op ×N. - **Inventory:** event-sourced source of truth; workflow-first UX; window second. - **Still genuinely open:** auto-update price book from recorded purchase prices (opt-in?); offcut reuse default-on vs opt-in (leaning opt-in toggle).