Transform panel: X/Y/W/H edit the anchors selected with Direct Selection (fixes #1824)

This commit is contained in:
Marco Arnulfo authored and bflatastic committed 2026-10-14 08:16:26 +07:00
1 parent 891a58eff8
commit 4ef0cb76e6
4 files changed
+87 -4

No files matched your search

+30 -1
View File
@@ -202,7 +202,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Set Bounds",
[],
None,
"{x?, y?, width?, height?, reference?: 0..8 (9-point grid), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; strokes/corners as object.scale)",
"{x?, y?, width?, height?, reference?: 0..8 (9-point grid), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; strokes/corners as object.scale; with anchor points selected (Direct Selection) and no copy, the values are the box around those points, square to the page, and only they move or scale, as with object.move (#1824))",
has_selection,
set_bounds
),
@@ -1400,6 +1400,13 @@ fn distribute_spacing(s: &mut Session, p: &Value) -> Result<Value> {
fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
let ids = selected_roots(s)?;
let st = s.doc()?;
if transforms_objects(p)
&& !bool_or(p, "copy", false)
&& let Some(b) = st.selection.anchor_bounds(&st.doc, s.prefs.use_preview_bounds)
{
return set_anchor_bounds(s, p, ids, b);
}
let d = &s.doc()?.doc;
// Sizes are measured square to the bounding box, which a rotation turns with the objects:
// the math below runs in its frame.
@@ -1477,6 +1484,28 @@ fn set_bounds(s: &mut Session, p: &Value) -> Result<Value> {
apply_transform(s, "Transform", ids, frame.conjugate(Affine::translate(to - new_rp) * scale), p, false)
}
/// `object.setBounds` with anchors selected with Direct Selection (#1824): the values are those of
/// `b`, the box around the anchors (and the objects selected whole) square to the page, and only
/// the anchors, their handles with them, and those objects move or scale.
fn set_anchor_bounds(s: &mut Session, p: &Value, ids: Vec<NodeId>, b: Rect) -> Result<Value> {
let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4) as usize;
let rp = vectorcraft_geom::reference_point(b, refi);
let mut w = f64_or(p, "width", b.width());
let mut h = f64_or(p, "height", b.height());
if bool_or(p, "proportional", false) {
if p.get("width").is_some() && b.width() > 0.0 {
h = b.height() * w / b.width();
} else if p.get("height").is_some() && b.height() > 0.0 {
w = b.width() * h / b.height();
}
}
// A single anchor, or anchors in a line, have no size to scale on that side.
let ratio = |size: f64, now: f64| if now > 1e-9 { size / now } else { 1.0 };
let scale = about(rp, Affine::scale_non_uniform(ratio(w, b.width()), ratio(h, b.height())));
let to = Point::new(f64_or(p, "x", rp.x), f64_or(p, "y", rp.y));
apply_transform(s, "Transform", ids, Affine::translate(to - rp) * scale, p, true)
}
/// The visual box of `ids` square to `frame`, as [`Document::oriented_bounds`] measures it, with
/// symbol instances measured without the strokes in their art
/// ([`vectorcraft_doc::SymbolBoxes::without_art_strokes`]).
+26
View File
@@ -821,6 +821,32 @@ fn transforms_act_on_direct_selected_anchors() {
assert_eq!(points(&s), [(25.0, 0.0), (75.0, 0.0), (100.0, 100.0), (0.0, 100.0)]);
}
/// #1824: with anchors selected with Direct Selection, the Transform panel's X/Y/W/H
/// (`object.setBounds`) are the box around those anchors, and only they move or scale.
#[test]
fn set_bounds_acts_on_direct_selected_anchors() {
let mut s = session();
let square = json!({"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 100, "y": 100}, {"x": 0, "y": 100}], "closed": true});
let id = NodeId(s.execute("path.create", &square).unwrap()["id"].as_u64().unwrap());
let points = |s: &Session| -> Vec<(f64, f64)> {
let NodeKind::Path { path, .. } = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("a path") };
path.subpaths[0].anchors.iter().map(|a| (a.p.x.round(), a.p.y.round())).collect()
};
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 1]]})).unwrap();
s.execute("object.setBounds", &json!({"x": 130, "reference": 0})).unwrap();
assert_eq!(points(&s), [(0.0, 0.0), (130.0, 0.0), (100.0, 100.0), (0.0, 100.0)], "only the selected anchor moves");
s.execute("object.setBounds", &json!({"y": 20, "reference": 4})).unwrap();
assert_eq!(points(&s), [(0.0, 0.0), (130.0, 20.0), (100.0, 100.0), (0.0, 100.0)]);
// Two anchors: W scales their box about the reference point, the others stay.
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 2], [0, 3]]})).unwrap();
s.execute("object.setBounds", &json!({"width": 50, "reference": 3})).unwrap();
assert_eq!(points(&s), [(0.0, 0.0), (130.0, 20.0), (50.0, 100.0), (0.0, 100.0)]);
// The path selected whole moves as a whole.
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
s.execute("object.setBounds", &json!({"x": 10, "reference": 0})).unwrap();
assert_eq!(points(&s), [(10.0, 0.0), (140.0, 20.0), (60.0, 100.0), (10.0, 100.0)]);
}
/// Arrange commands that leave the stacking order as it is record no undo step: Bring to Front and
/// Bring Forward on the front objects, Send to Back and Send Backward on the back ones, and Send to
/// Current Layer on objects already on top of the current layer.