mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
Object: Reset Bounding Box also squares the objects inside the selection
Reset Bounding Box set the angle to 0 on the selected objects only. A rotation changes the box angle of the selected objects and of every object inside them, so the members of a rotated group kept their angle: after rotating two rectangles 20 degrees, grouping them, rotating the group 10 degrees and resetting, Ungroup brought back a box at 30 degrees, and a second reset reported 2 objects to change. The command now also squares every object inside the selected ones: the children of groups, compound paths, clip groups, blends, envelopes, repeats and intertwines, and the art of linked opacity masks. The art of an unlinked mask does not turn with the object, so it keeps its angle. Ungroup, Release Clipping Mask, Release Compound Path, Blend Expand, Release Envelope, Release Repeat, Release Intertwine and Release Opacity Mask then bring back boxes square to the page. The whole reset stays one undo step. Two results change. `changed` counts every object reset, the objects inside included, so the steps above return 3 where they returned 1. A group whose own box is square but whose members are turned used to return 0 with no undo step; it now squares the members in one step. Docs: params text, docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -106,7 +106,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Reset Bounding Box",
|
||||
["Object", "Transform"],
|
||||
None,
|
||||
"{} square the bounding box of the selected objects to the page again after a rotation (their geometry stays; one undo step, none when no box is rotated) → {changed: objects reset}",
|
||||
"{} square the bounding box of the selected objects, and of every object inside them (the children of groups, compound paths, clip groups, blends, envelopes, repeats and intertwines, and the art of linked opacity masks), to the page again after a rotation (their geometry stays; one undo step, none when no box is rotated) → {changed: objects reset, the objects inside included}",
|
||||
has_selection,
|
||||
reset_bounding_box
|
||||
),
|
||||
@@ -223,21 +223,63 @@ pub(crate) fn squash(s: &mut Session, from: usize, label: &str) {
|
||||
|
||||
// ---------- Reset Bounding Box ----------
|
||||
|
||||
/// Squares each selected object and every object inside it: the children of groups, compound paths,
|
||||
/// clip groups, blends, envelopes, repeats and intertwines, and the art of linked opacity masks.
|
||||
fn reset_bounding_box(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let ids = selected_roots(s)?;
|
||||
let d = &s.doc()?.doc;
|
||||
let turned: Vec<NodeId> = ids.into_iter().filter(|id| d.node(*id).is_some_and(|n| n.bbox_angle != 0.0)).collect();
|
||||
if !turned.is_empty() {
|
||||
let mut changed = 0;
|
||||
let squared: Vec<(NodeId, Node)> = ids
|
||||
.into_iter()
|
||||
.filter_map(|id| {
|
||||
let (n, count) = squared(d.node(id)?);
|
||||
changed += count;
|
||||
Some((id, n?))
|
||||
})
|
||||
.collect();
|
||||
if !squared.is_empty() {
|
||||
s.edit("Reset Bounding Box", |d, _| {
|
||||
for id in &turned {
|
||||
if let Some(n) = d.node_mut(*id) {
|
||||
n.bbox_angle = 0.0;
|
||||
for (id, n) in squared {
|
||||
if let Some(slot) = d.node_mut(id) {
|
||||
*slot = n;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(json!({ "changed": turned.len() }))
|
||||
Ok(json!({ "changed": changed }))
|
||||
}
|
||||
|
||||
/// A copy of `n` with the angle of every object in its subtree set to 0, or `None` when no box in
|
||||
/// it stands at an angle, and the number of objects squared. The subtree takes in children and the
|
||||
/// art of linked opacity masks (an unlinked mask's art turns on its own); layers have no box. Only
|
||||
/// the nodes on the way to a turned object are copied.
|
||||
fn squared(n: &Node) -> (Option<Node>, usize) {
|
||||
let mut out = (!n.is_layer() && n.bbox_angle != 0.0).then(|| {
|
||||
let mut c = n.clone();
|
||||
c.bbox_angle = 0.0;
|
||||
c
|
||||
});
|
||||
let mut count = usize::from(out.is_some());
|
||||
for (i, c) in n.children().into_iter().flatten().enumerate() {
|
||||
let (child, k) = squared(c);
|
||||
count += k;
|
||||
if let Some(child) = child
|
||||
&& let Some(slot) = out.get_or_insert_with(|| n.clone()).children_mut().and_then(|ch| ch.get_mut(i))
|
||||
{
|
||||
*slot = Arc::new(child);
|
||||
}
|
||||
}
|
||||
if let Some(m) = n.extras().mask.as_ref().filter(|m| m.linked) {
|
||||
let (art, k) = squared(&m.art);
|
||||
count += k;
|
||||
if let Some(art) = art
|
||||
&& let Some(m) = out.get_or_insert_with(|| n.clone()).extras_if_any().and_then(|e| e.mask.as_mut())
|
||||
{
|
||||
m.art = Arc::new(art);
|
||||
}
|
||||
}
|
||||
(out, count)
|
||||
}
|
||||
|
||||
// ---------- Lock / Hide ----------
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! the box to the page again.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::OrientedBox;
|
||||
use vectorcraft_doc::{Node, OrientedBox};
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
use vectorcraft_tools::bbox::Handle;
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||||
@@ -141,6 +141,149 @@ fn reset_bounding_box_squares_the_box_in_one_step() {
|
||||
assert!(near(angle(&s, a), 45.0));
|
||||
}
|
||||
|
||||
/// The angles of every object in `n`'s subtree, opacity mask art included, layers left out.
|
||||
fn subtree_angles(n: &Node, out: &mut Vec<f64>) {
|
||||
if !n.is_layer() {
|
||||
out.push(n.bbox_angle);
|
||||
}
|
||||
for c in n.children().into_iter().flatten() {
|
||||
subtree_angles(c, out);
|
||||
}
|
||||
if let Some(m) = &n.extras().mask {
|
||||
subtree_angles(&m.art, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// The angles of every object in the document, opacity mask art included, layers left out.
|
||||
fn object_angles(s: &Session) -> Vec<f64> {
|
||||
let mut out = vec![];
|
||||
for l in &s.doc().unwrap().doc.layers {
|
||||
subtree_angles(l, &mut out);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_squares_the_objects_inside_a_group() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0, 100.0, 50.0);
|
||||
let b = rect(&mut s, 300.0, 100.0, 100.0, 50.0);
|
||||
sel(&mut s, &[a, b]);
|
||||
s.execute("object.rotate", &json!({"angle": 20})).unwrap();
|
||||
let g = id_of(&s.execute("object.group", &json!({})).unwrap());
|
||||
s.execute("object.rotate", &json!({"angle": 10})).unwrap();
|
||||
assert!(near(angle(&s, g), 10.0) && near(angle(&s, a), 30.0) && near(angle(&s, b), 30.0));
|
||||
let n = undo_len(&s);
|
||||
// The group and both members, in one step.
|
||||
assert_eq!(s.execute("object.resetBoundingBox", &json!({})).unwrap()["changed"], 3);
|
||||
assert_eq!(undo_len(&s), n + 1);
|
||||
assert!(object_angles(&s).iter().all(|a| *a == 0.0), "{:?}", object_angles(&s));
|
||||
s.execute("object.ungroup", &json!({})).unwrap();
|
||||
assert_eq!(selection_box(&s).angle, 0.0);
|
||||
assert_eq!(s.execute("object.resetBoundingBox", &json!({})).unwrap()["changed"], 0);
|
||||
// Undo brings every angle back.
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(near(angle(&s, g), 10.0) && near(angle(&s, a), 30.0) && near(angle(&s, b), 30.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_squares_the_turned_members_of_a_square_group() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0, 100.0, 50.0);
|
||||
let b = rect(&mut s, 300.0, 100.0, 100.0, 50.0);
|
||||
sel(&mut s, &[a, b]);
|
||||
s.execute("object.rotate", &json!({"angle": 30})).unwrap();
|
||||
s.execute("object.group", &json!({})).unwrap();
|
||||
assert_eq!(selection_box(&s).angle, 0.0);
|
||||
let n = undo_len(&s);
|
||||
assert_eq!(s.execute("object.resetBoundingBox", &json!({})).unwrap()["changed"], 2);
|
||||
assert_eq!(undo_len(&s), n + 1);
|
||||
s.execute("object.ungroup", &json!({})).unwrap();
|
||||
assert_eq!(selection_box(&s).angle, 0.0);
|
||||
}
|
||||
|
||||
/// Two rectangles turned 20°, put in a container by `make`, turned 10° with it and reset, then
|
||||
/// released by `release`: nothing stands at an angle before or after the release.
|
||||
fn reset_then_release(make: &str, release: &str) -> Session {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0, 100.0, 50.0);
|
||||
let b = rect(&mut s, 150.0, 120.0, 100.0, 50.0);
|
||||
sel(&mut s, &[a, b]);
|
||||
s.execute("object.rotate", &json!({"angle": 20})).unwrap();
|
||||
s.execute(make, &json!({})).unwrap();
|
||||
s.execute("object.rotate", &json!({"angle": 10})).unwrap();
|
||||
assert!(object_angles(&s).iter().any(|a| near(*a, 30.0)), "{make}: {:?}", object_angles(&s));
|
||||
s.execute("object.resetBoundingBox", &json!({})).unwrap();
|
||||
assert!(object_angles(&s).iter().all(|a| *a == 0.0), "{make}: {:?}", object_angles(&s));
|
||||
s.execute(release, &json!({})).unwrap();
|
||||
assert!(object_angles(&s).iter().all(|a| *a == 0.0), "{release}: {:?}", object_angles(&s));
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("object.resetBoundingBox", &json!({})).unwrap()["changed"], 0, "{release}");
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_then_release_clipping_mask() {
|
||||
reset_then_release("object.clippingMask.make", "object.clippingMask.release");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_then_release_compound_path() {
|
||||
reset_then_release("object.compoundPath.make", "object.compoundPath.release");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_then_expand_blend() {
|
||||
reset_then_release("object.blend.make", "object.blend.expand");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_then_release_envelope() {
|
||||
reset_then_release("object.envelope.makeWithWarp", "object.envelope.release");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_then_release_repeat() {
|
||||
reset_then_release("object.repeat.grid", "object.repeat.release");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_then_release_intertwine() {
|
||||
reset_then_release("object.intertwine.make", "object.intertwine.release");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_then_release_linked_opacity_mask() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0, 100.0, 50.0);
|
||||
let b = rect(&mut s, 150.0, 120.0, 100.0, 50.0);
|
||||
sel(&mut s, &[a, b]);
|
||||
s.execute("object.rotate", &json!({"angle": 20})).unwrap();
|
||||
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
|
||||
let mask = s.doc().unwrap().doc.node(a).unwrap().extras().mask.clone().unwrap();
|
||||
assert!(mask.linked && near(mask.art.bbox_angle, 20.0));
|
||||
drop(s);
|
||||
reset_then_release("transparency.makeOpacityMask", "transparency.releaseOpacityMask");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_bounding_box_leaves_the_art_of_an_unlinked_opacity_mask() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0, 100.0, 50.0);
|
||||
let b = rect(&mut s, 150.0, 120.0, 100.0, 50.0);
|
||||
sel(&mut s, &[a, b]);
|
||||
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
|
||||
s.execute("object.rotate", &json!({"angle": 20})).unwrap();
|
||||
s.execute("transparency.unlinkOpacityMask", &json!({})).unwrap();
|
||||
let mask_angle = |s: &Session| s.doc().unwrap().doc.node(a).unwrap().extras().mask.as_ref().unwrap().art.bbox_angle;
|
||||
assert!(near(angle(&s, a), 20.0) && near(mask_angle(&s), 20.0));
|
||||
// Only the object: its mask's art no longer turns with it.
|
||||
assert_eq!(s.execute("object.resetBoundingBox", &json!({})).unwrap()["changed"], 1);
|
||||
assert_eq!(angle(&s, a), 0.0);
|
||||
assert!(near(mask_angle(&s), 20.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_flips_keep_an_upright_box_upright() {
|
||||
let mut s = session();
|
||||
|
||||
Reference in New Issue
Block a user