Skip to main content

fission_ir/
viewport.rs

1use crate::op::LayoutUnit;
2use serde::{Deserialize, Serialize};
3use std::hash::Hash;
4
5/// A uniform 2D camera transform used by interactive viewports.
6///
7/// Screen coordinates are calculated as `world * scale + translation`.
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
9pub struct ViewportTransform {
10    /// Translation from world origin to screen origin in logical points.
11    pub translation: [LayoutUnit; 2],
12    /// Uniform scale. Invalid values normalize to `1.0` before use.
13    pub scale: f32,
14}
15
16impl ViewportTransform {
17    pub const IDENTITY: Self = Self {
18        translation: [0.0, 0.0],
19        scale: 1.0,
20    };
21
22    pub fn new(translation_x: LayoutUnit, translation_y: LayoutUnit, scale: f32) -> Self {
23        Self {
24            translation: [translation_x, translation_y],
25            scale,
26        }
27        .normalized()
28    }
29
30    pub fn normalized(self) -> Self {
31        Self {
32            translation: [
33                finite_or_zero(self.translation[0]),
34                finite_or_zero(self.translation[1]),
35            ],
36            scale: if self.scale.is_finite() && self.scale > 0.0 {
37                self.scale
38            } else {
39                1.0
40            },
41        }
42    }
43
44    pub fn world_to_screen(self, world: [LayoutUnit; 2]) -> [LayoutUnit; 2] {
45        let transform = self.normalized();
46        [
47            world[0] * transform.scale + transform.translation[0],
48            world[1] * transform.scale + transform.translation[1],
49        ]
50    }
51
52    pub fn screen_to_world(self, screen: [LayoutUnit; 2]) -> [LayoutUnit; 2] {
53        let transform = self.normalized();
54        [
55            (screen[0] - transform.translation[0]) / transform.scale,
56            (screen[1] - transform.translation[1]) / transform.scale,
57        ]
58    }
59
60    /// Changes scale while keeping the world point below `screen_focal_point`
61    /// fixed on screen.
62    pub fn with_scale_around(self, screen_focal_point: [LayoutUnit; 2], scale: f32) -> Self {
63        let current = self.normalized();
64        let world_focal_point = current.screen_to_world(screen_focal_point);
65        let next_scale = if scale.is_finite() && scale > 0.0 {
66            scale
67        } else {
68            current.scale
69        };
70        Self {
71            translation: [
72                screen_focal_point[0] - world_focal_point[0] * next_scale,
73                screen_focal_point[1] - world_focal_point[1] * next_scale,
74            ],
75            scale: next_scale,
76        }
77    }
78}
79
80impl Default for ViewportTransform {
81    fn default() -> Self {
82        Self::IDENTITY
83    }
84}
85
86impl Hash for ViewportTransform {
87    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
88        self.translation[0].to_bits().hash(state);
89        self.translation[1].to_bits().hash(state);
90        self.scale.to_bits().hash(state);
91    }
92}
93
94fn finite_or_zero(value: LayoutUnit) -> LayoutUnit {
95    if value.is_finite() {
96        value
97    } else {
98        0.0
99    }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
103pub enum ViewportPanAxis {
104    /// Disable panning while retaining configured zoom behavior.
105    None,
106    /// Permit horizontal translation only.
107    Horizontal,
108    /// Permit vertical translation only.
109    Vertical,
110    /// Permit translation on both axes.
111    #[default]
112    Both,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
116pub struct ViewportMargin {
117    pub left: LayoutUnit,
118    pub right: LayoutUnit,
119    pub top: LayoutUnit,
120    pub bottom: LayoutUnit,
121}
122
123impl ViewportMargin {
124    pub const ZERO: Self = Self {
125        left: 0.0,
126        right: 0.0,
127        top: 0.0,
128        bottom: 0.0,
129    };
130
131    pub fn all(value: LayoutUnit) -> Self {
132        let value = non_negative_finite(value);
133        Self {
134            left: value,
135            right: value,
136            top: value,
137            bottom: value,
138        }
139    }
140
141    pub fn normalized(self) -> Self {
142        Self {
143            left: non_negative_finite(self.left),
144            right: non_negative_finite(self.right),
145            top: non_negative_finite(self.top),
146            bottom: non_negative_finite(self.bottom),
147        }
148    }
149}
150
151impl Hash for ViewportMargin {
152    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
153        self.left.to_bits().hash(state);
154        self.right.to_bits().hash(state);
155        self.top.to_bits().hash(state);
156        self.bottom.to_bits().hash(state);
157    }
158}
159
160fn non_negative_finite(value: LayoutUnit) -> LayoutUnit {
161    if value.is_finite() {
162        value.max(0.0)
163    } else {
164        0.0
165    }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
169pub enum ViewportBoundary {
170    /// Do not constrain the camera translation.
171    #[default]
172    Unbounded,
173    /// Constrain the visible world to finite bounds plus an edge margin.
174    Finite {
175        min_x: LayoutUnit,
176        min_y: LayoutUnit,
177        max_x: LayoutUnit,
178        max_y: LayoutUnit,
179        margin: ViewportMargin,
180    },
181}
182
183impl ViewportBoundary {
184    pub fn finite(
185        min_x: LayoutUnit,
186        min_y: LayoutUnit,
187        max_x: LayoutUnit,
188        max_y: LayoutUnit,
189        margin: ViewportMargin,
190    ) -> Self {
191        Self::Finite {
192            min_x,
193            min_y,
194            max_x,
195            max_y,
196            margin,
197        }
198        .normalized()
199    }
200
201    pub fn normalized(self) -> Self {
202        match self {
203            Self::Unbounded => Self::Unbounded,
204            Self::Finite {
205                min_x,
206                min_y,
207                max_x,
208                max_y,
209                margin,
210            } if min_x.is_finite()
211                && min_y.is_finite()
212                && max_x.is_finite()
213                && max_y.is_finite() =>
214            {
215                Self::Finite {
216                    min_x: min_x.min(max_x),
217                    min_y: min_y.min(max_y),
218                    max_x: min_x.max(max_x),
219                    max_y: min_y.max(max_y),
220                    margin: margin.normalized(),
221                }
222            }
223            Self::Finite { .. } => Self::Unbounded,
224        }
225    }
226}
227
228impl Hash for ViewportBoundary {
229    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
230        match self {
231            Self::Unbounded => 0_u8.hash(state),
232            Self::Finite {
233                min_x,
234                min_y,
235                max_x,
236                max_y,
237                margin,
238            } => {
239                1_u8.hash(state);
240                min_x.to_bits().hash(state);
241                min_y.to_bits().hash(state);
242                max_x.to_bits().hash(state);
243                max_y.to_bits().hash(state);
244                margin.hash(state);
245            }
246        }
247    }
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
251pub enum ViewportClip {
252    /// Allow transformed content to paint beyond the viewport.
253    None,
254    /// Clip to the rectangular viewport without antialiasing its edge.
255    #[default]
256    HardEdge,
257    /// Clip to the rectangular viewport with an antialiased edge.
258    AntiAlias,
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
262pub enum ViewportZoomPolicy {
263    /// Disable every zoom gesture while retaining configured panning.
264    Disabled,
265    /// Accept touch or trackpad pinch gestures, but never wheel zoom.
266    PinchOnly,
267    /// Accept pinch gestures and zoom a wheel only while Control or Meta is held.
268    #[default]
269    WheelWithModifier,
270    /// Accept pinch gestures and use wheel or trackpad scroll deltas for zoom.
271    WheelAndTrackpad,
272}