diff --git a/crates/rnote-compose/src/lib.rs b/crates/rnote-compose/src/lib.rs index 5810cb0a7d..875dee2a62 100644 --- a/crates/rnote-compose/src/lib.rs +++ b/crates/rnote-compose/src/lib.rs @@ -19,6 +19,8 @@ pub mod ext; pub mod penevent; /// module for pen paths pub mod penpath; +/// module for misc operations on points +pub mod point_utils; /// utilities for serializing / deserializing pub mod serialize; /// module for shapes diff --git a/crates/rnote-compose/src/penpath/element.rs b/crates/rnote-compose/src/penpath/element.rs index a7f8696813..b0b7dad15a 100644 --- a/crates/rnote-compose/src/penpath/element.rs +++ b/crates/rnote-compose/src/penpath/element.rs @@ -1,5 +1,8 @@ // Imports -use crate::transform::Transformable; +use crate::{ + point_utils, + transform::{MirrorOrientation, Transformable}, +}; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -35,6 +38,10 @@ impl Transformable for Element { fn scale(&mut self, scale: na::Vector2) { self.pos = self.pos.component_mul(&scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + point_utils::mirror_point(&mut self.pos, centerline, orientation); + } } impl Element { diff --git a/crates/rnote-compose/src/penpath/mod.rs b/crates/rnote-compose/src/penpath/mod.rs index 0bbb1d1d7c..fbf2569684 100644 --- a/crates/rnote-compose/src/penpath/mod.rs +++ b/crates/rnote-compose/src/penpath/mod.rs @@ -9,7 +9,7 @@ pub use segment::Segment; // Imports use crate::ext::{KurboShapeExt, Vector2Ext}; use crate::shapes::{CubicBezier, Line, QuadraticBezier, Shapeable}; -use crate::transform::Transformable; +use crate::transform::{MirrorOrientation, Transformable}; use kurbo::Shape; use p2d::bounding_volume::{Aabb, BoundingVolume}; use serde::{Deserialize, Serialize}; @@ -98,6 +98,14 @@ impl Transformable for PenPath { segment.scale(scale); }); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + self.start.mirror(centerline, orientation); + + self.segments.iter_mut().for_each(|element| { + element.mirror(centerline, orientation); + }); + } } impl PenPath { diff --git a/crates/rnote-compose/src/penpath/segment.rs b/crates/rnote-compose/src/penpath/segment.rs index b8e4cae6c1..510e076e41 100644 --- a/crates/rnote-compose/src/penpath/segment.rs +++ b/crates/rnote-compose/src/penpath/segment.rs @@ -1,6 +1,9 @@ // Imports use super::Element; -use crate::transform::Transformable; +use crate::{ + point_utils, + transform::{MirrorOrientation, Transformable}, +}; use serde::{Deserialize, Serialize}; /// A single segment, usually of a pen path. @@ -93,6 +96,23 @@ impl Transformable for Segment { } } } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + match self { + Segment::LineTo { end } => { + end.mirror(centerline, orientation); + } + Segment::QuadBezTo { cp, end } => { + point_utils::mirror_point(cp, centerline, orientation); + end.mirror(centerline, orientation); + } + Segment::CubBezTo { cp1, cp2, end } => { + point_utils::mirror_point(cp1, centerline, orientation); + point_utils::mirror_point(cp2, centerline, orientation); + end.mirror(centerline, orientation); + } + } + } } impl Segment { diff --git a/crates/rnote-compose/src/point_utils.rs b/crates/rnote-compose/src/point_utils.rs new file mode 100644 index 0000000000..13d0001924 --- /dev/null +++ b/crates/rnote-compose/src/point_utils.rs @@ -0,0 +1,27 @@ +use crate::transform::MirrorOrientation; + +/// horizontally mirrors point around line 'x = centerline_x' +pub fn mirror_point_x(point: &mut na::Vector2, centerline_x: f64) { + point.x -= centerline_x; + point.x *= -1.0; + point.x += centerline_x; +} + +/// vertically mirrors point around line 'y = centerline_y' +pub fn mirror_point_y(point: &mut na::Vector2, centerline_y: f64) { + point.y -= centerline_y; + point.y *= -1.0; + point.y += centerline_y; +} + +/// mirror point around either Horizontal: 'x = centerline' or Vertical: 'y = centerline' line +pub fn mirror_point(point: &mut na::Vector2, centerline: f64, orientation: MirrorOrientation) { + match orientation { + MirrorOrientation::Horizontal => { + mirror_point_x(point, centerline); + } + MirrorOrientation::Vertical => { + mirror_point_y(point, centerline); + } + } +} diff --git a/crates/rnote-compose/src/shapes/arrow.rs b/crates/rnote-compose/src/shapes/arrow.rs index 66a567866d..a8e5e48c83 100644 --- a/crates/rnote-compose/src/shapes/arrow.rs +++ b/crates/rnote-compose/src/shapes/arrow.rs @@ -1,8 +1,8 @@ // Imports use super::Line; -use crate::ext::Vector2Ext; use crate::shapes::Shapeable; -use crate::transform::Transformable; +use crate::transform::{MirrorOrientation, Transformable}; +use crate::{ext::Vector2Ext, point_utils}; use kurbo::{PathEl, Shape}; use na::Rotation2; use p2d::bounding_volume::Aabb; @@ -56,6 +56,11 @@ impl Transformable for Arrow { self.start = self.start.component_mul(&scale); self.tip = self.tip.component_mul(&scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + point_utils::mirror_point(&mut self.start, centerline, orientation); + point_utils::mirror_point(&mut self.tip, centerline, orientation); + } } impl Shapeable for Arrow { diff --git a/crates/rnote-compose/src/shapes/cubbez.rs b/crates/rnote-compose/src/shapes/cubbez.rs index 3d8e0fc82a..e498a72808 100644 --- a/crates/rnote-compose/src/shapes/cubbez.rs +++ b/crates/rnote-compose/src/shapes/cubbez.rs @@ -2,8 +2,9 @@ use super::line::Line; use super::quadbez::QuadraticBezier; use crate::ext::{KurboShapeExt, Vector2Ext}; +use crate::point_utils; use crate::shapes::Shapeable; -use crate::transform::Transformable; +use crate::transform::{MirrorOrientation, Transformable}; use kurbo::Shape; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -50,6 +51,12 @@ impl Transformable for CubicBezier { self.cp2 = self.cp2.component_mul(&scale); self.end = self.end.component_mul(&scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + for point in [&mut self.start, &mut self.cp1, &mut self.cp2, &mut self.end] { + point_utils::mirror_point(point, centerline, orientation); + } + } } impl Shapeable for CubicBezier { diff --git a/crates/rnote-compose/src/shapes/ellipse.rs b/crates/rnote-compose/src/shapes/ellipse.rs index 287c0256af..c9e09a55e9 100644 --- a/crates/rnote-compose/src/shapes/ellipse.rs +++ b/crates/rnote-compose/src/shapes/ellipse.rs @@ -3,7 +3,7 @@ use super::Line; use crate::Transform; use crate::ext::{Affine2Ext, Vector2Ext}; use crate::shapes::Shapeable; -use crate::transform::Transformable; +use crate::transform::{MirrorOrientation, Transformable}; use kurbo::Shape; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -41,6 +41,10 @@ impl Transformable for Ellipse { fn scale(&mut self, scale: na::Vector2) { self.transform.append_scale_mut(scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + self.transform.append_mirror_mut(centerline, orientation); + } } impl Shapeable for Ellipse { diff --git a/crates/rnote-compose/src/shapes/line.rs b/crates/rnote-compose/src/shapes/line.rs index 0451690418..96a7307f83 100644 --- a/crates/rnote-compose/src/shapes/line.rs +++ b/crates/rnote-compose/src/shapes/line.rs @@ -1,9 +1,9 @@ // Imports -use crate::Transform; use crate::ext::{AabbExt, Vector2Ext}; use crate::shapes::Rectangle; use crate::shapes::Shapeable; -use crate::transform::Transformable; +use crate::transform::{MirrorOrientation, Transformable}; +use crate::{Transform, point_utils}; use kurbo::Shape; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -38,6 +38,11 @@ impl Transformable for Line { self.start = self.start.component_mul(&scale); self.end = self.end.component_mul(&scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + point_utils::mirror_point(&mut self.start, centerline, orientation); + point_utils::mirror_point(&mut self.end, centerline, orientation); + } } impl Shapeable for Line { diff --git a/crates/rnote-compose/src/shapes/polygon.rs b/crates/rnote-compose/src/shapes/polygon.rs index 6ba8619512..e598ed93b7 100644 --- a/crates/rnote-compose/src/shapes/polygon.rs +++ b/crates/rnote-compose/src/shapes/polygon.rs @@ -1,7 +1,8 @@ // Imports use super::{Line, Shapeable}; use crate::ext::{AabbExt, Vector2Ext}; -use crate::transform::Transformable; +use crate::point_utils; +use crate::transform::{MirrorOrientation, Transformable}; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -41,6 +42,14 @@ impl Transformable for Polygon { *p = p.component_mul(&scale); } } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + point_utils::mirror_point(&mut self.start, centerline, orientation); + + self.path.iter_mut().for_each(|point| { + point_utils::mirror_point(point, centerline, orientation); + }); + } } impl Shapeable for Polygon { diff --git a/crates/rnote-compose/src/shapes/polyline.rs b/crates/rnote-compose/src/shapes/polyline.rs index 9e4bb4d92e..c9e9bcc227 100644 --- a/crates/rnote-compose/src/shapes/polyline.rs +++ b/crates/rnote-compose/src/shapes/polyline.rs @@ -1,7 +1,7 @@ // Imports use super::{Line, Shapeable}; -use crate::ext::Vector2Ext; -use crate::transform::Transformable; +use crate::transform::{MirrorOrientation, Transformable}; +use crate::{ext::Vector2Ext, point_utils}; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -41,6 +41,14 @@ impl Transformable for Polyline { *p = p.component_mul(&scale); } } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + point_utils::mirror_point(&mut self.start, centerline, orientation); + + self.path.iter_mut().for_each(|point| { + point_utils::mirror_point(point, centerline, orientation); + }); + } } impl Shapeable for Polyline { diff --git a/crates/rnote-compose/src/shapes/quadbez.rs b/crates/rnote-compose/src/shapes/quadbez.rs index 0d7d4997e0..02350d0160 100644 --- a/crates/rnote-compose/src/shapes/quadbez.rs +++ b/crates/rnote-compose/src/shapes/quadbez.rs @@ -2,8 +2,9 @@ use super::CubicBezier; use super::line::Line; use crate::ext::{KurboShapeExt, Vector2Ext}; +use crate::point_utils; use crate::shapes::Shapeable; -use crate::transform::Transformable; +use crate::transform::{MirrorOrientation, Transformable}; use kurbo::Shape; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -44,6 +45,12 @@ impl Transformable for QuadraticBezier { self.cp = self.cp.component_mul(&scale); self.end = self.end.component_mul(&scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + for point in [&mut self.start, &mut self.cp, &mut self.end] { + point_utils::mirror_point(point, centerline, orientation); + } + } } impl Shapeable for QuadraticBezier { diff --git a/crates/rnote-compose/src/shapes/rectangle.rs b/crates/rnote-compose/src/shapes/rectangle.rs index c0cc3f5e54..afa769b153 100644 --- a/crates/rnote-compose/src/shapes/rectangle.rs +++ b/crates/rnote-compose/src/shapes/rectangle.rs @@ -3,7 +3,7 @@ use super::Line; use crate::Transform; use crate::ext::{AabbExt, Vector2Ext}; use crate::shapes::Shapeable; -use crate::transform::Transformable; +use crate::transform::{MirrorOrientation, Transformable}; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -81,6 +81,10 @@ impl Transformable for Rectangle { fn scale(&mut self, scale: na::Vector2) { self.transform.append_scale_mut(scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + self.transform.mirror(centerline, orientation); + } } impl Rectangle { diff --git a/crates/rnote-compose/src/shapes/shape.rs b/crates/rnote-compose/src/shapes/shape.rs index ad30f8298c..624ff1a3c6 100644 --- a/crates/rnote-compose/src/shapes/shape.rs +++ b/crates/rnote-compose/src/shapes/shape.rs @@ -2,7 +2,7 @@ use super::{ Arrow, CubicBezier, Ellipse, Line, Polygon, Polyline, QuadraticBezier, Rectangle, Shapeable, }; -use crate::transform::Transformable; +use crate::transform::{MirrorOrientation, Transformable}; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -129,6 +129,35 @@ impl Transformable for Shape { } } } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + match self { + Self::Line(line) => { + line.mirror(centerline, orientation); + } + Self::Arrow(arrow) => { + arrow.mirror(centerline, orientation); + } + Self::Rectangle(rectangle) => { + rectangle.mirror(centerline, orientation); + } + Self::Ellipse(ellipse) => { + ellipse.mirror(centerline, orientation); + } + Self::QuadraticBezier(quadratic_bezier) => { + quadratic_bezier.mirror(centerline, orientation); + } + Self::CubicBezier(cubic_bezier) => { + cubic_bezier.mirror(centerline, orientation); + } + Self::Polyline(polyline) => { + polyline.mirror(centerline, orientation); + } + Self::Polygon(polygon) => { + polygon.mirror(centerline, orientation); + } + } + } } impl Shapeable for Shape { diff --git a/crates/rnote-compose/src/transform/mod.rs b/crates/rnote-compose/src/transform/mod.rs index d6be392f15..816ca4beb1 100644 --- a/crates/rnote-compose/src/transform/mod.rs +++ b/crates/rnote-compose/src/transform/mod.rs @@ -18,6 +18,15 @@ pub struct Transform { pub affine: na::Affine2, } +/// Selection of which direction a mirror should use +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum MirrorOrientation { + /// Mirror is applied accross the line 'x = centerline' + Horizontal, + /// Mirror is applied accross the line 'y = centerline' + Vertical, +} + impl Default for Transform { fn default() -> Self { Self { @@ -53,6 +62,10 @@ impl Transformable for Transform { fn scale(&mut self, scale: na::Vector2) { self.append_scale_mut(scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + self.append_mirror_mut(centerline, orientation); + } } impl Transform { @@ -120,6 +133,31 @@ impl Transform { .unwrap(); } + /// Apply a reflection across either Horizontal: 'x = centerline' or Vertical: 'y = centerline' to the + /// affine matrix based on the orientation + pub fn append_mirror_mut(&mut self, centerline: f64, orientation: MirrorOrientation) { + let mirror_transformation = match orientation { + MirrorOrientation::Horizontal => { + na::matrix![ + -1.0, 0.0, 2.0 * centerline; + 0.0, 1.0, 0.0; + 0.0, 0.0, 1.0; + ] + } + MirrorOrientation::Vertical => { + na::matrix![ + 1.0, 0.0, 0.0; + 0.0, -1.0, 2.0 * centerline; + 0.0, 0.0, 1.0; + ] + } + }; + + let transformed_affine = mirror_transformation * self.affine.matrix(); + + self.affine = na::Affine2::from_matrix_unchecked(transformed_affine); + } + /// Convert the transform to a Svg attribute string, insertable into svg elements. pub fn to_svg_transform_attr_str(&self) -> String { let matrix = self.affine; diff --git a/crates/rnote-compose/src/transform/transformable.rs b/crates/rnote-compose/src/transform/transformable.rs index 27c9d37585..8ebe25d03d 100644 --- a/crates/rnote-compose/src/transform/transformable.rs +++ b/crates/rnote-compose/src/transform/transformable.rs @@ -1,3 +1,5 @@ +use crate::transform::MirrorOrientation; + /// Trait for types that can be (geometrically) transformed. pub trait Transformable { /// Translate (as in moves) by the given offset. @@ -6,4 +8,6 @@ pub trait Transformable { fn rotate(&mut self, angle: f64, center: na::Point2); /// Scale by the given scale-factor. fn scale(&mut self, scale: na::Vector2); + /// Mirror around either Horizontal: 'x = centerline' or Vertical: 'y = centerline' + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation); } diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 936c293fc2..4a753bb4a4 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -34,6 +34,7 @@ use p2d::bounding_volume::{Aabb, BoundingVolume}; use rnote_compose::eventresult::EventPropagation; use rnote_compose::ext::AabbExt; use rnote_compose::penevent::{PenEvent, ShortcutKey}; +use rnote_compose::transform::MirrorOrientation; use rnote_compose::{Color, SplitOrder}; use serde::{Deserialize, Serialize}; use snapshot::Snapshotable; @@ -753,6 +754,22 @@ impl Engine { | self.update_rendering_current_viewport() } + pub fn mirror_horizontal_selection(&mut self) -> WidgetFlags { + self.store.mirror_stroke( + &self.store.selection_keys_as_rendered(), + MirrorOrientation::Horizontal, + ) | self.record(Instant::now()) + | self.update_content_rendering_current_viewport() + } + + pub fn mirror_vertical_selection(&mut self) -> WidgetFlags { + self.store.mirror_stroke( + &self.store.selection_keys_as_rendered(), + MirrorOrientation::Vertical, + ) | self.record(Instant::now()) + | self.update_content_rendering_current_viewport() + } + pub fn select_with_bounds( &mut self, bounds: Aabb, diff --git a/crates/rnote-engine/src/render.rs b/crates/rnote-engine/src/render.rs index e70e865acd..a1db9eb864 100644 --- a/crates/rnote-engine/src/render.rs +++ b/crates/rnote-engine/src/render.rs @@ -8,7 +8,7 @@ use p2d::bounding_volume::{Aabb, BoundingVolume}; use piet::RenderContext; use rnote_compose::ext::AabbExt; use rnote_compose::shapes::{Rectangle, Shapeable}; -use rnote_compose::transform::Transformable; +use rnote_compose::transform::{MirrorOrientation, Transformable}; use serde::{Deserialize, Serialize}; use std::io::{self, Cursor}; use std::sync::Arc; @@ -186,6 +186,10 @@ impl Transformable for Image { fn scale(&mut self, scale: na::Vector2) { self.rect.scale(scale) } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + self.rect.mirror(centerline, orientation); + } } impl Image { diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 7acfa08093..edb089a007 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -3,6 +3,7 @@ use super::StrokeKey; use super::render_comp::RenderCompState; use crate::engine::StrokeContent; use crate::strokes::{Content, Stroke}; +use crate::widgetflags::PopupMessage; use crate::{StrokeStore, WidgetFlags}; use geo::intersects::Intersects; use geo::prelude::Contains; @@ -10,7 +11,7 @@ use p2d::bounding_volume::{Aabb, BoundingVolume}; use rnote_compose::Color; use rnote_compose::penpath::Element; use rnote_compose::shapes::Shapeable; -use rnote_compose::transform::Transformable; +use rnote_compose::transform::{MirrorOrientation, Transformable}; use std::sync::Arc; #[cfg(feature = "ui")] use tracing::error; @@ -298,6 +299,86 @@ impl StrokeStore { widget_flags } + /// Mirror stroke either horizontally or vertically for given set of keys + /// + /// The strokes need to update rendering after mirror + pub(crate) fn mirror_stroke( + &mut self, + keys: &[StrokeKey], + orientation: MirrorOrientation, + ) -> WidgetFlags { + let mut widget_flags = WidgetFlags::default(); + + if keys.is_empty() { + return widget_flags; + } + + let stroke_contains_text = keys.iter().any(|&key| { + matches!( + Arc::make_mut(&mut self.stroke_components) + .get_mut(key) + .map(Arc::make_mut), + Some(Stroke::TextStroke(_)) + ) + }); + + if stroke_contains_text { + widget_flags.popup_message = Some(PopupMessage::MirrorText); + return widget_flags; + } + + let all_stroke_bounds = self.strokes_bounds(keys); + + let min_component; + let max_component; + + match orientation { + MirrorOrientation::Horizontal => { + min_component = all_stroke_bounds + .iter() + .map(|aabb_element| aabb_element.mins.coords.x) + .reduce(|a, b| a.min(b)); + max_component = all_stroke_bounds + .iter() + .map(|aabb_element| aabb_element.maxs.coords.x) + .reduce(|a, b| a.max(b)); + } + MirrorOrientation::Vertical => { + min_component = all_stroke_bounds + .iter() + .map(|aabb_element| aabb_element.mins.coords.y) + .reduce(|a, b| a.min(b)); + max_component = all_stroke_bounds + .iter() + .map(|aabb_element| aabb_element.maxs.coords.y) + .reduce(|a, b| a.max(b)); + } + } + + let selection_centerline = + if let (Some(min_component), Some(max_component)) = (min_component, max_component) { + (min_component + max_component) / 2.0 + } else { + return widget_flags; + }; + + keys.iter().for_each(|&key| { + if let Some(stroke) = Arc::make_mut(&mut self.stroke_components) + .get_mut(key) + .map(Arc::make_mut) + { + stroke.mirror(selection_centerline, orientation); + self.set_rendering_dirty(key); + } + }); + self.update_geometry_for_strokes(keys); + + widget_flags.redraw = true; + widget_flags.store_modified = true; + + widget_flags + } + /// Invert the stroke, text and fill color of the given keys. /// /// Strokes then need to update their rendering. diff --git a/crates/rnote-engine/src/strokes/bitmapimage.rs b/crates/rnote-engine/src/strokes/bitmapimage.rs index 74f24ab898..facf1e1fd5 100644 --- a/crates/rnote-engine/src/strokes/bitmapimage.rs +++ b/crates/rnote-engine/src/strokes/bitmapimage.rs @@ -13,8 +13,8 @@ use rnote_compose::color; use rnote_compose::ext::{AabbExt, Affine2Ext}; use rnote_compose::shapes::Rectangle; use rnote_compose::shapes::Shapeable; -use rnote_compose::transform::Transform; use rnote_compose::transform::Transformable; +use rnote_compose::transform::{MirrorOrientation, Transform}; use serde::{Deserialize, Serialize}; use std::ops::Range; @@ -95,6 +95,10 @@ impl Transformable for BitmapImage { fn scale(&mut self, scale: na::Vector2) { self.rectangle.scale(scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + self.rectangle.mirror(centerline, orientation); + } } impl BitmapImage { diff --git a/crates/rnote-engine/src/strokes/brushstroke.rs b/crates/rnote-engine/src/strokes/brushstroke.rs index b3d344135c..898e7359bb 100644 --- a/crates/rnote-engine/src/strokes/brushstroke.rs +++ b/crates/rnote-engine/src/strokes/brushstroke.rs @@ -11,7 +11,7 @@ use rnote_compose::ext::AabbExt; use rnote_compose::penpath::{Element, Segment}; use rnote_compose::shapes::Shapeable; use rnote_compose::style::Composer; -use rnote_compose::transform::Transformable; +use rnote_compose::transform::{MirrorOrientation, Transformable}; use rnote_compose::{PenPath, Style}; use serde::{Deserialize, Serialize}; use tracing::error; @@ -255,6 +255,10 @@ impl Transformable for BrushStroke { self.style .set_stroke_width(self.style.stroke_width() * scale_scalar); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + self.path.mirror(centerline, orientation); + } } impl BrushStroke { diff --git a/crates/rnote-engine/src/strokes/shapestroke.rs b/crates/rnote-engine/src/strokes/shapestroke.rs index 9fa590180e..2d5a2133e5 100644 --- a/crates/rnote-engine/src/strokes/shapestroke.rs +++ b/crates/rnote-engine/src/strokes/shapestroke.rs @@ -7,6 +7,7 @@ use rnote_compose::ext::AabbExt; use rnote_compose::shapes::Shape; use rnote_compose::shapes::Shapeable; use rnote_compose::style::Composer; +use rnote_compose::transform::MirrorOrientation; use rnote_compose::transform::Transformable; use serde::{Deserialize, Serialize}; @@ -98,6 +99,10 @@ impl Transformable for ShapeStroke { self.style .set_stroke_width(self.style.stroke_width() * scale_scalar); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + self.shape.mirror(centerline, orientation); + } } impl ShapeStroke { diff --git a/crates/rnote-engine/src/strokes/stroke.rs b/crates/rnote-engine/src/strokes/stroke.rs index d426421554..b64e962c15 100644 --- a/crates/rnote-engine/src/strokes/stroke.rs +++ b/crates/rnote-engine/src/strokes/stroke.rs @@ -14,8 +14,8 @@ use rnote_compose::ext::AabbExt; use rnote_compose::penpath::Element; use rnote_compose::shapes::{Rectangle, Shapeable}; use rnote_compose::style::smooth::SmoothOptions; -use rnote_compose::transform::Transform; use rnote_compose::transform::Transformable; +use rnote_compose::transform::{MirrorOrientation, Transform}; use rnote_compose::{Color, PenPath, Style}; use serde::{Deserialize, Serialize}; use tracing::error; @@ -199,6 +199,26 @@ impl Transformable for Stroke { } } } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + match self { + Self::BrushStroke(brushstroke) => { + brushstroke.mirror(centerline, orientation); + } + Self::ShapeStroke(shape_stroke) => { + shape_stroke.mirror(centerline, orientation); + } + Self::VectorImage(vector_image) => { + vector_image.mirror(centerline, orientation); + } + Self::BitmapImage(bitmap_image) => { + bitmap_image.mirror(centerline, orientation); + } + Self::TextStroke(text_stroke) => { + text_stroke.mirror(centerline, orientation); + } + } + } } impl Stroke { diff --git a/crates/rnote-engine/src/strokes/textstroke.rs b/crates/rnote-engine/src/strokes/textstroke.rs index 2ca3b3e28d..e4239a2dbb 100644 --- a/crates/rnote-engine/src/strokes/textstroke.rs +++ b/crates/rnote-engine/src/strokes/textstroke.rs @@ -7,7 +7,7 @@ use p2d::bounding_volume::Aabb; use piet::{RenderContext, TextLayout, TextLayoutBuilder}; use rnote_compose::ext::{AabbExt, Affine2Ext, Vector2Ext}; use rnote_compose::shapes::Shapeable; -use rnote_compose::transform::Transformable; +use rnote_compose::transform::{MirrorOrientation, Transformable}; use rnote_compose::{Color, Transform, color}; use serde::{Deserialize, Serialize}; use std::ops::Range; @@ -465,6 +465,9 @@ impl Transformable for TextStroke { fn scale(&mut self, scale: na::Vector2) { self.transform.append_scale_mut(scale); } + + // no mirroring for text as of now + fn mirror(&mut self, _centerline: f64, _orientation: MirrorOrientation) {} } impl Shapeable for TextStroke { diff --git a/crates/rnote-engine/src/strokes/vectorimage.rs b/crates/rnote-engine/src/strokes/vectorimage.rs index 611b2874a9..c1749e6c00 100644 --- a/crates/rnote-engine/src/strokes/vectorimage.rs +++ b/crates/rnote-engine/src/strokes/vectorimage.rs @@ -12,8 +12,8 @@ use rnote_compose::color; use rnote_compose::ext::AabbExt; use rnote_compose::shapes::Rectangle; use rnote_compose::shapes::Shapeable; -use rnote_compose::transform::Transform; use rnote_compose::transform::Transformable; +use rnote_compose::transform::{MirrorOrientation, Transform}; use serde::{Deserialize, Serialize}; use std::ops::Range; use std::sync::Arc; @@ -134,6 +134,10 @@ impl Transformable for VectorImage { fn scale(&mut self, scale: na::Vector2) { self.rectangle.scale(scale); } + + fn mirror(&mut self, centerline: f64, orientation: MirrorOrientation) { + self.rectangle.mirror(centerline, orientation); + } } impl VectorImage { diff --git a/crates/rnote-engine/src/widgetflags.rs b/crates/rnote-engine/src/widgetflags.rs index d348063a9c..ed6f2733e6 100644 --- a/crates/rnote-engine/src/widgetflags.rs +++ b/crates/rnote-engine/src/widgetflags.rs @@ -1,3 +1,9 @@ +/// Types of messages that can be sent to the user via a dispatch_toast_text +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum PopupMessage { + MirrorText, +} + /// Flags returned to the UI widget that holds the engine. #[must_use] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -26,6 +32,10 @@ pub struct WidgetFlags { /// Meaning, when enabled instead of key events, text events are then emitted /// for regular unicode text. Used when writing text with the typewriter. pub enable_text_preprocessing: Option, + /// If Some, a popup message is sent to the user, through a dispatch_toast_text message. + /// Intended to notify the user that an operation they performed could not be completed + /// or is not possible + pub popup_message: Option, } impl Default for WidgetFlags { @@ -42,6 +52,7 @@ impl Default for WidgetFlags { hide_undo: None, hide_redo: None, enable_text_preprocessing: None, + popup_message: None, } } } @@ -74,5 +85,8 @@ impl std::ops::BitOrAssign for WidgetFlags { if rhs.enable_text_preprocessing.is_some() { self.enable_text_preprocessing = rhs.enable_text_preprocessing; } + if rhs.popup_message.is_some() { + self.popup_message = rhs.popup_message.clone(); + } } } diff --git a/crates/rnote-ui/data/icons/scalable/actions/selection-mirror-horizontal-symbolic.svg b/crates/rnote-ui/data/icons/scalable/actions/selection-mirror-horizontal-symbolic.svg new file mode 100644 index 0000000000..13202cdd0e --- /dev/null +++ b/crates/rnote-ui/data/icons/scalable/actions/selection-mirror-horizontal-symbolic.svg @@ -0,0 +1,147 @@ + +b'Body'b'Body' diff --git a/crates/rnote-ui/data/icons/scalable/actions/selection-mirror-vertical-symbolic.svg b/crates/rnote-ui/data/icons/scalable/actions/selection-mirror-vertical-symbolic.svg new file mode 100644 index 0000000000..b93d496862 --- /dev/null +++ b/crates/rnote-ui/data/icons/scalable/actions/selection-mirror-vertical-symbolic.svg @@ -0,0 +1,147 @@ + +b'Body'b'Body' diff --git a/crates/rnote-ui/data/resources.gresource.xml b/crates/rnote-ui/data/resources.gresource.xml index 5532364b4b..284b229293 100644 --- a/crates/rnote-ui/data/resources.gresource.xml +++ b/crates/rnote-ui/data/resources.gresource.xml @@ -115,6 +115,8 @@ icons/scalable/actions/return-origin-page-symbolic.svg icons/scalable/actions/save-symbolic.svg icons/scalable/actions/selection-deselect-all-symbolic.svg + icons/scalable/actions/selection-mirror-horizontal-symbolic.svg + icons/scalable/actions/selection-mirror-vertical-symbolic.svg icons/scalable/actions/selection-duplicate-symbolic.svg icons/scalable/actions/selection-invert-color-symbolic.svg icons/scalable/actions/selection-resize-lock-aspectratio-symbolic.svg diff --git a/crates/rnote-ui/data/ui/penssidebar/selectorpage.ui b/crates/rnote-ui/data/ui/penssidebar/selectorpage.ui index 705810264d..024d68aaae 100644 --- a/crates/rnote-ui/data/ui/penssidebar/selectorpage.ui +++ b/crates/rnote-ui/data/ui/penssidebar/selectorpage.ui @@ -100,6 +100,28 @@ + + + Mirror Selection Horizontally + win.selection-mirror-horizontal + selection-mirror-horizontal-symbolic + + + + + + Mirror Selection Vertically + win.selection-mirror-vertical + selection-mirror-vertical-symbolic + + + Invert Color Brightness of All Selected Strokes diff --git a/crates/rnote-ui/data/ui/shortcuts.ui b/crates/rnote-ui/data/ui/shortcuts.ui index 2b7e2ac840..6a83d20dcd 100644 --- a/crates/rnote-ui/data/ui/shortcuts.ui +++ b/crates/rnote-ui/data/ui/shortcuts.ui @@ -255,6 +255,18 @@ <ctrl><shift>z + + + Mirror Selection Horizontally + <ctrl>m + + + + + Mirror Selection Vertically + <ctrl><shift>m + + diff --git a/crates/rnote-ui/src/appwindow/actions.rs b/crates/rnote-ui/src/appwindow/actions.rs index 740ca2245e..3d862d971c 100644 --- a/crates/rnote-ui/src/appwindow/actions.rs +++ b/crates/rnote-ui/src/appwindow/actions.rs @@ -87,6 +87,12 @@ impl RnAppWindow { self.add_action(&action_selection_select_all); let action_selection_deselect_all = gio::SimpleAction::new("selection-deselect-all", None); self.add_action(&action_selection_deselect_all); + let action_selection_mirror_horizontal = + gio::SimpleAction::new("selection-mirror-horizontal", None); + self.add_action(&action_selection_mirror_horizontal); + let action_selection_mirror_vertical = + gio::SimpleAction::new("selection-mirror-vertical", None); + self.add_action(&action_selection_mirror_vertical); let action_clear_doc = gio::SimpleAction::new("clear-doc", None); self.add_action(&action_clear_doc); let action_new_doc = gio::SimpleAction::new("new-doc", None); @@ -464,6 +470,32 @@ impl RnAppWindow { } )); + // Mirror selection horizontally + action_selection_mirror_horizontal.connect_activate(clone!( + #[weak(rename_to=appwindow)] + self, + move |_, _| { + let Some(canvas) = appwindow.active_tab_canvas() else { + return; + }; + let widget_flags = canvas.engine_mut().mirror_horizontal_selection(); + appwindow.handle_widget_flags(widget_flags, &canvas); + } + )); + + // Mirror selection vertically + action_selection_mirror_vertical.connect_activate(clone!( + #[weak(rename_to=appwindow)] + self, + move |_, _| { + let Some(canvas) = appwindow.active_tab_canvas() else { + return; + }; + let widget_flags = canvas.engine_mut().mirror_vertical_selection(); + appwindow.handle_widget_flags(widget_flags, &canvas); + } + )); + // Clear doc action_clear_doc.connect_activate(clone!( #[weak(rename_to=appwindow)] @@ -1028,6 +1060,8 @@ impl RnAppWindow { app.set_accels_for_action("win.pen-style::eraser", &["4", "KP_4"]); app.set_accels_for_action("win.pen-style::selector", &["5", "KP_5"]); app.set_accels_for_action("win.pen-style::tools", &["6", "KP_6"]); + app.set_accels_for_action("win.selection-mirror-horizontal", &["m"]); + app.set_accels_for_action("win.selection-mirror-vertical", &["m"]); // shortcuts for devel build if config::PROFILE.to_lowercase().as_str() == "devel" { diff --git a/crates/rnote-ui/src/appwindow/mod.rs b/crates/rnote-ui/src/appwindow/mod.rs index d8b7e23bb3..049a4c1597 100644 --- a/crates/rnote-ui/src/appwindow/mod.rs +++ b/crates/rnote-ui/src/appwindow/mod.rs @@ -6,7 +6,7 @@ mod imp; // Imports use crate::{ FileType, RnApp, RnCanvas, RnCanvasWrapper, RnMainHeader, RnOverlays, RnSidebar, config, - dialogs, env, + dialogs, env, overlays, }; use adw::{prelude::*, subclass::prelude::*}; use core::cell::{Ref, RefMut}; @@ -19,6 +19,7 @@ use rnote_engine::ext::GdkRGBAExt; use rnote_engine::pens::PenStyle; use rnote_engine::pens::pensconfig::brushconfig::BrushStyle; use rnote_engine::pens::pensconfig::shaperconfig::ShaperStyle; +use rnote_engine::widgetflags::PopupMessage; use rnote_engine::{WidgetFlags, engine::EngineTask}; use std::path::Path; use tracing::{debug, error}; @@ -335,6 +336,16 @@ impl RnAppWindow { if let Some(enable_text_preprocessing) = widget_flags.enable_text_preprocessing { canvas.set_text_preprocessing(enable_text_preprocessing); } + if let Some(popup_message) = widget_flags.popup_message { + let popup_text = match popup_message { + PopupMessage::MirrorText => { + gettext("Mirroring selections containing text is not supported") + } + }; + + self.overlays() + .dispatch_toast_text(&popup_text, overlays::TEXT_TOAST_TIMEOUT_DEFAULT); + } } /// Get the active (selected) tab page.