Refactor internal shape and reduce reliance on Kurbo (#617)

* Dissolve Points from path

* Add handling for removing the first anchor

* Add function to turn handles into bez_paths

* Created overlay manager, wip

* WIP Refactor of VectorShape / Overlays / ShapeEditor

* WIP stripping vector shape, anchor, point.

* WIP Removed kurbo deps from vector shape, anchor, point

* WIP Further work to make vector shapes / anchors / points more standalone.

* WIP more pruning

* WIP Progress on overlay_renderer

* WIP more overlay_renderer work

* WIP more pruning, cleared warnings

* WIP decided ShapeRenderer wasn't an accurate name, ShapeAdapter now. Error squashing continues.

* WIP squashed more errors, now need to decide if anchors should have unique IDs

* WIP Errors squashed, now to actually make it work.

* WIP Moved vector structs to graphene, beginning to remove bezpath from shape_layer

* Refactoring: disentangle kurbo from apply_affine

* Refactor internal shape and remove reliance on Kurbo (PR #617) - Disentangle Kurbo (#619)

* Refactoring: disentangle kurbo from apply_affine

* Broke boolean operations, refactor in state which compiles

* "fixed" boolean operation refactor related errors

* fixed apply_affine, which would not have applied any type of affine

* Small Cleanup, readability

* Fix issue with overlay styles no longer showing selection state.

* Resolved error with point option

* WIP, figuring out how to have one source of truth for VectorShape. Trying to avoid cloning.

* WIP work on single source of truth vectorshapes

* More steps toward single source of truth VectorShape

* Continued wip on making VectorShapes mutably accessible without cloning

* Wip using paths to reference vectorshapes instead, need to restructure ShapeEditor

* Decided to allow temporary copies of vectorshapes.

* Removed HashSet for selected shape indices

* Added @TrueDoctor's id_storage.rs with some heavy modification. Added it to VectorShape. Isn't yet used for folders.

* Integrated UniqueElements<T> with VectorShape to store VectorAnchors

* Improved storage_id.rs perf and cleaned up it's interface

* Iterator Implementations and fixes (#637)

* Refactoring: disentangle kurbo from apply_affine

* Broke boolean operations, refactor in state which compiles

* "fixed" boolean operation refactor related errors

* fixed apply_affine, which would not have applied any type of affine

* implemented transforms for VectorAnchors
implemented Not for VectorControlPointType

* started adding Vector Shape implementations of shape prototypes

* added several useful implemtations to UniqueElements

* added another implemnation for UniqueElements to make working with iterators easier, fixed vector-shape errors

* package-lock.json

* clean up rebase, added back Layer paths

* added deref implementation for VectorShape

* unnecesary variable

* simplify code by removing levels of indirection

* fixed errors

* merge cleanup

* removed package-lock.json

* Removed .selected from VectorShape, it isn't needed as layers are selected not shapes specifically.

* Removed transform and layer_path from VectorShape

* Auto-saving tentitively working. Work toward Overlay transform issues.

* Overlays properly hiding and caching. Not clearing cache yet and some tool switching issues remain, but progress.

* Putting layers in folders changes their unique ID. This is problematic. Assumed this was not the case.

* Removed need for closed bool, changed VectorShape to a tuple struct.

* WIP Switched to layer paths as opposed to VectorShapes. Next up add messages for changing VectorShapes.

* Added initial messages to edit VectorShape points.

* DeleteSelectedPoints messages implemented, selection isn't working currently though.

* Selection messages arriving in document, but transform is wrong.

* Selection, Deselection working, delete working for first point.

* Working towards moving points again

* Removed extra vec from UniqueElements, attempting to squash ordering bug. Still appears to occur though.

* Delete more stable, clean up, renamed to HandleIn, HandleOut

* Further vec_unique cleanup

* Further cleanup

* Removed Deref / DerefMut from VectorShape

* Document version++, will likely revert before merge into master

* Seleting / deleting handles tentitively working again.

* Version number bump, fixed tests.

* Fixed comment in VecUnique

* Improved VecUnique descriptor comment

* Renamed VecUnique to IdBackedVec to further clarify usage.

* Resolved formatting.

* WIP Fixing dragging points

* Fixed an instance where an OverlayMessage could be sent to the main document incorrectly.

* Deleting all of a shapes points now gracefully deletes the layer instead of crashing.

* Fixed handle configurations that would panic on deletion

* Single anchor dragging restored with multi-dragging next plus handles

* sides.into()

* Handle and Multi-point dragging working

* WIP Handle symmetry working again

* Handle mirroring functional again.

* Cleaned up warnings

* Fixed overlay outline not matching shape

* Git branch fix of compatibility with new master

* Fixed closed shape bug, replaced kurbo ellipse

* Removed unused func, updated comments

* Deleting points can undo, multiple shape selection deletes now working

* Removed AddOverlay* operations

* Partial fix for select drift, added helpers

* Don't snap against dragging points

* Properly cleanup path outline with multiple shapes

* Clear all points in other selected shapes

* Actually don't snap against dragging points

* Fix path tool & add snap angle and break handle

* Fix handle being set to NaN causing render issues

* Fix cached overlays not showing line -> curve

* Add operations for modifying paths

* Remove kurbo from pen tool

* Do not snap against handles when anchor selected

* Fix overlays not being cleaned up on path tool

* Fix handle position after dragging

* Use `Anchor` for text & no kurbo in operations

* Replace kurbo to_svg function

* Ngon no longer center scales by default, still some weird behaviour when holding alt

* Cleanup overlays

* Fix render and bounding box doctests

* Fix fun to_svg error

* Fix compile error

* Some code review

* Remove legacy `SelectPoint` on doubleclick

* Remove font from test document

* Fix the pen tool selection changed

* Reorder imports

Co-authored-by: Dennis <dennis@kobert.dev>
Co-authored-by: Caleb Dennis <caleb.dennis429@gmail.com>
Co-authored-by: caleb <56044292+caleb-ad@users.noreply.github.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: 0hypercube <0hypercube@gmail.com>
Co-authored-by: 0HyperCube <78500760+0HyperCube@users.noreply.github.com>
This commit is contained in:
Oliver Davies
2022-07-05 15:02:18 -07:00
committed by Keavon Chambers
co-authored by Dennis Caleb Dennis caleb Keavon Chambers 0hypercube 0HyperCube
parent 3c2fff4465
commit 58675eb64d
48 changed files with 2461 additions and 1807 deletions
@@ -182,7 +182,7 @@ impl Fsm for ArtboardToolFsmState {
tool_data
.snap_handler
.start_snap(document, document.bounding_boxes(None, Some(tool_data.selected_board.unwrap()), font_cache), snap_x, snap_y);
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
ArtboardToolFsmState::ResizingBounds
} else {
@@ -197,7 +197,7 @@ impl Fsm for ArtboardToolFsmState {
tool_data
.snap_handler
.start_snap(document, document.bounding_boxes(None, Some(intersection[0]), font_cache), true, true);
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
responses.push_back(
PropertiesPanelMessage::SetActiveLayers {
@@ -213,7 +213,7 @@ impl Fsm for ArtboardToolFsmState {
tool_data.selected_board = Some(id);
tool_data.snap_handler.start_snap(document, document.bounding_boxes(None, Some(id), font_cache), true, true);
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
responses.push_back(
ArtboardMessage::AddArtboard {
@@ -191,7 +191,6 @@ impl Fsm for FreehandToolFsmState {
}
(Drawing, DragStop) | (Drawing, Abort) => {
if tool_data.points.len() >= 2 {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(remove_preview(tool_data));
responses.push_back(add_polyline(tool_data, global_tool_data));
responses.push_back(DocumentMessage::CommitTransaction.into());
@@ -167,10 +167,11 @@ impl GradientOverlay {
let fill = if selected { Fill::solid(COLOR_ACCENT) } else { Fill::solid(Color::WHITE) };
let operation = Operation::AddOverlayEllipse {
let operation = Operation::AddEllipse {
path: path.clone(),
transform: DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array(),
style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), fill),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
@@ -185,10 +186,11 @@ impl GradientOverlay {
let translation = start;
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let operation = Operation::AddOverlayLine {
let operation = Operation::AddLine {
path: path.clone(),
transform,
style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
@@ -315,7 +317,7 @@ struct GradientToolData {
pub fn start_snap(snap_handler: &mut SnapHandler, document: &DocumentMessageHandler, font_cache: &FontCache) {
snap_handler.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
snap_handler.add_all_document_handles(document, &[], &[]);
snap_handler.add_all_document_handles(document, &[], &[], &[]);
}
impl Fsm for GradientToolFsmState {
+1 -1
View File
@@ -173,7 +173,7 @@ impl Fsm for LineToolFsmState {
match (self, event) {
(Ready, DragStart) => {
tool_data.snap_handler.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
tool_data.drag_start = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
responses.push_back(DocumentMessage::StartTransaction.into());
+58 -22
View File
@@ -6,9 +6,11 @@ use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::snapping::SnapHandler;
use crate::viewport_tools::tool::{Fsm, SignalToMessageMap, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::viewport_tools::vector_editor::overlay_renderer::OverlayRenderer;
use crate::viewport_tools::vector_editor::shape_editor::ShapeEditor;
use graphene::intersection::Quad;
use graphene::layers::vector::constants::ControlPointType;
use glam::DVec2;
use serde::{Deserialize, Serialize};
@@ -32,6 +34,7 @@ pub enum PathToolMessage {
SelectionChanged,
// Tool-specific messages
Delete,
DragStart {
add_to_selection: Key,
},
@@ -82,8 +85,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PathTool {
use PathToolFsmState::*;
match self.fsm_state {
Ready => actions!(PathToolMessageDiscriminant; DragStart),
Dragging => actions!(PathToolMessageDiscriminant; DragStop, PointerMove),
Ready => actions!(PathToolMessageDiscriminant; DragStart, Delete),
Dragging => actions!(PathToolMessageDiscriminant; DragStop, PointerMove, Delete),
}
}
}
@@ -113,6 +116,7 @@ impl Default for PathToolFsmState {
#[derive(Default)]
struct PathToolData {
shape_editor: ShapeEditor,
overlay_renderer: OverlayRenderer,
snap_handler: SnapHandler,
drag_start_pos: DVec2,
@@ -137,39 +141,56 @@ impl Fsm for PathToolFsmState {
use PathToolMessage::*;
match (self, event) {
// TODO: Capture a tool event instead of doing this?
(_, SelectionChanged) => {
// Remove any residual overlays that might exist on selection change
tool_data.shape_editor.remove_overlays(responses);
// Set the previously selected layers to invisible
for layer_path in document.all_layers() {
tool_data.overlay_renderer.layer_overlay_visibility(&document.graphene_document, layer_path.to_vec(), false, responses);
}
// This currently creates new VectorManipulatorShapes for every shape, which is not ideal
// At least it is only on selection change for now
tool_data.shape_editor.set_shapes_to_modify(document.selected_visible_layers_vector_shapes(responses, font_cache));
// Set the newly targeted layers to visible
let layer_paths = document.selected_visible_layers().map(|layer_path| layer_path.to_vec()).collect();
tool_data.shape_editor.set_selected_layers(layer_paths);
// This can happen in any state (which is why we return self)
self
}
(_, DocumentIsDirty) => {
// Update the VectorManipulatorShapes by reference so they match the kurbo tool_data
for shape in &mut tool_data.shape_editor.shapes_to_modify {
shape.update_shape(document, responses);
// When the document has moved / needs to be redraw, re-render the overlays
// TODO the overlay system should probably receive this message instead of the tool
for layer_path in document.selected_visible_layers() {
tool_data.overlay_renderer.render_vector_shape_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
self
}
// Mouse down
(_, DragStart { add_to_selection }) => {
let add_to_selection = input.keyboard.get(add_to_selection as usize);
let toggle_add_to_selection = input.keyboard.get(add_to_selection as usize);
// Select the first point within the threshold (in pixels)
if tool_data.shape_editor.select_point(input.mouse.position, SELECTION_THRESHOLD, add_to_selection, responses) {
if let Some(mut new_selected) = tool_data
.shape_editor
.select_point(&document.graphene_document, input.mouse.position, SELECTION_THRESHOLD, toggle_add_to_selection, responses)
{
responses.push_back(DocumentMessage::StartTransaction.into());
let ignore_document = tool_data.shape_editor.shapes_to_modify.iter().map(|shape| shape.layer_path.clone()).collect::<Vec<_>>();
let ignore_document = tool_data.shape_editor.selected_layers().clone();
tool_data
.snap_handler
.start_snap(document, document.bounding_boxes(Some(&ignore_document), None, font_cache), true, true);
let include_handles = tool_data.shape_editor.shapes_to_modify.iter().map(|shape| shape.layer_path.as_slice()).collect::<Vec<_>>();
tool_data.snap_handler.add_all_document_handles(document, &include_handles, &[]);
// Do not snap against handles when anchor is selected
let mut extension = Vec::new();
for &(path, id, point_type) in new_selected.iter() {
if point_type == ControlPointType::Anchor {
extension.push((path, id, ControlPointType::InHandle));
extension.push((path, id, ControlPointType::OutHandle));
}
}
new_selected.extend(extension);
let include_handles = tool_data.shape_editor.selected_layers_ref();
tool_data.snap_handler.add_all_document_handles(document, &include_handles, &[], &new_selected);
tool_data.drag_start_pos = input.mouse.position;
Dragging
@@ -182,7 +203,7 @@ impl Fsm for PathToolFsmState {
.graphene_document
.intersects_quad_root(Quad::from_box([input.mouse.position - selection_size, input.mouse.position + selection_size]), font_cache);
if !intersection.is_empty() {
if add_to_selection {
if toggle_add_to_selection {
responses.push_back(DocumentMessage::AddSelectedLayers { additional_layers: intersection }.into());
} else {
responses.push_back(
@@ -194,7 +215,7 @@ impl Fsm for PathToolFsmState {
}
} else {
// Clear the previous selection if we didn't find anything
if !input.keyboard.get(add_to_selection as usize) {
if !input.keyboard.get(toggle_add_to_selection as usize) {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
}
}
@@ -215,7 +236,7 @@ impl Fsm for PathToolFsmState {
tool_data.alt_debounce = alt_pressed;
// Only on alt down
if alt_pressed {
tool_data.shape_editor.toggle_selected_mirror_angle();
tool_data.shape_editor.toggle_handle_mirroring_on_selected(true, false, responses);
}
}
@@ -223,12 +244,13 @@ impl Fsm for PathToolFsmState {
let shift_pressed = input.keyboard.get(shift_mirror_distance as usize);
if shift_pressed != tool_data.shift_debounce {
tool_data.shift_debounce = shift_pressed;
tool_data.shape_editor.toggle_selected_mirror_distance();
tool_data.shape_editor.toggle_handle_mirroring_on_selected(false, true, responses);
}
// Move the selected points by the mouse position
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
tool_data.shape_editor.move_selected_points(snapped_position - tool_data.drag_start_pos, true, responses);
tool_data.shape_editor.move_selected_points(snapped_position - tool_data.drag_start_pos, snapped_position, responses);
tool_data.drag_start_pos = snapped_position;
Dragging
}
// Mouse up
@@ -236,8 +258,22 @@ impl Fsm for PathToolFsmState {
tool_data.snap_handler.cleanup(responses);
Ready
}
// Delete key
(_, Delete) => {
// Delete the selected points and clean up overlays
responses.push_back(DocumentMessage::StartTransaction.into());
tool_data.shape_editor.delete_selected_points(responses);
responses.push_back(SelectionChanged.into());
for layer_path in document.all_layers() {
tool_data.overlay_renderer.clear_vector_shape_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
Ready
}
(_, Abort) => {
tool_data.shape_editor.remove_overlays(responses);
// TODO Tell overlay manager to remove the overlays
for layer_path in document.all_layers() {
tool_data.overlay_renderer.clear_vector_shape_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
Ready
}
(
+202 -152
View File
@@ -1,4 +1,4 @@
use crate::consts::CREATE_CURVE_THRESHOLD;
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
@@ -8,15 +8,15 @@ use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::snapping::SnapHandler;
use crate::viewport_tools::tool::{Fsm, SignalToMessageMap, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::viewport_tools::vector_editor::constants::ControlPointType;
use crate::viewport_tools::vector_editor::shape_editor::ShapeEditor;
use crate::viewport_tools::vector_editor::vector_shape::VectorShape;
use crate::viewport_tools::vector_editor::overlay_renderer::OverlayRenderer;
use graphene::layers::style;
use graphene::layers::vector::constants::ControlPointType;
use graphene::layers::vector::vector_anchor::VectorAnchor;
use graphene::layers::vector::vector_shape::VectorShape;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use kurbo::{PathEl, Point};
use serde::{Deserialize, Serialize};
#[derive(Default)]
@@ -45,12 +45,17 @@ pub enum PenToolMessage {
DocumentIsDirty,
#[remain::unsorted]
Abort,
#[remain::unsorted]
SelectionChanged,
// Tool-specific messages
Confirm,
DragStart,
DragStop,
PointerMove,
PointerMove {
snap_angle: Key,
break_handle: Key,
},
Undo,
UpdateOptions(PenOptionsUpdate),
}
@@ -58,7 +63,8 @@ pub enum PenToolMessage {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PenToolFsmState {
Ready,
Drawing,
DraggingHandle,
PlacingAnchor,
}
#[remain::sorted]
@@ -124,11 +130,9 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PenTool {
}
fn actions(&self) -> ActionList {
use PenToolFsmState::*;
match self.fsm_state {
Ready => actions!(PenToolMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
Drawing => actions!(PenToolMessageDiscriminant; DragStart, DragStop, PointerMove, Confirm, Abort),
PenToolFsmState::Ready => actions!(PenToolMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; DragStart, DragStop, PointerMove, Confirm, Abort),
}
}
}
@@ -138,7 +142,7 @@ impl ToolTransition for PenTool {
SignalToMessageMap {
document_dirty: Some(PenToolMessage::DocumentIsDirty.into()),
tool_abort: Some(PenToolMessage::Abort.into()),
selection_changed: None,
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
}
}
}
@@ -152,11 +156,8 @@ impl Default for PenToolFsmState {
struct PenToolData {
weight: f64,
path: Option<Vec<LayerId>>,
curve_shape: VectorShape,
bez_path: Vec<PathEl>,
overlay_renderer: OverlayRenderer,
snap_handler: SnapHandler,
shape_editor: ShapeEditor,
drag_start_position: DVec2,
}
impl Fsm for PenToolFsmState {
@@ -171,112 +172,181 @@ impl Fsm for PenToolFsmState {
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
use PenToolFsmState::*;
use PenToolMessage::*;
let transform = document.graphene_document.root.transform;
let transform = tool_data.path.as_ref().and_then(|path| document.graphene_document.multiply_transforms(path).ok()).unwrap_or_default();
if let ToolMessage::Pen(event) = event {
match (self, event) {
(_, DocumentIsDirty) => {
tool_data.shape_editor.update_shapes(document, responses);
(_, PenToolMessage::DocumentIsDirty) => {
// When the document has moved / needs to be redraw, re-render the overlays
// TODO the overlay system should probably receive this message instead of the tool
for layer_path in document.selected_visible_layers() {
tool_data.overlay_renderer.render_vector_shape_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
self
}
(Ready, DragStart) => {
(_, PenToolMessage::SelectionChanged) => {
// Set the previously selected layers to invisible
for layer_path in document.all_layers() {
tool_data.overlay_renderer.layer_overlay_visibility(&document.graphene_document, layer_path.to_vec(), false, responses);
}
self
}
(PenToolFsmState::Ready, PenToolMessage::DragStart) => {
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
// Create a new layer and prep snap system
tool_data.path = Some(document.get_path_for_new_layer());
tool_data.snap_handler.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
// Get the position and set properties
let transform = tool_data
.path
.as_ref()
.and_then(|path| document.graphene_document.multiply_transforms(&path[..path.len() - 1]).ok())
.unwrap_or_default();
let start_position = transform.inverse().transform_point2(snapped_position);
tool_data.weight = tool_options.line_weight;
// Create the initial shape with a `bez_path` (only contains a moveto initially)
if let Some(layer_path) = &tool_data.path {
tool_data.bez_path = start_bez_path(start_position);
responses.push_back(
Operation::AddShape {
path: layer_path.clone(),
transform: transform.to_cols_array(),
transform: DAffine2::IDENTITY.to_cols_array(),
insert_index: -1,
bez_path: tool_data.bez_path.clone().into_iter().collect(),
vector_path: Default::default(),
style: style::PathStyle::new(Some(style::Stroke::new(global_tool_data.primary_color, tool_data.weight)), style::Fill::None),
closed: false,
}
.into(),
);
responses.push_back(add_anchor(&tool_data.path, VectorAnchor::new(start_position)));
}
add_to_curve(tool_data, input, transform, document, responses);
Drawing
PenToolFsmState::DraggingHandle
}
(Drawing, DragStart) => {
tool_data.drag_start_position = input.mouse.position;
add_to_curve(tool_data, input, transform, document, responses);
Drawing
}
(Drawing, DragStop) => {
// Deselect everything (this means we are no longer dragging the handle)
tool_data.shape_editor.deselect_all(responses);
// If the drag does not exceed the threshold, then replace the curve with a line
if tool_data.drag_start_position.distance(input.mouse.position) < CREATE_CURVE_THRESHOLD {
// Modify the second to last element (as we have an unplaced element tracing to the cursor as the last element)
let replace_index = tool_data.bez_path.len() - 2;
let line_from_curve = convert_curve_to_line(tool_data.bez_path[replace_index]);
replace_path_element(tool_data, transform, replace_index, line_from_curve, responses);
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => PenToolFsmState::DraggingHandle,
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => {
// Add new point onto path
if let Some(layer_path) = &tool_data.path {
if let Some(vector_anchor) = get_vector_shape(layer_path, document).and_then(|shape| shape.anchors().last()) {
if let Some(anchor) = &vector_anchor.points[ControlPointType::OutHandle] {
responses.push_back(add_anchor(&tool_data.path, VectorAnchor::new(anchor.position)));
}
}
}
// Reselect the last point
if let Some(last_anchor) = tool_data.shape_editor.select_last_anchor() {
last_anchor.select_point(ControlPointType::Anchor as usize, true, responses);
PenToolFsmState::PlacingAnchor
}
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle }) => {
if let Some(layer_path) = &tool_data.path {
let mouse = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
let mut pos = transform.inverse().transform_point2(mouse);
if let Some(((&id, anchor), _previous)) = get_vector_shape(layer_path, document).and_then(last_2_anchors) {
if let Some(anchor) = anchor.points[ControlPointType::Anchor as usize].as_ref() {
pos = compute_snapped_angle(input, snap_angle, pos, anchor.position);
}
// Update points on current segment (to show preview of new handle)
let msg = Operation::MoveVectorPoint {
layer_path: layer_path.clone(),
id,
control_type: ControlPointType::OutHandle,
position: pos.into(),
};
responses.push_back(msg.into());
// Mirror handle of last segement
if !input.keyboard.get(break_handle as usize) && get_vector_shape(layer_path, document).map(|shape| shape.anchors().len() > 1).unwrap_or_default() {
if let Some(anchor) = anchor.points[ControlPointType::Anchor as usize].as_ref() {
pos = anchor.position - (pos - anchor.position);
}
let msg = Operation::MoveVectorPoint {
layer_path: layer_path.clone(),
id,
control_type: ControlPointType::InHandle,
position: pos.into(),
};
responses.push_back(msg.into());
}
}
}
// Move the newly selected points to the cursor
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
tool_data.shape_editor.move_selected_points(snapped_position, false, responses);
Drawing
self
}
(Drawing, PointerMove) => {
// Move selected points
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
tool_data.shape_editor.move_selected_points(snapped_position, false, responses);
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, .. }) => {
if let Some(layer_path) = &tool_data.path {
let mouse = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
let mut pos = transform.inverse().transform_point2(mouse);
Drawing
if let Some(((&id, _anchor), previous)) = get_vector_shape(layer_path, document).and_then(last_2_anchors) {
if let Some(relative) = previous.as_ref().and_then(|(_, anchor)| anchor.points[ControlPointType::Anchor as usize].as_ref()) {
pos = compute_snapped_angle(input, snap_angle, pos, relative.position);
}
for control_type in [ControlPointType::Anchor, ControlPointType::InHandle, ControlPointType::OutHandle] {
let msg = Operation::MoveVectorPoint {
layer_path: layer_path.clone(),
id,
control_type,
position: pos.into(),
};
responses.push_back(msg.into());
}
}
}
self
}
(Drawing, Confirm) | (Drawing, Abort) => {
// Cleanup, we are either canceling or finished drawing
if tool_data.bez_path.len() >= 2 {
// Remove the last segment
remove_from_curve(tool_data);
if let Some(layer_path) = &tool_data.path {
responses.push_back(apply_bez_path(layer_path.clone(), tool_data.bez_path.clone(), transform));
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Abort | PenToolMessage::Confirm) => {
// Abort or commit the transaction to the undo history
if let Some(layer_path) = tool_data.path.as_ref() {
if let Some(vector_shape) = (get_vector_shape(layer_path, document)).filter(|vector_shape| vector_shape.anchors().len() > 1) {
if let Some(((&(mut id), mut anchor), previous)) = last_2_anchors(vector_shape) {
// Remove the unplaced anchor if in anchor placing mode
if self == PenToolFsmState::PlacingAnchor {
let layer_path = layer_path.clone();
let op = Operation::RemoveVectorAnchor { layer_path, id };
responses.push_back(op.into());
if let Some((&new_id, new_anchor)) = previous {
id = new_id;
anchor = new_anchor;
}
}
// Remove the out handle if in dragging handle mode
let op = Operation::MoveVectorPoint {
layer_path: layer_path.clone(),
id,
control_type: ControlPointType::OutHandle,
position: anchor.points[ControlPointType::Anchor as usize].as_ref().unwrap().position.into(),
};
responses.push_back(op.into());
}
}
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(DocumentMessage::CommitTransaction.into());
} else {
responses.push_back(DocumentMessage::AbortTransaction.into());
}
tool_data.shape_editor.remove_overlays(responses);
tool_data.shape_editor.clear_shapes_to_modify();
// Clean up overlays
for layer_path in document.all_layers() {
tool_data.overlay_renderer.clear_vector_shape_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
tool_data.path = None;
tool_data.snap_handler.cleanup(responses);
Ready
PenToolFsmState::Ready
}
(_, Abort) => {
tool_data.shape_editor.remove_overlays(responses);
tool_data.shape_editor.clear_shapes_to_modify();
Ready
(_, PenToolMessage::Abort) => {
// Clean up overlays
for layer_path in document.all_layers() {
tool_data.overlay_renderer.clear_vector_shape_overlays(&document.graphene_document, layer_path.to_vec(), responses);
}
self
}
_ => self,
}
@@ -293,11 +363,29 @@ impl Fsm for PenToolFsmState {
label: String::from("Draw Path"),
plus: false,
}])]),
PenToolFsmState::Drawing => HintData(vec![
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => HintData(vec![
HintGroup(vec![HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Add Handle"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Lmb),
label: String::from("Extend Path"),
label: String::from("Add Control Point"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
mouse: None,
label: String::from("Snap 15°"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Break Handle"),
plus: false,
}]),
HintGroup(vec![HintInfo {
@@ -317,91 +405,53 @@ impl Fsm for PenToolFsmState {
}
}
/// Add to the curve and select the second anchor of the last point and the newly added anchor point
fn add_to_curve(tool_data: &mut PenToolData, input: &InputPreprocessorMessageHandler, transform: DAffine2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
// Refresh tool_data's representation of the path
update_path_representation(tool_data);
// TODO: Expand `pos` name below to the full word (position?)
/// Snap the angle of the line from relative to pos if the key is pressed
fn compute_snapped_angle(input: &InputPreprocessorMessageHandler, key: Key, pos: DVec2, relative: DVec2) -> DVec2 {
if input.keyboard.get(key as usize) {
let delta = relative - pos;
// Setup our position params
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
let position = transform.inverse().transform_point2(snapped_position);
let length = delta.length();
let mut angle = -delta.angle_between(DVec2::X);
// Add a curve to the path
if let Some(layer_path) = &tool_data.path {
// Push curve onto path
let point = Point { x: position.x, y: position.y };
tool_data.bez_path.push(PathEl::CurveTo(point, point, point));
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
responses.push_back(apply_bez_path(layer_path.clone(), tool_data.bez_path.clone(), transform));
let rotated = DVec2::new(length * angle.cos(), length * angle.sin());
relative - rotated
} else {
pos
}
}
// Clear previous overlays
tool_data.shape_editor.remove_overlays(responses);
// Create a new `shape` from the updated `bez_path`
let bez_path = tool_data.bez_path.clone().into_iter().collect();
tool_data.curve_shape = VectorShape::new(layer_path.to_vec(), transform, &bez_path, false, responses);
tool_data.shape_editor.set_shapes_to_modify(vec![tool_data.curve_shape.clone()]);
// Select the second to last `PathEl`'s handle
tool_data.shape_editor.set_shape_selected(0);
let handle_element = tool_data.shape_editor.select_nth_anchor(0, -2);
handle_element.select_point(ControlPointType::Handle2 as usize, true, responses);
// Select the last `PathEl`'s anchor point
if let Some(last_anchor) = tool_data.shape_editor.select_last_anchor() {
last_anchor.select_point(ControlPointType::Anchor as usize, true, responses);
/// Pushes an anchor to the current layer via an [Operation]
fn add_anchor(layer_path: &Option<Vec<LayerId>>, anchor: VectorAnchor) -> Message {
if let Some(layer_path) = layer_path {
Operation::PushVectorAnchor {
layer_path: layer_path.clone(),
anchor,
}
tool_data.shape_editor.set_selected_mirror_options(true, true);
.into()
} else {
Message::NoOp
}
}
/// Replace a `PathEl` with another inside of `bez_path` by index
fn replace_path_element(tool_data: &mut PenToolData, transform: DAffine2, replace_index: usize, replacement: PathEl, responses: &mut VecDeque<Message>) {
tool_data.bez_path[replace_index] = replacement;
if let Some(layer_path) = &tool_data.path {
responses.push_back(apply_bez_path(layer_path.clone(), tool_data.bez_path.clone(), transform));
}
/// Gets the currently editing [VectorShape]
fn get_vector_shape<'a>(layer_path: &'a [LayerId], document: &'a DocumentMessageHandler) -> Option<&'a VectorShape> {
document.graphene_document.layer(layer_path).ok().and_then(|layer| layer.as_vector_shape())
}
/// Remove a curve from the end of the `bez_path`
fn remove_from_curve(tool_data: &mut PenToolData) {
// Refresh tool_data's representation of the path
update_path_representation(tool_data);
tool_data.bez_path.pop();
}
type AnchorRef<'a> = (&'a u64, &'a VectorAnchor);
/// 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,
})]
}
/// Convert curve `PathEl` into a line `PathEl`
fn convert_curve_to_line(curve: PathEl) -> PathEl {
match curve {
PathEl::CurveTo(_, _, p) => PathEl::LineTo(p),
_ => PathEl::MoveTo(Point::ZERO),
}
}
/// Update tool_data's version of `bez_path` to match `ShapeEditor`'s version
fn update_path_representation(tool_data: &mut PenToolData) {
// TODO Update ShapeEditor to provide similar functionality
// We need to make sure we have the most up-to-date bez_path
if !tool_data.shape_editor.shapes_to_modify.is_empty() {
// Hacky way of saving the curve changes
tool_data.bez_path = tool_data.shape_editor.shapes_to_modify[0].bez_path.elements().to_vec();
}
}
/// 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()
/// Gets the last 2 [VectorAnchor] on the currently editing layer along with its id
fn last_2_anchors(vector_shape: &VectorShape) -> Option<(AnchorRef, Option<AnchorRef>)> {
vector_shape.anchors().enumerate().last().map(|last| {
(
last,
(vector_shape.anchors().len() > 1)
.then(|| vector_shape.anchors().enumerate().nth(vector_shape.anchors().len() - 2))
.flatten(),
)
})
}
@@ -431,7 +431,7 @@ impl Fsm for SelectToolFsmState {
tool_data.snap_handler.start_snap(document, document.bounding_boxes(Some(&selected), None, font_cache), snap_x, snap_y);
tool_data
.snap_handler
.add_all_document_handles(document, &[], &selected.iter().map(|x| x.as_slice()).collect::<Vec<_>>());
.add_all_document_handles(document, &[], &selected.iter().map(|x| x.as_slice()).collect::<Vec<_>>(), &[]);
tool_data.layers_dragging = selected;
@@ -7,10 +7,10 @@ use graphene::intersection::Quad;
use graphene::layers::layer_info::LayerDataType;
use graphene::layers::style::{self, Fill, Stroke};
use graphene::layers::text_layer::FontCache;
use graphene::layers::vector::vector_shape::VectorShape;
use graphene::{LayerId, Operation};
use glam::{DAffine2, DVec2};
use kurbo::{BezPath, Shape};
use std::collections::VecDeque;
/// Manages the overlay used by the select tool for outlining selected shapes and when hovering over a non selected shape.
@@ -33,13 +33,14 @@ impl PathOutline {
// Get layer data
let document_layer = document.graphene_document.layer(&document_layer_path).ok()?;
// TODO Purge this area of BezPath and Kurbo
// Get the bezpath from the shape or text
let path = match &document_layer.data {
LayerDataType::Shape(shape) => Some(shape.path.clone()),
LayerDataType::Text(text) => Some(text.to_bez_path_nonmut(font_cache)),
let vector_path = match &document_layer.data {
LayerDataType::Shape(layer_shape) => Some(layer_shape.shape.clone()),
LayerDataType::Text(text) => Some(text.to_vector_path_nonmut(font_cache)),
_ => document_layer
.aabounding_box_for_transform(DAffine2::IDENTITY, font_cache)
.map(|bounds| kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y).to_path(0.)),
.map(|[p1, p2]| VectorShape::new_rect(p1, p2)),
}?;
// Generate a new overlay layer if necessary
@@ -47,11 +48,12 @@ impl PathOutline {
Some(path) => path,
None => {
let overlay_path = vec![generate_uuid()];
let operation = Operation::AddOverlayShape {
let operation = Operation::AddShape {
path: overlay_path.clone(),
bez_path: BezPath::new(),
vector_path: Default::default(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
closed: false,
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
@@ -61,10 +63,7 @@ impl PathOutline {
};
// Update the shape bezpath
let operation = Operation::SetShapePath {
path: overlay.clone(),
bez_path: path,
};
let operation = Operation::SetShapePath { path: overlay.clone(), vector_path };
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
// Update the transform to match the document
@@ -21,7 +21,7 @@ impl Resize {
/// Starts a resize, assigning the snap targets and snapping the starting position.
pub fn start(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, mouse_position: DVec2, font_cache: &FontCache) {
self.snap_handler.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
self.snap_handler.add_all_document_handles(document, &[], &[]);
self.snap_handler.add_all_document_handles(document, &[], &[], &[]);
self.drag_start = self.snap_handler.snap_position(responses, document, mouse_position);
}
@@ -140,10 +140,11 @@ impl SelectedEdges {
pub fn add_bounding_box(responses: &mut Vec<Message>) -> Vec<LayerId> {
let path = vec![generate_uuid()];
let operation = Operation::AddOverlayRect {
let operation = Operation::AddRect {
path: path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
insert_index: -1,
};
responses.push(DocumentMessage::Overlays(operation.into()).into());
@@ -158,10 +159,11 @@ fn add_transform_handles(responses: &mut Vec<Message>) -> [Vec<LayerId>; 8] {
for item in &mut transform_handle_paths {
let current_path = vec![generate_uuid()];
let operation = Operation::AddOverlayRect {
let operation = Operation::AddRect {
path: current_path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.push(DocumentMessage::Overlays(operation.into()).into());
@@ -174,7 +174,7 @@ impl Fsm for SplineToolFsmState {
tool_data.path = Some(document.get_path_for_new_layer());
tool_data.snap_handler.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
let pos = transform.inverse().transform_point2(snapped_position);
@@ -216,7 +216,6 @@ impl Fsm for SplineToolFsmState {
}
(Drawing, Confirm) | (Drawing, Abort) => {
if tool_data.points.len() >= 2 {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(remove_preview(tool_data));
responses.push_back(add_spline(tool_data, global_tool_data, false));
responses.push_back(DocumentMessage::CommitTransaction.into());
+6 -11
View File
@@ -14,7 +14,6 @@ use graphene::layers::text_layer::FontCache;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use kurbo::Shape;
use serde::{Deserialize, Serialize};
#[derive(Default)]
@@ -217,10 +216,11 @@ fn resize_overlays(overlays: &mut Vec<Vec<LayerId>>, responses: &mut VecDeque<Me
let path = vec![generate_uuid()];
overlays.push(path.clone());
let operation = Operation::AddOverlayRect {
let operation = Operation::AddRect {
path,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
}
@@ -423,19 +423,14 @@ impl Fsm for TextToolFsmState {
(Editing, UpdateBounds { new_text }) => {
resize_overlays(&mut tool_data.overlays, responses, 1);
let text = document.graphene_document.layer(&tool_data.path).unwrap().as_text().unwrap();
let mut path = text.bounding_box(&new_text, text.load_face(font_cache)).to_path(0.1);
let quad = text.bounding_box(&new_text, text.load_face(font_cache));
fn glam_to_kurbo(transform: DAffine2) -> kurbo::Affine {
kurbo::Affine::new(transform.to_cols_array())
}
path.apply_affine(glam_to_kurbo(document.graphene_document.multiply_transforms(&tool_data.path).unwrap()));
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
let transformed_quad = document.graphene_document.multiply_transforms(&tool_data.path).unwrap() * quad;
let bounds = transformed_quad.bounding_box();
let operation = Operation::SetLayerTransformInViewport {
path: tool_data.overlays[0].clone(),
transform: transform_from_box(DVec2::new(x0, y0), DVec2::new(x1, y1)),
transform: transform_from_box(bounds[0], bounds[1]),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());