feat(stammbaum): add recentreAbove pan helper for the mobile anchor (#703)
recentreAbove recentres on a node and lifts it above the viewBox centre by a fraction of the zoomed viewBox height, measured against the auto-zoomed height. On a phone this lands the tapped anchor in the band above the bottom sheet instead of behind it (AC8). A zero bias is exactly a legible recentre. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
pinchZoom,
|
pinchZoom,
|
||||||
stepInertia,
|
stepInertia,
|
||||||
recentreOn,
|
recentreOn,
|
||||||
|
recentreAbove,
|
||||||
clampPan,
|
clampPan,
|
||||||
cornerView,
|
cornerView,
|
||||||
lerpView,
|
lerpView,
|
||||||
@@ -188,6 +189,34 @@ describe('recentreOn', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('recentreAbove', () => {
|
||||||
|
const node = { x: 300, y: 200 };
|
||||||
|
const base = { x: 100, y: 100 };
|
||||||
|
const baseH = 800;
|
||||||
|
|
||||||
|
it('matches the auto-zooming recentre when the bias is zero', () => {
|
||||||
|
expect(recentreAbove(node, base, { x: 0, y: 0, z: 2 }, baseH, 0)).toEqual(
|
||||||
|
recentreOn(node, base, { x: 0, y: 0, z: 2 }, true)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lifts the node up by a fraction of the zoomed viewBox height (clears the bottom sheet)', () => {
|
||||||
|
// recentreOn centres the node (pan.y = 100) at z=2; the bias adds
|
||||||
|
// 0.3 * (baseH / z) = 0.3 * 400 = 120, so the node sits ~20% from the top.
|
||||||
|
const next = recentreAbove(node, base, { x: 0, y: 0, z: 2 }, baseH, 0.3);
|
||||||
|
expect(next.x).toBe(200);
|
||||||
|
expect(next.z).toBe(2);
|
||||||
|
expect(next.y).toBeCloseTo(100 + 0.3 * (baseH / 2), 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('measures the bias against the auto-zoomed height when starting zoomed out', () => {
|
||||||
|
// z=0.4 auto-zooms up to LEGIBLE_ZOOM (1); the bias then uses baseH / 1.
|
||||||
|
const next = recentreAbove(node, base, { x: 0, y: 0, z: 0.4 }, baseH, 0.3);
|
||||||
|
expect(next.z).toBe(LEGIBLE_ZOOM);
|
||||||
|
expect(next.y).toBeCloseTo(100 + 0.3 * baseH, 6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('clampPan', () => {
|
describe('clampPan', () => {
|
||||||
// Base frame is 1000 x 800.
|
// Base frame is 1000 x 800.
|
||||||
it('forbids panning when the whole tree fits (z <= 1)', () => {
|
it('forbids panning when the whole tree fits (z <= 1)', () => {
|
||||||
|
|||||||
@@ -236,3 +236,21 @@ export function recentreOn(
|
|||||||
z: autoZoom ? clampZoom(Math.max(state.z, LEGIBLE_ZOOM)) : state.z
|
z: autoZoom ? clampZoom(Math.max(state.z, LEGIBLE_ZOOM)) : state.z
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recentre on a node but lift it above the viewBox centre by `biasFraction` of
|
||||||
|
* the zoomed viewBox height, so on a phone the tapped anchor lands in the band
|
||||||
|
* above the bottom sheet rather than behind it (#703 AC8). The bias is measured
|
||||||
|
* against the auto-zoomed height ({@link recentreOn} may snap zoom up to
|
||||||
|
* {@link LEGIBLE_ZOOM}), and a zero bias is exactly a legible recentre.
|
||||||
|
*/
|
||||||
|
export function recentreAbove(
|
||||||
|
nodeCentre: { x: number; y: number },
|
||||||
|
baseCentre: { x: number; y: number },
|
||||||
|
state: PanZoomState,
|
||||||
|
baseH: number,
|
||||||
|
biasFraction: number
|
||||||
|
): PanZoomState {
|
||||||
|
const centred = recentreOn(nodeCentre, baseCentre, state, true);
|
||||||
|
return { ...centred, y: centred.y + biasFraction * (baseH / centred.z) };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user