partforge 0.117.0 → 0.118.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.
@@ -1060,12 +1060,41 @@ export function tilePicker(host) {
1060
1060
  listener you attach yourself with `addEventListener`, rather than through `host.h`,
1061
1061
  is **not** guarded — wire listeners through `host.h`, or wrap your own in try/catch.
1062
1062
 
1063
- **Looking native.** The slot inherits the rail's font, colours and light/dark theme.
1064
- Bare `<button>`, `<input>` and `<select>` elements pick up the built-in looks
1065
- automatically; the built-in classes are available by name for the exact thing:
1063
+ **Looking native.** The slot inherits the rail's font and text colour, and the
1064
+ built-in looks come for free: bare `<button>`, `<input>` and `<select>` elements
1065
+ are already styled, the built-in classes are there by name for the exact thing
1066
1066
  `row`, `seg` (a segmented row of buttons), `action`, `ghost`, `num`, `text-input`,
1067
- `select-input`; and for SVG, `pf-hit` (clickable, with a `selected` state) and
1068
- `pf-drag`. Sub-controls mounted through `host.controls` *are* the built-in widgets.
1067
+ `select-input` and sub-controls mounted through `host.controls` *are* the
1068
+ built-in widgets. For SVG there are two classes: `pf-hit` (a clickable region,
1069
+ with a `selected` state) and `pf-drag`.
1070
+
1071
+ **Colour only with the rail's tokens.** Inheriting the rail's colours covers the
1072
+ text you did not style; it does not cover anything you colour yourself. For that,
1073
+ use a `--pf-*` custom property and never a hex literal or a named colour — the
1074
+ tokens flip with the light/dark theme and a literal cannot, so a `#333` border is
1075
+ a widget that is unreadable in one of the two themes. The ones worth knowing:
1076
+ `var(--pf-text)` and `var(--pf-text-2)` for text, `var(--pf-muted)` for a
1077
+ secondary label, `var(--pf-border)` for a rule or an outline,
1078
+ `var(--pf-surface-2)` for a filled chip, `var(--pf-accent)` with
1079
+ `var(--pf-on-accent)` for the selected or primary thing, and `var(--pf-err)` for
1080
+ a problem. That palette is the whole palette: the accent blue for the one thing
1081
+ that is chosen or primary, the surface and border greys for everything else. A
1082
+ widget that reaches past it for a red or a green is a widget that has stopped
1083
+ looking like the rest of the panel.
1084
+
1085
+ **Nest a region's label inside the region.** The common case then needs no colour
1086
+ from you at all: an SVG shape you leave unfilled inherits `var(--pf-text-2)`
1087
+ rather than SVG's own black, a `pf-hit` region is filled and outlined at rest,
1088
+ and a `<text>` *inside* that region stays readable through all three states —
1089
+ the rail gives it `var(--pf-text-2)` at rest and `var(--pf-on-accent)` once the
1090
+ region is selected and has gone solid accent underneath it, and clears the
1091
+ stroke it would otherwise inherit from the region's outline (SVG paints that
1092
+ around every glyph, which at label sizes leaves a pale ghost of the number).
1093
+ Draw the label as a sibling instead and none of that reaches it: a dark number
1094
+ sitting on the selected blue is the result, and you have to colour it yourself.
1095
+ To colour a
1096
+ region from your own data, set an inline `style` — a `fill="…"` attribute loses
1097
+ to the rail's rule.
1069
1098
 
1070
1099
  **Reading the part's own files.** Artwork can live beside the code (the tree is text,
1071
1100
  so an SVG, a `partforge-vector` JSON document or a JSON data file — not a PNG).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.117.0",
