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]