mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-18 18:38:05 +08:00
refactor click targets
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
use crate::math::math_ext::QuadExt;
|
||||
use crate::math::quad::Quad;
|
||||
use crate::vector::PointId;
|
||||
use bezier_rs::Subpath;
|
||||
use crate::vector::misc::rect_with_size;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, PathSeg, Rect, Shape};
|
||||
|
||||
use super::algorithms::intersection::bezpath_and_segment_intersections;
|
||||
use super::misc::{bezpath_loose_bounding_box, dvec2_to_point, is_bezpath_closed, pathseg_to_points, point_to_dvec2, rect_to_minmax, transform_rect};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FreePoint {
|
||||
@@ -22,7 +24,7 @@ impl FreePoint {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ClickTargetType {
|
||||
Subpath(Subpath<PointId>),
|
||||
BezPath(BezPath),
|
||||
FreePoint(FreePoint),
|
||||
}
|
||||
|
||||
@@ -31,14 +33,14 @@ pub enum ClickTargetType {
|
||||
pub struct ClickTarget {
|
||||
target_type: ClickTargetType,
|
||||
stroke_width: f64,
|
||||
bounding_box: Option<[DVec2; 2]>,
|
||||
bounding_box: Option<Rect>,
|
||||
}
|
||||
|
||||
impl ClickTarget {
|
||||
pub fn new_with_subpath(subpath: Subpath<PointId>, stroke_width: f64) -> Self {
|
||||
let bounding_box = subpath.loose_bounding_box();
|
||||
pub fn new_with_bezpath(bezpath: BezPath, stroke_width: f64) -> Self {
|
||||
let bounding_box = bezpath_loose_bounding_box(&bezpath);
|
||||
Self {
|
||||
target_type: ClickTargetType::Subpath(subpath),
|
||||
target_type: ClickTargetType::BezPath(bezpath),
|
||||
stroke_width,
|
||||
bounding_box,
|
||||
}
|
||||
@@ -47,10 +49,8 @@ impl ClickTarget {
|
||||
pub fn new_with_free_point(point: FreePoint) -> Self {
|
||||
const MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT: f64 = 1e-4 / 2.;
|
||||
let stroke_width = 10.;
|
||||
let bounding_box = Some([
|
||||
point.position - DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
|
||||
point.position + DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT),
|
||||
]);
|
||||
|
||||
let bounding_box = Some(rect_with_size(point.position, MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT));
|
||||
|
||||
Self {
|
||||
target_type: ClickTargetType::FreePoint(point),
|
||||
@@ -64,21 +64,26 @@ impl ClickTarget {
|
||||
}
|
||||
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box
|
||||
self.bounding_box.map(|bbox| rect_to_minmax(bbox))
|
||||
}
|
||||
|
||||
pub fn bounding_box_center(&self) -> Option<DVec2> {
|
||||
self.bounding_box.map(|bbox| bbox[0] + (bbox[1] - bbox[0]) / 2.)
|
||||
self.bounding_box.map(|bbox| point_to_dvec2(bbox.center()))
|
||||
}
|
||||
|
||||
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
|
||||
self.bounding_box.map(|[a, b]| [transform.transform_point2(a), transform.transform_point2(b)])
|
||||
self.bounding_box.map(|bbox| {
|
||||
[
|
||||
transform.transform_point2(DVec2::new(bbox.min_x(), bbox.min_y())),
|
||||
transform.transform_point2(DVec2::new(bbox.max_x(), bbox.max_y())),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref mut subpath) => {
|
||||
subpath.apply_transform(affine_transform);
|
||||
ClickTargetType::BezPath(ref mut subpath) => {
|
||||
subpath.apply_affine(Affine::new(affine_transform.to_cols_array()));
|
||||
}
|
||||
ClickTargetType::FreePoint(ref mut point) => {
|
||||
point.apply_transform(affine_transform);
|
||||
@@ -89,17 +94,17 @@ impl ClickTarget {
|
||||
|
||||
fn update_bbox(&mut self) {
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref subpath) => {
|
||||
self.bounding_box = subpath.bounding_box();
|
||||
ClickTargetType::BezPath(ref subpath) => {
|
||||
self.bounding_box = Some(subpath.bounding_box());
|
||||
}
|
||||
ClickTargetType::FreePoint(ref point) => {
|
||||
self.bounding_box = Some([point.position - DVec2::splat(self.stroke_width / 2.), point.position + DVec2::splat(self.stroke_width / 2.)]);
|
||||
self.bounding_box = Some(rect_with_size(point.position, self.stroke_width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
pub fn intersect_path(&self, mut selection_bezpath: BezPath, layer_transform: DAffine2) -> bool {
|
||||
// Check if the matrix is not invertible
|
||||
let mut layer_transform = layer_transform;
|
||||
if layer_transform.matrix2.determinant().abs() <= f64::EPSILON {
|
||||
@@ -107,56 +112,39 @@ impl ClickTarget {
|
||||
}
|
||||
|
||||
let inverse = layer_transform.inverse();
|
||||
let mut bezier_iter = || bezier_iter().map(|bezier| bezier.apply_transformation(|point| inverse.transform_point2(point)));
|
||||
selection_bezpath.apply_affine(Affine::new(inverse.to_cols_array()));
|
||||
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => {
|
||||
// Check if outlines intersect
|
||||
let outline_intersects = |path_segment: bezier_rs::Bezier| bezier_iter().any(|line| !path_segment.intersections(&line, None, None).is_empty());
|
||||
if subpath.iter().any(outline_intersects) {
|
||||
return true;
|
||||
}
|
||||
// Check if selection is entirely within the shape
|
||||
if subpath.closed() && bezier_iter().next().is_some_and(|bezier| subpath.contains_point(bezier.start)) {
|
||||
return true;
|
||||
}
|
||||
ClickTargetType::BezPath(click_target_bezpath) => {
|
||||
let inside = |segment: PathSeg| pathseg_to_points(segment).iter().filter_map(|point| *point).all(|point| selection_bezpath.contains(point));
|
||||
let intersects = |segment: PathSeg| !bezpath_and_segment_intersections(&selection_bezpath, segment, None, None).is_empty();
|
||||
|
||||
// Check if shape is entirely within selection
|
||||
let any_point_from_subpath = 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)
|
||||
click_target_bezpath.segments().any(|target_segment| inside(target_segment)) || click_target_bezpath.segments().any(|target_segment| intersects(target_segment))
|
||||
}
|
||||
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,
|
||||
ClickTargetType::FreePoint(point) => selection_bezpath.contains(dvec2_to_point(point.position)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the click target intersect the point (accounting for stroke size)
|
||||
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;
|
||||
let target_bounds = rect_with_size(point, self.stroke_width);
|
||||
// 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| (loose[0] - loose[1]).abs().cmpgt(DVec2::splat(1e-4)).any() && intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds))
|
||||
{
|
||||
if !self.bounding_box.is_some_and(|bbox| (transform_rect(bbox, layer_transform)).overlaps(target_bounds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allows for selecting lines
|
||||
// TODO: actual intersection of stroke
|
||||
let inflated_quad = Quad::from_box(target_bounds);
|
||||
self.intersect_path(|| inflated_quad.bezier_lines(), layer_transform)
|
||||
self.intersect_path(target_bounds.to_path(DEFAULT_ACCURACY), layer_transform)
|
||||
}
|
||||
|
||||
/// Does the click target intersect the point (not accounting for stroke size)
|
||||
pub fn intersect_point_no_stroke(&self, point: DVec2) -> bool {
|
||||
// Check if the point is within the bounding box
|
||||
if self
|
||||
.bounding_box
|
||||
.is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y)
|
||||
{
|
||||
if self.bounding_box.is_some_and(|bbox| bbox.contains(dvec2_to_point(point))) {
|
||||
// Check if the point is within the shape
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point),
|
||||
ClickTargetType::BezPath(bezpath) => is_bezpath_closed(bezpath) && bezpath.contains(dvec2_to_point(point)),
|
||||
ClickTargetType::FreePoint(free_point) => free_point.position == point,
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -3,8 +3,8 @@ use super::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
||||
use crate::vector::{SegmentId, Vector};
|
||||
use bezier_rs::{BezierHandles, ManipulatorGroup, Subpath};
|
||||
use dyn_any::DynAny;
|
||||
use glam::DVec2;
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez, Rect};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathEl, PathSeg, Point, QuadBez, Rect};
|
||||
use std::ops::Sub;
|
||||
|
||||
/// Represents different ways of calculating the centroid.
|
||||
@@ -204,7 +204,7 @@ pub fn bezpath_to_manipulator_groups(bezpath: &BezPath) -> (Vec<ManipulatorGroup
|
||||
(manipulator_groups, is_closed)
|
||||
}
|
||||
|
||||
fn pathseg_to_points(segment: PathSeg) -> [Option<Point>; 4] {
|
||||
pub fn pathseg_to_points(segment: PathSeg) -> [Option<Point>; 4] {
|
||||
match segment {
|
||||
PathSeg::Line(line) => [Some(line.p0), None, None, Some(line.p1)],
|
||||
PathSeg::Quad(quad_bez) => [Some(quad_bez.p0), None, Some(quad_bez.p1), Some(quad_bez.p2)],
|
||||
@@ -224,6 +224,32 @@ pub fn bezpath_loose_bounding_box(bezpath: &BezPath) -> Option<Rect> {
|
||||
.reduce(|bbox1, bbox2| combine(bbox1, bbox2))
|
||||
}
|
||||
|
||||
pub fn is_bezpath_closed(bezpath: &BezPath) -> bool {
|
||||
bezpath.elements().last().is_some_and(|el| *el == PathEl::ClosePath)
|
||||
}
|
||||
|
||||
pub fn combine_rect(r1: Rect, r2: Rect) -> Rect {
|
||||
Rect::new(r1.x0.min(r2.x0), r1.y0.min(r2.y0), r1.x1.max(r2.x1), r1.y1.max(r2.y1))
|
||||
}
|
||||
|
||||
pub fn rect_from_minmax(minmax: [DVec2; 2]) -> Rect {
|
||||
Rect::new(minmax[0].x, minmax[0].y, minmax[1].x, minmax[1].y)
|
||||
}
|
||||
|
||||
pub fn rect_to_minmax(rect: Rect) -> [DVec2; 2] {
|
||||
[DVec2::new(rect.min_x(), rect.min_y()), DVec2::new(rect.max_x(), rect.max_y())]
|
||||
}
|
||||
|
||||
pub fn transform_rect(rect: Rect, transform: DAffine2) -> Rect {
|
||||
let min = transform.transform_point2(DVec2::new(rect.x0, rect.y0));
|
||||
let max = transform.transform_point2(DVec2::new(rect.x1, rect.y1));
|
||||
Rect::new(min.x, min.y, max.x, max.y)
|
||||
}
|
||||
|
||||
pub fn rect_with_size(point: DVec2, size: f64) -> Rect {
|
||||
Rect::new(point.x - size / 2., point.y - size / 2., point.x + size / 2., point.y + size / 2.)
|
||||
}
|
||||
|
||||
/// Returns true if the [`PathSeg`] is equivalent to a line.
|
||||
///
|
||||
/// This is different from simply checking if the segment is [`PathSeg::Line`] or [`PathSeg::Quad`] or [`PathSeg::Cubic`]. Bezier curve can also be a line if the control points are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line.
|
||||
|
||||
@@ -157,7 +157,7 @@ impl Vector {
|
||||
|
||||
for target_type in target_types.into_iter() {
|
||||
match target_type.borrow() {
|
||||
ClickTargetType::Subpath(subpath) => vector.append_subpath(subpath, preserve_id),
|
||||
ClickTargetType::BezPath(bezpath) => vector.append_bezpath(bezpath.clone()),
|
||||
ClickTargetType::FreePoint(point) => vector.append_free_point(point, preserve_id),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user