partforge 0.16.0 → 0.17.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
CHANGED
|
@@ -151,11 +151,18 @@ k.prism({ points: roundedProfile(bracketOutline, 3), h: 4 }); // true CIRCLE co
|
|
|
151
151
|
// print clearance on an arbitrary cut profile, or an inset wall
|
|
152
152
|
k.extrude({ profile: offsetPolygon(slotPolygon(20, 3), 0.2), h: 10 }); // slot cut 0.2 mm looser all around
|
|
153
153
|
offsetPolygon(outline, -wall, { corners: "sharp" }); // inset a wall (see planter.js)
|
|
154
|
+
|
|
155
|
+
// A tab with one free-form curved side (exact on STEP, faceted at mesh LOD):
|
|
156
|
+
const tab = pathProfile([0, 0])
|
|
157
|
+
.lineTo([20, 0]).lineTo([20, 8])
|
|
158
|
+
.cubicTo([0, 8], [14, 16], [6, 16]) // curved top edge
|
|
159
|
+
.close();
|
|
160
|
+
k.extrude({ profile: tab, h: 3 });
|
|
154
161
|
```
|
|
155
162
|
|
|
156
163
|
2-D polygon helpers for `prism`/`extrude`/`loft`: `import { piePolygon, hexPolygon,
|
|
157
164
|
regularPolygon, roundedRectPolygon, starPolygon, slotPolygon, circleProfile, filletPolygon,
|
|
158
|
-
roundedProfile, offsetPolygon } from "partforge/geometry"`. `filletPolygon(points, r, { segs? })` rounds
|
|
165
|
+
roundedProfile, offsetPolygon, pathProfile } from "partforge/geometry"`. `filletPolygon(points, r, { segs? })` rounds
|
|
159
166
|
every corner of a CCW polygon (per-corner radius clamped so neighbouring arcs never overlap)
|
|
160
167
|
and returns points usable by `prism`/`extrude`/`loft` on both backends — but it **bakes each
|
|
161
168
|
corner into line facets**, so STEP corners are faceted. `roundedProfile(points, r | r[])`
|
|
@@ -173,6 +180,7 @@ an offset whose true result would collapse or split into multiple contours (e.g.
|
|
|
173
180
|
dumbbell past its waist) **throws** a greppable error rather than returning degenerate
|
|
174
181
|
geometry. Being pure, it works in `derive()` as well as `build()` — the natural home for
|
|
175
182
|
clearance math.
|
|
183
|
+
`pathProfile(start)` is a fluent builder for a curve-native path contour (`lineTo` / `arcTo` / `cubicTo` / `close`); cubic segments become exact B-rep spline edges on the OCCT/STEP backend and facet at the mesh LOD on Manifold — the same exact-vs-faceted split as `roundedProfile` arcs.
|
|
176
184
|
**Import geometry helpers from `partforge/geometry`, never from `partforge`** — the main
|
|
177
185
|
entry pulls in the DOM viewer/controls, and your build functions run in a Web Worker
|
|
178
186
|
(importing the main entry there throws `document is not defined`).
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -221,6 +221,18 @@ Variant literals under this entry: `offsetPolygon: delta must be a finite number
|
|
|
221
221
|
- **Cause:** the true offset of this shape at this `|delta|` is not a single simple polygon (e.g. insetting a dumbbell past its waist would split it in two) — out of `offsetPolygon`'s envelope.
|
|
222
222
|
- **Fix:** reduce `|delta|`, or decompose the profile into separately-offset simple contours.
|
|
223
223
|
|
|
224
|
+
## cubic-segment-mixes-arc-and-cubic
|
|
225
|
+
|
|
226
|
+
- **Symptom:** `extrude: <role> segment cannot mix arc (via) and cubic (c1/c2)`
|
|
227
|
+
- **Cause:** A path-contour segment carries both `via` (three-point arc) and `c1`/`c2` (cubic Bézier). A segment is exactly one kind.
|
|
228
|
+
- **Fix:** Drop `via` for a cubic, or drop `c1`/`c2` for an arc. Use `pathProfile().arcTo(to, via)` or `.cubicTo(to, c1, c2)` to build segments.
|
|
229
|
+
|
|
230
|
+
## cubic-segment-missing-controls
|
|
231
|
+
|
|
232
|
+
- **Symptom:** `extrude: <role> cubic segment needs c1 and c2 as finite [x,y]`
|
|
233
|
+
- **Cause:** A cubic segment is missing `c1` or `c2`, or a control point is not a finite `[x,y]` (e.g. `NaN`, wrong length).
|
|
234
|
+
- **Fix:** Provide both control points as finite `[x,y]`. A cubic Bézier needs two controls between the previous point and `to`.
|
|
235
|
+
|
|
224
236
|
# Hardware library
|
|
225
237
|
|
|
226
238
|
Reserved for `hardware-*` patterns (issue #30). No entries yet.
|
package/package.json
CHANGED
|
@@ -89,8 +89,9 @@ export function createOcctKernel(replicad) {
|
|
|
89
89
|
// Draw a closed Drawing from a Contour: a legacy 2-D point list (all straight edges,
|
|
90
90
|
// the former polyDrawing) OR an ArcContour whose { to, via } segments become true
|
|
91
91
|
// OCCT arc edges via threePointsArcTo — so a rounded corner survives to STEP as a
|
|
92
|
-
// real CIRCLE B-rep entity, not a fan of LINEs.
|
|
93
|
-
//
|
|
92
|
+
// real CIRCLE B-rep entity, not a fan of LINEs. Cubic segments ({ to, c1, c2 })
|
|
93
|
+
// map to cubicBezierCurveTo for exact B-rep spline edges. close() joins the last
|
|
94
|
+
// point back to the start with a straight edge (mirrors the implied ArcContour closure).
|
|
94
95
|
const contourDrawing = (contour) => {
|
|
95
96
|
if (Array.isArray(contour)) {
|
|
96
97
|
let pen = draw(contour[0]);
|
|
@@ -98,7 +99,10 @@ export function createOcctKernel(replicad) {
|
|
|
98
99
|
return pen.close();
|
|
99
100
|
}
|
|
100
101
|
let pen = draw(contour.start);
|
|
101
|
-
for (const seg of contour.segments)
|
|
102
|
+
for (const seg of contour.segments)
|
|
103
|
+
pen = seg.c1 ? pen.cubicBezierCurveTo(seg.to, seg.c1, seg.c2)
|
|
104
|
+
: seg.via ? pen.threePointsArcTo(seg.to, seg.via)
|
|
105
|
+
: pen.lineTo(seg.to);
|
|
102
106
|
return pen.close();
|
|
103
107
|
};
|
|
104
108
|
|
|
@@ -156,6 +156,32 @@ export function filletPolygon(points, r, { segs = 8 } = {}) {
|
|
|
156
156
|
return out;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// Fluent builder for a curve-native path contour { start, segments }. Segment kinds:
|
|
160
|
+
// lineTo → {to}, arcTo → {to,via} (three-point arc), cubicTo → {to,c1,c2} (cubic Bézier).
|
|
161
|
+
// close() returns the plain contour object (feeds extrude/revolve/prism), not a Solid.
|
|
162
|
+
export function pathProfile(start) {
|
|
163
|
+
const fin2 = (p, what) => {
|
|
164
|
+
if (!Array.isArray(p) || p.length < 2 || !Number.isFinite(p[0]) || !Number.isFinite(p[1]))
|
|
165
|
+
throw new Error(`pathProfile: ${what} must be a finite [x,y]`);
|
|
166
|
+
return [p[0], p[1]];
|
|
167
|
+
};
|
|
168
|
+
const s = fin2(start, "start");
|
|
169
|
+
const segments = [];
|
|
170
|
+
const api = {
|
|
171
|
+
lineTo(to) { segments.push({ to: fin2(to, "lineTo point") }); return api; },
|
|
172
|
+
arcTo(to, via) { segments.push({ to: fin2(to, "arcTo point"), via: fin2(via, "arcTo via") }); return api; },
|
|
173
|
+
cubicTo(to, c1, c2) {
|
|
174
|
+
segments.push({ to: fin2(to, "cubicTo point"), c1: fin2(c1, "cubicTo c1"), c2: fin2(c2, "cubicTo c2") });
|
|
175
|
+
return api;
|
|
176
|
+
},
|
|
177
|
+
close() {
|
|
178
|
+
if (segments.length < 1) throw new Error("pathProfile: need ≥1 segment before close()");
|
|
179
|
+
return { start: [s[0], s[1]], segments: segments.slice() }; // snapshot — chaining after close() must not mutate the returned contour
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
return api;
|
|
183
|
+
}
|
|
184
|
+
|
|
159
185
|
// Arc-aware sibling of filletPolygon: rounds the corners of a CCW polygon with the SAME
|
|
160
186
|
// tangent/centre/sweep math (via cornerArc), but instead of tessellating each arc into
|
|
161
187
|
// line facets it emits a canonical ArcContour { start, segments:[{to}|{to,via}], arc:true }
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
// Backend-shared 2-D region normalization + tessellation for extrude()/prism(). A contour
|
|
2
|
-
// is EITHER a bare points array (legacy, all straight edges) OR a canonical
|
|
3
|
-
// { start:[x,y], segments:[{to}|{to,via}]
|
|
4
|
-
// roundedProfile)
|
|
5
|
-
// (bare array = outer only), preserving
|
|
6
|
-
// arcs into point rings for the Manifold
|
|
7
|
-
//
|
|
8
|
-
//
|
|
2
|
+
// is EITHER a bare points array (legacy, all straight edges) OR a canonical path contour
|
|
3
|
+
// { start:[x,y], segments:[{to}|{to,via}|{to,c1,c2}] } carrying true circular arcs ({to,via},
|
|
4
|
+
// from roundedProfile) and/or cubic Béziers ({to,c1,c2}, from pathProfile). normalizeProfile
|
|
5
|
+
// validates the polymorphic { outer, holes } envelope (bare array = outer only), preserving
|
|
6
|
+
// each contour's shape; tessellateProfile turns arcs/cubics into point rings for the Manifold
|
|
7
|
+
// (mesh) path at the mesh LOD. The OCCT path consumes the same contour directly (contourDrawing
|
|
8
|
+
// → threePointsArcTo / cubicBezierCurveTo) for true CIRCLE / B-spline B-rep edges. Legacy
|
|
9
|
+
// point-array contours take the exact former path byte-for-byte — no cache-busting.
|
|
9
10
|
|
|
10
11
|
// An ArcContour is a non-array object carrying arcs symbolically.
|
|
11
12
|
export function isArcContour(c) {
|
|
12
13
|
return !!c && typeof c === "object" && !Array.isArray(c) && (c.arc === true || Array.isArray(c.segments));
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
// Curves generalize arcs; the symbolic-form predicate is the same. Prefer this name.
|
|
17
|
+
export const isPathContour = isArcContour;
|
|
18
|
+
|
|
15
19
|
function validateContour(c, role) {
|
|
16
20
|
if (isArcContour(c)) {
|
|
17
21
|
if (!Array.isArray(c.start) || c.start.length < 2)
|
|
18
22
|
throw new Error(`extrude: ${role} arc contour needs a start [x,y]`);
|
|
19
23
|
if (!Array.isArray(c.segments) || c.segments.length < 1)
|
|
20
24
|
throw new Error(`extrude: ${role} arc contour needs ≥1 segment`);
|
|
25
|
+
for (const s of c.segments) {
|
|
26
|
+
const hasCubic = s.c1 != null || s.c2 != null;
|
|
27
|
+
if (hasCubic) {
|
|
28
|
+
if (s.via != null)
|
|
29
|
+
throw new Error(`extrude: ${role} segment cannot mix arc (via) and cubic (c1/c2)`);
|
|
30
|
+
const ok = (p) => Array.isArray(p) && p.length >= 2 && Number.isFinite(p[0]) && Number.isFinite(p[1]);
|
|
31
|
+
if (!ok(s.c1) || !ok(s.c2))
|
|
32
|
+
throw new Error(`extrude: ${role} cubic segment needs c1 and c2 as finite [x,y]`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
21
35
|
return;
|
|
22
36
|
}
|
|
23
37
|
if (!Array.isArray(c) || c.length < 3) throw new Error(`extrude: ${role} needs ≥3 points`);
|
|
@@ -73,15 +87,50 @@ export function sampleArc(p0, via, p1, segs) {
|
|
|
73
87
|
return out;
|
|
74
88
|
}
|
|
75
89
|
|
|
90
|
+
// Flatten the cubic Bézier (p0,c1,c2,p1) into points p1…pN — EXCLUDING the start
|
|
91
|
+
// p0 (the ring already holds it), last point pinned exactly to p1. Adaptive: split
|
|
92
|
+
// at t=½ (de Casteljau) until the control polygon's total unsigned turn is ≤ 2π/segs
|
|
93
|
+
// — the exact generalization of sampleArc's "a point every 2π/segs of sweep", so a
|
|
94
|
+
// cubic tracing a circular arc facets like the arc primitive at the same segs. Summing
|
|
95
|
+
// |turn| at BOTH interior control points also catches S-curves a pure endpoint-tangent
|
|
96
|
+
// test would miss. Depth cap guarantees termination. Pure in (args, segs).
|
|
97
|
+
export function sampleBezier(p0, c1, c2, p1, segs) {
|
|
98
|
+
const maxTurn = (2 * Math.PI) / Math.max(3, segs);
|
|
99
|
+
const out = [];
|
|
100
|
+
const mid = (a, b) => [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
101
|
+
const turn = (u, v) => {
|
|
102
|
+
const du = Math.hypot(u[0], u[1]), dv = Math.hypot(v[0], v[1]);
|
|
103
|
+
if (du < 1e-12 || dv < 1e-12) return 0;
|
|
104
|
+
let c = (u[0] * v[0] + u[1] * v[1]) / (du * dv);
|
|
105
|
+
if (c > 1) c = 1; else if (c < -1) c = -1;
|
|
106
|
+
return Math.acos(c);
|
|
107
|
+
};
|
|
108
|
+
const recurse = (a, b, c, d, depth) => {
|
|
109
|
+
const ab = [b[0] - a[0], b[1] - a[1]];
|
|
110
|
+
const bc = [c[0] - b[0], c[1] - b[1]];
|
|
111
|
+
const cd = [d[0] - c[0], d[1] - c[1]];
|
|
112
|
+
if (depth >= 12 || turn(ab, bc) + turn(bc, cd) <= maxTurn) { out.push([d[0], d[1]]); return; }
|
|
113
|
+
const p01 = mid(a, b), p12 = mid(b, c), p23 = mid(c, d);
|
|
114
|
+
const p012 = mid(p01, p12), p123 = mid(p12, p23), m = mid(p012, p123);
|
|
115
|
+
recurse(a, p01, p012, m, depth + 1);
|
|
116
|
+
recurse(m, p123, p23, d, depth + 1);
|
|
117
|
+
};
|
|
118
|
+
recurse(p0, c1, c2, p1, 0);
|
|
119
|
+
if (out.length === 0) out.push([p1[0], p1[1]]);
|
|
120
|
+
out[out.length - 1] = [p1[0], p1[1]]; // pin the exact endpoint
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
76
124
|
// Tessellate a single contour into a CCW point ring. A legacy array is returned unchanged
|
|
77
|
-
// (identical to the former path);
|
|
78
|
-
// pushing their `to
|
|
125
|
+
// (identical to the former path); a path contour is walked start→segment→segment, lines
|
|
126
|
+
// pushing their `to`, arcs and cubics pushing their sampled points (sampleArc/sampleBezier).
|
|
79
127
|
export function tessellateContour(contour, segs) {
|
|
80
128
|
if (Array.isArray(contour)) return contour;
|
|
81
129
|
const ring = [[contour.start[0], contour.start[1]]];
|
|
82
130
|
let prev = contour.start;
|
|
83
131
|
for (const seg of contour.segments) {
|
|
84
|
-
if (seg.
|
|
132
|
+
if (seg.c1) for (const p of sampleBezier(prev, seg.c1, seg.c2, seg.to, segs)) ring.push(p);
|
|
133
|
+
else if (seg.via) for (const p of sampleArc(prev, seg.via, seg.to, segs)) ring.push(p);
|
|
85
134
|
else ring.push([seg.to[0], seg.to[1]]);
|
|
86
135
|
prev = seg.to;
|
|
87
136
|
}
|