mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-30 05:16:56 -06:00
137 lines
5.1 KiB
Rust
137 lines
5.1 KiB
Rust
use super::{Deserialize, Serialize};
|
|
|
|
/// The canvas view of a document: what's at the centre of the viewport and the zoom.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct ViewState {
|
|
/// Page coordinate at the viewport centre.
|
|
pub cx: f64,
|
|
pub cy: f64,
|
|
/// Zoom (1 = 100%).
|
|
pub zoom: f64,
|
|
/// Viewport size in logical pixels (set by the UI; used by zoom-to-fit).
|
|
pub vw: f64,
|
|
pub vh: f64,
|
|
pub pixel_grid: bool,
|
|
pub layout_grids: bool,
|
|
pub rulers: bool,
|
|
pub outlines: bool,
|
|
/// Outline mode for the selected layers only (the rest draws normally).
|
|
pub outlines_selection: bool,
|
|
/// Outline mode also outlines hidden layers (View ▸ Outlines ▸ Include hidden layers).
|
|
pub outline_hidden: bool,
|
|
/// Outline mode also draws every layer's box (View ▸ Outlines ▸ Include object bounds).
|
|
pub outline_bounds: bool,
|
|
/// Outline the masks (they don't draw themselves).
|
|
pub mask_outlines: bool,
|
|
pub pixel_preview: bool,
|
|
/// Pixel preview resolution: rendered pixels per page pixel (1 or 2).
|
|
pub pixel_scale: u8,
|
|
pub show_slices: bool,
|
|
pub comments: bool,
|
|
/// Others' cursors in a live session (View ▸ Multiplayer cursors).
|
|
pub multiplayer_cursors: bool,
|
|
pub frame_outlines: bool,
|
|
/// Soft-proof sRGB: the canvas marks the colours of a Display P3 file that sRGB can't show.
|
|
pub proof_srgb: bool,
|
|
pub snap_geometry: bool,
|
|
pub snap_objects: bool,
|
|
pub snap_pixel: bool,
|
|
pub ui_hidden: bool,
|
|
}
|
|
|
|
impl Default for ViewState {
|
|
fn default() -> Self {
|
|
Self {
|
|
cx: 0.0,
|
|
cy: 0.0,
|
|
zoom: 1.0,
|
|
vw: 1200.0,
|
|
vh: 800.0,
|
|
pixel_grid: true,
|
|
layout_grids: true,
|
|
rulers: false,
|
|
outlines: false,
|
|
outlines_selection: false,
|
|
outline_hidden: false,
|
|
outline_bounds: false,
|
|
mask_outlines: false,
|
|
pixel_preview: false,
|
|
pixel_scale: 1,
|
|
show_slices: false,
|
|
comments: true,
|
|
multiplayer_cursors: true,
|
|
frame_outlines: false,
|
|
proof_srgb: false,
|
|
snap_geometry: true,
|
|
snap_objects: true,
|
|
snap_pixel: true,
|
|
ui_hidden: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ViewState {
|
|
pub const MIN_ZOOM: f64 = 0.02;
|
|
pub const MAX_ZOOM: f64 = 256.0;
|
|
/// Page coordinate at the viewport's top-left.
|
|
pub fn origin(&self) -> (f64, f64) {
|
|
let z = self.zoom.max(1e-9);
|
|
(self.cx - self.vw / 2.0 / z, self.cy - self.vh / 2.0 / z)
|
|
}
|
|
pub fn set_zoom_at(&mut self, zoom: f64, anchor: (f64, f64)) {
|
|
let z = if zoom.is_finite() { zoom.clamp(Self::MIN_ZOOM, Self::MAX_ZOOM) } else { self.zoom };
|
|
let old = self.zoom.max(1e-9);
|
|
// Keep the page point under `anchor` fixed.
|
|
self.cx = anchor.0 + (self.cx - anchor.0) * old / z;
|
|
self.cy = anchor.1 + (self.cy - anchor.1) * old / z;
|
|
self.zoom = z;
|
|
}
|
|
/// Fit a page rectangle into the viewport with margin.
|
|
pub fn fit(&mut self, r: figcraft_geom::Rect, max_zoom: f64) {
|
|
if !(r.width() > 0.0 || r.height() > 0.0) || !r.x0.is_finite() {
|
|
self.cx = r.center().x;
|
|
self.cy = r.center().y;
|
|
return;
|
|
}
|
|
let margin = 64.0;
|
|
let zx = (self.vw - margin * 2.0).max(50.0) / r.width().max(1e-6);
|
|
let zy = (self.vh - margin * 2.0).max(50.0) / r.height().max(1e-6);
|
|
self.zoom = zx.min(zy).clamp(Self::MIN_ZOOM, max_zoom);
|
|
self.cx = r.center().x;
|
|
self.cy = r.center().y;
|
|
}
|
|
/// The zoom from which the pixel grid shows (400%).
|
|
pub const PIXEL_GRID_ZOOM: f64 = 4.0;
|
|
/// Whether the pixel grid draws: it's on and the canvas is zoomed in to 400% or more.
|
|
pub fn pixel_grid_shown(&self) -> bool {
|
|
self.pixel_grid && self.zoom >= Self::PIXEL_GRID_ZOOM - 1e-9
|
|
}
|
|
/// Pixel preview's resolution (rendered pixels per page pixel) when it's on and zoomed in past
|
|
/// it; below that the canvas is already coarser than the preview.
|
|
pub fn pixel_preview_scale(&self) -> Option<f64> {
|
|
let k = f64::from(self.pixel_scale.clamp(1, 2));
|
|
(self.pixel_preview && self.zoom > k).then_some(k)
|
|
}
|
|
/// A wheel or trackpad scroll of (`dx`, `dy`) screen points: the canvas pans with it (content
|
|
/// follows the fingers); with Shift a plain vertical wheel pans sideways.
|
|
pub fn scroll(&mut self, dx: f64, dy: f64, shift: bool) {
|
|
let (dx, dy) = if shift && dx == 0.0 { (dy, 0.0) } else { (dx, dy) };
|
|
if !(dx.is_finite() && dy.is_finite()) {
|
|
return;
|
|
}
|
|
let z = self.zoom.max(1e-6);
|
|
self.cx -= dx / z;
|
|
self.cy -= dy / z;
|
|
}
|
|
pub fn page_to_screen(&self, x: f64, y: f64) -> (f64, f64) {
|
|
let (ox, oy) = self.origin();
|
|
((x - ox) * self.zoom, (y - oy) * self.zoom)
|
|
}
|
|
pub fn screen_to_page(&self, x: f64, y: f64) -> (f64, f64) {
|
|
let (ox, oy) = self.origin();
|
|
let z = self.zoom.max(1e-9);
|
|
(ox + x / z, oy + y / z)
|
|
}
|
|
}
|