mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 05:55:32 +00:00
Transform: saved Scale and Reflect steps without a basis keep the page's axes
An action or journal entry recorded before Scale and Reflect followed the turned bounding box has no basis value. Replayed, such a step now acts along the page's axes, as it did when it was recorded; steps recorded now carry basis "box" and replay along the box. The built-in Reflect Horizontal action names basis "box", so it keeps reflecting across the box's own axis. The symbol test that reflects a turned instance names basis "page", which is the reflection it was written for.
This commit is contained in:
@@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Scale…",
|
||||
["Object", "Transform"],
|
||||
None,
|
||||
"{ids?: [id…] (selected first; default: the selection), sx: % (along the bounding box's width), sy?: % (along its height), origin?: [x,y] (a page point), basis?: \"box\"|\"page\" (box, the default: sx and sy follow the bounding box, turned with rotated objects until Reset Bounding Box; page: the page's axes), copy?, strokes?: bool (Scale Strokes & Effects: stroke weights, dashes and effect distances scale; off keeps them, type strokes included), corners?: bool (Scale Corners: live corner radii scale), objects?: bool (Transform Objects, default true; false: the art stays and, with patterns, only its pattern tiles transform), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size)} (strokes/corners default to the preferences). With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
"{ids?: [id…] (selected first; default: the selection), sx: % (along the bounding box's width), sy?: % (along its height), origin?: [x,y] (a page point), basis?: \"box\"|\"page\" (box, the default: sx and sy follow the bounding box, turned with rotated objects until Reset Bounding Box; page: the page's axes; a saved step without basis, recorded before it, plays along the page's axes), copy?, strokes?: bool (Scale Strokes & Effects: stroke weights, dashes and effect distances scale; off keeps them, type strokes included), corners?: bool (Scale Corners: live corner radii scale), objects?: bool (Transform Objects, default true; false: the art stays and, with patterns, only its pattern tiles transform), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size)} (strokes/corners default to the preferences). With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
has_selection,
|
||||
scale
|
||||
),
|
||||
@@ -52,7 +52,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Reflect…",
|
||||
["Object", "Transform"],
|
||||
None,
|
||||
"{ids?: [id…] (selected first; default: the selection), axis: \"vertical\"|\"horizontal\"|deg (the mirror line, counter-clockwise from horizontal; 0: horizontal; omitted: vertical; vertical and horizontal are the bounding box's own axes, deg a line on the page), origin?, basis?: \"box\"|\"page\" (for a named axis; box, the default: the bounding box's axes, turned with rotated objects until Reset Bounding Box; page: the page's axes), copy?, objects?: bool (Transform Objects, default true; false: the art stays and, with patterns, only its pattern tiles transform), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size)}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
"{ids?: [id…] (selected first; default: the selection), axis: \"vertical\"|\"horizontal\"|deg (the mirror line, counter-clockwise from horizontal; 0: horizontal; omitted: vertical; vertical and horizontal are the bounding box's own axes, deg a line on the page), origin?, basis?: \"box\"|\"page\" (for a named axis; box, the default: the bounding box's axes, turned with rotated objects until Reset Bounding Box; page: the page's axes; a saved step without basis, recorded before it, plays along the page's axes), copy?, objects?: bool (Transform Objects, default true; false: the art stays and, with patterns, only its pattern tiles transform), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size)}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
|
||||
has_selection,
|
||||
reflect
|
||||
),
|
||||
|
||||
@@ -366,9 +366,12 @@ pub(super) fn scale(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// `"box"` (the default) is the bounding box of `ids`, turned with rotated objects until Reset
|
||||
/// Bounding Box ([`Document::bbox_angle`]); `"page"` is the page's axes. Anchors selected with
|
||||
/// Direct Selection (with no copy and Transform Objects on) transform along the page's axes, as
|
||||
/// their own box does. The basis joins the journal entry, so a replay acts in the same frame.
|
||||
/// their own box does. The basis joins the journal entry, so a replay acts in the same frame. A
|
||||
/// saved step without `basis` ([`Session::replay`]) was recorded before Scale and Reflect followed
|
||||
/// the box, so it acts along the page's axes, as it did when it was recorded.
|
||||
fn basis_frame(s: &mut Session, p: &Value, cmd: &str, ids: &[NodeId]) -> Result<OrientedBox> {
|
||||
let page = choice(p, cmd, "basis", &["box", "page"])? == Some("page");
|
||||
let basis = choice(p, cmd, "basis", &["box", "page"])?;
|
||||
let page = basis == Some("page") || (basis.is_none() && s.replaying());
|
||||
let st = s.doc()?;
|
||||
let anchors = !bool_or(p, "copy", false)
|
||||
&& transforms_objects(p)
|
||||
|
||||
@@ -666,7 +666,7 @@ fn expanding_a_reflected_symbol_instance_keeps_its_box() {
|
||||
for reset in [false, true] {
|
||||
let (mut s, inst) = turned_instance(0.0);
|
||||
s.execute("object.rotate", &json!({"angle": 150})).unwrap();
|
||||
s.execute("object.reflect", &json!({"axis": "horizontal"})).unwrap();
|
||||
s.execute("object.reflect", &json!({"axis": "horizontal", "basis": "page"})).unwrap();
|
||||
assert!(near(angle(&s, inst), -170.0), "{}", angle(&s, inst));
|
||||
if reset {
|
||||
s.execute("object.resetBoundingBox", &json!({})).unwrap();
|
||||
@@ -786,6 +786,45 @@ fn scale_and_reflect_follow_the_turned_box() {
|
||||
assert!([got.x0 - want.x0, got.y0 - want.y0, got.x1 - want.x1, got.y1 - want.y1].iter().all(|d| d.abs() < 1e-6), "{got:?} {want:?}");
|
||||
}
|
||||
|
||||
/// A saved step without `basis` was recorded before Scale and Reflect followed the turned box, so a
|
||||
/// replay of it acts along the page's axes, as it did then, and its new journal entry says so. A
|
||||
/// step recorded now names its basis and replays along the box. A call without `basis` that is not
|
||||
/// a replay acts along the box.
|
||||
#[test]
|
||||
fn saved_steps_without_a_basis_replay_on_the_page() {
|
||||
// The turned rectangle after `run` runs on it: its page bounds and its box's angle.
|
||||
let after = |run: &dyn Fn(&mut Session)| {
|
||||
let mut s = session();
|
||||
let a = turned_rect(&mut s);
|
||||
run(&mut s);
|
||||
let st = (s.doc().unwrap().doc.bounds_of(&[a], false).unwrap(), angle(&s, a));
|
||||
(st, s.journal.last().unwrap().clone())
|
||||
};
|
||||
let same = |(r, a): (Rect, f64), (q, b): (Rect, f64)| [r.x0 - q.x0, r.y0 - q.y0, r.x1 - q.x1, r.y1 - q.y1, a - b].iter().all(|d| d.abs() < 1e-6);
|
||||
for (id, p) in [("object.scale", json!({"sx": 200, "sy": 100})), ("object.reflect", json!({"axis": "vertical"}))] {
|
||||
let mut on_page = p.clone();
|
||||
on_page["basis"] = json!("page");
|
||||
let (page, _) = after(&|s| {
|
||||
s.execute(id, &on_page).unwrap();
|
||||
});
|
||||
let (old, (_, noted)) = after(&|s| {
|
||||
s.replay(id, &p).unwrap();
|
||||
});
|
||||
assert!(same(old, page), "{id}: {old:?} {page:?}");
|
||||
assert_eq!(noted["basis"], "page", "{id}");
|
||||
|
||||
let (boxed, (recorded_id, recorded)) = after(&|s| {
|
||||
s.execute(id, &p).unwrap();
|
||||
});
|
||||
assert!(!same(boxed, page) && near(boxed.1, 45.0), "{id}: {boxed:?}");
|
||||
assert_eq!(recorded["basis"], "box", "{id}");
|
||||
let (replayed, _) = after(&|s| {
|
||||
s.replay(&recorded_id, &recorded).unwrap();
|
||||
});
|
||||
assert!(same(replayed, boxed), "{id}: {replayed:?} {boxed:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Once Reset Bounding Box squares the box, and for objects standing at different angles, Scale
|
||||
/// acts along the page's axes.
|
||||
#[test]
|
||||
|
||||
@@ -312,7 +312,7 @@ pub fn default_sets() -> Vec<ActionSet> {
|
||||
Action::new("Center on Artboard", vec![step("object.align", json!({"horizontal": "center", "vertical": "center", "to": "artboard"}))]),
|
||||
Action::new("Outline Text", vec![step("type.createOutlines", json!({}))]),
|
||||
Action::new("Rotate 90° CW", vec![step("object.rotate", json!({"angle": -90}))]),
|
||||
Action::new("Reflect Horizontal", vec![step("object.reflect", json!({"axis": "vertical"}))]),
|
||||
Action::new("Reflect Horizontal", vec![step("object.reflect", json!({"axis": "vertical", "basis": "box"}))]),
|
||||
],
|
||||
)]
|
||||
}
|
||||
@@ -2721,6 +2721,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The built-in Reflect Horizontal reflects a rotated object across its bounding box's own axis,
|
||||
/// as the Reflect dialog does, so the box keeps its angle; a saved step without a basis, recorded
|
||||
/// before Reflect followed the box, still reflects across the page's axis.
|
||||
#[test]
|
||||
fn the_built_in_reflect_follows_the_turned_box() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = vectorcraft_doc::NodeId(
|
||||
app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 100, "height": 50})).unwrap()["id"].as_u64().unwrap(),
|
||||
);
|
||||
app.run("select.set", json!({"ids": [id.0]})).unwrap();
|
||||
app.run("object.rotate", json!({"angle": 30})).unwrap();
|
||||
let angle = |app: &VectorcraftApp| app.session.active().unwrap().doc.bbox_angle(&[id]);
|
||||
let reflect = default_sets()[0].actions.iter().position(|a| a.name == "Reflect Horizontal").unwrap();
|
||||
app.ui.action_sets = default_sets();
|
||||
play(&mut app, 0, reflect).unwrap();
|
||||
assert!((angle(&app) - 30.0).abs() < 1e-6, "{}", angle(&app));
|
||||
app.ui.action_sets = vec![ActionSet::new("Old", vec![Action::new("Reflect", vec![("object.reflect".into(), json!({"axis": "vertical"}))])])];
|
||||
play(&mut app, 0, 0).unwrap();
|
||||
assert!((angle(&app) + 30.0).abs() < 1e-6, "{}", angle(&app));
|
||||
}
|
||||
|
||||
/// An action recorded with a key its command no longer reads still plays, accelerated and one
|
||||
/// step at a time, though the same command run by itself is refused.
|
||||
#[test]
|
||||
|
||||
+2
-2
@@ -5635,8 +5635,8 @@ axes, until Object › Transform › Reset Bounding Box squares the box to the p
|
||||
`origin` is still a page point, and a numeric `axis` is still a line on the page at that angle. Selections whose
|
||||
objects stand at different angles, and anchors selected with Direct Selection, use the page's axes; with `copy` or
|
||||
with `objects: false`, the whole objects or their pattern tiles transform, along the box. The journal entry
|
||||
records the `basis` used, so a recorded action replays in the same frame; an older recording without `basis` acts
|
||||
along the box. `object.transformAgain` repeats the transform's page matrix, also after Reset Bounding Box. The Scale
|
||||
records the `basis` used, so a recorded action replays in the same frame; a saved step without `basis` (an action or
|
||||
journal entry recorded before it) plays along the page's axes, as it did when it was recorded. `object.transformAgain` repeats the transform's page matrix, also after Reset Bounding Box. The Scale
|
||||
and Reflect dialogs, the Transform and Properties panels' flips, the Scale tool and the Reflect tool's default axis
|
||||
and Shift steps follow the box the same way; a Reflect axis drawn with the tool is the line drawn on the page.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user