forked from 1b-agent/opencadstudio-upstream
fix(scene): use median of MSPACE entity centroids for world_offset
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 <[email protected]>
This commit is contained in:
1 parent
7ee93f6aea
commit
9a4531bd8e
1 file changed
+55
-61
+55
-61
@@ -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>| -> 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<f64>, 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<f64> = centers.iter().map(|c| c[0]).collect();
|
||||
let mut ys: Vec<f64> = centers.iter().map(|c| c[1]).collect();
|
||||
let mut zs: Vec<f64> = 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<f64> = centers.iter().map(|c| (c[0] - mx).abs()).collect();
|
||||
let mut dy: Vec<f64> = 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user