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:
parent
bd844aaf94
commit
108b8be595
|
|
@ -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.;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ pub mod tool;
|
|||
pub mod tool_message;
|
||||
pub mod tool_message_handler;
|
||||
pub mod tools;
|
||||
pub mod vector_editor;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ pub enum ToolMessage {
|
|||
},
|
||||
DocumentIsDirty,
|
||||
ResetColors,
|
||||
SelectionChanged,
|
||||
SelectPrimaryColor {
|
||||
color: Color,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
pub mod constants;
|
||||
pub mod shape_editor;
|
||||
pub mod vector_anchor;
|
||||
pub mod vector_control_point;
|
||||
pub mod vector_shape;
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue