Skip to content

Commit 2910e50

Browse files
authored
Improve Shape tool arrow mode interactive drawing with angle modifier keys and endpoint gizmos (#3874)
* Make the order of Shape tool shape types consistent * Add Arrow shape modifier keys and snapping support * Add endpoint dragging to arrows * Show the default cursor when hovering line/arrow endpoints * Reduce duplicated function * Fix incorrect coordinate spaces * Improve endpoint dragging clarity
1 parent e7a2800 commit 2910e50

File tree

4 files changed

+177
-99
lines changed

4 files changed

+177
-99
lines changed

editor/src/messages/tool/common_functionality/shapes/arrow_shape.rs

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
use super::line_shape::{LineEnd, generate_line};
12
use super::shape_utility::ShapeToolModifierKey;
23
use super::*;
4+
use crate::consts::BOUNDS_SELECT_THRESHOLD;
35
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
46
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type};
57
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
68
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
79
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
810
use crate::messages::prelude::*;
911
use crate::messages::tool::common_functionality::graph_modification_utils;
12+
pub use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
13+
use crate::messages::tool::common_functionality::snapping::SnapData;
1014
use glam::{DAffine2, DVec2};
1115
use graph_craft::document::NodeInput;
1216
use graph_craft::document::value::TaggedValue;
@@ -31,20 +35,26 @@ impl Arrow {
3135
pub fn update_shape(
3236
document: &DocumentMessageHandler,
3337
input: &InputPreprocessorMessageHandler,
34-
_viewport: &ViewportMessageHandler,
38+
viewport: &ViewportMessageHandler,
3539
layer: LayerNodeIdentifier,
3640
tool_data: &mut ShapeToolData,
37-
_modifier: ShapeToolModifierKey,
41+
modifier: ShapeToolModifierKey,
3842
responses: &mut VecDeque<Message>,
3943
) {
40-
// Track current mouse position in viewport space
44+
let [center, snap_angle, lock_angle] = modifier;
45+
4146
tool_data.line_data.drag_current = input.mouse.position;
4247

43-
// Compute arrow_to in document space
44-
let document_to_viewport = document.metadata().document_to_viewport;
45-
let start_document = tool_data.data.drag_start;
46-
let end_document = document_to_viewport.inverse().transform_point2(input.mouse.position);
47-
let arrow_to = end_document - start_document;
48+
let keyboard = &input.keyboard;
49+
let ignore = [layer];
50+
let snap_data = SnapData::ignore(document, input, viewport, &ignore);
51+
let mut document_points = generate_line(tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center));
52+
53+
if tool_data.line_data.dragging_endpoint == Some(LineEnd::Start) {
54+
document_points.swap(0, 1);
55+
}
56+
57+
let arrow_to = document_points[1] - document_points[0];
4858

4959
if arrow_to.length() < 1e-6 {
5060
return;
@@ -54,7 +64,8 @@ impl Arrow {
5464
return;
5565
};
5666

57-
// Update Arrow node arrow_to in document space
67+
let document_to_viewport = document.metadata().document_to_viewport;
68+
5869
responses.add(NodeGraphMessage::SetInput {
5970
input_connector: InputConnector::node(node_id, 1),
6071
input: NodeInput::value(TaggedValue::DVec2(arrow_to), false),
@@ -63,13 +74,43 @@ impl Arrow {
6374
let scope = downstream.inverse() * document_to_viewport;
6475
responses.add(GraphOperationMessage::TransformSet {
6576
layer,
66-
transform: DAffine2::from_translation(start_document),
77+
transform: DAffine2::from_translation(document_points[0]),
6778
transform_in: TransformIn::Scope { scope },
6879
skip_rerender: false,
6980
});
7081

7182
responses.add(NodeGraphMessage::RunDocumentGraph);
7283
}
7384

74-
pub fn overlays(_document: &DocumentMessageHandler, _tool_data: &ShapeToolData, _overlay_context: &mut OverlayContext) {}
85+
pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, mouse_position: DVec2, overlay_context: &mut OverlayContext) {
86+
let arrow_layers: HashMap<LayerNodeIdentifier, [DVec2; 2]> = document
87+
.network_interface
88+
.selected_nodes()
89+
.selected_visible_and_unlocked_layers(&document.network_interface)
90+
.filter_map(|layer| {
91+
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::arrow::IDENTIFIER))?;
92+
let Some(&TaggedValue::DVec2(arrow_to)) = node_inputs[1].as_value() else { return None };
93+
94+
let transform = document.metadata().transform_to_viewport(layer);
95+
let viewport_start = transform.transform_point2(DVec2::ZERO);
96+
let viewport_end = transform.transform_point2(arrow_to);
97+
98+
if !arrow_to.abs_diff_eq(DVec2::ZERO, f64::EPSILON * 1000.) {
99+
let is_editing = shape_tool_data.line_data.editing_layer == Some(layer);
100+
for (i, pos) in [viewport_start, viewport_end].into_iter().enumerate() {
101+
let is_dragged = is_editing && matches!((i, &shape_tool_data.line_data.dragging_endpoint), (0, Some(LineEnd::Start)) | (1, Some(LineEnd::End)));
102+
if is_dragged || (pos - mouse_position).length_squared() < BOUNDS_SELECT_THRESHOLD.powi(2) {
103+
overlay_context.hover_manipulator_anchor(pos, is_dragged);
104+
} else {
105+
overlay_context.square(pos, Some(6.), None, None);
106+
}
107+
}
108+
}
109+
110+
Some((layer, [DVec2::ZERO, arrow_to]))
111+
})
112+
.collect();
113+
114+
shape_tool_data.line_data.selected_layers_with_position.extend(arrow_layers);
115+
}
75116
}

