Add snapping targets for b-box edges and multi-layer spacing distribution (#1793)

* Initial work on aligning bounding boxes

* Work in progress distribution

* Distribution snapping

* Distribution overlays

* Align points and clean up

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
James Lindsay 2024-08-05 06:12:20 +01:00 committed by GitHub
parent 0dbbabe73e
commit cdd179cf10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1333 additions and 300 deletions

View File

@ -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<LayerNodeIdentifier>,
},
SetSnapping {
snapping_enabled: Option<bool>,
bounding_box_snapping: Option<OptionBoundsSnapping>,
geometry_snapping: Option<OptionPointSnapping>,
#[serde(skip)]
closure: Option<for<'a> fn(&'a mut SnappingState) -> &'a mut bool>,
snapping_state: bool,
},
SetViewMode {
view_mode: ViewMode,

View File

@ -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<DocumentMessage, DocumentMessageData<'_>> 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(),
)

View File

@ -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<bool>,
pub corners: Option<bool>,
pub edge_midpoints: Option<bool>,
pub centers: Option<bool>,
}
#[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<bool>,
pub path_intersections: Option<bool>,
pub anchors: Option<bool>,
pub line_midpoints: Option<bool>,
pub normals: Option<bool>,
pub tangents: Option<bool>,
}
#[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(_))
}
}

View File

@ -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<Item = ManipulatorPointId> + '_ {
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

View File

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

View File

@ -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<SnapCandidatePoint>,
}
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<SnappedPoint> = None;
let mut snap_y: Option<SnappedPoint> = 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);
}
}
}

View File

@ -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<Rect>,
left: Vec<Rect>,
down: Vec<Rect>,
up: Vec<Rect>,
}
#[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<Rect>) {
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<Rect> {
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<Rect>)> {
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<DistributionMatch>, VecDeque<Rect>) {
if rectangles.is_empty() {
return (None, VecDeque::new());
}
let mut best: Option<(DistributionMatch, Rect, VecDeque<Rect>)> = 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<SnappedPoint> = None;
let mut snap_y: Option<SnappedPoint> = 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<SnappedPoint>, 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<SnappedPoint>, 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<Rect>) {
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<Rect>) {
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<Rect>, 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);
}

View File

@ -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<Quad>,
pub neighbors: Vec<DVec2>,
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<Quad>) -> Self {
pub fn new_quad(document_point: DVec2, source: SnapSource, target: SnapTarget, quad: Option<Quad>, 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<SnapCandidatePoint>, values: BBoxSnapValues, document: &DocumentMessageHandler) {
@ -375,14 +395,14 @@ pub fn get_bbox_points(quad: Quad, points: &mut Vec<SnapCandidatePoint>, 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));
}
}

View File

@ -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<Quad>,
pub source_bounds: Option<Quad>,
pub curves: [Option<Bezier>; 2],
pub distance: f64,
pub tolerance: f64,
pub distribution_boxes_x: VecDeque<Rect>,
pub distribution_equal_distance_x: Option<f64>,
pub distribution_boxes_y: VecDeque<Rect>,
pub distribution_equal_distance_y: Option<f64>,
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<DVec2>,
pub alignment_target_y: Option<DVec2>,
}
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<Rect>, 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()

View File

@ -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) {

View File

@ -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<ClosestSegment>,
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<Message>) {
fn start_dragging_point(
&mut self,
selected_points: SelectedPointsInfo,
input: &InputPreprocessorMessageHandler,
document: &DocumentMessageHandler,
shape_editor: &mut ShapeState,
responses: &mut VecDeque<Message>,
) {
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<Message>) {
// 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);

View File

@ -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};

View File

@ -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]]]

View File

@ -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<Item = DVec2>) -> Option<Self> {
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<Item = bezier_rs::Bezier> + '_ {
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::<DVec2>() / 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<Rect> for DAffine2 {
type Output = super::Quad;
fn mul(self, rhs: Rect) -> Self::Output {
self * super::Quad::from_box(rhs.0)
}
}
impl core::ops::Index<usize> for Rect {
type Output = DVec2;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl core::ops::IndexMut<usize> for Rect {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.0[index]
}
}
impl Into<Quad> for Rect {
fn into(self) -> Quad {
Quad::from_box(self.0)
}
}

View File

@ -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<u64>);
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.