feat(text): parse TEXT via acadrust + extend SDF text to MTEXT

Two related steps for the text pipeline:

- TEXT parsing now goes through acadrust's parse_plain_text (like MTEXT
  uses parse_mtext). text::acad_text_encode parses the value's %% codes —
  including %%u/%%o underline/overline and %%nnn — and re-encodes to the
  stroke tessellator's \L/\O + resolved-Unicode grammar, so OCS no longer
  tokenizes TEXT %% codes itself. (ATTRIB/TOLERANCE/DIMENSION still use the
  local tokenizer for now.) Bumps the acadrust pin to the commit that
  completes the TEXT/MTEXT control-code set.

- SDF text (OCS_TEXT_SDF) now covers MTEXT and every text-producing
  entity, not just top-level TEXT. TextStroke carries a GlyphRun (layout
  params); the collector walks convert()'s TruckObject::Text runs and
  applies annotation scale exactly as tessellate does for strokes.
  layout_glyph_quads shares lff::tokenize_run so %% specials resolve and
  \L/\O decorations are skipped (decoration LINES in SDF are still TODO).

Verified: stroke rendering unchanged with the flag off; SDF glyphs render
correctly for TEXT and MTEXT with OCS_TEXT_SDF=1. 154 lib tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-07 00:53:19 +03:00
1 parent b88e5e4ad5
commit d528a3e2a5
9 files changed
+149 -28

No files matched your search

+50 -12
View File
@@ -792,6 +792,8 @@ impl Scene {
&self,
enabled: bool,
) -> Vec<crate::scene::pipeline::text_gpu::TextVertex> {
use crate::scene::convert::acad_to_truck::{convert, TruckObject};
use crate::scene::convert::tessellate::entity_z;
use crate::scene::pipeline::text_gpu;
use crate::scene::text::{glyph_quads, sdf_atlas};
if !enabled {
@@ -800,26 +802,62 @@ impl Scene {
let Ok(mut atlas) = sdf_atlas::text_atlas().lock() else {
return Vec::new();
};
// Annotation scale matches the stroke path (tessellate): model-space
// text scales, paper/layout text does not.
let anno = if self.current_layout == "Model" {
self.annotation_scale as f64
} else {
1.0
};
let mut out = Vec::new();
for e in self.document.entities() {
if let acadrust::EntityType::Text(t) = e {
let p = crate::entities::text::text_run_placement(t, &self.document);
let color = self.render_style(e).0;
// Only text-producing entities — converting solids/hatches here
// would re-tessellate the whole document every frame.
if !matches!(
e,
acadrust::EntityType::Text(_) | acadrust::EntityType::MText(_)
) {
continue;
}
let Some(te) = convert(e, &self.document) else {
continue;
};
let TruckObject::Text(groups) = te.object else {
continue;
};
let base_color = self.render_style(e).0;
let elev = entity_z(e) as f64;
// Annotation scale anchors at the first run's origin (matches the
// stroke path so multi-line MText spreads identically).
let ref_origin = groups.first().map(|g| g.origin).unwrap_or([0.0, 0.0]);
for g in &groups {
let Some(run) = &g.run else {
continue;
};
let slx = (g.origin[0] - ref_origin[0]) * anno + ref_origin[0];
let sly = (g.origin[1] - ref_origin[1]) * anno + ref_origin[1];
let color = g
.color
.map(|c| [c[0], c[1], c[2], 1.0])
.unwrap_or(base_color);
let quads = glyph_quads::layout_glyph_quads(
&mut atlas,
p.height,
p.rotation,
p.width_factor,
p.oblique_angle,
1.0,
&p.font,
&p.value,
run.height,
run.rotation,
run.width_factor,
run.oblique,
run.tracking,
&run.font,
&run.text,
);
// `anno` scales the run-local quads; `slx/sly` is the
// annotation-scaled run origin — exactly as tessellate places
// the stroke geometry.
text_gpu::push_glyph_vertices(
&mut out,
&quads,
[p.origin[0], p.origin[1], p.elevation],
1.0,
[slx, sly, elev],
anno,
color,
0.0,
);