Improve snapping with better snap target names, tooltips, cleaner overlay labels, code cleanup

This commit is contained in:
Keavon Chambers 2025-01-09 00:47:12 -08:00
parent ae2637e08e
commit 07601a5c6c
12 changed files with 422 additions and 206 deletions

View file

@ -3,7 +3,7 @@ use super::node_graph::utility_types::Transform;
use super::overlays::utility_types::Pivot;
use super::utility_types::clipboards::Clipboard;
use super::utility_types::error::EditorError;
use super::utility_types::misc::{SnappingOptions, SnappingState, GET_SNAP_BOX_FUNCTIONS, GET_SNAP_GEOMETRY_FUNCTIONS};
use super::utility_types::misc::{SnappingOptions, SnappingState, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS};
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH};
@ -1779,28 +1779,27 @@ impl DocumentMessageHandler {
},
]
.into_iter()
.chain(GET_SNAP_BOX_FUNCTIONS.into_iter().map(|(name, closure)| LayoutGroup::Row {
.chain(SNAP_FUNCTIONS_FOR_BOUNDING_BOXES.into_iter().map(|(name, closure, tooltip)| 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())
.tooltip(tooltip)
.widget_holder(),
TextLabel::new(name).widget_holder(),
TextLabel::new(name).tooltip(tooltip).widget_holder(),
],
}))
.chain(
[LayoutGroup::Row {
widgets: vec![TextLabel::new(SnappingOptions::Geometry.to_string()).widget_holder()],
}]
.into_iter()
.chain(GET_SNAP_GEOMETRY_FUNCTIONS.into_iter().map(|(name, closure)| LayoutGroup::Row {
widgets: vec![
.chain([LayoutGroup::Row {
widgets: vec![TextLabel::new(SnappingOptions::Paths.to_string()).widget_holder()],
}])
.chain(SNAP_FUNCTIONS_FOR_PATHS.into_iter().map(|(name, closure, tooltip)| LayoutGroup::Row {
widgets: vec![
CheckboxInput::new(*closure(&mut snapping_state2))
.on_update(move |input: &CheckboxInput| DocumentMessage::SetSnapping { closure: Some(closure), snapping_state: input.checked }.into())
.tooltip(tooltip)
.widget_holder(),
TextLabel::new(name).widget_holder(),
TextLabel::new(name).tooltip(tooltip).widget_holder(),
],
})),
)
}))
.collect(),
)
.widget_holder(),

View file

