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

@@ -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 prim-face indexing. Geometry allocation and index arithmetic are bounded, and
identical finite inputs produce deterministic mesh output on every supported identical finite inputs produce deterministic mesh output on every supported
platform. 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# The controlled audit aggregates every expected failure by standardized C#
member ID and rejects unrelated fixture, assertion, compile, or symbol errors. member ID and rejects unrelated fixture, assertion, compile, or symbol errors.

View File

@@ -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.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.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain |
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | | `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.RLV` | 28 | 499 | callable failure-only shim |
| `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim | | `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim |
| `LibreMetaverse.Rendering.Simple` | 1 | 6 | callable failure-only shim | | `LibreMetaverse.Rendering.Simple` | 1 | 6 | callable failure-only shim |

View File

@@ -37,40 +37,14 @@ pub use crate::prim_mesher::Coord;
pub use crate::prim_mesher::Face; pub use crate::prim_mesher::Face;
/// C# type: `T:LibreMetaverse.PrimMesher.ObjMesh`. /// C# type: `T:LibreMetaverse.PrimMesher.ObjMesh`.
pub struct ObjMesh {
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.meshName`. /// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.meshName`.
pub mesh_name: String,
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.numPrimFaces`. /// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.numPrimFaces`.
pub num_prim_faces: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.viewerPolygons`. /// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.viewerPolygons`.
pub viewer_polygons: Vec<Vec<libremetaverse_prim_mesher::ViewerPolygon>>,
/// C# member: `F:LibreMetaverse.PrimMesher.ObjMesh.viewerVertices`. /// 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)`. /// 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)`. /// 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`. /// C# member: `M:LibreMetaverse.PrimMesher.ObjMesh.GetVertexIndexer`.
pub fn get_vertex_indexer( pub use crate::obj_mesh::ObjMesh;
&self,
) -> Result<libremetaverse_prim_mesher::VertexIndexer, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.ObjMesh.GetVertexIndexer",
)
}
}
/// C# type: `T:LibreMetaverse.PrimMesher.Path`. /// C# type: `T:LibreMetaverse.PrimMesher.Path`.
/// C# member: `F:LibreMetaverse.PrimMesher.Path.dimpleBegin`. /// C# member: `F:LibreMetaverse.PrimMesher.Path.dimpleBegin`.
@@ -208,170 +182,35 @@ pub use crate::prim_mesher::Profile;
pub use crate::prim_mesher::Quat; pub use crate::prim_mesher::Quat;
/// C# type: `T:LibreMetaverse.PrimMesher.SculptMap`. /// C# type: `T:LibreMetaverse.PrimMesher.SculptMap`.
pub struct SculptMap {
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.blueBytes`. /// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.blueBytes`.
pub blue_bytes: Vec<u8>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.greenBytes`. /// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.greenBytes`.
pub green_bytes: Vec<u8>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.height`. /// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.height`.
pub height: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.redBytes`. /// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.redBytes`.
pub red_bytes: Vec<u8>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.width`. /// C# member: `F:LibreMetaverse.PrimMesher.SculptMap.width`.
pub width: i32,
}
impl SculptMap {
/// C# member: `M:LibreMetaverse.PrimMesher.SculptMap.#ctor`. /// 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)`. /// 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)`. /// C# member: `M:LibreMetaverse.PrimMesher.SculptMap.ToRows(System.Boolean)`.
pub fn to_rows( pub use crate::sculpt::SculptMap;
&self,
mirror: bool,
) -> Result<Vec<Vec<libremetaverse_prim_mesher::Coord>>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.SculptMap.ToRows(System.Boolean)",
)
}
}
/// C# type: `T:LibreMetaverse.PrimMesher.SculptMesh`. /// C# type: `T:LibreMetaverse.PrimMesher.SculptMesh`.
pub struct SculptMesh {
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.coords`. /// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.coords`.
pub coords: Vec<libremetaverse_prim_mesher::Coord>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.faces`. /// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.faces`.
pub faces: Vec<libremetaverse_prim_mesher::Face>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.normals`. /// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.normals`.
pub normals: Vec<libremetaverse_prim_mesher::Coord>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.uvs`. /// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.uvs`.
pub uvs: Vec<libremetaverse_prim_mesher::UVCoord>,
/// C# member: `F:LibreMetaverse.PrimMesher.SculptMesh.viewerFaces`. /// 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)`. /// 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)`. /// 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)`. /// 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)`. /// 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)`. /// 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)`. /// 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)`. /// 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)`. /// 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`. /// 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)`. /// 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)`. /// 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)`. /// 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( pub use crate::sculpt::SculptMesh;
&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# type: `T:LibreMetaverse.PrimMesher.SculptMesh.SculptType`. /// C# type: `T:LibreMetaverse.PrimMesher.SculptMesh.SculptType`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -395,28 +234,12 @@ pub enum SculptMeshSculptType {
pub use crate::prim_mesher::UVCoord; pub use crate::prim_mesher::UVCoord;
/// C# type: `T:LibreMetaverse.PrimMesher.VertexIndexer`. /// C# type: `T:LibreMetaverse.PrimMesher.VertexIndexer`.
pub struct VertexIndexer {
/// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.numPrimFaces`. /// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.numPrimFaces`.
pub num_prim_faces: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.viewerPolygons`. /// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.viewerPolygons`.
pub viewer_polygons: Vec<Option<Vec<libremetaverse_prim_mesher::ViewerPolygon>>>,
/// C# member: `F:LibreMetaverse.PrimMesher.VertexIndexer.viewerVertices`. /// 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`. /// 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)`. /// C# member: `M:LibreMetaverse.PrimMesher.VertexIndexer.#ctor(LibreMetaverse.PrimMesher.PrimMesh)`.
pub fn new_with_prim_mesh( pub use crate::vertex_indexer::VertexIndexer;
prim_mesh: libremetaverse_prim_mesher::PrimMesh,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.VertexIndexer.#ctor(LibreMetaverse.PrimMesher.PrimMesh)",
)
}
}
/// C# type: `T:LibreMetaverse.PrimMesher.ViewerFace`. /// C# type: `T:LibreMetaverse.PrimMesher.ViewerFace`.
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerFace.coordIndex1`. /// C# member: `F:LibreMetaverse.PrimMesher.ViewerFace.coordIndex1`.
@@ -440,41 +263,15 @@ impl VertexIndexer {
pub use crate::prim_mesher::ViewerFace; pub use crate::prim_mesher::ViewerFace;
/// C# type: `T:LibreMetaverse.PrimMesher.ViewerPolygon`. /// C# type: `T:LibreMetaverse.PrimMesher.ViewerPolygon`.
pub struct ViewerPolygon {
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v1`. /// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v1`.
pub v1: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v2`. /// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v2`.
pub v2: i32,
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerPolygon.v3`. /// 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)`. /// 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> { pub use crate::vertex_indexer::ViewerPolygon;
libremetaverse_types::not_implemented(
"M:LibreMetaverse.PrimMesher.ViewerPolygon.#ctor(System.Int32,System.Int32,System.Int32)",
)
}
}
/// C# type: `T:LibreMetaverse.PrimMesher.ViewerVertex`. /// C# type: `T:LibreMetaverse.PrimMesher.ViewerVertex`.
pub struct ViewerVertex {
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.n`. /// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.n`.
pub n: libremetaverse_prim_mesher::Coord,
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.uv`. /// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.uv`.
pub uv: libremetaverse_prim_mesher::UVCoord,
/// C# member: `F:LibreMetaverse.PrimMesher.ViewerVertex.v`. /// 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)`. /// C# member: `M:LibreMetaverse.PrimMesher.ViewerVertex.#ctor(LibreMetaverse.PrimMesher.Coord,LibreMetaverse.PrimMesher.Coord,LibreMetaverse.PrimMesher.UVCoord)`.
pub fn new( pub use crate::vertex_indexer::ViewerVertex;
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)",
)
}
}

