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>
This commit is contained in:
parent
5bdfe483da
commit
5018963d3d
6 changed files with 257 additions and 72 deletions
|
|
@ -42,6 +42,25 @@ pub fn text_atlas() -> &'static Mutex<GlyphAtlas> {
|
|||
ATLAS.get_or_init(|| Mutex::new(GlyphAtlas::new(2048, 2048)))
|
||||
}
|
||||
|
||||
/// Bumped whenever already-baked entries' UVs stop addressing their tile:
|
||||
/// `grow_height` rescales every entry's V and `reset` rewinds the packer, but
|
||||
/// glyph quads built earlier keep the UVs they were laid out with. Those stale
|
||||
/// quads sample the wrong tile on screen and miss the export table entirely
|
||||
/// (silently dropping text from the PDF — issue #385 all over again, under
|
||||
/// issue #347's grow-the-atlas conditions). The tessellation memo guard folds
|
||||
/// this in, so a growth re-lays-out the text instead of leaving stale quads.
|
||||
static ATLAS_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// Current atlas UV generation — see [`ATLAS_GENERATION`]. Anything that caches
|
||||
/// glyph quads must treat a change here as "re-lay-out the text".
|
||||
pub fn generation() -> u64 {
|
||||
ATLAS_GENERATION.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn bump_generation() {
|
||||
ATLAS_GENERATION.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// TEXTFILL system variable — `true` fills TrueType glyphs (default), `false`
|
||||
/// draws them hollow (outline only). Global, like the AutoCAD system variable.
|
||||
static TEXTFILL: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
|
||||
|
|
@ -127,6 +146,8 @@ pub struct GlyphExport {
|
|||
/// filled TrueType glyph — the exporter fills these and ignores `strokes`;
|
||||
/// empty means "stroke `strokes`" (LFF pen font, or a hollow TTF glyph).
|
||||
pub fill_tris: Vec<[f32; 2]>,
|
||||
/// Baked with the 1.7× bold pen; the exporter widens its own pen to match.
|
||||
pub bold: bool,
|
||||
}
|
||||
|
||||
/// Pack a glyph quad's `uv_min` corner into a stable hash key for the export
|
||||
|
|
@ -229,6 +250,8 @@ impl GlyphAtlas {
|
|||
/// next use (e.g. after TEXTFILL toggles between fill and outline).
|
||||
pub fn reset(&mut self) {
|
||||
*self = GlyphAtlas::new(self.width, self.height);
|
||||
// The packer rewound, so every previously handed-out UV is now stale.
|
||||
bump_generation();
|
||||
}
|
||||
|
||||
/// Look up a glyph's atlas placement, baking + packing it on first use.
|
||||
|
|
@ -260,23 +283,39 @@ impl GlyphAtlas {
|
|||
/// glyph quads, look each quad's `uv_min` up here, and re-emit the glyph as
|
||||
/// vector strokes / fills instead of sampling the SDF texture. Re-resolves
|
||||
/// each entry's outline through `Face` (the same source `get_or_insert`
|
||||
/// baked from), so it reflects the current TEXTFILL mode.
|
||||
/// baked from), gated on TEXTFILL so a hollow-on-screen glyph exports hollow.
|
||||
pub fn export_table(&self) -> HashMap<u64, GlyphExport> {
|
||||
let fill_on = textfill();
|
||||
let mut out = HashMap::with_capacity(self.entries.len());
|
||||
// `Face::resolve` reparses a font, so resolve once per family rather than
|
||||
// once per baked glyph — a CJK drawing holds thousands of entries over a
|
||||
// handful of families, and the atlas mutex is held for this whole walk.
|
||||
let mut faces: HashMap<&str, Face> = HashMap::new();
|
||||
for ((family, ch, bold), entry) in &self.entries {
|
||||
let Some(entry) = entry else { continue };
|
||||
let face = Face::resolve(family);
|
||||
let face = faces
|
||||
.entry(family.as_str())
|
||||
.or_insert_with(|| Face::resolve(family));
|
||||
let Some(g) = face.glyph(*ch) else { continue };
|
||||
// Bold LFF glyphs bake with a wider pen but the same centrelines;
|
||||
// the exporter widens the pen itself, so the geometry is shared.
|
||||
let _ = bold;
|
||||
out.insert(
|
||||
uv_key(entry.uv_min),
|
||||
GlyphExport {
|
||||
plane_min: entry.plane_min,
|
||||
plane_max: entry.plane_max,
|
||||
strokes: g.strokes.clone(),
|
||||
fill_tris: g.fill_tris.clone(),
|
||||
// `bake_glyph` gates the texture fill on TEXTFILL the same way
|
||||
// (`textfill() && !g.fill_tris.is_empty()`); `Face::glyph` does
|
||||
// not, so without this the export ignores TEXTFILL 0 and prints
|
||||
// solid glyphs where the screen shows outlines.
|
||||
fill_tris: if fill_on {
|
||||
g.fill_tris.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
// Bold stroke glyphs bake at a 1.7× pen (`get_or_insert`) but
|
||||
// share the centrelines, so carry the flag and let the exporter
|
||||
// widen its own pen by the same factor.
|
||||
bold: *bold,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -304,6 +343,8 @@ impl GlyphAtlas {
|
|||
self.solid_uv[1] *= sy;
|
||||
self.height = new_h;
|
||||
self.dirty = true;
|
||||
// Every already-emitted quad's V now addresses the wrong tile.
|
||||
bump_generation();
|
||||
true
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue