Add selection cycle and gray pre-selection outlines to the Path tool, and Tab to swap Select/Path tools (#2818)

* Added initial version of this feature for the path tool

* Removed debug statements

* Thickened the overlay width

* Added hover highlighting for path tool

* Experimental switch to path tool at leaf layer

* Ghost outline initial implementation

* Added tab swap for select tool -> path tool

* Minor fix for Select Tool dbl click -> Path Tool

* Added support for ghosts when using GRS in the path tool

* Fixed GRS undo bug, vastly improved hover behavior and now clearly visualize next double click target

* Fixed unused import warnings

* Updated behavior to handle mouse movement cases, reverted line width to 1px

* Fixed merge behavioral issues

* Disabled Select Tool to Path Tool double click toggle, fixed single click drill through for special case

* Clean up of unused consts and comment

* Properly cancel the drill through state when the mouse moves

* Fix some stuff

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Oliver Davies 2025-07-13 01:26:36 -07:00 committed by GitHub
parent d6d1bbb1e6
commit e89cded4b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 198 additions and 28 deletions

View File

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

View File

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

View File

@ -1683,6 +1683,11 @@ impl DocumentMessageHandler {
})
}
pub fn click_list_no_parents<'a>(&'a self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + 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<LayerNodeIdentifier> {
self.click_list(ipp).last()

View File

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

View File

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

View File

@ -87,6 +87,7 @@ pub enum ToolMessage {
SelectRandomWorkingColor {
primary: bool,
},
ToggleSelectVsPath,
SwapColors,
Undo,
UpdateCursor,

View File

@ -284,6 +284,16 @@ impl MessageHandler<ToolMessage, ToolMessageContext<'_>> 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<ToolMessage, ToolMessageContext<'_>> for ToolMessageHandler
ActivateToolBrush,
ToggleSelectVsPath,
SelectRandomWorkingColor,
ResetColors,
SwapColors,
Undo,
);
list.extend(self.tool_state.tool_data.active_tool().actions());

View File

@ -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<HashMap<LayerNodeIdentifier, SelectedLayerState>>,
last_drill_through_click_position: Option<DVec2>,
drill_through_cycle_index: usize,
drill_through_cycle_count: usize,
hovered_layers: Vec<LayerNodeIdentifier>,
ghost_outline: Vec<(Vec<ClickTargetType>, 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<ClickTargetType> = 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<LayerNodeIdentifier> = 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<LayerNodeIdentifier> {
let drill_through_layers = document.click_list_no_parents(input).collect::<Vec<LayerNodeIdentifier>>();
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);

View File

@ -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<ClickTargetType>, DAffine2)>,
}
impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for TransformLayerMessageHandler {
@ -171,6 +175,12 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> 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<TransformLayerMessage, TransformLayerMessageContext<'_>> 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,13 +351,16 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
let selected_points: Vec<&ManipulatorPointId> = shape_editor.selected_points().collect();
let selected_segments = shape_editor.selected_segments().collect::<Vec<_>>();
if (using_path_tool && selected_points.is_empty() && selected_segments.is_empty())
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)) {
if let [point] = selected_points.as_slice() {
@ -399,6 +416,8 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> 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<Message>) {
self.transform_operation.hints(responses, self.local);
}
fn set_ghost_outline(ghost_outline: &mut Vec<(Vec<ClickTargetType>, 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(