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
12 changes: 12 additions & 0 deletions src/EditModeController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1480,6 +1480,18 @@
}
// LCOV_EXCL_STOP

void EditModeController::flushPendingVertexPaintForEntity(Ogre::Entity* entity)

Check warning on line 1483 in src/EditModeController.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make the type of this parameter a pointer-to-const. The current type of "entity" is "class Ogre::Entity *".

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3blCYPqqJtg0faUFt0&open=AZ3blCYPqqJtg0faUFt0&pullRequest=346
{
if (!entity || !m_editModeActive || m_editEntity != entity || !m_editableMesh)
return;
SentryReporter::addBreadcrumb(
"file.export",
QStringLiteral("Flushed pending vertex paint before export for entity '%1'")
.arg(QString::fromStdString(entity->getName())));
m_vertexPaintFlushPending = false;
m_editableMesh->commitVertexColorsToEntity(m_editEntity);
}

bool EditModeController::beginVertexPaintStroke(OgreWidget* widget, const QPoint& screenPos)
{
if (!m_vertexPaintEnabled || m_vertexPaintStrokeActive)
Expand Down
7 changes: 7 additions & 0 deletions src/EditModeController.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@ class EditModeController : public QObject
void setVertexPaintRadius(double r);
double vertexPaintStrength() const { return m_vertexPaintStrength; }
void setVertexPaintStrength(double s);

/**
* @brief Commits deferred vertex paint from EditableMesh into GPU vertex buffers.
* @param entity Must be the active edit target; otherwise this is a no-op.
* @note Call before mesh/FBX/glTF export so coalesced paint is not still pending on the event loop.
*/
void flushPendingVertexPaintForEntity(Ogre::Entity* entity);
/// @}

/// @name Topology operations
Expand Down
90 changes: 90 additions & 0 deletions src/FBX/FBXExporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,88 @@
m_w.endNode(); // LayerElementUV
}

