fix(text): no stroke-font tracking after big-font and fallback glyphs (#1470)

Every glyph advanced by the face's letter spacing, including CJK glyphs a
style's big font or the TrueType fallback supplies. Their advances already
carry the gap (big font) or side bearings (TrueType), so with an LFF primary
such as iso each ideograph took 1.33 heights instead of one.
Face::spacing_after(ch) gives the tracking only to glyphs the stroke font
draws itself; every layout path uses it.

The 1.8-height widths in the report were the TEXT bounds estimate counting
UTF-8 bytes; opencadcodec 1109d52 counts characters.

Pins opencadcodec 1109d52, opencadkernel 8a246b0, opencadgraph 3c4af1c.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-09-25 21:27:17 +03:00
1 parent fdc01e13c8
commit 89512c2f20
9 files changed
+41 -20

No files matched your search

Generated
+4 -4
View File
@@ -4201,7 +4201,7 @@ dependencies = [
[[package]]
name = "opencadcodec"
version = "0.5.5"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=7c20aa7#7c20aa7fad405845c5bd5cdcc2a232cdc2a4687d"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=1109d52#1109d52f65ae8f00d75f43445908bff48807c0c5"
dependencies = [
"ahash 0.8.12",
"anyhow",
@@ -4224,7 +4224,7 @@ dependencies = [
[[package]]
name = "opencadgraph"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=1543a5a#1543a5aeb0b871a2f23a3b55117c7f5af1e1d229"
source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=3c4af1c#3c4af1c3bdebc968630ad3c28046173bed0aa200"
dependencies = [
"opencadkernel",
"serde_json",
@@ -4233,7 +4233,7 @@ dependencies = [
[[package]]
name = "opencadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=d71a828#d71a8284f0388025e07f219c7b8f6bb86e7188ec"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=8a246b0#8a246b0026fb9c626a39a6ba8ccf681f1bf0461e"
dependencies = [
"cavalier_contours",
"opencadcodec",
@@ -4244,7 +4244,7 @@ dependencies = [
[[package]]
name = "opencadkernel-constraints"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=d71a828#d71a8284f0388025e07f219c7b8f6bb86e7188ec"
source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=8a246b0#8a246b0026fb9c626a39a6ba8ccf681f1bf0461e"
dependencies = [
"nalgebra",
]
+4 -4
View File
@@ -33,10 +33,10 @@ rfd = "0.17"
clap = { version = "4", features = ["derive", "string"] }
env_logger = "0.11"
log = "0.4"
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "7c20aa7", features = ["serde"] }
kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "d71a828", features = ["acis", "offset"] }
kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "d71a828" }
graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "1543a5a" }
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "1109d52", features = ["serde"] }
kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "8a246b0", features = ["acis", "offset"] }
kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "8a246b0" }
graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "3c4af1c" }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
+2 -2
View File
@@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true }
# Pulled in only by the `host` feature, which adds the `opencadcodec`-typed
# `HostApi` runtime surface. The default crate stays dependency-free so engine
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "7c20aa7", optional = true, features = ["serde"] }
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "1109d52", optional = true, features = ["serde"] }
# Runtime IPC and serialization (host feature only).
interprocess = { version = "2", optional = true }
@@ -39,7 +39,7 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] }
cargo-lock = "11"
# opencadcodec is scanned at build time to generate the embedded type registry.
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "7c20aa7", features = ["serde"] }
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "1109d52", features = ["serde"] }
[dev-dependencies]
serde_json = "1"
+1 -1
View File
@@ -8,7 +8,7 @@ publish = false
crate-type = ["cdylib"]
[dependencies]
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "7c20aa7", features = ["serde"] }
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "1109d52", features = ["serde"] }
bincode = "1.3"
serde = { version = "1", features = ["derive"] }
console_error_panic_hook = "0.1"
+1 -1
View File
@@ -1265,7 +1265,7 @@ dependencies = [
[[package]]
name = "opencadcodec"
version = "0.5.5"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=7c20aa7#7c20aa7fad405845c5bd5cdcc2a232cdc2a4687d"
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=1109d52#1109d52f65ae8f00d75f43445908bff48807c0c5"
dependencies = [
"ahash 0.8.12",
"anyhow",
+3 -3
View File
@@ -214,7 +214,7 @@ pub fn text_local_bounds(
max_y = max_y.max(sy);
}
}
cursor_x += glyph.advance + face.letter_spacing();
cursor_x += glyph.advance + face.spacing_after(ch);
}
None => {
cursor_x += 6.0 + face.letter_spacing();
@@ -964,7 +964,7 @@ pub fn measure_word(
let mut w = 0.0_f32;
for ch in text.chars() {
w += match face.glyph(ch) {
Some(g) => (g.advance + face.letter_spacing() * state.tracking) * scale,
Some(g) => (g.advance + face.spacing_after(ch) * state.tracking) * scale,
None => (6.0 + face.letter_spacing() * state.tracking) * scale,
};
}
@@ -2315,7 +2315,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
for (ci, ch) in text.chars().enumerate() {
let adv = match face.glyph(ch) {
Some(g) => {
(g.advance + face.letter_spacing() * tracking) * scale
(g.advance + face.spacing_after(ch) * tracking) * scale
}
None => (6.0 + face.letter_spacing() * tracking) * scale,
};
+19
View File
@@ -158,6 +158,25 @@ impl Face {
}
}
/// [`letter_spacing`](Self::letter_spacing) after the glyph drawn for
/// `ch`: a stroke font's tracking belongs to its own glyphs only. A
/// big-font glyph's advance already carries its gap and a TrueType
/// stand-in's its side bearings, so tracking them too spread CJK text
/// by a third of the height per character (#1470).
pub fn spacing_after(&self, ch: char) -> f32 {
match self {
Face::Lff(f) if f.glyph(ch).is_none() => 0.0,
Face::WithBig { primary, big } => {
if ttf_glyph::is_full_width(ch) && shx::bigfont_glyph(big, ch).is_some() {
0.0
} else {
primary.spacing_after(ch)
}
}
_ => self.letter_spacing(),
}
}
/// Width of a space (9-unit).
pub fn word_spacing(&self) -> f32 {
match self {
+5 -3
View File
@@ -143,12 +143,14 @@ pub fn layout_glyph_quads(
uv_min: e.uv_min,
uv_max: e.uv_max,
});
cursor_x += e.advance + face.letter_spacing() * tracking;
cursor_x += e.advance + face.spacing_after(ch) * tracking;
}
None => {
// No ink (whitespace glyph) or atlas full: advance only.
let adv = face.glyph(ch).map(|g| g.advance).unwrap_or(6.0);
cursor_x += adv + face.letter_spacing() * tracking;
cursor_x += match face.glyph(ch) {
Some(g) => g.advance + face.spacing_after(ch) * tracking,
None => 6.0 + face.letter_spacing() * tracking,
};
}
}
}
+2 -2
View File
@@ -441,7 +441,7 @@ pub fn tessellate_text_run(
Some(glyph) => {
emit_glyph(out, &glyph.strokes, *cursor_x);
emit_fill(fill_tris, &glyph.fill_tris, *cursor_x);
*cursor_x += glyph.advance + face.letter_spacing() * tracking;
*cursor_x += glyph.advance + face.spacing_after(ch) * tracking;
}
None => {
warn_missing_glyph(font_name, ch);
@@ -470,7 +470,7 @@ pub fn tessellate_text_run(
Some(glyph) => {
emit_glyph(&mut out, &glyph.strokes, cursor_x);
emit_fill(&mut fill_tris, &glyph.fill_tris, cursor_x);
cursor_x += glyph.advance + face.letter_spacing() * tracking;
cursor_x += glyph.advance + face.spacing_after(*c) * tracking;
}
None => {
warn_missing_glyph(font_name, *c);