Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1764,9 +1764,17 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphMessageContext<'a>> for NodeG
let is_fill_node = reference.as_ref().is_some_and(|r| *r == DefinitionIdentifier::ProtoNode(graphene_std::vector::fill::IDENTIFIER));
let is_fill_input = is_fill_node && input_index == graphene_std::vector::fill::PaintInput::INDEX;
let is_shape_generator_node = reference.as_ref().is_some_and(|r| {
[regular_polygon::IDENTIFIER, star::IDENTIFIER, arc::IDENTIFIER, spiral::IDENTIFIER, grid::IDENTIFIER, arrow::IDENTIFIER]
.into_iter()
.any(|id| *r == DefinitionIdentifier::ProtoNode(id))
[
regular_polygon::IDENTIFIER,
star::IDENTIFIER,
arc::IDENTIFIER,
spiral::IDENTIFIER,
grid::IDENTIFIER,
arrow::IDENTIFIER,
teardrop::IDENTIFIER,
]
.into_iter()
.any(|id| *r == DefinitionIdentifier::ProtoNode(id))
});

let input = NodeInput::value(*value, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,10 @@ 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<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::teardrop::IDENTIFIER))
}

pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name(&DefinitionIdentifier::ProtoNode(graphene_std::text::text::IDENTIFIER))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod rectangle_shape;
pub mod shape_utility;
pub mod spiral_shape;
pub mod star_shape;
pub mod teardrop_shape;

pub use super::resize::{viewport_zoom, window_aligned_transform_set};
pub use super::shapes::arrow_shape::Arrow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub enum ShapeType {
Circle,
Arc,
Spiral,
Teardrop,
Grid,
Arrow,
Line, // KEEP THIS AT THE END
Expand All @@ -47,6 +48,7 @@ impl ShapeType {
ShapeType::Circle,
ShapeType::Arc,
ShapeType::Spiral,
ShapeType::Teardrop,
ShapeType::Grid,
ShapeType::Arrow,
ShapeType::Line, // KEEP THIS AT THE END
Expand All @@ -57,7 +59,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::Teardrop
)
}

pub fn name(&self) -> String {
Expand All @@ -67,6 +72,7 @@ impl ShapeType {
Self::Circle => "Circle",
Self::Arc => "Arc",
Self::Spiral => "Spiral",
Self::Teardrop => "Teardrop",
Self::Grid => "Grid",
Self::Arrow => "Arrow",
Self::Line => "Line", // KEEP THIS AT THE END
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use super::shape_utility::ShapeToolModifierKey;
use super::*;
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_proto_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::tool_messages::tool_prelude::*;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;

#[derive(Default)]
pub struct Teardrop;

impl Teardrop {
pub fn create_node() -> NodeTemplate {
let node_type = resolve_proto_node_type(graphene_std::vector::generator_nodes::teardrop::IDENTIFIER).expect("Teardrop node can't be found");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When the Teardrop node is not registered, create_node panics while starting a draw and brings down the editor. Handle the missing definition locally or propagate the failure instead of calling expect.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs, line 17:

<comment>When the Teardrop node is not registered, `create_node` panics while starting a draw and brings down the editor. Handle the missing definition locally or propagate the failure instead of calling `expect`.</comment>

<file context>
@@ -0,0 +1,50 @@
+
+impl Teardrop {
+	pub fn create_node() -> NodeTemplate {
+		let node_type = resolve_proto_node_type(graphene_std::vector::generator_nodes::teardrop::IDENTIFIER).expect("Teardrop node can't be found");
+		node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), false))])
+	}
</file context>

node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.5), false)), Some(NodeInput::value(TaggedValue::F64(0.5), 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, 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_teardrop_id(layer, &document.network_interface) else {
return;
};

let radius = ((start - end) / 2. / viewport_zoom(document)).abs();

responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::teardrop::WidthInput),
input: NodeInput::value(TaggedValue::F64(radius.x), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, graphene_std::vector::generator_nodes::teardrop::HeightInput),
input: NodeInput::value(TaggedValue::F64(radius.y), false),
});
responses.add(window_aligned_transform_set(document, layer, start.midpoint(end), DVec2::ONE));
}
}
}
43 changes: 38 additions & 5 deletions editor/src/messages/tool/tool_messages/shape_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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::stroke_options::{StrokeOptionsUpdate, apply_stroke_option, create_stroke_options_popover_widget};
Expand Down Expand Up @@ -200,6 +201,12 @@ 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("Grid").label("Grid").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Grid),
Expand Down Expand Up @@ -323,6 +330,7 @@ fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data:
(circle::IDENTIFIER, ShapeType::Circle),
(arc::IDENTIFIER, ShapeType::Arc),
(spiral::IDENTIFIER, ShapeType::Spiral),
(teardrop::IDENTIFIER, ShapeType::Teardrop),
(grid::IDENTIFIER, ShapeType::Grid),
(arrow::IDENTIFIER, ShapeType::Arrow),
]
Expand All @@ -340,7 +348,7 @@ fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data:
}

