partforge 0.7.0 → 0.9.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/bin/cli.js +47 -14
- package/docs/AUTHORING-PARTS.md +118 -19
- package/docs/ERROR-PATTERNS.md +150 -0
- package/package.json +2 -1
- package/skills/partforge/SKILL.md +5 -0
- package/src/app-faceted-vase.js +10 -0
- package/src/faceted-vase-worker.js +3 -0
- package/src/framework/assembly.js +9 -3
- package/src/framework/geometry/helix-tube.js +10 -20
- package/src/framework/geometry/kernel-front.js +6 -0
- package/src/framework/geometry/kernel.js +5 -2
- package/src/framework/geometry/loft.js +79 -0
- package/src/framework/geometry/manifold-backend.js +22 -1
- package/src/framework/geometry/mesh-build.js +53 -0
- package/src/framework/geometry/occt-backend.js +70 -10
- package/src/framework/geometry/polygon.js +89 -0
- package/src/framework/geometry/profile.js +96 -0
- package/src/framework/geometry/sweep.js +151 -0
- package/src/parts/faceted-vase.js +75 -0
- package/src/testing/error-patterns.js +78 -0
- package/src/testing/measure.js +3 -1
- package/src/testing/verify.js +51 -17
package/bin/cli.js
CHANGED
|
@@ -14,10 +14,31 @@ import { measure } from "../src/testing/measure.js";
|
|
|
14
14
|
import { verify } from "../src/testing/verify.js";
|
|
15
15
|
import { renderViews } from "../src/testing/render.js";
|
|
16
16
|
import { createPickServer, requestPicks, formatPickResult } from "../src/framework/pick-request/server.js";
|
|
17
|
+
import { matchPattern } from "../src/testing/error-patterns.js";
|
|
17
18
|
|
|
18
19
|
const die = (msg) => { console.error(msg); process.exit(1); };
|
|
19
20
|
const USAGE = "usage: partforge <measure|render|pick-serve|pick> …";
|
|
20
21
|
|
|
22
|
+
// Crash contract (issue #27): with --json, a thrown error becomes structured
|
|
23
|
+
// stdout JSON; either way the message is matched against ERROR-PATTERNS.md and
|
|
24
|
+
// the pattern's fix is surfaced. Exit 1 always. NOTE on stdout purity: crash
|
|
25
|
+
// JSON is the only thing on stdout for errors thrown before any report printing
|
|
26
|
+
// (load/boot/measure). But verify() runs after printMeasure and can throw (an
|
|
27
|
+
// unknown metric in verify.expect, or a per-case build crash), so a throw after
|
|
28
|
+
// printing appends the JSON after the human lines — it is not pure. Consumers
|
|
29
|
+
// should prefer --out for robust machine parsing.
|
|
30
|
+
function crash(cmd, e, jsonMode) {
|
|
31
|
+
const message = e?.message || String(e);
|
|
32
|
+
const m = matchPattern(message);
|
|
33
|
+
if (jsonMode) {
|
|
34
|
+
console.log(JSON.stringify({ ok: false, error: { message, ...(m && { pattern: m.id, hint: m.fix }) } }, null, 2));
|
|
35
|
+
} else {
|
|
36
|
+
console.error(`${cmd} failed: ${message}`);
|
|
37
|
+
if (m) console.error(`pattern: ERROR-PATTERNS.md#${m.id} — ${m.fix}`);
|
|
38
|
+
}
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
const parse = (args, options, usage) => {
|
|
22
43
|
try {
|
|
23
44
|
return parseArgs({ args, options, strict: true, allowPositionals: true });
|
|
@@ -29,9 +50,9 @@ const parse = (args, options, usage) => {
|
|
|
29
50
|
async function loadPart(partPath, usage) {
|
|
30
51
|
if (!partPath) die(usage);
|
|
31
52
|
const mod = await import(pathToFileURL(resolve(process.cwd(), partPath)))
|
|
32
|
-
.catch((e) =>
|
|
53
|
+
.catch((e) => { throw new Error(`cannot load part "${partPath}": ${e.message}`); });
|
|
33
54
|
const part = mod.default;
|
|
34
|
-
if (!part?.parts || !part?.views)
|
|
55
|
+
if (!part?.parts || !part?.views) throw new Error(`"${partPath}" has no default-exported PartDefinition`);
|
|
35
56
|
return part;
|
|
36
57
|
}
|
|
37
58
|
|
|
@@ -46,27 +67,33 @@ const commands = {
|
|
|
46
67
|
json: { type: "boolean" },
|
|
47
68
|
out: { type: "string" },
|
|
48
69
|
}, usage);
|
|
49
|
-
const part = await loadPart(partPath, usage);
|
|
50
|
-
const kernel = await bootKernel(part);
|
|
51
70
|
try {
|
|
71
|
+
const part = await loadPart(partPath, usage);
|
|
72
|
+
const kernel = await bootKernel(part);
|
|
52
73
|
const report = measure(kernel, part, view);
|
|
53
74
|
printMeasure(report);
|
|
75
|
+
// Write --out right after measure succeeds, then re-write once verify has
|
|
76
|
+
// attached report.verify. If verify throws (unknown metric, per-case build
|
|
77
|
+
// crash) the file already holds the measure half (no `verify` key) — matching
|
|
78
|
+
// the doc's advice to prefer --out for exactly that non-pure-stdout case.
|
|
79
|
+
const writeOut = () => {
|
|
80
|
+
mkdirSync(dirname(resolve(flags.out)), { recursive: true });
|
|
81
|
+
writeFileSync(flags.out, JSON.stringify(report, null, 2));
|
|
82
|
+
};
|
|
83
|
+
if (flags.out) writeOut();
|
|
54
84
|
let vok = true;
|
|
55
85
|
if ((part.verify || flags.process) && !flags["no-verify"]) {
|
|
56
86
|
const v = verify(kernel, part, { process: flags.process, view });
|
|
57
87
|
printVerify(v);
|
|
58
88
|
report.verify = v;
|
|
59
89
|
vok = v.ok;
|
|
90
|
+
if (flags.out) writeOut();
|
|
60
91
|
}
|
|
61
|
-
if (flags.out) {
|
|
62
|
-
mkdirSync(dirname(resolve(flags.out)), { recursive: true });
|
|
63
|
-
writeFileSync(flags.out, JSON.stringify(report, null, 2));
|
|
64
|
-
console.log(`\nwrote ${flags.out}`);
|
|
65
|
-
}
|
|
92
|
+
if (flags.out) console.log(`\nwrote ${flags.out}`);
|
|
66
93
|
if (flags.json) console.log(JSON.stringify(report, null, 2));
|
|
67
94
|
process.exit(report.ok && vok ? 0 : 1);
|
|
68
95
|
} catch (e) {
|
|
69
|
-
|
|
96
|
+
crash("measure", e, !!flags.json);
|
|
70
97
|
}
|
|
71
98
|
},
|
|
72
99
|
|
|
@@ -76,15 +103,15 @@ const commands = {
|
|
|
76
103
|
views: { type: "string" },
|
|
77
104
|
out: { type: "string" },
|
|
78
105
|
}, usage);
|
|
79
|
-
const part = await loadPart(partPath, usage);
|
|
80
|
-
const kernel = await bootKernel(part);
|
|
81
106
|
try {
|
|
107
|
+
const part = await loadPart(partPath, usage);
|
|
108
|
+
const kernel = await bootKernel(part);
|
|
82
109
|
const views = flags.views ? flags.views.split(",") : undefined;
|
|
83
110
|
const files = await renderViews(kernel, part, view, { views, out: flags.out || "render" });
|
|
84
111
|
for (const f of files) console.log(`wrote ${f}`);
|
|
85
112
|
process.exit(0);
|
|
86
113
|
} catch (e) {
|
|
87
|
-
|
|
114
|
+
crash("render", e, false);
|
|
88
115
|
}
|
|
89
116
|
},
|
|
90
117
|
|
|
@@ -120,7 +147,9 @@ function printMeasure(r) {
|
|
|
120
147
|
}
|
|
121
148
|
const a = r.aggregate;
|
|
122
149
|
console.log(` ── view bbox ${a.bbox.map((n) => n.toFixed(1)).join("×")} vol ${(a.volume / 1000).toFixed(2)}cm³ tris ${a.triangleCount}`);
|
|
123
|
-
console.log(` overlaps: ${r.overlaps.length
|
|
150
|
+
console.log(` overlaps: ${r.overlaps.length
|
|
151
|
+
? r.overlaps.map((o) => `${o.a}×${o.b} (${o.volume.toFixed(1)}mm³ at [${o.location.map((n) => n.toFixed(1)).join(", ")}])`).join(", ")
|
|
152
|
+
: "none"}`);
|
|
124
153
|
}
|
|
125
154
|
|
|
126
155
|
function printVerify(v) {
|
|
@@ -130,6 +159,10 @@ function printVerify(v) {
|
|
|
130
159
|
for (const ch of c.checks) {
|
|
131
160
|
const icon = ch.status === "pass" ? "✓" : ch.status === "fail" ? "✗" : ch.status === "warn" ? "⚠" : "·";
|
|
132
161
|
console.log(` ${icon} ${ch.subpart ?? "_view"} ${ch.metric} ${ch.expr} (${ch.message})`);
|
|
162
|
+
if (ch.status === "fail" || ch.status === "warn") {
|
|
163
|
+
if (ch.location) console.log(` at [${ch.location.map((n) => n.toFixed(1)).join(", ")}]`);
|
|
164
|
+
if (ch.hint) console.log(` hint: ${ch.hint}${ch.pattern ? ` (ERROR-PATTERNS.md#${ch.pattern})` : ""}`);
|
|
165
|
+
}
|
|
133
166
|
}
|
|
134
167
|
}
|
|
135
168
|
const f = v.failures.length, w = v.warnings.length;
|
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -90,13 +90,55 @@ handles. The same code runs on **Manifold** (fast meshes — preview + STL + 3MF
|
|
|
90
90
|
|---|---|
|
|
91
91
|
| `k.cylinder(rBottom, rTop, h, { center? })` | cylinder/cone along +Z (frustum if radii differ) |
|
|
92
92
|
| `k.box(min, max)` | axis-aligned box from `[x,y,z]` min/max |
|
|
93
|
-
| `k.prism(points2D, h, { twist?, scaleTop? })` | extrude a 2-D polygon from z=0; optional `twist` (degrees over the height) and `scaleTop` (uniform top taper: 1 straight, <1 taper in, 0 → point/cone) |
|
|
93
|
+
| `k.prism(points2D, h, { twist?, scaleTop? })` | extrude a 2-D polygon (or an **arc profile** from `roundedProfile`) from z=0; optional `twist` (degrees over the height) and `scaleTop` (uniform top taper: 1 straight, <1 taper in, 0 → point/cone) |
|
|
94
|
+
| `k.extrude(profile, h, { twist?, scaleTop? })` | extrude a **polygon-with-holes** region from z=0 in one op — `profile` is `{ outer, holes? }` where each contour is a points array **or an arc profile** (`roundedProfile`, for true STEP fillets), or a bare points array / arc profile for outer-only; same `twist`/`scaleTop` as `prism` (both backends) |
|
|
95
|
+
| `k.loft(rings, { ruled?, closed? })` | stack polygon cross-sections into a solid — ruled walls between consecutive rings, capped ends (both backends; `closed:true` capless loops are Manifold-only). `ruled:false` (smooth C2 blend) is honoured only by OCCT/STEP export; the Manifold preview always shows faceted straight walls |
|
|
96
|
+
| `k.sweep(profile2D, path3D, { cornerRadius?, closed?, ruled?, smooth? })` | sweep a fixed 2-D profile along a 3-D polyline path — sharp mitered corners (or `cornerRadius` fillets), capped ends (both backends). `closed:true` capless loops and `smooth:true` (OCCT-native swept B-rep, STEP-exact / preview-faceted) are backend-specific, like loft's `closed`/`ruled:false`. `closed:true` loops must be **planar** — RMF frame-transport holonomy can seam-twist a non-planar closed loop where the last station rejoins the first, so only planar closed loops are supported/tested |
|
|
94
97
|
| `k.sphere(r)` | sphere centred at the origin |
|
|
95
98
|
| `k.revolve(points2D, { degrees })` | revolve a lathe profile `[[r,z],…]` (r ≥ 0) around the Z axis (full or partial) |
|
|
96
99
|
| `k.helixSweptTube({ pathR, profileR, pitch, turns, z0, lefthand })` | circle swept along a helix (e.g. a rope groove) |
|
|
97
100
|
| `k.union(solids[])` | boolean union |
|
|
98
101
|
|
|
99
|
-
|
|
102
|
+
**`loft` rings** — each ring is `{ polygon:[[x,y],…] | sides+radius, z, rotate?, scale? }`
|
|
103
|
+
(all rings must share the same vertex count; `rotate` is degrees about Z, `scale` is a
|
|
104
|
+
number or `[sx,sy]`). Author rings CCW and ordered by ascending `z` (the `regularPolygon`
|
|
105
|
+
/ `polygon.js` helpers are already CCW); loft self-corrects a fully-inverted result so
|
|
106
|
+
CW-wound or descending-z rings still export a valid outward solid. (Arc profiles from
|
|
107
|
+
`roundedProfile` are **not** accepted as loft rings yet — a ring must be a point array;
|
|
108
|
+
use `prism`/`extrude` for true-arc STEP export.) **`sweep`** takes the same CCW
|
|
109
|
+
`polygon.js` outline as its `profile2D` and a plain `[[x,y,z],…]` point list as its
|
|
110
|
+
`path3D`; the profile stays perpendicular to the path (a rotation-minimizing frame), with
|
|
111
|
+
sharp mitered corners by default or `cornerRadius` fillets. Worked snippets:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
// a square tube (extrude a region with a hole) — one op, no boolean cut
|
|
115
|
+
k.extrude({ outer: roundedRectPolygon(40, 30, 4), holes: [circleProfile(6)] }, 10);
|
|
116
|
+
|
|
117
|
+
// a tapered, twisting faceted vase wall (see src/parts/faceted-vase.js)
|
|
118
|
+
const rings = [];
|
|
119
|
+
for (let i = 0; i <= 24; i++) { const t = i / 24;
|
|
120
|
+
rings.push({ sides: 6, radius: 30 - 8 * t, z: 120 * t, rotate: 90 * t }); }
|
|
121
|
+
k.loft(rings); // ruled walls, capped ends
|
|
122
|
+
|
|
123
|
+
// a cable/hose: sweep a circle along a 3-D polyline, with rounded bends
|
|
124
|
+
k.sweep(circleProfile(3), [[0, 0, 0], [0, 0, 20], [15, 0, 20]], { cornerRadius: 5 });
|
|
125
|
+
|
|
126
|
+
// round every corner of any CCW outline, then extrude/loft/prism it
|
|
127
|
+
k.prism(filletPolygon(bracketOutline, 3), 4); // tessellated corners (faceted in STEP)
|
|
128
|
+
k.prism(roundedProfile(bracketOutline, 3), 4); // true CIRCLE corners in STEP export
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
2-D polygon helpers for `prism`/`extrude`/`loft`: `import { piePolygon, hexPolygon,
|
|
132
|
+
regularPolygon, roundedRectPolygon, starPolygon, circleProfile, filletPolygon,
|
|
133
|
+
roundedProfile } from "partforge/geometry"`. `filletPolygon(points, r, { segs? })` rounds
|
|
134
|
+
every corner of a CCW polygon (per-corner radius clamped so neighbouring arcs never overlap)
|
|
135
|
+
and returns points usable by `prism`/`extrude`/`loft` on both backends — but it **bakes each
|
|
136
|
+
corner into line facets**, so STEP corners are faceted. `roundedProfile(points, r | r[])`
|
|
137
|
+
rounds corners the same way but keeps them **mathematically true** — it carries the arc
|
|
138
|
+
symbolically so STEP export gets real circular edges. Use it for `prism`/`extrude` (not yet
|
|
139
|
+
`loft` — arc rings are rejected there in v1). A scalar `r` rounds every corner; a per-corner
|
|
140
|
+
`r[]` (length = points) rounds selectively (a `0`, a zero-length edge, or a straight/180°
|
|
141
|
+
corner stays sharp).
|
|
100
142
|
**Import geometry helpers from `partforge/geometry`, never from `partforge`** — the main
|
|
101
143
|
entry pulls in the DOM viewer/controls, and your build functions run in a Web Worker
|
|
102
144
|
(importing the main entry there throws `document is not defined`).
|
|
@@ -195,7 +237,12 @@ module-level mutable state. An impure build will silently return stale geometry.
|
|
|
195
237
|
Cache granularity follows the operations you call. Booleans and heavy primitives are
|
|
196
238
|
cached; cheap transforms are recomputed. To make a multi-step shape into a single
|
|
197
239
|
cache node, use (or add) a **compound op** like `k.boredCylinder({ od, h, bore })` —
|
|
198
|
-
it hashes from its own arguments and never exposes its internals to the cache.
|
|
240
|
+
it hashes from its own arguments and never exposes its internals to the cache. The heavy
|
|
241
|
+
primitives `loft`, `sweep`, `extrude`, `prism`, and `revolve` are cached this way too:
|
|
242
|
+
their hash folds every shape-affecting argument (each `loft` ring's points/`z`/`rotate`/`scale`,
|
|
243
|
+
`sweep`'s profile points/path points/`cornerRadius`/`closed`, `extrude`'s holes, an arc
|
|
244
|
+
profile's segment specs from `roundedProfile`, and the tessellation from `twist`), so
|
|
245
|
+
changing any of them is a fresh cache node while an identical rebuild is a hit.
|
|
199
246
|
|
|
200
247
|
---
|
|
201
248
|
|
|
@@ -384,9 +431,10 @@ stylesheet). `mount` looks up these element IDs:
|
|
|
384
431
|
Copy `demo.html` and change the title, the panel heading, and the `<script src>`. Two workers are spawned from your one worker entry
|
|
385
432
|
(`name` = `"manifold"` for preview/STL/3MF, `"occt"` for STEP — handled for you).
|
|
386
433
|
|
|
387
|
-
> Production deploy
|
|
388
|
-
> (
|
|
389
|
-
> `
|
|
434
|
+
> Production deploy compiles only the pages listed in `build.rollupOptions.input`
|
|
435
|
+
> (currently the landing gallery + the demo part pages). Other root `*.html` files are
|
|
436
|
+
> **dev-only** (Vite serves any root HTML in `npm run dev`) unless added there. To also
|
|
437
|
+
> ship one, add it to `build.rollupOptions.input` in `vite.config.js`.
|
|
390
438
|
|
|
391
439
|
### Developing against a local (linked) partforge
|
|
392
440
|
|
|
@@ -471,6 +519,54 @@ The `measure` function is also exported for vitest (boot a Manifold kernel as in
|
|
|
471
519
|
expect(r.subparts[0].holes).toBe(1); // e.g. expects one bore
|
|
472
520
|
});
|
|
473
521
|
|
|
522
|
+
### The diagnostics contract (for agents)
|
|
523
|
+
|
|
524
|
+
`partforge measure <part> --json` / `--out <file>` emits the machine-readable
|
|
525
|
+
report. Every `fail`/`warn` check in `verify.failures` / `verify.warnings`
|
|
526
|
+
carries:
|
|
527
|
+
|
|
528
|
+
- `hint` — one self-contained corrective sentence (always present),
|
|
529
|
+
- `pattern` — a stable [ERROR-PATTERNS.md](ERROR-PATTERNS.md) entry ID when one
|
|
530
|
+
applies (follow it with `ERROR-PATTERNS.md#<id>`),
|
|
531
|
+
- `location` — `[x, y, z]` in mm where the metric has one: `minWall` (thinnest
|
|
532
|
+
sample point) and `overlaps` (the center of the first offending intersection's
|
|
533
|
+
*bounding box* — a nearby indicator, not an exact point: when a pair overlaps in
|
|
534
|
+
more than one place the bbox center can fall in the empty space between regions).
|
|
535
|
+
Whole-solid metrics (bbox, volume, …) have none.
|
|
536
|
+
|
|
537
|
+
Subpart facts include `minWall` (number or `null` — null exactly when no reading
|
|
538
|
+
exists, e.g. the OCCT backend or min-wall measurement turned off, matching
|
|
539
|
+
`minWallAt`'s null) and `minWallAt` (`[x,y,z]` or `null`); overlap entries are
|
|
540
|
+
`{ a, b, volume, location }`.
|
|
541
|
+
|
|
542
|
+
A **thrown** error (bad part module, kernel failure) with `--json` prints pure
|
|
543
|
+
JSON to stdout and exits 1:
|
|
544
|
+
|
|
545
|
+
```json
|
|
546
|
+
{ "ok": false, "error": { "message": "…", "pattern": "<id>", "hint": "…" } }
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
`pattern`/`hint` appear when the message matches an ERROR-PATTERNS.md symptom
|
|
550
|
+
string. Exit codes: 0 pass, 1 gate failure or crash — unchanged. Caveat: a throw
|
|
551
|
+
*after* measure output has printed (e.g. an unknown metric in `verify.expect`, or
|
|
552
|
+
a per-case build crash) appends this JSON after the human lines, so stdout is no
|
|
553
|
+
longer pure JSON; prefer `--out` (or parse the trailing JSON object — the crash
|
|
554
|
+
JSON is pretty-printed across multiple lines) for robust machine parsing. With
|
|
555
|
+
`--out` the measure report is written to the file as soon as `measure` succeeds,
|
|
556
|
+
so even if a later `verify` throw crashes the run the file is there — it just
|
|
557
|
+
lacks the `verify` key.
|
|
558
|
+
|
|
559
|
+
**Part-authored hints.** Any `verify.expect` metric accepts `{ expr, hint }` in
|
|
560
|
+
place of a bare expression — use it to name the governing parameter:
|
|
561
|
+
|
|
562
|
+
```js
|
|
563
|
+
verify: {
|
|
564
|
+
expect: {
|
|
565
|
+
body: { minWall: { expr: ">=1.2", hint: "increase `wallThickness` or reduce `twist`" } },
|
|
566
|
+
},
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
474
570
|
---
|
|
475
571
|
|
|
476
572
|
## Self-verification (the `verify` block)
|
|
@@ -575,20 +671,23 @@ entirely on OCCT, its fillets are exact in the STEP **and** present in the print
|
|
|
575
671
|
|
|
576
672
|
## Conventions & gotchas
|
|
577
673
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
674
|
+
When something fails confusingly, **grep [ERROR-PATTERNS.md](ERROR-PATTERNS.md) for the
|
|
675
|
+
symptom first** — it maps error text → cause → fix. The invariants, one line each:
|
|
676
|
+
|
|
677
|
+
- **replicad (OCCT) transforms consume their input** — never reuse a transformed solid;
|
|
678
|
+
`.clone()` first ([replicad-consumed-operand](ERROR-PATTERNS.md#replicad-consumed-operand)).
|
|
679
|
+
- **Part modules are DOM-free and side-effect-free** — they load in both the main thread
|
|
680
|
+
and the worker ([worker-imports-main-entry](ERROR-PATTERNS.md#worker-imports-main-entry)).
|
|
681
|
+
- **`build` is a pure function of `(k, p, d)`** — impurity silently defeats the geometry
|
|
682
|
+
cache ([impure-build-stale-preview](ERROR-PATTERNS.md#impure-build-stale-preview)).
|
|
584
683
|
- **Units are millimetres** throughout.
|
|
585
|
-
- **Preview vs print quality
|
|
586
|
-
so the export path uses a separate high-res "print" kernel
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
-
|
|
591
|
-
|
|
684
|
+
- **Preview vs print quality:** Manifold bakes segment counts in at primitive creation,
|
|
685
|
+
so builds are quality-agnostic; the export path uses a separate high-res "print" kernel.
|
|
686
|
+
- **Display placement is view-independent**; only `place(..., { purpose: "export" })` may
|
|
687
|
+
depend on `view` ([view-dependent-display-place](ERROR-PATTERNS.md#view-dependent-display-place)).
|
|
688
|
+
- **Keep geometry backend-agnostic** (kernel calls only); only STEP requires OCCT
|
|
689
|
+
([probe-routed-to-occt](ERROR-PATTERNS.md#probe-routed-to-occt),
|
|
690
|
+
[occt-holes-watertight-na](ERROR-PATTERNS.md#occt-holes-watertight-na)).
|
|
592
691
|
|
|
593
692
|
---
|
|
594
693
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Error patterns — symptom-indexed lookup
|
|
2
|
+
|
|
3
|
+
When a build, test, `measure`, or `verify` run fails confusingly: **grep this file
|
|
4
|
+
for the symptom first** — the literal error text, or a phrase describing the
|
|
5
|
+
misbehavior — before debugging from scratch.
|
|
6
|
+
|
|
7
|
+
**How to add a pattern** (`##` headings are reserved for pattern entries — the lint
|
|
8
|
+
test parses every one; keep prose like this as plain paragraphs):
|
|
9
|
+
|
|
10
|
+
- One pattern per `## <id>` heading. The heading is a **stable kebab-case ID**:
|
|
11
|
+
permanent once committed — never renamed, never reused. External consumers
|
|
12
|
+
(issue #27 diagnostics, HARDWARE.md, skills) cite `ERROR-PATTERNS.md#<id>`.
|
|
13
|
+
- **Namespaces:** core framework patterns are bare slugs. Subsystem patterns take
|
|
14
|
+
a reserved prefix — `hardware-*` is reserved for the parts library (issue #30).
|
|
15
|
+
One `#`-level section per namespace.
|
|
16
|
+
- Entry shape — exactly these three list lines, then optional note paragraphs:
|
|
17
|
+
- **Symptom:** the literal string an agent would see, verbatim in backticks,
|
|
18
|
+
when one exists; otherwise the observable misbehavior. This is the grep target.
|
|
19
|
+
- **Cause:** one sentence.
|
|
20
|
+
- **Fix:** the concrete change, linking the governing rule
|
|
21
|
+
([AUTHORING-PARTS.md](AUTHORING-PARTS.md) section) rather than restating it.
|
|
22
|
+
- No tables inside entries.
|
|
23
|
+
- Code that throws should throw greppable strings: an error message thrown by
|
|
24
|
+
partforge should appear verbatim, in a backtick literal **at the start** of its
|
|
25
|
+
pattern's Symptom line. Only that leading literal is what the crash matcher
|
|
26
|
+
matches on — backticks used for prose later in the line never participate, so a
|
|
27
|
+
reworded Symptom must lead with the thrown string, not bury it mid-sentence.
|
|
28
|
+
- `test/error-patterns.test.js` lints this file's structure.
|
|
29
|
+
|
|
30
|
+
# Core framework
|
|
31
|
+
|
|
32
|
+
## worker-imports-main-entry
|
|
33
|
+
|
|
34
|
+
- **Symptom:** `ReferenceError: document is not defined` thrown from a worker build.
|
|
35
|
+
- **Cause:** The part (or a helper it imports) imports `partforge` instead of `partforge/geometry`, and the main entry pulls in the DOM viewer/controls.
|
|
36
|
+
- **Fix:** Import geometry helpers only from `partforge/geometry` in anything a worker loads. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Geometry: the kernel / `Solid` API".
|
|
37
|
+
|
|
38
|
+
## impure-build-stale-preview
|
|
39
|
+
|
|
40
|
+
- **Symptom:** Preview geometry doesn't change after editing the part's `build` (or changes once, then sticks), with no error anywhere.
|
|
41
|
+
- **Cause:** The preview kernel memoizes geometry by content hash, and an impure `build` (`Math.random`, clock, module-level mutable state) silently defeats it.
|
|
42
|
+
- **Fix:** Make `build` a pure function of `(k, p, d)`; move randomness/state into `derive` inputs or delete it. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Caching & determinism".
|
|
43
|
+
|
|
44
|
+
## replicad-consumed-operand
|
|
45
|
+
|
|
46
|
+
- **Symptom:** On the OCCT backend a solid is unexpectedly empty, or the build crashes, right after the same solid was transformed or used in a boolean — often only in STEP export, with the Manifold preview fine.
|
|
47
|
+
- **Cause:** replicad transforms and booleans (`translate`/`rotate`/`mirror`/`cut`/…) consume their operand — the input solid is deleted and a new one returned.
|
|
48
|
+
- **Fix:** Never reuse a solid after transforming it; take a `.clone()` first when you need the original again. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Geometry: the kernel / `Solid` API" (the `s.clone()` row).
|
|
49
|
+
|
|
50
|
+
The framework itself rebuilds each sub-part fresh per job and applies `place` once, which avoids the problem — follow the same pattern in your own code.
|
|
51
|
+
|
|
52
|
+
## probe-routed-to-occt
|
|
53
|
+
|
|
54
|
+
- **Symptom:** A part builds far slower than expected (preview takes seconds instead of milliseconds), and the worker logs show it running on the `occt` worker.
|
|
55
|
+
- **Cause:** The geometry-free probe runs `build` against a recording proxy (dummy query values), and a `fillet`/`chamfer`/`shell` call it reaches — including a branch the real build wouldn't take, since queries return dummies — routes the whole part to OCCT.
|
|
56
|
+
- **Fix:** Remove the CAD-only call the probe reaches unnecessarily, or force the backend with `meta.backend: "manifold"` (or `"occt"`). See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Fillet & chamfer (automatic OCCT backend)".
|
|
57
|
+
|
|
58
|
+
## boolean-not-watertight
|
|
59
|
+
|
|
60
|
+
- **Symptom:** `NOT watertight ✗` from `partforge measure` (non-zero exit) after adding a boolean cut or union.
|
|
61
|
+
- **Cause:** A coplanar-face or grazing-cut degeneracy — the tool surface exactly touches the body surface, leaving zero-thickness geometry.
|
|
62
|
+
- **Fix:** Overcut: extend the tool past the faces it pierces (e.g. the demo's cut tool is `h + 4` starting at `z = -2`) and avoid exactly-flush faces in unions. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Verifying a part headlessly (render + measure)".
|
|
63
|
+
|
|
64
|
+
## dual-kernel-same-process
|
|
65
|
+
|
|
66
|
+
- **Symptom:** A test file crashes or hangs (WASM abort) when it boots both geometry kernels.
|
|
67
|
+
- **Cause:** OCCT and Manifold WASM must not boot in the same process.
|
|
68
|
+
- **Fix:** Keep OCCT-booting tests in their own files (vitest isolates per file) and boot via `bootOcctKernel()` in a `beforeAll`. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Testing a part".
|
|
69
|
+
|
|
70
|
+
## view-dependent-display-place
|
|
71
|
+
|
|
72
|
+
- **Symptom:** A sub-part renders correctly in one view but appears misplaced (usually in its other-view pose) after switching views.
|
|
73
|
+
- **Cause:** A `place` that depends on `ctx.view` for `purpose: "display"` — display meshes are built once per sub-part and cached across views.
|
|
74
|
+
- **Fix:** Make display placement view-independent; only `place(..., { purpose: "export" })` may branch on `view`. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "The `PartDefinition` contract".
|
|
75
|
+
|
|
76
|
+
## wrong-node-version
|
|
77
|
+
|
|
78
|
+
- **Symptom:** Confusing failures during `npm install`, tests, or CLI runs — WASM load errors, syntax errors in dependencies, or kernels that never boot — on a machine that built fine before.
|
|
79
|
+
- **Cause:** The shell's default Node is older than the required Node 24 (`.nvmrc` pins it).
|
|
80
|
+
- **Fix:** Run `nvm use` before `npm install`, tests, or any `npx partforge` command. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Quickstart".
|
|
81
|
+
|
|
82
|
+
## worker-url-not-inline
|
|
83
|
+
|
|
84
|
+
- **Symptom:** The app loads but geometry never builds — the worker 404s or is missing from the production bundle (works in `npm run dev`, breaks in `npm run build`).
|
|
85
|
+
- **Cause:** The `new Worker(new URL(...))` call was moved out of the app entry file (into a helper or variable), so Vite's static analysis can't see and bundle the worker.
|
|
86
|
+
- **Fix:** Keep `new Worker(new URL("./<part>-worker.js", import.meta.url), ...)` inline in `src/app-<part>.js`. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Wiring a part into a runnable app".
|
|
87
|
+
|
|
88
|
+
## minwall-sliver-triangles
|
|
89
|
+
|
|
90
|
+
- **Symptom:** `⚠` minWall warnings from `verify` on a faceted part whose walls are clearly thicker than the profile minimum.
|
|
91
|
+
- **Cause:** The ray-shot wall-thickness measurement can catch sliver triangles at facet seams, reading a near-zero "wall" that isn't a designed wall.
|
|
92
|
+
- **Fix:** Check where the reported thin spot is: at a facet seam or chamfer transition it's a sliver artifact (minWall is a warning, never a gate — safe to note and move on); along a real wall, thicken the wall. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Self-verification (the `verify` block)".
|
|
93
|
+
|
|
94
|
+
## param-key-missing-from-defaults
|
|
95
|
+
|
|
96
|
+
- **Symptom:** The affected control's number box renders empty/blank (internally `numStr(undefined)` produces the string `NaN`, which a number input sanitizes to empty), or its range slider sits at a browser-default position and edits don't drive the geometry — no error is thrown — and if the key is `hidden`, no control is rendered for it at all.
|
|
97
|
+
- **Cause:** A `key` used in the `parameters` schema (slider, feature, or preset override) doesn't exist in `defaults` — every key must, including `hidden` ones.
|
|
98
|
+
- **Fix:** Add the key to `defaults` with a sensible starting value. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Parameters: the control-panel schema".
|
|
99
|
+
|
|
100
|
+
## dimmed-control-vestigial-param
|
|
101
|
+
|
|
102
|
+
- **Symptom:** A control renders dimmed (but still editable) and changing it does nothing on screen.
|
|
103
|
+
- **Cause:** No sub-part visible in the active view reads that parameter — the relevance-aware panel dims controls with no on-screen effect.
|
|
104
|
+
- **Fix:** This is a signal, not a bug: either the parameter is vestigial (delete it), the control is in the wrong section/view scope, or you're in a view that legitimately doesn't use it. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "The relevance-aware panel".
|
|
105
|
+
|
|
106
|
+
## linked-checkout-wasm-403
|
|
107
|
+
|
|
108
|
+
- **Symptom:** In a consuming app using an `npm link`ed partforge checkout, the kernel never boots and the dev-server network tab shows `403` on the Manifold/OCCT `.wasm` files.
|
|
109
|
+
- **Cause:** The linked checkout lives outside the app's project root, so Vite's dev server refuses to serve its files.
|
|
110
|
+
- **Fix:** Allow-list it: `server: { fs: { allow: ["./", "../partforge"] } }` in the app's `vite.config.js`. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Developing against a local (linked) partforge".
|
|
111
|
+
|
|
112
|
+
## ring-sector-full-circle
|
|
113
|
+
|
|
114
|
+
- **Symptom:** `ringSectorPolygon: arcDeg must be < 360 (use a cut for a full ring)`
|
|
115
|
+
- **Cause:** A full annulus can't be a single simple polygon — it's a contour-with-hole.
|
|
116
|
+
- **Fix:** Cut an inner cylinder from an outer one (or `k.extrude({ outer, holes })`); use `ringSectorPolygon` only for partial arcs. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Profiles & patterns".
|
|
117
|
+
|
|
118
|
+
## occt-closed-loop-unsupported
|
|
119
|
+
|
|
120
|
+
- **Symptom:** `loft: closed:true loops are only supported on the Manifold backend` (or the same message from `sweep:`) — typically during STEP export of a part that previews fine.
|
|
121
|
+
- **Cause:** Capless closed loops are a Manifold-only capability; the OCCT backend rejects them, and STEP export always runs on OCCT.
|
|
122
|
+
- **Fix:** Keep the part on Manifold (no STEP) or model the loop as a capped solid both backends support. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Geometry: the kernel / `Solid` API".
|
|
123
|
+
|
|
124
|
+
## smooth-geometry-faceted-preview
|
|
125
|
+
|
|
126
|
+
- **Symptom:** A `ruled:false` loft or `smooth:true` sweep looks faceted/straight-walled in the viewer even though the options are set.
|
|
127
|
+
- **Cause:** Smooth blending is OCCT-native; the Manifold preview always tessellates ruled straight walls — only STEP export carries the smooth surface.
|
|
128
|
+
- **Fix:** Nothing is wrong — verify smoothness in the exported STEP, not the preview. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Geometry: the kernel / `Solid` API".
|
|
129
|
+
|
|
130
|
+
## scale-moved-the-part
|
|
131
|
+
|
|
132
|
+
- **Symptom:** After `s.scale(f)` a part is resized but also relocated — features drift away from where they were built.
|
|
133
|
+
- **Cause:** `scale(factor, center?)` defaults its center to the origin, so scaling an off-origin solid about the origin also translates it.
|
|
134
|
+
- **Fix:** Pass the center you mean, e.g. `s.scale(f, s.boundingBox().center)` to resize in place. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Geometry: the kernel / `Solid` API".
|
|
135
|
+
|
|
136
|
+
## occt-holes-watertight-na
|
|
137
|
+
|
|
138
|
+
- **Symptom:** `watertight n/a` in `partforge measure` output, and `holes`/`watertight` assertions in a `verify` block don't run, on a part with fillets/chamfers.
|
|
139
|
+
- **Cause:** `holes` and `watertight` are Manifold-only topology facts, and this part auto-routed to OCCT — the assertions skip rather than fail.
|
|
140
|
+
- **Fix:** Expected behavior: assert on backend-independent facts (`bbox`, `volume`, `overlaps`) for OCCT parts, or split topology assertions into a Manifold-buildable configuration. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Self-verification (the `verify` block)".
|
|
141
|
+
|
|
142
|
+
## html-page-missing-in-prod
|
|
143
|
+
|
|
144
|
+
- **Symptom:** A part's page 404s in the production deploy while working fine under `npm run dev`.
|
|
145
|
+
- **Cause:** Only pages listed in `build.rollupOptions.input` are compiled by the production build; other root `*.html` pages are dev-only conveniences Vite serves without building.
|
|
146
|
+
- **Fix:** Add the page to `build.rollupOptions.input` in `vite.config.js` if it should ship. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Wiring a part into a runnable app".
|
|
147
|
+
|
|
148
|
+
# Hardware library
|
|
149
|
+
|
|
150
|
+
Reserved for `hardware-*` patterns (issue #30). No entries yet.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "partforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin",
|
|
19
19
|
"skills/partforge/SKILL.md",
|
|
20
20
|
"docs/AUTHORING-PARTS.md",
|
|
21
|
+
"docs/ERROR-PATTERNS.md",
|
|
21
22
|
"README.md"
|
|
22
23
|
],
|
|
23
24
|
"exports": {
|
|
@@ -56,3 +56,8 @@ Picks come back **in request order**, each echoing its prompt, so you can map th
|
|
|
56
56
|
|
|
57
57
|
- This only *reads* a click — it never edits files. You make the edits yourself after.
|
|
58
58
|
- The server is localhost-only and holds one request at a time.
|
|
59
|
+
|
|
60
|
+
## Related: debugging failures
|
|
61
|
+
|
|
62
|
+
If anything fails while you're editing a part, grep `docs/ERROR-PATTERNS.md` for the
|
|
63
|
+
symptom first — its preamble states the full grep-first rule.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import vasePart from "./parts/faceted-vase.js";
|
|
2
|
+
import { mount } from "./framework/index.js";
|
|
3
|
+
|
|
4
|
+
// Dev-only example app for the faceted vase part. Identical wiring to app-planter.js —
|
|
5
|
+
// only the imported definition and the worker entry differ per part. `npm run dev`,
|
|
6
|
+
// then open /faceted-vase.html.
|
|
7
|
+
mount(vasePart, {
|
|
8
|
+
createWorker: (name) =>
|
|
9
|
+
new Worker(new URL("./faceted-vase-worker.js", import.meta.url), { type: "module", name }),
|
|
10
|
+
});
|
|
@@ -6,7 +6,7 @@ import { viewSubParts, resolveParams, buildPosed } from "./jobs.js";
|
|
|
6
6
|
// Parts meant to fit together (e.g. a block seated in a pocket void) read ~0 and
|
|
7
7
|
// don't trip it. Manifold-only (needs Solid.intersect + Solid.volume); meant for
|
|
8
8
|
// part tests so an author/LLM editing a part sees collisions fail.
|
|
9
|
-
// → [{ a, b, volume }] for each offending pair (empty = no collisions)
|
|
9
|
+
// → [{ a, b, volume, location }] for each offending pair (empty = no collisions)
|
|
10
10
|
export function assemblyOverlaps(kernel, part, view, params = {}, { tolerance = 1 } = {}) {
|
|
11
11
|
const { p, d } = resolveParams(part, params);
|
|
12
12
|
const posed = viewSubParts(part, view, p).map((name) => ({
|
|
@@ -17,8 +17,14 @@ export function assemblyOverlaps(kernel, part, view, params = {}, { tolerance =
|
|
|
17
17
|
const overlaps = [];
|
|
18
18
|
for (let i = 0; i < posed.length; i++) {
|
|
19
19
|
for (let j = i + 1; j < posed.length; j++) {
|
|
20
|
-
const
|
|
21
|
-
|
|
20
|
+
const inter = posed[i].solid.intersect(posed[j].solid);
|
|
21
|
+
const volume = inter.volume();
|
|
22
|
+
if (volume > tolerance) {
|
|
23
|
+
// location = the intersection's bounding-box center — a nearby indicator, not
|
|
24
|
+
// an exact contact point: for a disjoint (multi-region) intersection it is the
|
|
25
|
+
// midpoint of those regions and can land in the empty space between them.
|
|
26
|
+
overlaps.push({ a: posed[i].name, b: posed[j].name, volume, location: inter.boundingBox().center });
|
|
27
|
+
}
|
|
22
28
|
}
|
|
23
29
|
}
|
|
24
30
|
kernel.cleanup?.(); // free the per-check WASM objects
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Builds a watertight triangle mesh of a circular profile swept along a helix in
|
|
2
2
|
// its frenet frame, then imports it as a Manifold solid. The profile stays
|
|
3
3
|
// perpendicular to the helix tangent (unlike twist-extrude), so it matches an
|
|
4
|
-
// exact frenet sweep. Winding is consistent-outward;
|
|
5
|
-
//
|
|
4
|
+
// exact frenet sweep. Winding is consistent-outward; the ring stitching + caps +
|
|
5
|
+
// ofMesh import are the shared ring-mesh helpers in mesh-build.js (also used by loft).
|
|
6
|
+
import { sideQuads, fanCap, manifoldFromMesh } from "./mesh-build.js";
|
|
7
|
+
|
|
6
8
|
const norm = (v) => { const m = Math.hypot(...v); return [v[0] / m, v[1] / m, v[2] / m]; };
|
|
7
9
|
const cross = (a, b) => [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]];
|
|
8
10
|
|
|
@@ -28,23 +30,11 @@ export function helixTube(wasm, opts) {
|
|
|
28
30
|
ctr[2] + profileR * (Math.cos(a) * N[2] + Math.sin(a) * B[2]));
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
|
-
// side
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
// end caps
|
|
38
|
-
const c0 = V.length / 3;
|
|
39
|
-
V.push(pathR * Math.cos(0), pathR * Math.sin(0), z0);
|
|
40
|
-
for (let j = 0; j < ringSegs; j++) Tr.push(c0, (j+1)%ringSegs, j);
|
|
41
|
-
const base = (n - 1) * ringSegs, cz = V.length / 3;
|
|
42
|
-
V.push(pathR * Math.cos(sign * phiMax), pathR * Math.sin(sign * phiMax), z0 + c * phiMax);
|
|
43
|
-
for (let j = 0; j < ringSegs; j++) Tr.push(cz, base + j, base + (j+1)%ringSegs);
|
|
33
|
+
sideQuads(Tr, n, ringSegs, false); // side walls between the n stations
|
|
34
|
+
// end caps fanned from each end's path-center (outward: bottom flipped, top not)
|
|
35
|
+
fanCap(V, Tr, 0, ringSegs, [pathR, 0, z0], true);
|
|
36
|
+
fanCap(V, Tr, (n - 1) * ringSegs, ringSegs,
|
|
37
|
+
[pathR * Math.cos(sign * phiMax), pathR * Math.sin(sign * phiMax), z0 + c * phiMax], false);
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
mesh.merge();
|
|
47
|
-
const out = wasm.Manifold.ofMesh(mesh);
|
|
48
|
-
mesh.delete?.(); // input mesh is consumed by ofMesh; free it (caller tracks `out`)
|
|
49
|
-
return out;
|
|
39
|
+
return manifoldFromMesh(wasm, V, Tr);
|
|
50
40
|
}
|
|
@@ -15,6 +15,12 @@ export function finishKernel(k) {
|
|
|
15
15
|
return rawPrism(pts, h, opts);
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
const rawExtrude = k.extrude;
|
|
19
|
+
k.extrude = (profile, h, opts) => {
|
|
20
|
+
if ((opts?.scaleTop ?? 1) < 0) throw new Error("extrude: scaleTop must be ≥ 0");
|
|
21
|
+
return rawExtrude(profile, h, opts);
|
|
22
|
+
};
|
|
23
|
+
|
|
18
24
|
const rawRevolve = k.revolve;
|
|
19
25
|
k.revolve = (pts, opts) => {
|
|
20
26
|
for (const [r] of pts) if (r < 0) throw new Error("revolve: profile radius must be ≥ 0");
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
// Ops every backend kernel must implement.
|
|
9
9
|
export const KERNEL_OPS = [
|
|
10
|
-
"cylinder", "boredCylinder", "sphere", "box", "prism", "revolve",
|
|
11
|
-
"helixSweptTube", "union", "toSTEP",
|
|
10
|
+
"cylinder", "boredCylinder", "sphere", "box", "prism", "extrude", "revolve",
|
|
11
|
+
"loft", "sweep", "helixSweptTube", "union", "toSTEP",
|
|
12
12
|
];
|
|
13
13
|
|
|
14
14
|
// Backend-optional kernel ops: the Manifold cache brackets + WASM lifetime hooks.
|
|
@@ -70,6 +70,9 @@ export const OCCT_ONLY_OPS = ["fillet", "chamfer", "shell"];
|
|
|
70
70
|
* @property {(r:number) => Solid} sphere sphere centred at the origin
|
|
71
71
|
* @property {(min:number[], max:number[]) => Solid} box
|
|
72
72
|
* @property {(points2D:number[][], h:number, opts?:{twist?:number,scaleTop?:number}) => Solid} prism extrude polygon from z=0 (optional twist° + uniform top taper)
|
|
73
|
+
* @property {(profile:number[][]|{outer:number[][],holes?:number[][][]}, h:number, opts?:{twist?:number,scaleTop?:number}) => Solid} extrude extrude a polygon-with-holes region from z=0 in one op (bare array = outer only)
|
|
74
|
+
* @property {(rings:{polygon?:number[][],sides?:number,radius?:number,z:number,rotate?:number,scale?:number|number[]}[], opts?:{ruled?:boolean,closed?:boolean}) => Solid} loft stack polygon cross-sections (per-ring z/rotate/scale), ruled walls, capped ends
|
|
75
|
+
* @property {(profile2D:number[][], path3D:number[][], opts?:{closed?:boolean,cornerRadius?:number,ruled?:boolean,smooth?:boolean}) => Solid} sweep sweep a fixed 2-D profile along a 3-D polyline path (sharp mitered corners or cornerRadius fillets; capped ends; closed:true loops and smooth:true native B-rep are backend-specific)
|
|
73
76
|
* @property {(points2D:number[][], opts?:{degrees?:number}) => Solid} revolve revolve a lathe profile [[r,z],…] around Z
|
|
74
77
|
* @property {(o:{pathR:number,profileR:number,pitch:number,turns:number,z0:number,lefthand:boolean}) => Solid} helixSweptTube
|
|
75
78
|
* @property {(solids:Solid[]) => Solid} union
|