From 390febb86f6ceebf1e74bce68cebd980a0f7dc53 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Thu, 15 Jan 2026 23:38:54 +0000 Subject: [PATCH 01/26] Added guide lines feature with ruler drag interaction --- editor/src/dispatcher.rs | 2 + .../src/messages/frontend/frontend_message.rs | 8 ++ .../menu_bar/menu_bar_message_handler.rs | 6 + .../document/document_message_handler.rs | 17 +++ .../document/overlays/guide_overlays.rs | 30 ++++ .../portfolio/document/overlays/mod.rs | 1 + .../overlays/overlays_message_handler.rs | 8 ++ .../portfolio/document/utility_types/guide.rs | 61 ++++++++ .../portfolio/document/utility_types/misc.rs | 22 +++ .../portfolio/document/utility_types/mod.rs | 1 + .../tool/common_functionality/snapping.rs | 11 +- .../snapping/guide_snapper.rs | 130 ++++++++++++++++++ .../tool/tool_messages/select_tool.rs | 95 +++++++++++++ .../src/components/panels/Document.svelte | 104 ++++++++++++++ .../widgets/inputs/RulerInput.svelte | 47 ++++++- 15 files changed, 540 insertions(+), 3 deletions(-) create mode 100644 editor/src/messages/portfolio/document/overlays/guide_overlays.rs create mode 100644 editor/src/messages/portfolio/document/utility_types/guide.rs create mode 100644 editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index f2c665648d..a995742ddf 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -287,6 +287,7 @@ impl Dispatcher { menu_bar_message_handler.canvas_tilted = document.document_ptz.tilt() != 0.; menu_bar_message_handler.canvas_flipped = document.document_ptz.flip; menu_bar_message_handler.rulers_visible = document.rulers_visible; + menu_bar_message_handler.guides_visible = document.guides_visible; menu_bar_message_handler.node_graph_open = document.is_graph_overlay_open(); menu_bar_message_handler.has_selected_nodes = selected_nodes.selected_nodes().next().is_some(); menu_bar_message_handler.has_selected_layers = selected_nodes.selected_visible_layers(&document.network_interface).next().is_some(); @@ -297,6 +298,7 @@ impl Dispatcher { menu_bar_message_handler.canvas_tilted = false; menu_bar_message_handler.canvas_flipped = false; menu_bar_message_handler.rulers_visible = false; + menu_bar_message_handler.guides_visible = false; menu_bar_message_handler.node_graph_open = false; menu_bar_message_handler.has_selected_nodes = false; menu_bar_message_handler.has_selected_layers = false; diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index e227c6821a..6258a83b48 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -329,6 +329,14 @@ pub enum FrontendMessage { UpdateUIScale { scale: f64, }, + UpdateGuidesData { + #[serde(rename = "horizontalGuides")] + horizontal_guides: Vec<(u64, f64)>, + #[serde(rename = "verticalGuides")] + vertical_guides: Vec<(u64, f64)>, + #[serde(rename = "documentToViewport")] + document_to_viewport: [f64; 6], + }, #[cfg(not(target_family = "wasm"))] RenderOverlays { diff --git a/editor/src/messages/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/menu_bar/menu_bar_message_handler.rs index 43946b2b7d..de0b18e533 100644 --- a/editor/src/messages/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/menu_bar/menu_bar_message_handler.rs @@ -11,6 +11,7 @@ pub struct MenuBarMessageHandler { pub canvas_tilted: bool, pub canvas_flipped: bool, pub rulers_visible: bool, + pub guides_visible: bool, pub node_graph_open: bool, pub has_selected_nodes: bool, pub has_selected_layers: bool, @@ -635,6 +636,11 @@ impl LayoutHolder for MenuBarMessageHandler { .tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleRulers)) .on_commit(|_| PortfolioMessage::ToggleRulers.into()) .disabled(no_active_document), + MenuListEntry::new("Guides") + .label("Guides") + .icon(if self.guides_visible { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .on_commit(|_| DocumentMessage::ToggleGuidesVisibility.into()) + .disabled(no_active_document), ], ]) .widget_instance(), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 1bc20e191c..6dbe0dd45f 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2,6 +2,7 @@ use super::DocumentHistory; use super::document_diff::diff_networks; use super::node_graph::document_node_definitions; use super::utility_types::error::EditorError; +use super::utility_types::guide::Guide; use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS, SnappingOptions, SnappingState}; use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, LayerStructureEntry, SelectedNodes}; @@ -156,6 +157,15 @@ pub struct DocumentMessageHandler { /// If the user clicks or Ctrl-clicks one layer, it becomes the start of the range selection and then Shift-clicking another layer selects all layers between the start and end. #[serde(skip)] layer_range_selection_reference: Option, + /// List of horizontal guide lines in document space. + #[serde(default)] + pub horizontal_guides: Vec, + /// List of vertical guide lines in document space. + #[serde(default)] + pub vertical_guides: Vec, + /// Whether guide lines are visible in the viewport. + #[serde(default = "default_guides_visible")] + pub guides_visible: bool, /// Whether or not the editor has executed the network to render the document yet. If this is opened as an inactive tab, it won't be loaded initially because the active tab is prioritized. #[serde(skip)] pub is_loaded: bool, @@ -200,6 +210,9 @@ impl Default for DocumentMessageHandler { saved_hash: None, auto_saved_hash: None, layer_range_selection_reference: None, + horizontal_guides: Vec::new(), + vertical_guides: Vec::new(), + guides_visible: true, is_loaded: false, } } @@ -3789,6 +3802,10 @@ fn default_document_network_interface() -> NodeNetworkInterface { network_interface } +fn default_guides_visible() -> bool { + true +} + /// Targets for the [`ClickXRayIter`]. In order to reduce computation, we prefer just a point/path test where possible. #[derive(Clone)] enum XRayTarget { diff --git a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs new file mode 100644 index 0000000000..2b3a72385c --- /dev/null +++ b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs @@ -0,0 +1,30 @@ +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::prelude::DocumentMessageHandler; +use glam::DVec2; +use graphene_std::renderer::Quad; + +const GUIDE_COLOR: &str = "#00BFFF"; + +pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { + let document_to_viewport = document + .navigation_handler + .calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz); + + let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.viewport.size().into()]); + + let [min, max] = bounds.bounding_box(); + let (min_x, max_x) = (min.x, max.x); + let (min_y, max_y) = (min.y, max.y); + + for guide in &document.horizontal_guides { + let start = DVec2::new(min_x, guide.position); + let end = DVec2::new(max_x, guide.position); + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(GUIDE_COLOR), None); + } + + for guide in &document.vertical_guides { + let start = DVec2::new(guide.position, min_y); + let end = DVec2::new(guide.position, max_y); + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(GUIDE_COLOR), None); + } +} diff --git a/editor/src/messages/portfolio/document/overlays/mod.rs b/editor/src/messages/portfolio/document/overlays/mod.rs index 514ccd7b63..6961cfee97 100644 --- a/editor/src/messages/portfolio/document/overlays/mod.rs +++ b/editor/src/messages/portfolio/document/overlays/mod.rs @@ -1,4 +1,5 @@ pub mod grid_overlays; +pub mod guide_overlays; mod overlays_message; mod overlays_message_handler; pub mod utility_functions; diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index c7acb50c59..aa5887aa8e 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -63,6 +63,13 @@ impl MessageHandler> for OverlaysMes viewport: *viewport, }, }); + responses.add(DocumentMessage::GuideOverlays { + context: OverlayContext { + render_context: canvas_context.clone(), + visibility_settings: visibility_settings.clone(), + viewport: *viewport, + }, + }); for provider in &self.overlay_providers { responses.add(provider(OverlayContext { render_context: canvas_context.clone(), @@ -80,6 +87,7 @@ impl MessageHandler> for OverlaysMes if visibility_settings.all() { responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() }); + responses.add(DocumentMessage::GuideOverlays { context: overlay_context.clone() }); for provider in &self.overlay_providers { responses.add(provider(overlay_context.clone())); diff --git a/editor/src/messages/portfolio/document/utility_types/guide.rs b/editor/src/messages/portfolio/document/utility_types/guide.rs new file mode 100644 index 0000000000..e19ddaf69e --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/guide.rs @@ -0,0 +1,61 @@ +use crate::application::generate_uuid; + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] +pub struct GuideId(u64); + +impl GuideId { + pub fn new() -> Self { + Self(generate_uuid()) + } + + pub fn from_raw(id: u64) -> Self { + Self(id) + } + + pub fn as_raw(&self) -> u64 { + self.0 + } +} + +impl Default for GuideId { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub enum GuideDirection { + Horizontal, + Vertical, +} + +#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct Guide { + pub id: GuideId, + pub direction: GuideDirection, + /// Position in document space (Y coordinate for horizontal guides, X coordinate for vertical guides) + pub position: f64, +} + +impl Guide { + pub fn new(direction: GuideDirection, position: f64) -> Self { + Self { + id: GuideId::new(), + direction, + position, + } + } + + pub fn with_id(id: GuideId, direction: GuideDirection, position: f64) -> Self { + Self { id, direction, position } + } + + pub fn horizontal(y: f64) -> Self { + Self::new(GuideDirection::Horizontal, y) + } + + pub fn vertical(x: f64) -> Self { + Self::new(GuideDirection::Vertical, x) + } +} diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 30e6a1e013..2ee7440b39 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -63,6 +63,7 @@ pub enum AlignAggregate { pub struct SnappingState { pub snapping_enabled: bool, pub grid_snapping: bool, + pub guides: bool, pub artboards: bool, pub tolerance: f64, pub bounding_box: BoundingBoxSnapping, @@ -75,6 +76,7 @@ impl Default for SnappingState { Self { snapping_enabled: true, grid_snapping: false, + guides: true, artboards: true, tolerance: 8., bounding_box: BoundingBoxSnapping::default(), @@ -106,6 +108,7 @@ impl SnappingState { }, SnapTarget::Artboard(_) => self.artboards, SnapTarget::Grid(_) => self.grid_snapping, + SnapTarget::Guide(_) => self.guides, SnapTarget::Alignment(AlignmentSnapTarget::AlignWithAnchorPoint) => self.path.align_with_anchor_point, SnapTarget::Alignment(_) => self.bounding_box.align_with_edges, SnapTarget::DistributeEvenly(_) => self.bounding_box.distribute_evenly, @@ -549,6 +552,23 @@ impl fmt::Display for GridSnapTarget { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GuideSnapTarget { + Horizontal, + Vertical, + Intersection, +} + +impl fmt::Display for GuideSnapTarget { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GuideSnapTarget::Horizontal => write!(f, "Guide: Horizontal"), + GuideSnapTarget::Vertical => write!(f, "Guide: Vertical"), + GuideSnapTarget::Intersection => write!(f, "Guide: Intersection"), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AlignmentSnapTarget { BoundingBoxCornerPoint, @@ -616,6 +636,7 @@ pub enum SnapTarget { Path(PathSnapTarget), Artboard(ArtboardSnapTarget), Grid(GridSnapTarget), + Guide(GuideSnapTarget), Alignment(AlignmentSnapTarget), DistributeEvenly(DistributionSnapTarget), } @@ -637,6 +658,7 @@ impl fmt::Display for SnapTarget { SnapTarget::Path(path_snap_target) => write!(f, "{path_snap_target}"), SnapTarget::Artboard(artboard_snap_target) => write!(f, "{artboard_snap_target}"), SnapTarget::Grid(grid_snap_target) => write!(f, "{grid_snap_target}"), + SnapTarget::Guide(guide_snap_target) => write!(f, "{guide_snap_target}"), SnapTarget::Alignment(alignment_snap_target) => write!(f, "{alignment_snap_target}"), SnapTarget::DistributeEvenly(distribution_snap_target) => write!(f, "{distribution_snap_target}"), } diff --git a/editor/src/messages/portfolio/document/utility_types/mod.rs b/editor/src/messages/portfolio/document/utility_types/mod.rs index a38ca2c545..06d8ecbdd0 100644 --- a/editor/src/messages/portfolio/document/utility_types/mod.rs +++ b/editor/src/messages/portfolio/document/utility_types/mod.rs @@ -1,5 +1,6 @@ pub mod document_metadata; pub mod error; +pub mod guide; pub mod misc; pub mod network_interface; pub mod nodes; diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 00cbe7e216..7c0b4c9335 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -1,6 +1,7 @@ mod alignment_snapper; mod distribution_snapper; mod grid_snapper; +mod guide_snapper; mod layer_snapper; mod snap_results; @@ -19,6 +20,7 @@ use graphene_std::vector::PointId; use graphene_std::vector::algorithms::intersection::filtered_segment_intersections; use graphene_std::vector::misc::point_to_dvec2; pub use grid_snapper::*; +pub use guide_snapper::*; use kurbo::ParamCurve; pub use layer_snapper::*; pub use snap_results::*; @@ -39,6 +41,7 @@ pub struct SnapManager { indicator: Option, layer_snapper: LayerSnapper, grid_snapper: GridSnapper, + guide_snapper: GuideSnapper, alignment_snapper: AlignmentSnapper, distribution_snapper: DistributionSnapper, candidates: Option>, @@ -173,6 +176,10 @@ fn get_closest_intersection(snap_to: DVec2, curves: &[SnappedCurve]) -> Option Option { + get_line_intersection(snap_to, lines, SnapTarget::Grid(GridSnapTarget::Intersection)) +} + +pub fn get_line_intersection(snap_to: DVec2, lines: &[SnappedLine], target: SnapTarget) -> Option { let mut best = None; for line_i in lines { for line_j in lines { @@ -182,7 +189,7 @@ fn get_grid_intersection(snap_to: DVec2, lines: &[SnappedLine]) -> Option Vec<(DVec2, DVec2, GuideSnapTarget)> { + let document = snap_data.document; + let mut lines = Vec::new(); + + if !document.guides_visible || !document.snapping_state.guides { + return lines; + } + + for guide in &document.horizontal_guides { + lines.push((DVec2::new(0.0, guide.position), DVec2::X, GuideSnapTarget::Horizontal)); + } + + for guide in &document.vertical_guides { + lines.push((DVec2::new(guide.position, 0.0), DVec2::Y, GuideSnapTarget::Vertical)); + } + + lines + } + + pub fn free_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults) { + let lines = self.get_snap_lines(snap_data); + let tolerance = snap_tolerance(snap_data.document); + + for (line_point, line_direction, snap_target) in lines { + let projected = (point.document_point - line_point).project_onto(line_direction) + line_point; + let distance = point.document_point.distance(projected); + + if !distance.is_finite() || distance > tolerance { + continue; + } + + let target = SnapTarget::Guide(snap_target); + if snap_data.document.snapping_state.target_enabled(target) { + snap_results.points.push(SnappedPoint { + snapped_point_document: projected, + source: point.source, + target, + source_bounds: point.quad, + distance, + tolerance, + ..Default::default() + }); + } + } + + let document = snap_data.document; + if document.snapping_state.target_enabled(SnapTarget::Guide(GuideSnapTarget::Intersection)) { + let tolerance = snap_tolerance(document); + let mut guide_lines: Vec = Vec::new(); + + for guide in &document.horizontal_guides { + guide_lines.push(SnappedLine { + point: SnappedPoint { + snapped_point_document: DVec2::new(0.0, guide.position), + source: point.source, + tolerance, + ..Default::default() + }, + direction: DVec2::X, + }); + } + + for guide in &document.vertical_guides { + guide_lines.push(SnappedLine { + point: SnappedPoint { + snapped_point_document: DVec2::new(guide.position, 0.0), + source: point.source, + tolerance, + ..Default::default() + }, + direction: DVec2::Y, + }); + } + + // Reuse the generic intersection finder from snapping module + if let Some(intersection) = super::get_line_intersection(point.document_point, &guide_lines, SnapTarget::Guide(GuideSnapTarget::Intersection)) { + if intersection.distance <= tolerance { + snap_results.points.push(intersection); + } + } + } + } + + pub fn constrained_snap(&mut self, snap_data: &mut SnapData, point: &SnapCandidatePoint, snap_results: &mut SnapResults, constraint: SnapConstraint) { + let tolerance = snap_tolerance(snap_data.document); + let projected = constraint.projection(point.document_point); + let lines = self.get_snap_lines(snap_data); + + let (constraint_start, constraint_direction) = match constraint { + SnapConstraint::Line { origin, direction } => (origin, direction.normalize_or_zero()), + SnapConstraint::Direction(direction) => (projected, direction.normalize_or_zero()), + _ => { + warn!("Circle constraint not supported for guide snapping"); + return; + } + }; + + for (line_point, line_direction, snap_target) in lines { + let Some(intersection) = Quad::intersect_rays(line_point, line_direction, constraint_start, constraint_direction) else { + continue; + }; + + let distance = intersection.distance(point.document_point); + let target = SnapTarget::Guide(snap_target); + + if distance < tolerance && snap_data.document.snapping_state.target_enabled(target) { + snap_results.points.push(SnappedPoint { + snapped_point_document: intersection, + source: point.source, + target, + at_intersection: false, + constrained: true, + source_bounds: point.quad, + distance, + tolerance, + ..Default::default() + }); + } + } + } +} diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 0bfb750ee0..2c3bf3a781 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -6,6 +6,7 @@ use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; +use crate::messages::portfolio::document::utility_types::guide::{GuideDirection, GuideId}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; @@ -477,6 +478,10 @@ enum SelectToolFsmState { }, RotatingBounds, DraggingPivot, + DraggingGuide { + guide_id: GuideId, + direction: GuideDirection, + }, } impl Default for SelectToolFsmState { @@ -723,6 +728,34 @@ pub fn create_bounding_box_transform(document: &DocumentMessageHandler) -> DAffi .unwrap_or_default() } +fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2) -> Option<(GuideId, GuideDirection)> { + const HIT_TOLERANCE: f64 = 5.0; + + if !document.guides_visible { + return None; + } + + let transform = document.metadata().document_to_viewport; + + // Checks horizontal guides (positioned by Y in document space) + for guide in &document.horizontal_guides { + let guide_viewport_y = transform.matrix2.y_axis.y * guide.position + transform.translation.y; + if (viewport_position.y - guide_viewport_y).abs() <= HIT_TOLERANCE { + return Some((guide.id, GuideDirection::Horizontal)); + } + } + + // Checks vertical guides (positioned by X in document space) + for guide in &document.vertical_guides { + let guide_viewport_x = transform.matrix2.x_axis.x * guide.position + transform.translation.x; + if (viewport_position.x - guide_viewport_x).abs() <= HIT_TOLERANCE { + return Some((guide.id, GuideDirection::Vertical)); + } + } + + None +} + impl Fsm for SelectToolFsmState { type ToolData = SelectToolData; type ToolOptions = (); @@ -1176,6 +1209,12 @@ impl Fsm for SelectToolFsmState { state } + // Check if clicking on a guide line - handle before transform cage interactions + else if let Some((guide_id, direction)) = hit_test_guide(document, input.mouse.position) { + tool_data.dragging_guide_id = Some(guide_id); + tool_data.dragging_guide_direction = Some(direction); + SelectToolFsmState::DraggingGuide { guide_id, direction } + } // Dragging one (or two, forming a corner) of the transform cage bounding box edges else if resize { tool_data.get_snap_candidates(document, input, viewport); @@ -1266,6 +1305,55 @@ impl Fsm for SelectToolFsmState { let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } + // Guide dragging - abort + (SelectToolFsmState::DraggingGuide { .. }, SelectToolMessage::Abort) => { + tool_data.dragging_guide_id = None; + tool_data.dragging_guide_direction = None; + let selection = tool_data.nested_selection_behavior; + SelectToolFsmState::Ready { selection } + } + // Guide dragging - pointer move + (SelectToolFsmState::DraggingGuide { guide_id, direction }, SelectToolMessage::PointerMove { .. }) => { + tool_data.drag_current = input.mouse.position; + + let transform = document.metadata().document_to_viewport; + // Converts viewport to document + let new_position = match direction { + GuideDirection::Horizontal => (input.mouse.position.y - transform.translation.y) / transform.matrix2.y_axis.y, + GuideDirection::Vertical => (input.mouse.position.x - transform.translation.x) / transform.matrix2.x_axis.x, + }; + + responses.add(DocumentMessage::MoveGuide { id: guide_id, position: new_position }); + + SelectToolFsmState::DraggingGuide { guide_id, direction } + } + (SelectToolFsmState::DraggingGuide { guide_id, direction }, SelectToolMessage::DragStop { .. }) => { + tool_data.drag_current = input.mouse.position; + + let transform = document.metadata().document_to_viewport; + let final_position = match direction { + GuideDirection::Horizontal => (input.mouse.position.y - transform.translation.y) / transform.matrix2.y_axis.y, + GuideDirection::Vertical => (input.mouse.position.x - transform.translation.x) / transform.matrix2.x_axis.x, + }; + + // Checks if dragged outside viewport - deletes the guide + let viewport_size = viewport.size().into_dvec2(); + let outside_viewport = input.mouse.position.x < 0.0 || input.mouse.position.y < 0.0 || input.mouse.position.x > viewport_size.x || input.mouse.position.y > viewport_size.y; + + if outside_viewport { + responses.add(DocumentMessage::DeleteGuide { id: guide_id }); + } else { + responses.add(DocumentMessage::MoveGuide { + id: guide_id, + position: final_position, + }); + } + + tool_data.dragging_guide_id = None; + tool_data.dragging_guide_direction = None; + let selection = tool_data.nested_selection_behavior; + SelectToolFsmState::Ready { selection } + } ( SelectToolFsmState::Dragging { axis, @@ -1903,6 +1991,13 @@ impl Fsm for SelectToolFsmState { let hint_data = HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]); hint_data.send_layout(responses); } + SelectToolFsmState::DraggingGuide { .. } => { + let hint_data = HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Move Guide")]), + ]); + hint_data.send_layout(responses); + } } } diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 5044490212..13368cffee 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -18,6 +18,7 @@ import { textInputCleanup } from "/src/utility-functions/keyboard-entry"; import { rasterizeSVGCanvas } from "/src/utility-functions/rasterization"; import { setupViewportResizeObserver, hasFirstArtworkBeenReceived, markFirstArtworkReceived } from "/src/utility-functions/viewports"; + import { generateGuideLineId } from "/wasm/pkg/graphite_wasm"; import type { EditorWrapper, MenuDirection, MouseCursorIcon, SRGBA8 } from "/wrapper/pkg/graphite_wasm_wrapper"; let rulerHorizontal: RulerInput | undefined; @@ -51,6 +52,10 @@ let rulerSelectionQuad: [number, number][] | undefined; let viewportBounds: DOMRect | undefined; + // Guide drag state + let draggingGuideId: bigint | undefined = undefined; + let draggingGuideDirection: "Horizontal" | "Vertical" | undefined = undefined; + // Rendered SVG viewport data let artworkSvg = ""; @@ -160,6 +165,101 @@ editor.panCanvas(0, -delta * scrollbarMultiplier.y); } + // Guide Drag Utilities + + type GuideDirection = "Horizontal" | "Vertical"; + + type GuideEditor = { + createGuideLine: (id: bigint, direction: string, mouseX: number, mouseY: number) => void; + moveGuideLine: (id: bigint, mouseX: number, mouseY: number) => void; + deleteGuideLine: (id: bigint) => void; + }; + + function isGuideEditor(x: unknown): x is GuideEditor { + return true; + } + + // Access the underlying EditorHandle for guide-specific methods + // EditorWrapper delegates to EditorHandle internally, so this cast is safe in the current architecture + function getEditorHandle(): GuideEditor { + const handle = editor; + if (isGuideEditor(handle)) return handle; + throw new Error("Expected editor to support guide line operations."); + } + + function isInRulerArea(event: PointerEvent, viewportRect: DOMRect, direction: GuideDirection): boolean { + return direction === "Horizontal" ? event.clientY < viewportRect.top : event.clientX < viewportRect.left; + } + + function createGuideDragHandlers(options: { deleteOnCancel: boolean }) { + const viewportEl = viewport; + if (!viewportEl) return null; + + const onMove = (event: PointerEvent) => { + if (draggingGuideId === undefined || !draggingGuideDirection) return; + const rect = viewportEl.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; + getEditorHandle().moveGuideLine(draggingGuideId, mouseX, mouseY); + }; + + const onRelease = (event: PointerEvent) => { + if (draggingGuideId === undefined || !draggingGuideDirection) return; + const rect = viewportEl.getBoundingClientRect(); + if (isInRulerArea(event, rect, draggingGuideDirection)) { + getEditorHandle().deleteGuideLine(draggingGuideId); + } + cleanup(); + }; + + const onEscape = (event: KeyboardEvent) => { + if (event.key !== "Escape" || draggingGuideId === undefined) return; + if (options.deleteOnCancel) getEditorHandle().deleteGuideLine(draggingGuideId); + cleanup(); + }; + + const onRightClick = (event: MouseEvent) => { + if (draggingGuideId === undefined) return; + event.preventDefault(); + if (options.deleteOnCancel) getEditorHandle().deleteGuideLine(draggingGuideId); + cleanup(); + }; + + const cleanup = () => { + draggingGuideId = undefined; + draggingGuideDirection = undefined; + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onRelease); + window.removeEventListener("keydown", onEscape); + window.removeEventListener("contextmenu", onRightClick); + }; + + return { onMove, onRelease, onEscape, onRightClick }; + } + + function startGuideDrag(options: { deleteOnCancel: boolean }) { + const handlers = createGuideDragHandlers(options); + if (!handlers) return; + + window.addEventListener("pointermove", handlers.onMove); + window.addEventListener("pointerup", handlers.onRelease); + window.addEventListener("keydown", handlers.onEscape); + window.addEventListener("contextmenu", handlers.onRightClick); + } + + // Guide Event Handlers + + function handleGuideDragStart(e: CustomEvent<{ direction: GuideDirection; mouseX: number; mouseY: number }>) { + const { direction, mouseX, mouseY } = e.detail; + + const guideId = generateGuideLineId(); + draggingGuideId = guideId; + draggingGuideDirection = direction; + + getEditorHandle().createGuideLine(guideId, direction, mouseX, mouseY); + startGuideDrag({ deleteOnCancel: true }); + } + function canvasPointerDown(e: PointerEvent) { const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable; @@ -630,6 +730,8 @@ cursorPosition={rulerCursorPosition} selectionQuad={rulerSelectionQuad} bind:this={rulerHorizontal} + on:guideLineDragStart={handleGuideDragStart} + viewportEl={viewport} /> {/if} @@ -647,6 +749,8 @@ cursorPosition={rulerCursorPosition} selectionQuad={rulerSelectionQuad} bind:this={rulerVertical} + on:guideLineDragStart={handleGuideDragStart} + viewportEl={viewport} /> {/if} diff --git a/frontend/src/components/widgets/inputs/RulerInput.svelte b/frontend/src/components/widgets/inputs/RulerInput.svelte index 428609ce74..c2e09c363a 100644 --- a/frontend/src/components/widgets/inputs/RulerInput.svelte +++ b/frontend/src/components/widgets/inputs/RulerInput.svelte @@ -1,5 +1,5 @@
-
+
{#each svgTexts as svgText} From 9e95d015dfb19c2f6efc7ae6a1529fee20e49551 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Fri, 16 Jan 2026 08:27:29 +0000 Subject: [PATCH 02/26] Fixed Guide Movement --- .../tool/tool_messages/select_tool.rs | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 2c3bf3a781..3d311a272a 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -517,6 +517,10 @@ struct SelectToolData { selected_layers_changed: bool, snap_candidates: Vec, auto_panning: AutoPanning, + dragging_guide_id: Option, + dragging_guide_direction: Option, + guide_drag_start_position: Option, + drag_start_center: ViewportPosition, } impl SelectToolData { @@ -1208,11 +1212,15 @@ impl Fsm for SelectToolFsmState { // tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]); state - } - // Check if clicking on a guide line - handle before transform cage interactions - else if let Some((guide_id, direction)) = hit_test_guide(document, input.mouse.position) { + } else if let Some((guide_id, direction)) = hit_test_guide(document, input.mouse.position) { tool_data.dragging_guide_id = Some(guide_id); tool_data.dragging_guide_direction = Some(direction); + + let original_position = match direction { + GuideDirection::Horizontal => document.horizontal_guides.iter().find(|g| g.id == guide_id).map(|g| g.position), + GuideDirection::Vertical => document.vertical_guides.iter().find(|g| g.id == guide_id).map(|g| g.position), + }; + tool_data.guide_drag_start_position = original_position; SelectToolFsmState::DraggingGuide { guide_id, direction } } // Dragging one (or two, forming a corner) of the transform cage bounding box edges @@ -1309,6 +1317,7 @@ impl Fsm for SelectToolFsmState { (SelectToolFsmState::DraggingGuide { .. }, SelectToolMessage::Abort) => { tool_data.dragging_guide_id = None; tool_data.dragging_guide_direction = None; + tool_data.guide_drag_start_position = None; let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } @@ -1316,26 +1325,22 @@ impl Fsm for SelectToolFsmState { (SelectToolFsmState::DraggingGuide { guide_id, direction }, SelectToolMessage::PointerMove { .. }) => { tool_data.drag_current = input.mouse.position; - let transform = document.metadata().document_to_viewport; - // Converts viewport to document - let new_position = match direction { - GuideDirection::Horizontal => (input.mouse.position.y - transform.translation.y) / transform.matrix2.y_axis.y, - GuideDirection::Vertical => (input.mouse.position.x - transform.translation.x) / transform.matrix2.x_axis.x, + // MoveGuide expects viewport coordinates and does the conversion internally + let viewport_position = match direction { + GuideDirection::Horizontal => input.mouse.position.y, + GuideDirection::Vertical => input.mouse.position.x, }; - responses.add(DocumentMessage::MoveGuide { id: guide_id, position: new_position }); + responses.add(DocumentMessage::MoveGuide { + id: guide_id, + position: viewport_position, + }); SelectToolFsmState::DraggingGuide { guide_id, direction } } (SelectToolFsmState::DraggingGuide { guide_id, direction }, SelectToolMessage::DragStop { .. }) => { tool_data.drag_current = input.mouse.position; - let transform = document.metadata().document_to_viewport; - let final_position = match direction { - GuideDirection::Horizontal => (input.mouse.position.y - transform.translation.y) / transform.matrix2.y_axis.y, - GuideDirection::Vertical => (input.mouse.position.x - transform.translation.x) / transform.matrix2.x_axis.x, - }; - // Checks if dragged outside viewport - deletes the guide let viewport_size = viewport.size().into_dvec2(); let outside_viewport = input.mouse.position.x < 0.0 || input.mouse.position.y < 0.0 || input.mouse.position.x > viewport_size.x || input.mouse.position.y > viewport_size.y; @@ -1343,14 +1348,20 @@ impl Fsm for SelectToolFsmState { if outside_viewport { responses.add(DocumentMessage::DeleteGuide { id: guide_id }); } else { + // MoveGuide expects viewport coordinates and does the conversion internally + let viewport_position = match direction { + GuideDirection::Horizontal => input.mouse.position.y, + GuideDirection::Vertical => input.mouse.position.x, + }; responses.add(DocumentMessage::MoveGuide { id: guide_id, - position: final_position, + position: viewport_position, }); } tool_data.dragging_guide_id = None; tool_data.dragging_guide_direction = None; + tool_data.guide_drag_start_position = None; let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } From 30cf143c12e9c06dc6f82d8bfaedf557bcb832b7 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Fri, 16 Jan 2026 15:15:54 +0000 Subject: [PATCH 03/26] Added cursor handling for guide dragging --- .../messages/tool/tool_messages/select_tool.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 3d311a272a..deddd991ea 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1336,6 +1336,15 @@ impl Fsm for SelectToolFsmState { position: viewport_position, }); + let cursor = match direction { + GuideDirection::Horizontal => MouseCursorIcon::NSResize, + GuideDirection::Vertical => MouseCursorIcon::EWResize, + }; + if tool_data.cursor != cursor { + tool_data.cursor = cursor; + responses.add(FrontendMessage::UpdateMouseCursor { cursor }); + } + SelectToolFsmState::DraggingGuide { guide_id, direction } } (SelectToolFsmState::DraggingGuide { guide_id, direction }, SelectToolMessage::DragStop { .. }) => { @@ -1549,6 +1558,13 @@ impl Fsm for SelectToolFsmState { cursor = MouseCursorIcon::Move; } + if let Some((_, direction)) = hit_test_guide(document, input.mouse.position) { + cursor = match direction { + GuideDirection::Horizontal => MouseCursorIcon::NSResize, + GuideDirection::Vertical => MouseCursorIcon::EWResize, + }; + } + // Generate the hover outline responses.add(OverlaysMessage::Draw); From 2b9e692172a8af45b11e92d92229a0f6b5e9d341 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Fri, 16 Jan 2026 18:53:15 +0000 Subject: [PATCH 04/26] Removed unneccessary code --- editor/src/messages/frontend/frontend_message.rs | 8 -------- editor/src/messages/tool/tool_messages/select_tool.rs | 11 +++++------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 6258a83b48..e227c6821a 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -329,14 +329,6 @@ pub enum FrontendMessage { UpdateUIScale { scale: f64, }, - UpdateGuidesData { - #[serde(rename = "horizontalGuides")] - horizontal_guides: Vec<(u64, f64)>, - #[serde(rename = "verticalGuides")] - vertical_guides: Vec<(u64, f64)>, - #[serde(rename = "documentToViewport")] - document_to_viewport: [f64; 6], - }, #[cfg(not(target_family = "wasm"))] RenderOverlays { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index deddd991ea..a96bfb4fc0 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -741,18 +741,17 @@ fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2) - let transform = document.metadata().document_to_viewport; - // Checks horizontal guides (positioned by Y in document space) + // Check horizontal guides (lines that run left-right, positioned by Y in document space) for guide in &document.horizontal_guides { - let guide_viewport_y = transform.matrix2.y_axis.y * guide.position + transform.translation.y; - if (viewport_position.y - guide_viewport_y).abs() <= HIT_TOLERANCE { + let guide_viewport = transform.transform_point2(DVec2::new(0.0, guide.position)); + if (viewport_position.y - guide_viewport.y).abs() <= HIT_TOLERANCE { return Some((guide.id, GuideDirection::Horizontal)); } } - // Checks vertical guides (positioned by X in document space) for guide in &document.vertical_guides { - let guide_viewport_x = transform.matrix2.x_axis.x * guide.position + transform.translation.x; - if (viewport_position.x - guide_viewport_x).abs() <= HIT_TOLERANCE { + let guide_viewport = transform.transform_point2(DVec2::new(guide.position, 0.0)); + if (viewport_position.x - guide_viewport.x).abs() <= HIT_TOLERANCE { return Some((guide.id, GuideDirection::Vertical)); } } From 5f1d4101b60df463abd90a057d0e5a88c071062b Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 17 Jan 2026 07:52:02 +0000 Subject: [PATCH 05/26] Test --- .../portfolio/document/document_message_handler.rs | 12 +++++++++++- .../portfolio/document/overlays/guide_overlays.rs | 7 +++++-- .../src/messages/tool/tool_messages/select_tool.rs | 13 ++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 6dbe0dd45f..bde4689d6d 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2,7 +2,7 @@ use super::DocumentHistory; use super::document_diff::diff_networks; use super::node_graph::document_node_definitions; use super::utility_types::error::EditorError; -use super::utility_types::guide::Guide; +use super::utility_types::guide::{Guide, GuideId}; use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS, SnappingOptions, SnappingState}; use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, LayerStructureEntry, SelectedNodes}; @@ -166,6 +166,9 @@ pub struct DocumentMessageHandler { /// Whether guide lines are visible in the viewport. #[serde(default = "default_guides_visible")] pub guides_visible: bool, + /// ID of the currently hovered guide for visual feedback. + #[serde(skip)] + pub hovered_guide_id: Option, /// Whether or not the editor has executed the network to render the document yet. If this is opened as an inactive tab, it won't be loaded initially because the active tab is prioritized. #[serde(skip)] pub is_loaded: bool, @@ -213,6 +216,7 @@ impl Default for DocumentMessageHandler { horizontal_guides: Vec::new(), vertical_guides: Vec::new(), guides_visible: true, + hovered_guide_id: None, is_loaded: false, } } @@ -740,6 +744,12 @@ impl MessageHandler> for DocumentMes // Mutates the network directly, so it must be queued to run after `AddTransaction` has snapshotted the document self.handle_expand_fill_stroke_on_selected_layers(responses); } + DocumentMessage::SetHoveredGuide { id } => { + if self.hovered_guide_id != id { + self.hovered_guide_id = id; + responses.add(OverlaysMessage::Draw); + } + } DocumentMessage::GroupSelectedLayers { group_folder_type } => { self.group_selected_layers(group_folder_type, responses); } diff --git a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs index 2b3a72385c..706a63b42a 100644 --- a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs @@ -4,6 +4,7 @@ use glam::DVec2; use graphene_std::renderer::Quad; const GUIDE_COLOR: &str = "#00BFFF"; +const GUIDE_HOVER_COLOR: &str = "#FF6600"; pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { let document_to_viewport = document @@ -19,12 +20,14 @@ pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut Ov for guide in &document.horizontal_guides { let start = DVec2::new(min_x, guide.position); let end = DVec2::new(max_x, guide.position); - overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(GUIDE_COLOR), None); + let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR }; + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(color), None); } for guide in &document.vertical_guides { let start = DVec2::new(guide.position, min_y); let end = DVec2::new(guide.position, max_y); - overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(GUIDE_COLOR), None); + let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR }; + overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(color), None); } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index a96bfb4fc0..3d062d9242 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -741,15 +741,17 @@ fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2) - let transform = document.metadata().document_to_viewport; + // Iterate in reverse order so most recently placed guide has priority (like Inkscape) // Check horizontal guides (lines that run left-right, positioned by Y in document space) - for guide in &document.horizontal_guides { + for guide in document.horizontal_guides.iter().rev() { let guide_viewport = transform.transform_point2(DVec2::new(0.0, guide.position)); if (viewport_position.y - guide_viewport.y).abs() <= HIT_TOLERANCE { return Some((guide.id, GuideDirection::Horizontal)); } } - for guide in &document.vertical_guides { + // Check vertical guides (lines that run up-down, positioned by X in document space) + for guide in document.vertical_guides.iter().rev() { let guide_viewport = transform.transform_point2(DVec2::new(guide.position, 0.0)); if (viewport_position.x - guide_viewport.x).abs() <= HIT_TOLERANCE { return Some((guide.id, GuideDirection::Vertical)); @@ -1557,11 +1559,16 @@ impl Fsm for SelectToolFsmState { cursor = MouseCursorIcon::Move; } - if let Some((_, direction)) = hit_test_guide(document, input.mouse.position) { + // Check if hovering over a guide and update hover state + let hovered_guide = hit_test_guide(document, input.mouse.position); + if let Some((guide_id, direction)) = hovered_guide { cursor = match direction { GuideDirection::Horizontal => MouseCursorIcon::NSResize, GuideDirection::Vertical => MouseCursorIcon::EWResize, }; + responses.add(DocumentMessage::SetHoveredGuide { id: Some(guide_id) }); + } else { + responses.add(DocumentMessage::SetHoveredGuide { id: None }); } // Generate the hover outline From ca8ec23f72ec3ca3dc4e423cc34aaae9664c2140 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 17 Jan 2026 15:57:57 +0000 Subject: [PATCH 06/26] Fix guide rotation alignment --- .../document/overlays/guide_overlays.rs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs index 706a63b42a..d0679d7749 100644 --- a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs @@ -1,7 +1,6 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::prelude::DocumentMessageHandler; use glam::DVec2; -use graphene_std::renderer::Quad; const GUIDE_COLOR: &str = "#00BFFF"; const GUIDE_HOVER_COLOR: &str = "#FF6600"; @@ -11,23 +10,25 @@ pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut Ov .navigation_handler .calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz); - let bounds = document_to_viewport.inverse() * Quad::from_box([DVec2::ZERO, overlay_context.viewport.size().into()]); - - let [min, max] = bounds.bounding_box(); - let (min_x, max_x) = (min.x, max.x); - let (min_y, max_y) = (min.y, max.y); + let viewport_size: DVec2 = overlay_context.viewport.size().into(); for guide in &document.horizontal_guides { - let start = DVec2::new(min_x, guide.position); - let end = DVec2::new(max_x, guide.position); + let guide_point_viewport = document_to_viewport.transform_point2(DVec2::new(0.0, guide.position)); + let viewport_y = guide_point_viewport.y; + + let start = DVec2::new(0.0, viewport_y); + let end = DVec2::new(viewport_size.x, viewport_y); let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR }; - overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(color), None); + overlay_context.line(start, end, Some(color), None); } for guide in &document.vertical_guides { - let start = DVec2::new(guide.position, min_y); - let end = DVec2::new(guide.position, max_y); + let guide_point_viewport = document_to_viewport.transform_point2(DVec2::new(guide.position, 0.0)); + let viewport_x = guide_point_viewport.x; + + let start = DVec2::new(viewport_x, 0.0); + let end = DVec2::new(viewport_x, viewport_size.y); let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR }; - overlay_context.line(document_to_viewport.transform_point2(start), document_to_viewport.transform_point2(end), Some(color), None); + overlay_context.line(start, end, Some(color), None); } } From 49561a84100d885200d707058dd2d98e3b95676e Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 17 Jan 2026 18:44:24 +0000 Subject: [PATCH 07/26] Fix guide alignment in case of rotated artboard --- .../portfolio/document/document_message_handler.rs | 1 + .../src/messages/tool/tool_messages/select_tool.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index bde4689d6d..a6a94e10f7 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -720,6 +720,7 @@ impl MessageHandler> for DocumentMes self.snapping_state.grid_snapping = visible; responses.add(OverlaysMessage::Draw); } +<<<<<<< HEAD DocumentMessage::BlendSelectedLayers => { self.group_selected_layers(GroupFolderType::Blend, responses); } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 3d062d9242..8b1fb1c89e 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -732,17 +732,18 @@ pub fn create_bounding_box_transform(document: &DocumentMessageHandler) -> DAffi .unwrap_or_default() } -fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2) -> Option<(GuideId, GuideDirection)> { +fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2, viewport: &ViewportMessageHandler) -> Option<(GuideId, GuideDirection)> { const HIT_TOLERANCE: f64 = 5.0; if !document.guides_visible { return None; } - let transform = document.metadata().document_to_viewport; + let transform = document + .navigation_handler + .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); - // Iterate in reverse order so most recently placed guide has priority (like Inkscape) - // Check horizontal guides (lines that run left-right, positioned by Y in document space) + // Iterates in reverse order so most recently placed guide has priority (like Inkscape) for guide in document.horizontal_guides.iter().rev() { let guide_viewport = transform.transform_point2(DVec2::new(0.0, guide.position)); if (viewport_position.y - guide_viewport.y).abs() <= HIT_TOLERANCE { @@ -750,7 +751,6 @@ fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2) - } } - // Check vertical guides (lines that run up-down, positioned by X in document space) for guide in document.vertical_guides.iter().rev() { let guide_viewport = transform.transform_point2(DVec2::new(guide.position, 0.0)); if (viewport_position.x - guide_viewport.x).abs() <= HIT_TOLERANCE { @@ -1213,7 +1213,7 @@ impl Fsm for SelectToolFsmState { // tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]); state - } else if let Some((guide_id, direction)) = hit_test_guide(document, input.mouse.position) { + } else if let Some((guide_id, direction)) = hit_test_guide(document, input.mouse.position, viewport) { tool_data.dragging_guide_id = Some(guide_id); tool_data.dragging_guide_direction = Some(direction); @@ -1560,7 +1560,7 @@ impl Fsm for SelectToolFsmState { } // Check if hovering over a guide and update hover state - let hovered_guide = hit_test_guide(document, input.mouse.position); + let hovered_guide = hit_test_guide(document, input.mouse.position, viewport); if let Some((guide_id, direction)) = hovered_guide { cursor = match direction { GuideDirection::Horizontal => MouseCursorIcon::NSResize, From 9ac6f7936cc32a8dc2834020e6ce3f3c7a894b52 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Mon, 19 Jan 2026 00:56:12 +0000 Subject: [PATCH 08/26] Fixed offset --- .../document/document_message_handler.rs | 7 -- .../document/overlays/guide_overlays.rs | 86 ++++++++++++++++--- .../tool/tool_messages/select_tool.rs | 50 +++++++---- .../widgets/inputs/RulerInput.svelte | 1 - 4 files changed, 108 insertions(+), 36 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index a6a94e10f7..2cd34380f5 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -720,7 +720,6 @@ impl MessageHandler> for DocumentMes self.snapping_state.grid_snapping = visible; responses.add(OverlaysMessage::Draw); } -<<<<<<< HEAD DocumentMessage::BlendSelectedLayers => { self.group_selected_layers(GroupFolderType::Blend, responses); } @@ -745,12 +744,6 @@ impl MessageHandler> for DocumentMes // Mutates the network directly, so it must be queued to run after `AddTransaction` has snapshotted the document self.handle_expand_fill_stroke_on_selected_layers(responses); } - DocumentMessage::SetHoveredGuide { id } => { - if self.hovered_guide_id != id { - self.hovered_guide_id = id; - responses.add(OverlaysMessage::Draw); - } - } DocumentMessage::GroupSelectedLayers { group_folder_type } => { self.group_selected_layers(group_folder_type, responses); } diff --git a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs index d0679d7749..56de57a37c 100644 --- a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs @@ -5,6 +5,64 @@ use glam::DVec2; const GUIDE_COLOR: &str = "#00BFFF"; const GUIDE_HOVER_COLOR: &str = "#FF6600"; +fn extend_line_to_viewport(point: DVec2, direction: DVec2, viewport_size: DVec2) -> Option<(DVec2, DVec2)> { + if direction.length_squared() < f64::EPSILON { + return None; + } + + let dir = direction.normalize(); + + // Calculates t values for intersections with viewport edges + let mut t_values = Vec::new(); + + if dir.x.abs() > f64::EPSILON { + let t = -point.x / dir.x; + let y = point.y + t * dir.y; + if y >= 0.0 && y <= viewport_size.y { + t_values.push(t); + } + } + + // Right edge (x = viewport_size.x) + if dir.x.abs() > f64::EPSILON { + let t = (viewport_size.x - point.x) / dir.x; + let y = point.y + t * dir.y; + if y >= 0.0 && y <= viewport_size.y { + t_values.push(t); + } + } + + // Top edge (y = 0) + if dir.y.abs() > f64::EPSILON { + let t = -point.y / dir.y; + let x = point.x + t * dir.x; + if x >= 0.0 && x <= viewport_size.x { + t_values.push(t); + } + } + + // Bottom edge (y = viewport_size.y) + if dir.y.abs() > f64::EPSILON { + let t = (viewport_size.y - point.y) / dir.y; + let x = point.x + t * dir.x; + if x >= 0.0 && x <= viewport_size.x { + t_values.push(t); + } + } + + if t_values.len() < 2 { + return None; + } + + let t_min = t_values.iter().cloned().fold(f64::INFINITY, f64::min); + let t_max = t_values.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + + let start = point + dir * t_min; + let end = point + dir * t_max; + + Some((start, end)) +} + pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { let document_to_viewport = document .navigation_handler @@ -13,22 +71,30 @@ pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut Ov let viewport_size: DVec2 = overlay_context.viewport.size().into(); for guide in &document.horizontal_guides { - let guide_point_viewport = document_to_viewport.transform_point2(DVec2::new(0.0, guide.position)); - let viewport_y = guide_point_viewport.y; + let doc_point = DVec2::new(0.0, guide.position); + let doc_direction = DVec2::X; // Horizontal guides run in the X direction in document space + + let viewport_point = document_to_viewport.transform_point2(doc_point); + let viewport_direction = document_to_viewport.transform_vector2(doc_direction); - let start = DVec2::new(0.0, viewport_y); - let end = DVec2::new(viewport_size.x, viewport_y); let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR }; - overlay_context.line(start, end, Some(color), None); + + if let Some((start, end)) = extend_line_to_viewport(viewport_point, viewport_direction, viewport_size) { + overlay_context.line(start, end, Some(color), None); + } } for guide in &document.vertical_guides { - let guide_point_viewport = document_to_viewport.transform_point2(DVec2::new(guide.position, 0.0)); - let viewport_x = guide_point_viewport.x; + let doc_point = DVec2::new(guide.position, 0.0); + let doc_direction = DVec2::Y; + + let viewport_point = document_to_viewport.transform_point2(doc_point); + let viewport_direction = document_to_viewport.transform_vector2(doc_direction); - let start = DVec2::new(viewport_x, 0.0); - let end = DVec2::new(viewport_x, viewport_size.y); let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR }; - overlay_context.line(start, end, Some(color), None); + + if let Some((start, end)) = extend_line_to_viewport(viewport_point, viewport_direction, viewport_size) { + overlay_context.line(start, end, Some(color), None); + } } } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 8b1fb1c89e..7985176225 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -743,18 +743,39 @@ fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2, v .navigation_handler .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); - // Iterates in reverse order so most recently placed guide has priority (like Inkscape) for guide in document.horizontal_guides.iter().rev() { - let guide_viewport = transform.transform_point2(DVec2::new(0.0, guide.position)); - if (viewport_position.y - guide_viewport.y).abs() <= HIT_TOLERANCE { - return Some((guide.id, GuideDirection::Horizontal)); + let doc_point = DVec2::new(0.0, guide.position); + let doc_direction = DVec2::X; + + let viewport_point = transform.transform_point2(doc_point); + let viewport_direction = transform.transform_vector2(doc_direction); + + if viewport_direction.length_squared() > f64::EPSILON { + let dir_normalized = viewport_direction.normalize(); + let to_mouse = viewport_position - viewport_point; + let perpendicular_dist = (to_mouse.x * dir_normalized.y - to_mouse.y * dir_normalized.x).abs(); + + if perpendicular_dist <= HIT_TOLERANCE { + return Some((guide.id, GuideDirection::Horizontal)); + } } } for guide in document.vertical_guides.iter().rev() { - let guide_viewport = transform.transform_point2(DVec2::new(guide.position, 0.0)); - if (viewport_position.x - guide_viewport.x).abs() <= HIT_TOLERANCE { - return Some((guide.id, GuideDirection::Vertical)); + let doc_point = DVec2::new(guide.position, 0.0); + let doc_direction = DVec2::Y; + + let viewport_point = transform.transform_point2(doc_point); + let viewport_direction = transform.transform_vector2(doc_direction); + + if viewport_direction.length_squared() > f64::EPSILON { + let dir_normalized = viewport_direction.normalize(); + let to_mouse = viewport_position - viewport_point; + let perpendicular_dist = (to_mouse.x * dir_normalized.y - to_mouse.y * dir_normalized.x).abs(); + + if perpendicular_dist <= HIT_TOLERANCE { + return Some((guide.id, GuideDirection::Vertical)); + } } } @@ -1327,14 +1348,10 @@ impl Fsm for SelectToolFsmState { tool_data.drag_current = input.mouse.position; // MoveGuide expects viewport coordinates and does the conversion internally - let viewport_position = match direction { - GuideDirection::Horizontal => input.mouse.position.y, - GuideDirection::Vertical => input.mouse.position.x, - }; - responses.add(DocumentMessage::MoveGuide { id: guide_id, - position: viewport_position, + mouse_x: input.mouse.position.x, + mouse_y: input.mouse.position.y, }); let cursor = match direction { @@ -1359,13 +1376,10 @@ impl Fsm for SelectToolFsmState { responses.add(DocumentMessage::DeleteGuide { id: guide_id }); } else { // MoveGuide expects viewport coordinates and does the conversion internally - let viewport_position = match direction { - GuideDirection::Horizontal => input.mouse.position.y, - GuideDirection::Vertical => input.mouse.position.x, - }; responses.add(DocumentMessage::MoveGuide { id: guide_id, - position: viewport_position, + mouse_x: input.mouse.position.x, + mouse_y: input.mouse.position.y, }); } diff --git a/frontend/src/components/widgets/inputs/RulerInput.svelte b/frontend/src/components/widgets/inputs/RulerInput.svelte index c2e09c363a..f7d707ad9b 100644 --- a/frontend/src/components/widgets/inputs/RulerInput.svelte +++ b/frontend/src/components/widgets/inputs/RulerInput.svelte @@ -21,7 +21,6 @@ export let microDivisions = 2; export let cursorPosition: { x: number; y: number } | undefined = undefined; export let selectionQuad: [number, number][] | undefined = undefined; - const dispatch = createEventDispatcher<{ guideLineDragStart: { direction: RulerDirection; mouseX: number; mouseY: number }; }>(); From 4cf39f8510239b05c8ecabd0402c1492bf324774 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 20 Jan 2026 06:09:06 +0000 Subject: [PATCH 09/26] Color change --- .../portfolio/document/overlays/guide_overlays.rs | 8 +++----- editor/src/messages/tool/tool_messages/select_tool.rs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs index 56de57a37c..853ad25126 100644 --- a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs @@ -1,10 +1,8 @@ +use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50}; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::prelude::DocumentMessageHandler; use glam::DVec2; -const GUIDE_COLOR: &str = "#00BFFF"; -const GUIDE_HOVER_COLOR: &str = "#FF6600"; - fn extend_line_to_viewport(point: DVec2, direction: DVec2, viewport_size: DVec2) -> Option<(DVec2, DVec2)> { if direction.length_squared() < f64::EPSILON { return None; @@ -77,7 +75,7 @@ pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut Ov let viewport_point = document_to_viewport.transform_point2(doc_point); let viewport_direction = document_to_viewport.transform_vector2(doc_direction); - let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR }; + let color = if document.hovered_guide_id == Some(guide.id) { COLOR_OVERLAY_BLUE_50 } else { COLOR_OVERLAY_BLUE }; if let Some((start, end)) = extend_line_to_viewport(viewport_point, viewport_direction, viewport_size) { overlay_context.line(start, end, Some(color), None); @@ -91,7 +89,7 @@ pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut Ov let viewport_point = document_to_viewport.transform_point2(doc_point); let viewport_direction = document_to_viewport.transform_vector2(doc_direction); - let color = if document.hovered_guide_id == Some(guide.id) { GUIDE_HOVER_COLOR } else { GUIDE_COLOR }; + let color = if document.hovered_guide_id == Some(guide.id) { COLOR_OVERLAY_BLUE_50 } else { COLOR_OVERLAY_BLUE }; if let Some((start, end)) = extend_line_to_viewport(viewport_point, viewport_direction, viewport_size) { overlay_context.line(start, end, Some(color), None); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 7985176225..f5c2358571 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1365,7 +1365,7 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::DraggingGuide { guide_id, direction } } - (SelectToolFsmState::DraggingGuide { guide_id, direction }, SelectToolMessage::DragStop { .. }) => { + (SelectToolFsmState::DraggingGuide { guide_id, direction: _ }, SelectToolMessage::DragStop { .. }) => { tool_data.drag_current = input.mouse.position; // Checks if dragged outside viewport - deletes the guide From df1672a486aefeff0e6566090334c960256093f2 Mon Sep 17 00:00:00 2001 From: Kulcode <152772205+jsjgdh@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:12:23 +0530 Subject: [PATCH 10/26] Update editor/src/messages/portfolio/document/overlays/guide_overlays.rs Co-authored-by: James Lindsay <78500760+0HyperCube@users.noreply.github.com> --- .../messages/portfolio/document/overlays/guide_overlays.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs index 853ad25126..34ff206492 100644 --- a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs @@ -4,11 +4,7 @@ use crate::messages::prelude::DocumentMessageHandler; use glam::DVec2; fn extend_line_to_viewport(point: DVec2, direction: DVec2, viewport_size: DVec2) -> Option<(DVec2, DVec2)> { - if direction.length_squared() < f64::EPSILON { - return None; - } - - let dir = direction.normalize(); + let dir = direction.try_normalize()?; // Calculates t values for intersections with viewport edges let mut t_values = Vec::new(); From 5cb66eb68ecc2ad0d895daa9bbbeb1e91c6dfe86 Mon Sep 17 00:00:00 2001 From: Kulcode <152772205+jsjgdh@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:12:44 +0530 Subject: [PATCH 11/26] Update editor/src/messages/portfolio/document/overlays/guide_overlays.rs Co-authored-by: James Lindsay <78500760+0HyperCube@users.noreply.github.com> --- .../document/overlays/guide_overlays.rs | 38 +++---------------- 1 file changed, 6 insertions(+), 32 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs index 34ff206492..fcaaf3c957 100644 --- a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs @@ -9,38 +9,12 @@ fn extend_line_to_viewport(point: DVec2, direction: DVec2, viewport_size: DVec2) // Calculates t values for intersections with viewport edges let mut t_values = Vec::new(); - if dir.x.abs() > f64::EPSILON { - let t = -point.x / dir.x; - let y = point.y + t * dir.y; - if y >= 0.0 && y <= viewport_size.y { - t_values.push(t); - } - } - - // Right edge (x = viewport_size.x) - if dir.x.abs() > f64::EPSILON { - let t = (viewport_size.x - point.x) / dir.x; - let y = point.y + t * dir.y; - if y >= 0.0 && y <= viewport_size.y { - t_values.push(t); - } - } - - // Top edge (y = 0) - if dir.y.abs() > f64::EPSILON { - let t = -point.y / dir.y; - let x = point.x + t * dir.x; - if x >= 0.0 && x <= viewport_size.x { - t_values.push(t); - } - } - - // Bottom edge (y = viewport_size.y) - if dir.y.abs() > f64::EPSILON { - let t = (viewport_size.y - point.y) / dir.y; - let x = point.x + t * dir.x; - if x >= 0.0 && x <= viewport_size.x { - t_values.push(t); + let edges = graphene_std::renderer::Quad::from_box([DVec2::ZERO, viewport_size]).all_edges(); + for [start, end] in edges { + let t_along_viewport = (point - start).perp_dot(dir) / (end - start).perp_dot(dir); + let t_along_direction = (point - start).perp_dot(end - start) / (end - start).perp_dot(dir); + if 0. <= t_along_viewport && t_along_viewport <= 1. && t_along_direction.is_finite() { + t_values.push(t_along_direction); } } From 571ce0ddf73fa00dcead9128b3a2abade9fcbeac Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Mon, 26 Jan 2026 17:47:36 +0000 Subject: [PATCH 12/26] Changes as per recommendation --- editor/src/consts.rs | 3 + editor/src/dispatcher.rs | 2 +- .../menu_bar/menu_bar_message_handler.rs | 3 +- .../portfolio/document/document_message.rs | 2 + .../document/document_message_handler.rs | 35 +++--- .../portfolio/document/guide_message.rs | 14 +++ .../document/guide_message_handler.rs | 111 ++++++++++++++++++ editor/src/messages/portfolio/document/mod.rs | 2 + .../document/overlays/guide_overlays.rs | 39 +++--- .../overlays/overlays_message_handler.rs | 5 +- .../snapping/guide_snapper.rs | 37 +++--- .../tool/tool_messages/select_tool.rs | 56 +++------ 12 files changed, 198 insertions(+), 111 deletions(-) create mode 100644 editor/src/messages/portfolio/document/guide_message.rs create mode 100644 editor/src/messages/portfolio/document/guide_message_handler.rs diff --git a/editor/src/consts.rs b/editor/src/consts.rs index a014e8891f..313a39651e 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -66,6 +66,9 @@ pub const SELECTION_DRAG_ANGLE: f64 = 90.; pub const LAYER_ORIGIN_CROSS_DIAMETER: f64 = 10.; pub const LAYER_ORIGIN_CROSS_THICKNESS: f64 = 1.; +// GUIDES +pub const GUIDE_HIT_TOLERANCE: f64 = 5.; + // PIVOT pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.; pub const PIVOT_CROSSHAIR_LENGTH: f64 = 9.; diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index a995742ddf..05ada91f95 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -287,7 +287,7 @@ impl Dispatcher { menu_bar_message_handler.canvas_tilted = document.document_ptz.tilt() != 0.; menu_bar_message_handler.canvas_flipped = document.document_ptz.flip; menu_bar_message_handler.rulers_visible = document.rulers_visible; - menu_bar_message_handler.guides_visible = document.guides_visible; + menu_bar_message_handler.guides_visible = document.guide_handler.guides_visible; menu_bar_message_handler.node_graph_open = document.is_graph_overlay_open(); menu_bar_message_handler.has_selected_nodes = selected_nodes.selected_nodes().next().is_some(); menu_bar_message_handler.has_selected_layers = selected_nodes.selected_visible_layers(&document.network_interface).next().is_some(); diff --git a/editor/src/messages/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/menu_bar/menu_bar_message_handler.rs index de0b18e533..5d25b09c55 100644 --- a/editor/src/messages/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/menu_bar/menu_bar_message_handler.rs @@ -1,6 +1,7 @@ use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::layout::utility_types::widget_prelude::*; +use crate::messages::portfolio::document::guide_message::GuideMessage; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; use crate::messages::prelude::*; use graphene_std::vector::misc::BooleanOperation; @@ -639,7 +640,7 @@ impl LayoutHolder for MenuBarMessageHandler { MenuListEntry::new("Guides") .label("Guides") .icon(if self.guides_visible { "CheckboxChecked" } else { "CheckboxUnchecked" }) - .on_commit(|_| DocumentMessage::ToggleGuidesVisibility.into()) + .on_commit(|_| GuideMessage::ToggleGuidesVisibility.into()) .disabled(no_active_document), ], ]) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index f6807909af..967eafcaab 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use super::utility_types::misc::{GroupFolderType, SnappingState}; use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::portfolio::document::data_panel::DataPanelMessage; +use crate::messages::portfolio::document::guide_message::GuideMessage; use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping}; @@ -41,6 +42,7 @@ pub enum DocumentMessage { DataPanel(DataPanelMessage), #[child] Resource(ResourceMessage), + Guide(GuideMessage), // Messages AlignSelectedLayers { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 2cd34380f5..20fae105e6 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2,7 +2,6 @@ use super::DocumentHistory; use super::document_diff::diff_networks; use super::node_graph::document_node_definitions; use super::utility_types::error::EditorError; -use super::utility_types::guide::{Guide, GuideId}; use super::utility_types::misc::{GroupFolderType, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS, SnappingOptions, SnappingState}; use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, LayerStructureEntry, SelectedNodes}; @@ -15,6 +14,7 @@ use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler}; use crate::messages::portfolio::document::graph_operation::utility_types::{ModifyInputsContext, TransformIn}; +use crate::messages::portfolio::document::guide_message_handler::{GuideMessageContext, GuideMessageHandler}; use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; @@ -86,6 +86,8 @@ pub struct DocumentMessageHandler { pub properties_panel_message_handler: PropertiesPanelMessageHandler, #[serde(skip)] pub data_panel_message_handler: DataPanelMessageHandler, + #[serde(flatten)] + pub guide_handler: GuideMessageHandler, // ============================================ // Fields that are saved in the document format @@ -157,18 +159,6 @@ pub struct DocumentMessageHandler { /// If the user clicks or Ctrl-clicks one layer, it becomes the start of the range selection and then Shift-clicking another layer selects all layers between the start and end. #[serde(skip)] layer_range_selection_reference: Option, - /// List of horizontal guide lines in document space. - #[serde(default)] - pub horizontal_guides: Vec, - /// List of vertical guide lines in document space. - #[serde(default)] - pub vertical_guides: Vec, - /// Whether guide lines are visible in the viewport. - #[serde(default = "default_guides_visible")] - pub guides_visible: bool, - /// ID of the currently hovered guide for visual feedback. - #[serde(skip)] - pub hovered_guide_id: Option, /// Whether or not the editor has executed the network to render the document yet. If this is opened as an inactive tab, it won't be loaded initially because the active tab is prioritized. #[serde(skip)] pub is_loaded: bool, @@ -185,6 +175,7 @@ impl Default for DocumentMessageHandler { overlays_message_handler: OverlaysMessageHandler::default(), properties_panel_message_handler: PropertiesPanelMessageHandler::default(), data_panel_message_handler: DataPanelMessageHandler::default(), + guide_handler: GuideMessageHandler::default(), // ============================================ // Fields that are saved in the document format // ============================================ @@ -213,10 +204,7 @@ impl Default for DocumentMessageHandler { saved_hash: None, auto_saved_hash: None, layer_range_selection_reference: None, - horizontal_guides: Vec::new(), - vertical_guides: Vec::new(), - guides_visible: true, - hovered_guide_id: None, + is_loaded: false, } } @@ -254,6 +242,14 @@ impl MessageHandler> for DocumentMes self.navigation_handler.process_message(message, responses, context); } + DocumentMessage::Guide(message) => { + let context = GuideMessageContext { + navigation_handler: &self.navigation_handler, + document_ptz: &self.document_ptz, + viewport, + }; + self.guide_handler.process_message(message, responses, context); + } DocumentMessage::Overlays(message) => { let visibility_settings = self.overlays_visibility_settings; @@ -1768,6 +1764,7 @@ impl MessageHandler> for DocumentMes ZoomCanvasTo200Percent, ZoomCanvasToFitAll, ); + common.extend(self.guide_handler.actions()); // Additional actions available on desktop #[cfg(not(target_family = "wasm"))] @@ -3806,10 +3803,6 @@ fn default_document_network_interface() -> NodeNetworkInterface { network_interface } -fn default_guides_visible() -> bool { - true -} - /// Targets for the [`ClickXRayIter`]. In order to reduce computation, we prefer just a point/path test where possible. #[derive(Clone)] enum XRayTarget { diff --git a/editor/src/messages/portfolio/document/guide_message.rs b/editor/src/messages/portfolio/document/guide_message.rs new file mode 100644 index 0000000000..6fb54c90ff --- /dev/null +++ b/editor/src/messages/portfolio/document/guide_message.rs @@ -0,0 +1,14 @@ +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::guide::{GuideDirection, GuideId}; +use crate::messages::prelude::*; + +#[impl_message(Message, DocumentMessage, Guide)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum GuideMessage { + CreateGuide { id: GuideId, direction: GuideDirection, mouse_x: f64, mouse_y: f64 }, + MoveGuide { id: GuideId, mouse_x: f64, mouse_y: f64 }, + DeleteGuide { id: GuideId }, + GuideOverlays { context: OverlayContext }, + ToggleGuidesVisibility, + SetHoveredGuide { id: Option }, +} diff --git a/editor/src/messages/portfolio/document/guide_message_handler.rs b/editor/src/messages/portfolio/document/guide_message_handler.rs new file mode 100644 index 0000000000..b3ae51e1df --- /dev/null +++ b/editor/src/messages/portfolio/document/guide_message_handler.rs @@ -0,0 +1,111 @@ +use super::utility_types::guide::{Guide, GuideDirection, GuideId}; +use crate::messages::portfolio::document::guide_message::{GuideMessage, GuideMessageDiscriminant}; +use crate::messages::portfolio::document::overlays::guide_overlays::guide_overlay; +use crate::messages::portfolio::document::utility_types::misc::PTZ; +use crate::messages::prelude::*; +use glam::DVec2; + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)] +#[serde(default)] +pub struct GuideMessageHandler { + #[serde(default)] + pub guides: Vec, + #[serde(default = "default_guides_visible")] + pub guides_visible: bool, + #[serde(skip)] + pub hovered_guide_id: Option, +} + +fn default_guides_visible() -> bool { + true +} + +impl Default for GuideMessageHandler { + fn default() -> Self { + Self { + guides: Vec::new(), + guides_visible: true, + hovered_guide_id: None, + } + } +} + +#[derive(ExtractField)] +pub struct GuideMessageContext<'a> { + pub navigation_handler: &'a NavigationMessageHandler, + pub document_ptz: &'a PTZ, + pub viewport: &'a ViewportMessageHandler, +} + +#[message_handler_data] +impl MessageHandler> for GuideMessageHandler { + fn actions(&self) -> ActionList { + actions!(GuideMessageDiscriminant; ToggleGuidesVisibility) + } + + fn process_message(&mut self, message: GuideMessage, responses: &mut VecDeque, context: GuideMessageContext) { + let GuideMessageContext { + navigation_handler, + document_ptz, + viewport, + } = context; + + match message { + GuideMessage::CreateGuide { id, direction, mouse_x, mouse_y } => { + let document_to_viewport = navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), document_ptz); + let viewport_to_document = document_to_viewport.inverse(); + + let viewport_point = DVec2::new(mouse_x, mouse_y); + let document_point = viewport_to_document.transform_point2(viewport_point); + + let document_position = match direction { + GuideDirection::Horizontal => document_point.y, + GuideDirection::Vertical => document_point.x, + }; + + let guide = Guide::with_id(id, direction, document_position); + self.guides.push(guide); + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + } + GuideMessage::MoveGuide { id, mouse_x, mouse_y } => { + let document_to_viewport = navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), document_ptz); + let viewport_to_document = document_to_viewport.inverse(); + + let viewport_point = DVec2::new(mouse_x, mouse_y); + let document_point = viewport_to_document.transform_point2(viewport_point); + + if let Some(guide) = self.guides.iter_mut().find(|guide| guide.id == id) { + guide.position = match guide.direction { + GuideDirection::Horizontal => document_point.y, + GuideDirection::Vertical => document_point.x, + }; + } + responses.add(OverlaysMessage::Draw); + } + GuideMessage::DeleteGuide { id } => { + self.guides.retain(|g| g.id != id); + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + } + GuideMessage::GuideOverlays { context: mut overlay_context } => { + if self.guides_visible { + let document_to_viewport = navigation_handler.calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), document_ptz); + guide_overlay(self, &mut overlay_context, document_to_viewport); + } + } + GuideMessage::ToggleGuidesVisibility => { + self.guides_visible = !self.guides_visible; + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + responses.add(MenuBarMessage::SendLayout); + } + GuideMessage::SetHoveredGuide { id } => { + if self.hovered_guide_id != id { + self.hovered_guide_id = id; + responses.add(OverlaysMessage::Draw); + } + } + } + } +} diff --git a/editor/src/messages/portfolio/document/mod.rs b/editor/src/messages/portfolio/document/mod.rs index fb8749ce73..f44926b915 100644 --- a/editor/src/messages/portfolio/document/mod.rs +++ b/editor/src/messages/portfolio/document/mod.rs @@ -7,6 +7,8 @@ mod storage_tests; pub mod data_panel; pub mod graph_operation; +pub mod guide_message; +pub mod guide_message_handler; pub mod navigation; pub mod node_graph; pub mod overlays; diff --git a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs index fcaaf3c957..cffc29a13d 100644 --- a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs @@ -1,7 +1,8 @@ use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50}; +use crate::messages::portfolio::document::guide_message_handler::GuideMessageHandler; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::prelude::DocumentMessageHandler; -use glam::DVec2; +use crate::messages::portfolio::document::utility_types::guide::GuideDirection; +use glam::{DAffine2, DVec2}; fn extend_line_to_viewport(point: DVec2, direction: DVec2, viewport_size: DVec2) -> Option<(DVec2, DVec2)> { let dir = direction.try_normalize()?; @@ -31,35 +32,23 @@ fn extend_line_to_viewport(point: DVec2, direction: DVec2, viewport_size: DVec2) Some((start, end)) } -pub fn guide_overlay(document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { - let document_to_viewport = document - .navigation_handler - .calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), &document.document_ptz); - +pub fn guide_overlay(guide_handler: &GuideMessageHandler, overlay_context: &mut OverlayContext, document_to_viewport: DAffine2) { let viewport_size: DVec2 = overlay_context.viewport.size().into(); - for guide in &document.horizontal_guides { - let doc_point = DVec2::new(0.0, guide.position); - let doc_direction = DVec2::X; // Horizontal guides run in the X direction in document space - - let viewport_point = document_to_viewport.transform_point2(doc_point); - let viewport_direction = document_to_viewport.transform_vector2(doc_direction); - - let color = if document.hovered_guide_id == Some(guide.id) { COLOR_OVERLAY_BLUE_50 } else { COLOR_OVERLAY_BLUE }; - - if let Some((start, end)) = extend_line_to_viewport(viewport_point, viewport_direction, viewport_size) { - overlay_context.line(start, end, Some(color), None); - } - } - - for guide in &document.vertical_guides { - let doc_point = DVec2::new(guide.position, 0.0); - let doc_direction = DVec2::Y; + for guide in &guide_handler.guides { + let (doc_point, doc_direction) = match guide.direction { + GuideDirection::Horizontal => (DVec2::new(0.0, guide.position), DVec2::X), + GuideDirection::Vertical => (DVec2::new(guide.position, 0.0), DVec2::Y), + }; let viewport_point = document_to_viewport.transform_point2(doc_point); let viewport_direction = document_to_viewport.transform_vector2(doc_direction); - let color = if document.hovered_guide_id == Some(guide.id) { COLOR_OVERLAY_BLUE_50 } else { COLOR_OVERLAY_BLUE }; + let color = if guide_handler.hovered_guide_id == Some(guide.id) { + COLOR_OVERLAY_BLUE_50 + } else { + COLOR_OVERLAY_BLUE + }; if let Some((start, end)) = extend_line_to_viewport(viewport_point, viewport_direction, viewport_size) { overlay_context.line(start, end, Some(color), None); diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index aa5887aa8e..c2ce19d533 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -1,4 +1,5 @@ use super::utility_types::{OverlayProvider, OverlaysVisibilitySettings}; +use crate::messages::portfolio::document::guide_message::GuideMessage; use crate::messages::prelude::*; #[derive(ExtractField)] @@ -63,7 +64,7 @@ impl MessageHandler> for OverlaysMes viewport: *viewport, }, }); - responses.add(DocumentMessage::GuideOverlays { + responses.add(GuideMessage::GuideOverlays { context: OverlayContext { render_context: canvas_context.clone(), visibility_settings: visibility_settings.clone(), @@ -87,7 +88,7 @@ impl MessageHandler> for OverlaysMes if visibility_settings.all() { responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() }); - responses.add(DocumentMessage::GuideOverlays { context: overlay_context.clone() }); + responses.add(GuideMessage::GuideOverlays { context: overlay_context.clone() }); for provider in &self.overlay_providers { responses.add(provider(overlay_context.clone())); diff --git a/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs index 17830b53b6..81fb4775d6 100644 --- a/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs @@ -1,4 +1,5 @@ use super::*; +use crate::messages::portfolio::document::utility_types::guide::GuideDirection; use crate::messages::portfolio::document::utility_types::misc::{GuideSnapTarget, SnapTarget}; use glam::DVec2; use graphene_std::renderer::Quad; @@ -11,16 +12,16 @@ impl GuideSnapper { let document = snap_data.document; let mut lines = Vec::new(); - if !document.guides_visible || !document.snapping_state.guides { + if !document.guide_handler.guides_visible || !document.snapping_state.guides { return lines; } - for guide in &document.horizontal_guides { - lines.push((DVec2::new(0.0, guide.position), DVec2::X, GuideSnapTarget::Horizontal)); - } - - for guide in &document.vertical_guides { - lines.push((DVec2::new(guide.position, 0.0), DVec2::Y, GuideSnapTarget::Vertical)); + for guide in &document.guide_handler.guides { + let (point, direction, snap_target) = match guide.direction { + GuideDirection::Horizontal => (DVec2::new(0.0, guide.position), DVec2::X, GuideSnapTarget::Horizontal), + GuideDirection::Vertical => (DVec2::new(guide.position, 0.0), DVec2::Y, GuideSnapTarget::Vertical), + }; + lines.push((point, direction, snap_target)); } lines @@ -57,27 +58,19 @@ impl GuideSnapper { let tolerance = snap_tolerance(document); let mut guide_lines: Vec = Vec::new(); - for guide in &document.horizontal_guides { - guide_lines.push(SnappedLine { - point: SnappedPoint { - snapped_point_document: DVec2::new(0.0, guide.position), - source: point.source, - tolerance, - ..Default::default() - }, - direction: DVec2::X, - }); - } - - for guide in &document.vertical_guides { + for guide in &document.guide_handler.guides { + let (snapped_point_document, direction) = match guide.direction { + GuideDirection::Horizontal => (DVec2::new(0.0, guide.position), DVec2::X), + GuideDirection::Vertical => (DVec2::new(guide.position, 0.0), DVec2::Y), + }; guide_lines.push(SnappedLine { point: SnappedPoint { - snapped_point_document: DVec2::new(guide.position, 0.0), + snapped_point_document, source: point.source, tolerance, ..Default::default() }, - direction: DVec2::Y, + direction, }); } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index f5c2358571..6e02ac4ac6 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -4,6 +4,7 @@ use super::tool_prelude::*; use crate::consts::*; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::guide_message::GuideMessage; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::guide::{GuideDirection, GuideId}; @@ -733,9 +734,7 @@ pub fn create_bounding_box_transform(document: &DocumentMessageHandler) -> DAffi } fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2, viewport: &ViewportMessageHandler) -> Option<(GuideId, GuideDirection)> { - const HIT_TOLERANCE: f64 = 5.0; - - if !document.guides_visible { + if !document.guide_handler.guides_visible { return None; } @@ -743,38 +742,21 @@ fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2, v .navigation_handler .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); - for guide in document.horizontal_guides.iter().rev() { - let doc_point = DVec2::new(0.0, guide.position); - let doc_direction = DVec2::X; - - let viewport_point = transform.transform_point2(doc_point); - let viewport_direction = transform.transform_vector2(doc_direction); - - if viewport_direction.length_squared() > f64::EPSILON { - let dir_normalized = viewport_direction.normalize(); - let to_mouse = viewport_position - viewport_point; - let perpendicular_dist = (to_mouse.x * dir_normalized.y - to_mouse.y * dir_normalized.x).abs(); - - if perpendicular_dist <= HIT_TOLERANCE { - return Some((guide.id, GuideDirection::Horizontal)); - } - } - } - - for guide in document.vertical_guides.iter().rev() { - let doc_point = DVec2::new(guide.position, 0.0); - let doc_direction = DVec2::Y; + for guide in document.guide_handler.guides.iter().rev() { + let (doc_point, doc_direction) = match guide.direction { + GuideDirection::Horizontal => (DVec2::new(0.0, guide.position), DVec2::X), + GuideDirection::Vertical => (DVec2::new(guide.position, 0.0), DVec2::Y), + }; let viewport_point = transform.transform_point2(doc_point); let viewport_direction = transform.transform_vector2(doc_direction); - if viewport_direction.length_squared() > f64::EPSILON { - let dir_normalized = viewport_direction.normalize(); + if let Some(dir_normalized) = viewport_direction.try_normalize() { let to_mouse = viewport_position - viewport_point; - let perpendicular_dist = (to_mouse.x * dir_normalized.y - to_mouse.y * dir_normalized.x).abs(); + let perpendicular_dist = to_mouse.perp_dot(dir_normalized).abs(); - if perpendicular_dist <= HIT_TOLERANCE { - return Some((guide.id, GuideDirection::Vertical)); + if perpendicular_dist <= GUIDE_HIT_TOLERANCE { + return Some((guide.id, guide.direction)); } } } @@ -1238,10 +1220,7 @@ impl Fsm for SelectToolFsmState { tool_data.dragging_guide_id = Some(guide_id); tool_data.dragging_guide_direction = Some(direction); - let original_position = match direction { - GuideDirection::Horizontal => document.horizontal_guides.iter().find(|g| g.id == guide_id).map(|g| g.position), - GuideDirection::Vertical => document.vertical_guides.iter().find(|g| g.id == guide_id).map(|g| g.position), - }; + let original_position = document.guide_handler.guides.iter().find(|g| g.id == guide_id).map(|g| g.position); tool_data.guide_drag_start_position = original_position; SelectToolFsmState::DraggingGuide { guide_id, direction } } @@ -1348,7 +1327,7 @@ impl Fsm for SelectToolFsmState { tool_data.drag_current = input.mouse.position; // MoveGuide expects viewport coordinates and does the conversion internally - responses.add(DocumentMessage::MoveGuide { + responses.add(GuideMessage::MoveGuide { id: guide_id, mouse_x: input.mouse.position.x, mouse_y: input.mouse.position.y, @@ -1373,10 +1352,9 @@ impl Fsm for SelectToolFsmState { let outside_viewport = input.mouse.position.x < 0.0 || input.mouse.position.y < 0.0 || input.mouse.position.x > viewport_size.x || input.mouse.position.y > viewport_size.y; if outside_viewport { - responses.add(DocumentMessage::DeleteGuide { id: guide_id }); + responses.add(GuideMessage::DeleteGuide { id: guide_id }); } else { - // MoveGuide expects viewport coordinates and does the conversion internally - responses.add(DocumentMessage::MoveGuide { + responses.add(GuideMessage::MoveGuide { id: guide_id, mouse_x: input.mouse.position.x, mouse_y: input.mouse.position.y, @@ -1580,9 +1558,9 @@ impl Fsm for SelectToolFsmState { GuideDirection::Horizontal => MouseCursorIcon::NSResize, GuideDirection::Vertical => MouseCursorIcon::EWResize, }; - responses.add(DocumentMessage::SetHoveredGuide { id: Some(guide_id) }); + responses.add(GuideMessage::SetHoveredGuide { id: Some(guide_id) }); } else { - responses.add(DocumentMessage::SetHoveredGuide { id: None }); + responses.add(GuideMessage::SetHoveredGuide { id: None }); } // Generate the hover outline From edd35a0c2557c9e038462f80335c9fb33836a62d Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Tue, 27 Jan 2026 14:27:47 +0000 Subject: [PATCH 13/26] remove unnecessary code --- .../tool/common_functionality/snapping/guide_snapper.rs | 1 - editor/src/messages/tool/tool_messages/select_tool.rs | 3 --- frontend/src/components/panels/Document.svelte | 2 -- frontend/src/components/widgets/inputs/RulerInput.svelte | 1 + 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs index 81fb4775d6..a895aee852 100644 --- a/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs @@ -74,7 +74,6 @@ impl GuideSnapper { }); } - // Reuse the generic intersection finder from snapping module if let Some(intersection) = super::get_line_intersection(point.document_point, &guide_lines, SnapTarget::Guide(GuideSnapTarget::Intersection)) { if intersection.distance <= tolerance { snap_results.points.push(intersection); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 6e02ac4ac6..fb56a7b920 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1314,7 +1314,6 @@ impl Fsm for SelectToolFsmState { let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } - // Guide dragging - abort (SelectToolFsmState::DraggingGuide { .. }, SelectToolMessage::Abort) => { tool_data.dragging_guide_id = None; tool_data.dragging_guide_direction = None; @@ -1322,11 +1321,9 @@ impl Fsm for SelectToolFsmState { let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } - // Guide dragging - pointer move (SelectToolFsmState::DraggingGuide { guide_id, direction }, SelectToolMessage::PointerMove { .. }) => { tool_data.drag_current = input.mouse.position; - // MoveGuide expects viewport coordinates and does the conversion internally responses.add(GuideMessage::MoveGuide { id: guide_id, mouse_x: input.mouse.position.x, diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 13368cffee..f34862d3b2 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -165,8 +165,6 @@ editor.panCanvas(0, -delta * scrollbarMultiplier.y); } - // Guide Drag Utilities - type GuideDirection = "Horizontal" | "Vertical"; type GuideEditor = { diff --git a/frontend/src/components/widgets/inputs/RulerInput.svelte b/frontend/src/components/widgets/inputs/RulerInput.svelte index f7d707ad9b..5d3e4c7ef2 100644 --- a/frontend/src/components/widgets/inputs/RulerInput.svelte +++ b/frontend/src/components/widgets/inputs/RulerInput.svelte @@ -213,6 +213,7 @@ } // Get the viewport element to compute positions relative to it + if (!viewportEl) { isDragging = false; return; From 2aa267e4bfbc33c5df6ede38bada87ecc6d376f1 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Wed, 6 May 2026 12:52:45 +0000 Subject: [PATCH 14/26] Name change --- editor/src/dispatcher.rs | 4 +- .../menu_bar/menu_bar_message_handler.rs | 12 +- .../portfolio/document/document_message.rs | 4 +- .../document/document_message_handler.rs | 18 ++- .../portfolio/document/guide_message.rs | 18 +-- .../document/guide_message_handler.rs | 125 +++++++++--------- .../document/overlays/guide_overlays.rs | 16 +-- .../overlays/overlays_message_handler.rs | 6 +- .../portfolio/document/utility_types/guide.rs | 26 ++-- .../portfolio/document/utility_types/misc.rs | 20 +-- .../tool/common_functionality/snapping.rs | 6 +- .../snapping/guide_snapper.rs | 36 ++--- .../tool/tool_messages/select_tool.rs | 113 ++++++---------- 13 files changed, 190 insertions(+), 214 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index 05ada91f95..a6418a3e6c 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -287,7 +287,7 @@ impl Dispatcher { menu_bar_message_handler.canvas_tilted = document.document_ptz.tilt() != 0.; menu_bar_message_handler.canvas_flipped = document.document_ptz.flip; menu_bar_message_handler.rulers_visible = document.rulers_visible; - menu_bar_message_handler.guides_visible = document.guide_handler.guides_visible; + menu_bar_message_handler.guide_lines_visible = document.guide_lines_message_handler.guide_lines_visible; menu_bar_message_handler.node_graph_open = document.is_graph_overlay_open(); menu_bar_message_handler.has_selected_nodes = selected_nodes.selected_nodes().next().is_some(); menu_bar_message_handler.has_selected_layers = selected_nodes.selected_visible_layers(&document.network_interface).next().is_some(); @@ -298,7 +298,7 @@ impl Dispatcher { menu_bar_message_handler.canvas_tilted = false; menu_bar_message_handler.canvas_flipped = false; menu_bar_message_handler.rulers_visible = false; - menu_bar_message_handler.guides_visible = false; + menu_bar_message_handler.guide_lines_visible = false; menu_bar_message_handler.node_graph_open = false; menu_bar_message_handler.has_selected_nodes = false; menu_bar_message_handler.has_selected_layers = false; diff --git a/editor/src/messages/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/menu_bar/menu_bar_message_handler.rs index 5d25b09c55..d44d6c4fee 100644 --- a/editor/src/messages/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/menu_bar/menu_bar_message_handler.rs @@ -1,7 +1,7 @@ use crate::messages::debug::utility_types::MessageLoggingVerbosity; use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::layout::utility_types::widget_prelude::*; -use crate::messages::portfolio::document::guide_message::GuideMessage; +use crate::messages::portfolio::document::guide_message::GuideLineMessage; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; use crate::messages::prelude::*; use graphene_std::vector::misc::BooleanOperation; @@ -12,7 +12,7 @@ pub struct MenuBarMessageHandler { pub canvas_tilted: bool, pub canvas_flipped: bool, pub rulers_visible: bool, - pub guides_visible: bool, + pub guide_lines_visible: bool, pub node_graph_open: bool, pub has_selected_nodes: bool, pub has_selected_layers: bool, @@ -637,10 +637,10 @@ impl LayoutHolder for MenuBarMessageHandler { .tooltip_shortcut(action_shortcut!(PortfolioMessageDiscriminant::ToggleRulers)) .on_commit(|_| PortfolioMessage::ToggleRulers.into()) .disabled(no_active_document), - MenuListEntry::new("Guides") - .label("Guides") - .icon(if self.guides_visible { "CheckboxChecked" } else { "CheckboxUnchecked" }) - .on_commit(|_| GuideMessage::ToggleGuidesVisibility.into()) + MenuListEntry::new("Guide Lines") + .label("Guide Lines") + .icon(if self.guide_lines_visible { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .on_commit(|_| GuideLineMessage::ToggleGuideLinesVisibility.into()) .disabled(no_active_document), ], ]) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 967eafcaab..fb36e38c9a 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use super::utility_types::misc::{GroupFolderType, SnappingState}; use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::portfolio::document::data_panel::DataPanelMessage; -use crate::messages::portfolio::document::guide_message::GuideMessage; +use crate::messages::portfolio::document::guide_message::GuideLineMessage; use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping}; @@ -42,7 +42,7 @@ pub enum DocumentMessage { DataPanel(DataPanelMessage), #[child] Resource(ResourceMessage), - Guide(GuideMessage), + GuideLine(GuideLineMessage), // Messages AlignSelectedLayers { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 20fae105e6..f257bc0673 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -14,7 +14,7 @@ use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler}; use crate::messages::portfolio::document::graph_operation::utility_types::{ModifyInputsContext, TransformIn}; -use crate::messages::portfolio::document::guide_message_handler::{GuideMessageContext, GuideMessageHandler}; +use crate::messages::portfolio::document::guide_message_handler::{GuideLinesMessageContext, GuideLinesMessageHandler}; use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::node_graph::utility_types::FrontendGraphDataType; @@ -87,7 +87,7 @@ pub struct DocumentMessageHandler { #[serde(skip)] pub data_panel_message_handler: DataPanelMessageHandler, #[serde(flatten)] - pub guide_handler: GuideMessageHandler, + pub guide_lines_message_handler: GuideLinesMessageHandler, // ============================================ // Fields that are saved in the document format @@ -175,7 +175,7 @@ impl Default for DocumentMessageHandler { overlays_message_handler: OverlaysMessageHandler::default(), properties_panel_message_handler: PropertiesPanelMessageHandler::default(), data_panel_message_handler: DataPanelMessageHandler::default(), - guide_handler: GuideMessageHandler::default(), + guide_lines_message_handler: GuideLinesMessageHandler::default(), // ============================================ // Fields that are saved in the document format // ============================================ @@ -242,13 +242,11 @@ impl MessageHandler> for DocumentMes self.navigation_handler.process_message(message, responses, context); } - DocumentMessage::Guide(message) => { - let context = GuideMessageContext { - navigation_handler: &self.navigation_handler, - document_ptz: &self.document_ptz, - viewport, + DocumentMessage::GuideLine(message) => { + let context = GuideLinesMessageContext { + document_to_viewport: self.metadata().document_to_viewport, }; - self.guide_handler.process_message(message, responses, context); + self.guide_lines_message_handler.process_message(message, responses, context); } DocumentMessage::Overlays(message) => { let visibility_settings = self.overlays_visibility_settings; @@ -1764,7 +1762,7 @@ impl MessageHandler> for DocumentMes ZoomCanvasTo200Percent, ZoomCanvasToFitAll, ); - common.extend(self.guide_handler.actions()); + common.extend(self.guide_lines_message_handler.actions()); // Additional actions available on desktop #[cfg(not(target_family = "wasm"))] diff --git a/editor/src/messages/portfolio/document/guide_message.rs b/editor/src/messages/portfolio/document/guide_message.rs index 6fb54c90ff..85e504ae6a 100644 --- a/editor/src/messages/portfolio/document/guide_message.rs +++ b/editor/src/messages/portfolio/document/guide_message.rs @@ -1,14 +1,14 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::portfolio::document::utility_types::guide::{GuideDirection, GuideId}; +use crate::messages::portfolio::document::utility_types::guide::{GuideLineDirection, GuideLineId}; use crate::messages::prelude::*; -#[impl_message(Message, DocumentMessage, Guide)] +#[impl_message(Message, DocumentMessage, GuideLine)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub enum GuideMessage { - CreateGuide { id: GuideId, direction: GuideDirection, mouse_x: f64, mouse_y: f64 }, - MoveGuide { id: GuideId, mouse_x: f64, mouse_y: f64 }, - DeleteGuide { id: GuideId }, - GuideOverlays { context: OverlayContext }, - ToggleGuidesVisibility, - SetHoveredGuide { id: Option }, +pub enum GuideLineMessage { + CreateGuideLine { id: GuideLineId, direction: GuideLineDirection, mouse_x: f64, mouse_y: f64 }, + MoveGuideLine { id: GuideLineId, mouse_x: f64, mouse_y: f64 }, + DeleteGuideLine { id: GuideLineId }, + GuideLinesOverlays { context: OverlayContext }, + ToggleGuideLinesVisibility, + SetHoveredGuideLine { id: Option }, } diff --git a/editor/src/messages/portfolio/document/guide_message_handler.rs b/editor/src/messages/portfolio/document/guide_message_handler.rs index b3ae51e1df..399465a9f5 100644 --- a/editor/src/messages/portfolio/document/guide_message_handler.rs +++ b/editor/src/messages/portfolio/document/guide_message_handler.rs @@ -1,108 +1,115 @@ -use super::utility_types::guide::{Guide, GuideDirection, GuideId}; -use crate::messages::portfolio::document::guide_message::{GuideMessage, GuideMessageDiscriminant}; -use crate::messages::portfolio::document::overlays::guide_overlays::guide_overlay; -use crate::messages::portfolio::document::utility_types::misc::PTZ; +use super::utility_types::guide::{GuideLine, GuideLineDirection, GuideLineId}; +use crate::messages::portfolio::document::guide_message::{GuideLineMessage, GuideLineMessageDiscriminant}; +use crate::messages::portfolio::document::overlays::guide_overlays::guide_lines_overlay; use crate::messages::prelude::*; -use glam::DVec2; +use glam::{DAffine2, DVec2}; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)] #[serde(default)] -pub struct GuideMessageHandler { +pub struct GuideLinesMessageHandler { #[serde(default)] - pub guides: Vec, - #[serde(default = "default_guides_visible")] - pub guides_visible: bool, + pub guide_lines: Vec, + #[serde(default = "default_guide_lines_visible")] + pub guide_lines_visible: bool, #[serde(skip)] - pub hovered_guide_id: Option, + pub hovered_guide_line_id: Option, } -fn default_guides_visible() -> bool { - true +fn default_guide_lines_visible() -> bool { + GuideLinesMessageHandler::default().guide_lines_visible } -impl Default for GuideMessageHandler { +impl GuideLinesMessageHandler { + pub fn hit_test(&self, viewport_position: DVec2, document_to_viewport: DAffine2) -> Option<(GuideLineId, GuideLineDirection)> { + let viewport_to_document = document_to_viewport.inverse(); + let document_position = viewport_to_document.transform_point2(viewport_position); + let document_scale = viewport_to_document.matrix2.determinant().abs().sqrt(); + let tolerance = crate::consts::GUIDE_HIT_TOLERANCE * document_scale; + + self.guide_lines + .iter() + .find(|guide_line| match guide_line.direction { + GuideLineDirection::Horizontal => (guide_line.position - document_position.y).abs() < tolerance, + GuideLineDirection::Vertical => (guide_line.position - document_position.x).abs() < tolerance, + }) + .map(|guide_line| (guide_line.id, guide_line.direction)) + } +} + +impl Default for GuideLinesMessageHandler { fn default() -> Self { Self { - guides: Vec::new(), - guides_visible: true, - hovered_guide_id: None, + guide_lines: Vec::new(), + guide_lines_visible: true, + hovered_guide_line_id: None, } } } #[derive(ExtractField)] -pub struct GuideMessageContext<'a> { - pub navigation_handler: &'a NavigationMessageHandler, - pub document_ptz: &'a PTZ, - pub viewport: &'a ViewportMessageHandler, +pub struct GuideLinesMessageContext { + pub document_to_viewport: DAffine2, } #[message_handler_data] -impl MessageHandler> for GuideMessageHandler { +impl MessageHandler for GuideLinesMessageHandler { fn actions(&self) -> ActionList { - actions!(GuideMessageDiscriminant; ToggleGuidesVisibility) + actions!(GuideLineMessageDiscriminant; ToggleGuideLinesVisibility) } - fn process_message(&mut self, message: GuideMessage, responses: &mut VecDeque, context: GuideMessageContext) { - let GuideMessageContext { - navigation_handler, - document_ptz, - viewport, - } = context; + fn process_message(&mut self, message: GuideLineMessage, responses: &mut VecDeque, context: GuideLinesMessageContext) { + let GuideLinesMessageContext { document_to_viewport } = context; + let viewport_to_document = document_to_viewport.inverse(); - match message { - GuideMessage::CreateGuide { id, direction, mouse_x, mouse_y } => { - let document_to_viewport = navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), document_ptz); - let viewport_to_document = document_to_viewport.inverse(); + let document_point = |mouse_x, mouse_y| { + let viewport_point = DVec2::new(mouse_x, mouse_y); + viewport_to_document.transform_point2(viewport_point) + }; - let viewport_point = DVec2::new(mouse_x, mouse_y); - let document_point = viewport_to_document.transform_point2(viewport_point); + match message { + GuideLineMessage::CreateGuideLine { id, direction, mouse_x, mouse_y } => { + let document_point = document_point(mouse_x, mouse_y); let document_position = match direction { - GuideDirection::Horizontal => document_point.y, - GuideDirection::Vertical => document_point.x, + GuideLineDirection::Horizontal => document_point.y, + GuideLineDirection::Vertical => document_point.x, }; - let guide = Guide::with_id(id, direction, document_position); - self.guides.push(guide); + let guide_line = GuideLine::with_id(id, direction, document_position); + self.guide_lines.push(guide_line); responses.add(OverlaysMessage::Draw); responses.add(PortfolioMessage::UpdateDocumentWidgets); } - GuideMessage::MoveGuide { id, mouse_x, mouse_y } => { - let document_to_viewport = navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), document_ptz); - let viewport_to_document = document_to_viewport.inverse(); - - let viewport_point = DVec2::new(mouse_x, mouse_y); - let document_point = viewport_to_document.transform_point2(viewport_point); + GuideLineMessage::MoveGuideLine { id, mouse_x, mouse_y } => { + let document_point = document_point(mouse_x, mouse_y); - if let Some(guide) = self.guides.iter_mut().find(|guide| guide.id == id) { - guide.position = match guide.direction { - GuideDirection::Horizontal => document_point.y, - GuideDirection::Vertical => document_point.x, + if let Some(guide_line) = self.guide_lines.iter_mut().find(|guide_line| guide_line.id == id) { + guide_line.position = match guide_line.direction { + GuideLineDirection::Horizontal => document_point.y, + GuideLineDirection::Vertical => document_point.x, }; } responses.add(OverlaysMessage::Draw); } - GuideMessage::DeleteGuide { id } => { - self.guides.retain(|g| g.id != id); + GuideLineMessage::DeleteGuideLine { id } => { + self.guide_lines.retain(|g| g.id != id); responses.add(OverlaysMessage::Draw); responses.add(PortfolioMessage::UpdateDocumentWidgets); } - GuideMessage::GuideOverlays { context: mut overlay_context } => { - if self.guides_visible { - let document_to_viewport = navigation_handler.calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), document_ptz); - guide_overlay(self, &mut overlay_context, document_to_viewport); + GuideLineMessage::GuideLinesOverlays { context: mut overlay_context } => { + if self.guide_lines_visible { + guide_lines_overlay(self, &mut overlay_context, document_to_viewport); } } - GuideMessage::ToggleGuidesVisibility => { - self.guides_visible = !self.guides_visible; + GuideLineMessage::ToggleGuideLinesVisibility => { + self.guide_lines_visible = !self.guide_lines_visible; responses.add(OverlaysMessage::Draw); responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(MenuBarMessage::SendLayout); } - GuideMessage::SetHoveredGuide { id } => { - if self.hovered_guide_id != id { - self.hovered_guide_id = id; + GuideLineMessage::SetHoveredGuideLine { id } => { + if self.hovered_guide_line_id != id { + self.hovered_guide_line_id = id; responses.add(OverlaysMessage::Draw); } } diff --git a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs index cffc29a13d..0a5a87ec28 100644 --- a/editor/src/messages/portfolio/document/overlays/guide_overlays.rs +++ b/editor/src/messages/portfolio/document/overlays/guide_overlays.rs @@ -1,7 +1,7 @@ use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50}; -use crate::messages::portfolio::document::guide_message_handler::GuideMessageHandler; +use crate::messages::portfolio::document::guide_message_handler::GuideLinesMessageHandler; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; -use crate::messages::portfolio::document::utility_types::guide::GuideDirection; +use crate::messages::portfolio::document::utility_types::guide::GuideLineDirection; use glam::{DAffine2, DVec2}; fn extend_line_to_viewport(point: DVec2, direction: DVec2, viewport_size: DVec2) -> Option<(DVec2, DVec2)> { @@ -32,19 +32,19 @@ fn extend_line_to_viewport(point: DVec2, direction: DVec2, viewport_size: DVec2) Some((start, end)) } -pub fn guide_overlay(guide_handler: &GuideMessageHandler, overlay_context: &mut OverlayContext, document_to_viewport: DAffine2) { +pub fn guide_lines_overlay(guide_lines_handler: &GuideLinesMessageHandler, overlay_context: &mut OverlayContext, document_to_viewport: DAffine2) { let viewport_size: DVec2 = overlay_context.viewport.size().into(); - for guide in &guide_handler.guides { - let (doc_point, doc_direction) = match guide.direction { - GuideDirection::Horizontal => (DVec2::new(0.0, guide.position), DVec2::X), - GuideDirection::Vertical => (DVec2::new(guide.position, 0.0), DVec2::Y), + for guide_line in &guide_lines_handler.guide_lines { + let (doc_point, doc_direction) = match guide_line.direction { + GuideLineDirection::Horizontal => (DVec2::new(0.0, guide_line.position), DVec2::X), + GuideLineDirection::Vertical => (DVec2::new(guide_line.position, 0.0), DVec2::Y), }; let viewport_point = document_to_viewport.transform_point2(doc_point); let viewport_direction = document_to_viewport.transform_vector2(doc_direction); - let color = if guide_handler.hovered_guide_id == Some(guide.id) { + let color = if guide_lines_handler.hovered_guide_line_id == Some(guide_line.id) { COLOR_OVERLAY_BLUE_50 } else { COLOR_OVERLAY_BLUE diff --git a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs index c2ce19d533..f8f076aa5a 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -1,5 +1,5 @@ use super::utility_types::{OverlayProvider, OverlaysVisibilitySettings}; -use crate::messages::portfolio::document::guide_message::GuideMessage; +use crate::messages::portfolio::document::guide_message::GuideLineMessage; use crate::messages::prelude::*; #[derive(ExtractField)] @@ -64,7 +64,7 @@ impl MessageHandler> for OverlaysMes viewport: *viewport, }, }); - responses.add(GuideMessage::GuideOverlays { + responses.add(GuideLineMessage::GuideLinesOverlays { context: OverlayContext { render_context: canvas_context.clone(), visibility_settings: visibility_settings.clone(), @@ -88,7 +88,7 @@ impl MessageHandler> for OverlaysMes if visibility_settings.all() { responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() }); - responses.add(GuideMessage::GuideOverlays { context: overlay_context.clone() }); + responses.add(GuideLineMessage::GuideLinesOverlays { context: overlay_context.clone() }); for provider in &self.overlay_providers { responses.add(provider(overlay_context.clone())); diff --git a/editor/src/messages/portfolio/document/utility_types/guide.rs b/editor/src/messages/portfolio/document/utility_types/guide.rs index e19ddaf69e..ef9acc106e 100644 --- a/editor/src/messages/portfolio/document/utility_types/guide.rs +++ b/editor/src/messages/portfolio/document/utility_types/guide.rs @@ -2,9 +2,9 @@ use crate::application::generate_uuid; #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize)] -pub struct GuideId(u64); +pub struct GuideLineId(u64); -impl GuideId { +impl GuideLineId { pub fn new() -> Self { Self(generate_uuid()) } @@ -18,44 +18,44 @@ impl GuideId { } } -impl Default for GuideId { +impl Default for GuideLineId { fn default() -> Self { Self::new() } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] -pub enum GuideDirection { +pub enum GuideLineDirection { Horizontal, Vertical, } #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] -pub struct Guide { - pub id: GuideId, - pub direction: GuideDirection, +pub struct GuideLine { + pub id: GuideLineId, + pub direction: GuideLineDirection, /// Position in document space (Y coordinate for horizontal guides, X coordinate for vertical guides) pub position: f64, } -impl Guide { - pub fn new(direction: GuideDirection, position: f64) -> Self { +impl GuideLine { + pub fn new(direction: GuideLineDirection, position: f64) -> Self { Self { - id: GuideId::new(), + id: GuideLineId::new(), direction, position, } } - pub fn with_id(id: GuideId, direction: GuideDirection, position: f64) -> Self { + pub fn with_id(id: GuideLineId, direction: GuideLineDirection, position: f64) -> Self { Self { id, direction, position } } pub fn horizontal(y: f64) -> Self { - Self::new(GuideDirection::Horizontal, y) + Self::new(GuideLineDirection::Horizontal, y) } pub fn vertical(x: f64) -> Self { - Self::new(GuideDirection::Vertical, x) + Self::new(GuideLineDirection::Vertical, x) } } diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 2ee7440b39..824bb714d0 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -63,7 +63,7 @@ pub enum AlignAggregate { pub struct SnappingState { pub snapping_enabled: bool, pub grid_snapping: bool, - pub guides: bool, + pub guide_lines: bool, pub artboards: bool, pub tolerance: f64, pub bounding_box: BoundingBoxSnapping, @@ -76,7 +76,7 @@ impl Default for SnappingState { Self { snapping_enabled: true, grid_snapping: false, - guides: true, + guide_lines: true, artboards: true, tolerance: 8., bounding_box: BoundingBoxSnapping::default(), @@ -108,7 +108,7 @@ impl SnappingState { }, SnapTarget::Artboard(_) => self.artboards, SnapTarget::Grid(_) => self.grid_snapping, - SnapTarget::Guide(_) => self.guides, + SnapTarget::GuideLine(_) => self.guide_lines, SnapTarget::Alignment(AlignmentSnapTarget::AlignWithAnchorPoint) => self.path.align_with_anchor_point, SnapTarget::Alignment(_) => self.bounding_box.align_with_edges, SnapTarget::DistributeEvenly(_) => self.bounding_box.distribute_evenly, @@ -553,18 +553,18 @@ impl fmt::Display for GridSnapTarget { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum GuideSnapTarget { +pub enum GuideLineSnapTarget { Horizontal, Vertical, Intersection, } -impl fmt::Display for GuideSnapTarget { +impl fmt::Display for GuideLineSnapTarget { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - GuideSnapTarget::Horizontal => write!(f, "Guide: Horizontal"), - GuideSnapTarget::Vertical => write!(f, "Guide: Vertical"), - GuideSnapTarget::Intersection => write!(f, "Guide: Intersection"), + GuideLineSnapTarget::Horizontal => write!(f, "Guide Line: Horizontal"), + GuideLineSnapTarget::Vertical => write!(f, "Guide Line: Vertical"), + GuideLineSnapTarget::Intersection => write!(f, "Guide Line: Intersection"), } } } @@ -636,7 +636,7 @@ pub enum SnapTarget { Path(PathSnapTarget), Artboard(ArtboardSnapTarget), Grid(GridSnapTarget), - Guide(GuideSnapTarget), + GuideLine(GuideLineSnapTarget), Alignment(AlignmentSnapTarget), DistributeEvenly(DistributionSnapTarget), } @@ -658,7 +658,7 @@ impl fmt::Display for SnapTarget { SnapTarget::Path(path_snap_target) => write!(f, "{path_snap_target}"), SnapTarget::Artboard(artboard_snap_target) => write!(f, "{artboard_snap_target}"), SnapTarget::Grid(grid_snap_target) => write!(f, "{grid_snap_target}"), - SnapTarget::Guide(guide_snap_target) => write!(f, "{guide_snap_target}"), + SnapTarget::GuideLine(guide_snap_target) => write!(f, "{guide_snap_target}"), SnapTarget::Alignment(alignment_snap_target) => write!(f, "{alignment_snap_target}"), SnapTarget::DistributeEvenly(distribution_snap_target) => write!(f, "{distribution_snap_target}"), } diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index 7c0b4c9335..ae30e208b5 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -41,7 +41,7 @@ pub struct SnapManager { indicator: Option, layer_snapper: LayerSnapper, grid_snapper: GridSnapper, - guide_snapper: GuideSnapper, + guide_line_snapper: GuideLineSnapper, alignment_snapper: AlignmentSnapper, distribution_snapper: DistributionSnapper, candidates: Option>, @@ -404,7 +404,7 @@ impl SnapManager { self.layer_snapper.free_snap(&mut snap_data, point, &mut snap_results, config); self.grid_snapper.free_snap(&mut snap_data, point, &mut snap_results); - self.guide_snapper.free_snap(&mut snap_data, point, &mut snap_results); + self.guide_line_snapper.free_snap(&mut snap_data, point, &mut snap_results); self.alignment_snapper.free_snap(&mut snap_data, point, &mut snap_results, config); self.distribution_snapper.free_snap(&mut snap_data, point, &mut snap_results, config); @@ -431,7 +431,7 @@ impl SnapManager { self.layer_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint, config); self.grid_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint); - self.guide_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint); + self.guide_line_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint); self.alignment_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint, config); self.distribution_snapper.constrained_snap(&mut snap_data, point, &mut snap_results, constraint, config); diff --git a/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs b/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs index a895aee852..35be940c54 100644 --- a/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs +++ b/editor/src/messages/tool/common_functionality/snapping/guide_snapper.rs @@ -1,25 +1,25 @@ use super::*; -use crate::messages::portfolio::document::utility_types::guide::GuideDirection; -use crate::messages::portfolio::document::utility_types::misc::{GuideSnapTarget, SnapTarget}; +use crate::messages::portfolio::document::utility_types::guide::GuideLineDirection; +use crate::messages::portfolio::document::utility_types::misc::{GuideLineSnapTarget, SnapTarget}; use glam::DVec2; use graphene_std::renderer::Quad; #[derive(Clone, Debug, Default)] -pub struct GuideSnapper; +pub struct GuideLineSnapper; -impl GuideSnapper { - fn get_snap_lines(&self, snap_data: &mut SnapData) -> Vec<(DVec2, DVec2, GuideSnapTarget)> { +impl GuideLineSnapper { + fn get_snap_lines(&self, snap_data: &mut SnapData) -> Vec<(DVec2, DVec2, GuideLineSnapTarget)> { let document = snap_data.document; let mut lines = Vec::new(); - if !document.guide_handler.guides_visible || !document.snapping_state.guides { + if !document.guide_lines_message_handler.guide_lines_visible || !document.snapping_state.guide_lines { return lines; } - for guide in &document.guide_handler.guides { - let (point, direction, snap_target) = match guide.direction { - GuideDirection::Horizontal => (DVec2::new(0.0, guide.position), DVec2::X, GuideSnapTarget::Horizontal), - GuideDirection::Vertical => (DVec2::new(guide.position, 0.0), DVec2::Y, GuideSnapTarget::Vertical), + for guide_line in &document.guide_lines_message_handler.guide_lines { + let (point, direction, snap_target) = match guide_line.direction { + GuideLineDirection::Horizontal => (DVec2::new(0.0, guide_line.position), DVec2::X, GuideLineSnapTarget::Horizontal), + GuideLineDirection::Vertical => (DVec2::new(guide_line.position, 0.0), DVec2::Y, GuideLineSnapTarget::Vertical), }; lines.push((point, direction, snap_target)); } @@ -39,7 +39,7 @@ impl GuideSnapper { continue; } - let target = SnapTarget::Guide(snap_target); + let target = SnapTarget::GuideLine(snap_target); if snap_data.document.snapping_state.target_enabled(target) { snap_results.points.push(SnappedPoint { snapped_point_document: projected, @@ -54,14 +54,14 @@ impl GuideSnapper { } let document = snap_data.document; - if document.snapping_state.target_enabled(SnapTarget::Guide(GuideSnapTarget::Intersection)) { + if document.snapping_state.target_enabled(SnapTarget::GuideLine(GuideLineSnapTarget::Intersection)) { let tolerance = snap_tolerance(document); let mut guide_lines: Vec = Vec::new(); - for guide in &document.guide_handler.guides { - let (snapped_point_document, direction) = match guide.direction { - GuideDirection::Horizontal => (DVec2::new(0.0, guide.position), DVec2::X), - GuideDirection::Vertical => (DVec2::new(guide.position, 0.0), DVec2::Y), + for guide_line in &document.guide_lines_message_handler.guide_lines { + let (snapped_point_document, direction) = match guide_line.direction { + GuideLineDirection::Horizontal => (DVec2::new(0.0, guide_line.position), DVec2::X), + GuideLineDirection::Vertical => (DVec2::new(guide_line.position, 0.0), DVec2::Y), }; guide_lines.push(SnappedLine { point: SnappedPoint { @@ -74,7 +74,7 @@ impl GuideSnapper { }); } - if let Some(intersection) = super::get_line_intersection(point.document_point, &guide_lines, SnapTarget::Guide(GuideSnapTarget::Intersection)) { + if let Some(intersection) = super::get_line_intersection(point.document_point, &guide_lines, SnapTarget::GuideLine(GuideLineSnapTarget::Intersection)) { if intersection.distance <= tolerance { snap_results.points.push(intersection); } @@ -102,7 +102,7 @@ impl GuideSnapper { }; let distance = intersection.distance(point.document_point); - let target = SnapTarget::Guide(snap_target); + let target = SnapTarget::GuideLine(snap_target); if distance < tolerance && snap_data.document.snapping_state.target_enabled(target) { snap_results.points.push(SnappedPoint { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index fb56a7b920..dc7a73b3b7 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -4,10 +4,11 @@ use super::tool_prelude::*; use crate::consts::*; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; -use crate::messages::portfolio::document::guide_message::GuideMessage; +use crate::messages::portfolio::document::guide_message::GuideLineMessage; +use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; -use crate::messages::portfolio::document::utility_types::guide::{GuideDirection, GuideId}; +use crate::messages::portfolio::document::utility_types::guide::{GuideLineDirection, GuideLineId}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes; @@ -479,9 +480,9 @@ enum SelectToolFsmState { }, RotatingBounds, DraggingPivot, - DraggingGuide { - guide_id: GuideId, - direction: GuideDirection, + DraggingGuideLine { + guide_line_id: GuideLineId, + direction: GuideLineDirection, }, } @@ -518,9 +519,9 @@ struct SelectToolData { selected_layers_changed: bool, snap_candidates: Vec, auto_panning: AutoPanning, - dragging_guide_id: Option, - dragging_guide_direction: Option, - guide_drag_start_position: Option, + dragging_guide_line_id: Option, + dragging_guide_line_direction: Option, + guide_line_drag_start_position: Option, drag_start_center: ViewportPosition, } @@ -733,36 +734,6 @@ pub fn create_bounding_box_transform(document: &DocumentMessageHandler) -> DAffi .unwrap_or_default() } -fn hit_test_guide(document: &DocumentMessageHandler, viewport_position: DVec2, viewport: &ViewportMessageHandler) -> Option<(GuideId, GuideDirection)> { - if !document.guide_handler.guides_visible { - return None; - } - - let transform = document - .navigation_handler - .calculate_offset_transform(viewport.center_in_viewport_space().into(), &document.document_ptz); - - for guide in document.guide_handler.guides.iter().rev() { - let (doc_point, doc_direction) = match guide.direction { - GuideDirection::Horizontal => (DVec2::new(0.0, guide.position), DVec2::X), - GuideDirection::Vertical => (DVec2::new(guide.position, 0.0), DVec2::Y), - }; - - let viewport_point = transform.transform_point2(doc_point); - let viewport_direction = transform.transform_vector2(doc_direction); - - if let Some(dir_normalized) = viewport_direction.try_normalize() { - let to_mouse = viewport_position - viewport_point; - let perpendicular_dist = to_mouse.perp_dot(dir_normalized).abs(); - - if perpendicular_dist <= GUIDE_HIT_TOLERANCE { - return Some((guide.id, guide.direction)); - } - } - } - - None -} impl Fsm for SelectToolFsmState { type ToolData = SelectToolData; @@ -1216,13 +1187,13 @@ impl Fsm for SelectToolFsmState { // tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]); state - } else if let Some((guide_id, direction)) = hit_test_guide(document, input.mouse.position, viewport) { - tool_data.dragging_guide_id = Some(guide_id); - tool_data.dragging_guide_direction = Some(direction); + } else if let Some((guide_line_id, direction)) = document.guide_lines_message_handler.hit_test(input.mouse.position, document.metadata().document_to_viewport) { + tool_data.dragging_guide_line_id = Some(guide_line_id); + tool_data.dragging_guide_line_direction = Some(direction); - let original_position = document.guide_handler.guides.iter().find(|g| g.id == guide_id).map(|g| g.position); - tool_data.guide_drag_start_position = original_position; - SelectToolFsmState::DraggingGuide { guide_id, direction } + let original_position = document.guide_lines_message_handler.guide_lines.iter().find(|g| g.id == guide_line_id).map(|g| g.position); + tool_data.guide_line_drag_start_position = original_position; + SelectToolFsmState::DraggingGuideLine { guide_line_id, direction } } // Dragging one (or two, forming a corner) of the transform cage bounding box edges else if resize { @@ -1314,53 +1285,53 @@ impl Fsm for SelectToolFsmState { let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } - (SelectToolFsmState::DraggingGuide { .. }, SelectToolMessage::Abort) => { - tool_data.dragging_guide_id = None; - tool_data.dragging_guide_direction = None; - tool_data.guide_drag_start_position = None; + (SelectToolFsmState::DraggingGuideLine { .. }, SelectToolMessage::Abort) => { + tool_data.dragging_guide_line_id = None; + tool_data.dragging_guide_line_direction = None; + tool_data.guide_line_drag_start_position = None; let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } - (SelectToolFsmState::DraggingGuide { guide_id, direction }, SelectToolMessage::PointerMove { .. }) => { + (SelectToolFsmState::DraggingGuideLine { guide_line_id, direction }, SelectToolMessage::PointerMove { .. }) => { tool_data.drag_current = input.mouse.position; - responses.add(GuideMessage::MoveGuide { - id: guide_id, + responses.add(GuideLineMessage::MoveGuideLine { + id: guide_line_id, mouse_x: input.mouse.position.x, mouse_y: input.mouse.position.y, }); let cursor = match direction { - GuideDirection::Horizontal => MouseCursorIcon::NSResize, - GuideDirection::Vertical => MouseCursorIcon::EWResize, + GuideLineDirection::Horizontal => MouseCursorIcon::NSResize, + GuideLineDirection::Vertical => MouseCursorIcon::EWResize, }; if tool_data.cursor != cursor { tool_data.cursor = cursor; responses.add(FrontendMessage::UpdateMouseCursor { cursor }); } - SelectToolFsmState::DraggingGuide { guide_id, direction } + SelectToolFsmState::DraggingGuideLine { guide_line_id, direction } } - (SelectToolFsmState::DraggingGuide { guide_id, direction: _ }, SelectToolMessage::DragStop { .. }) => { + (SelectToolFsmState::DraggingGuideLine { guide_line_id, direction: _ }, SelectToolMessage::DragStop { .. }) => { tool_data.drag_current = input.mouse.position; - // Checks if dragged outside viewport - deletes the guide + // Checks if dragged outside viewport - deletes the guide line let viewport_size = viewport.size().into_dvec2(); let outside_viewport = input.mouse.position.x < 0.0 || input.mouse.position.y < 0.0 || input.mouse.position.x > viewport_size.x || input.mouse.position.y > viewport_size.y; if outside_viewport { - responses.add(GuideMessage::DeleteGuide { id: guide_id }); + responses.add(GuideLineMessage::DeleteGuideLine { id: guide_line_id }); } else { - responses.add(GuideMessage::MoveGuide { - id: guide_id, + responses.add(GuideLineMessage::MoveGuideLine { + id: guide_line_id, mouse_x: input.mouse.position.x, mouse_y: input.mouse.position.y, }); } - tool_data.dragging_guide_id = None; - tool_data.dragging_guide_direction = None; - tool_data.guide_drag_start_position = None; + tool_data.dragging_guide_line_id = None; + tool_data.dragging_guide_line_direction = None; + tool_data.guide_line_drag_start_position = None; let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } @@ -1548,16 +1519,16 @@ impl Fsm for SelectToolFsmState { cursor = MouseCursorIcon::Move; } - // Check if hovering over a guide and update hover state - let hovered_guide = hit_test_guide(document, input.mouse.position, viewport); - if let Some((guide_id, direction)) = hovered_guide { + // Check if hovering over a guide line and update hover state + let hovered_guide_line = document.guide_lines_message_handler.hit_test(input.mouse.position, document.metadata().document_to_viewport); + if let Some((guide_line_id, direction)) = hovered_guide_line { cursor = match direction { - GuideDirection::Horizontal => MouseCursorIcon::NSResize, - GuideDirection::Vertical => MouseCursorIcon::EWResize, + GuideLineDirection::Horizontal => MouseCursorIcon::NSResize, + GuideLineDirection::Vertical => MouseCursorIcon::EWResize, }; - responses.add(GuideMessage::SetHoveredGuide { id: Some(guide_id) }); + responses.add(GuideLineMessage::SetHoveredGuideLine { id: Some(guide_line_id) }); } else { - responses.add(GuideMessage::SetHoveredGuide { id: None }); + responses.add(GuideLineMessage::SetHoveredGuideLine { id: None }); } // Generate the hover outline @@ -2013,10 +1984,10 @@ impl Fsm for SelectToolFsmState { let hint_data = HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]); hint_data.send_layout(responses); } - SelectToolFsmState::DraggingGuide { .. } => { + SelectToolFsmState::DraggingGuideLine { .. } => { let hint_data = HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Move Guide")]), + HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Move Guide Line")]), ]); hint_data.send_layout(responses); } From 32754ffdf7c3db829ca65277204c44f64f67424d Mon Sep 17 00:00:00 2001 From: Kulratan Date: Sun, 10 May 2026 16:10:07 +0000 Subject: [PATCH 15/26] Fix merge conflicts --- frontend/src/components/widgets/inputs/RulerInput.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/widgets/inputs/RulerInput.svelte b/frontend/src/components/widgets/inputs/RulerInput.svelte index 5d3e4c7ef2..3cfae082b2 100644 --- a/frontend/src/components/widgets/inputs/RulerInput.svelte +++ b/frontend/src/components/widgets/inputs/RulerInput.svelte @@ -1,5 +1,5 @@ @@ -244,6 +253,7 @@ bind:this={rulerInput} on:pointerdown={handlePointerDown} on:pointerup={handlePointerUp} + on:pointercancel={handlePointerCancel} style:cursor={direction === "Horizontal" ? "row-resize" : "col-resize"} > diff --git a/frontend/wrapper/src/editor_commands.rs b/frontend/wrapper/src/editor_commands.rs index 5baee86864..e5229a0056 100644 --- a/frontend/wrapper/src/editor_commands.rs +++ b/frontend/wrapper/src/editor_commands.rs @@ -662,10 +662,10 @@ mod editor_commands { fn create_guide_line(id: u64, direction: String, mouse_x: f64, mouse_y: f64) -> Message { use editor::messages::portfolio::document::guide_message::GuideLineMessage; use editor::messages::portfolio::document::utility_types::guide::{GuideLineDirection, GuideLineId}; - let direction = if direction == "Horizontal" { - GuideLineDirection::Horizontal - } else { - GuideLineDirection::Vertical + let direction = match direction.as_str() { + "Horizontal" => GuideLineDirection::Horizontal, + "Vertical" => GuideLineDirection::Vertical, + other => panic!("Invalid guide line direction: {other:?}"), }; GuideLineMessage::CreateGuideLine { id: GuideLineId::from_raw(id), From dc2c9ec81e74c909fd9071836e93eaae4c7a32e0 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Thu, 23 Jul 2026 20:24:46 +0000 Subject: [PATCH 21/26] Fix isGuideEditor type guard and add onDestroy drag cleanup --- .../portfolio/document/document_history.rs | 29 ++++++++++++++++++- .../document/document_message_handler.rs | 16 ++++++++++ .../document/guide_message_handler.rs | 8 +++++ .../src/components/panels/Document.svelte | 19 +++++++++++- 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_history.rs b/editor/src/messages/portfolio/document/document_history.rs index 9758fd3846..4854fb4afd 100644 --- a/editor/src/messages/portfolio/document/document_history.rs +++ b/editor/src/messages/portfolio/document/document_history.rs @@ -4,6 +4,7 @@ use std::collections::{BTreeMap, HashSet}; use document_graph_storage::Registry; use graph_craft::application_io::resource::{ResourceId, ResourceRegistry, ResourceStorage}; +use super::utility_types::guide::GuideLine; use super::utility_types::network_interface::NodeNetworkInterface; use super::utility_types::network_interface::storage_metadata::{StorageMetadataView, collect_network_view_settings}; @@ -20,6 +21,10 @@ pub struct DocumentHistory { legacy_undo_stack: VecDeque, /// Stack of document network snapshots for future history states. legacy_redo_stack: VecDeque, + /// Guide-line state paired with each legacy undo snapshot. + guide_undo_stack: VecDeque>, + /// Guide-line state paired with each legacy redo snapshot. + guide_redo_stack: VecDeque>, /// The `Gdd` working copy: owns the CRDT `Session` and mirrors edits to disk. `None` until the mount /// future built by `load_document` resolves. #[derivative(Debug = "ignore")] @@ -34,29 +39,51 @@ impl DocumentHistory { Self::push_capped(&mut self.legacy_undo_stack, snapshot); } + /// Push a guide-line snapshot alongside the most recent undo network snapshot. + pub fn push_guide_undo(&mut self, guide_lines: Vec) { + Self::push_capped(&mut self.guide_undo_stack, guide_lines); + } + /// Push a snapshot onto the redo stack, evicting the oldest entry past the history cap. pub fn push_redo(&mut self, snapshot: NodeNetworkInterface) { Self::push_capped(&mut self.legacy_redo_stack, snapshot); } + /// Push a guide-line snapshot alongside the most recent redo network snapshot. + pub fn push_guide_redo(&mut self, guide_lines: Vec) { + Self::push_capped(&mut self.guide_redo_stack, guide_lines); + } + /// Pop the most recent undo snapshot, or `None` when the stack is empty. pub fn pop_undo(&mut self) -> Option { self.legacy_undo_stack.pop_back() } + /// Pop the guide-line snapshot paired with the most recent undo entry. + pub fn pop_guide_undo(&mut self) -> Option> { + self.guide_undo_stack.pop_back() + } + /// Pop the most recent redo snapshot, or `None` when the stack is empty. pub fn pop_redo(&mut self) -> Option { self.legacy_redo_stack.pop_back() } + /// Pop the guide-line snapshot paired with the most recent redo entry. + pub fn pop_guide_redo(&mut self) -> Option> { + self.guide_redo_stack.pop_back() + } + /// Drop the most recently pushed undo snapshot (used to cancel a transaction that ended up unmodified). pub fn discard_last_undo(&mut self) { self.legacy_undo_stack.pop_back(); + self.guide_undo_stack.pop_back(); } /// Clear the redo stack, called when a fresh edit invalidates the redo future. pub fn clear_redo(&mut self) { self.legacy_redo_stack.clear(); + self.guide_redo_stack.clear(); } /// Add the resources referenced by every snapshot in both history stacks into `resources`, so @@ -258,7 +285,7 @@ impl DocumentHistory { } } - fn push_capped(stack: &mut VecDeque, snapshot: NodeNetworkInterface) { + fn push_capped(stack: &mut VecDeque, snapshot: T) { stack.push_back(snapshot); if stack.len() > crate::consts::MAX_UNDO_HISTORY_LEN { stack.pop_front(); diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index f257bc0673..bc74cd1e8e 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1346,6 +1346,7 @@ impl MessageHandler> for DocumentMes self.network_interface.start_transaction(); self.history.push_undo(self.network_interface.clone()); + self.history.push_guide_undo(self.guide_lines_message_handler.guide_lines.clone()); // Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents responses.add(PortfolioMessage::UpdateOpenDocumentsList); } @@ -2445,8 +2446,16 @@ impl DocumentMessageHandler { pub fn undo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque) -> Option { // If there is no history return and don't broadcast SelectionChanged let network_interface = self.history.pop_undo()?; + let guide_lines_snapshot = self.history.pop_guide_undo(); let previous_network = self.install_history_snapshot(network_interface, viewport); + // Restore guide lines from the snapshot and stash the current state for redo + if let Some(guide_lines) = guide_lines_snapshot { + let current_guides = std::mem::replace(&mut self.guide_lines_message_handler.guide_lines, guide_lines); + self.history.push_guide_redo(current_guides); + responses.add(OverlaysMessage::Draw); + } + // Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::SelectedNodesUpdated); @@ -2471,8 +2480,15 @@ impl DocumentMessageHandler { pub fn redo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque) -> Option { // If there is no history return and don't broadcast SelectionChanged let network_interface = self.history.pop_redo()?; + let guide_lines_snapshot = self.history.pop_guide_redo(); let previous_network = self.install_history_snapshot(network_interface, viewport); + // Restore guide lines from the redo snapshot and stash the current state for undo + if let Some(guide_lines) = guide_lines_snapshot { + let current_guides = std::mem::replace(&mut self.guide_lines_message_handler.guide_lines, guide_lines); + self.history.push_guide_undo(current_guides); + responses.add(OverlaysMessage::Draw); + } // Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::SelectedNodesUpdated); diff --git a/editor/src/messages/portfolio/document/guide_message_handler.rs b/editor/src/messages/portfolio/document/guide_message_handler.rs index 47050c8b55..875987d7c5 100644 --- a/editor/src/messages/portfolio/document/guide_message_handler.rs +++ b/editor/src/messages/portfolio/document/guide_message_handler.rs @@ -78,8 +78,10 @@ impl MessageHandler for GuideLinesMe GuideLineDirection::Vertical => document_point.x, }; + responses.add(DocumentMessage::StartTransaction); let guide_line = GuideLine::with_id(id, direction, document_position); self.guide_lines.push(guide_line); + responses.add(DocumentMessage::CommitTransaction); responses.add(OverlaysMessage::Draw); responses.add(PortfolioMessage::UpdateDocumentWidgets); } @@ -87,15 +89,19 @@ impl MessageHandler for GuideLinesMe let document_point = document_point(mouse_x, mouse_y); if let Some(guide_line) = self.guide_lines.iter_mut().find(|guide_line| guide_line.id == id) { + responses.add(DocumentMessage::StartTransaction); guide_line.position = match guide_line.direction { GuideLineDirection::Horizontal => document_point.y, GuideLineDirection::Vertical => document_point.x, }; + responses.add(DocumentMessage::CommitTransaction); } responses.add(OverlaysMessage::Draw); } GuideLineMessage::DeleteGuideLine { id } => { + responses.add(DocumentMessage::StartTransaction); self.guide_lines.retain(|g| g.id != id); + responses.add(DocumentMessage::CommitTransaction); responses.add(OverlaysMessage::Draw); responses.add(PortfolioMessage::UpdateDocumentWidgets); } @@ -105,7 +111,9 @@ impl MessageHandler for GuideLinesMe } } GuideLineMessage::ToggleGuideLinesVisibility => { + responses.add(DocumentMessage::StartTransaction); self.guide_lines_visible = !self.guide_lines_visible; + responses.add(DocumentMessage::CommitTransaction); responses.add(OverlaysMessage::Draw); responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(MenuBarMessage::SendLayout); diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 677cb71101..338d23faeb 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -173,7 +173,16 @@ }; function isGuideEditor(x: unknown): x is GuideEditor { - return true; + return ( + typeof x === "object" && + x !== null && + "createGuideLine" in x && + typeof (x as GuideEditor).createGuideLine === "function" && + "moveGuideLine" in x && + typeof (x as GuideEditor).moveGuideLine === "function" && + "deleteGuideLine" in x && + typeof (x as GuideEditor).deleteGuideLine === "function" + ); } // Access the underlying EditorHandle for guide-specific methods @@ -688,6 +697,14 @@ subscriptions.unsubscribeFrontendMessage("DisplayEditableTextboxUpdateFontData"); subscriptions.unsubscribeFrontendMessage("DisplayEditableTextboxTransform"); subscriptions.unsubscribeFrontendMessage("DisplayRemoveEditableTextbox"); + + // Cancel any in-progress guide drag so global window listeners don't leak + if (draggingGuideId !== undefined) { + const editorHandle = getEditorHandle(); + if (isGuideEditor(editorHandle)) editorHandle.deleteGuideLine(draggingGuideId); + draggingGuideId = undefined; + draggingGuideDirection = undefined; + } }); From 522ad4d5e130338b2eefd72d4dab2d066e8b4de2 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Thu, 23 Jul 2026 20:45:03 +0000 Subject: [PATCH 22/26] Snapping --- .../document/document_message_handler.rs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index bc74cd1e8e..3ee647fcdd 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -14,6 +14,7 @@ use crate::messages::input_mapper::utility_types::macros::action_shortcut; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::data_panel::{DataPanelMessageContext, DataPanelMessageHandler}; use crate::messages::portfolio::document::graph_operation::utility_types::{ModifyInputsContext, TransformIn}; +use crate::messages::portfolio::document::guide_message::GuideLineMessage; use crate::messages::portfolio::document::guide_message_handler::{GuideLinesMessageContext, GuideLinesMessageHandler}; use crate::messages::portfolio::document::node_graph::NodeGraphMessageContext; use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier; @@ -162,6 +163,9 @@ pub struct DocumentMessageHandler { /// Whether or not the editor has executed the network to render the document yet. If this is opened as an inactive tab, it won't be loaded initially because the active tab is prioritized. #[serde(skip)] pub is_loaded: bool, + /// Snap manager used exclusively for snapping guide lines during create/move operations. + #[serde(skip)] + guide_snap_manager: crate::messages::tool::common_functionality::snapping::SnapManager, } impl Default for DocumentMessageHandler { @@ -206,6 +210,7 @@ impl Default for DocumentMessageHandler { layer_range_selection_reference: None, is_loaded: false, + guide_snap_manager: Default::default(), } } } @@ -243,6 +248,12 @@ impl MessageHandler> for DocumentMes self.navigation_handler.process_message(message, responses, context); } DocumentMessage::GuideLine(message) => { + // Apply snapping to guide create/move before forwarding to the handler. + let message = if self.snapping_state.snapping_enabled { + self.snap_guide_message(message, ipp, viewport) + } else { + message + }; let context = GuideLinesMessageContext { document_to_viewport: self.metadata().document_to_viewport, }; @@ -1929,6 +1940,57 @@ impl DocumentMessageHandler { ClickXRayIter::new(&self.network_interface, XRayTarget::Point(point)) } + fn snap_guide_message(&mut self, message: GuideLineMessage, ipp: &InputPreprocessorMessageHandler, viewport: &ViewportMessageHandler) -> GuideLineMessage { + use crate::messages::portfolio::document::utility_types::guide::GuideLineDirection; + use crate::messages::portfolio::document::utility_types::misc::SnapSource; + use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapTypeConfiguration}; + + let snap_constrained = + |snap_manager: &mut crate::messages::tool::common_functionality::snapping::SnapManager, document: &DocumentMessageHandler, raw_viewport: DVec2, direction: GuideLineDirection| -> DVec2 { + let document_to_viewport = document.metadata().document_to_viewport; + let raw_doc = document_to_viewport.inverse().transform_point2(raw_viewport); + let snap_data = SnapData::new(document, ipp, viewport); + let point = SnapCandidatePoint::new_source(raw_doc, SnapSource::None); + let constraint = match direction { + // Horizontal guide: fixed Y position, constrain snap search along X + GuideLineDirection::Horizontal => SnapConstraint::Line { origin: raw_doc, direction: DVec2::X }, + // Vertical guide: fixed X position, constrain snap search along Y + GuideLineDirection::Vertical => SnapConstraint::Line { origin: raw_doc, direction: DVec2::Y }, + }; + let snapped = snap_manager.constrained_snap(&snap_data, &point, constraint, SnapTypeConfiguration::default()); + snap_manager.update_indicator(snapped.clone()); + document_to_viewport.transform_point2(snapped.snapped_point_document) + }; + + match message { + GuideLineMessage::CreateGuideLine { id, direction, mouse_x, mouse_y } => { + let snap_manager = unsafe { &mut *(&mut self.guide_snap_manager as *mut _) }; + let snapped = snap_constrained(snap_manager, self, DVec2::new(mouse_x, mouse_y), direction); + GuideLineMessage::CreateGuideLine { + id, + direction, + mouse_x: snapped.x, + mouse_y: snapped.y, + } + } + GuideLineMessage::MoveGuideLine { id, mouse_x, mouse_y } => { + if let Some(guide_line) = self.guide_lines_message_handler.guide_lines.iter().find(|g| g.id == id) { + let direction = guide_line.direction; + let snap_manager = unsafe { &mut *(&mut self.guide_snap_manager as *mut _) }; + let snapped = snap_constrained(snap_manager, self, DVec2::new(mouse_x, mouse_y), direction); + GuideLineMessage::MoveGuideLine { + id, + mouse_x: snapped.x, + mouse_y: snapped.y, + } + } else { + GuideLineMessage::MoveGuideLine { id, mouse_x, mouse_y } + } + } + other => other, + } + } + /// Find the deepest layer given in the sorted array (by returning the one which is not a folder from the list of layers under the click location). pub fn find_deepest(&self, node_list: &[LayerNodeIdentifier]) -> Option { node_list From 1db5c3cd6bb4cac9671efbd386ed039dedffa465 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Fri, 24 Jul 2026 11:07:06 +0000 Subject: [PATCH 23/26] Ai-Review --- .../portfolio/document/document_history.rs | 20 ++-- .../document/document_message_handler.rs | 98 +++++++++++++++---- .../document/guide_message_handler.rs | 2 - .../portfolio/document/utility_types/guide.rs | 14 +++ .../tool/tool_messages/select_tool.rs | 5 + 5 files changed, 107 insertions(+), 32 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_history.rs b/editor/src/messages/portfolio/document/document_history.rs index 4854fb4afd..c6212b27d3 100644 --- a/editor/src/messages/portfolio/document/document_history.rs +++ b/editor/src/messages/portfolio/document/document_history.rs @@ -4,7 +4,7 @@ use std::collections::{BTreeMap, HashSet}; use document_graph_storage::Registry; use graph_craft::application_io::resource::{ResourceId, ResourceRegistry, ResourceStorage}; -use super::utility_types::guide::GuideLine; +use super::utility_types::guide::GuideLinesState; use super::utility_types::network_interface::NodeNetworkInterface; use super::utility_types::network_interface::storage_metadata::{StorageMetadataView, collect_network_view_settings}; @@ -22,9 +22,9 @@ pub struct DocumentHistory { /// Stack of document network snapshots for future history states. legacy_redo_stack: VecDeque, /// Guide-line state paired with each legacy undo snapshot. - guide_undo_stack: VecDeque>, + guide_undo_stack: VecDeque, /// Guide-line state paired with each legacy redo snapshot. - guide_redo_stack: VecDeque>, + guide_redo_stack: VecDeque, /// The `Gdd` working copy: owns the CRDT `Session` and mirrors edits to disk. `None` until the mount /// future built by `load_document` resolves. #[derivative(Debug = "ignore")] @@ -40,8 +40,8 @@ impl DocumentHistory { } /// Push a guide-line snapshot alongside the most recent undo network snapshot. - pub fn push_guide_undo(&mut self, guide_lines: Vec) { - Self::push_capped(&mut self.guide_undo_stack, guide_lines); + pub fn push_guide_undo(&mut self, guide_state: GuideLinesState) { + Self::push_capped(&mut self.guide_undo_stack, guide_state); } /// Push a snapshot onto the redo stack, evicting the oldest entry past the history cap. @@ -50,8 +50,8 @@ impl DocumentHistory { } /// Push a guide-line snapshot alongside the most recent redo network snapshot. - pub fn push_guide_redo(&mut self, guide_lines: Vec) { - Self::push_capped(&mut self.guide_redo_stack, guide_lines); + pub fn push_guide_redo(&mut self, guide_state: GuideLinesState) { + Self::push_capped(&mut self.guide_redo_stack, guide_state); } /// Pop the most recent undo snapshot, or `None` when the stack is empty. @@ -60,7 +60,7 @@ impl DocumentHistory { } /// Pop the guide-line snapshot paired with the most recent undo entry. - pub fn pop_guide_undo(&mut self) -> Option> { + pub fn pop_guide_undo(&mut self) -> Option { self.guide_undo_stack.pop_back() } @@ -70,14 +70,14 @@ impl DocumentHistory { } /// Pop the guide-line snapshot paired with the most recent redo entry. - pub fn pop_guide_redo(&mut self) -> Option> { + pub fn pop_guide_redo(&mut self) -> Option { self.guide_redo_stack.pop_back() } /// Drop the most recently pushed undo snapshot (used to cancel a transaction that ended up unmodified). pub fn discard_last_undo(&mut self) { self.legacy_undo_stack.pop_back(); - self.guide_undo_stack.pop_back(); + let _ = self.guide_undo_stack.pop_back(); } /// Clear the redo stack, called when a fresh edit invalidates the redo future. diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 3ee647fcdd..763ffc8fbb 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1357,7 +1357,7 @@ impl MessageHandler> for DocumentMes self.network_interface.start_transaction(); self.history.push_undo(self.network_interface.clone()); - self.history.push_guide_undo(self.guide_lines_message_handler.guide_lines.clone()); + self.history.push_guide_undo(self.current_guide_state()); // Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents responses.add(PortfolioMessage::UpdateOpenDocumentsList); } @@ -1952,10 +1952,10 @@ impl DocumentMessageHandler { let snap_data = SnapData::new(document, ipp, viewport); let point = SnapCandidatePoint::new_source(raw_doc, SnapSource::None); let constraint = match direction { - // Horizontal guide: fixed Y position, constrain snap search along X - GuideLineDirection::Horizontal => SnapConstraint::Line { origin: raw_doc, direction: DVec2::X }, - // Vertical guide: fixed X position, constrain snap search along Y - GuideLineDirection::Vertical => SnapConstraint::Line { origin: raw_doc, direction: DVec2::Y }, + // Horizontal guide: position varies on Y, constrain snap search along Y + GuideLineDirection::Horizontal => SnapConstraint::Line { origin: raw_doc, direction: DVec2::Y }, + // Vertical guide: position varies on X, constrain snap search along X + GuideLineDirection::Vertical => SnapConstraint::Line { origin: raw_doc, direction: DVec2::X }, }; let snapped = snap_manager.constrained_snap(&snap_data, &point, constraint, SnapTypeConfiguration::default()); snap_manager.update_indicator(snapped.clone()); @@ -2478,6 +2478,13 @@ impl DocumentMessageHandler { paths } + pub fn current_guide_state(&self) -> crate::messages::portfolio::document::utility_types::guide::GuideLinesState { + crate::messages::portfolio::document::utility_types::guide::GuideLinesState { + guide_lines: self.guide_lines_message_handler.guide_lines.clone(), + guide_lines_visible: self.guide_lines_message_handler.guide_lines_visible, + } + } + pub fn undo_with_history(&mut self, document_id: DocumentId, viewport: &ViewportMessageHandler, resource_storage: &ResourceStorageMessageHandler, responses: &mut VecDeque) { let legacy_applied = if let Some(previous_network) = self.undo(viewport, responses) { self.history.push_redo(previous_network); @@ -2486,9 +2493,12 @@ impl DocumentMessageHandler { false }; - self.drive_storage_undo_redo(document_id, resource_storage, legacy_applied, true, responses); + if legacy_applied { + self.drive_storage_undo_redo(document_id, resource_storage, legacy_applied, true, responses); + } } +<<<<<<< HEAD /// Installs a history snapshot as the active network interface, carrying over the current view state and structure load, and returns the replaced interface. fn install_history_snapshot(&mut self, mut network_interface: NodeNetworkInterface, viewport: &ViewportMessageHandler) -> NodeNetworkInterface { // Set the previous network navigation metadata to the current navigation metadata @@ -2516,9 +2526,38 @@ impl DocumentMessageHandler { let current_guides = std::mem::replace(&mut self.guide_lines_message_handler.guide_lines, guide_lines); self.history.push_guide_redo(current_guides); responses.add(OverlaysMessage::Draw); +======= + pub fn undo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque) -> Option { + let network_snapshot = self.history.pop_undo(); + let guide_snapshot = self.history.pop_guide_undo(); + + if network_snapshot.is_none() && guide_snapshot.is_none() { + return None; +>>>>>>> 6cf49e119 (Ai-Review) + } + + if let Some(guide_state) = guide_snapshot { + let current_state = self.current_guide_state(); + self.guide_lines_message_handler.guide_lines = guide_state.guide_lines; + self.guide_lines_message_handler.guide_lines_visible = guide_state.guide_lines_visible; + self.history.push_guide_redo(current_state); + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); } - // Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents + let previous_network = if let Some(mut network_interface) = network_snapshot { + network_interface.copy_all_navigation_metadata(&self.network_interface); + std::mem::swap(&mut network_interface.resolved_types, &mut self.network_interface.resolved_types); + + let transform = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); + network_interface.set_document_to_viewport_transform(transform); + + network_interface.load_structure(); + Some(std::mem::replace(&mut self.network_interface, network_interface)) + } else { + None + }; + responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::SelectedNodesUpdated); responses.add(NodeGraphMessage::ForceRunDocumentGraph); @@ -2526,8 +2565,9 @@ impl DocumentMessageHandler { // TODO: Remove once the footprint is used to load the imports/export distances from the edge responses.add(NodeGraphMessage::UnloadWires); - Some(previous_network) + previous_network } + pub fn redo_with_history(&mut self, document_id: DocumentId, viewport: &ViewportMessageHandler, resource_storage: &ResourceStorageMessageHandler, responses: &mut VecDeque) { let legacy_applied = if let Some(previous_network) = self.redo(viewport, responses) { self.history.push_undo(previous_network); @@ -2536,32 +2576,50 @@ impl DocumentMessageHandler { false }; - self.drive_storage_undo_redo(document_id, resource_storage, legacy_applied, false, responses); + if legacy_applied { + self.drive_storage_undo_redo(document_id, resource_storage, legacy_applied, false, responses); + } } pub fn redo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque) -> Option { - // If there is no history return and don't broadcast SelectionChanged - let network_interface = self.history.pop_redo()?; - let guide_lines_snapshot = self.history.pop_guide_redo(); - let previous_network = self.install_history_snapshot(network_interface, viewport); + let network_snapshot = self.history.pop_redo(); + let guide_snapshot = self.history.pop_guide_redo(); - // Restore guide lines from the redo snapshot and stash the current state for undo - if let Some(guide_lines) = guide_lines_snapshot { - let current_guides = std::mem::replace(&mut self.guide_lines_message_handler.guide_lines, guide_lines); - self.history.push_guide_undo(current_guides); + if network_snapshot.is_none() && guide_snapshot.is_none() { + return None; + } + + if let Some(guide_state) = guide_snapshot { + let current_state = self.current_guide_state(); + self.guide_lines_message_handler.guide_lines = guide_state.guide_lines; + self.guide_lines_message_handler.guide_lines_visible = guide_state.guide_lines_visible; + self.history.push_guide_undo(current_state); responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); } - // Push the UpdateOpenDocumentsList message to the bus in order to update the save status of the open documents + + let previous_network = if let Some(network_interface) = network_snapshot { + Some(self.install_history_snapshot(network_interface, viewport)) + } else { + None + }; + responses.add(PortfolioMessage::UpdateOpenDocumentsList); responses.add(NodeGraphMessage::SelectedNodesUpdated); responses.add(NodeGraphMessage::ForceRunDocumentGraph); responses.add(NodeGraphMessage::UnloadWires); responses.add(NodeGraphMessage::SendWires); - Some(previous_network) + + previous_network } pub fn current_hash(&self) -> u64 { - self.network_interface.document_network().current_hash() + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + self.network_interface.document_network().current_hash().hash(&mut hasher); + self.guide_lines_message_handler.guide_lines.hash(&mut hasher); + self.guide_lines_message_handler.guide_lines_visible.hash(&mut hasher); + hasher.finish() } pub fn is_auto_saved(&self) -> bool { diff --git a/editor/src/messages/portfolio/document/guide_message_handler.rs b/editor/src/messages/portfolio/document/guide_message_handler.rs index 875987d7c5..00f634592c 100644 --- a/editor/src/messages/portfolio/document/guide_message_handler.rs +++ b/editor/src/messages/portfolio/document/guide_message_handler.rs @@ -89,12 +89,10 @@ impl MessageHandler for GuideLinesMe let document_point = document_point(mouse_x, mouse_y); if let Some(guide_line) = self.guide_lines.iter_mut().find(|guide_line| guide_line.id == id) { - responses.add(DocumentMessage::StartTransaction); guide_line.position = match guide_line.direction { GuideLineDirection::Horizontal => document_point.y, GuideLineDirection::Vertical => document_point.x, }; - responses.add(DocumentMessage::CommitTransaction); } responses.add(OverlaysMessage::Draw); } diff --git a/editor/src/messages/portfolio/document/utility_types/guide.rs b/editor/src/messages/portfolio/document/utility_types/guide.rs index ef9acc106e..4169319ce4 100644 --- a/editor/src/messages/portfolio/document/utility_types/guide.rs +++ b/editor/src/messages/portfolio/document/utility_types/guide.rs @@ -38,6 +38,14 @@ pub struct GuideLine { pub position: f64, } +impl std::hash::Hash for GuideLine { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.direction.hash(state); + self.position.to_bits().hash(state); + } +} + impl GuideLine { pub fn new(direction: GuideLineDirection, position: f64) -> Self { Self { @@ -59,3 +67,9 @@ impl GuideLine { Self::new(GuideLineDirection::Vertical, x) } } + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GuideLinesState { + pub guide_lines: Vec, + pub guide_lines_visible: bool, +} diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index ef5ea66f90..40bd387595 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1185,6 +1185,7 @@ impl Fsm for SelectToolFsmState { state } else if let Some((guide_line_id, direction)) = document.guide_lines_message_handler.hit_test(input.mouse.position, document.metadata().document_to_viewport) { + responses.add(DocumentMessage::StartTransaction); tool_data.dragging_guide_line_id = Some(guide_line_id); tool_data.dragging_guide_line_direction = Some(direction); @@ -1283,6 +1284,7 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::Ready { selection } } (SelectToolFsmState::DraggingGuideLine { .. }, SelectToolMessage::Abort) => { + responses.add(DocumentMessage::AbortTransaction); tool_data.dragging_guide_line_id = None; tool_data.dragging_guide_line_direction = None; tool_data.guide_line_drag_start_position = None; @@ -1317,6 +1319,7 @@ impl Fsm for SelectToolFsmState { let outside_viewport = input.mouse.position.x < 0.0 || input.mouse.position.y < 0.0 || input.mouse.position.x > viewport_size.x || input.mouse.position.y > viewport_size.y; if outside_viewport { + responses.add(DocumentMessage::AbortTransaction); responses.add(GuideLineMessage::DeleteGuideLine { id: guide_line_id }); } else { responses.add(GuideLineMessage::MoveGuideLine { @@ -1324,8 +1327,10 @@ impl Fsm for SelectToolFsmState { mouse_x: input.mouse.position.x, mouse_y: input.mouse.position.y, }); + responses.add(DocumentMessage::CommitTransaction); } + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); tool_data.dragging_guide_line_id = None; tool_data.dragging_guide_line_direction = None; tool_data.guide_line_drag_start_position = None; From e5731feb61a93c3e0e00a7301d4d5c64149856de Mon Sep 17 00:00:00 2001 From: Kulratan Date: Sat, 25 Jul 2026 08:30:44 +0000 Subject: [PATCH 24/26] Fix AI review feedback --- .../portfolio/document/document_history.rs | 4 +- .../document/document_message_handler.rs | 73 ++++++++----------- .../tool/tool_messages/select_tool.rs | 14 ---- 3 files changed, 31 insertions(+), 60 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_history.rs b/editor/src/messages/portfolio/document/document_history.rs index c6212b27d3..1647312fb2 100644 --- a/editor/src/messages/portfolio/document/document_history.rs +++ b/editor/src/messages/portfolio/document/document_history.rs @@ -75,9 +75,9 @@ impl DocumentHistory { } /// Drop the most recently pushed undo snapshot (used to cancel a transaction that ended up unmodified). - pub fn discard_last_undo(&mut self) { + pub fn discard_last_undo(&mut self) -> Option { self.legacy_undo_stack.pop_back(); - let _ = self.guide_undo_stack.pop_back(); + self.guide_undo_stack.pop_back() } /// Clear the redo stack, called when a fresh edit invalidates the redo future. diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 763ffc8fbb..f237a5da7c 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1373,7 +1373,10 @@ impl MessageHandler> for DocumentMes }, DocumentMessage::CancelTransaction => { self.network_interface.finish_transaction(); - self.history.discard_last_undo(); + if let Some(guide_state) = self.history.discard_last_undo() { + self.guide_lines_message_handler.guide_lines = guide_state.guide_lines; + responses.add(OverlaysMessage::Draw); + } } DocumentMessage::CommitTransaction => { if self.network_interface.transaction_status() == TransactionStatus::Finished { @@ -1945,27 +1948,27 @@ impl DocumentMessageHandler { use crate::messages::portfolio::document::utility_types::misc::SnapSource; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapConstraint, SnapData, SnapTypeConfiguration}; - let snap_constrained = - |snap_manager: &mut crate::messages::tool::common_functionality::snapping::SnapManager, document: &DocumentMessageHandler, raw_viewport: DVec2, direction: GuideLineDirection| -> DVec2 { - let document_to_viewport = document.metadata().document_to_viewport; - let raw_doc = document_to_viewport.inverse().transform_point2(raw_viewport); - let snap_data = SnapData::new(document, ipp, viewport); - let point = SnapCandidatePoint::new_source(raw_doc, SnapSource::None); - let constraint = match direction { - // Horizontal guide: position varies on Y, constrain snap search along Y - GuideLineDirection::Horizontal => SnapConstraint::Line { origin: raw_doc, direction: DVec2::Y }, - // Vertical guide: position varies on X, constrain snap search along X - GuideLineDirection::Vertical => SnapConstraint::Line { origin: raw_doc, direction: DVec2::X }, - }; - let snapped = snap_manager.constrained_snap(&snap_data, &point, constraint, SnapTypeConfiguration::default()); - snap_manager.update_indicator(snapped.clone()); - document_to_viewport.transform_point2(snapped.snapped_point_document) + let mut snap_manager = std::mem::take(&mut self.guide_snap_manager); + + let mut snap_constrained = |document: &DocumentMessageHandler, raw_viewport: DVec2, direction: GuideLineDirection| -> DVec2 { + let document_to_viewport = document.metadata().document_to_viewport; + let raw_doc = document_to_viewport.inverse().transform_point2(raw_viewport); + let snap_data = SnapData::new(document, ipp, viewport); + let point = SnapCandidatePoint::new_source(raw_doc, SnapSource::None); + let constraint = match direction { + // Horizontal guide: position varies on Y, constrain snap search along Y + GuideLineDirection::Horizontal => SnapConstraint::Line { origin: raw_doc, direction: DVec2::Y }, + // Vertical guide: position varies on X, constrain snap search along X + GuideLineDirection::Vertical => SnapConstraint::Line { origin: raw_doc, direction: DVec2::X }, }; + let snapped = snap_manager.constrained_snap(&snap_data, &point, constraint, SnapTypeConfiguration::default()); + snap_manager.update_indicator(snapped.clone()); + document_to_viewport.transform_point2(snapped.snapped_point_document) + }; - match message { + let result = match message { GuideLineMessage::CreateGuideLine { id, direction, mouse_x, mouse_y } => { - let snap_manager = unsafe { &mut *(&mut self.guide_snap_manager as *mut _) }; - let snapped = snap_constrained(snap_manager, self, DVec2::new(mouse_x, mouse_y), direction); + let snapped = snap_constrained(self, DVec2::new(mouse_x, mouse_y), direction); GuideLineMessage::CreateGuideLine { id, direction, @@ -1976,8 +1979,7 @@ impl DocumentMessageHandler { GuideLineMessage::MoveGuideLine { id, mouse_x, mouse_y } => { if let Some(guide_line) = self.guide_lines_message_handler.guide_lines.iter().find(|g| g.id == id) { let direction = guide_line.direction; - let snap_manager = unsafe { &mut *(&mut self.guide_snap_manager as *mut _) }; - let snapped = snap_constrained(snap_manager, self, DVec2::new(mouse_x, mouse_y), direction); + let snapped = snap_constrained(self, DVec2::new(mouse_x, mouse_y), direction); GuideLineMessage::MoveGuideLine { id, mouse_x: snapped.x, @@ -1988,7 +1990,10 @@ impl DocumentMessageHandler { } } other => other, - } + }; + + self.guide_snap_manager = snap_manager; + result } /// Find the deepest layer given in the sorted array (by returning the one which is not a folder from the list of layers under the click location). @@ -2515,25 +2520,12 @@ impl DocumentMessageHandler { std::mem::replace(&mut self.network_interface, network_interface) } - pub fn undo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque) -> Option { - // If there is no history return and don't broadcast SelectionChanged - let network_interface = self.history.pop_undo()?; - let guide_lines_snapshot = self.history.pop_guide_undo(); - let previous_network = self.install_history_snapshot(network_interface, viewport); - - // Restore guide lines from the snapshot and stash the current state for redo - if let Some(guide_lines) = guide_lines_snapshot { - let current_guides = std::mem::replace(&mut self.guide_lines_message_handler.guide_lines, guide_lines); - self.history.push_guide_redo(current_guides); - responses.add(OverlaysMessage::Draw); -======= pub fn undo(&mut self, viewport: &ViewportMessageHandler, responses: &mut VecDeque) -> Option { let network_snapshot = self.history.pop_undo(); let guide_snapshot = self.history.pop_guide_undo(); if network_snapshot.is_none() && guide_snapshot.is_none() { return None; ->>>>>>> 6cf49e119 (Ai-Review) } if let Some(guide_state) = guide_snapshot { @@ -2545,15 +2537,8 @@ impl DocumentMessageHandler { responses.add(PortfolioMessage::UpdateDocumentWidgets); } - let previous_network = if let Some(mut network_interface) = network_snapshot { - network_interface.copy_all_navigation_metadata(&self.network_interface); - std::mem::swap(&mut network_interface.resolved_types, &mut self.network_interface.resolved_types); - - let transform = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); - network_interface.set_document_to_viewport_transform(transform); - - network_interface.load_structure(); - Some(std::mem::replace(&mut self.network_interface, network_interface)) + let previous_network = if let Some(network_interface) = network_snapshot { + Some(self.install_history_snapshot(network_interface, viewport)) } else { None }; diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 40bd387595..6d8d65fc32 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -518,9 +518,6 @@ struct SelectToolData { selected_layers_changed: bool, snap_candidates: Vec, auto_panning: AutoPanning, - dragging_guide_line_id: Option, - dragging_guide_line_direction: Option, - guide_line_drag_start_position: Option, } impl SelectToolData { @@ -1186,11 +1183,6 @@ impl Fsm for SelectToolFsmState { state } else if let Some((guide_line_id, direction)) = document.guide_lines_message_handler.hit_test(input.mouse.position, document.metadata().document_to_viewport) { responses.add(DocumentMessage::StartTransaction); - tool_data.dragging_guide_line_id = Some(guide_line_id); - tool_data.dragging_guide_line_direction = Some(direction); - - let original_position = document.guide_lines_message_handler.guide_lines.iter().find(|g| g.id == guide_line_id).map(|g| g.position); - tool_data.guide_line_drag_start_position = original_position; SelectToolFsmState::DraggingGuideLine { guide_line_id, direction } } // Dragging one (or two, forming a corner) of the transform cage bounding box edges @@ -1285,9 +1277,6 @@ impl Fsm for SelectToolFsmState { } (SelectToolFsmState::DraggingGuideLine { .. }, SelectToolMessage::Abort) => { responses.add(DocumentMessage::AbortTransaction); - tool_data.dragging_guide_line_id = None; - tool_data.dragging_guide_line_direction = None; - tool_data.guide_line_drag_start_position = None; let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } @@ -1331,9 +1320,6 @@ impl Fsm for SelectToolFsmState { } responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); - tool_data.dragging_guide_line_id = None; - tool_data.dragging_guide_line_direction = None; - tool_data.guide_line_drag_start_position = None; let selection = tool_data.nested_selection_behavior; SelectToolFsmState::Ready { selection } } From 163eb65dfe2af38ee5e0e329d9dfe3c989fd4e44 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Sat, 25 Jul 2026 08:41:58 +0000 Subject: [PATCH 25/26] Fix AI review feedback --- editor/src/messages/portfolio/document/document_history.rs | 1 + .../src/messages/portfolio/document/document_message_handler.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/src/messages/portfolio/document/document_history.rs b/editor/src/messages/portfolio/document/document_history.rs index 1647312fb2..2ef2ab518b 100644 --- a/editor/src/messages/portfolio/document/document_history.rs +++ b/editor/src/messages/portfolio/document/document_history.rs @@ -75,6 +75,7 @@ impl DocumentHistory { } /// Drop the most recently pushed undo snapshot (used to cancel a transaction that ended up unmodified). + #[must_use] pub fn discard_last_undo(&mut self) -> Option { self.legacy_undo_stack.pop_back(); self.guide_undo_stack.pop_back() diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index f237a5da7c..2d0c0ac3fc 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -2503,7 +2503,6 @@ impl DocumentMessageHandler { } } -<<<<<<< HEAD /// Installs a history snapshot as the active network interface, carrying over the current view state and structure load, and returns the replaced interface. fn install_history_snapshot(&mut self, mut network_interface: NodeNetworkInterface, viewport: &ViewportMessageHandler) -> NodeNetworkInterface { // Set the previous network navigation metadata to the current navigation metadata From 0b4721e3e291e7d470c2e86d1170187bd6f4bf69 Mon Sep 17 00:00:00 2001 From: Kulratan Date: Mon, 27 Jul 2026 02:02:25 +0000 Subject: [PATCH 26/26] Fix typescript-eslint type assertion error in isGuideEditor --- frontend/src/components/panels/Document.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 338d23faeb..265e85e70a 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -177,11 +177,11 @@ typeof x === "object" && x !== null && "createGuideLine" in x && - typeof (x as GuideEditor).createGuideLine === "function" && + typeof x.createGuideLine === "function" && "moveGuideLine" in x && - typeof (x as GuideEditor).moveGuideLine === "function" && + typeof x.moveGuideLine === "function" && "deleteGuideLine" in x && - typeof (x as GuideEditor).deleteGuideLine === "function" + typeof x.deleteGuideLine === "function" ); }