Add Pen Tool (#536)
* First steps toward pen tool, similar to spline tool currently. * Broken WIP * Progress, but still glitchy * Improvements, a little farther * Pen tool functioning as expected * Merged master * Fixed commit bug and overlay flashing * Reordered import statements TODO: Resolve issue with last segment losing its handle position on confirm
This commit is contained in:
parent
f8c2be83dd
commit
d084775d81
|
|
@ -200,7 +200,7 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy
|
||||||
ToolType::BlurSharpen => None, // Some(BlurSharpenMessage::DocumentIsDirty.into()),
|
ToolType::BlurSharpen => None, // Some(BlurSharpenMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Relight => None, // Some(RelightMessage::DocumentIsDirty.into()),
|
ToolType::Relight => None, // Some(RelightMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Path => Some(PathMessage::DocumentIsDirty.into()),
|
ToolType::Path => Some(PathMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Pen => None, // Some(PenMessage::DocumentIsDirty.into()),
|
ToolType::Pen => Some(PenMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Freehand => None, // Some(FreehandMessage::DocumentIsDirty.into()),
|
ToolType::Freehand => None, // Some(FreehandMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Spline => None, // Some(SplineMessage::DocumentIsDirty.into()),
|
ToolType::Spline => None, // Some(SplineMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Line => None, // Some(LineMessage::DocumentIsDirty.into()),
|
ToolType::Line => None, // Some(LineMessage::DocumentIsDirty.into()),
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,8 @@ impl Default for PathToolFsmState {
|
||||||
struct PathToolData {
|
struct PathToolData {
|
||||||
shape_editor: ShapeEditor,
|
shape_editor: ShapeEditor,
|
||||||
snap_handler: SnapHandler,
|
snap_handler: SnapHandler,
|
||||||
|
|
||||||
|
drag_start_pos: DVec2,
|
||||||
alt_debounce: bool,
|
alt_debounce: bool,
|
||||||
shift_debounce: bool,
|
shift_debounce: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -151,6 +153,7 @@ impl Fsm for PathToolFsmState {
|
||||||
.map(|point| point.position)
|
.map(|point| point.position)
|
||||||
.collect();
|
.collect();
|
||||||
data.snap_handler.add_snap_points(document, snap_points);
|
data.snap_handler.add_snap_points(document, snap_points);
|
||||||
|
data.drag_start_pos = input.mouse.position;
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
// We didn't find a point nearby, so consider selecting the nearest shape instead
|
// We didn't find a point nearby, so consider selecting the nearest shape instead
|
||||||
|
|
@ -207,7 +210,7 @@ impl Fsm for PathToolFsmState {
|
||||||
|
|
||||||
// Move the selected points by the mouse position
|
// 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);
|
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);
|
data.shape_editor.move_selected_points(snapped_position - data.drag_start_pos, true, responses);
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
// Mouse up
|
// Mouse up
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::consts::DRAG_THRESHOLD;
|
|
||||||
use crate::document::DocumentMessageHandler;
|
use crate::document::DocumentMessageHandler;
|
||||||
use crate::frontend::utility_types::MouseCursorIcon;
|
use crate::frontend::utility_types::MouseCursorIcon;
|
||||||
use crate::input::keyboard::{Key, MouseMotion};
|
use crate::input::keyboard::{Key, MouseMotion};
|
||||||
|
|
@ -8,11 +7,14 @@ use crate::message_prelude::*;
|
||||||
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
use crate::viewport_tools::snapping::SnapHandler;
|
use crate::viewport_tools::snapping::SnapHandler;
|
||||||
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||||
|
use crate::viewport_tools::vector_editor::shape_editor::ShapeEditor;
|
||||||
|
use crate::viewport_tools::vector_editor::vector_shape::VectorShape;
|
||||||
|
|
||||||
use graphene::layers::style;
|
use graphene::layers::style;
|
||||||
use graphene::Operation;
|
use graphene::Operation;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
use kurbo::{PathEl, Point};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -38,6 +40,8 @@ impl Default for PenOptions {
|
||||||
pub enum PenMessage {
|
pub enum PenMessage {
|
||||||
// Standard messages
|
// Standard messages
|
||||||
#[remain::unsorted]
|
#[remain::unsorted]
|
||||||
|
DocumentIsDirty,
|
||||||
|
#[remain::unsorted]
|
||||||
Abort,
|
Abort,
|
||||||
|
|
||||||
// Tool-specific messages
|
// Tool-specific messages
|
||||||
|
|
@ -111,7 +115,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
|
||||||
|
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
|
Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
|
||||||
Drawing => actions!(PenMessageDiscriminant; DragStop, PointerMove, Confirm, Abort),
|
Drawing => actions!(PenMessageDiscriminant; DragStart, DragStop, PointerMove, Confirm, Abort),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,11 +127,12 @@ impl Default for PenToolFsmState {
|
||||||
}
|
}
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
struct PenToolData {
|
struct PenToolData {
|
||||||
points: Vec<DVec2>,
|
|
||||||
next_point: DVec2,
|
|
||||||
weight: u32,
|
weight: u32,
|
||||||
path: Option<Vec<LayerId>>,
|
path: Option<Vec<LayerId>>,
|
||||||
|
curve_shape: VectorShape,
|
||||||
|
bez_path: Vec<PathEl>,
|
||||||
snap_handler: SnapHandler,
|
snap_handler: SnapHandler,
|
||||||
|
shape_editor: ShapeEditor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fsm for PenToolFsmState {
|
impl Fsm for PenToolFsmState {
|
||||||
|
|
@ -151,67 +156,92 @@ impl Fsm for PenToolFsmState {
|
||||||
|
|
||||||
if let ToolMessage::Pen(event) = event {
|
if let ToolMessage::Pen(event) = event {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
|
(_, DocumentIsDirty) => {
|
||||||
|
data.shape_editor.update_shapes(document, responses);
|
||||||
|
self
|
||||||
|
}
|
||||||
(Ready, DragStart) => {
|
(Ready, DragStart) => {
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
data.path = Some(document.get_path_for_new_layer());
|
|
||||||
|
|
||||||
|
// Create a new layer and prep snap system
|
||||||
|
data.path = Some(document.get_path_for_new_layer());
|
||||||
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
||||||
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
||||||
|
|
||||||
let pos = transform.inverse().transform_point2(snapped_position);
|
// Get the position and set properties
|
||||||
|
let start_position = transform.inverse().transform_point2(snapped_position);
|
||||||
data.points.push(pos);
|
|
||||||
data.next_point = pos;
|
|
||||||
|
|
||||||
data.weight = tool_options.line_weight;
|
data.weight = tool_options.line_weight;
|
||||||
|
|
||||||
responses.push_back(add_polyline(data, tool_data, true));
|
// Create the initial shape with a bez_path (only contains a moveto initially)
|
||||||
|
if let Some(layer_path) = &data.path {
|
||||||
|
data.bez_path = start_bez_path(start_position);
|
||||||
|
responses.push_back(
|
||||||
|
Operation::AddShape {
|
||||||
|
path: layer_path.clone(),
|
||||||
|
transform: transform.to_cols_array(),
|
||||||
|
insert_index: -1,
|
||||||
|
bez_path: data.bez_path.clone().into_iter().collect(),
|
||||||
|
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), None),
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_to_curve(data, input, transform, document, responses);
|
||||||
|
Drawing
|
||||||
|
}
|
||||||
|
(Drawing, DragStart) => {
|
||||||
|
add_to_curve(data, input, transform, document, responses);
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(Drawing, DragStop) => {
|
(Drawing, DragStop) => {
|
||||||
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
// Deselect everything (this means we are no longer dragging the handle)
|
||||||
let pos = transform.inverse().transform_point2(snapped_position);
|
data.shape_editor.deselect_all(responses);
|
||||||
|
|
||||||
if let Some(last_pos) = data.points.last() {
|
// Reselect the last point
|
||||||
if last_pos.distance(pos) > DRAG_THRESHOLD {
|
if let Some(last_anchor) = data.shape_editor.select_last_anchor() {
|
||||||
data.points.push(pos);
|
last_anchor.select_point(0, true, responses);
|
||||||
data.next_point = pos;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
responses.push_back(remove_preview(data));
|
|
||||||
responses.push_back(add_polyline(data, tool_data, true));
|
|
||||||
|
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(Drawing, PointerMove) => {
|
(Drawing, PointerMove) => {
|
||||||
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
||||||
let pos = transform.inverse().transform_point2(snapped_position);
|
//data.shape_editor.update_shapes(document, responses);
|
||||||
data.next_point = pos;
|
data.shape_editor.move_selected_points(snapped_position, false, responses);
|
||||||
|
|
||||||
responses.push_back(remove_preview(data));
|
|
||||||
responses.push_back(add_polyline(data, tool_data, true));
|
|
||||||
|
|
||||||
Drawing
|
Drawing
|
||||||
}
|
}
|
||||||
(Drawing, Confirm) | (Drawing, Abort) => {
|
(Drawing, Confirm) | (Drawing, Abort) => {
|
||||||
if data.points.len() >= 2 {
|
// Add a curve to the path
|
||||||
|
if let Some(layer_path) = &data.path {
|
||||||
|
remove_curve_from_end(&mut data.bez_path);
|
||||||
|
responses.push_back(apply_bez_path(layer_path.clone(), data.bez_path.clone(), transform));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup, we are either canceling or finished drawing
|
||||||
|
if data.bez_path.len() >= 2 {
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
responses.push_back(remove_preview(data));
|
|
||||||
responses.push_back(add_polyline(data, tool_data, false));
|
|
||||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||||
} else {
|
} else {
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.shape_editor.remove_overlays(responses);
|
||||||
|
data.shape_editor.clear_shapes_to_modify();
|
||||||
|
|
||||||
data.path = None;
|
data.path = None;
|
||||||
data.points.clear();
|
|
||||||
data.snap_handler.cleanup(responses);
|
data.snap_handler.cleanup(responses);
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
(_, Abort) => {
|
||||||
|
data.shape_editor.remove_overlays(responses);
|
||||||
|
data.shape_editor.clear_shapes_to_modify();
|
||||||
|
Ready
|
||||||
|
}
|
||||||
_ => self,
|
_ => self,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -251,22 +281,70 @@ impl Fsm for PenToolFsmState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_preview(data: &PenToolData) -> Message {
|
// Add to the curve and select the second anchor of the last point and the newly added anchor point
|
||||||
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into()
|
fn add_to_curve(data: &mut PenToolData, input: &InputPreprocessorMessageHandler, transform: DAffine2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
}
|
// We need to make sure we have the most up-to-date bez_path
|
||||||
|
// Would like to remove this hack eventually
|
||||||
fn add_polyline(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> Message {
|
if !data.shape_editor.shapes_to_modify.is_empty() {
|
||||||
let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.x, p.y)).collect();
|
// Hacky way of saving the curve changes
|
||||||
if show_preview {
|
data.bez_path = data.shape_editor.shapes_to_modify[0].bez_path.elements().to_vec();
|
||||||
points.push((data.next_point.x, data.next_point.y))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Operation::AddPolyline {
|
// Setup our position params
|
||||||
path: data.path.clone().unwrap(),
|
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
||||||
insert_index: -1,
|
let position = transform.inverse().transform_point2(snapped_position);
|
||||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
|
||||||
points,
|
// Add a curve to the path
|
||||||
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), None),
|
if let Some(layer_path) = &data.path {
|
||||||
|
add_curve_to_end(position, &mut data.bez_path);
|
||||||
|
responses.push_back(apply_bez_path(layer_path.clone(), data.bez_path.clone(), transform));
|
||||||
|
|
||||||
|
// Clear previous overlays
|
||||||
|
data.shape_editor.remove_overlays(responses);
|
||||||
|
|
||||||
|
// Create a new shape from the updated bez_path
|
||||||
|
let bez_path = data.bez_path.clone().into_iter().collect();
|
||||||
|
data.curve_shape = VectorShape::new(layer_path.to_vec(), transform, &bez_path, false, responses);
|
||||||
|
data.shape_editor.set_shapes_to_modify(vec![data.curve_shape.clone()]);
|
||||||
|
|
||||||
|
// Select the second to last segment's handle
|
||||||
|
data.shape_editor.set_shape_selected(0);
|
||||||
|
let handle_element = data.shape_editor.select_nth_anchor(0, -2);
|
||||||
|
handle_element.select_point(2, true, responses);
|
||||||
|
|
||||||
|
// Select the last segment's anchor point
|
||||||
|
if let Some(last_anchor) = data.shape_editor.select_last_anchor() {
|
||||||
|
last_anchor.select_point(0, true, responses);
|
||||||
|
}
|
||||||
|
data.shape_editor.set_selected_mirror_options(true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the initial moveto for the bez_path
|
||||||
|
fn start_bez_path(start_position: DVec2) -> Vec<PathEl> {
|
||||||
|
vec![PathEl::MoveTo(Point {
|
||||||
|
x: start_position.x,
|
||||||
|
y: start_position.y,
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a curve to the bez_path
|
||||||
|
fn add_curve_to_end(point: DVec2, bez_path: &mut Vec<PathEl>) {
|
||||||
|
let point = Point { x: point.x, y: point.y };
|
||||||
|
bez_path.push(PathEl::CurveTo(point, point, point));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a curve to the bez_path
|
||||||
|
fn remove_curve_from_end(bez_path: &mut Vec<PathEl>) {
|
||||||
|
bez_path.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the bez_path to the shape in the viewport
|
||||||
|
fn apply_bez_path(layer_path: Vec<LayerId>, bez_path: Vec<PathEl>, transform: DAffine2) -> Message {
|
||||||
|
Operation::SetShapePathInViewport {
|
||||||
|
path: layer_path,
|
||||||
|
bez_path: bez_path.into_iter().collect(),
|
||||||
|
transform: transform.to_cols_array(),
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,12 @@ Overview:
|
||||||
|
|
||||||
use super::vector_shape::VectorShape;
|
use super::vector_shape::VectorShape;
|
||||||
use super::{constants::MINIMUM_MIRROR_THRESHOLD, vector_anchor::VectorAnchor, vector_control_point::VectorControlPoint};
|
use super::{constants::MINIMUM_MIRROR_THRESHOLD, vector_anchor::VectorAnchor, vector_control_point::VectorControlPoint};
|
||||||
|
use crate::document::DocumentMessageHandler;
|
||||||
use crate::message_prelude::Message;
|
use crate::message_prelude::Message;
|
||||||
use glam::DVec2;
|
|
||||||
|
use graphene::layers::layer_info::LayerDataType;
|
||||||
|
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
use std::collections::{HashSet, VecDeque};
|
use std::collections::{HashSet, VecDeque};
|
||||||
|
|
||||||
/// ShapeEditor is the container for all of the selected kurbo paths that are
|
/// ShapeEditor is the container for all of the selected kurbo paths that are
|
||||||
|
|
@ -30,8 +34,6 @@ pub struct ShapeEditor {
|
||||||
pub shapes_to_modify: Vec<VectorShape>,
|
pub shapes_to_modify: Vec<VectorShape>,
|
||||||
// Index of the shape that contained the most recent selected point
|
// Index of the shape that contained the most recent selected point
|
||||||
pub selected_shape_indices: HashSet<usize>,
|
pub selected_shape_indices: HashSet<usize>,
|
||||||
// The initial drag position of the mouse on drag start
|
|
||||||
pub drag_start_position: DVec2,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShapeEditor {
|
impl ShapeEditor {
|
||||||
|
|
@ -46,7 +48,7 @@ impl ShapeEditor {
|
||||||
log::trace!("Selecting: shape {} / anchor {} / point {}", shape_index, anchor_index, point_index);
|
log::trace!("Selecting: shape {} / anchor {} / point {}", shape_index, anchor_index, point_index);
|
||||||
|
|
||||||
// Add this shape to the selection
|
// Add this shape to the selection
|
||||||
self.add_selected_shape(shape_index);
|
self.set_shape_selected(shape_index);
|
||||||
|
|
||||||
// If the point we're selecting has already been selected
|
// 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
|
// we can assume this point exists.. since we did just click on it hense the unwrap
|
||||||
|
|
@ -65,12 +67,7 @@ impl ShapeEditor {
|
||||||
|
|
||||||
// Add which anchor and point was selected
|
// Add which anchor and point was selected
|
||||||
let selected_anchor = selected_shape.select_anchor(anchor_index);
|
let selected_anchor = selected_shape.select_anchor(anchor_index);
|
||||||
let selected_point = selected_anchor.select_point(point_index, should_select, responses);
|
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
|
// 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);
|
selected_anchor.set_mirroring((selected_anchor.angle_between_handles().abs() - std::f64::consts::PI).abs() < MINIMUM_MIRROR_THRESHOLD);
|
||||||
|
|
@ -111,11 +108,40 @@ impl ShapeEditor {
|
||||||
self.shapes_to_modify = selected_shapes;
|
self.shapes_to_modify = selected_shapes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a single shape to be modifed by providing a layer path
|
||||||
|
pub fn set_shapes_to_modify_from_layer(&mut self, layer_path: &[u64], transform: DAffine2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
// Setup the shape editor
|
||||||
|
let layer = document.graphene_document.layer(layer_path);
|
||||||
|
if let Ok(layer) = layer {
|
||||||
|
let shape = match &layer.data {
|
||||||
|
LayerDataType::Shape(shape) => Some(VectorShape::new(layer_path.to_vec(), transform, &shape.path, shape.closed, responses)),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
self.set_shapes_to_modify(vec![shape.expect("The layer provided didn't have a shape we could use.")]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all of the shapes we can modify
|
||||||
|
pub fn clear_shapes_to_modify(&mut self) {
|
||||||
|
self.shapes_to_modify.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a shape to the hashset of shapes we consider for selection
|
/// Add a shape to the hashset of shapes we consider for selection
|
||||||
pub fn add_selected_shape(&mut self, shape_index: usize) {
|
pub fn set_shape_selected(&mut self, shape_index: usize) {
|
||||||
self.selected_shape_indices.insert(shape_index);
|
self.selected_shape_indices.insert(shape_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the currently shapes we consider for selection
|
||||||
|
pub fn update_shapes(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
if self.shapes_to_modify.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for shape in self.shapes_to_modify.iter_mut() {
|
||||||
|
shape.update_shape(document, responses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Provide the shapes that the currently selected points are a part of
|
/// Provide the shapes that the currently selected points are a part of
|
||||||
pub fn selected_shapes(&self) -> impl Iterator<Item = &VectorShape> {
|
pub fn selected_shapes(&self) -> impl Iterator<Item = &VectorShape> {
|
||||||
self.shapes_to_modify
|
self.shapes_to_modify
|
||||||
|
|
@ -142,6 +168,31 @@ impl ShapeEditor {
|
||||||
self.selected_shapes_mut().flat_map(|shape| shape.selected_anchors_mut())
|
self.selected_shapes_mut().flat_map(|shape| shape.selected_anchors_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A mutable iterator of all the anchors, regardless of selection
|
||||||
|
pub fn anchors_mut(&mut self) -> impl Iterator<Item = &mut VectorAnchor> {
|
||||||
|
self.shapes_to_modify.iter_mut().flat_map(|shape| shape.anchors_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the last anchor in this shape
|
||||||
|
pub fn select_last_anchor(&mut self) -> Option<&mut VectorAnchor> {
|
||||||
|
if let Some(last) = self.shapes_to_modify.last_mut() {
|
||||||
|
return Some(last.select_last_anchor());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the Nth anchor of the shape, negative numbers index from the end
|
||||||
|
pub fn select_nth_anchor(&mut self, shape_index: usize, anchor_index: i32) -> &mut VectorAnchor {
|
||||||
|
let shape = &mut self.shapes_to_modify[shape_index];
|
||||||
|
if anchor_index < 0 {
|
||||||
|
let anchor_index = shape.anchors.len() - ((-anchor_index) as usize);
|
||||||
|
shape.select_anchor(anchor_index)
|
||||||
|
} else {
|
||||||
|
let anchor_index = anchor_index as usize;
|
||||||
|
shape.select_anchor(anchor_index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Provide the currently selected points by reference
|
/// Provide the currently selected points by reference
|
||||||
pub fn selected_points(&self) -> impl Iterator<Item = &VectorControlPoint> {
|
pub fn selected_points(&self) -> impl Iterator<Item = &VectorControlPoint> {
|
||||||
self.selected_shapes().flat_map(|shape| shape.selected_anchors()).flat_map(|anchors| anchors.selected_points())
|
self.selected_shapes().flat_map(|shape| shape.selected_anchors()).flat_map(|anchors| anchors.selected_points())
|
||||||
|
|
@ -155,10 +206,9 @@ impl ShapeEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move the selected points by dragging the moue
|
/// Move the selected points by dragging the moue
|
||||||
pub fn move_selected_points(&mut self, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
|
pub fn move_selected_points(&mut self, target: DVec2, relative: bool, responses: &mut VecDeque<Message>) {
|
||||||
let drag_start_position = self.drag_start_position;
|
|
||||||
for shape in self.selected_shapes_mut() {
|
for shape in self.selected_shapes_mut() {
|
||||||
shape.move_selected(mouse_position - drag_start_position, responses);
|
shape.move_selected(target, relative, responses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,6 +219,13 @@ impl ShapeEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_selected_mirror_options(&mut self, mirror_angle: bool, mirror_distance: bool) {
|
||||||
|
for anchor in self.selected_anchors_mut() {
|
||||||
|
anchor.handle_mirror_angle = mirror_angle;
|
||||||
|
anchor.handle_mirror_distance = mirror_distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle if the handles should mirror distance across the anchor position
|
/// Toggle if the handles should mirror distance across the anchor position
|
||||||
pub fn toggle_selected_mirror_distance(&mut self) {
|
pub fn toggle_selected_mirror_distance(&mut self) {
|
||||||
for anchor in self.selected_anchors_mut() {
|
for anchor in self.selected_anchors_mut() {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,3 @@
|
||||||
use glam::{DAffine2, DVec2};
|
|
||||||
use graphene::{LayerId, Operation};
|
|
||||||
use kurbo::{PathEl, Point, Vec2};
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE,
|
consts::VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE,
|
||||||
message_prelude::{DocumentMessage, Message},
|
message_prelude::{DocumentMessage, Message},
|
||||||
|
|
@ -13,6 +8,12 @@ use super::{
|
||||||
vector_control_point::VectorControlPoint,
|
vector_control_point::VectorControlPoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use graphene::{LayerId, Operation};
|
||||||
|
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
|
use kurbo::{PathEl, Point, Vec2};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
/// VectorAnchor is used to represent an anchor point on the path that can be moved.
|
/// VectorAnchor is used to represent an anchor point on the path that can be moved.
|
||||||
/// It contains 0-2 handles that are optionally displayed.
|
/// It contains 0-2 handles that are optionally displayed.
|
||||||
#[derive(PartialEq, Clone, Debug, Default)]
|
#[derive(PartialEq, Clone, Debug, Default)]
|
||||||
|
|
@ -49,7 +50,7 @@ impl VectorAnchor {
|
||||||
|
|
||||||
// TODO Cleanup the internals of this function
|
// TODO Cleanup the internals of this function
|
||||||
/// Move the selected points by the provided delta
|
/// 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) {
|
pub fn move_selected_points(&mut self, translation: DVec2, relative: bool, 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 {
|
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 {
|
if !selected || !mirror_angle {
|
||||||
return original;
|
return original;
|
||||||
|
|
@ -65,9 +66,17 @@ impl VectorAnchor {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let offset = |point: Point| -> Point {
|
||||||
|
if relative {
|
||||||
|
let relative = transform.inverse().transform_vector2(translation);
|
||||||
|
point + Vec2::new(relative.x, relative.y)
|
||||||
|
} else {
|
||||||
|
let absolute = transform.inverse().transform_point2(translation);
|
||||||
|
Point { x: absolute.x, y: absolute.y }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for selected_point in self.selected_points() {
|
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 h1_selected = ControlPointType::Handle1 == selected_point.manipulator_type;
|
||||||
let h2_selected = ControlPointType::Handle2 == selected_point.manipulator_type;
|
let h2_selected = ControlPointType::Handle2 == selected_point.manipulator_type;
|
||||||
let dragging_anchor = !(h1_selected || h2_selected);
|
let dragging_anchor = !(h1_selected || h2_selected);
|
||||||
|
|
@ -78,10 +87,10 @@ impl VectorAnchor {
|
||||||
let handle1_exists_and_selected = self.points[ControlPointType::Handle1].is_some() && self.points[ControlPointType::Handle1].as_ref().unwrap().is_selected;
|
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
|
// Move the anchor point and handle on the same path element
|
||||||
let selected_element = match &path_elements[selected_point.kurbo_element_id] {
|
let selected_element = match &path_elements[selected_point.kurbo_element_id] {
|
||||||
PathEl::MoveTo(p) => PathEl::MoveTo(*p + delta),
|
PathEl::MoveTo(p) => PathEl::MoveTo(offset(*p)),
|
||||||
PathEl::LineTo(p) => PathEl::LineTo(*p + delta),
|
PathEl::LineTo(p) => PathEl::LineTo(offset(*p)),
|
||||||
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, *p + delta),
|
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, offset(*p)),
|
||||||
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(*a1, if handle1_exists_and_selected { *a2 } else { *a2 + delta }, *p + delta),
|
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(*a1, if handle1_exists_and_selected { *a2 } else { offset(*a2) }, offset(*p)),
|
||||||
PathEl::ClosePath => PathEl::ClosePath,
|
PathEl::ClosePath => PathEl::ClosePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -92,7 +101,7 @@ impl VectorAnchor {
|
||||||
PathEl::MoveTo(p) => PathEl::MoveTo(*p),
|
PathEl::MoveTo(p) => PathEl::MoveTo(*p),
|
||||||
PathEl::LineTo(p) => PathEl::LineTo(*p),
|
PathEl::LineTo(p) => PathEl::LineTo(*p),
|
||||||
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, *p),
|
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, *p),
|
||||||
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(*a1 + delta, *a2, *p),
|
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(offset(*a1), *a2, *p),
|
||||||
PathEl::ClosePath => PathEl::ClosePath,
|
PathEl::ClosePath => PathEl::ClosePath,
|
||||||
};
|
};
|
||||||
path_elements[handle.kurbo_element_id] = neighbor;
|
path_elements[handle.kurbo_element_id] = neighbor;
|
||||||
|
|
@ -102,10 +111,10 @@ impl VectorAnchor {
|
||||||
if let Some(close_id) = self.close_element_id {
|
if let Some(close_id) = self.close_element_id {
|
||||||
// Move the invisible point that can be caused by MoveTo / closing the path
|
// Move the invisible point that can be caused by MoveTo / closing the path
|
||||||
path_elements[close_id] = match &path_elements[close_id] {
|
path_elements[close_id] = match &path_elements[close_id] {
|
||||||
PathEl::MoveTo(p) => PathEl::MoveTo(*p + delta),
|
PathEl::MoveTo(p) => PathEl::MoveTo(offset(*p)),
|
||||||
PathEl::LineTo(p) => PathEl::LineTo(*p + delta),
|
PathEl::LineTo(p) => PathEl::LineTo(offset(*p)),
|
||||||
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, *p + delta),
|
PathEl::QuadTo(a1, p) => PathEl::QuadTo(*a1, offset(*p)),
|
||||||
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(*a1, *a2 + delta, *p + delta),
|
PathEl::CurveTo(a1, a2, p) => PathEl::CurveTo(*a1, offset(*a2), offset(*p)),
|
||||||
PathEl::ClosePath => PathEl::ClosePath,
|
PathEl::ClosePath => PathEl::ClosePath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -121,10 +130,10 @@ impl VectorAnchor {
|
||||||
let (selected_element, anchor, selected_handle) = match &path_elements[selected_point.kurbo_element_id] {
|
let (selected_element, anchor, selected_handle) = match &path_elements[selected_point.kurbo_element_id] {
|
||||||
PathEl::MoveTo(p) => (PathEl::MoveTo(*p), *p, *p),
|
PathEl::MoveTo(p) => (PathEl::MoveTo(*p), *p, *p),
|
||||||
PathEl::LineTo(p) => (PathEl::LineTo(*p), *p, *p),
|
PathEl::LineTo(p) => (PathEl::LineTo(*p), *p, *p),
|
||||||
PathEl::QuadTo(a1, p) => (PathEl::QuadTo(*a1 + delta, *p), *p, *a1 + delta),
|
PathEl::QuadTo(a1, p) => (PathEl::QuadTo(offset(*a1), *p), *p, offset(*a1)),
|
||||||
PathEl::CurveTo(a1, a2, p) => {
|
PathEl::CurveTo(a1, a2, p) => {
|
||||||
let a1_point = if h2_selected { *a1 + delta } else { *a1 };
|
let a1_point = if h2_selected { offset(*a1) } else { *a1 };
|
||||||
let a2_point = if h1_selected { *a2 + delta } else { *a2 };
|
let a2_point = if h1_selected { offset(*a2) } else { *a2 };
|
||||||
(PathEl::CurveTo(a1_point, a2_point, *p), *p, if h1_selected { a2_point } else { a1_point })
|
(PathEl::CurveTo(a1_point, a2_point, *p), *p, if h1_selected { a2_point } else { a1_point })
|
||||||
}
|
}
|
||||||
PathEl::ClosePath => (PathEl::ClosePath, Point::ZERO, Point::ZERO),
|
PathEl::ClosePath => (PathEl::ClosePath, Point::ZERO, Point::ZERO),
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
use glam::DVec2;
|
use super::constants::ControlPointType;
|
||||||
use graphene::{
|
|
||||||
color::Color,
|
|
||||||
layers::style::{Fill, PathStyle, Stroke},
|
|
||||||
LayerId, Operation,
|
|
||||||
};
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::COLOR_ACCENT,
|
consts::COLOR_ACCENT,
|
||||||
message_prelude::{DocumentMessage, Message},
|
message_prelude::{DocumentMessage, Message},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::constants::ControlPointType;
|
use graphene::{
|
||||||
|
color::Color,
|
||||||
|
layers::style::{Fill, PathStyle, Stroke},
|
||||||
|
LayerId, Operation,
|
||||||
|
};
|
||||||
|
|
||||||
|
use glam::DVec2;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
/// VectorControlPoint represents any grabbable point, anchor or handle
|
/// VectorControlPoint represents any grabbable point, anchor or handle
|
||||||
#[derive(PartialEq, Clone, Debug)]
|
#[derive(PartialEq, Clone, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
use glam::{DAffine2, DVec2};
|
use super::{constants::ControlPointType, vector_anchor::VectorAnchor, vector_control_point::VectorControlPoint};
|
||||||
|
use crate::{
|
||||||
|
consts::COLOR_ACCENT,
|
||||||
|
document::DocumentMessageHandler,
|
||||||
|
message_prelude::{generate_uuid, DocumentMessage, Message},
|
||||||
|
};
|
||||||
|
|
||||||
use graphene::{
|
use graphene::{
|
||||||
color::Color,
|
color::Color,
|
||||||
layers::{
|
layers::{
|
||||||
|
|
@ -7,18 +13,12 @@ use graphene::{
|
||||||
},
|
},
|
||||||
LayerId, Operation,
|
LayerId, Operation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
use kurbo::{BezPath, PathEl};
|
use kurbo::{BezPath, PathEl};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::collections::VecDeque;
|
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
|
/// 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
|
/// For each kurbo path we keep a VectorShape which contains the handles and anchors for that path
|
||||||
#[derive(PartialEq, Clone, Debug, Default)]
|
#[derive(PartialEq, Clone, Debug, Default)]
|
||||||
|
|
@ -74,6 +74,13 @@ impl VectorShape {
|
||||||
&mut self.anchors[anchor_index]
|
&mut self.anchors[anchor_index]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The last anchor in the shape thus far
|
||||||
|
pub fn select_last_anchor(&mut self) -> &mut VectorAnchor {
|
||||||
|
let last_index = self.anchors.len() - 1;
|
||||||
|
self.selected_anchor_indices.insert(last_index);
|
||||||
|
&mut self.anchors[last_index]
|
||||||
|
}
|
||||||
|
|
||||||
/// Deselect an anchor
|
/// Deselect an anchor
|
||||||
pub fn deselect_anchor(&mut self, anchor_index: usize, responses: &mut VecDeque<Message>) {
|
pub fn deselect_anchor(&mut self, anchor_index: usize, responses: &mut VecDeque<Message>) {
|
||||||
self.anchors[anchor_index].clear_selected_points(responses);
|
self.anchors[anchor_index].clear_selected_points(responses);
|
||||||
|
|
@ -112,14 +119,19 @@ impl VectorShape {
|
||||||
.filter_map(|(index, anchor)| if self.selected_anchor_indices.contains(&index) { Some(anchor) } else { None })
|
.filter_map(|(index, anchor)| if self.selected_anchor_indices.contains(&index) { Some(anchor) } else { None })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a mutable interator of the anchors regardless of selection
|
||||||
|
pub fn anchors_mut(&mut self) -> impl Iterator<Item = &mut VectorAnchor> {
|
||||||
|
self.anchors.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
/// Move the selected point based on mouse input, if this is a handle we can control if we are mirroring or not
|
/// 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
|
/// 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>) {
|
pub fn move_selected(&mut self, target: DVec2, relative: bool, responses: &mut VecDeque<Message>) {
|
||||||
let transform = &self.transform.clone();
|
let transform = &self.transform.clone();
|
||||||
let mut edited_bez_path = self.elements.clone();
|
let mut edited_bez_path = self.elements.clone();
|
||||||
|
|
||||||
for selected_anchor in self.selected_anchors_mut() {
|
for selected_anchor in self.selected_anchors_mut() {
|
||||||
selected_anchor.move_selected_points(position_delta, &mut edited_bez_path, transform);
|
selected_anchor.move_selected_points(target, relative, &mut edited_bez_path, transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We've made our changes to the shape, submit them
|
// We've made our changes to the shape, submit them
|
||||||
|
|
|
||||||
|
|
@ -508,6 +508,18 @@ impl Document {
|
||||||
|
|
||||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat())
|
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat())
|
||||||
}
|
}
|
||||||
|
Operation::AddShape {
|
||||||
|
path,
|
||||||
|
transform,
|
||||||
|
insert_index,
|
||||||
|
style,
|
||||||
|
bez_path,
|
||||||
|
closed,
|
||||||
|
} => {
|
||||||
|
let shape = Shape::from_bez_path(bez_path.clone(), *style, *closed);
|
||||||
|
self.set_layer(path, Layer::new(LayerDataType::Shape(shape), *transform), *insert_index)?;
|
||||||
|
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }]].concat())
|
||||||
|
}
|
||||||
Operation::AddPolyline {
|
Operation::AddPolyline {
|
||||||
path,
|
path,
|
||||||
insert_index,
|
insert_index,
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,14 @@ pub enum Operation {
|
||||||
style: style::PathStyle,
|
style: style::PathStyle,
|
||||||
closed: bool,
|
closed: bool,
|
||||||
},
|
},
|
||||||
|
AddShape {
|
||||||
|
path: Vec<LayerId>,
|
||||||
|
transform: [f64; 6],
|
||||||
|
insert_index: isize,
|
||||||
|
bez_path: kurbo::BezPath,
|
||||||
|
style: style::PathStyle,
|
||||||
|
closed: bool,
|
||||||
|
},
|
||||||
DeleteLayer {
|
DeleteLayer {
|
||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue