Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8c03cdb
added buttons to UI with preliminary icons
podzolelements Aug 3, 2025
d543167
horizontal mirroring is functional for BrushStrokes
podzolelements Aug 3, 2025
039277e
center horizontal mirrors on selection area, not individual strokes
podzolelements Aug 3, 2025
761ede0
horizontal mirroring of ShapeStrokes is fully functional
podzolelements Aug 4, 2025
8555e6d
apply code formatting rules, no functional changes
podzolelements Aug 4, 2025
a5c3d41
horizontal mirroring functional on both bitmapped and vector images
podzolelements Aug 5, 2025
1eaaff6
vertical mirroring fully implemented
podzolelements Aug 6, 2025
a40d12a
update icons
podzolelements Aug 7, 2025
bd036d3
condense mirror_stroke_* functions with orientation parameter
podzolelements Aug 21, 2025
7fe6cc3
move point mirroring functions into rnote-compose
podzolelements Aug 21, 2025
0f64659
move affine matrix transformations into Transform
podzolelements Aug 22, 2025
d95cbb4
add mirroring to Segments and Elements; internalize PenPath mirroring
podzolelements Aug 22, 2025
ee8fd4d
move mirroring logic out of Stroke for point-based shapes
podzolelements Aug 22, 2025
9036e09
add mirroring for transform-based shapes
podzolelements Aug 22, 2025
01d37ce
add direct mirroring to BrushStrokes
podzolelements Aug 22, 2025
fa56d6f
add mirroring as an operation to the Transformable trait
podzolelements Aug 22, 2025
5061f26
condense Transformable mirror functions with orientation paremeter
podzolelements Aug 23, 2025
27bb53a
add orientation based mirror_point
podzolelements Aug 23, 2025
bff1482
fix code style, no functional changes
podzolelements Aug 24, 2025
a1b167f
prevent mirror operations with TextStrokes selected
podzolelements Aug 25, 2025
019285a
add keyboard shortcuts
podzolelements Aug 26, 2025
af9d5d0
change keyboard shortcuts and bind them to gtk accels
podzolelements Aug 26, 2025
9a63a48
modify WidgetFlags to send a popup message to the user
podzolelements Aug 26, 2025
e7322e0
relocate gettext call
podzolelements Aug 27, 2025
4159527
properly update stroke geometry
podzolelements Aug 27, 2025
53da69d
move WidgetFlags text popup proccessing into rnote-ui
podzolelements Aug 28, 2025
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
14 changes: 14 additions & 0 deletions crates/rnote-engine/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,20 @@ impl Engine {
| self.update_rendering_current_viewport()
}

pub fn mirror_horizontal_selection(&mut self) -> WidgetFlags {
self.store
.mirror_stroke_horizontal(&self.store.selection_keys_as_rendered())
| self.record(Instant::now())
| self.update_content_rendering_current_viewport()
}

pub fn mirror_vertical_selection(&mut self) -> WidgetFlags {
self.store
.mirror_stroke_vertical(&self.store.selection_keys_as_rendered())
| self.record(Instant::now())
| self.update_content_rendering_current_viewport()
}

pub fn select_with_bounds(
&mut self,
bounds: Aabb,
Expand Down
86 changes: 86 additions & 0 deletions crates/rnote-engine/src/store/stroke_comp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,92 @@ impl StrokeStore {
widget_flags
}

// Mirror stroke horizontally for given set of keys
//
// The strokes need to update rendering after mirror
pub(crate) fn mirror_stroke_horizontal(&mut self, keys: &[StrokeKey]) -> WidgetFlags {
let mut widget_flags = WidgetFlags::default();

if keys.is_empty() {
return widget_flags;
}

let all_stroke_bounds = self.strokes_bounds(keys);

let min_x = all_stroke_bounds
.iter()
.map(|aabb_element| aabb_element.mins.coords.x)
.reduce(|a, b| a.min(b));
let max_x = all_stroke_bounds
.iter()
.map(|aabb_element| aabb_element.maxs.coords.x)
.reduce(|a, b| a.max(b));

let selection_centerline_x = if let (Some(min_x), Some(max_x)) = (min_x, max_x) {
(min_x + max_x) / 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.horizontal_mirror(selection_centerline_x);
self.set_rendering_dirty(key);
}
});

widget_flags.redraw = true;
widget_flags.store_modified = true;

widget_flags
}

