Add an arrow to the Shape tool (#3343)

* add arrow shape feature in editor

Signed-off-by: krVatsal <kumarvatsal34@gmail.com>

* fix the arrow tool to show arrow in viewport space

Signed-off-by: krVatsal <kumarvatsal34@gmail.com>

* fix the direction of arrow and make the new arrow node

Signed-off-by: krVatsal <kumarvatsal34@gmail.com>

* updated arrow tool to hae start and end points

Signed-off-by: krVatsal <kumarvatsal34@gmail.com>

* fixed calculate point bug

Signed-off-by: krVatsal <kumarvatsal34@gmail.com>

* fixed some bugs of arrow positioning

Signed-off-by: krVatsal <kumarvatsal34@gmail.com>

* fixed formatting in whole codebase and added fill to arrow

Signed-off-by: krVatsal <kumarvatsal34@gmail.com>

* fix

---------

Signed-off-by: krVatsal <kumarvatsal34@gmail.com>
Co-authored-by: Timon <me@timon.zip>
This commit is contained in:
Vatsal Kumar 2026-01-12 06:28:28 +05:30 committed by GitHub
parent 479688d86b
commit 4fea2b0fe7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 186 additions and 8 deletions

View File

@ -364,6 +364,10 @@ pub fn get_arc_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInt
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Arc")
}
pub fn get_arrow_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Arrow")
}
pub fn get_spiral_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Spiral")
}

View File

@ -0,0 +1,90 @@
use super::shape_utility::ShapeToolModifierKey;
use super::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type;
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::network_interface::{InputConnector, NodeTemplate};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use glam::DVec2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
#[derive(Default)]
pub struct Arrow;
impl Arrow {
pub fn create_node(document: &DocumentMessageHandler, drag_start: DVec2) -> NodeTemplate {
let node_type = resolve_document_node_type("Arrow").expect("Arrow node does not exist");
let viewport_pos = document.metadata().document_to_viewport.transform_point2(drag_start);
node_type.node_template_input_override([
None,
Some(NodeInput::value(TaggedValue::DVec2(viewport_pos), false)), // start
Some(NodeInput::value(TaggedValue::DVec2(viewport_pos), false)), // end
Some(NodeInput::value(TaggedValue::F64(10.), false)), // shaft_width
Some(NodeInput::value(TaggedValue::F64(30.), false)), // head_width
Some(NodeInput::value(TaggedValue::F64(20.), false)), // head_length
])
}
pub fn update_shape(
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
_viewport: &ViewportMessageHandler,
layer: LayerNodeIdentifier,
tool_data: &mut ShapeToolData,
_modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
// Track current mouse position in viewport space
tool_data.line_data.drag_current = input.mouse.position;
// Convert both points to document space (matching Line tool pattern)
let document_to_viewport = document.metadata().document_to_viewport;
let start_document = tool_data.data.drag_start;
let end_document = document_to_viewport.inverse().transform_point2(tool_data.line_data.drag_current);
// Calculate length in document space for validation
let delta = end_document - start_document;
let length_document = delta.length();
if length_document < 1e-6 {
return;
}
let Some(node_id) = graph_modification_utils::get_arrow_id(layer, &document.network_interface) else {
return;
};
// Calculate proportional dimensions based on arrow length
let shaft_width = length_document * 0.1;
let head_width = length_document * 0.3;
let head_length = length_document * 0.2;
// Update Arrow node parameters with document space coordinates (like Line tool)
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::DVec2(start_document), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::DVec2(end_document), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 3),
input: NodeInput::value(TaggedValue::F64(shaft_width), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 4),
input: NodeInput::value(TaggedValue::F64(head_width), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 5),
input: NodeInput::value(TaggedValue::F64(head_length), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
pub fn overlays(_document: &DocumentMessageHandler, _tool_data: &ShapeToolData, _overlay_context: &mut OverlayContext) {}
}

View File

@ -1,4 +1,5 @@
pub mod arc_shape;
pub mod arrow_shape;
pub mod circle_shape;
pub mod ellipse_shape;
pub mod grid_shape;
@ -9,6 +10,7 @@ pub mod shape_utility;
pub mod spiral_shape;
pub mod star_shape;
pub use super::shapes::arrow_shape::Arrow;
pub use super::shapes::ellipse_shape::Ellipse;
pub use super::shapes::line_shape::{Line, LineEnd};
pub use super::shapes::rectangle_shape::Rectangle;

View File

@ -33,6 +33,7 @@ pub enum ShapeType {
Grid,
Rectangle,
Ellipse,
Arrow,
Line,
}
@ -47,6 +48,7 @@ impl ShapeType {
Self::Spiral => "Spiral",
Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse",
Self::Arrow => "Arrow",
Self::Line => "Line",
})
.into()
@ -57,6 +59,7 @@ impl ShapeType {
Self::Line => "Line Tool",
Self::Rectangle => "Rectangle Tool",
Self::Ellipse => "Ellipse Tool",
Self::Arrow => "Arrow Tool",
_ => "",
})
.into()
@ -75,6 +78,7 @@ impl ShapeType {
Self::Line => "VectorLineTool",
Self::Rectangle => "VectorRectangleTool",
Self::Ellipse => "VectorEllipseTool",
Self::Arrow => "VectorArrowTool",
_ => "",
})
.into()
@ -85,6 +89,7 @@ impl ShapeType {
Self::Line => ToolType::Line,
Self::Rectangle => ToolType::Rectangle,
Self::Ellipse => ToolType::Ellipse,
Self::Arrow => ToolType::Shape,
_ => ToolType::Shape,
}
}

View File

