Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions crates/rnote-compose/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ where
fn to_kurbo(self) -> kurbo::Affine;
/// converting from kurbo affine
fn from_kurbo(affine: kurbo::Affine) -> Self;
/// Transforms the Aabb vertices and calculates a new that contains them.
fn transform_aabb(&self, aabb: Aabb) -> Aabb;
}

impl Affine2Ext for na::Affine2<f64> {
Expand All @@ -490,6 +492,21 @@ impl Affine2Ext for na::Affine2<f64> {
))
.unwrap()
}

/// Transforms the Aabb and returns the minimum enclosing Aabb
fn transform_aabb(&self, aabb: Aabb) -> Aabb {
let p0 = self * na::point![aabb.mins[0], aabb.mins[1]];
let p1 = self * na::point![aabb.mins[0], aabb.maxs[1]];
let p2 = self * na::point![aabb.maxs[0], aabb.maxs[1]];
let p3 = self * na::point![aabb.maxs[0], aabb.mins[1]];

let min_x = p0[0].min(p1[0]).min(p2[0]).min(p3[0]);
let min_y = p0[1].min(p1[1]).min(p2[1]).min(p3[1]);
let max_x = p0[0].max(p1[0]).max(p2[0]).max(p3[0]);
let max_y = p0[1].max(p1[1]).max(p2[1]).max(p3[1]);

Aabb::new_positive(na::point![min_x, min_y], na::point![max_x, max_y])
}
}

/// Extension trait for types that implement [kurbo::Shape].
Expand Down
14 changes: 2 additions & 12 deletions crates/rnote-compose/src/transform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod transformable;
pub use transformable::Transformable;

// Imports
use crate::ext::{AabbExt, Affine2Ext};
use crate::ext::Affine2Ext;
use p2d::bounding_volume::Aabb;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -87,17 +87,7 @@ impl Transform {

/// Transforms the Aabb vertices and calculates a new that contains them.
pub fn transform_aabb(&self, aabb: Aabb) -> Aabb {
let p0 = self.affine * na::point![aabb.mins[0], aabb.mins[1]];
let p1 = self.affine * na::point![aabb.mins[0], aabb.maxs[1]];
let p2 = self.affine * na::point![aabb.maxs[0], aabb.maxs[1]];
let p3 = self.affine * na::point![aabb.maxs[0], aabb.mins[1]];

let min_x = p0[0].min(p1[0]).min(p2[0]).min(p3[0]);
let min_y = p0[1].min(p1[1]).min(p2[1]).min(p3[1]);
let max_x = p0[0].max(p1[0]).max(p2[0]).max(p3[0]);
let max_y = p0[1].max(p1[1]).max(p2[1]).max(p3[1]);

Aabb::new_positive(na::point![min_x, min_y], na::point![max_x, max_y])
self.affine.transform_aabb(aabb)
}

/// Append a translation to the transform.
Expand Down
185 changes: 126 additions & 59 deletions crates/rnote-engine/src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::engine::{EngineTask, EngineTaskSender};
use crate::tasks::{OneOffTaskError, OneOffTaskHandle};
use crate::{Document, WidgetFlags};
use p2d::bounding_volume::Aabb;
use rnote_compose::ext::AabbExt;
use rnote_compose::ext::{AabbExt, Affine2Ext};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::error;
Expand Down Expand Up @@ -34,6 +34,9 @@ pub struct Camera {
/// The camera zoom, origin at (0.0, 0.0).
#[serde(rename = "zoom")]
zoom: f64,
/// The camera rotation in radians.
#[serde(skip)]
rotation: f64,
/// The temporary zoom. Is used to overlay the "permanent" zoom.
#[serde(skip)]
temporary_zoom: f64,
Expand All @@ -54,6 +57,7 @@ impl Default for Camera {
offset: na::vector![-Self::OVERSHOOT_HORIZONTAL, -Self::OVERSHOOT_VERTICAL],
size: na::vector![800.0, 600.0],
zoom: 1.0,
rotation: 0.0,
temporary_zoom: 1.0,
scale_factor: 1.0,
zoom_task_handle: None,
Expand All @@ -67,6 +71,7 @@ impl Snapshotable for Camera {
offset: self.offset,
size: self.size,
zoom: self.zoom,
rotation: self.rotation,
..Default::default()
}
}
Expand Down Expand Up @@ -97,52 +102,51 @@ impl Camera {
self
}

pub fn with_rotation(mut self, rotation: f64) -> Self {
self.rotation = rotation;
self
}

/// The current viewport offset in surface coordinate space.
pub fn offset(&self) -> na::Vector2<f64> {
self.offset
}

pub fn set_offset(&mut self, offset: na::Vector2<f64>, doc: &Document) -> WidgetFlags {
let mut widget_flags = WidgetFlags::default();
let (lower, upper) = self.offset_lower_upper(doc);

let (mins, maxs) = self.surface_mins_maxs(doc);
let offset_maxs = na::vector![
(maxs.x - self.size.x).max(mins.x),
(maxs.y - self.size.y).max(mins.y)
];

self.offset = na::vector![
offset[0].clamp(lower[0], upper[0]),
offset[1].clamp(lower[1], upper[1])
offset.x.clamp(mins.x, offset_maxs.x),
offset.y.clamp(mins.y, offset_maxs.y)
];

widget_flags.view_modified = true;
widget_flags.resize = true;
widget_flags
}

/// The offset minimum and maximum values in surface coordinate space.
pub fn offset_lower_upper(&self, doc: &Document) -> (na::Vector2<f64>, na::Vector2<f64>) {
let total_zoom = self.total_zoom();
/// The minimum and maximum surface bounds (document including overshoot) in surface coordinate space.
pub fn surface_mins_maxs(&self, doc: &Document) -> (na::Vector2<f64>, na::Vector2<f64>) {
let document_bounds = self
.document_to_view_transform()
.transform_aabb(doc.bounds());

let (h_lower, h_upper) = match doc.config.layout {
Layout::FixedSize | Layout::ContinuousVertical => (
doc.x * total_zoom - Self::OVERSHOOT_HORIZONTAL,
(doc.x + doc.width) * total_zoom + Self::OVERSHOOT_HORIZONTAL,
),
Layout::SemiInfinite => (
doc.x * total_zoom - Self::OVERSHOOT_HORIZONTAL,
(doc.x + doc.width) * total_zoom,
),
Layout::Infinite => (doc.x * total_zoom, (doc.x + doc.width) * total_zoom),
};
let (v_lower, v_upper) = match doc.config.layout {
Layout::FixedSize | Layout::ContinuousVertical => (
doc.y * total_zoom - Self::OVERSHOOT_VERTICAL,
(doc.y + doc.height) * total_zoom + Self::OVERSHOOT_VERTICAL,
),
Layout::SemiInfinite => (
doc.y * total_zoom - Self::OVERSHOOT_VERTICAL,
(doc.y + doc.height) * total_zoom,
),
Layout::Infinite => (doc.y * total_zoom, (doc.y + doc.height) * total_zoom),
let bounds = match doc.config.layout {
Layout::FixedSize | Layout::ContinuousVertical | Layout::SemiInfinite => {
document_bounds.extend_by(na::vector![
Self::OVERSHOOT_HORIZONTAL,
Self::OVERSHOOT_VERTICAL
])
}
Layout::Infinite => document_bounds,
};

(na::vector![h_lower, v_lower], na::vector![h_upper, v_upper])
(bounds.mins.coords, bounds.maxs.coords)
}

/// The current viewport size in surface coordinate space.
Expand All @@ -168,7 +172,47 @@ impl Camera {
pub fn zoom_to(&mut self, zoom: f64) -> WidgetFlags {
let mut widget_flags = WidgetFlags::default();
self.zoom = zoom.clamp(Self::ZOOM_MIN, Self::ZOOM_MAX);
widget_flags.zoomed = true;
widget_flags.view_modified = true;
widget_flags.resize = true;
widget_flags.refresh_canvasmenu = true;
widget_flags.update_old_viewport = true;
widget_flags
}

/// The camera rotation in radians.
pub fn rotation(&self) -> f64 {
self.rotation
}

fn snap_angle(angle: f64, step: f64) -> f64 {
const SNAP_EPS: f64 = 1_f64.to_radians();

let k = (angle / step).round();
let snapped_angle = k * step;

if (angle - snapped_angle).abs() <= SNAP_EPS {
snapped_angle
} else {
angle
}
}

/// Normalizes angle to (-pi, pi].
fn normalize_angle(angle: f64) -> f64 {
std::f64::consts::PI - (std::f64::consts::PI - angle).rem_euclid(std::f64::consts::TAU)
}

pub fn set_rotation(&mut self, rotation: f64) -> WidgetFlags {
let mut widget_flags = WidgetFlags::default();

// snap angle to nearest 45 degrees and normalize it
// angle must not be close to zero, because it causes major rendering issues in GTK
self.rotation =
Self::normalize_angle(Self::snap_angle(rotation, std::f64::consts::FRAC_PI_4));

widget_flags.view_modified = true;
widget_flags.resize = true;
widget_flags.refresh_canvasmenu = true;
widget_flags
}

Expand All @@ -182,7 +226,9 @@ impl Camera {
let mut widget_flags = WidgetFlags::default();
self.temporary_zoom =
temporary_zoom.clamp(Camera::ZOOM_MIN / self.zoom, Camera::ZOOM_MAX / self.zoom);
widget_flags.zoomed_temporarily = true;
widget_flags.view_modified = true;
widget_flags.resize = true;
widget_flags.refresh_canvasmenu = true;
widget_flags
}

Expand Down Expand Up @@ -249,59 +295,76 @@ impl Camera {
}

/// The viewport in document coordinate space.
///
/// Returns the Aabb enclosing the (potentially rotated) viewport.
pub fn viewport(&self) -> Aabb {
let total_zoom = self.total_zoom();

Aabb::new_positive(
(self.offset / total_zoom).into(),
((self.offset + self.size) / total_zoom).into(),
)
let viewport_surface = Aabb::new(na::point![0.0, 0.0], na::Point2::from(self.size));
self.transform().inverse().transform_aabb(viewport_surface)
}

/// The current viewport center in document coordinate space.
pub fn viewport_center(&self) -> na::Vector2<f64> {
(self.offset + self.size * 0.5) / self.total_zoom()
self.transform()
.inverse()
.transform_point(&na::Point2::from(self.size * 0.5))
.coords
}

/// Set the viewport center.
///
/// `center` must be in document coordinate space.
pub fn set_viewport_center(&mut self, center: na::Vector2<f64>) -> WidgetFlags {
let mut widget_flags = WidgetFlags::default();
self.offset = center * self.total_zoom() - self.size * 0.5;

self.offset = self
.document_to_view_transform()
.transform_point(&na::Point2::from(center))
.coords
- self.size * 0.5;

widget_flags.view_modified = true;
widget_flags.resize = true;
widget_flags
}

/// Transform Aabb from document coords to surface coords.
pub fn transform_bounds(&self, bounds: Aabb) -> Aabb {
bounds.scale(self.total_zoom()).translate(-self.offset)
/// The transform from document coords to view coords (rotation + scale only).
///
/// To get the inverse, call `.inverse()`.
pub fn document_to_view_transform(&self) -> na::Affine2<f64> {
let total_zoom = self.total_zoom();

na::try_convert(
// LHS is applied onto RHS: rotate -> scale
na::Scale2::from(na::Vector2::from_element(total_zoom)).to_homogeneous()
* na::Rotation2::new(self.rotation).to_homogeneous(),
)
.unwrap()
}

/// Transform Aabb from surface coords to document coords.
pub fn transform_inv_bounds(&self, bounds: Aabb) -> Aabb {
bounds.translate(self.offset).scale(1.0 / self.total_zoom())
/// The transform from view coords to surface coords (translation only).
///
/// To get the inverse, call `.inverse()`.
pub fn view_to_surface_transform(&self) -> na::Affine2<f64> {
na::try_convert(na::Translation2::from(-self.offset).to_homogeneous()).unwrap()
}

/// The transform from document coords to surface coords.
/// The transform from document coords to surface coords (full).
///
/// To get the inverse, call `.inverse()`.
pub fn transform(&self) -> na::Affine2<f64> {
let total_zoom = self.total_zoom();

na::try_convert(
// LHS is applied onto RHS, so the order is scaling by zoom -> Translation by offset
na::Translation2::from(-self.offset).to_homogeneous()
* na::Scale2::from(na::Vector2::from_element(total_zoom)).to_homogeneous(),
// LHS is applied onto RHS: document -> view -> surface
self.view_to_surface_transform().to_homogeneous()
* self.document_to_view_transform().to_homogeneous(),
)
.unwrap()
}

/// The gsk transform for the GTK snapshot function.
///
/// GTKs transformations are applied on its coordinate system,
/// so we need to reverse the transformation order (translate, then scale).
/// so we need to reverse the transformation order (translate, then scale, then rotate).
/// Small rotation angles seem to cause major rendering issues.
///
/// To get the inverse, call .invert().
#[cfg(feature = "ui")]
pub fn transform_for_gtk_snapshot(&self) -> gtk4::gsk::Transform {
Expand All @@ -313,16 +376,20 @@ impl Camera {
-self.offset[1] as f32,
))
.scale(total_zoom as f32, total_zoom as f32)
.rotate(self.rotation.to_degrees() as f32)
}

/// Detects if a nudge is needed, meaning: the position is close to an edge of the current viewport.
pub fn detect_nudge_needed(&self, pos: na::Vector2<f64>) -> Option<NudgeDirection> {
const NUDGE_VIEWPORT_DIST: f64 = 10.0;
let viewport = self.viewport();
let nudge_north = pos[1] <= viewport.mins[1] + NUDGE_VIEWPORT_DIST;
let nudge_east = pos[0] >= viewport.maxs[0] - NUDGE_VIEWPORT_DIST;
let nudge_south = pos[1] >= viewport.maxs[1] - NUDGE_VIEWPORT_DIST;
let nudge_west = pos[0] <= viewport.mins[0] + NUDGE_VIEWPORT_DIST;

// Transform position into surface coordinates and compare against the surface viewport.
let pos_surface = self.transform().transform_point(&na::Point2::from(pos));

let nudge_north = pos_surface.y <= NUDGE_VIEWPORT_DIST;
let nudge_east = pos_surface.x >= self.size[0] - NUDGE_VIEWPORT_DIST;
let nudge_south = pos_surface.y >= self.size[1] - NUDGE_VIEWPORT_DIST;
let nudge_west = pos_surface.x <= NUDGE_VIEWPORT_DIST;

match (nudge_north, nudge_east, nudge_south, nudge_west) {
(true, false, _, false) => Some(NudgeDirection::North),
Expand Down
Loading