refactor(text): per-entity SDF text — all text kinds, selection/hover highlight

Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).

- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
  cached by the tess memo, cloned on hit, and transformed by the block
  expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
  the strokes; render gathers text_verts from the viewport wire set
  (`gather_text_verts`, cached on wire content id) — the old collector is
  removed. Covers model, dimension, table, block-internal and paper-sheet
  text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
  SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
  entity_aabb which mis-places the box for MTEXT; composite paths no longer
  clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
  highlight, keyed on selection_generation) mirroring the selected-wire
  xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-07-07 17:01:36 +03:00
commit 110683f419
47 changed files with 528 additions and 142 deletions

View file

@ -1345,6 +1345,7 @@ fn tessellate_dimension_inner(
let (ext1, ext2) = split_ext_lines(&geom.ext_lines);
if !ext1.is_empty() {
wires.push(WireModel {
text_verts: Vec::new(),
name: name.clone(),
points: ext1,
points_low: Vec::new(),
@ -1366,6 +1367,7 @@ fn tessellate_dimension_inner(
}
if !ext2.is_empty() {
wires.push(WireModel {
text_verts: Vec::new(),
name: name.clone(),
points: ext2,
points_low: Vec::new(),
@ -1387,6 +1389,7 @@ fn tessellate_dimension_inner(
}
} else {
wires.push(WireModel {
text_verts: Vec::new(),
name: name.clone(),
points: geom.ext_lines,
points_low: Vec::new(),
@ -1409,6 +1412,7 @@ fn tessellate_dimension_inner(
}
wires.push(WireModel {
text_verts: Vec::new(),
name: name.clone(),
points: geom.dim_lines,
points_low: Vec::new(),
@ -1447,6 +1451,7 @@ fn tessellate_dimension_inner(
aci_to_rgba(&c)
};
wires.push(WireModel {
text_verts: Vec::new(),
name: name.clone(),
points: vec![],
points_low: Vec::new(),