Snap to Glyph: the x-height guide stays on the glyphs' x-height

Character Rotation turned a glyph's baseline shift and scaling with the glyph, and the Skew path effect slants a glyph's height away from the path's normal, so the x-height guide moved off the glyphs. Each positioned glyph now carries the transform that removes its Character Rotation (PositionedGlyph::unturned), and the guide goes through the glyph's x-height point placed without it, measured across the baseline. Snap to Glyph also takes Smart Guides being on in the tools, as View › Snap to Glyph already does in the engine.
This commit is contained in:
dotCooCoo
2026-10-29 11:18:38 +07:00
committed by bflatastic
parent f87b8af62b
commit bbf01223cb
7 changed files with 73 additions and 5 deletions
+7
View File
@@ -216,6 +216,12 @@ impl Ctx<'_> {
Affine::rotate(-g.rotation.to_radians()) * Affine::translate((g.dx - lead, g.dy - g.bshift)) * Affine::scale_non_uniform(g.sx, g.sy)
}
};
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);
placed.inverse() * Affine::rotate(g.rotation.to_radians()) * placed
} else {
Affine::IDENTITY
};
let mut m = pre * local;
if self.vertical && self.on_path {
// Vertical path type keeps the baseline path and turns each glyph across it.
@@ -271,6 +277,7 @@ impl Ctx<'_> {
image,
colour,
bend: None,
unturned,
});
}
+1
View File
@@ -367,5 +367,6 @@ fn ruby_glyph(g: &PositionedGlyph, m: Affine, run: usize, byte: usize, line: usi
image: None,
colour: None,
bend: None,
unturned: Affine::IDENTITY,
}
}
+3
View File
@@ -136,6 +136,9 @@ pub struct PositionedGlyph {
/// the path ([`GlyphBend`]) so it meets its neighbours, and `xf` is only the straight glyph
/// nearest it (for the caret, hit testing and tools). `None`: the glyph is drawn through `xf`.
pub bend: Option<GlyphBend>,
/// 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,
}
/// How a glyph on a path is bent along it: a point of its pen space (x along its advance from its
+1
View File
@@ -163,6 +163,7 @@ fn lines_of_clusters(lines: &[&[(usize, usize, f64)]], rtl: bool) -> TextLayout
image: None,
colour: None,
bend: None,
unturned: Affine::IDENTITY,
});
x += advance;
}
+1 -1
View File
@@ -307,7 +307,7 @@ impl SnapKey {
exclude: exclude.to_vec(),
screen: cx.screen,
view: [cx.smart_guides, cx.snap_to_point, cx.snap_to_pixel, cx.snap_to_grid, cx.guides, cx.snap_tangent, cx.snap_perpendicular],
snap_to_glyph: cx.snap_to_glyph && cx.alignment_guides,
snap_to_glyph: cx.snap_to_glyph && cx.smart_guides && cx.alignment_guides,
style: GuideStyle::of(cx),
tolerances: [cx.snapping_tolerance, cx.snap_tolerance],
}
+4 -4
View File
@@ -292,10 +292,10 @@ impl Targets {
let ahead = Vec2::new(g.angle.cos(), g.angle.sin());
let up = Vec2::new(ahead.y, -ahead.x);
let dir = along(t.xf, ahead);
// The x-height in points (font units scaled as the glyph's height is) above the
// baseline the glyph is drawn on, raised or lowered by its baseline shift.
let shift = (g.xf * Point::ZERO - g.origin).dot(up);
let at = to_doc(g.origin + up * (shift + face.x_height() * along(g.xf, Vec2::new(0.0, 1.0)).hypot()));
// The x-height of the glyph as placed without its Character Rotation (raised or
// lowered by its baseline shift), measured across the baseline.
let top = g.xf * g.unturned * Point::new(0.0, -face.x_height());
let at = to_doc(g.origin + up * (top - g.origin).dot(up));
let same = last_x_height.is_some_and(|(a, d)| {
let (l, k) = (d.hypot(), dir.hypot());
l > 1e-12 && k > 1e-12 && (d / l - dir / k).hypot() <= 1e-9 && ((at - a).cross(d / l)).abs() <= 1e-6
+56
View File
@@ -651,6 +651,13 @@ fn pen_click_near_a_glyph_anchor_lands_on_it() {
};
assert!(first_anchor(true).distance(anchor) < 1e-9, "{:?} vs {anchor:?}", first_anchor(true));
assert!(first_anchor(false).distance(anchor) > 1e-3);
// Snap to Glyph works with Smart Guides on: Snap to Point alone does not land on glyph anchors.
let c = ToolContext { snap_to_glyph: true, smart_guides: false, snap_to_point: true, ..cx(&d, &s, &p) };
let mut pen = crate::pen::PenTool::default();
let acts = pen.pointer(&c, &PointerEvent::new(PointerKind::Down, click.x, click.y));
let Some(Action::Preview(_, v)) = acts.get(1) else { panic!("{acts:?}") };
let q = Point::new(v["anchors"][0]["x"].as_f64().unwrap(), v["anchors"][0]["y"].as_f64().unwrap());
assert!(q.distance(anchor) > 1e-3, "{q:?} landed on the glyph anchor with Smart Guides off");
}
/// Snap to Glyph: a single line of type has an x-height guide; a paragraph shows glyph bounds only.
@@ -673,6 +680,55 @@ fn drawn_points_line_up_with_the_x_height_of_a_single_line() {
let (d, _) = glyph_doc_with("H", 0.0, vectorcraft_doc::CharStyle { size: 60.0, baseline_shift: 10.0, ..Default::default() });
let (q, _) = Targets::for_drawing(&d, &[], None, true).snap_point(Point::new(400.0, x_height - 10.0 + 2.0), 4.0);
assert!((q.y - (x_height - 10.0)).abs() < 1e-9, "{q:?}, shifted x-height {}", x_height - 10.0);
// Character Rotation turns the glyph, not its baseline shift.
let style = vectorcraft_doc::CharStyle { size: 60.0, baseline_shift: 10.0, rotation: 30.0, ..Default::default() };
let (d, _) = glyph_doc_with("H", 0.0, style);
let (q, _) = Targets::for_drawing(&d, &[], None, true).snap_point(Point::new(400.0, x_height - 10.0 + 2.0), 4.0);
assert!((q.y - (x_height - 10.0)).abs() < 1e-9, "{q:?}, shifted x-height {}", x_height - 10.0);
// Glyph Scaling widens a rotated glyph without raising its x-height.
let mut t =
vectorcraft_doc::TextObject::point(Point::ZERO, "HH HH", vectorcraft_doc::CharStyle { size: 60.0, rotation: 90.0, ..Default::default() });
let frame = kurbo::Shape::to_path(&kurbo::Rect::new(50.0, 100.0, 450.0, 400.0), 0.1);
t.kind = vectorcraft_doc::TextKind::Area { frame: vectorcraft_geom::PathData::from_bezpath(&frame) };
t.xf = Affine::IDENTITY;
t.para.justify = vectorcraft_doc::Justify::JustifyAll;
t.para.justification.glyph_scaling = [100.0, 100.0, 200.0];
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::Text(Box::new(t.clone())))).unwrap();
let lay = layout_text(FontDb::global(), &t);
assert_eq!(lay.lines.len(), 1);
let g = lay.glyphs.iter().find(|g| !g.outline.elements().is_empty()).expect("a glyph with an outline");
assert!(g.scale > 1.2, "Glyph Scaling widens the glyph: {}", g.scale);
let expected = (t.xf * g.origin).y - (300.0 - x_height);
let (q, _) = Targets::for_drawing(&d, &[], None, true).snap_point(Point::new(30.0, expected + 2.0), 4.0);
assert!((q.y - expected).abs() < 1e-9, "{q:?}, x-height {expected}");
}
/// Snap to Glyph: the x-height guide of type skewed along a slanted path runs through the glyph's
/// x-height.
#[test]
fn the_x_height_guide_of_skewed_path_type_runs_through_the_glyphs_x_height() {
let mut path = kurbo::BezPath::new();
path.move_to((100.0, 400.0));
path.line_to((300.0, 54.0));
let mut t = vectorcraft_doc::TextObject::point(Point::ZERO, "H", vectorcraft_doc::CharStyle { size: 60.0, ..Default::default() });
t.kind = vectorcraft_doc::TextKind::OnPath { path: vectorcraft_geom::PathData::from_bezpath(&path), start: 0.0, end: None };
t.xf = Affine::IDENTITY;
t.path_effect = vectorcraft_doc::PathEffect::Skew;
let mut d = vectorcraft_doc::Document::new(500.0, 500.0);
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::Text(Box::new(t.clone())))).unwrap();
let lay = layout_text(FontDb::global(), &t);
let g = lay.glyphs.iter().find(|g| !g.outline.elements().is_empty()).expect("a glyph with an outline");
let face = FontDb::global().face_by_id(g.font_id).unwrap();
let top = t.xf * g.xf * Point::new(0.0, -face.x_height());
let ahead = Vec2::new(g.angle.cos(), g.angle.sin());
let normal = Vec2::new(ahead.y, -ahead.x);
// Beside the guide's line, past the glyph's end along it.
let p = top + ahead * 80.0 + normal * 1.5;
let (q, _) = Targets::for_drawing(&d, &[], None, true).snap_point(p, 4.0);
assert!((q - top).cross(ahead).abs() < 1e-6, "{q:?} is off the x-height line through {top:?}");
}
/// Snap to Glyph › Proximity Guides: the edges of each character's advance width are guides.