Layer and grid snapping systems (#1521)

* Grid overlays

* Rectangle tool basic snapping

* Fix bezier demos

* Fix bézier crate tests

* Constrained snapping for circle & shape tool

* Line tool snapping

* Pen tool snapping

* Path tool snapping

* Snapping whilst dragging layers (not constrained)

* Constrained drag

* Resize snapping

* Normal and tangent

* Cleanup

* Grid snapping

* Grid snapping

* Fix imports

* Fix bug in artboard tool

* Fix hang on 0 size grid spacing

* Fix NaN when scaling

* Polishing

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube
2024-01-13 14:32:10 +00:00
committed by GitHub
co-authored by Keavon Chambers
parent 78a1bb17cd
commit 456ca170a4
40 changed files with 2170 additions and 475 deletions
@@ -3,27 +3,24 @@ use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapManager;
use glam::{DAffine2, DVec2, Vec2Swizzles};
use super::snapping::{SnapCandidatePoint, SnapConstraint, SnapData};
#[derive(Clone, Debug, Default)]
pub struct Resize {
drag_start: ViewportPosition,
pub layer: Option<LayerNodeIdentifier>,
snap_manager: SnapManager,
pub snap_manager: SnapManager,
}
impl Resize {
/// Starts a resize, assigning the snap targets and snapping the starting position.
pub fn start(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
self.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
pub fn start(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
let root_transform = document.metadata().document_to_viewport;
self.drag_start = root_transform.inverse().transform_point2(self.snap_manager.snap_position(responses, document, input.mouse.position));
}
/// Recalculates snap targets without snapping the starting position.
pub fn recalculate_snaps(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
self.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
let point = SnapCandidatePoint::handle(root_transform.inverse().transform_point2(input.mouse.position));
let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
self.drag_start = snapped.snapped_point_document;
}
/// Calculate the drag start position in viewport space.
@@ -32,15 +29,7 @@ impl Resize {
root_transform.transform_point2(self.drag_start)
}
pub fn calculate_transform(
&mut self,
responses: &mut VecDeque<Message>,
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
center: Key,
lock_ratio: Key,
skip_rerender: bool,
) -> Option<Message> {
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
let Some(layer) = self.layer else {
return None;
};
@@ -49,22 +38,54 @@ impl Resize {
return None;
}
let mut start = self.viewport_drag_start(document);
let stop = self.snap_manager.snap_position(responses, document, ipp.mouse.position);
let mut size = stop - start;
if ipp.keyboard.get(lock_ratio as usize) {
size = size.abs().max(size.abs().yx()) * size.signum();
}
if ipp.keyboard.get(center as usize) {
start -= size;
size *= 2.;
let start = self.viewport_drag_start(document);
let mouse = input.mouse.position;
let to_viewport = document.metadata().document_to_viewport;
let document_mouse = to_viewport.inverse().transform_point2(mouse);
let mut points_viewport = [start, mouse];
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
let ratio = input.keyboard.get(lock_ratio as usize);
let centre = input.keyboard.get(center as usize);
let snap_data = SnapData::ignore(document, input, &ignore);
if ratio {
let size = points_viewport[1] - points_viewport[0];
let size = size.abs().max(size.abs().yx()) * size.signum();
points_viewport[1] = points_viewport[0] + size;
let end_document = to_viewport.inverse().transform_point2(points_viewport[1]);
let constraint = SnapConstraint::Line {
origin: self.drag_start,
direction: end_document - self.drag_start,
};
if centre {
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, None);
let far = SnapCandidatePoint::handle(2. * self.drag_start - end_document);
let snapped_far = self.snap_manager.constrained_snap(&snap_data, &far, constraint, None);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, None);
points_viewport[1] = to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
} else if centre {
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), None, false);
let snapped_far = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(2. * self.drag_start - document_mouse), None, false);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), None, false);
points_viewport[1] = to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
Some(
GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(size, 0., start),
transform: DAffine2::from_scale_angle_translation(points_viewport[1] - points_viewport[0], 0., points_viewport[0]),
transform_in: TransformIn::Viewport,
skip_rerender,
}
@@ -1,7 +1,9 @@
use super::graph_modification_utils;
use super::snapping::{group_smooth, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
use crate::consts::DRAG_THRESHOLD;
use crate::messages::portfolio::document::node_graph::VectorDataModification;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_from_id, get_manipulator_groups, get_mirror_handles, get_subpaths};
@@ -64,6 +66,52 @@ pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<Manipulato
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
impl ShapeState {
// Snap, returning a viewport delta
pub fn snap(&self, snap_manager: &mut SnapManager, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, previous_mouse: DVec2) -> DVec2 {
let mut snap_data = SnapData::new(document, input);
for (layer, state) in &self.selected_shape_state {
for point in &state.selected_points {
snap_data.manipulators.push((*layer, point.group));
}
}
let mouse_delta = document.metadata.document_to_viewport.inverse().transform_vector2(input.mouse.position - previous_mouse);
let mut offset = mouse_delta;
let mut best_snapped = SnappedPoint::infinite_snap(document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position));
for (layer, state) in &self.selected_shape_state {
let Some(subpaths) = get_subpaths(*layer, &document.network) else { continue };
let to_document = document.metadata.transform_to_document(*layer);
for subpath in subpaths {
for (index, group) in subpath.manipulator_groups().iter().enumerate() {
for handle in [SelectedType::Anchor, SelectedType::InHandle, SelectedType::OutHandle] {
if !state.is_selected(ManipulatorPointId::new(group.id, handle)) {
continue;
}
let source = if handle.is_handle() {
SnapSource::Geometry(GeometrySnapSource::Handle)
} else if group_smooth(group, to_document, subpath, index) {
SnapSource::Geometry(GeometrySnapSource::Smooth)
} else {
SnapSource::Geometry(GeometrySnapSource::Sharp)
};
let Some(position) = handle.get_position(&group) else { continue };
let point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source);
let snapped = snap_manager.free_snap(&snap_data, &point, None, false);
if best_snapped.other_snap_better(&snapped) {
offset = snapped.snapped_point_document - point.document_point + mouse_delta;
best_snapped = snapped;
}
}
}
}
}
snap_manager.update_indicator(best_snapped);
document.metadata.document_to_viewport.transform_vector2(offset)
}
/// Select the first point within the selection threshold.
/// Returns a tuple of the points if found and the offset, or `None` otherwise.
pub fn select_point(
@@ -1,126 +1,347 @@
use crate::consts::{SNAP_AXIS_TOLERANCE, SNAP_POINT_TOLERANCE};
mod grid_snapper;
mod layer_snapper;
mod snap_results;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget};
use crate::messages::prelude::*;
use glam::DVec2;
use bezier_rs::{Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
use std::cmp::Ordering;
pub use {grid_snapper::*, layer_snapper::*, snap_results::*};
/// Handles snapping and snap overlays
#[derive(Debug, Clone, Default)]
pub struct SnapManager {
point_targets: Option<Vec<DVec2>>,
bound_targets: Option<Vec<DVec2>>,
snap_x: bool,
snap_y: bool,
indicator: Option<SnappedPoint>,
layer_snapper: LayerSnapper,
grid_snapper: GridSnapper,
candidates: Option<Vec<LayerNodeIdentifier>>,
}
impl SnapManager {
/// Computes the necessary translation to the layer to snap it (as well as updating necessary overlays)
fn calculate_snap<R>(&mut self, targets: R, responses: &mut VecDeque<Message>) -> DVec2
where
R: Iterator<Item = DVec2> + Clone,
{
let empty = Vec::new();
let snap_points = self.snap_x && self.snap_y;
let axis = self.bound_targets.as_ref().unwrap_or(&empty);
let points = if snap_points { self.point_targets.as_ref().unwrap_or(&empty) } else { &empty };
let x_axis = if self.snap_x { axis } else { &empty }
.iter()
.flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).x)));
let y_axis = if self.snap_y { axis } else { &empty }
.iter()
.flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).y)));
let points = points.iter().flat_map(|&pos| targets.clone().map(move |goal| (pos, pos - goal, (pos - goal).length())));
let min_x = x_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
let min_y = y_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
let min_points = points.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
// Snap to a point if possible
let (clamped_closest_distance, _snapped_to_point) = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
(min_points.1, true)
} else {
// Do not move if over snap tolerance
let closest_distance = DVec2::new(min_x.unwrap_or_default().2, min_y.unwrap_or_default().2);
(
DVec2::new(
if closest_distance.x.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.x },
if closest_distance.y.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.y },
),
false,
)
};
responses.add(OverlaysMessage::Draw);
clamped_closest_distance
}
/// Gets a list of snap targets for the X and Y axes (if specified) in Viewport coords for the target layers (usually all layers or all non-selected layers.)
/// This should be called at the start of a drag.
pub fn start_snap(
&mut self,
document_message_handler: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
bounding_boxes: impl Iterator<Item = [DVec2; 2]>,
snap_x: bool,
snap_y: bool,
) {
let snapping_enabled = document_message_handler.snapping_state.snapping_enabled;
let bounding_box_snapping = document_message_handler.snapping_state.bounding_box_snapping;
if snapping_enabled && bounding_box_snapping {
self.snap_x = snap_x;
self.snap_y = snap_y;
// Could be made into sorted Vec or a HashSet for more performant lookups.
self.bound_targets = Some(
bounding_boxes
.flat_map(expand_bounds)
.filter(|&pos| pos.x >= 0. && pos.y >= 0. && pos.x < input.viewport_bounds.size().x && pos.y <= input.viewport_bounds.size().y)
.collect(),
);
self.point_targets = None;
#[derive(Clone, Copy, Debug, Default)]
pub enum SnapConstraint {
#[default]
None,
Line {
origin: DVec2,
direction: DVec2,
},
Direction(DVec2),
Circle {
centre: DVec2,
radius: f64,
},
}
impl SnapConstraint {
pub fn projection(&self, point: DVec2) -> DVec2 {
match *self {
Self::Line { origin, direction } if direction != DVec2::ZERO => (point - origin).project_onto(direction) + origin,
Self::Circle { centre, radius } => {
let from_centre = point - centre;
let distance = from_centre.length();
if distance > 0. {
centre + radius * from_centre / distance
} else {
// Point is exactly at the centre, so project right
centre + DVec2::new(radius, 0.)
}
}
_ => point,
}
}
pub fn direction(&self) -> DVec2 {
match *self {
Self::Line { direction, .. } | Self::Direction(direction) => direction,
_ => DVec2::ZERO,
}
}
}
pub fn snap_tolerance(document: &DocumentMessageHandler) -> f64 {
document.snapping_state.tolerance / document.navigation.zoom
}
/// Add arbitrary snapping points
///
/// This should be called after start_snap
pub fn add_snap_points(&mut self, document_message_handler: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, snap_points: impl Iterator<Item = DVec2>) {
let snapping_enabled = document_message_handler.snapping_state.snapping_enabled;
let node_snapping = document_message_handler.snapping_state.node_snapping;
if snapping_enabled && node_snapping {
let snap_points = snap_points.filter(|&pos| pos.x >= 0. && pos.y >= 0. && pos.x < input.viewport_bounds.size().x && pos.y <= input.viewport_bounds.size().y);
if let Some(targets) = &mut self.point_targets {
targets.extend(snap_points);
} else {
self.point_targets = Some(snap_points.collect());
fn compare_points(a: &&SnappedPoint, b: &&SnappedPoint) -> Ordering {
if (a.target.bounding_box() && !b.target.bounding_box()) || (a.at_intersection && !b.at_intersection) {
Ordering::Greater
} else if (!a.target.bounding_box() && b.target.bounding_box()) || (!a.at_intersection && b.at_intersection) {
Ordering::Less
} else {
a.distance.partial_cmp(&b.distance).unwrap()
}
}
fn get_closest_point(points: &[SnappedPoint]) -> Option<&SnappedPoint> {
points.iter().min_by(compare_points)
}
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);
curves.iter().filter(keep_curve).map(|curve| &curve.point).min_by(compare_points)
}
fn get_closest_line(lines: &[SnappedLine]) -> Option<&SnappedPoint> {
lines.iter().map(|curve| &curve.point).min_by(compare_points)
}
fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<SnappedPoint> {
let mut best = None;
for curve_i in curves {
if curve_i.point.target == SnapTarget::BoundingBox(BoundingBoxSnapTarget::Edge) {
continue;
}
for curve_j in curves {
if curve_j.point.target == SnapTarget::BoundingBox(BoundingBoxSnapTarget::Edge) {
continue;
}
if curve_i.start == curve_j.start && curve_i.layer == curve_j.layer {
continue;
}
for curve_i_t in curve_i.document_curve.intersections(&curve_j.document_curve, None, None) {
let snapped_point_document = curve_i.document_curve.evaluate(TValue::Parametric(curve_i_t));
let distance = snap_to.distance(snapped_point_document);
let i_closer = curve_i.point.distance < curve_j.point.distance;
let close = if i_closer { curve_i } else { curve_j };
let far = if i_closer { curve_j } else { curve_i };
if !best.as_ref().is_some_and(|best: &SnappedPoint| best.distance < distance) {
best = Some(SnappedPoint {
snapped_point_document,
distance,
target: SnapTarget::Geometry(GeometrySnapTarget::Intersection),
tolerance: close.point.tolerance,
curves: [Some(close.document_curve), Some(far.document_curve)],
source: close.point.source,
at_intersection: true,
contrained: true,
..Default::default()
})
}
}
}
}
/// Finds the closest snap from an array of layers to the specified snap targets in viewport coords.
/// Returns 0 for each axis that there is no snap less than the snap tolerance.
pub fn snap_layers(&mut self, responses: &mut VecDeque<Message>, document_message_handler: &DocumentMessageHandler, snap_anchors: Vec<DVec2>, mouse_delta: DVec2) -> DVec2 {
if document_message_handler.snapping_state.snapping_enabled {
self.calculate_snap(snap_anchors.iter().map(move |&snap| mouse_delta + snap), responses)
} else {
DVec2::ZERO
best
}
fn get_grid_intersection(snap_to: DVec2, lines: &[SnappedLine]) -> Option<SnappedPoint> {
let mut best = None;
for line_i in lines {
for line_j in lines {
if let Some(snapped_point_document) = Quad::intersect_rays(line_i.point.snapped_point_document, line_i.direction, line_j.point.snapped_point_document, line_j.direction) {
let distance = snap_to.distance(snapped_point_document);
if !best.as_ref().is_some_and(|best: &SnappedPoint| best.distance < distance) {
best = Some(SnappedPoint {
snapped_point_document,
distance,
target: SnapTarget::Grid(GridSnapTarget::Intersection),
tolerance: line_i.point.tolerance,
source: line_i.point.source,
at_intersection: true,
contrained: true,
..Default::default()
})
}
}
}
}
best
}
#[derive(Clone)]
pub struct SnapData<'a> {
pub document: &'a DocumentMessageHandler,
pub input: &'a InputPreprocessorMessageHandler,
pub ignore: &'a [LayerNodeIdentifier],
pub manipulators: Vec<(LayerNodeIdentifier, ManipulatorGroupId)>,
pub candidates: Option<&'a Vec<LayerNodeIdentifier>>,
}
impl<'a> SnapData<'a> {
pub fn new(document: &'a DocumentMessageHandler, input: &'a InputPreprocessorMessageHandler) -> Self {
Self::ignore(document, input, &[])
}
pub fn ignore(document: &'a DocumentMessageHandler, input: &'a InputPreprocessorMessageHandler, ignore: &'a [LayerNodeIdentifier]) -> Self {
Self {
document,
input,
ignore,
candidates: None,
manipulators: Vec::new(),
}
}
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)
}
fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: ManipulatorGroupId) -> bool {
self.manipulators.contains(&(layer, manipulator))
}
}
impl SnapManager {
pub fn update_indicator(&mut self, snapped_point: SnappedPoint) {
self.indicator = snapped_point.is_snapped().then_some(snapped_point);
}
pub fn clear_indicator(&mut self) {
self.indicator = None;
}
pub fn preview_draw(&mut self, snap_data: &SnapData, mouse: DVec2) {
let point = SnapCandidatePoint::handle(snap_data.document.metadata.document_to_viewport.inverse().transform_point2(mouse));
let snapped = self.free_snap(snap_data, &point, None, false);
self.update_indicator(snapped);
}
/// Handles snapping of a viewport position, returning another viewport position.
pub fn snap_position(&mut self, responses: &mut VecDeque<Message>, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 {
if document_message_handler.snapping_state.snapping_enabled {
self.calculate_snap([position_viewport].into_iter(), responses) + position_viewport
} else {
position_viewport
fn find_best_snap(snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: SnapResults, contrained: bool, off_screen: bool, to_path: bool) -> SnappedPoint {
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());
}
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) {
snapped_points.push(closest_curve.clone());
}
if document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Line)) {
if let Some(closest_line) = get_closest_line(&snap_results.grid_lines) {
snapped_points.push(closest_line.clone());
}
}
if !contrained {
if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Intersection)) {
if let Some(closest_curves_intersection) = get_closest_intersection(point.document_point, &snap_results.curves) {
snapped_points.push(closest_curves_intersection);
}
}
if document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Intersection)) {
if let Some(closest_grid_intersection) = get_grid_intersection(point.document_point, &snap_results.grid_lines) {
snapped_points.push(closest_grid_intersection);
}
}
}
if to_path {
snapped_points.retain(|i| matches!(i.target, SnapTarget::Geometry(_)));
}
let mut best_point = None;
for point in snapped_points {
let viewport_point = document.metadata.document_to_viewport.transform_point2(point.snapped_point_document);
let on_screen = viewport_point.cmpgt(DVec2::ZERO).all() && viewport_point.cmplt(snap_data.input.viewport_bounds.size()).all();
if !on_screen && !off_screen {
continue;
}
if point.distance > point.tolerance {
continue;
}
if best_point.as_ref().is_some_and(|best: &SnappedPoint| point.other_snap_better(best)) {
continue;
}
best_point = Some(point);
}
best_point.unwrap_or(SnappedPoint::infinite_snap(point.document_point))
}
fn find_candidates(snap_data: &SnapData, point: &SnapCandidatePoint, bbox: Option<Quad>) -> Vec<LayerNodeIdentifier> {
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.selected_nodes.layer_visible(layer, &document.network, &document.metadata) {
return;
}
if snap_data.ignore.contains(&layer) {
return;
}
if document.metadata.is_folder(layer) {
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);
}
}
add_candidates(LayerNodeIdentifier::ROOT, 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 {
if !point.document_point.is_finite() {
warn!("Snapping non-finite position");
return SnappedPoint::infinite_snap(DVec2::ZERO);
}
let mut snap_results = SnapResults::default();
if point.source_index == 0 {
self.candidates = None;
}
let mut snap_data = snap_data.clone();
snap_data.candidates = Some(&*self.candidates.get_or_insert_with(|| Self::find_candidates(&snap_data, point, bbox)));
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::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 {
if !point.document_point.is_finite() {
warn!("Snapping non-finite position");
return SnappedPoint::infinite_snap(DVec2::ZERO);
}
let mut snap_results = SnapResults::default();
if point.source_index == 0 {
self.candidates = None;
}
let mut snap_data = snap_data.clone();
snap_data.candidates = Some(&*self.candidates.get_or_insert_with(|| Self::find_candidates(&snap_data, point, bbox)));
self.layer_snapper.contrained_snap(&mut snap_data, point, &mut snap_results, constraint);
self.grid_snapper.contrained_snap(&mut snap_data, point, &mut snap_results, constraint);
Self::find_best_snap(&mut snap_data, point, snap_results, true, false, false)
}
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 {
for curve in &ind.curves {
let Some(curve) = curve else { continue };
overlay_context.outline([Subpath::from_bezier(curve)].iter(), to_viewport);
}
if let Some(quad) = ind.target_bounds {
overlay_context.quad(to_viewport * quad);
}
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, true);
}
}
/// Removes snap target data and overlays. Call this when snapping is done.
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
self.bound_targets = None;
self.point_targets = None;
self.candidates = None;
self.indicator = None;
responses.add(OverlaysMessage::Draw);
}
}
@@ -0,0 +1,186 @@
use super::*;
use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, GridSnapping, GridType, SnapTarget};
use bezier_rs::Bezier;
use glam::DVec2;
use graphene_core::renderer::Quad;
struct Line {
pub point: DVec2,
pub direction: DVec2,
}
#[derive(Clone, Debug, Default)]
pub struct GridSnapper;
impl GridSnapper {
// Rectangular grid has 4 lines around a point, 2 on y axis and 2 on x axis.
fn get_snap_lines_rectangular(&self, document_point: DVec2, snap_data: &mut SnapData, spacing: DVec2) -> Vec<Line> {
let document = snap_data.document;
let mut lines = Vec::new();
let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else {
return lines;
};
let origin = document.snapping_state.grid.origin;
for (direction, perpendicular) in [(DVec2::X, DVec2::Y), (DVec2::Y, DVec2::X)] {
lines.push(Line {
direction,
point: perpendicular * (((document_point - origin) / spacing).ceil() * spacing + origin),
});
lines.push(Line {
direction,
point: perpendicular * (((document_point - origin) / spacing).floor() * spacing + origin),
});
}
lines
}
// Isometric grid has 6 lines around a point, 2 y axis, 2 on the angle a, and 2 on the angle b.
fn get_snap_lines_isometric(&self, document_point: DVec2, snap_data: &mut SnapData, y_axis_spacing: f64, angle_a: f64, angle_b: f64) -> Vec<Line> {
let document = snap_data.document;
let mut lines = Vec::new();
let origin = document.snapping_state.grid.origin;
let tan_a = angle_a.to_radians().tan();
let tan_b = angle_b.to_radians().tan();
let spacing = DVec2::new(y_axis_spacing / (tan_a + tan_b), y_axis_spacing);
let Some(spacing_multiplier) = GridSnapping::compute_isometric_multiplier(y_axis_spacing, tan_a + tan_b, &document.navigation) else {
return lines;
};
let spacing = spacing * spacing_multiplier;
let x_max = ((document_point.x - origin.x) / spacing.x).ceil() * spacing.x + origin.x;
let x_min = ((document_point.x - origin.x) / spacing.x).floor() * spacing.x + origin.x;
lines.push(Line {
point: DVec2::new(x_max, 0.),
direction: DVec2::Y,
});
lines.push(Line {
point: DVec2::new(x_min, 0.),
direction: DVec2::Y,
});
let y_projected_onto_x = document_point.y + tan_a * (document_point.x - origin.x);
let y_onto_x_max = ((y_projected_onto_x - origin.y) / spacing.y).ceil() * spacing.y + origin.y;
let y_onto_x_min = ((y_projected_onto_x - origin.y) / spacing.y).floor() * spacing.y + origin.y;
lines.push(Line {
point: DVec2::new(origin.x, y_onto_x_max),
direction: DVec2::new(1., -tan_a),
});
lines.push(Line {
point: DVec2::new(origin.x, y_onto_x_min),
direction: DVec2::new(1., -tan_a),
});
let y_projected_onto_z = document_point.y - tan_b * (document_point.x - origin.x);
let y_onto_z_max = ((y_projected_onto_z - origin.y) / spacing.y).ceil() * spacing.y + origin.y;
let y_onto_z_min = ((y_projected_onto_z - origin.y) / spacing.y).floor() * spacing.y + origin.y;
lines.push(Line {
point: DVec2::new(origin.x, y_onto_z_max),
direction: DVec2::new(1., tan_b),
});
lines.push(Line {
point: DVec2::new(origin.x, y_onto_z_min),
direction: DVec2::new(1., tan_b),
});
lines
}
fn get_snap_lines(&self, document_point: DVec2, snap_data: &mut SnapData) -> Vec<Line> {
match snap_data.document.snapping_state.grid.grid_type {
GridType::Rectangle { spacing } => self.get_snap_lines_rectangular(document_point, snap_data, spacing),
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => self.get_snap_lines_isometric(document_point, snap_data, y_axis_spacing, angle_a, angle_b),
}
}
pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults) {
let lines = self.get_snap_lines(point.document_point, snap_data);
let tolerance = snap_tolerance(snap_data.document);
for line in lines {
let projected = (point.document_point - line.point).project_onto(line.direction) + line.point;
let distance = point.document_point.distance(projected);
if !distance.is_finite() {
continue;
}
if distance > tolerance {
continue;
}
if snap_data.document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Line))
|| snap_data.document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Intersection))
{
snap_results.grid_lines.push(SnappedLine {
direction: line.direction,
point: SnappedPoint {
snapped_point_document: projected,
source: point.source,
target: SnapTarget::Grid(GridSnapTarget::Line),
source_bounds: point.quad,
distance,
tolerance,
..Default::default()
},
});
}
let normal_target = SnapTarget::Grid(GridSnapTarget::LineNormal);
if snap_data.document.snapping_state.target_enabled(normal_target) {
for &neighbor in &point.neighbors {
let projected = (neighbor - line.point).project_onto(line.direction) + line.point;
let distance = point.document_point.distance(projected);
if distance > tolerance {
continue;
}
snap_results.points.push(SnappedPoint {
snapped_point_document: projected,
source: point.source,
source_bounds: point.quad,
target: normal_target,
distance,
tolerance,
..Default::default()
})
}
}
}
}
pub fn contrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint) {
let tolerance = snap_tolerance(snap_data.document);
let projected = constraint.projection(point.document_point);
let lines = self.get_snap_lines(projected, snap_data);
let (constraint_start, constraint_direction) = match constraint {
SnapConstraint::Line { origin, direction } => (origin, direction.normalize_or_zero()),
SnapConstraint::Direction(direction) => (projected, direction.normalize_or_zero()),
_ => unimplemented!(),
};
for line in lines {
let Some(intersection) = Quad::intersect_rays(line.point, line.direction, constraint_start, constraint_direction) else {
continue;
};
let distance = intersection.distance(point.document_point);
if distance < tolerance && snap_data.document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Line)) {
snap_results.points.push(SnappedPoint {
snapped_point_document: intersection,
source: point.source,
target: SnapTarget::Grid(GridSnapTarget::Line),
at_intersection: false,
contrained: true,
source_bounds: point.quad,
curves: [
Some(Bezier::from_linear_dvec2(projected - constraint_direction * tolerance, projected + constraint_direction * tolerance)),
None,
],
distance,
tolerance,
..Default::default()
})
}
}
}
}
@@ -0,0 +1,441 @@
use super::*;
use crate::consts::HIDE_HANDLE_DISTANCE;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{
BoardSnapSource, BoardSnapTarget, BoundingBoxSnapSource, BoundingBoxSnapTarget, GeometrySnapSource, GeometrySnapTarget, SnapSource, SnapTarget,
};
use crate::messages::prelude::*;
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
#[derive(Clone, Debug, Default)]
pub struct LayerSnapper {
points_to_snap: Vec<SnapCandidatePoint>,
paths_to_snap: Vec<SnapCandidatePath>,
}
impl LayerSnapper {
pub fn add_layer_bounds(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, target: SnapTarget) {
if !document.snapping_state.target_enabled(target) {
return;
}
let Some(bounds) = document.metadata.bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
return;
};
let bounds = document.metadata.transform_to_document(layer) * Quad::from_box(bounds);
if bounds.0.iter().any(|point| !point.is_finite()) {
return;
}
for document_curve in bounds.bezier_lines() {
self.paths_to_snap.push(SnapCandidatePath {
document_curve,
layer,
start: ManipulatorGroupId::new(),
target,
bounds: Some(bounds),
});
}
}
pub fn collect_paths(&mut self, snap_data: &mut SnapData, first_point: bool) {
if !first_point {
return;
}
let document = snap_data.document;
self.paths_to_snap.clear();
for layer in document.metadata.all_layers() {
if !document.metadata.is_artboard(layer) {
continue;
}
self.add_layer_bounds(document, layer, SnapTarget::Board(BoardSnapTarget::Edge));
}
for &layer in snap_data.get_candidates() {
let transform = document.metadata.transform_to_document(layer);
if !transform.is_finite() {
continue;
}
if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Intersection)) || document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Path))
{
for subpath in document.metadata.layer_outline(layer) {
for (start_index, curve) in subpath.iter().enumerate() {
let document_curve = curve.apply_transformation(|p| transform.transform_point2(p));
let start = subpath.manipulator_groups()[start_index].id;
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) {
continue;
}
self.paths_to_snap.push(SnapCandidatePath {
document_curve,
layer,
start,
target: SnapTarget::Geometry(GeometrySnapTarget::Path),
bounds: None,
});
}
}
}
if !snap_data.ignore_bounds(layer) {
self.add_layer_bounds(document, layer, SnapTarget::BoundingBox(BoundingBoxSnapTarget::Edge));
}
}
}
pub fn free_snap_paths(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults) {
self.collect_paths(snap_data, point.source_index == 0);
let document = snap_data.document;
let normals = document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Normal));
let tangents = document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Tangent));
let tolerance = snap_tolerance(document);
for path in &self.paths_to_snap {
let time = path.document_curve.project(point.document_point, None);
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
let distance = snapped_point_document.distance(point.document_point);
if distance < tolerance {
snap_results.curves.push(SnappedCurve {
layer: path.layer,
start: path.start,
document_curve: path.document_curve,
point: SnappedPoint {
snapped_point_document,
target: path.target,
distance,
tolerance,
curves: [path.bounds.is_none().then(|| path.document_curve), None],
source: point.source,
target_bounds: path.bounds,
..Default::default()
},
});
normals_and_tangents(path, normals, tangents, point, tolerance, snap_results);
}
}
}
pub fn snap_paths_constrained(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint) {
let document = snap_data.document;
self.collect_paths(snap_data, point.source_index == 0);
let tolerance = snap_tolerance(document);
let constraint_path = if let SnapConstraint::Circle { centre, radius } = constraint {
Subpath::new_ellipse(centre - DVec2::splat(radius), centre + DVec2::splat(radius))
} else {
let constrained_point = constraint.projection(point.document_point);
let direction = constraint.direction().normalize_or_zero();
let start = constrained_point - tolerance * direction;
let end = constrained_point + tolerance * direction;
Subpath::<ManipulatorGroupId>::new_line(start, end)
};
for path in &self.paths_to_snap {
for constraint_path in constraint_path.iter() {
for time in path.document_curve.intersections(&constraint_path, None, None) {
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
let distance = snapped_point_document.distance(point.document_point);
if distance < tolerance {
snap_results.points.push(SnappedPoint {
snapped_point_document,
target: path.target,
distance,
tolerance,
curves: [path.bounds.is_none().then(|| path.document_curve), Some(constraint_path)],
source: point.source,
target_bounds: path.bounds,
at_intersection: true,
..Default::default()
});
}
}
}
}
}
pub fn collect_anchors(&mut self, snap_data: &mut SnapData, first_point: bool) {
if !first_point {
return;
}
let document = snap_data.document;
self.points_to_snap.clear();
for layer in document.metadata.all_layers() {
if !document.metadata.is_artboard(layer) {
continue;
}
if document.snapping_state.target_enabled(SnapTarget::Board(BoardSnapTarget::Corner)) {
let Some(bounds) = document.metadata.bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
continue;
};
let quad = document.metadata.transform_to_document(layer) * Quad::from_box(bounds);
let values = BBoxSnapValues {
corner_source: SnapSource::Board(BoardSnapSource::Corner),
corner_target: SnapTarget::Board(BoardSnapTarget::Corner),
centre_source: SnapSource::Board(BoardSnapSource::Centre),
centre_target: SnapTarget::Board(BoardSnapTarget::Centre),
..Default::default()
};
get_bbox_points(quad, &mut self.points_to_snap, values, document);
}
}
for &layer in snap_data.get_candidates() {
get_layer_snap_points(layer, &snap_data, &mut self.points_to_snap);
if snap_data.ignore_bounds(layer) {
continue;
}
let Some(bounds) = document.metadata.bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
continue;
};
let quad = document.metadata.transform_to_document(layer) * Quad::from_box(bounds);
let values = BBoxSnapValues::BOUNDING_BOX;
get_bbox_points(quad, &mut self.points_to_snap, values, document);
}
}
pub fn snap_anchors(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, c: SnapConstraint, constrained_point: DVec2) {
self.collect_anchors(snap_data, point.source_index == 0);
//info!("Points to snap {:#?}", self.points_to_snap);
let mut best = None;
for candidate in &self.points_to_snap {
// Candidate is not on constraint
if !candidate.document_point.abs_diff_eq(c.projection(candidate.document_point), 1e-5) {
continue;
}
let distance = candidate.document_point.distance(constrained_point);
let tolerance = snap_tolerance(snap_data.document);
let candidate_better = |best: &SnappedPoint| {
if best.snapped_point_document.abs_diff_eq(candidate.document_point, 1e-5) {
!candidate.target.bounding_box()
} else {
distance < best.distance
}
};
if distance < tolerance && (best.is_none() || best.as_ref().is_some_and(|best| candidate_better(best))) {
best = Some(SnappedPoint {
snapped_point_document: candidate.document_point,
source: point.source,
target: candidate.target,
distance,
tolerance,
contrained: true,
target_bounds: candidate.quad,
..Default::default()
});
}
}
if let Some(result) = best {
snap_results.points.push(result);
}
}
pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults) {
self.snap_anchors(snap_data, point, snap_results, SnapConstraint::None, point.document_point);
self.free_snap_paths(snap_data, point, snap_results);
}
pub fn contrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint) {
self.snap_anchors(snap_data, point, snap_results, constraint, constraint.projection(point.document_point));
self.snap_paths_constrained(snap_data, point, snap_results, constraint);
}
}
fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, point: &SnapCandidatePoint, tolerance: f64, snap_results: &mut SnapResults) {
if normals && path.bounds.is_none() {
for &neighbour in &point.neighbors {
for t in path.document_curve.normals_to_point(neighbour) {
let normal_point = path.document_curve.evaluate(TValue::Parametric(t));
let distance = normal_point.distance(point.document_point);
if distance > tolerance {
continue;
}
snap_results.points.push(SnappedPoint {
snapped_point_document: normal_point,
target: SnapTarget::Geometry(GeometrySnapTarget::Normal),
distance,
tolerance,
curves: [Some(path.document_curve), None],
source: point.source,
contrained: true,
..Default::default()
});
}
}
}
if tangents && path.bounds.is_none() {
for &neighbour in &point.neighbors {
for t in path.document_curve.tangents_to_point(neighbour) {
let tangent_point = path.document_curve.evaluate(TValue::Parametric(t));
let distance = tangent_point.distance(point.document_point);
if distance > tolerance {
continue;
}
snap_results.points.push(SnappedPoint {
snapped_point_document: tangent_point,
target: SnapTarget::Geometry(GeometrySnapTarget::Tangent),
distance,
tolerance,
curves: [Some(path.document_curve), None],
source: point.source,
contrained: true,
..Default::default()
});
}
}
}
}
#[derive(Clone, Debug)]
struct SnapCandidatePath {
document_curve: Bezier,
layer: LayerNodeIdentifier,
start: ManipulatorGroupId,
target: SnapTarget,
bounds: Option<Quad>,
}
#[derive(Clone, Debug, Default)]
pub struct SnapCandidatePoint {
pub document_point: DVec2,
pub source: SnapSource,
pub target: SnapTarget,
pub source_index: usize,
pub quad: Option<Quad>,
pub neighbors: Vec<DVec2>,
}
impl SnapCandidatePoint {
pub fn new(document_point: DVec2, source: SnapSource, target: SnapTarget) -> Self {
Self::new_quad(document_point, source, target, None)
}
pub fn new_quad(document_point: DVec2, source: SnapSource, target: SnapTarget, quad: Option<Quad>) -> Self {
Self {
document_point,
source,
target,
quad: quad,
..Default::default()
}
}
pub fn new_source(document_point: DVec2, source: SnapSource) -> Self {
Self::new(document_point, source, SnapTarget::None)
}
pub fn handle(document_point: DVec2) -> Self {
Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::Sharp))
}
pub fn handle_neighbours(document_point: DVec2, neighbours: impl Into<Vec<DVec2>>) -> Self {
let mut point = Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::Sharp));
point.neighbors = neighbours.into();
point
}
}
#[derive(Default)]
struct BBoxSnapValues {
corner_source: SnapSource,
corner_target: SnapTarget,
edge_source: SnapSource,
edge_target: SnapTarget,
centre_source: SnapSource,
centre_target: SnapTarget,
}
impl BBoxSnapValues {
pub const BOUNDING_BOX: Self = Self {
corner_source: SnapSource::BoundingBox(BoundingBoxSnapSource::Corner),
corner_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::Corner),
edge_source: SnapSource::BoundingBox(BoundingBoxSnapSource::EdgeMidpoint),
edge_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::EdgeMidpoint),
centre_source: SnapSource::BoundingBox(BoundingBoxSnapSource::Centre),
centre_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::Centre),
};
}
fn get_bbox_points(quad: Quad, points: &mut Vec<SnapCandidatePoint>, values: BBoxSnapValues, document: &DocumentMessageHandler) {
for index in 0..4 {
let start = quad.0[index];
let end = quad.0[(index + 1) % 4];
if document.snapping_state.target_enabled(values.corner_target) {
points.push(SnapCandidatePoint::new_quad(start, values.corner_source, values.corner_target, Some(quad)));
}
if document.snapping_state.target_enabled(values.edge_target) {
points.push(SnapCandidatePoint::new_quad((start + end) / 2., values.edge_source, values.edge_target, Some(quad)));
}
}
if document.snapping_state.target_enabled(values.centre_target) {
points.push(SnapCandidatePoint::new_quad(quad.center(), values.centre_source, values.centre_target, Some(quad)));
}
}
fn handle_not_under(to_document: DAffine2) -> impl Fn(&DVec2) -> bool {
move |&offset: &DVec2| to_document.transform_vector2(offset).length_squared() >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE
}
fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<ManipulatorGroupId>, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) {
let document = snap_data.document;
// Midpoints of linear segments
if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::LineMidpoint)) {
for (index, curve) in subpath.iter().enumerate() {
if snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[index].id) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(index + 1) % subpath.len()].id) {
continue;
}
let in_handle = curve.handle_start().map(|handle| handle - curve.start).filter(handle_not_under(to_document));
let out_handle = curve.handle_end().map(|handle| handle - curve.end).filter(handle_not_under(to_document));
if in_handle.is_none() && out_handle.is_none() {
points.push(SnapCandidatePoint::new(
to_document.transform_point2(curve.start() * 0.5 + curve.end * 0.5),
SnapSource::Geometry(GeometrySnapSource::LineMidpoint),
SnapTarget::Geometry(GeometrySnapTarget::LineMidpoint),
));
}
}
}
// Anchors
for (index, group) in subpath.manipulator_groups().iter().enumerate() {
if snap_data.ignore_manipulator(layer, group.id) {
continue;
}
let smooth = group_smooth(group, to_document, subpath, index);
if smooth && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Smooth)) {
// Smooth points
points.push(SnapCandidatePoint::new(
to_document.transform_point2(group.anchor),
SnapSource::Geometry(GeometrySnapSource::Smooth),
SnapTarget::Geometry(GeometrySnapTarget::Smooth),
));
} else if !smooth && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Sharp)) {
// Sharp points
points.push(SnapCandidatePoint::new(
to_document.transform_point2(group.anchor),
SnapSource::Geometry(GeometrySnapSource::Sharp),
SnapTarget::Geometry(GeometrySnapTarget::Sharp),
));
}
}
}
pub fn group_smooth(group: &bezier_rs::ManipulatorGroup<ManipulatorGroupId>, to_document: DAffine2, subpath: &Subpath<ManipulatorGroupId>, index: usize) -> bool {
let anchor = group.anchor;
let handle_in = group.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
let handle_out = group.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document));
let at_end = !subpath.closed() && (index == 0 || index == subpath.len() - 1);
let smooth = handle_in.is_some_and(|handle_in| handle_out.is_some_and(|handle_out| handle_in.angle_between(handle_out) < 1e-5)) && !at_end;
smooth
}
pub fn get_layer_snap_points(layer: LayerNodeIdentifier, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>) {
let document = snap_data.document;
if document.metadata().is_artboard(layer) {
} else if document.metadata().is_folder(layer) {
for child in layer.decendants(document.metadata()) {
get_layer_snap_points(child, snap_data, points);
}
} else {
// Skip empty paths
if document.metadata.layer_outline(layer).next().is_none() {
return;
}
let to_document = document.metadata.transform_to_document(layer);
for subpath in document.metadata.layer_outline(layer) {
subpath_anchor_snap_points(layer, subpath, snap_data, points, to_document);
}
}
}
@@ -0,0 +1,85 @@
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{SnapSource, SnapTarget};
use bezier_rs::Bezier;
use glam::DVec2;
use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
#[derive(Clone, Debug, Default)]
pub struct SnapResults {
pub points: Vec<SnappedPoint>,
pub grid_lines: Vec<SnappedLine>,
pub curves: Vec<SnappedCurve>,
}
#[derive(Default, Debug, Clone)]
pub struct SnappedPoint {
pub snapped_point_document: DVec2,
pub source: SnapSource,
pub target: SnapTarget,
pub at_intersection: bool,
pub contrained: bool, // Found when looking for contrained
pub target_bounds: Option<Quad>,
pub source_bounds: Option<Quad>,
pub curves: [Option<Bezier>; 2],
pub distance: f64,
pub tolerance: f64,
}
impl SnappedPoint {
pub fn infinite_snap(snapped_point_document: DVec2) -> Self {
Self {
snapped_point_document,
distance: f64::INFINITY,
..Default::default()
}
}
pub fn from_source_point(snapped_point_document: DVec2, source: SnapSource) -> Self {
Self {
snapped_point_document,
source,
..Default::default()
}
}
pub fn other_snap_better(&self, other: &Self) -> bool {
if self.distance.is_finite() && !other.distance.is_finite() {
return false;
}
if !self.distance.is_finite() && other.distance.is_finite() {
return true;
}
let my_dist = self.distance;
let other_dist = other.distance;
// Prevent flickering when two points are equally close
let bias = 1e-2;
// Prefer closest
let other_closer = other_dist < my_dist + bias;
// We should prefer the most contrained option (e.g. intersection > path)
let other_more_contrained = other.contrained && !self.contrained;
let self_more_contrained = self.contrained && !other.contrained;
// Prefer nodes to intersections if both are at the same position
let contrained_at_same_pos = other.contrained && self.contrained && self.snapped_point_document.abs_diff_eq(other.snapped_point_document, 1.);
let other_better_constraint = contrained_at_same_pos && self.at_intersection && !other.at_intersection;
let self_better_constraint = contrained_at_same_pos && other.at_intersection && !self.at_intersection;
(other_closer || other_more_contrained || other_better_constraint) && !self_more_contrained && !self_better_constraint
}
pub fn is_snapped(&self) -> bool {
self.distance.is_finite()
}
}
#[derive(Clone, Debug, Default)]
pub struct SnappedLine {
pub point: SnappedPoint,
pub direction: DVec2,
}
#[derive(Clone, Debug)]
pub struct SnappedCurve {
pub layer: LayerNodeIdentifier,
pub start: ManipulatorGroupId,
pub point: SnappedPoint,
pub document_curve: Bezier,
}
@@ -8,6 +8,14 @@ use graphene_core::renderer::Quad;
use glam::{DAffine2, DVec2};
use super::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
pub struct SizeSnapData<'a> {
pub manager: &'a mut SnapManager,
pub points: &'a mut Vec<SnapCandidatePoint>,
pub snap_data: SnapData<'a>,
}
/// Contains the edges that are being dragged along with the original bounds.
#[derive(Clone, Debug, Default)]
pub struct SelectedEdges {
@@ -60,7 +68,7 @@ impl SelectedEdges {
}
/// Computes the new bounds with the given mouse move and modifier keys
pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center: bool, center_around: DVec2, constrain: bool) -> (DVec2, DVec2) {
pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center_around: Option<DVec2>, constrain: bool, snap: Option<SizeSnapData>) -> (DVec2, DVec2) {
let mouse = transform.inverse().transform_point2(mouse);
let mut min = self.bounds[0];
@@ -77,7 +85,7 @@ impl SelectedEdges {
}
let mut pivot = self.pivot_from_bounds(min, max);
if center {
if let Some(center_around) = center_around {
// The below ratio is: `dragging edge / being centered`.
// The `is_finite()` checks are in case the user is dragging the edge where the pivot is located (in which case the centering mode is ignored).
if self.top {
@@ -120,6 +128,56 @@ impl SelectedEdges {
let delta_size = new_size - size;
min -= delta_size * min_pivot;
max = min + new_size;
} else if let Some(SizeSnapData { manager, points, snap_data }) = snap {
let view_to_doc = snap_data.document.metadata.document_to_viewport.inverse();
let bounds_to_doc = view_to_doc * transform;
let mut best_snap = SnappedPoint::infinite_snap(pivot);
let mut best_scale_factor = DVec2::ONE;
let tolerance = snapping::snap_tolerance(snap_data.document);
for point in points {
let old_position = point.document_point;
let bounds_space = bounds_to_doc.inverse().transform_point2(point.document_point);
let normalised = (bounds_space - self.bounds[0]) / (self.bounds[1] - self.bounds[0]);
let updated = normalised * (max - min) + min;
point.document_point = bounds_to_doc.transform_point2(updated);
let mut snapped = if !(self.top || self.bottom) || !(self.left || self.right) {
let axis = if !(self.top || self.bottom) { DVec2::X } else { DVec2::Y };
let constraint = SnapConstraint::Line {
origin: point.document_point,
direction: bounds_to_doc.transform_vector2(axis),
};
manager.constrained_snap(&snap_data, point, constraint, None)
} else {
manager.free_snap(&snap_data, point, None, false)
};
point.document_point = old_position;
if !snapped.is_snapped() {
continue;
}
let snapped_bounds = bounds_to_doc.inverse().transform_point2(snapped.snapped_point_document);
let mut scale_factor = (snapped_bounds - pivot) / (updated - pivot);
if !(self.left || self.right) {
scale_factor.x = 1.
}
if !(self.top || self.bottom) {
scale_factor.y = 1.
}
snapped.distance = bounds_to_doc.transform_vector2((max - min) * (scale_factor - DVec2::ONE)).length();
if snapped.distance > tolerance || !snapped.distance.is_finite() {
continue;
}
if best_snap.other_snap_better(&snapped) {
best_snap = snapped;
best_scale_factor = scale_factor;
}
}
manager.update_indicator(best_snap);
min = pivot - (pivot - min) * best_scale_factor;
max = pivot - (pivot - max) * best_scale_factor;
}
(min, max - min)