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

This commit is contained in:
2026-08-09 04:59:49 +00:00
parent 5b3950c421
commit c6171dc625
10 changed files with 1451 additions and 330 deletions

View File

@@ -37,40 +37,14 @@ pub use crate::prim_mesher::Coord;
pub use crate::prim_mesher::Face;
/// C# type: `T:LibreMetaverse.PrimMesher.ObjMesh`.
pub struct ObjMesh {
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.meshName`.
pub mesh_name: String,
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.numPrimFaces`.
pub num_prim_faces: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.viewerPolygons`.
pub viewer_polygons: Vec<Vec<libremetaverse_prim_mesher::ViewerPolygon>>,
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.viewerVertices`.
pub viewer_vertices: Vec<Vec<libremetaverse_prim_mesher::ViewerVertex>>,
}
impl ObjMesh {
/// C# member: `M:LibreMetaverse.PrimMesher.ObjMesh.#ctor(System.IO.StreamReader)`.
pub fn new_with_stream_reader(
sr: libremetaverse_types::compat::StreamReader,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.ObjMesh.#ctor(System.IO.StreamReader)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.ObjMesh.#ctor(System.String)`.
pub fn new_with_string(path: String) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.ObjMesh.#ctor(System.String)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.ObjMesh.GetVertexIndexer`.
pub fn get_vertex_indexer(
&self,
) -> Result<libremetaverse_prim_mesher::VertexIndexer, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.ObjMesh.GetVertexIndexer",
)
}
}
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.meshName`.
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.numPrimFaces`.
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.viewerPolygons`.
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.viewerVertices`.
/// C# member: `M:LibreMetaverse.PrimMesher.ObjMesh.#ctor(System.IO.StreamReader)`.
/// C# member: `M:LibreMetaverse.PrimMesher.ObjMesh.#ctor(System.String)`.
/// C# member: `M:LibreMetaverse.PrimMesher.ObjMesh.GetVertexIndexer`.
pub use crate::obj_mesh::ObjMesh;
/// C# type: `T:LibreMetaverse.PrimMesher.Path`.
/// C# member: `F:LibreMetaverse.PrimMesher.Path.dimpleBegin`.
@@ -208,170 +182,35 @@ pub use crate::prim_mesher::Profile;
pub use crate::prim_mesher::Quat;
/// C# type: `T:LibreMetaverse.PrimMesher.SculptMap`.
pub struct SculptMap {
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.blueBytes`.
pub blue_bytes: Vec<u8>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.greenBytes`.
pub green_bytes: Vec<u8>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.height`.
pub height: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.redBytes`.
pub red_bytes: Vec<u8>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.width`.
pub width: i32,
}
impl SculptMap {
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMap.#ctor`.
pub fn new_with_constructor() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.PrimMesher.SculptMap.#ctor")
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMap.#ctor(LibreMetaverse.Imaging.ManagedImage,System.Int32)`.
pub fn new_with_managed_image_int32(
image: libremetaverse_imaging::ManagedImage,
lod: i32,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMap.#ctor(LibreMetaverse.Imaging.ManagedImage,System.Int32)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMap.ToRows(System.Boolean)`.
pub fn to_rows(
&self,
mirror: bool,
) -> Result<Vec<Vec<libremetaverse_prim_mesher::Coord>>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMap.ToRows(System.Boolean)",
)
}
}
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.blueBytes`.
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.greenBytes`.
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.height`.
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.redBytes`.
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.width`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMap.#ctor`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMap.#ctor(LibreMetaverse.Imaging.ManagedImage,System.Int32)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMap.ToRows(System.Boolean)`.
pub use crate::sculpt::SculptMap;
/// C# type: `T:LibreMetaverse.PrimMesher.SculptMesh`.
pub struct SculptMesh {
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.coords`.
pub coords: Vec<libremetaverse_prim_mesher::Coord>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.faces`.
pub faces: Vec<libremetaverse_prim_mesher::Face>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.normals`.
pub normals: Vec<libremetaverse_prim_mesher::Coord>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.uvs`.
pub uvs: Vec<libremetaverse_prim_mesher::UVCoord>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.viewerFaces`.
pub viewer_faces: Vec<libremetaverse_prim_mesher::ViewerFace>,
}
impl SculptMesh {
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Int32,System.Boolean)`.
pub fn new_with_managed_image_sculpt_type_int32_boolean(
sculpt_image: libremetaverse_imaging::ManagedImage,
sculpt_type: libremetaverse_prim_mesher::SculptMeshSculptType,
lod: i32,
viewer_mode: bool,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Int32,System.Boolean)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Int32,System.Boolean,System.Boolean,System.Boolean)`.
pub fn new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean(
sculpt_image: libremetaverse_imaging::ManagedImage,
sculpt_type: libremetaverse_prim_mesher::SculptMeshSculptType,
lod: i32,
viewer_mode: bool,
mirror: bool,
invert: bool,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Int32,System.Boolean,System.Boolean,System.Boolean)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(LibreMetaverse.PrimMesher.SculptMesh)`.
pub fn new_with_sculpt_mesh(
sm: libremetaverse_prim_mesher::SculptMesh,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(LibreMetaverse.PrimMesher.SculptMesh)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(System.Collections.Generic.List{System.Collections.Generic.List{LibreMetaverse.PrimMesher.Coord}},LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Boolean,System.Boolean,System.Boolean)`.
pub fn new_with_list_sculpt_type_boolean_boolean_boolean(
rows: Vec<Vec<libremetaverse_prim_mesher::Coord>>,
sculpt_type: libremetaverse_prim_mesher::SculptMeshSculptType,
viewer_mode: bool,
mirror: bool,
invert: bool,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(System.Collections.Generic.List{System.Collections.Generic.List{LibreMetaverse.PrimMesher.Coord}},LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Boolean,System.Boolean,System.Boolean)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single,System.Boolean)`.
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, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single,System.Boolean)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(System.String,LibreMetaverse.Imaging.ITextureCodec,System.Int32,System.Int32,System.Int32,System.Int32,System.Int32)`.
pub fn new_with_string_i_texture_codec_int32_int32_int32_int32_int32(
file_name: String,
codec: Box<dyn libremetaverse_imaging::ITextureCodec>,
sculpt_type: i32,
lod: i32,
viewer_mode: i32,
mirror: i32,
invert: i32,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(System.String,LibreMetaverse.Imaging.ITextureCodec,System.Int32,System.Int32,System.Int32,System.Int32,System.Int32)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.AddPos(System.Single,System.Single,System.Single)`.
pub fn add_pos(&self, x: f32, y: f32, z: f32) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.AddPos(System.Single,System.Single,System.Single)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.AddRot(LibreMetaverse.PrimMesher.Quat)`.
pub fn add_rot(&self, q: libremetaverse_prim_mesher::Quat) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.AddRot(LibreMetaverse.PrimMesher.Quat)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.Copy`.
pub fn copy(&self) -> Result<libremetaverse_prim_mesher::SculptMesh, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.PrimMesher.SculptMesh.Copy")
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.DumpRaw(System.String,System.String,System.String)`.
pub fn dump_raw(&self, path: String, name: String, title: String) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.DumpRaw(System.String,System.String,System.String)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.Scale(System.Single,System.Single,System.Single)`.
pub fn scale(&self, x: f32, y: f32, z: f32) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.Scale(System.Single,System.Single,System.Single)",
)
}
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.SculptMeshFromFile(System.String,LibreMetaverse.Imaging.ITextureCodec,LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Int32,System.Boolean)`.
pub fn sculpt_mesh_from_file(
&self,
file_name: String,
codec: Box<dyn libremetaverse_imaging::ITextureCodec>,
sculpt_type: libremetaverse_prim_mesher::SculptMeshSculptType,
lod: i32,
viewer_mode: bool,
) -> Result<libremetaverse_prim_mesher::SculptMesh, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMesh.SculptMeshFromFile(System.String,LibreMetaverse.Imaging.ITextureCodec,LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Int32,System.Boolean)",
)
}
}
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.coords`.
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.faces`.
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.normals`.
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.uvs`.
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.viewerFaces`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Int32,System.Boolean)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(LibreMetaverse.Imaging.ManagedImage,LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Int32,System.Boolean,System.Boolean,System.Boolean)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(LibreMetaverse.PrimMesher.SculptMesh)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(System.Collections.Generic.List{System.Collections.Generic.List{LibreMetaverse.PrimMesher.Coord}},LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Boolean,System.Boolean,System.Boolean)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(System.Single[0:,0:],System.Single,System.Single,System.Single,System.Single,System.Boolean)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.#ctor(System.String,LibreMetaverse.Imaging.ITextureCodec,System.Int32,System.Int32,System.Int32,System.Int32,System.Int32)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.AddPos(System.Single,System.Single,System.Single)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.AddRot(LibreMetaverse.PrimMesher.Quat)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.Copy`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.DumpRaw(System.String,System.String,System.String)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.Scale(System.Single,System.Single,System.Single)`.
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMesh.SculptMeshFromFile(System.String,LibreMetaverse.Imaging.ITextureCodec,LibreMetaverse.PrimMesher.SculptMesh.SculptType,System.Int32,System.Boolean)`.
pub use crate::sculpt::SculptMesh;
/// C# type: `T:LibreMetaverse.PrimMesher.SculptMesh.SculptType`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -395,28 +234,12 @@ pub enum SculptMeshSculptType {
pub use crate::prim_mesher::UVCoord;
/// C# type: `T:LibreMetaverse.PrimMesher.VertexIndexer`.
pub struct VertexIndexer {
/// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.numPrimFaces`.
pub num_prim_faces: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.viewerPolygons`.
pub viewer_polygons: Vec<Option<Vec<libremetaverse_prim_mesher::ViewerPolygon>>>,
/// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.viewerVertices`.
pub viewer_vertices: Vec<Vec<libremetaverse_prim_mesher::ViewerVertex>>,
}
impl VertexIndexer {
/// C# member: `M:LibreMetaverse.PrimMesher.VertexIndexer.#ctor`.
pub fn new_with_constructor() -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented("M:LibreMetaverse.PrimMesher.VertexIndexer.#ctor")
}
/// C# member: `M:LibreMetaverse.PrimMesher.VertexIndexer.#ctor(LibreMetaverse.PrimMesher.PrimMesh)`.
pub fn new_with_prim_mesh(
prim_mesh: libremetaverse_prim_mesher::PrimMesh,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.VertexIndexer.#ctor(LibreMetaverse.PrimMesher.PrimMesh)",
)
}
}
/// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.numPrimFaces`.
/// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.viewerPolygons`.
/// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.viewerVertices`.
/// C# member: `M:LibreMetaverse.PrimMesher.VertexIndexer.#ctor`.
/// C# member: `M:LibreMetaverse.PrimMesher.VertexIndexer.#ctor(LibreMetaverse.PrimMesher.PrimMesh)`.
pub use crate::vertex_indexer::VertexIndexer;
/// C# type: `T:LibreMetaverse.PrimMesher.ViewerFace`.
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerFace.coordIndex1`.
@@ -440,41 +263,15 @@ impl VertexIndexer {
pub use crate::prim_mesher::ViewerFace;
/// C# type: `T:LibreMetaverse.PrimMesher.ViewerPolygon`.
pub struct ViewerPolygon {
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v1`.
pub v1: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v2`.
pub v2: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v3`.
pub v3: i32,
}
impl ViewerPolygon {
/// C# member: `M:LibreMetaverse.PrimMesher.ViewerPolygon.#ctor(System.Int32,System.Int32,System.Int32)`.
pub fn new(v1: i32, v2: i32, v3: i32) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.ViewerPolygon.#ctor(System.Int32,System.Int32,System.Int32)",
)
}
}
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v1`.
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v2`.
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v3`.
/// C# member: `M:LibreMetaverse.PrimMesher.ViewerPolygon.#ctor(System.Int32,System.Int32,System.Int32)`.
pub use crate::vertex_indexer::ViewerPolygon;
/// C# type: `T:LibreMetaverse.PrimMesher.ViewerVertex`.
pub struct ViewerVertex {
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.n`.
pub n: libremetaverse_prim_mesher::Coord,
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.uv`.
pub uv: libremetaverse_prim_mesher::UVCoord,
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.v`.
pub v: libremetaverse_prim_mesher::Coord,
}
impl ViewerVertex {
/// C# member: `M:LibreMetaverse.PrimMesher.ViewerVertex.#ctor(LibreMetaverse.PrimMesher.Coord,LibreMetaverse.PrimMesher.Coord,LibreMetaverse.PrimMesher.UVCoord)`.
pub fn new(
coord: libremetaverse_prim_mesher::Coord,
normal: libremetaverse_prim_mesher::Coord,
uv: libremetaverse_prim_mesher::UVCoord,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.ViewerVertex.#ctor(LibreMetaverse.PrimMesher.Coord,LibreMetaverse.PrimMesher.Coord,LibreMetaverse.PrimMesher.UVCoord)",
)
}
}
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.n`.
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.uv`.
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.v`.
/// C# member: `M:LibreMetaverse.PrimMesher.ViewerVertex.#ctor(LibreMetaverse.PrimMesher.Coord,LibreMetaverse.PrimMesher.Coord,LibreMetaverse.PrimMesher.UVCoord)`.
pub use crate::vertex_indexer::ViewerVertex;

