partforge 0.99.0 → 0.100.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/docs/AUTHORING-PARTS.md +33 -0
- package/docs/ERROR-PATTERNS.md +1 -1
- package/docs/KERNEL-CONTRACT.md +1 -0
- package/package.json +1 -1
- package/src/framework/geometry/kernel-front.js +67 -0
- package/src/framework/geometry/kernel.js +3 -0
- package/src/framework/geometry/op-options.js +24 -0
- package/types/kernel.d.ts +28 -0
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -1221,6 +1221,36 @@ turn. A 30 mm shank is 20 turns and about half a second on Manifold; hundreds of
|
|
|
1221
1221
|
turns is millions of triangles and minutes behind the STEP button. Bound the
|
|
1222
1222
|
`length` and `pitch` your schema exposes accordingly.
|
|
1223
1223
|
|
|
1224
|
+
**Internal threads: use `k.tappedBore`, not a bore plus a `screwSweep`.** The
|
|
1225
|
+
periodic trick above is what saves an *external* thread from a boolean, and it
|
|
1226
|
+
has no internal equivalent — a tapped hole is a bore and a thread, and they have
|
|
1227
|
+
to be combined. Assembled by hand the obvious way, they are a trap:
|
|
1228
|
+
|
|
1229
|
+
```js
|
|
1230
|
+
// ✗ the bore wall and the thread root land on the same cylinder
|
|
1231
|
+
const bore = k.cylinder({ d: boreD, h: depth });
|
|
1232
|
+
const thread = k.screwSweep({ profile: [[boreD / 2, 0], …], pitch, turns });
|
|
1233
|
+
cap.cutAll([bore, thread]);
|
|
1234
|
+
// ✓ one tool, no shared face
|
|
1235
|
+
cap.cut(k.tappedBore({ d: boreD, pitch, turns, depth }));
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
Both cut tools touch along an exactly coincident cylinder without overlapping.
|
|
1239
|
+
Manifold shrugs; OCCT has to resolve a tangential contact between a cylinder and
|
|
1240
|
+
the thread root's swept spline surface, along a helix, and the intersector
|
|
1241
|
+
degenerates — on one real 6-turn cap the export did not finish in **fifteen
|
|
1242
|
+
minutes**, while `tappedBore` builds the same hole in about fifteen seconds
|
|
1243
|
+
([boolean-coincident-faces-hang](ERROR-PATTERNS.md#boolean-coincident-faces-hang)).
|
|
1244
|
+
|
|
1245
|
+
`k.tappedBore({ d, pitch, turns, depth?, crest?, lefthand?, rootSink?, overshoot? })`
|
|
1246
|
+
returns that hole as **one** solid to cut: `d` is the bore a tap would cut into,
|
|
1247
|
+
`crest` the radial thread height (default `0.15 · pitch`), `depth` the plain-bore
|
|
1248
|
+
length (default the thread's own). It owns both halves precisely so it can sink
|
|
1249
|
+
the thread's root *inside* the bore — which costs nothing, since the bore already
|
|
1250
|
+
removes that material, and is why the tangency never arises. `screwSweep` cannot
|
|
1251
|
+
do that for you: a thread cut into solid stock with no bore would then cut a
|
|
1252
|
+
deeper root and change your part.
|
|
1253
|
+
|
|
1224
1254
|
The hand-rolled equivalent, for the record: `screwSweep` is
|
|
1225
1255
|
`k.extrude({ profile, h, twist })` with the axial profile remapped to polar
|
|
1226
1256
|
(`ψ = −360·z/pitch`) and `twist = 360 · turns` — one full turn of twist per pitch
|
|
@@ -3375,6 +3405,9 @@ symptom first** — it maps error text → cause → fix. The invariants, one li
|
|
|
3375
3405
|
of deliberate clearance, or overshoot the cut. Mesh CSG shrugs; OCCT's boolean
|
|
3376
3406
|
degenerates, so the part previews instantly and the STEP export runs for minutes
|
|
3377
3407
|
([boolean-coincident-faces-hang](ERROR-PATTERNS.md#boolean-coincident-faces-hang)).
|
|
3408
|
+
For the case that causes this most often — a tapped hole — reach for
|
|
3409
|
+
`k.tappedBore`, which owns the bore and the thread together and cannot land them
|
|
3410
|
+
on the same face.
|
|
3378
3411
|
|
|
3379
3412
|
---
|
|
3380
3413
|
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -65,7 +65,7 @@ The framework itself rebuilds each sub-part fresh per job and applies `place` on
|
|
|
65
65
|
|
|
66
66
|
- **Symptom:** A part previews instantly but a STEP export (or any OCCT-path build) of one sub-part runs for minutes and never finishes, with no error, no warning, and no progress. Cutting each tool on its own is fast; only the combination hangs. Threaded parts are the usual victims.
|
|
67
67
|
- **Cause:** Two cut tools in the same `cutAll` (or a tool and the body) share an *exactly* coincident face — most often a bore whose radius equals a thread's root radius, so the bore wall and the thread root lie on the same cylinder. OCCT's boolean has to classify a surface that is simultaneously on both operands, and the intersection search degenerates. Manifold's mesh CSG does not care, which is why the preview is fine and only the exact kernel suffers. Measured on one real part: bore alone 0.4 s, thread alone 5.4 s, both together did not finish in fifteen minutes; moving the bore 0.05 mm brought the pair to 12.6 s.
|
|
68
|
-
- **Fix:**
|
|
68
|
+
- **Fix:** For a tapped hole — far and away the most common cause — use `k.tappedBore({ d, pitch, turns, depth })`, which returns the bore and its thread as one tool and cannot put them on the same face. Otherwise: give the surfaces a deliberate clearance instead of letting them land on the same number. Derive one from the other with an explicit gap — `const boreD = threadRootD - 2 * boreClearance;` with `boreClearance` around 0.05-0.1 mm — rather than reusing the same expression for both. The gap is far below a printable layer, so the fit is unchanged. The same rule covers a cut that ends exactly flush with a face (overshoot it by a few tenths, as the surrounding examples do with `+ 0.4` / `- 0.2`) and two tools that abut exactly end-to-end.
|
|
69
69
|
|
|
70
70
|
## chamfer-rescue-bisection
|
|
71
71
|
|
package/docs/KERNEL-CONTRACT.md
CHANGED
|
@@ -288,6 +288,7 @@ above. All ops return a `Solid`.
|
|
|
288
288
|
| `sweep({profile, path, closed?, cornerRadius?, ruled?, smooth?})` | Sweep a fixed CCW profile along a polyline with a rotation-minimizing frame; sharp mitered corners, or `cornerRadius` fillets; capped ends. |
|
|
289
289
|
| `helixSweptTube({pathR, profileR, pitch, turns, z0, lefthand})` | Circle of radius `profileR` swept along a helix (e.g. a rope groove). Circular profile on a frenet frame that rolls with the helix — **not for threads**; use `screwSweep`. |
|
|
290
290
|
| `screwSweep({profile, pitch, turns, lefthand})` | Screw-motion sweep of an axial lathe profile `[[r, z], …]` (r ≥ 0) — threads. The profile travels to `(r·cosθ, r·sinθ, z + pitch·θ/2π)`; `h = pitch · turns`. Axial extent must not exceed `pitch` or consecutive turns interpenetrate (throws). A profile spanning exactly `pitch` is **periodic**: first and last radius must agree, and it yields a complete threaded body needing no boolean. Compound: the polar-remapped, densified section extruded with `twist = 360 · turns`, exactly as composed in `kernel-front.js`; a backend may override only for caching, never for different geometry. Options-only. Parity: **within tolerance, not by construction** — both backends receive the identical densified polygon, but the mesh backend facets the twist at its own resolution while the B-rep backend builds an exact spline (`hull`'s parity class). |
|
|
291
|
+
| `tappedBore({d, pitch, turns, depth?, crest?, lefthand?, rootSink?, overshoot?})` | A tapped (internally threaded) hole as **one cut tool** — the plain bore of diameter `d` fused with its thread. Compound (`kernel-front.js`), so no backend implements it. Exists for robustness, not ergonomics: assembled by hand the bore wall and the thread root land on exactly the same cylinder, and OCCT's boolean cannot resolve that tangential contact along a helix — measured at fifteen minutes without finishing on a 6-turn cap (73–92 s even with OCCT's own `GlueShift`/`GlueFull` mitigation), against ~14 s for this op. Two offsets carry that: `rootSink` (default 0.2) sinks the thread's root INSIDE the bore by extending each flank COLINEARLY — the same swept surface carried further down, so everything at `r ≥ d/2` is identical to the hand-assembled construction and the union with the bore clips the rest (verified on Manifold, which can build the tangent form, to ~1e-5 of tool volume; sinking the root points radially instead would tilt the flanks and fatten the tooth outside the bore by a measured 2%). The extension steals axial root-flat width, so the sink is silently clamped where a full `rootSink` would consume it — the clamped overlap still clears the coincidence band by an order of magnitude. And `overshoot` (default 0.2) overhangs the bore past the thread at both ends, because flush ends are coincident faces one layer down, where the union does not hang but silently returns the bore alone (operands swapped, an empty solid). Both are refused at zero. `depth` defaults to the thread's own length `pitch · turns` and is refused SHORTER than it (the thread would poke out past the bore's far end); `crest` (radial thread height) defaults to `0.15 · pitch`; the tooth is a printable trapezoid, root flat `pitch/4` and crest flat `pitch/8`. This op owns BOTH halves on purpose: `screwSweep` cannot sink its own root, because a thread cut into solid stock with no bore would then cut deeper and change the part. Options-only. Parity: `screwSweep`'s class (within tolerance), which it composes. |
|
|
291
292
|
| `loftSmooth({sections, stations?, samples?, shading?, closed?})` | Spline-interpolated loft of ≥2 sparse control sections — loft-style ring specs `{polygon\|sides+radius\|curve contour\|Shape2D, z, rotate?, scale?, sharp?}`; vertex counts **may differ**. A point section may tag `sharp: [indices]` as true corners (integers in `0…points.length-1`, sorted/deduped silently); a curve/`Shape2D` section takes corners implicitly from its non-smooth joints (single-region, hole-free, `loftSmooth:`-prefixed `k.loft` validation) and rejects an explicit `sharp`. Every section must resolve to the **same corner count `m`** (frozen error otherwise); with `m ≥ 1` corner 0 anchors the seam (replacing vertex 0), with `m = 0` v1's vertex-0 anchor holds verbatim. Compound (`kernel-front.js` + `loft-smooth.js`): each section's outline is a closed centripetal Catmull-Rom split into `m` clamped open arcs at its corners (or one closed periodic CR when `m = 0`); the `samples` budget is apportioned across arcs by mean arc-length fraction (largest-remainder, min 1 span/arc) and each arc resampled by arc length — total ring vertex count is `samples`, identical across sections, exactly v1's invariant now corner-anchored. The cross-station direction is v1 verbatim (shared centroid-spine knots, per-vertex CR, reflection phantoms at the ends, or periodic knots when `closed: true`). What's new is emission: every station — the dense list and the sparse `stations:"controls"` list alike — is fitted back to an **all-cubic Bézier contour**, arc-by-arc, via exact 4-point CR→Bézier inversion, so **both backends receive identical curve rings**. A B-rep kernel lofts the sparse control wires with its native smooth skin (`ruled: false`) — curve-exact around each ring in STEP (the densified-*point*-wire alternative measured 23 s / WASM-abort territory, which curve wires don't hit). A mesh kernel densifies `stations` rings and lofts them through `k.loft`'s curve-mode per-segment sampling, creasing sharp/corner columns via loft's geometric corner policy. `closed: true` (default false; needs ≥3 control sections, frozen error otherwise) makes the cross-station CR periodic (no reflection phantoms, ring 0 not repeated) and is **Manifold-only**, same restriction as `loft` `closed: true`: a B-rep kernel throws `loftSmooth: closed:true loops are only supported on the Manifold backend` in the composition, before building any rings; combining `closed: true` with `stations:"controls"` is rejected as a defensive invariant (reachable only by explicitly passing the internal `stations:"controls"` value; the composition never produces the combination itself). Options-only. Defaults `stations = (n−1)·8+1` open / `n·8` closed (raised to the section count `n` when lower), `samples = max(64, largest section)` (raised to the corner count `m` when lower); clamps 2…1024 / 8…2048 — the defaults cap themselves at the ceilings, only explicit out-of-range values throw. The surface interpolates every control section exactly. Parity: **within tolerance** (`screwSweep`'s class, unchanged from v1 — ~0.4% measured on the propeller reference part, test-gated at 2%). STEP is now curve-exact around each ring (previously faceted at the `samples` LOD); the cross-station skin remains ThruSections' native fit, not the shared CR — exact cross-station B-splines are a v3 candidate. Additive: `sharp`, curve/`Shape2D` sections, and `closed` are new options on top of v1's `{sections, stations?, samples?, shading?}`; `CONTRACT_VERSION` stays 4 — the same non-bump precedent as `import` above, a refinement inside the op's already-stated tolerance class rather than a new one. |
|
|
292
293
|
| `heightfield(nameOrGrid, {w, d, base?, maxZ?, pitch?, invert?, range?, origin?})` | A depth map as a relief solid: a sampled grid top at `z = base + maxZ·f(v)`, skirt walls, and a flat base cap at `z = 0`. `nameOrGrid` is a name declared in the part's `images` field, or an inline `{width, height, data}` grid. Sample count per axis is `max(2, ceil(w/pitch))` and `max(2, ceil(d/pitch))`; if their product exceeds a vertex budget, `pitch` is scaled up uniformly to fit and, if still over, the two counts are shrunk in lockstep — with a `takeBuildWarnings` message rather than an error. `range` is a remap with clamped ends (`range[0]`→0, `range[1]`→1); `invert` applies after, as `1−v`. `origin` positions the footprint in XY only — the base always sits at `z = 0`. The image stretches to `w × d`; aspect is not preserved. **Axis convention:** sample row 0 (a source PNG's first scanline, i.e. its visual top in an image viewer) maps to the footprint's **−Y** edge — Y increases with row, the standard texture-coordinate mapping — so a depth map viewed from +Z looks vertically mirrored relative to the same file opened in a viewer; flip the source pixels before declaring the image if that orientation is unwanted (`invert` remaps height values, not position, and does not affect this). Fed by an underscore-prefixed side-channel (`_registerImage`), not part authors — see [Conformance classes](#conformance-classes). Required on both in-repo backends: Manifold imports the triangles directly, OCCT sews them into a faceted B-rep via `importSTL`, so STEP export carries a triangulated surface rather than an analytic one. Parity: **exact** — both backends receive byte-identical triangle data. Additive: `CONTRACT_VERSION` stays 4, the same precedent as `import` and `loftSmooth`. |
|
|
293
294
|
| `union(solids[])` | Boolean union of one or more solids. |
|
package/package.json
CHANGED
|
@@ -57,6 +57,73 @@ export function finishKernel(k) {
|
|
|
57
57
|
twist: (lefthand ? -360 : 360) * turns,
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
// Compound default: a tapped (internally threaded) hole, as ONE cut tool.
|
|
61
|
+
//
|
|
62
|
+
// The tool is a plain bore fused with a helical thread, and the whole reason
|
|
63
|
+
// this op exists is the ONE line that makes that fuse cheap: the thread's root
|
|
64
|
+
// is sunk INSIDE the bore rather than sitting on it. Hand-assembled, a tapped
|
|
65
|
+
// hole is written the obvious way — bore of diameter d, thread whose root
|
|
66
|
+
// radius is also d/2 — and those two tools then touch along an exactly
|
|
67
|
+
// coincident cylinder without overlapping. Mesh CSG does not care. OCCT does:
|
|
68
|
+
// it has to resolve a tangential contact between a cylinder and the swept
|
|
69
|
+
// B-spline surface of the thread root, along a helix, and the intersector
|
|
70
|
+
// degenerates. Measured on a real 6-turn M6-ish cap: the tangent form did not
|
|
71
|
+
// finish in fifteen minutes (with OCCT's own glue mitigation, 73-92 s); the
|
|
72
|
+
// sunk-root form is 8.4 s.
|
|
73
|
+
//
|
|
74
|
+
// Sinking is FREE, not a tolerance compromise: everything inside the bore is
|
|
75
|
+
// already removed by the bore, so the union is the same point set either way.
|
|
76
|
+
// That is exactly why this has to be an op that owns BOTH halves — screwSweep
|
|
77
|
+
// alone cannot sink its own root, because a thread cut into solid stock with
|
|
78
|
+
// no bore would then cut a deeper root and change the part.
|
|
79
|
+
k.tappedBore ??= ({ d, pitch, turns, depth, crest, lefthand = false, rootSink = 0.2, overshoot = 0.2 }) => {
|
|
80
|
+
const rootR = d / 2;
|
|
81
|
+
const majorR = rootR + (crest ?? 0.15 * pitch);
|
|
82
|
+
// A printable trapezoidal tooth: flat at root and crest so neither knife-edges
|
|
83
|
+
// at FDM resolution. Same shape a hand-rolled coarse thread converges on.
|
|
84
|
+
const rootFlat = pitch / 4;
|
|
85
|
+
const crestFlat = pitch / 8;
|
|
86
|
+
const rise = (pitch - rootFlat - crestFlat) / 2;
|
|
87
|
+
// Sink the root by extending the flanks COLINEARLY into the bore, not by
|
|
88
|
+
// moving the root points straight inward. A radially-moved root changes the
|
|
89
|
+
// flank slope, which fattens the tooth OUTSIDE the bore — measured at 2% of
|
|
90
|
+
// tool volume, and it lands exactly where a mating printed thread needs its
|
|
91
|
+
// clearance. A colinear extension is the same swept surface carried further
|
|
92
|
+
// down, so everything at r >= rootR is byte-identical to the hand-assembled
|
|
93
|
+
// tangent construction and the union with the bore clips the rest — the
|
|
94
|
+
// identity test in test/tapped-bore.test.js holds this to mesh precision.
|
|
95
|
+
// The extension steals axial root-flat width (e per side), so the sink is
|
|
96
|
+
// clamped where a full rootSink would consume it; the clamped overlap still
|
|
97
|
+
// clears the coincidence band by an order of magnitude.
|
|
98
|
+
const slope = rise / (majorR - rootR); // axial travel per unit of radial travel, along a flank
|
|
99
|
+
const sink = Math.min(rootSink, rootR, (0.45 * rootFlat) / slope);
|
|
100
|
+
const innerR = rootR - sink;
|
|
101
|
+
const e = slope * sink;
|
|
102
|
+
// Shifted down by e so the thread still spans [0, pitch*turns] — the bore's
|
|
103
|
+
// overshoot math below must not depend on the flank geometry.
|
|
104
|
+
const thread = k.screwSweep({
|
|
105
|
+
profile: [
|
|
106
|
+
[innerR, 0],
|
|
107
|
+
[innerR, rootFlat - 2 * e],
|
|
108
|
+
[majorR, rootFlat + rise - e],
|
|
109
|
+
[majorR, rootFlat + rise + crestFlat - e],
|
|
110
|
+
[innerR, pitch],
|
|
111
|
+
],
|
|
112
|
+
pitch, turns, lefthand,
|
|
113
|
+
});
|
|
114
|
+
// The bore overhangs the thread at BOTH ends, for the same reason the sink
|
|
115
|
+
// exists: flush is a coincident face. Built with both starting at z = 0 the
|
|
116
|
+
// bore's end cap is coplanar with the thread's first turn, and this union
|
|
117
|
+
// does not hang — it silently returns the bore alone (or, with the operands
|
|
118
|
+
// swapped, an empty solid). A wrong answer with no error is the worst of the
|
|
119
|
+
// three failure modes, and overhanging is what avoids it.
|
|
120
|
+
const threadLength = pitch * turns;
|
|
121
|
+
return k.union([
|
|
122
|
+
k.cylinder({ d, h: (depth ?? threadLength) + 2 * overshoot }).translate([0, 0, -overshoot]),
|
|
123
|
+
thread,
|
|
124
|
+
]);
|
|
125
|
+
};
|
|
126
|
+
|
|
60
127
|
// Compound default: spline-smoothed loft. The shared Catmull-Rom densifier
|
|
61
128
|
// (loft-smooth.js) now emits all-cubic curve rings on BOTH paths — every ring
|
|
62
129
|
// is contour IR ({start, segments:[{to,c1,c2}…]}), fitted exactly from the CR
|
|
@@ -27,6 +27,8 @@ export const KERNEL_OPS = [
|
|
|
27
27
|
"loftSmooth",
|
|
28
28
|
// Additive in 0.92 (same precedent): both backends implement it.
|
|
29
29
|
"heightfield",
|
|
30
|
+
// Additive (same precedent): a compound default, so neither backend implements it.
|
|
31
|
+
"tappedBore",
|
|
30
32
|
];
|
|
31
33
|
|
|
32
34
|
// Backend-optional kernel ops: the sub-part cache brackets + WASM lifetime hooks.
|
|
@@ -151,6 +153,7 @@ export const ROUTED_CAD_OPS = ["shell"];
|
|
|
151
153
|
* @property {(o:{profile:number[][],degrees?:number}) => Solid} revolve revolve a lathe profile [[r,z],…] around Z; legacy (points,opts) accepted for now (see file header)
|
|
152
154
|
* @property {(o:{pathR:number,profileR:number,pitch:number,turns:number,z0:number,lefthand:boolean}) => Solid} helixSweptTube
|
|
153
155
|
* @property {(o:{profile:number[][],pitch:number,turns:number,lefthand?:boolean}) => Solid} screwSweep screw-motion sweep of an axial [[r,z]] profile — threads; options-only
|
|
156
|
+
* @property {(o:{d:number,pitch:number,turns:number,depth?:number,crest?:number,lefthand?:boolean,rootSink?:number,overshoot?:number}) => Solid} tappedBore compound: a tapped hole as ONE cut tool — bore plus thread, root sunk inside the bore so the two never share a face
|
|
154
157
|
* @property {(o:{sections:object[],stations?:number,samples?:number,shading?:string,closed?:boolean}) => Solid} loftSmooth Catmull-Rom-densified loft of sparse control sections; options-only
|
|
155
158
|
* @property {(nameOrGrid: string|{width:number,height:number,data:Uint16Array}, opts: {w:number,d:number,base?:number,maxZ?:number,pitch?:number,invert?:boolean,range?:number[],origin?:"center"|"corner"}) => Solid} heightfield depth-map relief solid; nameOrGrid is a name declared in the part's `images` field or an inline grid
|
|
156
159
|
* @property {(solids:Solid[]) => Solid} union
|
|
@@ -272,6 +272,30 @@ export const KERNEL_OP_SPECS = {
|
|
|
272
272
|
boredCylinder: { toArgs: passThrough("boredCylinder", ["od", "h", "bore"], ["od", "h", "bore"]) },
|
|
273
273
|
helixSweptTube: { toArgs: passThrough("helixSweptTube",
|
|
274
274
|
["pathR", "profileR", "pitch", "turns", "z0", "lefthand"], ["pathR", "profileR", "pitch", "turns"]) },
|
|
275
|
+
tappedBore: {
|
|
276
|
+
toArgs: passThrough("tappedBore",
|
|
277
|
+
["d", "pitch", "turns", "depth", "crest", "lefthand", "rootSink", "overshoot"], ["d", "pitch", "turns"]),
|
|
278
|
+
check: (o) => {
|
|
279
|
+
if (!(o.d > 0)) throw new Error("tappedBore: d must be > 0");
|
|
280
|
+
if (!(o.pitch > 0)) throw new Error("tappedBore: pitch must be > 0");
|
|
281
|
+
if (!(o.turns > 0)) throw new Error("tappedBore: turns must be > 0");
|
|
282
|
+
if (o.depth != null && !(o.depth > 0)) throw new Error("tappedBore: depth must be > 0");
|
|
283
|
+
if (o.crest != null && !(o.crest > 0)) throw new Error("tappedBore: crest must be > 0");
|
|
284
|
+
// The sink is what keeps the bore and the thread root off the same
|
|
285
|
+
// cylinder; at zero they are tangent again and OCCT degenerates, which is
|
|
286
|
+
// the entire failure this op exists to prevent.
|
|
287
|
+
if (o.rootSink != null && !(o.rootSink > 0)) throw new Error("tappedBore: rootSink must be > 0");
|
|
288
|
+
if (o.rootSink != null && o.rootSink >= o.d / 2)
|
|
289
|
+
throw new Error("tappedBore: rootSink must be smaller than the bore radius");
|
|
290
|
+
if (o.overshoot != null && !(o.overshoot > 0)) throw new Error("tappedBore: overshoot must be > 0");
|
|
291
|
+
// The thread is always pitch*turns long; a shorter bore would leave it
|
|
292
|
+
// poking out past the bore's far end — a tap deeper than its own hole,
|
|
293
|
+
// and the overhang guarantee above quietly broken at that end.
|
|
294
|
+
if (o.depth != null && o.depth < o.pitch * o.turns)
|
|
295
|
+
throw new Error(
|
|
296
|
+
`tappedBore: depth (${o.depth}) must cover the thread (pitch*turns = ${o.pitch * o.turns}) — lower turns to Math.floor(depth / pitch), or raise depth`);
|
|
297
|
+
},
|
|
298
|
+
},
|
|
275
299
|
screwSweep: {
|
|
276
300
|
toArgs: passThrough("screwSweep", ["profile", "pitch", "turns", "lefthand"], ["profile", "pitch", "turns"]),
|
|
277
301
|
check: (o) => {
|
package/types/kernel.d.ts
CHANGED
|
@@ -381,6 +381,32 @@ export interface ScrewSweepOptions {
|
|
|
381
381
|
lefthand?: boolean;
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
/** `k.tappedBore` — a tapped (internally threaded) hole as ONE cut tool: the
|
|
385
|
+
* plain bore and its thread, fused, with the thread's root sunk inside the bore
|
|
386
|
+
* so the two never share a face. Hand-assembling the pair puts the bore wall and
|
|
387
|
+
* the thread root on exactly the same cylinder, which OCCT's boolean cannot
|
|
388
|
+
* resolve — measured at fifteen minutes without finishing, against ~10 s here. */
|
|
389
|
+
export interface TappedBoreOptions {
|
|
390
|
+
/** Bore (minor/root) diameter, mm — the hole a tap would cut into. */
|
|
391
|
+
d: number;
|
|
392
|
+
/** Axial rise per turn, mm. */
|
|
393
|
+
pitch: number;
|
|
394
|
+
/** Number of thread turns; the thread is `pitch * turns` long. */
|
|
395
|
+
turns: number;
|
|
396
|
+
/** Plain-bore length, mm. Defaults to the thread's own length. */
|
|
397
|
+
depth?: number;
|
|
398
|
+
/** Radial thread height (major radius minus root), mm. Defaults to `0.15 * pitch`. */
|
|
399
|
+
crest?: number;
|
|
400
|
+
lefthand?: boolean;
|
|
401
|
+
/** How far the thread's root sits INSIDE the bore, mm (default 0.2). Free
|
|
402
|
+
* rather than a compromise: the bore already removes that material, so the
|
|
403
|
+
* union is unchanged — it only stops the two tools being tangent. */
|
|
404
|
+
rootSink?: number;
|
|
405
|
+
/** How far the bore overhangs the thread at each end, mm (default 0.2).
|
|
406
|
+
* Flush ends are coincident faces, the same failure one layer down. */
|
|
407
|
+
overshoot?: number;
|
|
408
|
+
}
|
|
409
|
+
|
|
384
410
|
/** One `k.loftSmooth` control section. Point arrays may tag true corners with
|
|
385
411
|
* `sharp`; curve contours and Shape2D outlines carry corners implicitly. */
|
|
386
412
|
export interface LoftSmoothSection {
|
|
@@ -536,6 +562,8 @@ export interface GeometryKernel {
|
|
|
536
562
|
helixSweptTube(o: HelixSweptTubeOptions): Solid;
|
|
537
563
|
/** Sweep an axial lathe profile by screw motion — threads. */
|
|
538
564
|
screwSweep(o: ScrewSweepOptions): Solid;
|
|
565
|
+
/** A tapped hole as one cut tool — bore plus thread, never tangent. */
|
|
566
|
+
tappedBore(o: TappedBoreOptions): Solid;
|
|
539
567
|
/** Spline-interpolated loft of sparse control sections. */
|
|
540
568
|
loftSmooth(o: LoftSmoothOptions): Solid;
|
|
541
569
|
/** Rim round-overs via one lathe revolve; curve-exact in STEP. */
|