diff --git a/editor/graphite-test-document.graphite b/editor/graphite-test-document.graphite index 59bad99e5..57b1ef962 100644 --- a/editor/graphite-test-document.graphite +++ b/editor/graphite-test-document.graphite @@ -1 +1 @@ -{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":10689566813179949075,"layer_ids":[10689566813179949074],"layers":[{"visible":true,"name":"Folder 1","data":{"Folder":{"next_assignment_id":17222868332373661780,"layer_ids":[17222868332373661779],"layers":[{"visible":true,"name":"Shape 1","data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.5,1.0],"manipulator_type":"Anchor"},{"position":[0.7761415,1.0],"manipulator_type":"InHandle"},{"position":[0.22385850000000002,1.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.0,0.5],"manipulator_type":"Anchor"},{"position":[0.0,0.7761415],"manipulator_type":"InHandle"},{"position":[0.0,0.22385850000000002],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.5,0.0],"manipulator_type":"Anchor"},{"position":[0.22385850000000002,0.0],"manipulator_type":"InHandle"},{"position":[0.7761415,0.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[1.0,0.5],"manipulator_type":"Anchor"},{"position":[1.0,0.22385850000000002],"manipulator_type":"InHandle"},{"position":[1.0,0.7761415],"manipulator_type":"OutHandle"}]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5]},"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[379.0,0.0,-0.0,239.0],"translation":[-479.3046875,-99.5]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":6520881531418194372,"name":"Untitled Document","version":"0.0.12","document_mode":"DesignMode","view_mode":"Normal","snapping_enabled":true,"overlays_visible":true,"layer_metadata":[[[],{"selected":false,"expanded":true}],[[10689566813179949074],{"selected":false,"expanded":true}],[[10689566813179949074,17222868332373661779],{"selected":true,"expanded":false}]],"layer_range_selection_reference":[10689566813179949074,17222868332373661779],"navigation_handler":{"pan":[0.0,0.0],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":[[10689566813179949074,17222868332373661779],"Artwork"]}} \ No newline at end of file +{"graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":10689566813179949075,"layer_ids":[10689566813179949074],"layers":[{"visible":true,"name":"Folder 1","data":{"Folder":{"next_assignment_id":17222868332373661780,"layer_ids":[17222868332373661779],"layers":[{"visible":true,"name":"Shape 1","data":{"Shape":{"shape":{"elements":[{"points":[{"position":[0.5,1.0],"manipulator_type":"Anchor"},{"position":[0.7761415,1.0],"manipulator_type":"InHandle"},{"position":[0.22385850000000002,1.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.0,0.5],"manipulator_type":"Anchor"},{"position":[0.0,0.7761415],"manipulator_type":"InHandle"},{"position":[0.0,0.22385850000000002],"manipulator_type":"OutHandle"}]},{"points":[{"position":[0.5,0.0],"manipulator_type":"Anchor"},{"position":[0.22385850000000002,0.0],"manipulator_type":"InHandle"},{"position":[0.7761415,0.0],"manipulator_type":"OutHandle"}]},{"points":[{"position":[1.0,0.5],"manipulator_type":"Anchor"},{"position":[1.0,0.22385850000000002],"manipulator_type":"InHandle"},{"position":[1.0,0.7761415],"manipulator_type":"OutHandle"}]},{"points":[null,null,null]}],"element_ids":[1,2,3,4,5]},"style":{"stroke":null,"fill":{"Solid":{"red":0.0,"green":0.0,"blue":0.0,"alpha":1.0}}},"render_index":1}},"transform":{"matrix2":[379.0,0.0,-0.0,239.0],"translation":[-479.3046875,-99.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[0.0,0.0]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"saved_document_identifier":6520881531418194372,"name":"Untitled Document","version":"0.0.13","document_mode":"DesignMode","view_mode":"Normal","snapping_enabled":true,"overlays_visible":true,"layer_metadata":[[[],{"selected":false,"expanded":true}],[[10689566813179949074],{"selected":false,"expanded":true}],[[10689566813179949074,17222868332373661779],{"selected":true,"expanded":false}]],"layer_range_selection_reference":[10689566813179949074,17222868332373661779],"navigation_handler":{"pan":[0.0,0.0],"panning":false,"snap_tilt":false,"snap_tilt_released":false,"tilt":0.0,"tilting":false,"zoom":1.0,"zooming":false,"snap_zoom":false,"mouse_position":[0.0,0.0]},"artboard_message_handler":{"artboards_graphene_document":{"root":{"visible":true,"name":null,"data":{"Folder":{"next_assignment_id":0,"layer_ids":[],"layers":[]}},"transform":{"matrix2":[1.0,0.0,0.0,1.0],"translation":[1060.3046875,373.5]},"pivot":[0.5,0.5],"blend_mode":"Normal","opacity":1.0}},"artboard_ids":[]},"properties_panel_message_handler":{"active_selection":[[10689566813179949074,17222868332373661779],"Artwork"]}} diff --git a/editor/src/consts.rs b/editor/src/consts.rs index a12e23dd6..3124a337f 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -71,7 +71,7 @@ pub const DEFAULT_FONT_FAMILY: &str = "Merriweather"; pub const DEFAULT_FONT_STYLE: &str = "Normal (400)"; // Document -pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.12"; // Remember to save a simple document and replace the test file `graphite-test-document.graphite` +pub const GRAPHITE_DOCUMENT_VERSION: &str = "0.0.13"; // Remember to save a simple document and replace the test file `graphite-test-document.graphite` pub const DEFAULT_DOCUMENT_NAME: &str = "Untitled Document"; pub const FILE_SAVE_SUFFIX: &str = ".graphite"; diff --git a/editor/src/messages/portfolio/document/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/portfolio/document/transform_layer/transform_layer_message_handler.rs index 7cf36cdc8..b9b038736 100644 --- a/editor/src/messages/portfolio/document/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/portfolio/document/transform_layer/transform_layer_message_handler.rs @@ -39,7 +39,7 @@ impl<'a> MessageHandler> for TransformL selected.revert_operation(); typing.clear(); } else { - *selected.pivot = selected.calculate_pivot(font_cache); + *selected.pivot = selected.mean_average_of_pivots(font_cache); } *mouse_position = ipp.mouse.position; @@ -137,7 +137,7 @@ impl<'a> MessageHandler> for TransformL self.transform_operation.apply_transform_operation(&mut selected, self.snap); } TransformOperation::Rotating(rotation) => { - let selected_pivot = selected.calculate_pivot(font_cache); + let selected_pivot = selected.mean_average_of_pivots(font_cache); let angle = { let start_offset = self.mouse_position - selected_pivot; let end_offset = ipp.mouse.position - selected_pivot; diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index 29d717573..927486e5d 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -216,27 +216,26 @@ impl<'a> Selected<'a> { } } - pub fn calculate_pivot(&mut self, font_cache: &FontCache) -> DVec2 { - let xy_summation = self - .selected - .iter() - .map(|path| { - let multiplied_transform = self.document.multiply_transforms(path).unwrap(); - - let bounds = self - .document - .layer(path) - .unwrap() - .aabb_for_transform(multiplied_transform, font_cache) - .unwrap_or([multiplied_transform.translation; 2]); - - (bounds[0] + bounds[1]) / 2. - }) - .fold(DVec2::ZERO, |summation, next| summation + next); + pub fn mean_average_of_pivots(&mut self, font_cache: &FontCache) -> DVec2 { + let xy_summation = self.selected.iter().filter_map(|path| self.document.pivot(path, font_cache)).reduce(|a, b| a + b).unwrap_or_default(); xy_summation / self.selected.len() as f64 } + pub fn center_of_aabb(&mut self, font_cache: &FontCache) -> DVec2 { + let [min, max] = self + .selected + .iter() + .filter_map(|path| { + let multiplied_transform = self.document.multiply_transforms(path).unwrap(); + + self.document.layer(path).unwrap().aabb_for_transform(multiplied_transform, font_cache) + }) + .reduce(|a, b| [a[0].min(b[0]), a[1].max(b[1])]) + .unwrap_or_default(); + (min + max) / 2. + } + pub fn update_transforms(&mut self, delta: DAffine2) { if !self.selected.is_empty() { let pivot = DAffine2::from_translation(*self.pivot); diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index 10d983ce5..0b60413ee 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -39,9 +39,10 @@ impl SelectedEdges { /// Calculate the pivot for the operation (the opposite point to the edge dragged) pub fn calculate_pivot(&self) -> DVec2 { - let min = self.bounds[0]; - let max = self.bounds[1]; + self.pivot_from_bounds(self.bounds[0], self.bounds[1]) + } + fn pivot_from_bounds(&self, min: DVec2, max: DVec2) -> DVec2 { let x = if self.left { max.x } else if self.right { @@ -62,7 +63,7 @@ impl SelectedEdges { } /// Computes the new bounds with the given mouse move and modifier keys - pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center: bool, constrain: bool) -> (DVec2, DVec2) { + pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center: bool, center_around: DVec2, constrain: bool) -> (DVec2, DVec2) { let mouse = transform.inverse().transform_point2(mouse); let mut min = self.bounds[0]; @@ -73,73 +74,49 @@ impl SelectedEdges { max.y = mouse.y; } if self.left { - let delta = min.x - mouse.x; min.x = mouse.x; - max.x += delta; } else if self.right { max.x = mouse.x; } - let mut size = max - min; + let mut pivot = self.pivot_from_bounds(min, max); + if center { + if self.top { + max.y = center_around.y * 2. - min.y; + pivot.y = center_around.y; + } else if self.bottom { + min.y = center_around.y * 2. - max.y; + pivot.y = center_around.y; + } + if self.left { + max.x = center_around.x * 2. - min.x; + pivot.x = center_around.x; + } else if self.right { + min.x = center_around.x * 2. - max.x; + pivot.x = center_around.x; + } + } + if constrain { - size = match ((self.top || self.bottom), (self.left || self.right)) { + let size = max - min; + let min_pivot = (pivot - min) / size; + let new_size = match ((self.top || self.bottom), (self.left || self.right)) { (true, true) => DVec2::new(size.x, size.x / self.aspect_ratio).abs().max(DVec2::new(size.y * self.aspect_ratio, size.y).abs()) * size.signum(), (true, false) => DVec2::new(size.y * self.aspect_ratio, size.y), (false, true) => DVec2::new(size.x, size.x / self.aspect_ratio), _ => size, }; - } - if center { - if self.left || self.right { - size.x *= 2.; - } - - if self.bottom || self.top { - size.y *= 2.; - } + let delta_size = new_size - size; + min = min - delta_size * min_pivot; + max = min + new_size; } - (min, size) - } - - /// Offsets the transformation pivot in order to scale from the center - fn offset_pivot(&self, center: bool, size: DVec2) -> DVec2 { - let mut offset = DVec2::ZERO; - - if !center { - return offset; - } - - if self.right { - offset.x -= size.x / 2.; - } - if self.left { - offset.x += size.x / 2.; - } - if self.bottom { - offset.y -= size.y / 2.; - } - if self.top { - offset.y += size.y / 2.; - } - offset - } - - /// Moves the position to account for centering (only necessary with absolute transforms - e.g. with artboards) - pub fn center_position(&self, mut position: DVec2, size: DVec2) -> DVec2 { - if self.right { - position.x -= size.x / 2.; - } - if self.bottom { - position.y -= size.y / 2.; - } - - position + (min, max - min) } /// Calculates the required scaling to resize the bounding box - pub fn bounds_to_scale_transform(&self, center: bool, size: DVec2) -> DAffine2 { - DAffine2::from_translation(self.offset_pivot(center, size)) * DAffine2::from_scale(size / (self.bounds[1] - self.bounds[0])) + pub fn bounds_to_scale_transform(&self, size: DVec2) -> DAffine2 { + DAffine2::from_scale(size / (self.bounds[1] - self.bounds[0])) } } @@ -213,7 +190,8 @@ pub struct BoundingBoxOverlays { pub transform: DAffine2, pub selected_edges: Option, pub original_transforms: OriginalTransforms, - pub pivot: DVec2, + pub opposite_pivot: DVec2, + pub center_of_transformation: DVec2, } impl BoundingBoxOverlays { diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 3dadd3a60..bb56d0af8 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -176,7 +176,8 @@ impl Fsm for ArtboardToolFsmState { bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| { let edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds); - bounding_box.pivot = edges.calculate_pivot(); + bounding_box.opposite_pivot = edges.calculate_pivot(); + edges }); @@ -189,11 +190,17 @@ impl Fsm for ArtboardToolFsmState { let snap_x = selected_edges.2 || selected_edges.3; let snap_y = selected_edges.0 || selected_edges.1; - tool_data - .snap_manager - .start_snap(document, document.bounding_boxes(None, Some(tool_data.selected_board.unwrap()), font_cache), snap_x, snap_y); + let board = tool_data.selected_board.unwrap(); + tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, Some(board), font_cache), snap_x, snap_y); tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]); + if let Some(bounds) = &mut tool_data.bounding_box_overlays { + let pivot = document.artboard_message_handler.artboards_graphene_document.pivot(&[board], font_cache).unwrap_or_default(); + let root = document.graphene_document.root.transform; + let pivot = root.inverse().transform_point2(pivot); + bounds.center_of_transformation = pivot; + } + ArtboardToolFsmState::ResizingBounds } else { let tolerance = DVec2::splat(SELECTION_TOLERANCE); @@ -249,11 +256,7 @@ impl Fsm for ArtboardToolFsmState { let mouse_position = input.mouse.position; let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position); - let (mut position, size) = movement.new_size(snapped_mouse_position, bounds.transform, from_center, constrain_square); - if from_center { - position = movement.center_position(position, size); - } - + let (position, size) = movement.new_size(snapped_mouse_position, bounds.transform, from_center, bounds.center_of_transformation, constrain_square); responses.push_back( ArtboardMessage::ResizeArtboard { artboard: tool_data.selected_board.unwrap(), diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 34bc33647..cc737e092 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -418,7 +418,7 @@ impl Fsm for SelectToolFsmState { bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| { let edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds); - bounding_box.pivot = edges.calculate_pivot(); + bounding_box.opposite_pivot = edges.calculate_pivot(); edges }); @@ -452,13 +452,21 @@ impl Fsm for SelectToolFsmState { tool_data.layers_dragging = selected; + if let Some(bounds) = &mut tool_data.bounding_box_overlays { + let document = &document.graphene_document; + + let selected = &tool_data.layers_dragging.iter().collect::>(); + let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.center_of_transformation, selected, responses, document); + bounds.center_of_transformation = selected.mean_average_of_pivots(font_cache); + } + ResizingBounds } else if rotating_bounds { if let Some(bounds) = &mut tool_data.bounding_box_overlays { let selected = selected.iter().collect::>(); - let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document); + let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.center_of_transformation, &selected, responses, &document.graphene_document); - *selected.pivot = selected.calculate_pivot(font_cache); + bounds.center_of_transformation = selected.mean_average_of_pivots(font_cache); } tool_data.layers_dragging = selected; @@ -543,11 +551,12 @@ impl Fsm for SelectToolFsmState { let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position); - let (_, size) = movement.new_size(snapped_mouse_position, bounds.transform, center, axis_align); - let delta = movement.bounds_to_scale_transform(center, size); + let (_, size) = movement.new_size(snapped_mouse_position, bounds.transform, center, bounds.center_of_transformation, axis_align); + let delta = movement.bounds_to_scale_transform(size); - let selected = tool_data.layers_dragging.iter().collect::>(); - let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document); + let selected = &tool_data.layers_dragging.iter().collect::>(); + let pivot = if center { &mut bounds.center_of_transformation } else { &mut bounds.opposite_pivot }; + let mut selected = Selected::new(&mut bounds.original_transforms, pivot, selected, responses, &document.graphene_document); selected.update_transforms(delta); } @@ -557,8 +566,8 @@ impl Fsm for SelectToolFsmState { (RotatingBounds, PointerMove { snap_angle, .. }) => { if let Some(bounds) = &mut tool_data.bounding_box_overlays { let angle = { - let start_offset = tool_data.drag_start - bounds.pivot; - let end_offset = input.mouse.position - bounds.pivot; + 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) }; @@ -573,7 +582,7 @@ impl Fsm for SelectToolFsmState { let delta = DAffine2::from_angle(snapped_angle); let selected = tool_data.layers_dragging.iter().collect::>(); - let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document); + let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.center_of_transformation, &selected, responses, &document.graphene_document); selected.update_transforms(delta); } @@ -672,7 +681,7 @@ impl Fsm for SelectToolFsmState { let selected = tool_data.layers_dragging.iter().collect::>(); let mut selected = Selected::new( &mut bounding_box_overlays.original_transforms, - &mut bounding_box_overlays.pivot, + &mut bounding_box_overlays.opposite_pivot, &selected, responses, &document.graphene_document, @@ -865,7 +874,7 @@ impl SelectToolData { // Duplicate each previously selected layer and select the new ones. for layer_path in Document::shallowest_unique_layers(self.layers_dragging.iter_mut()) { - // Moves the origional back to its starting position. + // Moves the original back to its starting position. responses.push_front( Operation::TransformLayerInViewport { path: layer_path.clone(), @@ -875,7 +884,7 @@ impl SelectToolData { ); // Copy the layers. - // Not using the Copy message allows us to retrieve the ids of the new layers to initalise the drag. + // Not using the Copy message allows us to retrieve the ids of the new layers to initialize the drag. let layer = match document.graphene_document.layer(layer_path) { Ok(layer) => layer.clone(), Err(e) => { @@ -907,7 +916,7 @@ impl SelectToolData { /// Removes the duplicated layers. Called when alt is released and the layers have been duplicated. fn stop_duplicates(&mut self, responses: &mut VecDeque) { - let origionals = match self.not_duplicated_layers.take() { + let originals = match self.not_duplicated_layers.take() { Some(x) => x, None => return, }; @@ -919,8 +928,8 @@ impl SelectToolData { responses.push_back(Operation::DeleteLayer { path: layer_path.clone() }.into()); } - // Move the origional to under the mouse - for layer_path in Document::shallowest_unique_layers(origionals.iter()) { + // Move the original to under the mouse + for layer_path in Document::shallowest_unique_layers(originals.iter()) { responses.push_front( Operation::TransformLayerInViewport { path: layer_path.clone(), @@ -930,14 +939,14 @@ impl SelectToolData { ); } - // Select the origionals + // Select the originals responses.push_back( DocumentMessage::SetSelectedLayers { - replacement_selected_layers: origionals.clone(), + replacement_selected_layers: originals.clone(), } .into(), ); - self.layers_dragging = origionals; + self.layers_dragging = originals; } } diff --git a/graphene/src/document.rs b/graphene/src/document.rs index cbcb86efc..126cd98a3 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -367,6 +367,12 @@ impl Document { Ok(layer.data.bounding_box(layer.transform, font_cache).map(|bounds| (bounds, transform))) } + /// Compute the center of transformation multiplied with `Document::multiply_transforms`. + pub fn pivot(&self, path: &[LayerId], font_cache: &FontCache) -> Option { + let layer = self.layer(path).ok()?; + Some(self.multiply_transforms(path).unwrap_or_default().transform_point2(layer.layerspace_pivot(font_cache))) + } + pub fn visible_layers_bounding_box(&self, font_cache: &FontCache) -> Option<[DVec2; 2]> { let mut paths = vec![]; self.visible_layers(&mut vec![], &mut paths).ok()?; diff --git a/graphene/src/layers/layer_info.rs b/graphene/src/layers/layer_info.rs index 19cc76cce..01f8a5509 100644 --- a/graphene/src/layers/layer_info.rs +++ b/graphene/src/layers/layer_info.rs @@ -192,6 +192,9 @@ pub struct Layer { /// A transformation applied to the layer (translation, rotation, scaling, and shear). #[serde(with = "DAffine2Ref")] pub transform: glam::DAffine2, + /// The center of transformations like rotation or scaling with the shift key. + /// This is in local space (so the layer's transform should be applied). + pub pivot: DVec2, /// The cached SVG thumbnail view of the layer. #[serde(skip)] pub thumbnail_cache: String, @@ -217,6 +220,7 @@ impl Layer { name: None, data, transform: glam::DAffine2::from_cols_array(&transform), + pivot: DVec2::splat(0.5), cache: String::new(), thumbnail_cache: String::new(), svg_defs_cache: String::new(), @@ -363,6 +367,11 @@ impl Layer { self.transform * scale } + pub fn layerspace_pivot(&self, font_cache: &FontCache) -> DVec2 { + let [min, max] = self.aabb_for_transform(DAffine2::IDENTITY, font_cache).unwrap_or([DVec2::ZERO, DVec2::ONE]); + self.pivot * (max - min) + min + } + /// Get a mutable reference to the Folder wrapped by the layer. /// This operation will fail if the [Layer type](Layer::data) is not `LayerDataType::Folder`. pub fn as_folder_mut(&mut self) -> Result<&mut FolderLayer, DocumentError> { @@ -462,6 +471,7 @@ impl Clone for Layer { name: self.name.clone(), data: self.data.clone(), transform: self.transform, + pivot: self.pivot, cache: String::new(), thumbnail_cache: String::new(), svg_defs_cache: String::new(),