Implement viewport selection (#178)

* Begin implementing viewport selection

* Implement viewport click and drag selection for ellipse and rectangle

* Begin implementing line selection

* Remove debug prints

* Run cargo format

* Use DVec2 instead of kurbo::Point

* Line and polyline intersection

* Run cargo format

* Add fix for missing layer panel update

* Replace point selection with box selection

* Formatting

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Paul Kupper 2021-07-05 01:05:12 +02:00 committed by GitHub
parent 20420c1286
commit ad064602a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 293 additions and 25 deletions

View file

@ -1,4 +1,4 @@
use glam::DAffine2;
use glam::{DAffine2, DVec2};
use crate::{
layers::{self, style::PathStyle, Folder, Layer, LayerDataTypes, Line, PolyLine, Rect, Shape},
@ -64,6 +64,19 @@ impl Document {
svg
}
/// Checks whether each layer under `path` intersects with the provided `quad` and adds all intersection layers as paths to `intersections`.
pub fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
self.document_folder(path).unwrap().intersects_quad(quad, path, intersections);
return;
}
/// Checks whether each layer under the root path intersects with the provided `quad` and returns the paths to all intersecting layers.
pub fn intersects_quad_root(&self, quad: [DVec2; 4]) -> Vec<Vec<LayerId>> {
let mut intersections = Vec::new();
self.intersects_quad(quad, &mut vec![], &mut intersections);
intersections
}
fn is_mounted(&self, mount_path: &[LayerId], path: &[LayerId]) -> bool {
path.starts_with(mount_path) && self.work_mounted
}

View file

@ -0,0 +1,57 @@
use glam::DVec2;
use kurbo::{BezPath, Line, PathSeg, Point, Shape, Vec2};
fn to_point(vec: DVec2) -> Point {
Point::new(vec.x, vec.y)
}
pub fn intersect_quad_bez_path(quad: [DVec2; 4], shape: &BezPath, closed: bool) -> bool {
let lines = vec![
Line::new(to_point(quad[0]), to_point(quad[1])),
Line::new(to_point(quad[1]), to_point(quad[2])),
Line::new(to_point(quad[2]), to_point(quad[3])),
Line::new(to_point(quad[3]), to_point(quad[0])),
];
// check if outlines intersect
for path_segment in shape.segments() {
for line in &lines {
if !path_segment.intersect_line(*line).is_empty() {
return true;
}
}
}
// check if selection is entirely within the shape
if closed && shape.contains(to_point(quad[0])) {
return true;
}
// check if shape is entirely within the selection
if let Some(shape_point) = get_arbitrary_point_on_path(shape) {
let mut pos = 0;
let mut neg = 0;
for line in lines {
if line.p0 == shape_point {
return true;
};
let line_vec = Vec2::new(line.p1.x - line.p0.x, line.p1.y - line.p0.y);
let point_vec = Vec2::new(line.p1.x - shape_point.x, line.p1.y - shape_point.y);
let cross = line_vec.cross(point_vec);
if cross > 0.0 {
pos += 1;
} else if cross < 0.0 {
neg += 1;
}
if pos > 0 && neg > 0 {
return false;
}
}
}
true
}
pub fn get_arbitrary_point_on_path(path: &BezPath) -> Option<Point> {
path.segments().next().map(|seg| match seg {
PathSeg::Line(line) => line.p0,
PathSeg::Quad(quad) => quad.p0,
PathSeg::Cubic(cubic) => cubic.p0,
})
}

View file

@ -1,5 +1,10 @@
use glam::DAffine2;
use glam::DVec2;
use kurbo::Shape;
use crate::intersection::intersect_quad_bez_path;
use crate::LayerId;
use super::style;
use super::LayerData;
@ -16,10 +21,17 @@ impl Ellipse {
}
impl LayerData for Ellipse {
fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
kurbo::Ellipse::from_affine(kurbo::Affine::new(transform.to_cols_array())).to_path(0.1)
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
kurbo::Ellipse::from_affine(kurbo::Affine::new(transform.to_cols_array())).to_path(0.01)
}
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
let _ = write!(svg, r#"<path d="{}" {} />"#, self.to_kurbo_path(transform, style).to_svg(), style.render());
}
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), true) {
intersections.push(path.clone());
}
}
}

