@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/tabFns.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { ok, err, validationError, line, wireLoop, face, extrude, fuse, getSolids, isValid, isPlanarWire, vecAdd, vecScale, } from '@faicad/faijs/brepjs-compat';
|
|
2
|
+
import { normalizeSolid } from './internal.js';
|
|
3
|
+
import { regionFrames, regionDevExtent, mapToFrame2, addSlot } from './cutoutFns.js';
|
|
4
|
+
const EPS = 1e-6;
|
|
5
|
+
/**
|
|
6
|
+
* Fuse a rectangular tab onto a named flat region's edge — additive material (the
|
|
7
|
+
* counterpart of a cutout). The tab is built as a region-local rectangle just past
|
|
8
|
+
* the chosen `side`, placed onto the correct folded face via the region's world
|
|
9
|
+
* {@link FlatFrame}, extruded through the sheet thickness and fused to the solid. The
|
|
10
|
+
* same local rectangle, mapped through the region's developed {@link Frame2}, is
|
|
11
|
+
* recorded as a {@link TabFeature} so {@link unfold} extends the OUTER outline by the
|
|
12
|
+
* protrusion (tabs add material, so the developed area grows). Guards a valid,
|
|
13
|
+
* single-bodied solid.
|
|
14
|
+
*
|
|
15
|
+
* @param part - the sheet metal part to attach a tab to.
|
|
16
|
+
* @param spec - the tab specification.
|
|
17
|
+
* @returns the updated part with the tab feature recorded, or an error.
|
|
18
|
+
*/
|
|
19
|
+
export function addTab(part, spec) {
|
|
20
|
+
if (part.solid === undefined) {
|
|
21
|
+
return err(validationError('NO_SOLID', 'addTab: part has no folded solid to fuse onto'));
|
|
22
|
+
}
|
|
23
|
+
if (!Number.isFinite(spec.width) || spec.width <= 0) {
|
|
24
|
+
return err(validationError('INVALID_TAB', `tab width must be positive, got ${spec.width}`));
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isFinite(spec.length) || spec.length <= 0) {
|
|
27
|
+
return err(validationError('INVALID_TAB', `tab length must be positive, got ${spec.length}`));
|
|
28
|
+
}
|
|
29
|
+
if (!Number.isFinite(spec.offset) || spec.offset < -EPS) {
|
|
30
|
+
return err(validationError('INVALID_TAB', `tab offset must be non-negative, got ${spec.offset}`));
|
|
31
|
+
}
|
|
32
|
+
const framesResult = regionFrames(part, spec.region);
|
|
33
|
+
if (!framesResult.ok)
|
|
34
|
+
return framesResult;
|
|
35
|
+
const { regionId, world, dev } = framesResult.value;
|
|
36
|
+
const ext = regionDevExtent(part, regionId, world);
|
|
37
|
+
// The tab attaches to a region edge and runs `width` ALONG it; that span must lie
|
|
38
|
+
// within the edge's length, or the protrusion overhangs the region.
|
|
39
|
+
const edgeLen = spec.side === 'xmax' || spec.side === 'xmin' ? ext.vMax : ext.uMax;
|
|
40
|
+
if (spec.offset + spec.width > edgeLen + EPS) {
|
|
41
|
+
return err(validationError('TAB_OUT_OF_BOUNDS', `tab [${spec.offset}, ${spec.offset + spec.width}] on side '${spec.side}' exceeds edge length ${edgeLen}`));
|
|
42
|
+
}
|
|
43
|
+
const local = tabLocalRect(spec, ext);
|
|
44
|
+
const tool = buildTabSolid(local, world, part.thickness);
|
|
45
|
+
if (!tool.ok)
|
|
46
|
+
return tool;
|
|
47
|
+
const fused = fuse(part.solid, tool.value);
|
|
48
|
+
if (!fused.ok)
|
|
49
|
+
return fused;
|
|
50
|
+
const solid = normalizeSolid(fused.value);
|
|
51
|
+
if (!isValid(solid) || getSolids(solid).length > 1) {
|
|
52
|
+
return err(validationError('TAB_INVALID_SOLID', `addTab: tab on region '${spec.region}' did not fuse into a single valid body`));
|
|
53
|
+
}
|
|
54
|
+
const rect = devRect(local, dev);
|
|
55
|
+
const feature = {
|
|
56
|
+
spec,
|
|
57
|
+
region: regionId,
|
|
58
|
+
rect,
|
|
59
|
+
area: spec.width * spec.length,
|
|
60
|
+
};
|
|
61
|
+
return ok({ ...part, solid, tabs: [...(part.tabs ?? []), feature] });
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Self-fixturing tab-and-slot joint: fuse a tab on one region and punch a matching
|
|
65
|
+
* SLOT CUTOUT on the mating region, sized so the tab's cross-section
|
|
66
|
+
* (`width × thickness`) inserts into the slot. The slot is `tab.width + clearance`
|
|
67
|
+
* long by `thickness + clearance` wide, centred at the mating region's local
|
|
68
|
+
* `(x, y)`. The slot is always strictly larger than the tab cross-section, so the
|
|
69
|
+
* joint mates (verified numerically by callers). Clearance defaults to `0.1` mm.
|
|
70
|
+
*
|
|
71
|
+
* @param part - the sheet metal part to process.
|
|
72
|
+
* @param tab - the tab specification.
|
|
73
|
+
* @param slot - the slot placement.
|
|
74
|
+
* @returns the updated part with the tab-and-slot feature recorded, or an error.
|
|
75
|
+
*/
|
|
76
|
+
export function tabAndSlot(part, tab, slot) {
|
|
77
|
+
const clearance = slot.clearance ?? 0.1;
|
|
78
|
+
if (!Number.isFinite(clearance) || clearance < 0) {
|
|
79
|
+
return err(validationError('INVALID_CLEARANCE', `clearance must be non-negative, got ${clearance}`));
|
|
80
|
+
}
|
|
81
|
+
const withTab = addTab(part, tab);
|
|
82
|
+
if (!withTab.ok)
|
|
83
|
+
return withTab;
|
|
84
|
+
// The tab's inserted cross-section is `width` (along the edge) by `thickness`
|
|
85
|
+
// (through the sheet); the slot must clear both, so it's sized + clearance.
|
|
86
|
+
const slotLength = tab.width + clearance;
|
|
87
|
+
const slotWidth = part.thickness + clearance;
|
|
88
|
+
return addSlot(withTab.value, slot.region, {
|
|
89
|
+
x: slot.x,
|
|
90
|
+
y: slot.y,
|
|
91
|
+
length: slotLength,
|
|
92
|
+
width: slotWidth,
|
|
93
|
+
...(slot.angleDeg !== undefined ? { angleDeg: slot.angleDeg } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Region-local rectangle of the tab as four corners (CCW), extending OUTWARD past
|
|
98
|
+
* the chosen edge by `length`, spanning `[offset, offset+width]` along the edge.
|
|
99
|
+
*/
|
|
100
|
+
function tabLocalRect(spec, ext) {
|
|
101
|
+
const o = spec.offset;
|
|
102
|
+
const w = o + spec.width;
|
|
103
|
+
const l = spec.length;
|
|
104
|
+
switch (spec.side) {
|
|
105
|
+
case 'xmax':
|
|
106
|
+
return [
|
|
107
|
+
[ext.uMax, o],
|
|
108
|
+
[ext.uMax + l, o],
|
|
109
|
+
[ext.uMax + l, w],
|
|
110
|
+
[ext.uMax, w],
|
|
111
|
+
];
|
|
112
|
+
case 'xmin':
|
|
113
|
+
return [
|
|
114
|
+
[-l, o],
|
|
115
|
+
[0, o],
|
|
116
|
+
[0, w],
|
|
117
|
+
[-l, w],
|
|
118
|
+
];
|
|
119
|
+
case 'ymax':
|
|
120
|
+
return [
|
|
121
|
+
[o, ext.vMax],
|
|
122
|
+
[w, ext.vMax],
|
|
123
|
+
[w, ext.vMax + l],
|
|
124
|
+
[o, ext.vMax + l],
|
|
125
|
+
];
|
|
126
|
+
case 'ymin':
|
|
127
|
+
return [
|
|
128
|
+
[o, -l],
|
|
129
|
+
[w, -l],
|
|
130
|
+
[w, 0],
|
|
131
|
+
[o, 0],
|
|
132
|
+
];
|
|
133
|
+
default:
|
|
134
|
+
return spec.side;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Extrude the region-local tab rectangle through the sheet via the region world frame. */
|
|
138
|
+
function buildTabSolid(local, f, thickness) {
|
|
139
|
+
const worldPts = local.map(([x, y]) => vecAdd(vecAdd(f.origin, vecScale(f.u, x)), vecScale(f.v, y)));
|
|
140
|
+
const edges = [];
|
|
141
|
+
for (let i = 0; i < worldPts.length; i += 1) {
|
|
142
|
+
const a = worldPts[i];
|
|
143
|
+
const b = worldPts[(i + 1) % worldPts.length];
|
|
144
|
+
if (a === undefined || b === undefined) {
|
|
145
|
+
return err(validationError('TAB_TOOL_FAILED', 'failed to index tab rectangle points'));
|
|
146
|
+
}
|
|
147
|
+
edges.push(line(a, b));
|
|
148
|
+
}
|
|
149
|
+
const wire = wireLoop(edges);
|
|
150
|
+
if (!wire.ok)
|
|
151
|
+
return wire;
|
|
152
|
+
if (!isPlanarWire(wire.value)) {
|
|
153
|
+
return err(validationError('TAB_TOOL_FAILED', 'tab profile wire is not planar'));
|
|
154
|
+
}
|
|
155
|
+
const profile = face(wire.value);
|
|
156
|
+
if (!profile.ok)
|
|
157
|
+
return profile;
|
|
158
|
+
return extrude(profile.value, [f.n[0] * thickness, f.n[1] * thickness, f.n[2] * thickness]);
|
|
159
|
+
}
|
|
160
|
+
/** Developed-plane axis-aligned rectangle `[x0,y0,x1,y1]` of the local tab rect. */
|
|
161
|
+
function devRect(local, dev) {
|
|
162
|
+
let x0 = Infinity;
|
|
163
|
+
let y0 = Infinity;
|
|
164
|
+
let x1 = -Infinity;
|
|
165
|
+
let y1 = -Infinity;
|
|
166
|
+
for (const p of local) {
|
|
167
|
+
const [x, y] = mapToFrame2(p, dev);
|
|
168
|
+
if (x < x0)
|
|
169
|
+
x0 = x;
|
|
170
|
+
if (x > x1)
|
|
171
|
+
x1 = x;
|
|
172
|
+
if (y < y0)
|
|
173
|
+
y0 = y;
|
|
174
|
+
if (y > y1)
|
|
175
|
+
y1 = y;
|
|
176
|
+
}
|
|
177
|
+
return [x0, y0, x1, y1];
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=tabFns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabFns.js","sourceRoot":"","sources":["../src/tabFns.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type Result,\n type Vec3,\n type Solid,\n ok,\n err,\n validationError,\n line,\n wireLoop,\n face,\n extrude,\n fuse,\n getSolids,\n isValid,\n isPlanarWire,\n vecAdd,\n vecScale,\n} from '@faicad/faijs/brepjs-compat';\nimport type { TabSpec, TabFeature, SheetMetalPart } from './types.js';\nimport { normalizeSolid } from './internal.js';\nimport type { FlatFrame } from './authorFns.js';\nimport type { Frame2 } from './unfoldFns.js';\nimport { regionFrames, regionDevExtent, mapToFrame2, addSlot } from './cutoutFns.js';\n\nconst EPS = 1e-6;\n\ntype Pt2 = [number, number];\n\n/**\n * Fuse a rectangular tab onto a named flat region's edge — additive material (the\n * counterpart of a cutout). The tab is built as a region-local rectangle just past\n * the chosen `side`, placed onto the correct folded face via the region's world\n * {@link FlatFrame}, extruded through the sheet thickness and fused to the solid. The\n * same local rectangle, mapped through the region's developed {@link Frame2}, is\n * recorded as a {@link TabFeature} so {@link unfold} extends the OUTER outline by the\n * protrusion (tabs add material, so the developed area grows). Guards a valid,\n * single-bodied solid.\n *\n * @param part - the sheet metal part to attach a tab to.\n * @param spec - the tab specification.\n * @returns the updated part with the tab feature recorded, or an error.\n */\nexport function addTab(part: SheetMetalPart, spec: TabSpec): Result<SheetMetalPart> {\n if (part.solid === undefined) {\n return err(validationError('NO_SOLID', 'addTab: part has no folded solid to fuse onto'));\n }\n if (!Number.isFinite(spec.width) || spec.width <= 0) {\n return err(validationError('INVALID_TAB', `tab width must be positive, got ${spec.width}`));\n }\n if (!Number.isFinite(spec.length) || spec.length <= 0) {\n return err(validationError('INVALID_TAB', `tab length must be positive, got ${spec.length}`));\n }\n if (!Number.isFinite(spec.offset) || spec.offset < -EPS) {\n return err(validationError('INVALID_TAB', `tab offset must be non-negative, got ${spec.offset}`));\n }\n\n const framesResult = regionFrames(part, spec.region);\n if (!framesResult.ok) return framesResult;\n const { regionId, world, dev } = framesResult.value;\n\n const ext = regionDevExtent(part, regionId, world);\n // The tab attaches to a region edge and runs `width` ALONG it; that span must lie\n // within the edge's length, or the protrusion overhangs the region.\n const edgeLen = spec.side === 'xmax' || spec.side === 'xmin' ? ext.vMax : ext.uMax;\n if (spec.offset + spec.width > edgeLen + EPS) {\n return err(\n validationError(\n 'TAB_OUT_OF_BOUNDS',\n `tab [${spec.offset}, ${spec.offset + spec.width}] on side '${spec.side}' exceeds edge length ${edgeLen}`\n )\n );\n }\n\n const local = tabLocalRect(spec, ext);\n\n const tool = buildTabSolid(local, world, part.thickness);\n if (!tool.ok) return tool;\n\n const fused = fuse(part.solid, tool.value);\n if (!fused.ok) return fused;\n const solid = normalizeSolid(fused.value);\n if (!isValid(solid) || getSolids(solid).length > 1) {\n return err(\n validationError(\n 'TAB_INVALID_SOLID',\n `addTab: tab on region '${spec.region}' did not fuse into a single valid body`\n )\n );\n }\n\n const rect = devRect(local, dev);\n const feature: TabFeature = {\n spec,\n region: regionId,\n rect,\n area: spec.width * spec.length,\n };\n\n return ok({ ...part, solid, tabs: [...(part.tabs ?? []), feature] });\n}\n\n/** The mating-slot placement for a {@link tabAndSlot} joint. */\nexport interface SlotPlacement {\n region: string;\n x: number;\n y: number;\n clearance?: number | undefined;\n /** In-plane rotation of the slot (deg, CCW) in the mating region's local frame.\n * Default 0 = slot length along the region's +x (bend-axis) direction; set this\n * when the tab meets the slot region at a non-default orientation. */\n angleDeg?: number | undefined;\n}\n\n/**\n * Self-fixturing tab-and-slot joint: fuse a tab on one region and punch a matching\n * SLOT CUTOUT on the mating region, sized so the tab's cross-section\n * (`width × thickness`) inserts into the slot. The slot is `tab.width + clearance`\n * long by `thickness + clearance` wide, centred at the mating region's local\n * `(x, y)`. The slot is always strictly larger than the tab cross-section, so the\n * joint mates (verified numerically by callers). Clearance defaults to `0.1` mm.\n *\n * @param part - the sheet metal part to process.\n * @param tab - the tab specification.\n * @param slot - the slot placement.\n * @returns the updated part with the tab-and-slot feature recorded, or an error.\n */\nexport function tabAndSlot(\n part: SheetMetalPart,\n tab: TabSpec,\n slot: SlotPlacement\n): Result<SheetMetalPart> {\n const clearance = slot.clearance ?? 0.1;\n if (!Number.isFinite(clearance) || clearance < 0) {\n return err(validationError('INVALID_CLEARANCE', `clearance must be non-negative, got ${clearance}`));\n }\n\n const withTab = addTab(part, tab);\n if (!withTab.ok) return withTab;\n\n // The tab's inserted cross-section is `width` (along the edge) by `thickness`\n // (through the sheet); the slot must clear both, so it's sized + clearance.\n const slotLength = tab.width + clearance;\n const slotWidth = part.thickness + clearance;\n\n return addSlot(withTab.value, slot.region, {\n x: slot.x,\n y: slot.y,\n length: slotLength,\n width: slotWidth,\n ...(slot.angleDeg !== undefined ? { angleDeg: slot.angleDeg } : {}),\n });\n}\n\n/**\n * Region-local rectangle of the tab as four corners (CCW), extending OUTWARD past\n * the chosen edge by `length`, spanning `[offset, offset+width]` along the edge.\n */\nfunction tabLocalRect(spec: TabSpec, ext: { uMax: number; vMax: number }): Pt2[] {\n const o = spec.offset;\n const w = o + spec.width;\n const l = spec.length;\n switch (spec.side) {\n case 'xmax':\n return [\n [ext.uMax, o],\n [ext.uMax + l, o],\n [ext.uMax + l, w],\n [ext.uMax, w],\n ];\n case 'xmin':\n return [\n [-l, o],\n [0, o],\n [0, w],\n [-l, w],\n ];\n case 'ymax':\n return [\n [o, ext.vMax],\n [w, ext.vMax],\n [w, ext.vMax + l],\n [o, ext.vMax + l],\n ];\n case 'ymin':\n return [\n [o, -l],\n [w, -l],\n [w, 0],\n [o, 0],\n ];\n default:\n return spec.side satisfies never;\n }\n}\n\n/** Extrude the region-local tab rectangle through the sheet via the region world frame. */\nfunction buildTabSolid(local: Pt2[], f: FlatFrame, thickness: number): Result<Solid> {\n const worldPts: Vec3[] = local.map(([x, y]) =>\n vecAdd(vecAdd(f.origin, vecScale(f.u, x)), vecScale(f.v, y))\n );\n const edges = [];\n for (let i = 0; i < worldPts.length; i += 1) {\n const a = worldPts[i];\n const b = worldPts[(i + 1) % worldPts.length];\n if (a === undefined || b === undefined) {\n return err(validationError('TAB_TOOL_FAILED', 'failed to index tab rectangle points'));\n }\n edges.push(line(a, b));\n }\n const wire = wireLoop(edges);\n if (!wire.ok) return wire;\n if (!isPlanarWire(wire.value)) {\n return err(validationError('TAB_TOOL_FAILED', 'tab profile wire is not planar'));\n }\n const profile = face(wire.value);\n if (!profile.ok) return profile;\n return extrude(profile.value, [f.n[0] * thickness, f.n[1] * thickness, f.n[2] * thickness]);\n}\n\n/** Developed-plane axis-aligned rectangle `[x0,y0,x1,y1]` of the local tab rect. */\nfunction devRect(local: Pt2[], dev: Frame2): [number, number, number, number] {\n let x0 = Infinity;\n let y0 = Infinity;\n let x1 = -Infinity;\n let y1 = -Infinity;\n for (const p of local) {\n const [x, y] = mapToFrame2(p, dev);\n if (x < x0) x0 = x;\n if (x > x1) x1 = x;\n if (y < y0) y0 = y;\n if (y > y1) y1 = y;\n }\n return [x0, y0, x1, y1];\n}\n"]}
|