diff --git a/crates/engine/src/cmd/shape.rs b/crates/engine/src/cmd/shape.rs index e3bf3920..51b1a680 100644 --- a/crates/engine/src/cmd/shape.rs +++ b/crates/engine/src/cmd/shape.rs @@ -406,6 +406,8 @@ fn flip(s: &mut Session, p: &Value) -> Result { } else { x.flip_h = !x.flip_h; } + // `Xfrm::affine` flips first and rotates second; a true mirror also negates the rotation. + x.rot = (360.0 - x.rot_norm()) % 360.0; } Ok(()) }) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index f5359732..9d97c8e6 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3888,3 +3888,34 @@ fn change_case_spans_formatting_runs() { assert_eq!(body.paragraphs[0].runs[0].text, "STRA"); assert_eq!(body.paragraphs[0].runs[0].props.bold, Some(true)); } + +#[test] +fn flipping_a_rotated_shape_mirrors_its_outline() { + let corners = |s: &Session, id: deckcraft_model::ShapeId| { + let x = s.doc().unwrap().shape(id).unwrap().xfrm.unwrap(); + let a = x.affine(); + [(0.0, 0.0), (x.w, 0.0), (x.w, x.h), (0.0, x.h)].map(|(px, py)| { + let p = a * deckcraft_geom::Point::new(px, py); + ((p.x * 100.0).round() as i64, (p.y * 100.0).round() as i64) + }) + }; + let sorted = |mut c: [(i64, i64); 4]| { + c.sort_unstable(); + c + }; + for (cmd, horizontal) in [("arrange.flipHorizontal", true), ("arrange.flipVertical", false)] { + let mut s = session(); + let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 200, 50]})).unwrap(); + let id = deckcraft_model::ShapeId(r["id"].as_u64().unwrap() as u32); + s.execute("shape.rotate", &json!({"deg": 30})).unwrap(); + let before = corners(&s, id); + // Mirror about the box's centre line (centre is 200, 125). + let mirrored = before.map(|(x, y)| if horizontal { (40000 - x, y) } else { (x, 25000 - y) }); + s.execute(cmd, &json!({})).unwrap(); + assert_eq!(sorted(corners(&s, id)), sorted(mirrored), "{cmd} of a 30° rotated rectangle is not its mirror image"); + let rot = s.doc().unwrap().shape(id).unwrap().xfrm.unwrap().rot; + assert!((rot - 330.0).abs() < 1e-9, "{cmd} negates the rotation, got {rot}"); + s.execute(cmd, &json!({})).unwrap(); + assert_eq!(sorted(corners(&s, id)), sorted(before), "{cmd} twice restores the outline"); + } +}