1use std::time::Instant;
4use tracing::{debug, info, warn};
5
6use mesh_repair::Mesh;
7
8use crate::error::{ShellError, ShellResult};
9
10use super::adaptive::{AdaptiveSdfParams, create_adaptive_grid, interpolate_offsets_adaptive};
11use super::extract::extract_isosurface;
12use super::grid::{SdfGrid, SdfOffsetParams};
13use super::transfer::transfer_vertex_data;
14
15#[derive(Debug, Clone)]
17pub struct SdfOffsetStats {
18 pub grid_dims: [usize; 3],
20 pub total_voxels: usize,
22 pub sdf_time_ms: u64,
24 pub extraction_time_ms: u64,
26 pub transfer_time_ms: u64,
28 pub input_vertices: usize,
30 pub output_vertices: usize,
32 pub output_faces: usize,
34 pub adaptive_resolution: bool,
36 pub memory_savings_percent: f64,
38}
39
40#[derive(Debug)]
42pub struct SdfOffsetResult {
43 pub mesh: Mesh,
45 pub stats: SdfOffsetStats,
47}
48
49pub fn apply_sdf_offset(mesh: &Mesh, params: &SdfOffsetParams) -> ShellResult<SdfOffsetResult> {
69 let total_start = Instant::now();
70
71 if mesh.vertices.is_empty() {
72 return Err(ShellError::EmptyMesh);
73 }
74
75 let missing_offset = mesh.vertices.iter().filter(|v| v.offset.is_none()).count();
77
78 if missing_offset > 0 {
79 warn!(
80 missing = missing_offset,
81 total = mesh.vertices.len(),
82 "Some vertices missing offset values, using 0.0"
83 );
84 }
85
86 let input_vertices = mesh.vertices.len();
87
88 info!(
89 vertices = input_vertices,
90 faces = mesh.faces.len(),
91 voxel_size_mm = params.voxel_size_mm,
92 padding_mm = params.padding_mm,
93 adaptive = params.adaptive_resolution,
94 "Starting SDF offset"
95 );
96
97 if params.adaptive_resolution {
99 apply_sdf_offset_adaptive(mesh, params, input_vertices, total_start)
100 } else {
101 apply_sdf_offset_standard(mesh, params, input_vertices, total_start)
102 }
103}
104
105fn apply_sdf_offset_standard(
107 mesh: &Mesh,
108 params: &SdfOffsetParams,
109 input_vertices: usize,
110 total_start: Instant,
111) -> ShellResult<SdfOffsetResult> {
112 let mut grid = SdfGrid::from_mesh_bounds(
114 mesh,
115 params.voxel_size_mm,
116 params.padding_mm,
117 params.max_voxels,
118 )?;
119
120 info!(
121 dims = ?grid.dims,
122 total_voxels = grid.total_voxels(),
123 "Grid created (standard)"
124 );
125
126 let sdf_start = Instant::now();
128 grid.compute_sdf(mesh);
129 let sdf_time_ms = sdf_start.elapsed().as_millis() as u64;
130
131 debug!(sdf_time_ms, "SDF computation complete");
132
133 grid.interpolate_offsets(mesh, params.offset_neighbors);
135
136 grid.apply_variable_offset();
138
139 let extract_start = Instant::now();
141 let mut output_mesh = extract_isosurface(&grid)?;
142 let extraction_time_ms = extract_start.elapsed().as_millis() as u64;
143
144 debug!(
145 extraction_time_ms,
146 vertices = output_mesh.vertices.len(),
147 faces = output_mesh.faces.len(),
148 "Surface extraction complete"
149 );
150
151 let transfer_start = Instant::now();
153 transfer_vertex_data(mesh, &mut output_mesh)?;
154 let transfer_time_ms = transfer_start.elapsed().as_millis() as u64;
155
156 debug!(transfer_time_ms, "Vertex data transfer complete");
157
158 let total_time_ms = total_start.elapsed().as_millis();
159
160 let stats = SdfOffsetStats {
161 grid_dims: grid.dims,
162 total_voxels: grid.total_voxels(),
163 sdf_time_ms,
164 extraction_time_ms,
165 transfer_time_ms,
166 input_vertices,
167 output_vertices: output_mesh.vertices.len(),
168 output_faces: output_mesh.faces.len(),
169 adaptive_resolution: false,
170 memory_savings_percent: 0.0,
171 };
172
173 info!(
174 total_time_ms,
175 input_vertices = stats.input_vertices,
176 output_vertices = stats.output_vertices,
177 output_faces = stats.output_faces,
178 "SDF offset complete (standard)"
179 );
180
181 Ok(SdfOffsetResult {
182 mesh: output_mesh,
183 stats,
184 })
185}
186
187fn apply_sdf_offset_adaptive(
189 mesh: &Mesh,
190 params: &SdfOffsetParams,
191 input_vertices: usize,
192 total_start: Instant,
193) -> ShellResult<SdfOffsetResult> {
194 let adaptive_params = AdaptiveSdfParams {
196 fine_voxel_size_mm: params.voxel_size_mm,
197 coarse_voxel_size_mm: params.coarse_voxel_size_mm(),
198 refinement_distance_mm: params.refinement_distance_mm,
199 padding_mm: params.padding_mm,
200 max_voxels: params.max_voxels,
201 offset_neighbors: params.offset_neighbors,
202 };
203
204 let sdf_start = Instant::now();
206 let adaptive_result = create_adaptive_grid(mesh, &adaptive_params)?;
207 let mut grid = adaptive_result.grid;
208 let adaptive_stats = adaptive_result.stats;
209 let sdf_time_ms = sdf_start.elapsed().as_millis() as u64;
210
211 info!(
212 dims = ?grid.dims,
213 total_voxels = grid.total_voxels(),
214 coarse_voxels = adaptive_stats.coarse_voxels,
215 refined_voxels = adaptive_stats.refined_coarse_voxels,
216 memory_savings = format!("{:.1}%", adaptive_stats.memory_savings_percent),
217 "Adaptive grid created"
218 );
219
220 debug!(sdf_time_ms, "Adaptive SDF computation complete");
221
222 interpolate_offsets_adaptive(&mut grid, mesh, &adaptive_params);
224
225 grid.apply_variable_offset();
227
228 let extract_start = Instant::now();
230 let mut output_mesh = extract_isosurface(&grid)?;
231 let extraction_time_ms = extract_start.elapsed().as_millis() as u64;
232
233 debug!(
234 extraction_time_ms,
235 vertices = output_mesh.vertices.len(),
236 faces = output_mesh.faces.len(),
237 "Surface extraction complete"
238 );
239
240 let transfer_start = Instant::now();
242 transfer_vertex_data(mesh, &mut output_mesh)?;
243 let transfer_time_ms = transfer_start.elapsed().as_millis() as u64;
244
245 debug!(transfer_time_ms, "Vertex data transfer complete");
246
247 let total_time_ms = total_start.elapsed().as_millis();
248
249 let stats = SdfOffsetStats {
250 grid_dims: grid.dims,
251 total_voxels: grid.total_voxels(),
252 sdf_time_ms,
253 extraction_time_ms,
254 transfer_time_ms,
255 input_vertices,
256 output_vertices: output_mesh.vertices.len(),
257 output_faces: output_mesh.faces.len(),
258 adaptive_resolution: true,
259 memory_savings_percent: adaptive_stats.memory_savings_percent,
260 };
261
262 info!(
263 total_time_ms,
264 input_vertices = stats.input_vertices,
265 output_vertices = stats.output_vertices,
266 output_faces = stats.output_faces,
267 memory_savings = format!("{:.1}%", stats.memory_savings_percent),
268 "SDF offset complete (adaptive)"
269 );
270
271 Ok(SdfOffsetResult {
272 mesh: output_mesh,
273 stats,
274 })
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use mesh_repair::Vertex;
281
282 fn create_unit_cube() -> Mesh {
283 let mut mesh = Mesh::new();
284
285 for z in [0.0, 10.0] {
287 for y in [0.0, 10.0] {
288 for x in [0.0, 10.0] {
289 let mut v = Vertex::from_coords(x, y, z);
290 v.offset = Some(1.0); v.tag = Some(1);
292 mesh.vertices.push(v);
293 }
294 }
295 }
296
297 mesh.faces.push([0, 1, 3]);
299 mesh.faces.push([0, 3, 2]);
300 mesh.faces.push([4, 7, 5]);
301 mesh.faces.push([4, 6, 7]);
302 mesh.faces.push([0, 5, 1]);
303 mesh.faces.push([0, 4, 5]);
304 mesh.faces.push([2, 3, 7]);
305 mesh.faces.push([2, 7, 6]);
306 mesh.faces.push([0, 2, 6]);
307 mesh.faces.push([0, 6, 4]);
308 mesh.faces.push([1, 5, 7]);
309 mesh.faces.push([1, 7, 3]);
310
311 mesh
312 }
313
314 #[test]
315 fn test_sdf_offset_cube() {
316 let mesh = create_unit_cube();
317
318 let params = SdfOffsetParams {
319 voxel_size_mm: 1.0,
320 padding_mm: 5.0,
321 max_voxels: 1_000_000,
322 offset_neighbors: 4,
323 adaptive_resolution: false,
324 coarse_voxel_multiplier: 4.0,
325 refinement_distance_mm: 5.0,
326 use_gpu: false,
327 };
328
329 let result = apply_sdf_offset(&mesh, ¶ms).unwrap();
330
331 assert!(!result.mesh.vertices.is_empty());
333 assert!(!result.mesh.faces.is_empty());
334 assert!(!result.stats.adaptive_resolution);
335
336 let input_bounds = mesh.bounds().unwrap();
338 let output_bounds = result.mesh.bounds().unwrap();
339
340 let input_extent = input_bounds.1 - input_bounds.0;
341 let output_extent = output_bounds.1 - output_bounds.0;
342
343 assert!(
345 output_extent.x > input_extent.x,
346 "Output should be wider: {} vs {}",
347 output_extent.x,
348 input_extent.x
349 );
350 }
351
352 #[test]
353 fn test_sdf_offset_cube_adaptive() {
354 let mesh = create_unit_cube();
355
356 let params = SdfOffsetParams::adaptive();
357
358 let result = apply_sdf_offset(&mesh, ¶ms).unwrap();
359
360 assert!(!result.mesh.vertices.is_empty());
362 assert!(!result.mesh.faces.is_empty());
363 assert!(result.stats.adaptive_resolution);
364
365 let input_bounds = mesh.bounds().unwrap();
367 let output_bounds = result.mesh.bounds().unwrap();
368
369 let input_extent = input_bounds.1 - input_bounds.0;
370 let output_extent = output_bounds.1 - output_bounds.0;
371
372 assert!(
374 output_extent.x > input_extent.x,
375 "Adaptive output should be wider: {} vs {}",
376 output_extent.x,
377 input_extent.x
378 );
379 }
380
381 #[test]
382 fn test_sdf_offset_adaptive_presets() {
383 let mesh = create_unit_cube();
384
385 for params in [
387 SdfOffsetParams::adaptive(),
388 SdfOffsetParams::adaptive_high_quality(),
389 SdfOffsetParams::adaptive_large_mesh(),
390 ] {
391 let result = apply_sdf_offset(&mesh, ¶ms).unwrap();
392 assert!(!result.mesh.vertices.is_empty());
393 assert!(result.stats.adaptive_resolution);
394 }
395 }
396
397 #[test]
398 fn test_sdf_offset_empty_mesh() {
399 let mesh = Mesh::new();
400 let params = SdfOffsetParams::default();
401
402 let result = apply_sdf_offset(&mesh, ¶ms);
403 assert!(result.is_err());
404 }
405}