1014 lines
38 KiB
Rust
1014 lines
38 KiB
Rust
#![allow(clippy::too_many_arguments)]
|
|
|
|
use super::tool_prelude::*;
|
|
use crate::application::generate_uuid;
|
|
use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
|
|
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
|
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
|
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
|
|
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
|
use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name;
|
|
use crate::messages::tool::common_functionality::pivot::Pivot;
|
|
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint};
|
|
use crate::messages::tool::common_functionality::transformation_cage::*;
|
|
|
|
use graph_craft::document::{NodeId, NodeNetwork};
|
|
use graphene_core::renderer::Quad;
|
|
|
|
use std::fmt;
|
|
|
|
#[derive(Default)]
|
|
pub struct SelectTool {
|
|
fsm_state: SelectToolFsmState,
|
|
tool_data: SelectToolData,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Default)]
|
|
pub struct SelectOptions {
|
|
nested_selection_behavior: NestedSelectionBehavior,
|
|
}
|
|
|
|
#[remain::sorted]
|
|
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)]
|
|
pub enum SelectOptionsUpdate {
|
|
NestedSelectionBehavior(NestedSelectionBehavior),
|
|
}
|
|
|
|
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize, specta::Type)]
|
|
pub enum NestedSelectionBehavior {
|
|
#[default]
|
|
Deepest,
|
|
Shallowest,
|
|
}
|
|
|
|
impl fmt::Display for NestedSelectionBehavior {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
NestedSelectionBehavior::Deepest => write!(f, "Deep Select"),
|
|
NestedSelectionBehavior::Shallowest => write!(f, "Shallow Select"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[remain::sorted]
|
|
#[impl_message(Message, ToolMessage, Select)]
|
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
|
pub enum SelectToolMessage {
|
|
// Standard messages
|
|
#[remain::unsorted]
|
|
Abort,
|
|
#[remain::unsorted]
|
|
Overlays(OverlayContext),
|
|
|
|
// Tool-specific messages
|
|
DragStart {
|
|
add_to_selection: Key,
|
|
select_deepest: Key,
|
|
},
|
|
DragStop {
|
|
remove_from_selection: Key,
|
|
},
|
|
EditLayer,
|
|
Enter,
|
|
PointerMove {
|
|
axis_align: Key,
|
|
snap_angle: Key,
|
|
center: Key,
|
|
duplicate: Key,
|
|
},
|
|
SelectOptions(SelectOptionsUpdate),
|
|
SetPivot {
|
|
position: PivotPosition,
|
|
},
|
|
}
|
|
|
|
impl ToolMetadata for SelectTool {
|
|
fn icon_name(&self) -> String {
|
|
"GeneralSelectTool".into()
|
|
}
|
|
fn tooltip(&self) -> String {
|
|
"Select Tool".into()
|
|
}
|
|
fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType {
|
|
ToolType::Select
|
|
}
|
|
}
|
|
|
|
impl SelectTool {
|
|
fn deep_selection_widget(&self) -> WidgetHolder {
|
|
let layer_selection_behavior_entries = [NestedSelectionBehavior::Deepest, NestedSelectionBehavior::Shallowest]
|
|
.iter()
|
|
.map(|mode| {
|
|
MenuListEntry::new(mode.to_string())
|
|
.value(mode.to_string())
|
|
.on_update(move |_| SelectToolMessage::SelectOptions(SelectOptionsUpdate::NestedSelectionBehavior(*mode)).into())
|
|
})
|
|
.collect();
|
|
|
|
DropdownInput::new(vec![layer_selection_behavior_entries])
|
|
.selected_index(Some((self.tool_data.nested_selection_behavior == NestedSelectionBehavior::Shallowest) 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_widget(&self, disabled: bool) -> WidgetHolder {
|
|
PivotInput::new(self.tool_data.pivot.to_pivot_position())
|
|
.on_update(|pivot_input: &PivotInput| SelectToolMessage::SetPivot { position: pivot_input.position }.into())
|
|
.disabled(disabled)
|
|
.widget_holder()
|
|
}
|
|
|
|
fn alignment_widgets(&self, disabled: bool) -> impl Iterator<Item = WidgetHolder> {
|
|
[AlignAxis::X, AlignAxis::Y]
|
|
.into_iter()
|
|
.flat_map(|axis| [(axis, AlignAggregate::Min), (axis, AlignAggregate::Center), (axis, AlignAggregate::Max)])
|
|
.map(move |(axis, aggregate)| {
|
|
let (icon, tooltip) = match (axis, aggregate) {
|
|
(AlignAxis::X, AlignAggregate::Min) => ("AlignLeft", "Align Left"),
|
|
(AlignAxis::X, AlignAggregate::Center) => ("AlignHorizontalCenter", "Align Horizontal Center"),
|
|
(AlignAxis::X, AlignAggregate::Max) => ("AlignRight", "Align Right"),
|
|
(AlignAxis::Y, AlignAggregate::Min) => ("AlignTop", "Align Top"),
|
|
(AlignAxis::Y, AlignAggregate::Center) => ("AlignVerticalCenter", "Align Vertical Center"),
|
|
(AlignAxis::Y, AlignAggregate::Max) => ("AlignBottom", "Align Bottom"),
|
|
};
|
|
IconButton::new(icon, 24)
|
|
.tooltip(tooltip)
|
|
.on_update(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate }.into())
|
|
.disabled(disabled)
|
|
.widget_holder()
|
|
})
|
|
}
|
|
|
|
fn flip_widgets(&self, disabled: bool) -> impl Iterator<Item = WidgetHolder> {
|
|
[(FlipAxis::X, "Horizontal"), (FlipAxis::Y, "Vertical")].into_iter().map(move |(flip_axis, name)| {
|
|
IconButton::new("Flip".to_string() + name, 24)
|
|
.tooltip("Flip ".to_string() + name)
|
|
.on_update(move |_| DocumentMessage::FlipSelectedLayers { flip_axis }.into())
|
|
.disabled(disabled)
|
|
.widget_holder()
|
|
})
|
|
}
|
|
|
|
fn boolean_widgets(&self) -> impl Iterator<Item = WidgetHolder> {
|
|
["Union", "Subtract Front", "Subtract Back", "Intersect", "Difference"].into_iter().map(|name| {
|
|
IconButton::new(format!("Boolean{}", name.replace(' ', "")), 24)
|
|
.tooltip(format!("Boolean {name} (coming soon)"))
|
|
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into())
|
|
.widget_holder()
|
|
})
|
|
}
|
|
}
|
|
|
|
impl LayoutHolder for SelectTool {
|
|
fn layout(&self) -> Layout {
|
|
let mut widgets = Vec::new();
|
|
|
|
// Select mode (Deep/Shallow)
|
|
widgets.push(self.deep_selection_widget());
|
|
|
|
// Pivot
|
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
|
widgets.push(self.pivot_widget(self.tool_data.selected_layers_count == 0));
|
|
|
|
// Align
|
|
let disabled = self.tool_data.selected_layers_count < 2;
|
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
|
widgets.extend(self.alignment_widgets(disabled));
|
|
widgets.push(PopoverButton::new("Align", "Coming soon").disabled(disabled).widget_holder());
|
|
|
|
// Flip
|
|
let disabled = self.tool_data.selected_layers_count == 0;
|
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
|
widgets.extend(self.flip_widgets(disabled));
|
|
|
|
// Boolean
|
|
if self.tool_data.selected_layers_count >= 2 {
|
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
|
widgets.extend(self.boolean_widgets());
|
|
}
|
|
|
|
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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 {
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
fn actions(&self) -> ActionList {
|
|
use SelectToolFsmState::*;
|
|
|
|
match self.fsm_state {
|
|
Ready => actions!(SelectToolMessageDiscriminant;
|
|
DragStart,
|
|
PointerMove,
|
|
Abort,
|
|
EditLayer,
|
|
Enter,
|
|
),
|
|
_ => actions!(SelectToolMessageDiscriminant;
|
|
DragStop,
|
|
PointerMove,
|
|
Abort,
|
|
EditLayer,
|
|
Enter,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToolTransition for SelectTool {
|
|
fn event_to_message_map(&self) -> EventToMessageMap {
|
|
EventToMessageMap {
|
|
tool_abort: Some(SelectToolMessage::Abort.into()),
|
|
overlay_provider: Some(|overlay_context| SelectToolMessage::Overlays(overlay_context).into()),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
|
enum SelectToolFsmState {
|
|
#[default]
|
|
Ready,
|
|
Dragging,
|
|
DrawingBox,
|
|
ResizingBounds,
|
|
RotatingBounds,
|
|
DraggingPivot,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
struct SelectToolData {
|
|
drag_start: ViewportPosition,
|
|
drag_current: ViewportPosition,
|
|
layers_dragging: Vec<LayerNodeIdentifier>,
|
|
layer_selected_on_start: Option<LayerNodeIdentifier>,
|
|
select_single_layer: Option<LayerNodeIdentifier>,
|
|
has_dragged: bool,
|
|
non_duplicated_layers: Option<Vec<LayerNodeIdentifier>>,
|
|
bounding_box_manager: Option<BoundingBoxManager>,
|
|
snap_manager: SnapManager,
|
|
cursor: MouseCursorIcon,
|
|
pivot: Pivot,
|
|
nested_selection_behavior: NestedSelectionBehavior,
|
|
selected_layers_count: usize,
|
|
selected_layers_changed: bool,
|
|
snap_candidates: Vec<SnapCandidatePoint>,
|
|
}
|
|
|
|
impl SelectToolData {
|
|
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) {
|
|
self.snap_candidates.clear();
|
|
for &layer in &self.layers_dragging {
|
|
if (self.snap_candidates.len() as f64) < document.snapping_state.tolerance {
|
|
snapping::get_layer_snap_points(layer, &SnapData::new(document, input), &mut self.snap_candidates);
|
|
}
|
|
if let Some(bounds) = document.metadata.bounding_box_with_transform(layer, DAffine2::IDENTITY) {
|
|
let quad = document.metadata.transform_to_document(layer) * Quad::from_box(bounds);
|
|
snapping::get_bbox_points(quad, &mut self.snap_candidates, snapping::BBoxSnapValues::BOUNDING_BOX, document);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn selection_quad(&self) -> Quad {
|
|
let bbox = self.selection_box();
|
|
Quad::from_box(bbox)
|
|
}
|
|
|
|
fn selection_box(&self) -> [DVec2; 2] {
|
|
if self.drag_current == self.drag_start {
|
|
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
|
[self.drag_start - tolerance, self.drag_start + tolerance]
|
|
} else {
|
|
[self.drag_start, self.drag_current]
|
|
}
|
|
}
|
|
|
|
/// Duplicates the currently dragging layers. Called when Alt is pressed and the layers have not yet been duplicated.
|
|
fn start_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
|
self.non_duplicated_layers = Some(self.layers_dragging.clone());
|
|
let mut new_dragging = Vec::new();
|
|
for layer_ancestors in document.metadata().shallowest_unique_layers(self.layers_dragging.iter().copied().rev()) {
|
|
let Some(layer) = layer_ancestors.last().copied() else { continue };
|
|
let Some(parent) = layer.parent(&document.metadata) else { continue };
|
|
|
|
// Copy the layer
|
|
let node = layer.to_node();
|
|
let Some(node) = document.network().nodes.get(&node).and_then(|node| node.inputs.first()).and_then(|input| input.as_node()) else {
|
|
continue;
|
|
};
|
|
|
|
let nodes = NodeGraphMessageHandler::copy_nodes(
|
|
document.network(),
|
|
&document
|
|
.network()
|
|
.upstream_flow_back_from_nodes(vec![node], false)
|
|
.enumerate()
|
|
.map(|(index, (_, node_id))| (node_id, NodeId(index as u64)))
|
|
.collect(),
|
|
)
|
|
.collect();
|
|
|
|
// Moves the layer back to its starting position.
|
|
responses.add(GraphOperationMessage::TransformChange {
|
|
layer,
|
|
transform: DAffine2::from_translation(self.drag_start - self.drag_current),
|
|
transform_in: TransformIn::Viewport,
|
|
skip_rerender: true,
|
|
});
|
|
|
|
let id = NodeId(generate_uuid());
|
|
let insert_index = -1;
|
|
let layer = LayerNodeIdentifier::new_unchecked(id);
|
|
responses.add(GraphOperationMessage::NewCustomLayer { id, nodes, parent, insert_index });
|
|
new_dragging.push(layer);
|
|
}
|
|
let nodes = new_dragging.iter().map(|layer| layer.to_node()).collect();
|
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
|
|
self.layers_dragging = new_dragging;
|
|
}
|
|
|
|
/// Removes the duplicated layers. Called when Alt is released and the layers have previously been duplicated.
|
|
fn stop_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
|
let Some(original) = self.non_duplicated_layers.take() else {
|
|
return;
|
|
};
|
|
|
|
// Delete the duplicated layers
|
|
for layer_ancestors in document.metadata().shallowest_unique_layers(self.layers_dragging.iter().copied()) {
|
|
responses.add(GraphOperationMessage::DeleteLayer {
|
|
id: layer_ancestors.last().unwrap().to_node(),
|
|
});
|
|
}
|
|
|
|
for &layer in &original {
|
|
responses.add(GraphOperationMessage::TransformChange {
|
|
layer,
|
|
transform: DAffine2::from_translation(self.drag_current - self.drag_start),
|
|
transform_in: TransformIn::Viewport,
|
|
skip_rerender: true,
|
|
});
|
|
}
|
|
let nodes = original.iter().map(|layer| layer.to_node()).collect();
|
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
|
|
self.layers_dragging = original;
|
|
}
|
|
}
|
|
|
|
impl Fsm for SelectToolFsmState {
|
|
type ToolData = SelectToolData;
|
|
type ToolOptions = ();
|
|
|
|
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque<Message>) -> Self {
|
|
let ToolActionHandlerData { document, input, .. } = tool_action_data;
|
|
|
|
let ToolMessage::Select(event) = event else {
|
|
return self;
|
|
};
|
|
match (self, event) {
|
|
(_, SelectToolMessage::Overlays(mut overlay_context)) => {
|
|
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
|
|
|
|
let selected_layers_count = document.selected_nodes.selected_layers(document.metadata()).count();
|
|
tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count;
|
|
tool_data.selected_layers_count = selected_layers_count;
|
|
|
|
// Outline selected layers
|
|
for layer in document.selected_nodes.selected_visible_layers(document.network(), document.metadata()) {
|
|
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
|
|
}
|
|
|
|
// Get the layer the user is hovering over
|
|
let click = document.click(input.mouse.position, &document.network);
|
|
let not_selected_click = click.filter(|&hovered_layer| !document.selected_nodes.selected_layers_contains(hovered_layer, document.metadata()));
|
|
if let Some(layer) = not_selected_click {
|
|
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
|
|
}
|
|
|
|
// Update bounds
|
|
let transform = document
|
|
.selected_nodes
|
|
.selected_visible_layers(document.network(), document.metadata())
|
|
.next()
|
|
.map(|layer| document.metadata().transform_to_viewport(layer));
|
|
let transform = transform.unwrap_or(DAffine2::IDENTITY);
|
|
let bounds = document
|
|
.selected_nodes
|
|
.selected_visible_layers(document.network(), document.metadata())
|
|
.filter_map(|layer| {
|
|
document
|
|
.metadata()
|
|
.bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer))
|
|
})
|
|
.reduce(graphene_core::renderer::Quad::combine_bounds);
|
|
if let Some(bounds) = bounds {
|
|
let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default());
|
|
|
|
bounding_box_manager.bounds = bounds;
|
|
bounding_box_manager.transform = transform;
|
|
|
|
bounding_box_manager.render_overlays(&mut overlay_context);
|
|
} else {
|
|
tool_data.bounding_box_manager.take();
|
|
}
|
|
|
|
// Update pivot
|
|
tool_data.pivot.update_pivot(document, &mut overlay_context);
|
|
|
|
// Update dragging box
|
|
if self == Self::DrawingBox {
|
|
overlay_context.quad(Quad::from_box([tool_data.drag_start, tool_data.drag_current]));
|
|
}
|
|
|
|
self
|
|
}
|
|
(_, SelectToolMessage::EditLayer) => {
|
|
// Edit the clicked layer
|
|
if let Some(intersect) = document.click(input.mouse.position, &document.network) {
|
|
match tool_data.nested_selection_behavior {
|
|
NestedSelectionBehavior::Shallowest => edit_layer_shallowest_manipulation(document, intersect, responses),
|
|
NestedSelectionBehavior::Deepest => edit_layer_deepest_manipulation(intersect, &document.network, responses),
|
|
}
|
|
}
|
|
|
|
self
|
|
}
|
|
(SelectToolFsmState::Ready, SelectToolMessage::DragStart { add_to_selection, select_deepest }) => {
|
|
tool_data.drag_start = input.mouse.position;
|
|
tool_data.drag_current = input.mouse.position;
|
|
|
|
let dragging_bounds = tool_data.bounding_box_manager.as_mut().and_then(|bounding_box| {
|
|
let edges = bounding_box.check_selected_edges(input.mouse.position);
|
|
|
|
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
|
|
let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
|
|
bounding_box.opposite_pivot = selected_edges.calculate_pivot();
|
|
selected_edges
|
|
});
|
|
|
|
edges
|
|
});
|
|
|
|
let rotating_bounds = tool_data
|
|
.bounding_box_manager
|
|
.as_ref()
|
|
.map(|bounding_box| bounding_box.check_rotate(input.mouse.position))
|
|
.unwrap_or_default();
|
|
|
|
let mut selected: Vec<_> = document.selected_nodes.selected_visible_layers(document.network(), document.metadata()).collect();
|
|
let intersection = document.click(input.mouse.position, &document.network);
|
|
|
|
// 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.
|
|
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
|
|
// If the user clicks on new shape, make that layer their new selection.
|
|
// Otherwise enter the box select mode
|
|
|
|
let state =
|
|
// Dragging the pivot
|
|
if tool_data.pivot.is_over(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
|
|
}
|
|
// Dragging one (or two, forming a corner) of the transform cage bounding box edges
|
|
else if let Some(_selected_edges) = dragging_bounds {
|
|
responses.add(DocumentMessage::StartTransaction);
|
|
|
|
tool_data.layers_dragging = selected;
|
|
|
|
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
|
bounds.original_bound_transform = bounds.transform;
|
|
|
|
tool_data.layers_dragging.retain(|layer| document.network.nodes.contains_key(&layer.to_node()));
|
|
let mut selected = Selected::new(
|
|
&mut bounds.original_transforms,
|
|
&mut bounds.center_of_transformation,
|
|
&tool_data.layers_dragging,
|
|
responses,
|
|
&document.network,
|
|
&document.metadata,
|
|
None,
|
|
&ToolType::Select,
|
|
);
|
|
bounds.center_of_transformation = selected.mean_average_of_pivots();
|
|
}
|
|
tool_data.get_snap_candidates(document, input);
|
|
|
|
SelectToolFsmState::ResizingBounds
|
|
}
|
|
// Dragging near the transform cage bounding box to rotate it
|
|
else if rotating_bounds {
|
|
responses.add(DocumentMessage::StartTransaction);
|
|
|
|
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
|
tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node()));
|
|
let mut selected = Selected::new(
|
|
&mut bounds.original_transforms,
|
|
&mut bounds.center_of_transformation,
|
|
&selected,
|
|
responses,
|
|
&document.network,
|
|
&document.metadata,
|
|
None,
|
|
&ToolType::Select,
|
|
);
|
|
|
|
bounds.center_of_transformation = selected.mean_average_of_pivots();
|
|
}
|
|
|
|
tool_data.layers_dragging = selected;
|
|
|
|
SelectToolFsmState::RotatingBounds
|
|
}
|
|
// Dragging the selected layers around to transform them
|
|
else if intersection.is_some_and(|intersection| selected.iter().any(|selected_layer| intersection.starts_with(*selected_layer, document.metadata()))) {
|
|
responses.add(DocumentMessage::StartTransaction);
|
|
|
|
if tool_data.nested_selection_behavior == NestedSelectionBehavior::Deepest {
|
|
tool_data.select_single_layer = intersection;
|
|
} else {
|
|
tool_data.select_single_layer = intersection.and_then(|intersection| intersection.ancestors(&document.metadata).find(|ancestor| selected.contains(ancestor)));
|
|
}
|
|
|
|
tool_data.layers_dragging = selected;
|
|
|
|
tool_data.get_snap_candidates(document, input);
|
|
|
|
SelectToolFsmState::Dragging
|
|
}
|
|
// Dragging a selection box
|
|
else {
|
|
tool_data.layers_dragging = selected;
|
|
|
|
if !input.keyboard.key(add_to_selection) {
|
|
responses.add(DocumentMessage::DeselectAllLayers);
|
|
tool_data.layers_dragging.clear();
|
|
}
|
|
|
|
if let Some(intersection) = intersection {
|
|
responses.add(DocumentMessage::StartTransaction);
|
|
|
|
tool_data.layer_selected_on_start = Some(intersection);
|
|
selected = vec![intersection];
|
|
|
|
match tool_data.nested_selection_behavior {
|
|
NestedSelectionBehavior::Shallowest if !input.keyboard.key(select_deepest) => drag_shallowest_manipulation(responses, selected, tool_data, document),
|
|
_ => drag_deepest_manipulation(responses, selected, tool_data),
|
|
}
|
|
tool_data.get_snap_candidates(document, input);
|
|
SelectToolFsmState::Dragging
|
|
} else {
|
|
// Deselect all layers if using shallowest selection behavior
|
|
// Necessary since for shallowest mode, we need to know the current selected layers to determine the next
|
|
if tool_data.nested_selection_behavior == NestedSelectionBehavior::Shallowest {
|
|
responses.add(DocumentMessage::DeselectAllLayers);
|
|
tool_data.layers_dragging.clear();
|
|
}
|
|
SelectToolFsmState::DrawingBox
|
|
}
|
|
};
|
|
tool_data.non_duplicated_layers = None;
|
|
|
|
state
|
|
}
|
|
(SelectToolFsmState::Dragging, SelectToolMessage::PointerMove { axis_align, duplicate, .. }) => {
|
|
tool_data.has_dragged = true;
|
|
|
|
if input.keyboard.key(duplicate) && tool_data.non_duplicated_layers.is_none() {
|
|
tool_data.start_duplicates(document, responses);
|
|
} else if !input.keyboard.key(duplicate) && tool_data.non_duplicated_layers.is_some() {
|
|
tool_data.stop_duplicates(document, responses);
|
|
}
|
|
|
|
let axis_align = input.keyboard.key(axis_align);
|
|
let mouse_position = axis_align_drag(axis_align, input.mouse.position, tool_data.drag_start);
|
|
let total_mouse_delta_document = document.metadata.document_to_viewport.inverse().transform_vector2(mouse_position - tool_data.drag_start);
|
|
|
|
// Ignore the non duplicated layers if the current layers have not spawned yet.
|
|
let layers_exist = tool_data.layers_dragging.iter().all(|&layer| document.metadata().click_target(layer).is_some());
|
|
let ignore = tool_data.non_duplicated_layers.as_ref().filter(|_| !layers_exist).unwrap_or(&tool_data.layers_dragging);
|
|
|
|
let snap_data = SnapData::ignore(document, input, ignore);
|
|
let mouse_delta_document = document.metadata.document_to_viewport.inverse().transform_vector2(mouse_position - tool_data.drag_current);
|
|
let mut offset = mouse_delta_document;
|
|
let mut best_snap = SnappedPoint::infinite_snap(document.metadata.document_to_viewport.inverse().transform_point2(mouse_position));
|
|
|
|
for point in &mut tool_data.snap_candidates {
|
|
point.document_point += total_mouse_delta_document;
|
|
let snapped = if axis_align {
|
|
let constraint = SnapConstraint::Line {
|
|
origin: point.document_point,
|
|
direction: total_mouse_delta_document.normalize(),
|
|
};
|
|
tool_data.snap_manager.constrained_snap(&snap_data, &point, constraint, None)
|
|
} else {
|
|
tool_data.snap_manager.free_snap(&snap_data, &point, None, false)
|
|
};
|
|
if best_snap.other_snap_better(&snapped) {
|
|
offset = snapped.snapped_point_document - point.document_point + mouse_delta_document;
|
|
best_snap = snapped;
|
|
}
|
|
point.document_point -= total_mouse_delta_document;
|
|
}
|
|
tool_data.snap_manager.update_indicator(best_snap);
|
|
|
|
let mouse_delta = document.metadata.document_to_viewport.transform_vector2(offset);
|
|
|
|
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
|
|
for layer_ancestors in document.metadata().shallowest_unique_layers(tool_data.layers_dragging.iter().copied()) {
|
|
responses.add_front(GraphOperationMessage::TransformChange {
|
|
layer: *layer_ancestors.last().unwrap(),
|
|
transform: DAffine2::from_translation(mouse_delta),
|
|
transform_in: TransformIn::Viewport,
|
|
skip_rerender: false,
|
|
});
|
|
}
|
|
tool_data.drag_current += mouse_delta;
|
|
|
|
SelectToolFsmState::Dragging
|
|
}
|
|
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove { axis_align, center, .. }) => {
|
|
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
|
if let Some(movement) = &mut bounds.selected_edges {
|
|
let (center, constrain) = (input.keyboard.key(center), input.keyboard.key(axis_align));
|
|
|
|
let center = center.then_some(bounds.center_of_transformation);
|
|
let snap = Some(SizeSnapData {
|
|
manager: &mut tool_data.snap_manager,
|
|
points: &mut tool_data.snap_candidates,
|
|
snap_data: SnapData::ignore(document, input, &tool_data.layers_dragging),
|
|
});
|
|
let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap);
|
|
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
|
|
|
|
let pivot_transform = DAffine2::from_translation(pivot);
|
|
let transformation = pivot_transform * delta * pivot_transform.inverse();
|
|
|
|
tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node()));
|
|
let selected = &tool_data.layers_dragging;
|
|
let mut selected = Selected::new(
|
|
&mut bounds.original_transforms,
|
|
&mut pivot,
|
|
selected,
|
|
responses,
|
|
&document.network,
|
|
&document.metadata,
|
|
None,
|
|
&ToolType::Select,
|
|
);
|
|
|
|
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse());
|
|
}
|
|
}
|
|
SelectToolFsmState::ResizingBounds
|
|
}
|
|
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove { snap_angle, .. }) => {
|
|
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
|
let angle = {
|
|
let start_offset = tool_data.drag_start - bounds.center_of_transformation;
|
|
let end_offset = input.mouse.position - bounds.center_of_transformation;
|
|
|
|
start_offset.angle_between(end_offset)
|
|
};
|
|
|
|
let snapped_angle = if input.keyboard.key(snap_angle) {
|
|
let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
|
|
(angle / snap_resolution).round() * snap_resolution
|
|
} else {
|
|
angle
|
|
};
|
|
|
|
let delta = DAffine2::from_angle(snapped_angle);
|
|
|
|
tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node()));
|
|
let mut selected = Selected::new(
|
|
&mut bounds.original_transforms,
|
|
&mut bounds.center_of_transformation,
|
|
&tool_data.layers_dragging,
|
|
responses,
|
|
&document.network,
|
|
&document.metadata,
|
|
None,
|
|
&ToolType::Select,
|
|
);
|
|
|
|
selected.update_transforms(delta);
|
|
}
|
|
|
|
SelectToolFsmState::RotatingBounds
|
|
}
|
|
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove { .. }) => {
|
|
let mouse_position = input.mouse.position;
|
|
let snapped_mouse_position = mouse_position; //tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
|
tool_data.pivot.set_viewport_position(snapped_mouse_position, document, responses);
|
|
|
|
SelectToolFsmState::DraggingPivot
|
|
}
|
|
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove { .. }) => {
|
|
tool_data.drag_current = input.mouse.position;
|
|
responses.add(OverlaysMessage::Draw);
|
|
|
|
SelectToolFsmState::DrawingBox
|
|
}
|
|
(SelectToolFsmState::Ready, SelectToolMessage::PointerMove { .. }) => {
|
|
let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
|
|
|
|
// Dragging the pivot overrules the other operations
|
|
if tool_data.pivot.is_over(input.mouse.position) {
|
|
cursor = MouseCursorIcon::Move;
|
|
}
|
|
|
|
// Generate the hover outline
|
|
responses.add(OverlaysMessage::Draw);
|
|
|
|
if tool_data.cursor != cursor {
|
|
tool_data.cursor = cursor;
|
|
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
|
|
}
|
|
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(SelectToolFsmState::Dragging, SelectToolMessage::Enter) => {
|
|
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
|
|
true => DocumentMessage::Undo,
|
|
false => DocumentMessage::CommitTransaction,
|
|
};
|
|
tool_data.snap_manager.cleanup(responses);
|
|
responses.add_front(response);
|
|
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(SelectToolFsmState::Dragging, SelectToolMessage::DragStop { remove_from_selection }) => {
|
|
// Deselect layer if not snap dragging
|
|
if !tool_data.has_dragged && input.keyboard.key(remove_from_selection) && tool_data.layer_selected_on_start.is_none() {
|
|
let quad = tool_data.selection_quad();
|
|
let intersection = document.intersect_quad(quad, &document.network);
|
|
|
|
if let Some(path) = intersection.last() {
|
|
let replacement_selected_layers: Vec<_> = document
|
|
.selected_nodes
|
|
.selected_layers(document.metadata())
|
|
.filter(|&layer| !path.starts_with(layer, document.metadata()))
|
|
.collect();
|
|
|
|
tool_data.layers_dragging.clear();
|
|
tool_data.layers_dragging.extend(replacement_selected_layers.iter());
|
|
|
|
responses.add(NodeGraphMessage::SelectedNodesSet {
|
|
nodes: replacement_selected_layers.iter().map(|layer| layer.to_node()).collect(),
|
|
});
|
|
}
|
|
} else if let Some(selecting_layer) = tool_data.select_single_layer.take() {
|
|
if !tool_data.has_dragged {
|
|
responses.add(NodeGraphMessage::SelectedNodesSet {
|
|
nodes: vec![selecting_layer.to_node()],
|
|
});
|
|
}
|
|
}
|
|
|
|
tool_data.has_dragged = false;
|
|
tool_data.layer_selected_on_start = None;
|
|
|
|
responses.add(DocumentMessage::CommitTransaction);
|
|
tool_data.snap_manager.cleanup(responses);
|
|
tool_data.select_single_layer = None;
|
|
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(SelectToolFsmState::ResizingBounds, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => {
|
|
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
|
|
true => DocumentMessage::Undo,
|
|
false => DocumentMessage::CommitTransaction,
|
|
};
|
|
responses.add(response);
|
|
|
|
tool_data.snap_manager.cleanup(responses);
|
|
|
|
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
|
bounds.original_transforms.clear();
|
|
}
|
|
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(SelectToolFsmState::RotatingBounds, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => {
|
|
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
|
|
true => DocumentMessage::Undo,
|
|
false => DocumentMessage::CommitTransaction,
|
|
};
|
|
responses.add(response);
|
|
|
|
if let Some(bounds) = &mut tool_data.bounding_box_manager {
|
|
bounds.original_transforms.clear();
|
|
}
|
|
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(SelectToolFsmState::DraggingPivot, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => {
|
|
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
|
|
true => DocumentMessage::Undo,
|
|
false => DocumentMessage::CommitTransaction,
|
|
};
|
|
responses.add(response);
|
|
|
|
tool_data.snap_manager.cleanup(responses);
|
|
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(SelectToolFsmState::DrawingBox, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => {
|
|
let quad = tool_data.selection_quad();
|
|
let new_selected: HashSet<_> = document.intersect_quad(quad, &document.network).collect();
|
|
let current_selected: HashSet<_> = document.selected_nodes.selected_layers(document.metadata()).collect();
|
|
if new_selected != current_selected {
|
|
tool_data.layers_dragging = new_selected.into_iter().collect();
|
|
responses.add(DocumentMessage::StartTransaction);
|
|
responses.add(NodeGraphMessage::SelectedNodesSet {
|
|
nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(),
|
|
});
|
|
}
|
|
responses.add(OverlaysMessage::Draw);
|
|
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(SelectToolFsmState::Ready, SelectToolMessage::Enter) => {
|
|
let mut selected_layers = document.selected_nodes.selected_layers(document.metadata());
|
|
|
|
if let Some(layer) = selected_layers.next() {
|
|
// Check that only one layer is selected
|
|
if selected_layers.next().is_none() && is_layer_fed_by_node_of_name(layer, &document.network, "Text") {
|
|
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
|
|
responses.add(TextToolMessage::EditSelected);
|
|
}
|
|
}
|
|
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(SelectToolFsmState::Dragging, SelectToolMessage::Abort) => {
|
|
tool_data.snap_manager.cleanup(responses);
|
|
responses.add(DocumentMessage::Undo);
|
|
responses.add(OverlaysMessage::Draw);
|
|
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(_, SelectToolMessage::Abort) => {
|
|
tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node()));
|
|
if let Some(mut bounding_box_overlays) = tool_data.bounding_box_manager.take() {
|
|
let mut selected = Selected::new(
|
|
&mut bounding_box_overlays.original_transforms,
|
|
&mut bounding_box_overlays.opposite_pivot,
|
|
&tool_data.layers_dragging,
|
|
responses,
|
|
&document.network,
|
|
&document.metadata,
|
|
None,
|
|
&ToolType::Select,
|
|
);
|
|
|
|
selected.revert_operation();
|
|
}
|
|
|
|
responses.add(OverlaysMessage::Draw);
|
|
|
|
tool_data.snap_manager.cleanup(responses);
|
|
SelectToolFsmState::Ready
|
|
}
|
|
(_, SelectToolMessage::SetPivot { position }) => {
|
|
responses.add(DocumentMessage::StartTransaction);
|
|
|
|
let pos: Option<DVec2> = position.into();
|
|
tool_data.pivot.set_normalized_position(pos.unwrap(), document, responses);
|
|
|
|
self
|
|
}
|
|
_ => self,
|
|
}
|
|
}
|
|
|
|
fn standard_tool_messages(&self, message: &ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut Self::ToolData) -> bool {
|
|
// Check for standard hits or cursor events
|
|
match message {
|
|
ToolMessage::UpdateHints => {
|
|
let hint_data = HintData(vec![
|
|
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]),
|
|
HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale Selected")]),
|
|
HintGroup({
|
|
let mut hints = vec![HintInfo::mouse(MouseMotion::Lmb, "Select Object"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus()];
|
|
if tool_data.nested_selection_behavior == NestedSelectionBehavior::Shallowest {
|
|
hints.extend([HintInfo::keys([Key::Accel], "Deepest").prepend_plus(), HintInfo::mouse(MouseMotion::LmbDouble, "Deepen Selection")]);
|
|
}
|
|
hints
|
|
}),
|
|
HintGroup(vec![
|
|
HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"),
|
|
HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(),
|
|
]),
|
|
HintGroup(vec![
|
|
HintInfo::arrow_keys("Nudge Selected"),
|
|
HintInfo::keys([Key::Shift], "10x").prepend_plus(),
|
|
HintInfo::keys([Key::Alt], "Resize Corner").prepend_plus(),
|
|
HintInfo::keys([Key::Control], "Opp. Corner").prepend_plus(),
|
|
]),
|
|
HintGroup(vec![
|
|
HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "Move Duplicate"),
|
|
HintInfo::keys([Key::Control, Key::KeyD], "Duplicate").add_mac_keys([Key::Command, Key::KeyD]),
|
|
]),
|
|
]);
|
|
|
|
responses.add(FrontendMessage::UpdateInputHints { hint_data });
|
|
self.update_hints(responses);
|
|
true
|
|
}
|
|
ToolMessage::UpdateCursor => {
|
|
self.update_cursor(responses);
|
|
true
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn update_hints(&self, _responses: &mut VecDeque<Message>) {}
|
|
|
|
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
|
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
|
}
|
|
}
|
|
|
|
fn not_artboard<'a>(document: &'a DocumentMessageHandler) -> impl Fn(&LayerNodeIdentifier) -> bool + 'a {
|
|
|&layer| !document.metadata.is_artboard(layer)
|
|
}
|
|
|
|
fn drag_shallowest_manipulation(responses: &mut VecDeque<Message>, selected: Vec<LayerNodeIdentifier>, tool_data: &mut SelectToolData, document: &DocumentMessageHandler) {
|
|
for layer in selected {
|
|
let ancestor = layer
|
|
.ancestors(document.metadata())
|
|
.filter(not_artboard(document))
|
|
.find(|&ancestor| document.selected_nodes.selected_layers_contains(ancestor, document.metadata()));
|
|
|
|
let new_selected = ancestor.unwrap_or_else(|| {
|
|
layer
|
|
.ancestors(document.metadata())
|
|
.take_while(|&layer| layer != LayerNodeIdentifier::ROOT)
|
|
.filter(not_artboard(document))
|
|
.last()
|
|
.unwrap_or(layer)
|
|
});
|
|
tool_data.layers_dragging.retain(|layer| !layer.ancestors(document.metadata()).any(|ancestor| ancestor == new_selected));
|
|
tool_data.layers_dragging.push(new_selected);
|
|
}
|
|
|
|
responses.add(NodeGraphMessage::SelectedNodesSet {
|
|
nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(),
|
|
});
|
|
}
|
|
|
|
fn drag_deepest_manipulation(responses: &mut VecDeque<Message>, mut selected: Vec<LayerNodeIdentifier>, tool_data: &mut SelectToolData) {
|
|
tool_data.layers_dragging.append(&mut selected);
|
|
responses.add(NodeGraphMessage::SelectedNodesSet {
|
|
nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(),
|
|
});
|
|
}
|
|
|
|
fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, responses: &mut VecDeque<Message>) {
|
|
if document.selected_nodes.selected_layers_contains(layer, document.metadata()) {
|
|
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path });
|
|
return;
|
|
}
|
|
|
|
let Some(new_selected) = layer.ancestors(document.metadata()).filter(not_artboard(document)).find(|ancestor| {
|
|
ancestor
|
|
.parent(document.metadata())
|
|
.is_some_and(|parent| document.selected_nodes.selected_layers_contains(parent, document.metadata()))
|
|
}) else {
|
|
return;
|
|
};
|
|
|
|
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_selected.to_node()] });
|
|
}
|
|
|
|
fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, document_network: &NodeNetwork, responses: &mut VecDeque<Message>) {
|
|
if is_layer_fed_by_node_of_name(layer, document_network, "Text") {
|
|
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text });
|
|
responses.add(TextToolMessage::EditSelected);
|
|
} else if is_layer_fed_by_node_of_name(layer, document_network, "Shape") {
|
|
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path });
|
|
}
|
|
}
|