// Only the shapes whose control bar exposes per-shape parameters need a sync below.
// The rest (Ellipse, Rectangle, Line) just keep `shape_type` in step and rely on the shared Stroke/Fill controls.
// The rest (Teardrop, Ellipse, Rectangle, Line) just keep `shape_type` in step and rely on the shared Stroke/Fill controls.
match shape_type {
ShapeType::Polygon | ShapeType::Star => {
// Both `regular_polygon` and `star` are generic over `T: AsU64`, but the control bar widget always writes `u32`,
Expand Down Expand Up @@ -407,7 +415,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::Teardrop => {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Shape tool does not expose the teardrop's velocity parameter. Add a velocity control, tool state/update variant, and selection synchronization instead of treating Teardrop as a parameterless shape.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/tool/tool_messages/shape_tool.rs, line 418:

<comment>The Shape tool does not expose the teardrop's `velocity` parameter. Add a velocity control, tool state/update variant, and selection synchronization instead of treating Teardrop as a parameterless shape.</comment>

<file context>
@@ -407,7 +415,7 @@ fn sync_shape_options_from_selection(options: &mut ShapeToolOptions, tool_data:
 			}
 		}
-		ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Line | ShapeType::Circle => {}
+		ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Line | ShapeType::Circle | ShapeType::Teardrop => {}
 	}
 
</file context>

}

