Layer and grid snapping systems (#1521)
* Grid overlays * Rectangle tool basic snapping * Fix bezier demos * Fix bézier crate tests * Constrained snapping for circle & shape tool * Line tool snapping * Pen tool snapping * Path tool snapping * Snapping whilst dragging layers (not constrained) * Constrained drag * Resize snapping * Normal and tangent * Cleanup * Grid snapping * Grid snapping * Fix imports * Fix bug in artboard tool * Fix hang on 0 size grid spacing * Fix NaN when scaling * Polishing --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
78a1bb17cd
commit
456ca170a4
|
|
@ -55,6 +55,7 @@ web-sys = { workspace = true, features = [
|
|||
"Element",
|
||||
"HtmlCanvasElement",
|
||||
"CanvasRenderingContext2d",
|
||||
"TextMetrics",
|
||||
] }
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -18,22 +18,11 @@ pub const VIEWPORT_ROTATE_SNAP_INTERVAL: f64 = 15.;
|
|||
|
||||
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f64 = 0.95;
|
||||
|
||||
// Snapping axis
|
||||
pub const SNAP_AXIS_TOLERANCE: f64 = 3.;
|
||||
pub const SNAP_AXIS_OVERLAY_FADE_DISTANCE: f64 = 15.;
|
||||
pub const SNAP_AXIS_UNSNAPPED_OPACITY: f64 = 0.4;
|
||||
|
||||
// Snapping point
|
||||
pub const SNAP_POINT_OVERLAY_FADE_NEAR: f64 = 20.;
|
||||
pub const SNAP_POINT_OVERLAY_FADE_FAR: f64 = 40.;
|
||||
pub const SNAP_POINT_UNSNAPPED_OPACITY: f64 = 0.4;
|
||||
pub const SNAP_POINT_TOLERANCE: f64 = 5.;
|
||||
pub const SNAP_POINT_SIZE: f64 = 5.;
|
||||
|
||||
pub const DRAG_THRESHOLD: f64 = 1.;
|
||||
|
||||
pub const PATH_OUTLINE_WEIGHT: f64 = 2.;
|
||||
|
||||
// Transforming layer
|
||||
pub const ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
|
||||
|
|
@ -75,6 +64,7 @@ pub const SCALE_EFFECT: f64 = 0.5;
|
|||
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
|
||||
pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
|
||||
pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
||||
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
|
||||
|
||||
// Fonts
|
||||
pub const DEFAULT_FONT_FAMILY: &str = "Cabin";
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ pub fn default_mapping() -> Mapping {
|
|||
entry!(KeyUp(Lmb); action_dispatch=RectangleToolMessage::DragStop),
|
||||
entry!(KeyDown(Rmb); action_dispatch=RectangleToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=RectangleToolMessage::Abort),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=RectangleToolMessage::Resize { center: Alt, lock_ratio: Shift }),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=RectangleToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
|
||||
//
|
||||
// ImaginateToolMessage
|
||||
entry!(KeyDown(Lmb); action_dispatch=ImaginateToolMessage::DragStart),
|
||||
|
|
@ -160,21 +160,21 @@ pub fn default_mapping() -> Mapping {
|
|||
entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop),
|
||||
entry!(KeyDown(Rmb); action_dispatch=EllipseToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=EllipseToolMessage::Abort),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=EllipseToolMessage::Resize { center: Alt, lock_ratio: Shift }),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=EllipseToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
|
||||
//
|
||||
// PolygonToolMessage
|
||||
entry!(KeyDown(Lmb); action_dispatch=PolygonToolMessage::DragStart),
|
||||
entry!(KeyUp(Lmb); action_dispatch=PolygonToolMessage::DragStop),
|
||||
entry!(KeyDown(Rmb); action_dispatch=PolygonToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=PolygonToolMessage::Abort),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PolygonToolMessage::Resize { center: Alt, lock_ratio: Shift }),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift], action_dispatch=PolygonToolMessage::PointerMove { center: Alt, lock_ratio: Shift }),
|
||||
//
|
||||
// LineToolMessage
|
||||
entry!(KeyDown(Lmb); action_dispatch=LineToolMessage::DragStart),
|
||||
entry!(KeyUp(Lmb); action_dispatch=LineToolMessage::DragStop),
|
||||
entry!(KeyDown(Rmb); action_dispatch=LineToolMessage::Abort),
|
||||
entry!(KeyDown(Escape); action_dispatch=LineToolMessage::Abort),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=LineToolMessage::Redraw { center: Alt, lock_angle: Control, snap_angle: Shift }),
|
||||
entry!(PointerMove; refresh_keys=[Alt, Shift, Control], action_dispatch=LineToolMessage::PointerMove { center: Alt, lock_angle: Control, snap_angle: Shift }),
|
||||
//
|
||||
// PathToolMessage
|
||||
entry!(KeyDown(Lmb); action_dispatch=PathToolMessage::DragStart { add_to_selection: Shift }),
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ pub struct PopoverButton {
|
|||
#[serde(rename = "optionsWidget")]
|
||||
pub options_widget: SubLayout,
|
||||
|
||||
#[serde(rename = "popoverMinWidth")]
|
||||
pub popover_min_width: Option<u32>,
|
||||
|
||||
#[serde(skip)]
|
||||
pub tooltip_shortcut: Option<ActionKeys>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,4 +43,4 @@ pub enum Message {
|
|||
/// Specta isn't integrated with `impl_message`, so a remote impl must be provided using this struct.
|
||||
#[derive(specta::Type)]
|
||||
#[specta(inline, remote = "MessageDiscriminant")]
|
||||
pub struct MessageDiscriminantDef(u8);
|
||||
pub struct MessageDiscriminantDef(pub u8);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping};
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graph_craft::document::{NodeId, NodeNetwork};
|
||||
|
|
@ -63,6 +64,9 @@ pub enum DocumentMessage {
|
|||
open: bool,
|
||||
},
|
||||
GraphViewOverlayToggle,
|
||||
GridOptions(GridSnapping),
|
||||
GridOverlays(OverlayContext),
|
||||
GridVisible(bool),
|
||||
GroupSelectedLayers,
|
||||
ImaginateGenerate,
|
||||
ImaginateRandom {
|
||||
|
|
@ -118,7 +122,7 @@ pub enum DocumentMessage {
|
|||
SetSnapping {
|
||||
snapping_enabled: Option<bool>,
|
||||
bounding_box_snapping: Option<bool>,
|
||||
node_snapping: Option<bool>,
|
||||
geometry_snapping: Option<bool>,
|
||||
},
|
||||
SetViewMode {
|
||||
view_mode: ViewMode,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX,
|
|||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::node_graph::{GraphOperationHandlerData, NodeGraphHandlerData};
|
||||
use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay, overlay_options};
|
||||
use crate::messages::portfolio::document::properties_panel::utility_types::PropertiesPanelMessageHandlerData;
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{is_artboard, DocumentMetadata, LayerNodeIdentifier};
|
||||
|
|
@ -58,7 +59,7 @@ pub struct DocumentMessageHandler {
|
|||
#[serde(default = "default_commit_hash")]
|
||||
commit_hash: String,
|
||||
#[serde(default = "default_pan_tilt_zoom")]
|
||||
navigation: PTZ,
|
||||
pub navigation: PTZ,
|
||||
#[serde(default = "default_document_mode")]
|
||||
document_mode: DocumentMode,
|
||||
#[serde(default = "default_view_mode")]
|
||||
|
|
@ -438,6 +439,21 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
GraphViewOverlayToggle => {
|
||||
responses.add(DocumentMessage::GraphViewOverlay { open: !self.graph_view_overlay_open });
|
||||
}
|
||||
GridOptions(grid) => {
|
||||
self.snapping_state.grid = grid;
|
||||
self.snapping_state.grid_snapping = true;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
}
|
||||
GridOverlays(mut overlay_context) => {
|
||||
if self.snapping_state.grid_snapping {
|
||||
grid_overlay(self, &mut overlay_context)
|
||||
}
|
||||
}
|
||||
GridVisible(enabled) => {
|
||||
self.snapping_state.grid_snapping = enabled;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
GroupSelectedLayers => {
|
||||
// TODO: Add code that changes the insert index of the new folder based on the selected layer
|
||||
let parent = self
|
||||
|
|
@ -742,7 +758,7 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
SetSnapping {
|
||||
snapping_enabled,
|
||||
bounding_box_snapping,
|
||||
node_snapping,
|
||||
geometry_snapping,
|
||||
} => {
|
||||
if let Some(state) = snapping_enabled {
|
||||
self.snapping_state.snapping_enabled = state
|
||||
|
|
@ -750,8 +766,8 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> for DocumentMessageHand
|
|||
if let Some(state) = bounding_box_snapping {
|
||||
self.snapping_state.bounding_box_snapping = state
|
||||
}
|
||||
if let Some(state) = node_snapping {
|
||||
self.snapping_state.node_snapping = state
|
||||
if let Some(state) = geometry_snapping {
|
||||
self.snapping_state.geometry_snapping = state
|
||||
};
|
||||
}
|
||||
SetViewMode { view_mode } => {
|
||||
|
|
@ -1096,6 +1112,13 @@ impl DocumentMessageHandler {
|
|||
pub fn update_document_widgets(&self, responses: &mut VecDeque<Message>) {
|
||||
let snapping_state = self.snapping_state.clone();
|
||||
let mut widgets = vec![
|
||||
CheckboxInput::new(self.overlays_visible)
|
||||
.icon("Overlays")
|
||||
.tooltip("Overlays")
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into())
|
||||
.widget_holder(),
|
||||
PopoverButton::new("Overlays", "Coming soon").widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
CheckboxInput::new(snapping_state.snapping_enabled)
|
||||
.icon("Snapping")
|
||||
.tooltip("Snapping")
|
||||
|
|
@ -1104,7 +1127,7 @@ impl DocumentMessageHandler {
|
|||
DocumentMessage::SetSnapping {
|
||||
snapping_enabled: Some(snapping_enabled),
|
||||
bounding_box_snapping: Some(snapping_state.bounding_box_snapping),
|
||||
node_snapping: Some(snapping_state.node_snapping),
|
||||
geometry_snapping: Some(snapping_state.geometry_snapping),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
|
|
@ -1114,14 +1137,14 @@ impl DocumentMessageHandler {
|
|||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new(SnappingOptions::BoundingBoxes.to_string()).table_align(true).min_width(96).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
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),
|
||||
node_snapping: None,
|
||||
geometry_snapping: None,
|
||||
}
|
||||
.into()
|
||||
})
|
||||
|
|
@ -1131,15 +1154,15 @@ impl DocumentMessageHandler {
|
|||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new(SnappingOptions::Points.to_string()).table_align(true).min_width(96).widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
CheckboxInput::new(self.snapping_state.node_snapping)
|
||||
.tooltip(SnappingOptions::Points.to_string())
|
||||
TextLabel::new(SnappingOptions::Geometry.to_string()).table_align(true).min_width(96).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
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,
|
||||
node_snapping: Some(input.checked),
|
||||
geometry_snapping: Some(input.checked),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
|
|
@ -1149,19 +1172,15 @@ impl DocumentMessageHandler {
|
|||
])
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
CheckboxInput::new(true)
|
||||
CheckboxInput::new(self.snapping_state.grid_snapping)
|
||||
.icon("Grid")
|
||||
.tooltip("Grid")
|
||||
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into())
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::GridVisible(optional_input.checked).into())
|
||||
.widget_holder(),
|
||||
PopoverButton::new("Grid", "Coming soon").widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
CheckboxInput::new(self.overlays_visible)
|
||||
.icon("Overlays")
|
||||
.tooltip("Overlays")
|
||||
.on_update(|optional_input: &CheckboxInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into())
|
||||
PopoverButton::new("Grid", "Grid customization settings")
|
||||
.options_widget(overlay_options(&self.snapping_state.grid))
|
||||
.popover_min_width(Some(320))
|
||||
.widget_holder(),
|
||||
PopoverButton::new("Overlays", "Coming soon").widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
RadioInput::new(vec![
|
||||
RadioEntryData::default()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
use crate::consts::COLOR_OVERLAY_GRAY;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{GridSnapping, GridType};
|
||||
use crate::messages::prelude::*;
|
||||
use glam::DVec2;
|
||||
use graphene_core::renderer::Quad;
|
||||
|
||||
fn grid_overlay_rectangular(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, spacing: DVec2) {
|
||||
let origin = document.snapping_state.grid.origin;
|
||||
let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else {
|
||||
return;
|
||||
};
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]);
|
||||
|
||||
for primary in 0..2 {
|
||||
let secondary = 1 - primary;
|
||||
let min = bounds.0.iter().map(|&corner| corner[secondary]).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default();
|
||||
let max = bounds.0.iter().map(|&corner| corner[secondary]).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default();
|
||||
let primary_start = bounds.0.iter().map(|&corner| corner[primary]).min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default();
|
||||
let primary_end = bounds.0.iter().map(|&corner| corner[primary]).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap_or_default();
|
||||
let spacing = spacing[secondary];
|
||||
for line_index in 0..=((max - min) / spacing).ceil() as i32 {
|
||||
let secondary_pos = (((min - origin[secondary]) / spacing).ceil() + line_index as f64) * spacing + origin[secondary];
|
||||
let start = if primary == 0 {
|
||||
DVec2::new(primary_start, secondary_pos)
|
||||
} else {
|
||||
DVec2::new(secondary_pos, primary_start)
|
||||
};
|
||||
let end = if primary == 0 {
|
||||
DVec2::new(primary_end, secondary_pos)
|
||||
} else {
|
||||
DVec2::new(secondary_pos, primary_end)
|
||||
};
|
||||
overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(COLOR_OVERLAY_GRAY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn grid_overlay_isometric(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, y_axis_spacing: f64, angle_a: f64, angle_b: f64) {
|
||||
let cmp = |a: &f64, b: &f64| a.partial_cmp(b).unwrap();
|
||||
let origin = document.snapping_state.grid.origin;
|
||||
let document_to_viewport = document.metadata().document_to_viewport;
|
||||
let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.size]);
|
||||
let tan_a = angle_a.to_radians().tan();
|
||||
let tan_b = angle_b.to_radians().tan();
|
||||
let spacing = DVec2::new(y_axis_spacing / (tan_a + tan_b), y_axis_spacing);
|
||||
let Some(spacing_multiplier) = GridSnapping::compute_isometric_multiplier(y_axis_spacing, tan_a + tan_b, &document.navigation) else {
|
||||
return;
|
||||
};
|
||||
let isometric_spacing = spacing * spacing_multiplier;
|
||||
|
||||
let min_x = bounds.0.iter().map(|&corner| corner.x).min_by(cmp).unwrap_or_default();
|
||||
let max_x = bounds.0.iter().map(|&corner| corner.x).max_by(cmp).unwrap_or_default();
|
||||
let min_y = bounds.0.iter().map(|&corner| corner.y).min_by(cmp).unwrap_or_default();
|
||||
let max_y = bounds.0.iter().map(|&corner| corner.y).max_by(cmp).unwrap_or_default();
|
||||
let spacing = isometric_spacing.x;
|
||||
for line_index in 0..=((max_x - min_x) / spacing).ceil() as i32 {
|
||||
let x_pos = (((min_x - origin.x) / spacing).ceil() + line_index as f64) * spacing + origin.x;
|
||||
let start = DVec2::new(x_pos, min_y);
|
||||
let end = DVec2::new(x_pos, max_y);
|
||||
overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(COLOR_OVERLAY_GRAY));
|
||||
}
|
||||
|
||||
for (tan, multiply) in [(tan_a, -1.), (tan_b, 1.)] {
|
||||
let project = |corner: &DVec2| corner.y + multiply * tan * (corner.x - origin.x);
|
||||
let inverse_project = |corner: &DVec2| corner.y - tan * multiply * (corner.x - origin.x);
|
||||
let min_y = bounds.0.into_iter().min_by(|a, b| inverse_project(a).partial_cmp(&inverse_project(b)).unwrap()).unwrap_or_default();
|
||||
let max_y = bounds.0.into_iter().max_by(|a, b| inverse_project(a).partial_cmp(&inverse_project(b)).unwrap()).unwrap_or_default();
|
||||
let spacing = isometric_spacing.y;
|
||||
let lines = ((inverse_project(&max_y) - inverse_project(&min_y)) / spacing).ceil() as i32;
|
||||
for line_index in 0..=lines {
|
||||
let y_pos = (((inverse_project(&min_y) - origin.y) / spacing).ceil() + line_index as f64) * spacing + origin.y;
|
||||
let start = DVec2::new(min_x, project(&DVec2::new(min_x, y_pos)));
|
||||
let end = DVec2::new(max_x, project(&DVec2::new(max_x, y_pos)));
|
||||
overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(COLOR_OVERLAY_GRAY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grid_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
|
||||
match document.snapping_state.grid.grid_type {
|
||||
GridType::Rectangle { spacing } => grid_overlay_rectangular(document, overlay_context, spacing),
|
||||
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => grid_overlay_isometric(document, overlay_context, y_axis_spacing, angle_a, angle_b),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlay_options(grid: &GridSnapping) -> Vec<LayoutGroup> {
|
||||
let mut widgets = Vec::new();
|
||||
fn update_val<I>(grid: &GridSnapping, update: impl Fn(&mut GridSnapping, &I)) -> impl Fn(&I) -> Message {
|
||||
let grid = grid.clone();
|
||||
move |input: &I| {
|
||||
let mut grid = grid.clone();
|
||||
update(&mut grid, &input);
|
||||
DocumentMessage::GridOptions(grid).into()
|
||||
}
|
||||
}
|
||||
let update_origin = |grid, update: fn(&mut GridSnapping) -> Option<&mut f64>| {
|
||||
update_val::<NumberInput>(grid, move |grid, val| {
|
||||
if let Some(val) = val.value {
|
||||
if let Some(update) = update(grid) {
|
||||
*update = val;
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
widgets.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Origin").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(grid.origin.x))
|
||||
.label("X")
|
||||
.unit(" px")
|
||||
.min_width(98)
|
||||
.on_update(update_origin(&grid, |grid| Some(&mut grid.origin.x)))
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(grid.origin.y))
|
||||
.label("Y")
|
||||
.unit(" px")
|
||||
.min_width(98)
|
||||
.on_update(update_origin(&grid, |grid| Some(&mut grid.origin.y)))
|
||||
.widget_holder(),
|
||||
],
|
||||
});
|
||||
widgets.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Type").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
RadioInput::new(vec![
|
||||
RadioEntryData::new("Rectangular").on_update(update_val(grid, |grid, _| grid.grid_type = GridType::RECTANGLE)),
|
||||
RadioEntryData::new("Isometric").on_update(update_val(grid, |grid, _| grid.grid_type = GridType::ISOMETRIC)),
|
||||
])
|
||||
.selected_index(Some(if matches!(grid.grid_type, GridType::Rectangle { .. }) { 0 } else { 1 }))
|
||||
.widget_holder(),
|
||||
],
|
||||
});
|
||||
|
||||
match grid.grid_type {
|
||||
GridType::Rectangle { spacing } => widgets.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Spacing").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(spacing.x))
|
||||
.label("X")
|
||||
.unit(" px")
|
||||
.min(0.)
|
||||
.min_width(98)
|
||||
.on_update(update_origin(&grid, |grid| grid.grid_type.rect_spacing().map(|spacing| &mut spacing.x)))
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(spacing.y))
|
||||
.label("Y")
|
||||
.unit(" px")
|
||||
.min(0.)
|
||||
.min_width(98)
|
||||
.on_update(update_origin(&grid, |grid| grid.grid_type.rect_spacing().map(|spacing| &mut spacing.y)))
|
||||
.widget_holder(),
|
||||
],
|
||||
}),
|
||||
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => {
|
||||
widgets.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Spacing").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(y_axis_spacing))
|
||||
.label("Y")
|
||||
.unit(" px")
|
||||
.min(0.)
|
||||
.min_width(200)
|
||||
.on_update(update_origin(&grid, |grid| grid.grid_type.isometric_y_spacing()))
|
||||
.widget_holder(),
|
||||
],
|
||||
});
|
||||
widgets.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Angle A").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(angle_a))
|
||||
.unit("°")
|
||||
.min_width(200)
|
||||
.on_update(update_origin(&grid, |grid| grid.grid_type.angle_a()))
|
||||
.widget_holder(),
|
||||
],
|
||||
});
|
||||
widgets.push(LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
TextLabel::new("Angle B").table_align(true).widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(angle_b))
|
||||
.unit("°")
|
||||
.min_width(200)
|
||||
.on_update(update_origin(&grid, |grid| grid.grid_type.angle_b()))
|
||||
.widget_holder(),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
widgets
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod grid_overlays;
|
||||
mod overlays_message;
|
||||
mod overlays_message_handler;
|
||||
pub mod utility_functions;
|
||||
|
|
|
|||
|
|
@ -24,14 +24,22 @@ impl MessageHandler<OverlaysMessage, (bool, &InputPreprocessorMessageHandler)> f
|
|||
context.dyn_into().expect("Context should be a canvas 2d context")
|
||||
});
|
||||
|
||||
canvas.set_width(ipp.viewport_bounds.size().x as u32);
|
||||
canvas.set_height(ipp.viewport_bounds.size().y as u32);
|
||||
let size = ipp.viewport_bounds.size().as_uvec2();
|
||||
canvas.set_width(size.x);
|
||||
canvas.set_height(size.y);
|
||||
|
||||
context.clear_rect(0., 0., ipp.viewport_bounds.size().x, ipp.viewport_bounds.size().y);
|
||||
|
||||
if overlays_visible {
|
||||
responses.add(DocumentMessage::GridOverlays(OverlayContext {
|
||||
render_context: context.clone(),
|
||||
size: size.as_dvec2(),
|
||||
}));
|
||||
for provider in &self.overlay_providers {
|
||||
responses.add(provider(OverlayContext { render_context: context.clone() }));
|
||||
responses.add(provider(OverlayContext {
|
||||
render_context: context.clone(),
|
||||
size: size.as_dvec2(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,12 +39,12 @@ pub fn path_overlays(document: &DocumentMessageHandler, shape_editor: &mut Shape
|
|||
let not_under_anchor = |&position: &DVec2| transform.transform_point2(position).distance_squared(anchor_position) >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE;
|
||||
if let Some(in_handle) = manipulator_group.in_handle.filter(not_under_anchor) {
|
||||
let handle_position = transform.transform_point2(in_handle);
|
||||
overlay_context.line(handle_position, anchor_position);
|
||||
overlay_context.line(handle_position, anchor_position, None);
|
||||
overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::InHandle)));
|
||||
}
|
||||
if let Some(out_handle) = manipulator_group.out_handle.filter(not_under_anchor) {
|
||||
let handle_position = transform.transform_point2(out_handle);
|
||||
overlay_context.line(handle_position, anchor_position);
|
||||
overlay_context.line(handle_position, anchor_position, None);
|
||||
overlay_context.handle(handle_position, is_selected(selected, ManipulatorPointId::new(manipulator_group.id, SelectedType::OutHandle)));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ pub fn empty_provider() -> OverlayProvider {
|
|||
|_| Message::NoOp
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct OverlayContext {
|
||||
// Serde functionality isn't used but is required by the message system macros
|
||||
#[serde(skip, default = "overlay_canvas_context")]
|
||||
pub render_context: web_sys::CanvasRenderingContext2d,
|
||||
pub size: DVec2,
|
||||
}
|
||||
// Message hashing isn't used but is required by the message system macros
|
||||
impl core::hash::Hash for OverlayContext {
|
||||
|
|
@ -37,11 +38,11 @@ impl OverlayContext {
|
|||
self.render_context.stroke();
|
||||
}
|
||||
|
||||
pub fn line(&mut self, start: DVec2, end: DVec2) {
|
||||
pub fn line(&mut self, start: DVec2, end: DVec2, color: Option<&str>) {
|
||||
self.render_context.begin_path();
|
||||
self.render_context.move_to(start.x.round(), start.y.round());
|
||||
self.render_context.line_to(end.x.round(), end.y.round());
|
||||
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
|
||||
self.render_context.move_to(start.x, start.y);
|
||||
self.render_context.line_to(end.x, end.y);
|
||||
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color.unwrap_or(COLOR_OVERLAY_BLUE)));
|
||||
self.render_context.stroke();
|
||||
}
|
||||
|
||||
|
|
@ -131,4 +132,20 @@ impl OverlayContext {
|
|||
self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE));
|
||||
self.render_context.stroke();
|
||||
}
|
||||
|
||||
pub fn text(&self, text: &str, pos: DVec2, background: &str, padding: f64) {
|
||||
let pos = pos.round();
|
||||
let metrics = self.render_context.measure_text(text).expect("measure text");
|
||||
self.render_context.set_fill_style(&background.into());
|
||||
self.render_context.fill_rect(
|
||||
pos.x + metrics.actual_bounding_box_left(),
|
||||
pos.y - metrics.font_bounding_box_ascent() - metrics.font_bounding_box_descent() - padding * 2.,
|
||||
metrics.actual_bounding_box_right() - metrics.actual_bounding_box_left() + padding * 2.,
|
||||
metrics.font_bounding_box_ascent() + metrics.font_bounding_box_descent() + padding * 2.,
|
||||
);
|
||||
self.render_context.set_fill_style(&"white".into());
|
||||
self.render_context
|
||||
.fill_text(text, pos.x + padding, pos.y - padding - metrics.font_bounding_box_descent())
|
||||
.expect("draw text");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ impl DocumentMetadata {
|
|||
}
|
||||
|
||||
pub fn is_folder(&self, layer: LayerNodeIdentifier) -> bool {
|
||||
self.folders.contains(&layer)
|
||||
layer == LayerNodeIdentifier::ROOT || self.folders.contains(&layer)
|
||||
}
|
||||
|
||||
pub fn is_artboard(&self, layer: LayerNodeIdentifier) -> bool {
|
||||
|
|
|
|||
|
|
@ -58,30 +58,251 @@ impl DocumentMode {
|
|||
pub struct SnappingState {
|
||||
pub snapping_enabled: bool,
|
||||
pub bounding_box_snapping: bool,
|
||||
pub node_snapping: bool,
|
||||
pub geometry_snapping: bool,
|
||||
pub grid_snapping: bool,
|
||||
pub bounds: BoundsSnapping,
|
||||
pub nodes: NodeSnapping,
|
||||
pub grid: GridSnapping,
|
||||
pub tolerance: f64,
|
||||
pub artboards: bool,
|
||||
}
|
||||
|
||||
impl Default for SnappingState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
snapping_enabled: true,
|
||||
bounding_box_snapping: true,
|
||||
node_snapping: true,
|
||||
geometry_snapping: true,
|
||||
grid_snapping: false,
|
||||
bounds: BoundsSnapping {
|
||||
edges: true,
|
||||
corners: true,
|
||||
edge_midpoints: false,
|
||||
centres: false,
|
||||
},
|
||||
nodes: NodeSnapping {
|
||||
paths: true,
|
||||
path_intersections: true,
|
||||
sharp_nodes: true,
|
||||
smooth_nodes: true,
|
||||
line_midpoints: true,
|
||||
normals: true,
|
||||
tangents: true,
|
||||
},
|
||||
grid: GridSnapping {
|
||||
origin: DVec2::ZERO,
|
||||
grid_type: GridType::RECTANGLE,
|
||||
},
|
||||
tolerance: 8.,
|
||||
artboards: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl SnappingState {
|
||||
pub const fn target_enabled(&self, target: SnapTarget) -> bool {
|
||||
if !self.snapping_enabled {
|
||||
return false;
|
||||
}
|
||||
match target {
|
||||
SnapTarget::BoundingBox(bounding_box) if self.bounding_box_snapping => match bounding_box {
|
||||
BoundingBoxSnapTarget::Corner => self.bounds.corners,
|
||||
BoundingBoxSnapTarget::Edge => self.bounds.edges,
|
||||
BoundingBoxSnapTarget::EdgeMidpoint => self.bounds.edge_midpoints,
|
||||
BoundingBoxSnapTarget::Centre => self.bounds.centres,
|
||||
},
|
||||
SnapTarget::Geometry(nodes) if self.geometry_snapping => match nodes {
|
||||
GeometrySnapTarget::Smooth => self.nodes.smooth_nodes,
|
||||
GeometrySnapTarget::Sharp => self.nodes.sharp_nodes,
|
||||
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::Board(_) => self.artboards,
|
||||
SnapTarget::Grid(_) => self.grid_snapping,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BoundsSnapping {
|
||||
pub edges: bool,
|
||||
pub corners: bool,
|
||||
pub edge_midpoints: bool,
|
||||
pub centres: bool,
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct NodeSnapping {
|
||||
pub paths: bool,
|
||||
pub path_intersections: bool,
|
||||
pub sharp_nodes: bool,
|
||||
pub smooth_nodes: bool,
|
||||
pub line_midpoints: bool,
|
||||
pub normals: bool,
|
||||
pub tangents: bool,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum GridType {
|
||||
Rectangle { spacing: DVec2 },
|
||||
Isometric { y_axis_spacing: f64, angle_a: f64, angle_b: f64 },
|
||||
}
|
||||
impl GridType {
|
||||
pub const RECTANGLE: Self = GridType::Rectangle { spacing: DVec2::ONE };
|
||||
pub const ISOMETRIC: Self = GridType::Isometric {
|
||||
y_axis_spacing: 1.,
|
||||
angle_a: 30.,
|
||||
angle_b: 30.,
|
||||
};
|
||||
pub fn rect_spacing(&mut self) -> Option<&mut DVec2> {
|
||||
match self {
|
||||
Self::Rectangle { spacing } => Some(spacing),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn isometric_y_spacing(&mut self) -> Option<&mut f64> {
|
||||
match self {
|
||||
Self::Isometric { y_axis_spacing, .. } => Some(y_axis_spacing),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn angle_a(&mut self) -> Option<&mut f64> {
|
||||
match self {
|
||||
Self::Isometric { angle_a, .. } => Some(angle_a),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn angle_b(&mut self) -> Option<&mut f64> {
|
||||
match self {
|
||||
Self::Isometric { angle_b, .. } => Some(angle_b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct GridSnapping {
|
||||
pub origin: DVec2,
|
||||
pub grid_type: GridType,
|
||||
}
|
||||
impl GridSnapping {
|
||||
// Double grid size until it takes up at least 10px.
|
||||
pub fn compute_rectangle_spacing(mut size: DVec2, navigation: &PTZ) -> Option<DVec2> {
|
||||
let mut iterations = 0;
|
||||
size = size.abs();
|
||||
while (size * navigation.zoom).cmplt(DVec2::splat(10.)).any() {
|
||||
if iterations > 100 {
|
||||
return None;
|
||||
}
|
||||
size *= 2.;
|
||||
iterations += 1;
|
||||
}
|
||||
Some(size)
|
||||
}
|
||||
|
||||
// Double grid size until it takes up at least 10px.
|
||||
pub fn compute_isometric_multiplier(length: f64, divisor: f64, navigation: &PTZ) -> Option<f64> {
|
||||
let length = length.abs();
|
||||
let mut iterations = 0;
|
||||
let mut multiplier = 1.;
|
||||
while (length / divisor.abs().max(1.)) * multiplier * navigation.zoom < 10. {
|
||||
if iterations > 100 {
|
||||
return None;
|
||||
}
|
||||
multiplier *= 2.;
|
||||
iterations += 1;
|
||||
}
|
||||
Some(multiplier)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BoundingBoxSnapSource {
|
||||
Corner,
|
||||
Centre,
|
||||
EdgeMidpoint,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BoardSnapSource {
|
||||
Centre,
|
||||
Corner,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GeometrySnapSource {
|
||||
Smooth,
|
||||
Sharp,
|
||||
LineMidpoint,
|
||||
PathIntersection,
|
||||
Handle,
|
||||
}
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SnapSource {
|
||||
#[default]
|
||||
None,
|
||||
BoundingBox(BoundingBoxSnapSource),
|
||||
Board(BoardSnapSource),
|
||||
Geometry(GeometrySnapSource),
|
||||
}
|
||||
impl SnapSource {
|
||||
pub fn is_some(&self) -> bool {
|
||||
self != &Self::None
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BoundingBoxSnapTarget {
|
||||
Corner,
|
||||
Edge,
|
||||
EdgeMidpoint,
|
||||
Centre,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GeometrySnapTarget {
|
||||
Smooth,
|
||||
Sharp,
|
||||
LineMidpoint,
|
||||
Path,
|
||||
Normal,
|
||||
Tangent,
|
||||
Intersection,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BoardSnapTarget {
|
||||
Edge,
|
||||
Corner,
|
||||
Centre,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum GridSnapTarget {
|
||||
Line,
|
||||
LineNormal,
|
||||
Intersection,
|
||||
}
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SnapTarget {
|
||||
#[default]
|
||||
None,
|
||||
BoundingBox(BoundingBoxSnapTarget),
|
||||
Geometry(GeometrySnapTarget),
|
||||
Board(BoardSnapTarget),
|
||||
Grid(GridSnapTarget),
|
||||
}
|
||||
impl SnapTarget {
|
||||
pub fn is_some(&self) -> bool {
|
||||
self != &Self::None
|
||||
}
|
||||
pub fn bounding_box(&self) -> bool {
|
||||
matches!(self, Self::BoundingBox(_) | Self::Board(_))
|
||||
}
|
||||
}
|
||||
// TODO: implement icons for SnappingOptions eventually
|
||||
pub enum SnappingOptions {
|
||||
BoundingBoxes,
|
||||
Points,
|
||||
Geometry,
|
||||
}
|
||||
|
||||
impl fmt::Display for SnappingOptions {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
SnappingOptions::BoundingBoxes => write!(f, "Bounding Boxes"),
|
||||
SnappingOptions::Points => write!(f, "Points"),
|
||||
SnappingOptions::Geometry => write!(f, "Geometry"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,27 +3,24 @@ use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
|||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
|
||||
use glam::{DAffine2, DVec2, Vec2Swizzles};
|
||||
|
||||
use super::snapping::{SnapCandidatePoint, SnapConstraint, SnapData};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Resize {
|
||||
drag_start: ViewportPosition,
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
snap_manager: SnapManager,
|
||||
pub snap_manager: SnapManager,
|
||||
}
|
||||
|
||||
impl Resize {
|
||||
/// Starts a resize, assigning the snap targets and snapping the starting position.
|
||||
pub fn start(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
pub fn start(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
let root_transform = document.metadata().document_to_viewport;
|
||||
self.drag_start = root_transform.inverse().transform_point2(self.snap_manager.snap_position(responses, document, input.mouse.position));
|
||||
}
|
||||
|
||||
/// Recalculates snap targets without snapping the starting position.
|
||||
pub fn recalculate_snaps(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
let point = SnapCandidatePoint::handle(root_transform.inverse().transform_point2(input.mouse.position));
|
||||
let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
|
||||
self.drag_start = snapped.snapped_point_document;
|
||||
}
|
||||
|
||||
/// Calculate the drag start position in viewport space.
|
||||
|
|
@ -32,15 +29,7 @@ impl Resize {
|
|||
root_transform.transform_point2(self.drag_start)
|
||||
}
|
||||
|
||||
pub fn calculate_transform(
|
||||
&mut self,
|
||||
responses: &mut VecDeque<Message>,
|
||||
document: &DocumentMessageHandler,
|
||||
ipp: &InputPreprocessorMessageHandler,
|
||||
center: Key,
|
||||
lock_ratio: Key,
|
||||
skip_rerender: bool,
|
||||
) -> Option<Message> {
|
||||
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
|
||||
let Some(layer) = self.layer else {
|
||||
return None;
|
||||
};
|
||||
|
|
@ -49,22 +38,54 @@ impl Resize {
|
|||
return None;
|
||||
}
|
||||
|
||||
let mut start = self.viewport_drag_start(document);
|
||||
let stop = self.snap_manager.snap_position(responses, document, ipp.mouse.position);
|
||||
|
||||
let mut size = stop - start;
|
||||
if ipp.keyboard.get(lock_ratio as usize) {
|
||||
size = size.abs().max(size.abs().yx()) * size.signum();
|
||||
}
|
||||
if ipp.keyboard.get(center as usize) {
|
||||
start -= size;
|
||||
size *= 2.;
|
||||
let start = self.viewport_drag_start(document);
|
||||
let mouse = input.mouse.position;
|
||||
let to_viewport = document.metadata().document_to_viewport;
|
||||
let document_mouse = to_viewport.inverse().transform_point2(mouse);
|
||||
let mut points_viewport = [start, mouse];
|
||||
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
|
||||
let ratio = input.keyboard.get(lock_ratio as usize);
|
||||
let centre = input.keyboard.get(center as usize);
|
||||
let snap_data = SnapData::ignore(document, input, &ignore);
|
||||
if ratio {
|
||||
let size = points_viewport[1] - points_viewport[0];
|
||||
let size = size.abs().max(size.abs().yx()) * size.signum();
|
||||
points_viewport[1] = points_viewport[0] + size;
|
||||
let end_document = to_viewport.inverse().transform_point2(points_viewport[1]);
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: self.drag_start,
|
||||
direction: end_document - self.drag_start,
|
||||
};
|
||||
if centre {
|
||||
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, None);
|
||||
let far = SnapCandidatePoint::handle(2. * self.drag_start - end_document);
|
||||
let snapped_far = self.snap_manager.constrained_snap(&snap_data, &far, constraint, None);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
points_viewport[0] = to_viewport.transform_point2(best.snapped_point_document);
|
||||
points_viewport[1] = to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
|
||||
self.snap_manager.update_indicator(best);
|
||||
} else {
|
||||
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, None);
|
||||
points_viewport[1] = to_viewport.transform_point2(snapped.snapped_point_document);
|
||||
self.snap_manager.update_indicator(snapped);
|
||||
}
|
||||
} else if centre {
|
||||
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), None, false);
|
||||
let snapped_far = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(2. * self.drag_start - document_mouse), None, false);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
points_viewport[0] = to_viewport.transform_point2(best.snapped_point_document);
|
||||
points_viewport[1] = to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
|
||||
self.snap_manager.update_indicator(best);
|
||||
} else {
|
||||
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), None, false);
|
||||
points_viewport[1] = to_viewport.transform_point2(snapped.snapped_point_document);
|
||||
self.snap_manager.update_indicator(snapped);
|
||||
}
|
||||
|
||||
Some(
|
||||
GraphOperationMessage::TransformSet {
|
||||
layer,
|
||||
transform: DAffine2::from_scale_angle_translation(size, 0., start),
|
||||
transform: DAffine2::from_scale_angle_translation(points_viewport[1] - points_viewport[0], 0., points_viewport[0]),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use super::graph_modification_utils;
|
||||
use super::snapping::{group_smooth, SnapCandidatePoint, SnapData, SnapManager, SnappedPoint};
|
||||
use crate::consts::DRAG_THRESHOLD;
|
||||
use crate::messages::portfolio::document::node_graph::VectorDataModification;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_from_id, get_manipulator_groups, get_mirror_handles, get_subpaths};
|
||||
|
||||
|
|
@ -64,6 +66,52 @@ pub type OpposingHandleLengths = HashMap<LayerNodeIdentifier, HashMap<Manipulato
|
|||
|
||||
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
|
||||
impl ShapeState {
|
||||
// Snap, returning a viewport delta
|
||||
pub fn snap(&self, snap_manager: &mut SnapManager, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, previous_mouse: DVec2) -> DVec2 {
|
||||
let mut snap_data = SnapData::new(document, input);
|
||||
|
||||
for (layer, state) in &self.selected_shape_state {
|
||||
for point in &state.selected_points {
|
||||
snap_data.manipulators.push((*layer, point.group));
|
||||
}
|
||||
}
|
||||
|
||||
let mouse_delta = document.metadata.document_to_viewport.inverse().transform_vector2(input.mouse.position - previous_mouse);
|
||||
let mut offset = mouse_delta;
|
||||
let mut best_snapped = SnappedPoint::infinite_snap(document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position));
|
||||
for (layer, state) in &self.selected_shape_state {
|
||||
let Some(subpaths) = get_subpaths(*layer, &document.network) else { continue };
|
||||
|
||||
let to_document = document.metadata.transform_to_document(*layer);
|
||||
|
||||
for subpath in subpaths {
|
||||
for (index, group) in subpath.manipulator_groups().iter().enumerate() {
|
||||
for handle in [SelectedType::Anchor, SelectedType::InHandle, SelectedType::OutHandle] {
|
||||
if !state.is_selected(ManipulatorPointId::new(group.id, handle)) {
|
||||
continue;
|
||||
}
|
||||
let source = if handle.is_handle() {
|
||||
SnapSource::Geometry(GeometrySnapSource::Handle)
|
||||
} else if group_smooth(group, to_document, subpath, index) {
|
||||
SnapSource::Geometry(GeometrySnapSource::Smooth)
|
||||
} else {
|
||||
SnapSource::Geometry(GeometrySnapSource::Sharp)
|
||||
};
|
||||
let Some(position) = handle.get_position(&group) else { continue };
|
||||
let point = SnapCandidatePoint::new_source(to_document.transform_point2(position) + mouse_delta, source);
|
||||
let snapped = snap_manager.free_snap(&snap_data, &point, None, false);
|
||||
if best_snapped.other_snap_better(&snapped) {
|
||||
offset = snapped.snapped_point_document - point.document_point + mouse_delta;
|
||||
best_snapped = snapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
snap_manager.update_indicator(best_snapped);
|
||||
document.metadata.document_to_viewport.transform_vector2(offset)
|
||||
}
|
||||
|
||||
/// Select the first point within the selection threshold.
|
||||
/// Returns a tuple of the points if found and the offset, or `None` otherwise.
|
||||
pub fn select_point(
|
||||
|
|
|
|||
|
|
@ -1,126 +1,347 @@
|
|||
use crate::consts::{SNAP_AXIS_TOLERANCE, SNAP_POINT_TOLERANCE};
|
||||
mod grid_snapper;
|
||||
mod layer_snapper;
|
||||
mod snap_results;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
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::prelude::*;
|
||||
|
||||
use glam::DVec2;
|
||||
use bezier_rs::{Subpath, TValue};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use std::cmp::Ordering;
|
||||
pub use {grid_snapper::*, layer_snapper::*, snap_results::*};
|
||||
|
||||
/// Handles snapping and snap overlays
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SnapManager {
|
||||
point_targets: Option<Vec<DVec2>>,
|
||||
bound_targets: Option<Vec<DVec2>>,
|
||||
snap_x: bool,
|
||||
snap_y: bool,
|
||||
indicator: Option<SnappedPoint>,
|
||||
layer_snapper: LayerSnapper,
|
||||
grid_snapper: GridSnapper,
|
||||
candidates: Option<Vec<LayerNodeIdentifier>>,
|
||||
}
|
||||
|
||||
impl SnapManager {
|
||||
/// Computes the necessary translation to the layer to snap it (as well as updating necessary overlays)
|
||||
fn calculate_snap<R>(&mut self, targets: R, responses: &mut VecDeque<Message>) -> DVec2
|
||||
where
|
||||
R: Iterator<Item = DVec2> + Clone,
|
||||
{
|
||||
let empty = Vec::new();
|
||||
let snap_points = self.snap_x && self.snap_y;
|
||||
|
||||
let axis = self.bound_targets.as_ref().unwrap_or(&empty);
|
||||
let points = if snap_points { self.point_targets.as_ref().unwrap_or(&empty) } else { &empty };
|
||||
|
||||
let x_axis = if self.snap_x { axis } else { &empty }
|
||||
.iter()
|
||||
.flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).x)));
|
||||
let y_axis = if self.snap_y { axis } else { &empty }
|
||||
.iter()
|
||||
.flat_map(|&pos| targets.clone().map(move |goal| (pos, goal, (pos - goal).y)));
|
||||
let points = points.iter().flat_map(|&pos| targets.clone().map(move |goal| (pos, pos - goal, (pos - goal).length())));
|
||||
|
||||
let min_x = x_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
|
||||
let min_y = y_axis.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
|
||||
let min_points = points.clone().min_by(|a, b| a.2.abs().partial_cmp(&b.2.abs()).expect("Could not compare position."));
|
||||
|
||||
// Snap to a point if possible
|
||||
let (clamped_closest_distance, _snapped_to_point) = if let Some(min_points) = min_points.filter(|&(_, _, dist)| dist <= SNAP_POINT_TOLERANCE) {
|
||||
(min_points.1, true)
|
||||
} else {
|
||||
// Do not move if over snap tolerance
|
||||
let closest_distance = DVec2::new(min_x.unwrap_or_default().2, min_y.unwrap_or_default().2);
|
||||
(
|
||||
DVec2::new(
|
||||
if closest_distance.x.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.x },
|
||||
if closest_distance.y.abs() > SNAP_AXIS_TOLERANCE { 0. } else { closest_distance.y },
|
||||
),
|
||||
false,
|
||||
)
|
||||
};
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
||||
clamped_closest_distance
|
||||
}
|
||||
|
||||
/// Gets a list of snap targets for the X and Y axes (if specified) in Viewport coords for the target layers (usually all layers or all non-selected layers.)
|
||||
/// This should be called at the start of a drag.
|
||||
pub fn start_snap(
|
||||
&mut self,
|
||||
document_message_handler: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
bounding_boxes: impl Iterator<Item = [DVec2; 2]>,
|
||||
snap_x: bool,
|
||||
snap_y: bool,
|
||||
) {
|
||||
let snapping_enabled = document_message_handler.snapping_state.snapping_enabled;
|
||||
let bounding_box_snapping = document_message_handler.snapping_state.bounding_box_snapping;
|
||||
if snapping_enabled && bounding_box_snapping {
|
||||
self.snap_x = snap_x;
|
||||
self.snap_y = snap_y;
|
||||
|
||||
// Could be made into sorted Vec or a HashSet for more performant lookups.
|
||||
self.bound_targets = Some(
|
||||
bounding_boxes
|
||||
.flat_map(expand_bounds)
|
||||
.filter(|&pos| pos.x >= 0. && pos.y >= 0. && pos.x < input.viewport_bounds.size().x && pos.y <= input.viewport_bounds.size().y)
|
||||
.collect(),
|
||||
);
|
||||
self.point_targets = None;
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub enum SnapConstraint {
|
||||
#[default]
|
||||
None,
|
||||
Line {
|
||||
origin: DVec2,
|
||||
direction: DVec2,
|
||||
},
|
||||
Direction(DVec2),
|
||||
Circle {
|
||||
centre: DVec2,
|
||||
radius: f64,
|
||||
},
|
||||
}
|
||||
impl SnapConstraint {
|
||||
pub fn projection(&self, point: DVec2) -> DVec2 {
|
||||
match *self {
|
||||
Self::Line { origin, direction } if direction != DVec2::ZERO => (point - origin).project_onto(direction) + origin,
|
||||
Self::Circle { centre, radius } => {
|
||||
let from_centre = point - centre;
|
||||
let distance = from_centre.length();
|
||||
if distance > 0. {
|
||||
centre + radius * from_centre / distance
|
||||
} else {
|
||||
// Point is exactly at the centre, so project right
|
||||
centre + DVec2::new(radius, 0.)
|
||||
}
|
||||
}
|
||||
_ => point,
|
||||
}
|
||||
}
|
||||
pub fn direction(&self) -> DVec2 {
|
||||
match *self {
|
||||
Self::Line { direction, .. } | Self::Direction(direction) => direction,
|
||||
_ => DVec2::ZERO,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn snap_tolerance(document: &DocumentMessageHandler) -> f64 {
|
||||
document.snapping_state.tolerance / document.navigation.zoom
|
||||
}
|
||||
|
||||
/// Add arbitrary snapping points
|
||||
///
|
||||
/// This should be called after start_snap
|
||||
pub fn add_snap_points(&mut self, document_message_handler: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, snap_points: impl Iterator<Item = DVec2>) {
|
||||
let snapping_enabled = document_message_handler.snapping_state.snapping_enabled;
|
||||
let node_snapping = document_message_handler.snapping_state.node_snapping;
|
||||
if snapping_enabled && node_snapping {
|
||||
let snap_points = snap_points.filter(|&pos| pos.x >= 0. && pos.y >= 0. && pos.x < input.viewport_bounds.size().x && pos.y <= input.viewport_bounds.size().y);
|
||||
if let Some(targets) = &mut self.point_targets {
|
||||
targets.extend(snap_points);
|
||||
} else {
|
||||
self.point_targets = Some(snap_points.collect());
|
||||
fn compare_points(a: &&SnappedPoint, b: &&SnappedPoint) -> Ordering {
|
||||
if (a.target.bounding_box() && !b.target.bounding_box()) || (a.at_intersection && !b.at_intersection) {
|
||||
Ordering::Greater
|
||||
} else if (!a.target.bounding_box() && b.target.bounding_box()) || (!a.at_intersection && b.at_intersection) {
|
||||
Ordering::Less
|
||||
} else {
|
||||
a.distance.partial_cmp(&b.distance).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_closest_point(points: &[SnappedPoint]) -> Option<&SnappedPoint> {
|
||||
points.iter().min_by(compare_points)
|
||||
}
|
||||
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);
|
||||
curves.iter().filter(keep_curve).map(|curve| &curve.point).min_by(compare_points)
|
||||
}
|
||||
fn get_closest_line(lines: &[SnappedLine]) -> Option<&SnappedPoint> {
|
||||
lines.iter().map(|curve| &curve.point).min_by(compare_points)
|
||||
}
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for curve_j in curves {
|
||||
if curve_j.point.target == SnapTarget::BoundingBox(BoundingBoxSnapTarget::Edge) {
|
||||
continue;
|
||||
}
|
||||
if curve_i.start == curve_j.start && curve_i.layer == curve_j.layer {
|
||||
continue;
|
||||
}
|
||||
for curve_i_t in curve_i.document_curve.intersections(&curve_j.document_curve, None, None) {
|
||||
let snapped_point_document = curve_i.document_curve.evaluate(TValue::Parametric(curve_i_t));
|
||||
let distance = snap_to.distance(snapped_point_document);
|
||||
let i_closer = curve_i.point.distance < curve_j.point.distance;
|
||||
let close = if i_closer { curve_i } else { curve_j };
|
||||
let far = if i_closer { curve_j } else { curve_i };
|
||||
if !best.as_ref().is_some_and(|best: &SnappedPoint| best.distance < distance) {
|
||||
best = Some(SnappedPoint {
|
||||
snapped_point_document,
|
||||
distance,
|
||||
target: SnapTarget::Geometry(GeometrySnapTarget::Intersection),
|
||||
tolerance: close.point.tolerance,
|
||||
curves: [Some(close.document_curve), Some(far.document_curve)],
|
||||
source: close.point.source,
|
||||
at_intersection: true,
|
||||
contrained: true,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the closest snap from an array of layers to the specified snap targets in viewport coords.
|
||||
/// Returns 0 for each axis that there is no snap less than the snap tolerance.
|
||||
pub fn snap_layers(&mut self, responses: &mut VecDeque<Message>, document_message_handler: &DocumentMessageHandler, snap_anchors: Vec<DVec2>, mouse_delta: DVec2) -> DVec2 {
|
||||
if document_message_handler.snapping_state.snapping_enabled {
|
||||
self.calculate_snap(snap_anchors.iter().map(move |&snap| mouse_delta + snap), responses)
|
||||
} else {
|
||||
DVec2::ZERO
|
||||
best
|
||||
}
|
||||
fn get_grid_intersection(snap_to: DVec2, lines: &[SnappedLine]) -> Option<SnappedPoint> {
|
||||
let mut best = None;
|
||||
for line_i in lines {
|
||||
for line_j in lines {
|
||||
if let Some(snapped_point_document) = Quad::intersect_rays(line_i.point.snapped_point_document, line_i.direction, line_j.point.snapped_point_document, line_j.direction) {
|
||||
let distance = snap_to.distance(snapped_point_document);
|
||||
if !best.as_ref().is_some_and(|best: &SnappedPoint| best.distance < distance) {
|
||||
best = Some(SnappedPoint {
|
||||
snapped_point_document,
|
||||
distance,
|
||||
target: SnapTarget::Grid(GridSnapTarget::Intersection),
|
||||
tolerance: line_i.point.tolerance,
|
||||
source: line_i.point.source,
|
||||
at_intersection: true,
|
||||
contrained: true,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct SnapData<'a> {
|
||||
pub document: &'a DocumentMessageHandler,
|
||||
pub input: &'a InputPreprocessorMessageHandler,
|
||||
pub ignore: &'a [LayerNodeIdentifier],
|
||||
pub manipulators: Vec<(LayerNodeIdentifier, ManipulatorGroupId)>,
|
||||
pub candidates: Option<&'a Vec<LayerNodeIdentifier>>,
|
||||
}
|
||||
impl<'a> SnapData<'a> {
|
||||
pub fn new(document: &'a DocumentMessageHandler, input: &'a InputPreprocessorMessageHandler) -> Self {
|
||||
Self::ignore(document, input, &[])
|
||||
}
|
||||
pub fn ignore(document: &'a DocumentMessageHandler, input: &'a InputPreprocessorMessageHandler, ignore: &'a [LayerNodeIdentifier]) -> Self {
|
||||
Self {
|
||||
document,
|
||||
input,
|
||||
ignore,
|
||||
candidates: None,
|
||||
manipulators: Vec::new(),
|
||||
}
|
||||
}
|
||||
fn get_candidates(&self) -> &[LayerNodeIdentifier] {
|
||||
self.candidates.map_or([].as_slice(), |candidates| candidates.as_slice())
|
||||
}
|
||||
fn ignore_bounds(&self, layer: LayerNodeIdentifier) -> bool {
|
||||
self.manipulators.iter().any(|&(ignore, _)| ignore == layer)
|
||||
}
|
||||
fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: ManipulatorGroupId) -> bool {
|
||||
self.manipulators.contains(&(layer, manipulator))
|
||||
}
|
||||
}
|
||||
impl SnapManager {
|
||||
pub fn update_indicator(&mut self, snapped_point: SnappedPoint) {
|
||||
self.indicator = snapped_point.is_snapped().then_some(snapped_point);
|
||||
}
|
||||
pub fn clear_indicator(&mut self) {
|
||||
self.indicator = None;
|
||||
}
|
||||
pub fn preview_draw(&mut self, snap_data: &SnapData, mouse: DVec2) {
|
||||
let point = SnapCandidatePoint::handle(snap_data.document.metadata.document_to_viewport.inverse().transform_point2(mouse));
|
||||
let snapped = self.free_snap(snap_data, &point, None, false);
|
||||
self.update_indicator(snapped);
|
||||
}
|
||||
|
||||
/// Handles snapping of a viewport position, returning another viewport position.
|
||||
pub fn snap_position(&mut self, responses: &mut VecDeque<Message>, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 {
|
||||
if document_message_handler.snapping_state.snapping_enabled {
|
||||
self.calculate_snap([position_viewport].into_iter(), responses) + position_viewport
|
||||
} else {
|
||||
position_viewport
|
||||
fn find_best_snap(snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: SnapResults, contrained: bool, off_screen: bool, to_path: bool) -> SnappedPoint {
|
||||
let mut snapped_points = Vec::new();
|
||||
let document = snap_data.document;
|
||||
|
||||
if let Some(closest_point) = get_closest_point(&snap_results.points) {
|
||||
snapped_points.push(closest_point.clone());
|
||||
}
|
||||
let exclude_paths = !document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Path));
|
||||
if let Some(closest_curve) = get_closest_curve(&snap_results.curves, exclude_paths) {
|
||||
snapped_points.push(closest_curve.clone());
|
||||
}
|
||||
|
||||
if document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Line)) {
|
||||
if let Some(closest_line) = get_closest_line(&snap_results.grid_lines) {
|
||||
snapped_points.push(closest_line.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !contrained {
|
||||
if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Intersection)) {
|
||||
if let Some(closest_curves_intersection) = get_closest_intersection(point.document_point, &snap_results.curves) {
|
||||
snapped_points.push(closest_curves_intersection);
|
||||
}
|
||||
}
|
||||
if document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Intersection)) {
|
||||
if let Some(closest_grid_intersection) = get_grid_intersection(point.document_point, &snap_results.grid_lines) {
|
||||
snapped_points.push(closest_grid_intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if to_path {
|
||||
snapped_points.retain(|i| matches!(i.target, SnapTarget::Geometry(_)));
|
||||
}
|
||||
|
||||
let mut best_point = None;
|
||||
|
||||
for point in snapped_points {
|
||||
let viewport_point = document.metadata.document_to_viewport.transform_point2(point.snapped_point_document);
|
||||
let on_screen = viewport_point.cmpgt(DVec2::ZERO).all() && viewport_point.cmplt(snap_data.input.viewport_bounds.size()).all();
|
||||
if !on_screen && !off_screen {
|
||||
continue;
|
||||
}
|
||||
if point.distance > point.tolerance {
|
||||
continue;
|
||||
}
|
||||
if best_point.as_ref().is_some_and(|best: &SnappedPoint| point.other_snap_better(best)) {
|
||||
continue;
|
||||
}
|
||||
best_point = Some(point);
|
||||
}
|
||||
|
||||
best_point.unwrap_or(SnappedPoint::infinite_snap(point.document_point))
|
||||
}
|
||||
|
||||
fn find_candidates(snap_data: &SnapData, point: &SnapCandidatePoint, bbox: Option<Quad>) -> Vec<LayerNodeIdentifier> {
|
||||
let document = snap_data.document;
|
||||
let offset = snap_tolerance(document);
|
||||
let quad = bbox.map_or_else(|| Quad::from_box([point.document_point - offset, point.document_point + offset]), |quad| quad.inflate(offset));
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
fn add_candidates(layer: LayerNodeIdentifier, snap_data: &SnapData, quad: Quad, candidates: &mut Vec<LayerNodeIdentifier>) {
|
||||
let document = snap_data.document;
|
||||
if candidates.len() > 10 {
|
||||
return;
|
||||
}
|
||||
if !document.selected_nodes.layer_visible(layer, &document.network, &document.metadata) {
|
||||
return;
|
||||
}
|
||||
if snap_data.ignore.contains(&layer) {
|
||||
return;
|
||||
}
|
||||
if document.metadata.is_folder(layer) {
|
||||
for layer in layer.children(&document.metadata) {
|
||||
add_candidates(layer, snap_data, quad, candidates);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(bounds) = document.metadata.bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
|
||||
return;
|
||||
};
|
||||
let layer_bounds = document.metadata.transform_to_document(layer) * Quad::from_box(bounds);
|
||||
let screen_bounds = document.metadata.document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, snap_data.input.viewport_bounds.size()]);
|
||||
if quad.intersects(layer_bounds) && screen_bounds.intersects(layer_bounds) {
|
||||
candidates.push(layer);
|
||||
}
|
||||
}
|
||||
add_candidates(LayerNodeIdentifier::ROOT, snap_data, quad, &mut candidates);
|
||||
if candidates.len() > 10 {
|
||||
warn!("Snap candidate overflow");
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
pub fn free_snap(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, bbox: Option<Quad>, to_paths: bool) -> SnappedPoint {
|
||||
if !point.document_point.is_finite() {
|
||||
warn!("Snapping non-finite position");
|
||||
return SnappedPoint::infinite_snap(DVec2::ZERO);
|
||||
}
|
||||
|
||||
let mut snap_results = SnapResults::default();
|
||||
if point.source_index == 0 {
|
||||
self.candidates = None;
|
||||
}
|
||||
|
||||
let mut snap_data = snap_data.clone();
|
||||
snap_data.candidates = Some(&*self.candidates.get_or_insert_with(|| Self::find_candidates(&snap_data, point, bbox)));
|
||||
self.layer_snapper.free_snap(&mut snap_data, point, &mut snap_results);
|
||||
self.grid_snapper.free_snap(&mut snap_data, point, &mut snap_results);
|
||||
|
||||
Self::find_best_snap(&mut snap_data, point, snap_results, false, false, to_paths)
|
||||
}
|
||||
|
||||
pub fn constrained_snap(&mut self, snap_data: &SnapData, point: &SnapCandidatePoint, constraint: SnapConstraint, bbox: Option<Quad>) -> SnappedPoint {
|
||||
if !point.document_point.is_finite() {
|
||||
warn!("Snapping non-finite position");
|
||||
return SnappedPoint::infinite_snap(DVec2::ZERO);
|
||||
}
|
||||
|
||||
let mut snap_results = SnapResults::default();
|
||||
if point.source_index == 0 {
|
||||
self.candidates = None;
|
||||
}
|
||||
|
||||
let mut snap_data = snap_data.clone();
|
||||
snap_data.candidates = Some(&*self.candidates.get_or_insert_with(|| Self::find_candidates(&snap_data, point, bbox)));
|
||||
self.layer_snapper.contrained_snap(&mut snap_data, point, &mut snap_results, constraint);
|
||||
self.grid_snapper.contrained_snap(&mut snap_data, point, &mut snap_results, constraint);
|
||||
|
||||
Self::find_best_snap(&mut snap_data, point, snap_results, true, false, false)
|
||||
}
|
||||
|
||||
pub fn draw_overlays(&mut self, snap_data: SnapData, overlay_context: &mut OverlayContext) {
|
||||
let to_viewport = snap_data.document.metadata.document_to_viewport;
|
||||
if let Some(ind) = &self.indicator {
|
||||
for curve in &ind.curves {
|
||||
let Some(curve) = curve else { continue };
|
||||
overlay_context.outline([Subpath::from_bezier(curve)].iter(), to_viewport);
|
||||
}
|
||||
if let Some(quad) = ind.target_bounds {
|
||||
overlay_context.quad(to_viewport * quad);
|
||||
}
|
||||
let viewport = to_viewport.transform_point2(ind.snapped_point_document);
|
||||
|
||||
overlay_context.text(&format!("{:?} to {:?}", ind.source, ind.target), viewport - DVec2::new(0., 5.), "rgba(0, 0, 0, 0.8)", 3.);
|
||||
overlay_context.square(viewport, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes snap target data and overlays. Call this when snapping is done.
|
||||
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
|
||||
self.bound_targets = None;
|
||||
self.point_targets = None;
|
||||
self.candidates = None;
|
||||
self.indicator = None;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
use super::*;
|
||||
|
||||
use crate::messages::portfolio::document::utility_types::misc::{GridSnapTarget, GridSnapping, GridType, SnapTarget};
|
||||
|
||||
use bezier_rs::Bezier;
|
||||
use glam::DVec2;
|
||||
use graphene_core::renderer::Quad;
|
||||
|
||||
struct Line {
|
||||
pub point: DVec2,
|
||||
pub direction: DVec2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
pub struct GridSnapper;
|
||||
|
||||
impl GridSnapper {
|
||||
// Rectangular grid has 4 lines around a point, 2 on y axis and 2 on x axis.
|
||||
fn get_snap_lines_rectangular(&self, document_point: DVec2, snap_data: &mut SnapData, spacing: DVec2) -> Vec<Line> {
|
||||
let document = snap_data.document;
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let Some(spacing) = GridSnapping::compute_rectangle_spacing(spacing, &document.navigation) else {
|
||||
return lines;
|
||||
};
|
||||
let origin = document.snapping_state.grid.origin;
|
||||
for (direction, perpendicular) in [(DVec2::X, DVec2::Y), (DVec2::Y, DVec2::X)] {
|
||||
lines.push(Line {
|
||||
direction,
|
||||
point: perpendicular * (((document_point - origin) / spacing).ceil() * spacing + origin),
|
||||
});
|
||||
lines.push(Line {
|
||||
direction,
|
||||
point: perpendicular * (((document_point - origin) / spacing).floor() * spacing + origin),
|
||||
});
|
||||
}
|
||||
lines
|
||||
}
|
||||
// Isometric grid has 6 lines around a point, 2 y axis, 2 on the angle a, and 2 on the angle b.
|
||||
fn get_snap_lines_isometric(&self, document_point: DVec2, snap_data: &mut SnapData, y_axis_spacing: f64, angle_a: f64, angle_b: f64) -> Vec<Line> {
|
||||
let document = snap_data.document;
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let origin = document.snapping_state.grid.origin;
|
||||
|
||||
let tan_a = angle_a.to_radians().tan();
|
||||
let tan_b = angle_b.to_radians().tan();
|
||||
let spacing = DVec2::new(y_axis_spacing / (tan_a + tan_b), y_axis_spacing);
|
||||
let Some(spacing_multiplier) = GridSnapping::compute_isometric_multiplier(y_axis_spacing, tan_a + tan_b, &document.navigation) else {
|
||||
return lines;
|
||||
};
|
||||
let spacing = spacing * spacing_multiplier;
|
||||
|
||||
let x_max = ((document_point.x - origin.x) / spacing.x).ceil() * spacing.x + origin.x;
|
||||
let x_min = ((document_point.x - origin.x) / spacing.x).floor() * spacing.x + origin.x;
|
||||
lines.push(Line {
|
||||
point: DVec2::new(x_max, 0.),
|
||||
direction: DVec2::Y,
|
||||
});
|
||||
lines.push(Line {
|
||||
point: DVec2::new(x_min, 0.),
|
||||
direction: DVec2::Y,
|
||||
});
|
||||
|
||||
let y_projected_onto_x = document_point.y + tan_a * (document_point.x - origin.x);
|
||||
let y_onto_x_max = ((y_projected_onto_x - origin.y) / spacing.y).ceil() * spacing.y + origin.y;
|
||||
let y_onto_x_min = ((y_projected_onto_x - origin.y) / spacing.y).floor() * spacing.y + origin.y;
|
||||
lines.push(Line {
|
||||
point: DVec2::new(origin.x, y_onto_x_max),
|
||||
direction: DVec2::new(1., -tan_a),
|
||||
});
|
||||
lines.push(Line {
|
||||
point: DVec2::new(origin.x, y_onto_x_min),
|
||||
direction: DVec2::new(1., -tan_a),
|
||||
});
|
||||
|
||||
let y_projected_onto_z = document_point.y - tan_b * (document_point.x - origin.x);
|
||||
let y_onto_z_max = ((y_projected_onto_z - origin.y) / spacing.y).ceil() * spacing.y + origin.y;
|
||||
let y_onto_z_min = ((y_projected_onto_z - origin.y) / spacing.y).floor() * spacing.y + origin.y;
|
||||
lines.push(Line {
|
||||
point: DVec2::new(origin.x, y_onto_z_max),
|
||||
direction: DVec2::new(1., tan_b),
|
||||
});
|
||||
lines.push(Line {
|
||||
point: DVec2::new(origin.x, y_onto_z_min),
|
||||
direction: DVec2::new(1., tan_b),
|
||||
});
|
||||
|
||||
lines
|
||||
}
|
||||
fn get_snap_lines(&self, document_point: DVec2, snap_data: &mut SnapData) -> Vec<Line> {
|
||||
match snap_data.document.snapping_state.grid.grid_type {
|
||||
GridType::Rectangle { spacing } => self.get_snap_lines_rectangular(document_point, snap_data, spacing),
|
||||
GridType::Isometric { y_axis_spacing, angle_a, angle_b } => self.get_snap_lines_isometric(document_point, snap_data, y_axis_spacing, angle_a, angle_b),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults) {
|
||||
let lines = self.get_snap_lines(point.document_point, snap_data);
|
||||
let tolerance = snap_tolerance(snap_data.document);
|
||||
|
||||
for line in lines {
|
||||
let projected = (point.document_point - line.point).project_onto(line.direction) + line.point;
|
||||
let distance = point.document_point.distance(projected);
|
||||
if !distance.is_finite() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if distance > tolerance {
|
||||
continue;
|
||||
}
|
||||
|
||||
if snap_data.document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Line))
|
||||
|| snap_data.document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Intersection))
|
||||
{
|
||||
snap_results.grid_lines.push(SnappedLine {
|
||||
direction: line.direction,
|
||||
point: SnappedPoint {
|
||||
snapped_point_document: projected,
|
||||
source: point.source,
|
||||
target: SnapTarget::Grid(GridSnapTarget::Line),
|
||||
source_bounds: point.quad,
|
||||
distance,
|
||||
tolerance,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let normal_target = SnapTarget::Grid(GridSnapTarget::LineNormal);
|
||||
if snap_data.document.snapping_state.target_enabled(normal_target) {
|
||||
for &neighbor in &point.neighbors {
|
||||
let projected = (neighbor - line.point).project_onto(line.direction) + line.point;
|
||||
let distance = point.document_point.distance(projected);
|
||||
if distance > tolerance {
|
||||
continue;
|
||||
}
|
||||
snap_results.points.push(SnappedPoint {
|
||||
snapped_point_document: projected,
|
||||
source: point.source,
|
||||
source_bounds: point.quad,
|
||||
target: normal_target,
|
||||
distance,
|
||||
tolerance,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint) {
|
||||
let tolerance = snap_tolerance(snap_data.document);
|
||||
let projected = constraint.projection(point.document_point);
|
||||
let lines = self.get_snap_lines(projected, snap_data);
|
||||
let (constraint_start, constraint_direction) = match constraint {
|
||||
SnapConstraint::Line { origin, direction } => (origin, direction.normalize_or_zero()),
|
||||
SnapConstraint::Direction(direction) => (projected, direction.normalize_or_zero()),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
for line in lines {
|
||||
let Some(intersection) = Quad::intersect_rays(line.point, line.direction, constraint_start, constraint_direction) else {
|
||||
continue;
|
||||
};
|
||||
let distance = intersection.distance(point.document_point);
|
||||
if distance < tolerance && snap_data.document.snapping_state.target_enabled(SnapTarget::Grid(GridSnapTarget::Line)) {
|
||||
snap_results.points.push(SnappedPoint {
|
||||
snapped_point_document: intersection,
|
||||
source: point.source,
|
||||
target: SnapTarget::Grid(GridSnapTarget::Line),
|
||||
at_intersection: false,
|
||||
contrained: true,
|
||||
source_bounds: point.quad,
|
||||
curves: [
|
||||
Some(Bezier::from_linear_dvec2(projected - constraint_direction * tolerance, projected + constraint_direction * tolerance)),
|
||||
None,
|
||||
],
|
||||
distance,
|
||||
tolerance,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,441 @@
|
|||
use super::*;
|
||||
use crate::consts::HIDE_HANDLE_DISTANCE;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{
|
||||
BoardSnapSource, BoardSnapTarget, BoundingBoxSnapSource, BoundingBoxSnapTarget, GeometrySnapSource, GeometrySnapTarget, SnapSource, SnapTarget,
|
||||
};
|
||||
use crate::messages::prelude::*;
|
||||
use bezier_rs::{Bezier, Identifier, Subpath, TValue};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
let Some(bounds) = document.metadata.bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
|
||||
return;
|
||||
};
|
||||
let bounds = document.metadata.transform_to_document(layer) * Quad::from_box(bounds);
|
||||
if bounds.0.iter().any(|point| !point.is_finite()) {
|
||||
return;
|
||||
}
|
||||
for document_curve in bounds.bezier_lines() {
|
||||
self.paths_to_snap.push(SnapCandidatePath {
|
||||
document_curve,
|
||||
layer,
|
||||
start: ManipulatorGroupId::new(),
|
||||
target,
|
||||
bounds: Some(bounds),
|
||||
});
|
||||
}
|
||||
}
|
||||
pub fn collect_paths(&mut self, snap_data: &mut SnapData, first_point: bool) {
|
||||
if !first_point {
|
||||
return;
|
||||
}
|
||||
let document = snap_data.document;
|
||||
self.paths_to_snap.clear();
|
||||
|
||||
for layer in document.metadata.all_layers() {
|
||||
if !document.metadata.is_artboard(layer) {
|
||||
continue;
|
||||
}
|
||||
self.add_layer_bounds(document, layer, SnapTarget::Board(BoardSnapTarget::Edge));
|
||||
}
|
||||
for &layer in snap_data.get_candidates() {
|
||||
let transform = document.metadata.transform_to_document(layer);
|
||||
if !transform.is_finite() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Intersection)) || document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Path))
|
||||
{
|
||||
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));
|
||||
let start = subpath.manipulator_groups()[start_index].id;
|
||||
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) {
|
||||
continue;
|
||||
}
|
||||
self.paths_to_snap.push(SnapCandidatePath {
|
||||
document_curve,
|
||||
layer,
|
||||
start,
|
||||
target: SnapTarget::Geometry(GeometrySnapTarget::Path),
|
||||
bounds: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if !snap_data.ignore_bounds(layer) {
|
||||
self.add_layer_bounds(document, layer, SnapTarget::BoundingBox(BoundingBoxSnapTarget::Edge));
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn free_snap_paths(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults) {
|
||||
self.collect_paths(snap_data, point.source_index == 0);
|
||||
|
||||
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 tolerance = snap_tolerance(document);
|
||||
for path in &self.paths_to_snap {
|
||||
let time = path.document_curve.project(point.document_point, None);
|
||||
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
|
||||
|
||||
let distance = snapped_point_document.distance(point.document_point);
|
||||
|
||||
if distance < tolerance {
|
||||
snap_results.curves.push(SnappedCurve {
|
||||
layer: path.layer,
|
||||
start: path.start,
|
||||
document_curve: path.document_curve,
|
||||
point: SnappedPoint {
|
||||
snapped_point_document,
|
||||
target: path.target,
|
||||
distance,
|
||||
tolerance,
|
||||
curves: [path.bounds.is_none().then(|| path.document_curve), None],
|
||||
source: point.source,
|
||||
target_bounds: path.bounds,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
normals_and_tangents(path, normals, tangents, point, tolerance, snap_results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snap_paths_constrained(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint) {
|
||||
let document = snap_data.document;
|
||||
self.collect_paths(snap_data, point.source_index == 0);
|
||||
|
||||
let tolerance = snap_tolerance(document);
|
||||
let constraint_path = if let SnapConstraint::Circle { centre, radius } = constraint {
|
||||
Subpath::new_ellipse(centre - DVec2::splat(radius), centre + DVec2::splat(radius))
|
||||
} else {
|
||||
let constrained_point = constraint.projection(point.document_point);
|
||||
let direction = constraint.direction().normalize_or_zero();
|
||||
let start = constrained_point - tolerance * direction;
|
||||
let end = constrained_point + tolerance * direction;
|
||||
Subpath::<ManipulatorGroupId>::new_line(start, end)
|
||||
};
|
||||
|
||||
for path in &self.paths_to_snap {
|
||||
for constraint_path in constraint_path.iter() {
|
||||
for time in path.document_curve.intersections(&constraint_path, None, None) {
|
||||
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
|
||||
|
||||
let distance = snapped_point_document.distance(point.document_point);
|
||||
|
||||
if distance < tolerance {
|
||||
snap_results.points.push(SnappedPoint {
|
||||
snapped_point_document,
|
||||
target: path.target,
|
||||
distance,
|
||||
tolerance,
|
||||
curves: [path.bounds.is_none().then(|| path.document_curve), Some(constraint_path)],
|
||||
source: point.source,
|
||||
target_bounds: path.bounds,
|
||||
at_intersection: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_anchors(&mut self, snap_data: &mut SnapData, first_point: bool) {
|
||||
if !first_point {
|
||||
return;
|
||||
}
|
||||
let document = snap_data.document;
|
||||
self.points_to_snap.clear();
|
||||
|
||||
for layer in document.metadata.all_layers() {
|
||||
if !document.metadata.is_artboard(layer) {
|
||||
continue;
|
||||
}
|
||||
if document.snapping_state.target_enabled(SnapTarget::Board(BoardSnapTarget::Corner)) {
|
||||
let Some(bounds) = document.metadata.bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
|
||||
continue;
|
||||
};
|
||||
let quad = document.metadata.transform_to_document(layer) * Quad::from_box(bounds);
|
||||
let values = BBoxSnapValues {
|
||||
corner_source: SnapSource::Board(BoardSnapSource::Corner),
|
||||
corner_target: SnapTarget::Board(BoardSnapTarget::Corner),
|
||||
centre_source: SnapSource::Board(BoardSnapSource::Centre),
|
||||
centre_target: SnapTarget::Board(BoardSnapTarget::Centre),
|
||||
..Default::default()
|
||||
};
|
||||
get_bbox_points(quad, &mut self.points_to_snap, values, document);
|
||||
}
|
||||
}
|
||||
for &layer in snap_data.get_candidates() {
|
||||
get_layer_snap_points(layer, &snap_data, &mut self.points_to_snap);
|
||||
|
||||
if snap_data.ignore_bounds(layer) {
|
||||
continue;
|
||||
}
|
||||
let Some(bounds) = document.metadata.bounding_box_with_transform(layer, DAffine2::IDENTITY) else {
|
||||
continue;
|
||||
};
|
||||
let quad = document.metadata.transform_to_document(layer) * Quad::from_box(bounds);
|
||||
let values = BBoxSnapValues::BOUNDING_BOX;
|
||||
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) {
|
||||
self.collect_anchors(snap_data, point.source_index == 0);
|
||||
//info!("Points to snap {:#?}", self.points_to_snap);
|
||||
let mut best = None;
|
||||
for candidate in &self.points_to_snap {
|
||||
// Candidate is not on constraint
|
||||
if !candidate.document_point.abs_diff_eq(c.projection(candidate.document_point), 1e-5) {
|
||||
continue;
|
||||
}
|
||||
let distance = candidate.document_point.distance(constrained_point);
|
||||
let tolerance = snap_tolerance(snap_data.document);
|
||||
|
||||
let candidate_better = |best: &SnappedPoint| {
|
||||
if best.snapped_point_document.abs_diff_eq(candidate.document_point, 1e-5) {
|
||||
!candidate.target.bounding_box()
|
||||
} else {
|
||||
distance < best.distance
|
||||
}
|
||||
};
|
||||
if distance < tolerance && (best.is_none() || best.as_ref().is_some_and(|best| candidate_better(best))) {
|
||||
best = Some(SnappedPoint {
|
||||
snapped_point_document: candidate.document_point,
|
||||
source: point.source,
|
||||
target: candidate.target,
|
||||
distance,
|
||||
tolerance,
|
||||
contrained: true,
|
||||
target_bounds: candidate.quad,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(result) = best {
|
||||
snap_results.points.push(result);
|
||||
}
|
||||
}
|
||||
pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults) {
|
||||
self.snap_anchors(snap_data, point, snap_results, SnapConstraint::None, point.document_point);
|
||||
self.free_snap_paths(snap_data, point, snap_results);
|
||||
}
|
||||
|
||||
pub fn contrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint) {
|
||||
self.snap_anchors(snap_data, point, snap_results, constraint, constraint.projection(point.document_point));
|
||||
self.snap_paths_constrained(snap_data, point, snap_results, constraint);
|
||||
}
|
||||
}
|
||||
|
||||
fn normals_and_tangents(path: &SnapCandidatePath, normals: bool, tangents: bool, point: &SnapCandidatePoint, tolerance: f64, snap_results: &mut SnapResults) {
|
||||
if normals && path.bounds.is_none() {
|
||||
for &neighbour in &point.neighbors {
|
||||
for t in path.document_curve.normals_to_point(neighbour) {
|
||||
let normal_point = path.document_curve.evaluate(TValue::Parametric(t));
|
||||
let distance = normal_point.distance(point.document_point);
|
||||
if distance > tolerance {
|
||||
continue;
|
||||
}
|
||||
snap_results.points.push(SnappedPoint {
|
||||
snapped_point_document: normal_point,
|
||||
target: SnapTarget::Geometry(GeometrySnapTarget::Normal),
|
||||
distance,
|
||||
tolerance,
|
||||
curves: [Some(path.document_curve), None],
|
||||
source: point.source,
|
||||
contrained: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if tangents && path.bounds.is_none() {
|
||||
for &neighbour in &point.neighbors {
|
||||
for t in path.document_curve.tangents_to_point(neighbour) {
|
||||
let tangent_point = path.document_curve.evaluate(TValue::Parametric(t));
|
||||
let distance = tangent_point.distance(point.document_point);
|
||||
if distance > tolerance {
|
||||
continue;
|
||||
}
|
||||
snap_results.points.push(SnappedPoint {
|
||||
snapped_point_document: tangent_point,
|
||||
target: SnapTarget::Geometry(GeometrySnapTarget::Tangent),
|
||||
distance,
|
||||
tolerance,
|
||||
curves: [Some(path.document_curve), None],
|
||||
source: point.source,
|
||||
contrained: true,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SnapCandidatePath {
|
||||
document_curve: Bezier,
|
||||
layer: LayerNodeIdentifier,
|
||||
start: ManipulatorGroupId,
|
||||
target: SnapTarget,
|
||||
bounds: Option<Quad>,
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SnapCandidatePoint {
|
||||
pub document_point: DVec2,
|
||||
pub source: SnapSource,
|
||||
pub target: SnapTarget,
|
||||
pub source_index: usize,
|
||||
pub quad: Option<Quad>,
|
||||
pub neighbors: Vec<DVec2>,
|
||||
}
|
||||
impl SnapCandidatePoint {
|
||||
pub fn new(document_point: DVec2, source: SnapSource, target: SnapTarget) -> Self {
|
||||
Self::new_quad(document_point, source, target, None)
|
||||
}
|
||||
pub fn new_quad(document_point: DVec2, source: SnapSource, target: SnapTarget, quad: Option<Quad>) -> Self {
|
||||
Self {
|
||||
document_point,
|
||||
source,
|
||||
target,
|
||||
quad: quad,
|
||||
..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::Sharp))
|
||||
}
|
||||
pub fn handle_neighbours(document_point: DVec2, neighbours: impl Into<Vec<DVec2>>) -> Self {
|
||||
let mut point = Self::new_source(document_point, SnapSource::Geometry(GeometrySnapSource::Sharp));
|
||||
point.neighbors = neighbours.into();
|
||||
point
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct BBoxSnapValues {
|
||||
corner_source: SnapSource,
|
||||
corner_target: SnapTarget,
|
||||
edge_source: SnapSource,
|
||||
edge_target: SnapTarget,
|
||||
centre_source: SnapSource,
|
||||
centre_target: SnapTarget,
|
||||
}
|
||||
impl BBoxSnapValues {
|
||||
pub const BOUNDING_BOX: Self = Self {
|
||||
corner_source: SnapSource::BoundingBox(BoundingBoxSnapSource::Corner),
|
||||
corner_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::Corner),
|
||||
edge_source: SnapSource::BoundingBox(BoundingBoxSnapSource::EdgeMidpoint),
|
||||
edge_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::EdgeMidpoint),
|
||||
centre_source: SnapSource::BoundingBox(BoundingBoxSnapSource::Centre),
|
||||
centre_target: SnapTarget::BoundingBox(BoundingBoxSnapTarget::Centre),
|
||||
};
|
||||
}
|
||||
fn get_bbox_points(quad: Quad, points: &mut Vec<SnapCandidatePoint>, values: BBoxSnapValues, document: &DocumentMessageHandler) {
|
||||
for index in 0..4 {
|
||||
let start = quad.0[index];
|
||||
let end = quad.0[(index + 1) % 4];
|
||||
if document.snapping_state.target_enabled(values.corner_target) {
|
||||
points.push(SnapCandidatePoint::new_quad(start, values.corner_source, values.corner_target, Some(quad)));
|
||||
}
|
||||
if document.snapping_state.target_enabled(values.edge_target) {
|
||||
points.push(SnapCandidatePoint::new_quad((start + end) / 2., values.edge_source, values.edge_target, Some(quad)));
|
||||
}
|
||||
}
|
||||
if document.snapping_state.target_enabled(values.centre_target) {
|
||||
points.push(SnapCandidatePoint::new_quad(quad.center(), values.centre_source, values.centre_target, Some(quad)));
|
||||
}
|
||||
}
|
||||
|
||||
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<ManipulatorGroupId>, 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)) {
|
||||
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;
|
||||
}
|
||||
|
||||
let in_handle = curve.handle_start().map(|handle| handle - curve.start).filter(handle_not_under(to_document));
|
||||
let out_handle = curve.handle_end().map(|handle| handle - curve.end).filter(handle_not_under(to_document));
|
||||
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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Anchors
|
||||
for (index, group) in subpath.manipulator_groups().iter().enumerate() {
|
||||
if snap_data.ignore_manipulator(layer, group.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let smooth = group_smooth(group, to_document, subpath, index);
|
||||
|
||||
if smooth && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Smooth)) {
|
||||
// Smooth points
|
||||
points.push(SnapCandidatePoint::new(
|
||||
to_document.transform_point2(group.anchor),
|
||||
SnapSource::Geometry(GeometrySnapSource::Smooth),
|
||||
SnapTarget::Geometry(GeometrySnapTarget::Smooth),
|
||||
));
|
||||
} else if !smooth && document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::Sharp)) {
|
||||
// Sharp points
|
||||
points.push(SnapCandidatePoint::new(
|
||||
to_document.transform_point2(group.anchor),
|
||||
SnapSource::Geometry(GeometrySnapSource::Sharp),
|
||||
SnapTarget::Geometry(GeometrySnapTarget::Sharp),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group_smooth(group: &bezier_rs::ManipulatorGroup<ManipulatorGroupId>, to_document: DAffine2, subpath: &Subpath<ManipulatorGroupId>, index: usize) -> bool {
|
||||
let anchor = group.anchor;
|
||||
let handle_in = group.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
|
||||
let handle_out = group.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document));
|
||||
let at_end = !subpath.closed() && (index == 0 || index == subpath.len() - 1);
|
||||
let smooth = handle_in.is_some_and(|handle_in| handle_out.is_some_and(|handle_out| handle_in.angle_between(handle_out) < 1e-5)) && !at_end;
|
||||
smooth
|
||||
}
|
||||
pub fn get_layer_snap_points(layer: LayerNodeIdentifier, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>) {
|
||||
let document = snap_data.document;
|
||||
if document.metadata().is_artboard(layer) {
|
||||
} else if document.metadata().is_folder(layer) {
|
||||
for child in layer.decendants(document.metadata()) {
|
||||
get_layer_snap_points(child, snap_data, points);
|
||||
}
|
||||
} else {
|
||||
// Skip empty paths
|
||||
if document.metadata.layer_outline(layer).next().is_none() {
|
||||
return;
|
||||
}
|
||||
let to_document = document.metadata.transform_to_document(layer);
|
||||
for subpath in document.metadata.layer_outline(layer) {
|
||||
subpath_anchor_snap_points(layer, subpath, snap_data, points, to_document);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{SnapSource, SnapTarget};
|
||||
use bezier_rs::Bezier;
|
||||
use glam::DVec2;
|
||||
use graphene_core::renderer::Quad;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SnapResults {
|
||||
pub points: Vec<SnappedPoint>,
|
||||
pub grid_lines: Vec<SnappedLine>,
|
||||
pub curves: Vec<SnappedCurve>,
|
||||
}
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SnappedPoint {
|
||||
pub snapped_point_document: DVec2,
|
||||
pub source: SnapSource,
|
||||
pub target: SnapTarget,
|
||||
pub at_intersection: bool,
|
||||
pub contrained: bool, // Found when looking for contrained
|
||||
pub target_bounds: Option<Quad>,
|
||||
pub source_bounds: Option<Quad>,
|
||||
pub curves: [Option<Bezier>; 2],
|
||||
pub distance: f64,
|
||||
pub tolerance: f64,
|
||||
}
|
||||
impl SnappedPoint {
|
||||
pub fn infinite_snap(snapped_point_document: DVec2) -> Self {
|
||||
Self {
|
||||
snapped_point_document,
|
||||
distance: f64::INFINITY,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn from_source_point(snapped_point_document: DVec2, source: SnapSource) -> Self {
|
||||
Self {
|
||||
snapped_point_document,
|
||||
source,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn other_snap_better(&self, other: &Self) -> bool {
|
||||
if self.distance.is_finite() && !other.distance.is_finite() {
|
||||
return false;
|
||||
}
|
||||
if !self.distance.is_finite() && other.distance.is_finite() {
|
||||
return true;
|
||||
}
|
||||
|
||||
let my_dist = self.distance;
|
||||
let other_dist = other.distance;
|
||||
|
||||
// Prevent flickering when two points are equally close
|
||||
let bias = 1e-2;
|
||||
|
||||
// Prefer closest
|
||||
let other_closer = other_dist < my_dist + bias;
|
||||
|
||||
// We should prefer the most contrained option (e.g. intersection > path)
|
||||
let other_more_contrained = other.contrained && !self.contrained;
|
||||
let self_more_contrained = self.contrained && !other.contrained;
|
||||
|
||||
// Prefer nodes to intersections if both are at the same position
|
||||
let contrained_at_same_pos = other.contrained && self.contrained && self.snapped_point_document.abs_diff_eq(other.snapped_point_document, 1.);
|
||||
let other_better_constraint = contrained_at_same_pos && self.at_intersection && !other.at_intersection;
|
||||
let self_better_constraint = contrained_at_same_pos && other.at_intersection && !self.at_intersection;
|
||||
|
||||
(other_closer || other_more_contrained || other_better_constraint) && !self_more_contrained && !self_better_constraint
|
||||
}
|
||||
pub fn is_snapped(&self) -> bool {
|
||||
self.distance.is_finite()
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SnappedLine {
|
||||
pub point: SnappedPoint,
|
||||
pub direction: DVec2,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SnappedCurve {
|
||||
pub layer: LayerNodeIdentifier,
|
||||
pub start: ManipulatorGroupId,
|
||||
pub point: SnappedPoint,
|
||||
pub document_curve: Bezier,
|
||||
}
|
||||
|
|
@ -8,6 +8,14 @@ use graphene_core::renderer::Quad;
|
|||
|
||||
use glam::{DAffine2, DVec2};
|
||||
|
||||
use super::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
|
||||
|
||||
pub struct SizeSnapData<'a> {
|
||||
pub manager: &'a mut SnapManager,
|
||||
pub points: &'a mut Vec<SnapCandidatePoint>,
|
||||
pub snap_data: SnapData<'a>,
|
||||
}
|
||||
|
||||
/// Contains the edges that are being dragged along with the original bounds.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SelectedEdges {
|
||||
|
|
@ -60,7 +68,7 @@ impl SelectedEdges {
|
|||
}
|
||||
|
||||
/// Computes the new bounds with the given mouse move and modifier keys
|
||||
pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center: bool, center_around: DVec2, constrain: bool) -> (DVec2, DVec2) {
|
||||
pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center_around: Option<DVec2>, constrain: bool, snap: Option<SizeSnapData>) -> (DVec2, DVec2) {
|
||||
let mouse = transform.inverse().transform_point2(mouse);
|
||||
|
||||
let mut min = self.bounds[0];
|
||||
|
|
@ -77,7 +85,7 @@ impl SelectedEdges {
|
|||
}
|
||||
|
||||
let mut pivot = self.pivot_from_bounds(min, max);
|
||||
if center {
|
||||
if let Some(center_around) = center_around {
|
||||
// The below ratio is: `dragging edge / being centered`.
|
||||
// The `is_finite()` checks are in case the user is dragging the edge where the pivot is located (in which case the centering mode is ignored).
|
||||
if self.top {
|
||||
|
|
@ -120,6 +128,56 @@ impl SelectedEdges {
|
|||
let delta_size = new_size - size;
|
||||
min -= delta_size * min_pivot;
|
||||
max = min + new_size;
|
||||
} else if let Some(SizeSnapData { manager, points, snap_data }) = snap {
|
||||
let view_to_doc = snap_data.document.metadata.document_to_viewport.inverse();
|
||||
let bounds_to_doc = view_to_doc * transform;
|
||||
let mut best_snap = SnappedPoint::infinite_snap(pivot);
|
||||
let mut best_scale_factor = DVec2::ONE;
|
||||
let tolerance = snapping::snap_tolerance(snap_data.document);
|
||||
for point in points {
|
||||
let old_position = point.document_point;
|
||||
let bounds_space = bounds_to_doc.inverse().transform_point2(point.document_point);
|
||||
let normalised = (bounds_space - self.bounds[0]) / (self.bounds[1] - self.bounds[0]);
|
||||
let updated = normalised * (max - min) + min;
|
||||
point.document_point = bounds_to_doc.transform_point2(updated);
|
||||
let mut snapped = if !(self.top || self.bottom) || !(self.left || self.right) {
|
||||
let axis = if !(self.top || self.bottom) { DVec2::X } else { DVec2::Y };
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: point.document_point,
|
||||
direction: bounds_to_doc.transform_vector2(axis),
|
||||
};
|
||||
manager.constrained_snap(&snap_data, point, constraint, None)
|
||||
} else {
|
||||
manager.free_snap(&snap_data, point, None, false)
|
||||
};
|
||||
point.document_point = old_position;
|
||||
|
||||
if !snapped.is_snapped() {
|
||||
continue;
|
||||
}
|
||||
let snapped_bounds = bounds_to_doc.inverse().transform_point2(snapped.snapped_point_document);
|
||||
|
||||
let mut scale_factor = (snapped_bounds - pivot) / (updated - pivot);
|
||||
if !(self.left || self.right) {
|
||||
scale_factor.x = 1.
|
||||
}
|
||||
if !(self.top || self.bottom) {
|
||||
scale_factor.y = 1.
|
||||
}
|
||||
|
||||
snapped.distance = bounds_to_doc.transform_vector2((max - min) * (scale_factor - DVec2::ONE)).length();
|
||||
if snapped.distance > tolerance || !snapped.distance.is_finite() {
|
||||
continue;
|
||||
}
|
||||
if best_snap.other_snap_better(&snapped) {
|
||||
best_snap = snapped;
|
||||
best_scale_factor = scale_factor;
|
||||
}
|
||||
}
|
||||
manager.update_indicator(best_snap);
|
||||
|
||||
min = pivot - (pivot - min) * best_scale_factor;
|
||||
max = pivot - (pivot - max) * best_scale_factor;
|
||||
}
|
||||
|
||||
(min, max - min)
|
||||
|
|
|
|||
|
|
@ -116,12 +116,7 @@ impl ArtboardToolData {
|
|||
Some(edges)
|
||||
}
|
||||
|
||||
fn start_resizing(&mut self, selected_edges: (bool, bool, bool, bool), document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
let snap_x = selected_edges.2 || selected_edges.3;
|
||||
let snap_y = selected_edges.0 || selected_edges.1;
|
||||
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(), snap_x, snap_y);
|
||||
|
||||
fn start_resizing(&mut self, _selected_edges: (bool, bool, bool, bool), _document: &DocumentMessageHandler, _input: &InputPreprocessorMessageHandler) {
|
||||
if let Some(bounds) = &mut self.bounding_box_manager {
|
||||
bounds.center_of_transformation = (bounds.bounds[0] + bounds.bounds[1]) / 2.;
|
||||
}
|
||||
|
|
@ -138,7 +133,11 @@ impl ArtboardToolData {
|
|||
if let Some(intersection) = intersections.next() {
|
||||
self.selected_artboard = Some(intersection);
|
||||
|
||||
self.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
if let Some(bounds) = document.metadata().bounding_box_document(intersection) {
|
||||
let bounding_box_manager = self.bounding_box_manager.get_or_insert(BoundingBoxManager::default());
|
||||
bounding_box_manager.bounds = bounds;
|
||||
bounding_box_manager.transform = document.metadata().document_to_viewport;
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
|
|
@ -150,7 +149,7 @@ impl ArtboardToolData {
|
|||
}
|
||||
}
|
||||
|
||||
fn resize_artboard(&mut self, responses: &mut VecDeque<Message>, document: &DocumentMessageHandler, mouse_position: DVec2, from_center: bool, constrain_square: bool) {
|
||||
fn resize_artboard(&mut self, responses: &mut VecDeque<Message>, _document: &DocumentMessageHandler, mouse_position: DVec2, from_center: bool, constrain_square: bool) {
|
||||
let Some(bounds) = &self.bounding_box_manager else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -158,9 +157,8 @@ impl ArtboardToolData {
|
|||
return;
|
||||
};
|
||||
|
||||
let snapped_mouse_position: DVec2 = self.snap_manager.snap_position(responses, document, mouse_position);
|
||||
|
||||
let (position, size) = movement.new_size(snapped_mouse_position, bounds.transform, from_center, bounds.center_of_transformation, constrain_square);
|
||||
let centre = from_center.then_some(bounds.center_of_transformation);
|
||||
let (position, size) = movement.new_size(mouse_position, bounds.transform, centre, constrain_square, None);
|
||||
responses.add(GraphOperationMessage::ResizeArtboard {
|
||||
id: self.selected_artboard.unwrap().to_node(),
|
||||
location: position.round().as_ivec2(),
|
||||
|
|
@ -223,16 +221,9 @@ impl Fsm for ArtboardToolFsmState {
|
|||
(ArtboardToolFsmState::Dragging, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, .. }) => {
|
||||
if let Some(bounds) = &tool_data.bounding_box_manager {
|
||||
let axis_align = input.keyboard.get(constrain_axis_or_aspect as usize);
|
||||
|
||||
let mouse_position = axis_align_drag(axis_align, input.mouse.position, tool_data.drag_start);
|
||||
let mouse_delta = mouse_position - tool_data.drag_current;
|
||||
|
||||
let snap = bounds.evaluate_transform_handle_positions().into_iter().collect();
|
||||
let closest_move = tool_data.snap_manager.snap_layers(responses, document, snap, mouse_delta);
|
||||
|
||||
let size = bounds.bounds[1] - bounds.bounds[0];
|
||||
|
||||
let position = bounds.bounds[0] + bounds.transform.inverse().transform_vector2(mouse_position - tool_data.drag_current + closest_move);
|
||||
let position = bounds.bounds[0] + bounds.transform.inverse().transform_vector2(mouse_position - tool_data.drag_current);
|
||||
|
||||
responses.add(GraphOperationMessage::ResizeArtboard {
|
||||
id: tool_data.selected_artboard.unwrap().to_node(),
|
||||
|
|
@ -242,13 +233,13 @@ impl Fsm for ArtboardToolFsmState {
|
|||
|
||||
responses.add(BroadcastEvent::DocumentIsDirty);
|
||||
|
||||
tool_data.drag_current = mouse_position + closest_move;
|
||||
tool_data.drag_current = mouse_position;
|
||||
}
|
||||
ArtboardToolFsmState::Dragging
|
||||
}
|
||||
(ArtboardToolFsmState::Drawing, ArtboardToolMessage::PointerMove { constrain_axis_or_aspect, center }) => {
|
||||
let mouse_position = input.mouse.position;
|
||||
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
let snapped_mouse_position = mouse_position; //tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
|
||||
let root_transform = document.metadata().document_to_viewport.inverse();
|
||||
|
||||
|
|
@ -277,7 +268,8 @@ impl Fsm for ArtboardToolFsmState {
|
|||
let id = NodeId(generate_uuid());
|
||||
tool_data.selected_artboard = Some(LayerNodeIdentifier::new_unchecked(id));
|
||||
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
//tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
//tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
|
||||
responses.add(GraphOperationMessage::NewArtboard {
|
||||
id,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapData;
|
||||
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::uuid::generate_uuid;
|
||||
|
|
@ -48,7 +50,7 @@ pub enum EllipseOptionsUpdate {
|
|||
pub enum EllipseToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
CanvasTransformed,
|
||||
Overlays(OverlayContext),
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
|
|
@ -57,7 +59,7 @@ pub enum EllipseToolMessage {
|
|||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
Resize {
|
||||
PointerMove {
|
||||
center: Key,
|
||||
lock_ratio: Key,
|
||||
},
|
||||
|
|
@ -147,11 +149,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Ellipse
|
|||
match self.fsm_state {
|
||||
Ready => actions!(EllipseToolMessageDiscriminant;
|
||||
DragStart,
|
||||
PointerMove,
|
||||
),
|
||||
Drawing => actions!(EllipseToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
PointerMove,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -160,7 +163,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Ellipse
|
|||
impl ToolTransition for EllipseTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
canvas_transformed: Some(EllipseToolMessage::CanvasTransformed.into()),
|
||||
overlay_provider: Some(|overlay_context| EllipseToolMessage::Overlays(overlay_context).into()),
|
||||
tool_abort: Some(EllipseToolMessage::Abort.into()),
|
||||
working_color_changed: Some(EllipseToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
|
|
@ -195,12 +198,12 @@ impl Fsm for EllipseToolFsmState {
|
|||
return self;
|
||||
};
|
||||
match (self, event) {
|
||||
(EllipseToolFsmState::Drawing, EllipseToolMessage::CanvasTransformed) => {
|
||||
tool_data.data.recalculate_snaps(document, input);
|
||||
(_, EllipseToolMessage::Overlays(mut overlay_context)) => {
|
||||
shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
self
|
||||
}
|
||||
(EllipseToolFsmState::Ready, EllipseToolMessage::DragStart) => {
|
||||
shape_data.start(responses, document, input);
|
||||
shape_data.start(document, input);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// Create a new ellipse vector shape
|
||||
|
|
@ -223,12 +226,17 @@ impl Fsm for EllipseToolFsmState {
|
|||
|
||||
EllipseToolFsmState::Drawing
|
||||
}
|
||||
(state, EllipseToolMessage::Resize { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(responses, document, input, center, lock_ratio, false) {
|
||||
(EllipseToolFsmState::Drawing, EllipseToolMessage::PointerMove { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(document, input, center, lock_ratio, false) {
|
||||
responses.add(message);
|
||||
}
|
||||
|
||||
state
|
||||
self
|
||||
}
|
||||
(_, EllipseToolMessage::PointerMove { .. }) => {
|
||||
shape_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(EllipseToolFsmState::Drawing, EllipseToolMessage::DragStop) => {
|
||||
input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ pub struct GradientOptions {
|
|||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Gradient)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)]
|
||||
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)]
|
||||
pub enum GradientToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
|
|
@ -267,10 +267,6 @@ struct GradientToolData {
|
|||
drag_start: DVec2,
|
||||
}
|
||||
|
||||
pub fn start_snap(snap_manager: &mut SnapManager, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
}
|
||||
|
||||
impl Fsm for GradientToolFsmState {
|
||||
type ToolData = GradientToolData;
|
||||
type ToolOptions = GradientOptions;
|
||||
|
|
@ -303,7 +299,7 @@ impl Fsm for GradientToolFsmState {
|
|||
let Gradient { start, end, positions, .. } = gradient;
|
||||
let (start, end) = (transform.transform_point2(start), transform.transform_point2(end));
|
||||
|
||||
overlay_context.line(start, end);
|
||||
overlay_context.line(start, end, None);
|
||||
overlay_context.handle(start, dragging == Some(GradientDragTarget::Start));
|
||||
overlay_context.handle(end, dragging == Some(GradientDragTarget::End));
|
||||
|
||||
|
|
@ -430,7 +426,6 @@ impl Fsm for GradientToolFsmState {
|
|||
let pos = transform.transform_point2(pos);
|
||||
if pos.distance_squared(mouse) < tolerance {
|
||||
dragging = true;
|
||||
start_snap(&mut tool_data.snap_manager, document, input);
|
||||
tool_data.selected_gradient = Some(SelectedGradient {
|
||||
layer,
|
||||
transform,
|
||||
|
|
@ -474,8 +469,6 @@ impl Fsm for GradientToolFsmState {
|
|||
|
||||
tool_data.selected_gradient = Some(selected_gradient);
|
||||
|
||||
start_snap(&mut tool_data.snap_manager, document, input);
|
||||
|
||||
GradientToolFsmState::Drawing
|
||||
} else {
|
||||
GradientToolFsmState::Ready
|
||||
|
|
@ -484,7 +477,7 @@ impl Fsm for GradientToolFsmState {
|
|||
}
|
||||
(GradientToolFsmState::Drawing, GradientToolMessage::PointerMove { constrain_axis }) => {
|
||||
if let Some(selected_gradient) = &mut tool_data.selected_gradient {
|
||||
let mouse = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let mouse = input.mouse.position; // tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
selected_gradient.update_gradient(mouse, responses, input.keyboard.get(constrain_axis as usize), selected_gradient.gradient.gradient_type);
|
||||
}
|
||||
GradientToolFsmState::Drawing
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ impl Fsm for ImaginateToolFsmState {
|
|||
self
|
||||
}
|
||||
(ImaginateToolFsmState::Ready, ImaginateToolMessage::DragStart) => {
|
||||
shape_data.start(responses, document, input);
|
||||
shape_data.start(document, input);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
shape_data.layer = Some(LayerNodeIdentifier::new(NodeId(generate_uuid()), document.network()));
|
||||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
|
|
@ -168,7 +168,7 @@ impl Fsm for ImaginateToolFsmState {
|
|||
ImaginateToolFsmState::Drawing
|
||||
}
|
||||
(state, ImaginateToolMessage::Resize { center, lock_ratio }) => {
|
||||
let message = shape_data.calculate_transform(responses, document, input, center, lock_ratio, true);
|
||||
let message = shape_data.calculate_transform(document, input, center, lock_ratio, true);
|
||||
responses.try_add(message);
|
||||
|
||||
state
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
|
||||
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::uuid::generate_uuid;
|
||||
|
|
@ -39,7 +40,7 @@ pub enum LineToolMessage {
|
|||
#[remain::unsorted]
|
||||
DocumentIsDirty,
|
||||
#[remain::unsorted]
|
||||
CanvasTransformed,
|
||||
Overlays(OverlayContext),
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
|
|
@ -48,7 +49,7 @@ pub enum LineToolMessage {
|
|||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
Redraw {
|
||||
PointerMove {
|
||||
center: Key,
|
||||
lock_angle: Key,
|
||||
snap_angle: Key,
|
||||
|
|
@ -127,8 +128,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for LineToo
|
|||
|
||||
fn actions(&self) -> ActionList {
|
||||
match self.fsm_state {
|
||||
LineToolFsmState::Ready => actions!(LineToolMessageDiscriminant; DragStart),
|
||||
LineToolFsmState::Drawing => actions!(LineToolMessageDiscriminant; DragStop, Redraw, Abort),
|
||||
LineToolFsmState::Ready => actions!(LineToolMessageDiscriminant; DragStart, PointerMove),
|
||||
LineToolFsmState::Drawing => actions!(LineToolMessageDiscriminant; DragStop, PointerMove, Abort),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -136,7 +137,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for LineToo
|
|||
impl ToolTransition for LineTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
canvas_transformed: Some(LineToolMessage::CanvasTransformed.into()),
|
||||
overlay_provider: Some(|overlay_context| LineToolMessage::Overlays(overlay_context).into()),
|
||||
tool_abort: Some(LineToolMessage::Abort.into()),
|
||||
working_color_changed: Some(LineToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
|
|
@ -174,11 +175,14 @@ impl Fsm for LineToolFsmState {
|
|||
return self;
|
||||
};
|
||||
match (self, event) {
|
||||
(_, LineToolMessage::Overlays(mut overlay_context)) => {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
self
|
||||
}
|
||||
(LineToolFsmState::Ready, LineToolMessage::DragStart) => {
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
|
||||
let viewport_start = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
tool_data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(viewport_start);
|
||||
let point = SnapCandidatePoint::handle(document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position));
|
||||
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
|
||||
tool_data.drag_start = snapped.snapped_point_document;
|
||||
|
||||
let subpath = bezier_rs::Subpath::new_line(DVec2::ZERO, DVec2::X);
|
||||
|
||||
|
|
@ -195,15 +199,21 @@ impl Fsm for LineToolFsmState {
|
|||
|
||||
LineToolFsmState::Drawing
|
||||
}
|
||||
(LineToolFsmState::Drawing, LineToolMessage::Redraw { center, snap_angle, lock_angle }) => {
|
||||
tool_data.drag_current = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
(LineToolFsmState::Drawing, LineToolMessage::PointerMove { center, snap_angle, lock_angle }) => {
|
||||
tool_data.drag_current = input.mouse.position; // tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
let keyboard = &input.keyboard;
|
||||
let transform = document.metadata().document_to_viewport;
|
||||
responses.add(generate_transform(tool_data, transform, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center)));
|
||||
let ignore = if let Some(layer) = tool_data.layer { vec![layer] } else { vec![] };
|
||||
let snap_data = SnapData::ignore(document, input, &ignore);
|
||||
responses.add(generate_transform(tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center)));
|
||||
|
||||
LineToolFsmState::Drawing
|
||||
}
|
||||
(_, LineToolMessage::PointerMove { .. }) => {
|
||||
tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(LineToolFsmState::Drawing, LineToolMessage::DragStop) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
input.mouse.finish_transaction(tool_data.drag_start, responses);
|
||||
|
|
@ -250,38 +260,71 @@ impl Fsm for LineToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
fn generate_transform(tool_data: &mut LineToolData, document_to_viewport: DAffine2, lock_angle: bool, snap_angle: bool, center: bool) -> Message {
|
||||
let mut start = document_to_viewport.transform_point2(tool_data.drag_start);
|
||||
let line_vector = tool_data.drag_current - start;
|
||||
|
||||
let mut angle = -line_vector.angle_between(DVec2::X);
|
||||
fn generate_transform(tool_data: &mut LineToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, centre: bool) -> Message {
|
||||
let document_to_viewport = snap_data.document.metadata.document_to_viewport;
|
||||
let mut document_points = [tool_data.drag_start, document_to_viewport.inverse().transform_point2(tool_data.drag_current)];
|
||||
|
||||
let mut angle = -(document_points[1] - document_points[0]).angle_between(DVec2::X);
|
||||
let mut line_length = (document_points[1] - document_points[0]).length();
|
||||
if lock_angle {
|
||||
angle = tool_data.angle;
|
||||
}
|
||||
|
||||
if snap_angle {
|
||||
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
}
|
||||
|
||||
tool_data.angle = angle;
|
||||
|
||||
let mut line_length = line_vector.length();
|
||||
|
||||
if lock_angle {
|
||||
let angle_vec = DVec2::new(angle.cos(), angle.sin());
|
||||
line_length = line_vector.dot(angle_vec);
|
||||
line_length = (document_points[1] - document_points[0]).dot(angle_vec);
|
||||
}
|
||||
document_points[1] = document_points[0] + line_length * DVec2::new(angle.cos(), angle.sin());
|
||||
|
||||
let constrained = snap_angle || lock_angle;
|
||||
let snap = &mut tool_data.snap_manager;
|
||||
|
||||
let near_point = SnapCandidatePoint::handle_neighbours(document_points[1], [tool_data.drag_start]);
|
||||
let far_point = SnapCandidatePoint::handle_neighbours(2. * document_points[0] - document_points[1], [tool_data.drag_start]);
|
||||
|
||||
if constrained {
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: document_points[0],
|
||||
direction: document_points[1] - document_points[0],
|
||||
};
|
||||
if centre {
|
||||
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, None);
|
||||
let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, None);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
|
||||
document_points[0] = best.snapped_point_document;
|
||||
snap.update_indicator(best);
|
||||
} else {
|
||||
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, None);
|
||||
document_points[1] = snapped.snapped_point_document;
|
||||
snap.update_indicator(snapped);
|
||||
}
|
||||
} else if centre {
|
||||
let snapped = snap.free_snap(&snap_data, &near_point, None, false);
|
||||
let snapped_far = snap.free_snap(&snap_data, &far_point, None, false);
|
||||
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
|
||||
document_points[1] = document_points[0] * 2. - best.snapped_point_document;
|
||||
document_points[0] = best.snapped_point_document;
|
||||
snap.update_indicator(best);
|
||||
} else {
|
||||
let snapped = snap.free_snap(&snap_data, &near_point, None, false);
|
||||
document_points[1] = snapped.snapped_point_document;
|
||||
snap.update_indicator(snapped);
|
||||
}
|
||||
|
||||
if center {
|
||||
start -= line_length * DVec2::new(angle.cos(), angle.sin());
|
||||
line_length *= 2.;
|
||||
}
|
||||
// Used for keeping the same angle next frame
|
||||
tool_data.angle = -(document_points[1] - document_points[0]).angle_between(DVec2::X);
|
||||
|
||||
let viewport_points = [document_to_viewport.transform_point2(document_points[0]), document_to_viewport.transform_point2(document_points[1])];
|
||||
let line_length = (viewport_points[1] - viewport_points[0]).length();
|
||||
let angle = -(viewport_points[1] - viewport_points[0]).angle_between(DVec2::X);
|
||||
GraphOperationMessage::TransformSet {
|
||||
layer: tool_data.layer.unwrap(),
|
||||
transform: glam::DAffine2::from_scale_angle_translation(DVec2::new(line_length, 1.), angle, start),
|
||||
transform: glam::DAffine2::from_scale_angle_translation(DVec2::new(line_length, 1.), angle, viewport_points[0]),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
|
|||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{get_manipulator_from_id, get_mirror_handles, get_subpaths};
|
||||
use crate::messages::tool::common_functionality::shape_editor::{ManipulatorAngle, ManipulatorPointInfo, OpposingHandleLengths, SelectedPointsInfo, ShapeState};
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapData, SnapManager};
|
||||
|
||||
use graph_craft::document::NodeNetwork;
|
||||
use graphene_core::renderer::Quad;
|
||||
|
|
@ -297,9 +297,9 @@ impl PathToolData {
|
|||
}
|
||||
|
||||
// Move the selected points with the mouse
|
||||
let snapped_position = self.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
shape_editor.move_selected_points(&document.network, &document.metadata, snapped_position - self.previous_mouse_position, shift, responses);
|
||||
self.previous_mouse_position = snapped_position;
|
||||
let snapped_delta = shape_editor.snap(&mut self.snap_manager, document, input, self.previous_mouse_position);
|
||||
shape_editor.move_selected_points(&document.network, &document.metadata, snapped_delta, shift, responses);
|
||||
self.previous_mouse_position += snapped_delta;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -330,6 +330,8 @@ impl Fsm for PathToolFsmState {
|
|||
|
||||
if self == Self::DrawingBox {
|
||||
overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position]))
|
||||
} else if self == Self::Dragging {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
}
|
||||
|
||||
responses.add(PathToolMessage::SelectedPointUpdated);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_subpaths;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapManager;
|
||||
use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
|
||||
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::uuid::{generate_uuid, ManipulatorGroupId};
|
||||
|
|
@ -44,8 +44,6 @@ impl Default for PenOptions {
|
|||
pub enum PenToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
CanvasTransformed,
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
SelectionChanged,
|
||||
|
|
@ -171,6 +169,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
|
|||
DragStop,
|
||||
Confirm,
|
||||
Abort,
|
||||
PointerMove,
|
||||
),
|
||||
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
|
||||
DragStart,
|
||||
|
|
@ -186,7 +185,6 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
|
|||
impl ToolTransition for PenTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
canvas_transformed: Some(PenToolMessage::CanvasTransformed.into()),
|
||||
tool_abort: Some(PenToolMessage::Abort.into()),
|
||||
selection_changed: Some(PenToolMessage::SelectionChanged.into()),
|
||||
working_color_changed: Some(PenToolMessage::WorkingColorChanged.into()),
|
||||
|
|
@ -249,9 +247,11 @@ impl PenToolData {
|
|||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
|
||||
// Get the position and set properties
|
||||
let transform = document.metadata().transform_to_viewport(parent);
|
||||
let snapped_position = self.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let start_position = transform.inverse().transform_point2(snapped_position);
|
||||
let transform = document.metadata().transform_to_document(parent);
|
||||
let point = SnapCandidatePoint::handle(document.metadata.document_to_viewport.inverse().transform_point2(input.mouse.position));
|
||||
let snapped = self.snap_manager.free_snap(&SnapData::new(document, input), &point, None, false);
|
||||
let start_position = transform.inverse().transform_point2(snapped.snapped_point_document);
|
||||
self.snap_manager.update_indicator(snapped);
|
||||
self.weight = line_weight;
|
||||
|
||||
// Create the initial shape with a `bez_path` (only contains a moveto initially)
|
||||
|
|
@ -293,6 +293,7 @@ impl PenToolData {
|
|||
let previous_anchor = previous_manipulator_group.anchor;
|
||||
|
||||
// Break the control
|
||||
let transform = document.metadata.document_to_viewport * transform;
|
||||
let on_top = transform.transform_point2(last_anchor).distance_squared(transform.transform_point2(previous_anchor)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2);
|
||||
if !on_top {
|
||||
return None;
|
||||
|
|
@ -347,6 +348,7 @@ impl PenToolData {
|
|||
let first_anchor = first_manipulator_group.anchor;
|
||||
let last_in = inwards_handle.get_position(last_manipulator_group)?;
|
||||
|
||||
let transform = document.metadata.document_to_viewport * transform;
|
||||
let transformed_distance_between_squared = transform.transform_point2(last_anchor).distance_squared(transform.transform_point2(first_anchor));
|
||||
let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2);
|
||||
let should_close_path = transformed_distance_between_squared < snap_point_tolerance_squared && previous_manipulator_group.is_some();
|
||||
|
|
@ -394,7 +396,8 @@ impl PenToolData {
|
|||
Some(PenToolFsmState::PlacingAnchor)
|
||||
}
|
||||
|
||||
fn drag_handle(&mut self, document: &DocumentMessageHandler, transform: DAffine2, mouse: DVec2, modifiers: ModifierState, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
|
||||
fn drag_handle(&mut self, mut snap_data: SnapData, transform: DAffine2, mouse: DVec2, modifiers: ModifierState, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
|
||||
let document = snap_data.document;
|
||||
// Get subpath
|
||||
let subpath = &get_subpaths(self.layer?, &document.network)?[self.subpath_index];
|
||||
|
||||
|
|
@ -409,10 +412,10 @@ impl PenToolData {
|
|||
// Get manipulator points
|
||||
let last_anchor = last_manipulator_group.anchor;
|
||||
|
||||
let mouse = self.snap_manager.snap_position(responses, document, mouse);
|
||||
let pos = transform.inverse().transform_point2(mouse);
|
||||
let should_mirror = !modifiers.break_handle && self.should_mirror;
|
||||
|
||||
let pos = compute_snapped_angle(&mut self.angle, modifiers.lock_angle, modifiers.snap_angle, pos, last_anchor);
|
||||
snap_data.manipulators = vec![(self.layer?, last_manipulator_group.id)];
|
||||
let pos = self.compute_snapped_angle(snap_data, transform, modifiers.lock_angle, modifiers.snap_angle, should_mirror, mouse, Some(last_anchor), false);
|
||||
if !pos.is_finite() {
|
||||
return Some(PenToolFsmState::DraggingHandle);
|
||||
}
|
||||
|
|
@ -424,7 +427,6 @@ impl PenToolData {
|
|||
modification: VectorDataModification::SetManipulatorPosition { point, position: pos },
|
||||
});
|
||||
|
||||
let should_mirror = !modifiers.break_handle && self.should_mirror;
|
||||
// Mirror handle of last segment
|
||||
if should_mirror {
|
||||
// Could also be written as `last_anchor.position * 2 - pos` but this way avoids overflow/underflow better
|
||||
|
|
@ -446,7 +448,8 @@ impl PenToolData {
|
|||
Some(PenToolFsmState::DraggingHandle)
|
||||
}
|
||||
|
||||
fn place_anchor(&mut self, document: &DocumentMessageHandler, transform: DAffine2, mouse: DVec2, modifiers: ModifierState, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
|
||||
fn place_anchor(&mut self, mut snap_data: SnapData, transform: DAffine2, mouse: DVec2, modifiers: ModifierState, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
|
||||
let document = snap_data.document;
|
||||
// Get subpath
|
||||
let layer = self.layer?;
|
||||
let subpath = &get_subpaths(layer, &document.network)?[self.subpath_index];
|
||||
|
|
@ -463,23 +466,18 @@ impl PenToolData {
|
|||
// Get manipulator points
|
||||
let first_anchor = first_manipulator_group.anchor;
|
||||
|
||||
let mouse = self.snap_manager.snap_position(responses, document, mouse);
|
||||
let mut pos = transform.inverse().transform_point2(mouse);
|
||||
let previous_anchor = previous_manipulator_group.map(|group| group.anchor);
|
||||
|
||||
// Snap to the first point (to show close path)
|
||||
let show_close_path = mouse.distance_squared(transform.transform_point2(first_anchor)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2);
|
||||
if show_close_path {
|
||||
pos = first_anchor;
|
||||
}
|
||||
|
||||
if let Some(relative_previous_anchor) = previous_manipulator_group.map(|group| group.anchor) {
|
||||
let pos = if let Some(last_anchor) = previous_anchor.filter(|&a| mouse.distance_squared(transform.transform_point2(a)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2)) {
|
||||
// Snap to the previously placed point (to show break control)
|
||||
if mouse.distance_squared(transform.transform_point2(relative_previous_anchor)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2) {
|
||||
pos = relative_previous_anchor;
|
||||
} else {
|
||||
pos = compute_snapped_angle(&mut self.angle, modifiers.lock_angle, modifiers.snap_angle, pos, relative_previous_anchor);
|
||||
}
|
||||
}
|
||||
last_anchor
|
||||
} else if mouse.distance_squared(transform.transform_point2(first_anchor)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2) {
|
||||
// Snap to the first point (to show close path)
|
||||
first_anchor
|
||||
} else {
|
||||
snap_data.manipulators = vec![(self.layer?, last_manipulator_group.id)];
|
||||
self.compute_snapped_angle(snap_data, transform, modifiers.lock_angle, modifiers.snap_angle, false, mouse, previous_anchor, true)
|
||||
};
|
||||
|
||||
for manipulator_type in [SelectedType::Anchor, SelectedType::InHandle, SelectedType::OutHandle] {
|
||||
let point = ManipulatorPointId::new(last_manipulator_group.id, manipulator_type);
|
||||
|
|
@ -491,6 +489,65 @@ impl PenToolData {
|
|||
Some(PenToolFsmState::PlacingAnchor)
|
||||
}
|
||||
|
||||
/// Snap the angle of the line from relative to position if the key is pressed.
|
||||
fn compute_snapped_angle(&mut self, snap_data: SnapData, transform: DAffine2, lock_angle: bool, snap_angle: bool, mirror: bool, mouse: DVec2, relative: Option<DVec2>, neighbour: bool) -> DVec2 {
|
||||
let document = snap_data.document;
|
||||
let mut document_pos = document.metadata.document_to_viewport.inverse().transform_point2(mouse);
|
||||
let snap = &mut self.snap_manager;
|
||||
|
||||
let neighbours = relative.filter(|_| neighbour).map_or(Vec::new(), |neighbour| vec![neighbour]);
|
||||
|
||||
if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| snap_angle || lock_angle) {
|
||||
let resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
let angle = if lock_angle {
|
||||
self.angle
|
||||
} else {
|
||||
(-(relative - document_pos).angle_between(DVec2::X) / resolution).round() * resolution
|
||||
};
|
||||
document_pos = relative - (relative - document_pos).project_onto(DVec2::new(angle.cos(), angle.sin()));
|
||||
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: relative,
|
||||
direction: document_pos - relative,
|
||||
};
|
||||
let near_point = SnapCandidatePoint::handle_neighbours(document_pos, neighbours.clone());
|
||||
let far_point = SnapCandidatePoint::handle_neighbours(2. * relative - document_pos, neighbours);
|
||||
if mirror {
|
||||
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, None);
|
||||
let snapped_far = snap.constrained_snap(&snap_data, &far_point, constraint, None);
|
||||
document_pos = if snapped_far.other_snap_better(&snapped) {
|
||||
snapped.snapped_point_document
|
||||
} else {
|
||||
2. * relative - snapped_far.snapped_point_document
|
||||
};
|
||||
snap.update_indicator(if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far });
|
||||
} else {
|
||||
let snapped = snap.constrained_snap(&snap_data, &near_point, constraint, None);
|
||||
document_pos = snapped.snapped_point_document;
|
||||
snap.update_indicator(snapped);
|
||||
}
|
||||
} else if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)).filter(|_| mirror) {
|
||||
let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbours(document_pos, neighbours.clone()), None, false);
|
||||
let snapped_far = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbours(2. * relative - document_pos, neighbours), None, false);
|
||||
document_pos = if snapped_far.other_snap_better(&snapped) {
|
||||
snapped.snapped_point_document
|
||||
} else {
|
||||
2. * relative - snapped_far.snapped_point_document
|
||||
};
|
||||
snap.update_indicator(if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far });
|
||||
} else {
|
||||
let snapped = snap.free_snap(&snap_data, &SnapCandidatePoint::handle_neighbours(document_pos, neighbours), None, false);
|
||||
document_pos = snapped.snapped_point_document;
|
||||
snap.update_indicator(snapped);
|
||||
}
|
||||
|
||||
if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) {
|
||||
self.angle = -(relative - document_pos).angle_between(DVec2::X)
|
||||
}
|
||||
|
||||
transform.inverse().transform_point2(document_pos)
|
||||
}
|
||||
|
||||
fn finish_transaction(&mut self, fsm: PenToolFsmState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> Option<DocumentMessage> {
|
||||
// Get subpath
|
||||
let subpath = &get_subpaths(self.layer?, &document.network)?[self.subpath_index];
|
||||
|
|
@ -548,13 +605,13 @@ impl Fsm for PenToolFsmState {
|
|||
..
|
||||
} = tool_action_data;
|
||||
|
||||
let mut transform = tool_data.layer.map(|layer| document.metadata().transform_to_viewport(layer)).unwrap_or_default();
|
||||
let mut transform = tool_data.layer.map(|layer| document.metadata().transform_to_document(layer)).unwrap_or_default();
|
||||
|
||||
if !transform.inverse().is_finite() {
|
||||
let parent_transform = tool_data
|
||||
.layer
|
||||
.and_then(|layer| layer.parent(document.metadata()))
|
||||
.map(|layer| document.metadata().transform_to_viewport(layer));
|
||||
.map(|layer| document.metadata().transform_to_document(layer));
|
||||
|
||||
transform = parent_transform.unwrap_or(DAffine2::IDENTITY);
|
||||
}
|
||||
|
|
@ -567,16 +624,13 @@ impl Fsm for PenToolFsmState {
|
|||
return self;
|
||||
};
|
||||
match (self, event) {
|
||||
(_, PenToolMessage::CanvasTransformed) => {
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
self
|
||||
}
|
||||
(_, PenToolMessage::SelectionChanged) => {
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(_, PenToolMessage::Overlays(mut overlay_context)) => {
|
||||
path_overlays(document, shape_editor, &mut overlay_context);
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
self
|
||||
}
|
||||
|
|
@ -590,9 +644,6 @@ impl Fsm for PenToolFsmState {
|
|||
(PenToolFsmState::Ready, PenToolMessage::DragStart) => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// Initialize snapping
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
|
||||
// Disable this tool's mirroring
|
||||
tool_data.should_mirror = false;
|
||||
|
||||
|
|
@ -627,7 +678,10 @@ impl Fsm for PenToolFsmState {
|
|||
lock_angle: input.keyboard.key(lock_angle),
|
||||
break_handle: input.keyboard.key(break_handle),
|
||||
};
|
||||
tool_data.drag_handle(document, transform, input.mouse.position, modifiers, responses).unwrap_or(PenToolFsmState::Ready)
|
||||
let snap_data = SnapData::new(document, input);
|
||||
tool_data
|
||||
.drag_handle(snap_data, transform, input.mouse.position, modifiers, responses)
|
||||
.unwrap_or(PenToolFsmState::Ready)
|
||||
}
|
||||
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => {
|
||||
let modifiers = ModifierState {
|
||||
|
|
@ -636,9 +690,14 @@ impl Fsm for PenToolFsmState {
|
|||
break_handle: input.keyboard.key(break_handle),
|
||||
};
|
||||
tool_data
|
||||
.place_anchor(document, transform, input.mouse.position, modifiers, responses)
|
||||
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, modifiers, responses)
|
||||
.unwrap_or(PenToolFsmState::Ready)
|
||||
}
|
||||
(PenToolFsmState::Ready, PenToolMessage::PointerMove { .. }) => {
|
||||
tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Abort | PenToolMessage::Confirm) => {
|
||||
// Abort or commit the transaction to the undo history
|
||||
let message = tool_data.finish_transaction(self, document, responses).unwrap_or(DocumentMessage::AbortTransaction);
|
||||
|
|
@ -677,31 +736,6 @@ impl Fsm for PenToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Snap the angle of the line from relative to position if the key is pressed.
|
||||
fn compute_snapped_angle(cached_angle: &mut f64, lock_angle: bool, snap_angle: bool, position: DVec2, relative: DVec2) -> DVec2 {
|
||||
let delta = relative - position;
|
||||
let mut angle = -delta.angle_between(DVec2::X);
|
||||
|
||||
if lock_angle {
|
||||
angle = *cached_angle;
|
||||
}
|
||||
|
||||
if snap_angle {
|
||||
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
|
||||
angle = (angle / snap_resolution).round() * snap_resolution;
|
||||
}
|
||||
|
||||
*cached_angle = angle;
|
||||
|
||||
if snap_angle || lock_angle {
|
||||
let length = delta.length();
|
||||
let rotated = DVec2::new(length * angle.cos(), length * angle.sin());
|
||||
relative - rotated
|
||||
} else {
|
||||
position
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushes a [ManipulatorGroup] to the current layer via a [GraphOperationMessage].
|
||||
fn add_manipulator_group(layer: Option<LayerNodeIdentifier>, from_start: bool, manipulator_group: bezier_rs::ManipulatorGroup<ManipulatorGroupId>) -> Message {
|
||||
let Some(layer) = layer else {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapData;
|
||||
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::uuid::generate_uuid;
|
||||
|
|
@ -41,7 +43,7 @@ impl Default for PolygonOptions {
|
|||
pub enum PolygonToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
CanvasTransformed,
|
||||
Overlays(OverlayContext),
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
|
|
@ -50,7 +52,7 @@ pub enum PolygonToolMessage {
|
|||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
Resize {
|
||||
PointerMove {
|
||||
center: Key,
|
||||
lock_ratio: Key,
|
||||
},
|
||||
|
|
@ -187,11 +189,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Polygon
|
|||
match self.fsm_state {
|
||||
Ready => actions!(PolygonToolMessageDiscriminant;
|
||||
DragStart,
|
||||
PointerMove,
|
||||
),
|
||||
Drawing => actions!(PolygonToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
PointerMove,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +203,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Polygon
|
|||
impl ToolTransition for PolygonTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
canvas_transformed: Some(PolygonToolMessage::CanvasTransformed.into()),
|
||||
overlay_provider: Some(|overlay_context| PolygonToolMessage::Overlays(overlay_context).into()),
|
||||
tool_abort: Some(PolygonToolMessage::Abort.into()),
|
||||
working_color_changed: Some(PolygonToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
|
|
@ -235,12 +238,12 @@ impl Fsm for PolygonToolFsmState {
|
|||
return self;
|
||||
};
|
||||
match (self, event) {
|
||||
(PolygonToolFsmState::Drawing, PolygonToolMessage::CanvasTransformed) => {
|
||||
tool_data.data.recalculate_snaps(document, input);
|
||||
(_, PolygonToolMessage::Overlays(mut overlay_context)) => {
|
||||
polygon_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
self
|
||||
}
|
||||
(PolygonToolFsmState::Ready, PolygonToolMessage::DragStart) => {
|
||||
polygon_data.start(responses, document, input);
|
||||
polygon_data.start(document, input);
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
let subpath = match tool_options.primitive_shape_type {
|
||||
|
|
@ -263,12 +266,17 @@ impl Fsm for PolygonToolFsmState {
|
|||
|
||||
PolygonToolFsmState::Drawing
|
||||
}
|
||||
(state, PolygonToolMessage::Resize { center, lock_ratio }) => {
|
||||
if let Some(message) = polygon_data.calculate_transform(responses, document, input, center, lock_ratio, false) {
|
||||
(PolygonToolFsmState::Drawing, PolygonToolMessage::PointerMove { center, lock_ratio }) => {
|
||||
if let Some(message) = polygon_data.calculate_transform(document, input, center, lock_ratio, false) {
|
||||
responses.add(message);
|
||||
}
|
||||
|
||||
state
|
||||
self
|
||||
}
|
||||
(_, PolygonToolMessage::PointerMove { .. }) => {
|
||||
polygon_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(PolygonToolFsmState::Drawing, PolygonToolMessage::DragStop) => {
|
||||
input.mouse.finish_transaction(polygon_data.viewport_drag_start(document), responses);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use super::tool_prelude::*;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::snapping::SnapData;
|
||||
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_core::uuid::generate_uuid;
|
||||
|
|
@ -48,7 +50,7 @@ pub enum RectangleOptionsUpdate {
|
|||
pub enum RectangleToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
CanvasTransformed,
|
||||
Overlays(OverlayContext),
|
||||
#[remain::unsorted]
|
||||
Abort,
|
||||
#[remain::unsorted]
|
||||
|
|
@ -57,7 +59,7 @@ pub enum RectangleToolMessage {
|
|||
// Tool-specific messages
|
||||
DragStart,
|
||||
DragStop,
|
||||
Resize {
|
||||
PointerMove {
|
||||
center: Key,
|
||||
lock_ratio: Key,
|
||||
},
|
||||
|
|
@ -136,11 +138,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for Rectang
|
|||
match self.fsm_state {
|
||||
Ready => actions!(RectangleToolMessageDiscriminant;
|
||||
DragStart,
|
||||
PointerMove,
|
||||
),
|
||||
Drawing => actions!(RectangleToolMessageDiscriminant;
|
||||
DragStop,
|
||||
Abort,
|
||||
Resize,
|
||||
PointerMove,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +164,7 @@ impl ToolMetadata for RectangleTool {
|
|||
impl ToolTransition for RectangleTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
canvas_transformed: Some(RectangleToolMessage::CanvasTransformed.into()),
|
||||
overlay_provider: Some(|overlay_context| RectangleToolMessage::Overlays(overlay_context).into()),
|
||||
tool_abort: Some(RectangleToolMessage::Abort.into()),
|
||||
working_color_changed: Some(RectangleToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
|
|
@ -195,9 +198,6 @@ impl Fsm for RectangleToolFsmState {
|
|||
tool_options: &Self::ToolOptions,
|
||||
responses: &mut VecDeque<Message>,
|
||||
) -> Self {
|
||||
use RectangleToolFsmState::*;
|
||||
use RectangleToolMessage::*;
|
||||
|
||||
let shape_data = &mut tool_data.data;
|
||||
|
||||
let ToolMessage::Rectangle(event) = event else {
|
||||
|
|
@ -205,12 +205,12 @@ impl Fsm for RectangleToolFsmState {
|
|||
};
|
||||
|
||||
match (self, event) {
|
||||
(Drawing, CanvasTransformed) => {
|
||||
tool_data.data.recalculate_snaps(document, input);
|
||||
(_, RectangleToolMessage::Overlays(mut overlay_context)) => {
|
||||
shape_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
self
|
||||
}
|
||||
(Ready, DragStart) => {
|
||||
shape_data.start(responses, document, input);
|
||||
(RectangleToolFsmState::Ready, RectangleToolMessage::DragStart) => {
|
||||
shape_data.start(document, input);
|
||||
|
||||
let subpath = bezier_rs::Subpath::new_rect(DVec2::ZERO, DVec2::ONE);
|
||||
|
||||
|
|
@ -230,29 +230,34 @@ impl Fsm for RectangleToolFsmState {
|
|||
stroke: Stroke::new(tool_options.stroke.active_color(), tool_options.line_weight),
|
||||
});
|
||||
|
||||
Drawing
|
||||
RectangleToolFsmState::Drawing
|
||||
}
|
||||
(state, Resize { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(responses, document, input, center, lock_ratio, false) {
|
||||
(RectangleToolFsmState::Drawing, RectangleToolMessage::PointerMove { center, lock_ratio }) => {
|
||||
if let Some(message) = shape_data.calculate_transform(document, input, center, lock_ratio, false) {
|
||||
responses.add(message);
|
||||
}
|
||||
|
||||
state
|
||||
self
|
||||
}
|
||||
(Drawing, DragStop) => {
|
||||
(_, RectangleToolMessage::PointerMove { .. }) => {
|
||||
shape_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(RectangleToolFsmState::Drawing, RectangleToolMessage::DragStop) => {
|
||||
input.mouse.finish_transaction(shape_data.viewport_drag_start(document), responses);
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
Ready
|
||||
RectangleToolFsmState::Ready
|
||||
}
|
||||
(Drawing, Abort) => {
|
||||
(RectangleToolFsmState::Drawing, RectangleToolMessage::Abort) => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
|
||||
shape_data.cleanup(responses);
|
||||
|
||||
Ready
|
||||
RectangleToolFsmState::Ready
|
||||
}
|
||||
(_, WorkingColorChanged) => {
|
||||
(_, RectangleToolMessage::WorkingColorChanged) => {
|
||||
responses.add(RectangleToolMessage::UpdateOptions(RectangleOptionsUpdate::WorkingColors(
|
||||
Some(global_tool_data.primary_color),
|
||||
Some(global_tool_data.secondary_color),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate,
|
|||
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
|
||||
use crate::messages::tool::common_functionality::pivot::Pivot;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapManager};
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
|
||||
use graph_craft::document::NodeNetwork;
|
||||
|
|
@ -53,7 +53,7 @@ impl fmt::Display for NestedSelectionBehavior {
|
|||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Select)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||
pub enum SelectToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
|
|
@ -273,9 +273,17 @@ struct SelectToolData {
|
|||
nested_selection_behavior: NestedSelectionBehavior,
|
||||
selected_layers_count: usize,
|
||||
selected_layers_changed: bool,
|
||||
snap_candidates: Vec<SnapCandidatePoint>,
|
||||
}
|
||||
|
||||
impl SelectToolData {
|
||||
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
||||
self.snap_candidates.clear();
|
||||
for &layer in &self.layers_dragging {
|
||||
snapping::get_layer_snap_points(layer, &SnapData::new(document, input), &mut self.snap_candidates);
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_quad(&self) -> Quad {
|
||||
let bbox = self.selection_box();
|
||||
Quad::from_box(bbox)
|
||||
|
|
@ -381,6 +389,8 @@ impl Fsm for SelectToolFsmState {
|
|||
};
|
||||
match (self, event) {
|
||||
(_, SelectToolMessage::Overlays(mut overlay_context)) => {
|
||||
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
||||
|
||||
let selected_layers_count = document.selected_nodes.selected_layers(document.metadata()).count();
|
||||
tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count;
|
||||
tool_data.selected_layers_count = selected_layers_count;
|
||||
|
|
@ -478,7 +488,8 @@ impl Fsm for SelectToolFsmState {
|
|||
let state = if tool_data.pivot.is_over(input.mouse.position) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
//tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
//tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
|
||||
SelectToolFsmState::DraggingPivot
|
||||
} else if let Some(_selected_edges) = dragging_bounds {
|
||||
|
|
@ -502,6 +513,7 @@ impl Fsm for SelectToolFsmState {
|
|||
);
|
||||
bounds.center_of_transformation = selected.mean_average_of_pivots();
|
||||
}
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
|
||||
SelectToolFsmState::ResizingBounds
|
||||
} else if rotating_bounds {
|
||||
|
|
@ -534,9 +546,7 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
tool_data.layers_dragging = selected;
|
||||
|
||||
// tool_data
|
||||
// .snap_manager
|
||||
// .start_snap(document, input, document.bounding_boxes(Some(&tool_data.layers_dragging), None, font_cache), true, true);
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
|
||||
SelectToolFsmState::Dragging
|
||||
} else {
|
||||
|
|
@ -557,6 +567,7 @@ impl Fsm for SelectToolFsmState {
|
|||
NestedSelectionBehavior::Shallowest => drag_shallowest_manipulation(responses, selected, tool_data, document),
|
||||
NestedSelectionBehavior::Deepest => drag_deepest_manipulation(responses, selected, tool_data),
|
||||
}
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
SelectToolFsmState::Dragging
|
||||
} else {
|
||||
// Deselect all layers if using shallowest selection behavior
|
||||
|
|
@ -577,28 +588,46 @@ impl Fsm for SelectToolFsmState {
|
|||
// TODO: This is a cheat. Break out the relevant functionality from the handler above and call it from there and here.
|
||||
responses.add_front(SelectToolMessage::DocumentIsDirty);
|
||||
|
||||
let mouse_position = axis_align_drag(input.keyboard.key(axis_align), input.mouse.position, tool_data.drag_start);
|
||||
let axis_align = input.keyboard.key(axis_align);
|
||||
let mouse_position = axis_align_drag(axis_align, input.mouse.position, tool_data.drag_start);
|
||||
let total_mouse_delta_document = document.metadata.document_to_viewport.inverse().transform_vector2(mouse_position - tool_data.drag_start);
|
||||
|
||||
let mouse_delta = mouse_position - tool_data.drag_current;
|
||||
let snap_data = SnapData::ignore(document, input, &tool_data.layers_dragging);
|
||||
let mouse_delta_document = document.metadata.document_to_viewport.inverse().transform_vector2(mouse_position - tool_data.drag_current);
|
||||
let mut offset = mouse_delta_document;
|
||||
let mut best_snap = SnappedPoint::infinite_snap(document.metadata.document_to_viewport.inverse().transform_point2(mouse_position));
|
||||
|
||||
let snap = tool_data
|
||||
.layers_dragging
|
||||
.iter()
|
||||
.filter_map(|&layer| document.metadata().bounding_box_viewport(layer))
|
||||
.flat_map(snapping::expand_bounds)
|
||||
.collect();
|
||||
for point in &mut tool_data.snap_candidates {
|
||||
point.document_point += total_mouse_delta_document;
|
||||
let snapped = if axis_align {
|
||||
let constraint = SnapConstraint::Line {
|
||||
origin: point.document_point,
|
||||
direction: total_mouse_delta_document.normalize(),
|
||||
};
|
||||
tool_data.snap_manager.constrained_snap(&snap_data, &point, constraint, None)
|
||||
} else {
|
||||
tool_data.snap_manager.free_snap(&snap_data, &point, None, false)
|
||||
};
|
||||
if best_snap.other_snap_better(&snapped) {
|
||||
offset = snapped.snapped_point_document - point.document_point + mouse_delta_document;
|
||||
best_snap = snapped;
|
||||
}
|
||||
point.document_point -= total_mouse_delta_document;
|
||||
}
|
||||
tool_data.snap_manager.update_indicator(best_snap);
|
||||
|
||||
let mouse_delta = document.metadata.document_to_viewport.transform_vector2(offset);
|
||||
|
||||
let closest_move = tool_data.snap_manager.snap_layers(responses, document, snap, mouse_delta);
|
||||
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
|
||||
for layer_ancestors in document.metadata().shallowest_unique_layers(tool_data.layers_dragging.iter().copied()) {
|
||||
responses.add_front(GraphOperationMessage::TransformChange {
|
||||
layer: *layer_ancestors.last().unwrap(),
|
||||
transform: DAffine2::from_translation(mouse_delta + closest_move),
|
||||
transform: DAffine2::from_translation(mouse_delta),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
tool_data.drag_current = mouse_position + closest_move;
|
||||
tool_data.drag_current += mouse_delta;
|
||||
|
||||
// TODO: Reenable this feature after fixing it
|
||||
if false {
|
||||
|
|
@ -614,14 +643,16 @@ impl Fsm for SelectToolFsmState {
|
|||
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove { axis_align, center, .. }) => {
|
||||
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
||||
if let Some(movement) = &mut bounds.selected_edges {
|
||||
let (_center, axis_align) = (input.keyboard.key(center), input.keyboard.key(axis_align));
|
||||
let (_center, constrain) = (input.keyboard.key(center), input.keyboard.key(axis_align));
|
||||
let center = false; // TODO: Reenable this feature after fixing it
|
||||
|
||||
let mouse_position = input.mouse.position;
|
||||
|
||||
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
|
||||
let (position, size) = movement.new_size(snapped_mouse_position, bounds.original_bound_transform, center, bounds.center_of_transformation, axis_align);
|
||||
let centre = center.then_some(bounds.center_of_transformation);
|
||||
let snap = Some(SizeSnapData {
|
||||
manager: &mut tool_data.snap_manager,
|
||||
points: &mut tool_data.snap_candidates,
|
||||
snap_data: SnapData::ignore(document, input, &tool_data.layers_dragging),
|
||||
});
|
||||
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, centre, constrain, snap);
|
||||
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
|
||||
|
||||
let pivot_transform = DAffine2::from_translation(pivot);
|
||||
|
|
@ -682,7 +713,7 @@ impl Fsm for SelectToolFsmState {
|
|||
}
|
||||
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove { .. }) => {
|
||||
let mouse_position = input.mouse.position;
|
||||
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
let snapped_mouse_position = mouse_position; //tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
tool_data.pivot.set_viewport_position(snapped_mouse_position, document, responses);
|
||||
|
||||
SelectToolFsmState::DraggingPivot
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ impl Fsm for SplineToolFsmState {
|
|||
};
|
||||
match (self, event) {
|
||||
(_, SplineToolMessage::CanvasTransformed) => {
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
// tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
self
|
||||
}
|
||||
(SplineToolFsmState::Ready, SplineToolMessage::DragStart) => {
|
||||
|
|
@ -215,8 +215,9 @@ impl Fsm for SplineToolFsmState {
|
|||
let parent = document.new_layer_parent();
|
||||
let transform = document.metadata().transform_to_viewport(parent);
|
||||
|
||||
tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
//tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
//tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
let snapped_position = input.mouse.position; //tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
|
||||
let pos = transform.inverse().transform_point2(snapped_position);
|
||||
|
||||
|
|
@ -244,7 +245,7 @@ impl Fsm for SplineToolFsmState {
|
|||
let Some(layer) = tool_data.layer else {
|
||||
return SplineToolFsmState::Ready;
|
||||
};
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let snapped_position = input.mouse.position; //tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let transform = document.metadata().transform_to_viewport(layer);
|
||||
let pos = transform.inverse().transform_point2(snapped_position);
|
||||
|
||||
|
|
@ -263,7 +264,7 @@ impl Fsm for SplineToolFsmState {
|
|||
let Some(layer) = tool_data.layer else {
|
||||
return SplineToolFsmState::Ready;
|
||||
};
|
||||
let snapped_position = tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let snapped_position = input.mouse.position; // tool_data.snap_manager.snap_position(responses, document, input.mouse.position);
|
||||
let transform = document.metadata().transform_to_viewport(layer);
|
||||
let pos = transform.inverse().transform_point2(snapped_position);
|
||||
tool_data.next_point = pos;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
export let icon: IconName | undefined = undefined;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let popoverMinWidth = 1;
|
||||
|
||||
// Callbacks
|
||||
export let action: (() => void) | undefined = undefined;
|
||||
|
|
@ -28,7 +29,7 @@
|
|||
<IconLabel class="descriptive-icon" classes={{ open }} {disabled} {icon} {tooltip} />
|
||||
{/if}
|
||||
|
||||
<FloatingMenu {open} on:open={({ detail }) => (open = detail)} type="Popover" direction="Bottom">
|
||||
<FloatingMenu {open} on:open={({ detail }) => (open = detail)} minWidth={popoverMinWidth} type="Popover" direction="Bottom">
|
||||
<slot />
|
||||
</FloatingMenu>
|
||||
</LayoutRow>
|
||||
|
|
|
|||
|
|
@ -869,6 +869,8 @@ export class PopoverButton extends WidgetProps {
|
|||
@Transform(({ value }: { value: string }) => value || undefined)
|
||||
tooltip!: string | undefined;
|
||||
|
||||
popoverMinWidth: number | undefined;
|
||||
|
||||
optionsWidget: LayoutGroup[] | undefined;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,9 +130,9 @@ impl Bezier {
|
|||
|
||||
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively.
|
||||
/// The local extrema are defined to be points at which the derivative of the curve is equal to zero.
|
||||
fn unrestricted_local_extrema(&self) -> [Vec<f64>; 2] {
|
||||
fn unrestricted_local_extrema(&self) -> [[Option<f64>; 3]; 2] {
|
||||
match self.handles {
|
||||
BezierHandles::Linear => [Vec::new(), Vec::new()],
|
||||
BezierHandles::Linear => [[None; 3]; 2],
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
let a = handle - self.start;
|
||||
let b = self.end - handle;
|
||||
|
|
@ -156,13 +156,8 @@ impl Bezier {
|
|||
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively.
|
||||
/// The list of `t`-values returned are filtered such that they fall within the range `[0, 1]`.
|
||||
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/local-extrema/solo" title="Local Extrema Demo"></iframe>
|
||||
pub fn local_extrema(&self) -> [Vec<f64>; 2] {
|
||||
self.unrestricted_local_extrema()
|
||||
.into_iter()
|
||||
.map(|t_values| t_values.into_iter().filter(|&t| t > 0. && t < 1.).collect::<Vec<f64>>())
|
||||
.collect::<Vec<Vec<f64>>>()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
pub fn local_extrema(&self) -> [impl Iterator<Item = f64>; 2] {
|
||||
self.unrestricted_local_extrema().map(|t_values| t_values.into_iter().flatten().filter(|&t| t > 0. && t < 1.))
|
||||
}
|
||||
|
||||
/// Return the min and max corners that represent the bounding box of the curve.
|
||||
|
|
@ -223,17 +218,18 @@ impl Bezier {
|
|||
}
|
||||
}
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE))
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Returns list of `t`-values representing the inflection points of the curve.
|
||||
/// The inflection points are defined to be points at which the second derivative of the curve is equal to zero.
|
||||
pub fn unrestricted_inflections(&self) -> Vec<f64> {
|
||||
pub fn unrestricted_inflections(&self) -> impl Iterator<Item = f64> {
|
||||
match self.handles {
|
||||
// There exists no inflection points for linear and quadratic beziers.
|
||||
BezierHandles::Linear => Vec::new(),
|
||||
BezierHandles::Quadratic { .. } => Vec::new(),
|
||||
BezierHandles::Linear => [None; 3],
|
||||
BezierHandles::Quadratic { .. } => [None; 3],
|
||||
BezierHandles::Cubic { .. } => {
|
||||
// Axis align the curve.
|
||||
let translated_bezier = self.translate(-self.start);
|
||||
|
|
@ -257,6 +253,8 @@ impl Bezier {
|
|||
}
|
||||
}
|
||||
}
|
||||
.into_iter()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Returns list of parametric `t`-values representing the inflection points of the curve.
|
||||
|
|
@ -491,7 +489,7 @@ impl Bezier {
|
|||
|
||||
let discriminant = b * b - 4. * a * c;
|
||||
let two_times_a = 2. * a;
|
||||
for t in solve_quadratic(discriminant, two_times_a, b, c) {
|
||||
for t in solve_quadratic(discriminant, two_times_a, b, c).into_iter().flatten() {
|
||||
if (0.0..=1.).contains(&t) {
|
||||
let x = self.evaluate(TValue::Parametric(t)).x;
|
||||
if target_point.x >= x {
|
||||
|
|
@ -514,7 +512,7 @@ impl Bezier {
|
|||
let b = 3. * (p2.y - 2. * p1.y + self.start.y);
|
||||
let c = 3. * (p1.y - self.start.y);
|
||||
let d = self.start.y - target_point.y;
|
||||
for t in solve_cubic(a, b, c, d) {
|
||||
for t in solve_cubic(a, b, c, d).into_iter().flatten() {
|
||||
if (0.0..=1.).contains(&t) {
|
||||
let x = self.evaluate(TValue::Parametric(t)).x;
|
||||
if target_point.x >= x {
|
||||
|
|
@ -716,8 +714,8 @@ mod tests {
|
|||
// Linear bezier cannot have extrema
|
||||
let line = Bezier::from_linear_dvec2(DVec2::new(10., 10.), DVec2::new(50., 50.));
|
||||
let [x_extrema, y_extrema] = line.local_extrema();
|
||||
assert!(x_extrema.is_empty());
|
||||
assert!(y_extrema.is_empty());
|
||||
assert_eq!(y_extrema.count(), 0);
|
||||
assert_eq!(x_extrema.count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -725,26 +723,26 @@ mod tests {
|
|||
// Test with no x-extrema, no y-extrema
|
||||
let bezier1 = Bezier::from_quadratic_coordinates(40., 35., 149., 54., 155., 170.);
|
||||
let [x_extrema1, y_extrema1] = bezier1.local_extrema();
|
||||
assert!(x_extrema1.is_empty());
|
||||
assert!(y_extrema1.is_empty());
|
||||
assert_eq!(x_extrema1.count(), 0);
|
||||
assert_eq!(y_extrema1.count(), 0);
|
||||
|
||||
// Test with 1 x-extrema, no y-extrema
|
||||
let bezier2 = Bezier::from_quadratic_coordinates(45., 30., 170., 90., 45., 150.);
|
||||
let [x_extrema2, y_extrema2] = bezier2.local_extrema();
|
||||
assert_eq!(x_extrema2.len(), 1);
|
||||
assert!(y_extrema2.is_empty());
|
||||
assert_eq!(x_extrema2.count(), 1);
|
||||
assert_eq!(y_extrema2.count(), 0);
|
||||
|
||||
// Test with no x-extrema, 1 y-extrema
|
||||
let bezier3 = Bezier::from_quadratic_coordinates(30., 130., 100., 25., 150., 130.);
|
||||
let [x_extrema3, y_extrema3] = bezier3.local_extrema();
|
||||
assert!(x_extrema3.is_empty());
|
||||
assert_eq!(y_extrema3.len(), 1);
|
||||
assert_eq!(x_extrema3.count(), 0);
|
||||
assert_eq!(y_extrema3.count(), 1);
|
||||
|
||||
// Test with 1 x-extrema, 1 y-extrema
|
||||
let bezier4 = Bezier::from_quadratic_coordinates(50., 70., 170., 35., 60., 150.);
|
||||
let [x_extrema4, y_extrema4] = bezier4.local_extrema();
|
||||
assert_eq!(x_extrema4.len(), 1);
|
||||
assert_eq!(y_extrema4.len(), 1);
|
||||
assert_eq!(x_extrema4.count(), 1);
|
||||
assert_eq!(y_extrema4.count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -752,44 +750,44 @@ mod tests {
|
|||
// 0 x-extrema, 0 y-extrema
|
||||
let bezier1 = Bezier::from_cubic_coordinates(100., 105., 250., 250., 110., 150., 260., 260.);
|
||||
let [x_extrema1, y_extrema1] = bezier1.local_extrema();
|
||||
assert!(x_extrema1.is_empty());
|
||||
assert!(y_extrema1.is_empty());
|
||||
assert_eq!(x_extrema1.count(), 0);
|
||||
assert_eq!(y_extrema1.count(), 0);
|
||||
|
||||
// 1 x-extrema, 0 y-extrema
|
||||
let bezier2 = Bezier::from_cubic_coordinates(55., 145., 40., 40., 110., 110., 180., 40.);
|
||||
let [x_extrema2, y_extrema2] = bezier2.local_extrema();
|
||||
assert_eq!(x_extrema2.len(), 1);
|
||||
assert!(y_extrema2.is_empty());
|
||||
assert_eq!(x_extrema2.count(), 1);
|
||||
assert_eq!(y_extrema2.count(), 0);
|
||||
|
||||
// 1 x-extrema, 1 y-extrema
|
||||
let bezier3 = Bezier::from_cubic_coordinates(100., 105., 170., 10., 25., 20., 20., 120.);
|
||||
let [x_extrema3, y_extrema3] = bezier3.local_extrema();
|
||||
assert_eq!(x_extrema3.len(), 1);
|
||||
assert_eq!(y_extrema3.len(), 1);
|
||||
assert_eq!(x_extrema3.count(), 1);
|
||||
assert_eq!(y_extrema3.count(), 1);
|
||||
|
||||
// 1 x-extrema, 2 y-extrema
|
||||
let bezier4 = Bezier::from_cubic_coordinates(50., 90., 120., 16., 150., 190., 45., 150.);
|
||||
let [x_extrema4, y_extrema4] = bezier4.local_extrema();
|
||||
assert_eq!(x_extrema4.len(), 1);
|
||||
assert_eq!(y_extrema4.len(), 2);
|
||||
assert_eq!(x_extrema4.count(), 1);
|
||||
assert_eq!(y_extrema4.count(), 2);
|
||||
|
||||
// 2 x-extrema, 0 y-extrema
|
||||
let bezier5 = Bezier::from_cubic_coordinates(40., 170., 150., 160., 10., 10., 170., 10.);
|
||||
let [x_extrema5, y_extrema5] = bezier5.local_extrema();
|
||||
assert_eq!(x_extrema5.len(), 2);
|
||||
assert!(y_extrema5.is_empty());
|
||||
assert_eq!(x_extrema5.count(), 2);
|
||||
assert_eq!(y_extrema5.count(), 0);
|
||||
|
||||
// 2 x-extrema, 1 y-extrema
|
||||
let bezier6 = Bezier::from_cubic_coordinates(40., 170., 150., 160., 10., 10., 160., 45.);
|
||||
let [x_extrema6, y_extrema6] = bezier6.local_extrema();
|
||||
assert_eq!(x_extrema6.len(), 2);
|
||||
assert_eq!(y_extrema6.len(), 1);
|
||||
assert_eq!(x_extrema6.count(), 2);
|
||||
assert_eq!(y_extrema6.count(), 1);
|
||||
|
||||
// 2 x-extrema, 2 y-extrema
|
||||
let bezier7 = Bezier::from_cubic_coordinates(46., 60., 140., 10., 50., 160., 120., 120.);
|
||||
let [x_extrema7, y_extrema7] = bezier7.local_extrema();
|
||||
assert_eq!(x_extrema7.len(), 2);
|
||||
assert_eq!(y_extrema7.len(), 2);
|
||||
assert_eq!(x_extrema7.count(), 2);
|
||||
assert_eq!(y_extrema7.count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -90,10 +90,10 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
|
|||
|
||||
// TODO: Consider the shared point between adjacent beziers.
|
||||
self.iter().enumerate().fold([Vec::new(), Vec::new()], |mut acc, elem| {
|
||||
let extremas = elem.1.local_extrema();
|
||||
let [x, y] = elem.1.local_extrema();
|
||||
// Convert t-values of bezier curve to t-values of subpath
|
||||
acc[0].extend(extremas[0].iter().map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::<Vec<f64>>());
|
||||
acc[1].extend(extremas[1].iter().map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::<Vec<f64>>());
|
||||
acc[0].extend(x.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::<Vec<f64>>());
|
||||
acc[1].extend(y.map(|t| ((elem.0 as f64) + t) / number_of_curves).collect::<Vec<f64>>());
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,37 +93,31 @@ pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve:
|
|||
|
||||
/// Return the index and the value of the closest point in the LUT compared to the provided point.
|
||||
pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (usize, f64) {
|
||||
lut.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| (i, point.distance_squared(*p)))
|
||||
.min_by(|x, y| (x.1).partial_cmp(&(y.1)).unwrap())
|
||||
.unwrap()
|
||||
lut.iter().enumerate().map(|(i, p)| (i, point.distance_squared(*p))).min_by(|x, y| (x.1).total_cmp(&(y.1))).unwrap()
|
||||
}
|
||||
|
||||
// TODO: Use an `Option` return type instead of a `Vec`
|
||||
/// Find the roots of the linear equation `ax + b`.
|
||||
pub fn solve_linear(a: f64, b: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
pub fn solve_linear(a: f64, b: f64) -> [Option<f64>; 3] {
|
||||
// There exist roots when `a` is not 0
|
||||
if a.abs() > MAX_ABSOLUTE_DIFFERENCE {
|
||||
roots.push(-b / a);
|
||||
[Some(-b / a), None, None]
|
||||
} else {
|
||||
[None; 3]
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Find the roots of the linear equation `ax^2 + bx + c`.
|
||||
/// Precompute the `discriminant` (`b^2 - 4ac`) and `two_times_a` arguments prior to calling this function for efficiency purposes.
|
||||
pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
pub fn solve_quadratic(discriminant: f64, two_times_a: f64, b: f64, c: f64) -> [Option<f64>; 3] {
|
||||
let mut roots = [None; 3];
|
||||
if two_times_a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
roots = solve_linear(b, c);
|
||||
} else if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
roots.push(-b / (two_times_a));
|
||||
roots[0] = Some(-b / (two_times_a));
|
||||
} else if discriminant > 0. {
|
||||
let root_discriminant = discriminant.sqrt();
|
||||
roots.push((-b + root_discriminant) / (two_times_a));
|
||||
roots.push((-b - root_discriminant) / (two_times_a));
|
||||
roots[0] = Some((-b + root_discriminant) / (two_times_a));
|
||||
roots[1] = Some((-b - root_discriminant) / (two_times_a));
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
|
@ -139,8 +133,8 @@ fn cube_root(f: f64) -> f64 {
|
|||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Solve a cubic of the form `x^3 + px + q`, derivation from: <https://trans4mind.com/personal_development/mathematics/polynomials/cubicAlgebra.htm>.
|
||||
pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec<f64> {
|
||||
let mut roots = Vec::new();
|
||||
pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> [Option<f64>; 3] {
|
||||
let mut roots = [None; 3];
|
||||
if discriminant.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// When discriminant is 0 (check for approximation because of floating point errors), all roots are real, and 2 are repeated
|
||||
// filter out repeated roots (ie. roots whose distance is less than some epsilon)
|
||||
|
|
@ -149,15 +143,15 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec
|
|||
let root_1 = 2. * cube_root(-q_divided_by_2) - a_divided_by_3;
|
||||
let root_2 = cube_root(q_divided_by_2) - a_divided_by_3;
|
||||
if (root_1 - root_2).abs() > MIN_SEPARATION_VALUE {
|
||||
roots.push(root_1);
|
||||
roots[0] = Some(root_1);
|
||||
}
|
||||
roots.push(root_2);
|
||||
roots[1] = Some(root_2);
|
||||
} else if discriminant > 0. {
|
||||
// When discriminant > 0, there is one real and two imaginary roots
|
||||
let q_divided_by_2 = q / 2.;
|
||||
let square_root_discriminant = discriminant.powf(1. / 2.);
|
||||
|
||||
roots.push(cube_root(-q_divided_by_2 + square_root_discriminant) - cube_root(q_divided_by_2 + square_root_discriminant) - a / 3.);
|
||||
roots[0] = Some(cube_root(-q_divided_by_2 + square_root_discriminant) - cube_root(q_divided_by_2 + square_root_discriminant) - a / 3.);
|
||||
} else {
|
||||
// Otherwise, discriminant < 0 and there are three real roots
|
||||
let p_divided_by_3 = p / 3.;
|
||||
|
|
@ -166,16 +160,16 @@ pub fn solve_reformatted_cubic(discriminant: f64, a: f64, p: f64, q: f64) -> Vec
|
|||
let phi = (-q / (2. * cube_root_r.powi(3))).acos();
|
||||
|
||||
let two_times_cube_root_r = 2. * cube_root_r;
|
||||
roots.push(two_times_cube_root_r * (phi / 3.).cos() - a_divided_by_3);
|
||||
roots.push(two_times_cube_root_r * ((phi + 2. * PI) / 3.).cos() - a_divided_by_3);
|
||||
roots.push(two_times_cube_root_r * ((phi + 4. * PI) / 3.).cos() - a_divided_by_3);
|
||||
roots[0] = Some(two_times_cube_root_r * (phi / 3.).cos() - a_divided_by_3);
|
||||
roots[1] = Some(two_times_cube_root_r * ((phi + 2. * PI) / 3.).cos() - a_divided_by_3);
|
||||
roots[2] = Some(two_times_cube_root_r * ((phi + 4. * PI) / 3.).cos() - a_divided_by_3);
|
||||
}
|
||||
roots
|
||||
}
|
||||
|
||||
// TODO: Use an `impl Iterator` return type instead of a `Vec`
|
||||
/// Solve a cubic of the form `ax^3 + bx^2 + ct + d`.
|
||||
pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
|
||||
pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> [Option<f64>; 3] {
|
||||
if a.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
if b.abs() <= STRICT_MAX_ABSOLUTE_DIFFERENCE {
|
||||
// If both a and b are approximately 0, treat as a linear problem
|
||||
|
|
@ -327,43 +321,47 @@ mod tests {
|
|||
a.len() == b.len() && a.into_iter().zip(b).all(|(a, b)| f64_compare(a, b, max_abs_diff))
|
||||
}
|
||||
|
||||
fn collect_roots(roots: [Option<f64>; 3]) -> Vec<f64> {
|
||||
roots.into_iter().flatten().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_linear() {
|
||||
// Line that is on the x-axis
|
||||
assert!(solve_linear(0., 0.).is_empty());
|
||||
assert!(collect_roots(solve_linear(0., 0.)).is_empty());
|
||||
// Line that is parallel to but not on the x-axis
|
||||
assert!(solve_linear(0., 1.).is_empty());
|
||||
assert!(collect_roots(solve_linear(0., 1.)).is_empty());
|
||||
// Line with a non-zero slope
|
||||
assert!(solve_linear(2., -8.) == vec![4.]);
|
||||
assert!(collect_roots(solve_linear(2., -8.)) == vec![4.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_solve_cubic() {
|
||||
// discriminant == 0
|
||||
let roots1 = solve_cubic(1., 0., 0., 0.);
|
||||
let roots1 = collect_roots(solve_cubic(1., 0., 0., 0.));
|
||||
assert!(roots1 == vec![0.]);
|
||||
|
||||
let roots2 = solve_cubic(1., 3., 0., -4.);
|
||||
let roots2 = collect_roots(solve_cubic(1., 3., 0., -4.));
|
||||
assert!(roots2 == vec![1., -2.]);
|
||||
|
||||
// p == 0
|
||||
let roots3 = solve_cubic(1., 0., 0., -1.);
|
||||
let roots3 = collect_roots(solve_cubic(1., 0., 0., -1.));
|
||||
assert!(roots3 == vec![1.]);
|
||||
|
||||
// discriminant > 0
|
||||
let roots4 = solve_cubic(1., 3., 0., 2.);
|
||||
let roots4 = collect_roots(solve_cubic(1., 3., 0., 2.));
|
||||
assert!(f64_compare_vector(roots4, vec![-3.196], MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// discriminant < 0
|
||||
let roots5 = solve_cubic(1., 3., 0., -1.);
|
||||
let roots5 = collect_roots(solve_cubic(1., 3., 0., -1.));
|
||||
assert!(f64_compare_vector(roots5, vec![0.532, -2.879, -0.653], MAX_ABSOLUTE_DIFFERENCE));
|
||||
|
||||
// quadratic
|
||||
let roots6 = solve_cubic(0., 3., 0., -3.);
|
||||
let roots6 = collect_roots(solve_cubic(0., 3., 0., -3.));
|
||||
assert!(roots6 == vec![1., -1.]);
|
||||
|
||||
// linear
|
||||
let roots7 = solve_cubic(0., 0., 1., -1.);
|
||||
let roots7 = collect_roots(solve_cubic(0., 0., 1., -1.));
|
||||
assert!(roots7 == vec![1.]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,13 @@ impl Quad {
|
|||
}
|
||||
|
||||
/// Get all the edges in the quad.
|
||||
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
|
||||
pub fn edges(&self) -> [[DVec2; 2]; 4] {
|
||||
[[self.0[0], self.0[1]], [self.0[1], self.0[2]], [self.0[2], self.0[3]], [self.0[3], self.0[0]]]
|
||||
.into_iter()
|
||||
.map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
|
||||
/// Get all the edges in the quad as linear bezier curves
|
||||
pub fn bezier_lines(&self) -> impl Iterator<Item = bezier_rs::Bezier> + '_ {
|
||||
self.edges().into_iter().map(|[start, end]| bezier_rs::Bezier::from_linear_dvec2(start, end))
|
||||
}
|
||||
|
||||
/// Generates a [crate::vector::Subpath] of the quad
|
||||
|
|
@ -66,12 +69,38 @@ impl Quad {
|
|||
pub fn contains(&self, p: DVec2) -> bool {
|
||||
let mut inside = false;
|
||||
for (i, j) in (0..4).zip([3, 0, 1, 2]) {
|
||||
if (self.0[i].y > p.y) != (self.0[j].y > p.y) && p.x < (self.0[j].x - self.0[i].x * (p.y - self.0[i].y) / (self.0[j].y - self.0[i].y) + self.0[i].x) {
|
||||
if (self.0[i].y > p.y) != (self.0[j].y > p.y) && p.x < ((self.0[j].x - self.0[i].x) * (p.y - self.0[i].y) / (self.0[j].y - self.0[i].y) + self.0[i].x) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
inside
|
||||
}
|
||||
|
||||
/// https://www.cs.rpi.edu/~cutler/classes/computationalgeometry/F23/lectures/02_line_segment_intersections.pdf
|
||||
fn line_intersection_t(a: DVec2, b: DVec2, c: DVec2, d: DVec2) -> (f64, f64) {
|
||||
let t = ((a.x - c.x) * (c.y - d.y) - (a.y - c.y) * (c.x - d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x));
|
||||
let u = ((a.x - c.x) * (a.y - b.y) - (a.y - c.y) * (a.x - b.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x));
|
||||
|
||||
(t, u)
|
||||
}
|
||||
|
||||
fn intersect_lines(a: DVec2, b: DVec2, c: DVec2, d: DVec2) -> Option<DVec2> {
|
||||
let (t, u) = Self::line_intersection_t(a, b, c, d);
|
||||
((0. ..=1.).contains(&t) && (0. ..=1.).contains(&u)).then(|| a + t * (b - a))
|
||||
}
|
||||
|
||||
pub fn intersect_rays(a: DVec2, a_direction: DVec2, b: DVec2, b_direction: DVec2) -> Option<DVec2> {
|
||||
let (t, u) = Self::line_intersection_t(a, a + a_direction, b, b + b_direction);
|
||||
(t.is_finite() && u.is_finite()).then(|| a + t * a_direction)
|
||||
}
|
||||
|
||||
pub fn intersects(&self, other: Quad) -> bool {
|
||||
let intersects = self
|
||||
.edges()
|
||||
.into_iter()
|
||||
.any(|[a, b]| other.edges().into_iter().any(|[c, d]| Self::intersect_lines(a, b, c, d).is_some()));
|
||||
self.contains(other.center()) || other.contains(self.center()) || intersects
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Mul<Quad> for DAffine2 {
|
||||
|
|
@ -98,9 +127,26 @@ fn offset_quad() {
|
|||
fn quad_contains() {
|
||||
assert!(Quad::from_box([DVec2::ZERO, DVec2::ONE]).contains(DVec2::splat(0.5)));
|
||||
assert!(Quad::from_box([DVec2::ONE, DVec2::ZERO]).contains(DVec2::splat(0.5)));
|
||||
assert!(Quad::from_box([DVec2::splat(300.), DVec2::splat(500.)]).contains(DVec2::splat(350.)));
|
||||
assert!((DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).contains(DVec2::new(-0.5, 0.5)));
|
||||
|
||||
assert!(!Quad::from_box([DVec2::ZERO, DVec2::ONE]).contains(DVec2::new(1., 1.1)));
|
||||
assert!(!Quad::from_box([DVec2::ONE, DVec2::ZERO]).contains(DVec2::new(0.5, -0.01)));
|
||||
assert!(!(DAffine2::from_scale(DVec2::new(-1., 1.)) * Quad::from_box([DVec2::ZERO, DVec2::ONE])).contains(DVec2::splat(0.5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_lines() {
|
||||
assert_eq!(
|
||||
Quad::intersect_lines(DVec2::new(-5., 5.), DVec2::new(5., 5.), DVec2::new(2., 7.), DVec2::new(2., 3.)),
|
||||
Some(DVec2::new(2., 5.))
|
||||
);
|
||||
assert_eq!(Quad::intersect_lines(DVec2::new(4., 6.), DVec2::new(4., 5.), DVec2::new(2., 7.), DVec2::new(2., 3.)), None);
|
||||
assert_eq!(Quad::intersect_lines(DVec2::new(-5., 5.), DVec2::new(5., 5.), DVec2::new(2., 7.), DVec2::new(2., 9.)), None);
|
||||
}
|
||||
#[test]
|
||||
fn intersect_quad() {
|
||||
assert!(Quad::from_box([DVec2::ZERO, DVec2::splat(5.)]).intersects(Quad::from_box([DVec2::splat(4.), DVec2::splat(7.)])));
|
||||
assert!(Quad::from_box([DVec2::ZERO, DVec2::splat(5.)]).intersects(Quad::from_box([DVec2::splat(4.), DVec2::splat(4.2)])));
|
||||
assert!(!Quad::from_box([DVec2::ZERO, DVec2::splat(3.)]).intersects(Quad::from_box([DVec2::splat(4.), DVec2::splat(4.2)])));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ use wasm_bindgen::{Clamped, JsCast};
|
|||
use web_sys::window;
|
||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
|
||||
|
||||
#[cfg(any(feature = "resvg", feature = "vello"))]
|
||||
pub struct Canvas(CanvasRenderingContext2d);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
@ -284,7 +285,10 @@ fn decode_image_node<'a: 'input>(data: Arc<[u8]>) -> ImageFrame<Color> {
|
|||
pub use graph_craft::document::value::RenderOutput;
|
||||
pub struct RenderNode<Data, Surface, Parameter> {
|
||||
data: Data,
|
||||
#[cfg(any(feature = "resvg", feature = "vello"))]
|
||||
surface_handle: Surface,
|
||||
#[cfg(not(any(feature = "resvg", feature = "vello")))]
|
||||
surface_handle: PhantomData<Surface>,
|
||||
parameter: PhantomData<Parameter>,
|
||||
}
|
||||
|
||||
|
|
@ -417,10 +421,13 @@ where
|
|||
}
|
||||
#[automatically_derived]
|
||||
impl<Data, Surface, Parameter> RenderNode<Data, Surface, Parameter> {
|
||||
pub const fn new(data: Data, surface_handle: Surface) -> Self {
|
||||
pub fn new(data: Data, surface_handle: Surface) -> Self {
|
||||
Self {
|
||||
data,
|
||||
#[cfg(any(feature = "resvg", feature = "vello"))]
|
||||
surface_handle,
|
||||
#[cfg(not(any(feature = "resvg", feature = "vello")))]
|
||||
surface_handle: PhantomData,
|
||||
parameter: PhantomData,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -327,14 +327,14 @@ impl WasmBezier {
|
|||
}
|
||||
|
||||
pub fn local_extrema(&self) -> String {
|
||||
let local_extrema: [Vec<f64>; 2] = self.0.local_extrema();
|
||||
let local_extrema = self.0.local_extrema();
|
||||
|
||||
let bezier = self.get_bezier_path();
|
||||
let circles: String = local_extrema
|
||||
.iter()
|
||||
.into_iter()
|
||||
.zip([RED, GREEN])
|
||||
.flat_map(|(t_value_list, color)| {
|
||||
t_value_list.iter().map(|&t_value| {
|
||||
t_value_list.map(move |t_value| {
|
||||
let point = self.0.evaluate(TValue::Parametric(t_value));
|
||||
draw_circle(point, 3., color, 1.5, WHITE)
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue