forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
d528a3e2a5
commit
a561b088d2
2 files changed
+116
-11
No files matched your search
@@ -66,25 +66,68 @@ pub fn layout_glyph_quads(
|
||||
[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 mut cursor_x = 0.0f32;
|
||||
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
|
||||
// resolve to glyphs, spaces/missing advance the pen, decoration toggles emit
|
||||
// no glyph (they render as lines on the stroke path).
|
||||
for tok in crate::scene::text::lff::tokenize_run(text) {
|
||||
// resolve to glyphs, spaces/missing advance the pen, decoration toggles
|
||||
// open/close underline / overline / strikethrough runs.
|
||||
for tok in tokenize_run(text) {
|
||||
let ch = match tok {
|
||||
crate::scene::text::lff::Tok::Glyph(c) => c,
|
||||
crate::scene::text::lff::Tok::Space => {
|
||||
Tok::Glyph(c) => c,
|
||||
Tok::Space => {
|
||||
cursor_x += face.word_spacing();
|
||||
continue;
|
||||
}
|
||||
crate::scene::text::lff::Tok::Missing => {
|
||||
Tok::Missing => {
|
||||
cursor_x += 6.0 + face.letter_spacing() * tracking;
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -206,4 +270,17 @@ mod tests {
|
||||
"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_y: 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.
|
||||
dirty: bool,
|
||||
}
|
||||
@@ -106,20 +111,43 @@ struct BakedTile {
|
||||
advance: f32,
|
||||
}
|
||||
|
||||
/// Side of the reserved solid (all-255) block at the atlas corner.
|
||||
const SOLID_PX: u32 = 4;
|
||||
|
||||
impl GlyphAtlas {
|
||||
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 {
|
||||
width,
|
||||
height,
|
||||
data: vec![0u8; (width * height) as usize],
|
||||
data,
|
||||
entries: HashMap::new(),
|
||||
cursor_x: 0,
|
||||
cursor_x: solid,
|
||||
cursor_y: 0,
|
||||
shelf_h: 0,
|
||||
dirty: false,
|
||||
shelf_h: solid,
|
||||
// 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 {
|
||||
self.width
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user