View file

@ -1,3 +1,5 @@
use glam::DVec2;
use crate::{DocumentError, LayerId};
use super::{style, Layer, LayerData, LayerDataTypes};
@ -13,6 +15,10 @@ pub struct Folder {
}
impl LayerData for Folder {
fn to_kurbo_path(&self, _: glam::DAffine2, _: style::PathStyle) -> kurbo::BezPath {
unimplemented!()
}
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, _style: style::PathStyle) {
let _ = writeln!(svg, r#"<g transform="matrix("#);
transform.to_cols_array().iter().enumerate().for_each(|(i, f)| {
@ -26,8 +32,12 @@ impl LayerData for Folder {
let _ = writeln!(svg, "</g>");
}
fn to_kurbo_path(&mut self, _: glam::DAffine2, _: style::PathStyle) -> kurbo::BezPath {
unimplemented!()
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _style: style::PathStyle) {
for (layer, layer_id) in self.layers().iter().zip(&self.layer_ids) {
path.push(*layer_id);
layer.intersects_quad(quad, path, intersections);
path.pop();
}
}
}

View file

@ -1,6 +1,10 @@
use glam::DAffine2;
use glam::DVec2;
use kurbo::Point;
use crate::intersection::intersect_quad_bez_path;
use crate::LayerId;
use super::style;
use super::LayerData;
@ -17,7 +21,7 @@ impl Line {
}
impl LayerData for Line {
fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
fn new_point(a: DVec2) -> Point {
Point::new(a.x, a.y)
}
@ -26,10 +30,17 @@ impl LayerData for Line {
path.line_to(new_point(transform.transform_point2(DVec2::ONE)));
path
}
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
let [x1, y1] = transform.translation.to_array();
let [x2, y2] = transform.transform_point2(DVec2::ONE).to_array();
let _ = write!(svg, r#"<line x1="{}" y1="{}" x2="{}" y2="{}"{} />"#, x1, y1, x2, y2, style.render(),);
}
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), false) {
intersections.push(path.clone());
}
}
}

View file

@ -19,12 +19,16 @@ pub use shape::Shape;
pub mod folder;
use crate::DocumentError;
use crate::LayerId;
pub use folder::Folder;
use serde::{Deserialize, Serialize};
pub const SELECTION_TOLERANCE: f64 = 5.0;
pub trait LayerData {
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle);
fn to_kurbo_path(&mut self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath;
fn to_kurbo_path(&self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath;
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle);
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
@ -51,6 +55,15 @@ macro_rules! call_kurbo_path {
}
};
}
macro_rules! call_intersects_quad {
($self:ident.intersects_quad($quad:ident, $path:ident, $intersections:ident, $style:ident) { $($variant:ident),* }) => {
match $self {
$(Self::$variant(x) => x.intersects_quad($quad, $path, $intersections, $style)),*
}
};
}
impl LayerDataTypes {
pub fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
call_render! {
@ -64,7 +77,7 @@ impl LayerDataTypes {
}
}
}
pub fn to_kurbo_path(&mut self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath {
pub fn to_kurbo_path(&self, transform: glam::DAffine2, style: style::PathStyle) -> BezPath {
call_kurbo_path! {
self.to_kurbo_path(transform, style) {
Folder,
@ -76,6 +89,19 @@ impl LayerDataTypes {
}
}
}
pub fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
call_intersects_quad! {
self.intersects_quad(quad, path, intersections, style) {
Folder,
Ellipse,
Rect,
Line,
PolyLine,
Shape
}
}
}
}
#[derive(Serialize, Deserialize)]
@ -122,6 +148,20 @@ impl Layer {
self.cache.as_str()
}
pub fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>) {
let inv_transform = self.transform.inverse();
let transformed_quad = [
inv_transform.transform_point2(quad[0]),
inv_transform.transform_point2(quad[1]),
inv_transform.transform_point2(quad[2]),
inv_transform.transform_point2(quad[3]),
];
if !self.visible {
return;
}
self.data.intersects_quad(transformed_quad, path, intersections, self.style)
}
pub fn render_on(&mut self, svg: &mut String) {
*svg += self.render();
}
@ -129,12 +169,14 @@ impl Layer {
pub fn to_kurbo_path(&mut self) -> BezPath {
self.data.to_kurbo_path(self.transform, self.style)
}
pub fn as_folder_mut(&mut self) -> Result<&mut Folder, DocumentError> {
match &mut self.data {
LayerDataTypes::Folder(f) => Ok(f),
_ => Err(DocumentError::NotAFolder),
}
}
pub fn as_folder(&self) -> Result<&Folder, DocumentError> {
match &self.data {
LayerDataTypes::Folder(f) => Ok(&f),

View file

@ -1,3 +1,5 @@
use crate::{intersection::intersect_quad_bez_path, LayerId};
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use std::fmt::Write;
@ -17,7 +19,7 @@ impl PolyLine {
}
impl LayerData for PolyLine {
fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
let mut path = kurbo::BezPath::new();
self.points
.iter()
@ -27,6 +29,7 @@ impl LayerData for PolyLine {
.for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) });
path
}
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
if self.points.is_empty() {
return;
@ -40,6 +43,12 @@ impl LayerData for PolyLine {
}
let _ = write!(svg, r#""{} />"#, style.render());
}
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), false) {
intersections.push(path.clone());
}
}
}
#[cfg(test)]

