mirror of
https://github.com/penpot/penpot.git
synced 2026-09-20 02:46:14 +00:00
HQ tiles are 512px and the atlas stays at 4096² (64 full-size slots). Browser zoom plus a forced ?dpr= can need more visible tiles than that, and a framebuffer larger than the GPU allows. Pack interest tiles into smaller atlas cells, blit at 512 then scale, and inset Linear samples so seams do not bleed. Clamp the canvas backing store and DPR together, wrap Skia at the real drawingBuffer size, and wait one frame after DPR changes so CSS client size and overlays stay aligned.
153 lines
4.0 KiB
Rust
153 lines
4.0 KiB
Rust
use crate::math::{Matrix, Point, Rect, Size};
|
||
use std::ops::Mul;
|
||
|
||
#[derive(Debug, Copy, Clone)]
|
||
pub(crate) struct Viewbox {
|
||
pub pan: Point,
|
||
pub size: Size,
|
||
pub zoom: f32,
|
||
pub dpr: f32,
|
||
pub area: Rect,
|
||
}
|
||
|
||
impl Default for Viewbox {
|
||
fn default() -> Self {
|
||
Self {
|
||
pan: Point::new(0.0, 0.0),
|
||
size: Size::new(0.0, 0.0),
|
||
zoom: 1.0,
|
||
dpr: 1.0,
|
||
area: Rect::new_empty(),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[allow(dead_code)]
|
||
impl Viewbox {
|
||
pub fn new(width: f32, height: f32) -> Self {
|
||
let size = Size::new(width, height);
|
||
let area = Rect::from_size(size);
|
||
Self {
|
||
size,
|
||
area,
|
||
..Self::default()
|
||
}
|
||
}
|
||
|
||
pub fn dpr_width(&self) -> f32 {
|
||
self.size.width * self.dpr
|
||
}
|
||
|
||
pub fn dpr_height(&self) -> f32 {
|
||
self.size.height * self.dpr
|
||
}
|
||
|
||
pub fn width(&self) -> f32 {
|
||
self.size.width
|
||
}
|
||
|
||
pub fn height(&self) -> f32 {
|
||
self.size.height
|
||
}
|
||
|
||
pub fn set_all(&mut self, zoom: f32, pan_x: f32, pan_y: f32) {
|
||
self.pan.set(pan_x, pan_y);
|
||
self.zoom = zoom;
|
||
self.area.set_xywh(
|
||
-self.pan.x,
|
||
-self.pan.y,
|
||
self.size.width / self.zoom,
|
||
self.size.height / self.zoom,
|
||
);
|
||
}
|
||
|
||
pub fn set_wh(&mut self, width: f32, height: f32) {
|
||
self.size.set(width, height);
|
||
self.area
|
||
.set_wh(self.size.width / self.zoom, self.size.height / self.zoom);
|
||
}
|
||
|
||
pub fn set_dpr(&mut self, dpr: f32) {
|
||
self.dpr = dpr;
|
||
}
|
||
|
||
pub fn get_scale(&self) -> f32 {
|
||
self.zoom * self.dpr
|
||
}
|
||
|
||
pub fn get_offset(&self) -> Point {
|
||
self.area.tl().mul(self.get_scale())
|
||
}
|
||
|
||
pub fn pan(&self) -> Point {
|
||
self.pan
|
||
}
|
||
|
||
pub fn zoom(&self) -> f32 {
|
||
self.zoom
|
||
}
|
||
|
||
pub fn get_matrix(&self) -> Matrix {
|
||
let mut matrix = Matrix::new_identity();
|
||
matrix.post_translate(self.pan());
|
||
matrix.post_scale((self.zoom, self.zoom), None);
|
||
matrix
|
||
}
|
||
}
|
||
|
||
/// Scale `dpr` down so `floor(css * dpr)` fits in `max_dim` on both axes.
|
||
/// Used when a large viewport combined with a high DPR would exceed the GPU
|
||
/// (or our surface cap) on either axis.
|
||
pub fn clamp_dpr_for_surface(css_w: f32, css_h: f32, dpr: f32, max_dim: i32) -> f32 {
|
||
let css_w = css_w.max(1.0);
|
||
let css_h = css_h.max(1.0);
|
||
let dpr = dpr.max(0.0);
|
||
let max_dim = max_dim.max(1) as f32;
|
||
let raw_w = (css_w * dpr).floor().max(1.0);
|
||
let raw_h = (css_h * dpr).floor().max(1.0);
|
||
let scale = (max_dim / raw_w).min(max_dim / raw_h).min(1.0);
|
||
dpr * scale
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::clamp_dpr_for_surface;
|
||
|
||
#[test]
|
||
fn clamp_dpr_keeps_hidpi_viewport_under_cap() {
|
||
let dpr = clamp_dpr_for_surface(2560.0, 1440.0, 2.0, 8192);
|
||
assert!((dpr - 2.0).abs() < 1e-5);
|
||
assert!((2560.0 * dpr).floor() <= 8192.0);
|
||
}
|
||
|
||
#[test]
|
||
fn clamp_dpr_caps_very_large_viewport_at_dpr2() {
|
||
// 10240×5760 CSS at DPR 2 → 20480 px unclamped on the long edge.
|
||
let dpr = clamp_dpr_for_surface(10240.0, 5760.0, 2.0, 8192);
|
||
assert!((10240.0 * dpr).floor() <= 8192.0);
|
||
assert!((5760.0 * dpr).floor() <= 8192.0);
|
||
assert!(dpr < 2.0);
|
||
}
|
||
|
||
#[test]
|
||
fn clamp_dpr_caps_large_viewport_at_dpr2() {
|
||
let dpr = clamp_dpr_for_surface(5120.0, 2880.0, 2.0, 8192);
|
||
assert!((5120.0 * dpr).floor() <= 8192.0);
|
||
assert!(dpr < 2.0);
|
||
assert!(dpr > 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn clamp_dpr_physical_size_is_floor_of_css_times_dpr() {
|
||
let css_w = 5120.0;
|
||
let css_h = 2880.0;
|
||
let dpr = clamp_dpr_for_surface(css_w, css_h, 2.0, 8192);
|
||
let phys_w = (css_w * dpr).floor();
|
||
let phys_h = (css_h * dpr).floor();
|
||
assert!(phys_w <= 8192.0);
|
||
assert!(phys_h <= 8192.0);
|
||
assert!((css_w * dpr - phys_w).abs() < 1.0);
|
||
assert!((css_h * dpr - phys_h).abs() < 1.0);
|
||
}
|
||
}
|