use crate::{ PB_Command, PB_Face32, PB_KeyValuePair, PB_Matrix4x432, PB_Model32, PB_Reply, PB_Vertex32, TBError, }; use macaw::Vec3; use saft::BoundingBox; use std::collections::HashMap; use std::time; /// unpack the input PB_Model #[allow(clippy::type_complexity)] fn parse_input_pb_model(obj: &PB_Model32) -> Result<(Vec<(u32, u32)>, BoundingBox), TBError> { if obj.vertices.len() >= u32::MAX as usize { return Err(TBError::Overflow(format!( "Input data contains too many vertices. {}", obj.vertices.len() ))); } let mut aabb = BoundingBox::default(); for v in obj.vertices.iter() { aabb.extend(Vec3::new(v.x, v.y, v.z)); } let mut edges = Vec::<(u32, u32)>::with_capacity(obj.vertices.len() + 100); for face in obj.faces.iter() { if face.vertices.len() > 2 { return Err(TBError::ModelContainsFaces("Model can't contain any faces, only edges. Use the 2d_outline tool to remove faces".to_string())); } if face.vertices.len() < 2 { return Err(TBError::InvalidInputData( "Edge containing none or only one vertex".to_string(), )); }; edges.push(( *face.vertices.first().unwrap(), *face.vertices.last().unwrap(), )); } Ok((edges, aabb)) } /// initialize the sdf capsules and generate the mesh fn build_voxel( radius_multiplier: f32, divisions: f32, vertices: &[PB_Vertex32], edges: Vec<(u32, u32)>, aabb: BoundingBox, verbose: bool, ) -> Result< ( f32, // <- voxel_size saft::TriangleMesh, ), TBError, > { let dimensions = aabb.max - aabb.min; let max_dimension = dimensions.x.max(dimensions.y).max(dimensions.z); let radius = max_dimension * radius_multiplier; // unscaled let thickness = radius * 2.0; // unscaled let scale = (divisions / max_dimension) as f32; if verbose { println!( "Voxelizing using tube thickness. {} = {}*{}*{}", thickness, max_dimension, radius_multiplier, scale ); println!( "Voxelizing using divisions = {}, max dimension = {}, scale factor={} (max_dimension*scale={})", divisions, max_dimension, scale, (max_dimension as f32) * scale ); println!(); println!("aabb.high:{:?}", aabb.max); println!("aabb.low:{:?}", aabb.min); println!("delta:{:?}", aabb.max - aabb.min); } let mean_resolution = max_dimension * scale; if verbose { println!("mean_resolution:{:?}", mean_resolution); } let mesh_options = saft::MeshOptions { mean_resolution, max_resolution: mean_resolution, min_resolution: 8.0, }; let vertices: Vec<_> = vertices .iter() .map(|v| Vec3::new(v.x, v.y, v.z) * scale) .collect(); let now = time::Instant::now(); let mut graph = saft::Graph::default(); let radius = radius * scale; // now scaled let capsules: Vec<_> = edges .into_iter() .map(|(e0, e1)| graph.capsule([vertices[e0 as usize], vertices[e1 as usize]], radius)) .collect(); let root = graph.op_union_multi(capsules); let mesh = saft::mesh_from_sdf(&graph, root, mesh_options)?; if verbose { println!("mesh_from_sdf() duration: {:?}", now.elapsed()); } Ok((1.0 / scale, mesh)) } /// Build the return model, totally ignore colors pub(crate) fn build_output_bp_model( pb_model_name: String, pb_world: Option, voxel_size: f32, mesh: saft::TriangleMesh, ) -> Result { let pb_vertices: Vec = mesh .positions .iter() .map(|v| PB_Vertex32 { x: (voxel_size * v[0]), y: (voxel_size * v[1]), z: (voxel_size * v[2]), }) .collect(); Ok(PB_Model32 { name: pb_model_name, world_orientation: pb_world, vertices: pb_vertices, faces: vec![PB_Face32 { vertices: mesh.indices, }], }) } /// Run the saft_voxel command pub(crate) fn command( a_command: PB_Command, options: HashMap, verbose: bool, ) -> Result { let now = time::Instant::now(); if verbose { println!( r#" _________ _____ __ ____ ____ .__ / _____/____ _/ ____\/ |\ \ / /______ ___ ____ | | \_____ \\__ \\ __\\ __\ Y / _ \ \/ // __ \| | / \/ __ \| | | | \ ( <_> > <\ ___/| |__ /_______ (____ /__| |__| \___/ \____/__/\_ \\___ >____/ \/ \/ \/ \/"# ); } if a_command.models32.len() > 1 { return Err(TBError::InvalidInputData(format!( "This operation only supports one model as input:{}", a_command.models32.len() ))); } if a_command.models32.is_empty() { return Err(TBError::InvalidInputData( "Model did not contain any data (using model32)".to_string(), )); } let cmd_arg_radius_multiplier = options .get("RADIUS") .ok_or_else(|| TBError::InvalidInputData("Missing the RADIUS parameter".to_string()))? .parse::() .map_err(|_| { TBError::InvalidInputData("Could not parse the RADIUS parameter".to_string()) })? / 100.0; let cmd_arg_divisions = options .get("DIVISIONS") .ok_or_else(|| TBError::InvalidInputData("Missing the DIVISIONS parameter".to_string()))? .parse::() .map_err(|_| { TBError::InvalidInputData("Could not parse the DIVISIONS parameter".to_string()) })?; if !(9.9..400.1).contains(&cmd_arg_divisions) { return Err(TBError::InvalidInputData(format!( "The valid range of DIVISIONS is [{}..{}[% :({})", 10, 400, cmd_arg_divisions ))); } if verbose { for model in a_command.models32.iter() { println!("model.name:{:?}, ", model.name); println!("model.vertices:{:?}, ", model.vertices.len()); println!("model.faces:{:?}, ", model.faces.len()); println!( "model.world_orientation:{:?}, ", model.world_orientation.as_ref().map_or(0, |_| 16) ); println!("Tube radius:{:?} multiplier ", cmd_arg_radius_multiplier); println!("Voxel divisions:{:?} ", cmd_arg_divisions); println!(); } } let (edges, aabb) = parse_input_pb_model(&a_command.models32[0])?; let (voxel_size, mesh) = build_voxel( cmd_arg_radius_multiplier, cmd_arg_divisions, &a_command.models32[0].vertices, edges, aabb, verbose, )?; let packed_faces_model = build_output_bp_model( a_command.command.clone(), a_command.models32[0].world_orientation.clone(), voxel_size, mesh, )?; if verbose { println!( "Total number of vertices: {}", packed_faces_model.vertices.len() ); println!( "Total number of triangles: {}", packed_faces_model .faces .iter() .map(|x| x.vertices.len()) .sum::() / 3 ); } let reply = PB_Reply { options: vec![ PB_KeyValuePair { key: "ONLY_EDGES".to_string(), value: "False".to_string(), }, PB_KeyValuePair { key: "PACKED_FACES".to_string(), value: "True".to_string(), }, ], models: Vec::with_capacity(0), models32: vec![packed_faces_model], }; if verbose { println!("total duration: {:?}", now.elapsed()); } Ok(reply) }