3
+ "version": "0.118.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",
@@ -387,10 +387,10 @@ button.action:disabled { opacity: .5; cursor: default; }
387
387
  /* Custom controls (type: "custom"): a part-authored widget in the rail. The
388
388
  slot inherits the rail's cascade (font, colours, --pf-* tokens); these rules
389
389
  give BARE elements the built-in controls' look so a widget written with
390
- plain <button>/<input>/<select> still reads as native, and provide two
391
- opt-in classes for SVG: .pf-hit (a clickable region) and .pf-drag (a
392
- dragged one — touch-action: none is what lets a finger drag it on a phone
393
- instead of scrolling the sheet). */
390
+ plain <button>/<input>/<select> still reads as native, supply a themed
391
+ floor for hand-drawn SVG, and provide two opt-in classes for SVG: .pf-hit
392
+ (a clickable region) and .pf-drag (a dragged one — touch-action: none is
393
+ what lets a finger drag it on a phone instead of scrolling the sheet). */
394
394
  .pf-custom { margin: 9px 0; }
395
395
  .pf-custom.hidden { display: none; }
396
396
  .pf-custom-slot { display: flow-root; }
@@ -400,7 +400,12 @@ button.action:disabled { opacity: .5; cursor: default; }
400
400
  like .text-input (0,1,0) or .row .num (0,2,0). At true zero specificity,
401
401
  any class rule wins, so a named built-in class (.seg button, .text-input,
402
402
  select.select-input, …) always beats these bare-element defaults. */
403
- :where(.pf-custom) :where(svg) { display: block; max-width: 100%; height: auto; }
403
+ /* `fill` inherits in SVG, so this is the floor for everything the widget draws
404
+ and did not colour — including <text>. SVG's own initial fill is BLACK, which
405
+ is invisible on the dark theme, and that is what an author gets for free
406
+ unless the rail supplies a token. Inherited rather than matched, so a child
407
+ carrying its own fill="…" attribute still wins. */
408
+ :where(.pf-custom) :where(svg) { display: block; max-width: 100%; height: auto; fill: var(--pf-text-2); }
404
409
  :where(.pf-custom) :where(button) {
405
410
  font: inherit; font-size: 12px; padding: 5px 9px; cursor: pointer;
406
411
  background: transparent; border: 1px solid var(--pf-border); color: var(--pf-text-2);
@@ -417,9 +422,29 @@ button.action:disabled { opacity: .5; cursor: default; }
417
422
  outline: none; border-color: var(--pf-accent);
418
423
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent);
419
424
  }
420
- .pf-custom .pf-hit { cursor: pointer; }
425
+ /* A region the reader clicks. The RESTING fill and stroke are what make it read
426
+ as a control: without them a .pf-hit polygon is a black slab with no border
427
+ until it is hovered or selected. Zero specificity, so a widget that colours
428
+ regions from its own data still wins — with an inline `style`, since a
429
+ `fill="…"` attribute loses to any rule that matches the element, even this
430
+ one. Hover and selected below keep their own (0,2,x) weight. */
431
+ :where(.pf-custom) :where(.pf-hit) { cursor: pointer; fill: var(--pf-surface-2); stroke: var(--pf-border); }
421
432
  .pf-custom .pf-hit:hover { fill: color-mix(in oklab, var(--pf-accent) 25%, transparent); }
422
- .pf-custom .pf-hit.selected { fill: color-mix(in oklab, var(--pf-accent) 45%, transparent); stroke: var(--pf-accent); }
433
+ /* Selected is the SOLID accent, not a wash of it. A translucent accent lands on a
434
+ different value in each theme — pale over the light background, dark over the
435
+ dark one — so no single label colour is readable on both. Solid accent is one
436
+ colour in both, which is what makes --pf-on-accent below a rule rather than a
437
+ guess. A widget that would rather tint its own region overrides with `style`. */
438
+ .pf-custom .pf-hit.selected { fill: var(--pf-accent); stroke: var(--pf-accent); }
439
+ /* A label NESTED inside the region — the structure to recommend, because it is the
440
+ only one where the rail can keep the text readable as the region changes colour
441
+ underneath it. `fill` inherits, so without these the label takes the region's OWN
442
+ fill: grey on grey at rest, and accent on accent once selected. `stroke` inherits
443
+ too, and that one is worse than it looks: the region's outline colour gets painted
444
+ around every glyph, which at label sizes is most of the glyph — pale ghosts of the
445
+ numbers at rest, blue ones when selected. Hence stroke: none, not just a fill. */
446
+ :where(.pf-custom) :where(.pf-hit) text { fill: var(--pf-text-2); stroke: none; }
447
+ .pf-custom .pf-hit.selected text { fill: var(--pf-on-accent); }
423
448
  .pf-custom .pf-drag { touch-action: none; }
