From c6171dc6250267906c39cba8da0737bb1c3a10cf Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 9 Aug 2026 04:59:49 +0000 Subject: [PATCH] Implement sculpt mesh and OBJ support (#43) --- README.md | 7 + api/SHIM-COVERAGE.md | 2 +- .../src/generated.rs | 305 +----- crates/libremetaverse-prim-mesher/src/lib.rs | 3 + .../src/obj_mesh.rs | 308 ++++++ .../src/prim_mesher.rs | 77 +- .../libremetaverse-prim-mesher/src/sculpt.rs | 904 ++++++++++++++++++ .../src/vertex_indexer.rs | 167 ++++ tests/red-suite-baseline.json | 2 +- tools/generate_api_shims.py | 6 + 10 files changed, 1451 insertions(+), 330 deletions(-) create mode 100644 crates/libremetaverse-prim-mesher/src/obj_mesh.rs create mode 100644 crates/libremetaverse-prim-mesher/src/sculpt.rs create mode 100644 crates/libremetaverse-prim-mesher/src/vertex_indexer.rs diff --git a/README.md b/README.md index c5fad92..919e2d4 100644 --- a/README.md +++ b/README.md @@ -165,5 +165,12 @@ radius, and revolutions; cap and side winding; normals, UVs, viewer faces, and prim-face indexing. Geometry allocation and index arithmetic are bounded, and identical finite inputs produce deterministic mesh output on every supported platform. +Sculpt maps are sampled through the same checked imaging abstraction and can +produce plane, sphere, torus, and cylinder topology with reference seam, +mirror, inversion, LOD, normal, UV, and viewer-face behavior. Native viewer +indexing deduplicates vertices per prim face, while bounded Wavefront OBJ +ingestion preserves object/group output and position/UV/normal associations. +Malformed dimensions, channel layouts, topology, and indices return typed +errors without partial meshes or panics. The controlled audit aggregates every expected failure by standardized C# member ID and rejects unrelated fixture, assertion, compile, or symbol errors. diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index fe89f76..cf62f97 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -8,7 +8,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. | `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain | | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | -| `LibreMetaverse.PrimMesher` | 17 | 207 | native implementation: 9 types / 155 members; remaining surface is callable failure-only shims | +| `LibreMetaverse.PrimMesher` | 17 | 207 | native implementation: 15 types / 200 members; remaining surface is callable failure-only shims | | `LibreMetaverse.RLV` | 28 | 499 | callable failure-only shim | | `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim | | `LibreMetaverse.Rendering.Simple` | 1 | 6 | callable failure-only shim | diff --git a/crates/libremetaverse-prim-mesher/src/generated.rs b/crates/libremetaverse-prim-mesher/src/generated.rs index 5b4f608..2df33f4 100644 --- a/crates/libremetaverse-prim-mesher/src/generated.rs +++ b/crates/libremetaverse-prim-mesher/src/generated.rs @@ -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>, - /// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.viewerVertices`. - pub viewer_vertices: Vec>, -} -impl ObjMesh { - /// C# member: `M:LibreMetaverse.PrimMesher.ObjMesh.#ctor(System.IO.StreamReader)`. - pub fn new_with_stream_reader( - sr: libremetaverse_types::compat::StreamReader, - ) -> Result { - 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 { - 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_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, - /// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.greenBytes`. - pub green_bytes: Vec, - /// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.height`. - pub height: i32, - /// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.redBytes`. - pub red_bytes: Vec, - /// 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 { - 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 { - 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>, 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, - /// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.faces`. - pub faces: Vec, - /// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.normals`. - pub normals: Vec, - /// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.uvs`. - pub uvs: Vec, - /// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.viewerFaces`. - pub viewer_faces: Vec, -} -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 { - 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 { - 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 { - 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>, - sculpt_type: libremetaverse_prim_mesher::SculptMeshSculptType, - viewer_mode: bool, - mirror: bool, - invert: bool, - ) -> Result { - 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, - x_begin: f32, - x_end: f32, - y_begin: f32, - y_end: f32, - viewer_mode: bool, - ) -> Result { - 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, - sculpt_type: i32, - lod: i32, - viewer_mode: i32, - mirror: i32, - invert: i32, - ) -> Result { - 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_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, - sculpt_type: libremetaverse_prim_mesher::SculptMeshSculptType, - lod: i32, - viewer_mode: bool, - ) -> Result { - 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>>, - /// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.viewerVertices`. - pub viewer_vertices: Vec>, -} -impl VertexIndexer { - /// C# member: `M:LibreMetaverse.PrimMesher.VertexIndexer.#ctor`. - pub fn new_with_constructor() -> Result { - 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 { - 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 { - 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 { - 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; diff --git a/crates/libremetaverse-prim-mesher/src/lib.rs b/crates/libremetaverse-prim-mesher/src/lib.rs index fbc76aa..49be65c 100644 --- a/crates/libremetaverse-prim-mesher/src/lib.rs +++ b/crates/libremetaverse-prim-mesher/src/lib.rs @@ -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; diff --git a/crates/libremetaverse-prim-mesher/src/obj_mesh.rs b/crates/libremetaverse-prim-mesher/src/obj_mesh.rs new file mode 100644 index 0000000..aa4e6be --- /dev/null +++ b/crates/libremetaverse-prim-mesher/src/obj_mesh.rs @@ -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>, + pub viewer_vertices: Vec>, +} + +impl ObjMesh { + pub fn new_with_stream_reader(reader: StreamReader) -> Result { + 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 { + 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 { + 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 { + 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, + polygons: &mut Vec, + 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 { + Ok(Coord { + x: parse_float(tokens[1])?, + y: parse_float(tokens[2])?, + z: parse_float(tokens[3])?, + }) +} + +fn parse_float(value: &str) -> Result { + let value = value.parse::().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 { + let one_based = value.parse::().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, Error> { + match value { + None | Some("") => Ok(None), + Some(value) => parse_obj_index(value, len).map(Some), + } +} + +fn bounded_push(values: &mut Vec, 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::try_from(value).map_err(|_| Error::Argument) +} + +fn optional_i32(value: Option) -> Result { + 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", + .. + }) + )); + } +} diff --git a/crates/libremetaverse-prim-mesher/src/prim_mesher.rs b/crates/libremetaverse-prim-mesher/src/prim_mesher.rs index 126c343..142779c 100644 --- a/crates/libremetaverse-prim-mesher/src/prim_mesher.rs +++ b/crates/libremetaverse-prim-mesher/src/prim_mesher.rs @@ -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, 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> = - (0..face_count).map(|_| Vec::new()).collect(); - let mut viewer_polygons: Vec>> = - (0..face_count).map(|_| Some(Vec::new())).collect(); - let mut indices: Vec> = - (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, - vertices: &mut Vec, - coord_index: i32, - v: Coord, - n: Coord, - uv: UVCoord, -) -> Result { - 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, diff --git a/crates/libremetaverse-prim-mesher/src/sculpt.rs b/crates/libremetaverse-prim-mesher/src/sculpt.rs new file mode 100644 index 0000000..d5be5d4 --- /dev/null +++ b/crates/libremetaverse-prim-mesher/src/sculpt.rs @@ -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, + 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() % 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, + 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) + )); + } +} diff --git a/crates/libremetaverse-prim-mesher/src/vertex_indexer.rs b/crates/libremetaverse-prim-mesher/src/vertex_indexer.rs new file mode 100644 index 0000000..222f075 --- /dev/null +++ b/crates/libremetaverse-prim-mesher/src/vertex_indexer.rs @@ -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 { + 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 { + Ok(Self { v1, v2, v3 }) + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct VertexIndexer { + pub num_prim_faces: i32, + pub viewer_polygons: Vec>>, + pub viewer_vertices: Vec>, +} + +impl VertexIndexer { + pub fn new_with_constructor() -> Result { + Ok(Self::default()) + } + + pub fn new_with_prim_mesh(prim_mesh: PrimMesh) -> Result { + Self::from_viewer_faces(&prim_mesh.viewer_faces) + } + + pub(crate) fn from_viewer_faces(faces: &[crate::ViewerFace]) -> Result { + 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> = + (0..face_count).map(|_| Vec::new()).collect(); + let mut viewer_polygons: Vec>> = + (0..face_count).map(|_| Some(Vec::new())).collect(); + let mut indices: Vec> = (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, + vertices: &mut Vec, + coord_index: i32, + v: Coord, + n: Coord, + uv: UVCoord, +) -> Result { + 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 + }, + ] + ); + } +} diff --git a/tests/red-suite-baseline.json b/tests/red-suite-baseline.json index 06ca542..d2ed0a6 100644 --- a/tests/red-suite-baseline.json +++ b/tests/red-suite-baseline.json @@ -146,5 +146,5 @@ "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test", "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test" ], - "support_passes": 123 + "support_passes": 132 } diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index 1e6edb0..415f0a4 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -47,6 +47,12 @@ NATIVE_TYPES = { "T:LibreMetaverse.PrimMesher.Quat": "crate::prim_mesher::Quat", "T:LibreMetaverse.PrimMesher.UVCoord": "crate::prim_mesher::UVCoord", "T:LibreMetaverse.PrimMesher.ViewerFace": "crate::prim_mesher::ViewerFace", + "T:LibreMetaverse.PrimMesher.ObjMesh": "crate::obj_mesh::ObjMesh", + "T:LibreMetaverse.PrimMesher.SculptMap": "crate::sculpt::SculptMap", + "T:LibreMetaverse.PrimMesher.SculptMesh": "crate::sculpt::SculptMesh", + "T:LibreMetaverse.PrimMesher.VertexIndexer": "crate::vertex_indexer::VertexIndexer", + "T:LibreMetaverse.PrimMesher.ViewerPolygon": "crate::vertex_indexer::ViewerPolygon", + "T:LibreMetaverse.PrimMesher.ViewerVertex": "crate::vertex_indexer::ViewerVertex", "T:LibreMetaverse.Imaging.Skia.SkiaTextureCodec": "crate::skia_codec::SkiaTextureCodec", "T:LibreMetaverse.Imaging.ManagedImageCreator": "crate::j2k::ManagedImageCreator", "T:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions": "crate::j2k::ManagedImageInterleavedExtensions",