@ -62,11 +62,11 @@ impl DocumentMode {
pub struct SnappingState {
pub snapping_enabled: bool,
pub grid_snapping: bool,
pub bounds: BoundsSnapping,
pub nodes: PointSnapping,
pub grid: GridSnapping,
pub tolerance: f64,
pub artboards: bool,
pub tolerance: f64,
pub bounding_box: BoundingBoxSnapping,
pub path: PathSnapping,
pub grid: GridSnapping,
}
impl Default for SnappingState {
@ -74,11 +74,11 @@ impl Default for SnappingState {
Self {
snapping_enabled: true,
grid_snapping: false,
bounds: Default::default(),
nodes: Default::default(),
grid: Default::default(),
tolerance: 8.,
artboards: true,
tolerance: 8.,
bounding_box: BoundingBoxSnapping::default(),
path: PathSnapping::default(),
grid: GridSnapping::default(),
}
}
}
@ -89,26 +89,25 @@ impl SnappingState {
return false;
}
match target {
SnapTarget::BoundingBox(bounding_box) => match bounding_box {
BoundingBoxSnapTarget::Corner => self.bounds.corners,
BoundingBoxSnapTarget::Edge => self.bounds.edges,
BoundingBoxSnapTarget::EdgeMidpoint => self.bounds.edge_midpoints,
BoundingBoxSnapTarget::Center => self.bounds.centers,
SnapTarget::BoundingBox(target) => match target {
BoundingBoxSnapTarget::CornerPoint => self.bounding_box.corner_point,
BoundingBoxSnapTarget::AlongEdge => self.bounding_box.along_edge,
BoundingBoxSnapTarget::EdgeMidpoint => self.bounding_box.edge_midpoint,
BoundingBoxSnapTarget::CenterPoint => self.bounding_box.center_point,
},
SnapTarget::Geometry(nodes) => match nodes {
GeometrySnapTarget::AnchorWithColinearHandles => self.nodes.anchors,
GeometrySnapTarget::AnchorWithFreeHandles => self.nodes.anchors,
GeometrySnapTarget::LineMidpoint => self.nodes.line_midpoints,
GeometrySnapTarget::Path => self.nodes.paths,
GeometrySnapTarget::Normal => self.nodes.normals,
GeometrySnapTarget::Tangent => self.nodes.tangents,
GeometrySnapTarget::Intersection => self.nodes.path_intersections,
SnapTarget::Path(target) => match target {
PathSnapTarget::AnchorPointWithColinearHandles | PathSnapTarget::AnchorPointWithFreeHandles => self.path.anchor_point,
PathSnapTarget::LineMidpoint => self.path.line_midpoint,
PathSnapTarget::AlongPath => self.path.along_path,
PathSnapTarget::NormalToPath => self.path.normal_to_path,
PathSnapTarget::TangentToPath => self.path.tangent_to_path,
PathSnapTarget::IntersectionPoint => self.path.path_intersection_point,
},
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,
SnapTarget::Alignment(AlignmentSnapTarget::AlignWithAnchorPoint) => self.path.align_with_anchor_point,
SnapTarget::Alignment(_) => self.bounding_box.align_with_corner_point,
SnapTarget::DistributeEvenly(_) => self.bounding_box.distribute_evenly,
_ => false,
}
}
@ -116,50 +115,50 @@ impl SnappingState {
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct BoundsSnapping {
pub edges: bool,
pub corners: bool,
pub edge_midpoints: bool,
pub centers: bool,
pub align: bool,
pub distribute: bool,
pub struct BoundingBoxSnapping {
pub center_point: bool,
pub corner_point: bool,
pub edge_midpoint: bool,
pub along_edge: bool,
pub align_with_corner_point: bool,
pub distribute_evenly: bool,
}
impl Default for BoundsSnapping {
impl Default for BoundingBoxSnapping {
fn default() -> Self {
Self {
edges: true,
corners: true,
edge_midpoints: false,
centers: true,
align: true,
distribute: true,
center_point: true,
corner_point: true,
edge_midpoint: true,
along_edge: true,
align_with_corner_point: true,
distribute_evenly: true,
}
}
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct PointSnapping {
pub paths: bool,
pub path_intersections: bool,
pub anchors: bool,
pub line_midpoints: bool,
pub normals: bool,
pub tangents: bool,
pub align: bool,
pub struct PathSnapping {
pub anchor_point: bool,
pub line_midpoint: bool,
pub along_path: bool,
pub normal_to_path: bool,
pub tangent_to_path: bool,
pub path_intersection_point: bool,
pub align_with_anchor_point: bool, // TODO: Rename
}
impl Default for PointSnapping {
impl Default for PathSnapping {
fn default() -> Self {
Self {
paths: true,
path_intersections: true,
anchors: true,
line_midpoints: true,
normals: true,
tangents: true,
align: false,
anchor_point: true,
line_midpoint: true,
along_path: true,
normal_to_path: true,
tangent_to_path: true,
path_intersection_point: true,
align_with_anchor_point: true,
}
}
}
@ -265,34 +264,75 @@ impl GridSnapping {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundingBoxSnapSource {
Center,
Corner,
CornerPoint,
CenterPoint,
EdgeMidpoint,
}
impl fmt::Display for BoundingBoxSnapSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BoundingBoxSnapSource::CornerPoint => write!(f, "Bounding Box: Corner Point"),
BoundingBoxSnapSource::CenterPoint => write!(f, "Bounding Box: Center Point"),
BoundingBoxSnapSource::EdgeMidpoint => write!(f, "Bounding Box: Edge Midpoint"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtboardSnapSource {
Center,
Corner,
CornerPoint,
CenterPoint,
}
impl fmt::Display for ArtboardSnapSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArtboardSnapSource::CornerPoint => write!(f, "Artboard: Corner Point"),
ArtboardSnapSource::CenterPoint => write!(f, "Artboard: Center Point"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeometrySnapSource {
AnchorWithColinearHandles,
AnchorWithFreeHandles,
Handle,
pub enum PathSnapSource {
AnchorPointWithColinearHandles,
AnchorPointWithFreeHandles,
HandlePoint,
LineMidpoint,
Intersection,
IntersectionPoint,
}
impl fmt::Display for PathSnapSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PathSnapSource::AnchorPointWithColinearHandles | PathSnapSource::AnchorPointWithFreeHandles => write!(f, "Path: Anchor Point"),
PathSnapSource::HandlePoint => write!(f, "Path: Handle Point"),
PathSnapSource::LineMidpoint => write!(f, "Path: Line Midpoint"),
PathSnapSource::IntersectionPoint => write!(f, "Path: Intersection Point"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlignmentSnapSource {
BoundsCorner,
BoundsCenter,
BoundsEdgeMidpoint,
ArtboardCorner,
ArtboardCenter,
Handle,
BoundingBoxCornerPoint,
BoundingBoxCenterPoint,
BoundingBoxEdgeMidpoint,
ArtboardCornerPoint,
ArtboardCenterPoint,
}
impl fmt::Display for AlignmentSnapSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AlignmentSnapSource::BoundingBoxCornerPoint => write!(f, "{}", BoundingBoxSnapSource::CornerPoint),
AlignmentSnapSource::BoundingBoxCenterPoint => write!(f, "{}", BoundingBoxSnapSource::CenterPoint),
AlignmentSnapSource::BoundingBoxEdgeMidpoint => write!(f, "{}", BoundingBoxSnapSource::EdgeMidpoint),
AlignmentSnapSource::ArtboardCornerPoint => write!(f, "{}", ArtboardSnapSource::CornerPoint),
AlignmentSnapSource::ArtboardCenterPoint => write!(f, "{}", ArtboardSnapSource::CenterPoint),
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
@ -301,7 +341,7 @@ pub enum SnapSource {
None,
BoundingBox(BoundingBoxSnapSource),
Artboard(ArtboardSnapSource),
Geometry(GeometrySnapSource),
Path(PathSnapSource),
Alignment(AlignmentSnapSource),
}
@ -318,54 +358,164 @@ impl SnapSource {
pub fn center(&self) -> bool {
matches!(
self,
Self::Alignment(AlignmentSnapSource::ArtboardCenter | AlignmentSnapSource::BoundsCenter) | Self::Artboard(ArtboardSnapSource::Center) | Self::BoundingBox(BoundingBoxSnapSource::Center)
Self::Alignment(AlignmentSnapSource::ArtboardCenterPoint | AlignmentSnapSource::BoundingBoxCenterPoint)
| Self::Artboard(ArtboardSnapSource::CenterPoint)
| Self::BoundingBox(BoundingBoxSnapSource::CenterPoint)
)
}
}
impl fmt::Display for SnapSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SnapSource::None => write!(f, "None"),
SnapSource::BoundingBox(bounding_box_snap_source) => write!(f, "{bounding_box_snap_source}"),
SnapSource::Artboard(artboard_snap_source) => write!(f, "{artboard_snap_source}"),
SnapSource::Path(path_snap_source) => write!(f, "{path_snap_source}"),
SnapSource::Alignment(alignment_snap_source) => write!(f, "{alignment_snap_source}"),
}
}
}
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 SNAP_FUNCTIONS_FOR_BOUNDING_BOXES: [(&str, GetSnapState, &str); 6] = [
(
// TODO: Rename to "Beyond Edges" and update behavior to snap to an infinite extension of the bounding box edges
// TODO: (even when the layer is locally rotated) instead of horizontally/vertically aligning with the corner points
"Align with Corner Points",
(|snapping_state| &mut snapping_state.bounding_box.align_with_corner_point) as GetSnapState,
"Snaps to horizontal/vertical alignment with the corner points of any layer's bounding box",
),
(
"Corner Points",
(|snapping_state| &mut snapping_state.bounding_box.corner_point) as GetSnapState,
"Snaps to the four corners of any layer's bounding box",
),
(
"Center Points",
(|snapping_state| &mut snapping_state.bounding_box.center_point) as GetSnapState,
"Snaps to the center point of any layer's bounding box",
),
(
"Edge Midpoints",
(|snapping_state| &mut snapping_state.bounding_box.edge_midpoint) as GetSnapState,
"Snaps to any of the four points at the middle of the edges of any layer's bounding box",
),
(
"Along Edges",
(|snapping_state| &mut snapping_state.bounding_box.along_edge) as GetSnapState,
"Snaps anywhere along the four edges of any layer's bounding box",
),
(
"Distribute Evenly",
(|snapping_state| &mut snapping_state.bounding_box.distribute_evenly) as GetSnapState,
// TODO: Fix the bug/limitation that requires 'Center Points' and 'Corner Points' to be enabled
"Snaps to a consistent distance offset established by the bounding boxes of nearby layers\n(due to a bug, 'Center Points' and 'Corner Points' must be enabled)",
),
];
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),
pub const SNAP_FUNCTIONS_FOR_PATHS: [(&str, GetSnapState, &str); 7] = [
(
"Align with Anchor Points",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.align_with_anchor_point) as GetSnapState,
"Snaps to horizontal/vertical alignment with the anchor points of any vector path",
),
(
"Anchor Points",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.anchor_point) as GetSnapState,
"Snaps to the anchor point of any vector path",
),
(
// TODO: Extend to the midpoints of curved segments and rename to "Segment Midpoint"
"Line Midpoints",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.line_midpoint) as GetSnapState,
"Snaps to the point at the middle of any straight line segment of a vector path",
),
(
"Path Intersection Points",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.path_intersection_point) as GetSnapState,
"Snaps to any points where vector paths intersect",
),
(
"Along Paths",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.along_path) as GetSnapState,
"Snaps along the length of any vector path",
),
(
// TODO: This works correctly for line segments, but not curved segments.
// TODO: Therefore, we should make this use the normal in relation to the incoming curve, not the straight line between the incoming curve's start point and the path.
"Normal to Paths",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.normal_to_path) as GetSnapState,
// TODO: Fix the bug/limitation that requires 'Intersections of Paths' to be enabled
"Snaps a line to a point perpendicular to a vector path\n(due to a bug, 'Intersections of Paths' must be enabled)",
),
(
// TODO: This works correctly for line segments, but not curved segments.
// TODO: Therefore, we should make this use the tangent in relation to the incoming curve, not the straight line between the incoming curve's start point and the path.
"Tangent to Paths",
(|snapping_state: &mut SnappingState| &mut snapping_state.path.tangent_to_path) as GetSnapState,
// TODO: Fix the bug/limitation that requires 'Intersections of Paths' to be enabled
"Snaps a line to a point tangent to a vector path\n(due to a bug, 'Intersections of Paths' must be enabled)",
),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum BoundingBoxSnapTarget {
Center,
Corner,
Edge,
CornerPoint,
CenterPoint,
EdgeMidpoint,
AlongEdge,
}
impl fmt::Display for BoundingBoxSnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BoundingBoxSnapTarget::CornerPoint => write!(f, "Bounding Box: Corner Point"),
BoundingBoxSnapTarget::CenterPoint => write!(f, "Bounding Box: Center Point"),
BoundingBoxSnapTarget::EdgeMidpoint => write!(f, "Bounding Box: Edge Midpoint"),
BoundingBoxSnapTarget::AlongEdge => write!(f, "Bounding Box: Along Edge"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum GeometrySnapTarget {
AnchorWithColinearHandles,
AnchorWithFreeHandles,
pub enum PathSnapTarget {
AnchorPointWithColinearHandles,
AnchorPointWithFreeHandles,
LineMidpoint,
Path,
Normal,
Tangent,
Intersection,
AlongPath,
NormalToPath,
TangentToPath,
IntersectionPoint,
}
impl fmt::Display for PathSnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PathSnapTarget::AnchorPointWithColinearHandles | PathSnapTarget::AnchorPointWithFreeHandles => write!(f, "Path: Anchor Point"),
PathSnapTarget::LineMidpoint => write!(f, "Path: Line Midpoint"),
PathSnapTarget::AlongPath => write!(f, "Path: Along Path"),
PathSnapTarget::NormalToPath => write!(f, "Path: Normal to Path"),
PathSnapTarget::TangentToPath => write!(f, "Path: Tangent to Path"),
PathSnapTarget::IntersectionPoint => write!(f, "Path: Intersection Point"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtboardSnapTarget {
Edge,
Corner,
Center,
CornerPoint,
CenterPoint,
AlongEdge,
}
impl fmt::Display for ArtboardSnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ArtboardSnapTarget::CornerPoint => write!(f, "Artboard: Corner Point"),
ArtboardSnapTarget::CenterPoint => write!(f, "Artboard: Center Point"),
ArtboardSnapTarget::AlongEdge => write!(f, "Artboard: Along Edge"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -375,14 +525,37 @@ pub enum GridSnapTarget {
Intersection,
}
impl fmt::Display for GridSnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GridSnapTarget::Line => write!(f, "Grid: Along Line"),
GridSnapTarget::LineNormal => write!(f, "Grid: Normal to Line"),
GridSnapTarget::Intersection => write!(f, "Grid: Intersection Point"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlignmentSnapTarget {
BoundsCorner,
BoundsCenter,
ArtboardCorner,
ArtboardCenter,
Handle,
Intersection,
BoundingBoxCornerPoint,
BoundingBoxCenterPoint,
ArtboardCornerPoint,
ArtboardCenterPoint,
AlignWithAnchorPoint,
IntersectionPoint,
}
impl fmt::Display for AlignmentSnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AlignmentSnapTarget::BoundingBoxCornerPoint => write!(f, "{}", BoundingBoxSnapTarget::CornerPoint),
AlignmentSnapTarget::BoundingBoxCenterPoint => write!(f, "{}", BoundingBoxSnapTarget::CenterPoint),
AlignmentSnapTarget::ArtboardCornerPoint => write!(f, "{}", ArtboardSnapTarget::CornerPoint),
AlignmentSnapTarget::ArtboardCenterPoint => write!(f, "{}", ArtboardSnapTarget::CenterPoint),
AlignmentSnapTarget::AlignWithAnchorPoint => write!(f, "{}", PathSnapTarget::AnchorPointWithColinearHandles),
AlignmentSnapTarget::IntersectionPoint => write!(f, "{}", PathSnapTarget::IntersectionPoint),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -393,7 +566,21 @@ pub enum DistributionSnapTarget {
Left,
Up,
Down,
Xy,
XY,
}
impl fmt::Display for DistributionSnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DistributionSnapTarget::X => write!(f, "Distribute: X"),
DistributionSnapTarget::Y => write!(f, "Distribute: Y"),
DistributionSnapTarget::Right => write!(f, "Distribute: Right"),
DistributionSnapTarget::Left => write!(f, "Distribute: Left"),
DistributionSnapTarget::Up => write!(f, "Distribute: Up"),
DistributionSnapTarget::Down => write!(f, "Distribute: Down"),
DistributionSnapTarget::XY => write!(f, "Distribute: XY"),
}
}
}
impl DistributionSnapTarget {
@ -410,11 +597,11 @@ pub enum SnapTarget {
#[default]
None,
BoundingBox(BoundingBoxSnapTarget),
Geometry(GeometrySnapTarget),
Path(PathSnapTarget),
Artboard(ArtboardSnapTarget),
Grid(GridSnapTarget),
Alignment(AlignmentSnapTarget),
Distribution(DistributionSnapTarget),
DistributeEvenly(DistributionSnapTarget),
}
impl SnapTarget {
@ -426,17 +613,31 @@ impl SnapTarget {
}
}
impl fmt::Display for SnapTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SnapTarget::None => write!(f, "None"),
SnapTarget::BoundingBox(bounding_box_snap_target) => write!(f, "{bounding_box_snap_target}"),
SnapTarget::Path(path_snap_target) => write!(f, "{path_snap_target}"),
SnapTarget::Artboard(artboard_snap_target) => write!(f, "{artboard_snap_target}"),
SnapTarget::Grid(grid_snap_target) => write!(f, "{grid_snap_target}"),
SnapTarget::Alignment(alignment_snap_target) => write!(f, "{alignment_snap_target}"),
SnapTarget::DistributeEvenly(distribution_snap_target) => write!(f, "{distribution_snap_target}"),
}
}
}
// TODO: implement icons for SnappingOptions eventually
pub enum SnappingOptions {
BoundingBoxes,
Geometry,
Paths,
}
impl fmt::Display for SnappingOptions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SnappingOptions::BoundingBoxes => write!(f, "Bounding Boxes"),
SnappingOptions::Geometry => write!(f, "Geometry"),
SnappingOptions::Paths => write!(f, "Paths"),
}
}
}

View file

@ -1,7 +1,7 @@
use super::graph_modification_utils;
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::misc::{PathSnapSource, SnapSource};
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::snapping::SnapTypeConfiguration;
@ -193,9 +193,10 @@ impl ShapeState {
for &selected in &state.selected_points {
let source = match selected {
ManipulatorPointId::Anchor(_) if vector_data.colinear(selected) => SnapSource::Geometry(GeometrySnapSource::AnchorWithColinearHandles),
ManipulatorPointId::Anchor(_) => SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles),
_ => SnapSource::Geometry(GeometrySnapSource::Handle),
ManipulatorPointId::Anchor(_) if vector_data.colinear(selected) => SnapSource::Path(PathSnapSource::AnchorPointWithColinearHandles),
ManipulatorPointId::Anchor(_) => SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles),
// TODO: This doesn't actually work for handles, instead handles enter the arm above for free handles
ManipulatorPointId::PrimaryHandle(_) | ManipulatorPointId::EndHandle(_) => SnapSource::Path(PathSnapSource::HandlePoint),
};
let Some(position) = selected.get_position(&vector_data) else { continue };

View file

@ -8,7 +8,7 @@ pub use {alignment_snapper::*, distribution_snapper::*, grid_snapper::*, layer_s
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_SNAP_BACKGROUND, COLOR_OVERLAY_WHITE};
use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, Pivot};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, GridSnapTarget, SnapTarget};
use crate::messages::portfolio::document::utility_types::misc::{BoundingBoxSnapTarget, GridSnapTarget, PathSnapTarget, SnapTarget};
use crate::messages::prelude::*;
use bezier_rs::{Subpath, TValue};
@ -23,7 +23,7 @@ use std::cmp::Ordering;
/// Configuration for the relevant snap type
#[derive(Debug, Clone, Copy, Default)]
pub struct SnapTypeConfiguration {
pub only_geometry: bool,
pub only_path: bool,
pub use_existing_candidates: bool,
pub accept_distribution: bool,
pub bbox: Option<Rect>,
@ -111,7 +111,7 @@ fn get_closest_point(points: Vec<SnappedPoint>) -> Option<SnappedPoint> {
(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) };
let SnapTarget::DistributeEvenly(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;
@ -126,7 +126,7 @@ fn get_closest_point(points: Vec<SnappedPoint>) -> Option<SnappedPoint> {
}
}
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);
let keep_curve = |curve: &&SnappedCurve| !exclude_paths || curve.point.target != SnapTarget::Path(PathSnapTarget::AlongPath);
curves.iter().filter(keep_curve).map(|curve| &curve.point).min_by(compare_points)
}
fn get_closest_line(lines: &[SnappedLine]) -> Option<&SnappedPoint> {
@ -135,12 +135,12 @@ fn get_closest_line(lines: &[SnappedLine]) -> Option<&SnappedPoint> {
fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<SnappedPoint> {
let mut best = None;
for curve_i in curves {
if curve_i.point.target == SnapTarget::BoundingBox(BoundingBoxSnapTarget::Edge) {
if curve_i.point.target == SnapTarget::BoundingBox(BoundingBoxSnapTarget::AlongEdge) {
continue;
}
for curve_j in curves {
if curve_j.point.target == SnapTarget::BoundingBox(BoundingBoxSnapTarget::Edge) {
if curve_j.point.target == SnapTarget::BoundingBox(BoundingBoxSnapTarget::AlongEdge) {
continue;
}
if curve_i.start == curve_j.start && curve_i.layer == curve_j.layer {
@ -156,7 +156,7 @@ fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option<S
best = Some(SnappedPoint {
snapped_point_document,
distance,
target: SnapTarget::Geometry(GeometrySnapTarget::Intersection),
target: SnapTarget::Path(PathSnapTarget::IntersectionPoint),
tolerance: close.point.tolerance,
curves: [Some(close.document_curve), Some(far.document_curve)],
source: close.point.source,
@ -262,7 +262,7 @@ impl SnapManager {
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));
let exclude_paths = !document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AlongPath));
if let Some(closest_curve) = get_closest_curve(&snap_results.curves, exclude_paths) {
snapped_points.push(closest_curve.clone());
}
@ -274,7 +274,7 @@ impl SnapManager {
}
if !constrained {
if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Intersection)) {
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::IntersectionPoint)) {
if let Some(closest_curves_intersection) = get_closest_intersection(point.document_point, &snap_results.curves) {
snapped_points.push(closest_curves_intersection);
}
@ -287,7 +287,7 @@ impl SnapManager {
}
if to_path {
snapped_points.retain(|i| matches!(i.target, SnapTarget::Geometry(_)));
snapped_points.retain(|i| matches!(i.target, SnapTarget::Path(_)));
}
let mut best_point = None;
@ -382,7 +382,7 @@ impl SnapManager {
self.alignment_snapper.free_snap(&mut snap_data, point, &mut snap_results, config);
self.distribution_snapper.free_snap(&mut snap_data, point, &mut snap_results, config);
Self::find_best_snap(&mut snap_data, point, snap_results, false, false, config.only_geometry)
Self::find_best_snap(&mut snap_data, point, snap_results, false, false, config.only_path)
}
pub fn constrained_snap(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, constraint: SnapConstraint, config: SnapTypeConfiguration) -> SnappedPoint {
@ -408,7 +408,7 @@ impl SnapManager {
self.alignment_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint, config);
self.distribution_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint, config);
Self::find_best_snap(&mut snap_data, point, snap_results, true, false, config.only_geometry)
Self::find_best_snap(&mut snap_data, point, snap_results, true, false, config.only_path)
}
fn alignment_x_overlay(boxes: &VecDeque<Rect>, transform: DAffine2, overlay_context: &mut OverlayContext) {
@ -475,9 +475,9 @@ impl SnapManager {
}
if !any_align && ind.distribution_equal_distance_x.is_none() && ind.distribution_equal_distance_y.is_none() {
let text = format!("{:?} to {:?}", ind.source, ind.target);
let transform = DAffine2::from_translation(viewport - DVec2::new(0., 5.));
overlay_context.text(&text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_SNAP_BACKGROUND), transform, 5., [Pivot::Start, Pivot::End]);
let text = format!("[{}] from [{}]", ind.target, ind.source);
let transform = DAffine2::from_translation(viewport - DVec2::new(0., 4.));
overlay_context.text(&text, COLOR_OVERLAY_WHITE, Some(COLOR_OVERLAY_SNAP_BACKGROUND), transform, 4., [Pivot::Start, Pivot::End]);
overlay_context.square(viewport, Some(4.), Some(COLOR_OVERLAY_BLUE), Some(COLOR_OVERLAY_BLUE));
}
}

View file

@ -19,7 +19,7 @@ impl AlignmentSnapper {
let document = snap_data.document;
self.bounding_box_points.clear();
if !document.snapping_state.bounds.align {
if !document.snapping_state.bounding_box.align_with_corner_point {
return;
}
@ -28,7 +28,7 @@ impl AlignmentSnapper {
continue;
}
if document.snapping_state.target_enabled(SnapTarget::Artboard(ArtboardSnapTarget::Corner)) {
if document.snapping_state.target_enabled(SnapTarget::Artboard(ArtboardSnapTarget::CornerPoint)) {
let Some(bounds) = document.metadata().bounding_box_with_transform(layer, document.metadata().transform_to_document(layer)) else {
continue;
};
@ -52,7 +52,7 @@ impl AlignmentSnapper {
pub fn snap_bbox_points(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint, config: SnapTypeConfiguration) {
self.collect_bounding_box_points(snap_data, !config.use_existing_candidates);
let unselected_geometry = if snap_data.document.snapping_state.target_enabled(SnapTarget::Alignment(AlignmentSnapTarget::Handle)) {
let unselected_geometry = if snap_data.document.snapping_state.target_enabled(SnapTarget::Alignment(AlignmentSnapTarget::AlignWithAnchorPoint)) {
snap_data.node_snap_cache.map(|cache| cache.unselected.as_slice()).unwrap_or(&[])
} else {
&[]
@ -77,9 +77,9 @@ impl AlignmentSnapper {
[DVec2::new(point.document_point.x, target_position.y), DVec2::new(target_position.x, point.document_point.y)].map(Some)
};
let target_geometry = matches!(target_point.target, SnapTarget::Geometry(_));
let updated_target = if target_geometry {
SnapTarget::Alignment(AlignmentSnapTarget::Handle)
let target_path = matches!(target_point.target, SnapTarget::Path(_));
let updated_target = if target_path {
SnapTarget::Alignment(AlignmentSnapTarget::AlignWithAnchorPoint)
} else {
target_point.target
};
@ -90,7 +90,7 @@ impl AlignmentSnapper {
if distance_to_snapped < tolerance && snap_x.as_ref().map_or(true, |point| distance_to_align_target < point.distance_to_align_target) {
snap_x = Some(SnappedPoint {
snapped_point_document: point_on_x,
source: point.source, //ToDo map source
source: point.source, // TODO(0Hypercube): map source
target: updated_target,
target_bounds: target_point.quad,
distance: distance_to_snapped,
@ -109,7 +109,7 @@ impl AlignmentSnapper {
if distance_to_snapped < tolerance && snap_y.as_ref().map_or(true, |point| distance_to_align_target < point.distance_to_align_target) {
snap_y = Some(SnappedPoint {
snapped_point_document: point_on_y,
source: point.source, //ToDo map source
source: point.source, // TODO(0Hypercube): map source
target: updated_target,
target_bounds: target_point.quad,
distance: distance_to_snapped,
@ -137,7 +137,7 @@ impl AlignmentSnapper {
snap_results.points.push(SnappedPoint {
snapped_point_document: intersection,
source: point.source, // TODO: map source
target: SnapTarget::Alignment(AlignmentSnapTarget::Intersection),
target: SnapTarget::Alignment(AlignmentSnapTarget::IntersectionPoint),
target_bounds: snap_x.target_bounds,
distance,
tolerance,
@ -154,22 +154,23 @@ impl AlignmentSnapper {
_ => {}
}
}
pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, config: SnapTypeConfiguration) {
let is_bbox = matches!(point.source, SnapSource::BoundingBox(_));
let is_geometry = matches!(point.source, SnapSource::Geometry(_));
let geometry_selected = snap_data.has_manipulators();
let is_path = matches!(point.source, SnapSource::Path(_));
let path_selected = snap_data.has_manipulators();
if is_bbox || (is_geometry && geometry_selected) || (is_geometry && point.alignment) {
if is_bbox || (is_path && path_selected) || (is_path && point.alignment) {
self.snap_bbox_points(snap_data, point, snap_results, SnapConstraint::None, config);
}
}
pub fn constrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint, config: SnapTypeConfiguration) {
let is_bbox = matches!(point.source, SnapSource::BoundingBox(_));
let is_geometry = matches!(point.source, SnapSource::Geometry(_));
let geometry_selected = snap_data.has_manipulators();
let is_path = matches!(point.source, SnapSource::Path(_));
let path_selected = snap_data.has_manipulators();
if is_bbox || (is_geometry && geometry_selected) || (is_geometry && point.alignment) {
if is_bbox || (is_path && path_selected) || (is_path && point.alignment) {
self.snap_bbox_points(snap_data, point, snap_results, constraint, config);
}
}

View file

@ -210,7 +210,7 @@ impl DistributionSnapper {
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.target = SnapTarget::DistributeEvenly(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();
@ -325,7 +325,7 @@ impl DistributionSnapper {
pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, config: SnapTypeConfiguration) {
let Some(bounds) = config.bbox else { return };
if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::Center) || !snap_data.document.snapping_state.bounds.distribute {
if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::CenterPoint) || !snap_data.document.snapping_state.bounding_box.distribute_evenly {
return;
}
@ -335,7 +335,7 @@ impl DistributionSnapper {
pub fn constrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint, config: SnapTypeConfiguration) {
let Some(bounds) = config.bbox else { return };
if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::Center) || !snap_data.document.snapping_state.bounds.distribute {
if point.source != SnapSource::BoundingBox(BoundingBoxSnapSource::CenterPoint) || !snap_data.document.snapping_state.bounding_box.distribute_evenly {
return;
}

View file

@ -15,7 +15,6 @@ pub struct LayerSnapper {
points_to_snap: Vec<SnapCandidatePoint>,
paths_to_snap: Vec<SnapCandidatePath>,
}
impl LayerSnapper {
pub fn add_layer_bounds(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, target: SnapTarget) {
if !document.snapping_state.target_enabled(target) {
@ -49,6 +48,7 @@ impl LayerSnapper {
});
}
}
pub fn collect_paths(&mut self, snap_data: &mut SnapData, first_point: bool) {
if !first_point {
return;
@ -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::Artboard(ArtboardSnapTarget::Edge));
self.add_layer_bounds(document, layer, SnapTarget::Artboard(ArtboardSnapTarget::AlongEdge));
}
for &layer in snap_data.get_candidates() {
let transform = document.metadata().transform_to_document(layer);
@ -68,8 +68,7 @@ impl LayerSnapper {
continue;
}
if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Intersection)) || document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Path))
{
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::IntersectionPoint)) || document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AlongPath)) {
for subpath in document.metadata().layer_outline(layer) {
for (start_index, curve) in subpath.iter().enumerate() {
let document_curve = curve.apply_transformation(|p| transform.transform_point2(p));
@ -81,23 +80,24 @@ impl LayerSnapper {
document_curve,
layer,
start,
target: SnapTarget::Geometry(GeometrySnapTarget::Path),
target: SnapTarget::Path(PathSnapTarget::AlongPath),
bounds: None,
});
}
}
}
if !snap_data.ignore_bounds(layer) {
self.add_layer_bounds(document, layer, SnapTarget::BoundingBox(BoundingBoxSnapTarget::Edge));
self.add_layer_bounds(document, layer, SnapTarget::BoundingBox(BoundingBoxSnapTarget::AlongEdge));
}
}
}
pub fn free_snap_paths(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, config: SnapTypeConfiguration) {
self.collect_paths(snap_data, !config.use_existing_candidates);
let document = snap_data.document;
let normals = document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Normal));
let tangents = document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Tangent));
let normals = document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::NormalToPath));
let tangents = document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::TangentToPath));
let tolerance = snap_tolerance(document);
for path in &self.paths_to_snap {
@ -187,7 +187,7 @@ impl LayerSnapper {
return;
}
if document.snapping_state.target_enabled(SnapTarget::Artboard(ArtboardSnapTarget::Corner)) {
if document.snapping_state.target_enabled(SnapTarget::Artboard(ArtboardSnapTarget::CornerPoint)) {
let Some(bounds) = document
.network_interface
.document_metadata()
@ -217,6 +217,7 @@ impl LayerSnapper {
get_bbox_points(quad, &mut self.points_to_snap, values, document);
}
}
pub fn snap_anchors(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, c: SnapConstraint, constrained_point: DVec2) {
let mut best = None;
for candidate in &self.points_to_snap {
@ -251,6 +252,7 @@ impl LayerSnapper {
snap_results.points.push(result);
}
}
pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, config: SnapTypeConfiguration) {
self.collect_anchors(snap_data, !config.use_existing_candidates);
self.snap_anchors(snap_data, point, snap_results, SnapConstraint::None, point.document_point);
@ -275,7 +277,7 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
}
snap_results.points.push(SnappedPoint {
snapped_point_document: normal_point,
target: SnapTarget::Geometry(GeometrySnapTarget::Normal),
target: SnapTarget::Path(PathSnapTarget::NormalToPath),
distance,
tolerance,
curves: [Some(path.document_curve), None],
@ -296,7 +298,7 @@ fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool,
}
snap_results.points.push(SnappedPoint {
snapped_point_document: tangent_point,
target: SnapTarget::Geometry(GeometrySnapTarget::Tangent),
target: SnapTarget::Path(PathSnapTarget::TangentToPath),
distance,
tolerance,
curves: [Some(path.document_curve), None],
@ -317,6 +319,7 @@ struct SnapCandidatePath {
target: SnapTarget,
bounds: Option<Quad>,
}
#[derive(Clone, Debug, Default)]
pub struct SnapCandidatePoint {
pub document_point: DVec2,
@ -330,6 +333,7 @@ impl SnapCandidatePoint {
pub fn new(document_point: DVec2, source: SnapSource, target: SnapTarget) -> Self {
Self::new_quad(document_point, source, target, None, true)
}
pub fn new_quad(document_point: DVec2, source: SnapSource, target: SnapTarget, quad: Option<Quad>, alignment: bool) -> Self {
Self {
document_point,
@ -340,18 +344,22 @@ impl SnapCandidatePoint {
..Default::default()
}
}
pub fn new_source(document_point: DVec2, source: SnapSource) -> Self {
Self::new(document_point, source, SnapTarget::None)
}
pub fn handle(document_point: DVec2) -> Self {
Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles))
Self::new_source(document_point, SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles))
}
pub fn handle_neighbors(document_point: DVec2, neighbors: impl Into<Vec<DVec2>>) -> Self {
let mut point = Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles));
let mut point = Self::new_source(document_point, SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles));
point.neighbors = neighbors.into();
point
}
}
#[derive(Default)]
pub struct BBoxSnapValues {
corner_source: SnapSource,
@ -363,41 +371,42 @@ pub struct BBoxSnapValues {
}
impl BBoxSnapValues {
pub const BOUNDING_BOX: Self = Self {
corner_source: SnapSource::BoundingBox(BoundingBoxSnapSource::Corner),
corner_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::Corner),
corner_source: SnapSource::BoundingBox(BoundingBoxSnapSource::CornerPoint),
corner_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::CornerPoint),
edge_source: SnapSource::BoundingBox(BoundingBoxSnapSource::EdgeMidpoint),
edge_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::EdgeMidpoint),
center_source: SnapSource::BoundingBox(BoundingBoxSnapSource::Center),
center_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::Center),
center_source: SnapSource::BoundingBox(BoundingBoxSnapSource::CenterPoint),
center_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::CenterPoint),
};
pub const ARTBOARD: Self = Self {
corner_source: SnapSource::Artboard(ArtboardSnapSource::Corner),
corner_target: SnapTarget::Artboard(ArtboardSnapTarget::Corner),
corner_source: SnapSource::Artboard(ArtboardSnapSource::CornerPoint),
corner_target: SnapTarget::Artboard(ArtboardSnapTarget::CornerPoint),
edge_source: SnapSource::None,
edge_target: SnapTarget::None,
center_source: SnapSource::Artboard(ArtboardSnapSource::Center),
center_target: SnapTarget::Artboard(ArtboardSnapTarget::Center),
center_source: SnapSource::Artboard(ArtboardSnapSource::CenterPoint),
center_target: SnapTarget::Artboard(ArtboardSnapTarget::CenterPoint),
};
pub const ALIGN_BOUNDING_BOX: Self = Self {
corner_source: SnapSource::Alignment(AlignmentSnapSource::BoundsCorner),
corner_target: SnapTarget::Alignment(AlignmentSnapTarget::BoundsCorner),
corner_source: SnapSource::Alignment(AlignmentSnapSource::BoundingBoxCornerPoint),
corner_target: SnapTarget::Alignment(AlignmentSnapTarget::BoundingBoxCornerPoint),
edge_source: SnapSource::None,
edge_target: SnapTarget::None,
center_source: SnapSource::Alignment(AlignmentSnapSource::BoundsCenter),
center_target: SnapTarget::Alignment(AlignmentSnapTarget::BoundsCenter),
center_source: SnapSource::Alignment(AlignmentSnapSource::BoundingBoxCenterPoint),
center_target: SnapTarget::Alignment(AlignmentSnapTarget::BoundingBoxCenterPoint),
};
pub const ALIGN_ARTBOARD: Self = Self {
corner_source: SnapSource::Alignment(AlignmentSnapSource::ArtboardCorner),
corner_target: SnapTarget::Alignment(AlignmentSnapTarget::ArtboardCorner),
corner_source: SnapSource::Alignment(AlignmentSnapSource::ArtboardCornerPoint),
corner_target: SnapTarget::Alignment(AlignmentSnapTarget::ArtboardCornerPoint),
edge_source: SnapSource::None,
edge_target: SnapTarget::None,
center_source: SnapSource::Alignment(AlignmentSnapSource::ArtboardCenter),
center_target: SnapTarget::Alignment(AlignmentSnapTarget::ArtboardCenter),
center_source: SnapSource::Alignment(AlignmentSnapSource::ArtboardCenterPoint),
center_target: SnapTarget::Alignment(AlignmentSnapTarget::ArtboardCenterPoint),
};
}
pub fn get_bbox_points(quad: Quad, points: &mut Vec<SnapCandidatePoint>, values: BBoxSnapValues, document: &DocumentMessageHandler) {
for index in 0..4 {
let start = quad.0[index];
@ -409,6 +418,7 @@ pub fn get_bbox_points(quad: Quad, points: &mut Vec<SnapCandidatePoint>, values:
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), false));
}
@ -417,10 +427,12 @@ pub fn get_bbox_points(quad: Quad, points: &mut Vec<SnapCandidatePoint>, values:
fn handle_not_under(to_document: DAffine2) -> impl Fn(&DVec2) -> bool {
move |&offset: &DVec2| to_document.transform_vector2(offset).length_squared() >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE
}
fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<PointId>, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) {
let document = snap_data.document;
// Midpoints of linear segments
if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::LineMidpoint)) {
if document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::LineMidpoint)) {
for (index, curve) in subpath.iter().enumerate() {
if snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[index].id) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(index + 1) % subpath.len()].id) {
continue;
@ -434,12 +446,13 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
if in_handle.is_none() && out_handle.is_none() {
points.push(SnapCandidatePoint::new(
to_document.transform_point2(curve.start() * 0.5 + curve.end * 0.5),
SnapSource::Geometry(GeometrySnapSource::LineMidpoint),
SnapTarget::Geometry(GeometrySnapTarget::LineMidpoint),
SnapSource::Path(PathSnapSource::LineMidpoint),
SnapTarget::Path(PathSnapTarget::LineMidpoint),
));
}
}
}
// Anchors
for (index, group) in subpath.manipulator_groups().iter().enumerate() {
if snap_data.ignore_manipulator(layer, group.id) {
@ -452,19 +465,20 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Poin
let colinear = are_manipulator_handles_colinear(group, to_document, subpath, index);
if colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::AnchorWithColinearHandles)) {
// Colinear handles
// Colinear handles
if colinear && document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithColinearHandles)) {
points.push(SnapCandidatePoint::new(
to_document.transform_point2(group.anchor),
SnapSource::Geometry(GeometrySnapSource::AnchorWithColinearHandles),
SnapTarget::Geometry(GeometrySnapTarget::AnchorWithColinearHandles),
SnapSource::Path(PathSnapSource::AnchorPointWithColinearHandles),
SnapTarget::Path(PathSnapTarget::AnchorPointWithColinearHandles),
));
} else if !colinear && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::AnchorWithFreeHandles)) {
// Free handles
}
// Free handles
else if !colinear && document.snapping_state.target_enabled(SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles)) {
points.push(SnapCandidatePoint::new(
to_document.transform_point2(group.anchor),
SnapSource::Geometry(GeometrySnapSource::AnchorWithFreeHandles),
SnapTarget::Geometry(GeometrySnapTarget::AnchorWithFreeHandles),
SnapSource::Path(PathSnapSource::AnchorPointWithFreeHandles),
SnapTarget::Path(PathSnapTarget::AnchorPointWithFreeHandles),
));
}
}