/// Mirror stroke vertically for given set of keys
///
/// The strokes must update rendering after a mirror
pub(crate) fn mirror_stroke_vertical(&mut self, keys: &[StrokeKey]) -> WidgetFlags {
let mut widget_flags = WidgetFlags::default();

if keys.is_empty() {
return widget_flags;
}

let all_stroke_bounds = self.strokes_bounds(keys);

let min_y = all_stroke_bounds
.iter()
.map(|aabb_element| aabb_element.mins.coords.y)
.reduce(|a, b| a.min(b));
let max_y = all_stroke_bounds
.iter()
.map(|aabb_element| aabb_element.maxs.coords.y)
.reduce(|a, b| a.max(b));

let selection_centerline_y = if let (Some(min_y), Some(max_y)) = (min_y, max_y) {
(min_y + max_y) / 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.vertical_mirror(selection_centerline_y);
self.set_rendering_dirty(key);
}
});

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.
Expand Down
224 changes: 224 additions & 0 deletions crates/rnote-engine/src/strokes/stroke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,4 +672,228 @@ impl Stroke {
}
}
}

// horizontally mirrors point around line 'x = selection_centerline_x'
fn mirror_point_x(point: &mut na::Vector2<f64>, selection_centerline_x: f64) {
point.x -= selection_centerline_x;
point.x *= -1.0;
point.x += selection_centerline_x;
}

// performs a matrix transform on 2d affine matrix causing a reflection across line 'x = selection_centerline_x'
fn mirror_affine_x(affine: &mut na::Affine2<f64>, selection_centerline_x: f64) {
let mirror_transformation_x = na::matrix![
-1.0, 0.0, 2.0 * selection_centerline_x;
0.0, 1.0, 0.0;
0.0, 0.0, 1.0;
];

let transformed_affine = mirror_transformation_x * affine.matrix();

*affine = na::Affine2::from_matrix_unchecked(transformed_affine);
}

pub fn horizontal_mirror(&mut self, selection_centerline_x: f64) {
match self {
Stroke::BrushStroke(brushstroke) => {
let current_penpath_elements = brushstroke.path.clone().into_elements();

let mut coords = current_penpath_elements
.iter()
.map(|element| element.pos)
.collect::<Vec<na::Vector2<f64>>>();

for coord in coords.iter_mut() {
Self::mirror_point_x(coord, selection_centerline_x);
}

let new_penpath_elements: Vec<Element> = current_penpath_elements
.iter()
.zip(coords.iter())
.map(|(current_penpath_element, new_position)| Element {
pos: *new_position,
..*current_penpath_element
})
.collect();

if let Some(new_penpath) = PenPath::try_from_elements(new_penpath_elements) {
brushstroke.path = new_penpath;
}
}
Stroke::ShapeStroke(shape_stroke) => match &mut shape_stroke.shape {
rnote_compose::Shape::Line(line) => {
Self::mirror_point_x(&mut line.start, selection_centerline_x);
Self::mirror_point_x(&mut line.end, selection_centerline_x);
}
rnote_compose::Shape::Arrow(arrow) => {
Self::mirror_point_x(&mut arrow.start, selection_centerline_x);
Self::mirror_point_x(&mut arrow.tip, selection_centerline_x);
}
rnote_compose::Shape::Rectangle(rectangle) => {
Self::mirror_affine_x(&mut rectangle.transform.affine, selection_centerline_x);
}
rnote_compose::Shape::Ellipse(ellipse) => {
Self::mirror_affine_x(&mut ellipse.transform.affine, selection_centerline_x);
}
rnote_compose::Shape::QuadraticBezier(quadratic_bezier) => {
for point in [
&mut quadratic_bezier.start,
&mut quadratic_bezier.end,
&mut quadratic_bezier.cp,
] {
Self::mirror_point_x(point, selection_centerline_x);
}
}
rnote_compose::Shape::CubicBezier(cubic_bezier) => {
for point in [
&mut cubic_bezier.start,
&mut cubic_bezier.end,
&mut cubic_bezier.cp1,
&mut cubic_bezier.cp2,
] {
Self::mirror_point_x(point, selection_centerline_x);
}
}
rnote_compose::Shape::Polyline(polyline) => {
Self::mirror_point_x(&mut polyline.start, selection_centerline_x);

for point in polyline.path.iter_mut() {
Self::mirror_point_x(point, selection_centerline_x);
}
}
rnote_compose::Shape::Polygon(polygon) => {
Self::mirror_point_x(&mut polygon.start, selection_centerline_x);

for point in polygon.path.iter_mut() {
Self::mirror_point_x(point, selection_centerline_x);
}
}
},
Stroke::VectorImage(vector_image) => {
Self::mirror_affine_x(
&mut vector_image.rectangle.transform.affine,
selection_centerline_x,
);
}
Stroke::BitmapImage(bitmap_image) => {
Self::mirror_affine_x(
&mut bitmap_image.rectangle.transform.affine,
selection_centerline_x,
);
}
Stroke::TextStroke(_) => {}
}
}

