From 9a4531bd8edddc365120d0cd2e1c59b1fc8711fe Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 15 May 2026 22:58:32 +0300 Subject: [PATCH] fix(scene): use median of MSPACE entity centroids for world_offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Min/max midpoint is wrecked by a single bogus entity at WCS distance from the real drawing — a Ray with a bad direction, an orphan block- defn entity at WCS x = -510k while real geometry sits at WCS x = +510k. Header EXTMIN/EXTMAX bakes the same min/max midpoint, and the entity scan we added in 59da4f3 reproduced it: both lined up perfectly on the broken midpoint (drift = 0.000 in tracing) and parked world_offset 508 km from the dense cluster. Wires then rendered at f32 magnitudes ~5e5 — 6 cm precision on a drawing where the user is editing at mm. Switch the entity scan to per-entity centroids and take the **median** of x and y. Median is immune to a single far outlier regardless of how far it sits, so the same file that previously parked world_offset at (1445, 4277904) — halfway between the real drawing and a stray entity 512 km away — now resolves to (509099, 4274221), within 1 km of the actual drawing. Wire magnitudes drop from ~5e5 to ~4e3, restoring precision from 6 cm to ~0.5 mm (~120× improvement). local_extent_max also derives from the centroid spread (95th-percentile distance from the median × 2) rather than the raw min/max span, so the fit_all clamp rejects the same outliers that were previously inflating it. The header EXTMIN/EXTMAX path remains as a fallback for the case where the entity scan found nothing (truly empty MSPACE). Co-Authored-By: Claude Opus 4.7 --- src/scene/mod.rs | 116 ++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 61 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 0bbaed0b8..36a8a7dd1 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -251,10 +251,14 @@ fn compute_world_offset( std::collections::HashSet::new() }; - // ── 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]; + // ── Pass 1: collect per-entity centroids ───────────────────────────── + // Min/max midpoint is wrecked by a single bogus entity at WCS distance + // (e.g. a Ray with bad direction, an orphan reference at WCS x=-510k + // when the real drawing sits at WCS x=+510k — the midpoint lands + // halfway between, far from both clusters). Per-entity centroids let + // us take the median, which is immune to single outliers regardless + // of how far they sit. + let mut centers: Vec<[f64; 3]> = Vec::new(); for e in doc.entities() { let c = e.common(); let h = c.handle; @@ -305,25 +309,46 @@ fn compute_world_offset( { 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 cx = (bmin.x + bmax.x) * 0.5; + let cy = (bmin.y + bmax.y) * 0.5; + let cz = (bmin.z + bmax.z) * 0.5; + if !cx.is_finite() || !cy.is_finite() || !cz.is_finite() { + continue; } + if cx.abs() > SANE_EXTENT || cy.abs() > SANE_EXTENT { + continue; + } + centers.push([cx, cy, cz]); } - let entity_ok = have[0] && have[1]; + let entity_ok = !centers.is_empty(); + + // Median of per-entity centroids → robust drawing center. + // For local_extent_max: 95th-percentile distance from the median × 2 + // gives the half-span of the dense cluster while leaving room for + // legitimate outliers (sparse leaders, dimensions, scattered annotations). + let median = |v: &mut Vec| -> f64 { + v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + v[v.len() / 2] + }; + let percentile = |v: &mut Vec, frac: f64| -> f64 { + v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let i = ((v.len() as f64 - 1.0) * frac).round() as usize; + v[i] + }; + let (ecx, ecy, ecz, espan_max) = if entity_ok { + let mut xs: Vec = centers.iter().map(|c| c[0]).collect(); + let mut ys: Vec = centers.iter().map(|c| c[1]).collect(); + let mut zs: Vec = centers.iter().map(|c| c[2]).collect(); + let mx = median(&mut xs); + let my = median(&mut ys); + let mz = median(&mut zs); + let mut dx: Vec = centers.iter().map(|c| (c[0] - mx).abs()).collect(); + let mut dy: Vec = centers.iter().map(|c| (c[1] - my).abs()).collect(); + let p95 = percentile(&mut dx, 0.95).max(percentile(&mut dy, 0.95)); + (mx, my, mz, (p95 * 2.0).max(1.0) as f32) + } else { + (0.0, 0.0, 0.0, 0.0) + }; // ── Pass 2: read header extents ────────────────────────────────────── let h = &doc.header; @@ -336,7 +361,13 @@ fn compute_world_offset( && hmin.y.abs() < SANE_EXTENT && hmax.y.abs() < SANE_EXTENT; - let from_header = || -> ([f64; 3], f32) { + // Entity-derived offset is preferred whenever it's available — the + // median-of-centroids ignores Ray/orphan/duplicate-block-defn outliers + // that the header EXTMIN/EXTMAX (a min/max midpoint) bakes in. Header + // is the fallback only when the entity scan found nothing. + if entity_ok { + ([ecx, ecy, ecz], espan_max) + } else if header_ok { let offset = [ (hmin.x + hmax.x) * 0.5, (hmin.y + hmax.y) * 0.5, @@ -346,45 +377,8 @@ fn compute_world_offset( 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() - } - } + } else { + ([0.0; 3], 1e9_f32) } }