feat(stammbaum): centroid-anchored zoom (zoomAtPoint) (#692)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
parsePanZoomParams,
|
||||
serializePanZoomParams,
|
||||
screenDeltaToSvg,
|
||||
zoomAtPoint,
|
||||
DEFAULT_VIEW,
|
||||
DEFAULT_ZOOM,
|
||||
MIN_ZOOM,
|
||||
@@ -91,3 +92,31 @@ describe('screenDeltaToSvg', () => {
|
||||
expect(screenDeltaToSvg(10, 10, 1000, 800, 0, 0)).toEqual({ dx: 0, dy: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('zoomAtPoint', () => {
|
||||
// The anchor is expressed as an offset (in SVG units) from the base viewBox
|
||||
// centre. The fraction of the anchor across the viewBox must not change.
|
||||
const anchorScreenFraction = (state: { x: number; z: number }, anchorOffsetX: number) => {
|
||||
const baseW = 1000;
|
||||
const w = baseW / state.z;
|
||||
const centreOffset = anchorOffsetX - state.x; // anchor relative to viewBox centre
|
||||
return centreOffset / w + 0.5;
|
||||
};
|
||||
|
||||
it('keeps the canvas centre fixed when the anchor is the centre', () => {
|
||||
const next = zoomAtPoint({ x: 0, y: 0, z: 1 }, 2, 0, 0);
|
||||
expect(next).toEqual({ x: 0, y: 0, z: 2 });
|
||||
});
|
||||
|
||||
it('keeps an off-centre anchor at the same screen position across a zoom-in', () => {
|
||||
const before = { x: 0, y: 0, z: 1 };
|
||||
const after = zoomAtPoint(before, 2, 100, 50);
|
||||
expect(after.z).toBe(2);
|
||||
expect(anchorScreenFraction(after, 100)).toBeCloseTo(anchorScreenFraction(before, 100), 10);
|
||||
});
|
||||
|
||||
it('clamps the target zoom and makes no move when already at the bound', () => {
|
||||
const next = zoomAtPoint({ x: 30, y: 10, z: MAX_ZOOM }, 99, 200, 200);
|
||||
expect(next).toEqual({ x: 30, y: 10, z: MAX_ZOOM });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user