partforge 0.112.0 → 0.113.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
|
@@ -436,6 +436,7 @@ offsetPolygon(outline, -wall, { corners: "sharp" }); // inse
|
|
|
436
436
|
const tab = pathProfile([0, -w / 2])
|
|
437
437
|
.lineTo([len, -w / 2])
|
|
438
438
|
.arcTo([len, w / 2], [len + w / 2, 0]) // tip, via the apex
|
|
439
|
+
// same arc: .arcTo([len, w / 2], { r: w / 2 }) — radius form, no via to compute
|
|
439
440
|
.lineTo([0, w / 2])
|
|
440
441
|
.close();
|
|
441
442
|
k.extrude({ profile: tab, h: 3 });
|
|
@@ -445,6 +446,7 @@ const lip = pathProfile([0, 0])
|
|
|
445
446
|
.lineTo([20, 0]).lineTo([20, 8])
|
|
446
447
|
.cubicTo([0, 8], [14, 16], [6, 16]) // curved top edge
|
|
447
448
|
.close();
|
|
449
|
+
k.extrude({ profile: lip, h: 3 });
|
|
448
450
|
|
|
449
451
|
// Rounded enclosure: soft vertical edges, a softer lid, a flat base.
|
|
450
452
|
const shell = k.roundedBox({ size: [60, 40, 22], round: { side: 4, top: 2, bottom: 0 } });
|
|
@@ -470,7 +472,7 @@ dumbbell past its waist) **throws** a greppable error rather than returning dege
|
|
|
470
472
|
geometry. Being pure, it works in `derive()` as well as `build()` — the natural home for
|
|
471
473
|
clearance math.
|
|
472
474
|
`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.
|
|
473
|
-
`arcTo(to, via)` is a **three-point arc**: `via` is any point on the arc between the current point and `to` (its midpoint is the natural choice), and the sweep is whichever direction passes through it — so an arc's direction is a property of a point you can see, never of a sign. Build the symmetric half of a profile once and `mirrorProfile` it (see "Editing profiles") rather than writing the mirrored arcs by hand. `loft` accepts these contours as rings (every ring with the same segment signature lofts curve-to-curve).
|
|
475
|
+
`arcTo(to, via)` is a **three-point arc**: `via` is any point on the arc between the current point and `to` (its midpoint is the natural choice), and the sweep is whichever direction passes through it — so an arc's direction is a property of a point you can see, never of a sign. `arcTo(to, { r, sweep?, large? })` is the **radius form** for when you know the radius, not a point on the arc: it computes `via` from the current point, `to`, and `r`, emitting the exact same `{to, via}` segment the three-point form does. `sweep` names the direction the arc itself is traversed (default `"ccw"`), so on a counter-clockwise outline `"ccw"` bulges OUTWARD (a convex bump), and inward on a clockwise hole; `"cw"` is the reverse. `large` (default `false`) picks the major arc over the minor one when both are possible. A radius shorter than half the distance between the current point and `to` throws rather than being silently scaled up (the way SVG's arc command does) — the smallest circle joining the two points is a semicircle at `r = d/2`. Build the symmetric half of a profile once and `mirrorProfile` it (see "Editing profiles") rather than writing the mirrored arcs by hand. `loft` accepts these contours as rings (every ring with the same segment signature lofts curve-to-curve).
|
|
474
476
|
**`pathProfile` or an authored vector file?** Reach for `pathProfile` (and the polygon helpers above) when the geometry is **computed from parameters** — a profile whose dimensions come from `p`/`d`, which a JSON file cannot see. Reach for an authored `partforge-vector` document (`k.vector2d`, see "Vector geometry" below) when the geometry is **drawn** — a logo, a faceplate outline, a decorative cutout, where each number means one thing and gets edited on its own. The two are freely composable: both produce ordinary 2-D geometry that the same booleans and editing ops accept.
|
|
475
477
|
**Import geometry helpers from `partforge/geometry`, never from `partforge`** — the main
|
|
476
478
|
entry pulls in the DOM viewer/controls, and your build functions run in a Web Worker
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -316,6 +316,12 @@ Variant literals under this entry: `offsetPolygon: delta must be a finite number
|
|
|
316
316
|
- **Cause:** A cubic segment is missing `c1` or `c2`, or a control point is not a finite `[x,y]` (e.g. `NaN`, wrong length).
|
|
317
317
|
- **Fix:** Provide both control points as finite `[x,y]`. A cubic Bézier needs two controls between the previous point and `to`.
|
|
318
318
|
|
|
319
|
+
## arcto-radius-too-short
|
|
320
|
+
|
|
321
|
+
- **Symptom:** `pathProfile: arcTo r=<r> is shorter than half the chord (<half-chord>) from (<x0>, <y0>) to (<x1>, <y1>) — the smallest arc that can join these points has r=<half-chord> (a semicircle)`
|
|
322
|
+
- **Cause:** `pathProfile().arcTo(to, { r, sweep?, large? })`'s `r` is smaller than half the distance between the current point and `to` — no circle of that radius passes through both points.
|
|
323
|
+
- **Fix:** Raise `r` to at least half the chord (the message states the exact minimum), or move the endpoint closer. Unlike SVG's arc command, partforge refuses rather than silently scaling `r` up to fit — the model should learn the number it wrote was wrong rather than have it quietly corrected.
|
|
324
|
+
|
|
319
325
|
## shape2d-simple-not-single-region
|
|
320
326
|
|
|
321
327
|
- **Symptom:** `Shape2D.simple: result has N regions, not 1 (use toRegions())`
|
package/package.json
CHANGED
|
@@ -157,8 +157,9 @@ export function filletPolygon(points, r, { segs = 8 } = {}) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// Fluent builder for a curve-native path contour { start, segments }. Segment kinds:
|
|
160
|
-
// lineTo → {to}, arcTo → {to,via} (three-point arc)
|
|
161
|
-
// close() returns the plain contour object
|
|
160
|
+
// lineTo → {to}, arcTo → {to,via} (three-point arc) or {to,via} computed from a radius
|
|
161
|
+
// spec, cubicTo → {to,c1,c2} (cubic Bézier). close() returns the plain contour object
|
|
162
|
+
// (feeds extrude/revolve/prism), not a Solid.
|
|
162
163
|
export function pathProfile(start) {
|
|
163
164
|
const fin2 = (p, what) => {
|
|
164
165
|
if (!Array.isArray(p) || p.length < 2 || !Number.isFinite(p[0]) || !Number.isFinite(p[1]))
|
|
@@ -166,12 +167,73 @@ export function pathProfile(start) {
|
|
|
166
167
|
return [p[0], p[1]];
|
|
167
168
|
};
|
|
168
169
|
const s = fin2(start, "start");
|
|
170
|
+
let cur = s;
|
|
169
171
|
const segments = [];
|
|
170
172
|
const api = {
|
|
171
|
-
lineTo(to) {
|
|
172
|
-
|
|
173
|
+
lineTo(to) {
|
|
174
|
+
const p = fin2(to, "lineTo point");
|
|
175
|
+
segments.push({ to: p });
|
|
176
|
+
cur = p;
|
|
177
|
+
return api;
|
|
178
|
+
},
|
|
179
|
+
// arcTo(to, via) is the three-point form: `via` is any point on the arc.
|
|
180
|
+
// arcTo(to, { r, sweep?, large? }) is the radius form: `via` is computed here
|
|
181
|
+
// from the current point, `to`, and the radius spec so the emitted segment is
|
|
182
|
+
// byte-for-byte what the three-point form emits (see docs/AUTHORING-PARTS.md).
|
|
183
|
+
arcTo(to, second) {
|
|
184
|
+
const p1 = fin2(to, "arcTo point");
|
|
185
|
+
if (Array.isArray(second)) {
|
|
186
|
+
const via = fin2(second, "arcTo via");
|
|
187
|
+
segments.push({ to: p1, via });
|
|
188
|
+
cur = p1;
|
|
189
|
+
return api;
|
|
190
|
+
}
|
|
191
|
+
if (second !== null && typeof second === "object") {
|
|
192
|
+
const ARC_SPEC_KEYS = ["r", "sweep", "large"];
|
|
193
|
+
const unknownKeys = Object.keys(second).filter((k) => !ARC_SPEC_KEYS.includes(k));
|
|
194
|
+
if (unknownKeys.length > 0)
|
|
195
|
+
throw new Error(
|
|
196
|
+
`pathProfile: arcTo arc spec has unknown ${unknownKeys.length > 1 ? "keys" : "key"} ${unknownKeys.map((k) => JSON.stringify(k)).join(", ")} — the keys are r, sweep, large`,
|
|
197
|
+
);
|
|
198
|
+
const { r, sweep = "ccw", large = false } = second;
|
|
199
|
+
// Cheap key/enum/boolean checks run BEFORE the numeric ones below, so a
|
|
200
|
+
// typo'd sweep/large is reported on its own rather than being masked by
|
|
201
|
+
// an unrelated radius complaint on the same call.
|
|
202
|
+
if (sweep !== "ccw" && sweep !== "cw")
|
|
203
|
+
throw new Error(`pathProfile: arcTo sweep must be "ccw" or "cw", got ${JSON.stringify(sweep)}`);
|
|
204
|
+
if (typeof large !== "boolean")
|
|
205
|
+
throw new Error("pathProfile: arcTo large must be a boolean");
|
|
206
|
+
const [x0, y0] = cur;
|
|
207
|
+
const [x1, y1] = p1;
|
|
208
|
+
const dx = x1 - x0, dy = y1 - y0;
|
|
209
|
+
const d = Math.hypot(dx, dy);
|
|
210
|
+
if (d < 1e-9)
|
|
211
|
+
throw new Error(`pathProfile: arcTo to (${x1}, ${y1}) coincides with the current point`);
|
|
212
|
+
if (!(r > 0) || !Number.isFinite(r))
|
|
213
|
+
throw new Error(`pathProfile: arcTo r must be > 0 and finite, got ${JSON.stringify(r)}`);
|
|
214
|
+
if (r < d / 2 - 1e-9)
|
|
215
|
+
throw new Error(
|
|
216
|
+
`pathProfile: arcTo r=${r} is shorter than half the chord (${(d / 2).toFixed(4)}) from (${x0}, ${y0}) to (${x1}, ${y1}) — the smallest arc that can join these points has r=${(d / 2).toFixed(4)} (a semicircle)`,
|
|
217
|
+
);
|
|
218
|
+
const rr = Math.max(r, d / 2); // absorb the 1e-9 tolerance so h is never NaN
|
|
219
|
+
const h = Math.sqrt(rr * rr - (d / 2) * (d / 2)); // centre's distance from the chord midpoint
|
|
220
|
+
const ux = dx / d, uy = dy / d; // unit chord direction
|
|
221
|
+
const nx = -uy, ny = ux; // unit LEFT normal of the direction of travel
|
|
222
|
+
const mx = (x0 + x1) / 2, my = (y0 + y1) / 2;
|
|
223
|
+
const side = sweep === "ccw" ? -1 : 1; // which side of the chord the arc bulges to
|
|
224
|
+
const sgn = large ? side : -side;
|
|
225
|
+
const cx = mx + nx * h * sgn, cy = my + ny * h * sgn;
|
|
226
|
+
const via = [cx + nx * rr * side, cy + ny * rr * side]; // the arc's midpoint
|
|
227
|
+
segments.push({ to: p1, via });
|
|
228
|
+
cur = p1;
|
|
229
|
+
return api;
|
|
230
|
+
}
|
|
231
|
+
throw new Error("pathProfile: arcTo needs a via [x,y] or an { r, sweep?, large? } arc spec");
|
|
232
|
+
},
|
|
173
233
|
cubicTo(to, c1, c2) {
|
|
174
|
-
|
|
234
|
+
const p = fin2(to, "cubicTo point");
|
|
235
|
+
segments.push({ to: p, c1: fin2(c1, "cubicTo c1"), c2: fin2(c2, "cubicTo c2") });
|
|
236
|
+
cur = p;
|
|
175
237
|
return api;
|
|
176
238
|
},
|
|
177
239
|
close() {
|
package/types/geometry.d.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface PathProfileBuilder {
|
|
|
86
86
|
lineTo(to: Point2): PathProfileBuilder;
|
|
87
87
|
/** A circular arc to `to` passing through `via`. */
|
|
88
88
|
arcTo(to: Point2, via: Point2): PathProfileBuilder;
|
|
89
|
+
/** A circular arc to `to` of radius `r`; `via` is computed from the current point. */
|
|
90
|
+
arcTo(to: Point2, arc: { r: number; sweep?: "ccw" | "cw"; large?: boolean }): PathProfileBuilder;
|
|
89
91
|
/** A cubic Bézier to `to` with control points `c1`/`c2`. */
|
|
90
92
|
cubicTo(to: Point2, c1: Point2, c2: Point2): PathProfileBuilder;
|
|
91
93
|
/** Close the contour and return it. Needs at least one segment. */
|