diff --git a/README.md b/README.md index cafafe2e8e..554d7cd85a 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,13 @@ Learn more from the [website](https://graphite.rs/), subscribe to the [newslette ## Screenshots -[!["Painted Dreams" vector artwork](https://static.graphite.rs/content/index/gui-demo-painted-dreams__2.png)](https://editor.graphite.rs/#demo/painted-dreams) +[!["Painted Dreams" vector artwork](https://static.graphite.rs/content/index/gui-demo-painted-dreams__3.png)](https://editor.graphite.rs/#demo/painted-dreams) -![Magazine spread](https://static.graphite.rs/content/index/magazine-page-layout.png) +![Magazine spread](https://static.graphite.rs/content/index/magazine-page-layout__2.png) -[!["Valley of Spires" vector artwork](https://static.graphite.rs/content/index/gui-demo-node-graph-valley-of-spires__2.png)](https://editor.graphite.rs/#demo/valley-of-spires) +[!["Isometric Fountain" vector artwork](https://static.graphite.rs/content/index/gui-demo-node-graph-isometric-fountain.png)](https://editor.graphite.rs/#demo/isometric-fountain) -!["Marbled Mandelbrot" fractal raster artwork](https://static.graphite.rs/content/index/gui-demo-fractal__2.png) +!["Marbled Mandelbrot" fractal raster artwork](https://static.graphite.rs/content/index/gui-demo-fractal__3.png) ## Contributing/building the code diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 137565ed62..1450c4f45c 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -58,17 +58,7 @@ pub enum FrontendMessage { #[serde(rename = "commitDate")] commit_date: String, }, - TriggerCopyToClipboardBlobUrl { - #[serde(rename = "blobUrl")] - blob_url: String, - }, TriggerDelayedZoomCanvasToFitAll, - TriggerDownloadBlobUrl { - #[serde(rename = "layerName")] - layer_name: String, - #[serde(rename = "blobUrl")] - blob_url: String, - }, TriggerDownloadImage { svg: String, name: String, @@ -99,9 +89,6 @@ pub enum FrontendMessage { TriggerLoadPreferences, TriggerOpenDocument, TriggerPaste, - TriggerRevokeBlobUrl { - url: String, - }, TriggerSavePreferences { preferences: PreferencesMessageHandler, }, @@ -294,8 +281,4 @@ pub enum FrontendMessage { layout_target: LayoutTarget, diff: Vec, }, - UpdateZoomWithScroll { - #[serde(rename = "zoomWithScroll")] - zoom_with_scroll: bool, - }, } diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index f29c0fb2be..a684eba092 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -258,6 +258,8 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(MouseRight); action_dispatch=PenToolMessage::Confirm), entry!(KeyDown(Escape); action_dispatch=PenToolMessage::Confirm), entry!(KeyDown(Enter); action_dispatch=PenToolMessage::Confirm), + entry!(KeyDown(Delete); action_dispatch=PenToolMessage::RemovePreviousHandle), + entry!(KeyDown(Backspace); action_dispatch=PenToolMessage::RemovePreviousHandle), // // FreehandToolMessage entry!(PointerMove; action_dispatch=FreehandToolMessage::PointerMove), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index a1775eafc7..0b4c0a6b59 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1695,7 +1695,7 @@ impl DocumentMessageHandler { .unwrap_or(0) } - /// Loads layer resources such as creating the blob URLs for the images and loading all of the fonts in the document. + /// Loads all of the fonts in the document. pub fn load_layer_resources(&self, responses: &mut VecDeque) { let mut fonts = HashSet::new(); for (_node_id, node) in self.document_network().recursive_nodes() { diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index c99d83e3b4..b71d3e3874 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -41,16 +41,16 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape bezier_rs::BezierHandles::Quadratic { handle } if not_under_anchor(handle, bezier.start) && not_under_anchor(handle, bezier.end) => { overlay_context.line(handle, bezier.start, None); overlay_context.line(handle, bezier.end, None); - overlay_context.manipulator_handle(handle, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id))); + overlay_context.manipulator_handle(handle, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)), None); } bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => { if not_under_anchor(handle_start, bezier.start) { overlay_context.line(handle_start, bezier.start, None); - overlay_context.manipulator_handle(handle_start, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id))); + overlay_context.manipulator_handle(handle_start, is_selected(selected, ManipulatorPointId::PrimaryHandle(segment_id)), None); } if not_under_anchor(handle_end, bezier.end) { overlay_context.line(handle_end, bezier.end, None); - overlay_context.manipulator_handle(handle_end, is_selected(selected, ManipulatorPointId::EndHandle(segment_id))); + overlay_context.manipulator_handle(handle_end, is_selected(selected, ManipulatorPointId::EndHandle(segment_id)), None); } } _ => {} diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index f35f841852..edf903bdf5 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -127,7 +127,7 @@ impl OverlayContext { } } - pub fn manipulator_handle(&mut self, position: DVec2, selected: bool) { + pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { let position = position.round() - DVec2::splat(0.5); self.render_context.begin_path(); @@ -137,7 +137,7 @@ impl OverlayContext { let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; self.render_context.set_fill_style_str(fill); - self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); + self.render_context.set_stroke_style_str(color.unwrap_or(COLOR_OVERLAY_BLUE)); self.render_context.fill(); self.render_context.stroke(); } diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index b3c99e594f..6db72e8e56 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -91,7 +91,6 @@ impl SnappingState { match target { SnapTarget::BoundingBox(target) => match target { BoundingBoxSnapTarget::CornerPoint => self.bounding_box.corner_point, - BoundingBoxSnapTarget::AlongEdge => self.bounding_box.along_edge, BoundingBoxSnapTarget::EdgeMidpoint => self.bounding_box.edge_midpoint, BoundingBoxSnapTarget::CenterPoint => self.bounding_box.center_point, }, @@ -106,7 +105,7 @@ impl SnappingState { SnapTarget::Artboard(_) => self.artboards, SnapTarget::Grid(_) => self.grid_snapping, SnapTarget::Alignment(AlignmentSnapTarget::AlignWithAnchorPoint) => self.path.align_with_anchor_point, - SnapTarget::Alignment(_) => self.bounding_box.align_with_corner_point, + SnapTarget::Alignment(_) => self.bounding_box.align_with_edges, SnapTarget::DistributeEvenly(_) => self.bounding_box.distribute_evenly, _ => false, } @@ -119,8 +118,7 @@ pub struct BoundingBoxSnapping { pub center_point: bool, pub corner_point: bool, pub edge_midpoint: bool, - pub along_edge: bool, - pub align_with_corner_point: bool, + pub align_with_edges: bool, pub distribute_evenly: bool, } @@ -130,8 +128,7 @@ impl Default for BoundingBoxSnapping { center_point: true, corner_point: true, edge_midpoint: true, - along_edge: true, - align_with_corner_point: true, + align_with_edges: true, distribute_evenly: true, } } @@ -378,13 +375,11 @@ impl fmt::Display for SnapSource { } type GetSnapState = for<'a> fn(&'a mut SnappingState) -> &'a mut bool; -pub const SNAP_FUNCTIONS_FOR_BOUNDING_BOXES: [(&str, GetSnapState, &str); 6] = [ +pub const SNAP_FUNCTIONS_FOR_BOUNDING_BOXES: [(&str, GetSnapState, &str); 5] = [ ( - // TODO: Rename to "Beyond Edges" and update behavior to snap to an infinite extension of the bounding box edges - // TODO: (even when the layer is locally rotated) instead of horizontally/vertically aligning with the corner points - "Align with Corner Points", - (|snapping_state| &mut snapping_state.bounding_box.align_with_corner_point) as GetSnapState, - "Snaps to horizontal/vertical alignment with the corner points of any layer's bounding box", + "Align with Edges", + (|snapping_state| &mut snapping_state.bounding_box.align_with_edges) as GetSnapState, + "Snaps to horizontal/vertical alignment with the edges of any layer's bounding box", ), ( "Corner Points", @@ -401,11 +396,6 @@ pub const SNAP_FUNCTIONS_FOR_BOUNDING_BOXES: [(&str, GetSnapState, &str); 6] = [ (|snapping_state| &mut snapping_state.bounding_box.edge_midpoint) as GetSnapState, "Snaps to any of the four points at the middle of the edges of any layer's bounding box", ), - ( - "Along Edges", - (|snapping_state| &mut snapping_state.bounding_box.along_edge) as GetSnapState, - "Snaps anywhere along the four edges of any layer's bounding box", - ), ( "Distribute Evenly", (|snapping_state| &mut snapping_state.bounding_box.distribute_evenly) as GetSnapState, @@ -463,7 +453,6 @@ pub enum BoundingBoxSnapTarget { CornerPoint, CenterPoint, EdgeMidpoint, - AlongEdge, } impl fmt::Display for BoundingBoxSnapTarget { @@ -472,7 +461,6 @@ impl fmt::Display for BoundingBoxSnapTarget { BoundingBoxSnapTarget::CornerPoint => write!(f, "Bounding Box: Corner Point"), BoundingBoxSnapTarget::CenterPoint => write!(f, "Bounding Box: Center Point"), BoundingBoxSnapTarget::EdgeMidpoint => write!(f, "Bounding Box: Edge Midpoint"), - BoundingBoxSnapTarget::AlongEdge => write!(f, "Bounding Box: Along Edge"), } } } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 9482d2f2a8..f0f4d25148 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -304,7 +304,7 @@ impl MessageHandler> for PortfolioMes let () = fut.await; use wasm_bindgen::prelude::*; - #[wasm_bindgen(module = "/../frontend/src/wasm-communication/editor.ts")] + #[wasm_bindgen(module = "/../frontend/src/editor.ts")] extern "C" { #[wasm_bindgen(js_name = injectImaginatePollServerStatus)] fn inject(); diff --git a/editor/src/messages/preferences/preferences_message_handler.rs b/editor/src/messages/preferences/preferences_message_handler.rs index 940c8aed3a..f83234f502 100644 --- a/editor/src/messages/preferences/preferences_message_handler.rs +++ b/editor/src/messages/preferences/preferences_message_handler.rs @@ -96,7 +96,6 @@ impl MessageHandler for PreferencesMessageHandler { true => MappingVariant::ZoomWithScroll, }; responses.add(KeyMappingMessage::ModifyMapping(variant)); - responses.add(FrontendMessage::UpdateZoomWithScroll { zoom_with_scroll }); } } diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index d53b904335..6ecdbcea1d 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -116,6 +116,20 @@ impl ClosestSegment { (stroke_width_sq + tolerance_sq) < dist_sq } + pub fn handle_positions(&self, document_metadata: &DocumentMetadata) -> (Option, Option) { + // Transform to viewport space + let transform = document_metadata.transform_to_viewport(self.layer); + + // Split the Bezier at the parameter `t` + let [first, second] = self.bezier.split(TValue::Parametric(self.t)); + + // Transform the handle positions to viewport space + let first_handle = first.handle_end().map(|handle| transform.transform_point2(handle)); + let second_handle = second.handle_start().map(|handle| transform.transform_point2(handle)); + + (first_handle, second_handle) + } + pub fn adjusted_insert(&self, responses: &mut VecDeque) -> PointId { let layer = self.layer; let [first, second] = self.bezier.split(TValue::Parametric(self.t)); diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index a289ee7fde..9e402d41b6 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -8,7 +8,7 @@ pub use {alignment_snapper::*, distribution_snapper::*, grid_snapper::*, layer_s use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_SNAP_BACKGROUND, COLOR_OVERLAY_WHITE}; use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, Pivot}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GridSnapTarget, PathSnapTarget, SnapTarget}; +use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, PathSnapTarget, SnapTarget}; use crate::messages::prelude::*; use bezier_rs::{Subpath, TValue}; @@ -135,14 +135,7 @@ fn get_closest_line(lines: &[SnappedLine]) -> Option<&SnappedPoint> { fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option { let mut best = None; for curve_i in curves { - if curve_i.point.target == SnapTarget::BoundingBox(BoundingBoxSnapTarget::AlongEdge) { - continue; - } - for curve_j in curves { - if curve_j.point.target == SnapTarget::BoundingBox(BoundingBoxSnapTarget::AlongEdge) { - continue; - } if curve_i.start == curve_j.start && curve_i.layer == curve_j.layer { continue; } @@ -468,10 +461,10 @@ impl SnapManager { overlay_context.line(viewport, target, None); } for &target in align.iter().flatten() { - overlay_context.manipulator_handle(target, false); + overlay_context.manipulator_handle(target, false, None); } if any_align { - overlay_context.manipulator_handle(viewport, false); + overlay_context.manipulator_handle(viewport, false, None); } if !any_align && ind.distribution_equal_distance_x.is_none() && ind.distribution_equal_distance_y.is_none() { diff --git a/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs index 76cbb66e7f..b99b1ecc28 100644 --- a/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs @@ -19,7 +19,7 @@ impl AlignmentSnapper { let document = snap_data.document; self.bounding_box_points.clear(); - if !document.snapping_state.bounding_box.align_with_corner_point { + if !document.snapping_state.bounding_box.align_with_edges { return; } @@ -74,7 +74,9 @@ impl AlignmentSnapper { Quad::intersect_rays(target_point.document_point, DVec2::X, origin, direction), ] } else { - [DVec2::new(point.document_point.x, target_position.y), DVec2::new(target_position.x, point.document_point.y)].map(Some) + let Some(quad) = target_point.quad.map(|quad| quad.0) else { continue }; + let edges = [quad[1] - quad[0], quad[3] - quad[0]]; + edges.map(|edge| edge.try_normalize().map(|edge| (point.document_point - target_position).project_onto(edge) + target_position)) }; let target_path = matches!(target_point.target, SnapTarget::Path(_)); diff --git a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs index fb1d6767fc..a630ecd202 100644 --- a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs @@ -86,9 +86,6 @@ impl LayerSnapper { } } } - if !snap_data.ignore_bounds(layer) { - self.add_layer_bounds(document, layer, SnapTarget::BoundingBox(BoundingBoxSnapTarget::AlongEdge)); - } } } diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index ca855b90d7..239f0acc09 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -76,8 +76,8 @@ impl MessageHandler> for ToolMessageHandler { self.tool_is_active = true; // Send the old and new tools a transition to their FSM Abort states - let mut send_abort_to_tool = |tool_type, update_hints_and_cursor: bool| { - if let Some(tool) = tool_data.tools.get_mut(&tool_type) { + let mut send_abort_to_tool = |old_tool: ToolType, new_tool: ToolType, update_hints_and_cursor: bool| { + if let Some(tool) = tool_data.tools.get_mut(&new_tool) { let mut data = ToolActionHandlerData { document, document_id, @@ -101,9 +101,14 @@ impl MessageHandler> for ToolMessageHandler { tool.process_message(ToolMessage::UpdateCursor, responses, &mut data); } } + + if matches!(old_tool, ToolType::Path | ToolType::Select) { + responses.add(TransformLayerMessage::CancelTransformOperation); + } }; - send_abort_to_tool(tool_type, true); - send_abort_to_tool(old_tool, false); + + send_abort_to_tool(old_tool, tool_type, true); + send_abort_to_tool(old_tool, old_tool, false); // Unsubscribe old tool from the broadcaster tool_data.tools.get(&tool_type).unwrap().deactivate(responses); diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index 9e45a30c6e..a6676fb7d1 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -257,15 +257,15 @@ impl Fsm for GradientToolFsmState { let (start, end) = (transform.transform_point2(start), transform.transform_point2(end)); overlay_context.line(start, end, None); - overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start)); - overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End)); + overlay_context.manipulator_handle(start, dragging == Some(GradientDragTarget::Start), None); + overlay_context.manipulator_handle(end, dragging == Some(GradientDragTarget::End), None); for (index, (position, _)) in stops.0.into_iter().enumerate() { if position.abs() < f64::EPSILON * 1000. || (1. - position).abs() < f64::EPSILON * 1000. { continue; } - overlay_context.manipulator_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index))); + overlay_context.manipulator_handle(start.lerp(end, position), dragging == Some(GradientDragTarget::Step(index)), None); } } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index f1f080bc10..f7642467a9 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1,5 +1,5 @@ use super::tool_prelude::*; -use crate::consts::{COLOR_OVERLAY_YELLOW, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; +use crate::consts::{COLOR_OVERLAY_BLUE, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, INSERT_POINT_ON_SEGMENT_TOO_FAR_DISTANCE, SELECTION_THRESHOLD, SELECTION_TOLERANCE}; use crate::messages::portfolio::document::overlays::utility_functions::path_overlays; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -155,7 +155,10 @@ impl LayoutHolder for PathTool { }) .tooltip(colinear_handles_tooltip) .widget_holder(); - let colinear_handles_label = TextLabel::new("Colinear Handles").tooltip(colinear_handles_tooltip).widget_holder(); + let colinear_handles_label = TextLabel::new("Colinear Handles") + .disabled(self.tool_data.selection_status.is_none()) + .tooltip(colinear_handles_tooltip) + .widget_holder(); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![ @@ -521,6 +524,7 @@ impl PathToolData { handle_angle } + #[allow(clippy::too_many_arguments)] fn apply_snapping( &mut self, handle_direction: DVec2, @@ -550,6 +554,7 @@ impl PathToolData { document.metadata().document_to_viewport.transform_vector2(snap_result.snapped_point_document - handle_position) } + #[allow(clippy::too_many_arguments)] fn drag( &mut self, equidistant: bool, @@ -623,7 +628,13 @@ impl Fsm for PathToolFsmState { let state = tool_data.update_insertion(shape_editor, document, responses, input); if let Some(closest_segment) = &tool_data.segment { - overlay_context.manipulator_anchor(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_YELLOW)); + overlay_context.manipulator_anchor(closest_segment.closest_point_to_viewport(), false, Some(COLOR_OVERLAY_BLUE)); + if let (Some(handle1), Some(handle2)) = closest_segment.handle_positions(document.metadata()) { + overlay_context.line(closest_segment.closest_point_to_viewport(), handle1, Some(COLOR_OVERLAY_BLUE)); + overlay_context.line(closest_segment.closest_point_to_viewport(), handle2, Some(COLOR_OVERLAY_BLUE)); + overlay_context.manipulator_handle(handle1, false, Some(COLOR_OVERLAY_BLUE)); + overlay_context.manipulator_handle(handle2, false, Some(COLOR_OVERLAY_BLUE)); + } } responses.add(PathToolMessage::SelectedPointUpdated); @@ -649,19 +660,7 @@ impl Fsm for PathToolFsmState { self } (Self::InsertPoint, PathToolMessage::Escape | PathToolMessage::Delete | PathToolMessage::RightClick) => tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort), - (Self::InsertPoint, PathToolMessage::GRS { key: propagate }) => { - // MAYBE: use `InputMapperMessage::KeyDown(..)` instead - match propagate { - // TODO: Don't use `Key::G` directly, instead take it as a variable from the input mappings list like in all other places - Key::KeyG => responses.add(TransformLayerMessage::BeginGrab), - // TODO: Don't use `Key::R` directly, instead take it as a variable from the input mappings list like in all other places - Key::KeyR => responses.add(TransformLayerMessage::BeginRotate), - // TODO: Don't use `Key::S` directly, instead take it as a variable from the input mappings list like in all other places - Key::KeyS => responses.add(TransformLayerMessage::BeginScale), - _ => warn!("Unexpected GRS key"), - } - tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort) - } + (Self::InsertPoint, PathToolMessage::GRS { key: _ }) => PathToolFsmState::InsertPoint, // Mouse down ( _, diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 356200de3c..845276c002 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -62,6 +62,7 @@ pub enum PenToolMessage { Undo, UpdateOptions(PenOptionsUpdate), RecalculateLatestPointsPosition, + RemovePreviousHandle, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -175,6 +176,7 @@ impl<'a> MessageHandler> for PenTool PointerMove, Confirm, Abort, + RemovePreviousHandle, ), } } @@ -600,12 +602,12 @@ impl Fsm for PenToolFsmState { if self == PenToolFsmState::DraggingHandle && valid(next_anchor, handle_end) { // Draw the handle circle for the currently-being-dragged-out incoming handle (opposite the one currently being dragged out) - overlay_context.manipulator_handle(handle_end, false); + overlay_context.manipulator_handle(handle_end, false, None); } if valid(anchor_start, handle_start) { // Draw the handle circle for the most recently placed anchor's outgoing handle (which is currently influencing the currently-being-placed segment) - overlay_context.manipulator_handle(handle_start, false); + overlay_context.manipulator_handle(handle_start, false, None); } } else { // Draw the whole path and its manipulators when the user is clicking-and-dragging out from the most recently placed anchor to set its outgoing handle, during which it would otherwise not have its overlays drawn @@ -614,7 +616,7 @@ impl Fsm for PenToolFsmState { if self == PenToolFsmState::DraggingHandle && valid(next_anchor, next_handle_start) { // Draw the handle circle for the currently-being-dragged-out outgoing handle (the one currently being dragged out, under the user's cursor) - overlay_context.manipulator_handle(next_handle_start, false); + overlay_context.manipulator_handle(next_handle_start, false, None); } if self == PenToolFsmState::DraggingHandle { @@ -685,6 +687,15 @@ impl Fsm for PenToolFsmState { PenToolFsmState::PlacingAnchor } } + (PenToolFsmState::PlacingAnchor, PenToolMessage::RemovePreviousHandle) => { + if let Some(last_point) = tool_data.latest_points.last_mut() { + last_point.handle_start = last_point.pos; + responses.add(OverlaysMessage::Draw); + } else { + log::warn!("No latest point available to modify handle_start."); + } + self + } (PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data .finish_placing_handle(SnapData::new(document, input), transform, responses) .unwrap_or(PenToolFsmState::PlacingAnchor), diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index b680d81e06..94a1182b01 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -7,8 +7,9 @@ use crate::messages::tool::common_functionality::color_selector::{ToolColorOptio use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::snapping::SnapManager; -use graph_craft::document::{value::TaggedValue, NodeId, NodeInput}; +use graph_craft::document::{NodeId, NodeInput}; use graphene_core::Color; +use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; #[derive(Default)] pub struct SplineTool { @@ -177,8 +178,14 @@ impl ToolTransition for SplineTool { #[derive(Clone, Debug, Default)] struct SplineToolData { - points: Vec, + /// Points that are inserted. + points: Vec<(PointId, DVec2)>, + /// Point to be inserted. next_point: DVec2, + /// Point that was inserted temporarily to show preview. + preview_point: Option, + /// Segment that was inserted temporarily to show preview. + preview_segment: Option, weight: f64, layer: Option, snap_manager: SnapManager, @@ -205,9 +212,11 @@ impl Fsm for SplineToolFsmState { tool_data.weight = tool_options.line_weight; - let node_type = resolve_document_node_type("Spline").expect("Spline node does not exist"); - let node = node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::VecDVec2(Vec::new()), false))]); - let nodes = vec![(NodeId(0), node)]; + let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist"); + let path_node = path_node_type.default_node_template(); + let spline_node_type = resolve_document_node_type("Splines from Points").expect("Spline from Points node does not exist"); + let spline_node = spline_node_type.node_template_input_override([Some(NodeInput::node(NodeId(1), 0))]); + let nodes = vec![(NodeId(1), path_node), (NodeId(0), spline_node)]; let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, parent, responses); tool_options.fill.apply_fill(layer, responses); @@ -228,12 +237,11 @@ impl Fsm for SplineToolFsmState { let transform = document.metadata().transform_to_viewport(layer); let pos = transform.inverse().transform_point2(snapped_position); - if tool_data.points.last().map_or(true, |last_pos| last_pos.distance(pos) > DRAG_THRESHOLD) { - tool_data.points.push(pos); + if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(pos) > DRAG_THRESHOLD) { tool_data.next_point = pos; } - update_spline(document, tool_data, true, responses); + update_spline(tool_data, false, responses); SplineToolFsmState::Drawing } @@ -246,7 +254,7 @@ impl Fsm for SplineToolFsmState { let pos = transform.inverse().transform_point2(snapped_position); tool_data.next_point = pos; - update_spline(document, tool_data, true, responses); + update_spline(tool_data, true, responses); // Auto-panning let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()]; @@ -269,13 +277,15 @@ impl Fsm for SplineToolFsmState { } (SplineToolFsmState::Drawing, SplineToolMessage::Confirm | SplineToolMessage::Abort) => { if tool_data.points.len() >= 2 { - update_spline(document, tool_data, false, responses); + delete_preview(tool_data, responses); responses.add(DocumentMessage::EndTransaction); } else { responses.add(DocumentMessage::AbortTransaction); } tool_data.layer = None; + tool_data.preview_point = None; + tool_data.preview_segment = None; tool_data.points.clear(); tool_data.snap_manager.cleanup(responses); @@ -310,17 +320,49 @@ impl Fsm for SplineToolFsmState { } } -fn update_spline(document: &DocumentMessageHandler, tool_data: &SplineToolData, show_preview: bool, responses: &mut VecDeque) { - let mut points = tool_data.points.clone(); +fn update_spline(tool_data: &mut SplineToolData, show_preview: bool, responses: &mut VecDeque) { + delete_preview(tool_data, responses); + + let Some(layer) = tool_data.layer else { return }; + + let next_point_pos = tool_data.next_point; + let next_point_id = PointId::generate(); + let modification_type = VectorModificationType::InsertPoint { + id: next_point_id, + position: next_point_pos, + }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + if let Some((last_point_id, _)) = tool_data.points.last() { + let points = [*last_point_id, next_point_id]; + let id = SegmentId::generate(); + let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + + if show_preview { + tool_data.preview_segment = Some(id); + } + } + if show_preview { - points.push(tool_data.next_point) + tool_data.preview_point = Some(next_point_id); + } else { + tool_data.points.push((next_point_id, next_point_pos)); } - let value = TaggedValue::VecDVec2(points); +} +fn delete_preview(tool_data: &mut SplineToolData, responses: &mut VecDeque) { let Some(layer) = tool_data.layer else { return }; - let Some(node_id) = graph_modification_utils::NodeGraphLayer::new(layer, &document.network_interface).upstream_node_id_from_name("Spline") else { - return; - }; - responses.add_front(NodeGraphMessage::SetInputValue { node_id, input_index: 1, value }); + if let Some(id) = tool_data.preview_point { + let modification_type = VectorModificationType::RemovePoint { id }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + if let Some(id) = tool_data.preview_segment { + let modification_type = VectorModificationType::RemoveSegment { id }; + responses.add(GraphOperationMessage::Vector { layer, modification_type }); + } + + tool_data.preview_point = None; + tool_data.preview_segment = None; } diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index 6b2f3ac525..477796c89c 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -43,6 +43,7 @@ type TransformData<'a> = (&'a DocumentMessageHandler, &'a InputPreprocessorMessa impl MessageHandler> for TransformLayerMessageHandler { fn process_message(&mut self, message: TransformLayerMessage, responses: &mut VecDeque, (document, input, tool_data, shape_editor): TransformData) { let using_path_tool = tool_data.active_tool_type == ToolType::Path; + let using_select_tool = tool_data.active_tool_type == ToolType::Select; // TODO: Add support for transforming layer not in the document network let selected_layers = document @@ -75,10 +76,18 @@ impl MessageHandler> for TransformLayer let viewspace = document.metadata().transform_to_viewport(selected_layers[0]); let mut point_count: usize = 0; - let get_location = |point: &ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position)); + let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position)); let points = shape_editor.selected_points(); - - *selected.pivot = points.filter_map(get_location).inspect(|_| point_count += 1).sum::() / point_count as f64; + let selected_points: Vec<&ManipulatorPointId> = points.collect(); + + if let [point] = selected_points.as_slice() { + if let ManipulatorPointId::PrimaryHandle(_) | ManipulatorPointId::EndHandle(_) = point { + let anchor_position = point.get_anchor_position(&vector_data).unwrap(); + *selected.pivot = viewspace.transform_point2(anchor_position); + } else { + *selected.pivot = selected_points.iter().filter_map(get_location).inspect(|_| point_count += 1).sum::() / point_count as f64; + } + } } } else { *selected.pivot = selected.mean_average_of_pivots(); @@ -104,12 +113,13 @@ impl MessageHandler> for TransformLayer responses.add(NodeGraphMessage::RunDocumentGraph); } TransformLayerMessage::BeginGrab => { - if let TransformOperation::Grabbing(_) = self.transform_operation { - return; - } + if (!using_path_tool && !using_select_tool) + || (using_path_tool && shape_editor.selected_points().next().is_none()) + || selected_layers.is_empty() + || matches!(self.transform_operation, TransformOperation::Grabbing(_)) + { + selected.original_transforms.clear(); - // Don't allow grab with no selected layers - if selected_layers.is_empty() { return; } @@ -120,13 +130,42 @@ impl MessageHandler> for TransformLayer selected.original_transforms.clear(); } TransformLayerMessage::BeginRotate => { - if let TransformOperation::Rotating(_) = self.transform_operation { + let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); + + if (!using_path_tool && !using_select_tool) + || (using_path_tool && selected_points.is_empty()) + || selected_layers.is_empty() + || matches!(self.transform_operation, TransformOperation::Rotating(_)) + { + selected.original_transforms.clear(); return; } - // Don't allow rotate with no selected layers - if selected_layers.is_empty() { + let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) else { + selected.original_transforms.clear(); return; + }; + + if let [point] = selected_points.as_slice() { + if matches!(point, ManipulatorPointId::Anchor(_)) { + if let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) { + let handle1_length = handle1.length(&vector_data); + let handle2_length = handle2.length(&vector_data); + + if (handle1_length == 0. && handle2_length == 0.) || (handle1_length == f64::MAX && handle2_length == f64::MAX) { + return; + } + } + } else { + // TODO: Fix handle snap to anchor issue, see + + let handle_length = point.as_handle().map(|handle| handle.length(&vector_data)); + + if handle_length == Some(0.) { + selected.original_transforms.clear(); + return; + } + } } begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse); @@ -136,13 +175,41 @@ impl MessageHandler> for TransformLayer selected.original_transforms.clear(); } TransformLayerMessage::BeginScale => { - if let TransformOperation::Scaling(_) = self.transform_operation { + let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); + + if (using_path_tool && selected_points.is_empty()) + || (!using_path_tool && !using_select_tool) + || selected_layers.is_empty() + || matches!(self.transform_operation, TransformOperation::Scaling(_)) + { + selected.original_transforms.clear(); return; } - // Don't allow scale with no selected layers - if selected_layers.is_empty() { + let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) else { + selected.original_transforms.clear(); return; + }; + + if let [point] = selected_points.as_slice() { + if matches!(point, ManipulatorPointId::Anchor(_)) { + if let Some([handle1, handle2]) = point.get_handle_pair(&vector_data) { + let handle1_length = handle1.length(&vector_data); + let handle2_length = handle2.length(&vector_data); + + if (handle1_length == 0. && handle2_length == 0.) || (handle1_length == f64::MAX && handle2_length == f64::MAX) { + selected.original_transforms.clear(); + return; + } + } + } else { + let handle_length = point.as_handle().map(|handle| handle.length(&vector_data)); + + if handle_length == Some(0.) { + selected.original_transforms.clear(); + return; + } + } } begin_operation(self.transform_operation, &mut self.typing, &mut self.mouse_position, &mut self.start_mouse); @@ -215,6 +282,7 @@ impl MessageHandler> for TransformLayer } }; } + self.mouse_position = input.mouse.position; } TransformLayerMessage::SelectionChanged => { diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index fd51a51383..a43fb1a6ad 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -1,7 +1,7 @@