@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
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import { ok, err, validationError, line, wire, wireLoop, } from '@faicad/faijs/brepjs-compat';
|
|
2
|
+
import { featureTree, ROOT_FLAT_ID } from './featureTreeFns.js';
|
|
3
|
+
import { developedLength } from './allowanceFns.js';
|
|
4
|
+
import { sideFromAxisDir } from './internal.js';
|
|
5
|
+
/**
|
|
6
|
+
* Tree-driven analytic unfold of an arbitrary straight-bend part into its flat
|
|
7
|
+
* pattern. Walks the feature tree in BFS order placing every flat in the flat
|
|
8
|
+
* plane: the base occupies `[0,baseLength]×[0,width]`; each child flange lays a
|
|
9
|
+
* developed-bend strip (width = neutral-axis arc length) along the parent edge it
|
|
10
|
+
* folds from — respecting its offset/span along that edge — then its own flat past
|
|
11
|
+
* the strip, perpendicular to the edge and pointing outward. The developed outline
|
|
12
|
+
* is the rectilinear union of the base and every placed flat/strip rectangle,
|
|
13
|
+
* emitted as a single closed morph wire. A recorded corner miter replaces the
|
|
14
|
+
* shared reflex corner of two perpendicular base flanges with a 45° chamfer.
|
|
15
|
+
* Closed profiles produce a SEAM_CUT warning (the cycle-closing bend is left
|
|
16
|
+
* unfolded). Warnings ride inside the Ok payload.
|
|
17
|
+
*
|
|
18
|
+
* @param part - the sheet-metal part to develop into its flat pattern
|
|
19
|
+
* @returns the flat pattern (outline, bend lines, holes, form cuts/markers/hinges,
|
|
20
|
+
* lofted developments, developed area), the bend report, and any warnings — or
|
|
21
|
+
* an error if the part cannot be unfolded
|
|
22
|
+
*/
|
|
23
|
+
export function unfold(part) {
|
|
24
|
+
const treeResult = featureTree(part);
|
|
25
|
+
if (!treeResult.ok)
|
|
26
|
+
return treeResult;
|
|
27
|
+
const tree = treeResult.value;
|
|
28
|
+
const baseLength = part.baseLength;
|
|
29
|
+
if (!Number.isFinite(baseLength) || baseLength <= 0) {
|
|
30
|
+
return err(validationError('INVALID_BASE_LENGTH', `part baseLength must be positive, got ${baseLength}`));
|
|
31
|
+
}
|
|
32
|
+
const width = part.width;
|
|
33
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
34
|
+
return err(validationError('INVALID_WIDTH', `part width must be positive, got ${width}`));
|
|
35
|
+
}
|
|
36
|
+
const warnings = [...tree.warnings];
|
|
37
|
+
const layoutResult = layoutTree(part, tree, baseLength, width, (warning) => warnings.push(warning));
|
|
38
|
+
if (!layoutResult.ok)
|
|
39
|
+
return layoutResult;
|
|
40
|
+
const layout = layoutResult.value;
|
|
41
|
+
for (const placed of layout.flats) {
|
|
42
|
+
if (placed.kind !== 'flange')
|
|
43
|
+
continue;
|
|
44
|
+
if (part.thickness > 0 && placed.rule.innerRadius < part.thickness) {
|
|
45
|
+
warnings.push({
|
|
46
|
+
code: 'MIN_RADIUS',
|
|
47
|
+
message: `bend '${placed.id}' inner radius ${placed.rule.innerRadius} < thickness ${part.thickness}`,
|
|
48
|
+
featureId: placed.id,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Contour-flange developed strips lay out straight along their base edge; compute
|
|
53
|
+
// them first so their rectangles join the outline union below.
|
|
54
|
+
const contourResult = buildContourDevelopments(part);
|
|
55
|
+
if (!contourResult.ok)
|
|
56
|
+
return contourResult;
|
|
57
|
+
// Hems and jogs lay their multi-bend developed strips out straight along their
|
|
58
|
+
// region edge (the base or a flange), exactly like a contour flange.
|
|
59
|
+
const hemJogResult = buildHemJogDevelopments(part, layout);
|
|
60
|
+
if (!hemJogResult.ok)
|
|
61
|
+
return hemJogResult;
|
|
62
|
+
const rects = [];
|
|
63
|
+
for (const placed of layout.flats)
|
|
64
|
+
rects.push(placed.rect);
|
|
65
|
+
for (const strip of layout.strips)
|
|
66
|
+
rects.push(strip);
|
|
67
|
+
for (const strip of contourResult.value.strips)
|
|
68
|
+
rects.push(strip);
|
|
69
|
+
for (const strip of hemJogResult.value.strips)
|
|
70
|
+
rects.push(strip);
|
|
71
|
+
// Tabs are additive protrusions: their developed rectangles join the outer-outline
|
|
72
|
+
// union and add to the developed area.
|
|
73
|
+
const tabRects = (part.tabs ?? []).map((t) => ({ x0: t.rect[0], y0: t.rect[1], x1: t.rect[2], y1: t.rect[3] }));
|
|
74
|
+
for (const r of tabRects)
|
|
75
|
+
rects.push(r);
|
|
76
|
+
const notches = (part.reliefs ?? []).flatMap((relief) => relief.notches.map(([x0, y0, x1, y1]) => ({ x0, y0, x1, y1 })));
|
|
77
|
+
// The L-miter chamfer special-case can't express tabs either; once any tab is
|
|
78
|
+
// recorded, fall through to the rectilinear-union path (which absorbs tab rects).
|
|
79
|
+
// The 3D corner-miter cut still stands; only the developed chamfer is omitted.
|
|
80
|
+
const miters = tabRects.length > 0 ? [] : part.miters ?? [];
|
|
81
|
+
if (tabRects.length > 0 && (part.miters ?? []).length > 0) {
|
|
82
|
+
warnings.push({
|
|
83
|
+
code: 'MITER_NOT_DEVELOPED',
|
|
84
|
+
message: 'developed outline omits the corner-miter chamfer because a tab is present; the 3D miter cut is unaffected',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const outlineResult = buildOutline(rects, layout, miters, notches);
|
|
88
|
+
if (!outlineResult.ok)
|
|
89
|
+
return outlineResult;
|
|
90
|
+
layout.developedArea -= removedNotchArea(rects, notches);
|
|
91
|
+
for (const t of part.tabs ?? [])
|
|
92
|
+
layout.developedArea += t.area;
|
|
93
|
+
const holesResult = buildHoles(part);
|
|
94
|
+
if (!holesResult.ok)
|
|
95
|
+
return holesResult;
|
|
96
|
+
for (const c of part.cutouts ?? [])
|
|
97
|
+
layout.developedArea -= c.area;
|
|
98
|
+
const formsResult = buildForms(part);
|
|
99
|
+
if (!formsResult.ok)
|
|
100
|
+
return formsResult;
|
|
101
|
+
const loftedResult = buildLoftedDevelopments(part);
|
|
102
|
+
if (!loftedResult.ok)
|
|
103
|
+
return loftedResult;
|
|
104
|
+
for (const lf of part.loftedFlanges ?? []) {
|
|
105
|
+
layout.developedArea += lf.developedArea;
|
|
106
|
+
if (lf.approximate) {
|
|
107
|
+
warnings.push({
|
|
108
|
+
code: 'DEVELOPMENT_APPROXIMATE',
|
|
109
|
+
message: `lofted flange '${lf.id}' transition is not developable; its triangulated flat development is an approximation`,
|
|
110
|
+
featureId: lf.id,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const cf of part.contourFlanges ?? []) {
|
|
115
|
+
layout.developedArea += cf.developedLength * cf.span;
|
|
116
|
+
}
|
|
117
|
+
for (const h of part.hems ?? []) {
|
|
118
|
+
layout.developedArea += h.developedLength * h.span;
|
|
119
|
+
}
|
|
120
|
+
for (const j of part.jogs ?? []) {
|
|
121
|
+
layout.developedArea += j.developedLength * j.span;
|
|
122
|
+
}
|
|
123
|
+
const bendLines = layout.flats
|
|
124
|
+
.filter((p) => p.kind === 'flange')
|
|
125
|
+
.map((p) => ({
|
|
126
|
+
id: p.id,
|
|
127
|
+
line: bendLineEdge(p),
|
|
128
|
+
angleDeg: p.angleDeg,
|
|
129
|
+
direction: p.direction,
|
|
130
|
+
// The child frame's v axis is the develop-out direction; into-parent is its
|
|
131
|
+
// negation. Correct for chained flanges too (it is local to the placement).
|
|
132
|
+
inward: [-p.frame.v[0], -p.frame.v[1]],
|
|
133
|
+
}))
|
|
134
|
+
.concat(contourResult.value.bendLines)
|
|
135
|
+
.concat(hemJogResult.value.bendLines);
|
|
136
|
+
const pattern = {
|
|
137
|
+
outline: outlineResult.value,
|
|
138
|
+
bendLines,
|
|
139
|
+
holes: holesResult.value,
|
|
140
|
+
formCuts: formsResult.value.cuts,
|
|
141
|
+
formMarkers: formsResult.value.markers,
|
|
142
|
+
formHinges: formsResult.value.hinges,
|
|
143
|
+
loftedDevelopments: loftedResult.value,
|
|
144
|
+
developedArea: layout.developedArea,
|
|
145
|
+
};
|
|
146
|
+
const report = reportFromLayout(layout);
|
|
147
|
+
return ok({ pattern, report, warnings });
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Lay out the feature tree's flats in the flat plane, starting from the base and
|
|
151
|
+
* placing each bend's child flange along its parent edge: a developed strip laid
|
|
152
|
+
* outward from the edge, then the child's own flat. Produces the placed flats,
|
|
153
|
+
* their strips, and the total developed area.
|
|
154
|
+
*
|
|
155
|
+
* @param part - the sheet-metal part whose features are laid out
|
|
156
|
+
* @param tree - the feature tree built from the part
|
|
157
|
+
* @param baseLength - length of the base flat (u extent)
|
|
158
|
+
* @param width - width of the base flat (v extent)
|
|
159
|
+
* @param onWarning - optional callback invoked for warnings raised while unfolding
|
|
160
|
+
* @returns the laid-out flats, strips, and developed area, or an error if a
|
|
161
|
+
* referenced flat has no flange feature or is missing from the layout
|
|
162
|
+
*/
|
|
163
|
+
export function layoutTree(part, tree, baseLength, width, onWarning) {
|
|
164
|
+
const frames = new Map();
|
|
165
|
+
frames.set(ROOT_FLAT_ID, {
|
|
166
|
+
origin: [0, 0],
|
|
167
|
+
u: [1, 0],
|
|
168
|
+
v: [0, 1],
|
|
169
|
+
uLen: baseLength,
|
|
170
|
+
vLen: width,
|
|
171
|
+
});
|
|
172
|
+
const flats = [
|
|
173
|
+
{
|
|
174
|
+
kind: 'base',
|
|
175
|
+
id: ROOT_FLAT_ID,
|
|
176
|
+
frame: frames.get(ROOT_FLAT_ID),
|
|
177
|
+
rect: rectOf(frames.get(ROOT_FLAT_ID)),
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
const strips = [];
|
|
181
|
+
let developedArea = baseLength * width;
|
|
182
|
+
for (const treeBend of tree.bends) {
|
|
183
|
+
const childNode = tree.nodes.get(treeBend.child);
|
|
184
|
+
if (childNode === undefined || childNode.flange === undefined) {
|
|
185
|
+
return err(validationError('UNKNOWN_FLAT', `child flat '${treeBend.child}' has no flange feature`));
|
|
186
|
+
}
|
|
187
|
+
const flange = childNode.flange;
|
|
188
|
+
const parentFrame = frames.get(treeBend.parent);
|
|
189
|
+
if (parentFrame === undefined) {
|
|
190
|
+
return err(validationError('UNKNOWN_FLAT', `parent flat '${treeBend.parent}' missing from layout`));
|
|
191
|
+
}
|
|
192
|
+
const devResult = developedLength(treeBend.bend.angleDeg, part.thickness, treeBend.bend.rule, onWarning);
|
|
193
|
+
if (!devResult.ok)
|
|
194
|
+
return devResult;
|
|
195
|
+
const dev = devResult.value;
|
|
196
|
+
// Authored parts always record `baseEdge.side`; hand-built fixtures may encode
|
|
197
|
+
// the develop direction only via the bend axis, so fall back to that.
|
|
198
|
+
const side = flange.baseEdge.side ?? sideFromAxisDir(treeBend.bend.axisDir) ?? 'xmax';
|
|
199
|
+
const offset = flange.offset ?? flange.baseEdge.offset ?? 0;
|
|
200
|
+
const span = flange.span;
|
|
201
|
+
const length = flange.length;
|
|
202
|
+
const placed = placeChild(flange.id, parentFrame, side, offset, span, dev, length, {
|
|
203
|
+
angleDeg: treeBend.bend.angleDeg,
|
|
204
|
+
direction: treeBend.bend.direction,
|
|
205
|
+
rule: treeBend.bend.rule,
|
|
206
|
+
baseFull: treeBend.parent === ROOT_FLAT_ID && offset <= 1e-6 && span >= edgeLength(parentFrame, side) - 1e-6,
|
|
207
|
+
side,
|
|
208
|
+
});
|
|
209
|
+
frames.set(flange.id, placed.frame);
|
|
210
|
+
flats.push(placed);
|
|
211
|
+
strips.push(placed.strip);
|
|
212
|
+
developedArea += (dev + length) * span;
|
|
213
|
+
}
|
|
214
|
+
return ok({ baseLength, width, developedArea, flats, strips });
|
|
215
|
+
}
|
|
216
|
+
function edgeLength(f, side) {
|
|
217
|
+
return side === 'xmax' || side === 'xmin' ? f.vLen : f.uLen;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Place a child flange off `parent`'s edge `side`. The strip (developed bend) is
|
|
221
|
+
* laid from the edge outward by `dev`; the child flat follows for `length`. The
|
|
222
|
+
* child's own 2D frame starts past the strip so grandchildren chain correctly.
|
|
223
|
+
*/
|
|
224
|
+
function placeChild(id, parent, side, offset, span, dev, length, meta) {
|
|
225
|
+
// Edge basis in 2D: `along` runs the edge (bend axis), `out` points outward.
|
|
226
|
+
const { along, out, edgeOrigin } = edgeBasis2(parent, side);
|
|
227
|
+
const base = add2(edgeOrigin, scale2(along, offset));
|
|
228
|
+
const bendA = base;
|
|
229
|
+
const bendB = add2(base, scale2(along, span));
|
|
230
|
+
const stripFar = add2(base, scale2(out, dev));
|
|
231
|
+
const strip = rectFromCorners(base, add2(bendB, scale2(out, dev)));
|
|
232
|
+
const flatRect = rectFromCorners(stripFar, add2(add2(stripFar, scale2(along, span)), scale2(out, length)));
|
|
233
|
+
const childFrame = {
|
|
234
|
+
origin: stripFar,
|
|
235
|
+
u: along,
|
|
236
|
+
v: out,
|
|
237
|
+
uLen: span,
|
|
238
|
+
vLen: length,
|
|
239
|
+
};
|
|
240
|
+
return {
|
|
241
|
+
kind: 'flange',
|
|
242
|
+
id,
|
|
243
|
+
frame: childFrame,
|
|
244
|
+
rect: flatRect,
|
|
245
|
+
strip,
|
|
246
|
+
bendA,
|
|
247
|
+
bendB,
|
|
248
|
+
dev,
|
|
249
|
+
length,
|
|
250
|
+
span,
|
|
251
|
+
angleDeg: meta.angleDeg,
|
|
252
|
+
direction: meta.direction,
|
|
253
|
+
rule: meta.rule,
|
|
254
|
+
baseFull: meta.baseFull,
|
|
255
|
+
side: meta.side,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Edge origin and along/out unit directions for one side of a 2D frame. `along`
|
|
260
|
+
* runs along the edge (the bend axis), `out` points outward from the frame, and
|
|
261
|
+
* `edgeOrigin` is the starting corner of that edge.
|
|
262
|
+
*
|
|
263
|
+
* @param f - the 2D frame whose edge basis is computed
|
|
264
|
+
* @param side - which side of the frame ('xmax' | 'xmin' | 'ymax' | 'ymin')
|
|
265
|
+
* @returns the along/out unit directions and the edge's origin corner
|
|
266
|
+
*/
|
|
267
|
+
export function edgeBasis2(f, side) {
|
|
268
|
+
const uTop = add2(f.origin, scale2(f.u, f.uLen));
|
|
269
|
+
const vTop = add2(f.origin, scale2(f.v, f.vLen));
|
|
270
|
+
switch (side) {
|
|
271
|
+
case 'xmax':
|
|
272
|
+
return { along: f.v, out: f.u, edgeOrigin: uTop };
|
|
273
|
+
case 'xmin':
|
|
274
|
+
return { along: f.v, out: neg2(f.u), edgeOrigin: f.origin };
|
|
275
|
+
case 'ymax':
|
|
276
|
+
return { along: f.u, out: f.v, edgeOrigin: vTop };
|
|
277
|
+
case 'ymin':
|
|
278
|
+
return { along: f.u, out: neg2(f.v), edgeOrigin: f.origin };
|
|
279
|
+
default:
|
|
280
|
+
return side;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function bendLineEdge(p) {
|
|
284
|
+
return line([p.bendA[0], p.bendA[1], 0], [p.bendB[0], p.bendB[1], 0]);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* One closed developed-plane {@link Wire} per recorded cutout, traced from the
|
|
288
|
+
* feature's already-mapped `loop` (which {@link addCutout} positioned via the
|
|
289
|
+
* region's developed frame, so the loop sits at the cutout's matching flat-pattern
|
|
290
|
+
* spot). Emitted as interior holes of the pattern; the outer outline is unchanged.
|
|
291
|
+
*/
|
|
292
|
+
function buildHoles(part) {
|
|
293
|
+
const wires = [];
|
|
294
|
+
for (const c of part.cutouts ?? []) {
|
|
295
|
+
const loop = c.loop;
|
|
296
|
+
if (loop.length < 3) {
|
|
297
|
+
return err(validationError('CUTOUT_LOOP_TOO_SMALL', `cutout loop has ${loop.length} points, need ≥ 3`));
|
|
298
|
+
}
|
|
299
|
+
const edges = [];
|
|
300
|
+
for (let i = 0; i < loop.length; i += 1) {
|
|
301
|
+
const a = loop[i];
|
|
302
|
+
const b = loop[(i + 1) % loop.length];
|
|
303
|
+
if (a === undefined || b === undefined) {
|
|
304
|
+
return err(validationError('CUTOUT_LOOP_FAILED', 'failed to index cutout loop'));
|
|
305
|
+
}
|
|
306
|
+
edges.push(line([a[0], a[1], 0], [b[0], b[1], 0]));
|
|
307
|
+
}
|
|
308
|
+
const wire = wireLoop(edges);
|
|
309
|
+
if (!wire.ok)
|
|
310
|
+
return wire;
|
|
311
|
+
wires.push(wire.value);
|
|
312
|
+
}
|
|
313
|
+
return ok(wires);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Developed-plane wires for recorded form features: each louver U-cut and emboss
|
|
317
|
+
* footprint as a closed {@link Wire}, plus each louver hinge as an {@link Edge}. The
|
|
318
|
+
* loops were already mapped into developed coordinates when the form was recorded
|
|
319
|
+
* (via the region's developed frame), so this just traces them. Forms are net-
|
|
320
|
+
* material-neutral, so the outer outline and developed area are untouched.
|
|
321
|
+
*/
|
|
322
|
+
function buildForms(part) {
|
|
323
|
+
const cuts = [];
|
|
324
|
+
const markers = [];
|
|
325
|
+
const hinges = [];
|
|
326
|
+
for (const form of part.forms ?? []) {
|
|
327
|
+
for (const path of form.cuts) {
|
|
328
|
+
const cutWire = openPathWire(path);
|
|
329
|
+
if (!cutWire.ok)
|
|
330
|
+
return cutWire;
|
|
331
|
+
cuts.push(cutWire.value);
|
|
332
|
+
}
|
|
333
|
+
for (const loop of form.markers) {
|
|
334
|
+
const markerWire = closedLoopWire(loop);
|
|
335
|
+
if (!markerWire.ok)
|
|
336
|
+
return markerWire;
|
|
337
|
+
markers.push(markerWire.value);
|
|
338
|
+
}
|
|
339
|
+
if (form.hinge !== undefined) {
|
|
340
|
+
const [a, b] = form.hinge;
|
|
341
|
+
hinges.push(line([a[0], a[1], 0], [b[0], b[1], 0]));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return ok({ cuts, markers, hinges });
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Closed developed-plane {@link Wire}s for recorded lofted/ruled transition flanges.
|
|
348
|
+
* Each feature's triangulated `developedLoop` was already laid out flat when the
|
|
349
|
+
* flange was authored, so this just traces it into a wire; the boundary is non-
|
|
350
|
+
* rectilinear so it rides separately from the rectilinear-union outline.
|
|
351
|
+
*/
|
|
352
|
+
function buildLoftedDevelopments(part) {
|
|
353
|
+
const wires = [];
|
|
354
|
+
for (const lf of part.loftedFlanges ?? []) {
|
|
355
|
+
const w = closedLoopWire(lf.developedLoop);
|
|
356
|
+
if (!w.ok)
|
|
357
|
+
return w;
|
|
358
|
+
wires.push(w.value);
|
|
359
|
+
}
|
|
360
|
+
return ok(wires);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Lay each recorded contour flange's developed strip out STRAIGHT along its base
|
|
364
|
+
* edge: a rectangle `span` wide along the edge by `developedLength` out from it,
|
|
365
|
+
* returned for the outer-outline union. One bend line per arc segment is placed at
|
|
366
|
+
* that segment's cumulative developed offset out from the base edge — the EXACT
|
|
367
|
+
* arc-length sum, so the strip length equals Σ segment developed lengths with no
|
|
368
|
+
* error.
|
|
369
|
+
*/
|
|
370
|
+
function buildContourDevelopments(part) {
|
|
371
|
+
const strips = [];
|
|
372
|
+
const bendLines = [];
|
|
373
|
+
const baseFrame = {
|
|
374
|
+
origin: [0, 0],
|
|
375
|
+
u: [1, 0],
|
|
376
|
+
v: [0, 1],
|
|
377
|
+
uLen: part.baseLength,
|
|
378
|
+
vLen: part.width,
|
|
379
|
+
};
|
|
380
|
+
for (const cf of part.contourFlanges ?? []) {
|
|
381
|
+
const { along, out, edgeOrigin } = edgeBasis2(baseFrame, cf.side);
|
|
382
|
+
const base = add2(edgeOrigin, scale2(along, cf.offset));
|
|
383
|
+
strips.push(rectFromCorners(base, add2(add2(base, scale2(along, cf.span)), scale2(out, cf.developedLength))));
|
|
384
|
+
let cumulative = 0;
|
|
385
|
+
for (const seg of cf.segments) {
|
|
386
|
+
if (seg.kind === 'arc') {
|
|
387
|
+
const a = add2(base, scale2(out, cumulative));
|
|
388
|
+
const b = add2(a, scale2(along, cf.span));
|
|
389
|
+
bendLines.push({
|
|
390
|
+
id: seg.bendId ?? cf.id,
|
|
391
|
+
line: line([a[0], a[1], 0], [b[0], b[1], 0]),
|
|
392
|
+
angleDeg: seg.angleDeg ?? 0,
|
|
393
|
+
direction: seg.direction ?? 'up',
|
|
394
|
+
inward: [-out[0], -out[1]],
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
cumulative += seg.dev;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return ok({ strips, bendLines });
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Lay each recorded hem and jog developed strip out STRAIGHT along its region edge
|
|
404
|
+
* (the base or a flange), mirroring {@link buildContourDevelopments}. The region's
|
|
405
|
+
* developed-plane {@link Frame2} comes from the tree layout (so a hem on a flange
|
|
406
|
+
* lands on that flange's developed face). Each sub-bend (a hem curl arc or a jog
|
|
407
|
+
* step arc) gets a bend line at its EXACT cumulative developed offset out from the
|
|
408
|
+
* edge — Σ segment developed lengths, so the strip length is exact.
|
|
409
|
+
*/
|
|
410
|
+
function buildHemJogDevelopments(part, layout) {
|
|
411
|
+
const strips = [];
|
|
412
|
+
const bendLines = [];
|
|
413
|
+
const frameFor = (region) => {
|
|
414
|
+
const id = region === 'base' || region === 'face-0' ? ROOT_FLAT_ID : region;
|
|
415
|
+
for (const p of layout.flats) {
|
|
416
|
+
if (p.id === id)
|
|
417
|
+
return p.frame;
|
|
418
|
+
}
|
|
419
|
+
return undefined;
|
|
420
|
+
};
|
|
421
|
+
const allStrips = [
|
|
422
|
+
...(part.hems ?? []).map((h) => ({
|
|
423
|
+
region: h.region,
|
|
424
|
+
side: h.side,
|
|
425
|
+
offset: h.offset,
|
|
426
|
+
span: h.span,
|
|
427
|
+
developedLength: h.developedLength,
|
|
428
|
+
segments: h.segments,
|
|
429
|
+
})),
|
|
430
|
+
...(part.jogs ?? []).map((j) => ({
|
|
431
|
+
region: j.region,
|
|
432
|
+
side: j.side,
|
|
433
|
+
offset: j.offset,
|
|
434
|
+
span: j.span,
|
|
435
|
+
developedLength: j.developedLength,
|
|
436
|
+
segments: j.segments,
|
|
437
|
+
})),
|
|
438
|
+
];
|
|
439
|
+
for (const s of allStrips) {
|
|
440
|
+
const regionFrame = frameFor(s.region);
|
|
441
|
+
if (regionFrame === undefined) {
|
|
442
|
+
return err(validationError('UNKNOWN_HEMJOG_REGION', `hem/jog region '${s.region}' missing from layout`));
|
|
443
|
+
}
|
|
444
|
+
const { along, out, edgeOrigin } = edgeBasis2(regionFrame, s.side);
|
|
445
|
+
const base = add2(edgeOrigin, scale2(along, s.offset));
|
|
446
|
+
strips.push(rectFromCorners(base, add2(add2(base, scale2(along, s.span)), scale2(out, s.developedLength))));
|
|
447
|
+
let cumulative = 0;
|
|
448
|
+
for (const seg of s.segments) {
|
|
449
|
+
if (seg.kind === 'arc') {
|
|
450
|
+
const a = add2(base, scale2(out, cumulative));
|
|
451
|
+
const b = add2(a, scale2(along, s.span));
|
|
452
|
+
bendLines.push({
|
|
453
|
+
id: seg.bendId ?? s.region,
|
|
454
|
+
line: line([a[0], a[1], 0], [b[0], b[1], 0]),
|
|
455
|
+
angleDeg: seg.angleDeg ?? 0,
|
|
456
|
+
direction: seg.direction ?? 'up',
|
|
457
|
+
inward: [-out[0], -out[1]],
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
cumulative += seg.dev;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return ok({ strips, bendLines });
|
|
464
|
+
}
|
|
465
|
+
/** Trace a closed developed-plane loop (≥ 3 points) into a morph {@link Wire}. */
|
|
466
|
+
function closedLoopWire(loop) {
|
|
467
|
+
if (loop.length < 3) {
|
|
468
|
+
return err(validationError('FORM_LOOP_TOO_SMALL', `form loop has ${loop.length} points, need ≥ 3`));
|
|
469
|
+
}
|
|
470
|
+
const edges = [];
|
|
471
|
+
for (let i = 0; i < loop.length; i += 1) {
|
|
472
|
+
const a = loop[i];
|
|
473
|
+
const b = loop[(i + 1) % loop.length];
|
|
474
|
+
if (a === undefined || b === undefined) {
|
|
475
|
+
return err(validationError('FORM_LOOP_FAILED', 'failed to index form loop'));
|
|
476
|
+
}
|
|
477
|
+
edges.push(line([a[0], a[1], 0], [b[0], b[1], 0]));
|
|
478
|
+
}
|
|
479
|
+
return wireLoop(edges);
|
|
480
|
+
}
|
|
481
|
+
/** Trace an OPEN developed-plane path (≥ 2 points) into a morph {@link Wire}. */
|
|
482
|
+
function openPathWire(path) {
|
|
483
|
+
if (path.length < 2) {
|
|
484
|
+
return err(validationError('FORM_PATH_TOO_SHORT', `form cut path has ${path.length} points, need ≥ 2`));
|
|
485
|
+
}
|
|
486
|
+
const edges = [];
|
|
487
|
+
for (let i = 0; i + 1 < path.length; i += 1) {
|
|
488
|
+
const a = path[i];
|
|
489
|
+
const b = path[i + 1];
|
|
490
|
+
if (a === undefined || b === undefined) {
|
|
491
|
+
return err(validationError('FORM_LOOP_FAILED', 'failed to index form cut path'));
|
|
492
|
+
}
|
|
493
|
+
edges.push(line([a[0], a[1], 0], [b[0], b[1], 0]));
|
|
494
|
+
}
|
|
495
|
+
return wire(edges);
|
|
496
|
+
}
|
|
497
|
+
function rectOf(f) {
|
|
498
|
+
const c0 = f.origin;
|
|
499
|
+
const c1 = add2(add2(f.origin, scale2(f.u, f.uLen)), scale2(f.v, f.vLen));
|
|
500
|
+
return rectFromCorners(c0, c1);
|
|
501
|
+
}
|
|
502
|
+
function rectFromCorners(a, b) {
|
|
503
|
+
return {
|
|
504
|
+
x0: Math.min(a[0], b[0]),
|
|
505
|
+
y0: Math.min(a[1], b[1]),
|
|
506
|
+
x1: Math.max(a[0], b[0]),
|
|
507
|
+
y1: Math.max(a[1], b[1]),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
function add2(a, b) {
|
|
511
|
+
return [a[0] + b[0], a[1] + b[1]];
|
|
512
|
+
}
|
|
513
|
+
function scale2(a, s) {
|
|
514
|
+
return [a[0] * s, a[1] * s];
|
|
515
|
+
}
|
|
516
|
+
function neg2(a) {
|
|
517
|
+
return [-a[0], -a[1]];
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Outline of the developed pattern. For the simple two-perpendicular-flange L
|
|
521
|
+
* (base + an east + a north flange) a recorded corner miter chamfers the reflex
|
|
522
|
+
* vertex; otherwise the outline is the rectilinear union of all placed rectangles
|
|
523
|
+
* emitted as one closed loop. Holes are not possible for the supported shapes
|
|
524
|
+
* (every arm attaches to the simply-connected base or a chain thereof).
|
|
525
|
+
*/
|
|
526
|
+
function buildOutline(rects, layout, miters, notches) {
|
|
527
|
+
// The lShapedMiter chamfer special-case can't express a notch, so once any relief
|
|
528
|
+
// is recorded fall through to the rectilinear-union path (which subtracts notches).
|
|
529
|
+
const miterCorners = notches.length > 0 ? undefined : lShapedMiter(layout, miters);
|
|
530
|
+
const corners = miterCorners ?? rectilinearUnion(rects, notches);
|
|
531
|
+
if (corners.length < 3) {
|
|
532
|
+
return err(validationError('OUTLINE_BUILD_FAILED', 'developed outline has fewer than 3 vertices'));
|
|
533
|
+
}
|
|
534
|
+
const edges = [];
|
|
535
|
+
for (let i = 0; i < corners.length; i += 1) {
|
|
536
|
+
const from = corners[i];
|
|
537
|
+
const to = corners[(i + 1) % corners.length];
|
|
538
|
+
if (from === undefined || to === undefined) {
|
|
539
|
+
return err(validationError('OUTLINE_BUILD_FAILED', 'failed to index outline corners'));
|
|
540
|
+
}
|
|
541
|
+
edges.push(line([from[0], from[1], 0], [to[0], to[1], 0]));
|
|
542
|
+
}
|
|
543
|
+
return wireLoop(edges);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Special-case the classic L: a base with exactly one east (+X) and one north
|
|
547
|
+
* (+Y) full-span flange and a recorded miter between them. Returns the chamfered
|
|
548
|
+
* hexagon; `undefined` if this isn't that shape (fall back to the union outline).
|
|
549
|
+
*/
|
|
550
|
+
function lShapedMiter(layout, miters) {
|
|
551
|
+
const flanges = layout.flats.filter((p) => p.kind === 'flange');
|
|
552
|
+
if (flanges.length !== 2)
|
|
553
|
+
return undefined;
|
|
554
|
+
const east = flanges.find((f) => f.baseFull && f.side === 'xmax');
|
|
555
|
+
const north = flanges.find((f) => f.baseFull && f.side === 'ymax');
|
|
556
|
+
if (east === undefined || north === undefined)
|
|
557
|
+
return undefined;
|
|
558
|
+
const baseLength = layout.baseLength;
|
|
559
|
+
const width = layout.width;
|
|
560
|
+
const maxX = baseLength + east.dev + east.length;
|
|
561
|
+
const maxY = width + north.dev + north.length;
|
|
562
|
+
const gap = miterGapFor(miters, east.id, north.id);
|
|
563
|
+
if (gap === undefined || gap <= 0) {
|
|
564
|
+
return [
|
|
565
|
+
[0, 0],
|
|
566
|
+
[maxX, 0],
|
|
567
|
+
[maxX, width],
|
|
568
|
+
[baseLength, width],
|
|
569
|
+
[baseLength, maxY],
|
|
570
|
+
[0, maxY],
|
|
571
|
+
];
|
|
572
|
+
}
|
|
573
|
+
return [
|
|
574
|
+
[0, 0],
|
|
575
|
+
[maxX, 0],
|
|
576
|
+
[maxX, width],
|
|
577
|
+
[baseLength + gap, width],
|
|
578
|
+
[baseLength, width + gap],
|
|
579
|
+
[baseLength, maxY],
|
|
580
|
+
[0, maxY],
|
|
581
|
+
];
|
|
582
|
+
}
|
|
583
|
+
function miterGapFor(miters, aId, bId) {
|
|
584
|
+
for (const m of miters) {
|
|
585
|
+
if ((m.flangeA === aId && m.flangeB === bId) || (m.flangeA === bId && m.flangeB === aId)) {
|
|
586
|
+
return m.gap;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Outline loop of the union of axis-aligned rectangles, assuming the union is a
|
|
593
|
+
* single simply-connected rectilinear region (true for every shape this package
|
|
594
|
+
* authors: arms attached to a connected base). Built by a vertical-line sweep
|
|
595
|
+
* tracking the covered y-spans, then tracing the boundary contour. The sweep emits
|
|
596
|
+
* a CCW loop of corner points with no collinear duplicates.
|
|
597
|
+
*/
|
|
598
|
+
function rectilinearUnion(rects, notches = []) {
|
|
599
|
+
const cuts = [...rects.flatMap((r) => [r.x0, r.x1]), ...notches.flatMap((r) => [r.x0, r.x1])];
|
|
600
|
+
const cutsY = [...rects.flatMap((r) => [r.y0, r.y1]), ...notches.flatMap((r) => [r.y0, r.y1])];
|
|
601
|
+
const xs = uniqueSorted(cuts);
|
|
602
|
+
const ys = uniqueSorted(cutsY);
|
|
603
|
+
if (xs.length < 2 || ys.length < 2)
|
|
604
|
+
return [];
|
|
605
|
+
// Cell-occupancy grid: a cell is filled if its centre lies in the rectangle union
|
|
606
|
+
// and in no notch (notches subtract material; they sit on the boundary so the
|
|
607
|
+
// result stays simply-connected).
|
|
608
|
+
const nx = xs.length - 1;
|
|
609
|
+
const ny = ys.length - 1;
|
|
610
|
+
const filled = new Set();
|
|
611
|
+
for (let i = 0; i < nx; i += 1) {
|
|
612
|
+
const cx = (xs[i] + xs[i + 1]) / 2;
|
|
613
|
+
for (let j = 0; j < ny; j += 1) {
|
|
614
|
+
const cy = (ys[j] + ys[j + 1]) / 2;
|
|
615
|
+
const inRect = rects.some((r) => cx > r.x0 && cx < r.x1 && cy > r.y0 && cy < r.y1);
|
|
616
|
+
const inNotch = notches.some((r) => cx > r.x0 && cx < r.x1 && cy > r.y0 && cy < r.y1);
|
|
617
|
+
if (inRect && !inNotch)
|
|
618
|
+
filled.add(i * ny + j);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
// Collect boundary unit-edges (segments between adjacent grid nodes that border
|
|
622
|
+
// a filled and an unfilled cell), oriented CCW (filled cell on the left).
|
|
623
|
+
const segs = [];
|
|
624
|
+
const isFilled = (i, j) => i >= 0 && i < nx && j >= 0 && j < ny && filled.has(i * ny + j);
|
|
625
|
+
for (let i = 0; i < nx; i += 1) {
|
|
626
|
+
for (let j = 0; j < ny; j += 1) {
|
|
627
|
+
if (!isFilled(i, j))
|
|
628
|
+
continue;
|
|
629
|
+
const x0 = xs[i];
|
|
630
|
+
const x1 = xs[i + 1];
|
|
631
|
+
const y0 = ys[j];
|
|
632
|
+
const y1 = ys[j + 1];
|
|
633
|
+
// Bottom edge: boundary if cell below is empty. CCW → left-to-right.
|
|
634
|
+
if (!isFilled(i, j - 1))
|
|
635
|
+
segs.push({ a: [x0, y0], b: [x1, y0] });
|
|
636
|
+
// Top edge: boundary if cell above empty. CCW → right-to-left.
|
|
637
|
+
if (!isFilled(i, j + 1))
|
|
638
|
+
segs.push({ a: [x1, y1], b: [x0, y1] });
|
|
639
|
+
// Left edge: boundary if cell left empty. CCW → top-to-bottom.
|
|
640
|
+
if (!isFilled(i - 1, j))
|
|
641
|
+
segs.push({ a: [x0, y1], b: [x0, y0] });
|
|
642
|
+
// Right edge: boundary if cell right empty. CCW → bottom-to-top.
|
|
643
|
+
if (!isFilled(i + 1, j))
|
|
644
|
+
segs.push({ a: [x1, y0], b: [x1, y1] });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return traceLoop(segs);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Area of material removed by the relief notches: the portion of each notch that
|
|
651
|
+
* overlaps the rectangle union, summed over a shared cell grid so overlapping
|
|
652
|
+
* notches (e.g. two corner reliefs meeting) are not double-counted.
|
|
653
|
+
*/
|
|
654
|
+
function removedNotchArea(rects, notches) {
|
|
655
|
+
if (notches.length === 0)
|
|
656
|
+
return 0;
|
|
657
|
+
const xs = uniqueSorted([
|
|
658
|
+
...rects.flatMap((r) => [r.x0, r.x1]),
|
|
659
|
+
...notches.flatMap((r) => [r.x0, r.x1]),
|
|
660
|
+
]);
|
|
661
|
+
const ys = uniqueSorted([
|
|
662
|
+
...rects.flatMap((r) => [r.y0, r.y1]),
|
|
663
|
+
...notches.flatMap((r) => [r.y0, r.y1]),
|
|
664
|
+
]);
|
|
665
|
+
let area = 0;
|
|
666
|
+
for (let i = 0; i + 1 < xs.length; i += 1) {
|
|
667
|
+
const x0 = xs[i];
|
|
668
|
+
const x1 = xs[i + 1];
|
|
669
|
+
const cx = (x0 + x1) / 2;
|
|
670
|
+
for (let j = 0; j + 1 < ys.length; j += 1) {
|
|
671
|
+
const y0 = ys[j];
|
|
672
|
+
const y1 = ys[j + 1];
|
|
673
|
+
const cy = (y0 + y1) / 2;
|
|
674
|
+
const inRect = rects.some((r) => cx > r.x0 && cx < r.x1 && cy > r.y0 && cy < r.y1);
|
|
675
|
+
const inNotch = notches.some((r) => cx > r.x0 && cx < r.x1 && cy > r.y0 && cy < r.y1);
|
|
676
|
+
if (inRect && inNotch)
|
|
677
|
+
area += (x1 - x0) * (y1 - y0);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return area;
|
|
681
|
+
}
|
|
682
|
+
/** Chain oriented boundary segments into a single loop, dropping collinear points. */
|
|
683
|
+
function traceLoop(segs) {
|
|
684
|
+
if (segs.length === 0)
|
|
685
|
+
return [];
|
|
686
|
+
const key = (p) => `${p[0]}|${p[1]}`;
|
|
687
|
+
const from = new Map();
|
|
688
|
+
for (const s of segs)
|
|
689
|
+
from.set(key(s.a), s.b);
|
|
690
|
+
const startSeg = segs[0];
|
|
691
|
+
const start = startSeg.a;
|
|
692
|
+
const raw = [start];
|
|
693
|
+
let cur = startSeg.b;
|
|
694
|
+
let guard = 0;
|
|
695
|
+
const limit = segs.length + 4;
|
|
696
|
+
while (key(cur) !== key(start) && guard < limit) {
|
|
697
|
+
raw.push(cur);
|
|
698
|
+
const next = from.get(key(cur));
|
|
699
|
+
if (next === undefined)
|
|
700
|
+
break;
|
|
701
|
+
cur = next;
|
|
702
|
+
guard += 1;
|
|
703
|
+
}
|
|
704
|
+
// Drop collinear interior points so consecutive edges aren't degenerate.
|
|
705
|
+
const out = [];
|
|
706
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
707
|
+
const prev = raw[(i - 1 + raw.length) % raw.length];
|
|
708
|
+
const here = raw[i];
|
|
709
|
+
const next = raw[(i + 1) % raw.length];
|
|
710
|
+
const d1x = here[0] - prev[0];
|
|
711
|
+
const d1y = here[1] - prev[1];
|
|
712
|
+
const d2x = next[0] - here[0];
|
|
713
|
+
const d2y = next[1] - here[1];
|
|
714
|
+
if (Math.abs(d1x * d2y - d1y * d2x) > 1e-9)
|
|
715
|
+
out.push(here);
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
718
|
+
}
|
|
719
|
+
function uniqueSorted(values) {
|
|
720
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
721
|
+
const out = [];
|
|
722
|
+
for (const v of sorted) {
|
|
723
|
+
const last = out[out.length - 1];
|
|
724
|
+
if (last === undefined || Math.abs(v - last) > 1e-9)
|
|
725
|
+
out.push(v);
|
|
726
|
+
}
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Build the bend report for a laid-out tree: one entry per placed flange with its
|
|
731
|
+
* angle, radius, allowance, flat length, and direction, plus the overall
|
|
732
|
+
* bounding-box span of the developed pattern.
|
|
733
|
+
*
|
|
734
|
+
* @param layout - the tree layout produced by {@link layoutTree}
|
|
735
|
+
* @returns the bend entries and the total flat pattern size
|
|
736
|
+
*/
|
|
737
|
+
export function reportFromLayout(layout) {
|
|
738
|
+
const flanges = layout.flats.filter((p) => p.kind === 'flange');
|
|
739
|
+
const bends = flanges.map((f) => ({
|
|
740
|
+
id: f.id,
|
|
741
|
+
angleDeg: f.angleDeg,
|
|
742
|
+
radius: f.rule.innerRadius,
|
|
743
|
+
allowance: f.dev,
|
|
744
|
+
flatLength: f.length,
|
|
745
|
+
direction: f.direction,
|
|
746
|
+
}));
|
|
747
|
+
// totalFlatSize is the overall bounding-box span of the developed pattern, which
|
|
748
|
+
// for parts whose flanges extend into negative coordinates (xmin/ymin sides) is
|
|
749
|
+
// the max−min extent, not just the max.
|
|
750
|
+
let minX = 0;
|
|
751
|
+
let minY = 0;
|
|
752
|
+
let maxX = layout.baseLength;
|
|
753
|
+
let maxY = layout.width;
|
|
754
|
+
for (const r of layout.flats.map((p) => p.rect).concat(layout.strips)) {
|
|
755
|
+
if (r.x0 < minX)
|
|
756
|
+
minX = r.x0;
|
|
757
|
+
if (r.y0 < minY)
|
|
758
|
+
minY = r.y0;
|
|
759
|
+
if (r.x1 > maxX)
|
|
760
|
+
maxX = r.x1;
|
|
761
|
+
if (r.y1 > maxY)
|
|
762
|
+
maxY = r.y1;
|
|
763
|
+
}
|
|
764
|
+
return { bends, totalFlatSize: [maxX - minX, maxY - minY] };
|
|
765
|
+
}
|
|
766
|
+
//# sourceMappingURL=unfoldFns.js.map
|