mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
Raises the LR graph zoom ceiling from 200% to 400%. TB is unchanged at 200%. Zoom and pan are now tracked separately per direction instead of shared. Switching LR to TB and back restores the viewport you left in each mode, so a round trip no longer loses your position. Previously a single shared zoom value was clamped down whenever you switched into TB, which meant going LR to TB and back cost you your LR zoom. `run-overview.tsx` holds two view states, remembered per run under `<runId>-TB` and `<runId>-LR`. `clampZoom` and `zoomAtPoint` take a `direction` argument and apply the matching ceiling, so the clamp-on-direction-change effect is gone. 24 tests in `graph-viewport.test.ts`. Requirements: docs/brainstorms/2026-07-21-graph-zoom-lr-increase-requirements.md Plan: docs/plans/2026-07-21-graph-zoom-lr-increase-plan.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Fabro <noreply@fabro.sh>
59 lines
2.4 KiB
TypeScript
59 lines
2.4 KiB
TypeScript
// Pan/zoom viewport math for the run graph. There's no React in here, so it can be
|
|
// unit-tested on its own. Zoom is a continuous percentage rather than a discrete step
|
|
// index, which is what keeps cursor-anchored ⌘-scroll zoom smooth.
|
|
//
|
|
// The playground canvas (components/playground/canvas) has the same hand-rolled
|
|
// pan/zoom but still uses a discrete step index. If it ever wants cursor-anchored
|
|
// zoom, it can import this module.
|
|
|
|
export const GRAPH_MIN_ZOOM = 25; // percent; the clamp bounds. Widen if you want more range.
|
|
export const GRAPH_MAX_ZOOM_TB = 200; // top-down orientation
|
|
export const GRAPH_MAX_ZOOM_LR = 400; // left-right orientation
|
|
export const GRAPH_MAX_ZOOM = GRAPH_MAX_ZOOM_TB; // backward compat alias, defaults to TB
|
|
|
|
export type GraphView = { zoom: number; pan: { x: number; y: number } };
|
|
|
|
// Initial viewport shown when a graph first loads: 75% zoom, centered.
|
|
export const DEFAULT_GRAPH_VIEW: GraphView = { zoom: 75, pan: { x: 0, y: 0 } };
|
|
|
|
export const clampZoom = (zoom: number, direction?: "LR" | "TB"): number => {
|
|
const max = direction === "LR" ? GRAPH_MAX_ZOOM_LR : GRAPH_MAX_ZOOM_TB;
|
|
return Math.min(max, Math.max(GRAPH_MIN_ZOOM, zoom));
|
|
};
|
|
|
|
// How fast wheel/pinch input zooms; tune to taste.
|
|
const WHEEL_SENSITIVITY = 0.002;
|
|
|
|
/**
|
|
* Zoom factor for a wheel event's `deltaY`, for feeding into `zoomAtPoint`.
|
|
* exp() keeps equal scrolls up and down exact inverses and the factor above 0.
|
|
*/
|
|
export const wheelZoomFactor = (deltaY: number): number =>
|
|
Math.exp(-deltaY * WHEEL_SENSITIVITY);
|
|
|
|
/**
|
|
* Scale `view.zoom` by `factor`, keeping the content point under `cursor` fixed on
|
|
* screen. `cursor` is measured from the container CENTER (matching the graph's
|
|
* `transform-origin: center center`) and defaults to it, which is what the toolbar
|
|
* +/- buttons want.
|
|
*
|
|
* Derivation: with `translate(pan) scale(s)` about the center, a content point at
|
|
* pre-transform offset q sits at screen offset `pan + s*q`. Holding the point under
|
|
* the cursor (offset c) fixed while s -> s' gives `pan' = c*(1-k) + k*pan`, k = s'/s.
|
|
*/
|
|
export function zoomAtPoint(
|
|
view: GraphView,
|
|
factor: number,
|
|
cursor: { x: number; y: number } = { x: 0, y: 0 },
|
|
direction?: "LR" | "TB",
|
|
): GraphView {
|
|
const zoom = clampZoom(view.zoom * factor, direction);
|
|
const k = zoom / view.zoom; // applied ratio after clamping
|
|
return {
|
|
zoom,
|
|
pan: {
|
|
x: cursor.x * (1 - k) + k * view.pan.x,
|
|
y: cursor.y * (1 - k) + k * view.pan.y,
|
|
},
|
|
};
|
|
}
|