mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add lasso selection mode (#2235)
* add document method to check intersection and insideness with polygon * impl lasso selection * impl lasso select to always add to selection * add hint for lasso selection * fix calculating selection mode based on direction on each pointer move * fix lasso polygon lagging behing mouse position * add overlay to draw lasso polygon with fill color same as quad * fix comment * change removing from selection key binding from Sift + Ctrl to just Alt * impl Alt to shrink selection for quad in path tool * refactor rename SelectionType to SelectionShape * impl lasso overlay for path tool * impl selecting anchors and handles intersection lasso in path tool * add keys hint info * fix converting lasso polygon to closed subpath which is has less than two points * Code review * impl preferences-based selection mode to the Path tool for only for overlays, --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -52,6 +52,7 @@ pub const DEFAULT_STROKE_WIDTH: f64 = 2.;
|
||||
|
||||
// SELECT TOOL
|
||||
pub const SELECTION_TOLERANCE: f64 = 5.;
|
||||
pub const DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD: f64 = 15.;
|
||||
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
||||
pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.;
|
||||
pub const PIVOT_CROSSHAIR_LENGTH: f64 = 9.;
|
||||
|
||||
@@ -98,8 +98,8 @@ pub fn input_mappings() -> Mapping {
|
||||
//
|
||||
// SelectToolMessage
|
||||
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt })),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, select_deepest: Accel }),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Shift, negative_box_selection: Control }),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, remove_from_selection: Alt, select_deepest: Accel, lasso_select: Control }),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Alt }),
|
||||
entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter),
|
||||
entry!(DoubleClick(MouseButton::Left); action_dispatch=SelectToolMessage::EditLayer),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=SelectToolMessage::Abort),
|
||||
@@ -213,7 +213,7 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
|
||||
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
|
||||
entry!(KeyDown(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift }),
|
||||
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift, lasso_select: Control }),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
|
||||
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
|
||||
entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }),
|
||||
@@ -224,8 +224,8 @@ pub fn input_mappings() -> Mapping {
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
|
||||
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints),
|
||||
entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { extend_selection: Shift }),
|
||||
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { extend_selection: Shift }),
|
||||
entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { extend_selection: Shift, shrink_selection: Alt }),
|
||||
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { extend_selection: Shift, shrink_selection: Alt }),
|
||||
entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSmoothSharp),
|
||||
entry!(KeyDown(ArrowRight); action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: 0. }),
|
||||
entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }),
|
||||
|
||||
@@ -25,13 +25,14 @@ use crate::messages::tool::tool_messages::tool_prelude::Key;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use crate::node_graph_executor::NodeGraphExecutor;
|
||||
|
||||
use bezier_rs::Subpath;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeNetwork, OldNodeNetwork};
|
||||
use graphene_core::raster::image::ImageFrame;
|
||||
use graphene_core::raster::BlendMode;
|
||||
use graphene_core::vector::style::ViewMode;
|
||||
use graphene_std::renderer::{ClickTarget, Quad};
|
||||
use graphene_std::vector::path_bool_lib;
|
||||
use graphene_std::vector::{path_bool_lib, PointId};
|
||||
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
|
||||
@@ -1405,6 +1406,19 @@ impl DocumentMessageHandler {
|
||||
self.intersect_quad(viewport_quad, ipp).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
}
|
||||
|
||||
/// Runs an intersection test with all layers and a viewport space subpath
|
||||
pub fn intersect_polygon<'a>(&'a self, mut viewport_polygon: Subpath<PointId>, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
|
||||
let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
|
||||
viewport_polygon.apply_transform(document_to_viewport.inverse());
|
||||
|
||||
ClickXRayIter::new(&self.network_interface, XRayTarget::Polygon(viewport_polygon))
|
||||
}
|
||||
|
||||
/// Runs an intersection test with all layers and a viewport space subpath; ignoring artboards
|
||||
pub fn intersect_polygon_no_artboards<'a>(&'a self, viewport_polygon: Subpath<PointId>, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
|
||||
self.intersect_polygon(viewport_polygon, ipp).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
}
|
||||
|
||||
pub fn is_layer_fully_inside(&self, layer: &LayerNodeIdentifier, quad: graphene_core::renderer::Quad) -> bool {
|
||||
// Get the bounding box of the layer in document space
|
||||
let Some(bounding_box) = self.metadata().bounding_box_viewport(*layer) else { return false };
|
||||
@@ -1428,6 +1442,22 @@ impl DocumentMessageHandler {
|
||||
layer_left >= quad_left && layer_right <= quad_right && layer_top <= quad_top && layer_bottom >= quad_bottom
|
||||
}
|
||||
|
||||
pub fn is_layer_fully_inside_polygon(&self, layer: &LayerNodeIdentifier, ipp: &InputPreprocessorMessageHandler, mut viewport_polygon: Subpath<PointId>) -> bool {
|
||||
let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
|
||||
viewport_polygon.apply_transform(document_to_viewport.inverse());
|
||||
|
||||
let layer_click_targets = self.network_interface.document_metadata().click_targets(*layer);
|
||||
let layer_transform = self.network_interface.document_metadata().transform_to_document(*layer);
|
||||
|
||||
layer_click_targets.is_some_and(|targets| {
|
||||
targets.iter().all(|target| {
|
||||
let mut subpath = target.subpath().clone();
|
||||
subpath.apply_transform(layer_transform);
|
||||
subpath.is_inside_subpath(&viewport_polygon, None, None)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Find all of the layers that were clicked on from a viewport space location
|
||||
pub fn click_xray(&self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
||||
let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz);
|
||||
@@ -2172,6 +2202,7 @@ enum XRayTarget {
|
||||
Point(DVec2),
|
||||
Quad(Quad),
|
||||
Path(Vec<path_bool_lib::PathSegment>),
|
||||
Polygon(Subpath<PointId>),
|
||||
}
|
||||
|
||||
/// The result for the [`ClickXRayIter`] on the layer
|
||||
@@ -2275,6 +2306,10 @@ impl<'a> ClickXRayIter<'a> {
|
||||
}
|
||||
XRayTarget::Quad(quad) => self.check_layer_area_target(click_targets, clip, layer, quad_to_path_lib_segments(*quad), transform),
|
||||
XRayTarget::Path(path) => self.check_layer_area_target(click_targets, clip, layer, path.clone(), transform),
|
||||
XRayTarget::Polygon(polygon) => {
|
||||
let polygon = polygon.iter_closed().map(|line| path_bool_lib::PathSegment::Line(line.start, line.end)).collect();
|
||||
self.check_layer_area_target(click_targets, clip, layer, polygon, transform)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,22 @@ impl core::hash::Hash for OverlayContext {
|
||||
|
||||
impl OverlayContext {
|
||||
pub fn quad(&mut self, quad: Quad, color_fill: Option<&str>) {
|
||||
self.dashed_quad(quad, color_fill, None, None, None);
|
||||
self.dashed_polygon(&quad.0, color_fill, None, None, None);
|
||||
}
|
||||
|
||||
pub fn dashed_quad(&mut self, quad: Quad, color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
|
||||
self.dashed_polygon(&quad.0, color_fill, dash_width, dash_gap_width, dash_offset);
|
||||
}
|
||||
|
||||
pub fn polygon(&mut self, polygon: &[DVec2], color_fill: Option<&str>) {
|
||||
self.dashed_polygon(&polygon, color_fill, None, None, None);
|
||||
}
|
||||
|
||||
pub fn dashed_polygon(&mut self, polygon: &[DVec2], color_fill: Option<&str>, dash_width: Option<f64>, dash_gap_width: Option<f64>, dash_offset: Option<f64>) {
|
||||
if polygon.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
// Set the dash pattern
|
||||
@@ -63,10 +75,10 @@ impl OverlayContext {
|
||||
}
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.render_context.move_to(quad.0[3].x.round() - 0.5, quad.0[3].y.round() - 0.5);
|
||||
self.render_context.move_to(polygon.last().unwrap().x.round() - 0.5, polygon.last().unwrap().y.round() - 0.5);
|
||||
|
||||
for i in 0..4 {
|
||||
self.render_context.line_to(quad.0[i].x.round() - 0.5, quad.0[i].y.round() - 0.5);
|
||||
for point in polygon {
|
||||
self.render_context.line_to(point.x.round() - 0.5, point.y.round() - 0.5);
|
||||
}
|
||||
|
||||
if let Some(color_fill) = color_fill {
|
||||
|
||||
@@ -7,13 +7,32 @@ use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
|
||||
use crate::messages::tool::tool_messages::path_tool::PointSelectState;
|
||||
|
||||
use bezier_rs::{Bezier, BezierHandles, TValue};
|
||||
use bezier_rs::{Bezier, BezierHandles, Subpath, TValue};
|
||||
use graphene_core::transform::Transform;
|
||||
use graphene_core::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::vector::{HandleId, SegmentId};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum SelectionChange {
|
||||
Clear,
|
||||
Extend,
|
||||
Shrink,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum SelectionShape<'a> {
|
||||
Box([DVec2; 2]),
|
||||
Lasso(&'a Vec<DVec2>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum SelectionShapeType {
|
||||
Box,
|
||||
Lasso,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
|
||||
pub enum ManipulatorAngle {
|
||||
#[default]
|
||||
@@ -1394,9 +1413,9 @@ impl ShapeState {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn select_all_in_quad(&mut self, network_interface: &NodeNetworkInterface, quad: [DVec2; 2], clear_selection: bool) {
|
||||
pub fn select_all_in_shape(&mut self, network_interface: &NodeNetworkInterface, selection_shape: SelectionShape, selection_change: SelectionChange) {
|
||||
for (&layer, state) in &mut self.selected_shape_state {
|
||||
if clear_selection {
|
||||
if selection_change == SelectionChange::Clear {
|
||||
state.clear_points()
|
||||
}
|
||||
|
||||
@@ -1413,13 +1432,34 @@ impl ShapeState {
|
||||
assert!(vector_data.point_domain.ids().contains(&end));
|
||||
}
|
||||
|
||||
let polygon_subpath = if let SelectionShape::Lasso(polygon) = selection_shape {
|
||||
if polygon.len() < 2 {
|
||||
return;
|
||||
}
|
||||
let polygon: Subpath<PointId> = Subpath::from_anchors_linear(polygon.to_vec(), true);
|
||||
Some(polygon)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for (id, bezier, _, _) in vector_data.segment_bezier_iter() {
|
||||
for (position, id) in [(bezier.handle_start(), ManipulatorPointId::PrimaryHandle(id)), (bezier.handle_end(), ManipulatorPointId::EndHandle(id))] {
|
||||
let Some(position) = position else { continue };
|
||||
let transformed_position = transform.transform_point2(position);
|
||||
|
||||
if quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all() {
|
||||
state.select_point(id);
|
||||
let select = match selection_shape {
|
||||
SelectionShape::Box(quad) => quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all(),
|
||||
SelectionShape::Lasso(_) => polygon_subpath
|
||||
.as_ref()
|
||||
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
|
||||
.contains_point(transformed_position),
|
||||
};
|
||||
|
||||
if select {
|
||||
match selection_change {
|
||||
SelectionChange::Shrink => state.deselect_point(id),
|
||||
_ => state.select_point(id),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1427,8 +1467,19 @@ impl ShapeState {
|
||||
for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) {
|
||||
let transformed_position = transform.transform_point2(position);
|
||||
|
||||
if quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all() {
|
||||
state.select_point(ManipulatorPointId::Anchor(id));
|
||||
let select = match selection_shape {
|
||||
SelectionShape::Box(quad) => quad[0].min(quad[1]).cmple(transformed_position).all() && quad[0].max(quad[1]).cmpge(transformed_position).all(),
|
||||
SelectionShape::Lasso(_) => polygon_subpath
|
||||
.as_ref()
|
||||
.expect("If `selection_shape` is a polygon then subpath is constructed beforehand.")
|
||||
.contains_point(transformed_position),
|
||||
};
|
||||
|
||||
if select {
|
||||
match selection_change {
|
||||
SelectionChange::Shrink => state.deselect_point(ManipulatorPointId::Anchor(id)),
|
||||
_ => state.select_point(ManipulatorPointId::Anchor(id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
use super::select_tool::extend_lasso;
|
||||
use super::tool_prelude::*;
|
||||
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::consts::{
|
||||
COLOR_OVERLAY_BLUE, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, 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;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, ShapeState};
|
||||
use crate::messages::tool::common_functionality::shape_editor::{
|
||||
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
|
||||
};
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
|
||||
|
||||
use graphene_core::renderer::Quad;
|
||||
@@ -35,9 +41,11 @@ pub enum PathToolMessage {
|
||||
DeleteAndBreakPath,
|
||||
DragStop {
|
||||
extend_selection: Key,
|
||||
shrink_selection: Key,
|
||||
},
|
||||
Enter {
|
||||
extend_selection: Key,
|
||||
shrink_selection: Key,
|
||||
},
|
||||
Escape,
|
||||
ClosePath,
|
||||
@@ -51,6 +59,7 @@ pub enum PathToolMessage {
|
||||
MouseDown {
|
||||
direct_insert_without_sliding: Key,
|
||||
extend_selection: Key,
|
||||
lasso_select: Key,
|
||||
},
|
||||
NudgeSelectedPoints {
|
||||
delta_x: f64,
|
||||
@@ -230,7 +239,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
||||
DeleteAndBreakPath,
|
||||
SwapSelectedHandles,
|
||||
),
|
||||
PathToolFsmState::DrawingBox => actions!(PathToolMessageDiscriminant;
|
||||
PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant;
|
||||
FlipSmoothSharp,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
@@ -283,7 +292,9 @@ enum PathToolFsmState {
|
||||
#[default]
|
||||
Ready,
|
||||
Dragging(DraggingState),
|
||||
DrawingBox,
|
||||
Drawing {
|
||||
selection_shape: SelectionShapeType,
|
||||
},
|
||||
InsertPoint,
|
||||
}
|
||||
|
||||
@@ -295,6 +306,8 @@ enum InsertEndKind {
|
||||
#[derive(Default)]
|
||||
struct PathToolData {
|
||||
snap_manager: SnapManager,
|
||||
lasso_polygon: Vec<DVec2>,
|
||||
selection_mode: Option<SelectionMode>,
|
||||
drag_start_pos: DVec2,
|
||||
previous_mouse_position: DVec2,
|
||||
toggle_colinear_debounce: bool,
|
||||
@@ -323,6 +336,37 @@ impl PathToolData {
|
||||
self.saved_points_before_anchor_select_toggle.clear();
|
||||
}
|
||||
|
||||
pub fn selection_quad(&self) -> Quad {
|
||||
let bbox = self.selection_box();
|
||||
Quad::from_box(bbox)
|
||||
}
|
||||
|
||||
pub fn calculate_direction(&mut self) -> SelectionMode {
|
||||
let bbox = self.selection_box();
|
||||
let above_threshold = bbox[1].distance_squared(bbox[0]) > DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD.powi(2);
|
||||
|
||||
if self.selection_mode.is_none() && above_threshold {
|
||||
let mode = if bbox[1].x < bbox[0].x {
|
||||
SelectionMode::Touched
|
||||
} else {
|
||||
// This also covers the case where they're equal: the area is zero, so we use `Enclosed` to ensure the selection ends up empty, as nothing will be enclosed by an empty area
|
||||
SelectionMode::Enclosed
|
||||
};
|
||||
self.selection_mode = Some(mode);
|
||||
}
|
||||
|
||||
self.selection_mode.unwrap_or(SelectionMode::Touched)
|
||||
}
|
||||
|
||||
pub fn selection_box(&self) -> [DVec2; 2] {
|
||||
if self.previous_mouse_position == self.drag_start_pos {
|
||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||
[self.drag_start_pos - tolerance, self.drag_start_pos + tolerance]
|
||||
} else {
|
||||
[self.drag_start_pos, self.previous_mouse_position]
|
||||
}
|
||||
}
|
||||
|
||||
fn start_insertion(&mut self, responses: &mut VecDeque<Message>, segment: ClosestSegment) -> PathToolFsmState {
|
||||
if self.segment.is_some() {
|
||||
warn!("Segment was `Some(..)` before `start_insertion`")
|
||||
@@ -370,6 +414,7 @@ impl PathToolData {
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn mouse_down(
|
||||
&mut self,
|
||||
shape_editor: &mut ShapeState,
|
||||
@@ -378,6 +423,7 @@ impl PathToolData {
|
||||
responses: &mut VecDeque<Message>,
|
||||
extend_selection: bool,
|
||||
direct_insert_without_sliding: bool,
|
||||
lasso_select: bool,
|
||||
) -> PathToolFsmState {
|
||||
self.double_click_handled = false;
|
||||
self.opposing_handle_lengths = None;
|
||||
@@ -419,12 +465,13 @@ impl PathToolData {
|
||||
|
||||
PathToolFsmState::Dragging(self.dragging_state)
|
||||
}
|
||||
// Start drawing a box
|
||||
// Start drawing
|
||||
else {
|
||||
self.drag_start_pos = input.mouse.position;
|
||||
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
|
||||
|
||||
PathToolFsmState::DrawingBox
|
||||
let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
|
||||
PathToolFsmState::Drawing { selection_shape }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,13 +706,28 @@ impl Fsm for PathToolFsmState {
|
||||
path_overlays(document, shape_editor, &mut overlay_context);
|
||||
|
||||
match self {
|
||||
Self::DrawingBox => {
|
||||
let fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
Self::Drawing { selection_shape } => {
|
||||
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.05)
|
||||
.rgba_hex();
|
||||
fill_color.insert(0, '#');
|
||||
let fill_color = Some(fill_color.as_str());
|
||||
|
||||
overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position]), Some(&("#".to_string() + &fill_color)));
|
||||
let mut selection_direction = tool_action_data.preferences.get_selection_mode();
|
||||
if selection_direction == SelectionMode::Directional {
|
||||
selection_direction = tool_data.calculate_direction();
|
||||
}
|
||||
|
||||
let quad = tool_data.selection_quad();
|
||||
let polygon = &tool_data.lasso_polygon;
|
||||
|
||||
match (selection_shape, selection_direction) {
|
||||
(SelectionShapeType::Box, SelectionMode::Enclosed) => overlay_context.dashed_quad(quad, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Lasso, SelectionMode::Enclosed) => overlay_context.dashed_polygon(polygon, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Box, _) => overlay_context.quad(quad, fill_color),
|
||||
(SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, fill_color),
|
||||
}
|
||||
}
|
||||
Self::Dragging(_) => {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
@@ -694,7 +756,7 @@ impl Fsm for PathToolFsmState {
|
||||
}
|
||||
|
||||
// `Self::InsertPoint` case:
|
||||
(Self::InsertPoint, PathToolMessage::MouseDown { extend_selection, .. } | PathToolMessage::Enter { extend_selection }) => {
|
||||
(Self::InsertPoint, PathToolMessage::MouseDown { extend_selection, .. } | PathToolMessage::Enter { extend_selection, .. }) => {
|
||||
tool_data.double_click_handled = true;
|
||||
let extend_selection = input.keyboard.get(extend_selection as usize);
|
||||
tool_data.end_insertion(shape_editor, responses, InsertEndKind::Add { extend_selection })
|
||||
@@ -713,15 +775,20 @@ impl Fsm for PathToolFsmState {
|
||||
PathToolMessage::MouseDown {
|
||||
direct_insert_without_sliding,
|
||||
extend_selection,
|
||||
lasso_select,
|
||||
},
|
||||
) => {
|
||||
let extend_selection = input.keyboard.get(extend_selection as usize);
|
||||
let lasso_select = input.keyboard.get(lasso_select as usize);
|
||||
let direct_insert_without_sliding = input.keyboard.get(direct_insert_without_sliding as usize);
|
||||
|
||||
tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, direct_insert_without_sliding)
|
||||
tool_data.selection_mode = None;
|
||||
tool_data.lasso_polygon.clear();
|
||||
|
||||
tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, direct_insert_without_sliding, lasso_select)
|
||||
}
|
||||
(
|
||||
PathToolFsmState::DrawingBox,
|
||||
PathToolFsmState::Drawing { selection_shape },
|
||||
PathToolMessage::PointerMove {
|
||||
equidistant,
|
||||
toggle_colinear,
|
||||
@@ -731,6 +798,11 @@ impl Fsm for PathToolFsmState {
|
||||
},
|
||||
) => {
|
||||
tool_data.previous_mouse_position = input.mouse.position;
|
||||
|
||||
if selection_shape == SelectionShapeType::Lasso {
|
||||
extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position);
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
// Auto-panning
|
||||
@@ -754,7 +826,7 @@ impl Fsm for PathToolFsmState {
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
PathToolFsmState::DrawingBox
|
||||
PathToolFsmState::Drawing { selection_shape }
|
||||
}
|
||||
(
|
||||
PathToolFsmState::Dragging(_),
|
||||
@@ -829,13 +901,13 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
PathToolFsmState::Dragging(tool_data.dragging_state)
|
||||
}
|
||||
(PathToolFsmState::DrawingBox, PathToolMessage::PointerOutsideViewport { .. }) => {
|
||||
(PathToolFsmState::Drawing { selection_shape: selection_type }, PathToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
if let Some(offset) = tool_data.auto_panning.shift_viewport(input, responses) {
|
||||
tool_data.drag_start_pos += offset;
|
||||
}
|
||||
|
||||
PathToolFsmState::DrawingBox
|
||||
PathToolFsmState::Drawing { selection_shape: selection_type }
|
||||
}
|
||||
(
|
||||
PathToolFsmState::Dragging(dragging_state),
|
||||
@@ -887,14 +959,30 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
state
|
||||
}
|
||||
(PathToolFsmState::DrawingBox, PathToolMessage::Enter { extend_selection }) => {
|
||||
(PathToolFsmState::Drawing { selection_shape }, PathToolMessage::Enter { extend_selection, shrink_selection }) => {
|
||||
let extend_selection = input.keyboard.get(extend_selection as usize);
|
||||
let shrink_selection = input.keyboard.get(shrink_selection as usize);
|
||||
|
||||
let selection_change = if shrink_selection {
|
||||
SelectionChange::Shrink
|
||||
} else if extend_selection {
|
||||
SelectionChange::Extend
|
||||
} else {
|
||||
SelectionChange::Clear
|
||||
};
|
||||
|
||||
if tool_data.drag_start_pos == tool_data.previous_mouse_position {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
} else {
|
||||
shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !extend_selection);
|
||||
match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
let bbox = [tool_data.drag_start_pos, tool_data.previous_mouse_position];
|
||||
shape_editor.select_all_in_shape(&document.network_interface, SelectionShape::Box(bbox), selection_change);
|
||||
}
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(&document.network_interface, SelectionShape::Lasso(&tool_data.lasso_polygon), selection_change),
|
||||
}
|
||||
}
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
PathToolFsmState::Ready
|
||||
@@ -904,25 +992,40 @@ impl Fsm for PathToolFsmState {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(PathToolFsmState::DrawingBox, PathToolMessage::Escape | PathToolMessage::RightClick) => {
|
||||
(PathToolFsmState::Drawing { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
// Mouse up
|
||||
(PathToolFsmState::DrawingBox, PathToolMessage::DragStop { extend_selection }) => {
|
||||
(PathToolFsmState::Drawing { selection_shape }, PathToolMessage::DragStop { extend_selection, shrink_selection }) => {
|
||||
let extend_selection = input.keyboard.get(extend_selection as usize);
|
||||
let shrink_selection = input.keyboard.get(shrink_selection as usize);
|
||||
|
||||
let select_kind = if shrink_selection {
|
||||
SelectionChange::Shrink
|
||||
} else if extend_selection {
|
||||
SelectionChange::Extend
|
||||
} else {
|
||||
SelectionChange::Clear
|
||||
};
|
||||
|
||||
if tool_data.drag_start_pos == tool_data.previous_mouse_position {
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
|
||||
} else {
|
||||
shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !extend_selection);
|
||||
match selection_shape {
|
||||
SelectionShapeType::Box => {
|
||||
let bbox = [tool_data.drag_start_pos, tool_data.previous_mouse_position];
|
||||
shape_editor.select_all_in_shape(&document.network_interface, SelectionShape::Box(bbox), select_kind);
|
||||
}
|
||||
SelectionShapeType::Lasso => shape_editor.select_all_in_shape(&document.network_interface, SelectionShape::Lasso(&tool_data.lasso_polygon), select_kind),
|
||||
}
|
||||
}
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::DragStop { extend_selection }) => {
|
||||
(_, PathToolMessage::DragStop { extend_selection, .. }) => {
|
||||
if tool_data.select_anchor_toggled {
|
||||
shape_editor.deselect_all_points();
|
||||
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle);
|
||||
@@ -1058,6 +1161,7 @@ impl Fsm for PathToolFsmState {
|
||||
let hint_data = match self {
|
||||
PathToolFsmState::Ready => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]),
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]),
|
||||
// TODO: Only show if at least one anchor is selected, and dynamically show either "Smooth" or "Sharp" based on the current state
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDouble, "Make Anchor Smooth/Sharp")]),
|
||||
@@ -1114,11 +1218,12 @@ impl Fsm for PathToolFsmState {
|
||||
|
||||
dragging_hint_data
|
||||
}
|
||||
PathToolFsmState::DrawingBox => HintData(vec![
|
||||
PathToolFsmState::Drawing { .. } => HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
||||
HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(),
|
||||
HintInfo::keys([Key::Shift], "Extend").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "Subtract").prepend_plus(),
|
||||
]),
|
||||
]),
|
||||
PathToolFsmState::InsertPoint => HintData(vec![
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
|
||||
use crate::consts::{DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
@@ -12,10 +12,12 @@ use crate::messages::portfolio::document::utility_types::transformation::Selecte
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_text, is_layer_fed_by_node_of_name};
|
||||
use crate::messages::tool::common_functionality::pivot::Pivot;
|
||||
use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
use crate::messages::tool::common_functionality::{auto_panning::AutoPanning, measure};
|
||||
|
||||
use bezier_rs::Subpath;
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::text::load_face;
|
||||
@@ -74,14 +76,23 @@ pub enum SelectToolMessage {
|
||||
Overlays(OverlayContext),
|
||||
|
||||
// Tool-specific messages
|
||||
DragStart { extend_selection: Key, select_deepest: Key },
|
||||
DragStop { remove_from_selection: Key, negative_box_selection: Key },
|
||||
DragStart {
|
||||
extend_selection: Key,
|
||||
remove_from_selection: Key,
|
||||
select_deepest: Key,
|
||||
lasso_select: Key,
|
||||
},
|
||||
DragStop {
|
||||
remove_from_selection: Key,
|
||||
},
|
||||
EditLayer,
|
||||
Enter,
|
||||
PointerMove(SelectToolPointerKeys),
|
||||
PointerOutsideViewport(SelectToolPointerKeys),
|
||||
SelectOptions(SelectOptionsUpdate),
|
||||
SetPivot { position: PivotPosition },
|
||||
SetPivot {
|
||||
position: PivotPosition,
|
||||
},
|
||||
}
|
||||
|
||||
impl ToolMetadata for SelectTool {
|
||||
@@ -252,10 +263,11 @@ impl ToolTransition for SelectTool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
enum SelectToolFsmState {
|
||||
Ready { selection: NestedSelectionBehavior },
|
||||
DrawingBox,
|
||||
Drawing { selection_shape: SelectionShapeType },
|
||||
Dragging,
|
||||
ResizingBounds,
|
||||
RotatingBounds,
|
||||
@@ -273,6 +285,8 @@ impl Default for SelectToolFsmState {
|
||||
struct SelectToolData {
|
||||
drag_start: ViewportPosition,
|
||||
drag_current: ViewportPosition,
|
||||
lasso_polygon: Vec<ViewportPosition>,
|
||||
selection_mode: Option<SelectionMode>,
|
||||
layers_dragging: Vec<LayerNodeIdentifier>,
|
||||
layer_selected_on_start: Option<LayerNodeIdentifier>,
|
||||
select_single_layer: Option<LayerNodeIdentifier>,
|
||||
@@ -308,14 +322,21 @@ impl SelectToolData {
|
||||
Quad::from_box(bbox)
|
||||
}
|
||||
|
||||
pub fn calculate_direction(&self) -> SelectionMode {
|
||||
pub fn calculate_direction(&mut self) -> SelectionMode {
|
||||
let bbox: [DVec2; 2] = self.selection_box();
|
||||
if bbox[1].x < bbox[0].x {
|
||||
SelectionMode::Touched
|
||||
} else {
|
||||
// This also covers the case where they're equal: the area is zero, so we use `Enclosed` to ensure the selection ends up empty, as nothing will be enclosed by an empty area
|
||||
SelectionMode::Enclosed
|
||||
let above_threshold = bbox[1].distance_squared(bbox[0]) > DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD.powi(2);
|
||||
|
||||
if self.selection_mode.is_none() && above_threshold {
|
||||
let mode = if bbox[1].x < bbox[0].x {
|
||||
SelectionMode::Touched
|
||||
} else {
|
||||
// This also covers the case where they're equal: the area is zero, so we use `Enclosed` to ensure the selection ends up empty, as nothing will be enclosed by an empty area
|
||||
SelectionMode::Enclosed
|
||||
};
|
||||
self.selection_mode = Some(mode);
|
||||
}
|
||||
|
||||
self.selection_mode.unwrap_or(SelectionMode::Touched)
|
||||
}
|
||||
|
||||
pub fn selection_box(&self) -> [DVec2; 2] {
|
||||
@@ -327,6 +348,22 @@ impl SelectToolData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersect_lasso_no_artboards(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) -> Vec<LayerNodeIdentifier> {
|
||||
if self.lasso_polygon.len() < 2 {
|
||||
return Vec::new();
|
||||
}
|
||||
let polygon = Subpath::from_anchors_linear(self.lasso_polygon.clone(), true);
|
||||
document.intersect_polygon_no_artboards(polygon, input).collect()
|
||||
}
|
||||
|
||||
pub fn is_layer_inside_lasso_polygon(&self, layer: &LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) -> bool {
|
||||
if self.lasso_polygon.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
let polygon = Subpath::from_anchors_linear(self.lasso_polygon.clone(), true);
|
||||
document.is_layer_fully_inside_polygon(layer, input, polygon)
|
||||
}
|
||||
|
||||
/// Duplicates the currently dragging layers. Called when Alt is pressed and the layers have not yet been duplicated.
|
||||
fn start_duplicates(&mut self, document: &mut DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
self.non_duplicated_layers = Some(self.layers_dragging.clone());
|
||||
@@ -493,8 +530,8 @@ impl Fsm for SelectToolFsmState {
|
||||
// Update pivot
|
||||
tool_data.pivot.update_pivot(document, &mut overlay_context);
|
||||
|
||||
// Check if the tool is in box selection mode
|
||||
if matches!(self, Self::DrawingBox) {
|
||||
// Check if the tool is in selection mode
|
||||
if let Self::Drawing { selection_shape } = self {
|
||||
// Get the updated selection box bounds
|
||||
let quad = Quad::from_box([tool_data.drag_start, tool_data.drag_current]);
|
||||
|
||||
@@ -505,9 +542,16 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
// Draw outline visualizations on the layers to be selected
|
||||
let mut draw_layer_outline = |layer| overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
|
||||
let intersection = document.intersect_quad_no_artboards(quad, input);
|
||||
let intersection: Vec<LayerNodeIdentifier> = match selection_shape {
|
||||
SelectionShapeType::Box => document.intersect_quad_no_artboards(quad, input).collect(),
|
||||
SelectionShapeType::Lasso => tool_data.intersect_lasso_no_artboards(document, input),
|
||||
};
|
||||
if selection_direction == SelectionMode::Enclosed {
|
||||
for layer in intersection.filter(|layer| document.is_layer_fully_inside(layer, quad)) {
|
||||
let is_inside = |layer: &LayerNodeIdentifier| match selection_shape {
|
||||
SelectionShapeType::Box => document.is_layer_fully_inside(layer, quad),
|
||||
SelectionShapeType::Lasso => tool_data.is_layer_inside_lasso_polygon(layer, document, input),
|
||||
};
|
||||
for layer in intersection.into_iter().filter(is_inside) {
|
||||
draw_layer_outline(layer);
|
||||
}
|
||||
} else {
|
||||
@@ -524,10 +568,13 @@ impl Fsm for SelectToolFsmState {
|
||||
fill_color.insert(0, '#');
|
||||
let fill_color = Some(fill_color.as_str());
|
||||
|
||||
if selection_direction == SelectionMode::Enclosed {
|
||||
overlay_context.dashed_quad(quad, fill_color, Some(4.), Some(4.), Some(0.5));
|
||||
} else {
|
||||
overlay_context.quad(quad, fill_color);
|
||||
let polygon = &tool_data.lasso_polygon;
|
||||
|
||||
match (selection_shape, selection_direction) {
|
||||
(SelectionShapeType::Box, SelectionMode::Enclosed) => overlay_context.dashed_quad(quad, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Lasso, SelectionMode::Enclosed) => overlay_context.dashed_polygon(polygon, fill_color, Some(4.), Some(4.), Some(0.5)),
|
||||
(SelectionShapeType::Box, _) => overlay_context.quad(quad, fill_color),
|
||||
(SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, fill_color),
|
||||
}
|
||||
}
|
||||
// Only highlight layers if the viewport is not being panned (middle mouse button is pressed)
|
||||
@@ -566,9 +613,18 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
self
|
||||
}
|
||||
(SelectToolFsmState::Ready { .. }, SelectToolMessage::DragStart { extend_selection, select_deepest }) => {
|
||||
(
|
||||
SelectToolFsmState::Ready { .. },
|
||||
SelectToolMessage::DragStart {
|
||||
extend_selection,
|
||||
remove_from_selection,
|
||||
select_deepest,
|
||||
lasso_select,
|
||||
},
|
||||
) => {
|
||||
tool_data.drag_start = input.mouse.position;
|
||||
tool_data.drag_current = input.mouse.position;
|
||||
tool_data.selection_mode = None;
|
||||
|
||||
let dragging_bounds = tool_data.bounding_box_manager.as_mut().and_then(|bounding_box| {
|
||||
let edges = bounding_box.check_selected_edges(input.mouse.position);
|
||||
@@ -697,8 +753,7 @@ impl Fsm for SelectToolFsmState {
|
||||
// Dragging a selection box
|
||||
else {
|
||||
tool_data.layers_dragging = selected;
|
||||
|
||||
if !input.keyboard.key(extend_selection) {
|
||||
if !input.keyboard.key(extend_selection) && !input.keyboard.key(remove_from_selection) {
|
||||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
tool_data.layers_dragging.clear();
|
||||
}
|
||||
@@ -716,7 +771,8 @@ impl Fsm for SelectToolFsmState {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
SelectToolFsmState::Dragging
|
||||
} else {
|
||||
SelectToolFsmState::DrawingBox
|
||||
let selection_shape = if input.keyboard.key(lasso_select) { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
|
||||
SelectToolFsmState::Drawing { selection_shape }
|
||||
}
|
||||
};
|
||||
tool_data.non_duplicated_layers = None;
|
||||
@@ -873,10 +929,14 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
SelectToolFsmState::DraggingPivot
|
||||
}
|
||||
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
(SelectToolFsmState::Drawing { selection_shape }, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
tool_data.drag_current = input.mouse.position;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
if selection_shape == SelectionShapeType::Lasso {
|
||||
extend_lasso(&mut tool_data.lasso_polygon, tool_data.drag_current);
|
||||
}
|
||||
|
||||
// AutoPanning
|
||||
let messages = [
|
||||
SelectToolMessage::PointerOutsideViewport(modifier_keys.clone()).into(),
|
||||
@@ -884,7 +944,7 @@ impl Fsm for SelectToolFsmState {
|
||||
];
|
||||
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
|
||||
|
||||
SelectToolFsmState::DrawingBox
|
||||
SelectToolFsmState::Drawing { selection_shape }
|
||||
}
|
||||
(SelectToolFsmState::Ready { .. }, SelectToolMessage::PointerMove(_)) => {
|
||||
let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
|
||||
@@ -931,7 +991,7 @@ impl Fsm for SelectToolFsmState {
|
||||
|
||||
self
|
||||
}
|
||||
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerOutsideViewport(_)) => {
|
||||
(SelectToolFsmState::Drawing { .. }, SelectToolMessage::PointerOutsideViewport(_)) => {
|
||||
// AutoPanning
|
||||
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
|
||||
tool_data.drag_start += shift;
|
||||
@@ -960,7 +1020,7 @@ impl Fsm for SelectToolFsmState {
|
||||
let selection = tool_data.nested_selection_behavior;
|
||||
SelectToolFsmState::Ready { selection }
|
||||
}
|
||||
(SelectToolFsmState::Dragging, SelectToolMessage::DragStop { remove_from_selection, .. }) => {
|
||||
(SelectToolFsmState::Dragging, SelectToolMessage::DragStop { remove_from_selection }) => {
|
||||
// Deselect layer if not snap dragging
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
|
||||
@@ -1059,13 +1119,7 @@ impl Fsm for SelectToolFsmState {
|
||||
let selection = tool_data.nested_selection_behavior;
|
||||
SelectToolFsmState::Ready { selection }
|
||||
}
|
||||
(
|
||||
SelectToolFsmState::DrawingBox,
|
||||
SelectToolMessage::DragStop {
|
||||
remove_from_selection,
|
||||
negative_box_selection,
|
||||
},
|
||||
) => {
|
||||
(SelectToolFsmState::Drawing { selection_shape }, SelectToolMessage::DragStop { remove_from_selection }) => {
|
||||
let quad = tool_data.selection_quad();
|
||||
|
||||
let mut selection_direction = tool_action_data.preferences.get_selection_mode();
|
||||
@@ -1073,17 +1127,24 @@ impl Fsm for SelectToolFsmState {
|
||||
selection_direction = tool_data.calculate_direction();
|
||||
}
|
||||
|
||||
let intersection = document.intersect_quad_no_artboards(quad, input);
|
||||
let intersection: Vec<LayerNodeIdentifier> = match selection_shape {
|
||||
SelectionShapeType::Box => document.intersect_quad_no_artboards(quad, input).collect(),
|
||||
SelectionShapeType::Lasso => tool_data.intersect_lasso_no_artboards(document, input),
|
||||
};
|
||||
let new_selected: HashSet<_> = if selection_direction == SelectionMode::Enclosed {
|
||||
intersection.filter(|layer| document.is_layer_fully_inside(layer, quad)).collect()
|
||||
let is_inside = |layer: &LayerNodeIdentifier| match selection_shape {
|
||||
SelectionShapeType::Box => document.is_layer_fully_inside(layer, quad),
|
||||
SelectionShapeType::Lasso => tool_data.is_layer_inside_lasso_polygon(layer, document, input),
|
||||
};
|
||||
intersection.into_iter().filter(is_inside).collect()
|
||||
} else {
|
||||
intersection.collect()
|
||||
intersection.into_iter().collect()
|
||||
};
|
||||
|
||||
let current_selected: HashSet<_> = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect();
|
||||
if new_selected != current_selected {
|
||||
// Negative selection when both Shift and Ctrl are pressed
|
||||
if input.keyboard.key(remove_from_selection) && input.keyboard.key(negative_box_selection) {
|
||||
if input.keyboard.key(remove_from_selection) {
|
||||
let updated_selection = current_selected
|
||||
.into_iter()
|
||||
.filter(|layer| !new_selected.iter().any(|selected| layer.starts_with(*selected, document.metadata())))
|
||||
@@ -1114,6 +1175,9 @@ impl Fsm for SelectToolFsmState {
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
tool_data.lasso_polygon.clear();
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
let selection = tool_data.nested_selection_behavior;
|
||||
@@ -1199,7 +1263,9 @@ impl Fsm for SelectToolFsmState {
|
||||
}),
|
||||
HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
||||
HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(),
|
||||
HintInfo::keys([Key::Shift], "Extend").prepend_plus(),
|
||||
HintInfo::keys([Key::Alt], "Subtract").prepend_plus(),
|
||||
HintInfo::keys([Key::Control], "Lasso").prepend_plus(),
|
||||
]),
|
||||
HintGroup(vec![HintInfo::multi_keys([[Key::KeyG], [Key::KeyR], [Key::KeyS]], "Grab/Rotate/Scale Selected")]),
|
||||
HintGroup(vec![
|
||||
@@ -1226,10 +1292,10 @@ impl Fsm for SelectToolFsmState {
|
||||
]);
|
||||
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
||||
}
|
||||
SelectToolFsmState::DrawingBox => {
|
||||
SelectToolFsmState::Drawing { .. } => {
|
||||
let hint_data = HintData(vec![
|
||||
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Control, Key::Shift], "Remove from Selection").add_mac_keys([Key::Command, Key::Shift])]),
|
||||
HintGroup(vec![HintInfo::keys([Key::Shift], "Extend"), HintInfo::keys([Key::Alt], "Subtract")]),
|
||||
// TODO: Re-select deselected layers during drag when Shift is pressed, and re-deselect if Shift is released before drag ends.
|
||||
// TODO: (See https://discord.com/channels/731730685944922173/1216976541947531264/1321360311298818048)
|
||||
// HintGroup(vec![HintInfo::keys([Key::Shift], "Extend Selection")])
|
||||
@@ -1328,3 +1394,17 @@ fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, network_interface
|
||||
responses.add(TextToolMessage::EditSelected);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend_lasso(lasso_polygon: &mut Vec<DVec2>, point: DVec2) {
|
||||
if lasso_polygon.len() < 2 {
|
||||
lasso_polygon.push(point);
|
||||
} else {
|
||||
let last_points = lasso_polygon.last_chunk::<2>().unwrap();
|
||||
let distance = last_points[0].distance_squared(last_points[1]);
|
||||
|
||||
if distance < SELECTION_TOLERANCE.powi(2) {
|
||||
lasso_polygon.pop();
|
||||
}
|
||||
lasso_polygon.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user