Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement extending, joining, and creating new subpaths with the Spline tool #2203

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.;
// Pen tool
pub const CREATE_CURVE_THRESHOLD: f64 = 5.;

// Spline tool
pub const PATH_JOIN_THRESHOLD: f64 = 5.;
pub const EXTEND_PATH_THRESHOLD: f64 = 5.;

// Line tool
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;

Expand Down
2 changes: 1 addition & 1 deletion editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ pub fn input_mappings() -> Mapping {
//
// SplineToolMessage
entry!(PointerMove; action_dispatch=SplineToolMessage::PointerMove),
entry!(KeyDown(MouseLeft); action_dispatch=SplineToolMessage::DragStart),
entry!(KeyDown(MouseLeft); action_dispatch=SplineToolMessage::DragStart { append_to_selected: Shift }),
entry!(KeyUp(MouseLeft); action_dispatch=SplineToolMessage::DragStop),
entry!(KeyDown(MouseRight); action_dispatch=SplineToolMessage::Confirm),
entry!(KeyDown(Escape); action_dispatch=SplineToolMessage::Confirm),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,32 @@ use glam::DVec2;

/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
pub fn should_extend(document: &DocumentMessageHandler, goal: DVec2, tolerance: f64, layers: impl Iterator<Item = LayerNodeIdentifier>) -> Option<(LayerNodeIdentifier, PointId, DVec2)> {
closest_point(document, goal, tolerance, layers, |_| false)
}

/// Determine the closest point to the goal point under max_distance.
/// Additionally exclude checking closeness to the point which given to exclude() returns true.
pub fn closest_point<T>(
document: &DocumentMessageHandler,
goal: DVec2,
max_distance: f64,
layers: impl Iterator<Item = LayerNodeIdentifier>,
exclude: T,
) -> Option<(LayerNodeIdentifier, PointId, DVec2)>
where
T: Fn(PointId) -> bool,
{
let mut best = None;
let mut best_distance_squared = tolerance * tolerance;
let mut best_distance_squared = max_distance * max_distance;
for layer in layers {
let viewspace = document.metadata().transform_to_viewport(layer);
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
continue;
};
for id in vector_data.single_connected_points() {
if exclude(id) {
continue;
}
let Some(point) = vector_data.point_domain.position_from_id(id) else { continue };

let distance_squared = viewspace.transform_point2(point).distance_squared(goal);
Expand Down
157 changes: 128 additions & 29 deletions editor/src/messages/tool/tool_messages/spline_tool.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use super::tool_prelude::*;
use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD};
use crate::consts::{DEFAULT_STROKE_WIDTH, DRAG_THRESHOLD, EXTEND_PATH_THRESHOLD, PATH_JOIN_THRESHOLD};
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
use crate::messages::portfolio::document::overlays::utility_functions::path_endpoint_overlays;
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::auto_panning::AutoPanning;
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, SnapData, SnapManager, SnapTypeConfiguration, SnappedPoint};

use crate::messages::tool::common_functionality::utility_functions::{closest_point, should_extend};

use graph_craft::document::{NodeId, NodeInput};
use graphene_core::Color;
Expand Down Expand Up @@ -38,13 +42,14 @@ impl Default for SplineOptions {
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum SplineToolMessage {
// Standard messages
Overlays(OverlayContext),
CanvasTransformed,
Abort,
WorkingColorChanged,

// Tool-specific messages
Confirm,
DragStart,
DragStart { append_to_selected: Key },
DragStop,
PointerMove,
PointerOutsideViewport,
Expand Down Expand Up @@ -152,6 +157,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for SplineT
Undo,
DragStart,
DragStop,
PointerMove,
Confirm,
Abort,
),
Expand All @@ -168,6 +174,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for SplineT
impl ToolTransition for SplineTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
overlay_provider: Some(|overlay_context: OverlayContext| SplineToolMessage::Overlays(overlay_context).into()),
canvas_transformed: Some(SplineToolMessage::CanvasTransformed.into()),
tool_abort: Some(SplineToolMessage::Abort.into()),
working_color_changed: Some(SplineToolMessage::WorkingColorChanged.into()),
Expand All @@ -178,7 +185,7 @@ impl ToolTransition for SplineTool {

#[derive(Clone, Debug, Default)]
struct SplineToolData {
/// Points that are inserted.
/// list of points inserted.
points: Vec<(PointId, DVec2)>,
/// Point to be inserted.
next_point: DVec2,
Expand All @@ -192,26 +199,83 @@ struct SplineToolData {
auto_panning: AutoPanning,
}

impl SplineToolData {
fn cleanup(&mut self) {
self.layer = None;
self.preview_point = None;
self.preview_segment = None;
self.points = Vec::new();
}

/// get snapped point but ignoring current layer
fn snapped_point(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) -> SnappedPoint {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
let snap_data = SnapData::ignore(document, input, &ignore);
let snapped = self.snap_manager.free_snap(&snap_data, &point, SnapTypeConfiguration::default());
snapped
}
}

impl Fsm for SplineToolFsmState {
type ToolData = SplineToolData;
type ToolOptions = SplineOptions;

fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData {
document, global_tool_data, input, ..
document,
global_tool_data,
input,
shape_editor,
..
} = tool_action_data;

let ToolMessage::Spline(event) = event else { return self };
match (self, event) {
(_, SplineToolMessage::CanvasTransformed) => self,
(SplineToolFsmState::Ready, SplineToolMessage::DragStart) => {
(_, SplineToolMessage::Overlays(mut overlay_context)) => {
path_endpoint_overlays(document, shape_editor, &mut overlay_context);
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
self
}
(SplineToolFsmState::Ready, SplineToolMessage::DragStart { append_to_selected }) => {
responses.add(DocumentMessage::StartTransaction);

tool_data.cleanup();
tool_data.weight = tool_options.line_weight;

// Extend an endpoint of the selected path
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
if let Some((layer, point, position)) = should_extend(document, input.mouse.position, EXTEND_PATH_THRESHOLD, selected_nodes.selected_layers(document.metadata())) {
tool_data.layer = Some(layer);
tool_data.points.push((point, position));
// update next point to preview current mouse pos instead of pointing last mouse pos when DragStop event occured.
tool_data.next_point = position;

extend_spline(tool_data, true, responses);

return SplineToolFsmState::Drawing;
}

// Create new path in the same layer when shift is down
if input.keyboard.key(append_to_selected) {
let mut selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&document.network_interface);
let existing_layer = selected_layers_except_artboards.next().filter(|_| selected_layers_except_artboards.next().is_none());
if let Some(layer) = existing_layer {
tool_data.layer = Some(layer);

let transform = document.metadata().transform_to_viewport(layer);
let position = transform.inverse().transform_point2(input.mouse.position);
tool_data.next_point = position;

return SplineToolFsmState::Drawing;
}
}

responses.add(DocumentMessage::DeselectAllLayers);

let parent = document.new_layer_bounding_artboard(input);

tool_data.weight = tool_options.line_weight;

let path_node_type = resolve_document_node_type("Path").expect("Path node does not exist");
let path_node = path_node_type.default_node_template();
let spline_node_type = resolve_document_node_type("Splines from Points").expect("Spline from Points node does not exist");
Expand All @@ -228,40 +292,50 @@ impl Fsm for SplineToolFsmState {
SplineToolFsmState::Drawing
}
(SplineToolFsmState::Drawing, SplineToolMessage::DragStop) => {
responses.add(DocumentMessage::EndTransaction);

let Some(layer) = tool_data.layer else {
if tool_data.layer.is_none() {
return SplineToolFsmState::Ready;
};
let snapped_position = input.mouse.position;
let transform = document.metadata().transform_to_viewport(layer);
let pos = transform.inverse().transform_point2(snapped_position);

if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(pos) > DRAG_THRESHOLD) {
tool_data.next_point = pos;
if join_path(document, input.mouse.position, tool_data, responses) {
responses.add(DocumentMessage::EndTransaction);
return SplineToolFsmState::Ready;
}
tool_data.next_point = tool_data.snapped_point(document, input).snapped_point_document;
if tool_data.points.last().map_or(true, |last_pos| last_pos.1.distance(tool_data.next_point) > DRAG_THRESHOLD) {
extend_spline(tool_data, false, responses);
}

update_spline(tool_data, false, responses);

SplineToolFsmState::Drawing
}
(SplineToolFsmState::Drawing, SplineToolMessage::PointerMove) => {
let Some(layer) = tool_data.layer else {
return SplineToolFsmState::Ready;
};
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;
let ignore = |cp: PointId| tool_data.preview_point.is_some_and(|pp| pp == cp) || tool_data.points.last().is_some_and(|(ep, _)| *ep == cp);
let join_point = closest_point(document, input.mouse.position, PATH_JOIN_THRESHOLD, vec![layer].into_iter(), ignore);

update_spline(tool_data, true, responses);
// endpoints snapping
if let Some((_, _, point)) = join_point {
tool_data.next_point = point;
tool_data.snap_manager.clear_indicator();
} else {
let snapped_point = tool_data.snapped_point(document, input);
tool_data.next_point = snapped_point.snapped_point_document;
tool_data.snap_manager.update_indicator(snapped_point);
}

extend_spline(tool_data, true, responses);

// Auto-panning
let messages = [SplineToolMessage::PointerOutsideViewport.into(), SplineToolMessage::PointerMove.into()];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);

SplineToolFsmState::Drawing
}
(_, SplineToolMessage::PointerMove) => {
tool_data.snap_manager.preview_draw(&SnapData::new(document, input), input.mouse.position);
responses.add(OverlaysMessage::Draw);
self
}
(SplineToolFsmState::Drawing, SplineToolMessage::PointerOutsideViewport) => {
// Auto-panning
let _ = tool_data.auto_panning.shift_viewport(input, responses);
Expand All @@ -283,11 +357,8 @@ impl Fsm for SplineToolFsmState {
responses.add(DocumentMessage::AbortTransaction);
}

tool_data.layer = None;
tool_data.preview_point = None;
tool_data.preview_segment = None;
tool_data.points.clear();
tool_data.snap_manager.cleanup(responses);
tool_data.cleanup();

SplineToolFsmState::Ready
}
Expand Down Expand Up @@ -320,7 +391,35 @@ impl Fsm for SplineToolFsmState {
}
}

fn update_spline(tool_data: &mut SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) {
/// Return `true` only if new segment is inserted to connect two end points in the selected layer otherwise `false`.
fn join_path(document: &DocumentMessageHandler, mouse_pos: DVec2, tool_data: &mut SplineToolData, responses: &mut VecDeque<Message>) -> bool {
let Some((endpoint, _)) = tool_data.points.last().map(|p| *p) else {
return false;
};
// use preview_point to get current dragging position.
let preview_point = tool_data.preview_point;
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
let selected_layers = selected_nodes.selected_layers(document.metadata());
// get the closest point to mouse position which is not preview_point or end_point.
let Some((layer, point, _)) = closest_point(document, mouse_pos, PATH_JOIN_THRESHOLD, selected_layers, |cp| {
preview_point.is_some_and(|pp| pp == cp) || cp == endpoint
}) else {
return false;
};

// NOTE: deleting preview point before joining two endponts because
// last point inserted could be preview point and segment which is after the endpoint
delete_preview(tool_data, responses);

let points = [endpoint, point];
let id = SegmentId::generate();
let modification_type = VectorModificationType::InsertSegment { id, points, handles: [None, None] };
responses.add(GraphOperationMessage::Vector { layer, modification_type });

true
}

fn extend_spline(tool_data: &mut SplineToolData, show_preview: bool, responses: &mut VecDeque<Message>) {
delete_preview(tool_data, responses);

let Some(layer) = tool_data.layer else { return };
Expand Down
Loading