From 89512c2f20858cf7ccb7757ca2cba017060538b8 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 25 Sep 2026 21:27:17 +0300 Subject: [PATCH] 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 --- Cargo.lock | 8 ++++---- Cargo.toml | 8 ++++---- crates/ocs_plugin_api/Cargo.toml | 4 ++-- crates/ocs_web_worker/Cargo.toml | 2 +- plugins/opencad-python/Cargo.lock | 2 +- src/entities/text_support.rs | 6 +++--- src/scene/text/font_face.rs | 19 +++++++++++++++++++ src/scene/text/glyph_quads.rs | 8 +++++--- src/scene/text/lff.rs | 4 ++-- 9 files changed, 41 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 26659a805..a9881fcf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 7ad93dc7e..d6cc5899f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index 91efdc937..e160ec5d1 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -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" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index 229386607..8dc3430c2 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -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" diff --git a/plugins/opencad-python/Cargo.lock b/plugins/opencad-python/Cargo.lock index 039e59eaf..fe0b3cc43 100644 --- a/plugins/opencad-python/Cargo.lock +++ b/plugins/opencad-python/Cargo.lock @@ -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", diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index 1682852d3..a80735e18 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -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, }; diff --git a/src/scene/text/font_face.rs b/src/scene/text/font_face.rs index 42d29cbdb..10041a874 100644 --- a/src/scene/text/font_face.rs +++ b/src/scene/text/font_face.rs @@ -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 { diff --git a/src/scene/text/glyph_quads.rs b/src/scene/text/glyph_quads.rs index 2256a5da1..ffe4d5109 100644 --- a/src/scene/text/glyph_quads.rs +++ b/src/scene/text/glyph_quads.rs @@ -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, + }; } } } diff --git a/src/scene/text/lff.rs b/src/scene/text/lff.rs index 8f93a3a73..0fe1f877b 100644 --- a/src/scene/text/lff.rs +++ b/src/scene/text/lff.rs @@ -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);