diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 58585e2d..7c55c05a 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -101,6 +101,7 @@ pub const MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY: f64 = 48.; // PATH TOOL pub const MANIPULATOR_GROUP_MARKER_SIZE: f64 = 6.; pub const SELECTION_THRESHOLD: f64 = 10.; +pub const DRILL_THROUGH_THRESHOLD: f64 = 10.; pub const HIDE_HANDLE_DISTANCE: f64 = 3.; pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.; pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.; @@ -134,13 +135,15 @@ pub const SCALE_EFFECT: f64 = 0.5; // COLORS pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff"; +pub const COLOR_OVERLAY_BLUE_50: &str = "#00a8ff80"; pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848"; pub const COLOR_OVERLAY_YELLOW_DULL: &str = "#d7ba8b"; pub const COLOR_OVERLAY_GREEN: &str = "#63ce63"; pub const COLOR_OVERLAY_RED: &str = "#ef5454"; pub const COLOR_OVERLAY_GRAY: &str = "#cccccc"; +pub const COLOR_OVERLAY_GRAY_25: &str = "#cccccc40"; pub const COLOR_OVERLAY_WHITE: &str = "#ffffff"; -pub const COLOR_OVERLAY_LABEL_BACKGROUND: &str = "#000000cc"; +pub const COLOR_OVERLAY_BLACK_75: &str = "#000000bf"; // DOCUMENT pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index ad3f42a3..02dafca1 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -317,6 +317,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyX); modifiers=[Shift], action_dispatch=ToolMessage::SwapColors), entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=ToolMessage::SelectRandomWorkingColor { primary: true }), entry!(KeyDown(KeyC); modifiers=[Alt, Shift], action_dispatch=ToolMessage::SelectRandomWorkingColor { primary: false }), + entry!(KeyDownNoRepeat(Tab); action_dispatch=ToolMessage::ToggleSelectVsPath), // // DocumentMessage entry!(KeyDown(Space); modifiers=[Control], action_dispatch=DocumentMessage::GraphViewOverlayToggle), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 1d1c6aa9..91303f8d 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1683,6 +1683,11 @@ impl DocumentMessageHandler { }) } + pub fn click_list_no_parents<'a>(&'a self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator + use<'a> { + self.click_xray(ipp) + .filter(move |&layer| !self.network_interface.is_artboard(&layer.to_node(), &[]) && !layer.has_children(self.network_interface.document_metadata())) + } + /// Find the deepest layer that has been clicked on from a location in viewport space. pub fn click(&self, ipp: &InputPreprocessorMessageHandler) -> Option { self.click_list(ipp).last() diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 3d12ba49..86cdd858 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -1,7 +1,8 @@ use super::utility_functions::overlay_canvas_context; use crate::consts::{ - COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, - COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, + COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, + COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, + PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, }; use crate::messages::prelude::Message; use bezier_rs::{Bezier, Subpath}; @@ -349,6 +350,7 @@ impl OverlayContext { self.render_context.rect(corner.x, corner.y, size, size); self.render_context.set_fill_style_str(color_fill); self.render_context.set_stroke_style_str(color_stroke); + self.render_context.set_line_width(1.); self.render_context.fill(); self.render_context.stroke(); @@ -631,11 +633,9 @@ impl OverlayContext { pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) { self.start_dpi_aware_transform(); - let color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb(); - self.render_context.begin_path(); self.bezier_command(bezier, transform, true); - self.render_context.set_stroke_style_str(&color); + self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50); self.render_context.set_line_width(4.); self.render_context.stroke(); @@ -727,6 +727,7 @@ impl OverlayContext { let color = color.unwrap_or(COLOR_OVERLAY_BLUE); self.render_context.set_stroke_style_str(color); + self.render_context.set_line_width(1.); self.render_context.stroke(); } } diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 63319b32..c9628e3c 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -4,7 +4,7 @@ mod grid_snapper; mod layer_snapper; mod snap_results; -use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_LABEL_BACKGROUND, COLOR_OVERLAY_WHITE}; +use crate::consts::{COLOR_OVERLAY_BLACK_75, COLOR_OVERLAY_BLUE, 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::{GridSnapTarget, PathSnapTarget, SnapTarget}; @@ -482,7 +482,7 @@ impl SnapManager { if !any_align && ind.distribution_equal_distance_horizontal.is_none() && ind.distribution_equal_distance_vertical.is_none() { let text = format!("[{}] from [{}]", ind.target, ind.source); let transform = DAffine2::from_translation(viewport - DVec2::new(0., 4.)); - overlay_context.text(&text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_LABEL_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]); + overlay_context.text(&text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_BLACK_75), transform, 4., [Pivot::Start, Pivot::End]); overlay_context.square(viewport, Some(4.), Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE)); } } diff --git a/editor/src/messages/tool/tool_message.rs b/editor/src/messages/tool/tool_message.rs index c281149a..70668a26 100644 --- a/editor/src/messages/tool/tool_message.rs +++ b/editor/src/messages/tool/tool_message.rs @@ -87,6 +87,7 @@ pub enum ToolMessage { SelectRandomWorkingColor { primary: bool, }, + ToggleSelectVsPath, SwapColors, Undo, UpdateCursor, diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index df1d7faa..93b37963 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -284,6 +284,16 @@ impl MessageHandler> for ToolMessageHandler document_data.update_working_colors(responses); // TODO: Make this an event } + ToolMessage::ToggleSelectVsPath => { + // If we have the select tool active, toggle to the path tool and vice versa + let tool_data = &mut self.tool_state.tool_data; + let active_tool_type = tool_data.active_tool_type; + if active_tool_type == ToolType::Select { + responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Path }); + } else { + responses.add(ToolMessage::ActivateTool { tool_type: ToolType::Select }); + } + } ToolMessage::SwapColors => { let document_data = &mut self.tool_state.document_tool_data; @@ -359,9 +369,12 @@ impl MessageHandler> for ToolMessageHandler ActivateToolBrush, + ToggleSelectVsPath, + SelectRandomWorkingColor, ResetColors, SwapColors, + Undo, ); list.extend(self.tool_state.tool_data.active_tool().actions()); diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index b8683626..a8b10afa 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -1,8 +1,8 @@ use super::select_tool::extend_lasso; use super::tool_prelude::*; use crate::consts::{ - COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, - SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, + COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, DRILL_THROUGH_THRESHOLD, + HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE, }; use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments}; use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext}; @@ -20,6 +20,7 @@ use crate::messages::tool::common_functionality::utility_functions::{calculate_s use bezier_rs::{Bezier, BezierHandles, TValue}; use graphene_std::renderer::Quad; use graphene_std::transform::ReferencePoint; +use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::{HandleExt, HandleId, NoHashBuilder, SegmentId, VectorData}; use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType}; use std::vec; @@ -516,6 +517,11 @@ struct PathToolData { started_drawing_from_inside: bool, first_selected_with_single_click: bool, stored_selection: Option>, + last_drill_through_click_position: Option, + drill_through_cycle_index: usize, + drill_through_cycle_count: usize, + hovered_layers: Vec, + ghost_outline: Vec<(Vec, DAffine2)>, } impl PathToolData { @@ -524,10 +530,6 @@ impl PathToolData { PathToolFsmState::Dragging(self.dragging_state) } - fn remove_saved_points(&mut self) { - self.saved_points_before_anchor_select_toggle.clear(); - } - pub fn selection_quad(&self, metadata: &DocumentMetadata) -> Quad { let bbox = self.selection_box(metadata); Quad::from_box(bbox) @@ -576,6 +578,49 @@ impl PathToolData { self.selection_status = selection_status; } + fn remove_saved_points(&mut self) { + self.saved_points_before_anchor_select_toggle.clear(); + } + + fn reset_drill_through_cycle(&mut self) { + self.last_drill_through_click_position = None; + self.drill_through_cycle_index = 0; + } + + fn next_drill_through_cycle(&mut self, position: DVec2) -> usize { + if self.last_drill_through_click_position.map_or(true, |last_pos| last_pos.distance(position) > DRILL_THROUGH_THRESHOLD) { + // New position, reset cycle + self.drill_through_cycle_index = 0; + } else { + // Same position, advance cycle + self.drill_through_cycle_index = (self.drill_through_cycle_index + 1) % self.drill_through_cycle_count.max(1); + } + self.last_drill_through_click_position = Some(position); + self.drill_through_cycle_index + } + + fn peek_drill_through_index(&self) -> usize { + if self.drill_through_cycle_count == 0 { + 0 + } else { + (self.drill_through_cycle_index + 1) % self.drill_through_cycle_count.max(1) + } + } + + fn has_drill_through_mouse_moved(&self, position: DVec2) -> bool { + self.last_drill_through_click_position.map_or(true, |last_pos| last_pos.distance(position) > DRILL_THROUGH_THRESHOLD) + } + + fn set_ghost_outline(&mut self, shape_editor: &ShapeState, document: &DocumentMessageHandler) { + self.ghost_outline.clear(); + for &layer in shape_editor.selected_shape_state.keys() { + // We probably need to collect here + let outline: Vec = document.metadata().layer_with_free_points_outline(layer).cloned().collect(); + let transform = document.metadata().transform_to_viewport(layer); + self.ghost_outline.push((outline, transform)); + } + } + // TODO: This function is for basic point select mode. We definitely need to make a new one for the segment select mode. #[allow(clippy::too_many_arguments)] fn mouse_down( @@ -619,6 +664,8 @@ impl PathToolData { ) { responses.add(DocumentMessage::StartTransaction); + self.set_ghost_outline(shape_editor, document); + self.last_clicked_point_was_selected = already_selected; // If the point is already selected and shift (`extend_selection`) is used, keep the selection unchanged. @@ -703,6 +750,8 @@ impl PathToolData { else if let Some(segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { responses.add(DocumentMessage::StartTransaction); + self.set_ghost_outline(shape_editor, document); + if segment_editing_mode && !molding_in_segment_edit { let layer = segment.layer(); let segment_id = segment.segment(); @@ -723,7 +772,6 @@ impl PathToolData { } self.drag_start_pos = input.mouse.position; - let viewport_to_document = document.metadata().document_to_viewport.inverse(); self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position); @@ -746,6 +794,8 @@ impl PathToolData { else if let Some(layer) = document.click(input) { if shape_editor.selected_shape_state.is_empty() { self.first_selected_with_single_click = true; + // This ensures we don't need to double click a second time to get the drill through to work + self.last_drill_through_click_position = Some(input.mouse.position); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] }); } @@ -1408,6 +1458,7 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::SelectionChanged) => { // Set the newly targeted layers to visible let target_layers = document.network_interface.selected_nodes().selected_layers(document.metadata()).collect(); + shape_editor.set_selected_layers(target_layers); responses.add(OverlaysMessage::Draw); @@ -1426,6 +1477,12 @@ impl Fsm for PathToolFsmState { self } (_, PathToolMessage::Overlays(mut overlay_context)) => { + if matches!(self, Self::Dragging(_)) { + for (outline, transform) in &tool_data.ghost_outline { + overlay_context.outline(outline.iter(), *transform, Some(COLOR_OVERLAY_GRAY)); + } + } + // TODO: find the segment ids of which the selected points are a part of match tool_options.path_overlay_mode { @@ -1527,6 +1584,34 @@ impl Fsm for PathToolFsmState { } } } + + // Show outlines for hovered layers with appropriate highlighting + let currently_selected_layer = document.network_interface.selected_nodes().selected_layers(document.metadata()).next(); + let next_selected_index = tool_data.peek_drill_through_index(); + let mouse_has_moved = tool_data.has_drill_through_mouse_moved(input.mouse.position); + + for (index, &hovered_layer) in tool_data.hovered_layers.iter().enumerate() { + // Skip already highlighted selected layer + if Some(hovered_layer) == currently_selected_layer { + continue; + } + + let layer_to_viewport = document.metadata().transform_to_viewport(hovered_layer); + let outline = document.metadata().layer_with_free_points_outline(hovered_layer); + + // Determine highlight color based on drill-through state + let color = match (index, mouse_has_moved) { + // If the layer is the next selected one and mouse has not moved, highlight it blue + (i, false) if i == next_selected_index => COLOR_OVERLAY_BLUE, + // If the layer is the first hovered one and mouse has moved, highlight it blue + (0, true) => COLOR_OVERLAY_BLUE, + // Otherwise, use gray + _ => COLOR_OVERLAY_GRAY, + }; + + // TODO: Make this draw underneath all other overlays + overlay_context.outline(outline, layer_to_viewport, Some(color)); + } } Self::Drawing { selection_shape } => { let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()) @@ -1794,6 +1879,23 @@ impl Fsm for PathToolFsmState { tool_data.adjacent_anchor_offset = None; tool_data.stored_selection = None; + if tool_data.has_drill_through_mouse_moved(input.mouse.position) { + tool_data.reset_drill_through_cycle(); + } + + // When moving the cursor around we want to update the hovered layers + let new_hovered_layers: Vec = document + .click_list_no_parents(input) + .filter(|&layer| { + // Filter out artboards and parent holders, and already selected layers + !document.network_interface.is_artboard(&layer.to_node(), &[]) + }) + .collect(); + + if tool_data.hovered_layers != new_hovered_layers { + tool_data.hovered_layers = new_hovered_layers; + } + responses.add(OverlaysMessage::Draw); self @@ -1994,6 +2096,7 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Ready } (_, PathToolMessage::DragStop { extend_selection, .. }) => { + tool_data.ghost_outline.clear(); let extend_selection = input.keyboard.get(extend_selection as usize); let drag_occurred = tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD; @@ -2137,6 +2240,20 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::DoubleClick { extend_selection, shrink_selection }) => { // Double-clicked on a point (flip smooth/sharp behavior) let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD); + + let mut get_drill_through_layer = || -> Option { + let drill_through_layers = document.click_list_no_parents(input).collect::>(); + if drill_through_layers.is_empty() { + tool_data.reset_drill_through_cycle(); + None + } else { + tool_data.drill_through_cycle_count = drill_through_layers.len(); + let cycle_index = tool_data.next_drill_through_cycle(input.mouse.position); + let layer = drill_through_layers.get(cycle_index); + if cycle_index == 0 { drill_through_layers.first().copied() } else { layer.copied() } + } + }; + if nearest_point.is_some() { // Flip the selected point between smooth and sharp if !tool_data.double_click_handled && tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD { @@ -2153,17 +2270,17 @@ impl Fsm for PathToolFsmState { return PathToolFsmState::Ready; } // Double-clicked on a filled region - else if let Some(layer) = document.click(input) { + else if let Some(layer) = &get_drill_through_layer() { let extend_selection = input.keyboard.get(extend_selection as usize); let shrink_selection = input.keyboard.get(shrink_selection as usize); - if shape_editor.is_selected_layer(layer) { + if shape_editor.is_selected_layer(*layer) { if extend_selection && !tool_data.first_selected_with_single_click { responses.add(NodeGraphMessage::SelectedNodesRemove { nodes: vec![layer.to_node()] }); if let Some(selection) = &tool_data.stored_selection { let mut selection = selection.clone(); - selection.remove(&layer); + selection.remove(layer); shape_editor.selected_shape_state = selection; tool_data.stored_selection = None; } @@ -2175,19 +2292,19 @@ impl Fsm for PathToolFsmState { tool_data.stored_selection = None; } - let state = shape_editor.selected_shape_state.get_mut(&layer).expect("No state for selected layer"); + let state = shape_editor.selected_shape_state.get_mut(layer).expect("No state for selected layer"); state.deselect_all_points_in_layer(); state.deselect_all_segments_in_layer(); } else if !tool_data.first_selected_with_single_click { // Select according to the selected editing mode let point_editing_mode = tool_options.path_editing_mode.point_editing_mode; let segment_editing_mode = tool_options.path_editing_mode.segment_editing_mode; - shape_editor.select_connected(document, layer, input.mouse.position, point_editing_mode, segment_editing_mode); + shape_editor.select_connected(document, *layer, input.mouse.position, point_editing_mode, segment_editing_mode); // Select all the other layers back again if let Some(selection) = &tool_data.stored_selection { let mut selection = selection.clone(); - selection.remove(&layer); + selection.remove(layer); for (layer, state) in selection { shape_editor.selected_shape_state.insert(layer, state); 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 603cda06..20c6b722 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 @@ -1,4 +1,4 @@ -use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, SLOWING_DIVISOR}; +use crate::consts::{ANGLE_MEASURE_RADIUS_FACTOR, ARC_MEASURE_RADIUS_FACTOR_RANGE, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GRAY_25, SLOWING_DIVISOR}; use crate::messages::input_mapper::utility_types::input_mouse::{DocumentPosition, ViewportPosition}; use crate::messages::portfolio::document::overlays::utility_types::{OverlayProvider, Pivot}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -12,6 +12,7 @@ use crate::messages::tool::utility_types::{ToolData, ToolType}; use glam::{DAffine2, DVec2}; use graphene_std::renderer::Quad; use graphene_std::vector::ManipulatorPointId; +use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::{VectorData, VectorModificationType}; use std::f64::consts::{PI, TAU}; @@ -61,6 +62,9 @@ pub struct TransformLayerMessageHandler { handle: DVec2, last_point: DVec2, grs_pen_handle: bool, + + // Ghost outlines for Path Tool + ghost_outline: Vec<(Vec, DAffine2)>, } impl MessageHandler> for TransformLayerMessageHandler { @@ -171,6 +175,12 @@ impl MessageHandler> for return; } + if using_path_tool { + for (outline, transform) in &self.ghost_outline { + overlay_context.outline(outline.iter(), *transform, Some(COLOR_OVERLAY_GRAY_25)); + } + } + let viewport_box = input.viewport_bounds.size(); let axis_constraint = self.transform_operation.axis_constraint(); @@ -284,6 +294,10 @@ impl MessageHandler> for responses.add(NodeGraphMessage::RunDocumentGraph); } + if using_path_tool { + self.ghost_outline.clear(); + } + responses.add(SelectToolMessage::PivotShift { offset: None, flush: true }); if final_transform { @@ -337,12 +351,15 @@ impl MessageHandler> for let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect(); let selected_segments = shape_editor.selected_segments().collect::>(); - if (using_path_tool && selected_points.is_empty() && selected_segments.is_empty()) - || (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool) - || selected_layers.is_empty() - || transform_type.equivalent_to(self.transform_operation) - { - return; + if using_path_tool { + Self::set_ghost_outline(&mut self.ghost_outline, shape_editor, document); + if (selected_points.is_empty() && selected_segments.is_empty()) + || (!using_path_tool && !using_select_tool && !using_pen_tool && !using_shape_tool) + || selected_layers.is_empty() + || transform_type.equivalent_to(self.transform_operation) + { + return; + } } if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) { @@ -399,6 +416,8 @@ impl MessageHandler> for responses.add(PenToolMessage::Abort); responses.add(ToolMessage::UpdateHints); + } else if using_path_tool { + self.ghost_outline.clear(); } else { selected.original_transforms.clear(); self.typing.clear(); @@ -657,6 +676,16 @@ impl TransformLayerMessageHandler { pub fn hints(&self, responses: &mut VecDeque) { self.transform_operation.hints(responses, self.local); } + + fn set_ghost_outline(ghost_outline: &mut Vec<(Vec, DAffine2)>, shape_editor: &ShapeState, document: &DocumentMessageHandler) { + ghost_outline.clear(); + for &layer in shape_editor.selected_shape_state.keys() { + // We probably need to collect here + let outline = document.metadata().layer_with_free_points_outline(layer).cloned().collect(); + let transform = document.metadata().transform_to_viewport(layer); + ghost_outline.push((outline, transform)); + } + } } fn calculate_pivot(