//! Native sculpt-map sampling and topology generation. #![allow(clippy::cast_precision_loss)] // Pixel and grid coordinates are converted to C# Single. #![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map. #![allow(clippy::must_use_candidate)] // Attributes are not part of the mapped C# surface. #![allow(clippy::needless_pass_by_value)] // Owned images and rows mirror mapped signatures. #![allow(clippy::too_many_arguments)] // File and image constructors have fixed signatures. #![allow(clippy::too_many_lines)] // Topology construction follows the golden sequence directly. use crate::{Coord, Error, Face, Quat, SculptMeshSculptType, UVCoord, ViewerFace}; use libremetaverse_imaging::{ DEFAULT_MAX_ENCODED_BYTES, DEFAULT_MAX_PIXELS, ITextureCodec, ManagedImage, ManagedImageImageChannels, }; use std::fs::File; const MAX_SCULPT_AXIS: usize = 4_096; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct SculptMap { pub blue_bytes: Vec, pub green_bytes: Vec, pub height: i32, pub red_bytes: Vec, pub width: i32, } impl SculptMap { pub fn new_with_constructor() -> Result { Ok(Self::default()) } pub fn new_with_managed_image_int32(image: ManagedImage, lod: i32) -> Result { image.validate()?; if lod <= 0 { return Err(Error::Argument); } let original_width = usize::try_from(image.width).map_err(|_| Error::Argument)?; let original_height = usize::try_from(image.height).map_err(|_| Error::Argument)?; let lod = usize::try_from(lod).map_err(|_| Error::Argument)?; let lod_pixels = lod.checked_mul(lod).ok_or(Error::Argument)?; let budget_side = lod.checked_mul(2).ok_or(Error::Argument)?; let budget_pixels = budget_side .checked_mul(budget_side) .ok_or(Error::Argument)?; let original_pixels = original_width .checked_mul(original_height) .ok_or(Error::Argument)?; let small_map = original_pixels <= lod_pixels; let mut width = original_width; let mut height = original_height; let mut needs_scaling = false; while width.checked_mul(height).ok_or(Error::Argument)? > budget_pixels { width >>= 1; height >>= 1; needs_scaling = true; if width == 0 || height == 0 { return Err(Error::Argument); } } let mut scaled; let source = if needs_scaling { scaled = image.clone()?; scaled.resize_bilinear(to_i32(width)?, to_i32(height)?)?; &scaled } else { &image }; if width.checked_mul(height).ok_or(Error::Argument)? > lod_pixels { width >>= 1; height >>= 1; if width == 0 || height == 0 { return Err(Error::Argument); } } let output_width = width .checked_add(usize::from(!small_map)) .ok_or(Error::Argument)?; let output_height = height .checked_add(usize::from(!small_map)) .ok_or(Error::Argument)?; let bytes = output_width .checked_mul(output_height) .ok_or(Error::Argument)?; if bytes > DEFAULT_MAX_PIXELS || output_width > MAX_SCULPT_AXIS || output_height > MAX_SCULPT_AXIS { return Err(Error::Argument); } let mut result = Self { blue_bytes: vec![0; bytes], green_bytes: vec![0; bytes], height: to_i32(output_height)?, red_bytes: vec![0; bytes], width: to_i32(output_width)?, }; let mut output = 0usize; if small_map { for y in 0..height { for x in 0..width { let (red, green, blue) = sample_pixel(source, x, y)?; result.red_bytes[output] = red; result.green_bytes[output] = green; result.blue_bytes[output] = blue; output += 1; } } } else { for y in 0..=height { let source_y = if y < height { y.checked_mul(2) } else { y.checked_mul(2).and_then(|n| n.checked_sub(1)) } .ok_or(Error::Argument)?; for x in 0..=width { let source_x = if x < width { x.checked_mul(2) } else { x.checked_mul(2).and_then(|n| n.checked_sub(1)) } .ok_or(Error::Argument)?; let (red, green, blue) = sample_pixel(source, source_x, source_y)?; result.red_bytes[output] = red; result.green_bytes[output] = green; result.blue_bytes[output] = blue; output += 1; } } } Ok(result) } pub fn to_rows(&self, mirror: bool) -> Result>, Error> { let width = usize::try_from(self.width).map_err(|_| Error::Argument)?; let height = usize::try_from(self.height).map_err(|_| Error::Argument)?; let pixels = width.checked_mul(height).ok_or(Error::Argument)?; if width == 0 || height == 0 || pixels > DEFAULT_MAX_PIXELS || self.red_bytes.len() != pixels || self.green_bytes.len() != pixels || self.blue_bytes.len() != pixels { return Err(Error::InvalidOperation); } let mut rows = Vec::with_capacity(height); for y in 0..height { let mut row = Vec::with_capacity(width); for x in 0..width { let index = y .checked_mul(width) .and_then(|n| n.checked_add(x)) .ok_or(Error::Argument)?; let red = f32::from(self.red_bytes[index]) / 255.0 - 0.5; let green = f32::from(self.green_bytes[index]) / 255.0 - 0.5; let blue = f32::from(self.blue_bytes[index]) / 255.0 - 0.5; row.push(Coord { x: if mirror { -red } else { red }, y: green, z: blue, }); } rows.push(row); } Ok(rows) } } fn sample_pixel(image: &ManagedImage, x: usize, y: usize) -> Result<(u8, u8, u8), Error> { let width = usize::try_from(image.width).map_err(|_| Error::Argument)?; let height = usize::try_from(image.height).map_err(|_| Error::Argument)?; if x >= width || y >= height { return Err(Error::IndexOutOfRange); } let index = y .checked_mul(width) .and_then(|n| n.checked_add(x)) .ok_or(Error::Argument)?; let red = *image.red.get(index).ok_or(Error::IndexOutOfRange)?; if image.channels.contains(ManagedImageImageChannels::COLOR) && image.green.len() == width * height && image.blue.len() == width * height { Ok((red, image.green[index], image.blue[index])) } else { Ok((red, red, red)) } } #[derive(Clone, Debug, Default, PartialEq)] pub struct SculptMesh { pub coords: Vec, pub faces: Vec, pub normals: Vec, pub uvs: Vec, pub viewer_faces: Vec, } impl SculptMesh { pub fn new_with_managed_image_sculpt_type_int32_boolean( image: ManagedImage, sculpt_type: SculptMeshSculptType, lod: i32, viewer_mode: bool, ) -> Result { Self::new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean( image, sculpt_type, lod, viewer_mode, false, false, ) } pub fn new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean( image: ManagedImage, sculpt_type: SculptMeshSculptType, lod: i32, viewer_mode: bool, mirror: bool, invert: bool, ) -> Result { let rows = SculptMap::new_with_managed_image_int32(image, lod)?.to_rows(mirror)?; Self::new_with_list_sculpt_type_boolean_boolean_boolean( rows, sculpt_type, viewer_mode, mirror, invert, ) } pub fn new_with_sculpt_mesh(mesh: Self) -> Result { Ok(mesh.clone()) } pub fn new_with_list_sculpt_type_boolean_boolean_boolean( mut rows: Vec>, sculpt_type: SculptMeshSculptType, viewer_mode: bool, mirror: bool, mut invert: bool, ) -> Result { validate_rows(&rows)?; if mirror { invert = !invert; } let original_width = rows[0].len(); if sculpt_type != SculptMeshSculptType::Plane { if rows.len().is_multiple_of(2) { for row in &mut rows { row.push(row[0]); } } else { for row in &mut rows { row[0] = row[original_width - 1]; } } } let top_pole = rows[0][original_width / 2]; let bottom_pole = rows[rows.len() - 1][original_width / 2]; if sculpt_type == SculptMeshSculptType::Sphere { if rows.len().is_multiple_of(2) { let count = rows[0].len(); rows.insert(0, vec![top_pole; count]); rows.push(vec![bottom_pole; count]); } else { for coord in &mut rows[0] { *coord = top_pole; } let last = rows.len() - 1; for coord in &mut rows[last] { *coord = bottom_pole; } } } if sculpt_type == SculptMeshSculptType::Torus { rows.push(rows[0].clone()); } validate_rows(&rows)?; let height = rows.len(); let width = rows[0].len(); let vertices = width.checked_mul(height).ok_or(Error::Argument)?; if vertices > DEFAULT_MAX_PIXELS { return Err(Error::Argument); } let face_count = width .checked_sub(1) .and_then(|x| height.checked_sub(1).and_then(|y| x.checked_mul(y))) .and_then(|n| n.checked_mul(2)) .ok_or(Error::Argument)?; let mut mesh = Self { coords: Vec::with_capacity(vertices), faces: Vec::with_capacity(face_count), normals: if viewer_mode { vec![Coord::default(); vertices] } else { Vec::new() }, uvs: if viewer_mode { Vec::with_capacity(vertices) } else { Vec::new() }, viewer_faces: if viewer_mode { Vec::with_capacity(face_count) } else { Vec::new() }, }; let width_unit = 1.0 / (width - 1) as f32; let height_unit = 1.0 / (height - 1) as f32; for (y, row) in rows.iter().enumerate() { for (x, coord) in row.iter().copied().enumerate() { mesh.coords.push(coord); if viewer_mode { mesh.uvs.push(UVCoord { u: width_unit * x as f32, v: height_unit * y as f32, }); } if y > 0 && x > 0 { let p4 = y .checked_mul(width) .and_then(|n| n.checked_add(x)) .ok_or(Error::Argument)?; let p3 = p4 - 1; let p2 = p4 - width; let p1 = p3 - width; let (a, b) = if invert { ((p1, p4, p3), (p1, p2, p4)) } else { ((p1, p3, p4), (p1, p4, p2)) }; mesh.faces.push(make_face(a, viewer_mode)?); mesh.faces.push(make_face(b, viewer_mode)?); } } } if viewer_mode { mesh.calculate_vertex_normals(sculpt_type, width, height)?; } mesh.validate()?; Ok(mesh) } pub fn new_with_single_array_single_single_single_single_boolean( z_map: Vec, x_begin: f32, x_end: f32, y_begin: f32, y_end: f32, viewer_mode: bool, ) -> Result { if z_map.iter().any(|value| !value.is_finite()) { return Err(Error::Argument); } let side = z_map.len().isqrt(); if side < 2 || side.checked_mul(side) != Some(z_map.len()) { return Err(Error::Argument); } let rows: Vec> = z_map.chunks_exact(side).map(<[f32]>::to_vec).collect(); Self::from_height_rows(rows, x_begin, x_end, y_begin, y_end, viewer_mode) } /// Builds the full rectangular form of the C# two-dimensional height-map /// constructor. The generated compatibility signature is flattened, so /// that entry point accepts square maps and delegates here. pub fn from_height_rows( z_map: Vec>, x_begin: f32, x_end: f32, y_begin: f32, y_end: f32, viewer_mode: bool, ) -> Result { let height = z_map.len(); let width = z_map.first().map(Vec::len).ok_or(Error::Argument)?; if width < 2 || height < 2 || width > MAX_SCULPT_AXIS || height > MAX_SCULPT_AXIS || z_map.iter().any(|row| row.len() != width) || z_map.iter().flatten().any(|value| !value.is_finite()) { return Err(Error::Argument); } let x_step = (x_end - x_begin) / (width - 1) as f32; let y_step = (y_end - y_begin) / (height - 1) as f32; let rows: Vec> = (0..height) .map(|y| { (0..width) .map(|x| Coord { x: x_begin + x as f32 * x_step, y: y_begin + y as f32 * y_step, z: z_map[y][x], }) .collect() }) .collect(); let mut mesh = Self::new_with_list_sculpt_type_boolean_boolean_boolean( rows, SculptMeshSculptType::Plane, viewer_mode, false, true, )?; if viewer_mode { for uv in &mut mesh.uvs { uv.v = 1.0 - uv.v; } for face in &mut mesh.viewer_faces { face.uv1.v = 1.0 - face.uv1.v; face.uv2.v = 1.0 - face.uv2.v; face.uv3.v = 1.0 - face.uv3.v; } } Ok(mesh) } pub fn new_with_string_i_texture_codec_int32_int32_int32_int32_int32( file_name: String, codec: Box, sculpt_type: i32, lod: i32, viewer_mode: i32, mirror: i32, invert: i32, ) -> Result { let kind = sculpt_type_from_i32(sculpt_type)?; let image = decode_file(&file_name, codec.as_ref())?; Self::new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean( image, kind, lod, viewer_mode != 0, mirror != 0, invert != 0, ) } pub fn sculpt_mesh_from_file( &self, file_name: String, codec: Box, sculpt_type: SculptMeshSculptType, lod: i32, viewer_mode: bool, ) -> Result { let image = decode_file(&file_name, codec.as_ref())?; Self::new_with_managed_image_sculpt_type_int32_boolean(image, sculpt_type, lod, viewer_mode) } fn calculate_vertex_normals( &mut self, sculpt_type: SculptMeshSculptType, width: usize, height: usize, ) -> Result<(), Error> { for face in &self.faces { let normal = face_normal(&self.coords, *face)?; for index in [face.n1, face.n2, face.n3] { let index = checked_index(index, self.normals.len())?; self.normals[index] = Coord::add(self.normals[index], normal); } } for normal in &mut self.normals { normal.normalize()?; } if sculpt_type != SculptMeshSculptType::Plane { for y in 0..height { let first = y.checked_mul(width).ok_or(Error::Argument)?; let last = first.checked_add(width - 1).ok_or(Error::Argument)?; let mut normal = Coord::add(self.normals[first], self.normals[last]); normal.normalize()?; self.normals[first] = normal; self.normals[last] = normal; } } for face in &self.faces { let mut viewer = ViewerFace::new(0)?; viewer.v1 = self.coords[checked_index(face.v1, self.coords.len())?]; viewer.v2 = self.coords[checked_index(face.v2, self.coords.len())?]; viewer.v3 = self.coords[checked_index(face.v3, self.coords.len())?]; viewer.coord_index1 = face.v1; viewer.coord_index2 = face.v2; viewer.coord_index3 = face.v3; viewer.n1 = self.normals[checked_index(face.n1, self.normals.len())?]; viewer.n2 = self.normals[checked_index(face.n2, self.normals.len())?]; viewer.n3 = self.normals[checked_index(face.n3, self.normals.len())?]; viewer.uv1 = self.uvs[checked_index(face.uv1, self.uvs.len())?]; viewer.uv2 = self.uvs[checked_index(face.uv2, self.uvs.len())?]; viewer.uv3 = self.uvs[checked_index(face.uv3, self.uvs.len())?]; self.viewer_faces.push(viewer); } Ok(()) } fn validate(&self) -> Result<(), Error> { if self .coords .iter() .any(|coord| !coord.x.is_finite() || !coord.y.is_finite() || !coord.z.is_finite()) || self .normals .iter() .any(|coord| !coord.x.is_finite() || !coord.y.is_finite() || !coord.z.is_finite()) || self .uvs .iter() .any(|uv| !uv.u.is_finite() || !uv.v.is_finite()) { return Err(Error::InvalidOperation); } for face in &self.faces { checked_index(face.v1, self.coords.len())?; checked_index(face.v2, self.coords.len())?; checked_index(face.v3, self.coords.len())?; } Ok(()) } pub fn copy(&self) -> Result { Ok(self.clone()) } pub fn add_pos(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> { for coord in &mut self.coords { coord.x += x; coord.y += y; coord.z += z; } for face in &mut self.viewer_faces { face.add_pos(x, y, z)?; } Ok(()) } pub fn add_rot(&mut self, q: Quat) -> Result<(), Error> { for coord in &mut self.coords { *coord = Coord::mul_with_coord_quat(*coord, q); } for normal in &mut self.normals { *normal = Coord::mul_with_coord_quat(*normal, q); } for face in &mut self.viewer_faces { face.add_rot(q)?; } Ok(()) } pub fn scale(&mut self, x: f32, y: f32, z: f32) -> Result<(), Error> { let multiplier = Coord { x, y, z }; for coord in &mut self.coords { *coord = Coord::mul_with_coord_coord(*coord, multiplier); } for face in &mut self.viewer_faces { face.scale(x, y, z)?; } Ok(()) } pub fn dump_raw(&self, path: String, name: String, title: String) -> Result<(), Error> { crate::prim_mesher::dump_raw_geometry(&self.coords, &self.faces, path, name, title) } } fn validate_rows(rows: &[Vec]) -> Result<(), Error> { let width = rows.first().map(Vec::len).ok_or(Error::Argument)?; if rows.len() < 2 || width < 2 || rows.len() > MAX_SCULPT_AXIS || width > MAX_SCULPT_AXIS { return Err(Error::Argument); } if rows.iter().any(|row| row.len() != width) || rows .iter() .flatten() .any(|coord| !coord.x.is_finite() || !coord.y.is_finite() || !coord.z.is_finite()) { return Err(Error::Argument); } Ok(()) } fn make_face(indices: (usize, usize, usize), viewer_mode: bool) -> Result { let (v1, v2, v3) = (to_i32(indices.0)?, to_i32(indices.1)?, to_i32(indices.2)?); Ok(if viewer_mode { Face { v1, v2, v3, n1: v1, n2: v2, n3: v3, uv1: v1, uv2: v2, uv3: v3, ..Face::default() } } else { Face { v1, v2, v3, ..Face::default() } }) } fn face_normal(coords: &[Coord], face: Face) -> Result { let first = coords[checked_index(face.v1, coords.len())?]; let second = coords[checked_index(face.v2, coords.len())?]; let third = coords[checked_index(face.v3, coords.len())?]; let edge1 = Coord { x: second.x - first.x, y: second.y - first.y, z: second.z - first.z, }; let edge2 = Coord { x: third.x - first.x, y: third.y - first.y, z: third.z - first.z, }; let mut normal = Coord::cross(edge1, edge2)?; normal.normalize()?; Ok(normal) } fn checked_index(index: i32, len: usize) -> Result { let index = usize::try_from(index).map_err(|_| Error::IndexOutOfRange)?; if index < len { Ok(index) } else { Err(Error::IndexOutOfRange) } } fn to_i32(value: usize) -> Result { i32::try_from(value).map_err(|_| Error::Argument) } fn sculpt_type_from_i32(value: i32) -> Result { match value & 0x07 { 1 => Ok(SculptMeshSculptType::Sphere), 2 => Ok(SculptMeshSculptType::Torus), 3 => Ok(SculptMeshSculptType::Plane), 4 => Ok(SculptMeshSculptType::Cylinder), _ => Err(Error::Argument), } } fn decode_file(path: &str, codec: &dyn ITextureCodec) -> Result { let file = File::open(path).map_err(|_| Error::InvalidOperation)?; let length = file.metadata().map_err(|_| Error::InvalidOperation)?.len(); if length > DEFAULT_MAX_ENCODED_BYTES as u64 { return Err(Error::Argument); } codec.decode(Box::new(file)) } #[cfg(test)] mod tests { use super::*; use libremetaverse_types::compat::ReadWrite; use std::fmt::Write as _; use std::fs; use std::io::Read; fn grid(width: usize, height: usize) -> Vec> { (0..height) .map(|y| { (0..width) .map(|x| Coord { x: x as f32, y: y as f32, z: (x + y) as f32 * 0.1, }) .collect() }) .collect() } #[test] fn sculpt_map_sampling_matches_reference_grid_and_gray_rules() { let mut image = ManagedImage::new(4, 4, ManagedImageImageChannels::COLOR).expect("color image"); for index in 0..16 { image.red[index] = u8::try_from(index).expect("sample"); image.green[index] = u8::try_from(index + 20).expect("sample"); image.blue[index] = u8::try_from(index + 40).expect("sample"); } let map = SculptMap::new_with_managed_image_int32(image, 2).expect("sculpt map"); assert_eq!((map.width, map.height), (3, 3)); assert_eq!(map.red_bytes, vec![0, 2, 3, 8, 10, 11, 12, 14, 15]); assert_eq!(map.green_bytes[4], 30); assert_eq!(map.blue_bytes[8], 55); let mut gray = ManagedImage::new(2, 2, ManagedImageImageChannels::GRAY).expect("gray image"); gray.red = vec![0, 64, 128, 255]; let map = SculptMap::new_with_managed_image_int32(gray, 4).expect("small map"); assert_eq!(map.red_bytes, map.green_bytes); assert_eq!(map.red_bytes, map.blue_bytes); let rows = map.to_rows(true).expect("mirrored rows"); assert!((rows[0][0].x - 0.5).abs() < f32::EPSILON); assert!((rows[1][1].z - 0.5).abs() < f32::EPSILON); } #[test] fn topology_modes_match_golden_counts_and_seams() { let plane = SculptMesh::new_with_list_sculpt_type_boolean_boolean_boolean( grid(3, 3), SculptMeshSculptType::Plane, true, false, false, ) .expect("plane"); assert_eq!( ( plane.coords.len(), plane.faces.len(), plane.normals.len(), plane.uvs.len(), plane.viewer_faces.len(), ), (9, 8, 9, 9, 8) ); let sphere = SculptMesh::new_with_list_sculpt_type_boolean_boolean_boolean( grid(4, 4), SculptMeshSculptType::Sphere, true, false, false, ) .expect("sphere"); assert_eq!((sphere.coords.len(), sphere.faces.len()), (30, 40)); assert_eq!(sphere.coords[0], sphere.coords[4]); assert_eq!(sphere.coords[25], sphere.coords[29]); let torus = SculptMesh::new_with_list_sculpt_type_boolean_boolean_boolean( grid(4, 4), SculptMeshSculptType::Torus, true, false, false, ) .expect("torus"); assert_eq!((torus.coords.len(), torus.faces.len()), (25, 32)); assert_eq!(&torus.coords[0..5], &torus.coords[20..25]); let cylinder = SculptMesh::new_with_list_sculpt_type_boolean_boolean_boolean( grid(4, 4), SculptMeshSculptType::Cylinder, false, false, false, ) .expect("cylinder"); assert_eq!((cylinder.coords.len(), cylinder.faces.len()), (20, 24)); } #[test] fn copy_transforms_and_flat_height_map_are_real_operations() { let mut mesh = SculptMesh::new_with_single_array_single_single_single_single_boolean( vec![0.0, 0.0, 0.0, 1.0], -1.0, 1.0, -1.0, 1.0, true, ) .expect("height map"); assert_eq!((mesh.coords.len(), mesh.faces.len()), (4, 2)); assert_eq!((mesh.uvs[0].v, mesh.uvs[3].v), (1.0, 0.0)); let original = mesh.coords[0]; let copy = mesh.copy().expect("copy"); mesh.add_pos(2.0, 3.0, 4.0).expect("translate"); mesh.scale(2.0, 0.5, 1.0).expect("scale"); mesh.add_rot( Quat::new_with_single_single_single_single(0.0, 0.0, 0.0, 1.0).expect("identity"), ) .expect("rotate"); assert_eq!(copy.coords[0], original); assert_ne!(mesh.coords[0], copy.coords[0]); assert_eq!(mesh.viewer_faces.len(), 2); let rectangular = SculptMesh::from_height_rows( vec![vec![0.0, 0.5, 1.0], vec![1.0, 0.5, 0.0]], 0.0, 2.0, 0.0, 1.0, false, ) .expect("rectangular height map"); assert_eq!((rectangular.coords.len(), rectangular.faces.len()), (6, 4)); } struct FixedCodec; impl ITextureCodec for FixedCodec { fn decode( &self, mut stream: Box, ) -> Result { let mut marker = Vec::new(); stream .read_to_end(&mut marker) .map_err(|_| libremetaverse_imaging::Error::InvalidOperation)?; if marker != b"sculpt" { return Err(libremetaverse_imaging::Error::Parse { position: 0, context: "test sculpt", }); } let mut image = ManagedImage::new(2, 2, ManagedImageImageChannels::GRAY)?; image.red = vec![0, 64, 128, 255]; Ok(image) } } #[test] fn file_constructor_decodes_through_imaging_abstraction_and_raw_is_stable() { let directory = std::env::temp_dir(); let stem = format!("metacrate-sculpt-{}", std::process::id()); let input = directory.join(format!("{stem}.map")); fs::write(&input, b"sculpt").expect("write fixture"); let mesh = SculptMesh::new_with_string_i_texture_codec_int32_int32_int32_int32_int32( input.to_string_lossy().into_owned(), Box::new(FixedCodec), SculptMeshSculptType::Plane as i32, 4, 1, 0, 0, ) .expect("decoded sculpt"); assert_eq!( (mesh.coords.len(), mesh.faces.len(), mesh.viewer_faces.len()), (4, 2, 2) ); mesh.dump_raw( directory.to_string_lossy().into_owned(), stem.clone(), "golden".to_owned(), ) .expect("raw output"); let output = directory.join(format!("{stem}_golden.raw")); let raw = fs::read_to_string(&output).expect("raw contents"); assert_eq!(raw.lines().count(), 2); let mut expected = String::new(); for face in &mesh.faces { writeln!( expected, "{} {} {}", mesh.coords[usize::try_from(face.v1).expect("index")], mesh.coords[usize::try_from(face.v2).expect("index")], mesh.coords[usize::try_from(face.v3).expect("index")], ) .expect("format raw"); } assert_eq!(raw, expected); fs::remove_file(input).expect("remove fixture"); fs::remove_file(output).expect("remove output"); } #[test] fn malformed_maps_rows_and_height_maps_return_typed_errors() { let malformed = SculptMap { width: 2, height: 2, red_bytes: vec![0; 3], green_bytes: vec![0; 4], blue_bytes: vec![0; 4], }; assert_eq!(malformed.to_rows(false), Err(Error::InvalidOperation)); assert!(matches!( SculptMesh::new_with_list_sculpt_type_boolean_boolean_boolean( vec![vec![Coord::default(); 2], vec![Coord::default(); 3]], SculptMeshSculptType::Plane, false, false, false, ), Err(Error::Argument) )); assert!(matches!( SculptMesh::new_with_single_array_single_single_single_single_boolean( vec![0.0; 6], 0.0, 1.0, 0.0, 1.0, false, ), Err(Error::Argument) )); } }