Overhaul Path Tool (#498)

* First pass cleanup omw to handles

* Handles dragging with anchors, handles still not draggable and some bugs

* Dragging single side of handle works, need to create mirror case

* In progress addition of improved anchor / handle representation

* partially working

* Handle dragging working for non-end points, normal anchor drag bugged

* Fixed corner cases, fixed anchors without handles bug

* Add snapping

* Change path tool selection by clicking on shape

* Fixed path close point being draggable

* Variable length handle, firstpass of alt to stop mirroring

* Alt improved, not done. Only update structures when needed. Added snapping for selected shapes

* Can now undo path edits

* Do not maintain angle between non-mirrored handles

* Replaced segment based overlay setup with anchor based setup

* Cleanup, handle angle comparison bug remains. Investigating.

* Added OverlayPooler. May closely associate overlays to VectorManipulatorAnchors instead.

* Moved anchor / segment creation logic out of document_message_handler

* Overlays are now managed by VectorManipulatorShapes

* Fixed inconsistent handle mirroring.

* Clearly shows which point you have selected

* Removed OverlayPooler system

* Added more comments

* Removed all clones of the vector structures. A little uglier but better.

* Resolved Text path initialization bug with a workaround.

* Cleaned up comments

* More comment cleanup

* Fixed issue with quad handle dragging unwanted behavior, renamed VectorShapeManipulator

* In progress refactor to allow multi-selection

* In progress dragging multiple points, selection works, transform still has issues

* Added Multiselect, major refactor

* Commented out progress for selection change, bug with hop back on multiple shapes

* Removed debug og

* Resolved issue with merge

* Minor cleanup, added a few comments

* Review changes

* Resolved unclear comment

* Fixed snap back for now

* Add todo comment for future snap back fix

* Working situations where curve paths do not close. Thanks for points it out  @pkupper

* Tweaked selection size

* Fix curve start point dragability, renames, cleanup

* Separated into multiple files, applied @TrueDoctor review feedback

* Resolved tests failing due to doc generation

* Re-added closed, added concept of distance mirroring

* Added shift distance mirroring, removed debounce from anchor

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Thank you for the reviews @TrueDoctor and @pkupper
This commit is contained in:
Oliver Davies 2022-02-09 12:49:14 -08:00 committed by Keavon Chambers
parent bd844aaf94
commit 108b8be595
17 changed files with 1407 additions and 526 deletions

View File

@ -34,6 +34,7 @@ pub const BOUNDS_ROTATE_THRESHOLD: f64 = 40.;
// Path tool
pub const VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE: f64 = 5.;
pub const SELECTION_THRESHOLD: f64 = 10.;
// Line tool
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;

View File

@ -1,6 +1,6 @@
use super::clipboards::Clipboard;
use super::layer_panel::{layer_panel_entry, LayerDataTypeDiscriminant, LayerMetadata, LayerPanelEntry, RawBuffer};
use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis, VectorManipulatorSegment, VectorManipulatorShape};
use super::utility_types::{AlignAggregate, AlignAxis, DocumentSave, FlipAxis};
use super::vectorize_layer_metadata;
use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler};
use crate::consts::{
@ -12,6 +12,7 @@ use crate::layout::widgets::{
WidgetCallback, WidgetHolder, WidgetLayout,
};
use crate::message_prelude::*;
use crate::viewport_tools::vector_editor::vector_shape::VectorShape;
use crate::EditorError;
use graphene::document::Document as GrapheneDocument;
@ -21,7 +22,6 @@ use graphene::layers::style::ViewMode;
use graphene::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation};
use glam::{DAffine2, DVec2};
use kurbo::PathSeg;
use log::warn;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -138,49 +138,26 @@ impl DocumentMessageHandler {
self.graphene_document.combined_viewport_bounding_box(paths)
}
// TODO: Consider moving this to some kind of overlays manager in the future
pub fn selected_visible_layers_vector_points(&self) -> Vec<VectorManipulatorShape> {
/// Create a new vector shape representation with the underlying kurbo data, VectorManipulatorShape
pub fn selected_visible_layers_vector_shapes(&self, responses: &mut VecDeque<Message>) -> Vec<VectorShape> {
let shapes = self.selected_layers().filter_map(|path_to_shape| {
let viewport_transform = self.graphene_document.generate_transform_relative_to_viewport(path_to_shape).ok()?;
let layer = self.graphene_document.layer(path_to_shape);
// Filter out the non-visible layers from the `filter_map`
match &layer {
Ok(layer) if layer.visible => {}
_ => return None,
};
let (path, closed) = match &layer.ok()?.data {
// TODO: This ClosePath check does not handle all cases, fix this soon
LayerDataType::Shape(shape) => Some((shape.path.clone(), shape.path.elements().last() == Some(&kurbo::PathEl::ClosePath))),
LayerDataType::Text(text) => Some((text.to_bez_path_nonmut(), true)),
// TODO: Create VectorManipulatorShape when creating a kurbo shape as a stopgap, rather than on each new selection
match &layer.ok()?.data {
LayerDataType::Shape(shape) => Some(VectorShape::new(path_to_shape.to_vec(), viewport_transform, &shape.path, shape.closed, responses)),
LayerDataType::Text(text) => Some(VectorShape::new(path_to_shape.to_vec(), viewport_transform, &text.to_bez_path_nonmut(), true, responses)),
_ => None,
}?;
let segments = path
.segments()
.map(|segment| -> VectorManipulatorSegment {
let place = |point: kurbo::Point| -> DVec2 { viewport_transform.transform_point2(DVec2::from((point.x, point.y))) };
match segment {
PathSeg::Line(line) => VectorManipulatorSegment::Line(place(line.p0), place(line.p1)),
PathSeg::Quad(quad) => VectorManipulatorSegment::Quad(place(quad.p0), place(quad.p1), place(quad.p2)),
PathSeg::Cubic(cubic) => VectorManipulatorSegment::Cubic(place(cubic.p0), place(cubic.p1), place(cubic.p2), place(cubic.p3)),
}
})
.collect::<Vec<VectorManipulatorSegment>>();
Some(VectorManipulatorShape {
layer_path: path_to_shape.to_vec(),
path,
segments,
transform: viewport_transform,
closed,
})
}
});
// TODO: Consider refactoring this in a way that avoids needing to collect() so we can skip the heap allocations
shapes.collect::<Vec<VectorManipulatorShape>>()
shapes.collect::<Vec<VectorShape>>()
}
pub fn selected_layers(&self) -> impl Iterator<Item = &[LayerId]> {
@ -693,7 +670,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
}
// TODO: Correctly update layer panel in clear_selection instead of here
responses.push_back(FolderChanged { affected_folder_path: vec![] }.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(DocumentMessage::SelectionChanged.into());
}
AlignSelectedLayers { axis, aggregate } => {
self.backup(responses);
@ -756,7 +733,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
responses.push_front(DocumentOperation::DeleteLayer { path: path.to_vec() }.into());
}
responses.push_front(ToolMessage::DocumentIsDirty.into());
responses.push_front(DocumentMessage::SelectionChanged.into());
}
DeselectAllLayers => {
responses.push_front(SetSelectedLayers { replacement_selected_layers: vec![] }.into());
@ -1042,6 +1019,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
}
SelectionChanged => {
// TODO: Hoist this duplicated code into wider system
responses.push_back(ToolMessage::SelectionChanged.into());
responses.push_back(ToolMessage::DocumentIsDirty.into());
}
SelectLayer { layer_path, ctrl, shift } => {
@ -1068,7 +1046,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessorMessageHandler> for Docum
}
.into(),
);
responses.push_back(ToolMessage::DocumentIsDirty.into());
responses.push_back(DocumentMessage::SelectionChanged.into());
} else {
paths.push(layer_path.clone());
}

View File

@ -1,9 +1,7 @@
pub use super::layer_panel::{layer_panel_entry, LayerMetadata, LayerPanelEntry, RawBuffer};
use graphene::document::Document as GrapheneDocument;
use graphene::LayerId;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -28,24 +26,3 @@ pub enum AlignAggregate {
Center,
Average,
}
#[derive(PartialEq, Clone, Debug)]
pub enum VectorManipulatorSegment {
Line(DVec2, DVec2),
Quad(DVec2, DVec2, DVec2),
Cubic(DVec2, DVec2, DVec2, DVec2),
}
#[derive(PartialEq, Clone, Debug)]
pub struct VectorManipulatorShape {
/// The path to the layer
pub layer_path: Vec<LayerId>,
/// The outline of the shape
pub path: kurbo::BezPath,
/// The control points / manipulator handles
pub segments: Vec<VectorManipulatorSegment>,
/// The compound Bezier curve is closed
pub closed: bool,
/// The transformation matrix to apply
pub transform: DAffine2,
}

