mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-28 17:18:12 +08:00
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:
committed by
Keavon Chambers
parent
bd844aaf94
commit
108b8be595
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user