This commit is contained in:
mTvare 2025-07-07 11:02:51 +00:00 committed by GitHub
commit 91e68dd564
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1790 additions and 1129 deletions

1751
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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";

View file

@ -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();

View file

@ -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,

View file

@ -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");

View file

@ -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);

View file

@ -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()

View file

@ -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,

View file

@ -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();

View file

@ -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()))
}

View file

@ -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

View file

@ -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));
}

View file

@ -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.

View file

@ -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()
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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,
}
}

View file

@ -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 {

View file

@ -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) {

View file

@ -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();

View file

@ -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 },

View file

@ -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,
}
}

View file

@ -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.]);

View file

@ -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);
}