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

@@ -36,12 +36,35 @@ function rectsCentroid(svg: SVGElement): { x: number; y: number } {
}
describe('StammbaumTree viewBox', () => {
it('offsets the viewBox origin by the pan state (#692)', async () => {
render(StammbaumTree, {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
edges: [],
selectedId: null,
panZoom: { x: 100, y: 40, z: 1 },
showGutter: false,
onSelect: () => {}
});
const svg = document.querySelector('svg')!;
const [x, y, w, h] = parseViewBox(svg);
// Same dimensions as the unpanned default (z=1)…
expect(w).toBe(1200);
expect(h).toBe(800);
// …but the origin is shifted by the pan offset.
const unpannedX = -(1200 / 2 - 160 / 2); // single 160-wide node centred
expect(x).toBeCloseTo(unpannedX + 100, 6);
expect(y).toBeCloseTo(-(800 / 2 - 56 / 2) + 40, 6);
});
it('uses the minimum size and centers a single node', async () => {
render(StammbaumTree, {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -114,7 +137,7 @@ describe('StammbaumTree viewBox', () => {
}
],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -174,7 +197,7 @@ describe('StammbaumTree viewBox', () => {
}
],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -277,7 +300,7 @@ describe('StammbaumTree viewBox', () => {
}
],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -335,7 +358,7 @@ describe('StammbaumTree viewBox', () => {
}
],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -368,7 +391,7 @@ describe('StammbaumTree viewBox', () => {
}
],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -393,7 +416,7 @@ describe('StammbaumTree node rendering branches', () => {
],
edges: [],
selectedId: ID_A,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -409,7 +432,7 @@ describe('StammbaumTree node rendering branches', () => {
],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -422,7 +445,7 @@ describe('StammbaumTree node rendering branches', () => {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true, deathYear: 1950 }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -434,7 +457,7 @@ describe('StammbaumTree node rendering branches', () => {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -447,7 +470,7 @@ describe('StammbaumTree node rendering branches', () => {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect
});
@@ -462,7 +485,7 @@ describe('StammbaumTree node rendering branches', () => {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect
});
@@ -478,7 +501,7 @@ describe('StammbaumTree node rendering branches', () => {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect
});
@@ -493,7 +516,7 @@ describe('StammbaumTree node rendering branches', () => {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect
});
@@ -520,7 +543,7 @@ describe('StammbaumTree node rendering branches', () => {
}
],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -547,7 +570,7 @@ describe('StammbaumTree node rendering branches', () => {
}
],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -575,7 +598,7 @@ describe('StammbaumTree node rendering branches', () => {
}
],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -588,7 +611,7 @@ describe('StammbaumTree node rendering branches', () => {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -607,7 +630,7 @@ describe('StammbaumTree node rendering branches', () => {
nodes: [{ id: ID_A, displayName: 'Anna', familyMember: true }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -636,7 +659,7 @@ describe('StammbaumTree node rendering branches', () => {
],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -653,7 +676,7 @@ describe('StammbaumTree node rendering branches', () => {
],
edges: [],
selectedId: ID_A,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -674,7 +697,7 @@ describe('StammbaumTree node rendering branches', () => {
],
edges: [],
selectedId: ID_A,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {}
});
@@ -696,7 +719,7 @@ describe('StammbaumTree generation gutter (#689)', () => {
],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {},
showGutter: true
});
@@ -713,7 +736,7 @@ describe('StammbaumTree generation gutter (#689)', () => {
nodes: [{ id: ID_A, displayName: 'Herbert', familyMember: true, generation: 3 }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {},
showGutter: true
});
@@ -730,7 +753,7 @@ describe('StammbaumTree generation gutter (#689)', () => {
nodes: [{ id: ID_A, displayName: 'Herbert', familyMember: true, generation: 3 }],
edges: [],
selectedId: null,
zoom: 1,
panZoom: { x: 0, y: 0, z: 1 },
onSelect: () => {},
showGutter: false
});