From cdd179cf108c02b908a00f0dbf2a77ef1417273d Mon Sep 17 00:00:00 2001 From: James Lindsay <78500760+0HyperCube@users.noreply.github.com> Date: Mon, 5 Aug 2024 06:12:20 +0100 Subject: [PATCH] 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 --- .../portfolio/document/document_message.rs | 8 +- .../document/document_message_handler.rs | 172 +---- .../portfolio/document/utility_types/misc.rs | 137 ++-- .../tool/common_functionality/shape_editor.rs | 19 +- .../tool/common_functionality/snapping.rs | 228 +++++-- .../snapping/alignment_snapper.rs | 172 +++++ .../snapping/distribution_snapper.rs | 588 ++++++++++++++++++ .../snapping/layer_snapper.rs | 42 +- .../snapping/snap_results.rs | 44 +- .../transformation_cage.rs | 7 +- .../messages/tool/tool_messages/path_tool.rs | 56 +- .../gcore/src/graphic_element/renderer.rs | 2 + .../src/graphic_element/renderer/quad.rs | 5 + .../src/graphic_element/renderer/rect.rs | 125 ++++ .../src/vector/vector_data/attributes.rs | 28 + 15 files changed, 1333 insertions(+), 300 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs create mode 100644 editor/src/messages/tool/common_functionality/snapping/distribution_snapper.rs create mode 100644 node-graph/gcore/src/graphic_element/renderer/rect.rs diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index a423ccdc..0e75f0b6 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -1,4 +1,4 @@ -use super::utility_types::misc::{OptionBoundsSnapping, OptionPointSnapping}; +use super::utility_types::misc::SnappingState; use super::utility_types::network_interface::NodeNetworkInterface; use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -140,9 +140,9 @@ pub enum DocumentMessage { new_layer: Option, }, SetSnapping { - snapping_enabled: Option, - bounding_box_snapping: Option, - geometry_snapping: Option, + #[serde(skip)] + closure: Option fn(&'a mut SnappingState) -> &'a mut bool>, + snapping_state: bool, }, SetViewMode { view_mode: ViewMode, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 15720163..0d6adb5a 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1,7 +1,7 @@ use super::node_graph::utility_types::Transform; use super::utility_types::clipboards::Clipboard; use super::utility_types::error::EditorError; -use super::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, OptionBoundsSnapping, OptionPointSnapping, SnappingOptions, SnappingState}; +use super::utility_types::misc::{SnappingOptions, SnappingState, GET_SNAP_BOX_FUNCTIONS, GET_SNAP_GEOMETRY_FUNCTIONS}; use super::utility_types::network_interface::NodeNetworkInterface; use super::utility_types::nodes::{CollapsedLayers, SelectedNodes}; use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH}; @@ -918,63 +918,9 @@ impl MessageHandler> for DocumentMessag DocumentMessage::SetRangeSelectionLayer { new_layer } => { self.layer_range_selection_reference = new_layer; } - DocumentMessage::SetSnapping { - snapping_enabled, - bounding_box_snapping, - geometry_snapping, - } => { - if let Some(state) = snapping_enabled { - self.snapping_state.snapping_enabled = state - }; - - if let Some(OptionBoundsSnapping { - edge_midpoints, - edges, - centers, - corners, - }) = bounding_box_snapping - { - if let Some(state) = edge_midpoints { - self.snapping_state.bounds.edge_midpoints = state - }; - if let Some(state) = edges { - self.snapping_state.bounds.edges = state - }; - if let Some(state) = centers { - self.snapping_state.bounds.centers = state - }; - if let Some(state) = corners { - self.snapping_state.bounds.corners = state - }; - } - - if let Some(OptionPointSnapping { - paths, - path_intersections, - anchors, - line_midpoints, - normals, - tangents, - }) = geometry_snapping - { - if let Some(state) = path_intersections { - self.snapping_state.nodes.path_intersections = state - }; - if let Some(state) = paths { - self.snapping_state.nodes.paths = state - }; - if let Some(state) = anchors { - self.snapping_state.nodes.anchors = state - }; - if let Some(state) = line_midpoints { - self.snapping_state.nodes.line_midpoints = state - }; - if let Some(state) = normals { - self.snapping_state.nodes.normals = state - }; - if let Some(state) = tangents { - self.snapping_state.nodes.tangents = state - }; + DocumentMessage::SetSnapping { closure, snapping_state } => { + if let Some(closure) = closure { + *closure(&mut self.snapping_state) = snapping_state; } } DocumentMessage::SetViewMode { view_mode } => { @@ -1564,7 +1510,8 @@ impl DocumentMessageHandler { // Document bar (right portion of the bar above the viewport) - let snapping_state = self.snapping_state.clone(); + let mut snapping_state = self.snapping_state.clone(); + let mut snapping_state2 = self.snapping_state.clone(); let mut widgets = vec![ CheckboxInput::new(self.overlays_visible) @@ -1589,11 +1536,9 @@ impl DocumentMessageHandler { .tooltip("Snapping") .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::ToggleSnapping)) .on_update(move |optional_input: &CheckboxInput| { - let snapping_enabled = optional_input.checked; DocumentMessage::SetSnapping { - snapping_enabled: Some(snapping_enabled), - bounding_box_snapping: None, - geometry_snapping: None, + closure: Some(|snapping_state| &mut snapping_state.snapping_enabled), + snapping_state: optional_input.checked, } .into() }) @@ -1609,92 +1554,27 @@ impl DocumentMessageHandler { }, ] .into_iter() + .chain(GET_SNAP_BOX_FUNCTIONS.into_iter().map(|(name, closure)| LayoutGroup::Row { + widgets: vec![ + CheckboxInput::new(*closure(&mut snapping_state)) + .on_update(move |input: &CheckboxInput| DocumentMessage::SetSnapping { closure: Some(closure), snapping_state: input.checked }.into()) + .widget_holder(), + TextLabel::new(name).widget_holder(), + ], + })) .chain( - [ - (BoundingBoxSnapTarget::Center, snapping_state.bounds.centers), - (BoundingBoxSnapTarget::Corner, snapping_state.bounds.corners), - (BoundingBoxSnapTarget::Edge, snapping_state.bounds.edges), - (BoundingBoxSnapTarget::EdgeMidpoint, snapping_state.bounds.edge_midpoints), - ] + [LayoutGroup::Row { + widgets: vec![TextLabel::new(SnappingOptions::Geometry.to_string()).widget_holder()], + }] .into_iter() - .map(|(enum_type, bound_state)| LayoutGroup::Row { + .chain(GET_SNAP_GEOMETRY_FUNCTIONS.into_iter().map(|(name, closure)| LayoutGroup::Row { widgets: vec![ - CheckboxInput::new(bound_state) - .on_update(move |input: &CheckboxInput| { - DocumentMessage::SetSnapping { - snapping_enabled: None, - bounding_box_snapping: Some(OptionBoundsSnapping { - edges: if enum_type == BoundingBoxSnapTarget::Edge { Some(input.checked) } else { None }, - edge_midpoints: if enum_type == BoundingBoxSnapTarget::EdgeMidpoint { Some(input.checked) } else { None }, - centers: if enum_type == BoundingBoxSnapTarget::Center { Some(input.checked) } else { None }, - corners: if enum_type == BoundingBoxSnapTarget::Corner { Some(input.checked) } else { None }, - }), - geometry_snapping: None, - } - .into() - }) - .widget_holder(), - TextLabel::new(enum_type.to_string()).widget_holder(), - ], - }) - .chain( - [ - LayoutGroup::Row { - widgets: vec![TextLabel::new(SnappingOptions::Geometry.to_string()).widget_holder()], - }, - LayoutGroup::Row { - widgets: vec![ - CheckboxInput::new(snapping_state.nodes.anchors) - .on_update(move |input: &CheckboxInput| { - DocumentMessage::SetSnapping { - snapping_enabled: None, - bounding_box_snapping: None, - geometry_snapping: Some(OptionPointSnapping { - anchors: Some(input.checked), - ..Default::default() - }), - } - .into() - }) - .widget_holder(), - TextLabel::new("Anchor").widget_holder(), - ], - }, - ] - .into_iter() - .chain( - [ - (GeometrySnapTarget::LineMidpoint, snapping_state.nodes.line_midpoints), - (GeometrySnapTarget::Path, snapping_state.nodes.paths), - (GeometrySnapTarget::Normal, snapping_state.nodes.normals), - (GeometrySnapTarget::Tangent, snapping_state.nodes.tangents), - (GeometrySnapTarget::Intersection, snapping_state.nodes.path_intersections), - ] - .into_iter() - .map(|(enum_type, bound_state)| LayoutGroup::Row { - widgets: vec![ - CheckboxInput::new(bound_state) - .on_update(move |input: &CheckboxInput| { - DocumentMessage::SetSnapping { - snapping_enabled: None, - bounding_box_snapping: None, - geometry_snapping: Some(OptionPointSnapping { - anchors: None, - line_midpoints: if enum_type == GeometrySnapTarget::LineMidpoint { Some(input.checked) } else { None }, - paths: if enum_type == GeometrySnapTarget::Path { Some(input.checked) } else { None }, - normals: if enum_type == GeometrySnapTarget::Normal { Some(input.checked) } else { None }, - tangents: if enum_type == GeometrySnapTarget::Tangent { Some(input.checked) } else { None }, - path_intersections: if enum_type == GeometrySnapTarget::Intersection { Some(input.checked) } else { None }, - }), - } - .into() - }) - .widget_holder(), - TextLabel::new(enum_type.to_string()).widget_holder(), - ], - }), - ), - ), + CheckboxInput::new(*closure(&mut snapping_state2)) + .on_update(move |input: &CheckboxInput| DocumentMessage::SetSnapping { closure: Some(closure), snapping_state: input.checked }.into()) + .widget_holder(), + TextLabel::new(name).widget_holder(), + ], + })), ) .collect(), ) diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 0766df5d..34610ea5 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -104,8 +104,11 @@ impl SnappingState { GeometrySnapTarget::Tangent => self.nodes.tangents, GeometrySnapTarget::Intersection => self.nodes.path_intersections, }, - SnapTarget::Board(_) => self.artboards, + SnapTarget::Artboard(_) => self.artboards, SnapTarget::Grid(_) => self.grid_snapping, + SnapTarget::Alignment(AlignmentSnapTarget::Handle) => self.nodes.align, + SnapTarget::Alignment(_) => self.bounds.align, + SnapTarget::Distribution(_) => self.bounds.distribute, _ => false, } } @@ -118,6 +121,8 @@ pub struct BoundsSnapping { pub corners: bool, pub edge_midpoints: bool, pub centers: bool, + pub align: bool, + pub distribute: bool, } impl Default for BoundsSnapping { @@ -127,18 +132,12 @@ impl Default for BoundsSnapping { corners: true, edge_midpoints: false, centers: true, + align: true, + distribute: true, } } } -#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct OptionBoundsSnapping { - pub edges: Option, - pub corners: Option, - pub edge_midpoints: Option, - pub centers: Option, -} - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[serde(default)] pub struct PointSnapping { @@ -148,6 +147,7 @@ pub struct PointSnapping { pub line_midpoints: bool, pub normals: bool, pub tangents: bool, + pub align: bool, } impl Default for PointSnapping { @@ -159,20 +159,11 @@ impl Default for PointSnapping { line_midpoints: true, normals: true, tangents: true, + align: false, } } } -#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct OptionPointSnapping { - pub paths: Option, - pub path_intersections: Option, - pub anchors: Option, - pub line_midpoints: Option, - pub normals: Option, - pub tangents: Option, -} - #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub enum GridType { Rectangle { spacing: DVec2 }, @@ -280,7 +271,7 @@ pub enum BoundingBoxSnapSource { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BoardSnapSource { +pub enum ArtboardSnapSource { Center, Corner, } @@ -294,13 +285,24 @@ pub enum GeometrySnapSource { Intersection, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AlignmentSnapSource { + BoundsCorner, + BoundsCenter, + BoundsEdgeMidpoint, + ArtboardCorner, + ArtboardCenter, + Handle, +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum SnapSource { #[default] None, BoundingBox(BoundingBoxSnapSource), - Board(BoardSnapSource), + Artboard(ArtboardSnapSource), Geometry(GeometrySnapSource), + Alignment(AlignmentSnapSource), } impl SnapSource { @@ -308,10 +310,38 @@ impl SnapSource { self != &Self::None } pub fn bounding_box(&self) -> bool { - matches!(self, Self::BoundingBox(_) | Self::Board(_)) + matches!(self, Self::BoundingBox(_) | Self::Artboard(_)) + } + pub fn align(&self) -> bool { + matches!(self, Self::Alignment(_)) + } + pub fn center(&self) -> bool { + matches!( + self, + Self::Alignment(AlignmentSnapSource::ArtboardCenter | AlignmentSnapSource::BoundsCenter) | Self::Artboard(ArtboardSnapSource::Center) | Self::BoundingBox(BoundingBoxSnapSource::Center) + ) } } +type GetSnapState = for<'a> fn(&'a mut SnappingState) -> &'a mut bool; +pub const GET_SNAP_BOX_FUNCTIONS: [(&str, GetSnapState); 6] = [ + ("Box Center", (|snapping_state| &mut snapping_state.bounds.centers) as GetSnapState), + ("Box Corner", (|snapping_state| &mut snapping_state.bounds.corners) as GetSnapState), + ("Along Edge", (|snapping_state| &mut snapping_state.bounds.edges) as GetSnapState), + ("Midpoint of Edge", (|snapping_state| &mut snapping_state.bounds.edge_midpoints) as GetSnapState), + ("Align to Box", (|snapping_state| &mut snapping_state.bounds.align) as GetSnapState), + ("Evenly Distribute Boxes", (|snapping_state| &mut snapping_state.bounds.distribute) as GetSnapState), +]; +pub const GET_SNAP_GEOMETRY_FUNCTIONS: [(&str, GetSnapState); 7] = [ + ("Anchor", (|snapping_state: &mut SnappingState| &mut snapping_state.nodes.anchors) as GetSnapState), + ("Line Midpoint", (|snapping_state: &mut SnappingState| &mut snapping_state.nodes.line_midpoints) as GetSnapState), + ("Path", (|snapping_state: &mut SnappingState| &mut snapping_state.nodes.paths) as GetSnapState), + ("Normal to Path", (|snapping_state: &mut SnappingState| &mut snapping_state.nodes.normals) as GetSnapState), + ("Tangent to Path", (|snapping_state: &mut SnappingState| &mut snapping_state.nodes.tangents) as GetSnapState), + ("Intersection", (|snapping_state: &mut SnappingState| &mut snapping_state.nodes.path_intersections) as GetSnapState), + ("Align to Selected Path", (|snapping_state: &mut SnappingState| &mut snapping_state.nodes.align) as GetSnapState), +]; + #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum BoundingBoxSnapTarget { Center, @@ -320,17 +350,6 @@ pub enum BoundingBoxSnapTarget { EdgeMidpoint, } -impl fmt::Display for BoundingBoxSnapTarget { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Center => write!(f, "Box Center"), - Self::Corner => write!(f, "Box Corner"), - Self::Edge => write!(f, "Along Edge"), - Self::EdgeMidpoint => write!(f, "Midpoint of Edge"), - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum GeometrySnapTarget { AnchorWithColinearHandles, @@ -342,22 +361,8 @@ pub enum GeometrySnapTarget { Intersection, } -impl fmt::Display for GeometrySnapTarget { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::AnchorWithColinearHandles => write!(f, "Anchor (Colinear Handles)"), - Self::AnchorWithFreeHandles => write!(f, "Anchor (Free Handles)"), - Self::LineMidpoint => write!(f, "Line Midpoint"), - Self::Path => write!(f, "Path"), - Self::Normal => write!(f, "Normal to Path"), - Self::Tangent => write!(f, "Tangent to Path"), - Self::Intersection => write!(f, "Intersection"), - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BoardSnapTarget { +pub enum ArtboardSnapTarget { Edge, Corner, Center, @@ -370,14 +375,46 @@ pub enum GridSnapTarget { Intersection, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AlignmentSnapTarget { + BoundsCorner, + BoundsCenter, + ArtboardCorner, + ArtboardCenter, + Handle, + Intersection, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DistributionSnapTarget { + X, + Y, + Right, + Left, + Up, + Down, + Xy, +} + +impl DistributionSnapTarget { + pub const fn is_x(&self) -> bool { + matches!(self, Self::Left | Self::Right | Self::X) + } + pub const fn is_y(&self) -> bool { + matches!(self, Self::Up | Self::Down | Self::Y) + } +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum SnapTarget { #[default] None, BoundingBox(BoundingBoxSnapTarget), Geometry(GeometrySnapTarget), - Board(BoardSnapTarget), + Artboard(ArtboardSnapTarget), Grid(GridSnapTarget), + Alignment(AlignmentSnapTarget), + Distribution(DistributionSnapTarget), } impl SnapTarget { @@ -385,7 +422,7 @@ impl SnapTarget { self != &Self::None } pub fn bounding_box(&self) -> bool { - matches!(self, Self::BoundingBox(_) | Self::Board(_)) + matches!(self, Self::BoundingBox(_) | Self::Artboard(_)) } } diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 632ece27..39cc8652 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -1,5 +1,5 @@ use super::graph_modification_utils; -use super::snapping::{SnapCandidatePoint, SnapData, SnapManager, SnappedPoint}; +use super::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource}; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; @@ -25,6 +25,9 @@ pub struct SelectedLayerState { } impl SelectedLayerState { + pub fn selected(&self) -> impl Iterator + '_ { + self.selected_points.iter().copied() + } pub fn is_selected(&self, point: ManipulatorPointId) -> bool { self.selected_points.contains(&point) } @@ -167,18 +170,8 @@ impl ClosestSegment { // 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 { - let Some(vector_data) = document.metadata().compute_modified_vector(*layer, &document.network_interface) else { - continue; - }; - for point in &state.selected_points { - let Some(anchor) = point.get_anchor(&vector_data) else { continue }; - snap_data.manipulators.push((*layer, anchor)); - } - } + pub fn snap(&self, snap_manager: &mut SnapManager, snap_cache: &SnapCache, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, previous_mouse: DVec2) -> DVec2 { + let snap_data = SnapData::new_snap_cache(document, input, snap_cache); let mouse_delta = document .network_interface diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index ae8d0432..084f8b21 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -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, layer_snapper: LayerSnapper, grid_snapper: GridSnapper, + alignment_snapper: AlignmentSnapper, + distribution_snapper: DistributionSnapper, candidates: Option>, + alignment_candidates: Option>, } #[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) -> Option { + 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, NoHashBuilder>, + pub unselected: Vec, +} + #[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>, + pub alignment_candidates: Option<&'a Vec>, } 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) -> 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) -> Vec { + 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) { - 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, to_paths: bool) -> SnappedPoint { + fn find_candidates(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, bbox: Option) { + 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, 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) -> SnappedPoint { + pub fn constrained_snap(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, constraint: SnapConstraint, bbox: Option) -> 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, 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, 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)); + } } } diff --git a/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs new file mode 100644 index 00000000..6b506ba5 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/snapping/alignment_snapper.rs @@ -0,0 +1,172 @@ +use super::*; +use crate::messages::portfolio::document::utility_types::misc::*; + +use graphene_core::renderer::Quad; + +use glam::{DAffine2, DVec2}; + +#[derive(Clone, Debug, Default)] +pub struct AlignmentSnapper { + bounding_box_points: Vec, +} + +impl AlignmentSnapper { + pub fn collect_bounding_box_points(&mut self, snap_data: &mut SnapData, first_point: bool) { + if !first_point { + return; + } + + let document = snap_data.document; + + self.bounding_box_points.clear(); + if !document.snapping_state.bounds.align { + return; + } + + for layer in document.metadata().all_layers() { + if !document.network_interface.is_artboard(&layer.to_node(), &[]) || snap_data.ignore.contains(&layer) { + continue; + } + + if document.snapping_state.target_enabled(SnapTarget::Artboard(ArtboardSnapTarget::Corner)) { + let Some(bounds) = document.metadata().bounding_box_with_transform(layer, document.metadata().transform_to_document(layer)) else { + continue; + }; + + get_bbox_points(Quad::from_box(bounds), &mut self.bounding_box_points, BBoxSnapValues::ALIGN_ARTBOARD, document); + } + } + for &layer in snap_data.alignment_candidates.map_or([].as_slice(), |candidates| candidates.as_slice()) { + 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::ALIGN_BOUNDING_BOX; + get_bbox_points(quad, &mut self.bounding_box_points, values, document); + } + } + + pub fn snap_bbox_points(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint) { + self.collect_bounding_box_points(snap_data, point.source_index == 0); + let unselected_geometry = if snap_data.document.snapping_state.target_enabled(SnapTarget::Alignment(AlignmentSnapTarget::Handle)) { + snap_data.node_snap_cache.map(|cache| cache.unselected.as_slice()).unwrap_or(&[]) + } else { + &[] + }; + + // TODO: snap handle points + let document = snap_data.document; + let tolerance = snap_tolerance(document); + + let mut consider_x = true; + let mut consider_y = true; + if let SnapConstraint::Line { direction, .. } = constraint { + let direction = direction.normalize_or_zero(); + if direction.x.abs() < 1e-5 { + consider_y = false; + } else if direction.y.abs() < 1e-5 { + consider_x = false; + } + } + + let mut snap_x: Option = None; + let mut snap_y: Option = None; + + for target_point in self.bounding_box_points.iter().chain(unselected_geometry) { + let target_position = target_point.document_point; + + let point_on_x = DVec2::new(point.document_point.x, target_position.y); + let dist_x = (target_position.y - point.document_point.y).abs(); + + let point_on_y = DVec2::new(target_position.x, point.document_point.y); + let dist_y = (target_position.x - point.document_point.x).abs(); + + let target_geometry = matches!(target_point.target, SnapTarget::Geometry(_)); + let updated_target = if target_geometry { + SnapTarget::Alignment(AlignmentSnapTarget::Handle) + } else { + target_point.target + }; + + if consider_x && dist_x < tolerance && snap_x.as_ref().map_or(true, |point| dist_y < point.distance_to_align_target) { + snap_x = Some(SnappedPoint { + snapped_point_document: point_on_x, + source: point.source, //ToDo map source + target: updated_target, + target_bounds: target_point.quad, + distance: dist_x, + tolerance, + distance_to_align_target: dist_y, + alignment_target_x: Some(target_position), + fully_constrained: true, + ..Default::default() + }); + } + if consider_y && dist_y < tolerance && snap_y.as_ref().map_or(true, |point| dist_x < point.distance_to_align_target) { + snap_y = Some(SnappedPoint { + snapped_point_document: point_on_y, + source: point.source, //ToDo map source + target: updated_target, + target_bounds: target_point.quad, + distance: dist_y, + tolerance, + distance_to_align_target: dist_x, + alignment_target_y: Some(target_position), + fully_constrained: true, + ..Default::default() + }); + } + } + + match (snap_x, snap_y) { + (Some(snap_x), Some(snap_y)) => { + let intersection = DVec2::new(snap_y.snapped_point_document.x, snap_x.snapped_point_document.y); + let distance = intersection.distance(point.document_point); + + if distance >= tolerance { + snap_results.points.push(if snap_x.distance < snap_y.distance { snap_x } else { snap_y }); + return; + } + + snap_results.points.push(SnappedPoint { + snapped_point_document: intersection, + source: point.source, // TODO: map source + target: SnapTarget::Alignment(AlignmentSnapTarget::Intersection), + target_bounds: snap_x.target_bounds, + distance, + tolerance, + alignment_target_x: snap_x.alignment_target_x, + alignment_target_y: snap_y.alignment_target_y, + constrained: true, + ..Default::default() + }); + } + (Some(snap_x), None) => snap_results.points.push(snap_x), + (None, Some(snap_y)) => snap_results.points.push(snap_y), + (None, None) => {} + } + } + pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults) { + let is_bbox = matches!(point.source, SnapSource::BoundingBox(_)); + let is_geometry = matches!(point.source, SnapSource::Geometry(_)); + let geometry_selected = snap_data.has_manipulators(); + + if is_bbox || (is_geometry && geometry_selected) || (is_geometry && point.alignment) { + self.snap_bbox_points(snap_data, point, snap_results, SnapConstraint::None); + } + } + + pub fn constrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint) { + let is_bbox = matches!(point.source, SnapSource::BoundingBox(_)); + let is_geometry = matches!(point.source, SnapSource::Geometry(_)); + let geometry_selected = snap_data.has_manipulators(); + + if is_bbox || (is_geometry && geometry_selected) || (is_geometry && point.alignment) { + self.snap_bbox_points(snap_data, point, snap_results, constraint); + } + } +} diff --git a/editor/src/messages/tool/common_functionality/snapping/distribution_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/distribution_snapper.rs new file mode 100644 index 00000000..a6d75525 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/snapping/distribution_snapper.rs @@ -0,0 +1,588 @@ +use super::*; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::misc::*; +use crate::messages::prelude::*; + +use graphene_core::renderer::Quad; + +use glam::DVec2; + +#[derive(Clone, Debug, Default)] +pub struct DistributionSnapper { + right: Vec, + left: Vec, + down: Vec, + up: Vec, +} + +#[cfg_attr(test, derive(Debug, PartialEq))] +pub struct DistributionMatch { + pub equal: f64, + pub first: f64, +} + +fn dist_right(a: Rect, b: Rect) -> f64 { + -a.max().x + b.min().x +} +fn dist_left(a: Rect, b: Rect) -> f64 { + a.min().x - b.max().x +} +fn dist_down(a: Rect, b: Rect) -> f64 { + -a.max().y + b.min().y +} +fn dist_up(a: Rect, b: Rect) -> f64 { + a.min().y - b.max().y +} + +impl DistributionSnapper { + fn add_bounds(&mut self, layer: LayerNodeIdentifier, snap_data: &mut SnapData, bbox_to_snap: Rect, max_extent: f64) { + let document = snap_data.document; + + let Some(bounds) = document.metadata().bounding_box_with_transform(layer, document.metadata().transform_to_document(layer)) else { + return; + }; + let bounds = Rect::from_box(bounds); + if bounds.intersects(bbox_to_snap) { + return; + } + + let difference = bounds.center() - bbox_to_snap.center(); + + let x_bounds = bbox_to_snap.expand_by(max_extent, 0.); + let y_bounds = bbox_to_snap.expand_by(0., max_extent); + + if x_bounds.intersects(bounds) { + if difference.x > 0. { + self.right.push(bounds); + } else { + self.left.push(bounds); + } + } else if y_bounds.intersects(bounds) { + if difference.y > 0. { + self.down.push(bounds); + } else { + self.up.push(bounds); + } + } + } + + pub fn collect_bounding_box_points(&mut self, snap_data: &mut SnapData, first_point: bool, bbox_to_snap: Rect) { + if !first_point { + return; + } + + let document = snap_data.document; + + self.right.clear(); + self.left.clear(); + self.down.clear(); + self.up.clear(); + + let screen_bounds = (document.metadata().document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, snap_data.input.viewport_bounds.size()])).bounding_box(); + let max_extent = (screen_bounds[1] - screen_bounds[0]).abs().max_element(); + + for layer in document.metadata().all_layers() { + if document.network_interface.is_artboard(&layer.to_node(), &[]) && !snap_data.ignore.contains(&layer) { + self.add_bounds(layer, snap_data, bbox_to_snap, max_extent); + } + } + + for &layer in snap_data.alignment_candidates.map_or([].as_slice(), |candidates| candidates.as_slice()) { + if !snap_data.ignore_bounds(layer) { + self.add_bounds(layer, snap_data, bbox_to_snap, max_extent); + } + } + + self.right.sort_unstable_by(|a, b| a.center().x.total_cmp(&b.center().x)); + self.left.sort_unstable_by(|a, b| b.center().x.total_cmp(&a.center().x)); + self.down.sort_unstable_by(|a, b| a.center().y.total_cmp(&b.center().y)); + self.up.sort_unstable_by(|a, b| b.center().y.total_cmp(&a.center().y)); + + Self::merge_intersecting(&mut self.right); + Self::merge_intersecting(&mut self.left); + Self::merge_intersecting(&mut self.down); + Self::merge_intersecting(&mut self.up); + } + + fn merge_intersecting(rectangles: &mut Vec) { + let mut index = 0; + while index < rectangles.len() { + let insert_index = index; + let mut obelisk = rectangles[index]; + + while index + 1 < rectangles.len() && rectangles[index].intersects(rectangles[index + 1]) { + index += 1; + obelisk = Rect::combine_bounds(obelisk, rectangles[index]); + } + + if index > insert_index { + rectangles.insert(insert_index, obelisk); + index += 1; + } + + index += 1; + } + } + + fn exact_further_matches(source: Rect, rectangles: &[Rect], dist_fn: fn(Rect, Rect) -> f64, first_dist: f64, depth: u8) -> VecDeque { + if rectangles.is_empty() || depth > 10 { + return VecDeque::from([source]); + } + + for (index, &rect) in rectangles.iter().enumerate() { + let next_dist = dist_fn(source, rect); + + if (first_dist - next_dist).abs() < 5e-5 * depth as f64 { + let mut results = Self::exact_further_matches(rect, &rectangles[(index + 1)..], dist_fn, first_dist, depth + 1); + results.push_front(source); + return results; + } + } + + VecDeque::from([source]) + } + + fn matches_within_tolerance(source: Rect, rectangles: &[Rect], tolerance: f64, dist_fn: fn(Rect, Rect) -> f64, first_dist: f64) -> Option<(f64, VecDeque)> { + for (index, &rect) in rectangles.iter().enumerate() { + let next_dist = dist_fn(source, rect); + + if (first_dist - next_dist).abs() < tolerance { + let this_dist = next_dist; + let results = Self::exact_further_matches(rect, &rectangles[(index + 1)..], dist_fn, this_dist, 2); + return Some((this_dist, results)); + } + } + + None + } + + fn top_level_matches(source: Rect, rectangles: &[Rect], tolerance: f64, dist_fn: fn(Rect, Rect) -> f64) -> (Option, VecDeque) { + if rectangles.is_empty() { + return (None, VecDeque::new()); + } + + let mut best: Option<(DistributionMatch, Rect, VecDeque)> = None; + for (index, &rect) in rectangles.iter().enumerate() { + let first_dist = dist_fn(source, rect); + + let Some((equal_dist, results)) = Self::matches_within_tolerance(rect, &rectangles[(index + 1)..], tolerance, dist_fn, first_dist) else { + continue; + }; + if best.as_ref().is_some_and(|(_, _, best)| best.len() >= results.len()) { + continue; + } + + best = Some((DistributionMatch { first: first_dist, equal: equal_dist }, rect, results)); + } + + if let Some((dist, rect, mut results)) = best { + results.push_front(rect); + (Some(dist), results) + } else { + (None, VecDeque::from([rectangles[0]])) + } + } + + fn snap_bbox_points(&self, tolerance: f64, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint, bounds: Rect) { + let mut consider_x = true; + let mut consider_y = true; + if let SnapConstraint::Line { direction, .. } = constraint { + let direction = direction.normalize_or_zero(); + if direction.x == 0. { + consider_x = false; + } else if direction.y == 0. { + consider_y = false; + } + } + + let mut snap_x: Option = None; + let mut snap_y: Option = None; + + self.x(consider_x, bounds, tolerance, &mut snap_x, point); + self.y(consider_y, bounds, tolerance, &mut snap_y, point); + + match (snap_x, snap_y) { + (Some(x), Some(y)) => { + let x_bounds = Rect::from_box(x.source_bounds.unwrap_or_default().bounding_box()); + let y_bounds = Rect::from_box(y.source_bounds.unwrap_or_default().bounding_box()); + let final_bounds = Rect::from_box([0, 1].map(|index| DVec2::new(x_bounds[index].x, y_bounds[index].y))); + + let mut final_point = x; + final_point.snapped_point_document += y.snapped_point_document - point.document_point; + final_point.source_bounds = Some(final_bounds.into()); + final_point.target = SnapTarget::Distribution(DistributionSnapTarget::Xy); + final_point.distribution_boxes_y = y.distribution_boxes_y; + final_point.distribution_equal_distance_y = y.distribution_equal_distance_y; + final_point.distance = (final_point.distance * final_point.distance + y.distance * y.distance).sqrt(); + snap_results.points.push(final_point); + } + (Some(x), None) => snap_results.points.push(x), + (None, Some(y)) => snap_results.points.push(y), + (None, None) => {} + } + } + + fn x(&self, consider_x: bool, bounds: Rect, tolerance: f64, snap_x: &mut Option, point: &SnapCandidatePoint) { + // Right + if consider_x && !self.right.is_empty() { + let (equal_dist, mut vec_right) = Self::top_level_matches(bounds, &self.right, tolerance, dist_right); + if let Some(distances) = equal_dist { + let translation = DVec2::X * (distances.first - distances.equal); + vec_right.push_front(bounds.translate(translation)); + + for &left in Self::exact_further_matches(bounds.translate(translation), &self.left, dist_left, distances.equal, 2).iter().skip(1) { + vec_right.push_front(left); + } + + *snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Right, vec_right, distances, bounds, translation, tolerance)) + } + } + + // Left + if consider_x && !self.left.is_empty() && snap_x.is_none() { + let (equal_dist, mut vec_left) = Self::top_level_matches(bounds, &self.left, tolerance, dist_left); + if let Some(distances) = equal_dist { + let translation = -DVec2::X * (distances.first - distances.equal); + vec_left.make_contiguous().reverse(); + vec_left.push_back(bounds.translate(translation)); + + for &right in Self::exact_further_matches(bounds.translate(translation), &self.right, dist_right, distances.equal, 2).iter().skip(1) { + vec_left.push_back(right); + } + + *snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Left, vec_left, distances, bounds, translation, tolerance)) + } + } + + // Center X + if consider_x && !self.left.is_empty() && !self.right.is_empty() && snap_x.is_none() { + let target_x = (self.right[0].min() + self.left[0].max()).x / 2.; + + let offset = target_x - bounds.center().x; + + if offset.abs() < tolerance { + let translation = DVec2::X * offset; + let equal = bounds.translate(translation).min().x - self.left[0].max().x; + let first = equal + offset; + let distances = DistributionMatch { first, equal }; + let boxes = VecDeque::from([self.left[0], bounds.translate(translation), self.right[0]]); + *snap_x = Some(SnappedPoint::distribute(point, DistributionSnapTarget::X, boxes, distances, bounds, translation, tolerance)) + } + } + } + + fn y(&self, consider_y: bool, bounds: Rect, tolerance: f64, snap_y: &mut Option, point: &SnapCandidatePoint) { + // Down + if consider_y && !self.down.is_empty() { + let (equal_dist, mut vec_down) = Self::top_level_matches(bounds, &self.down, tolerance, dist_down); + if let Some(distances) = equal_dist { + let translation = DVec2::Y * (distances.first - distances.equal); + vec_down.push_front(bounds.translate(translation)); + + for &up in Self::exact_further_matches(bounds.translate(translation), &self.up, dist_up, distances.equal, 2).iter().skip(1) { + vec_down.push_front(up); + } + + *snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Down, vec_down, distances, bounds, translation, tolerance)) + } + } + + // Up + if consider_y && !self.up.is_empty() && snap_y.is_none() { + let (equal_dist, mut vec_up) = Self::top_level_matches(bounds, &self.up, tolerance, dist_up); + if let Some(distances) = equal_dist { + let translation = -DVec2::Y * (distances.first - distances.equal); + vec_up.make_contiguous().reverse(); + vec_up.push_back(bounds.translate(translation)); + + for &down in Self::exact_further_matches(bounds.translate(translation), &self.down, dist_down, distances.equal, 2).iter().skip(1) { + vec_up.push_back(down); + } + + *snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Up, vec_up, distances, bounds, translation, tolerance)) + } + } + + // Center Y + if consider_y && !self.up.is_empty() && !self.down.is_empty() && snap_y.is_none() { + let target_y = (self.down[0].min() + self.up[0].max()).y / 2.; + + let offset = target_y - bounds.center().y; + + if offset.abs() < tolerance { + let translation = DVec2::Y * offset; + + let equal = bounds.translate(translation).min().y - self.up[0].max().y; + let first = equal + offset; + let distances = DistributionMatch { first, equal }; + + let boxes = VecDeque::from([self.up[0], bounds.translate(translation), self.down[0]]); + + *snap_y = Some(SnappedPoint::distribute(point, DistributionSnapTarget::Y, boxes, distances, bounds, translation, tolerance)) + } + } + } + + pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, bounds: Option) { + let Some(bounds) = bounds else { return }; + if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::Center) || !snap_data.document.snapping_state.bounds.distribute { + return; + } + + self.collect_bounding_box_points(snap_data, point.source_index == 0, bounds); + self.snap_bbox_points(snap_tolerance(snap_data.document), point, snap_results, SnapConstraint::None, bounds); + } + + pub fn constrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint, bounds: Option) { + let Some(bounds) = bounds else { return }; + if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::Center) || !snap_data.document.snapping_state.bounds.distribute { + return; + } + self.collect_bounding_box_points(snap_data, point.source_index == 0, bounds); + self.snap_bbox_points(snap_tolerance(snap_data.document), point, snap_results, constraint, bounds); + } +} + +#[test] +fn merge_intersecting_test() { + let mut rectangles = vec![Rect::from_square(DVec2::ZERO, 2.), Rect::from_square(DVec2::new(10., 0.), 2.)]; + DistributionSnapper::merge_intersecting(&mut rectangles); + assert_eq!(rectangles.len(), 2); + + let mut rectangles = vec![ + Rect::from_square(DVec2::ZERO, 2.), + Rect::from_square(DVec2::new(1., 0.), 2.), + Rect::from_square(DVec2::new(10., 0.), 2.), + Rect::from_square(DVec2::new(11., 0.), 2.), + ]; + DistributionSnapper::merge_intersecting(&mut rectangles); + assert_eq!(rectangles.len(), 6); + assert_eq!(rectangles[0], Rect::from_box([DVec2::new(-2., -2.), DVec2::new(3., 2.)])); + assert_eq!(rectangles[3], Rect::from_box([DVec2::new(8., -2.), DVec2::new(13., 2.)])); +} + +#[test] +fn dist_simple_2() { + let rectangles = [10., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)); + let source = Rect::from_square(DVec2::new(0.5, 0.), 2.); + let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 1., dist_right); + assert_eq!(offset, Some(DistributionMatch { first: 5.5, equal: 6. })); + assert_eq!(rectangles.len(), 2); +} + +#[test] +fn dist_simple_3() { + let rectangles = [10., 20., 30.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)); + let source = Rect::from_square(DVec2::new(0.5, 0.), 2.); + let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 1., dist_right); + assert_eq!(offset, Some(DistributionMatch { first: 5.5, equal: 6. })); + assert_eq!(rectangles.len(), 3); +} + +#[test] +fn dist_out_of_tolerance() { + let rectangles = [10., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)); + let source = Rect::from_square(DVec2::new(0.5, 0.), 2.); + let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 0.4, dist_right); + assert_eq!(offset, None); + assert_eq!(rectangles.len(), 1); +} + +#[test] +fn dist_with_nonsense() { + let source = Rect::from_square(DVec2::new(0.5, 0.), 2.); + let rectangles = [2., 10., 15., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)); + let (offset, rectangles) = DistributionSnapper::top_level_matches(source, &rectangles, 1., dist_right); + assert_eq!(offset, Some(DistributionMatch { first: 5.5, equal: 6. })); + assert_eq!(rectangles.len(), 2); +} + +#[cfg(test)] +fn assert_boxes_in_order(rectangles: &VecDeque, index: usize) { + for (&first, &second) in rectangles.iter().zip(rectangles.iter().skip(1)) { + assert!(first.max()[index] < second.min()[index], "{first:?} {second:?} {index}") + } +} + +#[test] +fn dist_snap_point_right() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.right = [2., 10., 15., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + dist_snapper.left = [-2.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0.5, 0.), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3); + assert_eq!(snap_results.points[0].distribution_boxes_x[0], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0); +} + +#[test] +fn dist_snap_point_right_left() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.right = [2., 10., 15., 20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + dist_snapper.left = [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0.5, 0.), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 5); + assert_eq!(snap_results.points[0].distribution_boxes_x[1], Rect::from_square(DVec2::new(-10., 0.), 2.)); + assert_eq!(snap_results.points[0].distribution_boxes_x[2], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0); +} + +#[test] +fn dist_snap_point_left() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.left = [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0.5, 0.), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3); + assert_eq!(snap_results.points[0].distribution_boxes_x[2], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0); +} + +#[test] +fn dist_snap_point_left_right() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.left = [-2., -10., -15., -20.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + dist_snapper.right = [2., 10., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0.5, 0.), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 4); + assert_eq!(snap_results.points[0].distribution_boxes_x[2], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0); +} + +#[test] +fn dist_snap_point_center_x() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.left = [-10., -15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + dist_snapper.right = [10., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0.5, 0.), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3); + assert_eq!(snap_results.points[0].distribution_boxes_x[1], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0); +} + +// ---------------------------------- + +#[test] +fn dist_snap_point_down() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.down = [2., 10., 15., 20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + dist_snapper.up = [-2.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0., 0.5), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3); + assert_eq!(snap_results.points[0].distribution_boxes_y[0], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1); +} + +#[test] +fn dist_snap_point_down_up() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.down = [2., 10., 15., 20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + dist_snapper.up = [-2., -10., -15., -20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0., 0.5), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 5); + assert_eq!(snap_results.points[0].distribution_boxes_y[1], Rect::from_square(DVec2::new(0., -10.), 2.)); + assert_eq!(snap_results.points[0].distribution_boxes_y[2], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1); +} + +#[test] +fn dist_snap_point_up() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.up = [-2., -10., -15., -20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0., 0.5), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3); + assert_eq!(snap_results.points[0].distribution_boxes_y[2], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1); +} + +#[test] +fn dist_snap_point_up_down() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.up = [-2., -10., -15., -20.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + dist_snapper.down = [2., 10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0., 0.5), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 4); + assert_eq!(snap_results.points[0].distribution_boxes_y[2], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1); +} + +#[test] +fn dist_snap_point_center_y() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.up = [-10., -15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + dist_snapper.down = [10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0., 0.5), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5); + assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3); + assert_eq!(snap_results.points[0].distribution_boxes_y[1], Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1); +} + +#[test] +fn dist_snap_point_center_xy() { + let mut dist_snapper = DistributionSnapper::default(); + dist_snapper.up = [-10., -15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + dist_snapper.down = [10., 15.].map(|y| Rect::from_square(DVec2::new(0., y), 2.)).to_vec(); + dist_snapper.left = [-12., -15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + dist_snapper.right = [12., 15.].map(|x| Rect::from_square(DVec2::new(x, 0.), 2.)).to_vec(); + let source = Rect::from_square(DVec2::new(0.3, 0.4), 2.); + let snap_results = &mut SnapResults::default(); + dist_snapper.snap_bbox_points(1., &SnapCandidatePoint::default(), snap_results, SnapConstraint::None, source); + assert_eq!(snap_results.points.len(), 1); + assert_eq!(snap_results.points[0].distance, 0.5000000000000001); + assert_eq!(snap_results.points[0].distribution_equal_distance_x, Some(8.)); + assert_eq!(snap_results.points[0].distribution_equal_distance_y, Some(6.)); + assert_eq!(snap_results.points[0].distribution_boxes_x.len(), 3); + assert_eq!(snap_results.points[0].distribution_boxes_y.len(), 3); + assert_eq!(Rect::from_box(snap_results.points[0].source_bounds.unwrap().bounding_box()), Rect::from_square(DVec2::new(0., 0.), 2.)); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_x, 0); + assert_boxes_in_order(&snap_results.points[0].distribution_boxes_y, 1); +} diff --git a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs index 19ebd1c4..8f81ef66 100644 --- a/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/layer_snapper.rs @@ -60,7 +60,7 @@ impl LayerSnapper { if !document.network_interface.is_artboard(&layer.to_node(), &[]) || snap_data.ignore.contains(&layer) { continue; } - self.add_layer_bounds(document, layer, SnapTarget::Board(BoardSnapTarget::Edge)); + self.add_layer_bounds(document, layer, SnapTarget::Artboard(ArtboardSnapTarget::Edge)); } for &layer in snap_data.get_candidates() { let transform = document.metadata().transform_to_document(layer); @@ -183,7 +183,7 @@ impl LayerSnapper { continue; } - if document.snapping_state.target_enabled(SnapTarget::Board(BoardSnapTarget::Corner)) { + if document.snapping_state.target_enabled(SnapTarget::Artboard(ArtboardSnapTarget::Corner)) { let Some(bounds) = document .network_interface .document_metadata() @@ -316,17 +316,19 @@ pub struct SnapCandidatePoint { pub source_index: usize, pub quad: Option, pub neighbors: Vec, + pub alignment: bool, } impl SnapCandidatePoint { pub fn new(document_point: DVec2, source: SnapSource, target: SnapTarget) -> Self { - Self::new_quad(document_point, source, target, None) + Self::new_quad(document_point, source, target, None, true) } - pub fn new_quad(document_point: DVec2, source: SnapSource, target: SnapTarget, quad: Option) -> Self { + pub fn new_quad(document_point: DVec2, source: SnapSource, target: SnapTarget, quad: Option, alignment: bool) -> Self { Self { document_point, source, target, quad, + alignment, ..Default::default() } } @@ -362,12 +364,30 @@ impl BBoxSnapValues { }; pub const ARTBOARD: Self = Self { - corner_source: SnapSource::Board(BoardSnapSource::Corner), - corner_target: SnapTarget::Board(BoardSnapTarget::Corner), + corner_source: SnapSource::Artboard(ArtboardSnapSource::Corner), + corner_target: SnapTarget::Artboard(ArtboardSnapTarget::Corner), edge_source: SnapSource::None, edge_target: SnapTarget::None, - center_source: SnapSource::Board(BoardSnapSource::Center), - center_target: SnapTarget::Board(BoardSnapTarget::Center), + center_source: SnapSource::Artboard(ArtboardSnapSource::Center), + center_target: SnapTarget::Artboard(ArtboardSnapTarget::Center), + }; + + pub const ALIGN_BOUNDING_BOX: Self = Self { + corner_source: SnapSource::Alignment(AlignmentSnapSource::BoundsCorner), + corner_target: SnapTarget::Alignment(AlignmentSnapTarget::BoundsCorner), + edge_source: SnapSource::None, + edge_target: SnapTarget::None, + center_source: SnapSource::Alignment(AlignmentSnapSource::BoundsCenter), + center_target: SnapTarget::Alignment(AlignmentSnapTarget::BoundsCenter), + }; + + pub const ALIGN_ARTBOARD: Self = Self { + corner_source: SnapSource::Alignment(AlignmentSnapSource::ArtboardCorner), + corner_target: SnapTarget::Alignment(AlignmentSnapTarget::ArtboardCorner), + edge_source: SnapSource::None, + edge_target: SnapTarget::None, + center_source: SnapSource::Alignment(AlignmentSnapSource::ArtboardCenter), + center_target: SnapTarget::Alignment(AlignmentSnapTarget::ArtboardCenter), }; } pub fn get_bbox_points(quad: Quad, points: &mut Vec, values: BBoxSnapValues, document: &DocumentMessageHandler) { @@ -375,14 +395,14 @@ pub fn get_bbox_points(quad: Quad, points: &mut Vec, values: 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))); + points.push(SnapCandidatePoint::new_quad(start, values.corner_source, values.corner_target, Some(quad), false)); } 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))); + points.push(SnapCandidatePoint::new_quad((start + end) / 2., values.edge_source, values.edge_target, Some(quad), false)); } } if document.snapping_state.target_enabled(values.center_target) { - points.push(SnapCandidatePoint::new_quad(quad.center(), values.center_source, values.center_target, Some(quad))); + points.push(SnapCandidatePoint::new_quad(quad.center(), values.center_source, values.center_target, Some(quad), false)); } } diff --git a/editor/src/messages/tool/common_functionality/snapping/snap_results.rs b/editor/src/messages/tool/common_functionality/snapping/snap_results.rs index 3985e86d..45c22914 100644 --- a/editor/src/messages/tool/common_functionality/snapping/snap_results.rs +++ b/editor/src/messages/tool/common_functionality/snapping/snap_results.rs @@ -1,9 +1,15 @@ +use std::collections::VecDeque; + use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::utility_types::misc::{SnapSource, SnapTarget}; +use crate::messages::portfolio::document::utility_types::misc::{DistributionSnapTarget, SnapSource, SnapTarget}; +use crate::messages::tool::common_functionality::snapping::SnapCandidatePoint; use bezier_rs::Bezier; use glam::DVec2; use graphene_core::renderer::Quad; use graphene_core::vector::PointId; +use graphene_std::renderer::Rect; + +use super::DistributionMatch; #[derive(Clone, Debug, Default)] pub struct SnapResults { @@ -18,13 +24,24 @@ pub struct SnappedPoint { pub target: SnapTarget, pub at_intersection: bool, pub constrained: bool, // Found when looking for constrained + pub fully_constrained: bool, pub target_bounds: Option, pub source_bounds: Option, pub curves: [Option; 2], pub distance: f64, pub tolerance: f64, + pub distribution_boxes_x: VecDeque, + pub distribution_equal_distance_x: Option, + pub distribution_boxes_y: VecDeque, + pub distribution_equal_distance_y: Option, + pub distance_to_align_target: f64, // If aligning so that the top is aligned but the X pos is 200 from the target, this is 200. + pub alignment_target_x: Option, + pub alignment_target_y: Option, } impl SnappedPoint { + pub fn align(&self) -> bool { + self.alignment_target_x.is_some() || self.alignment_target_y.is_some() + } pub fn infinite_snap(snapped_point_document: DVec2) -> Self { Self { snapped_point_document, @@ -39,6 +56,25 @@ impl SnappedPoint { ..Default::default() } } + pub fn distribute(point: &SnapCandidatePoint, target: DistributionSnapTarget, boxes: VecDeque, distances: DistributionMatch, bounds: Rect, translation: DVec2, tolerance: f64) -> Self { + let is_x = target.is_x(); + + let [distribution_boxes_x, distribution_boxes_y] = if is_x { [boxes, Default::default()] } else { [Default::default(), boxes] }; + Self { + snapped_point_document: point.document_point + translation, + source: point.source, + target: SnapTarget::Distribution(target), + distribution_boxes_x, + distribution_equal_distance_x: is_x.then_some(distances.equal), + distribution_boxes_y, + distribution_equal_distance_y: (!is_x).then_some(distances.equal), + distance: (distances.first - distances.equal).abs(), + constrained: true, + source_bounds: Some(bounds.translate(translation).into()), + tolerance, + ..Default::default() + } + } pub fn other_snap_better(&self, other: &Self) -> bool { if self.distance.is_finite() && !other.distance.is_finite() { return false; @@ -60,12 +96,16 @@ impl SnappedPoint { let other_more_constrained = other.constrained && !self.constrained; let self_more_constrained = self.constrained && !other.constrained; + let both_align = other.align() && self.align(); + let other_better_align = !other.align() && self.align() || (both_align && !self.source.center() && other.source.center()); + let self_better_align = !self.align() && other.align() || (both_align && !other.source.center() && self.source.center()); + // Prefer nodes to intersections if both are at the same position let constrained_at_same_pos = other.constrained && self.constrained && self.snapped_point_document.abs_diff_eq(other.snapped_point_document, 1.); let other_better_constraint = constrained_at_same_pos && self.at_intersection && !other.at_intersection; let self_better_constraint = constrained_at_same_pos && other.at_intersection && !self.at_intersection; - (other_closer || other_more_constrained || other_better_constraint) && !self_more_constrained && !self_better_constraint + (other_closer || other_more_constrained || other_better_align || other_better_constraint) && !self_more_constrained && !self_better_align && !self_better_constraint } pub fn is_snapped(&self) -> bool { self.distance.is_finite() diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index 4a5820b6..17f754aa 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -7,6 +7,7 @@ use crate::messages::prelude::*; use graphene_core::renderer::Quad; use glam::{DAffine2, DVec2}; +use graphene_std::renderer::Rect; use super::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint}; @@ -225,6 +226,8 @@ pub fn snap_drag(start: DVec2, current: DVec2, axis_align: bool, snap_data: Snap let mut offset = mouse_delta_document; let mut best_snap = SnappedPoint::infinite_snap(document.metadata().document_to_viewport.inverse().transform_point2(mouse_position)); + let bbox = Rect::point_iter(candidates.iter().map(|candidate| candidate.document_point + total_mouse_delta_document)); + for point in candidates { let mut point = point.clone(); point.document_point += total_mouse_delta_document; @@ -237,10 +240,10 @@ pub fn snap_drag(start: DVec2, current: DVec2, axis_align: bool, snap_data: Snap origin: point.document_point, direction: total_mouse_delta_document.try_normalize().unwrap_or(DVec2::X), }, - None, + bbox, ) } else { - snap_manager.free_snap(&snap_data, &point, None, false) + snap_manager.free_snap(&snap_data, &point, bbox, false) }; if best_snap.other_snap_better(&snapped) { diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 449d5f23..26e4c3a4 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -5,11 +5,12 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; -use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState}; -use crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager}; +use crate::messages::tool::common_functionality::shape_editor::{ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, ShapeState}; +use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapData, SnapManager}; use graphene_core::renderer::Quad; use graphene_core::vector::ManipulatorPointId; +use graphene_std::vector::NoHashBuilder; use std::vec; @@ -258,6 +259,7 @@ struct PathToolData { /// The available information varies depending on whether `None`, `One`, or `Multiple` points are currently selected. selection_status: SelectionStatus, segment: Option, + snap_cache: SnapCache, double_click_handled: bool, auto_panning: AutoPanning, } @@ -323,7 +325,7 @@ impl PathToolData { if let Some(selected_points) = shape_editor.change_point_selection(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD, add_to_selection) { if let Some(selected_points) = selected_points { self.drag_start_pos = input.mouse.position; - self.start_dragging_point(selected_points, input, document, responses); + self.start_dragging_point(selected_points, input, document, shape_editor, responses); responses.add(OverlaysMessage::Draw); } PathToolFsmState::Dragging @@ -359,26 +361,40 @@ impl PathToolData { } } - fn start_dragging_point(&mut self, mut selected_points: SelectedPointsInfo, input: &InputPreprocessorMessageHandler, document: &DocumentMessageHandler, responses: &mut VecDeque) { + fn start_dragging_point( + &mut self, + selected_points: SelectedPointsInfo, + input: &InputPreprocessorMessageHandler, + document: &DocumentMessageHandler, + shape_editor: &mut ShapeState, + responses: &mut VecDeque, + ) { responses.add(DocumentMessage::StartTransaction); - // TODO: enable snapping + let mut manipulators = HashMap::with_hasher(NoHashBuilder); + let mut unselected = Vec::new(); + for (&layer, state) in &shape_editor.selected_shape_state { + let Some(vector_data) = document.metadata().compute_modified_vector(layer, &document.network_interface) else { + continue; + }; + let transform = document.metadata().transform_to_document(layer); - // self - // .snap_manager - // .start_snap(document, input, document.bounding_boxes(Some(&selected_layers), None, font_cache), true, true); - - // Do not snap against handles when anchor is selected - let mut additional_selected_points = Vec::new(); - for point in selected_points.points.iter() { - let Some(anchor) = point.point_id.as_anchor() else { continue }; - - let connected = selected_points.vector_data.segment_domain.all_connected(anchor).map(|handle| handle.to_manipulator_point()); - let filtered = connected.filter(|point| point.get_position(&selected_points.vector_data).is_some()); - let point_info = filtered.map(|point_id| ManipulatorPointInfo { layer: point.layer, point_id }); - additional_selected_points.extend(point_info); + let mut layer_manipulators = HashSet::with_hasher(NoHashBuilder); + for point in state.selected() { + let Some(anchor) = point.get_anchor(&vector_data) else { continue }; + layer_manipulators.insert(anchor); + } + for (&id, &position) in vector_data.point_domain.ids().iter().zip(vector_data.point_domain.positions()) { + if layer_manipulators.contains(&id) { + continue; + } + unselected.push(SnapCandidatePoint::handle(transform.transform_point2(position))) + } + if !layer_manipulators.is_empty() { + manipulators.insert(layer, layer_manipulators); + } } - selected_points.points.extend(additional_selected_points); + self.snap_cache = SnapCache { manipulators, unselected }; let viewport_to_document = document.metadata().document_to_viewport.inverse(); self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position - selected_points.offset); @@ -412,7 +428,7 @@ impl PathToolData { fn drag(&mut self, equidistant: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { // Move the selected points with the mouse let previous_mouse = document.metadata().document_to_viewport.transform_point2(self.previous_mouse_position); - let snapped_delta = shape_editor.snap(&mut self.snap_manager, document, input, previous_mouse); + let snapped_delta = shape_editor.snap(&mut self.snap_manager, &self.snap_cache, document, input, previous_mouse); let handle_lengths = if equidistant { None } else { self.opposing_handle_lengths.take() }; shape_editor.move_selected_points(handle_lengths, document, snapped_delta, equidistant, responses); self.previous_mouse_position += document.metadata().document_to_viewport.inverse().transform_vector2(snapped_delta); diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index a558b784..991d7062 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -1,5 +1,7 @@ mod quad; +mod rect; pub use quad::Quad; +pub use rect::Rect; use crate::raster::bbox::Bbox; use crate::raster::{BlendMode, Image, ImageFrame}; diff --git a/node-graph/gcore/src/graphic_element/renderer/quad.rs b/node-graph/gcore/src/graphic_element/renderer/quad.rs index 3ae65cc9..e131b8bd 100644 --- a/node-graph/gcore/src/graphic_element/renderer/quad.rs +++ b/node-graph/gcore/src/graphic_element/renderer/quad.rs @@ -16,6 +16,11 @@ impl Quad { Self([bbox[0], bbox[0] + size * DVec2::X, bbox[1], bbox[0] + size * DVec2::Y]) } + /// Create a quad from the center and offset (distance from center to middle of an edge) + pub fn from_square(center: DVec2, offset: f64) -> Self { + Self::from_box([center - offset, center + offset]) + } + /// Get all the edges in the quad. pub fn edges(&self) -> [[DVec2; 2]; 4] { [[self.0[0], self.0[1]], [self.0[1], self.0[2]], [self.0[2], self.0[3]], [self.0[3], self.0[0]]] diff --git a/node-graph/gcore/src/graphic_element/renderer/rect.rs b/node-graph/gcore/src/graphic_element/renderer/rect.rs new file mode 100644 index 00000000..0af6e5e4 --- /dev/null +++ b/node-graph/gcore/src/graphic_element/renderer/rect.rs @@ -0,0 +1,125 @@ +use glam::{DAffine2, DVec2}; + +use super::Quad; + +#[derive(Debug, Clone, Default, Copy, PartialEq)] +/// An axis aligned rect defined by two vertices. +pub struct Rect(pub [DVec2; 2]); + +impl Rect { + /// Create a zero sized quad at the point + #[must_use] + pub fn from_point(point: DVec2) -> Self { + Self([point; 2]) + } + + /// Convert a box defined by two corner points to a quad. + #[must_use] + pub fn from_box(bbox: [DVec2; 2]) -> Self { + Self([bbox[0].min(bbox[1]), bbox[0].max(bbox[1])]) + } + + /// Create a quad from the center and offset (distance from center to middle of an edge) + #[must_use] + pub fn from_square(center: DVec2, offset: f64) -> Self { + Self::from_box([center - offset, center + offset]) + } + + /// Create an AABB from an iter of points, returning None if empty. + #[must_use] + pub fn point_iter(points: impl Iterator) -> Option { + let mut bounds = None; + for point in points { + let bounds = bounds.get_or_insert(Self::from_point(point)); + bounds[0] = bounds[0].min(point); + bounds[1] = bounds[1].max(point); + } + bounds + } + + /// Get all the edges in the rect. + #[must_use] + pub fn edges(&self) -> [[DVec2; 2]; 4] { + let corners = [self[0], DVec2::new(self[0].x, self[1].y), self[1], DVec2::new(self[1].y, self[0].x)]; + [[corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]]] + } + + /// Get all the edges in the rect as linear bezier curves + pub fn bezier_lines(&self) -> impl Iterator + '_ { + self.edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end)) + } + + /// Gets the center of a rect + #[must_use] + pub fn center(&self) -> DVec2 { + self.0.iter().sum::() / 2. + } + + /// Take the outside bounds of two axis aligned rectangles, which are defined by two corner points. + #[must_use] + pub fn combine_bounds(a: Self, b: Self) -> Self { + Self::from_box([a[0].min(b[0]), a[1].max(b[1])]) + } + + /// Expand a rect by a certain amount on top/bottom and on left/right + #[must_use] + pub fn expand_by(&self, x: f64, y: f64) -> Self { + let delta = DVec2::new(x, y); + Self::from_box([self[0] - delta, self[1] + delta]) + } + + /// Expand a rect by a certain amount on top/bottom and on left/right + #[must_use] + pub fn intersects(&self, other: Self) -> bool { + let [mina, maxa] = [self[0].min(self[1]), self[0].max(self[1])]; + let [minb, maxb] = [other[0].min(other[1]), other[0].max(other[1])]; + mina.x <= maxb.x && minb.x <= maxa.x && mina.y <= maxb.y && minb.y <= maxa.y + } + + /// Does this rect contain a point + #[must_use] + pub fn contains(&self, p: DVec2) -> bool { + (self[0].x < p.x && p.x < self[1].x) && (self[0].y < p.y && p.y < self[1].y) + } + + #[must_use] + pub fn min(&self) -> DVec2 { + self.0[0].min(self.0[1]) + } + + #[must_use] + pub fn max(&self) -> DVec2 { + self.0[0].max(self.0[1]) + } + + #[must_use] + pub fn translate(&self, offset: DVec2) -> Self { + Self([self.0[0] + offset, self.0[1] + offset]) + } +} + +impl core::ops::Mul for DAffine2 { + type Output = super::Quad; + + fn mul(self, rhs: Rect) -> Self::Output { + self * super::Quad::from_box(rhs.0) + } +} + +impl core::ops::Index for Rect { + type Output = DVec2; + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} +impl core::ops::IndexMut for Rect { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.0[index] + } +} + +impl Into for Rect { + fn into(self) -> Quad { + Quad::from_box(self.0) + } +} diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index d65bc7c8..5859d7de 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -39,6 +39,34 @@ macro_rules! create_ids { create_ids! { PointId, SegmentId, RegionId, StrokeId, FillId } +/// A no-op hasher that allows writing u64s (the id type). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct NoHash(Option); + +impl core::hash::Hasher for NoHash { + fn finish(&self) -> u64 { + self.0.unwrap() + } + fn write(&mut self, _bytes: &[u8]) { + unimplemented!() + } + fn write_u64(&mut self, i: u64) { + debug_assert!(self.0.is_none()); + self.0 = Some(i) + } +} + +/// A hash builder that builds the [`NoHash`] hasher. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct NoHashBuilder; + +impl core::hash::BuildHasher for NoHashBuilder { + type Hasher = NoHash; + fn build_hasher(&self) -> Self::Hasher { + NoHash::default() + } +} + #[derive(Clone, Debug, Default, PartialEq, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Stores data which is per-point. Each point is merely a position and can be used in a point cloud or to for a bézier path. In future this will be extendable at runtime with custom attributes.