From 7c890209b3eecef77183c5624c6139de47a5e4e0 Mon Sep 17 00:00:00 2001 From: LennartAppAkademie Date: Fri, 3 Apr 2026 13:25:03 +0200 Subject: [PATCH 1/5] added teardrop and heard shape to the predefined shaped --- .../gizmos/gizmo_manager.rs | 31 ++++ .../graph_modification_utils.rs | 8 + .../shapes/heart_shape.rs | 163 ++++++++++++++++++ .../tool/common_functionality/shapes/mod.rs | 2 + .../shapes/shape_utility.rs | 4 + .../shapes/teardrop_shape.rs | 163 ++++++++++++++++++ .../messages/tool/tool_messages/shape_tool.rs | 52 +++++- .../vector-types/src/subpath/core.rs | 42 +++++ .../nodes/vector/src/generator_nodes.rs | 25 +++ 9 files changed, 488 insertions(+), 2 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/shapes/heart_shape.rs create mode 100644 editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs diff --git a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs index 962e9a5d06..b3733e0626 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -8,10 +8,13 @@ use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::arc_shape::ArcGizmoHandler; use crate::messages::tool::common_functionality::shapes::circle_shape::CircleGizmoHandler; use crate::messages::tool::common_functionality::shapes::grid_shape::GridGizmoHandler; +use crate::messages::tool::common_functionality::shapes::heart_shape::HeartGizmoHandler; use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::common_functionality::shapes::spiral_shape::SpiralGizmoHandler; use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler; +use crate::messages::tool::common_functionality::shapes::teardrop_shape::TeardropGizmoHandler; + use glam::DVec2; use std::collections::VecDeque; @@ -32,6 +35,8 @@ pub enum ShapeGizmoHandlers { Circle(CircleGizmoHandler), Grid(GridGizmoHandler), Spiral(SpiralGizmoHandler), + Teardrop(TeardropGizmoHandler), + Heart(HeartGizmoHandler), } impl ShapeGizmoHandlers { @@ -45,6 +50,8 @@ impl ShapeGizmoHandlers { Self::Circle(_) => "circle", Self::Grid(_) => "grid", Self::Spiral(_) => "spiral", + Self::Teardrop(_) => "teardrop", + Self::Heart(_) => "heart", Self::None => "none", } } @@ -58,6 +65,8 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.handle_state(layer, mouse_position, document, responses), Self::Grid(h) => h.handle_state(layer, mouse_position, document, responses), Self::Spiral(h) => h.handle_state(layer, mouse_position, document, responses), + Self::Teardrop(h) => h.handle_state(layer, mouse_position, document, responses), + Self::Heart(h) => h.handle_state(layer, mouse_position, document, responses), Self::None => {} } } @@ -71,6 +80,8 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.is_any_gizmo_hovered(), Self::Grid(h) => h.is_any_gizmo_hovered(), Self::Spiral(h) => h.is_any_gizmo_hovered(), + Self::Teardrop(h) => h.is_any_gizmo_hovered(), + Self::Heart(h) => h.is_any_gizmo_hovered(), Self::None => false, } } @@ -84,6 +95,8 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.handle_click(), Self::Grid(h) => h.handle_click(), Self::Spiral(h) => h.handle_click(), + Self::Teardrop(h) => h.handle_click(), + Self::Heart(h) => h.handle_click(), Self::None => {} } } @@ -97,6 +110,8 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.handle_update(drag_start, document, input, responses), Self::Grid(h) => h.handle_update(drag_start, document, input, responses), Self::Spiral(h) => h.handle_update(drag_start, document, input, responses), + Self::Teardrop(h) => h.handle_update(drag_start, document, input, responses), + Self::Heart(h) => h.handle_update(drag_start, document, input, responses), Self::None => {} } } @@ -110,6 +125,8 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.cleanup(), Self::Grid(h) => h.cleanup(), Self::Spiral(h) => h.cleanup(), + Self::Teardrop(h) => h.cleanup(), + Self::Heart(h) => h.cleanup(), Self::None => {} } } @@ -131,6 +148,8 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Grid(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Spiral(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), + Self::Teardrop(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), + Self::Heart(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -151,6 +170,8 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Grid(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Spiral(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), + Self::Teardrop(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), + Self::Heart(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -163,6 +184,8 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.mouse_cursor_icon(), Self::Grid(h) => h.mouse_cursor_icon(), Self::Spiral(h) => h.mouse_cursor_icon(), + Self::Teardrop(h) => h.mouse_cursor_icon(), + Self::Heart(h) => h.mouse_cursor_icon(), Self::None => None, } } @@ -214,6 +237,14 @@ impl GizmoManager { if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() { return Some(ShapeGizmoHandlers::Spiral(SpiralGizmoHandler::default())); } + // Teardrop + if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() { + return Some(ShapeGizmoHandlers::Teardrop(TeardropGizmoHandler::default())); + } + // Heart + if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() { + return Some(ShapeGizmoHandlers::Heart(HeartGizmoHandler::default())); + } None } 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 51ec764b0c..f4d54871e1 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -377,6 +377,14 @@ pub fn get_spiral_id(layer: LayerNodeIdentifier, network_interface: &NodeNetwork NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::spiral::IDENTIFIER)) } +pub fn get_teardrop_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::spiral::IDENTIFIER)) +} + +pub fn get_heart_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::spiral::IDENTIFIER)) +} + pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER)) } 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..64db139258 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs @@ -0,0 +1,163 @@ +use super::shape_utility::{ShapeToolModifierKey, update_radius_sign}; +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, 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::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::{NumberOfPointsDial, NumberOfPointsDialState}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::{PointRadiusHandle, PointRadiusHandleState}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; +use crate::messages::tool::tool_messages::tool_prelude::*; +use core::f64; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Default)] +pub struct HeartGizmoHandler { + number_of_points_dial: NumberOfPointsDial, + point_radius_handle: PointRadiusHandle, +} + +impl ShapeGizmoHandler for HeartGizmoHandler { + fn is_any_gizmo_hovered(&self) -> bool { + self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered() + } + + fn handle_state(&mut self, selected_heart_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + self.number_of_points_dial.handle_actions(selected_heart_layer, mouse_position, document, responses); + self.point_radius_handle.handle_actions(selected_heart_layer, document, mouse_position, responses); + } + + fn handle_click(&mut self) { + if self.number_of_points_dial.is_hovering() { + self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging); + return; + } + + if self.point_radius_handle.hovered() { + self.point_radius_handle.update_state(PointRadiusHandleState::Dragging); + } + } + + fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + if self.number_of_points_dial.is_dragging() { + self.number_of_points_dial.update_number_of_sides(document, input, responses, drag_start); + } + + if self.point_radius_handle.is_dragging_or_snapped() { + self.point_radius_handle.update_inner_radius(document, input, responses, drag_start); + } + } + + fn overlays( + &self, + document: &DocumentMessageHandler, + selected_heart_layer: Option, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + self.number_of_points_dial.overlays(document, selected_heart_layer, shape_editor, mouse_position, overlay_context); + self.point_radius_handle.overlays(selected_heart_layer, document, overlay_context); + } + + fn dragging_overlays( + &self, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + if self.number_of_points_dial.is_dragging() { + self.number_of_points_dial.overlays(document, None, shape_editor, mouse_position, overlay_context); + } + + if self.point_radius_handle.is_dragging_or_snapped() { + self.point_radius_handle.overlays(None, document, overlay_context); + } + } + + fn cleanup(&mut self) { + self.number_of_points_dial.cleanup(); + self.point_radius_handle.cleanup(); + } + + fn mouse_cursor_icon(&self) -> Option { + if self.number_of_points_dial.is_dragging() || self.number_of_points_dial.is_hovering() { + return Some(MouseCursorIcon::EWResize); + } + + if self.point_radius_handle.is_dragging_or_snapped() || self.point_radius_handle.hovered() { + return Some(MouseCursorIcon::Default); + } + + None + } +} + +#[derive(Default)] +pub struct Heart; + +impl Heart { + pub fn create_node(_vertices: u32) -> NodeTemplate { + let identifier = DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::heart::IDENTIFIER); + let node_type = resolve_document_node_type(&identifier).expect("Heart node can't be found"); + node_type.node_template_input_override([ + None, + Some(NodeInput::value(TaggedValue::F64(50.), 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, lock_ratio, _] = modifier; + + if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, viewport, center, lock_ratio) { + // TODO: We need to determine how to allow the polygon node to make irregular shapes + update_radius_sign(end, start, layer, document, responses); + + let dimensions = (start - end).abs(); + + // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly + let mut scale = DVec2::ONE; + let radius: f64; + if dimensions.x > dimensions.y { + scale.x = dimensions.x / dimensions.y; + radius = dimensions.y / 2.; + } else { + scale.y = dimensions.y / dimensions.x; + radius = dimensions.x / 2.; + } + + let Some(node_id) = graph_modification_utils::get_heart_id(layer, &document.network_interface) else { + return; + }; + + 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(scale, 0., (start + end) / 2.), + 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 b005f61a19..878fe4ca7f 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -3,12 +3,14 @@ pub mod arrow_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; pub mod shape_utility; pub mod spiral_shape; pub mod star_shape; +pub mod teardrop_shape; pub use super::shapes::arrow_shape::Arrow; pub use super::shapes::ellipse_shape::Ellipse; 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 96dbdd5165..60afa906b2 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -35,6 +35,8 @@ pub enum ShapeType { Spiral, Grid, Arrow, + Teardrop, + Heart, Line, // KEEP THIS AT THE END Rectangle, // KEEP THIS AT THE END Ellipse, // KEEP THIS AT THE END @@ -50,6 +52,8 @@ impl ShapeType { Self::Spiral => "Spiral", Self::Grid => "Grid", Self::Arrow => "Arrow", + Self::Teardrop => "Teardrop", + Self::Heart => "Heart", Self::Line => "Line", Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", diff --git a/editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs b/editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs new file mode 100644 index 0000000000..156a4f4ae2 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs @@ -0,0 +1,163 @@ +use super::shape_utility::{ShapeToolModifierKey, update_radius_sign}; +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, 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::tool::common_functionality::gizmos::shape_gizmos::number_of_points_dial::{NumberOfPointsDial, NumberOfPointsDialState}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::{PointRadiusHandle, PointRadiusHandleState}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; +use crate::messages::tool::tool_messages::tool_prelude::*; +use core::f64; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Default)] +pub struct TeardropGizmoHandler { + number_of_points_dial: NumberOfPointsDial, + point_radius_handle: PointRadiusHandle, +} + +impl ShapeGizmoHandler for TeardropGizmoHandler { + fn is_any_gizmo_hovered(&self) -> bool { + self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered() + } + + fn handle_state(&mut self, selected_teardrop_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + self.number_of_points_dial.handle_actions(selected_teardrop_layer, mouse_position, document, responses); + self.point_radius_handle.handle_actions(selected_teardrop_layer, document, mouse_position, responses); + } + + fn handle_click(&mut self) { + if self.number_of_points_dial.is_hovering() { + self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging); + return; + } + + if self.point_radius_handle.hovered() { + self.point_radius_handle.update_state(PointRadiusHandleState::Dragging); + } + } + + fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + if self.number_of_points_dial.is_dragging() { + self.number_of_points_dial.update_number_of_sides(document, input, responses, drag_start); + } + + if self.point_radius_handle.is_dragging_or_snapped() { + self.point_radius_handle.update_inner_radius(document, input, responses, drag_start); + } + } + + fn overlays( + &self, + document: &DocumentMessageHandler, + selected_teardrop_layer: Option, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + self.number_of_points_dial.overlays(document, selected_teardrop_layer, shape_editor, mouse_position, overlay_context); + self.point_radius_handle.overlays(selected_teardrop_layer, document, overlay_context); + } + + fn dragging_overlays( + &self, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + if self.number_of_points_dial.is_dragging() { + self.number_of_points_dial.overlays(document, None, shape_editor, mouse_position, overlay_context); + } + + if self.point_radius_handle.is_dragging_or_snapped() { + self.point_radius_handle.overlays(None, document, overlay_context); + } + } + + fn cleanup(&mut self) { + self.number_of_points_dial.cleanup(); + self.point_radius_handle.cleanup(); + } + + fn mouse_cursor_icon(&self) -> Option { + if self.number_of_points_dial.is_dragging() || self.number_of_points_dial.is_hovering() { + return Some(MouseCursorIcon::EWResize); + } + + if self.point_radius_handle.is_dragging_or_snapped() || self.point_radius_handle.hovered() { + return Some(MouseCursorIcon::Default); + } + + None + } +} + +#[derive(Default)] +pub struct Teardrop; + +impl Teardrop { + pub fn create_node(_vertices: u32) -> NodeTemplate { + let identifier = DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::teardrop::IDENTIFIER); + let node_type = resolve_document_node_type(&identifier).expect("Teardrop node can't be found"); + node_type.node_template_input_override([ + None, + Some(NodeInput::value(TaggedValue::F64(50.), 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, lock_ratio, _] = modifier; + + if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, viewport, center, lock_ratio) { + // TODO: We need to determine how to allow the polygon node to make irregular shapes + update_radius_sign(end, start, layer, document, responses); + + let dimensions = (start - end).abs(); + + // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly + let mut scale = DVec2::ONE; + let radius: f64; + if dimensions.x > dimensions.y { + scale.x = dimensions.x / dimensions.y; + radius = dimensions.y / 2.; + } else { + scale.y = dimensions.y / dimensions.x; + radius = dimensions.x / 2.; + } + + let Some(node_id) = graph_modification_utils::get_teardrop_id(layer, &document.network_interface) else { + return; + }; + + 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(scale, 0., (start + end) / 2.), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } + } +} diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index fcad13c52b..fbaf1521d0 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -12,11 +12,13 @@ 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::heart_shape::Heart; use crate::messages::tool::common_functionality::shapes::line_shape::LineToolData; use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon; use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, clicked_on_shape_endpoints, transform_cage_overlays}; use crate::messages::tool::common_functionality::shapes::spiral_shape::Spiral; use crate::messages::tool::common_functionality::shapes::star_shape::Star; +use crate::messages::tool::common_functionality::shapes::teardrop_shape::Teardrop; use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle}; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::transformation_cage::{BoundingBoxManager, EdgeBool}; @@ -187,6 +189,18 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetInstance { } .into() }), + MenuListEntry::new("Teardrop").label("Teardrop").on_commit(move |_| { + ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(ShapeType::Teardrop), + } + .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_instance() } @@ -905,7 +919,16 @@ 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::Rectangle + | ShapeType::Ellipse + | ShapeType::Teardrop + | ShapeType::Heart => { tool_data.data.start(document, input, viewport); } ShapeType::Arrow | ShapeType::Line => { @@ -928,6 +951,8 @@ impl Fsm for ShapeToolFsmState { ShapeType::Spiral => Spiral::create_node(tool_options.spiral_type, tool_options.turns), ShapeType::Grid => Grid::create_node(tool_options.grid_type), ShapeType::Arrow => Arrow::create_node(tool_options.arrow_shaft_width, tool_options.arrow_head_width, tool_options.arrow_head_length), + ShapeType::Teardrop => Teardrop::create_node(tool_options.vertices), // FIXME + ShapeType::Heart => Heart::create_node(tool_options.vertices), // FIXME ShapeType::Line => Line::create_node(), ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Ellipse => Ellipse::create_node(), @@ -939,7 +964,16 @@ 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::Rectangle + | ShapeType::Ellipse + | ShapeType::Teardrop + | ShapeType::Heart => { defered_responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -1001,6 +1035,8 @@ impl Fsm for ShapeToolFsmState { 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::Arrow => Arrow::update_shape(document, input, viewport, layer, tool_data, modifier, responses), + ShapeType::Teardrop => Teardrop::update_shape(document, input, viewport, layer, tool_data, modifier, responses), + ShapeType::Heart => Heart::update_shape(document, input, viewport, layer, tool_data, modifier, responses), ShapeType::Line => Line::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), @@ -1253,6 +1289,16 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Teardrop"), + HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + ShapeType::Heart => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Heart"), + HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], ShapeType::Line => vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"), HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(), @@ -1284,6 +1330,8 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Heart => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Line => HintGroup(vec![ HintInfo::keys([Key::Shift], "15° Increments"), HintInfo::keys([Key::Alt], "From Center"), diff --git a/node-graph/libraries/vector-types/src/subpath/core.rs b/node-graph/libraries/vector-types/src/subpath/core.rs index 1a5a12caa0..50c6e3607b 100644 --- a/node-graph/libraries/vector-types/src/subpath/core.rs +++ b/node-graph/libraries/vector-types/src/subpath/core.rs @@ -345,6 +345,48 @@ impl Subpath { Self::from_anchors(anchors, true) } + pub fn new_teardrop(center: DVec2, radius: f64, spike_radius: f64) -> Self { + let radius = radius.abs(); + let spike_radius = spike_radius.max(0.0); + + let tip = center + DVec2::new(0.0, -spike_radius); + let left = center + DVec2::new(-radius, 0.0); + let right = center + DVec2::new(radius, 0.0); + let bottom = center + DVec2::new(0.0, radius); + + // Based on https://pomax.github.io/bezierinfo/#circles_cubic + const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; + let handle_offset = radius * HANDLE_OFFSET_FACTOR; + + let manipulator_groups = vec![ + ManipulatorGroup::new(tip, None, None), + ManipulatorGroup::new(right, Some(right - handle_offset * DVec2::Y), Some(right + handle_offset * DVec2::Y)), + ManipulatorGroup::new(bottom, Some(bottom + handle_offset * DVec2::X), Some(bottom - handle_offset * DVec2::X)), + ManipulatorGroup::new(left, Some(left + handle_offset * DVec2::Y), Some(left - handle_offset * DVec2::Y)), + ]; + + Subpath::new(manipulator_groups, true) + } + + pub fn new_heart(center: DVec2, radius: f64, spike_radius: f64) -> Self { + let radius = radius.abs(); + let spike_radius = spike_radius.max(0.0); + + let cleft = center + DVec2::new(0.0, -radius * 0.25); + let right_side = center + DVec2::new(radius, -radius * 0.25); + let bottom = center + DVec2::new(0.0, spike_radius); + let left_side = center + DVec2::new(-radius, -radius * 0.25); + + let manipulator_groups = vec![ + ManipulatorGroup::new(cleft, Some(cleft + DVec2::new(-radius * 0.2, -radius * 0.5)), Some(cleft + DVec2::new(radius * 0.2, -radius * 0.5))), + ManipulatorGroup::new(right_side, Some(right_side + DVec2::new(0.0, -radius * 0.5)), Some(right_side + DVec2::new(0.0, radius * 0.5))), + ManipulatorGroup::new(bottom, None, None), + ManipulatorGroup::new(left_side, Some(left_side + DVec2::new(0.0, radius * 0.5)), Some(left_side + DVec2::new(0.0, -radius * 0.5))), + ]; + + Subpath::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 66c8009e02..b4dca4e583 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -186,6 +186,31 @@ fn star( Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))) } +/// Generates a teardrop shape with an round body and one spike +#[node_macro::node(category("Vector: Shape"))] +fn teardrop( + _: impl Ctx, + _primary: (), + #[unit(" px")] + #[default(50.)] + radius: f64, +) -> Table { + let radius = radius.abs(); + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_teardrop(DVec2::ZERO, radius, radius * 2.0))) +} + +/// Generates a heart shape with two radii on top and one spike at the bottom +#[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, radius * 2.0))) +} #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)] From a9e66cad82578582886554f08c365db110bf0b0b Mon Sep 17 00:00:00 2001 From: LennartAppAkademie Date: Fri, 3 Apr 2026 13:51:54 +0200 Subject: [PATCH 2/5] Move HANDLE_OFFSET_FACTOR constant to consts.rs and added description --- node-graph/libraries/vector-types/src/subpath/consts.rs | 3 +++ node-graph/libraries/vector-types/src/subpath/core.rs | 6 ------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/node-graph/libraries/vector-types/src/subpath/consts.rs b/node-graph/libraries/vector-types/src/subpath/consts.rs index 300407f526..dac13da19d 100644 --- a/node-graph/libraries/vector-types/src/subpath/consts.rs +++ b/node-graph/libraries/vector-types/src/subpath/consts.rs @@ -2,3 +2,6 @@ /// Constant used to determine if `f64`s are equivalent. pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3; + +// Constant to approximate a quarter circle with a cubic Bézier curve, from https://pomax.github.io/bezierinfo/#circles_cubic +pub const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; diff --git a/node-graph/libraries/vector-types/src/subpath/core.rs b/node-graph/libraries/vector-types/src/subpath/core.rs index 50c6e3607b..0adcdcb968 100644 --- a/node-graph/libraries/vector-types/src/subpath/core.rs +++ b/node-graph/libraries/vector-types/src/subpath/core.rs @@ -180,8 +180,6 @@ impl Subpath { return vec![ManipulatorGroup::new_anchor(point1), ManipulatorGroup::new_anchor(point2)]; } - // Constant from https://pomax.github.io/bezierinfo/#circles_cubic - const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; let handle_offset = radius * HANDLE_OFFSET_FACTOR; vec![ ManipulatorGroup::new(point1, None, Some(point1 + handle_offset * (corner - point1).normalize())), @@ -209,8 +207,6 @@ impl Subpath { let left = DVec2::new(corner1.x, center.y); let right = DVec2::new(corner2.x, center.y); - // Based on https://pomax.github.io/bezierinfo/#circles_cubic - const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; let handle_offset = size * HANDLE_OFFSET_FACTOR * 0.5; let manipulator_groups = vec![ @@ -354,8 +350,6 @@ impl Subpath { let right = center + DVec2::new(radius, 0.0); let bottom = center + DVec2::new(0.0, radius); - // Based on https://pomax.github.io/bezierinfo/#circles_cubic - const HANDLE_OFFSET_FACTOR: f64 = 0.551784777779014; let handle_offset = radius * HANDLE_OFFSET_FACTOR; let manipulator_groups = vec![ From 1230d309677ab5aae2652c9d330d30a2b7f27434 Mon Sep 17 00:00:00 2001 From: LennartAppAkademie Date: Fri, 3 Apr 2026 16:16:06 +0200 Subject: [PATCH 3/5] Fix incorrect references introduced in the branch initial commit --- .../tool/common_functionality/gizmos/gizmo_manager.rs | 4 ++-- .../tool/common_functionality/graph_modification_utils.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs index b3733e0626..9226959b29 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -238,11 +238,11 @@ impl GizmoManager { return Some(ShapeGizmoHandlers::Spiral(SpiralGizmoHandler::default())); } // Teardrop - if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() { + if graph_modification_utils::get_teardrop_id(layer, &document.network_interface).is_some() { return Some(ShapeGizmoHandlers::Teardrop(TeardropGizmoHandler::default())); } // Heart - if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() { + if graph_modification_utils::get_heart_id(layer, &document.network_interface).is_some() { return Some(ShapeGizmoHandlers::Heart(HeartGizmoHandler::default())); } 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 f4d54871e1..76ef606e8b 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -378,11 +378,11 @@ pub fn get_spiral_id(layer: LayerNodeIdentifier, network_interface: &NodeNetwork } pub fn get_teardrop_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { - NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::spiral::IDENTIFIER)) + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::teardrop::IDENTIFIER)) } pub fn get_heart_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { - NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::spiral::IDENTIFIER)) + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::heart::IDENTIFIER)) } pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { From 9cfb3477d795b283bc53c5cdff4363a123f449b7 Mon Sep 17 00:00:00 2001 From: LennartAppAkademie Date: Fri, 3 Apr 2026 16:24:18 +0200 Subject: [PATCH 4/5] Add width and height to heart, tail_length to teardrop for shape manipulation --- .../shapes/heart_shape.rs | 30 +++++++++---------- .../shapes/teardrop_shape.rs | 27 ++++++++--------- .../nodes/vector/src/generator_nodes.rs | 28 ++++++++++++----- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs b/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs index 64db139258..9ffdae33e2 100644 --- a/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs @@ -11,7 +11,6 @@ use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::tool_messages::tool_prelude::*; -use core::f64; use glam::DAffine2; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; @@ -111,7 +110,8 @@ impl Heart { let node_type = resolve_document_node_type(&identifier).expect("Heart node can't be found"); node_type.node_template_input_override([ None, - Some(NodeInput::value(TaggedValue::F64(50.), false)), + Some(NodeInput::value(TaggedValue::F64(100.), false)), + Some(NodeInput::value(TaggedValue::F64(100.), false)), ]) } @@ -127,21 +127,11 @@ impl Heart { let [center, lock_ratio, _] = modifier; if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, viewport, center, lock_ratio) { - // TODO: We need to determine how to allow the polygon node to make irregular shapes update_radius_sign(end, start, layer, document, responses); let dimensions = (start - end).abs(); - - // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly - let mut scale = DVec2::ONE; - let radius: f64; - if dimensions.x > dimensions.y { - scale.x = dimensions.x / dimensions.y; - radius = dimensions.y / 2.; - } else { - scale.y = dimensions.y / dimensions.x; - radius = dimensions.x / 2.; - } + let width = dimensions.x; + let height = dimensions.y; let Some(node_id) = graph_modification_utils::get_heart_id(layer, &document.network_interface) else { return; @@ -149,12 +139,20 @@ impl Heart { responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::F64(radius), false), + input: NodeInput::value(TaggedValue::F64(width), false), }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::F64(height), false), + }); + + let center_x = (start.x + end.x) / 2.0; + let center_y = (start.y + end.y) / 2.0; + responses.add(GraphOperationMessage::TransformSet { layer, - transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.), + transform: DAffine2::from_translation(DVec2::new(center_x, center_y)), transform_in: TransformIn::Viewport, skip_rerender: false, }); diff --git a/editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs b/editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs index 156a4f4ae2..6ff9e2d87a 100644 --- a/editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs @@ -11,7 +11,6 @@ use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::tool_messages::tool_prelude::*; -use core::f64; use glam::DAffine2; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; @@ -112,6 +111,7 @@ impl Teardrop { node_type.node_template_input_override([ None, Some(NodeInput::value(TaggedValue::F64(50.), false)), + Some(NodeInput::value(TaggedValue::F64(50.), false)), ]) } @@ -127,21 +127,11 @@ impl Teardrop { let [center, lock_ratio, _] = modifier; if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, viewport, center, lock_ratio) { - // TODO: We need to determine how to allow the polygon node to make irregular shapes update_radius_sign(end, start, layer, document, responses); let dimensions = (start - end).abs(); - - // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly - let mut scale = DVec2::ONE; - let radius: f64; - if dimensions.x > dimensions.y { - scale.x = dimensions.x / dimensions.y; - radius = dimensions.y / 2.; - } else { - scale.y = dimensions.y / dimensions.x; - radius = dimensions.x / 2.; - } + let radius = dimensions.x / 2.0; + let tail_length = (dimensions.y - radius).max(0.0); let Some(node_id) = graph_modification_utils::get_teardrop_id(layer, &document.network_interface) else { return; @@ -152,9 +142,18 @@ impl Teardrop { input: NodeInput::value(TaggedValue::F64(radius), false), }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::F64(tail_length), false), + }); + + let top = start.y.min(end.y); + let center_x = (start.x + end.x) / 2.0; + let center_y = top + tail_length; + responses.add(GraphOperationMessage::TransformSet { layer, - transform: DAffine2::from_scale_angle_translation(scale, 0., (start + end) / 2.), + transform: DAffine2::from_translation(DVec2::new(center_x, center_y)), transform_in: TransformIn::Viewport, skip_rerender: false, }); diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index b4dca4e583..2734f7c88a 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -186,7 +186,7 @@ fn star( Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_star_polygon(DVec2::splat(-diameter), points, diameter, inner_diameter))) } -/// Generates a teardrop shape with an round body and one spike +/// Generates a teardrop shape with a round body and a pointed tail #[node_macro::node(category("Vector: Shape"))] fn teardrop( _: impl Ctx, @@ -194,22 +194,34 @@ fn teardrop( #[unit(" px")] #[default(50.)] radius: f64, + #[unit(" px")] + #[default(100.)] + tail_length: f64, ) -> Table { - let radius = radius.abs(); - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_teardrop(DVec2::ZERO, radius, radius * 2.0))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_teardrop( + DVec2::ZERO, + radius, + tail_length, + ))) } -/// Generates a heart shape with two radii on top and one spike at the bottom +/// Generates a heart shape with top lobes and a bottom point #[node_macro::node(category("Vector: Shape"))] fn heart( _: impl Ctx, _primary: (), #[unit(" px")] - #[default(50.)] - radius: f64, + #[default(100.)] + width: f64, + #[unit(" px")] + #[default(100.)] + height: f64, ) -> Table { - let radius = radius.abs(); - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_heart(DVec2::ZERO, radius, radius * 2.0))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_heart( + DVec2::ZERO, + width, + height, + ))) } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] From ec4ee3e098f117eb3c0c3a72063b092bf8ef9377 Mon Sep 17 00:00:00 2001 From: LennartAppAkademie Date: Fri, 3 Apr 2026 17:59:50 +0200 Subject: [PATCH 5/5] Add top_lobe_roundness to heart for independent lobe manipulation and refined the heart constrains --- .../shapes/heart_shape.rs | 1 + .../vector-types/src/subpath/core.rs | 33 +++++++++++++------ .../nodes/vector/src/generator_nodes.rs | 13 ++------ 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs b/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs index 9ffdae33e2..2e600f1717 100644 --- a/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs @@ -112,6 +112,7 @@ impl Heart { None, Some(NodeInput::value(TaggedValue::F64(100.), false)), Some(NodeInput::value(TaggedValue::F64(100.), false)), + Some(NodeInput::value(TaggedValue::F64(50.), false)), ]) } diff --git a/node-graph/libraries/vector-types/src/subpath/core.rs b/node-graph/libraries/vector-types/src/subpath/core.rs index 0adcdcb968..4848cb85bf 100644 --- a/node-graph/libraries/vector-types/src/subpath/core.rs +++ b/node-graph/libraries/vector-types/src/subpath/core.rs @@ -362,20 +362,33 @@ impl Subpath { Subpath::new(manipulator_groups, true) } - pub fn new_heart(center: DVec2, radius: f64, spike_radius: f64) -> Self { - let radius = radius.abs(); - let spike_radius = spike_radius.max(0.0); + pub fn new_heart(center: DVec2, width: f64, height: f64, top_lobe_roundness: f64) -> Self { + let w = width.abs() / 2.0; + let h = height.abs() / 2.0; + + let top_lobe_roundness = top_lobe_roundness.clamp(1.0, 100.0); + + let c = center + DVec2::new(0.0, -h * 0.5); + let bottom = center + DVec2::new(0.0, h); + let left_lobe = center + DVec2::new(-w, -h * 0.25); + let right_lobe = center + DVec2::new(w, -h * 0.25); + + let bulge = h * top_lobe_roundness / 50.0; + + let c_out = center + DVec2::new(w * 0.4, -h * 0.5 - bulge); + let c_in = center + DVec2::new(-w * 0.4, -h * 0.5 - bulge); + + let right_in = center + DVec2::new(w, -h * 0.2 - bulge); + let right_out = center + DVec2::new(w, h * 0.4); - let cleft = center + DVec2::new(0.0, -radius * 0.25); - let right_side = center + DVec2::new(radius, -radius * 0.25); - let bottom = center + DVec2::new(0.0, spike_radius); - let left_side = center + DVec2::new(-radius, -radius * 0.25); + let left_in = center + DVec2::new(-w, h * 0.4); + let left_out = center + DVec2::new(-w, -h * 0.2 - bulge); let manipulator_groups = vec![ - ManipulatorGroup::new(cleft, Some(cleft + DVec2::new(-radius * 0.2, -radius * 0.5)), Some(cleft + DVec2::new(radius * 0.2, -radius * 0.5))), - ManipulatorGroup::new(right_side, Some(right_side + DVec2::new(0.0, -radius * 0.5)), Some(right_side + DVec2::new(0.0, radius * 0.5))), + ManipulatorGroup::new(c, Some(c_in), Some(c_out)), + ManipulatorGroup::new(right_lobe, Some(right_in), Some(right_out)), ManipulatorGroup::new(bottom, None, None), - ManipulatorGroup::new(left_side, Some(left_side + DVec2::new(0.0, radius * 0.5)), Some(left_side + DVec2::new(0.0, -radius * 0.5))), + ManipulatorGroup::new(left_lobe, Some(left_in), Some(left_out)), ]; Subpath::new(manipulator_groups, true) diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 2734f7c88a..5e27d8475d 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -198,11 +198,7 @@ fn teardrop( #[default(100.)] tail_length: f64, ) -> Table { - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_teardrop( - DVec2::ZERO, - radius, - tail_length, - ))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_teardrop(DVec2::ZERO, radius, tail_length))) } /// Generates a heart shape with top lobes and a bottom point @@ -216,12 +212,9 @@ fn heart( #[unit(" px")] #[default(100.)] height: f64, + #[default(50.)] top_lobe_roundness: f64, ) -> Table { - Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_heart( - DVec2::ZERO, - width, - height, - ))) + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_heart(DVec2::ZERO, width, height, top_lobe_roundness))) } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]