View File

@ -91,8 +91,8 @@ impl Default for Mapping {
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::DragStart { add_to_selection: KeyShift }, key_down=Lmb},
entry! {action=PathMessage::PointerMove { alt_mirror_angle: KeyAlt, shift_mirror_distance: KeyShift }, message=InputMapperMessage::PointerMove},
entry! {action=PathMessage::DragStop, key_up=Lmb},
// Pen
entry! {action=PenMessage::PointerMove, message=InputMapperMessage::PointerMove},

View File

@ -3,3 +3,4 @@ pub mod tool;
pub mod tool_message;
pub mod tool_message_handler;
pub mod tools;
pub mod vector_editor;

View File

@ -108,6 +108,19 @@ impl SnapHandler {
}
}
/// Add arbitrary snapping points
/// This should be called after start_snap
pub fn add_snap_points(&mut self, document_message_handler: &DocumentMessageHandler, snap_points: Vec<DVec2>) {
if document_message_handler.snapping_enabled {
let (mut x_targets, mut y_targets): (Vec<f64>, Vec<f64>) = snap_points.into_iter().map(|vec| vec.into()).unzip();
if let Some((new_x_targets, new_y_targets)) = &mut self.snap_targets {
x_targets.append(new_x_targets);
y_targets.append(new_y_targets);
self.snap_targets = Some((x_targets, y_targets));
}
}
}
/// Finds the closest snap from an array of layers to the specified snap targets in viewport coords.
/// Returns 0 for each axis that there is no snap less than the snap tolerance.
pub fn snap_layers(

View File

@ -179,6 +179,7 @@ impl fmt::Display for ToolType {
pub enum StandardToolMessageType {
Abort,
DocumentIsDirty,
SelectionChanged,
}
// TODO: Find a nicer way in Rust to make this generic so we don't have to manually map to enum variants
@ -231,6 +232,10 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy
ToolType::Shape => Some(ShapeMessage::Abort.into()),
_ => None,
},
StandardToolMessageType::SelectionChanged => match tool {
ToolType::Path => Some(PathMessage::SelectionChanged.into()),
_ => None,
},
}
}

View File

@ -86,6 +86,7 @@ pub enum ToolMessage {
},
DocumentIsDirty,
ResetColors,
SelectionChanged,
SelectPrimaryColor {
color: Color,
},

View File

