partforge 0.20.0 → 0.20.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
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",
@@ -0,0 +1,68 @@
1
+ import planterPart from "./parts/planter.js";
2
+ import { mount } from "./framework/index.js";
3
+
4
+ // Dev-only smoke harness for the 0.12.0 embedding contract (see embed-test.html).
5
+ // Exercises: explicit `elements` refs (no legacy IDs on the page), runtime.ready,
6
+ // runtime.dispose() (including dispose-before-first-build and rapid remount cycles),
7
+ // onBuild outcomes, and always-on onPick with the { selection, label, prompt, token }
8
+ // payload — i.e. exactly what partforge-cloud's chat chips will consume.
9
+
10
+ const logEl = document.getElementById("log");
11
+ function log(kind, msg) {
12
+ const line = document.createElement("div");
13
+ line.className = kind;
14
+ line.textContent = `${new Date().toISOString().slice(11, 19)} ${msg}`;
15
+ logEl.prepend(line);
16
+ }
17
+
18
+ const byId = (id) => document.getElementById(id);
19
+ const elements = {
20
+ viewer: byId("pf-viewer"),
21
+ controls: byId("pf-controls"),
22
+ status: { status: byId("pf-status"), busy: byId("pf-busy"), phase: byId("pf-phase") },
23
+ tabs: byId("pf-tabs"),
24
+ exports: { stl: byId("pf-stl"), step: byId("pf-step") }, // no 3MF button on purpose (optional)
25
+ chrome: { pause: byId("pf-pause"), reframe: byId("pf-reframe"), theme: byId("pf-theme") },
26
+ };
27
+
28
+ let runtime = null;
29
+
30
+ function doMount() {
31
+ if (runtime) { log("life", "already mounted — dispose first"); return; }
32
+ runtime = mount(planterPart, {
33
+ createWorker: (name) =>
34
+ new Worker(new URL("./planter-worker.js", import.meta.url), { type: "module", name }),
35
+ elements,
36
+ onBuild: (b) => log("build", `onBuild: ${b.status}${b.ms != null ? ` (${Math.round(b.ms)} ms)` : ""}${b.error ? ` — ${b.error}` : ""}`),
37
+ onPick: ({ label, prompt, token, selection }) => {
38
+ log("pick", `onPick label="${label}"`);
39
+ log("pick", ` prompt: ${prompt}`);
40
+ log("pick", ` token: ${token}`);
41
+ console.log("onPick payload", { label, prompt, token, selection });
42
+ },
43
+ });
44
+ log("life", "mounted");
45
+ runtime.ready.then(
46
+ () => log("life", "ready ✓ (first build succeeded)"),
47
+ (e) => log("life", `ready ✗ (${e.message})`),
48
+ );
49
+ }
50
+
51
+ function doDispose() {
52
+ if (!runtime) { log("life", "not mounted"); return; }
53
+ runtime.dispose();
54
+ runtime.dispose(); // idempotence check — must be a no-op
55
+ runtime = null;
56
+ log("life", "disposed (×2 — second call must be a no-op)");
57
+ }
58
+
59
+ document.getElementById("btn-mount").addEventListener("click", doMount);
60
+ document.getElementById("btn-dispose").addEventListener("click", doDispose);
61
+ // The React-unmount torture test: dispose immediately after mount, repeatedly.
62
+ // Expect five "ready ✗ (disposed before first build)" lines, then a final clean mount.
63
+ document.getElementById("btn-cycle").addEventListener("click", () => {
64
+ for (let i = 0; i < 5; i++) { doDispose(); doMount(); doDispose(); }
65
+ doMount();
66
+ });
67
+
68
+ doMount();
@@ -12,7 +12,7 @@
12
12
  // capability (Manifold can't do toSTEP; both backends now define shape2d, so
13
13
  // that stub is dead in practice — kept as a safety net for a future backend).
14
14
  // The per-Solid twin of this layer is addSugar() in solid-sugar.js.
15
- import opentype from "opentype.js";
15
+ import * as opentype from "opentype.js";
16
16
  import { KernelCapabilityError } from "./errors.js";
17
17
  import { isPlainOptions, KERNEL_OP_SPECS } from "./op-options.js";
18
18
  import { textGlyphs } from "./text2d.js";
@@ -19,6 +19,8 @@ export default {
19
19
  description: "Height of the uppercase letters. The second line scales with it." },
20
20
  { key: "depth", label: "Relief depth", unit: "mm", min: 0.4, max: 3, step: 0.1,
21
21
  description: "How far the lettering is raised above (emboss) or recessed into (engrave) the plate face." },
22
+ { key: "stroke", label: "Stroke offset", unit: "mm", min: -0.3, max: 1.5, step: 0.05,
23
+ description: "Grow (>0, bolder) or shrink (<0, thinner) the letters with a **Shape2D offset** — the same operation used for print clearance. Large negative values collapse thin strokes, so the letters hold at their thinnest valid size rather than breaking." },
22
24
  ],
23
25
  },
24
26
  {
@@ -43,14 +45,19 @@ export default {
43
45
  ],
44
46
  },
45
47
  ],
46
- defaults: { size: 8, depth: 1.2, margin: 4, corner: 3, thickness: 3, engrave: 0 },
48
+ defaults: { size: 8, depth: 1.2, stroke: 0, margin: 4, corner: 3, thickness: 3, engrave: 0 },
47
49
  parts: {
48
50
  plate: {
49
51
  label: "Nameplate",
50
52
  views: ["plate"],
51
53
  export: { name: "nameplate" },
52
54
  build: (k, p) => {
53
- const text = k.text2d(LABEL, { size: p.size, align: "center", valign: "middle", lineHeight: p.size * 1.7 });
55
+ let text = k.text2d(LABEL, { size: p.size, align: "center", valign: "middle", lineHeight: p.size * 1.7 });
56
+ // Shape2D offset on the lettering: grow (>0, bolder) or shrink (<0, thinner). Guard
57
+ // against a shrink that collapses thin strokes — keep the un-offset letters if so.
58
+ if (p.stroke !== 0) {
59
+ try { const g = text.offset(p.stroke); if (g.area() > 1) text = g; } catch { /* collapsed — keep un-offset */ }
60
+ }
54
61
  const bb = text.boundingBox();
55
62
  const w = (bb.max[0] - bb.min[0]) + 2 * p.margin;
56
63
  const h = (bb.max[1] - bb.min[1]) + 2 * p.margin;