mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 10:08:11 +08:00
Add snapping targets for b-box edges and multi-layer spacing distribution (#1793)
* Initial work on aligning bounding boxes * Work in progress distribution * Distribution snapping * Distribution overlays * Align points and clean up * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
0dbbabe73e
commit
cdd179cf10
@@ -1,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(_))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user