// ── LayerElementColor (vertex colors) ──
const auto* colElem = vData->vertexDeclaration->findElementBySemantic(Ogre::VES_DIFFUSE);
if (colElem)
{
std::vector<double> colors(vData->vertexCount * 4);
auto vbuf = vData->vertexBufferBinding->getBuffer(colElem->getSource());
auto* base = static_cast<const unsigned char*>(
vbuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
for (size_t j = 0; j < vData->vertexCount; ++j)
{
const Ogre::RGBA* p;
colElem->baseVertexPointerToElement(
const_cast<unsigned char*>(base + j * vbuf->getVertexSize()), &p);

Check failure on line 1035 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

const_cast removing const qualification from the type of a pointer may lead to an undefined behaviour.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3S&open=AZ3bNB2uNdHnfRgrLh3S&pullRequest=346
Ogre::ColourValue cv;
if (colElem->getType() == Ogre::VET_COLOUR_ABGR)

Check failure on line 1037 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3P&open=AZ3bNB2uNdHnfRgrLh3P&pullRequest=346
cv.setAsABGR(*p);
else
cv.setAsARGB(*p);
colors[j * 4 + 0] = cv.r;
colors[j * 4 + 1] = cv.g;
colors[j * 4 + 2] = cv.b;
colors[j * 4 + 3] = cv.a;
}
vbuf->unlock();

// Expand to per-polygon-vertex with reversed winding to match PolygonVertexIndex
std::vector<double> expandedColors;
if (iData && iData->indexCount > 0)
{
expandedColors.resize(iData->indexCount * 4);
auto ibuf = iData->indexBuffer;
auto* ibase2 = static_cast<const unsigned char*>(
ibuf->lock(Ogre::HardwareBuffer::HBL_READ_ONLY));
bool use32 = ibuf->getType() == Ogre::HardwareIndexBuffer::IT_32BIT;
for (size_t f = 0; f < iData->indexCount / 3; ++f)

Check failure on line 1057 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not nest more than 3 if|for|do|while|switch statements.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3Q&open=AZ3bNB2uNdHnfRgrLh3Q&pullRequest=346
{
uint32_t vi0 = use32
? reinterpret_cast<const uint32_t*>(ibase2)[f * 3 + 0]

Check warning on line 1060 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "reinterpret_cast" with a safer operation.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3T&open=AZ3bNB2uNdHnfRgrLh3T&pullRequest=346
: reinterpret_cast<const uint16_t*>(ibase2)[f * 3 + 0];

Check warning on line 1061 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "reinterpret_cast" with a safer operation.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3U&open=AZ3bNB2uNdHnfRgrLh3U&pullRequest=346
uint32_t vi1 = use32
? reinterpret_cast<const uint32_t*>(ibase2)[f * 3 + 1]

Check warning on line 1063 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "reinterpret_cast" with a safer operation.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3V&open=AZ3bNB2uNdHnfRgrLh3V&pullRequest=346
: reinterpret_cast<const uint16_t*>(ibase2)[f * 3 + 1];

Check warning on line 1064 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "reinterpret_cast" with a safer operation.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3W&open=AZ3bNB2uNdHnfRgrLh3W&pullRequest=346
uint32_t vi2 = use32
? reinterpret_cast<const uint32_t*>(ibase2)[f * 3 + 2]

Check warning on line 1066 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "reinterpret_cast" with a safer operation.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3X&open=AZ3bNB2uNdHnfRgrLh3X&pullRequest=346
: reinterpret_cast<const uint16_t*>(ibase2)[f * 3 + 2];

Check warning on line 1067 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace "reinterpret_cast" with a safer operation.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3Y&open=AZ3bNB2uNdHnfRgrLh3Y&pullRequest=346

// Reversed winding: (v0, v2, v1)
size_t base = f * 12;

Check warning on line 1070 in src/FBX/FBXExporter.cpp

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Declaration shadows a local variable "base" in the outer scope.

See more on https://sonarcloud.io/project/issues?id=fernandotonon_QtMeshEditor&issues=AZ3bNB2uNdHnfRgrLh3R&open=AZ3bNB2uNdHnfRgrLh3R&pullRequest=346
auto copy = [&](size_t outVertex, uint32_t vi) {
expandedColors[outVertex + 0] = colors[vi * 4 + 0];
expandedColors[outVertex + 1] = colors[vi * 4 + 1];
expandedColors[outVertex + 2] = colors[vi * 4 + 2];
expandedColors[outVertex + 3] = colors[vi * 4 + 3];
};
copy(base + 0, vi0);
copy(base + 4, vi2);
copy(base + 8, vi1);
}
ibuf->unlock();
}
else
{
expandedColors = colors;
}

m_w.beginNode("LayerElementColor");
m_w.writePropertyI(0);
m_w.endProperties();

m_w.beginNode("Version"); m_w.writePropertyI(101); m_w.endProperties(); m_w.endNodeLeaf();
m_w.beginNode("Name"); m_w.writePropertyS(""); m_w.endProperties(); m_w.endNodeLeaf();
m_w.beginNode("MappingInformationType"); m_w.writePropertyS("ByPolygonVertex"); m_w.endProperties(); m_w.endNodeLeaf();
m_w.beginNode("ReferenceInformationType"); m_w.writePropertyS("Direct"); m_w.endProperties(); m_w.endNodeLeaf();

m_w.beginNode("Colors");
m_w.writePropertyArrayD(expandedColors);
m_w.endProperties();
m_w.endNodeLeaf();

m_w.endNode(); // LayerElementColor
}

// ── LayerElementMaterial ──
{
// Each Model has exactly one material connected, so index is always 0
Expand Down Expand Up @@ -1065,6 +1147,14 @@
m_w.beginNode("TypedIndex"); m_w.writePropertyI(0); m_w.endProperties(); m_w.endNodeLeaf();
m_w.endNode();
}
if (colElem)
{
m_w.beginNode("LayerElement");
m_w.endProperties();
m_w.beginNode("Type"); m_w.writePropertyS("LayerElementColor"); m_w.endProperties(); m_w.endNodeLeaf();
m_w.beginNode("TypedIndex"); m_w.writePropertyI(0); m_w.endProperties(); m_w.endNodeLeaf();
m_w.endNode();
}
{
m_w.beginNode("LayerElement");
m_w.endProperties();
Expand Down
59 changes: 59 additions & 0 deletions src/FBX/FBXExporter_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,65 @@ TEST_F(FBXExporterCoverageTest, UVs_VFlipped) {
cleanup(r);
}

TEST_F(FBXExporterCoverageTest, VertexColors_WritesLayerElementColor) {
const std::string name = uniqueName("fbx_colors");
auto meshPtr = createInMemoryTriangleMeshWithVertexColors(name);
ASSERT_TRUE(!!meshPtr);
auto* node = Manager::getSingleton()->addSceneNode(QString::fromStdString(name + "_node"));
auto* entity = Manager::getSingleton()->createEntity(node, meshPtr);
ASSERT_NE(entity, nullptr);

auto r = exportAndParse(entity);
ASSERT_TRUE(r.success);

auto* objects = findTopLevel(r.nodes, "Objects");
ASSERT_NE(objects, nullptr);
auto geomNodes = objects->findAll("Geometry");
ASSERT_EQ(geomNodes.size(), 1u);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

auto* colorLayer = geomNodes[0]->find("LayerElementColor");
ASSERT_NE(colorLayer, nullptr);
auto* mapType = colorLayer->find("MappingInformationType");
ASSERT_NE(mapType, nullptr);
EXPECT_EQ(mapType->properties[0].stringVal, "ByPolygonVertex");
auto* refType = colorLayer->find("ReferenceInformationType");
ASSERT_NE(refType, nullptr);
EXPECT_EQ(refType->properties[0].stringVal, "Direct");

auto* colors = colorLayer->find("Colors");
ASSERT_NE(colors, nullptr);
// One tri × 3 polygon-vertices × 4 components (exporter reverses winding: v0, v2, v1).
ASSERT_EQ(colors->properties[0].doubleArray.size(), 12u);
const auto& c = colors->properties[0].doubleArray;
EXPECT_NEAR(c[0], 1.0, 1e-4);
EXPECT_NEAR(c[1], 0.0, 1e-4);
EXPECT_NEAR(c[2], 0.0, 1e-4);
EXPECT_NEAR(c[3], 1.0, 1e-4); // red corner (vi0)
EXPECT_NEAR(c[4], 0.0, 1e-4);
EXPECT_NEAR(c[5], 0.0, 1e-4);
EXPECT_NEAR(c[6], 1.0, 1e-4);
EXPECT_NEAR(c[7], 1.0, 1e-4); // blue corner (vi2)
EXPECT_NEAR(c[8], 0.0, 1e-4);
EXPECT_NEAR(c[9], 1.0, 1e-4);
EXPECT_NEAR(c[10], 0.0, 1e-4);
EXPECT_NEAR(c[11], 1.0, 1e-4); // green corner (vi1)

auto* layer = geomNodes[0]->find("Layer");
ASSERT_NE(layer, nullptr);
bool hasColor = false;
for (const auto* le : layer->findAll("LayerElement")) {
auto* typeNode = le->find("Type");
if (typeNode && !typeNode->properties.empty()
&& typeNode->properties[0].stringVal == "LayerElementColor") {
hasColor = true;
break;
}
}
EXPECT_TRUE(hasColor);

cleanup(r);
}

TEST_F(FBXExporterCoverageTest, NoNormals_SkipsNormalLayer) {
auto name = uniqueName("nonorm");
auto* entity = createMeshNoNormalsNoUVs(name);
Expand Down
Loading
Loading