diff --git a/include/utils/MeshUtils.h b/include/utils/MeshUtils.h index 1d81b7acf0..c549c86656 100644 --- a/include/utils/MeshUtils.h +++ b/include/utils/MeshUtils.h @@ -5,6 +5,8 @@ #define UTILS_MESH_UTILS_H #include +#include +#include #include "geometry/Triangle2F.h" #include "geometry/Triangle3D.h" @@ -13,6 +15,7 @@ namespace cura { class Point3D; +class Mesh; namespace MeshUtils { @@ -21,6 +24,37 @@ std::optional getBarycentricCoordinates(const Point3D& point, const Tri Point2F getUVCoordinates(const Point3D& barycentric_coordinates, const Triangle2F& uv_coordinates); +/*! + * Load texture data from PNG binary data and attach it to a mesh. + * This function handles PNG parsing, metadata extraction, and texture attachment. + * + * @param texture_data Binary PNG data as bytes + * @param mesh The mesh to attach the texture to + * @param source_description Description of the texture source for logging (e.g., filename or "network data") + * @param log_no_metadata Whether to log when no metadata is found (file loading) vs silently return (network data) + * @return true if the texture was loaded successfully with valid metadata, false otherwise + */ +bool loadTextureFromPngData(const std::vector& texture_data, Mesh& mesh, const std::string& source_description, bool log_no_metadata = true); + +/*! + * Load texture data from a PNG file and attach it to a mesh. + * Convenience wrapper around loadTextureFromPngData for file-based loading. + * + * @param mesh The mesh to attach the texture to + * @param texture_filename The path to the PNG texture file + * @return true if the texture was loaded successfully, false otherwise + */ +bool loadTextureFromFile(Mesh& mesh, const std::string& texture_filename); + +/*! + * Load texture data from PNG data provided as a string and attach it to a mesh. + * Convenience wrapper around loadTextureFromPngData for string-based loading. + * + * @param texture_str Binary PNG data as string + * @param mesh The mesh to attach the texture to + */ +void loadTextureFromString(const std::string& texture_str, Mesh& mesh); + } // namespace MeshUtils } // namespace cura diff --git a/src/MeshGroup.cpp b/src/MeshGroup.cpp index 91c0f15924..c8d0a97924 100644 --- a/src/MeshGroup.cpp +++ b/src/MeshGroup.cpp @@ -5,6 +5,10 @@ #include #include +#include +#include +#include +#include #include #include #include @@ -16,6 +20,7 @@ #include "settings/types/Ratio.h" //For the shrinkage percentage and scale factor. #include "utils/Matrix4x3D.h" //To transform the input meshes for shrinkage compensation and to align in command line mode. +#include "utils/MeshUtils.h" #include "utils/Point2F.h" #include "utils/Point3F.h" //To accept incoming meshes with floating point vertices. #include "utils/gettime.h" @@ -172,7 +177,7 @@ bool loadMeshSTL_ascii(Mesh* mesh, const char* filename, const Matrix4x3D& matri return true; } -bool loadMeshSTL_binary(Mesh* mesh, const char* filename, const Matrix4x3D& matrix) +bool loadMeshSTL_binary(Mesh* mesh, const char* filename, const Matrix4x3D& matrix, const std::vector* uv_coordinates = nullptr) { FILE* f = fopen(filename, "rb"); @@ -206,6 +211,8 @@ bool loadMeshSTL_binary(Mesh* mesh, const char* filename, const Matrix4x3D& matr // Every Face is 50 Bytes: Normal(3*float), Vertices(9*float), 2 Bytes Spacer mesh->faces_.reserve(face_count); mesh->vertices_.reserve(face_count); + + size_t vertex_index = 0; for (size_t i = 0; i < face_count; i++) { if (fread(buffer, 50, 1, f) != 1) @@ -218,7 +225,20 @@ bool loadMeshSTL_binary(Mesh* mesh, const char* filename, const Matrix4x3D& matr Point3LL v0 = matrix.apply(Point3F(v[0], v[1], v[2]).toPoint3d()); Point3LL v1 = matrix.apply(Point3F(v[3], v[4], v[5]).toPoint3d()); Point3LL v2 = matrix.apply(Point3F(v[6], v[7], v[8]).toPoint3d()); - mesh->addFace(v0, v1, v2); + + // Handle UV coordinates if provided + if (uv_coordinates && vertex_index + 2 < uv_coordinates->size()) + { + std::optional uv0 = (*uv_coordinates)[vertex_index]; + std::optional uv1 = (*uv_coordinates)[vertex_index + 1]; + std::optional uv2 = (*uv_coordinates)[vertex_index + 2]; + vertex_index += 3; + mesh->addFace(v0, v1, v2, uv0, uv1, uv2); + } + else + { + mesh->addFace(v0, v1, v2); + } } fclose(f); mesh->finish(); @@ -390,6 +410,100 @@ bool loadMeshOBJ(Mesh* mesh, const std::string& filename, const Matrix4x3D& matr return ! mesh->faces_.empty(); } + +/*! + * Load UV coordinates from a binary file and store them for later application to mesh faces. + * + * @param uv_filename The path to the binary UV file + * @param uv_coordinates Vector to store the loaded UV coordinates + * @return true if UV coordinates were loaded successfully, false otherwise + */ +bool loadUVCoordinatesFromFile(const std::string& uv_filename, std::vector& uv_coordinates) +{ + if (! std::filesystem::exists(uv_filename)) + { + return false; // File doesn't exist, not an error + } + + std::ifstream file(uv_filename, std::ios::binary); + if (! file.is_open()) + { + spdlog::warn("Failed to open UV file: {}", uv_filename); + return false; + } + + // Read vertex count (uint32) + uint32_t vertex_count; + if (! file.read(reinterpret_cast(&vertex_count), sizeof(uint32_t))) + { + spdlog::warn("Failed to read vertex count from UV file: {}", uv_filename); + return false; + } + + + // Read UV coordinates (2 floats per vertex) + uv_coordinates.resize(vertex_count); + const std::streamsize uv_data_size = vertex_count * 2 * sizeof(float); + + if (! file.read(reinterpret_cast(uv_coordinates.data()), uv_data_size)) + { + spdlog::warn("Failed to read UV coordinates from file: {}", uv_filename); + return false; + } + + spdlog::info("Loaded {} UV coordinates from: {}", vertex_count, uv_filename); + return true; +} + + +bool loadMeshSTL_with_uv(Mesh* mesh, const char* filename, const Matrix4x3D& matrix, const std::vector& uv_coordinates) +{ + FILE* f = fopen(filename, "rb"); + if (f == nullptr) + { + return false; + } + + // assign filename to mesh_name + mesh->mesh_name_ = filename; + + // Skip any whitespace at the beginning of the file. + unsigned long long num_whitespace = 0; // Number of whitespace characters. + unsigned char whitespace; + if (fread(&whitespace, 1, 1, f) != 1) + { + fclose(f); + return false; + } + while (isspace(whitespace)) + { + num_whitespace++; + if (fread(&whitespace, 1, 1, f) != 1) + { + fclose(f); + return false; + } + } + fseek(f, num_whitespace, SEEK_SET); // Seek to the place after all whitespace (we may have just read too far). + + char buffer[6]; + if (fread(buffer, 5, 1, f) != 1) + { + fclose(f); + return false; + } + fclose(f); + + buffer[5] = '\0'; + if (stringcasecompare(buffer, "solid") == 0) + { + // ASCII STL with UV coordinates not currently supported + spdlog::warn("ASCII STL with UV coordinates not supported, use binary STL: {}", filename); + return false; + } + return loadMeshSTL_binary(mesh, filename, matrix, &uv_coordinates); +} + bool loadMeshIntoMeshGroup(MeshGroup* meshgroup, const char* filename, const Matrix4x3D& transformation, Settings& object_parent_settings) { TimeKeeper load_timer; @@ -398,8 +512,41 @@ bool loadMeshIntoMeshGroup(MeshGroup* meshgroup, const char* filename, const Mat if (ext && (strcmp(ext, ".stl") == 0 || strcmp(ext, ".STL") == 0)) { Mesh mesh(object_parent_settings); - if (loadMeshSTL(&mesh, filename, transformation)) // Load it! If successful... + // Check for corresponding UV and PNG files + std::string filename_str(filename); + std::string base_filename = filename_str.substr(0, filename_str.find_last_of('.')); + std::string uv_filename = base_filename + ".uv"; + std::string texture_filename = base_filename + ".png"; + + std::vector uv_coordinates; + bool has_uv = loadUVCoordinatesFromFile(uv_filename, uv_coordinates); + + bool load_success = false; + if (has_uv) { + spdlog::info("Loading STL with UV coordinates from: {}", uv_filename); + load_success = loadMeshSTL_with_uv(&mesh, filename, transformation, uv_coordinates); + } + else + { + load_success = loadMeshSTL(&mesh, filename, transformation); + } + if (load_success) // Load it! If successful... + { + // Try to load the PNG texture if it exists + if (std::filesystem::exists(texture_filename)) + { + spdlog::info("Found texture file: {}", texture_filename); + if (MeshUtils::loadTextureFromFile(mesh, texture_filename)) + { + spdlog::info("Successfully loaded texture from: {}", texture_filename); + } + else + { + spdlog::warn("Failed to load texture from: {}", texture_filename); + } + } + meshgroup->meshes.push_back(mesh); spdlog::info("loading '{}' took {:03.3f} seconds", filename, load_timer.restart()); return true; diff --git a/src/communication/ArcusCommunicationPrivate.cpp b/src/communication/ArcusCommunicationPrivate.cpp index b138432056..6db92fa021 100644 --- a/src/communication/ArcusCommunicationPrivate.cpp +++ b/src/communication/ArcusCommunicationPrivate.cpp @@ -18,6 +18,7 @@ #include "Slice.h" #include "settings/types/LayerIndex.h" #include "utils/Matrix4x3D.h" //To convert vertices to integer-points. +#include "utils/MeshUtils.h" #include "utils/Point3F.h" //To accept vertices (which are provided in floating point). namespace cura @@ -163,125 +164,7 @@ void ArcusCommunication::Private::readMeshGroupMessage(const proto::ObjectList& void ArcusCommunication::Private::loadTextureData(const std::string& texture_str, Mesh& mesh) { - if (texture_str.empty()) - { - return; - } - - auto texture_data = reinterpret_cast(texture_str.data()); - const size_t texture_size = texture_str.size(); - png_image raw_texture = {}; - raw_texture.version = PNG_IMAGE_VERSION; - if (! png_image_begin_read_from_memory(&raw_texture, texture_data, texture_size)) - { - spdlog::error("Error when beginning reading mesh texture: {}", raw_texture.message); - return; - } - - std::vector buffer(PNG_IMAGE_SIZE(raw_texture)); - if (! png_image_finish_read(&raw_texture, nullptr, buffer.data(), 0, nullptr) || buffer.empty()) - { - spdlog::error("Error when finishing reading mesh texture: {}", raw_texture.message); - return; - } - - // Make sure pointer will be destroyed when leaving - std::unique_ptr png_ptr( - png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr), - [](png_structp png_ptr_destroy) - { - png_destroy_read_struct(&png_ptr_destroy, nullptr, nullptr); - }); - if (! png_ptr) - { - return; - } - - // Make sure pointer will be destroyed when leaving - std::unique_ptr info_ptr( - png_create_info_struct(png_ptr.get()), - [](png_infop info_ptr_destroy) - { - png_destroy_read_struct(nullptr, &info_ptr_destroy, nullptr); - }); - if (! info_ptr) - { - return; - } - - if (setjmp(png_jmpbuf(png_ptr.get())) != 0) - { - return; - } - - struct PngReadContext - { - const unsigned char* data; - size_t size; - size_t offset; - } read_context{ texture_data, texture_size, 0 }; - - png_set_read_fn( - png_ptr.get(), - &read_context, - [](const png_structp read_png_ptr, const png_bytep out_bytes, const png_size_t byte_count_to_read) - { - auto* context = static_cast(png_get_io_ptr(read_png_ptr)); - if (context->offset + byte_count_to_read > context->size) - { - png_error(read_png_ptr, "Read beyond end of buffer"); - } - memcpy(out_bytes, context->data + context->offset, byte_count_to_read); - context->offset += byte_count_to_read; - }); - png_read_info(png_ptr.get(), info_ptr.get()); - - png_textp text_ptr; - int num_text; - if (png_get_text(png_ptr.get(), info_ptr.get(), &text_ptr, &num_text) <= 0) - { - return; - } - - auto texture_data_mapping = std::make_shared(); - for (int i = 0; i < num_text; ++i) - { - if (std::string(text_ptr[i].key) == "Description") - { - rapidjson::MemoryStream json_memory_stream(text_ptr[i].text, text_ptr[i].text_length); - - rapidjson::Document json_document; - json_document.ParseStream(json_memory_stream); - if (json_document.HasParseError()) - { - spdlog::error("Error parsing texture data mapping (offset {}): {}", json_document.GetErrorOffset(), GetParseError_En(json_document.GetParseError())); - return; - } - - for (auto it = json_document.MemberBegin(); it != json_document.MemberEnd(); ++it) - { - std::string feature_name = it->name.GetString(); - - const rapidjson::Value& array = it->value; - if (array.IsArray() && array.Size() == 2) - { - (*texture_data_mapping)[feature_name] = TextureBitField{ array[0].GetUint(), array[1].GetUint() }; - } - } - - break; - } - } - - if (! texture_data_mapping->empty()) - { - mesh.texture_ = std::make_shared( - raw_texture.width, - raw_texture.height, - PNG_IMAGE_SAMPLE_COMPONENT_SIZE(raw_texture.format) * PNG_IMAGE_SAMPLE_CHANNELS(raw_texture.format), - std::move(buffer)); - mesh.texture_data_mapping_ = texture_data_mapping; - } + MeshUtils::loadTextureFromString(texture_str, mesh); } } // namespace cura diff --git a/src/utils/MeshUtils.cpp b/src/utils/MeshUtils.cpp index 9c0efee4fc..20d27ebc23 100644 --- a/src/utils/MeshUtils.cpp +++ b/src/utils/MeshUtils.cpp @@ -3,8 +3,19 @@ #include "utils/MeshUtils.h" +#include +#include +#include #include +#include +#include +#include +#include +#include + +#include "TextureDataMapping.h" +#include "mesh.h" #include "utils/Point2F.h" #include "utils/Point3D.h" @@ -52,4 +63,203 @@ Point2F getUVCoordinates(const Point3D& barycentric_coordinates, const Triangle2 (uv_coordinates[0].y_ * barycentric_coordinates.x_) + (uv_coordinates[1].y_ * barycentric_coordinates.y_) + (uv_coordinates[2].y_ * barycentric_coordinates.z_)); } +bool loadTextureFromPngData(const std::vector& texture_data, Mesh& mesh, const std::string& source_description, bool log_no_metadata) +{ + if (texture_data.empty()) + { + return false; + } + + // Use PNG library to parse the texture + png_image raw_texture = {}; + raw_texture.version = PNG_IMAGE_VERSION; + if (! png_image_begin_read_from_memory(&raw_texture, texture_data.data(), texture_data.size())) + { + if (log_no_metadata) + { + spdlog::warn("Error reading PNG texture {}: {}", source_description, raw_texture.message); + } + else + { + spdlog::error("Error when beginning reading mesh texture: {}", raw_texture.message); + } + return false; + } + + std::vector buffer(PNG_IMAGE_SIZE(raw_texture)); + if (! png_image_finish_read(&raw_texture, nullptr, buffer.data(), 0, nullptr) || buffer.empty()) + { + if (log_no_metadata) + { + spdlog::warn("Error finishing PNG texture read {}: {}", source_description, raw_texture.message); + } + else + { + spdlog::error("Error when finishing reading mesh texture: {}", raw_texture.message); + } + return false; + } + + // Create PNG reading structures to extract metadata + std::unique_ptr png_ptr( + png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr), + [](png_structp png_ptr_destroy) + { + png_destroy_read_struct(&png_ptr_destroy, nullptr, nullptr); + }); + if (! png_ptr) + { + return false; + } + + std::unique_ptr info_ptr( + png_create_info_struct(png_ptr.get()), + [](png_infop info_ptr_destroy) + { + png_destroy_read_struct(nullptr, &info_ptr_destroy, nullptr); + }); + if (! info_ptr) + { + return false; + } + + if (setjmp(png_jmpbuf(png_ptr.get())) != 0) + { + return false; + } + + struct PngReadContext + { + const unsigned char* data; + size_t size; + size_t offset; + } read_context{ texture_data.data(), texture_data.size(), 0 }; + + png_set_read_fn( + png_ptr.get(), + &read_context, + [](const png_structp read_png_ptr, const png_bytep out_bytes, const png_size_t byte_count_to_read) + { + auto* context = static_cast(png_get_io_ptr(read_png_ptr)); + if (context->offset + byte_count_to_read > context->size) + { + png_error(read_png_ptr, "Read beyond end of buffer"); + } + memcpy(out_bytes, context->data + context->offset, byte_count_to_read); + context->offset += byte_count_to_read; + }); + png_read_info(png_ptr.get(), info_ptr.get()); + + // Extract metadata from PNG text fields + png_textp text_ptr; + int num_text; + if (png_get_text(png_ptr.get(), info_ptr.get(), &text_ptr, &num_text) <= 0) + { + if (log_no_metadata) + { + spdlog::info("No metadata found in texture: {}", source_description); + } + return false; + } + + auto texture_data_mapping = std::make_shared(); + for (int i = 0; i < num_text; ++i) + { + if (std::string(text_ptr[i].key) == "Description") + { + rapidjson::MemoryStream json_memory_stream(text_ptr[i].text, text_ptr[i].text_length); + + rapidjson::Document json_document; + json_document.ParseStream(json_memory_stream); + if (json_document.HasParseError()) + { + if (log_no_metadata) + { + spdlog::warn( + "Error parsing texture metadata in {} (offset {}): {}", + source_description, + json_document.GetErrorOffset(), + GetParseError_En(json_document.GetParseError())); + } + else + { + spdlog::error("Error parsing texture data mapping (offset {}): {}", json_document.GetErrorOffset(), GetParseError_En(json_document.GetParseError())); + } + return false; + } + + // Parse the paint feature manifest + for (auto it = json_document.MemberBegin(); it != json_document.MemberEnd(); ++it) + { + std::string feature_name = it->name.GetString(); + + const rapidjson::Value& array = it->value; + if (array.IsArray() && array.Size() == 2) + { + (*texture_data_mapping)[feature_name] = TextureBitField{ array[0].GetUint(), array[1].GetUint() }; + } + } + + break; + } + } + + if (! texture_data_mapping->empty()) + { + mesh.texture_ = std::make_shared( + raw_texture.width, + raw_texture.height, + PNG_IMAGE_SAMPLE_COMPONENT_SIZE(raw_texture.format) * PNG_IMAGE_SAMPLE_CHANNELS(raw_texture.format), + std::move(buffer)); + mesh.texture_data_mapping_ = texture_data_mapping; + + if (log_no_metadata) + { + spdlog::info("Loaded texture {} with {} paint features", source_description, texture_data_mapping->size()); + } + return true; + } + + return false; +} + +bool loadTextureFromFile(Mesh& mesh, const std::string& texture_filename) +{ + if (! std::filesystem::exists(texture_filename)) + { + return false; // File doesn't exist, not an error + } + + // Read PNG file into memory + std::ifstream file(texture_filename, std::ios::binary | std::ios::ate); + if (! file.is_open()) + { + spdlog::warn("Failed to open texture file: {}", texture_filename); + return false; + } + + const std::streamsize file_size = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector file_data(file_size); + if (! file.read(reinterpret_cast(file_data.data()), file_size)) + { + spdlog::warn("Failed to read texture file: {}", texture_filename); + return false; + } + + return loadTextureFromPngData(file_data, mesh, texture_filename, true); +} + +void loadTextureFromString(const std::string& texture_str, Mesh& mesh) +{ + if (texture_str.empty()) + { + return; + } + + std::vector texture_data(texture_str.begin(), texture_str.end()); + loadTextureFromPngData(texture_data, mesh, "network data", false); +} + } // namespace cura::MeshUtils