partforge 0.46.3 → 0.46.4

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.46.3",
3
+ "version": "0.46.4",
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",
@@ -22,6 +22,20 @@ function btn(className, text, label) {
22
22
  return b;
23
23
  }
24
24
 
25
+ // Where the transport bar may sit, given the stage width, the bar's natural
26
+ // width, and the viewbar's left edge (all px, viewbarLeft stage-relative).
27
+ // null → the CSS default (centered) already clears the viewbar. Otherwise
28
+ // inline overrides: `left` slides the bar toward the stage's `margin`, and
29
+ // when even that isn't enough, `maxWidth` caps the bar so the `gap` holds.
30
+ export function planAnimBarPlacement({ stageWidth, barWidth, viewbarLeft }, { gap = 10, margin = 12 } = {}) {
31
+ const centeredLeft = (stageWidth - barWidth) / 2;
32
+ const limit = viewbarLeft - gap - barWidth;
33
+ if (centeredLeft <= limit) return null;
34
+ const left = Math.max(margin, limit);
35
+ const available = Math.max(0, viewbarLeft - gap - margin);
36
+ return barWidth > available ? { left, maxWidth: available } : { left };
37
+ }
38
+
25
39
  // A setter for an element's text that MUTATES its existing text node instead of
26
40
  // replacing it. `el.textContent = x` always replaces the node, and WebKit will
27
41
  // not dispatch a `click` on an element whose text node was replaced between
@@ -260,6 +274,65 @@ export function attachAnimationControls(viewer, part, { container, applyValues,
260
274
  syncStructure();
261
275
  syncUi();
262
276
 
277
+ // --- placement: keep clear of the viewbar ---------------------------------
278
+ // chrome.css centers the bar (left: 50% / translateX(-50%)), and nothing in
279
+ // CSS can stop that centered position sliding under #viewbar when the stage
280
+ // narrows — the viewbar's width is dynamic (cutaway's Flip/Reset appear and
281
+ // disappear), so a static reservation would either overlap or waste centre
282
+ // space. Measure instead: when the two bars' vertical bands intersect, clamp
283
+ // the bar's left so a 10px gap to the viewbar holds, capping its width if
284
+ // even the stage's 12px margin isn't enough. Overrides are inline and
285
+ // cleared at the top of every pass, so chrome.css (or a host that
286
+ // re-anchors either bar out of the shared band) stays authoritative the
287
+ // moment the constraint stops binding. The clear-measure-apply sequence is
288
+ // loop-safe: it settles within one frame, so ResizeObserver — which reports
289
+ // rendered sizes at frame boundaries — never sees the intermediate state.
290
+ const viewbarEl = container.querySelector("#viewbar");
291
+ let placementRaf = 0;
292
+ function applyPlacement() {
293
+ placementRaf = 0;
294
+ bar.style.left = "";
295
+ bar.style.transform = "";
296
+ bar.style.maxWidth = "";
297
+ bar.style.overflow = "";
298
+ const vb = viewbarEl?.getBoundingClientRect();
299
+ const barRect = bar.getBoundingClientRect();
300
+ if (!vb || barRect.top >= vb.bottom || barRect.bottom <= vb.top) return;
301
+ const stageRect = container.getBoundingClientRect();
302
+ const plan = planAnimBarPlacement({
303
+ stageWidth: stageRect.width,
304
+ barWidth: barRect.width,
305
+ viewbarLeft: vb.left - stageRect.left,
306
+ });
307
+ if (!plan) return;
308
+ bar.style.left = `${plan.left}px`;
309
+ bar.style.transform = "none";
310
+ // maxWidth is the last resort, only reached when even the margin can't
311
+ // hold the gap — and the bar's flex children have hard minimums (~320px)
312
+ // that don't shrink to fit a tighter cap. overflow:hidden only applies
313
+ // here, inline, because a static rule in app.css would also clip the ⓘ
314
+ // info popover in the (far more common) uncapped state.
315
+ if (plan.maxWidth != null) {
316
+ bar.style.maxWidth = `${plan.maxWidth}px`;
317
+ bar.style.overflow = "hidden";
318
+ }
319
+ }
320
+ function schedulePlacement() {
321
+ if (typeof requestAnimationFrame !== "function") return applyPlacement();
322
+ if (!placementRaf) placementRaf = requestAnimationFrame(applyPlacement);
323
+ }
324
+ // Observing the bar itself catches content-driven width changes (step label
325
+ // text, animation switch); the viewbar, cutaway's actions; the stage, rail
326
+ // drags and window resizes.
327
+ const placementObserver = typeof ResizeObserver === "function"
328
+ ? new ResizeObserver(schedulePlacement) : null;
329
+ if (placementObserver) {
330
+ placementObserver.observe(container);
331
+ placementObserver.observe(bar);
332
+ if (viewbarEl) placementObserver.observe(viewbarEl);
333
+ }
334
+ schedulePlacement();
335
+
263
336
  const runtime = {
264
337
  // An unknown name is a host bug, not a request to play whatever happens to
265
338
  // be selected — say so and do nothing rather than silently animating
@@ -306,6 +379,8 @@ export function attachAnimationControls(viewer, part, { container, applyValues,
306
379
  pick.removeEventListener("change", onPick);
307
380
  resetBtn.removeEventListener("click", onResetClick);
308
381
  info.dispose();
382
+ placementObserver?.disconnect();
383
+ if (placementRaf && typeof cancelAnimationFrame === "function") cancelAnimationFrame(placementRaf);
309
384
  bar.remove();
310
385
  },
311
386
  __viewer: viewer, // test hook only
@@ -222,6 +222,9 @@ button.action:focus-visible, .adv-toggle:focus-visible, #viewbar button:focus-vi
222
222
  .pf-anim-bar {
223
223
  display: flex; align-items: center; gap: 8px;
224
224
  padding: 6px 10px;
225
+ /* Never shorter than #viewbar's card sharing the same bottom edge:
226
+ 34px button + 2×4px padding + 2×1px border (border-box). */
227
+ min-height: 44px;
225
228
  background: var(--pf-surface); border: 1px solid var(--pf-border);
226
229
  border-radius: var(--pf-radius-control); box-shadow: var(--pf-shadow-float);
227
230
  }
@@ -249,6 +252,11 @@ button.action:focus-visible, .adv-toggle:focus-visible, #viewbar button:focus-vi
249
252
  background: var(--pf-muted); pointer-events: none;
250
253
  }
251
254
 
255
+ @media (max-width: 360px) {
256
+ /* #viewbar's card is 40px here (30px buttons) — keep the floor in step. */
257
+ .pf-anim-bar { min-height: 40px; }
258
+ }
259
+
252
260
  /* Legacy id-only markup only: classed markup's viewbar lives inside .pf-stage
253
261
  (bottom-right, see chrome.css's .pf-float-viewbar) so it never meets the
254
262
  top-left floating #panel card. Legacy markup still floats #viewbar top-right
@@ -163,7 +163,10 @@
163
163
 
164
164
  /* --- animation transport bar (generated by animation-controls.js) ----------
165
165
  PLACEMENT ONLY, per the rule above; appearance lives in app.css next to
166
- #viewbar's, so a host that re-anchors this bar still inherits its chrome. */
166
+ #viewbar's, so a host that re-anchors this bar still inherits its chrome.
167
+ animation-controls.js may inline-override left/transform/max-width (and
168
+ overflow while width-capped) to hold a 10px gap to #viewbar, and clears
169
+ the overrides whenever centered placement fits. */
167
170
  .pf-anim-bar {
168
171
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
169
172
  z-index: 15; max-width: calc(100% - 24px);