Add pivot type selection with Custom Pivot, Origin (Average Point), and Origin (Active Object) to the Select tool (#2730)

* add origin

* cleanup pivot

* a lot of stuff

* reset pivot

* fix transform with pivot issues

* fixes

* some more cleanup

* fixes

* finally works

* origin fixes

* fix spaces

* fix using dragged_layers

* simplify pivot logic

* fix bugs

* fix the final bug

* fix in select_tool

* fix updates

* some more refactors to fix misunderstanding and refactor

* add checkboxes

* fix labels

* fix stuff which broke at merge

* update

* cargo fmt

* fix serde crash

* fix pivot not updating on move

* fix pivot not becoming last active refernce

* fix redraw issues

* add: active pivot

* cargo fmt

* fix pivot showing up in default mode

* add: pivot pin

* fix: use pin icons

* cargo: cargo lock update?

* fix: use checkbox instead of Overlays

* refactor: add dot to path_tool

* add: active origins

* UI tweaks

* add: add all of the stuff for path tool

* remove: unused layer

* fix: pivot pinning and origin angle

* fix: pin only if moved in first place

* cargo: fmt

* fix: pivot use disabled method

* fix: remove redudant NoOp

* fix: 3 stuff

* fix: select from elsewhere

* fix: compass rose wobbling around

* add: move pivot on grab

* add: move pivot on nudge

* add: move pivot on Grab

* Code review and tooltips

* fixes

* fixes

* fixes

* fix: skipping artboard on bounds calculation

* fix: by default have origin

* Fix prior fix

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
mTvare
2025-07-08 14:54:54 +05:30
committed by GitHub
parent a1d93da532
commit 8a68683a93
27 changed files with 1864 additions and 1139 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,8 +134,8 @@ 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_YELLOW_DULL: &str = "#d7ba8b";
pub const COLOR_OVERLAY_GREEN: &str = "#63ce63";
pub const COLOR_OVERLAY_RED: &str = "#ef5454";
pub const COLOR_OVERLAY_GRAY: &str = "#cccccc";

View File

@@ -471,6 +471,8 @@ pub struct ReferencePointInput {
pub disabled: bool,
pub tooltip: String,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]

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;
@@ -1716,6 +1718,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()
}
@@ -2269,6 +2279,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

@@ -1308,7 +1308,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)],
@@ -1327,7 +1326,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(transform_nodes::transform::IDENTIFIER),
@@ -1395,7 +1393,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

@@ -191,7 +191,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_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, 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,12 +33,14 @@ pub enum OverlaysType {
HoverOutline,
SelectionOutline,
Pivot,
Origin,
Path,
Anchors,
Handles,
}
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
#[serde(default)]
pub struct OverlaysVisibilitySettings {
pub all: bool,
pub artboard_name: bool,
@@ -49,6 +51,7 @@ pub struct OverlaysVisibilitySettings {
pub hover_outline: bool,
pub selection_outline: bool,
pub pivot: bool,
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_YELLOW_DULL);
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();
@@ -731,11 +765,11 @@ impl OverlayContext {
// └──┴──┴──┴──┘
let pixels = [(0, 0), (2, 2)];
for &(x, y) in &pixels {
let index = (x + y * PATTERN_WIDTH as usize) * 4;
let index = (x + y * PATTERN_WIDTH) * 4;
data[index..index + 4].copy_from_slice(&color.to_rgba8_srgb());
}
let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&mut data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();
let image_data = web_sys::ImageData::new_with_u8_clamped_array_and_sh(wasm_bindgen::Clamped(&data), PATTERN_WIDTH as u32, PATTERN_HEIGHT as u32).unwrap();
pattern_context.put_image_data(&image_data, 0., 0.).unwrap();
let pattern = self.render_context.create_pattern_with_offscreen_canvas(&pattern_canvas, "repeat").unwrap().unwrap();

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,25 +9,32 @@ 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_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,26 @@ 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> {
use graphene_std::transform_nodes::transform::TranslateInput;
if let TaggedValue::DVec2(origin) = NodeGraphLayer::new(layer, network_interface).find_input("Transform", TranslateInput::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,184 @@
//! 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: PivotToolSource) -> WidgetHolder {
IconButton::new(if active { "PinActive" } else { "PinInactive" }, 24)
.tooltip(String::from(if active { "Unpin Custom Pivot" } else { "Pin Custom Pivot" }) + "\n\nUnless pinned, the pivot will return to its prior reference point when a new selection is made.")
.disabled(!enabled)
.on_update(move |_| match source {
PivotToolSource::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::TogglePivotPinned).into(),
PivotToolSource::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::TogglePivotPinned).into(),
})
.widget_holder()
}
pub fn pivot_reference_point_widget(disabled: bool, reference_point: ReferencePoint, source: PivotToolSource) -> WidgetHolder {
ReferencePointInput::new(reference_point)
.tooltip("Custom Pivot Reference Point\n\nPlaces the pivot at a corner, edge, or center of the selection bounds, unless it is dragged elsewhere.")
.disabled(disabled)
.on_update(move |pivot_input: &ReferencePointInput| match source {
PivotToolSource::Select => SelectToolMessage::SetPivot { position: pivot_input.value }.into(),
PivotToolSource::Path => PathToolMessage::SetPivot { position: pivot_input.value }.into(),
})
.widget_holder()
}
pub fn pivot_gizmo_type_widget(state: PivotGizmoState, source: PivotToolSource) -> Vec<WidgetHolder> {
let gizmo_type_entries = [PivotGizmoType::Pivot, PivotGizmoType::Average, PivotGizmoType::Active]
.iter()
.map(|gizmo_type| {
MenuListEntry::new(format!("{gizmo_type:?}")).label(gizmo_type.to_string()).on_commit({
let value = source.clone();
move |_| match value {
PivotToolSource::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::PivotGizmoType(*gizmo_type)).into(),
PivotToolSource::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::PivotGizmoType(*gizmo_type)).into(),
}
})
})
.collect();
vec![
CheckboxInput::new(!state.disabled)
.tooltip(
"Pivot Gizmo\n\
\n\
Enabled: the chosen gizmo type is shown and used to control rotation and scaling.\n\
Disabled: rotation and scaling occurs about the center of the selection bounds.",
)
.on_update(move |optional_input: &CheckboxInput| match source {
PivotToolSource::Select => SelectToolMessage::SelectOptions(SelectOptionsUpdate::TogglePivotGizmoType(optional_input.checked)).into(),
PivotToolSource::Path => PathToolMessage::UpdateOptions(PathOptionsUpdate::TogglePivotGizmoType(optional_input.checked)).into(),
})
.widget_holder(),
Separator::new(SeparatorType::Related).widget_holder(),
DropdownInput::new(vec![gizmo_type_entries])
.selected_index(Some(match state.gizmo_type {
PivotGizmoType::Pivot => 0,
PivotGizmoType::Average => 1,
PivotGizmoType::Active => 2,
}))
.tooltip(
"Pivot Gizmo Type\n\
\n\
Selects which gizmo type is shown and used as the center of rotation/scaling transformations.\n\
\n\
Custom Pivot: rotates and scales relative to the selection bounds, or elsewhere if dragged.\n\
Origin (Average Point): rotates and scales about the average point of all selected layer origins.\n\
Origin (Active Object): rotates and scales about the origin of the most recently selected layer.",
)
.disabled(state.disabled)
.widget_holder(),
]
}
#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum PivotToolSource {
Path,
#[default]
Select,
}
#[derive(PartialEq, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct PivotGizmo {
pub pivot: Pivot,
pub state: PivotGizmoState,
pub layer: Option<LayerNodeIdentifier>,
pub point: Option<ManipulatorPointId>,
}
impl PivotGizmo {
pub fn position(&self, document: &DocumentMessageHandler) -> DVec2 {
let network = &document.network_interface;
(!self.state.disabled)
.then_some({
match self.state.gizmo_type {
PivotGizmoType::Average => Some(network.selected_nodes().selected_visible_and_unlocked_layers_mean_average_origin(network)),
PivotGizmoType::Pivot => self.pivot.pivot,
PivotGizmoType::Active => self.layer.map(|layer| graph_modification_utils::get_viewport_origin(layer, network)),
}
})
.flatten()
.unwrap_or_else(|| self.pivot.transform_from_normalized.transform_point2(DVec2::splat(0.5)))
}
pub fn recalculate_transform(&mut self, document: &DocumentMessageHandler) -> DAffine2 {
self.pivot.recalculate_pivot(document);
self.pivot.transform_from_normalized
}
pub fn pin_active(&self) -> bool {
self.pivot.pinned && self.state.is_pivot_type()
}
pub fn pivot_disconnected(&self) -> bool {
self.pivot.old_pivot_position == ReferencePoint::None
}
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum PivotGizmoType {
// Pivot
#[default]
Pivot,
// Origin
Average,
Active,
// TODO: Add "Individual"
}
#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub struct PivotGizmoState {
pub disabled: bool,
pub gizmo_type: PivotGizmoType,
}
impl PivotGizmoState {
pub fn is_pivot_type(&self) -> bool {
self.gizmo_type == PivotGizmoType::Pivot || self.disabled
}
pub fn is_pivot(&self) -> bool {
self.gizmo_type == PivotGizmoType::Pivot && !self.disabled
}
}
impl fmt::Display for PivotGizmoType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PivotGizmoType::Pivot => write!(f, "Custom Pivot"),
PivotGizmoType::Average => write!(f, "Origin (Average Point)"),
PivotGizmoType::Active => write!(f, "Origin (Active Object)"),
// TODO: Add "Origin (Individual)"
}
}
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Pivot {
/// Pivot between (0,0) and (1,1)
normalized_pivot: DVec2,
/// Transform to get from normalized pivot to viewspace
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_point: 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 +188,62 @@ impl Default for Pivot {
transform_from_normalized: Default::default(),
pivot: Default::default(),
old_pivot_position: ReferencePoint::Center,
active: true,
last_non_none_reference_point: 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);
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
}
/// Recomputes the pivot position and transform.
fn recalculate_pivot(&mut self, document: &DocumentMessageHandler) {
if !self.active {
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 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
let transform = selected
.selected_visible_and_unlocked_layers(&document.network_interface)
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
.map(|layer| document.metadata().transform_to_viewport_with_first_transform_node_if_group(layer, &document.network_interface))
.unwrap_or_default();
let bounds = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter_map(|layer| {
document
.metadata()
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
})
.reduce(graphene_std::renderer::Quad::combine_bounds);
let [min, max] = bounds.unwrap_or([DVec2::ZERO, DVec2::ONE]);
self.transform_from_normalized = transform * DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
if self.old_pivot_position != ReferencePoint::None {
self.pivot = Some(self.transform_from_normalized.transform_point2(self.normalized_pivot));
}
}
pub fn recalculate_pivot_for_layer(&mut self, document: &DocumentMessageHandler, bounds: Option<[DVec2; 2]>) {
let selected = document.network_interface.selected_nodes();
if !selected.has_selected_nodes() {
self.normalized_pivot = DVec2::splat(0.5);
self.pivot = None;
return;
};
// 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 +254,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>,
center_of_transformation: 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 = center_of_transformation.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 = center_of_transformation.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::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_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 },
PivotGizmoType(PivotGizmoType),
TogglePivotGizmoType(bool),
TogglePivotPinned,
}
impl ToolMetadata for PathTool {
@@ -255,6 +263,20 @@ impl LayoutHolder for PathTool {
.selected_index(Some(self.options.path_overlay_mode as u32))
.widget_holder();
let [_checkbox, _dropdown] = {
let pivot_gizmo_type_widget = pivot_gizmo_type_widget(self.tool_data.pivot_gizmo.state, PivotToolSource::Path);
[pivot_gizmo_type_widget[0].clone(), pivot_gizmo_type_widget[2].clone()]
};
let has_something = !self.tool_data.saved_points_before_anchor_convert_smooth_sharp.is_empty();
let _pivot_reference = pivot_reference_point_widget(
has_something || !self.tool_data.pivot_gizmo.state.is_pivot(),
self.tool_data.pivot_gizmo.pivot.to_pivot_position(),
PivotToolSource::Path,
);
let _pin_pivot = pin_pivot_widget(self.tool_data.pivot_gizmo.pin_active(), false, PivotToolSource::Path);
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![
x_location,
@@ -268,8 +290,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 +323,29 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
self.options.path_editing_mode.segment_editing_mode = enabled;
responses.add(OverlaysMessage::Draw);
}
PathOptionsUpdate::PivotGizmoType(gizmo_type) => {
if !self.tool_data.pivot_gizmo.state.disabled {
self.tool_data.pivot_gizmo.state.gizmo_type = gizmo_type;
responses.add(ToolMessage::UpdateHints);
let pivot_gizmo = self.tool_data.pivot_gizmo();
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
responses.add(NodeGraphMessage::RunDocumentGraph);
self.send_layout(responses, LayoutTarget::ToolOptions);
}
}
PathOptionsUpdate::TogglePivotGizmoType(state) => {
self.tool_data.pivot_gizmo.state.disabled = !state;
responses.add(ToolMessage::UpdateHints);
responses.add(NodeGraphMessage::RunDocumentGraph);
self.send_layout(responses, LayoutTarget::ToolOptions);
}
PathOptionsUpdate::TogglePivotPinned => {
self.tool_data.pivot_gizmo.pivot.pinned = !self.tool_data.pivot_gizmo.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 +496,8 @@ struct PathToolData {
last_click_time: u64,
dragging_state: DraggingState,
angle: f64,
pivot_gizmo: PivotGizmo,
ordered_points: Vec<ManipulatorPointId>,
opposite_handle_position: Option<DVec2>,
last_clicked_point_was_selected: bool,
last_clicked_segment_was_selected: bool,
@@ -1315,6 +1370,16 @@ impl PathToolData {
}
}
}
fn pivot_gizmo(&self) -> PivotGizmo {
self.pivot_gizmo.clone()
}
fn sync_history(&mut self, points: &[ManipulatorPointId]) {
self.ordered_points.retain(|layer| points.contains(layer));
self.ordered_points.extend(points.iter().find(|&layer| !self.ordered_points.contains(layer)));
self.pivot_gizmo.point = self.ordered_points.last().copied()
}
}
impl Fsm for PathToolFsmState {
@@ -1327,6 +1392,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 once gizmos are implemented for path_tool
tool_data.pivot_gizmo.state.disabled = true;
match (self, event) {
(_, PathToolMessage::SelectionChanged) => {
// Set the newly targeted layers to visible
@@ -1343,6 +1412,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 +1782,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 +2274,18 @@ impl Fsm for PathToolFsmState {
responses.add(DocumentMessage::EndTransaction);
PathToolFsmState::Ready
}
(_, PathToolMessage::SetPivot { position }) => {
responses.add(DocumentMessage::StartTransaction);
tool_data.pivot_gizmo.pivot.last_non_none_reference_point = position;
let position: Option<DVec2> = position.into();
tool_data.pivot_gizmo.pivot.set_normalized_position(position.unwrap());
let pivot_gizmo = tool_data.pivot_gizmo();
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
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::{PivotGizmo, PivotGizmoType, PivotToolSource, pin_pivot_widget, pivot_gizmo_type_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),
PivotGizmoType(PivotGizmoType),
TogglePivotGizmoType(bool),
TogglePivotPinned,
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
@@ -95,6 +99,14 @@ pub enum SelectToolMessage {
SetPivot {
position: ReferencePoint,
},
SyncHistory,
ShiftSelectedNodes {
offset: DVec2,
},
PivotShift {
offset: Option<DVec2>,
flush: bool,
},
}
impl ToolMetadata for SelectTool {
@@ -122,14 +134,12 @@ impl SelectTool {
DropdownInput::new(vec![layer_selection_behavior_entries])
.selected_index(Some((self.tool_data.nested_selection_behavior == NestedSelectionBehavior::Deepest) as u32))
.tooltip("Choose if clicking nested layers directly selects the deepest, or selects the shallowest and deepens by double clicking")
.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)
.tooltip(
"Selection Mode\n\
\n\
Shallow Select: clicks initially select the least-nested layers and double clicks drill deeper into the folder hierarchy.\n\
Deep Select: clicks directly select the most-nested layers in the folder hierarchy.",
)
.widget_holder()
}
@@ -178,7 +188,7 @@ impl SelectTool {
fn boolean_widgets(&self, selected_count: usize) -> impl Iterator<Item = WidgetHolder> + use<> {
let list = <BooleanOperation as graphene_std::registry::ChoiceTypeStatic>::list();
list.into_iter().map(|i| i.into_iter()).flatten().map(move |(operation, info)| {
list.iter().flat_map(|i| i.iter()).map(move |(operation, info)| {
let mut tooltip = info.label.to_string();
if let Some(doc) = info.docstring.as_deref() {
tooltip.push_str("\n\n");
@@ -203,9 +213,29 @@ impl LayoutHolder for SelectTool {
// Select mode (Deep/Shallow)
widgets.push(self.deep_selection_widget());
// Pivot
// Pivot gizmo 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(pivot_gizmo_type_widget(self.tool_data.pivot_gizmo.state, PivotToolSource::Select));
if self.tool_data.pivot_gizmo.state.is_pivot_type() {
// Nine-position reference point 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.pivot_gizmo.state.is_pivot(),
self.tool_data.pivot_gizmo.pivot.to_pivot_position(),
PivotToolSource::Select,
));
// Pivot pin button
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
let pin_active = self.tool_data.pivot_gizmo.pin_active();
let pin_enabled = self.tool_data.pivot_gizmo.pivot.old_pivot_position == ReferencePoint::None && !self.tool_data.pivot_gizmo.state.disabled;
if pin_active || pin_enabled {
widgets.push(pin_pivot_widget(pin_active, pin_enabled, PivotToolSource::Select));
}
}
// Align
let disabled = self.tool_data.selected_layers_count < 2;
@@ -244,14 +274,43 @@ 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_reference_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::PivotGizmoType(gizmo_type) => {
if !self.tool_data.pivot_gizmo.state.disabled {
self.tool_data.pivot_gizmo.state.gizmo_type = *gizmo_type;
responses.add(ToolMessage::UpdateHints);
let pivot_gizmo = self.tool_data.pivot_gizmo();
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
responses.add(NodeGraphMessage::RunDocumentGraph);
redraw_reference_pivot = true;
}
}
SelectOptionsUpdate::TogglePivotGizmoType(state) => {
self.tool_data.pivot_gizmo.state.disabled = !state;
responses.add(ToolMessage::UpdateHints);
responses.add(NodeGraphMessage::RunDocumentGraph);
redraw_reference_pivot = true;
}
SelectOptionsUpdate::TogglePivotPinned => {
self.tool_data.pivot_gizmo.pivot.pinned = !self.tool_data.pivot_gizmo.pivot.pinned;
responses.add(ToolMessage::UpdateHints);
responses.add(NodeGraphMessage::RunDocumentGraph);
redraw_reference_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.pivot_gizmo.pivot.should_refresh_pivot_position() || self.tool_data.selected_layers_changed || redraw_reference_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 +382,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 +391,9 @@ struct SelectToolData {
bounding_box_manager: Option<BoundingBoxManager>,
snap_manager: SnapManager,
cursor: MouseCursorIcon,
pivot: Pivot,
pivot_gizmo: PivotGizmo,
pivot_gizmo_start: Option<DVec2>,
pivot_gizmo_shift: Option<DVec2>,
compass_rose: CompassRose,
line_center: DVec2,
skew_edge: EdgeBool,
@@ -497,6 +559,24 @@ impl SelectToolData {
responses.add(NodeGraphMessage::SendGraph);
self.layers_dragging = original;
}
fn state_from_pivot_gizmo(&self, mouse: DVec2) -> Option<SelectToolFsmState> {
match self.pivot_gizmo.state.gizmo_type {
PivotGizmoType::Pivot if self.pivot_gizmo.state.is_pivot() => self.pivot_gizmo.pivot.is_over(mouse).then_some(SelectToolFsmState::DraggingPivot),
_ => None,
}
}
fn pivot_gizmo(&self) -> PivotGizmo {
self.pivot_gizmo.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.pivot_gizmo.layer = self.ordered_layers.last().copied()
}
}
impl Fsm for SelectToolFsmState {
@@ -710,8 +790,63 @@ 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.pivot_gizmo.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.pivot_gizmo.state.gizmo_type == PivotGizmoType::Average {
let mut count = 0_usize;
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.pivot_gizmo.state.gizmo_type == PivotGizmoType::Active {
origin_angle = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.find(|&layer| Some(layer) == tool_data.pivot_gizmo.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.pivot_gizmo.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.pivot_gizmo.state.is_pivot() && overlay_context.visibility_settings.pivot() && has_layers;
tool_data.pivot_gizmo.pivot.recalculate_pivot(document);
let pivot = draw_pivot.then_some(tool_data.pivot_gizmo.pivot.pivot).flatten();
if let Some(pivot) = pivot {
let offset = tool_data
.pivot_gizmo_start
.map(|offset| tool_data.pivot_gizmo.pivot_disconnected().then_some(tool_data.drag_current - offset).unwrap_or_default())
.unwrap_or_default();
let shift = tool_data.pivot_gizmo_shift.unwrap_or_default();
overlay_context.pivot(pivot + offset + shift, angle);
}
// Update compass rose
if overlay_context.visibility_settings.compass_rose() {
@@ -837,6 +972,7 @@ impl Fsm for SelectToolFsmState {
(SelectionShapeType::Lasso, _) => overlay_context.polygon(polygon, None, fill_color),
}
}
self
}
(_, SelectToolMessage::EditLayer) => {
@@ -868,7 +1004,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 position = tool_data.pivot_gizmo().position(document);
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging, Some(position));
// 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 +1020,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_pivot_gizmo(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 +1051,14 @@ 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.pivot_gizmo_start = Some(tool_data.drag_current);
SelectToolFsmState::Dragging {
axis,
using_compass,
@@ -941,6 +1077,12 @@ 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.pivot_gizmo.pivot.pinned {
let position = tool_data.pivot_gizmo.pivot.last_non_none_reference_point;
responses.add(SelectToolMessage::SetPivot { position });
}
tool_data.layers_dragging.clear();
}
@@ -955,6 +1097,9 @@ impl Fsm for SelectToolFsmState {
tool_data.get_snap_candidates(document, input);
responses.add(DocumentMessage::StartTransaction);
tool_data.pivot_gizmo_start = Some(tool_data.drag_current);
SelectToolFsmState::Dragging {
axis: Axis::None,
using_compass: false,
@@ -1098,7 +1243,10 @@ 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.pivot_gizmo.pivot.set_viewport_position(snapped_mouse_position);
responses.add(NodeGraphMessage::RunDocumentGraph);
// Auto-panning
let messages = [
@@ -1143,7 +1291,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_pivot_gizmo(input.mouse.position).is_some() {
cursor = MouseCursorIcon::Move;
}
@@ -1283,20 +1431,32 @@ impl Fsm for SelectToolFsmState {
tool_data.snap_manager.cleanup(responses);
tool_data.select_single_layer = None;
if let Some(start) = tool_data.pivot_gizmo_start {
let offset = tool_data.pivot_gizmo.pivot_disconnected().then_some(tool_data.drag_current - start).unwrap_or_default();
if let Some(v) = tool_data.pivot_gizmo.pivot.pivot.as_mut() {
*v += offset;
}
}
tool_data.pivot_gizmo_start = None;
let pivot_gizmo = tool_data.pivot_gizmo();
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
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 pivot_gizmo = tool_data.pivot_gizmo();
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
responses.add(response);
tool_data.axis_align = false;
tool_data.snap_manager.cleanup(responses);
@@ -1432,8 +1592,48 @@ impl Fsm for SelectToolFsmState {
(_, SelectToolMessage::SetPivot { position }) => {
responses.add(DocumentMessage::StartTransaction);
tool_data.pivot_gizmo.pivot.last_non_none_reference_point = position;
tool_data.pivot_gizmo.pivot.pinned = false;
let pos: Option<DVec2> = position.into();
tool_data.pivot.set_normalized_position(pos.unwrap(), document, responses);
tool_data.pivot_gizmo.pivot.set_normalized_position(pos.unwrap());
let pivot_gizmo = tool_data.pivot_gizmo();
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
responses.add(NodeGraphMessage::RunDocumentGraph);
self
}
(_, SelectToolMessage::SyncHistory) => {
tool_data.sync_history(document);
self
}
(_, SelectToolMessage::ShiftSelectedNodes { offset }) => {
let offset = document.metadata().document_to_viewport.transform_vector2(offset);
if tool_data.pivot_gizmo.pivot_disconnected() {
if let Some(v) = tool_data.pivot_gizmo.pivot.pivot.as_mut() {
*v += offset;
}
let pivot_gizmo = tool_data.pivot_gizmo();
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
}
self
}
(_, SelectToolMessage::PivotShift { offset, flush }) => {
if flush {
tool_data.pivot_gizmo.pivot.pivot.as_mut().map(|v| *v += tool_data.pivot_gizmo_shift.take().unwrap_or_default());
let pivot_gizmo = tool_data.pivot_gizmo();
responses.add(TransformLayerMessage::SetPivotGizmo { pivot_gizmo });
return self;
}
if tool_data.pivot_gizmo.pivot_disconnected() {
tool_data.pivot_gizmo_shift = offset;
}
self
}
@@ -1658,6 +1858,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::PivotGizmo;
use glam::DVec2;
#[impl_message(Message, ToolMessage, TransformLayer)]
@@ -29,4 +30,5 @@ pub enum TransformLayerMessage {
TypeDecimalPoint,
TypeDigit { digit: u8 },
TypeNegate,
SetPivotGizmo { pivot_gizmo: PivotGizmo },
}

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::{PivotGizmo, PivotGizmoType};
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,
pivot_gizmo: PivotGizmo,
pivot: ViewportPosition,
path_bounds: Option<[DVec2; 2]>,
local_pivot: DocumentPosition,
local_mouse_start: DocumentPosition,
grab_target: DocumentPosition,
@@ -61,27 +65,64 @@ 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>,
gizmo: &mut PivotGizmo,
) -> (Option<(DVec2, DVec2)>, Option<[DVec2; 2]>) {
let average_position = || {
let mut point_count = 0_usize;
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])
}
});
gizmo.pivot.recalculate_pivot_for_layer(document, bounds);
let position = || {
(if !gizmo.state.disabled {
match gizmo.state.gizmo_type {
PivotGizmoType::Average => None,
PivotGizmoType::Active => gizmo.point.and_then(|p| get_location(&p)),
PivotGizmoType::Pivot => gizmo.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_position), 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_position, 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 +218,17 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
}
if !using_path_tool {
*selected.pivot = selected.mean_average_of_pivots();
self.pivot_gizmo.recalculate_transform(document);
*selected.pivot = self.pivot_gizmo.position(document);
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 = self.local_pivot;
}
// 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 +241,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.pivot_gizmo,
) {
*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,116 +276,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()
// 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);
responses.add(SelectToolMessage::PivotShift {
offset: Some(viewport_translate),
flush: false,
});
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);
}
}
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.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]);
}
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);
}
}
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]);
}
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]);
}
}
}
@@ -364,6 +411,8 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
responses.add(NodeGraphMessage::RunDocumentGraph);
}
responses.add(SelectToolMessage::PivotShift { offset: None, flush: true });
if final_transform {
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
}
@@ -487,6 +536,7 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
responses.add(ToolMessage::UpdateHints);
}
responses.add(SelectToolMessage::PivotShift { offset: None, flush: false });
responses.add(OverlaysMessage::RemoveProvider(TRANSFORM_GRS_OVERLAY_PROVIDER));
}
TransformLayerMessage::ConstrainX => {
@@ -694,6 +744,9 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
self.initial_transform,
)
}
TransformLayerMessage::SetPivotGizmo { pivot_gizmo } => {
self.pivot_gizmo = pivot_gizmo;
}
}
}

View File

@@ -7,13 +7,14 @@
export let value: string;
export let disabled = false;
export let tooltip: string | undefined = undefined;
function setValue(newValue: ReferencePoint) {
dispatch("value", newValue);
}
</script>
<div class="reference-point-input" class:disabled>
<div class="reference-point-input" class:disabled title={tooltip}>
<button on:click={() => setValue("TopLeft")} class="row-1 col-1" class:active={value === "TopLeft"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("TopCenter")} class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div /></button>
<button on:click={() => setValue("TopRight")} class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div /></button>

View File

@@ -1340,6 +1340,9 @@ export class ReferencePointInput extends WidgetProps {
value!: ReferencePoint;
disabled!: boolean;
@Transform(({ value }: { value: string }) => value || undefined)
tooltip!: string | undefined;
}
// WIDGET

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