Files
OpenNest/docs/nest-file-format.md
T
ajandClaude Opus 5.5 0ca4e64189 fix(bestfit): drop saved pair sets whose canonical frame changed
Saved best-fit pairs are in the drawing's canonical frame, but the canonical
angle is recomputed on load and was not saved with the set. When that
computation changes (RotatingCalipers checking every hull edge, marks no
longer counted as material), old pairs were replayed in a turned frame and
could overlap.

Each saved set now records canonicalAngle, and BestFitCacheStore.Import
skips sets whose angle no longer matches the recomputed one (1e-9 rad,
modulo a full turn). Sets from older files have no angle and are dropped;
their pairs are recomputed when next needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:04:23 -04:00

318 lines
19 KiB
Markdown

# Nest file format and saved cutting state
`.nest` is a ZIP archive with additive version-2 JSON metadata in `nest.json`.
Old files remain readable. Older applications ignore the new cutting-state fields;
opening and saving with an older application loses that state.
## Job metadata
`nest.json` carries the workflow fields shown in the nest info dialog:
`name`, `customer`, `dateCreated`, `dateLastModified`, `notes`, `material`,
`thickness`, plus `status` (`"Draft"`, `"Quote"`, `"ToBeCut"`, `"HasBeenCut"`) and `madeBy`.
Status and made-by are additive: files written before them load as Quote with an
empty maker, and an unrecognized status value likewise falls back to Quote. Builds
before `Draft` therefore open a Draft nest as Quote, and saving it there keeps Quote.
## Programs and placements
- `programs/program-N` is drawing N's clean G-code program; optional
`programs/program-N-subs` contains its hole sub-programs.
- Each serialized plate has a one-based `id`. Its `parts` array excludes cut-offs;
each placement retains `drawingId`, `x`, `y`, and `rotation` (radians).
- A placement with `HasManualLeadIns` also writes `parts/plate-P/part-K`, and
`parts/plate-P/part-K-subs` when it has hole sub-programs. P is the serialized
plate ID; K is the zero-based index in that plate's serialized parts array.
Empty plates and cut-offs do not consume these part indices.
- The placement's `program` field references that ZIP entry. Its G-code is saved
in the already-rotated local part frame, with the placed origin at `(x, y)`.
Existing `:LEADIN`, `:LEADOUT`, and other layer tags, tab gaps, and G65 calls
are retained rather than regenerated by a cutting strategy during load.
Hole IDs may be negative: they are identifiers, not validity sentinels.
- `drawingHash` protects against restoring obsolete cutting paths after an
externally edited drawing. It is uppercase SHA-256 of UTF-8 text composed of
the main text's decimal UTF-16 character count, `:`, main text, and sub-program
text (empty when absent). The hash includes both drawing entries so changing a
hole alone invalidates the placed cutting program. Text is read without a BOM;
otherwise whitespace and line endings participate in the hash.
The reader constructs and places a clean part first, then installs its saved
program without rotating it again. The drawing reference remains authoritative
for Remove Lead-ins: removing them restores the clean drawing at the saved pose.
Successful restoration marks the part as carrying lead-ins and restores its
`leadInsLocked` flag. The per-part `CuttingParameters` reference remains null;
the saved program, not strategy regeneration, determines the cutting geometry.
Missing `program` references in legacy files load clean and silently, even when
old transient flags were true. A missing or mismatched drawing hash likewise
loads clean silently. A referenced but missing, unparseable, or motionless part
program, or an unresolved/empty hole sub-program, leaves that part clean and
unlocked without discarding the rest of the nest. `NestReader.Warnings` identifies
the plate, zero-based part index, and entry; File > Open displays these warnings.
The program reader is not a general G-code or geometric safety validator; the
saved cutting geometry is not validated against the drawing.
Nests converted from PEP before their drawings carried etch marks keep those
marks only in the saved placed programs. When a drawing has no etch (scribe)
moves and every placement with a saved program carries the same marks, the
reader adds those marks to the drawing in its own frame (after its existing
moves) and refreshes that drawing's clean placements, so Remove Lead-ins and
Clear Lead-ins & Cut-offs keep the marks. Saved programs are never changed. If
the saved programs disagree, the drawing is left without marks and no warning
is raised. The drawing hash is checked against the file text before this
repair, so saved programs still load; the next save writes the marks into
`programs/program-N`.
## Plate cutting parameters
Each plate's optional `cuttingParameters` stores the settings used by Assign
Lead-ins and Place Lead-in. Missing settings stay null, preserving the old default
selection behavior. `OpenNest.IO.CuttingParametersSerializer` owns the shared
cross-platform DTO mapping; the desktop settings serializer delegates to it so
existing saved settings keep their JSON contract. Settings are independent of
the saved part programs: restoring settings never regenerates those programs.
## Clamp zones
Each plate may carry an optional `clamps` array of fixed rectangular keep-out
regions in plate coordinates: `id`, `x`, `y`, `length` (X extent) and `width`
(Y extent, the depth intrusion from the `y = 0` operator-side edge for machine
clamps). Zones are exclusion and display data only - never material, quantity,
utilization, cut sequence or CNC output. Files without the key load with no
zones, and an individually malformed entry (blank id, non-positive size) is
dropped while the rest of the nest loads.
`plateDefaults.clampRail` optionally stores the nest's machine clamp rail:
`width` (along the edge), `depth` (into the sheet) and `centers` (X positions
measured from the plate origin along the origin-side edge). Clamps on the other
edges are listed in optional `farSideCenters` (X, along the edge across the
sheet's width from the origin), `originEndCenters` (Y, along the edge through
the origin) and `farEndCenters` (Y, along the edge across the sheet's length);
each is omitted when empty, so origin-side-only rails are written exactly as
before. Rail clamp ids (`clamp-N`) number the origin-side clamps first, then
far side, origin end and far end, each by ascending center. Older readers
ignore the extra arrays; a plate's saved `clamps` then no longer match their
rail and load as explicit zones. New plates follow
the rail and resolve it for their own size and quadrant; clamps wholly beyond a
sheet are skipped and one still reaching onto it keeps its whole footprint, so
a zone may extend past the sheet edge. The key is omitted
when a nest has no rail, so such files are unchanged; a missing, empty or
malformed rail loads as none. On load a plate follows the rail again only when
its saved `clamps` are exactly the rail's zones for that sheet; any other saved
zones are kept as written.
The rail's optional `removalMinutes` is the time to take one clamp off and put
it back; a missing or invalid value loads as five minutes. A plate on which
clamps were removed (by Auto Nest as a last resort, or by hand) lists their rail ids in an optional
`removedClamps` array (omitted when none were removed); its `clamps` already
exclude them, and it follows the rail minus those ids. A malformed
`removedClamps` value reads as none removed (non-string entries are skipped);
the saved `clamps` load either way. A plate that follows a rail other than the nest's default
saves it in its own optional `clampRail` (same shape), so its removal state and
time survive a reload; plates following the default rail omit it.
An empty plate (no parts, no cut-offs) is omitted from the file unless that
would lose clamp zones: it is omitted like any other empty plate only when a
new plate from the saved defaults would recreate it (same size, quadrant, edge
and part spacing, zones the saved rail recreates), which keeps the editor's trailing empty sheet
out of the file while it matches the defaults. Nest-defaults edits move only an
untouched trailing empty, cutoff-free sheet to the new size, quadrant, edge and
part spacing, re-resolving its rail. It must have exactly matched the old defaults
and followed their rail with no removed clamps or explicit zones (or have had no
rail and no zones when the defaults had none). Edited sheets, middle empty sheets,
and sheets with parts, cut-offs, removed clamps or explicit zones are not changed.
An empty plate with other zones, or a rail-following one whose size, quadrant or
spacing differs from the saved defaults, is still written.
## Support grid
`plateDefaults.supportBed` optionally stores the machine support bed every sheet of
the nest rests on (see [support tabs](support-tabs.md)): `type` (`grid`, the only
layout so far), `pitchX`, `pitchY`, `offsetX`, `offsetY` (the first grid point,
measured from the plate origin into the sheet), `registrationTolerance` and
`stabilityMargin`, all in the nest's units. The key is omitted when the nest has
none, so such files are unchanged. A missing pitch, a non-numeric value, an unknown
`type` or an invalid value (a non-positive pitch, a negative tolerance or margin)
loads as no bed; the rest of the nest loads. Every plate follows the nest's bed.
## Drawing edits
`DrawingProgramSnapshot` captures serialized main and hole-program text before
the converter is loaded. After an accepted edit, only parts referencing drawings
whose program text changed are rebuilt (lead-ins, tabs, and locks cleared); their
location and rotation remain unchanged. Reference identity, not editable names,
keys this comparison. The desktop invalidates the affected layout graphics.
Name, quantity, and color edits leave placed programs alone. The properties
editor previously called `LayoutPart.Update`, a color refresh, not `Part.Update`;
it now explicitly refreshes only the edited drawing's layout colors.
## Saved best-fit pairs (cache only)
Optional `bestfits/bestfit-N` entries hold drawing N's cached best-fit pair
results: a JSON array of sets, each with `plateWidth`, `plateHeight` and
`spacing` (the plate size and part spacing it was filtered for),
`canonicalAngle` and `results`. Each result has `part1Rotation`,
`part2Rotation`, `part2OffsetX`, `part2OffsetY`, `strategyType`, `testNumber`,
`candidateSpacing`, `rotatedArea`, `boundingWidth`, `boundingHeight`,
`optimalRotation`, `keep`, `reason`, `trueArea` and `hullAngles`. Angles are
radians; pair geometry is in the drawing's canonical frame.
The canonical angle is not saved with the drawing: readers recompute it from
the program, so a change to that computation turns the frame of drawings saved
before it. `canonicalAngle` records the drawing's canonical angle when the set
was saved, and `BestFitCacheStore` drops a set on load unless it matches the
recomputed angle (within 1e-9 radians, modulo a full turn), since pairs replayed
in a turned frame can overlap. Sets from files saved before the field existed
have no angle and are dropped the same way; their pairs are recomputed when
next needed.
The entries are only a cache: readers may ignore them and writers may omit
them, at the cost of recomputing pairs. Writers save only sets whose
(width, length, spacing) matches a plate in the nest, and only kept results; a
drawing without such a set gets no entry. `NestReader` and `NestWriter` touch
these entries only when given an `IBestFitStore` through `BestFitStore`;
without one, a reader skips them unparsed and a writer writes none. The front
ends pass the engine's `BestFitCacheStore.Shared`, which adds loaded sets to
the process-wide `BestFitCache` without replacing sets already computed.
## Drawing library links
A drawing copied from a server drawing library carries a `library` object:
`serverKey` (the normalized server URL), `id`, `revision`, and a snapshot of that
revision's `cutType`, `thickness`, `materialName`, `materialGrade`, `units`, `notes`
and `packageSha256`. It is omitted for other drawings. Read leniently: a value that
is not an object, has the wrong types, a blank server key, an empty id, a revision
below 1, a cut type or units that are not one supported name, or a negative
thickness loads the drawing without a link and adds a reader warning. Converter
edits keep the link as provenance; split pieces get none. Drawing packages leave
the link out, so linking a drawing does not change its package bytes.
### Drawing packages
A library drawing's geometry travels as a canonical drawing package
(`OpenNest.IO.DrawingPackage`): a `.nest` archive with one drawing and no plates,
fixed archive timestamps and dates, and no best-fit entries, so equal content has
equal bytes and SHA-256. The server stores each distinct package once by that hash.
A package holds geometry and geometry-bound data only: program, source entities and
offset, suppressed entity IDs, entity roles, bends, cut pins, breakoffs, tabbed cutouts and material
density. Fields
the server's library record owns are written as their defaults, so editing them never
changes the hash:
- nest and drawing `name` (blank), drawing `customer`, material `name` and `grade` (blank);
- `color`, `customColor`, `priority`, `cutFirst` and `constraints` (omitted);
- `quantity.required` (zero) and `library` (omitted).
Adding a library drawing to a nest ignores whatever a package holds for these fields
and applies the record's values.
## Drawing colors
`color` is the drawing's display fill. A drawing writes `customColor: true` when
someone picked that color on purpose (Drawing Properties, or a library record with a
stored color); it is omitted when false. Opening a nest in the desktop app, or
changing the color scheme, reassigns every non-custom, non-cutoff drawing a color
from the active scheme's palette, so the view follows the application's scheme
rather than whichever scheme the file was saved under. Files written before this
flag have no custom colors and are recolored on open. Split pieces keep the flag.
## Drawing cut-first flag
A drawing may write `cutFirst: true` to have automatic part sequencing cut its parts
before every other part (see [part sequencing](automatic-scrap-cutoffs.md#part-sequencing));
it is omitted when false, so older readers ignore it. Split pieces keep the flag.
## Drawing cut pins
A drawing may carry a `cutPins` array, omitted when empty. Each pin has `x`/`y`
in the drawing's source frame (the frame of its source entities, which does not
move when the program is re-origined at its first rapid; the program frame is the
source frame minus `source.offset`), a `leadIn` and `leadOut` in the same shape as
plate cutting parameters, and an optional `tab` omitted for a closed contour.
Pins are part of the drawing's content, so drawing packages carry them and they
change the package hash. Pins are read strictly per entry but leniently per file:
an entry that is not an object, has wrong types, an unknown lead or tab type, an
invalid dimension, or a tab together with a lead-out is skipped with a reader
warning naming the drawing and pin; the other pins and the drawing load. A
`cutPins` value that is not an array loads no pins, with a warning. Converter
edits keep pins (they stay in the source frame); split pieces and canonical
engine copies get none.
A placement may write `drawingPinsOff: true` to ignore its drawing's pins on that
part only; it is omitted when false. Any value other than a boolean loads as
false (the drawing's pins apply) with a warning naming the plate and part.
## Drawing breakoffs
A drawing may carry a `breakoffs` array, omitted when empty. Each entry has
`edgeIds` (source entity IDs of the joining line), `flip` (omitted when false),
`tabWidth` and `tabCenters` (distances along the joining line), all in the source
frame; see [breakoffs](breakoffs.md). The stored program already has them applied,
so readers never rebuild geometry from them. An entry with no edges, a tab width that
is not positive and finite, or a nonfinite tab center is dropped with a reader
warning; the drawing and its program load unchanged, only the breakoff stops being
editable.
Break cuts are saved as motions tagged `:BREAK`. In a drawing program each break cut
travels with its scrap on the left; placed programs may cut it either way. Versions
without breakoff support cannot read the tag.
## Drawing tabbed cutouts
A drawing may carry a `tabbedCutouts` array, omitted when empty. Each entry has
`edgeIds` (source entity IDs of the hole's contour), `tabWidth` and `tabCenters`
(distances counterclockwise around the hole from its lowest-left vertex), all in the
source frame; see [tabbed cutouts](tabbed-cutouts.md). The stored program already has
them applied, so readers never rebuild geometry from them. An entry with no edges, a tab
width that is not positive and finite, or a nonfinite tab center is dropped with a reader
warning; the drawing and its program load unchanged, only the cutout stops being editable.
Tab spans are saved as motions tagged `:TAB`. They bound material but are never cut;
versions without tabbed cutout support read them as ordinary cut motions.
## Drawing entity roles
`entities/entities-N` holds drawing N's source entities, the IDs of suppressed
(excluded) ones in `suppressed`, and an optional `roles` object, omitted when empty,
mapping source entity IDs to `"cut"`, `"etch"` or `"display"`. An entity without an
entry keeps the role its layer implies: `ETCH`, `SCRIBE` and `ENGRAVE` etch, every
other layer cuts. The stored program already has the roles applied: etched entities
are `:SCRIBE` motions and display-only entities are not in the program at all, so
posts, nesting, area and verification never see them; the desktop draws them with
each placed part. Roles are geometry-bound data, so drawing packages carry them and
they change the package hash. Unknown role names and malformed IDs are skipped (that
entity keeps its layer role). Load-time etch repair only uses entities whose role is
etch, so an etch-layer entity set to cut stays cut. Split pieces get no roles.
Versions without role support ignore `roles`; they keep the program as saved but
lose the overrides, and display-only geometry, on the next converter edit.
## Verification and limits
The automated tests cover save/reopen/re-save, rotated/manual/locked parts,
lead-outs and tab gaps, hole binding, clean removal, stale drawings, damaged
entries isolated from other parts, plate settings, and CI Fiber posting before
and after reload. Drawing-change decision logic runs on Linux; desktop rendering,
dialog values, and clicks require Windows acceptance.
Known separate issue: the cutting strategy currently labels contour cuts
`Display`. CI Fiber includes them, but GravographIS skips Display. Saving restores
the existing layers; this change does not alter the strategy or either post.
Future hardening: make numeric G-code serialization/parsing culture-invariant
as a coordinated compatibility change; do not change just one side. Program
content deduplication is optional and must not change per-part ownership.
## Stock-option prices
The existing `plateOptions` entries retain width, length and `cost`; no new `.nest`
format version is required. Legacy zero cost means unspecified, not a free sheet.
Positive costs are generic per-sheet values in caller-consistent units. Solving
requires all available options to be priced or all to be unspecified.
Desktop and accepted MCP/API jobs retain offered options (API rows with zero available
quantity are excluded because `.nest` options do not retain inventory). MCP save also preserves
loaded job metadata, plate defaults and salvage settings, and accepted whole-job
requirements are stored on drawings. Options do not persist finite inventory or
associate a historical purchase price with each physical sheet. API quote archives
additionally retain request costs and validated proposal cost summaries; old archives
without summaries load with `Costs == null`.