mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 11:08:12 +08:00
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
This commit is contained in:
@@ -650,62 +650,77 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> 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![
|
||||
|
||||
@@ -107,8 +107,8 @@ impl MessageHandler<NavigationMessage, NavigationMessageData<'_>> 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<NavigationMessage, NavigationMessageData<'_>> 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<NavigationMessage, NavigationMessageData<'_>> 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<NavigationMessage, NavigationMessageData<'_>> 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<NavigationMessage, NavigationMessageData<'_>> 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<NavigationMessage, NavigationMessageData<'_>> 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<NavigationMessage, NavigationMessageData<'_>> 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);
|
||||
|
||||
@@ -116,6 +116,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> 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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user