mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Merge e4c9c0d713
into 24c6281644
This commit is contained in:
commit
91e68dd564
24 changed files with 1790 additions and 1129 deletions
1751
Cargo.lock
generated
1751
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -61,6 +61,7 @@ pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
|||
pub const PIVOT_CROSSHAIR_THICKNESS: f64 = 1.;
|
||||
pub const PIVOT_CROSSHAIR_LENGTH: f64 = 9.;
|
||||
pub const PIVOT_DIAMETER: f64 = 5.;
|
||||
pub const DOWEL_PIN_RADIUS: f64 = 4.;
|
||||
|
||||
// COMPASS ROSE
|
||||
pub const COMPASS_ROSE_RING_INNER_DIAMETER: f64 = 13.;
|
||||
|
@ -133,10 +134,10 @@ pub const SCALE_EFFECT: f64 = 0.5;
|
|||
|
||||
// COLORS
|
||||
pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff";
|
||||
pub const COLOR_OVERLAY_BLUE_50: &str = "rgba(0, 168, 255, 0.5)";
|
||||
pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848";
|
||||
pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
|
||||
pub const COLOR_OVERLAY_RED: &str = "#ef5454";
|
||||
pub const COLOR_OVERLAY_ORANGE: &str = "#e27a44";
|
||||
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";
|
||||
pub const COLOR_OVERLAY_WHITE: &str = "#ffffff";
|
||||
pub const COLOR_OVERLAY_LABEL_BACKGROUND: &str = "#000000cc";
|
||||
|
|
|
@ -750,6 +750,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
// 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)));
|
||||
responses.add(SelectToolMessage::ShiftSelectedNodes { offset: transform.translation });
|
||||
|
||||
for layer in self.network_interface.shallowest_unique_layers(&[]).filter(|layer| can_move(*layer)) {
|
||||
responses.add(GraphOperationMessage::TransformChange {
|
||||
|
@ -1185,6 +1186,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
OverlaysType::HoverOutline => visibility_settings.hover_outline = visible,
|
||||
OverlaysType::SelectionOutline => visibility_settings.selection_outline = visible,
|
||||
OverlaysType::Pivot => visibility_settings.pivot = visible,
|
||||
OverlaysType::Origin => visibility_settings.origin = visible,
|
||||
OverlaysType::Path => visibility_settings.path = visible,
|
||||
OverlaysType::Anchors => {
|
||||
visibility_settings.anchors = visible;
|
||||
|
@ -1714,6 +1716,14 @@ impl DocumentMessageHandler {
|
|||
.reduce(graphene_std::renderer::Quad::combine_bounds)
|
||||
}
|
||||
|
||||
pub fn selected_visible_and_unlock_layers_bounding_box_document(&self) -> Option<[DVec2; 2]> {
|
||||
self.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&self.network_interface)
|
||||
.map(|layer| self.metadata().nonzero_bounding_box(layer))
|
||||
.reduce(graphene_std::renderer::Quad::combine_bounds)
|
||||
}
|
||||
|
||||
pub fn document_network(&self) -> &NodeNetwork {
|
||||
self.network_interface.document_network()
|
||||
}
|
||||
|
@ -2267,6 +2277,24 @@ impl DocumentMessageHandler {
|
|||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
vec![
|
||||
CheckboxInput::new(self.overlays_visibility_settings.pivot)
|
||||
.on_update(|optional_input: &CheckboxInput| {
|
||||
DocumentMessage::SetOverlaysVisibility {
|
||||
visible: optional_input.checked,
|
||||
overlays_type: Some(OverlaysType::Origin),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.for_label(checkbox_id.clone())
|
||||
.widget_holder(),
|
||||
TextLabel::new("Transform Origin".to_string()).for_checkbox(&mut checkbox_id).widget_holder(),
|
||||
]
|
||||
},
|
||||
},
|
||||
LayoutGroup::Row {
|
||||
widgets: {
|
||||
let mut checkbox_id = CheckboxId::default();
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::portfolio::document::utility_types::network_interface::NodeTemplate;
|
||||
use crate::messages::prelude::*;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use glam::{DAffine2, IVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Artboard;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
|
@ -52,10 +52,6 @@ pub enum GraphOperationMessage {
|
|||
transform_in: TransformIn,
|
||||
skip_rerender: bool,
|
||||
},
|
||||
TransformSetPivot {
|
||||
layer: LayerNodeIdentifier,
|
||||
pivot: DVec2,
|
||||
},
|
||||
Vector {
|
||||
layer: LayerNodeIdentifier,
|
||||
modification_type: VectorModificationType,
|
||||
|
|
|
@ -89,15 +89,6 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
|
|||
modify_inputs.transform_set(transform, transform_in, skip_rerender);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::TransformSetPivot { layer, pivot } => {
|
||||
if layer == LayerNodeIdentifier::ROOT_PARENT {
|
||||
log::error!("Cannot run TransformSetPivot on ROOT_PARENT");
|
||||
return;
|
||||
}
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.pivot_set(pivot);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::Vector { layer, modification_type } => {
|
||||
if layer == LayerNodeIdentifier::ROOT_PARENT {
|
||||
log::error!("Cannot run Vector on ROOT_PARENT");
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::portfolio::document::utility_types::network_interface::{self, InputConnector, NodeNetworkInterface, OutputConnector};
|
||||
use crate::messages::prelude::*;
|
||||
use bezier_rs::Subpath;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use glam::{DAffine2, IVec2};
|
||||
use graph_craft::concrete;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
|
@ -458,12 +458,6 @@ impl<'a> ModifyInputsContext<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn pivot_set(&mut self, new_pivot: DVec2) {
|
||||
let Some(transform_node_id) = self.existing_node_id("Transform", true) else { return };
|
||||
|
||||
self.set_input_with_refresh(InputConnector::node(transform_node_id, 5), NodeInput::value(TaggedValue::DVec2(new_pivot), false), false);
|
||||
}
|
||||
|
||||
pub fn vector_modify(&mut self, modification_type: VectorModificationType) {
|
||||
let Some(path_node_id) = self.existing_node_id("Path", true) else { return };
|
||||
self.network_interface.vector_modify(&path_node_id, modification_type);
|
||||
|
|
|
@ -1633,7 +1633,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
NodeInput::value(TaggedValue::F64(0.), false),
|
||||
NodeInput::value(TaggedValue::DVec2(DVec2::ONE), false),
|
||||
NodeInput::value(TaggedValue::DVec2(DVec2::ZERO), false),
|
||||
NodeInput::value(TaggedValue::DVec2(DVec2::splat(0.5)), false),
|
||||
],
|
||||
implementation: DocumentNodeImplementation::Network(NodeNetwork {
|
||||
exports: vec![NodeInput::node(NodeId(1), 0)],
|
||||
|
@ -1652,7 +1651,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
NodeInput::network(concrete!(f64), 2),
|
||||
NodeInput::network(concrete!(DVec2), 3),
|
||||
NodeInput::network(concrete!(DVec2), 4),
|
||||
NodeInput::network(concrete!(DVec2), 5),
|
||||
],
|
||||
manual_composition: Some(concrete!(Context)),
|
||||
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::transform_nodes::TransformNode")),
|
||||
|
@ -1720,7 +1718,6 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
|
|||
}),
|
||||
),
|
||||
InputMetadata::with_name_description_override("Skew", "TODO", WidgetOverride::Custom("transform_skew".to_string())),
|
||||
InputMetadata::with_name_description_override("Pivot", "TODO", WidgetOverride::Hidden),
|
||||
],
|
||||
output_names: vec!["Data".to_string()],
|
||||
..Default::default()
|
||||
|
|
|
@ -198,7 +198,7 @@ pub struct FrontendClickTargets {
|
|||
pub modify_import_export: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
use super::utility_functions::overlay_canvas_context;
|
||||
use crate::consts::{
|
||||
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER,
|
||||
COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_ORANGE, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER,
|
||||
COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
};
|
||||
use crate::messages::prelude::Message;
|
||||
use bezier_rs::{Bezier, Subpath};
|
||||
use core::borrow::Borrow;
|
||||
use core::f64::consts::{FRAC_PI_2, TAU};
|
||||
use core::f64::consts::{FRAC_PI_2, PI, TAU};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::math::quad::Quad;
|
||||
|
@ -33,6 +33,7 @@ pub enum OverlaysType {
|
|||
HoverOutline,
|
||||
SelectionOutline,
|
||||
Pivot,
|
||||
Origin,
|
||||
Path,
|
||||
Anchors,
|
||||
Handles,
|
||||
|
@ -49,6 +50,8 @@ pub struct OverlaysVisibilitySettings {
|
|||
pub hover_outline: bool,
|
||||
pub selection_outline: bool,
|
||||
pub pivot: bool,
|
||||
#[serde(default)]
|
||||
pub origin: bool,
|
||||
pub path: bool,
|
||||
pub anchors: bool,
|
||||
pub handles: bool,
|
||||
|
@ -66,6 +69,7 @@ impl Default for OverlaysVisibilitySettings {
|
|||
hover_outline: true,
|
||||
selection_outline: true,
|
||||
pivot: true,
|
||||
origin: true,
|
||||
path: true,
|
||||
anchors: true,
|
||||
handles: true,
|
||||
|
@ -110,6 +114,10 @@ impl OverlaysVisibilitySettings {
|
|||
self.all && self.pivot
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> bool {
|
||||
self.all && self.origin
|
||||
}
|
||||
|
||||
pub fn path(&self) -> bool {
|
||||
self.all && self.path
|
||||
}
|
||||
|
@ -423,10 +431,7 @@ impl OverlayContext {
|
|||
|
||||
pub fn draw_scale(&mut self, start: DVec2, scale: f64, radius: f64, text: &str) {
|
||||
let sign = scale.signum();
|
||||
let mut fill_color = graphene_std::Color::from_rgb_str(crate::consts::COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.05)
|
||||
.to_rgba_hex_srgb();
|
||||
let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_WHITE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb();
|
||||
fill_color.insert(0, '#');
|
||||
let fill_color = Some(fill_color.as_str());
|
||||
self.line(start + DVec2::X * radius * sign, start + DVec2::X * (radius * scale), None, None);
|
||||
|
@ -463,10 +468,7 @@ impl OverlayContext {
|
|||
|
||||
// Hover ring
|
||||
if show_hover_ring {
|
||||
let mut fill_color = graphene_std::Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap())
|
||||
.unwrap()
|
||||
.with_alpha(0.5)
|
||||
.to_rgba_hex_srgb();
|
||||
let mut fill_color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.5).to_rgba_hex_srgb();
|
||||
fill_color.insert(0, '#');
|
||||
|
||||
self.render_context.set_line_width(HOVER_RING_STROKE_WIDTH);
|
||||
|
@ -550,6 +552,36 @@ impl OverlayContext {
|
|||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn dowel_pin(&mut self, position: DVec2, angle: f64, color: Option<&str>) {
|
||||
let (x, y) = (position.round() - DVec2::splat(0.5)).into();
|
||||
let color = color.unwrap_or(COLOR_OVERLAY_ORANGE);
|
||||
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
// Draw the background circle with a white fill and blue outline
|
||||
self.render_context.begin_path();
|
||||
self.render_context.arc(x, y, DOWEL_PIN_RADIUS, 0., TAU).expect("Failed to draw the circle");
|
||||
self.render_context.set_fill_style_str(COLOR_OVERLAY_WHITE);
|
||||
self.render_context.fill();
|
||||
self.render_context.set_stroke_style_str(color);
|
||||
self.render_context.stroke();
|
||||
|
||||
// Draw the two blue filled sectors
|
||||
self.render_context.begin_path();
|
||||
// Top-left sector
|
||||
self.render_context.move_to(x, y);
|
||||
self.render_context.arc(x, y, DOWEL_PIN_RADIUS, FRAC_PI_2 + angle, PI + angle).expect("Failed to draw arc");
|
||||
self.render_context.close_path();
|
||||
// Bottom-right sector
|
||||
self.render_context.move_to(x, y);
|
||||
self.render_context.arc(x, y, DOWEL_PIN_RADIUS, PI + FRAC_PI_2 + angle, TAU + angle).expect("Failed to draw arc");
|
||||
self.render_context.close_path();
|
||||
self.render_context.set_fill_style_str(color);
|
||||
self.render_context.fill();
|
||||
|
||||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
/// Used by the Pen and Path tools to outline the path of the shape.
|
||||
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
@ -599,9 +631,11 @@ impl OverlayContext {
|
|||
pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
||||
let color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb();
|
||||
|
||||
self.render_context.begin_path();
|
||||
self.bezier_command(bezier, transform, true);
|
||||
self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50);
|
||||
self.render_context.set_stroke_style_str(&color);
|
||||
self.render_context.set_line_width(4.);
|
||||
self.render_context.stroke();
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use super::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use super::network_interface::NodeNetworkInterface;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use glam::DVec2;
|
||||
use graph_craft::document::{NodeId, NodeNetwork};
|
||||
use serde::ser::SerializeStruct;
|
||||
|
||||
|
@ -98,6 +100,22 @@ impl SelectedNodes {
|
|||
.filter(move |&layer| self.layer_visible(layer, network_interface) && !self.layer_locked(layer, network_interface))
|
||||
}
|
||||
|
||||
pub fn selected_visible_and_unlocked_layers_mean_average_origin<'a>(&'a self, network_interface: &'a NodeNetworkInterface) -> DVec2 {
|
||||
let (sum, count) = self
|
||||
.selected_visible_and_unlocked_layers(network_interface)
|
||||
.map(|layer| graph_modification_utils::get_viewport_origin(layer, network_interface))
|
||||
.fold((glam::DVec2::ZERO, 0), |(sum, count), item| (sum + item, count + 1));
|
||||
if count == 0 { DVec2::ZERO } else { sum / count as f64 }
|
||||
}
|
||||
|
||||
pub fn selected_visible_and_unlocked_median_points<'a>(&'a self, network_interface: &'a NodeNetworkInterface) -> DVec2 {
|
||||
let (sum, count) = self
|
||||
.selected_visible_and_unlocked_layers(network_interface)
|
||||
.map(|layer| graph_modification_utils::get_viewport_center(layer, network_interface))
|
||||
.fold((glam::DVec2::ZERO, 0), |(sum, count), item| (sum + item, count + 1));
|
||||
if count == 0 { DVec2::ZERO } else { sum / count as f64 }
|
||||
}
|
||||
|
||||
pub fn selected_layers<'a>(&'a self, metadata: &'a DocumentMetadata) -> impl Iterator<Item = LayerNodeIdentifier> + 'a {
|
||||
metadata.all_layers().filter(|layer| self.0.contains(&layer.to_node()))
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ use crate::messages::portfolio::document::graph_operation::transform_utils;
|
|||
use crate::messages::portfolio::document::graph_operation::utility_types::{ModifyInputsContext, TransformIn};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::utility_types::ToolType;
|
||||
use glam::{DAffine2, DMat2, DVec2};
|
||||
|
@ -537,17 +536,6 @@ impl<'a> Selected<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn mean_average_of_pivots(&mut self) -> DVec2 {
|
||||
let xy_summation = self
|
||||
.selected
|
||||
.iter()
|
||||
.map(|&layer| graph_modification_utils::get_viewport_pivot(layer, self.network_interface))
|
||||
.reduce(|a, b| a + b)
|
||||
.unwrap_or_default();
|
||||
|
||||
xy_summation / self.selected.len() as f64
|
||||
}
|
||||
|
||||
pub fn center_of_aabb(&mut self) -> DVec2 {
|
||||
let [min, max] = self
|
||||
.selected
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
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;
|
||||
|
@ -10,26 +9,34 @@ pub struct CompassRose {
|
|||
}
|
||||
|
||||
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 selected = document.network_interface.selected_nodes();
|
||||
|
||||
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)
|
||||
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(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
.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]);
|
||||
let transform = transform * DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
|
||||
|
||||
self.compass_center = transform.transform_point2(DVec2::splat(0.5));
|
||||
}
|
||||
|
||||
|
|
|
@ -243,20 +243,25 @@ pub fn new_custom(id: NodeId, nodes: Vec<(NodeId, NodeTemplate)>, parent: LayerN
|
|||
LayerNodeIdentifier::new_unchecked(id)
|
||||
}
|
||||
|
||||
/// Locate the final pivot from the transform (TODO: decide how the pivot should actually work)
|
||||
pub fn get_pivot(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<DVec2> {
|
||||
let pivot_node_input_index = 5;
|
||||
if let TaggedValue::DVec2(pivot) = NodeGraphLayer::new(layer, network_interface).find_input("Transform", pivot_node_input_index)? {
|
||||
Some(*pivot)
|
||||
/// Locate the origin of the transform node
|
||||
pub fn get_origin(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<DVec2> {
|
||||
let origin_node_input_index = 1;
|
||||
if let TaggedValue::DVec2(origin) = NodeGraphLayer::new(layer, network_interface).find_input("Transform", origin_node_input_index)? {
|
||||
Some(*origin)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_viewport_pivot(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> DVec2 {
|
||||
pub fn get_viewport_origin(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> DVec2 {
|
||||
let origin = get_origin(layer, network_interface).unwrap_or_default();
|
||||
network_interface.document_metadata().document_to_viewport.transform_point2(origin)
|
||||
}
|
||||
|
||||
pub fn get_viewport_center(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> DVec2 {
|
||||
let [min, max] = network_interface.document_metadata().nonzero_bounding_box(layer);
|
||||
let pivot = get_pivot(layer, network_interface).unwrap_or(DVec2::splat(0.5));
|
||||
network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * pivot)
|
||||
let center = DVec2::splat(0.5);
|
||||
network_interface.document_metadata().transform_to_viewport(layer).transform_point2(min + (max - min) * center)
|
||||
}
|
||||
|
||||
/// Get the current gradient of a layer from the closest "Fill" node.
|
||||
|
|
|
@ -1,26 +1,180 @@
|
|||
//! Handler for the pivot overlay visible on the selected layer(s) whilst using the Select tool which controls the center of rotation/scale and origin of the layer.
|
||||
//! Handler for the pivot overlay visible on the selected layer(s) whilst using the Select tool which controls the center of rotation/scale.
|
||||
|
||||
use super::graph_modification_utils;
|
||||
use crate::consts::PIVOT_DIAMETER;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
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 std::collections::VecDeque;
|
||||
use graphene_std::{transform::ReferencePoint, vector::ManipulatorPointId};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub fn pin_pivot_widget(active: bool, enabled: bool, source: Source) -> WidgetHolder {
|
||||
IconButton::new(if active { "PinActive" } else { "PinInactive" }, 24)
|
||||
.tooltip(if active { "Unpin Transform Pivot" } else { "Pin Transform Pivot" })
|
||||
.disabled(!enabled)
|
||||
.on_update(move |_| match source {
|
||||
Source::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::TogglePivotPinned()).into(),
|
||||
Source::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::TogglePivotPinned()).into(),
|
||||
})
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
pub fn pivot_reference_point_widget(disabled: bool, reference_point: ReferencePoint, source: Source) -> WidgetHolder {
|
||||
ReferencePointInput::new(reference_point)
|
||||
.on_update(move |pivot_input: &ReferencePointInput| match source {
|
||||
Source::Select => SelectToolMessage::SetPivot { position: pivot_input.value }.into(),
|
||||
Source::Path => PathToolMessage::SetPivot { position: pivot_input.value }.into(),
|
||||
})
|
||||
.disabled(disabled)
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
pub fn dot_type_widget(state: DotState, source: Source) -> Vec<WidgetHolder> {
|
||||
let dot_type_entries = [DotType::Pivot, DotType::Average, DotType::Active]
|
||||
.iter()
|
||||
.map(|dot_type| {
|
||||
MenuListEntry::new(format!("{dot_type:?}")).label(dot_type.to_string()).on_commit({
|
||||
let value = source.clone();
|
||||
move |_| match value {
|
||||
Source::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::DotType(*dot_type)).into(),
|
||||
Source::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::DotType(*dot_type)).into(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
vec![
|
||||
CheckboxInput::new(state.enabled)
|
||||
.tooltip("Disable Transform Pivot Point")
|
||||
.on_update(move |optional_input: &CheckboxInput| match source {
|
||||
Source::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::ToggleDotType(optional_input.checked)).into(),
|
||||
Source::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::ToggleDotType(optional_input.checked)).into(),
|
||||
})
|
||||
.widget_holder(),
|
||||
Separator::new(SeparatorType::Related).widget_holder(),
|
||||
DropdownInput::new(vec![dot_type_entries])
|
||||
.selected_index(Some(match state.dot {
|
||||
DotType::Pivot => 0,
|
||||
DotType::Average => 1,
|
||||
DotType::Active => 2,
|
||||
}))
|
||||
.tooltip("Choose between type of Transform Pivot Point")
|
||||
.disabled(!state.enabled)
|
||||
.widget_holder(),
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Source {
|
||||
Path,
|
||||
#[default]
|
||||
Select,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Dot {
|
||||
pub pivot: Pivot,
|
||||
pub state: DotState,
|
||||
pub layer: Option<LayerNodeIdentifier>,
|
||||
pub point: Option<ManipulatorPointId>,
|
||||
}
|
||||
|
||||
impl Dot {
|
||||
pub fn position(&self, document: &DocumentMessageHandler) -> DVec2 {
|
||||
let network = &document.network_interface;
|
||||
self.state
|
||||
.enabled
|
||||
.then_some({
|
||||
match self.state.dot {
|
||||
DotType::Average => Some(network.selected_nodes().selected_visible_and_unlocked_layers_mean_average_origin(network)),
|
||||
DotType::Pivot => self.pivot.pivot,
|
||||
DotType::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 DotType {
|
||||
// Pivot
|
||||
#[default]
|
||||
Pivot,
|
||||
// Origin
|
||||
Average,
|
||||
Active,
|
||||
// TODO: Add "Individual"
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct DotState {
|
||||
pub enabled: bool,
|
||||
pub dot: DotType,
|
||||
}
|
||||
|
||||
impl Default for DotState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
dot: DotType::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DotState {
|
||||
pub fn is_pivot_type(&self) -> bool {
|
||||
self.dot == DotType::Pivot || !self.enabled
|
||||
}
|
||||
|
||||
pub fn is_pivot(&self) -> bool {
|
||||
self.dot == DotType::Pivot && self.enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DotType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
DotType::Pivot => write!(f, "Custom Pivot"),
|
||||
DotType::Average => write!(f, "Origin (Average Point)"),
|
||||
DotType::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
|
||||
transform_from_normalized: DAffine2,
|
||||
/// The viewspace pivot position (if applicable)
|
||||
pivot: Option<DVec2>,
|
||||
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
|
||||
old_pivot_position: ReferencePoint,
|
||||
pub old_pivot_position: ReferencePoint,
|
||||
/// The last ReferencePoint which wasn't none
|
||||
pub last_non_none_reference: ReferencePoint,
|
||||
/// Used to enable and disable the pivot
|
||||
active: bool,
|
||||
pub pinned: bool,
|
||||
/// Had selected_visible_and_unlocked_layers
|
||||
pub empty: bool,
|
||||
}
|
||||
|
||||
impl Default for Pivot {
|
||||
|
@ -30,84 +184,62 @@ impl Default for Pivot {
|
|||
transform_from_normalized: Default::default(),
|
||||
pivot: Default::default(),
|
||||
old_pivot_position: ReferencePoint::Center,
|
||||
active: true,
|
||||
last_non_none_reference: ReferencePoint::Center,
|
||||
pinned: false,
|
||||
empty: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pivot {
|
||||
/// Calculates the transform that gets from normalized pivot to viewspace.
|
||||
fn get_layer_pivot_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 {
|
||||
let [min, max] = document.metadata().nonzero_bounding_box(layer);
|
||||
/// 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 bounds_transform = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
|
||||
let layer_transform = document.metadata().transform_to_viewport(layer);
|
||||
layer_transform * bounds_transform
|
||||
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(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
||||
/// Recomputes the pivot position and transform.
|
||||
fn recalculate_pivot(&mut self, document: &DocumentMessageHandler) {
|
||||
if !self.active {
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
// If no layers are selected then we revert things back to default
|
||||
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;
|
||||
};
|
||||
|
||||
// Add one because the first item is consumed above.
|
||||
let selected_layers_count = layers.count() + 1;
|
||||
|
||||
// If just one layer is selected we can use its inner transform (as it accounts for rotation)
|
||||
if selected_layers_count == 1 {
|
||||
let normalized_pivot = graph_modification_utils::get_pivot(first, &document.network_interface).unwrap_or(DVec2::splat(0.5));
|
||||
self.normalized_pivot = normalized_pivot;
|
||||
self.transform_from_normalized = Self::get_layer_pivot_transform(first, document);
|
||||
self.pivot = Some(self.transform_from_normalized.transform_point2(normalized_pivot));
|
||||
} else {
|
||||
// If more than one layer is selected we use the AABB with the mean of the pivots
|
||||
let xy_summation = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.network_interface))
|
||||
.reduce(|a, b| a + b)
|
||||
.unwrap_or_default();
|
||||
|
||||
let pivot = xy_summation / selected_layers_count as f64;
|
||||
self.pivot = Some(pivot);
|
||||
let [min, max] = document.selected_visible_and_unlock_layers_bounding_box_viewport().unwrap_or([DVec2::ZERO, DVec2::ONE]);
|
||||
self.normalized_pivot = (pivot - min) / (max - min);
|
||||
|
||||
self.transform_from_normalized = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext, draw_data: Option<(f64,)>) {
|
||||
if !overlay_context.visibility_settings.pivot() {
|
||||
self.active = false;
|
||||
return;
|
||||
} else {
|
||||
self.active = true;
|
||||
}
|
||||
|
||||
self.recalculate_pivot(document);
|
||||
if let (Some(pivot), Some(data)) = (self.pivot, draw_data) {
|
||||
overlay_context.pivot(pivot, data.0);
|
||||
}
|
||||
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 {
|
||||
if !self.active {
|
||||
return false;
|
||||
}
|
||||
|
||||
let new = self.to_pivot_position();
|
||||
let should_refresh = new != self.old_pivot_position;
|
||||
self.old_pivot_position = new;
|
||||
|
@ -118,37 +250,24 @@ impl Pivot {
|
|||
self.normalized_pivot.into()
|
||||
}
|
||||
|
||||
/// Sets the viewport position of the pivot for all selected layers.
|
||||
pub fn set_viewport_position(&self, position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if !self.active {
|
||||
/// 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;
|
||||
}
|
||||
};
|
||||
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) {
|
||||
let transform = Self::get_layer_pivot_transform(layer, document);
|
||||
// Only update the pivot when computed position is finite.
|
||||
if transform.matrix2.determinant().abs() <= f64::EPSILON {
|
||||
return;
|
||||
};
|
||||
let pivot = transform.inverse().transform_point2(position);
|
||||
responses.add(GraphOperationMessage::TransformSetPivot { layer, pivot });
|
||||
}
|
||||
self.normalized_pivot = self.transform_from_normalized.inverse().transform_point2(position);
|
||||
self.pivot = Some(position);
|
||||
}
|
||||
|
||||
/// Set the pivot using the normalized transform that is set above.
|
||||
pub fn set_normalized_position(&self, position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
if !self.active {
|
||||
return;
|
||||
}
|
||||
|
||||
self.set_viewport_position(self.transform_from_normalized.transform_point2(position), document, responses);
|
||||
/// 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 {
|
||||
if !self.active {
|
||||
return false;
|
||||
}
|
||||
self.pivot.filter(|&pivot| mouse.distance_squared(pivot) < (PIVOT_DIAMETER / 2.).powi(2)).is_some()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -393,6 +393,7 @@ pub fn transforming_transform_cage(
|
|||
input: &InputPreprocessorMessageHandler,
|
||||
responses: &mut VecDeque<Message>,
|
||||
layers_dragging: &mut Vec<LayerNodeIdentifier>,
|
||||
pos: Option<DVec2>,
|
||||
) -> (bool, bool, bool) {
|
||||
let dragging_bounds = bounding_box_manager.as_mut().and_then(|bounding_box| {
|
||||
let edges = bounding_box.check_selected_edges(input.mouse.position);
|
||||
|
@ -429,17 +430,12 @@ pub fn transforming_transform_cage(
|
|||
}
|
||||
});
|
||||
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
layers_dragging,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
None,
|
||||
);
|
||||
bounds.center_of_transformation = selected.mean_average_of_pivots();
|
||||
bounds.center_of_transformation = pos.unwrap_or_else(|| {
|
||||
document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers_mean_average_origin(&document.network_interface)
|
||||
});
|
||||
|
||||
// Check if we're hovering over a skew triangle
|
||||
let edges = bounds.check_selected_edges(input.mouse.position);
|
||||
|
@ -469,18 +465,12 @@ pub fn transforming_transform_cage(
|
|||
}
|
||||
});
|
||||
|
||||
let mut selected = Selected::new(
|
||||
&mut bounds.original_transforms,
|
||||
&mut bounds.center_of_transformation,
|
||||
&selected,
|
||||
responses,
|
||||
&document.network_interface,
|
||||
None,
|
||||
&ToolType::Select,
|
||||
None,
|
||||
);
|
||||
|
||||
bounds.center_of_transformation = selected.mean_average_of_pivots();
|
||||
bounds.center_of_transformation = pos.unwrap_or_else(|| {
|
||||
document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers_mean_average_origin(&document.network_interface)
|
||||
});
|
||||
}
|
||||
|
||||
*layers_dragging = selected;
|
||||
|
|
|
@ -181,6 +181,11 @@ impl MessageHandler<ToolMessage, ToolMessageData<'_>> for ToolMessageHandler {
|
|||
send: Box::new(TransformLayerMessage::SelectionChanged.into()),
|
||||
});
|
||||
|
||||
responses.add(BroadcastMessage::SubscribeEvent {
|
||||
on: BroadcastEvent::SelectionChanged,
|
||||
send: Box::new(SelectToolMessage::SyncHistory.into()),
|
||||
});
|
||||
|
||||
self.tool_is_active = true;
|
||||
|
||||
let tool_data = &mut self.tool_state.tool_data;
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node
|
|||
use crate::messages::portfolio::document::utility_types::transformation::Axis;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::pivot::{Dot, DotType, Source, dot_type_widget, pin_pivot_widget, pivot_reference_point_widget};
|
||||
use crate::messages::tool::common_functionality::shape_editor::{
|
||||
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedLayerState, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
|
||||
};
|
||||
|
@ -18,6 +19,7 @@ use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandi
|
|||
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate};
|
||||
use bezier_rs::{Bezier, BezierHandles, TValue};
|
||||
use graphene_std::renderer::Quad;
|
||||
use graphene_std::transform::ReferencePoint;
|
||||
use graphene_std::vector::{HandleExt, HandleId, NoHashBuilder, SegmentId, VectorData};
|
||||
use graphene_std::vector::{ManipulatorPointId, PointId, VectorModificationType};
|
||||
use std::vec;
|
||||
|
@ -106,6 +108,9 @@ pub enum PathToolMessage {
|
|||
SelectedPointYChanged {
|
||||
new_y: f64,
|
||||
},
|
||||
SetPivot {
|
||||
position: ReferencePoint,
|
||||
},
|
||||
SwapSelectedHandles,
|
||||
UpdateOptions(PathOptionsUpdate),
|
||||
UpdateSelectedPointsStatus {
|
||||
|
@ -141,6 +146,9 @@ pub enum PathOptionsUpdate {
|
|||
OverlayModeType(PathOverlayMode),
|
||||
PointEditingMode { enabled: bool },
|
||||
SegmentEditingMode { enabled: bool },
|
||||
DotType(DotType),
|
||||
ToggleDotType(bool),
|
||||
TogglePivotPinned(),
|
||||
}
|
||||
|
||||
impl ToolMetadata for PathTool {
|
||||
|
@ -255,6 +263,16 @@ impl LayoutHolder for PathTool {
|
|||
.selected_index(Some(self.options.path_overlay_mode as u32))
|
||||
.widget_holder();
|
||||
|
||||
let [_checkbox, _dropdown] = {
|
||||
let dot_widget = dot_type_widget(self.tool_data.dot.state, Source::Path);
|
||||
[dot_widget[0].clone(), dot_widget[2].clone()]
|
||||
};
|
||||
|
||||
let has_somrthing = !self.tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty();
|
||||
let _pivot_reference = pivot_reference_point_widget(has_somrthing || !self.tool_data.dot.state.is_pivot(), self.tool_data.dot.pivot.to_pivot_position(), Source::Path);
|
||||
|
||||
let _pin_pivot = pin_pivot_widget(self.tool_data.dot.pin_active(), false, Source::Path);
|
||||
|
||||
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
|
||||
widgets: vec![
|
||||
x_location,
|
||||
|
@ -268,8 +286,16 @@ impl LayoutHolder for PathTool {
|
|||
point_editing_mode,
|
||||
related_seperator.clone(),
|
||||
segment_editing_mode,
|
||||
unrelated_seperator,
|
||||
unrelated_seperator.clone(),
|
||||
path_overlay_mode_widget,
|
||||
unrelated_seperator.clone(),
|
||||
// checkbox.clone(),
|
||||
// related_seperator.clone(),
|
||||
// dropdown.clone(),
|
||||
// unrelated_seperator,
|
||||
// pivot_reference,
|
||||
// related_seperator.clone(),
|
||||
// pin_pivot,
|
||||
],
|
||||
}]))
|
||||
}
|
||||
|
@ -293,6 +319,29 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
|
|||
self.options.path_editing_mode.segment_editing_mode = enabled;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
PathOptionsUpdate::DotType(dot_type) => {
|
||||
if self.tool_data.dot.state.enabled {
|
||||
self.tool_data.dot.state.dot = dot_type;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
let dot = self.tool_data.get_as_dot();
|
||||
responses.add(TransformLayerMessage::SetDot { dot });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
}
|
||||
PathOptionsUpdate::ToggleDotType(state) => {
|
||||
self.tool_data.dot.state.enabled = state;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
PathOptionsUpdate::TogglePivotPinned() => {
|
||||
self.tool_data.dot.pivot.pinned = !self.tool_data.dot.pivot.pinned;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
},
|
||||
ToolMessage::Path(PathToolMessage::ClosePath) => {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
|
@ -443,6 +492,8 @@ struct PathToolData {
|
|||
last_click_time: u64,
|
||||
dragging_state: DraggingState,
|
||||
angle: f64,
|
||||
dot: Dot,
|
||||
ordered_points: Vec<ManipulatorPointId>,
|
||||
opposite_handle_position: Option<DVec2>,
|
||||
last_clicked_point_was_selected: bool,
|
||||
last_clicked_segment_was_selected: bool,
|
||||
|
@ -1315,6 +1366,16 @@ impl PathToolData {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_as_dot(&self) -> Dot {
|
||||
self.dot.clone()
|
||||
}
|
||||
|
||||
fn sync_history(&mut self, points: &Vec<ManipulatorPointId>) {
|
||||
self.ordered_points.retain(|layer| points.contains(layer));
|
||||
self.ordered_points.extend(points.iter().find(|&layer| !self.ordered_points.contains(layer)));
|
||||
self.dot.point = self.ordered_points.last().map(|x| *x)
|
||||
}
|
||||
}
|
||||
|
||||
impl Fsm for PathToolFsmState {
|
||||
|
@ -1327,6 +1388,10 @@ impl Fsm for PathToolFsmState {
|
|||
update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options);
|
||||
|
||||
let ToolMessage::Path(event) = event else { return self };
|
||||
|
||||
// TODO(mTvare6): Remove it once dots are implemented for path_tool
|
||||
tool_data.dot.state.enabled = false;
|
||||
|
||||
match (self, event) {
|
||||
(_, PathToolMessage::SelectionChanged) => {
|
||||
// Set the newly targeted layers to visible
|
||||
|
@ -1343,6 +1408,9 @@ impl Fsm for PathToolFsmState {
|
|||
shape_editor.update_selected_anchors_status(display_anchors);
|
||||
shape_editor.update_selected_handles_status(display_handles);
|
||||
|
||||
let new_points = shape_editor.selected_points().copied().collect::<Vec<_>>();
|
||||
tool_data.sync_history(&new_points);
|
||||
|
||||
self
|
||||
}
|
||||
(_, PathToolMessage::Overlays(mut overlay_context)) => {
|
||||
|
@ -1710,14 +1778,8 @@ impl Fsm for PathToolFsmState {
|
|||
}
|
||||
(PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => {
|
||||
tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize);
|
||||
|
||||
if !tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty() {
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||
}
|
||||
|
||||
if tool_data.adjacent_anchor_offset.is_some() {
|
||||
tool_data.adjacent_anchor_offset = None;
|
||||
}
|
||||
tool_data.saved_points_before_anchor_convert_smooth_sharp.clear();
|
||||
tool_data.adjacent_anchor_offset = None;
|
||||
tool_data.stored_selection = None;
|
||||
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
|
@ -2208,6 +2270,18 @@ impl Fsm for PathToolFsmState {
|
|||
responses.add(DocumentMessage::EndTransaction);
|
||||
PathToolFsmState::Ready
|
||||
}
|
||||
(_, PathToolMessage::SetPivot { position }) => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
tool_data.dot.pivot.last_non_none_reference = position;
|
||||
let pos: Option<DVec2> = position.into();
|
||||
tool_data.dot.pivot.set_normalized_position(pos.unwrap());
|
||||
let dot = tool_data.get_as_dot();
|
||||
responses.add(TransformLayerMessage::SetDot { dot });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
||||
self
|
||||
}
|
||||
(_, _) => PathToolFsmState::Ready,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,10 @@ use crate::messages::portfolio::document::utility_types::nodes::SelectedNodes;
|
|||
use crate::messages::preferences::SelectionMode;
|
||||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::compass_rose::{Axis, CompassRose};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
|
||||
use crate::messages::tool::common_functionality::measure;
|
||||
use crate::messages::tool::common_functionality::pivot::Pivot;
|
||||
use crate::messages::tool::common_functionality::pivot::{Dot, DotType, Source, dot_type_widget, pin_pivot_widget, pivot_reference_point_widget};
|
||||
use crate::messages::tool::common_functionality::shape_editor::SelectionShapeType;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
|
@ -43,6 +44,9 @@ pub struct SelectOptions {
|
|||
#[derive(PartialEq, Eq, Clone, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub enum SelectOptionsUpdate {
|
||||
NestedSelectionBehavior(NestedSelectionBehavior),
|
||||
DotType(DotType),
|
||||
ToggleDotType(bool),
|
||||
TogglePivotPinned(),
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
|
@ -95,6 +99,10 @@ pub enum SelectToolMessage {
|
|||
SetPivot {
|
||||
position: ReferencePoint,
|
||||
},
|
||||
SyncHistory,
|
||||
ShiftSelectedNodes {
|
||||
offset: DVec2,
|
||||
},
|
||||
}
|
||||
|
||||
impl ToolMetadata for SelectTool {
|
||||
|
@ -126,13 +134,6 @@ impl SelectTool {
|
|||
.widget_holder()
|
||||
}
|
||||
|
||||
fn pivot_reference_point_widget(&self, disabled: bool) -> WidgetHolder {
|
||||
ReferencePointInput::new(self.tool_data.pivot.to_pivot_position())
|
||||
.on_update(|pivot_input: &ReferencePointInput| SelectToolMessage::SetPivot { position: pivot_input.value }.into())
|
||||
.disabled(disabled)
|
||||
.widget_holder()
|
||||
}
|
||||
|
||||
fn alignment_widgets(&self, disabled: bool) -> impl Iterator<Item = WidgetHolder> + use<> {
|
||||
[AlignAxis::X, AlignAxis::Y]
|
||||
.into_iter()
|
||||
|
@ -203,9 +204,26 @@ impl LayoutHolder for SelectTool {
|
|||
// Select mode (Deep/Shallow)
|
||||
widgets.push(self.deep_selection_widget());
|
||||
|
||||
// Pivot
|
||||
// Dot Type (checkbox + dropdown for pivot/origin)
|
||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||
widgets.push(self.pivot_reference_point_widget(self.tool_data.selected_layers_count == 0));
|
||||
widgets.extend(dot_type_widget(self.tool_data.dot.state, Source::Select));
|
||||
|
||||
if self.tool_data.dot.state.is_pivot_type() {
|
||||
// Reference point 9-box widget
|
||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||
widgets.push(pivot_reference_point_widget(
|
||||
self.tool_data.selected_layers_count == 0 || !self.tool_data.dot.state.is_pivot(),
|
||||
self.tool_data.dot.pivot.to_pivot_position(),
|
||||
Source::Select,
|
||||
));
|
||||
|
||||
// Pivot pin
|
||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||
|
||||
let pin_enabled = self.tool_data.dot.pivot.old_pivot_position == ReferencePoint::None || !self.tool_data.dot.state.enabled;
|
||||
|
||||
widgets.push(pin_pivot_widget(self.tool_data.dot.pin_active(), pin_enabled, Source::Select));
|
||||
}
|
||||
|
||||
// Align
|
||||
let disabled = self.tool_data.selected_layers_count < 2;
|
||||
|
@ -244,14 +262,42 @@ impl LayoutHolder for SelectTool {
|
|||
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for SelectTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
|
||||
if let ToolMessage::Select(SelectToolMessage::SelectOptions(SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior))) = message {
|
||||
self.tool_data.nested_selection_behavior = nested_selection_behavior;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
let mut redraw_ref_pivot = false;
|
||||
if let ToolMessage::Select(SelectToolMessage::SelectOptions(ref option_update)) = message {
|
||||
match option_update {
|
||||
SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior) => {
|
||||
self.tool_data.nested_selection_behavior = *nested_selection_behavior;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
}
|
||||
SelectOptionsUpdate::DotType(dot_type) => {
|
||||
if self.tool_data.dot.state.enabled {
|
||||
self.tool_data.dot.state.dot = *dot_type;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
let dot = self.tool_data.get_as_dot();
|
||||
responses.add(TransformLayerMessage::SetDot { dot });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
redraw_ref_pivot = true;
|
||||
}
|
||||
}
|
||||
SelectOptionsUpdate::ToggleDotType(state) => {
|
||||
self.tool_data.dot.state.enabled = *state;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
redraw_ref_pivot = true;
|
||||
}
|
||||
|
||||
SelectOptionsUpdate::TogglePivotPinned() => {
|
||||
self.tool_data.dot.pivot.pinned = !self.tool_data.dot.pivot.pinned;
|
||||
responses.add(ToolMessage::UpdateHints);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
redraw_ref_pivot = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, false);
|
||||
|
||||
if self.tool_data.pivot.should_refresh_pivot_position() || self.tool_data.selected_layers_changed {
|
||||
if self.tool_data.dot.pivot.should_refresh_pivot_position() || self.tool_data.selected_layers_changed || redraw_ref_pivot {
|
||||
// Send the layout containing the updated pivot position (a bit ugly to do it here not in the fsm but that doesn't have SelectTool)
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
self.tool_data.selected_layers_changed = false;
|
||||
|
@ -323,7 +369,8 @@ struct SelectToolData {
|
|||
drag_current: ViewportPosition,
|
||||
lasso_polygon: Vec<ViewportPosition>,
|
||||
selection_mode: Option<SelectionMode>,
|
||||
layers_dragging: Vec<LayerNodeIdentifier>,
|
||||
layers_dragging: Vec<LayerNodeIdentifier>, // Unordered, often used as temporary buffer
|
||||
ordered_layers: Vec<LayerNodeIdentifier>, // Ordered list of layers
|
||||
layer_selected_on_start: Option<LayerNodeIdentifier>,
|
||||
select_single_layer: Option<LayerNodeIdentifier>,
|
||||
axis_align: bool,
|
||||
|
@ -331,7 +378,8 @@ struct SelectToolData {
|
|||
bounding_box_manager: Option<BoundingBoxManager>,
|
||||
snap_manager: SnapManager,
|
||||
cursor: MouseCursorIcon,
|
||||
pivot: Pivot,
|
||||
dot: Dot,
|
||||
dot_start: Option<DVec2>,
|
||||
compass_rose: CompassRose,
|
||||
line_center: DVec2,
|
||||
skew_edge: EdgeBool,
|
||||
|
@ -497,6 +545,24 @@ impl SelectToolData {
|
|||
responses.add(NodeGraphMessage::SendGraph);
|
||||
self.layers_dragging = original;
|
||||
}
|
||||
|
||||
fn state_from_dot(&self, mouse: DVec2) -> Option<SelectToolFsmState> {
|
||||
match self.dot.state.dot {
|
||||
DotType::Pivot => self.dot.pivot.is_over(mouse).then_some(SelectToolFsmState::DraggingPivot),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_as_dot(&self) -> Dot {
|
||||
self.dot.clone()
|
||||
}
|
||||
|
||||
fn sync_history(&mut self, document: &DocumentMessageHandler) {
|
||||
let layers: Vec<_> = document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface).collect();
|
||||
self.ordered_layers.retain(|layer| layers.contains(layer));
|
||||
self.ordered_layers.extend(layers.iter().find(|&layer| !self.ordered_layers.contains(layer)));
|
||||
self.dot.layer = self.ordered_layers.last().map(|x| *x)
|
||||
}
|
||||
}
|
||||
|
||||
impl Fsm for SelectToolFsmState {
|
||||
|
@ -710,8 +776,62 @@ impl Fsm for SelectToolFsmState {
|
|||
.flatten()
|
||||
});
|
||||
|
||||
// Update pivot
|
||||
tool_data.pivot.update_pivot(document, &mut overlay_context, Some((angle,)));
|
||||
let mut active_origin = None;
|
||||
let mut origin_angle = 0.;
|
||||
if overlay_context.visibility_settings.origin() && !tool_data.dot.state.is_pivot_type() {
|
||||
let get_angle = |layer: LayerNodeIdentifier| -> f64 {
|
||||
let quad = Quad::from_box([DVec2::ZERO, DVec2::ONE]);
|
||||
let bounds = document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface) * quad;
|
||||
(bounds.top_left() - bounds.top_right()).to_angle()
|
||||
};
|
||||
if tool_data.dot.state.dot == DotType::Average {
|
||||
let mut count = 0;
|
||||
|
||||
let sum: f64 = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.map(get_angle)
|
||||
.inspect(|_| count += 1)
|
||||
.sum();
|
||||
if count > 0 {
|
||||
origin_angle = sum / count as f64;
|
||||
}
|
||||
} else if tool_data.dot.state.dot == DotType::Active {
|
||||
origin_angle = document
|
||||
.network_interface
|
||||
.selected_nodes()
|
||||
.selected_visible_and_unlocked_layers(&document.network_interface)
|
||||
.find(|&layer| Some(layer) == tool_data.dot.layer)
|
||||
.iter()
|
||||
.map(|&layer| get_angle(layer))
|
||||
.sum();
|
||||
}
|
||||
|
||||
for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) {
|
||||
let origin = graph_modification_utils::get_viewport_origin(layer, &document.network_interface);
|
||||
if Some(layer) == tool_data.dot.layer {
|
||||
active_origin = Some(origin);
|
||||
continue;
|
||||
}
|
||||
overlay_context.dowel_pin(origin, origin_angle, None);
|
||||
}
|
||||
}
|
||||
if let Some(origin) = active_origin {
|
||||
overlay_context.dowel_pin(origin, origin_angle, Some(COLOR_OVERLAY_YELLOW));
|
||||
}
|
||||
|
||||
let has_layers = document.network_interface.selected_nodes().has_selected_nodes();
|
||||
let draw_pivot = tool_data.dot.state.is_pivot() && overlay_context.visibility_settings.pivot() && has_layers;
|
||||
tool_data.dot.pivot.recalculate_pivot(document);
|
||||
let pivot = draw_pivot.then_some(tool_data.dot.pivot.pivot).flatten();
|
||||
if let Some(pivot) = pivot {
|
||||
let offset = tool_data
|
||||
.dot_start
|
||||
.map(|offset| tool_data.dot.pivot_disconnected().then_some(tool_data.drag_current - offset).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
overlay_context.pivot(pivot + offset, angle);
|
||||
}
|
||||
|
||||
// Update compass rose
|
||||
if overlay_context.visibility_settings.compass_rose() {
|
||||
|
@ -837,6 +957,7 @@ impl Fsm for SelectToolFsmState {
|
|||
(SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, None, fill_color),
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
(_, SelectToolMessage::EditLayer) => {
|
||||
|
@ -868,7 +989,8 @@ impl Fsm for SelectToolFsmState {
|
|||
let intersection_list = document.click_list(input).collect::<Vec<_>>();
|
||||
let intersection = document.find_deepest(&intersection_list);
|
||||
|
||||
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging);
|
||||
let pos = tool_data.get_as_dot().position(document);
|
||||
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging, Some(pos));
|
||||
|
||||
// If the user is dragging the bounding box bounds, go into ResizingBounds mode.
|
||||
// If the user is dragging the rotate trigger, go into RotatingBounds mode.
|
||||
|
@ -883,20 +1005,17 @@ impl Fsm for SelectToolFsmState {
|
|||
let angle = bounds.map_or(0., |quad| (quad.top_left() - quad.top_right()).to_angle());
|
||||
let mouse_position = input.mouse.position;
|
||||
let compass_rose_state = tool_data.compass_rose.compass_rose_state(mouse_position, angle);
|
||||
let is_over_pivot = tool_data.pivot.is_over(mouse_position);
|
||||
|
||||
let show_compass = bounds.is_some_and(|quad| quad.all_sides_at_least_width(COMPASS_ROSE_HOVER_RING_DIAMETER) && quad.contains(mouse_position));
|
||||
let can_grab_compass_rose = compass_rose_state.can_grab() && (show_compass || bounds.is_none());
|
||||
|
||||
let state = if is_over_pivot
|
||||
// Dragging the pivot
|
||||
{
|
||||
let state = if let Some(state) = tool_data.state_from_dot(input.mouse.position) {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
// tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true);
|
||||
// tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]);
|
||||
|
||||
SelectToolFsmState::DraggingPivot
|
||||
state
|
||||
}
|
||||
// Dragging one (or two, forming a corner) of the transform cage bounding box edges
|
||||
else if resize {
|
||||
|
@ -917,12 +1036,13 @@ impl Fsm for SelectToolFsmState {
|
|||
}
|
||||
|
||||
tool_data.layers_dragging = selected;
|
||||
|
||||
tool_data.get_snap_candidates(document, input);
|
||||
let (axis, using_compass) = {
|
||||
let axis_state = compass_rose_state.axis_type().filter(|_| can_grab_compass_rose);
|
||||
(axis_state.unwrap_or_default(), axis_state.is_some())
|
||||
};
|
||||
|
||||
tool_data.dot_start = Some(tool_data.drag_current);
|
||||
SelectToolFsmState::Dragging {
|
||||
axis,
|
||||
using_compass,
|
||||
|
@ -941,6 +1061,10 @@ impl Fsm for SelectToolFsmState {
|
|||
let extend = input.keyboard.key(extend_selection);
|
||||
if !extend && !input.keyboard.key(remove_from_selection) {
|
||||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
if !tool_data.dot.pivot.pinned {
|
||||
let position = tool_data.dot.pivot.last_non_none_reference;
|
||||
responses.add(SelectToolMessage::SetPivot { position });
|
||||
}
|
||||
tool_data.layers_dragging.clear();
|
||||
}
|
||||
|
||||
|
@ -955,6 +1079,8 @@ impl Fsm for SelectToolFsmState {
|
|||
tool_data.get_snap_candidates(document, input);
|
||||
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
tool_data.dot_start = Some(tool_data.drag_current);
|
||||
SelectToolFsmState::Dragging {
|
||||
axis: Axis::None,
|
||||
using_compass: false,
|
||||
|
@ -1098,7 +1224,8 @@ impl Fsm for SelectToolFsmState {
|
|||
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove(modifier_keys)) => {
|
||||
let mouse_position = input.mouse.position;
|
||||
let snapped_mouse_position = mouse_position;
|
||||
tool_data.pivot.set_viewport_position(snapped_mouse_position, document, responses);
|
||||
tool_data.dot.pivot.set_viewport_position(snapped_mouse_position);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
|
@ -1143,7 +1270,7 @@ impl Fsm for SelectToolFsmState {
|
|||
.map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true, dragging_bounds, Some(tool_data.skew_edge)));
|
||||
|
||||
// Dragging the pivot overrules the other operations
|
||||
if tool_data.pivot.is_over(input.mouse.position) {
|
||||
if tool_data.state_from_dot(input.mouse.position).is_some() {
|
||||
cursor = MouseCursorIcon::Move;
|
||||
}
|
||||
|
||||
|
@ -1283,19 +1410,26 @@ impl Fsm for SelectToolFsmState {
|
|||
tool_data.snap_manager.cleanup(responses);
|
||||
tool_data.select_single_layer = None;
|
||||
|
||||
if let Some(start) = tool_data.dot_start {
|
||||
let offset = tool_data.dot.pivot_disconnected().then_some(tool_data.drag_current - start).unwrap_or_default();
|
||||
tool_data.dot.pivot.pivot.as_mut().map(|v| *v += offset);
|
||||
}
|
||||
tool_data.dot_start = None;
|
||||
|
||||
let dot = tool_data.get_as_dot();
|
||||
responses.add(TransformLayerMessage::SetDot { dot });
|
||||
|
||||
let selection = tool_data.nested_selection_behavior;
|
||||
SelectToolFsmState::Ready { selection }
|
||||
}
|
||||
(
|
||||
SelectToolFsmState::ResizingBounds
|
||||
| SelectToolFsmState::SkewingBounds { .. }
|
||||
| SelectToolFsmState::RotatingBounds
|
||||
| SelectToolFsmState::Dragging { .. }
|
||||
| SelectToolFsmState::DraggingPivot,
|
||||
SelectToolFsmState::ResizingBounds | SelectToolFsmState::SkewingBounds { .. } | SelectToolFsmState::RotatingBounds | SelectToolFsmState::DraggingPivot,
|
||||
SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter,
|
||||
) => {
|
||||
let drag_too_small = input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON;
|
||||
let response = if drag_too_small { DocumentMessage::AbortTransaction } else { DocumentMessage::EndTransaction };
|
||||
let dot = tool_data.get_as_dot();
|
||||
responses.add(TransformLayerMessage::SetDot { dot });
|
||||
responses.add(response);
|
||||
tool_data.axis_align = false;
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
@ -1432,8 +1566,25 @@ impl Fsm for SelectToolFsmState {
|
|||
(_, SelectToolMessage::SetPivot { position }) => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
||||
tool_data.dot.pivot.last_non_none_reference = position;
|
||||
tool_data.dot.pivot.pinned = false;
|
||||
let pos: Option<DVec2> = position.into();
|
||||
tool_data.pivot.set_normalized_position(pos.unwrap(), document, responses);
|
||||
tool_data.dot.pivot.set_normalized_position(pos.unwrap());
|
||||
let dot = tool_data.get_as_dot();
|
||||
responses.add(TransformLayerMessage::SetDot { dot });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
|
||||
self
|
||||
}
|
||||
(_, SelectToolMessage::SyncHistory) => {
|
||||
tool_data.sync_history(&document);
|
||||
|
||||
self
|
||||
}
|
||||
(_, SelectToolMessage::ShiftSelectedNodes { offset }) => {
|
||||
if tool_data.dot.pivot_disconnected() {
|
||||
tool_data.dot.pivot.pivot.as_mut().map(|v| *v += offset);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
@ -1658,6 +1809,7 @@ fn drag_deepest_manipulation(responses: &mut VecDeque<Message>, selected: Vec<La
|
|||
.next()
|
||||
.expect("ROOT_PARENT should have a layer child when clicking"),
|
||||
);
|
||||
|
||||
if !remove {
|
||||
tool_data.layers_dragging.extend(vec![layer]);
|
||||
} else {
|
||||
|
|
|
@ -557,7 +557,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
}
|
||||
}
|
||||
|
||||
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging);
|
||||
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging, None);
|
||||
|
||||
if !input.keyboard.key(Key::Control) {
|
||||
match (resize, rotate, skew) {
|
||||
|
|
|
@ -9,7 +9,6 @@ use crate::messages::portfolio::document::utility_types::network_interface::Inpu
|
|||
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name};
|
||||
use crate::messages::tool::common_functionality::pivot::Pivot;
|
||||
use crate::messages::tool::common_functionality::resize::Resize;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
|
@ -283,7 +282,6 @@ struct TextToolData {
|
|||
// Since the overlays must be drawn without knowledge of the inputs
|
||||
cached_resize_bounds: [DVec2; 2],
|
||||
bounding_box_manager: Option<BoundingBoxManager>,
|
||||
pivot: Pivot,
|
||||
snap_candidates: Vec<SnapCandidatePoint>,
|
||||
// TODO: Handle multiple layers in the future
|
||||
layer_dragging: Option<ResizingLayer>,
|
||||
|
@ -526,7 +524,6 @@ impl Fsm for TextToolFsmState {
|
|||
}
|
||||
|
||||
bounding_box_manager.render_overlays(&mut overlay_context, false);
|
||||
tool_data.pivot.update_pivot(document, &mut overlay_context, None);
|
||||
}
|
||||
} else {
|
||||
tool_data.bounding_box_manager.take();
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
|||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::transformation::TransformType;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::pivot::Dot;
|
||||
use glam::DVec2;
|
||||
|
||||
#[impl_message(Message, ToolMessage, TransformLayer)]
|
||||
|
@ -16,6 +17,7 @@ pub enum TransformLayerMessage {
|
|||
BeginGrab,
|
||||
BeginRotate,
|
||||
BeginScale,
|
||||
SetDot { dot: Dot },
|
||||
BeginGRS { operation: TransformType },
|
||||
BeginGrabPen { last_point: DVec2, handle: DVec2 },
|
||||
BeginRotatePen { last_point: DVec2, handle: DVec2 },
|
||||
|
|
|
@ -5,6 +5,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::portfolio::document::utility_types::misc::PTZ;
|
||||
use crate::messages::portfolio::document::utility_types::transformation::{Axis, OriginalTransforms, Selected, TransformOperation, TransformType, Typing};
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::pivot::{Dot, DotType};
|
||||
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
|
||||
use crate::messages::tool::tool_messages::tool_prelude::Key;
|
||||
use crate::messages::tool::utility_types::{ToolData, ToolType};
|
||||
|
@ -34,8 +35,11 @@ pub struct TransformLayerMessageHandler {
|
|||
start_mouse: ViewportPosition,
|
||||
|
||||
original_transforms: OriginalTransforms,
|
||||
dot: Dot,
|
||||
pivot: ViewportPosition,
|
||||
|
||||
path_bounds: Option<[DVec2; 2]>,
|
||||
|
||||
local_pivot: DocumentPosition,
|
||||
local_mouse_start: DocumentPosition,
|
||||
grab_target: DocumentPosition,
|
||||
|
@ -61,27 +65,66 @@ impl TransformLayerMessageHandler {
|
|||
}
|
||||
}
|
||||
|
||||
fn calculate_pivot(selected_points: &Vec<&ManipulatorPointId>, vector_data: &VectorData, viewspace: DAffine2, get_location: impl Fn(&ManipulatorPointId) -> Option<DVec2>) -> Option<(DVec2, DVec2)> {
|
||||
fn calculate_pivot(
|
||||
document: &DocumentMessageHandler,
|
||||
selected_points: &Vec<&ManipulatorPointId>,
|
||||
vector_data: &VectorData,
|
||||
viewspace: DAffine2,
|
||||
get_location: impl Fn(&ManipulatorPointId) -> Option<DVec2>,
|
||||
dot: &mut Dot,
|
||||
) -> (Option<(DVec2, DVec2)>, Option<[DVec2; 2]>) {
|
||||
let average_position = || {
|
||||
let mut point_count = 0;
|
||||
selected_points.iter().filter_map(|p| get_location(p)).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64
|
||||
};
|
||||
let bounds = selected_points.iter().filter_map(|p| get_location(p)).fold(None, |acc: Option<[DVec2; 2]>, point| {
|
||||
if let Some([mut min, mut max]) = acc {
|
||||
min.x = min.x.min(point.x);
|
||||
min.y = min.y.min(point.y);
|
||||
max.x = max.x.max(point.x);
|
||||
max.y = max.y.max(point.y);
|
||||
Some([min, max])
|
||||
} else {
|
||||
Some([point, point])
|
||||
}
|
||||
});
|
||||
dot.pivot.recalculate_pivot_for_layer(document, bounds);
|
||||
let position = || {
|
||||
{
|
||||
if dot.state.enabled {
|
||||
match dot.state.dot {
|
||||
DotType::Average => None,
|
||||
DotType::Active => dot.point.and_then(|p| get_location(&p)),
|
||||
DotType::Pivot => dot.pivot.pivot,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(average_position)
|
||||
};
|
||||
let [point] = selected_points.as_slice() else {
|
||||
// Handle the case where there are multiple points
|
||||
let mut point_count = 0;
|
||||
let average_position = selected_points.iter().filter_map(|p| get_location(p)).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64;
|
||||
|
||||
return Some((average_position, average_position));
|
||||
let position = position();
|
||||
return (Some((position, position)), bounds);
|
||||
};
|
||||
|
||||
match point {
|
||||
ManipulatorPointId::PrimaryHandle(_) | ManipulatorPointId::EndHandle(_) => {
|
||||
// Get the anchor position and transform it to the pivot
|
||||
let pivot_pos = point.get_anchor_position(vector_data).map(|anchor_position| viewspace.transform_point2(anchor_position))?;
|
||||
let target = viewspace.transform_point2(point.get_position(vector_data)?);
|
||||
Some((pivot_pos, target))
|
||||
let (Some(pivot_pos), Some(position)) = (
|
||||
point.get_anchor_position(vector_data).map(|anchor_position| viewspace.transform_point2(anchor_position)),
|
||||
point.get_position(vector_data),
|
||||
) else {
|
||||
return (None, None);
|
||||
};
|
||||
let target = viewspace.transform_point2(position);
|
||||
(Some((pivot_pos, target)), None)
|
||||
}
|
||||
_ => {
|
||||
// Calculate the average position of all selected points
|
||||
let mut point_count = 0;
|
||||
let average_position = selected_points.iter().filter_map(|p| get_location(p)).inspect(|_| point_count += 1).sum::<DVec2>() / point_count as f64;
|
||||
Some((average_position, average_position))
|
||||
let position = position();
|
||||
(Some((position, position)), bounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -177,18 +220,20 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
}
|
||||
|
||||
if !using_path_tool {
|
||||
*selected.pivot = selected.mean_average_of_pivots();
|
||||
self.dot.recalculate_transform(document);
|
||||
let network_interface = &document.network_interface;
|
||||
let mean_center_bbox = network_interface.selected_nodes().selected_visible_and_unlocked_layers_mean_average_origin(network_interface);
|
||||
let dot_position = self.dot.position(document);
|
||||
*selected.pivot = dot_position;
|
||||
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
|
||||
self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(selected.mean_average_of_pivots());
|
||||
self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(mean_center_bbox);
|
||||
}
|
||||
// Here vector data from all layers is not considered which can be a problem in pivot calculation
|
||||
else if let Some(vector_data) = selected_layers.first().and_then(|&layer| document.network_interface.compute_modified_vector(layer)) {
|
||||
*selected.original_transforms = OriginalTransforms::default();
|
||||
|
||||
let viewspace = document.metadata().transform_to_viewport(selected_layers[0]);
|
||||
|
||||
let selected_segments = shape_editor.selected_segments().collect::<HashSet<_>>();
|
||||
|
||||
let mut affected_points = shape_editor.selected_points().copied().collect::<Vec<_>>();
|
||||
|
||||
for (segment_id, _, start, end) in vector_data.segment_bezier_iter() {
|
||||
|
@ -201,8 +246,16 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
let affected_point_refs = affected_points.iter().collect();
|
||||
|
||||
let get_location = |point: &&ManipulatorPointId| point.get_position(&vector_data).map(|position| viewspace.transform_point2(position));
|
||||
if let Some((new_pivot, grab_target)) = calculate_pivot(&affected_point_refs, &vector_data, viewspace, |point: &ManipulatorPointId| get_location(&point)) {
|
||||
if let (Some((new_pivot, grab_target)), bounds) = calculate_pivot(
|
||||
document,
|
||||
&affected_point_refs,
|
||||
&vector_data,
|
||||
viewspace,
|
||||
|point: &ManipulatorPointId| get_location(&point),
|
||||
&mut self.dot,
|
||||
) {
|
||||
*selected.pivot = new_pivot;
|
||||
self.path_bounds = bounds;
|
||||
|
||||
self.local_pivot = document_to_viewport.inverse().transform_point2(*selected.pivot);
|
||||
self.grab_target = document_to_viewport.inverse().transform_point2(grab_target);
|
||||
|
@ -228,117 +281,115 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
return;
|
||||
}
|
||||
|
||||
for layer in document.metadata().all_layers() {
|
||||
if !document.network_interface.is_artboard(&layer.to_node(), &[]) {
|
||||
continue;
|
||||
};
|
||||
let viewport_box = input.viewport_bounds.size();
|
||||
let axis_constraint = self.transform_operation.axis_constraint();
|
||||
|
||||
let viewport_box = input.viewport_bounds.size();
|
||||
let axis_constraint = self.transform_operation.axis_constraint();
|
||||
let format_rounded = |value: f64, precision: usize| {
|
||||
if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
|
||||
format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
} else {
|
||||
self.typing.string.clone()
|
||||
}
|
||||
};
|
||||
|
||||
let format_rounded = |value: f64, precision: usize| {
|
||||
if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
|
||||
format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string()
|
||||
} else {
|
||||
self.typing.string.clone()
|
||||
// TODO: Ensure removing this and adding this doesn't change the position of layers under PTZ ops
|
||||
// responses.add(TransformLayerMessage::PointerMove {
|
||||
// slow_key: SLOW_KEY,
|
||||
// increments_key: INCREMENTS_KEY,
|
||||
// });
|
||||
|
||||
match self.transform_operation {
|
||||
TransformOperation::None => (),
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let translation = translation.to_dvec(self.initial_transform, self.increments);
|
||||
let viewport_translate = document_to_viewport.transform_vector2(translation);
|
||||
let pivot = document_to_viewport.transform_point2(self.grab_target);
|
||||
let quad = Quad::from_box([pivot, pivot + viewport_translate]).0;
|
||||
let e1 = (self.layer_bounding_box.0[1] - self.layer_bounding_box.0[0]).normalize_or(DVec2::X);
|
||||
|
||||
if matches!(axis_constraint, Axis::Both | Axis::X) && translation.x != 0. {
|
||||
let end = if self.local { (quad[1] - quad[0]).rotate(e1) + quad[0] } else { quad[1] };
|
||||
overlay_context.dashed_line(quad[0], end, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
|
||||
let x_transform = DAffine2::from_translation((quad[0] + end) / 2.);
|
||||
overlay_context.text(&format_rounded(translation.x, 3), COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Ensure removing this and adding this doesn't change the position of layers under PTZ ops
|
||||
// responses.add(TransformLayerMessage::PointerMove {
|
||||
// slow_key: SLOW_KEY,
|
||||
// increments_key: INCREMENTS_KEY,
|
||||
// });
|
||||
|
||||
match self.transform_operation {
|
||||
TransformOperation::None => (),
|
||||
TransformOperation::Grabbing(translation) => {
|
||||
let translation = translation.to_dvec(self.initial_transform, self.increments);
|
||||
let viewport_translate = document_to_viewport.transform_vector2(translation);
|
||||
let pivot = document_to_viewport.transform_point2(self.grab_target);
|
||||
let quad = Quad::from_box([pivot, pivot + viewport_translate]).0;
|
||||
let e1 = (self.layer_bounding_box.0[1] - self.layer_bounding_box.0[0]).normalize_or(DVec2::X);
|
||||
|
||||
if matches!(axis_constraint, Axis::Both | Axis::X) && translation.x != 0. {
|
||||
let end = if self.local { (quad[1] - quad[0]).rotate(e1) + quad[0] } else { quad[1] };
|
||||
overlay_context.dashed_line(quad[0], end, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
|
||||
let x_transform = DAffine2::from_translation((quad[0] + end) / 2.);
|
||||
overlay_context.text(&format_rounded(translation.x, 3), COLOR_OVERLAY_BLUE, None, x_transform, 4., [Pivot::Middle, Pivot::End]);
|
||||
}
|
||||
|
||||
if matches!(axis_constraint, Axis::Both | Axis::Y) && translation.y != 0. {
|
||||
let end = if self.local { (quad[3] - quad[0]).rotate(e1) + quad[0] } else { quad[3] };
|
||||
overlay_context.dashed_line(quad[0], end, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
let x_parameter = viewport_translate.x.clamp(-1., 1.);
|
||||
let y_transform = DAffine2::from_translation((quad[0] + end) / 2. + x_parameter * DVec2::X * 0.);
|
||||
let pivot_selection = if x_parameter >= -1e-3 { Pivot::Start } else { Pivot::End };
|
||||
if axis_constraint != Axis::Both || self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
|
||||
overlay_context.text(&format_rounded(translation.y, 2), COLOR_OVERLAY_BLUE, None, y_transform, 3., [pivot_selection, Pivot::Middle]);
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(axis_constraint, Axis::Both) && translation.x != 0. && translation.y != 0. {
|
||||
overlay_context.line(quad[1], quad[2], None, None);
|
||||
overlay_context.line(quad[3], quad[2], None, None);
|
||||
if matches!(axis_constraint, Axis::Both | Axis::Y) && translation.y != 0. {
|
||||
let end = if self.local { (quad[3] - quad[0]).rotate(e1) + quad[0] } else { quad[3] };
|
||||
overlay_context.dashed_line(quad[0], end, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
let x_parameter = viewport_translate.x.clamp(-1., 1.);
|
||||
let y_transform = DAffine2::from_translation((quad[0] + end) / 2. + x_parameter * DVec2::X * 0.);
|
||||
let pivot_selection = if x_parameter >= -1e-3 { Pivot::Start } else { Pivot::End };
|
||||
if axis_constraint != Axis::Both || self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
|
||||
overlay_context.text(&format_rounded(translation.y, 2), COLOR_OVERLAY_BLUE, None, y_transform, 3., [pivot_selection, Pivot::Middle]);
|
||||
}
|
||||
}
|
||||
TransformOperation::Scaling(scale) => {
|
||||
let scale = scale.to_f64(self.increments);
|
||||
let text = format!("{}x", format_rounded(scale, 3));
|
||||
let pivot = document_to_viewport.transform_point2(self.local_pivot);
|
||||
let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start);
|
||||
let local_edge = start_mouse - pivot;
|
||||
let local_edge = project_edge_to_quad(local_edge, &self.layer_bounding_box, self.local, axis_constraint);
|
||||
let boundary_point = pivot + local_edge * scale.min(1.);
|
||||
let end_point = pivot + local_edge * scale.max(1.);
|
||||
|
||||
if scale > 0. {
|
||||
overlay_context.dashed_line(pivot, boundary_point, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
}
|
||||
overlay_context.line(boundary_point, end_point, None, None);
|
||||
|
||||
let transform = DAffine2::from_translation(boundary_point.midpoint(pivot) + local_edge.perp().normalize_or(DVec2::X) * local_edge.element_product().signum() * 24.);
|
||||
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => {
|
||||
let angle = rotation.to_f64(self.increments);
|
||||
let pivot = document_to_viewport.transform_point2(self.local_pivot);
|
||||
let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start);
|
||||
let offset_angle = if self.grs_pen_handle {
|
||||
self.handle - self.last_point
|
||||
} else if using_path_tool {
|
||||
start_mouse - pivot
|
||||
} else {
|
||||
self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right()
|
||||
};
|
||||
let tilt_offset = document.document_ptz.unmodified_tilt();
|
||||
let offset_angle = offset_angle.to_angle() + tilt_offset;
|
||||
let width = viewport_box.max_element();
|
||||
let radius = start_mouse.distance(pivot);
|
||||
let arc_radius = ANGLE_MEASURE_RADIUS_FACTOR * width;
|
||||
let radius = radius.clamp(ARC_MEASURE_RADIUS_FACTOR_RANGE.0 * width, ARC_MEASURE_RADIUS_FACTOR_RANGE.1 * width);
|
||||
let angle_in_degrees = angle.to_degrees();
|
||||
let display_angle = if angle_in_degrees.is_sign_positive() {
|
||||
angle_in_degrees - (angle_in_degrees / 360.).floor() * 360.
|
||||
} else if angle_in_degrees.is_sign_negative() {
|
||||
angle_in_degrees - ((angle_in_degrees / 360.).floor() + 1.) * 360.
|
||||
} else {
|
||||
angle_in_degrees
|
||||
};
|
||||
let text = format!("{}°", format_rounded(display_angle, 2));
|
||||
let text_texture_width = overlay_context.get_width(&text) / 2.;
|
||||
let text_texture_height = 12.;
|
||||
let text_angle_on_unit_circle = DVec2::from_angle((angle % TAU) / 2. + offset_angle);
|
||||
let text_texture_position = DVec2::new(
|
||||
(arc_radius + 4. + text_texture_width) * text_angle_on_unit_circle.x,
|
||||
(arc_radius + text_texture_height) * text_angle_on_unit_circle.y,
|
||||
);
|
||||
let transform = DAffine2::from_translation(text_texture_position + pivot);
|
||||
overlay_context.draw_angle(pivot, radius, arc_radius, offset_angle, angle);
|
||||
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
if matches!(axis_constraint, Axis::Both) && translation.x != 0. && translation.y != 0. {
|
||||
overlay_context.line(quad[1], quad[2], None, None);
|
||||
overlay_context.line(quad[3], quad[2], None, None);
|
||||
}
|
||||
}
|
||||
TransformOperation::Scaling(scale) => {
|
||||
let scale = scale.to_f64(self.increments);
|
||||
let text = format!("{}x", format_rounded(scale, 3));
|
||||
let pivot = document_to_viewport.transform_point2(self.local_pivot);
|
||||
let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start);
|
||||
let local_edge = start_mouse - pivot;
|
||||
let local_edge = project_edge_to_quad(local_edge, &self.layer_bounding_box, self.local, axis_constraint);
|
||||
let boundary_point = pivot + local_edge * scale.min(1.);
|
||||
let end_point = pivot + local_edge * scale.max(1.);
|
||||
|
||||
if scale > 0. {
|
||||
overlay_context.dashed_line(pivot, boundary_point, None, None, Some(2.), Some(2.), Some(0.5));
|
||||
}
|
||||
overlay_context.line(boundary_point, end_point, None, None);
|
||||
|
||||
let transform = DAffine2::from_translation(boundary_point.midpoint(pivot) + local_edge.perp().normalize_or(DVec2::X) * local_edge.element_product().signum() * 24.);
|
||||
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
TransformOperation::Rotating(rotation) => {
|
||||
let angle = rotation.to_f64(self.increments);
|
||||
let pivot = document_to_viewport.transform_point2(self.local_pivot);
|
||||
let start_mouse = document_to_viewport.transform_point2(self.local_mouse_start);
|
||||
let offset_angle = if self.grs_pen_handle {
|
||||
self.handle - self.last_point
|
||||
} else if using_path_tool {
|
||||
start_mouse - pivot
|
||||
} else {
|
||||
self.layer_bounding_box.top_right() - self.layer_bounding_box.top_right()
|
||||
};
|
||||
let tilt_offset = document.document_ptz.unmodified_tilt();
|
||||
let offset_angle = offset_angle.to_angle() + tilt_offset;
|
||||
let width = viewport_box.max_element();
|
||||
let radius = start_mouse.distance(pivot);
|
||||
let arc_radius = ANGLE_MEASURE_RADIUS_FACTOR * width;
|
||||
let radius = radius.clamp(ARC_MEASURE_RADIUS_FACTOR_RANGE.0 * width, ARC_MEASURE_RADIUS_FACTOR_RANGE.1 * width);
|
||||
let angle_in_degrees = angle.to_degrees();
|
||||
let display_angle = if angle_in_degrees.is_sign_positive() {
|
||||
angle_in_degrees - (angle_in_degrees / 360.).floor() * 360.
|
||||
} else if angle_in_degrees.is_sign_negative() {
|
||||
angle_in_degrees - ((angle_in_degrees / 360.).floor() + 1.) * 360.
|
||||
} else {
|
||||
angle_in_degrees
|
||||
};
|
||||
let text = format!("{}°", format_rounded(display_angle, 2));
|
||||
let text_texture_width = overlay_context.get_width(&text) / 2.;
|
||||
let text_texture_height = 12.;
|
||||
let text_angle_on_unit_circle = DVec2::from_angle((angle % TAU) / 2. + offset_angle);
|
||||
let text_texture_position = DVec2::new(
|
||||
(arc_radius + 4. + text_texture_width) * text_angle_on_unit_circle.x,
|
||||
(arc_radius + text_texture_height) * text_angle_on_unit_circle.y,
|
||||
);
|
||||
let transform = DAffine2::from_translation(text_texture_position + pivot);
|
||||
overlay_context.draw_angle(pivot, radius, arc_radius, offset_angle, angle);
|
||||
overlay_context.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(_) = self.path_bounds {
|
||||
// overlay_context.quad(Quad::from_box(bounds), None, None);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -694,6 +745,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
|
|||
self.initial_transform,
|
||||
)
|
||||
}
|
||||
TransformLayerMessage::SetDot { dot } => self.dot = dot,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ async fn transform<T: 'n + 'static>(
|
|||
rotate: f64,
|
||||
scale: DVec2,
|
||||
skew: DVec2,
|
||||
_pivot: DVec2,
|
||||
) -> Instances<T> {
|
||||
let matrix = DAffine2::from_scale_angle_translation(scale, rotate, translate) * DAffine2::from_cols_array(&[1., skew.y, skew.x, 1., 0., 0.]);
|
||||
|
||||
|
|
|
@ -352,8 +352,7 @@ async fn copy_to_points<I: 'n + Send + Clone>(
|
|||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(scale), rotation, translation);
|
||||
|
||||
for mut instance in instance.instance_ref_iter().map(|instance| instance.to_instance_cloned()) {
|
||||
let local_matrix = DAffine2::from_mat2(instance.transform.matrix2);
|
||||
instance.transform = transform * local_matrix;
|
||||
instance.transform = transform * instance.transform;
|
||||
|
||||
result_table.push(instance);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue