Type: Cmd/Ctrl+Shift+X resets horizontal scale to 100%

The reference app resets the horizontal scale of type to 100% with
Cmd/Ctrl+Shift+X. VectorCraft had no command for it, and the chord
cut the selection: egui reports it as a Cut with Shift held.

type.resetHorizontalScale (Reset Horizontal Scale, default
Cmd+Shift+X) sets the horizontal scale to 100% in the text the Type
tool has selected, or else in all the text of every selected type
object, in one undo step. The value is the one the Character panel
shows, so a type object stretched to twice its width keeps 50% stored.
The vertical scale does not change. At a caret the command leaves the
text as it is. Type objects given in ids are reset in full, as
type.resetTracking does.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-16 14:58:09 +07:00
committed by bflatastic
parent ad1e8d1112
commit 6dfdf643d9
25 changed files with 144 additions and 1 deletions
+29
View File
@@ -112,6 +112,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_selection,
reset_tracking
),
cmd!(
"type.resetHorizontalScale",
"Reset Horizontal Scale",
[],
Some("Cmd+Shift+X"),
"{ids?: [id…]} set the horizontal scale to 100% (as the Character panel shows it, the object's own transform included) in all the text of the type objects in `ids`, else in the text the Type tool has selected (nothing at a caret), else in every selected type object, as one undo step → {ids}",
has_selection,
reset_horizontal_scale
),
cmd!(
"text.discardEmpty",
"Discard Empty Type",
@@ -752,6 +761,26 @@ fn reset_tracking(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({ "ids": ranges.iter().map(|(id, ..)| id.0).collect::<Vec<_>>() }))
}
/// Cmd/Ctrl+Shift+X: horizontal scale 100% (the Character panel's value, which includes the
/// object's own transform) in the type objects in `ids`, else in the Type tool's selected text,
/// else in every selected type object. At a caret the command leaves the text as it is.
fn reset_horizontal_scale(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "type.resetHorizontalScale";
let ranges = type_ranges(s, p, C, |_, _| None)?;
if !ranges.is_empty() {
s.edit("Reset Horizontal Scale", |d, _| {
for &(id, a, b, _) in &ranges {
let t = text_mut(d, id).ok_or(EngineError::NoNode(id))?;
let h_scale = super::typecmd::local_type_value(t, 100.0, (1.0, 10000.0), true, C)?;
edit::style_range(&mut t.runs, a, b, |st| st.h_scale = h_scale);
refresh_bounds(t);
}
Ok(())
})?;
}
Ok(json!({ "ids": ranges.iter().map(|(id, ..)| id.0).collect::<Vec<_>>() }))
}
/// Tracking (1/1000 em) that makes the first paragraph of `t` exactly `target` wide, if it fits
/// on one line at all.
fn headline_tracking(t: &TextObject, target: f64) -> Option<f64> {
+44
View File
@@ -1096,6 +1096,50 @@ fn reset_tracking_sets_tracking_and_kerning_to_zero() {
assert_eq!(style_at(&s, other, 0).tracking, 0.0, "the reset stays");
}
/// Cmd/Ctrl+Shift+X resets the horizontal scale to 100% in every selected type object and in the
/// Type tool's selected text, as one undo step, and leaves the vertical scale as it is. At a caret
/// it leaves the text as it is. On a stretched object the scale the Character panel shows, which includes the
/// object's own stretch, goes to 100%. Type objects given in `ids` are reset in full while the
/// Type tool edits.
#[test]
fn reset_horizontal_scale_sets_it_to_100_percent() {
use vectorcraft_tools::PointerKind::{Down, Up};
let mut s = new_doc();
let id = NodeId(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "Hello world", "size": 20})).unwrap()["id"].as_u64().unwrap());
let scales = |s: &Session, byte| {
let st = style_at(s, id, byte);
(st.h_scale, st.v_scale)
};
s.execute("text.setRangeStyle", &json!({"id": id.0, "hScale": 150, "vScale": 80})).unwrap();
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
assert_eq!(find_command("type.resetHorizontalScale").unwrap().shortcut, Some("Cmd+Shift+X"));
assert_eq!(s.execute("type.resetHorizontalScale", &json!({})).unwrap()["ids"], json!([id.0]));
assert_eq!((scales(&s, 0), scales(&s, 10)), ((100.0, 80.0), (100.0, 80.0)));
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(scales(&s, 0), (150.0, 80.0), "one undo step");
// A type object stretched to twice its width shows 200% and keeps half of it stored.
let wide = NodeId(s.execute("text.create", &json!({"x": 100, "y": 300, "text": "Wide", "size": 20})).unwrap()["id"].as_u64().unwrap());
s.execute("select.set", &json!({"ids": [wide.0]})).unwrap();
s.execute("object.scale", &json!({"sx": 200, "sy": 100})).unwrap();
s.execute("type.resetHorizontalScale", &json!({})).unwrap();
assert_eq!(style_at(&s, wide, 0).h_scale, 50.0);
// With the Type tool, it resets only the selected text.
let v = ViewInfo::default();
s.select_tool("type", v).unwrap();
gesture(&mut s, v, &[(Down, 102.0, 95.0), (Up, 102.0, 95.0)]);
assert_eq!(s.tool_options()["editing"], json!(id.0), "editing the text");
s.set_tool_option("select", &json!({"start": 0, "end": 5}));
s.execute("type.resetHorizontalScale", &json!({})).unwrap();
assert_eq!((scales(&s, 4), scales(&s, 6)), ((100.0, 80.0), (150.0, 80.0)));
// At a caret it leaves the text as it is.
s.set_tool_option("select", &json!({"start": 8, "end": 8}));
assert_eq!(s.execute("type.resetHorizontalScale", &json!({})).unwrap()["ids"], json!([]));
assert_eq!((scales(&s, 7), scales(&s, 8)), ((150.0, 80.0), (150.0, 80.0)));
// While the Type tool edits, the type objects in `ids` are reset in full.
s.execute("type.resetHorizontalScale", &json!({"ids": [id.0]})).unwrap();
assert_eq!((scales(&s, 6), scales(&s, 10)), ((100.0, 80.0), (100.0, 80.0)));
}
/// Type › Fill New Type Objects With Placeholder Text (#394, on by default): type the Type tool
/// places starts with placeholder text, selected, so typing replaces it; off, it starts empty.
#[test]
+20
View File
@@ -1120,6 +1120,26 @@ fn cmd_alt_q_resets_tracking_and_kerning_for_agents() {
assert_eq!((&runs[0]["style"]["tracking"], &runs[0]["style"]["kerning"]), (&json!(0.0), &Value::Null), "{runs}");
}
/// Cmd+Shift+X reaches agents: the placeholder text the Type tool selects goes back to a
/// horizontal scale of 100%.
#[test]
fn cmd_shift_x_resets_horizontal_scale_for_agents() {
let mut s = server();
let events = json!([{"kind": "down", "x": 100, "y": 100}, {"kind": "up", "x": 100, "y": 100}]);
let r = call(&mut s, 1, "pointer_gesture", json!({"tool": "type", "events": events}));
assert_eq!(r["isError"], false, "{r}");
let id = serde_json::from_str::<Value>(&text_of(&r)).unwrap()["selection"][0].as_u64().unwrap();
let r = call(&mut s, 2, "run_command", json!({"command": "text.setRangeStyle", "params": {"id": id, "hScale": 150}}));
assert_eq!(r["isError"], false, "{r}");
let r = call(&mut s, 3, "press_key", json!({"key": "X", "mods": {"cmd": true, "shift": true}}));
assert_eq!(r["isError"], false, "{r}");
assert_eq!(serde_json::from_str::<Value>(&text_of(&r)).unwrap()["command"], "type.resetHorizontalScale", "{r}");
let r = call(&mut s, 4, "run_command", json!({"command": "text.getRange", "params": {"id": id}}));
let runs = serde_json::from_str::<Value>(&text_of(&r)).unwrap()["runs"].clone();
assert_eq!(runs.as_array().unwrap().len(), 1, "{runs}");
assert_eq!(runs[0]["style"]["h_scale"], json!(100.0), "{runs}");
}
/// The drawing tools snap to Smart Guides over MCP as with the mouse (#506): a Rectangle drawn
/// from near another object's corner starts on it, and a Pen anchor placed beside that object's
/// centre lines up with it.
+1
View File
@@ -4881,6 +4881,7 @@ theme Appearance Външен вид
# Type › Reset Tracking and Kerning
Reset Tracking and Kerning Нулиране на разредката и кернинга
Reset Horizontal Scale Нулиране на хоризонталния мащаб
@msg span must be "object" or "selection", got {v} span трябва да е "object" или "selection", а не {v}
@msg span: "selection" places a `gradient`, not a swatch span: "selection" поставя `gradient`, а не мостра
@msg span: "selection" places a gradient: give `gradient` span: "selection" поставя градиент: задайте `gradient`
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3485,6 +3485,7 @@ axis Both Obě
Increase Font Size Zvětšit velikost písma
Increase Font Size by Five Steps Zvětšit velikost písma o pět kroků
Reset Tracking and Kerning Obnovit prostrkání a vyrovnání párů
Reset Horizontal Scale Obnovit vodorovnou změnu velikosti
Step Type Krokovat text
Get Text Range Získat rozsah textu
Punctuation Interpunkce
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1055,6 +1055,7 @@
Increase Font Size Schriftgrad erhöhen
Increase Font Size by Five Steps Schriftgrad um fünf Schritte erhöhen
Reset Tracking and Kerning Laufweite und Kerning zurücksetzen
Reset Horizontal Scale Horizontale Skalierung zurücksetzen
Spacing Abstand
Step Type Text schrittweise ändern
Fit in View In Ansicht einpassen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4833,6 +4833,7 @@ effect Points Σημεία
Precision: Ακρίβεια:
Preview (1 inch on the page) Προεπισκόπηση (1 ίντσα στη σελίδα)
Registration: Σημείο αναφοράς:
Reset Horizontal Scale Επαναφορά οριζόντιας κλίμακας
Reset Tracking and Kerning Επαναφορά απόστασης χαρακτήρων και διαγραμμάτωσης
Sample Color from Screen Δειγματοληψία χρώματος από την οθόνη
Set as Default Ορισμός ως προεπιλογή
Can't render this file because it contains an unexpected character in line 1859 and column 19.
+1
View File
@@ -1072,6 +1072,7 @@
Increase Font Size Aumentar tamaño de fuente
Increase Font Size by Five Steps Aumentar tamaño de fuente en cinco incrementos
Reset Tracking and Kerning Restablecer espaciado e interletraje
Reset Horizontal Scale Restablecer escala horizontal
Spacing Espaciado
Step Type Ajustar texto por incrementos
Fit in View Encajar en la vista
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4845,6 +4845,7 @@ mark-style Japanese Japanilainen
Precision: Tarkkuus:
Preview (1 inch on the page) Esikatselu (1 tuuma sivulla)
Registration: Kohdistuspiste:
Reset Horizontal Scale Palauta vaakaskaalaus
Reset Tracking and Kerning Palauta merkkiväli ja parivälistys
Sample Color from Screen Poimi väri näytöltä
Set as Default Aseta oletukseksi
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1071,6 +1071,7 @@
Increase Font Size Augmenter la taille de police
Increase Font Size by Five Steps Augmenter la taille de police de cinq incréments
Reset Tracking and Kerning Réinitialiser l’approche et le crénage
Reset Horizontal Scale Réinitialiser la mise à l’échelle horizontale
Spacing Espacement
Step Type Texte par incréments
Fit in View Ajuster à la vue
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2913,6 +2913,7 @@
Reset Envelope Shape Burkoló alakjának visszaállítása
Reset Essentials Essentials visszaállítása
Reset Essentials Classic Essentials Classic visszaállítása
Reset Horizontal Scale Vízszintes méretezés visszaállítása
Reset Keyboard Shortcuts Billentyűparancsok visszaállítása
Reset Layout Layout visszaállítása
Reset OpenType Features OpenType-funkció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
@@ -1071,6 +1071,7 @@
Increase Font Size Aumenta dimensione font
Increase Font Size by Five Steps Aumenta dimensione font di cinque incrementi
Reset Tracking and Kerning Reimposta spaziatura caratteri e crenatura
Reset Horizontal Scale Reimposta scala orizzontale
Spacing Spaziatura
Step Type Dimensione testo a incrementi
Fit in View Adatta alla vista
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -332,6 +332,7 @@
Increase Font Size フォントサイズを大きく
Increase Font Size by Five Steps フォントサイズを5段階大きく
Reset Tracking and Kerning トラッキングとカーニングをリセット
Reset Horizontal Scale 水平比率をリセット
Step Type 文字を段階的に調整
Find Font… フォントの検索と置換…
Change Case 大文字と小文字の変更
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -328,6 +328,7 @@
Increase Font Size 글꼴 크기 키우기
Increase Font Size by Five Steps 글꼴 크기 5단계 키우기
Reset Tracking and Kerning 자간 및 커닝 재설정
Reset Horizontal Scale 가로 비율 재설정
Step Type 단계별 문자
Find Font… 글꼴 찾기…
Change Case 대소문자 변경
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3318,6 +3318,7 @@ axis Both Obie
Increase Font Size Zwiększ rozmiar czcionki
Increase Font Size by Five Steps Zwiększ rozmiar czcionki o pięć kroków
Reset Tracking and Kerning Resetuj światło i kerning
Reset Horizontal Scale Resetuj skalę poziomą
Step Type Krok tekstu
Get Text Range Pobierz zakres tekstu
Punctuation Interpunkcja
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -952,6 +952,7 @@
Increase Font Size Aumentar Tamanho da Fonte
Increase Font Size by Five Steps Aumentar Tamanho da Fonte em Cinco Incrementos
Reset Tracking and Kerning Redefinir Rastreamento e Kerning
Reset Horizontal Scale Redefinir Escala Horizontal
Step Type Ajustar Texto em Incrementos
Fit dashes to corners and ends Ajustar tracejados aos cantos e extremidades
Fit in Window Ajustar na Janela
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1069,6 +1069,7 @@
Increase Font Size Увеличить размер шрифта
Increase Font Size by Five Steps Увеличить размер шрифта на пять шагов
Reset Tracking and Kerning Сбросить трекинг и кернинг
Reset Horizontal Scale Сбросить масштаб по горизонтали
Spacing Интервал
Step Type Подгонять текст по шагам
Fit in View Вписать в вид
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4837,6 +4837,7 @@ theme Appearance Utseende
Precision: Precision:
Preview (1 inch on the page) Förhandsgranskning (1 tum på sidan)
Registration: Registreringspunkt:
Reset Horizontal Scale Återställ horisontell skala
Reset Tracking and Kerning Återställ spärrning och kerning
Sample Color from Screen Hämta färg från skärmen
Set as Default Ange som standard
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -1065,6 +1065,7 @@
Increase Font Size Збільшити розмір шрифту
Increase Font Size by Five Steps Збільшити розмір шрифту на п’ять кроків
Reset Tracking and Kerning Скинути трекінг і кернінг
Reset Horizontal Scale Скинути масштаб по горизонталі
Spacing Інтервал
Step Type Підганяти текст кроками
Fit in View Вписати у вигляд
Can't render this file because it contains an unexpected character in line 718 and column 40.
+1
View File
@@ -2662,6 +2662,7 @@
Reset Envelope Mesh Đặt lại lưới Envelope
Reset Envelope Shape Đặt lại hình dạng Envelope
Reset Essentials Đặt lại Essentials
Reset Horizontal Scale Đặt lại tỷ lệ ngang
Reset Keyboard Shortcuts Đặt lại phím tắt
Reset OpenType Features Đặt lại tính năng OpenType
Reset Panel Đặt lại bảng
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
2662
2663
2664
2665
2666
2667
2668
+1
View File
@@ -1020,6 +1020,7 @@
Increase Font Size 增大字体大小
Increase Font Size by Five Steps 按五个增量增大字体大小
Reset Tracking and Kerning 重置字距和字偶距
Reset Horizontal Scale 重置水平缩放
Spacing 间距
Step Type 按增量调整文字
Fit in View 适合视图
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1043,6 +1043,7 @@
Increase Font Size 放大字體大小
Increase Font Size by Five Steps 按五個增量放大字體大小
Reset Tracking and Kerning 重設字距與字距微調
Reset Horizontal Scale 重設水平縮放
Spacing 間距
Step Type 依增量調整文字
Fit in View 符合檢視
Can't render this file because it contains an unexpected character in line 6 and column 47.
+28
View File
@@ -811,6 +811,34 @@ mod tests {
assert_eq!(spacing(&mut app), [(json!("One "), json!(200.0), json!(-40.0)), (json!("Two"), json!(0.0), json!(null))]);
}
/// Cmd+Shift+X resets the horizontal scale of the selected type to 100%, and of the Type
/// tool's selected text while it edits. egui reports the chord as a Cut with Shift held, and
/// the type object is not cut.
#[test]
fn cmd_shift_x_resets_horizontal_scale() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "One Two"})).unwrap()["id"].as_u64().unwrap();
let stretched = |app: &mut VectorcraftApp| app.session.execute("text.setRangeStyle", &json!({"id": id, "hScale": 150})).unwrap();
let scales = |app: &mut VectorcraftApp| {
let r = app.session.execute("text.getRange", &json!({"id": id})).unwrap();
r["runs"].as_array().unwrap().iter().map(|run| (run["text"].clone(), run["style"]["h_scale"].clone())).collect::<Vec<_>>()
};
let chord = vec![egui::Event::ModifiersChanged(Modifiers::COMMAND | Modifiers::SHIFT), egui::Event::Cut];
stretched(&mut app);
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
frame(&mut app, chord.clone());
assert_eq!(scales(&mut app), [(json!("One Two"), json!(100.0))]);
// While the Type tool edits: only its selection, and the text stays.
stretched(&mut app);
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 4, "end": 7}));
assert!(app.session.tool_wants_text());
frame(&mut app, chord);
assert_eq!(scales(&mut app), [(json!("One "), json!(150.0)), (json!("Two"), json!(100.0))]);
}
/// Tab shows and hides the panels, and in the Type tool it types a tab, without moving the
/// keyboard focus on to a field: the keys after it (a tool letter, the next letter typed) still
/// work.
+3 -1
View File
@@ -2025,7 +2025,9 @@ Pen's second anchor at (150, 431), in line with the square's centre.
a caret or tracking of the selection (←/→), leading (↑/↓) and baseline shift (Shift+↑/↓), five steps with Cmd/Ctrl
too. `type.resetTracking` (Cmd+Alt+Q, `press_key {key: "Q", mods: {cmd: true, alt: true}}`) sets tracking to 0 and
kerning to Auto in the type objects in `ids`, the Type tool's selected text or the selected type objects; at a caret
it resets only the kerning of the character before it.
it resets only the kerning of the character before it. `type.resetHorizontalScale` (Cmd+Shift+X,
`press_key {key: "X", mods: {cmd: true, shift: true}}`) sets the horizontal scale to 100%, as the Character panel
shows it, in the same text; at a caret it leaves the text as it is.
- `missingGlyphProtection` (on by default): `text.setStyle` and `text.setRangeStyle` changing the font leave the
characters the new font has no glyph for in the font that had one.
- `typeSelectionByPathOnly` (off by default): on, the Selection tools (`pointer_gesture` with `selection`, and the
+1
View File
@@ -452,6 +452,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). Not yet: Select All across threaded frames (one frame's text is selected).
- Cmd/Ctrl+Alt+Shift+. and , (`type.size.increaseByFive`, `type.size.decreaseByFive`) step the font size of the selected type, or of the text the Type tool has selected, by five times the Size/Leading increment (Preferences › Type); Cmd/Ctrl+Shift+. and , still step it by one increment.
- Cmd/Ctrl+Alt+Q (`type.resetTracking`) sets the tracking of the selected type, or of the text the Type tool has selected, to 0 and its kerning to Auto (0 in the Character panel), in one undo step; at a caret it resets the kerning of the character before it.
- Cmd/Ctrl+Shift+X (`type.resetHorizontalScale`) sets the horizontal scale of the selected type, or of the text the Type tool has selected, to 100% as the Character panel shows it (the object's own stretch included), in one undo step; at a caret it leaves the text as it is.
- Shortcut sets other apps save as text (`.kys`) open in Import Shortcut Set from File › Open, from Edit › Keyboard Shortcuts › Import… in the desktop app, or from a drop on the window. It matches each entry to a tool, command or panel by name and decodes the file's key and modifier codes where VectorCraft's default shortcuts agree on them; the user answers the rest, and Apply fills the Keyboard Shortcuts dialog's working set. The answers are kept with the preferences for the next import (`shortcutSet.read`, `importMatches.*`).
- On Windows and Linux the app bar is the window's title bar (its own minimize, maximize and close buttons, drag to move, edges to resize), with the dragon app icon as the brand mark; dialogs and menus size to their content. As on a native menu bar, once one menu is open, moving the pointer onto another title opens that one.
- **Tools:**