From 33553e1c5092f1b0e4fe68b209895d498e91ed66 Mon Sep 17 00:00:00 2001 From: Enrico Turri Date: Thu, 8 Feb 2018 13:26:50 +0100 Subject: [PATCH] 3mf Exporter - 1st installment --- lib/Slic3r/GUI/MainFrame.pm | 3 + lib/Slic3r/GUI/Plater.pm | 30 ++- xs/src/libslic3r/Format/3mf.cpp | 353 +++++++++++++++++++++++++++++++- xs/src/libslic3r/Format/3mf.hpp | 8 +- xs/xsp/Model.xsp | 16 ++ 5 files changed, 400 insertions(+), 10 deletions(-) diff --git a/lib/Slic3r/GUI/MainFrame.pm b/lib/Slic3r/GUI/MainFrame.pm index 3b82ee157..35260011c 100644 --- a/lib/Slic3r/GUI/MainFrame.pm +++ b/lib/Slic3r/GUI/MainFrame.pm @@ -244,6 +244,9 @@ sub _init_menubar { $self->_append_menu_item($self->{plater_menu}, "Export plate as AMF...", 'Export current plate as AMF', sub { $plater->export_amf; }, undef, 'brick_go.png'); + $self->_append_menu_item($self->{plater_menu}, "Export plate as 3MF...", 'Export current plate as 3MF', sub { + $plater->export_3mf; + }, undef, 'brick_go.png'); $self->{object_menu} = $self->{plater}->object_menu; $self->on_plater_selection_changed(0); diff --git a/lib/Slic3r/GUI/Plater.pm b/lib/Slic3r/GUI/Plater.pm index a5a98d649..d9c586685 100644 --- a/lib/Slic3r/GUI/Plater.pm +++ b/lib/Slic3r/GUI/Plater.pm @@ -1550,11 +1550,39 @@ sub export_amf { $self->statusbar->SetStatusText("AMF file exported to $output_file"); } +sub export_3mf { + my ($self) = @_; + return if !@{$self->{objects}}; + # Ask user for a file name to write into. + my $output_file = $self->_get_export_file('3MF') or return; + my $res = $self->{model}->store_3mf($output_file); + if ($res) + { + $self->statusbar->SetStatusText("3MF file exported to $output_file"); + } + else + { + $self->statusbar->SetStatusText("Error exporting 3MF file $output_file"); + } +} + # Ask user to select an output file for a given file format (STl, AMF, 3MF). # Propose a default file name based on the 'output_filename_format' configuration value. sub _get_export_file { my ($self, $format) = @_; - my $suffix = $format eq 'STL' ? '.stl' : '.amf.xml'; + my $suffix = ''; + if ($format eq 'STL') + { + $suffix = '.stl'; + } + elsif ($format eq 'AMF') + { + $suffix = '.amf.xml'; + } + elsif ($format eq '3MF') + { + $suffix = '.3mf'; + } my $output_file = eval { $self->{print}->output_filepath($main::opt{output} // '') }; Slic3r::GUI::catch_error($self) and return undef; $output_file =~ s/\.[gG][cC][oO][dD][eE]$/$suffix/; diff --git a/xs/src/libslic3r/Format/3mf.cpp b/xs/src/libslic3r/Format/3mf.cpp index 6057e1c11..7fe9352b3 100644 --- a/xs/src/libslic3r/Format/3mf.cpp +++ b/xs/src/libslic3r/Format/3mf.cpp @@ -5,13 +5,18 @@ #include #include +#include +#include #include #include #include -const std::string MODEL_FOLDER = "3d\\"; +const std::string MODEL_FOLDER = "3D/"; const std::string MODEL_EXTENSION = ".model"; +const std::string MODEL_FILE = "3D/3dmodel.model"; // << this is the only format of the string which works with CURA +const std::string CONTENT_TYPES_FILE = "[Content_Types].xml"; +const std::string RELATIONSHIPS_FILE = "_rels/.rels"; const char* MODEL_TAG = "model"; const char* RESOURCES_TAG = "resources"; @@ -380,9 +385,12 @@ namespace Slic3r { { std::string name(stat.m_filename); std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) -> unsigned char { return std::tolower(c); }); - std::replace(name.begin(), name.end(), '/', '\\'); + std::replace(name.begin(), name.end(), '\\', '/'); - if ((name.find(MODEL_FOLDER) == 0) && (name.rfind(MODEL_EXTENSION) == name.length() - MODEL_EXTENSION.length())) + std::string lc_model_folder(MODEL_FOLDER); + std::transform(lc_model_folder.begin(), lc_model_folder.end(), lc_model_folder.begin(), [](unsigned char c) -> unsigned char { return std::tolower(c); }); + + if ((name.find(lc_model_folder) == 0) && (name.rfind(MODEL_EXTENSION) == name.length() - MODEL_EXTENSION.length())) { if (!_extract_model_from_archive_miniz(archive, stat)) { @@ -971,7 +979,326 @@ namespace Slic3r { importer->_handle_end_xml_element(name); } - bool load_3mf(const char* path, Model* model, const char* object_name) + class _3MF_Exporter + { + struct BuildItem + { + unsigned int id; + Matrix4x4 matrix; + + BuildItem(unsigned int id, const Matrix4x4& matrix); + }; + + typedef std::vector BuildItemsList; + + std::vector m_errors; + + public: + bool save_model_to_file(const std::string& filename, Model& model); + + const std::vector& get_errors() const; + + private: + bool _save_model_to_file_miniz(const std::string& filename, Model& model); + bool _add_content_types_file_to_archive_miniz(mz_zip_archive& archive); + bool _add_relationships_file_to_archive_miniz(mz_zip_archive& archive); + bool _add_model_file_to_archive_miniz(mz_zip_archive& archive, Model& model); + bool _add_object_to_model_stream(std::stringstream& stream, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items); + bool _add_mesh_to_object_stream(std::stringstream& stream, ModelObject& object); + bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items); + }; + + _3MF_Exporter::BuildItem::BuildItem(unsigned int id, const Matrix4x4& matrix) + : id(id) + , matrix(matrix) + { + } + + bool _3MF_Exporter::save_model_to_file(const std::string& filename, Model& model) + { + return _save_model_to_file_miniz(filename, model); + } + + const std::vector& _3MF_Exporter::get_errors() const + { + return m_errors; + } + + bool _3MF_Exporter::_save_model_to_file_miniz(const std::string& filename, Model& model) + { + mz_zip_archive archive; + mz_zip_zero_struct(&archive); + + mz_bool res = mz_zip_writer_init_file(&archive, filename.c_str(), 0); + if (res == 0) + { + m_errors.push_back("Unable to open the file"); + return false; + } + + // adds content types file + if (!_add_content_types_file_to_archive_miniz(archive)) + { + mz_zip_writer_end(&archive); + boost::filesystem::remove(filename); + return false; + } + + // adds relationships file + if (!_add_relationships_file_to_archive_miniz(archive)) + { + mz_zip_writer_end(&archive); + boost::filesystem::remove(filename); + return false; + } + + // adds model file + if (!_add_model_file_to_archive_miniz(archive, model)) + { + mz_zip_writer_end(&archive); + boost::filesystem::remove(filename); + return false; + } + + if (!mz_zip_writer_finalize_archive(&archive)) + { + mz_zip_writer_end(&archive); + boost::filesystem::remove(filename); + m_errors.push_back("Unable to finalize the archive"); + return false; + } + + mz_zip_writer_end(&archive); + + return true; + } + + bool _3MF_Exporter::_add_content_types_file_to_archive_miniz(mz_zip_archive& archive) + { + std::stringstream stream; + stream << "\n"; + stream << "\n"; + stream << " \n"; + stream << " \n"; + stream << ""; + + std::string out = stream.str(); + + if (!mz_zip_writer_add_mem(&archive, CONTENT_TYPES_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) + { + m_errors.push_back("Unable to add content types file to archive"); + return false; + } + + return true; + } + + bool _3MF_Exporter::_add_relationships_file_to_archive_miniz(mz_zip_archive& archive) + { + std::stringstream stream; + stream << "\n"; + stream << "\n"; + stream << " \n"; + stream << ""; + + std::string out = stream.str(); + + if (!mz_zip_writer_add_mem(&archive, RELATIONSHIPS_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) + { + m_errors.push_back("Unable to add relationships file to archive"); + return false; + } + + return true; + } + + bool _3MF_Exporter::_add_model_file_to_archive_miniz(mz_zip_archive& archive, Model& model) + { + std::stringstream stream; + stream << "\n"; + stream << "\n"; + stream << " \n"; + + BuildItemsList build_items; + + unsigned int object_id = 1; + for (ModelObject* obj : model.objects) + { + if (obj == nullptr) + continue; + + if (!_add_object_to_model_stream(stream, object_id, *obj, build_items)) + { + m_errors.push_back("Unable to add object to archive"); + return false; + } + } + + + stream << " \n"; + + if (!_add_build_to_model_stream(stream, build_items)) + { + m_errors.push_back("Unable to add build to archive"); + return false; + } + + stream << "\n"; + + std::string out = stream.str(); + + if (!mz_zip_writer_add_mem(&archive, MODEL_FILE.c_str(), (const void*)out.data(), out.length(), MZ_DEFAULT_COMPRESSION)) + { + m_errors.push_back("Unable to add model file to archive"); + return false; + } + + return true; + } + + bool _3MF_Exporter::_add_object_to_model_stream(std::stringstream& stream, unsigned int& object_id, ModelObject& object, BuildItemsList& build_items) + { + unsigned int id = 0; + for (const ModelInstance* instance : object.instances) + { + if (instance == nullptr) + continue; + + unsigned int instance_id = object_id + id; + stream << " \n"; + + if (id == 0) + { + if (!_add_mesh_to_object_stream(stream, object)) + { + m_errors.push_back("Unable to add mesh to archive"); + return false; + } + } + else + { + stream << " \n"; + stream << " \n"; + stream << " \n"; + } + + Eigen::Affine3f transform; + transform = Eigen::Translation3f((float)(instance->offset.x + object.origin_translation.x), (float)(instance->offset.y + object.origin_translation.y), (float)object.origin_translation.z) + * Eigen::AngleAxisf((float)instance->rotation, Eigen::Vector3f::UnitZ()) + * Eigen::Scaling((float)instance->scaling_factor); + build_items.emplace_back(instance_id, transform.matrix()); + + stream << " \n"; + + ++id; + } + + object_id += id; + return true; + } + + bool _3MF_Exporter::_add_mesh_to_object_stream(std::stringstream& stream, ModelObject& object) + { + stream << " \n"; + stream << " \n"; + + typedef std::map VolumeToOffsetMap; + VolumeToOffsetMap volumes_offset; + unsigned int vertices_count = 0; + for (ModelVolume* volume : object.volumes) + { + if (volume == nullptr) + continue; + + volumes_offset.insert(VolumeToOffsetMap::value_type(volume, vertices_count)); + + if (!volume->mesh.repaired) + volume->mesh.repair(); + + stl_file& stl = volume->mesh.stl; + if (stl.v_shared == nullptr) + stl_generate_shared_vertices(&stl); + + if (stl.stats.shared_vertices == 0) + { + m_errors.push_back("Found invalid mesh"); + return false; + } + + vertices_count += stl.stats.shared_vertices; + + for (int i = 0; i < stl.stats.shared_vertices; ++i) + { + stream << " \n"; + } + } + + stream << " \n"; + stream << " \n"; + + for (ModelVolume* volume : object.volumes) + { + if (volume == nullptr) + continue; + + VolumeToOffsetMap::const_iterator offset_it = volumes_offset.find(volume); + assert(offset_it != volumes_offset.end()); + + stl_file& stl = volume->mesh.stl; + + for (uint32_t i = 0; i < stl.stats.number_of_facets; ++i) + { + stream << " second << "\" "; + } + stream << "/>\n"; + } + + } + + stream << " \n"; + stream << " \n"; + + return true; + } + + bool _3MF_Exporter::_add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) + { + if (build_items.size() == 0) + { + m_errors.push_back("No build item found"); + return false; + } + + stream << " \n"; + + for (const BuildItem& item : build_items) + { + stream << " \n"; + } + + stream << " \n"; + + return true; + } + + bool load_3mf(const char* path, Model* model) { if ((path == nullptr) || (model == nullptr)) return false; @@ -980,11 +1307,23 @@ namespace Slic3r { bool res = importer.load_model_from_file(path, *model); if (!res) - { const std::vector& errors = importer.get_errors(); - int a = 0; - } return res; } + + bool store_3mf(const char* path, Model* model) + { + if ((path == nullptr) || (model == nullptr)) + return false; + + _3MF_Exporter exporter; + bool res = exporter.save_model_to_file(path, *model); + + if (!res) + const std::vector& errors = exporter.get_errors(); + + return res; + } + } // namespace Slic3r diff --git a/xs/src/libslic3r/Format/3mf.hpp b/xs/src/libslic3r/Format/3mf.hpp index 11eb4c388..ff178e3de 100644 --- a/xs/src/libslic3r/Format/3mf.hpp +++ b/xs/src/libslic3r/Format/3mf.hpp @@ -5,8 +5,12 @@ namespace Slic3r { class Model; - // Load an 3mf file into a provided model. - extern bool load_3mf(const char* path, Model* model, const char* object_name = nullptr); + // Load a 3mf file into the given model. + extern bool load_3mf(const char* path, Model* model); + + // Save the given model into a 3mf file. + // The model could be modified during the export process if meshes are not repaired or have no shared vertices + extern bool store_3mf(const char* path, Model* model); }; // namespace Slic3r #endif /* slic3r_Format_3mf_hpp_ */ diff --git a/xs/xsp/Model.xsp b/xs/xsp/Model.xsp index c96c56386..9a2a50437 100644 --- a/xs/xsp/Model.xsp +++ b/xs/xsp/Model.xsp @@ -6,6 +6,7 @@ #include "libslic3r/PrintConfig.hpp" #include "libslic3r/Slicing.hpp" #include "libslic3r/Format/AMF.hpp" +#include "libslic3r/Format/3mf.hpp" #include "libslic3r/Format/OBJ.hpp" #include "libslic3r/Format/PRUS.hpp" #include "libslic3r/Format/STL.hpp" @@ -91,6 +92,8 @@ %code%{ TriangleMesh mesh = THIS->mesh(); RETVAL = Slic3r::store_stl(path, &mesh, binary); %}; bool store_amf(char *path) %code%{ RETVAL = Slic3r::store_amf(path, THIS); %}; + bool store_3mf(char *path) + %code%{ RETVAL = Slic3r::store_3mf(path, THIS); %}; %{ @@ -135,6 +138,19 @@ load_amf(CLASS, path) OUTPUT: RETVAL +Model* +load_3mf(CLASS, path) + char* CLASS; + char* path; + CODE: + RETVAL = new Model(); + if (! load_3mf(path, RETVAL)) { + delete RETVAL; + RETVAL = NULL; + } + OUTPUT: + RETVAL + Model* load_prus(CLASS, path) char* CLASS;