camera2d.rs
/*
ENSnano, a 3d graphical application for DNA nanostructures.
Copyright (C) 2021 Nicolas Levy <nicolaspierrelevy@gmail.com> and Nicolas Schabanel <nicolas.schabanel@ens-lyon.fr>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! This modules defines a 2D camera for the FlatScene.
//!
//! The `Globals` struct contains the value that must be send to the GPU to compute the view
//! matrix. The `Camera` struct modifies a `Globals` attribute and perform some view <-> world
//! coordinate conversion.
use crate::consts::*;
use iced_winit::winit;
use ultraviolet::Vec2;
use winit::{dpi::PhysicalPosition, event::MouseScrollDelta};
pub struct Camera {
globals: Globals,
was_updated: bool,
old_globals: Globals,
pub bottom: bool,
}
impl Camera {
pub fn new(globals: Globals, bottom: bool) -> Self {
Self {
old_globals: globals,
globals,
was_updated: true,
bottom,
}
}
/// Return true if the globals have been modified since the last time `self.get_update()` was
/// called.
pub fn was_updated(&self) -> bool {
self.was_updated
}
/// Return the globals
pub fn get_globals(&self) -> &Globals {
&self.globals
}
/// Return the globals if self was updated,
pub fn update(&mut self) -> Option<&Globals> {
if self.was_updated {
self.was_updated = false;
Some(&self.globals)
} else {
None
}
}
/// Moves the camera, according to a mouse movement expressed in *normalized screen
/// coordinates*
pub fn process_mouse(&mut self, delta_x: f32, delta_y: f32) {
let (x, y) = self.transform_vec(delta_x, delta_y);
self.globals.scroll_offset[0] = self.old_globals.scroll_offset[0] - x;
self.globals.scroll_offset[1] = self.old_globals.scroll_offset[1] - y;
self.was_updated = true;
}
/// Perform a zoom so that the point under the cursor stays at the same position on display
pub fn process_scroll(
&mut self,
delta: &MouseScrollDelta,
cursor_position: PhysicalPosition<f64>,
) {
let scroll = match delta {
MouseScrollDelta::LineDelta(_, scroll) => *scroll,
MouseScrollDelta::PixelDelta(PhysicalPosition { y: scroll, .. }) => {
(*scroll as f32) / 100.
}
}
.min(1.)
.max(-1.);
let mult_const = 1.25_f32.powf(scroll);
let fixed_point =
Vec2::from(self.screen_to_world(cursor_position.x as f32, cursor_position.y as f32));
self.globals.zoom *= mult_const;
self.globals.zoom = self.globals.zoom.min(MAX_ZOOM_2D);
let delta = fixed_point
- Vec2::from(self.screen_to_world(cursor_position.x as f32, cursor_position.y as f32));
self.globals.scroll_offset[0] += delta.x;
self.globals.scroll_offset[1] += delta.y;
self.end_movement();
self.was_updated = true;
}
pub fn zoom_closer(&mut self) {
self.globals.zoom = self.globals.zoom.max(MAX_ZOOM_2D / 2.);
}
/// Descrete zoom on the scene
#[allow(dead_code)]
pub fn zoom_in(&mut self) {
self.globals.zoom *= 1.25;
self.was_updated = true;
}
/// Descrete zoom out of the scene
#[allow(dead_code)]
pub fn zoom_out(&mut self) {
self.globals.zoom *= 0.8;
self.was_updated = true;
}
/// Notify the camera that the current movement is over.
pub fn end_movement(&mut self) {
self.old_globals = self.globals;
}
/// Notify the camera that the size of the drawing area has been modified
pub fn resize(&mut self, res_x: f32, res_y: f32) {
self.globals.resolution[0] = res_x;
self.globals.resolution[1] = res_y;
self.was_updated = true;
}
pub fn set_center(&mut self, center: Vec2) {
self.globals.scroll_offset = center.into();
self.was_updated = true;
self.end_movement();
}
pub fn set_zoom(&mut self, zoom: f32) {
self.globals.zoom = zoom;
}
/// Convert a *vector* in screen coordinate to a vector in world coordinate. (Does not apply
/// the translation)
fn transform_vec(&self, x: f32, y: f32) -> (f32, f32) {
(
self.globals.resolution[0] * x / self.globals.zoom,
self.globals.resolution[1] * y / self.globals.zoom,
)
}
/// Convert a *point* in screen ([0, x_res] * [0, y_res]) coordinate to a point in world coordiantes.
pub fn screen_to_world(&self, x_screen: f32, y_screen: f32) -> (f32, f32) {
// The screen coordinates have the y axis pointed down, and so does the 2d world
// coordinates. So we do not flip the y axis.
let x_ndc = 2. * x_screen / self.globals.resolution[0] - 1.;
let y_ndc = if self.bottom {
2. * (y_screen - self.globals.resolution[1]) / self.globals.resolution[1] - 1.
} else {
2. * y_screen / self.globals.resolution[1] - 1.
};
(
x_ndc * self.globals.resolution[0] / (2. * self.globals.zoom)
+ self.globals.scroll_offset[0],
y_ndc * self.globals.resolution[1] / (2. * self.globals.zoom)
+ self.globals.scroll_offset[1],
)
}
pub fn norm_screen_to_world(&self, x_normed: f32, y_normed: f32) -> (f32, f32) {
if self.bottom {
self.screen_to_world(
x_normed * self.globals.resolution[0],
(y_normed + 1.) * self.globals.resolution[1],
)
} else {
self.screen_to_world(
x_normed * self.globals.resolution[0],
y_normed * self.globals.resolution[1],
)
}
}
/// Convert a *point* in world coordinates to a point in normalized screen ([0, 1] * [0, 1]) coordinates
pub fn world_to_norm_screen(&self, x_world: f32, y_world: f32) -> (f32, f32) {
// The screen coordinates have the y axis pointed down, and so does the 2d world
// coordinates. So we do not flip the y axis.
let temp = (
x_world - self.globals.scroll_offset[0],
y_world - self.globals.scroll_offset[1],
);
let coord_ndc = (
temp.0 * 2. * self.globals.zoom / self.globals.resolution[0],
temp.1 * 2. * self.globals.zoom / self.globals.resolution[1],
);
((coord_ndc.0 + 1.) / 2., (coord_ndc.1 + 1.) / 2.)
}
pub fn fit(&mut self, mut rectangle: FitRectangle) {
rectangle.finish();
rectangle.adjust_height(1.1);
let zoom_x = self.globals.resolution[0] / rectangle.width().unwrap();
let zoom_y = self.globals.resolution[1] / rectangle.height().unwrap();
if zoom_x < zoom_y {
self.globals.zoom = zoom_x;
} else {
self.globals.zoom = zoom_y;
}
let (center_x, center_y) = rectangle.center().unwrap();
self.globals.scroll_offset[0] = center_x;
self.globals.scroll_offset[1] = center_y;
self.was_updated = true;
self.end_movement();
}
pub fn can_see_world_point(&self, point: Vec2) -> bool {
let normalized_coord = self.world_to_norm_screen(point.x, point.y);
normalized_coord.0 >= 0.015
&& normalized_coord.0 <= 1. - 0.015
&& normalized_coord.1 >= 0.015
&& normalized_coord.1 <= 1. - 0.015
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct Globals {
pub resolution: [f32; 2],
pub scroll_offset: [f32; 2],
pub zoom: f32,
pub _padding: f32,
}
unsafe impl bytemuck::Zeroable for Globals {}
unsafe impl bytemuck::Pod for Globals {}
#[derive(Debug, Clone, Copy, Default)]
pub struct FitRectangle {
pub min_x: Option<f32>,
pub max_x: Option<f32>,
pub min_y: Option<f32>,
pub max_y: Option<f32>,
}
impl FitRectangle {
pub fn new() -> Self {
Default::default()
}
pub fn add_point(&mut self, point: ultraviolet::Vec2) {
self.min_x = self.min_x.map(|x| x.min(point.x)).or(Some(point.x));
self.max_x = self.max_x.map(|x| x.max(point.x)).or(Some(point.x));
self.min_y = self.min_y.map(|y| y.min(point.y)).or(Some(point.y));
self.max_y = self.max_y.map(|y| y.max(point.y)).or(Some(point.y));
}
pub fn finish(&mut self) {
let width = self.width().unwrap_or(0.);
let height = self.height().unwrap_or(0.);
if width <= Self::min_width() {
let diff = Self::min_width() - width;
self.min_x = self.min_x.map(|x| x - diff / 4.).or(Some(-5.));
self.max_x = self.max_x.map(|x| x + 3. * diff / 4.).or(Some(15.))
}
if height <= Self::min_height() {
let diff = Self::min_height() - height;
self.min_y = self.min_y.map(|y| y - diff / 7.).or(Some(-5.));
self.max_y = self.max_y.map(|y| y + 6. * diff / 7.).or(Some(30.));
}
}
pub fn adjust_height(&mut self, factor: f32) {
let height = self.height().unwrap_or(0.);
let delta = (factor - 1.) / 2.;
self.min_y.as_mut().map(|y| *y -= delta * height);
self.max_y.as_mut().map(|y| *y += delta * height);
}
fn width(&self) -> Option<f32> {
let max_x = self.max_x?;
let min_x = self.min_x?;
Some(max_x - min_x)
}
fn height(&self) -> Option<f32> {
let max_y = self.max_y?;
let min_y = self.min_y?;
Some(max_y - min_y)
}
fn center(&self) -> Option<(f32, f32)> {
let max_x = self.max_x?;
let min_x = self.min_x?;
let max_y = self.max_y?;
let min_y = self.min_y?;
Some(((max_x + min_x) / 2., (max_y + min_y) / 2.))
}
fn min_width() -> f32 {
20f32
}
fn min_height() -> f32 {
35f32
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty_rectangle() {
let rect = FitRectangle::new();
assert!(rect.width().is_none());
assert!(rect.height().is_none());
}
#[test]
fn minimum_height_after_finish() {
let mut rect = FitRectangle::new();
rect.finish();
let height = rect.height().unwrap();
assert!(height >= FitRectangle::min_height())
}
#[test]
fn minimum_width_after_finish() {
let mut rect = FitRectangle::new();
rect.finish();
let width = rect.width().unwrap();
assert!(width >= FitRectangle::min_width())
}
#[test]
fn correct_width() {
let mut rect = FitRectangle::new();
rect.add_point(Vec2::new(-3., 4.));
rect.add_point(Vec2::new(-2., 5.));
rect.add_point(Vec2::new(-1., -2.));
let width = rect.width().unwrap();
assert!((width - (2.)).abs() < 1e-5);
}
#[test]
fn correct_height() {
let mut rect = FitRectangle::new();
rect.add_point(Vec2::new(-3., 4.));
rect.add_point(Vec2::new(-2., 5.));
rect.add_point(Vec2::new(-1., -2.));
let height = rect.height().unwrap();
assert!((height - 7.).abs() < 1e-5);
}
}