mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 21:18:11 +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:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
477a3f6670
commit
878f5d3bf7
@@ -108,7 +108,11 @@ impl SelectedLayerState {
|
||||
}
|
||||
|
||||
pub fn selected_points_count(&self) -> usize {
|
||||
self.selected_points.len()
|
||||
let count = self.selected_points.iter().fold(0, |acc, point| {
|
||||
let is_ignored = (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors);
|
||||
acc + if is_ignored { 0 } else { 1 }
|
||||
});
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -449,7 +449,11 @@ impl SnapManager {
|
||||
if let Some(ind) = &self.indicator {
|
||||
for layer in &ind.outline_layers {
|
||||
let &Some(layer) = layer else { continue };
|
||||
overlay_context.outline(snap_data.document.metadata().layer_outline(layer), snap_data.document.metadata().transform_to_viewport(layer), None);
|
||||
overlay_context.outline(
|
||||
snap_data.document.metadata().layer_with_free_points_outline(layer),
|
||||
snap_data.document.metadata().transform_to_viewport(layer),
|
||||
None,
|
||||
);
|
||||
}
|
||||
if let Some(quad) = ind.target_bounds {
|
||||
overlay_context.quad(to_viewport * quad, None, None);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use super::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
|
||||
use crate::consts::{
|
||||
BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY, MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS,
|
||||
MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY, RESIZE_HANDLE_SIZE, SELECTION_DRAG_ANGLE, SKEW_TRIANGLE_OFFSET,
|
||||
SKEW_TRIANGLE_SIZE,
|
||||
BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_OVERLAY_WHITE, MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT, MAXIMUM_ALT_SCALE_FACTOR, MIN_LENGTH_FOR_CORNERS_VISIBILITY,
|
||||
MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS, MIN_LENGTH_FOR_MIDPOINT_VISIBILITY, MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR, MIN_LENGTH_FOR_SKEW_TRIANGLE_VISIBILITY, RESIZE_HANDLE_SIZE,
|
||||
SELECTION_DRAG_ANGLE, SKEW_TRIANGLE_OFFSET, SKEW_TRIANGLE_SIZE,
|
||||
};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
@@ -52,6 +52,8 @@ enum TransformCageSizeCategory {
|
||||
Narrow,
|
||||
/// - 
|
||||
Flat,
|
||||
/// A single point in space with no width or height.
|
||||
Point,
|
||||
}
|
||||
|
||||
impl SelectedEdges {
|
||||
@@ -635,7 +637,12 @@ impl BoundingBoxManager {
|
||||
fn overlay_display_category(&self) -> TransformCageSizeCategory {
|
||||
let quad = self.transform * Quad::from_box(self.bounds);
|
||||
|
||||
// Check if the area is essentially zero because either the width or height is smaller than an epsilon
|
||||
// Check if the bounds are essentially the same because the width and height are smaller than MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT
|
||||
if self.is_bounds_point() {
|
||||
return TransformCageSizeCategory::Point;
|
||||
}
|
||||
|
||||
// Check if the area is essentially zero because either the width or height is smaller than MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT
|
||||
if self.is_bounds_flat() {
|
||||
return TransformCageSizeCategory::Flat;
|
||||
}
|
||||
@@ -661,7 +668,12 @@ impl BoundingBoxManager {
|
||||
|
||||
/// Determine if these bounds are flat ([`TransformCageSizeCategory::Flat`]), which means that the width and/or height is essentially zero and the bounds are a line with effectively no area. This can happen on actual lines (axis-aligned, i.e. drawn horizontally or vertically) or when an element is scaled to zero in X or Y. A flat transform cage can still be rotated by a transformation, but its local space remains flat.
|
||||
fn is_bounds_flat(&self) -> bool {
|
||||
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any()
|
||||
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).any()
|
||||
}
|
||||
|
||||
/// Determine if these bounds are point ([`TransformCageSizeCategory::Point`]), which means that the width and height are essentially zero and the bounds are a point with no area. This can happen on points when an element is scaled to zero in both X and Y, or if an element is just a single anchor point. A point transform cage cannot be rotated by a transformation, and its local space remains a point.
|
||||
fn is_bounds_point(&self) -> bool {
|
||||
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT)).all()
|
||||
}
|
||||
|
||||
/// Determine if the given point in viewport space falls within the bounds of `self`.
|
||||
@@ -699,7 +711,7 @@ impl BoundingBoxManager {
|
||||
let [edge_min_x, edge_min_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_RESIZE_TO_INCLUDE_INTERIOR);
|
||||
let [midpoint_threshold_x, midpoint_threshold_y] = self.compute_viewport_threshold(MIN_LENGTH_FOR_EDGE_RESIZE_PRIORITY_OVER_CORNERS);
|
||||
|
||||
if min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y && cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y {
|
||||
if (min.x - cursor.x < threshold_x && min.y - cursor.y < threshold_y) && (cursor.x - max.x < threshold_x && cursor.y - max.y < threshold_y) {
|
||||
let mut top = (cursor.y - min.y).abs() < threshold_y;
|
||||
let mut bottom = (max.y - cursor.y).abs() < threshold_y;
|
||||
let mut left = (cursor.x - min.x).abs() < threshold_x;
|
||||
@@ -741,11 +753,11 @@ impl BoundingBoxManager {
|
||||
}
|
||||
|
||||
// On bounds with no width/height, disallow transformation in the relevant axis
|
||||
if width < f64::EPSILON * 1000. {
|
||||
if width < MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT {
|
||||
left = false;
|
||||
right = false;
|
||||
}
|
||||
if height < f64::EPSILON * 1000. {
|
||||
if height < MAX_LENGTH_FOR_NO_WIDTH_OR_HEIGHT {
|
||||
top = false;
|
||||
bottom = false;
|
||||
}
|
||||
@@ -767,9 +779,12 @@ impl BoundingBoxManager {
|
||||
let [threshold_x, threshold_y] = self.compute_viewport_threshold(BOUNDS_ROTATE_THRESHOLD);
|
||||
let cursor = self.transform.inverse().transform_point2(cursor);
|
||||
|
||||
let flat = (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any();
|
||||
let flat = self.is_bounds_flat();
|
||||
let point = self.is_bounds_point();
|
||||
let within_square_bounds = |center: &DVec2| center.x - threshold_x < cursor.x && cursor.x < center.x + threshold_x && center.y - threshold_y < cursor.y && cursor.y < center.y + threshold_y;
|
||||
if flat {
|
||||
if point {
|
||||
false
|
||||
} else if flat {
|
||||
[self.bounds[0], self.bounds[1]].iter().any(within_square_bounds)
|
||||
} else {
|
||||
self.evaluate_transform_handle_positions().iter().any(within_square_bounds)
|
||||
|
||||
Reference in New Issue
Block a user