partforge 0.17.0 → 0.20.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.
Files changed (43) hide show
  1. package/docs/AUTHORING-PARTS.md +121 -1
  2. package/docs/ERROR-PATTERNS.md +21 -0
  3. package/package.json +3 -1
  4. package/src/app-bracket.js +9 -0
  5. package/src/app-nameplate.js +9 -0
  6. package/src/app-text-smoke.js +10 -0
  7. package/src/bracket-worker.js +3 -0
  8. package/src/framework/app.css +23 -6
  9. package/src/framework/cutaway-controls.js +155 -0
  10. package/src/framework/cutaway-gizmo.js +686 -0
  11. package/src/framework/cutaway-math.js +53 -0
  12. package/src/framework/cutaway-render.js +338 -0
  13. package/src/framework/cutaway.js +469 -0
  14. package/src/framework/fonts.js +36 -0
  15. package/src/framework/geometry/curve-fill.js +86 -0
  16. package/src/framework/geometry/fonts/Roboto-LICENSE.txt +93 -0
  17. package/src/framework/geometry/fonts/Roboto-Regular.ttf +0 -0
  18. package/src/framework/geometry/fonts/default-font.js +3 -0
  19. package/src/framework/geometry/kernel-front.js +57 -1
  20. package/src/framework/geometry/kernel.js +20 -2
  21. package/src/framework/geometry/manifold-backend.js +78 -7
  22. package/src/framework/geometry/occt-backend.js +95 -5
  23. package/src/framework/geometry/op-options.js +4 -0
  24. package/src/framework/geometry/shape2d-regions.js +134 -0
  25. package/src/framework/geometry/shape2d-sugar.js +11 -0
  26. package/src/framework/geometry/text2d.js +98 -0
  27. package/src/framework/geometry-service.js +21 -2
  28. package/src/framework/jobs.js +9 -0
  29. package/src/framework/mount.js +278 -223
  30. package/src/framework/selection/hover.js +102 -36
  31. package/src/framework/selection/raycast.js +4 -1
  32. package/src/framework/tooltip.js +282 -0
  33. package/src/framework/viewer-controls.js +25 -2
  34. package/src/framework/viewer-lighting.js +13 -0
  35. package/src/framework/viewer.js +83 -10
  36. package/src/nameplate-worker.js +3 -0
  37. package/src/parts/bracket.js +76 -0
  38. package/src/parts/demo.js +1 -1
  39. package/src/parts/nameplate.js +67 -0
  40. package/src/parts/text-smoke.js +21 -0
  41. package/src/testing/manifold.js +6 -2
  42. package/src/testing/occt.js +6 -2
  43. package/src/text-smoke-worker.js +3 -0
