Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/draw/shape/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,43 @@ use crate::util;
use super::Shape;

impl Shape {
/// Limits movement so every translated integer coordinate stays representable.
pub(crate) fn constrain_translation(&self, dx: i32, dy: i32) -> (i32, i32) {
let constrain = |position: i32, delta: i32| {
let position = i64::from(position);
let clamped = i64::from(delta).clamp(
i64::from(i32::MIN) - position,
i64::from(i32::MAX) - position,
);
i32::try_from(clamped)
.expect("anchor-constrained movement lies between zero and the requested i32 delta")
};

match self {
Shape::Line { x1, y1, x2, y2, .. } | Shape::Arrow { x1, y1, x2, y2, .. } => (
constrain(*x2, constrain(*x1, dx)),
constrain(*y2, constrain(*y1, dy)),
),
Shape::Rect { x, y, .. }
| Shape::BlurRect { x, y, .. }
| Shape::Image { x, y, .. }
| Shape::Text { x, y, .. }
| Shape::StepMarker { x, y, .. }
| Shape::StickyNote { x, y, .. } => (constrain(*x, dx), constrain(*y, dy)),
Shape::Ellipse { cx, cy, .. } | Shape::Spotlight { cx, cy, .. } => {
(constrain(*cx, dx), constrain(*cy, dy))
}
Shape::Polygon { points, .. } | Shape::EraserStroke { points, .. } => {
points.iter().fold((dx, dy), |(dx, dy), &(x, y)| {
(constrain(x, dx), constrain(y, dy))
})
}
Shape::Freehand { .. }
| Shape::FreehandPressure { .. }
| Shape::MarkerStroke { .. } => (dx, dy),
}
}

pub(crate) fn translate(&mut self, dx: i32, dy: i32) {
match self {
Shape::Freehand { points, .. } => {
Expand Down
54 changes: 38 additions & 16 deletions src/input/state/core/selection_actions/translation/bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,30 @@ impl InputState {
}
}

fn clamp_axis_delta(position: i32, size: i32, screen: i32, delta: i32) -> i32 {
if screen <= 0 || size <= 0 {
fn clamp_axis_delta(position: i32, size: i32, start: i32, extent: i32, delta: i32) -> i32 {
if extent <= 0 || size <= 0 {
return delta;
}

let end = position.saturating_add(size);
let (min_delta, max_delta) = if size <= screen {
(0i32.saturating_sub(position), screen.saturating_sub(end))
let position = i64::from(position);
let end = position + i64::from(size);
let start = i64::from(start);
let view_end = start + i64::from(extent);
let (min_delta, max_delta) = if size <= extent {
(start - position, view_end - end)
} else {
(screen.saturating_sub(end), 0i32.saturating_sub(position))
(view_end - end, start - position)
};

delta.clamp(min_delta, max_delta)
// A panned or zoomed view may already clip the selection. Stop further
// outward movement without forcing it back into view against the drag.
// Oversized selections can align past the viewport, but their painted
// bounds must stay within the coordinate range used by shape transforms.
let min_delta = min_delta.min(0).max(i64::from(i32::MIN) - position);
let max_delta = max_delta.max(0).min((i64::from(i32::MAX) - end).max(0));
let clamped = i64::from(delta).clamp(min_delta, max_delta);
i32::try_from(clamped)
.expect("clamped movement lies between zero and the requested i32 delta")
}

pub(super) fn clamp_selection_translation(
Expand All @@ -99,21 +110,32 @@ impl InputState {
dy: i32,
) -> Option<(i32, i32)> {
let bounds = self.movable_selection_bounds(measurer)?;
let visible = self.visible_canvas_rect();
let (screen_width, screen_height) = self.view.screen_size();
let screen_width = screen_width.min(i32::MAX as u32) as i32;
let screen_height = screen_height.min(i32::MAX as u32) as i32;

let clamped_dx = if dx == 0 {
0
// Shape bounds and pointer deltas are canvas coordinates, so clamp
// against the viewport after applying the board pan and zoom.
let clamped_dx = if dx == 0 || screen_width == 0 {
dx
} else {
Self::clamp_axis_delta(bounds.x, bounds.width, screen_width, dx)
Self::clamp_axis_delta(bounds.x, bounds.width, visible.x, visible.width, dx)
};
let clamped_dy = if dy == 0 {
0
let clamped_dy = if dy == 0 || screen_height == 0 {
dy
} else {
Self::clamp_axis_delta(bounds.y, bounds.height, screen_height, dy)
Self::clamp_axis_delta(bounds.y, bounds.height, visible.y, visible.height, dy)
};

Some((clamped_dx, clamped_dy))
let frame = self.boards.active_frame();
let translation = self
.selected_shape_ids()
.iter()
.filter_map(|id| frame.shape(*id))
.filter(|drawn| !drawn.locked)
.fold((clamped_dx, clamped_dy), |(dx, dy), drawn| {
drawn.shape.constrain_translation(dx, dy)
});

Some(translation)
}
}
26 changes: 16 additions & 10 deletions src/input/state/core/selection_actions/translation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,20 @@ impl InputState {
let Some(bounds) = self.movable_selection_bounds(measurer) else {
return false;
};
let screen_width = self.view.screen_width().min(i32::MAX as u32) as i32;
if screen_width <= 0 {
if self.view.screen_width() == 0 {
return false;
}

let visible = self.visible_canvas_rect();
let target_x = if to_start {
0
visible.x
} else {
screen_width - bounds.width
visible
.x
.saturating_add(visible.width)
.saturating_sub(bounds.width)
};
let dx = target_x - bounds.x;
let dx = target_x.saturating_sub(bounds.x);
if dx == 0 {
return false;
}
Expand All @@ -102,17 +105,20 @@ impl InputState {
let Some(bounds) = self.movable_selection_bounds(measurer) else {
return false;
};
let screen_height = self.view.screen_height().min(i32::MAX as u32) as i32;
if screen_height <= 0 {
if self.view.screen_height() == 0 {
return false;
}

let visible = self.visible_canvas_rect();
let target_y = if to_start {
0
visible.y
} else {
screen_height - bounds.height
visible
.y
.saturating_add(visible.height)
.saturating_sub(bounds.height)
};
let dy = target_y - bounds.y;
let dy = target_y.saturating_sub(bounds.y);
if dy == 0 {
return false;
}
Expand Down
1 change: 1 addition & 0 deletions src/input/state/tests/selection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ mod deletion;
mod double_click;
mod duplicate;
mod mid_drag_actions;
mod movement;
mod reorder;
Loading
Loading