fix(view): exclude XLine/Ray display segments from ZOOM Extents (#284)

XLine/Ray tessellate as +/-1e6 display segments (entities/ray.rs), and
both fit_all outlier defenses miss them: the IQR reject passes because a
construction line through the drawing has its centroid at its base
point, inside the consensus cluster; and the per-point lim filter passes
because local_extent_max is computed once at document load and stays at
the 1e9 default for drawings created fresh in-app. The far endpoints
then poison the bounds and the view fits +/-1e6, shrinking real
geometry to a dot.

Exclude XLine/Ray wires from the extents up front (AutoCAD likewise
ignores infinite lines in ZOOM Extents), falling back to fitting their
base points when the drawing holds nothing else.

Regression tests verified to fail without the fix (camera distance
3,000,000) and pass with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Griffin 2026-07-05 14:08:43 -07:00
commit a3107d95e4
2 changed files with 105 additions and 2 deletions

View file

@ -636,6 +636,30 @@ impl Scene {
if self.current_layout != "Model" {
wires.extend(self.viewport_content_wires(layout_block, None, None));
}
// Ray / XLine tessellate as ±DISPLAY_EXTENT display segments
// (entities/ray.rs) — their endpoints are rendering artifacts, not
// drawing extent. A construction line through the drawing defeats
// both rejects below: its centroid sits at the base point (inside
// the consensus cluster), and in a fresh drawing `local_extent_max`
// is still the 1e9 default, so the far points pass the `lim` filter
// too (issue #284). Exclude them up front — matching AutoCAD, where
// infinite lines never contribute to ZOOM Extents — and fall back
// to their base points if the drawing holds nothing else.
let mut infinite_base_pts: Vec<glam::Vec3> = Vec::new();
wires.retain(|w| {
let is_infinite = Self::handle_from_wire_name(&w.name)
.and_then(|h| self.document.get_entity(h))
.map(|e| matches!(e, EntityType::XLine(_) | EntityType::Ray(_)))
.unwrap_or(false);
if is_infinite {
infinite_base_pts.extend(
w.key_vertices
.iter()
.map(|v| glam::Vec3::new(v[0] as f32, v[1] as f32, v[2] as f32)),
);
}
!is_infinite
});
// 3D solids render as meshes, not wires, so collect their (offset-rel)
// XY AABBs separately — a drawing of only solids has no wires to fit.
let mesh_aabbs: Vec<[f32; 4]> = self
@ -650,7 +674,7 @@ impl Scene {
.map(|(_, set)| set.world_aabb)
.filter(|a| a[0].is_finite() && a[2].is_finite())
.collect();
if wires.is_empty() && mesh_aabbs.is_empty() {
if wires.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() {
return;
}
@ -687,7 +711,7 @@ impl Scene {
});
}
}
if cents.is_empty() && mesh_aabbs.is_empty() {
if cents.is_empty() && mesh_aabbs.is_empty() && infinite_base_pts.is_empty() {
return;
}
@ -757,6 +781,14 @@ impl Scene {
min = min.min(glam::Vec3::new(*ax, *ay, 0.0));
max = max.max(glam::Vec3::new(*bx, *by, 0.0));
}
// Drawing holds only infinite construction geometry: fit the view to
// its base points rather than leaving the camera unchanged.
if min.x > max.x {
for p in &infinite_base_pts {
min = min.min(*p);
max = max.max(*p);
}
}
// If no usable points found, leave the camera unchanged.
if min.x > max.x {
return;