editor/src/messages/tool/common_functionality/shapes/line_shape.rs

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ impl Line {
8787
});
8888
}
8989

90-
pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) {
90+
pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, mouse_position: DVec2, overlay_context: &mut OverlayContext) {
9191
shape_tool_data.line_data.selected_layers_with_position = document
9292
.network_interface
9393
.selected_nodes()
@@ -106,8 +106,15 @@ impl Line {
106106
let viewport_end = transform.transform_point2(line_to);
107107
if !line_to.abs_diff_eq(DVec2::ZERO, f64::EPSILON * 1000.) {
108108
overlay_context.line(viewport_start, viewport_end, None, None);
109-
overlay_context.square(viewport_start, Some(6.), None, None);
110-
overlay_context.square(viewport_end, Some(6.), None, None);
109+
let is_editing = shape_tool_data.line_data.editing_layer == Some(layer);
110+
for (i, pos) in [viewport_start, viewport_end].into_iter().enumerate() {
111+
let is_dragged = is_editing && matches!((i, &shape_tool_data.line_data.dragging_endpoint), (0, Some(LineEnd::Start)) | (1, Some(LineEnd::End)));
112+
if is_dragged || (pos - mouse_position).length_squared() < BOUNDS_SELECT_THRESHOLD.powi(2) {
113+
overlay_context.hover_manipulator_anchor(pos, is_dragged);
114+
} else {
115+
overlay_context.square(pos, Some(6.), None, None);
116+
}
117+
}
111118
}
112119

113120
// Store local-space positions for endpoint editing
@@ -117,7 +124,7 @@ impl Line {
117124
}
118125
}
119126

120-
fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] {
127+
pub fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] {
121128
let document_to_viewport = snap_data.document.metadata().document_to_viewport;
122129
let mut document_points = [tool_data.data.drag_start, document_to_viewport.inverse().transform_point2(tool_data.line_data.drag_current)];
123130

@@ -180,42 +187,6 @@ fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle:
180187
document_points
181188
}
182189

183-
pub fn clicked_on_line_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, shape_tool_data: &mut ShapeToolData) -> bool {
184-
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::line::IDENTIFIER)) else {
185-
return false;
186-
};
187-
188-
let Some(&TaggedValue::DVec2(line_to)) = node_inputs[1].as_value() else {
189-
return false;
190-
};
191-
192-
// Line goes from local origin (0,0) to line_to, positioned by the Transform node
193-
let local_start = DVec2::ZERO;
194-
let local_end = line_to;
195-
196-
let transform = document.metadata().transform_to_viewport(layer);
197-
let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
198-
let viewport_y = transform.transform_vector2(DVec2::Y).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
199-
let threshold_x = transform.inverse().transform_vector2(viewport_x).length();
200-
let threshold_y = transform.inverse().transform_vector2(viewport_y).length();
201-
202-
let drag_start = input.mouse.position;
203-
let [start, end] = [local_start, local_end].map(|point| transform.transform_point2(point));
204-
205-
let start_click = (drag_start.y - start.y).abs() < threshold_y && (drag_start.x - start.x).abs() < threshold_x;
206-
let end_click = (drag_start.y - end.y).abs() < threshold_y && (drag_start.x - end.x).abs() < threshold_x;
207-
208-
if start_click || end_click {
209-
shape_tool_data.line_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start });
210-
// Convert the anchor endpoint (the one NOT being dragged) to document space for drag_start
211-
let anchor_local = if end_click { local_start } else { local_end };
212-
shape_tool_data.data.drag_start = document.metadata().transform_to_document(layer).transform_point2(anchor_local);
213-
shape_tool_data.line_data.editing_layer = Some(layer);
214-
return true;
215-
}
216-
false
217-
}
218-
219190
#[cfg(test)]
220191
mod test_line_tool {
221192
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;

editor/src/messages/tool/common_functionality/shapes/shape_utility.rs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::ShapeToolData;
2-
use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT};
2+
use super::line_shape::LineEnd;
3+
use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT, BOUNDS_SELECT_THRESHOLD};
34
use crate::messages::frontend::utility_types::MouseCursorIcon;
45
use crate::messages::message::Message;
56
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
@@ -33,10 +34,10 @@ pub enum ShapeType {
3334
Arc,
3435
Spiral,
3536
Grid,
36-
Rectangle,
37-
Ellipse,
3837
Arrow,
39-
Line,
38+
Line, // KEEP THIS AT THE END
39+
Rectangle, // KEEP THIS AT THE END
40+
Ellipse, // KEEP THIS AT THE END
4041
}
4142

