Factor in artboard clipping to the click target x-ray function (#2028)

* Add clipping to the click xray function

* Clip area rays as well

* Code review nits

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
James Lindsay 2024-10-13 00:38:17 +01:00 committed by GitHub
parent 3b0e9587eb
commit d6f46f73da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 204 additions and 57 deletions

View file

@ -17,7 +17,7 @@ use dyn_any::DynAny;
use base64::Engine;
use glam::{DAffine2, DVec2};
use num_traits::Zero;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fmt::Write;
#[cfg(feature = "vello")]
use vello::*;
@ -57,34 +57,28 @@ impl ClickTarget {
self.bounding_box = self.subpath.bounding_box();
}
/// Does the click target intersect the rectangle
pub fn intersect_rectangle(&self, document_quad: Quad, layer_transform: DAffine2) -> bool {
/// Does the click target intersect the path
pub fn intersect_path<It: Iterator<Item = bezier_rs::Bezier>>(&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::<i32>() != 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)
@ -277,6 +271,7 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform {
pub struct RenderMetadata {
pub footprints: HashMap<NodeId, (Footprint, DAffine2)>,
pub click_targets: HashMap<NodeId, Vec<ClickTarget>>,
pub clip_targets: HashSet<NodeId>,
}
pub trait GraphicElementRendered {
@ -650,6 +645,9 @@ impl GraphicElementRendered for Artboard {
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
metadata.footprints.insert(element_id, (footprint, DAffine2::from_translation(self.location.as_dvec2())));
if self.clip {
metadata.clip_targets.insert(element_id);
}
}
footprint.transform *= self.transform();
self.graphic_group.collect_metadata(metadata, footprint, None);

View file

@ -435,9 +435,9 @@ fn construct_closed_subpath() {
let circle = bezier_rs::Subpath::new_ellipse(DVec2::NEG_ONE, DVec2::ONE);
let vector_data = VectorData::from_subpath(&circle);
assert_eq!(vector_data.point_domain.ids().len(), 4);
let bézier_paths = vector_data.segment_bezier_iter().map(|(_, bézier, _, _)| zier).collect::<Vec<_>>();
assert_eq!(bézier_paths.len(), 4);
assert!(bézier_paths.iter().all(|&bézier| circle.iter().any(|original_bézier| original_bézier == zier)));
let bezier_paths = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
assert_eq!(bezier_paths.len(), 4);
assert!(bezier_paths.iter().all(|&bezier| circle.iter().any(|original_bezier| original_bezier == bezier)));
let generated = vector_data.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[circle]);
@ -445,12 +445,12 @@ fn construct_closed_subpath() {
#[test]
fn construct_open_subpath() {
let bézier = bezier_rs::Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::NEG_ONE, DVec2::ONE, DVec2::X);
let subpath = bezier_rs::Subpath::from_bezier(&bézier);
let bezier = bezier_rs::Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::NEG_ONE, DVec2::ONE, DVec2::X);
let subpath = bezier_rs::Subpath::from_bezier(&bezier);
let vector_data = VectorData::from_subpath(&subpath);
assert_eq!(vector_data.point_domain.ids().len(), 2);
let bézier_paths = vector_data.segment_bezier_iter().map(|(_, bézier, _, _)| zier).collect::<Vec<_>>();
assert_eq!(bézier_paths, vec![zier]);
let bezier_paths = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
assert_eq!(bezier_paths, vec![bezier]);
let generated = vector_data.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[subpath]);
@ -465,9 +465,9 @@ fn construct_many_subpath() {
let vector_data = VectorData::from_subpaths([&curve, &circle], false);
assert_eq!(vector_data.point_domain.ids().len(), 6);
let bézier_paths = vector_data.segment_bezier_iter().map(|(_, bézier, _, _)| zier).collect::<Vec<_>>();
assert_eq!(bézier_paths.len(), 5);
assert!(bézier_paths.iter().all(|&bézier| circle.iter().chain(curve.iter()).any(|original_bézier| original_bézier == zier)));
let bezier_paths = vector_data.segment_bezier_iter().map(|(_, bezier, _, _)| bezier).collect::<Vec<_>>();
assert_eq!(bezier_paths.len(), 5);
assert!(bezier_paths.iter().all(|&bezier| circle.iter().chain(curve.iter()).any(|original_bezier| original_bezier == bezier)));
let generated = vector_data.stroke_bezier_paths().collect::<Vec<_>>();
assert_subpath_eq(&generated, &[curve, circle]);

View file

@ -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> {
path_bool(a, b, PathBooleanOperation::Difference)
}
fn boolean_intersect(a: Path, b: Path) -> Vec<Path> {
pub fn boolean_intersect(a: Path, b: Path) -> Vec<Path> {
path_bool(a, b, PathBooleanOperation::Intersection)
}

View file

@ -19,7 +19,7 @@ use graphene_core::{Color, WasmNotSend};
use base64::Engine;
#[cfg(target_arch = "wasm32")]
use glam::DAffine2;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::Clamped;
@ -225,6 +225,7 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>(
let mut metadata = RenderMetadata {
footprints: HashMap::new(),
click_targets: HashMap::new(),
clip_targets: HashSet::new(),
};
data.collect_metadata(&mut metadata, footprint, None);