diff --git a/Cargo.lock b/Cargo.lock index ad33fbff4..dcdeccfd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.0" -source = "git+https://github.com/HakanSeven12/acadrust?branch=main#eae0ec3ca1777c1a89d78374fe56a1edc130e5ed" +source = "git+https://github.com/HakanSeven12/acadrust?branch=main#2a2d2785a4e2eee8c723ffe06abe00a39810e42b" dependencies = [ "ahash 0.8.12", "anyhow", @@ -3160,7 +3160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d" dependencies = [ "bytecount", - "memchr 2.8.2", + "memchr 1.0.2", "nom 8.0.0", ] diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs index cb129822e..0485905db 100644 --- a/src/entities/attribute.rs +++ b/src/entities/attribute.rs @@ -292,6 +292,7 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) origin, color: None, fill_tris, + run: None, }); } let _ = input.line_count; // round-trip only — recomputed above diff --git a/src/entities/text.rs b/src/entities/text.rs index c32508147..4fc2ba8ad 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -6,7 +6,7 @@ use crate::entities::text_support::{ resolve_dxf_special_chars, resolve_text_style, text_local_bounds, }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TextStroke, TruckEntity, TruckObject}; +use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke, TruckEntity, TruckObject}; use crate::scene::text::lff; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; @@ -66,9 +66,41 @@ pub struct TextPlacement { pub wcs_insertion: [f64; 3], } +/// Parse a TEXT value's `%%` control codes through acadrust's `parse_plain_text` +/// (the same parser MTEXT uses), then re-encode into the stroke tessellator's +/// inline grammar: specials arrive resolved to Unicode, and `%%u`/`%%o` +/// underline/overline become `\L…\l` / `\O…\o` decoration markers. This keeps +/// TEXT parsing in acadrust rather than OCS's own tokenizer. +fn acad_text_encode(value: &str) -> String { + use acadrust::entities::mtext_format::parse_plain_text; + let doc = parse_plain_text(value); + let mut out = String::new(); + for para in &doc.paragraphs { + for span in ¶.spans { + let (u, o) = (span.properties.underline(), span.properties.overline()); + if u { + out.push_str("\\L"); + } + if o { + out.push_str("\\O"); + } + out.push_str(&span.text); + if o { + out.push_str("\\o"); + } + if u { + out.push_str("\\l"); + } + } + } + out +} + fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { let p = text_run_placement(t, document); let snap_pt = glam::DVec3::new(p.wcs_insertion[0], p.wcs_insertion[1], p.wcs_insertion[2]); + // Parse `%%` codes via acadrust, re-encoded for the stroke tessellator. + let value = acad_text_encode(&p.value); // Strokes are in glyph-local space (origin = [0,0]). let (strokes, fill_tris) = lff::tessellate_text_ex( [0.0, 0.0], @@ -77,7 +109,7 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { p.width_factor, p.oblique_angle, &p.font, - &p.value, + &value, ); TruckEntity { object: TruckObject::Text(vec![TextStroke { @@ -85,6 +117,15 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { origin: p.origin, color: None, fill_tris, + run: Some(GlyphRun { + text: value, + font: p.font.clone(), + height: p.height, + rotation: p.rotation, + width_factor: p.width_factor, + oblique: p.oblique_angle, + tracking: 1.0, + }), }]), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index 7562410c3..5dbea79d7 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -1,7 +1,7 @@ use acadrust::types::aci_table::aci_to_rgb; use acadrust::{CadDocument, EntityType}; -use crate::scene::convert::acad_to_truck::TextStroke; +use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke}; use crate::scene::text::font_face::Face; use crate::scene::text::lff; @@ -1175,6 +1175,17 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout { origin, color, fill_tris, + // `text` is the plain word (specials already resolved, + // no decoration markers); `run_h` is raw like the strokes. + run: Some(GlyphRun { + text: text.clone(), + font: font_name.to_string(), + height: run_h, + rotation: rot, + width_factor: signed_wf, + oblique, + tracking, + }), }); if opts.want_glyph_boxes { // Per-character boxes, advancing exactly as diff --git a/src/entities/tolerance.rs b/src/entities/tolerance.rs index 56968f914..3c62229c9 100644 --- a/src/entities/tolerance.rs +++ b/src/entities/tolerance.rs @@ -249,6 +249,7 @@ impl TruckConvertible for Tolerance { origin, color: None, fill_tris: vec![], + run: None, }]), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], diff --git a/src/scene/convert/acad_to_truck.rs b/src/scene/convert/acad_to_truck.rs index fcd5208d1..52516b9f9 100644 --- a/src/scene/convert/acad_to_truck.rs +++ b/src/scene/convert/acad_to_truck.rs @@ -19,6 +19,25 @@ pub struct TextStroke { pub origin: [f64; 2], pub color: Option<[f32; 3]>, pub fill_tris: Vec<[f32; 2]>, + /// Layout inputs to rebuild this run as per-glyph SDF quads (see + /// `scene::text::glyph_quads`). `Some` on runs wired for the SDF text + /// renderer; `None` leaves the run to the stroke path only. Heights are + /// raw (pre annotation-scale), matching `strokes` — the SDF collector + /// applies annotation scale the same way `tessellate` does for strokes. + pub run: Option, +} + +/// Per-run text-layout inputs needed to reproduce a run as SDF glyph quads. +#[derive(Clone, Debug)] +pub struct GlyphRun { + pub text: String, + pub font: String, + /// Raw height in drawing units (annotation scale applied later). + pub height: f32, + pub rotation: f32, + pub width_factor: f32, + pub oblique: f32, + pub tracking: f32, } #[allow(dead_code)] diff --git a/src/scene/text/glyph_quads.rs b/src/scene/text/glyph_quads.rs index dc7ca667e..314657a68 100644 --- a/src/scene/text/glyph_quads.rs +++ b/src/scene/text/glyph_quads.rs @@ -70,11 +70,22 @@ pub fn layout_glyph_quads( let mut cursor_x = 0.0f32; let mut quads = Vec::new(); - for ch in text.chars() { - if ch == ' ' { - cursor_x += face.word_spacing(); - continue; - } + // Same token walk as `tessellate_text_run` (LFF branch): DXF `%%` specials + // resolve to glyphs, spaces/missing advance the pen, decoration toggles emit + // no glyph (they render as lines on the stroke path). + for tok in crate::scene::text::lff::tokenize_run(text) { + let ch = match tok { + crate::scene::text::lff::Tok::Glyph(c) => c, + crate::scene::text::lff::Tok::Space => { + cursor_x += face.word_spacing(); + continue; + } + crate::scene::text::lff::Tok::Missing => { + cursor_x += 6.0 + face.letter_spacing() * tracking; + continue; + } + crate::scene::text::lff::Tok::Deco(..) => continue, + }; match atlas.get_or_insert(font_name, ch) { Some(e) => { let (lo, hi) = (e.plane_min, e.plane_max); @@ -91,8 +102,7 @@ pub fn layout_glyph_quads( cursor_x += e.advance + face.letter_spacing() * tracking; } None => { - // No ink (whitespace glyph) or atlas full: advance only, using - // the glyph's own advance when the font knows it. + // No ink (whitespace glyph) or atlas full: advance only. let adv = face.glyph(ch).map(|g| g.advance).unwrap_or(6.0); cursor_x += adv + face.letter_spacing() * tracking; } diff --git a/src/scene/text/lff.rs b/src/scene/text/lff.rs index e4a31f152..fba204ba3 100644 --- a/src/scene/text/lff.rs +++ b/src/scene/text/lff.rs @@ -193,7 +193,7 @@ pub fn get_font(name: &str) -> &'static Font { /// Decoration line a token toggles. #[derive(Clone, Copy)] -enum Deco { +pub(crate) enum Deco { Under, Over, Strike, @@ -202,7 +202,7 @@ enum Deco { /// How a decoration token changes state. `\L`/`\O`/`\K` turn on (idempotent), /// `\l`/`\o`/`\k` turn off; `%%u`/`%%o` flip. #[derive(Clone, Copy)] -enum Op { +pub(crate) enum Op { On, Off, Toggle, @@ -211,7 +211,7 @@ enum Op { /// One unit of a text run after inline-code resolution. Shared by the stroke /// (LFF) and shaped (TTF) renderers so both interpret the DXF inline grammar /// — `\L…\l` decorations and `%%` specials — identically. -enum Tok { +pub(crate) enum Tok { /// A renderable character (DXF `%%d`/`%%p`/`%%c`/`%%nnn` already resolved). Glyph(char), /// A literal space. @@ -225,7 +225,7 @@ enum Tok { /// Resolve a run's inline codes into a flat token stream. Mirrors the original /// inline parser in `tessellate_text_run` exactly (verified by the golden test). -fn tokenize_run(text: &str) -> Vec { +pub(crate) fn tokenize_run(text: &str) -> Vec { let mut toks = Vec::new(); let mut chars = text.chars().peekable(); while let Some(ch) = chars.next() { diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index bd0eb0ce5..049145d32 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -792,6 +792,8 @@ impl Scene { &self, enabled: bool, ) -> Vec { + 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, );