@structupath/pi-steel 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/DATA_PROVENANCE.json +23 -0
  2. package/DATA_PROVENANCE.md +35 -0
  3. package/PUBLIC_DATA_POLICY.md +52 -0
  4. package/README.md +114 -33
  5. package/docs/assets/pi-steel-demo.gif +0 -0
  6. package/docs/assets/pi-steel-gallery.webp +0 -0
  7. package/package.json +35 -4
  8. package/pyproject.toml +28 -0
  9. package/requirements-dev.txt +5 -0
  10. package/requirements-render.txt +1 -0
  11. package/requirements-tested.txt +9 -0
  12. package/requirements.txt +7 -0
  13. package/scripts/check-data-provenance.py +140 -0
  14. package/scripts/check-public-data.py +401 -0
  15. package/scripts/doctor.py +127 -0
  16. package/skills/_shared/bootstrap.py +32 -0
  17. package/skills/_shared/pi_steel/__init__.py +47 -0
  18. package/skills/_shared/pi_steel/cli.py +167 -0
  19. package/skills/_shared/pi_steel/contracts.py +87 -0
  20. package/skills/_shared/pi_steel/geometry_verify.py +243 -0
  21. package/skills/_shared/pi_steel/parsing.py +205 -0
  22. package/skills/_shared/pi_steel/run_manifest.py +317 -0
  23. package/skills/_shared/pi_steel/validation.py +597 -0
  24. package/skills/_shared/schemas/estimate-package.schema.json +424 -0
  25. package/skills/_shared/schemas/nest-result.schema.json +443 -0
  26. package/skills/_shared/schemas/run-manifest.schema.json +145 -0
  27. package/skills/steel-estimate/SKILL.md +119 -0
  28. package/skills/steel-estimate/references/estimate-package-example.json +91 -0
  29. package/skills/steel-estimate/references/output-contract.md +47 -0
  30. package/skills/steel-estimate/scripts/acknowledge-finding.py +193 -0
  31. package/skills/steel-estimate/scripts/build-estimate-package.py +836 -0
  32. package/skills/steel-nest/SKILL.md +50 -23
  33. package/skills/steel-nest/references/FIXTURE_PROVENANCE.md +12 -0
  34. package/skills/steel-nest/references/example_job.json +21 -25
  35. package/skills/steel-nest/references/job_template.json +11 -9
  36. package/skills/steel-nest/scripts/nest.py +1276 -250
  37. package/skills/steel-rfq/SKILL.md +93 -175
  38. package/skills/steel-rfq/assets/company-profile.example.json +11 -5
  39. package/skills/steel-rfq/references/rfq-input.md +58 -0
  40. package/skills/steel-rfq/scripts/generate-rfq.py +1221 -0
  41. package/skills/steel-rfq/scripts/recalc.py +33 -10
  42. package/skills/steel-takeoff/SKILL.md +12 -8
  43. package/skills/steel-takeoff/assets/bom-template.csv +1 -1
  44. package/skills/steel-takeoff/references/takeoff-procedures.md +3 -1
  45. package/skills/steel-takeoff/scripts/calculate-weight.sh +5 -10
  46. package/skills/steel-takeoff/scripts/lookup-member.sh +2 -2
  47. package/skills/steel-takeoff/scripts/validate-bom.py +151 -196
  48. package/skills/steel-rfq/scripts/__pycache__/recalc.cpython-312.pyc +0 -0
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: steel-nest
3
- description: "Nest steel parts onto stock plates and estimate material — the plate-layout / cutting step that CAM software (SigmaNEST, Hypertherm, FANUC) does. Use this skill whenever someone mentions nesting, plate layout, plate optimization, cut list, cutting plan, yield, drop/remnant, how many sheets/plates a job needs, how much plate to buy, or laying parts out on a sheet. Also trigger when a new order/SO comes in and someone asks 'how much material', 'how many plates', 'what's the yield', or 'lay these parts out' — even if they don't say the word 'nest'. Produces a nesting layout (PDF + PNG), a cut list, yield/scrap/remnant numbers, material cost, and a DXF of the nest. Rectangular parts nest exactly; irregular parts nest by bounding box."
3
+ description: "Nest steel parts onto compatible stock plates and estimate material — the plate-layout / cutting step that CAM software does. Use this skill whenever someone mentions nesting, plate layout, plate optimization, cut list, cutting plan, yield, remnant candidates, how many sheets/plates a job needs, how much plate to buy, or laying parts out on a sheet. Produces a verified result, packing utilization, net material yield, guarded layouts, and optional cost totals only when an explicit basis exists."
4
4
  ---
