mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
* Fix Message Tree: Enforce Structure and Visibility * Code review * fix the erroreous ouputs * error handling for MessageHandler * Fix website visualization HTML generation * error handling for tuple-style message enum variant * cleanup * Update messages * Normalize BroadcastEvent --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
297 lines
10 KiB
Rust
297 lines
10 KiB
Rust
//! Handler for the pivot overlay visible on the selected layer(s) whilst using the Select tool which controls the center of rotation/scale.
|
|
|
|
use crate::consts::PIVOT_DIAMETER;
|
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
|
use crate::messages::prelude::*;
|
|
use crate::messages::tool::common_functionality::graph_modification_utils;
|
|
use crate::messages::tool::tool_messages::path_tool::PathOptionsUpdate;
|
|
use crate::messages::tool::tool_messages::select_tool::SelectOptionsUpdate;
|
|
use crate::messages::tool::tool_messages::tool_prelude::*;
|
|
use glam::{DAffine2, DVec2};
|
|
use graphene_std::transform::ReferencePoint;
|
|
use graphene_std::vector::misc::ManipulatorPointId;
|
|
use std::fmt;
|
|
|
|
pub fn pin_pivot_widget(active: bool, enabled: bool, source: PivotToolSource) -> WidgetHolder {
|
|
IconButton::new(if active { "PinActive" } else { "PinInactive" }, 24)
|
|
.tooltip(String::from(if active { "Unpin Custom Pivot" } else { "Pin Custom Pivot" }) + "\n\nUnless pinned, the pivot will return to its prior reference point when a new selection is made.")
|
|
.disabled(!enabled)
|
|
.on_update(move |_| match source {
|
|
PivotToolSource::Select => SelectToolMessage::SelectOptions {
|
|
options: SelectOptionsUpdate::TogglePivotPinned,
|
|
}
|
|
.into(),
|
|
PivotToolSource::Path => PathToolMessage::UpdateOptions {
|
|
options: PathOptionsUpdate::TogglePivotPinned,
|
|
}
|
|
.into(),
|
|
})
|
|
.widget_holder()
|
|
}
|
|
|
|
pub fn pivot_reference_point_widget(disabled: bool, reference_point: ReferencePoint, source: PivotToolSource) -> WidgetHolder {
|
|
ReferencePointInput::new(reference_point)
|
|
.tooltip("Custom Pivot Reference Point\n\nPlaces the pivot at a corner, edge, or center of the selection bounds, unless it is dragged elsewhere.")
|
|
.disabled(disabled)
|
|
.on_update(move |pivot_input: &ReferencePointInput| match source {
|
|
PivotToolSource::Select => SelectToolMessage::SetPivot { position: pivot_input.value }.into(),
|
|
PivotToolSource::Path => PathToolMessage::SetPivot { position: pivot_input.value }.into(),
|
|
})
|
|
.widget_holder()
|
|
}
|
|
|
|
pub fn pivot_gizmo_type_widget(state: PivotGizmoState, source: PivotToolSource) -> Vec<WidgetHolder> {
|
|
let gizmo_type_entries = [PivotGizmoType::Pivot, PivotGizmoType::Average, PivotGizmoType::Active]
|
|
.iter()
|
|
.map(|gizmo_type| {
|
|
MenuListEntry::new(format!("{gizmo_type:?}")).label(gizmo_type.to_string()).on_commit({
|
|
let value = source.clone();
|
|
move |_| match value {
|
|
PivotToolSource::Select => SelectToolMessage::SelectOptions {
|
|
options: SelectOptionsUpdate::PivotGizmoType(*gizmo_type),
|
|
}
|
|
.into(),
|
|
PivotToolSource::Path => PathToolMessage::UpdateOptions {
|
|
options: PathOptionsUpdate::PivotGizmoType(*gizmo_type),
|
|
}
|
|
.into(),
|
|
}
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
vec![
|
|
CheckboxInput::new(!state.disabled)
|
|
.tooltip(
|
|
"Pivot Gizmo\n\
|
|
\n\
|
|
Enabled: the chosen gizmo type is shown and used to control rotation and scaling.\n\
|
|
Disabled: rotation and scaling occurs about the center of the selection bounds.",
|
|
)
|
|
.on_update(move |optional_input: &CheckboxInput| match source {
|
|
PivotToolSource::Select => SelectToolMessage::SelectOptions {
|
|
options: SelectOptionsUpdate::TogglePivotGizmoType(optional_input.checked),
|
|
}
|
|
.into(),
|
|
PivotToolSource::Path => PathToolMessage::UpdateOptions {
|
|
options: PathOptionsUpdate::TogglePivotGizmoType(optional_input.checked),
|
|
}
|
|
.into(),
|
|
})
|
|
.widget_holder(),
|
|
Separator::new(SeparatorType::Related).widget_holder(),
|
|
DropdownInput::new(vec![gizmo_type_entries])
|
|
.selected_index(Some(match state.gizmo_type {
|
|
PivotGizmoType::Pivot => 0,
|
|
PivotGizmoType::Average => 1,
|
|
PivotGizmoType::Active => 2,
|
|
}))
|
|
.tooltip(
|
|
"Pivot Gizmo Type\n\
|
|
\n\
|
|
Selects which gizmo type is shown and used as the center of rotation/scaling transformations.\n\
|
|
\n\
|
|
Custom Pivot: rotates and scales relative to the selection bounds, or elsewhere if dragged.\n\
|
|
Origin (Average Point): rotates and scales about the average point of all selected layer origins.\n\
|
|
Origin (Active Object): rotates and scales about the origin of the most recently selected layer.",
|
|
)
|
|
.disabled(state.disabled)
|
|
.widget_holder(),
|
|
]
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
|
pub enum PivotToolSource {
|
|
Path,
|
|
#[default]
|
|
Select,
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
|
pub struct PivotGizmo {
|
|
pub pivot: Pivot,
|
|
pub state: PivotGizmoState,
|
|
pub layer: Option<LayerNodeIdentifier>,
|
|
pub point: Option<ManipulatorPointId>,
|
|
}
|
|
|
|
impl PivotGizmo {
|
|
pub fn position(&self, document: &DocumentMessageHandler) -> DVec2 {
|
|
let network = &document.network_interface;
|
|
(!self.state.disabled)
|
|
.then_some({
|
|
match self.state.gizmo_type {
|
|
PivotGizmoType::Average => Some(network.selected_nodes().selected_visible_and_unlocked_layers_mean_average_origin(network)),
|
|
PivotGizmoType::Pivot => self.pivot.pivot,
|
|
PivotGizmoType::Active => self.layer.map(|layer| graph_modification_utils::get_viewport_origin(layer, network)),
|
|
}
|
|
})
|
|
.flatten()
|
|
.unwrap_or_else(|| self.pivot.transform_from_normalized.transform_point2(DVec2::splat(0.5)))
|
|
}
|
|
|
|
pub fn recalculate_transform(&mut self, document: &DocumentMessageHandler) -> DAffine2 {
|
|
self.pivot.recalculate_pivot(document);
|
|
self.pivot.transform_from_normalized
|
|
}
|
|
|
|
pub fn pin_active(&self) -> bool {
|
|
self.pivot.pinned && self.state.is_pivot_type()
|
|
}
|
|
|
|
pub fn pivot_disconnected(&self) -> bool {
|
|
self.pivot.old_pivot_position == ReferencePoint::None
|
|
}
|
|
}
|
|
|
|
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
|
pub enum PivotGizmoType {
|
|
// Pivot
|
|
#[default]
|
|
Pivot,
|
|
// Origin
|
|
Average,
|
|
Active,
|
|
// TODO: Add "Individual"
|
|
}
|
|
|
|
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
|
pub struct PivotGizmoState {
|
|
pub disabled: bool,
|
|
pub gizmo_type: PivotGizmoType,
|
|
}
|
|
|
|
impl PivotGizmoState {
|
|
pub fn is_pivot_type(&self) -> bool {
|
|
self.gizmo_type == PivotGizmoType::Pivot || self.disabled
|
|
}
|
|
|
|
pub fn is_pivot(&self) -> bool {
|
|
self.gizmo_type == PivotGizmoType::Pivot && !self.disabled
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for PivotGizmoType {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
PivotGizmoType::Pivot => write!(f, "Custom Pivot"),
|
|
PivotGizmoType::Average => write!(f, "Origin (Average Point)"),
|
|
PivotGizmoType::Active => write!(f, "Origin (Active Object)"),
|
|
// TODO: Add "Origin (Individual)"
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
|
pub struct Pivot {
|
|
/// Pivot between (0,0) and (1,1)
|
|
normalized_pivot: DVec2,
|
|
/// Transform to get from normalized pivot to viewspace
|
|
pub transform_from_normalized: DAffine2,
|
|
/// The viewspace pivot position
|
|
pub pivot: Option<DVec2>,
|
|
/// The old pivot position in the GUI, used to reduce refreshes of the document bar
|
|
pub old_pivot_position: ReferencePoint,
|
|
/// The last ReferencePoint which wasn't none
|
|
pub last_non_none_reference_point: ReferencePoint,
|
|
/// Used to enable and disable the pivot
|
|
pub pinned: bool,
|
|
/// Had selected_visible_and_unlocked_layers
|
|
pub empty: bool,
|
|
}
|
|
|
|
impl Default for Pivot {
|
|
fn default() -> Self {
|
|
Self {
|
|
normalized_pivot: DVec2::splat(0.5),
|
|
transform_from_normalized: Default::default(),
|
|
pivot: Default::default(),
|
|
old_pivot_position: ReferencePoint::Center,
|
|
last_non_none_reference_point: ReferencePoint::Center,
|
|
pinned: false,
|
|
empty: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Pivot {
|
|
/// Recomputes the pivot position and transform.
|
|
pub fn recalculate_pivot(&mut self, document: &DocumentMessageHandler) {
|
|
let selected = document.network_interface.selected_nodes();
|
|
self.empty = !selected.has_selected_nodes();
|
|
if !selected.has_selected_nodes() {
|
|
return;
|
|
}
|
|
|
|
let transform = selected
|
|
.selected_visible_and_unlocked_layers(&document.network_interface)
|
|
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
|
|
.map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface))
|
|
.unwrap_or_default();
|
|
|
|
let bounds = document
|
|
.network_interface
|
|
.selected_nodes()
|
|
.selected_visible_and_unlocked_layers(&document.network_interface)
|
|
.filter_map(|layer| {
|
|
document
|
|
.metadata()
|
|
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
|
|
})
|
|
.reduce(graphene_std::renderer::Quad::combine_bounds);
|
|
|
|
let [min, max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]);
|
|
self.transform_from_normalized = transform * DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
|
|
|
|
if self.old_pivot_position != ReferencePoint::None {
|
|
self.pivot = Some(self.transform_from_normalized.transform_point2(self.normalized_pivot));
|
|
}
|
|
}
|
|
|
|
pub fn recalculate_pivot_for_layer(&mut self, document: &DocumentMessageHandler, bounds: Option<[DVec2; 2]>) {
|
|
let selected = document.network_interface.selected_nodes();
|
|
if !selected.has_selected_nodes() {
|
|
self.normalized_pivot = DVec2::splat(0.5);
|
|
self.pivot = None;
|
|
return;
|
|
};
|
|
|
|
let [min, max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]);
|
|
self.transform_from_normalized = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
|
|
self.pivot = Some(self.transform_from_normalized.transform_point2(self.normalized_pivot));
|
|
}
|
|
|
|
/// Answers if the pivot widget has changed (so we should refresh the tool bar at the top of the canvas).
|
|
pub fn should_refresh_pivot_position(&mut self) -> bool {
|
|
let new = self.to_pivot_position();
|
|
let should_refresh = new != self.old_pivot_position;
|
|
self.old_pivot_position = new;
|
|
should_refresh
|
|
}
|
|
|
|
pub fn to_pivot_position(&self) -> ReferencePoint {
|
|
self.normalized_pivot.into()
|
|
}
|
|
|
|
/// Sets the viewport position of the pivot.
|
|
pub fn set_viewport_position(&mut self, position: DVec2) {
|
|
if self.transform_from_normalized.matrix2.determinant().abs() <= f64::EPSILON {
|
|
return;
|
|
};
|
|
|
|
self.normalized_pivot = self.transform_from_normalized.inverse().transform_point2(position);
|
|
self.pivot = Some(position);
|
|
}
|
|
|
|
/// Set the pivot using a normalized position.
|
|
pub fn set_normalized_position(&mut self, position: DVec2) {
|
|
self.normalized_pivot = position;
|
|
self.pivot = Some(self.transform_from_normalized.transform_point2(position));
|
|
}
|
|
|
|
/// Answers if the pointer is currently positioned over the pivot.
|
|
pub fn is_over(&self, mouse: DVec2) -> bool {
|
|
self.pivot.filter(|&pivot| mouse.distance_squared(pivot) < (PIVOT_DIAMETER / 2.).powi(2)).is_some()
|
|
}
|
|
}
|