424
449
  .pf-custom-error {
425
450
  margin: 4px 0; padding: 6px 8px; border: 1px solid var(--pf-err); border-radius: var(--pf-radius-control);
@@ -147,9 +147,22 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
147
147
  const images = new Map();
148
148
  // Boundary ops route through cache.lookup; on a miss `make` runs the WASM op,
149
149
  // tracks the result, and returns the triple the cache needs to pin/dispose it.
150
+ // Delete an embind handle once. Every WASM object a build makes is T()-tracked
151
+ // for cleanup(), and the cached ones are ALSO owned by the solid cache, which
152
+ // disposes them when a later round evicts them. Those two owners only stay
153
+ // apart because cleanup() normally runs at the end of every build round and
154
+ // empties `tracked` — a build that THROWS on a path without a finally-cleanup
155
+ // (the oracle's measure(), a host's own build loop) leaves that round's new
156
+ // cached solids on `tracked`; the next round evicts them, the cache deletes
157
+ // them, and the cleanup after that deletes them again: "Manifold instance
158
+ // already deleted", on every build for the rest of the kernel's life. Two
159
+ // failing builds in a row was the reproduction (2026-09-18: plate → boss →
160
+ // bad cut → hole → bad cut → anything). embind knows whether a handle is
161
+ // gone, so both owners ask before deleting instead of trusting the other.
162
+ const del = (o) => { if (!o?.isDeleted?.()) o?.delete?.(); };
150
163
  const cached = (hash, computeM) => cache.lookup(hash, () => {
151
164
  const m = computeM(); // already T()-tracked by the op
152
- return { value: wrap(m, hash), pin: m, dispose: () => m.delete?.() };
165
+ return { value: wrap(m, hash), pin: m, dispose: () => del(m) };
153
166
  });
154
167
 
155
168
  // Booleans commute with any invertible affine map, so a transform EVERY operand
@@ -188,7 +201,7 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
188
201
  // the entry (cleanup() skips pinned objects).
189
202
  const csFor = (shape) => cache.lookup(h("cs2d", shape._hash, segs), () => {
190
203
  const cs = T(CrossSection.ofPolygons(regionPolys(shape._regions, segsAt), "EvenOdd"));
191
- return { value: cs, pin: cs, dispose: () => cs.delete?.() };
204
+ return { value: cs, pin: cs, dispose: () => del(cs) };
192
205
  });
193
206
  // The same shape as a LATHE profile: its arcs sampled at the double-curvature count
194
207
  // (every sample becomes a full ring of the sweep — the quadratic cost circle-segs.js
@@ -196,7 +209,7 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
196
209
  // own key. Keyed on the tier, not a count: the count varies per arc radius.
197
210
  const csForLathe = (shape) => cache.lookup(h("cs2d-lathe", shape._hash, quality), () => {
198
211
  const cs = T(CrossSection.ofPolygons(regionPolys(shape._regions, dcAt), "EvenOdd"));
199
- return { value: cs, pin: cs, dispose: () => cs.delete?.() };
212
+ return { value: cs, pin: cs, dispose: () => del(cs) };
200
213
  });
201
214
 
202
215
  // Copy the mesh out into JS-owned arrays (so it survives cleanup) and free the
@@ -885,7 +898,7 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
885
898
  _warnProfile: profileWarner.warn,
886
899
  // Free every WASM object created since the last cleanup EXCEPT solids the cache
887
900
  // still pins (they must survive for the next build to resume from them).
888
- cleanup: () => { for (const o of tracked) if (!cache.isPinned(o)) o.delete?.(); tracked.length = 0; },
901
+ cleanup: () => { for (const o of tracked) if (!cache.isPinned(o)) del(o); tracked.length = 0; },
889
902
  });
890
903
  return kernel;
891
904
  }