@ -56,6 +56,11 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
send_abort_to_tool(old_tool, tool_message, false);
}
// Send the SelectionChanged message to the active tool, this will ensure the selection is updated
if let Some(message) = standard_tool_message(tool_type, StandardToolMessageType::SelectionChanged) {
responses.push_back(message.into());
}
// Send the DocumentIsDirty message to the active tool's sub-tool message handler
if let Some(message) = standard_tool_message(tool_type, StandardToolMessageType::DocumentIsDirty) {
responses.push_back(message.into());
@ -86,6 +91,12 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
update_working_colors(document_data, responses);
}
SelectionChanged => {
let active_tool = self.tool_state.tool_data.active_tool_type;
if let Some(message) = standard_tool_message(active_tool, StandardToolMessageType::SelectionChanged) {
responses.push_back(message.into());
}
}
SelectPrimaryColor { color } => {
let document_data = &mut self.tool_state.document_tool_data;
document_data.primary_color = color;

View File

@ -1,5 +1,4 @@
use crate::consts::{COLOR_ACCENT, VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE};
use crate::document::utility_types::{VectorManipulatorSegment, VectorManipulatorShape};
use crate::consts::SELECTION_THRESHOLD;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
@ -7,14 +6,13 @@ use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::snapping::SnapHandler;
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::viewport_tools::vector_editor::shape_editor::ShapeEditor;
use graphene::color::Color;
use graphene::layers::style::{self, Fill, PathStyle, Stroke};
use graphene::Operation;
use glam::DVec2;
use graphene::intersection::Quad;
use glam::{DAffine2, DVec2};
use kurbo::{BezPath, PathEl, Vec2};
use serde::{Deserialize, Serialize};
#[derive(Default)]
@ -32,11 +30,18 @@ pub enum PathMessage {
Abort,
#[remain::unsorted]
DocumentIsDirty,
#[remain::unsorted]
SelectionChanged,
// Tool-specific messages
DragStart,
DragStart {
add_to_selection: Key,
},
DragStop,
PointerMove,
PointerMove {
alt_mirror_angle: Key,
shift_mirror_distance: Key,
},
}
impl PropertyHolder for Path {}
@ -85,25 +90,12 @@ impl Default for PathToolFsmState {
}
}
#[derive(Clone, Debug, Default)]
#[derive(Default)]
struct PathToolData {
anchor_marker_pool: Vec<Vec<LayerId>>,
handle_marker_pool: Vec<Vec<LayerId>>,
anchor_handle_line_pool: Vec<Vec<LayerId>>,
shape_outline_pool: Vec<Vec<LayerId>>,
selected_shapes: Vec<VectorManipulatorShape>,
selection: PathToolSelection,
}
impl PathToolData {}
#[derive(Clone, Debug, Default)]
struct PathToolSelection {
closest_layer_path: Vec<LayerId>,
closest_shape_id: usize,
overlay_path: Vec<LayerId>,
bez_path_elements: Vec<kurbo::PathEl>,
bez_segment_id: usize,
shape_editor: ShapeEditor,
snap_handler: SnapHandler,
alt_debounce: bool,
shift_debounce: bool,
}
impl Fsm for PathToolFsmState {
@ -125,298 +117,115 @@ impl Fsm for PathToolFsmState {
use PathToolFsmState::*;
match (self, event) {
(_, DocumentIsDirty) => {
let (mut anchor_i, mut handle_i, mut line_i, mut shape_i) = (0, 0, 0, 0);
// TODO: Capture a tool event instead of doing this?
(_, SelectionChanged) => {
// Remove any residual overlays that might exist on selection change
data.shape_editor.remove_overlays(responses);
let shapes_to_draw = document.selected_visible_layers_vector_points();
// Grow the overlay pools by the shortfall, if any
let (total_anchors, total_handles, total_anchor_handle_lines) = calculate_total_overlays_per_type(&shapes_to_draw);
let total_shapes = shapes_to_draw.len();
grow_overlay_pool_entries(&mut data.shape_outline_pool, total_shapes, add_shape_outline, responses);
grow_overlay_pool_entries(&mut data.anchor_handle_line_pool, total_anchor_handle_lines, add_anchor_handle_line, responses);
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);
// Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function
const BIAS: f64 = 0.0001;
// Draw the overlays for each shape
for shape_to_draw in &shapes_to_draw {
let shape_layer_path = &data.shape_outline_pool[shape_i];
responses.push_back(
DocumentMessage::Overlays(
Operation::SetShapePathInViewport {
path: shape_layer_path.clone(),
bez_path: shape_to_draw.path.clone(),
transform: shape_to_draw.transform.to_cols_array(),
}
.into(),
)
.into(),
);
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerVisibility {
path: shape_layer_path.clone(),
visible: true,
}
.into(),
)
.into(),
);
shape_i += 1;
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 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 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::Overlays(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into());
line_i += 1;
}
// 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 marker = &data.anchor_marker_pool[anchor_i];
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into());
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];
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::Overlays(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into());
handle_i += 1;
}
}
// Hide the remaining pooled overlays
for i in anchor_i..data.anchor_marker_pool.len() {
let marker = data.anchor_marker_pool[i].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker, visible: false }.into()).into());
}
for i in handle_i..data.handle_marker_pool.len() {
let marker = data.handle_marker_pool[i].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker, visible: false }.into()).into());
}
for i in line_i..data.anchor_handle_line_pool.len() {
let line = data.anchor_handle_line_pool[i].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: line, visible: false }.into()).into());
}
for i in shape_i..data.shape_outline_pool.len() {
let shape_i = data.shape_outline_pool[i].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: shape_i, visible: false }.into()).into());
}
// This currently creates new VectorManipulatorShapes for every shape, which is not ideal
// Atleast it is only on selection change for now
data.shape_editor.set_shapes_to_modify(document.selected_visible_layers_vector_shapes(responses));
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<LayerId>, shape_offset: usize },
Handle { handle_i: usize, layer_path: Vec<LayerId>, shape_offset: usize },
}
#[derive(Debug)]
struct Point {
point_type: PointType,
mouse_proximity: f64,
(_, DocumentIsDirty) => {
// Update the VectorManipulatorShapes by reference so they match the kurbo data
for shape in &mut data.shape_editor.shapes_to_modify {
shape.update_shape(document, responses);
}
self
}
// Mouse down
(_, DragStart { add_to_selection }) => {
let add_to_selection = input.keyboard.get(add_to_selection as usize);
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<PathEl> = (&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<PathEl> = (&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::Overlays(
Operation::SetLayerFill {
path: data.selection.overlay_path.clone(),
color: COLOR_ACCENT,
}
.into(),
)
.into(),
);
// Select the first point within the threshold (in pixels)
if data.shape_editor.select_point(input.mouse.position, SELECTION_THRESHOLD, add_to_selection, responses) {
responses.push_back(DocumentMessage::StartTransaction.into());
data.snap_handler.start_snap(document, document.visible_layers(), true, true);
let snap_points = data
.shape_editor
.shapes_to_modify
.iter()
.flat_map(|shape| shape.anchors.iter().flat_map(|anchor| anchor.points[0].as_ref()))
.map(|point| point.position)
.collect();
data.snap_handler.add_snap_points(document, snap_points);
Dragging
} else {
}
// We didn't find a point nearby, so consider selecting the nearest shape instead
else {
let selection_size = DVec2::new(2.0, 2.0);
// Select shapes directly under our mouse
let intersection = document
.graphene_document
.intersects_quad_root(Quad::from_box([input.mouse.position - selection_size, input.mouse.position + selection_size]));
if !intersection.is_empty() {
if add_to_selection {
responses.push_back(DocumentMessage::AddSelectedLayers { additional_layers: intersection }.into());
} else {
responses.push_back(
DocumentMessage::SetSelectedLayers {
replacement_selected_layers: intersection,
}
.into(),
);
}
} else {
// Clear the previous selection if we didn't find anything
if !input.keyboard.get(add_to_selection as usize) {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
}
}
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(),
// Dragging
(
Dragging,
PointerMove {
alt_mirror_angle,
shift_mirror_distance,
},
) => {
// Determine when alt state changes
let alt_pressed = input.keyboard.get(alt_mirror_angle as usize);
if alt_pressed != data.alt_debounce {
data.alt_debounce = alt_pressed;
// Only on alt down
if alt_pressed {
data.shape_editor.toggle_selected_mirror_angle();
}
.into(),
);
}
// Determine when shift state changes
let shift_pressed = input.keyboard.get(shift_mirror_distance as usize);
if shift_pressed != data.shift_debounce {
data.shift_debounce = shift_pressed;
data.shape_editor.toggle_selected_mirror_distance();
}
// Move the selected points by the mouse position
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
data.shape_editor.move_selected_points(snapped_position, responses);
Dragging
}
(_, PointerMove) => self,
// Mouse up
(_, DragStop) => {
let style = PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE)));
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerStyle {
path: data.selection.overlay_path.clone(),
style,
}
.into(),
)
.into(),
);
data.snap_handler.cleanup(responses);
Ready
}
(_, Abort) => {
// Destory the overlay layer pools
while let Some(layer) = data.anchor_marker_pool.pop() {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer }.into()).into());
}
while let Some(layer) = data.handle_marker_pool.pop() {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer }.into()).into());
}
while let Some(layer) = data.anchor_handle_line_pool.pop() {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer }.into()).into());
}
while let Some(layer) = data.shape_outline_pool.pop() {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer }.into()).into());
}
data.shape_editor.remove_overlays(responses);
Ready
}
(
_,
PointerMove {
alt_mirror_angle: _,
shift_mirror_distance: _,
},
) => self,
}
} else {
self
@ -430,20 +239,20 @@ impl Fsm for PathToolFsmState {
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Lmb),
label: String::from("Select Point (coming soon)"),
label: String::from("Select Point"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Add/Remove Point"),
label: String::from("Add/Remove Point (coming soon)"),
plus: true,
},
]),
HintGroup(vec![HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Drag Selected (coming soon)"),
label: String::from("Drag Selected"),
plus: false,
}]),
HintGroup(vec![
@ -486,7 +295,20 @@ impl Fsm for PathToolFsmState {
},
]),
]),
PathToolFsmState::Dragging => HintData(vec![]),
PathToolFsmState::Dragging => HintData(vec![
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("Toggle Mirror Angle"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Hold To Mirror Distance"),
plus: false,
}]),
]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
@ -496,163 +318,3 @@ impl Fsm for PathToolFsmState {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
struct VectorManipulatorTypes {
anchors: Vec<glam::DVec2>,
handles: Vec<glam::DVec2>,
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 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)
}
fn grow_overlay_pool_entries<F>(pool: &mut Vec<Vec<LayerId>>, total: usize, add_overlay_function: F, responses: &mut VecDeque<Message>)
where
F: Fn(&mut VecDeque<Message>) -> Vec<LayerId>,
{
if pool.len() < total {
let additional = total - pool.len();
pool.reserve(additional);
for _ in 0..additional {
let marker = add_overlay_function(responses);
pool.push(marker);
}
}
}
fn add_anchor_marker(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayRect {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
fn add_handle_marker(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayEllipse {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
fn add_anchor_handle_line(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayLine {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), None),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
fn add_shape_outline(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayShape {
path: layer_path.clone(),
bez_path: BezPath::default(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), None),
closed: false,
};
responses.push_back(DocumentMessage::Overlays(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
}

View File

@ -0,0 +1,27 @@
use std::ops::{Index, IndexMut};
// Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function
pub const ROUNDING_BIAS: f64 = 0.0001;
// The angle threshold in radians that we should mirror handles if we are below
pub const MINIMUM_MIRROR_THRESHOLD: f64 = 0.1;
#[repr(usize)]
#[derive(PartialEq, Clone, Debug)]
pub enum ControlPointType {
Anchor = 0,
Handle1 = 1,
Handle2 = 2,
}
// Allows us to use ManipulatorType for indexing
impl<T> Index<ControlPointType> for [T; 3] {
type Output = T;
fn index(&self, mt: ControlPointType) -> &T {
&self[mt as usize]
}
}
// Allows us to use ManipulatorType for indexing, mutably
impl<T> IndexMut<ControlPointType> for [T; 3] {
fn index_mut(&mut self, mt: ControlPointType) -> &mut T {
&mut self[mt as usize]
}
}

View File

@ -0,0 +1,5 @@
pub mod constants;
pub mod shape_editor;
pub mod vector_anchor;
pub mod vector_control_point;
pub mod vector_shape;

View File

@ -0,0 +1,216 @@
/*
Overview:
ShapeEditor
/ \
VectorShape ... VectorShape <- ShapeEditor contains many VectorShapes
/ \
VectorAnchor ... VectorAnchor <- VectorShape contains many VectorAnchors
VectorAnchor <- Container for the anchor metadata and optional VectorControlPoints
/
[Option<VectorControlPoint>; 3] <- [0] is the anchor's draggable point (but not metadata), [1] is the handle1's draggable point, [2] is the handle2's draggable point
/ | \
"Anchor" "Handle1" "Handle2" <- These are VectorControlPoints and the only editable / draggable "primitive"
*/
use super::vector_shape::VectorShape;
use super::{constants::MINIMUM_MIRROR_THRESHOLD, vector_anchor::VectorAnchor, vector_control_point::VectorControlPoint};
use crate::message_prelude::Message;
use glam::DVec2;
use std::collections::{HashSet, VecDeque};
/// ShapeEditor is the container for all of the selected kurbo paths that are
/// represented as VectorShapes and provides functionality required
/// to query and create the VectorShapes / VectorAnchors / VectorControlPoints
#[derive(Clone, Debug, Default)]
pub struct ShapeEditor {
// The shapes we can select anchors / handles from
pub shapes_to_modify: Vec<VectorShape>,
// Index of the shape that contained the most recent selected point
pub selected_shape_indices: HashSet<usize>,
// The initial drag position of the mouse on drag start
pub drag_start_position: DVec2,
}
impl ShapeEditor {
/// Select the first point within the selection threshold
/// Returns true if we've found a point, false otherwise
pub fn select_point(&mut self, mouse_position: DVec2, select_threshold: f64, add_to_selection: bool, responses: &mut VecDeque<Message>) -> bool {
if self.shapes_to_modify.is_empty() {
return false;
}
if let Some((shape_index, anchor_index, point_index)) = self.find_nearest_point_indicies(mouse_position, select_threshold) {
log::trace!("Selecting: shape {} / anchor {} / point {}", shape_index, anchor_index, point_index);
// Add this shape to the selection
self.add_selected_shape(shape_index);
// If the point we're selecting has already been selected
// we can assume this point exists.. since we did just click on it hense the unwrap
let is_point_selected = self.shapes_to_modify[shape_index].anchors[anchor_index].points[point_index].as_ref().unwrap().is_selected;
// Deselected if we're not adding to the selection
if !add_to_selection && !is_point_selected {
self.deselect_all(responses);
}
let selected_shape = &mut self.shapes_to_modify[shape_index];
selected_shape.elements = selected_shape.bez_path.clone().into_iter().collect();
// Should we select or deselect the point?
let should_select = if is_point_selected { !(add_to_selection && is_point_selected) } else { true };
// Add which anchor and point was selected
let selected_anchor = selected_shape.select_anchor(anchor_index);
let selected_point = selected_anchor.select_point(point_index, should_select, responses);
// Set the drag start position based on the selected point
if let Some(point) = selected_point {
self.drag_start_position = point.position;
}
// Due to the shape data structure not persisting across shape selection changes we need to rely on the kurbo path to know if we should mirror
selected_anchor.set_mirroring((selected_anchor.angle_between_handles().abs() - std::f64::consts::PI).abs() < MINIMUM_MIRROR_THRESHOLD);
return true;
}
false
}
/// Find a point that is within the selection threshold and return an index to the shape, anchor, and point
pub fn find_nearest_point_indicies(&mut self, mouse_position: DVec2, select_threshold: f64) -> Option<(usize, usize, usize)> {
if self.shapes_to_modify.is_empty() {
return None;
}
let select_threshold_squared = select_threshold * select_threshold;
// Find the closest control point among all elements of shapes_to_modify
for shape_index in 0..self.shapes_to_modify.len() {
if let Some((anchor_index, point_index, distance_squared)) = self.closest_point_indices(&self.shapes_to_modify[shape_index], mouse_position) {
// Choose the first point under the threshold
if distance_squared < select_threshold_squared {
log::trace!("Selecting: shape {} / anchor {} / point {}", shape_index, anchor_index, point_index);
return Some((shape_index, anchor_index, point_index));
}
}
}
None
}
/// A wrapper for find_nearest_point_indicies and returns a mutable VectorControlPoint
pub fn find_nearest_point(&mut self, mouse_position: DVec2, select_threshold: f64) -> Option<&mut VectorControlPoint> {
let (shape_index, anchor_index, point_index) = self.find_nearest_point_indicies(mouse_position, select_threshold)?;
let selected_shape = &mut self.shapes_to_modify[shape_index];
selected_shape.anchors[anchor_index].points[point_index].as_mut()
}
/// Set the shapes we consider for selection, we will choose draggable handles / anchors from these shapes.
pub fn set_shapes_to_modify(&mut self, selected_shapes: Vec<VectorShape>) {
self.shapes_to_modify = selected_shapes;
}
/// Add a shape to the hashset of shapes we consider for selection
pub fn add_selected_shape(&mut self, shape_index: usize) {
self.selected_shape_indices.insert(shape_index);
}
/// Provide the shapes that the currently selected points are a part of
pub fn selected_shapes(&self) -> impl Iterator<Item = &VectorShape> {
self.shapes_to_modify
.iter()
.enumerate()
.filter_map(|(index, shape)| if self.selected_shape_indices.contains(&index) { Some(shape) } else { None })
}
/// Provide the mutable shapes that the currently selected points are a part of
pub fn selected_shapes_mut(&mut self) -> impl Iterator<Item = &mut VectorShape> {
self.shapes_to_modify
.iter_mut()
.enumerate()
.filter_map(|(index, shape)| if self.selected_shape_indices.contains(&index) { Some(shape) } else { None })
}
/// Provide the currently selected anchor by reference
pub fn selected_anchors(&self) -> impl Iterator<Item = &VectorAnchor> {
self.selected_shapes().flat_map(|shape| shape.selected_anchors())
}
/// Provide the currently selected anchors by mutable reference
pub fn selected_anchors_mut(&mut self) -> impl Iterator<Item = &mut VectorAnchor> {
self.selected_shapes_mut().flat_map(|shape| shape.selected_anchors_mut())
}
/// Provide the currently selected points by reference
pub fn selected_points(&self) -> impl Iterator<Item = &VectorControlPoint> {
self.selected_shapes().flat_map(|shape| shape.selected_anchors()).flat_map(|anchors| anchors.selected_points())
}
/// Provide the currently selected points by mutable reference
pub fn selected_points_mut(&mut self) -> impl Iterator<Item = &mut VectorControlPoint> {
self.selected_shapes_mut()
.flat_map(|shape| shape.selected_anchors_mut())
.flat_map(|anchors| anchors.selected_points_mut())
}
/// Move the selected points by dragging the moue
pub fn move_selected_points(&mut self, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
let drag_start_position = self.drag_start_position;
for shape in self.selected_shapes_mut() {
shape.move_selected(mouse_position - drag_start_position, responses);
}
}
/// Toggle if the handles should mirror angle across the anchor positon
pub fn toggle_selected_mirror_angle(&mut self) {
for anchor in self.selected_anchors_mut() {
anchor.handle_mirror_angle = !anchor.handle_mirror_angle;
}
}
/// Toggle if the handles should mirror distance across the anchor position
pub fn toggle_selected_mirror_distance(&mut self) {
for anchor in self.selected_anchors_mut() {
anchor.handle_mirror_distance = !anchor.handle_mirror_distance;
}
}
/// Remove all of the overlays from the shapes the manipulation handler has created
pub fn deselect_all(&mut self, responses: &mut VecDeque<Message>) {
for shape in self.shapes_to_modify.iter_mut() {
shape.clear_selected_anchors(responses);
// Apply the final elements to the shape
// Fixes the snapback problem
shape.elements = shape.bez_path.clone().into_iter().collect();
}
}
/// Remove all of the overlays for the VectorManipulators / shape
pub fn remove_overlays(&mut self, responses: &mut VecDeque<Message>) {
for shape in self.shapes_to_modify.iter_mut() {
shape.remove_overlays(responses)
}
}
// TODO Use quadtree or some equivalent spatial acceleration structure to improve this to O(log(n))
/// Find the closest point, anchor and distance so we can select path elements
/// Brute force comparison to determine which handle / anchor we want to select, O(n)
fn closest_point_indices(&self, shape: &VectorShape, pos: glam::DVec2) -> Option<(usize, usize, f64)> {
let mut closest_distance_squared: f64 = f64::MAX; // Not ideal
let mut result: Option<(usize, usize, f64)> = None;
for (anchor_index, anchor) in shape.anchors.iter().enumerate() {
let point_index = anchor.closest_point(pos);
if let Some(point) = &anchor.points[point_index] {
if point.can_be_selected {
let distance_squared = point.position.distance_squared(pos);
if distance_squared < closest_distance_squared {
closest_distance_squared = distance_squared;
result = Some((anchor_index, point_index, distance_squared));
}
}
}
}
result
}
}

View File

@ -0,0 +1,404 @@
use glam::{DAffine2, DVec2};
use graphene::{LayerId, Operation};
use kurbo::{PathEl, Point, Vec2};
use std::collections::VecDeque;
use crate::{
consts::VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE,
message_prelude::{DocumentMessage, Message},
};
use super::{
constants::{ControlPointType, ROUNDING_BIAS},
vector_control_point::VectorControlPoint,
};
/// VectorAnchor is used to represent an anchor point on the path that can be moved.
/// It contains 0-2 handles that are optionally displayed.
#[derive(PartialEq, Clone, Debug, Default)]
pub struct VectorAnchor {
// Editable points for the anchor & handles
pub points: [Option<VectorControlPoint>; 3],
// The overlays for this handle line rendering
pub handle_line_overlays: (Option<Vec<LayerId>>, Option<Vec<LayerId>>),
// Does this anchor point have a path close element?
pub close_element_id: Option<usize>,
// Should we maintain the angle between the handles?
pub handle_mirror_angle: bool,
// Should we make the handles equidistance from the anchor?
pub handle_mirror_distance: bool,
}
impl VectorAnchor {
/// Finds the closest VectorControlPoint owned by this anchor. This can be the handles or the anchor itself
pub fn closest_point(&self, target: glam::DVec2) -> usize {
let mut closest_index: usize = 0;
let mut closest_distance_squared: f64 = f64::MAX; // Not ideal
for (index, point) in self.points.iter().enumerate() {
if let Some(point) = point {
let distance_squared = point.position.distance_squared(target);
if distance_squared < closest_distance_squared {
closest_distance_squared = distance_squared;
closest_index = index;
}
}
}
closest_index
}
// TODO Cleanup the internals of this function
/// Move the selected points by the provided delta
pub fn move_selected_points(&mut self, position_delta: DVec2, path_elements: &mut Vec<kurbo::PathEl>, transform: &DAffine2) {
let place_mirrored_handle = |center: kurbo::Point, original: kurbo::Point, target: kurbo::Point, selected: bool, mirror_angle: bool, mirror_distance: bool| -> kurbo::Point {
if !selected || !mirror_angle {
return original;
}
// Keep rotational similarity, but distance variable
let radius = if mirror_distance { center.distance(target) } else { center.distance(original) };
let phi = (center - target).atan2();
kurbo::Point {
x: radius * phi.cos() + center.x,
y: radius * phi.sin() + center.y,
}
};
for selected_point in self.selected_points() {
let delta = transform.inverse().transform_vector2(position_delta);
let delta = Vec2::new(delta.x, delta.y);
let h1_selected = ControlPointType::Handle1 == selected_point.manipulator_type;
let h2_selected = ControlPointType::Handle2 == selected_point.manipulator_type;
let dragging_anchor = !(h1_selected || h2_selected);
// This section is particularly ugly and could use revision. Kurbo makes it somewhat difficult based on its approach.
// If neither handle is selected, we are dragging an anchor point
if dragging_anchor {
let handle1_exists_and_selected = self.points[ControlPointType::Handle1].is_some() && self.points[ControlPointType::Handle1].as_ref().unwrap().is_selected;
// Move the anchor point and handle on the same path element
let selected_element = match &path_elements[selected_point.kurbo_element_id] {
PathEl::MoveTo(p) => PathEl::MoveTo(*p + delta),
PathEl::LineTo(p) => PathEl::LineTo(*p + delta),
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, *p + delta),
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(*a1, if handle1_exists_and_selected { *a2 } else { *a2 + delta }, *p + delta),
PathEl::ClosePath => PathEl::ClosePath,
};
// Move the handle on the adjacent path element
if let Some(handle) = &self.points[ControlPointType::Handle2] {
if !handle.is_selected {
let neighbor = match &path_elements[handle.kurbo_element_id] {
PathEl::MoveTo(p) => PathEl::MoveTo(*p),
PathEl::LineTo(p) => PathEl::LineTo(*p),
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, *p),
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(*a1 + delta, *a2, *p),
PathEl::ClosePath => PathEl::ClosePath,
};
path_elements[handle.kurbo_element_id] = neighbor;
}
}
if let Some(close_id) = self.close_element_id {
// Move the invisible point that can be caused by MoveTo / closing the path
path_elements[close_id] = match &path_elements[close_id] {
PathEl::MoveTo(p) => PathEl::MoveTo(*p + delta),
PathEl::LineTo(p) => PathEl::LineTo(*p + delta),
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, *p + delta),
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(*a1, *a2 + delta, *p + delta),
PathEl::ClosePath => PathEl::ClosePath,
};
}
path_elements[selected_point.kurbo_element_id] = selected_element;
}
// We are dragging a handle
else {
let should_mirror_angle = self.handle_mirror_angle;
let should_mirror_distance = self.handle_mirror_distance;
// Move the selected handle
let (selected_element, anchor, selected_handle) = match &path_elements[selected_point.kurbo_element_id] {
PathEl::MoveTo(p) => (PathEl::MoveTo(*p), *p, *p),
PathEl::LineTo(p) => (PathEl::LineTo(*p), *p, *p),
PathEl::QuadTo(a1, p) => (PathEl::QuadTo(*a1 + delta, *p), *p, *a1 + delta),
PathEl::CurveTo(a1, a2, p) => {
let a1_point = if h2_selected { *a1 + delta } else { *a1 };
let a2_point = if h1_selected { *a2 + delta } else { *a2 };
(PathEl::CurveTo(a1_point, a2_point, *p), *p, if h1_selected { a2_point } else { a1_point })
}
PathEl::ClosePath => (PathEl::ClosePath, Point::ZERO, Point::ZERO),
};
let opposing_handle = self.opposing_handle(selected_point);
let only_one_handle_selected = !(selected_point.is_selected && opposing_handle.is_some() && opposing_handle.as_ref().unwrap().is_selected);
// Only move the handles if we don't have both handles selected
if only_one_handle_selected {
// Move the opposing handle on the adjacent path element
if let Some(handle) = opposing_handle {
let handle_point = transform.inverse().transform_point2(handle.position);
let handle_point = Point { x: handle_point.x, y: handle_point.y };
let neighbor = match &path_elements[handle.kurbo_element_id] {
PathEl::MoveTo(p) => PathEl::MoveTo(*p),
PathEl::LineTo(p) => PathEl::LineTo(*p),
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, *p),
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(
place_mirrored_handle(
anchor,
if h1_selected { handle_point } else { *a1 },
selected_handle,
h1_selected,
should_mirror_angle,
should_mirror_distance,
),
place_mirrored_handle(
*p,
if h2_selected { handle_point } else { *a2 },
selected_handle,
h2_selected,
should_mirror_angle,
should_mirror_distance,
),
*p,
),
PathEl::ClosePath => PathEl::ClosePath,
};
path_elements[handle.kurbo_element_id] = neighbor;
}
}
path_elements[selected_point.kurbo_element_id] = selected_element;
}
}
}
/// Returns true is any points in this anchor are selected
pub fn is_selected(&self) -> bool {
self.points.iter().flatten().any(|pnt| pnt.is_selected)
}
/// Set a point to selected by ID
pub fn select_point(&mut self, point_id: usize, selected: bool, responses: &mut VecDeque<Message>) -> Option<&mut VectorControlPoint> {
if let Some(point) = self.points[point_id].as_mut() {
point.set_selected(selected, responses);
}
self.points[point_id].as_mut()
}
/// Clear the selected points for this anchor
pub fn clear_selected_points(&mut self, responses: &mut VecDeque<Message>) {
for point in self.points.iter_mut().flatten() {
point.set_selected(false, responses);
}
}
/// Provides the selected points in this anchor
pub fn selected_points(&self) -> impl Iterator<Item = &VectorControlPoint> {
self.points.iter().flatten().filter(|pnt| pnt.is_selected)
}
/// Provides mutable selected points in this anchor
pub fn selected_points_mut(&mut self) -> impl Iterator<Item = &mut VectorControlPoint> {
self.points.iter_mut().flatten().filter(|pnt| pnt.is_selected)
}
/// Angle between handles in radians
pub fn angle_between_handles(&self) -> f64 {
if let [Some(a1), Some(h1), Some(h2)] = &self.points {
return (a1.position - h1.position).angle_between(a1.position - h2.position);
}
0.0
}
/// Returns the opposing handle to the handle provided
pub fn opposing_handle(&self, handle: &VectorControlPoint) -> &Option<VectorControlPoint> {
if let Some(point) = &self.points[ControlPointType::Handle1] {
if point == handle {
return &self.points[ControlPointType::Handle2];
}
};
if let Some(point) = &self.points[ControlPointType::Handle2] {
if point == handle {
return &self.points[ControlPointType::Handle1];
}
};
&None
}
/// Set the mirroring state
pub fn set_mirroring(&mut self, mirroring: bool) {
self.handle_mirror_angle = mirroring;
}
/// Helper function to more easily set position of VectorControlPoints
pub fn set_point_position(&mut self, point_index: usize, position: DVec2) {
if let Some(point) = &mut self.points[point_index] {
point.position = position;
}
}
/// Updates the position of the anchor based on the kurbo path
pub fn place_anchor_overlay(&self, responses: &mut VecDeque<Message>) {
if let Some(anchor_point) = &self.points[ControlPointType::Anchor] {
if let Some(anchor_overlay) = &anchor_point.overlay_path {
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
let angle = 0.;
let translation = (anchor_point.position - (scale / 2.) + ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerTransformInViewport {
path: anchor_overlay.clone(),
transform,
}
.into(),
)
.into(),
);
}
}
}
/// Updates the position of the handle's overlays based on the kurbo path
pub fn place_handle_overlay(&self, responses: &mut VecDeque<Message>) {
if let Some(anchor_point) = &self.points[ControlPointType::Anchor] {
// Helper function to keep things DRY
let mut place_handle_and_line = |handle: &VectorControlPoint, line: &Option<Vec<LayerId>>| {
if let Some(line_overlay) = line {
let line_vector = anchor_point.position - handle.position;
let scale = DVec2::splat(line_vector.length());
let angle = -line_vector.angle_between(DVec2::X);
let translation = (handle.position + ROUNDING_BIAS).round() + DVec2::splat(0.5);
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerTransformInViewport {
path: line_overlay.clone(),
transform,
}
.into(),
)
.into(),
);
}
if let Some(line_overlay) = &handle.overlay_path {
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
let angle = 0.;
let translation = (handle.position - (scale / 2.) + ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerTransformInViewport {
path: line_overlay.clone(),
transform,
}
.into(),
)
.into(),
);
}
};
let [_, h1, h2] = &self.points;
let (line1, line2) = &self.handle_line_overlays;
if let Some(handle) = &h1 {
place_handle_and_line(handle, line1);
}
if let Some(handle) = &h2 {
place_handle_and_line(handle, line2);
}
}
}
/// Removes the anchor overlay from the overlay document
pub fn remove_anchor_overlay(&mut self, responses: &mut VecDeque<Message>) {
if let Some(anchor_point) = &mut self.points[ControlPointType::Anchor] {
if let Some(overlay_path) = &anchor_point.overlay_path {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_path.clone() }.into()).into());
}
anchor_point.overlay_path = None;
}
}
/// Removes the handles overlay from the overlay document
pub fn remove_handle_overlay(&mut self, responses: &mut VecDeque<Message>) {
let [_, h1, h2] = &mut self.points;
let (line1, line2) = &mut self.handle_line_overlays;
// Helper function to keep things DRY
let mut delete_message = |handle: &Option<Vec<LayerId>>| {
if let Some(overlay_path) = handle {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_path.clone() }.into()).into());
}
};
// Delete the handles themselves
if let Some(handle) = h1 {
delete_message(&handle.overlay_path);
handle.overlay_path = None;
}
if let Some(handle) = h2 {
delete_message(&handle.overlay_path);
handle.overlay_path = None;
}
// Delete the handle line layers
delete_message(line1);
delete_message(line2);
self.handle_line_overlays = (None, None);
}
/// Clear overlays for this anchor, do this prior to deletion
pub fn remove_overlays(&mut self, responses: &mut VecDeque<Message>) {
self.remove_anchor_overlay(responses);
self.remove_handle_overlay(responses);
}
/// Sets the visibility of the anchors overlay
pub fn set_anchor_visiblity(&self, visibility: bool, responses: &mut VecDeque<Message>) {
if let Some(anchor_point) = &self.points[ControlPointType::Anchor] {
if let Some(overlay_path) = &anchor_point.overlay_path {
responses.push_back(self.visibility_message(overlay_path.clone(), visibility));
}
}
}
/// Sets the visibility of the handles overlay
pub fn set_handle_visiblity(&self, visibility: bool, responses: &mut VecDeque<Message>) {
let [_, h1, h2] = &self.points;
let (line1, line2) = &self.handle_line_overlays;
if let Some(handle) = h1 {
if let Some(overlay_path) = &handle.overlay_path {
responses.push_back(self.visibility_message(overlay_path.clone(), visibility));
}
}
if let Some(handle) = h2 {
if let Some(overlay_path) = &handle.overlay_path {
responses.push_back(self.visibility_message(overlay_path.clone(), visibility));
}
}
if let Some(overlay_path) = &line1 {
responses.push_back(self.visibility_message(overlay_path.clone(), visibility));
}
if let Some(overlay_path) = &line2 {
responses.push_back(self.visibility_message(overlay_path.clone(), visibility));
}
}
/// Create a visibility message for an overlay
fn visibility_message(&self, layer_path: Vec<LayerId>, visibility: bool) -> Message {
DocumentMessage::Overlays(
Operation::SetLayerVisibility {
path: layer_path,
visible: visibility,
}
.into(),
)
.into()
}
}

