cad-editor/tests/pdf_export_text_check.rs
Hakan Seven 5018963d3d fix(export): project viewport text into paper space
Review follow-ups to #390, which fixed PDF/print text for model space but
left the layout-plot path — the one issue #385 was filed from — broken.

viewport_content_wires rewrote points/snap_pts/key_vertices/aabb and then
cloned text_verts through untouched, so paper-space viewport text stayed at
model (UTM) coordinates: a dimension at model (25000, 12000) plotted its
lines onto the sheet and its glyphs kilometres off the page. A text-only
wire fared worse — TEXT/MTEXT carry no stroke points since text went
SDF-only, so it was dropped outright by the empty-clip and AABB rejections.
Project the glyphs through the same proj_abs the snap points use (via the
existing map_text_verts helper), cull per glyph on the quad centroid, count
them towards the paper AABB, and dim them like the wire colour.

Also in emit_text, which diverged from the wire pass it mirrors:
- divide the pen by `scale`, or a Fit/windowed plot prints text as
  near-invisible hairlines while its own lines stay correct;
- apply the CTB plot style, or a monochrome.ctb plot renders the lines
  black and leaves the text on its screen colour;
- reset the dash pattern, which is persistent PDF state the wire pass
  leaves set — a HIDDEN-linetype last wire printed glyph outlines dashed.

export_table's doc claimed it honoured TEXTFILL and widened the bold pen
itself; it did neither. Gate fill_tris on textfill() so hollow-on-screen
text exports hollow, carry the bold flag and widen the pen by the 1.7x the
bake uses, and resolve Face once per family instead of once per glyph (the
atlas mutex is held for that whole walk).

uv_key identifies a glyph by its tile's uv_min, but grow_height rescales
every entry's V and reset rewinds the packer, leaving already-built quads
addressing the wrong tile — garbage on screen and a silent miss in the
export table, i.e. #385 again under #347's grow-the-atlas conditions. Bump
an atlas generation on both and fold it into the tessellation memo guard so
the text re-lays-out instead. Quads built earlier in the same pass that
grows are still stale for that pass; the guard heals them on the next one.

Tests: the integration test's "some wire has text_verts" assert passed on
pre-fix code too (the exporter ignored them; the scene always carried
them), and its dimension half was satisfied by the TEXT alone — assert per
entity handle instead, and stop colliding on fixed names in the shared temp
root. The unit test's bbox came from the same quads the mapping reads, so
it held by construction; pin the run's own world box instead. That still
cannot catch a mirrored corner assignment — verified by mirroring it — so
say so rather than claim coverage the assert doesn't have.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:06:15 +03:00

111 lines
3.6 KiB
Rust

// Regression for #385: text and dimension text must reach the PDF / print
// export. From v0.8.2 text renders on-screen only as GPU SDF glyph quads
// (`WireModel::text_verts`); the CPU PDF exporter used to draw only wire
// stroke `points`, so all text — standalone TEXT/MTEXT and dimension text —
// vanished from exported PDFs (present in v0.7.6).
//
// The op-level placement checks live in `pdf_export`'s own unit tests, which can
// see `emit_text`'s ops. What only an integration test can cover is that the
// real entity -> scene.entity_wires() -> export_pdf path carries text at all,
// per entity kind — so that is what this asserts.
use acadrust::entities::{Dimension, DimensionLinear, Text};
use acadrust::types::Vector3;
use acadrust::EntityType;
use OpenCADStudio::io::pdf_export::export_pdf;
use OpenCADStudio::scene::Scene;
#[test]
fn text_and_dim_reach_pdf_export() {
let mut scene = Scene::new();
let t = Text::with_value("HELLO", Vector3::new(10.0, 10.0, 0.0)).with_height(5.0);
let text_h = scene.add_entity(EntityType::Text(t));
// A linear dimension — its measurement value is synthesized as SDF text too,
// and dimension text is what #385 was actually filed about.
let dim = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(40.0, 0.0, 0.0));
let dim_h = scene.add_entity(EntityType::Dimension(Dimension::Linear(dim)));
let wires = scene.entity_wires();
// Assert per entity, not "some wire somewhere has text": both asserts below
// would otherwise be satisfied by the TEXT alone, leaving a dimension-text
// regression — the actual reported bug — green.
// Tessellation names each wire after its entity handle (see the
// `w.name.parse::<u64>()` lookup in the render pipeline).
let has_text = |h: acadrust::types::Handle| {
let want = h.value().to_string();
wires
.iter()
.any(|w| w.name == want && !w.text_verts.is_empty())
};
assert!(
has_text(text_h),
"TEXT carries no glyph quads to the exporter"
);
assert!(
has_text(dim_h),
"DIMENSION carries no glyph quads to the exporter — dim text would be \
missing from the PDF (#385)"
);
// End-to-end: the same wire set with and without text. A private temp dir
// keeps concurrent runs (and other users on a shared build host) from
// colliding on a fixed name in the shared temp root.
let dir = std::env::temp_dir().join(format!("ocs385-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("temp dir");
let p_text = dir.join("with_text.pdf");
export_pdf(
&wires,
&[],
&[],
210.0,
297.0,
0.0,
0.0,
0,
1.0,
None,
&p_text,
None,
)
.expect("export with text");
let with_text = std::fs::read(&p_text).expect("read pdf");
assert!(with_text.starts_with(b"%PDF"), "not a PDF");
let stripped: Vec<_> = wires
.iter()
.cloned()
.map(|mut w| {
w.text_verts.clear();
w
})
.collect();
let p_bare = dir.join("no_text.pdf");
export_pdf(
&stripped,
&[],
&[],
210.0,
297.0,
0.0,
0.0,
0,
1.0,
None,
&p_bare,
None,
)
.expect("export without text");
let no_text = std::fs::read(&p_bare).expect("read pdf");
assert!(
with_text.len() > no_text.len(),
"text added no content to the PDF: {} !> {}",
with_text.len(),
no_text.len()
);
let _ = std::fs::remove_dir_all(&dir);
}