Co-Study4Grid / frontend /src /utils /smoothPanZoom.ts
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
4 kB
// Copyright (c) 2025-2026, RTE (https://www.rte-france.com)
// This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0.
// If a copy of the Mozilla Public License, version 2.0 was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
// This file is part of Co-Study4Grid a Power Grid Study tool Assistant Interface to help solve contigencies for a grid state under study.
import { useSyncExternalStore } from 'react';
import { interactionLogger } from './interactionLogger';
/**
* Pan/zoom rendering mode preference — an OPT-IN, off-by-default setting that
* changes how usePanZoom renders a gesture on large grids:
*
* - 'off' (default, safe everywhere): per-frame viewBox rewrite + the
* interaction-time paint culling in App.css. GPU-independent.
* - 'gpu' : compositor-only CSS transform on the live <svg> during the
* gesture, baking back to viewBox on settle. Smoother on GPU, but
* Chrome still re-rasters the huge vector layer each frame, so the
* win is modest (~1.2–1.5×) and it REGRESSES on software/VDI.
* - 'bitmap' : rasterise the NAD to a <canvas> ONCE at gesture start and
* transform that bitmap during the gesture (compositor-only, no
* vector re-raster), baking back to the live SVG on settle.
* Benchmarked at 120 fps / 0 dropped frames on the 5247-VL grid
* (~6× the GPU path) and composites cheaply even in software — at
* the cost of a one-shot raster of a multi-MB SVG at gesture start.
*
* Modelled as a tiny singleton (like interactionLogger / gameBridge / useTheme)
* rather than threaded through props, because four independent usePanZoom
* instances (N / N-1 / Action / overview) read it imperatively at gesture
* start, and only the Settings selector writes it. localStorage-persisted.
*/
export type PanZoomMode = 'off' | 'gpu' | 'bitmap';
const STORAGE_KEY = 'cs4g-smooth-pan-zoom';
const readInitial = (): PanZoomMode => {
try {
const v = localStorage.getItem(STORAGE_KEY);
if (v === 'gpu' || v === 'bitmap' || v === 'off') return v;
if (v === '1') return 'gpu'; // migrate the legacy boolean storage
return 'off';
} catch {
// localStorage unavailable (private mode, SSR-like envs) — default off.
return 'off';
}
};
let mode: PanZoomMode = readInitial();
const listeners = new Set<() => void>();
/** Imperative read for the hot path (usePanZoom reads this at gesture start). */
export const getSmoothPanZoomMode = (): PanZoomMode => mode;
/** Back-compat boolean read — true when either smooth mode is active. */
export const isSmoothPanZoomEnabled = (): boolean => mode !== 'off';
export const setSmoothPanZoomMode = (next: PanZoomMode): void => {
if (next === mode) return;
mode = next;
try {
localStorage.setItem(STORAGE_KEY, next);
} catch {
// ignore — same fallback as readInitial.
}
interactionLogger.record('smooth_pan_zoom_toggled', { enabled: next !== 'off', mode: next });
listeners.forEach(l => l());
};
/** Back-compat boolean setter (true → 'gpu', false → 'off'). */
export const setSmoothPanZoomEnabled = (enabled: boolean): void => {
setSmoothPanZoomMode(enabled ? 'gpu' : 'off');
};
const subscribe = (listener: () => void): (() => void) => {
listeners.add(listener);
return () => { listeners.delete(listener); };
};
/** React binding for the Settings selector. */
export const useSmoothPanZoom = (): {
mode: PanZoomMode;
setMode: (m: PanZoomMode) => void;
enabled: boolean;
setEnabled: (b: boolean) => void;
} => {
const value = useSyncExternalStore(subscribe, getSmoothPanZoomMode, () => 'off' as PanZoomMode);
return { mode: value, setMode: setSmoothPanZoomMode, enabled: value !== 'off', setEnabled: setSmoothPanZoomEnabled };
};