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-04 22:12:20 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent 0dbbabe73e
commit cdd179cf10
15 changed files with 1333 additions and 300 deletions
@@ -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,
@@ -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(),
)
@@ -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(_))
}
}