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..3277dbfebb 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -8,6 +8,7 @@ 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; @@ -32,6 +33,7 @@ pub enum ShapeGizmoHandlers { Circle(CircleGizmoHandler), Grid(GridGizmoHandler), Spiral(SpiralGizmoHandler), + Heart(HeartGizmoHandler), } impl ShapeGizmoHandlers { @@ -45,6 +47,7 @@ impl ShapeGizmoHandlers { Self::Circle(_) => "circle", Self::Grid(_) => "grid", Self::Spiral(_) => "spiral", + Self::Heart(_) => "heart", Self::None => "none", } } @@ -58,6 +61,7 @@ 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::Heart(h) => h.handle_state(layer, mouse_position, document, responses), Self::None => {} } } @@ -71,6 +75,7 @@ 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::Heart(h) => h.is_any_gizmo_hovered(), Self::None => false, } } @@ -84,6 +89,7 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.handle_click(), Self::Grid(h) => h.handle_click(), Self::Spiral(h) => h.handle_click(), + Self::Heart(h) => h.handle_click(), Self::None => {} } } @@ -97,6 +103,7 @@ 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::Heart(h) => h.handle_update(drag_start, document, input, responses), Self::None => {} } } @@ -110,6 +117,7 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.cleanup(), Self::Grid(h) => h.cleanup(), Self::Spiral(h) => h.cleanup(), + Self::Heart(h) => h.cleanup(), Self::None => {} } } @@ -131,6 +139,7 @@ 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::Heart(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -151,6 +160,7 @@ 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::Heart(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -163,6 +173,7 @@ impl ShapeGizmoHandlers { Self::Circle(h) => h.mouse_cursor_icon(), Self::Grid(h) => h.mouse_cursor_icon(), Self::Spiral(h) => h.mouse_cursor_icon(), + Self::Heart(h) => h.mouse_cursor_icon(), Self::None => None, } } @@ -214,6 +225,10 @@ impl GizmoManager { if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() { return Some(ShapeGizmoHandlers::Spiral(SpiralGizmoHandler::default())); } + // Heart + if graph_modification_utils::get_heart_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 e658f7846a..c5acb35e01 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -427,6 +427,10 @@ pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::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::generator_nodes::heart::IDENTIFIER)) +} + pub fn get_grid_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::grid::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..2fc5d4fffd --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs @@ -0,0 +1,113 @@ +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::message::Message; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_proto_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::{DocumentMessageHandler, InputPreprocessorMessageHandler}; +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, 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; +use std::collections::VecDeque; + +/// Placeholder gizmo handler for the Heart shape. +/// The heart's parametric controls (cleavage, lobes, shoulder, etc.) are adjusted via the Properties panel. +#[derive(Clone, Debug, Default)] +pub struct HeartGizmoHandler; + +impl ShapeGizmoHandler for HeartGizmoHandler { + fn is_any_gizmo_hovered(&self) -> bool { + false + } + + fn handle_state(&mut self, _layer: LayerNodeIdentifier, _mouse_position: DVec2, _document: &DocumentMessageHandler, _responses: &mut VecDeque) {} + + fn handle_click(&mut self) {} + + fn handle_update(&mut self, _drag_start: DVec2, _document: &DocumentMessageHandler, _input: &InputPreprocessorMessageHandler, _responses: &mut VecDeque) {} + + fn overlays( + &self, + _document: &DocumentMessageHandler, + _selected_layer: Option, + _input: &InputPreprocessorMessageHandler, + _shape_editor: &mut &mut ShapeState, + _mouse_position: DVec2, + _overlay_context: &mut OverlayContext, + ) { + } + + fn dragging_overlays( + &self, + _document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + _shape_editor: &mut &mut ShapeState, + _mouse_position: DVec2, + _overlay_context: &mut OverlayContext, + ) { + } + + fn cleanup(&mut self) {} + + fn mouse_cursor_icon(&self) -> Option { + None + } +} + +#[derive(Default)] +pub struct Heart; + +impl Heart { + pub fn create_node() -> NodeTemplate { + let node_type = resolve_proto_node_type(graphene_std::vector::generator_nodes::heart::IDENTIFIER).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, lock_ratio, _] = modifier; + + if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, viewport, center, lock_ratio) { + let Some(node_id) = graph_modification_utils::get_heart_id(layer, &document.network_interface) else { + return; + }; + + let dimensions = (start - end).abs(); + + 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.; + } + + 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..d79f591a30 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -3,6 +3,7 @@ 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; 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 5f0d6da016..426cddbce4 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,7 @@ pub enum ShapeType { Spiral, Grid, Arrow, + Heart, Line, // KEEP THIS AT THE END Rectangle, // KEEP THIS AT THE END Ellipse, // KEEP THIS AT THE END @@ -50,6 +51,7 @@ impl ShapeType { ShapeType::Spiral, ShapeType::Grid, ShapeType::Arrow, + ShapeType::Heart, ShapeType::Line, // KEEP THIS AT THE END ShapeType::Rectangle, // KEEP THIS AT THE END ShapeType::Ellipse, // KEEP THIS AT THE END @@ -58,7 +60,10 @@ impl ShapeType { /// True if this shape mode's fill checkbox is ticked by default when nothing is selected. /// Spiral/Grid/Line are open paths and default to fill-off, the closed shapes default to fill-on. pub fn defaults_to_fill(&self) -> bool { - matches!(self, Self::Polygon | Self::Star | Self::Circle | Self::Arc | Self::Rectangle | Self::Ellipse | Self::Arrow) + matches!( + self, + Self::Polygon | Self::Star | Self::Circle | Self::Arc | Self::Rectangle | Self::Ellipse | Self::Arrow | Self::Heart + ) } pub fn name(&self) -> String { @@ -70,6 +75,7 @@ impl ShapeType { Self::Spiral => "Spiral", Self::Grid => "Grid", Self::Arrow => "Arrow", + Self::Heart => "Heart", Self::Line => "Line", // KEEP THIS AT THE END Self::Rectangle => "Rectangle", // KEEP THIS AT THE END Self::Ellipse => "Ellipse", // KEEP THIS AT THE END diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 671c989d43..475b5488a0 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -16,6 +16,7 @@ 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}; @@ -212,6 +213,12 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetInstance { } .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() } @@ -347,6 +354,7 @@ fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data: (spiral::IDENTIFIER, ShapeType::Spiral), (grid::IDENTIFIER, ShapeType::Grid), (arrow::IDENTIFIER, ShapeType::Arrow), + (heart::IDENTIFIER, ShapeType::Heart), ] .into_iter() .find_map(|(id, shape)| layer_view.upstream_node_id_from_name(&proto(id)).map(|_| shape)) else { @@ -430,7 +438,7 @@ fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data: changed = true; } } - ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Line | ShapeType::Circle => {} + ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Line | ShapeType::Circle | ShapeType::Heart => {} } changed @@ -1116,7 +1124,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::Rectangle | ShapeType::Ellipse | ShapeType::Heart => { tool_data.data.start(document, input, viewport); } ShapeType::Arrow | ShapeType::Line => { @@ -1139,6 +1147,7 @@ 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::Heart => Heart::create_node(), ShapeType::Line => Line::create_node(), ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Ellipse => Ellipse::create_node(), @@ -1150,7 +1159,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::Rectangle | ShapeType::Ellipse | ShapeType::Heart => { defered_responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -1212,6 +1221,7 @@ 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::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), @@ -1480,13 +1490,20 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque 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(), + ])], }; HintData(hint_groups) } ShapeToolFsmState::Drawing(shape) => { let mut common_hint_group = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; let tool_hint_group = match shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Polygon | ShapeType::Star | ShapeType::Arc | ShapeType::Heart => { + HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]) + } ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Spiral => HintGroup(vec![]), ShapeType::Grid => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index e3abf2005a..2ecc693d1e 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -179,6 +179,110 @@ fn regular_polygon( List::new_from_element(Vector::from_subpath(subpath::Subpath::new_regular_polygon(DVec2::splat(-radius), points, radius))) } +/// Generates a heart shape with parametric control over the cleavage, lobes, shoulders, and bottom point. +#[node_macro::node(category("Vector: Shape"))] +fn heart( + _: impl Ctx, + _primary: (), + #[unit(" px")] + #[default(50)] + radius: f64, + /// How far the top V dips below the upper bound of the heart. + #[default(0.2)] + #[range((0., 0.6))] + #[hard_min(0.)] + #[hard_max(0.6)] + cleavage_depth: f64, + /// Half-angle of the top V. Zero collapses the V into a smooth join. + #[default(45.)] + #[range((0., 89.))] + #[hard_min(0.)] + #[hard_max(89.)] + cleavage_angle: Angle, + /// Tangent length leaving the top cusp, controlling the upper roundness of each lobe. + #[default(0.55)] + #[range((0., 1.2))] + #[hard_min(0.)] + #[hard_max(1.2)] + lobe_fullness: f64, + /// Vertical position of the side anchor (positive raises the shoulder). + #[default(0.5)] + #[range((-0.5, 0.9))] + #[hard_min(-0.5)] + #[hard_max(0.9)] + shoulder_height: f64, + /// Horizontal position of the side anchor. + #[default(1.)] + #[range((0., 1.4))] + #[hard_min(0.)] + #[hard_max(1.4)] + shoulder_width: f64, + /// Rotation of the shoulder tangent from vertical. Positive leans the shoulder outward at top. + #[default(0.)] + #[range((-60., 60.))] + #[hard_min(-60.)] + #[hard_max(60.)] + shoulder_tilt: Angle, + /// Tangent length at the shoulder going up, controlling the curvature of the upper lobe side. + #[default(0.55)] + #[range((0., 1.2))] + #[hard_min(0.)] + #[hard_max(1.2)] + upper_curvature: f64, + /// Tangent length at the shoulder going down, controlling the curvature of the lower side. + #[default(1.)] + #[range((0., 1.5))] + #[hard_min(0.)] + #[hard_max(1.5)] + lower_curvature: f64, + /// Half-angle of the bottom V. Zero produces a needle-sharp point with vertical tangents. + #[default(30.)] + #[range((0., 89.))] + #[hard_min(0.)] + #[hard_max(89.)] + point_sharpness: Angle, + /// Tangent length arriving at the bottom cusp, controlling how the sides taper into the point. + #[default(0.7)] + #[range((0., 1.2))] + #[hard_min(0.)] + #[hard_max(1.2)] + taper_length: f64, +) -> List { + let cleavage_angle = cleavage_angle.to_radians(); + let point_sharpness = point_sharpness.to_radians(); + let shoulder_tilt = shoulder_tilt.to_radians(); + + // Anchor points for the right half plus the y-axis cusps, in normalized coordinates (y points downward). + let top = DVec2::new(0., -1. + cleavage_depth); + let shoulder = DVec2::new(shoulder_width, -shoulder_height); + let bottom = DVec2::new(0., 1.); + + // Unit tangent directions, all measured from the upward vertical. + let top_dir = DVec2::new(cleavage_angle.sin(), -cleavage_angle.cos()); + let bottom_dir_out = DVec2::new(point_sharpness.sin(), -point_sharpness.cos()); + let shoulder_up = DVec2::new(shoulder_tilt.sin(), -shoulder_tilt.cos()); + let shoulder_down = -shoulder_up; + + // Cubic Bezier control points for the right half. + let c1 = top + top_dir * lobe_fullness; + let c2 = shoulder + shoulder_up * upper_curvature; + let c3 = shoulder + shoulder_down * lower_curvature; + let c4 = bottom + bottom_dir_out * taper_length; + + let mirror = |p: DVec2| DVec2::new(-p.x, p.y); + + // Closed clockwise path: T → S → B → S' → T. Joins at T and B are sharp; joins at the shoulders are G1. + let manipulator_groups = [ + subpath::ManipulatorGroup::new(top * radius, Some(mirror(c1) * radius), Some(c1 * radius)), + subpath::ManipulatorGroup::new(shoulder * radius, Some(c2 * radius), Some(c3 * radius)), + subpath::ManipulatorGroup::new(bottom * radius, Some(c4 * radius), Some(mirror(c4) * radius)), + subpath::ManipulatorGroup::new(mirror(shoulder) * radius, Some(mirror(c3) * radius), Some(mirror(c2) * radius)), + ] + .to_vec(); + + List::new_from_element(Vector::from_subpath(subpath::Subpath::new(manipulator_groups, true))) +} + /// Generates an n-pointed star shape with inner and outer points at chosen radii from the center. #[node_macro::node(category("Vector: Shape"))] fn star(