feat(stammbaum): mobile read path — pan, zoom, fit-to-view (#692) #694
@@ -5,8 +5,10 @@ import {
|
|||||||
serializePanZoomParams,
|
serializePanZoomParams,
|
||||||
screenDeltaToSvg,
|
screenDeltaToSvg,
|
||||||
zoomAtPoint,
|
zoomAtPoint,
|
||||||
|
recentreOn,
|
||||||
DEFAULT_VIEW,
|
DEFAULT_VIEW,
|
||||||
DEFAULT_ZOOM,
|
DEFAULT_ZOOM,
|
||||||
|
LEGIBLE_ZOOM,
|
||||||
MIN_ZOOM,
|
MIN_ZOOM,
|
||||||
MAX_ZOOM
|
MAX_ZOOM
|
||||||
} from './panZoom';
|
} from './panZoom';
|
||||||
@@ -120,3 +122,26 @@ describe('zoomAtPoint', () => {
|
|||||||
expect(next).toEqual({ x: 30, y: 10, z: MAX_ZOOM });
|
expect(next).toEqual({ x: 30, y: 10, z: MAX_ZOOM });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('recentreOn', () => {
|
||||||
|
const node = { x: 300, y: 200 };
|
||||||
|
const base = { x: 100, y: 100 };
|
||||||
|
|
||||||
|
it('pans so the node sits at the viewBox centre, keeping the current zoom', () => {
|
||||||
|
expect(recentreOn(node, base, { x: 0, y: 0, z: 1 }, false)).toEqual({ x: 200, y: 100, z: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auto-zooms up to LEGIBLE_ZOOM when zoomed out (OQ-005)', () => {
|
||||||
|
const next = recentreOn(node, base, { x: 0, y: 0, z: 0.4 }, true);
|
||||||
|
expect(next.z).toBe(LEGIBLE_ZOOM);
|
||||||
|
expect({ x: next.x, y: next.y }).toEqual({ x: 200, y: 100 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reduce an already-legible zoom when auto-zooming', () => {
|
||||||
|
expect(recentreOn(node, base, { x: 0, y: 0, z: 2 }, true).z).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves zoom untouched when auto-zoom is off', () => {
|
||||||
|
expect(recentreOn(node, base, { x: 0, y: 0, z: 0.4 }, false).z).toBe(0.4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export const MIN_ZOOM = 0.25;
|
|||||||
export const MAX_ZOOM = 3.0;
|
export const MAX_ZOOM = 3.0;
|
||||||
export const DEFAULT_ZOOM = 1;
|
export const DEFAULT_ZOOM = 1;
|
||||||
|
|
||||||
|
/** Minimum zoom a recentre will snap up to so the focal node's text is legible (OQ-005). */
|
||||||
|
export const LEGIBLE_ZOOM = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The canvas view state. `x`/`y` are pan offsets applied to the viewBox centre
|
* The canvas view state. `x`/`y` are pan offsets applied to the viewBox centre
|
||||||
* (SVG user units); `z` is the zoom factor. The default `{0, 0, 1}` frames the
|
* (SVG user units); `z` is the zoom factor. The default `{0, 0, 1}` frames the
|
||||||
@@ -106,3 +109,23 @@ export function zoomAtPoint(
|
|||||||
z
|
z
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pan so a node sits at the viewBox centre (US-PAN-005). Because the viewBox
|
||||||
|
* centre is `baseCentre + pan` independent of zoom, centring is a pure pan:
|
||||||
|
* `pan = nodeCentre - baseCentre`. When `autoZoom` is set, a zoomed-out view is
|
||||||
|
* snapped up to {@link LEGIBLE_ZOOM} so the focal node's text is readable
|
||||||
|
* (OQ-005); an already-legible zoom is preserved.
|
||||||
|
*/
|
||||||
|
export function recentreOn(
|
||||||
|
nodeCentre: { x: number; y: number },
|
||||||
|
baseCentre: { x: number; y: number },
|
||||||
|
state: PanZoomState,
|
||||||
|
autoZoom: boolean
|
||||||
|
): PanZoomState {
|
||||||
|
return {
|
||||||
|
x: nodeCentre.x - baseCentre.x,
|
||||||
|
y: nodeCentre.y - baseCentre.y,
|
||||||
|
z: autoZoom ? clampZoom(Math.max(state.z, LEGIBLE_ZOOM)) : state.z
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user