Merge pull request #1494 from storytold/w/1448-transform-objects

Fix #1448, #1460: Transform Objects in the Transform dialogs and Transform Each, and Reflect across an angle
This commit is contained in:
BFlat 2026-10-13 03:11:24 +07:00 committed by GitHub
commit fcc52c3c7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 174 additions and 40 deletions

View File

@ -341,6 +341,15 @@ pub fn uses_pattern(n: &Node, name: Option<&str>) -> bool {
})
}
/// Transform Patterns without Transform Objects: `m` moves the pattern tiles of `n` and of
/// everything inside it, and the art stays where it is.
pub fn transform_patterns_only(n: &mut Node, m: Affine) {
transform_pattern_paints(n, m);
for c in n.children_mut().into_iter().flatten() {
transform_patterns_only(Arc::make_mut(c), m);
}
}
/// Apply `m` to the pattern placement of every pattern paint of `n` (Transform Patterns).
pub fn transform_pattern_paints(n: &mut Node, m: Affine) {
// Without pattern paints there is nothing to change: don't copy the shared appearance.

View File

@ -75,7 +75,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Transform Each…",
["Object", "Transform"],
Some("Cmd+Alt+Shift+D"),
"{scaleH?: % (100), scaleV?: % (100), moveH?: pt, moveV?: pt (down = +), rotate?: deg (counter-clockwise), reflectX?: bool (flip vertically), reflectY?: bool (flip horizontally), random?: bool, seed?: n, reference?: 0..8 (9-point grid, 4 = centre), copy?: bool, strokes?: bool (Scale Strokes & Effects), corners?: bool (Scale Corners; both default to the preferences), patterns?: bool (Transform Patterns; default: prefs transformPatternTiles)} transform every selected object about its own reference point → {ids}",
"{scaleH?: % (100), scaleV?: % (100), moveH?: pt, moveV?: pt (down = +), rotate?: deg (counter-clockwise), reflectX?: bool (flip vertically), reflectY?: bool (flip horizontally), random?: bool, seed?: n, reference?: 0..8 (9-point grid, 4 = centre), copy?: bool, strokes?: bool (Scale Strokes & Effects), corners?: bool (Scale Corners; both default to the preferences), objects?: bool (Transform Objects, default true; false: the art stays and, with patterns, only its pattern tiles transform), patterns?: bool (Transform Patterns; default: prefs transformPatternTiles)} transform every selected object about its own reference point → {ids}",
has_selection,
transform_each
),
@ -307,6 +307,7 @@ fn transform_each(s: &mut Session, p: &Value) -> Result<Value> {
let random = bool_or(p, "random", false);
let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4).min(8) as usize;
let copy = bool_or(p, "copy", false);
let objects = super::object::transforms_objects(p);
if sh == 0.0 || sv == 0.0 {
return Err(bad("object.transformEach", "scale must be non-zero"));
}
@ -336,7 +337,7 @@ fn transform_each(s: &mut Session, p: &Value) -> Result<Value> {
let m = Affine::translate((k_mh, k_mv))
* about(o, Affine::rotate(-k_rot.to_radians()) * refl * Affine::scale_non_uniform(k_sh / 100.0, k_sv / 100.0));
if let Some(n) = d.node_mut(*id) {
n.transform(m, sc);
super::object::transform_node(n, m, sc, objects);
}
}
Ok(targets)

View File

