diff --git a/src/EditableMesh.cpp b/src/EditableMesh.cpp index ea2b2344d..67b0a66a1 100644 --- a/src/EditableMesh.cpp +++ b/src/EditableMesh.cpp @@ -323,16 +323,36 @@ bool EditableMesh::commitToEntity(Ogre::Entity* entity) } void EditableMesh::buildSubMeshBuffers(Ogre::SubMesh* subMesh, - const EditableSubMesh& editSub) + const EditableSubMesh& editSubIn) { - if (!subMesh || editSub.vertices.empty() || editSub.triangles.empty()) - return; + if (!subMesh || editSubIn.vertices.empty()) return; + + // n-gon synchronisation: if the caller populated `faces`, that's + // canonical and `triangles` is meant to be a fan-triangulation + // mirror. Re-triangulate defensively here so the GPU buffer always + // matches the live face data even if the caller forgot to call + // `triangulateFaces()` after mutating `faces`. + // + // We work on a local copy when re-triangulating is needed so the + // input EditableSubMesh stays untouched (this method takes the + // submesh by const&). Triangle-only submeshes incur no copy. + EditableSubMesh local; + const EditableSubMesh* editSub = &editSubIn; + if (!editSubIn.faces.empty()) { + local.vertices = editSubIn.vertices; // shallow-but-fine — we don't write + local.faces = editSubIn.faces; + local.materialName = editSubIn.materialName; + local.usesSharedVertices = editSubIn.usesSharedVertices; + triangulateFaces(local); + editSub = &local; + } + if (editSub->triangles.empty()) return; // Replace any existing vertex data with a fresh one. if (subMesh->vertexData) delete subMesh->vertexData; subMesh->useSharedVertices = false; subMesh->vertexData = new Ogre::VertexData(); - subMesh->vertexData->vertexCount = editSub.vertices.size(); + subMesh->vertexData->vertexCount = static_cast(editSub->vertices.size()); auto* decl = subMesh->vertexData->vertexDeclaration; auto* binding = subMesh->vertexData->vertexBufferBinding; @@ -342,19 +362,19 @@ void EditableMesh::buildSubMeshBuffers(Ogre::SubMesh* subMesh, decl->addElement(0, offset, Ogre::VET_FLOAT3, Ogre::VES_POSITION); offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3); - bool hasNormals = editSub.vertices[0].hasNormal; + bool hasNormals = editSub->vertices[0].hasNormal; if (hasNormals) { decl->addElement(0, offset, Ogre::VET_FLOAT3, Ogre::VES_NORMAL); offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT3); } - bool hasUVs = editSub.vertices[0].hasUV; + bool hasUVs = editSub->vertices[0].hasUV; if (hasUVs) { decl->addElement(0, offset, Ogre::VET_FLOAT2, Ogre::VES_TEXTURE_COORDINATES); offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT2); } - bool hasTangents = editSub.vertices[0].hasTangent; + bool hasTangents = editSub->vertices[0].hasTangent; if (hasTangents) { decl->addElement(0, offset, Ogre::VET_FLOAT4, Ogre::VES_TANGENT); offset += Ogre::VertexElement::getTypeSize(Ogre::VET_FLOAT4); @@ -363,11 +383,11 @@ void EditableMesh::buildSubMeshBuffers(Ogre::SubMesh* subMesh, // Create interleaved vertex buffer. size_t vertSize = decl->getVertexSize(0); auto vbuf = Ogre::HardwareBufferManager::getSingleton().createVertexBuffer( - vertSize, editSub.vertices.size(), + vertSize, editSub->vertices.size(), Ogre::HardwareBuffer::HBU_DYNAMIC_WRITE_ONLY, true); auto* dest = static_cast(vbuf->lock(Ogre::HardwareBuffer::HBL_DISCARD)); - for (const auto& v : editSub.vertices) { + for (const auto& v : editSub->vertices) { *dest++ = v.position.x; *dest++ = v.position.y; *dest++ = v.position.z; @@ -386,22 +406,22 @@ void EditableMesh::buildSubMeshBuffers(Ogre::SubMesh* subMesh, binding->setBinding(0, vbuf); // Create index buffer (16-bit if possible, else 32-bit). - bool use32bit = editSub.vertices.size() > 65535; + bool use32bit = editSub->vertices.size() > 65535; auto ibuf = Ogre::HardwareBufferManager::getSingleton().createIndexBuffer( use32bit ? Ogre::HardwareIndexBuffer::IT_32BIT : Ogre::HardwareIndexBuffer::IT_16BIT, - editSub.triangles.size() * 3, + editSub->triangles.size() * 3, Ogre::HardwareBuffer::HBU_DYNAMIC_WRITE_ONLY, true); if (use32bit) { auto* idx = static_cast(ibuf->lock(Ogre::HardwareBuffer::HBL_DISCARD)); - for (const auto& tri : editSub.triangles) { + for (const auto& tri : editSub->triangles) { *idx++ = tri.indices[0]; *idx++ = tri.indices[1]; *idx++ = tri.indices[2]; } } else { auto* idx = static_cast(ibuf->lock(Ogre::HardwareBuffer::HBL_DISCARD)); - for (const auto& tri : editSub.triangles) { + for (const auto& tri : editSub->triangles) { *idx++ = static_cast(tri.indices[0]); *idx++ = static_cast(tri.indices[1]); *idx++ = static_cast(tri.indices[2]); @@ -410,7 +430,7 @@ void EditableMesh::buildSubMeshBuffers(Ogre::SubMesh* subMesh, ibuf->unlock(); subMesh->indexData->indexBuffer = ibuf; - subMesh->indexData->indexCount = editSub.triangles.size() * 3; + subMesh->indexData->indexCount = static_cast(editSub->triangles.size() * 3); subMesh->indexData->indexStart = 0; } @@ -543,6 +563,22 @@ size_t EditableMesh::totalTriangleCount() const return total; } +size_t totalFaceCount(const std::vector& subMeshes) +{ + size_t total = 0; + for (const auto& sub : subMeshes) { + total += sub.faces.empty() ? sub.triangles.size() : sub.faces.size(); + } + return total; +} + +void syncTriangulation(std::vector& subMeshes) +{ + for (auto& sub : subMeshes) { + if (!sub.faces.empty()) triangulateFaces(sub); + } +} + void EditableMesh::setVertexPosition(size_t subMeshIndex, size_t vertexIndex, const Ogre::Vector3& pos) { if (subMeshIndex < m_subMeshes.size() && vertexIndex < m_subMeshes[subMeshIndex].vertices.size()) @@ -589,6 +625,11 @@ Ogre::Vector2 EditableMesh::getVertexUV(size_t subMeshIndex, size_t vertexIndex) void EditableMesh::recalculateNormals() { for (auto& sub : m_subMeshes) { + // n-gon sync: when faces is canonical, refresh the triangle + // mirror so the normal-accumulation loop below sees the actual + // current geometry. Triangle-only submeshes pay nothing here. + if (!sub.faces.empty()) triangulateFaces(sub); + // Zero out all normals for (auto& v : sub.vertices) { v.normal = Ogre::Vector3::ZERO; @@ -626,6 +667,8 @@ void EditableMesh::recalculateNormals() void EditableMesh::recalculateNormalsFlat() { for (auto& sub : m_subMeshes) { + if (!sub.faces.empty()) triangulateFaces(sub); + // Zero out all normals for (auto& v : sub.vertices) { v.normal = Ogre::Vector3::ZERO; diff --git a/src/EditableMesh.h b/src/EditableMesh.h index 7a38f8926..91cd7e359 100644 --- a/src/EditableMesh.h +++ b/src/EditableMesh.h @@ -161,6 +161,33 @@ void triangulateFaces(EditableSubMesh& sub); */ void promoteTrianglesToFaces(EditableSubMesh& sub); +/** + * @brief Re-triangulate every submesh whose `faces` is non-empty. + * + * Convenience over `triangulateFaces(sub)` for a whole mesh: walks the + * submesh array and resyncs each one whose canonical face storage has + * changed. No-op for legacy triangle-only submeshes. + * + * Free function rather than a method on `EditableMesh` to keep the + * class size below SonarQube's 35-method ceiling. + * + * @param subMeshes The submesh vector to sync (typically `mesh.subMeshes()`). + */ +void syncTriangulation(std::vector& subMeshes); + +/** + * @brief Total polygonal-face count across a submesh vector. + * + * For each submesh, returns `faces.size()` when n-gons are canonical, + * else `triangles.size()`. The `EditableMesh::totalTriangleCount()` + * counterpart still reports the fan-triangulation count regardless of + * representation. + * + * Free function for the same reason as `syncTriangulation` — keeps + * `EditableMesh` below the class-method limit. + */ +size_t totalFaceCount(const std::vector& subMeshes); + /** * @brief Indexed mesh representation for topology queries and editing. * diff --git a/src/EditableMesh_test.cpp b/src/EditableMesh_test.cpp index 83c5462f9..bd5f2acc1 100644 --- a/src/EditableMesh_test.cpp +++ b/src/EditableMesh_test.cpp @@ -804,3 +804,111 @@ TEST_F(EditModeControllerTest, SoftSelectionWeightsInEditMode) { ctrl->exitEditMode(false); Manager::getSingleton()->destroySceneNode("EditMode_soft_weights_node"); } + +// =========================================================================== +// EditableFace + n-gon helpers (chunk 1) and triangulation sync (chunk 2) +// =========================================================================== + +TEST(EditableMeshStandalone, SyncTriangulationFanTriangulatesQuadFaces) { + EditableMesh mesh; + EditableSubMesh sub; + EditableVertex v; + sub.vertices = {v, v, v, v}; + EditableFace f; + f.indices = {0, 1, 2, 3}; + sub.faces.push_back(std::move(f)); + // triangles deliberately stale (empty); syncTriangulation must + // populate it from faces. + mesh.subMeshes().push_back(std::move(sub)); + + syncTriangulation(mesh.subMeshes()); + ASSERT_EQ(mesh.subMeshes()[0].triangles.size(), 2u) + << "quad must yield 2 fan triangles after syncTriangulation"; +} + +TEST(EditableMeshStandalone, SyncTriangulationLeavesTriOnlySubmeshAlone) { + EditableMesh mesh; + EditableSubMesh sub; + EditableVertex v; + sub.vertices = {v, v, v}; + EditableTriangle t; + t.indices[0] = 0; t.indices[1] = 1; t.indices[2] = 2; + sub.triangles.push_back(t); + // faces intentionally empty — triangle-only legacy submesh. + mesh.subMeshes().push_back(std::move(sub)); + + syncTriangulation(mesh.subMeshes()); + EXPECT_EQ(mesh.subMeshes()[0].triangles.size(), 1u); + EXPECT_TRUE(mesh.subMeshes()[0].faces.empty()); +} + +TEST(EditableMeshStandalone, TotalFaceCountFallsBackToTriangleCountForLegacy) { + EditableMesh mesh; + EditableSubMesh sub; + EditableVertex v; + sub.vertices = {v, v, v, v, v}; + EditableTriangle t; + t.indices[0] = 0; t.indices[1] = 1; t.indices[2] = 2; + sub.triangles.push_back(t); + t.indices[0] = 0; t.indices[1] = 2; t.indices[2] = 3; + sub.triangles.push_back(t); + t.indices[0] = 0; t.indices[1] = 3; t.indices[2] = 4; + sub.triangles.push_back(t); + mesh.subMeshes().push_back(std::move(sub)); + + EXPECT_EQ(totalFaceCount(mesh.subMeshes()), 3u) + << "legacy submesh: face count == triangle count"; +} + +TEST(EditableMeshStandalone, TotalFaceCountReportsNGonsWhenPresent) { + EditableMesh mesh; + EditableSubMesh sub; + EditableVertex v; + sub.vertices = {v, v, v, v, v}; + EditableFace pent; + pent.indices = {0, 1, 2, 3, 4}; + sub.faces.push_back(std::move(pent)); + triangulateFaces(sub); // produces 3 fan tris + mesh.subMeshes().push_back(std::move(sub)); + + EXPECT_EQ(totalFaceCount(mesh.subMeshes()), 1u) + << "pentagon: 1 face (n-gon canonical), not 3 (triangle mirror)"; + EXPECT_EQ(mesh.totalTriangleCount(), 3u); +} + +TEST(EditableMeshStandalone, RecalculateNormalsResyncsTrianglesFromFaces) { + // Build a quad mesh where `triangles` is intentionally stale — + // recalculateNormals should triangulate from `faces` first so the + // resulting normals reflect the live geometry, not the stale tris. + EditableMesh mesh; + EditableSubMesh sub; + auto mkV = [](float x, float y, float z) { + EditableVertex v; + v.position = Ogre::Vector3(x, y, z); + v.hasNormal = true; + v.normal = Ogre::Vector3::UNIT_Z; + return v; + }; + // Quad in the XY plane with normal +Z. + sub.vertices = { + mkV(0, 0, 0), mkV(1, 0, 0), mkV(1, 1, 0), mkV(0, 1, 0), + }; + EditableFace f; + f.indices = {0, 1, 2, 3}; + sub.faces.push_back(std::move(f)); + // triangles starts empty; recalculateNormals must sync it before + // accumulating normals. + mesh.subMeshes().push_back(std::move(sub)); + + mesh.recalculateNormals(); + + // After recalc, triangles must be the fan triangulation (2 tris). + EXPECT_EQ(mesh.subMeshes()[0].triangles.size(), 2u); + + // Every vertex should end up with normal == +Z (within tolerance). + for (const auto& v : mesh.subMeshes()[0].vertices) { + EXPECT_NEAR(v.normal.z, 1.0f, 1e-4f); + EXPECT_NEAR(v.normal.x, 0.0f, 1e-4f); + EXPECT_NEAR(v.normal.y, 0.0f, 1e-4f); + } +}