View file

@ -63,7 +63,7 @@ impl SnappedPoint {
Self {
snapped_point_document: point.document_point + translation,
source: point.source,
target: SnapTarget::Distribution(target),
target: SnapTarget::DistributeEvenly(target),
distribution_boxes_x,
distribution_equal_distance_x: is_x.then_some(distances.equal),
distribution_boxes_y,

View file

@ -1340,7 +1340,7 @@ fn get_selected_faces<'a>(predicate: &'a impl Fn(u8) -> bool, flags: &'a HashMap
fn walk_faces<'a>(faces: &'a [DualVertexKey], edges: &SlotMap<DualEdgeKey, DualGraphHalfEdge>, vertices: &SlotMap<DualVertexKey, DualGraphVertex>) -> impl Iterator<Item = PathSegment> + 'a {
let face_set: HashSet<_> = faces.iter().copied().collect();
// TODO: Try using a binary search to avioid the hashset construction
// TODO: Try using a binary search to avoid the hashset construction
let is_removed_edge = |edge: &DualGraphHalfEdge| face_set.contains(&edge.incident_vertex) == face_set.contains(&edges[edge.twin.unwrap()].incident_vertex);
let mut edge_to_next = HashMap::new();
@ -1481,7 +1481,7 @@ impl Display for BooleanError {
match self {
Self::MultipleOuterFaces => f.write_str("Found multiple candidates for the outer face in a connected component of the dual graph."),
Self::NoEarInPolygon => f.write_str("Failed to compute winding order for one of the faces, this usually happens when the polygon is malformed."),
Self::InvalidPathCommand(cmd) => f.write_fmt(format_args!("Encountered a '{cmd}' while parsing the svg data which was not recogniezed")),
Self::InvalidPathCommand(cmd) => f.write_fmt(format_args!("Encountered a '{cmd}' while parsing the svg data which was not recognized")),
}
}
}

