Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9883e79
feat(cutlist): add linear cut-list optimization skill
claude Aug 23, 2026
2f6fdf7
feat(pipeline): run member cut-lists through estimate and RFQ
claude Aug 23, 2026
67224f2
docs(release): document cut-list skill and cut v0.3.0
claude Aug 23, 2026
59ef99d
fix(cutlist): resolve review findings on correctness and reuse
claude Aug 23, 2026
568c95a
feat(contracts): model linear stock in the canonical estimate
claude Aug 23, 2026
a16e93c
fix(pipeline): suppress verified cutting list on non-ready runs
claude Aug 23, 2026
81eedaf
fix(cutlist): address review findings on stock input strictness
claude Aug 23, 2026
b479317
Merge squash-merged main (PR #6) back into working branch
claude Aug 23, 2026
bb56f57
feat(cutlist): bounded exact search and cost-first ranking
claude Aug 23, 2026
164b69c
feat(cutlist): exact search explores optimal partial plans
claude Aug 23, 2026
0012cb6
test(cutlist): cover review's partial-plan case
claude Aug 23, 2026
86c4a0b
Merge squash-merged main (PR #7) back into working branch
claude Aug 23, 2026
8f8e998
feat(nest): true polygon outlines for irregular parts
claude Aug 23, 2026
82e767d
fix(outline): reject self-touching rings, distinct fallback identities
claude Aug 23, 2026
72d1f1b
Merge squash-merged main (PR #8) back into working branch
claude Aug 23, 2026
cd399dd
docs(plans): true-shape compaction plan for v0.5
claude Aug 23, 2026
09b2aa5
feat(geometry): polygon clearance primitives for true-shape work
claude Aug 23, 2026
e082941
docs(plans): scan-to-first-contact slide, not binary search
claude Aug 23, 2026
1a4b2af
Merge origin/main after PR #9 squash merge
claude Aug 24, 2026
010b256
v0.5.0: verified true-shape compaction for outlined irregular parts
claude Aug 24, 2026
318c961
Point README at package.json for the release version
claude Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ All notable changes to `@structupath/pi-steel` are documented here. The format
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.0] - 2026-08-24

### Added

- Verified true-shape compaction for outlined irregular parts: after
MaxRects bounding-box packing, placements slide left-then-down in
fixed 1/32-in scan-to-first-contact steps until their true profiles
(not their boxes) reach the kerf-plus-gap clearance, so complementary
profiles interlock and recover plate. Every compacted plate is
re-checked by an independent polygon-clearance verifier that rebuilds
profiles from the published placement data alone; any failure discards
compaction for that plate with a non-blocking `COMPACTION_REJECTED`
finding and keeps the proven bounding-box layout. Plate reports record
`compaction` (`ran`, `accepted`, `recovered_in`, `passes`), remnant
candidates are rebuilt from the compacted bounding boxes (still
rectangle-based and uncertified), and a `true_shape_utilization_pct`
metric reports exact profile area on plates whose irregular parts all
carry validated outlines. Enabled by default in the direct engine and
the estimate pipeline; `--no-compact-outlines` opts out, and the choice
is captured in the configuration hash. Burn-DXF eligibility and the
review-required posture for irregular parts are unchanged.

## [0.4.0] - 2026-08-23

### Added
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ does not invent pricing.
`steel-nest` uses MaxRects bin packing for rectangular parts and reports yield,
scrap, reusable drops, and unplaced material. Irregular parts may carry a true
polygon outline for exact areas, weights, hole checks, and drawn profiles; they
are placed by bounding box and always flagged.
are placed by bounding box, always flagged, and then compacted along their true
profiles in fixed scan-to-first-contact steps — every compacted plate must pass
an independent polygon-clearance verifier or the bounding-box layout is kept.

Per-sheet `burn_plate_N.dxf` files are emitted only when the full nest is
complete and every supported hole remains inside its part. Otherwise, pi-steel
Expand Down Expand Up @@ -147,7 +149,8 @@ npm run provenance:check # shape-data integrity and recorded decision
npm run release:check # complete release gate
```

The current package version is `0.4.0`. `release:check` verifies the test,
The current package version is declared in `package.json` and recorded per
release in `CHANGELOG.md`. `release:check` verifies the test,
privacy, package-content, shape-data integrity, ownership, license, and
redistribution contracts before publication.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@structupath/pi-steel",
"version": "0.4.0",
"version": "0.5.0",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"description": "Structural steel estimating for Pi \u2014 validated takeoffs, plate nesting, guarded DXF output, and review-ready RFQ packages.",
"type": "module",
"keywords": [
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pi-steel-runtime"
version = "0.4.0"
version = "0.5.0"
description = "Python runtime dependencies and test configuration for pi-steel"
requires-python = ">=3.11,<3.14"
dependencies = [
Expand Down
125 changes: 125 additions & 0 deletions skills/_shared/pi_steel/geometry_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,117 @@ def polygon_within_rect(
)


TRUE_SHAPE_EPSILON = 1e-6


def placed_profile(placement: dict[str, Any]) -> list[tuple[float, float]] | None:
"""True profile of one placement in plate coordinates.

Rebuilt from published placement data alone (outline, rotation flag,
original height, position) so verification never trusts a packing or
compaction algorithm's internal bookkeeping. Rotation follows the
placement contract: a rotated part maps local (x, y) to (oh - y, x).
"""
outline = placement.get("outline")
if (
placement.get("shape") == "irregular"
and isinstance(outline, list)
and len(outline) >= 3
):
if not all(_finite_point(point) for point in outline):
return None
if placement.get("rotated"):
oh = placement.get("oh")
if not isinstance(oh, (int, float)) or isinstance(oh, bool) or not math.isfinite(oh):
return None
local = [(oh - point[1], point[0]) for point in outline]
else:
local = [(point[0], point[1]) for point in outline]
else:
width, height = placement["w"], placement["h"]
local = [(0.0, 0.0), (width, 0.0), (width, height), (0.0, height)]
x, y = placement["x"], placement["y"]
return [(x + px, y + py) for px, py in local]


def verify_true_shape_placements(
placements: list[dict[str, Any]],
*,
usable_width: float,
usable_height: float,
clearance: float,
path_prefix: str = "$",
) -> list[dict[str, Any]]:
"""Independently verify true-profile clearance on one plate.

Profiles are rebuilt from published placement data via placed_profile.
Pairs whose bounding boxes keep the clearance on either axis are
provably safe, so exact polygon distances run only on the remaining
candidate pairs.
"""
findings: list[dict[str, Any]] = []
profiles: dict[int, list[tuple[float, float]]] = {}
for index, placement in enumerate(placements):
path = f"{path_prefix}.placements[{index}]"
if not _finite_placement_values(placement):
findings.append(
{
"code": "nonfinite_placement",
"path": path,
"message": "Placement coordinates and dimensions must be finite.",
}
)
continue
profile = placed_profile(placement)
if profile is None:
findings.append(
{
"code": "invalid_profile",
"path": path,
"message": (
"Placement profile could not be rebuilt for "
"true-shape verification."
),
}
)
continue
profiles[index] = profile
if not polygon_within_rect(profile, usable_width, usable_height):
findings.append(
{
"code": "true_shape_out_of_bounds",
"path": path,
"message": (
"True profile must remain inside the edge-margin "
"boundary."
),
}
)
for first_index, second_index in _overlap_candidate_pairs(
placements, clearance, TRUE_SHAPE_EPSILON
):
if first_index not in profiles or second_index not in profiles:
continue
distance = polygon_min_distance(
profiles[first_index], profiles[second_index]
)
if distance < clearance - TRUE_SHAPE_EPSILON:
findings.append(
{
"code": "true_shape_clearance_violation",
"path": (
f"{path_prefix}.placements"
f"[{first_index},{second_index}]"
),
"message": (
"True profiles overlap or violate the required "
"kerf-plus-gap clearance."
),
}
)
return findings


def gross_area(geometry: dict[str, Any]) -> float:
if geometry.get("shape") == "irregular":
outline = geometry.get("outline")
Expand Down Expand Up @@ -504,6 +615,20 @@ def verify_nest_placements(
}
)

if (plate.get("compaction") or {}).get("accepted"):
# Compacted plates interlock true profiles, so bounding boxes
# may legitimately come closer than the clearance; the pairwise
# contract is enforced on the rebuilt profiles instead.
findings.extend(
verify_true_shape_placements(
placements,
usable_width=usable_width,
usable_height=usable_height,
clearance=inter_part_clearance,
path_prefix=f"$.plate_reports[{plate_index}]",
)
)
continue
for first_index, second_index in _overlap_candidate_pairs(
placements, inter_part_clearance, epsilon
):
Expand Down
18 changes: 16 additions & 2 deletions skills/_shared/schemas/nest-result.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@
"part_gap_in": { "type": "number", "minimum": 0 },
"edge_margin_in": { "type": "number", "minimum": 0 },
"density_lb_in3": { "type": "number", "exclusiveMinimum": 0 },
"unit_system": { "type": "string" }
"unit_system": { "type": "string" },
"compact_outlines": { "type": "boolean" }
},
"additionalProperties": false
},
Expand Down Expand Up @@ -125,7 +126,8 @@
"required": ["packing_utilization_pct", "net_material_yield_pct"],
"properties": {
"packing_utilization_pct": { "$ref": "#/$defs/metric" },
"net_material_yield_pct": { "$ref": "#/$defs/metric" }
"net_material_yield_pct": { "$ref": "#/$defs/metric" },
"true_shape_utilization_pct": { "$ref": "#/$defs/metric" }
},
"additionalProperties": false
},
Expand Down Expand Up @@ -366,6 +368,18 @@
"type": "array",
"items": { "$ref": "#/$defs/remnantCandidate" }
},
"compaction": {
"type": "object",
"required": ["ran", "accepted", "recovered_in", "passes"],
"properties": {
"ran": { "type": "boolean" },
"accepted": { "type": "boolean" },
"recovered_in": { "type": "number", "minimum": 0 },
"passes": { "type": "integer", "minimum": 0 }
},
"additionalProperties": false
},
"true_shape_utilization_pct": { "$ref": "#/$defs/metric" },
"placements": {
"type": "array",
"items": { "$ref": "#/$defs/placement" }
Expand Down
11 changes: 10 additions & 1 deletion skills/steel-estimate/scripts/build-estimate-package.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,10 @@ def build_pipeline(args) -> tuple[dict[str, Any], Path]:
density_lb_in3=args.density_lb_in3,
)
if nest_job is not None:
nest_result = nest_engine.run_job(nest_job)
nest_result = nest_engine.run_job(
nest_job,
compact_outlines=not getattr(args, "no_compact_outlines", False),
)
for nest_finding in nest_result["validation_findings"]:
findings.append(
_finding(
Expand Down Expand Up @@ -860,6 +863,7 @@ def build_pipeline(args) -> tuple[dict[str, Any], Path]:
"part_gap_in": args.part_gap_in,
"edge_margin_in": args.edge_margin_in,
"density_lb_in3": args.density_lb_in3,
"compact_outlines": not getattr(args, "no_compact_outlines", False),
"mill_lengths_ft": mill_lengths,
"cutlist_kerf_in": args.cutlist_kerf_in,
"end_trim_in": args.end_trim_in,
Expand Down Expand Up @@ -1118,6 +1122,11 @@ def main(argv=None) -> int:
default="40,50,60",
help="Comma-separated purchasable mill lengths for member cut-lists",
)
parser.add_argument(
"--no-compact-outlines",
action="store_true",
help="Keep pure bounding-box nest layouts (skip verified compaction)",
)
parser.add_argument("--cutlist-kerf-in", type=float, default=0.125)
parser.add_argument("--end-trim-in", type=float, default=0.25)
parser.add_argument("--min-drop-in", type=float, default=24.0)
Expand Down
7 changes: 5 additions & 2 deletions skills/steel-nest/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ Be honest with the user about the boundary — it protects the shop from over-tr
- Multiple compatible plate sizes with independently verified bounds, non-overlap, and material grouping.
- 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.
- **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.
- Separate packing-utilization and net-material-yield percentages with approximation labels.
- **Verified true-shape compaction** for outlined irregular parts: after bounding-box packing, parts slide left-then-down in fixed 1/32-in scan-to-first-contact steps until their true profiles (not their boxes) reach the kerf-plus-gap clearance, letting complementary profiles interlock. Every compacted plate is re-checked by an independent polygon-clearance verifier that rebuilds profiles from the published placements; a failed check discards compaction for that plate (`COMPACTION_REJECTED` warning) and keeps the proven bounding-box layout. Per-plate results are recorded under `compaction` (`ran`, `accepted`, `recovered_in`, `passes`). Disable with `--no-compact-outlines`.
- Separate packing-utilization and net-material-yield percentages with approximation labels, plus `true_shape_utilization_pct` (exact profile area over plate area) on plates whose irregular parts all carry validated outlines.
- Remnant candidates per plate, explicitly not certified reusable drops.
- Material weight and optional cost by one explicit basis per stock entry (`cost_per_lb` or `cost_per_sheet`, never both).
- Labeled layout (PDF + one PNG per plate).
- **Reference files**: `reference_nest.dxf` plus `reference_plate_N.dxf`, with sheet outlines, bounding boxes, holes, and labels for estimating review.
- **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.

**Approximate — always flag it:**
- **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, give the part an `outline` — a list of `[x, y]` vertices tracing the true profile in the part's local frame (bounding box spanning `0..width` × `0..height`, simple polygon, no crossing edges). The engine then computes the exact shoelace area (`outline_exact`), checks that holes stay inside the true profile (not just the box), and draws the real outline in layouts and reference DXFs. Without an outline, supply the true cut area (in²) in `area`, or the bounding box is used as an estimate. Placement is still by bounding box — this is NOT true-shape nesting like a dedicated CAM engine.
- **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, give the part an `outline` — a list of `[x, y]` vertices tracing the true profile in the part's local frame (bounding box spanning `0..width` × `0..height`, simple polygon, no crossing edges). The engine then computes the exact shoelace area (`outline_exact`), checks that holes stay inside the true profile (not just the box), draws the real outline in layouts and reference DXFs, and recovers plate through the verified compaction pass above. Without an outline, supply the true cut area (in²) in `area`, or the bounding box is used as an estimate. Initial placement is still by bounding box and compaction only slides parts along fixed axes — this is NOT free-rotation no-fit-polygon nesting like a dedicated CAM engine, and remnant candidates stay rectangle-based and uncertified.
- 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.

**Do NOT pretend to do:**
Expand Down Expand Up @@ -69,6 +70,8 @@ current after a blocked rerun.

Use `--geometry-verified-only` when reference-only geometry does not satisfy the
request. The command still publishes its QA diagnostics, but exits unsuccessfully.
Use `--no-compact-outlines` to keep pure bounding-box layouts (skip the verified
true-shape compaction pass); the flag is recorded in the configuration hash.

Each run contains:

Expand Down
Loading
Loading