mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 15:18:12 +08:00
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>
This commit is contained in:
co-authored by
hypercube
Keavon Chambers
parent
a8e209e44c
commit
2615d86934
@@ -359,13 +359,29 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
||||
continue;
|
||||
}
|
||||
let Some(bounds) = self.metadata().bounding_box_document(layer) else { continue };
|
||||
let min = bounds[0].min(bounds[1]);
|
||||
let max = bounds[0].max(bounds[1]);
|
||||
|
||||
let name = self.network_interface.display_name(&layer.to_node(), &[]);
|
||||
|
||||
// Calculate position of the text
|
||||
let corner_pos = if !self.document_ptz.flip {
|
||||
// Use the top-left corner
|
||||
min
|
||||
} else {
|
||||
// Use the top-right corner, which appears to be the top-left due to being flipped
|
||||
DVec2::new(max.x, min.y)
|
||||
};
|
||||
|
||||
// When the canvas is flipped, mirror the text so it appears correctly
|
||||
let scale = if !self.document_ptz.flip { DVec2::ONE } else { DVec2::new(-1., 1.) };
|
||||
|
||||
// Create a transform that puts the text at the true top-left regardless of flip
|
||||
let transform = self.metadata().document_to_viewport
|
||||
* DAffine2::from_translation(bounds[0].min(bounds[1]))
|
||||
* DAffine2::from_translation(corner_pos)
|
||||
* DAffine2::from_scale(DVec2::splat(self.document_ptz.zoom().recip()))
|
||||
* DAffine2::from_translation(-DVec2::Y * 4.);
|
||||
* DAffine2::from_translation(-DVec2::Y * 4.)
|
||||
* DAffine2::from_scale(scale);
|
||||
|
||||
overlay_context.text(&name, COLOR_OVERLAY_GRAY, None, transform, 0., [Pivot::Start, Pivot::End]);
|
||||
}
|
||||
@@ -1477,6 +1493,9 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
||||
self.network_interface.document_bounds_document_space(true)
|
||||
};
|
||||
if let Some(bounds) = bounds {
|
||||
if self.document_ptz.flip {
|
||||
responses.add(NavigationMessage::CanvasFlip);
|
||||
}
|
||||
responses.add(NavigationMessage::CanvasTiltSet { angle_radians: 0. });
|
||||
responses.add(NavigationMessage::FitViewportToBounds { bounds, prevent_zoom_past_100: true });
|
||||
} else {
|
||||
@@ -2365,7 +2384,7 @@ impl DocumentMessageHandler {
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
];
|
||||
|
||||
widgets.extend(navigation_controls(&self.document_ptz, &self.navigation_handler, "Canvas"));
|
||||
widgets.extend(navigation_controls(&self.document_ptz, &self.navigation_handler, false));
|
||||
|
||||
let tilt_value = self.navigation_handler.snapped_tilt(self.document_ptz.tilt()) / (std::f64::consts::PI / 180.);
|
||||
if tilt_value.abs() > 0.00001 {
|
||||
@@ -2817,8 +2836,8 @@ impl<'a> ClickXRayIter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHandler, tooltip_name: &str) -> [WidgetHolder; 5] {
|
||||
[
|
||||
pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHandler, node_graph: bool) -> Vec<WidgetHolder> {
|
||||
let mut list = vec![
|
||||
IconButton::new("ZoomIn", 24)
|
||||
.tooltip("Zoom In")
|
||||
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasZoomIncrease))
|
||||
@@ -2835,40 +2854,23 @@ pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHand
|
||||
.on_update(|_| NavigationMessage::CanvasTiltResetAndZoomTo100Percent.into())
|
||||
.disabled(ptz.tilt().abs() < 1e-4 && (ptz.zoom() - 1.).abs() < 1e-4)
|
||||
.widget_holder(),
|
||||
// PopoverButton::new()
|
||||
// .popover_layout(vec![
|
||||
// LayoutGroup::Row {
|
||||
// widgets: vec![TextLabel::new(format!("{tooltip_name} Navigation")).bold(true).widget_holder()],
|
||||
// },
|
||||
// LayoutGroup::Row {
|
||||
// widgets: vec![TextLabel::new({
|
||||
// let tilt = if tooltip_name == "Canvas" { "Tilt:\n• Alt + Middle Click Drag\n\n" } else { "" };
|
||||
// format!(
|
||||
// "
|
||||
// Interactive controls in this\n\
|
||||
// menu are coming soon.\n\
|
||||
// \n\
|
||||
// Pan:\n\
|
||||
// • Middle Click Drag\n\
|
||||
// \n\
|
||||
// {tilt}Zoom:\n\
|
||||
// • Shift + Middle Click Drag\n\
|
||||
// • Ctrl + Scroll Wheel Roll
|
||||
// "
|
||||
// )
|
||||
// .trim()
|
||||
// })
|
||||
// .multiline(true)
|
||||
// .widget_holder()],
|
||||
// },
|
||||
// ])
|
||||
// .widget_holder(),
|
||||
];
|
||||
if ptz.flip && !node_graph {
|
||||
list.push(
|
||||
IconButton::new("Reverse", 24)
|
||||
.tooltip("Flip the canvas back to its standard orientation")
|
||||
.tooltip_shortcut(action_keys!(NavigationMessageDiscriminant::CanvasFlip))
|
||||
.on_update(|_| NavigationMessage::CanvasFlip.into())
|
||||
.widget_holder(),
|
||||
);
|
||||
}
|
||||
list.extend([
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
NumberInput::new(Some(navigation_handler.snapped_zoom(ptz.zoom()) * 100.))
|
||||
.unit("%")
|
||||
.min(0.000001)
|
||||
.max(1000000.)
|
||||
.tooltip(format!("{tooltip_name} Zoom"))
|
||||
.tooltip(if node_graph { "Node Graph Zoom" } else { "Canvas Zoom" })
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
NavigationMessage::CanvasZoomSet {
|
||||
zoom_factor: number_input.value.unwrap() / 100.,
|
||||
@@ -2879,7 +2881,8 @@ pub fn navigation_controls(ptz: &PTZ, navigation_handler: &NavigationMessageHand
|
||||
.increment_callback_decrease(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into())
|
||||
.increment_callback_increase(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into())
|
||||
.widget_holder(),
|
||||
]
|
||||
]);
|
||||
list
|
||||
}
|
||||
|
||||
impl Iterator for ClickXRayIter<'_> {
|
||||
|
||||
@@ -20,6 +20,7 @@ pub enum NavigationMessage {
|
||||
CanvasZoomIncrease { center_on_mouse: bool },
|
||||
CanvasZoomMouseWheel,
|
||||
CanvasZoomSet { zoom_factor: f64 },
|
||||
CanvasFlip,
|
||||
EndCanvasPTZ { abort_transform: bool },
|
||||
EndCanvasPTZWithClick { commit_key: Key },
|
||||
FitViewportToBounds { bounds: [DVec2; 2], prevent_zoom_past_100: bool },
|
||||
|
||||
@@ -204,6 +204,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
if !graph_view_overlay_open {
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
}
|
||||
}
|
||||
NavigationMessage::CanvasZoomDecrease { center_on_mouse } => {
|
||||
@@ -273,6 +274,22 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
responses.add(NodeGraphMessage::SetGridAlignedEdges);
|
||||
}
|
||||
NavigationMessage::CanvasFlip => {
|
||||
if graph_view_overlay_open {
|
||||
return;
|
||||
}
|
||||
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
log::error!("Could not get mutable PTZ in CanvasFlip");
|
||||
return;
|
||||
};
|
||||
|
||||
ptz.flip = !ptz.flip;
|
||||
|
||||
responses.add(DocumentMessage::PTZUpdate);
|
||||
responses.add(BroadcastEvent::CanvasTransformed);
|
||||
responses.add(MenuBarMessage::SendLayout);
|
||||
responses.add(PortfolioMessage::UpdateDocumentWidgets);
|
||||
}
|
||||
NavigationMessage::EndCanvasPTZ { abort_transform } => {
|
||||
let Some(ptz) = get_ptz_mut(document_ptz, network_interface, graph_view_overlay_open, breadcrumb_network_path) else {
|
||||
log::error!("Could not get mutable PTZ in EndCanvasPTZ");
|
||||
@@ -393,9 +410,11 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
||||
..
|
||||
} => {
|
||||
let tilt_raw_not_snapped = {
|
||||
// Compute the angle in document space to counter for the canvas being flipped
|
||||
let viewport_to_document = network_interface.document_metadata().document_to_viewport.inverse();
|
||||
let half_viewport = ipp.viewport_bounds.size() / 2.;
|
||||
let start_offset = self.mouse_position - half_viewport;
|
||||
let end_offset = ipp.mouse.position - half_viewport;
|
||||
let start_offset = viewport_to_document.transform_vector2(self.mouse_position - half_viewport);
|
||||
let end_offset = viewport_to_document.transform_vector2(ipp.mouse.position - half_viewport);
|
||||
let angle = start_offset.angle_to(end_offset);
|
||||
|
||||
tilt_raw_not_snapped + angle
|
||||
@@ -471,6 +490,7 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> for Navigation
|
||||
CanvasZoomDecrease,
|
||||
CanvasZoomIncrease,
|
||||
CanvasZoomMouseWheel,
|
||||
CanvasFlip,
|
||||
FitViewportToSelection,
|
||||
);
|
||||
|
||||
@@ -513,15 +533,16 @@ impl NavigationMessageHandler {
|
||||
let tilt = ptz.tilt();
|
||||
let zoom = ptz.zoom();
|
||||
|
||||
let scaled_center = viewport_center / self.snapped_zoom(zoom);
|
||||
let scale = self.snapped_zoom(zoom);
|
||||
let scale_vec = if ptz.flip { DVec2::new(-scale, scale) } else { DVec2::splat(scale) };
|
||||
let scaled_center = viewport_center / scale_vec;
|
||||
|
||||
// Try to avoid fractional coordinates to reduce anti aliasing.
|
||||
let scale = self.snapped_zoom(zoom);
|
||||
let rounded_pan = ((pan + scaled_center) * scale).round() / scale - scaled_center;
|
||||
|
||||
// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
|
||||
let offset_transform = DAffine2::from_translation(scaled_center);
|
||||
let scale_transform = DAffine2::from_scale(DVec2::splat(scale));
|
||||
let scale_transform = DAffine2::from_scale(scale_vec);
|
||||
let angle_transform = DAffine2::from_angle(self.snapped_tilt(tilt));
|
||||
let translation_transform = DAffine2::from_translation(rounded_pan);
|
||||
scale_transform * offset_transform * angle_transform * translation_transform
|
||||
|
||||
@@ -1969,7 +1969,7 @@ impl NodeGraphMessageHandler {
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
];
|
||||
widgets.extend(navigation_controls(node_graph_ptz, navigation_handler, "Node Graph"));
|
||||
widgets.extend(navigation_controls(node_graph_ptz, navigation_handler, true));
|
||||
widgets.extend([
|
||||
Separator::new(SeparatorType::Unrelated).widget_holder(),
|
||||
TextButton::new("Node Graph")
|
||||
|
||||
@@ -648,11 +648,18 @@ pub struct PTZ {
|
||||
tilt: f64,
|
||||
/// Scale factor.
|
||||
zoom: f64,
|
||||
/// Flipped status.
|
||||
pub flip: bool,
|
||||
}
|
||||
|
||||
impl Default for PTZ {
|
||||
fn default() -> Self {
|
||||
Self { pan: DVec2::ZERO, tilt: 0., zoom: 1. }
|
||||
Self {
|
||||
pan: DVec2::ZERO,
|
||||
tilt: 0.,
|
||||
zoom: 1.,
|
||||
flip: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user