From 774e085109f8b7ae8e4691ccc6777c90ac3a67f6 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 7 May 2026 01:42:22 -0700 Subject: [PATCH] Heart node --- .../nodes/vector/src/generator_nodes.rs | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index e3abf2005a..0e64e348ec 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, +) -> Table { + 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(); + + Table::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(