-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Feature/heart shape tool integration #4167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 4 commits
3430420
125a795
042c7fb
c782d38
4cac540
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| //! A generic, rotary dial that edits a discrete `u32` node parameter (e.g. a polygon's side count). | ||
| //! | ||
| //! Like [`GenericSliderGizmo`](super::generic_slider_gizmo::GenericSliderGizmo), this is fully | ||
| //! data-driven from the [gizmo registry]: it is anchored at the layer's origin and converts the | ||
| //! angle the user drags around that origin into integer steps. | ||
| //! | ||
| //! [gizmo registry]: crate::messages::tool::common_functionality::gizmos::gizmo_registry | ||
|
|
||
| use crate::consts::{GIZMO_HIDE_THRESHOLD, NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH}; | ||
| use crate::messages::frontend::utility_types::MouseCursorIcon; | ||
| use crate::messages::message::Message; | ||
| 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; | ||
| use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage, Responses}; | ||
| use crate::messages::tool::common_functionality::gizmos::generic_gizmos::read_u32_input; | ||
| use crate::messages::tool::common_functionality::gizmos::gizmo_registry::GizmoInfo; | ||
| use glam::DVec2; | ||
| use graph_craft::ProtoNodeIdentifier; | ||
| use graph_craft::document::NodeId; | ||
| use graph_craft::document::NodeInput; | ||
| use graph_craft::document::value::TaggedValue; | ||
| use std::collections::VecDeque; | ||
|
|
||
| /// How many degrees of rotation correspond to one integer step. | ||
| const DIAL_DEGREES_PER_STEP: f64 = 30.; | ||
| /// Viewport radius of the drawn dial indicator. | ||
| const DIAL_INDICATOR_RADIUS: f64 = NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH; | ||
|
|
||
| #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] | ||
| pub enum GenericDialState { | ||
| #[default] | ||
| Inactive, | ||
| Hover, | ||
| Dragging, | ||
| } | ||
|
|
||
| /// A rotary dial bound to one `u32` parameter of one node. | ||
| #[derive(Clone, Debug)] | ||
| pub struct GenericDialGizmo { | ||
| layer: LayerNodeIdentifier, | ||
| node_id: NodeId, | ||
| identifier: ProtoNodeIdentifier, | ||
| info: GizmoInfo, | ||
| state: GenericDialState, | ||
| /// Parameter value captured when the drag began. | ||
| initial_value: u32, | ||
| } | ||
|
|
||
| impl GenericDialGizmo { | ||
| pub fn new(layer: LayerNodeIdentifier, node_id: NodeId, identifier: ProtoNodeIdentifier, info: GizmoInfo) -> Self { | ||
| Self { | ||
| layer, | ||
| node_id, | ||
| identifier, | ||
| info, | ||
| state: GenericDialState::Inactive, | ||
| initial_value: 0, | ||
| } | ||
| } | ||
|
|
||
| pub fn is_hovered(&self) -> bool { | ||
| self.state == GenericDialState::Hover | ||
| } | ||
|
|
||
| pub fn is_dragging(&self) -> bool { | ||
| self.state == GenericDialState::Dragging | ||
| } | ||
|
|
||
| pub fn cleanup(&mut self) { | ||
| self.state = GenericDialState::Inactive; | ||
| } | ||
|
|
||
| pub fn handle_click(&mut self) { | ||
| if self.state == GenericDialState::Hover { | ||
| self.state = GenericDialState::Dragging; | ||
| } | ||
| } | ||
|
|
||
| fn current_value(&self, document: &DocumentMessageHandler) -> Option<u32> { | ||
| read_u32_input(self.layer, document, &self.identifier, self.info.parameter_index) | ||
| } | ||
|
|
||
| /// Hover detection: the dial occupies a disc of `DIAL_INDICATOR_RADIUS` around the layer origin. | ||
| pub fn handle_state(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { | ||
| if self.state == GenericDialState::Dragging { | ||
| return; | ||
| } | ||
|
|
||
| if self.current_value(document).is_none() { | ||
| self.state = GenericDialState::Inactive; | ||
| return; | ||
| } | ||
|
|
||
| let viewport = document.metadata().transform_to_viewport(self.layer); | ||
| let center = viewport.transform_point2(DVec2::ZERO); | ||
|
|
||
| // Hide the dial when the shape is too small on screen. | ||
| let extent = viewport.transform_point2(DVec2::new(1., 0.)).distance(center); | ||
| if extent < f64::EPSILON { | ||
| self.state = GenericDialState::Inactive; | ||
| return; | ||
| } | ||
|
|
||
| if mouse_position.distance(center) <= DIAL_INDICATOR_RADIUS { | ||
| if self.state != GenericDialState::Hover { | ||
| self.state = GenericDialState::Hover; | ||
| // Capture the reference value now, since `handle_click` (which starts the drag) has no | ||
| // access to the document. | ||
| self.initial_value = self.current_value(document).unwrap_or(0); | ||
| responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); | ||
| } | ||
| } else if self.state == GenericDialState::Hover { | ||
| self.state = GenericDialState::Inactive; | ||
| responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); | ||
| } | ||
| } | ||
|
|
||
| /// Convert the angle swept around the layer origin (relative to the drag start) into integer | ||
| /// steps, clamped to the registry's bounds. | ||
| pub fn handle_update(&self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { | ||
| let viewport = document.metadata().transform_to_viewport(self.layer); | ||
| let center = viewport.transform_point2(DVec2::ZERO); | ||
|
|
||
| let start_vector = drag_start - center; | ||
| let current_vector = input.mouse.position - center; | ||
| let (Some(start_dir), Some(current_dir)) = (start_vector.try_normalize(), current_vector.try_normalize()) else { | ||
| return; | ||
| }; | ||
|
|
||
| // Signed angle (radians) swept from the drag-start direction to the current direction. | ||
| let swept_degrees = start_dir.angle_to(current_dir).to_degrees(); | ||
| let steps = (swept_degrees / DIAL_DEGREES_PER_STEP).round() as i64; | ||
|
|
||
| let min = self.info.min.map(|m| m as i64).unwrap_or(0); | ||
| let max = self.info.max.map(|m| m as i64).unwrap_or(i64::MAX); | ||
| let new_value = (self.initial_value as i64 + steps).clamp(min, max) as u32; | ||
|
|
||
| responses.add(NodeGraphMessage::SetInput { | ||
| input_connector: InputConnector::node(self.node_id, self.info.parameter_index), | ||
| input: NodeInput::value(TaggedValue::U32(new_value), false), | ||
| }); | ||
| responses.add(NodeGraphMessage::RunDocumentGraph); | ||
| } | ||
|
|
||
| /// Draw the dial ring and a tick pointing toward the current mouse-derived angle. | ||
| pub fn overlays(&self, document: &DocumentMessageHandler, mouse_position: DVec2, overlay_context: &mut OverlayContext) { | ||
| if self.state == GenericDialState::Inactive { | ||
| return; | ||
| } | ||
|
|
||
| let viewport = document.metadata().transform_to_viewport(self.layer); | ||
| let center = viewport.transform_point2(DVec2::ZERO); | ||
|
|
||
| let extent = viewport.transform_point2(DVec2::new(1., 0.)).distance(center); | ||
| if extent < GIZMO_HIDE_THRESHOLD / DIAL_INDICATOR_RADIUS { | ||
| // Shape is extremely small; skip to avoid a cluttered overlay. | ||
| return; | ||
| } | ||
|
|
||
| overlay_context.circle(center, DIAL_INDICATOR_RADIUS, None, None); | ||
|
|
||
| // Tick line pointing from the center toward the mouse, giving the dial a sense of direction. | ||
| if let Some(direction) = (mouse_position - center).try_normalize() { | ||
| overlay_context.line(center, center + direction * DIAL_INDICATOR_RADIUS, None, None); | ||
| } | ||
| } | ||
|
|
||
| pub fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> { | ||
| match self.state { | ||
| GenericDialState::Hover | GenericDialState::Dragging => Some(MouseCursorIcon::EWResize), | ||
| GenericDialState::Inactive => None, | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| //! A generic, draggable handle that edits a continuous `f64` node parameter (e.g. a radius). | ||
| //! | ||
| //! Unlike the hand-written shape gizmos in `shape_gizmos`, this gizmo is fully driven by data | ||
| //! from the [gizmo registry](crate::messages::tool::common_functionality::gizmos::gizmo_registry): | ||
| //! it knows nothing about the specific node it edits beyond the node id, the parameter index, and | ||
| //! the registry's [`GizmoInfo`]. This is what lets any node opt into a slider with zero custom code. | ||
|
|
||
| use crate::consts::GIZMO_HIDE_THRESHOLD; | ||
| use crate::messages::frontend::utility_types::MouseCursorIcon; | ||
| use crate::messages::message::Message; | ||
| 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; | ||
| use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage, Responses}; | ||
| use crate::messages::tool::common_functionality::gizmos::generic_gizmos::read_f64_input; | ||
| use crate::messages::tool::common_functionality::gizmos::gizmo_registry::{GizmoInfo, PositionHint}; | ||
|
Check warning on line 16 in editor/src/messages/tool/common_functionality/gizmos/generic_gizmos/generic_slider_gizmo.rs
|
||
| use glam::DVec2; | ||
| use graph_craft::ProtoNodeIdentifier; | ||
| use graph_craft::document::NodeInput; | ||
| use graph_craft::document::value::TaggedValue; | ||
| use graph_craft::document::NodeId; | ||
| use std::collections::VecDeque; | ||
|
|
||
| /// Pixel radius within which the mouse is considered to be hovering the handle. | ||
| const SLIDER_HANDLE_HOVER_THRESHOLD: f64 = 8.; | ||
|
|
||
| #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] | ||
| pub enum GenericSliderState { | ||
| #[default] | ||
| Inactive, | ||
| Hover, | ||
| Dragging, | ||
| } | ||
|
|
||
| /// A draggable slider handle bound to one `f64` parameter of one node. | ||
| #[derive(Clone, Debug)] | ||
| pub struct GenericSliderGizmo { | ||
| layer: LayerNodeIdentifier, | ||
| node_id: NodeId, | ||
| identifier: ProtoNodeIdentifier, | ||
| info: GizmoInfo, | ||
| state: GenericSliderState, | ||
| /// The parameter value captured when the drag began, used as the clamping/anchor reference. | ||
| initial_value: f64, | ||
| } | ||
|
|
||
| impl GenericSliderGizmo { | ||
| pub fn new(layer: LayerNodeIdentifier, node_id: NodeId, identifier: ProtoNodeIdentifier, info: GizmoInfo) -> Self { | ||
| Self { | ||
| layer, | ||
| node_id, | ||
| identifier, | ||
| info, | ||
| state: GenericSliderState::Inactive, | ||
| initial_value: 0., | ||
| } | ||
| } | ||
|
|
||
| pub fn is_hovered(&self) -> bool { | ||
| self.state == GenericSliderState::Hover | ||
| } | ||
|
|
||
| pub fn is_dragging(&self) -> bool { | ||
| self.state == GenericSliderState::Dragging | ||
| } | ||
|
|
||
| pub fn cleanup(&mut self) { | ||
| self.state = GenericSliderState::Inactive; | ||
| } | ||
|
|
||
| /// Begin a drag if currently hovered. | ||
| pub fn handle_click(&mut self) { | ||
| if self.state == GenericSliderState::Hover { | ||
| self.state = GenericSliderState::Dragging; | ||
| } | ||
| } | ||
|
|
||
| fn current_value(&self, document: &DocumentMessageHandler) -> Option<f64> { | ||
| read_f64_input(self.layer, document, &self.identifier, self.info.parameter_index) | ||
| } | ||
|
|
||
| /// The handle's anchor point, in the layer's local coordinate space, derived from the current | ||
| /// parameter value and the registry's position hint. | ||
| fn handle_position_local(&self, value: f64) -> DVec2 { | ||
| match self.info.position_hint { | ||
| // A length-like parameter: place the handle that far out along the local +X axis. | ||
| PositionHint::ParameterDerived => DVec2::new(value.abs(), 0.), | ||
| // Generic fall-backs map the value onto the local +X axis as well; bounding-box-aware | ||
| // hints are refined as more node types adopt the slider. | ||
| _ => DVec2::new(value.abs(), 0.), | ||
| } | ||
| } | ||
|
|
||
| /// Detect hover by measuring the mouse's distance to the handle in viewport space. | ||
| pub fn handle_state(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { | ||
| // Never override an in-progress drag. | ||
| if self.state == GenericSliderState::Dragging { | ||
| return; | ||
| } | ||
|
|
||
| let Some(value) = self.current_value(document) else { | ||
| self.state = GenericSliderState::Inactive; | ||
| return; | ||
| }; | ||
|
|
||
| let viewport = document.metadata().transform_to_viewport(self.layer); | ||
| let center = viewport.transform_point2(DVec2::ZERO); | ||
| let handle = viewport.transform_point2(self.handle_position_local(value)); | ||
|
|
||
| // Hide the gizmo when the shape is too small on screen to interact with reliably. | ||
| if handle.distance(center) < GIZMO_HIDE_THRESHOLD { | ||
| self.state = GenericSliderState::Inactive; | ||
| return; | ||
| } | ||
|
|
||
| if mouse_position.distance(handle) <= SLIDER_HANDLE_HOVER_THRESHOLD { | ||
| if self.state != GenericSliderState::Hover { | ||
| self.state = GenericSliderState::Hover; | ||
| // Capture the reference value now, since `handle_click` (which starts the drag) has no | ||
| // access to the document. | ||
| self.initial_value = value; | ||
| responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); | ||
| } | ||
| } else if self.state == GenericSliderState::Hover { | ||
| self.state = GenericSliderState::Inactive; | ||
| responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); | ||
| } | ||
| } | ||
|
|
||
| /// Update the parameter live while dragging. The new value is the mouse's position projected | ||
| /// onto the local +X axis, clamped to the registry's min/max bounds. | ||
| pub fn handle_update(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { | ||
| let viewport = document.metadata().transform_to_viewport(self.layer); | ||
| let local_mouse = viewport.inverse().transform_point2(input.mouse.position); | ||
|
|
||
| let mut value = local_mouse.x; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Drag math in Prompt for AI agents |
||
|
|
||
| // Preserve the sign of the original value for parameters (like radius) that can be negative. | ||
| if self.initial_value.is_sign_negative() { | ||
| value = -value; | ||
| } | ||
|
|
||
| value = self.clamp(value); | ||
|
|
||
| responses.add(NodeGraphMessage::SetInput { | ||
| input_connector: InputConnector::node(self.node_id, self.info.parameter_index), | ||
| input: NodeInput::value(TaggedValue::F64(value), false), | ||
| }); | ||
| responses.add(NodeGraphMessage::RunDocumentGraph); | ||
| } | ||
|
|
||
| fn clamp(&self, value: f64) -> f64 { | ||
| let mut value = value; | ||
| if let Some(min) = self.info.min { | ||
| value = value.max(min); | ||
| } | ||
| if let Some(max) = self.info.max { | ||
| value = value.min(max); | ||
| } | ||
| value | ||
| } | ||
|
|
||
| /// Draw the handle dot, plus a guide line from the layer origin while hovered or dragging. | ||
| pub fn overlays(&self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { | ||
| if self.state == GenericSliderState::Inactive { | ||
| return; | ||
| } | ||
|
|
||
| let Some(value) = self.current_value(document) else { return }; | ||
| let viewport = document.metadata().transform_to_viewport(self.layer); | ||
| let center = viewport.transform_point2(DVec2::ZERO); | ||
| let handle = viewport.transform_point2(self.handle_position_local(value)); | ||
|
|
||
| if handle.distance(center) < GIZMO_HIDE_THRESHOLD { | ||
| return; | ||
| } | ||
|
|
||
| overlay_context.line(center, handle, None, None); | ||
| overlay_context.manipulator_handle(handle, self.state == GenericSliderState::Dragging, None); | ||
| } | ||
|
|
||
| pub fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> { | ||
| match self.state { | ||
| GenericSliderState::Hover | GenericSliderState::Dragging => Some(MouseCursorIcon::EWResize), | ||
| GenericSliderState::Inactive => None, | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.