From a224c1d233169b426dae48edf7e91c513faa39f1 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Thu, 15 Jan 2026 23:38:54 +0000 Subject: [PATCH 1/9] 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 + .../portfolio/document/document_message.rs | 19 +++ .../document/document_message_handler.rs | 96 +++++++++++++ .../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 | 97 +++++++++++++ .../src/components/panels/Document.svelte | 131 +++++++++++++++++- .../widgets/inputs/RulerInput.svelte | 46 +++++- frontend/wasm/src/editor_api.rs | 48 +++++++ frontend/wasm/src/lib.rs | 19 +++ 18 files changed, 731 insertions(+), 5 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 cfffefefe2..261197d425 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -258,6 +258,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(); @@ -268,6 +269,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 0467abe18c..4cb0b0ad94 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -354,6 +354,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 b06b7397a7..43464e838c 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, @@ -614,6 +615,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.rs b/editor/src/messages/portfolio/document/document_message.rs index b6bc9b63ae..6f34eb476d 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -5,6 +5,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::portfolio::document::data_panel::DataPanelMessage; 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::guide::{GuideDirection, GuideId}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping}; use crate::messages::portfolio::utility_types::PanelType; use crate::messages::prelude::*; @@ -82,6 +83,24 @@ pub enum DocumentMessage { GridVisibility { visible: bool, }, + // Guide messages + CreateGuide { + id: GuideId, + direction: GuideDirection, + position: f64, + }, + MoveGuide { + id: GuideId, + position: f64, + }, + DeleteGuide { + id: GuideId, + }, + GuideOverlays { + context: OverlayContext, + }, + ToggleGuidesVisibility, + SendGuidesData, GroupSelectedLayers { group_folder_type: GroupFolderType, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 6402f609fd..686018f0d3 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1,6 +1,7 @@ use super::node_graph::document_node_definitions; use super::node_graph::utility_types::Transform; 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, SelectedNodes}; @@ -138,6 +139,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, @@ -179,6 +189,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, } } @@ -619,6 +632,85 @@ impl MessageHandler> for DocumentMes self.snapping_state.grid_snapping = visible; responses.add(OverlaysMessage::Draw); } + // Guide messages + DocumentMessage::CreateGuide { id, direction, position } => { + // Convert viewport position to document space + let viewport_point = match direction { + // For horizontal guides: position is Y viewport coordinate + super::utility_types::guide::GuideDirection::Horizontal => DVec2::new(0.0, position), + // For vertical guides: position is X viewport coordinate + super::utility_types::guide::GuideDirection::Vertical => DVec2::new(position, 0.0), + }; + let document_point = self.metadata().document_to_viewport.inverse().transform_point2(viewport_point); + let document_position = match direction { + super::utility_types::guide::GuideDirection::Horizontal => document_point.y, + super::utility_types::guide::GuideDirection::Vertical => document_point.x, + }; + + let guide = Guide::with_id(id, direction, document_position); + match direction { + super::utility_types::guide::GuideDirection::Horizontal => self.horizontal_guides.push(guide), + super::utility_types::guide::GuideDirection::Vertical => self.vertical_guides.push(guide), + } + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + responses.add(DocumentMessage::SendGuidesData); + } + DocumentMessage::MoveGuide { id, position } => { + // Get the transform before mutable borrowing the guides + let viewport_to_document = self.metadata().document_to_viewport.inverse(); + + // Search in both guide lists and update the position + if let Some(guide) = self.horizontal_guides.iter_mut().find(|guide| guide.id == id) { + let viewport_point = DVec2::new(0.0, position); + let document_point = viewport_to_document.transform_point2(viewport_point); + guide.position = document_point.y; + } else if let Some(guide) = self.vertical_guides.iter_mut().find(|guide| guide.id == id) { + let viewport_point = DVec2::new(position, 0.0); + let document_point = viewport_to_document.transform_point2(viewport_point); + guide.position = document_point.x; + } + responses.add(OverlaysMessage::Draw); + responses.add(DocumentMessage::SendGuidesData); + } + DocumentMessage::DeleteGuide { id } => { + // Remove from horizontal guides + self.horizontal_guides.retain(|g| g.id != id); + // Remove from vertical guides + self.vertical_guides.retain(|g| g.id != id); + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + responses.add(DocumentMessage::SendGuidesData); + } + DocumentMessage::GuideOverlays { context: mut overlay_context } => { + if self.guides_visible { + super::overlays::guide_overlays::guide_overlay(self, &mut overlay_context); + } + } + DocumentMessage::ToggleGuidesVisibility => { + self.guides_visible = !self.guides_visible; + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + responses.add(MenuBarMessage::SendLayout); + } + DocumentMessage::SendGuidesData => { + let transform = self.metadata().document_to_viewport; + let horizontal_guides: Vec<(u64, f64)> = self.horizontal_guides.iter().map(|g| (g.id.as_raw(), g.position)).collect(); + let vertical_guides: Vec<(u64, f64)> = self.vertical_guides.iter().map(|g| (g.id.as_raw(), g.position)).collect(); + let document_to_viewport = [ + transform.matrix2.x_axis.x, + transform.matrix2.x_axis.y, + transform.matrix2.y_axis.x, + transform.matrix2.y_axis.y, + transform.translation.x, + transform.translation.y, + ]; + responses.add(FrontendMessage::UpdateGuidesData { + horizontal_guides, + vertical_guides, + document_to_viewport, + }); + } DocumentMessage::GroupSelectedLayers { group_folder_type } => { responses.add(DocumentMessage::AddTransaction); @@ -2991,6 +3083,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 4445dbfe84..2b2218096b 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 3bf436251c..b7b14c4b20 100644 --- a/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs +++ b/editor/src/messages/portfolio/document/overlays/overlays_message_handler.rs @@ -57,6 +57,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(), @@ -74,6 +81,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 c474bf9665..535c405a7f 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -60,6 +60,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, @@ -72,6 +73,7 @@ impl Default for SnappingState { Self { snapping_enabled: true, grid_snapping: false, + guides: true, artboards: true, tolerance: 8., bounding_box: BoundingBoxSnapping::default(), @@ -103,6 +105,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, @@ -531,6 +534,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, @@ -598,6 +618,7 @@ pub enum SnapTarget { Path(PathSnapTarget), Artboard(ArtboardSnapTarget), Grid(GridSnapTarget), + Guide(GuideSnapTarget), Alignment(AlignmentSnapTarget), DistributeEvenly(DistributionSnapTarget), } @@ -619,6 +640,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 8bed0dbb85..6f2862f40b 100644 --- a/editor/src/messages/portfolio/document/utility_types/mod.rs +++ b/editor/src/messages/portfolio/document/utility_types/mod.rs @@ -1,6 +1,7 @@ pub mod clipboards; 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 9b53cce626..e14fadba77 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 608da0bc89..65ca648539 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; @@ -365,6 +366,10 @@ enum SelectToolFsmState { }, RotatingBounds, DraggingPivot, + DraggingGuide { + guide_id: GuideId, + direction: GuideDirection, + }, } impl Default for SelectToolFsmState { @@ -400,6 +405,8 @@ struct SelectToolData { selected_layers_changed: bool, snap_candidates: Vec, auto_panning: AutoPanning, + dragging_guide_id: Option, + dragging_guide_direction: Option, drag_start_center: ViewportPosition, } @@ -591,6 +598,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 = (); @@ -1059,6 +1094,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); @@ -1149,6 +1190,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, @@ -1770,6 +1860,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 18f5f5d74e..5b7703c487 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -55,6 +55,11 @@ let rulerInterval = 100; let rulersVisible = true; + // Guide drag state + let guideIdCounter = BigInt(0); + let draggingGuideId: bigint | undefined = undefined; + let draggingGuideDirection: "Horizontal" | "Vertical" | undefined = undefined; + // Rendered SVG viewport data let artworkSvg = ""; @@ -174,9 +179,117 @@ editor.handle.panCanvas(0, -delta * scrollbarMultiplier.y); } + // Guide Drag Utilities + + type GuideDirection = "Horizontal" | "Vertical"; + + function getViewportElement(): HTMLElement | undefined { + return viewport ?? (window.document.querySelector("[data-viewport]") as HTMLElement) ?? undefined; + } + + function getGuidePosition(event: PointerEvent, viewportRect: DOMRect, direction: GuideDirection): number { + return direction === "Horizontal" ? event.clientY - viewportRect.top : event.clientX - viewportRect.left; + } + + 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 = getViewportElement(); + if (!viewportEl) return null; + + const onMove = (event: PointerEvent) => { + if (draggingGuideId === undefined || !draggingGuideDirection) return; + const rect = viewportEl.getBoundingClientRect(); + const position = getGuidePosition(event, rect, draggingGuideDirection); + editor.handle.moveGuide(draggingGuideId, position); + }; + + const onRelease = (event: PointerEvent) => { + if (draggingGuideId === undefined || !draggingGuideDirection) return; + const rect = viewportEl.getBoundingClientRect(); + if (isInRulerArea(event, rect, draggingGuideDirection)) { + editor.handle.deleteGuide(draggingGuideId); + } + cleanup(); + }; + + const onEscape = (event: KeyboardEvent) => { + if (event.key !== "Escape" || draggingGuideId === undefined) return; + if (options.deleteOnCancel) editor.handle.deleteGuide(draggingGuideId); + cleanup(); + }; + + const onRightClick = (event: MouseEvent) => { + if (draggingGuideId === undefined) return; + event.preventDefault(); + if (options.deleteOnCancel) editor.handle.deleteGuide(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; position: number }>) { + const { direction, position } = e.detail; + + guideIdCounter++; + draggingGuideId = guideIdCounter; + draggingGuideDirection = direction; + + editor.handle.createGuide(guideIdCounter, direction, position); + startGuideDrag({ deleteOnCancel: true }); + } + + function tryStartExistingGuideDrag(e: PointerEvent): boolean { + if (e.button !== 0) return false; + + const viewportEl = getViewportElement(); + if (!viewportEl) return false; + + const rect = viewportEl.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const guideHit = editor.handle.findGuideAtPosition(x, y); + if (!guideHit) return false; + + const { id, direction } = guideHit as { id: bigint; direction: string }; + draggingGuideId = id; + draggingGuideDirection = direction as GuideDirection; + + startGuideDrag({ deleteOnCancel: false }); + e.stopPropagation(); + return true; + } + function canvasPointerDown(e: PointerEvent) { const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable; + if (tryStartExistingGuideDrag(e)) return; + if (!onEditbox) viewport?.setPointerCapture(e.pointerId); if (window.document.activeElement instanceof HTMLElement) { window.document.activeElement.blur(); @@ -546,13 +659,27 @@ {#if rulersVisible} - + {/if} {#if rulersVisible} - + {/if} diff --git a/frontend/src/components/widgets/inputs/RulerInput.svelte b/frontend/src/components/widgets/inputs/RulerInput.svelte index 1c907bcbd3..40258ca979 100644 --- a/frontend/src/components/widgets/inputs/RulerInput.svelte +++ b/frontend/src/components/widgets/inputs/RulerInput.svelte @@ -3,7 +3,7 @@ -
+
{#each svgTexts as svgText} diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 8f386c3879..68cb382be0 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -11,6 +11,7 @@ use editor::messages::clipboard::utility_types::ClipboardContentRaw; use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys; use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta}; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use editor::messages::portfolio::document::utility_types::guide::{GuideDirection, GuideId}; use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport; use editor::messages::portfolio::utility_types::{FontCatalog, FontCatalogFamily, Platform}; use editor::messages::prelude::*; @@ -162,6 +163,21 @@ impl EditorHandle { return; } + // Cache guide data for native mode hit-testing + if let FrontendMessage::UpdateGuidesData { + ref horizontal_guides, + ref vertical_guides, + ref document_to_viewport, + } = message + { + crate::CACHED_GUIDES.with(|cache| { + let mut cache = cache.borrow_mut(); + cache.horizontal_guides = horizontal_guides.clone(); + cache.vertical_guides = vertical_guides.clone(); + cache.document_to_viewport = *document_to_viewport; + }); + } + if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message { message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() }; } @@ -869,6 +885,38 @@ impl EditorHandle { }; self.dispatch(message); } + + /// Create a new guide line from a ruler drag with direction: "Horizontal" or "Vertical" + #[wasm_bindgen(js_name = createGuide)] + pub fn create_guide(&self, id: u64, direction: String, position: f64) { + let id = GuideId::from_raw(id); + let direction = match direction.as_str() { + "Horizontal" => GuideDirection::Horizontal, + "Vertical" => GuideDirection::Vertical, + _ => { + log::error!("Invalid guide direction: {}", direction); + return; + } + }; + let message = DocumentMessage::CreateGuide { id, direction, position }; + self.dispatch(message); + } + + /// Move an existing guide to a new position + #[wasm_bindgen(js_name = moveGuide)] + pub fn move_guide(&self, id: u64, position: f64) { + let id = GuideId::from_raw(id); + let message = DocumentMessage::MoveGuide { id, position }; + self.dispatch(message); + } + + /// Delete a guide by its ID + #[wasm_bindgen(js_name = deleteGuide)] + pub fn delete_guide(&self, id: u64) { + let id = GuideId::from_raw(id); + let message = DocumentMessage::DeleteGuide { id }; + self.dispatch(message); + } } // ============================================================================ diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index d3141288b3..621a9ebacc 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -24,6 +24,25 @@ thread_local! { pub static EDITOR: Mutex> = const { Mutex::new(None) }; pub static MESSAGE_BUFFER: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; pub static EDITOR_HANDLE: Mutex> = const { Mutex::new(None) }; + pub static CACHED_GUIDES: std::cell::RefCell = const { std::cell::RefCell::new(CachedGuideData::new()) }; +} + +/// Cached guide data for native mode hit-testing +#[derive(Default)] +pub struct CachedGuideData { + pub horizontal_guides: Vec<(u64, f64)>, + pub vertical_guides: Vec<(u64, f64)>, + pub document_to_viewport: [f64; 6], +} + +impl CachedGuideData { + pub const fn new() -> Self { + Self { + horizontal_guides: Vec::new(), + vertical_guides: Vec::new(), + document_to_viewport: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], // Identity transform + } + } } /// Initialize the backend From bfa7fb00afaeed77c0b36fe407a68d2808e48fc4 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Fri, 16 Jan 2026 08:27:29 +0000 Subject: [PATCH 2/9] Fixed Guide Movement --- .../tool/tool_messages/select_tool.rs | 40 +++++++++++-------- 1 file changed, 24 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 65ca648539..42508f6935 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -407,6 +407,7 @@ struct SelectToolData { auto_panning: AutoPanning, dragging_guide_id: Option, dragging_guide_direction: Option, + guide_drag_start_position: Option, drag_start_center: ViewportPosition, } @@ -1093,11 +1094,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 @@ -1194,6 +1199,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 } } @@ -1201,26 +1207,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; @@ -1228,14 +1230,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 e3a0a63b1f1dba443a0c8aea68967022cc886c93 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Fri, 16 Jan 2026 15:15:54 +0000 Subject: [PATCH 3/9] 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 42508f6935..2885c04e11 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -1218,6 +1218,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 { .. }) => { @@ -1421,6 +1430,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 0a9610a13f78e8e7ed61d6f1cea38dacc968e5a9 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Fri, 16 Jan 2026 18:53:15 +0000 Subject: [PATCH 4/9] Removed unneccessary code --- .../src/messages/frontend/frontend_message.rs | 8 ------- .../portfolio/document/document_message.rs | 1 - .../document/document_message_handler.rs | 21 ---------------- .../tool/tool_messages/select_tool.rs | 11 ++++----- .../src/components/panels/Document.svelte | 24 ------------------- frontend/wasm/src/editor_api.rs | 15 ------------ frontend/wasm/src/lib.rs | 19 --------------- 7 files changed, 5 insertions(+), 94 deletions(-) diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index 4cb0b0ad94..0467abe18c 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -354,14 +354,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/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 6f34eb476d..d1b3e67835 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -100,7 +100,6 @@ pub enum DocumentMessage { context: OverlayContext, }, ToggleGuidesVisibility, - SendGuidesData, GroupSelectedLayers { group_folder_type: GroupFolderType, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 686018f0d3..89a35026ed 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -654,7 +654,6 @@ impl MessageHandler> for DocumentMes } responses.add(OverlaysMessage::Draw); responses.add(PortfolioMessage::UpdateDocumentWidgets); - responses.add(DocumentMessage::SendGuidesData); } DocumentMessage::MoveGuide { id, position } => { // Get the transform before mutable borrowing the guides @@ -671,7 +670,6 @@ impl MessageHandler> for DocumentMes guide.position = document_point.x; } responses.add(OverlaysMessage::Draw); - responses.add(DocumentMessage::SendGuidesData); } DocumentMessage::DeleteGuide { id } => { // Remove from horizontal guides @@ -680,7 +678,6 @@ impl MessageHandler> for DocumentMes self.vertical_guides.retain(|g| g.id != id); responses.add(OverlaysMessage::Draw); responses.add(PortfolioMessage::UpdateDocumentWidgets); - responses.add(DocumentMessage::SendGuidesData); } DocumentMessage::GuideOverlays { context: mut overlay_context } => { if self.guides_visible { @@ -693,24 +690,6 @@ impl MessageHandler> for DocumentMes responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(MenuBarMessage::SendLayout); } - DocumentMessage::SendGuidesData => { - let transform = self.metadata().document_to_viewport; - let horizontal_guides: Vec<(u64, f64)> = self.horizontal_guides.iter().map(|g| (g.id.as_raw(), g.position)).collect(); - let vertical_guides: Vec<(u64, f64)> = self.vertical_guides.iter().map(|g| (g.id.as_raw(), g.position)).collect(); - let document_to_viewport = [ - transform.matrix2.x_axis.x, - transform.matrix2.x_axis.y, - transform.matrix2.y_axis.x, - transform.matrix2.y_axis.y, - transform.translation.x, - transform.translation.y, - ]; - responses.add(FrontendMessage::UpdateGuidesData { - horizontal_guides, - vertical_guides, - document_to_viewport, - }); - } DocumentMessage::GroupSelectedLayers { group_folder_type } => { responses.add(DocumentMessage::AddTransaction); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 2885c04e11..7f290634a9 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -608,18 +608,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)); } } diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 5b7703c487..55251e0faa 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -263,33 +263,9 @@ startGuideDrag({ deleteOnCancel: true }); } - function tryStartExistingGuideDrag(e: PointerEvent): boolean { - if (e.button !== 0) return false; - - const viewportEl = getViewportElement(); - if (!viewportEl) return false; - - const rect = viewportEl.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - - const guideHit = editor.handle.findGuideAtPosition(x, y); - if (!guideHit) return false; - - const { id, direction } = guideHit as { id: bigint; direction: string }; - draggingGuideId = id; - draggingGuideDirection = direction as GuideDirection; - - startGuideDrag({ deleteOnCancel: false }); - e.stopPropagation(); - return true; - } - function canvasPointerDown(e: PointerEvent) { const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable; - if (tryStartExistingGuideDrag(e)) return; - if (!onEditbox) viewport?.setPointerCapture(e.pointerId); if (window.document.activeElement instanceof HTMLElement) { window.document.activeElement.blur(); diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 68cb382be0..8d9c719583 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -163,21 +163,6 @@ impl EditorHandle { return; } - // Cache guide data for native mode hit-testing - if let FrontendMessage::UpdateGuidesData { - ref horizontal_guides, - ref vertical_guides, - ref document_to_viewport, - } = message - { - crate::CACHED_GUIDES.with(|cache| { - let mut cache = cache.borrow_mut(); - cache.horizontal_guides = horizontal_guides.clone(); - cache.vertical_guides = vertical_guides.clone(); - cache.document_to_viewport = *document_to_viewport; - }); - } - if let FrontendMessage::UpdateDocumentLayerStructure { data_buffer } = message { message = FrontendMessage::UpdateDocumentLayerStructureJs { data_buffer: data_buffer.into() }; } diff --git a/frontend/wasm/src/lib.rs b/frontend/wasm/src/lib.rs index 621a9ebacc..d3141288b3 100644 --- a/frontend/wasm/src/lib.rs +++ b/frontend/wasm/src/lib.rs @@ -24,25 +24,6 @@ thread_local! { pub static EDITOR: Mutex> = const { Mutex::new(None) }; pub static MESSAGE_BUFFER: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; pub static EDITOR_HANDLE: Mutex> = const { Mutex::new(None) }; - pub static CACHED_GUIDES: std::cell::RefCell = const { std::cell::RefCell::new(CachedGuideData::new()) }; -} - -/// Cached guide data for native mode hit-testing -#[derive(Default)] -pub struct CachedGuideData { - pub horizontal_guides: Vec<(u64, f64)>, - pub vertical_guides: Vec<(u64, f64)>, - pub document_to_viewport: [f64; 6], -} - -impl CachedGuideData { - pub const fn new() -> Self { - Self { - horizontal_guides: Vec::new(), - vertical_guides: Vec::new(), - document_to_viewport: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], // Identity transform - } - } } /// Initialize the backend From 96afd072adc0efd005a0768a609cf585dfa13ab6 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 17 Jan 2026 07:52:02 +0000 Subject: [PATCH 5/9] Test --- .../messages/portfolio/document/document_message.rs | 3 +++ .../portfolio/document/document_message_handler.rs | 12 +++++++++++- .../portfolio/document/overlays/guide_overlays.rs | 7 +++++-- .../src/messages/tool/tool_messages/select_tool.rs | 13 ++++++++++--- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index d1b3e67835..2e5fa3aa5d 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -100,6 +100,9 @@ pub enum DocumentMessage { context: OverlayContext, }, ToggleGuidesVisibility, + SetHoveredGuide { + id: Option, + }, GroupSelectedLayers { group_folder_type: GroupFolderType, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 89a35026ed..cf4fcd0d66 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1,7 +1,7 @@ use super::node_graph::document_node_definitions; use super::node_graph::utility_types::Transform; 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, SelectedNodes}; @@ -148,6 +148,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, @@ -192,6 +195,7 @@ impl Default for DocumentMessageHandler { horizontal_guides: Vec::new(), vertical_guides: Vec::new(), guides_visible: true, + hovered_guide_id: None, is_loaded: false, } } @@ -690,6 +694,12 @@ impl MessageHandler> for DocumentMes responses.add(PortfolioMessage::UpdateDocumentWidgets); responses.add(MenuBarMessage::SendLayout); } + DocumentMessage::SetHoveredGuide { id } => { + if self.hovered_guide_id != id { + self.hovered_guide_id = id; + responses.add(OverlaysMessage::Draw); + } + } DocumentMessage::GroupSelectedLayers { group_folder_type } => { responses.add(DocumentMessage::AddTransaction); 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 7f290634a9..24ffdc89b3 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -608,15 +608,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)); @@ -1429,11 +1431,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 564c4e82a0b3d42138128749929878742e98c379 Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 17 Jan 2026 15:57:57 +0000 Subject: [PATCH 6/9] 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 9764f8dc3a43d680fa70d2ef2e0cdad681e3d3cc Mon Sep 17 00:00:00 2001 From: Kulratan Thapar Date: Sat, 17 Jan 2026 18:44:24 +0000 Subject: [PATCH 7/9] Fix guide alignment in case of rotated artboard --- .../document/document_message_handler.rs | 51 ++++++++++++------- .../tool/tool_messages/select_tool.rs | 14 ++--- .../src/components/panels/Document.svelte | 9 ++-- frontend/wasm/src/editor_api.rs | 6 +++ 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index cf4fcd0d66..74501a5a8d 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -638,17 +638,27 @@ impl MessageHandler> for DocumentMes } // Guide messages DocumentMessage::CreateGuide { id, direction, position } => { - // Convert viewport position to document space - let viewport_point = match direction { - // For horizontal guides: position is Y viewport coordinate - super::utility_types::guide::GuideDirection::Horizontal => DVec2::new(0.0, position), - // For vertical guides: position is X viewport coordinate - super::utility_types::guide::GuideDirection::Vertical => DVec2::new(position, 0.0), - }; - let document_point = self.metadata().document_to_viewport.inverse().transform_point2(viewport_point); + // Calculates document-to-viewport transform with offset + let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); + let document_position = match direction { - super::utility_types::guide::GuideDirection::Horizontal => document_point.y, - super::utility_types::guide::GuideDirection::Vertical => document_point.x, + super::utility_types::guide::GuideDirection::Horizontal => { + // Solve: matrix.y_axis.y * doc_y + translation.y = viewport_y + let scale_y = document_to_viewport.matrix2.y_axis.y; + if scale_y.abs() > f64::EPSILON { + (position - document_to_viewport.translation.y) / scale_y + } else { + 0.0 + } + } + super::utility_types::guide::GuideDirection::Vertical => { + let scale_x = document_to_viewport.matrix2.x_axis.x; + if scale_x.abs() > f64::EPSILON { + (position - document_to_viewport.translation.x) / scale_x + } else { + 0.0 + } + } }; let guide = Guide::with_id(id, direction, document_position); @@ -660,18 +670,23 @@ impl MessageHandler> for DocumentMes responses.add(PortfolioMessage::UpdateDocumentWidgets); } DocumentMessage::MoveGuide { id, position } => { - // Get the transform before mutable borrowing the guides - let viewport_to_document = self.metadata().document_to_viewport.inverse(); + // Calculate the correct document-to-viewport transform with offset + let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); // Search in both guide lists and update the position + // Use the same formula as CreateGuide to solve for document position if let Some(guide) = self.horizontal_guides.iter_mut().find(|guide| guide.id == id) { - let viewport_point = DVec2::new(0.0, position); - let document_point = viewport_to_document.transform_point2(viewport_point); - guide.position = document_point.y; + // Solve: matrix.y_axis.y * doc_y + translation.y = viewport_y + let scale_y = document_to_viewport.matrix2.y_axis.y; + if scale_y.abs() > f64::EPSILON { + guide.position = (position - document_to_viewport.translation.y) / scale_y; + } } else if let Some(guide) = self.vertical_guides.iter_mut().find(|guide| guide.id == id) { - let viewport_point = DVec2::new(position, 0.0); - let document_point = viewport_to_document.transform_point2(viewport_point); - guide.position = document_point.x; + // Solve: matrix.x_axis.x * doc_x + translation.x = viewport_x + let scale_x = document_to_viewport.matrix2.x_axis.x; + if scale_x.abs() > f64::EPSILON { + guide.position = (position - document_to_viewport.translation.x) / scale_x; + } } responses.add(OverlaysMessage::Draw); } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 24ffdc89b3..a802436d8a 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -599,17 +599,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 { @@ -617,7 +618,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 { @@ -1095,7 +1095,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); @@ -1432,7 +1432,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, diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 55251e0faa..7fb84c3d7b 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -1,6 +1,8 @@