mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-03 04:52:16 +00:00
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:
parent
3c2fff4465
commit
58675eb64d
48 changed files with 2461 additions and 1807 deletions
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -46,4 +46,5 @@
|
|||
"html.format.wrapLineLength": 200,
|
||||
"files.eol": "\n",
|
||||
"files.insertFinalNewline": true,
|
||||
"rust-analyzer.procMacro.attributes.enable": true,
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{layout::widgets::*, message_prelude::FrontendMessage};
|
||||
use crate::layout::widgets::*;
|
||||
use crate::message_prelude::FrontendMessage;
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,7 +182,7 @@ impl Fsm for ArtboardToolFsmState {
|
|||
tool_data
|
||||
.snap_handler
|
||||
.start_snap(document, document.bounding_boxes(None, Some(tool_data.selected_board.unwrap()), font_cache), snap_x, snap_y);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
|
||||
|
||||
ArtboardToolFsmState::ResizingBounds
|
||||
} else {
|
||||
|
@ -197,7 +197,7 @@ impl Fsm for ArtboardToolFsmState {
|
|||
tool_data
|
||||
.snap_handler
|
||||
.start_snap(document, document.bounding_boxes(None, Some(intersection[0]), font_cache), true, true);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
|
||||
|
||||
responses.push_back(
|
||||
PropertiesPanelMessage::SetActiveLayers {
|
||||
|
@ -213,7 +213,7 @@ impl Fsm for ArtboardToolFsmState {
|
|||
tool_data.selected_board = Some(id);
|
||||
|
||||
tool_data.snap_handler.start_snap(document, document.bounding_boxes(None, Some(id), font_cache), true, true);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
|
||||
|
||||
responses.push_back(
|
||||
ArtboardMessage::AddArtboard {
|
||||
|
|
|
@ -191,7 +191,6 @@ impl Fsm for FreehandToolFsmState {
|
|||
}
|
||||
(Drawing, DragStop) | (Drawing, Abort) => {
|
||||
if tool_data.points.len() >= 2 {
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(remove_preview(tool_data));
|
||||
responses.push_back(add_polyline(tool_data, global_tool_data));
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
|
|
|
@ -167,10 +167,11 @@ impl GradientOverlay {
|
|||
|
||||
let fill = if selected { Fill::solid(COLOR_ACCENT) } else { Fill::solid(Color::WHITE) };
|
||||
|
||||
let operation = Operation::AddOverlayEllipse {
|
||||
let operation = Operation::AddEllipse {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::from_scale_angle_translation(size, 0., translation - size / 2.).to_cols_array(),
|
||||
style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), fill),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
|
@ -185,10 +186,11 @@ impl GradientOverlay {
|
|||
let translation = start;
|
||||
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
|
||||
|
||||
let operation = Operation::AddOverlayLine {
|
||||
let operation = Operation::AddLine {
|
||||
path: path.clone(),
|
||||
transform,
|
||||
style: PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
|
@ -315,7 +317,7 @@ struct GradientToolData {
|
|||
|
||||
pub fn start_snap(snap_handler: &mut SnapHandler, document: &DocumentMessageHandler, font_cache: &FontCache) {
|
||||
snap_handler.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
snap_handler.add_all_document_handles(document, &[], &[]);
|
||||
snap_handler.add_all_document_handles(document, &[], &[], &[]);
|
||||
}
|
||||
|
||||
impl Fsm for GradientToolFsmState {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
}
|
||||
(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::consts::CREATE_CURVE_THRESHOLD;
|
||||
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
|
||||
use crate::document::DocumentMessageHandler;
|
||||
use crate::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::input::keyboard::{Key, MouseMotion};
|
||||
|
@ -8,15 +8,15 @@ use crate::message_prelude::*;
|
|||
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||
use crate::viewport_tools::snapping::SnapHandler;
|
||||
use crate::viewport_tools::tool::{Fsm, SignalToMessageMap, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
use crate::viewport_tools::vector_editor::constants::ControlPointType;
|
||||
use crate::viewport_tools::vector_editor::shape_editor::ShapeEditor;
|
||||
use crate::viewport_tools::vector_editor::vector_shape::VectorShape;
|
||||
use crate::viewport_tools::vector_editor::overlay_renderer::OverlayRenderer;
|
||||
|
||||
use graphene::layers::style;
|
||||
use graphene::layers::vector::constants::ControlPointType;
|
||||
use graphene::layers::vector::vector_anchor::VectorAnchor;
|
||||
use graphene::layers::vector::vector_shape::VectorShape;
|
||||
use graphene::Operation;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{PathEl, Point};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -45,12 +45,17 @@ pub enum PenToolMessage {
|
|||
DocumentIsDirty,
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
SelectionChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
Confirm,
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
PointerMove {
|
||||
snap_angle: Key,
|
||||
break_handle: Key,
|
||||
},
|
||||
Undo,
|
||||
UpdateOptions(PenOptionsUpdate),
|
||||
}
|
||||
|
@ -58,7 +63,8 @@ pub enum PenToolMessage {
|
|||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PenToolFsmState {
|
||||
Ready,
|
||||
Drawing,
|
||||
DraggingHandle,
|
||||
PlacingAnchor,
|
||||
}
|
||||
|
||||
#[remain::sorted]
|
||||
|
@ -124,11 +130,9 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for PenTool {
|
|||
}
|
||||
|
||||
fn actions(&self) -> ActionList {
|
||||
use PenToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(PenToolMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
|
||||
Drawing => actions!(PenToolMessageDiscriminant; DragStart, DragStop, PointerMove, Confirm, Abort),
|
||||
PenToolFsmState::Ready => actions!(PenToolMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
|
||||
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; DragStart, DragStop, PointerMove, Confirm, Abort),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,7 +142,7 @@ impl ToolTransition for PenTool {
|
|||
SignalToMessageMap {
|
||||
document_dirty: Some(PenToolMessage::DocumentIsDirty.into()),
|
||||
tool_abort: Some(PenToolMessage::Abort.into()),
|
||||
selection_changed: None,
|
||||
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,11 +156,8 @@ impl Default for PenToolFsmState {
|
|||
struct PenToolData {
|
||||
weight: f64,
|
||||
path: Option<Vec<LayerId>>,
|
||||
curve_shape: VectorShape,
|
||||
bez_path: Vec<PathEl>,
|
||||
overlay_renderer: OverlayRenderer,
|
||||
snap_handler: SnapHandler,
|
||||
shape_editor: ShapeEditor,
|
||||
drag_start_position: DVec2,
|
||||
}
|
||||
|
||||
impl Fsm for PenToolFsmState {
|
||||
|
@ -171,112 +172,181 @@ impl Fsm for PenToolFsmState {
|
|||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use PenToolFsmState::*;
|
||||
use PenToolMessage::*;
|
||||
|
||||
let transform = document.graphene_document.root.transform;
|
||||
let transform = tool_data.path.as_ref().and_then(|path| document.graphene_document.multiply_transforms(path).ok()).unwrap_or_default();
|
||||
|
||||
if let ToolMessage::Pen(event) = event {
|
||||
match (self, event) {
|
||||
(_, DocumentIsDirty) => {
|
||||
tool_data.shape_editor.update_shapes(document, responses);
|
||||
(_, PenToolMessage::DocumentIsDirty) => {
|
||||
// When the document has moved / needs to be redraw, re-render the overlays
|
||||
// TODO the overlay system should probably receive this message instead of the tool
|
||||
for layer_path in document.selected_visible_layers() {
|
||||
tool_data.overlay_renderer.render_vector_shape_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
self
|
||||
}
|
||||
(Ready, DragStart) => {
|
||||
(_, PenToolMessage::SelectionChanged) => {
|
||||
// Set the previously selected layers to invisible
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.layer_overlay_visibility(&document.graphene_document, layer_path.to_vec(), false, responses);
|
||||
}
|
||||
self
|
||||
}
|
||||
(PenToolFsmState::Ready, PenToolMessage::DragStart) => {
|
||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
|
||||
// Create a new layer and prep snap system
|
||||
tool_data.path = Some(document.get_path_for_new_layer());
|
||||
tool_data.snap_handler.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
|
||||
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
// Get the position and set properties
|
||||
let transform = tool_data
|
||||
.path
|
||||
.as_ref()
|
||||
.and_then(|path| document.graphene_document.multiply_transforms(&path[..path.len() - 1]).ok())
|
||||
.unwrap_or_default();
|
||||
let start_position = transform.inverse().transform_point2(snapped_position);
|
||||
tool_data.weight = tool_options.line_weight;
|
||||
|
||||
// Create the initial shape with a `bez_path` (only contains a moveto initially)
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
tool_data.bez_path = start_bez_path(start_position);
|
||||
responses.push_back(
|
||||
Operation::AddShape {
|
||||
path: layer_path.clone(),
|
||||
transform: transform.to_cols_array(),
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
insert_index: -1,
|
||||
bez_path: tool_data.bez_path.clone().into_iter().collect(),
|
||||
vector_path: Default::default(),
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(global_tool_data.primary_color, tool_data.weight)), style::Fill::None),
|
||||
closed: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(add_anchor(&tool_data.path, VectorAnchor::new(start_position)));
|
||||
}
|
||||
|
||||
add_to_curve(tool_data, input, transform, document, responses);
|
||||
Drawing
|
||||
PenToolFsmState::DraggingHandle
|
||||
}
|
||||
(Drawing, DragStart) => {
|
||||
tool_data.drag_start_position = input.mouse.position;
|
||||
add_to_curve(tool_data, input, transform, document, responses);
|
||||
Drawing
|
||||
}
|
||||
(Drawing, DragStop) => {
|
||||
// Deselect everything (this means we are no longer dragging the handle)
|
||||
tool_data.shape_editor.deselect_all(responses);
|
||||
|
||||
// If the drag does not exceed the threshold, then replace the curve with a line
|
||||
if tool_data.drag_start_position.distance(input.mouse.position) < CREATE_CURVE_THRESHOLD {
|
||||
// Modify the second to last element (as we have an unplaced element tracing to the cursor as the last element)
|
||||
let replace_index = tool_data.bez_path.len() - 2;
|
||||
let line_from_curve = convert_curve_to_line(tool_data.bez_path[replace_index]);
|
||||
replace_path_element(tool_data, transform, replace_index, line_from_curve, responses);
|
||||
(PenToolFsmState::PlacingAnchor, PenToolMessage::DragStart) => PenToolFsmState::DraggingHandle,
|
||||
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => {
|
||||
// Add new point onto path
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
if let Some(vector_anchor) = get_vector_shape(layer_path, document).and_then(|shape| shape.anchors().last()) {
|
||||
if let Some(anchor) = &vector_anchor.points[ControlPointType::OutHandle] {
|
||||
responses.push_back(add_anchor(&tool_data.path, VectorAnchor::new(anchor.position)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reselect the last point
|
||||
if let Some(last_anchor) = tool_data.shape_editor.select_last_anchor() {
|
||||
last_anchor.select_point(ControlPointType::Anchor as usize, true, responses);
|
||||
PenToolFsmState::PlacingAnchor
|
||||
}
|
||||
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle }) => {
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
let mouse = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
|
||||
let mut pos = transform.inverse().transform_point2(mouse);
|
||||
if let Some(((&id, anchor), _previous)) = get_vector_shape(layer_path, document).and_then(last_2_anchors) {
|
||||
if let Some(anchor) = anchor.points[ControlPointType::Anchor as usize].as_ref() {
|
||||
pos = compute_snapped_angle(input, snap_angle, pos, anchor.position);
|
||||
}
|
||||
|
||||
// Update points on current segment (to show preview of new handle)
|
||||
let msg = Operation::MoveVectorPoint {
|
||||
layer_path: layer_path.clone(),
|
||||
id,
|
||||
control_type: ControlPointType::OutHandle,
|
||||
position: pos.into(),
|
||||
};
|
||||
responses.push_back(msg.into());
|
||||
|
||||
// Mirror handle of last segement
|
||||
if !input.keyboard.get(break_handle as usize) && get_vector_shape(layer_path, document).map(|shape| shape.anchors().len() > 1).unwrap_or_default() {
|
||||
if let Some(anchor) = anchor.points[ControlPointType::Anchor as usize].as_ref() {
|
||||
pos = anchor.position - (pos - anchor.position);
|
||||
}
|
||||
let msg = Operation::MoveVectorPoint {
|
||||
layer_path: layer_path.clone(),
|
||||
id,
|
||||
control_type: ControlPointType::InHandle,
|
||||
position: pos.into(),
|
||||
};
|
||||
responses.push_back(msg.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move the newly selected points to the cursor
|
||||
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
|
||||
tool_data.shape_editor.move_selected_points(snapped_position, false, responses);
|
||||
|
||||
Drawing
|
||||
self
|
||||
}
|
||||
(Drawing, PointerMove) => {
|
||||
// Move selected points
|
||||
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
|
||||
tool_data.shape_editor.move_selected_points(snapped_position, false, responses);
|
||||
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, .. }) => {
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
let mouse = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
|
||||
let mut pos = transform.inverse().transform_point2(mouse);
|
||||
|
||||
Drawing
|
||||
if let Some(((&id, _anchor), previous)) = get_vector_shape(layer_path, document).and_then(last_2_anchors) {
|
||||
if let Some(relative) = previous.as_ref().and_then(|(_, anchor)| anchor.points[ControlPointType::Anchor as usize].as_ref()) {
|
||||
pos = compute_snapped_angle(input, snap_angle, pos, relative.position);
|
||||
}
|
||||
|
||||
for control_type in [ControlPointType::Anchor, ControlPointType::InHandle, ControlPointType::OutHandle] {
|
||||
let msg = Operation::MoveVectorPoint {
|
||||
layer_path: layer_path.clone(),
|
||||
id,
|
||||
control_type,
|
||||
position: pos.into(),
|
||||
};
|
||||
responses.push_back(msg.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(Drawing, Confirm) | (Drawing, Abort) => {
|
||||
// Cleanup, we are either canceling or finished drawing
|
||||
if tool_data.bez_path.len() >= 2 {
|
||||
// Remove the last segment
|
||||
remove_from_curve(tool_data);
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
responses.push_back(apply_bez_path(layer_path.clone(), tool_data.bez_path.clone(), transform));
|
||||
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Abort | PenToolMessage::Confirm) => {
|
||||
// Abort or commit the transaction to the undo history
|
||||
if let Some(layer_path) = tool_data.path.as_ref() {
|
||||
if let Some(vector_shape) = (get_vector_shape(layer_path, document)).filter(|vector_shape| vector_shape.anchors().len() > 1) {
|
||||
if let Some(((&(mut id), mut anchor), previous)) = last_2_anchors(vector_shape) {
|
||||
// Remove the unplaced anchor if in anchor placing mode
|
||||
if self == PenToolFsmState::PlacingAnchor {
|
||||
let layer_path = layer_path.clone();
|
||||
let op = Operation::RemoveVectorAnchor { layer_path, id };
|
||||
responses.push_back(op.into());
|
||||
if let Some((&new_id, new_anchor)) = previous {
|
||||
id = new_id;
|
||||
anchor = new_anchor;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the out handle if in dragging handle mode
|
||||
let op = Operation::MoveVectorPoint {
|
||||
layer_path: layer_path.clone(),
|
||||
id,
|
||||
control_type: ControlPointType::OutHandle,
|
||||
position: anchor.points[ControlPointType::Anchor as usize].as_ref().unwrap().position.into(),
|
||||
};
|
||||
responses.push_back(op.into());
|
||||
}
|
||||
}
|
||||
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
} else {
|
||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||
}
|
||||
|
||||
tool_data.shape_editor.remove_overlays(responses);
|
||||
tool_data.shape_editor.clear_shapes_to_modify();
|
||||
|
||||
// Clean up overlays
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.clear_vector_shape_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
tool_data.path = None;
|
||||
tool_data.snap_handler.cleanup(responses);
|
||||
|
||||
Ready
|
||||
PenToolFsmState::Ready
|
||||
}
|
||||
(_, Abort) => {
|
||||
tool_data.shape_editor.remove_overlays(responses);
|
||||
tool_data.shape_editor.clear_shapes_to_modify();
|
||||
Ready
|
||||
(_, PenToolMessage::Abort) => {
|
||||
// Clean up overlays
|
||||
for layer_path in document.all_layers() {
|
||||
tool_data.overlay_renderer.clear_vector_shape_overlays(&document.graphene_document, layer_path.to_vec(), responses);
|
||||
}
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
|
@ -293,11 +363,29 @@ impl Fsm for PenToolFsmState {
|
|||
label: String::from("Draw Path"),
|
||||
plus: false,
|
||||
}])]),
|
||||
PenToolFsmState::Drawing => HintData(vec![
|
||||
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => HintData(vec![
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
mouse: Some(MouseMotion::LmbDrag),
|
||||
label: String::from("Add Handle"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![],
|
||||
mouse: Some(MouseMotion::Lmb),
|
||||
label: String::from("Extend Path"),
|
||||
label: String::from("Add Control Point"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||
mouse: None,
|
||||
label: String::from("Break Handle"),
|
||||
plus: false,
|
||||
}]),
|
||||
HintGroup(vec![HintInfo {
|
||||
|
@ -317,91 +405,53 @@ impl Fsm for PenToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Add to the curve and select the second anchor of the last point and the newly added anchor point
|
||||
fn add_to_curve(tool_data: &mut PenToolData, input: &InputPreprocessorMessageHandler, transform: DAffine2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
// Refresh tool_data's representation of the path
|
||||
update_path_representation(tool_data);
|
||||
// TODO: Expand `pos` name below to the full word (position?)
|
||||
/// Snap the angle of the line from relative to pos if the key is pressed
|
||||
fn compute_snapped_angle(input: &InputPreprocessorMessageHandler, key: Key, pos: DVec2, relative: DVec2) -> DVec2 {
|
||||
if input.keyboard.get(key as usize) {
|
||||
let delta = relative - pos;
|
||||
|
||||
// Setup our position params
|
||||
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
|
||||
let position = transform.inverse().transform_point2(snapped_position);
|
||||
let length = delta.length();
|
||||
let mut angle = -delta.angle_between(DVec2::X);
|
||||
|
||||
// Add a curve to the path
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
// Push curve onto path
|
||||
let point = Point { x: position.x, y: position.y };
|
||||
tool_data.bez_path.push(PathEl::CurveTo(point, point, point));
|
||||
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
|
||||
responses.push_back(apply_bez_path(layer_path.clone(), tool_data.bez_path.clone(), transform));
|
||||
let rotated = DVec2::new(length * angle.cos(), length * angle.sin());
|
||||
relative - rotated
|
||||
} else {
|
||||
pos
|
||||
}
|
||||
}
|
||||
|
||||
// Clear previous overlays
|
||||
tool_data.shape_editor.remove_overlays(responses);
|
||||
|
||||
// Create a new `shape` from the updated `bez_path`
|
||||
let bez_path = tool_data.bez_path.clone().into_iter().collect();
|
||||
tool_data.curve_shape = VectorShape::new(layer_path.to_vec(), transform, &bez_path, false, responses);
|
||||
tool_data.shape_editor.set_shapes_to_modify(vec![tool_data.curve_shape.clone()]);
|
||||
|
||||
// Select the second to last `PathEl`'s handle
|
||||
tool_data.shape_editor.set_shape_selected(0);
|
||||
let handle_element = tool_data.shape_editor.select_nth_anchor(0, -2);
|
||||
handle_element.select_point(ControlPointType::Handle2 as usize, true, responses);
|
||||
|
||||
// Select the last `PathEl`'s anchor point
|
||||
if let Some(last_anchor) = tool_data.shape_editor.select_last_anchor() {
|
||||
last_anchor.select_point(ControlPointType::Anchor as usize, true, responses);
|
||||
/// Pushes an anchor to the current layer via an [Operation]
|
||||
fn add_anchor(layer_path: &Option<Vec<LayerId>>, anchor: VectorAnchor) -> Message {
|
||||
if let Some(layer_path) = layer_path {
|
||||
Operation::PushVectorAnchor {
|
||||
layer_path: layer_path.clone(),
|
||||
anchor,
|
||||
}
|
||||
tool_data.shape_editor.set_selected_mirror_options(true, true);
|
||||
.into()
|
||||
} else {
|
||||
Message::NoOp
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace a `PathEl` with another inside of `bez_path` by index
|
||||
fn replace_path_element(tool_data: &mut PenToolData, transform: DAffine2, replace_index: usize, replacement: PathEl, responses: &mut VecDeque<Message>) {
|
||||
tool_data.bez_path[replace_index] = replacement;
|
||||
if let Some(layer_path) = &tool_data.path {
|
||||
responses.push_back(apply_bez_path(layer_path.clone(), tool_data.bez_path.clone(), transform));
|
||||
}
|
||||
/// Gets the currently editing [VectorShape]
|
||||
fn get_vector_shape<'a>(layer_path: &'a [LayerId], document: &'a DocumentMessageHandler) -> Option<&'a VectorShape> {
|
||||
document.graphene_document.layer(layer_path).ok().and_then(|layer| layer.as_vector_shape())
|
||||
}
|
||||
|
||||
/// Remove a curve from the end of the `bez_path`
|
||||
fn remove_from_curve(tool_data: &mut PenToolData) {
|
||||
// Refresh tool_data's representation of the path
|
||||
update_path_representation(tool_data);
|
||||
tool_data.bez_path.pop();
|
||||
}
|
||||
type AnchorRef<'a> = (&'a u64, &'a VectorAnchor);
|
||||
|
||||
/// Create the initial moveto for the `bez_path`
|
||||
fn start_bez_path(start_position: DVec2) -> Vec<PathEl> {
|
||||
vec![PathEl::MoveTo(Point {
|
||||
x: start_position.x,
|
||||
y: start_position.y,
|
||||
})]
|
||||
}
|
||||
|
||||
/// Convert curve `PathEl` into a line `PathEl`
|
||||
fn convert_curve_to_line(curve: PathEl) -> PathEl {
|
||||
match curve {
|
||||
PathEl::CurveTo(_, _, p) => PathEl::LineTo(p),
|
||||
_ => PathEl::MoveTo(Point::ZERO),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update tool_data's version of `bez_path` to match `ShapeEditor`'s version
|
||||
fn update_path_representation(tool_data: &mut PenToolData) {
|
||||
// TODO Update ShapeEditor to provide similar functionality
|
||||
// We need to make sure we have the most up-to-date bez_path
|
||||
if !tool_data.shape_editor.shapes_to_modify.is_empty() {
|
||||
// Hacky way of saving the curve changes
|
||||
tool_data.bez_path = tool_data.shape_editor.shapes_to_modify[0].bez_path.elements().to_vec();
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply the `bez_path` to the `shape` in the viewport
|
||||
fn apply_bez_path(layer_path: Vec<LayerId>, bez_path: Vec<PathEl>, transform: DAffine2) -> Message {
|
||||
Operation::SetShapePathInViewport {
|
||||
path: layer_path,
|
||||
bez_path: bez_path.into_iter().collect(),
|
||||
transform: transform.to_cols_array(),
|
||||
}
|
||||
.into()
|
||||
/// Gets the last 2 [VectorAnchor] on the currently editing layer along with its id
|
||||
fn last_2_anchors(vector_shape: &VectorShape) -> Option<(AnchorRef, Option<AnchorRef>)> {
|
||||
vector_shape.anchors().enumerate().last().map(|last| {
|
||||
(
|
||||
last,
|
||||
(vector_shape.anchors().len() > 1)
|
||||
.then(|| vector_shape.anchors().enumerate().nth(vector_shape.anchors().len() - 2))
|
||||
.flatten(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -431,7 +431,7 @@ impl Fsm for SelectToolFsmState {
|
|||
tool_data.snap_handler.start_snap(document, document.bounding_boxes(Some(&selected), None, font_cache), snap_x, snap_y);
|
||||
tool_data
|
||||
.snap_handler
|
||||
.add_all_document_handles(document, &[], &selected.iter().map(|x| x.as_slice()).collect::<Vec<_>>());
|
||||
.add_all_document_handles(document, &[], &selected.iter().map(|x| x.as_slice()).collect::<Vec<_>>(), &[]);
|
||||
|
||||
tool_data.layers_dragging = selected;
|
||||
|
||||
|
|
|
@ -7,10 +7,10 @@ use graphene::intersection::Quad;
|
|||
use graphene::layers::layer_info::LayerDataType;
|
||||
use graphene::layers::style::{self, Fill, Stroke};
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
use graphene::layers::vector::vector_shape::VectorShape;
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{BezPath, Shape};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// Manages the overlay used by the select tool for outlining selected shapes and when hovering over a non selected shape.
|
||||
|
@ -33,13 +33,14 @@ impl PathOutline {
|
|||
// Get layer data
|
||||
let document_layer = document.graphene_document.layer(&document_layer_path).ok()?;
|
||||
|
||||
// TODO Purge this area of BezPath and Kurbo
|
||||
// Get the bezpath from the shape or text
|
||||
let path = match &document_layer.data {
|
||||
LayerDataType::Shape(shape) => Some(shape.path.clone()),
|
||||
LayerDataType::Text(text) => Some(text.to_bez_path_nonmut(font_cache)),
|
||||
let vector_path = match &document_layer.data {
|
||||
LayerDataType::Shape(layer_shape) => Some(layer_shape.shape.clone()),
|
||||
LayerDataType::Text(text) => Some(text.to_vector_path_nonmut(font_cache)),
|
||||
_ => document_layer
|
||||
.aabounding_box_for_transform(DAffine2::IDENTITY, font_cache)
|
||||
.map(|bounds| kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y).to_path(0.)),
|
||||
.map(|[p1, p2]| VectorShape::new_rect(p1, p2)),
|
||||
}?;
|
||||
|
||||
// Generate a new overlay layer if necessary
|
||||
|
@ -47,11 +48,12 @@ impl PathOutline {
|
|||
Some(path) => path,
|
||||
None => {
|
||||
let overlay_path = vec![generate_uuid()];
|
||||
let operation = Operation::AddOverlayShape {
|
||||
let operation = Operation::AddShape {
|
||||
path: overlay_path.clone(),
|
||||
bez_path: BezPath::new(),
|
||||
vector_path: Default::default(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, PATH_OUTLINE_WEIGHT)), Fill::None),
|
||||
closed: false,
|
||||
insert_index: -1,
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
};
|
||||
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
@ -61,10 +63,7 @@ impl PathOutline {
|
|||
};
|
||||
|
||||
// Update the shape bezpath
|
||||
let operation = Operation::SetShapePath {
|
||||
path: overlay.clone(),
|
||||
bez_path: path,
|
||||
};
|
||||
let operation = Operation::SetShapePath { path: overlay.clone(), vector_path };
|
||||
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
// Update the transform to match the document
|
||||
|
|
|
@ -21,7 +21,7 @@ impl Resize {
|
|||
/// Starts a resize, assigning the snap targets and snapping the starting position.
|
||||
pub fn start(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, mouse_position: DVec2, font_cache: &FontCache) {
|
||||
self.snap_handler.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
self.snap_handler.add_all_document_handles(document, &[], &[]);
|
||||
self.snap_handler.add_all_document_handles(document, &[], &[], &[]);
|
||||
self.drag_start = self.snap_handler.snap_position(responses, document, mouse_position);
|
||||
}
|
||||
|
||||
|
|
|
@ -140,10 +140,11 @@ impl SelectedEdges {
|
|||
pub fn add_bounding_box(responses: &mut Vec<Message>) -> Vec<LayerId> {
|
||||
let path = vec![generate_uuid()];
|
||||
|
||||
let operation = Operation::AddOverlayRect {
|
||||
let operation = Operation::AddRect {
|
||||
path: path.clone(),
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Fill::None),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
|
@ -158,10 +159,11 @@ fn add_transform_handles(responses: &mut Vec<Message>) -> [Vec<LayerId>; 8] {
|
|||
for item in &mut transform_handle_paths {
|
||||
let current_path = vec![generate_uuid()];
|
||||
|
||||
let operation = Operation::AddOverlayRect {
|
||||
let operation = Operation::AddRect {
|
||||
path: current_path.clone(),
|
||||
transform: DAffine2::ZERO.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Fill::solid(Color::WHITE)),
|
||||
insert_index: -1,
|
||||
};
|
||||
responses.push(DocumentMessage::Overlays(operation.into()).into());
|
||||
|
||||
|
|
|
@ -174,7 +174,7 @@ impl Fsm for SplineToolFsmState {
|
|||
tool_data.path = Some(document.get_path_for_new_layer());
|
||||
|
||||
tool_data.snap_handler.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[]);
|
||||
tool_data.snap_handler.add_all_document_handles(document, &[], &[], &[]);
|
||||
let snapped_position = tool_data.snap_handler.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
let pos = transform.inverse().transform_point2(snapped_position);
|
||||
|
@ -216,7 +216,6 @@ impl Fsm for SplineToolFsmState {
|
|||
}
|
||||
(Drawing, Confirm) | (Drawing, Abort) => {
|
||||
if tool_data.points.len() >= 2 {
|
||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
responses.push_back(remove_preview(tool_data));
|
||||
responses.push_back(add_spline(tool_data, global_tool_data, false));
|
||||
responses.push_back(DocumentMessage::CommitTransaction.into());
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
315
editor/src/viewport_tools/vector_editor/overlay_renderer.rs
Normal file
315
editor/src/viewport_tools/vector_editor/overlay_renderer.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
use crate::JS_EDITOR_HANDLES;
|
||||
|
||||
use editor::{input::keyboard::Key, message_prelude::FrontendMessage};
|
||||
use editor::input::keyboard::Key;
|
||||
use editor::message_prelude::FrontendMessage;
|
||||
|
||||
use std::panic;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
|
|
@ -404,7 +404,7 @@ impl PathGraph {
|
|||
concat_paths(&mut curve, &self.edge(vertices[index - 1].0, vertices[index].0, vertices[index].1).unwrap().curve);
|
||||
}
|
||||
curve.push(PathEl::ClosePath);
|
||||
ShapeLayer::from_bez_path(BezPath::from_vec(curve), style.clone(), false)
|
||||
ShapeLayer::new(BezPath::from_vec(curve).iter().into(), style.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -535,24 +535,24 @@ pub fn composite_boolean_operation(mut select: BooleanOperation, shapes: &mut Ve
|
|||
// TODO: check if shapes are filled
|
||||
// TODO: Bug: shape with at least two subpaths and comprised of many unions sometimes has erroneous movetos embedded in edges
|
||||
pub fn boolean_operation(mut select: BooleanOperation, alpha: &mut ShapeLayer, beta: &mut ShapeLayer) -> Result<Vec<ShapeLayer>, BooleanOperationError> {
|
||||
if alpha.path.is_empty() || beta.path.is_empty() {
|
||||
if alpha.shape.anchors().is_empty() || beta.shape.anchors().is_empty() {
|
||||
return Err(BooleanOperationError::InvalidSelection);
|
||||
}
|
||||
if select == BooleanOperation::SubtractBack {
|
||||
select = BooleanOperation::SubtractFront;
|
||||
swap(alpha, beta);
|
||||
}
|
||||
alpha.path = close_path(&alpha.path);
|
||||
beta.path = close_path(&beta.path);
|
||||
let beta_reverse = close_path(&reverse_path(&beta.path));
|
||||
let alpha_dir = Cycle::direction_for_path(&alpha.path)?;
|
||||
let beta_dir = Cycle::direction_for_path(&beta.path)?;
|
||||
let mut alpha_shape = close_path(&(&alpha.shape).into());
|
||||
let beta_shape = close_path(&(&beta.shape).into());
|
||||
let beta_reverse = close_path(&reverse_path(&beta_shape));
|
||||
let alpha_dir = Cycle::direction_for_path(&alpha_shape)?;
|
||||
let beta_dir = Cycle::direction_for_path(&beta_shape)?;
|
||||
match select {
|
||||
BooleanOperation::Union => {
|
||||
match if beta_dir == alpha_dir {
|
||||
PathGraph::from_paths(&alpha.path, &beta.path)
|
||||
PathGraph::from_paths(&alpha_shape, &beta_shape)
|
||||
} else {
|
||||
PathGraph::from_paths(&alpha.path, &beta_reverse)
|
||||
PathGraph::from_paths(&alpha_shape, &beta_reverse)
|
||||
} {
|
||||
Ok(graph) => {
|
||||
let mut cycles = graph.get_cycles();
|
||||
|
@ -562,16 +562,20 @@ pub fn boolean_operation(mut select: BooleanOperation, alpha: &mut ShapeLayer, b
|
|||
&alpha.style,
|
||||
);
|
||||
for interior in collect_shapes(&graph, &mut cycles, |dir| dir != alpha_dir, |_| &alpha.style)? {
|
||||
add_subpath(&mut boolean_union.path, interior.path);
|
||||
//TODO: this is not very efficient or nice to read
|
||||
let mut a_path: BezPath = (&boolean_union.shape).into();
|
||||
let b_path: BezPath = (&interior.shape).into();
|
||||
add_subpath(&mut a_path, b_path);
|
||||
boolean_union.shape = a_path.iter().into();
|
||||
}
|
||||
Ok(vec![boolean_union])
|
||||
}
|
||||
Err(BooleanOperationError::NoIntersections) => {
|
||||
// If shape is inside the other the Union is just the larger
|
||||
// Check could also be done with area and single ray cast
|
||||
if cast_horizontal_ray(point_on_curve(&beta.path), &alpha.path) % 2 != 0 {
|
||||
if cast_horizontal_ray(point_on_curve(&beta_shape), &alpha_shape) % 2 != 0 {
|
||||
Ok(vec![alpha.clone()])
|
||||
} else if cast_horizontal_ray(point_on_curve(&alpha.path), &beta.path) % 2 != 0 {
|
||||
} else if cast_horizontal_ray(point_on_curve(&alpha_shape), &beta_shape) % 2 != 0 {
|
||||
beta.style = alpha.style.clone();
|
||||
Ok(vec![beta.clone()])
|
||||
} else {
|
||||
|
@ -583,17 +587,17 @@ pub fn boolean_operation(mut select: BooleanOperation, alpha: &mut ShapeLayer, b
|
|||
}
|
||||
BooleanOperation::Difference => {
|
||||
let graph = if beta_dir != alpha_dir {
|
||||
PathGraph::from_paths(&alpha.path, &beta.path)?
|
||||
PathGraph::from_paths(&alpha_shape, &beta_shape)?
|
||||
} else {
|
||||
PathGraph::from_paths(&alpha.path, &beta_reverse)?
|
||||
PathGraph::from_paths(&alpha_shape, &beta_reverse)?
|
||||
};
|
||||
collect_shapes(&graph, &mut graph.get_cycles(), |_| true, |dir| if dir == alpha_dir { &alpha.style } else { &beta.style })
|
||||
}
|
||||
BooleanOperation::Intersection => {
|
||||
match if beta_dir == alpha_dir {
|
||||
PathGraph::from_paths(&alpha.path, &beta.path)
|
||||
PathGraph::from_paths(&alpha_shape, &beta_shape)
|
||||
} else {
|
||||
PathGraph::from_paths(&alpha.path, &beta_reverse)
|
||||
PathGraph::from_paths(&alpha_shape, &beta_reverse)
|
||||
} {
|
||||
Ok(graph) => {
|
||||
let mut cycles = graph.get_cycles();
|
||||
|
@ -610,10 +614,10 @@ pub fn boolean_operation(mut select: BooleanOperation, alpha: &mut ShapeLayer, b
|
|||
}
|
||||
Err(BooleanOperationError::NoIntersections) => {
|
||||
// Check could also be done with area and single ray cast
|
||||
if cast_horizontal_ray(point_on_curve(&beta.path), &alpha.path) % 2 != 0 {
|
||||
if cast_horizontal_ray(point_on_curve(&beta_shape), &alpha_shape) % 2 != 0 {
|
||||
beta.style = alpha.style.clone();
|
||||
Ok(vec![beta.clone()])
|
||||
} else if cast_horizontal_ray(point_on_curve(&alpha.path), &beta.path) % 2 != 0 {
|
||||
} else if cast_horizontal_ray(point_on_curve(&alpha_shape), &beta_shape) % 2 != 0 {
|
||||
Ok(vec![alpha.clone()])
|
||||
} else {
|
||||
Err(BooleanOperationError::NothingDone)
|
||||
|
@ -627,14 +631,14 @@ pub fn boolean_operation(mut select: BooleanOperation, alpha: &mut ShapeLayer, b
|
|||
}
|
||||
BooleanOperation::SubtractFront => {
|
||||
match if beta_dir != alpha_dir {
|
||||
PathGraph::from_paths(&alpha.path, &beta.path)
|
||||
PathGraph::from_paths(&alpha_shape, &beta_shape)
|
||||
} else {
|
||||
PathGraph::from_paths(&alpha.path, &beta_reverse)
|
||||
PathGraph::from_paths(&alpha_shape, &beta_reverse)
|
||||
} {
|
||||
Ok(graph) => collect_shapes(&graph, &mut graph.get_cycles(), |dir| dir == alpha_dir, |_| &alpha.style),
|
||||
Err(BooleanOperationError::NoIntersections) => {
|
||||
if cast_horizontal_ray(point_on_curve(&beta.path), &alpha.path) % 2 != 0 {
|
||||
add_subpath(&mut alpha.path, if beta_dir == alpha_dir { reverse_path(&beta.path) } else { beta.path.clone() });
|
||||
if cast_horizontal_ray(point_on_curve(&beta_shape), &alpha_shape) % 2 != 0 {
|
||||
add_subpath(&mut alpha_shape, if beta_dir == alpha_dir { reverse_path(&beta_shape) } else { beta_shape });
|
||||
Ok(vec![alpha.clone()])
|
||||
} else {
|
||||
Err(BooleanOperationError::NothingDone)
|
||||
|
@ -654,7 +658,7 @@ pub fn cast_horizontal_ray(from: Point, into: &BezPath) -> usize {
|
|||
});
|
||||
let mut intersects = Vec::new();
|
||||
for ref mut seg in into.segments() {
|
||||
if seg.bounding_box().x1 > from.x {
|
||||
if kurbo::ParamCurveExtrema::bounding_box(seg).x1 > from.x {
|
||||
line_curve_intersections((&mut ray, seg), |_, b| valid_t(b), &mut intersects);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
use crate::boolean_ops::composite_boolean_operation;
|
||||
use crate::intersection::Quad;
|
||||
use crate::layers;
|
||||
use crate::layers::folder_layer::FolderLayer;
|
||||
use crate::layers::image_layer::ImageLayer;
|
||||
use crate::layers::layer_info::{Layer, LayerData, LayerDataType, LayerDataTypeDiscriminant};
|
||||
use crate::layers::shape_layer::ShapeLayer;
|
||||
use crate::layers::style::RenderData;
|
||||
use crate::layers::text_layer::{Font, FontCache, TextLayer};
|
||||
use crate::layers::vector::vector_shape::VectorShape;
|
||||
use crate::{DocumentError, DocumentResponse, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::Affine;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::max;
|
||||
|
@ -100,7 +99,7 @@ impl Document {
|
|||
}
|
||||
|
||||
/// Returns a mutable reference to the layer or folder at the path.
|
||||
fn layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
|
||||
pub fn layer_mut(&mut self, path: &[LayerId]) -> Result<&mut Layer, DocumentError> {
|
||||
if path.is_empty() {
|
||||
return Ok(&mut self.root);
|
||||
}
|
||||
|
@ -117,7 +116,7 @@ impl Document {
|
|||
match (self.multiply_transforms(path), &self.layer(path)?.data) {
|
||||
(Ok(shape_transform), LayerDataType::Shape(shape)) => {
|
||||
let mut new_shape = shape.clone();
|
||||
new_shape.path.apply_affine(Affine::new((undo_viewport * shape_transform).to_cols_array()));
|
||||
new_shape.shape.apply_affine(undo_viewport * shape_transform);
|
||||
shapes.push(new_shape);
|
||||
}
|
||||
(Ok(_), _) => return Err(DocumentError::InvalidPath),
|
||||
|
@ -127,6 +126,43 @@ impl Document {
|
|||
Ok(shapes)
|
||||
}
|
||||
|
||||
/// Return a copy of all VectorShapes currently in the document.
|
||||
pub fn all_vector_shapes(&self) -> Vec<VectorShape> {
|
||||
self.root.iter().flat_map(|layer| layer.as_vector_shape_copy()).collect::<Vec<VectorShape>>()
|
||||
}
|
||||
|
||||
/// Returns references to all VectorShapes currently in the document.
|
||||
pub fn all_vector_shapes_ref(&self) -> Vec<&VectorShape> {
|
||||
self.root.iter().flat_map(|layer| layer.as_vector_shape()).collect::<Vec<&VectorShape>>()
|
||||
}
|
||||
|
||||
/// Returns a reference to the requested VectorShape by providing a path to its owner layer.
|
||||
pub fn vector_shape_ref<'a>(&'a self, path: &[LayerId]) -> Option<&'a VectorShape> {
|
||||
self.layer(path).ok()?.as_vector_shape()
|
||||
}
|
||||
|
||||
/// Returns a mutable reference of the requested VectorShape by providing a path to its owner layer.
|
||||
pub fn vector_shape_mut<'a>(&'a mut self, path: &'a [LayerId]) -> Option<&'a mut VectorShape> {
|
||||
self.layer_mut(path).ok()?.as_vector_shape_mut()
|
||||
}
|
||||
|
||||
/// Set a VectorShape at the specified path.
|
||||
pub fn set_vector_shape(&mut self, path: &[LayerId], shape: VectorShape) {
|
||||
let layer = self.layer_mut(path);
|
||||
if let Ok(layer) = layer {
|
||||
if let LayerDataType::Shape(shape_layer) = &mut layer.data {
|
||||
shape_layer.shape = shape;
|
||||
// Is this needed?
|
||||
layer.cache_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set VectorShapes for multiple paths at once.
|
||||
pub fn set_vector_shapes<'a>(&'a mut self, paths: impl Iterator<Item = &'a [LayerId]>, shapes: Vec<VectorShape>) {
|
||||
paths.zip(shapes).for_each(|(path, shape)| self.set_vector_shape(path, shape));
|
||||
}
|
||||
|
||||
pub fn common_layer_path_prefix<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> &'a [LayerId] {
|
||||
layers.reduce(|a, b| &a[..a.iter().zip(b.iter()).take_while(|&(a, b)| a == b).count()]).unwrap_or_default()
|
||||
}
|
||||
|
@ -467,15 +503,6 @@ impl Document {
|
|||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::AddOverlayEllipse { path, transform, style } => {
|
||||
let mut ellipse = ShapeLayer::ellipse(style);
|
||||
ellipse.render_index = -1;
|
||||
|
||||
let layer = Layer::new(LayerDataType::Shape(ellipse), transform);
|
||||
self.set_layer(&path, layer, -1)?;
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path }]].concat())
|
||||
}
|
||||
Operation::AddRect { path, insert_index, transform, style } => {
|
||||
let layer = Layer::new(LayerDataType::Shape(ShapeLayer::rectangle(style)), transform);
|
||||
|
||||
|
@ -483,15 +510,6 @@ impl Document {
|
|||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::AddOverlayRect { path, transform, style } => {
|
||||
let mut rect = ShapeLayer::rectangle(style);
|
||||
rect.render_index = -1;
|
||||
|
||||
let layer = Layer::new(LayerDataType::Shape(rect), transform);
|
||||
self.set_layer(&path, layer, -1)?;
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path }]].concat())
|
||||
}
|
||||
Operation::AddLine { path, insert_index, transform, style } => {
|
||||
let layer = Layer::new(LayerDataType::Shape(ShapeLayer::line(style)), transform);
|
||||
|
||||
|
@ -499,15 +517,6 @@ impl Document {
|
|||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::AddOverlayLine { path, transform, style } => {
|
||||
let mut line = ShapeLayer::line(style);
|
||||
line.render_index = -1;
|
||||
|
||||
let layer = Layer::new(LayerDataType::Shape(line), transform);
|
||||
self.set_layer(&path, layer, -1)?;
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path }]].concat())
|
||||
}
|
||||
Operation::AddText {
|
||||
path,
|
||||
insert_index,
|
||||
|
@ -577,24 +586,14 @@ impl Document {
|
|||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::AddOverlayShape { path, style, bez_path, closed } => {
|
||||
let mut shape = ShapeLayer::from_bez_path(bez_path, style, closed);
|
||||
shape.render_index = -1;
|
||||
|
||||
let layer = Layer::new(LayerDataType::Shape(shape), DAffine2::IDENTITY.to_cols_array());
|
||||
self.set_layer(&path, layer, -1)?;
|
||||
|
||||
Some([vec![DocumentChanged, CreatedLayer { path }]].concat())
|
||||
}
|
||||
Operation::AddShape {
|
||||
path,
|
||||
transform,
|
||||
insert_index,
|
||||
style,
|
||||
bez_path,
|
||||
closed,
|
||||
vector_path,
|
||||
} => {
|
||||
let shape = ShapeLayer::from_bez_path(bez_path, style, closed);
|
||||
let shape = ShapeLayer::new(vector_path, style);
|
||||
self.set_layer(&path, Layer::new(LayerDataType::Shape(shape), transform), insert_index)?;
|
||||
Some([vec![DocumentChanged, CreatedLayer { path }]].concat())
|
||||
}
|
||||
|
@ -759,36 +758,57 @@ impl Document {
|
|||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
Operation::SetShapePath { path, bez_path } => {
|
||||
Operation::SetShapePath { path, vector_path } => {
|
||||
self.mark_as_dirty(&path)?;
|
||||
|
||||
if let LayerDataType::Shape(shape) = &mut self.layer_mut(&path)?.data {
|
||||
shape.path = bez_path;
|
||||
shape.shape = vector_path;
|
||||
}
|
||||
Some(vec![DocumentChanged, LayerChanged { path }])
|
||||
}
|
||||
Operation::SetShapePathInViewport { path, bez_path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
self.set_transform_relative_to_viewport(&path, transform)?;
|
||||
self.mark_as_dirty(&path)?;
|
||||
|
||||
// Not using Document::layer_mut is necessary because we also need to borrow the font cache
|
||||
let mut current_folder = &mut self.root;
|
||||
let (folder_path, id) = split_path(&path)?;
|
||||
for id in folder_path {
|
||||
current_folder = current_folder.as_folder_mut()?.layer_mut(*id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?;
|
||||
Operation::InsertVectorAnchor { layer_path, anchor, after_id } => {
|
||||
if let Ok(Some(shape)) = self.layer_mut(&layer_path).map(|layer| layer.as_vector_shape_mut()) {
|
||||
shape.anchors_mut().insert(anchor, after_id);
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
}
|
||||
let layer_mut = current_folder.as_folder_mut()?.layer_mut(id).ok_or_else(|| DocumentError::LayerNotFound(folder_path.into()))?;
|
||||
|
||||
if let LayerDataType::Text(t) = &mut layer_mut.data {
|
||||
let bezpath = t.to_bez_path(t.load_face(font_cache));
|
||||
layer_mut.data = layers::layer_info::LayerDataType::Shape(ShapeLayer::from_bez_path(bezpath, t.path_style.clone(), true));
|
||||
Some([update_thumbnails_upstream(&layer_path), vec![DocumentChanged, LayerChanged { path: layer_path }]].concat())
|
||||
}
|
||||
Operation::PushVectorAnchor { layer_path, anchor } => {
|
||||
if let Ok(Some(shape)) = self.layer_mut(&layer_path).map(|layer| layer.as_vector_shape_mut()) {
|
||||
shape.anchors_mut().push(anchor);
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
}
|
||||
|
||||
if let LayerDataType::Shape(shape) = &mut layer_mut.data {
|
||||
shape.path = bez_path;
|
||||
Some([update_thumbnails_upstream(&layer_path), vec![DocumentChanged, LayerChanged { path: layer_path }]].concat())
|
||||
}
|
||||
Operation::RemoveVectorAnchor { layer_path, id } => {
|
||||
if let Ok(Some(shape)) = self.layer_mut(&layer_path).map(|layer| layer.as_vector_shape_mut()) {
|
||||
shape.anchors_mut().remove(id);
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
}
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
Some([update_thumbnails_upstream(&layer_path), vec![DocumentChanged, LayerChanged { path: layer_path }]].concat())
|
||||
}
|
||||
Operation::MoveVectorPoint {
|
||||
layer_path,
|
||||
id,
|
||||
control_type,
|
||||
position,
|
||||
} => {
|
||||
if let Ok(Some(shape)) = self.layer_mut(&layer_path).map(|layer| layer.as_vector_shape_mut()) {
|
||||
if let Some(anchor) = shape.anchors_mut().by_id_mut(id) {
|
||||
anchor.set_point_position(control_type as usize, position.into());
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
}
|
||||
}
|
||||
Some([update_thumbnails_upstream(&layer_path), vec![DocumentChanged, LayerChanged { path: layer_path }]].concat())
|
||||
}
|
||||
Operation::RemoveVectorPoint { layer_path, id, control_type } => {
|
||||
if let Ok(Some(shape)) = self.layer_mut(&layer_path).map(|layer| layer.as_vector_shape_mut()) {
|
||||
if let Some(anchor) = shape.anchors_mut().by_id_mut(id) {
|
||||
anchor.points[control_type as usize] = None;
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
}
|
||||
}
|
||||
Some([update_thumbnails_upstream(&layer_path), vec![DocumentChanged, LayerChanged { path: layer_path }]].concat())
|
||||
}
|
||||
Operation::TransformLayerInScope { path, transform, scope } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
|
@ -864,6 +884,84 @@ impl Document {
|
|||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
|
||||
// We may not want the concept of selection here. For now leaving though.
|
||||
Operation::SelectVectorPoints { layer_path, point_ids, add } => {
|
||||
let layer = self.layer_mut(&layer_path)?;
|
||||
if let Some(shape) = layer.as_vector_shape_mut() {
|
||||
if !add {
|
||||
shape.clear_selected_anchors();
|
||||
}
|
||||
shape.select_points(&point_ids, true);
|
||||
}
|
||||
Some(vec![LayerChanged { path: layer_path.clone() }])
|
||||
}
|
||||
Operation::DeselectVectorPoints { layer_path, point_ids } => {
|
||||
let layer = self.layer_mut(&layer_path)?;
|
||||
if let Some(shape) = layer.as_vector_shape_mut() {
|
||||
shape.select_points(&point_ids, false);
|
||||
}
|
||||
Some(vec![LayerChanged { path: layer_path.clone() }])
|
||||
}
|
||||
Operation::DeselectAllVectorPoints { layer_path } => {
|
||||
let layer = self.layer_mut(&layer_path)?;
|
||||
if let Some(shape) = layer.as_vector_shape_mut() {
|
||||
shape.clear_selected_anchors();
|
||||
}
|
||||
Some(vec![LayerChanged { path: layer_path.clone() }])
|
||||
}
|
||||
Operation::DeleteSelectedVectorPoints { layer_paths } => {
|
||||
let mut responses = vec![];
|
||||
for layer_path in layer_paths {
|
||||
let layer = self.layer_mut(&layer_path)?;
|
||||
if let Some(shape) = layer.as_vector_shape_mut() {
|
||||
// Delete the selected points.
|
||||
shape.delete_selected();
|
||||
|
||||
// Delete the layer if there are no longer any anchors
|
||||
if (shape.anchors().len() - 1) == 0 {
|
||||
self.delete(&layer_path)?;
|
||||
responses.push(DocumentChanged);
|
||||
responses.push(DocumentResponse::DeletedLayer { path: layer_path });
|
||||
return Ok(Some(responses));
|
||||
}
|
||||
|
||||
// If we still have anchors, update the layer and thumbnails
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
responses.push(DocumentChanged);
|
||||
responses.push(LayerChanged { path: layer_path.clone() });
|
||||
responses.append(&mut update_thumbnails_upstream(&layer_path));
|
||||
}
|
||||
}
|
||||
Some(responses)
|
||||
}
|
||||
Operation::MoveSelectedVectorPoints { layer_path, delta, absolute_position } => {
|
||||
if let Ok(viewspace) = self.generate_transform_relative_to_viewport(&layer_path) {
|
||||
let objectspace = &viewspace.inverse();
|
||||
let delta = objectspace.transform_vector2(DVec2::new(delta.0, delta.1));
|
||||
let absolute_position = objectspace.transform_point2(DVec2::new(absolute_position.0, absolute_position.1));
|
||||
let layer = self.layer_mut(&layer_path)?;
|
||||
if let Some(shape) = layer.as_vector_shape_mut() {
|
||||
shape.move_selected(delta, absolute_position, &viewspace);
|
||||
}
|
||||
}
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
Some([vec![DocumentChanged, LayerChanged { path: layer_path.clone() }], update_thumbnails_upstream(&layer_path)].concat())
|
||||
}
|
||||
Operation::SetSelectedHandleMirroring {
|
||||
layer_path,
|
||||
toggle_distance,
|
||||
toggle_angle,
|
||||
} => {
|
||||
let layer = self.layer_mut(&layer_path)?;
|
||||
if let Some(shape) = layer.as_vector_shape_mut() {
|
||||
for anchor in shape.selected_anchors_any_points_mut() {
|
||||
anchor.toggle_mirroring(toggle_distance, toggle_angle);
|
||||
}
|
||||
}
|
||||
// This does nothing visually so we don't need to send any messages
|
||||
None
|
||||
}
|
||||
};
|
||||
Ok(responses)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::boolean_ops::{split_path_seg, subdivide_path_seg};
|
||||
use crate::consts::{F64LOOSE, F64PRECISE};
|
||||
use crate::layers::vector::vector_shape::VectorShape;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, ParamCurveDeriv, ParamCurveExtrema, PathSeg, Point, QuadBez, Rect, Shape, Vec2};
|
||||
|
@ -37,6 +38,24 @@ impl Quad {
|
|||
path.close_path();
|
||||
path
|
||||
}
|
||||
|
||||
/// Generates a [VectorShape] of the quad
|
||||
pub fn vector_shape(&self) -> VectorShape {
|
||||
VectorShape::from_points(self.0.into_iter(), true)
|
||||
}
|
||||
|
||||
/// Generates the axis aligned bounding box of the quad
|
||||
pub fn bounding_box(&self) -> [DVec2; 2] {
|
||||
[
|
||||
self.0.into_iter().reduce(|a, b| a.min(b)).unwrap_or_default(),
|
||||
self.0.into_iter().reduce(|a, b| a.max(b)).unwrap_or_default(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Gets the center of a quad
|
||||
pub fn center(&self) -> DVec2 {
|
||||
self.0.iter().sum::<DVec2>() / 4.
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Quad> for DAffine2 {
|
||||
|
@ -73,7 +92,7 @@ pub fn intersect_quad_bez_path(quad: Quad, shape: &BezPath, filled: bool) -> boo
|
|||
return true;
|
||||
}
|
||||
// Check if selection is entirely within the shape
|
||||
if filled && shape.contains(to_point(quad.0[0])) {
|
||||
if filled && shape.contains(to_point(quad.center())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -843,8 +862,11 @@ mod tests {
|
|||
use super::*;
|
||||
#[allow(unused_imports)]
|
||||
use crate::boolean_ops::point_on_curve;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use std::{fs::File, io::Write};
|
||||
use std::fs::File;
|
||||
#[allow(unused_imports)]
|
||||
use std::io::Write;
|
||||
|
||||
/// Two intersect points, on different `PathSegs`.
|
||||
#[ignore]
|
||||
|
|
172
graphene/src/layers/id_vec.rs
Normal file
172
graphene/src/layers/id_vec.rs
Normal file
|
@ -0,0 +1,172 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
/// Brief description: A vec that allows indexing elements by both index and an assigned unique ID
|
||||
/// Goals of this Data Structure:
|
||||
/// - Drop-in replacement for a Vec.
|
||||
/// - Provide an auto-assigned Unique ID per element upon insertion.
|
||||
/// - Add elements to the start or end.
|
||||
/// - Insert element by Unique ID. Insert directly after an existing element by its Unique ID.
|
||||
/// - Access data by providing Unique ID.
|
||||
/// - Maintain ordering among the elements.
|
||||
/// - Remove elements without changing Unique IDs.
|
||||
/// This data structure is somewhat similar to a linked list in terms of invarients.
|
||||
/// The downside is that currently it requires a lot of iteration.
|
||||
|
||||
type ElementId = u64;
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct IdBackedVec<T> {
|
||||
/// Contained elements
|
||||
elements: Vec<T>,
|
||||
/// The IDs of the [Elements] contained within this
|
||||
element_ids: Vec<ElementId>,
|
||||
/// The ID that will be assigned to the next element that is added to this
|
||||
#[serde(skip)]
|
||||
next_id: ElementId,
|
||||
}
|
||||
|
||||
impl<T> IdBackedVec<T> {
|
||||
/// Push a new element to the start of the vector
|
||||
pub fn push_front(&mut self, element: T) -> Option<ElementId> {
|
||||
self.next_id += 1;
|
||||
self.elements.insert(0, element);
|
||||
self.element_ids.insert(0, self.next_id);
|
||||
Some(self.next_id)
|
||||
}
|
||||
|
||||
// Push an element to the end of the vector
|
||||
pub fn push_end(&mut self, element: T) -> Option<ElementId> {
|
||||
self.next_id += 1;
|
||||
self.elements.push(element);
|
||||
self.element_ids.push(self.next_id);
|
||||
Some(self.next_id)
|
||||
}
|
||||
|
||||
/// Insert an element adjacent to the given ID
|
||||
pub fn insert(&mut self, element: T, id: ElementId) -> Option<ElementId> {
|
||||
if let Some(index) = self.index_from_id(id) {
|
||||
self.next_id += 1;
|
||||
self.elements.insert(index, element);
|
||||
self.element_ids.insert(index, self.next_id);
|
||||
return Some(self.next_id);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Push an element to the end of the vector
|
||||
/// Overriden from Vec, so adding values without creating an id cannot occur
|
||||
pub fn push(&mut self, element: T) -> Option<ElementId> {
|
||||
self.push_end(element)
|
||||
}
|
||||
|
||||
/// Add a range of elements of elements to the end of this vector
|
||||
pub fn push_range<I>(&mut self, elements: I) -> Vec<ElementId>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
{
|
||||
let mut ids = vec![];
|
||||
for element in elements {
|
||||
if let Some(id) = self.push_end(element) {
|
||||
ids.push(id);
|
||||
}
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
/// Remove an element with a given element ID from the within this container.
|
||||
/// This operation will return false if the element ID is not found.
|
||||
/// Preserve unique ID lookup by using swap end and updating hashmap
|
||||
pub fn remove(&mut self, to_remove_id: ElementId) -> Option<T> {
|
||||
if let Some(index) = self.index_from_id(to_remove_id) {
|
||||
self.element_ids.remove(index);
|
||||
return Some(self.elements.remove(index));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get a single element with a given element ID from the within this container.
|
||||
pub fn by_id(&self, id: ElementId) -> Option<&T> {
|
||||
let index = self.index_from_id(id)?;
|
||||
Some(&self.elements[index])
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a single element with a given element ID from the within this container.
|
||||
pub fn by_id_mut(&mut self, id: ElementId) -> Option<&mut T> {
|
||||
let index = self.index_from_id(id)?;
|
||||
Some(&mut self.elements[index])
|
||||
}
|
||||
|
||||
/// Get an element based on its index
|
||||
pub fn by_index(&self, index: usize) -> Option<&T> {
|
||||
self.elements.get(index)
|
||||
}
|
||||
|
||||
/// Get a mutable element based on its index
|
||||
pub fn by_index_mut(&mut self, index: usize) -> Option<&mut T> {
|
||||
self.elements.get_mut(index)
|
||||
}
|
||||
|
||||
/// Clear the elements and unique ids
|
||||
pub fn clear(&mut self) {
|
||||
self.elements.clear();
|
||||
self.element_ids.clear();
|
||||
}
|
||||
|
||||
/// Enumerate the ids and elements in this container `(&ElementId, &T)`
|
||||
pub fn enumerate(&self) -> impl Iterator<Item = (&ElementId, &T)> {
|
||||
self.element_ids.iter().zip(self.elements.iter())
|
||||
}
|
||||
|
||||
/// Mutably Enumerate the ids and elements in this container `(&ElementId, &mut T)`
|
||||
pub fn enumerate_mut(&mut self) -> impl Iterator<Item = (&ElementId, &mut T)> {
|
||||
self.element_ids.iter().zip(self.elements.iter_mut())
|
||||
}
|
||||
|
||||
/// If this container contains an element with the given ID
|
||||
pub fn contains(&self, id: ElementId) -> bool {
|
||||
self.element_ids.contains(&id)
|
||||
}
|
||||
|
||||
/// Get the index of an element with the given ID
|
||||
pub fn index_from_id(&self, element_id: ElementId) -> Option<usize> {
|
||||
// Though this is a linear traversal, it is still likely faster than using a hashmap
|
||||
self.element_ids.iter().position(|&id| id == element_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for IdBackedVec<T> {
|
||||
fn default() -> Self {
|
||||
IdBackedVec {
|
||||
elements: vec![],
|
||||
element_ids: vec![],
|
||||
next_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows for usage of UniqueElements as a Vec<T>
|
||||
impl<T> Deref for IdBackedVec<T> {
|
||||
type Target = [T];
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.elements
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Consider removing this, it could allow for ElementIds and Elements to get out of sync
|
||||
/// Allows for mutable usage of UniqueElements as a Vec<T>
|
||||
impl<T> DerefMut for IdBackedVec<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.elements
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows use with iterators
|
||||
/// Also allows constructing UniqueElements with collect
|
||||
impl<A> FromIterator<A> for IdBackedVec<A> {
|
||||
fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
|
||||
let mut new = IdBackedVec::default();
|
||||
// Add to the end of the existing elements
|
||||
new.push_range(iter);
|
||||
new
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ use super::image_layer::ImageLayer;
|
|||
use super::shape_layer::ShapeLayer;
|
||||
use super::style::{PathStyle, RenderData};
|
||||
use super::text_layer::TextLayer;
|
||||
use super::vector::vector_shape::VectorShape;
|
||||
use crate::intersection::Quad;
|
||||
use crate::layers::text_layer::FontCache;
|
||||
use crate::DocumentError;
|
||||
|
@ -103,7 +104,7 @@ pub trait LayerData {
|
|||
/// assert_eq!(
|
||||
/// svg,
|
||||
/// "<g transform=\"matrix(\n1,-0,-0,1,-0,-0)\">\
|
||||
/// <path d=\"M0 0L1 0L1 1L0 1Z\" fill=\"none\" />\
|
||||
/// <path d=\"M0,0L0,1L1,1L1,0Z\" fill=\"none\" />\
|
||||
/// </g>"
|
||||
/// );
|
||||
/// ```
|
||||
|
@ -371,6 +372,27 @@ impl Layer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn as_vector_shape(&self) -> Option<&VectorShape> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Some(&s.shape),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vector_shape_copy(&self) -> Option<VectorShape> {
|
||||
match &self.data {
|
||||
LayerDataType::Shape(s) => Some(s.shape.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_vector_shape_mut(&mut self) -> Option<&mut VectorShape> {
|
||||
match &mut self.data {
|
||||
LayerDataType::Shape(s) => Some(&mut s.shape),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the Folder wrapped by the layer.
|
||||
/// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Folder`.
|
||||
pub fn as_folder(&self) -> Result<&FolderLayer, DocumentError> {
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
pub mod blend_mode;
|
||||
/// Contains the [FolderLayer](folder_layer::FolderLayer) type that encapsulates other layers, including more folders.
|
||||
pub mod folder_layer;
|
||||
pub mod id_vec;
|
||||
/// Contains the [ImageLayer](image_layer::ImageLayer) type that contains a bitmap image.
|
||||
pub mod image_layer;
|
||||
/// Contains the base [Layer](layer_info::Layer) type, an abstraction over the different types of layers.
|
||||
|
@ -27,3 +28,4 @@ pub mod shape_layer;
|
|||
pub mod style;
|
||||
/// Contains the [TextLayer](text_layer::TextLayer) type.
|
||||
pub mod text_layer;
|
||||
pub mod vector;
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
use super::layer_info::LayerData;
|
||||
use super::style::{self, PathStyle, RenderData, ViewMode};
|
||||
use super::vector::vector_shape::VectorShape;
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::layers::text_layer::FontCache;
|
||||
use crate::LayerId;
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{Affine, BezPath, Shape as KurboShape};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||
Affine::new(transform.to_cols_array())
|
||||
}
|
||||
|
||||
/// A generic SVG element defined using Bezier paths.
|
||||
/// Shapes are rendered as
|
||||
/// [`<path>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path)
|
||||
|
@ -21,21 +17,19 @@ fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
|||
/// group that the transformation matrix is applied to.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct ShapeLayer {
|
||||
/// A Bezier path.
|
||||
pub path: BezPath,
|
||||
/// The geometry of the layer.
|
||||
pub shape: VectorShape,
|
||||
/// The visual style of the shape.
|
||||
pub style: style::PathStyle,
|
||||
// TODO: We might be able to remove this in a future refactor
|
||||
pub render_index: i32,
|
||||
/// Whether or not the [path](ShapeLayer::path) connects to itself.
|
||||
pub closed: bool,
|
||||
}
|
||||
|
||||
impl LayerData for ShapeLayer {
|
||||
fn render(&mut self, svg: &mut String, svg_defs: &mut String, transforms: &mut Vec<DAffine2>, render_data: RenderData) {
|
||||
let mut path = self.path.clone();
|
||||
let mut vector_shape = self.shape.clone();
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = vector_shape.bounding_box();
|
||||
let layer_bounds = [(x0, y0).into(), (x1, y1).into()];
|
||||
|
||||
let transform = self.transform(transforms, render_data.view_mode);
|
||||
|
@ -44,9 +38,9 @@ impl LayerData for ShapeLayer {
|
|||
let _ = write!(svg, "<!-- SVG shape has an invalid transform -->");
|
||||
return;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
vector_shape.apply_affine(transform);
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = vector_shape.bounding_box();
|
||||
let transformed_bounds = [(x0, y0).into(), (x1, y1).into()];
|
||||
|
||||
let _ = writeln!(svg, r#"<g transform="matrix("#);
|
||||
|
@ -57,33 +51,37 @@ impl LayerData for ShapeLayer {
|
|||
let _ = write!(
|
||||
svg,
|
||||
r#"<path d="{}" {} />"#,
|
||||
path.to_svg(),
|
||||
vector_shape.to_svg(),
|
||||
self.style.render(render_data.view_mode, svg_defs, transform, layer_bounds, transformed_bounds)
|
||||
);
|
||||
let _ = svg.write_str("</g>");
|
||||
}
|
||||
|
||||
fn bounding_box(&self, transform: glam::DAffine2, _font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
use kurbo::Shape;
|
||||
|
||||
let mut path = self.path.clone();
|
||||
let mut vector_shape = self.shape.clone();
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
vector_shape.apply_affine(transform);
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = vector_shape.bounding_box();
|
||||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, _font_cache: &FontCache) {
|
||||
if intersect_quad_bez_path(quad, &self.path, self.style.fill().is_some()) {
|
||||
let filled = self.style.fill().is_some() || self.shape.anchors().last().filter(|anchor| anchor.is_close()).is_some();
|
||||
if intersect_quad_bez_path(quad, &(&self.shape).into(), filled) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShapeLayer {
|
||||
/// Construct a new [ShapeLayer] with the specified [VectorShape] and [PathStyle]
|
||||
pub fn new(shape: VectorShape, style: PathStyle) -> Self {
|
||||
Self { shape, style, render_index: 1 }
|
||||
}
|
||||
|
||||
pub fn transform(&self, transforms: &[DAffine2], mode: ViewMode) -> DAffine2 {
|
||||
let start = match (mode, self.render_index) {
|
||||
(ViewMode::Outline, _) => 0,
|
||||
|
@ -93,15 +91,7 @@ impl ShapeLayer {
|
|||
transforms.iter().skip(start).fold(DAffine2::IDENTITY, |a, b| a * *b)
|
||||
}
|
||||
|
||||
pub fn from_bez_path(bez_path: BezPath, style: PathStyle, closed: bool) -> Self {
|
||||
Self {
|
||||
path: bez_path,
|
||||
style,
|
||||
render_index: 1,
|
||||
closed,
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO The behavior of ngon changed from the previous iteration slightly, match original behavior
|
||||
/// Create an N-gon.
|
||||
///
|
||||
/// # Panics
|
||||
|
@ -132,136 +122,55 @@ impl ShapeLayer {
|
|||
path.close_path();
|
||||
|
||||
Self {
|
||||
path,
|
||||
shape: VectorShape::new_ngon(DVec2::new(0., 0.), sides.into(), 1.),
|
||||
style,
|
||||
render_index: 1,
|
||||
closed: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a rectangular shape.
|
||||
pub fn rectangle(style: PathStyle) -> Self {
|
||||
Self {
|
||||
path: kurbo::Rect::new(0., 0., 1., 1.).to_path(0.01),
|
||||
shape: VectorShape::new_rect(DVec2::new(0., 0.), DVec2::new(1., 1.)),
|
||||
style,
|
||||
render_index: 1,
|
||||
closed: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an elliptical shape.
|
||||
pub fn ellipse(style: PathStyle) -> Self {
|
||||
Self {
|
||||
path: kurbo::Ellipse::from_rect(kurbo::Rect::new(0., 0., 1., 1.)).to_path(0.01),
|
||||
shape: VectorShape::new_ellipse(DVec2::new(0., 0.), DVec2::new(1., 1.)),
|
||||
style,
|
||||
render_index: 1,
|
||||
closed: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a straight line from (0, 0) to (1, 0).
|
||||
pub fn line(style: PathStyle) -> Self {
|
||||
Self {
|
||||
path: kurbo::Line::new((0., 0.), (1., 0.)).to_path(0.01),
|
||||
shape: VectorShape::new_line(DVec2::new(0., 0.), DVec2::new(1., 0.)),
|
||||
style,
|
||||
render_index: 1,
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a polygonal line that visits each provided point.
|
||||
pub fn poly_line(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
|
||||
let mut path = kurbo::BezPath::new();
|
||||
points
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.map(|v: DVec2| kurbo::Point { x: v.x, y: v.y })
|
||||
.enumerate()
|
||||
.for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) });
|
||||
|
||||
Self {
|
||||
path,
|
||||
shape: VectorShape::new_poly_line(points),
|
||||
style,
|
||||
render_index: 0,
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a smooth bezier spline that passes through all given points.
|
||||
/// The algorithm used in this implementation is described here: <https://www.particleincell.com/2012/bezier-splines/>
|
||||
pub fn spline(points: Vec<impl Into<glam::DVec2>>, style: PathStyle) -> Self {
|
||||
let mut path = kurbo::BezPath::new();
|
||||
|
||||
// Creating a bezier spline is only necessary for 3 or more points.
|
||||
// For 2 given points a line segment is created instead.
|
||||
if points.len() > 2 {
|
||||
let points: Vec<_> = points.into_iter().map(|v| v.into()).map(|v: DVec2| kurbo::Vec2 { x: v.x, y: v.y }).collect();
|
||||
|
||||
// Number of bezier segments
|
||||
let n = points.len() - 1;
|
||||
|
||||
// Control points for each bezier segment
|
||||
let mut p1 = vec![kurbo::Vec2::ZERO; n];
|
||||
let mut p2 = vec![kurbo::Vec2::ZERO; n];
|
||||
|
||||
// Tri-diagonal matrix coefficients a, b and c (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
|
||||
let mut a = vec![1.0; n];
|
||||
a[0] = 0.0;
|
||||
a[n - 1] = 2.0;
|
||||
|
||||
let mut b = vec![4.0; n];
|
||||
b[0] = 2.0;
|
||||
b[n - 1] = 7.0;
|
||||
|
||||
let mut c = vec![1.0; n];
|
||||
c[n - 1] = 0.0;
|
||||
|
||||
let mut r: Vec<_> = (0..n).map(|i| 4.0 * points[i] + 2.0 * points[i + 1]).collect();
|
||||
r[0] = points[0] + (2.0 * points[1]);
|
||||
r[n - 1] = 8.0 * points[n - 1] + points[n];
|
||||
|
||||
// Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
|
||||
for i in 1..n {
|
||||
let m = a[i] / b[i - 1];
|
||||
b[i] -= m * c[i - 1];
|
||||
let last_iteration_r = r[i - 1];
|
||||
r[i] -= m * last_iteration_r;
|
||||
}
|
||||
|
||||
// Determine first control point for each segment
|
||||
p1[n - 1] = r[n - 1] / b[n - 1];
|
||||
for i in (0..n - 1).rev() {
|
||||
p1[i] = (r[i] - c[i] * p1[i + 1]) / b[i];
|
||||
}
|
||||
|
||||
// Determine second control point per segment from first
|
||||
for i in 0..n - 1 {
|
||||
p2[i] = 2.0 * points[i + 1] - p1[i + 1];
|
||||
}
|
||||
p2[n - 1] = 0.5 * (points[n] + p1[n - 1]);
|
||||
|
||||
// Create bezier path from given points and computed control points
|
||||
points.into_iter().enumerate().for_each(|(i, p)| {
|
||||
if i == 0 {
|
||||
path.move_to(p.to_point())
|
||||
} else {
|
||||
path.curve_to(p1[i - 1].to_point(), p2[i - 1].to_point(), p.to_point())
|
||||
}
|
||||
});
|
||||
} else {
|
||||
points
|
||||
.into_iter()
|
||||
.map(|v| v.into())
|
||||
.map(|v: DVec2| kurbo::Point { x: v.x, y: v.y })
|
||||
.enumerate()
|
||||
.for_each(|(i, p)| if i == 0 { path.move_to(p) } else { path.line_to(p) });
|
||||
}
|
||||
|
||||
Self {
|
||||
path,
|
||||
shape: VectorShape::new_spline(points),
|
||||
style,
|
||||
render_index: 0,
|
||||
closed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
use super::layer_info::LayerData;
|
||||
use super::style::{PathStyle, RenderData, ViewMode};
|
||||
use super::vector::vector_shape::VectorShape;
|
||||
use crate::intersection::{intersect_quad_bez_path, Quad};
|
||||
use crate::LayerId;
|
||||
pub use font_cache::{Font, FontCache};
|
||||
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{Affine, BezPath, Rect, Shape};
|
||||
use rustybuzz::Face;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
mod font_cache;
|
||||
mod to_kurbo;
|
||||
|
||||
fn glam_to_kurbo(transform: DAffine2) -> Affine {
|
||||
Affine::new(transform.to_cols_array())
|
||||
}
|
||||
mod to_path;
|
||||
|
||||
/// A line, or multiple lines, of text drawn in the document.
|
||||
/// Like [ShapeLayers](super::shape_layer::ShapeLayer), [TextLayer] are rendered as
|
||||
|
@ -33,7 +29,7 @@ pub struct TextLayer {
|
|||
#[serde(skip)]
|
||||
pub editable: bool,
|
||||
#[serde(skip)]
|
||||
pub cached_path: Option<BezPath>,
|
||||
pub cached_path: Option<VectorShape>,
|
||||
}
|
||||
|
||||
impl LayerData for TextLayer {
|
||||
|
@ -72,12 +68,12 @@ impl LayerData for TextLayer {
|
|||
} else {
|
||||
let buzz_face = self.load_face(render_data.font_cache);
|
||||
|
||||
let mut path = self.to_bez_path(buzz_face);
|
||||
let mut path = self.to_vector_path(buzz_face);
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
let bounds = [(x0, y0).into(), (x1, y1).into()];
|
||||
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
path.apply_affine(transform);
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
let transformed_bounds = [(x0, y0).into(), (x1, y1).into()];
|
||||
|
@ -95,21 +91,17 @@ impl LayerData for TextLayer {
|
|||
fn bounding_box(&self, transform: glam::DAffine2, font_cache: &FontCache) -> Option<[DVec2; 2]> {
|
||||
let buzz_face = Some(self.load_face(font_cache)?);
|
||||
|
||||
let mut path = self.bounding_box(&self.text, buzz_face).to_path(0.1);
|
||||
|
||||
if transform.matrix2 == DMat2::ZERO {
|
||||
return None;
|
||||
}
|
||||
path.apply_affine(glam_to_kurbo(transform));
|
||||
|
||||
let kurbo::Rect { x0, y0, x1, y1 } = path.bounding_box();
|
||||
Some([(x0, y0).into(), (x1, y1).into()])
|
||||
Some((transform * self.bounding_box(&self.text, buzz_face)).bounding_box())
|
||||
}
|
||||
|
||||
fn intersects_quad(&self, quad: Quad, path: &mut Vec<LayerId>, intersections: &mut Vec<Vec<LayerId>>, font_cache: &FontCache) {
|
||||
let buzz_face = self.load_face(font_cache);
|
||||
|
||||
if intersect_quad_bez_path(quad, &self.bounding_box(&self.text, buzz_face).to_path(0.), true) {
|
||||
if intersect_quad_bez_path(quad, &self.bounding_box(&self.text, buzz_face).path(), true) {
|
||||
intersections.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
@ -144,10 +136,10 @@ impl TextLayer {
|
|||
new
|
||||
}
|
||||
|
||||
/// Converts to a [BezPath], populating the cache if necessary.
|
||||
/// Converts to a [VectorShape], populating the cache if necessary.
|
||||
#[inline]
|
||||
pub fn to_bez_path(&mut self, buzz_face: Option<Face>) -> BezPath {
|
||||
if self.cached_path.as_ref().filter(|x| !x.is_empty()).is_none() {
|
||||
pub fn to_vector_path(&mut self, buzz_face: Option<Face>) -> VectorShape {
|
||||
if self.cached_path.as_ref().filter(|x| !x.anchors().is_empty()).is_none() {
|
||||
let path = self.generate_path(buzz_face);
|
||||
self.cached_path = Some(path.clone());
|
||||
return path;
|
||||
|
@ -155,23 +147,23 @@ impl TextLayer {
|
|||
self.cached_path.clone().unwrap()
|
||||
}
|
||||
|
||||
/// Converts to a [BezPath], without populating the cache.
|
||||
/// Converts to a [VectorShape], without populating the cache.
|
||||
#[inline]
|
||||
pub fn to_bez_path_nonmut(&self, font_cache: &FontCache) -> BezPath {
|
||||
pub fn to_vector_path_nonmut(&self, font_cache: &FontCache) -> VectorShape {
|
||||
let buzz_face = self.load_face(font_cache);
|
||||
|
||||
self.cached_path.clone().filter(|x| !x.is_empty()).unwrap_or_else(|| self.generate_path(buzz_face))
|
||||
self.cached_path.clone().filter(|x| !x.anchors().is_empty()).unwrap_or_else(|| self.generate_path(buzz_face))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn generate_path(&self, buzz_face: Option<Face>) -> BezPath {
|
||||
to_kurbo::to_kurbo(&self.text, buzz_face, self.size, self.line_width)
|
||||
pub fn generate_path(&self, buzz_face: Option<Face>) -> VectorShape {
|
||||
to_path::to_path(&self.text, buzz_face, self.size, self.line_width)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bounding_box(&self, text: &str, buzz_face: Option<Face>) -> Rect {
|
||||
let far = to_kurbo::bounding_box(text, buzz_face, self.size, self.line_width);
|
||||
Rect::new(0., 0., far.x, far.y)
|
||||
pub fn bounding_box(&self, text: &str, buzz_face: Option<Face>) -> Quad {
|
||||
let far = to_path::bounding_box(text, buzz_face, self.size, self.line_width);
|
||||
Quad::from_box([DVec2::ZERO, far])
|
||||
}
|
||||
|
||||
pub fn update_text(&mut self, text: String, font_cache: &FontCache) {
|
||||
|
|
|
@ -1,42 +1,55 @@
|
|||
use crate::layers::vector::constants::ControlPointType;
|
||||
use crate::layers::vector::vector_anchor::VectorAnchor;
|
||||
use crate::layers::vector::vector_control_point::VectorControlPoint;
|
||||
use crate::layers::vector::vector_shape::VectorShape;
|
||||
|
||||
use glam::DVec2;
|
||||
use kurbo::{BezPath, Point, Vec2};
|
||||
use rustybuzz::{GlyphBuffer, UnicodeBuffer};
|
||||
use ttf_parser::{GlyphId, OutlineBuilder};
|
||||
|
||||
struct Builder {
|
||||
path: BezPath,
|
||||
pos: Point,
|
||||
offset: Vec2,
|
||||
path: VectorShape,
|
||||
pos: DVec2,
|
||||
offset: DVec2,
|
||||
ascender: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
fn point(&self, x: f32, y: f32) -> DVec2 {
|
||||
self.pos + self.offset + DVec2::new(x as f64, self.ascender - y as f64) * self.scale
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineBuilder for Builder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
self.path.move_to(self.pos + self.offset + Vec2::new(x as f64, self.ascender - y as f64) * self.scale);
|
||||
let anchor = self.point(x, y);
|
||||
if self.path.anchors().last().filter(|el| el.points.iter().any(Option::is_some)).is_some() {
|
||||
self.path.anchors_mut().push_end(VectorAnchor::closed());
|
||||
}
|
||||
self.path.anchors_mut().push_end(VectorAnchor::new(anchor));
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.path.line_to(self.pos + self.offset + Vec2::new(x as f64, self.ascender - y as f64) * self.scale);
|
||||
let anchor = self.point(x, y);
|
||||
self.path.anchors_mut().push_end(VectorAnchor::new(anchor));
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
self.path.quad_to(
|
||||
self.pos + self.offset + Vec2::new(x1 as f64, self.ascender - y1 as f64) * self.scale,
|
||||
self.pos + self.offset + Vec2::new(x2 as f64, self.ascender - y2 as f64) * self.scale,
|
||||
);
|
||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
||||
self.path.anchors_mut().last_mut().unwrap().points[ControlPointType::OutHandle] = Some(VectorControlPoint::new(handle, ControlPointType::OutHandle));
|
||||
self.path.anchors_mut().push_end(VectorAnchor::new(anchor));
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||
self.path.curve_to(
|
||||
self.pos + self.offset + Vec2::new(x1 as f64, self.ascender - y1 as f64) * self.scale,
|
||||
self.pos + self.offset + Vec2::new(x2 as f64, self.ascender - y2 as f64) * self.scale,
|
||||
self.pos + self.offset + Vec2::new(x3 as f64, self.ascender - y3 as f64) * self.scale,
|
||||
);
|
||||
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
||||
self.path.anchors_mut().last_mut().unwrap().points[ControlPointType::OutHandle] = Some(VectorControlPoint::new(handle1, ControlPointType::OutHandle));
|
||||
self.path.anchors_mut().push_end(VectorAnchor::new(anchor));
|
||||
self.path.anchors_mut().last_mut().unwrap().points[ControlPointType::InHandle] = Some(VectorControlPoint::new(handle2, ControlPointType::InHandle));
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.path.close_path();
|
||||
self.path.anchors_mut().push_end(VectorAnchor::closed());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,19 +80,19 @@ fn wrap_word(line_width: Option<f64>, glyph_buffer: &GlyphBuffer, scale: f64, x_
|
|||
false
|
||||
}
|
||||
|
||||
pub fn to_kurbo(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> BezPath {
|
||||
pub fn to_path(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, line_width: Option<f64>) -> VectorShape {
|
||||
let buzz_face = match buzz_face {
|
||||
Some(face) => face,
|
||||
// Show blank layer if font has not loaded
|
||||
None => return BezPath::default(),
|
||||
None => return VectorShape::default(),
|
||||
};
|
||||
|
||||
let (scale, line_height, mut buffer) = font_properties(&buzz_face, font_size);
|
||||
|
||||
let mut builder = Builder {
|
||||
path: BezPath::new(),
|
||||
pos: Point::ZERO,
|
||||
offset: Vec2::ZERO,
|
||||
path: VectorShape::new(),
|
||||
pos: DVec2::ZERO,
|
||||
offset: DVec2::ZERO,
|
||||
ascender: (buzz_face.ascender() as f64 / buzz_face.height() as f64) * font_size / scale,
|
||||
scale,
|
||||
};
|
||||
|
@ -91,23 +104,23 @@ pub fn to_kurbo(str: &str, buzz_face: Option<rustybuzz::Face>, font_size: f64, l
|
|||
let glyph_buffer = rustybuzz::shape(&buzz_face, &[], buffer);
|
||||
|
||||
if wrap_word(line_width, &glyph_buffer, scale, builder.pos.x) {
|
||||
builder.pos = Point::new(0., builder.pos.y + line_height);
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
|
||||
for (glyph_position, glyph_info) in glyph_buffer.glyph_positions().iter().zip(glyph_buffer.glyph_infos()) {
|
||||
if let Some(line_width) = line_width {
|
||||
if builder.pos.x + (glyph_position.x_advance as f64 * builder.scale) >= line_width {
|
||||
builder.pos = Point::new(0., builder.pos.y + line_height);
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
}
|
||||
builder.offset = Vec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale;
|
||||
builder.offset = DVec2::new(glyph_position.x_offset as f64, glyph_position.y_offset as f64) * builder.scale;
|
||||
buzz_face.outline_glyph(GlyphId(glyph_info.glyph_id as u16), &mut builder);
|
||||
builder.pos += Vec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * builder.scale;
|
||||
builder.pos += DVec2::new(glyph_position.x_advance as f64, glyph_position.y_advance as f64) * builder.scale;
|
||||
}
|
||||
|
||||
buffer = glyph_buffer.clear();
|
||||
}
|
||||
builder.pos = Point::new(0., builder.pos.y + line_height);
|
||||
builder.pos = DVec2::new(0., builder.pos.y + line_height);
|
||||
}
|
||||
builder.path
|
||||
}
|
50
graphene/src/layers/vector/constants.rs
Normal file
50
graphene/src/layers/vector/constants.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use std::ops::{Index, IndexMut, Not};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[repr(usize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy, Serialize, Deserialize)]
|
||||
pub enum ControlPointType {
|
||||
Anchor = 0,
|
||||
InHandle = 1,
|
||||
OutHandle = 2,
|
||||
}
|
||||
|
||||
impl ControlPointType {
|
||||
pub fn from_index(index: usize) -> ControlPointType {
|
||||
match index {
|
||||
0 => ControlPointType::Anchor,
|
||||
1 => ControlPointType::InHandle,
|
||||
2 => ControlPointType::OutHandle,
|
||||
_ => ControlPointType::Anchor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Not for ControlPointType {
|
||||
type Output = Self;
|
||||
fn not(self) -> Self::Output {
|
||||
match self {
|
||||
ControlPointType::InHandle => ControlPointType::OutHandle,
|
||||
ControlPointType::OutHandle => ControlPointType::InHandle,
|
||||
_ => ControlPointType::Anchor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ControlPointType for indexing, mutably
|
||||
impl<T> IndexMut<ControlPointType> for [T; 3] {
|
||||
fn index_mut(&mut self, mt: ControlPointType) -> &mut T {
|
||||
&mut self[mt as usize]
|
||||
}
|
||||
}
|
||||
|
||||
// Remove when no longer needed
|
||||
pub const SELECTION_THRESHOLD: f64 = 10.;
|
4
graphene/src/layers/vector/mod.rs
Normal file
4
graphene/src/layers/vector/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod constants;
|
||||
pub mod vector_anchor;
|
||||
pub mod vector_control_point;
|
||||
pub mod vector_shape;
|
301
graphene/src/layers/vector/vector_anchor.rs
Normal file
301
graphene/src/layers/vector/vector_anchor.rs
Normal file
|
@ -0,0 +1,301 @@
|
|||
use super::{
|
||||
constants::{ControlPointType, SELECTION_THRESHOLD},
|
||||
vector_control_point::VectorControlPoint,
|
||||
};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Brief overview of VectorAnchor
|
||||
/// 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 InHandle's draggable point, [2] is the OutHandle's draggable point
|
||||
/// / | \
|
||||
/// "Anchor" "InHandle" "OutHandle" <- These are VectorControlPoints and the only editable "primitive"
|
||||
|
||||
/// VectorAnchor is used to represent an anchor point + handles on the path that can be moved.
|
||||
/// It contains 0-2 handles that are optionally available.
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Default)]
|
||||
pub struct VectorAnchor {
|
||||
// Editable points for the anchor & handles
|
||||
pub points: [Option<VectorControlPoint>; 3],
|
||||
|
||||
#[serde(skip)]
|
||||
// The editor state of the anchor and handles
|
||||
pub editor_state: VectorAnchorState,
|
||||
}
|
||||
|
||||
impl VectorAnchor {
|
||||
/// Create a new anchor with the given position
|
||||
pub fn new(anchor_pos: DVec2) -> Self {
|
||||
Self {
|
||||
// An anchor and 2x None's which represent non-existent handles
|
||||
points: [Some(VectorControlPoint::new(anchor_pos, ControlPointType::Anchor)), None, None],
|
||||
editor_state: VectorAnchorState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new anchor with the given anchor position and handles
|
||||
pub fn new_with_handles(anchor_pos: DVec2, handle_in_pos: Option<DVec2>, handle_out_pos: Option<DVec2>) -> Self {
|
||||
Self {
|
||||
points: match (handle_in_pos, handle_out_pos) {
|
||||
(Some(pos1), Some(pos2)) => [
|
||||
Some(VectorControlPoint::new(anchor_pos, ControlPointType::Anchor)),
|
||||
Some(VectorControlPoint::new(pos1, ControlPointType::InHandle)),
|
||||
Some(VectorControlPoint::new(pos2, ControlPointType::OutHandle)),
|
||||
],
|
||||
(None, Some(pos2)) => [
|
||||
Some(VectorControlPoint::new(anchor_pos, ControlPointType::Anchor)),
|
||||
None,
|
||||
Some(VectorControlPoint::new(pos2, ControlPointType::OutHandle)),
|
||||
],
|
||||
(Some(pos1), None) => [
|
||||
Some(VectorControlPoint::new(anchor_pos, ControlPointType::Anchor)),
|
||||
Some(VectorControlPoint::new(pos1, ControlPointType::InHandle)),
|
||||
None,
|
||||
],
|
||||
(None, None) => [Some(VectorControlPoint::new(anchor_pos, ControlPointType::Anchor)), None, None],
|
||||
},
|
||||
editor_state: VectorAnchorState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a VectorAnchor that represents a close path signal
|
||||
pub fn closed() -> Self {
|
||||
Self {
|
||||
// An anchor being None indicates a ClosePath (aka a path end)
|
||||
points: [None, None, None],
|
||||
editor_state: VectorAnchorState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Does this [VectorAnchor] represent a close signal?
|
||||
pub fn is_close(&self) -> bool {
|
||||
self.points[ControlPointType::Anchor].is_none() && self.points[ControlPointType::InHandle].is_none()
|
||||
}
|
||||
|
||||
/// Finds the closest VectorControlPoint owned by this anchor. This can be the handles or the anchor itself
|
||||
pub fn closest_point(&self, transform_space: &DAffine2, 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 = transform_space.transform_point2(point.position).distance_squared(target);
|
||||
if distance_squared < closest_distance_squared {
|
||||
closest_distance_squared = distance_squared;
|
||||
closest_index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
closest_index
|
||||
}
|
||||
|
||||
/// Move the selected points by the provided transform
|
||||
pub fn move_selected_points(&mut self, delta: DVec2, absolute_position: DVec2, viewspace: &DAffine2) {
|
||||
let mirror_angle = self.editor_state.mirror_angle_between_handles;
|
||||
// Invert distance since we want it to start disabled
|
||||
let mirror_distance = !self.editor_state.mirror_distance_between_handles;
|
||||
|
||||
// TODO Use an ID as opposed to distance, stopgap for now
|
||||
// Transformed into viewspace so SELECTION_THRESHOLD is in pixels
|
||||
let is_drag_target = |point: &mut VectorControlPoint| -> bool { viewspace.transform_point2(absolute_position).distance(viewspace.transform_point2(point.position)) < SELECTION_THRESHOLD };
|
||||
|
||||
// Move the point absolutely or relatively depending on if the point is under the cursor (the last selected point)
|
||||
let move_point = |point: &mut VectorControlPoint, delta: DVec2, absolute_position: DVec2| {
|
||||
if is_drag_target(point) {
|
||||
point.position = absolute_position;
|
||||
} else {
|
||||
point.position += delta;
|
||||
}
|
||||
assert!(point.position.is_finite(), "Point is not finite")
|
||||
};
|
||||
|
||||
// Find the correctly mirrored handle position based on mirroring settings
|
||||
let move_symmetrical_handle = |position: DVec2, opposing_handle: Option<&mut VectorControlPoint>, center: DVec2| {
|
||||
// Early out for cases where we can't mirror
|
||||
if !mirror_angle || opposing_handle.is_none() {
|
||||
return;
|
||||
}
|
||||
let opposing_handle = opposing_handle.unwrap();
|
||||
|
||||
// Keep rotational similarity, but distance variable
|
||||
let radius = if mirror_distance { center.distance(position) } else { center.distance(opposing_handle.position) };
|
||||
|
||||
if let Some(offset) = (position - center).try_normalize() {
|
||||
opposing_handle.position = center - offset * radius;
|
||||
assert!(opposing_handle.position.is_finite(), "Oposing handle not finite")
|
||||
}
|
||||
};
|
||||
|
||||
// If no points are selected, why are we here at all?
|
||||
if !self.any_points_selected() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the anchor is selected ignore any handle mirroring / dragging
|
||||
// Drag all points
|
||||
if self.is_anchor_selected() {
|
||||
for point in self.points_mut() {
|
||||
move_point(point, delta, absolute_position);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the anchor isn't selected, but both handles are
|
||||
// Drag only handles
|
||||
if self.both_handles_selected() {
|
||||
for point in self.selected_handles_mut() {
|
||||
move_point(point, delta, absolute_position);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the anchor isn't selected, and only one handle is selected
|
||||
// Drag the single handle
|
||||
let reflect_center = self.points[ControlPointType::Anchor].as_ref().unwrap().position;
|
||||
let selected_handle = self.selected_handles_mut().next().unwrap();
|
||||
move_point(selected_handle, delta, absolute_position);
|
||||
|
||||
// Move the opposing handle symmetrically if our mirroring flags allow
|
||||
let selected_handle = &selected_handle.clone();
|
||||
let opposing_handle = self.opposing_handle_mut(selected_handle);
|
||||
move_symmetrical_handle(selected_handle.position, opposing_handle, reflect_center);
|
||||
}
|
||||
|
||||
/// Delete any VectorControlPoint that are selected, this includes handles or the anchor
|
||||
pub fn delete_selected(&mut self) {
|
||||
for point_option in self.points.iter_mut() {
|
||||
if let Some(point) = point_option {
|
||||
if point.editor_state.is_selected {
|
||||
*point_option = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if any points in this anchor are selected
|
||||
pub fn any_points_selected(&self) -> bool {
|
||||
self.points.iter().flatten().any(|pnt| pnt.editor_state.is_selected)
|
||||
}
|
||||
|
||||
/// Returns true if the anchor point is selected
|
||||
pub fn is_anchor_selected(&self) -> bool {
|
||||
if let Some(anchor) = &self.points[0] {
|
||||
anchor.editor_state.is_selected
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines if two handle points are selected
|
||||
pub fn both_handles_selected(&self) -> bool {
|
||||
self.points.iter().skip(1).flatten().filter(|pnt| pnt.editor_state.is_selected).count() == 2
|
||||
}
|
||||
|
||||
/// Set a point to selected by ID
|
||||
pub fn select_point(&mut self, point_id: usize, selected: bool) -> Option<&mut VectorControlPoint> {
|
||||
if let Some(point) = self.points[point_id].as_mut() {
|
||||
point.set_selected(selected);
|
||||
}
|
||||
self.points[point_id].as_mut()
|
||||
}
|
||||
|
||||
/// Clear the selected points for this anchor
|
||||
pub fn clear_selected_points(&mut self) {
|
||||
for point in self.points.iter_mut().flatten() {
|
||||
point.set_selected(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the points in this anchor
|
||||
pub fn points(&self) -> impl Iterator<Item = &VectorControlPoint> {
|
||||
self.points.iter().flatten()
|
||||
}
|
||||
|
||||
/// Provides the points in this anchor
|
||||
pub fn points_mut(&mut self) -> impl Iterator<Item = &mut VectorControlPoint> {
|
||||
self.points.iter_mut().flatten()
|
||||
}
|
||||
|
||||
/// Provides the selected points in this anchor
|
||||
pub fn selected_points(&self) -> impl Iterator<Item = &VectorControlPoint> {
|
||||
self.points.iter().flatten().filter(|pnt| pnt.editor_state.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.editor_state.is_selected)
|
||||
}
|
||||
|
||||
/// Provides the selected handles attached to this anchor
|
||||
pub fn selected_handles(&self) -> impl Iterator<Item = &VectorControlPoint> {
|
||||
self.points.iter().skip(1).flatten().filter(|pnt| pnt.editor_state.is_selected)
|
||||
}
|
||||
|
||||
/// Provides the mutable selected handles attached to this anchor
|
||||
pub fn selected_handles_mut(&mut self) -> impl Iterator<Item = &mut VectorControlPoint> {
|
||||
self.points.iter_mut().skip(1).flatten().filter(|pnt| pnt.editor_state.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
|
||||
/// Returns the anchor handle if the anchor is provided
|
||||
pub fn opposing_handle(&self, handle: &VectorControlPoint) -> Option<&VectorControlPoint> {
|
||||
self.points[!handle.manipulator_type].as_ref()
|
||||
}
|
||||
/// Returns the opposing handle to the handle provided, mutable
|
||||
/// Returns the anchor handle if the anchor is provided, mutable
|
||||
pub fn opposing_handle_mut(&mut self, handle: &VectorControlPoint) -> Option<&mut VectorControlPoint> {
|
||||
self.points[!handle.manipulator_type].as_mut()
|
||||
}
|
||||
|
||||
/// Set the mirroring state
|
||||
pub fn toggle_mirroring(&mut self, toggle_distance: bool, toggle_angle: bool) {
|
||||
if toggle_distance {
|
||||
self.editor_state.mirror_distance_between_handles = !self.editor_state.mirror_distance_between_handles;
|
||||
}
|
||||
if toggle_angle {
|
||||
self.editor_state.mirror_angle_between_handles = !self.editor_state.mirror_angle_between_handles;
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to more easily set position of VectorControlPoints
|
||||
pub fn set_point_position(&mut self, point_index: usize, position: DVec2) {
|
||||
assert!(position.is_finite(), "Tried to set_point_position to non finite");
|
||||
if let Some(point) = &mut self.points[point_index] {
|
||||
point.position = position;
|
||||
} else {
|
||||
self.points[point_index] = Some(VectorControlPoint::new(position, ControlPointType::from_index(point_index)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply an affine transformation the points
|
||||
pub fn transform(&mut self, transform: &DAffine2) {
|
||||
for point in self.points_mut() {
|
||||
point.transform(transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct VectorAnchorState {
|
||||
// If we should maintain the angle between the handles
|
||||
pub mirror_angle_between_handles: bool,
|
||||
// If we should make the handles equidistance from the anchor?
|
||||
pub mirror_distance_between_handles: bool,
|
||||
}
|
||||
|
||||
impl Default for VectorAnchorState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mirror_angle_between_handles: true,
|
||||
mirror_distance_between_handles: true,
|
||||
}
|
||||
}
|
||||
}
|
76
graphene/src/layers/vector/vector_control_point.rs
Normal file
76
graphene/src/layers/vector/vector_control_point.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use super::constants::ControlPointType;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// VectorControlPoint represents any editable point, anchor or handle
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct VectorControlPoint {
|
||||
/// The sibling element if this is a handle
|
||||
pub position: glam::DVec2,
|
||||
/// The type of manipulator this point is
|
||||
pub manipulator_type: ControlPointType,
|
||||
|
||||
#[serde(skip)]
|
||||
/// The state specific to the editor
|
||||
pub editor_state: VectorControlPointState,
|
||||
}
|
||||
|
||||
impl Default for VectorControlPoint {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: DVec2::ZERO,
|
||||
manipulator_type: ControlPointType::Anchor,
|
||||
editor_state: VectorControlPointState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VectorControlPoint {
|
||||
/// Initialize a new control point
|
||||
pub fn new(position: glam::DVec2, manipulator_type: ControlPointType) -> Self {
|
||||
assert!(position.is_finite(), "tried to create point with non finite position");
|
||||
Self {
|
||||
position,
|
||||
manipulator_type,
|
||||
editor_state: VectorControlPointState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets if this point is selected
|
||||
pub fn set_selected(&mut self, selected: bool) {
|
||||
self.editor_state.is_selected = selected;
|
||||
}
|
||||
|
||||
pub fn is_selected(&self) -> bool {
|
||||
self.editor_state.is_selected
|
||||
}
|
||||
|
||||
/// Apply given transform to this point
|
||||
pub fn transform(&mut self, delta: &DAffine2) {
|
||||
self.position = delta.transform_point2(self.position);
|
||||
assert!(self.position.is_finite(), "tried to transform point to non finite position");
|
||||
}
|
||||
|
||||
/// Move by a delta amount
|
||||
pub fn move_by(&mut self, delta: &DVec2) {
|
||||
self.position += *delta;
|
||||
assert!(self.position.is_finite(), "tried to move point to non finite position");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct VectorControlPointState {
|
||||
/// If this control point can be selected
|
||||
pub can_be_selected: bool,
|
||||
/// Is this control point currently selected
|
||||
pub is_selected: bool,
|
||||
}
|
||||
|
||||
impl Default for VectorControlPointState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
can_be_selected: true,
|
||||
is_selected: false,
|
||||
}
|
||||
}
|
||||
}
|
529
graphene/src/layers/vector/vector_shape.rs
Normal file
529
graphene/src/layers/vector/vector_shape.rs
Normal file
|
@ -0,0 +1,529 @@
|
|||
use super::constants::ControlPointType;
|
||||
use super::vector_anchor::VectorAnchor;
|
||||
use super::vector_control_point::VectorControlPoint;
|
||||
use crate::layers::id_vec::IdBackedVec;
|
||||
use crate::layers::layer_info::{Layer, LayerDataType};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{BezPath, PathEl, Rect, Shape};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// VectorShape represents a single vector shape, containing many anchors
|
||||
/// For each closed shape we keep a VectorShape which contains the handles and anchors that define that shape.
|
||||
#[derive(PartialEq, Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct VectorShape(IdBackedVec<VectorAnchor>);
|
||||
|
||||
impl VectorShape {
|
||||
// ** SHAPE INITIALIZATION **
|
||||
|
||||
/// Create a new VectorShape with no anchors or handles
|
||||
pub fn new() -> Self {
|
||||
VectorShape { ..Default::default() }
|
||||
}
|
||||
|
||||
/// Construct a [VectorShape] from a point iterator
|
||||
pub fn from_points(points: impl Iterator<Item = DVec2>, closed: bool) -> Self {
|
||||
let anchors = points.map(VectorAnchor::new);
|
||||
let mut p_line = VectorShape(IdBackedVec::default());
|
||||
p_line.0.push_range(anchors);
|
||||
if closed {
|
||||
p_line.0.push(VectorAnchor::closed());
|
||||
}
|
||||
p_line
|
||||
}
|
||||
|
||||
/// Create a new VectorShape from a kurbo Shape
|
||||
/// This exists to smooth the transition away from Kurbo
|
||||
pub fn from_kurbo_shape<T: Shape>(shape: &T) -> Self {
|
||||
shape.path_elements(0.1).into()
|
||||
}
|
||||
|
||||
// ** PRIMITIVE CONSTRUCTION **
|
||||
|
||||
/// constructs a rectangle with `p1` as the lower left and `p2` as the top right
|
||||
pub fn new_rect(p1: DVec2, p2: DVec2) -> Self {
|
||||
VectorShape(
|
||||
vec![
|
||||
VectorAnchor::new(p1),
|
||||
VectorAnchor::new(DVec2::new(p1.x, p2.y)),
|
||||
VectorAnchor::new(p2),
|
||||
VectorAnchor::new(DVec2::new(p2.x, p1.y)),
|
||||
VectorAnchor::closed(),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_ellipse(p1: DVec2, p2: DVec2) -> Self {
|
||||
let x_height = DVec2::new((p2.x - p1.x).abs(), 0.);
|
||||
let y_height = DVec2::new(0., (p2.y - p1.y).abs());
|
||||
let center = (p1 + p2) * 0.5;
|
||||
let top = center + y_height * 0.5;
|
||||
let bottom = center - y_height * 0.5;
|
||||
let left = center + x_height * 0.5;
|
||||
let right = center - x_height * 0.5;
|
||||
|
||||
// Constant explained here https://stackoverflow.com/a/27863181
|
||||
let curve_constant = 0.55228_3;
|
||||
let handle_offset_x = x_height * curve_constant * 0.5;
|
||||
let handle_offset_y = y_height * curve_constant * 0.5;
|
||||
|
||||
VectorShape(
|
||||
vec![
|
||||
VectorAnchor::new_with_handles(top, Some(top + handle_offset_x), Some(top - handle_offset_x)),
|
||||
VectorAnchor::new_with_handles(right, Some(right + handle_offset_y), Some(right - handle_offset_y)),
|
||||
VectorAnchor::new_with_handles(bottom, Some(bottom - handle_offset_x), Some(bottom + handle_offset_x)),
|
||||
VectorAnchor::new_with_handles(left, Some(left - handle_offset_y), Some(left + handle_offset_y)),
|
||||
VectorAnchor::closed(),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// constructs an ngon
|
||||
/// `radius` is the distance from the `center` to any vertex, or the radius of the circle the ngon may be inscribed inside
|
||||
/// `sides` is the number of sides
|
||||
pub fn new_ngon(center: DVec2, sides: u64, radius: f64) -> Self {
|
||||
let mut anchors = vec![];
|
||||
for i in 0..sides {
|
||||
let angle = (i as f64) * std::f64::consts::TAU / (sides as f64);
|
||||
let center = center + DVec2::ONE * radius;
|
||||
let position = VectorAnchor::new(DVec2::new(center.x + radius * f64::cos(angle), center.y + radius * f64::sin(angle)) * 0.5);
|
||||
anchors.push(position);
|
||||
}
|
||||
anchors.push(VectorAnchor::closed());
|
||||
VectorShape(anchors.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Constructs a line from `p1` to `p2`
|
||||
pub fn new_line(p1: DVec2, p2: DVec2) -> Self {
|
||||
VectorShape(vec![VectorAnchor::new(p1), VectorAnchor::new(p2)].into_iter().collect())
|
||||
}
|
||||
|
||||
/// Constructs a set of lines from `p1` to `pN`
|
||||
pub fn new_poly_line<T: Into<glam::DVec2>>(points: Vec<T>) -> Self {
|
||||
let anchors = points.into_iter().map(|point| VectorAnchor::new(point.into()));
|
||||
let mut p_line = VectorShape(IdBackedVec::default());
|
||||
p_line.0.push_range(anchors);
|
||||
p_line
|
||||
}
|
||||
|
||||
pub fn new_spline<T: Into<glam::DVec2>>(points: Vec<T>) -> Self {
|
||||
let mut new = Self::default();
|
||||
// shadow `points`
|
||||
let points: Vec<DVec2> = points.into_iter().map(Into::<glam::DVec2>::into).collect();
|
||||
|
||||
// Number of points = number of points to find handles for
|
||||
let n = points.len();
|
||||
|
||||
// matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html)
|
||||
// because the 'a' coefficients are all 1 they need not be stored
|
||||
// this algorithm does a variation of the above algorithm.
|
||||
// Instead of using the traditional cubic: a + bt + ct^2 + dt^3, we use the bezier cubic.
|
||||
|
||||
let mut b = vec![DVec2::new(4.0, 4.0); n];
|
||||
b[0] = DVec2::new(2.0, 2.0);
|
||||
b[n - 1] = DVec2::new(2.0, 2.0);
|
||||
|
||||
let mut c = vec![DVec2::new(1.0, 1.0); n];
|
||||
|
||||
// 'd' is the the second point in a cubic bezier, which is what we solve for
|
||||
let mut d = vec![DVec2::ZERO; n];
|
||||
|
||||
d[0] = DVec2::new(2.0 * points[1].x + points[0].x, 2.0 * points[1].y + points[0].y);
|
||||
d[n - 1] = DVec2::new(3.0 * points[n - 1].x, 3.0 * points[n - 1].y);
|
||||
for idx in 1..(n - 1) {
|
||||
d[idx] = DVec2::new(4.0 * points[idx].x + 2.0 * points[idx + 1].x, 4.0 * points[idx].y + 2.0 * points[idx + 1].y);
|
||||
}
|
||||
|
||||
// Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
|
||||
// do row operations to eliminate `a` coefficients
|
||||
c[0] /= -b[0];
|
||||
d[0] /= -b[0];
|
||||
for i in 1..n {
|
||||
b[i] += c[i - 1];
|
||||
// for some reason the below line makes the borrow checker mad
|
||||
//d[i] += d[i-1]
|
||||
d[i] = d[i] + d[i - 1];
|
||||
c[i] /= -b[i];
|
||||
d[i] /= -b[i];
|
||||
}
|
||||
|
||||
// at this point b[i] == -a[i + 1], a[i] == 0,
|
||||
// do row operations to eliminate 'c' coefficients and solve
|
||||
d[n - 1] *= -1.0;
|
||||
for i in (0..n - 1).rev() {
|
||||
d[i] = d[i] - (c[i] * d[i + 1]);
|
||||
d[i] *= -1.0; //d[i] /= b[i]
|
||||
}
|
||||
|
||||
// given the second point in the n'th cubic bezier, the third point is given by 2 * points[n+1] - b[n+1].
|
||||
// to find 'handle1_pos' for the n'th point we need the n-1 cubic bezier
|
||||
new.0.push_end(VectorAnchor::new_with_handles(points[0], None, Some(d[0])));
|
||||
for i in 1..n - 1 {
|
||||
new.0.push_end(VectorAnchor::new_with_handles(points[i], Some(2.0 * points[i] - d[i]), Some(d[i])));
|
||||
}
|
||||
new.0.push_end(VectorAnchor::new_with_handles(points[n - 1], Some(2.0 * points[n - 1] - d[n - 1]), None));
|
||||
|
||||
new
|
||||
}
|
||||
|
||||
/// Move the selected points by the delta vector
|
||||
pub fn move_selected(&mut self, delta: DVec2, absolute_position: DVec2, viewspace: &DAffine2) {
|
||||
self.selected_anchors_any_points_mut()
|
||||
.for_each(|anchor| anchor.move_selected_points(delta, absolute_position, viewspace));
|
||||
}
|
||||
|
||||
/// Delete the selected points from the VectorShape
|
||||
pub fn delete_selected(&mut self) {
|
||||
let mut ids_to_delete: Vec<u64> = vec![];
|
||||
for (id, anchor) in self.anchors_mut().enumerate_mut() {
|
||||
if anchor.is_anchor_selected() {
|
||||
ids_to_delete.push(*id);
|
||||
} else {
|
||||
anchor.delete_selected();
|
||||
}
|
||||
}
|
||||
|
||||
for id in ids_to_delete {
|
||||
self.anchors_mut().remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply a transformation to all of the VectorShape points
|
||||
pub fn apply_affine(&mut self, affine: DAffine2) {
|
||||
for anchor in self.anchors_mut().iter_mut() {
|
||||
anchor.transform(&affine);
|
||||
}
|
||||
}
|
||||
|
||||
// ** SELECTION OF POINTS **
|
||||
|
||||
/// Select a single point by providing (AnchorId, ControlPointType)
|
||||
pub fn select_point(&mut self, point: (u64, ControlPointType), selected: bool) -> Option<&mut VectorAnchor> {
|
||||
let (anchor_id, point_id) = point;
|
||||
if let Some(anchor) = self.anchors_mut().by_id_mut(anchor_id) {
|
||||
anchor.select_point(point_id as usize, selected);
|
||||
return Some(anchor);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Select points in the VectorShape, given by (AnchorId, ControlPointType)
|
||||
pub fn select_points(&mut self, points: &[(u64, ControlPointType)], selected: bool) {
|
||||
points.iter().for_each(|point| {
|
||||
self.select_point(*point, selected);
|
||||
});
|
||||
}
|
||||
|
||||
/// Select all the anchors in this shape
|
||||
pub fn select_all_anchors(&mut self) {
|
||||
for anchor in self.anchors_mut().iter_mut() {
|
||||
anchor.select_point(ControlPointType::Anchor as usize, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Select an anchor by index
|
||||
pub fn select_anchor_by_index(&mut self, anchor_index: usize) -> Option<&mut VectorAnchor> {
|
||||
if let Some(anchor) = self.anchors_mut().by_index_mut(anchor_index) {
|
||||
anchor.select_point(ControlPointType::Anchor as usize, true);
|
||||
return Some(anchor);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The last anchor in the shape
|
||||
pub fn select_last_anchor(&mut self) -> Option<&mut VectorAnchor> {
|
||||
if let Some(anchor) = self.anchors_mut().last_mut() {
|
||||
anchor.select_point(ControlPointType::Anchor as usize, true);
|
||||
return Some(anchor);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Clear all the selected anchors, and clear the selected points on the anchors
|
||||
pub fn clear_selected_anchors(&mut self) {
|
||||
for anchor in self.anchors_mut().iter_mut() {
|
||||
anchor.clear_selected_points();
|
||||
}
|
||||
}
|
||||
|
||||
// ** ACCESSING ANCHORS **
|
||||
|
||||
/// Return all the selected anchors, reference
|
||||
pub fn selected_anchors(&self) -> impl Iterator<Item = &VectorAnchor> {
|
||||
self.anchors().iter().filter(|anchor| anchor.is_anchor_selected())
|
||||
}
|
||||
|
||||
/// Return all the selected anchors, mutable
|
||||
pub fn selected_anchors_mut(&mut self) -> impl Iterator<Item = &mut VectorAnchor> {
|
||||
self.anchors_mut().iter_mut().filter(|anchor| anchor.is_anchor_selected())
|
||||
}
|
||||
|
||||
/// Return all the selected anchors that have any children points selected, reference
|
||||
pub fn selected_anchors_any_points(&self) -> impl Iterator<Item = &VectorAnchor> {
|
||||
self.anchors().iter().filter(|anchor| anchor.any_points_selected())
|
||||
}
|
||||
|
||||
/// Return all the selected anchors that have any children points selected, mutable
|
||||
pub fn selected_anchors_any_points_mut(&mut self) -> impl Iterator<Item = &mut VectorAnchor> {
|
||||
self.anchors_mut().iter_mut().filter(|anchor| anchor.any_points_selected())
|
||||
}
|
||||
|
||||
/// An alias for `self.0`
|
||||
pub fn anchors(&self) -> &IdBackedVec<VectorAnchor> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns a [VectorControlPoint] from the last [VectorAnchor]
|
||||
pub fn last_point(&self, control_type: ControlPointType) -> Option<&VectorControlPoint> {
|
||||
self.anchors().last().and_then(|anchor| anchor.points[control_type].as_ref())
|
||||
}
|
||||
|
||||
/// Returns a [VectorControlPoint] from the last [VectorAnchor], mutably
|
||||
pub fn last_point_mut(&mut self, control_type: ControlPointType) -> Option<&mut VectorControlPoint> {
|
||||
self.anchors_mut().last_mut().and_then(|anchor| anchor.points[control_type].as_mut())
|
||||
}
|
||||
|
||||
/// Returns a [VectorControlPoint] from the first [VectorAnchor]
|
||||
pub fn first_point(&self, control_type: ControlPointType) -> Option<&VectorControlPoint> {
|
||||
self.anchors().first().and_then(|anchor| anchor.points[control_type].as_ref())
|
||||
}
|
||||
|
||||
/// Returns a [VectorControlPoint] from the first [VectorAnchor]
|
||||
pub fn first_point_mut(&mut self, control_type: ControlPointType) -> Option<&mut VectorControlPoint> {
|
||||
self.anchors_mut().first_mut().and_then(|anchor| anchor.points[control_type].as_mut())
|
||||
}
|
||||
|
||||
/// Should we close the shape?
|
||||
pub fn should_close_shape(&self) -> bool {
|
||||
if self.last_point(ControlPointType::Anchor).is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.first_point(ControlPointType::Anchor)
|
||||
.unwrap()
|
||||
.position
|
||||
.distance(self.last_point(ControlPointType::Anchor).unwrap().position)
|
||||
< 0.001 // TODO Replace with constant, a small epsilon
|
||||
}
|
||||
|
||||
/// Close the shape if able
|
||||
pub fn close_shape(&mut self) {
|
||||
if self.should_close_shape() {
|
||||
self.anchors_mut().push_end(VectorAnchor::closed());
|
||||
}
|
||||
}
|
||||
|
||||
/// An alias for `self.0` mutable
|
||||
pub fn anchors_mut(&mut self) -> &mut IdBackedVec<VectorAnchor> {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
// ** INTERFACE WITH KURBO **
|
||||
|
||||
// TODO Implement our own a local bounding box calculation
|
||||
/// Return the bounding box of the shape
|
||||
pub fn bounding_box(&self) -> Rect {
|
||||
<&Self as Into<BezPath>>::into(self).bounding_box()
|
||||
}
|
||||
|
||||
/// Use kurbo to convert this shape into an SVG path
|
||||
pub fn to_svg(&mut self) -> String {
|
||||
fn write_positions(result: &mut String, values: [Option<DVec2>; 3]) {
|
||||
use std::fmt::Write;
|
||||
let count = values.into_iter().flatten().count();
|
||||
for (index, pos) in values.into_iter().flatten().enumerate() {
|
||||
write!(result, "{},{}", pos.x, pos.y).unwrap();
|
||||
if index != count - 1 {
|
||||
result.push(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = String::new();
|
||||
// The out position from the previous VectorAnchor
|
||||
let mut last_out_handle = None;
|
||||
// The values from the last moveto (for closing the path)
|
||||
let (mut first_in_handle, mut first_in_anchor) = (None, None);
|
||||
// Should the next element be a moveto?
|
||||
let mut start_new_contour = true;
|
||||
for vector_anchor in self.anchors().iter() {
|
||||
let in_handle = vector_anchor.points[ControlPointType::InHandle].as_ref().map(|anchor| anchor.position);
|
||||
let anchor = vector_anchor.points[ControlPointType::Anchor].as_ref().map(|anchor| anchor.position);
|
||||
let out_handle = vector_anchor.points[ControlPointType::OutHandle].as_ref().map(|anchor| anchor.position);
|
||||
|
||||
let command = match (last_out_handle.is_some(), in_handle.is_some(), anchor.is_some()) {
|
||||
(_, _, true) if start_new_contour => 'M',
|
||||
(true, false, true) | (false, true, true) => 'Q',
|
||||
(true, true, true) => 'C',
|
||||
(false, false, true) => 'L',
|
||||
(_, false, false) => 'Z',
|
||||
_ => panic!("Invalid shape {:#?}", self),
|
||||
};
|
||||
|
||||
// Complete the last curve
|
||||
if command == 'Z' {
|
||||
if last_out_handle.is_some() && first_in_handle.is_some() {
|
||||
result.push('C');
|
||||
write_positions(&mut result, [last_out_handle, first_in_handle, first_in_anchor]);
|
||||
} else if last_out_handle.is_some() || first_in_handle.is_some() {
|
||||
result.push('Q');
|
||||
write_positions(&mut result, [last_out_handle, first_in_handle, first_in_anchor]);
|
||||
} else {
|
||||
result.push('Z');
|
||||
}
|
||||
} else if command == 'M' {
|
||||
// Update the last moveto position
|
||||
(first_in_handle, first_in_anchor) = (in_handle, anchor);
|
||||
result.push(command);
|
||||
write_positions(&mut result, [None, None, anchor]);
|
||||
} else {
|
||||
result.push(command);
|
||||
write_positions(&mut result, [last_out_handle, in_handle, anchor]);
|
||||
}
|
||||
start_new_contour = command == 'Z';
|
||||
last_out_handle = out_handle;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// ** CONVERSIONS **
|
||||
|
||||
/// Convert a mutable layer into a mutable VectorShape
|
||||
impl<'a> TryFrom<&'a mut Layer> for &'a mut VectorShape {
|
||||
type Error = &'static str;
|
||||
fn try_from(layer: &'a mut Layer) -> Result<&'a mut VectorShape, Self::Error> {
|
||||
match &mut layer.data {
|
||||
LayerDataType::Shape(layer) => Ok(&mut layer.shape),
|
||||
// TODO Resolve converting text into a VectorShape at the layer level
|
||||
// LayerDataType::Text(text) => Some(VectorShape::new(path_to_shape.to_vec(), viewport_transform, true)),
|
||||
_ => Err("Did not find any shape data in the layer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a reference to a layer into a reference of a VectorShape
|
||||
impl<'a> TryFrom<&'a Layer> for &'a VectorShape {
|
||||
type Error = &'static str;
|
||||
fn try_from(layer: &'a Layer) -> Result<&'a VectorShape, Self::Error> {
|
||||
match &layer.data {
|
||||
LayerDataType::Shape(layer) => Ok(&layer.shape),
|
||||
// TODO Resolve converting text into a VectorShape at the layer level
|
||||
// LayerDataType::Text(text) => Some(VectorShape::new(path_to_shape.to_vec(), viewport_transform, true)),
|
||||
_ => Err("Did not find any shape data in the layer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a BezPath from a VectorShape
|
||||
impl From<&VectorShape> for BezPath {
|
||||
fn from(vector_shape: &VectorShape) -> Self {
|
||||
// Take anchors and create path elements: line, quad or curve, or a close indicator
|
||||
let anchors_to_path_el = |first: &VectorAnchor, second: &VectorAnchor| -> (PathEl, bool) {
|
||||
match [
|
||||
&first.points[ControlPointType::OutHandle],
|
||||
&second.points[ControlPointType::InHandle],
|
||||
&second.points[ControlPointType::Anchor],
|
||||
] {
|
||||
[None, None, Some(anchor)] => (PathEl::LineTo(point_to_kurbo(anchor)), false),
|
||||
[None, Some(in_handle), Some(anchor)] => (PathEl::QuadTo(point_to_kurbo(in_handle), point_to_kurbo(anchor)), false),
|
||||
[Some(out_handle), None, Some(anchor)] => (PathEl::QuadTo(point_to_kurbo(out_handle), point_to_kurbo(anchor)), false),
|
||||
[Some(out_handle), Some(in_handle), Some(anchor)] => (PathEl::CurveTo(point_to_kurbo(out_handle), point_to_kurbo(in_handle), point_to_kurbo(anchor)), false),
|
||||
[Some(out_handle), None, None] => {
|
||||
if let Some(first_anchor) = vector_shape.anchors().first() {
|
||||
(
|
||||
if let Some(in_handle) = &first_anchor.points[ControlPointType::InHandle] {
|
||||
PathEl::CurveTo(
|
||||
point_to_kurbo(out_handle),
|
||||
point_to_kurbo(in_handle),
|
||||
point_to_kurbo(first_anchor.points[ControlPointType::Anchor].as_ref().unwrap()),
|
||||
)
|
||||
} else {
|
||||
PathEl::QuadTo(point_to_kurbo(out_handle), point_to_kurbo(first_anchor.points[ControlPointType::Anchor].as_ref().unwrap()))
|
||||
},
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
(PathEl::ClosePath, true)
|
||||
}
|
||||
}
|
||||
[None, None, None] => (PathEl::ClosePath, true),
|
||||
_ => panic!("Invalid path element {:#?}", vector_shape),
|
||||
}
|
||||
};
|
||||
|
||||
if vector_shape.anchors().is_empty() {
|
||||
return BezPath::new();
|
||||
}
|
||||
|
||||
let mut bez_path = vec![];
|
||||
let mut start_new_shape = true;
|
||||
|
||||
for elements in vector_shape.anchors().windows(2) {
|
||||
let first = &elements[0];
|
||||
let second = &elements[1];
|
||||
|
||||
// Tell kurbo cursor to move to the first anchor
|
||||
if start_new_shape {
|
||||
if let Some(anchor) = &first.points[ControlPointType::Anchor] {
|
||||
bez_path.push(PathEl::MoveTo(point_to_kurbo(anchor)));
|
||||
}
|
||||
}
|
||||
|
||||
// Create a path element from our first, second anchors in the window
|
||||
let (path_el, should_start_new_shape) = anchors_to_path_el(first, second);
|
||||
start_new_shape = should_start_new_shape;
|
||||
bez_path.push(path_el);
|
||||
if should_start_new_shape && bez_path.last().filter(|&&el| el == PathEl::ClosePath).is_none() {
|
||||
bez_path.push(PathEl::ClosePath)
|
||||
}
|
||||
}
|
||||
|
||||
BezPath::from_vec(bez_path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a VectorShape from a BezPath
|
||||
impl<T: Iterator<Item = PathEl>> From<T> for VectorShape {
|
||||
fn from(path: T) -> Self {
|
||||
let mut vector_shape = VectorShape::new();
|
||||
for path_el in path {
|
||||
match path_el {
|
||||
PathEl::MoveTo(p) => {
|
||||
vector_shape.anchors_mut().push_end(VectorAnchor::new(kurbo_point_to_dvec2(p)));
|
||||
}
|
||||
PathEl::LineTo(p) => {
|
||||
vector_shape.anchors_mut().push_end(VectorAnchor::new(kurbo_point_to_dvec2(p)));
|
||||
}
|
||||
PathEl::QuadTo(p0, p1) => {
|
||||
vector_shape.anchors_mut().push_end(VectorAnchor::new(kurbo_point_to_dvec2(p1)));
|
||||
vector_shape.anchors_mut().last_mut().unwrap().points[ControlPointType::InHandle] = Some(VectorControlPoint::new(kurbo_point_to_dvec2(p0), ControlPointType::InHandle));
|
||||
}
|
||||
PathEl::CurveTo(p0, p1, p2) => {
|
||||
vector_shape.anchors_mut().last_mut().unwrap().points[ControlPointType::OutHandle] = Some(VectorControlPoint::new(kurbo_point_to_dvec2(p0), ControlPointType::OutHandle));
|
||||
vector_shape.anchors_mut().push_end(VectorAnchor::new(kurbo_point_to_dvec2(p2)));
|
||||
vector_shape.anchors_mut().last_mut().unwrap().points[ControlPointType::InHandle] = Some(VectorControlPoint::new(kurbo_point_to_dvec2(p1), ControlPointType::InHandle));
|
||||
}
|
||||
PathEl::ClosePath => {
|
||||
vector_shape.anchors_mut().push_end(VectorAnchor::closed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vector_shape
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn point_to_kurbo(point: &VectorControlPoint) -> kurbo::Point {
|
||||
kurbo::Point::new(point.position.x, point.position.y)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn kurbo_point_to_dvec2(point: kurbo::Point) -> DVec2 {
|
||||
DVec2::new(point.x, point.y)
|
||||
}
|
|
@ -2,6 +2,9 @@ use crate::boolean_ops::BooleanOperation as BooleanOperationType;
|
|||
use crate::layers::blend_mode::BlendMode;
|
||||
use crate::layers::layer_info::Layer;
|
||||
use crate::layers::style::{self, Stroke};
|
||||
use crate::layers::vector::constants::ControlPointType;
|
||||
use crate::layers::vector::vector_anchor::VectorAnchor;
|
||||
use crate::layers::vector::vector_shape::VectorShape;
|
||||
use crate::LayerId;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -19,33 +22,18 @@ pub enum Operation {
|
|||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddOverlayEllipse {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddRect {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddOverlayRect {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddLine {
|
||||
path: Vec<LayerId>,
|
||||
insert_index: isize,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddOverlayLine {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddText {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
|
@ -97,19 +85,12 @@ pub enum Operation {
|
|||
sides: u32,
|
||||
style: style::PathStyle,
|
||||
},
|
||||
AddOverlayShape {
|
||||
path: Vec<LayerId>,
|
||||
bez_path: kurbo::BezPath,
|
||||
style: style::PathStyle,
|
||||
closed: bool,
|
||||
},
|
||||
AddShape {
|
||||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
insert_index: isize,
|
||||
bez_path: kurbo::BezPath,
|
||||
vector_path: VectorShape,
|
||||
style: style::PathStyle,
|
||||
closed: bool,
|
||||
},
|
||||
BooleanOperation {
|
||||
operation: BooleanOperationType,
|
||||
|
@ -118,6 +99,16 @@ pub enum Operation {
|
|||
DeleteLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
DeleteSelectedVectorPoints {
|
||||
layer_paths: Vec<Vec<LayerId>>,
|
||||
},
|
||||
DeselectVectorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
point_ids: Vec<(u64, ControlPointType)>,
|
||||
},
|
||||
DeselectAllVectorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
},
|
||||
DuplicateLayer {
|
||||
path: Vec<LayerId>,
|
||||
},
|
||||
|
@ -127,6 +118,11 @@ pub enum Operation {
|
|||
font_style: String,
|
||||
size: f64,
|
||||
},
|
||||
MoveSelectedVectorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
delta: (f64, f64),
|
||||
absolute_position: (f64, f64),
|
||||
},
|
||||
RenameLayer {
|
||||
layer_path: Vec<LayerId>,
|
||||
new_name: String,
|
||||
|
@ -151,14 +147,38 @@ pub enum Operation {
|
|||
path: Vec<LayerId>,
|
||||
transform: [f64; 6],
|
||||
},
|
||||
SelectVectorPoints {
|
||||
layer_path: Vec<LayerId>,
|
||||
point_ids: Vec<(u64, ControlPointType)>,
|
||||
add: bool,
|
||||
},
|
||||
SetShapePath {
|
||||
path: Vec<LayerId>,
|
||||
bez_path: kurbo::BezPath,
|
||||
vector_path: VectorShape,
|
||||
},
|
||||
SetShapePathInViewport {
|
||||
path: Vec<LayerId>,
|
||||
bez_path: kurbo::BezPath,
|
||||
transform: [f64; 6],
|
||||
InsertVectorAnchor {
|
||||
layer_path: Vec<LayerId>,
|
||||
anchor: VectorAnchor,
|
||||
after_id: u64,
|
||||
},
|
||||
PushVectorAnchor {
|
||||
layer_path: Vec<LayerId>,
|
||||
anchor: VectorAnchor,
|
||||
},
|
||||
RemoveVectorAnchor {
|
||||
layer_path: Vec<LayerId>,
|
||||
id: u64,
|
||||
},
|
||||
MoveVectorPoint {
|
||||
layer_path: Vec<LayerId>,
|
||||
id: u64,
|
||||
control_type: ControlPointType,
|
||||
position: (f64, f64),
|
||||
},
|
||||
RemoveVectorPoint {
|
||||
layer_path: Vec<LayerId>,
|
||||
id: u64,
|
||||
control_type: ControlPointType,
|
||||
},
|
||||
TransformLayerInScope {
|
||||
path: Vec<LayerId>,
|
||||
|
@ -205,6 +225,11 @@ pub enum Operation {
|
|||
path: Vec<LayerId>,
|
||||
stroke: Stroke,
|
||||
},
|
||||
SetSelectedHandleMirroring {
|
||||
layer_path: Vec<LayerId>,
|
||||
toggle_distance: bool,
|
||||
toggle_angle: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Operation {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue