Skip to content

Commit 90a8036

Browse files
mTvare6Keavon
andauthored
Fix self-chaining of transforms; fix compass rose getting offset when rotating a layer (#2296)
* Fix self-chaining of transforms and compass rose under single layer https://discord.com/channels/731730685944922173/931942323644928040/1340632846863175702 https://discord.com/channels/731730685944922173/931942323644928040/1340608972071243906 * When not invertible transformation, do nothing * Fix overlays and compass control when can't be visible * Simplify selection logic in compass states * Show compass only if it was possible that it could be seen before dragging * Prevent resizing line objects * Code review --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent e444785 commit 90a8036

File tree

8 files changed

+136
-93
lines changed

8 files changed

+136
-93
lines changed

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2321,7 +2321,7 @@ pub struct ClickXRayIter<'a> {
23212321
}
23222322

23232323
fn quad_to_path_lib_segments(quad: Quad) -> Vec<path_bool_lib::PathSegment> {
2324-
quad.edges().into_iter().map(|[start, end]| path_bool_lib::PathSegment::Line(start, end)).collect()
2324+
quad.all_edges().into_iter().map(|[start, end]| path_bool_lib::PathSegment::Line(start, end)).collect()
23252325
}
23262326

23272327
fn click_targets_to_path_lib_segments<'a>(click_targets: impl Iterator<Item = &'a ClickTarget>, transform: DAffine2) -> Vec<path_bool_lib::PathSegment> {

editor/src/messages/tool/common_functionality/compass_rose.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::consts::{COMPASS_ROSE_ARROW_CLICK_TARGET_ANGLE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER};
2+
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
23
use crate::messages::prelude::DocumentMessageHandler;
34

45
use glam::{DAffine2, DVec2};
@@ -10,9 +11,27 @@ pub struct CompassRose {
1011
}
1112

1213
impl CompassRose {
13-
pub fn refresh_transform(&mut self, document: &DocumentMessageHandler) {
14-
let [min, max] = document.selected_visible_and_unlock_layers_bounding_box_viewport().unwrap_or([DVec2::ZERO, DVec2::ONE]);
15-
self.compass_center = (DAffine2::from_translation(min) * DAffine2::from_scale(max - min)).transform_point2(DVec2::splat(0.5));
14+
fn get_layer_pivot_transform(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> DAffine2 {
15+
let [min, max] = document.metadata().nonzero_bounding_box(layer);
16+
17+
let bounds_transform = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
18+
let layer_transform = document.metadata().transform_to_viewport(layer);
19+
layer_transform * bounds_transform
20+
}
21+
pub fn refresh_position(&mut self, document: &DocumentMessageHandler) {
22+
let selected_nodes = document.network_interface.selected_nodes(&[]).unwrap();
23+
let mut layers = selected_nodes.selected_visible_and_unlocked_layers(&document.network_interface);
24+
25+
let Some(first) = layers.next() else { return };
26+
let count = layers.count() + 1;
27+
let transform = if count == 1 {
28+
Self::get_layer_pivot_transform(first, document)
29+
} else {
30+
let [min, max] = document.selected_visible_and_unlock_layers_bounding_box_viewport().unwrap_or([DVec2::ZERO, DVec2::ONE]);
31+
DAffine2::from_translation(min) * DAffine2::from_scale(max - min)
32+
};
33+
34+
self.compass_center = transform.transform_point2(DVec2::splat(0.5));
1635
}
1736

1837
pub fn compass_rose_position(&self) -> DVec2 {

editor/src/messages/tool/common_functionality/pivot.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,12 @@ impl Pivot {
111111
.selected_visible_and_unlocked_layers(&document.network_interface)
112112
{
113113
let transform = Self::get_layer_pivot_transform(layer, document);
114+
// Only update the pivot when computed position is finite.
115+
if transform.matrix2.determinant().abs() <= f64::EPSILON {
116+
return;
117+
};
114118
let pivot = transform.inverse().transform_point2(position);
115-
// Only update the pivot when computed position is finite. Infinite can happen when scale is 0.
116-
if pivot.is_finite() {
117-
responses.add(GraphOperationMessage::TransformSetPivot { layer, pivot });
118-
}
119+
responses.add(GraphOperationMessage::TransformSetPivot { layer, pivot });
119120
}
120121
}
121122

editor/src/messages/tool/common_functionality/transformation_cage.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ impl BoundingBoxManager {
411411

412412
fn overlay_display_category(&self, quad: Quad) -> HandleDisplayCategory {
413413
// Check if the area is essentially zero because either the width or height is smaller than an epsilon
414-
if (self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any() {
414+
if self.is_bounds_flat() {
415415
return HandleDisplayCategory::Flat;
416416
}
417417

@@ -434,6 +434,10 @@ impl BoundingBoxManager {
434434
HandleDisplayCategory::Narrow
435435
}
436436

437+
fn is_bounds_flat(&self) -> bool {
438+
(self.bounds[0] - self.bounds[1]).abs().cmple(DVec2::splat(1e-4)).any()
439+
}
440+
437441
/// Compute the threshold in viewport space. This only works with affine transforms as it assumes lines remain parallel.
438442
fn compute_viewport_threshold(&self, scalar: f64) -> [f64; 2] {
439443
let inverse = self.transform.inverse();
@@ -521,18 +525,18 @@ impl BoundingBoxManager {
521525

522526
/// Gets the required mouse cursor to show resizing bounds or optionally rotation
523527
pub fn get_cursor(&self, input: &InputPreprocessorMessageHandler, rotate: bool) -> MouseCursorIcon {
524-
if let Some((top, bottom, left, right)) = self.check_selected_edges(input.mouse.position) {
525-
match (top, bottom, left, right) {
528+
let edges = self.check_selected_edges(input.mouse.position);
529+
530+
match edges {
531+
Some((top, bottom, left, right)) if self.is_bounds_flat() => match (top, bottom, left, right) {
526532
(true, _, false, false) | (_, true, false, false) => MouseCursorIcon::NSResize,
527533
(false, false, true, _) | (false, false, _, true) => MouseCursorIcon::EWResize,
528534
(true, _, true, _) | (_, true, _, true) => MouseCursorIcon::NWSEResize,
529535
(true, _, _, true) | (_, true, true, _) => MouseCursorIcon::NESWResize,
530536
_ => MouseCursorIcon::Default,
531-
}
532-
} else if rotate && self.check_rotate(input.mouse.position) {
533-
MouseCursorIcon::Rotate
534-
} else {
535-
MouseCursorIcon::Default
537+
},
538+
_ if rotate && self.check_rotate(input.mouse.position) => MouseCursorIcon::Rotate,
539+
_ => MouseCursorIcon::Default,
536540
}
537541
}
538542
}

editor/src/messages/tool/tool_messages/select_tool.rs

Lines changed: 81 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#![allow(clippy::too_many_arguments)]
22

33
use super::tool_prelude::*;
4-
use crate::consts::{COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, ROTATE_INCREMENT, SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE};
4+
use crate::consts::{
5+
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COMPASS_ROSE_HOVER_RING_DIAMETER, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, ROTATE_INCREMENT, SELECTION_DRAG_ANGLE,
6+
SELECTION_TOLERANCE,
7+
};
58
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
69
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
710
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@@ -577,11 +580,11 @@ impl Fsm for SelectToolFsmState {
577580

578581
let show_compass = !(can_get_into_other_states || is_resizing_or_rotating);
579582
let show_compass_with_ring = bounds.map(|bounds| transform * Quad::from_box(bounds)).and_then(|quad| {
580-
show_compass
583+
(show_compass && quad.all_sides_at_least_width(COMPASS_ROSE_HOVER_RING_DIAMETER))
581584
.then_some(
582585
matches!(self, SelectToolFsmState::Dragging { .. })
583586
.then_some(show_hover_ring)
584-
.or(quad.contains(mouse_position).then_some(show_hover_ring)),
587+
.or((quad.contains(mouse_position)).then_some(show_hover_ring)),
585588
)
586589
.flatten()
587590
});
@@ -590,7 +593,7 @@ impl Fsm for SelectToolFsmState {
590593
tool_data.pivot.update_pivot(document, &mut overlay_context, angle);
591594

592595
// Update compass rose
593-
tool_data.compass_rose.refresh_transform(document);
596+
tool_data.compass_rose.refresh_position(document);
594597
let compass_center = tool_data.compass_rose.compass_rose_position();
595598
overlay_context.compass_rose(compass_center, angle, show_compass_with_ring);
596599

@@ -600,29 +603,31 @@ impl Fsm for SelectToolFsmState {
600603
compass_rose_state.axis_type().and_then(|axis| axis.is_constraint().then_some((axis, true)))
601604
};
602605

603-
if let Some((axis, hover)) = axis_state {
604-
if axis.is_constraint() {
605-
let e0 = tool_data
606-
.bounding_box_manager
607-
.as_ref()
608-
.map(|man| man.transform * Quad::from_box(man.bounds))
609-
.map_or(DVec2::X, |quad| (quad.top_left() - quad.top_right()).normalize_or(DVec2::X));
610-
611-
let (direction, color) = match axis {
612-
Axis::X => (e0, COLOR_OVERLAY_RED),
613-
Axis::Y => (e0.perp(), COLOR_OVERLAY_GREEN),
614-
_ => unreachable!(),
615-
};
616-
617-
let viewport_diagonal = input.viewport_bounds.size().length();
618-
619-
let color = if !hover {
620-
color
621-
} else {
622-
let color_string = &graphene_std::Color::from_rgb_str(color.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).rgba_hex();
623-
&format!("#{}", color_string)
624-
};
625-
overlay_context.line(compass_center - direction * viewport_diagonal, compass_center + direction * viewport_diagonal, Some(color));
606+
if show_compass_with_ring.is_some() {
607+
if let Some((axis, hover)) = axis_state {
608+
if axis.is_constraint() {
609+
let e0 = tool_data
610+
.bounding_box_manager
611+
.as_ref()
612+
.map(|man| man.transform * Quad::from_box(man.bounds))
613+
.map_or(DVec2::X, |quad| (quad.top_left() - quad.top_right()).normalize_or(DVec2::X));
614+
615+
let (direction, color) = match axis {
616+
Axis::X => (e0, COLOR_OVERLAY_RED),
617+
Axis::Y => (e0.perp(), COLOR_OVERLAY_GREEN),
618+
_ => unreachable!(),
619+
};
620+
621+
let viewport_diagonal = input.viewport_bounds.size().length();
622+
623+
let color = if !hover {
624+
color
625+
} else {
626+
let color_string = &graphene_std::Color::from_rgb_str(color.strip_prefix('#').unwrap()).unwrap().with_alpha(0.25).rgba_hex();
627+
&format!("#{}", color_string)
628+
};
629+
overlay_context.line(compass_center - direction * viewport_diagonal, compass_center + direction * viewport_diagonal, Some(color));
630+
}
626631
}
627632
}
628633

@@ -776,16 +781,24 @@ impl Fsm for SelectToolFsmState {
776781
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
777782
// If the user clicks on new shape, make that layer their new selection.
778783
// Otherwise enter the box select mode
784+
let bounds = tool_data.bounding_box_manager.as_ref().map(|man| man.transform * Quad::from_box(man.bounds));
779785

780-
let angle = tool_data
781-
.bounding_box_manager
782-
.as_ref()
783-
.map(|man| man.transform * Quad::from_box(man.bounds))
784-
.map_or(0., |quad| (quad.top_left() - quad.top_right()).to_angle());
786+
let angle = bounds.map_or(0., |quad| (quad.top_left() - quad.top_right()).to_angle());
785787
let mouse_position = input.mouse.position;
786788
let compass_rose_state = tool_data.compass_rose.compass_rose_state(mouse_position, angle);
787789
let is_over_pivot = tool_data.pivot.is_over(mouse_position);
788790

791+
let show_compass = bounds.is_some_and(|quad| quad.all_sides_at_least_width(COMPASS_ROSE_HOVER_RING_DIAMETER) && quad.contains(mouse_position));
792+
let can_grab_compass_rose = compass_rose_state.can_grab() && show_compass;
793+
let is_flat_layer = document
794+
.network_interface
795+
.selected_nodes(&[])
796+
.unwrap()
797+
.selected_visible_and_unlocked_layers(&document.network_interface)
798+
.find(|layer| !document.network_interface.is_artboard(&layer.to_node(), &[]))
799+
.map(|layer| document.metadata().transform_to_viewport(layer))
800+
.is_none_or(|transform| transform.matrix2.determinant().abs() <= f64::EPSILON);
801+
789802
let state =
790803
// Dragging the pivot
791804
if is_over_pivot {
@@ -796,15 +809,30 @@ impl Fsm for SelectToolFsmState {
796809

797810
SelectToolFsmState::DraggingPivot
798811
}
799-
// Dragging one (or two, forming a corner) of the transform cage bounding box edges
800-
else if dragging_bounds.is_some() {
812+
// Dragging the selected layers around to transform them
813+
else if can_grab_compass_rose || intersection.is_some_and(|intersection| selected.iter().any(|selected_layer| intersection.starts_with(*selected_layer, document.metadata()))) {
801814
responses.add(DocumentMessage::StartTransaction);
802815

816+
if input.keyboard.key(select_deepest) || tool_data.nested_selection_behavior == NestedSelectionBehavior::Deepest {
817+
tool_data.select_single_layer = intersection;
818+
} else {
819+
tool_data.select_single_layer = intersection.and_then(|intersection| intersection.ancestors(document.metadata()).find(|ancestor| selected.contains(ancestor)));
820+
}
821+
803822
tool_data.layers_dragging = selected;
804823

805-
if let Some(bounds) = &mut tool_data.bounding_box_manager {
806-
bounds.original_bound_transform = bounds.transform;
824+
tool_data.get_snap_candidates(document, input);
825+
let (axis, using_compass) = {
826+
let axis_state = compass_rose_state.axis_type().filter(|_| can_grab_compass_rose);
827+
(axis_state.unwrap_or_default(), axis_state.is_some())
828+
};
829+
SelectToolFsmState::Dragging { axis, using_compass }
830+
}
831+
// Dragging near the transform cage bounding box to rotate it
832+
else if rotating_bounds {
833+
responses.add(DocumentMessage::StartTransaction);
807834

835+
if let Some(bounds) = &mut tool_data.bounding_box_manager {
808836
tool_data.layers_dragging.retain(|layer| {
809837
if *layer != LayerNodeIdentifier::ROOT_PARENT {
810838
document.network_interface.network(&[]).unwrap().nodes.contains_key(&layer.to_node())
@@ -813,32 +841,33 @@ impl Fsm for SelectToolFsmState {
813841
false
814842
}
815843
});
816-
817844
let mut selected = Selected::new(
818845
&mut bounds.original_transforms,
819846
&mut bounds.center_of_transformation,
820-
&tool_data.layers_dragging,
847+
&selected,
821848
responses,
822849
&document.network_interface,
823850
None,
824851
&ToolType::Select,
825852
None
826853
);
854+
827855
bounds.center_of_transformation = selected.mean_average_of_pivots();
828856
}
829-
tool_data.get_snap_candidates(document, input);
830857

831-
if input.keyboard.key(skew) {
832-
SelectToolFsmState::SkewingBounds
833-
}else{
834-
SelectToolFsmState::ResizingBounds
835-
}
858+
tool_data.layers_dragging = selected;
859+
860+
SelectToolFsmState::RotatingBounds
836861
}
837-
// Dragging near the transform cage bounding box to rotate it
838-
else if rotating_bounds {
862+
// Dragging one (or two, forming a corner) of the transform cage bounding box edges
863+
else if dragging_bounds.is_some() && !is_flat_layer {
839864
responses.add(DocumentMessage::StartTransaction);
840865

866+
tool_data.layers_dragging = selected;
867+
841868
if let Some(bounds) = &mut tool_data.bounding_box_manager {
869+
bounds.original_bound_transform = bounds.transform;
870+
842871
tool_data.layers_dragging.retain(|layer| {
843872
if *layer != LayerNodeIdentifier::ROOT_PARENT {
844873
document.network_interface.network(&[]).unwrap().nodes.contains_key(&layer.to_node())
@@ -847,41 +876,25 @@ impl Fsm for SelectToolFsmState {
847876
false
848877
}
849878
});
879+
850880
let mut selected = Selected::new(
851881
&mut bounds.original_transforms,
852882
&mut bounds.center_of_transformation,
853-
&selected,
883+
&tool_data.layers_dragging,
854884
responses,
855885
&document.network_interface,
856886
None,
857887
&ToolType::Select,
858888
None
859889
);
860-
861890
bounds.center_of_transformation = selected.mean_average_of_pivots();
862891
}
892+
tool_data.get_snap_candidates(document, input);
863893

864-
tool_data.layers_dragging = selected;
865-
866-
SelectToolFsmState::RotatingBounds
867-
}
868-
// Dragging the selected layers around to transform them
869-
else if compass_rose_state.can_grab() || intersection.is_some_and(|intersection| selected.iter().any(|selected_layer| intersection.starts_with(*selected_layer, document.metadata()))) {
870-
responses.add(DocumentMessage::StartTransaction);
871-
872-
if input.keyboard.key(select_deepest) || tool_data.nested_selection_behavior == NestedSelectionBehavior::Deepest {
873-
tool_data.select_single_layer = intersection;
894+
if input.keyboard.key(skew) {
895+
SelectToolFsmState::SkewingBounds
874896
} else {
875-
tool_data.select_single_layer = intersection.and_then(|intersection| intersection.ancestors(document.metadata()).find(|ancestor| selected.contains(ancestor)));
876-
}
877-
878-
tool_data.layers_dragging = selected;
879-
880-
tool_data.get_snap_candidates(document, input);
881-
let axis = compass_rose_state.axis_type();
882-
match axis {
883-
Some(axis) => SelectToolFsmState::Dragging { axis, using_compass: true },
884-
None => SelectToolFsmState::Dragging { axis: Axis::None, using_compass: false }
897+
SelectToolFsmState::ResizingBounds
885898
}
886899
}
887900
// Dragging a selection box

editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,6 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
357357
|| selected_layers.is_empty()
358358
|| matches!(self.transform_operation, TransformOperation::Grabbing(_))
359359
{
360-
selected.original_transforms.clear();
361-
362360
return;
363361
}
364362

@@ -384,7 +382,6 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
384382
|| selected_layers.is_empty()
385383
|| matches!(self.transform_operation, TransformOperation::Rotating(_))
386384
{
387-
selected.original_transforms.clear();
388385
return;
389386
}
390387

@@ -438,7 +435,6 @@ impl MessageHandler<TransformLayerMessage, TransformData<'_>> for TransformLayer
438435
|| selected_layers.is_empty()
439436
|| matches!(self.transform_operation, TransformOperation::Scaling(_))
440437
{
441-
selected.original_transforms.clear();
442438
return;
443439
}
444440

node-graph/gcore/src/graphic_element/renderer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ impl ClickTarget {
9090
// This bounding box is not very accurate as it is the axis aligned version of the transformed bounding box. However it is fast.
9191
if !self
9292
.bounding_box
93-
.is_some_and(|loose| intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds))
93+
.is_some_and(|loose| (loose[0] - loose[1]).abs().cmpgt(DVec2::splat(1e-4)).all() && intersects((layer_transform * Quad::from_box(loose)).bounding_box(), target_bounds))
9494
{
9595
return false;
9696
}

0 commit comments

Comments
 (0)