View File

@@ -3,7 +3,10 @@
extern crate self as libremetaverse_prim_mesher;
mod generated;
mod obj_mesh;
mod prim_mesher;
mod sculpt;
mod vertex_indexer;
pub use generated::*;
pub use libremetaverse_imaging as imaging;

View File

@@ -0,0 +1,308 @@
//! Bounded Wavefront OBJ ingestion compatible with `ObjMesh.cs`.
#![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.
use crate::{Coord, Error, UVCoord, VertexIndexer, ViewerPolygon, ViewerVertex};
use libremetaverse_imaging::DEFAULT_MAX_ENCODED_BYTES;
use libremetaverse_types::compat::StreamReader;
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Read;
const MAX_OBJ_ELEMENTS: usize = 16_777_216;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ObjMesh {
pub mesh_name: String,
pub num_prim_faces: i32,
pub viewer_polygons: Vec<Vec<ViewerPolygon>>,
pub viewer_vertices: Vec<Vec<ViewerVertex>>,
}
impl ObjMesh {
pub fn new_with_stream_reader(reader: StreamReader) -> Result<Self, Error> {
if reader.0.len() > DEFAULT_MAX_ENCODED_BYTES {
return Err(Error::Argument);
}
let text = String::from_utf8(reader.0).map_err(|_| Error::Parse {
position: 0,
context: "OBJ UTF-8",
})?;
Self::parse(&text)
}
pub fn new_with_string(path: String) -> Result<Self, Error> {
let metadata = fs::metadata(&path).map_err(|_| Error::InvalidOperation)?;
if metadata.len() > DEFAULT_MAX_ENCODED_BYTES as u64 {
return Err(Error::Argument);
}
let file = File::open(path).map_err(|_| Error::InvalidOperation)?;
let limit = u64::try_from(DEFAULT_MAX_ENCODED_BYTES)
.map_err(|_| Error::Argument)?
.checked_add(1)
.ok_or(Error::Argument)?;
let mut bytes =
Vec::with_capacity(usize::try_from(metadata.len()).map_err(|_| Error::Argument)?);
file.take(limit)
.read_to_end(&mut bytes)
.map_err(|_| Error::InvalidOperation)?;
if bytes.len() > DEFAULT_MAX_ENCODED_BYTES {
return Err(Error::Argument);
}
Self::new_with_stream_reader(StreamReader(bytes))
}
fn parse(text: &str) -> Result<Self, Error> {
let mut result = Self::default();
let mut coords = Vec::new();
let mut normals = Vec::new();
let mut uvs = Vec::new();
let mut face_vertices = Vec::new();
let mut face_polygons = Vec::new();
let mut lookup: HashMap<(i32, i32, i32), i32> = HashMap::new();
for raw in text.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let tokens: Vec<&str> = line.split_whitespace().collect();
let command = tokens[0];
if command.eq_ignore_ascii_case("v") {
if tokens.len() >= 4 {
bounded_push(&mut coords, parse_coord(&tokens)?)?;
}
} else if command.eq_ignore_ascii_case("vt") {
if tokens.len() >= 2 {
let u = parse_float(tokens[1])?;
let v = if tokens.len() > 2 {
parse_float(tokens[2])?
} else {
0.0
};
bounded_push(&mut uvs, UVCoord { u, v })?;
}
} else if command.eq_ignore_ascii_case("vn") {
if tokens.len() >= 4 {
bounded_push(&mut normals, parse_coord(&tokens)?)?;
}
} else if command.eq_ignore_ascii_case("o") {
if tokens.len() > 1 {
tokens[1].clone_into(&mut result.mesh_name);
}
} else if command.eq_ignore_ascii_case("g") {
finish_face(
&mut result,
&mut face_vertices,
&mut face_polygons,
&mut lookup,
)?;
} else if command.eq_ignore_ascii_case("f") && tokens.len() >= 4 {
let mut polygon = [0_i32; 3];
for (slot, token) in polygon.iter_mut().zip(&tokens[1..4]) {
let mut parts = token.split('/');
let position = parse_obj_index(parts.next().unwrap_or_default(), coords.len())?;
let texture = parse_optional_obj_index(parts.next(), uvs.len())?;
let normal = parse_optional_obj_index(parts.next(), normals.len())?;
let key = (
to_i32(position)?,
optional_i32(texture)?,
optional_i32(normal)?,
);
if let Some(index) = lookup.get(&key) {
*slot = *index;
} else {
let vertex = ViewerVertex {
v: coords[position],
n: normal.map_or(Coord::default(), |index| normals[index]),
uv: texture.map_or(UVCoord::default(), |index| uvs[index]),
};
bounded_push(&mut face_vertices, vertex)?;
let index = to_i32(face_vertices.len() - 1)?;
lookup.insert(key, index);
*slot = index;
}
}
bounded_push(
&mut face_polygons,
ViewerPolygon {
v1: polygon[0],
v2: polygon[1],
v3: polygon[2],
},
)?;
}
}
finish_face(
&mut result,
&mut face_vertices,
&mut face_polygons,
&mut lookup,
)?;
Ok(result)
}
pub fn get_vertex_indexer(&self) -> Result<VertexIndexer, Error> {
let polygons = self.viewer_polygons.iter().cloned().map(Some).collect();
Ok(VertexIndexer {
num_prim_faces: self.num_prim_faces,
viewer_polygons: polygons,
viewer_vertices: self.viewer_vertices.clone(),
})
}
}
fn finish_face(
mesh: &mut ObjMesh,
vertices: &mut Vec<ViewerVertex>,
polygons: &mut Vec<ViewerPolygon>,
lookup: &mut HashMap<(i32, i32, i32), i32>,
) -> Result<(), Error> {
if !vertices.is_empty() && !polygons.is_empty() {
mesh.viewer_vertices.push(std::mem::take(vertices));
mesh.viewer_polygons.push(std::mem::take(polygons));
lookup.clear();
mesh.num_prim_faces = mesh.num_prim_faces.checked_add(1).ok_or(Error::Argument)?;
}
Ok(())
}
fn parse_coord(tokens: &[&str]) -> Result<Coord, Error> {
Ok(Coord {
x: parse_float(tokens[1])?,
y: parse_float(tokens[2])?,
z: parse_float(tokens[3])?,
})
}
fn parse_float(value: &str) -> Result<f32, Error> {
let value = value.parse::<f32>().map_err(|_| Error::Parse {
position: 0,
context: "OBJ number",
})?;
if value.is_finite() {
Ok(value)
} else {
Err(Error::Argument)
}
}
fn parse_obj_index(value: &str, len: usize) -> Result<usize, Error> {
let one_based = value.parse::<i32>().map_err(|_| Error::Parse {
position: 0,
context: "OBJ index",
})?;
let index = one_based.checked_sub(1).ok_or(Error::IndexOutOfRange)?;
let index = usize::try_from(index).map_err(|_| Error::IndexOutOfRange)?;
if index < len {
Ok(index)
} else {
Err(Error::IndexOutOfRange)
}
}
fn parse_optional_obj_index(value: Option<&str>, len: usize) -> Result<Option<usize>, Error> {
match value {
None | Some("") => Ok(None),
Some(value) => parse_obj_index(value, len).map(Some),
}
}
fn bounded_push<T>(values: &mut Vec<T>, value: T) -> Result<(), Error> {
if values.len() >= MAX_OBJ_ELEMENTS {
return Err(Error::Argument);
}
values.push(value);
Ok(())
}
fn to_i32(value: usize) -> Result<i32, Error> {
i32::try_from(value).map_err(|_| Error::Argument)
}
fn optional_i32(value: Option<usize>) -> Result<i32, Error> {
value.map_or(Ok(-1), to_i32)
}
#[cfg(test)]
mod tests {
use super::*;
const GOLDEN: &str = "\
o sculpt_part\n\
v 0 0 0\n\
v 1 0 0\n\
v 0 1 0\n\
v 0 0 1\n\
vt 0 0\n\
vt 1 0\n\
vt 0 1\n\
vn 0 0 1\n\
g front\n\
f 1/1/1 2/2/1 3/3/1\n\
f 1/1/1 3/3/1 2/2/1\n\
g side\n\
f 1//1 2//1 4//1\n";
#[test]
fn obj_golden_groups_vertices_uvs_and_normals_deterministically() {
let first = ObjMesh::new_with_stream_reader(StreamReader(GOLDEN.as_bytes().to_vec()))
.expect("OBJ parse");
let second = ObjMesh::new_with_stream_reader(StreamReader(GOLDEN.as_bytes().to_vec()))
.expect("OBJ parse");
assert_eq!(first, second);
assert_eq!(first.mesh_name, "sculpt_part");
assert_eq!(first.num_prim_faces, 2);
assert_eq!(first.viewer_polygons[0].len(), 2);
assert_eq!(first.viewer_vertices[0].len(), 3);
assert_eq!(
first.viewer_polygons[1],
vec![ViewerPolygon {
v1: 0,
v2: 1,
v3: 2
}]
);
assert_eq!(first.viewer_vertices[1][0].uv, UVCoord::default());
let indexer = first.get_vertex_indexer().expect("indexer");
assert_eq!(indexer.num_prim_faces, 2);
assert_eq!(indexer.viewer_polygons[0].as_ref().expect("face").len(), 2);
}
#[test]
fn obj_ignores_non_triangle_tail_like_reference() {
let source = "v 0 0 0\nv 1 0 0\nv 0 1 0\nv 0 0 1\nf 1 2 3 4\n";
let mesh = ObjMesh::new_with_stream_reader(StreamReader(source.as_bytes().to_vec()))
.expect("OBJ parse");
assert_eq!(
mesh.viewer_polygons[0],
vec![ViewerPolygon {
v1: 0,
v2: 1,
v3: 2
}]
);
}
#[test]
fn malformed_obj_returns_typed_errors_without_panicking() {
let malformed = "v 0 0 0\nv 1 0 0\nf 1 2 9\n";
assert_eq!(
ObjMesh::new_with_stream_reader(StreamReader(malformed.as_bytes().to_vec())),
Err(Error::IndexOutOfRange)
);
let malformed = "v NaN 0 0\n";
assert_eq!(
ObjMesh::new_with_stream_reader(StreamReader(malformed.as_bytes().to_vec())),
Err(Error::Argument)
);
assert!(matches!(
ObjMesh::new_with_stream_reader(StreamReader(vec![0xff])),
Err(Error::Parse {
context: "OBJ UTF-8",
..
})
));
}
}