View File

@ -0,0 +1,74 @@
use glam::DVec2;
use graphene::{
color::Color,
layers::style::{Fill, PathStyle, Stroke},
LayerId, Operation,
};
use std::collections::VecDeque;
use crate::{
consts::COLOR_ACCENT,
message_prelude::{DocumentMessage, Message},
};
use super::constants::ControlPointType;
/// VectorControlPoint represents any grabbable point, anchor or handle
#[derive(PartialEq, Clone, Debug)]
pub struct VectorControlPoint {
// The associated position in the BezPath
pub kurbo_element_id: usize,
// The sibling element if this is a handle
pub position: glam::DVec2,
// The path to the overlay for this point rendering
pub overlay_path: Option<Vec<LayerId>>,
// The type of manipulator this point is
pub manipulator_type: ControlPointType,
// Can be selected
pub can_be_selected: bool,
// Is this point currently selected?
pub is_selected: bool,
}
impl Default for VectorControlPoint {
fn default() -> Self {
Self {
kurbo_element_id: 0,
position: DVec2::ZERO,
overlay_path: None,
manipulator_type: ControlPointType::Anchor,
can_be_selected: true,
is_selected: false,
}
}
}
const POINT_STROKE_WIDTH: f32 = 2.0;
impl VectorControlPoint {
/// Sets if this point is selected and updates the overlay to represent that
pub fn set_selected(&mut self, selected: bool, responses: &mut VecDeque<Message>) {
if selected {
self.set_overlay_style(POINT_STROKE_WIDTH + 1.0, COLOR_ACCENT, COLOR_ACCENT, responses);
} else {
self.set_overlay_style(POINT_STROKE_WIDTH, COLOR_ACCENT, Color::WHITE, responses);
}
self.is_selected = selected;
}
/// Sets the overlay style for this point
pub fn set_overlay_style(&self, stroke_width: f32, stroke_color: Color, fill_color: Color, responses: &mut VecDeque<Message>) {
if let Some(overlay_path) = &self.overlay_path {
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerStyle {
path: overlay_path.clone(),
style: PathStyle::new(Some(Stroke::new(stroke_color, stroke_width)), Some(Fill::new(fill_color))),
}
.into(),
)
.into(),
);
}
}
}

