9.5 KiB
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+finishenum +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
AvailableStockinterface 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 ofraw | sanded | clear | stain | paint.finish_color: str = ""— hex, for paint/stain.- [v2] Migration: old scenes store
finishes: ["sanded"];from_dictmaps 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 towardfinish_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:
CutItemgainsfinal_length_in,final_width_in; existinglength_in/width_inbecome the rough cut size.- For a part whose
finish != raw:rough_length = final + sanding_allowance;rough_width = final + sanding_allowanceonly 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) default1/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:
- Mark Purchased — Shopping tab → "Add these to shop inventory?" →
purchaseevents. Optionally record price paid. - 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.
- 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
- Material + finish fields + color resolver
- Finish costs in the estimate
- Manufacturing allowance in CutPlan (rough vs final) — with the lumber-section caveat
- Batch quantity in CutPlan
- Inventory ledger model (event-sourced)
- Purchase / Record-build / Use-offcuts workflows
- 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
finishenum + color now; sheen later. - Pricing:
stock-based in v1now 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).