Cord/crates/cord-decompile/src/fit.rs

1186 lines
38 KiB
Rust

use crate::density::{DensityMap, SurfaceType};
use crate::mesh::Vec3;
use crate::sparse_grid::{SparseGrid, SurfaceSample};
use crate::DecompileConfig;
#[derive(Debug, Clone)]
pub struct DetectedPrimitive {
pub kind: PrimitiveKind,
pub support: Vec<usize>,
pub fit_error: f64,
}
#[derive(Debug, Clone)]
pub enum PrimitiveKind {
Plane { point: Vec3, normal: Vec3 },
Sphere { center: Vec3, radius: f64 },
Cylinder { point: Vec3, axis: Vec3, radius: f64 },
Box {
center: Vec3,
half_extents: [f64; 3],
rotation_axis: Vec3,
rotation_angle: f64,
},
}
pub fn detect_primitives(
grid: &SparseGrid,
density_map: &DensityMap,
config: &DecompileConfig,
) -> Vec<DetectedPrimitive> {
let samples = grid.surface_samples();
if samples.is_empty() {
return Vec::new();
}
let mut remaining: Vec<bool> = vec![true; samples.len()];
let mut primitives = Vec::new();
let min_support = (samples.len() as f64 * config.min_support_ratio).max(3.0) as usize;
// Iterative RANSAC: detect largest primitive, remove its inliers, repeat
for _ in 0..20 {
let active_count = remaining.iter().filter(|&&r| r).count();
if active_count < min_support {
break;
}
let best = ransac_detect(
&samples,
&remaining,
density_map,
config,
);
if let Some(mut prim) = best {
if prim.support.len() < min_support {
break;
}
refine_and_expand(&mut prim, &samples, &remaining, config);
for &idx in &prim.support {
remaining[idx] = false;
}
prim.support.sort_unstable();
primitives.push(prim);
} else {
break;
}
}
// Post-detection: try to consolidate clusters of primitives into boxes.
// Group primitives by spatial proximity and test if each cluster is a box.
consolidate_into_boxes(&mut primitives, &samples, config);
primitives
}
/// Try to replace non-box primitives with box detections.
///
/// For each non-box primitive, check if its support points form a box.
/// If the box fit is better (higher coverage), replace the original.
/// Then try merging overlapping box-candidates and neighboring primitives.
fn consolidate_into_boxes(
primitives: &mut Vec<DetectedPrimitive>,
samples: &[SurfaceSample],
config: &DecompileConfig,
) {
// Pass 1: for each non-box primitive, split its support into spatial clusters
// and try fitting a box to each cluster.
let mut new_prims = Vec::new();
let mut any_replaced = false;
for prim in primitives.iter() {
if matches!(prim.kind, PrimitiveKind::Box { .. }) || prim.support.len() < 6 {
new_prims.push(prim.clone());
continue;
}
let clusters = spatial_cluster(&prim.support, samples, config.distance_threshold * 5.0);
let mut box_replacements: Vec<DetectedPrimitive> = Vec::new();
for cluster in &clusters {
if cluster.len() < 6 { continue; }
if let Some(box_prim) = try_fit_box_relaxed(samples, cluster, config) {
// Box must capture at least 60% of the cluster's points
let coverage = box_prim.support.len() as f64 / cluster.len() as f64;
// Box mean error must be lower than original
let box_mean_err = box_prim.fit_error / box_prim.support.len().max(1) as f64;
let orig_mean_err = prim.fit_error / prim.support.len().max(1) as f64;
if coverage > 0.6 && box_mean_err <= orig_mean_err {
box_replacements.push(box_prim);
}
}
}
if box_replacements.is_empty() {
new_prims.push(prim.clone());
} else {
new_prims.extend(box_replacements);
any_replaced = true;
}
}
if any_replaced {
*primitives = new_prims;
}
// Pass 2: merge overlapping primitives into boxes
if primitives.len() < 2 {
return;
}
let bboxes: Vec<(Vec3, Vec3)> = primitives.iter().map(|prim| {
let mut lo = Vec3::new(f64::MAX, f64::MAX, f64::MAX);
let mut hi = Vec3::new(f64::MIN, f64::MIN, f64::MIN);
for &idx in &prim.support {
if idx >= samples.len() { continue; }
let p = samples[idx].position;
lo.x = lo.x.min(p.x); lo.y = lo.y.min(p.y); lo.z = lo.z.min(p.z);
hi.x = hi.x.max(p.x); hi.y = hi.y.max(p.y); hi.z = hi.z.max(p.z);
}
(lo, hi)
}).collect();
let mut used = vec![false; primitives.len()];
let mut replacements: Vec<(Vec<usize>, DetectedPrimitive)> = Vec::new();
for i in 0..primitives.len() {
if used[i] { continue; }
let mut group = vec![i];
for j in (i + 1)..primitives.len() {
if used[j] { continue; }
if bboxes_overlap(&bboxes[i], &bboxes[j]) {
group.push(j);
}
}
if group.len() < 2 { continue; }
let mut all_indices: Vec<usize> = Vec::new();
for &gi in &group {
all_indices.extend_from_slice(&primitives[gi].support);
}
all_indices.sort_unstable();
all_indices.dedup();
if let Some(box_prim) = try_fit_box(samples, &all_indices, config) {
let box_coverage = box_prim.support.len() as f64 / all_indices.len() as f64;
if box_coverage > 0.5 {
for &gi in &group {
used[gi] = true;
}
replacements.push((group, box_prim));
}
}
}
if !replacements.is_empty() {
let mut new_prims = Vec::new();
for (i, prim) in primitives.drain(..).enumerate() {
if !used[i] {
new_prims.push(prim);
}
}
for (_, box_prim) in replacements {
new_prims.push(box_prim);
}
*primitives = new_prims;
}
}
/// Split a set of sample indices into spatially connected clusters.
/// Uses a simple grid-based spatial hash for O(n) clustering.
fn spatial_cluster(
indices: &[usize],
samples: &[SurfaceSample],
cell_size: f64,
) -> Vec<Vec<usize>> {
use std::collections::HashMap;
if indices.len() < 2 || cell_size < 1e-10 {
return vec![indices.to_vec()];
}
let inv = 1.0 / cell_size;
let mut grid: HashMap<(i64, i64, i64), Vec<usize>> = HashMap::new();
for &idx in indices {
if idx >= samples.len() { continue; }
let p = samples[idx].position;
let key = (
(p.x * inv).floor() as i64,
(p.y * inv).floor() as i64,
(p.z * inv).floor() as i64,
);
grid.entry(key).or_default().push(idx);
}
// BFS to find connected components
let keys: Vec<(i64, i64, i64)> = grid.keys().cloned().collect();
let mut visited: HashMap<(i64, i64, i64), bool> = HashMap::new();
let mut clusters = Vec::new();
for key in &keys {
if visited.get(key).copied().unwrap_or(false) { continue; }
let mut cluster_indices = Vec::new();
let mut queue = vec![*key];
visited.insert(*key, true);
while let Some(k) = queue.pop() {
if let Some(pts) = grid.get(&k) {
cluster_indices.extend_from_slice(pts);
}
// Check 26 neighbors
for dz in -1..=1i64 {
for dy in -1..=1i64 {
for dx in -1..=1i64 {
if dx == 0 && dy == 0 && dz == 0 { continue; }
let nk = (k.0 + dx, k.1 + dy, k.2 + dz);
if grid.contains_key(&nk) && !visited.get(&nk).copied().unwrap_or(false) {
visited.insert(nk, true);
queue.push(nk);
}
}
}
}
}
if !cluster_indices.is_empty() {
clusters.push(cluster_indices);
}
}
clusters
}
fn bboxes_overlap(a: &(Vec3, Vec3), b: &(Vec3, Vec3)) -> bool {
let (a_lo, a_hi) = a;
let (b_lo, b_hi) = b;
a_lo.x <= b_hi.x && a_hi.x >= b_lo.x
&& a_lo.y <= b_hi.y && a_hi.y >= b_lo.y
&& a_lo.z <= b_hi.z && a_hi.z >= b_lo.z
}
/// Re-fit primitive parameters from support set, then expand inliers.
/// Runs 2 iterations of refine→expand to converge on the best fit.
fn refine_and_expand(
prim: &mut DetectedPrimitive,
samples: &[SurfaceSample],
remaining: &[bool],
config: &DecompileConfig,
) {
for _ in 0..2 {
match &mut prim.kind {
PrimitiveKind::Sphere { center, radius } => {
// Re-estimate center as mean of support positions minus their radial direction
if prim.support.len() < 4 { return; }
let mut sum_c = Vec3::zero();
let mut count = 0.0;
for &idx in &prim.support {
if idx >= samples.len() { continue; }
let p = samples[idx].position;
let n = samples[idx].normal;
let c = p - n * *radius;
sum_c = sum_c + c;
count += 1.0;
}
if count < 1.0 { return; }
*center = sum_c * (1.0 / count);
// Re-estimate radius
let mut sum_r = 0.0;
for &idx in &prim.support {
if idx >= samples.len() { continue; }
sum_r += (samples[idx].position - *center).length();
}
*radius = sum_r / count;
// Expand: re-scan all remaining points
let thresh = config.distance_threshold * 1.5;
prim.support.clear();
prim.fit_error = 0.0;
for (idx, &active) in remaining.iter().enumerate() {
if !active || idx >= samples.len() { continue; }
let dist = ((samples[idx].position - *center).length() - *radius).abs();
if dist < thresh {
prim.support.push(idx);
prim.fit_error += dist;
}
}
}
PrimitiveKind::Cylinder { point, axis, radius } => {
if prim.support.len() < 5 { return; }
// Re-estimate radius
let mut sum_r = 0.0;
let mut count = 0.0;
for &idx in &prim.support {
if idx >= samples.len() { continue; }
let p = samples[idx].position;
let v = p - *point;
let along = *axis * v.dot(*axis);
let radial = (v - along).length();
sum_r += radial;
count += 1.0;
}
if count < 1.0 { return; }
*radius = sum_r / count;
let thresh = config.distance_threshold * 1.5;
prim.support.clear();
prim.fit_error = 0.0;
for (idx, &active) in remaining.iter().enumerate() {
if !active || idx >= samples.len() { continue; }
let p = samples[idx].position;
let v = p - *point;
let along = *axis * v.dot(*axis);
let dist = ((v - along).length() - *radius).abs();
if dist < thresh {
prim.support.push(idx);
prim.fit_error += dist;
}
}
}
PrimitiveKind::Plane { point, normal } => {
if prim.support.len() < 3 { return; }
// Re-estimate normal and offset from support mean
let mut sum_n = Vec3::zero();
let mut sum_p = Vec3::zero();
let mut count = 0.0;
for &idx in &prim.support {
if idx >= samples.len() { continue; }
sum_n = sum_n + samples[idx].normal;
sum_p = sum_p + samples[idx].position;
count += 1.0;
}
if count < 1.0 { return; }
*normal = sum_n.normalized();
*point = sum_p * (1.0 / count);
let d = normal.dot(*point);
let thresh = config.distance_threshold * 1.5;
prim.support.clear();
prim.fit_error = 0.0;
for (idx, &active) in remaining.iter().enumerate() {
if !active || idx >= samples.len() { continue; }
let dist = (normal.dot(samples[idx].position) - d).abs();
let agree = normal.dot(samples[idx].normal).abs();
if dist < thresh && agree > config.normal_threshold * 0.9 {
prim.support.push(idx);
prim.fit_error += dist;
}
}
}
PrimitiveKind::Box { .. } => {}
}
}
}
fn ransac_detect(
samples: &[SurfaceSample],
remaining: &[bool],
density_map: &DensityMap,
config: &DecompileConfig,
) -> Option<DetectedPrimitive> {
let active: Vec<usize> = remaining.iter().enumerate()
.filter(|(_, &r)| r)
.map(|(i, _)| i)
.collect();
if active.len() < 3 {
return None;
}
let mut best: Option<DetectedPrimitive> = None;
let mut rng = SimpleRng::new(42);
// Global geometric detection (box, sphere) before random RANSAC
for candidate in [
try_fit_box(samples, &active, config),
try_fit_global_sphere(samples, &active, config),
].into_iter().flatten() {
let score = fit_score(&candidate);
let dominated = match &best {
Some(b) => score > fit_score(b),
None => true,
};
if dominated {
best = Some(candidate);
}
}
for _ in 0..config.ransac_iterations {
let candidates = [
try_fit_plane(samples, &active, &mut rng, config),
try_fit_sphere(samples, &active, &mut rng, config),
try_fit_cylinder(samples, &active, density_map, &mut rng, config),
];
for candidate in candidates.into_iter().flatten() {
let dominated = match &best {
Some(b) => {
let c_score = fit_score(&candidate);
let b_score = fit_score(b);
c_score > b_score
}
None => true,
};
if dominated {
best = Some(candidate);
}
}
}
best
}
/// Quality-weighted score for primitive ranking.
/// Planes get a strong bonus (simpler parametric form, merge into boxes later).
/// Score = support_count * quality_factor / (1 + mean_error)
fn fit_score(prim: &DetectedPrimitive) -> f64 {
let support = prim.support.len() as f64;
let mean_err = if prim.support.is_empty() {
1.0
} else {
prim.fit_error / support
};
let complexity_bonus = match &prim.kind {
PrimitiveKind::Plane { .. } => 1.5,
PrimitiveKind::Sphere { .. } => 1.0,
PrimitiveKind::Cylinder { .. } => 0.8,
PrimitiveKind::Box { .. } => 1.3,
};
support * complexity_bonus / (1.0 + mean_err)
}
fn try_fit_plane(
samples: &[SurfaceSample],
active: &[usize],
rng: &mut SimpleRng,
config: &DecompileConfig,
) -> Option<DetectedPrimitive> {
if active.len() < 3 {
return None;
}
let i0 = active[rng.next_usize(active.len())];
let i1 = active[rng.next_usize(active.len())];
let i2 = active[rng.next_usize(active.len())];
if i0 == i1 || i1 == i2 || i0 == i2 {
return None;
}
let p0 = samples[i0].position;
let p1 = samples[i1].position;
let p2 = samples[i2].position;
let normal = (p1 - p0).cross(p2 - p0).normalized();
if normal.length() < 0.5 {
return None;
}
let d = normal.dot(p0);
let mut support = Vec::new();
let mut total_error = 0.0;
for &idx in active {
let dist = (normal.dot(samples[idx].position) - d).abs();
let normal_agree = normal.dot(samples[idx].normal).abs();
if dist < config.distance_threshold && normal_agree > config.normal_threshold {
total_error += dist;
support.push(idx);
}
}
if support.len() < 3 {
return None;
}
Some(DetectedPrimitive {
kind: PrimitiveKind::Plane { point: p0, normal },
support,
fit_error: total_error,
})
}
fn try_fit_sphere(
samples: &[SurfaceSample],
active: &[usize],
rng: &mut SimpleRng,
config: &DecompileConfig,
) -> Option<DetectedPrimitive> {
if active.len() < 4 {
return None;
}
// Pick two points; use their normals to estimate center.
// If normals point radially from a common center, the intersection
// of the two normal rays gives the center.
let i0 = active[rng.next_usize(active.len())];
let i1 = active[rng.next_usize(active.len())];
if i0 == i1 { return None; }
let p0 = samples[i0].position;
let n0 = samples[i0].normal;
let p1 = samples[i1].position;
let n1 = samples[i1].normal;
// Closest point between two rays: p0 + t*n0 and p1 + s*n1
let center = closest_point_two_rays(p0, n0, p1, n1)?;
let r0 = (center - p0).length();
let r1 = (center - p1).length();
if (r0 - r1).abs() > config.distance_threshold * 10.0 {
return None;
}
let radius = (r0 + r1) * 0.5;
if radius < 1e-6 {
return None;
}
let mut support = Vec::new();
let mut total_error = 0.0;
for &idx in active {
let dist = ((samples[idx].position - center).length() - radius).abs();
if dist < config.distance_threshold {
total_error += dist;
support.push(idx);
}
}
if support.len() < 4 {
return None;
}
Some(DetectedPrimitive {
kind: PrimitiveKind::Sphere { center, radius },
support,
fit_error: total_error,
})
}
fn try_fit_cylinder(
samples: &[SurfaceSample],
active: &[usize],
density_map: &DensityMap,
rng: &mut SimpleRng,
config: &DecompileConfig,
) -> Option<DetectedPrimitive> {
if active.len() < 5 {
return None;
}
// Cylinder detection: pick two points with curved surface type,
// cross their normals to get the axis direction.
let curved: Vec<usize> = active.iter()
.filter(|&&idx| {
density_map.cells.get(&samples[idx].cell_key)
.map_or(false, |d| d.surface_type == SurfaceType::Curved)
})
.copied()
.collect();
let source = if curved.len() >= 2 { &curved } else { active };
let i0 = source[rng.next_usize(source.len())];
let i1 = source[rng.next_usize(source.len())];
if i0 == i1 { return None; }
let n0 = samples[i0].normal;
let n1 = samples[i1].normal;
let axis = n0.cross(n1).normalized();
if axis.length() < 0.3 {
return None;
}
// Project points onto the plane perpendicular to the axis.
// The cylinder's cross-section in that plane is a circle.
let p0_proj = project_onto_plane(samples[i0].position, axis);
let p1_proj = project_onto_plane(samples[i1].position, axis);
// Use the normals (also projected) to find the center
let n0_proj = project_onto_plane(n0, axis).normalized();
let n1_proj = project_onto_plane(n1, axis).normalized();
let center_2d = closest_point_two_rays(p0_proj, n0_proj, p1_proj, n1_proj)?;
let r0 = (p0_proj - center_2d).length();
let r1 = (p1_proj - center_2d).length();
if (r0 - r1).abs() > config.distance_threshold * 10.0 {
return None;
}
let radius = (r0 + r1) * 0.5;
if radius < 1e-6 {
return None;
}
// center_2d is in the projected plane; reconstruct 3D point on axis
let axis_point = center_2d; // The projection zeroed the axis component
let mut support = Vec::new();
let mut total_error = 0.0;
for &idx in active {
let p = samples[idx].position;
let p_proj = project_onto_plane(p, axis);
let dist = ((p_proj - center_2d).length() - radius).abs();
if dist >= config.distance_threshold { continue; }
// Normal check: the point's normal projected onto the cross-section plane
// should be roughly radial from the center (for side surface points)
let n = samples[idx].normal;
let n_along_axis = axis * n.dot(axis);
let n_radial = (n - n_along_axis).normalized();
let expected_radial = (p_proj - center_2d).normalized();
let normal_agree = n_radial.dot(expected_radial).abs();
// Accept if normal is radial (side surface) or axial (cap)
let is_axial = n.dot(axis).abs() > 0.8;
if normal_agree > 0.5 || is_axial {
total_error += dist;
support.push(idx);
}
}
if support.len() < 5 {
return None;
}
Some(DetectedPrimitive {
kind: PrimitiveKind::Cylinder { point: axis_point, axis, radius },
support,
fit_error: total_error,
})
}
/// Axis-aligned box detection.
///
/// Global sphere detection using normal convergence.
///
/// For each point, the estimated center is `p - r*n`. If many points
/// produce a consistent center, they form a sphere. Estimates center
/// from a subsample, then scores all active points.
fn try_fit_global_sphere(
samples: &[SurfaceSample],
active: &[usize],
config: &DecompileConfig,
) -> Option<DetectedPrimitive> {
if active.len() < 10 {
return None;
}
// Estimate a candidate radius from the bounding extent of active points
let mut min_p = Vec3::new(f64::MAX, f64::MAX, f64::MAX);
let mut max_p = Vec3::new(f64::MIN, f64::MIN, f64::MIN);
for &idx in active {
let p = samples[idx].position;
min_p.x = min_p.x.min(p.x); min_p.y = min_p.y.min(p.y); min_p.z = min_p.z.min(p.z);
max_p.x = max_p.x.max(p.x); max_p.y = max_p.y.max(p.y); max_p.z = max_p.z.max(p.z);
}
let extent = max_p - min_p;
let max_dim = extent.x.max(extent.y).max(extent.z);
let min_dim = extent.x.min(extent.y).min(extent.z);
// Sphere test: all three dimensions should be similar
if min_dim < max_dim * 0.75 {
return None;
}
let r_est = max_dim / 2.0;
// Estimate center: for each point, center = position - normal * radius
// Use a subsample for speed
let step = (active.len() / 200).max(1);
let mut sum_c = Vec3::zero();
let mut count = 0.0;
for i in (0..active.len()).step_by(step) {
let idx = active[i];
let p = samples[idx].position;
let n = samples[idx].normal;
let c = p - n * r_est;
sum_c = sum_c + c;
count += 1.0;
}
if count < 3.0 { return None; }
let mut center = sum_c * (1.0 / count);
// Refine radius from estimated center
let mut sum_r = 0.0;
let mut r_count = 0.0;
for i in (0..active.len()).step_by(step) {
let idx = active[i];
let r = (samples[idx].position - center).length();
sum_r += r;
r_count += 1.0;
}
let mut radius = sum_r / r_count;
// Second pass: refine center using all points near the sphere
let thresh = config.distance_threshold * 2.0;
for _ in 0..3 {
let mut new_center = Vec3::zero();
let mut new_count = 0.0;
let mut new_radius = 0.0;
for &idx in active {
let p = samples[idx].position;
let dist = ((p - center).length() - radius).abs();
if dist < thresh {
let n = samples[idx].normal;
new_center = new_center + (p - n * radius);
new_radius += (p - center).length();
new_count += 1.0;
}
}
if new_count < 3.0 { break; }
center = new_center * (1.0 / new_count);
radius = new_radius / new_count;
}
// Score
let mut support = Vec::new();
let mut total_error = 0.0;
for &idx in active {
let dist = ((samples[idx].position - center).length() - radius).abs();
if dist < thresh {
support.push(idx);
total_error += dist;
}
}
let min_support = (active.len() as f64 * 0.3).max(10.0) as usize;
if support.len() < min_support {
return None;
}
Some(DetectedPrimitive {
kind: PrimitiveKind::Sphere { center, radius },
support,
fit_error: total_error,
})
}
/// Axis-aligned box detection.
///
/// Groups active points by which axis-aligned face they belong to
/// (based on normal direction), then checks if 4+ groups describe
/// a consistent box. Requires points on at least 4 of the 6 faces.
fn try_fit_box(
samples: &[SurfaceSample],
active: &[usize],
config: &DecompileConfig,
) -> Option<DetectedPrimitive> {
if active.len() < 6 {
return None;
}
let normal_threshold = 0.95;
// Bucket points by face normal: +X, -X, +Y, -Y, +Z, -Z
let mut face_points: [Vec<usize>; 6] = Default::default();
for &idx in active {
let n = samples[idx].normal;
let ax = n.x.abs();
let ay = n.y.abs();
let az = n.z.abs();
let max_comp = ax.max(ay).max(az);
if max_comp < normal_threshold { continue; }
if ax == max_comp {
if n.x > 0.0 { face_points[0].push(idx); }
else { face_points[1].push(idx); }
} else if ay == max_comp {
if n.y > 0.0 { face_points[2].push(idx); }
else { face_points[3].push(idx); }
} else {
if n.z > 0.0 { face_points[4].push(idx); }
else { face_points[5].push(idx); }
}
}
// Need points on at least 4 faces to identify a box
let populated_faces = face_points.iter().filter(|f| f.len() >= 3).count();
if populated_faces < 4 {
return None;
}
// Axis-aligned points should be a majority of all active points.
// For a true box, most surface points have axis-aligned normals.
// For a cylinder, only cap faces are axis-aligned.
let aligned_total: usize = face_points.iter().map(|f| f.len()).sum();
if (aligned_total as f64) < active.len() as f64 * 0.4 {
return None;
}
// Verify each face is actually planar: low variance along the face normal axis
for (i, points) in face_points.iter().enumerate() {
if points.len() < 3 { continue; }
let axis_idx = i / 2;
let vals: Vec<f64> = points.iter().map(|&idx| {
let p = samples[idx].position;
match axis_idx { 0 => p.x, 1 => p.y, _ => p.z }
}).collect();
let mean = vals.iter().sum::<f64>() / vals.len() as f64;
let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / vals.len() as f64;
let std_dev = variance.sqrt();
// If variance is high relative to the threshold, this isn't a flat face
if std_dev > config.distance_threshold * 3.0 {
return None;
}
}
// Compute face positions (average coordinate along the face normal axis)
let mut face_pos = [f64::NAN; 6];
for (i, points) in face_points.iter().enumerate() {
if points.len() < 3 { continue; }
let axis_idx = i / 2;
let sum: f64 = points.iter().map(|&idx| {
let p = samples[idx].position;
match axis_idx { 0 => p.x, 1 => p.y, _ => p.z }
}).sum();
face_pos[i] = sum / points.len() as f64;
}
// Verify consistency: opposing faces should bracket the box
let mut center = Vec3::zero();
let mut half = [0.0f64; 3];
let mut axes_found = 0;
for axis_idx in 0..3 {
let pos_face = axis_idx * 2;
let neg_face = axis_idx * 2 + 1;
if face_pos[pos_face].is_nan() && face_pos[neg_face].is_nan() {
continue;
}
if !face_pos[pos_face].is_nan() && !face_pos[neg_face].is_nan() {
let hi = face_pos[pos_face].max(face_pos[neg_face]);
let lo = face_pos[pos_face].min(face_pos[neg_face]);
let mid = (hi + lo) / 2.0;
let h = (hi - lo) / 2.0;
match axis_idx {
0 => center.x = mid,
1 => center.y = mid,
_ => center.z = mid,
}
half[axis_idx] = h;
axes_found += 1;
} else {
// Single face found: estimate from point extent along this axis
let face_idx = if !face_pos[pos_face].is_nan() { pos_face } else { neg_face };
let all_box_points: Vec<usize> = face_points.iter().flat_map(|f| f.iter().copied()).collect();
let (lo, hi) = extent_along_axis(&all_box_points, samples, axis_idx);
let mid = (hi + lo) / 2.0;
let h = (hi - lo) / 2.0;
if h < 1e-6 { continue; }
match axis_idx {
0 => center.x = mid,
1 => center.y = mid,
_ => center.z = mid,
}
half[axis_idx] = h;
let _ = face_idx;
axes_found += 1;
}
}
if axes_found < 2 {
return None;
}
// If one axis is missing, estimate from the extent of support points
if axes_found == 2 {
for axis_idx in 0..3 {
if half[axis_idx] > 1e-6 { continue; }
let all_box_points: Vec<usize> = face_points.iter().flat_map(|f| f.iter().copied()).collect();
let (lo, hi) = extent_along_axis(&all_box_points, samples, axis_idx);
let mid = (hi + lo) / 2.0;
let h = (hi - lo) / 2.0;
if h < 1e-6 { continue; }
match axis_idx {
0 => center.x = mid,
1 => center.y = mid,
_ => center.z = mid,
}
half[axis_idx] = h;
}
}
if half[0] < 1e-6 || half[1] < 1e-6 || half[2] < 1e-6 {
return None;
}
// Score: use wider threshold for box scoring to capture edge/corner samples
let mut support = Vec::new();
let mut total_error = 0.0;
let thresh = config.distance_threshold * 2.0;
for &idx in active {
let p = samples[idx].position;
let local = Vec3::new(
(p.x - center.x).abs(),
(p.y - center.y).abs(),
(p.z - center.z).abs(),
);
// SDF of a box at origin with half_extents
let q = Vec3::new(
local.x - half[0],
local.y - half[1],
local.z - half[2],
);
let outside = Vec3::new(q.x.max(0.0), q.y.max(0.0), q.z.max(0.0));
let inside_dist = q.x.max(q.y).max(q.z).min(0.0);
let dist = (outside.length() + inside_dist).abs();
if dist < thresh {
total_error += dist;
support.push(idx);
}
}
let min_support = (active.len() as f64 * 0.1).max(6.0) as usize;
if support.len() < min_support {
return None;
}
Some(DetectedPrimitive {
kind: PrimitiveKind::Box {
center,
half_extents: half,
rotation_axis: Vec3::new(1.0, 0.0, 0.0),
rotation_angle: 0.0,
},
support,
fit_error: total_error,
})
}
/// Relaxed box detection for consolidation pass.
/// Lower normal threshold and aligned fraction requirement.
fn try_fit_box_relaxed(
samples: &[SurfaceSample],
active: &[usize],
config: &DecompileConfig,
) -> Option<DetectedPrimitive> {
if active.len() < 6 {
return None;
}
let normal_threshold = 0.85;
let mut face_points: [Vec<usize>; 6] = Default::default();
for &idx in active {
if idx >= samples.len() { continue; }
let n = samples[idx].normal;
let ax = n.x.abs();
let ay = n.y.abs();
let az = n.z.abs();
let max_comp = ax.max(ay).max(az);
if max_comp < normal_threshold { continue; }
if ax == max_comp {
if n.x > 0.0 { face_points[0].push(idx); }
else { face_points[1].push(idx); }
} else if ay == max_comp {
if n.y > 0.0 { face_points[2].push(idx); }
else { face_points[3].push(idx); }
} else {
if n.z > 0.0 { face_points[4].push(idx); }
else { face_points[5].push(idx); }
}
}
let populated_faces = face_points.iter().filter(|f| f.len() >= 3).count();
if populated_faces < 4 {
return None;
}
// Relaxed: only 25% axis-aligned required
let aligned_total: usize = face_points.iter().map(|f| f.len()).sum();
if (aligned_total as f64) < active.len() as f64 * 0.25 {
return None;
}
// Face planarity check
for (i, points) in face_points.iter().enumerate() {
if points.len() < 3 { continue; }
let axis_idx = i / 2;
let vals: Vec<f64> = points.iter().map(|&idx| {
let p = samples[idx].position;
match axis_idx { 0 => p.x, 1 => p.y, _ => p.z }
}).collect();
let mean = vals.iter().sum::<f64>() / vals.len() as f64;
let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / vals.len() as f64;
if variance.sqrt() > config.distance_threshold * 5.0 {
return None;
}
}
let mut face_pos = [f64::NAN; 6];
for (i, points) in face_points.iter().enumerate() {
if points.len() < 3 { continue; }
let axis_idx = i / 2;
let sum: f64 = points.iter().map(|&idx| {
let p = samples[idx].position;
match axis_idx { 0 => p.x, 1 => p.y, _ => p.z }
}).sum();
face_pos[i] = sum / points.len() as f64;
}
let mut center = Vec3::zero();
let mut half = [0.0f64; 3];
let mut axes_found = 0;
for axis_idx in 0..3 {
let pos_face = axis_idx * 2;
let neg_face = axis_idx * 2 + 1;
if face_pos[pos_face].is_nan() && face_pos[neg_face].is_nan() {
continue;
}
if !face_pos[pos_face].is_nan() && !face_pos[neg_face].is_nan() {
let hi = face_pos[pos_face].max(face_pos[neg_face]);
let lo = face_pos[pos_face].min(face_pos[neg_face]);
match axis_idx {
0 => center.x = (hi + lo) / 2.0,
1 => center.y = (hi + lo) / 2.0,
_ => center.z = (hi + lo) / 2.0,
}
half[axis_idx] = (hi - lo) / 2.0;
axes_found += 1;
} else {
let all_pts: Vec<usize> = face_points.iter().flat_map(|f| f.iter().copied()).collect();
let (lo, hi) = extent_along_axis(&all_pts, samples, axis_idx);
if hi - lo < 1e-6 { continue; }
match axis_idx {
0 => center.x = (hi + lo) / 2.0,
1 => center.y = (hi + lo) / 2.0,
_ => center.z = (hi + lo) / 2.0,
}
half[axis_idx] = (hi - lo) / 2.0;
axes_found += 1;
}
}
if axes_found < 2 || half[0] < 1e-6 || half[1] < 1e-6 || half[2] < 1e-6 {
// Estimate missing dimensions from overall extent
for axis_idx in 0..3 {
if half[axis_idx] >= 1e-6 { continue; }
let (lo, hi) = extent_along_axis(active, samples, axis_idx);
if hi - lo < 1e-6 { return None; }
match axis_idx {
0 => center.x = (hi + lo) / 2.0,
1 => center.y = (hi + lo) / 2.0,
_ => center.z = (hi + lo) / 2.0,
}
half[axis_idx] = (hi - lo) / 2.0;
}
}
if half[0] < 1e-6 || half[1] < 1e-6 || half[2] < 1e-6 {
return None;
}
let mut support = Vec::new();
let mut total_error = 0.0;
let thresh = config.distance_threshold * 2.0;
for &idx in active {
if idx >= samples.len() { continue; }
let p = samples[idx].position;
let local = Vec3::new(
(p.x - center.x).abs(),
(p.y - center.y).abs(),
(p.z - center.z).abs(),
);
let q = Vec3::new(local.x - half[0], local.y - half[1], local.z - half[2]);
let outside = Vec3::new(q.x.max(0.0), q.y.max(0.0), q.z.max(0.0));
let inside_dist = q.x.max(q.y).max(q.z).min(0.0);
let dist = (outside.length() + inside_dist).abs();
if dist < thresh {
support.push(idx);
total_error += dist;
}
}
let min_support = (active.len() as f64 * 0.3).max(6.0) as usize;
if support.len() < min_support {
return None;
}
Some(DetectedPrimitive {
kind: PrimitiveKind::Box {
center,
half_extents: half,
rotation_axis: Vec3::new(1.0, 0.0, 0.0),
rotation_angle: 0.0,
},
support,
fit_error: total_error,
})
}
fn extent_along_axis(points: &[usize], samples: &[SurfaceSample], axis: usize) -> (f64, f64) {
let mut lo = f64::MAX;
let mut hi = f64::MIN;
for &idx in points {
let p = samples[idx].position;
let v = match axis { 0 => p.x, 1 => p.y, _ => p.z };
lo = lo.min(v);
hi = hi.max(v);
}
(lo, hi)
}
fn project_onto_plane(v: Vec3, normal: Vec3) -> Vec3 {
v - normal * v.dot(normal)
}
fn closest_point_two_rays(p0: Vec3, d0: Vec3, p1: Vec3, d1: Vec3) -> Option<Vec3> {
let w0 = p0 - p1;
let a = d0.dot(d0);
let b = d0.dot(d1);
let c = d1.dot(d1);
let d = d0.dot(w0);
let e = d1.dot(w0);
let denom = a * c - b * b;
if denom.abs() < 1e-10 {
return None;
}
let t = (b * e - c * d) / denom;
let s = (a * e - b * d) / denom;
let closest_on_0 = p0 + d0 * t;
let closest_on_1 = p1 + d1 * s;
Some(Vec3 {
x: (closest_on_0.x + closest_on_1.x) * 0.5,
y: (closest_on_0.y + closest_on_1.y) * 0.5,
z: (closest_on_0.z + closest_on_1.z) * 0.5,
})
}
/// Xorshift64 RNG for RANSAC sampling.
struct SimpleRng {
state: u64,
}
impl SimpleRng {
fn new(seed: u64) -> Self {
Self { state: seed.wrapping_add(1) }
}
fn next_u64(&mut self) -> u64 {
self.state ^= self.state << 13;
self.state ^= self.state >> 7;
self.state ^= self.state << 17;
self.state
}
fn next_usize(&mut self, max: usize) -> usize {
(self.next_u64() % max as u64) as usize
}
}