woodshop/MATERIALS_INVENTORY_PLAN.md

9.5 KiB
Raw Permalink Blame History

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).
  • paintfinish_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_p3ci_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 57 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).