Split out viewport handling into its own message handler (#3331)

* extract viewport handeling

* fix web overlays

* some cleanup

* remove some physical conversions

* fix resize snapping

* fixup

* apply some review feedback

* make viewport api more ergonomic

* fix

* fix web overlay canvas clear size

* clear workaround for canvas

* rename trigger message

---------

Co-authored-by: Dennis Kobert <dennis@kobert.dev>
This commit is contained in:
Timon
2025-11-08 08:59:38 +00:00
committed by GitHub
co-authored by Dennis Kobert
parent 3490111b96
commit 881ec0b193
60 changed files with 1326 additions and 607 deletions
+27 -23
View File
@@ -20,7 +20,7 @@
import type { DocumentState } from "@graphite/state-providers/document";
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
import { updateBoundsOfViewports } from "@graphite/utility-functions/viewports";
import { updateBoundsOfViewports as updateViewport } from "@graphite/utility-functions/viewports";
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@@ -74,21 +74,21 @@
let cursorEyedropperPreviewColorSecondary = "";
// Canvas dimensions
let canvasSvgWidth: number | undefined = undefined;
let canvasSvgHeight: number | undefined = undefined;
let canvasWidth: number | undefined = undefined;
let canvasHeight: number | undefined = undefined;
let devicePixelRatio: number | undefined;
// Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
$: canvasWidthRoundedToEven = canvasSvgWidth && (canvasSvgWidth % 2 === 1 ? canvasSvgWidth + 1 : canvasSvgWidth);
$: canvasHeightRoundedToEven = canvasSvgHeight && (canvasSvgHeight % 2 === 1 ? canvasSvgHeight + 1 : canvasSvgHeight);
$: canvasWidthRoundedToEven = canvasWidth && (canvasWidth % 2 === 1 ? canvasWidth + 1 : canvasWidth);
$: canvasHeightRoundedToEven = canvasHeight && (canvasHeight % 2 === 1 ? canvasHeight + 1 : canvasHeight);
// Used to set the canvas element size on the page.
// The value above in pixels, or if undefined, we fall back to 100% as a non-pixel-perfect backup that's hopefully short-lived
$: canvasWidthCSS = canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%";
$: canvasHeightCSS = canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%";
$: canvasWidthScaled = canvasSvgWidth && devicePixelRatio && Math.floor(canvasSvgWidth * devicePixelRatio);
$: canvasHeightScaled = canvasSvgHeight && devicePixelRatio && Math.floor(canvasSvgHeight * devicePixelRatio);
$: canvasWidthScaled = canvasWidth && devicePixelRatio && Math.floor(canvasWidth * devicePixelRatio);
$: canvasHeightScaled = canvasHeight && devicePixelRatio && Math.floor(canvasHeight * devicePixelRatio);
// Used to set the canvas rendering dimensions.
$: canvasWidthScaledRoundedToEven = canvasWidthScaled && (canvasWidthScaled % 2 === 1 ? canvasWidthScaled + 1 : canvasWidthScaled);
@@ -226,14 +226,14 @@
}
cursorEyedropper = true;
if (canvasSvgWidth === undefined || canvasSvgHeight === undefined) return undefined;
if (canvasWidth === undefined || canvasHeight === undefined) return undefined;
cursorLeft = mousePosition.x;
cursorTop = mousePosition.y;
// This works nearly perfectly, but sometimes at odd DPI scale factors like 1.25, the anti-aliasing color can yield slightly incorrect colors (potential room for future improvement)
const dpiFactor = window.devicePixelRatio;
const [width, height] = [canvasSvgWidth, canvasSvgHeight];
const [width, height] = [canvasWidth, canvasHeight];
const outsideArtboardsColor = getComputedStyle(window.document.documentElement).getPropertyValue("--color-2-mildblack");
const outsideArtboards = `<rect x="0" y="0" width="100%" height="100%" fill="${outsideArtboardsColor}" />`;
@@ -381,6 +381,22 @@
showTextInput = false;
}
function updateViewportInfo() {
if (!viewport) return;
// Resize the canvas
canvasWidth = Math.ceil(parseFloat(getComputedStyle(viewport).width));
canvasHeight = Math.ceil(parseFloat(getComputedStyle(viewport).height));
devicePixelRatio = window.devicePixelRatio || 1.0;
// Resize the rulers
rulerHorizontal?.resize();
rulerVertical?.resize();
// Send the new bounds of the viewports to the backend
if (viewport.parentElement) updateViewport(editor);
}
onMount(() => {
// Not compatible with Safari:
// <https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#browser_compatibility>
@@ -393,8 +409,7 @@
mediaQueryList.addEventListener("change", updatePixelRatio);
removeUpdatePixelRatio = () => mediaQueryList.removeEventListener("change", updatePixelRatio);
devicePixelRatio = window.devicePixelRatio;
editor.handle.setDevicePixelRatio(devicePixelRatio);
updateViewportInfo();
};
updatePixelRatio();
@@ -462,18 +477,7 @@
window.dispatchEvent(new Event("resize"));
const viewportResizeObserver = new ResizeObserver(() => {
if (!viewport) return;
// Resize the canvas
canvasSvgWidth = Math.ceil(parseFloat(getComputedStyle(viewport).width));
canvasSvgHeight = Math.ceil(parseFloat(getComputedStyle(viewport).height));
// Resize the rulers
rulerHorizontal?.resize();
rulerVertical?.resize();
// Send the new bounds of the viewports to the backend
if (viewport.parentElement) updateBoundsOfViewports(editor);
updateViewportInfo();
});
if (viewport) viewportResizeObserver.observe(viewport);
});
+5 -7
View File
@@ -2,13 +2,11 @@ import { type Editor } from "@graphite/editor";
export function updateBoundsOfViewports(editor: Editor) {
const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]"));
const boundsOfViewports = viewports.map((canvas) => {
const bounds = canvas.getBoundingClientRect();
return [bounds.left, bounds.top, bounds.right, bounds.bottom];
});
const flattened = boundsOfViewports.flat();
const data = Float64Array.from(flattened);
if (viewports.length <= 0) return;
if (boundsOfViewports.length > 0) editor.handle.boundsOfViewports(data);
const bounds = viewports[0].getBoundingClientRect();
const scale = window.devicePixelRatio || 1;
editor.handle.updateViewport(bounds.x, bounds.y, bounds.width, bounds.height, scale);
}
+5 -15
View File
@@ -8,7 +8,7 @@ use crate::helpers::translate_key;
use crate::{EDITOR_HANDLE, EDITOR_HAS_CRASHED, Error, MESSAGE_BUFFER};
use editor::consts::FILE_EXTENSION;
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta};
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport;
use editor::messages::portfolio::utility_types::Platform;
@@ -516,13 +516,10 @@ impl EditorHandle {
self.dispatch(message);
}
/// Send new bounds when document panel viewports get resized or moved within the editor
/// [left, top, right, bottom]...
#[wasm_bindgen(js_name = boundsOfViewports)]
pub fn bounds_of_viewports(&self, bounds_of_viewports: &[f64]) {
let chunked: Vec<_> = bounds_of_viewports.chunks(4).map(ViewportBounds::from_slice).collect();
let message = InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports: chunked };
/// Send new viewport info to the backend
#[wasm_bindgen(js_name = updateViewport)]
pub fn update_viewport(&self, x: f64, y: f64, width: f64, height: f64, scale: f64) {
let message = ViewportMessage::Update { x, y, width, height, scale };
self.dispatch(message);
}
@@ -533,13 +530,6 @@ impl EditorHandle {
self.dispatch(message);
}
/// Inform the overlays system of the current device pixel ratio
#[wasm_bindgen(js_name = setDevicePixelRatio)]
pub fn set_device_pixel_ratio(&self, ratio: f64) {
let message = PortfolioMessage::SetDevicePixelRatio { ratio };
self.dispatch(message);
}
/// Mouse movement within the screenspace bounds of the viewport
#[wasm_bindgen(js_name = onMouseMove)]
pub fn on_mouse_move(&self, x: f64, y: f64, mouse_keys: u8, modifiers: u8) {