@faicad/sheetmetal 0.13.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/dist/allowanceFns.d.ts +42 -0
- package/dist/allowanceFns.d.ts.map +1 -0
- package/dist/allowanceFns.js +63 -0
- package/dist/allowanceFns.js.map +1 -0
- package/dist/api.d.ts +312 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +367 -0
- package/dist/api.js.map +1 -0
- package/dist/authorFns.d.ts +118 -0
- package/dist/authorFns.d.ts.map +1 -0
- package/dist/authorFns.js +562 -0
- package/dist/authorFns.js.map +1 -0
- package/dist/bendTableFns.d.ts +87 -0
- package/dist/bendTableFns.d.ts.map +1 -0
- package/dist/bendTableFns.js +345 -0
- package/dist/bendTableFns.js.map +1 -0
- package/dist/compat.d.ts +90 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +99 -0
- package/dist/compat.js.map +1 -0
- package/dist/contourFlangeFns.d.ts +122 -0
- package/dist/contourFlangeFns.d.ts.map +1 -0
- package/dist/contourFlangeFns.js +403 -0
- package/dist/contourFlangeFns.js.map +1 -0
- package/dist/cutoutFns.d.ts +95 -0
- package/dist/cutoutFns.d.ts.map +1 -0
- package/dist/cutoutFns.js +300 -0
- package/dist/cutoutFns.js.map +1 -0
- package/dist/dxfFns.d.ts +48 -0
- package/dist/dxfFns.d.ts.map +1 -0
- package/dist/dxfFns.js +236 -0
- package/dist/dxfFns.js.map +1 -0
- package/dist/facade.d.ts +449 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +535 -0
- package/dist/facade.js.map +1 -0
- package/dist/featureTreeFns.d.ts +76 -0
- package/dist/featureTreeFns.d.ts.map +1 -0
- package/dist/featureTreeFns.js +190 -0
- package/dist/featureTreeFns.js.map +1 -0
- package/dist/foldFns.d.ts +83 -0
- package/dist/foldFns.d.ts.map +1 -0
- package/dist/foldFns.js +744 -0
- package/dist/foldFns.js.map +1 -0
- package/dist/foreignUnfoldFns.d.ts +62 -0
- package/dist/foreignUnfoldFns.d.ts.map +1 -0
- package/dist/foreignUnfoldFns.js +854 -0
- package/dist/foreignUnfoldFns.js.map +1 -0
- package/dist/formFns.d.ts +58 -0
- package/dist/formFns.d.ts.map +1 -0
- package/dist/formFns.js +300 -0
- package/dist/formFns.js.map +1 -0
- package/dist/hemFns.d.ts +17 -0
- package/dist/hemFns.d.ts.map +1 -0
- package/dist/hemFns.js +212 -0
- package/dist/hemFns.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +27 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +38 -0
- package/dist/internal.js.map +1 -0
- package/dist/jogFns.d.ts +18 -0
- package/dist/jogFns.d.ts.map +1 -0
- package/dist/jogFns.js +180 -0
- package/dist/jogFns.js.map +1 -0
- package/dist/loftedFlangeFns.d.ts +23 -0
- package/dist/loftedFlangeFns.d.ts.map +1 -0
- package/dist/loftedFlangeFns.js +306 -0
- package/dist/loftedFlangeFns.js.map +1 -0
- package/dist/materials.d.ts +16 -0
- package/dist/materials.d.ts.map +1 -0
- package/dist/materials.js +44 -0
- package/dist/materials.js.map +1 -0
- package/dist/miterFns.d.ts +31 -0
- package/dist/miterFns.d.ts.map +1 -0
- package/dist/miterFns.js +128 -0
- package/dist/miterFns.js.map +1 -0
- package/dist/nestFns.d.ts +116 -0
- package/dist/nestFns.d.ts.map +1 -0
- package/dist/nestFns.js +517 -0
- package/dist/nestFns.js.map +1 -0
- package/dist/polygonFns.d.ts +144 -0
- package/dist/polygonFns.d.ts.map +1 -0
- package/dist/polygonFns.js +324 -0
- package/dist/polygonFns.js.map +1 -0
- package/dist/reliefFns.d.ts +44 -0
- package/dist/reliefFns.d.ts.map +1 -0
- package/dist/reliefFns.js +367 -0
- package/dist/reliefFns.js.map +1 -0
- package/dist/reportFns.d.ts +33 -0
- package/dist/reportFns.d.ts.map +1 -0
- package/dist/reportFns.js +71 -0
- package/dist/reportFns.js.map +1 -0
- package/dist/tabFns.d.ts +43 -0
- package/dist/tabFns.d.ts.map +1 -0
- package/dist/tabFns.js +179 -0
- package/dist/tabFns.js.map +1 -0
- package/dist/types.d.ts +629 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/unfoldFns.d.ts +120 -0
- package/dist/unfoldFns.d.ts.map +1 -0
- package/dist/unfoldFns.js +766 -0
- package/dist/unfoldFns.js.map +1 -0
- package/dist/validateFns.d.ts +18 -0
- package/dist/validateFns.d.ts.map +1 -0
- package/dist/validateFns.js +196 -0
- package/dist/validateFns.js.map +1 -0
- package/package.json +45 -0
package/dist/nestFns.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { ok, err, validationError, getEdges, curveStartPoint } from '@faicad/faijs/brepjs-compat';
|
|
2
|
+
import { multiPatternToDXF } from './dxfFns.js';
|
|
3
|
+
import { wireToPolygon, transformPolygon, polygonBounds, polygonsOverlapWithClearance, signedArea, } from './polygonFns.js';
|
|
4
|
+
const EPS = 1e-9;
|
|
5
|
+
/**
|
|
6
|
+
* Bounding-box nest: arrange developed flat patterns onto stock sheets to reduce
|
|
7
|
+
* waste. This is BOUNDING-BOX nesting — each part is packed as its axis-aligned
|
|
8
|
+
* outline bounding box, so parts never interlock and concave parts leave their
|
|
9
|
+
* bounding-box waste unused. True-shape (no-fit-polygon) nesting is a follow-up.
|
|
10
|
+
*
|
|
11
|
+
* Algorithm: a shelf (level) packing heuristic. Parts are sorted largest-first by
|
|
12
|
+
* bbox height then area, then placed left-to-right into horizontal shelves; a new
|
|
13
|
+
* shelf opens below when the current row is full, and a NEW sheet opens when the
|
|
14
|
+
* next shelf would overflow the usable height. With `allowRotation`, each part is
|
|
15
|
+
* tried both at 0° and 90° and the orientation that fits the current shelf (or,
|
|
16
|
+
* failing that, fits at all) is kept. A part larger than the usable sheet even
|
|
17
|
+
* rotated is reported in `unplaced` with a warning — never dropped silently, never
|
|
18
|
+
* retried in an infinite loop.
|
|
19
|
+
*
|
|
20
|
+
* @param patterns - developed flat patterns to place onto the sheets.
|
|
21
|
+
* @param options - sheet dimensions plus margin, spacing, rotation and strategy.
|
|
22
|
+
* @returns `Result<NestResult>` - the populated sheets plus any unplaced patterns and
|
|
23
|
+
* warnings, or an error for invalid sheet/options or an unmeasurable outline.
|
|
24
|
+
*/
|
|
25
|
+
export function nest(patterns, options) {
|
|
26
|
+
const { sheet } = options;
|
|
27
|
+
const margin = options.margin ?? 0;
|
|
28
|
+
const spacing = options.spacing ?? 0;
|
|
29
|
+
const allowRotation = options.allowRotation ?? false;
|
|
30
|
+
if (!Number.isFinite(sheet.width) || sheet.width <= 0 || !Number.isFinite(sheet.height) || sheet.height <= 0) {
|
|
31
|
+
return err(validationError('INVALID_SHEET', `sheet width/height must be positive, got ${sheet.width}×${sheet.height}`));
|
|
32
|
+
}
|
|
33
|
+
if (!Number.isFinite(margin) || margin < 0 || !Number.isFinite(spacing) || spacing < 0) {
|
|
34
|
+
return err(validationError('INVALID_NEST_OPTS', `margin and spacing must be non-negative finite numbers, got margin=${margin} spacing=${spacing}`));
|
|
35
|
+
}
|
|
36
|
+
const usableW = sheet.width - 2 * margin;
|
|
37
|
+
const usableH = sheet.height - 2 * margin;
|
|
38
|
+
if (usableW <= EPS || usableH <= EPS) {
|
|
39
|
+
return err(validationError('INVALID_SHEET', `margin ${margin} leaves no usable area on a ${sheet.width}×${sheet.height} sheet`));
|
|
40
|
+
}
|
|
41
|
+
if (options.strategy === 'nfp') {
|
|
42
|
+
return nestNfp(patterns, margin, spacing, allowRotation, usableW, usableH);
|
|
43
|
+
}
|
|
44
|
+
const boxes = [];
|
|
45
|
+
for (const pattern of patterns) {
|
|
46
|
+
const b = patternBbox(pattern);
|
|
47
|
+
if (!b.ok)
|
|
48
|
+
return b;
|
|
49
|
+
boxes.push(b.value);
|
|
50
|
+
}
|
|
51
|
+
const usableArea = usableW * usableH;
|
|
52
|
+
const warnings = [];
|
|
53
|
+
const unplaced = [];
|
|
54
|
+
// Largest-first by height then area — the standard shelf-packing sort that keeps
|
|
55
|
+
// tall parts from fragmenting later shelves.
|
|
56
|
+
const order = boxes.map((_, i) => i).sort((a, b) => {
|
|
57
|
+
const ba = boxes[a];
|
|
58
|
+
const bb = boxes[b];
|
|
59
|
+
if (ba === undefined || bb === undefined)
|
|
60
|
+
return 0;
|
|
61
|
+
if (Math.abs(bb.height - ba.height) > EPS)
|
|
62
|
+
return bb.height - ba.height;
|
|
63
|
+
return bb.width * bb.height - ba.width * ba.height;
|
|
64
|
+
});
|
|
65
|
+
const placeable = [];
|
|
66
|
+
for (const idx of order) {
|
|
67
|
+
const box = boxes[idx];
|
|
68
|
+
if (box === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
const fits = fitsUsable(box, usableW, usableH, false);
|
|
71
|
+
const fitsRot = allowRotation && fitsUsable(box, usableW, usableH, true);
|
|
72
|
+
if (!fits && !fitsRot) {
|
|
73
|
+
unplaced.push(idx);
|
|
74
|
+
warnings.push({
|
|
75
|
+
code: 'PART_TOO_LARGE',
|
|
76
|
+
message: `pattern ${idx} (${box.width.toFixed(2)}×${box.height.toFixed(2)}) does not fit the usable sheet ${usableW.toFixed(2)}×${usableH.toFixed(2)} even rotated; left unplaced`,
|
|
77
|
+
featureId: `pattern-${idx}`,
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
placeable.push(idx);
|
|
82
|
+
}
|
|
83
|
+
const { sheets, dropped } = packShelves(placeable, boxes, usableW, usableH, margin, spacing, allowRotation, usableArea);
|
|
84
|
+
for (const idx of dropped) {
|
|
85
|
+
// Should be unreachable (pre-filtered parts fit an empty sheet); guards against
|
|
86
|
+
// a future regression silently dropping a part.
|
|
87
|
+
unplaced.push(idx);
|
|
88
|
+
warnings.push({
|
|
89
|
+
code: 'PART_TOO_LARGE',
|
|
90
|
+
message: `pattern ${idx} could not be placed on a fresh sheet; left unplaced`,
|
|
91
|
+
featureId: `pattern-${idx}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
unplaced.sort((a, b) => a - b);
|
|
95
|
+
return ok({ sheets, unplaced, warnings });
|
|
96
|
+
}
|
|
97
|
+
const NFP_ROTATIONS = [0, 90, 180, 270];
|
|
98
|
+
/**
|
|
99
|
+
* True-shape (no-fit-polygon) nest. Packs the actual outline polygons so concave
|
|
100
|
+
* parts interlock. This is a HEURISTIC bottom-left-fill packer (not provably
|
|
101
|
+
* optimal): parts are placed largest-first; each part slides to the lowest-then-
|
|
102
|
+
* leftmost feasible position on the current sheet where its outline polygon does not
|
|
103
|
+
* overlap (within `spacing`) any already-placed polygon and stays inside the usable
|
|
104
|
+
* area. Feasible positions are found by a discretised raster scan (a finite grid
|
|
105
|
+
* stepped by a fraction of the smallest part extent), so the search always
|
|
106
|
+
* terminates and a part can slide into a neighbour's concave notch.
|
|
107
|
+
*
|
|
108
|
+
* Each part is tried at 0/90/180/270° (90/270 only when `allowRotation`; 0/180
|
|
109
|
+
* otherwise — 180° keeps the footprint but enables interlocking); the orientation
|
|
110
|
+
* giving the lowest-leftmost feasible placement wins. A part that fits no position on
|
|
111
|
+
* the current sheet opens a new sheet; one that fits no empty sheet in any orientation
|
|
112
|
+
* goes to `unplaced` with a PART_TOO_LARGE warning — never dropped, never an infinite
|
|
113
|
+
* loop.
|
|
114
|
+
*/
|
|
115
|
+
function nestNfp(patterns, margin, spacing, allowRotation, usableW, usableH) {
|
|
116
|
+
const parts = [];
|
|
117
|
+
for (let i = 0; i < patterns.length; i += 1) {
|
|
118
|
+
const pattern = patterns[i];
|
|
119
|
+
if (pattern === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
const built = buildNfpPart(i, pattern, allowRotation);
|
|
122
|
+
if (!built.ok)
|
|
123
|
+
return built;
|
|
124
|
+
parts.push(built.value);
|
|
125
|
+
}
|
|
126
|
+
const usableArea = usableW * usableH;
|
|
127
|
+
const warnings = [];
|
|
128
|
+
const unplaced = [];
|
|
129
|
+
// Largest-first by outline area — large concave parts placed first leave pockets
|
|
130
|
+
// the smaller parts can interlock into.
|
|
131
|
+
parts.sort((a, b) => b.area - a.area);
|
|
132
|
+
// Bottom-left-fill raster step: a fraction of the smallest part extent. Finer steps
|
|
133
|
+
// find tighter interlocks at more cost; this fixed ratio keeps the packer
|
|
134
|
+
// deterministic and bounded (the scan grid is finite, so it always terminates).
|
|
135
|
+
const step = rasterStep(parts, usableW, usableH);
|
|
136
|
+
// Pre-filter parts that fit no empty usable sheet in any tried orientation.
|
|
137
|
+
const placeable = [];
|
|
138
|
+
for (const part of parts) {
|
|
139
|
+
const fits = part.orientations.some((o) => o.w <= usableW + EPS && o.h <= usableH + EPS);
|
|
140
|
+
if (!fits) {
|
|
141
|
+
unplaced.push(part.index);
|
|
142
|
+
const o0 = part.orientations[0];
|
|
143
|
+
const dims = o0 === undefined ? '?' : `${o0.w.toFixed(2)}×${o0.h.toFixed(2)}`;
|
|
144
|
+
warnings.push({
|
|
145
|
+
code: 'PART_TOO_LARGE',
|
|
146
|
+
message: `pattern ${part.index} (${dims}) does not fit the usable sheet ${usableW.toFixed(2)}×${usableH.toFixed(2)} even rotated; left unplaced`,
|
|
147
|
+
featureId: `pattern-${part.index}`,
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
placeable.push(part);
|
|
152
|
+
}
|
|
153
|
+
const sheets = [];
|
|
154
|
+
let current = [];
|
|
155
|
+
let placements = [];
|
|
156
|
+
let currentArea = 0;
|
|
157
|
+
const flush = () => {
|
|
158
|
+
if (placements.length === 0)
|
|
159
|
+
return;
|
|
160
|
+
sheets.push({ placements, utilization: currentArea / usableArea });
|
|
161
|
+
};
|
|
162
|
+
for (const part of placeable) {
|
|
163
|
+
const placed = placeOnSheet(part, current, spacing, margin, usableW, usableH, step);
|
|
164
|
+
if (placed === undefined) {
|
|
165
|
+
// Current sheet full for this part — close it and start fresh. Every placeable
|
|
166
|
+
// part fits an empty usable sheet, so this retry must succeed; if it somehow
|
|
167
|
+
// does not, route the part to unplaced (self-enforcing safety net, no loop).
|
|
168
|
+
flush();
|
|
169
|
+
current = [];
|
|
170
|
+
placements = [];
|
|
171
|
+
currentArea = 0;
|
|
172
|
+
const retry = placeOnSheet(part, current, spacing, margin, usableW, usableH, step);
|
|
173
|
+
if (retry === undefined) {
|
|
174
|
+
unplaced.push(part.index);
|
|
175
|
+
warnings.push({
|
|
176
|
+
code: 'PART_TOO_LARGE',
|
|
177
|
+
message: `pattern ${part.index} could not be placed on a fresh sheet; left unplaced`,
|
|
178
|
+
featureId: `pattern-${part.index}`,
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
current.push({ poly: retry.poly, bounds: polygonBounds(retry.poly) });
|
|
183
|
+
placements.push(retry.placement);
|
|
184
|
+
currentArea += part.area;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
current.push({ poly: placed.poly, bounds: polygonBounds(placed.poly) });
|
|
188
|
+
placements.push(placed.placement);
|
|
189
|
+
currentArea += part.area;
|
|
190
|
+
}
|
|
191
|
+
flush();
|
|
192
|
+
unplaced.sort((a, b) => a - b);
|
|
193
|
+
return ok({ sheets, unplaced, warnings });
|
|
194
|
+
}
|
|
195
|
+
/** Build the orientation set for one pattern outline (origin-anchored polygons). */
|
|
196
|
+
function buildNfpPart(index, pattern, allowRotation) {
|
|
197
|
+
const polyResult = wireToPolygon(pattern.outline);
|
|
198
|
+
if (!polyResult.ok)
|
|
199
|
+
return polyResult;
|
|
200
|
+
let base = polyResult.value;
|
|
201
|
+
// Normalise to CCW so the area sign and point-in-polygon orientation are consistent.
|
|
202
|
+
if (signedArea(base) < 0)
|
|
203
|
+
base = base.slice().reverse();
|
|
204
|
+
const area = Math.abs(signedArea(base));
|
|
205
|
+
if (area <= EPS) {
|
|
206
|
+
return err(validationError('EMPTY_OUTLINE', `pattern ${index} outline has degenerate area`));
|
|
207
|
+
}
|
|
208
|
+
const rotations = allowRotation ? NFP_ROTATIONS : [0, 180];
|
|
209
|
+
const orientations = [];
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
for (const rot of rotations) {
|
|
212
|
+
const rotated = transformPolygon(base, 0, 0, rot);
|
|
213
|
+
const [minX, minY, maxX, maxY] = polygonBounds(rotated);
|
|
214
|
+
const anchored = transformPolygon(rotated, -minX, -minY, 0);
|
|
215
|
+
const w = maxX - minX;
|
|
216
|
+
const h = maxY - minY;
|
|
217
|
+
// Skip an orientation whose anchored polygon is identical to an earlier one (e.g.
|
|
218
|
+
// 180° of a centrally-symmetric part). Keyed on the rounded vertex loop, NOT the
|
|
219
|
+
// bounding box — a concave part can share its bbox across rotations while its
|
|
220
|
+
// SHAPE (and thus its interlocking behaviour) differs.
|
|
221
|
+
const key = anchored.map(([x, y]) => `${x.toFixed(4)},${y.toFixed(4)}`).join(';');
|
|
222
|
+
if (seen.has(key))
|
|
223
|
+
continue;
|
|
224
|
+
seen.add(key);
|
|
225
|
+
orientations.push({ rotationDeg: rot, poly: anchored, w, h });
|
|
226
|
+
}
|
|
227
|
+
return ok({ index, area, orientations });
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Bottom-left-fill raster step. The scan grid steps by a fraction of the smallest
|
|
231
|
+
* part extent, so a part can slide INTO a neighbour's concave notch (a fit a sparse
|
|
232
|
+
* bbox-corner candidate set would miss). Clamped to keep the per-sheet scan bounded.
|
|
233
|
+
*/
|
|
234
|
+
function rasterStep(parts, usableW, usableH) {
|
|
235
|
+
let minExtent = Infinity;
|
|
236
|
+
for (const part of parts) {
|
|
237
|
+
for (const o of part.orientations) {
|
|
238
|
+
minExtent = Math.min(minExtent, o.w, o.h);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!Number.isFinite(minExtent) || minExtent <= EPS)
|
|
242
|
+
return Math.max(usableW, usableH) / 100;
|
|
243
|
+
// ~1/8 of the smallest extent gives tight interlocks at a bounded scan count; never
|
|
244
|
+
// finer than 1/400 of the larger sheet dimension (caps the worst-case grid size).
|
|
245
|
+
const floor = Math.max(usableW, usableH) / 400;
|
|
246
|
+
return Math.max(minExtent / 8, floor);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Find the lowest-then-leftmost feasible placement of `part` on the current sheet
|
|
250
|
+
* across all its orientations, by a discretised bottom-left raster scan. Returns the
|
|
251
|
+
* translated polygon (for later overlap tests) and the {@link Placement}, or
|
|
252
|
+
* undefined if nothing fits. The scan is over a finite grid so it always terminates.
|
|
253
|
+
*/
|
|
254
|
+
function placeOnSheet(part, placed, spacing, margin, usableW, usableH, step) {
|
|
255
|
+
let best;
|
|
256
|
+
for (const o of part.orientations) {
|
|
257
|
+
if (o.w > usableW + EPS || o.h > usableH + EPS)
|
|
258
|
+
continue;
|
|
259
|
+
const fit = scanOrientation(part.index, o, placed, spacing, margin, usableW, usableH, step);
|
|
260
|
+
if (fit === undefined)
|
|
261
|
+
continue;
|
|
262
|
+
// Lower y wins; ties broken by lower x — the bottom-left-fill objective.
|
|
263
|
+
if (best === undefined ||
|
|
264
|
+
fit.key[1] < best.key[1] - EPS ||
|
|
265
|
+
(Math.abs(fit.key[1] - best.key[1]) <= EPS && fit.key[0] < best.key[0] - EPS)) {
|
|
266
|
+
best = fit;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (best === undefined)
|
|
270
|
+
return undefined;
|
|
271
|
+
return { poly: best.poly, placement: best.placement };
|
|
272
|
+
}
|
|
273
|
+
/** Lowest-then-leftmost feasible raster position of one orientation, or undefined. */
|
|
274
|
+
function scanOrientation(index, o, placed, spacing, margin, usableW, usableH, step) {
|
|
275
|
+
const maxX = margin + usableW - o.w;
|
|
276
|
+
const maxY = margin + usableH - o.h;
|
|
277
|
+
// Scan bottom-to-top, left-to-right; the first feasible cell is the bottom-left fit.
|
|
278
|
+
for (let y = margin; y <= maxY + EPS; y += step) {
|
|
279
|
+
for (let x = margin; x <= maxX + EPS; x += step) {
|
|
280
|
+
const candidate = transformPolygon(o.poly, x, y, 0);
|
|
281
|
+
if (overlapsAny(candidate, placed, spacing))
|
|
282
|
+
continue;
|
|
283
|
+
return { poly: candidate, placement: { patternIndex: index, x, y, rotationDeg: o.rotationDeg }, key: [x, y] };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
function overlapsAny(candidate, placed, spacing) {
|
|
289
|
+
// Broad-phase bbox reject first (cheap), then the exact polygon overlap test.
|
|
290
|
+
const [cMinX, cMinY, cMaxX, cMaxY] = polygonBounds(candidate);
|
|
291
|
+
for (const pp of placed) {
|
|
292
|
+
const [pMinX, pMinY, pMaxX, pMaxY] = pp.bounds;
|
|
293
|
+
if (cMinX > pMaxX + spacing + EPS || pMinX > cMaxX + spacing + EPS)
|
|
294
|
+
continue;
|
|
295
|
+
if (cMinY > pMaxY + spacing + EPS || pMinY > cMaxY + spacing + EPS)
|
|
296
|
+
continue;
|
|
297
|
+
if (polygonsOverlapWithClearance(candidate, pp.poly, spacing))
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
/** Does a part fit the usable sheet in the given orientation? */
|
|
303
|
+
function fitsUsable(box, usableW, usableH, rotated) {
|
|
304
|
+
const w = rotated ? box.height : box.width;
|
|
305
|
+
const h = rotated ? box.width : box.height;
|
|
306
|
+
return w <= usableW + EPS && h <= usableH + EPS;
|
|
307
|
+
}
|
|
308
|
+
function packShelves(placeable, boxes, usableW, usableH, margin, spacing, allowRotation, usableArea) {
|
|
309
|
+
const sheets = [];
|
|
310
|
+
const dropped = [];
|
|
311
|
+
let state = newSheet(margin);
|
|
312
|
+
const commit = () => {
|
|
313
|
+
sheets.push({ placements: state.placements, utilization: state.placedArea / usableArea });
|
|
314
|
+
};
|
|
315
|
+
for (const idx of placeable) {
|
|
316
|
+
const box = boxes[idx];
|
|
317
|
+
if (box === undefined)
|
|
318
|
+
continue;
|
|
319
|
+
if (!tryPlace(state, box, idx, usableW, usableH, margin, spacing, allowRotation)) {
|
|
320
|
+
// Current sheet is full for this part — close it and open a fresh one. Every
|
|
321
|
+
// part in `placeable` fits an empty usable sheet, so this second attempt should
|
|
322
|
+
// always succeed; record it as dropped (caller routes to `unplaced`) as a
|
|
323
|
+
// self-enforcing safety net rather than relying on that reasoning alone.
|
|
324
|
+
commit();
|
|
325
|
+
state = newSheet(margin);
|
|
326
|
+
if (!tryPlace(state, box, idx, usableW, usableH, margin, spacing, allowRotation)) {
|
|
327
|
+
dropped.push(idx);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (state.placements.length > 0)
|
|
332
|
+
commit();
|
|
333
|
+
return { sheets, dropped };
|
|
334
|
+
}
|
|
335
|
+
function newSheet(margin) {
|
|
336
|
+
return { placements: [], shelfY: margin, shelfH: 0, cursorX: margin, placedArea: 0 };
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Try to place `box` on the current sheet, mutating `state` on success. Tries the
|
|
340
|
+
* current shelf first (with rotation if allowed), then opens a new shelf below.
|
|
341
|
+
* Returns false when the part fits neither the current nor a new shelf on this
|
|
342
|
+
* sheet (the caller then opens a new sheet).
|
|
343
|
+
*/
|
|
344
|
+
function tryPlace(state, box, idx, usableW, usableH, margin, spacing, allowRotation) {
|
|
345
|
+
const right = margin + usableW;
|
|
346
|
+
const top = margin + usableH;
|
|
347
|
+
// Candidate orientations: 0° always, 90° when allowed (and the box is not square).
|
|
348
|
+
const orientations = [{ rot: 0, w: box.width, h: box.height }];
|
|
349
|
+
if (allowRotation && Math.abs(box.width - box.height) > EPS) {
|
|
350
|
+
orientations.push({ rot: 90, w: box.height, h: box.width });
|
|
351
|
+
}
|
|
352
|
+
const firstOnShelf = state.cursorX <= margin + EPS;
|
|
353
|
+
// 1) Fit on the current open shelf. Prefer the orientation that keeps the shelf
|
|
354
|
+
// shortest (least new vertical waste) among those that fit horizontally and
|
|
355
|
+
// within the shelf's established height once started.
|
|
356
|
+
let best;
|
|
357
|
+
for (const o of orientations) {
|
|
358
|
+
const xStart = firstOnShelf ? state.cursorX : state.cursorX + spacing;
|
|
359
|
+
if (xStart + o.w > right + EPS)
|
|
360
|
+
continue;
|
|
361
|
+
// A started shelf has a fixed bottom; the part must fit under the usable top
|
|
362
|
+
// from the shelf bottom (it may grow shelfH up to the top).
|
|
363
|
+
if (state.shelfY + o.h > top + EPS)
|
|
364
|
+
continue;
|
|
365
|
+
if (best === undefined || o.h < best.h - EPS)
|
|
366
|
+
best = o;
|
|
367
|
+
}
|
|
368
|
+
if (best !== undefined) {
|
|
369
|
+
const xStart = firstOnShelf ? state.cursorX : state.cursorX + spacing;
|
|
370
|
+
state.placements.push({ patternIndex: idx, x: xStart, y: state.shelfY, rotationDeg: best.rot });
|
|
371
|
+
state.cursorX = xStart + best.w;
|
|
372
|
+
state.shelfH = Math.max(state.shelfH, best.h);
|
|
373
|
+
state.placedArea += best.w * best.h;
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
// 2) Open a new shelf below the current one (if anything is on the current shelf).
|
|
377
|
+
if (state.placements.length === 0)
|
|
378
|
+
return false;
|
|
379
|
+
const nextShelfY = state.shelfY + state.shelfH + spacing;
|
|
380
|
+
let bestNew;
|
|
381
|
+
for (const o of orientations) {
|
|
382
|
+
if (margin + o.w > right + EPS)
|
|
383
|
+
continue;
|
|
384
|
+
if (nextShelfY + o.h > top + EPS)
|
|
385
|
+
continue;
|
|
386
|
+
if (bestNew === undefined || o.h < bestNew.h - EPS)
|
|
387
|
+
bestNew = o;
|
|
388
|
+
}
|
|
389
|
+
if (bestNew === undefined)
|
|
390
|
+
return false;
|
|
391
|
+
state.shelfY = nextShelfY;
|
|
392
|
+
state.shelfH = bestNew.h;
|
|
393
|
+
state.cursorX = margin + bestNew.w;
|
|
394
|
+
state.placements.push({ patternIndex: idx, x: margin, y: nextShelfY, rotationDeg: bestNew.rot });
|
|
395
|
+
state.placedArea += bestNew.w * bestNew.h;
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Axis-aligned bounding box of a pattern's OUTER outline wire, read from the wire
|
|
400
|
+
* vertices (each edge start point) — the same 2D-coordinate read the DXF/SVG writers
|
|
401
|
+
* use. Only the outer outline is packed; holes ride inside it.
|
|
402
|
+
*
|
|
403
|
+
* @param pattern - the flat pattern whose outer outline is measured.
|
|
404
|
+
* @returns `Result<Bbox>` - the axis-aligned bounding box of the outer outline, or an
|
|
405
|
+
* error if the outline is empty or degenerate.
|
|
406
|
+
*/
|
|
407
|
+
export function patternBbox(pattern) {
|
|
408
|
+
const edges = getEdges(pattern.outline);
|
|
409
|
+
if (edges.length === 0) {
|
|
410
|
+
return err(validationError('EMPTY_OUTLINE', 'pattern outline has no edges to bound'));
|
|
411
|
+
}
|
|
412
|
+
let minX = Infinity;
|
|
413
|
+
let minY = Infinity;
|
|
414
|
+
let maxX = -Infinity;
|
|
415
|
+
let maxY = -Infinity;
|
|
416
|
+
for (const edge of edges) {
|
|
417
|
+
const p = curveStartPoint(edge);
|
|
418
|
+
if (p[0] < minX)
|
|
419
|
+
minX = p[0];
|
|
420
|
+
if (p[0] > maxX)
|
|
421
|
+
maxX = p[0];
|
|
422
|
+
if (p[1] < minY)
|
|
423
|
+
minY = p[1];
|
|
424
|
+
if (p[1] > maxY)
|
|
425
|
+
maxY = p[1];
|
|
426
|
+
}
|
|
427
|
+
const width = maxX - minX;
|
|
428
|
+
const height = maxY - minY;
|
|
429
|
+
if (!Number.isFinite(minX) || width <= EPS || height <= EPS) {
|
|
430
|
+
return err(validationError('EMPTY_OUTLINE', 'pattern outline bounding box is degenerate'));
|
|
431
|
+
}
|
|
432
|
+
return ok({ minX, minY, width, height });
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Sheet-placement transform for one placement: shift the pattern's developed-plane
|
|
436
|
+
* bbox lower-left to the origin, rotate `rotationDeg` (CCW) about it, then translate
|
|
437
|
+
* the rotated shape so its rotated bounding box's lower-left lands at `(x, y)` on the
|
|
438
|
+
* sheet. A point at developed `(px, py)` therefore lands inside the box the packer
|
|
439
|
+
* reserved. The 0° and 90° branches are kept as closed-form integer-stable maps so
|
|
440
|
+
* the bbox-strategy DXF output is byte-identical to the pre-NFP writer; finer angles
|
|
441
|
+
* (180/270 and any nfp-emitted value) take the general rotation path.
|
|
442
|
+
*/
|
|
443
|
+
function placementTransform(box, x, y, rotationDeg) {
|
|
444
|
+
if (rotationDeg === 0) {
|
|
445
|
+
return ([px, py]) => [x + (px - box.minX), y + (py - box.minY)];
|
|
446
|
+
}
|
|
447
|
+
if (rotationDeg === 90) {
|
|
448
|
+
// After shifting to origin: (lx, ly) in [0,w]×[0,h]. Rotate 90° CCW: (-ly, lx)
|
|
449
|
+
// → [-h,0]×[0,w]; add h to bring x back to [0,h], then translate to (x, y).
|
|
450
|
+
return ([px, py]) => {
|
|
451
|
+
const lx = px - box.minX;
|
|
452
|
+
const ly = py - box.minY;
|
|
453
|
+
return [x + (box.height - ly), y + lx];
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
// General CCW rotation about the bbox lower-left, then re-anchor the rotated bbox's
|
|
457
|
+
// lower-left to (x, y) so the placement matches the box the packer reserved.
|
|
458
|
+
const rad = (rotationDeg * Math.PI) / 180;
|
|
459
|
+
const c = Math.cos(rad);
|
|
460
|
+
const s = Math.sin(rad);
|
|
461
|
+
const corners = [
|
|
462
|
+
[0, 0],
|
|
463
|
+
[box.width, 0],
|
|
464
|
+
[box.width, box.height],
|
|
465
|
+
[0, box.height],
|
|
466
|
+
];
|
|
467
|
+
let minRx = Infinity;
|
|
468
|
+
let minRy = Infinity;
|
|
469
|
+
for (const [cx, cy] of corners) {
|
|
470
|
+
const rx = cx * c - cy * s;
|
|
471
|
+
const ry = cx * s + cy * c;
|
|
472
|
+
if (rx < minRx)
|
|
473
|
+
minRx = rx;
|
|
474
|
+
if (ry < minRy)
|
|
475
|
+
minRy = ry;
|
|
476
|
+
}
|
|
477
|
+
return ([px, py]) => {
|
|
478
|
+
const lx = px - box.minX;
|
|
479
|
+
const ly = py - box.minY;
|
|
480
|
+
const rx = lx * c - ly * s;
|
|
481
|
+
const ry = lx * s + ly * c;
|
|
482
|
+
return [x + (rx - minRx), y + (ry - minRy)];
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Emit one fabrication-ready DXF for a single nested sheet: every pattern placed on
|
|
487
|
+
* `result.sheets[sheetIndex]` is translated and rotated onto the sheet (via the same
|
|
488
|
+
* bounding boxes the packer used) and written through {@link multiPatternToDXF}.
|
|
489
|
+
* `patterns` must be the SAME array passed to {@link nest} (placements index into it).
|
|
490
|
+
*
|
|
491
|
+
* @param result - the {@link NestResult} returned by {@link nest}.
|
|
492
|
+
* @param patterns - the same `FlatPattern[]` array that was passed to {@link nest}.
|
|
493
|
+
* @param sheetIndex - index into `result.sheets` of the sheet to emit.
|
|
494
|
+
* @param options - optional DXF options forwarded to {@link multiPatternToDXF}.
|
|
495
|
+
* @returns `Result<string>` - the DXF text for the selected sheet, or an error if the
|
|
496
|
+
* sheet index is invalid, a placement references a missing pattern, or a pattern
|
|
497
|
+
* outline cannot be measured.
|
|
498
|
+
*/
|
|
499
|
+
export function nestToDXF(result, patterns, sheetIndex, options) {
|
|
500
|
+
const sheet = result.sheets[sheetIndex];
|
|
501
|
+
if (sheet === undefined) {
|
|
502
|
+
return err(validationError('INVALID_SHEET_INDEX', `no nested sheet at index ${sheetIndex} (have ${result.sheets.length})`));
|
|
503
|
+
}
|
|
504
|
+
const placed = [];
|
|
505
|
+
for (const p of sheet.placements) {
|
|
506
|
+
const pattern = patterns[p.patternIndex];
|
|
507
|
+
if (pattern === undefined) {
|
|
508
|
+
return err(validationError('INVALID_PATTERN_INDEX', `placement references missing pattern ${p.patternIndex}`));
|
|
509
|
+
}
|
|
510
|
+
const box = patternBbox(pattern);
|
|
511
|
+
if (!box.ok)
|
|
512
|
+
return box;
|
|
513
|
+
placed.push({ pattern, transform: placementTransform(box.value, p.x, p.y, p.rotationDeg) });
|
|
514
|
+
}
|
|
515
|
+
return multiPatternToDXF(placed, options ?? {});
|
|
516
|
+
}
|
|
517
|
+
//# sourceMappingURL=nestFns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nestFns.js","sourceRoot":"","sources":["../src/nestFns.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Result, ok, err, validationError, getEdges, curveStartPoint } from '@faicad/faijs/brepjs-compat';\nimport type { FlatPattern, SheetMetalWarning } from './types.js';\nimport { multiPatternToDXF, type DxfOptions, type Transform2, type PlacedPattern } from './dxfFns.js';\nimport {\n wireToPolygon,\n transformPolygon,\n polygonBounds,\n polygonsOverlapWithClearance,\n signedArea,\n type Polygon,\n} from './polygonFns.js';\n\n/** Stock sheet the parts are packed onto (the gross blank, before margin). */\nexport interface SheetSpec {\n width: number;\n height: number;\n}\n\n/** Nesting options: the stock sheet plus optional margin, spacing, rotation and strategy. */\nexport interface NestOptions {\n sheet: SheetSpec;\n /** Clear border kept empty around the sheet edge (per side). Default `0`. */\n margin?: number | undefined;\n /** Minimum clear gap between any two placed parts (and the bbox padding used\n * for the non-overlap test). Default `0`. */\n spacing?: number | undefined;\n /** Try a 90° rotation per part and keep whichever orientation fits/packs better. */\n allowRotation?: boolean | undefined;\n /**\n * Packing strategy. Default `\"bbox\"`: each part is packed as its axis-aligned\n * bounding box (the original {@link nest} behavior — fast, parts never interlock).\n * `\"nfp\"` is true-shape / no-fit-polygon nesting: the actual outline polygons are\n * packed so concave (L-shaped) parts interlock for higher material utilization.\n * The NFP packer is a HEURISTIC (bottom-left-fill, largest-first) — not provably\n * optimal — but it never overlaps parts and never drops a part silently.\n */\n strategy?: 'bbox' | 'nfp' | undefined;\n}\n\n/**\n * Where a single pattern lands on a sheet. For the bbox strategy `rotationDeg` is\n * `0` or `90`; the true-shape (nfp) strategy may also emit `180` or `270`. `(x, y)`\n * is the lower-left of the part's transformed bounding box in sheet coordinates.\n */\nexport interface Placement {\n /** Index into the `patterns` array passed to {@link nest}. */\n patternIndex: number;\n /** Lower-left x of the part's (rotated) bounding box, in sheet coordinates. */\n x: number;\n /** Lower-left y of the part's (rotated) bounding box, in sheet coordinates. */\n y: number;\n rotationDeg: number;\n}\n\n/** One populated sheet produced by {@link nest}: where each pattern landed and how full it is. */\nexport interface NestSheet {\n placements: Placement[];\n /**\n * Σ placed part areas ÷ usable-sheet area, in `(0, 1]`. \"Usable\" =\n * `(width − 2·margin) × (height − 2·margin)`. For the `\"bbox\"` strategy the part\n * area is its BOUNDING-BOX area (intra-bbox waste of a non-rectangular part is not\n * credited — a conservative measure). For the `\"nfp\"` true-shape strategy it is the\n * actual OUTLINE-POLYGON area, so an L-shaped part credits only its true material,\n * making the two strategies' utilizations directly comparable on the same parts.\n */\n utilization: number;\n}\n\n/** Outcome of {@link nest}: the placed sheets, any unplaced patterns, and warnings. */\nexport interface NestResult {\n sheets: NestSheet[];\n /** Indices of patterns too large for the usable sheet even rotated. */\n unplaced: number[];\n warnings: SheetMetalWarning[];\n}\n\n/** Axis-aligned bounding box of one pattern outline, in developed-plane coords. */\nexport interface Bbox {\n minX: number;\n minY: number;\n width: number;\n height: number;\n}\n\nconst EPS = 1e-9;\n\n/**\n * Bounding-box nest: arrange developed flat patterns onto stock sheets to reduce\n * waste. This is BOUNDING-BOX nesting — each part is packed as its axis-aligned\n * outline bounding box, so parts never interlock and concave parts leave their\n * bounding-box waste unused. True-shape (no-fit-polygon) nesting is a follow-up.\n *\n * Algorithm: a shelf (level) packing heuristic. Parts are sorted largest-first by\n * bbox height then area, then placed left-to-right into horizontal shelves; a new\n * shelf opens below when the current row is full, and a NEW sheet opens when the\n * next shelf would overflow the usable height. With `allowRotation`, each part is\n * tried both at 0° and 90° and the orientation that fits the current shelf (or,\n * failing that, fits at all) is kept. A part larger than the usable sheet even\n * rotated is reported in `unplaced` with a warning — never dropped silently, never\n * retried in an infinite loop.\n *\n * @param patterns - developed flat patterns to place onto the sheets.\n * @param options - sheet dimensions plus margin, spacing, rotation and strategy.\n * @returns `Result<NestResult>` - the populated sheets plus any unplaced patterns and\n * warnings, or an error for invalid sheet/options or an unmeasurable outline.\n */\nexport function nest(patterns: FlatPattern[], options: NestOptions): Result<NestResult> {\n const { sheet } = options;\n const margin = options.margin ?? 0;\n const spacing = options.spacing ?? 0;\n const allowRotation = options.allowRotation ?? false;\n\n if (!Number.isFinite(sheet.width) || sheet.width <= 0 || !Number.isFinite(sheet.height) || sheet.height <= 0) {\n return err(validationError('INVALID_SHEET', `sheet width/height must be positive, got ${sheet.width}×${sheet.height}`));\n }\n if (!Number.isFinite(margin) || margin < 0 || !Number.isFinite(spacing) || spacing < 0) {\n return err(validationError('INVALID_NEST_OPTS', `margin and spacing must be non-negative finite numbers, got margin=${margin} spacing=${spacing}`));\n }\n\n const usableW = sheet.width - 2 * margin;\n const usableH = sheet.height - 2 * margin;\n if (usableW <= EPS || usableH <= EPS) {\n return err(validationError('INVALID_SHEET', `margin ${margin} leaves no usable area on a ${sheet.width}×${sheet.height} sheet`));\n }\n\n if (options.strategy === 'nfp') {\n return nestNfp(patterns, margin, spacing, allowRotation, usableW, usableH);\n }\n\n const boxes: Bbox[] = [];\n for (const pattern of patterns) {\n const b = patternBbox(pattern);\n if (!b.ok) return b;\n boxes.push(b.value);\n }\n\n const usableArea = usableW * usableH;\n const warnings: SheetMetalWarning[] = [];\n const unplaced: number[] = [];\n\n // Largest-first by height then area — the standard shelf-packing sort that keeps\n // tall parts from fragmenting later shelves.\n const order = boxes.map((_, i) => i).sort((a, b) => {\n const ba = boxes[a];\n const bb = boxes[b];\n if (ba === undefined || bb === undefined) return 0;\n if (Math.abs(bb.height - ba.height) > EPS) return bb.height - ba.height;\n return bb.width * bb.height - ba.width * ba.height;\n });\n\n const placeable: number[] = [];\n for (const idx of order) {\n const box = boxes[idx];\n if (box === undefined) continue;\n const fits = fitsUsable(box, usableW, usableH, false);\n const fitsRot = allowRotation && fitsUsable(box, usableW, usableH, true);\n if (!fits && !fitsRot) {\n unplaced.push(idx);\n warnings.push({\n code: 'PART_TOO_LARGE',\n message: `pattern ${idx} (${box.width.toFixed(2)}×${box.height.toFixed(2)}) does not fit the usable sheet ${usableW.toFixed(2)}×${usableH.toFixed(2)} even rotated; left unplaced`,\n featureId: `pattern-${idx}`,\n });\n continue;\n }\n placeable.push(idx);\n }\n\n const { sheets, dropped } = packShelves(placeable, boxes, usableW, usableH, margin, spacing, allowRotation, usableArea);\n for (const idx of dropped) {\n // Should be unreachable (pre-filtered parts fit an empty sheet); guards against\n // a future regression silently dropping a part.\n unplaced.push(idx);\n warnings.push({\n code: 'PART_TOO_LARGE',\n message: `pattern ${idx} could not be placed on a fresh sheet; left unplaced`,\n featureId: `pattern-${idx}`,\n });\n }\n\n unplaced.sort((a, b) => a - b);\n return ok({ sheets, unplaced, warnings });\n}\n\n/** A part outline normalised to the developed-plane origin, with its candidate\n * orientations precomputed. Each orientation's polygon is anchored so its own\n * bounding box lower-left sits at `(0, 0)`. */\ninterface NfpPart {\n index: number;\n /** Outline area (orientation-invariant) — used for the largest-first sort. */\n area: number;\n /** Candidate orientations to try, largest-first packing keeps the lowest-left fit. */\n orientations: NfpOrientation[];\n}\n\ninterface NfpOrientation {\n rotationDeg: number;\n /** Polygon anchored with its bounding-box lower-left at the origin. */\n poly: Polygon;\n /** Width/height of the (rotated) bounding box. */\n w: number;\n h: number;\n}\n\n/** One concrete placement on the current sheet: the orientation polygon translated\n * to its sheet position, kept (with its cached bounds) for the overlap test against\n * later parts. */\ninterface PlacedPoly {\n poly: Polygon;\n bounds: [number, number, number, number];\n}\n\nconst NFP_ROTATIONS = [0, 90, 180, 270];\n\n/**\n * True-shape (no-fit-polygon) nest. Packs the actual outline polygons so concave\n * parts interlock. This is a HEURISTIC bottom-left-fill packer (not provably\n * optimal): parts are placed largest-first; each part slides to the lowest-then-\n * leftmost feasible position on the current sheet where its outline polygon does not\n * overlap (within `spacing`) any already-placed polygon and stays inside the usable\n * area. Feasible positions are found by a discretised raster scan (a finite grid\n * stepped by a fraction of the smallest part extent), so the search always\n * terminates and a part can slide into a neighbour's concave notch.\n *\n * Each part is tried at 0/90/180/270° (90/270 only when `allowRotation`; 0/180\n * otherwise — 180° keeps the footprint but enables interlocking); the orientation\n * giving the lowest-leftmost feasible placement wins. A part that fits no position on\n * the current sheet opens a new sheet; one that fits no empty sheet in any orientation\n * goes to `unplaced` with a PART_TOO_LARGE warning — never dropped, never an infinite\n * loop.\n */\nfunction nestNfp(\n patterns: FlatPattern[],\n margin: number,\n spacing: number,\n allowRotation: boolean,\n usableW: number,\n usableH: number\n): Result<NestResult> {\n const parts: NfpPart[] = [];\n for (let i = 0; i < patterns.length; i += 1) {\n const pattern = patterns[i];\n if (pattern === undefined) continue;\n const built = buildNfpPart(i, pattern, allowRotation);\n if (!built.ok) return built;\n parts.push(built.value);\n }\n\n const usableArea = usableW * usableH;\n const warnings: SheetMetalWarning[] = [];\n const unplaced: number[] = [];\n\n // Largest-first by outline area — large concave parts placed first leave pockets\n // the smaller parts can interlock into.\n parts.sort((a, b) => b.area - a.area);\n\n // Bottom-left-fill raster step: a fraction of the smallest part extent. Finer steps\n // find tighter interlocks at more cost; this fixed ratio keeps the packer\n // deterministic and bounded (the scan grid is finite, so it always terminates).\n const step = rasterStep(parts, usableW, usableH);\n\n // Pre-filter parts that fit no empty usable sheet in any tried orientation.\n const placeable: NfpPart[] = [];\n for (const part of parts) {\n const fits = part.orientations.some((o) => o.w <= usableW + EPS && o.h <= usableH + EPS);\n if (!fits) {\n unplaced.push(part.index);\n const o0 = part.orientations[0];\n const dims = o0 === undefined ? '?' : `${o0.w.toFixed(2)}×${o0.h.toFixed(2)}`;\n warnings.push({\n code: 'PART_TOO_LARGE',\n message: `pattern ${part.index} (${dims}) does not fit the usable sheet ${usableW.toFixed(2)}×${usableH.toFixed(2)} even rotated; left unplaced`,\n featureId: `pattern-${part.index}`,\n });\n continue;\n }\n placeable.push(part);\n }\n\n const sheets: NestSheet[] = [];\n let current: PlacedPoly[] = [];\n let placements: Placement[] = [];\n let currentArea = 0;\n\n const flush = (): void => {\n if (placements.length === 0) return;\n sheets.push({ placements, utilization: currentArea / usableArea });\n };\n\n for (const part of placeable) {\n const placed = placeOnSheet(part, current, spacing, margin, usableW, usableH, step);\n if (placed === undefined) {\n // Current sheet full for this part — close it and start fresh. Every placeable\n // part fits an empty usable sheet, so this retry must succeed; if it somehow\n // does not, route the part to unplaced (self-enforcing safety net, no loop).\n flush();\n current = [];\n placements = [];\n currentArea = 0;\n const retry = placeOnSheet(part, current, spacing, margin, usableW, usableH, step);\n if (retry === undefined) {\n unplaced.push(part.index);\n warnings.push({\n code: 'PART_TOO_LARGE',\n message: `pattern ${part.index} could not be placed on a fresh sheet; left unplaced`,\n featureId: `pattern-${part.index}`,\n });\n continue;\n }\n current.push({ poly: retry.poly, bounds: polygonBounds(retry.poly) });\n placements.push(retry.placement);\n currentArea += part.area;\n continue;\n }\n current.push({ poly: placed.poly, bounds: polygonBounds(placed.poly) });\n placements.push(placed.placement);\n currentArea += part.area;\n }\n\n flush();\n unplaced.sort((a, b) => a - b);\n return ok({ sheets, unplaced, warnings });\n}\n\n/** Build the orientation set for one pattern outline (origin-anchored polygons). */\nfunction buildNfpPart(index: number, pattern: FlatPattern, allowRotation: boolean): Result<NfpPart> {\n const polyResult = wireToPolygon(pattern.outline);\n if (!polyResult.ok) return polyResult;\n let base = polyResult.value;\n // Normalise to CCW so the area sign and point-in-polygon orientation are consistent.\n if (signedArea(base) < 0) base = base.slice().reverse();\n const area = Math.abs(signedArea(base));\n if (area <= EPS) {\n return err(validationError('EMPTY_OUTLINE', `pattern ${index} outline has degenerate area`));\n }\n\n const rotations = allowRotation ? NFP_ROTATIONS : [0, 180];\n const orientations: NfpOrientation[] = [];\n const seen = new Set<string>();\n for (const rot of rotations) {\n const rotated = transformPolygon(base, 0, 0, rot);\n const [minX, minY, maxX, maxY] = polygonBounds(rotated);\n const anchored = transformPolygon(rotated, -minX, -minY, 0);\n const w = maxX - minX;\n const h = maxY - minY;\n // Skip an orientation whose anchored polygon is identical to an earlier one (e.g.\n // 180° of a centrally-symmetric part). Keyed on the rounded vertex loop, NOT the\n // bounding box — a concave part can share its bbox across rotations while its\n // SHAPE (and thus its interlocking behaviour) differs.\n const key = anchored.map(([x, y]) => `${x.toFixed(4)},${y.toFixed(4)}`).join(';');\n if (seen.has(key)) continue;\n seen.add(key);\n orientations.push({ rotationDeg: rot, poly: anchored, w, h });\n }\n return ok({ index, area, orientations });\n}\n\n/**\n * Bottom-left-fill raster step. The scan grid steps by a fraction of the smallest\n * part extent, so a part can slide INTO a neighbour's concave notch (a fit a sparse\n * bbox-corner candidate set would miss). Clamped to keep the per-sheet scan bounded.\n */\nfunction rasterStep(parts: NfpPart[], usableW: number, usableH: number): number {\n let minExtent = Infinity;\n for (const part of parts) {\n for (const o of part.orientations) {\n minExtent = Math.min(minExtent, o.w, o.h);\n }\n }\n if (!Number.isFinite(minExtent) || minExtent <= EPS) return Math.max(usableW, usableH) / 100;\n // ~1/8 of the smallest extent gives tight interlocks at a bounded scan count; never\n // finer than 1/400 of the larger sheet dimension (caps the worst-case grid size).\n const floor = Math.max(usableW, usableH) / 400;\n return Math.max(minExtent / 8, floor);\n}\n\n/**\n * Find the lowest-then-leftmost feasible placement of `part` on the current sheet\n * across all its orientations, by a discretised bottom-left raster scan. Returns the\n * translated polygon (for later overlap tests) and the {@link Placement}, or\n * undefined if nothing fits. The scan is over a finite grid so it always terminates.\n */\nfunction placeOnSheet(\n part: NfpPart,\n placed: PlacedPoly[],\n spacing: number,\n margin: number,\n usableW: number,\n usableH: number,\n step: number\n): { poly: Polygon; placement: Placement } | undefined {\n let best: { poly: Polygon; placement: Placement; key: [number, number] } | undefined;\n for (const o of part.orientations) {\n if (o.w > usableW + EPS || o.h > usableH + EPS) continue;\n const fit = scanOrientation(part.index, o, placed, spacing, margin, usableW, usableH, step);\n if (fit === undefined) continue;\n // Lower y wins; ties broken by lower x — the bottom-left-fill objective.\n if (\n best === undefined ||\n fit.key[1] < best.key[1] - EPS ||\n (Math.abs(fit.key[1] - best.key[1]) <= EPS && fit.key[0] < best.key[0] - EPS)\n ) {\n best = fit;\n }\n }\n if (best === undefined) return undefined;\n return { poly: best.poly, placement: best.placement };\n}\n\n/** Lowest-then-leftmost feasible raster position of one orientation, or undefined. */\nfunction scanOrientation(\n index: number,\n o: NfpOrientation,\n placed: PlacedPoly[],\n spacing: number,\n margin: number,\n usableW: number,\n usableH: number,\n step: number\n): { poly: Polygon; placement: Placement; key: [number, number] } | undefined {\n const maxX = margin + usableW - o.w;\n const maxY = margin + usableH - o.h;\n // Scan bottom-to-top, left-to-right; the first feasible cell is the bottom-left fit.\n for (let y = margin; y <= maxY + EPS; y += step) {\n for (let x = margin; x <= maxX + EPS; x += step) {\n const candidate = transformPolygon(o.poly, x, y, 0);\n if (overlapsAny(candidate, placed, spacing)) continue;\n return { poly: candidate, placement: { patternIndex: index, x, y, rotationDeg: o.rotationDeg }, key: [x, y] };\n }\n }\n return undefined;\n}\n\nfunction overlapsAny(candidate: Polygon, placed: PlacedPoly[], spacing: number): boolean {\n // Broad-phase bbox reject first (cheap), then the exact polygon overlap test.\n const [cMinX, cMinY, cMaxX, cMaxY] = polygonBounds(candidate);\n for (const pp of placed) {\n const [pMinX, pMinY, pMaxX, pMaxY] = pp.bounds;\n if (cMinX > pMaxX + spacing + EPS || pMinX > cMaxX + spacing + EPS) continue;\n if (cMinY > pMaxY + spacing + EPS || pMinY > cMaxY + spacing + EPS) continue;\n if (polygonsOverlapWithClearance(candidate, pp.poly, spacing)) return true;\n }\n return false;\n}\n\n/** Does a part fit the usable sheet in the given orientation? */\nfunction fitsUsable(box: Bbox, usableW: number, usableH: number, rotated: boolean): boolean {\n const w = rotated ? box.height : box.width;\n const h = rotated ? box.width : box.height;\n return w <= usableW + EPS && h <= usableH + EPS;\n}\n\ninterface ShelfState {\n placements: Placement[];\n /** Bottom y (sheet coords) of the current open shelf. */\n shelfY: number;\n /** Height of the current open shelf (the tallest part placed in it). */\n shelfH: number;\n /** Next free x (sheet coords) on the current shelf. */\n cursorX: number;\n placedArea: number;\n}\n\nfunction packShelves(\n placeable: number[],\n boxes: Bbox[],\n usableW: number,\n usableH: number,\n margin: number,\n spacing: number,\n allowRotation: boolean,\n usableArea: number\n): { sheets: NestSheet[]; dropped: number[] } {\n const sheets: NestSheet[] = [];\n const dropped: number[] = [];\n let state = newSheet(margin);\n\n const commit = (): void => {\n sheets.push({ placements: state.placements, utilization: state.placedArea / usableArea });\n };\n\n for (const idx of placeable) {\n const box = boxes[idx];\n if (box === undefined) continue;\n\n if (!tryPlace(state, box, idx, usableW, usableH, margin, spacing, allowRotation)) {\n // Current sheet is full for this part — close it and open a fresh one. Every\n // part in `placeable` fits an empty usable sheet, so this second attempt should\n // always succeed; record it as dropped (caller routes to `unplaced`) as a\n // self-enforcing safety net rather than relying on that reasoning alone.\n commit();\n state = newSheet(margin);\n if (!tryPlace(state, box, idx, usableW, usableH, margin, spacing, allowRotation)) {\n dropped.push(idx);\n }\n }\n }\n\n if (state.placements.length > 0) commit();\n return { sheets, dropped };\n}\n\nfunction newSheet(margin: number): ShelfState {\n return { placements: [], shelfY: margin, shelfH: 0, cursorX: margin, placedArea: 0 };\n}\n\n/**\n * Try to place `box` on the current sheet, mutating `state` on success. Tries the\n * current shelf first (with rotation if allowed), then opens a new shelf below.\n * Returns false when the part fits neither the current nor a new shelf on this\n * sheet (the caller then opens a new sheet).\n */\nfunction tryPlace(\n state: ShelfState,\n box: Bbox,\n idx: number,\n usableW: number,\n usableH: number,\n margin: number,\n spacing: number,\n allowRotation: boolean\n): boolean {\n const right = margin + usableW;\n const top = margin + usableH;\n\n // Candidate orientations: 0° always, 90° when allowed (and the box is not square).\n const orientations: { rot: 0 | 90; w: number; h: number }[] = [{ rot: 0, w: box.width, h: box.height }];\n if (allowRotation && Math.abs(box.width - box.height) > EPS) {\n orientations.push({ rot: 90, w: box.height, h: box.width });\n }\n\n const firstOnShelf = state.cursorX <= margin + EPS;\n\n // 1) Fit on the current open shelf. Prefer the orientation that keeps the shelf\n // shortest (least new vertical waste) among those that fit horizontally and\n // within the shelf's established height once started.\n let best: { rot: 0 | 90; w: number; h: number } | undefined;\n for (const o of orientations) {\n const xStart = firstOnShelf ? state.cursorX : state.cursorX + spacing;\n if (xStart + o.w > right + EPS) continue;\n // A started shelf has a fixed bottom; the part must fit under the usable top\n // from the shelf bottom (it may grow shelfH up to the top).\n if (state.shelfY + o.h > top + EPS) continue;\n if (best === undefined || o.h < best.h - EPS) best = o;\n }\n if (best !== undefined) {\n const xStart = firstOnShelf ? state.cursorX : state.cursorX + spacing;\n state.placements.push({ patternIndex: idx, x: xStart, y: state.shelfY, rotationDeg: best.rot });\n state.cursorX = xStart + best.w;\n state.shelfH = Math.max(state.shelfH, best.h);\n state.placedArea += best.w * best.h;\n return true;\n }\n\n // 2) Open a new shelf below the current one (if anything is on the current shelf).\n if (state.placements.length === 0) return false;\n const nextShelfY = state.shelfY + state.shelfH + spacing;\n let bestNew: { rot: 0 | 90; w: number; h: number } | undefined;\n for (const o of orientations) {\n if (margin + o.w > right + EPS) continue;\n if (nextShelfY + o.h > top + EPS) continue;\n if (bestNew === undefined || o.h < bestNew.h - EPS) bestNew = o;\n }\n if (bestNew === undefined) return false;\n state.shelfY = nextShelfY;\n state.shelfH = bestNew.h;\n state.cursorX = margin + bestNew.w;\n state.placements.push({ patternIndex: idx, x: margin, y: nextShelfY, rotationDeg: bestNew.rot });\n state.placedArea += bestNew.w * bestNew.h;\n return true;\n}\n\n/**\n * Axis-aligned bounding box of a pattern's OUTER outline wire, read from the wire\n * vertices (each edge start point) — the same 2D-coordinate read the DXF/SVG writers\n * use. Only the outer outline is packed; holes ride inside it.\n *\n * @param pattern - the flat pattern whose outer outline is measured.\n * @returns `Result<Bbox>` - the axis-aligned bounding box of the outer outline, or an\n * error if the outline is empty or degenerate.\n */\nexport function patternBbox(pattern: FlatPattern): Result<Bbox> {\n const edges = getEdges(pattern.outline);\n if (edges.length === 0) {\n return err(validationError('EMPTY_OUTLINE', 'pattern outline has no edges to bound'));\n }\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const edge of edges) {\n const p = curveStartPoint(edge);\n if (p[0] < minX) minX = p[0];\n if (p[0] > maxX) maxX = p[0];\n if (p[1] < minY) minY = p[1];\n if (p[1] > maxY) maxY = p[1];\n }\n const width = maxX - minX;\n const height = maxY - minY;\n if (!Number.isFinite(minX) || width <= EPS || height <= EPS) {\n return err(validationError('EMPTY_OUTLINE', 'pattern outline bounding box is degenerate'));\n }\n return ok({ minX, minY, width, height });\n}\n\n/**\n * Sheet-placement transform for one placement: shift the pattern's developed-plane\n * bbox lower-left to the origin, rotate `rotationDeg` (CCW) about it, then translate\n * the rotated shape so its rotated bounding box's lower-left lands at `(x, y)` on the\n * sheet. A point at developed `(px, py)` therefore lands inside the box the packer\n * reserved. The 0° and 90° branches are kept as closed-form integer-stable maps so\n * the bbox-strategy DXF output is byte-identical to the pre-NFP writer; finer angles\n * (180/270 and any nfp-emitted value) take the general rotation path.\n */\nfunction placementTransform(box: Bbox, x: number, y: number, rotationDeg: number): Transform2 {\n if (rotationDeg === 0) {\n return ([px, py]) => [x + (px - box.minX), y + (py - box.minY)];\n }\n if (rotationDeg === 90) {\n // After shifting to origin: (lx, ly) in [0,w]×[0,h]. Rotate 90° CCW: (-ly, lx)\n // → [-h,0]×[0,w]; add h to bring x back to [0,h], then translate to (x, y).\n return ([px, py]) => {\n const lx = px - box.minX;\n const ly = py - box.minY;\n return [x + (box.height - ly), y + lx];\n };\n }\n // General CCW rotation about the bbox lower-left, then re-anchor the rotated bbox's\n // lower-left to (x, y) so the placement matches the box the packer reserved.\n const rad = (rotationDeg * Math.PI) / 180;\n const c = Math.cos(rad);\n const s = Math.sin(rad);\n const corners: [number, number][] = [\n [0, 0],\n [box.width, 0],\n [box.width, box.height],\n [0, box.height],\n ];\n let minRx = Infinity;\n let minRy = Infinity;\n for (const [cx, cy] of corners) {\n const rx = cx * c - cy * s;\n const ry = cx * s + cy * c;\n if (rx < minRx) minRx = rx;\n if (ry < minRy) minRy = ry;\n }\n return ([px, py]) => {\n const lx = px - box.minX;\n const ly = py - box.minY;\n const rx = lx * c - ly * s;\n const ry = lx * s + ly * c;\n return [x + (rx - minRx), y + (ry - minRy)];\n };\n}\n\n/**\n * Emit one fabrication-ready DXF for a single nested sheet: every pattern placed on\n * `result.sheets[sheetIndex]` is translated and rotated onto the sheet (via the same\n * bounding boxes the packer used) and written through {@link multiPatternToDXF}.\n * `patterns` must be the SAME array passed to {@link nest} (placements index into it).\n *\n * @param result - the {@link NestResult} returned by {@link nest}.\n * @param patterns - the same `FlatPattern[]` array that was passed to {@link nest}.\n * @param sheetIndex - index into `result.sheets` of the sheet to emit.\n * @param options - optional DXF options forwarded to {@link multiPatternToDXF}.\n * @returns `Result<string>` - the DXF text for the selected sheet, or an error if the\n * sheet index is invalid, a placement references a missing pattern, or a pattern\n * outline cannot be measured.\n */\nexport function nestToDXF(\n result: NestResult,\n patterns: FlatPattern[],\n sheetIndex: number,\n options?: DxfOptions\n): Result<string> {\n const sheet = result.sheets[sheetIndex];\n if (sheet === undefined) {\n return err(validationError('INVALID_SHEET_INDEX', `no nested sheet at index ${sheetIndex} (have ${result.sheets.length})`));\n }\n const placed: PlacedPattern[] = [];\n for (const p of sheet.placements) {\n const pattern = patterns[p.patternIndex];\n if (pattern === undefined) {\n return err(validationError('INVALID_PATTERN_INDEX', `placement references missing pattern ${p.patternIndex}`));\n }\n const box = patternBbox(pattern);\n if (!box.ok) return box;\n placed.push({ pattern, transform: placementTransform(box.value, p.x, p.y, p.rotationDeg) });\n }\n return multiPatternToDXF(placed, options ?? {});\n}\n"]}
|