mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Transform panel: X/Y/W/H edit the anchors selected with Direct Selection (fixes #1824)
This commit is contained in:
1 parent
891a58eff8
commit
4ef0cb76e6
4 files changed
+87
-4
No files matched your search
@@ -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`]).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -977,8 +977,10 @@ impl VectorcraftApp {
|
||||
self.ui.recent_fonts.get(..n).unwrap_or(&self.ui.recent_fonts)
|
||||
}
|
||||
|
||||
/// The selection's bounding box, rotated with rotated objects ([`Session::transform_box`]). The
|
||||
/// canvas and the transform fields read it every frame: it is measured once per revision.
|
||||
/// The selection's bounding box, rotated with rotated objects ([`Session::transform_box`]); with
|
||||
/// anchors selected with Direct Selection, the box around them square to the page, which
|
||||
/// `object.setBounds` then edits (#1824). The canvas and the transform fields read it every
|
||||
/// frame: it is measured once per revision.
|
||||
pub fn selection_box(&mut self) -> Option<vectorcraft_doc::OrientedBox> {
|
||||
let st = self.session.active()?;
|
||||
let key = (st.uid, st.revision, self.session.prefs.use_preview_bounds);
|
||||
@@ -987,7 +989,10 @@ impl VectorcraftApp {
|
||||
{
|
||||
return b;
|
||||
}
|
||||
let b = self.session.transform_box(&st.selection.objects);
|
||||
let b = match st.selection.anchor_bounds(&st.doc, self.session.prefs.use_preview_bounds) {
|
||||
Some(r) => Some(vectorcraft_doc::OrientedBox::aligned(r)),
|
||||
None => self.session.transform_box(&st.selection.objects),
|
||||
};
|
||||
self.canvas.selection_box = Some((key, b));
|
||||
b
|
||||
}
|
||||
|
||||
@@ -486,6 +486,29 @@ mod tests {
|
||||
assert_eq!(selected_guides(&app), None);
|
||||
}
|
||||
|
||||
/// #1824: with an anchor selected with Direct Selection the panel shows that anchor's position,
|
||||
/// and a new X moves it alone.
|
||||
#[test]
|
||||
fn an_anchor_selection_shows_and_moves_its_own_box() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
|
||||
let square = json!({"anchors": [{"x": 20, "y": 30}, {"x": 120, "y": 30}, {"x": 120, "y": 130}, {"x": 20, "y": 130}], "closed": true});
|
||||
let id = app.run("path.create", square).unwrap()["id"].as_u64().unwrap();
|
||||
let shown = texts(&mut app, show);
|
||||
assert!(shown.iter().any(|t| t == "70 pt") && shown.iter().any(|t| t == "100 pt"), "the whole path: {shown:?}");
|
||||
app.run("select.anchors", json!({"id": id, "anchors": [[0, 1]]})).unwrap();
|
||||
let shown = texts(&mut app, show);
|
||||
for v in ["120 pt", "30 pt", "0 pt"] {
|
||||
assert!(shown.iter().any(|t| t == v), "{v} in {shown:?}");
|
||||
}
|
||||
assert!(!shown.iter().any(|t| t == "70 pt" || t == "100 pt"), "{shown:?}");
|
||||
app.run("object.setBounds", json!({"x": 150, "reference": 4})).unwrap();
|
||||
let n = first_selected(&app).unwrap();
|
||||
let Some(path) = n.path_data() else { panic!("a path") };
|
||||
let xs: Vec<f64> = path.subpaths[0].anchors.iter().map(|a| a.p.x).collect();
|
||||
assert_eq!(xs, [20.0, 150.0, 120.0, 20.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn artboard_fields_keep_the_reference_point() {
|
||||
let r = Rect::new(0.0, 0.0, 300.0, 200.0);
|
||||
|
||||
Reference in new issue
Block a user