mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
View: Rotate View turns the view to a preset angle, and view.rotate sets any angle
In the View menu, Reset Rotate View was the only rotation item. The
status bar's angle menu set its twelve preset angles by writing the
view's rotation itself, and no command took an angle to turn the view
to.
View › Rotate View now lists the status bar's angles (0°, 15°, 30°,
45°, 60°, 90°, 180°, -15°, -30°, -45°, -60° and -90°) and checks the
one the view is turned to. The new UI command `view.rotate {angle}`
turns the view to any angle about the center of the canvas and
returns the angle it set, normalized to (-180, 180] as a Rotate View
drag normalizes it. The menu's items and the status bar's angle menu
run it from one list of angles; the status bar sets the same angles as
before. Without a document the command returns "no document" and the
items are disabled. Run from the command palette, which passes no
angle, Rotate View opens a dialog that asks for the angle, with the
view's current angle filled in.
The de, it, ja, ko, pl, pt-br, ru, uk, vi and zh-hant catalogs have a
new "Rotate View" row. The strings the catalogs cover leave out the
angle labels, as they leave out Type › Size's point sizes.
Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -7,7 +7,7 @@ use serde_json::json;
|
||||
use vectorcraft_doc::NodeKind;
|
||||
|
||||
use crate::panels::stroke as stroke_panel;
|
||||
use crate::state::{ZOOM_STOPS, zoom_label};
|
||||
use crate::state::{ROTATE_VIEW_ANGLES, ZOOM_STOPS, zoom_label};
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::widgets;
|
||||
use crate::{VectorcraftApp, icons, menus, titlebar};
|
||||
@@ -558,11 +558,10 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
egui::ComboBox::from_id_salt("rot-combo").selected_text(egui::RichText::new(format!("{rot:.0}°")).size(11.5)).width(52.0).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for a in [0.0, 15.0, 30.0, 45.0, 60.0, 90.0, 180.0, -15.0, -30.0, -45.0, -60.0, -90.0] {
|
||||
if ui.selectable_label(rot == a, format!("{a:.0}°")).clicked()
|
||||
&& let Some(v) = app.view_mut()
|
||||
{
|
||||
v.rotation = a;
|
||||
for (label, a) in ROTATE_VIEW_ANGLES {
|
||||
if ui.selectable_label((rot - a).abs() < 1e-9, label).clicked() {
|
||||
// It fails only without a document, when there is no view to turn.
|
||||
app.run("view.rotate", json!({"angle": a})).ok();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2357,6 +2357,7 @@
|
||||
Reset Panel Bedienfeld zurücksetzen
|
||||
Reset Preferences Voreinstellungen zurücksetzen
|
||||
Reset Profiles Profile zurücksetzen
|
||||
Rotate View Ansicht drehen
|
||||
Reset Rotate View Ansichtsdrehung zurücksetzen
|
||||
Reset the rows to the artwork's colors Zeilen auf die Farben der Grafik zurücksetzen
|
||||
Reset to Default Auf Standard zurücksetzen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2408,6 +2408,7 @@
|
||||
Reset Panel Reimposta pannello
|
||||
Reset Preferences Reimposta preferenze
|
||||
Reset Profiles Reimposta profili
|
||||
Rotate View Ruota vista
|
||||
Reset Rotate View Reimposta rotazione vista
|
||||
Reset the rows to the artwork's colors Reimposta le righe sui colori della grafica
|
||||
Reset to Default Reimposta valori predefiniti
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -483,6 +483,7 @@
|
||||
Fit Artboard in Window アートボードを全体表示
|
||||
Fit All in Window すべてのアートボードを全体表示
|
||||
Actual Size 100% 表示
|
||||
Rotate View ビューを回転
|
||||
Reset Rotate View ビューを回転の初期化
|
||||
Hide Edges エッジを隠す
|
||||
Hide Artboards アートボードを隠す
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -479,6 +479,7 @@
|
||||
Fit Artboard in Window 창에 아트보드 맞추기
|
||||
Fit All in Window 창에 모두 맞추기
|
||||
Actual Size 실제 크기
|
||||
Rotate View 보기 회전
|
||||
Reset Rotate View 회전 보기 재설정
|
||||
Hide Edges 가장자리 숨기기
|
||||
Hide Artboards 아트보드 숨기기
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -444,6 +444,7 @@
|
||||
Fit Artboard in Window Dopasuj obszar kompozycji do okna
|
||||
Fit All in Window Dopasuj wszystko do okna
|
||||
Actual Size Rzeczywisty rozmiar
|
||||
Rotate View Obróć widok
|
||||
Reset Rotate View Resetuj obrót widoku
|
||||
Hide Edges Ukryj krawędzie
|
||||
Show Edges Pokaż krawędzie
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1986,6 +1986,7 @@
|
||||
Reset Panel Redefinir Painel
|
||||
Reset Preferences Redefinir Preferências
|
||||
Reset Profiles Redefinir Perfis
|
||||
Rotate View Girar Visualização
|
||||
Reset Rotate View Redefinir Visualização Girada
|
||||
Reset the rows to the artwork's colors Redefinir as linhas para as cores da arte
|
||||
Reset to Default Redefinir para o Padrão
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2404,6 +2404,7 @@
|
||||
Reset Panel Сбросить панель
|
||||
Reset Preferences Сбросить настройки
|
||||
Reset Profiles Сбросить профили
|
||||
Rotate View Повернуть вид
|
||||
Reset Rotate View Сбросить поворот вида
|
||||
Reset the rows to the artwork's colors Сбросить строки к цветам рисунка
|
||||
Reset to Default Сбросить к значениям по умолчанию
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2400,6 +2400,7 @@
|
||||
Reset Panel Скинути панель
|
||||
Reset Preferences Скинути налаштування
|
||||
Reset Profiles Скинути профілі
|
||||
Rotate View Повернути вид
|
||||
Reset Rotate View Скинути поворот виду
|
||||
Reset the rows to the artwork's colors Скинути рядки до кольорів малюнка
|
||||
Reset to Default Скинути до значень за замовчуванням
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 715 and column 40.
|
@@ -2656,6 +2656,7 @@
|
||||
Reset Panel Đặt lại bảng
|
||||
Reset Preferences Đặt lại tùy chọn
|
||||
Reset Profiles Đặt lại hồ sơ màu
|
||||
Rotate View Xoay chế độ xem
|
||||
Reset Rotate View Đặt lại xoay chế độ xem
|
||||
Reset Ruler Origin Đặt lại gốc thước
|
||||
Reset the rows to the artwork's colors Đặt lại các hàng về màu của tác phẩm
|
||||
|
||||
|
@@ -2386,6 +2386,7 @@
|
||||
Reset Panel 重設面板
|
||||
Reset Preferences 重設偏好設定
|
||||
Reset Profiles 重設描述檔
|
||||
Rotate View 旋轉檢視
|
||||
Reset Rotate View 重設旋轉檢視
|
||||
Reset the rows to the artwork's colors 將各列重設為圖稿的顏色
|
||||
Reset to Default 重設為預設值
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -123,6 +123,8 @@ mod tests_recovery;
|
||||
#[cfg(test)]
|
||||
mod tests_removeanchors;
|
||||
#[cfg(test)]
|
||||
mod tests_rotateview;
|
||||
#[cfg(test)]
|
||||
mod tests_ruler_origin;
|
||||
#[cfg(test)]
|
||||
mod tests_save;
|
||||
|
||||
@@ -252,6 +252,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
("view.snapToPoint", "Snap to Point", "Cmd+Alt+'", "{}"),
|
||||
("view.presentation", "Presentation Mode", "Shift+F", "{}"),
|
||||
("view.screenMode", "Screen Mode", "F", "{mode?: 0..2} (no param cycles)"),
|
||||
(
|
||||
"view.rotate",
|
||||
"Rotate View",
|
||||
"",
|
||||
"{angle: degrees, clockwise on screen as the status bar shows it} turn the view to that angle about the center of the canvas; returns {angle}, normalized to (-180, 180]",
|
||||
),
|
||||
("view.rotateReset", "Reset Rotate View", "Cmd+Shift+1", "{}"),
|
||||
("window.control", "Control", "", "{}"),
|
||||
("window.toolbar", "Tools", "", "{}"),
|
||||
@@ -1192,6 +1198,21 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
};
|
||||
Ok(json!(app.ui.screen_mode))
|
||||
}
|
||||
// View › Rotate View and the status bar's angle menu: the view turns about the center of the
|
||||
// canvas to the angle, normalized as a Rotate View drag normalizes it.
|
||||
"view.rotate" => {
|
||||
let key = "angle";
|
||||
let Some(angle) = p.get(key).and_then(Value::as_f64).filter(|a| a.is_finite()) else {
|
||||
return Some(Err(format!("`{key}` must be a number")));
|
||||
};
|
||||
match app.view_mut() {
|
||||
Some(v) => {
|
||||
v.rotation = vectorcraft_geom::normalize_deg(angle);
|
||||
Ok(json!({ "angle": v.rotation }))
|
||||
}
|
||||
None => Err("no document".into()),
|
||||
}
|
||||
}
|
||||
"view.rotateReset" => {
|
||||
if let Some(v) = app.view_mut() {
|
||||
v.rotation = 0.0;
|
||||
@@ -1583,6 +1604,9 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
||||
"view.snapToGrid" => v.snap_to_grid,
|
||||
"view.snapToPoint" => v.snap_to_point,
|
||||
"view.rulers" => v.rulers,
|
||||
// A View › Rotate View item is checked when the active view is turned to its angle; without
|
||||
// a document no item is checked.
|
||||
"view.rotate" => app.view().zip(p.get("angle").and_then(Value::as_f64)).is_some_and(|(view, a)| (view.rotation - a).abs() < 1e-9),
|
||||
"window.control" => app.ui.control_bar,
|
||||
"window.toolbar" => app.ui.toolbar,
|
||||
"window.toolbarAdvanced" => app.ui.toolbar_advanced,
|
||||
@@ -2033,7 +2057,8 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
||||
| "view.zoomOut"
|
||||
| "view.fitArtboard"
|
||||
| "view.fitAll"
|
||||
| "view.actualSize" => app.session.active().is_some(),
|
||||
| "view.actualSize"
|
||||
| "view.rotate" => app.session.active().is_some(),
|
||||
id if id.starts_with("file.openRecent") => recent_slot(app, id).is_some(),
|
||||
"file.clearRecent" => !app.ui.recent_files.is_empty(),
|
||||
id if id.starts_with("type.recentFont") => {
|
||||
@@ -2610,6 +2635,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
c("Fit All in Window", "view.fitAll"),
|
||||
c("Actual Size", "view.actualSize"),
|
||||
Sep,
|
||||
sub("Rotate View", crate::state::ROTATE_VIEW_ANGLES.iter().map(|(l, a)| cp(l, "view.rotate", json!({ "angle": a }))).collect()),
|
||||
c("Reset Rotate View", "view.rotateReset"),
|
||||
Sep,
|
||||
c("Hide Edges", "view.edges"),
|
||||
@@ -3247,6 +3273,17 @@ pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Rotate View with no angle, as the command palette runs it, opens a dialog for the angle, with
|
||||
// the view's angle filled in.
|
||||
if id == "view.rotate" && p.as_object().is_none_or(|o| o.is_empty()) {
|
||||
match app.view_mut().map(|v| v.rotation) {
|
||||
Some(angle) => {
|
||||
app.ui.dialog = Some(crate::state::Dialog::new("command", json!({"__command": id, "__label": "Rotate View", "angle": angle})));
|
||||
}
|
||||
None => app.status("no document"),
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Save Selection…: a name dialog, starting from the first free "Selection N".
|
||||
if id == "select.save" && p.as_object().is_none_or(|o| o.is_empty()) {
|
||||
let name = app.session.active().map(|d| vectorcraft_engine::doc::SavedSelection::default_name(&d.doc.saved_selections)).unwrap_or_default();
|
||||
@@ -3681,7 +3718,8 @@ pub const DYNAMIC_LABELS: &[&str] = &[
|
||||
|
||||
/// Every English string the menus, the command palette, the panel registry, the toolbar and the
|
||||
/// Preferences dialog can show: what a complete language catalog has to cover. Names that are user
|
||||
/// data (fonts, recent files, saved views, libraries) and the point sizes are left out.
|
||||
/// data (fonts, recent files, saved views, libraries), the point sizes and the view angles are
|
||||
/// left out.
|
||||
pub fn menu_strings() -> std::collections::BTreeSet<String> {
|
||||
use std::collections::BTreeSet;
|
||||
fn user_data(id: &str) -> bool {
|
||||
@@ -3692,9 +3730,14 @@ pub fn menu_strings() -> std::collections::BTreeSet<String> {
|
||||
match it {
|
||||
Item::Cmd(l, id, _) => {
|
||||
let size = l.strip_suffix(" pt").is_some_and(|n| n.chars().all(|c| c.is_ascii_digit()));
|
||||
// View › Rotate View lists angles: an optional minus sign, digits and a degree sign.
|
||||
let angle = l
|
||||
.strip_suffix('°')
|
||||
.map(|n| n.strip_prefix('-').unwrap_or(n))
|
||||
.is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()));
|
||||
// VectorCraft › Language lists each language by its own name.
|
||||
let language_name = *id == "app.language" && crate::i18n::Lang::all().any(|lang| lang.name() == *l);
|
||||
if !user_data(id) && !size && !language_name && *l != "—" {
|
||||
if !user_data(id) && !size && !angle && !language_name && *l != "—" {
|
||||
out.insert(l.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,23 @@ impl View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The status bar's angle menu and View › Rotate View list these preset view angles, each as
|
||||
/// (label, degrees clockwise on screen).
|
||||
pub const ROTATE_VIEW_ANGLES: [(&str, f64); 12] = [
|
||||
("0°", 0.0),
|
||||
("15°", 15.0),
|
||||
("30°", 30.0),
|
||||
("45°", 45.0),
|
||||
("60°", 60.0),
|
||||
("90°", 90.0),
|
||||
("180°", 180.0),
|
||||
("-15°", -15.0),
|
||||
("-30°", -30.0),
|
||||
("-45°", -45.0),
|
||||
("-60°", -60.0),
|
||||
("-90°", -90.0),
|
||||
];
|
||||
|
||||
/// Illustrator's preset zoom stops, in percent.
|
||||
pub const ZOOM_STOPS: [f64; 26] = [
|
||||
3.13, 4.17, 6.25, 8.33, 12.5, 16.67, 25.0, 33.33, 50.0, 66.67, 100.0, 150.0, 200.0, 300.0, 400.0, 600.0, 800.0, 1200.0, 1600.0, 2400.0, 3200.0,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! View › Rotate View and `view.rotate`: the view turns to one of the status bar's preset angles
|
||||
//! from the menu, or to any angle from the command, and the menu checks the angle the view is turned to.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use crate::{VectorcraftApp, menus};
|
||||
|
||||
/// These are the status bar's angles and their labels, in the status bar's order.
|
||||
const ANGLES: [(&str, f64); 12] = [
|
||||
("0°", 0.0),
|
||||
("15°", 15.0),
|
||||
("30°", 30.0),
|
||||
("45°", 45.0),
|
||||
("60°", 60.0),
|
||||
("90°", 90.0),
|
||||
("180°", 180.0),
|
||||
("-15°", -15.0),
|
||||
("-30°", -30.0),
|
||||
("-45°", -45.0),
|
||||
("-60°", -60.0),
|
||||
("-90°", -90.0),
|
||||
];
|
||||
|
||||
fn with_document() -> VectorcraftApp {
|
||||
let mut app = VectorcraftApp::new(Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
/// Returns the labels of the View › Rotate View items that are checked.
|
||||
fn checked(app: &VectorcraftApp) -> Vec<&'static str> {
|
||||
ANGLES.iter().filter(|(_, a)| menus::checked(app, "view.rotate", &json!({ "angle": a })) == Some(true)).map(|(l, _)| *l).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_rotate_turns_the_view_to_any_angle() {
|
||||
let mut app = with_document();
|
||||
let center = app.view().unwrap().center;
|
||||
assert_eq!(app.run("view.rotate", json!({"angle": 30})).unwrap(), json!({"angle": 30.0}));
|
||||
let v = app.view().unwrap();
|
||||
assert_eq!((v.rotation, v.center), (30.0, center), "it turns about the center of the canvas");
|
||||
// Angles are normalized to (-180, 180], as a Rotate View drag normalizes them.
|
||||
for (angle, want) in [(390.0, 30.0), (-190.0, 170.0), (-180.0, 180.0), (12.5, 12.5)] {
|
||||
assert_eq!(app.run("view.rotate", json!({ "angle": angle })).unwrap(), json!({ "angle": want }), "{angle}");
|
||||
assert_eq!(app.view().unwrap().rotation, want, "{angle}");
|
||||
}
|
||||
for p in [json!({"angle": "x"}), json!({}), json!({"angle": null})] {
|
||||
assert_eq!(app.run("view.rotate", p.clone()).unwrap_err(), "`angle` must be a number", "{p}");
|
||||
}
|
||||
assert_eq!(app.view().unwrap().rotation, 12.5, "an error leaves the view as it was");
|
||||
let mut empty = VectorcraftApp::new(Session::new(), crate::Services::default());
|
||||
assert_eq!(empty.run("view.rotate", json!({"angle": 30})).unwrap_err(), "no document");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_view_menu_lists_the_status_bar_angles_and_checks_the_current_one() {
|
||||
let mut app = with_document();
|
||||
let entries = menus::menu_entries(&app);
|
||||
let at = entries.iter().position(|e| e.path == ["View", "Rotate View"]).expect("View › Rotate View");
|
||||
let (items, after) = entries[at..].split_at(ANGLES.len());
|
||||
let shown: Vec<_> = items.iter().map(|e| (e.path.join(" › "), e.label.as_str(), e.command.as_deref(), e.params.clone(), e.enabled)).collect();
|
||||
let want: Vec<_> = ANGLES.iter().map(|(l, a)| ("View › Rotate View".to_string(), *l, Some("view.rotate"), json!({ "angle": a }), true)).collect();
|
||||
assert_eq!(shown, want);
|
||||
assert_eq!((after[0].path.join(" › "), after[0].label.as_str()), ("View".to_string(), "Reset Rotate View"), "the submenu comes right before it");
|
||||
|
||||
app.run("view.rotate", json!({"angle": 30})).unwrap();
|
||||
assert_eq!(checked(&app), ["30°"]);
|
||||
app.run("view.rotate", json!({"angle": 17})).unwrap();
|
||||
assert!(checked(&app).is_empty());
|
||||
// Every item stays a check item, unchecked, while the view is at another angle.
|
||||
assert!(ANGLES.iter().all(|(_, a)| menus::checked(&app, "view.rotate", &json!({ "angle": a })) == Some(false)));
|
||||
// A click on 180° runs its command.
|
||||
let item = &items[6];
|
||||
let (id, p) = menus::click_target(&item.label, item.command.as_deref().unwrap(), &item.params);
|
||||
menus::invoke(&mut app, &id, p);
|
||||
assert_eq!(app.view().unwrap().rotation, 180.0);
|
||||
assert_eq!(checked(&app), ["180°"]);
|
||||
}
|
||||
|
||||
/// Rotate View with no angle, as the command palette runs it, opens a dialog with the view's angle
|
||||
/// filled in, and OK turns the view to the angle entered.
|
||||
#[test]
|
||||
fn rotate_view_without_an_angle_asks_for_one() {
|
||||
assert!(crate::palette::items().iter().any(|(label, id, _)| (label.as_str(), id.as_str()) == ("Rotate View", "view.rotate")));
|
||||
let mut app = with_document();
|
||||
app.run("view.rotate", json!({"angle": 30})).unwrap();
|
||||
menus::invoke(&mut app, "view.rotate", json!({}));
|
||||
let mut d = app.ui.dialog.clone().expect("Rotate View dialog");
|
||||
assert_eq!((d.kind.as_str(), d.str("__command"), d.str("__label")), ("command", "view.rotate".into(), "Rotate View".into()));
|
||||
assert_eq!(d.fields.get("angle"), Some(&json!(30.0)));
|
||||
d.fields.insert("angle".into(), json!(-45));
|
||||
app.ui.dialog = Some(d);
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!(app.view().unwrap().rotation, -45.0);
|
||||
// Without a document no dialog opens, and the status bar shows "no document".
|
||||
let mut empty = VectorcraftApp::new(Session::new(), crate::Services::default());
|
||||
menus::invoke(&mut empty, "view.rotate", json!({}));
|
||||
assert!(empty.ui.dialog.is_none());
|
||||
assert_eq!(empty.ui.status, "no document");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotate_view_needs_a_document() {
|
||||
let empty = VectorcraftApp::new(Session::new(), crate::Services::default());
|
||||
assert!(!menus::enabled(&empty, "view.rotate"));
|
||||
assert_eq!(menus::checked(&empty, "view.rotate", &json!({"angle": 0})), Some(false), "a check item without a document");
|
||||
assert!(menus::enabled(&with_document(), "view.rotate"));
|
||||
}
|
||||
|
||||
/// The strings the catalogs cover include the submenu's name and leave out the angle labels, as they
|
||||
/// leave out Type › Size's point sizes.
|
||||
#[test]
|
||||
fn only_the_submenu_name_needs_translating() {
|
||||
let strings = menus::menu_strings();
|
||||
assert!(strings.contains("Rotate View"));
|
||||
for (label, _) in ANGLES {
|
||||
assert!(!strings.contains(label), "{label}");
|
||||
}
|
||||
}
|
||||
@@ -1862,6 +1862,10 @@ draws, and View › Snap to Grid snaps to its subdivisions. Changing either sets
|
||||
every document they read except a native file and the native document that an SVG, PDF, `.ai` or EPS file carries
|
||||
(`restored: true`), which keep the grid they were saved with.
|
||||
|
||||
`view.rotate {angle}` (desktop app) turns the view to `angle` degrees about the center of the canvas; a positive angle
|
||||
turns the art clockwise on screen. View › Rotate View and the status bar's angle menu run it. It returns `{angle}`, the
|
||||
angle it set, normalized to (-180, 180], and `view.rotateReset` turns the view back to 0.
|
||||
|
||||
The Smart Guides preferences (Preferences › Smart Guides) apply to `pointer_gesture` with Smart Guides on (the
|
||||
default view) and to the mouse; they change what the tools show and how far a target pulls, and only Construction
|
||||
Guides change where a snapped point lands:
|
||||
|
||||
@@ -492,6 +492,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Align to Artboard uses the active artboard, the one the Artboards panel highlights (`object.align {artboard}`). A press with a selection tool inside an artboard makes that artboard active; with the Selection tool, a Shift-click on an empty spot inside an artboard makes it active and keeps the selection. One selected object is aligned to the active artboard too, whatever Align To says (a lone object is never the key object). When an action plays, its Align to Artboard, All on Active Artboard, Paste in Place, Paste in Front and Paste in Back steps that name no artboard use the artboard that is active then.
|
||||
With Align to Artboard chosen, the Align panel's Distribute Objects and Distribute Spacing buttons use the active artboard (`object.distribute` and `object.distributeSpacing` with `to: "artboard"` and `artboard`). The first object's left or top edge goes to the artboard's left or top edge, and the last object's right or bottom edge goes to the artboard's right or bottom edge. Distribute Objects spaces the other objects evenly by its button's line, and two objects are enough. Distribute Spacing makes the gaps between the objects equal. With anchor points selected, the buttons space the points among themselves and do not use the artboard. When an action plays, its Distribute Objects and Distribute Spacing steps with Align to Artboard use the artboard that is active then.
|
||||
Choosing the Artboard tool (Shift+O, its toolbar button, Edit Artboards in Properties or Document Setup, or the Artboards panel menu's Artboard Options…) starts it on the active artboard: the canvas outlines that artboard, and Delete, Cut, Copy, the Transform panel and Artboard Options act on it.
|
||||
View › Rotate View turns the view to the status bar's preset angles (`view.rotate {angle}`, which takes any angle).
|
||||
When the view angle changes (a Rotate View drag, the status bar's angle menu, Reset Rotate View or a saved view) and the desktop app renders the frame at the new angle on the render worker, the canvas draws the last frame turned to the new angle until that frame is ready. It used to draw no art until then.
|
||||
- **Geometry and effects:**
|
||||
- Pathfinder (10 exact curve booleans; single-result shape operations retain source names and SVG IDs, #1293), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. With one group selected alone, the Pathfinder panel's buttons work on its members (among other selected objects a group still acts as one shape), and a greyed-out button says what it needs (#1450; behaviour reference: [combine objects using Pathfinder effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/combine-objects-using-pathfinder-effects.html); one plain path selected alone stays disabled, which the documentation doesn't settle). Split Into Grid takes the number, height or width, gutter and total of the rows and of the columns, Add Guides and Preview, and several selected paths become one grid painted as the topmost one (#1462, `object.path.splitIntoGrid`; behaviour reference: [divide or split objects](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/divide-or-split-objects.html); a typed height or width keeps the gutter and changes the total, our choice). Object › Path › Remove Anchor Points (`path.removeAnchors`) removes the direct-selected anchors without opening their paths (the Delete key still deletes them with their segments): the neighbours keep their handle directions and their facing handles are refitted so one cubic follows the two old segments, and straight sides stay straight. The Delete Anchor Point tool uses the same refit. The Control bar, the contextual task bar, the Properties quick actions and the canvas context menu offer it when anchors are selected. With anchors selected, the Control bar and the Properties panel also convert them to corner or smooth (`path.convertAnchors`), connect end points (Join) and cut the path at them (`path.cutAtAnchors`, also on the task bar and the context menu): a closed path opens there and an open one becomes one path per piece, each cut leaving one of its two coincident anchors selected so a drag pulls the path apart. In the Pathfinder panel a group selected alone counts its nested shapes, so an expanded stroked object keeps the Shape Modes enabled (#1642, #1635).
|
||||
|
||||
Reference in New Issue
Block a user