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
parent 6042b32a86
commit 20cfd5f600
48 changed files with 2461 additions and 1807 deletions

View File

@@ -460,8 +460,23 @@ mod test {
}
#[test]
/// If this test is failing take a look at `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`, it may need to be updated.
/// This test will fail when you make changes to the underlying serialization format for a document.
fn check_if_graphite_file_version_upgrade_is_needed() {
use crate::layout::widgets::{LayoutGroup, TextLabel, Widget};
let print_problem_to_terminal_on_failure = |value: &String| {
println!();
println!("-------------------------------------------------");
println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file!");
println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`");
println!("After bumping this version number, please replace the `graphite-test-document.graphite` with a valid file [saved from the editor].");
println!("DisplayDialogError details:");
println!();
println!("Description: {}", value);
println!("-------------------------------------------------");
println!();
panic!()
};
init_logger();
set_uuid_seed(0);
@@ -473,20 +488,11 @@ mod test {
});
for response in responses {
// Check for the existence of the file format incompatibility warning dialog after opening the test file
if let FrontendMessage::UpdateDialogDetails { layout_target: _, layout } = response {
if let LayoutGroup::Row { widgets } = &layout[0] {
if let Widget::TextLabel(TextLabel { value, .. }) = &widgets[0].widget {
println!();
println!("-------------------------------------------------");
println!("Failed test due to receiving a DisplayDialogError while loading the Graphite sample file!");
println!("This is most likely caused by forgetting to bump the `GRAPHITE_DOCUMENT_VERSION` in `editor/src/consts.rs`");
println!("Once bumping this version number please replace the `graphite-test-document.graphite` with a valid file.");
println!("DisplayDialogError details:");
println!();
println!("Description: {}", value);
println!("-------------------------------------------------");
println!();
panic!()
print_problem_to_terminal_on_failure(value);
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -70,5 +70,5 @@ pub const DEFAULT_FONT_FAMILY: &str = "Merriweather";
pub const DEFAULT_FONT_STYLE: &str = "Normal (400)";
// Document
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.8"; // Remember to save a simple document and replace the test file at: editor\src\communication\graphite-test-document.graphite
pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.10"; // Remember to save a simple document and replace the test file at: editor\src\communication\graphite-test-document.graphite
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f32 = 1.05;

View File

@@ -1,8 +1,8 @@
use crate::document::PortfolioMessageHandler;
use crate::layout::{layout_message::LayoutTarget, widgets::PropertyHolder};
use crate::message_prelude::*;
use super::*;
use crate::document::PortfolioMessageHandler;
use crate::layout::layout_message::LayoutTarget;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
#[derive(Debug, Default, Clone)]
pub struct DialogMessageHandler {

View File

@@ -1,4 +1,5 @@
use crate::{layout::widgets::*, message_prelude::FrontendMessage};
use crate::layout::widgets::*;
use crate::message_prelude::FrontendMessage;
use std::fmt::Write;

View File

@@ -1,4 +1,5 @@
use crate::{layout::widgets::*, message_prelude::FrontendMessage};
use crate::layout::widgets::*;
use crate::message_prelude::FrontendMessage;
/// A dialog to notify users of a non-fatal error.
pub struct Error {

View File

@@ -53,7 +53,9 @@ pub enum DocumentMessage {
layer_path: Vec<LayerId>,
},
DeleteSelectedLayers,
DeleteSelectedVectorPoints,
DeselectAllLayers,
DeselectAllVectorPoints,
DirtyRenderDocument,
DirtyRenderDocumentInOutlineView,
DocumentHistoryBackward,
@@ -81,6 +83,11 @@ pub enum DocumentMessage {
insert_index: isize,
reverse_index: bool,
},
MoveSelectedVectorPoints {
layer_path: Vec<LayerId>,
delta: (f64, f64),
absolute_position: (f64, f64),
},
NudgeSelectedLayers {
delta_x: f64,
delta_y: f64,
@@ -144,6 +151,11 @@ pub enum DocumentMessage {
ToggleLayerVisibility {
layer_path: Vec<LayerId>,
},
ToggleSelectedHandleMirroring {
layer_path: Vec<LayerId>,
toggle_distance: bool,
toggle_angle: bool,
},
Undo,
UngroupLayers {
folder_path: Vec<LayerId>,

View File

@@ -14,7 +14,6 @@ use crate::layout::widgets::{
SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout,
};
use crate::message_prelude::*;
use crate::viewport_tools::vector_editor::vector_shape::VectorShape;
use crate::EditorError;
use graphene::color::Color;
@@ -24,6 +23,7 @@ use graphene::layers::folder_layer::FolderLayer;
use graphene::layers::layer_info::{LayerDataType, LayerDataTypeDiscriminant};
use graphene::layers::style::{Fill, RenderData, ViewMode};
use graphene::layers::text_layer::{Font, FontCache};
use graphene::layers::vector::vector_shape::VectorShape;
use graphene::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation};
use glam::{DAffine2, DVec2};
@@ -158,28 +158,6 @@ impl DocumentMessageHandler {
self.artboard_message_handler.artboards_graphene_document.bounding_box_and_transform(path, font_cache).unwrap_or(None)
}
/// Create a new vector shape representation with the underlying kurbo data, VectorManipulatorShape
pub fn selected_visible_layers_vector_shapes(&self, responses: &mut VecDeque<Message>, font_cache: &FontCache) -> Vec<VectorShape> {
let shapes = self.selected_layers().filter_map(|path_to_shape| {
let viewport_transform = self.graphene_document.generate_transform_relative_to_viewport(path_to_shape).ok()?;
let layer = self.graphene_document.layer(path_to_shape);
match &layer {
Ok(layer) if layer.visible => {}
_ => return None,
};
// TODO: Create VectorManipulatorShape when creating a kurbo shape as a stopgap, rather than on each new selection
match &layer.ok()?.data {
LayerDataType::Shape(shape) => Some(VectorShape::new(path_to_shape.to_vec(), viewport_transform, &shape.path, shape.closed, responses)),
LayerDataType::Text(text) => Some(VectorShape::new(path_to_shape.to_vec(), viewport_transform, &text.to_bez_path_nonmut(font_cache), true, responses)),
_ => None,
}
});
shapes.collect::<Vec<VectorShape>>()
}
pub fn selected_layers(&self) -> impl Iterator<Item = &[LayerId]> {
self.layer_metadata.iter().filter_map(|(path, data)| data.selected.then(|| path.as_slice()))
}
@@ -223,6 +201,22 @@ impl DocumentMessageHandler {
})
}
/// Returns a copy of all the currently selected VectorShapes.
pub fn selected_vector_shapes(&self) -> Vec<VectorShape> {
self.selected_visible_layers()
.flat_map(|layer| self.graphene_document.layer(layer))
.flat_map(|layer| layer.as_vector_shape_copy())
.collect::<Vec<VectorShape>>()
}
/// Returns references to all the currently selected VectorShapes.
pub fn selected_vector_shapes_ref(&self) -> Vec<&VectorShape> {
self.selected_visible_layers()
.flat_map(|layer| self.graphene_document.layer(layer))
.flat_map(|layer| layer.as_vector_shape())
.collect::<Vec<&VectorShape>>()
}
/// Returns the bounding boxes for all visible layers and artboards, optionally excluding any paths.
pub fn bounding_boxes<'a>(&'a self, ignore_document: Option<&'a Vec<Vec<LayerId>>>, ignore_artboard: Option<LayerId>, font_cache: &'a FontCache) -> impl Iterator<Item = [DVec2; 2]> + 'a {
self.visible_layers()
@@ -998,14 +992,28 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
responses.push_front(BroadcastSignal::SelectionChanged.into());
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
}
DeleteSelectedVectorPoints => {
responses.push_back(StartTransaction.into());
responses.push_front(
DocumentOperation::DeleteSelectedVectorPoints {
layer_paths: self.selected_layers_without_children().iter().map(|path| path.to_vec()).collect(),
}
.into(),
);
}
DeselectAllLayers => {
responses.push_front(SetSelectedLayers { replacement_selected_layers: vec![] }.into());
self.layer_range_selection_reference.clear();
}
DeselectAllVectorPoints => {
for layer_path in self.selected_layers_without_children() {
responses.push_back(DocumentOperation::DeselectAllVectorPoints { layer_path: layer_path.to_vec() }.into());
}
}
DirtyRenderDocument => {
// Mark all non-overlay caches as dirty
GrapheneDocument::mark_children_as_dirty(&mut self.graphene_document.root);
responses.push_back(DocumentMessage::RenderDocument.into());
}
DirtyRenderDocumentInOutlineView => {
@@ -1163,6 +1171,12 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
.into(),
);
}
MoveSelectedVectorPoints { layer_path, delta, absolute_position } => {
self.backup(responses);
if let Ok(_layer) = self.graphene_document.layer(&layer_path) {
responses.push_back(DocumentOperation::MoveSelectedVectorPoints { layer_path, delta, absolute_position }.into());
}
}
NudgeSelectedLayers { delta_x, delta_y } => {
self.backup(responses);
for path in self.selected_layers().map(|path| path.to_vec()) {
@@ -1462,6 +1476,20 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
responses.push_back(DocumentOperation::ToggleLayerVisibility { path: layer_path }.into());
responses.push_back(BroadcastSignal::DocumentIsDirty.into());
}
ToggleSelectedHandleMirroring {
layer_path,
toggle_distance,
toggle_angle,
} => {
responses.push_back(
DocumentOperation::SetSelectedHandleMirroring {
layer_path,
toggle_distance,
toggle_angle,
}
.into(),
);
}
Undo => {
responses.push_back(BroadcastSignal::ToolAbort.into());
responses.push_back(DocumentHistoryBackward.into());

View File

@@ -1,11 +1,12 @@
use super::clipboards::{CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use super::{DocumentMessageHandler, MenuBarMessageHandler};
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
use crate::dialog;
use crate::frontend::utility_types::FrontendDocumentDetails;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::layout_message::LayoutTarget;
use crate::layout::widgets::PropertyHolder;
use crate::{dialog, message_prelude::*};
use crate::message_prelude::*;
use graphene::layers::layer_info::LayerDataTypeDiscriminant;
use graphene::layers::text_layer::{Font, FontCache};

View File

@@ -103,9 +103,11 @@ impl Default for Mapping {
// Path
entry! {action=PathToolMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb},
entry! {action=PathToolMessage::PointerMove { alt_mirror_angle: KeyAlt, shift_mirror_distance: KeyShift }, message=InputMapperMessage::PointerMove},
entry! {action=PathToolMessage::Delete, key_down=KeyDelete},
entry! {action=PathToolMessage::Delete, key_down=KeyBackspace},
entry! {action=PathToolMessage::DragStop, key_up=Lmb},
// Pen
entry! {action=PenToolMessage::PointerMove, message=InputMapperMessage::PointerMove},
entry! {action=PenToolMessage::PointerMove { snap_angle: KeyControl, break_handle: KeyShift }, message=InputMapperMessage::PointerMove},
entry! {action=PenToolMessage::DragStart, key_down=Lmb},
entry! {action=PenToolMessage::DragStop, key_up=Lmb},
entry! {action=PenToolMessage::Confirm, key_down=Rmb},

View File

@@ -1,10 +1,10 @@
use std::rc::Rc;
use super::layout_message::LayoutTarget;
use crate::{input::keyboard::Key, message_prelude::*};
use crate::input::keyboard::Key;
use crate::message_prelude::*;
use derivative::*;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
pub trait PropertyHolder {
fn properties(&self) -> Layout {

View File

@@ -7,6 +7,7 @@ use crate::message_prelude::*;
use graphene::layers::layer_info::{Layer, LayerDataType};
use graphene::layers::style::{self, Stroke};
use graphene::layers::vector::constants::ControlPointType;
use graphene::{LayerId, Operation};
use glam::{DAffine2, DVec2};
@@ -52,16 +53,18 @@ impl SnapOverlays {
responses.push_back(
DocumentMessage::Overlays(
if is_axis {
Operation::AddOverlayLine {
Operation::AddLine {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), style::Fill::None),
insert_index: -1,
}
} else {
Operation::AddOverlayEllipse {
Operation::AddEllipse {
path: layer_path.clone(),
transform,
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
insert_index: -1,
}
}
.into(),
@@ -246,43 +249,44 @@ impl SnapHandler {
/// Add the control points (optionally including bézier handles) of the specified shape layer to the snapping points
///
/// This should be called after start_snap
pub fn add_snap_path(&mut self, document_message_handler: &DocumentMessageHandler, layer: &Layer, path: &[LayerId], include_handles: bool) {
if let LayerDataType::Shape(s) = &layer.data {
pub fn add_snap_path(&mut self, document_message_handler: &DocumentMessageHandler, layer: &Layer, path: &[LayerId], include_handles: bool, ignore_points: &[(&[LayerId], u64, ControlPointType)]) {
if let LayerDataType::Shape(shape_layer) = &layer.data {
let transform = document_message_handler.graphene_document.multiply_transforms(path).unwrap();
let snap_points = s
.path
.iter()
.flat_map(|shape| {
let snap_points = shape_layer
.shape
.anchors()
.enumerate()
.flat_map(|(id, shape)| {
if include_handles {
match shape {
kurbo::PathEl::MoveTo(point) => vec![point],
kurbo::PathEl::LineTo(point) => vec![point],
kurbo::PathEl::QuadTo(handle1, point) => vec![handle1, point],
kurbo::PathEl::CurveTo(handle1, handle2, point) => vec![handle1, handle2, point],
kurbo::PathEl::ClosePath => vec![],
}
[
(*id, &shape.points[ControlPointType::Anchor]),
(*id, &shape.points[ControlPointType::InHandle]),
(*id, &shape.points[ControlPointType::OutHandle]),
]
} else {
match shape {
kurbo::PathEl::MoveTo(point) => vec![point],
kurbo::PathEl::LineTo(point) => vec![point],
kurbo::PathEl::QuadTo(_, point) => vec![point],
kurbo::PathEl::CurveTo(_, _, point) => vec![point],
kurbo::PathEl::ClosePath => vec![],
}
[(*id, &shape.points[ControlPointType::Anchor]), (0, &None), (0, &None)]
}
})
.map(|point| DVec2::new(point.x, point.y))
.filter_map(|(id, point)| point.as_ref().map(|val| (id, val)))
.filter(|(id, point)| !ignore_points.contains(&(path, *id, point.manipulator_type)))
.map(|(_id, point)| DVec2::new(point.position.x, point.position.y))
.map(|pos| transform.transform_point2(pos));
self.add_snap_points(document_message_handler, snap_points);
}
}
/// Adds all of the shape handles in the document, including bézier handles of the points specified
pub fn add_all_document_handles(&mut self, document_message_handler: &DocumentMessageHandler, include_handles: &[&[LayerId]], exclude: &[&[LayerId]]) {
pub fn add_all_document_handles(
&mut self,
document_message_handler: &DocumentMessageHandler,
include_handles: &[&[LayerId]],
exclude: &[&[LayerId]],
ignore_points: &[(&[LayerId], u64, ControlPointType)],
) {
for path in document_message_handler.all_layers() {
if !exclude.contains(&path) {
let layer = document_message_handler.graphene_document.layer(path).expect("Could not get layer for snapping");
self.add_snap_path(document_message_handler, layer, path, include_handles.contains(&path));
self.add_snap_path(document_message_handler, layer, path, include_handles.contains(&path), ignore_points);
}
}
}

View File

@@ -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 {

View File

@@ -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());

View File

@@ -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 {

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());

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
}
(

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(),
)
})
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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());

View File

@@ -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());

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());

View File

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

View File

@@ -1,5 +1,6 @@
pub mod constants;
pub mod overlay_renderer;
pub mod shape_editor;
pub mod vector_anchor;
pub mod vector_control_point;
pub mod vector_shape;
use graphene::layers::vector::vector_anchor;
use graphene::layers::vector::vector_control_point;
use graphene::layers::vector::vector_shape;

View File

@@ -0,0 +1,315 @@
use super::constants::ROUNDING_BIAS;
use super::vector_anchor::VectorAnchor;
use super::vector_control_point::VectorControlPoint;
use crate::consts::{COLOR_ACCENT, PATH_OUTLINE_WEIGHT, VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE};
use crate::message_prelude::{generate_uuid, DocumentMessage, Message};
use graphene::color::Color;
use graphene::document::Document;
use graphene::layers::style::{self, Fill, Stroke};
use graphene::layers::vector::constants::ControlPointType;
use graphene::layers::vector::vector_shape::VectorShape;
use graphene::{LayerId, Operation};
use glam::{DAffine2, DVec2};
use std::collections::{HashMap, VecDeque};
/// AnchorOverlay is the collection of overlays that make up an anchor
/// Notably the anchor point, handles and the lines for the handles
type AnchorOverlays = [Option<Vec<LayerId>>; 5];
type AnchorId = u64;
const POINT_STROKE_WEIGHT: f64 = 2.;
#[derive(Clone, Debug, Default)]
pub struct OverlayRenderer {
shape_overlay_cache: HashMap<LayerId, Vec<LayerId>>,
anchor_overlay_cache: HashMap<(LayerId, AnchorId), AnchorOverlays>,
}
impl OverlayRenderer {
pub fn new() -> Self {
OverlayRenderer {
anchor_overlay_cache: HashMap::new(),
shape_overlay_cache: HashMap::new(),
}
}
pub fn render_vector_shape_overlays(&mut self, document: &Document, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
let transform = document.generate_transform_relative_to_viewport(&layer_path).ok().unwrap();
if let Ok(layer) = document.layer(&layer_path) {
let layer_id = layer_path.last().unwrap();
self.layer_overlay_visibility(document, layer_path.clone(), true, responses);
if let Some(shape) = layer.as_vector_shape() {
let outline_cache = self.shape_overlay_cache.get(layer_id);
log::trace!("Overlay: Outline cache {:?}", &outline_cache);
// Create an outline if we do not have a cached one
if outline_cache == None {
let outline_path = self.create_shape_outline_overlay(shape.clone(), responses);
self.shape_overlay_cache.insert(*layer_id, outline_path.clone());
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
log::trace!("Overlay: Creating new outline {:?}", &outline_path);
} else if let Some(outline_path) = outline_cache {
log::trace!("Overlay: Updating overlays for {:?} owning layer: {:?}", outline_path, layer_id);
Self::modify_outline_overlays(outline_path.clone(), shape.clone(), responses);
Self::place_outline_overlays(outline_path.clone(), &transform, responses);
}
// Create, place and style the anchor / handle overlays
for (anchor_id, anchor) in shape.anchors().enumerate() {
let anchor_cache = self.anchor_overlay_cache.get_mut(&(*layer_id, *anchor_id));
// If cached update placement and style
if let Some(anchor_overlays) = anchor_cache {
log::trace!("Overlay: Updating detail overlays for {:?}", anchor_overlays);
Self::place_anchor_overlays(anchor, anchor_overlays, &transform, responses);
Self::style_overlays(anchor, anchor_overlays, responses);
} else {
// Create if not cached
let mut anchor_overlays = [
Some(self.create_anchor_overlay(responses)),
Self::create_handle_overlay_if_exists(&anchor.points[ControlPointType::InHandle], responses),
Self::create_handle_overlay_if_exists(&anchor.points[ControlPointType::OutHandle], responses),
Self::create_handle_line_overlay_if_exists(&anchor.points[ControlPointType::InHandle], responses),
Self::create_handle_line_overlay_if_exists(&anchor.points[ControlPointType::OutHandle], responses),
];
Self::place_anchor_overlays(anchor, &mut anchor_overlays, &transform, responses);
Self::style_overlays(anchor, &anchor_overlays, responses);
self.anchor_overlay_cache.insert((*layer_id, *anchor_id), anchor_overlays);
}
}
// TODO Handle removing shapes from cache so we don't memory leak
// Eventually will get replaced with am immediate mode renderer for overlays
}
}
}
pub fn clear_vector_shape_overlays(&mut self, document: &Document, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
let layer_id = layer_path.last().unwrap();
// Remove the shape outline overlays
if let Some(overlay_path) = self.shape_overlay_cache.get(layer_id) {
Self::remove_outline_overlays(overlay_path.clone(), responses)
}
self.shape_overlay_cache.remove(layer_id);
// Remove the anchor overlays
if let Ok(layer) = document.layer(&layer_path) {
if let Some(shape) = layer.as_vector_shape() {
for (id, _) in shape.anchors().enumerate() {
if let Some(anchor_overlays) = self.anchor_overlay_cache.get(&(*layer_id, *id)) {
Self::remove_anchor_overlays(anchor_overlays, responses);
self.anchor_overlay_cache.remove(&(*layer_id, *id));
}
}
}
}
}
pub fn layer_overlay_visibility(&mut self, document: &Document, layer_path: Vec<LayerId>, visibility: bool, responses: &mut VecDeque<Message>) {
let layer_id = layer_path.last().unwrap();
// Hide the shape outline overlays
if let Some(overlay_path) = self.shape_overlay_cache.get(layer_id) {
Self::set_outline_overlay_visibility(overlay_path.clone(), visibility, responses);
}
// Hide the anchor overlays
if let Ok(layer) = document.layer(&layer_path) {
if let Some(shape) = layer.as_vector_shape() {
for (id, _) in shape.anchors().enumerate() {
if let Some(anchor_overlays) = self.anchor_overlay_cache.get(&(*layer_id, *id)) {
Self::set_anchor_overlay_visibility(anchor_overlays, visibility, responses);
}
}
}
}
}
/// Create the kurbo shape that matches the selected viewport shape
fn create_shape_outline_overlay(&self, vector_path: VectorShape, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddShape {
path: layer_path.clone(),
vector_path,
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create a single anchor overlay and return its layer id
fn create_anchor_overlay(&self, responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddRect {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create a single handle overlay and return its layer id
fn create_handle_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddEllipse {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
insert_index: -1,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create a single handle overlay and return its layer id if it exists
fn create_handle_overlay_if_exists(handle: &Option<VectorControlPoint>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
handle.as_ref().map(|_| Self::create_handle_overlay(responses))
}
/// Create the shape outline overlay and return its layer id
fn create_handle_line_overlay(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddLine {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
insert_index: -1,
};
responses.push_front(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
/// Create the shape outline overlay and return its layer id
fn create_handle_line_overlay_if_exists(handle: &Option<VectorControlPoint>, responses: &mut VecDeque<Message>) -> Option<Vec<LayerId>> {
handle.as_ref().map(|_| Self::create_handle_line_overlay(responses))
}
fn place_outline_overlays(outline_path: Vec<LayerId>, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
let transform_message = Self::overlay_transform_message(outline_path, parent_transform.to_cols_array());
responses.push_back(transform_message);
}
fn modify_outline_overlays(outline_path: Vec<LayerId>, vector_path: VectorShape, responses: &mut VecDeque<Message>) {
let outline_modify_message = Self::overlay_modify_message(outline_path, vector_path);
responses.push_back(outline_modify_message);
}
/// Updates the position of the overlays based on the VectorShape points
fn place_anchor_overlays(anchor: &VectorAnchor, overlays: &mut AnchorOverlays, parent_transform: &DAffine2, responses: &mut VecDeque<Message>) {
if let Some(anchor_point) = &anchor.points[ControlPointType::Anchor] {
// Helper function to keep things DRY
let mut place_handle_and_line = |handle: &VectorControlPoint, line_source: &mut Option<Vec<LayerId>>, marker_source: &mut Option<Vec<LayerId>>| {
let line_overlay = line_source.take().unwrap_or_else(|| Self::create_handle_line_overlay(responses));
let line_vector = parent_transform.transform_point2(anchor_point.position) - parent_transform.transform_point2(handle.position);
let scale = DVec2::splat(line_vector.length());
let angle = -line_vector.angle_between(DVec2::X);
let translation = (parent_transform.transform_point2(handle.position) + ROUNDING_BIAS).round() + DVec2::splat(0.5);
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.push_back(Self::overlay_transform_message(line_overlay.clone(), transform));
*line_source = Some(line_overlay);
let marker_overlay = marker_source.take().unwrap_or_else(|| Self::create_handle_overlay(responses));
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
let angle = 0.;
let translation = (parent_transform.transform_point2(handle.position) - (scale / 2.) + ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.push_back(Self::overlay_transform_message(marker_overlay.clone(), transform));
*marker_source = Some(marker_overlay);
};
// Place the handle overlays
let [_, h1, h2] = &anchor.points;
let [a, b, c, line1, line2] = overlays;
let markers = [a, b, c];
if let Some(handle) = &h1 {
place_handle_and_line(handle, line1, markers[handle.manipulator_type as usize]);
}
if let Some(handle) = &h2 {
place_handle_and_line(handle, line2, markers[handle.manipulator_type as usize]);
}
// Place the anchor point overlay
if let Some(anchor_overlay) = &overlays[ControlPointType::Anchor as usize] {
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
let angle = 0.;
let translation = (parent_transform.transform_point2(anchor_point.position) - (scale / 2.) + ROUNDING_BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let message = Self::overlay_transform_message(anchor_overlay.clone(), transform);
responses.push_back(message);
}
}
}
/// Removes the anchor / handle overlays from the overlay document
fn remove_anchor_overlays(overlay_paths: &AnchorOverlays, responses: &mut VecDeque<Message>) {
overlay_paths.iter().flatten().for_each(|layer_id| {
log::trace!("Overlay: Sending delete message for: {:?}", layer_id);
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer_id.clone() }.into()).into());
});
}
fn remove_outline_overlays(overlay_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: overlay_path }.into()).into());
}
/// Sets the visibility of the handles overlay
fn set_anchor_overlay_visibility(anchor_overlays: &AnchorOverlays, visibility: bool, responses: &mut VecDeque<Message>) {
anchor_overlays.iter().flatten().for_each(|layer_id| {
responses.push_back(Self::overlay_visibility_message(layer_id.clone(), visibility));
});
}
fn set_outline_overlay_visibility(overlay_path: Vec<LayerId>, visibility: bool, responses: &mut VecDeque<Message>) {
responses.push_back(Self::overlay_visibility_message(overlay_path, visibility));
}
/// Create a visibility message for an overlay
fn overlay_visibility_message(layer_path: Vec<LayerId>, visibility: bool) -> Message {
DocumentMessage::Overlays(
Operation::SetLayerVisibility {
path: layer_path,
visible: visibility,
}
.into(),
)
.into()
}
/// Create a transform message for an overlay
fn overlay_transform_message(layer_path: Vec<LayerId>, transform: [f64; 6]) -> Message {
DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path: layer_path, transform }.into()).into()
}
/// Create an update message for an overlay
fn overlay_modify_message(layer_path: Vec<LayerId>, vector_path: VectorShape) -> Message {
DocumentMessage::Overlays(Operation::SetShapePath { path: layer_path, vector_path }.into()).into()
}
/// Sets the overlay style for this point
fn style_overlays(anchor: &VectorAnchor, overlays: &AnchorOverlays, responses: &mut VecDeque<Message>) {
// TODO Move the style definitions out of the VectorShape, should be looked up from a stylesheet or similar
let selected_style = style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, POINT_STROKE_WEIGHT + 1.0)), Fill::solid(COLOR_ACCENT));
let deselected_style = style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, POINT_STROKE_WEIGHT)), Fill::solid(Color::WHITE));
// Update if the anchor / handle points are shown as selected
// Here the index is important, even though overlays[..] has five elements we only care about the first three
for (index, point) in anchor.points.iter().enumerate() {
if let Some(point) = point {
if let Some(overlay) = &overlays[index] {
// log::debug!("style_overlays: {:?}", &overlay);
let style = if point.editor_state.is_selected { selected_style.clone() } else { deselected_style.clone() };
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerStyle { path: overlay.clone(), style }.into()).into());
}
}
}
}
}

View File

@@ -1,273 +1,264 @@
/*
Overview:
ShapeEditor
/ \
VectorShape ... VectorShape <- ShapeEditor contains many VectorShapes
/ \
VectorAnchor ... VectorAnchor <- VectorShape contains many VectorAnchors
VectorAnchor <- Container for the anchor metadata and optional VectorControlPoints
/
[Option<VectorControlPoint>; 3] <- [0] is the anchor's draggable point (but not metadata), [1] is the handle1's draggable point, [2] is the handle2's draggable point
/ | \
"Anchor" "Handle1" "Handle2" <- These are VectorControlPoints and the only editable / draggable "primitive"
*/
// Overview:
// ShapeEditor
// / \
// selected_shape_layers <- Paths to selected layers that may contain VectorShapes
// | |
// VectorShape ... VectorShape <- Reference from layer paths, one Vectorshape per layer
// / \
// VectorAnchor ... VectorAnchor <- VectorShape contains many VectorAnchors
use super::vector_anchor::VectorAnchor;
use super::vector_control_point::VectorControlPoint;
use super::vector_shape::VectorShape;
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::{DocumentMessage, Message};
use graphene::layers::layer_info::LayerDataType;
use graphene::layers::vector::constants::ControlPointType;
use graphene::{LayerId, Operation};
use glam::{DAffine2, DVec2};
use std::collections::{HashSet, VecDeque};
use glam::DVec2;
use graphene::document::Document;
use std::collections::VecDeque;
/// ShapeEditor is the container for all of the selected kurbo paths that are
/// ShapeEditor is the container for all of the layer paths that are
/// represented as VectorShapes and provides functionality required
/// to query and create the VectorShapes / VectorAnchors / VectorControlPoints
#[derive(Clone, Debug, Default)]
pub struct ShapeEditor {
// The shapes we can select anchors / handles from
pub shapes_to_modify: Vec<VectorShape>,
// Index of the shape that contained the most recent selected point
pub selected_shape_indices: HashSet<usize>,
// The layers we can select and edit anchors / handles from
selected_layers: Vec<Vec<LayerId>>,
}
// TODO Consider keeping a list of selected anchors to minimize traversals of the layers
impl ShapeEditor {
/// Select the first point within the selection threshold
/// Returns true if we've found a point, false otherwise
pub fn select_point(&mut self, mouse_position: DVec2, select_threshold: f64, add_to_selection: bool, responses: &mut VecDeque<Message>) -> bool {
if self.shapes_to_modify.is_empty() {
return false;
/// Returns the points if found, none otherwise
pub fn select_point(
&self,
document: &Document,
mouse_position: DVec2,
select_threshold: f64,
add_to_selection: bool,
responses: &mut VecDeque<Message>,
) -> Option<Vec<(&[LayerId], u64, ControlPointType)>> {
if self.selected_layers.is_empty() {
return None;
}
if let Some((shape_index, anchor_index, point_index)) = self.find_nearest_point_indicies(mouse_position, select_threshold) {
log::trace!("Selecting: shape {} / anchor {} / point {}", shape_index, anchor_index, point_index);
// Add this shape to the selection
self.set_shape_selected(shape_index);
if let Some((shape_layer_path, anchor_id, point_index)) = self.find_nearest_point_indicies(document, mouse_position, select_threshold) {
log::trace!("Selecting: anchor {} / point {}", anchor_id, point_index);
// If the point we're selecting has already been selected
// we can assume this point exists.. since we did just click on it hense the unwrap
let is_point_selected = self.shapes_to_modify[shape_index].anchors[anchor_index].points[point_index].as_ref().unwrap().is_selected;
let is_point_selected = self.shape(document, shape_layer_path).unwrap().anchors().by_id(anchor_id).unwrap().points[point_index]
.as_ref()
.unwrap()
.editor_state
.is_selected;
// Deselected if we're not adding to the selection
if !add_to_selection && !is_point_selected {
self.deselect_all(responses);
}
let point_position = self.shape(document, shape_layer_path).unwrap().anchors().by_id(anchor_id).unwrap().points[point_index]
.as_ref()
.unwrap()
.position;
let selected_shape = &mut self.shapes_to_modify[shape_index];
selected_shape.elements = selected_shape.bez_path.clone().into_iter().collect();
// The currently selected points (which are then modified to reflect the selection)
let mut points = self
.selected_layers()
.iter()
.filter_map(|path| document.layer(path).ok().map(|layer| (path, layer)))
.filter_map(|(path, shape)| shape.as_vector_shape().map(|vector| (path, vector)))
.flat_map(|(path, shape)| {
shape
.anchors()
.enumerate()
.filter(|(_id, anchor)| anchor.is_anchor_selected())
.flat_map(|(id, anchor)| anchor.selected_points().map(move |point| (id, point.manipulator_type)))
.map(|(anchor, control_point)| (path.as_slice(), *anchor, control_point))
})
.collect::<Vec<_>>();
// let selected_shape = self.shape(document, shape_layer_path).unwrap();
// Should we select or deselect the point?
let should_select = if is_point_selected { !(add_to_selection && is_point_selected) } else { true };
let should_select = if is_point_selected { !add_to_selection } else { true };
// Add which anchor and point was selected
let selected_anchor = selected_shape.select_anchor(anchor_index);
selected_anchor.select_point(point_index, should_select, responses);
// This is selecting the anchor only for now, next to generalize to points
if should_select {
let add = add_to_selection || is_point_selected;
let point = (anchor_id, ControlPointType::from_index(point_index));
// Clear all point in other selected shapes
if !(add) {
responses.push_back(DocumentMessage::DeselectAllVectorPoints.into());
points = vec![(shape_layer_path, point.0, point.1)];
} else {
points.push((shape_layer_path, point.0, point.1));
}
responses.push_back(
Operation::SelectVectorPoints {
layer_path: shape_layer_path.to_vec(),
point_ids: vec![point],
add,
}
.into(),
);
// Snap the selected point to the cursor
if let Ok(viewspace) = document.generate_transform_relative_to_viewport(shape_layer_path) {
self.move_selected_points(mouse_position - viewspace.transform_point2(point_position), mouse_position, responses)
}
} else {
responses.push_back(
Operation::DeselectVectorPoints {
layer_path: shape_layer_path.to_vec(),
point_ids: vec![(anchor_id, ControlPointType::from_index(point_index))],
}
.into(),
);
points.retain(|x| *x != (shape_layer_path, anchor_id, ControlPointType::from_index(point_index)))
}
// Due to the shape data structure not persisting across shape selection changes we need to rely on the kurbo path to know if we should mirror
selected_anchor.set_mirroring((selected_anchor.angle_between_handles().abs() - std::f64::consts::PI).abs() < MINIMUM_MIRROR_THRESHOLD);
return true;
return Some(points);
}
false
// Deselect all points if no nearby point
responses.push_back(DocumentMessage::DeselectAllVectorPoints.into());
None
}
/// A wrapper for find_nearest_point_indicies and returns a VectorControlPoint
pub fn find_nearest_point<'a>(&'a self, document: &'a Document, mouse_position: DVec2, select_threshold: f64) -> Option<&'a VectorControlPoint> {
let (shape_layer_path, anchor_id, point_index) = self.find_nearest_point_indicies(document, mouse_position, select_threshold)?;
let selected_shape = self.shape(document, shape_layer_path).unwrap();
if let Some(anchor) = selected_shape.anchors().by_id(anchor_id) {
return anchor.points[point_index].as_ref();
}
None
}
/// Set the shapes we consider for selection, we will choose draggable handles / anchors from these shapes.
pub fn set_selected_layers(&mut self, target_layers: Vec<Vec<LayerId>>) {
self.selected_layers = target_layers;
}
pub fn selected_layers(&self) -> &Vec<Vec<LayerId>> {
&self.selected_layers
}
pub fn selected_layers_ref(&self) -> Vec<&[LayerId]> {
self.selected_layers.iter().map(|l| l.as_slice()).collect::<Vec<_>>()
}
/// Clear all of the shapes we can modify
pub fn clear_selected_layers(&mut self) {
self.selected_layers.clear();
}
pub fn has_selected_layers(&self) -> bool {
!self.selected_layers.is_empty()
}
/// Provide the currently selected anchor by reference
pub fn selected_anchors<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a VectorAnchor> {
self.iter(document).flat_map(|shape| shape.selected_anchors())
}
/// A mutable iterator of all the anchors, regardless of selection
pub fn anchors<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a VectorAnchor> {
self.iter(document).flat_map(|shape| shape.anchors().iter())
}
/// Provide the currently selected points by reference
pub fn selected_points<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a VectorControlPoint> {
self.selected_anchors(document).flat_map(|anchors| anchors.selected_points())
}
/// Move the selected points by dragging the moue
pub fn move_selected_points(&self, delta: DVec2, absolute_position: DVec2, responses: &mut VecDeque<Message>) {
for layer_path in &self.selected_layers {
responses.push_back(
DocumentMessage::MoveSelectedVectorPoints {
layer_path: layer_path.clone(),
delta: (delta.x, delta.y),
absolute_position: (absolute_position.x, absolute_position.y),
}
.into(),
);
}
}
/// Dissolve the selected points
pub fn delete_selected_points(&self, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::DeleteSelectedVectorPoints.into());
}
/// Toggle if the handles should mirror angle across the anchor positon
pub fn toggle_handle_mirroring_on_selected(&self, toggle_angle: bool, toggle_distance: bool, responses: &mut VecDeque<Message>) {
for layer_path in &self.selected_layers {
responses.push_back(
DocumentMessage::ToggleSelectedHandleMirroring {
layer_path: layer_path.clone(),
toggle_angle,
toggle_distance,
}
.into(),
);
}
}
/// Deselect all anchors from the shapes the manipulation handler has created
pub fn deselect_all_points(&self, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::DeselectAllVectorPoints.into());
}
/// Iterate over the shapes
pub fn iter<'a>(&'a self, document: &'a Document) -> impl Iterator<Item = &'a VectorShape> + 'a {
self.selected_layers.iter().flat_map(|layer_id| document.layer(layer_id)).filter_map(|shape| shape.as_vector_shape())
}
/// Find a point that is within the selection threshold and return an index to the shape, anchor, and point
pub fn find_nearest_point_indicies(&mut self, mouse_position: DVec2, select_threshold: f64) -> Option<(usize, usize, usize)> {
if self.shapes_to_modify.is_empty() {
fn find_nearest_point_indicies(&self, document: &Document, mouse_position: DVec2, select_threshold: f64) -> Option<(&[LayerId], u64, usize)> {
if self.selected_layers.is_empty() {
return None;
}
let select_threshold_squared = select_threshold * select_threshold;
// Find the closest control point among all elements of shapes_to_modify
for shape_index in 0..self.shapes_to_modify.len() {
if let Some((anchor_index, point_index, distance_squared)) = self.closest_point_indices(&self.shapes_to_modify[shape_index], mouse_position) {
for layer in self.selected_layers.iter() {
if let Some((anchor_id, point_index, distance_squared)) = self.closest_point_in_layer(document, layer, mouse_position) {
// Choose the first point under the threshold
if distance_squared < select_threshold_squared {
log::trace!("Selecting: shape {} / anchor {} / point {}", shape_index, anchor_index, point_index);
return Some((shape_index, anchor_index, point_index));
log::trace!("Selecting: anchor {} / point {}", anchor_id, point_index);
return Some((layer, anchor_id, point_index));
}
}
}
None
}
/// A wrapper for find_nearest_point_indicies and returns a mutable VectorControlPoint
pub fn find_nearest_point(&mut self, mouse_position: DVec2, select_threshold: f64) -> Option<&mut VectorControlPoint> {
let (shape_index, anchor_index, point_index) = self.find_nearest_point_indicies(mouse_position, select_threshold)?;
let selected_shape = &mut self.shapes_to_modify[shape_index];
selected_shape.anchors[anchor_index].points[point_index].as_mut()
}
/// Set the shapes we consider for selection, we will choose draggable handles / anchors from these shapes.
pub fn set_shapes_to_modify(&mut self, selected_shapes: Vec<VectorShape>) {
self.shapes_to_modify = selected_shapes;
}
/// 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
pub fn set_shape_selected(&mut self, shape_index: usize) {
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
pub fn selected_shapes(&self) -> impl Iterator<Item = &VectorShape> {
self.shapes_to_modify
.iter()
.enumerate()
.filter_map(|(index, shape)| if self.selected_shape_indices.contains(&index) { Some(shape) } else { None })
}
/// Provide the mutable shapes that the currently selected points are a part of
pub fn selected_shapes_mut(&mut self) -> impl Iterator<Item = &mut VectorShape> {
self.shapes_to_modify
.iter_mut()
.enumerate()
.filter_map(|(index, shape)| if self.selected_shape_indices.contains(&index) { Some(shape) } else { None })
}
/// Provide the currently selected anchor by reference
pub fn selected_anchors(&self) -> impl Iterator<Item = &VectorAnchor> {
self.selected_shapes().flat_map(|shape| shape.selected_anchors())
}
/// Provide the currently selected anchors by mutable reference
pub fn selected_anchors_mut(&mut self) -> impl Iterator<Item = &mut VectorAnchor> {
self.selected_shapes_mut().flat_map(|shape| shape.selected_anchors_mut())
}
/// 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
pub fn selected_points(&self) -> impl Iterator<Item = &VectorControlPoint> {
self.selected_shapes().flat_map(|shape| shape.selected_anchors()).flat_map(|anchors| anchors.selected_points())
}
/// Provide the currently selected points by mutable reference
pub fn selected_points_mut(&mut self) -> impl Iterator<Item = &mut VectorControlPoint> {
self.selected_shapes_mut()
.flat_map(|shape| shape.selected_anchors_mut())
.flat_map(|anchors| anchors.selected_points_mut())
}
/// Move the selected points by dragging the moue
pub fn move_selected_points(&mut self, target: DVec2, relative: bool, responses: &mut VecDeque<Message>) {
for shape in self.selected_shapes_mut() {
shape.move_selected(target, relative, responses);
}
}
/// Toggle if the handles should mirror angle across the anchor positon
pub fn toggle_selected_mirror_angle(&mut self) {
for anchor in self.selected_anchors_mut() {
anchor.handle_mirror_angle = !anchor.handle_mirror_angle;
}
}
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
pub fn toggle_selected_mirror_distance(&mut self) {
for anchor in self.selected_anchors_mut() {
anchor.handle_mirror_distance = !anchor.handle_mirror_distance;
}
}
/// Remove all of the overlays from the shapes the manipulation handler has created
pub fn deselect_all(&mut self, responses: &mut VecDeque<Message>) {
for shape in self.shapes_to_modify.iter_mut() {
shape.clear_selected_anchors(responses);
// Apply the final elements to the shape
// Fixes the snapback problem
shape.elements = shape.bez_path.clone().into_iter().collect();
}
}
/// Remove all of the overlays for the VectorManipulators / shape
pub fn remove_overlays(&mut self, responses: &mut VecDeque<Message>) {
for shape in self.shapes_to_modify.iter_mut() {
shape.remove_overlays(responses)
}
}
// TODO Use quadtree or some equivalent spatial acceleration structure to improve this to O(log(n))
/// Find the closest point, anchor and distance so we can select path elements
/// Brute force comparison to determine which handle / anchor we want to select, O(n)
fn closest_point_indices(&self, shape: &VectorShape, pos: glam::DVec2) -> Option<(usize, usize, f64)> {
fn closest_point_in_layer(&self, document: &Document, layer_path: &[LayerId], pos: glam::DVec2) -> Option<(u64, usize, f64)> {
let mut closest_distance_squared: f64 = f64::MAX; // Not ideal
let mut result: Option<(usize, usize, f64)> = None;
for (anchor_index, anchor) in shape.anchors.iter().enumerate() {
let point_index = anchor.closest_point(pos);
if let Some(point) = &anchor.points[point_index] {
if point.can_be_selected {
let distance_squared = point.position.distance_squared(pos);
if distance_squared < closest_distance_squared {
closest_distance_squared = distance_squared;
result = Some((anchor_index, point_index, distance_squared));
let mut result: Option<(u64, usize, f64)> = None;
if let Some(shape) = document.layer(layer_path).ok()?.as_vector_shape() {
let viewspace = document.generate_transform_relative_to_viewport(layer_path).ok()?;
for (anchor_id, anchor) in shape.anchors().enumerate() {
let point_index = anchor.closest_point(&viewspace, pos);
if let Some(point) = &anchor.points[point_index] {
if point.editor_state.can_be_selected {
let distance_squared = viewspace.transform_point2(point.position).distance_squared(pos);
if distance_squared < closest_distance_squared {
closest_distance_squared = distance_squared;
result = Some((*anchor_id, point_index, distance_squared));
}
}
}
}
}
result
}
fn shape<'a>(&'a self, document: &'a Document, layer_id: &[u64]) -> Option<&'a VectorShape> {
document.layer(layer_id).ok()?.as_vector_shape()
}
}

View File

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

View File

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

View File

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