5
5
 
6
6
  # Steel Plate Nesting & Estimate
7
7
 
8
8
  ## What This Skill Does
9
9
 
10
- This is the in-house version of the plate-nesting step a CAM package (SigmaNEST / Hypertherm / FANUC) performs: it takes a list of parts and the plate stock on hand, packs the parts onto as few plates as possible, and tells you the yield, the drops you can reuse, the material weight, and the cost. It also draws the layout and exports a DXF.
10
+ This is an estimating-oriented version of the plate-nesting step performed by CAM software. It validates and separates material/grade/thickness groups, packs them onto compatible stock, independently verifies placements, and reports two different metrics: bounding-footprint packing utilization and net material yield. Leftover rectangles are remnant candidates, not certified reusable stock.
11
11
 
12
- It exists so that the moment an order lands, anyone in the shop can get a fast, repeatable material number for quoting and a layout the cutter can follow without waiting on the CAM seat.
12
+ It exists so an estimator can get a fast, repeatable material number and a reviewable layout. It does not replace CAM setup or operator verification.
13
13
 
14
14
  ## What It Does Well vs. What It Doesn't
15
15
 
@@ -17,18 +17,22 @@ Be honest with the user about the boundary — it protects the shop from over-tr
17
17
 
18
18
  **Reliable:**
19
19
  - Rectangular / plate-blank parts nest **exactly** (MaxRects bin-packing with rotation).
20
- - Multiple plate sizes, kerf + gap spacing, edge margin (grip/clamp keep-out).
21
- - **Holes and rectangular cutouts** on any part subtracted from weight/cost, rotated with the part, and cut as real geometry in the output.
22
- - Yield %, scrap weight, largest reusable **drop** per plate.
23
- - Material weight and cost (by $/lb which also values scrap — or by $/sheet).
20
+ - Multiple compatible plate sizes with independently verified bounds, non-overlap, and material grouping.
21
+ - Explicit clearance ownership: edge margin is the plate-to-part keep-out; kerf plus part gap is the minimum edge-to-edge clearance between parts. No trailing kerf/gap is required at the usable plate boundary.
22
+ - **Holes and rectangular cutouts** on verified rectangular parts — subtracted from weight/cost, rotated with the part, and emitted as cut geometry only when the complete job passes the output gate.
23
+ - Separate packing-utilization and net-material-yield percentages with approximation labels.
24
+ - Remnant candidates per plate, explicitly not certified reusable drops.
25
+ - Material weight and optional cost by one explicit basis per stock entry (`cost_per_lb` or `cost_per_sheet`, never both).
24
26
  - Labeled layout (PDF + one PNG per plate).
25
- - **Burn-table files**: one DXF **per sheet** (`burn_plate_N.dxf`) with part outlines on layer `PROFILE` and holes on layer `HOLES`, origin at the sheet corner ready to import into the table's CAM. Plus `nest.dxf`, an all-sheets overview.
27
+ - **Reference files**: `reference_nest.dxf` plus `reference_plate_N.dxf`, with sheet outlines, bounding boxes, holes, and labels for estimating review.
28
+ - **Guarded cut-geometry files**: one DXF per sheet (`burn_plate_N.dxf`) containing only closed part outlines on `PROFILE` and holes/cutouts on `HOLES`, with origin at the sheet corner. They exist only when every part is rectangular, every required part fits, and every supported hole stays inside its part.
26
29
 
