Skip to content

Commit d8d2a51

Browse files
0SlowPoke0Keavon
andauthored
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>
1 parent 1875779 commit d8d2a51

File tree

19 files changed

+980
-599
lines changed

19 files changed

+980
-599
lines changed

editor/src/consts.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
122122
// GIZMOS
123123
pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
124124
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
125-
pub const NUMBER_OF_POINTS_HANDLE_SPOKE_EXTENSION: f64 = 1.2;
126-
pub const NUMBER_OF_POINTS_HANDLE_SPOKE_LENGTH: f64 = 10.;
125+
pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2;
126+
pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.;
127127
pub const GIZMO_HIDE_THRESHOLD: f64 = 20.;
128128

129129
// SCROLLBARS

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,7 +1624,7 @@ impl DocumentMessageHandler {
16241624
subpath.is_inside_subpath(&viewport_polygon, None, None)
16251625
}
16261626
ClickTargetType::FreePoint(point) => {
1627-
let mut point = point.clone();
1627+
let mut point = *point;
16281628
point.apply_transform(layer_transform);
16291629
viewport_polygon.contains_point(point.position)
16301630
}
@@ -3346,9 +3346,9 @@ mod document_message_handler_tests {
33463346
let rect_bbox_after = document.metadata().bounding_box_viewport(rect_layer).unwrap();
33473347

33483348
// Verifing the rectangle maintains approximately the same position in viewport space
3349-
let before_center = (rect_bbox_before[0] + rect_bbox_before[1]) / 2.; // TODO: Should be: DVec2(0.0, -25.0), regression (#2688) causes it to be: DVec2(100.0, 25.0)
3350-
let after_center = (rect_bbox_after[0] + rect_bbox_after[1]) / 2.; // TODO: Should be: DVec2(0.0, -25.0), regression (#2688) causes it to be: DVec2(200.0, 75.0)
3351-
let distance = before_center.distance(after_center); // TODO: Should be: 0.0, regression (#2688) causes it to be: 111.80339887498948
3349+
let before_center = (rect_bbox_before[0] + rect_bbox_before[1]) / 2.; // TODO: Should be: DVec2(0., -25.), regression (#2688) causes it to be: DVec2(100., 25.)
3350+
let after_center = (rect_bbox_after[0] + rect_bbox_after[1]) / 2.; // TODO: Should be: DVec2(0., -25.), regression (#2688) causes it to be: DVec2(200., 75.)
3351+
let distance = before_center.distance(after_center); // TODO: Should be: 0., regression (#2688) causes it to be: 111.80339887498948
33523352

33533353
assert!(
33543354
distance < 1.,
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
use crate::messages::message::Message;
2+
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
3+
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
4+
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler};
5+
use crate::messages::tool::common_functionality::graph_modification_utils;
6+
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
7+
use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler;
8+
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
9+
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
10+
use glam::DVec2;
11+
use std::collections::VecDeque;
12+
13+
/// A unified enum wrapper around all available shape-specific gizmo handlers.
14+
///
15+
/// This abstraction allows `GizmoManager` to interact with different shape gizmos (like Star or Polygon)
16+
/// using a common interface without needing to know the specific shape type at compile time.
17+
///
18+
/// Each variant stores a concrete handler (e.g., `StarGizmoHandler`, `PolygonGizmoHandler`) that implements
19+
/// the shape-specific logic for rendering overlays, responding to input, and modifying shape parameters.
20+
#[derive(Clone, Debug, Default)]
21+
pub enum ShapeGizmoHandlers {
22+
#[default]
23+
None,
24+
Star(StarGizmoHandler),
25+
Polygon(PolygonGizmoHandler),
26+
}
27+
28+
impl ShapeGizmoHandlers {
29+
/// Returns the kind of shape the handler is managing, such as `"star"` or `"polygon"`.
30+
/// Used for grouping logic and distinguishing between handler types at runtime.
31+
pub fn kind(&self) -> &'static str {
32+
match self {
33+
Self::Star(_) => "star",
34+
Self::Polygon(_) => "polygon",
35+
Self::None => "none",
36+
}
37+
}
38+
39+
/// Dispatches interaction state updates to the corresponding shape-specific handler.
40+
pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
41+
match self {
42+
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
43+
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
44+
Self::None => {}
45+
}
46+
}
47+
48+
/// Checks if any interactive part of the gizmo is currently hovered.
49+
pub fn is_any_gizmo_hovered(&self) -> bool {
50+
match self {
51+
Self::Star(h) => h.is_any_gizmo_hovered(),
52+
Self::Polygon(h) => h.is_any_gizmo_hovered(),
53+
Self::None => false,
54+
}
55+
}
56+
57+
/// Passes the click interaction to the appropriate gizmo handler if one is hovered.
58+
pub fn handle_click(&mut self) {
59+
match self {
60+
Self::Star(h) => h.handle_click(),
61+
Self::Polygon(h) => h.handle_click(),
62+
Self::None => {}
63+
}
64+
}
65+
66+
/// Updates the gizmo state while the user is dragging a handle (e.g., adjusting radius).
67+
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
68+
match self {
69+
Self::Star(h) => h.handle_update(drag_start, document, input, responses),
70+
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
71+
Self::None => {}
72+
}
73+
}
74+
75+
/// Cleans up any state used by the gizmo handler.
76+
pub fn cleanup(&mut self) {
77+
match self {
78+
Self::Star(h) => h.cleanup(),
79+
Self::Polygon(h) => h.cleanup(),
80+
Self::None => {}
81+
}
82+
}
83+
84+
/// Draws overlays like control points or outlines for the shape handled by this gizmo.
85+
pub fn overlays(
86+
&self,
87+
document: &DocumentMessageHandler,
88+
layer: Option<LayerNodeIdentifier>,
89+
input: &InputPreprocessorMessageHandler,
90+
shape_editor: &mut &mut ShapeState,
91+
mouse_position: DVec2,
92+
overlay_context: &mut OverlayContext,
93+
) {
94+
match self {
95+
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
96+
Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
97+
Self::None => {}
98+
}
99+
}
100+
101+
/// Draws live-updating overlays during drag interactions for the shape handled by this gizmo.
102+
pub fn dragging_overlays(
103+
&self,
104+
document: &DocumentMessageHandler,
105+
input: &InputPreprocessorMessageHandler,
106+
shape_editor: &mut &mut ShapeState,
107+
mouse_position: DVec2,
108+
overlay_context: &mut OverlayContext,
109+
) {
110+
match self {
111+
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
112+
Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
113+
Self::None => {}
114+
}
115+
}
116+
}
117+
118+
/// Central manager that coordinates shape gizmo handlers for interactive editing on the canvas.
119+
///
120+
/// The `GizmoManager` is responsible for detecting which shapes are selected, activating the appropriate
121+
/// shape-specific gizmo, and routing user interactions (hover, click, drag) to the correct handler.
122+
/// It allows editing multiple shapes of the same type or focusing on a single active shape when a gizmo is hovered.
123+
///
124+
/// ## Responsibilities:
125+
/// - Detect which selected layers support shape gizmos (e.g., stars, polygons)
126+
/// - Activate the correct handler and manage state between frames
127+
/// - Route click, hover, and drag events to the proper shape gizmo
128+
/// - Render overlays and dragging visuals
129+
#[derive(Clone, Debug, Default)]
130+
pub struct GizmoManager {
131+
active_shape_handler: Option<ShapeGizmoHandlers>,
132+
layers_handlers: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)>,
133+
}
134+
135+
impl GizmoManager {
136+
/// Detects and returns a shape gizmo handler based on the layer type (e.g., star, polygon).
137+
///
138+
/// Returns `None` if the given layer does not represent a shape with a registered gizmo.
139+
pub fn detect_shape_handler(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<ShapeGizmoHandlers> {
140+
// Star
141+
if graph_modification_utils::get_star_id(layer, &document.network_interface).is_some() {
142+
return Some(ShapeGizmoHandlers::Star(StarGizmoHandler::default()));
143+
}
144+
145+
// Polygon
146+
if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() {
147+
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
148+
}
149+
150+
None
151+
}
152+
153+
/// Returns `true` if a gizmo is currently active (hovered or being interacted with).
154+
pub fn hovering_over_gizmo(&self) -> bool {
155+
self.active_shape_handler.is_some()
156+
}
157+
158+
/// Called every frame to check selected layers and update the active shape gizmo, if hovered.
159+
///
160+
/// Also groups all shape layers with the same kind of gizmo to support overlays for multi-shape editing.
161+
pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
162+
let mut handlers_layer: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)> = Vec::new();
163+
164+
for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) {
165+
if let Some(mut handler) = Self::detect_shape_handler(layer, document) {
166+
handler.handle_state(layer, mouse_position, document, responses);
167+
let is_hovered = handler.is_any_gizmo_hovered();
168+
169+
if is_hovered {
170+
self.layers_handlers.clear();
171+
self.active_shape_handler = Some(handler);
172+
return;
173+
}
174+
175+
// Try to group this handler with others of the same type
176+
if let Some((_, layers)) = handlers_layer.iter_mut().find(|(existing_handler, _)| existing_handler.kind() == handler.kind()) {
177+
layers.push(layer);
178+
} else {
179+
handlers_layer.push((handler, vec![layer]));
180+
}
181+
}
182+
}
183+
184+
self.layers_handlers = handlers_layer;
185+
self.active_shape_handler = None;
186+
}
187+
188+
/// Handles click interactions if a gizmo is active. Returns `true` if a gizmo handled the click.
189+
pub fn handle_click(&mut self) -> bool {
190+
if let Some(handle) = &mut self.active_shape_handler {
191+
handle.handle_click();
192+
return true;
193+
}
194+
false
195+
}
196+
197+
pub fn handle_cleanup(&mut self) {
198+
if let Some(handle) = &mut self.active_shape_handler {
199+
handle.cleanup();
200+
}
201+
}
202+
203+
/// Passes drag update data to the active gizmo to update shape parameters live.
204+
pub fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
205+
if let Some(handle) = &mut self.active_shape_handler {
206+
handle.handle_update(drag_start, document, input, responses);
207+
}
208+
}
209+
210+
/// Draws overlays for the currently active shape gizmo during a drag interaction.
211+
pub fn dragging_overlays(
212+
&self,
213+
document: &DocumentMessageHandler,
214+
input: &InputPreprocessorMessageHandler,
215+
shape_editor: &mut &mut ShapeState,
216+
mouse_position: DVec2,
217+
overlay_context: &mut OverlayContext,
218+
) {
219+
if let Some(handle) = &self.active_shape_handler {
220+
handle.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context);
221+
}
222+
}
223+
224+
/// Draws overlays for either the active gizmo (if hovered) or all grouped selected gizmos.
225+
///
226+
/// If no single gizmo is active, it renders overlays for all grouped layers with associated handlers.
227+
pub fn overlays(
228+
&self,
229+
document: &DocumentMessageHandler,
230+
input: &InputPreprocessorMessageHandler,
231+
shape_editor: &mut &mut ShapeState,
232+
mouse_position: DVec2,
233+
overlay_context: &mut OverlayContext,
234+
) {
235+
if let Some(handler) = &self.active_shape_handler {
236+
handler.overlays(document, None, input, shape_editor, mouse_position, overlay_context);
237+
return;
238+
}
239+
240+
for (handler, selected_layers) in &self.layers_handlers {
241+
for layer in selected_layers {
242+
handler.overlays(document, Some(*layer), input, shape_editor, mouse_position, overlay_context);
243+
}
244+
}
245+
}
246+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod gizmo_manager;
2+
pub mod shape_gizmos;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod number_of_points_dial;
2+
pub mod point_radius_handle;

0 commit comments

Comments
 (0)