From 11f15bd6afa7f355b79e2b296f9397c5cb5ad783 Mon Sep 17 00:00:00 2001 From: Leonard Pauli Date: Sat, 8 Jan 2022 14:25:24 +0100 Subject: [PATCH] Path Tool: Implement anchor point dragging (#451) * #82 path-tool: WIP selecting control point working * Fix bug where duplication with Ctrl+D doesn't properly duplicate (#423) * bug fix: duplication didn't properly duplicate * cargo fmt * changed the formatting slightly for readability * Small cleanups, changed color of handles upon selection * Fix changes from merge * Remove duplicate anchor points on top of one another * Fix possible issues with thumbnails not being updated from Graphene operations * path-tool: attempt to move control points on click * Add dragging for control points * Editing shape anchors functional. Handles next. * Comment cleanup & slight cleanup of closest_anchor(..) * Removing conflict with master * Tiny code tweaks Co-authored-by: Keavon Chambers Co-authored-by: caleb <56044292+caleb-ad@users.noreply.github.com> Co-authored-by: otdavies Co-authored-by: Dennis --- editor/src/document/document_file.rs | 9 + editor/src/input/input_mapper.rs | 4 + editor/src/tool/tools/path.rs | 363 ++++++++++++++++++++++----- graphene/src/document.rs | 23 +- graphene/src/layers/simple_shape.rs | 18 +- graphene/src/operation.rs | 5 + 6 files changed, 351 insertions(+), 71 deletions(-) diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index 9b14b83b1..65992efd2 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -55,8 +55,15 @@ pub enum VectorManipulatorSegment { #[derive(PartialEq, Clone, Debug)] pub struct VectorManipulatorShape { + /// The path to the layer + pub layer_path: Vec, + /// The outline of the shape pub path: kurbo::BezPath, + /// The control points / manipulator handles pub segments: Vec, + /// The compound Bezier curve is closed + pub closed: bool, + /// The transformation matrix to apply pub transform: DAffine2, } @@ -266,9 +273,11 @@ impl DocumentMessageHandler { .collect::>(); Some(VectorManipulatorShape { + layer_path: path_to_shape.to_vec(), path, segments, transform: viewport_transform, + closed: shape.closed, }) }); diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 1216156e9..dc2dff343 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -186,6 +186,10 @@ impl Default for Mapping { entry! {action=LineMessage::Abort, key_down=Rmb}, entry! {action=LineMessage::Abort, key_down=KeyEscape}, entry! {action=LineMessage::Redraw{center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift}, triggers=[KeyAlt, KeyShift, KeyControl]}, + // Path + entry! {action=PathMessage::DragStart, key_down=Lmb}, + entry! {action=PathMessage::PointerMove, message=InputMapperMessage::PointerMove}, + entry! {action=PathMessage::DragStop, key_up=Lmb}, // Pen entry! {action=PenMessage::PointerMove, message=InputMapperMessage::PointerMove}, entry! {action=PenMessage::DragStart, key_down=Lmb}, diff --git a/editor/src/tool/tools/path.rs b/editor/src/tool/tools/path.rs index 713acb539..bb360192d 100644 --- a/editor/src/tool/tools/path.rs +++ b/editor/src/tool/tools/path.rs @@ -13,9 +13,12 @@ use glam::{DAffine2, DVec2}; use graphene::color::Color; use graphene::layers::style; use graphene::layers::style::Fill; +use graphene::layers::style::PathStyle; use graphene::layers::style::Stroke; use graphene::Operation; use kurbo::BezPath; +use kurbo::PathEl; +use kurbo::Vec2; use serde::{Deserialize, Serialize}; #[derive(Default)] @@ -27,6 +30,10 @@ pub struct Path { #[impl_message(Message, ToolMessage, Path)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum PathMessage { + DragStart, + PointerMove, + DragStop, + // Standard messages Abort, DocumentIsDirty, @@ -47,10 +54,12 @@ impl<'a> MessageHandler> for Path { } } + // Different actions depending on state may be wanted: fn actions(&self) -> ActionList { use PathToolFsmState::*; match self.fsm_state { - Ready => actions!(PathMessageDiscriminant;), + Ready => actions!(PathMessageDiscriminant; DragStart), + Dragging => actions!(PathMessageDiscriminant; DragStop, PointerMove), } } } @@ -58,6 +67,7 @@ impl<'a> MessageHandler> for Path { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum PathToolFsmState { Ready, + Dragging, } impl Default for PathToolFsmState { @@ -72,9 +82,19 @@ struct PathToolData { handle_marker_pool: Vec>, anchor_handle_line_pool: Vec>, shape_outline_pool: Vec>, + selected_shapes: Vec, + selection: PathToolSelection, } impl PathToolData {} +#[derive(Clone, Debug, Default)] +struct PathToolSelection { + closest_layer_path: Vec, + closest_shape_id: usize, + overlay_path: Vec, + bez_path_elements: Vec, + bez_segment_id: usize, +} impl Fsm for PathToolFsmState { type ToolData = PathToolData; @@ -85,7 +105,7 @@ impl Fsm for PathToolFsmState { document: &DocumentMessageHandler, _tool_data: &DocumentToolData, data: &mut Self::ToolData, - _input: &InputPreprocessor, + input: &InputPreprocessor, responses: &mut VecDeque, ) -> Self { if let ToolMessage::Path(event) = event { @@ -134,60 +154,52 @@ impl Fsm for PathToolFsmState { ); shape_i += 1; - for segment in &shape_to_draw.segments { - // TODO: We draw each anchor point twice because segment has it on both ends, fix this - let (anchors, handles, anchor_handle_lines) = match segment { - VectorManipulatorSegment::Line(a1, a2) => (vec![*a1, *a2], vec![], vec![]), - VectorManipulatorSegment::Quad(a1, h1, a2) => (vec![*a1, *a2], vec![*h1], vec![(*h1, *a1)]), - VectorManipulatorSegment::Cubic(a1, h1, h2, a2) => (vec![*a1, *a2], vec![*h1, *h2], vec![(*h1, *a1), (*h2, *a2)]), - }; + let segment = shape_manipulator_points(shape_to_draw); - // Draw the line connecting the anchor with handle for cubic and quadratic bezier segments - for anchor_handle_line in anchor_handle_lines { - let marker = data.anchor_handle_line_pool[line_i].clone(); + // Draw the line connecting the anchor with handle for cubic and quadratic bezier segments + for anchor_handle_line in segment.anchor_handle_lines { + let marker = &data.anchor_handle_line_pool[line_i]; - let line_vector = anchor_handle_line.0 - anchor_handle_line.1; + let line_vector = anchor_handle_line.0 - anchor_handle_line.1; - let scale = DVec2::splat(line_vector.length()); - let angle = -line_vector.angle_between(DVec2::X); - let translation = (anchor_handle_line.1 + BIAS).round() + DVec2::splat(0.5); - let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); + let scale = DVec2::splat(line_vector.length()); + let angle = -line_vector.angle_between(DVec2::X); + let translation = (anchor_handle_line.1 + BIAS).round() + DVec2::splat(0.5); + let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); - responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into()); - responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker, visible: true }.into()).into()); + responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into()); + responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into()); - line_i += 1; - } + line_i += 1; + } - // Draw the draggable square points on the end of every line segment or bezier curve segment - for anchor in anchors { - let marker = data.anchor_marker_pool[anchor_i].clone(); + // Draw the draggable square points on the end of every line segment or bezier curve segment + for anchor in segment.anchors { + let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE); + let angle = 0.; + let translation = (anchor - (scale / 2.) + BIAS).round(); + let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); - let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE); - let angle = 0.; - let translation = (anchor - (scale / 2.) + BIAS).round(); - let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); + let marker = &data.anchor_marker_pool[anchor_i]; + responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into()); + responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into()); - responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into()); - responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker, visible: true }.into()).into()); + anchor_i += 1; + } - anchor_i += 1; - } + // Draw the draggable handle for cubic and quadratic bezier segments + for handle in segment.handles { + let marker = &data.handle_marker_pool[handle_i]; - // Draw the draggable handle for cubic and quadratic bezier segments - for handle in handles { - let marker = data.handle_marker_pool[handle_i].clone(); + let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE); + let angle = 0.; + let translation = (handle - (scale / 2.) + BIAS).round(); + let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); - let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE); - let angle = 0.; - let translation = (handle - (scale / 2.) + BIAS).round(); - let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array(); + responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into()); + responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into()); - responses.push_back(DocumentMessage::Overlay(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into()); - responses.push_back(DocumentMessage::Overlay(Operation::SetLayerVisibility { path: marker, visible: true }.into()).into()); - - handle_i += 1; - } + handle_i += 1; } } @@ -211,6 +223,170 @@ impl Fsm for PathToolFsmState { self } + (_, DragStart) => { + // todo: DRY refactor (this arm is very similar to the (_, RedrawOverlay) arm) + + let mouse_pos = input.mouse.position; + let mut points = Vec::new(); + + let (mut anchor_i, mut handle_i, _line_i, _shape_i) = (0, 0, 0, 0); + let shapes_to_draw = document.selected_visible_layers_vector_points(); + let (total_anchors, total_handles, _total_anchor_handle_lines) = calculate_total_overlays_per_type(&shapes_to_draw); + grow_overlay_pool_entries(&mut data.anchor_marker_pool, total_anchors, add_anchor_marker, responses); + grow_overlay_pool_entries(&mut data.handle_marker_pool, total_handles, add_handle_marker, responses); + + #[derive(Debug)] + enum PointType { + Anchor { anchor_i: usize, layer_path: Vec, shape_offset: usize }, + Handle { handle_i: usize, layer_path: Vec, shape_offset: usize }, + } + #[derive(Debug)] + struct Point { + point_type: PointType, + mouse_proximity: f64, + } + + impl Point { + fn new(_position: DVec2, point_type: PointType, mouse_proximity: f64) -> Self { + Self { point_type, mouse_proximity } + } + } + + // TODO simplify the following block + let select_threshold = 6.; + let select_threshold_squared = select_threshold * select_threshold; + + for (shape_offset, shape_to_draw) in shapes_to_draw.iter().enumerate() { + let segment = shape_manipulator_points(shape_to_draw); + + for anchor in segment.anchors { + let d2 = mouse_pos.distance_squared(anchor); + if d2 < select_threshold_squared { + points.push(Point::new( + anchor, + PointType::Anchor { + anchor_i, + layer_path: shape_to_draw.layer_path.clone(), + shape_offset, + }, + d2, + )); + } + anchor_i += 1; + } + + for (_, handle) in segment.handles.into_iter().enumerate() { + let d2 = mouse_pos.distance_squared(handle); + if d2 < select_threshold_squared { + points.push(Point::new( + handle, + PointType::Handle { + handle_i, + layer_path: shape_to_draw.layer_path.clone(), + shape_offset, + }, + d2, + )); + } + handle_i += 1; + } + } + + points.sort_by(|a, b| a.mouse_proximity.partial_cmp(&b.mouse_proximity).unwrap_or(std::cmp::Ordering::Equal)); + let closest_point_within_click_threshold = points.first(); + + if let Some(point) = closest_point_within_click_threshold { + let path = match point.point_type { + PointType::Anchor { + anchor_i, + ref layer_path, + shape_offset, + } => { + data.selected_shapes = shapes_to_draw; + let shape = &data.selected_shapes[shape_offset]; + let path = shape.path.clone(); + let bez: Vec = (&path).into_iter().collect(); + let transformed = shape.transform.inverse().transform_point2(input.mouse.position); + data.selection.bez_segment_id = closest_anchor(&bez, Vec2::new(transformed.x, transformed.y)); + data.selection.bez_path_elements = bez; + data.selection.closest_layer_path = layer_path.clone(); + data.selection.closest_shape_id = shape_offset; + data.anchor_marker_pool[anchor_i].clone() + } + PointType::Handle { + handle_i, + ref layer_path, + shape_offset, + } => { + // TODO make this work for the handles, right now just selects the anchors + data.selected_shapes = shapes_to_draw; + let shape = &data.selected_shapes[shape_offset]; + let path = shape.path.clone(); + let bez: Vec = (&path).into_iter().collect(); + let transformed = shape.transform.inverse().transform_point2(input.mouse.position); + data.selection.bez_segment_id = closest_anchor(&bez, Vec2::new(transformed.x, transformed.y)); + data.selection.bez_path_elements = bez; + data.selection.closest_layer_path = layer_path.clone(); + data.selection.closest_shape_id = shape_offset; + data.handle_marker_pool[handle_i].clone() + } + }; + + data.selection.overlay_path = path; + responses.push_back( + DocumentMessage::Overlay( + Operation::SetLayerFill { + path: data.selection.overlay_path.clone(), + color: COLOR_ACCENT, + } + .into(), + ) + .into(), + ); + Dragging + } else { + Ready + } + } + (Dragging, PointerMove) => { + let shape = &data.selected_shapes[data.selection.closest_shape_id]; + let transformed = shape.transform.inverse().transform_point2(input.mouse.position); + let delta: Vec2 = Vec2::new(transformed.x, transformed.y); + let replacement = match &data.selection.bez_path_elements[data.selection.bez_segment_id] { + PathEl::MoveTo(_) => PathEl::MoveTo(delta.to_point()), + PathEl::LineTo(_) => PathEl::LineTo(delta.to_point()), + PathEl::QuadTo(a1, _) => PathEl::QuadTo(*a1, delta.to_point()), + PathEl::CurveTo(a1, a2, _) => PathEl::CurveTo(*a1, *a2, delta.to_point()), + PathEl::ClosePath => unreachable!(), + }; + data.selection.bez_path_elements[data.selection.bez_segment_id] = replacement; + + responses.push_back( + Operation::SetShapePathInViewport { + path: data.selection.closest_layer_path.clone(), + bez_path: data.selection.bez_path_elements.clone().into_iter().collect(), + transform: shape.transform.to_cols_array(), + } + .into(), + ); + + Dragging + } + (_, PointerMove) => self, + (_, DragStop) => { + let style = PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))); + responses.push_back( + DocumentMessage::Overlay( + Operation::SetLayerStyle { + path: data.selection.overlay_path.clone(), + style, + } + .into(), + ) + .into(), + ); + Ready + } (_, Abort) => { // Destory the overlay layer pools while let Some(layer) = data.anchor_marker_pool.pop() { @@ -297,26 +473,76 @@ impl Fsm for PathToolFsmState { }, ]), ]), + PathToolFsmState::Dragging => HintData(vec![]), }; responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); } } -fn calculate_total_overlays_per_type(shapes_to_draw: &[VectorManipulatorShape]) -> (usize, usize, usize) { +struct VectorManipulatorTypes { + anchors: Vec, + handles: Vec, + anchor_handle_lines: Vec<(glam::DVec2, glam::DVec2)>, +} + +fn shape_manipulator_points(shape: &VectorManipulatorShape) -> VectorManipulatorTypes { + // TODO: Performance can be improved by using three iterators (calling `.iter()` for each of the three) instead of a vector, achievable with some file restructuring + let initial_counts = calculate_shape_overlays_per_type(shape); + let mut result = VectorManipulatorTypes { + anchors: Vec::with_capacity(initial_counts.0), + handles: Vec::with_capacity(initial_counts.1), + anchor_handle_lines: Vec::with_capacity(initial_counts.2), + }; + + for (i, segment) in shape.segments.iter().enumerate() { + // An open shape needs an extra point, which is part of the first segment (when `i` is 0) + let include_start_and_end = !shape.closed && i == 0; + + match segment { + VectorManipulatorSegment::Line(a1, a2) => { + result.anchors.extend(if include_start_and_end { vec![*a1, *a2] } else { vec![*a2] }); + } + VectorManipulatorSegment::Quad(a1, h1, a2) => { + result.anchors.extend(if include_start_and_end { vec![*a1, *a2] } else { vec![*a2] }); + result.handles.extend(vec![*h1]); + result.anchor_handle_lines.extend(vec![(*h1, *a1)]); + } + VectorManipulatorSegment::Cubic(a1, h1, h2, a2) => { + result.anchors.extend(if include_start_and_end { vec![*a1, *a2] } else { vec![*a2] }); + result.handles.extend(vec![*h1, *h2]); + result.anchor_handle_lines.extend(vec![(*h1, *a1), (*h2, *a2)]); + } + }; + } + + result +} + +fn calculate_total_overlays_per_type(shapes: &[VectorManipulatorShape]) -> (usize, usize, usize) { + shapes.iter().fold((0, 0, 0), |acc, shape| { + let counts = calculate_shape_overlays_per_type(shape); + (acc.0 + counts.0, acc.1 + counts.1, acc.2 + counts.2) + }) +} + +fn calculate_shape_overlays_per_type(shape: &VectorManipulatorShape) -> (usize, usize, usize) { let (mut total_anchors, mut total_handles, mut total_anchor_handle_lines) = (0, 0, 0); - for shape_to_draw in shapes_to_draw { - for segment in &shape_to_draw.segments { - let (anchors, handles, anchor_handle_lines) = match segment { - VectorManipulatorSegment::Line(_, _) => (2, 0, 0), - VectorManipulatorSegment::Quad(_, _, _) => (2, 1, 1), - VectorManipulatorSegment::Cubic(_, _, _, _) => (2, 2, 2), - }; - total_anchors += anchors; - total_handles += handles; - total_anchor_handle_lines += anchor_handle_lines; - } + for segment in &shape.segments { + let (anchors, handles, anchor_handle_lines) = match segment { + VectorManipulatorSegment::Line(_, _) => (1, 0, 0), + VectorManipulatorSegment::Quad(_, _, _) => (1, 1, 1), + VectorManipulatorSegment::Cubic(_, _, _, _) => (1, 2, 2), + }; + total_anchors += anchors; + total_handles += handles; + total_anchor_handle_lines += anchor_handle_lines; + } + + // A non-closed shape does not reuse the start and end point, so there is one extra + if !shape.closed { + total_anchors += 1; } (total_anchors, total_handles, total_anchor_handle_lines) @@ -383,8 +609,33 @@ fn add_shape_outline(responses: &mut VecDeque) -> Vec { path: layer_path.clone(), bez_path: BezPath::default(), style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Some(Fill::none())), + closed: false, }; responses.push_back(DocumentMessage::Overlay(operation.into()).into()); layer_path } + +// Brute force comparison to determine which path element we want to select +fn closest_anchor(bez: &[kurbo::PathEl], pos: kurbo::Vec2) -> usize { + let mut closest: usize = 0; + let mut closest_distance: f64 = f64::MAX; + for (i, el) in bez.iter().enumerate() { + let p = match el { + kurbo::PathEl::MoveTo(p) => Some(p.to_vec2()), + kurbo::PathEl::LineTo(p) => Some(p.to_vec2()), + kurbo::PathEl::QuadTo(_, p) => Some(p.to_vec2()), + kurbo::PathEl::CurveTo(_, _, p) => Some(p.to_vec2()), + kurbo::PathEl::ClosePath => None, + }; + if p.is_none() { + continue; + } + let distance_squared = (p.unwrap() - pos).hypot2(); + if distance_squared < closest_distance { + closest_distance = distance_squared; + closest = i; + } + } + closest +} diff --git a/graphene/src/document.rs b/graphene/src/document.rs index 613ce7ca9..24d46b81c 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -448,8 +448,8 @@ impl Document { Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) } - Operation::AddOverlayShape { path, style, bez_path } => { - let mut shape = Shape::from_bez_path(bez_path.clone(), *style, false); + Operation::AddOverlayShape { path, style, bez_path, closed } => { + let mut shape = Shape::from_bez_path(bez_path.clone(), *style, *closed); shape.render_index = -1; let layer = Layer::new(LayerDataType::Shape(shape), DAffine2::IDENTITY.to_cols_array()); @@ -546,14 +546,14 @@ impl Document { self.set_layer(path, Layer::new(LayerDataType::Folder(Folder::default()), DAffine2::IDENTITY.to_cols_array()), -1)?; self.mark_as_dirty(path)?; - Some(vec![DocumentChanged, CreatedLayer { path: path.clone() }]) + Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(path)].concat()) } Operation::TransformLayer { path, transform } => { let layer = self.layer_mut(path).unwrap(); let transform = DAffine2::from_cols_array(transform) * layer.transform; layer.transform = transform; self.mark_as_dirty(path)?; - Some(vec![DocumentChanged]) + Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) } Operation::TransformLayerInViewport { path, transform } => { let transform = DAffine2::from_cols_array(transform); @@ -567,6 +567,17 @@ impl Document { self.mark_as_dirty(path)?; Some([vec![DocumentChanged], update_thumbnails_upstream(path)].concat()) } + Operation::SetShapePath { path, bez_path } => { + self.mark_as_dirty(path)?; + + match &mut self.layer_mut(path)?.data { + LayerDataType::Shape(shape) => { + shape.path = bez_path.clone(); + } + LayerDataType::Folder(_) => (), + } + Some(vec![DocumentChanged, LayerChanged { path: path.clone() }]) + } Operation::SetShapePathInViewport { path, bez_path, transform } => { let transform = DAffine2::from_cols_array(transform); self.set_transform_relative_to_viewport(path, transform)?; @@ -578,7 +589,7 @@ impl Document { } LayerDataType::Folder(_) => (), } - Some(vec![DocumentChanged, LayerChanged { path: path.clone() }]) + Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(path)].concat()) } Operation::TransformLayerInScope { path, transform, scope } => { let transform = DAffine2::from_cols_array(transform); @@ -632,7 +643,7 @@ impl Document { _ => return Err(DocumentError::NotAShape), } self.mark_as_dirty(path)?; - Some(vec![DocumentChanged, LayerChanged { path: path.clone() }]) + Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(path)].concat()) } Operation::SetLayerFill { path, color } => { let layer = self.layer_mut(path)?; diff --git a/graphene/src/layers/simple_shape.rs b/graphene/src/layers/simple_shape.rs index 7924636ca..8995e6653 100644 --- a/graphene/src/layers/simple_shape.rs +++ b/graphene/src/layers/simple_shape.rs @@ -26,7 +26,7 @@ pub struct Shape { pub path: BezPath, pub style: style::PathStyle, pub render_index: i32, - pub solid: bool, + pub closed: bool, } impl LayerData for Shape { @@ -62,7 +62,7 @@ impl LayerData for Shape { } fn intersects_quad(&self, quad: Quad, path: &mut Vec, intersections: &mut Vec>) { - if intersect_quad_bez_path(quad, &self.path, self.solid) { + if intersect_quad_bez_path(quad, &self.path, self.closed) { intersections.push(path.clone()); } } @@ -78,12 +78,12 @@ impl Shape { transforms.iter().skip(start).cloned().reduce(|a, b| a * b).unwrap_or(DAffine2::IDENTITY) } - pub fn from_bez_path(bez_path: BezPath, style: PathStyle, solid: bool) -> Self { + pub fn from_bez_path(bez_path: BezPath, style: PathStyle, closed: bool) -> Self { Self { path: bez_path, style, render_index: 1, - solid, + closed, } } @@ -111,7 +111,7 @@ impl Shape { path, style, render_index: 1, - solid: true, + closed: true, } } pub fn rectangle(style: PathStyle) -> Self { @@ -119,7 +119,7 @@ impl Shape { path: kurbo::Rect::new(0., 0., 1., 1.).to_path(0.01), style, render_index: 1, - solid: true, + closed: true, } } pub fn ellipse(style: PathStyle) -> Self { @@ -127,7 +127,7 @@ impl Shape { path: kurbo::Ellipse::from_rect(kurbo::Rect::new(0., 0., 1., 1.)).to_path(0.01), style, render_index: 1, - solid: true, + closed: true, } } pub fn line(style: PathStyle) -> Self { @@ -135,7 +135,7 @@ impl Shape { path: kurbo::Line::new((0., 0.), (1., 0.)).to_path(0.01), style, render_index: 1, - solid: true, + closed: false, } } pub fn poly_line(points: Vec>, style: PathStyle) -> Self { @@ -150,7 +150,7 @@ impl Shape { path, style, render_index: 0, - solid: false, + closed: false, } } } diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index d54c0b0b7..2c20bbe0f 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -65,6 +65,7 @@ pub enum Operation { path: Vec, bez_path: kurbo::BezPath, style: style::PathStyle, + closed: bool, }, DeleteLayer { path: Vec, @@ -96,6 +97,10 @@ pub enum Operation { path: Vec, transform: [f64; 6], }, + SetShapePath { + path: Vec, + bez_path: kurbo::BezPath, + }, SetShapePathInViewport { path: Vec, bez_path: kurbo::BezPath,