Add Heart shape tool

- Add new_heart() method
- Add Heart node to vector generator nodes
- Add Heart to ShapeType enum and UI dropdown
This commit is contained in:
Ashish Mohapatra 2025-11-26 19:23:46 +05:30
parent 5ebf6d6bc0
commit a78ee3c52a
7 changed files with 106 additions and 7 deletions

View file

@ -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<NodeId> {
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")?;

View file

@ -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<Message>,
) {
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,
});
}
}

View file

@ -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;

View file

@ -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",

View file

@ -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<Mess
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
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::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<Mess
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Circle | ShapeType::Heart => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Spiral => HintGroup(vec![]),
};

View file

@ -317,6 +317,18 @@ impl<PointId: Identifier> Subpath<PointId> {
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;

View file

@ -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<Vector> {
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;
}