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:
Priyanshu
2025-01-31 00:37:49 -08:00
committed by GitHub
co-authored by Keavon Chambers
parent f462963a36
commit 6802365e14
7 changed files with 365 additions and 81 deletions
@@ -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 {