refactor(text): SDF for all text kinds; remove the legacy stroke text path

SDF is the only text renderer now — the stroke path and its LOD ladder are
gone, and the remaining text kinds render as SDF glyph quads too.

- ATTDEF / ATTRIB: carry a GlyphRun so single-line attribute text renders as
  SDF (multi-line already did via layout_mtext). Tolerance / complex-linetype
  text already SDF.
- Remove `sdf_text_enabled()` (the OCS_TEXT_SDF flag / kill switch) and take
  the SDF path unconditionally everywhere: tessellate Text arm (per-group by
  GlyphRun presence), hit_test fallbacks, render gather, complex_lt, table,
  mleader.
- Delete the text LOD ladder: tess.rs baseline/greek block and the
  block_cache greek/baseline path (emit_greeked_text / emit_text_baseline),
  plus the now-dead helpers text_baseline_points / text_greek_obb_tris /
  text_obb_corners_native / mtext_line_count and the LocalWire
  text_height_local / text_obb_local fields. All text is crisp at every zoom;
  text entities are exempt from the sub-pixel stub cull.
- Fix a latent bug: block-internal SDF text was culled because its LocalWire
  aabb_local was [0,0,0,0] (no points) — fold the glyph-quad extent into
  aabb_local and exempt text_verts-carrying wires from the sub-pixel cull.
- Clear the pre-existing dead-code warnings (TextPlacement.elevation, TabKind,
  MTextRunKind::Tab).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-07 21:07:00 +03:00
1 parent 67ce45d40b
commit 5c15091e74
12 files changed
+145 -1238

No files matched your search

+49 -54
View File
@@ -145,37 +145,34 @@ pub fn click_hit<'a>(
// text boxes qualify (never some other empty wire). Lowest priority — real
// edges and fills above always win. Prefer the tightest box so a click over
// overlapping text picks the smallest.
if crate::scene::text::sdf_atlas::sdf_text_enabled() {
let mut best_area = f32::MAX;
let mut best_box: Option<&str> = None;
for wire in wires {
if wire.text_verts.is_empty()
|| !wire.points.is_empty()
|| !wire.fill_tris.is_empty()
|| wire.aabb == WireModel::UNBOUNDED_AABB
{
continue;
}
let [minx, miny, maxx, maxy] = wire.aabb;
let (mut sx0, mut sy0, mut sx1, mut sy1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] {
let s = world_to_screen(glam::DVec3::new(cx as f64, cy as f64, 0.0), view_rot, eye, bounds);
sx0 = sx0.min(s.x);
sx1 = sx1.max(s.x);
sy0 = sy0.min(s.y);
sy1 = sy1.max(s.y);
}
if cursor.x >= sx0 && cursor.x <= sx1 && cursor.y >= sy0 && cursor.y <= sy1 {
let area = (sx1 - sx0) * (sy1 - sy0);
if area < best_area {
best_area = area;
best_box = Some(wire.name.as_str());
}
let mut best_area = f32::MAX;
let mut best_box: Option<&str> = None;
for wire in wires {
if wire.text_verts.is_empty()
|| !wire.points.is_empty()
|| !wire.fill_tris.is_empty()
|| wire.aabb == WireModel::UNBOUNDED_AABB
{
continue;
}
let [minx, miny, maxx, maxy] = wire.aabb;
let (mut sx0, mut sy0, mut sx1, mut sy1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] {
let s = world_to_screen(glam::DVec3::new(cx as f64, cy as f64, 0.0), view_rot, eye, bounds);
sx0 = sx0.min(s.x);
sx1 = sx1.max(s.x);
sy0 = sy0.min(s.y);
sy1 = sy1.max(s.y);
}
if cursor.x >= sx0 && cursor.x <= sx1 && cursor.y >= sy0 && cursor.y <= sy1 {
let area = (sx1 - sx0) * (sy1 - sy0);
if area < best_area {
best_area = area;
best_box = Some(wire.name.as_str());
}
}
return best_box;
}
None
best_box
}
/// Like `click_hit` but returns every wire within the click threshold,
@@ -219,32 +216,30 @@ pub fn click_hits_all<'a>(
// wire carrying glyph quads) so selection cycling steps through text too —
// the same discriminator/fallback `click_hit` uses. Ranked after real
// geometry (distance = the click threshold, above every proximity hit).
if crate::scene::text::sdf_atlas::sdf_text_enabled() {
for wire in wires {
if wire.text_verts.is_empty()
|| !wire.points.is_empty()
|| !wire.fill_tris.is_empty()
|| wire.aabb == WireModel::UNBOUNDED_AABB
{
continue;
}
let [minx, miny, maxx, maxy] = wire.aabb;
let (mut sx0, mut sy0, mut sx1, mut sy1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] {
let s = world_to_screen(
glam::DVec3::new(cx as f64, cy as f64, 0.0),
view_rot,
eye,
bounds,
);
sx0 = sx0.min(s.x);
sx1 = sx1.max(s.x);
sy0 = sy0.min(s.y);
sy1 = sy1.max(s.y);
}
if cursor.x >= sx0 && cursor.x <= sx1 && cursor.y >= sy0 && cursor.y <= sy1 {
hits.push((CLICK_THRESHOLD_PX, &wire.name));
}
for wire in wires {
if wire.text_verts.is_empty()
|| !wire.points.is_empty()
|| !wire.fill_tris.is_empty()
|| wire.aabb == WireModel::UNBOUNDED_AABB
{
continue;
}
let [minx, miny, maxx, maxy] = wire.aabb;
let (mut sx0, mut sy0, mut sx1, mut sy1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] {
let s = world_to_screen(
glam::DVec3::new(cx as f64, cy as f64, 0.0),
view_rot,
eye,
bounds,
);
sx0 = sx0.min(s.x);
sx1 = sx1.max(s.x);
sy0 = sy0.min(s.y);
sy1 = sy1.max(s.y);
}
if cursor.x >= sx0 && cursor.x <= sx1 && cursor.y >= sy0 && cursor.y <= sy1 {
hits.push((CLICK_THRESHOLD_PX, &wire.name));
}
}
hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));