mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-25 17:48:12 +08:00
Refactor shape gizmo interactivity to support future shape tools (#2748)
* impl GizmoHandlerTrait,Gizmo-manager and add comments * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
1875779b0a
commit
d8d2a51926
@@ -0,0 +1,246 @@
|
||||
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::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::polygon_shape::PolygonGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
|
||||
use glam::DVec2;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// A unified enum wrapper around all available shape-specific gizmo handlers.
|
||||
///
|
||||
/// This abstraction allows `GizmoManager` to interact with different shape gizmos (like Star or Polygon)
|
||||
/// using a common interface without needing to know the specific shape type at compile time.
|
||||
///
|
||||
/// Each variant stores a concrete handler (e.g., `StarGizmoHandler`, `PolygonGizmoHandler`) that implements
|
||||
/// the shape-specific logic for rendering overlays, responding to input, and modifying shape parameters.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum ShapeGizmoHandlers {
|
||||
#[default]
|
||||
None,
|
||||
Star(StarGizmoHandler),
|
||||
Polygon(PolygonGizmoHandler),
|
||||
}
|
||||
|
||||
impl ShapeGizmoHandlers {
|
||||
/// Returns the kind of shape the handler is managing, such as `"star"` or `"polygon"`.
|
||||
/// Used for grouping logic and distinguishing between handler types at runtime.
|
||||
pub fn kind(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Star(_) => "star",
|
||||
Self::Polygon(_) => "polygon",
|
||||
Self::None => "none",
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches interaction state updates to the corresponding shape-specific handler.
|
||||
pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
match self {
|
||||
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
|
||||
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if any interactive part of the gizmo is currently hovered.
|
||||
pub fn is_any_gizmo_hovered(&self) -> bool {
|
||||
match self {
|
||||
Self::Star(h) => h.is_any_gizmo_hovered(),
|
||||
Self::Polygon(h) => h.is_any_gizmo_hovered(),
|
||||
Self::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Passes the click interaction to the appropriate gizmo handler if one is hovered.
|
||||
pub fn handle_click(&mut self) {
|
||||
match self {
|
||||
Self::Star(h) => h.handle_click(),
|
||||
Self::Polygon(h) => h.handle_click(),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the gizmo state while the user is dragging a handle (e.g., adjusting radius).
|
||||
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
match self {
|
||||
Self::Star(h) => h.handle_update(drag_start, document, input, responses),
|
||||
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up any state used by the gizmo handler.
|
||||
pub fn cleanup(&mut self) {
|
||||
match self {
|
||||
Self::Star(h) => h.cleanup(),
|
||||
Self::Polygon(h) => h.cleanup(),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws overlays like control points or outlines for the shape handled by this gizmo.
|
||||
pub fn overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
layer: Option<LayerNodeIdentifier>,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
match self {
|
||||
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws live-updating overlays during drag interactions for the shape handled by this gizmo.
|
||||
pub fn dragging_overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
match self {
|
||||
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Central manager that coordinates shape gizmo handlers for interactive editing on the canvas.
|
||||
///
|
||||
/// The `GizmoManager` is responsible for detecting which shapes are selected, activating the appropriate
|
||||
/// shape-specific gizmo, and routing user interactions (hover, click, drag) to the correct handler.
|
||||
/// It allows editing multiple shapes of the same type or focusing on a single active shape when a gizmo is hovered.
|
||||
///
|
||||
/// ## Responsibilities:
|
||||
/// - Detect which selected layers support shape gizmos (e.g., stars, polygons)
|
||||
/// - Activate the correct handler and manage state between frames
|
||||
/// - Route click, hover, and drag events to the proper shape gizmo
|
||||
/// - Render overlays and dragging visuals
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GizmoManager {
|
||||
active_shape_handler: Option<ShapeGizmoHandlers>,
|
||||
layers_handlers: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)>,
|
||||
}
|
||||
|
||||
impl GizmoManager {
|
||||
/// Detects and returns a shape gizmo handler based on the layer type (e.g., star, polygon).
|
||||
///
|
||||
/// Returns `None` if the given layer does not represent a shape with a registered gizmo.
|
||||
pub fn detect_shape_handler(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<ShapeGizmoHandlers> {
|
||||
// Star
|
||||
if graph_modification_utils::get_star_id(layer, &document.network_interface).is_some() {
|
||||
return Some(ShapeGizmoHandlers::Star(StarGizmoHandler::default()));
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() {
|
||||
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns `true` if a gizmo is currently active (hovered or being interacted with).
|
||||
pub fn hovering_over_gizmo(&self) -> bool {
|
||||
self.active_shape_handler.is_some()
|
||||
}
|
||||
|
||||
/// Called every frame to check selected layers and update the active shape gizmo, if hovered.
|
||||
///
|
||||
/// Also groups all shape layers with the same kind of gizmo to support overlays for multi-shape editing.
|
||||
pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let mut handlers_layer: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)> = Vec::new();
|
||||
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) {
|
||||
if let Some(mut handler) = Self::detect_shape_handler(layer, document) {
|
||||
handler.handle_state(layer, mouse_position, document, responses);
|
||||
let is_hovered = handler.is_any_gizmo_hovered();
|
||||
|
||||
if is_hovered {
|
||||
self.layers_handlers.clear();
|
||||
self.active_shape_handler = Some(handler);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to group this handler with others of the same type
|
||||
if let Some((_, layers)) = handlers_layer.iter_mut().find(|(existing_handler, _)| existing_handler.kind() == handler.kind()) {
|
||||
layers.push(layer);
|
||||
} else {
|
||||
handlers_layer.push((handler, vec![layer]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.layers_handlers = handlers_layer;
|
||||
self.active_shape_handler = None;
|
||||
}
|
||||
|
||||
/// Handles click interactions if a gizmo is active. Returns `true` if a gizmo handled the click.
|
||||
pub fn handle_click(&mut self) -> bool {
|
||||
if let Some(handle) = &mut self.active_shape_handler {
|
||||
handle.handle_click();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn handle_cleanup(&mut self) {
|
||||
if let Some(handle) = &mut self.active_shape_handler {
|
||||
handle.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/// Passes drag update data to the active gizmo to update shape parameters live.
|
||||
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if let Some(handle) = &mut self.active_shape_handler {
|
||||
handle.handle_update(drag_start, document, input, responses);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws overlays for the currently active shape gizmo during a drag interaction.
|
||||
pub fn dragging_overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
if let Some(handle) = &self.active_shape_handler {
|
||||
handle.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws overlays for either the active gizmo (if hovered) or all grouped selected gizmos.
|
||||
///
|
||||
/// If no single gizmo is active, it renders overlays for all grouped layers with associated handlers.
|
||||
pub fn overlays(
|
||||
&self,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
shape_editor: &mut &mut ShapeState,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
if let Some(handler) = &self.active_shape_handler {
|
||||
handler.overlays(document, None, input, shape_editor, mouse_position, overlay_context);
|
||||
return;
|
||||
}
|
||||
|
||||
for (handler, selected_layers) in &self.layers_handlers {
|
||||
for layer in selected_layers {
|
||||
handler.overlays(document, Some(*layer), input, shape_editor, mouse_position, overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod gizmo_manager;
|
||||
pub mod shape_gizmos;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod number_of_points_dial;
|
||||
pub mod point_radius_handle;
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
use crate::consts::{GIZMO_HIDE_THRESHOLD, NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION, NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH, POINT_RADIUS_HANDLE_SEGMENT_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::Responses;
|
||||
use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage};
|
||||
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::{extract_polygon_parameters, inside_polygon, inside_star, polygon_outline, polygon_vertex_position, star_outline};
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_star_parameters, star_vertex_position};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub enum NumberOfPointsDialState {
|
||||
#[default]
|
||||
Inactive,
|
||||
Hover,
|
||||
Dragging,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NumberOfPointsDial {
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
pub initial_points: u32,
|
||||
pub handle_state: NumberOfPointsDialState,
|
||||
}
|
||||
|
||||
impl NumberOfPointsDial {
|
||||
pub fn cleanup(&mut self) {
|
||||
self.handle_state = NumberOfPointsDialState::Inactive;
|
||||
self.layer = None;
|
||||
}
|
||||
|
||||
pub fn update_state(&mut self, state: NumberOfPointsDialState) {
|
||||
self.handle_state = state;
|
||||
}
|
||||
|
||||
pub fn is_hovering(&self) -> bool {
|
||||
self.handle_state == NumberOfPointsDialState::Hover
|
||||
}
|
||||
|
||||
pub fn is_dragging(&self) -> bool {
|
||||
self.handle_state == NumberOfPointsDialState::Dragging
|
||||
}
|
||||
|
||||
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
match &self.handle_state {
|
||||
NumberOfPointsDialState::Inactive => {
|
||||
// Star
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
let point_on_max_radius = star_vertex_position(viewport, 0, sides, radius1, radius2);
|
||||
|
||||
if mouse_position.distance(center) < NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
|
||||
self.layer = Some(layer);
|
||||
self.initial_points = sides;
|
||||
self.update_state(NumberOfPointsDialState::Hover);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
|
||||
}
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
let point_on_max_radius = polygon_vertex_position(viewport, 0, sides, radius);
|
||||
|
||||
if mouse_position.distance(center) < NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
|
||||
self.layer = Some(layer);
|
||||
self.initial_points = sides;
|
||||
self.update_state(NumberOfPointsDialState::Hover);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
|
||||
}
|
||||
}
|
||||
}
|
||||
NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
if mouse_position.distance(center) > NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && matches!(&self.handle_state, NumberOfPointsDialState::Hover) {
|
||||
self.update_state(NumberOfPointsDialState::Inactive);
|
||||
self.layer = None;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlays(&self, document: &DocumentMessageHandler, layer: Option<LayerNodeIdentifier>, shape_editor: &mut &mut ShapeState, mouse_position: DVec2, overlay_context: &mut OverlayContext) {
|
||||
match &self.handle_state {
|
||||
NumberOfPointsDialState::Inactive => {
|
||||
let Some(layer) = layer else { return };
|
||||
|
||||
// Star
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let radius = radius1.max(radius2);
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
|
||||
if closest_segment.layer() == layer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let point_on_max_radius = star_vertex_position(viewport, 0, sides, radius1, radius2);
|
||||
|
||||
if inside_star(viewport, sides, radius1, radius2, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
|
||||
self.draw_spokes(center, viewport, sides, radius, overlay_context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
if let Some(closest_segment) = shape_editor.upper_closest_segment(&document.network_interface, mouse_position, POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD) {
|
||||
if closest_segment.layer() == layer {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let point_on_max_radius = polygon_vertex_position(viewport, 0, sides, radius);
|
||||
|
||||
if inside_polygon(viewport, sides, radius, mouse_position) && point_on_max_radius.distance(center) > GIZMO_HIDE_THRESHOLD {
|
||||
self.draw_spokes(center, viewport, sides, radius, overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging => {
|
||||
let Some(layer) = self.layer else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Get the star's greater radius or polygon's radius, as well as the number of sides
|
||||
let Some((sides, radius)) = extract_star_parameters(Some(layer), document)
|
||||
.map(|(sides, r1, r2)| (sides, r1.max(r2)))
|
||||
.or_else(|| extract_polygon_parameters(Some(layer), document))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
// Draw either the star or polygon outline
|
||||
star_outline(Some(layer), document, overlay_context);
|
||||
polygon_outline(Some(layer), document, overlay_context);
|
||||
|
||||
self.draw_spokes(center, viewport, sides, radius, overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_spokes(&self, center: DVec2, viewport: DAffine2, sides: u32, radius: f64, overlay_context: &mut OverlayContext) {
|
||||
for i in 0..sides {
|
||||
let angle = ((i as f64) * TAU) / (sides as f64);
|
||||
|
||||
let point = viewport.transform_point2(DVec2 {
|
||||
x: radius * angle.sin(),
|
||||
y: -radius * angle.cos(),
|
||||
});
|
||||
|
||||
let Some(direction) = (point - center).try_normalize() else { continue };
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
let end_point = direction * NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH;
|
||||
if matches!(self.handle_state, NumberOfPointsDialState::Hover | NumberOfPointsDialState::Dragging) {
|
||||
overlay_context.line(center, end_point * NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION + center, None, None);
|
||||
} else {
|
||||
overlay_context.line(center, end_point + center, None, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
|
||||
let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start);
|
||||
let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum();
|
||||
let net_delta = (delta.length() / 25.).round() * sign;
|
||||
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_point_count = ((self.initial_points as i32) + (net_delta as i32)).max(3);
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, 1),
|
||||
input: NodeInput::value(TaggedValue::U32(new_point_count as u32), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
use crate::consts::GIZMO_HIDE_THRESHOLD;
|
||||
use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD};
|
||||
use crate::messages::frontend::utility_types::MouseCursorIcon;
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector};
|
||||
use crate::messages::prelude::FrontendMessage;
|
||||
use crate::messages::prelude::Responses;
|
||||
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{draw_snapping_ticks, extract_polygon_parameters, polygon_outline, polygon_vertex_position, star_outline};
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_star_parameters, star_vertex_position};
|
||||
use glam::DVec2;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_4, PI, SQRT_2};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub enum PointRadiusHandleState {
|
||||
#[default]
|
||||
Inactive,
|
||||
Hover,
|
||||
Dragging,
|
||||
Snapped(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct PointRadiusHandle {
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
point: u32,
|
||||
radius_index: usize,
|
||||
snap_radii: Vec<f64>,
|
||||
initial_radius: f64,
|
||||
handle_state: PointRadiusHandleState,
|
||||
}
|
||||
|
||||
impl PointRadiusHandle {
|
||||
pub fn cleanup(&mut self) {
|
||||
self.handle_state = PointRadiusHandleState::Inactive;
|
||||
self.snap_radii.clear();
|
||||
self.layer = None;
|
||||
}
|
||||
|
||||
pub fn hovered(&self) -> bool {
|
||||
self.handle_state == PointRadiusHandleState::Hover
|
||||
}
|
||||
|
||||
pub fn is_dragging_or_snapped(&self) -> bool {
|
||||
self.handle_state == PointRadiusHandleState::Dragging || matches!(self.handle_state, PointRadiusHandleState::Snapped(_))
|
||||
}
|
||||
|
||||
pub fn update_state(&mut self, state: PointRadiusHandleState) {
|
||||
self.handle_state = state;
|
||||
}
|
||||
|
||||
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
|
||||
match &self.handle_state {
|
||||
PointRadiusHandleState::Inactive => {
|
||||
// Draw the point handle gizmo for the star shape
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
for i in 0..2 * sides {
|
||||
let (radius, radius_index) = if i % 2 == 0 { (radius1, 2) } else { (radius2, 3) };
|
||||
let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
self.radius_index = radius_index;
|
||||
self.layer = Some(layer);
|
||||
self.point = i;
|
||||
self.snap_radii = Self::calculate_snap_radii(document, layer, radius_index);
|
||||
self.initial_radius = radius;
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
self.update_state(PointRadiusHandleState::Hover);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the point handle gizmo for the polygon shape
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
for i in 0..sides {
|
||||
let point = polygon_vertex_position(viewport, i as i32, sides, radius);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
self.radius_index = 2;
|
||||
self.layer = Some(layer);
|
||||
self.point = i;
|
||||
self.snap_radii.clear();
|
||||
self.initial_radius = radius;
|
||||
self.update_state(PointRadiusHandleState::Hover);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
// Star
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let point = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
|
||||
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
|
||||
self.update_state(PointRadiusHandleState::Inactive);
|
||||
self.layer = None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let point = polygon_vertex_position(viewport, self.point as i32, sides, radius);
|
||||
|
||||
if matches!(&self.handle_state, PointRadiusHandleState::Hover) && (mouse_position - point).length() > 5. {
|
||||
self.update_state(PointRadiusHandleState::Inactive);
|
||||
self.layer = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
PointRadiusHandleState::Snapped(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlays(
|
||||
&self,
|
||||
selected_star_layer: Option<LayerNodeIdentifier>,
|
||||
document: &DocumentMessageHandler,
|
||||
input: &InputPreprocessorMessageHandler,
|
||||
mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
match &self.handle_state {
|
||||
PointRadiusHandleState::Inactive => {
|
||||
let Some(layer) = selected_star_layer else { return };
|
||||
|
||||
// Draw the point handle gizmo for the star shape
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
for i in 0..(2 * sides) {
|
||||
let point = star_vertex_position(viewport, i as i32, sides, radius1, radius2);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
let viewport_diagonal = input.viewport_bounds.size().length();
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
let Some(direction) = (point - center).try_normalize() else { continue };
|
||||
|
||||
overlay_context.manipulator_handle(point, true, None);
|
||||
let angle = ((i as f64) * PI) / (sides as f64);
|
||||
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
|
||||
|
||||
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
overlay_context.manipulator_handle(point, false, None);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the point handle gizmo for the Polygon shape
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
|
||||
for i in 0..sides {
|
||||
let point = polygon_vertex_position(viewport, i as i32, sides, radius);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
let viewport_diagonal = input.viewport_bounds.size().length();
|
||||
|
||||
// If the user zooms out such that shape is very small hide the gizmo
|
||||
if point.distance(center) < GIZMO_HIDE_THRESHOLD {
|
||||
return;
|
||||
}
|
||||
|
||||
if point.distance(mouse_position) < 5. {
|
||||
let Some(direction) = (point - center).try_normalize() else { continue };
|
||||
|
||||
overlay_context.manipulator_handle(point, true, None);
|
||||
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
overlay_context.manipulator_handle(point, false, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PointRadiusHandleState::Dragging | PointRadiusHandleState::Hover => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
let viewport_diagonal = input.viewport_bounds.size().length();
|
||||
|
||||
// Star
|
||||
if let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) {
|
||||
let angle = ((self.point as f64) * PI) / (sides as f64);
|
||||
let point = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
|
||||
let Some(direction) = (point - center).try_normalize() else { return };
|
||||
|
||||
// Draws the ray from the center to the dragging point extending till the viewport
|
||||
overlay_context.manipulator_handle(point, true, None);
|
||||
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
|
||||
star_outline(Some(layer), document, overlay_context);
|
||||
|
||||
// Make the ticks for snapping
|
||||
|
||||
// If dragging to make radius negative don't show the
|
||||
if (mouse_position - center).dot(direction) < 0. {
|
||||
return;
|
||||
}
|
||||
draw_snapping_ticks(&self.snap_radii, direction, viewport, angle, overlay_context);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Polygon
|
||||
if let Some((sides, radius)) = extract_polygon_parameters(Some(layer), document) {
|
||||
let point = polygon_vertex_position(viewport, self.point as i32, sides, radius);
|
||||
|
||||
let Some(direction) = (point - center).try_normalize() else { return };
|
||||
|
||||
// Draws the ray from the center to the dragging point extending till the viewport
|
||||
overlay_context.manipulator_handle(point, true, None);
|
||||
overlay_context.line(center, center + direction * viewport_diagonal, None, None);
|
||||
|
||||
polygon_outline(Some(layer), document, overlay_context);
|
||||
}
|
||||
}
|
||||
PointRadiusHandleState::Snapped(snapping_index) => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some((sides, radius1, radius2)) = extract_star_parameters(Some(layer), document) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
match snapping_index {
|
||||
// Make a triangle with previous two points
|
||||
0 => {
|
||||
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 2, sides, radius1, radius2);
|
||||
let outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
|
||||
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
|
||||
overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
|
||||
let l1 = (before_outer_position - outer_position).length() * 0.2;
|
||||
let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return };
|
||||
let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return };
|
||||
let Some(direction) = (center - outer_position).try_normalize() else { return };
|
||||
|
||||
let new_point = SQRT_2 * l1 * direction + outer_position;
|
||||
|
||||
let before_outer_position = l1 * l1_direction + outer_position;
|
||||
let point_position = l1 * l2_direction + outer_position;
|
||||
|
||||
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
}
|
||||
1 => {
|
||||
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
|
||||
|
||||
let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, sides, radius1, radius2);
|
||||
|
||||
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
|
||||
overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
|
||||
let l1 = (before_outer_position - point_position).length() * 0.2;
|
||||
let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return };
|
||||
let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return };
|
||||
let Some(direction) = (center - point_position).try_normalize() else { return };
|
||||
|
||||
let new_point = SQRT_2 * l1 * direction + point_position;
|
||||
|
||||
let before_outer_position = l1 * l1_direction + point_position;
|
||||
let after_point_position = l1 * l2_direction + point_position;
|
||||
|
||||
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
}
|
||||
i => {
|
||||
// Use `self.point` as absolute reference as it matches the index of vertices of the star starting from 0
|
||||
if i % 2 != 0 {
|
||||
// Flipped case
|
||||
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
|
||||
let target_index = (1 - (*i as i32)).abs() + (self.point as i32);
|
||||
let target_point_position = star_vertex_position(viewport, target_index, sides, radius1, radius2);
|
||||
|
||||
let mirrored_index = 2 * (self.point as i32) - target_index;
|
||||
let mirrored = star_vertex_position(viewport, mirrored_index, sides, radius1, radius2);
|
||||
|
||||
overlay_context.line(point_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(point_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
} else {
|
||||
let outer_index = (self.point as i32) - 1;
|
||||
let outer_position = star_vertex_position(viewport, outer_index, sides, radius1, radius2);
|
||||
|
||||
// The vertex which is colinear with the point we are dragging and its previous outer vertex
|
||||
let target_index = (self.point as i32) + (*i as i32) - 1;
|
||||
let target_point_position = star_vertex_position(viewport, target_index, sides, radius1, radius2);
|
||||
|
||||
let mirrored_index = 2 * outer_index - target_index;
|
||||
|
||||
let mirrored = star_vertex_position(viewport, mirrored_index, sides, radius1, radius2);
|
||||
|
||||
overlay_context.line(outer_position, target_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(outer_position, mirrored, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
star_outline(Some(layer), document, overlay_context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_snap_radii(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, radius_index: usize) -> Vec<f64> {
|
||||
let mut snap_radii = Vec::new();
|
||||
|
||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star") else {
|
||||
return snap_radii;
|
||||
};
|
||||
|
||||
let other_index = if radius_index == 3 { 2 } else { 3 };
|
||||
|
||||
let Some(&TaggedValue::F64(other_radius)) = node_inputs[other_index].as_value() else {
|
||||
return snap_radii;
|
||||
};
|
||||
let Some(&TaggedValue::U32(sides)) = node_inputs[1].as_value() else {
|
||||
return snap_radii;
|
||||
};
|
||||
|
||||
// Inner radius for 90°
|
||||
let b = FRAC_PI_4 * 3. - PI / (sides as f64);
|
||||
let angle = b.sin();
|
||||
let required_radius = (other_radius / angle) * FRAC_1_SQRT_2;
|
||||
|
||||
snap_radii.push(required_radius);
|
||||
|
||||
// Also push the case when the when it length increases more than the other
|
||||
|
||||
let flipped = other_radius * angle * SQRT_2;
|
||||
|
||||
snap_radii.push(flipped);
|
||||
|
||||
for i in 1..sides {
|
||||
let sides = sides as f64;
|
||||
let i = i as f64;
|
||||
let denominator = 2. * ((PI * (i - 1.)) / sides).cos() * ((PI * i) / sides).sin();
|
||||
let numerator = ((2. * PI * i) / sides).sin();
|
||||
let factor = numerator / denominator;
|
||||
|
||||
if factor < 0. {
|
||||
break;
|
||||
}
|
||||
|
||||
if other_radius * factor > 1e-6 {
|
||||
snap_radii.push(other_radius * factor);
|
||||
}
|
||||
|
||||
snap_radii.push((other_radius * 1.) / factor);
|
||||
}
|
||||
|
||||
snap_radii
|
||||
}
|
||||
|
||||
fn check_snapping(&self, new_radius: f64, original_radius: f64) -> Option<(usize, f64)> {
|
||||
self.snap_radii
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, rad)| (**rad - new_radius).abs() < POINT_RADIUS_HANDLE_SNAP_THRESHOLD)
|
||||
.min_by(|(i_a, a), (i_b, b)| {
|
||||
let dist_a = (**a - new_radius).abs();
|
||||
let dist_b = (**b - new_radius).abs();
|
||||
|
||||
// Check if either index is 0 or 1 and prioritize them
|
||||
match (*i_a == 0 || *i_a == 1, *i_b == 0 || *i_b == 1) {
|
||||
// `a` is priority index, `b` is not
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
// `b` is priority index, `a` is not
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
// Normal comparison
|
||||
_ => dist_a.partial_cmp(&dist_b).unwrap_or(std::cmp::Ordering::Equal),
|
||||
}
|
||||
})
|
||||
.map(|(i, rad)| (i, *rad - original_radius))
|
||||
}
|
||||
|
||||
pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
|
||||
let Some(layer) = self.layer else { return };
|
||||
|
||||
let Some(node_id) = graph_modification_utils::get_star_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer);
|
||||
let document_transform = document.network_interface.document_metadata().transform_to_document(layer);
|
||||
let center = viewport_transform.transform_point2(DVec2::ZERO);
|
||||
let radius_index = self.radius_index;
|
||||
|
||||
let original_radius = self.initial_radius;
|
||||
|
||||
let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - document_transform.inverse().transform_point2(drag_start);
|
||||
let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center;
|
||||
let projection = delta.project_onto(radius);
|
||||
let sign = radius.dot(delta).signum();
|
||||
|
||||
let mut net_delta = projection.length() * sign;
|
||||
let new_radius = original_radius + net_delta;
|
||||
|
||||
self.update_state(PointRadiusHandleState::Dragging);
|
||||
if let Some((index, snapped_delta)) = self.check_snapping(new_radius, original_radius) {
|
||||
net_delta = snapped_delta;
|
||||
self.update_state(PointRadiusHandleState::Snapped(index));
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::SetInput {
|
||||
input_connector: InputConnector::node(node_id, radius_index),
|
||||
input: NodeInput::value(TaggedValue::F64(original_radius + net_delta), false),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user