changed
Expand Down Expand Up @@ -1088,7 +1096,15 @@ 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::Teardrop
| ShapeType::Grid
| ShapeType::Rectangle
| ShapeType::Ellipse => {
tool_data.data.start(document, input, viewport);
}
ShapeType::Arrow | ShapeType::Line => {
Expand All @@ -1109,6 +1125,7 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Circle => Circle::create_node(),
ShapeType::Arc => Arc::create_node(tool_options.arc_type),
ShapeType::Spiral => Spiral::create_node(tool_options.spiral_type, tool_options.turns),
ShapeType::Teardrop => Teardrop::create_node(),
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::Line => Line::create_node(),
Expand All @@ -1122,7 +1139,15 @@ 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::Teardrop
| ShapeType::Grid
| ShapeType::Rectangle
| ShapeType::Ellipse => {
defered_responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
Expand Down Expand Up @@ -1184,6 +1209,7 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Circle => Circle::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
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::Teardrop => Teardrop::update_shape(document, input, viewport, layer, tool_data, modifier, 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::Line => Line::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
Expand Down Expand Up @@ -1427,6 +1453,11 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Spiral")]),
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Turns")]),
],
ShapeType::Teardrop => 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::Grid => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Grid"),
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
Expand Down Expand Up @@ -1460,7 +1491,9 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
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::Teardrop => {
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")]),
Expand Down
35 changes: 35 additions & 0 deletions node-graph/libraries/vector-types/src/vector/algorithms/shapes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,41 @@ pub fn spiral_bezpath(a: f64, outer_radius: f64, turns: f64, start_angle: f64, d
bezpath_from_anchors(&anchors, false)
}

/// Constructs a teardrop with `corner1` and `corner2` as the two corners of the bounding box.
pub fn teardrop_bezpath(corner1: DVec2, corner2: DVec2, velocity: f64) -> BezPath {
let size = (corner1 - corner2).abs();

// the bottom half of the teardrop is a circle, upon which these calculations are heavily based
let circle_center = DVec2::new((corner1.x + corner2.x) / 2., (corner1.y + (2. * velocity - 1.) * corner2.y) / (2. * velocity));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When velocity is zero or negative, this function divides by it in both circle_center and vertical_handle_offset, emitting non-finite path coordinates. The node declares only a soft range, so typed or graph-provided values are not hard-clamped; validate a finite positive velocity before these calculations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/libraries/vector-types/src/vector/algorithms/shapes.rs, line 268:

<comment>When `velocity` is zero or negative, this function divides by it in both `circle_center` and `vertical_handle_offset`, emitting non-finite path coordinates. The node declares only a soft range, so typed or graph-provided values are not hard-clamped; validate a finite positive velocity before these calculations.</comment>

<file context>
@@ -260,6 +260,41 @@ pub fn spiral_bezpath(a: f64, outer_radius: f64, turns: f64, start_angle: f64, d
+	let size = (corner1 - corner2).abs();
+
+	// the bottom half of the teardrop is a circle, upon which these calculations are heavily based
+	let circle_center = DVec2::new((corner1.x + corner2.x) / 2., (corner1.y + (2. * velocity - 1.) * corner2.y) / (2. * velocity));
+
+	let top = DVec2::new(circle_center.x, corner1.y);
</file context>
Suggested change
let circle_center = DVec2::new((corner1.x + corner2.x) / 2., (corner1.y + (2. * velocity - 1.) * corner2.y) / (2. * velocity));
\tlet velocity = if velocity.is_finite() && velocity > 0. { velocity } else { 1.7 };\n\tlet circle_center = DVec2::new((corner1.x + corner2.x) / 2., (corner1.y + (2. * velocity - 1.) * corner2.y) / (2. * velocity));


let top = DVec2::new(circle_center.x, corner1.y);
let bottom = DVec2::new(circle_center.x, corner2.y);
let left = DVec2::new(corner1.x, circle_center.y);
let right = DVec2::new(corner2.x, circle_center.y);

// because we modify the dimensions vertically, the handle_offset remains the same as
// for a circle *horizontally*, but the vertical handle_offset must be adjusted
let horizontal_handle_offset = size * HANDLE_OFFSET_FACTOR * 0.5;
let vertical_handle_offset = horizontal_handle_offset / velocity;

// I've found that the teardrop looks better when its sides go up a little steeper than they go down
let roundness_multiplier = 1.6;
let roundness = vertical_handle_offset * roundness_multiplier;

// both handles for the top point of the teardrop, to make it pointier
let point_handle_multiplier = 0.28;
let point_handles = Some(top + size * point_handle_multiplier * DVec2::Y);

let anchors = [
Anchor::new(top, point_handles, point_handles),
Anchor::new(right, Some(right - roundness * DVec2::Y), Some(right + vertical_handle_offset * DVec2::Y)),
Anchor::new(bottom, Some(bottom + horizontal_handle_offset * DVec2::X), Some(bottom - horizontal_handle_offset * DVec2::X)),
Anchor::new(left, Some(left + vertical_handle_offset * DVec2::Y), Some(left - roundness * DVec2::Y)),
];

bezpath_from_anchors(&anchors, true)
}

pub fn calculate_growth_factor(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 {
match spiral_type {
SpiralType::Archimedean => {
Expand Down
33 changes: 33 additions & 0 deletions node-graph/nodes/vector/src/generator_nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,39 @@ fn spiral(
)))
}

/// Generates a teardrop shape using the given dimensions
#[node_macro::node(category("Vector: Shape"))]
fn teardrop(
_: impl Ctx,
_primary: (),
#[unit(" px")]
#[default(30)]
width: Item<f64>,
#[unit(" px")]
#[default(51)]
height: Item<f64>,
#[default(1.7)]
#[range]
#[soft(1.4..3.8)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a node input supplies velocity at or below zero, the soft range does not prevent invalid geometry: teardrop_bezpath divides by this value. Add a hard lower bound such as #[hard(1..)] before the soft range.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At node-graph/nodes/vector/src/generator_nodes.rs, line 93:

<comment>When a node input supplies `velocity` at or below zero, the soft range does not prevent invalid geometry: `teardrop_bezpath` divides by this value. Add a hard lower bound such as `#[hard(1..)]` before the soft range.</comment>

<file context>
@@ -77,6 +77,39 @@ fn spiral(
+	height: Item<f64>,
+	#[default(1.7)]
+	#[range]
+	#[soft(1.4..3.8)]
+	velocity: Item<f64>,
+) -> Item<Vector> {
</file context>
Suggested change
#[soft(1.4..3.8)]
#[hard(1..)]
#[soft(1.4..3.8)]

velocity: Item<f64>,
) -> Item<Vector> {
let radius = DVec2::new(*width.element(), *height.element());
let corner1 = -radius;
let corner2 = radius;
let velocity = *velocity.element();

let mut teardrop = Vector::from_bezpath(shapes::teardrop_bezpath(corner1, corner2, velocity));

let len = teardrop.segment_domain.ids().len();
for i in 0..len - 1 {
teardrop
.colinear_manipulators
.push([HandleId::end(teardrop.segment_domain.ids()[i]), HandleId::primary(teardrop.segment_domain.ids()[(i + 1) % len])]);
}

Item::new_from_element(teardrop)
}

/// Generates an ellipse shape (an oval or stretched circle) with the chosen radii.
#[node_macro::node(category("Vector: Shape"))]
fn ellipse(
Expand Down