27
30
  **Approximate — always flag it:**
28
31
  - **Irregular parts** (gussets, brackets, curved profiles, parts with holes) are nested by their **bounding box**, not true shape. Real yield is a little better than reported. For exact weight/cost on those, get the true cut area (in²) into the part's `area` field. This is NOT true-shape nesting like a dedicated CAM engine.
32
+ - Any irregular part suppresses all fabrication-style DXFs for that job. The remaining PDF, PNG, report, JSON, and `reference_nest.dxf` outputs are estimating aids, not cutting instructions.
29
33
 
30
34
  **Do NOT pretend to do:**
31
- - **Machine-ready G-code / NC** with kerf compensation, pierce points, and lead-ins for a specific controller. That is machine-specific and safety-critical and must come from the real post-processor. The burn DXF from this skill is a geometry/import file — the table's CAM (Hypertherm ProNest, FastCAM, SigmaNEST, Lantek, or the controller's own importer) applies kerf comp, lead-ins and pierce. Say so plainly if asked for G-code; offer the DXF as the correct hand-off.
35
+ - **Machine-ready G-code / NC** with kerf compensation, pierce points, and lead-ins for a specific controller. That is machine-specific and safety-critical and must come from the real post-processor. When a burn DXF is emitted, it is still an import geometry file that requires operator and CAM verification. Say so plainly if asked for G-code.
32
36
 
33
37
  ## Inputs to Gather
34
38
 
