Skip to content

Commit 2615d86

Browse files
Sidharth-Singh100HyperCubeKeavon
authored
Add PTZ support for flipping the canvas (#2394)
* feat: flip canvas * move canvas_flipped from NavigationMessageHandler to PTZ * fix artboard overlay flip * Code review * Improvements --------- Co-authored-by: hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent a8e209e commit 2615d86

File tree

9 files changed

+95
-47
lines changed

9 files changed

+95
-47
lines changed

editor/src/messages/input_mapper/input_mappings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,7 @@ pub fn input_mappings() -> Mapping {
400400
entry!(KeyDown(NumpadAdd); modifiers=[Accel], action_dispatch=NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }),
401401
entry!(KeyDown(Equal); modifiers=[Accel], action_dispatch=NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }),
402402
entry!(KeyDown(Minus); modifiers=[Accel], action_dispatch=NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }),
403+
entry!(KeyDown(KeyF); modifiers=[Alt], action_dispatch=NavigationMessage::CanvasFlip),
403404
entry!(WheelScroll; modifiers=[Control], action_dispatch=NavigationMessage::CanvasZoomMouseWheel),
404405
entry!(WheelScroll; modifiers=[Shift], action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: true }),
405406
entry!(WheelScroll; action_dispatch=NavigationMessage::CanvasPanMouseWheel { use_y_as_x: false }),

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

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -359,13 +359,29 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
359359
continue;
360360
}
361361
let Some(bounds) = self.metadata().bounding_box_document(layer) else { continue };
362+
let min = bounds[0].min(bounds[1]);
363+
let max = bounds[0].max(bounds[1]);
362364

363365
let name = self.network_interface.display_name(&layer.to_node(), &[]);
364366

367+
// Calculate position of the text
368+
let corner_pos = if !self.document_ptz.flip {
369+
// Use the top-left corner
370+
min
371+
} else {
372+
// Use the top-right corner, which appears to be the top-left due to being flipped
373+
DVec2::new(max.x, min.y)
374+
};
375+
376+
// When the canvas is flipped, mirror the text so it appears correctly
377+
let scale = if !self.document_ptz.flip { DVec2::ONE } else { DVec2::new(-1., 1.) };
378+
379+
// Create a transform that puts the text at the true top-left regardless of flip
365380
let transform = self.metadata().document_to_viewport
366-
* DAffine2::from_translation(bounds[0].min(bounds[1]))
381+
* DAffine2::from_translation(corner_pos)
367382
* DAffine2::from_scale(DVec2::splat(self.document_ptz.zoom().recip()))
368-
* DAffine2::from_translation(-DVec2::Y * 4.);
383+
* DAffine2::from_translation(-DVec2::Y * 4.)
384+
* DAffine2::from_scale(scale);
369385

