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

+2 -186
View File
@@ -1084,7 +1084,7 @@ impl MultiLeaderTess for MultiLeader {
entity_color: [f32; 4],
line_weight_px: f32,
anno_scale: f32,
world_per_pixel: Option<f32>,
_world_per_pixel: Option<f32>,
bg_color: [f32; 4],
) -> Vec<crate::scene::model::wire_model::WireModel> {
use crate::scene::convert::tessellate::{
@@ -1508,28 +1508,7 @@ impl MultiLeaderTess for MultiLeader {
color_or_inherit(&ctx.text_color, entity_color)
};
// Same LOD ladder used for top-level Text / MText (see scene/mod.rs):
// h_px < 1 → baseline line (skip glyphs)
// 1 ≤ h < 5 → greeked rect in text color (skip glyphs)
// h_px ≥ 5 → full per-glyph stroke tessellation
let lod_h_px = world_per_pixel.map(|wpp| height / wpp);
let lod_mode = match lod_h_px {
Some(h) if h < 1.0 => 0,
Some(h) if h < 5.0 => 1,
_ => 2,
};
// Helper: map a (local_x, local_y) in the text's pre-rotation frame
// (origin at the insertion point) into WCS render space.
let to_wcs = |lx: f32, ly: f32| -> [f32; 3] {
[
local_ins_x + lx * cos_r - ly * sin_r,
local_ins_y + lx * sin_r + ly * cos_r,
z,
]
};
if crate::scene::text::sdf_atlas::sdf_text_enabled() {
{
// SDF text: emit glyph quads instead of strokes (crisp at every
// zoom, so no baseline/greek LOD). `layout_mtext` already placed
// each run at its final position/rotation with a `GlyphRun`, so
@@ -1597,169 +1576,6 @@ impl MultiLeaderTess for MultiLeader {
fill_tris_low: Vec::new(),
});
}
} else if lod_mode == 0 {
// Baseline of the top line only.
let line_w = line_widths.first().copied().unwrap_or(0.0);
let len_px = world_per_pixel
.map(|wpp| line_w / wpp)
.unwrap_or(f32::INFINITY);
if len_px >= 2.0 {
let line_y_local = v_offset;
let p0 = to_wcs(-line_w * h_anchor, line_y_local);
let p1 = to_wcs(line_w * (1.0 - h_anchor), line_y_local);
wires.push(WireModel {
text_verts: Vec::new(),
name: name.clone(),
points: vec![p0, p1],
points_low: Vec::new(),
color: text_color,
selected,
aci: 0,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px,
snap_pts: vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)],
tangent_geoms: vec![],
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
}
} else if lod_mode == 1 {
// One filled rect per line — keeps the visual "text lives here
// per row" hint that multi-line MText carries, in the text's
// own color. Empty `points` opts out of the face3d 0.45 dim so
// the fill renders at full intensity.
let mut greek_tris: Vec<[f32; 3]> = Vec::with_capacity(line_widths.len() * 6);
for (i, &line_w) in line_widths.iter().enumerate() {
let li = i as f32;
let line_y_bottom = -li * line_h + v_offset;
let line_y_top = line_y_bottom + height;
if line_w <= 0.0 {
continue;
}
let left = -line_w * h_anchor;
let right = line_w * (1.0 - h_anchor);
let bl = to_wcs(left, line_y_bottom);
let br = to_wcs(right, line_y_bottom);
let tr = to_wcs(right, line_y_top);
let tl = to_wcs(left, line_y_top);
greek_tris.extend_from_slice(&[bl, br, tr, bl, tr, tl]);
}
if !greek_tris.is_empty() {
wires.push(WireModel {
text_verts: Vec::new(),
name: name.clone(),
points: vec![],
points_low: Vec::new(),
color: text_color,
selected,
aci: 0,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)],
tangent_geoms: vec![],
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: greek_tris,
// fill_tris_low intentionally empty: this fill renders on
// the top-level path, where consumers treat a short low
// half as all-zero, so it draws at f32 precision (sub-
// metre error at UTM scale) — not a crash. Follow-up:
// double-single-split via points_to_ds to match emit_wire.
fill_tris_low: Vec::new(),
});
}
} else {
// Pre-tessellated by `layout_mtext`. Each TextStroke is in
// local glyph space with its world origin (already offset-
// relative because we passed local_ins_x/y) stored as f64.
let mut text_points: Vec<[f32; 3]> = Vec::new();
let mut text_fill_tris: Vec<[f32; 3]> = Vec::new();
for ts in &layout.strokes {
let ox = ts.origin[0] as f32;
let oy = ts.origin[1] as f32;
for stroke in &ts.strokes {
if stroke.len() < 2 {
continue;
}
text_points.push(nan);
for &[x, y] in stroke {
text_points.push([x + ox, y + oy, z]);
}
}
for &[x, y] in &ts.fill_tris {
text_fill_tris.push([x + ox, y + oy, z]);
}
}
let mut is_first = true;
if !text_points.is_empty() {
let snap = if is_first {
is_first = false;
vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)]
} else {
vec![]
};
wires.push(WireModel {
text_verts: Vec::new(),
name: name.clone(),
points: text_points,
points_low: Vec::new(),
color: text_color,
selected,
aci: 0,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px,
snap_pts: snap,
tangent_geoms: vec![],
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
});
}
if !text_fill_tris.is_empty() {
let snap = if is_first {
vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)]
} else {
vec![]
};
wires.push(WireModel {
text_verts: Vec::new(),
name: name.clone(),
points: vec![],
points_low: Vec::new(),
color: text_color,
selected,
aci: 0,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px,
snap_pts: snap,
tangent_geoms: vec![],
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
vp_scissor: None,
fill_tris: text_fill_tris,
// fill_tris_low intentionally empty: this fill renders on
// the top-level path, where consumers treat a short low
// half as all-zero, so it draws at f32 precision (sub-
// metre error at UTM scale) — not a crash. Follow-up:
// double-single-split via points_to_ds to match emit_wire.
fill_tris_low: Vec::new(),
});
}
}
// Text frame / background-fill rectangle in local frame, then rotated to WCS.