forked from 1b-agent/opencadstudio-upstream
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:
1 parent
b88e5e4ad5
commit
d528a3e2a5
9 files changed
+149
-28
No files matched your search
Generated
+2
-2
@@ -71,7 +71,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.0"
|
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 = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -3160,7 +3160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
|
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytecount",
|
"bytecount",
|
||||||
"memchr 2.8.2",
|
"memchr 1.0.2",
|
||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument)
|
|||||||
origin,
|
origin,
|
||||||
color: None,
|
color: None,
|
||||||
fill_tris,
|
fill_tris,
|
||||||
|
run: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let _ = input.line_count; // round-trip only — recomputed above
|
let _ = input.line_count; // round-trip only — recomputed above
|
||||||
|
|||||||
+43
-2
@@ -6,7 +6,7 @@ use crate::entities::text_support::{
|
|||||||
resolve_dxf_special_chars, resolve_text_style, text_local_bounds,
|
resolve_dxf_special_chars, resolve_text_style, text_local_bounds,
|
||||||
};
|
};
|
||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
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::text::lff;
|
||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||||
use crate::scene::model::wire_model::SnapHint;
|
use crate::scene::model::wire_model::SnapHint;
|
||||||
@@ -66,9 +66,41 @@ pub struct TextPlacement {
|
|||||||
pub wcs_insertion: [f64; 3],
|
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 {
|
fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
|
||||||
let p = text_run_placement(t, document);
|
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]);
|
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]).
|
// Strokes are in glyph-local space (origin = [0,0]).
|
||||||
let (strokes, fill_tris) = lff::tessellate_text_ex(
|
let (strokes, fill_tris) = lff::tessellate_text_ex(
|
||||||
[0.0, 0.0],
|
[0.0, 0.0],
|
||||||
@@ -77,7 +109,7 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
|
|||||||
p.width_factor,
|
p.width_factor,
|
||||||
p.oblique_angle,
|
p.oblique_angle,
|
||||||
&p.font,
|
&p.font,
|
||||||
&p.value,
|
&value,
|
||||||
);
|
);
|
||||||
TruckEntity {
|
TruckEntity {
|
||||||
object: TruckObject::Text(vec![TextStroke {
|
object: TruckObject::Text(vec![TextStroke {
|
||||||
@@ -85,6 +117,15 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
|
|||||||
origin: p.origin,
|
origin: p.origin,
|
||||||
color: None,
|
color: None,
|
||||||
fill_tris,
|
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)],
|
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use acadrust::types::aci_table::aci_to_rgb;
|
use acadrust::types::aci_table::aci_to_rgb;
|
||||||
use acadrust::{CadDocument, EntityType};
|
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::font_face::Face;
|
||||||
use crate::scene::text::lff;
|
use crate::scene::text::lff;
|
||||||
|
|
||||||
@@ -1175,6 +1175,17 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
|||||||
origin,
|
origin,
|
||||||
color,
|
color,
|
||||||
fill_tris,
|
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 {
|
if opts.want_glyph_boxes {
|
||||||
// Per-character boxes, advancing exactly as
|
// Per-character boxes, advancing exactly as
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ impl TruckConvertible for Tolerance {
|
|||||||
origin,
|
origin,
|
||||||
color: None,
|
color: None,
|
||||||
fill_tris: vec![],
|
fill_tris: vec![],
|
||||||
|
run: None,
|
||||||
}]),
|
}]),
|
||||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![],
|
||||||
|
|||||||
@@ -19,6 +19,25 @@ pub struct TextStroke {
|
|||||||
pub origin: [f64; 2],
|
pub origin: [f64; 2],
|
||||||
pub color: Option<[f32; 3]>,
|
pub color: Option<[f32; 3]>,
|
||||||
pub fill_tris: Vec<[f32; 2]>,
|
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<GlyphRun>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
@@ -70,11 +70,22 @@ pub fn layout_glyph_quads(
|
|||||||
let mut cursor_x = 0.0f32;
|
let mut cursor_x = 0.0f32;
|
||||||
let mut quads = Vec::new();
|
let mut quads = Vec::new();
|
||||||
|
|
||||||
for ch in text.chars() {
|
// Same token walk as `tessellate_text_run` (LFF branch): DXF `%%` specials
|
||||||
if ch == ' ' {
|
// resolve to glyphs, spaces/missing advance the pen, decoration toggles emit
|
||||||
cursor_x += face.word_spacing();
|
// no glyph (they render as lines on the stroke path).
|
||||||
continue;
|
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) {
|
match atlas.get_or_insert(font_name, ch) {
|
||||||
Some(e) => {
|
Some(e) => {
|
||||||
let (lo, hi) = (e.plane_min, e.plane_max);
|
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;
|
cursor_x += e.advance + face.letter_spacing() * tracking;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// No ink (whitespace glyph) or atlas full: advance only, using
|
// No ink (whitespace glyph) or atlas full: advance only.
|
||||||
// the glyph's own advance when the font knows it.
|
|
||||||
let adv = face.glyph(ch).map(|g| g.advance).unwrap_or(6.0);
|
let adv = face.glyph(ch).map(|g| g.advance).unwrap_or(6.0);
|
||||||
cursor_x += adv + face.letter_spacing() * tracking;
|
cursor_x += adv + face.letter_spacing() * tracking;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ pub fn get_font(name: &str) -> &'static Font {
|
|||||||
|
|
||||||
/// Decoration line a token toggles.
|
/// Decoration line a token toggles.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum Deco {
|
pub(crate) enum Deco {
|
||||||
Under,
|
Under,
|
||||||
Over,
|
Over,
|
||||||
Strike,
|
Strike,
|
||||||
@@ -202,7 +202,7 @@ enum Deco {
|
|||||||
/// How a decoration token changes state. `\L`/`\O`/`\K` turn on (idempotent),
|
/// How a decoration token changes state. `\L`/`\O`/`\K` turn on (idempotent),
|
||||||
/// `\l`/`\o`/`\k` turn off; `%%u`/`%%o` flip.
|
/// `\l`/`\o`/`\k` turn off; `%%u`/`%%o` flip.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum Op {
|
pub(crate) enum Op {
|
||||||
On,
|
On,
|
||||||
Off,
|
Off,
|
||||||
Toggle,
|
Toggle,
|
||||||
@@ -211,7 +211,7 @@ enum Op {
|
|||||||
/// One unit of a text run after inline-code resolution. Shared by the stroke
|
/// 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
|
/// (LFF) and shaped (TTF) renderers so both interpret the DXF inline grammar
|
||||||
/// — `\L…\l` decorations and `%%` specials — identically.
|
/// — `\L…\l` decorations and `%%` specials — identically.
|
||||||
enum Tok {
|
pub(crate) enum Tok {
|
||||||
/// A renderable character (DXF `%%d`/`%%p`/`%%c`/`%%nnn` already resolved).
|
/// A renderable character (DXF `%%d`/`%%p`/`%%c`/`%%nnn` already resolved).
|
||||||
Glyph(char),
|
Glyph(char),
|
||||||
/// A literal space.
|
/// A literal space.
|
||||||
@@ -225,7 +225,7 @@ enum Tok {
|
|||||||
|
|
||||||
/// Resolve a run's inline codes into a flat token stream. Mirrors the original
|
/// 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).
|
/// inline parser in `tessellate_text_run` exactly (verified by the golden test).
|
||||||
fn tokenize_run(text: &str) -> Vec<Tok> {
|
pub(crate) fn tokenize_run(text: &str) -> Vec<Tok> {
|
||||||
let mut toks = Vec::new();
|
let mut toks = Vec::new();
|
||||||
let mut chars = text.chars().peekable();
|
let mut chars = text.chars().peekable();
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
|
|||||||
+50
-12
@@ -792,6 +792,8 @@ impl Scene {
|
|||||||
&self,
|
&self,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
) -> Vec<crate::scene::pipeline::text_gpu::TextVertex> {
|
) -> 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::pipeline::text_gpu;
|
||||||
use crate::scene::text::{glyph_quads, sdf_atlas};
|
use crate::scene::text::{glyph_quads, sdf_atlas};
|
||||||
if !enabled {
|
if !enabled {
|
||||||
@@ -800,26 +802,62 @@ impl Scene {
|
|||||||
let Ok(mut atlas) = sdf_atlas::text_atlas().lock() else {
|
let Ok(mut atlas) = sdf_atlas::text_atlas().lock() else {
|
||||||
return Vec::new();
|
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();
|
let mut out = Vec::new();
|
||||||
for e in self.document.entities() {
|
for e in self.document.entities() {
|
||||||
if let acadrust::EntityType::Text(t) = e {
|
// Only text-producing entities — converting solids/hatches here
|
||||||
let p = crate::entities::text::text_run_placement(t, &self.document);
|
// would re-tessellate the whole document every frame.
|
||||||
let color = self.render_style(e).0;
|
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(
|
let quads = glyph_quads::layout_glyph_quads(
|
||||||
&mut atlas,
|
&mut atlas,
|
||||||
p.height,
|
run.height,
|
||||||
p.rotation,
|
run.rotation,
|
||||||
p.width_factor,
|
run.width_factor,
|
||||||
p.oblique_angle,
|
run.oblique,
|
||||||
1.0,
|
run.tracking,
|
||||||
&p.font,
|
&run.font,
|
||||||
&p.value,
|
&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(
|
text_gpu::push_glyph_vertices(
|
||||||
&mut out,
|
&mut out,
|
||||||
&quads,
|
&quads,
|
||||||
[p.origin[0], p.origin[1], p.elevation],
|
[slx, sly, elev],
|
||||||
1.0,
|
anno,
|
||||||
color,
|
color,
|
||||||
0.0,
|
0.0,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user