Merge pull request #27064 from ScholliYT/addObjectNameTo3mfExport

Mesh: Add object name attribute to 3MF export
This commit is contained in:
Chris Hennes
2026-02-08 03:22:56 -06:00
committed by GitHub
9 changed files with 136 additions and 23 deletions

View File

@@ -117,6 +117,44 @@ std::basic_string<XMLCh> XMLTools::toXMLString(const char* const fromTranscode)
return str;
}
/*!
* \brief Escape special XML characters in a string.
*
* Replaces XML special characters (&, <, >, ", ') with their entity equivalents
* (&amp;, &lt;, &gt;, &quot;, &apos;).
*
* \param input The string to escape
* \return The escaped string safe for use in XML content or attributes
*/
std::string XMLTools::escapeXml(const std::string& input)
{
std::string output;
output.reserve(input.size());
for (char ch : input) {
switch (ch) {
case '&':
output.append("&amp;");
break;
case '<':
output.append("&lt;");
break;
case '>':
output.append("&gt;");
break;
case '"':
output.append("&quot;");
break;
case '\'':
output.append("&apos;");
break;
default:
output.push_back(ch);
break;
}
}
return output;
}
void XMLTools::terminate()
{
transcoder.reset();

View File

@@ -47,6 +47,7 @@ class BaseExport XMLTools
public:
static std::string toStdString(const XMLCh* const toTranscode);
static std::basic_string<XMLCh> toXMLString(const char* const fromTranscode);
static std::string escapeXml(const std::string& input);
static void initialize();
static void terminate();

View File

@@ -29,6 +29,7 @@
#include "Core/Evaluation.h"
#include "Core/MeshKernel.h"
#include <Base/Tools.h>
#include <Base/XMLTools.h>
#include "Writer3MF.h"
@@ -74,11 +75,11 @@ void Writer3MF::Finish(std::ostream& str)
str << "</model>\n";
}
bool Writer3MF::AddMesh(const MeshKernel& mesh, const Base::Matrix4D& mat)
bool Writer3MF::AddMesh(const MeshKernel& mesh, const Base::Matrix4D& mat, const std::string& name)
{
int id = ++objectIndex;
SaveBuildItem(id, mat);
return SaveObject(zip, id, mesh);
return SaveObject(zip, id, mesh, name);
}
void Writer3MF::AddResource(const Resource3MF& res)
@@ -111,7 +112,7 @@ bool Writer3MF::Save()
return true;
}
bool Writer3MF::SaveObject(std::ostream& str, int id, const MeshKernel& mesh) const
bool Writer3MF::SaveObject(std::ostream& str, int id, const MeshKernel& mesh, const std::string& name) const
{
// NOLINTBEGIN(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers)
const MeshPointArray& rPoints = mesh.GetPoints();
@@ -121,7 +122,11 @@ bool Writer3MF::SaveObject(std::ostream& str, int id, const MeshKernel& mesh) co
return false;
}
str << Base::blanks(2) << "<object id=\"" << id << "\" type=\"" << GetType(mesh) << "\">\n";
str << Base::blanks(2) << "<object id=\"" << id << "\" type=\"" << GetType(mesh) << "\"";
if (!name.empty()) {
str << " name=\"" << XMLTools::escapeXml(name) << "\"";
}
str << ">\n";
str << Base::blanks(3) << "<mesh>\n";
// vertices

View File

