partforge 0.104.0 → 0.105.1
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
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// active — getView() is the driver's only window onto it.
|
|
13
13
|
import { viewAnimations, createPlayback, stepIndexAt } from "./animation.js";
|
|
14
14
|
import { createInfoPopover, attachInfo } from "./controls.js";
|
|
15
|
+
import { stageClearFor } from "./stage-clear.js";
|
|
15
16
|
|
|
16
17
|
function el(tag, className, text) {
|
|
17
18
|
const node = document.createElement(tag);
|
|
@@ -237,7 +238,16 @@ export function attachAnimationControls(viewer, part, {
|
|
|
237
238
|
const nextAnimBtn = btn("pf-anim-page", "›", "Next animation");
|
|
238
239
|
bar.append(prevAnimBtn, pick, title);
|
|
239
240
|
const infoSlot = el("span", "pf-anim-info");
|
|
240
|
-
|
|
241
|
+
// Inline SVGs, not the ▶/⏸ characters: U+23F8 takes its EMOJI presentation
|
|
242
|
+
// on iOS, so the pause glyph rendered as a color sticker in an otherwise
|
|
243
|
+
// monochrome bar. Both glyphs are PERSISTENT children toggled by the
|
|
244
|
+
// data-playing attribute (app.css) — the WebKit click-loss rule below
|
|
245
|
+
// forbids replacing the button's children while a press may be down, and an
|
|
246
|
+
// attribute write never eats a click.
|
|
247
|
+
const playBtn = btn("pf-anim-play", null, "Play animation");
|
|
248
|
+
playBtn.innerHTML =
|
|
249
|
+
'<svg class="pf-anim-glyph-play" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M7 4.5v15l13-7.5z"/></svg>'
|
|
250
|
+
+ '<svg class="pf-anim-glyph-pause" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M7 4.5h3.4v15H7zM13.6 4.5H17v15h-3.6z"/></svg>';
|
|
241
251
|
const scrubWrap = el("span", "pf-anim-scrub-wrap");
|
|
242
252
|
const scrub = document.createElement("input");
|
|
243
253
|
scrub.type = "range";
|
|
@@ -404,10 +414,12 @@ export function attachAnimationControls(viewer, part, {
|
|
|
404
414
|
// syncUi() runs on every playback frame. Two rules keep that from eating the
|
|
405
415
|
// user's clicks, and they are independent:
|
|
406
416
|
//
|
|
407
|
-
// 1.
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
417
|
+
// 1. The play button's glyph flip is an ATTRIBUTE toggle over two persistent
|
|
418
|
+
// SVG children (app.css keys which one shows on [data-playing]) — its
|
|
419
|
+
// children are never touched after construction. This is the one that
|
|
420
|
+
// matters, because it holds even when the glyph legitimately changes
|
|
421
|
+
// under a held finger — playback ending mid-press. (The bubble's text
|
|
422
|
+
// still goes through textSetter, the text-node form of the same rule.)
|
|
411
423
|
// 2. Each element has its own renderer that leaves early when its own value
|
|
412
424
|
// is unchanged, so a playing transport touches only what actually moved:
|
|
413
425
|
// the scrubber's value, and aria-valuetext when it crosses a reporting
|
|
@@ -420,13 +432,12 @@ export function attachAnimationControls(viewer, part, {
|
|
|
420
432
|
// for. Measured in WebKit, any press held >= 40ms lost it (a real click is
|
|
421
433
|
// ~100ms; a synthetic 0ms one survives, which is why automated clicking never
|
|
422
434
|
// saw it). Reset was never affected: nothing rewrites that button per frame.
|
|
423
|
-
const setPlayGlyph = textSetter(playBtn);
|
|
424
435
|
let shownActive = null;
|
|
425
436
|
|
|
426
437
|
function renderPlayButton(active) {
|
|
427
438
|
if (active === shownActive) return;
|
|
428
439
|
shownActive = active;
|
|
429
|
-
|
|
440
|
+
playBtn.toggleAttribute("data-playing", active);
|
|
430
441
|
const label = active ? "Pause animation" : "Play animation";
|
|
431
442
|
playBtn.setAttribute("aria-label", label);
|
|
432
443
|
playBtn.title = label;
|
|
@@ -665,13 +676,14 @@ export function attachAnimationControls(viewer, part, {
|
|
|
665
676
|
const barRect = bar.getBoundingClientRect();
|
|
666
677
|
// Publish the bar's vertical claim on the stage as --pf-anim-clear: the
|
|
667
678
|
// distance from the stage's bottom edge to the bar's top, 0px when the bar
|
|
668
|
-
// is hidden (a view with no animations
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
//
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
679
|
+
// is hidden — by us (a view with no animations, sketch mode: the inline
|
|
680
|
+
// display) OR by a host stylesheet, which leaves our inline display alone
|
|
681
|
+
// and empties the bar's box, the case stageClearFor tests. Hosts that
|
|
682
|
+
// float their own chrome at the stage's bottom-centre (partforge-cloud's
|
|
683
|
+
// status/forging stack) read it to sit above the bar instead of under it;
|
|
684
|
+
// with no bar mounted the property is never set and a var() fallback of
|
|
685
|
+
// 0px applies.
|
|
686
|
+
const clear = bar.style.display === "none" ? 0 : stageClearFor(stageRect, barRect);
|
|
675
687
|
container.style.setProperty("--pf-anim-clear", `${clear}px`);
|
|
676
688
|
const cubeEl = container.querySelector(cubeSelector);
|
|
677
689
|
const viewbarRect = viewbarEl?.getBoundingClientRect() ?? null;
|
|
@@ -692,8 +704,8 @@ export function attachAnimationControls(viewer, part, {
|
|
|
692
704
|
// back in here.
|
|
693
705
|
function isCrowded({ stageRect, barRect, viewbarRect, cubeRect, cubeEl }) {
|
|
694
706
|
// A bar that is not on screen cannot be crowded by anything — the same
|
|
695
|
-
//
|
|
696
|
-
if (bar.style.display === "none") return false;
|
|
707
|
+
// two conditions the --pf-anim-clear calculation above uses.
|
|
708
|
+
if (bar.style.display === "none" || barRect.height === 0) return false;
|
|
697
709
|
const nominal = nominalClusterRect(viewbarRect, cubeRect, publishedSize(cubeEl));
|
|
698
710
|
// Same vertical-band test the placement path applies, against the nominal
|
|
699
711
|
// rect: bands that do not intersect cannot collide, so there is nothing to
|
package/src/framework/app.css
CHANGED
|
@@ -737,6 +737,14 @@ button.action:focus-visible, .adv-toggle:focus-visible, .sec-title:focus-visible
|
|
|
737
737
|
cursor: pointer; font-size: 13px; padding: 2px 4px;
|
|
738
738
|
}
|
|
739
739
|
.pf-anim-bar button:hover { color: var(--pf-text-2); }
|
|
740
|
+
/* Play/pause is two PERSISTENT inline SVGs toggled by [data-playing] — never
|
|
741
|
+
the ▶/⏸ characters (U+23F8 renders as a color emoji on iOS) and never a
|
|
742
|
+
child swap (WebKit drops a click whose press started on a replaced node —
|
|
743
|
+
see animation-controls.js). */
|
|
744
|
+
.pf-anim-play { display: inline-flex; align-items: center; justify-content: center; }
|
|
745
|
+
.pf-anim-play .pf-anim-glyph-pause { display: none; }
|
|
746
|
+
.pf-anim-play[data-playing] .pf-anim-glyph-play { display: none; }
|
|
747
|
+
.pf-anim-play[data-playing] .pf-anim-glyph-pause { display: block; }
|
|
740
748
|
.pf-anim-title, .pf-anim-pick {
|
|
741
749
|
font-family: var(--pf-mono); font-size: 11px; color: var(--pf-text-2);
|
|
742
750
|
}
|
|
@@ -845,6 +853,20 @@ button.action:focus-visible, .adv-toggle:focus-visible, .sec-title:focus-visible
|
|
|
845
853
|
.pf-anim-scrub { min-height: 44px; }
|
|
846
854
|
}
|
|
847
855
|
|
|
856
|
+
/* On a phone the full transport is a two-row player squatting on a small
|
|
857
|
+
stage. Collapse it to the decisions that matter — which animation, and
|
|
858
|
+
play/pause: the timeline, reset and ⓘ stand down below the narrow
|
|
859
|
+
breakpoint. WIDTH-keyed, deliberately not the coarse-pointer condition the
|
|
860
|
+
target-size block above shares: a coarse-pointer iPad has the room and
|
|
861
|
+
keeps the full transport; only the small screen loses the player bar.
|
|
862
|
+
Playback itself still visits every chapter; scrubbing wants a real
|
|
863
|
+
screen. */
|
|
864
|
+
@media (max-width: 719px) {
|
|
865
|
+
.pf-anim-bar .pf-anim-scrub-wrap,
|
|
866
|
+
.pf-anim-bar .pf-anim-reset,
|
|
867
|
+
.pf-anim-bar .pf-anim-info { display: none; }
|
|
868
|
+
}
|
|
869
|
+
|
|
848
870
|
/* Legacy id-only markup only: classed markup's viewbar lives inside .pf-stage
|
|
849
871
|
(bottom-right, see chrome.css's .pf-float-viewbar) so it never meets the
|
|
850
872
|
top-left floating #panel card. Legacy markup still floats #viewbar top-right
|
package/src/framework/mount.js
CHANGED
|
@@ -33,6 +33,7 @@ import { createAnnotateMode } from "./annotate/annotate-mode.js";
|
|
|
33
33
|
import { attachAnnotateControls } from "./annotate/annotate-controls.js";
|
|
34
34
|
import { attachSketchToolbar } from "./annotate/sketch-toolbar.js";
|
|
35
35
|
import { attachViewcubeControls } from "./viewcube/viewcube-controls.js";
|
|
36
|
+
import { stageClearFor } from "./stage-clear.js";
|
|
36
37
|
|
|
37
38
|
// The mount handle, factored out so its shape is unit-testable without booting
|
|
38
39
|
// the full mount() pipeline (WASM + workers + DOM).
|
|
@@ -599,16 +600,14 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
|
|
|
599
600
|
}
|
|
600
601
|
// Publish the viewbar's vertical claim so chrome.css can stack the cube on
|
|
601
602
|
// top of it without hardcoding a height that cutaway/measure/annotate
|
|
602
|
-
// action rows can change.
|
|
603
|
+
// action rows can change. A hidden bar (ours or a host stylesheet's)
|
|
604
|
+
// claims 0 — stage-clear.js has the why; the observer fires on the hide
|
|
605
|
+
// itself, since the bar's box goes to zero.
|
|
603
606
|
const viewbarEl = els.viewer.querySelector("#viewbar");
|
|
604
607
|
if (viewbarEl && typeof ResizeObserver === "function") {
|
|
605
608
|
const publishViewbarClear = () => {
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
els.viewer.style.setProperty(
|
|
609
|
-
"--pf-viewbar-clear",
|
|
610
|
-
`${Math.max(0, Math.round(stageRect.bottom - barRect.top))}px`,
|
|
611
|
-
);
|
|
609
|
+
const clear = stageClearFor(els.viewer.getBoundingClientRect(), viewbarEl.getBoundingClientRect());
|
|
610
|
+
els.viewer.style.setProperty("--pf-viewbar-clear", `${clear}px`);
|
|
612
611
|
};
|
|
613
612
|
const viewbarObserver = new ResizeObserver(publishViewbarClear);
|
|
614
613
|
viewbarObserver.observe(viewbarEl);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The one arithmetic behind the stage's two "clearance" custom properties —
|
|
2
|
+
// --pf-viewbar-clear (mount.js) and --pf-anim-clear (animation-controls.js):
|
|
3
|
+
// how far up from the stage's bottom edge a floating bar reaches, so chrome
|
|
4
|
+
// that stacks above it (the view cube, a host's status pills) can sit clear.
|
|
5
|
+
//
|
|
6
|
+
// A bar with NO BOX claims nothing. `getBoundingClientRect()` on a
|
|
7
|
+
// `display: none` element is all zeros, and `stageRect.bottom - 0` is the
|
|
8
|
+
// stage's bottom edge in viewport pixels — the whole stage height, or more.
|
|
9
|
+
// Publishing that would fling everything stacked on the bar off the top of
|
|
10
|
+
// the stage. The inline-style check the anim bar used to make
|
|
11
|
+
// (`bar.style.display === "none"`) only saw its own hide; a host hiding the
|
|
12
|
+
// bar from a stylesheet (partforge-cloud's part-open sheet does exactly that
|
|
13
|
+
// to #viewbar and .pf-anim-bar) left the publishers measuring the zero rect.
|
|
14
|
+
// The rect itself is the test: hidden by anyone, for any reason, it is empty.
|
|
15
|
+
export function stageClearFor(stageRect, barRect) {
|
|
16
|
+
if (!barRect || barRect.height === 0) return 0;
|
|
17
|
+
return Math.max(0, Math.round(stageRect.bottom - barRect.top));
|
|
18
|
+
}
|