View File

@@ -19,8 +19,7 @@
#![allow(clippy::too_many_arguments)] // Constructor and extrusion helpers mirror fixed APIs.
#![allow(clippy::too_many_lines)] // The extrusion sequence remains reviewable against C#.
use crate::{Error, PathType, VertexIndexer, ViewerPolygon, ViewerVertex};
use std::collections::HashMap;
use crate::{Error, PathType, VertexIndexer};
use std::f32::consts::{PI, TAU};
use std::fmt;
use std::fs::File;
@@ -1777,61 +1776,7 @@ impl PrimMesh {
}
pub fn get_vertex_indexer(&self) -> Result<Option<VertexIndexer>, Error> {
if self.viewer_mode && !self.viewer_faces.is_empty() {
let max_face = self
.viewer_faces
.iter()
.map(|face| face.prim_face_number)
.max()
.unwrap_or(0);
let num_prim_faces = max_face.checked_add(1).ok_or(Error::Argument)?;
let face_count = usize::try_from(num_prim_faces).map_err(|_| Error::Argument)?;
if face_count > MAX_PROFILE_VERTICES {
return Err(Error::Argument);
}
let mut viewer_vertices: Vec<Vec<ViewerVertex>> =
(0..face_count).map(|_| Vec::new()).collect();
let mut viewer_polygons: Vec<Option<Vec<ViewerPolygon>>> =
(0..face_count).map(|_| Some(Vec::new())).collect();
let mut indices: Vec<HashMap<i32, i32>> =
(0..face_count).map(|_| HashMap::new()).collect();
for face in &self.viewer_faces {
let face_index = idx(face.prim_face_number, face_count)?;
let vertices = &mut viewer_vertices[face_index];
let map = &mut indices[face_index];
let v1 = indexed_viewer_vertex(
map,
vertices,
face.coord_index1,
face.v1,
face.n1,
face.uv1,
)?;
let v2 = indexed_viewer_vertex(
map,
vertices,
face.coord_index2,
face.v2,
face.n2,
face.uv2,
)?;
let v3 = indexed_viewer_vertex(
map,
vertices,
face.coord_index3,
face.v3,
face.n3,
face.uv3,
)?;
viewer_polygons[face_index]
.as_mut()
.ok_or(Error::InvalidOperation)?
.push(ViewerPolygon { v1, v2, v3 });
}
Ok(Some(VertexIndexer {
num_prim_faces,
viewer_polygons,
viewer_vertices,
}))
Ok(Some(VertexIndexer::from_viewer_faces(&self.viewer_faces)?))
} else {
Ok(None)
}
@@ -1927,23 +1872,7 @@ fn assign_viewer_normals(
viewer.n3 = normals[idx(face.n3, normals.len())?];
Ok(())
}
fn indexed_viewer_vertex(
indices: &mut HashMap<i32, i32>,
vertices: &mut Vec<ViewerVertex>,
coord_index: i32,
v: Coord,
n: Coord,
uv: UVCoord,
) -> Result<i32, Error> {
if let Some(index) = indices.get(&coord_index) {
return Ok(*index);
}
let index = i32::try_from(vertices.len()).map_err(|_| Error::Argument)?;
vertices.push(ViewerVertex { n, uv, v });
indices.insert(coord_index, index);
Ok(index)
}
fn dump_raw_geometry(
pub(crate) fn dump_raw_geometry(
coords: &[Coord],
faces: &[Face],
path: String,

View 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)
));
}
}

