feat(text): draw SDF text decorations (underline/overline/strike)
The SDF glyph path skipped decoration toggles, so underline / overline / strikethrough lines were missing from OCS_TEXT_SDF text. Reserve a small solid (all-255) cell in the glyph atlas and expose solid_uv(); layout_glyph_quads now tracks the decoration runs (mirroring the stroke tessellator's UNDER/OVER/STRIKE positions) and emits a thin quad per run whose corners all sample the solid cell — the SDF shader fills it solid in the run colour, so decorations render through the same pass as glyphs, no extra pipeline. Verified in-app; SDF text now covers glyphs, %% specials, and decorations for both TEXT and MTEXT. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
d528a3e2a5
commit
a561b088d2
2 changed files with 116 additions and 11 deletions
|
|
@ -66,25 +66,68 @@ pub fn layout_glyph_quads(
|
||||||
[sx * cos_r - sy * sin_r, sx * sin_r + sy * cos_r]
|
[sx * cos_r - sy * sin_r, sx * sin_r + sy * cos_r]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::scene::text::lff::{tokenize_run, Deco, Op, Tok};
|
||||||
|
|
||||||
|
// Decoration line positions (glyph units) and half-thickness — mirror the
|
||||||
|
// stroke tessellator's constants so SDF decorations align with strokes.
|
||||||
|
const UNDER_Y: f32 = -1.5;
|
||||||
|
const OVER_Y: f32 = 10.5;
|
||||||
|
const STRIKE_Y: f32 = 4.5;
|
||||||
|
const DECO_HALF: f32 = 0.35;
|
||||||
|
|
||||||
|
let solid = atlas.solid_uv();
|
||||||
let face = Face::resolve(font_name);
|
let face = Face::resolve(font_name);
|
||||||
let mut cursor_x = 0.0f32;
|
let mut cursor_x = 0.0f32;
|
||||||
let mut quads = Vec::new();
|
let mut quads = Vec::new();
|
||||||
|
// Open decoration runs (pen x where each turned on) and the spans to draw.
|
||||||
|
let mut under: Option<f32> = None;
|
||||||
|
let mut over: Option<f32> = None;
|
||||||
|
let mut strike: Option<f32> = None;
|
||||||
|
let mut decos: Vec<(f32, f32, f32)> = Vec::new(); // (start_x, end_x, y)
|
||||||
|
|
||||||
// Same token walk as `tessellate_text_run` (LFF branch): DXF `%%` specials
|
// Same token walk as `tessellate_text_run` (LFF branch): DXF `%%` specials
|
||||||
// resolve to glyphs, spaces/missing advance the pen, decoration toggles emit
|
// resolve to glyphs, spaces/missing advance the pen, decoration toggles
|
||||||
// no glyph (they render as lines on the stroke path).
|
// open/close underline / overline / strikethrough runs.
|
||||||
for tok in crate::scene::text::lff::tokenize_run(text) {
|
for tok in tokenize_run(text) {
|
||||||
let ch = match tok {
|
let ch = match tok {
|
||||||
crate::scene::text::lff::Tok::Glyph(c) => c,
|
Tok::Glyph(c) => c,
|
||||||
crate::scene::text::lff::Tok::Space => {
|
Tok::Space => {
|
||||||
cursor_x += face.word_spacing();
|
cursor_x += face.word_spacing();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
crate::scene::text::lff::Tok::Missing => {
|
Tok::Missing => {
|
||||||
cursor_x += 6.0 + face.letter_spacing() * tracking;
|
cursor_x += 6.0 + face.letter_spacing() * tracking;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
crate::scene::text::lff::Tok::Deco(..) => continue,
|
Tok::Deco(deco, op) => {
|
||||||
|
let (slot, y) = match deco {
|
||||||
|
Deco::Under => (&mut under, UNDER_Y),
|
||||||
|
Deco::Over => (&mut over, OVER_Y),
|
||||||
|
Deco::Strike => (&mut strike, STRIKE_Y),
|
||||||
|
};
|
||||||
|
match op {
|
||||||
|
Op::On => {
|
||||||
|
if slot.is_none() {
|
||||||
|
*slot = Some(cursor_x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Op::Off => {
|
||||||
|
if let Some(s) = slot.take() {
|
||||||
|
decos.push((s, cursor_x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Op::Toggle => {
|
||||||
|
*slot = match slot.take() {
|
||||||
|
Some(s) => {
|
||||||
|
decos.push((s, cursor_x, y));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
None => Some(cursor_x),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match atlas.get_or_insert(font_name, ch) {
|
match atlas.get_or_insert(font_name, ch) {
|
||||||
Some(e) => {
|
Some(e) => {
|
||||||
|
|
@ -109,6 +152,27 @@ pub fn layout_glyph_quads(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any decoration still open at the end runs to the final pen position.
|
||||||
|
for (slot, y) in [(under, UNDER_Y), (over, OVER_Y), (strike, STRIKE_Y)] {
|
||||||
|
if let Some(s) = slot {
|
||||||
|
decos.push((s, cursor_x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Decoration lines: thin quads sampling the atlas's solid texel so the SDF
|
||||||
|
// shader fills them solid in the run colour.
|
||||||
|
for (s, e, y) in decos {
|
||||||
|
quads.push(GlyphQuad {
|
||||||
|
corners: [
|
||||||
|
xform(s, y - DECO_HALF, 0.0),
|
||||||
|
xform(e, y - DECO_HALF, 0.0),
|
||||||
|
xform(e, y + DECO_HALF, 0.0),
|
||||||
|
xform(s, y + DECO_HALF, 0.0),
|
||||||
|
],
|
||||||
|
uv_min: solid,
|
||||||
|
uv_max: solid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
quads
|
quads
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,4 +270,17 @@ mod tests {
|
||||||
"double height ~ double quad extent, got {r}"
|
"double height ~ double quad extent, got {r}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn underline_emits_an_extra_solid_quad() {
|
||||||
|
let mut atlas = GlyphAtlas::new(512, 512);
|
||||||
|
let plain = layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", "AB");
|
||||||
|
// `\L…\l` underlines the run — same glyph quads plus one decoration quad.
|
||||||
|
let under = layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", "\\LAB\\l");
|
||||||
|
assert_eq!(under.len(), plain.len() + 1, "underline adds one quad");
|
||||||
|
// The decoration quad samples the atlas's solid texel (degenerate UV).
|
||||||
|
let deco = under.last().unwrap();
|
||||||
|
assert_eq!(deco.uv_min, deco.uv_max, "decoration quad uses the solid UV");
|
||||||
|
assert_eq!(deco.uv_min, atlas.solid_uv());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,11 @@ pub struct GlyphAtlas {
|
||||||
cursor_x: u32,
|
cursor_x: u32,
|
||||||
cursor_y: u32,
|
cursor_y: u32,
|
||||||
shelf_h: u32,
|
shelf_h: u32,
|
||||||
|
/// UV of a fully-inside (value 255) texel, reserved at the atlas corner.
|
||||||
|
/// A quad whose corners all map here samples a constant "inside" field, so
|
||||||
|
/// the SDF shader fills it solid — used to draw decoration lines
|
||||||
|
/// (underline / overline / strikethrough) through the glyph pipeline.
|
||||||
|
solid_uv: [f32; 2],
|
||||||
/// Set when `data` changed since the last upload; the GPU side clears it.
|
/// Set when `data` changed since the last upload; the GPU side clears it.
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -106,20 +111,43 @@ struct BakedTile {
|
||||||
advance: f32,
|
advance: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Side of the reserved solid (all-255) block at the atlas corner.
|
||||||
|
const SOLID_PX: u32 = 4;
|
||||||
|
|
||||||
impl GlyphAtlas {
|
impl GlyphAtlas {
|
||||||
pub fn new(width: u32, height: u32) -> Self {
|
pub fn new(width: u32, height: u32) -> Self {
|
||||||
|
let mut data = vec![0u8; (width * height) as usize];
|
||||||
|
// Reserve a small solid block at (0,0) for decoration lines; the shelf
|
||||||
|
// packer starts past it so glyphs never overwrite it.
|
||||||
|
let solid = SOLID_PX.min(width).min(height);
|
||||||
|
for y in 0..solid {
|
||||||
|
for x in 0..solid {
|
||||||
|
data[(y * width + x) as usize] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
data: vec![0u8; (width * height) as usize],
|
data,
|
||||||
entries: HashMap::new(),
|
entries: HashMap::new(),
|
||||||
cursor_x: 0,
|
cursor_x: solid,
|
||||||
cursor_y: 0,
|
cursor_y: 0,
|
||||||
shelf_h: 0,
|
shelf_h: solid,
|
||||||
dirty: false,
|
// Centre of the solid block, safely inside it under bilinear filtering.
|
||||||
|
solid_uv: [
|
||||||
|
(solid as f32 * 0.5) / width as f32,
|
||||||
|
(solid as f32 * 0.5) / height as f32,
|
||||||
|
],
|
||||||
|
dirty: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// UV of a fully-inside texel; a quad with all corners at this UV renders
|
||||||
|
/// solid. Used for decoration lines.
|
||||||
|
pub fn solid_uv(&self) -> [f32; 2] {
|
||||||
|
self.solid_uv
|
||||||
|
}
|
||||||
|
|
||||||
pub fn width(&self) -> u32 {
|
pub fn width(&self) -> u32 {
|
||||||
self.width
|
self.width
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue