diff --git a/rayforge/doceditor/editor.py b/rayforge/doceditor/editor.py index d9738c9b..e5a04413 100644 --- a/rayforge/doceditor/editor.py +++ b/rayforge/doceditor/editor.py @@ -83,6 +83,9 @@ def __init__( self._on_processing_state_changed ) + # Global preference for maintaining aspect ratio during transforms. + self.aspect_ratio_locked: bool = True + # Instantiate and link command handlers, passing dependencies. self.asset = AssetCmd(self) self.edit = EditCmd(self) diff --git a/rayforge/doceditor/ui/item_properties.py b/rayforge/doceditor/ui/item_properties.py index 4cead371..26005313 100644 --- a/rayforge/doceditor/ui/item_properties.py +++ b/rayforge/doceditor/ui/item_properties.py @@ -99,7 +99,7 @@ def __init__( # Fixed Ratio Switch self.fixed_ratio_switch = Adw.SwitchRow( - title=_("Fixed Ratio"), active=True + title=_("Fixed Ratio"), active=self.editor.aspect_ratio_locked ) self.fixed_ratio_switch.connect( "notify::active", self._on_fixed_ratio_toggled @@ -415,7 +415,10 @@ def _on_shear_changed(self, spin_row, GParamSpec): self._in_update = False def _on_fixed_ratio_toggled(self, switch_row, GParamSpec): + if self._in_update: + return logger.debug(f"Fixed ratio toggled: {switch_row.get_active()}") + self.editor.aspect_ratio_locked = switch_row.get_active() # Check if the primary selected item is a workpiece or stock item is_ratio_lockable = self.items and isinstance( self.items[0], (WorkPiece, StockItem, Group) @@ -705,6 +708,10 @@ def _update_row_visibility_and_details(self, item: DocItem): is_single_workpiece or is_single_stockitem or is_single_group ) + self._in_update = True + self.fixed_ratio_switch.set_active(self.editor.aspect_ratio_locked) + self._in_update = False + self.source_file_row.set_visible(is_single_workpiece) self.fixed_ratio_switch.set_sensitive( is_single_item_with_size or is_single_group diff --git a/rayforge/workbench/canvas/canvas.py b/rayforge/workbench/canvas/canvas.py index dfc09cb6..1e8ea4b6 100644 --- a/rayforge/workbench/canvas/canvas.py +++ b/rayforge/workbench/canvas/canvas.py @@ -12,6 +12,7 @@ from .region import ( ElementRegion, BBOX_REGIONS, + CORNER_RESIZE_HANDLES, RESIZE_HANDLES, ROTATE_HANDLES, ROTATE_SHEAR_HANDLES, @@ -139,6 +140,23 @@ def find_by_data(self, data: Any) -> Optional[CanvasElement]: """ return self.root.find_by_data(data) + def _get_ratio_lock_default(self) -> bool: + """ + Base implementation for whether aspect ratio should be locked during + resize. Subclasses can override to bind to UI state. + """ + return False + + def _should_constrain_aspect(self, active_region: ElementRegion) -> bool: + """ + Determines if the current resize interaction should maintain aspect + ratio, applying Shift as an inverter of the default preference. Only + corner handles honor aspect locking; edge handles always deform. + """ + if active_region not in CORNER_RESIZE_HANDLES: + return False + return self._get_ratio_lock_default() ^ self._shift_pressed + def find_by_type( self, thetype: Any ) -> Generator[CanvasElement, None, None]: @@ -863,13 +881,16 @@ def on_mouse_drag(self, gesture, offset_x: float, offset_y: float): self._selection_group.apply_move(world_dx, world_dy) elif self._resizing: if self._active_origin: + constrain_aspect = self._should_constrain_aspect( + self._active_region + ) self._selection_group.resize_from_drag( self._active_region, world_dx, world_dy, self._active_origin, self._ctrl_pressed, - self._shift_pressed, + constrain_aspect, ) for elem in self._selection_group.elements: elem.trigger_update() @@ -930,6 +951,9 @@ def on_mouse_drag(self, gesture, offset_x: float, offset_y: float): and self._initial_transform and self._initial_world_transform ): + constrain_aspect = self._should_constrain_aspect( + self._active_region + ) transform.resize_element( element=self._drag_target, world_dx=world_dx, @@ -938,7 +962,7 @@ def on_mouse_drag(self, gesture, offset_x: float, offset_y: float): initial_world_transform=self._initial_world_transform, active_region=self._active_region, view_transform=self.view_transform, - shift_pressed=self._shift_pressed, + constrain_aspect=constrain_aspect, ctrl_pressed=self._ctrl_pressed, ) self._drag_target.trigger_update() diff --git a/rayforge/workbench/canvas/multiselect.py b/rayforge/workbench/canvas/multiselect.py index 16dc8eef..8e237c0d 100644 --- a/rayforge/workbench/canvas/multiselect.py +++ b/rayforge/workbench/canvas/multiselect.py @@ -246,7 +246,7 @@ def resize_from_drag( offset_y: float, active_origin: Tuple[float, float, float, float], ctrl_pressed: bool, - shift_pressed: bool, + constrain_aspect: bool, ): """ Calculates and applies the new group bounding box by calling the @@ -267,7 +267,7 @@ def resize_from_drag( active_region=active_region, drag_delta=(offset_x, offset_y), is_flipped=self.canvas.view_transform.is_flipped(), - constrain_aspect=shift_pressed, + constrain_aspect=constrain_aspect, from_center=ctrl_pressed, min_size=min_size_world, ) diff --git a/rayforge/workbench/canvas/transform.py b/rayforge/workbench/canvas/transform.py index 863708ff..e2c6703e 100644 --- a/rayforge/workbench/canvas/transform.py +++ b/rayforge/workbench/canvas/transform.py @@ -129,7 +129,7 @@ def resize_element( initial_world_transform: Matrix, active_region: ElementRegion, view_transform: Matrix, - shift_pressed: bool, + constrain_aspect: bool, ctrl_pressed: bool, ): """ @@ -156,7 +156,7 @@ def resize_element( active_region=active_region, drag_delta=local_delta, is_flipped=view_transform.is_flipped(), # Pass the flag - constrain_aspect=shift_pressed, + constrain_aspect=constrain_aspect, from_center=ctrl_pressed, min_size=min_size_local, ) diff --git a/rayforge/workbench/surface.py b/rayforge/workbench/surface.py index 3e49e55f..a373a41b 100644 --- a/rayforge/workbench/surface.py +++ b/rayforge/workbench/surface.py @@ -91,6 +91,11 @@ def __init__( self.edit_sketch_requested = Signal() self.edit_stock_item_requested = Signal() + # Aspect-ratio lock preference shared with the properties panel. + self.editor.aspect_ratio_locked = ( + getattr(self.editor, "aspect_ratio_locked", True) is True + ) + # Connect to generic signals from the base Canvas class self.move_begin.connect(self._on_any_transform_begin) self.resize_begin.connect(self._on_resize_begin) @@ -120,6 +125,17 @@ def show_travel_moves(self) -> bool: """Returns True if travel moves should be rendered.""" return self._show_travel_moves + def _get_ratio_lock_default(self) -> bool: + """ + Returns the current aspect-ratio lock preference shared with the + properties panel. Defaults to True for workpieces. + """ + return bool(getattr(self.editor, "aspect_ratio_locked", True)) + + def set_aspect_ratio_locked(self, locked: bool) -> None: + """Synchronizes the lock state with the editor.""" + self.editor.aspect_ratio_locked = locked + def set_laser_dot_visible(self, visible: bool = True) -> None: self._laser_dot.set_visible(visible) self.queue_draw()