| use crate::consts::{COMPASS_ROSE_ARROW_CLICK_TARGET_ANGLE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER}; |
| use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; |
| use crate::messages::prelude::DocumentMessageHandler; |
| use glam::{DAffine2, DVec2}; |
| use std::f64::consts::FRAC_PI_2; |
|
|
| #[derive(Clone, Default, Debug)] |
| pub struct CompassRose { |
| compass_center: DVec2, |
| } |
|
|
| impl CompassRose { |
| fn get_layer_pivot_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 { |
| let [min, max] = document.metadata().nonzero_bounding_box(layer); |
|
|
| let bounds_transform = DAffine2::from_translation(min) * DAffine2::from_scale(max - min); |
| let layer_transform = document.metadata().transform_to_viewport(layer); |
| layer_transform * bounds_transform |
| } |
| pub fn refresh_position(&mut self, document: &DocumentMessageHandler) { |
| let selected_nodes = document.network_interface.selected_nodes(); |
| let mut layers = selected_nodes.selected_visible_and_unlocked_layers(&document.network_interface); |
|
|
| let Some(first) = layers.next() else { return }; |
| let count = layers.count() + 1; |
| let transform = if count == 1 { |
| Self::get_layer_pivot_transform(first, document) |
| } else { |
| let [min, max] = document.selected_visible_and_unlock_layers_bounding_box_viewport().unwrap_or([DVec2::ZERO, DVec2::ONE]); |
| DAffine2::from_translation(min) * DAffine2::from_scale(max - min) |
| }; |
|
|
| self.compass_center = transform.transform_point2(DVec2::splat(0.5)); |
| } |
|
|
| pub fn compass_rose_position(&self) -> DVec2 { |
| self.compass_center |
| } |
|
|
| pub fn compass_rose_state(&self, mouse: DVec2, angle: f64) -> CompassRoseState { |
| const COMPASS_ROSE_RING_INNER_RADIUS_SQUARED: f64 = (COMPASS_ROSE_RING_INNER_DIAMETER / 2.) * (COMPASS_ROSE_RING_INNER_DIAMETER / 2.); |
| const COMPASS_ROSE_HOVER_RING_RADIUS_SQUARED: f64 = (COMPASS_ROSE_HOVER_RING_DIAMETER / 2.) * (COMPASS_ROSE_HOVER_RING_DIAMETER / 2.); |
|
|
| let compass_distance_squared = mouse.distance_squared(self.compass_center); |
|
|
| if !(COMPASS_ROSE_RING_INNER_RADIUS_SQUARED..COMPASS_ROSE_HOVER_RING_RADIUS_SQUARED).contains(&compass_distance_squared) { |
| return CompassRoseState::None; |
| } |
|
|
| let angle = (mouse - self.compass_center).angle_to(DVec2::from_angle(angle)).abs(); |
| let resolved_angle = (FRAC_PI_2 - angle).abs(); |
| let angular_width = COMPASS_ROSE_ARROW_CLICK_TARGET_ANGLE.to_radians(); |
|
|
| if resolved_angle < angular_width { |
| CompassRoseState::AxisY |
| } else if resolved_angle > (FRAC_PI_2 - angular_width) { |
| CompassRoseState::AxisX |
| } else { |
| CompassRoseState::Ring |
| } |
| } |
| } |
|
|
| #[derive(Clone, Debug, PartialEq)] |
| pub enum CompassRoseState { |
| Ring, |
| AxisX, |
| AxisY, |
| None, |
| } |
|
|
| impl CompassRoseState { |
| pub fn can_grab(&self) -> bool { |
| matches!(self, Self::Ring | Self::AxisX | Self::AxisY) |
| } |
|
|
| pub fn is_ring(&self) -> bool { |
| matches!(self, Self::Ring) |
| } |
|
|
| pub fn axis_type(&self) -> Option<Axis> { |
| match self { |
| CompassRoseState::AxisX => Some(Axis::X), |
| CompassRoseState::AxisY => Some(Axis::Y), |
| CompassRoseState::Ring => Some(Axis::None), |
| _ => None, |
| } |
| } |
| } |
|
|
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] |
| pub enum Axis { |
| #[default] |
| None, |
| X, |
| Y, |
| } |
|
|
| impl Axis { |
| pub fn is_constraint(&self) -> bool { |
| matches!(self, Self::X | Self::Y) |
| } |
| } |
|
|
| impl From<Axis> for DVec2 { |
| fn from(value: Axis) -> Self { |
| match value { |
| Axis::X => DVec2::X, |
| Axis::Y => DVec2::Y, |
| Axis::None => DVec2::ZERO, |
| } |
| } |
| } |
|
|