View File

@ -0,0 +1,491 @@
use glam::{DAffine2, DVec2};
use graphene::{
color::Color,
layers::{
layer_info::LayerDataType,
style::{self, Fill, Stroke},
},
LayerId, Operation,
};
use kurbo::{BezPath, PathEl};
use std::collections::HashSet;
use std::collections::VecDeque;
use crate::{
consts::COLOR_ACCENT,
document::DocumentMessageHandler,
message_prelude::{generate_uuid, DocumentMessage, Message},
};
use super::{constants::ControlPointType, vector_anchor::VectorAnchor, vector_control_point::VectorControlPoint};
/// VectorShape represents a single kurbo shape and maintains a parallel data structure
/// For each kurbo path we keep a VectorShape which contains the handles and anchors for that path
#[derive(PartialEq, Clone, Debug, Default)]
pub struct VectorShape {
/// The path to the shape layer
pub layer_path: Vec<LayerId>,
/// The outline of the shape via kurbo
pub bez_path: kurbo::BezPath,
/// The elements of the kurbo shape
pub elements: Vec<kurbo::PathEl>,
/// The anchors that are made up of the control points / handles
pub anchors: Vec<VectorAnchor>,
/// The overlays for the shape, anchors and manipulator handles
pub shape_overlay: Option<Vec<LayerId>>,
/// If the compound Bezier curve is closed
pub closed: bool,
/// The transformation matrix to apply
pub transform: DAffine2,
// Indices for the most recent select point anchors
pub selected_anchor_indices: HashSet<usize>,
}
type IndexedEl = (usize, kurbo::PathEl);
impl VectorShape {
pub fn new(layer_path: Vec<LayerId>, transform: DAffine2, bez_path: &BezPath, closed: bool, responses: &mut VecDeque<Message>) -> Self {
let mut shape = VectorShape {
layer_path,
bez_path: bez_path.clone(),
closed,
transform,
elements: bez_path.into_iter().collect(),
..Default::default()
};
shape.shape_overlay = Some(shape.create_shape_outline_overlay(responses));
shape.anchors = shape.create_anchors_from_kurbo(responses);
// TODO: This is a hack to allow Text to work. The shape isn't a path until this message is sent (it appears)
responses.push_back(
Operation::SetShapePathInViewport {
path: shape.layer_path.clone(),
bez_path: shape.elements.clone().into_iter().collect(),
transform: shape.transform.to_cols_array(),
}
.into(),
);
shape
}
/// Select an anchor
pub fn select_anchor(&mut self, anchor_index: usize) -> &mut VectorAnchor {
self.selected_anchor_indices.insert(anchor_index);
&mut self.anchors[anchor_index]
}
/// Deselect an anchor
pub fn deselect_anchor(&mut self, anchor_index: usize, responses: &mut VecDeque<Message>) {
self.anchors[anchor_index].clear_selected_points(responses);
self.selected_anchor_indices.remove(&anchor_index);
}
/// Select all the anchors in this shape
pub fn select_all_anchors(&mut self, responses: &mut VecDeque<Message>) {
for (index, anchor) in self.anchors.iter_mut().enumerate() {
self.selected_anchor_indices.insert(index);
anchor.select_point(0, true, responses);
}
}
/// Clear all the selected anchors, and clear the selected points on the anchors
pub fn clear_selected_anchors(&mut self, responses: &mut VecDeque<Message>) {
for anchor_index in self.selected_anchor_indices.iter() {
self.anchors[*anchor_index].clear_selected_points(responses);
}
self.selected_anchor_indices.clear();
}
/// Return all the selected anchors by reference
pub fn selected_anchors(&self) -> impl Iterator<Item = &VectorAnchor> {
self.anchors
.iter()
.enumerate()
.filter_map(|(index, anchor)| if self.selected_anchor_indices.contains(&index) { Some(anchor) } else { None })
}
/// Return all the selected anchors, mutable
pub fn selected_anchors_mut(&mut self) -> impl Iterator<Item = &mut VectorAnchor> {
self.anchors
.iter_mut()
.enumerate()
.filter_map(|(index, anchor)| if self.selected_anchor_indices.contains(&index) { Some(anchor) } else { None })
}
/// Move the selected point based on mouse input, if this is a handle we can control if we are mirroring or not
/// A wrapper around move_point to handle mirror state / submit the changes
pub fn move_selected(&mut self, position_delta: DVec2, responses: &mut VecDeque<Message>) {
let transform = &self.transform.clone();
let mut edited_bez_path = self.elements.clone();
for selected_anchor in self.selected_anchors_mut() {
selected_anchor.move_selected_points(position_delta, &mut edited_bez_path, transform);
}
// We've made our changes to the shape, submit them
responses.push_back(
Operation::SetShapePathInViewport {
path: self.layer_path.clone(),
bez_path: edited_bez_path.into_iter().collect(),
transform: self.transform.to_cols_array(),
}
.into(),
);
}
/// Update the anchors and segments to match the kurbo shape
/// Should be called whenever the kurbo shape changes
pub fn update_shape(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let viewport_transform = document.graphene_document.generate_transform_relative_to_viewport(&self.layer_path).unwrap();
let layer = document.graphene_document.layer(&self.layer_path).unwrap();
if let LayerDataType::Shape(shape) = &layer.data {
let path = shape.path.clone();
self.transform = viewport_transform;
// Update point positions
self.update_anchors_from_kurbo(&path);
self.bez_path = path;
// Update the overlays to represent the changes to the kurbo path
self.place_shape_outline_overlay(responses);
self.place_anchor_overlays(responses);
self.place_handle_overlays(responses);
}
}
/// Place point in local space in relation to this shape's transform
fn to_local_space(&self, point: kurbo::Point) -> DVec2 {
self.transform.transform_point2(DVec2::from((point.x, point.y)))
}
/// Create an anchor on the boundary between two kurbo PathElements with optional handles
fn create_anchor(&self, first: Option<IndexedEl>, second: Option<IndexedEl>, responses: &mut VecDeque<Message>) -> VectorAnchor {
let mut handle1 = None;
let mut anchor_position: glam::DVec2 = glam::DVec2::ZERO;
let mut handle2 = None;
let mut anchor_element_id: usize = 0;
let create_point = |id: usize, point: DVec2, overlay_path: Vec<LayerId>, manipulator_type: ControlPointType| -> VectorControlPoint {
VectorControlPoint {
kurbo_element_id: id,
position: point,
overlay_path: Some(overlay_path),
can_be_selected: true,
manipulator_type,
is_selected: false,
}
};
if let Some((first_element_id, first_element)) = first {
anchor_element_id = first_element_id;
match first_element {
kurbo::PathEl::MoveTo(anchor) | kurbo::PathEl::LineTo(anchor) => anchor_position = self.to_local_space(anchor),
kurbo::PathEl::QuadTo(handle, anchor) | kurbo::PathEl::CurveTo(_, handle, anchor) => {
anchor_position = self.to_local_space(anchor);
handle1 = Some(create_point(
first_element_id,
self.to_local_space(handle),
self.create_handle_overlay(responses),
ControlPointType::Handle1,
));
}
_ => (),
}
}
if let Some((second_element_id, second_element)) = second {
match second_element {
kurbo::PathEl::CurveTo(handle, _, _) | kurbo::PathEl::QuadTo(handle, _) => {
handle2 = Some(create_point(
second_element_id,
self.to_local_space(handle),
self.create_handle_overlay(responses),
ControlPointType::Handle2,
));
}
_ => (),
}
}
VectorAnchor {
handle_line_overlays: (self.create_handle_line_overlay(&handle1, responses), self.create_handle_line_overlay(&handle2, responses)),
points: [
Some(create_point(anchor_element_id, anchor_position, self.create_anchor_overlay(responses), ControlPointType::Anchor)),
handle1,
handle2,
],
close_element_id: None,
handle_mirror_angle: true,
handle_mirror_distance: false,
}
}
/// Close the path by checking if the distance between the last element and the first MoveTo is less than the tolerance.
/// If so, create a new anchor at the first point. Otherwise, create a new anchor at the last point.
fn close_path(
&self,
points: &mut Vec<VectorAnchor>,
to_replace: usize,
first_path_element: Option<IndexedEl>,
last_path_element: Option<IndexedEl>,
recent_move_to: Option<IndexedEl>,
responses: &mut VecDeque<Message>,
) {
if let (Some(first), Some(last), Some(move_to)) = (first_path_element, last_path_element, recent_move_to) {
let position_equal = match (move_to.1, last.1) {
(PathEl::MoveTo(p1), PathEl::LineTo(p2)) => p1.distance_squared(p2) < 0.01,
(PathEl::MoveTo(p1), PathEl::QuadTo(_, p2)) => p1.distance_squared(p2) < 0.01,
(PathEl::MoveTo(p1), PathEl::CurveTo(_, _, p2)) => p1.distance_squared(p2) < 0.01,
_ => false,
};
// Does this end in the same position it started?
if position_equal {
points[to_replace].remove_overlays(responses);
points[to_replace] = self.create_anchor(Some(last), Some(first), responses);
points[to_replace].close_element_id = Some(move_to.0);
} else {
points.push(self.create_anchor(Some(last), Some(first), responses));
}
}
}
/// Create the anchors from the kurbo path, only done during of new anchors construction
fn create_anchors_from_kurbo(&self, responses: &mut VecDeque<Message>) -> Vec<VectorAnchor> {
// We need the indices paired with the kurbo path elements
let indexed_elements = self.bez_path.elements().iter().enumerate().map(|(index, element)| (index, *element)).collect::<Vec<IndexedEl>>();
// Create the manipulation points
let mut anchors: Vec<VectorAnchor> = vec![];
let (mut first_path_element, mut last_path_element): (Option<IndexedEl>, Option<IndexedEl>) = (None, None);
let mut last_move_to_element: Option<IndexedEl> = None;
let mut ended_with_close_path = false;
let mut first_move_to_id: usize = 0;
// TODO Consider using a LL(1) grammar to improve readability
// Create an anchor at each join between two kurbo segments
for elements in indexed_elements.windows(2) {
let (_, current_element) = elements[0];
let (_, next_element) = elements[1];
ended_with_close_path = false;
if matches!(current_element, kurbo::PathEl::ClosePath) {
continue;
}
// An anchor cannot stradle a line / curve segment and a ClosePath segment
if matches!(next_element, kurbo::PathEl::ClosePath) {
ended_with_close_path = true;
if self.closed {
self.close_path(&mut anchors, first_move_to_id, first_path_element, last_path_element, last_move_to_element, responses);
} else {
anchors.push(self.create_anchor(last_path_element, None, responses));
}
continue;
}
// Keep track of the first and last elements of this shape
if matches!(current_element, kurbo::PathEl::MoveTo(_)) {
last_move_to_element = Some(elements[0]);
first_path_element = Some(elements[1]);
first_move_to_id = anchors.len();
}
last_path_element = Some(elements[1]);
anchors.push(self.create_anchor(Some(elements[0]), Some(elements[1]), responses));
}
// If the path definition didn't include a ClosePath, we still need to behave as though it did
if !ended_with_close_path {
if self.closed {
self.close_path(&mut anchors, first_move_to_id, first_path_element, last_path_element, last_move_to_element, responses);
} else {
anchors.push(self.create_anchor(last_path_element, None, responses));
}
}
anchors
}
/// Update the anchors to match the kurbo path
fn update_anchors_from_kurbo(&mut self, path: &BezPath) {
let space_transform = |point: kurbo::Point| self.transform.transform_point2(DVec2::from((point.x, point.y)));
for anchor_index in 0..self.anchors.len() {
let elements = path.elements();
let anchor = &mut self.anchors[anchor_index];
if let Some(anchor_point) = &mut anchor.points[ControlPointType::Anchor] {
match elements[anchor_point.kurbo_element_id] {
kurbo::PathEl::MoveTo(anchor_position) | kurbo::PathEl::LineTo(anchor_position) => anchor.set_point_position(ControlPointType::Anchor as usize, space_transform(anchor_position)),
kurbo::PathEl::QuadTo(handle_position, anchor_position) | kurbo::PathEl::CurveTo(_, handle_position, anchor_position) => {
anchor.set_point_position(ControlPointType::Anchor as usize, space_transform(anchor_position));
if anchor.points[ControlPointType::Handle1].is_some() {
anchor.set_point_position(ControlPointType::Handle1 as usize, space_transform(handle_position));
}
}
_ => (),
}
if let Some(handle) = &mut anchor.points[ControlPointType::Handle2] {
match elements[handle.kurbo_element_id] {
kurbo::PathEl::CurveTo(handle_position, _, _) | kurbo::PathEl::QuadTo(handle_position, _) => {
anchor.set_point_position(ControlPointType::Handle2 as usize, space_transform(handle_position));
}
_ => (),
}
}
}
}
}
/// Create the kurbo shape that matches the selected viewport shape
fn create_shape_outline_overlay(&self, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayShape {
path: layer_path.clone(),
bez_path: self.bez_path.clone(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), None),
closed: false,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create a single anchor overlay and return its layer id
fn create_anchor_overlay(&self, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayRect {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create a single handle overlay and return its layer id
fn create_handle_overlay(&self, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayEllipse {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create the shape outline overlay and return its layer id
fn create_handle_line_overlay(&self, handle: &Option<VectorControlPoint>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
if handle.is_none() {
return None;
}
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayLine {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), None),
};
responses.push_front(DocumentMessage::Overlays(operation.into()).into());
Some(layer_path)
}
/// Update the positions of the anchor points based on the kurbo path
fn place_shape_outline_overlay(&self, responses: &mut VecDeque<Message>) {
if let Some(overlay_path) = &self.shape_overlay {
responses.push_back(
DocumentMessage::Overlays(
Operation::SetShapePathInViewport {
path: overlay_path.clone(),
bez_path: self.bez_path.clone(),
transform: self.transform.to_cols_array(),
}
.into(),
)
.into(),
);
}
}
/// Update the positions of the anchor points based on the kurbo path
fn place_anchor_overlays(&self, responses: &mut VecDeque<Message>) {
for anchor in &self.anchors {
anchor.place_anchor_overlay(responses);
}
}
/// Update the positions of the handle points and lines based on the kurbo path
fn place_handle_overlays(&self, responses: &mut VecDeque<Message>) {
for anchor in &self.anchors {
anchor.place_handle_overlay(responses);
}
}
/// Remove all of the overlays from the shape
pub fn remove_overlays(&mut self, responses: &mut VecDeque<Message>) {
self.remove_shape_outline_overlay(responses);
self.remove_anchor_overlays(responses);
self.remove_handle_overlays(responses);
}
/// Remove the outline around the shape
pub fn remove_shape_outline_overlay(&mut self, responses: &mut VecDeque<Message>) {
if let Some(overlay_path) = &self.shape_overlay {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_path.clone() }.into()).into());
}
self.shape_overlay = None;
}
/// Remove the all the anchor overlays
pub fn remove_anchor_overlays(&mut self, responses: &mut VecDeque<Message>) {
for anchor in &mut self.anchors {
anchor.remove_anchor_overlay(responses);
}
}
/// Remove the all the anchor overlays
pub fn remove_handle_overlays(&mut self, responses: &mut VecDeque<Message>) {
for anchor in &mut self.anchors {
anchor.remove_handle_overlay(responses);
}
}
/// Eventually we will want to hide the overlays instead of clearing them when selecting a new shape
pub fn set_overlay_visibility(&mut self, visibility: bool, responses: &mut VecDeque<Message>) {
self.set_shape_outline_visiblity(visibility, responses);
self.set_anchors_visiblity(visibility, responses);
self.set_handles_visiblity(visibility, responses);
}
/// Set the visibility of the shape outline
pub fn set_shape_outline_visiblity(&self, visibility: bool, responses: &mut VecDeque<Message>) {
if let Some(overlay_path) = &self.shape_overlay {
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerVisibility {
path: overlay_path.clone(),
visible: visibility,
}
.into(),
)
.into(),
);
}
}
/// Set visibility on all of the anchors in this shape
pub fn set_anchors_visiblity(&self, visibility: bool, responses: &mut VecDeque<Message>) {
for anchor in &self.anchors {
anchor.set_anchor_visiblity(visibility, responses);
}
}
/// Set visibility on all of the handles in this shape
pub fn set_handles_visiblity(&self, visibility: bool, responses: &mut VecDeque<Message>) {
for anchor in &self.anchors {
anchor.set_handle_visiblity(visibility, responses);
}
}
}

