mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-21 06:58:12 +08:00
Add snapping targets for b-box edges and multi-layer spacing distribution (#1793)
* Initial work on aligning bounding boxes * Work in progress distribution * Distribution snapping * Distribution overlays * Align points and clean up * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
0dbbabe73e
commit
cdd179cf10
@@ -1,7 +1,9 @@
|
||||
mod alignment_snapper;
|
||||
mod distribution_snapper;
|
||||
mod grid_snapper;
|
||||
mod layer_snapper;
|
||||
mod snap_results;
|
||||
pub use {grid_snapper::*, layer_snapper::*, snap_results::*};
|
||||
pub use {alignment_snapper::*, distribution_snapper::*, grid_snapper::*, layer_snapper::*, snap_results::*};
|
||||
|
||||
use crate::consts::COLOR_OVERLAY_BLUE;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
@@ -12,8 +14,10 @@ use crate::messages::prelude::*;
|
||||
use bezier_rs::{Subpath, TValue};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::vector::PointId;
|
||||
use graphene_std::renderer::Rect;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::vector::NoHashBuilder;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// Handles snapping and snap overlays
|
||||
@@ -22,7 +26,10 @@ pub struct SnapManager {
|
||||
indicator: Option<SnappedPoint>,
|
||||
layer_snapper: LayerSnapper,
|
||||
grid_snapper: GridSnapper,
|
||||
alignment_snapper: AlignmentSnapper,
|
||||
distribution_snapper: DistributionSnapper,
|
||||
candidates: Option<Vec<LayerNodeIdentifier>>,
|
||||
alignment_candidates: Option<Vec<LayerNodeIdentifier>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
@@ -77,8 +84,37 @@ fn compare_points(a: &&SnappedPoint, b: &&SnappedPoint) -> Ordering {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_closest_point(points: &[SnappedPoint]) -> Option<&SnappedPoint> {
|
||||
points.iter().min_by(compare_points)
|
||||
fn find_align(a: &SnappedPoint, b: &SnappedPoint) -> Ordering {
|
||||
(a.distance, a.distance_to_align_target).partial_cmp(&(b.distance, b.distance_to_align_target)).unwrap()
|
||||
}
|
||||
|
||||
fn get_closest_point(points: Vec<SnappedPoint>) -> Option<SnappedPoint> {
|
||||
let mut best_not_align = None;
|
||||
let mut best_align = None;
|
||||
for point in points {
|
||||
if !point.align() && !best_not_align.as_ref().is_some_and(|best| compare_points(&best, &&point).is_ge()) {
|
||||
best_not_align = Some(point);
|
||||
} else if point.align() && !best_align.as_ref().is_some_and(|best| find_align(best, &point).is_ge()) {
|
||||
best_align = Some(point)
|
||||
}
|
||||
}
|
||||
match (best_not_align, best_align) {
|
||||
(None, None) => None,
|
||||
(Some(result), None) | (None, Some(result)) => Some(result),
|
||||
(Some(mut result), Some(align)) => {
|
||||
let SnapTarget::Distribution(distribution) = result.target else { return Some(result) };
|
||||
if distribution.is_x() && align.alignment_target_x.is_some() {
|
||||
result.snapped_point_document.y = align.snapped_point_document.y;
|
||||
result.alignment_target_x = align.alignment_target_x;
|
||||
}
|
||||
if distribution.is_y() && align.alignment_target_y.is_some() {
|
||||
result.snapped_point_document.x = align.snapped_point_document.x;
|
||||
result.alignment_target_y = align.alignment_target_y;
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn get_closest_curve(curves: &[SnappedCurve], exclude_paths: bool) -> Option<&SnappedPoint> {
|
||||
let keep_curve = |curve: &&SnappedCurve| !exclude_paths || curve.point.target != SnapTarget::Geometry(GeometrySnapTarget::Path);
|
||||
@@ -148,13 +184,21 @@ fn get_grid_intersection(snap_to: DVec2, lines: &[SnappedLine]) -> Option<Snappe
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SnapCache {
|
||||
pub manipulators: HashMap<LayerNodeIdentifier, HashSet<PointId, NoHashBuilder>, NoHashBuilder>,
|
||||
pub unselected: Vec<SnapCandidatePoint>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SnapData<'a> {
|
||||
pub document: &'a DocumentMessageHandler,
|
||||
pub input: &'a InputPreprocessorMessageHandler,
|
||||
pub ignore: &'a [LayerNodeIdentifier],
|
||||
pub manipulators: Vec<(LayerNodeIdentifier, PointId)>,
|
||||
pub node_snap_cache: Option<&'a SnapCache>,
|
||||
pub candidates: Option<&'a Vec<LayerNodeIdentifier>>,
|
||||
pub alignment_candidates: Option<&'a Vec<LayerNodeIdentifier>>,
|
||||
}
|
||||
impl<'a> SnapData<'a> {
|
||||
pub fn new(document: &'a DocumentMessageHandler, input: &'a InputPreprocessorMessageHandler) -> Self {
|
||||
@@ -166,17 +210,27 @@ impl<'a> SnapData<'a> {
|
||||
input,
|
||||
ignore,
|
||||
candidates: None,
|
||||
manipulators: Vec::new(),
|
||||
alignment_candidates: None,
|
||||
node_snap_cache: None,
|
||||
}
|
||||
}
|
||||
pub fn new_snap_cache(document: &'a DocumentMessageHandler, input: &'a InputPreprocessorMessageHandler, snap_cache: &'a SnapCache) -> Self {
|
||||
Self {
|
||||
node_snap_cache: Some(snap_cache),
|
||||
..Self::new(document, input)
|
||||
}
|
||||
}
|
||||
fn get_candidates(&self) -> &[LayerNodeIdentifier] {
|
||||
self.candidates.map_or([].as_slice(), |candidates| candidates.as_slice())
|
||||
}
|
||||
fn ignore_bounds(&self, layer: LayerNodeIdentifier) -> bool {
|
||||
self.manipulators.iter().any(|&(ignore, _)| ignore == layer)
|
||||
self.node_snap_cache.is_some_and(|cache| cache.manipulators.contains_key(&layer))
|
||||
}
|
||||
fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: impl Into<PointId>) -> bool {
|
||||
self.manipulators.contains(&(layer, manipulator.into()))
|
||||
fn ignore_manipulator(&self, layer: LayerNodeIdentifier, target: PointId) -> bool {
|
||||
self.node_snap_cache.and_then(|cache| cache.manipulators.get(&layer)).is_some_and(|points| points.contains(&target))
|
||||
}
|
||||
fn has_manipulators(&self) -> bool {
|
||||
self.node_snap_cache.is_some_and(|cache| !cache.manipulators.is_empty())
|
||||
}
|
||||
}
|
||||
impl SnapManager {
|
||||
@@ -196,8 +250,8 @@ impl SnapManager {
|
||||
let mut snapped_points = Vec::new();
|
||||
let document = snap_data.document;
|
||||
|
||||
if let Some(closest_point) = get_closest_point(&snap_results.points) {
|
||||
snapped_points.push(closest_point.clone());
|
||||
if let Some(closest_point) = get_closest_point(snap_results.points) {
|
||||
snapped_points.push(closest_point);
|
||||
}
|
||||
let exclude_paths = !document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Path));
|
||||
if let Some(closest_curve) = get_closest_curve(&snap_results.curves, exclude_paths) {
|
||||
@@ -247,51 +301,56 @@ impl SnapManager {
|
||||
best_point.unwrap_or(SnappedPoint::infinite_snap(point.document_point))
|
||||
}
|
||||
|
||||
fn find_candidates(snap_data: &SnapData, point: &SnapCandidatePoint, bbox: Option<Quad>) -> Vec<LayerNodeIdentifier> {
|
||||
fn add_candidates(&mut self, layer: LayerNodeIdentifier, snap_data: &SnapData, quad: Quad) {
|
||||
let document = snap_data.document;
|
||||
let offset = snap_tolerance(document);
|
||||
let quad = bbox.map_or_else(|| Quad::from_box([point.document_point - offset, point.document_point + offset]), |quad| quad.inflate(offset));
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
fn add_candidates(layer: LayerNodeIdentifier, snap_data: &SnapData, quad: Quad, candidates: &mut Vec<LayerNodeIdentifier>) {
|
||||
let document = snap_data.document;
|
||||
if candidates.len() > 10 {
|
||||
return;
|
||||
if !document.network_interface.is_visible(&layer.to_node(), &[]) {
|
||||
return;
|
||||
}
|
||||
if snap_data.ignore.contains(&layer) {
|
||||
return;
|
||||
}
|
||||
if layer.has_children(document.metadata()) {
|
||||
for layer in layer.children(document.metadata()) {
|
||||
self.add_candidates(layer, snap_data, quad);
|
||||
}
|
||||
if !document.network_interface.selected_nodes(&[]).unwrap().layer_visible(layer, &document.network_interface) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
let Some(bounds) = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
|
||||
return;
|
||||
};
|
||||
let layer_bounds = document.metadata().transform_to_document(layer) * Quad::from_box(bounds);
|
||||
let screen_bounds = document.metadata().document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, snap_data.input.viewport_bounds.size()]);
|
||||
if screen_bounds.intersects(layer_bounds) {
|
||||
if !self.alignment_candidates.as_ref().is_some_and(|candidates| candidates.len() > 100) {
|
||||
self.alignment_candidates.get_or_insert_with(Vec::new).push(layer);
|
||||
}
|
||||
if snap_data.ignore.contains(&layer) {
|
||||
return;
|
||||
}
|
||||
if layer.has_children(document.metadata()) {
|
||||
for layer in layer.children(document.metadata()) {
|
||||
add_candidates(layer, snap_data, quad, candidates);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(bounds) = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
|
||||
return;
|
||||
};
|
||||
let layer_bounds = document.metadata().transform_to_document(layer) * Quad::from_box(bounds);
|
||||
let screen_bounds = document.metadata().document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, snap_data.input.viewport_bounds.size()]);
|
||||
if quad.intersects(layer_bounds) && screen_bounds.intersects(layer_bounds) {
|
||||
candidates.push(layer);
|
||||
if quad.intersects(layer_bounds) && !self.candidates.as_ref().is_some_and(|candidates| candidates.len() > 10) {
|
||||
self.candidates.get_or_insert_with(Vec::new).push(layer);
|
||||
}
|
||||
}
|
||||
|
||||
for layer in LayerNodeIdentifier::ROOT_PARENT.children(document.metadata()) {
|
||||
add_candidates(layer, snap_data, quad, &mut candidates);
|
||||
}
|
||||
|
||||
if candidates.len() > 10 {
|
||||
warn!("Snap candidate overflow");
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
pub fn free_snap(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, bbox: Option<Quad>, to_paths: bool) -> SnappedPoint {
|
||||
fn find_candidates(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, bbox: Option<Rect>) {
|
||||
let document = snap_data.document;
|
||||
let offset = snap_tolerance(document);
|
||||
let quad = bbox.map_or_else(|| Quad::from_square(point.document_point, offset), |quad| Quad::from_box(quad.0).inflate(offset));
|
||||
|
||||
self.candidates = None;
|
||||
self.alignment_candidates = None;
|
||||
for layer in LayerNodeIdentifier::ROOT_PARENT.children(document.metadata()) {
|
||||
self.add_candidates(layer, snap_data, quad);
|
||||
}
|
||||
|
||||
if self.alignment_candidates.as_ref().is_some_and(|candidates| candidates.len() > 100) {
|
||||
warn!("Alignment candidate overflow");
|
||||
}
|
||||
if self.candidates.as_ref().is_some_and(|candidates| candidates.len() > 10) {
|
||||
warn!("Snap candidate overflow");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn free_snap(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, bbox: Option<Rect>, to_paths: bool) -> SnappedPoint {
|
||||
if !point.document_point.is_finite() {
|
||||
warn!("Snapping non-finite position");
|
||||
return SnappedPoint::infinite_snap(DVec2::ZERO);
|
||||
@@ -303,14 +362,21 @@ impl SnapManager {
|
||||
}
|
||||
|
||||
let mut snap_data = snap_data.clone();
|
||||
snap_data.candidates = Some(&*self.candidates.get_or_insert_with(|| Self::find_candidates(&snap_data, point, bbox)));
|
||||
if snap_data.candidates.is_none() {
|
||||
self.find_candidates(&snap_data, point, bbox);
|
||||
}
|
||||
snap_data.candidates = self.candidates.as_ref();
|
||||
snap_data.alignment_candidates = self.alignment_candidates.as_ref();
|
||||
|
||||
self.layer_snapper.free_snap(&mut snap_data, point, &mut snap_results);
|
||||
self.grid_snapper.free_snap(&mut snap_data, point, &mut snap_results);
|
||||
self.alignment_snapper.free_snap(&mut snap_data, point, &mut snap_results);
|
||||
self.distribution_snapper.free_snap(&mut snap_data, point, &mut snap_results, bbox);
|
||||
|
||||
Self::find_best_snap(&mut snap_data, point, snap_results, false, false, to_paths)
|
||||
}
|
||||
|
||||
pub fn constrained_snap(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, constraint: SnapConstraint, bbox: Option<Quad>) -> SnappedPoint {
|
||||
pub fn constrained_snap(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, constraint: SnapConstraint, bbox: Option<Rect>) -> SnappedPoint {
|
||||
if !point.document_point.is_finite() {
|
||||
warn!("Snapping non-finite position");
|
||||
return SnappedPoint::infinite_snap(DVec2::ZERO);
|
||||
@@ -322,13 +388,54 @@ impl SnapManager {
|
||||
}
|
||||
|
||||
let mut snap_data = snap_data.clone();
|
||||
snap_data.candidates = Some(&*self.candidates.get_or_insert_with(|| Self::find_candidates(&snap_data, point, bbox)));
|
||||
if snap_data.candidates.is_none() {
|
||||
self.find_candidates(&snap_data, point, bbox);
|
||||
}
|
||||
snap_data.candidates = self.candidates.as_ref();
|
||||
snap_data.alignment_candidates = self.alignment_candidates.as_ref();
|
||||
|
||||
self.layer_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint);
|
||||
self.grid_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint);
|
||||
self.alignment_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint);
|
||||
self.distribution_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint, bbox);
|
||||
|
||||
Self::find_best_snap(&mut snap_data, point, snap_results, true, false, false)
|
||||
}
|
||||
|
||||
fn alignment_x_overlay(boxes: &VecDeque<Rect>, transform: DAffine2, overlay_context: &mut OverlayContext) {
|
||||
let y_size = transform.inverse().transform_vector2(DVec2::Y * 8.).length();
|
||||
for (&first, &second) in boxes.iter().zip(boxes.iter().skip(1)) {
|
||||
let bottom = first.center().y < second.center().y + y_size;
|
||||
let y = if bottom { first.max() } else { first.min() }.y;
|
||||
let start = DVec2::new(first.max().x, y);
|
||||
let end = DVec2::new(second.min().x, y);
|
||||
let signed_size = if bottom { y_size } else { -y_size };
|
||||
overlay_context.line(transform.transform_point2(start), transform.transform_point2(start + DVec2::Y * signed_size));
|
||||
overlay_context.line(transform.transform_point2(end), transform.transform_point2(end + DVec2::Y * signed_size));
|
||||
overlay_context.line(
|
||||
transform.transform_point2(start + DVec2::Y * signed_size / 2.),
|
||||
transform.transform_point2(end + DVec2::Y * signed_size / 2.),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn alignment_y_overlay(boxes: &VecDeque<Rect>, transform: DAffine2, overlay_context: &mut OverlayContext) {
|
||||
let x_size = transform.inverse().transform_vector2(DVec2::X * 8.).length();
|
||||
for (&first, &second) in boxes.iter().zip(boxes.iter().skip(1)) {
|
||||
let right = first.center().x < second.center().x + x_size;
|
||||
let x = if right { first.max() } else { first.min() }.x;
|
||||
let start = DVec2::new(x, first.max().y);
|
||||
let end = DVec2::new(x, second.min().y);
|
||||
let signed_size = if right { x_size } else { -x_size };
|
||||
overlay_context.line(transform.transform_point2(start), transform.transform_point2(start + DVec2::X * signed_size));
|
||||
overlay_context.line(transform.transform_point2(end), transform.transform_point2(end + DVec2::X * signed_size));
|
||||
overlay_context.line(
|
||||
transform.transform_point2(start + DVec2::X * signed_size / 2.),
|
||||
transform.transform_point2(end + DVec2::X * signed_size / 2.),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_overlays(&mut self, snap_data: SnapData, overlay_context: &mut OverlayContext) {
|
||||
let to_viewport = snap_data.document.metadata().document_to_viewport;
|
||||
if let Some(ind) = &self.indicator {
|
||||
@@ -341,8 +448,25 @@ impl SnapManager {
|
||||
}
|
||||
let viewport = to_viewport.transform_point2(ind.snapped_point_document);
|
||||
|
||||
overlay_context.text(&format!("{:?} to {:?}", ind.source, ind.target), viewport - DVec2::new(0., 5.), "rgba(0, 0, 0, 0.8)", 3.);
|
||||
overlay_context.square(viewport, Some(4.), Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE));
|
||||
Self::alignment_x_overlay(&ind.distribution_boxes_x, to_viewport, overlay_context);
|
||||
Self::alignment_y_overlay(&ind.distribution_boxes_y, to_viewport, overlay_context);
|
||||
|
||||
let align = [ind.alignment_target_x, ind.alignment_target_y].map(|target| target.map(|target| to_viewport.transform_point2(target)));
|
||||
let any_align = align.iter().flatten().next().is_some();
|
||||
for &target in align.iter().flatten() {
|
||||
overlay_context.line(viewport, target);
|
||||
}
|
||||
for &target in align.iter().flatten() {
|
||||
overlay_context.manipulator_handle(target, false);
|
||||
}
|
||||
if any_align {
|
||||
overlay_context.manipulator_handle(viewport, false);
|
||||
}
|
||||
|
||||
if !any_align && ind.distribution_equal_distance_x.is_none() && ind.distribution_equal_distance_y.is_none() {
|
||||
overlay_context.text(&format!("{:?} to {:?}", ind.source, ind.target), viewport - DVec2::new(0., 5.), "rgba(0, 0, 0, 0.8)", 3.);
|
||||
overlay_context.square(viewport, Some(4.), Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user