diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 838940ddb..092a2516e 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -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, }); } diff --git a/crates/text/src/layout/ruby.rs b/crates/text/src/layout/ruby.rs index 4f2c76fe2..54681da89 100644 --- a/crates/text/src/layout/ruby.rs +++ b/crates/text/src/layout/ruby.rs @@ -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, } } diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 4299e61d2..b401905bb 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -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, + /// 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 diff --git a/crates/text/src/tests/bidi.rs b/crates/text/src/tests/bidi.rs index af67365b0..1ec51d15a 100644 --- a/crates/text/src/tests/bidi.rs +++ b/crates/text/src/tests/bidi.rs @@ -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; } diff --git a/crates/tools/src/guides/snap.rs b/crates/tools/src/guides/snap.rs index c3401b010..894e7ca5a 100644 --- a/crates/tools/src/guides/snap.rs +++ b/crates/tools/src/guides/snap.rs @@ -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], } diff --git a/crates/tools/src/guides/targets.rs b/crates/tools/src/guides/targets.rs index ad5662a27..994e28b34 100644 --- a/crates/tools/src/guides/targets.rs +++ b/crates/tools/src/guides/targets.rs @@ -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 diff --git a/crates/tools/src/guides/tests.rs b/crates/tools/src/guides/tests.rs index bfd56de69..200ab5008 100644 --- a/crates/tools/src/guides/tests.rs +++ b/crates/tools/src/guides/tests.rs @@ -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.