View File

@@ -0,0 +1,167 @@
//! Viewer-face indexing compatible with `VertexIndexer.cs`.
#![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)] // PrimMesh ownership matches the mapped constructor.
use crate::{Coord, Error, PrimMesh, UVCoord};
use std::collections::HashMap;
const MAX_PRIM_FACES: usize = 65_536;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ViewerVertex {
pub n: Coord,
pub uv: UVCoord,
pub v: Coord,
}
impl ViewerVertex {
pub fn new(coord: Coord, normal: Coord, uv: UVCoord) -> Result<Self, Error> {
Ok(Self {
n: normal,
uv,
v: coord,
})
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ViewerPolygon {
pub v1: i32,
pub v2: i32,
pub v3: i32,
}
impl ViewerPolygon {
pub fn new(v1: i32, v2: i32, v3: i32) -> Result<Self, Error> {
Ok(Self { v1, v2, v3 })
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct VertexIndexer {
pub num_prim_faces: i32,
pub viewer_polygons: Vec<Option<Vec<ViewerPolygon>>>,
pub viewer_vertices: Vec<Vec<ViewerVertex>>,
}
impl VertexIndexer {
pub fn new_with_constructor() -> Result<Self, Error> {
Ok(Self::default())
}
pub fn new_with_prim_mesh(prim_mesh: PrimMesh) -> Result<Self, Error> {
Self::from_viewer_faces(&prim_mesh.viewer_faces)
}
pub(crate) fn from_viewer_faces(faces: &[crate::ViewerFace]) -> Result<Self, Error> {
let max_face = faces
.iter()
.map(|face| face.prim_face_number)
.max()
.unwrap_or(0);
let num_prim_faces = max_face.checked_add(1).ok_or(Error::Argument)?;
let face_count = usize::try_from(num_prim_faces).map_err(|_| Error::Argument)?;
if face_count > MAX_PRIM_FACES {
return Err(Error::Argument);
}
let mut viewer_vertices: Vec<Vec<ViewerVertex>> =
(0..face_count).map(|_| Vec::new()).collect();
let mut viewer_polygons: Vec<Option<Vec<ViewerPolygon>>> =
(0..face_count).map(|_| Some(Vec::new())).collect();
let mut indices: Vec<HashMap<i32, i32>> = (0..face_count).map(|_| HashMap::new()).collect();
for face in faces {
let face_index =
usize::try_from(face.prim_face_number).map_err(|_| Error::IndexOutOfRange)?;
if face_index >= face_count {
return Err(Error::IndexOutOfRange);
}
let vertices = &mut viewer_vertices[face_index];
let map = &mut indices[face_index];
let v1 = indexed_vertex(map, vertices, face.coord_index1, face.v1, face.n1, face.uv1)?;
let v2 = indexed_vertex(map, vertices, face.coord_index2, face.v2, face.n2, face.uv2)?;
let v3 = indexed_vertex(map, vertices, face.coord_index3, face.v3, face.n3, face.uv3)?;
viewer_polygons[face_index]
.as_mut()
.ok_or(Error::InvalidOperation)?
.push(ViewerPolygon { v1, v2, v3 });
}
Ok(Self {
num_prim_faces,
viewer_polygons,
viewer_vertices,
})
}
}
fn indexed_vertex(
indices: &mut HashMap<i32, i32>,
vertices: &mut Vec<ViewerVertex>,
coord_index: i32,
v: Coord,
n: Coord,
uv: UVCoord,
) -> Result<i32, Error> {
if coord_index < 0 {
return Err(Error::IndexOutOfRange);
}
if let Some(index) = indices.get(&coord_index) {
return Ok(*index);
}
let index = i32::try_from(vertices.len()).map_err(|_| Error::Argument)?;
vertices.push(ViewerVertex { n, uv, v });
indices.insert(coord_index, index);
Ok(index)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ViewerFace;
#[test]
fn direct_constructor_deduplicates_coordinate_indices_per_prim_face() {
let mut first = ViewerFace::new(0).expect("face");
first.coord_index1 = 0;
first.coord_index2 = 1;
first.coord_index3 = 2;
first.v1 = Coord {
x: 0.0,
y: 0.0,
z: 0.0,
};
first.v2 = Coord {
x: 1.0,
y: 0.0,
z: 0.0,
};
first.v3 = Coord {
x: 0.0,
y: 1.0,
z: 0.0,
};
let mut second = first;
second.coord_index2 = 2;
second.coord_index3 = 3;
let indexer = VertexIndexer::from_viewer_faces(&[first, second]).expect("indexer");
assert_eq!(indexer.num_prim_faces, 1);
assert_eq!(indexer.viewer_vertices[0].len(), 4);
assert_eq!(
indexer.viewer_polygons[0].as_ref().expect("polygons"),
&vec![
ViewerPolygon {
v1: 0,
v2: 1,
v3: 2
},
ViewerPolygon {
v1: 0,
v2: 2,
v3: 3
},
]
);
}
}