Implement sculpt mesh and OBJ support (#43)
Some checks failed
JPEG 2000 feature / linux (push) Has been cancelled
JPEG 2000 feature / macos (push) Has been cancelled
JPEG 2000 feature / windows (push) Has been cancelled
Skia feature / linux (push) Has been cancelled
Skia feature / macos (push) Has been cancelled
Skia feature / windows (push) Has been cancelled
Some checks failed
JPEG 2000 feature / linux (push) Has been cancelled
JPEG 2000 feature / macos (push) Has been cancelled
JPEG 2000 feature / windows (push) Has been cancelled
Skia feature / linux (push) Has been cancelled
Skia feature / macos (push) Has been cancelled
Skia feature / windows (push) Has been cancelled
This commit is contained in:
904
crates/libremetaverse-prim-mesher/src/sculpt.rs
Normal file
904
crates/libremetaverse-prim-mesher/src/sculpt.rs
Normal file
@@ -0,0 +1,904 @@
|
||||
//! 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<u8>,
|
||||
pub green_bytes: Vec<u8>,
|
||||
pub height: i32,
|
||||
pub red_bytes: Vec<u8>,
|
||||
pub width: i32,
|
||||
}
|
||||
|
||||
impl SculptMap {
|
||||
pub fn new_with_constructor() -> Result<Self, Error> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
pub fn new_with_managed_image_int32(image: ManagedImage, lod: i32) -> Result<Self, Error> {
|
||||
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<Vec<Vec<Coord>>, 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<Coord>,
|
||||
pub faces: Vec<Face>,
|
||||
pub normals: Vec<Coord>,
|
||||
pub uvs: Vec<UVCoord>,
|
||||
pub viewer_faces: Vec<ViewerFace>,
|
||||
}
|
||||
|
||||
impl SculptMesh {
|
||||
pub fn new_with_managed_image_sculpt_type_int32_boolean(
|
||||
image: ManagedImage,
|
||||
sculpt_type: SculptMeshSculptType,
|
||||
lod: i32,
|
||||
viewer_mode: bool,
|
||||
) -> Result<Self, Error> {
|
||||
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<Self, Error> {
|
||||
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<Self, Error> {
|
||||
Ok(mesh.clone())
|
||||
}
|
||||
|
||||
pub fn new_with_list_sculpt_type_boolean_boolean_boolean(
|
||||
mut rows: Vec<Vec<Coord>>,
|
||||
sculpt_type: SculptMeshSculptType,
|
||||
viewer_mode: bool,
|
||||
mirror: bool,
|
||||
mut invert: bool,
|
||||
) -> Result<Self, Error> {
|
||||
validate_rows(&rows)?;
|
||||
if mirror {
|
||||
invert = !invert;
|
||||
}
|
||||
let original_width = rows[0].len();
|
||||
if sculpt_type != SculptMeshSculptType::Plane {
|
||||
if rows.len() % 2 == 0 {
|
||||
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() % 2 == 0 {
|
||||
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<f32>,
|
||||
x_begin: f32,
|
||||
x_end: f32,
|
||||
y_begin: f32,
|
||||
y_end: f32,
|
||||
viewer_mode: bool,
|
||||
) -> Result<Self, Error> {
|
||||
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<Vec<f32>> = 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<Vec<f32>>,
|
||||
x_begin: f32,
|
||||
x_end: f32,
|
||||
y_begin: f32,
|
||||
y_end: f32,
|
||||
viewer_mode: bool,
|
||||
) -> Result<Self, Error> {
|
||||
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<Vec<Coord>> = (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<dyn ITextureCodec>,
|
||||
sculpt_type: i32,
|
||||
lod: i32,
|
||||
viewer_mode: i32,
|
||||
mirror: i32,
|
||||
invert: i32,
|
||||
) -> Result<Self, Error> {
|
||||
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<dyn ITextureCodec>,
|
||||
sculpt_type: SculptMeshSculptType,
|
||||
lod: i32,
|
||||
viewer_mode: bool,
|
||||
) -> Result<Self, Error> {
|
||||
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<Self, Error> {
|
||||
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<Coord>]) -> 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<Face, Error> {
|
||||
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<Coord, Error> {
|
||||
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<usize, Error> {
|
||||
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, Error> {
|
||||
i32::try_from(value).map_err(|_| Error::Argument)
|
||||
}
|
||||
|
||||
fn sculpt_type_from_i32(value: i32) -> Result<SculptMeshSculptType, Error> {
|
||||
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<ManagedImage, Error> {
|
||||
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<Vec<Coord>> {
|
||||
(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<dyn ReadWrite + Send>,
|
||||
) -> Result<ManagedImage, libremetaverse_imaging::Error> {
|
||||
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)
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user