Skip to main content

blockworx_paint/
vantage.rs

1//! Where the camera stands, and the transform it puts the world under.
2
3use blockworx_geom::{Pos2, Rect, Vec2, WorldPx};
4
5use crate::{Factor, Font, Zoom};
6
7/// A move of the camera a host read off its pointer, in screen space: a drag
8/// pan, a wheel notch, a pinch. Manual control, which is why the session
9/// abandons any framing still easing when one arrives.
10#[derive(Clone, Copy, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
11pub enum Move {
12    Pan(Vec2),
13    /// Scale the zoom by `factor`, leaving the world under `anchor` where it
14    /// is — the "zoom about the cursor" every zoom input wants.
15    Zoom {
16        factor: Factor,
17        anchor: Pos2,
18    },
19}
20
21/// The whole of what a `view`-kind undo entry restores.
22///
23/// The two numbers the view holds, not a world rect — a rect would have to be
24/// re-framed against a viewport that may have resized since, which would land
25/// the camera somewhere the user never was.
26///
27/// The transform is here rather than in a backend because two backends read
28/// it: the on-screen painter converts every draw through it, and a recording
29/// canvas has to place its marks where the painter would have.
30#[derive(Clone, Copy, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
31pub struct Vantage {
32    pub zoom: Zoom,
33    pub translation: Vec2,
34}
35
36impl Vantage {
37    /// The camera at rest: unity zoom, no pan.
38    pub const fn resting() -> Self {
39        Self {
40            zoom: Zoom::unity(),
41            translation: Vec2::ZERO,
42        }
43    }
44
45    /// `origin` is the screen corner the viewport was laid out at.
46    pub fn world_to_screen(self, origin: Pos2, world: Pos2) -> Pos2 {
47        origin + self.translation + world.to_vec2() * self.zoom.get()
48    }
49
50    pub fn screen_to_world(self, origin: Pos2, screen: Pos2) -> Pos2 {
51        ((screen - origin - self.translation) / self.zoom.get()).to_pos2()
52    }
53
54    pub fn remap_rect(self, origin: Pos2, world: Rect) -> Rect {
55        Rect::from_min_max(
56            self.world_to_screen(origin, world.min),
57            self.world_to_screen(origin, world.max),
58        )
59    }
60
61    /// World-space length → screen-space length. The single place a world
62    /// magnitude (radius, rounding, stroke width, wrap width) loses its unit.
63    pub fn remap_len(self, len: WorldPx) -> f32 {
64        len.get() * self.zoom.get()
65    }
66
67    /// The same face at the size it is drawn on screen.
68    pub fn remap_font(self, font: &Font) -> Font {
69        Font::new(font.size * self.zoom.get(), font.family.clone())
70    }
71
72    /// The world rect `viewport` shows from here.
73    pub fn visible(self, viewport: Rect) -> Rect {
74        Rect::from_min_max(
75            self.screen_to_world(viewport.min, viewport.min),
76            self.screen_to_world(viewport.min, viewport.max),
77        )
78    }
79
80    /// The vantage that frames `content` centred in `within`, a screen region
81    /// inside the viewport whose corner is `viewport_min`. `within` is not
82    /// necessarily the whole viewport: floating chrome may leave less of it
83    /// clear, and framing against the raw rect lands the model under the
84    /// chrome. Padding is the caller's — it pads `content` before framing it.
85    pub fn framing(within: Rect, viewport_min: Pos2, content: Rect, max_zoom: Zoom) -> Self {
86        let zoom = Zoom::framing(within.size(), content.size(), max_zoom);
87        Self {
88            zoom,
89            translation: (within.center() - viewport_min) - content.center().to_vec2() * zoom.get(),
90        }
91    }
92
93    /// The same camera zoomed by `factor` about `anchor` (screen space, in a
94    /// viewport cornered at `origin`), leaving the world under `anchor` where
95    /// it was. Lands inside [`Zoom::WORKED_MIN`]`..=`[`Zoom::WORKED_MAX`] — a
96    /// user working the camera stays in the band, where a framing may use the
97    /// whole range.
98    #[must_use]
99    pub fn zoomed_about(self, origin: Pos2, anchor: Pos2, factor: Factor) -> Self {
100        let zoom =
101            Zoom::new(self.zoom.get() * factor.get()).clamp(Zoom::WORKED_MIN, Zoom::WORKED_MAX);
102        let from_corner = anchor - origin;
103        let world_offset = from_corner - self.translation;
104        Self {
105            zoom,
106            translation: from_corner - world_offset * (zoom.get() / self.zoom.get()),
107        }
108    }
109}