@ -23,7 +23,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Transform",
[],
None,
"{matrix: [a,b,c,d,e,f], copy?: bool, ids?, strokes?: bool, corners?: bool, typeAreas?: bool, anchors?: bool, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} apply an affine to the selection (or ids); anchors (without ids or copy): paths with anchors selected with Direct Selection have only those anchors and their handles transformed, as the Transform tools and object.move/rotate/scale/reflect/shear do (default: the whole objects); strokes/corners: Scale Strokes & Effects / Scale Corners (default: the preferences); typeAreas: area type among them (not type inside a group) reshapes its frame by the matrix and its text reflows at its size, as a bounding-box handle drag does (default: the type transforms too)",
"{matrix: [a,b,c,d,e,f], copy?: bool, ids?, strokes?: bool, corners?: bool, typeAreas?: bool, anchors?: bool, 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)} apply an affine to the selection (or ids); anchors (without ids or copy): paths with anchors selected with Direct Selection have only those anchors and their handles transformed, as the Transform tools and object.move/rotate/scale/reflect/shear do (default: the whole objects); strokes/corners: Scale Strokes & Effects / Scale Corners (default: the preferences); typeAreas: area type among them (not type inside a group) reshapes its frame by the matrix and its text reflows at its size, as a bounding-box handle drag does (default: the type transforms too)",
has_doc,
transform
),
@ -32,7 +32,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Move…",
["Object", "Transform"],
Some("Cmd+Shift+M"),
"{dx, dy, copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
"{dx, dy, 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)}. 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,
move_cmd
),
@ -41,7 +41,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Rotate…",
["Object", "Transform"],
None,
"{angle: deg (counter-clockwise), absolute?: bool (angle is the bounding box's new angle, not an amount), origin?: [x,y] (default: the bounding box centre), copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} → {ids}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
"{angle: deg (counter-clockwise), absolute?: bool (angle is the bounding box's new angle, not an amount), origin?: [x,y] (default: the bounding box centre), 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)} → {ids}. 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,
rotate
),
@ -50,7 +50,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Scale…",
["Object", "Transform"],
None,
"{sx: %, sy?: %, origin?: [x,y], 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), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} (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.",
"{sx: %, sy?: %, origin?: [x,y], 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)} (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
),
@ -59,7 +59,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Reflect…",
["Object", "Transform"],
None,
"{axis: \"vertical\"|\"horizontal\"|deg, origin?, copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
"{axis: \"vertical\"|\"horizontal\"|deg, origin?, 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)}. 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
),
@ -68,7 +68,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Shear…",
["Object", "Transform"],
None,
"{angle: deg, axis?: \"horizontal\"|\"vertical\", origin?, copy?, patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)}. With anchors selected with Direct Selection (and no copy), only those anchors and their handles transform, about the centre of their own box.",
"{angle: deg, axis?: \"horizontal\"|\"vertical\", origin?, 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)}. 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,
shear
),
@ -267,8 +267,24 @@ pub(crate) fn transform_patterns(s: &mut Session, p: &Value, ids: &[NodeId]) ->
Ok(on)
}
/// Transform Objects (`objects` param, on unless it is false): off, a transform leaves the art
/// where it is and, with Transform Patterns, moves only its pattern tiles.
pub(crate) fn transforms_objects(p: &Value) -> bool {
bool_or(p, "objects", true)
}
/// `xf` on `n` with Transform Objects (`objects`, see [`transforms_objects`]) on or off.
pub(crate) fn transform_node(n: &mut Node, xf: Affine, sc: Scaling, objects: bool) {
if objects {
n.transform(xf, sc);
} else if sc.patterns {
vectorcraft_doc::pattern::transform_patterns_only(n, xf);
}
}
/// Apply `xf` to `ids` (`copy` param: duplicate first; `strokes`/`corners`: see [`scaling`];
/// `patterns`: see [`transform_patterns`]; `typeAreas`: see [`resize_type_area`]). Outside an
/// `patterns`: see [`transform_patterns`]; `objects`: see [`transforms_objects`]; `typeAreas`:
/// see [`resize_type_area`]). Outside an
/// interaction, records `xf` and the options it used for Transform Again. In an interaction whose
/// document is still its snapshot, a transform with `copy` keeps its copies ([`preview_copies`]),
/// and the next one that copies the same objects of the same snapshot transforms those copies.
@ -278,6 +294,7 @@ pub(crate) fn transform_patterns(s: &mut Session, p: &Value, ids: &[NodeId]) ->
pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf: Affine, p: &Value, anchors: bool) -> Result<Value> {
let copy = bool_or(p, "copy", false);
let areas = bool_or(p, "typeAreas", false);
let objects = transforms_objects(p);
let mut sc = if Scaling::factor(xf).is_some() { scaling(s, p) } else { Scaling::default() };
sc.patterns = transform_patterns(s, p, &ids)?;
// A transform of ids that name no object, or an identity transform without a copy, moves
@ -285,7 +302,7 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
// With a copy, the reply lists the copies, and there are none.
let d = &s.doc()?.doc;
let none = d.index_paths(ids.iter().copied()).is_empty();
let ids = if anchors && !copy && !s.doc()?.selection.anchors.is_empty() {
let ids = if anchors && objects && !copy && !s.doc()?.selection.anchors.is_empty() {
if xf != Affine::IDENTITY {
super::path::transform_anchors(s, label, xf, sc)?;
}
@ -310,9 +327,9 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
let paths = d.index_paths(targets.iter().copied());
for id in &targets {
if let Some(n) = paths.get(id).and_then(|p| d.node_mut_at(p))
&& !(areas && resize_type_area(n, xf))
&& !(objects && areas && resize_type_area(n, xf))
{
n.transform(xf, sc);
transform_node(n, xf, sc, objects);
}
}
Ok(targets)
@ -320,7 +337,7 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
};
let st = s.doc_mut()?;
if st.interaction.is_none() {
let mut options = json!({ "copy": copy, "typeAreas": areas, "patterns": sc.patterns });
let mut options = json!({ "copy": copy, "typeAreas": areas, "patterns": sc.patterns, "objects": objects });
if Scaling::factor(xf).is_some() {
options["strokes"] = json!(sc.strokes);
options["corners"] = json!(!sc.keep_corners);

View File

@ -712,7 +712,10 @@ fn a_transform_that_moves_nothing_records_no_undo_step() {
assert_eq!(history(&s), before, "{id} {p}");
assert_eq!(s.journal.last().unwrap(), &(id.to_string(), p));
}
assert_eq!(s.doc().unwrap().last_transform, Some((Affine::IDENTITY, json!({"copy": false, "typeAreas": false, "patterns": false}))));
assert_eq!(
s.doc().unwrap().last_transform,
Some((Affine::IDENTITY, json!({"copy": false, "typeAreas": false, "patterns": false, "objects": true})))
);
// A Nudge of 0 with an anchor of the rectangle direct-selected keeps it a live shape.
let is_live = |s: &Session| matches!(s.doc().unwrap().doc.node(a).unwrap().kind, NodeKind::Path { live: Some(_), .. });
assert!(is_live(&s), "a rectangle is a live shape");

View File

@ -205,6 +205,45 @@ fn transform_patterns_only_moves_placement() {
assert_eq!(xf.as_coeffs(), [2.0, 0.0, 0.0, 2.0, 5.0, 0.0]);
}
/// #1448, #1460: with Transform Objects off, the transform commands leave the art where it is and
/// move only its pattern tiles (Transform Patterns on), or nothing at all; Transform Again repeats
/// that.
#[test]
fn transforms_without_transform_objects_move_only_the_pattern() {
let mut s = session();
make_dots(&mut s);
let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0);
s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap();
s.execute("select.set", &json!({"ids": [big.0]})).unwrap();
let before = node(&s, big).geometric_bounds();
let tiles = |s: &Session| {
let n = node(s, big);
let Paint::Pattern { xf, .. } = n.appearance.fill_paint() else { panic!() };
xf.as_coeffs()
};
for (cmd, p, want) in [
("object.move", json!({"dx": 5, "dy": 0}), [1.0, 0.0, 0.0, 1.0, 5.0, 0.0]),
("object.transformAgain", json!({}), [1.0, 0.0, 0.0, 1.0, 10.0, 0.0]),
("object.reflect", json!({"axis": "vertical", "origin": [0, 0]}), [-1.0, 0.0, 0.0, 1.0, -10.0, 0.0]),
("object.transformEach", json!({"moveH": 10, "reference": 0}), [-1.0, 0.0, 0.0, 1.0, 0.0, 0.0]),
] {
// Transform Again repeats the move as it was made, options included.
if cmd != "object.transformAgain" {
let mut off = p.clone();
off["objects"] = json!(false);
off["patterns"] = json!(false);
s.execute(cmd, &off).unwrap();
assert_eq!(node(&s, big).geometric_bounds(), before, "{cmd}: neither transforms");
off["patterns"] = json!(true);
s.execute(cmd, &off).unwrap();
} else {
s.execute(cmd, &p).unwrap();
}
assert_eq!(node(&s, big).geometric_bounds(), before, "{cmd}");
assert_eq!(tiles(&s), want, "{cmd}");
}
}
#[test]
fn list_reports_patterns_and_edit_state() {
let mut s = session();

View File

@ -64,7 +64,7 @@ fn lengths(kind: &str) -> &'static [&'static str] {
/// number: fields that ↑/↓ step by one (Shift: ten) and that do math, as the distances do.
fn suffix(kind: &str, key: &str) -> Option<&'static str> {
match (kind, key) {
("rotate" | "shear", "angle") => Some("°"),
("rotate" | "shear" | "reflect", "angle") => Some("°"),
("scale", "sx" | "sy") => Some("%"),
_ => None,
}
@ -103,7 +103,8 @@ fn choices(kind: &str, key: &str) -> Vec<(&'static str, &'static str)> {
match (kind, key) {
("offsetPath", "joins") => vec![("miter", tl!("Miter")), ("round", tl!("Round")), ("bevel", tl!("Bevel"))],
("average", "axis") => vec![axis("horizontal", "Horizontal"), axis("vertical", "Vertical"), axis("both", "Both")],
("reflect" | "shear", "axis") => vec![axis("horizontal", "Horizontal"), axis("vertical", "Vertical")],
("reflect", "axis") => vec![axis("horizontal", "Horizontal"), axis("vertical", "Vertical"), axis("angle", "Angle")],
("shear", "axis") => vec![axis("horizontal", "Horizontal"), axis("vertical", "Vertical")],
_ => vec![],
}
}

View File

@ -44,7 +44,7 @@ pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) {
"rotate" | "reflect" | "scale" | "shear" | "artboardOptions" => {
let mut base = match kind {
"rotate" => json!({"angle": 0}),
"reflect" => json!({"axis": "vertical"}),
"reflect" => json!({"axis": "vertical", "angle": 90}),
"scale" => json!({"sx": 100, "sy": 100, "uniform": true}),
"shear" => json!({"angle": 0, "axis": "horizontal"}),
_ => json!({}),

View File

@ -1,5 +1,6 @@
//! Transform dialogs: Move, Rotate, Scale, Reflect and Shear (with Transform Patterns, Copy and
//! Preview; Scale also has Uniform, Scale Corners and Scale Strokes & Effects). With Preview on
//! Transform dialogs: Move, Rotate, Scale, Reflect and Shear (with Transform Objects, Transform
//! Patterns, Copy and Preview; Scale also has Uniform, Scale Corners and Scale Strokes & Effects;
//! Reflect's axis is Horizontal, Vertical or an angle). With Preview on
//! (`preview`, off when the dialog opens), the transform shows on the canvas as the values change.
use serde_json::{Value, json};
@ -27,8 +28,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
form::check(ui, d, "uniform", tl!("Uniform"));
scale_options(app, ui, d);
}
d.fields.insert("patterns".into(), Value::Bool(transform_patterns(app, d)));
form::check(ui, d, "patterns", tl!("Transform Patterns"));
object_options(app, ui, d);
form::check(ui, d, "copy", tl!("Copy (make a transformed copy)"));
let (cmd, mut params) = command(app, d);
if d.kind == "scale" {
@ -43,6 +43,26 @@ fn transform_patterns(app: &VectorcraftApp, d: &Dialog) -> bool {
d.fields.get("patterns").and_then(Value::as_bool).unwrap_or(app.session.prefs.transform_pattern_tiles)
}
/// Transform Objects: the dialog's field, on until it is switched off (the pattern tiles alone
/// transform then, with Transform Patterns).
fn transform_objects(d: &Dialog) -> bool {
d.fields.get("objects").and_then(Value::as_bool).unwrap_or(true)
}
/// The Transform Objects and Transform Patterns checkboxes (fields `objects` and `patterns`).
pub(super) fn object_options(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
d.fields.insert("objects".into(), Value::Bool(transform_objects(d)));
form::check(ui, d, "objects", tl!("Transform Objects"));
d.fields.insert("patterns".into(), Value::Bool(transform_patterns(app, d)));
form::check(ui, d, "patterns", tl!("Transform Patterns"));
}
/// Add the dialog's Transform Objects and Transform Patterns options to command `params`.
pub(super) fn add_object_options(app: &VectorcraftApp, d: &Dialog, params: &mut Value) {
params["objects"] = json!(transform_objects(d));
params["patterns"] = json!(transform_patterns(app, d));
}
/// The scale options: (dialog field, preference, label).
const SCALE_OPTIONS: [(&str, &str, &str); 2] = [("corners", "scaleCorners", "Scale Corners"), ("strokes", "scaleStrokes", "Scale Strokes & Effects")];
@ -88,22 +108,23 @@ fn origin_params(d: &Dialog, mut p: Value) -> Value {
/// The dialog's command and its params (Scale's without its scale options).
fn command(app: &VectorcraftApp, d: &Dialog) -> (&'static str, Value) {
let (copy, patterns) = (d.bool("copy"), transform_patterns(app, d));
let (id, params) = match d.kind.as_str() {
// Move has no reference point.
"move" => return ("object.move", json!({"dx": d.f64("dx", 0.0), "dy": d.f64("dy", 0.0), "copy": copy, "patterns": patterns})),
"rotate" => ("object.rotate", json!({"angle": d.f64("angle", 0.0), "copy": copy, "patterns": patterns})),
let (id, mut params) = match d.kind.as_str() {
"move" => ("object.move", json!({"dx": d.f64("dx", 0.0), "dy": d.f64("dy", 0.0)})),
"rotate" => ("object.rotate", json!({"angle": d.f64("angle", 0.0)})),
"scale" => {
let sx = d.f64("sx", 100.0);
let sy = if d.bool("uniform") { sx } else { d.f64("sy", 100.0) };
("object.scale", json!({"sx": sx, "sy": sy, "copy": copy, "patterns": patterns}))
("object.scale", json!({"sx": sx, "sy": sy}))
}
"reflect" => {
("object.reflect", json!({"axis": d.fields.get("axis").cloned().unwrap_or(json!("vertical")), "copy": copy, "patterns": patterns}))
}
_ => ("object.shear", json!({"angle": d.f64("angle", 0.0), "axis": d.str("axis"), "copy": copy, "patterns": patterns})),
// The axis by name, or the angle it is at.
"reflect" if d.str("axis") == "angle" => ("object.reflect", json!({"axis": d.f64("angle", 90.0)})),
"reflect" => ("object.reflect", json!({"axis": d.fields.get("axis").cloned().unwrap_or(json!("vertical"))})),
_ => ("object.shear", json!({"angle": d.f64("angle", 0.0), "axis": d.str("axis")})),
};
(id, origin_params(d, params))
params["copy"] = json!(d.bool("copy"));
add_object_options(app, d, &mut params);
// Move has no reference point.
if id == "object.move" { (id, params) } else { (id, origin_params(d, params)) }
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
@ -214,6 +235,32 @@ mod tests {
assert_eq!(size(&app), (60.0, 20.0));
}
/// #1460: Reflect's axis is Horizontal, Vertical or an angle, and Transform Objects goes with
/// the command (on until switched off).
#[test]
fn reflect_takes_an_angle_and_transform_objects() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 60, "height": 20})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
for (axis, objects, size) in [("angle", true, (20.0, 60.0)), ("vertical", false, (20.0, 60.0))] {
crate::menus::invoke(&mut app, "object.reflect", json!({}));
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| crate::dialogs::show(&mut app, ui.ctx()));
out.textures_delta.clear();
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!((&d.fields["objects"], &d.fields["angle"]), (&json!(true), &json!(90)));
d.fields.extend([("axis".to_string(), json!(axis)), ("angle".to_string(), json!(45)), ("objects".to_string(), json!(objects))]);
crate::dialogs::confirm(&mut app).unwrap();
let (cmd, p) = app.session.journal.last().unwrap();
let want = if axis == "angle" { json!(45.0) } else { json!(axis) };
assert_eq!((cmd.as_str(), &p["axis"], &p["objects"]), ("object.reflect", &want, &json!(objects)));
// Across the 45° axis the rectangle's sides swap; without Transform Objects it stays.
let b = app.session.doc().unwrap().doc.layers[0].children().unwrap()[0].geometric_bounds().unwrap();
assert_eq!((b.width().round(), b.height().round()), size);
}
}
/// Transform Patterns starts from General › Transform Pattern Tiles (#394) and goes with the
/// command.
#[test]

View File

@ -1,15 +1,15 @@
//! Object → Transform → Transform Each: scale, move and rotate every selected object about its own
//! reference point, with reflection, randomness, Scale Corners and Scale Strokes & Effects
//! (`object.transformEach`), previewed live on the canvas. Its Scale and Move sliders, Rotate dial
//! reference point, with reflection, randomness, Transform Objects, Transform Patterns, Scale
//! Corners and Scale Strokes & Effects (`object.transformEach`), previewed live on the canvas. Its Scale and Move sliders, Rotate dial
//! and reflection, reference point and Random options are the Transform effect's too.
//!
//! Fields: `scaleH`, `scaleV` (%), `moveH`, `moveV` (pt), `rotate` (°), `reflectX`, `reflectY`,
//! `random`, `reference` (0..8, the 9-point grid), `copy`, `corners`, `strokes` (default: the
//! preferences, which OK updates) and `preview`.
//! `random`, `reference` (0..8, the 9-point grid), `copy`, `objects`, `patterns`, `corners`,
//! `strokes` (the last two default to the preferences, which OK updates) and `preview`.
use serde_json::{Value, json};
use super::transform::{add_scale_options, scale_options, with_scale_options};
use super::transform::{add_object_options, add_scale_options, object_options, scale_options, with_scale_options};
use super::{DialogSpec, form};
use crate::VectorcraftApp;
use crate::state::Dialog;
@ -102,19 +102,23 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
ui.vertical(|ui| {
widgets::subheader(ui, tl!("Options"));
ui.add_space(4.0);
object_options(app, ui, d);
scale_options(app, ui, d);
reflect_options(ui, d);
form::check(ui, d, "copy", tl!("Copy"));
});
});
let mut p = params(d);
add_object_options(app, d, &mut p);
add_scale_options(app, d, &mut p);
form::preview(app, ui, d, "Transform Each", CMD, p);
false
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let p = with_scale_options(app, d, params(d))?;
let mut p = params(d);
add_object_options(app, d, &mut p);
let p = with_scale_options(app, d, p)?;
form::commit_preview(app, CMD, p)
}
@ -150,6 +154,6 @@ mod tests {
assert!((n.geometric_bounds().unwrap().width() - 100.0).abs() < 1e-6);
assert_eq!(n.appearance.stroke_width(), 1.0, "strokes kept their weight");
let (cmd, p) = app.session.journal.last().unwrap();
assert_eq!((cmd.as_str(), &p["strokes"]), (CMD, &json!(false)));
assert_eq!((cmd.as_str(), &p["strokes"], &p["objects"]), (CMD, &json!(false), &json!(true)), "#1448: Transform Objects goes along");
}
}

View File

@ -4679,3 +4679,4 @@ mark-style Japanese Japonské
@msg {label} requires a shape with at least two anchors {label} vyžaduje tvar alespoň se dvěma kotevními body
@msg The shape is too complex for a 3D effect Tvar je pro 3D efekt příliš složitý
@msg The 3D surface is too complex; simplify the shape 3D povrch je příliš složitý; zjednodušte tvar
Transform Objects Transformovat objekty

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4202,3 +4202,4 @@ theme Appearance Erscheinungsbild
@msg {label} requires a shape with at least two anchors {label} erfordert eine Form mit mindestens zwei Ankerpunkten
@msg The shape is too complex for a 3D effect Die Form ist für einen 3D-Effekt zu komplex
@msg The 3D surface is too complex; simplify the shape Die 3D-Oberfläche ist zu komplex; vereinfachen Sie die Form
Transform Objects Objekte transformieren

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4619,3 +4619,4 @@ mark-style Japanese Japonesas
@msg {label} requires a shape with at least two anchors {label} requiere una forma con al menos dos puntos de ancla
@msg The shape is too complex for a 3D effect La forma es demasiado compleja para un efecto 3D
@msg The 3D surface is too complex; simplify the shape La superficie 3D es demasiado compleja; simplifique la forma
Transform Objects Transformar objetos

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4255,3 +4255,4 @@ lighting Light Lumière
@msg {label} requires a shape with at least two anchors {label} nécessite une forme comportant au moins deux points d'ancrage
@msg The shape is too complex for a 3D effect La forme est trop complexe pour un effet 3D
@msg The 3D surface is too complex; simplify the shape La surface 3D est trop complexe ; simplifiez la forme
Transform Objects Transformer les objets

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4645,3 +4645,4 @@ theme Appearance Kinézet
@msg {label} requires a shape with at least two anchors {label} legalább két szerkesztőpontos alakzatot igényel
@msg The shape is too complex for a 3D effect Az alakzat túl összetett a 3D effektushoz
@msg The 3D surface is too complex; simplify the shape A 3D felület túl összetett; egyszerűsítse az alakzatot
Transform Objects Objektumok transzformálása

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4247,3 +4247,4 @@ lighting Light Luce
@msg {label} requires a shape with at least two anchors {label} richiede una forma con almeno due punti di ancoraggio
@msg The shape is too complex for a 3D effect La forma è troppo complessa per un effetto 3D
@msg The 3D surface is too complex; simplify the shape La superficie 3D è troppo complessa; semplificate la forma
Transform Objects Trasforma oggetti

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4241,3 +4241,4 @@ lighting Light ライト
@msg {label} requires a shape with at least two anchors {label} には 2 つ以上のアンカーポイントを持つシェイプが必要です
@msg The shape is too complex for a 3D effect シェイプが複雑すぎるため 3D 効果を適用できません
@msg The 3D surface is too complex; simplify the shape 3D サーフェスが複雑すぎます。シェイプを単純化してください
Transform Objects オブジェクトの変形

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4211,3 +4211,4 @@ lighting Light 광원
@msg {label} requires a shape with at least two anchors {label}에는 고정점이 두 개 이상인 모양이 필요합니다
@msg The shape is too complex for a 3D effect 모양이 너무 복잡하여 3D 효과를 적용할 수 없습니다
@msg The 3D surface is too complex; simplify the shape 3D 표면이 너무 복잡합니다. 모양을 단순화하십시오
Transform Objects 개체 변형

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3688,3 +3688,4 @@ axis Both Ambos
Create Compound Path From Ellipses Criar Caminho Composto a partir das Elipses
Fill Grid Preencher Grade
Select two or more objects, or a group Selecione dois ou mais objetos, ou um grupo
Transform Objects Transformar objetos

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -4243,3 +4243,4 @@ lighting Light Свет
@msg {label} requires a shape with at least two anchors {label}: требуется фигура не менее чем с двумя опорными точками
@msg The shape is too complex for a 3D effect Фигура слишком сложна для 3D-эффекта
@msg The 3D surface is too complex; simplify the shape 3D-поверхность слишком сложна; упростите фигуру
Transform Objects Трансформировать объекты

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -4227,3 +4227,4 @@ lighting Light Світло
@msg {label} requires a shape with at least two anchors {label}: потрібна фігура щонайменше з двома опорними точками
@msg The shape is too complex for a 3D effect Фігура надто складна для 3D-ефекту
@msg The 3D surface is too complex; simplify the shape 3D-поверхня надто складна; спростіть фігуру
Transform Objects Трансформувати об’єкти

Can't render this file because it has a wrong number of fields in line 13.

View File

@ -3838,3 +3838,4 @@ lighting Light 光源
@msg {label} requires a shape with at least two anchors {label}需要至少有两个锚点的形状
@msg The shape is too complex for a 3D effect 形状过于复杂,无法应用 3D 效果
@msg The 3D surface is too complex; simplify the shape 3D 表面过于复杂;请简化形状
Transform Objects 变换对象

Can't render this file because it contains an unexpected character in line 2 and column 73.

View File

@ -3619,3 +3619,4 @@ lighting Light 光源
@msg {label} requires a shape with at least two anchors {label}需要至少有兩個錨點的形狀
@msg The shape is too complex for a 3D effect 形狀過於複雜,無法套用 3D 效果
@msg The 3D surface is too complex; simplify the shape 3D 表面過於複雜;請簡化形狀
Transform Objects 變形物件

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -3017,7 +3017,7 @@ fn menu_dialog(app: &VectorcraftApp, id: &str) -> Option<(&'static str, Value)>
"object.rotate" => ("rotate", json!({"angle": 0})),
"object.scale" => ("scale", json!({"sx": 100, "sy": 100, "uniform": true})),
"object.transformEach" => (crate::dialogs::transform_each::KIND, crate::dialogs::transform_each::fields()),
"object.reflect" => ("reflect", json!({"axis": "vertical"})),
"object.reflect" => ("reflect", json!({"axis": "vertical", "angle": 90})),
"object.shear" => ("shear", json!({"angle": 0, "axis": "horizontal"})),
"path.average" => ("average", json!({"axis": "both"})),
// Offset Path opens on the values it was last confirmed with.