From 457619794b6bcbaae7be60bb841a06595e59c51a Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 8 Nov 2024 16:16:20 -0800 Subject: [PATCH] Improve nudging when tilted and add Artboard tool nudge resizing; disable menu bar entries when no layer is selected (#2098) * Make nudging follow a tilted viewport * Add artboard nudge resizing --- .../messages/input_mapper/input_mappings.rs | 48 +++---- .../document/document_message_handler.rs | 117 ++++++++++-------- .../navigation/navigation_message_handler.rs | 18 +-- .../node_graph/node_graph_message_handler.rs | 3 + .../portfolio/document/utility_types/misc.rs | 17 ++- .../menu_bar/menu_bar_message_handler.rs | 36 ++++-- .../portfolio/portfolio_message_handler.rs | 7 ++ .../tool/common_functionality/shape_editor.rs | 8 +- .../transformation_cage.rs | 2 +- .../tool/tool_messages/artboard_tool.rs | 90 ++++++++++++-- libraries/bezier-rs/src/subpath/core.rs | 4 +- node-graph/gcore/src/ops.rs | 2 + 12 files changed, 237 insertions(+), 115 deletions(-) diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 8f3b18aa..b5bf6235 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -109,30 +109,30 @@ pub fn input_mappings() -> Mapping { entry!(KeyUp(MouseLeft); action_dispatch=ArtboardToolMessage::PointerUp), entry!(KeyDown(Delete); action_dispatch=ArtboardToolMessage::DeleteSelected), entry!(KeyDown(Backspace); action_dispatch=ArtboardToolMessage::DeleteSelected), - entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowLeft], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowRight], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowUp); modifiers=[Shift], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowLeft], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowRight], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowDown); modifiers=[Shift], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: 0., delta_y: BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowUp], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowDown], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowLeft); modifiers=[Shift], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: 0. }), - entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowUp], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowDown], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }), - entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }), - entry!(KeyDown(ArrowUp); modifiers=[ArrowLeft], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }), - entry!(KeyDown(ArrowUp); modifiers=[ArrowRight], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }), - entry!(KeyDown(ArrowUp); action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: 0., delta_y: -NUDGE_AMOUNT }), - entry!(KeyDown(ArrowDown); modifiers=[ArrowLeft], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }), - entry!(KeyDown(ArrowDown); modifiers=[ArrowRight], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }), - entry!(KeyDown(ArrowDown); action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: 0., delta_y: NUDGE_AMOUNT }), - entry!(KeyDown(ArrowLeft); modifiers=[ArrowUp], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }), - entry!(KeyDown(ArrowLeft); modifiers=[ArrowDown], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }), - entry!(KeyDown(ArrowLeft); action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: 0. }), - entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }), - entry!(KeyDown(ArrowRight); modifiers=[ArrowDown], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }), - entry!(KeyDown(ArrowRight); action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: 0. }), + entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowLeft], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); modifiers=[Shift, ArrowRight], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); modifiers=[Shift], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowLeft], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[Shift, ArrowRight], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[Shift], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: 0., delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowUp], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[Shift, ArrowDown], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[Shift], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -BIG_NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowUp], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[Shift, ArrowDown], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); modifiers=[ArrowLeft], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); modifiers=[ArrowRight], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowUp); action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: 0., delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[ArrowLeft], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); modifiers=[ArrowRight], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowDown); action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: 0., delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[ArrowUp], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); modifiers=[ArrowDown], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowLeft); action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: -NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); modifiers=[ArrowDown], action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT, resize: Alt, resize_opposite_corner: Control }), + entry!(KeyDown(ArrowRight); action_dispatch=ArtboardToolMessage::NudgeSelected { delta_x: NUDGE_AMOUNT, delta_y: 0., resize: Alt, resize_opposite_corner: Control }), entry!(KeyDown(MouseRight); action_dispatch=ArtboardToolMessage::Abort), entry!(KeyDown(Escape); action_dispatch=ArtboardToolMessage::Abort), // diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 5ec6352c..0475f7c8 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -650,62 +650,77 @@ impl MessageHandler> for DocumentMessag } => { responses.add(DocumentMessage::AddTransaction); - let opposite_corner = ipp.keyboard.key(resize_opposite_corner); - let delta = DVec2::new(delta_x, delta_y); - let network_interface = &self.network_interface; - let can_move = move |layer| { - network_interface + let resize = ipp.keyboard.key(resize); + let resize_opposite_corner = ipp.keyboard.key(resize_opposite_corner); + + let can_move = |layer| { + self.network_interface .selected_nodes(&[]) - .is_some_and(|selected| selected.layer_visible(layer, network_interface) && !selected.layer_locked(layer, network_interface)) + .is_some_and(|selected| selected.layer_visible(layer, &self.network_interface) && !selected.layer_locked(layer, &self.network_interface)) }; - match ipp.keyboard.key(resize) { - // Nudge translation - false => { - for layer in self.network_interface.shallowest_unique_layers(&[]).filter(|layer| can_move(*layer)) { - responses.add(GraphOperationMessage::TransformChange { - layer, - transform: DAffine2::from_translation(delta), - transform_in: TransformIn::Local, - skip_rerender: false, - }); - } + // Nudge translation without resizing + if !resize { + let transform = DAffine2::from_translation(DVec2::from_angle(-self.document_ptz.tilt()).rotate(DVec2::new(delta_x, delta_y))); + + for layer in self.network_interface.shallowest_unique_layers(&[]).filter(|layer| can_move(*layer)) { + responses.add(GraphOperationMessage::TransformChange { + layer, + transform, + transform_in: TransformIn::Local, + skip_rerender: false, + }); } - // Nudge resize - true => { - let selected_bounding_box = self.network_interface.selected_bounds_document_space(false, &[]); - let Some([existing_top_left, existing_bottom_right]) = selected_bounding_box else { return }; - let size = existing_bottom_right - existing_top_left; - let new_size = size + if opposite_corner { -delta } else { delta }; - let enlargement_factor = new_size / size; + return; + } - let position = existing_top_left + if opposite_corner { delta } else { DVec2::ZERO }; - let mut pivot = (existing_top_left * enlargement_factor - position) / (enlargement_factor - DVec2::splat(1.)); - if !pivot.x.is_finite() { - pivot.x = 0.; - } - if !pivot.y.is_finite() { - pivot.y = 0.; - } + let selected_bounding_box = self.network_interface.selected_bounds_document_space(false, &[]); + let Some([existing_top_left, existing_bottom_right]) = selected_bounding_box else { return }; - let scale = DAffine2::from_scale(enlargement_factor); - let pivot = DAffine2::from_translation(pivot); - let transformation = pivot * scale * pivot.inverse(); - let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz); + // Swap and negate coordinates as needed to match the resize direction that's closest to the current tilt angle + let tilt = (self.document_ptz.tilt() + std::f64::consts::TAU) % std::f64::consts::TAU; + let (delta_x, delta_y, opposite_x, opposite_y) = match ((tilt + std::f64::consts::FRAC_PI_4) / std::f64::consts::FRAC_PI_2).floor() as i32 % 4 { + 0 => (delta_x, delta_y, false, false), + 1 => (delta_y, -delta_x, false, true), + 2 => (-delta_x, -delta_y, true, true), + 3 => (-delta_y, delta_x, true, false), + _ => unreachable!(), + }; - for layer in self.network_interface.shallowest_unique_layers(&[]).filter(|layer| can_move(*layer)) { - let to = document_to_viewport.inverse() * self.metadata().downstream_transform_to_viewport(layer); - let original_transform = self.metadata().upstream_transform(layer.to_node()); - let new = to.inverse() * transformation * to * original_transform; - responses.add(GraphOperationMessage::TransformSet { - layer, - transform: new, - transform_in: TransformIn::Local, - skip_rerender: false, - }); - } - } + let size = existing_bottom_right - existing_top_left; + let enlargement = DVec2::new( + if resize_opposite_corner != opposite_x { -delta_x } else { delta_x }, + if resize_opposite_corner != opposite_y { -delta_y } else { delta_y }, + ); + let enlargement_factor = (enlargement + size) / size; + + let position = DVec2::new( + existing_top_left.x + if resize_opposite_corner != opposite_x { delta_x } else { 0. }, + existing_top_left.y + if resize_opposite_corner != opposite_y { delta_y } else { 0. }, + ); + let mut pivot = (existing_top_left * enlargement_factor - position) / (enlargement_factor - DVec2::ONE); + if !pivot.x.is_finite() { + pivot.x = 0.; + } + if !pivot.y.is_finite() { + pivot.y = 0.; + } + let scale = DAffine2::from_scale(enlargement_factor); + let pivot = DAffine2::from_translation(pivot); + let transformation = pivot * scale * pivot.inverse(); + let document_to_viewport = self.navigation_handler.calculate_offset_transform(ipp.viewport_bounds.center(), &self.document_ptz); + + for layer in self.network_interface.shallowest_unique_layers(&[]).filter(|layer| can_move(*layer)) { + let to = document_to_viewport.inverse() * self.metadata().downstream_transform_to_viewport(layer); + let original_transform = self.metadata().upstream_transform(layer.to_node()); + let new = to.inverse() * transformation * to * original_transform; + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: new, + transform_in: TransformIn::Local, + skip_rerender: false, + }); } } DocumentMessage::PasteImage { @@ -1814,7 +1829,7 @@ impl DocumentMessageHandler { widgets.extend(navigation_controls(&self.document_ptz, &self.navigation_handler, "Canvas")); - let tilt_value = self.navigation_handler.snapped_tilt(self.document_ptz.tilt) / (std::f64::consts::PI / 180.); + let tilt_value = self.navigation_handler.snapped_tilt(self.document_ptz.tilt()) / (std::f64::consts::PI / 180.); if tilt_value.abs() > 0.00001 { widgets.extend([ Separator::new(SeparatorType::Related).widget_holder(), @@ -1835,7 +1850,7 @@ impl DocumentMessageHandler { } .into() }) - .tooltip("Document tilt within the viewport") + .tooltip("Canvas Tilt") .on_update(|number_input: &NumberInput| { NavigationMessage::CanvasTiltSet { angle_radians: number_input.value.unwrap().to_radians(), @@ -2178,7 +2193,7 @@ pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHand .tooltip("Reset Tilt and Zoom to 100%") .tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasTiltResetAndZoomTo100Percent)) .on_update(|_| NavigationMessage::CanvasTiltResetAndZoomTo100Percent.into()) - .disabled(ptz.tilt.abs() < 1e-4 && (ptz.zoom() - 1.).abs() < 1e-4) + .disabled(ptz.tilt().abs() < 1e-4 && (ptz.zoom() - 1.).abs() < 1e-4) .widget_holder(), PopoverButton::new() .popover_layout(vec![ diff --git a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs index bef8a6a8..55d7d704 100644 --- a/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs +++ b/editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs @@ -107,8 +107,8 @@ impl MessageHandler> for Navigation }); self.navigation_operation = NavigationOperation::Tilt { - tilt_original_for_abort: ptz.tilt, - tilt_raw_not_snapped: ptz.tilt, + tilt_original_for_abort: ptz.tilt(), + tilt_raw_not_snapped: ptz.tilt(), snap: false, }; @@ -179,7 +179,7 @@ impl MessageHandler> for Navigation log::error!("Could not get mutable PTZ in CanvasTiltResetAndZoomTo100Percent"); return; }; - ptz.tilt = 0.; + ptz.set_tilt(0.); ptz.set_zoom(1.); if graph_view_overlay_open { responses.add(NodeGraphMessage::UpdateGraphBarRight); @@ -194,7 +194,7 @@ impl MessageHandler> for Navigation log::error!("Could not get mutable PTZ in CanvasTiltSet"); return; }; - ptz.tilt = angle_radians; + ptz.set_tilt(angle_radians); responses.add(DocumentMessage::PTZUpdate); if !graph_view_overlay_open { responses.add(PortfolioMessage::UpdateDocumentWidgets); @@ -277,7 +277,7 @@ impl MessageHandler> for Navigation match self.navigation_operation { NavigationOperation::None => {} NavigationOperation::Tilt { tilt_original_for_abort, .. } => { - ptz.tilt = tilt_original_for_abort; + ptz.set_tilt(tilt_original_for_abort); } NavigationOperation::Pan { pan_original_for_abort, .. } => { ptz.pan = pan_original_for_abort; @@ -289,7 +289,7 @@ impl MessageHandler> for Navigation } // Final chance to apply snapping if the key was pressed during this final frame - ptz.tilt = self.snapped_tilt(ptz.tilt); + ptz.set_tilt(self.snapped_tilt(ptz.tilt())); ptz.set_zoom(self.snapped_zoom(ptz.zoom())); responses.add(DocumentMessage::PTZUpdate); if graph_view_overlay_open { @@ -397,7 +397,7 @@ impl MessageHandler> for Navigation log::error!("Could not get mutable PTZ in Tilt"); return; }; - ptz.tilt = self.snapped_tilt(tilt_raw_not_snapped); + ptz.set_tilt(self.snapped_tilt(tilt_raw_not_snapped)); let snap = ipp.keyboard.get(snap as usize); @@ -407,7 +407,7 @@ impl MessageHandler> for Navigation snap, }; - responses.add(NavigationMessage::CanvasTiltSet { angle_radians: ptz.tilt }); + responses.add(NavigationMessage::CanvasTiltSet { angle_radians: ptz.tilt() }); } NavigationOperation::Zoom { zoom_raw_not_snapped, @@ -503,7 +503,7 @@ impl NavigationMessageHandler { pub fn calculate_offset_transform(&self, viewport_center: DVec2, ptz: &PTZ) -> DAffine2 { let pan = ptz.pan; - let tilt = ptz.tilt; + let tilt = ptz.tilt(); let zoom = ptz.zoom(); let scaled_center = viewport_center / self.snapped_zoom(zoom); diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index b6575ca8..1d4a996f 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -116,6 +116,7 @@ impl<'a> MessageHandler> for NodeGrap new_layer: selected_layers.first().cloned(), }); } + responses.add(MenuBarMessage::SendLayout); responses.add(NodeGraphMessage::UpdateLayerPanel); responses.add(NodeGraphMessage::SendSelectedNodes); responses.add(ArtboardToolMessage::UpdateSelectedArtboard); @@ -1701,6 +1702,7 @@ impl NodeGraphMessageHandler { IconLabel::new("File").tooltip("Name of the current document").widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), TextInput::new(context.document_name) + .tooltip("Name of the current document") .on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into()) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), @@ -1748,6 +1750,7 @@ impl NodeGraphMessageHandler { IconLabel::new("Layer").tooltip("Name of the selected layer").widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), TextInput::new(context.network_interface.frontend_display_name(&layer, context.selection_network_path)) + .tooltip("Name of the selected layer") .on_update(move |text_input| { NodeGraphMessage::SetDisplayName { node_id: layer, diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 34610ea5..6e69a1ba 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -444,8 +444,11 @@ impl fmt::Display for SnappingOptions { #[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(default)] pub struct PTZ { + /// Offset distance. pub pan: DVec2, - pub tilt: f64, + /// Angle in radians. + tilt: f64, + /// Scale factor. zoom: f64, } @@ -456,10 +459,22 @@ impl Default for PTZ { } impl PTZ { + /// Get the tilt angle between -180° and 180° in radians. + pub fn tilt(&self) -> f64 { + (((self.tilt + std::f64::consts::PI) % std::f64::consts::TAU) + std::f64::consts::TAU) % std::f64::consts::TAU - std::f64::consts::PI + } + + /// Set a new tilt angle in radians. + pub fn set_tilt(&mut self, tilt: f64) { + self.tilt = tilt; + } + + /// Get the scale factor. pub fn zoom(&self) -> f64 { self.zoom } + /// Set a new scale factor. pub fn set_zoom(&mut self, zoom: f64) { self.zoom = zoom.clamp(crate::consts::VIEWPORT_ZOOM_SCALE_MIN, crate::consts::VIEWPORT_ZOOM_SCALE_MAX) } diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index a88e426e..a50bc2a8 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -7,6 +7,8 @@ pub struct MenuBarMessageData { pub has_active_document: bool, pub rulers_visible: bool, pub node_graph_open: bool, + pub has_selected_nodes: bool, + pub has_selected_layers: bool, } #[derive(Debug, Clone, Default)] @@ -14,6 +16,8 @@ pub struct MenuBarMessageHandler { has_active_document: bool, rulers_visible: bool, node_graph_open: bool, + has_selected_nodes: bool, + has_selected_layers: bool, } impl MessageHandler for MenuBarMessageHandler { @@ -22,10 +26,14 @@ impl MessageHandler for MenuBarMessageHandle has_active_document, rulers_visible, node_graph_open, + has_selected_nodes, + has_selected_layers, } = data; self.has_active_document = has_active_document; self.rulers_visible = rulers_visible; self.node_graph_open = node_graph_open; + self.has_selected_nodes = has_selected_nodes; + self.has_selected_layers = has_selected_layers; match message { MenuBarMessage::SendLayout => self.send_layout(responses, LayoutTarget::MenuBar), @@ -41,6 +49,8 @@ impl LayoutHolder for MenuBarMessageHandler { fn layout(&self) -> Layout { let no_active_document = !self.has_active_document; let node_graph_open = self.node_graph_open; + let has_selected_nodes = self.has_selected_nodes; + let has_selected_layers = self.has_selected_layers; let menu_bar_entries = vec![ MenuBarEntry { @@ -147,7 +157,7 @@ impl LayoutHolder for MenuBarMessageHandler { label: "Cut".into(), shortcut: action_keys!(PortfolioMessageDiscriminant::Cut), action: MenuBarEntry::create_action(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, MenuBarEntry { @@ -155,7 +165,7 @@ impl LayoutHolder for MenuBarMessageHandler { icon: Some("Copy".into()), shortcut: action_keys!(PortfolioMessageDiscriminant::Copy), action: MenuBarEntry::create_action(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, MenuBarEntry { @@ -185,7 +195,7 @@ impl LayoutHolder for MenuBarMessageHandler { label: "Deselect All".into(), shortcut: action_keys!(DocumentMessageDiscriminant::DeselectAllLayers), action: MenuBarEntry::create_action(|_| DocumentMessage::DeselectAllLayers.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_nodes, ..MenuBarEntry::default() }, ], @@ -208,7 +218,7 @@ impl LayoutHolder for MenuBarMessageHandler { icon: Some("Trash".into()), shortcut: action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers), action: MenuBarEntry::create_action(|_| DocumentMessage::DeleteSelectedLayers.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_nodes, ..MenuBarEntry::default() }], vec![ @@ -216,55 +226,55 @@ impl LayoutHolder for MenuBarMessageHandler { label: "Grab Selected".into(), shortcut: action_keys!(TransformLayerMessageDiscriminant::BeginGrab), action: MenuBarEntry::create_action(|_| TransformLayerMessage::BeginGrab.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, MenuBarEntry { label: "Rotate Selected".into(), shortcut: action_keys!(TransformLayerMessageDiscriminant::BeginRotate), action: MenuBarEntry::create_action(|_| TransformLayerMessage::BeginRotate.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, MenuBarEntry { label: "Scale Selected".into(), shortcut: action_keys!(TransformLayerMessageDiscriminant::BeginScale), action: MenuBarEntry::create_action(|_| TransformLayerMessage::BeginScale.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, ], vec![MenuBarEntry { label: "Order".into(), action: MenuBarEntry::no_action(), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, children: MenuBarEntryChildren(vec![vec![ MenuBarEntry { label: "Raise To Front".into(), shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaiseToFront), action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersRaiseToFront.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, MenuBarEntry { label: "Raise".into(), shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaise), action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersRaise.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, MenuBarEntry { label: "Lower".into(), shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLower), action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersLower.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, MenuBarEntry { label: "Lower to Back".into(), shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLowerToBack), action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersLowerToBack.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, ]]), @@ -325,7 +335,7 @@ impl LayoutHolder for MenuBarMessageHandler { label: "Zoom to Fit Selection".into(), shortcut: action_keys!(NavigationMessageDiscriminant::FitViewportToSelection), action: MenuBarEntry::create_action(|_| NavigationMessage::FitViewportToSelection.into()), - disabled: no_active_document, + disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, MenuBarEntry { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 2171cc00..b49c9634 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -50,11 +50,16 @@ impl MessageHandler> for PortfolioMes let mut has_active_document = false; let mut rulers_visible = false; let mut node_graph_open = false; + let mut has_selected_nodes = false; + let mut has_selected_layers = false; if let Some(document) = self.active_document_id.and_then(|document_id| self.documents.get_mut(&document_id)) { has_active_document = true; rulers_visible = document.rulers_visible; node_graph_open = document.is_graph_overlay_open(); + let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap(); + has_selected_nodes = selected_nodes.selected_nodes().next().is_some(); + has_selected_layers = selected_nodes.selected_visible_layers(&document.network_interface).next().is_some(); } self.menu_bar_message_handler.process_message( message, @@ -63,6 +68,8 @@ impl MessageHandler> for PortfolioMes has_active_document, rulers_visible, node_graph_open, + has_selected_nodes, + has_selected_layers, }, ); } diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 70eca293..76d51f4f 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -10,7 +10,7 @@ use bezier_rs::{Bezier, BezierHandles, TValue}; use graphene_core::transform::Transform; use graphene_core::vector::{ManipulatorPointId, PointId, VectorData, VectorModificationType}; -use glam::DVec2; +use glam::{DAffine2, DVec2}; use graphene_std::vector::{HandleId, SegmentId}; #[derive(Debug, PartialEq, Eq, Copy, Clone, Default)] @@ -628,7 +628,11 @@ impl ShapeState { let transform_to_viewport_space = document.metadata().transform_to_viewport(layer); let transform_to_document_space = document.metadata().transform_to_document(layer); - let delta_transform = if in_viewport_space { transform_to_viewport_space } else { transform_to_document_space }; + let delta_transform = if in_viewport_space { + transform_to_viewport_space + } else { + DAffine2::from_angle(document.document_ptz.tilt()) * transform_to_document_space + }; let delta = delta_transform.inverse().transform_vector2(delta); for &point in state.selected_points.iter() { diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index 892d264c..647799c5 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -189,7 +189,7 @@ impl SelectedEdges { if !enlargement_factor.y.is_finite() || old_size.y.abs() < f64::EPSILON * 1000. { enlargement_factor.y = 1.; } - let mut pivot = (self.bounds[0] * enlargement_factor - position) / (enlargement_factor - DVec2::splat(1.)); + let mut pivot = (self.bounds[0] * enlargement_factor - position) / (enlargement_factor - DVec2::ONE); if !pivot.x.is_finite() { pivot.x = 0.; } diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 4ee8112a..8536aac0 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -29,7 +29,7 @@ pub enum ArtboardToolMessage { // Tool-specific messages UpdateSelectedArtboard, DeleteSelected, - NudgeSelected { delta_x: f64, delta_y: f64 }, + NudgeSelected { delta_x: f64, delta_y: f64, resize: Key, resize_opposite_corner: Key }, PointerDown, PointerMove { constrain_axis_or_aspect: Key, center: Key }, PointerOutsideViewport { constrain_axis_or_aspect: Key, center: Key }, @@ -465,19 +465,85 @@ impl Fsm for ArtboardToolFsmState { ArtboardToolFsmState::Ready { hovered } } - (_, ArtboardToolMessage::NudgeSelected { delta_x, delta_y }) => { - if let Some(bounds) = &mut tool_data.bounding_box_manager { - if tool_data.selected_artboard.unwrap() == LayerNodeIdentifier::ROOT_PARENT { - log::error!("Selected artboard cannot be ROOT_PARENT"); - } else { - responses.add(GraphOperationMessage::ResizeArtboard { - layer: tool_data.selected_artboard.unwrap(), - location: DVec2::new(bounds.bounds[0].x + delta_x, bounds.bounds[0].y + delta_y).round().as_ivec2(), - dimensions: (bounds.bounds[1] - bounds.bounds[0]).round().as_ivec2(), - }); - } + ( + _, + ArtboardToolMessage::NudgeSelected { + delta_x, + delta_y, + resize, + resize_opposite_corner, + }, + ) => { + let Some(bounds) = &mut tool_data.bounding_box_manager else { + return ArtboardToolFsmState::Ready { hovered }; + }; + let Some(selected_artboard) = tool_data.selected_artboard else { + return ArtboardToolFsmState::Ready { hovered }; + }; + if selected_artboard == LayerNodeIdentifier::ROOT_PARENT { + log::error!("Selected artboard cannot be ROOT_PARENT"); + return ArtboardToolFsmState::Ready { hovered }; } + let resize = input.keyboard.key(resize); + let resize_opposite_corner = input.keyboard.key(resize_opposite_corner); + let [existing_top_left, existing_bottom_right] = bounds.bounds; + + // Nudge translation without resizing + if !resize { + let delta = DVec2::from_angle(-document.document_ptz.tilt()).rotate(DVec2::new(delta_x, delta_y)); + + responses.add(GraphOperationMessage::ResizeArtboard { + layer: selected_artboard, + location: DVec2::new(existing_top_left.x + delta.x, existing_top_left.y + delta.y).round().as_ivec2(), + dimensions: (existing_bottom_right - existing_top_left).round().as_ivec2(), + }); + + return ArtboardToolFsmState::Ready { hovered }; + } + + // Swap and negate coordinates as needed to match the resize direction that's closest to the current tilt angle + let tilt = (document.document_ptz.tilt() + std::f64::consts::TAU) % std::f64::consts::TAU; + let (delta_x, delta_y, opposite_x, opposite_y) = match ((tilt + std::f64::consts::FRAC_PI_4) / std::f64::consts::FRAC_PI_2).floor() as i32 % 4 { + 0 => (delta_x, delta_y, false, false), + 1 => (delta_y, -delta_x, false, true), + 2 => (-delta_x, -delta_y, true, true), + 3 => (-delta_y, delta_x, true, false), + _ => unreachable!(), + }; + + let size = existing_bottom_right - existing_top_left; + let enlargement = DVec2::new( + if resize_opposite_corner != opposite_x { -delta_x } else { delta_x }, + if resize_opposite_corner != opposite_y { -delta_y } else { delta_y }, + ); + let enlargement_factor = (enlargement + size) / size; + + let position = DVec2::new( + existing_top_left.x + if resize_opposite_corner != opposite_x { delta_x } else { 0. }, + existing_top_left.y + if resize_opposite_corner != opposite_y { delta_y } else { 0. }, + ); + let mut pivot = (existing_top_left * enlargement_factor - position) / (enlargement_factor - DVec2::ONE); + if !pivot.x.is_finite() { + pivot.x = 0.; + } + if !pivot.y.is_finite() { + pivot.y = 0.; + } + let scale = DAffine2::from_scale(enlargement_factor); + let pivot = DAffine2::from_translation(pivot); + let transformation = pivot * scale * pivot.inverse(); + let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz); + let to = document_to_viewport.inverse() * document.metadata().downstream_transform_to_viewport(selected_artboard); + let original_transform = document.metadata().upstream_transform(selected_artboard.to_node()); + let new = to.inverse() * transformation * to * original_transform; + + responses.add(GraphOperationMessage::ResizeArtboard { + layer: selected_artboard, + location: position.round().as_ivec2(), + dimensions: new.transform_vector2(existing_bottom_right - existing_top_left).round().as_ivec2(), + }); + ArtboardToolFsmState::Ready { hovered } } (ArtboardToolFsmState::Dragging | ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds, ArtboardToolMessage::Abort) => { diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index b48db37a..00a5c9a6 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -426,9 +426,9 @@ pub fn solve_spline_first_handle_closed(points: &[DVec2]) -> Vec { // Matrix coefficients `a`, `b` and `c` (see https://mathworld.wolfram.com/CubicSpline.html). // We don't really need to allocate them but it keeps the maths understandable. - let a = vec![DVec2::splat(1.); len_points]; + let a = vec![DVec2::ONE; len_points]; let b = vec![DVec2::splat(4.); len_points]; - let c = vec![DVec2::splat(1.); len_points]; + let c = vec![DVec2::ONE; len_points]; let mut cmod = vec![DVec2::ZERO; len_points]; let mut u = vec![DVec2::ZERO; len_points]; diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index 29f07969..766a6a73 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -163,12 +163,14 @@ fn random( // To u32 #[node_macro::node(name("To u32"), category("Math: Numeric"))] fn to_u32(_: (), #[implementations(f64, f32)] value: U) -> u32 { + let value = U::clamp(value, U::from(0.).unwrap(), U::from(u32::MAX as f64).unwrap()); value.to_u32().unwrap() } // To u64 #[node_macro::node(name("To u64"), category("Math: Numeric"))] fn to_u64(_: (), #[implementations(f64, f32)] value: U) -> u64 { + let value = U::clamp(value, U::from(0.).unwrap(), U::from(u64::MAX as f64).unwrap()); value.to_u64().unwrap() }