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/CMakeLists.txt b/CMakeLists.txt index c531247..8dcd516 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,14 @@ target_link_libraries(Savitar PUBLIC pugixml::pugixml) target_include_directories(Savitar PUBLIC - $ - $ + $ + $ PRIVATE - $ - ) + $ +) option(ENABLE_TESTING "Enable unit-testing" OFF) if (ENABLE_TESTING) enable_testing() add_subdirectory(tests) -endif() +endif () diff --git a/include/Savitar/Face.h b/include/Savitar/Face.h index 555b981..34293b8 100644 --- a/include/Savitar/Face.h +++ b/include/Savitar/Face.h @@ -4,25 +4,38 @@ #ifndef FACE_H #define FACE_H +#include "Savitar/UVCoordinatesIndices.h" + +#include + namespace Savitar { 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); + Face(int v1, int v2, int v3, const std::optional& uv_coordinates = std::nullopt); ~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_; 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..5c3e806 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,28 @@ 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; + /** * Set the vertices of the meshdata by bytearray (as set from python) * @@ -77,9 +100,19 @@ class MeshData */ 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); + 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..651e3aa 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 @@ -29,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. @@ -42,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. @@ -68,6 +74,12 @@ class Scene */ [[nodiscard]] const std::map& getMetadata() 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. * This is in milimeter by default. @@ -76,10 +88,25 @@ 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; + + 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: 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/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 new file mode 100644 index 0000000..9524406 --- /dev/null +++ b/include/Savitar/TextureData.h @@ -0,0 +1,64 @@ +// Copyright (c) 2025 Ultimaker B.V. +// libSavitar is released under the terms of the LGPLv3 or higher. + +#ifndef TEXTUREDATA_H +#define TEXTUREDATA_H + +#include "Types.h" +#include "UVCoordinate.h" + +#include +#include +#include +#include + +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; // The ID of the associated texture + std::vector coordinates; // The actual UV coordinates of the group + }; + + TextureData(); + virtual ~TextureData() = default; + + 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; + + /** + * 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: + 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..0e34d79 --- /dev/null +++ b/include/Savitar/UVCoordinatesIndices.h @@ -0,0 +1,32 @@ +// 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 +{ +/** + * 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: + 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..253fb42 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) { } @@ -14,12 +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 48ec377..4206545 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,73 @@ 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; +} + +void MeshData::setUVCoordinatesPerVertexAsBytes(const bytearray& data, const std::string& texture_path, Scene* scene) +{ + 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 + 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_); +} + bytearray MeshData::getFacesAsBytes() { bytearray face_data; @@ -152,12 +232,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++; } } @@ -190,7 +283,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 +292,9 @@ std::vector MeshData::getVertices() { return vertices_; } + +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..6a69f3d 100644 --- a/src/Scene.cpp +++ b/src/Scene.cpp @@ -2,8 +2,10 @@ // libSavitar is released under the terms of the LGPLv3 or higher. #include "Savitar/Scene.h" +#include "Savitar/Namespace.h" + +#include #include -#include #include using namespace Savitar; @@ -27,8 +29,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 +44,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) { @@ -83,6 +87,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); + + const std::vector all_nodes = getAllSceneNodes(); + for (SceneNode* scene_node : all_nodes) + { + scene_node->setId(getNextAvailableResourceId()); + } + + for (SceneNode* scene_node : all_nodes) + { + // Create item + pugi::xml_node object = resources_node.append_child("object"); + 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(); + } + 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") = std::to_string(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(std::to_string(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") = std::to_string(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"); @@ -139,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; @@ -179,3 +300,23 @@ 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); +} + +void Scene::addTexturePath(const std::string& texture_path, const int texture_id) +{ + texture_data_.addTexturePath(texture_path, texture_id); +} + +void Scene::setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id, const int group_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 new file mode 100644 index 0000000..e4b25bf --- /dev/null +++ b/src/TextureData.cpp @@ -0,0 +1,132 @@ +// 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)); + } + } +} + +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; +} + +void TextureData::setUVCoordinatesGroupFromBytes(const bytearray& data, const int texture_id, const int group_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; + 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]); + } + + if (! group.coordinates.empty()) + { + uv_coordinates_.emplace(group_id, std::move(group)); + } +} + +void TextureData::addTexturePath(const std::string& texture_path, const int id) +{ + textures_paths_.emplace(id, texture_path); +} + +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); +} + +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/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); 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_; +} 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()); }