/// vertically mirrors point around line 'y = selection_centerline_y'
fn mirror_point_y(point: &mut na::Vector2<f64>, selection_centerline_y: f64) {
point.y -= selection_centerline_y;
point.y *= -1.0;
point.y += selection_centerline_y;
}

/// performs a matrix transform on 2d affine matrix causing a reflection across line 'y = selection_centerline_y'
fn mirror_affine_y(affine: &mut na::Affine2<f64>, selection_centerline_y: f64) {
let mirror_transformation_y = na::matrix![
1.0, 0.0, 0.0;
0.0, -1.0, 2.0 * selection_centerline_y;
0.0, 0.0, 1.0;
];

let transformed_affine = mirror_transformation_y * affine.matrix();

*affine = na::Affine2::from_matrix_unchecked(transformed_affine);
}

pub fn vertical_mirror(&mut self, selection_centerline_y: f64) {
match self {
Stroke::BrushStroke(brushstroke) => {
let current_penpath_elements = brushstroke.path.clone().into_elements();

let mut coords = current_penpath_elements
.iter()
.map(|element| element.pos)
.collect::<Vec<na::Vector2<f64>>>();

for coord in coords.iter_mut() {
Self::mirror_point_y(coord, selection_centerline_y);
}

let new_penpath_elements: Vec<Element> = current_penpath_elements
.iter()
.zip(coords.iter())
.map(|(current_penpath_element, new_position)| Element {
pos: *new_position,
..*current_penpath_element
})
.collect();

if let Some(new_penpath) = PenPath::try_from_elements(new_penpath_elements) {
brushstroke.path = new_penpath;
}
}
Stroke::ShapeStroke(shape_stroke) => match &mut shape_stroke.shape {
rnote_compose::Shape::Line(line) => {
Self::mirror_point_y(&mut line.start, selection_centerline_y);
Self::mirror_point_y(&mut line.end, selection_centerline_y);
}
rnote_compose::Shape::Arrow(arrow) => {
Self::mirror_point_y(&mut arrow.start, selection_centerline_y);
Self::mirror_point_y(&mut arrow.tip, selection_centerline_y);
}
rnote_compose::Shape::Rectangle(rectangle) => {
Self::mirror_affine_y(&mut rectangle.transform.affine, selection_centerline_y);
}
rnote_compose::Shape::Ellipse(ellipse) => {
Self::mirror_affine_y(&mut ellipse.transform.affine, selection_centerline_y);
}
rnote_compose::Shape::QuadraticBezier(quadratic_bezier) => {
for point in [
&mut quadratic_bezier.start,
&mut quadratic_bezier.end,
&mut quadratic_bezier.cp,
] {
Self::mirror_point_y(point, selection_centerline_y);
}
}
rnote_compose::Shape::CubicBezier(cubic_bezier) => {
for point in [
&mut cubic_bezier.start,
&mut cubic_bezier.end,
&mut cubic_bezier.cp1,
&mut cubic_bezier.cp2,
] {
Self::mirror_point_y(point, selection_centerline_y);
}
}
rnote_compose::Shape::Polyline(polyline) => {
Self::mirror_point_y(&mut polyline.start, selection_centerline_y);

for point in polyline.path.iter_mut() {
Self::mirror_point_y(point, selection_centerline_y);
}
}
rnote_compose::Shape::Polygon(polygon) => {
Self::mirror_point_y(&mut polygon.start, selection_centerline_y);

for point in polygon.path.iter_mut() {
Self::mirror_point_y(point, selection_centerline_y);
}
}
},
Stroke::VectorImage(vector_image) => {
Self::mirror_affine_y(
&mut vector_image.rectangle.transform.affine,
selection_centerline_y,
);
}
Stroke::BitmapImage(bitmap_image) => {
Self::mirror_affine_y(
&mut bitmap_image.rectangle.transform.affine,
selection_centerline_y,
);
}
Stroke::TextStroke(_) => {}
}
}
}
Loading