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

@ -110,7 +110,11 @@ impl Dispatcher {
self.message_queues.extend(buffered_queue);
};
let graphene_std::renderer::RenderMetadata { footprints, click_targets } = render_metadata;
let graphene_std::renderer::RenderMetadata {
footprints,
click_targets,
clip_targets,
} = render_metadata;
let mut update_upstream_transform = VecDeque::new();
update_upstream_transform.push_back(DocumentMessage::UpdateUpstreamTransforms { upstream_transforms: footprints }.into());
@ -118,6 +122,7 @@ impl Dispatcher {
let mut update_click_targets = VecDeque::new();
update_click_targets.push_back(DocumentMessage::UpdateClickTargets { click_targets }.into());
update_click_targets.push_back(DocumentMessage::UpdateClipTargets { clip_targets }.into());
self.message_queues.push(update_click_targets);
}
Message::NoOp => {}

View file

@ -162,6 +162,9 @@ pub enum DocumentMessage {
UpdateClickTargets {
click_targets: HashMap<NodeId, Vec<ClickTarget>>,
},
UpdateClipTargets {
clip_targets: HashSet<NodeId>,
},
UpdateVectorModify {
vector_modify: HashMap<NodeId, VectorData>,
},

View file

@ -29,6 +29,8 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeNetwork, OldNodeNetwork};
use graphene_core::raster::{BlendMode, ImageFrame};
use graphene_core::vector::style::ViewMode;
use graphene_std::renderer::{ClickTarget, Quad};
use graphene_std::vector::path_bool_lib;
use glam::{DAffine2, DVec2, IVec2};
@ -1109,6 +1111,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
.collect();
self.network_interface.update_click_targets(layer_click_targets);
}
DocumentMessage::UpdateClipTargets { clip_targets } => {
self.network_interface.update_clip_targets(clip_targets);
}
DocumentMessage::UpdateVectorModify { vector_modify } => {
self.network_interface.update_vector_modify(vector_modify);
}
@ -1333,31 +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<Item = LayerNodeIdentifier> + '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<Item = LayerNodeIdentifier> + '_ {
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);
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_map(|layer| self.metadata().click_targets(layer).map(|targets| (layer, targets)))
.filter(move |(layer, target)| target.iter().any(|target| target.intersect_point(point, self.metadata().transform_to_document(*layer))))
.map(|(layer, _)| layer)
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).
@ -2099,3 +2092,140 @@ fn default_document_network_interface() -> NodeNetworkInterface {
network_interface.add_export(TaggedValue::ArtboardGroup(graphene_core::ArtboardGroup::EMPTY), -1, "".to_string(), &[]);
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<path_bool_lib::PathSegment>),
}
/// 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<LayerNodeIdentifier>,
network_interface: &'a NodeNetworkInterface,
parent_targets: Vec<(LayerNodeIdentifier, XRayTarget)>,
}
fn quad_to_path_lib_segments(quad: Quad) -> Vec<path_bool_lib::PathSegment> {
quad.edges().into_iter().map(|[start, end]| path_bool_lib::PathSegment::Line(start, end)).collect()
}
fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> Vec<path_bool_lib::PathSegment> {
let segment = |bezier: bezier_rs::Bezier| match bezier.handles {
bezier_rs::BezierHandles::Linear => path_bool_lib::PathSegment::Line(bezier.start, bezier.end),
bezier_rs::BezierHandles::Quadratic { handle } => path_bool_lib::PathSegment::Quadratic(bezier.start, handle, bezier.end),
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => path_bool_lib::PathSegment::Cubic(bezier.start, handle_start, handle_end, bezier.end),
};
click_targets
.flat_map(|target| target.subpath().iter())
.map(|bezier| segment(bezier.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,
parent_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<ClickTarget>>, clip: bool, layer: LayerNodeIdentifier, path: Vec<path_bool_lib::PathSegment>, transform: DAffine2) -> XRayResult {
// Convert back to Bezier-rs types for intersections
let segment = |bezier: &path_bool_lib::PathSegment| match *bezier {
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::<Vec<_>>();
if subtracted.is_empty() {
use_children = false;
} else {
// All child layers will use the new clipped target area
self.parent_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 invisible and locked layers
if !selected_layers.layer_visible(layer, self.network_interface) || selected_layers.layer_locked(layer, self.network_interface) {
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 target = &self.parent_targets.last().expect("In `check_layer()`: 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),
}
}
}
impl<'a> Iterator for ClickXRayIter<'a> {
type Item = LayerNodeIdentifier;
fn next(&mut self) -> Option<Self::Item> {
// While there are still layers in the layer tree
while let Some(layer) = self.next_layer.take() {
let XRayResult { clicked, use_children } = self.check_layer(layer);
let metadata = self.network_interface.document_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 layer 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.parent_targets.last().is_some_and(|(id, _)| *id == ancestor) {
self.parent_targets.pop();
}
self.next_layer = ancestor.next_sibling(metadata)
}
if clicked {
return Some(layer);
}
}
assert!(self.parent_targets.is_empty(), "The parent targets should always be empty (since we have left all layers)");
None
}
}

View file

@ -1339,10 +1339,8 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> 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);
}
}

View file

@ -7,7 +7,7 @@ use graphene_std::vector::PointId;
use graphene_std::vector::VectorData;
use glam::{DAffine2, DVec2};
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;
// ================
@ -21,6 +21,7 @@ pub struct DocumentMetadata {
pub upstream_transforms: HashMap<NodeId, (Footprint, DAffine2)>,
pub structure: HashMap<LayerNodeIdentifier, NodeRelations>,
pub click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
pub clip_targets: HashSet<NodeId>,
pub vector_modify: HashMap<NodeId, VectorData>,
/// Transform from document space to viewport space.
pub document_to_viewport: DAffine2,
@ -33,6 +34,7 @@ impl Default for DocumentMetadata {
structure: HashMap::new(),
vector_modify: HashMap::new(),
click_targets: HashMap::new(),
clip_targets: HashSet::new(),
document_to_viewport: DAffine2::IDENTITY,
}
}
@ -149,6 +151,10 @@ impl DocumentMetadata {
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().map(ClickTarget::subpath)
}
pub fn is_clip(&self, node: NodeId) -> bool {
self.clip_targets.contains(&node)
}
}
// ===================

View file

@ -2930,6 +2930,11 @@ impl NodeNetworkInterface {
self.document_metadata.click_targets = new_click_targets;
}
/// Update the cached clip targets of the layers
pub fn update_clip_targets(&mut self, new_clip_targets: HashSet<NodeId>) {
self.document_metadata.clip_targets = new_clip_targets;
}
/// Update the vector modify of the layers
pub fn update_vector_modify(&mut self, new_vector_modify: HashMap<NodeId, VectorData>) {
self.document_metadata.vector_modify = new_vector_modify;

View file

@ -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();

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);