partforge 0.46.0 → 0.46.3
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
|
@@ -22,6 +22,24 @@ function btn(className, text, label) {
|
|
|
22
22
|
return b;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// A setter for an element's text that MUTATES its existing text node instead of
|
|
26
|
+
// replacing it. `el.textContent = x` always replaces the node, and WebKit will
|
|
27
|
+
// not dispatch a `click` on an element whose text node was replaced between
|
|
28
|
+
// mousedown and mouseup — so a label that can change while the user is pressing
|
|
29
|
+
// it must be written this way or the press is silently swallowed. Isolated in
|
|
30
|
+
// WebKit on a 120ms press: `textContent = <identical string>` loses the click,
|
|
31
|
+
// `node.data = <same>` never does, and neither does an attribute write.
|
|
32
|
+
//
|
|
33
|
+
// This is what makes the transport's glyph safe even when it legitimately flips
|
|
34
|
+
// mid-press (playback ending under the user's finger), which no amount of
|
|
35
|
+
// skipping redundant writes can cover.
|
|
36
|
+
function textSetter(element) {
|
|
37
|
+
const node = element.firstChild?.nodeType === 3
|
|
38
|
+
? element.firstChild
|
|
39
|
+
: element.appendChild(document.createTextNode(""));
|
|
40
|
+
return (value) => { if (node.data !== value) node.data = value; };
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
export function attachAnimationControls(viewer, part, { container, applyValues, getParamValues }) {
|
|
26
44
|
// A malformed animations block must degrade to "no transport bar", never a
|
|
27
45
|
// crashed mount — lint reports the specifics; the viewer just goes without.
|
|
@@ -92,19 +110,59 @@ export function attachAnimationControls(viewer, part, { container, applyValues,
|
|
|
92
110
|
}
|
|
93
111
|
}
|
|
94
112
|
|
|
113
|
+
// syncUi() runs on every playback frame. Two rules keep that from eating the
|
|
114
|
+
// user's clicks, and they are independent:
|
|
115
|
+
//
|
|
116
|
+
// 1. Text goes through textSetter (see above), so the button's text node is
|
|
117
|
+
// mutated and never replaced. This is the one that matters, because it
|
|
118
|
+
// holds even when the glyph legitimately changes under a held finger —
|
|
119
|
+
// playback ending, or a step boundary crossing, mid-press.
|
|
120
|
+
// 2. Each element has its own renderer that leaves early when its own value
|
|
121
|
+
// is unchanged, so a playing transport does no DOM work per frame at all.
|
|
122
|
+
// Per ELEMENT, not per write: driving several elements off one shared
|
|
123
|
+
// state key means a step change also redraws the button, which was still
|
|
124
|
+
// enough to lose the click before rule 1 was in place.
|
|
125
|
+
//
|
|
126
|
+
// Without rule 1 this cost the pause click outright: press, release, no click
|
|
127
|
+
// event at all, and only ever while playing — the one moment the button is
|
|
128
|
+
// for. Measured in WebKit, any press held >= 40ms lost it (a real click is
|
|
129
|
+
// ~100ms; a synthetic 0ms one survives, which is why automated clicking never
|
|
130
|
+
// saw it). Reset was never affected: nothing rewrites that button per frame.
|
|
131
|
+
const setPlayGlyph = textSetter(playBtn);
|
|
132
|
+
const setStepText = textSetter(stepLabel);
|
|
133
|
+
let shownActive = null;
|
|
134
|
+
let shownStep = null;
|
|
135
|
+
|
|
136
|
+
function renderPlayButton(active) {
|
|
137
|
+
if (active === shownActive) return;
|
|
138
|
+
shownActive = active;
|
|
139
|
+
setPlayGlyph(active ? "⏸" : "▶");
|
|
140
|
+
const label = active ? "Pause animation" : "Play animation";
|
|
141
|
+
playBtn.setAttribute("aria-label", label);
|
|
142
|
+
playBtn.title = label;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderStepLabel(stepIndex) {
|
|
146
|
+
if (current.steps.length <= 1 || stepIndex === shownStep) return;
|
|
147
|
+
shownStep = stepIndex;
|
|
148
|
+
const step = current.steps[stepIndex];
|
|
149
|
+
setStepText(`${stepIndex + 1}/${current.steps.length} · ${step.label}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
95
152
|
function syncUi() {
|
|
96
153
|
const { status, t, stepIndex } = playback.state();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
playBtn.title = playBtn.getAttribute("aria-label");
|
|
154
|
+
// The scrubber is the one thing that genuinely moves every frame. Assigning
|
|
155
|
+
// `.value` updates a property rather than replacing a child node, so it
|
|
156
|
+
// costs no clicks — and it is not what anyone reaches for mid-playback.
|
|
101
157
|
scrub.value = String(Math.round(t * 1000));
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
stepLabel.textContent = `${stepIndex + 1}/${current.steps.length} · ${step.label}`;
|
|
105
|
-
}
|
|
158
|
+
renderPlayButton(status === "playing" || status === "intro");
|
|
159
|
+
renderStepLabel(stepIndex);
|
|
106
160
|
}
|
|
107
161
|
|
|
162
|
+
// selectAnimation swaps in a fresh playback whose state can coincide with the
|
|
163
|
+
// outgoing one; the chrome still has to re-render for the new animation.
|
|
164
|
+
function invalidateUi() { shownActive = null; shownStep = null; }
|
|
165
|
+
|
|
108
166
|
// --- driver -----------------------------------------------------------------
|
|
109
167
|
// A frame that throws — a malformed cue or track that slipped past lint, a
|
|
110
168
|
// viewer that rejects a view name — must cost that frame, not the render
|
|
@@ -162,6 +220,8 @@ export function attachAnimationControls(viewer, part, { container, applyValues,
|
|
|
162
220
|
playback = createPlayback(current);
|
|
163
221
|
if (animations.length > 1) pick.value = name;
|
|
164
222
|
syncStructure();
|
|
223
|
+
invalidateUi(); // a fresh playback starts idle at step 0 — the same key the
|
|
224
|
+
// outgoing one may have been showing, but for another animation
|
|
165
225
|
syncUi();
|
|
166
226
|
}
|
|
167
227
|
|
|
@@ -312,7 +312,12 @@ export function createManifoldKernel(wasm, { quality = "preview" } = {}) {
|
|
|
312
312
|
return cached(h("revolve", pts._hash, degrees, segs), () => T(pts._cs.revolve(segs, degrees)));
|
|
313
313
|
return cached(h("revolve", pts, degrees, segs), () => T(Manifold.revolve([pts], segs, degrees)));
|
|
314
314
|
},
|
|
315
|
-
|
|
315
|
+
// A one-solid union is an identity — no new WASM / cache entry (avoids double-free):
|
|
316
|
+
// unionRaw's reduce returns the operand's own Manifold untouched, so caching it
|
|
317
|
+
// would pin one WASM object under two entries and eviction would dispose it twice.
|
|
318
|
+
union: (solids) => solids.length === 1
|
|
319
|
+
? solids[0]
|
|
320
|
+
: cached(h("union", solids.map((s) => s._hash)), () => unionRaw(solids.map((s) => s._m))),
|
|
316
321
|
shape2d,
|
|
317
322
|
beginSubPart: (name) => cache.begin(name),
|
|
318
323
|
endSubPart: () => cache.end(),
|
package/types/kernel.d.ts
CHANGED
|
@@ -267,6 +267,8 @@ export interface LoftOptions {
|
|
|
267
267
|
ruled?: boolean;
|
|
268
268
|
/** Capless loop — Manifold only. */
|
|
269
269
|
closed?: boolean;
|
|
270
|
+
/** Overrides the facet-vs-smooth shading inference. */
|
|
271
|
+
shading?: "smooth" | "faceted";
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
export interface SweepOptions {
|