Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 57 additions & 14 deletions src/EditableMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>(editSub->vertices.size());

auto* decl = subMesh->vertexData->vertexDeclaration;
auto* binding = subMesh->vertexData->vertexBufferBinding;
Expand All @@ -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);
Expand All @@ -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<float*>(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;
Expand All @@ -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<uint32_t*>(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<uint16_t*>(ibuf->lock(Ogre::HardwareBuffer::HBL_DISCARD));
for (const auto& tri : editSub.triangles) {
for (const auto& tri : editSub->triangles) {
*idx++ = static_cast<uint16_t>(tri.indices[0]);
*idx++ = static_cast<uint16_t>(tri.indices[1]);
*idx++ = static_cast<uint16_t>(tri.indices[2]);
Expand All @@ -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<uint32_t>(editSub->triangles.size() * 3);
subMesh->indexData->indexStart = 0;
}

Expand Down Expand Up @@ -543,6 +563,22 @@ size_t EditableMesh::totalTriangleCount() const
return total;
}

size_t totalFaceCount(const std::vector<EditableSubMesh>& 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<EditableSubMesh>& 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())
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
27 changes: 27 additions & 0 deletions src/EditableMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<EditableSubMesh>& 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<EditableSubMesh>& subMeshes);

/**
* @brief Indexed mesh representation for topology queries and editing.
*
Expand Down
108 changes: 108 additions & 0 deletions src/EditableMesh_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading