@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.
Files changed (113) hide show
  1. package/dist/allowanceFns.d.ts +42 -0
  2. package/dist/allowanceFns.d.ts.map +1 -0
  3. package/dist/allowanceFns.js +63 -0
  4. package/dist/allowanceFns.js.map +1 -0
  5. package/dist/api.d.ts +312 -0
  6. package/dist/api.d.ts.map +1 -0
  7. package/dist/api.js +367 -0
  8. package/dist/api.js.map +1 -0
  9. package/dist/authorFns.d.ts +118 -0
  10. package/dist/authorFns.d.ts.map +1 -0
  11. package/dist/authorFns.js +562 -0
  12. package/dist/authorFns.js.map +1 -0
  13. package/dist/bendTableFns.d.ts +87 -0
  14. package/dist/bendTableFns.d.ts.map +1 -0
  15. package/dist/bendTableFns.js +345 -0
  16. package/dist/bendTableFns.js.map +1 -0
  17. package/dist/compat.d.ts +90 -0
  18. package/dist/compat.d.ts.map +1 -0
  19. package/dist/compat.js +99 -0
  20. package/dist/compat.js.map +1 -0
  21. package/dist/contourFlangeFns.d.ts +122 -0
  22. package/dist/contourFlangeFns.d.ts.map +1 -0
  23. package/dist/contourFlangeFns.js +403 -0
  24. package/dist/contourFlangeFns.js.map +1 -0
  25. package/dist/cutoutFns.d.ts +95 -0
  26. package/dist/cutoutFns.d.ts.map +1 -0
  27. package/dist/cutoutFns.js +300 -0
  28. package/dist/cutoutFns.js.map +1 -0
  29. package/dist/dxfFns.d.ts +48 -0
  30. package/dist/dxfFns.d.ts.map +1 -0
  31. package/dist/dxfFns.js +236 -0
  32. package/dist/dxfFns.js.map +1 -0
  33. package/dist/facade.d.ts +449 -0
  34. package/dist/facade.d.ts.map +1 -0
  35. package/dist/facade.js +535 -0
  36. package/dist/facade.js.map +1 -0
  37. package/dist/featureTreeFns.d.ts +76 -0
  38. package/dist/featureTreeFns.d.ts.map +1 -0
  39. package/dist/featureTreeFns.js +190 -0
  40. package/dist/featureTreeFns.js.map +1 -0
  41. package/dist/foldFns.d.ts +83 -0
  42. package/dist/foldFns.d.ts.map +1 -0
  43. package/dist/foldFns.js +744 -0
  44. package/dist/foldFns.js.map +1 -0
  45. package/dist/foreignUnfoldFns.d.ts +62 -0
  46. package/dist/foreignUnfoldFns.d.ts.map +1 -0
  47. package/dist/foreignUnfoldFns.js +854 -0
  48. package/dist/foreignUnfoldFns.js.map +1 -0
  49. package/dist/formFns.d.ts +58 -0
  50. package/dist/formFns.d.ts.map +1 -0
  51. package/dist/formFns.js +300 -0
  52. package/dist/formFns.js.map +1 -0
  53. package/dist/hemFns.d.ts +17 -0
  54. package/dist/hemFns.d.ts.map +1 -0
  55. package/dist/hemFns.js +212 -0
  56. package/dist/hemFns.js.map +1 -0
  57. package/dist/index.d.ts +43 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +32 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/internal.d.ts +27 -0
  62. package/dist/internal.d.ts.map +1 -0
  63. package/dist/internal.js +38 -0
  64. package/dist/internal.js.map +1 -0
  65. package/dist/jogFns.d.ts +18 -0
  66. package/dist/jogFns.d.ts.map +1 -0
  67. package/dist/jogFns.js +180 -0
  68. package/dist/jogFns.js.map +1 -0
  69. package/dist/loftedFlangeFns.d.ts +23 -0
  70. package/dist/loftedFlangeFns.d.ts.map +1 -0
  71. package/dist/loftedFlangeFns.js +306 -0
  72. package/dist/loftedFlangeFns.js.map +1 -0
  73. package/dist/materials.d.ts +16 -0
  74. package/dist/materials.d.ts.map +1 -0
  75. package/dist/materials.js +44 -0
  76. package/dist/materials.js.map +1 -0
  77. package/dist/miterFns.d.ts +31 -0
  78. package/dist/miterFns.d.ts.map +1 -0
  79. package/dist/miterFns.js +128 -0
  80. package/dist/miterFns.js.map +1 -0
  81. package/dist/nestFns.d.ts +116 -0
  82. package/dist/nestFns.d.ts.map +1 -0
  83. package/dist/nestFns.js +517 -0
  84. package/dist/nestFns.js.map +1 -0
  85. package/dist/polygonFns.d.ts +144 -0
  86. package/dist/polygonFns.d.ts.map +1 -0
  87. package/dist/polygonFns.js +324 -0
  88. package/dist/polygonFns.js.map +1 -0
  89. package/dist/reliefFns.d.ts +44 -0
  90. package/dist/reliefFns.d.ts.map +1 -0
  91. package/dist/reliefFns.js +367 -0
  92. package/dist/reliefFns.js.map +1 -0
  93. package/dist/reportFns.d.ts +33 -0
  94. package/dist/reportFns.d.ts.map +1 -0
  95. package/dist/reportFns.js +71 -0
  96. package/dist/reportFns.js.map +1 -0
  97. package/dist/tabFns.d.ts +43 -0
  98. package/dist/tabFns.d.ts.map +1 -0
  99. package/dist/tabFns.js +179 -0
  100. package/dist/tabFns.js.map +1 -0
  101. package/dist/types.d.ts +629 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/types.js +2 -0
  104. package/dist/types.js.map +1 -0
  105. package/dist/unfoldFns.d.ts +120 -0
  106. package/dist/unfoldFns.d.ts.map +1 -0
  107. package/dist/unfoldFns.js +766 -0
  108. package/dist/unfoldFns.js.map +1 -0
  109. package/dist/validateFns.d.ts +18 -0
  110. package/dist/validateFns.d.ts.map +1 -0
  111. package/dist/validateFns.js +196 -0
  112. package/dist/validateFns.js.map +1 -0
  113. package/package.json +45 -0
