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, 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 { let samples = grid.surface_samples(); if samples.is_empty() { return Vec::new(); } let mut remaining: Vec = 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, 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 = 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, 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 = 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> { 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> = 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 { let active: Vec = remaining.iter().enumerate() .filter(|(_, &r)| r) .map(|(i, _)| i) .collect(); if active.len() < 3 { return None; } let mut best: Option = 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 { 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 { 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 { 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 = 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 { 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 { 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; 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 = 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::() / vals.len() as f64; let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::() / 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 = 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 = 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 { if active.len() < 6 { return None; } let normal_threshold = 0.85; let mut face_points: [Vec; 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 = 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::() / vals.len() as f64; let variance = vals.iter().map(|v| (v - mean).powi(2)).sum::() / 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 = 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 { 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 } }