Merge pull request #2336 from dotCooCoo/feat/rotate-view-to-selection

View: Rotate View to Selection turns the view square to the selected object
This commit is contained in:
BFlat
2026-10-16 15:16:18 +07:00
committed by GitHub
23 changed files with 137 additions and 3 deletions
+1
View File
@@ -2370,6 +2370,7 @@
Reset Preferences Нулиране на настройките
Reset Profiles Нулиране на профилите
Reset Rotate View Нулиране на изгледа на завъртане
Rotate View to Selection Завъртане на изгледа спрямо селекцията
Reset the rows to the artwork's colors Нулиране на редовете към цветовете на илюстрацията
Reset to Default Връщане към стандартните
Reset to White, Black Нулиране към бяло, черно
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -490,6 +490,7 @@
Fit All in Window Přizpůsobit vše oknu
Actual Size Skutečná velikost
Reset Rotate View Obnovit otočení zobrazení
Rotate View to Selection Otočit zobrazení podle výběru
Hide Edges Skrýt okraje
Show Edges Zobrazit okraje
Hide Artboards Skrýt pracovní plochy
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2366,6 +2366,7 @@
Reset Profiles Profile zurücksetzen
Rotate View Ansicht drehen
Reset Rotate View Ansichtsdrehung zurücksetzen
Rotate View to Selection Ansicht nach Auswahl drehen
Reset the rows to the artwork's colors Zeilen auf die Farben der Grafik zurücksetzen
Reset to Default Auf Standard zurücksetzen
Reset to White, Black Auf Weiß, Schwarz zurücksetzen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2904,6 +2904,7 @@ theme Appearance Εμφάνιση
Reset Panel Επαναφορά πίνακα
Reset Profiles Επαναφορά προφίλ
Reset Rotate View Επαναφορά περιστροφής προβολής
Rotate View to Selection Περιστροφή προβολής ως προς την επιλογή
Reset the rows to the artwork's colors Επαναφορά γραμμών στα χρώματα του σχεδίου
Reset to White, Black Επαναφορά σε λευκό και μαύρο
Reset with Mesh Επαναφορά με πλέγμα
Can't render this file because it contains an unexpected character in line 1857 and column 19.
+1
View File
@@ -2414,6 +2414,7 @@
Reset Preferences Restablecer preferencias
Reset Profiles Restablecer perfiles
Reset Rotate View Restablecer rotación de vista
Rotate View to Selection Rotar vista según la selección
Reset the rows to the artwork's colors Restablecer las filas a los colores de la ilustración
Reset to Default Restablecer valores predeterminados
Reset to White, Black Restablecer a blanco, negro
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -483,6 +483,7 @@
Fit All in Window Sovita kaikki ikkunaan
Actual Size Todellinen koko
Reset Rotate View Palauta näkymän kierto
Rotate View to Selection Kierrä näkymää valinnan mukaan
Hide Edges Piilota reunat
Show Edges Näytä reunat
Hide Artboards Piilota piirtoalustat
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2411,6 +2411,7 @@
Reset Preferences Réinitialiser les préférences
Reset Profiles Réinitialiser les profils
Reset Rotate View Réinitialiser la rotation de l’affichage
Rotate View to Selection Faire pivoter l’affichage selon la sélection
Reset the rows to the artwork's colors Réinitialiser les lignes avec les couleurs de l’illustration
Reset to Default Rétablir les valeurs par défaut
Reset to White, Black Réinitialiser à blanc, noir
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2920,6 +2920,7 @@
Reset Printing and Proofing Printing and Proofing visszaállítása
Reset Profiles Profilok visszaállítása
Reset Rotate View Nézetforgatás visszaállítása
Rotate View to Selection Nézet elforgatása a kijelöléshez
Reset the rows to the artwork's colors Sorok visszaállítása a grafika színeire
Reset to default Alapérték visszaállítása
Reset to Default Alapérték visszaállítása
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2417,6 +2417,7 @@
Reset Profiles Reimposta profili
Rotate View Ruota vista
Reset Rotate View Reimposta rotazione vista
Rotate View to Selection Ruota vista in base alla selezione
Reset the rows to the artwork's colors Reimposta le righe sui colori della grafica
Reset to Default Reimposta valori predefiniti
Reset to White, Black Reimposta su bianco, nero
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -487,6 +487,7 @@
Actual Size 100% 表示
Rotate View ビューを回転
Reset Rotate View ビューを回転の初期化
Rotate View to Selection 選択範囲に合わせてビューを回転
Hide Edges エッジを隠す
Hide Artboards アートボードを隠す
Show Print Tiling プリント分割を表示
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -483,6 +483,7 @@
Actual Size 실제 크기
Rotate View 보기 회전
Reset Rotate View 회전 보기 재설정
Rotate View to Selection 선택 영역에 맞춰 보기 회전
Hide Edges 가장자리 숨기기
Hide Artboards 아트보드 숨기기
Show Print Tiling 인쇄 타일 표시
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -448,6 +448,7 @@
Actual Size Rzeczywisty rozmiar
Rotate View Obróć widok
Reset Rotate View Resetuj obrót widoku
Rotate View to Selection Obróć widok do zaznaczenia
Hide Edges Ukryj krawędzie
Show Edges Pokaż krawędzie
Hide Artboards Ukryj obszary kompozycji
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1992,6 +1992,7 @@
Reset Profiles Redefinir Perfis
Rotate View Girar Visualização
Reset Rotate View Redefinir Visualização Girada
Rotate View to Selection Girar Visualização para a Seleção
Reset the rows to the artwork's colors Redefinir as linhas para as cores da arte
Reset to Default Redefinir para o Padrão
Reset to White, Black Redefinir para Branco, Preto
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2413,6 +2413,7 @@
Reset Profiles Сбросить профили
Rotate View Повернуть вид
Reset Rotate View Сбросить поворот вида
Rotate View to Selection Повернуть вид по выделению
Reset the rows to the artwork's colors Сбросить строки к цветам рисунка
Reset to Default Сбросить к значениям по умолчанию
Reset to White, Black Сбросить к белому, чёрному
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2370,6 +2370,7 @@
Reset Preferences Återställ inställningar
Reset Profiles Återställ profiler
Reset Rotate View Återställ Rotera vy
Rotate View to Selection Rotera vyn efter markeringen
Reset the rows to the artwork's colors Återställ raderna till teckningens färger
Reset to Default Återställ till standard
Reset to White, Black Återställ till vitt, svart
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -2409,6 +2409,7 @@
Reset Profiles Скинути профілі
Rotate View Повернути вид
Reset Rotate View Скинути поворот виду
Rotate View to Selection Повернути вид за виділенням
Reset the rows to the artwork's colors Скинути рядки до кольорів малюнка
Reset to Default Скинути до значень за замовчуванням
Reset to White, Black Скинути до білого, чорного
Can't render this file because it contains an unexpected character in line 718 and column 40.
+1
View File
@@ -2667,6 +2667,7 @@
Reset Profiles Đặt lại hồ sơ màu
Rotate View Xoay chế độ xem
Reset Rotate View Đặt lại xoay chế độ xem
Rotate View to Selection Xoay chế độ xem theo vùng chọn
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
Reset to Default Đặt lại về mặc định
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
2667
2668
2669
2670
2671
2672
2673
+1
View File
@@ -2296,6 +2296,7 @@
Reset Preferences 重置首选项
Reset Profiles 重置配置文件
Reset Rotate View 重置旋转视图
Rotate View to Selection 按所选对象旋转视图
Reset the rows to the artwork's colors 将各行重置为图稿的颜色
Reset to Default 恢复默认
Reset to White, Black 重置为白、黑
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2391,6 +2391,7 @@
Reset Profiles 重設描述檔
Rotate View 旋轉檢視
Reset Rotate View 重設旋轉檢視
Rotate View to Selection 依選取範圍旋轉檢視
Reset the rows to the artwork's colors 將各列重設為圖稿的顏色
Reset to Default 重設為預設值
Reset to White, Black 重設為白色、黑色
Can't render this file because it contains an unexpected character in line 6 and column 47.
+24
View File
@@ -264,6 +264,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{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.rotateToSelection",
"Rotate View to Selection",
"",
"{} turn the view about the center of the canvas to the angle the selected objects share (selectionRotation in document.inspect, 0 when they differ), so their bounding box is square to the window; returns {angle}",
),
("view.rotateReset", "Reset Rotate View", "Cmd+Shift+1", "{}"),
("window.control", "Control", "", "{}"),
("window.toolbar", "Tools", "", "{}"),
@@ -1252,6 +1258,22 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
None => Err("no document".into()),
}
}
// View › Rotate View to Selection: the view turns about the center of the canvas to the angle
// of the selection's bounding box, so the box is square to the window.
"view.rotateToSelection" => {
let Some(st) = app.session.active() else { return Some(Err("no document".into())) };
if st.selection.objects.is_empty() {
return Some(Err("nothing selected".into()));
}
let angle = vectorcraft_geom::normalize_deg(st.doc.bbox_angle(&st.selection.objects));
match app.view_mut() {
Some(v) => {
v.rotation = angle;
Ok(json!({ "angle": angle }))
}
None => Err("no document".into()),
}
}
"view.rotateReset" => {
if let Some(v) = app.view_mut() {
v.rotation = 0.0;
@@ -2121,6 +2143,7 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
| "view.fitAll"
| "view.actualSize"
| "view.rotate" => app.session.active().is_some(),
"view.rotateToSelection" => app.session.active().is_some_and(|d| !d.selection.objects.is_empty()),
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") => {
@@ -2703,6 +2726,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
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("Rotate View to Selection", "view.rotateToSelection"),
c("Reset Rotate View", "view.rotateReset"),
Sep,
c("Hide Edges", "view.edges"),
+90 -2
View File
@@ -1,7 +1,9 @@
//! 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.
//! View › Rotate View to Selection (`view.rotateToSelection`) turns the view to the angle of the
//! selection's bounding box.
use serde_json::json;
use serde_json::{Value, json};
use vectorcraft_engine::Session;
use crate::{VectorcraftApp, menus};
@@ -62,7 +64,12 @@ fn the_view_menu_lists_the_status_bar_angles_and_checks_the_current_one() {
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");
let next: Vec<_> = after[..2].iter().map(|e| (e.path.join(" › "), e.label.as_str(), e.command.as_deref())).collect();
let want = [
("View".to_string(), "Rotate View to Selection", Some("view.rotateToSelection")),
("View".to_string(), "Reset Rotate View", Some("view.rotateReset")),
];
assert_eq!(next, want, "the submenu comes right before them");
app.run("view.rotate", json!({"angle": 30})).unwrap();
assert_eq!(checked(&app), ["30°"]);
@@ -109,6 +116,87 @@ fn rotate_view_needs_a_document() {
assert!(menus::enabled(&with_document(), "view.rotate"));
}
/// Selects the object `id` alone and turns it by `angle` degrees.
fn turn(app: &mut VectorcraftApp, id: &Value, angle: f64) {
app.run("select.set", json!({ "ids": [id] })).unwrap();
app.run("object.rotate", json!({ "angle": angle })).unwrap();
}
/// Draws a 120 × 60 pt rectangle at `x` and returns its id.
fn rectangle(app: &mut VectorcraftApp, x: f64) -> Value {
app.run("shape.rectangle", json!({"x": x, "y": 80, "width": 120, "height": 60})).unwrap()["id"].clone()
}
/// Says whether the sides of the selection's bounding box run across and down the window, as the
/// canvas draws the box in the active view.
fn square_on_screen(app: &VectorcraftApp) -> bool {
let st = app.session.active().unwrap();
let corners = st.doc.oriented_bounds(&st.selection.objects, false).unwrap().corners();
let rect = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(800.0, 600.0));
let to_screen = crate::canvas::Xf::new(rect, app.view().unwrap()).affine();
let [a, b, c, _] = corners.map(|p| to_screen * p);
let (top, side) = (b - a, c - b);
top.x.abs() > 1.0 && top.y.abs() < 1e-6 && side.x.abs() < 1e-6 && side.y.abs() > 1.0
}
/// Runs Rotate View to Selection and returns the angle it reports, after checking that the view is
/// turned to that angle and that it is the selection's angle `document.inspect` reports.
fn rotate_to_selection(app: &mut VectorcraftApp) -> f64 {
let shared = app.run("document.inspect", json!({})).unwrap()["selectionRotation"].as_f64().unwrap();
let angle = app.run("view.rotateToSelection", json!({})).unwrap()["angle"].as_f64().unwrap();
assert_eq!((angle, app.view().unwrap().rotation), (shared, shared));
angle
}
#[test]
fn rotate_view_to_selection_squares_the_view_to_the_selected_object() {
let near = |a: f64, b: f64| (a - b).abs() < 1e-9;
let mut app = with_document();
let id = rectangle(&mut app, 40.0);
turn(&mut app, &id, 30.0);
let (zoom, center) = (app.view().unwrap().zoom, app.view().unwrap().center);
assert!(!square_on_screen(&app), "the box stands at 30° before the view turns");
let angle = rotate_to_selection(&mut app);
assert!(near(angle, 30.0), "{angle}");
let v = app.view().unwrap();
assert_eq!((v.zoom, v.center), (zoom, center), "the view turns about the center of the canvas");
assert!(square_on_screen(&app));
// Point type turned the other way squares the view the same way.
let text = app.run("text.create", json!({"x": 300, "y": 300, "text": "Turned"})).unwrap()["id"].clone();
turn(&mut app, &text, -50.0);
let angle = rotate_to_selection(&mut app);
assert!(near(angle, -50.0), "{angle}");
assert!(square_on_screen(&app));
// Objects turned to different angles share no angle: the view turns back to 0.
let other = rectangle(&mut app, 400.0);
turn(&mut app, &other, 45.0);
app.run("select.set", json!({ "ids": [id, other] })).unwrap();
assert_eq!(rotate_to_selection(&mut app), 0.0);
assert!(square_on_screen(&app));
}
#[test]
fn rotate_view_to_selection_needs_a_selected_object() {
let mut app = with_document();
let id = rectangle(&mut app, 40.0);
turn(&mut app, &id, 30.0);
assert!(menus::enabled(&app, "view.rotateToSelection"));
app.run("select.none", json!({})).unwrap();
assert!(!menus::enabled(&app, "view.rotateToSelection"));
app.run("view.rotate", json!({"angle": 15})).unwrap();
assert_eq!(app.run("view.rotateToSelection", json!({})).unwrap_err(), "nothing selected");
assert_eq!(app.view().unwrap().rotation, 15.0, "an error leaves the view as it was");
let mut empty = VectorcraftApp::new(Session::new(), crate::Services::default());
assert!(!menus::enabled(&empty, "view.rotateToSelection"));
assert_eq!(empty.run("view.rotateToSelection", json!({})).unwrap_err(), "no document");
// The command palette lists it.
let listed = crate::palette::items().into_iter().any(|(label, id, _)| label == "Rotate View to Selection" && id == "view.rotateToSelection");
assert!(listed);
}
/// 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]
+3
View File
@@ -1943,6 +1943,9 @@ every document they read except a native file and the native document that an SV
`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.
`view.rotateToSelection` (desktop app, View › Rotate View to Selection) turns the view about the center of the canvas to
the angle the selected objects share (`selectionRotation` in `document.inspect`, 0 when their angles differ), so their
bounding box is square to the window. It returns `{angle}`, and with nothing selected it returns "nothing selected".
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
+1 -1
View File
@@ -513,7 +513,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
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.
Object › Artboards › Fit to Artwork Bounds and Fit to Selected Art resize the active artboard, as the Artboards panel menu's Fit to Artwork Bounds does (`artboard.fitToArt {index}`, `artboard.fitToSelection {index}`). They used to resize artboard 1 whichever artboard was active. When an action plays, its Fit to Artwork Bounds and Fit to Selected Art steps that name no artboard resize the artboard that is active then.
The canvas draws the active artboard's edge in black, as the public [Select artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/select-artboards.html) page describes. It draws the other artboards' edges in gray. With several artboards selected by the Artboard tool, the canvas draws the black edge around the active one, which the tool shows with handles and its name. It used to draw artboard 1's edge in black whichever artboard was active.
View › Rotate View turns the view to the status bar's preset angles (`view.rotate {angle}`, which takes any angle).
View › Rotate View turns the view to the status bar's preset angles (`view.rotate {angle}`, which takes any angle). View › Rotate View to Selection turns the view to the angle of the selected objects' bounding box, so the box is square to the window (`view.rotateToSelection`).
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.
With the view rotated, a second Escape less than a second after the first resets the rotation, as Reset Rotate View does. The first Escape may end something else, such as isolation mode or a path being drawn. The second resets the view only when it has nothing else to end, and an Escape that closes a menu does not count. After a click, or a press of another key that is not a modifier (Shift, Control, Alt/Option or Command), the next Escape counts as the first. With the Rotate View tool chosen, one Escape resets the view.
The status bar navigator's field shows the active artboard's number and name, and its drop-down lists every artboard by number and name, as the Artboards panel does. Choosing an artboard in the list makes it the active artboard and fits it in the window (`view.goToArtboard`). The list opens on the active artboard's row and lays out only the rows in view and at most one more.