@@ -0,0 +1,562 @@
1
+ import { ok, err, validationError, box, cylinder, fuse, cut, intersect, rotate, translate, vecAdd, vecSub, vecScale, vecDot, vecCross, vecNormalize, } from '@faicad/faijs/brepjs-compat';
2
+ import { normalizeSolid } from './internal.js';
3
+ import { ROOT_FLAT_ID, featureTree } from './featureTreeFns.js';
4
+ /**
5
+ * Stable edge reference for flange attachment. The base flat is `face-0`; every
6
+ * flange face is `face-<n+1>` in authoring order. The leading edge of a flat is
7
+ * `edgeIndex 0`. The reference also carries `parentId`/`side`/`offset`/`extent`
8
+ * so the feature tree and recursive unfold can resolve the exact parent edge a
9
+ * flange folds from without reading topology back out of the B-rep.
10
+ *
11
+ * @param faceIndex - the face index of the flat: `0` for the base, `n+1` for the
12
+ * flange authored in the `n`-th position.
13
+ * @returns an {@link EdgeRef} pointing at the flat's leading edge (`edgeIndex 0`).
14
+ */
15
+ export function baseEdgeRef(faceIndex) {
16
+ return { kind: 'index', faceIndex, edgeIndex: 0 };
17
+ }
18
+ /**
19
+ * Author a sheet-metal part: a base flat plus an arbitrary tree of flanges. Each
20
+ * flange folds off one of the four edges of its parent flat (the base by default,
21
+ * or another flange via `parent`), in the requested direction (up/down), over an
22
+ * optional sub-span of that edge (`offset`/`width`). Returns a {@link SheetMetalPart}
23
+ * carrying the folded 3D solid and the recorded bend feature tree the unfold
24
+ * consumes. All construction stays on the public, OCCT-WASM-safe API.
25
+ *
26
+ * @param spec - the authoring spec: sheet thickness, base flat dimensions, the
27
+ * flange list, optional material, and optional closing seams.
28
+ * @returns a `Result<SheetMetalPart>` — `ok` with the folded part on success, or
29
+ * `err` with a `validationError` for an invalid thickness / base / flange /
30
+ * seam specification.
31
+ */
32
+ export function authorPart(spec) {
33
+ const { thickness } = spec;
34
+ if (!Number.isFinite(thickness) || thickness <= 0) {
35
+ return err(validationError('INVALID_THICKNESS', `thickness must be positive, got ${thickness}`));
36
+ }
37
+ const { length: baseLen, width } = spec.base;
38
+ if (!Number.isFinite(baseLen) || baseLen <= 0) {
39
+ return err(validationError('INVALID_BASE_LENGTH', `base.length must be positive, got ${baseLen}`));
40
+ }
41
+ if (!Number.isFinite(width) || width <= 0) {
42
+ return err(validationError('INVALID_BASE_WIDTH', `base.width must be positive, got ${width}`));
43
+ }
44
+ const seen = new Set();
45
+ for (const flange of spec.flanges) {
46
+ // `::` is reserved as the seam-bend id delimiter (`seam::<parent>::<child>`);
47
+ // allowing it in a flange id would make the seam parent/child ambiguous to parse.
48
+ // `root`/`face-0` are reserved sentinels for the base flat — a flange reusing
49
+ // either would silently overwrite the base frame and fold off wrong geometry.
50
+ if (flange.id === '' || flange.id.includes('::') || flange.id === ROOT_FLAT_ID || flange.id === 'face-0') {
51
+ return err(validationError('INVALID_FLANGE_ID', `flange id must be non-empty, must not contain '::', and must not reuse the reserved ids 'root'/'face-0', got '${flange.id}'`));
52
+ }
53
+ if (seen.has(flange.id)) {
54
+ return err(validationError('DUPLICATE_FLANGE', `duplicate flange id '${flange.id}'`));
55
+ }
56
+ seen.add(flange.id);
57
+ }
58
+ // Two flanges on the same parent edge must not overlap along that edge.
59
+ const overlap = checkEdgeOverlaps(spec.flanges, baseLen, width);
60
+ if (!overlap.ok)
61
+ return overlap;
62
+ const ROOT = ROOT_FLAT_ID;
63
+ const frames = new Map();
64
+ // Frame origin sits on the sheet's −normal (bottom) surface; the flat spans
65
+ // `thickness` along +n. This matches the canonical assembly, whose z=0 plane is
66
+ // the sheet bottom and whose bend cylinder sits at z = T+r (up) / −r (down).
67
+ frames.set(ROOT, {
68
+ id: ROOT,
69
+ origin: [0, 0, 0],
70
+ u: [1, 0, 0],
71
+ v: [0, 1, 0],
72
+ n: [0, 0, 1],
73
+ uLen: baseLen,
74
+ vLen: width,
75
+ });
76
+ let solid = box(baseLen, width, thickness);
77
+ const flanges = [];
78
+ const bends = [];
79
+ // Author in spec order; a chained flange must follow its parent. Resolve the
80
+ // parent frame by flange id (or the base for a root flange).
81
+ for (const flange of spec.flanges) {
82
+ const parentId = flange.parent ?? ROOT;
83
+ const parentFrame = frames.get(parentId);
84
+ if (parentFrame === undefined) {
85
+ return err(validationError('UNKNOWN_PARENT', `flange '${flange.id}' parent '${flange.parent}' not authored yet`));
86
+ }
87
+ const built = buildFlange(solid, parentFrame, thickness, flange);
88
+ if (!built.ok)
89
+ return built;
90
+ solid = built.value.solid;
91
+ flanges.push(built.value.flange);
92
+ bends.push(built.value.bend);
93
+ frames.set(flange.id, built.value.childFrame);
94
+ }
95
+ for (const seam of spec.seams ?? []) {
96
+ const seamParent = seam.parent === 'face-0' ? ROOT : seam.parent;
97
+ const seamChild = seam.child === 'face-0' ? ROOT : seam.child;
98
+ if (!frames.has(seamParent) || !frames.has(seamChild)) {
99
+ return err(validationError('UNKNOWN_SEAM_FLAT', `seam ${seam.parent}↔${seam.child} references an unauthored flat`));
100
+ }
101
+ if (!Number.isFinite(seam.angleDeg) || seam.angleDeg <= 0 || seam.angleDeg > 180) {
102
+ return err(validationError('INVALID_SEAM_ANGLE', `seam ${seam.parent}↔${seam.child} angleDeg must be in (0, 180], got ${seam.angleDeg}`));
103
+ }
104
+ if (!Number.isFinite(seam.rule.innerRadius) || seam.rule.innerRadius < 0) {
105
+ return err(validationError('INVALID_SEAM_RADIUS', `seam ${seam.parent}↔${seam.child} innerRadius must be non-negative`));
106
+ }
107
+ const parentFrame = frames.get(seamParent);
108
+ bends.push({
109
+ id: `seam::${seamParent}::${seamChild}`,
110
+ axisOrigin: [parentFrame.origin[0], parentFrame.origin[1], parentFrame.origin[2]],
111
+ axisDir: [parentFrame.u[0], parentFrame.u[1], parentFrame.u[2]],
112
+ angleDeg: seam.angleDeg,
113
+ direction: 'up',
114
+ rule: seam.rule,
115
+ });
116
+ }
117
+ return ok({
118
+ thickness,
119
+ baseLength: baseLen,
120
+ width,
121
+ ...(spec.material !== undefined ? { material: spec.material } : {}),
122
+ flanges,
123
+ bends,
124
+ solid: normalizeSolid(solid),
125
+ });
126
+ }
127
+ /**
128
+ * World {@link FlatFrame} of every flat region of an authored part, keyed by region
129
+ * id (`'root'` for the base). Reconstructed from the recorded bend/flange feature
130
+ * tree using the exact same edge/transform/child-frame math {@link authorPart} uses
131
+ * to place the geometry, so a cutout authored in a region's local `(x, y)` lands on
132
+ * the matching folded face. Walks the feature tree in BFS order so a chained
133
+ * flange's parent frame is always known first.
134
+ *
135
+ * @param part - the authored sheet-metal part whose flat regions to resolve.
136
+ * @param tree - optional pre-built {@link FeatureTree}; when omitted the tree is
137
+ * reconstructed from `part` via {@link featureTree}.
138
+ * @returns a `Result<Map<string, FlatFrame>>` — `ok` with a map of region id
139
+ * (`'root'` for the base, otherwise the flange id) to its world {@link FlatFrame},
140
+ * or `err` when a flange's parent frame is missing from the tree.
141
+ */
142
+ export function worldFrames(part, tree) {
143
+ const frames = new Map();
144
+ frames.set(ROOT_FLAT_ID, {
145
+ id: ROOT_FLAT_ID,
146
+ origin: [0, 0, 0],
147
+ u: [1, 0, 0],
148
+ v: [0, 1, 0],
149
+ n: [0, 0, 1],
150
+ uLen: part.baseLength,
151
+ vLen: part.width,
152
+ });
153
+ const treeResult = tree !== undefined ? ok(tree) : featureTree(part);
154
+ if (!treeResult.ok)
155
+ return treeResult;
156
+ const flangeById = new Map();
157
+ for (const f of part.flanges)
158
+ flangeById.set(f.id, f);
159
+ for (const tb of treeResult.value.bends) {
160
+ const flange = flangeById.get(tb.child);
161
+ if (flange === undefined)
162
+ continue;
163
+ const parent = frames.get(tb.parent);
164
+ if (parent === undefined) {
165
+ return err(validationError('UNKNOWN_PARENT', `worldFrames: parent '${tb.parent}' missing for '${tb.child}'`));
166
+ }
167
+ const side = flange.baseEdge.side ?? 'xmax';
168
+ const direction = flange.direction ?? 'up';
169
+ const offset = flange.offset ?? flange.baseEdge.offset ?? 0;
170
+ const r = flange.rule.innerRadius;
171
+ const edge = parentEdge(parent, side);
172
+ const sign = direction === 'up' ? 1 : -1;
173
+ const place = frameTransform(edge.dir, edge.out, parent.n, edge.start, offset);
174
+ frames.set(flange.id, computeChildFrame(flange.id, place, flange.span, flange.angleDeg, part.thickness, r, sign, flange.length));
175
+ }
176
+ return ok(frames);
177
+ }
178
+ /**
179
+ * Reject two flanges on the same parent edge whose [offset, offset+width] ranges
180
+ * overlap. The edge length a flange's default span fills must be the *resolved*
181
+ * parent-edge length, not the base's: a chained flange folds off another flange
182
+ * whose edges have that flange's own span/length, so keying the default off the
183
+ * base dimensions would compute a bogus span and spuriously reject valid pairs.
184
+ */
185
+ function checkEdgeOverlaps(specs, baseLen, width) {
186
+ // Resolved child-frame dims per flange: spanLen along the bend axis, flatLen out.
187
+ // Walked in spec order (a chained flange always follows its parent), mirroring
188
+ // the authoring loop so parent frames are known before their children.
189
+ const dims = new Map();
190
+ const edgeLenFor = (parentId, side) => {
191
+ const onWidthEdge = side === 'xmax' || side === 'xmin';
192
+ if (parentId === ROOT_FLAT_ID)
193
+ return onWidthEdge ? width : baseLen;
194
+ const p = dims.get(parentId);
195
+ if (p === undefined)
196
+ return onWidthEdge ? width : baseLen;
197
+ // Parent flange frame: uLen = its span (xmax/xmin edges), vLen = its length.
198
+ return onWidthEdge ? p.flatLen : p.spanLen;
199
+ };
200
+ const byEdge = new Map();
201
+ for (const f of specs) {
202
+ const side = f.side ?? 'xmax';
203
+ const parentId = f.parent ?? ROOT_FLAT_ID;
204
+ const key = `${parentId}::${side}`;
205
+ const edgeLen = edgeLenFor(parentId, side);
206
+ const lo = f.offset ?? 0;
207
+ const span = f.width ?? edgeLen;
208
+ const hi = lo + span;
209
+ const list = byEdge.get(key) ?? [];
210
+ for (const r of list) {
211
+ if (lo < r.hi - 1e-9 && r.lo < hi - 1e-9) {
212
+ return err(validationError('OVERLAPPING_FLANGES', `flange '${f.id}' overlaps '${r.id}' on edge '${key}' ([${lo}, ${hi}] ∩ [${r.lo}, ${r.hi}])`));
213
+ }
214
+ }
215
+ list.push({ id: f.id, lo, hi });
216
+ byEdge.set(key, list);
217
+ dims.set(f.id, { spanLen: span, flatLen: f.length });
218
+ }
219
+ return ok(undefined);
220
+ }
221
+ /**
222
+ * Build one flange off `parent`'s chosen edge and fuse it to `base`. The flange
223
+ * assembly (cylindrical bend patch + flat) is constructed in a canonical frame
224
+ * (bend axis +Y at the origin, outward run +X, top surface +Z) honouring the
225
+ * up/down fold, then rigidly mapped onto the parent edge's world frame.
226
+ */
227
+ function buildFlange(base, parent, thickness, flange) {
228
+ if (!Number.isFinite(flange.length) || flange.length <= 0) {
229
+ return err(validationError('INVALID_FLANGE_LENGTH', `flange '${flange.id}' length must be positive`));
230
+ }
231
+ if (!Number.isFinite(flange.angleDeg) || flange.angleDeg <= 0 || flange.angleDeg > 180) {
232
+ return err(validationError('INVALID_FLANGE_ANGLE', `flange '${flange.id}' angleDeg must be in (0, 180], got ${flange.angleDeg}`));
233
+ }
234
+ const r = flange.rule.innerRadius;
235
+ if (!Number.isFinite(r) || r < 0) {
236
+ return err(validationError('INVALID_RADIUS', `flange '${flange.id}' innerRadius must be non-negative`));
237
+ }
238
+ const side = flange.side ?? 'xmax';
239
+ const direction = flange.direction ?? 'up';
240
+ const edge = parentEdge(parent, side);
241
+ const offset = flange.offset ?? 0;
242
+ const span = flange.width ?? edge.length;
243
+ if (!Number.isFinite(offset) || offset < -1e-9) {
244
+ return err(validationError('INVALID_OFFSET', `flange '${flange.id}' offset must be non-negative`));
245
+ }
246
+ if (!Number.isFinite(span) || span <= 0) {
247
+ return err(validationError('INVALID_FLANGE_WIDTH', `flange '${flange.id}' width must be positive`));
248
+ }
249
+ if (offset + span > edge.length + 1e-6) {
250
+ return err(validationError('FLANGE_OUT_OF_BOUNDS', `flange '${flange.id}' [${offset}, ${offset + span}] exceeds parent edge length ${edge.length}`));
251
+ }
252
+ // Canonical assembly: bend axis +Y, outward run +X, top surface +Z, spanning
253
+ // +Y over [0, span]. Up folds toward +Z, down folds below the base plane.
254
+ const canonAxisZ = direction === 'up' ? thickness + r : -r;
255
+ const canonOrigin = [0, 0, canonAxisZ];
256
+ const patch = buildBendPatch(canonOrigin, flange.angleDeg, r, span, thickness, direction);
257
+ if (!patch.ok)
258
+ return patch;
259
+ const flat = buildFlangeFlat(canonOrigin, flange.angleDeg, span, thickness, flange.length, direction);
260
+ // World frame the canonical assembly maps onto: bend axis = edge.dir, outward
261
+ // run = edge.out, top normal = parent.n. The bend axis sits at offset along the
262
+ // edge; the canonical contact (axis straight above the base seam) maps onto the
263
+ // parent edge line.
264
+ const place = frameTransform(edge.dir, edge.out, parent.n, edge.start, offset);
265
+ const patchPlaced = place.solid(patch.value);
266
+ const flatPlaced = place.solid(flat);
267
+ const fusedPatch = fuse(base, patchPlaced);
268
+ if (!fusedPatch.ok)
269
+ return fusedPatch;
270
+ const fused = fuse(fusedPatch.value, flatPlaced);
271
+ if (!fused.ok)
272
+ return fused;
273
+ const sign = direction === 'up' ? 1 : -1;
274
+ // Recorded bend axis = the cylinder centre line: parent edge + n·(T+r up | r down).
275
+ const axisOrigin = vecAdd(vecAdd(edge.start, vecScale(edge.dir, offset)), vecScale(parent.n, canonAxisZ));
276
+ const bend = {
277
+ id: flange.id,
278
+ axisOrigin: [axisOrigin[0], axisOrigin[1], axisOrigin[2]],
279
+ axisDir: [edge.dir[0], edge.dir[1], edge.dir[2]],
280
+ angleDeg: flange.angleDeg,
281
+ direction,
282
+ rule: flange.rule,
283
+ };
284
+ const childFrame = computeChildFrame(flange.id, place, span, flange.angleDeg, thickness, r, sign, flange.length);
285
+ // True folded AABB of the whole flange (bend patch + flat) in world space.
286
+ // Computed from the real placed geometry so chained flanges — whose bend axis
287
+ // sits off the z=0 base plane — get a correct box, not the z=0-flattened one a
288
+ // purely analytic re-fold would produce. The collision check reuses this.
289
+ const foldedBounds = flangeFoldedBounds(childFrame, edge, offset, span, parent.n, canonAxisZ, thickness);
290
+ const flangeFeature = {
291
+ id: flange.id,
292
+ baseEdge: {
293
+ kind: 'index',
294
+ faceIndex: parent.id === ROOT_FLAT_ID ? 0 : -1,
295
+ edgeIndex: 0,
296
+ parentId: parent.id,
297
+ side,
298
+ offset,
299
+ extent: span,
300
+ },
301
+ length: flange.length,
302
+ span,
303
+ offset,
304
+ angleDeg: flange.angleDeg,
305
+ direction,
306
+ rule: flange.rule,
307
+ foldedBounds,
308
+ ...(flange.miter !== undefined ? { miter: flange.miter } : {}),
309
+ };
310
+ return ok({ solid: fused.value, flange: flangeFeature, bend, childFrame });
311
+ }
312
+ /**
313
+ * Resolve one of the four edges of a flat frame. The bend axis `dir` is chosen so
314
+ * the canonical assembly maps onto it by a proper rotation: `out × dir = n`, i.e.
315
+ * `dir = n × out`. `start` is the edge endpoint from which `dir` runs along the
316
+ * edge, so the flange spans `start + offset·dir` to `start + (offset+span)·dir`.
317
+ */
318
+ function parentEdge(f, side) {
319
+ const o = f.origin;
320
+ const uTop = vecAdd(o, vecScale(f.u, f.uLen));
321
+ const vTop = vecAdd(o, vecScale(f.v, f.vLen));
322
+ const uvTop = vecAdd(uTop, vecScale(f.v, f.vLen));
323
+ let out;
324
+ let length;
325
+ let a;
326
+ let b;
327
+ switch (side) {
328
+ case 'xmax':
329
+ out = f.u;
330
+ length = f.vLen;
331
+ a = uTop;
332
+ b = uvTop;
333
+ break;
334
+ case 'xmin':
335
+ out = vecScale(f.u, -1);
336
+ length = f.vLen;
337
+ a = o;
338
+ b = vTop;
339
+ break;
340
+ case 'ymax':
341
+ out = f.v;
342
+ length = f.uLen;
343
+ a = vTop;
344
+ b = uvTop;
345
+ break;
346
+ case 'ymin':
347
+ out = vecScale(f.v, -1);
348
+ length = f.uLen;
349
+ a = o;
350
+ b = uTop;
351
+ break;
352
+ }
353
+ const dir = vecNormalize(vecCross(f.n, out));
354
+ // Pick the endpoint from which `dir` points toward the other endpoint.
355
+ const toB = vecSub(b, a);
356
+ const start = vecDot(toB, dir) >= 0 ? a : b;
357
+ return { dir, out, start, length };
358
+ }
359
+ /**
360
+ * Frame of the child flange's flat (bottom/−normal surface, near corner), for
361
+ * chaining grandchildren. Derived by folding the canonical flat by ∓θ about the
362
+ * bend axis, then mapping through the same rigid transform that placed the flange.
363
+ */
364
+ function computeChildFrame(id, place, span, angleDeg, thickness, r, sign, length) {
365
+ const theta = (angleDeg * Math.PI) / 180;
366
+ const axisZ = sign > 0 ? thickness + r : -r;
367
+ // Canonical fold = rotate by −sign·θ about +Y at pivot [0,0,axisZ]. Run +X and
368
+ // normal +Z fold accordingly; the flat's near-bottom corner ([0,0,0]) rotates
369
+ // about the pivot. (rotate by a about +Y: [x,z]→[x cos a + z sin a, −x sin a + z cos a].)
370
+ const a = -sign * theta;
371
+ const ca = Math.cos(a);
372
+ const sa = Math.sin(a);
373
+ const runC = [ca, 0, -sa];
374
+ const nC = [sa, 0, ca];
375
+ const dz = -axisZ;
376
+ const cornerC = [dz * sa, 0, axisZ + dz * ca];
377
+ return {
378
+ id,
379
+ origin: place.point(cornerC),
380
+ u: place.vector([0, 1, 0]),
381
+ v: place.vector(runC),
382
+ n: place.vector(nC),
383
+ uLen: span,
384
+ vLen: length,
385
+ };
386
+ }
387
+ /**
388
+ * World-space AABB of a folded flange: the union of the flat box (from its child
389
+ * frame) and the bend region between the parent edge and the bend-axis cylinder.
390
+ * Built from the real placed corners, so a chained flange folded off a vertical
391
+ * wall is bounded correctly rather than collapsed into the z=0 base plane.
392
+ */
393
+ function flangeFoldedBounds(child, edge, offset, span, parentN, canonAxisZ, thickness) {
394
+ const corners = [];
395
+ // Flat box: child.origin + [0,uLen]·u + [0,vLen]·v + [0,thickness]·n.
396
+ for (const s of [0, child.uLen]) {
397
+ for (const l of [0, child.vLen]) {
398
+ for (const t of [0, thickness]) {
399
+ corners.push(vecAdd(vecAdd(vecAdd(child.origin, vecScale(child.u, s)), vecScale(child.v, l)), vecScale(child.n, t)));
400
+ }
401
+ }
402
+ }
403
+ // Bend region: from the parent edge contact (sheet at the seam) out to the bend
404
+ // axis line, over the flange span. Captures the cylindrical patch's footprint.
405
+ const edgeBase = vecAdd(edge.start, vecScale(edge.dir, offset));
406
+ const axisLine = vecAdd(edgeBase, vecScale(parentN, canonAxisZ));
407
+ for (const s of [0, span]) {
408
+ const along = vecScale(edge.dir, s);
409
+ corners.push(vecAdd(edgeBase, along));
410
+ corners.push(vecAdd(vecAdd(edgeBase, along), vecScale(parentN, thickness)));
411
+ corners.push(vecAdd(axisLine, along));
412
+ }
413
+ return aabbOf(corners);
414
+ }
415
+ function aabbOf(corners) {
416
+ let xMin = Infinity;
417
+ let xMax = -Infinity;
418
+ let yMin = Infinity;
419
+ let yMax = -Infinity;
420
+ let zMin = Infinity;
421
+ let zMax = -Infinity;
422
+ for (const c of corners) {
423
+ if (c[0] < xMin)
424
+ xMin = c[0];
425
+ if (c[0] > xMax)
426
+ xMax = c[0];
427
+ if (c[1] < yMin)
428
+ yMin = c[1];
429
+ if (c[1] > yMax)
430
+ yMax = c[1];
431
+ if (c[2] < zMin)
432
+ zMin = c[2];
433
+ if (c[2] > zMax)
434
+ zMax = c[2];
435
+ }
436
+ return { xMin, xMax, yMin, yMax, zMin, zMax };
437
+ }
438
+ /**
439
+ * Rigid transform mapping the canonical flange assembly (bend axis +Y, run +X,
440
+ * normal +Z, contact at the origin) onto a world edge frame: align +X→`runT`,
441
+ * +Y→`axisT`, +Z→`nT`, then slide the axis to `edgeStart + offset·axisT`.
442
+ */
443
+ function frameTransform(axisT, runT, nT, edgeStart, offset) {
444
+ // Rotation matrix R with columns [runT | axisT | nT] sends canonical X,Y,Z to
445
+ // the target basis. Convert to axis-angle and rotate about the origin.
446
+ const m = [
447
+ runT[0], axisT[0], nT[0],
448
+ runT[1], axisT[1], nT[1],
449
+ runT[2], axisT[2], nT[2],
450
+ ];
451
+ const aa = matrixToAxisAngle(m);
452
+ const target = vecAdd(edgeStart, vecScale(axisT, offset));
453
+ const applyR = (v) => vecAdd(vecAdd(vecScale(runT, v[0]), vecScale(axisT, v[1])), vecScale(nT, v[2]));
454
+ return {
455
+ solid: (s) => {
456
+ const rotated = aa.angleDeg === 0 ? s : rotate(s, aa.angleDeg, { at: [0, 0, 0], axis: aa.axis });
457
+ return translate(rotated, target);
458
+ },
459
+ point: (p) => vecAdd(applyR(p), target),
460
+ vector: applyR,
461
+ };
462
+ }
463
+ /** Axis-angle of a 3×3 rotation matrix (row-major). */
464
+ function matrixToAxisAngle(m) {
465
+ const m00 = m[0] ?? 0;
466
+ const m01 = m[1] ?? 0;
467
+ const m02 = m[2] ?? 0;
468
+ const m10 = m[3] ?? 0;
469
+ const m11 = m[4] ?? 0;
470
+ const m12 = m[5] ?? 0;
471
+ const m20 = m[6] ?? 0;
472
+ const m21 = m[7] ?? 0;
473
+ const m22 = m[8] ?? 0;
474
+ const trace = m00 + m11 + m22;
475
+ const cos = Math.max(-1, Math.min(1, (trace - 1) / 2));
476
+ const angle = Math.acos(cos);
477
+ const angleDeg = (angle * 180) / Math.PI;
478
+ if (angle < 1e-9)
479
+ return { axis: [0, 0, 1], angleDeg: 0 };
480
+ if (Math.PI - angle < 1e-6) {
481
+ // 180°: axis is the column of (R + I) with the largest diagonal.
482
+ const xx = (m00 + 1) / 2;
483
+ const yy = (m11 + 1) / 2;
484
+ const zz = (m22 + 1) / 2;
485
+ let axis;
486
+ if (xx >= yy && xx >= zz) {
487
+ const x = Math.sqrt(Math.max(xx, 0));
488
+ axis = [x, (m01 + m10) / (4 * x), (m02 + m20) / (4 * x)];
489
+ }
490
+ else if (yy >= zz) {
491
+ const y = Math.sqrt(Math.max(yy, 0));
492
+ axis = [(m01 + m10) / (4 * y), y, (m12 + m21) / (4 * y)];
493
+ }
494
+ else {
495
+ const z = Math.sqrt(Math.max(zz, 0));
496
+ axis = [(m02 + m20) / (4 * z), (m12 + m21) / (4 * z), z];
497
+ }
498
+ return { axis: vecNormalize(axis), angleDeg: 180 };
499
+ }
500
+ const denom = 2 * Math.sin(angle);
501
+ const axis = [(m21 - m12) / denom, (m02 - m20) / denom, (m10 - m01) / denom];
502
+ return { axis: vecNormalize(axis), angleDeg };
503
+ }
504
+ /**
505
+ * Cylindrical bend patch swept by the fold. Built as a hollow tube (outer R+T,
506
+ * inner R) along +Y, intersected with the angular wedge for the fold. For an up
507
+ * bend the axis sits above the base (sweep from −Z toward the flange); for a down
508
+ * bend it sits below (sweep from +Z toward the flange).
509
+ */
510
+ function buildBendPatch(axisOrigin, thetaDeg, r, width, thickness, direction) {
511
+ const outerR = r + thickness;
512
+ const outer = cylinder(outerR, width, { at: axisOrigin, axis: [0, 1, 0] });
513
+ let tube = outer;
514
+ if (r > 1e-9) {
515
+ const inner = cylinder(r, width, { at: axisOrigin, axis: [0, 1, 0] });
516
+ const carved = cut(outer, inner);
517
+ if (!carved.ok)
518
+ return carved;
519
+ tube = carved.value;
520
+ }
521
+ const wedge = buildWedge(axisOrigin, thetaDeg, outerR, width, direction);
522
+ if (!wedge.ok)
523
+ return wedge;
524
+ return intersect(tube, wedge.value);
525
+ }
526
+ /**
527
+ * Convex angular wedge (≤180°) isolating the fold sweep. The inner arc starts at
528
+ * the base contact (straight down −Z for an up bend, straight up +Z for a down
529
+ * bend) and sweeps to the flange. Built as the intersection of the +X half-space
530
+ * through the bend axis with that half-space rotated by the fold; the tube
531
+ * intersection trims it to the true annular sector.
532
+ */
533
+ function buildWedge(axisOrigin, thetaDeg, outerR, width, direction) {
534
+ const span = outerR * 2 + 2;
535
+ const margin = 1;
536
+ const blockA = box(span, width + 2 * margin, 2 * span);
537
+ const halfA = translate(blockA, [
538
+ axisOrigin[0],
539
+ axisOrigin[1] - margin,
540
+ axisOrigin[2] - span,
541
+ ]);
542
+ // Up bends sweep from −Z (rotate +X start plane by +(180−θ)); down bends mirror
543
+ // about the axis, sweeping from +Z (rotate by −(180−θ)).
544
+ const sign = direction === 'up' ? 1 : -1;
545
+ const halfB = rotate(halfA, sign * (180 - thetaDeg), { at: axisOrigin, axis: [0, 1, 0] });
546
+ return intersect(halfA, halfB);
547
+ }
548
+ /** The flange flat, built lying along +X past the bend, then folded to angle θ. */
549
+ function buildFlangeFlat(axisOrigin, thetaDeg, width, thickness, length, direction) {
550
+ // Unfolded the flat is coplanar with the base sheet; for an up bend it lies in
551
+ // Z∈[0,T] folding up (rotate −θ about +Y sends +X toward +Z); for a down bend it
552
+ // lies in Z∈[−T,0] folding down (rotate +θ sends +X toward −Z).
553
+ if (direction === 'up') {
554
+ const flat = box(length, width, thickness);
555
+ const positioned = translate(flat, [axisOrigin[0], axisOrigin[1], 0]);
556
+ return rotate(positioned, -thetaDeg, { at: axisOrigin, axis: [0, 1, 0] });
557
+ }
558
+ const flat = box(length, width, thickness);
559
+ const positioned = translate(flat, [axisOrigin[0], axisOrigin[1], -thickness]);
560
+ return rotate(positioned, thetaDeg, { at: axisOrigin, axis: [0, 1, 0] });
561
+ }
562
+ //# sourceMappingURL=authorFns.js.map