Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow selection by clicking open shapes that are visibly filled #1711

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
13 changes: 11 additions & 2 deletions editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag

impl DocumentMessageHandler {
/// Runs an intersection test with all layers and a viewport space quad
/// TODO: properly check if layer is filled
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this resolved?

pub fn intersect_quad<'a>(&'a self, viewport_quad: graphene_core::renderer::Quad, network: &'a NodeNetwork) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
let document_quad = self.metadata.document_to_viewport.inverse() * viewport_quad;
self.metadata
Expand All @@ -882,7 +883,11 @@ impl DocumentMessageHandler {
.filter(|&layer| !self.selected_nodes.layer_locked(layer, self.metadata()))
.filter(|&layer| !is_artboard(layer, network))
.filter_map(|layer| self.metadata.click_target(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))))
.filter(move |(layer, target)| {
target
.iter()
.any(move |target| target.intersect_rectangle(document_quad, self.metadata.transform_to_document(*layer), true))
})
.map(|(layer, _)| layer)
}

Expand All @@ -895,7 +900,11 @@ impl DocumentMessageHandler {
.filter(|&layer| self.selected_nodes.layer_visible(layer, self.metadata()))
.filter(|&layer| !self.selected_nodes.layer_locked(layer, self.metadata()))
.filter_map(|layer| self.metadata.click_target(layer).map(|targets| (layer, targets)))
.filter(move |(layer, target)| target.iter().any(|target: &ClickTarget| target.intersect_point(point, self.metadata.transform_to_document(*layer))))
.filter(move |(layer, target)| {
target
.iter()
.any(|target: &ClickTarget| target.intersect_point(point, self.metadata.transform_to_document(*layer), layer.visibly_filled(&self.network)))
})
.map(|(layer, _)| layer)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use super::nodes::SelectedNodes;

use graph_craft::document::{DocumentNode, NodeId, NodeNetwork};
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, NodeId, NodeInput, NodeNetwork};
use graphene_core::renderer::ClickTarget;
use graphene_core::renderer::Quad;
use graphene_core::transform::Footprint;
use graphene_core::vector::style::FillType;
use graphene_std::vector::PointId;

use glam::{DAffine2, DVec2};
use graphene_std::vector::PointId;
use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64;

Expand Down Expand Up @@ -532,6 +534,23 @@ impl LayerNodeIdentifier {
.last()
.expect("There should be a layer before the root")
}

pub fn visibly_filled(&self, network: &NodeNetwork) -> bool {
network
.upstream_flow_back_from_nodes(vec![self.to_node()], true)
.filter(|(node, _)| node.name == "Fill")
.any(|(node, _)| {
node.inputs.iter().any(|node_input| {
let NodeInput::Value { tagged_value, .. } = node_input else { return false };

match tagged_value {
TaggedValue::OptionalColor(optional_color) => optional_color.map(|color| color.a() > f32::EPSILON).unwrap_or(false),
TaggedValue::FillType(fill_type) => fill_type == &FillType::Gradient,
_ => false,
}
})
})
}
}

// ========
Expand Down
28 changes: 27 additions & 1 deletion libraries/bezier-rs/src/subpath/solvers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,33 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {

/// Does a path contain a point? Based on the non zero winding
pub fn contains_point(&self, target_point: DVec2) -> bool {
self.iter().map(|bezier| bezier.winding(target_point)).sum::<i32>() != 0
if self.closed() {
// Winding number for a closed curve.
return self.iter().map(|bezier| bezier.winding(target_point)).sum::<i32>() != 0;
}

// The curve is not closed. Pretend that it is and compute the winding number
// change as if we had a segment from the last point back to the first.

// Position of first and last anchor points.
let first_anchor_point = self.manipulator_groups.first().map(|group| group.anchor);
let last_anchor_point = self.manipulator_groups.last().map(|group| group.anchor);
let endpoints = first_anchor_point.zip(last_anchor_point);

// Weight interpolating intersection location from last to first anchor point. Reject weights outside of [0, 1].
let t = endpoints.map(|(first, last)| (target_point.y - last.y) / (first.y - last.y)).filter(|t| (0.0..=1.).contains(t));
// Compute point of intersection.
// Reject points that are right of the click location since we compute winding numbers by ray-casting left.
let intersection_point = endpoints.zip(t).map(|((first, last), t)| t * first + (1. - t) * last).filter(|p| p.x <= target_point.x);
let winding_modification = first_anchor_point.zip(intersection_point).map_or_else(
// None variant implies no intersection and no modification to winding number.
|| 0,
// Clockwise (decrement winding number) and counterclockwise (increment winding number) intersection respectively.
|(first, intersection)| if first.y >= intersection.y { -1 } else { 1 },
);

// Add the winding modification to the winding number of the rest of the curve.
self.iter().map(|bezier| bezier.winding(target_point)).sum::<i32>() + winding_modification != 0
Comment on lines +272 to +298
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this just involve temporarily closing the shape, calculating the value, then un-closing it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Keavon this would require the function to take the mutable shape which is unnecessarily restrictive and would involve cloning in some cases. Although I agree that this could be simplified to use the existing winding function instead of re-implementing it:

let modification = endpoints.map_or(0, |(p1, p2)| bezier_rs::Bezier::from_linear_dvec2(p1, p2).winding(target_point));

}

/// Randomly places points across the filled surface of this subpath (which is assumed to be closed).
Expand Down
8 changes: 4 additions & 4 deletions node-graph/gcore/src/graphic_element/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub struct ClickTarget {

impl ClickTarget {
/// 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, filled: bool) -> bool {
// Check if the matrix is not invertible
if layer_transform.matrix2.determinant().abs() <= std::f64::EPSILON {
return false;
Expand All @@ -37,7 +37,7 @@ impl ClickTarget {
return true;
}
// Check if selection is entirely within the shape
if self.subpath.closed() && self.subpath.contains_point(quad.center()) {
if filled && self.subpath.contains_point(quad.center()) {
return true;
}

Expand All @@ -51,11 +51,11 @@ impl ClickTarget {
}

/// 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, filled: bool) -> bool {
// 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.)]);
self.intersect_rectangle(inflated_quad, layer_transform)
self.intersect_rectangle(inflated_quad, layer_transform, filled)
}
}

Expand Down