@ -9,6 +9,7 @@ use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoMan
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::shapes::arc_shape::Arc;
use crate::messages::tool::common_functionality::shapes::arrow_shape::Arrow;
use crate::messages::tool::common_functionality::shapes::circle_shape::Circle;
use crate::messages::tool::common_functionality::shapes::grid_shape::Grid;
use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints};
@ -168,6 +169,30 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetInstance {
}
.into()
}),
MenuListEntry::new("Rectangle").label("Rectangle").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Rectangle),
}
.into()
}),
MenuListEntry::new("Ellipse").label("Ellipse").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Ellipse),
}
.into()
}),
MenuListEntry::new("Arrow").label("Arrow").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Arrow),
}
.into()
}),
MenuListEntry::new("Line").label("Line").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Line),
}
.into()
}),
]];
DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_instance()
}
@ -807,7 +832,7 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => {
tool_data.data.start(document, input, viewport);
}
ShapeType::Line => {
ShapeType::Arrow | ShapeType::Line => {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = tool_data
.data
@ -828,6 +853,7 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Grid => Grid::create_node(tool_options.grid_type),
ShapeType::Rectangle => Rectangle::create_node(),
ShapeType::Ellipse => Ellipse::create_node(),
ShapeType::Arrow => Arrow::create_node(document, tool_data.data.drag_start),
ShapeType::Line => Line::create_node(document, tool_data.data.drag_start),
};
@ -847,6 +873,11 @@ impl Fsm for ShapeToolFsmState {
tool_options.fill.apply_fill(layer, defered_responses);
}
ShapeType::Arrow => {
tool_data.line_data.weight = tool_options.line_weight;
tool_data.line_data.editing_layer = Some(layer);
tool_options.fill.apply_fill(layer, defered_responses);
}
ShapeType::Line => {
tool_data.line_data.weight = tool_options.line_weight;
tool_data.line_data.editing_layer = Some(layer);
@ -874,6 +905,7 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Star => Star::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Circle => Circle::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Arc => Arc::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Arrow => Arrow::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Spiral => Spiral::update_shape(document, input, viewport, layer, tool_data, responses),
ShapeType::Grid => Grid::update_shape(document, input, layer, tool_options.grid_type, tool_data, modifier, responses),
ShapeType::Rectangle => Rectangle::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
@ -1018,6 +1050,7 @@ impl Fsm for ShapeToolFsmState {
}
tool_data.line_data.dragging_endpoint = None;
tool_data.line_data.editing_layer = None;
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
@ -1035,6 +1068,7 @@ impl Fsm for ShapeToolFsmState {
responses.add(DocumentMessage::AbortTransaction);
tool_data.data.cleanup(responses);
tool_data.line_data.dragging_endpoint = None;
tool_data.line_data.editing_layer = None;
tool_data.gizmo_manager.handle_cleanup();
@ -1130,6 +1164,7 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
ShapeType::Arrow => vec![HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arrow")])],
};
HintData(hint_groups)
}
@ -1145,6 +1180,7 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
ShapeType::Arrow => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Angle")]),
ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Spiral => HintGroup(vec![]),
};

View File

@ -312,6 +312,37 @@ impl<PointId: Identifier> Subpath<PointId> {
Self::from_anchors([p1, p2], false)
}
/// Constructs an arrow shape from start and end points with parametric control over dimensions
pub fn new_arrow(start: DVec2, end: DVec2, shaft_width: f64, head_width: f64, head_length: f64) -> Self {
let delta = end - start;
let length = delta.length();
if length < 1e-10 {
// Degenerate case: return a point
return Self::from_anchors([start], true);
}
let direction = delta / length;
let perpendicular = DVec2::new(-direction.y, direction.x);
let half_shaft = shaft_width * 0.5;
let half_head = head_width * 0.5;
let head_base_distance = (length - head_length).max(0.);
let head_base = start + direction * head_base_distance;
let anchors = [
start - perpendicular * half_shaft, // Tail bottom
head_base - perpendicular * half_shaft, // Head base bottom (shaft)
head_base - perpendicular * half_head, // Head base bottom (wide)
end, // Tip
head_base + perpendicular * half_head, // Head base top (wide)
head_base + perpendicular * half_shaft, // Head base top (shaft)
start + perpendicular * half_shaft, // Tail top
];
Self::from_anchors(anchors, true)
}
pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, start_angle: f64, delta_theta: f64, spiral_type: SpiralType) -> Self {
let mut manipulator_groups = Vec::new();
let mut prev_in_handle = None;

View File

@ -188,16 +188,26 @@ fn star<T: AsU64>(
/// Generates a line with endpoints at the two chosen coordinates.
#[node_macro::node(category("Vector: Shape"))]
fn line(
fn arrow(
_: impl Ctx,
_primary: (),
/// Coordinate of the line's initial endpoint.
#[default(0., 0.)]
start: PixelSize,
/// Coordinate of the line's terminal endpoint.
#[default(100., 100.)]
end: PixelSize,
#[default(0., 0.)] start: PixelSize,
#[default(100., 0.)] end: PixelSize,
#[unit(" px")]
#[default(10)]
shaft_width: f64,
#[unit(" px")]
#[default(30)]
head_width: f64,
#[unit(" px")]
#[default(20)]
head_length: f64,
) -> Table<Vector> {
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_arrow(start, end, shaft_width, head_width, head_length)))
}
#[node_macro::node(category("Vector: Shape"))]
fn line(_: impl Ctx, _primary: (), #[default(0., 0.)] start: PixelSize, #[default(100., 100.)] end: PixelSize) -> Table<Vector> {
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_line(start, end)))
}