View file

@ -1,6 +1,10 @@
use glam::DAffine2;
use glam::DVec2;
use kurbo::Point;
use crate::intersection::intersect_quad_bez_path;
use crate::LayerId;
use super::style;
use super::LayerData;
@ -17,7 +21,7 @@ impl Rect {
}
impl LayerData for Rect {
fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> kurbo::BezPath {
fn new_point(a: DVec2) -> Point {
Point::new(a.x, a.y)
}
@ -32,4 +36,10 @@ impl LayerData for Rect {
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
let _ = write!(svg, r#"<path d="{}" {} />"#, self.to_kurbo_path(transform, style).to_svg(), style.render());
}
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), true) {
intersections.push(path.clone());
}
}
}

View file

@ -1,3 +1,8 @@
use glam::DAffine2;
use glam::DVec2;
use crate::intersection::intersect_quad_bez_path;
use crate::LayerId;
use kurbo::BezPath;
use kurbo::Vec2;
@ -20,7 +25,7 @@ impl Shape {
}
impl LayerData for Shape {
fn to_kurbo_path(&mut self, transform: glam::DAffine2, _style: style::PathStyle) -> BezPath {
fn to_kurbo_path(&self, transform: glam::DAffine2, _style: style::PathStyle) -> BezPath {
fn unit_rotation(theta: f64) -> Vec2 {
Vec2::new(-theta.sin(), theta.cos())
}
@ -66,4 +71,10 @@ impl LayerData for Shape {
fn render(&mut self, svg: &mut String, transform: glam::DAffine2, style: style::PathStyle) {
let _ = write!(svg, r#"<path d="{}" {} />"#, self.to_kurbo_path(transform, style).to_svg(), style.render());
}
fn intersects_quad(&self, quad: [DVec2; 4], path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, style: style::PathStyle) {
if intersect_quad_bez_path(quad, &self.to_kurbo_path(DAffine2::IDENTITY, style), true) {
intersections.push(path.clone());
}
}
}

View file

@ -1,5 +1,6 @@
pub mod color;
pub mod document;
pub mod intersection;
pub mod layers;
pub mod operation;
pub mod response;

View file

@ -281,6 +281,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
for path in paths {
responses.extend(self.select_layer(&path));
}
// TODO: Correctly update layer panel in clear_selection instead of here
responses.extend(self.handle_folder_changed(Vec::new()));
}
Undo => {
// this is a temporary fix and will be addressed by #123

View file

@ -104,6 +104,12 @@ impl Default for Mapping {
fn default() -> Self {
let (up, down, pointer_move) = mapping![
entry! {action=DocumentMessage::PasteLayers, key_down=KeyV, modifiers=[KeyControl]},
// Select
entry! {action=SelectMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=SelectMessage::DragStart, key_down=Lmb},
entry! {action=SelectMessage::DragStop, key_up=Lmb},
entry! {action=SelectMessage::Abort, key_down=Rmb},
entry! {action=SelectMessage::Abort, key_down=KeyEscape},
// Rectangle
entry! {action=RectangleMessage::Center, key_down=KeyAlt},
entry! {action=RectangleMessage::UnCenter, key_up=KeyAlt},

View file

@ -1,4 +1,11 @@
use crate::input::InputPreprocessor;
use document_core::color::Color;
use document_core::layers::style::Fill;
use document_core::layers::style::Stroke;
use document_core::layers::{style, SELECTION_TOLERANCE};
use document_core::Operation;
use glam::{DAffine2, DVec2};
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{message_prelude::*, SvgDocument};
@ -11,19 +18,29 @@ pub struct Select {
#[impl_message(Message, ToolMessage, Select)]
#[derive(PartialEq, Clone, Debug)]
pub enum SelectMessage {
DragStart,
DragStop,
MouseMove,
Abort,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
}
advertise_actions!();
fn actions(&self) -> ActionList {
use SelectToolFsmState::*;
match self.fsm_state {
Ready => actions!(SelectMessageDiscriminant; DragStart),
Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SelectToolFsmState {
Ready,
Dragging,
}
impl Default for SelectToolFsmState {
@ -32,29 +49,96 @@ impl Default for SelectToolFsmState {
}
}
#[derive(Default)]
struct SelectToolData;
#[derive(Clone, Debug, Default)]
struct SelectToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
}
impl Fsm for SelectToolFsmState {
type ToolData = SelectToolData;
fn transition(
self,
event: ToolMessage,
_document: &SvgDocument,
_tool_data: &DocumentToolData,
_data: &mut Self::ToolData,
_input: &InputPreprocessor,
_responses: &mut VecDeque<Message>,
) -> Self {
fn transition(self, event: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
let transform = document.root.transform;
use SelectMessage::*;
use SelectToolFsmState::*;
if let ToolMessage::Select(event) = event {
match (self, event) {
(Ready, MouseMove) => self,
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
Dragging
}
(Dragging, MouseMove) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
let (point_1, point_2) = if data.drag_start == data.drag_current {
let (x, y) = (data.drag_current.x as f64, data.drag_current.y as f64);
(
DVec2::new(x - SELECTION_TOLERANCE, y - SELECTION_TOLERANCE),
DVec2::new(x + SELECTION_TOLERANCE, y + SELECTION_TOLERANCE),
)
} else {
(
DVec2::new(data.drag_start.x as f64, data.drag_start.y as f64),
DVec2::new(data.drag_current.x as f64, data.drag_current.y as f64),
)
};
let quad = [
DVec2::new(point_1.x, point_1.y),
DVec2::new(point_2.x, point_1.y),
DVec2::new(point_2.x, point_2.y),
DVec2::new(point_1.x, point_2.y),
];
if data.drag_start == data.drag_current {
if let Some(intersection) = document.intersects_quad_root(quad).last() {
responses.push_back(DocumentMessage::SelectLayers(vec![intersection.clone()]).into());
} else {
responses.push_back(DocumentMessage::SelectLayers(vec![]).into());
}
} else {
responses.push_back(DocumentMessage::SelectLayers(document.intersects_quad_root(quad)).into());
}
Ready
}
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
Ready
}
_ => self,
}
} else {
self
}
}
}
fn make_operation(data: &SelectToolData, _tool_data: &DocumentToolData, transform: DAffine2) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
Operation::AddRect {
path: vec![],
insert_index: -1,
transform: (transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x31, 0x94, 0xD6), 2.0)), Some(Fill::none())),
}
.into()
}