use crate::canvas::shapes::Shapes;
use egui::emath::TSTransform;
use egui::{
Color32, Id, PointerButton, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, pos2,
};
pub(crate) const HANDLE_RADIUS: f32 = 5.0;
pub(crate) const HANDLE_HIT: f32 = HANDLE_RADIUS + 6.0;
fn handle_positions(bbox: Rect) -> [Pos2; 8] {
let (cx, cy) = (bbox.center().x, bbox.center().y);
[
pos2(bbox.min.x, bbox.min.y),
pos2(bbox.max.x, bbox.min.y),
pos2(bbox.max.x, bbox.max.y),
pos2(bbox.min.x, bbox.max.y),
pos2(cx, bbox.min.y),
pos2(bbox.max.x, cy),
pos2(cx, bbox.max.y),
pos2(bbox.min.x, cy),
]
}
fn handle_origin(bbox: Rect, handle: usize) -> Pos2 {
let opposite = (handle + 2) % 4;
if handle < 4 {
handle_positions(bbox)[opposite]
} else {
handle_positions(bbox)[4 + opposite]
}
}
fn scale_ratio(old: f32, new: f32) -> f32 {
if old.abs() > 1e-3 {
(new / old).max(0.01)
} else {
1.0
}
}
fn box_scale_factors(
bbox: Rect,
handle: usize,
origin: Pos2,
target: Pos2,
free_aspect: bool,
) -> (f32, f32) {
let old = handle_positions(bbox)[handle] - origin;
let new = target - origin;
match handle {
0..=3 if free_aspect => (scale_ratio(old.x, new.x), scale_ratio(old.y, new.y)),
0..=3 => {
let square = old.x * old.x + old.y * old.y;
let uniform = if square > 1e-6 {
((new.x * old.x + new.y * old.y) / square).max(0.01)
} else {
1.0
};
(uniform, uniform)
}
4 | 6 => (1.0, scale_ratio(old.y, new.y)),
_ => (scale_ratio(old.x, new.x), 1.0),
}
}
pub(crate) fn along_axis_factor(angle: f32, scale_x: f32, scale_y: f32) -> f32 {
let (sin, cos) = angle.sin_cos();
((cos * scale_x).powi(2) + (sin * scale_y).powi(2)).sqrt()
}
pub(crate) fn scaled_box(bbox: Rect, origin: Pos2, scale_x: f32, scale_y: f32) -> Rect {
let map = |p: Pos2| {
let offset = p - origin;
origin + Vec2::new(offset.x * scale_x, offset.y * scale_y)
};
Rect::from_two_pos(map(bbox.min), map(bbox.max))
}
#[derive(Debug, Clone)]
pub(crate) struct BoxScale<P> {
pub(crate) handle: usize,
pub(crate) start_box: Rect,
pub(crate) start_pointer: Pos2,
pub(crate) start: Vec<P>,
}
impl<P> BoxScale<P> {
pub(crate) fn factors(
&self,
pointer: Pos2,
about_centre: bool,
free_aspect: bool,
) -> (Pos2, f32, f32) {
let origin = if about_centre {
self.start_box.center()
} else {
handle_origin(self.start_box, self.handle)
};
let target = handle_positions(self.start_box)[self.handle] + (pointer - self.start_pointer);
let (scale_x, scale_y) =
box_scale_factors(self.start_box, self.handle, origin, target, free_aspect);
(origin, scale_x, scale_y)
}
}
pub(crate) fn box_handles(
ui: &mut Ui,
overlay: &mut Shapes,
scene_transform: &TSTransform,
shown_box: Rect,
) -> Option<usize> {
let global_box = Rect::from_min_max(
*scene_transform * shown_box.min,
*scene_transform * shown_box.max,
);
overlay.rect_stroke(
global_box,
0.0,
Stroke::new(1.5, Color32::from_white_alpha(160)),
StrokeKind::Outside,
);
let mut started = None;
for (index, &handle_scene) in handle_positions(shown_box).iter().enumerate() {
let id = ui.id().with(("bounding box handle", index));
let centre = *scene_transform * handle_scene;
if drag_handle(ui, overlay, centre, Color32::WHITE, id)
.drag_started_by(PointerButton::Primary)
{
started = Some(index);
}
}
started
}
pub(crate) fn drag_handle(
ui: &mut Ui,
overlay: &mut Shapes,
centre: Pos2,
fill: Color32,
id: Id,
) -> Response {
let to_local = ui
.ctx()
.layer_transform_from_global(ui.layer_id())
.unwrap_or_default();
let hit = Rect::from_center_size(
to_local * centre,
Vec2::splat(HANDLE_HIT * to_local.scaling),
);
let response = ui.interact(hit, id, Sense::drag());
overlay.circle(
centre,
HANDLE_RADIUS,
fill,
Stroke::new(1.0, Color32::BLACK),
);
response
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_edge_handle_scales_one_axis_only() {
let bbox = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 200.0));
let origin = handle_origin(bbox, 5);
let (x, y) = box_scale_factors(bbox, 5, origin, pos2(150.0, 400.0), false);
assert_eq!((x, y), (1.5, 1.0));
let origin = handle_origin(bbox, 6);
let (x, y) = box_scale_factors(bbox, 6, origin, pos2(400.0, 300.0), false);
assert_eq!((x, y), (1.0, 1.5));
}
#[test]
fn a_corner_handle_preserves_the_aspect_ratio_until_shift() {
let bbox = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 200.0));
let origin = handle_origin(bbox, 2);
let (x, y) = box_scale_factors(bbox, 2, origin, pos2(200.0, 400.0), false);
assert!((x - 2.0).abs() < 1e-5, "uniform along the diagonal: {x}");
assert_eq!(x, y, "the aspect ratio is kept");
let (x, y) = box_scale_factors(bbox, 2, origin, pos2(200.0, 400.0), true);
assert!((x - 2.0).abs() < 1e-5);
assert!((y - 2.0).abs() < 1e-5);
let (x, y) = box_scale_factors(bbox, 2, origin, pos2(50.0, 400.0), true);
assert!((x - 0.5).abs() < 1e-5);
assert!((y - 2.0).abs() < 1e-5);
}
#[test]
fn a_box_that_does_not_stretch_leaves_the_fixtures_alone() {
for turns in 0..16 {
let angle = turns as f32 * std::f32::consts::TAU / 16.0;
let factor = along_axis_factor(angle, 1.0, 1.0);
assert!((factor - 1.0).abs() < 1e-5, "at {angle} rad: {factor}");
}
}
#[test]
fn an_axis_aligned_fixture_follows_its_own_axis() {
assert!((along_axis_factor(0.0, 2.0, 0.5) - 2.0).abs() < 1e-5);
let right_angle = std::f32::consts::FRAC_PI_2;
assert!((along_axis_factor(right_angle, 2.0, 0.5) - 0.5).abs() < 1e-5);
}
#[test]
fn a_corner_handle_lands_where_the_cursor_is() {
let bbox = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 200.0));
let origin = handle_origin(bbox, 3); let target = pos2(-100.0, 400.0);
let (x, y) = box_scale_factors(bbox, 3, origin, target, false);
let scaled = scaled_box(bbox, origin, x, y);
assert!((scaled.min.x - target.x).abs() < 1e-3, "{scaled:?}");
assert!((scaled.max.y - target.y).abs() < 1e-3, "{scaled:?}");
}
}