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 0c2d87a1f0..a913bb69bc 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..b2e5589599 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/arrow_shape.rs @@ -0,0 +1,84 @@ +use super::shape_utility::ShapeToolModifierKey; +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +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::{DAffine2, 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() -> NodeTemplate { + let node_type = resolve_document_node_type("Arrow").expect("Arrow node does not exist"); + node_type.node_template_input_override([ + None, + Some(NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false)), // start + Some(NodeInput::value(TaggedValue::DVec2(DVec2::new(100., 0.)), 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, + layer: LayerNodeIdentifier, + tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let [center, lock_ratio, _] = modifier; + + let Some([start, end]) = tool_data.data.calculate_points(document, input, center, lock_ratio) else { + return; + }; + + let delta = end - start; + let length = delta.length(); + if length < 1e-6 { + return; + } + + let Some(node_id) = graph_modification_utils::get_arrow_id(layer, &document.network_interface) else { + return; + }; + + // Calculate proportional dimensions + let shaft_width = length * 0.1; + let head_width = length * 0.3; + let head_length = length * 0.2; + + // Update Arrow node parameters - now using start/end points instead of transform + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::DVec2(start), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::DVec2(end), 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), + }); + } + + 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 fb7d913faa..07bcc9e211 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() @@ -67,6 +70,7 @@ impl ShapeType { Self::Line => "VectorLineTool", Self::Rectangle => "VectorRectangleTool", Self::Ellipse => "VectorEllipseTool", + Self::Arrow => "VectorArrowTool", _ => "", }) .into() @@ -77,6 +81,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 235c830c74..96d8d1628a 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) -> WidgetHolder { } .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_holder() } @@ -806,8 +831,8 @@ 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); + ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse | ShapeType::Arrow => { + tool_data.data.start(document, input) } ShapeType::Line => { let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); @@ -826,6 +851,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Star => Star::create_node(tool_options.vertices), ShapeType::Circle => Circle::create_node(), ShapeType::Arc => Arc::create_node(tool_options.arc_type), + ShapeType::Arrow => Arrow::create_node(), ShapeType::Spiral => Spiral::create_node(tool_options.spiral_type, tool_options.turns), ShapeType::Grid => Grid::create_node(tool_options.grid_type), ShapeType::Rectangle => Rectangle::create_node(), @@ -839,7 +865,7 @@ impl Fsm for ShapeToolFsmState { let defered_responses = &mut VecDeque::new(); match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => { + ShapeType::Arrow | ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => { defered_responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -872,11 +898,12 @@ impl Fsm for ShapeToolFsmState { }; match tool_data.current_shape { - ShapeType::Polygon => Polygon::update_shape(document, input, viewport, layer, tool_data, modifier, responses), - 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::Spiral => Spiral::update_shape(document, input, viewport, layer, tool_data, responses), + ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Circle => Circle::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Arrow => Arrow::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Spiral => Spiral::update_shape(document, input, 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), ShapeType::Ellipse => Ellipse::update_shape(document, input, viewport, layer, tool_data, modifier, responses), @@ -1132,6 +1159,7 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque vec![HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arrow")])], }; HintData(hint_groups) } @@ -1147,6 +1175,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/gcore/src/subpath/core.rs b/node-graph/gcore/src/subpath/core.rs index a8329d99cf..eec78d8cde 100644 --- a/node-graph/gcore/src/subpath/core.rs +++ b/node-graph/gcore/src/subpath/core.rs @@ -317,6 +317,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/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index ed286ac212..4f93b68ad5 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/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))) }