brepjs-sheetmetal 0.4.3 → 0.5.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/README.md +6 -0
- package/dist/api.d.ts +2 -0
- package/dist/brepjs-sheetmetal.cjs +213 -99
- package/dist/brepjs-sheetmetal.js +214 -101
- package/dist/cutoutFns.d.ts +10 -0
- package/dist/facade.d.ts +3 -0
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,6 +94,12 @@ feature in that region's **local** frame: `(0, 0)` is the region's near corner,
|
|
|
94
94
|
profile is built in region-local coords, then placed via the region's world frame (origin/u/v/n), so a
|
|
95
95
|
hole authored on a folded flange lands on the flange face, not the base plane. Rejects a feature outside
|
|
96
96
|
the region (`CUTOUT_OUT_OF_BOUNDS`) and guards a valid, single-bodied result (`CUTOUT_SEVERED_SOLID`).
|
|
97
|
+
- `addCutouts(part, specs)` punches a list of cutouts in **one** boolean cut. Each `addCutout` call is a cut
|
|
98
|
+
on the solid that the call before it made larger, so a chain of calls slows down as it goes. Use
|
|
99
|
+
`addCutouts` for a vent pattern or a row of holes: the result is the same part, with the features recorded
|
|
100
|
+
in `specs` order. A spec that fails validation fails the whole call.
|
|
101
|
+
- A round hole is a true cylinder in the solid, and an obround slot has true arcs. Their developed loops are
|
|
102
|
+
polylines of 48 segments for a full circle.
|
|
97
103
|
- `unfold` emits the matching developed loop at the cutout's flat-pattern location (mapped through the same
|
|
98
104
|
region's developed frame) and drops `developedArea` by the cutout areas.
|
|
99
105
|
|
package/dist/api.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export declare function autoReliefs(part: SheetMetalPart, spec?: ReliefSpec): Re
|
|
|
33
33
|
export declare function relieveCorner(part: SheetMetalPart, flangeIdA: string, flangeIdB: string, spec?: ReliefSpec): Result<SheetMetalPart>;
|
|
34
34
|
/** Punch a cutout (hole / slot / polygon) through a named flat region's thickness. */
|
|
35
35
|
export declare function addCutout(part: SheetMetalPart, spec: CutoutSpec): Result<SheetMetalPart>;
|
|
36
|
+
/** Punch many cutouts in one boolean cut. Use this for a pattern of cutouts. */
|
|
37
|
+
export declare function addCutouts(part: SheetMetalPart, specs: readonly CutoutSpec[]): Result<SheetMetalPart>;
|
|
36
38
|
/** Punch a circular hole of `diameter` centred at region-local `(x, y)`. */
|
|
37
39
|
export declare function addHole(part: SheetMetalPart, region: string, x: number, y: number, diameter: number): Result<SheetMetalPart>;
|
|
38
40
|
/** Punch a slot (rectangular or obround) centred at region-local `(x, y)`. */
|
|
@@ -1686,14 +1686,14 @@ function describePlanar(face) {
|
|
|
1686
1686
|
/** True if `point` (already on the face's surface) lies within the face's trimmed boundary. */
|
|
1687
1687
|
function liesOnFace(face, point) {
|
|
1688
1688
|
try {
|
|
1689
|
-
var _usingCtx$
|
|
1690
|
-
const probe = _usingCtx$
|
|
1689
|
+
var _usingCtx$2 = _usingCtx();
|
|
1690
|
+
const probe = _usingCtx$2.u((0, brepjs.vertex)(point));
|
|
1691
1691
|
const d = (0, brepjs.measureDistance)(probe, face);
|
|
1692
1692
|
return d.ok && d.value < GEOM_TOL;
|
|
1693
1693
|
} catch (_) {
|
|
1694
|
-
_usingCtx$
|
|
1694
|
+
_usingCtx$2.e = _;
|
|
1695
1695
|
} finally {
|
|
1696
|
-
_usingCtx$
|
|
1696
|
+
_usingCtx$2.d();
|
|
1697
1697
|
}
|
|
1698
1698
|
}
|
|
1699
1699
|
/** Normal-direction gap between two planar faces, or `null` unless they are antiparallel. */
|
|
@@ -1935,6 +1935,9 @@ function buildBendGraph(flats, bends, warnings) {
|
|
|
1935
1935
|
if (bendTouchesFlat(bend, flat)) touching.push(fi);
|
|
1936
1936
|
}
|
|
1937
1937
|
if (touching.length < 2) {
|
|
1938
|
+
const [only] = touching;
|
|
1939
|
+
const flat = only !== void 0 ? flats[only] : void 0;
|
|
1940
|
+
if (flat !== void 0 && goesThroughFlat(bend, flat)) continue;
|
|
1938
1941
|
warnings.push({
|
|
1939
1942
|
code: "DETECTION_INCOMPLETE",
|
|
1940
1943
|
message: `bend '${bend.id}' connects ${touching.length} flats (expected 2); ignored`,
|
|
@@ -1954,11 +1957,28 @@ function buildBendGraph(flats, bends, warnings) {
|
|
|
1954
1957
|
}
|
|
1955
1958
|
return { edges };
|
|
1956
1959
|
}
|
|
1960
|
+
/**
|
|
1961
|
+
* True for a cylinder that goes through the sheet: a round hole, the end of an
|
|
1962
|
+
* obround slot or a rounded corner of the outline. A bend axis lies in the plane
|
|
1963
|
+
* of its flats; this axis is along the flat normal, and the cylinder meets both
|
|
1964
|
+
* faces of the panel. A boss or a blind recess meets one face only, so it keeps
|
|
1965
|
+
* its warning.
|
|
1966
|
+
*/
|
|
1967
|
+
function goesThroughFlat(bend, flat) {
|
|
1968
|
+
if (Math.abs((0, brepjs.vecDot)(bend.fit.axisDir, flat.normal)) <= .99) return false;
|
|
1969
|
+
return flat.faces.every((ff) => bend.faces.some((bf) => facesShareEdge(bf, ff)));
|
|
1970
|
+
}
|
|
1957
1971
|
/** True if any of the bend's faces shares an edge with either face of the flat. */
|
|
1958
1972
|
function bendTouchesFlat(bend, flat) {
|
|
1959
|
-
for (const bf of bend.faces) for (const ff of flat.faces) if ((
|
|
1973
|
+
for (const bf of bend.faces) for (const ff of flat.faces) if (facesShareEdge(bf, ff)) return true;
|
|
1960
1974
|
return false;
|
|
1961
1975
|
}
|
|
1976
|
+
/** True if two faces share an edge. `sharedEdges` returns handles that the caller owns. */
|
|
1977
|
+
function facesShareEdge(a, b) {
|
|
1978
|
+
const edges = (0, brepjs.sharedEdges)(a, b);
|
|
1979
|
+
for (const edge of edges) edge.delete();
|
|
1980
|
+
return edges.length > 0;
|
|
1981
|
+
}
|
|
1962
1982
|
/**
|
|
1963
1983
|
* Lay out detected flats into the developed plane. A spanning tree over the bend
|
|
1964
1984
|
* graph (BFS from flat 0) places each flat as a rectangle and each tree bend as a
|
|
@@ -2944,29 +2964,13 @@ function buildWedge$1(axisOrigin, thetaDeg, outerR, width, direction) {
|
|
|
2944
2964
|
}
|
|
2945
2965
|
/** The flange flat, built lying along +X past the bend, then folded to angle θ. */
|
|
2946
2966
|
function buildFlangeFlat(axisOrigin, thetaDeg, width, thickness, length, direction) {
|
|
2947
|
-
if (direction === "up") {
|
|
2948
|
-
const flat = (0, brepjs.box)(length, width, thickness);
|
|
2949
|
-
const positioned = (0, brepjs.translate)(flat, [
|
|
2950
|
-
axisOrigin[0],
|
|
2951
|
-
axisOrigin[1],
|
|
2952
|
-
0
|
|
2953
|
-
]);
|
|
2954
|
-
return (0, brepjs.rotate)(positioned, -thetaDeg, {
|
|
2955
|
-
at: axisOrigin,
|
|
2956
|
-
axis: [
|
|
2957
|
-
0,
|
|
2958
|
-
1,
|
|
2959
|
-
0
|
|
2960
|
-
]
|
|
2961
|
-
});
|
|
2962
|
-
}
|
|
2963
2967
|
const flat = (0, brepjs.box)(length, width, thickness);
|
|
2964
2968
|
const positioned = (0, brepjs.translate)(flat, [
|
|
2965
2969
|
axisOrigin[0],
|
|
2966
2970
|
axisOrigin[1],
|
|
2967
|
-
|
|
2971
|
+
0
|
|
2968
2972
|
]);
|
|
2969
|
-
return (0, brepjs.rotate)(positioned, thetaDeg, {
|
|
2973
|
+
return (0, brepjs.rotate)(positioned, (direction === "up" ? -1 : 1) * thetaDeg, {
|
|
2970
2974
|
at: axisOrigin,
|
|
2971
2975
|
axis: [
|
|
2972
2976
|
0,
|
|
@@ -3343,7 +3347,10 @@ function dist2(a, b) {
|
|
|
3343
3347
|
var EPS$5 = 1e-6;
|
|
3344
3348
|
/** A hair of over-cut so the tool pokes through both faces cleanly. */
|
|
3345
3349
|
var HAIR$1 = .05;
|
|
3346
|
-
/**
|
|
3350
|
+
/**
|
|
3351
|
+
* Polyline segments approximating a circular boundary (hole / obround end cap) in
|
|
3352
|
+
* the developed loop. The 3D cut tool keeps the true circle and arcs.
|
|
3353
|
+
*/
|
|
3347
3354
|
var CIRCLE_SEGMENTS$1 = 48;
|
|
3348
3355
|
/**
|
|
3349
3356
|
* Punch a cutout (hole / slot / polygon) through a named flat region's thickness.
|
|
@@ -3354,42 +3361,78 @@ var CIRCLE_SEGMENTS$1 = 48;
|
|
|
3354
3361
|
* mapped through the region's developed {@link Frame2} and recorded as a
|
|
3355
3362
|
* {@link CutoutFeature}, so {@link unfold} emits the matching loop in the flat
|
|
3356
3363
|
* pattern and {@link fold} can replay it. Guards a valid, single-bodied solid.
|
|
3364
|
+
*
|
|
3365
|
+
* Each call is one boolean cut on a solid that the cut before it made larger. For
|
|
3366
|
+
* a pattern of cutouts, {@link addCutouts} makes one cut for all of them.
|
|
3357
3367
|
*/
|
|
3358
3368
|
function addCutout(part, spec) {
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3369
|
+
return punch(part, [spec], "addCutout");
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Punch many cutouts in one boolean cut. The result is the same part that a chain
|
|
3373
|
+
* of {@link addCutout} calls gives, with the features recorded in `specs` order,
|
|
3374
|
+
* but the cost does not grow with the square of the count. A spec that fails
|
|
3375
|
+
* validation fails the whole call, and no cutout is made.
|
|
3376
|
+
*/
|
|
3377
|
+
function addCutouts(part, specs) {
|
|
3378
|
+
return punch(part, specs, "addCutouts");
|
|
3379
|
+
}
|
|
3380
|
+
function punch(part, specs, label) {
|
|
3381
|
+
try {
|
|
3382
|
+
var _usingCtx$1 = _usingCtx();
|
|
3383
|
+
if (part.solid === void 0) return (0, brepjs.err)((0, brepjs.validationError)("NO_SOLID", `${label}: part has no folded solid to cut`));
|
|
3384
|
+
if (specs.length === 0) return (0, brepjs.ok)(part);
|
|
3385
|
+
const treeResult = featureTree(part);
|
|
3386
|
+
if (!treeResult.ok) return treeResult;
|
|
3387
|
+
const tree = treeResult.value;
|
|
3388
|
+
const framesResult = worldFrames(part, tree);
|
|
3389
|
+
if (!framesResult.ok) return framesResult;
|
|
3390
|
+
const layout = layoutTree(part, tree, part.baseLength, part.width);
|
|
3391
|
+
if (!layout.ok) return layout;
|
|
3392
|
+
const scope = _usingCtx$1.u(new brepjs.DisposalScope());
|
|
3393
|
+
const tools = [];
|
|
3394
|
+
const features = [];
|
|
3395
|
+
for (const spec of specs) {
|
|
3396
|
+
const regionId = resolveRegionId(spec.region);
|
|
3397
|
+
const worldFrame = framesResult.value.get(regionId);
|
|
3398
|
+
if (worldFrame === void 0) return (0, brepjs.err)((0, brepjs.validationError)("UNKNOWN_REGION", `${label}: region '${spec.region}' not found`));
|
|
3399
|
+
const localResult = localLoop(spec);
|
|
3400
|
+
if (!localResult.ok) return localResult;
|
|
3401
|
+
const local = localResult.value;
|
|
3402
|
+
const oob = checkInBounds(outlineBounds(spec, local), regionExtent(part, regionId, worldFrame));
|
|
3403
|
+
if (oob !== void 0) return (0, brepjs.err)((0, brepjs.validationError)("CUTOUT_OUT_OF_BOUNDS", oob));
|
|
3404
|
+
const dev = developedFrame(layout.value, regionId);
|
|
3405
|
+
if (!dev.ok) return dev;
|
|
3406
|
+
const toolResult = buildTool(spec, local, worldFrame, part.thickness, scope);
|
|
3407
|
+
if (!toolResult.ok) return toolResult;
|
|
3408
|
+
tools.push(scope.register(toolResult.value));
|
|
3409
|
+
const loop = local.map((p) => mapToFrame2(p, dev.value));
|
|
3410
|
+
features.push({
|
|
3411
|
+
spec,
|
|
3412
|
+
region: regionId,
|
|
3413
|
+
loop,
|
|
3414
|
+
area: polygonArea(loop)
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
const [only] = tools;
|
|
3418
|
+
const cutResult = tools.length === 1 && only !== void 0 ? (0, brepjs.cut)(part.solid, only) : (0, brepjs.cutAll)(part.solid, tools);
|
|
3419
|
+
if (!cutResult.ok) return cutResult;
|
|
3420
|
+
const solid = normalizeSolid(cutResult.value);
|
|
3421
|
+
if (!(0, brepjs.isValid)(solid) || (0, brepjs.getSolids)(solid).length > 1) {
|
|
3422
|
+
const [first] = specs;
|
|
3423
|
+
const what = specs.length === 1 && first !== void 0 ? `${first.kind} on region '${first.region}' severs` : `the ${specs.length} cutouts sever`;
|
|
3424
|
+
return (0, brepjs.err)((0, brepjs.validationError)("CUTOUT_SEVERED_SOLID", `${label}: ${what} the part into multiple bodies`));
|
|
3425
|
+
}
|
|
3426
|
+
return (0, brepjs.ok)({
|
|
3427
|
+
...part,
|
|
3428
|
+
solid,
|
|
3429
|
+
cutouts: [...part.cutouts ?? [], ...features]
|
|
3430
|
+
});
|
|
3431
|
+
} catch (_) {
|
|
3432
|
+
_usingCtx$1.e = _;
|
|
3433
|
+
} finally {
|
|
3434
|
+
_usingCtx$1.d();
|
|
3435
|
+
}
|
|
3393
3436
|
}
|
|
3394
3437
|
/** Punch a circular hole of `diameter` centred at region-local `(x, y)`. */
|
|
3395
3438
|
function addHole(part, region, x, y, diameter) {
|
|
@@ -3436,7 +3479,9 @@ function regionFrames(part, region) {
|
|
|
3436
3479
|
if (!framesResult.ok) return framesResult;
|
|
3437
3480
|
const world = framesResult.value.get(regionId);
|
|
3438
3481
|
if (world === void 0) return (0, brepjs.err)((0, brepjs.validationError)("UNKNOWN_REGION", `region '${region}' not found`));
|
|
3439
|
-
const
|
|
3482
|
+
const layout = layoutTree(part, tree, part.baseLength, part.width);
|
|
3483
|
+
if (!layout.ok) return layout;
|
|
3484
|
+
const dev = developedFrame(layout.value, regionId);
|
|
3440
3485
|
if (!dev.ok) return dev;
|
|
3441
3486
|
return (0, brepjs.ok)({
|
|
3442
3487
|
regionId,
|
|
@@ -3510,45 +3555,107 @@ function regionExtent(part, regionId, worldFrame) {
|
|
|
3510
3555
|
vMax: worldFrame.vLen
|
|
3511
3556
|
};
|
|
3512
3557
|
}
|
|
3513
|
-
/**
|
|
3514
|
-
|
|
3515
|
-
|
|
3558
|
+
/**
|
|
3559
|
+
* Local-frame bounding box of the outline that the 3D tool cuts. A hole and the
|
|
3560
|
+
* ends of an obround slot are true circles, which reach past the vertices of
|
|
3561
|
+
* their developed polyline, so their boxes come from the circle centres.
|
|
3562
|
+
*/
|
|
3563
|
+
function outlineBounds(spec, local) {
|
|
3564
|
+
if (spec.kind === "hole") {
|
|
3565
|
+
const r = spec.diameter / 2;
|
|
3566
|
+
return {
|
|
3567
|
+
xMin: spec.x - r,
|
|
3568
|
+
xMax: spec.x + r,
|
|
3569
|
+
yMin: spec.y - r,
|
|
3570
|
+
yMax: spec.y + r
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
if (spec.kind === "slot" && spec.round === true) {
|
|
3574
|
+
const hw = spec.width / 2;
|
|
3575
|
+
const straight = Math.max(spec.length / 2 - hw, 0);
|
|
3576
|
+
const rad = (spec.angleDeg ?? 0) * Math.PI / 180;
|
|
3577
|
+
const dx = Math.abs(straight * Math.cos(rad)) + hw;
|
|
3578
|
+
const dy = Math.abs(straight * Math.sin(rad)) + hw;
|
|
3579
|
+
return {
|
|
3580
|
+
xMin: spec.x - dx,
|
|
3581
|
+
xMax: spec.x + dx,
|
|
3582
|
+
yMin: spec.y - dy,
|
|
3583
|
+
yMax: spec.y + dy
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
const xs = local.map((p) => p[0]);
|
|
3587
|
+
const ys = local.map((p) => p[1]);
|
|
3588
|
+
return {
|
|
3589
|
+
xMin: Math.min(...xs),
|
|
3590
|
+
xMax: Math.max(...xs),
|
|
3591
|
+
yMin: Math.min(...ys),
|
|
3592
|
+
yMax: Math.max(...ys)
|
|
3593
|
+
};
|
|
3594
|
+
}
|
|
3595
|
+
/** Out-of-bounds message, or undefined if the whole outline lies inside the region. */
|
|
3596
|
+
function checkInBounds(b, ext) {
|
|
3597
|
+
if (b.xMin < -1e-6 || b.xMax > ext.uMax + EPS$5 || b.yMin < -1e-6 || b.yMax > ext.vMax + EPS$5) return `cutout extent [${b.xMin.toFixed(3)}, ${b.xMax.toFixed(3)}]×[${b.yMin.toFixed(3)}, ${b.yMax.toFixed(3)}] lies outside region extent [0,${ext.uMax}]×[0,${ext.vMax}]`;
|
|
3516
3598
|
}
|
|
3517
3599
|
/**
|
|
3518
|
-
* Build the 3D cut tool
|
|
3519
|
-
* (`origin + x·u + y·v − HAIR·n`),
|
|
3520
|
-
*
|
|
3521
|
-
*
|
|
3600
|
+
* Build the 3D cut tool on the region's bottom surface, a hair below it
|
|
3601
|
+
* (`origin + x·u + y·v − HAIR·n`), through the sheet by `thickness + 2·HAIR` along
|
|
3602
|
+
* +n. Using the world frame's `u`/`v`/`n` is what lands the cut on the correct
|
|
3603
|
+
* folded face. A hole is a true cylinder and an obround slot has true arcs, so a
|
|
3604
|
+
* round cutout adds one or two curved faces to the solid, not one planar face for
|
|
3605
|
+
* each segment of its developed loop. Intermediate shapes go to `scope`.
|
|
3522
3606
|
*/
|
|
3523
|
-
function buildTool(local, f, thickness) {
|
|
3607
|
+
function buildTool(spec, local, f, thickness, scope) {
|
|
3524
3608
|
const base = (0, brepjs.vecAdd)(f.origin, (0, brepjs.vecScale)(f.n, -.05));
|
|
3525
|
-
const
|
|
3526
|
-
const
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3609
|
+
const toWorld = ([x, y]) => (0, brepjs.vecAdd)((0, brepjs.vecAdd)(base, (0, brepjs.vecScale)(f.u, x)), (0, brepjs.vecScale)(f.v, y));
|
|
3610
|
+
const depth = thickness + 2 * HAIR$1;
|
|
3611
|
+
if (spec.kind === "hole") return (0, brepjs.ok)((0, brepjs.cylinder)(spec.diameter / 2, depth, {
|
|
3612
|
+
at: toWorld([spec.x, spec.y]),
|
|
3613
|
+
axis: f.n
|
|
3614
|
+
}));
|
|
3615
|
+
const edges = spec.kind === "slot" && spec.round === true ? obroundEdges(spec, toWorld) : polylineEdges(local, toWorld);
|
|
3616
|
+
for (const e of edges) scope.register(e);
|
|
3533
3617
|
const wire = (0, brepjs.wireLoop)(edges);
|
|
3534
3618
|
if (!wire.ok) return wire;
|
|
3619
|
+
scope.register(wire.value);
|
|
3535
3620
|
if (!(0, brepjs.isPlanarWire)(wire.value)) return (0, brepjs.err)((0, brepjs.validationError)("CUTOUT_TOOL_FAILED", "cutout profile wire is not planar"));
|
|
3536
3621
|
const profile = (0, brepjs.face)(wire.value);
|
|
3537
3622
|
if (!profile.ok) return profile;
|
|
3538
|
-
|
|
3623
|
+
scope.register(profile.value);
|
|
3539
3624
|
return (0, brepjs.extrude)(profile.value, [
|
|
3540
3625
|
f.n[0] * depth,
|
|
3541
3626
|
f.n[1] * depth,
|
|
3542
3627
|
f.n[2] * depth
|
|
3543
3628
|
]);
|
|
3544
3629
|
}
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3630
|
+
function polylineEdges(local, toWorld) {
|
|
3631
|
+
const pts = local.map(toWorld);
|
|
3632
|
+
return pts.map((a, i) => (0, brepjs.line)(a, pts[(i + 1) % pts.length] ?? a));
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* Edges of an obround slot: two straight sides joined by two semicircles of radius
|
|
3636
|
+
* `width/2`. A slot as wide as it is long has no straight side, so it is two
|
|
3637
|
+
* semicircles (a circle).
|
|
3638
|
+
*/
|
|
3639
|
+
function obroundEdges(spec, toWorld) {
|
|
3640
|
+
const hw = spec.width / 2;
|
|
3641
|
+
const straight = Math.max(spec.length / 2 - hw, 0);
|
|
3642
|
+
const rad = (spec.angleDeg ?? 0) * Math.PI / 180;
|
|
3643
|
+
const c = Math.cos(rad);
|
|
3644
|
+
const s = Math.sin(rad);
|
|
3645
|
+
const at = (x, y) => toWorld([spec.x + x * c - y * s, spec.y + x * s + y * c]);
|
|
3646
|
+
const rightBottom = at(straight, -hw);
|
|
3647
|
+
const rightTop = at(straight, hw);
|
|
3648
|
+
const leftTop = at(-straight, hw);
|
|
3649
|
+
const leftBottom = at(-straight, -hw);
|
|
3650
|
+
const edges = [(0, brepjs.threePointArc)(rightBottom, at(straight + hw, 0), rightTop)];
|
|
3651
|
+
if (straight > EPS$5) edges.push((0, brepjs.line)(rightTop, leftTop));
|
|
3652
|
+
edges.push((0, brepjs.threePointArc)(leftTop, at(-straight - hw, 0), leftBottom));
|
|
3653
|
+
if (straight > EPS$5) edges.push((0, brepjs.line)(leftBottom, rightBottom));
|
|
3654
|
+
return edges;
|
|
3655
|
+
}
|
|
3656
|
+
/** Developed-plane {@link Frame2} of a region (base or flange) in the unfold layout. */
|
|
3657
|
+
function developedFrame(layout, regionId) {
|
|
3658
|
+
const placed = layout.flats.find((p) => p.id === regionId);
|
|
3552
3659
|
if (placed === void 0) return (0, brepjs.err)((0, brepjs.validationError)("UNKNOWN_REGION", `developedFrame: region '${regionId}' missing from layout`));
|
|
3553
3660
|
return (0, brepjs.ok)(placed.frame);
|
|
3554
3661
|
}
|
|
@@ -4228,22 +4335,17 @@ function foldWithWarnings(input) {
|
|
|
4228
4335
|
if (!relieved.ok) return relieved;
|
|
4229
4336
|
part = relieved.value;
|
|
4230
4337
|
}
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
}
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
region: region.id
|
|
4243
|
-
});
|
|
4244
|
-
if (!cutResult.ok) return cutResult;
|
|
4245
|
-
part = cutResult.value;
|
|
4246
|
-
}
|
|
4338
|
+
const cutoutSpecs = (input.baseCutouts ?? []).map((spec) => ({
|
|
4339
|
+
...spec,
|
|
4340
|
+
region: ROOT_FLAT_ID
|
|
4341
|
+
}));
|
|
4342
|
+
for (const region of input.regions) for (const spec of region.cutouts ?? []) cutoutSpecs.push({
|
|
4343
|
+
...spec,
|
|
4344
|
+
region: region.id
|
|
4345
|
+
});
|
|
4346
|
+
const cutResult = addCutouts(part, cutoutSpecs);
|
|
4347
|
+
if (!cutResult.ok) return cutResult;
|
|
4348
|
+
part = cutResult.value;
|
|
4247
4349
|
for (const spec of input.baseTabs ?? []) {
|
|
4248
4350
|
const tabbed = addTab(part, {
|
|
4249
4351
|
...spec,
|
|
@@ -7120,6 +7222,10 @@ function relieveCorner(part, flangeIdA, flangeIdB, spec) {
|
|
|
7120
7222
|
function addCutout$1(part, spec) {
|
|
7121
7223
|
return addCutout(part, spec);
|
|
7122
7224
|
}
|
|
7225
|
+
/** Punch many cutouts in one boolean cut. Use this for a pattern of cutouts. */
|
|
7226
|
+
function addCutouts$1(part, specs) {
|
|
7227
|
+
return addCutouts(part, specs);
|
|
7228
|
+
}
|
|
7123
7229
|
/** Punch a circular hole of `diameter` centred at region-local `(x, y)`. */
|
|
7124
7230
|
function addHole$1(part, region, x, y, diameter) {
|
|
7125
7231
|
return addHole(part, region, x, y, diameter);
|
|
@@ -7282,6 +7388,10 @@ var SheetMetalPartHandle = class SheetMetalPartHandle {
|
|
|
7282
7388
|
cutout(spec) {
|
|
7283
7389
|
return new SheetMetalPartHandle(unwrapOrThrow(addCutout$1(this.part, spec)));
|
|
7284
7390
|
}
|
|
7391
|
+
/** Punch many cutouts in one boolean cut. Use this for a pattern of cutouts. */
|
|
7392
|
+
cutouts(specs) {
|
|
7393
|
+
return new SheetMetalPartHandle(unwrapOrThrow(addCutouts$1(this.part, specs)));
|
|
7394
|
+
}
|
|
7285
7395
|
/** Punch a circular hole of `diameter` centred at region-local `(x, y)`. */
|
|
7286
7396
|
hole(region, x, y, diameter) {
|
|
7287
7397
|
return new SheetMetalPartHandle(unwrapOrThrow(addHole$1(this.part, region, x, y, diameter)));
|
|
@@ -7437,6 +7547,9 @@ var SheetMetalBuilder = class SheetMetalBuilder {
|
|
|
7437
7547
|
cutout(spec) {
|
|
7438
7548
|
return this.build().cutout(spec);
|
|
7439
7549
|
}
|
|
7550
|
+
cutouts(specs) {
|
|
7551
|
+
return this.build().cutouts(specs);
|
|
7552
|
+
}
|
|
7440
7553
|
hole(region, x, y, diameter) {
|
|
7441
7554
|
return this.build().hole(region, x, y, diameter);
|
|
7442
7555
|
}
|
|
@@ -7518,6 +7631,7 @@ exports.SheetMetalPartHandle = SheetMetalPartHandle;
|
|
|
7518
7631
|
exports.addBendRelief = addBendRelief;
|
|
7519
7632
|
exports.addBendTable = addBendTable;
|
|
7520
7633
|
exports.addCutout = addCutout;
|
|
7634
|
+
exports.addCutouts = addCutouts;
|
|
7521
7635
|
exports.addForm = addForm;
|
|
7522
7636
|
exports.addHole = addHole;
|
|
7523
7637
|
exports.addPolygonCutout = addPolygonCutout;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { box, curveEndPoint, curveStartPoint, cut, cylinder, err, extrude, face, faceCenter, fuse, getBounds, getEdges, getFaces, getSolids, getSurfaceType, intersect, isErr, isOk, isPlanarWire, isSolid, isValid, line, measureArea, measureDistance, measureLength, measureVolume, normalAt, ok, pointOnSurface, rotate, sharedEdges, translate, validationError, vecAdd, vecCross, vecDot, vecLength, vecNormalize, vecScale, vecSub, vertex, wire, wireLoop } from "brepjs";
|
|
1
|
+
import { DisposalScope, box, curveEndPoint, curveStartPoint, cut, cutAll, cylinder, err, extrude, face, faceCenter, fuse, getBounds, getEdges, getFaces, getSolids, getSurfaceType, intersect, isErr, isOk, isPlanarWire, isSolid, isValid, line, measureArea, measureDistance, measureLength, measureVolume, normalAt, ok, pointOnSurface, rotate, sharedEdges, threePointArc, translate, validationError, vecAdd, vecCross, vecDot, vecLength, vecNormalize, vecScale, vecSub, vertex, wire, wireLoop } from "brepjs";
|
|
2
2
|
// src/materials.ts
|
|
3
3
|
var DEFAULT_K_FACTOR = .44;
|
|
4
4
|
function gauge(name, thickness, kFactor = DEFAULT_K_FACTOR) {
|
|
@@ -1685,14 +1685,14 @@ function describePlanar(face) {
|
|
|
1685
1685
|
/** True if `point` (already on the face's surface) lies within the face's trimmed boundary. */
|
|
1686
1686
|
function liesOnFace(face, point) {
|
|
1687
1687
|
try {
|
|
1688
|
-
var _usingCtx$
|
|
1689
|
-
const probe = _usingCtx$
|
|
1688
|
+
var _usingCtx$2 = _usingCtx();
|
|
1689
|
+
const probe = _usingCtx$2.u(vertex(point));
|
|
1690
1690
|
const d = measureDistance(probe, face);
|
|
1691
1691
|
return d.ok && d.value < GEOM_TOL;
|
|
1692
1692
|
} catch (_) {
|
|
1693
|
-
_usingCtx$
|
|
1693
|
+
_usingCtx$2.e = _;
|
|
1694
1694
|
} finally {
|
|
1695
|
-
_usingCtx$
|
|
1695
|
+
_usingCtx$2.d();
|
|
1696
1696
|
}
|
|
1697
1697
|
}
|
|
1698
1698
|
/** Normal-direction gap between two planar faces, or `null` unless they are antiparallel. */
|
|
@@ -1934,6 +1934,9 @@ function buildBendGraph(flats, bends, warnings) {
|
|
|
1934
1934
|
if (bendTouchesFlat(bend, flat)) touching.push(fi);
|
|
1935
1935
|
}
|
|
1936
1936
|
if (touching.length < 2) {
|
|
1937
|
+
const [only] = touching;
|
|
1938
|
+
const flat = only !== void 0 ? flats[only] : void 0;
|
|
1939
|
+
if (flat !== void 0 && goesThroughFlat(bend, flat)) continue;
|
|
1937
1940
|
warnings.push({
|
|
1938
1941
|
code: "DETECTION_INCOMPLETE",
|
|
1939
1942
|
message: `bend '${bend.id}' connects ${touching.length} flats (expected 2); ignored`,
|
|
@@ -1953,11 +1956,28 @@ function buildBendGraph(flats, bends, warnings) {
|
|
|
1953
1956
|
}
|
|
1954
1957
|
return { edges };
|
|
1955
1958
|
}
|
|
1959
|
+
/**
|
|
1960
|
+
* True for a cylinder that goes through the sheet: a round hole, the end of an
|
|
1961
|
+
* obround slot or a rounded corner of the outline. A bend axis lies in the plane
|
|
1962
|
+
* of its flats; this axis is along the flat normal, and the cylinder meets both
|
|
1963
|
+
* faces of the panel. A boss or a blind recess meets one face only, so it keeps
|
|
1964
|
+
* its warning.
|
|
1965
|
+
*/
|
|
1966
|
+
function goesThroughFlat(bend, flat) {
|
|
1967
|
+
if (Math.abs(vecDot(bend.fit.axisDir, flat.normal)) <= .99) return false;
|
|
1968
|
+
return flat.faces.every((ff) => bend.faces.some((bf) => facesShareEdge(bf, ff)));
|
|
1969
|
+
}
|
|
1956
1970
|
/** True if any of the bend's faces shares an edge with either face of the flat. */
|
|
1957
1971
|
function bendTouchesFlat(bend, flat) {
|
|
1958
|
-
for (const bf of bend.faces) for (const ff of flat.faces) if (
|
|
1972
|
+
for (const bf of bend.faces) for (const ff of flat.faces) if (facesShareEdge(bf, ff)) return true;
|
|
1959
1973
|
return false;
|
|
1960
1974
|
}
|
|
1975
|
+
/** True if two faces share an edge. `sharedEdges` returns handles that the caller owns. */
|
|
1976
|
+
function facesShareEdge(a, b) {
|
|
1977
|
+
const edges = sharedEdges(a, b);
|
|
1978
|
+
for (const edge of edges) edge.delete();
|
|
1979
|
+
return edges.length > 0;
|
|
1980
|
+
}
|
|
1961
1981
|
/**
|
|
1962
1982
|
* Lay out detected flats into the developed plane. A spanning tree over the bend
|
|
1963
1983
|
* graph (BFS from flat 0) places each flat as a rectangle and each tree bend as a
|
|
@@ -2943,29 +2963,13 @@ function buildWedge$1(axisOrigin, thetaDeg, outerR, width, direction) {
|
|
|
2943
2963
|
}
|
|
2944
2964
|
/** The flange flat, built lying along +X past the bend, then folded to angle θ. */
|
|
2945
2965
|
function buildFlangeFlat(axisOrigin, thetaDeg, width, thickness, length, direction) {
|
|
2946
|
-
if (direction === "up") {
|
|
2947
|
-
const flat = box(length, width, thickness);
|
|
2948
|
-
const positioned = translate(flat, [
|
|
2949
|
-
axisOrigin[0],
|
|
2950
|
-
axisOrigin[1],
|
|
2951
|
-
0
|
|
2952
|
-
]);
|
|
2953
|
-
return rotate(positioned, -thetaDeg, {
|
|
2954
|
-
at: axisOrigin,
|
|
2955
|
-
axis: [
|
|
2956
|
-
0,
|
|
2957
|
-
1,
|
|
2958
|
-
0
|
|
2959
|
-
]
|
|
2960
|
-
});
|
|
2961
|
-
}
|
|
2962
2966
|
const flat = box(length, width, thickness);
|
|
2963
2967
|
const positioned = translate(flat, [
|
|
2964
2968
|
axisOrigin[0],
|
|
2965
2969
|
axisOrigin[1],
|
|
2966
|
-
|
|
2970
|
+
0
|
|
2967
2971
|
]);
|
|
2968
|
-
return rotate(positioned, thetaDeg, {
|
|
2972
|
+
return rotate(positioned, (direction === "up" ? -1 : 1) * thetaDeg, {
|
|
2969
2973
|
at: axisOrigin,
|
|
2970
2974
|
axis: [
|
|
2971
2975
|
0,
|
|
@@ -3342,7 +3346,10 @@ function dist2(a, b) {
|
|
|
3342
3346
|
var EPS$5 = 1e-6;
|
|
3343
3347
|
/** A hair of over-cut so the tool pokes through both faces cleanly. */
|
|
3344
3348
|
var HAIR$1 = .05;
|
|
3345
|
-
/**
|
|
3349
|
+
/**
|
|
3350
|
+
* Polyline segments approximating a circular boundary (hole / obround end cap) in
|
|
3351
|
+
* the developed loop. The 3D cut tool keeps the true circle and arcs.
|
|
3352
|
+
*/
|
|
3346
3353
|
var CIRCLE_SEGMENTS$1 = 48;
|
|
3347
3354
|
/**
|
|
3348
3355
|
* Punch a cutout (hole / slot / polygon) through a named flat region's thickness.
|
|
@@ -3353,42 +3360,78 @@ var CIRCLE_SEGMENTS$1 = 48;
|
|
|
3353
3360
|
* mapped through the region's developed {@link Frame2} and recorded as a
|
|
3354
3361
|
* {@link CutoutFeature}, so {@link unfold} emits the matching loop in the flat
|
|
3355
3362
|
* pattern and {@link fold} can replay it. Guards a valid, single-bodied solid.
|
|
3363
|
+
*
|
|
3364
|
+
* Each call is one boolean cut on a solid that the cut before it made larger. For
|
|
3365
|
+
* a pattern of cutouts, {@link addCutouts} makes one cut for all of them.
|
|
3356
3366
|
*/
|
|
3357
3367
|
function addCutout(part, spec) {
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3368
|
+
return punch(part, [spec], "addCutout");
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* Punch many cutouts in one boolean cut. The result is the same part that a chain
|
|
3372
|
+
* of {@link addCutout} calls gives, with the features recorded in `specs` order,
|
|
3373
|
+
* but the cost does not grow with the square of the count. A spec that fails
|
|
3374
|
+
* validation fails the whole call, and no cutout is made.
|
|
3375
|
+
*/
|
|
3376
|
+
function addCutouts(part, specs) {
|
|
3377
|
+
return punch(part, specs, "addCutouts");
|
|
3378
|
+
}
|
|
3379
|
+
function punch(part, specs, label) {
|
|
3380
|
+
try {
|
|
3381
|
+
var _usingCtx$1 = _usingCtx();
|
|
3382
|
+
if (part.solid === void 0) return err(validationError("NO_SOLID", `${label}: part has no folded solid to cut`));
|
|
3383
|
+
if (specs.length === 0) return ok(part);
|
|
3384
|
+
const treeResult = featureTree(part);
|
|
3385
|
+
if (!treeResult.ok) return treeResult;
|
|
3386
|
+
const tree = treeResult.value;
|
|
3387
|
+
const framesResult = worldFrames(part, tree);
|
|
3388
|
+
if (!framesResult.ok) return framesResult;
|
|
3389
|
+
const layout = layoutTree(part, tree, part.baseLength, part.width);
|
|
3390
|
+
if (!layout.ok) return layout;
|
|
3391
|
+
const scope = _usingCtx$1.u(new DisposalScope());
|
|
3392
|
+
const tools = [];
|
|
3393
|
+
const features = [];
|
|
3394
|
+
for (const spec of specs) {
|
|
3395
|
+
const regionId = resolveRegionId(spec.region);
|
|
3396
|
+
const worldFrame = framesResult.value.get(regionId);
|
|
3397
|
+
if (worldFrame === void 0) return err(validationError("UNKNOWN_REGION", `${label}: region '${spec.region}' not found`));
|
|
3398
|
+
const localResult = localLoop(spec);
|
|
3399
|
+
if (!localResult.ok) return localResult;
|
|
3400
|
+
const local = localResult.value;
|
|
3401
|
+
const oob = checkInBounds(outlineBounds(spec, local), regionExtent(part, regionId, worldFrame));
|
|
3402
|
+
if (oob !== void 0) return err(validationError("CUTOUT_OUT_OF_BOUNDS", oob));
|
|
3403
|
+
const dev = developedFrame(layout.value, regionId);
|
|
3404
|
+
if (!dev.ok) return dev;
|
|
3405
|
+
const toolResult = buildTool(spec, local, worldFrame, part.thickness, scope);
|
|
3406
|
+
if (!toolResult.ok) return toolResult;
|
|
3407
|
+
tools.push(scope.register(toolResult.value));
|
|
3408
|
+
const loop = local.map((p) => mapToFrame2(p, dev.value));
|
|
3409
|
+
features.push({
|
|
3410
|
+
spec,
|
|
3411
|
+
region: regionId,
|
|
3412
|
+
loop,
|
|
3413
|
+
area: polygonArea(loop)
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
const [only] = tools;
|
|
3417
|
+
const cutResult = tools.length === 1 && only !== void 0 ? cut(part.solid, only) : cutAll(part.solid, tools);
|
|
3418
|
+
if (!cutResult.ok) return cutResult;
|
|
3419
|
+
const solid = normalizeSolid(cutResult.value);
|
|
3420
|
+
if (!isValid(solid) || getSolids(solid).length > 1) {
|
|
3421
|
+
const [first] = specs;
|
|
3422
|
+
const what = specs.length === 1 && first !== void 0 ? `${first.kind} on region '${first.region}' severs` : `the ${specs.length} cutouts sever`;
|
|
3423
|
+
return err(validationError("CUTOUT_SEVERED_SOLID", `${label}: ${what} the part into multiple bodies`));
|
|
3424
|
+
}
|
|
3425
|
+
return ok({
|
|
3426
|
+
...part,
|
|
3427
|
+
solid,
|
|
3428
|
+
cutouts: [...part.cutouts ?? [], ...features]
|
|
3429
|
+
});
|
|
3430
|
+
} catch (_) {
|
|
3431
|
+
_usingCtx$1.e = _;
|
|
3432
|
+
} finally {
|
|
3433
|
+
_usingCtx$1.d();
|
|
3434
|
+
}
|
|
3392
3435
|
}
|
|
3393
3436
|
/** Punch a circular hole of `diameter` centred at region-local `(x, y)`. */
|
|
3394
3437
|
function addHole(part, region, x, y, diameter) {
|
|
@@ -3435,7 +3478,9 @@ function regionFrames(part, region) {
|
|
|
3435
3478
|
if (!framesResult.ok) return framesResult;
|
|
3436
3479
|
const world = framesResult.value.get(regionId);
|
|
3437
3480
|
if (world === void 0) return err(validationError("UNKNOWN_REGION", `region '${region}' not found`));
|
|
3438
|
-
const
|
|
3481
|
+
const layout = layoutTree(part, tree, part.baseLength, part.width);
|
|
3482
|
+
if (!layout.ok) return layout;
|
|
3483
|
+
const dev = developedFrame(layout.value, regionId);
|
|
3439
3484
|
if (!dev.ok) return dev;
|
|
3440
3485
|
return ok({
|
|
3441
3486
|
regionId,
|
|
@@ -3509,45 +3554,107 @@ function regionExtent(part, regionId, worldFrame) {
|
|
|
3509
3554
|
vMax: worldFrame.vLen
|
|
3510
3555
|
};
|
|
3511
3556
|
}
|
|
3512
|
-
/**
|
|
3513
|
-
|
|
3514
|
-
|
|
3557
|
+
/**
|
|
3558
|
+
* Local-frame bounding box of the outline that the 3D tool cuts. A hole and the
|
|
3559
|
+
* ends of an obround slot are true circles, which reach past the vertices of
|
|
3560
|
+
* their developed polyline, so their boxes come from the circle centres.
|
|
3561
|
+
*/
|
|
3562
|
+
function outlineBounds(spec, local) {
|
|
3563
|
+
if (spec.kind === "hole") {
|
|
3564
|
+
const r = spec.diameter / 2;
|
|
3565
|
+
return {
|
|
3566
|
+
xMin: spec.x - r,
|
|
3567
|
+
xMax: spec.x + r,
|
|
3568
|
+
yMin: spec.y - r,
|
|
3569
|
+
yMax: spec.y + r
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
if (spec.kind === "slot" && spec.round === true) {
|
|
3573
|
+
const hw = spec.width / 2;
|
|
3574
|
+
const straight = Math.max(spec.length / 2 - hw, 0);
|
|
3575
|
+
const rad = (spec.angleDeg ?? 0) * Math.PI / 180;
|
|
3576
|
+
const dx = Math.abs(straight * Math.cos(rad)) + hw;
|
|
3577
|
+
const dy = Math.abs(straight * Math.sin(rad)) + hw;
|
|
3578
|
+
return {
|
|
3579
|
+
xMin: spec.x - dx,
|
|
3580
|
+
xMax: spec.x + dx,
|
|
3581
|
+
yMin: spec.y - dy,
|
|
3582
|
+
yMax: spec.y + dy
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
const xs = local.map((p) => p[0]);
|
|
3586
|
+
const ys = local.map((p) => p[1]);
|
|
3587
|
+
return {
|
|
3588
|
+
xMin: Math.min(...xs),
|
|
3589
|
+
xMax: Math.max(...xs),
|
|
3590
|
+
yMin: Math.min(...ys),
|
|
3591
|
+
yMax: Math.max(...ys)
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
/** Out-of-bounds message, or undefined if the whole outline lies inside the region. */
|
|
3595
|
+
function checkInBounds(b, ext) {
|
|
3596
|
+
if (b.xMin < -1e-6 || b.xMax > ext.uMax + EPS$5 || b.yMin < -1e-6 || b.yMax > ext.vMax + EPS$5) return `cutout extent [${b.xMin.toFixed(3)}, ${b.xMax.toFixed(3)}]×[${b.yMin.toFixed(3)}, ${b.yMax.toFixed(3)}] lies outside region extent [0,${ext.uMax}]×[0,${ext.vMax}]`;
|
|
3515
3597
|
}
|
|
3516
3598
|
/**
|
|
3517
|
-
* Build the 3D cut tool
|
|
3518
|
-
* (`origin + x·u + y·v − HAIR·n`),
|
|
3519
|
-
*
|
|
3520
|
-
*
|
|
3599
|
+
* Build the 3D cut tool on the region's bottom surface, a hair below it
|
|
3600
|
+
* (`origin + x·u + y·v − HAIR·n`), through the sheet by `thickness + 2·HAIR` along
|
|
3601
|
+
* +n. Using the world frame's `u`/`v`/`n` is what lands the cut on the correct
|
|
3602
|
+
* folded face. A hole is a true cylinder and an obround slot has true arcs, so a
|
|
3603
|
+
* round cutout adds one or two curved faces to the solid, not one planar face for
|
|
3604
|
+
* each segment of its developed loop. Intermediate shapes go to `scope`.
|
|
3521
3605
|
*/
|
|
3522
|
-
function buildTool(local, f, thickness) {
|
|
3606
|
+
function buildTool(spec, local, f, thickness, scope) {
|
|
3523
3607
|
const base = vecAdd(f.origin, vecScale(f.n, -.05));
|
|
3524
|
-
const
|
|
3525
|
-
const
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3608
|
+
const toWorld = ([x, y]) => vecAdd(vecAdd(base, vecScale(f.u, x)), vecScale(f.v, y));
|
|
3609
|
+
const depth = thickness + 2 * HAIR$1;
|
|
3610
|
+
if (spec.kind === "hole") return ok(cylinder(spec.diameter / 2, depth, {
|
|
3611
|
+
at: toWorld([spec.x, spec.y]),
|
|
3612
|
+
axis: f.n
|
|
3613
|
+
}));
|
|
3614
|
+
const edges = spec.kind === "slot" && spec.round === true ? obroundEdges(spec, toWorld) : polylineEdges(local, toWorld);
|
|
3615
|
+
for (const e of edges) scope.register(e);
|
|
3532
3616
|
const wire = wireLoop(edges);
|
|
3533
3617
|
if (!wire.ok) return wire;
|
|
3618
|
+
scope.register(wire.value);
|
|
3534
3619
|
if (!isPlanarWire(wire.value)) return err(validationError("CUTOUT_TOOL_FAILED", "cutout profile wire is not planar"));
|
|
3535
3620
|
const profile = face(wire.value);
|
|
3536
3621
|
if (!profile.ok) return profile;
|
|
3537
|
-
|
|
3622
|
+
scope.register(profile.value);
|
|
3538
3623
|
return extrude(profile.value, [
|
|
3539
3624
|
f.n[0] * depth,
|
|
3540
3625
|
f.n[1] * depth,
|
|
3541
3626
|
f.n[2] * depth
|
|
3542
3627
|
]);
|
|
3543
3628
|
}
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3629
|
+
function polylineEdges(local, toWorld) {
|
|
3630
|
+
const pts = local.map(toWorld);
|
|
3631
|
+
return pts.map((a, i) => line(a, pts[(i + 1) % pts.length] ?? a));
|
|
3632
|
+
}
|
|
3633
|
+
/**
|
|
3634
|
+
* Edges of an obround slot: two straight sides joined by two semicircles of radius
|
|
3635
|
+
* `width/2`. A slot as wide as it is long has no straight side, so it is two
|
|
3636
|
+
* semicircles (a circle).
|
|
3637
|
+
*/
|
|
3638
|
+
function obroundEdges(spec, toWorld) {
|
|
3639
|
+
const hw = spec.width / 2;
|
|
3640
|
+
const straight = Math.max(spec.length / 2 - hw, 0);
|
|
3641
|
+
const rad = (spec.angleDeg ?? 0) * Math.PI / 180;
|
|
3642
|
+
const c = Math.cos(rad);
|
|
3643
|
+
const s = Math.sin(rad);
|
|
3644
|
+
const at = (x, y) => toWorld([spec.x + x * c - y * s, spec.y + x * s + y * c]);
|
|
3645
|
+
const rightBottom = at(straight, -hw);
|
|
3646
|
+
const rightTop = at(straight, hw);
|
|
3647
|
+
const leftTop = at(-straight, hw);
|
|
3648
|
+
const leftBottom = at(-straight, -hw);
|
|
3649
|
+
const edges = [threePointArc(rightBottom, at(straight + hw, 0), rightTop)];
|
|
3650
|
+
if (straight > EPS$5) edges.push(line(rightTop, leftTop));
|
|
3651
|
+
edges.push(threePointArc(leftTop, at(-straight - hw, 0), leftBottom));
|
|
3652
|
+
if (straight > EPS$5) edges.push(line(leftBottom, rightBottom));
|
|
3653
|
+
return edges;
|
|
3654
|
+
}
|
|
3655
|
+
/** Developed-plane {@link Frame2} of a region (base or flange) in the unfold layout. */
|
|
3656
|
+
function developedFrame(layout, regionId) {
|
|
3657
|
+
const placed = layout.flats.find((p) => p.id === regionId);
|
|
3551
3658
|
if (placed === void 0) return err(validationError("UNKNOWN_REGION", `developedFrame: region '${regionId}' missing from layout`));
|
|
3552
3659
|
return ok(placed.frame);
|
|
3553
3660
|
}
|
|
@@ -4227,22 +4334,17 @@ function foldWithWarnings(input) {
|
|
|
4227
4334
|
if (!relieved.ok) return relieved;
|
|
4228
4335
|
part = relieved.value;
|
|
4229
4336
|
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
}
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
region: region.id
|
|
4242
|
-
});
|
|
4243
|
-
if (!cutResult.ok) return cutResult;
|
|
4244
|
-
part = cutResult.value;
|
|
4245
|
-
}
|
|
4337
|
+
const cutoutSpecs = (input.baseCutouts ?? []).map((spec) => ({
|
|
4338
|
+
...spec,
|
|
4339
|
+
region: ROOT_FLAT_ID
|
|
4340
|
+
}));
|
|
4341
|
+
for (const region of input.regions) for (const spec of region.cutouts ?? []) cutoutSpecs.push({
|
|
4342
|
+
...spec,
|
|
4343
|
+
region: region.id
|
|
4344
|
+
});
|
|
4345
|
+
const cutResult = addCutouts(part, cutoutSpecs);
|
|
4346
|
+
if (!cutResult.ok) return cutResult;
|
|
4347
|
+
part = cutResult.value;
|
|
4246
4348
|
for (const spec of input.baseTabs ?? []) {
|
|
4247
4349
|
const tabbed = addTab(part, {
|
|
4248
4350
|
...spec,
|
|
@@ -7119,6 +7221,10 @@ function relieveCorner(part, flangeIdA, flangeIdB, spec) {
|
|
|
7119
7221
|
function addCutout$1(part, spec) {
|
|
7120
7222
|
return addCutout(part, spec);
|
|
7121
7223
|
}
|
|
7224
|
+
/** Punch many cutouts in one boolean cut. Use this for a pattern of cutouts. */
|
|
7225
|
+
function addCutouts$1(part, specs) {
|
|
7226
|
+
return addCutouts(part, specs);
|
|
7227
|
+
}
|
|
7122
7228
|
/** Punch a circular hole of `diameter` centred at region-local `(x, y)`. */
|
|
7123
7229
|
function addHole$1(part, region, x, y, diameter) {
|
|
7124
7230
|
return addHole(part, region, x, y, diameter);
|
|
@@ -7281,6 +7387,10 @@ var SheetMetalPartHandle = class SheetMetalPartHandle {
|
|
|
7281
7387
|
cutout(spec) {
|
|
7282
7388
|
return new SheetMetalPartHandle(unwrapOrThrow(addCutout$1(this.part, spec)));
|
|
7283
7389
|
}
|
|
7390
|
+
/** Punch many cutouts in one boolean cut. Use this for a pattern of cutouts. */
|
|
7391
|
+
cutouts(specs) {
|
|
7392
|
+
return new SheetMetalPartHandle(unwrapOrThrow(addCutouts$1(this.part, specs)));
|
|
7393
|
+
}
|
|
7284
7394
|
/** Punch a circular hole of `diameter` centred at region-local `(x, y)`. */
|
|
7285
7395
|
hole(region, x, y, diameter) {
|
|
7286
7396
|
return new SheetMetalPartHandle(unwrapOrThrow(addHole$1(this.part, region, x, y, diameter)));
|
|
@@ -7436,6 +7546,9 @@ var SheetMetalBuilder = class SheetMetalBuilder {
|
|
|
7436
7546
|
cutout(spec) {
|
|
7437
7547
|
return this.build().cutout(spec);
|
|
7438
7548
|
}
|
|
7549
|
+
cutouts(specs) {
|
|
7550
|
+
return this.build().cutouts(specs);
|
|
7551
|
+
}
|
|
7439
7552
|
hole(region, x, y, diameter) {
|
|
7440
7553
|
return this.build().hole(region, x, y, diameter);
|
|
7441
7554
|
}
|
|
@@ -7505,4 +7618,4 @@ function foldFlat(input) {
|
|
|
7505
7618
|
function fromSolid(solid, opts) {
|
|
7506
7619
|
return new ForeignSolidHandle(solid, opts);
|
|
7507
7620
|
}
|
|
7508
|
-
export { ALUMINUM_GAUGES, DEFAULT_K_FACTOR, ForeignSolidHandle, MATERIALS, ROOT_FLAT_ID, STEEL_GAUGES, SheetMetalBuilder, SheetMetalError, SheetMetalPartHandle, addBendRelief, addBendTable, addCutout, addForm, addHole, addPolygonCutout, addSlot, addTab, allowance, author, authorContourFlange, authorLoftedFlange, authorPart, autoBendReliefs, autoMiterCorner, autoReliefs, baseEdgeRef, bendAllowance, bendRelief, bendTable, buildFeatureGraph, buildFeatureTree, buildReport, contourFlange, cornerRelief, developed, developedLength, emboss, featureTree, fitCylinder, flatPatternToDXF, flatPatternToPolylines, fold, foldFlat, fold$1 as foldPart, foldWithWarnings, fromPart, fromSolid, getBendTable, getMaterial, hem, jog, loftedFlange, louver, miter, miterCorner, miterCut, multiPatternToDXF, nest, nestToDXF, neutralRadius, partToFlatInput, patternBbox, patternToFlatInput, pointInPolygon, polygonBounds, polygonsOverlap, polygonsOverlapWithClearance, registerBendTable, relieveCorner, report, reportFrom, reportFromUnfold, reportJSON, reportToJSON, resolveAllowance, resolveBendAllowance, segmentsIntersect, sheetMetal, signedArea, tabAndSlot, toDXF, transformPolygon, unfold, unfoldForeignSolid, unfold$1 as unfoldPart, unfoldSolid, validate, validatePart, wireToPolygon };
|
|
7621
|
+
export { ALUMINUM_GAUGES, DEFAULT_K_FACTOR, ForeignSolidHandle, MATERIALS, ROOT_FLAT_ID, STEEL_GAUGES, SheetMetalBuilder, SheetMetalError, SheetMetalPartHandle, addBendRelief, addBendTable, addCutout, addCutouts, addForm, addHole, addPolygonCutout, addSlot, addTab, allowance, author, authorContourFlange, authorLoftedFlange, authorPart, autoBendReliefs, autoMiterCorner, autoReliefs, baseEdgeRef, bendAllowance, bendRelief, bendTable, buildFeatureGraph, buildFeatureTree, buildReport, contourFlange, cornerRelief, developed, developedLength, emboss, featureTree, fitCylinder, flatPatternToDXF, flatPatternToPolylines, fold, foldFlat, fold$1 as foldPart, foldWithWarnings, fromPart, fromSolid, getBendTable, getMaterial, hem, jog, loftedFlange, louver, miter, miterCorner, miterCut, multiPatternToDXF, nest, nestToDXF, neutralRadius, partToFlatInput, patternBbox, patternToFlatInput, pointInPolygon, polygonBounds, polygonsOverlap, polygonsOverlapWithClearance, registerBendTable, relieveCorner, report, reportFrom, reportFromUnfold, reportJSON, reportToJSON, resolveAllowance, resolveBendAllowance, segmentsIntersect, sheetMetal, signedArea, tabAndSlot, toDXF, transformPolygon, unfold, unfoldForeignSolid, unfold$1 as unfoldPart, unfoldSolid, validate, validatePart, wireToPolygon };
|
package/dist/cutoutFns.d.ts
CHANGED
|
@@ -12,8 +12,18 @@ type Pt2 = [number, number];
|
|
|
12
12
|
* mapped through the region's developed {@link Frame2} and recorded as a
|
|
13
13
|
* {@link CutoutFeature}, so {@link unfold} emits the matching loop in the flat
|
|
14
14
|
* pattern and {@link fold} can replay it. Guards a valid, single-bodied solid.
|
|
15
|
+
*
|
|
16
|
+
* Each call is one boolean cut on a solid that the cut before it made larger. For
|
|
17
|
+
* a pattern of cutouts, {@link addCutouts} makes one cut for all of them.
|
|
15
18
|
*/
|
|
16
19
|
export declare function addCutout(part: SheetMetalPart, spec: CutoutSpec): Result<SheetMetalPart>;
|
|
20
|
+
/**
|
|
21
|
+
* Punch many cutouts in one boolean cut. The result is the same part that a chain
|
|
22
|
+
* of {@link addCutout} calls gives, with the features recorded in `specs` order,
|
|
23
|
+
* but the cost does not grow with the square of the count. A spec that fails
|
|
24
|
+
* validation fails the whole call, and no cutout is made.
|
|
25
|
+
*/
|
|
26
|
+
export declare function addCutouts(part: SheetMetalPart, specs: readonly CutoutSpec[]): Result<SheetMetalPart>;
|
|
17
27
|
/** Punch a circular hole of `diameter` centred at region-local `(x, y)`. */
|
|
18
28
|
export declare function addHole(part: SheetMetalPart, region: string, x: number, y: number, diameter: number): Result<SheetMetalPart>;
|
|
19
29
|
/** Punch a slot centred at `(x, y)`: `length` along the slot axis by `width` across. */
|
package/dist/facade.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ declare class SheetMetalPartHandle {
|
|
|
24
24
|
cornerRelief(flangeIdA: string, flangeIdB: string, spec?: ReliefSpec): SheetMetalPartHandle;
|
|
25
25
|
/** Punch a cutout (hole / slot / polygon) through a named flat region. */
|
|
26
26
|
cutout(spec: CutoutSpec): SheetMetalPartHandle;
|
|
27
|
+
/** Punch many cutouts in one boolean cut. Use this for a pattern of cutouts. */
|
|
28
|
+
cutouts(specs: readonly CutoutSpec[]): SheetMetalPartHandle;
|
|
27
29
|
/** Punch a circular hole of `diameter` centred at region-local `(x, y)`. */
|
|
28
30
|
hole(region: string, x: number, y: number, diameter: number): SheetMetalPartHandle;
|
|
29
31
|
/** Punch a slot (rectangular or obround) centred at region-local `(x, y)`. */
|
|
@@ -123,6 +125,7 @@ declare class SheetMetalBuilder {
|
|
|
123
125
|
autoReliefs(spec?: ReliefSpec): SheetMetalPartHandle;
|
|
124
126
|
cornerRelief(flangeIdA: string, flangeIdB: string, spec?: ReliefSpec): SheetMetalPartHandle;
|
|
125
127
|
cutout(spec: CutoutSpec): SheetMetalPartHandle;
|
|
128
|
+
cutouts(specs: readonly CutoutSpec[]): SheetMetalPartHandle;
|
|
126
129
|
hole(region: string, x: number, y: number, diameter: number): SheetMetalPartHandle;
|
|
127
130
|
slot(region: string, opts: {
|
|
128
131
|
x: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type { AuthorSpec, BaseFlatSpec, FlangeSpec, FlangeSide, SeamSpec, } from
|
|
|
14
14
|
export { miterCut, autoMiterCorner } from './miterFns.js';
|
|
15
15
|
export type { MiterPlane } from './miterFns.js';
|
|
16
16
|
export { addBendRelief, autoBendReliefs, cornerRelief } from './reliefFns.js';
|
|
17
|
-
export { addCutout, addHole, addSlot, addPolygonCutout } from './cutoutFns.js';
|
|
17
|
+
export { addCutout, addCutouts, addHole, addSlot, addPolygonCutout } from './cutoutFns.js';
|
|
18
18
|
export { addTab, tabAndSlot } from './tabFns.js';
|
|
19
19
|
export type { SlotPlacement } from './tabFns.js';
|
|
20
20
|
export { addForm, louver, emboss } from './formFns.js';
|