Skip to content

Commit

Permalink
Clip area rays as well
Browse files Browse the repository at this point in the history
  • Loading branch information
0HyperCube authored and Keavon committed Oct 12, 2024
1 parent 92f463e commit bcd1f55
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 58 deletions.
151 changes: 116 additions & 35 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<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);

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).
Expand Down Expand Up @@ -2102,49 +2093,139 @@ 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<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,
point: DVec2,
paret_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()
}

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<Item = &'a ClickTarget>, transform: DAffine2) -> Vec<path_bool_lib::PathSegment> {
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<ClickTarget>>, clip: bool, layer: LayerNodeIdentifier, path: Vec<path_bool_lib::PathSegment>, 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::<Vec<_>>();
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)
}
}

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 (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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
6 changes: 3 additions & 3 deletions editor/src/messages/tool/tool_messages/select_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
24 changes: 9 additions & 15 deletions node-graph/gcore/src/graphic_element/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<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)
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion node-graph/gstd/src/vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
}

0 comments on commit bcd1f55

Please sign in to comment.