forked from 1b-agent/opencadstudio-upstream
refactor(text): SDF for all text kinds; remove the legacy stroke text path
SDF is the only text renderer now — the stroke path and its LOD ladder are gone, and the remaining text kinds render as SDF glyph quads too. - ATTDEF / ATTRIB: carry a GlyphRun so single-line attribute text renders as SDF (multi-line already did via layout_mtext). Tolerance / complex-linetype text already SDF. - Remove `sdf_text_enabled()` (the OCS_TEXT_SDF flag / kill switch) and take the SDF path unconditionally everywhere: tessellate Text arm (per-group by GlyphRun presence), hit_test fallbacks, render gather, complex_lt, table, mleader. - Delete the text LOD ladder: tess.rs baseline/greek block and the block_cache greek/baseline path (emit_greeked_text / emit_text_baseline), plus the now-dead helpers text_baseline_points / text_greek_obb_tris / text_obb_corners_native / mtext_line_count and the LocalWire text_height_local / text_obb_local fields. All text is crisp at every zoom; text entities are exempt from the sub-pixel stub cull. - Fix a latent bug: block-internal SDF text was culled because its LocalWire aabb_local was [0,0,0,0] (no points) — fold the glyph-quad extent into aabb_local and exempt text_verts-carrying wires from the sub-pixel cull. - Clear the pre-existing dead-code warnings (TextPlacement.elevation, TabKind, MTextRunKind::Tab). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
67ce45d40b
commit
5c15091e74
12 files changed
+145
-1238
No files matched your search
@@ -11,7 +11,7 @@ use crate::entities::text_support::{
|
||||
MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
|
||||
};
|
||||
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::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
use crate::scene::text::lff;
|
||||
@@ -295,7 +295,18 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument)
|
||||
origin,
|
||||
color: None,
|
||||
fill_tris,
|
||||
run: None,
|
||||
// Carry a GlyphRun so the attribute renders as SDF glyph quads
|
||||
// (same as TEXT); the strokes above are the historical fallback and
|
||||
// are suppressed by the per-group SDF path.
|
||||
run: Some(GlyphRun {
|
||||
text: encoded,
|
||||
font: resolved.font_name.clone(),
|
||||
height: input.height as f32,
|
||||
rotation,
|
||||
width_factor,
|
||||
oblique: oblique_angle,
|
||||
tracking: 1.0,
|
||||
}),
|
||||
});
|
||||
}
|
||||
let _ = input.line_count; // round-trip only — recomputed above
|
||||
|
||||
+2
-186
@@ -1084,7 +1084,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
entity_color: [f32; 4],
|
||||
line_weight_px: f32,
|
||||
anno_scale: f32,
|
||||
world_per_pixel: Option<f32>,
|
||||
_world_per_pixel: Option<f32>,
|
||||
bg_color: [f32; 4],
|
||||
) -> Vec<crate::scene::model::wire_model::WireModel> {
|
||||
use crate::scene::convert::tessellate::{
|
||||
@@ -1508,28 +1508,7 @@ impl MultiLeaderTess for MultiLeader {
|
||||
color_or_inherit(&ctx.text_color, entity_color)
|
||||
};
|
||||
|
||||
// Same LOD ladder used for top-level Text / MText (see scene/mod.rs):
|
||||
// h_px < 1 → baseline line (skip glyphs)
|
||||
// 1 ≤ h < 5 → greeked rect in text color (skip glyphs)
|
||||
// h_px ≥ 5 → full per-glyph stroke tessellation
|
||||
let lod_h_px = world_per_pixel.map(|wpp| height / wpp);
|
||||
let lod_mode = match lod_h_px {
|
||||
Some(h) if h < 1.0 => 0,
|
||||
Some(h) if h < 5.0 => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
// Helper: map a (local_x, local_y) in the text's pre-rotation frame
|
||||
// (origin at the insertion point) into WCS render space.
|
||||
let to_wcs = |lx: f32, ly: f32| -> [f32; 3] {
|
||||
[
|
||||
local_ins_x + lx * cos_r - ly * sin_r,
|
||||
local_ins_y + lx * sin_r + ly * cos_r,
|
||||
z,
|
||||
]
|
||||
};
|
||||
|
||||
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
|
||||
@@ -1597,169 +1576,6 @@ impl MultiLeaderTess for MultiLeader {
|
||||
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
|
||||
.map(|wpp| line_w / wpp)
|
||||
.unwrap_or(f32::INFINITY);
|
||||
if len_px >= 2.0 {
|
||||
let line_y_local = v_offset;
|
||||
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(),
|
||||
color: text_color,
|
||||
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: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
}
|
||||
} else if lod_mode == 1 {
|
||||
// One filled rect per line — keeps the visual "text lives here
|
||||
// per row" hint that multi-line MText carries, in the text's
|
||||
// own color. Empty `points` opts out of the face3d 0.45 dim so
|
||||
// the fill renders at full intensity.
|
||||
let mut greek_tris: Vec<[f32; 3]> = Vec::with_capacity(line_widths.len() * 6);
|
||||
for (i, &line_w) in line_widths.iter().enumerate() {
|
||||
let li = i as f32;
|
||||
let line_y_bottom = -li * line_h + v_offset;
|
||||
let line_y_top = line_y_bottom + height;
|
||||
if line_w <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let left = -line_w * h_anchor;
|
||||
let right = line_w * (1.0 - h_anchor);
|
||||
let bl = to_wcs(left, line_y_bottom);
|
||||
let br = to_wcs(right, line_y_bottom);
|
||||
let tr = to_wcs(right, line_y_top);
|
||||
let tl = to_wcs(left, line_y_top);
|
||||
greek_tris.extend_from_slice(&[bl, br, tr, bl, tr, tl]);
|
||||
}
|
||||
if !greek_tris.is_empty() {
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
points_low: Vec::new(),
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
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: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: greek_tris,
|
||||
// fill_tris_low intentionally empty: this fill renders on
|
||||
// the top-level path, where consumers treat a short low
|
||||
// half as all-zero, so it draws at f32 precision (sub-
|
||||
// metre error at UTM scale) — not a crash. Follow-up:
|
||||
// double-single-split via points_to_ds to match emit_wire.
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Pre-tessellated by `layout_mtext`. Each TextStroke is in
|
||||
// local glyph space with its world origin (already offset-
|
||||
// relative because we passed local_ins_x/y) stored as f64.
|
||||
let mut text_points: Vec<[f32; 3]> = Vec::new();
|
||||
let mut text_fill_tris: Vec<[f32; 3]> = Vec::new();
|
||||
for ts in &layout.strokes {
|
||||
let ox = ts.origin[0] as f32;
|
||||
let oy = ts.origin[1] as f32;
|
||||
for stroke in &ts.strokes {
|
||||
if stroke.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
text_points.push(nan);
|
||||
for &[x, y] in stroke {
|
||||
text_points.push([x + ox, y + oy, z]);
|
||||
}
|
||||
}
|
||||
for &[x, y] in &ts.fill_tris {
|
||||
text_fill_tris.push([x + ox, y + oy, z]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut is_first = true;
|
||||
if !text_points.is_empty() {
|
||||
let snap = if is_first {
|
||||
is_first = false;
|
||||
vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: text_points,
|
||||
points_low: Vec::new(),
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: snap,
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
}
|
||||
if !text_fill_tris.is_empty() {
|
||||
let snap = if is_first {
|
||||
vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
wires.push(WireModel {
|
||||
text_verts: Vec::new(),
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
points_low: Vec::new(),
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: snap,
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: text_fill_tris,
|
||||
// fill_tris_low intentionally empty: this fill renders on
|
||||
// the top-level path, where consumers treat a short low
|
||||
// half as all-zero, so it draws at f32 precision (sub-
|
||||
// metre error at UTM scale) — not a crash. Follow-up:
|
||||
// double-single-split via points_to_ds to match emit_wire.
|
||||
fill_tris_low: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
||||
|
||||
+23
-59
@@ -492,10 +492,8 @@ pub fn tessellate_table(
|
||||
|
||||
// Accumulators keyed by quantised colour (+ weight for borders).
|
||||
let mut fills: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>)> = HashMap::default();
|
||||
let mut texts: HashMap<[u8; 4], ([f32; 4], Vec<[f32; 3]>, Vec<[f32; 3]>)> = HashMap::default();
|
||||
// SDF cell text: glyph quads (per-vertex coloured) collected across all
|
||||
// cells; emitted as one text-carrying wire at the end. Empty when SDF off.
|
||||
let sdf_on = crate::scene::text::sdf_atlas::sdf_text_enabled();
|
||||
// cells; emitted as one text-carrying wire at the end.
|
||||
let mut text_verts: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
||||
let mut borders: HashMap<([u8; 4], u32), ([f32; 4], f32, Vec<[f32; 3]>)> = HashMap::default();
|
||||
let mut emitted: rustc_hash::FxHashSet<(i32, i32, i32, i32)> = rustc_hash::FxHashSet::default();
|
||||
@@ -713,55 +711,29 @@ pub fn tessellate_table(
|
||||
} else {
|
||||
entity_color
|
||||
};
|
||||
if sdf_on {
|
||||
// SDF: glyph quads per run at its placed origin (rotation baked
|
||||
// in by layout_glyph_quads), coloured by the cell's text colour.
|
||||
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 text_verts,
|
||||
&quads,
|
||||
[ts.origin[0], ts.origin[1], to.z as f64],
|
||||
1.0,
|
||||
tcol,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let entry = texts
|
||||
.entry(key4(tcol))
|
||||
.or_insert_with(|| (tcol, Vec::new(), Vec::new()));
|
||||
let buf = &mut entry.1;
|
||||
let tris_buf = &mut entry.2;
|
||||
// SDF glyph quads per run at its placed origin (rotation baked in by
|
||||
// layout_glyph_quads), coloured by the cell's text colour.
|
||||
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
||||
for ts in &layout.strokes {
|
||||
let sx = ts.origin[0] as f32;
|
||||
let sy = ts.origin[1] as f32;
|
||||
for stroke in &ts.strokes {
|
||||
if stroke.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
if !buf.is_empty() {
|
||||
buf.push([f32::NAN; 3]);
|
||||
}
|
||||
for &[x, y] in stroke {
|
||||
buf.push([x + sx, y + sy, (to.z as f64) as f32]);
|
||||
}
|
||||
}
|
||||
for &[x, y] in &ts.fill_tris {
|
||||
tris_buf.push([x + sx, y + sy, (to.z as f64) as f32]);
|
||||
}
|
||||
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 text_verts,
|
||||
&quads,
|
||||
[ts.origin[0], ts.origin[1], to.z as f64],
|
||||
1.0,
|
||||
tcol,
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -809,14 +781,6 @@ pub fn tessellate_table(
|
||||
out.push(mk(color, pts, vec![], lw));
|
||||
}
|
||||
}
|
||||
for (_, (color, pts, tris)) in texts {
|
||||
if !pts.is_empty() {
|
||||
out.push(mk(color, pts, vec![], line_weight_px));
|
||||
}
|
||||
if !tris.is_empty() {
|
||||
out.push(mk(color, vec![], tris, line_weight_px));
|
||||
}
|
||||
}
|
||||
// SDF cell text: one wire carrying the glyph quads (per-vertex coloured) +
|
||||
// a glyph-bounds AABB (f64 accumulate → f32) so the text draws + picks;
|
||||
// empty points so it adds no stroke geometry.
|
||||
|
||||
@@ -53,8 +53,6 @@ pub(crate) fn sync_text_alignment_point(t: &mut Text) {
|
||||
pub struct TextPlacement {
|
||||
/// Run-local origin (glyph space `[0,0]` maps here), WCS xy, kept f64.
|
||||
pub origin: [f64; 2],
|
||||
/// Entity elevation (WCS z).
|
||||
pub elevation: f64,
|
||||
pub height: f32,
|
||||
pub rotation: f32,
|
||||
pub width_factor: f32,
|
||||
@@ -232,7 +230,6 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||
];
|
||||
TextPlacement {
|
||||
origin,
|
||||
elevation: wsz,
|
||||
height: t.height as f32,
|
||||
rotation,
|
||||
width_factor,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use acadrust::types::aci_table::aci_to_rgb;
|
||||
use acadrust::{CadDocument, EntityType};
|
||||
use acadrust::CadDocument;
|
||||
|
||||
use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke};
|
||||
use crate::scene::text::font_face::Face;
|
||||
@@ -236,7 +236,9 @@ pub enum InlineColor {
|
||||
True([f32; 3]),
|
||||
}
|
||||
|
||||
/// Tab-stop alignment kind (from `\pt<L|C|R><pos>` entries).
|
||||
/// Tab-stop alignment kind (from `\pt<L|C|R><pos>` entries). `Center` / `Right`
|
||||
/// are DXF-spec kinds the parser does not emit yet (only `Left` is produced).
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TabKind {
|
||||
Left,
|
||||
@@ -297,6 +299,8 @@ pub enum MTextRunKind {
|
||||
/// Renderable glyph text (DXF specials resolved, decoration markers stripped).
|
||||
Glyphs(String),
|
||||
/// `\t` — jump the cursor to the next tab stop (or default tab interval).
|
||||
/// Handled in the layout matches but not emitted by the parser yet.
|
||||
#[allow(dead_code)]
|
||||
Tab,
|
||||
}
|
||||
|
||||
@@ -482,87 +486,6 @@ pub fn adapt_mtext_paragraphs(
|
||||
// `adapt_mtext_paragraphs`, `layout_mtext`, `mtext_line_count`,
|
||||
// `text_local_bounds`, and `resolve_dxf_special_chars`.
|
||||
|
||||
/// Total number of visible lines an MText renders to — explicit `\P` /
|
||||
/// `\n` / `\N` breaks plus word-wrap induced sublines when
|
||||
/// `rectangle_width > 0`. Drives LOD splits (greek-rect per row, baseline
|
||||
/// counts) without re-running the full stroke tessellation; routes through
|
||||
/// the same parse + atomise + wrap pipeline as `layout_mtext` so the LOD
|
||||
/// row count and the rendered row count never disagree.
|
||||
pub fn mtext_line_count(
|
||||
m: &acadrust::entities::MText,
|
||||
document: &CadDocument,
|
||||
anno_scale: f32,
|
||||
) -> usize {
|
||||
let resolved = resolve_text_style(&m.style, document);
|
||||
let entity_h = (m.height as f32) * anno_scale;
|
||||
let base_wf_abs = resolved.width_factor.max(0.01);
|
||||
let base_wf = if resolved.is_backward { -base_wf_abs } else { base_wf_abs };
|
||||
let base_font_name = resolved.font_name.clone();
|
||||
let rect_w = (m.rectangle_width as f32) * anno_scale;
|
||||
|
||||
let paragraphs = adapt_mtext_paragraphs(&m.value, entity_h, true);
|
||||
let mut total = 0usize;
|
||||
for para in ¶graphs {
|
||||
let mut atoms: Vec<LayoutAtom> = Vec::new();
|
||||
for run in ¶.runs {
|
||||
match &run.kind {
|
||||
MTextRunKind::Glyphs(text) => {
|
||||
let mut word = String::new();
|
||||
for ch in text.chars() {
|
||||
if ch == ' ' || ch == '\u{00A0}' {
|
||||
if !word.is_empty() {
|
||||
atoms.push(LayoutAtom {
|
||||
kind: AtomKind::Word(std::mem::take(&mut word)),
|
||||
state: run.state.clone(),
|
||||
});
|
||||
}
|
||||
atoms.push(LayoutAtom {
|
||||
kind: AtomKind::Space,
|
||||
state: run.state.clone(),
|
||||
});
|
||||
} else {
|
||||
word.push(ch);
|
||||
}
|
||||
}
|
||||
if !word.is_empty() {
|
||||
atoms.push(LayoutAtom {
|
||||
kind: AtomKind::Word(word),
|
||||
state: run.state.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
MTextRunKind::Tab => atoms.push(LayoutAtom {
|
||||
kind: AtomKind::Tab,
|
||||
state: run.state.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
// Same edge-trim the renderer applies — otherwise a trailing space
|
||||
// can inflate the wrap result by one extra sub-line.
|
||||
let first_word = atoms
|
||||
.iter()
|
||||
.position(|a| !matches!(a.kind, AtomKind::Space))
|
||||
.unwrap_or(atoms.len());
|
||||
atoms.drain(..first_word);
|
||||
while matches!(atoms.last().map(|a| &a.kind), Some(AtomKind::Space)) {
|
||||
atoms.pop();
|
||||
}
|
||||
let wrapped = wrap_paragraph(
|
||||
atoms,
|
||||
rect_w,
|
||||
para.indent_first,
|
||||
para.indent_left,
|
||||
para.indent_right,
|
||||
¶.tab_stops,
|
||||
entity_h,
|
||||
base_wf,
|
||||
&base_font_name,
|
||||
);
|
||||
total += wrapped.len().max(1);
|
||||
}
|
||||
total.max(1)
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
// Shared MText layout / render pipeline
|
||||
// ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -1255,332 +1178,6 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
glyph_boxes,
|
||||
}
|
||||
}
|
||||
pub(crate) fn text_obb_corners_native(
|
||||
e: &EntityType,
|
||||
anno_scale: f32,
|
||||
mtext_lines_override: Option<usize>,
|
||||
) -> Option<[[f64; 3]; 4]> {
|
||||
use acadrust::entities::attribute_definition::{
|
||||
HorizontalAlignment as AttrHA, VerticalAlignment as AttrVA,
|
||||
};
|
||||
use acadrust::entities::{AttachmentPoint, TextHorizontalAlignment, TextVerticalAlignment};
|
||||
|
||||
let anno = anno_scale as f64;
|
||||
|
||||
// Map Attribute alignment enums to the same h/v anchor fractions used by
|
||||
// Text. Kept local because the Attribute enum is distinct from Text's.
|
||||
let attr_h_anchor = |ha: AttrHA| -> f64 {
|
||||
match ha {
|
||||
AttrHA::Left => 0.0,
|
||||
AttrHA::Center | AttrHA::Middle | AttrHA::Aligned | AttrHA::Fit => 0.5,
|
||||
AttrHA::Right => 1.0,
|
||||
}
|
||||
};
|
||||
let attr_v_anchor = |va: AttrVA| -> f64 {
|
||||
match va {
|
||||
AttrVA::Baseline | AttrVA::Bottom => 0.0,
|
||||
AttrVA::Middle => 0.5,
|
||||
AttrVA::Top => 1.0,
|
||||
}
|
||||
};
|
||||
let attr_use_align_pt = |ha: AttrHA, va: AttrVA| -> bool {
|
||||
!matches!((ha, va), (AttrHA::Left, AttrVA::Baseline))
|
||||
};
|
||||
|
||||
let (ix, iy, iz, w, h, rot, h_anchor, v_anchor) = match e {
|
||||
EntityType::Text(t) => {
|
||||
let h_world = t.height * anno;
|
||||
let w_factor = if t.width_factor > 0.0 { t.width_factor } else { 1.0 };
|
||||
let n = t.value.chars().count().max(1) as f64;
|
||||
// Approximate glyph width: AutoCAD's stroke fonts average ~0.6 em.
|
||||
let w_world = n * h_world * w_factor * 0.6;
|
||||
let h_anchor = match t.horizontal_alignment {
|
||||
TextHorizontalAlignment::Left => 0.0,
|
||||
TextHorizontalAlignment::Center
|
||||
| TextHorizontalAlignment::Middle
|
||||
| TextHorizontalAlignment::Aligned
|
||||
| TextHorizontalAlignment::Fit => 0.5,
|
||||
TextHorizontalAlignment::Right => 1.0,
|
||||
};
|
||||
let v_anchor = match t.vertical_alignment {
|
||||
TextVerticalAlignment::Baseline | TextVerticalAlignment::Bottom => 0.0,
|
||||
TextVerticalAlignment::Middle => 0.5,
|
||||
TextVerticalAlignment::Top => 1.0,
|
||||
};
|
||||
// AutoCAD writes `alignment_point` (DXF 11) whenever the text
|
||||
// isn't simple Left+Baseline; in that case it — not the
|
||||
// insertion_point — is the anchor the alignment fractions map to.
|
||||
let use_alignment_pt = !matches!(
|
||||
(t.horizontal_alignment, t.vertical_alignment),
|
||||
(
|
||||
TextHorizontalAlignment::Left,
|
||||
TextVerticalAlignment::Baseline,
|
||||
)
|
||||
);
|
||||
let anchor = match (use_alignment_pt, t.alignment_point) {
|
||||
(true, Some(p)) => p,
|
||||
_ => t.insertion_point,
|
||||
};
|
||||
(
|
||||
anchor.x,
|
||||
anchor.y,
|
||||
anchor.z,
|
||||
w_world,
|
||||
h_world,
|
||||
t.rotation,
|
||||
h_anchor,
|
||||
v_anchor,
|
||||
)
|
||||
}
|
||||
EntityType::AttributeDefinition(a) => {
|
||||
let h_world = a.height * anno;
|
||||
let w_factor = if a.width_factor > 0.0 { a.width_factor } else { 1.0 };
|
||||
// Render the tag in preview when no default; matches `attribute.rs`.
|
||||
let display = if a.default_value.is_empty() { &a.tag } else { &a.default_value };
|
||||
let n = display.chars().count().max(1) as f64;
|
||||
let w_world = n * h_world * w_factor * 0.6;
|
||||
let h_anchor = attr_h_anchor(a.horizontal_alignment);
|
||||
let v_anchor = attr_v_anchor(a.vertical_alignment);
|
||||
let anchor = if attr_use_align_pt(a.horizontal_alignment, a.vertical_alignment) {
|
||||
a.alignment_point
|
||||
} else {
|
||||
a.insertion_point
|
||||
};
|
||||
(
|
||||
anchor.x, anchor.y, anchor.z, w_world, h_world, a.rotation, h_anchor, v_anchor,
|
||||
)
|
||||
}
|
||||
EntityType::AttributeEntity(a) => {
|
||||
let h_world = a.height * anno;
|
||||
let w_factor = if a.width_factor > 0.0 { a.width_factor } else { 1.0 };
|
||||
let n = a.value.chars().count().max(1) as f64;
|
||||
let w_world = n * h_world * w_factor * 0.6;
|
||||
let h_anchor = attr_h_anchor(a.horizontal_alignment);
|
||||
let v_anchor = attr_v_anchor(a.vertical_alignment);
|
||||
let anchor = if attr_use_align_pt(a.horizontal_alignment, a.vertical_alignment) {
|
||||
a.alignment_point
|
||||
} else {
|
||||
a.insertion_point
|
||||
};
|
||||
(
|
||||
anchor.x, anchor.y, anchor.z, w_world, h_world, a.rotation, h_anchor, v_anchor,
|
||||
)
|
||||
}
|
||||
EntityType::Tolerance(t) => {
|
||||
// Approximate: a feature control frame is roughly 1 line tall;
|
||||
// width comes from char-count of the (already-stripped) text.
|
||||
// GD&T symbols all advance one cell, so plain char count is close.
|
||||
let raw_h = if t.text_height > 0.0 { t.text_height } else { 2.5 };
|
||||
let h_world = raw_h * anno;
|
||||
// Each cell ≈ 1.4 × height (matches `tolerance.rs` min_cell_w).
|
||||
let n = t.text.chars().filter(|c| *c != '\n').count().max(1) as f64;
|
||||
let w_world = h_world * 1.4 * n * 0.5; // rough — fine for LOD greek
|
||||
// direction encodes rotation as a vector.
|
||||
let rot = (t.direction.y).atan2(t.direction.x);
|
||||
(
|
||||
t.insertion_point.x,
|
||||
t.insertion_point.y,
|
||||
t.insertion_point.z,
|
||||
w_world,
|
||||
h_world * 1.5, // frame is taller than glyph cap by ~0.5 h
|
||||
rot,
|
||||
0.0, // anchored at insertion point (bottom-left)
|
||||
0.0,
|
||||
)
|
||||
}
|
||||
EntityType::MText(m) => {
|
||||
let h_world = m.height * anno;
|
||||
let raw_lines = (m.value.matches('\n').count() + 1) as f64;
|
||||
let effective_lines = match mtext_lines_override {
|
||||
Some(n) => n.max(1) as f64,
|
||||
None => raw_lines,
|
||||
};
|
||||
let w_world = if m.rectangle_width > 0.0 {
|
||||
m.rectangle_width
|
||||
} else {
|
||||
h_world * 8.0 * effective_lines.max(1.0)
|
||||
};
|
||||
// Wrap-aware override beats `rectangle_height` — the stored
|
||||
// height can be stale on DWGs that were re-saved without
|
||||
// updating the bounds.
|
||||
let total_h = if mtext_lines_override.is_some() {
|
||||
h_world * effective_lines.max(1.0) * m.line_spacing_factor.max(0.5)
|
||||
} else {
|
||||
m.rectangle_height.unwrap_or(
|
||||
h_world * raw_lines.max(1.0) * m.line_spacing_factor.max(0.5),
|
||||
)
|
||||
};
|
||||
// MText `attachment_point` puts `insertion_point` at one of the
|
||||
// 9 corners/midpoints of the text bbox. h_anchor / v_anchor are
|
||||
// fractions from (left, bottom) of the bbox.
|
||||
let (h_anchor, v_anchor) = match m.attachment_point {
|
||||
AttachmentPoint::TopLeft => (0.0, 1.0),
|
||||
AttachmentPoint::TopCenter => (0.5, 1.0),
|
||||
AttachmentPoint::TopRight => (1.0, 1.0),
|
||||
AttachmentPoint::MiddleLeft => (0.0, 0.5),
|
||||
AttachmentPoint::MiddleCenter => (0.5, 0.5),
|
||||
AttachmentPoint::MiddleRight => (1.0, 0.5),
|
||||
AttachmentPoint::BottomLeft => (0.0, 0.0),
|
||||
AttachmentPoint::BottomCenter => (0.5, 0.0),
|
||||
AttachmentPoint::BottomRight => (1.0, 0.0),
|
||||
};
|
||||
(
|
||||
m.insertion_point.x,
|
||||
m.insertion_point.y,
|
||||
m.insertion_point.z,
|
||||
w_world,
|
||||
total_h,
|
||||
m.rotation,
|
||||
h_anchor,
|
||||
v_anchor,
|
||||
)
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let x0 = -h_anchor * w;
|
||||
let x1 = (1.0 - h_anchor) * w;
|
||||
let y0 = -v_anchor * h;
|
||||
let y1 = (1.0 - v_anchor) * h;
|
||||
|
||||
let (s, c) = (rot.sin(), rot.cos());
|
||||
let rot_pt = |lx: f64, ly: f64| -> [f64; 3] {
|
||||
let rx = lx * c - ly * s;
|
||||
let ry = lx * s + ly * c;
|
||||
[ix + rx, iy + ry, iz]
|
||||
};
|
||||
|
||||
Some([
|
||||
rot_pt(x0, y0),
|
||||
rot_pt(x1, y0),
|
||||
rot_pt(x1, y1),
|
||||
rot_pt(x0, y1),
|
||||
])
|
||||
}
|
||||
pub(crate) fn text_baseline_points(
|
||||
e: &EntityType,
|
||||
anno_scale: f32,
|
||||
n_lines: usize,
|
||||
) -> Vec<[f32; 3]> {
|
||||
let Some(corners) = text_obb_corners_native(e, anno_scale, Some(n_lines)) else {
|
||||
return vec![];
|
||||
};
|
||||
let line_h = match e {
|
||||
EntityType::Text(t) => (t.height * anno_scale as f64) as f32,
|
||||
EntityType::MText(m) => (m.height * anno_scale as f64) as f32,
|
||||
EntityType::AttributeDefinition(a) => (a.height * anno_scale as f64) as f32,
|
||||
EntityType::AttributeEntity(a) => (a.height * anno_scale as f64) as f32,
|
||||
EntityType::Tolerance(t) => {
|
||||
let raw = if t.text_height > 0.0 { t.text_height } else { 2.5 };
|
||||
(raw * anno_scale as f64) as f32
|
||||
}
|
||||
_ => return vec![],
|
||||
};
|
||||
if line_h <= 0.0 {
|
||||
return vec![];
|
||||
}
|
||||
let n_lines = n_lines.max(1);
|
||||
let cast = |p: [f64; 3]| -> [f32; 3] {
|
||||
[(p[0]) as f32, (p[1]) as f32, (p[2]) as f32]
|
||||
};
|
||||
let bl = cast(corners[0]);
|
||||
let br = cast(corners[1]);
|
||||
let full_tl = cast(corners[3]);
|
||||
|
||||
let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]);
|
||||
let ulen = (ux * ux + uy * uy + uz * uz).sqrt();
|
||||
if ulen < 1e-9 {
|
||||
return vec![bl, br];
|
||||
}
|
||||
let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen);
|
||||
|
||||
let mut pts = Vec::with_capacity(n_lines * 3);
|
||||
for i in 0..n_lines {
|
||||
// i = 0 is the topmost line — its bottom sits one line_h below
|
||||
// the OBB top (≈ `ulen`). For n_lines > ulen/line_h the deepest
|
||||
// baselines clamp to the OBB bottom.
|
||||
let bot_off = ((i + 1) as f32) * line_h;
|
||||
let along = (ulen - bot_off).max(0.0);
|
||||
let p0 = [bl[0] + nx * along, bl[1] + ny * along, bl[2] + nz * along];
|
||||
let p1 = [br[0] + nx * along, br[1] + ny * along, br[2] + nz * along];
|
||||
if !pts.is_empty() {
|
||||
pts.push([f32::NAN; 3]);
|
||||
}
|
||||
pts.extend_from_slice(&[p0, p1]);
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
||||
/// Filled tris for a greeked top-level Text / MText. One 2-triangle rect
|
||||
/// per visible line — `n_lines` is the actual rendered line count from
|
||||
/// `mtext_line_count` (1 for Text). Stacked top → bottom along the OBB's
|
||||
/// up direction. The face3d pipeline skips its 0.45 dim for wires with
|
||||
/// empty `points`, so these tris render at the literal text color.
|
||||
pub(crate) fn text_greek_obb_tris(
|
||||
e: &EntityType,
|
||||
anno_scale: f32,
|
||||
n_lines: usize,
|
||||
) -> Vec<[f32; 3]> {
|
||||
let Some(corners) = text_obb_corners_native(e, anno_scale, Some(n_lines)) else {
|
||||
return vec![];
|
||||
};
|
||||
let line_h = match e {
|
||||
EntityType::Text(t) => (t.height * anno_scale as f64) as f32,
|
||||
EntityType::MText(m) => (m.height * anno_scale as f64) as f32,
|
||||
EntityType::AttributeDefinition(a) => (a.height * anno_scale as f64) as f32,
|
||||
EntityType::AttributeEntity(a) => (a.height * anno_scale as f64) as f32,
|
||||
EntityType::Tolerance(t) => {
|
||||
let raw = if t.text_height > 0.0 { t.text_height } else { 2.5 };
|
||||
(raw * anno_scale as f64) as f32
|
||||
}
|
||||
_ => return vec![],
|
||||
};
|
||||
if line_h <= 0.0 {
|
||||
return vec![];
|
||||
}
|
||||
let n_lines = n_lines.max(1);
|
||||
let cast = |p: [f64; 3]| -> [f32; 3] {
|
||||
[(p[0]) as f32, (p[1]) as f32, (p[2]) as f32]
|
||||
};
|
||||
let bl = cast(corners[0]);
|
||||
let br = cast(corners[1]);
|
||||
let full_tl = cast(corners[3]);
|
||||
|
||||
let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]);
|
||||
let ulen = (ux * ux + uy * uy + uz * uz).sqrt();
|
||||
if ulen < 1e-9 {
|
||||
return vec![];
|
||||
}
|
||||
let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen);
|
||||
|
||||
let mut tris = Vec::with_capacity(n_lines * 6);
|
||||
for i in 0..n_lines {
|
||||
let top_along = (ulen - (i as f32) * line_h).max(0.0);
|
||||
let bot_along = (ulen - ((i + 1) as f32) * line_h).max(0.0);
|
||||
let tl = [
|
||||
bl[0] + nx * top_along,
|
||||
bl[1] + ny * top_along,
|
||||
bl[2] + nz * top_along,
|
||||
];
|
||||
let tr = [
|
||||
br[0] + nx * top_along,
|
||||
br[1] + ny * top_along,
|
||||
br[2] + nz * top_along,
|
||||
];
|
||||
let lbl = [
|
||||
bl[0] + nx * bot_along,
|
||||
bl[1] + ny * bot_along,
|
||||
bl[2] + nz * bot_along,
|
||||
];
|
||||
let lbr = [
|
||||
br[0] + nx * bot_along,
|
||||
br[1] + ny * bot_along,
|
||||
br[2] + nz * bot_along,
|
||||
];
|
||||
tris.extend_from_slice(&[lbl, lbr, tr, lbl, tr, tl]);
|
||||
}
|
||||
tris
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
Reference in new issue
Block a user