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:
commit
fcc52c3c7b
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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![],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!({}),
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue