mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 20:08:42 +00:00
Free Transform: Constrain keeps a Free Distort corner on its diagonal
The widget's Constrain button was grayed out in Free Distort, and neither it nor Shift changed a Free Distort corner drag. Constrain is now available in Free Transform and Free Distort, and stays unavailable in Perspective Distort. With Constrain on, or Shift held, a corner dragged in Free Distort moves only along the line through the opposite corner, in the box's own proportions, and the other three corners stay in place. On a turned box the line runs through the box's own corners. A box with no size leaves the corner where it is. Cmd/Ctrl+Alt+Shift on a corner still drags in Perspective Distort. The measurement label shows how far the corner moved.
This commit is contained in:
@@ -600,6 +600,35 @@ fn free_transform_distort_mode_via_pointer() {
|
||||
assert_eq!(s.journal.last().map(|j| j.0.as_str()), Some("object.distort"));
|
||||
}
|
||||
|
||||
/// With Constrain on, a Free Distort corner keeps to the diagonal through the opposite corner, in
|
||||
/// one undo step that undoes and redoes whole.
|
||||
#[test]
|
||||
fn free_transform_constrained_distort_keeps_the_corner_on_its_diagonal() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
|
||||
let v = ViewInfo::default();
|
||||
s.select_tool("freeTransform", v).unwrap();
|
||||
s.set_tool_option("mode", &json!("distort"));
|
||||
s.set_tool_option("constrain", &json!(true));
|
||||
let before = undo_len(&s);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 300.0, 200.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 320.0, 230.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 340.0, 260.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 340.0, 260.0), v).unwrap();
|
||||
let pts = |s: &Session| -> Vec<Point> { s.doc().unwrap().doc.node(a).unwrap().path_data().unwrap().anchors().map(|(_, _, an)| an.p).collect() };
|
||||
let has_all = |pts: &[Point], want: [(f64, f64); 4]| want.iter().all(|&(x, y)| pts.iter().any(|q| q.distance(Point::new(x, y)) < 1e-6));
|
||||
// (40, 60) along the diagonal (200, 100) is 0.28 of it, (56, 28).
|
||||
let distorted = [(100.0, 100.0), (300.0, 100.0), (356.0, 228.0), (100.0, 200.0)];
|
||||
let original = [(100.0, 100.0), (300.0, 100.0), (300.0, 200.0), (100.0, 200.0)];
|
||||
assert!(has_all(&pts(&s), distorted), "{:?}", pts(&s));
|
||||
assert_eq!(undo_len(&s), before + 1);
|
||||
assert_eq!(s.journal.last().map(|j| j.0.as_str()), Some("object.distort"));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(has_all(&pts(&s), original), "{:?}", pts(&s));
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert!(has_all(&pts(&s), distorted), "{:?}", pts(&s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_transform_cmd_held_once_a_corner_drag_started_distorts_in_one_step() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
//! Alt from centre), outside the corners rotates, inside moves, and Cmd-dragging a side handle
|
||||
//! shears along that side. A scale snaps as the Selection tool's does. The Perspective Distort and Free Distort modes (tool option `mode`, or
|
||||
//! Cmd / Cmd+Alt+Shift while dragging a corner like classic Illustrator) move the corners of the
|
||||
//! box and preview `object.distort`. After a rotation the box stays turned with the selection, as
|
||||
//! the Selection tool's does: its handles scale, shear and distort along the selection's own sides.
|
||||
//! box and preview `object.distort`; in Free Distort, Shift or the Constrain option keeps a dragged
|
||||
//! corner on the diagonal through the opposite corner. After a rotation the box stays turned with
|
||||
//! the selection, as the Selection tool's does: its handles scale, shear and distort along the selection's own sides.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::OrientedBox;
|
||||
@@ -123,6 +124,18 @@ fn corner_index(h: Handle) -> Option<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The part of drag `d` that lies along the diagonal from corner `h` of `r` to the opposite
|
||||
/// corner. A Free Distort corner dragged with Shift or Constrain moves only along that line, so its
|
||||
/// offset from the opposite corner keeps the box's width-to-height ratio. A box with no width and no height gives no movement.
|
||||
pub fn along_diagonal(r: Rect, h: Handle, d: Vec2) -> Vec2 {
|
||||
let u = h.pos(r) - h.opposite().pos(r);
|
||||
let len2 = u.hypot2();
|
||||
if len2 <= 1e-18 || !len2.is_finite() {
|
||||
return Vec2::ZERO;
|
||||
}
|
||||
u * (d.dot(u) / len2)
|
||||
}
|
||||
|
||||
/// The distorted quad for dragging handle `h` of `r` by `d`.
|
||||
pub fn distort_quad(r: Rect, h: Handle, d: Vec2, mode: DistortMode) -> [Point; 4] {
|
||||
let mut q = rect_corners(r);
|
||||
@@ -194,9 +207,13 @@ impl FreeTransformTool {
|
||||
self.guides.clear();
|
||||
let xf = match d.op {
|
||||
Op::Corners(h, mode) => {
|
||||
// Shift or Constrain keeps a Free Distort corner on its diagonal.
|
||||
let constrained = shift && mode == DistortMode::Distort && h.is_corner();
|
||||
let local = if constrained { along_diagonal(bx.rect, h, local) } else { local };
|
||||
let q = distort_quad(bx.rect, h, local, mode).map(|c| bx.to_doc() * c);
|
||||
self.quad = Some(q);
|
||||
self.measure = cx.measurement_labels.then(|| (p, cx.offset_label(delta.x, delta.y)));
|
||||
let moved = if constrained { bx.to_doc() * (h.pos(bx.rect) + local) - bx.to_doc() * h.pos(bx.rect) } else { delta };
|
||||
self.measure = cx.measurement_labels.then(|| (p, cx.offset_label(moved.x, moved.y)));
|
||||
let corners: Vec<Value> = q.iter().map(|c| json!([c.x, c.y])).collect();
|
||||
let r = bx.rect;
|
||||
let mut params = json!({ "corners": corners, "from": [r.x0, r.y0, r.x1, r.y1] });
|
||||
@@ -571,6 +588,123 @@ mod tests {
|
||||
assert_eq!(q[2], Point::new(10.0, 10.0));
|
||||
}
|
||||
|
||||
/// The corners an `object.distort` preview sends.
|
||||
fn sent_corners(a: &[Action]) -> Vec<Point> {
|
||||
let Some(Action::Preview(c, v)) = a.last() else { panic!("{a:?}") };
|
||||
assert_eq!(c, "object.distort");
|
||||
v["corners"].as_array().unwrap().iter().map(|p| Point::new(p[0].as_f64().unwrap(), p[1].as_f64().unwrap())).collect()
|
||||
}
|
||||
|
||||
fn assert_corners(got: &[Point], want: [(f64, f64); 4]) {
|
||||
for (g, w) in got.iter().zip(want) {
|
||||
assert!(g.distance(Point::new(w.0, w.1)) < 1e-9, "{got:?} != {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A 200 × 100 rectangle at (100, 100)–(300, 200), selected.
|
||||
fn wide_rect() -> (vectorcraft_doc::Document, Selection, vectorcraft_doc::NodeId) {
|
||||
let (mut d, id) = doc_with_rect();
|
||||
let wide = Affine::translate((100.0, 100.0)) * Affine::scale_non_uniform(2.0, 1.0) * Affine::translate((-100.0, -100.0));
|
||||
d.node_mut(id).unwrap().transform(wide, false);
|
||||
let mut s = Selection::default();
|
||||
s.add(id);
|
||||
(d, s, id)
|
||||
}
|
||||
|
||||
/// In Free Distort, Shift or the Constrain option keeps the dragged corner on the diagonal
|
||||
/// through the opposite corner; the other three corners stay put.
|
||||
#[test]
|
||||
fn free_distort_corner_keeps_to_its_diagonal_with_shift_or_constrain() {
|
||||
let (d, s, _) = wide_rect();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
// The bottom-right corner dragged by (40, 60): along the diagonal (200, 100) that is
|
||||
// (40·200 + 60·100) / (200² + 100²) = 0.28 of it, (56, 28).
|
||||
let fixed = [(100.0, 100.0), (300.0, 100.0), (356.0, 228.0), (100.0, 200.0)];
|
||||
let drag = |t: &mut FreeTransformTool, m: Mods| {
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 300.0, 200.0).with_mods(m));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 340.0, 260.0).with_mods(m));
|
||||
t.pointer(&cx, &ev(PointerKind::Up, 340.0, 260.0).with_mods(m));
|
||||
sent_corners(&a)
|
||||
};
|
||||
let mut t = FreeTransformTool::default();
|
||||
t.set_option("mode", &json!("distort"));
|
||||
assert_corners(&drag(&mut t, Mods::default()), [(100.0, 100.0), (300.0, 100.0), (340.0, 260.0), (100.0, 200.0)]);
|
||||
assert_corners(&drag(&mut t, shift), fixed);
|
||||
t.set_option("constrain", &json!(true));
|
||||
assert_corners(&drag(&mut t, Mods::default()), fixed);
|
||||
// Cmd+Shift on a corner in the Free Transform mode is Free Distort, constrained too.
|
||||
let mut t = FreeTransformTool::default();
|
||||
assert_corners(&drag(&mut t, Mods { cmd: true, shift: true, ..Default::default() }), fixed);
|
||||
// Cmd+Alt+Shift stays Perspective Distort, with Constrain on as well.
|
||||
t.set_option("constrain", &json!(true));
|
||||
let perspective = Mods { cmd: true, alt: true, shift: true, ..Default::default() };
|
||||
assert_corners(&drag(&mut t, perspective), [(100.0, 100.0), (300.0, 40.0), (300.0, 260.0), (100.0, 200.0)]);
|
||||
// The measurement label shows how far the corner moved.
|
||||
let cx = ToolContext { measurement_labels: true, ..cx };
|
||||
let mut t = FreeTransformTool::default();
|
||||
t.set_option("mode", &json!("distort"));
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 300.0, 200.0).with_mods(shift));
|
||||
t.pointer(&cx, &ev(PointerKind::Drag, 340.0, 260.0).with_mods(shift));
|
||||
let want = cx.offset_label(56.0, 28.0);
|
||||
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Measure { text, .. } if text == &want)), "{want}");
|
||||
}
|
||||
|
||||
/// Shift pressed in the middle of a Free Distort corner drag constrains it from then on, in the
|
||||
/// same undo step; letting go frees the corner again.
|
||||
#[test]
|
||||
fn shift_pressed_during_a_free_distort_drag_constrains_it() {
|
||||
let (d, s, _) = wide_rect();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = FreeTransformTool::default();
|
||||
t.set_option("mode", &json!("distort"));
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 300.0, 200.0));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 310.0, 210.0));
|
||||
assert_eq!(a[0], Action::Begin("Distort".into()));
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 340.0, 260.0).with_mods(Mods { shift: true, ..Default::default() }));
|
||||
assert_eq!(a.len(), 1, "no new undo step: {a:?}");
|
||||
assert_corners(&sent_corners(&a), [(100.0, 100.0), (300.0, 100.0), (356.0, 228.0), (100.0, 200.0)]);
|
||||
let a = t.pointer(&cx, &ev(PointerKind::Drag, 340.0, 260.0));
|
||||
assert_corners(&sent_corners(&a), [(100.0, 100.0), (300.0, 100.0), (340.0, 260.0), (100.0, 200.0)]);
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 340.0, 260.0)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
/// A turned box is constrained along its own diagonal.
|
||||
#[test]
|
||||
fn a_turned_box_is_constrained_along_its_own_diagonal() {
|
||||
let (mut d, s, id) = wide_rect();
|
||||
let c = Vec2::new(200.0, 150.0);
|
||||
let turn = Affine::translate(c) * Affine::rotate(-30f64.to_radians()) * Affine::translate(-c);
|
||||
d.node_mut(id).unwrap().transform(turn, false);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let bx = selection_box(&cx).unwrap();
|
||||
assert!((bx.rect.width() - 200.0).abs() < 1e-9 && (bx.rect.height() - 100.0).abs() < 1e-9, "{bx:?}");
|
||||
let corner = Handle::BottomRight.pos(bx.rect);
|
||||
let (from, to) = (bx.to_doc() * corner, bx.to_doc() * (corner + Vec2::new(40.0, 60.0)));
|
||||
let mut t = FreeTransformTool::default();
|
||||
t.set_option("mode", &json!("distort"));
|
||||
t.set_option("constrain", &json!(true));
|
||||
t.pointer(&cx, &ev(PointerKind::Down, from.x, from.y));
|
||||
let got = sent_corners(&t.pointer(&cx, &ev(PointerKind::Drag, to.x, to.y)));
|
||||
let want = bx.to_doc() * (corner + Vec2::new(56.0, 28.0));
|
||||
assert!(got[2].distance(want) < 1e-9, "{got:?} {want:?}");
|
||||
for (i, k) in [(0, Handle::TopLeft), (1, Handle::TopRight), (3, Handle::BottomLeft)] {
|
||||
assert!(got[i].distance(bx.to_doc() * k.pos(bx.rect)) < 1e-9, "{got:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A box with no size has no diagonal to move along; a line keeps to its own length.
|
||||
#[test]
|
||||
fn along_diagonal_of_a_degenerate_box() {
|
||||
assert_eq!(along_diagonal(Rect::new(5.0, 5.0, 5.0, 5.0), Handle::BottomRight, Vec2::new(3.0, 4.0)), Vec2::ZERO);
|
||||
assert_eq!(along_diagonal(Rect::new(5.0, 0.0, 5.0, 10.0), Handle::BottomRight, Vec2::new(3.0, 4.0)), Vec2::new(0.0, 4.0));
|
||||
let d = along_diagonal(Rect::new(0.0, 0.0, 10.0, 10.0), Handle::TopLeft, Vec2::new(1.0, 0.0));
|
||||
assert!(d.x.is_finite() && (d - Vec2::new(0.5, 0.5)).hypot() < 1e-12, "{d:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distort_drag_shows_move_measurement_labels() {
|
||||
let (d, id) = doc_with_rect();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! The Free Transform widget (#598): while the Free Transform tool is active, a small floating box
|
||||
//! at the canvas's top left with Constrain (as holding Shift: proportional scaling, moves and
|
||||
//! rotations by 45°) over the tool's three modes, Free Transform, Perspective Distort and Free
|
||||
//! Distort. It sets the tool's options through `tool.setOption` (`constrain`, `mode`), as agents
|
||||
//! rotations by 45°, and Free Distort corners kept on their diagonal) over the tool's three modes,
|
||||
//! Free Transform, Perspective Distort and Free Distort. It sets the tool's options through `tool.setOption` (`constrain`, `mode`), as agents
|
||||
//! do; the modifier keys keep working while dragging.
|
||||
|
||||
use egui::{Color32, CornerRadius, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
@@ -61,8 +61,8 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &Ui, canvas: Rect) {
|
||||
.shadow(egui::epaint::Shadow { offset: [0, 2], blur: 8, spread: 0, color: Color32::from_black_alpha(60) })
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 2.0;
|
||||
// Constrain shapes only what the Free Transform mode does.
|
||||
if button(ui, Glyph::Icon("link"), tl!("Constrain"), constrain, mode == "free") {
|
||||
// Constrain shapes what the Free Transform and Free Distort modes do.
|
||||
if button(ui, Glyph::Icon("link"), tl!("Constrain"), constrain, mode != "perspective") {
|
||||
set = Some(("constrain", json!(!constrain)));
|
||||
}
|
||||
let (line, _) = ui.allocate_exact_size(vec2(BUTTON, 3.0), Sense::hover());
|
||||
@@ -148,7 +148,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// The widget shows with the Free Transform tool only; its buttons set the mode and Constrain
|
||||
/// (inert in the distort modes); a corner dragged in Free Distort moves that corner alone.
|
||||
/// (inert in Perspective Distort); a corner dragged in Free Distort moves that corner alone, and
|
||||
/// with Constrain on it keeps to the diagonal through the opposite corner.
|
||||
#[test]
|
||||
fn the_widget_sets_the_mode_and_a_corner_drag_then_distorts() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
@@ -175,26 +176,50 @@ mod tests {
|
||||
assert_eq!(options(&app), ("free".into(), true), "Constrain on");
|
||||
click(&mut app, &ctx, at(3));
|
||||
assert_eq!(options(&app), ("distort".into(), true));
|
||||
click(&mut app, &ctx, at(0));
|
||||
assert_eq!(options(&app), ("distort".into(), true), "Constrain is inert in Free Distort");
|
||||
click(&mut app, &ctx, at(2));
|
||||
assert_eq!(options(&app).0, "perspective");
|
||||
click(&mut app, &ctx, at(0));
|
||||
assert_eq!(options(&app), ("perspective".into(), true), "Constrain is inert in Perspective Distort");
|
||||
click(&mut app, &ctx, at(3));
|
||||
// Free Distort: the top-left corner dragged alone.
|
||||
// Free Distort with Constrain on: the top-left corner dragged by (-20, -10) keeps to the
|
||||
// diagonal (-100, -60), so it moves by 2,600 / 13,600 of it, about (-19.1, -11.5).
|
||||
let xf = Xf::new(canvas, app.view().unwrap());
|
||||
let (from, to) = (xf.to_screen(Point::new(100.0, 100.0)), xf.to_screen(Point::new(80.0, 90.0)));
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(from), button(from, true)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(from + (to - from) / 2.0)]);
|
||||
frame(&mut app, &ctx, vec![egui::Event::PointerMoved(to)]);
|
||||
frame(&mut app, &ctx, vec![button(to, false)]);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let st = app.session.active().unwrap();
|
||||
let node = st.doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap();
|
||||
let pts: Vec<Point> = node.path_data().unwrap().anchors().map(|(.., a)| a.p).collect();
|
||||
let near = |q: Point| pts.iter().any(|p| p.distance(q) < 0.5);
|
||||
let drag = |app: &mut VectorcraftApp, from: Point, to: Point| {
|
||||
let (from, to) = (xf.to_screen(from), xf.to_screen(to));
|
||||
frame(app, &ctx, vec![egui::Event::PointerMoved(from), button(from, true)]);
|
||||
frame(app, &ctx, vec![egui::Event::PointerMoved(from + (to - from) / 2.0)]);
|
||||
frame(app, &ctx, vec![egui::Event::PointerMoved(to)]);
|
||||
frame(app, &ctx, vec![button(to, false)]);
|
||||
frame(app, &ctx, vec![]);
|
||||
};
|
||||
let corners = |app: &VectorcraftApp| -> Vec<Point> {
|
||||
let st = app.session.active().unwrap();
|
||||
let node = st.doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap();
|
||||
node.path_data().unwrap().anchors().map(|(.., a)| a.p).collect()
|
||||
};
|
||||
let has = |pts: &[Point], q: Point| pts.iter().any(|p| p.distance(q) < 0.5);
|
||||
drag(&mut app, Point::new(100.0, 100.0), Point::new(80.0, 90.0));
|
||||
let k = (20.0 * 100.0 + 10.0 * 60.0) / (100.0f64 * 100.0 + 60.0 * 60.0);
|
||||
let pts = corners(&app);
|
||||
assert!(
|
||||
near(Point::new(80.0, 90.0)) && near(Point::new(200.0, 100.0)) && near(Point::new(200.0, 160.0)) && near(Point::new(100.0, 160.0)),
|
||||
has(&pts, Point::new(100.0 - 100.0 * k, 100.0 - 60.0 * k))
|
||||
&& has(&pts, Point::new(200.0, 100.0))
|
||||
&& has(&pts, Point::new(200.0, 160.0))
|
||||
&& has(&pts, Point::new(100.0, 160.0)),
|
||||
"{pts:?}"
|
||||
);
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
// Constrain off in Free Distort: the corner moves with the pointer.
|
||||
click(&mut app, &ctx, at(0));
|
||||
assert_eq!(options(&app), ("distort".into(), false), "Constrain works in Free Distort");
|
||||
drag(&mut app, Point::new(100.0, 100.0), Point::new(80.0, 90.0));
|
||||
let pts = corners(&app);
|
||||
assert!(
|
||||
has(&pts, Point::new(80.0, 90.0))
|
||||
&& has(&pts, Point::new(200.0, 100.0))
|
||||
&& has(&pts, Point::new(200.0, 160.0))
|
||||
&& has(&pts, Point::new(100.0, 160.0)),
|
||||
"{pts:?}"
|
||||
);
|
||||
assert_eq!(app.session.tool_id(), TOOL);
|
||||
|
||||
@@ -876,6 +876,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Align New Objects to Pixel Grid (`document.setup {alignNewObjectsToPixelGrid}`, a checkbox in New Document's Advanced Options and a checked item in the Transform panel menu) gives every path and compound path drawn in the document, the paths of a new grid or flare included, the pixel-aligned property, snapped in the step that draws it; it is on in the Mobile and Web presets and off in the others, saved presets and Recent keep it, and the document saves it. Type, images and groups do not take it, and art already in the document, pasted, duplicated or placed keeps its own property (VectorCraft's own rules) ([Pixel-aligned property](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/pixel-preview-mode-for-web-graphics.html); `tests::pixelalign::align_new_objects_makes_new_paths_pixel_aligned`, `align_new_objects_reaches_grids_flares_and_add_on_shapes`, `align_new_objects_is_saved_and_read_back`, `tests::newdoc::align_new_objects_to_pixel_grid_follows_the_preset`, `panels::transform::tests::align_new_objects_menu_item_toggles_the_active_document`, `dialogs::new_document::tests::align_new_objects_checkbox_follows_the_preset_and_reaches_the_document`).
|
||||
- Object › Transform › Reset Bounding Box squares the box of every object inside the selected ones as well: the children of groups, compound paths, clip groups, blends, envelopes, repeats and intertwines, and the art of linked opacity masks. Ungroup and the release commands then bring back boxes square to the page (VectorCraft's own behavior; `bboxrotate::reset_bounding_box_squares_the_objects_inside_a_group`, `reset_bounding_box_squares_the_turned_members_of_a_square_group`, `reset_bounding_box_then_release_clipping_mask` and the other `reset_bounding_box_then_*` tests, `reset_bounding_box_leaves_the_art_of_an_unlinked_opacity_mask`).
|
||||
- After a rotation the Free Transform tool shows and drags the same turned bounding box as the Selection tool: its handles, their cursors and its rotate zone sit on the selection's own sides, a handle scales along those sides from the opposite handle ([the help page](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/scale-objects.html)), Cmd/Ctrl on a side handle shears along that side, and Perspective Distort and Free Distort start from the turned box's corners (`object.distort` takes `angle` with `from`). The Scale and Reflect tools, their dialogs and commands, and the Transform panel's flips keep the page's axes (VectorCraft's own rule; `bboxrotate::free_transform_handles_turn_with_the_object_and_scale_along_its_axes`, `free_transform_shears_along_the_turned_side`, `free_transform_distort_starts_from_the_turned_box`, `xform::distort_reads_from_in_a_turned_frame_and_rejects_a_bad_angle`, tools `xform::free::tests::the_box_and_its_handles_turn_with_the_selection`).
|
||||
- The Free Transform widget's Constrain works in Free Distort as well as in Free Transform ([the help page](https://helpx.adobe.com/illustrator/using/tool-techniques/free-transform-tool.html)). With Constrain on, or Shift held, a corner dragged in Free Distort moves only along the line through the opposite corner, in the box's own proportions, and the other three corners stay in place; on a turned box the line runs through the box's own corners. The line is VectorCraft's own rule. Cmd/Ctrl+Alt+Shift on a corner still drags in Perspective Distort, where Constrain stays unavailable (tools `xform::free::tests::free_distort_corner_keeps_to_its_diagonal_with_shift_or_constrain`, `shift_pressed_during_a_free_distort_drag_constrains_it`, `a_turned_box_is_constrained_along_its_own_diagonal`, `along_diagonal_of_a_degenerate_box`; engine `xform::free_transform_constrained_distort_keeps_the_corner_on_its_diagonal`; ui-egui `free_transform::tests::the_widget_sets_the_mode_and_a_corner_drag_then_distorts`).
|
||||
- Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. Native menus explicitly end tracking on command selection, outside window presses and Escape; menu updates wait until tracking ends, and a dismissing press never starts a canvas gesture (M14.8).
|
||||
- The Learn panel (Window › Learn, or Explore tutorials beside the search icon on the app bar) has a For you tab with a Get started section and a Quick skill guide card, and a Skills tab that lists VectorCraft's own skills with their tutorial counts and empty progress rings; a skill lists its tutorials, a tutorial shows its numbered steps, and Back goes up a level; `learn.list` and `learn.show {tutorial}` script it ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `panels::learn::tests`).
|
||||
- A tool's name in a Learn tutorial step is a blue link that shows where the tool is without choosing it (`learn.locateTool {tool}`): its toolbar button is marked, a collapsed group's flyout opens with the tool's row marked, a torn-off strip comes to the front, and a tool no shown toolbar holds is marked in the All Tools drawer. The mark, its 3 seconds and the hidden-toolbar cases are VectorCraft's own rules ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/learn-skills-quick-skill-guide.html); `panels::learn::tests`, the `a_learn_link_*` tests in `toolbar::tests`).
|
||||
|
||||
Reference in New Issue
Block a user