Type: Touch Type moves a character rotated near 90 degrees across its line

Baseline shift now moves a character in the line's frame, outside
Character Rotation, so a positive shift moves a character up from its
line at any rotation, and the character turns about its shifted pen
position. The Touch Type tool splits a drag into its part along the line
(kerning) and its part up from the line (baseline shift) at every
rotation; before, within about 6 degrees of a quarter turn it dropped
the cross-line part.

Characters that already combine a rotation with a baseline shift are
drawn in a new position. Their stored values do not change.

- text: glyph transform order, PositionedGlyph::pivot (the turning
  point), tab leaders in the same order
- tools: Touch Type splits drags against the line's up axis and measures
  rotate-handle turns about the shifted pen position
- engine: text.touchType description; dy is up from the line
- docs: mcp.md Touch Type paragraph, target-app-parity.md progress line
This commit is contained in:
dotCooCoo
2026-11-01 11:08:57 +07:00
committed by bflatastic
parent 94ef9b87c2
commit 5716a996ab
12 changed files with 230 additions and 24 deletions
+3 -4
View File
@@ -48,7 +48,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Touch Type",
[],
None,
"{id, start: byte (a character of it), dx?: pt, dy?: pt, hScale?: %, vScale?: %, rotation?: deg} change one character (a ligature, one glyph for several characters, as a whole) as a Touch Type tool drag does, as one undo step: dx moves it along its line in reading order by changing the kerning of the character before it on the line and its own kerning, and the characters after it stay where they are (an error for the first character of a line and for a right-to-left character; limited to what kerning from -1000 to 10000 can give); dy raises it (baseline shift, along its own up axis when it is rotated); dx and dy are document points; hScale (including the object transform), vScale and rotation set its scales and rotation as text.setRangeStyle does. The kerning of the two characters also keeps the character and the ones after it where they were along the line when the font kerned them with their neighbors; a wider or narrower character moves the ones after it by the change in its width → {id, start, end} (the character's bytes)",
"{id, start: byte (a character of it), dx?: pt, dy?: pt, hScale?: %, vScale?: %, rotation?: deg} change one character (a ligature, one glyph for several characters, as a whole) as a Touch Type tool drag does, as one undo step: dx moves it along its line in reading order by changing the kerning of the character before it on the line and its own kerning, and the characters after it stay where they are (an error for the first character of a line and for a right-to-left character; limited to what kerning from -1000 to 10000 can give); dy raises it (baseline shift, up from its line whatever its rotation, also for the first character of a line and a right-to-left character); dx and dy are document points; hScale (including the object transform), vScale and rotation set its scales and rotation as text.setRangeStyle does. The kerning of the two characters also keeps the character and the ones after it where they were along the line when the font kerned them with their neighbors; a wider or narrower character moves the ones after it by the change in its width → {id, start, end} (the character's bytes)",
has_doc,
touch_type
),
@@ -916,9 +916,8 @@ fn touch_type(s: &mut Session, p: &Value) -> Result<Value> {
});
};
// The baseline shift that raises the character `dy` document points. Baseline shift moves it
// along its own up axis, turned with its rotation (the new one, when the call sets it), by a
// length that the object transform, Shrink Text to Fit and a path effect such as Skew scale:
// two layouts with the call's other changes and shifts one step apart, both within the range,
// up from its line, whatever its rotation, by a length that the object transform, Shrink Text
// to Fit and a path effect such as Skew scale: two layouts with the call's other changes and shifts one step apart, both within the range,
// measure that length. Neither shift is one that a run already has, so the character's run
// splits off alike in both.
let raise = if dy == 0.0 {
+59 -2
View File
@@ -518,7 +518,7 @@ fn touch_type_moves_one_character_in_centered_and_right_aligned_lines() {
}
}
/// `dy` moves a rotated character that many document points along its own up axis, also in type
/// `dy` moves a rotated character that many document points up from its line, also in type
/// scaled unevenly.
#[test]
fn touch_type_raises_a_rotated_character_in_unevenly_scaled_type() {
@@ -529,9 +529,66 @@ fn touch_type_raises_a_rotated_character_in_unevenly_scaled_type() {
let before = glyph_pens(&s, id);
s.execute("text.touchType", &json!({"id": id.0, "start": 1, "dy": 10})).unwrap();
let (x, y) = moved_by(&before, &glyph_pens(&s, id), 1);
assert!(((x * x + y * y).sqrt() - 10.0).abs() < 1e-6, "({x}, {y})");
assert!(near((x, y), (0.0, -10.0)), "({x}, {y})");
}
/// A character rotated near or at a quarter turn moves across its line and along it: in "ABCD"
/// with the B at 85, 90 and 95 degrees, `dx` 10 and `dy` 4 move the B 10 pt right and 4 pt up and
/// leave the C and D where they are, through baseline shift and kerning only, with Auto and
/// Optical kerning and in type scaled unevenly. Undo, redo and the native format keep the place.
#[test]
fn touch_type_moves_a_character_turned_near_a_quarter_turn_across_its_line() {
for rotation in [85.0, 90.0, 95.0, -90.0] {
for (kerning, scaled) in [("auto", false), ("optical", false), ("auto", true)] {
let case = format!("{rotation}°, {kerning}, scaled {scaled}");
let mut s = session();
let id = NodeId(s.execute("text.create", &json!({"x": 100, "y": 100, "text": "ABCD", "size": 40})).unwrap()["id"].as_u64().unwrap());
s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": kerning})).unwrap();
if scaled {
s.execute("object.transform", &json!({"ids": [id.0], "matrix": [2, 0, 0, 0.5, 0, 0]})).unwrap();
}
s.execute("text.touchType", &json!({"id": id.0, "start": 1, "rotation": rotation})).unwrap();
let before = glyph_pens(&s, id);
let runs_before = obj(&s, id).runs;
s.execute("text.touchType", &json!({"id": id.0, "start": 1, "dx": 10, "dy": 4})).unwrap();
let after = glyph_pens(&s, id);
assert!(near(moved_by(&before, &after, 1), (10.0, -4.0)), "{case}: {:?}", moved_by(&before, &after, 1));
for byte in [0, 2, 3] {
assert!(near(moved_by(&before, &after, byte), (0.0, 0.0)), "{case}, byte {byte}: {:?}", moved_by(&before, &after, byte));
}
// Only kerning and baseline shift change.
let t = obj(&s, id);
let style = |byte: usize| vectorcraft_text::edit::slice_runs(&t.runs, byte, byte + 1)[0].style.clone();
let plain = |mut st: CharStyle| {
st.kerning = None;
st.baseline_shift = 0.0;
st
};
for byte in 0..4 {
let was = vectorcraft_text::edit::slice_runs(&runs_before, byte, byte + 1)[0].style.clone();
assert_eq!(plain(style(byte)), plain(was), "{case}, byte {byte}");
}
let shift = style(1).baseline_shift;
assert!(shift > 0.0, "{case}: {shift}");
// A drag straight across the line is baseline shift alone.
let before = glyph_pens(&s, id);
s.execute("text.touchType", &json!({"id": id.0, "start": 1, "dy": -6})).unwrap();
assert!(near(moved_by(&before, &glyph_pens(&s, id), 1), (0.0, 6.0)), "{case}: {:?}", moved_by(&before, &glyph_pens(&s, id), 1));
s.execute("edit.undo", &json!({})).unwrap();
let moved = glyph_pens(&s, id);
assert!(near(moved_by(&after, &moved, 1), (0.0, 0.0)), "{case}: undone");
s.execute("edit.redo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
// The native format keeps both attributes and the place they give.
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
let NodeKind::Text(reopened) = &back.node(id).unwrap().kind else { panic!("not text") };
assert_eq!(reopened.runs, obj(&s, id).runs, "{case}");
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), reopened);
let pens: Vec<(usize, Point)> = lay.glyphs.iter().map(|g| (g.byte, reopened.xf * (g.xf * Point::ZERO))).collect();
assert!(near(moved_by(&moved, &pens, 1), (0.0, 0.0)), "{case}: reopened");
}
}
}
/// `dy` moves a character that many document points in type scaled up 2000 times, where baseline
/// shift is limited to 0.648 pt of text space.
#[test]