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.
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -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,
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
|
1068
|
-
|
|
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
package/src/framework/app.css
CHANGED
|
@@ -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,
|
|
391
|
-
opt-in classes for SVG: .pf-hit
|
|
392
|
-
dragged one — touch-action: none is
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
901
|
+
cleanup: () => { for (const o of tracked) if (!cache.isPinned(o)) del(o); tracked.length = 0; },
|
|
889
902
|
});
|
|
890
903
|
return kernel;
|
|
891
904
|
}
|