feat(stammbaum): drive viewBox from PanZoomState (pan + zoom) (#692)

Replace the scalar zoom prop with a {x,y,z} PanZoomState. The viewBox centre
is offset by the pan and width/height scaled by zoom; the default {0,0,1}
frames the whole tree (fit-to-screen). Page header buttons now step view.z
through clampZoom over the resolved 0.25–3.0 range.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-29 16:35:49 +02:00
parent 197b668f20
commit 0422af8980
4 changed files with 77 additions and 36 deletions

View File

@@ -8,6 +8,7 @@ import {
ROW_GAP,
type Layout
} from '$lib/person/genealogy/layout/buildLayout';
import type { PanZoomState } from '$lib/person/genealogy/panZoom';
type PersonNodeDTO = components['schemas']['PersonNodeDTO'];
type RelationshipDTO = components['schemas']['RelationshipDTO'];
@@ -16,7 +17,7 @@ interface Props {
nodes: PersonNodeDTO[];
edges: RelationshipDTO[];
selectedId: string | null;
zoom: number;
panZoom: PanZoomState;
onSelect: (id: string) => void;
/**
* Force-show or force-hide the generation gutter. When undefined, falls
@@ -27,7 +28,7 @@ interface Props {
showGutter?: boolean;
}
let { nodes, edges, selectedId, zoom, onSelect, showGutter }: Props = $props();
let { nodes, edges, selectedId, panZoom, onSelect, showGutter }: Props = $props();
const layout = $derived.by<Layout>(() => buildLayout(nodes, edges));
@@ -79,12 +80,20 @@ const gutterRows = $derived.by<GutterRow[]>(() => {
return rows;
});
// Base viewBox geometry at z=1, no pan — the whole tree framed (#692). Pan
// offsets shift the centre; zoom scales width/height inversely. The default
// {x:0,y:0,z:1} therefore fits the tree to the element (fit-to-screen).
const baseDims = $derived({ w: layout.viewW + gutterWidth, h: layout.viewH });
const baseCentre = $derived({
x: layout.viewX - gutterWidth + baseDims.w / 2,
y: layout.viewY + layout.viewH / 2
});
const viewBox = $derived.by(() => {
const totalW = layout.viewW + gutterWidth;
const w = totalW / zoom;
const h = layout.viewH / zoom;
const cx = layout.viewX - gutterWidth + totalW / 2;
const cy = layout.viewY + layout.viewH / 2;
const w = baseDims.w / panZoom.z;
const h = baseDims.h / panZoom.z;
const cx = baseCentre.x + panZoom.x;
const cy = baseCentre.y + panZoom.y;
return `${cx - w / 2} ${cy - h / 2} ${w} ${h}`;
});