@cutoptim/engine 0.0.1 → 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +231 -4
- package/dist/index.d.ts +78 -0
- package/dist/index.js +127 -0
- package/dist/types.gen.d.ts +1154 -0
- package/dist/types.gen.js +3 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/dist/webhooks.d.ts +13 -0
- package/dist/webhooks.js +74 -0
- package/package.json +46 -5
|
@@ -0,0 +1,1154 @@
|
|
|
1
|
+
/** One webhook event. Thin on purpose: the plan is read with GET /v1/jobs/{id}, never pushed. */
|
|
2
|
+
export interface WebhookEvent {
|
|
3
|
+
/** Event id, `evt_…` — stable across retries; deduplicate on it. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** `webhook.test` is sent only by the dashboard's "Send test event" button. */
|
|
6
|
+
type: "job.succeeded" | "job.failed" | "webhook.test";
|
|
7
|
+
/** Unix seconds when the event was created (not when this attempt was sent). */
|
|
8
|
+
created: number;
|
|
9
|
+
/** The type of the key that submitted the job. */
|
|
10
|
+
env: "live" | "test";
|
|
11
|
+
/** For job events: the job. For webhook.test: a `message`. */
|
|
12
|
+
data: {
|
|
13
|
+
jobId?: string;
|
|
14
|
+
mode?: "2d" | "nest";
|
|
15
|
+
engine?: "max";
|
|
16
|
+
status?: "succeeded" | "failed";
|
|
17
|
+
message?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** The request is well-formed. No solve ran and no quota was spent. */
|
|
21
|
+
export interface ValidateResponse {
|
|
22
|
+
/** Always true on a 200 — a malformed request is a 400 with the exact bad field path (`details.path`) instead. The body is the SAME one the matching optimize endpoint takes. */
|
|
23
|
+
valid: boolean;
|
|
24
|
+
/** The endpoint you called. */
|
|
25
|
+
mode: "2d" | "1d" | "wood" | "nest";
|
|
26
|
+
/** Shape version. Currently `"1"`. */
|
|
27
|
+
contractVersion: string;
|
|
28
|
+
/** Nest only — whether the nesting engine is built into this deployment. Absent for the rectangular modes. */
|
|
29
|
+
engineEnabled?: boolean;
|
|
30
|
+
/** Part rows sent, and the total quantity after `qty` expansion. Check it against the part cap before you spend a call. */
|
|
31
|
+
parts: {
|
|
32
|
+
/** Number of part rows sent. */
|
|
33
|
+
rows: number;
|
|
34
|
+
/** Total quantity across all part rows (Σ qty). */
|
|
35
|
+
total: number;
|
|
36
|
+
};
|
|
37
|
+
/** The same for stock. */
|
|
38
|
+
stock: {
|
|
39
|
+
/** Number of stock rows sent. */
|
|
40
|
+
rows: number;
|
|
41
|
+
/** Total quantity across all stock rows (Σ qty). */
|
|
42
|
+
total: number;
|
|
43
|
+
};
|
|
44
|
+
/** Approximate, solve-free feasibility notes — e.g. a part that fits no stock (2D and nest: by bounding box; wood: no matching section long enough). Empty ⇒ every part fits some stock. Ignores trim/kerf/material (a shape check, not a solve). */
|
|
45
|
+
warnings: Array<string>;
|
|
46
|
+
}
|
|
47
|
+
/** The error envelope for every failure the engine itself produces. */
|
|
48
|
+
export interface Error {
|
|
49
|
+
/** Stable machine-readable code. Branch on this, never on `message`. (`service_unavailable` means an engine is not built into that deployment: the nesting engine for `POST /v1/optimize/nest`, or the async `max` engine for `engine:"max"`. `solve_timeout` is produced only by `POST /v1/optimize/nest`, whose subprocess has its own budget; the rectangular paths are bounded at the proxy and answer 504 without this envelope. The two `idempotency_*` codes (409) are sent only to a request that carried an `Idempotency-Key` header.) */
|
|
50
|
+
error: "invalid_request" | "unauthorized" | "quota_exceeded" | "key_revoked" | "too_large" | "busy" | "service_unavailable" | "solve_timeout" | "internal" | "idempotency_conflict" | "idempotency_in_progress";
|
|
51
|
+
/** Human-readable explanation. Wording may change between releases. */
|
|
52
|
+
message: string;
|
|
53
|
+
/** Present on some errors. For `invalid_request` it carries `path`, the dotted/indexed location of the offending field (e.g. `parts[0].h`) — and, when `engine: "max"` was asked for something that tier cannot model, an `unsupported` array naming each one. For `too_large` it carries `path`, `max` and sometimes `got` — except a request BODY over the size cap, which carries `maxBytes` alone (the body is refused before it is parsed, so there is no field path). An unknown `engine` id adds `available`, the ids this deployment accepts. On `/v1/optimize/nest` a part outline that crosses itself carries `reason: "self_intersecting"` with `path` (`parts[i].polygon`), or with the nesting engine's internal `itemId` when it cannot be traced back to a request row. Treat unknown keys as informational: new ones are additive. The public demo key's refusals (403 `key_revoked`, 429 `busy`) carry `reason` (`demo_scope` / `demo_rate_limit`) and `testKeyUrl`; a rate refusal adds `limit`, a size refusal `maxParts` or `maxStockRows`. */
|
|
54
|
+
details?: {
|
|
55
|
+
path?: string;
|
|
56
|
+
max?: number;
|
|
57
|
+
got?: number;
|
|
58
|
+
/** The request-body cap in bytes (`too_large` on the body itself). */
|
|
59
|
+
maxBytes?: number;
|
|
60
|
+
/** What `engine: "max"` cannot model in this request. */
|
|
61
|
+
unsupported?: Array<string>;
|
|
62
|
+
/** Engine ids this deployment accepts (unknown `engine`). */
|
|
63
|
+
available?: Array<string>;
|
|
64
|
+
/** A finer cause within the code: `self_intersecting` (a nest outline crosses itself), `demo_scope` or `demo_rate_limit` (a public demo key refusal). */
|
|
65
|
+
reason?: string;
|
|
66
|
+
/** Public demo key refusals only: where a signed-in account creates a free test key (never billed). */
|
|
67
|
+
testKeyUrl?: string;
|
|
68
|
+
/** Public demo key rate refusals only (`demo_rate_limit`): which allowance ran out — `ip-min`, `ip-day`, `global-min`, `global-day`, or `capacity` (every solve slot busy; the demo never queues). */
|
|
69
|
+
limit?: string;
|
|
70
|
+
/** Public demo key only: the parts cap (total quantity) a demo request may carry. */
|
|
71
|
+
maxParts?: number;
|
|
72
|
+
/** Public demo key only: the stock-row cap a demo request may carry. */
|
|
73
|
+
maxStockRows?: number;
|
|
74
|
+
/** Nest only, when no request row could be named: the nesting engine's internal id of the rejected outline. */
|
|
75
|
+
itemId?: number;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Routing failure. Uses a code (`not_found`) that is intentionally outside the `Error` enum, because it is produced by the router rather than by the API contract. */
|
|
79
|
+
export interface NotFoundError {
|
|
80
|
+
error: "not_found";
|
|
81
|
+
message: string;
|
|
82
|
+
}
|
|
83
|
+
/** An [x, y] vertex, in the caller's chosen unit. */
|
|
84
|
+
export type NestPoint = Array<number>;
|
|
85
|
+
/** An ordered ring of ≥3 vertices. May be given closed (first == last) or open; the DTO layer normalises. Winding order is not required — it is oriented internally. */
|
|
86
|
+
export type NestRing = Array<NestPoint>;
|
|
87
|
+
/** An exclusion / inferior-quality zone ON a sheet: a defect, a clamp footprint, a pre-printed area — something the rectangular API cannot express. */
|
|
88
|
+
export interface NestZone {
|
|
89
|
+
polygon: NestRing;
|
|
90
|
+
/** A zone of quality Q is a hazard for any part whose `minQuality` exceeds Q. Default 0 = hard-forbidden for any part with the default `minQuality` of 1. */
|
|
91
|
+
quality?: number;
|
|
92
|
+
}
|
|
93
|
+
/** Read this part's outline out of an SVG or DXF FILE instead of listing coordinates. The same parser the CutOptim app uses for its Nesting mode runs server-side, so a part library that already exists as CAD files needs no path/arc flattener of your own. **The file replaces ONLY the geometry.** `qty`, `material`, `allowedRotations`, `minQuality`, `priority` and `meta` behave exactly as they do on a `polygon` part. **Exactly ONE outline per source.** Holes are folded in automatically (a ring contained in another becomes a hole), so a typical single-part drawing — outline plus bolt holes — is one outline. A file describing several separate parts is a 400 that names `POST /v1/import/nest`, which splits it. **Nothing is stored.** The bytes exist only as this request body, are parsed in memory and are gone when the response is written: no disk, no database, no temp file, no log line. There is nothing to delete and nothing retained. **Units are reported, never applied.** A DXF may declare `$INSUNITS`; it comes back in `imported.units` and several disagreeing files produce a warning, but coordinates pass through verbatim — this API converts nothing (see the unit rule on `/v1/optimize/nest`). */
|
|
94
|
+
export interface NestSource {
|
|
95
|
+
/** The file TEXT — not base64, not a URL, not a multipart upload: both formats are text documents, so they travel inside the JSON body like any other field. Max 4,000,000 characters per file and 8,000,000 across the request; over either is a 413. ⚠️ The HTTP body cap differs by path: 10 MB on `/v1/optimize/nest` and `/v1/import/nest`, but the DEFAULT 1 MB on `/v1/validate/nest`, which is the only nest path that needs no key. A source body over 1 MB therefore validates on the optimize path, not the free one. */
|
|
96
|
+
content: string;
|
|
97
|
+
/** Omit to sniff the content (an `<svg` tag ⇒ svg, otherwise dxf). An explicit format always wins — including over a misleading `filename` extension. */
|
|
98
|
+
format?: "svg" | "dxf";
|
|
99
|
+
/** Used for format detection AND as the part name when the row has no `name` (extension stripped). Never touches a filesystem. */
|
|
100
|
+
filename?: string;
|
|
101
|
+
/** Curve / arc flattening tolerance, in the FILE's own coordinate unit. Larger = fewer vertices. This is the lever when a dense outline trips the 2,000-vertex per-ring cap. */
|
|
102
|
+
flattenTolerance?: number;
|
|
103
|
+
}
|
|
104
|
+
/** The same four fields `parts[].source` takes, at the top level. See NestSource for the caps, the sniffing rule and the no-storage guarantee. */
|
|
105
|
+
export interface NestImportRequest {
|
|
106
|
+
/** The file TEXT (not base64) — the same 4,000,000-character cap as `parts[].source`. */
|
|
107
|
+
content: string;
|
|
108
|
+
/** Omit to sniff; an explicit value wins over the filename extension. */
|
|
109
|
+
format?: "svg" | "dxf";
|
|
110
|
+
/** Used for detection and to NAME the parts: one outline keeps the bare name, several are numbered `<name> 1`, `<name> 2`, … */
|
|
111
|
+
filename?: string;
|
|
112
|
+
/** Curve / arc flattening tolerance in the file's own unit. */
|
|
113
|
+
flattenTolerance?: number;
|
|
114
|
+
}
|
|
115
|
+
/** One outline, in exactly the shape a `parts[]` row wants — paste it in and add your own qty / material / rotations. */
|
|
116
|
+
export interface NestImportedPart {
|
|
117
|
+
/** Named from `filename`: one outline keeps the bare name, several are numbered. */
|
|
118
|
+
name: string;
|
|
119
|
+
polygon: NestRing;
|
|
120
|
+
/** Present only when the outline actually contained a ring. */
|
|
121
|
+
holes?: Array<NestRing>;
|
|
122
|
+
}
|
|
123
|
+
export interface NestImportResponse {
|
|
124
|
+
/** The parser that actually ran — useful when you let the server sniff. */
|
|
125
|
+
format: "svg" | "dxf";
|
|
126
|
+
/** One entry per closed outline, largest first — each already in the shape a `parts[]` row wants: paste it in and add your own qty / material / allowedRotations. Deterministic: the same bytes always give the same vertices in the same order. */
|
|
127
|
+
parts: Array<NestImportedPart>;
|
|
128
|
+
/** Total vertices after flattening, outlines and holes together. Compare against the 2,000-per-ring cap before you build a big request. */
|
|
129
|
+
vertices: number;
|
|
130
|
+
/** A DXF `$INSUNITS` declaration when the file carries one. REPORTED, NEVER APPLIED — the API converts nothing, so this is information for you, not a transformation we performed. Absent for SVG (the format has no unit) and for a DXF that declares none. */
|
|
131
|
+
sourceUnit?: "mm" | "in";
|
|
132
|
+
/** What the parser could not honour — e.g. an SVG `<use>` clone (not expanded), or geometry that never closed into a contour. SVG `transform=` attributes (on the element and its groups) are applied, and shapes inside `<defs>`, `<clipPath>`, `<mask>` or hidden with `display:none` are not read. Empty means the file was read whole. */
|
|
133
|
+
warnings: Array<string>;
|
|
134
|
+
/** Whether THIS deployment can also SOLVE a nest. Importing works everywhere (it is pure parsing); on a deployment without the nesting engine `/v1/optimize/nest` answers 503, and this flag tells you that up front. Mirrors the field on `/v1/validate/nest`. */
|
|
135
|
+
engineEnabled: boolean;
|
|
136
|
+
/** Nest contract version. Currently `"1"`. */
|
|
137
|
+
contractVersion: string;
|
|
138
|
+
}
|
|
139
|
+
/** What the request's `source` files contributed. ABSENT when every part carried `polygon` coordinates, which is what keeps such a request byte-identical to before the feature existed. */
|
|
140
|
+
export interface NestImportSummary {
|
|
141
|
+
/** Part ROWS whose geometry came from a file. */
|
|
142
|
+
parts: number;
|
|
143
|
+
/** Total vertices those files produced, after flattening (outlines + holes) — the number to watch against the 2,000-vertex per-ring cap. */
|
|
144
|
+
vertices: number;
|
|
145
|
+
/** Distinct units the files DECLARED (DXF `$INSUNITS`); empty when none did. Reporting only — more than one entry also raises a warning, because mixing units silently produces a plan that validates and is physically wrong, and the server must not "fix" it by converting — no field in this API asserts a unit. */
|
|
146
|
+
units: Array<"mm" | "in">;
|
|
147
|
+
}
|
|
148
|
+
/** A part. Give its outline as `polygon` coordinates OR as a `source` file — exactly one of the two (both, or neither, is a 400). */
|
|
149
|
+
export type NestPart = {
|
|
150
|
+
/** Optional label, echoed on every placement. Defaults to `source.filename` without its extension when a file was given, otherwise `Part <1-based row index>`. */
|
|
151
|
+
name?: string;
|
|
152
|
+
polygon?: NestRing;
|
|
153
|
+
source?: NestSource;
|
|
154
|
+
/** Optional interior holes — a part with a cut-out. NOT accepted together with `source`: the file already carries its own holes. */
|
|
155
|
+
holes?: Array<NestRing>;
|
|
156
|
+
/** Copies to place. */
|
|
157
|
+
qty?: number;
|
|
158
|
+
/** Allowed rotations in DEGREES (e.g. [0,90,180,270]). Omit or "continuous" for free rotation. */
|
|
159
|
+
allowedRotations?: Array<number> | "continuous";
|
|
160
|
+
/** The part may only be placed where sheet quality ≥ this. Default 1 → it avoids every quality-0 exclusion zone. Raise it to keep the part off inferior-but-not-forbidden zones too. */
|
|
161
|
+
minQuality?: number;
|
|
162
|
+
/** Must-cut. With `options.respectStock` (a capped supply, where not every part may fit) these parts are placed first, so a plain part never takes the sheet space a must-cut part needs, and every plan comparison ranks the plan that keeps more of them ahead. Honoured by both engines. Without `respectStock` every part that fits is placed anyway, so the flag changes nothing. A must-cut part that fits no sheet is still listed in `unplaced`. */
|
|
163
|
+
priority?: boolean;
|
|
164
|
+
/** A part of material X nests only on material-X sheets; the job is partitioned by material. */
|
|
165
|
+
material?: string;
|
|
166
|
+
/** Opaque JSON (your ERP ids), echoed verbatim on every placed copy. Never affects the layout. */
|
|
167
|
+
meta?: Record<string, unknown>;
|
|
168
|
+
} & ({
|
|
169
|
+
polygon: NestRing;
|
|
170
|
+
} | {
|
|
171
|
+
source: NestSource;
|
|
172
|
+
});
|
|
173
|
+
/** A stock sheet type: either a rectangle (w & h) OR an arbitrary polygon outline (an off-cut remnant). Exactly one of {w & h} | polygon. */
|
|
174
|
+
export interface NestStock {
|
|
175
|
+
/** Rectangular sheet size. Give w & h OR polygon, not both. */
|
|
176
|
+
w?: number;
|
|
177
|
+
/** Rectangular sheet size. Give w & h OR polygon, not both. */
|
|
178
|
+
h?: number;
|
|
179
|
+
/** Arbitrary sheet outline (an off-cut remnant, a non-rectangular board) instead of w & h. */
|
|
180
|
+
polygon?: NestRing;
|
|
181
|
+
/** A HARD cap only when options.respectStock is true. */
|
|
182
|
+
qty?: number;
|
|
183
|
+
/** Per-sheet price. Summed into `totalPrice` / `stockUsage[].totalPrice`, and the ranking key of `options.minimizeCost`. */
|
|
184
|
+
price?: number;
|
|
185
|
+
/** Integer relative per-sheet cost, used by `options.minimizeCost` for a row that has no `price` (e.g. when price is not money). */
|
|
186
|
+
cost?: number;
|
|
187
|
+
/** This sheet serves only material-matching parts. */
|
|
188
|
+
material?: string;
|
|
189
|
+
/** Per-sheet excluded / inferior zones: a defect, a clamp footprint, a pre-printed area — what the rectangular API cannot express. */
|
|
190
|
+
exclusions?: Array<NestZone>;
|
|
191
|
+
/** Echoed on every sheet cut from this stock row. */
|
|
192
|
+
meta?: Record<string, unknown>;
|
|
193
|
+
}
|
|
194
|
+
/** Solve options. All optional. */
|
|
195
|
+
export interface NestOptions {
|
|
196
|
+
/** Minimum clearance between parts and between a part and any hazard (sheet edge / zone). Use for kerf / beam / torch width. Default 0. The sheet-edge part can be set separately with `edgeClearance`. */
|
|
197
|
+
minSeparation?: number;
|
|
198
|
+
/** Minimum distance between any part and the SHEET EDGE, independent of `minSeparation` (e.g. 5 between parts, 0 or 10 at the edge). Omit it and the edge keeps `minSeparation` (the previous behaviour, unchanged). Both engines. Rectangular sheets only — with a `polygon` sheet it is a 400, as is a value that leaves no room on a sheet (the sheet minus twice `edgeClearance − minSeparation` must stay positive). Exclusion zones keep `minSeparation`, like parts. */
|
|
199
|
+
edgeClearance?: number;
|
|
200
|
+
/** Search budget for engine `max`, in milliseconds (default 60000; clamped to 10000–180000, and a clamp is reported in `warnings`). Ignored by `lbf` (no search budget). The job finishes about 1–2 s after the budget, plus any time spent queued; for a job of a few dozen parts 15000 is usually enough. Poll `GET /v1/jobs/{id}` every 2–3 s — polling costs no quota. */
|
|
201
|
+
timeBudgetMs?: number;
|
|
202
|
+
/** Determinism: fixed seed → reproducible layout. Omit and the server pins a fixed default, so the response stays reproducible. */
|
|
203
|
+
seed?: number;
|
|
204
|
+
/** How engine `lbf` chooses the sheet size when a material has two or more stock rows. It always tries the rows in your order, each row on its own and a cheapest-first order, and keeps the best, so the result does not depend on the order you list them in. Ranking: most must-cut (`parts[].priority`) parts placed → most parts placed → (with `minimizeCost`) lowest total price → least total sheet area → fewest sheets. `minimizeCost` ranks by `price` (or `cost` when a row has no price) only when EVERY row of that material carries one; otherwise it falls back to sheet area and says so in `warnings`. Since 2026-09-22 (F3) the plan may also MIX sizes within a material: every sheet, least-filled first, is re-nested onto the cheaper or smaller rows and replaced when the whole plan ranks better — typically the half-empty last sheet moves to a smaller size. Engine `max` uses the same ranking to decide whether its search result replaces the `lbf` plan. */
|
|
205
|
+
minimizeCost?: boolean;
|
|
206
|
+
/** Treat each stock qty as a hard cap (both engines; the mixed-size re-nest never uses more sheets of a row than its qty). */
|
|
207
|
+
respectStock?: boolean;
|
|
208
|
+
/** Polygon simplification tolerance (max area deviation as a fraction). Speeds up dense DXF outlines with hundreds of vertices. 0 disables. */
|
|
209
|
+
simplifyTolerance?: number;
|
|
210
|
+
/** Compact the least-filled sheet of each material (both engines, default on). After the plan is final, that sheet's parts are re-nested on the same sheet with rotation SUBSETS of what each part allows — {0,180}, {0,90,180,270} and a single orientation, intersected with a discrete `allowedRotations`, never a new angle — and the variant with the smallest used area (`sheets[].usedWidth × usedHeight`) is kept. With engine `max` the sheet (when it holds two or more parts) is then also re-packed by the strip search itself — along the sheet's height and, on a one-sheet plan, also along its width; a few seconds on top of the search budget, under a hard time limit — and the result replaces the sheet only when it is strictly smaller under `compactFor` and keeps the spacing and the edge clearance. Sheet count, sizes, price and placed parts are unchanged by construction. Runs within a work budget (a very full last sheet may be left as it is). `false` returns the uncompacted layout. */
|
|
211
|
+
compact?: boolean;
|
|
212
|
+
/** Which used measure the compaction minimises — match it to how you charge a partial sheet. `box` (default): the smallest used bounding area (`usedArea`). `horizontal`: the lowest `usedHeight`, for charging a strip across the full sheet width. `vertical`: the smallest `usedWidth`, for charging a strip over the full sheet height. Ties fall back to the used area; the strip modes also try every copy turned a quarter (90° or 270°, where `allowedRotations` permits it). It only changes which compacted layout is kept: sheet count, sizes, price and placed parts never change, and the result is never worse than the uncompacted layout on that measure. Ignored with `compact: false`. */
|
|
213
|
+
compactFor?: "box" | "horizontal" | "vertical";
|
|
214
|
+
}
|
|
215
|
+
export interface OptimizeNestRequest {
|
|
216
|
+
/** One or more parts to nest, one NestPart per row. */
|
|
217
|
+
parts: Array<NestPart>;
|
|
218
|
+
/** One or more sheet types, one NestStock per row. */
|
|
219
|
+
stock: Array<NestStock>;
|
|
220
|
+
options?: NestOptions;
|
|
221
|
+
/** `lbf` (default) is single-pass, synchronous and instant. `max` is ASYNCHRONOUS: 202 + poll `GET /v1/jobs/{id}`; it runs `lbf` first, then a time-budgeted search that fills sheets one at a time looking for FEWER sheets, and never returns more than `lbf`, nor a plan that ranks worse (price with `minimizeCost`, otherwise sheet area). It accepts several sheet sizes per material and `respectStock`; a `polygon` sheet or exclusion zones are served by `lbf` inside the job, with a warning. `sparrow` is a deprecated alias of `max`. */
|
|
222
|
+
engine?: "lbf" | "max" | "sparrow";
|
|
223
|
+
/** Additive inline export of the achieved layout. `svg` returns a self-contained, styled `<svg>` string (one titled band per sheet, parts drawn as filled polygons with holes, exclusion zones hatched); `dxf` returns an R12 document (closed `POLYLINE`/`VERTEX` outlines on STOCK/PARTS/HOLES/ZONES layers, part names on LABELS). Both are unit-agnostic, exactly like the request coordinates. The geometry in `sheets[]` is returned either way. */
|
|
224
|
+
include?: Array<"svg" | "dxf">;
|
|
225
|
+
}
|
|
226
|
+
/** A placed copy. Carries the rigid transform (rotation THEN translation), not a re-emitted polygon: rotate the input outline by `rotation` about its origin, then add (x, y). */
|
|
227
|
+
export interface PlacedNestPart {
|
|
228
|
+
/** The requested name, or the generated default. */
|
|
229
|
+
name: string;
|
|
230
|
+
/** 0-based index into `sheets`. */
|
|
231
|
+
sheet: number;
|
|
232
|
+
/** Translation x (applied after rotation). */
|
|
233
|
+
x: number;
|
|
234
|
+
/** Translation y. */
|
|
235
|
+
y: number;
|
|
236
|
+
/** Degrees, applied about the part's own origin first. May be negative (jagua's own value); the reconstruction is exact regardless of sign. */
|
|
237
|
+
rotation: number;
|
|
238
|
+
/** The material this copy nested from. */
|
|
239
|
+
material?: string;
|
|
240
|
+
/** The part's opaque passthrough. */
|
|
241
|
+
meta?: Record<string, unknown>;
|
|
242
|
+
}
|
|
243
|
+
export interface NestSheet {
|
|
244
|
+
/** Present for rectangular sheets. Always the real sheet size (also with `edgeClearance`). */
|
|
245
|
+
w?: number;
|
|
246
|
+
/** Present for rectangular sheets. Always the real sheet size (also with `edgeClearance`). */
|
|
247
|
+
h?: number;
|
|
248
|
+
/** Present for arbitrary-outline sheets instead of w/h. */
|
|
249
|
+
polygon?: NestRing;
|
|
250
|
+
/** Index into the request `stock[]` this sheet was cut from — the key an ERP uses to tie the sheet back to its stock row. */
|
|
251
|
+
stock: number;
|
|
252
|
+
/** The stock row's price, or null. */
|
|
253
|
+
price: number | null;
|
|
254
|
+
/** This sheet's fill = placed polygon area / sheet area. */
|
|
255
|
+
density: number;
|
|
256
|
+
/** Σ perimeter of the placed parts' outer outlines AND holes on this sheet, 3 decimals, in your unit — the contour length a laser / plasma / waterjet head follows. Geometric: no common-line cutting, lead-in/out or micro-joints (your CAM adds those). */
|
|
257
|
+
cutLength: number;
|
|
258
|
+
/** Pierce points on this sheet: one per closed contour (each placed part's outline + one per hole). */
|
|
259
|
+
pierces: number;
|
|
260
|
+
/** The farthest x any placed part reaches, measured from the sheet origin (0,0), 3 decimals, in your unit — real sheet coordinates (with `edgeClearance` it includes the edge gap). With `usedHeight`: the used bounding box from the origin corner, for partial-sheet charging (full sheet / horizontal cut = usedHeight × w / vertical cut = usedWidth × h / boundary box — the charging rule is yours). A `polygon` sheet: the same, in the polygon's own coordinate frame. */
|
|
261
|
+
usedWidth: number;
|
|
262
|
+
/** The farthest y any placed part reaches, from the sheet origin (see `usedWidth`). */
|
|
263
|
+
usedHeight: number;
|
|
264
|
+
/** `usedWidth × usedHeight` (rounded to 2 decimals). */
|
|
265
|
+
usedArea: number;
|
|
266
|
+
/** Placements on this sheet. */
|
|
267
|
+
parts: Array<PlacedNestPart>;
|
|
268
|
+
/** The zones that applied to this sheet, echoed for rendering. */
|
|
269
|
+
exclusions?: Array<NestZone>;
|
|
270
|
+
/** Present when the sheet carried a material. */
|
|
271
|
+
material?: string;
|
|
272
|
+
/** Echoed from the stock row's meta. */
|
|
273
|
+
meta?: Record<string, unknown>;
|
|
274
|
+
}
|
|
275
|
+
/** Job totals. */
|
|
276
|
+
export interface NestMetrics {
|
|
277
|
+
/** Sheets used. Equals `sheets.length`. ⚠️ The honest, cross-comparable metric between nesting and the rectangular modes. */
|
|
278
|
+
sheetCount: number;
|
|
279
|
+
/** ⚠️ Placed POLYGON area / used sheet area — the honest fill (concave pockets count as empty). NOT comparable to a rectangular packer's `yieldPct`, which counts each bounding box as solid and so reads higher for a worse result; the cross-comparable metric is `sheetCount`. */
|
|
280
|
+
density: number;
|
|
281
|
+
/** Part copies placed. */
|
|
282
|
+
placed: number;
|
|
283
|
+
/** Part copies requested. */
|
|
284
|
+
total: number;
|
|
285
|
+
/** Sum of used sheet prices. */
|
|
286
|
+
totalPrice: number;
|
|
287
|
+
/** Σ `sheets[].cutLength` — placed parts only (outlines + holes), in your unit. */
|
|
288
|
+
cutLength: number;
|
|
289
|
+
/** Σ `sheets[].pierces`. */
|
|
290
|
+
pierces: number;
|
|
291
|
+
}
|
|
292
|
+
/** Sheets used per request stock ROW ("quantity per sheet size"). The unit is the row, not the size: two rows with the same w × h but a different price or meta are two entries. */
|
|
293
|
+
export interface NestStockUsage {
|
|
294
|
+
/** Index into the request `stock[]`. */
|
|
295
|
+
stock: number;
|
|
296
|
+
/** Present for rectangular rows. */
|
|
297
|
+
w?: number;
|
|
298
|
+
/** Present for rectangular rows. */
|
|
299
|
+
h?: number;
|
|
300
|
+
/** Present when the row carries a material. */
|
|
301
|
+
material?: string;
|
|
302
|
+
/** Sheets cut from this row. */
|
|
303
|
+
sheetCount: number;
|
|
304
|
+
/** Σ sheet price (0 for a row without a price, the same rule as `metrics.totalPrice`). */
|
|
305
|
+
totalPrice: number;
|
|
306
|
+
/** Placed polygon area / total area of this row's sheets. */
|
|
307
|
+
density: number;
|
|
308
|
+
/** Σ `sheets[].usedArea` of this row's sheets — the used bounding area to total per sheet size. */
|
|
309
|
+
usedArea: number;
|
|
310
|
+
}
|
|
311
|
+
export interface UnplacedNest {
|
|
312
|
+
/** The part name. */
|
|
313
|
+
name: string;
|
|
314
|
+
/** How many copies could not be placed. */
|
|
315
|
+
qty: number;
|
|
316
|
+
}
|
|
317
|
+
/** One material's totals across the sheets cut from it. */
|
|
318
|
+
export interface NestMaterialSummary {
|
|
319
|
+
/** The tag exactly as you sent it. Free text, matched exactly. */
|
|
320
|
+
material: string;
|
|
321
|
+
/** Sheets cut from this material. */
|
|
322
|
+
sheetCount: number;
|
|
323
|
+
/** This material's own fill: placed polygon area / area of its sheets — a polygon fill, not comparable to the rectangular modes' `yieldPct`. */
|
|
324
|
+
density: number;
|
|
325
|
+
/** Part copies of this material placed. */
|
|
326
|
+
placed: number;
|
|
327
|
+
/** Part copies of this material requested (Σ qty). */
|
|
328
|
+
total: number;
|
|
329
|
+
/** Sum of the prices of the sheets used for this material. */
|
|
330
|
+
totalPrice: number;
|
|
331
|
+
}
|
|
332
|
+
export interface UnmatchedMaterialNest {
|
|
333
|
+
/** The tag that has no stock of its own anywhere in the request. */
|
|
334
|
+
material: string;
|
|
335
|
+
/** The demand rows in that material, in the nest unplaced shape: name, qty. */
|
|
336
|
+
parts: Array<UnplacedNest>;
|
|
337
|
+
}
|
|
338
|
+
export interface OptimizeNestResponse {
|
|
339
|
+
/** The engine that served the request: `lbf` for a synchronous nest, `max` for the result of an async `max` job (read from `GET /v1/jobs/{id}`). */
|
|
340
|
+
engine: "lbf" | "max";
|
|
341
|
+
/** Algorithm version = package version + pinned jagua-rs commit + a hash of the mapping layer. Versions independently of the rectangular engines. */
|
|
342
|
+
engineVersion: string;
|
|
343
|
+
/** Nest contract version, currently `"1"`. Versions independently of the `/v1/` rectangular contract — it is a different path and engine family. */
|
|
344
|
+
contractVersion: string;
|
|
345
|
+
/** `true` for `lbf` (a pinned seed makes the layout reproducible) — unless a comparison of alternative layouts (several stock rows, right angles) hit its time limit on the server (then `false`, with a warning). For `max` it is `false` whenever a time-limited search ran — the search for fewer sheets, or the re-packing of the least-filled sheet (`options.compact`) — and `true` only when the `lbf` pass alone decided the layout (a single-part sheet, `compact: false`). */
|
|
346
|
+
deterministic: boolean;
|
|
347
|
+
/** One entry per used sheet. */
|
|
348
|
+
sheets: Array<NestSheet>;
|
|
349
|
+
metrics: NestMetrics;
|
|
350
|
+
/** One entry per USED request stock row, sorted by `stock`: sheets used, total price and density. Always present. */
|
|
351
|
+
stockUsage: Array<NestStockUsage>;
|
|
352
|
+
/** Demand that could not be placed — a plan-plus-warning, not an error. */
|
|
353
|
+
unplaced: Array<UnplacedNest>;
|
|
354
|
+
/** e.g. unplaced parts, a material with no matching stock, or — for `max` — how the result was reached (for instance that the `lbf` layout was kept because no layout with fewer sheets was found in the time budget). */
|
|
355
|
+
warnings: Array<string>;
|
|
356
|
+
/** Per-material rollup, present only for a material-aware request. */
|
|
357
|
+
materials?: Array<NestMaterialSummary>;
|
|
358
|
+
/** Parts whose material has no matching stock. Present only when it happens. */
|
|
359
|
+
unmatchedMaterials?: Array<UnmatchedMaterialNest>;
|
|
360
|
+
imported?: NestImportSummary;
|
|
361
|
+
/** Additive — a self-contained SVG of the achieved layout, present only when `include` has "svg". */
|
|
362
|
+
svg?: string;
|
|
363
|
+
/** Additive — an R12 DXF of the achieved layout, present only when `include` has "dxf". */
|
|
364
|
+
dxf?: string;
|
|
365
|
+
/** The solve time; environment-dependent. */
|
|
366
|
+
timing: {
|
|
367
|
+
/** Milliseconds spent solving. */
|
|
368
|
+
solveMs: number;
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
export interface Part2D {
|
|
372
|
+
/** Optional label, echoed on every placement. Defaults to `Part <1-based row index>`. */
|
|
373
|
+
name?: string;
|
|
374
|
+
/** Part width. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
375
|
+
w: number;
|
|
376
|
+
/** Part height. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
377
|
+
h: number;
|
|
378
|
+
/** How many identical copies. Expanded server-side into individual pieces; the sum across all rows must not exceed 2000. */
|
|
379
|
+
qty?: number;
|
|
380
|
+
/** May the part be turned 90°? Set false for print-direction parts. For grain, prefer `grain` — it says WHICH side the grain runs along, so the part is turned to match the sheet instead of being frozen in the orientation you happened to send. */
|
|
381
|
+
rotatable?: boolean;
|
|
382
|
+
/** Grain direction: the side of the part the grain runs along (`w` or `h`). On a sheet whose stock row has a `grain`, the part is placed with its grain along the sheet's, turned 90° when it has to be (`rotated: true` on the placement) — `rotatable` is then ignored for it. With `material`, only boards of the part's own material count. No grained stock → no effect, and `warnings` says so. In a matched set a member's own `grain` is not used: the set's `grain` decides, and a `grain:` line in `matchedSets[].warnings` names the members it overrides. */
|
|
383
|
+
grain?: "w" | "h";
|
|
384
|
+
/** DEPRECATED — still accepted and still working exactly as before. Parts sharing a group are kept on one sheet. If a group is too large for one sheet the members are split and a string in `warnings` names the group. It only keeps them on ONE SHEET: it does not place them next to each other, so it gives no continuous grain across them. For fronts whose figure must run on (a drawer front over two doors) use `matchedSets`. @deprecated */
|
|
385
|
+
grainGroup?: string;
|
|
386
|
+
/** Must-cut. Wins board space over non-priority parts when stock is capped — only meaningful together with `options.respectStock`. */
|
|
387
|
+
priority?: boolean;
|
|
388
|
+
/** Per-edge banding. Each side names the banding TYPE as a free-string reference (e.g. `"ABS oak 22×1"`); an absent or empty side is not banded. Library-independent — send your own ERP reference codes and the `edgeBanding` response block groups the metreage by that exact string. Metadata only: it never changes placement, it is summarised post-hoc into linear metres. 2D only. */
|
|
389
|
+
edgeBanding?: {
|
|
390
|
+
/** Type reference for the top edge (runs the part width). */
|
|
391
|
+
top?: string;
|
|
392
|
+
/** Type reference for the right edge (runs the part height). */
|
|
393
|
+
right?: string;
|
|
394
|
+
/** Type reference for the bottom edge (runs the part width). */
|
|
395
|
+
bottom?: string;
|
|
396
|
+
/** Type reference for the left edge (runs the part height). */
|
|
397
|
+
left?: string;
|
|
398
|
+
};
|
|
399
|
+
/** Material identity — a HARD placement constraint. A part of material X is cut ONLY from stock of material X (unlike `edgeBanding`, this changes the layout). Free string, library-independent: send your own ERP code. Absent = the "unspecified" partition (matches only material-less stock). With no part or stock carrying `material`, the whole solve is byte-identical to before. */
|
|
400
|
+
material?: string;
|
|
401
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
402
|
+
meta?: Record<string, unknown>;
|
|
403
|
+
/** The row's id, so a `matchedSets` layout can name it. Any JSON value is accepted here. Only an id a set REFERS TO is checked: it must be a non-empty string of printable characters, at most 64 long, or a safe integer (`7` and `"7"` are the same id), and it must be on exactly one part. Every other id is ignored, whatever it holds (a float, an object, null, repeated, long, empty): always without a non-empty `matchedSets`, and on the rows no set names with one — so an ERP article number is safe to send. */
|
|
404
|
+
id?: unknown;
|
|
405
|
+
}
|
|
406
|
+
/** Parts cut out of ONE piece of board, side by side or one above the other, exactly one kerf apart, so the figure of the veneer runs on across them (a drawer front over two doors). The set travels through the optimizer as one rectangle — the block — and comes back as its members. Every refusal is a 400 before the quota is reserved; a size mismatch is a warning (`matchedSets[].warnings`), not an error. At most 24 members per set; a part can be in one set only. Engines `heuristic` and `balanced` (which does not rank `priority`, so under `respectStock` a set can lose its place there); `max` refuses a request with sets (400 `details.unsupported`). */
|
|
407
|
+
export interface MatchedSet {
|
|
408
|
+
/** Unique among the sets; at most 64 characters. Echoed on every member placement and in `matchedSets[]` of the response (as a string). */
|
|
409
|
+
id: string | number;
|
|
410
|
+
/** Optional label, echoed in the response rollup. Printable characters only. */
|
|
411
|
+
name?: string;
|
|
412
|
+
/** How many instances (three identical cabinets → 3). The parts' `qty` must cover every instance. */
|
|
413
|
+
qty?: number;
|
|
414
|
+
/** The SET's grain: along the block's width (`w`) or height (`h`); it decides the direction of every member (their own `grain` is not used inside the block). Absent/null = no grain: the block may turn when every member is `rotatable`. */
|
|
415
|
+
grain?: "w" | "h" | null;
|
|
416
|
+
/** When the block fits no stock sheet: `split` places the members one by one (with a warning), `unplaced` lists them in `unplaced`. */
|
|
417
|
+
onMiss?: "split" | "unplaced";
|
|
418
|
+
/** The gap between the members ON THE CABINET (default: the kerf). The `align` check measures the lines with it; on the sheet the members still stand one kerf apart, and when the kerf is wider a `gap:` warning says so. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
419
|
+
installGap?: number;
|
|
420
|
+
layout: MatchedSetLayout;
|
|
421
|
+
}
|
|
422
|
+
/** Where each member sits, as lines of `parts[].id` values. `rows`: each line is a row of members side by side, the rows stacked top to bottom (`[["DF"], ["DOOR", "DOOR"]]` = a drawer front over two doors). `cols`: each line is a column of members one above the other, the columns side by side. A part may appear more than once in one set. Positions are numbered in reading order: top to bottom, left to right. */
|
|
423
|
+
export interface MatchedSetLayout {
|
|
424
|
+
/** `rows`: each line is a row of members side by side, the rows stacked top to bottom. `cols`: each line is a column, the columns side by side. */
|
|
425
|
+
dir: "rows" | "cols";
|
|
426
|
+
/** The members, as `parts[].id` values — `[["DF"], ["DOOR", "DOOR"]]` is a drawer front over two doors. */
|
|
427
|
+
lines: Array<Array<string | number>>;
|
|
428
|
+
}
|
|
429
|
+
export interface Stock2D {
|
|
430
|
+
/** Sheet width. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
431
|
+
w: number;
|
|
432
|
+
/** Sheet height. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
433
|
+
h: number;
|
|
434
|
+
/** How many sheets of this format are available. A HARD cap only when `options.respectStock` is true; otherwise the format is treated as unlimited. The sum across all rows must not exceed 5000. */
|
|
435
|
+
qty?: number;
|
|
436
|
+
/** Price per sheet. Drives `metrics.totalPrice` and `options.minimizeCost`. */
|
|
437
|
+
price?: number;
|
|
438
|
+
/** A stock unit of material X serves only material-X parts. Free string; see the part `material`. */
|
|
439
|
+
material?: string;
|
|
440
|
+
/** "Use this stock up first" — a soft preference ranked BELOW sheet/rod count. Steers which format is spent first without spending more of it. */
|
|
441
|
+
priority?: boolean;
|
|
442
|
+
/** Grain direction of this sheet: the side the grain runs along (`w` or `h`). Absent = no grain (MDF, melamine): parts without a `grain` rotate freely on it, while a part WITH a `grain` keeps the orientation the grained rows give it (send `material` to keep grained and plain boards apart). Echoed on every `sheets[]` entry cut from this row. Rows whose grain runs different ways may be mixed; `options.trim` and a forced `options.firstCut` still apply to each sheet in its own row's orientation. */
|
|
443
|
+
grain?: "w" | "h";
|
|
444
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
445
|
+
meta?: Record<string, unknown>;
|
|
446
|
+
}
|
|
447
|
+
/** Per-side edge trim removed before nesting. Asymmetric on purpose: a damaged long edge may need more than the others, and a symmetric trim would then give a wrong plan, not merely a worse one. `left` shifts placements right, `top` shifts them down. The sides are those of each sheet as sent — also on a `grain` row whose grain runs the other way. */
|
|
448
|
+
export interface Trim2D {
|
|
449
|
+
/** Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
450
|
+
left?: number;
|
|
451
|
+
/** Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
452
|
+
right?: number;
|
|
453
|
+
/** Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
454
|
+
top?: number;
|
|
455
|
+
/** Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
456
|
+
bottom?: number;
|
|
457
|
+
}
|
|
458
|
+
export interface Options2D {
|
|
459
|
+
/** Blade width. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
460
|
+
kerf?: number;
|
|
461
|
+
/** Accept a cut that overshoots by up to this much, i.e. allow parts to encroach on the kerf. Turns "two 501 mm parts do not fit a 1000 mm board" into one board. */
|
|
462
|
+
tolerance?: number;
|
|
463
|
+
trim?: Trim2D;
|
|
464
|
+
/** Force the orientation of the first cut across the board. `auto` searches both and also enables two extra strategy families that are not direction-aware. Applies to each sheet as sent, also on a `grain` row whose grain runs the other way. */
|
|
465
|
+
firstCut?: "auto" | "horizontal" | "vertical";
|
|
466
|
+
/** Rank candidate layouts by total stock price instead of sheet count. Needs `price` on the stock rows to mean anything. */
|
|
467
|
+
minimizeCost?: boolean;
|
|
468
|
+
/** Treat each stock row's `qty` as a hard cap. Parts that then do not fit come back in `unplaced` instead of on an extra invented sheet. */
|
|
469
|
+
respectStock?: boolean;
|
|
470
|
+
/** Output filter only: report a leftover rectangle only if its SHORT side is at least this. Does not change the layout. */
|
|
471
|
+
minOffcut?: number;
|
|
472
|
+
/** Panel-saw stage limit — a count of machine PHASES (axis changes), not raw cut-tree depth. A real kitchen job is commonly 9 cuts deep but only 3 stages. An unsatisfiable limit still returns the best plan plus a string in `warnings`; it is never an error. */
|
|
473
|
+
maxCutStages?: number | null;
|
|
474
|
+
/** Prefer layouts that turn fewer parts, at a possible small yield cost. */
|
|
475
|
+
minimizeRotations?: boolean;
|
|
476
|
+
/** Search depth, trading solve time against yield. `balanced` (the default) runs the full multi-strategy best-of. `fast` skips the two expensive refinements — the per-board area-combination search and the tail right-sizing that moves the last boards onto a smaller stock size: markedly faster on large jobs (hundreds of parts) for a small yield cost, while staying guillotine-valid and never denser than `balanced` — its candidate set is a strict subset. On small jobs the two are usually identical. Applies to engine `heuristic`; the `balanced` engine (MaxRects) ignores it. */
|
|
477
|
+
effort?: "fast" | "balanced";
|
|
478
|
+
/** A boolean. With `matchedSets` and the `svg` export: draw the dashed frame and the "SET A" badge of every set instance, in ONE separate group (`<g id="matched-sets" data-role="annotation">`) a laser or CNC import can drop in one move. `false` leaves that group out (the position codes in the part labels stay). Read only in a request with sets — there a value other than `true`, `false` or `null` is a 400; without sets the field is ignored, whatever it holds. */
|
|
479
|
+
setOutlines?: boolean;
|
|
480
|
+
}
|
|
481
|
+
export interface Optimize2DRequest {
|
|
482
|
+
parts: Array<Part2D>;
|
|
483
|
+
stock: Array<Stock2D>;
|
|
484
|
+
options?: Options2D;
|
|
485
|
+
/** Which optimizer to run. `heuristic` is the guillotine multi-strategy packer: highest yield, every layout edge-to-edge saw-cuttable, always a full `cutPlan`. `balanced` is a MaxRects free-nesting packer: much faster on large jobs for a small yield cost, but its layouts are often not guillotine (`guillotineValid: false`, `cutPlan: null`), and it does not model `tolerance`, `minimizeCost`, `grainGroup`, `maxCutStages` or `minimizeRotations` — set one of those and a string in `warnings` tells you it was ignored. `max` is the ASYNCHRONOUS tree-search tier (2D only): it reaches the proven optimum on far more jobs at the cost of seconds-to-a-minute per solve, so instead of a plan it returns **202** with a `jobId` — poll `GET /v1/jobs/{id}` for the result. Still deterministic and guillotine-valid. Where the tier is not enabled on a deployment, `engine: "max"` fails closed with **503**. **What `max` models.** It packs ONE stock format at full sheet size with unlimited supply and a fixed 3-stage guillotine pattern. Anything that would make the returned plan physically WRONG is therefore REFUSED with **400** at submission — before a call is reserved and before you wait for the solve: a second `stock` row, `options.trim`, `options.respectStock`, any `material`, any `grainGroup`, a non-empty `matchedSets` (an empty one is fine). Options that only make the plan less tuned are accepted and listed in `warnings`: `tolerance`, `minimizeCost`, `maxCutStages` (fixed at 3), `minimizeRotations`, `firstCut`, `effort`. A `max` result reports no `offcuts`. Send those jobs to `heuristic`. The default never changes silently. */
|
|
486
|
+
engine?: "heuristic" | "balanced" | "max";
|
|
487
|
+
/** Three kinds of token. NARROWING (`cutPlan`, `offcuts`): omit the field entirely to get both; a present array keeps only the narrowing tokens it lists (an empty array drops both). ADDITIVE EXPORT (`svg`, `csv`, `dxf`): each adds an inline export STRING to the response (`svg` a standalone 2D layout — 2D only, a warning is returned for 1D/wood; `dxf` an R12/AC1009 drawing; `csv` a cut list, RFC 4180 quoted (a part name with a comma or a quote stays one field) and formula-injection-guarded (a name beginning with `=`, `+`, `-`, `@`, TAB or CR is prefixed with an apostrophe so a spreadsheet reads it as text, not a formula — CWE-1236). 2D DATA (`labels`, `planHash`, `pdf`, `labelsPdf`): `labels` adds per-piece label data (`labels[]`, see `Label2D`), `planHash` a 64-hex fingerprint of the plan (the same plan always gives the same hash), `pdf` the cutting-plan PDF and `labelsPdf` the label PDF, both base64-encoded and drawn by the same code as the CutOptim app. These four are 2D only: a 1D or wood request returns a `warnings` entry instead. `pdf` renders at most 500 parts (total quantity); a larger 2D request with `pdf` is a 413 `too_large` with `details.path: "include"`. Export and data tokens do not affect narrowing, so `["svg"]` adds svg and — listing no narrowing token — drops cutPlan/offcuts; use `["cutPlan","offcuts","svg"]` to keep everything. */
|
|
488
|
+
include?: Array<"cutPlan" | "offcuts" | "svg" | "csv" | "dxf" | "labels" | "planHash" | "pdf" | "labelsPdf">;
|
|
489
|
+
/** Matched sets (grain matching): groups of parts cut side by side from one piece of board. Send a NON-EMPTY ARRAY of sets (at most 50); members are named by `parts[].id`. Only a non-empty array switches the feature on: absent, `null`, `[]` — and any value that is not an array — is a request without sets, answered exactly as before (no error). With sets the response adds `sheets[].parts[].matchedSet` on every member and a `matchedSets` rollup; `csv` gains `Set` and `Position` columns at the end, `svg` the `matched-sets` frame group, `dxf` the position code on LABELS; `pdf` and `labelsPdf` draw the members as ordinary parts for now (a `warnings` line says so; the set marks are in `sheets[].parts[].matchedSet`, `csv` and `svg`). Engine `max` refuses a non-empty array (400 `details.unsupported`); an empty one is fine. 2D only: the 1D, wood and nest endpoints ignore this field and `parts[].id`. */
|
|
490
|
+
matchedSets?: Array<MatchedSet>;
|
|
491
|
+
}
|
|
492
|
+
/** A linear part. There is no `rotatable` or `grainGroup` in 1D. */
|
|
493
|
+
export interface Part1D {
|
|
494
|
+
/** Optional label. Defaults to `Part <1-based row index>`. */
|
|
495
|
+
name?: string;
|
|
496
|
+
/** Cut length. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
497
|
+
length: number;
|
|
498
|
+
/** How many copies. The sum across all rows must not exceed 2000. */
|
|
499
|
+
qty?: number;
|
|
500
|
+
/** Must-cut; wins stock when `options.respectStock` caps it. */
|
|
501
|
+
priority?: boolean;
|
|
502
|
+
/** Material identity — a HARD placement constraint. A part of material X is cut ONLY from stock of material X (unlike `edgeBanding`, this changes the layout). Free string, library-independent: send your own ERP code. Absent = the "unspecified" partition (matches only material-less stock). With no part or stock carrying `material`, the whole solve is byte-identical to before. */
|
|
503
|
+
material?: string;
|
|
504
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
505
|
+
meta?: Record<string, unknown>;
|
|
506
|
+
}
|
|
507
|
+
export interface Stock1D {
|
|
508
|
+
/** Bar length. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
509
|
+
length: number;
|
|
510
|
+
/** Bars available. A hard cap only with `options.respectStock`. */
|
|
511
|
+
qty?: number;
|
|
512
|
+
/** Price per bar. */
|
|
513
|
+
price?: number;
|
|
514
|
+
/** A stock unit of material X serves only material-X parts. Free string; see the part `material`. */
|
|
515
|
+
material?: string;
|
|
516
|
+
/** "Use this stock up first" — a soft preference ranked BELOW sheet/rod count. Steers which format is spent first without spending more of it. */
|
|
517
|
+
priority?: boolean;
|
|
518
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
519
|
+
meta?: Record<string, unknown>;
|
|
520
|
+
}
|
|
521
|
+
/** A bar has two ends, not four edges. `start` is the leading trim (it shifts the first part along the bar, so the first `pos` equals `start`); `end` is the trailing trim. */
|
|
522
|
+
export interface Trim1D {
|
|
523
|
+
/** Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
524
|
+
start?: number;
|
|
525
|
+
/** Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
526
|
+
end?: number;
|
|
527
|
+
}
|
|
528
|
+
/** The 1D option set is deliberately smaller: there is no `firstCut`, `maxCutStages` or `minimizeRotations`, because none of them means anything on a bar. */
|
|
529
|
+
export interface Options1D {
|
|
530
|
+
/** Blade width. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
531
|
+
kerf?: number;
|
|
532
|
+
/** Accept cuts overshooting by up to this much. Pieces that only fitted thanks to it are counted in `metrics.toleranceAcceptedCount` and noted in `warnings`. */
|
|
533
|
+
tolerance?: number;
|
|
534
|
+
trim?: Trim1D;
|
|
535
|
+
/** Rank by total bar price. */
|
|
536
|
+
minimizeCost?: boolean;
|
|
537
|
+
/** Treat `qty` as a hard cap. */
|
|
538
|
+
respectStock?: boolean;
|
|
539
|
+
/** Output filter: report a leftover only if it is at least this long. */
|
|
540
|
+
minOffcut?: number;
|
|
541
|
+
}
|
|
542
|
+
export interface Optimize1DRequest {
|
|
543
|
+
parts: Array<Part1D>;
|
|
544
|
+
stock: Array<Stock1D>;
|
|
545
|
+
options?: Options1D;
|
|
546
|
+
/** `max` is not offered here: it is a 2D (and, separately, nesting) tier, and `engine: "max"` on this path is a **400** before a call is charged. In 1D `balanced` resolves to the same packer as `heuristic` — the linear packer is already a best-of-multiple-strategies search, so there is no separate balanced mode. The value is accepted and echoed back so a single client can send one engine id for both endpoints. */
|
|
547
|
+
engine?: "heuristic" | "balanced";
|
|
548
|
+
/** Three kinds of token. NARROWING (`cutPlan`, `offcuts`): omit the field entirely to get both; a present array keeps only the narrowing tokens it lists (an empty array drops both). ADDITIVE EXPORT (`svg`, `csv`, `dxf`): each adds an inline export STRING to the response (`svg` a standalone 2D layout — 2D only, a warning is returned for 1D/wood; `dxf` an R12/AC1009 drawing; `csv` a cut list, RFC 4180 quoted (a part name with a comma or a quote stays one field) and formula-injection-guarded (a name beginning with `=`, `+`, `-`, `@`, TAB or CR is prefixed with an apostrophe so a spreadsheet reads it as text, not a formula — CWE-1236). 2D DATA (`labels`, `planHash`, `pdf`, `labelsPdf`): `labels` adds per-piece label data (`labels[]`, see `Label2D`), `planHash` a 64-hex fingerprint of the plan (the same plan always gives the same hash), `pdf` the cutting-plan PDF and `labelsPdf` the label PDF, both base64-encoded and drawn by the same code as the CutOptim app. These four are 2D only: a 1D or wood request returns a `warnings` entry instead. `pdf` renders at most 500 parts (total quantity); a larger 2D request with `pdf` is a 413 `too_large` with `details.path: "include"`. Export and data tokens do not affect narrowing, so `["svg"]` adds svg and — listing no narrowing token — drops cutPlan/offcuts; use `["cutPlan","offcuts","svg"]` to keep everything. */
|
|
549
|
+
include?: Array<"cutPlan" | "offcuts" | "svg" | "csv" | "dxf" | "labels" | "planHash" | "pdf" | "labelsPdf">;
|
|
550
|
+
}
|
|
551
|
+
/** A timber part. Same as a 1D part plus the CROSS-SECTION, which decides which stock it can come out of at all. */
|
|
552
|
+
export interface PartWood {
|
|
553
|
+
/** Optional label. Defaults to `Part <1-based row index>`. */
|
|
554
|
+
name?: string;
|
|
555
|
+
/** One side of the cross-section. ORDER DOES NOT MATTER — 50×100 and 100×50 are the same bar turned over and land in the same section. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
556
|
+
sw: number;
|
|
557
|
+
/** The other side of the cross-section. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
558
|
+
sh: number;
|
|
559
|
+
/** Cut length. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
560
|
+
length: number;
|
|
561
|
+
/** How many copies. The sum across all rows must not exceed 2000. */
|
|
562
|
+
qty?: number;
|
|
563
|
+
/** Must-cut; wins stock when `options.respectStock` caps it. Applied WITHIN the part's own cross-section, since another section's bars could not have yielded it anyway. */
|
|
564
|
+
priority?: boolean;
|
|
565
|
+
/** Material identity — composes WITH the cross-section: two materials at the same 50×50 become two sections (a 50×50 Pine-Treated part cannot come from a 50×50 Pine-Untreated bar). Free string, library-independent. Absent = today's section-only grouping. */
|
|
566
|
+
material?: string;
|
|
567
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
568
|
+
meta?: Record<string, unknown>;
|
|
569
|
+
}
|
|
570
|
+
export interface StockWood {
|
|
571
|
+
/** Optional stock label, echoed back as `sections[].stockName` so a plan can name the material an operator has to fetch. */
|
|
572
|
+
name?: string;
|
|
573
|
+
/** One side of the cross-section (either order). Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
574
|
+
sw: number;
|
|
575
|
+
/** The other side of the cross-section. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
576
|
+
sh: number;
|
|
577
|
+
/** Bar length. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
578
|
+
length: number;
|
|
579
|
+
/** Bars available. A hard cap only with `options.respectStock`. */
|
|
580
|
+
qty?: number;
|
|
581
|
+
/** Price per bar. */
|
|
582
|
+
price?: number;
|
|
583
|
+
/** A stock unit of material X serves only material-X parts. Free string; see the part `material`. */
|
|
584
|
+
material?: string;
|
|
585
|
+
/** "Use this stock up first" — a soft preference ranked BELOW sheet/rod count. Steers which format is spent first without spending more of it. */
|
|
586
|
+
priority?: boolean;
|
|
587
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
588
|
+
meta?: Record<string, unknown>;
|
|
589
|
+
}
|
|
590
|
+
/** Identical to `Options1D` — a cross-section changes what fits, not how a bar is cut. */
|
|
591
|
+
export interface OptionsWood {
|
|
592
|
+
/** Blade width. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
593
|
+
kerf?: number;
|
|
594
|
+
/** Accept cuts overshooting by up to this much. Counted in `metrics.toleranceAcceptedCount` and noted in `warnings`. */
|
|
595
|
+
tolerance?: number;
|
|
596
|
+
trim?: Trim1D;
|
|
597
|
+
/** Rank by total bar price. */
|
|
598
|
+
minimizeCost?: boolean;
|
|
599
|
+
/** Treat `qty` as a hard cap. Applied PER CROSS-SECTION. */
|
|
600
|
+
respectStock?: boolean;
|
|
601
|
+
/** Output filter: report a leftover only if it is at least this long. */
|
|
602
|
+
minOffcut?: number;
|
|
603
|
+
}
|
|
604
|
+
export interface OptimizeWoodRequest {
|
|
605
|
+
parts: Array<PartWood>;
|
|
606
|
+
/** At most 50 rows. Since every cross-section needs at least one stock row, this is also the practical ceiling on cross-sections in a single job; a wider catalogue should be split into several requests. */
|
|
607
|
+
stock: Array<StockWood>;
|
|
608
|
+
options?: OptionsWood;
|
|
609
|
+
/** As in 1D, `max` is not offered here (a **400** before a call is charged). `balanced` resolves to the same packer as `heuristic`: within a section a timber job is a length problem, and free nesting has nothing to offer it. The value is accepted, echoed back, and a warning states which packer actually ran. */
|
|
610
|
+
engine?: "heuristic" | "balanced";
|
|
611
|
+
/** Three kinds of token. NARROWING (`cutPlan`, `offcuts`): omit the field entirely to get both; a present array keeps only the narrowing tokens it lists (an empty array drops both). ADDITIVE EXPORT (`svg`, `csv`, `dxf`): each adds an inline export STRING to the response (`svg` a standalone 2D layout — 2D only, a warning is returned for 1D/wood; `dxf` an R12/AC1009 drawing; `csv` a cut list, RFC 4180 quoted (a part name with a comma or a quote stays one field) and formula-injection-guarded (a name beginning with `=`, `+`, `-`, `@`, TAB or CR is prefixed with an apostrophe so a spreadsheet reads it as text, not a formula — CWE-1236). 2D DATA (`labels`, `planHash`, `pdf`, `labelsPdf`): `labels` adds per-piece label data (`labels[]`, see `Label2D`), `planHash` a 64-hex fingerprint of the plan (the same plan always gives the same hash), `pdf` the cutting-plan PDF and `labelsPdf` the label PDF, both base64-encoded and drawn by the same code as the CutOptim app. These four are 2D only: a 1D or wood request returns a `warnings` entry instead. `pdf` renders at most 500 parts (total quantity); a larger 2D request with `pdf` is a 413 `too_large` with `details.path: "include"`. Export and data tokens do not affect narrowing, so `["svg"]` adds svg and — listing no narrowing token — drops cutPlan/offcuts; use `["cutPlan","offcuts","svg"]` to keep everything. */
|
|
612
|
+
include?: Array<"cutPlan" | "offcuts" | "svg" | "csv" | "dxf" | "labels" | "planHash" | "pdf" | "labelsPdf">;
|
|
613
|
+
}
|
|
614
|
+
/** One blade movement, in physically executable order (parent before children: you cannot crosscut a strip before you have ripped it off). */
|
|
615
|
+
export interface CutStep {
|
|
616
|
+
/** 0-based index into `sheets` (2D) or `rods` (1D). Named `sheet` in both. */
|
|
617
|
+
sheet: number;
|
|
618
|
+
/** 1-based order within THIS sheet/rod. It RESTARTS at 1 on every sheet — it is not a running number across the whole job. */
|
|
619
|
+
step: number;
|
|
620
|
+
/** `h` = a horizontal cut line: the blade travels along x and separates top from bottom. `v` = a vertical cut line: the blade travels along y and separates left from right. In 1D `axis` is always `v`. */
|
|
621
|
+
axis: "h" | "v";
|
|
622
|
+
/** The blade's LOW-COORDINATE edge, not its centre line: the `y` value for `h`, the `x` value for `v`. The kerf occupies `pos` to `pos + kerf`, i.e. it advances in the direction the coordinate GROWS — downward for `h`, rightward for `v` (see the coordinate system in `info.description`). The material on the low side of the line — above it for `h`, to its left for `v` — is the piece that cut frees. In 1D `pos` is the offset along the bar at which that part is freed, measured from the bar end that `trim.start` trims. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
623
|
+
pos: number;
|
|
624
|
+
/** How far the blade travels for this cut — the extent of the REGION it crosses, not the whole board (the industry convention). Always `0` in 1D: a bar crosscut has no travel distance to report. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
625
|
+
length: number;
|
|
626
|
+
/** 1-based machine pass. Starts at 1 and increments ONLY when the axis flips relative to the parent cut, so ripping a board into six strips is one stage and crosscutting them is the next. This is the panel-saw sense of "3-stage cutting", not tree depth, and it is what `options.maxCutStages` constrains. */
|
|
627
|
+
stage: number;
|
|
628
|
+
}
|
|
629
|
+
/** A part on a board. `x`/`y` is its top-left corner measured from the top-left corner of the sheet, `x` growing right and `y` growing downward. */
|
|
630
|
+
export interface PlacedPart2D {
|
|
631
|
+
/** The requested `name`, or the generated default `Part <1-based row index>`. */
|
|
632
|
+
name: string;
|
|
633
|
+
/** Distance from the sheet's left edge. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
634
|
+
x: number;
|
|
635
|
+
/** Distance from the sheet's top edge. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
636
|
+
y: number;
|
|
637
|
+
/** PLACED width — already swapped with the requested height when `rotated` is true. No client-side swap needed. */
|
|
638
|
+
w: number;
|
|
639
|
+
/** PLACED height. See `w`. */
|
|
640
|
+
h: number;
|
|
641
|
+
/** True if the part was turned 90° from the requested `w`×`h` — also when its `grain` made the turn necessary. Informational: `w`/`h` here already reflect it. */
|
|
642
|
+
rotated: boolean;
|
|
643
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
644
|
+
meta?: Record<string, unknown>;
|
|
645
|
+
matchedSet?: PlacedMatchedSet;
|
|
646
|
+
}
|
|
647
|
+
/** Present ONLY on a placed member of a matched set (never in a request without `matchedSets`): which set and instance it belongs to and where in it. `position` counts the members in reading order on the cabinet front — top to bottom, left to right — from 1; with the set's `letter` it is the code the `csv` `Position` column and the `svg` labels print (A1, A2 …). */
|
|
648
|
+
export interface PlacedMatchedSet {
|
|
649
|
+
/** The set's `id`, as a string. */
|
|
650
|
+
id: string;
|
|
651
|
+
/** Which instance of the set (1 … `qty`). */
|
|
652
|
+
instance: number;
|
|
653
|
+
/** The member's place in reading order, from 1. */
|
|
654
|
+
position: number;
|
|
655
|
+
/** Where it sits, when that is unambiguous (a row or column of 2–3 plain members). Absent otherwise. */
|
|
656
|
+
hint?: "left" | "middle" | "right" | "top" | "bottom";
|
|
657
|
+
}
|
|
658
|
+
/** One requested matched set: what became of its instances. Present ONLY in a request with sets. */
|
|
659
|
+
export interface MatchedSetSummary {
|
|
660
|
+
/** The set's `id`, as a string. */
|
|
661
|
+
id: string;
|
|
662
|
+
/** A, B … in request order — the letter of the `csv` `Set` column and the `svg` frame badge. */
|
|
663
|
+
letter: string;
|
|
664
|
+
/** The set's `name`, when one was sent. */
|
|
665
|
+
name?: string;
|
|
666
|
+
/** Instances asked for (`qty`). */
|
|
667
|
+
requested: number;
|
|
668
|
+
/** Instances cut as ONE block, members side by side on a sheet. */
|
|
669
|
+
asBlock: number;
|
|
670
|
+
/** Instances whose block fit no stock sheet, so the members were placed one by one (`onMiss: "split"`) — or, in a material-aware job with NO stock of the set's material, went to `unmatchedMaterials` like every part of that material. */
|
|
671
|
+
split: number;
|
|
672
|
+
/** Instances whose members are listed in `unplaced` (`onMiss: "unplaced"`, or no room left on the sheets) — or in `unmatchedMaterials` when the job has no stock of the set's material. */
|
|
673
|
+
unplaced: number;
|
|
674
|
+
/** The block in the set's own frame: the members plus one kerf between them. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
675
|
+
block: {
|
|
676
|
+
w: number;
|
|
677
|
+
h: number;
|
|
678
|
+
};
|
|
679
|
+
/** One line per problem, each starting with its code: `align:` (the lines are not flush), `gap:` (the kerf is wider than `installGap`; both judged at 1/64 of your unit or a quarter kerf, whichever is smaller), `grain:` (the set's grain overrides the own grain of the members it names), `split:` / `unplaced:` (the block did not go on a sheet), `unmatched:` (no stock of the set's material), `priority:` (it may lose its place under `respectStock`), `grainGroup:` (the members are in different grainGroups). Empty when all is well. */
|
|
680
|
+
warnings: Array<string>;
|
|
681
|
+
}
|
|
682
|
+
/** A leftover rectangle, in the same coordinate system as the placements. */
|
|
683
|
+
export interface Rect {
|
|
684
|
+
/** Distance of the leftover's top-left corner from the sheet's left edge. */
|
|
685
|
+
x: number;
|
|
686
|
+
/** Distance of the leftover's top-left corner from the sheet's top edge. */
|
|
687
|
+
y: number;
|
|
688
|
+
/** Width of the leftover. */
|
|
689
|
+
w: number;
|
|
690
|
+
/** Height of the leftover. */
|
|
691
|
+
h: number;
|
|
692
|
+
}
|
|
693
|
+
export interface Sheet {
|
|
694
|
+
/** FULL sheet width as supplied in `stock`, trim included — not the usable width. */
|
|
695
|
+
w: number;
|
|
696
|
+
/** FULL sheet height, trim included. */
|
|
697
|
+
h: number;
|
|
698
|
+
/** The `price` of the stock row this sheet came from, or `null` if none was given. */
|
|
699
|
+
price: number | null;
|
|
700
|
+
/** Placements on this sheet. */
|
|
701
|
+
parts: Array<PlacedPart2D>;
|
|
702
|
+
/** Usable leftovers, filtered by `options.minOffcut`. ABSENT (not empty) when `include` excludes `offcuts`. */
|
|
703
|
+
offcuts?: Array<Rect>;
|
|
704
|
+
/** The material this sheet (and every part/offcut on it) was cut from. Absent when the job carries no material. */
|
|
705
|
+
material?: string;
|
|
706
|
+
/** The stock row's `grain`, echoed: which side of this sheet the grain runs along; every grained part on it lies with its grain along that side. Absent when the row has none. */
|
|
707
|
+
grain?: "w" | "h";
|
|
708
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
709
|
+
meta?: Record<string, unknown>;
|
|
710
|
+
}
|
|
711
|
+
/** Aggregate numbers for the whole job. */
|
|
712
|
+
export interface Metrics2D {
|
|
713
|
+
/** Sheets used. Equals `sheets.length`. */
|
|
714
|
+
sheetCount: number;
|
|
715
|
+
/** Placed part area ÷ total FULL sheet area × 100, rounded to 2 decimals. Trim and kerf count as waste, which is why a trimmed job scores lower than a bare area sum. */
|
|
716
|
+
yieldPct: number;
|
|
717
|
+
/** Pieces placed, after `qty` expansion. */
|
|
718
|
+
placed: number;
|
|
719
|
+
/** Pieces requested, after `qty` expansion. */
|
|
720
|
+
total: number;
|
|
721
|
+
/** Collinear cuts merged: same axis, same coordinate, same stage count once. Physically "how many fence settings the operator makes". Deliberately NOT claimed to equal any competitor's count — only measured to land in the same range. */
|
|
722
|
+
cutLines: number;
|
|
723
|
+
/** Every cut, one per strip crossed — how many separate passes the saw makes. */
|
|
724
|
+
sawPasses: number;
|
|
725
|
+
/** Total distance sawn, summed over every cut, rounded to 3 decimals. Convention-independent: merging collinear cuts does not shorten the material. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
726
|
+
cutLength: number;
|
|
727
|
+
/** Sum of the used sheets’ prices, rounded to 2 decimals. `0` when no stock row carried a `price`. */
|
|
728
|
+
totalPrice: number;
|
|
729
|
+
}
|
|
730
|
+
/** Parts that did not fit, aggregated back by name + dimensions. */
|
|
731
|
+
export interface Unplaced2D {
|
|
732
|
+
/** The part name. */
|
|
733
|
+
name: string;
|
|
734
|
+
/** As requested. */
|
|
735
|
+
w: number;
|
|
736
|
+
/** As requested. */
|
|
737
|
+
h: number;
|
|
738
|
+
/** How many of this part are unplaced. */
|
|
739
|
+
qty: number;
|
|
740
|
+
}
|
|
741
|
+
export interface Timing {
|
|
742
|
+
/** Wall-clock milliseconds spent inside the packer, rounded to 2 decimals. Excludes parsing, authorization and queueing. */
|
|
743
|
+
solveMs: number;
|
|
744
|
+
}
|
|
745
|
+
export interface EdgeBandingByType {
|
|
746
|
+
/** The caller-supplied type reference this metreage is grouped under. */
|
|
747
|
+
reference: string;
|
|
748
|
+
/** Linear metres of this reference, rounded to 3 decimals. See `EdgeBanding` for the millimetre assumption. */
|
|
749
|
+
meters: number;
|
|
750
|
+
}
|
|
751
|
+
export interface EdgeBandingByPart {
|
|
752
|
+
/** The part label, or `Part <1-based row>` when the request omitted a name. */
|
|
753
|
+
name: string;
|
|
754
|
+
/** Total banded metres for this part row, already multiplied by `qty`, rounded to 3 decimals. */
|
|
755
|
+
meters: number;
|
|
756
|
+
/** This part's metreage split by reference. */
|
|
757
|
+
byType: Array<EdgeBandingByType>;
|
|
758
|
+
}
|
|
759
|
+
export interface LabelEdge {
|
|
760
|
+
/** The banding type reference exactly as sent in `edgeBanding`. */
|
|
761
|
+
reference: string;
|
|
762
|
+
/** Length of this edge on one piece: the entered `w` for top/bottom, the entered `h` for left/right. */
|
|
763
|
+
length: number;
|
|
764
|
+
}
|
|
765
|
+
/** Label data for one placed piece. `w`/`h` are the ENTERED size, `rotated` says the piece was turned 90 degrees on the sheet, `x`/`y` equal the placed position in `sheets[sheet].parts`. `edges` are in the entered orientation and list only banded sides. */
|
|
766
|
+
export interface Label2D {
|
|
767
|
+
/** Index into `sheets`. */
|
|
768
|
+
sheet: number;
|
|
769
|
+
/** The part name, as on the placed piece in `sheets[sheet].parts`. */
|
|
770
|
+
name: string;
|
|
771
|
+
/** 1-based row of the request `parts` array this piece came from. */
|
|
772
|
+
partNo: number;
|
|
773
|
+
/** The ENTERED width — not swapped when the piece was rotated on the sheet. */
|
|
774
|
+
w: number;
|
|
775
|
+
/** The ENTERED height — not swapped when the piece was rotated on the sheet. */
|
|
776
|
+
h: number;
|
|
777
|
+
/** True when the piece was turned 90 degrees on the sheet. */
|
|
778
|
+
rotated: boolean;
|
|
779
|
+
/** Placed position on the sheet, the same as the piece in `sheets[sheet].parts`. */
|
|
780
|
+
x: number;
|
|
781
|
+
/** Placed position on the sheet, the same as the piece in `sheets[sheet].parts`. */
|
|
782
|
+
y: number;
|
|
783
|
+
/** Material of the sheet it was cut from; absent when unspecified. */
|
|
784
|
+
material?: string;
|
|
785
|
+
/** The part's own grain axis as entered; absent when none. */
|
|
786
|
+
grain?: "w" | "h";
|
|
787
|
+
/** Banded sides only, in the entered orientation (a `LabelEdge` each); absent when the part has no banding. */
|
|
788
|
+
edges?: {
|
|
789
|
+
top?: LabelEdge;
|
|
790
|
+
right?: LabelEdge;
|
|
791
|
+
bottom?: LabelEdge;
|
|
792
|
+
left?: LabelEdge;
|
|
793
|
+
};
|
|
794
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
795
|
+
meta?: Record<string, unknown>;
|
|
796
|
+
}
|
|
797
|
+
/** Linear metres of edge banding the order consumes, grouped by the caller-supplied type reference and reported per part AND as an order total. Present ONLY when at least one part requested banding, so a banding-free response omits the key. Exact geometry, no waste margin — the workshop adds its own. ⚠️ The metres ASSUME MILLIMETRE input: top and bottom edges run the part width, left and right run the height, multiplied by qty, divided by 1000. This is the one response figure named for a unit (edge banding is bought by the metre, and the division only means metres in millimetres); in another unit the raw edge length is w/h × qty and these numbers are that divided by 1000. 2D only. */
|
|
798
|
+
export interface EdgeBanding {
|
|
799
|
+
/** Order-wide total across every banded edge, rounded to 3 decimals. */
|
|
800
|
+
totalMeters: number;
|
|
801
|
+
/** Order total split by reference, sorted by reference. */
|
|
802
|
+
byType: Array<EdgeBandingByType>;
|
|
803
|
+
/** One entry per requested part row that has any banded edge. */
|
|
804
|
+
byPart: Array<EdgeBandingByPart>;
|
|
805
|
+
}
|
|
806
|
+
export interface Optimize2DResponse {
|
|
807
|
+
/** Which engine actually ran. `max` appears only inside the `result` of a finished async job (GET /v1/jobs/{id}), never in a synchronous 200. */
|
|
808
|
+
engine: "heuristic" | "balanced" | "max";
|
|
809
|
+
/** Algorithm identity: the package version plus a content hash of the algorithm source. Changes automatically whenever the packer changes, and independently per engine. Pin or compare this if you cache. */
|
|
810
|
+
engineVersion: string;
|
|
811
|
+
/** Shape version, matching the `/v1/` in the path. Currently `"1"`. */
|
|
812
|
+
contractVersion: string;
|
|
813
|
+
/** Always true. Present so a client can assert the guarantee it relies on. */
|
|
814
|
+
deterministic: true;
|
|
815
|
+
/** One entry per sheet used, in cutting order. */
|
|
816
|
+
sheets: Array<Sheet>;
|
|
817
|
+
metrics: Metrics2D;
|
|
818
|
+
/** The sawing plan, ordered. `null` when `guillotineValid` is false — a non-guillotine layout has no edge-to-edge cut sequence, so there is nothing honest to return. ABSENT entirely when `include` excludes `cutPlan`. In 2D `step` restarts per sheet. */
|
|
819
|
+
cutPlan?: Array<CutStep> | null;
|
|
820
|
+
/** Parts that did not fit, aggregated by name + size. Empty when everything fitted. */
|
|
821
|
+
unplaced: Array<Unplaced2D>;
|
|
822
|
+
/** Human-readable notes about the plan: unplaced parts, an unsatisfiable `maxCutStages`, a grain group split across sheets, a non-guillotine layout, options the chosen engine does not model, or — in a request with `matchedSets` — a set not cut as one block (the detail per set is in `matchedSets[].warnings`) and the `pdf` / `labelsPdf` note below. Free text — do not parse it; branch on `unplaced`, `guillotineValid`, `metrics` and `matchedSets` instead. */
|
|
823
|
+
warnings: Array<string>;
|
|
824
|
+
/** True when the layout can be produced with edge-to-edge cuts, i.e. on a panel saw. False is information, not an error: the nesting is denser but a panel saw cannot make it. `heuristic` is always true; `balanced` often is not. */
|
|
825
|
+
guillotineValid: boolean;
|
|
826
|
+
edgeBanding?: EdgeBanding;
|
|
827
|
+
/** Per-material rollup (sheet count, yield, placed, requested total, price). The flat `sheets` stay the primary output — each carries `material`. ABSENT for a material-free job, which stays byte-identical. */
|
|
828
|
+
materials?: Array<MaterialSummary2D>;
|
|
829
|
+
/** Demand whose material has NO matching-material stock (distinct from `unplaced` = did not fit; the fix is to declare/buy the material). ABSENT when there is no mismatch. */
|
|
830
|
+
unmatchedMaterials?: Array<UnmatchedMaterial2D>;
|
|
831
|
+
/** One entry per requested matched set, in request order. ABSENT for a request without sets. */
|
|
832
|
+
matchedSets?: Array<MatchedSetSummary>;
|
|
833
|
+
/** Inline SVG of the 2D layout (self-contained, no external references). Present ONLY when `include` contains `svg`. 2D only. With `matchedSets` the part labels carry the position code (A1, A2 …) and a `matched-sets` group draws each set's frame (`options.setOutlines: false` leaves the group out). */
|
|
834
|
+
svg?: string;
|
|
835
|
+
/** Inline CSV cut list. Present ONLY when `include` contains `csv`. With `matchedSets` two columns, `Set` and `Position`, are appended at the end of every row. */
|
|
836
|
+
csv?: string;
|
|
837
|
+
/** Inline DXF (R12/AC1009) on layers STOCK/PARTS/LABELS. Present ONLY when `include` contains `dxf`. With `matchedSets` the LABELS text of a member carries its position code. */
|
|
838
|
+
dxf?: string;
|
|
839
|
+
/** Per-piece label data, one entry per placed piece in plan order. Present ONLY when `include` contains `labels`. 2D only. */
|
|
840
|
+
labels?: Array<Label2D>;
|
|
841
|
+
/** sha256 hex fingerprint (64 hex characters) of the canonical plan: the same plan always gives the same hash. Present ONLY when `include` contains `planHash`. 2D only. */
|
|
842
|
+
planHash?: string;
|
|
843
|
+
/** Worksheet (cutting-plan) PDF of this plan, base64, drawn by the same code as the CutOptim app. Present ONLY when `include` contains `pdf`; at most 500 parts (total quantity), else 413 `too_large`. 2D only. With `matchedSets` the members are drawn as ordinary parts, with no set frame or position code, and `warnings` says so — the set marks are in `sheets[].parts[].matchedSet`, `csv` and `svg`. */
|
|
844
|
+
pdf?: string;
|
|
845
|
+
/** Label PDF of this plan, base64. Present ONLY when `include` contains `labelsPdf`. 2D only. With `matchedSets` the members get ordinary labels, with no position code, and `warnings` says so. */
|
|
846
|
+
labelsPdf?: string;
|
|
847
|
+
timing: Timing;
|
|
848
|
+
}
|
|
849
|
+
/** One material's totals across the sheets cut from it. */
|
|
850
|
+
export interface MaterialSummary2D {
|
|
851
|
+
/** The tag exactly as you sent it. Free text, matched exactly. */
|
|
852
|
+
material: string;
|
|
853
|
+
/** Sheets cut from this material. */
|
|
854
|
+
sheetCount: number;
|
|
855
|
+
/** Used area ÷ total area of THIS material's sheets, %. */
|
|
856
|
+
yieldPct: number;
|
|
857
|
+
/** Parts of this material placed. */
|
|
858
|
+
placed: number;
|
|
859
|
+
/** Parts of this material requested (Σ qty). */
|
|
860
|
+
total: number;
|
|
861
|
+
/** Sum of the prices of the sheets used for this material. */
|
|
862
|
+
totalPrice: number;
|
|
863
|
+
}
|
|
864
|
+
/** Demand of a material for which the job supplied ZERO stock of that material. */
|
|
865
|
+
export interface UnmatchedMaterial2D {
|
|
866
|
+
/** The tag that has no stock of its own anywhere in the request. */
|
|
867
|
+
material: string;
|
|
868
|
+
/** The demand rows in that material, in the 2D unplaced shape: name, w, h, qty. */
|
|
869
|
+
parts: Array<Unplaced2D>;
|
|
870
|
+
}
|
|
871
|
+
export interface PlacedPart1D {
|
|
872
|
+
/** The requested name, or the generated default. */
|
|
873
|
+
name: string;
|
|
874
|
+
/** Offset of the part’s NEAR end from the bar end that `trim.start` trims. The first part therefore starts at `trim.start`, and each subsequent `pos` adds one kerf. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side. */
|
|
875
|
+
pos: number;
|
|
876
|
+
/** The part length, as requested. */
|
|
877
|
+
length: number;
|
|
878
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
879
|
+
meta?: Record<string, unknown>;
|
|
880
|
+
}
|
|
881
|
+
export interface Rod {
|
|
882
|
+
/** FULL bar length as supplied in `stock`. */
|
|
883
|
+
length: number;
|
|
884
|
+
/** Price of the stock row, or `null`. */
|
|
885
|
+
price: number | null;
|
|
886
|
+
/** USABLE off-cut left on this bar, rounded to 3 decimals: the kerf of the cut that frees it from the last piece is already deducted, so it is the reclaimable length, not the raw gap. Reported even when it is below `options.minOffcut` — `minOffcut` filters `offcuts`, not this. */
|
|
887
|
+
remaining: number;
|
|
888
|
+
/** In cutting order along the bar. */
|
|
889
|
+
parts: Array<PlacedPart1D>;
|
|
890
|
+
/** At most one entry: `[remaining]` when it is greater than 0 and at least `options.minOffcut`, otherwise `[]`. ABSENT when `include` excludes `offcuts`. */
|
|
891
|
+
offcuts?: Array<number>;
|
|
892
|
+
/** The material this rod was cut from. Absent when the job carries no material. */
|
|
893
|
+
material?: string;
|
|
894
|
+
/** Opaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order. */
|
|
895
|
+
meta?: Record<string, unknown>;
|
|
896
|
+
}
|
|
897
|
+
export interface Metrics1D {
|
|
898
|
+
/** Bars used. Equals `rods.length`. */
|
|
899
|
+
rodCount: number;
|
|
900
|
+
/** Placed length ÷ total FULL bar length × 100, rounded to 2 decimals. Trim and kerf count as waste. */
|
|
901
|
+
yieldPct: number;
|
|
902
|
+
/** Pieces placed, after `qty` expansion. */
|
|
903
|
+
placed: number;
|
|
904
|
+
/** Pieces requested, after `qty` expansion. */
|
|
905
|
+
total: number;
|
|
906
|
+
/** Total crosscuts across all used bars — one per placed piece, since each piece is freed by a cut at its far end. There is no length to report for a bar cut, which is why 1D has no `cutLength`. */
|
|
907
|
+
cuts: number;
|
|
908
|
+
/** Sum of used bar prices, rounded to 2 decimals. */
|
|
909
|
+
totalPrice: number;
|
|
910
|
+
/** How many pieces fitted only because `options.tolerance` allowed an overshoot. */
|
|
911
|
+
toleranceAcceptedCount: number;
|
|
912
|
+
}
|
|
913
|
+
export interface Unplaced1D {
|
|
914
|
+
/** The part name. */
|
|
915
|
+
name: string;
|
|
916
|
+
/** As requested. */
|
|
917
|
+
length: number;
|
|
918
|
+
/** How many could not be placed. */
|
|
919
|
+
qty: number;
|
|
920
|
+
}
|
|
921
|
+
export interface Optimize1DResponse {
|
|
922
|
+
engine: "heuristic" | "balanced";
|
|
923
|
+
engineVersion: string;
|
|
924
|
+
contractVersion: string;
|
|
925
|
+
deterministic: true;
|
|
926
|
+
rods: Array<Rod>;
|
|
927
|
+
metrics: Metrics1D;
|
|
928
|
+
/** One step per placed piece. `sheet` is the ROD index and `step` restarts at 1 on each rod; `axis` is always `v`, `length` always 0 and `stage` always 1. ABSENT when `include` excludes `cutPlan`. Unlike 2D it is never `null` in practice, because a linear plan is always producible. */
|
|
929
|
+
cutPlan?: Array<CutStep> | null;
|
|
930
|
+
unplaced: Array<Unplaced1D>;
|
|
931
|
+
warnings: Array<string>;
|
|
932
|
+
/** Per-material rollup (rod count, yield, placed, requested total, price). Each rod carries `material`. ABSENT for a material-free job. */
|
|
933
|
+
materials?: Array<MaterialSummary1D>;
|
|
934
|
+
/** Demand whose material has NO matching-material stock (distinct from `unplaced`). ABSENT when there is no mismatch. */
|
|
935
|
+
unmatchedMaterials?: Array<UnmatchedMaterial1D>;
|
|
936
|
+
/** Inline CSV cut list. Present ONLY when `include` contains `csv`. */
|
|
937
|
+
csv?: string;
|
|
938
|
+
/** Inline DXF (R12/AC1009). Present ONLY when `include` contains `dxf`. (`svg` is 2D only — a warning is returned instead.) */
|
|
939
|
+
dxf?: string;
|
|
940
|
+
timing: Timing;
|
|
941
|
+
}
|
|
942
|
+
/** One material's totals across the rods cut from it. */
|
|
943
|
+
export interface MaterialSummary1D {
|
|
944
|
+
/** The tag exactly as you sent it. Free text, matched exactly. */
|
|
945
|
+
material: string;
|
|
946
|
+
/** Bars cut from this material. */
|
|
947
|
+
rodCount: number;
|
|
948
|
+
/** Placed length ÷ total bar length of THIS material's rods, %. */
|
|
949
|
+
yieldPct: number;
|
|
950
|
+
/** Parts of this material placed. */
|
|
951
|
+
placed: number;
|
|
952
|
+
/** Parts of this material requested (Σ qty). */
|
|
953
|
+
total: number;
|
|
954
|
+
/** Sum of the prices of the bars used for this material. */
|
|
955
|
+
totalPrice: number;
|
|
956
|
+
}
|
|
957
|
+
/** Demand of a material for which the job supplied ZERO stock of that material. */
|
|
958
|
+
export interface UnmatchedMaterial1D {
|
|
959
|
+
/** The tag that has no stock of its own anywhere in the request. */
|
|
960
|
+
material: string;
|
|
961
|
+
/** The demand rows in that material, in the 1D unplaced shape: name, length, qty. */
|
|
962
|
+
parts: Array<Unplaced1D>;
|
|
963
|
+
}
|
|
964
|
+
/** A `CutStep` that also names its cross-section. ⚠️ `sheet` is the rod index WITHIN that section, not a job-wide rod counter — pair `section` + `sheet` to locate the bar. */
|
|
965
|
+
export type WoodCutStep = (CutStep);
|
|
966
|
+
/** One cross-section's own totals — no need to re-derive them from `rods`. */
|
|
967
|
+
export interface MetricsSection {
|
|
968
|
+
/** Bars used in this section. */
|
|
969
|
+
rodCount: number;
|
|
970
|
+
/** Placed length ÷ full bar length × 100 FOR THIS SECTION, 2 decimals. */
|
|
971
|
+
yieldPct: number;
|
|
972
|
+
/** Pieces of this section placed, after `qty` expansion. */
|
|
973
|
+
placed: number;
|
|
974
|
+
/** Pieces of this section requested, after `qty` expansion. */
|
|
975
|
+
total: number;
|
|
976
|
+
/** Crosscuts in this section — one per placed piece. */
|
|
977
|
+
cuts: number;
|
|
978
|
+
/** Sum of this section's used bar prices, 2 decimals. */
|
|
979
|
+
totalPrice: number;
|
|
980
|
+
}
|
|
981
|
+
export interface WoodSection {
|
|
982
|
+
/** Normalised key `<short>x<long>`, e.g. `50x100` — short side first, so 50×100 and 100×50 are one section. */
|
|
983
|
+
section: string;
|
|
984
|
+
/** Short side of the cross-section (normalised). */
|
|
985
|
+
sw: number;
|
|
986
|
+
/** Long side of the cross-section (normalised). */
|
|
987
|
+
sh: number;
|
|
988
|
+
/** Label of the stock row matched to this section, or `null` if none was given. */
|
|
989
|
+
stockName: string | null;
|
|
990
|
+
/** The section's material. Two materials at one cross-section are TWO sections. Absent when the job carries no material. */
|
|
991
|
+
material?: string;
|
|
992
|
+
/** Identical shape to a 1D rod. */
|
|
993
|
+
rods: Array<Rod>;
|
|
994
|
+
metrics: MetricsSection;
|
|
995
|
+
/** Parts of THIS section that had stock but did not fit in it. */
|
|
996
|
+
unplaced: Array<Unplaced1D>;
|
|
997
|
+
}
|
|
998
|
+
/** Demand for a cross-section you supplied NO stock for. Deliberately separate from `unplaced`: the fix is to declare or buy the material, not to add length or relax a limit. */
|
|
999
|
+
export interface WoodUnmatched {
|
|
1000
|
+
/** The cross-section key nothing in stock matched. */
|
|
1001
|
+
section: string;
|
|
1002
|
+
/** That cross-section's two sides (normalised: short, long). */
|
|
1003
|
+
sw: number;
|
|
1004
|
+
/** That cross-section's two sides (normalised: short, long). */
|
|
1005
|
+
sh: number;
|
|
1006
|
+
/** Set when the section exists but only in a DIFFERENT material. */
|
|
1007
|
+
material?: string;
|
|
1008
|
+
/** The demand rows in that section, in the 1D unplaced shape: name, length, qty. */
|
|
1009
|
+
parts: Array<Unplaced1D>;
|
|
1010
|
+
/** Total pieces in this section, summed over `parts`. */
|
|
1011
|
+
qty: number;
|
|
1012
|
+
}
|
|
1013
|
+
/** Job-wide totals across every section. */
|
|
1014
|
+
export interface MetricsWood {
|
|
1015
|
+
/** Cross-sections that were solved. Equals `sections.length`; excludes `unmatched` sections. */
|
|
1016
|
+
sectionCount: number;
|
|
1017
|
+
/** Bars used across every section. */
|
|
1018
|
+
rodCount: number;
|
|
1019
|
+
/** Job-wide placed length ÷ full bar length × 100, 2 decimals. Computed over the bars actually used, so an `unmatched` section neither helps nor hurts it. */
|
|
1020
|
+
yieldPct: number;
|
|
1021
|
+
/** Pieces placed across every section. */
|
|
1022
|
+
placed: number;
|
|
1023
|
+
/** Every piece requested — INCLUDING those in `unmatched` sections. */
|
|
1024
|
+
total: number;
|
|
1025
|
+
/** Crosscuts across every used bar. */
|
|
1026
|
+
cuts: number;
|
|
1027
|
+
/** Sum of the used bars’ prices, 2 decimals. */
|
|
1028
|
+
totalPrice: number;
|
|
1029
|
+
/** How many pieces fitted only because `options.tolerance` allowed an overshoot. */
|
|
1030
|
+
toleranceAcceptedCount: number;
|
|
1031
|
+
}
|
|
1032
|
+
export interface OptimizeWoodResponse {
|
|
1033
|
+
engine: "heuristic" | "balanced";
|
|
1034
|
+
engineVersion: string;
|
|
1035
|
+
contractVersion: string;
|
|
1036
|
+
deterministic: true;
|
|
1037
|
+
/** Replaces `rods` at the top level: one entry per cross-section that had matching stock, in request order — each matched to its own stock and solved on its own. */
|
|
1038
|
+
sections: Array<WoodSection>;
|
|
1039
|
+
unmatched: Array<WoodUnmatched>;
|
|
1040
|
+
metrics: MetricsWood;
|
|
1041
|
+
/** One step per placed piece across all sections — as 1D, except every step also carries `section`, and `sheet` is the rod index WITHIN that section, not a job-wide counter. ABSENT when `include` excludes `cutPlan`. Never `null` in practice — a linear plan is always producible. */
|
|
1042
|
+
cutPlan?: Array<WoodCutStep> | null;
|
|
1043
|
+
/** Did-not-fit parts aggregated across sections that DID have stock. Parts with no stock at all are in `unmatched` instead. */
|
|
1044
|
+
unplaced: Array<Unplaced1D>;
|
|
1045
|
+
warnings: Array<string>;
|
|
1046
|
+
/** Inline CSV cut list. Present ONLY when `include` contains `csv`. */
|
|
1047
|
+
csv?: string;
|
|
1048
|
+
/** Inline DXF (R12/AC1009). Present ONLY when `include` contains `dxf`. (`svg` is 2D only — a warning is returned instead.) */
|
|
1049
|
+
dxf?: string;
|
|
1050
|
+
timing: Timing;
|
|
1051
|
+
}
|
|
1052
|
+
export interface UsageResponse {
|
|
1053
|
+
/** The tier slug frozen onto the key when it was minted. Re-pricing the product later does not rewrite a live key. */
|
|
1054
|
+
plan: string;
|
|
1055
|
+
/** Calls counted in the current UTC calendar month, across EVERY key on the account — revoked keys included, so revoking a key cannot un-spend what it spent (053) — not just the one you called with. The quota is account-wide. */
|
|
1056
|
+
used: number;
|
|
1057
|
+
/** The monthly cap for the ACCOUNT: the largest monthly quota frozen onto any of its ACTIVE keys (049). All active keys draw on this one allowance; creating more keys does not create more quota. Keys are for separating environments and integrations (and for revoking one without disturbing the rest). */
|
|
1058
|
+
limit: number;
|
|
1059
|
+
/** `limit` − `used`, never negative. */
|
|
1060
|
+
remaining: number;
|
|
1061
|
+
/** First day of next month as `YYYY-MM-DD` — a DATE, not a timestamp. The counter resets at 00:00 UTC on that day. */
|
|
1062
|
+
periodEnd: string;
|
|
1063
|
+
/** Non-secret display prefix of the calling key, so you can tell which key you queried. The key itself is never returned by any endpoint. */
|
|
1064
|
+
keyPrefix: string;
|
|
1065
|
+
/** Shape version. Currently `"1"`. */
|
|
1066
|
+
contractVersion: string;
|
|
1067
|
+
/** Present (`"test"`) only for a TEST key: created in the dashboard, never billed, with its own monthly allowance separate from the live keys. Absent for a live key — a live key gets exactly the body it always got. */
|
|
1068
|
+
env?: "test";
|
|
1069
|
+
/** Present (`"tenant"`) only for a key CutOptim issues to its own first-party service. Absent for every key you create in the dashboard. */
|
|
1070
|
+
keyClass?: "tenant";
|
|
1071
|
+
}
|
|
1072
|
+
export interface HealthResponse {
|
|
1073
|
+
/** Always `"healthy"` when the process answers. */
|
|
1074
|
+
status: "healthy";
|
|
1075
|
+
/** Always `"cutoptim-engine"`. */
|
|
1076
|
+
service: "cutoptim-engine";
|
|
1077
|
+
/** Shape version. Currently `"1"`. */
|
|
1078
|
+
contractVersion: string;
|
|
1079
|
+
/** The DEFAULT engine's version (`heuristic`), not a per-engine list. */
|
|
1080
|
+
engineVersion: string;
|
|
1081
|
+
/** SYNCHRONOUS engine ids this deployment accepts in `engine`. The asynchronous `max` tier is reported in `maxEngines`, never here — it does not answer in-line. */
|
|
1082
|
+
engines: Array<"heuristic" | "balanced">;
|
|
1083
|
+
/** Optimize paths this deployment serves, so the endpoint set is discoverable without reading the docs — the same role `engines` plays for engine ids. `nest` is listed only where the nesting engine is built in. */
|
|
1084
|
+
modes: Array<"2d" | "1d" | "wood" | "nest">;
|
|
1085
|
+
/** Nesting engine ids this deployment can serve (empty where the nesting engine is not built in). `max` (async, preview) appears only where its binary is built in and the async tier is switched on — like `maxEngines`, built for, not a live worker check (the production API lists both). */
|
|
1086
|
+
nestEngines?: Array<"lbf" | "max">;
|
|
1087
|
+
/** The asynchronous tree-search tier this deployment is BUILT for — `["max"]` where the binary is present and the tier is switched on, `[]` otherwise. ⚠️ Built for is not the same as ready right now: this endpoint deliberately reads no database (a datastore outage must not take health down with it), so it cannot see whether the worker that solves the jobs is alive. A submission can therefore still answer 503 while this list contains `"max"`. An empty list means never; a populated one means normally. */
|
|
1088
|
+
maxEngines?: Array<"max">;
|
|
1089
|
+
/** Whole seconds since process start. */
|
|
1090
|
+
uptimeSec: number;
|
|
1091
|
+
}
|
|
1092
|
+
export type Optimize2dBody = Optimize2DRequest;
|
|
1093
|
+
export type Optimize2dResult = Optimize2DResponse | {
|
|
1094
|
+
/** Poll GET /v1/jobs/{id} with this id. */
|
|
1095
|
+
jobId: string;
|
|
1096
|
+
/** Always `queued` at submission. */
|
|
1097
|
+
status: "queued";
|
|
1098
|
+
mode: "2d";
|
|
1099
|
+
engine: "max";
|
|
1100
|
+
/** Suggested delay before the first poll. */
|
|
1101
|
+
pollAfterMs: number;
|
|
1102
|
+
quota?: {
|
|
1103
|
+
reserved?: number;
|
|
1104
|
+
used?: number;
|
|
1105
|
+
limit?: number;
|
|
1106
|
+
};
|
|
1107
|
+
};
|
|
1108
|
+
export type Optimize1dBody = Optimize1DRequest;
|
|
1109
|
+
export type Optimize1dResult = Optimize1DResponse;
|
|
1110
|
+
export type OptimizeWoodBody = OptimizeWoodRequest;
|
|
1111
|
+
export type OptimizeWoodResult = OptimizeWoodResponse;
|
|
1112
|
+
export type OptimizeNestBody = OptimizeNestRequest;
|
|
1113
|
+
export type OptimizeNestResult = OptimizeNestResponse | {
|
|
1114
|
+
/** Poll GET /v1/jobs/{id} with this id. */
|
|
1115
|
+
jobId: string;
|
|
1116
|
+
/** Always `queued` at submission. */
|
|
1117
|
+
status: "queued";
|
|
1118
|
+
mode: "nest";
|
|
1119
|
+
engine: "max";
|
|
1120
|
+
/** Suggested delay before the first poll (the search runs for its time budget). */
|
|
1121
|
+
pollAfterMs: number;
|
|
1122
|
+
quota?: {
|
|
1123
|
+
reserved?: number;
|
|
1124
|
+
used?: number;
|
|
1125
|
+
limit?: number;
|
|
1126
|
+
};
|
|
1127
|
+
};
|
|
1128
|
+
export type Validate2DBody = Optimize2DRequest;
|
|
1129
|
+
export type Validate2DResult = ValidateResponse;
|
|
1130
|
+
export type Validate1DBody = Optimize1DRequest;
|
|
1131
|
+
export type Validate1DResult = ValidateResponse;
|
|
1132
|
+
export type ValidateWoodBody = OptimizeWoodRequest;
|
|
1133
|
+
export type ValidateWoodResult = ValidateResponse;
|
|
1134
|
+
export type ValidateNestBody = OptimizeNestRequest;
|
|
1135
|
+
export type ValidateNestResult = ValidateResponse;
|
|
1136
|
+
export type GetJobResult = {
|
|
1137
|
+
jobId: string;
|
|
1138
|
+
status: "queued" | "running" | "succeeded" | "failed";
|
|
1139
|
+
/** Which optimize path the job came from; decides the shape of `result`. */
|
|
1140
|
+
mode: "2d" | "nest";
|
|
1141
|
+
engine: "max";
|
|
1142
|
+
/** Present only when `status` is `succeeded`: an `Optimize2DResponse` for `mode: "2d"`, an `OptimizeNestResponse` for `mode: "nest"`. */
|
|
1143
|
+
result?: Optimize2DResponse | OptimizeNestResponse;
|
|
1144
|
+
/** Present only when `status` is `failed`. */
|
|
1145
|
+
error?: string;
|
|
1146
|
+
/** When the job was submitted. */
|
|
1147
|
+
createdAt?: string;
|
|
1148
|
+
/** `null` while `queued` or `running`. */
|
|
1149
|
+
finishedAt?: string | null;
|
|
1150
|
+
};
|
|
1151
|
+
export type ImportNestBody = NestImportRequest;
|
|
1152
|
+
export type ImportNestResult = NestImportResponse;
|
|
1153
|
+
export type GetUsageResult = UsageResponse;
|
|
1154
|
+
export type GetHealthResult = HealthResponse;
|