Skip to content

Add 'Align to Artboard' feature (issue #1720)#3981

Open
afrdbaig7 wants to merge 1 commit intoGraphiteEditor:masterfrom
afrdbaig7:align-to-artboard-clean
Open

Add 'Align to Artboard' feature (issue #1720)#3981
afrdbaig7 wants to merge 1 commit intoGraphiteEditor:masterfrom
afrdbaig7:align-to-artboard-clean

Conversation

@afrdbaig7
Copy link
Copy Markdown
Contributor

Fixes #1720
This PR adds support for aligning layers relative to their artboard, addressing the requirement outlined in issue #1720. Previously, alignment operations were always computed based on the collective bounding box of the selected layers, which made it cumbersome to position elements consistently within an artboard. With this change, a new “To Artboard” option is introduced in the Select tool’s alignment controls, allowing users to switch the alignment reference from the selection bounds to the shared artboard.

Under the hood, this introduces a helper to resolve the common artboard ancestor for the current selection and compute its bounding box in viewport space. The alignment message has been extended with an align_to_artboard flag, ensuring the behavior is explicitly controlled while keeping existing functionality intact. The global menu bar alignment actions were also updated to work with the new message signature, defaulting to the original selection-based alignment to avoid regressions.

When the option is enabled, alignment is performed relative to the artboard containing all selected layers, with a safe fallback to selection bounds if no shared artboard exists. Additionally, alignment actions are now available even for a single selected layer when aligning to the artboard, improving usability. The changes are scoped strictly to this feature, excluding unrelated modifications, and have been tested locally to confirm correct behavior across both modes.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

Confidence score: 4/5

  • This PR looks safe to merge overall; the only reported issue is low-to-moderate severity and doesn’t suggest widespread regressions.
  • In editor/src/messages/portfolio/document/document_message_handler.rs, align-to-artboard now becomes a no-op when selection spans multiple artboards, which could surprise users relying on alignment behavior.
  • Pay close attention to editor/src/messages/portfolio/document/document_message_handler.rs - multi-artboard alignment currently returns early instead of falling back to selection bounds.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="editor/src/messages/portfolio/document/document_message_handler.rs">

<violation number="1" location="editor/src/messages/portfolio/document/document_message_handler.rs:281">
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.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +281 to +285
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()
};
Copy link
Copy Markdown
Contributor

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
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/document_message_handler.rs, line 281:

<comment>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.</comment>

<file context>
@@ -273,12 +273,17 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
 					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 {
</file context>
Suggested change
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()
};
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()
};

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an "Align to Artboard" feature, allowing users to align selected layers relative to their shared artboard. Key changes include updating the AlignSelectedLayers message, implementing logic to find the shared artboard's bounding box, and adding a toggle in the selection tool's alignment options. Feedback highlights critical merge conflict markers that need resolution, a missing fallback to selection bounds when no shared artboard is found, and opportunities to simplify code using std::mem::take and removing redundant UI tooltips.

Comment on lines +1521 to 1553
<<<<<<< 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))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This file contains unresolved merge conflict markers (<<<<<<<, =======, >>>>>>>). This will prevent the code from compiling and must be resolved.

It looks like you intended to add the selected_layers_shared_artboard_bounding_box_viewport function while keeping the existing document_transform_from_mouse function.

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

Comment on lines +281 to +285
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()
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 selected_layers_shared_artboard_bounding_box_viewport() returns None, which causes the AlignSelectedLayers handler to return early.

To match the described behavior, you could use .or_else() to provide the fallback. For example:

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.

Comment on lines +186 to +190
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function can be simplified by using std::mem::take. This is more idiomatic and concise for swapping a value with its default.

	fn should_refresh_align_to_artboard(&mut self) -> bool {
		std::mem::take(&mut self.tool_data.align_to_artboard_changed)
	}

Comment on lines +282 to +286
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(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tooltip_label and tooltip_description are duplicated for both the CheckboxInput and its associated TextLabel. Since the label is linked to the checkbox with for_checkbox, the tooltip will be shown when hovering over either widget. You can remove the redundant tooltips from the TextLabel to simplify the code.

					TextLabel::new("To Artboard")
						.for_checkbox(align_to_artboard_checkbox_id)
						.widget_instance(),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Alignment button options (align to artboard)

1 participant