diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index c6dd3f7aae..534d10cf18 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -255,6 +255,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.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(); @@ -265,6 +266,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.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 1b620ea7d2..953c12f9c5 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_lines_message::GuideLinesMessage; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; use crate::messages::prelude::*; use graphene_std::vector::misc::BooleanOperation; @@ -11,6 +12,7 @@ pub struct MenuBarMessageHandler { pub canvas_tilted: bool, pub canvas_flipped: bool, pub rulers_visible: bool, + pub guide_lines_visible: bool, pub node_graph_open: bool, pub has_selected_nodes: bool, pub has_selected_layers: bool, @@ -628,6 +630,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.guide_lines_visible { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .on_commit(|_| GuideLinesMessage::ToggleGuideLinesVisibility.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 49fadb6a74..273ad6156b 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_lines_message::GuideLinesMessage; 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}; @@ -37,6 +38,8 @@ pub enum DocumentMessage { PropertiesPanel(PropertiesPanelMessage), #[child] DataPanel(DataPanelMessage), + #[child] + GuideLines(GuideLinesMessage), // 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 8654fbdfec..037d909565 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -12,6 +12,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::TransformIn; +use crate::messages::portfolio::document::guide_lines_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; @@ -79,6 +80,8 @@ pub struct DocumentMessageHandler { pub properties_panel_message_handler: PropertiesPanelMessageHandler, #[serde(skip)] pub data_panel_message_handler: DataPanelMessageHandler, + #[serde(flatten)] + pub guide_lines_message_handler: GuideLinesMessageHandler, // ============================================ // Fields that are saved in the document format @@ -157,6 +160,7 @@ impl Default for DocumentMessageHandler { overlays_message_handler: OverlaysMessageHandler::default(), properties_panel_message_handler: PropertiesPanelMessageHandler::default(), data_panel_message_handler: DataPanelMessageHandler::default(), + guide_lines_message_handler: GuideLinesMessageHandler::default(), // ============================================ // Fields that are saved in the document format // ============================================ @@ -182,6 +186,7 @@ impl Default for DocumentMessageHandler { saved_hash: None, auto_saved_hash: None, layer_range_selection_reference: None, + is_loaded: false, } } @@ -218,6 +223,14 @@ impl MessageHandler> for DocumentMes self.navigation_handler.process_message(message, responses, context); } + DocumentMessage::GuideLines(message) => { + let context = GuideLinesMessageContext { + navigation_handler: &self.navigation_handler, + document_ptz: &self.document_ptz, + viewport, + }; + self.guide_lines_message_handler.process_message(message, responses, context); + } DocumentMessage::Overlays(message) => { let visibility_settings = self.overlays_visibility_settings; @@ -1481,6 +1494,7 @@ impl MessageHandler> for DocumentMes ZoomCanvasTo200Percent, ZoomCanvasToFitAll, ); + 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_lines_message.rs b/editor/src/messages/portfolio/document/guide_lines_message.rs new file mode 100644 index 0000000000..6ff121254a --- /dev/null +++ b/editor/src/messages/portfolio/document/guide_lines_message.rs @@ -0,0 +1,29 @@ +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::guide_line::{GuideLineDirection, GuideLineId}; +use crate::messages::prelude::*; + +#[impl_message(Message, DocumentMessage, GuideLines)] +#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum GuideLinesMessage { + CreateGuideLine { + id: GuideLineId, + direction: GuideLineDirection, + mouse_x: f64, + mouse_y: f64, + }, + MoveGuideLine { + id: GuideLineId, + mouse_x: f64, + mouse_y: f64, + }, + DeleteGuideLine { + id: GuideLineId, + }, + GuideLineOverlays { + context: OverlayContext, + }, + ToggleGuideLinesVisibility, + SetHoveredGuideLine { + id: Option, + }, +} diff --git a/editor/src/messages/portfolio/document/guide_lines_message_handler.rs b/editor/src/messages/portfolio/document/guide_lines_message_handler.rs new file mode 100644 index 0000000000..3780c28875 --- /dev/null +++ b/editor/src/messages/portfolio/document/guide_lines_message_handler.rs @@ -0,0 +1,102 @@ +use super::utility_types::guide_line::{GuideLine, GuideLineDirection, GuideLineId}; +use crate::messages::portfolio::document::guide_lines_message::{GuideLinesMessage, GuideLinesMessageDiscriminant}; +use crate::messages::portfolio::document::overlays::guide_line_overlays::guide_line_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 GuideLinesMessageHandler { + pub guide_lines: Vec, + pub guide_lines_visible: bool, + #[serde(skip)] + pub hovered_guide_line_id: Option, +} + +impl Default for GuideLinesMessageHandler { + fn default() -> Self { + Self { + guide_lines: Vec::new(), + guide_lines_visible: true, + hovered_guide_line_id: None, + } + } +} + +#[derive(ExtractField)] +pub struct GuideLinesMessageContext<'a> { + pub navigation_handler: &'a NavigationMessageHandler, + pub document_ptz: &'a PTZ, + pub viewport: &'a ViewportMessageHandler, +} + +#[message_handler_data] +impl MessageHandler> for GuideLinesMessageHandler { + fn actions(&self) -> ActionList { + actions!(GuideLinesMessageDiscriminant; ToggleGuideLinesVisibility) + } + + fn process_message(&mut self, message: GuideLinesMessage, responses: &mut VecDeque, context: GuideLinesMessageContext) { + let GuideLinesMessageContext { + navigation_handler, + document_ptz, + viewport, + } = context; + + let viewport_to_document_point = |mouse_x: f64, mouse_y: f64| -> DVec2 { + let document_to_viewport = navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), document_ptz); + document_to_viewport.inverse().transform_point2(DVec2::new(mouse_x, mouse_y)) + }; + + match message { + GuideLinesMessage::CreateGuideLine { id, direction, mouse_x, mouse_y } => { + let document_point = viewport_to_document_point(mouse_x, mouse_y); + + let document_position = match direction { + GuideLineDirection::Horizontal => document_point.y, + GuideLineDirection::Vertical => document_point.x, + }; + + let guide = GuideLine::with_id(id, direction, document_position); + self.guide_lines.push(guide); + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + } + GuideLinesMessage::MoveGuideLine { id, mouse_x, mouse_y } => { + let document_point = viewport_to_document_point(mouse_x, mouse_y); + + if let Some(guide) = self.guide_lines.iter_mut().find(|guide| guide.id == id) { + guide.position = match guide.direction { + GuideLineDirection::Horizontal => document_point.y, + GuideLineDirection::Vertical => document_point.x, + }; + } + responses.add(OverlaysMessage::Draw); + } + GuideLinesMessage::DeleteGuideLine { id } => { + self.guide_lines.retain(|g| g.id != id); + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + } + GuideLinesMessage::GuideLineOverlays { context: mut overlay_context } => { + if self.guide_lines_visible { + let document_to_viewport = navigation_handler.calculate_offset_transform(overlay_context.viewport.center_in_viewport_space().into(), document_ptz); + guide_line_overlay(self, &mut overlay_context, document_to_viewport); + } + } + GuideLinesMessage::ToggleGuideLinesVisibility => { + self.guide_lines_visible = !self.guide_lines_visible; + responses.add(OverlaysMessage::Draw); + responses.add(PortfolioMessage::UpdateDocumentWidgets); + responses.add(MenuBarMessage::SendLayout); + } + GuideLinesMessage::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/mod.rs b/editor/src/messages/portfolio/document/mod.rs index 767126b248..6199fc338b 100644 --- a/editor/src/messages/portfolio/document/mod.rs +++ b/editor/src/messages/portfolio/document/mod.rs @@ -3,6 +3,8 @@ mod document_message_handler; pub mod data_panel; pub mod graph_operation; +pub mod guide_lines_message; +pub mod guide_lines_message_handler; pub mod navigation; pub mod node_graph; pub mod overlays; diff --git a/editor/src/messages/portfolio/document/overlays/guide_line_overlays.rs b/editor/src/messages/portfolio/document/overlays/guide_line_overlays.rs new file mode 100644 index 0000000000..c0592c737f --- /dev/null +++ b/editor/src/messages/portfolio/document/overlays/guide_line_overlays.rs @@ -0,0 +1,57 @@ +use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50}; +use crate::messages::portfolio::document::guide_lines_message_handler::GuideLinesMessageHandler; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::guide_line::GuideLineDirection; +use glam::{DAffine2, DVec2}; + +fn extend_line_to_viewport(point: DVec2, direction: DVec2, viewport_size: DVec2) -> Option<(DVec2, DVec2)> { + let dir = direction.try_normalize()?; + + // Calculates t values for intersections with viewport edges + let mut t_values = Vec::new(); + + 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); + } + } + + 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_line_overlay(guide_lines_message_handler: &GuideLinesMessageHandler, overlay_context: &mut OverlayContext, document_to_viewport: DAffine2) { + let viewport_size: DVec2 = overlay_context.viewport.size().into(); + + for guide in &guide_lines_message_handler.guide_lines { + let (doc_point, doc_direction) = match guide.direction { + GuideLineDirection::Horizontal => (DVec2::new(0.0, guide.position), DVec2::X), + GuideLineDirection::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 guide_lines_message_handler.hovered_guide_line_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/mod.rs b/editor/src/messages/portfolio/document/overlays/mod.rs index 514ccd7b63..e54e58bdf0 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_line_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..d4c0eba3da 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,6 @@ use super::utility_types::{OverlayProvider, OverlaysVisibilitySettings}; +#[cfg(not(test))] +use crate::messages::portfolio::document::guide_lines_message::GuideLinesMessage; use crate::messages::prelude::*; #[derive(ExtractField)] @@ -57,6 +59,13 @@ impl MessageHandler> for OverlaysMes viewport: *viewport, }, }); + responses.add(GuideLinesMessage::GuideLineOverlays { + 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 +83,7 @@ impl MessageHandler> for OverlaysMes if visibility_settings.all() { responses.add(DocumentMessage::GridOverlays { context: overlay_context.clone() }); + responses.add(GuideLinesMessage::GuideLineOverlays { 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_line.rs b/editor/src/messages/portfolio/document/utility_types/guide_line.rs new file mode 100644 index 0000000000..ef9acc106e --- /dev/null +++ b/editor/src/messages/portfolio/document/utility_types/guide_line.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 GuideLineId(u64); + +impl GuideLineId { + 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 GuideLineId { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub enum GuideLineDirection { + Horizontal, + Vertical, +} + +#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] +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 GuideLine { + pub fn new(direction: GuideLineDirection, position: f64) -> Self { + Self { + id: GuideLineId::new(), + direction, + position, + } + } + + pub fn with_id(id: GuideLineId, direction: GuideLineDirection, position: f64) -> Self { + Self { id, direction, position } + } + + pub fn horizontal(y: f64) -> Self { + Self::new(GuideLineDirection::Horizontal, y) + } + + pub fn vertical(x: f64) -> Self { + Self::new(GuideLineDirection::Vertical, x) + } +} diff --git a/editor/src/messages/portfolio/document/utility_types/mod.rs b/editor/src/messages/portfolio/document/utility_types/mod.rs index 8bed0dbb85..ab8f9275eb 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_line; pub mod misc; pub mod network_interface; pub mod nodes; diff --git a/frontend/src/components/panels/Document.svelte b/frontend/src/components/panels/Document.svelte index 17ef4d1ad4..ef2ccbd373 100644 --- a/frontend/src/components/panels/Document.svelte +++ b/frontend/src/components/panels/Document.svelte @@ -17,6 +17,7 @@ import { textInputCleanup } from "/src/utility-functions/keyboard-entry"; import { rasterizeSVGCanvas } from "/src/utility-functions/rasterization"; import { setupViewportResizeObserver } from "/src/utility-functions/viewports"; + import { generateGuideLineId } from "/wrapper/pkg/graphite_wasm_wrapper"; import type { Color, EditorWrapper, MenuDirection, MouseCursorIcon } from "/wrapper/pkg/graphite_wasm_wrapper"; let rulerHorizontal: RulerInput | undefined; @@ -45,6 +46,10 @@ let rulerInterval = 100; let rulersVisible = true; + // Guide drag state + let draggingGuideLineId: bigint | undefined = undefined; + let draggingGuideLineDirection: "Horizontal" | "Vertical" | undefined = undefined; + // Rendered SVG viewport data let artworkSvg = ""; @@ -150,6 +155,91 @@ editor.panCanvas(0, -delta * scrollbarMultiplier.y); } + type GuideLineDirection = "Horizontal" | "Vertical"; + + function getViewportElement(): HTMLElement | undefined { + return viewport; + } + + function getGuideLineMousePosition(event: PointerEvent, viewportRect: DOMRect): { mouseX: number; mouseY: number } { + return { + mouseX: event.clientX - viewportRect.left, + mouseY: event.clientY - viewportRect.top, + }; + } + + function isInRulerArea(event: PointerEvent, viewportRect: DOMRect, direction: GuideLineDirection): boolean { + return direction === "Horizontal" ? event.clientY < viewportRect.top : event.clientX < viewportRect.left; + } + + function createGuideLineDragHandlers(options: { deleteOnCancel: boolean }) { + const viewportEl = getViewportElement(); + if (!viewportEl) return null; + + const onMove = (event: PointerEvent) => { + if (draggingGuideLineId === undefined || !draggingGuideLineDirection) return; + const rect = viewportEl.getBoundingClientRect(); + const { mouseX, mouseY } = getGuideLineMousePosition(event, rect); + editor.moveGuideLine(draggingGuideLineId, mouseX, mouseY); + }; + + const onRelease = (event: PointerEvent) => { + if (draggingGuideLineId === undefined || !draggingGuideLineDirection) return; + const rect = viewportEl.getBoundingClientRect(); + if (isInRulerArea(event, rect, draggingGuideLineDirection)) { + editor.deleteGuideLine(draggingGuideLineId); + } + cleanup(); + }; + + const onEscape = (event: KeyboardEvent) => { + if (event.key !== "Escape" || draggingGuideLineId === undefined) return; + if (options.deleteOnCancel) editor.deleteGuideLine(draggingGuideLineId); + cleanup(); + }; + + const onRightClick = (event: MouseEvent) => { + if (draggingGuideLineId === undefined) return; + event.preventDefault(); + if (options.deleteOnCancel) editor.deleteGuideLine(draggingGuideLineId); + cleanup(); + }; + + const cleanup = () => { + draggingGuideLineId = undefined; + draggingGuideLineDirection = undefined; + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onRelease); + window.removeEventListener("keydown", onEscape); + window.removeEventListener("contextmenu", onRightClick); + }; + + return { onMove, onRelease, onEscape, onRightClick }; + } + + function startGuideLineDrag(options: { deleteOnCancel: boolean }) { + const handlers = createGuideLineDragHandlers(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 handleGuideLineDragStart(e: CustomEvent<{ direction: GuideLineDirection; mouseX: number; mouseY: number }>) { + const { direction, mouseX, mouseY } = e.detail; + + const guideLineId = generateGuideLineId(); + draggingGuideLineId = guideLineId; + draggingGuideLineDirection = direction; + + editor.createGuideLine(guideLineId, direction, mouseX, mouseY); + startGuideLineDrag({ deleteOnCancel: true }); + } + function canvasPointerDown(e: PointerEvent) { const onEditbox = e.target instanceof HTMLDivElement && e.target.contentEditable; @@ -595,13 +685,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 1c8511ace7..e215c6655a 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} diff --git a/frontend/wrapper/src/editor_wrapper.rs b/frontend/wrapper/src/editor_wrapper.rs index 6cd530cb68..4204492a53 100644 --- a/frontend/wrapper/src/editor_wrapper.rs +++ b/frontend/wrapper/src/editor_wrapper.rs @@ -16,7 +16,9 @@ use editor::consts::FILE_EXTENSION; 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::guide_lines_message::GuideLinesMessage; use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use editor::messages::portfolio::document::utility_types::guide_line::{GuideLineDirection, GuideLineId}; use editor::messages::portfolio::document::utility_types::network_interface::ImportOrExport; use editor::messages::portfolio::utility_types::{FontCatalog, FontCatalogFamily}; use editor::messages::prelude::*; @@ -34,6 +36,12 @@ use wasm_bindgen::prelude::*; static IMAGE_DATA_HASH: AtomicU64 = AtomicU64::new(0); +/// Generates a unique guide ID +#[wasm_bindgen(js_name = generateGuideLineId)] +pub fn generate_guide_id() -> u64 { + editor::application::generate_uuid() +} + /// This struct is, via wasm-bindgen, used by JS to interact with the editor backend. It does this by calling functions, which are `impl`ed #[wasm_bindgen] #[derive(Clone)] @@ -926,6 +934,38 @@ impl EditorWrapper { }; self.dispatch(message); } + + /// Create a new guide line from a ruler drag + #[wasm_bindgen(js_name = createGuideLine)] + pub fn create_guide(&self, id: u64, direction: String, mouse_x: f64, mouse_y: f64) { + let id = GuideLineId::from_raw(id); + let direction = match direction.as_str() { + "Horizontal" => GuideLineDirection::Horizontal, + "Vertical" => GuideLineDirection::Vertical, + _ => { + log::error!("Invalid guide direction: {}", direction); + return; + } + }; + let message = GuideLinesMessage::CreateGuideLine { id, direction, mouse_x, mouse_y }; + self.dispatch(message); + } + + /// Move an existing guide to a new position + #[wasm_bindgen(js_name = moveGuideLine)] + pub fn move_guide(&self, id: u64, mouse_x: f64, mouse_y: f64) { + let id = GuideLineId::from_raw(id); + let message = GuideLinesMessage::MoveGuideLine { id, mouse_x, mouse_y }; + self.dispatch(message); + } + + /// Delete a guide by its ID + #[wasm_bindgen(js_name = deleteGuideLine)] + pub fn delete_guide(&self, id: u64) { + let id = GuideLineId::from_raw(id); + let message = GuideLinesMessage::DeleteGuideLine { id }; + self.dispatch(message); + } } // ====================================================================