@@ -0,0 +1,98 @@
1
+ // Pure (kernel-free) text layout: an opentype.js font + string → per-glyph curve-
2
+ // contour region specs ({outer, holes}, pathProfile contours), positioned + scaled
3
+ // so uppercase letters are `size` mm tall, in math (y-up, CCW) coordinates. The
4
+ // kernel's text2d maps these to k.shape2d and unions them. No WASM, no DOM.
5
+ import { pathProfile } from "./polygon.js";
6
+ import { resolveCurveFill } from "./curve-fill.js";
7
+
8
+ const capHeightUnits = (font) =>
9
+ font.tables?.os2?.sCapHeight || font.charToGlyph("H").getBoundingBox().y2 || font.unitsPerEm * 0.7;
10
+
11
+ // opentype path commands (font units, y-DOWN) → array of pathProfile contours
12
+ // (y flipped to math up). Each M starts a new contour; Q elevates to cubic.
13
+ function glyphContours(glyph, font) {
14
+ const cmds = glyph.getPath(0, 0, font.unitsPerEm).commands;
15
+ const contours = [];
16
+ let pen = null, cur = null, start = null;
17
+ const P = ([x, y]) => [x, -y]; // y-down → y-up
18
+ for (const c of cmds) {
19
+ if (c.type === "M") { if (pen) contours.push(pen.close()); start = cur = P([c.x, c.y]); pen = pathProfile(start); }
20
+ else if (c.type === "L") { cur = P([c.x, c.y]); pen.lineTo(cur); }
21
+ else if (c.type === "C") { pen.cubicTo(P([c.x, c.y]), P([c.x1, c.y1]), P([c.x2, c.y2])); cur = P([c.x, c.y]); }
22
+ else if (c.type === "Q") { // quadratic → cubic elevation
23
+ const p0 = cur, q = P([c.x1, c.y1]), end = P([c.x, c.y]);
24
+ const c1 = [p0[0] + (2/3)*(q[0]-p0[0]), p0[1] + (2/3)*(q[1]-p0[1])];
25
+ const c2 = [end[0] + (2/3)*(q[0]-end[0]), end[1] + (2/3)*(q[1]-end[1])];
26
+ pen.cubicTo(end, c1, c2); cur = end;
27
+ }
28
+ else if (c.type === "Z") { if (pen) { contours.push(pen.close()); pen = null; } }
29
+ }
30
+ if (pen) contours.push(pen.close());
31
+ return contours;
32
+ }
33
+
34
+ // Translate a pathProfile contour by (dx,dy) and scale by s (about origin, post-translate order: scale then translate).
35
+ const xform = (contour, s, dx, dy) => {
36
+ const T = ([x, y]) => [x * s + dx, y * s + dy];
37
+ const out = { start: T(contour.start), segments: contour.segments.map((seg) => {
38
+ const m = { to: T(seg.to) };
39
+ if (seg.via) m.via = T(seg.via);
40
+ if (seg.c1) { m.c1 = T(seg.c1); m.c2 = T(seg.c2); }
41
+ return m;
42
+ }) };
43
+ return out;
44
+ };
45
+
46
+ export function textGlyphs(font, string, { size = 10, align = "center", valign = "middle",
47
+ lineHeight, tracking = 0, kerning = true } = {}) {
48
+ const upm = font.unitsPerEm;
49
+ const s = size / capHeightUnits(font); // font units → mm (cap height)
50
+ const lineAdv = (lineHeight ?? (font.ascender - font.descender) / upm * size * 1.0);
51
+ const kern = (a, b) => { if (!kerning || !a || !b) return 0; try { return font.getKerningValue(a, b); } catch { return 0; } };
52
+ // Every OpenType outline format — TrueType (glyf) and PostScript (CFF/CFF2) — is filled
53
+ // with the NONZERO winding rule (the glyf / Type 2 charstring imaging model). even-odd is
54
+ // not an OpenType fill rule; the resolver keeps it only as a general capability.
55
+ const fillRule = "nonzero";
56
+
57
+ const lines = string.split("\n");
58
+ // 1) lay out each line in font-unit x, collect glyph region specs + line width (mm)
59
+ const laid = lines.map((line) => {
60
+ // One glyph per input character via font.charToGlyph, NOT font.stringToGlyphs.
61
+ // stringToGlyphs runs opentype.js's bidi/GSUB text-shaping engine (ligatures,
62
+ // and — unconditionally, regardless of the `features` option — ccmp glyph
63
+ // composition). That engine eagerly instantiates a lookup method for every
64
+ // subtable in play and throws for lookup types it hasn't implemented (e.g.
65
+ // "lookupType 6 - substFormat: 2", class-based chaining contextual
66
+ // substitution) even when the actual input never matches that subtable's
67
+ // coverage. Real-world fonts commonly carry such lookups in their ccmp
68
+ // feature (e.g. the bundled Roboto, for accent composition), so
69
+ // stringToGlyphs throws on almost any 2+ character string against them —
70
+ // this is a per-character CAD label generator, not a typesetting engine, so
71
+ // literal glyph-per-character mapping (no ligatures/substitution) is exactly
72
+ // the semantics wanted here anyway.
73
+ const glyphs = Array.from(line).map((ch) => font.charToGlyph(ch));
74
+ let penX = 0; const specs = [];
75
+ glyphs.forEach((g, i) => {
76
+ if (i > 0) penX += kern(glyphs[i - 1], g);
77
+ for (const region of resolveCurveFill(glyphContours(g, font), { fillRule }))
78
+ specs.push({ region, penX }); // remember this glyph's pen origin (font units)
79
+ penX += g.advanceWidth + (tracking / s); // tracking is mm → font units
80
+ });
81
+ return { specs, widthMm: penX * s };
82
+ });
83
+
84
+ const totalH = (lines.length - 1) * lineAdv + size; // block height (mm), caps as the line box
85
+ const blockDy = valign === "top" ? -size : valign === "bottom" ? totalH - size
86
+ : valign === "middle" ? (totalH / 2 - size) : 0; // "baseline" → 0
87
+
88
+ const out = [];
89
+ laid.forEach(({ specs, widthMm }, li) => {
90
+ const alignDx = align === "center" ? -widthMm / 2 : align === "right" ? -widthMm : 0;
91
+ const dy = -li * lineAdv + blockDy; // lines stack downward
92
+ for (const { region, penX } of specs) {
93
+ const dx = penX * s + alignDx;
94
+ out.push({ outer: xform(region.outer, s, dx, dy), holes: region.holes.map((h) => xform(h, s, dx, dy)) });
95
+ }
96
+ });
97
+ return out;
98
+ }
@@ -5,8 +5,27 @@
5
5
  // `new Worker(new URL("./part-worker.js", import.meta.url), { type:"module", name })`
