mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 01:48:11 +08:00
Added fine-grained choices to Snapping options popover (#1730)
* Added Customised Snapping * Fix snapping choice to Anchors; clean up popover menu code --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
3019cc7253
commit
67ba5bcecf
@@ -12,6 +12,8 @@ use graphene_core::Color;
|
||||
|
||||
use glam::DAffine2;
|
||||
|
||||
use super::utility_types::misc::{OptionBoundsSnapping, OptionPointSnapping};
|
||||
|
||||
#[impl_message(Message, PortfolioMessage, Document)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DocumentMessage {
|
||||
@@ -126,8 +128,8 @@ pub enum DocumentMessage {
|
||||
},
|
||||
SetSnapping {
|
||||
snapping_enabled: Option<bool>,
|
||||
bounding_box_snapping: Option<bool>,
|
||||
geometry_snapping: Option<bool>,
|
||||
bounding_box_snapping: Option<OptionBoundsSnapping>,
|
||||
geometry_snapping: Option<OptionPointSnapping>,
|
||||
},
|
||||
SetViewMode {
|
||||
view_mode: ViewMode,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::utility_types::error::EditorError;
|
||||
use super::utility_types::misc::{SnappingOptions, SnappingState};
|
||||
use super::utility_types::misc::{BoundingBoxSnapTarget, GeometrySnapTarget, OptionBoundsSnapping, OptionPointSnapping, SnappingOptions, SnappingState};
|
||||
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
|
||||
use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH};
|
||||
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, SCALE_EFFECT, SCROLLBAR_SPACING};
|
||||
@@ -718,12 +718,56 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
||||
if let Some(state) = snapping_enabled {
|
||||
self.snapping_state.snapping_enabled = state
|
||||
};
|
||||
if let Some(state) = bounding_box_snapping {
|
||||
self.snapping_state.bounding_box_snapping = 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
|
||||
};
|
||||
}
|
||||
if let Some(state) = geometry_snapping {
|
||||
self.snapping_state.geometry_snapping = state
|
||||
};
|
||||
}
|
||||
DocumentMessage::SetViewMode { view_mode } => {
|
||||
self.view_mode = view_mode;
|
||||
@@ -1149,7 +1193,16 @@ impl DocumentMessageHandler {
|
||||
.tooltip("Overlays")
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into())
|
||||
.widget_holder(),
|
||||
PopoverButton::new("Overlays", "Coming soon").widget_holder(),
|
||||
PopoverButton::new()
|
||||
.popover_layout(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Overlays").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Coming soon").widget_holder()],
|
||||
},
|
||||
])
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
CheckboxInput::new(snapping_state.snapping_enabled)
|
||||
.icon("Snapping")
|
||||
@@ -1158,47 +1211,112 @@ impl DocumentMessageHandler {
|
||||
let snapping_enabled = optional_input.checked;
|
||||
DocumentMessage::SetSnapping {
|
||||
snapping_enabled: Some(snapping_enabled),
|
||||
bounding_box_snapping: Some(snapping_state.bounding_box_snapping),
|
||||
geometry_snapping: Some(snapping_state.geometry_snapping),
|
||||
bounding_box_snapping: None,
|
||||
geometry_snapping: None,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
PopoverButton::new("Snapping", "Snap customization settings")
|
||||
.options_widget(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(snapping_state.bounding_box_snapping)
|
||||
.tooltip(SnappingOptions::BoundingBoxes.to_string())
|
||||
.on_update(move |input: &CheckboxInput| {
|
||||
DocumentMessage::SetSnapping {
|
||||
snapping_enabled: None,
|
||||
bounding_box_snapping: Some(input.checked),
|
||||
geometry_snapping: None,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).widget_holder(),
|
||||
],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
CheckboxInput::new(self.snapping_state.geometry_snapping)
|
||||
.tooltip(SnappingOptions::Geometry.to_string())
|
||||
.on_update(|input: &CheckboxInput| {
|
||||
DocumentMessage::SetSnapping {
|
||||
snapping_enabled: None,
|
||||
bounding_box_snapping: None,
|
||||
geometry_snapping: Some(input.checked),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_holder(),
|
||||
TextLabel::new(SnappingOptions::Geometry.to_string()).widget_holder(),
|
||||
],
|
||||
},
|
||||
])
|
||||
PopoverButton::new()
|
||||
.popover_layout(
|
||||
[
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Snapping").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).widget_holder()],
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.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),
|
||||
]
|
||||
.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: 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(),
|
||||
],
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.collect(),
|
||||
)
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
CheckboxInput::new(self.snapping_state.grid_snapping)
|
||||
@@ -1206,8 +1324,8 @@ impl DocumentMessageHandler {
|
||||
.tooltip("Grid")
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisible(optional_input.checked).into())
|
||||
.widget_holder(),
|
||||
PopoverButton::new("Grid", "Grid customization settings")
|
||||
.options_widget(overlay_options(&self.snapping_state.grid))
|
||||
PopoverButton::new()
|
||||
.popover_layout(overlay_options(&self.snapping_state.grid))
|
||||
.popover_min_width(Some(320))
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
@@ -1230,7 +1348,16 @@ impl DocumentMessageHandler {
|
||||
_ => Some(1),
|
||||
})
|
||||
.widget_holder(),
|
||||
PopoverButton::new("View Mode", "Coming soon").widget_holder(),
|
||||
PopoverButton::new()
|
||||
.popover_layout(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("View Mode").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Coming soon").widget_holder()],
|
||||
},
|
||||
])
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
IconButton::new("ZoomIn", 24)
|
||||
.tooltip("Zoom In")
|
||||
@@ -1247,25 +1374,34 @@ impl DocumentMessageHandler {
|
||||
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::ResetCanvasTiltAndZoomTo100Percent))
|
||||
.on_update(|_| NavigationMessage::ResetCanvasTiltAndZoomTo100Percent.into())
|
||||
.widget_holder(),
|
||||
PopoverButton::new(
|
||||
"Canvas Navigation",
|
||||
"
|
||||
Interactive controls in this\n\
|
||||
menu are coming soon.\n\
|
||||
\n\
|
||||
Pan:\n\
|
||||
• Middle Click Drag\n\
|
||||
\n\
|
||||
Tilt:\n\
|
||||
• Alt + Middle Click Drag\n\
|
||||
\n\
|
||||
Zoom:\n\
|
||||
• Shift + Middle Click Drag\n\
|
||||
• Ctrl + Scroll Wheel Roll
|
||||
"
|
||||
.trim(),
|
||||
)
|
||||
.widget_holder(),
|
||||
PopoverButton::new()
|
||||
.popover_layout(vec![
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Canvas Navigation").bold(true).widget_holder()],
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new(
|
||||
"
|
||||
Interactive controls in this\n\
|
||||
menu are coming soon.\n\
|
||||
\n\
|
||||
Pan:\n\
|
||||
• Middle Click Drag\n\
|
||||
\n\
|
||||
Tilt:\n\
|
||||
• Alt + Middle Click Drag\n\
|
||||
\n\
|
||||
Zoom:\n\
|
||||
• Shift + Middle Click Drag\n\
|
||||
• Ctrl + Scroll Wheel Roll
|
||||
"
|
||||
.trim(),
|
||||
)
|
||||
.multiline(true)
|
||||
.widget_holder()],
|
||||
},
|
||||
])
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(self.navigation_handler.snapped_scale(self.navigation.zoom) * 100.))
|
||||
.unit("%")
|
||||
|
||||
@@ -105,6 +105,9 @@ pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||
}
|
||||
})
|
||||
};
|
||||
widgets.push(LayoutGroup::Row {
|
||||
widgets: vec![TextLabel::new("Grid").bold(true).widget_holder()],
|
||||
});
|
||||
widgets.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Origin").table_align(true).widget_holder(),
|
||||
|
||||
@@ -57,8 +57,6 @@ impl DocumentMode {
|
||||
/// SnappingState determines the current individual snapping states
|
||||
pub struct SnappingState {
|
||||
pub snapping_enabled: bool,
|
||||
pub bounding_box_snapping: bool,
|
||||
pub geometry_snapping: bool,
|
||||
pub grid_snapping: bool,
|
||||
pub bounds: BoundsSnapping,
|
||||
pub nodes: PointSnapping,
|
||||
@@ -70,8 +68,6 @@ impl Default for SnappingState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
snapping_enabled: true,
|
||||
bounding_box_snapping: true,
|
||||
geometry_snapping: true,
|
||||
grid_snapping: false,
|
||||
bounds: BoundsSnapping {
|
||||
edges: true,
|
||||
@@ -82,8 +78,7 @@ impl Default for SnappingState {
|
||||
nodes: PointSnapping {
|
||||
paths: true,
|
||||
path_intersections: true,
|
||||
point_handles_free: true,
|
||||
point_handles_colinear: true,
|
||||
anchors: true,
|
||||
line_midpoints: true,
|
||||
normals: true,
|
||||
tangents: true,
|
||||
@@ -103,15 +98,15 @@ impl SnappingState {
|
||||
return false;
|
||||
}
|
||||
match target {
|
||||
SnapTarget::BoundingBox(bounding_box) if self.bounding_box_snapping => match bounding_box {
|
||||
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::Geometry(nodes) if self.geometry_snapping => match nodes {
|
||||
GeometrySnapTarget::HandlesColinear => self.nodes.point_handles_colinear,
|
||||
GeometrySnapTarget::HandlesFree => self.nodes.point_handles_free,
|
||||
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,
|
||||
@@ -131,16 +126,31 @@ pub struct BoundsSnapping {
|
||||
pub edge_midpoints: bool,
|
||||
pub centers: bool,
|
||||
}
|
||||
#[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)]
|
||||
pub struct PointSnapping {
|
||||
pub paths: bool,
|
||||
pub path_intersections: bool,
|
||||
pub point_handles_free: bool,
|
||||
pub point_handles_colinear: bool,
|
||||
pub anchors: bool,
|
||||
pub line_midpoints: bool,
|
||||
pub normals: bool,
|
||||
pub tangents: bool,
|
||||
}
|
||||
#[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 },
|
||||
@@ -216,8 +226,8 @@ impl GridSnapping {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BoundingBoxSnapSource {
|
||||
Corner,
|
||||
Center,
|
||||
Corner,
|
||||
EdgeMidpoint,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -227,11 +237,11 @@ pub enum BoardSnapSource {
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GeometrySnapSource {
|
||||
HandlesColinear,
|
||||
HandlesFree,
|
||||
LineMidpoint,
|
||||
PathIntersection,
|
||||
AnchorWithColinearHandles,
|
||||
AnchorWithFreeHandles,
|
||||
Handle,
|
||||
LineMidpoint,
|
||||
Intersection,
|
||||
}
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SnapSource {
|
||||
@@ -249,23 +259,50 @@ impl SnapSource {
|
||||
matches!(self, Self::BoundingBox(_) | Self::Board(_))
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum BoundingBoxSnapTarget {
|
||||
Center,
|
||||
Corner,
|
||||
Edge,
|
||||
EdgeMidpoint,
|
||||
Center,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
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 {
|
||||
HandlesColinear,
|
||||
HandlesFree,
|
||||
AnchorWithColinearHandles,
|
||||
AnchorWithFreeHandles,
|
||||
LineMidpoint,
|
||||
Path,
|
||||
Normal,
|
||||
Tangent,
|
||||
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 {
|
||||
Edge,
|
||||
|
||||
Reference in New Issue
Block a user