partforge 0.85.3 → 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/package.json +1 -1
- package/src/parts/propeller.js +122 -46
package/package.json
CHANGED
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
|
},
|