198 lines
9.1 KiB
Markdown
198 lines
9.1 KiB
Markdown
# Materials, Finish, Batch Builds & Shop Inventory — Plan (v2, reconciled)
|
||
|
||
Design plan for four related features, reconciled after a Codex review.
|
||
Nothing built yet. v2 changes vs v1 are marked **[v2]**.
|
||
|
||
## 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; price key designed to extend to (stock,
|
||
material, grade).
|
||
- **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).
|