View file

@ -282,7 +282,7 @@ impl Default for VectorData {
}
}
/// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curviture).
/// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature).
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ManipulatorPointId {

View file

@ -55,7 +55,7 @@ The right side of the control bar has controls related to the active document an
| | |
|-|-|
| Overlays | <p>When checked (default), overlays are shown. When unchecked, they are hidden. Overlays are the temporary contextual visualizations (like bounding boxes and vector manipulators) that are usually blue and appear atop the viewport when using tools.</p> |
| Snapping | <p>When checked (default), drawing and dragging shapes and vector points means they will snap to other areas of geometric interest like corners or anchor points. When unchecked, the selection moves freely.<br /><br />Fine-grained options are available by clicking the overflow button to access its options popover menu:</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/snapping-popover__3.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>Snapping options relating to **Bounding Boxes**:</p><p><ul><li>**Box Center** enables snapping to the center point of any layer's bounding box.</li><li>**Box Corner** enables snapping to the four corners of any layer's bounding box.</li><li>**Along Edge** enables snapping anywhere along the four edges of any layer's bounding box.</li><li>**Midpoint of Edge** enables snapping to any of the four points at the middle of the edges of any layer's bounding box.</li><li>**Align to Box** enables snapping to anywhere outside any layer's bounding box along the four lines extending outward from its edges.</li><li>**Evenly Distribute Boxes** enables snapping to a consistent distance offset established by the bounding boxes of nearby layers (due to a bug, **Box Center** and **Box Corner** must be enabled).</li></ul></p><p>Snapping options relating to **Geometry**:</p><p><ul><li>**Anchor** enables snapping to the anchor point of any vector path.</li><li>**Line Midpoint** enables snapping to the point at the middle of any straight line segment of a vector path.</li><li>**Path** enables snapping along the length of any vector path.</li><li>**Normal to Path** enables snapping a line to a point perpendicular to a vector path (due to a bug, **Intersection** must be enabled).</li><li>**Tangent to Path** enables snapping a line to a point tangent to a vector path (due to a bug, **Intersection** must be enabled).</li><li>**Intersection** enables snapping to any points where vector paths intersect.</li><li>**Align to Selected Path** enables snapping to the handle control points of a selected vector layer.</li></ul></p> |
| Snapping | <p>When checked (default), drawing and dragging shapes and vector points means they will snap to other areas of geometric interest like corners or anchor points. When unchecked, the selection moves freely.<br /><br />Fine-grained options are available by clicking the overflow button to access its options popover menu:</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/snapping-popover__4.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>Snapping options relating to **Bounding Boxes**:</p><p><ul><li>**Align with Corner Points**: Snaps to horizontal/vertical alignment with the corner points of any layer's bounding box.</li><li>**Corner Points**: Snaps to the four corners of any layer's bounding box.</li><li>**Center Points**: Snaps to the center point of any layer's bounding box.</li><li>**Edge Midpoints**: Snaps to any of the four points at the middle of the edges of any layer's bounding box.</li><li>**Along Edges**: Snaps anywhere along the four edges of any layer's bounding box.</li><li>**Distribute Evenly**: Snaps to a consistent distance offset established by the bounding boxes of nearby layers (due to a bug, **Center Points** and **Corner Points** must be enabled).</li></ul></p><p>Snapping options relating to **Paths**:</p><p><ul><li>**Align with Anchor Points**: Snaps to horizontal/vertical alignment with the anchor points of any vector path.</li><li>**Anchor Points**: Snaps to the anchor point of any vector path.</li><li>**Line Midpoints**: Snaps to the point at the middle of any straight line segment of a vector path.</li><li>**Path Intersection Points**: Snaps to any points where vector paths intersect.</li><li>**Along Paths**: Snaps along the length of any vector path.</li><li>**Normal to Paths**: Snaps a line to a point perpendicular to a vector path (due to a bug, **Intersections of Paths** must be enabled).</li><li>**Tangent to Paths**: Snaps a line to a point tangent to a vector path (due to a bug, **Intersections of Paths** must be enabled).</li></ul></p> |
| Grid | <p>When checked (off by default), grid lines are shown and snapping to them becomes active. The initial grid scale is 1 document unit, helping you draw pixel-perfect artwork.</p><ul><li><p>**Type** sets whether the grid pattern is made of squares or triangles.</p><p>**Rectangular** is a pattern of horizontal and vertical lines:</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/grid-rectangular-popover__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>It has one option unique to this mode:</p><ul><li>**Spacing** is the width and height of the rectangle grid cells.</li></ul><p>**Isometric** is a pattern of triangles:</p><p><img src="https://static.graphite.rs/content/learn/interface/document-panel/grid-isometric-popover__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="Snapping options popover menu" /></p><p>It has two options unique to this mode:</p><ul><li>**Y Spacing** is the height between vertical repetitions of the grid.</li><li>**Angles** is the slant of the upward and downward sloped grid lines.</li></ul></li><li>**Display** gives control over the appearance of the grid. The **Display as dotted grid** checkbox (off by default) replaces the solid lines with dots at their intersection points.</li><li>**Origin** is the position in the canvas where the repeating grid pattern begins from. If you need an offset for the grid where an intersection occurs at a specific location, set those coordinates.</li></ul> |
| View Mode | <p>**Normal** (default): The artwork is rendered normally.</p><p>**Outline**: The artwork is rendered as a wireframe.</p><p>**Pixels**: **Not implemented yet.** The artwork is rendered as it would appear when exported as a bitmap image at 100% scale regardless of the viewport zoom level.</p> |
| Zoom In | <p>Zooms the viewport in to the next whole increment.</p> |

View file

@ -50,7 +50,7 @@ body {
@media screen and (max-width: 780px) {
:root {
--font-size-link: calc(1rem * 24 / 18);
--font-size-link: calc(1rem * 4 / 3);
--page-edge-padding: 28px;
--border-thickness: 1px;
}