mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 05:18:19 +00:00
Use a coarse bounding box to avoid a detailed check for intersection when clicking artwork (#1887)
* Add bounding box check for intersection * Cache the bounding box * private subpath
This commit is contained in:
parent
32f5fba3e3
commit
ea44d1440a
5 changed files with 65 additions and 27 deletions
|
@ -1313,7 +1313,7 @@ impl DocumentMessageHandler {
|
||||||
.filter_map(|node| {
|
.filter_map(|node| {
|
||||||
let node_metadata = self.node_graph_handler.node_metadata.get(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)?;
|
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)
|
.reduce(graphene_core::renderer::Quad::combine_bounds)
|
||||||
}
|
}
|
||||||
|
|
|
@ -651,10 +651,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||||
DVec2::new(context_menu_viewport.x + width, context_menu_viewport.y + height),
|
DVec2::new(context_menu_viewport.x + width, context_menu_viewport.y + height),
|
||||||
[5.; 4],
|
[5.; 4],
|
||||||
);
|
);
|
||||||
let context_menu_click_target = ClickTarget {
|
let context_menu_click_target = ClickTarget::new(context_menu_subpath, 1.);
|
||||||
subpath: context_menu_subpath,
|
|
||||||
stroke_width: 1.,
|
|
||||||
};
|
|
||||||
if context_menu_click_target.intersect_point(viewport_location, DAffine2::IDENTITY) {
|
if context_menu_click_target.intersect_point(viewport_location, DAffine2::IDENTITY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1047,7 +1044,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||||
let Some(bounding_box) = self
|
let Some(bounding_box) = self
|
||||||
.node_metadata
|
.node_metadata
|
||||||
.get(&selected_node_id)
|
.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 {
|
else {
|
||||||
log::error!("Could not get bounding box for node: {selected_node_id}");
|
log::error!("Could not get bounding box for node: {selected_node_id}");
|
||||||
return;
|
return;
|
||||||
|
@ -1692,7 +1689,7 @@ impl NodeGraphMessageHandler {
|
||||||
|
|
||||||
let subpath = bezier_rs::Subpath::new_rounded_rect(click_target_corner_1, corner2, [radius; 4]);
|
let subpath = bezier_rs::Subpath::new_rounded_rect(click_target_corner_1, corner2, [radius; 4]);
|
||||||
let stroke_width = 1.;
|
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
|
// Create input/output click targets
|
||||||
let mut input_click_targets = Vec::new();
|
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_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.),
|
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);
|
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_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.),
|
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);
|
output_click_targets.push(output_click_target);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1742,14 +1739,14 @@ impl NodeGraphMessageHandler {
|
||||||
|
|
||||||
let stroke_width = 1.;
|
let stroke_width = 1.;
|
||||||
let subpath = Subpath::new_ellipse(input_top_left + layer_input_offset, input_bottom_right + layer_input_offset);
|
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);
|
input_click_targets.push(layer_input_click_target);
|
||||||
|
|
||||||
if node.inputs.iter().filter(|input| input.is_exposed()).count() > 1 {
|
if node.inputs.iter().filter(|input| input.is_exposed()).count() > 1 {
|
||||||
let layer_input_offset = corner1 + DVec2::new(0., 24.);
|
let layer_input_offset = corner1 + DVec2::new(0., 24.);
|
||||||
let stroke_width = 1.;
|
let stroke_width = 1.;
|
||||||
let subpath = Subpath::new_ellipse(input_top_left + layer_input_offset, input_bottom_right + layer_input_offset);
|
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);
|
input_click_targets.push(input_click_target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1757,14 +1754,14 @@ impl NodeGraphMessageHandler {
|
||||||
let layer_output_offset = corner1 + DVec2::new(2. * 24., -8.);
|
let layer_output_offset = corner1 + DVec2::new(2. * 24., -8.);
|
||||||
let stroke_width = 1.;
|
let stroke_width = 1.;
|
||||||
let subpath = Subpath::new_ellipse(input_top_left + layer_output_offset, input_bottom_right + layer_output_offset);
|
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);
|
output_click_targets.push(layer_output_click_target);
|
||||||
|
|
||||||
// Update visibility button click target
|
// Update visibility button click target
|
||||||
let visibility_offset = corner1 + DVec2::new(width as f64, 24.);
|
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 subpath = Subpath::new_rounded_rect(DVec2::new(-12., -12.) + visibility_offset, DVec2::new(12., 12.) + visibility_offset, [3.; 4]);
|
||||||
let stroke_width = 1.;
|
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);
|
visibility_click_target = Some(layer_visibility_click_target);
|
||||||
}
|
}
|
||||||
let node_metadata = NodeMetadata {
|
let node_metadata = NodeMetadata {
|
||||||
|
@ -1785,7 +1782,7 @@ impl NodeGraphMessageHandler {
|
||||||
let radius = 3.;
|
let radius = 3.;
|
||||||
let subpath = bezier_rs::Subpath::new_rounded_rect(corner1.into(), corner2.into(), [radius; 4]);
|
let subpath = bezier_rs::Subpath::new_rounded_rect(corner1.into(), corner2.into(), [radius; 4]);
|
||||||
let stroke_width = 1.;
|
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 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);
|
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() {
|
for _ in 0..network.exports.len() {
|
||||||
let stroke_width = 1.;
|
let stroke_width = 1.;
|
||||||
let subpath = Subpath::new_ellipse(input_top_left + node_top_left, input_bottom_right + node_top_left);
|
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);
|
input_click_targets.push(top_left_input);
|
||||||
|
|
||||||
node_top_left += 24.;
|
node_top_left += 24.;
|
||||||
|
@ -1840,7 +1837,7 @@ impl NodeGraphMessageHandler {
|
||||||
let radius = 3.;
|
let radius = 3.;
|
||||||
let subpath = bezier_rs::Subpath::new_rounded_rect(corner1.into(), corner2.into(), [radius; 4]);
|
let subpath = bezier_rs::Subpath::new_rounded_rect(corner1.into(), corner2.into(), [radius; 4]);
|
||||||
let stroke_width = 1.;
|
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 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);
|
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 {
|
for _ in 0..import_count {
|
||||||
let stroke_width = 1.;
|
let stroke_width = 1.;
|
||||||
let subpath = Subpath::new_ellipse(input_top_left + node_top_right, input_bottom_right + node_top_right);
|
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);
|
output_click_targets.push(top_left_input);
|
||||||
|
|
||||||
node_top_right.y += 24.;
|
node_top_right.y += 24.;
|
||||||
|
@ -1876,7 +1873,7 @@ impl NodeGraphMessageHandler {
|
||||||
let bounds = self
|
let bounds = self
|
||||||
.node_metadata
|
.node_metadata
|
||||||
.iter()
|
.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);
|
.reduce(Quad::combine_bounds);
|
||||||
self.bounding_box_subpath = bounds.map(|bounds| bezier_rs::Subpath::new_rect(bounds[0], bounds[1]));
|
self.bounding_box_subpath = bounds.map(|bounds| bezier_rs::Subpath::new_rect(bounds[0], bounds[1]));
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ impl DocumentMetadata {
|
||||||
}
|
}
|
||||||
self.click_targets
|
self.click_targets
|
||||||
.get(&layer)
|
.get(&layer)
|
||||||
.map(|click| click.iter().map(|click| &click.subpath))
|
.map(|click| click.iter().map(ClickTarget::subpath))
|
||||||
.map(|subpaths| VectorData::from_subpaths(subpaths, true))
|
.map(|subpaths| VectorData::from_subpaths(subpaths, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,7 +315,7 @@ impl DocumentMetadata {
|
||||||
self.click_targets
|
self.click_targets
|
||||||
.get(&layer)?
|
.get(&layer)?
|
||||||
.iter()
|
.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)
|
.reduce(Quad::combine_bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,7 +371,7 @@ impl DocumentMetadata {
|
||||||
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> {
|
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> {
|
||||||
static EMPTY: Vec<ClickTarget> = Vec::new();
|
static EMPTY: Vec<ClickTarget> = Vec::new();
|
||||||
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -276,6 +276,27 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
||||||
.reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])])
|
.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.
|
/// 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]`.
|
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
|
||||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/inflections/solo" title="Inflections Demo"></iframe>
|
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/inflections/solo" title="Inflections Demo"></iframe>
|
||||||
|
|
|
@ -23,11 +23,21 @@ use vello::*;
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct ClickTarget {
|
pub struct ClickTarget {
|
||||||
pub subpath: bezier_rs::Subpath<PointId>,
|
subpath: bezier_rs::Subpath<PointId>,
|
||||||
pub stroke_width: f64,
|
stroke_width: f64,
|
||||||
|
bounding_box: Option<[DVec2; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClickTarget {
|
impl ClickTarget {
|
||||||
|
pub fn new(subpath: bezier_rs::Subpath<PointId>, stroke_width: f64) -> Self {
|
||||||
|
let bounding_box = subpath.loose_bounding_box();
|
||||||
|
Self { subpath, stroke_width, bounding_box }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subpath(&self) -> &bezier_rs::Subpath<PointId> {
|
||||||
|
&self.subpath
|
||||||
|
}
|
||||||
|
|
||||||
/// Does the click target intersect the rectangle
|
/// Does the click target intersect the rectangle
|
||||||
pub fn intersect_rectangle(&self, document_quad: Quad, layer_transform: DAffine2) -> bool {
|
pub fn intersect_rectangle(&self, document_quad: Quad, layer_transform: DAffine2) -> bool {
|
||||||
// Check if the matrix is not invertible
|
// Check if the matrix is not invertible
|
||||||
|
@ -60,9 +70,19 @@ impl ClickTarget {
|
||||||
|
|
||||||
/// Does the click target intersect the point (accounting for stroke size)
|
/// Does the click target intersect the point (accounting for stroke size)
|
||||||
pub fn intersect_point(&self, point: DVec2, layer_transform: DAffine2) -> bool {
|
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
|
// Allows for selecting lines
|
||||||
// TODO: actual intersection of stroke
|
// 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)
|
self.intersect_rectangle(inflated_quad, layer_transform)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,7 +363,7 @@ impl GraphicElementRendered for VectorData {
|
||||||
}
|
}
|
||||||
subpath
|
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")]
|
#[cfg(feature = "vello")]
|
||||||
|
@ -558,7 +578,7 @@ impl GraphicElementRendered for Artboard {
|
||||||
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||||
let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
|
let mut subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
|
||||||
subpath.apply_transform(self.graphic_group.transform.inverse());
|
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 {
|
fn contains_artboard(&self) -> bool {
|
||||||
|
@ -674,7 +694,7 @@ impl GraphicElementRendered for ImageFrame<Color> {
|
||||||
|
|
||||||
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
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")]
|
#[cfg(feature = "vello")]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue