From 90d2c1ddf36f4ed8b3cdf6853846bcce1f8ba3db Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 28 May 2025 11:02:38 +0200 Subject: [PATCH 1/7] Load UV coordinates for mesh data CURA-12544 --- .clang-format | 2 +- include/Savitar/Face.h | 8 ++- include/Savitar/MeshData.h | 16 +++++- include/Savitar/Scene.h | 6 ++ include/Savitar/TextureData.h | 42 ++++++++++++++ include/Savitar/UVCoordinate.h | 27 +++++++++ include/Savitar/UVCoordinatesIndices.h | 28 ++++++++++ src/Face.cpp | 11 +++- src/MeshData.cpp | 76 ++++++++++++++++++++++++-- src/Scene.cpp | 18 +++++- src/TextureData.cpp | 69 +++++++++++++++++++++++ src/UVCoordinate.cpp | 22 ++++++++ src/UVCoordinatesIndices.cpp | 34 ++++++++++++ 13 files changed, 347 insertions(+), 12 deletions(-) create mode 100644 include/Savitar/TextureData.h create mode 100644 include/Savitar/UVCoordinate.h create mode 100644 include/Savitar/UVCoordinatesIndices.h create mode 100644 src/TextureData.cpp create mode 100644 src/UVCoordinate.cpp create mode 100644 src/UVCoordinatesIndices.cpp diff --git a/.clang-format b/.clang-format index 04eda27..1a4d036 100644 --- a/.clang-format +++ b/.clang-format @@ -38,7 +38,7 @@ BreakBeforeBraces: Allman BreakBeforeTernaryOperators: true BreakConstructorInitializers: BeforeComma BreakStringLiterals: true -ColumnLimit: 240 +ColumnLimit: 180 CommentPragmas: '^ IWYU pragma:' CompactNamespaces: false PackConstructorInitializers: CurrentLine diff --git a/include/Savitar/Face.h b/include/Savitar/Face.h index 555b981..a5bc0d0 100644 --- a/include/Savitar/Face.h +++ b/include/Savitar/Face.h @@ -4,6 +4,10 @@ #ifndef FACE_H #define FACE_H +#include "Savitar/UVCoordinatesIndices.h" + +#include + namespace Savitar { class Face @@ -12,17 +16,19 @@ class Face /** * A face uses the index of 3 vertices to describe a triangle */ - Face(int v1, int v2, int v3); + Face(int v1, int v2, int v3, const std::optional& uv_coordinates = std::nullopt); ~Face() = default; [[nodiscard]] int getV1() const; [[nodiscard]] int getV2() const; [[nodiscard]] int getV3() const; + [[nodiscard]] const std::optional& getUVCoordinates() const; private: int vertex_1_index_; int vertex_2_index_; int vertex_3_index_; + std::optional uv_coordinates_; }; } // namespace Savitar diff --git a/include/Savitar/MeshData.h b/include/Savitar/MeshData.h index 4bdafb6..209a635 100644 --- a/include/Savitar/MeshData.h +++ b/include/Savitar/MeshData.h @@ -4,8 +4,6 @@ #ifndef MESHDATA_H #define MESHDATA_H -#include -#include #include #include "Savitar/Face.h" @@ -16,6 +14,9 @@ namespace Savitar { + +class Scene; + class MeshData { public: @@ -56,6 +57,10 @@ class MeshData */ [[nodiscard]] bytearray getFlatVerticesAsBytes(); + [[nodiscard]] bytearray getUVCoordinatesPerVertexAsBytes(const Scene* scene) const; + + [[nodiscard]] std::string getTexturePath(const Scene* scene) const; + /** * Set the vertices of the meshdata by bytearray (as set from python) * @@ -72,14 +77,21 @@ class MeshData [[nodiscard]] std::vector getVertices(); + [[nodiscard]] int getUVGroupId() const; + /** * Reset the data of the MeshData object. */ void clear(); +private: + template + static void exportToByteArray(bytearray& data, const T value); + private: std::vector vertices_; std::vector faces_; + int uv_group_id_{ -1 }; }; } // namespace Savitar diff --git a/include/Savitar/Scene.h b/include/Savitar/Scene.h index 50d2cef..38ccd1b 100644 --- a/include/Savitar/Scene.h +++ b/include/Savitar/Scene.h @@ -5,6 +5,7 @@ #define SCENE_H #include "Savitar/SceneNode.h" +#include "TextureData.h" #include // For std::map #include // For std::string @@ -76,10 +77,15 @@ class Scene void setUnit(std::string unit); + [[nodiscard]] std::string getTexturePathFromGroupId(const int uv_group_id) const; + + [[nodiscard]] const TextureData::UVCoordinatesGroup* getUVCoordinatesGroup(const int uv_group_id) const; + private: std::vector scene_nodes_; std::map metadata_; std::string unit_{ "millimeter" }; + TextureData texture_data_; /** * Used to recursively create SceneNode objects based on xml nodes. diff --git a/include/Savitar/TextureData.h b/include/Savitar/TextureData.h new file mode 100644 index 0000000..31d42de --- /dev/null +++ b/include/Savitar/TextureData.h @@ -0,0 +1,42 @@ +// Copyright (c) 2025 Ultimaker B.V. +// libSavitar is released under the terms of the LGPLv3 or higher. + +#ifndef TEXTUREDATA_H +#define TEXTUREDATA_H + +#include "UVCoordinate.h" + +#include +#include +#include +#include + +namespace Savitar +{ +class TextureData +{ +public: + struct UVCoordinatesGroup + { + int texture_id; + std::vector coordinates; + }; + + TextureData(); + virtual ~TextureData() = default; + + void fillByXMLNode(pugi::xml_node xml_node); + + [[nodiscard]] std::string getTexturePath(const int texture_id) const; + + [[nodiscard]] const UVCoordinatesGroup* getUVCoordinatesGroup(const int id) const; + + [[nodiscard]] std::string getTexturePathFromGroupId(const int uv_group_id) const; + +private: + std::map textures_paths_; + std::map uv_coordinates_; +}; +} // namespace Savitar + +#endif \ No newline at end of file diff --git a/include/Savitar/UVCoordinate.h b/include/Savitar/UVCoordinate.h new file mode 100644 index 0000000..4060ba5 --- /dev/null +++ b/include/Savitar/UVCoordinate.h @@ -0,0 +1,27 @@ +// Copyright (c) 2025 Ultimaker B.V. +// libSavitar is released under the terms of the LGPLv3 or higher. + +#ifndef UVCOORDINATE_H +#define UVCOORDINATE_H + +namespace Savitar +{ +class UVCoordinate +{ +public: + /** + * A UV coordinate represents the position of the point on a texture image. + */ + UVCoordinate(float u, float v); + virtual ~UVCoordinate() = default; + + [[nodiscard]] float getU() const; + [[nodiscard]] float getV() const; + +private: + float u_; + float v_; +}; +} // namespace Savitar + +#endif \ No newline at end of file diff --git a/include/Savitar/UVCoordinatesIndices.h b/include/Savitar/UVCoordinatesIndices.h new file mode 100644 index 0000000..1decb6c --- /dev/null +++ b/include/Savitar/UVCoordinatesIndices.h @@ -0,0 +1,28 @@ +// Copyright (c) 2025 Ultimaker B.V. +// libSavitar is released under the terms of the LGPLv3 or higher. + +#ifndef UVCOORDINATESINDICES_H +#define UVCOORDINATESINDICES_H + +namespace Savitar +{ +class UVCoordinatesIndices +{ +public: + UVCoordinatesIndices(int group_index, int vertex_1_index, int vertex_2_index, int vertex_3_index); + virtual ~UVCoordinatesIndices() = default; + + [[nodiscard]] int getGroupIndex() const; + [[nodiscard]] int getV1() const; + [[nodiscard]] int getV2() const; + [[nodiscard]] int getV3() const; + +private: + int group_index_{}; + int vertex_1_index_{}; + int vertex_2_index_{}; + int vertex_3_index_{}; +}; +} // namespace Savitar + +#endif \ No newline at end of file diff --git a/src/Face.cpp b/src/Face.cpp index c4e6f1c..65d84e3 100644 --- a/src/Face.cpp +++ b/src/Face.cpp @@ -5,7 +5,11 @@ using namespace Savitar; -Face::Face(int v1, int v2, int v3) : vertex_1_index_{ v1 }, vertex_2_index_{ v2 }, vertex_3_index_{ v3 } +Face::Face(int v1, int v2, int v3, const std::optional& uv_coordinates) + : vertex_1_index_{ v1 } + , vertex_2_index_{ v2 } + , vertex_3_index_{ v3 } + , uv_coordinates_(uv_coordinates) { } @@ -23,3 +27,8 @@ int Face::getV3() const { return vertex_3_index_; } + +const std::optional& Face::getUVCoordinates() const +{ + return uv_coordinates_; +} diff --git a/src/MeshData.cpp b/src/MeshData.cpp index 48ec377..84cd4e6 100644 --- a/src/MeshData.cpp +++ b/src/MeshData.cpp @@ -2,9 +2,11 @@ // libSavitar is released under the terms of the LGPLv3 or higher. #include "Savitar/MeshData.h" + +#include "Savitar/Scene.h" + #include #include -#include #include //For std::runtime_error. using namespace Savitar; @@ -37,8 +39,19 @@ void MeshData::fillByXMLNode(pugi::xml_node xml_node) pugi::xml_node xml_triangles = xml_node.child("triangles"); for (pugi::xml_node face = xml_triangles.child("triangle"); face != nullptr; face = face.next_sibling("triangle")) { - Face temp_face = Face(face.attribute("v1").as_int(), face.attribute("v2").as_int(), face.attribute("v3").as_int()); - this->faces_.push_back(temp_face); + const pugi::xml_attribute attribute_uv_group_id = face.attribute("pid"); + const pugi::xml_attribute attribute_uv_id_v1 = face.attribute("p1"); + const pugi::xml_attribute attribute_uv_id_v2 = face.attribute("p2"); + const pugi::xml_attribute attribute_uv_id_v3 = face.attribute("p3"); + + std::optional uv_coordinates; + if (attribute_uv_group_id != nullptr && attribute_uv_id_v1 != nullptr && attribute_uv_id_v2 != nullptr && attribute_uv_id_v3 != nullptr) + { + uv_coordinates.emplace(attribute_uv_group_id.as_int(), attribute_uv_id_v1.as_int(), attribute_uv_id_v2.as_int(), attribute_uv_id_v3.as_int()); + uv_group_id_ = attribute_uv_group_id.as_int(); + } + + this->faces_.emplace_back(face.attribute("v1").as_int(), face.attribute("v2").as_int(), face.attribute("v3").as_int(), uv_coordinates); } } @@ -124,6 +137,50 @@ bytearray MeshData::getFlatVerticesAsBytes() return vertices_data; } +bytearray MeshData::getUVCoordinatesPerVertexAsBytes(const Scene* scene) const +{ + const TextureData::UVCoordinatesGroup* uv_coordinates_group = scene->getUVCoordinatesGroup(uv_group_id_); + if (uv_coordinates_group == nullptr) + { + return {}; + } + const std::vector& uv_coordinates = uv_coordinates_group->coordinates; + + bytearray uv_data; + uv_data.reserve(faces_.size() * 3 * 2); + + for (auto& face : faces_) + { + const std::optional& uv_coordinates_opt = face.getUVCoordinates(); + if (! uv_coordinates_opt.has_value()) + { + return {}; + } + const UVCoordinatesIndices& uv_coordinates_indices = uv_coordinates_opt.value(); + + for (const int uv_index : { uv_coordinates_indices.getV1(), uv_coordinates_indices.getV2(), uv_coordinates_indices.getV3() }) + { + if (uv_index >= 0 && uv_index < uv_coordinates.size()) + { + const UVCoordinate& uv_coordinate = uv_coordinates.at(uv_index); + exportToByteArray(uv_data, uv_coordinate.getU()); + exportToByteArray(uv_data, uv_coordinate.getV()); + } + else + { + return {}; + } + } + } + + return uv_data; +} + +std::string MeshData::getTexturePath(const Scene* scene) const +{ + return scene->getTexturePathFromGroupId(uv_group_id_); +} + bytearray MeshData::getFacesAsBytes() { bytearray face_data; @@ -190,7 +247,7 @@ void MeshData::setFacesFromBytes(const bytearray& data) for (int i = 0; i + 2 < num_ints; i += 3) { - Face temp_face = Face(int_array[i], int_array[i + 1], int_array[i + 2]); + Face temp_face = Face(int_array[i], int_array[i + 1], int_array[i + 2], std::nullopt); faces_.push_back(temp_face); } } @@ -199,3 +256,14 @@ std::vector MeshData::getVertices() { return vertices_; } + +int MeshData::getUVGroupId() const +{ + return uv_group_id_; +} + +template +void MeshData::exportToByteArray(bytearray& data, const T value) +{ + data.insert(data.end(), reinterpret_cast(&value), reinterpret_cast(&value) + sizeof(T)); +} \ No newline at end of file diff --git a/src/Scene.cpp b/src/Scene.cpp index 28ea2f3..1c5e697 100644 --- a/src/Scene.cpp +++ b/src/Scene.cpp @@ -27,8 +27,6 @@ void Scene::fillByXMLNode(pugi::xml_node xml_node) { unit_ = xml_node.attribute("unit").as_string(); - pugi::xml_node resources = xml_node.child("resources"); - // Handle metadata: for (pugi::xml_node metadata_node = xml_node.child("metadata"); metadata_node; metadata_node = metadata_node.next_sibling("metadata")) { @@ -44,10 +42,14 @@ void Scene::fillByXMLNode(pugi::xml_node xml_node) setMetaDataEntry(key, value, type, preserve); } + // Handle UV coordinates, which are stored outside the objects + pugi::xml_node resources = xml_node.child("resources"); + texture_data_.fillByXMLNode(resources); + pugi::xml_node build = xml_node.child("build"); for (pugi::xml_node item = build.child("item"); item != nullptr; item = item.next_sibling("item")) { - // Found a item in the build. The items are linked to objects by objectid. + // Found an item in the build. The items are linked to objects by objectid. pugi::xml_node object_node = resources.find_child_by_attribute("object", "id", item.attribute("objectid").value()); if (object_node != nullptr) { @@ -179,3 +181,13 @@ void Scene::setUnit(std::string unit) { unit_ = unit; } + +std::string Scene::getTexturePathFromGroupId(const int uv_group_id) const +{ + return texture_data_.getTexturePathFromGroupId(uv_group_id); +} + +const TextureData::UVCoordinatesGroup* Scene::getUVCoordinatesGroup(const int uv_group_id) const +{ + return texture_data_.getUVCoordinatesGroup(uv_group_id); +} diff --git a/src/TextureData.cpp b/src/TextureData.cpp new file mode 100644 index 0000000..185f075 --- /dev/null +++ b/src/TextureData.cpp @@ -0,0 +1,69 @@ +// Copyright (c) 2025 Ultimaker B.V. +// libSavitar is released under the terms of the LGPLv3 or higher. + +#include "Savitar/TextureData.h" + +#include + +using namespace Savitar; + +TextureData::TextureData() +{ +} + +void TextureData::fillByXMLNode(pugi::xml_node xml_node) +{ + // Handle textures paths + for (pugi::xml_node texture_node = xml_node.child("m:texture2d"); texture_node; texture_node = texture_node.next_sibling("m:texture2d")) + { + const int id = texture_node.attribute("id").as_int(-1); + const std::string path = texture_node.attribute("path").as_string(); + if (id >= 0 && ! path.empty()) + { + textures_paths_.emplace(id, std::move(path)); + } + } + + // Handle UV coordinates groups + for (pugi::xml_node texture_group_node = xml_node.child("m:texture2dgroup"); texture_group_node; texture_group_node = texture_group_node.next_sibling("m:texture2dgroup")) + { + const int id = texture_group_node.attribute("id").as_int(-1); + const int texture_id = texture_group_node.attribute("texid").as_int(-1); + if (id >= 0 && texture_id >= 0 && textures_paths_.find(texture_id) != textures_paths_.end()) + { + UVCoordinatesGroup group; + group.texture_id = texture_id; + + for (pugi::xml_node uv_coordinate_node = texture_group_node.child("m:tex2coord"); uv_coordinate_node; + uv_coordinate_node = uv_coordinate_node.next_sibling("m:tex2coord")) + { + group.coordinates.emplace_back(uv_coordinate_node.attribute("u").as_float(), uv_coordinate_node.attribute("v").as_float()); + } + + uv_coordinates_.emplace(id, std::move(group)); + } + } +} + +const TextureData::UVCoordinatesGroup* TextureData::getUVCoordinatesGroup(const int id) const +{ + auto iterator = uv_coordinates_.find(id); + return iterator != uv_coordinates_.end() ? &iterator->second : nullptr; +} + +std::string TextureData::getTexturePath(const int texture_id) const +{ + auto iterator = textures_paths_.find(texture_id); + return iterator != textures_paths_.end() ? iterator->second : ""; +} + +std::string TextureData::getTexturePathFromGroupId(const int uv_group_id) const +{ + const UVCoordinatesGroup* group = getUVCoordinatesGroup(uv_group_id); + if (! group) + { + return ""; + } + + return getTexturePath(group->texture_id); +} diff --git a/src/UVCoordinate.cpp b/src/UVCoordinate.cpp new file mode 100644 index 0000000..fd24036 --- /dev/null +++ b/src/UVCoordinate.cpp @@ -0,0 +1,22 @@ +// Copyright (c) 2025 Ultimaker B.V. +// libSavitar is released under the terms of the LGPLv3 or higher. + +#include "Savitar/UVCoordinate.h" + +using namespace Savitar; + +UVCoordinate::UVCoordinate(float u, float v) + : u_(u) + , v_(v) +{ +}; + +float UVCoordinate::getU() const +{ + return u_; +} + +float UVCoordinate::getV() const +{ + return v_; +} diff --git a/src/UVCoordinatesIndices.cpp b/src/UVCoordinatesIndices.cpp new file mode 100644 index 0000000..8a95e30 --- /dev/null +++ b/src/UVCoordinatesIndices.cpp @@ -0,0 +1,34 @@ +// Copyright (c) 2025 Ultimaker B.V. +// libSavitar is released under the terms of the LGPLv3 or higher. + +#include "Savitar/UVCoordinatesIndices.h" + +using namespace Savitar; + +UVCoordinatesIndices::UVCoordinatesIndices(int group_index, int vertex_1_index, int vertex_2_index, int vertex_3_index) + : group_index_(group_index) + , vertex_1_index_(vertex_1_index) + , vertex_2_index_(vertex_2_index) + , vertex_3_index_(vertex_3_index) +{ +} + +int UVCoordinatesIndices::getGroupIndex() const +{ + return group_index_; +} + +int UVCoordinatesIndices::getV1() const +{ + return vertex_1_index_; +} + +int UVCoordinatesIndices::getV2() const +{ + return vertex_2_index_; +} + +int UVCoordinatesIndices::getV3() const +{ + return vertex_3_index_; +} From 0e65f21ee96ae9211f92465945c4b9c9b864f71d Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 28 May 2025 15:12:16 +0200 Subject: [PATCH 2/7] Allow storing UV coordinates CURA-12544 --- include/Savitar/Face.h | 7 +++ include/Savitar/MeshData.h | 4 ++ include/Savitar/Scene.h | 7 +++ include/Savitar/TextureData.h | 5 ++ src/Face.cpp | 20 +++++++ src/MeshData.cpp | 37 ++++++++++++ src/Scene.cpp | 109 +++++++++++++++++++++++++++++++++- src/TextureData.cpp | 57 ++++++++++++++++++ src/ThreeMFParser.cpp | 98 +----------------------------- 9 files changed, 247 insertions(+), 97 deletions(-) diff --git a/include/Savitar/Face.h b/include/Savitar/Face.h index a5bc0d0..ca555a6 100644 --- a/include/Savitar/Face.h +++ b/include/Savitar/Face.h @@ -20,9 +20,16 @@ class Face ~Face() = default; [[nodiscard]] int getV1() const; + void setV1(const int v1); + [[nodiscard]] int getV2() const; + void setV2(const int v2); + [[nodiscard]] int getV3() const; + void setV3(const int v3); + [[nodiscard]] const std::optional& getUVCoordinates() const; + void setUVCoordinates(const std::optional& uv_coordinates); private: int vertex_1_index_; diff --git a/include/Savitar/MeshData.h b/include/Savitar/MeshData.h index 209a635..6cc77b1 100644 --- a/include/Savitar/MeshData.h +++ b/include/Savitar/MeshData.h @@ -59,6 +59,8 @@ class MeshData [[nodiscard]] bytearray getUVCoordinatesPerVertexAsBytes(const Scene* scene) const; + void setUVCoordinatesPerVertexAsBytes(const bytearray& data, Scene* scene); + [[nodiscard]] std::string getTexturePath(const Scene* scene) const; /** @@ -79,6 +81,8 @@ class MeshData [[nodiscard]] int getUVGroupId() const; + void setUVGroupId(const int uv_group_id); + /** * Reset the data of the MeshData object. */ diff --git a/include/Savitar/Scene.h b/include/Savitar/Scene.h index 38ccd1b..9fc774d 100644 --- a/include/Savitar/Scene.h +++ b/include/Savitar/Scene.h @@ -43,6 +43,11 @@ class Scene */ void fillByXMLNode(pugi::xml_node xml_node); + /** + * Serialise the scene to model_node + */ + void toXmlNode(pugi::xml_node& model_node); + /** * Store a metadata entry as metadata. * @param key The key of the metadata. @@ -81,6 +86,8 @@ class Scene [[nodiscard]] const TextureData::UVCoordinatesGroup* getUVCoordinatesGroup(const int uv_group_id) const; + [[nodiscard]] int setUVCoordinatesGroupFromBytes(const bytearray& data); + private: std::vector scene_nodes_; std::map metadata_; diff --git a/include/Savitar/TextureData.h b/include/Savitar/TextureData.h index 31d42de..8acfe55 100644 --- a/include/Savitar/TextureData.h +++ b/include/Savitar/TextureData.h @@ -4,6 +4,7 @@ #ifndef TEXTUREDATA_H #define TEXTUREDATA_H +#include "Types.h" #include "UVCoordinate.h" #include @@ -27,10 +28,14 @@ class TextureData void fillByXMLNode(pugi::xml_node xml_node); + void toXmlNode(pugi::xml_node& resources_node); + [[nodiscard]] std::string getTexturePath(const int texture_id) const; [[nodiscard]] const UVCoordinatesGroup* getUVCoordinatesGroup(const int id) const; + [[nodiscard]] int setUVCoordinatesGroupFromBytes(const bytearray& data); + [[nodiscard]] std::string getTexturePathFromGroupId(const int uv_group_id) const; private: diff --git a/src/Face.cpp b/src/Face.cpp index 65d84e3..253fb42 100644 --- a/src/Face.cpp +++ b/src/Face.cpp @@ -18,17 +18,37 @@ int Face::getV1() const return vertex_1_index_; } +void Face::setV1(const int v1) +{ + vertex_1_index_ = v1; +} + int Face::getV2() const { return vertex_2_index_; } +void Face::setV2(const int v2) +{ + vertex_2_index_ = v2; +} + int Face::getV3() const { return vertex_3_index_; } +void Face::setV3(const int v3) +{ + vertex_3_index_ = v3; +} + const std::optional& Face::getUVCoordinates() const { return uv_coordinates_; } + +void Face::setUVCoordinates(const std::optional& uv_coordinates) +{ + uv_coordinates_ = uv_coordinates; +} diff --git a/src/MeshData.cpp b/src/MeshData.cpp index 84cd4e6..089c13c 100644 --- a/src/MeshData.cpp +++ b/src/MeshData.cpp @@ -176,6 +176,25 @@ bytearray MeshData::getUVCoordinatesPerVertexAsBytes(const Scene* scene) const return uv_data; } +void MeshData::setUVCoordinatesPerVertexAsBytes(const bytearray& data, Scene* scene) +{ + uv_group_id_ = scene->setUVCoordinatesGroupFromBytes(data); + + // Although 3MF format is capable of handling various UV coordinates set for a single vertex used by different triangles, Cura + // always uses the same coordinate per vertex, so just make the indices match the vertices + for (Face& face : faces_) + { + if (uv_group_id_ >= 0) + { + face.setUVCoordinates(UVCoordinatesIndices(uv_group_id_, face.getV1(), face.getV2(), face.getV3())); + } + else + { + face.setUVCoordinates(std::nullopt); + } + } +} + std::string MeshData::getTexturePath(const Scene* scene) const { return scene->getTexturePathFromGroupId(uv_group_id_); @@ -209,12 +228,25 @@ void MeshData::toXmlNode(pugi::xml_node& node) } pugi::xml_node triangles_node = node.append_child("triangles"); + size_t face_index = 0; for (auto& face : faces_) { pugi::xml_node triangle_node = triangles_node.append_child("triangle"); triangle_node.append_attribute("v1") = face.getV1(); triangle_node.append_attribute("v2") = face.getV2(); triangle_node.append_attribute("v3") = face.getV3(); + + const std::optional& uv_coordinates_opt = face.getUVCoordinates(); + if (uv_group_id_ >= 0 && uv_coordinates_opt.has_value()) + { + const UVCoordinatesIndices& uv_coordinates = uv_coordinates_opt.value(); + triangle_node.append_attribute("pid") = uv_group_id_; + triangle_node.append_attribute("p1") = uv_coordinates.getV1(); + triangle_node.append_attribute("p2") = uv_coordinates.getV2(); + triangle_node.append_attribute("p3") = uv_coordinates.getV3(); + } + + face_index++; } } @@ -262,6 +294,11 @@ int MeshData::getUVGroupId() const return uv_group_id_; } +void MeshData::setUVGroupId(const int uv_group_id) +{ + uv_group_id_ = uv_group_id; +} + template void MeshData::exportToByteArray(bytearray& data, const T value) { diff --git a/src/Scene.cpp b/src/Scene.cpp index 1c5e697..058b0c5 100644 --- a/src/Scene.cpp +++ b/src/Scene.cpp @@ -2,8 +2,9 @@ // libSavitar is released under the terms of the LGPLv3 or higher. #include "Savitar/Scene.h" +#include "Savitar/Namespace.h" + #include -#include #include using namespace Savitar; @@ -85,6 +86,107 @@ void Scene::fillByXMLNode(pugi::xml_node xml_node) } } +void Scene::toXmlNode(pugi::xml_node& model_node) +{ + pugi::xml_node resources_node = model_node.append_child("resources"); + pugi::xml_node build_node = model_node.append_child("build"); + + model_node.append_attribute("unit") = getUnit().c_str(); + model_node.append_attribute("xmlns") = xml_namespace::getDefaultUri().c_str(); + model_node.append_attribute("xmlns:cura") = xml_namespace::getCuraUri().c_str(); + model_node.append_attribute("xml:lang") = "en-US"; + + texture_data_.toXmlNode(resources_node); + + for (int i = 0; i < getAllSceneNodes().size(); i++) + { + SceneNode* scene_node = getAllSceneNodes().at(i); + scene_node->setId(std::to_string(i + 1)); + } + + for (SceneNode* scene_node : getAllSceneNodes()) + { + // Create item + pugi::xml_node object = resources_node.append_child("object"); + object.append_attribute("id") = scene_node->getId().c_str(); + if (! scene_node->getName().empty()) + { + object.append_attribute("name") = scene_node->getName().c_str(); + } + object.append_attribute("type") = scene_node->getType().c_str(); + + const std::map& per_object_settings = scene_node->getSettings(); + if (! per_object_settings.empty()) + { + pugi::xml_node settings = object.append_child("metadatagroup"); + for (const auto& setting_pair : per_object_settings) + { + pugi::xml_node setting = settings.append_child("metadata"); + setting.append_attribute("name") = setting_pair.first.c_str(); + setting.text().set(setting_pair.second.value.c_str()); + if (setting_pair.second.type != "xs:string") // xs:string is the default type and doesn't need to be written. + { + setting.append_attribute("type") = setting_pair.second.type.c_str(); + } + if (setting_pair.second.preserve) + { + setting.append_attribute("preserve") = "true"; + } + } + } + + if (! scene_node->getMeshData().getVertices().empty()) + { + pugi::xml_node mesh = object.append_child("mesh"); + scene_node->getMeshData().toXmlNode(mesh); + } + + if (! scene_node->getChildren().empty()) + { + pugi::xml_node components = object.append_child("components"); + for (SceneNode* child_scene_node : scene_node->getChildren()) + { + pugi::xml_node component = components.append_child("component"); + component.append_attribute("objectid") = child_scene_node->getId().c_str(); + component.append_attribute("transform") = child_scene_node->getTransformation().c_str(); + } + } + if (scene_node->getMeshNode() != nullptr) + { + if (! object.child("metadatagroup")) + { + object.append_child("metadatagroup"); + } + pugi::xml_node mesh_node_setting = object.child("metadatagroup").append_child("metadata"); + mesh_node_setting.append_attribute("name") = "mesh_node_objectid"; + mesh_node_setting.text().set(scene_node->getMeshNode()->getId().c_str()); + mesh_node_setting.append_attribute("preserve") = "true"; + } + } + + for (SceneNode* scene_node : getSceneNodes()) + { + pugi::xml_node item = build_node.append_child("item"); + item.append_attribute("objectid") = scene_node->getId().c_str(); + item.append_attribute("transform") = scene_node->getTransformation().c_str(); + } + + for (const auto& metadata_pair : getMetadata()) + { + pugi::xml_node metadata_node = model_node.append_child("metadata"); + metadata_node.append_attribute("name") = metadata_pair.first.c_str(); + metadata_node.text().set(metadata_pair.second.value.c_str()); + if (metadata_pair.second.type != "xs:string") // xs:string is the default and doesn't need to get written then. + { + metadata_node.append_attribute("type") = metadata_pair.second.type.c_str(); + } + if (metadata_pair.second.preserve) + { + metadata_node.append_attribute("preserve") = "true"; + } + } +} + SceneNode* Scene::createSceneNodeFromObject(pugi::xml_node root_node, pugi::xml_node object_node) { pugi::xml_node components = object_node.child("components"); @@ -191,3 +293,8 @@ const TextureData::UVCoordinatesGroup* Scene::getUVCoordinatesGroup(const int uv { return texture_data_.getUVCoordinatesGroup(uv_group_id); } + +int Scene::setUVCoordinatesGroupFromBytes(const bytearray& data) +{ + return texture_data_.setUVCoordinatesGroupFromBytes(data); +} diff --git a/src/TextureData.cpp b/src/TextureData.cpp index 185f075..88479e6 100644 --- a/src/TextureData.cpp +++ b/src/TextureData.cpp @@ -45,12 +45,69 @@ void TextureData::fillByXMLNode(pugi::xml_node xml_node) } } +void TextureData::toXmlNode(pugi::xml_node& resources_node) +{ + // Handle textures paths + for (const auto& texture_path : textures_paths_) + { + pugi::xml_node texture_node = resources_node.append_child("m:texture2d"); + texture_node.append_attribute("id") = texture_path.first; + texture_node.append_attribute("path") = texture_path.second.c_str(); + texture_node.append_attribute("contenttype") = "image/png"; + } + + // Handle UV coordinates groups + for (const auto& uv_coordinates_group : uv_coordinates_) + { + pugi::xml_node group_node = resources_node.append_child("m:texture2dgroup"); + group_node.append_attribute("id") = uv_coordinates_group.first; + group_node.append_attribute("texid") = uv_coordinates_group.second.texture_id; + + for (const UVCoordinate& coordinate : uv_coordinates_group.second.coordinates) + { + pugi::xml_node coordinate_node = group_node.append_child("m:tex2coord"); + coordinate_node.append_attribute("u") = coordinate.getU(); + coordinate_node.append_attribute("v") = coordinate.getV(); + } + } +} + const TextureData::UVCoordinatesGroup* TextureData::getUVCoordinatesGroup(const int id) const { auto iterator = uv_coordinates_.find(id); return iterator != uv_coordinates_.end() ? &iterator->second : nullptr; } +int TextureData::setUVCoordinatesGroupFromBytes(const bytearray& data) +{ + // Find first unused id for group + int id = 0; + while (uv_coordinates_.find(id) != uv_coordinates_.end()) + { + ++id; + } + + // Interpret byte array as array of floats. + const float* float_array = reinterpret_cast(data.data()); + const size_t num_bytes = data.size(); + const size_t num_coordinates = (num_bytes / sizeof(float)) / 2; + + UVCoordinatesGroup group; + for (size_t i = 0; i < num_coordinates; ++i) + { + group.coordinates.emplace_back(float_array[i * 2], float_array[i * 2 + 1]); + } + + if (group.coordinates.empty()) + { + return -1; + } + + uv_coordinates_.emplace(id, std::move(group)); + + return id; +} + std::string TextureData::getTexturePath(const int texture_id) const { auto iterator = textures_paths_.find(texture_id); diff --git a/src/ThreeMFParser.cpp b/src/ThreeMFParser.cpp index 7dcb591..06b0be4 100644 --- a/src/ThreeMFParser.cpp +++ b/src/ThreeMFParser.cpp @@ -2,10 +2,9 @@ // libSavitar is released under the terms of the LGPLv3 or higher. #include "Savitar/ThreeMFParser.h" -#include "Savitar/Namespace.h" #include "Savitar/Scene.h" + #include -#include #include using namespace Savitar; @@ -29,101 +28,8 @@ std::string ThreeMFParser::sceneToString(Scene scene) { pugi::xml_document document; pugi::xml_node model_node = document.append_child("model"); - pugi::xml_node resources_node = model_node.append_child("resources"); - pugi::xml_node build_node = model_node.append_child("build"); - - model_node.append_attribute("unit") = scene.getUnit().c_str(); - model_node.append_attribute("xmlns") = xml_namespace::getDefaultUri().c_str(); - model_node.append_attribute("xmlns:cura") = xml_namespace::getCuraUri().c_str(); - model_node.append_attribute("xml:lang") = "en-US"; - - for (int i = 0; i < scene.getAllSceneNodes().size(); i++) - { - SceneNode* scene_node = scene.getAllSceneNodes().at(i); - scene_node->setId(std::to_string(i + 1)); - } - - for (SceneNode* scene_node : scene.getAllSceneNodes()) - { - // Create item - pugi::xml_node object = resources_node.append_child("object"); - object.append_attribute("id") = scene_node->getId().c_str(); - if (! scene_node->getName().empty()) - { - object.append_attribute("name") = scene_node->getName().c_str(); - } - object.append_attribute("type") = scene_node->getType().c_str(); - - const std::map& per_object_settings = scene_node->getSettings(); - if (! per_object_settings.empty()) - { - pugi::xml_node settings = object.append_child("metadatagroup"); - for (const auto& setting_pair : per_object_settings) - { - pugi::xml_node setting = settings.append_child("metadata"); - setting.append_attribute("name") = setting_pair.first.c_str(); - setting.text().set(setting_pair.second.value.c_str()); - if (setting_pair.second.type != "xs:string") // xs:string is the default type and doesn't need to be written. - { - setting.append_attribute("type") = setting_pair.second.type.c_str(); - } - if (setting_pair.second.preserve) - { - setting.append_attribute("preserve") = "true"; - } - } - } - - if (! scene_node->getMeshData().getVertices().empty()) - { - pugi::xml_node mesh = object.append_child("mesh"); - scene_node->getMeshData().toXmlNode(mesh); - } - - if (! scene_node->getChildren().empty()) - { - pugi::xml_node components = object.append_child("components"); - for (SceneNode* child_scene_node : scene_node->getChildren()) - { - pugi::xml_node component = components.append_child("component"); - component.append_attribute("objectid") = child_scene_node->getId().c_str(); - component.append_attribute("transform") = child_scene_node->getTransformation().c_str(); - } - } - if (scene_node->getMeshNode() != nullptr) - { - if (! object.child("metadatagroup")) - { - object.append_child("metadatagroup"); - } - pugi::xml_node mesh_node_setting = object.child("metadatagroup").append_child("metadata"); - mesh_node_setting.append_attribute("name") = "mesh_node_objectid"; - mesh_node_setting.text().set(scene_node->getMeshNode()->getId().c_str()); - mesh_node_setting.append_attribute("preserve") = "true"; - } - } - - for (SceneNode* scene_node : scene.getSceneNodes()) - { - pugi::xml_node item = build_node.append_child("item"); - item.append_attribute("objectid") = scene_node->getId().c_str(); - item.append_attribute("transform") = scene_node->getTransformation().c_str(); - } - for (const auto& metadata_pair : scene.getMetadata()) - { - pugi::xml_node metadata_node = model_node.append_child("metadata"); - metadata_node.append_attribute("name") = metadata_pair.first.c_str(); - metadata_node.text().set(metadata_pair.second.value.c_str()); - if (metadata_pair.second.type != "xs:string") // xs:string is the default and doesn't need to get written then. - { - metadata_node.append_attribute("type") = metadata_pair.second.type.c_str(); - } - if (metadata_pair.second.preserve) - { - metadata_node.append_attribute("preserve") = "true"; - } - } + scene.toXmlNode(model_node); std::stringstream ss; document.save(ss); From 25c883ed8154730bbb421251987c119a85c25682 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 3 Jun 2025 13:25:15 +0200 Subject: [PATCH 3/7] Allow exporting UV coordinates and texture information CURA-12544 --- CMakeLists.txt | 28 +++++++++++++++++----------- include/Savitar/MeshData.h | 2 +- include/Savitar/Scene.h | 4 +++- include/Savitar/TextureData.h | 4 +++- src/MeshData.cpp | 5 +++-- src/Scene.cpp | 9 +++++++-- src/TextureData.cpp | 16 +++++++++++++++- 7 files changed, 49 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c531247..045be56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,16 +13,19 @@ set(savitar_SRCS src/MeshData.cpp src/Vertex.cpp src/Face.cpp - ) + src/UVCoordinate.cpp + src/UVCoordinatesIndices.cpp + src/TextureData.cpp +) -if(BUILD_SHARED_LIBS) +if (BUILD_SHARED_LIBS) add_library(Savitar SHARED ${savitar_SRCS}) - if(WIN32) + if (WIN32) set_target_properties(Savitar PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) - endif() -else() + endif () +else () add_library(Savitar STATIC ${savitar_SRCS}) -endif() +endif () set_project_warnings(Savitar) @@ -30,14 +33,17 @@ target_link_libraries(Savitar PUBLIC pugixml::pugixml) target_include_directories(Savitar PUBLIC - $ - $ + $ + $ PRIVATE - $ - ) + $ +) + +#add_executable(load3mf load3mf.cpp) +#target_link_libraries(load3mf Savitar) option(ENABLE_TESTING "Enable unit-testing" OFF) if (ENABLE_TESTING) enable_testing() add_subdirectory(tests) -endif() +endif () diff --git a/include/Savitar/MeshData.h b/include/Savitar/MeshData.h index 6cc77b1..7f6ea9c 100644 --- a/include/Savitar/MeshData.h +++ b/include/Savitar/MeshData.h @@ -59,7 +59,7 @@ class MeshData [[nodiscard]] bytearray getUVCoordinatesPerVertexAsBytes(const Scene* scene) const; - void setUVCoordinatesPerVertexAsBytes(const bytearray& data, Scene* scene); + void setUVCoordinatesPerVertexAsBytes(const bytearray& data, const std::string& texture_path, Scene* scene); [[nodiscard]] std::string getTexturePath(const Scene* scene) const; diff --git a/include/Savitar/Scene.h b/include/Savitar/Scene.h index 9fc774d..4bbfcfd 100644 --- a/include/Savitar/Scene.h +++ b/include/Savitar/Scene.h @@ -86,7 +86,9 @@ class Scene [[nodiscard]] const TextureData::UVCoordinatesGroup* getUVCoordinatesGroup(const int uv_group_id) const; - [[nodiscard]] int setUVCoordinatesGroupFromBytes(const bytearray& data); + [[nodiscard]] int addTexturePath(const std::string& texture_path); + + [[nodiscard]] int setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id); private: std::vector scene_nodes_; diff --git a/include/Savitar/TextureData.h b/include/Savitar/TextureData.h index 8acfe55..be97847 100644 --- a/include/Savitar/TextureData.h +++ b/include/Savitar/TextureData.h @@ -34,7 +34,9 @@ class TextureData [[nodiscard]] const UVCoordinatesGroup* getUVCoordinatesGroup(const int id) const; - [[nodiscard]] int setUVCoordinatesGroupFromBytes(const bytearray& data); + [[nodiscard]] int setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id); + + [[nodiscard]] int addTexturePath(const std::string& texture_path); [[nodiscard]] std::string getTexturePathFromGroupId(const int uv_group_id) const; diff --git a/src/MeshData.cpp b/src/MeshData.cpp index 089c13c..d2a87b0 100644 --- a/src/MeshData.cpp +++ b/src/MeshData.cpp @@ -176,9 +176,10 @@ bytearray MeshData::getUVCoordinatesPerVertexAsBytes(const Scene* scene) const return uv_data; } -void MeshData::setUVCoordinatesPerVertexAsBytes(const bytearray& data, Scene* scene) +void MeshData::setUVCoordinatesPerVertexAsBytes(const bytearray& data, const std::string& texture_path, Scene* scene) { - uv_group_id_ = scene->setUVCoordinatesGroupFromBytes(data); + const int texture_id = scene->addTexturePath(texture_path); + uv_group_id_ = scene->setUVCoordinatesGroupFromBytes(data, texture_id); // Although 3MF format is capable of handling various UV coordinates set for a single vertex used by different triangles, Cura // always uses the same coordinate per vertex, so just make the indices match the vertices diff --git a/src/Scene.cpp b/src/Scene.cpp index 058b0c5..92e703e 100644 --- a/src/Scene.cpp +++ b/src/Scene.cpp @@ -294,7 +294,12 @@ const TextureData::UVCoordinatesGroup* Scene::getUVCoordinatesGroup(const int uv return texture_data_.getUVCoordinatesGroup(uv_group_id); } -int Scene::setUVCoordinatesGroupFromBytes(const bytearray& data) +int Scene::addTexturePath(const std::string& texture_path) { - return texture_data_.setUVCoordinatesGroupFromBytes(data); + return texture_data_.addTexturePath(texture_path); +} + +int Scene::setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id) +{ + return texture_data_.setUVCoordinatesGroupFromBytes(data, texture_id); } diff --git a/src/TextureData.cpp b/src/TextureData.cpp index 88479e6..0461744 100644 --- a/src/TextureData.cpp +++ b/src/TextureData.cpp @@ -78,7 +78,7 @@ const TextureData::UVCoordinatesGroup* TextureData::getUVCoordinatesGroup(const return iterator != uv_coordinates_.end() ? &iterator->second : nullptr; } -int TextureData::setUVCoordinatesGroupFromBytes(const bytearray& data) +int TextureData::setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id) { // Find first unused id for group int id = 0; @@ -93,6 +93,7 @@ int TextureData::setUVCoordinatesGroupFromBytes(const bytearray& data) const size_t num_coordinates = (num_bytes / sizeof(float)) / 2; UVCoordinatesGroup group; + group.texture_id = texture_id; for (size_t i = 0; i < num_coordinates; ++i) { group.coordinates.emplace_back(float_array[i * 2], float_array[i * 2 + 1]); @@ -108,6 +109,19 @@ int TextureData::setUVCoordinatesGroupFromBytes(const bytearray& data) return id; } +int TextureData::addTexturePath(const std::string& texture_path) +{ + // Find first unused id for textures + int id = 0; + while (textures_paths_.find(id) != textures_paths_.end()) + { + ++id; + } + + textures_paths_.emplace(id, texture_path); + return id; +} + std::string TextureData::getTexturePath(const int texture_id) const { auto iterator = textures_paths_.find(texture_id); From e5fb29d8d799458b791f2a615d73f1d69b58f36c Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 3 Jun 2025 14:56:41 +0200 Subject: [PATCH 4/7] Save all resources with different IDs CURA-12544 --- include/Savitar/Scene.h | 8 ++++--- include/Savitar/SceneNode.h | 6 ++--- include/Savitar/TextureData.h | 6 +++-- src/MeshData.cpp | 7 ++++-- src/Scene.cpp | 43 ++++++++++++++++++++++++----------- src/SceneNode.cpp | 8 +++---- src/TextureData.cpp | 38 ++++++++++++------------------- tests/ThreeMFParserTest.cpp | 2 +- 8 files changed, 67 insertions(+), 51 deletions(-) diff --git a/include/Savitar/Scene.h b/include/Savitar/Scene.h index 4bbfcfd..ce6d4a8 100644 --- a/include/Savitar/Scene.h +++ b/include/Savitar/Scene.h @@ -30,7 +30,7 @@ class Scene */ [[nodiscard]] std::vector getSceneNodes(); - [[nodiscard]] std::vector getAllSceneNodes(); + [[nodiscard]] std::vector getAllSceneNodes() const; /** * Add a scene node to the scene. @@ -74,6 +74,8 @@ class Scene */ [[nodiscard]] const std::map& getMetadata() const; + int getNextAvailableResourceId() const; + /** * Get the unit (milimeter, inch, etc) of the scene. * This is in milimeter by default. @@ -86,9 +88,9 @@ class Scene [[nodiscard]] const TextureData::UVCoordinatesGroup* getUVCoordinatesGroup(const int uv_group_id) const; - [[nodiscard]] int addTexturePath(const std::string& texture_path); + void addTexturePath(const std::string& texture_path, const int texture_id); - [[nodiscard]] int setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id); + void setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id, const int group_id); private: std::vector scene_nodes_; diff --git a/include/Savitar/SceneNode.h b/include/Savitar/SceneNode.h index 15b0e47..6821683 100644 --- a/include/Savitar/SceneNode.h +++ b/include/Savitar/SceneNode.h @@ -44,9 +44,9 @@ class SceneNode /** * Get the (unique) identifier of the node. */ - [[nodiscard]] std::string getId(); + [[nodiscard]] int getId() const; - void setId(std::string id); + void setId(int id); /** * Get the (non-unique) display name of the node. @@ -81,7 +81,7 @@ class SceneNode std::vector children_; MeshData mesh_data_; std::map settings_; - std::string id_; + int id_{ -1 }; std::string name_; std::string type_{ "model" }; std::string component_path_; diff --git a/include/Savitar/TextureData.h b/include/Savitar/TextureData.h index be97847..b97d089 100644 --- a/include/Savitar/TextureData.h +++ b/include/Savitar/TextureData.h @@ -34,12 +34,14 @@ class TextureData [[nodiscard]] const UVCoordinatesGroup* getUVCoordinatesGroup(const int id) const; - [[nodiscard]] int setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id); + void setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id, const int group_id); - [[nodiscard]] int addTexturePath(const std::string& texture_path); + void addTexturePath(const std::string& texture_path, const int id); [[nodiscard]] std::string getTexturePathFromGroupId(const int uv_group_id) const; + [[nodiscard]] int getNextAvailableResourceId() const; + private: std::map textures_paths_; std::map uv_coordinates_; diff --git a/src/MeshData.cpp b/src/MeshData.cpp index d2a87b0..c001b85 100644 --- a/src/MeshData.cpp +++ b/src/MeshData.cpp @@ -178,8 +178,11 @@ bytearray MeshData::getUVCoordinatesPerVertexAsBytes(const Scene* scene) const void MeshData::setUVCoordinatesPerVertexAsBytes(const bytearray& data, const std::string& texture_path, Scene* scene) { - const int texture_id = scene->addTexturePath(texture_path); - uv_group_id_ = scene->setUVCoordinatesGroupFromBytes(data, texture_id); + const int texture_id = scene->getNextAvailableResourceId(); + scene->addTexturePath(texture_path, texture_id); + + uv_group_id_ = scene->getNextAvailableResourceId(); + scene->setUVCoordinatesGroupFromBytes(data, texture_id, uv_group_id_); // Although 3MF format is capable of handling various UV coordinates set for a single vertex used by different triangles, Cura // always uses the same coordinate per vertex, so just make the indices match the vertices diff --git a/src/Scene.cpp b/src/Scene.cpp index 92e703e..c594e36 100644 --- a/src/Scene.cpp +++ b/src/Scene.cpp @@ -4,6 +4,7 @@ #include "Savitar/Scene.h" #include "Savitar/Namespace.h" +#include #include #include @@ -98,17 +99,17 @@ void Scene::toXmlNode(pugi::xml_node& model_node) texture_data_.toXmlNode(resources_node); - for (int i = 0; i < getAllSceneNodes().size(); i++) + const std::vector all_nodes = getAllSceneNodes(); + for (SceneNode* scene_node : all_nodes) { - SceneNode* scene_node = getAllSceneNodes().at(i); - scene_node->setId(std::to_string(i + 1)); + scene_node->setId(getNextAvailableResourceId()); } - for (SceneNode* scene_node : getAllSceneNodes()) + for (SceneNode* scene_node : all_nodes) { // Create item pugi::xml_node object = resources_node.append_child("object"); - object.append_attribute("id") = scene_node->getId().c_str(); + object.append_attribute("id") = std::to_string(scene_node->getId()).c_str(); if (! scene_node->getName().empty()) { object.append_attribute("name") = scene_node->getName().c_str(); @@ -147,7 +148,7 @@ void Scene::toXmlNode(pugi::xml_node& model_node) for (SceneNode* child_scene_node : scene_node->getChildren()) { pugi::xml_node component = components.append_child("component"); - component.append_attribute("objectid") = child_scene_node->getId().c_str(); + component.append_attribute("objectid") = std::to_string(child_scene_node->getId()).c_str(); component.append_attribute("transform") = child_scene_node->getTransformation().c_str(); } } @@ -159,7 +160,7 @@ void Scene::toXmlNode(pugi::xml_node& model_node) } pugi::xml_node mesh_node_setting = object.child("metadatagroup").append_child("metadata"); mesh_node_setting.append_attribute("name") = "mesh_node_objectid"; - mesh_node_setting.text().set(scene_node->getMeshNode()->getId().c_str()); + mesh_node_setting.text().set(std::to_string(scene_node->getMeshNode()->getId()).c_str()); mesh_node_setting.append_attribute("preserve") = "true"; } } @@ -167,7 +168,7 @@ void Scene::toXmlNode(pugi::xml_node& model_node) for (SceneNode* scene_node : getSceneNodes()) { pugi::xml_node item = build_node.append_child("item"); - item.append_attribute("objectid") = scene_node->getId().c_str(); + item.append_attribute("objectid") = std::to_string(scene_node->getId()).c_str(); item.append_attribute("transform") = scene_node->getTransformation().c_str(); } @@ -243,7 +244,23 @@ SceneNode* Scene::createSceneNodeFromObject(pugi::xml_node root_node, pugi::xml_ return scene_node; } -std::vector Scene::getAllSceneNodes() +int Scene::getNextAvailableResourceId() const +{ + int id = 0; + + std::vector all_nodes = getAllSceneNodes(); + const auto iterator_max = std::max_element(all_nodes.begin(), all_nodes.end(), [](const SceneNode* lhs, const SceneNode* rhs) { return lhs->getId() > rhs->getId(); }); + if (iterator_max != all_nodes.end()) + { + id = (*iterator_max)->getId() + 1; + } + + id = std::max(id, texture_data_.getNextAvailableResourceId()); + + return id; +} + +std::vector Scene::getAllSceneNodes() const { std::vector all_nodes; @@ -294,12 +311,12 @@ const TextureData::UVCoordinatesGroup* Scene::getUVCoordinatesGroup(const int uv return texture_data_.getUVCoordinatesGroup(uv_group_id); } -int Scene::addTexturePath(const std::string& texture_path) +void Scene::addTexturePath(const std::string& texture_path, const int texture_id) { - return texture_data_.addTexturePath(texture_path); + texture_data_.addTexturePath(texture_path, texture_id); } -int Scene::setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id) +void Scene::setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id, const int group_id) { - return texture_data_.setUVCoordinatesGroupFromBytes(data, texture_id); + texture_data_.setUVCoordinatesGroupFromBytes(data, texture_id, group_id); } diff --git a/src/SceneNode.cpp b/src/SceneNode.cpp index a4c04ce..ba86548 100644 --- a/src/SceneNode.cpp +++ b/src/SceneNode.cpp @@ -78,7 +78,7 @@ void SceneNode::setType(const std::string& type) void SceneNode::fillByXMLNode(pugi::xml_node xml_node) { settings_.clear(); - id_ = xml_node.attribute("id").as_string(); + id_ = xml_node.attribute("id").as_int(); name_ = xml_node.attribute("name").as_string(); if (xml_node.child("mesh") != nullptr) @@ -158,7 +158,7 @@ void SceneNode::parseComponentData(const std::string& xml_string) document.load_string(xml_string.c_str()); pugi::xml_node xml_node = document; - for (const std::string child_name : {"model", "resources", "object", "mesh"}) + for (const std::string child_name : { "model", "resources", "object", "mesh" }) { xml_node = xml_node.child(child_name.c_str()); if (xml_node == nullptr) @@ -171,12 +171,12 @@ void SceneNode::parseComponentData(const std::string& xml_string) mesh_data_.fillByXMLNode(xml_node); } -std::string SceneNode::getId() +int SceneNode::getId() const { return id_; } -void SceneNode::setId(std::string id) +void SceneNode::setId(const int id) { id_ = id; } diff --git a/src/TextureData.cpp b/src/TextureData.cpp index 0461744..e4b25bf 100644 --- a/src/TextureData.cpp +++ b/src/TextureData.cpp @@ -78,15 +78,8 @@ const TextureData::UVCoordinatesGroup* TextureData::getUVCoordinatesGroup(const return iterator != uv_coordinates_.end() ? &iterator->second : nullptr; } -int TextureData::setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id) +void TextureData::setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id, const int group_id) { - // Find first unused id for group - int id = 0; - while (uv_coordinates_.find(id) != uv_coordinates_.end()) - { - ++id; - } - // Interpret byte array as array of floats. const float* float_array = reinterpret_cast(data.data()); const size_t num_bytes = data.size(); @@ -99,27 +92,15 @@ int TextureData::setUVCoordinatesGroupFromBytes(const bytearray& data, const int group.coordinates.emplace_back(float_array[i * 2], float_array[i * 2 + 1]); } - if (group.coordinates.empty()) + if (! group.coordinates.empty()) { - return -1; + uv_coordinates_.emplace(group_id, std::move(group)); } - - uv_coordinates_.emplace(id, std::move(group)); - - return id; } -int TextureData::addTexturePath(const std::string& texture_path) +void TextureData::addTexturePath(const std::string& texture_path, const int id) { - // Find first unused id for textures - int id = 0; - while (textures_paths_.find(id) != textures_paths_.end()) - { - ++id; - } - textures_paths_.emplace(id, texture_path); - return id; } std::string TextureData::getTexturePath(const int texture_id) const @@ -138,3 +119,14 @@ std::string TextureData::getTexturePathFromGroupId(const int uv_group_id) const return getTexturePath(group->texture_id); } + +int TextureData::getNextAvailableResourceId() const +{ + int id = 0; + while (textures_paths_.find(id) != textures_paths_.end() || uv_coordinates_.find(id) != uv_coordinates_.end()) + { + ++id; + } + + return id; +} diff --git a/tests/ThreeMFParserTest.cpp b/tests/ThreeMFParserTest.cpp index 8af6bf5..0cdf25d 100644 --- a/tests/ThreeMFParserTest.cpp +++ b/tests/ThreeMFParserTest.cpp @@ -103,7 +103,7 @@ TEST_F(ThreeMFParserTest, parse) EXPECT_EQ(nodes[0]->getName().compare("test_object"), 0); EXPECT_EQ(nodes[1]->getName().compare(""), 0); - EXPECT_EQ(nodes[1]->getId().compare("2"), 0); + EXPECT_EQ(nodes[1]->getId(), 2); EXPECT_FALSE(nodes[1]->getTransformation().empty()); } From 3ca62be0c1735e1d5c5c5b4271eba9afe82999ee Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 3 Jun 2025 15:40:59 +0200 Subject: [PATCH 5/7] Fix wrong retrieved available ID CURA-12544 --- src/Scene.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Scene.cpp b/src/Scene.cpp index c594e36..6a69f3d 100644 --- a/src/Scene.cpp +++ b/src/Scene.cpp @@ -249,7 +249,7 @@ int Scene::getNextAvailableResourceId() const int id = 0; std::vector all_nodes = getAllSceneNodes(); - const auto iterator_max = std::max_element(all_nodes.begin(), all_nodes.end(), [](const SceneNode* lhs, const SceneNode* rhs) { return lhs->getId() > rhs->getId(); }); + const auto iterator_max = std::max_element(all_nodes.begin(), all_nodes.end(), [](const SceneNode* lhs, const SceneNode* rhs) { return lhs->getId() < rhs->getId(); }); if (iterator_max != all_nodes.end()) { id = (*iterator_max)->getId() + 1; From a0322fb965026e251153ab111b91d15499e2e861 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 3 Jun 2025 16:22:59 +0200 Subject: [PATCH 6/7] Clean debug code CURA-12544 --- CMakeLists.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 045be56..8dcd516 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,9 +39,6 @@ target_include_directories(Savitar $ ) -#add_executable(load3mf load3mf.cpp) -#target_link_libraries(load3mf Savitar) - option(ENABLE_TESTING "Enable unit-testing" OFF) if (ENABLE_TESTING) enable_testing() From 8db6981489c29f183b08900b56a05785871102b0 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 3 Jun 2025 16:48:27 +0200 Subject: [PATCH 7/7] Add documentation CURA-12544 --- include/Savitar/Face.h | 2 +- include/Savitar/MeshData.h | 25 +++++++++++++++++++++---- include/Savitar/Scene.h | 12 +++++++++++- include/Savitar/TextureData.h | 17 +++++++++++++++-- include/Savitar/UVCoordinatesIndices.h | 4 ++++ src/MeshData.cpp | 10 ---------- 6 files changed, 52 insertions(+), 18 deletions(-) diff --git a/include/Savitar/Face.h b/include/Savitar/Face.h index ca555a6..34293b8 100644 --- a/include/Savitar/Face.h +++ b/include/Savitar/Face.h @@ -14,7 +14,7 @@ class Face { public: /** - * A face uses the index of 3 vertices to describe a triangle + * A face uses the index of 3 vertices to describe a triangle, and possibly indices of UV coordinates */ Face(int v1, int v2, int v3, const std::optional& uv_coordinates = std::nullopt); ~Face() = default; diff --git a/include/Savitar/MeshData.h b/include/Savitar/MeshData.h index 7f6ea9c..5c3e806 100644 --- a/include/Savitar/MeshData.h +++ b/include/Savitar/MeshData.h @@ -57,10 +57,26 @@ class MeshData */ [[nodiscard]] bytearray getFlatVerticesAsBytes(); + /** + * Retrieves the list of actual UV coordinates for each vertex of each face, as a raw byte array + * @param scene The scene which actually holds the UV coordinates set (can be shared amongst meshes) + * @return The coordinates array, or an empty array if the mesh doesn't have texture data + */ [[nodiscard]] bytearray getUVCoordinatesPerVertexAsBytes(const Scene* scene) const; + /** + * Sets the UV coordinates from a raw byte array, containing the actual coordinates for each vertex of each face + * @param data The raw coordinates data + * @param texture_path The path of the texture file te be stored besides the model description + * @param scene The scene which actually holds the UV coordinates set (can be shared amongst meshes) + */ void setUVCoordinatesPerVertexAsBytes(const bytearray& data, const std::string& texture_path, Scene* scene); + /** + * Get the path of the texture used by this mesh + * @param scene The scene which actually contains the list of stored textures (can be shared amongst models) + * @return The texture path, or an empty string if the mesh doesn't have texture data + */ [[nodiscard]] std::string getTexturePath(const Scene* scene) const; /** @@ -79,16 +95,17 @@ class MeshData [[nodiscard]] std::vector getVertices(); - [[nodiscard]] int getUVGroupId() const; - - void setUVGroupId(const int uv_group_id); - /** * Reset the data of the MeshData object. */ void clear(); private: + /** + * @tparam T The type of data to be exported + * @param data The byte array containing the exported raw data + * @param value The value to be exported as raw data in the byte array + */ template static void exportToByteArray(bytearray& data, const T value); diff --git a/include/Savitar/Scene.h b/include/Savitar/Scene.h index ce6d4a8..651e3aa 100644 --- a/include/Savitar/Scene.h +++ b/include/Savitar/Scene.h @@ -74,7 +74,11 @@ class Scene */ [[nodiscard]] const std::map& getMetadata() const; - int getNextAvailableResourceId() const; + /** + * Find the next available resource ID amongst actually stored texture, UV coordinates and scene nodes. This should be called before + * adding any of these resources, so that IDs are unique in the end. + */ + [[nodiscard]] int getNextAvailableResourceId() const; /** * Get the unit (milimeter, inch, etc) of the scene. @@ -90,6 +94,12 @@ class Scene void addTexturePath(const std::string& texture_path, const int texture_id); + /** + * Stores a UV coordinates group from raw data + * @param data The raw data to be stored + * @param texture_id The ID of the associated texture + * @param group_id The ID of the newly created coordinates group + */ void setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id, const int group_id); private: diff --git a/include/Savitar/TextureData.h b/include/Savitar/TextureData.h index b97d089..9524406 100644 --- a/include/Savitar/TextureData.h +++ b/include/Savitar/TextureData.h @@ -14,13 +14,16 @@ namespace Savitar { +/** + * The TextureData stores UV coordinates groups and textures paths, that will end-up as resources in the global model description + */ class TextureData { public: struct UVCoordinatesGroup { - int texture_id; - std::vector coordinates; + int texture_id; // The ID of the associated texture + std::vector coordinates; // The actual UV coordinates of the group }; TextureData(); @@ -34,12 +37,22 @@ class TextureData [[nodiscard]] const UVCoordinatesGroup* getUVCoordinatesGroup(const int id) const; + /** + * Loads a UV coordinates group from raw data + * @param data The actual UV coordinates as an array of floats + * @param texture_id The ID of the associated texture + * @param group_id The ID of the newly created group + */ void setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id, const int group_id); void addTexturePath(const std::string& texture_path, const int id); [[nodiscard]] std::string getTexturePathFromGroupId(const int uv_group_id) const; + /** + * Find the next available resource ID amongst actually stored texture and UV coordinates. This should be called before + * adding any of these resources, so that IDs are unique in the end. + */ [[nodiscard]] int getNextAvailableResourceId() const; private: diff --git a/include/Savitar/UVCoordinatesIndices.h b/include/Savitar/UVCoordinatesIndices.h index 1decb6c..0e34d79 100644 --- a/include/Savitar/UVCoordinatesIndices.h +++ b/include/Savitar/UVCoordinatesIndices.h @@ -6,6 +6,10 @@ namespace Savitar { +/** + * UV coordinates indices contains the UV group ID and associated indices for each point of a face. A single mesh may contain indices + * from multiple texture. + */ class UVCoordinatesIndices { public: diff --git a/src/MeshData.cpp b/src/MeshData.cpp index c001b85..4206545 100644 --- a/src/MeshData.cpp +++ b/src/MeshData.cpp @@ -293,16 +293,6 @@ std::vector MeshData::getVertices() return vertices_; } -int MeshData::getUVGroupId() const -{ - return uv_group_id_; -} - -void MeshData::setUVGroupId(const int uv_group_id) -{ - uv_group_id_ = uv_group_id; -} - template void MeshData::exportToByteArray(bytearray& data, const T value) {