370386
overlay_context.text(&name, COLOR_OVERLAY_GRAY, None, transform, 0., [Pivot::Start, Pivot::End]);
371387
}
@@ -1477,6 +1493,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
14771493
self.network_interface.document_bounds_document_space(true)
14781494
};
14791495
if let Some(bounds) = bounds {
1496+
if self.document_ptz.flip {
1497+
responses.add(NavigationMessage::CanvasFlip);
1498+
}
14801499
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
14811500
responses.add(NavigationMessage::FitViewportToBounds { bounds, prevent_zoom_past_100: true });
14821501
} else {
@@ -2365,7 +2384,7 @@ impl DocumentMessageHandler {
23652384
Separator::new(SeparatorType::Unrelated).widget_holder(),
23662385
];
23672386

2368-
widgets.extend(navigation_controls(&self.document_ptz, &self.navigation_handler, "Canvas"));
2387+
widgets.extend(navigation_controls(&self.document_ptz, &self.navigation_handler, false));
23692388

23702389
let tilt_value = self.navigation_handler.snapped_tilt(self.document_ptz.tilt()) / (std::f64::consts::PI / 180.);
23712390
if tilt_value.abs() > 0.00001 {
@@ -2817,8 +2836,8 @@ impl<'a> ClickXRayIter<'a> {
28172836
}
28182837
}
28192838

2820-
pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHandler, tooltip_name: &str) -> [WidgetHolder; 5] {
2821-
[
2839+
pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHandler, node_graph: bool) -> Vec<WidgetHolder> {
2840+
let mut list = vec![
28222841
IconButton::new("ZoomIn", 24)
28232842
.tooltip("Zoom In")
28242843
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasZoomIncrease))
@@ -2835,40 +2854,23 @@ pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHand
28352854
.on_update(|_| NavigationMessage::CanvasTiltResetAndZoomTo100Percent.into())
28362855
.disabled(ptz.tilt().abs() < 1e-4 && (ptz.zoom() - 1.).abs() < 1e-4)
28372856
.widget_holder(),
2838-
// PopoverButton::new()
2839-
// .popover_layout(vec![
2840-
// LayoutGroup::Row {
2841-
// widgets: vec![TextLabel::new(format!("{tooltip_name} Navigation")).bold(true).widget_holder()],
2842-
// },
2843-
// LayoutGroup::Row {
2844-
// widgets: vec![TextLabel::new({
2845-
// let tilt = if tooltip_name == "Canvas" { "Tilt:\n• Alt + Middle Click Drag\n\n" } else { "" };
2846-
// format!(
2847-
// "
2848-
// Interactive controls in this\n\
2849-
// menu are coming soon.\n\
2850-
// \n\
2851-
// Pan:\n\
2852-
// • Middle Click Drag\n\
2853-
// \n\
2854-
// {tilt}Zoom:\n\
2855-
// • Shift + Middle Click Drag\n\
2856-
// • Ctrl + Scroll Wheel Roll
2857-
// "
2858-
// )
2859-
// .trim()
2860-
// })
2861-
// .multiline(true)
2862-
// .widget_holder()],
2863-
// },
2864-
// ])
2865-
// .widget_holder(),
2857+
];
2858+
if ptz.flip && !node_graph {
2859+
list.push(
2860+
IconButton::new("Reverse", 24)
2861+
.tooltip("Flip the canvas back to its standard orientation")
2862+
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasFlip))
2863+
.on_update(|_| NavigationMessage::CanvasFlip.into())
2864+
.widget_holder(),
2865+
);
2866+
}
2867+
list.extend([
28662868
Separator::new(SeparatorType::Related).widget_holder(),
28672869
NumberInput::new(Some(navigation_handler.snapped_zoom(ptz.zoom()) * 100.))
28682870
.unit("%")
28692871
.min(0.000001)
28702872
.max(1000000.)
2871-
.tooltip(format!("{tooltip_name} Zoom"))
2873+
.tooltip(if node_graph { "Node Graph Zoom" } else { "Canvas Zoom" })
28722874
.on_update(|number_input: &NumberInput| {
28732875
NavigationMessage::CanvasZoomSet {
28742876
zoom_factor: number_input.value.unwrap() / 100.,
@@ -2879,7 +2881,8 @@ pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHand
28792881
.increment_callback_decrease(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into())
28802882
.increment_callback_increase(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into())
28812883
.widget_holder(),
2882-
]
2884+
]);
2885+
list
28832886
}
28842887

28852888
impl Iterator for ClickXRayIter<'_> {

editor/src/messages/portfolio/document/navigation/navigation_message.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub enum NavigationMessage {
2020
CanvasZoomIncrease { center_on_mouse: bool },
2121
CanvasZoomMouseWheel,
2222
CanvasZoomSet { zoom_factor: f64 },
23+
CanvasFlip,
2324
EndCanvasPTZ { abort_transform: bool },
2425
EndCanvasPTZWithClick { commit_key: Key },
2526
FitViewportToBounds { bounds: [DVec2; 2], prevent_zoom_past_100: bool },

editor/src/messages/portfolio/document/navigation/navigation_message_handler.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
204204
responses.add(DocumentMessage::PTZUpdate);
205205
if !graph_view_overlay_open {
206206
responses.add(PortfolioMessage::UpdateDocumentWidgets);
207+
responses.add(MenuBarMessage::SendLayout);
207208
}
208209
}
209210
NavigationMessage::CanvasZoomDecrease { center_on_mouse } => {
@@ -273,6 +274,22 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
273274
responses.add(DocumentMessage::PTZUpdate);
274275
responses.add(NodeGraphMessage::SetGridAlignedEdges);
275276
}
277+
NavigationMessage::CanvasFlip => {
278+
if graph_view_overlay_open {
279+
return;
280+
}
281+
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
282+
log::error!("Could not get mutable PTZ in CanvasFlip");
283+
return;
284+
};
285+
286+
ptz.flip = !ptz.flip;
287+
288+
responses.add(DocumentMessage::PTZUpdate);
289+
responses.add(BroadcastEvent::CanvasTransformed);
290+
responses.add(MenuBarMessage::SendLayout);
291+
responses.add(PortfolioMessage::UpdateDocumentWidgets);
292+
}
276293
NavigationMessage::EndCanvasPTZ { abort_transform } => {
277294
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
278295
log::error!("Could not get mutable PTZ in EndCanvasPTZ");
@@ -393,9 +410,11 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
393410
..
394411
} => {
395412
let tilt_raw_not_snapped = {
413+
// Compute the angle in document space to counter for the canvas being flipped
414+
let viewport_to_document = network_interface.document_metadata().document_to_viewport.inverse();
396415
let half_viewport = ipp.viewport_bounds.size() / 2.;
397-
let start_offset = self.mouse_position - half_viewport;
398-
let end_offset = ipp.mouse.position - half_viewport;
416+
let start_offset = viewport_to_document.transform_vector2(self.mouse_position - half_viewport);
417+
let end_offset = viewport_to_document.transform_vector2(ipp.mouse.position - half_viewport);
399418
let angle = start_offset.angle_to(end_offset);
400419

401420
tilt_raw_not_snapped + angle
@@ -471,6 +490,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
471490
CanvasZoomDecrease,
472491
CanvasZoomIncrease,
473492
CanvasZoomMouseWheel,
493+
CanvasFlip,
474494
FitViewportToSelection,
475495
);
476496

@@ -513,15 +533,16 @@ impl NavigationMessageHandler {
513533
let tilt = ptz.tilt();
514534
let zoom = ptz.zoom();
515535

516-
let scaled_center = viewport_center / self.snapped_zoom(zoom);
536+
let scale = self.snapped_zoom(zoom);
537+
let scale_vec = if ptz.flip { DVec2::new(-scale, scale) } else { DVec2::splat(scale) };
538+
let scaled_center = viewport_center / scale_vec;
517539

518540
// Try to avoid fractional coordinates to reduce anti aliasing.
519-
let scale = self.snapped_zoom(zoom);
520541
let rounded_pan = ((pan + scaled_center) * scale).round() / scale - scaled_center;
521542

522543
// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
523544
let offset_transform = DAffine2::from_translation(scaled_center);
524-
let scale_transform = DAffine2::from_scale(DVec2::splat(scale));
545+
let scale_transform = DAffine2::from_scale(scale_vec);
525546
let angle_transform = DAffine2::from_angle(self.snapped_tilt(tilt));
526547
let translation_transform = DAffine2::from_translation(rounded_pan);
527548
scale_transform * offset_transform * angle_transform * translation_transform

editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1969,7 +1969,7 @@ impl NodeGraphMessageHandler {
19691969
.widget_holder(),
19701970
Separator::new(SeparatorType::Unrelated).widget_holder(),
19711971
];
1972-
widgets.extend(navigation_controls(node_graph_ptz, navigation_handler, "Node Graph"));
1972+
widgets.extend(navigation_controls(node_graph_ptz, navigation_handler, true));
19731973
widgets.extend([
19741974
Separator::new(SeparatorType::Unrelated).widget_holder(),
19751975
TextButton::new("Node Graph")

editor/src/messages/portfolio/document/utility_types/misc.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,11 +648,18 @@ pub struct PTZ {
648648
tilt: f64,
649649
/// Scale factor.
650650
zoom: f64,
651+
/// Flipped status.
652+
pub flip: bool,
651653
}
652654

653655
impl Default for PTZ {
654656
fn default() -> Self {
655-
Self { pan: DVec2::ZERO, tilt: 0., zoom: 1. }
657+
Self {
658+
pan: DVec2::ZERO,
659+
tilt: 0.,
660+
zoom: 1.,
661+
flip: false,
662+
}
656663
}
657664
}
658665

editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use graphene_std::vector::misc::BooleanOperation;
99
#[derive(Debug, Clone, Default)]
1010
pub struct MenuBarMessageHandler {
1111
pub has_active_document: bool,
12+
pub canvas_tilted: bool,
13+
pub canvas_flipped: bool,
1214
pub rulers_visible: bool,
1315
pub node_graph_open: bool,
1416
pub has_selected_nodes: bool,
@@ -503,7 +505,7 @@ impl LayoutHolder for MenuBarMessageHandler {
503505
icon: Some("TiltReset".into()),
504506
shortcut: action_keys!(NavigationMessageDiscriminant::CanvasTiltSet),
505507
action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasTiltSet { angle_radians: 0.into() }.into()),
506-
disabled: no_active_document || node_graph_open,
508+
disabled: no_active_document || node_graph_open || !self.canvas_tilted,
507509
..MenuBarEntry::default()
508510
},
509511
],
@@ -525,15 +527,15 @@ impl LayoutHolder for MenuBarMessageHandler {
525527
..MenuBarEntry::default()
526528
},
527529
MenuBarEntry {
528-
label: "Zoom to Fit Selection".into(),
530+
label: "Zoom to Selection".into(),
529531
icon: Some("FrameSelected".into()),
530532
shortcut: action_keys!(NavigationMessageDiscriminant::FitViewportToSelection),
531533
action: MenuBarEntry::create_action(|_| NavigationMessage::FitViewportToSelection.into()),
532534
disabled: no_active_document || !has_selected_layers,
533535
..MenuBarEntry::default()
534536
},
535537
MenuBarEntry {
536-
label: "Zoom to Fit All".into(),
538+
label: "Zoom to Fit".into(),
537539
icon: Some("FrameAll".into()),
538540
shortcut: action_keys!(DocumentMessageDiscriminant::ZoomCanvasToFitAll),
539541
action: MenuBarEntry::create_action(|_| DocumentMessage::ZoomCanvasToFitAll.into()),
@@ -557,6 +559,14 @@ impl LayoutHolder for MenuBarMessageHandler {
557559
..MenuBarEntry::default()
558560
},
559561
],
562+
vec![MenuBarEntry {
563+
label: "Flip".into(),
564+
icon: Some(if self.canvas_flipped { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
565+
shortcut: action_keys!(NavigationMessageDiscriminant::CanvasFlip),
566+
action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasFlip.into()),
567+
disabled: no_active_document || node_graph_open,
568+
..MenuBarEntry::default()
569+
}],
560570
vec![MenuBarEntry {
561571
label: "Rulers".into(),
562572
icon: Some(if self.rulers_visible { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),

editor/src/messages/portfolio/portfolio_message_handler.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
6969
// Sub-messages
7070
PortfolioMessage::MenuBar(message) => {
7171
self.menu_bar_message_handler.has_active_document = false;
72+
self.menu_bar_message_handler.canvas_tilted = false;
73+
self.menu_bar_message_handler.canvas_flipped = false;
7274
self.menu_bar_message_handler.rulers_visible = false;
7375
self.menu_bar_message_handler.node_graph_open = false;
7476
self.menu_bar_message_handler.has_selected_nodes = false;
@@ -80,6 +82,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
8082

8183
if let Some(document) = self.active_document_id.and_then(|document_id| self.documents.get_mut(&document_id)) {
8284
self.menu_bar_message_handler.has_active_document = true;
85+
self.menu_bar_message_handler.canvas_tilted = document.document_ptz.tilt() != 0.;
86+
self.menu_bar_message_handler.canvas_flipped = document.document_ptz.flip;
8387
self.menu_bar_message_handler.rulers_visible = document.rulers_visible;
8488
self.menu_bar_message_handler.node_graph_open = document.is_graph_overlay_open();
8589
let selected_nodes = document.network_interface.selected_nodes();

website/content/learn/interface/menu-bar.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,11 @@ The **View menu** lists actions related to the view of the canvas within the vie
9696
| Reset Tilt | <p>Sets the viewport tilt angle back to 0°.</p> |
9797
| Zoom In | <p>Narrows the view to the next whole zoom increment, such as:</p><p>25%, 33.33%, 40%, 50%, 66.67%, 80%, 100%, 125%, 160%, 200%, 250%, 320%, 400%, 500%</p> |
9898
| Zoom Out | <p>Widens the view to the next whole zoom increment, such as above.</p> |
99-
| Zoom to Fit Selection | <p>Zooms and frames the viewport to the bounding box of the selected layer(s).</p> |
100-
| Zoom to Fit All | <p>Zooms and frames the viewport to fit all artboards, or all artwork if using infinite canvas.</p> |
99+
| Zoom to Selection | <p>Zooms and frames the viewport to the bounding box of the selected layer(s).</p> |
100+
| Zoom to Fit | <p>Zooms and frames the viewport to fit all artboards, or all artwork if using infinite canvas.</p> |
101101
| Zoom to 100% | <p>Zooms the viewport in or out to 100% scale, making the document and viewport scales match 1:1.</p> |
102102
| Zoom to 200% | <p>Zooms the viewport in or out to 200% scale, displaying the artwork at twice the actual size.</p> |
103+
| Flip | <p>Mirrors the viewport horizontally, flipping the view of the artwork until deactivated.</p> |
103104
| Rulers | <p>Toggles visibility of the rulers along the top/left edges of the viewport.</p> |
104105

105106
## Help

0 commit comments

Comments
 (0)