From 58675eb64d6996984eac78e1cf08b4821014dad9 Mon Sep 17 00:00:00 2001 From: Oliver Davies Date: Tue, 5 Jul 2022 15:02:18 -0700 Subject: [PATCH] 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 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 Co-authored-by: Caleb Dennis Co-authored-by: caleb <56044292+caleb-ad@users.noreply.github.com> Co-authored-by: Keavon Chambers Co-authored-by: 0hypercube <0hypercube@gmail.com> Co-authored-by: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> --- .vscode/settings.json | 1 + editor/src/communication/dispatcher.rs | 28 +- .../graphite-test-document.graphite | 2 +- editor/src/consts.rs | 2 +- editor/src/dialog/dialog_message_handler.rs | 8 +- .../src/dialog/dialogs/coming_soon_dialog.rs | 3 +- editor/src/dialog/dialogs/error_dialog.rs | 3 +- editor/src/document/document_message.rs | 12 + .../src/document/document_message_handler.rs | 76 ++- .../src/document/portfolio_message_handler.rs | 3 +- editor/src/input/input_mapper.rs | 4 +- editor/src/layout/widgets.rs | 6 +- editor/src/viewport_tools/snapping.rs | 54 +- .../src/viewport_tools/tools/artboard_tool.rs | 6 +- .../src/viewport_tools/tools/freehand_tool.rs | 1 - .../src/viewport_tools/tools/gradient_tool.rs | 8 +- editor/src/viewport_tools/tools/line_tool.rs | 2 +- editor/src/viewport_tools/tools/path_tool.rs | 80 ++- editor/src/viewport_tools/tools/pen_tool.rs | 354 +++++++----- .../src/viewport_tools/tools/select_tool.rs | 2 +- .../tools/shared/path_outline.rs | 23 +- .../src/viewport_tools/tools/shared/resize.rs | 2 +- .../tools/shared/transformation_cage.rs | 6 +- .../src/viewport_tools/tools/spline_tool.rs | 3 +- editor/src/viewport_tools/tools/text_tool.rs | 17 +- .../viewport_tools/vector_editor/constants.rs | 25 +- .../src/viewport_tools/vector_editor/mod.rs | 7 +- .../vector_editor/overlay_renderer.rs | 315 +++++++++++ .../vector_editor/shape_editor.rs | 433 +++++++------- .../vector_editor/vector_anchor.rs | 413 -------------- .../vector_editor/vector_control_point.rs | 74 --- .../vector_editor/vector_shape.rs | 501 ----------------- frontend/wasm/src/helpers.rs | 3 +- graphene/src/boolean_ops.rs | 50 +- graphene/src/document.rs | 226 +++++--- graphene/src/intersection.rs | 26 +- graphene/src/layers/id_vec.rs | 172 ++++++ graphene/src/layers/layer_info.rs | 24 +- graphene/src/layers/mod.rs | 2 + graphene/src/layers/shape_layer.rs | 141 +---- graphene/src/layers/text_layer.rs | 44 +- .../text_layer/{to_kurbo.rs => to_path.rs} | 65 ++- graphene/src/layers/vector/constants.rs | 50 ++ graphene/src/layers/vector/mod.rs | 4 + graphene/src/layers/vector/vector_anchor.rs | 301 ++++++++++ .../src/layers/vector/vector_control_point.rs | 76 +++ graphene/src/layers/vector/vector_shape.rs | 529 ++++++++++++++++++ graphene/src/operation.rs | 81 ++- 48 files changed, 2461 insertions(+), 1807 deletions(-) create mode 100644 editor/src/viewport_tools/vector_editor/overlay_renderer.rs delete mode 100644 editor/src/viewport_tools/vector_editor/vector_anchor.rs delete mode 100644 editor/src/viewport_tools/vector_editor/vector_control_point.rs delete mode 100644 editor/src/viewport_tools/vector_editor/vector_shape.rs create mode 100644 graphene/src/layers/id_vec.rs rename graphene/src/layers/text_layer/{to_kurbo.rs => to_path.rs} (61%) create mode 100644 graphene/src/layers/vector/constants.rs create mode 100644 graphene/src/layers/vector/mod.rs create mode 100644 graphene/src/layers/vector/vector_anchor.rs create mode 100644 graphene/src/layers/vector/vector_control_point.rs create mode 100644 graphene/src/layers/vector/vector_shape.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 707690262..166ded0dd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,4 +46,5 @@ "html.format.wrapLineLength": 200, "files.eol": "\n", "files.insertFinalNewline": true, + "rust-analyzer.procMacro.attributes.enable": true, } diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 6defb93d0..7183dcb0c 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -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); } } } diff --git a/editor/src/communication/graphite-test-document.graphite b/editor/src/communication/graphite-test-document.graphite index b4fb917d4..2fa78b2db 100644 --- a/editor/src/communication/graphite-test-document.graphite +++ b/editor/src/communication/graphite-test-document.graphite @@ -1 +1 @@ -{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":9872175665704159397,"layer_ids":[9872175665704159396],"layers":[{"visible":true,"name":null,"data":{"Shape":{"path":[{"MoveTo":{"x":0.0,"y":0.0}},{"LineTo":{"x":1.0,"y":0.0}},{"LineTo":{"x":1.0,"y":1.0}},{"LineTo":{"x":0.0,"y":1.0}},"ClosePath"],"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1,"closed":true}},"transform":{"matrix2":[376.0,0.0,-0.0,214.0],"translation":[903.0,393.0]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"blend_mode":"Normal","opacity":1.0},"font_cache":{"data":{},"default_font":null}},"saved_document_identifier":15130871412783076140,"name":"Untitled Document","version":"0.0.8","document_mode":"DesignMode","view_mode":"Normal","snapping_enabled":true,"overlays_visible":true,"layer_metadata":[[[],{"selected":false,"expanded":true}],[[9872175665704159396],{"selected":false,"expanded":false}]],"layer_range_selection_reference":[],"movement_handler":{"pan":[0.0,0.0],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":11149268176678832548,"layer_ids":[11149268176678832547],"layers":[{"visible":true,"name":null,"data":{"Shape":{"path":[{"MoveTo":{"x":0.0,"y":0.0}},{"LineTo":{"x":1.0,"y":0.0}},{"LineTo":{"x":1.0,"y":1.0}},{"LineTo":{"x":0.0,"y":1.0}},"ClosePath"],"style":{"stroke":null,"fill":{"Solid":{"red":1.0,"green":1.0,"blue":1.0,"alpha":1.0}}},"render_index":1,"closed":true}},"transform":{"matrix2":[885.0,0.0,-0.0,461.0],"translation":[657.0,273.0]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"blend_mode":"Normal","opacity":1.0},"font_cache":{"data":{},"default_font":null}},"artboard_ids":[11149268176678832547]},"properties_panel_message_handler":{"active_selection":null}} \ No newline at end of file +{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":5985789465543063412,"layer_ids":[3526279254690554630,14455049941182576981,2264120728080545131,11029257369377086983,11600227885950905622,5985789465543063411],"layers":[{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.5,1.0],"manipulator_type":"Anchor"},{"position":[0.7761415,1.0],"manipulator_type":"InHandle"},{"position":[0.22385850000000002,1.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.0,0.5],"manipulator_type":"Anchor"},{"position":[0.0,0.7761415],"manipulator_type":"InHandle"},{"position":[0.0,0.22385850000000002],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.5,0.0],"manipulator_type":"Anchor"},{"position":[0.22385850000000002,0.0],"manipulator_type":"InHandle"},{"position":[0.7761415,0.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[1.0,0.5],"manipulator_type":"Anchor"},{"position":[1.0,0.22385850000000002],"manipulator_type":"InHandle"},{"position":[1.0,0.7761415],"manipulator_type":"OutHandle"}]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5]},"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[369.0,0.0,-0.0,480.0],"translation":[-927.566650390625,-365.5]},"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[-865.566650390625,-424.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-864.566650390625,-424.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-854.566650390625,-427.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-832.566650390625,-429.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-793.566650390625,-433.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-743.566650390625,-435.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-682.566650390625,-435.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-615.566650390625,-437.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-560.566650390625,-439.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-509.566650390625,-435.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-480.566650390625,-429.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-453.566650390625,-417.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-419.566650390625,-398.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-379.566650390625,-376.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-356.566650390625,-359.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-335.566650390625,-339.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-325.566650390625,-319.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-312.566650390625,-288.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-304.566650390625,-257.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-299.566650390625,-225.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-298.566650390625,-205.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-298.566650390625,-182.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-298.566650390625,-163.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-299.566650390625,-141.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-304.566650390625,-113.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-308.566650390625,-87.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-311.566650390625,-67.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-314.566650390625,-48.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-316.566650390625,-35.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-322.566650390625,-17.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-326.566650390625,-5.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-334.566650390625,14.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-338.566650390625,27.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-345.566650390625,45.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-348.566650390625,55.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-354.566650390625,70.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-360.566650390625,83.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-365.566650390625,91.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-369.566650390625,98.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-375.566650390625,106.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-381.566650390625,114.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-385.566650390625,122.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-394.566650390625,130.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-402.566650390625,137.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-409.566650390625,142.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-417.566650390625,148.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-426.566650390625,154.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-437.566650390625,161.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-451.566650390625,166.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-468.566650390625,175.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-483.566650390625,181.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-507.566650390625,188.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-527.566650390625,193.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-552.566650390625,197.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-576.566650390625,200.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-605.566650390625,201.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-631.566650390625,201.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-661.566650390625,201.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-691.566650390625,200.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-724.566650390625,198.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-751.566650390625,195.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-778.566650390625,191.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-797.566650390625,190.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-832.566650390625,187.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-857.566650390625,183.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-897.566650390625,176.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-925.566650390625,171.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-964.566650390625,160.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-985.566650390625,152.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1010.566650390625,143.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1022.566650390625,137.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1029.566650390625,133.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1036.566650390625,127.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1042.566650390625,117.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1047.566650390625,98.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1051.566650390625,70.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1058.566650390625,37.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1062.566650390625,5.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1069.566650390625,-37.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1076.566650390625,-84.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1076.566650390625,-130.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1071.566650390625,-180.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1069.566650390625,-206.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1069.566650390625,-230.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1069.566650390625,-247.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1066.566650390625,-272.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1065.566650390625,-291.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1062.566650390625,-315.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1059.566650390625,-328.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1058.566650390625,-337.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1055.566650390625,-348.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1049.566650390625,-360.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1041.566650390625,-377.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1033.566650390625,-393.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1024.566650390625,-407.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1020.566650390625,-413.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1015.566650390625,-418.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-1001.566650390625,-430.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-990.566650390625,-436.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-977.566650390625,-441.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-967.566650390625,-444.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-959.566650390625,-444.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-952.566650390625,-444.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-933.566650390625,-442.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-905.566650390625,-436.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-889.566650390625,-432.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-880.566650390625,-430.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-877.566650390625,-429.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-876.566650390625,-428.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-874.566650390625,-426.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-870.566650390625,-424.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-869.566650390625,-424.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-867.566650390625,-424.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-865.566650390625,-424.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-861.566650390625,-425.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-857.566650390625,-426.5],"manipulator_type":"Anchor"},null,null]},{"points":[{"position":[-853.566650390625,-427.5],"manipulator_type":"Anchor"},null,null]}],"element_ids":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117]},"style":{"stroke":{"color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},"weight":5.0,"dash_lengths":[0.0],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0},"fill":"None"},"render_index":0}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.5,1.0],"manipulator_type":"Anchor"},{"position":[0.7761415,1.0],"manipulator_type":"InHandle"},{"position":[0.22385850000000002,1.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.0,0.5],"manipulator_type":"Anchor"},{"position":[0.0,0.7761415],"manipulator_type":"InHandle"},{"position":[0.0,0.22385850000000002],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.5,0.0],"manipulator_type":"Anchor"},{"position":[0.22385850000000002,0.0],"manipulator_type":"InHandle"},{"position":[0.7761415,0.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[1.0,0.5],"manipulator_type":"Anchor"},{"position":[1.0,0.22385850000000002],"manipulator_type":"InHandle"},{"position":[1.0,0.7761415],"manipulator_type":"OutHandle"}]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5]},"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[337.94872665058745,-35.51369323805279,47.56585157675848,452.6372085455832],"translation":[92.9195736497411,-415.17298085605853]},"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[-48.69253069314482,-159.10513506221514],"manipulator_type":"Anchor"},null,{"position":[-70.52138079035899,-513.9526137439627],"manipulator_type":"OutHandle"}]},{"points":[{"position":[222.83237058696454,-503.0361992753027],"manipulator_type":"Anchor"},{"position":[-158.90270804291185,-442.8223058399684],"manipulator_type":"InHandle"},{"position":[604.5674492168409,-563.250092710637],"manipulator_type":"OutHandle"}]},{"points":[{"position":[664.0813704731281,-320.5874513553998],"manipulator_type":"Anchor"},{"position":[669.902428975468,-471.1676241628193],"manipulator_type":"InHandle"},{"position":[658.2603119707883,-170.00727854798032],"manipulator_type":"OutHandle"}]},{"points":[{"position":[561.9483011516083,149.32709051135873],"manipulator_type":"Anchor"},{"position":[779.4132177618326,129.56669690727574],"manipulator_type":"InHandle"},{"position":[344.4833845413841,169.08748411544173],"manipulator_type":"OutHandle"}]},{"points":[{"position":[-68.1412419353403,141.32950801660445],"manipulator_type":"Anchor"},{"position":[-11.996778942496576,322.5035681388116],"manipulator_type":"InHandle"},{"position":[-98.31104242698265,43.97377680505987],"manipulator_type":"OutHandle"}]},{"points":[{"position":[-49.90031956271264,-155.88608010049916],"manipulator_type":"Anchor"},{"position":[-49.90031956271264,-155.88608010049916],"manipulator_type":"InHandle"},{"position":[-49.90031956271264,-155.88608010049916],"manipulator_type":"OutHandle"}]}],"element_ids":[1,2,3,4,5,6]},"style":{"stroke":{"color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},"weight":5.0,"dash_lengths":[0.0],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0},"fill":"None"},"render_index":1}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Shape":{"shape":{"elements":[{"points":[{"position":[-851.5216124852599,440.1011734805685],"manipulator_type":"Anchor"},null,{"position":[-745.4998088356991,742.8112831395855],"manipulator_type":"OutHandle"}]},{"points":[{"position":[90.98724947544201,508.0318434771365],"manipulator_type":"Anchor"},{"position":[-127.65914203173338,840.2218480977616],"manipulator_type":"InHandle"},{"position":[90.98724947544201,508.0318434771365],"manipulator_type":"OutHandle"}]}],"element_ids":[1,2]},"style":{"stroke":{"color":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0},"weight":5.0,"dash_lengths":[0.0],"dash_offset":0.0,"line_cap":"Butt","line_join":"Miter","line_join_miter_limit":4.0},"fill":"None"},"render_index":1}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"blend_mode":"Normal","opacity":1.0},{"visible":true,"name":null,"data":{"Text":{"text":"ein Fisch","path_style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"size":24.0,"line_width":null,"font":{"font_family":"Metal Mania","font_style":"Normal (400)"}}},"transform":{"matrix2":[17.266988107524366,-1.8145193943273525,1.814519394327352,17.266988107524366],"translation":[-884.6986699020281,-831.9173312579314]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[0.42065729978864125,0.0442052096218941,-0.0442052096218941,0.42065729978864125],"translation":[991.1799052249601,406.7405332186784]},"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":1812026093844533227,"name":"Untitled Document","version":"0.0.10","document_mode":"DesignMode","view_mode":"Normal","snapping_enabled":true,"overlays_visible":true,"layer_metadata":[[[3526279254690554630],{"selected":false,"expanded":false}],[[14455049941182576981],{"selected":false,"expanded":false}],[[2264120728080545131],{"selected":false,"expanded":false}],[[],{"selected":false,"expanded":true}],[[11600227885950905622],{"selected":false,"expanded":false}],[[5985789465543063411],{"selected":false,"expanded":false}],[[11029257369377086983],{"selected":false,"expanded":false}]],"layer_range_selection_reference":[],"movement_handler":{"pan":[172.02008130552463,39.54846254599968],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.10470175813131441,"tilting":false,"zoom":0.4229735977849993,"zooming":false,"snap_zoom":false,"mouse_position":[1485.0,319.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[0.42065729978864125,0.0442052096218941,-0.0442052096218941,0.42065729978864125],"translation":[991.1799052249601,406.7405332186784]},"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":null}} diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 2f4e3ab31..150b6f7db 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -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; diff --git a/editor/src/dialog/dialog_message_handler.rs b/editor/src/dialog/dialog_message_handler.rs index ff592c224..49b828457 100644 --- a/editor/src/dialog/dialog_message_handler.rs +++ b/editor/src/dialog/dialog_message_handler.rs @@ -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 { diff --git a/editor/src/dialog/dialogs/coming_soon_dialog.rs b/editor/src/dialog/dialogs/coming_soon_dialog.rs index bb1967be8..6a724dcd1 100644 --- a/editor/src/dialog/dialogs/coming_soon_dialog.rs +++ b/editor/src/dialog/dialogs/coming_soon_dialog.rs @@ -1,4 +1,5 @@ -use crate::{layout::widgets::*, message_prelude::FrontendMessage}; +use crate::layout::widgets::*; +use crate::message_prelude::FrontendMessage; use std::fmt::Write; diff --git a/editor/src/dialog/dialogs/error_dialog.rs b/editor/src/dialog/dialogs/error_dialog.rs index 6d6bc578f..3041e02ef 100644 --- a/editor/src/dialog/dialogs/error_dialog.rs +++ b/editor/src/dialog/dialogs/error_dialog.rs @@ -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 { diff --git a/editor/src/document/document_message.rs b/editor/src/document/document_message.rs index fdea73498..e21c7d445 100644 --- a/editor/src/document/document_message.rs +++ b/editor/src/document/document_message.rs @@ -53,7 +53,9 @@ pub enum DocumentMessage { layer_path: Vec, }, DeleteSelectedLayers, + DeleteSelectedVectorPoints, DeselectAllLayers, + DeselectAllVectorPoints, DirtyRenderDocument, DirtyRenderDocumentInOutlineView, DocumentHistoryBackward, @@ -81,6 +83,11 @@ pub enum DocumentMessage { insert_index: isize, reverse_index: bool, }, + MoveSelectedVectorPoints { + layer_path: Vec, + 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, }, + ToggleSelectedHandleMirroring { + layer_path: Vec, + toggle_distance: bool, + toggle_angle: bool, + }, Undo, UngroupLayers { folder_path: Vec, diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index bdfe1ebec..6503b6ddc 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -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, font_cache: &FontCache) -> Vec { - 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::>() - } - pub fn selected_layers(&self) -> impl Iterator { 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 { + self.selected_visible_layers() + .flat_map(|layer| self.graphene_document.layer(layer)) + .flat_map(|layer| layer.as_vector_shape_copy()) + .collect::>() + } + + /// 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::>() + } + /// 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>>, ignore_artboard: Option, font_cache: &'a FontCache) -> impl Iterator + 'a { self.visible_layers() @@ -998,14 +992,28 @@ impl MessageHandler { + 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 { + 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 { + responses.push_back( + DocumentOperation::SetSelectedHandleMirroring { + layer_path, + toggle_distance, + toggle_angle, + } + .into(), + ); + } Undo => { responses.push_back(BroadcastSignal::ToolAbort.into()); responses.push_back(DocumentHistoryBackward.into()); diff --git a/editor/src/document/portfolio_message_handler.rs b/editor/src/document/portfolio_message_handler.rs index 59ccd1a71..4ef4ad879 100644 --- a/editor/src/document/portfolio_message_handler.rs +++ b/editor/src/document/portfolio_message_handler.rs @@ -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}; diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 4e969e591..d421cc154 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -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}, diff --git a/editor/src/layout/widgets.rs b/editor/src/layout/widgets.rs index 3e5adfc2b..cced18267 100644 --- a/editor/src/layout/widgets.rs +++ b/editor/src/layout/widgets.rs @@ -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 { diff --git a/editor/src/viewport_tools/snapping.rs b/editor/src/viewport_tools/snapping.rs index f47842995..79527edd1 100644 --- a/editor/src/viewport_tools/snapping.rs +++ b/editor/src/viewport_tools/snapping.rs @@ -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); } } } diff --git a/editor/src/viewport_tools/tools/artboard_tool.rs b/editor/src/viewport_tools/tools/artboard_tool.rs index 6696ddfb1..8fb5aa676 100644 --- a/editor/src/viewport_tools/tools/artboard_tool.rs +++ b/editor/src/viewport_tools/tools/artboard_tool.rs @@ -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 { diff --git a/editor/src/viewport_tools/tools/freehand_tool.rs b/editor/src/viewport_tools/tools/freehand_tool.rs index f7361b72e..17578c0de 100644 --- a/editor/src/viewport_tools/tools/freehand_tool.rs +++ b/editor/src/viewport_tools/tools/freehand_tool.rs @@ -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()); diff --git a/editor/src/viewport_tools/tools/gradient_tool.rs b/editor/src/viewport_tools/tools/gradient_tool.rs index 3e0c22fea..905b9e1c5 100644 --- a/editor/src/viewport_tools/tools/gradient_tool.rs +++ b/editor/src/viewport_tools/tools/gradient_tool.rs @@ -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 { diff --git a/editor/src/viewport_tools/tools/line_tool.rs b/editor/src/viewport_tools/tools/line_tool.rs index 1a85149c7..901cf841f 100644 --- a/editor/src/viewport_tools/tools/line_tool.rs +++ b/editor/src/viewport_tools/tools/line_tool.rs @@ -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()); diff --git a/editor/src/viewport_tools/tools/path_tool.rs b/editor/src/viewport_tools/tools/path_tool.rs index eeb8667ef..e335ebdce 100644 --- a/editor/src/viewport_tools/tools/path_tool.rs +++ b/editor/src/viewport_tools/tools/path_tool.rs @@ -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> 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::>(); + 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::>(); - 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 } ( diff --git a/editor/src/viewport_tools/tools/pen_tool.rs b/editor/src/viewport_tools/tools/pen_tool.rs index 0b85fe029..f38736e26 100644 --- a/editor/src/viewport_tools/tools/pen_tool.rs +++ b/editor/src/viewport_tools/tools/pen_tool.rs @@ -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> 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>, - curve_shape: VectorShape, - bez_path: Vec, + 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, ) -> 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) { - // 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>, 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) { - 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 { - 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, bez_path: Vec, 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)> { + 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(), + ) + }) } diff --git a/editor/src/viewport_tools/tools/select_tool.rs b/editor/src/viewport_tools/tools/select_tool.rs index 7cf8bd27b..65112b0c6 100644 --- a/editor/src/viewport_tools/tools/select_tool.rs +++ b/editor/src/viewport_tools/tools/select_tool.rs @@ -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::>()); + .add_all_document_handles(document, &[], &selected.iter().map(|x| x.as_slice()).collect::>(), &[]); tool_data.layers_dragging = selected; diff --git a/editor/src/viewport_tools/tools/shared/path_outline.rs b/editor/src/viewport_tools/tools/shared/path_outline.rs index 207358e6b..a59e82b13 100644 --- a/editor/src/viewport_tools/tools/shared/path_outline.rs +++ b/editor/src/viewport_tools/tools/shared/path_outline.rs @@ -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 diff --git a/editor/src/viewport_tools/tools/shared/resize.rs b/editor/src/viewport_tools/tools/shared/resize.rs index a20d25095..49e08b39c 100644 --- a/editor/src/viewport_tools/tools/shared/resize.rs +++ b/editor/src/viewport_tools/tools/shared/resize.rs @@ -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, 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); } diff --git a/editor/src/viewport_tools/tools/shared/transformation_cage.rs b/editor/src/viewport_tools/tools/shared/transformation_cage.rs index a063b16c0..82f7edfc3 100644 --- a/editor/src/viewport_tools/tools/shared/transformation_cage.rs +++ b/editor/src/viewport_tools/tools/shared/transformation_cage.rs @@ -140,10 +140,11 @@ impl SelectedEdges { pub fn add_bounding_box(responses: &mut Vec) -> Vec { 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) -> [Vec; 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()); diff --git a/editor/src/viewport_tools/tools/spline_tool.rs b/editor/src/viewport_tools/tools/spline_tool.rs index 0bac70b03..aa5f929e1 100644 --- a/editor/src/viewport_tools/tools/spline_tool.rs +++ b/editor/src/viewport_tools/tools/spline_tool.rs @@ -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()); diff --git a/editor/src/viewport_tools/tools/text_tool.rs b/editor/src/viewport_tools/tools/text_tool.rs index fa91ba097..ce72805b5 100644 --- a/editor/src/viewport_tools/tools/text_tool.rs +++ b/editor/src/viewport_tools/tools/text_tool.rs @@ -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>, responses: &mut VecDeque { 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()); diff --git a/editor/src/viewport_tools/vector_editor/constants.rs b/editor/src/viewport_tools/vector_editor/constants.rs index a620a29b3..095cfb045 100644 --- a/editor/src/viewport_tools/vector_editor/constants.rs +++ b/editor/src/viewport_tools/vector_editor/constants.rs @@ -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 Index 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 IndexMut for [T; 3] { - fn index_mut(&mut self, mt: ControlPointType) -> &mut T { - &mut self[mt as usize] - } -} diff --git a/editor/src/viewport_tools/vector_editor/mod.rs b/editor/src/viewport_tools/vector_editor/mod.rs index df324f393..93480221e 100644 --- a/editor/src/viewport_tools/vector_editor/mod.rs +++ b/editor/src/viewport_tools/vector_editor/mod.rs @@ -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; diff --git a/editor/src/viewport_tools/vector_editor/overlay_renderer.rs b/editor/src/viewport_tools/vector_editor/overlay_renderer.rs new file mode 100644 index 000000000..1cc4de446 --- /dev/null +++ b/editor/src/viewport_tools/vector_editor/overlay_renderer.rs @@ -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>; 5]; +type AnchorId = u64; + +const POINT_STROKE_WEIGHT: f64 = 2.; + +#[derive(Clone, Debug, Default)] +pub struct OverlayRenderer { + shape_overlay_cache: HashMap>, + 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, responses: &mut VecDeque) { + 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, responses: &mut VecDeque) { + 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, visibility: bool, responses: &mut VecDeque) { + 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) -> Vec { + 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) -> Vec { + 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) -> Vec { + 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, responses: &mut VecDeque) -> Option> { + 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) -> Vec { + 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, responses: &mut VecDeque) -> Option> { + handle.as_ref().map(|_| Self::create_handle_line_overlay(responses)) + } + + fn place_outline_overlays(outline_path: Vec, parent_transform: &DAffine2, responses: &mut VecDeque) { + 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, vector_path: VectorShape, responses: &mut VecDeque) { + 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) { + 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>, marker_source: &mut Option>| { + 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) { + 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, responses: &mut VecDeque) { + 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) { + 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, visibility: bool, responses: &mut VecDeque) { + responses.push_back(Self::overlay_visibility_message(overlay_path, visibility)); + } + + /// Create a visibility message for an overlay + fn overlay_visibility_message(layer_path: Vec, 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, 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, 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) { + // 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()); + } + } + } + } +} diff --git a/editor/src/viewport_tools/vector_editor/shape_editor.rs b/editor/src/viewport_tools/vector_editor/shape_editor.rs index 713fd4213..7ad3d9349 100644 --- a/editor/src/viewport_tools/vector_editor/shape_editor.rs +++ b/editor/src/viewport_tools/vector_editor/shape_editor.rs @@ -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; 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, - // Index of the shape that contained the most recent selected point - pub selected_shape_indices: HashSet, + // The layers we can select and edit anchors / handles from + selected_layers: Vec>, } +// 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) -> 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, + ) -> Option> { + 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::>(); + + // 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>) { + self.selected_layers = target_layers; + } + + pub fn selected_layers(&self) -> &Vec> { + &self.selected_layers + } + + pub fn selected_layers_ref(&self) -> Vec<&[LayerId]> { + self.selected_layers.iter().map(|l| l.as_slice()).collect::>() + } + + /// 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 { + 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 { + 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 { + 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) { + 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) { + 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) { + 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) { + responses.push_back(DocumentMessage::DeselectAllVectorPoints.into()); + } + + /// Iterate over the shapes + pub fn iter<'a>(&'a self, document: &'a Document) -> impl Iterator + '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) { - 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) { - // 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) { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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) { - 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) { - 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) { - 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() + } } diff --git a/editor/src/viewport_tools/vector_editor/vector_anchor.rs b/editor/src/viewport_tools/vector_editor/vector_anchor.rs deleted file mode 100644 index a2dbe49b9..000000000 --- a/editor/src/viewport_tools/vector_editor/vector_anchor.rs +++ /dev/null @@ -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; 3], - // The overlays for this handle line rendering - pub handle_line_overlays: (Option>, Option>), - - // Does this anchor point have a path close element? - pub close_element_id: Option, - // 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) -> 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) { - 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 { - 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 { - 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 { - 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) { - 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) { - 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>| { - 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) { - 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) { - 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>| { - 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) { - 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) { - 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) { - 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, visibility: bool) -> Message { - DocumentMessage::Overlays( - Operation::SetLayerVisibility { - path: layer_path, - visible: visibility, - } - .into(), - ) - .into() - } -} diff --git a/editor/src/viewport_tools/vector_editor/vector_control_point.rs b/editor/src/viewport_tools/vector_editor/vector_control_point.rs deleted file mode 100644 index 698a22c8c..000000000 --- a/editor/src/viewport_tools/vector_editor/vector_control_point.rs +++ /dev/null @@ -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>, - // 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) { - 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) { - 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(), - ); - } - } -} diff --git a/editor/src/viewport_tools/vector_editor/vector_shape.rs b/editor/src/viewport_tools/vector_editor/vector_shape.rs deleted file mode 100644 index 15b037efe..000000000 --- a/editor/src/viewport_tools/vector_editor/vector_shape.rs +++ /dev/null @@ -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, - /// The outline of the shape via kurbo - pub bez_path: kurbo::BezPath, - /// The elements of the kurbo shape - pub elements: Vec, - /// The anchors that are made up of the control points / handles - pub anchors: Vec, - /// The overlays for the shape, anchors and manipulator handles - pub shape_overlay: Option>, - /// 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, -} -type IndexedEl = (usize, kurbo::PathEl); - -impl VectorShape { - pub fn new(layer_path: Vec, transform: DAffine2, bez_path: &BezPath, closed: bool, responses: &mut VecDeque) -> 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) { - 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) { - 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) { - 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 { - 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 { - 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 { - 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) { - 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) { - 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, second: Option, responses: &mut VecDeque) -> 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, 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, - to_replace: usize, - first_path_element: Option, - last_path_element: Option, - recent_move_to: Option, - responses: &mut VecDeque, - ) { - 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) -> Vec { - // 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::>(); - - // Create the manipulation points - let mut anchors: Vec = vec![]; - let (mut first_path_element, mut last_path_element): (Option, Option) = (None, None); - let mut last_move_to_element: Option = 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) -> Vec { - 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) -> Vec { - 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) -> Vec { - 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, responses: &mut VecDeque) -> Option> { - 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) { - 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) { - 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) { - 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) { - 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) { - 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) { - 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) { - 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) { - 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) { - 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) { - 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) { - for anchor in &self.anchors { - anchor.set_handle_visiblity(visibility, responses); - } - } -} diff --git a/frontend/wasm/src/helpers.rs b/frontend/wasm/src/helpers.rs index e5794cbd4..caab8b783 100644 --- a/frontend/wasm/src/helpers.rs +++ b/frontend/wasm/src/helpers.rs @@ -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::*; diff --git a/graphene/src/boolean_ops.rs b/graphene/src/boolean_ops.rs index cb9cf35e9..7cd0d8d35 100644 --- a/graphene/src/boolean_ops.rs +++ b/graphene/src/boolean_ops.rs @@ -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, 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); } } diff --git a/graphene/src/document.rs b/graphene/src/document.rs index ab0fe734c..edafc5900 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -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 { + self.root.iter().flat_map(|layer| layer.as_vector_shape_copy()).collect::>() + } + + /// 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::>() + } + + /// 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, shapes: Vec) { + paths.zip(shapes).for_each(|(path, shape)| self.set_vector_shape(path, shape)); + } + pub fn common_layer_path_prefix<'a>(&self, layers: impl Iterator) -> &'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) } diff --git a/graphene/src/intersection.rs b/graphene/src/intersection.rs index 5ffc586b4..b2dfdaf47 100644 --- a/graphene/src/intersection.rs +++ b/graphene/src/intersection.rs @@ -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::() / 4. + } } impl Mul 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] diff --git a/graphene/src/layers/id_vec.rs b/graphene/src/layers/id_vec.rs new file mode 100644 index 000000000..4306517a4 --- /dev/null +++ b/graphene/src/layers/id_vec.rs @@ -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 { + /// Contained elements + elements: Vec, + /// The IDs of the [Elements] contained within this + element_ids: Vec, + /// The ID that will be assigned to the next element that is added to this + #[serde(skip)] + next_id: ElementId, +} + +impl IdBackedVec { + /// Push a new element to the start of the vector + pub fn push_front(&mut self, element: T) -> Option { + 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 { + 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 { + 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 { + self.push_end(element) + } + + /// Add a range of elements of elements to the end of this vector + pub fn push_range(&mut self, elements: I) -> Vec + where + I: IntoIterator, + { + 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 { + 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 { + 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 { + 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 { + // 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 Default for IdBackedVec { + fn default() -> Self { + IdBackedVec { + elements: vec![], + element_ids: vec![], + next_id: 0, + } + } +} + +/// Allows for usage of UniqueElements as a Vec +impl Deref for IdBackedVec { + 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 +impl DerefMut for IdBackedVec { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.elements + } +} + +/// Allows use with iterators +/// Also allows constructing UniqueElements with collect +impl FromIterator for IdBackedVec { + fn from_iter>(iter: T) -> Self { + let mut new = IdBackedVec::default(); + // Add to the end of the existing elements + new.push_range(iter); + new + } +} diff --git a/graphene/src/layers/layer_info.rs b/graphene/src/layers/layer_info.rs index 3b5a53489..6bda061b2 100644 --- a/graphene/src/layers/layer_info.rs +++ b/graphene/src/layers/layer_info.rs @@ -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, /// "\ - /// \ + /// \ /// " /// ); /// ``` @@ -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 { + 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> { diff --git a/graphene/src/layers/mod.rs b/graphene/src/layers/mod.rs index c89041eea..7cd3efc8c 100644 --- a/graphene/src/layers/mod.rs +++ b/graphene/src/layers/mod.rs @@ -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; diff --git a/graphene/src/layers/shape_layer.rs b/graphene/src/layers/shape_layer.rs index fc1624307..7b41703c8 100644 --- a/graphene/src/layers/shape_layer.rs +++ b/graphene/src/layers/shape_layer.rs @@ -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 /// [``](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, 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, ""); 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#""#, - 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(""); } 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, intersections: &mut Vec>, _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>, 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: pub fn spline(points: Vec>, 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, } } } diff --git a/graphene/src/layers/text_layer.rs b/graphene/src/layers/text_layer.rs index 2f436ed5f..c717498b4 100644 --- a/graphene/src/layers/text_layer.rs +++ b/graphene/src/layers/text_layer.rs @@ -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, + pub cached_path: Option, } 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, intersections: &mut Vec>, 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) -> BezPath { - if self.cached_path.as_ref().filter(|x| !x.is_empty()).is_none() { + pub fn to_vector_path(&mut self, buzz_face: Option) -> 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) -> BezPath { - to_kurbo::to_kurbo(&self.text, buzz_face, self.size, self.line_width) + pub fn generate_path(&self, buzz_face: Option) -> 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) -> 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) -> 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) { diff --git a/graphene/src/layers/text_layer/to_kurbo.rs b/graphene/src/layers/text_layer/to_path.rs similarity index 61% rename from graphene/src/layers/text_layer/to_kurbo.rs rename to graphene/src/layers/text_layer/to_path.rs index 98ee3d8d8..e8c3e8fbb 100644 --- a/graphene/src/layers/text_layer/to_kurbo.rs +++ b/graphene/src/layers/text_layer/to_path.rs @@ -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, glyph_buffer: &GlyphBuffer, scale: f64, x_ false } -pub fn to_kurbo(str: &str, buzz_face: Option, font_size: f64, line_width: Option) -> BezPath { +pub fn to_path(str: &str, buzz_face: Option, font_size: f64, line_width: Option) -> 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, 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 } diff --git a/graphene/src/layers/vector/constants.rs b/graphene/src/layers/vector/constants.rs new file mode 100644 index 000000000..b7f14062e --- /dev/null +++ b/graphene/src/layers/vector/constants.rs @@ -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 Index 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 IndexMut 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.; diff --git a/graphene/src/layers/vector/mod.rs b/graphene/src/layers/vector/mod.rs new file mode 100644 index 000000000..e87d4866a --- /dev/null +++ b/graphene/src/layers/vector/mod.rs @@ -0,0 +1,4 @@ +pub mod constants; +pub mod vector_anchor; +pub mod vector_control_point; +pub mod vector_shape; diff --git a/graphene/src/layers/vector/vector_anchor.rs b/graphene/src/layers/vector/vector_anchor.rs new file mode 100644 index 000000000..3e9be5dbb --- /dev/null +++ b/graphene/src/layers/vector/vector_anchor.rs @@ -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; 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; 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, handle_out_pos: Option) -> 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 { + self.points.iter().flatten() + } + + /// Provides the points in this anchor + pub fn points_mut(&mut self) -> impl Iterator { + self.points.iter_mut().flatten() + } + + /// Provides the selected points in this anchor + pub fn selected_points(&self) -> impl Iterator { + 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 { + 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 { + 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 { + 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, + } + } +} diff --git a/graphene/src/layers/vector/vector_control_point.rs b/graphene/src/layers/vector/vector_control_point.rs new file mode 100644 index 000000000..41414c766 --- /dev/null +++ b/graphene/src/layers/vector/vector_control_point.rs @@ -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, + } + } +} diff --git a/graphene/src/layers/vector/vector_shape.rs b/graphene/src/layers/vector/vector_shape.rs new file mode 100644 index 000000000..a3215b72b --- /dev/null +++ b/graphene/src/layers/vector/vector_shape.rs @@ -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); + +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, 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(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>(points: Vec) -> 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>(points: Vec) -> Self { + let mut new = Self::default(); + // shadow `points` + let points: Vec = points.into_iter().map(Into::::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 = 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 { + self.anchors().iter().filter(|anchor| anchor.is_anchor_selected()) + } + + /// Return all the selected anchors, mutable + pub fn selected_anchors_mut(&mut self) -> impl Iterator { + 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 { + 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 { + self.anchors_mut().iter_mut().filter(|anchor| anchor.any_points_selected()) + } + + /// An alias for `self.0` + pub fn anchors(&self) -> &IdBackedVec { + &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 { + &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>::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; 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> From 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) +} diff --git a/graphene/src/operation.rs b/graphene/src/operation.rs index 651e6593b..8f9bc14ae 100644 --- a/graphene/src/operation.rs +++ b/graphene/src/operation.rs @@ -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, - transform: [f64; 6], - style: style::PathStyle, - }, AddRect { path: Vec, insert_index: isize, transform: [f64; 6], style: style::PathStyle, }, - AddOverlayRect { - path: Vec, - transform: [f64; 6], - style: style::PathStyle, - }, AddLine { path: Vec, insert_index: isize, transform: [f64; 6], style: style::PathStyle, }, - AddOverlayLine { - path: Vec, - transform: [f64; 6], - style: style::PathStyle, - }, AddText { path: Vec, transform: [f64; 6], @@ -97,19 +85,12 @@ pub enum Operation { sides: u32, style: style::PathStyle, }, - AddOverlayShape { - path: Vec, - bez_path: kurbo::BezPath, - style: style::PathStyle, - closed: bool, - }, AddShape { path: Vec, 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, }, + DeleteSelectedVectorPoints { + layer_paths: Vec>, + }, + DeselectVectorPoints { + layer_path: Vec, + point_ids: Vec<(u64, ControlPointType)>, + }, + DeselectAllVectorPoints { + layer_path: Vec, + }, DuplicateLayer { path: Vec, }, @@ -127,6 +118,11 @@ pub enum Operation { font_style: String, size: f64, }, + MoveSelectedVectorPoints { + layer_path: Vec, + delta: (f64, f64), + absolute_position: (f64, f64), + }, RenameLayer { layer_path: Vec, new_name: String, @@ -151,14 +147,38 @@ pub enum Operation { path: Vec, transform: [f64; 6], }, + SelectVectorPoints { + layer_path: Vec, + point_ids: Vec<(u64, ControlPointType)>, + add: bool, + }, SetShapePath { path: Vec, - bez_path: kurbo::BezPath, + vector_path: VectorShape, }, - SetShapePathInViewport { - path: Vec, - bez_path: kurbo::BezPath, - transform: [f64; 6], + InsertVectorAnchor { + layer_path: Vec, + anchor: VectorAnchor, + after_id: u64, + }, + PushVectorAnchor { + layer_path: Vec, + anchor: VectorAnchor, + }, + RemoveVectorAnchor { + layer_path: Vec, + id: u64, + }, + MoveVectorPoint { + layer_path: Vec, + id: u64, + control_type: ControlPointType, + position: (f64, f64), + }, + RemoveVectorPoint { + layer_path: Vec, + id: u64, + control_type: ControlPointType, }, TransformLayerInScope { path: Vec, @@ -205,6 +225,11 @@ pub enum Operation { path: Vec, stroke: Stroke, }, + SetSelectedHandleMirroring { + layer_path: Vec, + toggle_distance: bool, + toggle_angle: bool, + }, } impl Operation {