From bcd1f55bc3a7d7dc82a25a2e53092f8498d11514 Mon Sep 17 00:00:00 2001 From: hypercube <0hypercube@gmail.com> Date: Sat, 12 Oct 2024 17:47:05 +0100 Subject: [PATCH] Clip area rays as well --- .../document/document_message_handler.rs | 151 ++++++++++++++---- .../node_graph/node_graph_message_handler.rs | 6 +- .../tool/tool_messages/select_tool.rs | 6 +- .../gcore/src/graphic_element/renderer.rs | 24 ++- node-graph/gstd/src/vector.rs | 3 +- 5 files changed, 132 insertions(+), 58 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 653e64d5eb..94ce5b3801 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -31,6 +31,8 @@ use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::vector::style::ViewMode; use glam::{DAffine2, DVec2, IVec2}; +use graphene_std::renderer::{ClickTarget, Quad}; +use graphene_std::vector::path_bool_lib; pub struct DocumentMessageData<'a> { pub document_id: DocumentId, @@ -1336,30 +1338,19 @@ impl DocumentMessageHandler { let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz); let document_quad = document_to_viewport.inverse() * viewport_quad; - self.metadata() - .all_layers() - .filter(|&layer| self.network_interface.selected_nodes(&[]).unwrap().layer_visible(layer, &self.network_interface)) - .filter(|&layer| !self.network_interface.selected_nodes(&[]).unwrap().layer_locked(layer, &self.network_interface)) - .filter(|&layer| !self.network_interface.is_artboard(&layer.to_node(), &[])) - .filter_map(|layer| self.metadata().click_targets(layer).map(|targets| (layer, targets))) - .filter(move |(layer, target)| { - target - .iter() - .any(move |target| target.intersect_rectangle(document_quad, self.metadata().transform_to_document(*layer))) - }) - .map(|(layer, _)| layer) + ClickXRayIter::new(&self.network_interface, XRayTarget::Quad(document_quad)) + } + + /// Runs an intersection test with all layers and a viewport space quad; ignoring artboards + pub fn intersect_quad_no_artboards<'a>(&'a self, viewport_quad: graphene_core::renderer::Quad, ipp: &InputPreprocessorMessageHandler) -> impl Iterator + 'a { + self.intersect_quad(viewport_quad, ipp).filter(|layer| !self.network_interface.is_artboard(&layer.to_node(), &[])) } /// Find all of the layers that were clicked on from a viewport space location pub fn click_xray(&self, ipp: &InputPreprocessorMessageHandler) -> impl Iterator + '_ { let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz); let point = document_to_viewport.inverse().transform_point2(ipp.mouse.position); - - ClickXRayIter { - next_layer: LayerNodeIdentifier::ROOT_PARENT.first_child(self.metadata()), - network_interface: &self.network_interface, - point, - } + ClickXRayIter::new(&self.network_interface, XRayTarget::Point(point)) } /// Find the deepest layer given in the sorted array (by returning the one which is not a folder from the list of layers under the click location). @@ -2102,32 +2093,108 @@ fn default_document_network_interface() -> NodeNetworkInterface { network_interface } +/// Targets for the [`ClickXRayIter`]. In order to reduce computation, we prefer just a point/path test where possible. +#[derive(Clone)] +enum XRayTarget { + Point(DVec2), + Quad(Quad), + Path(Vec), +} + +/// The result for the [`ClickXRayIter`] on the layer +struct XRayResult { + clicked: bool, + use_children: bool, +} + +/// An iterator for finding layers within an [`XRayTarget`]. Constructed by [`DocumentMessageHandler::intersect_quad`] and [`DocumentMessageHandler::click_xray`]. #[derive(Clone)] pub struct ClickXRayIter<'a> { next_layer: Option, network_interface: &'a NodeNetworkInterface, - point: DVec2, + paret_targets: Vec<(LayerNodeIdentifier, XRayTarget)>, +} + +fn quad_to_path_lib_segments(quad: Quad) -> Vec { + quad.edges().into_iter().map(|[start, end]| path_bool_lib::PathSegment::Line(start, end)).collect() } -impl ClickXRayIter<'_> { - fn check_clicked_and_use_children(&self, layer: LayerNodeIdentifier) -> (bool, bool) { - let (mut clicked, mut use_children) = (true, true); +fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator, transform: DAffine2) -> Vec { + let segment = |bézier: bezier_rs::Bezier| match bézier.handles { + bezier_rs::BezierHandles::Linear => path_bool_lib::PathSegment::Line(bézier.start, bézier.end), + bezier_rs::BezierHandles::Quadratic { handle } => path_bool_lib::PathSegment::Quadratic(bézier.start, handle, bézier.end), + bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bézier.start, handle_start, handle_end, bézier.end), + }; + click_targets + .flat_map(|target| target.subpath().iter()) + .map(|bézier| segment(bézier.apply_transformation(|x| transform.transform_point2(x)))) + .collect() +} + +impl<'a> ClickXRayIter<'a> { + fn new(network_interface: &'a NodeNetworkInterface, target: XRayTarget) -> Self { + Self { + next_layer: LayerNodeIdentifier::ROOT_PARENT.first_child(network_interface.document_metadata()), + network_interface, + paret_targets: vec![(LayerNodeIdentifier::ROOT_PARENT, target)], + } + } + + /// Handles the checking of the layer where the target is a rect or path + fn check_layer_area_target(&mut self, click_targets: Option<&Vec>, clip: bool, layer: LayerNodeIdentifier, path: Vec, transform: DAffine2) -> XRayResult { + // Convert back to bézier-rs types for intersections + let segment = |bézier: &path_bool_lib::PathSegment| match *bézier { + path_bool_lib::PathSegment::Line(start, end) => bezier_rs::Bezier::from_linear_dvec2(start, end), + path_bool_lib::PathSegment::Cubic(start, h1, h2, end) => bezier_rs::Bezier::from_cubic_dvec2(start, h1, h2, end), + path_bool_lib::PathSegment::Quadratic(start, h1, end) => bezier_rs::Bezier::from_quadratic_dvec2(start, h1, end), + path_bool_lib::PathSegment::Arc(_, _, _, _, _, _, _) => unimplemented!(), + }; + let get_clip = || path.iter().map(segment); + + let intersects = click_targets.map_or(false, |targets| targets.iter().any(|target| target.intersect_path(get_clip, transform))); + let clicked = intersects; + let mut use_children = !clip || intersects; + + // In the case of a clip path where the area partially intersects, it is necessary to do a boolean operation. + // We do this on this using the target area to reduce computation (as the target area is usually very simple). + if clip && intersects { + let clip_path = click_targets_to_path_lib_segments(click_targets.iter().flat_map(|x| x.iter()), transform); + let subtracted = graphene_std::vector::boolean_intersect(path, clip_path).into_iter().flatten().collect::>(); + if subtracted.is_empty() { + use_children = false; + } else { + // All child layers will use the new clipped target area + self.paret_targets.push((layer, XRayTarget::Path(subtracted))); + } + } + XRayResult { clicked, use_children } + } + + /// Handles the checking of the layer to find if it has been clicked + fn check_layer(&mut self, layer: LayerNodeIdentifier) -> XRayResult { let selected_layers = self.network_interface.selected_nodes(&[]).unwrap(); + // Discard invisble and locked layers if !selected_layers.layer_visible(layer, &self.network_interface) || selected_layers.layer_locked(layer, &self.network_interface) { - return (false, false); // Skip this layer and children if the layer is invisible or locked + return XRayResult { clicked: false, use_children: false }; } + let click_targets = self.network_interface.document_metadata().click_targets(layer); let transform = self.network_interface.document_metadata().transform_to_document(layer); - let intersects = click_targets.map_or(false, |targets| targets.iter().any(|target| target.intersect_point(self.point, transform))); - - if !intersects { - clicked = false; - } - - if self.network_interface.document_metadata().is_clip(layer.to_node()) && !intersects { - use_children = false; + let target = &self.paret_targets.last().expect("there should be a target").1; + let clip = self.network_interface.document_metadata().is_clip(layer.to_node()); + + match target { + // Single points are much cheaper than paths so have their own special case + XRayTarget::Point(point) => { + let intersects = click_targets.map_or(false, |targets| targets.iter().any(|target| target.intersect_point(*point, transform))); + XRayResult { + clicked: intersects, + use_children: !clip || intersects, + } + } + 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), } - (clicked, use_children) } } @@ -2135,16 +2202,30 @@ impl<'a> Iterator for ClickXRayIter<'a> { type Item = LayerNodeIdentifier; fn next(&mut self) -> Option { + // While there are still layers in the layer tree while let Some(layer) = self.next_layer.take() { - let (clicked, use_children) = self.check_clicked_and_use_children(layer); + let XRayResult { clicked, use_children } = self.check_layer(layer); let metadata = self.network_interface.document_metadata(); - let child = use_children.then(|| layer.first_child(metadata)).flatten(); - self.next_layer = child.or_else(|| layer.ancestors(metadata).find_map(|ancestor| ancestor.next_sibling(metadata))); + // If we should use the children and also there is a child, that child is the next layer. + self.next_layer = use_children.then(|| layer.first_child(metadata)).flatten(); + + // If we aren't using children, iterate up the ancestors until there is a lyer with a sibling + for ancestor in layer.ancestors(metadata) { + if self.next_layer.is_some() { + break; + } + // If there is a clipped area for this ancestor (that we are now exiting), discard it. + if self.paret_targets.last().is_some_and(|(id, _)| *id == ancestor) { + self.paret_targets.pop(); + } + self.next_layer = ancestor.next_sibling(metadata) + } if clicked { return Some(layer); } } + assert!(self.paret_targets.is_empty(), "The parent targets should always be empty (since we have left all layers)"); None } } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 4349081d6d..fe0871d314 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -1339,10 +1339,8 @@ impl<'a> MessageHandler> for NodeGrap log::error!("Could not get transient metadata for node {node_id}"); continue; }; - if click_targets - .node_click_target - .intersect_rectangle(Quad::from_box([box_selection_start, box_selection_end_graph]), DAffine2::IDENTITY) - { + let quad = Quad::from_box([box_selection_start, box_selection_end_graph]); + if click_targets.node_click_target.intersect_path(|| quad.bezier_lines(), DAffine2::IDENTITY) { nodes.insert(node_id); } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index d5c88f7f55..ec938b4067 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -471,7 +471,7 @@ impl Fsm for SelectToolFsmState { let quad = Quad::from_box([tool_data.drag_start, tool_data.drag_current]); // Draw outline visualizations on the layers to be selected - for layer in document.intersect_quad(quad, input) { + for layer in document.intersect_quad_no_artboards(quad, input) { overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); } @@ -914,7 +914,7 @@ impl Fsm for SelectToolFsmState { if !tool_data.has_dragged && input.keyboard.key(remove_from_selection) && tool_data.layer_selected_on_start.is_none() { let quad = tool_data.selection_quad(); - let intersection = document.intersect_quad(quad, input); + let intersection = document.intersect_quad_no_artboards(quad, input); if let Some(path) = intersection.last() { let replacement_selected_layers: Vec<_> = document @@ -1007,7 +1007,7 @@ impl Fsm for SelectToolFsmState { } (SelectToolFsmState::DrawingBox { .. }, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => { let quad = tool_data.selection_quad(); - let new_selected: HashSet<_> = document.intersect_quad(quad, input).collect(); + let new_selected: HashSet<_> = document.intersect_quad_no_artboards(quad, input).collect(); let current_selected: HashSet<_> = document.network_interface.selected_nodes(&[]).unwrap().selected_layers(document.metadata()).collect(); if new_selected != current_selected { tool_data.layers_dragging = new_selected.into_iter().collect(); diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 71868ae347..a58aa85f1c 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -58,33 +58,27 @@ impl ClickTarget { } /// Does the click target intersect the rectangle - pub fn intersect_rectangle(&self, document_quad: Quad, layer_transform: DAffine2) -> bool { + pub fn intersect_path>(&self, mut bezier_iter: impl FnMut() -> It, layer_transform: DAffine2) -> bool { // Check if the matrix is not invertible if layer_transform.matrix2.determinant().abs() <= f64::EPSILON { return false; } - let quad = layer_transform.inverse() * document_quad; + let inverse = layer_transform.inverse(); + let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point))); // Check if outlines intersect - if self - .subpath - .iter() - .any(|path_segment| quad.bezier_lines().any(|line| !path_segment.intersections(&line, None, None).is_empty())) - { + let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty()); + if self.subpath.iter().any(outline_intersects) { return true; } // Check if selection is entirely within the shape - if self.subpath.closed() && self.subpath.contains_point(quad.center()) { + if self.subpath.closed() && bezier_iter().next().is_some_and(|bezier| self.subpath.contains_point(bezier.start)) { return true; } // Check if shape is entirely within selection - self.subpath - .manipulator_groups() - .first() - .map(|group| group.anchor) - .map(|shape_point| quad.contains(shape_point)) - .unwrap_or_default() + let any_point_from_subpath = self.subpath.manipulator_groups().first().map(|group| group.anchor); + any_point_from_subpath.is_some_and(|shape_point| bezier_iter().map(|bezier| bezier.winding(shape_point)).sum::() != 0) } /// Does the click target intersect the point (accounting for stroke size) @@ -102,7 +96,7 @@ impl ClickTarget { // Allows for selecting lines // TODO: actual intersection of stroke let inflated_quad = Quad::from_box(target_bounds); - self.intersect_rectangle(inflated_quad, layer_transform) + self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform) } /// Does the click target intersect the point (not accounting for stroke size) diff --git a/node-graph/gstd/src/vector.rs b/node-graph/gstd/src/vector.rs index 760bb9bcd9..54c1b2c527 100644 --- a/node-graph/gstd/src/vector.rs +++ b/node-graph/gstd/src/vector.rs @@ -5,6 +5,7 @@ use graphene_core::transform::Transform; use graphene_core::vector::misc::BooleanOperation; pub use graphene_core::vector::*; use graphene_core::{Color, GraphicElement, GraphicGroup}; +pub use path_bool as path_bool_lib; use path_bool::FillRule; use path_bool::PathBooleanOperation; @@ -342,6 +343,6 @@ fn boolean_subtract(a: Path, b: Path) -> Vec { path_bool(a, b, PathBooleanOperation::Difference) } -fn boolean_intersect(a: Path, b: Path) -> Vec { +pub fn boolean_intersect(a: Path, b: Path) -> Vec { path_bool(a, b, PathBooleanOperation::Intersection) }