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:
parent
fd0b8fc1d4
commit
110683f419
47 changed files with 528 additions and 142 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -562,6 +562,7 @@ impl LeaderTess for Leader {
|
|||
|
||||
if verts.len() < 2 {
|
||||
return WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name,
|
||||
points: vec![],
|
||||
points_low: Vec::new(),
|
||||
|
|
@ -662,6 +663,7 @@ impl LeaderTess for Leader {
|
|||
}
|
||||
|
||||
WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name,
|
||||
points,
|
||||
points_low: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -1284,6 +1284,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
// WireModels so the renderer respects per-piece coloring.
|
||||
let mut wires: Vec<WireModel> = Vec::new();
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points,
|
||||
points_low: Vec::new(),
|
||||
|
|
@ -1525,7 +1526,75 @@ impl MultiLeaderTess for MultiLeader {
|
|||
]
|
||||
};
|
||||
|
||||
if lod_mode == 0 {
|
||||
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
|
||||
// this mirrors the top-level Text arm (anno = 1, origin as-is).
|
||||
// Base colour stays neutral — selection / hover recolouring is
|
||||
// done by the text-highlight overlay.
|
||||
let neutral = color_or_inherit(&ctx.text_color, entity_color);
|
||||
let mut sdf_verts: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
||||
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
||||
for ts in &layout.strokes {
|
||||
let Some(run) = &ts.run else { continue };
|
||||
let quads = crate::scene::text::glyph_quads::layout_glyph_quads(
|
||||
&mut atlas,
|
||||
run.height,
|
||||
run.rotation,
|
||||
run.width_factor,
|
||||
run.oblique,
|
||||
run.tracking,
|
||||
&run.font,
|
||||
&run.text,
|
||||
);
|
||||
crate::scene::pipeline::text_gpu::push_glyph_vertices(
|
||||
&mut sdf_verts,
|
||||
&quads,
|
||||
[ts.origin[0], ts.origin[1], z as f64],
|
||||
1.0,
|
||||
neutral,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
if !sdf_verts.is_empty() {
|
||||
// Pick box from the glyph quads (f64 accumulate → f32).
|
||||
let (mut nx, mut ny, mut xx, mut xy) =
|
||||
(f64::MAX, f64::MAX, f64::MIN, f64::MIN);
|
||||
for v in &sdf_verts {
|
||||
let x = v.pos[0] as f64 + v.pos_low[0] as f64;
|
||||
let y = v.pos[1] as f64 + v.pos_low[1] as f64;
|
||||
nx = nx.min(x);
|
||||
xx = xx.max(x);
|
||||
ny = ny.min(y);
|
||||
xy = xy.max(y);
|
||||
}
|
||||
wires.push(WireModel {
|
||||
text_verts: sdf_verts,
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
points_low: Vec::new(),
|
||||
color: neutral,
|
||||
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: [nx as f32, ny as f32, xx as f32, xy as f32],
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
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
|
||||
|
|
@ -1536,6 +1605,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
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(),
|
||||
|
|
@ -1578,6 +1648,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
}
|
||||
if !greek_tris.is_empty() {
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
points_low: Vec::new(),
|
||||
|
|
@ -1634,6 +1705,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
vec![]
|
||||
};
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: text_points,
|
||||
points_low: Vec::new(),
|
||||
|
|
@ -1660,6 +1732,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
vec![]
|
||||
};
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
points_low: Vec::new(),
|
||||
|
|
@ -1724,6 +1797,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
wcs_corners[3],
|
||||
];
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
points_low: Vec::new(),
|
||||
|
|
@ -1754,6 +1828,7 @@ impl MultiLeaderTess for MultiLeader {
|
|||
wcs_corners[0],
|
||||
];
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name,
|
||||
points: frame_points,
|
||||
points_low: Vec::new(),
|
||||
|
|
|
|||
|
|
@ -739,6 +739,7 @@ pub fn tessellate_table(
|
|||
let mk =
|
||||
|color: [f32; 4], points: Vec<[f32; 3]>, fill_tris: Vec<[f32; 3]>, lw: f32| -> WireModel {
|
||||
WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points,
|
||||
points_low: Vec::new(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue