diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index c0d1081501..779261faa6 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -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 { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Arrow") +} + pub fn get_spiral_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Spiral") } diff --git a/editor/src/messages/tool/common_functionality/shapes/arrow_shape.rs b/editor/src/messages/tool/common_functionality/shapes/arrow_shape.rs new file mode 100644 index 0000000000..ba0bd99223 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/arrow_shape.rs @@ -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, + ) { + // 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) {} +} diff --git a/editor/src/messages/tool/common_functionality/shapes/mod.rs b/editor/src/messages/tool/common_functionality/shapes/mod.rs index 5031a6224e..b005f61a19 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -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; diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index a94847c514..46ecea8baf 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -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, } } diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index ead0e1e1ce..898ec09d54 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -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() } @@ -805,9 +830,9 @@ impl Fsm for ShapeToolFsmState { match tool_data.current_shape { ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => { - tool_data.data.start(document, input, viewport); + 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 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 HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Angle")]), ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Spiral => HintGroup(vec![]), }; diff --git a/node-graph/libraries/vector-types/src/subpath/core.rs b/node-graph/libraries/vector-types/src/subpath/core.rs index 2d6ff253fd..ae1922c233 100644 --- a/node-graph/libraries/vector-types/src/subpath/core.rs +++ b/node-graph/libraries/vector-types/src/subpath/core.rs @@ -312,6 +312,37 @@ impl Subpath { 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; diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 361b6dd8fb..098b1782ac 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -188,16 +188,26 @@ fn star( /// 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 { + 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 { Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_line(start, end))) }