partforge 0.56.0 → 0.57.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.56.0",
3
+ "version": "0.57.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",
@@ -249,11 +249,19 @@ export function attachAnimationControls(viewer, part, { container, applyValues,
249
249
  scrubWrap.addEventListener("pointerleave", onWrapPointerLeave);
250
250
  scrubWrap.addEventListener("pointercancel", onWrapPointerLeave);
251
251
 
252
+ // Late-bound hook into the placement section below (a plain call would hit
253
+ // the TDZ on its `let placementRaf` — setup invokes syncStructure before the
254
+ // placement block runs). Wired to schedulePlacement once that block exists,
255
+ // so a view switch that shows/hides the bar re-publishes --pf-anim-clear
256
+ // even where ResizeObserver is absent.
257
+ let onStructureChanged = null;
258
+
252
259
  // Per-view + per-animation chrome: which chooser shows, the picker's options,
253
260
  // title, ⓘ description, pager labels, scrubber ticks. A view with no
254
261
  // animations hides the whole bar rather than showing an empty transport.
255
262
  function syncStructure() {
256
263
  bar.style.display = current ? "" : "none";
264
+ onStructureChanged?.();
257
265
  hideChapterBubble();
258
266
  if (!current) return;
259
267
  const paged = animations.length > 1;
@@ -536,10 +544,20 @@ export function attachAnimationControls(viewer, part, { container, applyValues,
536
544
  bar.style.maxWidth = "";
537
545
  bar.style.overflow = "";
538
546
  bar.classList.remove("pf-squeezed");
539
- const vb = viewbarEl?.getBoundingClientRect();
547
+ const stageRect = container.getBoundingClientRect();
540
548
  const barRect = bar.getBoundingClientRect();
549
+ // Publish the bar's vertical claim on the stage as --pf-anim-clear: the
550
+ // distance from the stage's bottom edge to the bar's top, 0px when the bar
551
+ // is hidden (a view with no animations). Hosts that float their own chrome
552
+ // at the stage's bottom-centre (partforge-cloud's status/forging stack)
553
+ // read it to sit above the bar instead of under it; with no bar mounted
554
+ // the property is never set and a var() fallback of 0px applies.
555
+ const clear = bar.style.display === "none"
556
+ ? 0
557
+ : Math.max(0, Math.round(stageRect.bottom - barRect.top));
558
+ container.style.setProperty("--pf-anim-clear", `${clear}px`);
559
+ const vb = viewbarEl?.getBoundingClientRect();
541
560
  if (!vb || barRect.top >= vb.bottom || barRect.bottom <= vb.top) return;
542
- const stageRect = container.getBoundingClientRect();
543
561
  const plan = planAnimBarPlacement({
544
562
  stageWidth: stageRect.width,
545
563
  barWidth: barRect.width,
@@ -581,6 +599,7 @@ export function attachAnimationControls(viewer, part, { container, applyValues,
581
599
  placementObserver.observe(bar);
582
600
  if (viewbarEl) placementObserver.observe(viewbarEl);
583
601
  }
602
+ onStructureChanged = schedulePlacement; // see the hook's declaration above
584
603
  schedulePlacement();
585
604
 
586
605
  const runtime = {
@@ -662,6 +681,7 @@ export function attachAnimationControls(viewer, part, { container, applyValues,
662
681
  scrubWrap.removeEventListener("pointercancel", onWrapPointerLeave);
663
682
  hideChapterBubble(); // also clears hoverInside
664
683
  chapterBubble.remove(); // a stage child, so the bar taking itself out misses it
684
+ container.style.removeProperty("--pf-anim-clear"); // no bar, no claim
665
685
  bar.remove();
666
686
  },
667
687
  __viewer: viewer, // test hook only
@@ -234,7 +234,13 @@
234
234
  #viewbar's, so a host that re-anchors this bar still inherits its chrome.
235
235
  animation-controls.js may inline-override left/transform/max-width (and
236
236
  overflow while width-capped) to hold a 10px gap to #viewbar, and clears
237
- the overrides whenever centered placement fits. */
237
+ the overrides whenever centered placement fits.
238
+
239
+ animation-controls.js also publishes --pf-anim-clear on the STAGE element:
240
+ the px distance from the stage's bottom edge to the visible bar's top (0px
241
+ while the active view has no animations; unset when no view declares any).
242
+ A host floating its own chrome at the stage's bottom-centre should anchor it
243
+ at calc(var(--pf-anim-clear, 0px) + <gap>) to stack above the bar. */
238
244
  .pf-anim-bar {
239
245
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
240
246
  z-index: 15; max-width: calc(100% - 24px);
@@ -453,8 +453,10 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
453
453
  if (needed.every(isCurrent)) {
454
454
  showView(needed);
455
455
  ui.setExportEnabled(true);
456
- const tris = needed.reduce((s, n) => s + viewer.subTriangles(n), 0);
457
- ui.setStatus(`${tris.toLocaleString()} triangles`);
456
+ // The line used to read "N triangles" here; that debug readout now goes
457
+ // to the console (see the `meshes` case). Clearing keeps a stale
458
+ // "phase…" from outliving the build it described.
459
+ ui.setStatus("");
458
460
  } else if (needed.every((n) => viewer.hasSubMesh(n))) {
459
461
  showView(needed); // stale but present — keep it visible during regenerate
460
462
  ui.setExportEnabled(false);
@@ -498,7 +500,8 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
498
500
  ui.hideBusy();
499
501
  refreshView();
500
502
  if (data.ms && missingParts().length === 0) {
501
- ui.setStatus(`${ui.statusText()} · ${(data.ms / 1000).toFixed(1)} s`);
503
+ const tris = viewSubParts(part, view(), params).reduce((s, n) => s + viewer.subTriangles(n), 0);
504
+ console.debug(`partforge: built ${tris.toLocaleString()} triangles in ${(data.ms / 1000).toFixed(1)} s`);
502
505
  }
503
506
  dbg?.update({ ms: data.ms, hits: data.cache?.hits ?? 0, misses: data.cache?.misses ?? 0, skipped: lastGen.skipped, rebuilt: lastGen.rebuilt, posed: lastGen.posed });
504
507
  onBuild?.({ status: "success", ms: data.ms });
@@ -545,8 +548,10 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
545
548
  case "error":
546
549
  loop.buildDone();
547
550
  ui.hideBusy();
548
- ui.setStatus(`failed: ${data.message}`, true);
551
+ // refreshView FIRST: its all-current branch clears the status line,
552
+ // so writing the failure after it keeps the message on screen.
549
553
  refreshView();
554
+ ui.setStatus(`failed: ${data.message}`, true);
550
555
  onBuild?.({ status: "error", error: data.message });
551
556
  if (!readySettled) { readySettled = true; rejectReady(new Error(data.message)); }
552
557
  break;
@@ -10,6 +10,5 @@ export function createStatusUi({ status, busy, phase, exports = [] }) {
10
10
  showBusy(p) { phase.textContent = `${p}…`; busy.classList.add("show"); },
11
11
  hideBusy() { busy.classList.remove("show"); },
12
12
  setExportEnabled(on) { exportBtns.forEach((b) => { b.disabled = !on; }); },
13
- statusText: () => status.textContent,
14
13
  };
15
14
  }