Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions include/utils/MeshUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#define UTILS_MESH_UTILS_H

#include <optional>
#include <string>
#include <vector>

#include "geometry/Triangle2F.h"
#include "geometry/Triangle3D.h"
Expand All @@ -13,6 +15,7 @@ namespace cura
{

class Point3D;
class Mesh;

namespace MeshUtils
{
Expand All @@ -21,6 +24,37 @@ std::optional<Point3D> 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<unsigned char>& 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
Expand Down
153 changes: 150 additions & 3 deletions src/MeshGroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

#include <fstream>
#include <limits>
#include <png.h>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/memorystream.h>
#include <regex>
#include <stdio.h>
#include <string.h>
Expand All @@ -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"
Expand Down Expand Up @@ -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<Point2F>* uv_coordinates = nullptr)
{
FILE* f = fopen(filename, "rb");

Expand Down Expand Up @@ -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)
Expand All @@ -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<Point2F> uv0 = (*uv_coordinates)[vertex_index];
std::optional<Point2F> uv1 = (*uv_coordinates)[vertex_index + 1];
std::optional<Point2F> 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();
Expand Down Expand Up @@ -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<Point2F>& 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<char*>(&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<char*>(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<Point2F>& 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;
Expand All @@ -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<Point2F> 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;
Expand Down
121 changes: 2 additions & 119 deletions src/communication/ArcusCommunicationPrivate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<const unsigned char*>(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<uint8_t> 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_struct, void (*)(png_structp)> 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<png_info, void (*)(png_infop)> 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<PngReadContext*>(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<TextureDataMapping>();
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<Image>(
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
Expand Down
Loading