View File

@@ -3,7 +3,10 @@
extern crate self as libremetaverse_prim_mesher; extern crate self as libremetaverse_prim_mesher;
mod generated; mod generated;
mod obj_mesh;
mod prim_mesher; mod prim_mesher;
mod sculpt;
mod vertex_indexer;
pub use generated::*; pub use generated::*;
pub use libremetaverse_imaging as imaging; 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_arguments)] // Constructor and extrusion helpers mirror fixed APIs.
#![allow(clippy::too_many_lines)] // The extrusion sequence remains reviewable against C#. #![allow(clippy::too_many_lines)] // The extrusion sequence remains reviewable against C#.
use crate::{Error, PathType, VertexIndexer, ViewerPolygon, ViewerVertex}; use crate::{Error, PathType, VertexIndexer};
use std::collections::HashMap;
use std::f32::consts::{PI, TAU}; use std::f32::consts::{PI, TAU};
use std::fmt; use std::fmt;
use std::fs::File; use std::fs::File;
@@ -1777,61 +1776,7 @@ impl PrimMesh {
} }
pub fn get_vertex_indexer(&self) -> Result<Option<VertexIndexer>, Error> { pub fn get_vertex_indexer(&self) -> Result<Option<VertexIndexer>, Error> {
if self.viewer_mode && !self.viewer_faces.is_empty() { if self.viewer_mode && !self.viewer_faces.is_empty() {
let max_face = self Ok(Some(VertexIndexer::from_viewer_faces(&self.viewer_faces)?))
.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,
}))
} else { } else {
Ok(None) Ok(None)
} }
@@ -1927,23 +1872,7 @@ fn assign_viewer_normals(
viewer.n3 = normals[idx(face.n3, normals.len())?]; viewer.n3 = normals[idx(face.n3, normals.len())?];
Ok(()) Ok(())
} }
fn indexed_viewer_vertex( pub(crate) fn dump_raw_geometry(
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(
coords: &[Coord], coords: &[Coord],
faces: &[Face], faces: &[Face],
path: String, 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
},
]
);
}
}

View File

@@ -146,5 +146,5 @@
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test", "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test",
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test" "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test"
], ],
"support_passes": 123 "support_passes": 132
} }

View File

@@ -47,6 +47,12 @@ NATIVE_TYPES = {
"T:LibreMetaverse.PrimMesher.Quat": "crate::prim_mesher::Quat", "T:LibreMetaverse.PrimMesher.Quat": "crate::prim_mesher::Quat",
"T:LibreMetaverse.PrimMesher.UVCoord": "crate::prim_mesher::UVCoord", "T:LibreMetaverse.PrimMesher.UVCoord": "crate::prim_mesher::UVCoord",
"T:LibreMetaverse.PrimMesher.ViewerFace": "crate::prim_mesher::ViewerFace", "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.Skia.SkiaTextureCodec": "crate::skia_codec::SkiaTextureCodec",
"T:LibreMetaverse.Imaging.ManagedImageCreator": "crate::j2k::ManagedImageCreator", "T:LibreMetaverse.Imaging.ManagedImageCreator": "crate::j2k::ManagedImageCreator",
"T:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions": "crate::j2k::ManagedImageInterleavedExtensions", "T:LibreMetaverse.Imaging.ManagedImageInterleavedExtensions": "crate::j2k::ManagedImageInterleavedExtensions",