Mesh: Add object name attribute to 3MF export

Export mesh object names as 'name' attribute in 3MF files according to
the 3MF specification. This allows downstream tools to identify objects
by their original FreeCAD names.

Changes:
- Moved Exporter::xmlEscape() to Base XMLTools::escapeXml() for shared XML entity escaping
- Update Writer3MF::AddMesh() to accept and output object names with default "" where no attribute will be added
- Updated all codepaths leading to 3mf exports to include the object name
This commit is contained in:
Tom Stein
2026-01-21 16:23:27 +01:00
committed by Chris Hennes
parent 44fd6754d8
commit 49c80ebe5c
7 changed files with 56 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;