Files
figcraft/crates/engine/src/rotated_select_tests.rs
T
BFlatandClaude Opus 5.5 b572363ac8 M1: rotated handle box for layers that share a rotation (#30)
Selecting several layers with the same rotation now shows the handle box
turned with them (the size label reads the box's own width and height),
and resizing scales them along the box's axes instead of the page's.
Different or right-angle rotations keep the page-aligned box.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 04:57:31 +07:00

83 lines
4.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Handle boxes for several layers that share a rotation.
use serde_json::json;
use crate::Session;
use crate::params::id_of;
use crate::tools::{PointerEvent, PointerKind, selection_box, selection_frame};
use figcraft_doc::NodeId;
use figcraft_geom::{Affine, Point};
fn rect(s: &mut Session, x: f64, y: f64, rotation: f64) -> NodeId {
let id = id_of(&s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": 100, "height": 50})).unwrap()["id"]).unwrap();
s.execute("node.set", &json!({"id": id, "props": {"rotation": rotation}})).unwrap();
id
}
fn near(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}
/// The layer's corners in the box's axes.
fn corners_in(s: &Session, frame: Affine, id: NodeId) -> Vec<Point> {
let doc = s.doc().unwrap();
let n = doc.node(id).unwrap();
let m = frame.inverse() * doc.world_transform(id);
[(0.0, 0.0), (n.width, 0.0), (n.width, n.height), (0.0, n.height)].iter().map(|(x, y)| m * Point::new(*x, *y)).collect()
}
#[test]
fn shared_rotation_turns_the_box_with_the_layers() {
let mut s = Session::new();
let ids = [rect(&mut s, 0.0, 0.0, 30.0), rect(&mut s, 40.0, 120.0, 30.0)];
let (frame, w, h) = selection_frame(s.doc().unwrap(), &ids).unwrap();
// The box's axes follow the layers': each sits upright in it, and the box hugs them.
let pts: Vec<Point> = ids.iter().flat_map(|i| corners_in(&s, frame, *i)).collect();
let fold = |f: fn(f64, f64) -> f64, init: f64, k: fn(&Point) -> f64| pts.iter().map(k).fold(init, f);
assert!(near(fold(f64::min, f64::INFINITY, |p| p.x), 0.0) && near(fold(f64::min, f64::INFINITY, |p| p.y), 0.0));
assert!(near(fold(f64::max, f64::NEG_INFINITY, |p| p.x), w) && near(fold(f64::max, f64::NEG_INFINITY, |p| p.y), h));
let c = (frame.inverse() * s.doc().unwrap().world_transform(ids[0])).as_coeffs();
assert!(near(c[0], 1.0) && near(c[1], 0.0) && near(c[2], 0.0) && near(c[3], 1.0), "{c:?}");
// Tighter than the page-aligned bounds.
let b = s.doc().unwrap().bounds_of(&ids).unwrap();
assert!(w * h < b.width() * b.height() - 1.0);
}
#[test]
fn different_or_right_angle_rotations_keep_the_page_aligned_box() {
let mut s = Session::new();
for (r0, r1) in [(30.0, 45.0), (90.0, 90.0), (0.0, 180.0)] {
let ids = [rect(&mut s, 0.0, 0.0, r0), rect(&mut s, 200.0, 50.0, r1)];
let doc = s.doc().unwrap();
let b = doc.bounds_of(&ids).unwrap();
let q = selection_box(doc, &ids).unwrap();
assert_eq!(q, [Point::new(b.x0, b.y0), Point::new(b.x1, b.y0), Point::new(b.x1, b.y1), Point::new(b.x0, b.y1)], "{r0} {r1}");
}
}
#[test]
fn resizing_a_rotated_multi_selection_scales_along_its_axes() {
let mut s = Session::new();
let ids = [rect(&mut s, 0.0, 0.0, 30.0), rect(&mut s, 40.0, 120.0, 30.0)];
s.execute("select.set", &json!({"ids": ids})).unwrap();
let (frame, w, h) = selection_frame(s.doc().unwrap(), &ids).unwrap();
// Drag the bottom-right handle to make the box 1.5× wider and 2× taller.
let (from, to) = (frame * Point::new(w, h), frame * Point::new(w * 1.5, h * 2.0));
s.pointer(&PointerEvent::at(PointerKind::Down, from.x, from.y, 1.0)).unwrap();
s.pointer(&PointerEvent::at(PointerKind::Move, to.x, to.y, 1.0)).unwrap();
s.pointer(&PointerEvent::at(PointerKind::Up, to.x, to.y, 1.0)).unwrap();
let doc = s.doc().unwrap();
let (f2, w2, h2) = selection_frame(doc, &ids).unwrap();
assert!(near(w2, w * 1.5) && near(h2, h * 2.0), "{w2} {h2}");
// The opposite corner stays put and the layers keep their rotation and grow with the box.
assert!((f2 * Point::ZERO - frame * Point::ZERO).hypot() < 1e-6);
for i in ids {
let n = doc.node(i).unwrap();
assert!(near(n.width, 150.0) && near(n.height, 100.0), "{} {}", n.width, n.height);
assert!(near(figcraft_geom::rotation_degrees(n.transform), 30.0));
}
// One undo step restores both.
s.execute("edit.undo", &json!({})).unwrap();
assert!(near(s.doc().unwrap().node(ids[0]).unwrap().width, 100.0));
}