mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-18 18:38:05 +08:00
Add overlays for free-floating anchors on hovered/selected vector layers (#2630)
* Add selection overlay for free-floating anchors * Add hover overlay for free-floating anchors * Refactor outline_free_floating anchor * Add single-anchor click targets on VectorData * Modify ClickTarget to adapt for Subpath and PointGroup * Fix Rust formatting * Remove debug statements * Add point groups support in VectorDataTable::add_upstream_click_targets * Improve overlay for free floating anchors * Remove datatype for nodes_to_shift * Fix formatting in select_tool.rs * Lints * Code review * Remove references to point_group * Refactor ManipulatorGroup for FreePoint in ClickTargetGroup * Rename ClickTargetGroup to ClickTargetType * Refactor outline_free_floating_anchors into outline * Adapt TransformCage to disable dragging and rotating on a single anchor layer * Fix hover on single points * Fix comments * Lints * Code review pass --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
@@ -20,22 +20,63 @@ use std::fmt::Write;
|
||||
#[cfg(feature = "vello")]
|
||||
use vello::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FreePoint {
|
||||
pub id: PointId,
|
||||
pub position: DVec2,
|
||||
}
|
||||
|
||||
impl FreePoint {
|
||||
pub fn new(id: PointId, position: DVec2) -> Self {
|
||||
Self { id, position }
|
||||
}
|
||||
|
||||
pub fn apply_transform(&mut self, transform: DAffine2) {
|
||||
self.position = transform.transform_point2(self.position);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum ClickTargetType {
|
||||
Subpath(bezier_rs::Subpath<PointId>),
|
||||
FreePoint(FreePoint),
|
||||
}
|
||||
|
||||
/// Represents a clickable target for the layer
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ClickTarget {
|
||||
subpath: bezier_rs::Subpath<PointId>,
|
||||
target_type: ClickTargetType,
|
||||
stroke_width: f64,
|
||||
bounding_box: Option<[DVec2; 2]>,
|
||||
}
|
||||
|
||||
impl ClickTarget {
|
||||
pub fn new(subpath: bezier_rs::Subpath<PointId>, stroke_width: f64) -> Self {
|
||||
pub fn new_with_subpath(subpath: bezier_rs::Subpath<PointId>, stroke_width: f64) -> Self {
|
||||
let bounding_box = subpath.loose_bounding_box();
|
||||
Self { subpath, stroke_width, bounding_box }
|
||||
Self {
|
||||
target_type: ClickTargetType::Subpath(subpath),
|
||||
stroke_width,
|
||||
bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subpath(&self) -> &bezier_rs::Subpath<PointId> {
|
||||
&self.subpath
|
||||
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),
|
||||
]);
|
||||
|
||||
Self {
|
||||
target_type: ClickTargetType::FreePoint(point),
|
||||
stroke_width,
|
||||
bounding_box,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target_type(&self) -> &ClickTargetType {
|
||||
&self.target_type
|
||||
}
|
||||
|
||||
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||
@@ -47,12 +88,26 @@ impl ClickTarget {
|
||||
}
|
||||
|
||||
pub fn apply_transform(&mut self, affine_transform: DAffine2) {
|
||||
self.subpath.apply_transform(affine_transform);
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref mut subpath) => {
|
||||
subpath.apply_transform(affine_transform);
|
||||
}
|
||||
ClickTargetType::FreePoint(ref mut point) => {
|
||||
point.apply_transform(affine_transform);
|
||||
}
|
||||
}
|
||||
self.update_bbox();
|
||||
}
|
||||
|
||||
fn update_bbox(&mut self) {
|
||||
self.bounding_box = self.subpath.bounding_box();
|
||||
match self.target_type {
|
||||
ClickTargetType::Subpath(ref subpath) => {
|
||||
self.bounding_box = 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.)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the click target intersect the path
|
||||
@@ -66,19 +121,24 @@ impl ClickTarget {
|
||||
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
|
||||
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() && bezier_iter().next().is_some_and(|bezier| self.subpath.contains_point(bezier.start)) {
|
||||
return true;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Check if shape is entirely within selection
|
||||
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)
|
||||
// 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)
|
||||
}
|
||||
ClickTargetType::FreePoint(point) => bezier_iter().map(|bezier: bezier_rs::Bezier| bezier.winding(point.position)).sum::<i32>() != 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the click target intersect the point (accounting for stroke size)
|
||||
@@ -107,7 +167,10 @@ impl ClickTarget {
|
||||
.is_some_and(|bbox| bbox[0].x <= point.x && point.x <= bbox[1].x && bbox[0].y <= point.y && point.y <= bbox[1].y)
|
||||
{
|
||||
// Check if the point is within the shape
|
||||
self.subpath.closed() && self.subpath.contains_point(point)
|
||||
match self.target_type() {
|
||||
ClickTargetType::Subpath(subpath) => subpath.closed() && subpath.contains_point(point),
|
||||
ClickTargetType::FreePoint(free_point) => free_point.position == point,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -653,10 +716,23 @@ impl GraphicElementRendered for VectorDataTable {
|
||||
subpath
|
||||
};
|
||||
|
||||
// For free-floating anchors, we need to add a click target for each
|
||||
let single_anchors_targets = instance.point_domain.ids().iter().filter_map(|&point_id| {
|
||||
if instance.connected_count(point_id) == 0 {
|
||||
let anchor = instance.point_domain.position_from_id(point_id).unwrap_or_default();
|
||||
let point = FreePoint::new(point_id, anchor);
|
||||
|
||||
Some(ClickTarget::new_with_free_point(point))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let click_targets = instance
|
||||
.stroke_bezier_paths()
|
||||
.map(fill)
|
||||
.map(|subpath| ClickTarget::new(subpath, stroke_width))
|
||||
.map(|subpath| ClickTarget::new_with_subpath(subpath, stroke_width))
|
||||
.chain(single_anchors_targets.into_iter())
|
||||
.collect::<Vec<ClickTarget>>();
|
||||
|
||||
metadata.click_targets.insert(element_id, click_targets);
|
||||
@@ -680,10 +756,25 @@ impl GraphicElementRendered for VectorDataTable {
|
||||
subpath
|
||||
};
|
||||
click_targets.extend(instance.instance.stroke_bezier_paths().map(fill).map(|subpath| {
|
||||
let mut click_target = ClickTarget::new(subpath, stroke_width);
|
||||
let mut click_target = ClickTarget::new_with_subpath(subpath, stroke_width);
|
||||
click_target.apply_transform(*instance.transform);
|
||||
click_target
|
||||
}));
|
||||
|
||||
// For free-floating anchors, we need to add a click target for each
|
||||
let single_anchors_targets = instance.instance.point_domain.ids().iter().filter_map(|&point_id| {
|
||||
if instance.instance.connected_count(point_id) > 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let anchor = instance.instance.point_domain.position_from_id(point_id).unwrap_or_default();
|
||||
let point = FreePoint::new(point_id, anchor);
|
||||
|
||||
let mut click_target = ClickTarget::new_with_free_point(point);
|
||||
click_target.apply_transform(*instance.transform);
|
||||
Some(click_target)
|
||||
});
|
||||
click_targets.extend(single_anchors_targets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,7 +876,7 @@ impl GraphicElementRendered for Artboard {
|
||||
fn collect_metadata(&self, metadata: &mut RenderMetadata, mut footprint: Footprint, element_id: Option<NodeId>) {
|
||||
if let Some(element_id) = element_id {
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.)]);
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
metadata.local_transforms.insert(element_id, DAffine2::from_translation(self.location.as_dvec2()));
|
||||
if self.clip {
|
||||
@@ -798,7 +889,7 @@ impl GraphicElementRendered for Artboard {
|
||||
|
||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let subpath_rectangle = Subpath::new_rect(DVec2::ZERO, self.dimensions.as_dvec2());
|
||||
click_targets.push(ClickTarget::new(subpath_rectangle, 0.));
|
||||
click_targets.push(ClickTarget::new_with_subpath(subpath_rectangle, 0.));
|
||||
}
|
||||
|
||||
fn contains_artboard(&self) -> bool {
|
||||
@@ -909,7 +1000,7 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
|
||||
let Some(element_id) = element_id else { return };
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.)]);
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
// TODO: Find a way to handle more than one row of the graphical data table
|
||||
if let Some(image) = self.instance_ref_iter().next() {
|
||||
@@ -919,7 +1010,7 @@ impl GraphicElementRendered for RasterDataTable<CPU> {
|
||||
|
||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
click_targets.push(ClickTarget::new(subpath, 0.));
|
||||
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
|
||||
}
|
||||
|
||||
fn to_graphic_element(&self) -> GraphicElement {
|
||||
@@ -976,7 +1067,7 @@ impl GraphicElementRendered for RasterDataTable<GPU> {
|
||||
let Some(element_id) = element_id else { return };
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new(subpath, 0.)]);
|
||||
metadata.click_targets.insert(element_id, vec![ClickTarget::new_with_subpath(subpath, 0.)]);
|
||||
metadata.upstream_footprints.insert(element_id, footprint);
|
||||
// TODO: Find a way to handle more than one row of the graphical data table
|
||||
if let Some(image) = self.instance_ref_iter().next() {
|
||||
@@ -986,7 +1077,7 @@ impl GraphicElementRendered for RasterDataTable<GPU> {
|
||||
|
||||
fn add_upstream_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
|
||||
let subpath = Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
click_targets.push(ClickTarget::new(subpath, 0.));
|
||||
click_targets.push(ClickTarget::new_with_subpath(subpath, 0.));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ mod modification;
|
||||
use super::misc::{dvec2_to_point, point_to_dvec2};
|
||||
use super::style::{PathStyle, Stroke};
|
||||
use crate::instances::Instances;
|
||||
use crate::renderer::{ClickTargetType, FreePoint};
|
||||
use crate::{AlphaBlending, Color, GraphicGroupTable};
|
||||
pub use attributes::*;
|
||||
use bezier_rs::ManipulatorGroup;
|
||||
@@ -115,11 +116,6 @@ impl core::hash::Hash for VectorData {
|
||||
}
|
||||
|
||||
impl VectorData {
|
||||
/// Construct some new vector data from a single subpath with an identity transform and black fill.
|
||||
pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
|
||||
Self::from_subpaths([subpath], false)
|
||||
}
|
||||
|
||||
/// Push a subpath to the vector data
|
||||
pub fn append_subpath(&mut self, subpath: impl Borrow<bezier_rs::Subpath<PointId>>, preserve_id: bool) {
|
||||
let subpath: &bezier_rs::Subpath<PointId> = subpath.borrow();
|
||||
@@ -135,6 +131,8 @@ impl VectorData {
|
||||
let mut segment_id = self.segment_domain.next_id();
|
||||
let mut last_point = None;
|
||||
let mut first_point = None;
|
||||
|
||||
// Construct a bezier segment from the two manipulators on the subpath.
|
||||
for pair in subpath.manipulator_groups().windows(2) {
|
||||
let start = last_point.unwrap_or_else(|| {
|
||||
let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) {
|
||||
@@ -178,11 +176,28 @@ impl VectorData {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_free_point(&mut self, point: &FreePoint, preserve_id: bool) {
|
||||
let mut point_id = self.point_domain.next_id();
|
||||
|
||||
// Use the current point ID if it's not already in the domain, otherwise generate a new one
|
||||
let id = if preserve_id && !self.point_domain.ids().contains(&point.id) {
|
||||
point.id
|
||||
} else {
|
||||
point_id.next_id()
|
||||
};
|
||||
self.point_domain.push(id, point.position);
|
||||
}
|
||||
|
||||
/// Appends a Kurbo BezPath to the vector data.
|
||||
pub fn append_bezpath(&mut self, bezpath: kurbo::BezPath) {
|
||||
AppendBezpath::append_bezpath(self, bezpath);
|
||||
}
|
||||
|
||||
/// Construct some new vector data from a single subpath with an identity transform and black fill.
|
||||
pub fn from_subpath(subpath: impl Borrow<bezier_rs::Subpath<PointId>>) -> Self {
|
||||
Self::from_subpaths([subpath], false)
|
||||
}
|
||||
|
||||
/// Construct some new vector data from subpaths with an identity transform and black fill.
|
||||
pub fn from_subpaths(subpaths: impl IntoIterator<Item = impl Borrow<bezier_rs::Subpath<PointId>>>, preserve_id: bool) -> Self {
|
||||
let mut vector_data = Self::default();
|
||||
@@ -194,6 +209,19 @@ impl VectorData {
|
||||
vector_data
|
||||
}
|
||||
|
||||
pub fn from_target_types(target_types: impl IntoIterator<Item = impl Borrow<ClickTargetType>>, preserve_id: bool) -> Self {
|
||||
let mut vector_data = Self::default();
|
||||
|
||||
for target_type in target_types.into_iter() {
|
||||
match target_type.borrow() {
|
||||
ClickTargetType::Subpath(subpath) => vector_data.append_subpath(subpath, preserve_id),
|
||||
ClickTargetType::FreePoint(point) => vector_data.append_free_point(point, preserve_id),
|
||||
}
|
||||
}
|
||||
|
||||
vector_data
|
||||
}
|
||||
|
||||
/// Compute the bounding boxes of the bezpaths without any transform
|
||||
pub fn bounding_box_rect(&self) -> Option<Rect> {
|
||||
self.bounding_box_with_transform_rect(DAffine2::IDENTITY)
|
||||
|
||||
Reference in New Issue
Block a user