feat(lod): preserve 3D position for sub-pixel entity stubs
Yakındayken tessellate edilen 3D entity'ler (Face3D, Solid3D, Mesh, PolyfaceMesh, PolygonMesh, Body, Region) <5 px eşiğine düştüğünde, eski tek-çizgili stub'ın z=0 düzlemine indirilmesi yüzünden farklı bir konumda görünüyordu. Yeni lod_stub_wire_3d entity'nin 3D AABB kübünü (8 köşe, 12 kenar) çiziyor: - XY'de entity_aabb local frame'i (world_offset shift edilmiş) - Z aralığı bbox.min/max.z, world_offset[2] subtract edilmiş 2D entity stub'ı (lod_stub_wire) text fallback'leri ve diğer 2D geometriler için z=0'da kalıyor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
3a64f98755
commit
26bcd89358
1 changed files with 116 additions and 9 deletions
125
src/scene/mod.rs
125
src/scene/mod.rs
|
|
@ -4694,21 +4694,58 @@ fn tessellate_entity(
|
||||||
// (baseline-line / greek / full) and must reach it
|
// (baseline-line / greek / full) and must reach it
|
||||||
// even when projected size is sub-5 px.
|
// even when projected size is sub-5 px.
|
||||||
let is_text = matches!(e, EntityType::Text(_) | EntityType::MText(_));
|
let is_text = matches!(e, EntityType::Text(_) | EntityType::MText(_));
|
||||||
|
let is_3d_entity = matches!(
|
||||||
|
e,
|
||||||
|
EntityType::Face3D(_)
|
||||||
|
| EntityType::Solid3D(_)
|
||||||
|
| EntityType::Mesh(_)
|
||||||
|
| EntityType::PolyfaceMesh(_)
|
||||||
|
| EntityType::PolygonMesh(_)
|
||||||
|
| EntityType::Body(_)
|
||||||
|
| EntityType::Region(_)
|
||||||
|
);
|
||||||
if !is_text && w_px.max(h_px) / wpp < 5.0 {
|
if !is_text && w_px.max(h_px) / wpp < 5.0 {
|
||||||
// Sub-pixel entity: emit a stub instead of
|
// Sub-pixel entity: emit a stub instead of
|
||||||
// nothing. Stays visible as a 1-pixel speck,
|
// nothing so it stays visible / selectable /
|
||||||
// tracks its `selected` highlight across zoom
|
// hit-test'able at any zoom. 2-D entities
|
||||||
// levels, and remains hit-test'able via the
|
// get the cheap diagonal segment; 3-D
|
||||||
// AABB. See #19.
|
// entities get an AABB cube so their
|
||||||
|
// footprint doesn't drift when the camera
|
||||||
|
// crosses the LOD threshold. See #19.
|
||||||
let (entity_color, _, _, _, aci_idx) =
|
let (entity_color, _, _, _, aci_idx) =
|
||||||
render::render_style_for(document, e);
|
render::render_style_for(document, e);
|
||||||
let entity_color = render::adapt_to_bg(entity_color, bg_color);
|
let entity_color = render::adapt_to_bg(entity_color, bg_color);
|
||||||
|
if is_3d_entity {
|
||||||
|
// `ab` is already in the local frame
|
||||||
|
// (entity_aabb subtracted world_offset
|
||||||
|
// XY). The bbox z fields are still in
|
||||||
|
// WCS, so subtract `world_offset[2]` to
|
||||||
|
// match — otherwise the stub sits at a
|
||||||
|
// different z than the full tessellation
|
||||||
|
// and the geometry visibly shifts when
|
||||||
|
// the camera crosses the LOD threshold.
|
||||||
|
let bbox = e.as_entity().bounding_box();
|
||||||
|
let oz = world_offset[2];
|
||||||
|
let z_min = (bbox.min.z - oz) as f32;
|
||||||
|
let z_max = (bbox.max.z - oz) as f32;
|
||||||
|
return vec![lod_stub_wire_3d(
|
||||||
|
h.value().to_string(),
|
||||||
|
entity_color,
|
||||||
|
sel,
|
||||||
|
aci_idx,
|
||||||
|
ab,
|
||||||
|
z_min,
|
||||||
|
z_max,
|
||||||
|
)];
|
||||||
|
}
|
||||||
return vec![lod_stub_wire(
|
return vec![lod_stub_wire(
|
||||||
h.value().to_string(),
|
h.value().to_string(),
|
||||||
entity_color,
|
entity_color,
|
||||||
sel,
|
sel,
|
||||||
aci_idx,
|
aci_idx,
|
||||||
ab,
|
ab,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5122,13 +5159,16 @@ fn tessellate_entity(
|
||||||
if len_px < 2.0 {
|
if len_px < 2.0 {
|
||||||
// Text projects to under 2 px — fall back to the
|
// Text projects to under 2 px — fall back to the
|
||||||
// generic LOD stub so the entity stays visible /
|
// generic LOD stub so the entity stays visible /
|
||||||
// selectable. #19.
|
// selectable. #19. Text is 2-D in the XY plane so
|
||||||
|
// z_min = z_max = 0 keeps the historical behaviour.
|
||||||
return vec![lod_stub_wire(
|
return vec![lod_stub_wire(
|
||||||
h.value().to_string(),
|
h.value().to_string(),
|
||||||
entity_color,
|
entity_color,
|
||||||
sel,
|
sel,
|
||||||
aci,
|
aci,
|
||||||
aabb,
|
aabb,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
return vec![WireModel {
|
return vec![WireModel {
|
||||||
|
|
@ -5152,12 +5192,15 @@ fn tessellate_entity(
|
||||||
if h_px < 5.0 && aabb != WireModel::UNBOUNDED_AABB {
|
if h_px < 5.0 && aabb != WireModel::UNBOUNDED_AABB {
|
||||||
let fill_tris = crate::entities::text_support::text_greek_obb_tris(e, anno_scale, world_offset, n_lines);
|
let fill_tris = crate::entities::text_support::text_greek_obb_tris(e, anno_scale, world_offset, n_lines);
|
||||||
if fill_tris.is_empty() {
|
if fill_tris.is_empty() {
|
||||||
|
// Text greek fallback: also 2-D, keep stub at z=0.
|
||||||
return vec![lod_stub_wire(
|
return vec![lod_stub_wire(
|
||||||
h.value().to_string(),
|
h.value().to_string(),
|
||||||
entity_color,
|
entity_color,
|
||||||
sel,
|
sel,
|
||||||
aci,
|
aci,
|
||||||
aabb,
|
aabb,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
// Greek text renders via the face3d fill batch, which colours
|
// Greek text renders via the face3d fill batch, which colours
|
||||||
|
|
@ -5253,10 +5296,13 @@ fn lod_stub_wire(
|
||||||
selected: bool,
|
selected: bool,
|
||||||
aci: u8,
|
aci: u8,
|
||||||
aabb: [f32; 4],
|
aabb: [f32; 4],
|
||||||
|
z_min: f32,
|
||||||
|
z_max: f32,
|
||||||
) -> WireModel {
|
) -> WireModel {
|
||||||
let [ax, ay, bx, by] = aabb;
|
let [ax, ay, bx, by] = aabb;
|
||||||
let cx = (ax + bx) * 0.5;
|
let cx = (ax + bx) * 0.5;
|
||||||
let cy = (ay + by) * 0.5;
|
let cy = (ay + by) * 0.5;
|
||||||
|
let cz = (z_min + z_max) * 0.5;
|
||||||
// Mirror what tessellate.rs does for the non-stub paths: bake the
|
// Mirror what tessellate.rs does for the non-stub paths: bake the
|
||||||
// selection-highlight colour into the wire so a re-tessellate triggered
|
// selection-highlight colour into the wire so a re-tessellate triggered
|
||||||
// by a zoom-induced LOD change keeps the entity highlighted. Without
|
// by a zoom-induced LOD change keeps the entity highlighted. Without
|
||||||
|
|
@ -5266,9 +5312,12 @@ fn lod_stub_wire(
|
||||||
let stored_color = if selected { WireModel::SELECTED } else { color };
|
let stored_color = if selected { WireModel::SELECTED } else { color };
|
||||||
WireModel {
|
WireModel {
|
||||||
name,
|
name,
|
||||||
// Diagonal: projects to 1-5 px at the LOD threshold so the entity
|
// Diagonal of the entity's 3D AABB so depth tests against
|
||||||
// shows as a tiny mark.
|
// shaded / hidden-line geometry are correct — the stub doesn't
|
||||||
points: vec![[ax, ay, 0.0], [bx, by, 0.0]],
|
// flatten to z=0 and pop in front of objects that sit at a
|
||||||
|
// different elevation. 2D entities (text fallbacks) pass
|
||||||
|
// z_min = z_max = 0 to keep the historical behaviour.
|
||||||
|
points: vec![[ax, ay, z_min], [bx, by, z_max]],
|
||||||
color: stored_color,
|
color: stored_color,
|
||||||
selected,
|
selected,
|
||||||
aci,
|
aci,
|
||||||
|
|
@ -5277,7 +5326,65 @@ fn lod_stub_wire(
|
||||||
line_weight_px: 1.0,
|
line_weight_px: 1.0,
|
||||||
snap_pts: vec![],
|
snap_pts: vec![],
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![],
|
||||||
key_vertices: vec![[cx, cy, 0.0]],
|
key_vertices: vec![[cx, cy, cz]],
|
||||||
|
aabb,
|
||||||
|
plinegen: true,
|
||||||
|
vp_scissor: None,
|
||||||
|
fill_tris: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sub-pixel LOD stub for 3D entities. Emits the entity's 3D AABB as a
|
||||||
|
/// 12-edge cube so the geometry occupies the same screen footprint and
|
||||||
|
/// depth range as the full tessellation, just with a tiny constant cost
|
||||||
|
/// (12 line segments). Without this, the diagonal stub used by
|
||||||
|
/// `lod_stub_wire` cuts off at two opposite bbox corners and drifts
|
||||||
|
/// visibly when the camera crosses the LOD threshold.
|
||||||
|
fn lod_stub_wire_3d(
|
||||||
|
name: String,
|
||||||
|
color: [f32; 4],
|
||||||
|
selected: bool,
|
||||||
|
aci: u8,
|
||||||
|
aabb: [f32; 4],
|
||||||
|
z_min: f32,
|
||||||
|
z_max: f32,
|
||||||
|
) -> WireModel {
|
||||||
|
let [x0, y0, x1, y1] = aabb;
|
||||||
|
let (z0, z1) = if z_min <= z_max { (z_min, z_max) } else { (z_max, z_min) };
|
||||||
|
let p = [
|
||||||
|
[x0, y0, z0], [x1, y0, z0], [x1, y1, z0], [x0, y1, z0],
|
||||||
|
[x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1],
|
||||||
|
];
|
||||||
|
// 12 edges = 4 bottom-face + 4 top-face + 4 vertical connectors.
|
||||||
|
const EDGES: [(usize, usize); 12] = [
|
||||||
|
(0, 1), (1, 2), (2, 3), (3, 0),
|
||||||
|
(4, 5), (5, 6), (6, 7), (7, 4),
|
||||||
|
(0, 4), (1, 5), (2, 6), (3, 7),
|
||||||
|
];
|
||||||
|
let mut points: Vec<[f32; 3]> = Vec::with_capacity(EDGES.len() * 3);
|
||||||
|
for (a, b) in EDGES {
|
||||||
|
if !points.is_empty() {
|
||||||
|
points.push([f32::NAN; 3]);
|
||||||
|
}
|
||||||
|
points.push(p[a]);
|
||||||
|
points.push(p[b]);
|
||||||
|
}
|
||||||
|
let stored_color = if selected { WireModel::SELECTED } else { color };
|
||||||
|
WireModel {
|
||||||
|
name,
|
||||||
|
points,
|
||||||
|
color: stored_color,
|
||||||
|
selected,
|
||||||
|
aci,
|
||||||
|
pattern_length: 0.0,
|
||||||
|
pattern: [0.0; 8],
|
||||||
|
line_weight_px: 1.0,
|
||||||
|
snap_pts: vec![],
|
||||||
|
tangent_geoms: vec![],
|
||||||
|
// No `key_vertices` — Face3DGpu requires 4 corners to emit a
|
||||||
|
// fill quad, and we don't want this stub painted as a solid
|
||||||
|
// face. The wire pass still draws its 12 edges.
|
||||||
|
key_vertices: vec![],
|
||||||
aabb,
|
aabb,
|
||||||
plinegen: true,
|
plinegen: true,
|
||||||
vp_scissor: None,
|
vp_scissor: None,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue