mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
fix(G11): Reflect X/Y flip the correct axes (#1910)
Transform Each had Reflect X and Reflect Y swapped relative to Effect › Distort › Transform and the Transform panel. Properties flip buttons also used inverted tooltips. Align them so Horizontal is left-right and Vertical is top-bottom. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Cursor
parent
a8e9b9d86e
commit
c53e161e37
@@ -76,7 +76,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), 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}",
|
||||
"{scaleH?: % (100), scaleV?: % (100), moveH?: pt, moveV?: pt (down = +), rotate?: deg (counter-clockwise), reflectX?: bool (flip left-right, as Reflect X in the Transform Each dialog), reflectY?: bool (flip top-bottom, as Reflect Y), 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
|
||||
),
|
||||
@@ -363,7 +363,9 @@ fn transform_each_as(s: &mut Session, p: &Value, label: &str) -> Result<Value> {
|
||||
k_rot = rot * (rng.next() * 2.0 - 1.0);
|
||||
}
|
||||
let o = vectorcraft_geom::reference_point(b, refi);
|
||||
let refl = Affine::scale_non_uniform(if ry { -1.0 } else { 1.0 }, if rx { -1.0 } else { 1.0 });
|
||||
// Reflect X / Reflect Y match Object › Transform › Transform Each and Effect › Distort › Transform:
|
||||
// Reflect X flips left-right (negate x), Reflect Y flips top-bottom (negate y).
|
||||
let refl = Affine::scale_non_uniform(if rx { -1.0 } else { 1.0 }, if ry { -1.0 } else { 1.0 });
|
||||
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) {
|
||||
|
||||
@@ -357,6 +357,36 @@ fn transform_each_random_is_deterministic_with_seed() {
|
||||
assert!(x.x0.abs() <= 50.0 && x.width() == 100.0);
|
||||
}
|
||||
|
||||
/// #1910: Transform Each's Reflect X flips left-right; Reflect Y flips top-bottom (same as
|
||||
/// Effect › Distort › Transform and Illustrator's Transform Each checkboxes).
|
||||
#[test]
|
||||
fn transform_each_reflect_x_flips_left_right_reflect_y_flips_top_bottom() {
|
||||
let mut s = session();
|
||||
// A right-pointing triangle: tip at x=100, base on the left.
|
||||
let id =
|
||||
id_of(&s.execute("path.create", &json!({"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 50}, {"x": 0, "y": 100}], "closed": true})).unwrap());
|
||||
sel(&mut s, &[id]);
|
||||
s.execute("object.transformEach", &json!({"reflectX": true})).unwrap();
|
||||
let tip_x = match &node(&s, id).kind {
|
||||
// The tip is the only vertex at y=50; Reflect X about the centre maps (100, 50) → (0, 50).
|
||||
NodeKind::Path { path, .. } => path.subpaths[0].anchors.iter().find(|a| (a.p.y - 50.0).abs() < 1e-6).map(|a| a.p.x).unwrap_or(f64::NAN),
|
||||
_ => panic!("expected a path"),
|
||||
};
|
||||
assert!((tip_x - 0.0).abs() < 1e-6, "reflectX must move the tip from x=100 to x=0, got {tip_x}");
|
||||
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
// A down-pointing tip at (50, 100); Reflect Y about the centre (50, 50) maps it to (50, 0).
|
||||
let id2 =
|
||||
id_of(&s.execute("path.create", &json!({"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 50, "y": 100}], "closed": true})).unwrap());
|
||||
sel(&mut s, &[id2]);
|
||||
s.execute("object.transformEach", &json!({"reflectY": true})).unwrap();
|
||||
let tip_y = match &node(&s, id2).kind {
|
||||
NodeKind::Path { path, .. } => path.subpaths[0].anchors.iter().find(|a| (a.p.x - 50.0).abs() < 1e-6).map(|a| a.p.y).unwrap_or(f64::NAN),
|
||||
_ => panic!("expected a path"),
|
||||
};
|
||||
assert!((tip_y - 0.0).abs() < 1e-6, "reflectY must move the tip from y=100 to y=0, got {tip_y}");
|
||||
}
|
||||
|
||||
/// Transform Each with its default values, or with any values that move nothing (both scales 100%,
|
||||
/// no move, rotation or reflection, Random or not), records no undo step and replies with the
|
||||
/// objects. With only a layer selected there is no object to transform: with Copy or without, it
|
||||
|
||||
@@ -454,12 +454,12 @@ pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
app.run("object.rotate", json!({"angle": a, "absolute": true})).ok();
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
// Lucide's flip icons are named for their mirror line: flip-vertical-2 draws a vertical
|
||||
// line (the left-right flip), flip-horizontal-2 a horizontal one (top-bottom).
|
||||
if widgets::icon_button(ui, "flip-vertical-2", tl!("Flip Along Horizontal Axis"), false, 24.0).clicked() {
|
||||
// Same mapping as the Transform panel (#1910): Lucide flip-vertical-2 is the vertical
|
||||
// mirror line (Flip Horizontal / left-right); flip-horizontal-2 is Flip Vertical.
|
||||
if widgets::icon_button(ui, "flip-vertical-2", tl!("Flip Horizontal"), false, 24.0).clicked() {
|
||||
app.run("object.reflect", json!({"axis": "vertical"})).ok();
|
||||
}
|
||||
if widgets::icon_button(ui, "flip-horizontal-2", tl!("Flip Along Vertical Axis"), false, 24.0).clicked() {
|
||||
if widgets::icon_button(ui, "flip-horizontal-2", tl!("Flip Vertical"), false, 24.0).clicked() {
|
||||
app.run("object.reflect", json!({"axis": "horizontal"})).ok();
|
||||
}
|
||||
// More Options: the Transform panel's Scale Corners and Scale Strokes & Effects, open
|
||||
|
||||
@@ -151,6 +151,9 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
closed, the document left modified (#1865, #1685).
|
||||
- **Done:** the Shape Builder offers the empty areas closed shapes enclose as regions, such as the middle of four
|
||||
circles that touch, and merges them with the regions around them (#1691). Gap Detection is still missing.
|
||||
- **Done (#1910):** Transform Each's Reflect X flips left-right and Reflect Y flips top-bottom (aligned with
|
||||
Effect › Distort › Transform). Properties flip buttons use the same Flip Horizontal / Flip Vertical labels as
|
||||
the Transform panel.
|
||||
- **Missing (noted for reporters, 2026-10-14):** deleting an interior anchor of an open path joins across the gap,
|
||||
where the documented behaviour removes the point and its connecting segments. Inside the estimate below.
|
||||
- **Missing:** Shape Builder misses planar regions and targets whole shapes (#937); Transform effect dialog's invisible checkboxes and missing options (#885);
|
||||
@@ -292,6 +295,7 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | G11: Transform Each Reflect X/Y and Properties flip labels match left-right / top-bottom (#1910). Estimates unchanged. |
|
||||
| 2026-10-14 | minor | Quick Image Trace presets reveal the adjustment panel with Advanced open; Properties reopens it. Sliders keep their preset row until release so the re-trace is committed. UI regressions added; scores and remaining estimates unchanged. |
|
||||
| 2026-10-14 | minor | Revolve: automatic smooth vector shading, default-on Cap with continuous curved rims, and bounded seam joins; angular cut sides remain open. Scores and hour estimates unchanged at this rounding |
|
||||
| 2026-10-14 | minor | No entry closed. G8 and G11 record the later merges of 2026-10-14 (#1852, #1862–#1865, #1871–#1873, #1839, #1840, #1856); #1683 (Dim Images) and #1685 (failed-save dialog) are done by #1864 and #1865. Gaps noted for reporters: G1 Effect › Warp through Warp Options (#1760), Color Picker Tab order (#1692), aligning to a key anchor (#1636; the public Align page does not say how the key anchor is chosen); G7 custom toolbars (#1759), workspace panel widths (#1753); G11 Shape Builder regions between closed shapes (#1691), deleting an interior anchor of an open path; G13 Intertwine (#1763), gradient mesh from an embedded image (#1676), Image Trace radial gradients (draft #1849). Average and Join with a corner or smooth choice was already listed (G1). G12: a line two merges had left twice is there once, without Appearance of Black for output, which acts since #1750. Estimates unchanged |
|
||||
|
||||
Reference in New Issue
Block a user