perf(scene): text LOD ladder 1/5 px with baseline-line tier
Replaces the 2/4 drop/greek/full ladder with 1/5 baseline/greek/full so zoomed-out text still leaves a visual hint instead of disappearing: h_px < 1 → OBB baseline line in the text color 1 ≤ h < 5 → greeked OBB rect h_px ≥ 5 → full per-glyph stroke Extra guard: in the baseline tier, the line itself is dropped when its projected length is under 2 px (single-char text seen edge-on would otherwise emit a sub-pixel segment). Text/MText is exempted from the generic 5 px AABB cull (top-level and block-defn paths) so it can reach this ladder even when its bounding box projects below the threshold. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5344d0d6d6
commit
0584e3c862
2 changed files with 145 additions and 13 deletions
|
|
@ -718,6 +718,83 @@ fn emit_greeked_text(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit a 2-point baseline line for a sub-pixel text LocalWire — same color
|
||||||
|
/// resolution as `emit_wire`, but the wire's stored glyph points are
|
||||||
|
/// replaced with the OBB's baseline edge (`obb[0] → obb[1]`). Falls back to
|
||||||
|
/// a no-op when no OBB is cached (rotation/attachment unknown).
|
||||||
|
fn emit_text_baseline(
|
||||||
|
lw: &LocalWire,
|
||||||
|
accum_xform: &Transform,
|
||||||
|
defn_lo: [f64; 3],
|
||||||
|
ctx: &ExpandCtx,
|
||||||
|
out: &mut Batches,
|
||||||
|
wpp: f32,
|
||||||
|
) {
|
||||||
|
let Some(obb) = lw.text_obb_local else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let [ox, oy, oz] = ctx.world_offset;
|
||||||
|
let [lo_x, lo_y, lo_z] = defn_lo;
|
||||||
|
let xf = |p: [f32; 3]| -> [f32; 3] {
|
||||||
|
let w = accum_xform.apply(Vector3::new(
|
||||||
|
p[0] as f64 + lo_x,
|
||||||
|
p[1] as f64 + lo_y,
|
||||||
|
p[2] as f64 + lo_z,
|
||||||
|
));
|
||||||
|
[(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32]
|
||||||
|
};
|
||||||
|
let p0 = xf(obb[0]);
|
||||||
|
let p1 = xf(obb[1]);
|
||||||
|
|
||||||
|
// Skip the baseline too if the line itself projects under 2 px on screen.
|
||||||
|
let dx = p1[0] - p0[0];
|
||||||
|
let dy = p1[1] - p0[1];
|
||||||
|
let len_px = (dx * dx + dy * dy).sqrt() / wpp;
|
||||||
|
if len_px < 2.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let final_color = if ctx.selected {
|
||||||
|
WireModel::SELECTED
|
||||||
|
} else if lw.color_is_byblock {
|
||||||
|
ctx.ins_color
|
||||||
|
} else {
|
||||||
|
lw.color
|
||||||
|
};
|
||||||
|
let final_color = if ctx.is_xref && !ctx.selected {
|
||||||
|
fade_toward_bg(final_color, ctx.bg_color)
|
||||||
|
} else {
|
||||||
|
final_color
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = style_key(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true);
|
||||||
|
let entry = out
|
||||||
|
.by_style
|
||||||
|
.entry(key)
|
||||||
|
.or_insert_with(|| BatchEntry::new(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true));
|
||||||
|
|
||||||
|
let needs_sep = !entry.points.is_empty()
|
||||||
|
&& !entry.points.last().map(|p| p[0].is_nan()).unwrap_or(false);
|
||||||
|
if needs_sep {
|
||||||
|
entry.points.push([f32::NAN; 3]);
|
||||||
|
}
|
||||||
|
for q in [p0, p1] {
|
||||||
|
if q[0] < entry.min_x {
|
||||||
|
entry.min_x = q[0];
|
||||||
|
}
|
||||||
|
if q[1] < entry.min_y {
|
||||||
|
entry.min_y = q[1];
|
||||||
|
}
|
||||||
|
if q[0] > entry.max_x {
|
||||||
|
entry.max_x = q[0];
|
||||||
|
}
|
||||||
|
if q[1] > entry.max_y {
|
||||||
|
entry.max_y = q[1];
|
||||||
|
}
|
||||||
|
entry.points.push(q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn aabb_pixel_size(local_aabb: [f32; 4], world_per_pixel: f32) -> f32 {
|
fn aabb_pixel_size(local_aabb: [f32; 4], world_per_pixel: f32) -> f32 {
|
||||||
let w = (local_aabb[2] - local_aabb[0]).abs();
|
let w = (local_aabb[2] - local_aabb[0]).abs();
|
||||||
let h = (local_aabb[3] - local_aabb[1]).abs();
|
let h = (local_aabb[3] - local_aabb[1]).abs();
|
||||||
|
|
@ -918,13 +995,17 @@ fn expand_defn(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(wpp) = ctx.world_per_pixel {
|
if let Some(wpp) = ctx.world_per_pixel {
|
||||||
if aabb_pixel_size(local, wpp) < MIN_PIXEL_SIZE {
|
// Text/MText follows its own ladder below (baseline-line
|
||||||
|
// / greek / full) — must reach it even when the wire's
|
||||||
|
// AABB falls under MIN_PIXEL_SIZE.
|
||||||
|
let is_text = lw.text_height_local.is_some();
|
||||||
|
if !is_text && aabb_pixel_size(local, wpp) < MIN_PIXEL_SIZE {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Text LOD ladder: text inside a block follows the same
|
// Text LOD ladder: text inside a block follows the same
|
||||||
// 5 / 10 px cull/greek/full rules as top-level text. We
|
// 1 / 5 px baseline/greek/full rules as top-level text.
|
||||||
// have to apply the Insert's transform scale to the
|
// We apply the Insert's transform scale to the stored
|
||||||
// stored local glyph height to get the screen height.
|
// local glyph height to get the screen height.
|
||||||
if let Some(h_local) = lw.text_height_local {
|
if let Some(h_local) = lw.text_height_local {
|
||||||
let m = &accum_xform.matrix.m;
|
let m = &accum_xform.matrix.m;
|
||||||
let sy = ((m[1][0] * m[1][0]
|
let sy = ((m[1][0] * m[1][0]
|
||||||
|
|
@ -933,10 +1014,11 @@ fn expand_defn(
|
||||||
.sqrt() as f32;
|
.sqrt() as f32;
|
||||||
let h_world = h_local * sy;
|
let h_world = h_local * sy;
|
||||||
let h_px = h_world / wpp;
|
let h_px = h_world / wpp;
|
||||||
if h_px < 2.0 {
|
if h_px < 1.0 {
|
||||||
|
emit_text_baseline(lw, accum_xform, defn_lo, ctx, out, wpp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if h_px < 4.0 {
|
if h_px < 5.0 {
|
||||||
emit_greeked_text(lw, local, accum_xform, defn_lo, ctx, out);
|
emit_greeked_text(lw, local, accum_xform, defn_lo, ctx, out);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3913,7 +3913,11 @@ fn tessellate_entity(
|
||||||
let w = (ab[2] - ab[0]).abs();
|
let w = (ab[2] - ab[0]).abs();
|
||||||
let h = (ab[3] - ab[1]).abs();
|
let h = (ab[3] - ab[1]).abs();
|
||||||
// Keep in sync with `block_cache::MIN_PIXEL_SIZE`.
|
// Keep in sync with `block_cache::MIN_PIXEL_SIZE`.
|
||||||
if w.max(h) / wpp < 5.0 {
|
// Text/MText have their own LOD ladder below
|
||||||
|
// (baseline-line / greek / full) and must reach it
|
||||||
|
// even when projected size is sub-5 px.
|
||||||
|
let is_text = matches!(e, EntityType::Text(_) | EntityType::MText(_));
|
||||||
|
if !is_text && w.max(h) / wpp < 5.0 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4131,9 +4135,9 @@ fn tessellate_entity(
|
||||||
|
|
||||||
// Text-specific LOD ladder, keyed off the entity's glyph height in
|
// Text-specific LOD ladder, keyed off the entity's glyph height in
|
||||||
// pixels (anno-scaled):
|
// pixels (anno-scaled):
|
||||||
// < 2 px → drop entirely (illegible, no point even greeking)
|
// < 1 px → baseline line in the text's color (text-here hint)
|
||||||
// 2–4 px → emit a greeked rect in the text's color
|
// 1–5 px → greeked OBB rect in the text's color
|
||||||
// ≥ 4 px → full per-glyph stroke tessellation
|
// ≥ 5 px → full per-glyph stroke tessellation
|
||||||
if let Some(wpp) = world_per_pixel {
|
if let Some(wpp) = world_per_pixel {
|
||||||
let text_height: Option<f64> = match e {
|
let text_height: Option<f64> = match e {
|
||||||
EntityType::Text(t) => Some(t.height * anno_scale as f64),
|
EntityType::Text(t) => Some(t.height * anno_scale as f64),
|
||||||
|
|
@ -4142,10 +4146,38 @@ fn tessellate_entity(
|
||||||
};
|
};
|
||||||
if let Some(h_world) = text_height {
|
if let Some(h_world) = text_height {
|
||||||
let h_px = (h_world as f32) / wpp;
|
let h_px = (h_world as f32) / wpp;
|
||||||
if h_px < 2.0 {
|
if h_px < 1.0 {
|
||||||
return vec![];
|
let pts = text_baseline_points(e, anno_scale, world_offset);
|
||||||
|
if pts.len() < 2 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
// Skip the baseline too if the line itself projects under
|
||||||
|
// 2 px (e.g. a 1-char text seen edge-on).
|
||||||
|
let dx = pts[1][0] - pts[0][0];
|
||||||
|
let dy = pts[1][1] - pts[0][1];
|
||||||
|
let len_px = (dx * dx + dy * dy).sqrt() / wpp;
|
||||||
|
if len_px < 2.0 {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
return vec![WireModel {
|
||||||
|
name: h.value().to_string(),
|
||||||
|
points: pts,
|
||||||
|
color: entity_color,
|
||||||
|
selected: sel,
|
||||||
|
aci,
|
||||||
|
pattern_length: 0.0,
|
||||||
|
pattern: [0.0; 8],
|
||||||
|
line_weight_px: 1.0,
|
||||||
|
snap_pts: vec![],
|
||||||
|
tangent_geoms: vec![],
|
||||||
|
key_vertices: vec![],
|
||||||
|
aabb,
|
||||||
|
plinegen: true,
|
||||||
|
vp_scissor: None,
|
||||||
|
fill_tris: vec![],
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
if h_px < 4.0 && aabb != WireModel::UNBOUNDED_AABB {
|
if h_px < 5.0 && aabb != WireModel::UNBOUNDED_AABB {
|
||||||
let fill_tris = text_greek_obb_tris(e, anno_scale, world_offset);
|
let fill_tris = text_greek_obb_tris(e, anno_scale, world_offset);
|
||||||
// The face3d pipeline dims fill_tris colors to 45% to fake
|
// The face3d pipeline dims fill_tris colors to 45% to fake
|
||||||
// ambient occlusion on PolyfaceMesh / 3DFACE solids. Greeked
|
// ambient occlusion on PolyfaceMesh / 3DFACE solids. Greeked
|
||||||
|
|
@ -4335,6 +4367,24 @@ pub(crate) fn text_obb_corners_native(
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Baseline endpoints (`p00 → p10` of the OBB) for a sub-pixel Text / MText,
|
||||||
|
/// in world-offset-subtracted f32 space. Used as the lowest LOD step before
|
||||||
|
/// the entity is dropped entirely.
|
||||||
|
fn text_baseline_points(
|
||||||
|
e: &EntityType,
|
||||||
|
anno_scale: f32,
|
||||||
|
world_offset: [f64; 3],
|
||||||
|
) -> Vec<[f32; 3]> {
|
||||||
|
let Some(corners) = text_obb_corners_native(e, anno_scale) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let [ox, oy, oz] = world_offset;
|
||||||
|
let cast = |p: [f64; 3]| -> [f32; 3] {
|
||||||
|
[(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32]
|
||||||
|
};
|
||||||
|
vec![cast(corners[0]), cast(corners[1])]
|
||||||
|
}
|
||||||
|
|
||||||
/// 6-vertex (2-triangle) fill for a greeked top-level Text / MText, in
|
/// 6-vertex (2-triangle) fill for a greeked top-level Text / MText, in
|
||||||
/// world-offset-subtracted f32 space.
|
/// world-offset-subtracted f32 space.
|
||||||
fn text_greek_obb_tris(
|
fn text_greek_obb_tris(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue