fix(zoom): include inserted block extents

Use current rendered geometry instead of load-time cluster spans when fitting model extents.

Refs #853
This commit is contained in:
Hakan Seven committed 2026-08-25 22:11:18 +03:00
1 parent 968ee3fda4
commit 81de94fdbb
2 files changed
+14 -52

No files matched your search

+12 -45
View File
@@ -1335,15 +1335,8 @@ impl Scene {
self.annotation_all_visible(),
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 — an infinite construction
// line should never contribute to ZOOM Extents — and fall back to their
// base points if the drawing holds nothing else.
// Infinite display segments do not contribute to drawing extents.
// Preserve their base points as a fallback for otherwise empty drawings.
let mut infinite_base_pts: Vec<glam::Vec3> = Vec::new();
wires.retain(|w| {
let is_infinite = Self::handle_from_wire_name(&w.name)
@@ -1389,18 +1382,12 @@ impl Scene {
return;
}
// Per-wire centroid pass — used both for the absolute-magnitude reject
// (`local_extent_max`) and for the IQR-based outlier reject below.
// A wire whose centroid sits far outside the drawing's consensus
// cluster is an orphan (block-defn entity that leaked into MSPACE,
// bogus hatch boundary, Ray/XLine far point) and must not poison the
// bounding box.
// Collect wire centroids for robust outlier rejection.
struct WireCent {
idx: usize,
cx: f32,
cy: f32,
}
let lim = self.local_extent_max;
let mut cents: Vec<WireCent> = Vec::with_capacity(wires.len());
for (idx, wire) in wires.iter().enumerate() {
let mut sx = 0.0_f64;
@@ -1426,26 +1413,8 @@ impl Scene {
return;
}
// Robust drawing centre (median centroid). `lim` is a span RELATIVE to
// this centre, so every reject below is distance-from-centre — geometry
// now reaches fit_all as absolute coordinates (no world_offset), which
// at UTM scale are ~5.7e6; an absolute `|x| > lim` test would reject the
// entire drawing and make ZOOM Extents a no-op.
let (mcx, mcy) = {
let mut xs: Vec<f32> = cents.iter().map(|c| c.cx).collect();
let mut ys: Vec<f32> = cents.iter().map(|c| c.cy).collect();
if xs.is_empty() {
(0.0_f32, 0.0_f32)
} else {
xs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
ys.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
(xs[xs.len() / 2], ys[ys.len() / 2])
}
};
// IQR-based reject only kicks in with enough samples for the quartiles
// to be meaningful. Below that, the centre-relative `lim` filter is the
// only gate (legacy behavior).
// Quartile rejection needs enough samples to be meaningful. With fewer
// wires, every finite visible point belongs to the drawing extents.
let (rx_lo, rx_hi, ry_lo, ry_hi) = if cents.len() >= 8 {
let mut xs: Vec<f32> = cents.iter().map(|c| c.cx).collect();
let mut ys: Vec<f32> = cents.iter().map(|c| c.cy).collect();
@@ -1456,17 +1425,18 @@ impl Scene {
let q3x = q(&xs, 0.75);
let q1y = q(&ys, 0.25);
let q3y = q(&ys, 0.75);
// k=10× the inter-quartile span is permissive enough to keep
// legitimate sparse outlying geometry (annotation labels, scattered
// dim leaders) but tight enough to drop a single wire stranded at
// -world_offset. `max(1.0)` guards against a degenerate IQR=0
// (e.g. all wires at the same centroid).
// Keep sparse annotations while rejecting isolated corrupt wires.
const K: f32 = 10.0;
let dx = (q3x - q1x).max(1.0) * K;
let dy = (q3y - q1y).max(1.0) * K;
(q1x - dx, q3x + dx, q1y - dy, q3y + dy)
} else {
(mcx - lim, mcx + lim, mcy - lim, mcy + lim)
(
f32::NEG_INFINITY,
f32::INFINITY,
f32::NEG_INFINITY,
f32::INFINITY,
)
};
let mut min = glam::Vec3::splat(f32::MAX);
@@ -1480,9 +1450,6 @@ impl Scene {
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
continue;
}
if (x - mcx).abs() > lim || (y - mcy).abs() > lim {
continue;
}
min = min.min(glam::Vec3::new(x, y, z));
max = max.max(glam::Vec3::new(x, y, z));
}
+2 -7
View File
@@ -1657,14 +1657,9 @@ pub struct Scene {
pub bg_color: [f32; 4],
/// Custom paper-space background fill color for Wipeout entities.
pub paper_bg_color: [f32; 4],
/// Largest local-space coordinate expected from real geometry, derived from
/// EXTMIN/EXTMAX (10× safety margin). Used by fit_all() to ignore garbage
/// entity coordinates (origin-stuck entities, bad Ray/XLine direction vectors).
/// Dense model-space cluster half-span used for viewport recovery.
pub local_extent_max: f32,
/// Robust centre (median of entity centroids) of the dense model-space
/// cluster. Used together with `local_extent_max` to frame a viewport whose
/// saved view is missing — aiming at the raw extents centre would land in
/// the empty gap when a drawing has a second, far cluster.
/// Dense model-space cluster median used for viewport recovery.
pub local_center: [f64; 2],
/// Current annotation scale (CANNOSCALE equivalent).
/// Multiplier applied to Text/MText/Dimension sizes during tessellation.