mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
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:
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -227,12 +227,22 @@ impl Ctx<'_> {
|
||||
let local = match &g.inline {
|
||||
// Inline graphics: art space to glyph space (no font outline).
|
||||
Some(ib) => Affine::translate((-lead, -g.bshift)) * ib.xf,
|
||||
// Baseline shift moves the glyph across its line, and Character Rotation then turns it
|
||||
// about its shifted pen position, so the shift is across the line at any rotation.
|
||||
None => {
|
||||
Affine::rotate(-g.rotation.to_radians()) * Affine::translate((g.dx - lead, g.dy - g.bshift)) * Affine::scale_non_uniform(g.sx, g.sy)
|
||||
Affine::translate((0.0, -g.bshift))
|
||||
* Affine::rotate(-g.rotation.to_radians())
|
||||
* Affine::translate((g.dx - lead, g.dy))
|
||||
* Affine::scale_non_uniform(g.sx, g.sy)
|
||||
}
|
||||
};
|
||||
// The turning point in font units, which later moves of the glyph's transform keep.
|
||||
let pivot = match local.determinant() {
|
||||
det if det != 0.0 && det.is_finite() => local.inverse() * Point::new(0.0, -g.bshift),
|
||||
_ => Point::ZERO,
|
||||
};
|
||||
let unturned = if g.inline.is_none() && g.rotation != 0.0 && g.sx != 0.0 && g.sy != 0.0 {
|
||||
let placed = Affine::translate((g.dx - lead, g.dy - g.bshift)) * Affine::scale_non_uniform(g.sx, g.sy);
|
||||
let placed = Affine::translate((g.dx - lead, g.dy)) * Affine::scale_non_uniform(g.sx, g.sy);
|
||||
placed.inverse() * Affine::rotate(g.rotation.to_radians()) * placed
|
||||
} else {
|
||||
Affine::IDENTITY
|
||||
@@ -306,6 +316,7 @@ impl Ctx<'_> {
|
||||
colour,
|
||||
bend: None,
|
||||
unturned,
|
||||
pivot,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ pub(super) fn leader_outline(db: &FontDb, g: &SGlyph, leader: &str, origin: f64,
|
||||
}
|
||||
let first = ((from - origin) / width - EPS).ceil();
|
||||
let copies = (((to - origin) / width + EPS).floor() - first).clamp(0.0, MAX_LEADER_COPIES) as usize;
|
||||
let local = Affine::rotate(-g.rotation.to_radians()) * Affine::translate((0.0, -g.bshift)) * Affine::scale_non_uniform(g.sx, g.sy);
|
||||
let local = Affine::translate((0.0, -g.bshift)) * Affine::rotate(-g.rotation.to_radians()) * Affine::scale_non_uniform(g.sx, g.sy);
|
||||
for k in 0..copies {
|
||||
let mut pen = origin + (first + k as f64) * width;
|
||||
for &(gid, adv) in &glyphs {
|
||||
|
||||
@@ -368,5 +368,6 @@ fn ruby_glyph(g: &PositionedGlyph, m: Affine, run: usize, byte: usize, line: usi
|
||||
colour: None,
|
||||
bend: None,
|
||||
unturned: Affine::IDENTITY,
|
||||
pivot: m * g.origin,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,9 @@ pub struct PositionedGlyph {
|
||||
/// Font units → font units: `xf * unturned` places the glyph as it would be placed without
|
||||
/// its Character Rotation. The identity for a glyph without one.
|
||||
pub unturned: Affine,
|
||||
/// Font units: `xf * pivot` is the point its Character Rotation turns it about, its pen
|
||||
/// position on the baseline moved by its baseline shift.
|
||||
pub pivot: Point,
|
||||
}
|
||||
|
||||
/// How a glyph on a path is bent along it: a point of its pen space (x along its advance from its
|
||||
|
||||
@@ -164,6 +164,7 @@ fn lines_of_clusters(lines: &[&[(usize, usize, f64)]], rtl: bool) -> TextLayout
|
||||
colour: None,
|
||||
bend: None,
|
||||
unturned: Affine::IDENTITY,
|
||||
pivot: Point::new(x, baseline),
|
||||
});
|
||||
x += advance;
|
||||
}
|
||||
|
||||
@@ -1430,6 +1430,51 @@ fn restyling_one_character_keeps_its_kerning_with_its_neighbors() {
|
||||
assert!((l.lines[1].baseline - l.lines[0].baseline - 60.0).abs() < 1e-9, "{:?}", l.lines);
|
||||
}
|
||||
|
||||
/// Baseline shift moves a rotated character up from its line by the shift, at any rotation, in
|
||||
/// point type and on a slanted path, and leaves its neighbors where they are. It turns about its
|
||||
/// pen position moved by the shift (`xf * pivot`), and `xf * unturned` still places the shifted
|
||||
/// glyph without its rotation.
|
||||
#[test]
|
||||
fn baseline_shift_moves_a_rotated_character_across_its_line() {
|
||||
let slanted = vectorcraft_geom::PathData::from_bezpath(&kurbo::Line::new((0.0, 0.0), (300.0, -300.0)).to_path(1e-3));
|
||||
for on_path in [false, true] {
|
||||
for rotation in [0.0, 45.0, 85.0, 90.0, 95.0, -90.0] {
|
||||
let at = |shift: f64| {
|
||||
let mut t = avav(1, |s| {
|
||||
s.rotation = rotation;
|
||||
s.baseline_shift = shift;
|
||||
});
|
||||
if on_path {
|
||||
t.kind = TextKind::OnPath { path: slanted.clone(), start: 0.0, end: None };
|
||||
}
|
||||
layout(db(), &t)
|
||||
};
|
||||
let (plain, shifted) = (at(0.0), at(10.0));
|
||||
let pen = |l: &TextLayout, byte: usize| l.glyphs.iter().find(|g| g.byte == byte).map(|g| g.xf * Point::ZERO).unwrap();
|
||||
let v = plain.glyphs.iter().find(|g| g.byte == 1).unwrap();
|
||||
let up = kurbo::Vec2::new(v.angle.sin(), -v.angle.cos());
|
||||
let moved = pen(&shifted, 1) - pen(&plain, 1);
|
||||
assert!((moved - up * 10.0).hypot() < 1e-6, "on a path {on_path}, {rotation}°: {moved:?}, up {up:?}");
|
||||
for byte in [0, 2, 3] {
|
||||
assert!((pen(&shifted, byte) - pen(&plain, byte)).hypot() < 1e-6, "on a path {on_path}, {rotation}°: byte {byte} moved");
|
||||
}
|
||||
let g = shifted.glyphs.iter().find(|g| g.byte == 1).unwrap();
|
||||
let turn_at = g.xf * g.pivot;
|
||||
assert!((turn_at - (v.origin + up * 10.0)).hypot() < 1e-6, "on a path {on_path}, {rotation}°: turns about {turn_at:?}");
|
||||
let unrotated = layout(db(), &{
|
||||
let mut t = avav(1, |s| s.baseline_shift = 10.0);
|
||||
if on_path {
|
||||
t.kind = TextKind::OnPath { path: slanted.clone(), start: 0.0, end: None };
|
||||
}
|
||||
t
|
||||
});
|
||||
let u = unrotated.glyphs.iter().find(|g| g.byte == 1).unwrap();
|
||||
let (a, b) = ((g.xf * g.unturned).as_coeffs(), u.xf.as_coeffs());
|
||||
assert!(a.iter().zip(b).all(|(a, b)| (a - b).abs() < 1e-6), "on a path {on_path}, {rotation}°: {a:?} vs {b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracking_or_kerning_on_one_character_keeps_the_kerning_before_it() {
|
||||
let plain = pens(&TextObject::point(Point::ZERO, "AVAV", style(40.0)));
|
||||
|
||||
@@ -53,12 +53,14 @@ struct CharBox {
|
||||
quad: [Point; 4],
|
||||
/// The glyph's origin on its shifted baseline, which the scale handles measure from.
|
||||
pen: Point,
|
||||
/// Its pen position on the line's baseline, which character rotation turns it about.
|
||||
/// The point character rotation turns it about: its pen position on the line's baseline,
|
||||
/// moved by its baseline shift.
|
||||
pivot: Point,
|
||||
/// A unit vector along its line in reading order.
|
||||
along: Vec2,
|
||||
/// The glyph's own x axis and its up axis (with its rotation, which baseline shift moves it
|
||||
/// along), unit vectors.
|
||||
/// A unit vector up from its line, without its rotation: the way baseline shift moves it.
|
||||
up: Vec2,
|
||||
/// The glyph's own x axis and its up axis (with its rotation), unit vectors.
|
||||
ex: Vec2,
|
||||
ey: Vec2,
|
||||
/// Whether the tool moves it along its line by kerning: a character comes before it on its
|
||||
@@ -177,8 +179,9 @@ fn char_box(t: &TextObject, lay: &TextLayout, gi: usize) -> Option<CharBox> {
|
||||
start: g.byte,
|
||||
quad,
|
||||
pen,
|
||||
pivot: g.origin,
|
||||
pivot: m * g.pivot,
|
||||
along: if g.rtl { -d } else { d },
|
||||
up: unit(linear(m * g.unturned, Vec2::new(0.0, -1.0))),
|
||||
ex,
|
||||
ey,
|
||||
kerned: !g.rtl && lay.glyphs.iter().any(|h| h.line == g.line && h.len > 0 && h.byte + h.len == g.byte),
|
||||
@@ -239,20 +242,21 @@ impl TouchTypeTool {
|
||||
}
|
||||
|
||||
/// Document-space move for a character box (`b` of `t`): kerning along the line and baseline
|
||||
/// shift along the character's up axis.
|
||||
/// shift up from it, whatever the character's rotation. The move (text space) is split into
|
||||
/// its parts along the line and up from it; a character that is not kerned only moves up or
|
||||
/// down. When the two axes are parallel, the params hold no move.
|
||||
fn move_params(id: NodeId, t: &TextObject, b: &CharBox, moved: Vec2) -> Value {
|
||||
let doc_len = |v: Vec2| linear(t.xf, v).hypot();
|
||||
let det = b.along.cross(b.ey);
|
||||
let (along, rise) = if !b.kerned {
|
||||
(0.0, moved.dot(b.ey))
|
||||
} else if det.abs() > 0.1 {
|
||||
(moved.cross(b.ey) / det, b.along.cross(moved) / det)
|
||||
let det = b.along.cross(b.up);
|
||||
let (along, rise) = if det.abs() <= 1e-9 || !det.is_finite() {
|
||||
(0.0, 0.0)
|
||||
} else {
|
||||
(moved.dot(b.along), 0.0)
|
||||
let (along, rise) = (moved.cross(b.up) / det, b.along.cross(moved) / det);
|
||||
if b.kerned { (along, rise) } else { (0.0, rise) }
|
||||
};
|
||||
let mut out = json!({"id": id.0, "start": b.start});
|
||||
let dx = along * doc_len(b.along);
|
||||
let dy = rise * doc_len(b.ey);
|
||||
let dy = rise * doc_len(b.up);
|
||||
if dx != 0.0 {
|
||||
out["dx"] = json!(dx);
|
||||
}
|
||||
|
||||
@@ -140,6 +140,89 @@ fn dragging_the_box_moves_the_character_along_its_line_and_up() {
|
||||
assert!(close(p3.get("dx").and_then(|v| v.as_f64()).unwrap_or(0.0), 0.0) && close(p3["dy"].as_f64().unwrap(), 4.0), "{p3}");
|
||||
}
|
||||
|
||||
/// 40 pt point type "Type" at (100, 200) whose "y" is rotated by `rotation` degrees, selected.
|
||||
fn doc_with_rotated_y(rotation: f64) -> (Document, NodeId, Selection) {
|
||||
let (mut d, id, s) = doc_with_type();
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
||||
edit::style_range(&mut t.runs, 1, 2, |st| st.rotation = rotation);
|
||||
}
|
||||
(d, id, s)
|
||||
}
|
||||
|
||||
/// Near and at a quarter turn, a drag still splits into kerning along the line and baseline shift
|
||||
/// across it: right 10 pt and up 4 pt is `dx` 10 and `dy` 4 at every rotation, and a drag straight
|
||||
/// across the line is all `dy`. Arrow keys nudge the same way.
|
||||
#[test]
|
||||
fn a_character_rotated_near_a_quarter_turn_moves_across_its_line() {
|
||||
for rotation in [45.0, 85.0, 90.0, 95.0, -90.0] {
|
||||
let (d, id, s) = doc_with_rotated_y(rotation);
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = TouchTypeTool { target: Some((id, 1)), ..Default::default() };
|
||||
let q = quad(&t, &cx);
|
||||
let at = q[0].midpoint(q[2]);
|
||||
let acts = drag(&mut t, &cx, at, at + Vec2::new(10.0, -4.0));
|
||||
let p1 = preview(&acts[1]);
|
||||
assert!(close(p1["dx"].as_f64().unwrap(), 10.0) && close(p1["dy"].as_f64().unwrap(), 4.0), "{rotation}°: {p1}");
|
||||
let acts = drag(&mut t, &cx, at, at + Vec2::new(0.0, -6.0));
|
||||
let p2 = preview(&acts[1]);
|
||||
assert!(p2.get("dx").is_none_or(|v| close(v.as_f64().unwrap(), 0.0)) && close(p2["dy"].as_f64().unwrap(), 6.0), "{rotation}°: {p2}");
|
||||
let acts = t.key(&cx, ToolKey::Up, Mods::default());
|
||||
let p3 = match &acts[0] {
|
||||
Action::Exec(c, v) if c == "text.touchType" => v,
|
||||
other => panic!("{other:?}"),
|
||||
};
|
||||
assert!(close(p3["dy"].as_f64().unwrap(), 1.0) && p3.get("dx").is_none_or(|v| close(v.as_f64().unwrap(), 0.0)), "{rotation}°: {p3}");
|
||||
}
|
||||
}
|
||||
|
||||
/// In type scaled unevenly, a drag's parts are measured in document points: right 10 pt and up 4
|
||||
/// pt of a quarter-turned character is `dx` 10 and `dy` 4.
|
||||
#[test]
|
||||
fn a_quarter_turned_character_in_unevenly_scaled_type_follows_the_pointer() {
|
||||
let (mut d, id, s) = doc_with_rotated_y(90.0);
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
||||
t.xf = Affine::scale_non_uniform(2.0, 0.5) * t.xf;
|
||||
}
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = TouchTypeTool { target: Some((id, 1)), ..Default::default() };
|
||||
let q = quad(&t, &cx);
|
||||
let at = q[0].midpoint(q[2]);
|
||||
let acts = drag(&mut t, &cx, at, at + Vec2::new(10.0, -4.0));
|
||||
let p1 = preview(&acts[1]);
|
||||
assert!(close(p1["dx"].as_f64().unwrap(), 10.0) && close(p1["dy"].as_f64().unwrap(), 4.0), "{p1}");
|
||||
}
|
||||
|
||||
/// A character with a baseline shift turns about its pen position moved up by the shift, rotated
|
||||
/// or not, and the rotate handle measures its turn about that point.
|
||||
#[test]
|
||||
fn a_shifted_character_turns_about_its_shifted_pen_position() {
|
||||
for rotation in [0.0, 30.0, 90.0] {
|
||||
let (mut d, id, s) = doc_with_rotated_y(rotation);
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
||||
edit::style_range(&mut t.runs, 1, 2, |st| st.baseline_shift = 10.0);
|
||||
}
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let t = TouchTypeTool { target: Some((id, 1)), ..Default::default() };
|
||||
let (_, text, b) = t.current(&cx).expect("a character is selected");
|
||||
let lay = vectorcraft_text::layout(FontDb::global(), text);
|
||||
let y = lay.glyphs.iter().find(|g| g.byte == 1).unwrap();
|
||||
let want = y.origin + Vec2::new(0.0, -10.0);
|
||||
assert!((b.pivot - want).hypot() < 1e-6, "{rotation}°: {:?}, want {want:?}", b.pivot);
|
||||
// The rotate handle turned a quarter turn about that point is a quarter turn more.
|
||||
let mut t = t;
|
||||
let top = text.xf * b.handle(Part::Rotate);
|
||||
let centre = text.xf * want;
|
||||
let r = top - centre;
|
||||
let acts = drag(&mut t, &cx, top, centre + Vec2::new(r.y, -r.x));
|
||||
let ps = preview(&acts[1]);
|
||||
let turned = (ps["rotation"].as_f64().unwrap() - rotation - 90.0).rem_euclid(360.0);
|
||||
assert!(turned.min(360.0 - turned) < 1e-6, "{rotation}°: {ps}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrow_keys_nudge_the_selected_character() {
|
||||
let (d, id, s) = doc_with_type();
|
||||
|
||||
+4
-3
@@ -5168,9 +5168,10 @@ offset of the character; a ligature (one glyph for several characters) changes a
|
||||
`end` give its bytes. `dx` (document points, in reading order) changes the kerning of the character before it on
|
||||
its line and its own kerning, so the character moves by `dx` and the characters after it stay where they are. The first
|
||||
character of a line and a right-to-left character take no `dx`, and `dx` is limited to what kerning from -1000 to 10000 (1/1000 em) on the two
|
||||
characters can give. `dy` raises it by baseline shift, which moves a rotated character along its own up axis. The tool
|
||||
splits a drag of a character rotated by up to about 84 degrees between kerning and baseline shift, so the character
|
||||
follows the pointer; nearer a quarter turn, a drag moves it only along its line. `hScale`, `vScale`
|
||||
characters can give. `dy` raises it by baseline shift, which moves the character up from its line at any rotation, a quarter
|
||||
turn included; a character rotated with a baseline shift turns about its shifted pen position. The tool splits a drag
|
||||
into its part along the line (kerning) and its part up from the line (baseline shift), so the character follows the
|
||||
pointer at any rotation; the first character of a line and a right-to-left character follow it only up and down. `hScale`, `vScale`
|
||||
and `rotation` set its scales and rotation as `text.setRangeStyle` does. Where the font kerned the character with its
|
||||
neighbors, the kerning of the two characters keeps it and the characters after it where they were along the line; a
|
||||
wider or narrower character moves the characters after it by the change in its width. Touch Type gives the character
|
||||
|
||||
@@ -739,6 +739,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
While a Type tool edits type, a click that lands on no type ends the editing without making new type, and that tool stays active; the next click makes new type (#2871, #2854).
|
||||
Duplicate Artboards, the Artboard tool's Alt-drag and Paste give the new artboard an id that no other artboard has, also when an artboard's id is the largest possible value (#1718). Deleting the copy or the original then leaves the other's guides.
|
||||
- **Touch Type:** a click with the Touch Type tool (Shift+T) selects one character and shows a box around it. Dragging the box or its lower-left handle moves the character. The upper-left handle scales it vertically, the lower-right handle scales it horizontally, the upper-right handle scales it proportionally, and the upper-middle handle rotates it. Each drag is one undo step that sets the character's kerning, baseline shift, scales or rotation, and the type stays editable (`text.touchType`; behavior reference: [Touch Type tool](https://web.archive.org/web/20261007185617/https://helpx.adobe.com/illustrator/using/tool-techniques/touch-type-tool.html), [Add text with the Type, Area Type, and Touch Type tools](https://web.archive.org/web/20260926095922/https://helpx.adobe.com/illustrator/desktop/design-with-text/add-manage-text/add-text-illustrator-text-tools.html)).
|
||||
A character rotated near or at a quarter turn follows a Touch Type drag across its line as well as along it: baseline shift moves a character up from its line at any rotation, and the character turns about its shifted pen position.
|
||||
- **Temporary tools:** Cmd with any other tool drags with the selection tool used last (Selection, Direct Selection or Group Selection). Held Space pans and Cmd+Space zooms (Cmd+Alt+Space out) whatever the tool.
|
||||
Space held while the Zoom tool drags a marquee moves the marquee and keeps its size ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). When Space is let go, the drag sizes the marquee again from where it moved to.
|
||||
A Space+Shift drag rotates the view whatever the tool, as a Rotate View tool drag does, and Shift does not round the angle to 15°.
|
||||
|
||||
Reference in New Issue
Block a user