View File

@ -17,6 +17,7 @@ pub struct Shape {
pub path: BezPath,
pub style: style::PathStyle,
pub render_index: i32,
pub closed: bool,
}
impl LayerData for Shape {
@ -74,6 +75,7 @@ impl Shape {
path: bez_path,
style,
render_index: 1,
closed,
}
}
@ -102,7 +104,12 @@ impl Shape {
path.close_path();
Self { path, style, render_index: 1 }
Self {
path,
style,
render_index: 1,
closed: true,
}
}
pub fn rectangle(style: PathStyle) -> Self {
@ -110,6 +117,7 @@ impl Shape {
path: kurbo::Rect::new(0., 0., 1., 1.).to_path(0.01),
style,
render_index: 1,
closed: true,
}
}
@ -118,6 +126,7 @@ impl Shape {
path: kurbo::Ellipse::from_rect(kurbo::Rect::new(0., 0., 1., 1.)).to_path(0.01),
style,
render_index: 1,
closed: true,
}
}
@ -126,6 +135,7 @@ impl Shape {
path: kurbo::Line::new((0., 0.), (1., 0.)).to_path(0.01),
style,
render_index: 1,
closed: false,
}
}
@ -138,6 +148,11 @@ impl Shape {
.enumerate()
.for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) });
Self { path, style, render_index: 0 }
Self {
path,
style,
render_index: 0,
closed: false,
}
}
}