diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 392dea5f..d4bc7008 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1313,7 +1313,7 @@ impl DocumentMessageHandler { .filter_map(|node| { let node_metadata = self.node_graph_handler.node_metadata.get(node)?; let node_graph_to_viewport = self.node_graph_to_viewport.get(&self.node_graph_handler.network)?; - node_metadata.node_click_target.subpath.bounding_box_with_transform(*node_graph_to_viewport) + node_metadata.node_click_target.subpath().bounding_box_with_transform(*node_graph_to_viewport) }) .reduce(graphene_core::renderer::Quad::combine_bounds) } 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 a18cb247..d52b970b 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 @@ -651,10 +651,7 @@ impl<'a> MessageHandler> for NodeGrap DVec2::new(context_menu_viewport.x + width, context_menu_viewport.y + height), [5.; 4], ); - let context_menu_click_target = ClickTarget { - subpath: context_menu_subpath, - stroke_width: 1., - }; + let context_menu_click_target = ClickTarget::new(context_menu_subpath, 1.); if context_menu_click_target.intersect_point(viewport_location, DAffine2::IDENTITY) { return; } @@ -1047,7 +1044,7 @@ impl<'a> MessageHandler> for NodeGrap let Some(bounding_box) = self .node_metadata .get(&selected_node_id) - .and_then(|node_metadata| node_metadata.node_click_target.subpath.bounding_box()) + .and_then(|node_metadata| node_metadata.node_click_target.subpath().bounding_box()) else { log::error!("Could not get bounding box for node: {selected_node_id}"); return; @@ -1692,7 +1689,7 @@ impl NodeGraphMessageHandler { let subpath = bezier_rs::Subpath::new_rounded_rect(click_target_corner_1, corner2, [radius; 4]); let stroke_width = 1.; - let node_click_target = ClickTarget { subpath, stroke_width }; + let node_click_target = ClickTarget::new(subpath, stroke_width); // Create input/output click targets let mut input_click_targets = Vec::new(); @@ -1722,7 +1719,7 @@ impl NodeGraphMessageHandler { input_top_left + corner1 + DVec2::new(0., node_row_index as f64 * 24.), input_bottom_right + corner1 + DVec2::new(0., node_row_index as f64 * 24.), ); - let input_click_target = ClickTarget { subpath, stroke_width }; + let input_click_target = ClickTarget::new(subpath, stroke_width); input_click_targets.push(input_click_target); } @@ -1732,7 +1729,7 @@ impl NodeGraphMessageHandler { input_top_left + node_top_right + DVec2::new(0., node_row_index as f64 * 24.), input_bottom_right + node_top_right + DVec2::new(0., node_row_index as f64 * 24.), ); - let output_click_target = ClickTarget { subpath, stroke_width }; + let output_click_target = ClickTarget::new(subpath, stroke_width); output_click_targets.push(output_click_target); } } else { @@ -1742,14 +1739,14 @@ impl NodeGraphMessageHandler { let stroke_width = 1.; let subpath = Subpath::new_ellipse(input_top_left + layer_input_offset, input_bottom_right + layer_input_offset); - let layer_input_click_target = ClickTarget { subpath, stroke_width }; + let layer_input_click_target = ClickTarget::new(subpath, stroke_width); input_click_targets.push(layer_input_click_target); if node.inputs.iter().filter(|input| input.is_exposed()).count() > 1 { let layer_input_offset = corner1 + DVec2::new(0., 24.); let stroke_width = 1.; let subpath = Subpath::new_ellipse(input_top_left + layer_input_offset, input_bottom_right + layer_input_offset); - let input_click_target = ClickTarget { subpath, stroke_width }; + let input_click_target = ClickTarget::new(subpath, stroke_width); input_click_targets.push(input_click_target); } @@ -1757,14 +1754,14 @@ impl NodeGraphMessageHandler { let layer_output_offset = corner1 + DVec2::new(2. * 24., -8.); let stroke_width = 1.; let subpath = Subpath::new_ellipse(input_top_left + layer_output_offset, input_bottom_right + layer_output_offset); - let layer_output_click_target = ClickTarget { subpath, stroke_width }; + let layer_output_click_target = ClickTarget::new(subpath, stroke_width); output_click_targets.push(layer_output_click_target); // Update visibility button click target let visibility_offset = corner1 + DVec2::new(width as f64, 24.); let subpath = Subpath::new_rounded_rect(DVec2::new(-12., -12.) + visibility_offset, DVec2::new(12., 12.) + visibility_offset, [3.; 4]); let stroke_width = 1.; - let layer_visibility_click_target = ClickTarget { subpath, stroke_width }; + let layer_visibility_click_target = ClickTarget::new(subpath, stroke_width); visibility_click_target = Some(layer_visibility_click_target); } let node_metadata = NodeMetadata { @@ -1785,7 +1782,7 @@ impl NodeGraphMessageHandler { let radius = 3.; let subpath = bezier_rs::Subpath::new_rounded_rect(corner1.into(), corner2.into(), [radius; 4]); let stroke_width = 1.; - let node_click_target = ClickTarget { subpath, stroke_width }; + let node_click_target = ClickTarget::new(subpath, stroke_width); let node_top_left = network.exports_metadata.1 * grid_size as i32; let mut node_top_left = DVec2::new(node_top_left.x as f64, node_top_left.y as f64); @@ -1802,7 +1799,7 @@ impl NodeGraphMessageHandler { for _ in 0..network.exports.len() { let stroke_width = 1.; let subpath = Subpath::new_ellipse(input_top_left + node_top_left, input_bottom_right + node_top_left); - let top_left_input = ClickTarget { subpath, stroke_width }; + let top_left_input = ClickTarget::new(subpath, stroke_width); input_click_targets.push(top_left_input); node_top_left += 24.; @@ -1840,7 +1837,7 @@ impl NodeGraphMessageHandler { let radius = 3.; let subpath = bezier_rs::Subpath::new_rounded_rect(corner1.into(), corner2.into(), [radius; 4]); let stroke_width = 1.; - let node_click_target = ClickTarget { subpath, stroke_width }; + let node_click_target = ClickTarget::new(subpath, stroke_width); let node_top_right = network.imports_metadata.1 * grid_size as i32; let mut node_top_right = DVec2::new(node_top_right.x as f64 + width as f64, node_top_right.y as f64); @@ -1856,7 +1853,7 @@ impl NodeGraphMessageHandler { for _ in 0..import_count { let stroke_width = 1.; let subpath = Subpath::new_ellipse(input_top_left + node_top_right, input_bottom_right + node_top_right); - let top_left_input = ClickTarget { subpath, stroke_width }; + let top_left_input = ClickTarget::new(subpath, stroke_width); output_click_targets.push(top_left_input); node_top_right.y += 24.; @@ -1876,7 +1873,7 @@ impl NodeGraphMessageHandler { let bounds = self .node_metadata .iter() - .filter_map(|(_, node_metadata)| node_metadata.node_click_target.subpath.bounding_box()) + .filter_map(|(_, node_metadata)| node_metadata.node_click_target.subpath().bounding_box()) .reduce(Quad::combine_bounds); self.bounding_box_subpath = bounds.map(|bounds| bezier_rs::Subpath::new_rect(bounds[0], bounds[1])); } diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index c2f6cd44..009f2a45 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -80,7 +80,7 @@ impl DocumentMetadata { } self.click_targets .get(&layer) - .map(|click| click.iter().map(|click| &click.subpath)) + .map(|click| click.iter().map(ClickTarget::subpath)) .map(|subpaths| VectorData::from_subpaths(subpaths, true)) } @@ -315,7 +315,7 @@ impl DocumentMetadata { self.click_targets .get(&layer)? .iter() - .filter_map(|click_target| click_target.subpath.bounding_box_with_transform(transform)) + .filter_map(|click_target| click_target.subpath().bounding_box_with_transform(transform)) .reduce(Quad::combine_bounds) } @@ -371,7 +371,7 @@ impl DocumentMetadata { pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator> { static EMPTY: Vec = Vec::new(); let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY); - click_targets.iter().map(|click_target| &click_target.subpath) + click_targets.iter().map(ClickTarget::subpath) } } diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 418c089a..61bae895 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -276,6 +276,27 @@ impl Subpath { .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) } + /// Return the min and max corners that represent the loose bounding box of the subpath (bounding box of all handles and anchors). + pub fn loose_bounding_box(&self) -> Option<[DVec2; 2]> { + self.manipulator_groups + .iter() + .flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)]) + .flatten() + .map(|pos| [pos, pos]) + .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) + } + + /// Return the min and max corners that represent the loose bounding box of the subpath, after a given affine transform. + pub fn loose_bounding_box_with_transform(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> { + self.manipulator_groups + .iter() + .flat_map(|group| [group.in_handle, group.out_handle, Some(group.anchor)]) + .flatten() + .map(|pos| transform.transform_point2(pos)) + .map(|pos| [pos, pos]) + .reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) + } + /// Returns list of `t`-values representing the inflection points of the subpath. /// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`. /// diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index fbede2e8..aef313cc 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -23,11 +23,21 @@ use vello::*; #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ClickTarget { - pub subpath: bezier_rs::Subpath, - pub stroke_width: f64, + subpath: bezier_rs::Subpath, + stroke_width: f64, + bounding_box: Option<[DVec2; 2]>, } impl ClickTarget { + pub fn new(subpath: bezier_rs::Subpath, stroke_width: f64) -> Self { + let bounding_box = subpath.loose_bounding_box(); + Self { subpath, stroke_width, bounding_box } + } + + pub fn subpath(&self) -> &bezier_rs::Subpath { + &self.subpath + } + /// Does the click target intersect the rectangle pub fn intersect_rectangle(&self, document_quad: Quad, layer_transform: DAffine2) -> bool { // Check if the matrix is not invertible @@ -60,9 +70,19 @@ impl ClickTarget { /// Does the click target intersect the point (accounting for stroke size) pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool { + let target_bounds = [point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)]; + let intersects = |a: [DVec2; 2], b: [DVec2; 2]| a[0].x <= b[1].x && a[1].x >= b[0].x && a[0].y <= b[1].y && a[1].y >= b[0].y; + // This bounding box is not very accurate as it is the axis aligned version of the transformed bounding box. However it is fast. + if !self + .bounding_box + .is_some_and(|loose| intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds)) + { + return false; + } + // Allows for selecting lines // TODO: actual intersection of stroke - let inflated_quad = Quad::from_box([point - DVec2::splat(self.stroke_width / 2.), point + DVec2::splat(self.stroke_width / 2.)]); + let inflated_quad = Quad::from_box(target_bounds); self.intersect_rectangle(inflated_quad, layer_transform) } } @@ -343,7 +363,7 @@ impl GraphicElementRendered for VectorData { } subpath }; - click_targets.extend(self.stroke_bezier_paths().map(fill).map(|subpath| ClickTarget { stroke_width, subpath })); + click_targets.extend(self.stroke_bezier_paths().map(fill).map(|subpath| ClickTarget::new(subpath, stroke_width))); } #[cfg(feature = "vello")] @@ -558,7 +578,7 @@ impl GraphicElementRendered for Artboard { fn add_click_targets(&self, click_targets: &mut Vec) { let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2()); subpath.apply_transform(self.graphic_group.transform.inverse()); - click_targets.push(ClickTarget { stroke_width: 0., subpath }); + click_targets.push(ClickTarget::new(subpath, 0.)); } fn contains_artboard(&self) -> bool { @@ -674,7 +694,7 @@ impl GraphicElementRendered for ImageFrame { fn add_click_targets(&self, click_targets: &mut Vec) { let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE); - click_targets.push(ClickTarget { subpath, stroke_width: 0. }); + click_targets.push(ClickTarget::new(subpath, 0.)); } #[cfg(feature = "vello")]