From 9957b071438a9fcee502641faac8bc4354d45c40 Mon Sep 17 00:00:00 2001 From: Felix Zwettler Date: Tue, 15 Aug 2023 17:34:01 +0200 Subject: [PATCH 01/10] add polyline shape and builder --- crates/rnote-compose/src/builders/mod.rs | 11 +- .../src/builders/polylinebuilder.rs | 139 +++++++++ crates/rnote-compose/src/meson.build | 2 + crates/rnote-compose/src/penevents.rs | 2 +- crates/rnote-compose/src/shapes/line.rs | 5 + crates/rnote-compose/src/shapes/mod.rs | 25 +- crates/rnote-compose/src/shapes/polyline.rs | 99 +++++++ crates/rnote-compose/src/shapes/shape.rs | 16 +- crates/rnote-compose/src/style/mod.rs | 22 +- crates/rnote-compose/src/style/rough/mod.rs | 29 +- crates/rnote-compose/src/style/smooth/mod.rs | 35 ++- crates/rnote-engine/src/pens/shaper.rs | 3 +- .../shapebuilder-polyline-symbolic.svg | 275 ++++++++++++++++++ .../rnote-ui/data/resources.gresource.xml.in | 11 +- crates/rnote-ui/src/penssidebar/shaperpage.rs | 2 + 15 files changed, 655 insertions(+), 21 deletions(-) create mode 100644 crates/rnote-compose/src/builders/polylinebuilder.rs create mode 100644 crates/rnote-compose/src/shapes/polyline.rs create mode 100644 crates/rnote-ui/data/icons/scalable/actions/shapebuilder-polyline-symbolic.svg diff --git a/crates/rnote-compose/src/builders/mod.rs b/crates/rnote-compose/src/builders/mod.rs index 5eac7aa471..5dacde8c1c 100644 --- a/crates/rnote-compose/src/builders/mod.rs +++ b/crates/rnote-compose/src/builders/mod.rs @@ -11,6 +11,7 @@ mod penpathbuildable; mod penpathcurvedbuilder; mod penpathmodeledbuilder; mod penpathsimplebuilder; +mod polylinebuilder; mod quadbezbuilder; mod quadrantcoordsystem2dbuilder; mod rectanglebuilder; @@ -31,6 +32,7 @@ pub use penpathbuildable::PenPathBuilderProgress; pub use penpathcurvedbuilder::PenPathCurvedBuilder; pub use penpathmodeledbuilder::PenPathModeledBuilder; pub use penpathsimplebuilder::PenPathSimpleBuilder; +pub use polylinebuilder::PolylineBuilder; pub use quadbezbuilder::QuadBezBuilder; pub use quadrantcoordsystem2dbuilder::QuadrantCoordSystem2DBuilder; pub use rectanglebuilder::RectangleBuilder; @@ -75,12 +77,15 @@ pub enum ShapeBuilderType { /// A foci ellipse builder #[serde(rename = "foci_ellipse")] FociEllipse, - /// An quadbez builder + /// A quadbez builder #[serde(rename = "quadbez")] QuadBez, - /// An cubic bezier builder + /// A cubic bezier builder #[serde(rename = "cubbez")] CubBez, + /// A poyline builder + #[serde(rename = "polyline")] + Polyline, } impl ShapeBuilderType { @@ -98,6 +103,7 @@ impl ShapeBuilderType { "shapebuilder-fociellipse-symbolic" => Some(Self::FociEllipse), "shapebuilder-quadbez-symbolic" => Some(Self::QuadBez), "shapebuilder-cubbez-symbolic" => Some(Self::CubBez), + "shapebuilder-polyline-symbolic" => Some(Self::Polyline), _ => None, } } @@ -118,6 +124,7 @@ impl ShapeBuilderType { Self::FociEllipse => String::from("shapebuilder-fociellipse-symbolic"), Self::QuadBez => String::from("shapebuilder-quadbez-symbolic"), Self::CubBez => String::from("shapebuilder-cubbez-symbolic"), + Self::Polyline => String::from("shapebuilder-polyline-symbolic"), } } } diff --git a/crates/rnote-compose/src/builders/polylinebuilder.rs b/crates/rnote-compose/src/builders/polylinebuilder.rs new file mode 100644 index 0000000000..c57ad398b6 --- /dev/null +++ b/crates/rnote-compose/src/builders/polylinebuilder.rs @@ -0,0 +1,139 @@ +// Imports +use super::shapebuildable::{ShapeBuilderCreator, ShapeBuilderProgress}; +use super::ShapeBuildable; +use crate::constraints::ConstraintRatio; +use crate::penevents::{KeyboardKey, PenEvent, PenState}; +use crate::penpath::Element; +use crate::shapes::Polyline; +use crate::style::{indicators, Composer}; +use crate::Constraints; +use crate::{Shape, Style}; +use p2d::bounding_volume::{Aabb, BoundingVolume}; +use piet::RenderContext; +use std::time::Instant; + +/// Line builder. +#[derive(Debug, Clone)] +pub struct PolylineBuilder { + /// Start position. + start: na::Vector2, + /// Current position. + current: na::Vector2, + /// Path + path: Vec>, + /// Pen state + pen_state: PenState, + /// Finish the polyline on the next `PenEvent::Up` + finish: bool, +} + +impl ShapeBuilderCreator for PolylineBuilder { + fn start(element: Element, _now: Instant) -> Self { + Self { + start: element.pos, + current: element.pos, + path: Vec::new(), + pen_state: PenState::Down, + finish: false, + } + } +} + +impl ShapeBuildable for PolylineBuilder { + fn handle_event( + &mut self, + event: PenEvent, + _now: Instant, + mut constraints: Constraints, + ) -> ShapeBuilderProgress { + // we always want to allow horizontal and vertical constraints while building a polyline + constraints.ratios.insert(ConstraintRatio::Horizontal); + constraints.ratios.insert(ConstraintRatio::Vertical); + + match event { + PenEvent::Down { element, .. } => { + if self.pen_state == PenState::Up + && (element.pos - self.path.last().copied().unwrap_or(self.start)).magnitude() + < Self::FINISH_TRESHOLD_DIST + { + self.finish = true; + } + self.pen_state = PenState::Down; + + if let Some(last) = self.path.last() { + self.current = constraints.constrain(element.pos - *last) + *last; + } else { + self.current = constraints.constrain(element.pos - self.start) + self.start; + } + } + PenEvent::Up { .. } => { + if self.finish { + return ShapeBuilderProgress::Finished(vec![Shape::Polyline( + self.state_as_polyline(), + )]); + } + + if self.pen_state == PenState::Down { + self.path.push(self.current); + } + self.pen_state = PenState::Up; + } + PenEvent::Proximity { .. } => { + self.pen_state = PenState::Proximity; + } + PenEvent::KeyPressed { keyboard_key, .. } => { + if keyboard_key == KeyboardKey::Escape { + return ShapeBuilderProgress::Finished(vec![Shape::Polyline( + self.state_as_polyline(), + )]); + } + } + _ => { + self.pen_state = PenState::Up; + } + } + + ShapeBuilderProgress::InProgress + } + + fn bounds(&self, style: &Style, zoom: f64) -> Option { + let mut polyline = self.state_as_polyline(); + if !self.finish { + polyline.path.push(self.current); + } + Some( + polyline + .composed_bounds(style) + .loosened(indicators::POS_INDICATOR_RADIUS / zoom), + ) + } + + fn draw_styled(&self, cx: &mut piet_cairo::CairoRenderContext, style: &Style, zoom: f64) { + cx.save().unwrap(); + + let mut polyline = self.state_as_polyline(); + if !self.finish { + polyline.path.push(self.current); + } + polyline.draw_composed(cx, style); + + indicators::draw_pos_indicator(cx, PenState::Up, self.start, zoom); + if !self.finish { + indicators::draw_pos_indicator(cx, PenState::Down, self.current, zoom); + } + + cx.restore().unwrap(); + } +} + +impl PolylineBuilder { + const FINISH_TRESHOLD_DIST: f64 = 5.0; + + /// The current state as a polyline. + pub fn state_as_polyline(&self) -> Polyline { + Polyline { + start: self.start, + path: self.path.clone(), + } + } +} diff --git a/crates/rnote-compose/src/meson.build b/crates/rnote-compose/src/meson.build index 9f6d01da60..a0a99745a0 100644 --- a/crates/rnote-compose/src/meson.build +++ b/crates/rnote-compose/src/meson.build @@ -13,6 +13,7 @@ rnote_compose_sources = files( 'builders/penpathcurvedbuilder.rs', 'builders/penpathmodeledbuilder.rs', 'builders/penpathsimplebuilder.rs', + 'builders/polylinebuilder.rs', 'builders/quadbezbuilder.rs', 'builders/quadrantcoordsystem2dbuilder.rs', 'builders/rectanglebuilder.rs', @@ -25,6 +26,7 @@ rnote_compose_sources = files( 'shapes/ellipse.rs', 'shapes/line.rs', 'shapes/mod.rs', + 'shapes/polyline.rs', 'shapes/quadbez.rs', 'shapes/rectangle.rs', 'shapes/shape.rs', diff --git a/crates/rnote-compose/src/penevents.rs b/crates/rnote-compose/src/penevents.rs index ebb3d623d5..a4e23bd667 100644 --- a/crates/rnote-compose/src/penevents.rs +++ b/crates/rnote-compose/src/penevents.rs @@ -163,7 +163,7 @@ pub enum ModifierKey { } /// The current pen state. Used wherever there is internal state. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum PenState { /// Up. Up, diff --git a/crates/rnote-compose/src/shapes/line.rs b/crates/rnote-compose/src/shapes/line.rs index 91f59a72b7..231e21bb28 100644 --- a/crates/rnote-compose/src/shapes/line.rs +++ b/crates/rnote-compose/src/shapes/line.rs @@ -55,6 +55,11 @@ impl Shapeable for Line { } impl Line { + /// A new line. + pub fn new(start: na::Vector2, end: na::Vector2) -> Self { + Self { start, end } + } + /// Create a rectangle rotated in the direction of the line, with the given width. pub fn line_w_width_to_rect(&self, width: f64) -> Rectangle { let vec = self.end - self.start; diff --git a/crates/rnote-compose/src/shapes/mod.rs b/crates/rnote-compose/src/shapes/mod.rs index e42afc5bcc..606a03afdd 100644 --- a/crates/rnote-compose/src/shapes/mod.rs +++ b/crates/rnote-compose/src/shapes/mod.rs @@ -1,20 +1,29 @@ // Modules -mod arrow; -/// cubic bezier curves +/// Arrow +pub mod arrow; +/// Cubic-bezier curve pub mod cubbez; -mod ellipse; -mod line; -/// quadratic bezier curves +/// Ellipse +pub mod ellipse; +/// Line +pub mod line; +/// Polyline +pub mod polyline; +/// Polyline pub mod quadbez; -mod rectangle; -mod shape; -mod shapeable; +/// Rectangle +pub mod rectangle; +/// Shape +pub mod shape; +/// Shapeable +pub mod shapeable; // Re-exports pub use arrow::Arrow; pub use cubbez::CubicBezier; pub use ellipse::Ellipse; pub use line::Line; +pub use polyline::Polyline; pub use quadbez::QuadraticBezier; pub use rectangle::Rectangle; pub use shape::Shape; diff --git a/crates/rnote-compose/src/shapes/polyline.rs b/crates/rnote-compose/src/shapes/polyline.rs new file mode 100644 index 0000000000..8350cd35e7 --- /dev/null +++ b/crates/rnote-compose/src/shapes/polyline.rs @@ -0,0 +1,99 @@ +use p2d::bounding_volume::Aabb; +// Imports +use serde::{Deserialize, Serialize}; + +use crate::ext::Vector2Ext; +use crate::transform::Transformable; + +use super::{Line, Shapeable}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default, rename = "polyline")] +/// A Polyline. +pub struct Polyline { + /// The polyline start + #[serde(rename = "start")] + pub start: na::Vector2, + /// The polyline path + #[serde(rename = "path")] + pub path: Vec>, +} + +impl Transformable for Polyline { + fn translate(&mut self, offset: na::Vector2) { + self.start += offset; + for p in &mut self.path { + *p += offset; + } + } + + fn rotate(&mut self, angle: f64, center: na::Point2) { + let mut isometry = na::Isometry2::identity(); + isometry.append_rotation_wrt_point_mut(&na::UnitComplex::new(angle), ¢er); + + self.start = isometry.transform_point(&self.start.into()).coords; + for p in &mut self.path { + *p = isometry.transform_point(&(*p).into()).coords; + } + } + + fn scale(&mut self, scale: na::Vector2) { + self.start = self.start.component_mul(&scale); + for p in &mut self.path { + *p = p.component_mul(&scale); + } + } +} + +impl Shapeable for Polyline { + fn bounds(&self) -> Aabb { + let mut bounds = Aabb::new(self.start.into(), self.start.into()); + for p in &self.path { + bounds.take_point((*p).into()); + } + bounds + } + + fn hitboxes(&self) -> Vec { + let mut hitboxes = Vec::with_capacity(self.path.len() + 1); + hitboxes.push(Aabb::new(self.start.into(), self.start.into())); + + let mut prev = self.start; + for p in &self.path { + let n_splits = super::hitbox_elems_for_shape_len((p - prev).magnitude()); + let line = Line::new(prev, *p); + + hitboxes.extend(line.split(n_splits).into_iter().map(|line| line.bounds())); + + prev = *p; + } + + hitboxes + } +} + +impl Polyline { + /// A new polyline + pub fn new(start: na::Vector2) -> Self { + Self { + start, + path: Vec::new(), + } + } + + /// Convert to [kurbo::BezPath]. + pub fn to_kurbo(&self) -> kurbo::BezPath { + let iter = std::iter::once(kurbo::PathEl::MoveTo(self.start.to_kurbo_point())).chain( + self.path + .iter() + .map(|p| kurbo::PathEl::LineTo(p.to_kurbo_point())), + ); + kurbo::BezPath::from_iter(iter) + } +} + +impl Extend> for Polyline { + fn extend>>(&mut self, iter: T) { + self.path.extend(iter); + } +} diff --git a/crates/rnote-compose/src/shapes/shape.rs b/crates/rnote-compose/src/shapes/shape.rs index 9baab81e3e..25d9d60fd2 100644 --- a/crates/rnote-compose/src/shapes/shape.rs +++ b/crates/rnote-compose/src/shapes/shape.rs @@ -1,5 +1,5 @@ // Imports -use super::{Arrow, CubicBezier, Ellipse, Line, QuadraticBezier, Rectangle, Shapeable}; +use super::{Arrow, CubicBezier, Ellipse, Line, Polyline, QuadraticBezier, Rectangle, Shapeable}; use crate::transform::Transformable; use p2d::bounding_volume::Aabb; use serde::{Deserialize, Serialize}; @@ -26,6 +26,9 @@ pub enum Shape { #[serde(rename = "cubbez")] /// A cubic bezier curve shape. CubicBezier(CubicBezier), + #[serde(rename = "polyline")] + /// A polyline shape. + Polyline(Polyline), } impl Default for Shape { @@ -55,6 +58,9 @@ impl Transformable for Shape { Self::CubicBezier(cubbez) => { cubbez.translate(offset); } + Self::Polyline(polyline) => { + polyline.translate(offset); + } } } @@ -78,6 +84,9 @@ impl Transformable for Shape { Self::CubicBezier(cubbez) => { cubbez.rotate(angle, center); } + Self::Polyline(polyline) => { + polyline.rotate(angle, center); + } } } @@ -101,6 +110,9 @@ impl Transformable for Shape { Self::CubicBezier(cubbez) => { cubbez.scale(scale); } + Self::Polyline(polyline) => { + polyline.scale(scale); + } } } } @@ -114,6 +126,7 @@ impl Shapeable for Shape { Self::Ellipse(ellipse) => ellipse.bounds(), Self::QuadraticBezier(quadbez) => quadbez.bounds(), Self::CubicBezier(cubbez) => cubbez.bounds(), + Self::Polyline(polyline) => polyline.bounds(), } } fn hitboxes(&self) -> Vec { @@ -124,6 +137,7 @@ impl Shapeable for Shape { Self::Ellipse(ellipse) => ellipse.hitboxes(), Self::QuadraticBezier(quadbez) => quadbez.hitboxes(), Self::CubicBezier(cubbez) => cubbez.hitboxes(), + Self::Polyline(polyline) => polyline.hitboxes(), } } } diff --git a/crates/rnote-compose/src/style/mod.rs b/crates/rnote-compose/src/style/mod.rs index 59f1b7ba2c..e58d029e9f 100644 --- a/crates/rnote-compose/src/style/mod.rs +++ b/crates/rnote-compose/src/style/mod.rs @@ -16,7 +16,7 @@ use self::smooth::SmoothOptions; use self::textured::TexturedOptions; // Imports -use crate::shapes::{Arrow, CubicBezier, Ellipse, Line, QuadraticBezier, Rectangle}; +use crate::shapes::{Arrow, CubicBezier, Ellipse, Line, Polyline, QuadraticBezier, Rectangle}; use crate::{Color, PenPath, Shape}; use anyhow::Context; pub use composer::Composer; @@ -198,6 +198,24 @@ impl Composer