fix(text): bake draw-order depth into glyphs so fills can't cover text

Fill triangles (DIMTFILL backgrounds, MLEADER masks, hatches) carry the
entity's draw-order rank, but SDF glyph quads were pushed at neutral 0.0
— any entity in the front half of the draw order had its own background
fill painted over its text. gather_text_verts now adds the host wire's
wire_draw_depth to each glyph: a text background lands at the same
biased depth (glyphs draw after fills and win the LessEqual test), and
text respects draw order against other entities' fills too.

The #403 glyph-reuse fast path gains a rank-stability gate: adding or
removing an entity shifts every sibling's bias (the rank denominator
changes), so reuse is only safe across all-Modified edits.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-23 01:13:33 +03:00
1 parent f407835543
commit 94609fc54a
2 files changed
+43 -4

No files matched your search

+11
View File
@@ -1416,6 +1416,17 @@ impl Scene {
/// references (their baked text moves with the instance) — an edit to any of
/// those, or any removal, invalidates it; a plain line / arc / polyline edit
/// does not.
/// Whether the per-entity draw-order ranks are unchanged since
/// `last_epoch`: every delta is a Modify (an Add / Remove changes the rank
/// denominator and shifts every sibling's bias — same rule as the
/// `draw_depth_map` cache).
pub(super) fn draw_ranks_stable(&self, last_epoch: u64) -> bool {
match self.replay_since(last_epoch) {
Some(deltas) => deltas.iter().all(|&(_, k)| k == ChangeKind::Modified),
None => false,
}
}
fn text_unchanged(&self, last_epoch: u64) -> bool {
match self.replay_since(last_epoch) {
Some(deltas) => deltas.iter().all(|&(h, k)| {
+32 -4
View File
@@ -998,6 +998,7 @@ impl Scene {
wires: &[WireModel],
wire_content_id: u64,
source_key: u64,
depth_map: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> std::sync::Arc<Vec<crate::scene::pipeline::text_gpu::TextVertex>> {
use std::sync::Arc;
{
@@ -1010,12 +1011,19 @@ impl Scene {
// text-bearing entity changed since the last build, the glyphs are
// identical, so reuse them instead of re-walking every wire. Reuse is
// per `source_key`: another source's glyphs are a different wire set,
// not an older build of this one (#403).
// not an older build of this one (#403). The glyphs also bake the
// entity draw-order depth (below), which shifts whenever an entity is
// added or removed — reuse only across rank-stable (all-Modified)
// edits.
{
let reuse = {
let last = self.last_sdf_text.borrow();
match last.get(&source_key) {
Some((epoch, arc)) if self.text_unchanged(*epoch) => Some(arc.clone()),
Some((epoch, arc))
if self.text_unchanged(*epoch) && self.draw_ranks_stable(*epoch) =>
{
Some(arc.clone())
}
_ => None,
}
};
@@ -1032,7 +1040,22 @@ impl Scene {
let mut out: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
for w in wires {
if !w.text_verts.is_empty() {
out.extend_from_slice(&w.text_verts);
// Bake the host wire's draw-order depth into its glyphs so
// text layers like the rest of the entity: its own background
// fill lands at the same biased depth (glyphs draw after fills
// and win the LessEqual test), and a hatch later in draw order
// still covers the text. A tessellation-time value (block-local
// compose) survives as an offset on top of the wire's depth.
let d = crate::scene::pipeline::wire_gpu::wire_draw_depth(w, depth_map);
if d == 0.0 {
out.extend_from_slice(&w.text_verts);
} else {
out.extend(w.text_verts.iter().map(|tv| {
let mut tv = *tv;
tv.draw_depth += d;
tv
}));
}
}
}
let verts = Arc::new(out);
@@ -1370,7 +1393,12 @@ impl Scene {
} else {
0x3000_0000_0000_0000 | inst.handle.value()
};
let text_verts = self.gather_text_verts(&all_wires, wire_content_id, text_source_key);
let text_verts = self.gather_text_verts(
&all_wires,
wire_content_id,
text_source_key,
&self.draw_depth_map(),
);
// Grip-drag / command-preview glyphs, excluded from the epoch-cached base
// gather above. Two sources, both tiny (one operation's worth) and walked
// per frame: the overlay wires' own glyphs (MOVE / COPY / ROTATE / SCALE /