fix(scene): cross-check world_offset against MSPACE entity AABB, not just DXF header

world_offset (the precision-preserving translation subtracted from every
f32 wire point) came purely from the DXF $EXTMIN/$EXTMAX header. Three
real-world failure modes drove geometry far from the local origin and
killed f32 precision on MSPACE wires:

  * Sentinel header (1e20 / -1e20) when the writer never computed
    extents → world_offset = [0,0,0] → UTM-authored drawings render at
    WCS 4M with single-pixel jitter.
  * Stale header — drawing was edited but extents weren't refreshed →
    offset center sits well outside the actual entity centroid.
  * Civil-3D-style header that reports only a single Insert footprint,
    leaving direct MSPACE Line/Polyline/Hatch geometry far from the
    chosen offset.

Block defns already self-correct via `block_cache::build_defn`'s
per-defn bbox scan; top-level had no equivalent and was the inconsistent
half of the system. Add `compute_world_offset()` that scans MSPACE
entity bounding boxes (same `bounding_box()` API + SANE_EXTENT and
zero-placeholder filters that `build_defn` uses) and:

  * Header invalid + entity scan ok → use entity scan.
  * Header ok + entity scan empty → keep header (truly empty file).
  * Both ok → cross-check: if the header center drifts more than 10×
    its own half-span from the entity centroid, the header is stale —
    trust the entity scan.
  * Both unavailable → ([0,0,0], 1e9) sentinel.

`compute_and_set_world_offset` now delegates to the same helper so the
load path and any later recompute path can't diverge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-15 21:56:19 +03:00
1 parent 2716bcbbd8
commit 59da4f35a9
1 file changed
+157 -40
+157 -40
View File
@@ -101,24 +101,22 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
.unwrap_or(Handle::NULL)
});
// world_offset (from DXF header extents — fast and works when the
// writer correctly folded Insert→Block expansion into $EXTMIN/$EXTMAX).
let h = &doc.header;
let (min, max) = (h.model_space_extents_min, h.model_space_extents_max);
let valid = min.x < max.x && min.y < max.y;
let (world_offset, local_extent_max) = if valid {
let offset = [
(min.x + max.x) * 0.5,
(min.y + max.y) * 0.5,
(min.z + max.z) * 0.5,
];
let hw = ((max.x - min.x) * 0.5) as f32;
let hh = ((max.y - min.y) * 0.5) as f32;
let hz = ((max.z - min.z) * 0.5).max(1.0) as f32;
(offset, hw.max(hh).max(hz) * 10.0)
} else {
([0.0; 3], 1e9_f32)
};
// world_offset selection
//
// Header `$EXTMIN`/`$EXTMAX` is the fast path, but it's untrustworthy:
// the sentinel (1e20 / -1e20) when the writer never computed extents,
// stale values when a drawing was edited and extents weren't refreshed,
// and Civil-3D-style top-level extents that span only an Insert's
// bounding box rather than the actual MSPACE geometry. Any of those
// leave the precision-preserving offset wrong, so direct MSPACE
// entities render at huge magnitudes and f32 wires lose precision.
//
// Cross-check the header against a per-entity AABB scan of MSPACE
// (same `bounding_box()` API and same SANE_EXTENT/zero-placeholder
// filters that `block_cache::build_defn` already uses for block defns)
// and prefer the entity-scan when the header center drifts more than
// 10× its own half-span away from the entity centroid.
let (world_offset, local_extent_max) = compute_world_offset(doc, model_block);
use rayon::prelude::*;
@@ -204,6 +202,143 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
}
}
/// Pick the model-space precision-preserving offset and the `fit_all`
/// outlier-rejection limit.
///
/// Tries header `$EXTMIN/$EXTMAX` first, then cross-checks against a direct
/// MSPACE entity AABB scan. The entity scan wins when the header is invalid
/// (sentinel / sub-empty) or when the header center has drifted more than
/// 10× its own half-span from the entity centroid (a stale-extents DXF).
fn compute_world_offset(
doc: &acadrust::CadDocument,
model_block: Handle,
) -> ([f64; 3], f32) {
// Mirrors `block_cache::SANE_EXTENT` — wire coords past this magnitude
// are treated as corruption rather than precision-relevant geometry.
const SANE_EXTENT: f64 = 1.0e8;
// ── Pass 1: scan MSPACE entity bounding boxes ────────────────────────
let mut emin = [f64::INFINITY; 3];
let mut emax = [f64::NEG_INFINITY; 3];
let mut have = [false; 3];
for e in doc.entities() {
let c = e.common();
if c.owner_handle != model_block {
continue;
}
// Skip block-defn sentinels and AttributeDefinition — same as
// block_cache::build_defn. Their bboxes don't represent drawable
// MSPACE geometry.
if matches!(
e,
EntityType::Block(_)
| EntityType::BlockEnd(_)
| EntityType::AttributeDefinition(_)
) {
continue;
}
let (bmin, bmax) = match e {
EntityType::Insert(ins) => (ins.insert_point, ins.insert_point),
_ => {
let bb = e.as_entity().bounding_box();
(bb.min, bb.max)
}
};
// Empty-entity placeholder (Polyline/Hatch/Spline/Mesh with no
// vertices). Including these would pull the centroid toward origin
// and destroy precision on UTM-authored content.
if bmin.x == 0.0
&& bmin.y == 0.0
&& bmin.z == 0.0
&& bmax.x == 0.0
&& bmax.y == 0.0
&& bmax.z == 0.0
{
continue;
}
let lo = [bmin.x, bmin.y, bmin.z];
let hi = [bmax.x, bmax.y, bmax.z];
for i in 0..3 {
if !lo[i].is_finite() || !hi[i].is_finite() {
continue;
}
if lo[i].abs() > SANE_EXTENT || hi[i].abs() > SANE_EXTENT {
continue;
}
if lo[i] < emin[i] {
emin[i] = lo[i];
}
if hi[i] > emax[i] {
emax[i] = hi[i];
}
have[i] = true;
}
}
let entity_ok = have[0] && have[1];
// ── Pass 2: read header extents ──────────────────────────────────────
let h = &doc.header;
let hmin = h.model_space_extents_min;
let hmax = h.model_space_extents_max;
let header_ok = hmin.x < hmax.x
&& hmin.y < hmax.y
&& hmin.x.abs() < SANE_EXTENT
&& hmax.x.abs() < SANE_EXTENT
&& hmin.y.abs() < SANE_EXTENT
&& hmax.y.abs() < SANE_EXTENT;
let from_header = || -> ([f64; 3], f32) {
let offset = [
(hmin.x + hmax.x) * 0.5,
(hmin.y + hmax.y) * 0.5,
(hmin.z + hmax.z) * 0.5,
];
let hw = ((hmax.x - hmin.x) * 0.5) as f32;
let hh = ((hmax.y - hmin.y) * 0.5) as f32;
let hz = ((hmax.z - hmin.z) * 0.5).max(1.0) as f32;
(offset, hw.max(hh).max(hz) * 10.0)
};
let from_entity = || -> ([f64; 3], f32) {
let z_have = have[2];
let offset = [
(emin[0] + emax[0]) * 0.5,
(emin[1] + emax[1]) * 0.5,
if z_have { (emin[2] + emax[2]) * 0.5 } else { 0.0 },
];
let hw = ((emax[0] - emin[0]) * 0.5) as f32;
let hh = ((emax[1] - emin[1]) * 0.5) as f32;
let hz = if z_have {
((emax[2] - emin[2]) * 0.5).max(1.0) as f32
} else {
1.0
};
(offset, hw.max(hh).max(hz) * 10.0)
};
match (header_ok, entity_ok) {
(false, false) => ([0.0; 3], 1e9_f32),
(false, true) => from_entity(),
(true, false) => from_header(),
(true, true) => {
// Stale-header detection: how many header half-spans is the
// header center away from the entity centroid? > 10 → trust
// entities, header is lying.
let hcx = (hmin.x + hmax.x) * 0.5;
let hcy = (hmin.y + hmax.y) * 0.5;
let ecx = (emin[0] + emax[0]) * 0.5;
let ecy = (emin[1] + emax[1]) * 0.5;
let hhw = ((hmax.x - hmin.x).abs() * 0.5).max(1.0);
let hhh = ((hmax.y - hmin.y).abs() * 0.5).max(1.0);
let drift = ((hcx - ecx).abs() / hhw).max((hcy - ecy).abs() / hhh);
if drift > 10.0 {
from_entity()
} else {
from_header()
}
}
}
}
pub struct Scene {
pub camera: Rc<RefCell<Camera>>,
pub selection: Rc<RefCell<SelectionState>>,
@@ -454,28 +589,10 @@ impl Scene {
#[allow(dead_code)]
pub fn compute_and_set_world_offset(&mut self) {
let h = &self.document.header;
let min = h.model_space_extents_min;
let max = h.model_space_extents_max;
// Sentinel: DXF files with no geometry have EXTMIN = 1e20, EXTMAX = -1e20.
// Fall back to no offset so the scene stays at the origin.
let valid = min.x < max.x && min.y < max.y;
if valid {
self.world_offset = [
(min.x + max.x) * 0.5,
(min.y + max.y) * 0.5,
(min.z + max.z) * 0.5,
];
// 10× the EXTMIN→EXTMAX half-diagonal gives fit_all() a threshold to
// reject origin-stuck/corrupted entities far outside the drawing area.
let hw = ((max.x - min.x) * 0.5) as f32;
let hh = ((max.y - min.y) * 0.5) as f32;
let hz = ((max.z - min.z) * 0.5).max(1.0) as f32;
self.local_extent_max = hw.max(hh).max(hz) * 10.0;
} else {
self.world_offset = [0.0; 3];
self.local_extent_max = 1e9;
}
let model_block = self.model_space_block_handle();
let (offset, lim) = compute_world_offset(&self.document, model_block);
self.world_offset = offset;
self.local_extent_max = lim;
}
/// Returns true if this viewport should display model-space content