partforge 0.85.2 → 0.85.4
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/ERROR-PATTERNS.md
CHANGED
|
@@ -529,9 +529,9 @@ Variant literal for a curve-adjacent corner: `filletProfile: corner <i> at (<x>,
|
|
|
529
529
|
|
|
530
530
|
## loftsmooth-samples-out-of-range
|
|
531
531
|
|
|
532
|
-
- **Symptom:** `loftSmooth: samples must be 8…2048` —
|
|
533
|
-
- **Cause:** `samples` is clamped to 8…2048. The default
|
|
534
|
-
- **Fix:** pass a `samples` value in 8…2048, and thin any control section above ~2048 points
|
|
532
|
+
- **Symptom:** `loftSmooth: samples must be 8…2048` — an explicit `samples` was rejected.
|
|
533
|
+
- **Cause:** `samples` is clamped to 8…2048. The default `max(64, largest section)` caps itself at 2048, so omitting the option never trips this — even when a control section has more than 2048 points.
|
|
534
|
+
- **Fix:** pass a `samples` value in 8…2048, or omit it for the capped default — and thin any control section above ~2048 points: `loftSmooth` interpolates a smooth outline through sparse control points, so sections that dense defeat the sparse-sections design (pass the dense rings straight to `k.loft` instead). See the `loftSmooth` row in [KERNEL-CONTRACT.md](KERNEL-CONTRACT.md).
|
|
535
535
|
|
|
536
536
|
## duplicate-preset-name-throws
|
|
537
537
|
|
package/docs/KERNEL-CONTRACT.md
CHANGED
|
@@ -288,7 +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
|
-
| `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 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. |
|
|
291
|
+
| `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
292
|
| `union(solids[])` | Boolean union of one or more solids. |
|
|
293
293
|
| `text2d(string, {size, font?, align?, valign?, lineHeight?, tracking?, kerning?})` | Outline-font text → `Shape2D`. `size` = cap height (mm). `font` = declared name / inline bytes / default. Build-time; curve-exact on OCCT, faceted on Manifold. |
|
|
294
294
|
| `hull(inputs[])` | Convex hull of all inputs (each a `Shape2D`, a curve contour, or an `[[x,y],…]` point list) → a convex `Shape2D`. Backend-agnostic: a pure-JS monotone-chain hull over the inputs' sampled points (curved inputs tessellated at a fixed LOD), lifted via `shape2d` (see the parity note below). Throws on an empty input array or a degenerate (collinear/point-count < 3) hull. |
|
package/package.json
CHANGED
|
@@ -309,7 +309,8 @@ function reconcile(resolved, V) {
|
|
|
309
309
|
* — the B-rep path, where the backend's native smooth loft (`ruled: false`)
|
|
310
310
|
* does the skinning through exact wires and only the around-ring
|
|
311
311
|
* reconciliation is needed; incompatible with closed:true;
|
|
312
|
-
* samples — output vertex count around each ring (default max(64, largest
|
|
312
|
+
* samples — output vertex count around each ring (default max(64, largest
|
|
313
|
+
* section), capped at 2048);
|
|
313
314
|
* closed — periodic spine: the spline wraps from the last control section back
|
|
314
315
|
* to the first (no duplicate ring at the wrap point), needs ≥ 3 sections.
|
|
315
316
|
* @returns {Array<{polygon: {start, segments}, z: number}>}
|
|
@@ -320,10 +321,11 @@ export function smoothLoftRings(sections, { stations, samples, closed = false }
|
|
|
320
321
|
if (closed && stations === "controls")
|
|
321
322
|
throw new Error('loftSmooth: closed:true cannot combine with stations:"controls"');
|
|
322
323
|
if (closed && n < 3) throw new Error("loftSmooth: closed:true needs at least 3 control sections");
|
|
323
|
-
// The
|
|
324
|
+
// The defaults cap at the clamp ceilings: at 129+ sections the raw 8-per-span
|
|
325
|
+
// stations (and at a 2049+-point section the raw largest-section samples)
|
|
324
326
|
// would trip the range check on an option the caller never passed.
|
|
325
327
|
const S = stations ?? Math.min(closed ? n * 8 : (n - 1) * 8 + 1, 1024);
|
|
326
|
-
const V = samples ?? Math.max(64, ...resolved.map((r) => r.pts2d.length));
|
|
328
|
+
const V = samples ?? Math.min(Math.max(64, ...resolved.map((r) => r.pts2d.length)), 2048);
|
|
327
329
|
if (stations !== "controls" && !(Number.isFinite(S) && S >= 2 && S <= 1024))
|
|
328
330
|
throw new Error('loftSmooth: stations must be 2…1024 (or "controls")');
|
|
329
331
|
if (!(Number.isFinite(V) && V >= 8 && V <= 2048)) throw new Error("loftSmooth: samples must be 8…2048");
|
package/src/parts/propeller.js
CHANGED
|
@@ -1,15 +1,56 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// THE reference part for organic lofted geometry (docs/AUTHORING-PARTS.md "Smooth
|
|
2
|
+
// organic lofts"): a boat propeller — bored hub + N airfoil blades, each blade one
|
|
3
|
+
// `k.loftSmooth` of 5 sparse control sections. If you are building a boat propeller,
|
|
4
|
+
// a fan, an impeller, or an airplane prop, start from this file: the blade recipe
|
|
5
|
+
// (airfoil ring → chord/twist schedule up the span → smooth loft → radial placement)
|
|
6
|
+
// is the whole pattern, and everything else is ordinary hub plumbing.
|
|
7
|
+
//
|
|
8
|
+
// Vocabulary → code:
|
|
9
|
+
// span — blade length root→tip (the loft's z axis, before placement)
|
|
10
|
+
// chord — airfoil width at a station (`rootChord`→`tipChord` × CHORD_MUL)
|
|
11
|
+
// pitch — per-section `rotate` (deg): angle between chord and rotation plane
|
|
12
|
+
// washout — the root→tip pitch DROP (`twistRoot` > `twistTip`), standard on
|
|
13
|
+
// real props/fans so the tip doesn't stall/over-bite
|
|
14
|
+
// camber — curvature of the airfoil midline (% chord): lift/thrust asymmetry
|
|
15
|
+
// handedness — `lefthand` mirrors every section and negates pitch: a true
|
|
16
|
+
// mirror-image prop for counter-rotating pairs or reversed shafts
|
|
17
|
+
//
|
|
18
|
+
// The blade-frame trick: each blade is lofted SPAN-UP (+Z), so its sections are
|
|
19
|
+
// plain 2-D rings and `rotate` is exactly the pitch angle; `rotateY(90)` then lays
|
|
20
|
+
// it radial along +X, where the chord sits in the axis/rotation-plane frame. Clone
|
|
21
|
+
// and `rotateZ` for the other blades; never author sections in the placed frame.
|
|
22
|
+
//
|
|
23
|
+
// Adapting this part (change parameters, not plumbing):
|
|
24
|
+
// boat propeller — the defaults: 3 blades, high pitch (62°→30°), thick sections,
|
|
25
|
+
// generous camber, stubby hub. Wide-chord "kaplan" styles: raise CHORD_MUL mid.
|
|
26
|
+
// fan / desk fan — more blades (4–6+), FLAT pitch (`twistRoot` ~35°, `twistTip`
|
|
27
|
+
// ~20°), thin sections (`thickness` 5–8%), modest camber (2–4%), longer span
|
|
28
|
+
// relative to hub. Reversible fans want `camber: 0` (symmetric sections).
|
|
29
|
+
// airplane prop — 2 blades, long span, small chord, washout similar to defaults.
|
|
30
|
+
// counter-rotating pair — build twice, second with `lefthand: 1`.
|
|
31
|
+
// Leave alone: the loft/`sharp` plumbing, the blade frame, the CCW/TE-at-vertex-0
|
|
32
|
+
// contracts of `airfoil()` below.
|
|
33
|
+
//
|
|
34
|
+
// The "Surface" section is didactic, not part of the propeller: untick **Smooth**
|
|
35
|
+
// to see the raw `k.loft` of the same sparse sections (the chunky failure mode
|
|
36
|
+
// `loftSmooth` exists to fix). Contract: docs/KERNEL-CONTRACT.md `loftSmooth` row.
|
|
37
|
+
// Specs: docs/superpowers/specs/2026-08-24-loft-smooth-design.md,
|
|
38
|
+
// docs/superpowers/specs/2026-08-25-loft-smooth-v2-design.md
|
|
7
39
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
40
|
+
/**
|
|
41
|
+
* NACA-4-ish airfoil section as a closed CCW point ring.
|
|
42
|
+
* Contract (relied on by `bladeSections` and the `sharp` tag): the ring starts at
|
|
43
|
+
* the TRAILING EDGE (vertex 0), runs TE→LE along the upper surface, LE→TE along
|
|
44
|
+
* the lower, stays CCW, and is centered near the quarter chord so per-section
|
|
45
|
+
* `rotate` pitches about a sensible axis. `n` points per surface; cosine spacing
|
|
46
|
+
* clusters points at the leading edge — exactly the uneven spacing loftSmooth's
|
|
47
|
+
* centripetal resampler is built for, so ~12 points per surface is plenty.
|
|
48
|
+
* @param {number} chord section width, mm
|
|
49
|
+
* @param {number} thickPct max thickness, % of chord
|
|
50
|
+
* @param {number} camberPct midline camber, % of chord (0 = symmetric)
|
|
51
|
+
* @param {number} n points per surface (ring has 2n vertices)
|
|
52
|
+
*/
|
|
53
|
+
export const airfoil = (chord, thickPct, camberPct, n) => {
|
|
13
54
|
const t = thickPct / 100, m = camberPct / 100, p = 0.4;
|
|
14
55
|
const yt = (x) => 5 * t * (0.2969 * Math.sqrt(x) - 0.126 * x - 0.3516 * x * x + 0.2843 * x ** 3 - 0.1036 * x ** 4);
|
|
15
56
|
const yc = (x) => (x < p ? (m / (p * p)) * (2 * p * x - x * x) : (m / ((1 - p) ** 2)) * (1 - 2 * p + 2 * p * x - x * x));
|
|
@@ -24,31 +65,52 @@ const airfoil = (chord, thickPct, camberPct, n) => {
|
|
|
24
65
|
return pts.map(([x, y]) => [(x - 0.3) * chord, y * chord]);
|
|
25
66
|
};
|
|
26
67
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
68
|
+
// Mirror a section ring across its chord (y → −y) for a lefthand blade, keeping
|
|
69
|
+
// both `airfoil()` contracts intact: re-reversing the order restores CCW winding,
|
|
70
|
+
// and pinning the first point keeps the trailing edge at vertex 0 for `sharp`.
|
|
71
|
+
const mirrorRing = (pts) => {
|
|
72
|
+
const m = pts.map(([x, y]) => [x, -y]);
|
|
73
|
+
return [m[0], ...m.slice(1).reverse()];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// The blade's spanwise schedules. SPAN_T places the 5 control stations along the
|
|
77
|
+
// span (clustered toward the tip, where shape changes fastest); CHORD_MUL shapes
|
|
78
|
+
// the outline on top of the root→tip chord taper — widest mid-span, closing to a
|
|
79
|
+
// rounded tip. This is where a different blade silhouette lives.
|
|
29
80
|
const SPAN_T = [0, 0.3, 0.6, 0.85, 1];
|
|
30
81
|
const CHORD_MUL = [1, 1.12, 1.0, 0.72, 0.28];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The 5 sparse control sections for one blade, span-up: `{polygon, z, rotate,
|
|
85
|
+
* sharp?}` ring specs ready for `k.loftSmooth({sections})`. Chord tapers
|
|
86
|
+
* root→tip (× CHORD_MUL), thickness thins 45% toward the tip, pitch interpolates
|
|
87
|
+
* `twistRoot`→`twistTip` (washout). `lefthand` mirrors each ring and negates
|
|
88
|
+
* pitch — together exactly a mirror of the whole blade.
|
|
89
|
+
*/
|
|
90
|
+
export const bladeSections = (p) =>
|
|
91
|
+
SPAN_T.map((t, i) => {
|
|
92
|
+
const ring = airfoil(
|
|
34
93
|
(p.rootChord + (p.tipChord - p.rootChord) * t) * CHORD_MUL[i],
|
|
35
94
|
p.thickness * (1 - 0.45 * t), // blades thin toward the tip
|
|
36
95
|
p.camber,
|
|
37
96
|
p.sectionPts,
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
polygon: p.lefthand ? mirrorRing(ring) : ring,
|
|
100
|
+
// The trailing edge is the ring's two end vertices (upper TE vertex 0, lower
|
|
101
|
+
// TE the last) — this NACA closure (coefficient -0.1036) already brings them
|
|
102
|
+
// together at the same point, so the "gap" between them is a genuine zero-
|
|
103
|
+
// length edge, not a blunt base. Tagging vertex 0 as the single corner keeps
|
|
104
|
+
// that meeting point a crease instead of letting the CR spline round it off;
|
|
105
|
+
// a *second* tag at the last vertex would mark a zero-length arc between two
|
|
106
|
+
// coincident corners, which — after the per-section pitch `rotate` below
|
|
107
|
+
// collapses their sub-epsilon separation to bit-identical floats — the
|
|
108
|
+
// resampler rejects as a zero-perimeter section.
|
|
109
|
+
...(p.sharpTE && p.smooth ? { sharp: [0] } : {}),
|
|
110
|
+
z: p.span * t,
|
|
111
|
+
rotate: (p.lefthand ? -1 : 1) * (p.twistRoot + (p.twistTip - p.twistRoot) * t),
|
|
112
|
+
};
|
|
113
|
+
});
|
|
52
114
|
|
|
53
115
|
export default {
|
|
54
116
|
meta: { title: "Propeller", units: "mm", background: 0x15181d },
|
|
@@ -56,22 +118,32 @@ export default {
|
|
|
56
118
|
{
|
|
57
119
|
id: "prop",
|
|
58
120
|
title: "Propeller",
|
|
59
|
-
description: "A boat propeller: bored hub + airfoil blades
|
|
121
|
+
description: "A boat propeller: bored hub + airfoil blades, every blade surface a `loftSmooth` of 5 sparse control sections. Also the starting point for fans and airplane props — see the file header's \"Adapting this part\".",
|
|
60
122
|
controls: [
|
|
61
|
-
{ key: "blades", label: "Blades", min: 2, max: 6, step: 1
|
|
62
|
-
|
|
63
|
-
{ key: "
|
|
64
|
-
|
|
123
|
+
{ key: "blades", label: "Blades", min: 2, max: 6, step: 1,
|
|
124
|
+
description: "Boat props: 3–4. Fans: 4–6. Airplane props: 2." },
|
|
125
|
+
{ key: "span", label: "Blade span", unit: "mm", min: 30, max: 120, step: 1,
|
|
126
|
+
description: "Root-to-tip blade length. Overall diameter ≈ hub + 2×span." },
|
|
127
|
+
{ key: "rootChord", label: "Root chord", unit: "mm", min: 10, max: 50, step: 1,
|
|
128
|
+
description: "Airfoil width where the blade meets the hub." },
|
|
129
|
+
{ key: "tipChord", label: "Tip chord", unit: "mm", min: 6, max: 40, step: 1,
|
|
130
|
+
description: "Airfoil width at the tip, before the outline's own taper closes it." },
|
|
65
131
|
{ key: "twistRoot", label: "Root pitch", unit: "°", min: 0, max: 80, step: 1,
|
|
66
|
-
description: "Blade angle at the root
|
|
67
|
-
{ key: "twistTip", label: "Tip pitch", unit: "°", min: 0, max: 80, step: 1
|
|
132
|
+
description: "Blade angle at the root; 0° puts the chord in the rotation plane. Boat props run steep (50–70°); fans flat (25–40°)." },
|
|
133
|
+
{ key: "twistTip", label: "Tip pitch", unit: "°", min: 0, max: 80, step: 1,
|
|
134
|
+
description: "Blade angle at the tip. Keep it below root pitch (washout) so the tip doesn't over-bite." },
|
|
68
135
|
{ key: "thickness", label: "Thickness", unit: "%", min: 4, max: 25, step: 1,
|
|
69
|
-
description: "Airfoil thickness as % of chord, at the root." },
|
|
70
|
-
{ key: "camber", label: "Camber", unit: "%", min: 0, max: 12, step: 1
|
|
136
|
+
description: "Airfoil thickness as % of chord, at the root (thins 45% toward the tip). Boat props 10–15%; fans 5–8%." },
|
|
137
|
+
{ key: "camber", label: "Camber", unit: "%", min: 0, max: 12, step: 1,
|
|
138
|
+
description: "Midline curvature as % of chord — the thrust asymmetry. 0 = symmetric section (reversible fans)." },
|
|
139
|
+
{ key: "lefthand", type: "checkbox", label: "Left-hand rotation",
|
|
140
|
+
description: "Mirror-image blades for a counter-rotating pair or a reversed shaft. Every section mirrors and pitch negates — a true mirror of the whole propeller." },
|
|
71
141
|
{ type: "group", title: "Hub", collapsed: "auto", controls: [
|
|
72
|
-
{ key: "hubD", label: "Hub diameter", unit: "mm", min: 14, max: 60, step: 1
|
|
142
|
+
{ key: "hubD", label: "Hub diameter", unit: "mm", min: 14, max: 60, step: 1,
|
|
143
|
+
description: "Blade roots sink to 62% of hub radius, so the union always has generous overlap." },
|
|
73
144
|
{ key: "hubH", label: "Hub length", unit: "mm", min: 10, max: 60, step: 1 },
|
|
74
|
-
{ key: "boreD", label: "Shaft bore", unit: "mm", min: 2, max: 20, step: 0.5
|
|
145
|
+
{ key: "boreD", label: "Shaft bore", unit: "mm", min: 2, max: 20, step: 0.5,
|
|
146
|
+
description: "Through-hole for the shaft — the part's one expected hole (see `verify`)." },
|
|
75
147
|
] },
|
|
76
148
|
],
|
|
77
149
|
},
|
|
@@ -81,11 +153,13 @@ export default {
|
|
|
81
153
|
description: "**Smooth** interpolates the 5 sparse control sections with `loftSmooth`; off shows the raw `k.loft` of the same sections. **Sharp trailing edge** tags the TE vertex as a crease instead of letting the spline smear it. **Stations/Samples** are the densifier resolution; **Section points** is how sparse the control sections are.",
|
|
82
154
|
controls: [
|
|
83
155
|
{ key: "smooth", type: "checkbox", label: "Smooth (loftSmooth)",
|
|
84
|
-
description: "A/B toggle: spline-densified vs raw loft of identical control sections." },
|
|
156
|
+
description: "A/B toggle: spline-densified vs raw loft of identical control sections. Leave ON in real parts — the raw loft is the chunky failure mode." },
|
|
85
157
|
{ key: "sharpTE", type: "checkbox", label: "Sharp trailing edge", when: { smooth: 1 },
|
|
86
158
|
description: "Tags the trailing-edge vertex as a true corner — the spline interpolates it with a crease instead of smearing it round." },
|
|
87
|
-
{ key: "stations", label: "Stations", min: 5, max: 128, step: 1, when: { smooth: 1 }
|
|
88
|
-
|
|
159
|
+
{ key: "stations", label: "Stations", min: 5, max: 128, step: 1, when: { smooth: 1 },
|
|
160
|
+
description: "Interpolated rings along the span. The default (48) is visually converged; more costs build time." },
|
|
161
|
+
{ key: "samples", label: "Samples / ring", min: 16, max: 512, step: 4, when: { smooth: 1 },
|
|
162
|
+
description: "Spline-fit resolution around each ring. STEP file size scales with this; volume converges by ~48." },
|
|
89
163
|
{ key: "sectionPts", label: "Section points", min: 6, max: 40, step: 1,
|
|
90
164
|
description: "Points per airfoil *surface* in each control section — the sparse input both paths share." },
|
|
91
165
|
],
|
|
@@ -93,7 +167,7 @@ export default {
|
|
|
93
167
|
],
|
|
94
168
|
defaults: {
|
|
95
169
|
blades: 3, span: 70, rootChord: 26, tipChord: 16, twistRoot: 62, twistTip: 30,
|
|
96
|
-
thickness: 12, camber: 6, hubD: 30, hubH: 26, boreD: 8,
|
|
170
|
+
thickness: 12, camber: 6, lefthand: 0, hubD: 30, hubH: 26, boreD: 8,
|
|
97
171
|
smooth: 1, sharpTE: 1, stations: 48, samples: 128, sectionPts: 12,
|
|
98
172
|
},
|
|
99
173
|
parts: {
|
|
@@ -120,7 +194,9 @@ export default {
|
|
|
120
194
|
views: { propeller: { label: "Propeller" } },
|
|
121
195
|
verify: {
|
|
122
196
|
expect: {
|
|
123
|
-
//
|
|
197
|
+
// holes: 1 — exactly the shaft bore; a second hole means a blade/hub union
|
|
198
|
+
// gap. bbox bounds catch a runaway parameter. overlaps: 0 — the union must
|
|
199
|
+
// leave one watertight body, no interpenetrating leftovers.
|
|
124
200
|
propeller: { holes: 1, bbox: "<=[300,300,300]" },
|
|
125
201
|
_view: { overlaps: 0 },
|
|
126
202
|
},
|
package/types/kernel.d.ts
CHANGED
|
@@ -401,7 +401,7 @@ export interface LoftSmoothOptions {
|
|
|
401
401
|
sections: LoftSmoothSection[];
|
|
402
402
|
/** Output ring count along the spine (default 8 per span + 1; closed: 8 per section; capped at 1024). */
|
|
403
403
|
stations?: number;
|
|
404
|
-
/** Output vertex count around each ring (default max(64, largest section)). */
|
|
404
|
+
/** Output vertex count around each ring (default max(64, largest section), capped at 2048). */
|
|
405
405
|
samples?: number;
|
|
406
406
|
shading?: "smooth" | "faceted";
|
|
407
407
|
/** Capless loop — Manifold only, ≥3 sections. */
|