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..9226959b29 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_teardrop_id(layer, &document.network_interface).is_some() { + return Some(ShapeGizmoHandlers::Teardrop(TeardropGizmoHandler::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 51ec764b0c..76ef606e8b 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::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::generator_nodes::heart::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..2e600f1717 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/heart_shape.rs @@ -0,0 +1,162 @@ +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 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(100.), false)), + Some(NodeInput::value(TaggedValue::F64(100.), false)), + 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) { + update_radius_sign(end, start, layer, document, responses); + + let dimensions = (start - end).abs(); + let width = dimensions.x; + let height = dimensions.y; + + 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(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_translation(DVec2::new(center_x, center_y)), + 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..6ff9e2d87a --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/teardrop_shape.rs @@ -0,0 +1,162 @@ +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 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)), + 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) { + update_radius_sign(end, start, layer, document, responses); + + let dimensions = (start - end).abs(); + 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; + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + 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_translation(DVec2::new(center_x, center_y)), + 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/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 1a5a12caa0..4848cb85bf 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![ @@ -345,6 +341,59 @@ 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); + + 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, 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 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(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_lobe, Some(left_in), Some(left_out)), + ]; + + 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..5e27d8475d 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -186,6 +186,36 @@ 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 a round body and a pointed tail +#[node_macro::node(category("Vector: Shape"))] +fn teardrop( + _: impl Ctx, + _primary: (), + #[unit(" px")] + #[default(50.)] + radius: f64, + #[unit(" px")] + #[default(100.)] + tail_length: f64, +) -> Table { + 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 +#[node_macro::node(category("Vector: Shape"))] +fn heart( + _: impl Ctx, + _primary: (), + #[unit(" px")] + #[default(100.)] + width: f64, + #[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, top_lobe_roundness))) +} #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, node_macro::ChoiceType)]