mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 07:11:31 +00: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();
|
||||
|
||||
+5
-1
@@ -5144,7 +5144,11 @@ turn it with the object while its path geometry is still baked, so after a rotat
|
||||
stay square to it, as in the reference app. `object.rotate {angle, absolute: true}` turns the selection to an angle
|
||||
(the Transform and Properties panels' Rotate field), and `object.setBounds` measures width, height and the reference
|
||||
point along the turned box (x, y stay page coordinates). Objects turned together share the box. A new group starts
|
||||
square to the page. `object.resetBoundingBox` squares the box again without moving the art.
|
||||
square to the page. `object.resetBoundingBox` squares the box again without moving the art. It 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 (an unlinked mask's art 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.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"object.rotate","params":{"angle":45,"absolute":true}}}
|
||||
|
||||
@@ -698,6 +698,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- The History panel menu's Clear History (`edit.clearHistory`) removes every step to undo and to redo and leaves the document as it is; it is not an undo step itself, and it is disabled when there is no step to clear.
|
||||
- The New Document from State button at the bottom of the History panel (`edit.newDocumentFromState`) makes a new document, which becomes active, from the state the panel has selected ([public source](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/undo-edits-and-manage-design-history.html)). VectorCraft's own rules: the new document takes the next Untitled-N name, is marked unsaved, has nothing selected and has no steps to undo or redo, and the source document and its steps stay as they are.
|
||||
- The Transform panel menu's Transform Object Only, Transform Pattern Only and Transform Both (`transformPanelPatterns`) choose whether the fields, flips and menu flips of the Transform panel and the Properties panel's Transform section move the object and leave its pattern tiles, move the tiles alone, or both (until a choice they follow General › Transform Pattern Tiles, as before); Use Registration Point for Symbol (`useRegistrationPointForSymbol`) measures and transforms one selected symbol instance from its symbol's registration point, where the instance's flips, rotations and shears put it ([Transform panel overview](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/transform-panel-overview.html); `panels::transform::tests::the_menu_chooses_whether_the_object_its_pattern_or_both_transform`, `use_registration_point_for_symbol_measures_an_instance_from_its_registration_point`, `use_registration_point_for_symbol_follows_a_transformed_instance`, `tests_prefs::transform_panel_patterns_defaults_to_following_the_general_preference`).
|
||||
- Object › Transform › Reset Bounding Box squares the box of every object inside the selected ones as well: the children of groups, compound paths, clip groups, blends, envelopes, repeats and intertwines, and the art of linked opacity masks. Ungroup and the release commands then bring back boxes square to the page (VectorCraft's own behavior; `bboxrotate::reset_bounding_box_squares_the_objects_inside_a_group`, `reset_bounding_box_squares_the_turned_members_of_a_square_group`, `reset_bounding_box_then_release_clipping_mask` and the other `reset_bounding_box_then_*` tests, `reset_bounding_box_leaves_the_art_of_an_unlinked_opacity_mask`).
|
||||
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. Native menus explicitly end tracking on command selection, outside window presses and Escape; menu updates wait until tracking ends, and a dismissing press never starts a canvas gesture (M14.8).
|
||||
- The errors of the blend spine commands (Move Spine Point, Reshape Spine Segment, Add Spine Point and Delete Spine Point) show in the status bar in the interface language: each has a row in every catalog that translates all status and error messages and in Bulgarian (`blend_spine_param_errors_are_translated`).
|
||||
- The errors of the parameter checks that many commands share show in the status bar in the interface language: a value that is not a number where a number goes (`object.move` with `"dx": "60 mm"`, and `object.rotate`, `object.setBounds` and the other commands that call `check_numbers`), a missing number or index, `ids` that are not a list of object ids, an object id that is not a non-negative integer or names no object, and a date that is not Unix seconds. Each has a row in every catalog that translates all status and error messages, and in Bulgarian, Danish, Indonesian and Dutch (`shared_param_check_errors_are_translated`).
|
||||
|
||||
Reference in New Issue
Block a user