fix(library): drop the leftover zoom in the mark editor, re-anchor the framed crop

Two zoom-interaction bugs in the library viewer:

- Opening the WATERMARK editor now resets a photo zoom left over from the
  panel-closed pass. The zoom had shifted the mark's hit box along with the
  matrix and tripped the `libZoom.s <= 1.01` guards, so after visiting another
  tab and zooming, the mark could neither be dragged nor pinched.
- A polaroid/wall photo window that MOVES (deck grows: a chip slider or the
  watermark text field) now re-anchors the photo transform by the same u/v the
  export uses, so the crop inside the frame no longer slides away from the card
  (at 2.5x the content used to drift 2.5x the card's move).

Verified on device (9a6a7277): the mark drags again after tab-switch + zoom;
opening WATERMARK returns the photo to the pre-zoom framing; the framed crop
tracks the card within ~2 px over a 62 px deck-driven move.
This commit is contained in:
2026-09-10 17:40:50 +07:00
parent f4a5b0c683
commit f13fd8e0f3
+35
View File
@@ -975,6 +975,15 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
// point / double-tap again to reset to 100%. Transform is a Skia matrix
// P' = scale*P + (tx,ty); gesture math keeps the focal point stationary.
const [libZoom, setLibZoom] = useState({ s: 1, tx: 0, ty: 0 });
// The mark editor works in the UN-zoomed composition: a photo zoom left over
// from the panel-closed pass (zoom in to check a slider, then come back to
// WATERMARK) shifted the mark's hit box with the matrix AND tripped the
// `libZoom.s <= 1.01` guards, so the mark could neither be dragged nor
// pinched. Opening the editor therefore drops the photo zoom.
useEffect(() => {
if (wmEditing) setLibZoom((z) => (z.s <= 1.001 ? z : { s: 1, tx: 0, ty: 0 }));
}, [wmEditing]);
const pinchRef = useRef<{ d: number; s: number; tx: number; ty: number; fx: number; fy: number } | null>(null);
const pinchUsedRef = useRef(false);
// Framed-window drag: anchor at touch-down (finger + transform origin), so a
@@ -1054,6 +1063,32 @@ const Viewfinder = forwardRef<ViewfinderHandle, ViewfinderProps>(function Viewfi
if (selectedFrame === 'wallframe' && wallGeo && wallImage) return wallGeo.window;
return null;
};
// Framed photo windows move when the deck grows (slider) or shrinks: the
// photo content is scaled about its own top-left, so a plain move of the
// window under a zoomed photo slides the visible crop away from the card.
// Re-anchor the transform when the window rect changes so the SAME part of
// the photo stays inside the frame (same u/v math the export uses).
const winRectRef = useRef<{ x: number; y: number; w: number; h: number } | null>(null);
useEffect(() => {
const win = zoomWindowRect();
const prev = winRectRef.current;
winRectRef.current = win ? { ...win } : null;
if (!prev || !win) return;
if (prev.x === win.x && prev.y === win.y && prev.w === win.w && prev.h === win.h) return;
setLibZoom((z) => {
if (z.s <= 1.001) return z;
const tlx = z.tx + (z.s - 1) * prev.x;
const tly = z.ty + (z.s - 1) * prev.y;
const u = (prev.w / 2 - tlx) / (z.s * prev.w);
const v = (prev.h / 2 - tly) / (z.s * prev.h);
return {
s: z.s,
tx: win.w / 2 - u * z.s * win.w - (z.s - 1) * win.x,
ty: win.h / 2 - v * z.s * win.h - (z.s - 1) * win.y,
};
});
});
// Clamp pan/zoom so the photo always keeps COVERING the framed window (never
// drags far enough to reveal the card/mat through the transparent opening).
// Transform is P' = s*P + t in canvas coords; with the photo drawn over the