@@ -36,8 +40,8 @@ Everything drives a single job JSON (schema in `references/job_template.json`; a
36
40
 
37
41
  Gather three things:
38
42
 
39
- 1. **Parts** — for each unique part: name, width × height (inches; use the bounding box for odd shapes), quantity, whether it's `rect` or `irregular`, and whether rotation is allowed (`rotatable: false` locks grain/rolling direction for anisotropic material or directional finish). If a part has **holes or cutouts** and you want them cut in the burn file (and netted out of weight), add a `holes` list — each hole's `x,y` is its center from the part's lower-left corner: round = `{"dia":, "x":, "y":}`, rectangular cutout = `{"w":, "h":, "x":, "y":}`. Holes are optional; skip them if you only need the layout/estimate.
40
- 2. **Stock** — plate size(s) on hand (width × height × thickness), how many sheets are available (or `unlimited` to buy as needed), and price (`cost_per_lb` preferred; `cost_per_sheet` works too).
43
+ 1. **Parts** — for each unique part: name, width × height (inches; use the bounding box for odd shapes), quantity, whether it's `rect` or `irregular`, and whether rotation is allowed (`rotatable: false` locks grain/rolling direction for anisotropic material or directional finish). If a rectangular part has **holes or cutouts**, add a `holes` list — each hole's `x,y` is its center from the part's lower-left corner: round = `{"dia":, "x":, "y":}`, rectangular cutout = `{"w":, "h":, "x":, "y":}`. A supported hole must remain fully inside its part or fabrication-style DXFs are suppressed. Holes are optional; skip them if you only need the layout/estimate.
44
+ 2. **Stock** — plate size(s), explicit material, grade, and thickness, plus finite quantity or `unlimited`. A price is optional; if provided, use exactly one approved basis (`cost_per_lb` or `cost_per_sheet`) and retain its source outside this legacy JSON boundary.
41
45
  3. **Cut settings** — kerf, part gap, edge margin, material density. Sensible defaults are in the template; only ask if the user hasn't implied them. Common kerf: plasma ~0.06", oxy-fuel ~0.10", laser ~0.02", waterjet ~0.03".
42
46
 
43
47
  If a spreadsheet is provided, read it with pandas first, map columns to the part fields, and confirm your interpretation before nesting. Do not silently guess quantities or dimensions.
@@ -55,33 +59,56 @@ Write the job JSON, then run the engine:
55
59
  python3 scripts/nest.py --job <job.json> --out <outdir>
56
60
  ```
57
61
 
58
- Outputs land in `<outdir>/`:
62
+ The legacy job JSON shape and command arguments remain accepted. Material, grade,
63
+ thickness, and imperial unit basis must now be explicit at job level or on each
64
+ part/stock entry; the engine never infers those facts from a display name. `<outdir>` is a
65
+ publication root rather than a flat artifact directory. Every invocation creates
66
+ `<outdir>/runs/<run-id>/` and atomically updates `<outdir>/latest-run.json`; follow
67
+ that pointer to find the current run. This prevents an older burn file from appearing
68
+ current after a blocked rerun.
69
+
70
+ Use `--geometry-verified-only` when reference-only geometry does not satisfy the
71
+ request. The command still publishes its QA diagnostics, but exits unsuccessfully.
72
+
73
+ Each run contains:
74
+
75
+ - `run-manifest.json` and `qa-report.json` — outcome, readiness, hashes, warnings, and the exact artifact allow-list
59
76
  - `layout.pdf` — every plate drawn (holes shown) + a summary page (the main deliverable)
60
77
  - `plate_1.png`, `plate_2.png`, … — one image per plate
61
- - `burn_plate_1.dxf`, `burn_plate_2.dxf`, … — **one DXF per sheet for the burn table** (PROFILE + HOLES layers, origin at sheet corner)
62
- - `nest.dxf` — all sheets side-by-side, one overview file
63
- - `rfq_nesting.json` — the Material / Nesting Plan / Drop Notes block for the `steel-rfq` hand-off (see below)
78
+ - `burn_plate_1.dxf`, `burn_plate_2.dxf`, … — geometry-verified cut entities only; absent for review-required or blocked runs
79
+ - `reference_nest.dxf` and `reference_plate_N.dxf` explicitly reference-only layouts
80
+ - `rfq_nesting.json` — versioned `1.0.0` Material / Nesting Plan / Remnant Candidate rows for the `steel-rfq` hand-off; absent on blocked runs
64
81
  - `report.txt` — the text report
65
- - `result.json` — structured result (plates, placements, holes, yield, cost) for downstream use
82
+ - `result.json` — schema-versioned result with normalized input/configuration hashes, algorithm version, placements, verifier findings, both metrics, and cost status
83
+
84
+ Exit meanings:
85
+
86
+ - `0` — `ready`; requested artifacts were published and cut geometry is verified within the supported rectangular scope
87
+ - `2` — `review_required`; safe reference outputs were published, but no burn DXF exists
88
+ - `3` — `blocked`; validation or unplaced material prevented fabrication-style output
89
+ - `4` — a required runtime capability is missing
90
+ - `1` — usage or internal error
66
91
 
67
- The engine has no third-party build dependencies beyond `ezdxf`, `matplotlib`, and `numpy`. Install once if missing: `pip install --break-system-packages ezdxf matplotlib numpy`.
92
+ Install the declared dependencies from the package root with
93
+ `python3 -m pip install -r requirements-dev.txt`. Formula baking remains an optional
94
+ capability reported by `python3 scripts/doctor.py`.
68
95
 
69
96
  ## What to Deliver
70
97
 
71
- Always deliver the **PDF layout** and give the headline numbers in the message: plates used, overall yield %, total material cost, and any parts that **did not fit** (the report flags these — never hide them; it means they need more or bigger stock). Offer the DXF and per-plate PNGs. If the parts were irregular, restate the bounding-box caveat so the quote isn't over-trusted.
98
+ Always deliver the **PDF layout** and give the headline numbers in the message: plates used, packing utilization, net material yield, cost status, and aggregated quantities that did not fit. Offer the reference DXF and per-plate PNGs. Offer burn DXFs only when they were emitted by the guard, and still state that CAM/operator verification is required.
72
99
 
73
- Verify before presenting: the engine already checks that no parts overlap and all fit in-bounds, but sanity-check the yield and cost against the plate count (e.g., cost = plates × sheet cost, or plate weight × $/lb). If a plate shows very low yield, mention it — it's usually the tail plate and may be worth holding parts for the next order.
100
+ Verify before presenting: require `verification.status = verified`, reconcile known cost to its recorded per-sheet or per-pound basis, and never turn a missing or incomplete cost into `$0`.
74
101
 
75
102
  ## Integration with steel-rfq
76
103
 
77
- The `steel-rfq` skill has a "Nesting / Drop Reference" table. This engine writes exactly that data to `rfq_nesting.json` on every run one row per plate material with `material`, `nesting_plan`, `drop_notes`, plus `sheets_needed` (for cross-checking the estimate's assumed sheet count) and `total_cost`. When an RFQ involves plate, `steel-rfq` builds a nest job from the plate parts, runs this engine, and reads `rfq_nesting.json` straight into its table. Keep this JSON shape stable — the RFQ skill depends on those field names.
104
+ The `steel-rfq` skill has a "Nesting / Drop Reference" table. The engine writes the versioned object `{schema_version, source_nest_result_version, rows}`. Rows remain separate by stock identity, material, grade, thickness, and sheet size, even when display names match. Resolve the current run through `latest-run.json` and reject unknown handoff versions.
78
105
 
79
106
  ## Common Variations
80
107
 
81
- **Mixed thickness / grade in one order** — nest each thickness as its own job (parts of different thickness can't share a plate). Run the engine once per thickness and combine the numbers in the summary.
108
+ **Mixed thickness / grade in one order** — provide the basis on each part and stock entry. The engine creates independent compatible groups and never opens an incompatible plate.
82
109
 
83
- **"Just tell me how many sheets"** — still run it; the plate count is the answer, and you get the yield and cost for free. Don't estimate sheet count by dividing areas packing loss makes that wrong.
110
+ **"Just tell me how many sheets"** — still run it; the plate count is the answer. Cost remains absent unless an explicit basis is supplied.
84
111
 
85
- **Remnant/drop reuse** — add the leftover drop from a previous job as another `stock` entry (its size, `qty: 1`) so the engine tries to consume it first-ish. Note: with multiple stock sizes the engine fills them in the order listed, so list drops/offcuts first if you want them used up.
112
+ **Remnant reuse** — output rectangles are candidates only. Measure, identify, status, and approve a candidate as inventory before supplying it as a distinct stock entry in a later estimate.
86
113
 
87
114
  **Grain / directional material** — set `rotatable: false` on those parts so the nester won't spin them 90°.
@@ -0,0 +1,12 @@
1
+ # Fixture Provenance
2
+
3
+ `example_job.json` and `job_template.json` are synthetic public examples created
4
+ from scratch for pi-steel documentation and tests. They are not copied, transformed,
5
+ rounded, renamed, or anonymized from a company, customer, vendor, bid, drawing,
6
+ takeoff, inventory record, or production job.
7
+
8
+ - Creator: StructuPath pi-steel maintainers
9
+ - Creation method: deliberately invented geometry and identifiers
10
+ - Public-data review: 2026-07-28
11
+ - Commercial data: none
12
+ - Private source artifacts: none
@@ -1,6 +1,9 @@
1
1
  {
2
- "job_name": "SO-4471 Conveyor Frame Brackets",
3
- "customer": "Rocky Mtn Aggregate",
2
+ "job_name": "SYNTHETIC-DEMO-001 Generated Training Geometry",
3
+ "customer": "Example Customer",
4
+ "material": "carbon_steel",
5
+ "grade": "A36",
6
+ "unit_system": "imperial",
4
7
  "settings": {
5
8
  "kerf_in": 0.06,
6
9
  "part_gap_in": 0.25,
@@ -10,38 +13,31 @@
10
13
  },
11
14
  "stock": [
12
15
  {
13
- "name": "A36 Plate 1/2\"",
14
- "width": 96,
15
- "height": 48,
16
+ "name": "Synthetic Plate",
17
+ "width": 60,
18
+ "height": 30,
16
19
  "thickness": 0.5,
17
- "qty": 6,
18
- "cost_per_lb": 0.92
20
+ "qty": 3
19
21
  }
20
22
  ],
21
23
  "parts": [
22
- { "name": "Base Plate BP1", "width": 14, "height": 10, "qty": 6, "shape": "rect",
24
+ { "source_id": "SYNTHETIC-SRC-RECT-A", "name": "Training Rect A", "width": 11, "height": 7, "qty": 3, "shape": "rect",
23
25
  "holes": [
24
- { "dia": 0.875, "x": 2, "y": 2 },
25
- { "dia": 0.875, "x": 12, "y": 2 },
26
- { "dia": 0.875, "x": 2, "y": 8 },
27
- { "dia": 0.875, "x": 12, "y": 8 }
26
+ { "dia": 0.75, "x": 2, "y": 2 },
27
+ { "dia": 0.75, "x": 9, "y": 5 }
28
28
  ] },
29
- { "name": "Column Cap CC1", "width": 16, "height": 16, "qty": 4, "shape": "rect",
29
+ { "source_id": "SYNTHETIC-SRC-RECT-B", "name": "Training Rect B", "width": 13, "height": 9, "qty": 2, "shape": "rect",
30
30
  "holes": [
31
- { "dia": 1.0625, "x": 3, "y": 3 },
32
- { "dia": 1.0625, "x": 13, "y": 3 },
33
- { "dia": 1.0625, "x": 3, "y": 13 },
34
- { "dia": 1.0625, "x": 13, "y": 13 }
31
+ { "dia": 0.625, "x": 3, "y": 3 },
32
+ { "dia": 0.625, "x": 10, "y": 6 }
35
33
  ] },
36
- { "name": "Stiffener ST1", "width": 9, "height": 6, "qty": 20, "shape": "rect" },
37
- { "name": "Gusset G1", "width": 12, "height": 12, "qty": 12, "shape": "irregular", "area": 72 },
38
- { "name": "Flat Bar FB1", "width": 30, "height": 4, "qty": 10, "shape": "rect" },
39
- { "name": "Cover CV1", "width": 24, "height": 18, "qty": 3, "shape": "rect",
34
+ { "source_id": "SYNTHETIC-SRC-IRREGULAR-C", "name": "Training Irregular C", "width": 9, "height": 8, "qty": 4, "shape": "irregular", "area": 28 },
35
+ { "source_id": "SYNTHETIC-SRC-RECT-D", "name": "Training Rect D", "width": 17, "height": 5, "qty": 5, "shape": "rect" },
36
+ { "source_id": "SYNTHETIC-SRC-RECT-E", "name": "Training Rect E", "width": 19, "height": 11, "qty": 2, "shape": "rect",
40
37
  "holes": [
41
- { "w": 6, "h": 4, "x": 12, "y": 9 }
38
+ { "w": 5, "h": 3, "x": 9.5, "y": 5.5 }
42
39
  ] },
43
- { "name": "Clip CL1", "width": 5, "height": 3, "qty": 24, "shape": "rect",
44
- "holes": [ { "dia": 0.5625, "x": 2.5, "y": 1.5 } ] },
45
- { "name": "Web Plate WP1", "width": 40, "height": 12, "qty": 4, "shape": "rect" }
40
+ { "source_id": "SYNTHETIC-SRC-RECT-F", "name": "Training Rect F", "width": 6, "height": 4, "qty": 7, "shape": "rect",
41
+ "holes": [ { "dia": 0.5, "x": 3, "y": 2 } ] }
46
42
  ]
47
43
  }
@@ -1,8 +1,11 @@
1
1
  {
2
- "job_name": "SO-XXXX Short description",
3
- "customer": "Customer name",
2
+ "job_name": "SYNTHETIC-DEMO-XXX Short description",
3
+ "customer": "Example Customer",
4
+ "material": "carbon_steel",
5
+ "grade": "A36",
6
+ "unit_system": "imperial",
4
7
 
5
- "_comment_settings": "Cut/gap in inches. kerf = torch cut width (plasma ~0.06, oxy ~0.1, laser ~0.02). part_gap = clearance between parts on top of kerf. edge_margin = keep-out from plate edge (clamp/grip zone). density lb/in^3: A36 steel = 0.2836, aluminum = 0.098, stainless 304 = 0.289.",
8
+ "_comment_settings": "Cut/gap in inches. kerf + part_gap is the minimum edge-to-edge clearance between parts. edge_margin alone is the plate-edge keep-out; no trailing kerf/gap is required at that boundary. density lb/in^3 must match the explicit material basis.",
6
9
  "settings": {
7
10
  "kerf_in": 0.06,
8
11
  "part_gap_in": 0.25,
@@ -11,28 +14,27 @@
11
14
  "density_lb_in3": 0.2836
12
15
  },
13
16
 
14
- "_comment_stock": "One entry per plate size you have on hand. qty = how many sheets available (or set \"unlimited\": true to buy as many as needed). Price EITHER by cost_per_lb OR cost_per_sheet cost_per_lb is preferred because it also values the scrap.",
17
+ "_comment_stock": "One entry per distinct plate size/material/grade/thickness. qty = finite sheets available or set unlimited=true. Add either cost_per_lb or cost_per_sheet from an approved project input, never both; public templates intentionally contain no pricing.",
15
18
  "stock": [
16
19
  {
17
20
  "name": "A36 Plate 1/2\"",
18
21
  "width": 96,
19
22
  "height": 48,
20
23
  "thickness": 0.5,
21
- "qty": 4,
22
- "cost_per_lb": 0.92
24
+ "qty": 4
23
25
  }
24
26
  ],
25
27
 
26
28
  "_comment_parts": "One entry per unique part. width/height in inches (bounding box for irregular parts). qty = pieces needed. rotatable=false to lock grain/rolling direction. shape: \"rect\" nests exactly; \"irregular\" nests by bounding box — add \"area\" (true cut area in^2) for exact weight/cost on those.",
27
29
  "_comment_holes": "Optional per-part \"holes\": each hole's x,y is its CENTER measured from the part's lower-left corner. Round hole = {\"dia\": D, \"x\":, \"y\":}. Rectangular cutout = {\"w\":, \"h\":, \"x\":, \"y\":}. Holes are subtracted from weight/cost, rotate with the part, and are cut as real geometry on the HOLES layer of the burn-table DXF.",
28
30
  "parts": [
29
- { "name": "Part A", "width": 12, "height": 8, "qty": 6, "shape": "rect",
31
+ { "source_id": "SYNTHETIC-SRC-PART-A", "name": "Part A", "width": 12, "height": 8, "qty": 6, "shape": "rect",
30
32
  "holes": [
31
33
  { "dia": 0.875, "x": 2, "y": 2 },
32
34
  { "dia": 0.875, "x": 10, "y": 6 }
33
35
  ] },
34
- { "name": "Part B", "width": 10, "height": 10, "qty": 4, "shape": "irregular", "area": 62 },
35
- { "name": "Part C", "width": 24, "height": 3, "qty": 10, "shape": "rect", "rotatable": false,
36
+ { "source_id": "SYNTHETIC-SRC-PART-B", "name": "Part B", "width": 10, "height": 10, "qty": 4, "shape": "irregular", "area": 62 },
37
+ { "source_id": "SYNTHETIC-SRC-PART-C", "name": "Part C", "width": 24, "height": 3, "qty": 10, "shape": "rect", "rotatable": false,
36
38
  "holes": [ { "w": 3, "h": 1, "x": 12, "y": 1.5 } ] }
37
39
  ]
38
40
  }