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

Merged
merged 21 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
83aa914
visualize spline end points using overlays
indierusty Jan 16, 2025
b74c587
implement for spline tool to extend path by draging end points
indierusty Jan 17, 2025
345e7a7
allow holding Shift to begin drawing a new spline subpath in the same…
indierusty Jan 17, 2025
4d38634
implement spline tool to join two endpoints
indierusty Jan 18, 2025
8889a64
fix naming
indierusty Jan 18, 2025
ccd1552
refactor spline tool
indierusty Jan 19, 2025
7ded22e
impl spline tool snapping and overlays
indierusty Jan 19, 2025
8aba576
fix joining path and refactor
indierusty Jan 20, 2025
3d0764d
improve join_path comment
indierusty Jan 20, 2025
11298ea
fix snapping overlays flickering by ignoring snapping in current layer
indierusty Jan 21, 2025
44cf06e
fix inserting single point on aborting spline tool
indierusty Jan 21, 2025
1f68f93
add snapping for endpoint even when regular snapping is disabled
indierusty Jan 21, 2025
0bc30dd
Merge branch 'master' into spline-tool-editing
Keavon Jan 24, 2025
75cc2e4
fix extending
indierusty Jan 25, 2025
c525dcd
Merge branch 'spline-tool-editing' of github.com:indierusty/Graphite …
indierusty Jan 25, 2025
acd4ec4
Merge branch 'master' into spline-tool-editing
Keavon Jan 25, 2025
662a0bb
fix inserting new point instead of extending and Add hint for Shift t…
indierusty Jan 25, 2025
49334a4
Merge branch 'master' into spline-tool-editing
Keavon Jan 25, 2025
9076dc0
fix grammatical errors and code style
indierusty Jan 25, 2025
31b5ad1
Merge branch 'master' into spline-tool-editing
Keavon Jan 25, 2025
e18eacc
Code review
Keavon Jan 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions editor/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ 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.;

// 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
174 changes: 144 additions & 30 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, PATH_JOIN_THRESHOLD, SNAP_POINT_TOLERANCE};
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};

indierusty marked this conversation as resolved.
Show resolved Hide resolved
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,40 +185,104 @@ impl ToolTransition for SplineTool {

#[derive(Clone, Debug, Default)]
struct SplineToolData {
/// Points that are inserted.
/// list of points inserted.
indierusty marked this conversation as resolved.
Show resolved Hide resolved
points: Vec<(PointId, DVec2)>,
/// Point to be inserted.
next_point: DVec2,
/// Point that was inserted temporarily to show preview.
preview_point: Option<PointId>,
/// Segment that was inserted temporarily to show preview.
preview_segment: Option<SegmentId>,
extend: bool,
weight: f64,
layer: Option<LayerNodeIdentifier>,
snap_manager: SnapManager,
auto_panning: AutoPanning,
}

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

/// get snapped point but ignoring current layer
indierusty marked this conversation as resolved.
Show resolved Hide resolved
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;

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, SnapTypeConfiguration::default());
let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);

// 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, viewport, SNAP_POINT_TOLERANCE, 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.
indierusty marked this conversation as resolved.
Show resolved Hide resolved
tool_data.next_point = position;
tool_data.extend = true;

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 +299,55 @@ impl Fsm for SplineToolFsmState {
SplineToolFsmState::Drawing
}
(SplineToolFsmState::Drawing, SplineToolMessage::DragStop) => {
responses.add(DocumentMessage::EndTransaction);

let Some(layer) = tool_data.layer else {
// if extending ignore first DragStop event to avoid inserting new point.
indierusty marked this conversation as resolved.
Show resolved Hide resolved
if tool_data.extend {
tool_data.extend = false;
return SplineToolFsmState::Drawing;
}
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);

// endpoints snapping
indierusty marked this conversation as resolved.
Show resolved Hide resolved
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);
}

update_spline(tool_data, true, responses);
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 +369,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 All @@ -304,7 +387,10 @@ impl Fsm for SplineToolFsmState {

fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Draw Spline")])]),
SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::Lmb, "Draw Spline"),
HintInfo::keys([Key::Shift], "Append to Selected Layer").prepend_plus(),
])]),
SplineToolFsmState::Drawing => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Extend Spline")]),
Expand All @@ -320,7 +406,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.
indierusty marked this conversation as resolved.
Show resolved Hide resolved
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.
indierusty marked this conversation as resolved.
Show resolved Hide resolved
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 {
indierusty marked this conversation as resolved.
Show resolved Hide resolved
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
indierusty marked this conversation as resolved.
Show resolved Hide resolved
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