@@ -76,9 +76,10 @@ public:
* \brief Add a mesh object resource to the 3MF file.
* \param mesh The mesh object to be written
* \param mat The placement of the mesh object
* \param name The name of the mesh object which can later be displayed by downstream applications
* \return true if the added mesh could be written successfully, false otherwise.
*/
bool AddMesh(const MeshKernel& mesh, const Base::Matrix4D& mat);
bool AddMesh(const MeshKernel& mesh, const Base::Matrix4D& mat, const std::string& name = "");
/*!
* \brief AddResource
* Add an additional resource to the 3MF file.
@@ -97,7 +98,7 @@ private:
std::string GetType(const MeshKernel& mesh) const;
void SaveBuildItem(int id, const Base::Matrix4D& mat);
static std::string DumpMatrix(const Base::Matrix4D& mat);
bool SaveObject(std::ostream& str, int id, const MeshKernel& mesh) const;
bool SaveObject(std::ostream& str, int id, const MeshKernel& mesh, const std::string& name) const;
bool SaveRels(std::ostream& str) const;
bool SaveContent(std::ostream& str) const;

View File

@@ -2193,7 +2193,7 @@ void MeshOutput::SaveXML(Base::Writer& writer) const
bool MeshOutput::Save3MF(std::ostream& output) const
{
Writer3MF writer(output);
writer.AddMesh(_rclMesh, _transform);
writer.AddMesh(_rclMesh, _transform, objectName);
return writer.Save();
}

View File

@@ -38,6 +38,7 @@
#include <Base/Sequencer.h>
#include <Base/Stream.h>
#include <Base/Tools.h>
#include <Base/XMLTools.h>
#include "Core/Iterator.h"
#include "Core/IO/Writer3MF.h"
#include <zipios++/zipoutputstream.h>
@@ -91,17 +92,6 @@ static std::vector<std::string> expandSubObjectNames(
Exporter::Exporter() = default;
// static
std::string Exporter::xmlEscape(const std::string& input)
{
std::string out(input);
boost::replace_all(out, "&", "&amp;");
boost::replace_all(out, "\"", "&quot;");
boost::replace_all(out, "'", "&apos;");
boost::replace_all(out, "<", "&lt;");
boost::replace_all(out, ">", "&gt;");
return out;
}
int Exporter::addObject(App::DocumentObject* obj, float tol)
{
int count = 0;
@@ -304,8 +294,7 @@ Exporter3MF::~Exporter3MF()
bool Exporter3MF::addMesh(const char* name, const MeshObject& mesh)
{
boost::ignore_unused(name);
bool ok = d->writer3mf.AddMesh(mesh.getKernel(), mesh.getTransform());
bool ok = d->writer3mf.AddMesh(mesh.getKernel(), mesh.getTransform(), name);
if (ok) {
for (const auto& it : d->ext) {
d->writer3mf.AddResource(it->addMesh(mesh));
@@ -421,7 +410,8 @@ bool ExporterAMF::addMesh(const char* name, const MeshObject& mesh)
Base::SequencerLauncher seq("Saving...", 2 * numFacets + 1);
*outputStreamPtr << "\t<object id=\"" << nextObjectIndex << "\">\n";
*outputStreamPtr << "\t\t<metadata type=\"name\">" << xmlEscape(name) << "</metadata>\n";
*outputStreamPtr << "\t\t<metadata type=\"name\">" << XMLTools::escapeXml(name)
<< "</metadata>\n";
*outputStreamPtr << "\t\t<mesh>\n"
<< "\t\t\t<vertices>\n";

View File

@@ -69,8 +69,6 @@ public:
Exporter& operator=(Exporter&&) = delete;
protected:
/// Does some simple escaping of characters for XML-type exports
static std::string xmlEscape(const std::string& input);
void throwIfNoPermission(const std::string&);
std::map<const App::DocumentObject*, std::vector<std::string>> subObjectNameCache;

View File

@@ -29,6 +29,7 @@ add_executable(Base_tests_run
Vector3D.cpp
ViewProj.cpp
Writer.cpp
XMLTools.cpp
)
setup_qt_test(InventorBuilder)

View File

@@ -0,0 +1,79 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include <gtest/gtest.h>
#include <Base/XMLTools.h>
class TestXMLTools: public testing::Test
{
};
TEST_F(TestXMLTools, EscapeXmlEmptyString)
{
EXPECT_EQ(XMLTools::escapeXml(""), "");
}
TEST_F(TestXMLTools, EscapeXmlNoChangesForSafeText)
{
EXPECT_EQ(XMLTools::escapeXml("abcXYZ_123"), "abcXYZ_123");
EXPECT_EQ(XMLTools::escapeXml("hello world"), "hello world");
EXPECT_EQ(XMLTools::escapeXml("line1\nline2\tend"), "line1\nline2\tend");
}
TEST_F(TestXMLTools, EscapeXmlEscapesAmpersand)
{
EXPECT_EQ(XMLTools::escapeXml("&"), "&amp;");
EXPECT_EQ(XMLTools::escapeXml("a&b"), "a&amp;b");
}
TEST_F(TestXMLTools, EscapeXmlEscapesLessThanAndGreaterThan)
{
EXPECT_EQ(XMLTools::escapeXml("<"), "&lt;");
EXPECT_EQ(XMLTools::escapeXml(">"), "&gt;");
EXPECT_EQ(XMLTools::escapeXml("a<b"), "a&lt;b");
EXPECT_EQ(XMLTools::escapeXml("a>b"), "a&gt;b");
EXPECT_EQ(XMLTools::escapeXml("a<b>c"), "a&lt;b&gt;c");
}
TEST_F(TestXMLTools, EscapeXmlEscapesQuotes)
{
EXPECT_EQ(XMLTools::escapeXml("\""), "&quot;");
EXPECT_EQ(XMLTools::escapeXml("'"), "&apos;");
EXPECT_EQ(XMLTools::escapeXml("a\"b"), "a&quot;b");
EXPECT_EQ(XMLTools::escapeXml("a'b"), "a&apos;b");
}
TEST_F(TestXMLTools, EscapeXmlEscapesAllFiveInOneString)
{
// input: & < > " '
EXPECT_EQ(XMLTools::escapeXml("&<>\"'"), "&amp;&lt;&gt;&quot;&apos;");
}
TEST_F(TestXMLTools, EscapeXmlDoesNotDoubleEscapeNewlyInsertedEntities)
{
// This test specifically catches the classic bug where you replace
// '<' with "&lt;" and then later replace '&' with "&amp;" and end up with "&amp;lt;".
EXPECT_EQ(XMLTools::escapeXml("<"), "&lt;");
EXPECT_EQ(XMLTools::escapeXml(">"), "&gt;");
EXPECT_EQ(XMLTools::escapeXml("\""), "&quot;");
EXPECT_EQ(XMLTools::escapeXml("'"), "&apos;");
}
TEST_F(TestXMLTools, EscapeXmlComplexMixedContent)
{
const std::string in = "Tom & Jerry <\"fun\"> 'n' games";
const std::string out = "Tom &amp; Jerry &lt;&quot;fun&quot;&gt; &apos;n&apos; games";
EXPECT_EQ(XMLTools::escapeXml(in), out);
}
TEST_F(TestXMLTools, EscapeXmlMultipleAdjacentCharacters)
{
EXPECT_EQ(XMLTools::escapeXml("&&&&"), "&amp;&amp;&amp;&amp;");
EXPECT_EQ(XMLTools::escapeXml("<<<<"), "&lt;&lt;&lt;&lt;");
EXPECT_EQ(XMLTools::escapeXml("\"\"''"), "&quot;&quot;&apos;&apos;");
}
TEST_F(TestXMLTools, EscapeXmlPreservesWhitespaceAndControlCommonInText)
{
EXPECT_EQ(XMLTools::escapeXml(" a \r\n b \t c "), " a \r\n b \t c ");
}