mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Transform: Transform Again repeats the options of the transform it repeats
Transform Again kept only the last transform's matrix and its Copy flag. After a Selection tool handle drag resized area type, Transform Again scaled the type instead of resizing the type area again. After Move, Rotate, Scale, Reflect or Shear with Transform Patterns set differently from General › Transform Pattern Tiles, Transform Again followed the preference, and after a scale whose strokes or corners param differed from Scale Strokes & Effects or Scale Corners, it followed those preferences. A recorded Transform Again played back with the preferences in effect at playback, although its step records the strokes, corners and patterns values it used. last_transform now keeps the options with the matrix, as last_perspective keeps the perspective.transform params. apply_transform stores the copy, typeAreas and patterns values a transform command used, and strokes and corners when it scales. commit_interaction stores the params of a drag's last object.transform preview with the values that preview noted. object.transformAgain passes the options to the transform it repeats, and a true or false value given in its own params under one of those names takes precedence. A recorded step plays back with the values it records. Three engine tests fail without the change: Transform Again after a handle drag on area type, Transform Again after a Move with patterns: false while Transform Pattern Tiles is on and after a Scale with strokes: true while Scale Strokes & Effects is off, and a recorded Transform Again played back with Transform Pattern Tiles on. Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
This commit is contained in:
1 parent
f714a3f9ff
commit
ac8d7ebce8
8 files changed
+124
-13
No files matched your search
@@ -72,7 +72,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
shear
|
||||
),
|
||||
cmd!("object.transformAgain", "Transform Again", ["Object", "Transform"], Some("Cmd+D"), "{}", has_selection, transform_again),
|
||||
cmd!(
|
||||
"object.transformAgain",
|
||||
"Transform Again",
|
||||
["Object", "Transform"],
|
||||
Some("Cmd+D"),
|
||||
"{copy?, strokes?, corners?, patterns?, typeAreas?: bool (as object.transform's; default: the value the transform it repeats used, else object.transform's default)} the last transform's matrix on the selection (after a perspective move or scale: that plane-space transform, without these params)",
|
||||
has_selection,
|
||||
transform_again
|
||||
),
|
||||
cmd!(
|
||||
"object.nudge",
|
||||
"Nudge",
|
||||
@@ -253,8 +261,8 @@ pub(crate) fn transform_patterns(s: &mut Session, p: &Value, ids: &[NodeId]) ->
|
||||
}
|
||||
|
||||
/// Apply `xf` to `ids` (`copy` param: duplicate first; `strokes`/`corners`: see [`scaling`];
|
||||
/// `patterns`: see [`transform_patterns`]; `typeAreas`: see [`resize_type_area`]). Records
|
||||
/// Transform Again.
|
||||
/// `patterns`: see [`transform_patterns`]; `typeAreas`: see [`resize_type_area`]). Outside an
|
||||
/// interaction, records `xf` and the options it used for Transform Again.
|
||||
pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf: Affine, p: &Value) -> Result<Value> {
|
||||
let copy = bool_or(p, "copy", false);
|
||||
let areas = bool_or(p, "typeAreas", false);
|
||||
@@ -284,7 +292,12 @@ pub(crate) fn apply_transform(s: &mut Session, label: &str, ids: Vec<NodeId>, xf
|
||||
};
|
||||
let st = s.doc_mut()?;
|
||||
if st.interaction.is_none() {
|
||||
st.last_transform = Some((xf, copy));
|
||||
let mut options = json!({ "copy": copy, "typeAreas": areas, "patterns": sc.patterns });
|
||||
if Scaling::factor(xf).is_some() {
|
||||
options["strokes"] = json!(sc.strokes);
|
||||
options["corners"] = json!(!sc.keep_corners);
|
||||
}
|
||||
st.last_transform = Some((xf, options));
|
||||
st.last_perspective = None;
|
||||
}
|
||||
Ok(json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))
|
||||
@@ -393,13 +406,18 @@ fn shear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
apply_transform(s, "Shear", ids, about(o, m), p)
|
||||
}
|
||||
|
||||
fn transform_again(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
/// The last transform's matrix on the selection, with the options `last_transform` keeps for it; an
|
||||
/// option that `p` sets to true or false (a recorded Transform Again's) takes precedence.
|
||||
fn transform_again(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(again) = s.doc()?.last_perspective.clone() {
|
||||
return super::distortcmds::transform_again(s, &again);
|
||||
}
|
||||
let (m, copy) = s.doc()?.last_transform.ok_or_else(|| EngineError::Other("no previous transform".into()))?;
|
||||
let (m, mut options) = s.doc()?.last_transform.clone().ok_or_else(|| EngineError::Other("no previous transform".into()))?;
|
||||
if let (Some(o), Some(given)) = (options.as_object_mut(), p.as_object()) {
|
||||
o.extend(given.iter().filter(|(_, v)| v.is_boolean()).map(|(k, v)| (k.clone(), v.clone())));
|
||||
}
|
||||
let ids = selected_roots(s)?;
|
||||
apply_transform(s, "Transform Again", ids, m, &json!({ "copy": copy }))
|
||||
apply_transform(s, "Transform Again", ids, m, &options)
|
||||
}
|
||||
|
||||
enum Arrange {
|
||||
|
||||
@@ -133,8 +133,11 @@ pub struct DocState {
|
||||
pub interaction: Option<Interaction>,
|
||||
/// Edits made while this is open are one undo step (a scrubbed numeric field).
|
||||
pub undo_group: Option<UndoGroup>,
|
||||
/// For Object → Transform → Transform Again (⌘D).
|
||||
pub last_transform: Option<(Affine, bool)>,
|
||||
/// For Object → Transform → Transform Again (⌘D): the last transform's matrix and the options
|
||||
/// Transform Again passes with it. A transform command keeps the `copy`, `typeAreas` and
|
||||
/// `patterns` it used, and `strokes` and `corners` when it scales; a drag keeps the params of its
|
||||
/// last preview with the values that preview noted ([`Session::note_journal`]).
|
||||
pub last_transform: Option<(Affine, Value)>,
|
||||
/// What Select → Reselect repeats: the last selection command, its params and the objects
|
||||
/// selected when it ran (a Same command's reference objects).
|
||||
pub last_selection_cmd: Option<(String, Value, Vec<NodeId>)>,
|
||||
@@ -1388,7 +1391,14 @@ impl Session {
|
||||
if preview.0 == "object.transform" {
|
||||
let m = cmd::matrix_param(&preview.1, "matrix");
|
||||
if let Some(m) = m {
|
||||
st.last_transform = Some((m, copy));
|
||||
// The options are the last preview's params with the values it noted, without the
|
||||
// matrix and the ids.
|
||||
let mut options = preview.1.clone();
|
||||
if let Some(o) = options.as_object_mut() {
|
||||
o.remove("matrix");
|
||||
o.remove("ids");
|
||||
}
|
||||
st.last_transform = Some((m, options));
|
||||
st.last_perspective = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,7 +712,7 @@ 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, false)));
|
||||
assert_eq!(s.doc().unwrap().last_transform, Some((Affine::IDENTITY, json!({"copy": false, "typeAreas": false, "patterns": false}))));
|
||||
// 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");
|
||||
|
||||
@@ -540,6 +540,66 @@ fn transform_pattern_tiles_is_the_transforms_default() {
|
||||
assert_eq!(tiles(&s), [0.5, 0.0, 0.0, 0.5, 5.0, 0.0]);
|
||||
}
|
||||
|
||||
/// A 50 pt square at (100, 100) with a 1 pt stroke, filled with a 20 pt pattern, selected.
|
||||
fn patterned_square(s: &mut Session) -> NodeId {
|
||||
let tile = square(s, 0.0, 0.0);
|
||||
s.execute("select.set", &json!({"ids": [tile.0]})).unwrap();
|
||||
s.execute("object.pattern.make", &json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
|
||||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||||
let id = square(s, 100.0, 100.0);
|
||||
s.execute("paint.setFill", &json!({"ids": [id.0], "swatch": "Dots"})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
id
|
||||
}
|
||||
|
||||
/// The tile transform of the pattern fill of `id`.
|
||||
fn pattern_tiles(s: &Session, id: NodeId) -> [f64; 6] {
|
||||
match s.doc().unwrap().doc.node(id).unwrap().appearance.fill_paint() {
|
||||
vectorcraft_color::Paint::Pattern { xf, .. } => xf.as_coeffs(),
|
||||
p => panic!("not a pattern: {p:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform Again repeats the options of the transform it repeats, whatever the preferences say:
|
||||
/// a Move with Transform Patterns off while Transform Pattern Tiles is on, and a Scale with Scale
|
||||
/// Strokes & Effects on while its preference is off.
|
||||
#[test]
|
||||
fn transform_again_repeats_the_options_of_the_transform_it_repeats() {
|
||||
let mut s = new_doc();
|
||||
let id = patterned_square(&mut s);
|
||||
set_pref(&mut s, "transformPatternTiles", json!(true));
|
||||
s.execute("object.move", &json!({"dx": 10, "dy": 0, "patterns": false})).unwrap();
|
||||
s.execute("object.transformAgain", &json!({})).unwrap();
|
||||
assert_eq!(top_left(&s, id), (120.0, 100.0), "the square moved twice");
|
||||
assert_eq!(pattern_tiles(&s, id), [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], "the tiles stay");
|
||||
assert_eq!(s.journal.last().unwrap().1, json!({"patterns": false}), "the journal keeps the choice");
|
||||
s.execute("object.scale", &json!({"sx": 200, "strokes": true})).unwrap();
|
||||
s.execute("object.transformAgain", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.node(id).unwrap().appearance.stroke_width(), 4.0, "the 1 pt stroke scaled twice");
|
||||
}
|
||||
|
||||
/// A recorded Transform Again plays back with the Transform Patterns its step records, whatever the
|
||||
/// transform it repeats used.
|
||||
#[test]
|
||||
fn a_recorded_transform_again_plays_back_with_its_transform_patterns() {
|
||||
let mut s = new_doc();
|
||||
let id = patterned_square(&mut s);
|
||||
s.execute("object.move", &json!({"dx": 10, "dy": 0})).unwrap();
|
||||
let start = s.journal.len();
|
||||
s.execute("object.transformAgain", &json!({})).unwrap();
|
||||
let action = s.journal_for_action(start);
|
||||
assert_eq!(action, [("object.transformAgain".to_string(), json!({"patterns": false}))], "recorded with the preference off");
|
||||
// With the preference on, a Move moves the tiles, and the recorded Transform Again leaves them.
|
||||
set_pref(&mut s, "transformPatternTiles", json!(true));
|
||||
s.execute("object.move", &json!({"dx": 10, "dy": 0})).unwrap();
|
||||
assert_eq!(pattern_tiles(&s, id), [1.0, 0.0, 0.0, 1.0, 10.0, 0.0]);
|
||||
for (cmd, p) in &action {
|
||||
s.execute(cmd, p).unwrap();
|
||||
}
|
||||
assert_eq!(top_left(&s, id), (140.0, 100.0), "the square moved");
|
||||
assert_eq!(pattern_tiles(&s, id), [1.0, 0.0, 0.0, 1.0, 10.0, 0.0], "the tiles stay");
|
||||
}
|
||||
|
||||
/// General › Transform Pattern Tiles (#394) applies to Align and Distribute as it does to the
|
||||
/// transforms: off, the tiles of a pattern-filled square stay in place when the square moves;
|
||||
/// on, they move with it, and `patterns: false` keeps them in place for one call.
|
||||
|
||||
@@ -108,6 +108,24 @@ fn a_handle_drag_resizes_the_type_area_and_the_text_reflows() {
|
||||
assert!(layout(&s, id).overflow);
|
||||
}
|
||||
|
||||
/// Transform Again after a handle drag on area type repeats the drag: it resizes the type area
|
||||
/// again, and the type keeps its size.
|
||||
#[test]
|
||||
fn transform_again_after_a_handle_drag_resizes_the_type_area_again() {
|
||||
let mut s = session();
|
||||
let id = area(&mut s, 40.0, 40.0, 120.0, 40.0, STORY);
|
||||
sel(&mut s, &[id]);
|
||||
// The bottom-right handle, from (160, 80) to (200, 140): 4/3 as wide and 2.5 times as tall.
|
||||
drag(&mut s, "selection", Point::new(160.0, 80.0), Point::new(200.0, 140.0));
|
||||
s.execute("object.transformAgain", &json!({})).unwrap();
|
||||
assert_eq!(last_step(&s), "Transform Again");
|
||||
let r = frame(&s, id);
|
||||
assert!(near(r.origin(), Point::ZERO) && (r.width() - 640.0 / 3.0).abs() < 1e-6 && (r.height() - 250.0).abs() < 1e-6, "{r:?}");
|
||||
let t = text(&s, id);
|
||||
assert_eq!(t.xf, Affine::translate((40.0, 40.0)), "the type isn't scaled");
|
||||
assert!(t.runs.iter().all(|r| r.style.size == 12.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_type_still_scales_with_its_handles() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -1444,6 +1444,10 @@ its strokes' effects (drop shadow offsets and blur, offsets, radii…; `effect.l
|
||||
relative Roughen, Tweak and Zig Zag sizes stay percentages), in groups and layers too. With it off, nothing painted
|
||||
changes size, and type keeps its character strokes' weight. With Scale Corners off, live corner radii keep their
|
||||
size. The journal entry of a scaling command records the `strokes` and `corners` it used, so actions replay alike.
|
||||
`object.transformAgain` applies the last transform's matrix with the `copy`, `strokes`, `corners`, `patterns` and
|
||||
`typeAreas` values that transform used, and a true or false value given in its own params takes precedence. After a
|
||||
drag of art without a pattern, `patterns` follows the preference `transformPatternTiles`. Its journal entry records
|
||||
the `strokes`, `corners` and `patterns` it used, as a transform's does.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"object.scale","params":{"sx":200,"strokes":true}}}
|
||||
|
||||
@@ -322,7 +322,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||
| Live vector effects | 5 | 85% | Outline Object, Pathfinder Hard/Soft Mix and Trap, SVG Filters, the Transform effect's Transform Objects/Patterns and Scale Strokes & Effects options | 6–10 |
|
||||
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 35% | ~36 Photoshop-style filters (Artistic, Brush Strokes, Sketch, Stylize), Smart Blur's Edge Only / Overlay Edge modes, Glass's and Texturizer's Load Texture and the Effect Gallery; Document Raster Effects Settings, raster effects in PDF, the pixel-filter pipeline, Radial Blur, Smart Blur, Unsharp Mask, Glowing Edges, Distort (Diffuse Glow, Glass, Ocean Ripple), Pixelate (Color Halftone, Crystallize, Mezzotint, Pointillize), Texture (Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer) and Video (De-Interlace, NTSC Colors) are done | 19–31 |
|
||||
| 3D and Materials | 4 | ~8% | Initial Revolve only, with simple lighting and Revolve-specific visible-surface expansion; Extrude & Bevel, Inflate, Rotate, caps, materials, shadows and more complete visibility remain | 45–75 |
|
||||
| Type core | 9 | 80% | Middle Eastern features beyond bidi layout and Paragraph Direction (Character Direction, kashidas, the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); Hyphenation… and Justification… options (Every-line Composer for justified and ragged text, per object, is done; it is tuned by eye, not yet compared side by side with Illustrator; mixed type sizes in a paragraph still break single-line), Optical Margin Alignment, paragraph spacing limit for vertically justified area type, hidden characters, paragraph attributes in the Type tool's copy and paste (the characters keep their styles, the paragraphs take the destination's), mixed-value display in the Paragraph panel for a selection across differing paragraphs, Transform Again after a type-area resize (it scales the type), partial selection of type-area anchors by marquee, optional font-size preservation during group scaling (scaled character controls fixed in #1034) | 14–19 |
|
||||
| Type core | 9 | 80% | Middle Eastern features beyond bidi layout and Paragraph Direction (Character Direction, kashidas, the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); Hyphenation… and Justification… options (Every-line Composer for justified and ragged text, per object, is done; it is tuned by eye, not yet compared side by side with Illustrator; mixed type sizes in a paragraph still break single-line), Optical Margin Alignment, paragraph spacing limit for vertically justified area type, hidden characters, paragraph attributes in the Type tool's copy and paste (the characters keep their styles, the paragraphs take the destination's), mixed-value display in the Paragraph panel for a selection across differing paragraphs, partial selection of type-area anchors by marquee, optional font-size preservation during group scaling (scaled character controls fixed in #1034) | 14–19 |
|
||||
| Type advanced | 4 | 50% | CJK composition for vertical type (ruby, mojikumi beyond Line-end Punctuation Half Width — the other sets (and their paragraph-start bracket and indent rules), custom kinsoku sets (beyond Hard, Soft and None) and the Mojikumi Settings dialog —, manual tate-chu-yoko), variable font axis sliders (named instances are styles), tab leaders, spell check (open dictionary), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support (beyond parity, not scored: inline symbols in text) | 20–28 |
|
||||
| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | symbol libraries (original), dynamic symbols, envelope Anti-Alias/Preserve Shape output and warp point editing, brush interpolation in blends (M8 fidelity pass done) | 5–8 |
|
||||
| Image Trace, graphs, image tools | 3 | 72% | graph Column designs (Graph Design, Marker designs and series colours set with Group Selection are done), Create Object Mosaic, per-feature stroke/fill choice and varying widths in Image Trace's Create Strokes (Vector Halftone and the Image Trace View modes, Palette and Create Strokes are done) | 6–10 |
|
||||
@@ -432,6 +432,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Cutting:** Eraser, Scissors, Knife, Mirror & Cut, Line Cut and Rectangle Cut (real geometry, compound paths keep their holes).
|
||||
- **Transform:** Rotate, Reflect, Scale, Shear (click or Alt-click snaps the reference point to anchors and centres; Alt-click opens the tool's dialog, and a drag with Alt down at the release, from the press or pressed mid-drag, transforms a copy), Reshape, Free Transform (free and perspective distort and shear, switched by the keys held during a handle drag).
|
||||
Align, Distribute, Distribute Spacing, Reflect and Shear return an error for any `horizontal`, `vertical`, `to`, `axis`, `bounds` or `spacing` value they don't take, listing the choices, and Align returns one when a call gives neither `horizontal` nor `vertical`; nothing moves and no undo step is recorded.
|
||||
Transform Again uses the Copy, Transform Patterns, Scale Strokes & Effects and Scale Corners choices of the transform it repeats; after a bounding-box handle drag on area type, it resizes the type area again and the type keeps its size (`object.transformAgain`).
|
||||
- **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout; with Direct Selection, select corner anchors to show and round just those corners (#381). Each corner keeps its own radius and kind (Round, Inverted Round, Chamfer): Alt-click a widget to cycle the kind, double-click it for the Corners dialog, and the Properties and Transform radius fields set the selected corners (`object.setLiveShape {corners, radius, kind}`). Corners stay circular when the rectangle is scaled unevenly or along its own sides when rotated (the radius keeps its size, or scales by the mean scale with Scale Corners on); only a shear distorts them. On a rectangle that isn't square every corner is the same circle: a radius past half the shorter side stops there for all of them, the widgets sit on the corners as drawn and turn the edited corners red at that limit, CSS export writes the drawn radii, and rectangles saved stretched by older versions get circular corners when their corners are next set (#442). Live Corners work on any path's corners too, as on a rectangle's (#511): a star's, a live polygon's (which also shows them to the Selection tool, has a Corner Radius field in the Properties and Transform panels and keeps its radius as its sides change) and a pen path's, wherever two sides meet at an angle at an anchor that isn't smooth: curved sides too, trimmed to what the cut leaves of them (#1144). Direct Selection shows the widgets of every corner, or of the corners holding selected anchors; each corner is a circle tangent to both sides, drawn no larger than takes its cut halfway along its shorter side; a path keeps its uncut outline so its corners stay editable, and the same command, dialog, angle preference and View › Hide Corner Widget apply.
|
||||
- **Pen:** Alt pressed in the middle of dragging out an anchor splits its handles and keeps the curve already shaped into it (only the outgoing handle follows the pointer); Space held while dragging out an anchor moves it, handles and all. A press on the first anchor of the path being drawn closes it whatever the drag then does: the drag shapes the closing curve (symmetric handles, Alt breaks them) and the release closes the path wherever it ends (#501). Clicking the last anchor of the path being drawn retracts its outgoing handle (convert cursor), and dragging from it pulls out a new one on its own. Auto Add/Delete: over a selected path, a click on a segment adds an anchor and a click on an anchor deletes it (add and delete cursors); Shift held, or Preferences › General › Disable Auto Add/Delete, starts a new path instead. Alt held over a handle end or an anchor of a selected path (the one being drawn too) works as the Anchor Point tool, with its convert cursor: dragging a handle moves it alone, clicking a smooth anchor makes it a corner, dragging an anchor pulls out new symmetric handles, dragging a segment reshapes it; drawing then goes on. Cmd (Ctrl) held drags with the selection tool chosen last, Direct Selection until one is (handles, anchors and segments of the path being drawn or another), and the Pen then goes on drawing its path while that stays selected; the pointer shows that tool's while Cmd is held. The same holds for agents (`pointer_gesture` with `mods.cmd`), as the borrowing now lives in the engine (#494).
|
||||
- **Direct Selection (#1278):** clicking a segment picks its connection, with hollow endpoint anchors and only that curve's direction handles. Delete leaves the neighboring curves and appearance intact, opens/splits contours in one undo step, and another Delete removes the remaining selected path. A whole-path drag and deliberate anchor selection keep their existing behavior; egui frames and native control/art checked, OS mouse/visual QA pending.
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Free Transform, Scale and Reflect along a rotated box | missing | |
|
||||
| Free Transform widget: constrain, free distort, perspective distort; the key-held variants | done | |
|
||||
| Transform tools: click sets the reference point (snaps to anchors/centres), Alt-click opens the dialog, Alt-release copies | done | |
|
||||
| Transform Again, Transform Each, Scale Strokes & Effects, Scale Corners | done | Transform Again after a type-area resize scales the type (gap) |
|
||||
| Transform Again, Transform Each, Scale Strokes & Effects, Scale Corners | done | |
|
||||
| Reference point locator (nine squares) in Control bar and Transform panel | done | |
|
||||
|
||||
## Nudging, numeric entry and precision
|
||||
|
||||
Reference in new issue
Block a user