From a78ee3c52aefba0ccafcfae4e712c078274396ed Mon Sep 17 00:00:00 2001 From: Ashish Mohapatra Date: Wed, 26 Nov 2025 19:23:46 +0530 Subject: [PATCH] Add Heart shape tool - Add new_heart() method - Add Heart node to vector generator nodes - Add Heart to ShapeType enum and UI dropdown --- .../graph_modification_utils.rs | 4 ++ .../shapes/heart_shape.rs | 53 +++++++++++++++++++ .../tool/common_functionality/shapes/mod.rs | 2 + .../shapes/shape_utility.rs | 2 + .../messages/tool/tool_messages/shape_tool.rs | 27 +++++++--- .../vector-types/src/subpath/core.rs | 12 +++++ .../nodes/vector/src/generator_nodes.rs | 13 +++++ 7 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/shapes/heart_shape.rs 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..c9fe59926a 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -376,6 +376,10 @@ pub fn get_grid_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Grid") } +pub fn get_heart_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Heart") +} + /// Gets properties from the Text node pub fn get_text(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<(&String, &Font, TypesettingConfig, bool)> { let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Text")?; diff --git a/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs b/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs new file mode 100644 index 0000000000..8dd6eab986 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs @@ -0,0 +1,53 @@ +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::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeToolModifierKey; +use crate::messages::tool::tool_messages::shape_tool::ShapeToolData; +use crate::messages::tool::tool_messages::tool_prelude::*; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; + +#[derive(Default)] +pub struct Heart; + +impl Heart { + pub fn create_node() -> NodeTemplate { + let node_type = resolve_document_node_type("Heart").expect("Heart node can't be found"); + node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.), false))]) + } + + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + viewport: &ViewportMessageHandler, + layer: LayerNodeIdentifier, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let center = modifier[0]; + let [start, end] = shape_tool_data.data.calculate_circle_points(document, ipp, viewport, center); + let Some(node_id) = graph_modification_utils::get_heart_id(layer, &document.network_interface) else { + return; + }; + + let dimensions = (start - end).abs(); + + let radius: f64 = if dimensions.x > dimensions.y { dimensions.y / 2. } else { dimensions.x / 2. }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::F64(radius), false), + }); + + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., start.midpoint(end)), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/mod.rs b/editor/src/messages/tool/common_functionality/shapes/mod.rs index 5031a6224e..0f8d947da9 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -2,6 +2,7 @@ pub mod arc_shape; pub mod circle_shape; pub mod ellipse_shape; pub mod grid_shape; +pub mod heart_shape; pub mod line_shape; pub mod polygon_shape; pub mod rectangle_shape; @@ -10,6 +11,7 @@ pub mod spiral_shape; pub mod star_shape; pub use super::shapes::ellipse_shape::Ellipse; +pub use super::shapes::heart_shape::Heart; pub use super::shapes::line_shape::{Line, LineEnd}; pub use super::shapes::rectangle_shape::Rectangle; pub use crate::messages::tool::tool_messages::shape_tool::ShapeToolData; 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..528648472c 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -31,6 +31,7 @@ pub enum ShapeType { Arc, Spiral, Grid, + Heart, Rectangle, Ellipse, Line, @@ -45,6 +46,7 @@ impl ShapeType { Self::Arc => "Arc", Self::Grid => "Grid", Self::Spiral => "Spiral", + Self::Heart => "Heart", Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", Self::Line => "Line", diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 235c830c74..6107d29eb1 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -11,6 +11,7 @@ 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::circle_shape::Circle; use crate::messages::tool::common_functionality::shapes::grid_shape::Grid; +use crate::messages::tool::common_functionality::shapes::heart_shape::Heart; use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints}; use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon; use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays}; @@ -168,6 +169,12 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { } .into() }), + MenuListEntry::new("Heart").label("Heart").on_commit(move |_| { + ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(ShapeType::Heart), + } + .into() + }), ]]; DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder() } @@ -806,7 +813,7 @@ 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 => { + ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Heart | ShapeType::Rectangle | ShapeType::Ellipse => { tool_data.data.start(document, input, viewport); } ShapeType::Line => { @@ -828,6 +835,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Arc => Arc::create_node(tool_options.arc_type), ShapeType::Spiral => Spiral::create_node(tool_options.spiral_type, tool_options.turns), ShapeType::Grid => Grid::create_node(tool_options.grid_type), + ShapeType::Heart => Heart::create_node(), ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Ellipse => Ellipse::create_node(), ShapeType::Line => Line::create_node(document, tool_data.data.drag_start), @@ -839,7 +847,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::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Heart | ShapeType::Rectangle | ShapeType::Ellipse => { defered_responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -878,6 +886,7 @@ impl Fsm for ShapeToolFsmState { 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::Grid => Grid::update_shape(document, input, layer, tool_options.grid_type, tool_data, modifier, responses), + ShapeType::Heart => Heart::update_shape(document, input, viewport, layer, 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), ShapeType::Line => Line::update_shape(document, input, viewport, layer, tool_data, modifier, responses), @@ -1118,10 +1127,14 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Circle"), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])], + ShapeType::Circle => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Circle"), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + ShapeType::Heart => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Heart"), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], ShapeType::Arc => vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arc"), HintInfo::keys([Key::Shift], "Constrain Arc").prepend_plus(), @@ -1147,7 +1160,7 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Circle | ShapeType::Heart => 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 c5600d61a1..d41ebcaf5e 100644 --- a/node-graph/libraries/vector-types/src/subpath/core.rs +++ b/node-graph/libraries/vector-types/src/subpath/core.rs @@ -317,6 +317,18 @@ impl Subpath { Self::from_anchors([p1, p2], false) } + /// Constructs a heart shape centered at the origin with the given radius. + pub fn new_heart(center: DVec2, radius: f64) -> Self { + let bottom = center + DVec2::new(0., radius); + let top = center + DVec2::new(0., -radius * 0.4); + + let manipulator_groups = vec![ + ManipulatorGroup::new(bottom, Some(bottom + DVec2::new(radius * 1.2, -radius * 0.9)), Some(bottom + DVec2::new(-radius * 1.2, -radius * 0.9))), + ManipulatorGroup::new(top, Some(top + DVec2::new(-radius * 1.2, -radius * 0.6)), Some(top + DVec2::new(radius * 1.2, -radius * 0.6))), + ]; + Self::new(manipulator_groups, 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 df587528ef..d45c1a3d87 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -201,6 +201,19 @@ fn line( Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_line(start, end))) } +/// Generates a heart shape with a chosen radius. +#[node_macro::node(category("Vector: Shape"))] +fn heart( + _: impl Ctx, + _primary: (), + #[unit(" px")] + #[default(50.)] + radius: f64, +) -> Table { + let radius = radius.abs(); + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_heart(DVec2::ZERO, radius))) +} + trait GridSpacing { fn as_dvec2(&self) -> DVec2; }