4243
impl ShapeType {
@@ -46,12 +47,12 @@ impl ShapeType {
4647
Self::Star => "Star",
4748
Self::Circle => "Circle",
4849
Self::Arc => "Arc",
49-
Self::Grid => "Grid",
5050
Self::Spiral => "Spiral",
51-
Self::Rectangle => "Rectangle",
52-
Self::Ellipse => "Ellipse",
51+
Self::Grid => "Grid",
5352
Self::Arrow => "Arrow",
5453
Self::Line => "Line",
54+
Self::Rectangle => "Rectangle",
55+
Self::Ellipse => "Ellipse",
5556
})
5657
.into()
5758
}
@@ -61,7 +62,6 @@ impl ShapeType {
6162
Self::Line => "Line Tool",
6263
Self::Rectangle => "Rectangle Tool",
6364
Self::Ellipse => "Ellipse Tool",
64-
Self::Arrow => "Arrow Tool",
6565
_ => "",
6666
})
6767
.into()
@@ -80,7 +80,6 @@ impl ShapeType {
8080
Self::Line => "VectorLineTool",
8181
Self::Rectangle => "VectorRectangleTool",
8282
Self::Ellipse => "VectorEllipseTool",
83-
Self::Arrow => "VectorArrowTool",
8483
_ => "",
8584
})
8685
.into()
@@ -91,7 +90,6 @@ impl ShapeType {
9190
Self::Line => ToolType::Line,
9291
Self::Rectangle => ToolType::Rectangle,
9392
Self::Ellipse => ToolType::Ellipse,
94-
Self::Arrow => ToolType::Shape,
9593
_ => ToolType::Shape,
9694
}
9795
}
@@ -154,6 +152,42 @@ pub trait ShapeGizmoHandler {
154152
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon>;
155153
}
156154

155+
/// Check if the mouse clicked on either endpoint of a line-like shape (Line or Arrow).
156+
pub fn clicked_on_shape_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, shape_tool_data: &mut ShapeToolData) -> bool {
157+
let line_like_shape_nodes = [
158+
DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::line::IDENTIFIER),
159+
DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::arrow::IDENTIFIER),
160+
];
161+
162+
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
163+
let endpoint = line_like_shape_nodes.iter().find_map(|id| {
164+
let node_inputs = node_graph_layer.find_node_inputs(id)?;
165+
let &TaggedValue::DVec2(endpoint) = node_inputs[1].as_value()? else { return None };
166+
Some(endpoint)
167+
});
168+
let Some(endpoint) = endpoint else { return false };
169+
170+
let local_start = DVec2::ZERO;
171+
let local_end = endpoint;
172+
173+
let transform = document.metadata().transform_to_viewport(layer);
174+
let mouse_pos = input.mouse.position;
175+
let [start, end] = [local_start, local_end].map(|point| transform.transform_point2(point));
176+
177+
let start_click = (mouse_pos - start).length_squared() < BOUNDS_SELECT_THRESHOLD.powi(2);
178+
let end_click = (mouse_pos - end).length_squared() < BOUNDS_SELECT_THRESHOLD.powi(2);
179+
let endpoint_click = start_click || end_click;
180+
181+
if endpoint_click {
182+
shape_tool_data.line_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start });
183+
let anchor_local = if end_click { local_start } else { local_end };
184+
shape_tool_data.data.drag_start = document.metadata().transform_to_document(layer).transform_point2(anchor_local);
185+
shape_tool_data.line_data.editing_layer = Some(layer);
186+
}
187+
188+
endpoint_click
189+
}
190+
157191
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
158192
pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
159193
let sign_num = if end[1] > start[1] { 1. } else { -1. };

0 commit comments

Comments
 (0)