-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add 'Align to Artboard' feature (issue #1720) #3981
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -273,12 +273,17 @@ | |
| let mut graph_operation_message_handler = GraphOperationMessageHandler {}; | ||
| graph_operation_message_handler.process_message(message, responses, context); | ||
| } | ||
| DocumentMessage::AlignSelectedLayers { axis, aggregate } => { | ||
| DocumentMessage::AlignSelectedLayers { axis, aggregate, align_to_artboard } => { | ||
| let axis = match axis { | ||
| AlignAxis::X => DVec2::X, | ||
| AlignAxis::Y => DVec2::Y, | ||
| }; | ||
| let Some(combined_box) = self.network_interface.selected_layers_artwork_bounding_box_viewport() else { | ||
| let combined_box = if align_to_artboard { | ||
| self.selected_layers_shared_artboard_bounding_box_viewport() | ||
| } else { | ||
| self.network_interface.selected_layers_artwork_bounding_box_viewport() | ||
| }; | ||
|
Comment on lines
+281
to
+285
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR description states that when aligning to an artboard, it should fall back to selection bounds if no shared artboard exists. However, the current implementation will simply skip the alignment because To match the described behavior, you could use let combined_box = if align_to_artboard {
self.selected_layers_shared_artboard_bounding_box_viewport()
.or_else(|| self.network_interface.selected_layers_artwork_bounding_box_viewport())
} else {
self.network_interface.selected_layers_artwork_bounding_box_viewport()
};If the current behavior of skipping alignment is intended, please consider updating the pull request description to reflect this. |
||
| let Some(combined_box) = combined_box else { | ||
| return; | ||
| }; | ||
|
|
||
|
|
@@ -1513,11 +1518,38 @@ | |
| } | ||
|
|
||
| impl DocumentMessageHandler { | ||
| <<<<<<< HEAD | ||
| /// Translates a viewport mouse position to a document-space transform, or uses the viewport center if no mouse position is given. | ||
| fn document_transform_from_mouse(&self, mouse: Option<(f64, f64)>, viewport: &ViewportMessageHandler) -> DAffine2 { | ||
| let viewport_pos: DVec2 = mouse.map_or_else(|| viewport.center_in_viewport_space().into_dvec2() + viewport.offset().into_dvec2(), |pos| pos.into()); | ||
| let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz); | ||
| DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_pos - viewport.offset().into_dvec2())) | ||
| ======= | ||
| fn selected_layers_shared_artboard_bounding_box_viewport(&self) -> Option<[DVec2; 2]> { | ||
| let selected_nodes = self.network_interface.selected_nodes(); | ||
| let mut selected_layers = selected_nodes.selected_unlocked_layers(&self.network_interface).peekable(); | ||
| selected_layers.peek()?; | ||
|
|
||
| let mut shared_artboard = None; | ||
|
|
||
| for layer in selected_layers { | ||
| let layer_artboard = if self.network_interface.is_artboard(&layer.to_node(), &[]) { | ||
| Some(layer) | ||
| } else { | ||
| layer | ||
| .ancestors(self.metadata()) | ||
| .find(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT && self.network_interface.is_artboard(&ancestor.to_node(), &[])) | ||
| }; | ||
|
|
||
| match (shared_artboard, layer_artboard) { | ||
| (None, Some(artboard)) => shared_artboard = Some(artboard), | ||
| (Some(shared_artboard), Some(artboard)) if shared_artboard == artboard => {} | ||
| _ => return None, | ||
| } | ||
| } | ||
|
|
||
| shared_artboard.and_then(|artboard| self.metadata().bounding_box_viewport(artboard)) | ||
| >>>>>>> 6525a73f6 (Add 'Align to Artboard' feature (issue #1720)) | ||
|
Check failure on line 1552 in editor/src/messages/portfolio/document/document_message_handler.rs
|
||
| } | ||
|
Comment on lines
+1521
to
1553
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file contains unresolved merge conflict markers ( It looks like you intended to add the /// Translates a viewport mouse position to a document-space transform, or uses the viewport center if no mouse position is given.
fn document_transform_from_mouse(&self, mouse: Option<(f64, f64)>, viewport: &ViewportMessageHandler) -> DAffine2 {
let viewport_pos: DVec2 = mouse.map_or_else(|| viewport.center_in_viewport_space().into_dvec2() + viewport.offset().into_dvec2(), |pos| pos.into());
let document_to_viewport = self.navigation_handler.calculate_offset_transform(viewport.center_in_viewport_space().into(), &self.document_ptz);
DAffine2::from_translation(document_to_viewport.inverse().transform_point2(viewport_pos - viewport.offset().into_dvec2()))
}
fn selected_layers_shared_artboard_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
let selected_nodes = self.network_interface.selected_nodes();
let mut selected_layers = selected_nodes.selected_unlocked_layers(&self.network_interface).peekable();
selected_layers.peek()?;
let mut shared_artboard = None;
for layer in selected_layers {
let layer_artboard = if self.network_interface.is_artboard(&layer.to_node(), &[]) {
Some(layer)
} else {
layer
.ancestors(self.metadata())
.find(|ancestor| *ancestor != LayerNodeIdentifier::ROOT_PARENT && self.network_interface.is_artboard(&ancestor.to_node(), &[]))
};
match (shared_artboard, layer_artboard) {
(None, Some(artboard)) => shared_artboard = Some(artboard),
(Some(shared_artboard), Some(artboard)) if shared_artboard == artboard => {}
_ => return None,
}
}
shared_artboard.and_then(|artboard| self.metadata().bounding_box_viewport(artboard))
} |
||
|
|
||
| /// Runs an intersection test with all layers and a viewport space quad | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -110,6 +110,9 @@ pub enum SelectToolMessage { | |
| SelectOptions { | ||
| options: SelectOptionsUpdate, | ||
| }, | ||
| SetAlignToArtboard { | ||
| align_to_artboard: bool, | ||
| }, | ||
| SetPivot { | ||
| position: ReferencePoint, | ||
| }, | ||
|
|
@@ -159,7 +162,7 @@ impl SelectTool { | |
| .widget_instance() | ||
| } | ||
|
|
||
| fn alignment_widgets(&self, disabled: bool) -> impl Iterator<Item = WidgetInstance> + use<> { | ||
| fn alignment_widgets(&self, disabled: bool, align_to_artboard: bool) -> impl Iterator<Item = WidgetInstance> + use<> { | ||
| [AlignAxis::X, AlignAxis::Y] | ||
| .into_iter() | ||
| .flat_map(|axis| [(axis, AlignAggregate::Min), (axis, AlignAggregate::Center), (axis, AlignAggregate::Max)]) | ||
|
|
@@ -174,12 +177,18 @@ impl SelectTool { | |
| }; | ||
| IconButton::new(icon, 24) | ||
| .tooltip_label(label) | ||
| .on_update(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate }.into()) | ||
| .on_update(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate, align_to_artboard }.into()) | ||
| .disabled(disabled) | ||
| .widget_instance() | ||
| }) | ||
| } | ||
|
|
||
| fn should_refresh_align_to_artboard(&mut self) -> bool { | ||
| let align_to_artboard_changed = self.tool_data.align_to_artboard_changed; | ||
| self.tool_data.align_to_artboard_changed = false; | ||
| align_to_artboard_changed | ||
| } | ||
|
Comment on lines
+186
to
+190
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| fn flip_widgets(&self, disabled: bool) -> impl Iterator<Item = WidgetInstance> + use<> { | ||
| [(FlipAxis::X, "FlipHorizontal", "Flip Horizontal"), (FlipAxis::Y, "FlipVertical", "Flip Vertical")] | ||
| .into_iter() | ||
|
|
@@ -252,9 +261,33 @@ impl LayoutHolder for SelectTool { | |
| } | ||
|
|
||
| // Align | ||
| let disabled = self.tool_data.selected_layers_count < 2; | ||
| let align_to_artboard = self.tool_data.align_to_artboard; | ||
| let disabled = self.tool_data.selected_layers_count == 0 || (!align_to_artboard && self.tool_data.selected_layers_count < 2); | ||
| let align_to_artboard_checkbox_id = CheckboxId::new(); | ||
| widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance()); | ||
| widgets.extend(self.alignment_widgets(disabled)); | ||
| widgets.extend(self.alignment_widgets(disabled, align_to_artboard)); | ||
| widgets.push( | ||
| PopoverButton::new() | ||
| .icon("AlignVerticalCenter") | ||
| .tooltip_label("Alignment Options") | ||
| .tooltip_description("Change how alignment uses the selection bounds or artboard bounds.") | ||
| .popover_min_width(Some(190)) | ||
| .popover_layout(Layout(vec![LayoutGroup::row(vec![ | ||
| CheckboxInput::new(align_to_artboard) | ||
| .for_label(align_to_artboard_checkbox_id) | ||
| .tooltip_label("To Artboard") | ||
| .tooltip_description("Align selected layers to their shared artboard instead of the selection bounds.") | ||
| .on_update(|input: &CheckboxInput| SelectToolMessage::SetAlignToArtboard { align_to_artboard: input.checked }.into()) | ||
| .widget_instance(), | ||
| TextLabel::new("To Artboard") | ||
| .tooltip_label("To Artboard") | ||
| .tooltip_description("Align selected layers to their shared artboard instead of the selection bounds.") | ||
| .for_checkbox(align_to_artboard_checkbox_id) | ||
| .widget_instance(), | ||
|
Comment on lines
+282
to
+286
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The TextLabel::new("To Artboard")
.for_checkbox(align_to_artboard_checkbox_id)
.widget_instance(), |
||
| ])])) | ||
| .disabled(self.tool_data.selected_layers_count == 0) | ||
| .widget_instance(), | ||
| ); | ||
|
|
||
| // Flip | ||
| let disabled = self.tool_data.selected_layers_count == 0; | ||
|
|
@@ -312,7 +345,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Sele | |
|
|
||
| self.fsm_state.process_event(message, &mut self.tool_data, context, &(), responses, false); | ||
|
|
||
| if self.tool_data.pivot_gizmo.pivot.should_refresh_pivot_position() || self.tool_data.selected_layers_changed || redraw_reference_pivot { | ||
| if self.tool_data.pivot_gizmo.pivot.should_refresh_pivot_position() || self.should_refresh_align_to_artboard() || 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; | ||
|
|
@@ -406,6 +439,8 @@ struct SelectToolData { | |
| snap_candidates: Vec<SnapCandidatePoint>, | ||
| auto_panning: AutoPanning, | ||
| drag_start_center: ViewportPosition, | ||
| align_to_artboard: bool, | ||
| align_to_artboard_changed: bool, | ||
| } | ||
|
|
||
| impl SelectToolData { | ||
|
|
@@ -611,6 +646,11 @@ impl Fsm for SelectToolFsmState { | |
|
|
||
| let ToolMessage::Select(event) = event else { return self }; | ||
| match (self, event) { | ||
| (_, SelectToolMessage::SetAlignToArtboard { align_to_artboard }) => { | ||
| tool_data.align_to_artboard = align_to_artboard; | ||
| tool_data.align_to_artboard_changed = true; | ||
| self | ||
| } | ||
| (_, SelectToolMessage::Overlays { context: mut overlay_context }) => { | ||
| tool_data.snap_manager.draw_overlays(SnapData::new(document, input, viewport), &mut overlay_context); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Alignment to artboard now returns early when there’s no shared artboard, so the align command becomes a no-op if the selection spans multiple artboards. Consider falling back to selection bounds in that case so alignment still works even when the artboard-relative mode can’t resolve a shared artboard.
Prompt for AI agents