From 5458ca9bae0398556c5154afcd86cdbd296cf12d Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 29 May 2026 16:23:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(stammbaum):=20add=20clampZoom=20with=20res?= =?UTF-8?q?olved=200.25=E2=80=933.0=20zoom=20bounds=20(#692)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../src/lib/person/genealogy/panZoom.test.ts | 26 +++++++++++++++++++ frontend/src/lib/person/genealogy/panZoom.ts | 19 ++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 frontend/src/lib/person/genealogy/panZoom.test.ts create mode 100644 frontend/src/lib/person/genealogy/panZoom.ts diff --git a/frontend/src/lib/person/genealogy/panZoom.test.ts b/frontend/src/lib/person/genealogy/panZoom.test.ts new file mode 100644 index 00000000..509acdd8 --- /dev/null +++ b/frontend/src/lib/person/genealogy/panZoom.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { clampZoom, MIN_ZOOM, MAX_ZOOM } from './panZoom'; + +describe('clampZoom', () => { + it('returns the value unchanged when within range', () => { + expect(clampZoom(1)).toBe(1); + expect(clampZoom(0.5)).toBe(0.5); + expect(clampZoom(2.75)).toBe(2.75); + }); + + it('clamps below MIN_ZOOM up to MIN_ZOOM', () => { + expect(clampZoom(0.1)).toBe(MIN_ZOOM); + expect(clampZoom(0)).toBe(MIN_ZOOM); + expect(clampZoom(-5)).toBe(MIN_ZOOM); + }); + + it('clamps above MAX_ZOOM down to MAX_ZOOM', () => { + expect(clampZoom(5)).toBe(MAX_ZOOM); + expect(clampZoom(3.0001)).toBe(MAX_ZOOM); + }); + + it('exposes the resolved zoom bounds', () => { + expect(MIN_ZOOM).toBe(0.25); + expect(MAX_ZOOM).toBe(3.0); + }); +}); diff --git a/frontend/src/lib/person/genealogy/panZoom.ts b/frontend/src/lib/person/genealogy/panZoom.ts new file mode 100644 index 00000000..7b107440 --- /dev/null +++ b/frontend/src/lib/person/genealogy/panZoom.ts @@ -0,0 +1,19 @@ +/** + * Pan/zoom geometry for the Stammbaum canvas (#692). + * + * The Stammbaum renders zoom by deriving the SVG `viewBox` rather than applying + * a CSS transform (see `StammbaumTree.svelte`). This module is the single source + * of truth for the zoom bounds, the view-state shape, and every pure geometry + * helper used by the gesture action, the URL serialiser, and the page. Keeping + * the math here (and free of DOM access) makes it unit-testable in the node + * project. See ADR-026 for why this is custom rather than a third-party library. + */ + +/** Resolved zoom bounds (OQ-001). */ +export const MIN_ZOOM = 0.25; +export const MAX_ZOOM = 3.0; + +/** Clamp a zoom factor into the supported range. */ +export function clampZoom(z: number): number { + return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z)); +}