// 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 import type { ViewBox } from '../../types'; /** * Bitmap snapshot of the live NAD for the opt-in "Bitmap" pan/zoom mode. * * The diagnostic that motivated this mode: Chrome RE-RASTERS the ~100k-node * vector SVG layer on every CSS transform, so even the GPU-transform mode only * buys ~1.2–1.5× (a pure ±2px translate still costs ~48ms/frame on the 5247-VL * grid). Rasterising the SVG to a ONCE at gesture start and * transforming THAT bitmap during the gesture hits 120fps / 0 dropped frames * (~6× the GPU path) because the compositor just moves a flat texture — no * vector re-raster. See benchmarks/interaction_paint/. * * Two fidelity prerequisites the naive snapshot misses: * 1. **foreignObject taint.** pypowsybl's HTML VL labels live in ; * `drawImage()` of an SVG containing one throws SecurityError / taints * the canvas. They are stripped from the clone — and the gesture culls them * anyway (`.svg-interacting`), so the bitmap matches what the user sees. * 2. **App.css class-based paint.** Overload halos, the contingency glow and * flow-delta colours come from App.css *stylesheet rules on classed clones* * (see utils/svg/highlights.ts), NOT inline attributes. An SVG rendered in * isolation as an does not see the host page's stylesheet, so on the * N-1 / Action tabs those halos would VANISH. We fix this by copying the * relevant App.css rules + the resolved theme tokens into a ` : ''; return serialized.replace(/]*>/, open + styleTag); }; /** * Rasterise an SVG markup string onto a dpr-scaled canvas. Prefers * `createImageBitmap` (off-main-thread decode → no jank); falls back to an * `` blob decode where it's unavailable / rejects (e.g. older Safari). */ export const rasterizeMarkupToCanvas = async ( markup: string, width: number, height: number, dpr: number, ): Promise => { const blob = new Blob([markup], { type: 'image/svg+xml;charset=utf-8' }); let source: CanvasImageSource | null = null; let bitmap: ImageBitmap | null = null; let url: string | null = null; if (typeof createImageBitmap === 'function') { try { bitmap = await createImageBitmap(blob); source = bitmap; } catch { source = null; } } if (!source) { url = URL.createObjectURL(blob); const img = new Image(); img.width = width; img.height = height; await new Promise((resolve, reject) => { img.onload = () => resolve(); img.onerror = () => reject(new Error('snapshot image decode failed')); img.src = url as string; }); source = img; } try { const canvas = document.createElement('canvas'); canvas.width = Math.max(1, Math.round(width * dpr)); canvas.height = Math.max(1, Math.round(height * dpr)); const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('no 2d context for snapshot'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.drawImage(source, 0, 0, width, height); return canvas; } finally { if (url) URL.revokeObjectURL(url); if (bitmap) bitmap.close(); } };