6
6
  // pattern INLINE — Vite only bundles a worker (and its backend chunks) when it sees
7
7
  // that literal call, so the framework can't construct it from a passed-in URL.
8
+ function terminateWorkers(workers) {
9
+ const errors = [];
10
+ for (const worker of workers) {
11
+ try { worker.terminate(); } catch (error) { errors.push(error); }
12
+ }
13
+ if (errors.length === 1) throw errors[0];
14
+ if (errors.length > 1) {
15
+ throw new AggregateError(errors, "geometry worker termination failed");
16
+ }
17
+ }
18
+
8
19
  export function createGeometryService({ createWorker, onMessage }) {
9
- const workers = { manifold: createWorker("manifold"), occt: createWorker("occt") };
20
+ const manifold = createWorker("manifold");
21
+ let occt;
22
+ try {
23
+ occt = createWorker("occt");
24
+ } catch (error) {
25
+ try { terminateWorkers([manifold]); } catch { /* preserve the worker creation error */ }
26
+ throw error;
27
+ }
28
+ const workers = { manifold, occt };
10
29
  workers.manifold.onmessage = onMessage;
11
30
  workers.occt.onmessage = onMessage;
12
31
  // Post a job to the chosen backend's worker. The message's own `type` says what to
@@ -14,6 +33,6 @@ export function createGeometryService({ createWorker, onMessage }) {
14
33
  // — manifold for preview/STL/3MF, occt for STEP (the caller passes "occt" for that).
15
34
  return {
16
35
  send: (msg, backend = "manifold") => workers[backend].postMessage(msg),
17
- terminate: () => { workers.manifold.terminate(); workers.occt.terminate(); },
36
+ terminate: () => terminateWorkers([workers.manifold, workers.occt]),
18
37
  };
19
38
  }
@@ -1,5 +1,6 @@
1
1
  import { meshTo3MF } from "./geometry/threemf.js";
2
2
  import { resolveDerived } from "./derive.js";
3
+ import { resolveFonts } from "./fonts.js";
3
4
 
4
5
  // Names of the sub-parts a view shows: declared in the view and enabled for these
5
6
  // params. Order follows Object.keys(part.parts) (definition order).
@@ -56,6 +57,14 @@ export async function handle(kernel, part, msg, post) {
56
57
  const exportName = (name) => part.parts[name].export?.name ?? name;
57
58
 
58
59
  try {
60
+ // Preload any part-declared fonts into the kernel before building — once per
61
+ // font name; a lazy dynamic import because this is async context (unlike the
62
+ // synchronous kernel-front), so it doesn't cost sync callers anything.
63
+ if (part.fonts && kernel._fonts) {
64
+ const opentype = (await import("opentype.js")).default;
65
+ const bufs = await resolveFonts(part.fonts);
66
+ for (const [name, buf] of bufs) if (!kernel._fonts.has(name)) kernel._fonts.set(name, opentype.parse(buf));
67
+ }
59
68
  // Inside the try so a throwing derive posts an error the UI can show,
60
69
  // instead of killing the worker turn silently (an endless spinner).
61
70
  const { p, d } = resolveParams(part, msg.params);