diff --git a/scripts/startup/bl_ui/space_topbar.py b/scripts/startup/bl_ui/space_topbar.py index 3d61e7f6c68..bf571f3b191 100644 --- a/scripts/startup/bl_ui/space_topbar.py +++ b/scripts/startup/bl_ui/space_topbar.py @@ -508,6 +508,8 @@ class TOPBAR_MT_file_export(Menu): self.layout.operator("wm.obj_export", text="Wavefront (.obj)") if bpy.app.build_options.io_ply: self.layout.operator("wm.ply_export", text="Stanford PLY (.ply) (experimental)") + if bpy.app.build_options.io_stl: + self.layout.operator("wm.stl_export", text="STL (.stl) (experimental)") class TOPBAR_MT_file_external_data(Menu): diff --git a/source/blender/editors/io/io_ops.c b/source/blender/editors/io/io_ops.c index d8c4d140ad2..0acada666ec 100644 --- a/source/blender/editors/io/io_ops.c +++ b/source/blender/editors/io/io_ops.c @@ -72,5 +72,6 @@ void ED_operatortypes_io(void) #ifdef WITH_IO_STL WM_operatortype_append(WM_OT_stl_import); + WM_operatortype_append(WM_OT_stl_export); #endif } diff --git a/source/blender/editors/io/io_stl_ops.c b/source/blender/editors/io/io_stl_ops.c index bbdb494e48a..a8320293621 100644 --- a/source/blender/editors/io/io_stl_ops.c +++ b/source/blender/editors/io/io_stl_ops.c @@ -14,14 +14,162 @@ # include "DNA_space_types.h" +# include "ED_fileselect.h" # include "ED_outliner.h" # include "RNA_access.h" # include "RNA_define.h" +# include "BLT_translation.h" + +# include "UI_interface.h" +# include "UI_resources.h" + # include "IO_stl.h" # include "io_stl_ops.h" +static int wm_stl_export_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) +{ + ED_fileselect_ensure_default_filepath(C, op, ".stl"); + + WM_event_add_fileselect(C, op); + return OPERATOR_RUNNING_MODAL; +} + +static int wm_stl_export_execute(bContext *C, wmOperator *op) +{ + if (!RNA_struct_property_is_set_ex(op->ptr, "filepath", false)) { + BKE_report(op->reports, RPT_ERROR, "No filename given"); + return OPERATOR_CANCELLED; + } + struct STLExportParams export_params; + RNA_string_get(op->ptr, "filepath", export_params.filepath); + export_params.forward_axis = RNA_enum_get(op->ptr, "forward_axis"); + export_params.up_axis = RNA_enum_get(op->ptr, "up_axis"); + export_params.global_scale = RNA_float_get(op->ptr, "global_scale"); + export_params.use_apply_modifiers = RNA_boolean_get(op->ptr, "use_apply_modifiers"); + export_params.use_selection_only = RNA_boolean_get(op->ptr, "use_selection_only"); + export_params.use_ascii = RNA_boolean_get(op->ptr, "use_ascii"); + export_params.use_batch = RNA_boolean_get(op->ptr, "use_batch"); + + STL_export(C, &export_params); + + return OPERATOR_FINISHED; +} + +static void ui_stl_export_settings(uiLayout *layout, PointerRNA *op_props_ptr) +{ + uiLayoutSetPropSep(layout, true); + uiLayoutSetPropDecorate(layout, false); + + uiLayout *box, *col, *sub; + + box = uiLayoutBox(layout); + col = uiLayoutColumn(box, false); + uiItemR(col, op_props_ptr, "use_ascii", 0, IFACE_("ASCII"), ICON_NONE); + uiItemR(col, op_props_ptr, "use_batch", 0, IFACE_("Batch"), ICON_NONE); + + box = uiLayoutBox(layout); + sub = uiLayoutColumnWithHeading(box, false, IFACE_("Include")); + uiItemR(sub, op_props_ptr, "use_selection_only", 0, IFACE_("Selection Only"), ICON_NONE); + + box = uiLayoutBox(layout); + sub = uiLayoutColumnWithHeading(box, false, IFACE_("Transform")); + uiItemR(sub, op_props_ptr, "global_scale", 0, IFACE_("Scale"), ICON_NONE); + uiItemR(sub, op_props_ptr, "use_scene_unit", 0, IFACE_("Scene Unit"), ICON_NONE); + uiItemR(sub, op_props_ptr, "forward_axis", 0, IFACE_("Forward"), ICON_NONE); + uiItemR(sub, op_props_ptr, "up_axis", 0, IFACE_("Up"), ICON_NONE); + + box = uiLayoutBox(layout); + sub = uiLayoutColumnWithHeading(box, false, IFACE_("Geometry")); + uiItemR(sub, op_props_ptr, "use_apply_modifiers", 0, IFACE_("Apply Modifiers"), ICON_NONE); +} + +static void wm_stl_export_draw(bContext *UNUSED(C), wmOperator *op) +{ + PointerRNA ptr; + RNA_pointer_create(NULL, op->type->srna, op->properties, &ptr); + ui_stl_export_settings(op->layout, &ptr); +} + +/** + * Return true if any property in the UI is changed. + */ +static bool wm_stl_export_check(bContext *UNUSED(C), wmOperator *op) +{ + char filepath[FILE_MAX]; + bool changed = false; + RNA_string_get(op->ptr, "filepath", filepath); + + if (!BLI_path_extension_check(filepath, ".stl")) { + BLI_path_extension_ensure(filepath, FILE_MAX, ".stl"); + RNA_string_set(op->ptr, "filepath", filepath); + changed = true; + } + return changed; +} + +void WM_OT_stl_export(struct wmOperatorType *ot) +{ + PropertyRNA *prop; + + ot->name = "Export STL"; + ot->description = "Save the scene to an STL file"; + ot->idname = "WM_OT_stl_export"; + + ot->invoke = wm_stl_export_invoke; + ot->exec = wm_stl_export_execute; + ot->poll = WM_operator_winactive; + ot->ui = wm_stl_export_draw; + ot->check = wm_stl_export_check; + + ot->flag = OPTYPE_PRESET; + + WM_operator_properties_filesel(ot, + FILE_TYPE_FOLDER, + FILE_BLENDER, + FILE_SAVE, + WM_FILESEL_FILEPATH | WM_FILESEL_SHOW_PROPS, + FILE_DEFAULTDISPLAY, + FILE_SORT_DEFAULT); + + RNA_def_boolean(ot->srna, + "use_ascii", + false, + "ASCII Format", + "Export file in ASCII format, export as binary otherwise"); + RNA_def_boolean( + ot->srna, "use_batch", false, "Batch Export", "Export each object to a separate file"); + RNA_def_boolean(ot->srna, + "use_selection_only", + false, + "Export Selected Objects", + "Export only selected objects instead of all supported objects"); + + RNA_def_float(ot->srna, "global_scale", 1.0f, 1e-6f, 1e6f, "Scale", "", 0.001f, 1000.0f); + RNA_def_boolean(ot->srna, + "use_scene_unit", + false, + "Scene Unit", + "Apply current scene's unit (as defined by unit scale) to exported data"); + + prop = RNA_def_enum(ot->srna, "forward_axis", io_transform_axis, IO_AXIS_Y, "Forward Axis", ""); + RNA_def_property_update_runtime(prop, (void *)io_ui_forward_axis_update); + + prop = RNA_def_enum(ot->srna, "up_axis", io_transform_axis, IO_AXIS_Z, "Up Axis", ""); + RNA_def_property_update_runtime(prop, (void *)io_ui_up_axis_update); + + RNA_def_boolean(ot->srna, + "use_apply_modifiers", + true, + "Apply Modifiers", + "Apply modifiers to exported meshes"); + + /* Only show .stl files by default. */ + prop = RNA_def_string(ot->srna, "filter_glob", "*.stl", 0, "Extension Filter", ""); + RNA_def_property_flag(prop, PROP_HIDDEN); +} + static int wm_stl_import_invoke(bContext *C, wmOperator *op, const wmEvent *event) { return WM_operator_filesel(C, op, event); diff --git a/source/blender/io/stl/CMakeLists.txt b/source/blender/io/stl/CMakeLists.txt index 3a250d78499..d1499ef4ee2 100644 --- a/source/blender/io/stl/CMakeLists.txt +++ b/source/blender/io/stl/CMakeLists.txt @@ -3,6 +3,7 @@ set(INC . importer + exporter ../common ../../blenkernel ../../blenlib @@ -15,6 +16,7 @@ set(INC ../../nodes ../../windowmanager ../../../../extern/fast_float + ../../../../extern/fmtlib/include ../../../../intern/guardedalloc ) @@ -28,12 +30,18 @@ set(SRC importer/stl_import_ascii_reader.cc importer/stl_import_binary_reader.cc importer/stl_import_mesh.cc + exporter/stl_export.cc + exporter/stl_export_writer.cc IO_stl.h importer/stl_import.hh importer/stl_import_ascii_reader.hh importer/stl_import_binary_reader.hh importer/stl_import_mesh.hh + exporter/stl_export_binary_writer.hh + exporter/stl_export_ascii_writer.hh + exporter/stl_export_writer.hh + exporter/stl_export.hh ) set(LIB diff --git a/source/blender/io/stl/IO_stl.cc b/source/blender/io/stl/IO_stl.cc index b26c1533692..d3605009bbe 100644 --- a/source/blender/io/stl/IO_stl.cc +++ b/source/blender/io/stl/IO_stl.cc @@ -7,6 +7,7 @@ #include "BLI_timeit.hh" #include "IO_stl.h" +#include "stl_export.hh" #include "stl_import.hh" void STL_import(bContext *C, const struct STLImportParams *import_params) @@ -14,3 +15,9 @@ void STL_import(bContext *C, const struct STLImportParams *import_params) SCOPED_TIMER("STL Import"); blender::io::stl::importer_main(C, *import_params); } + +void STL_export(bContext *C, const struct STLExportParams *export_params) +{ + SCOPED_TIMER("STL Export"); + blender::io::stl::exporter_main(C, *export_params); +} diff --git a/source/blender/io/stl/IO_stl.h b/source/blender/io/stl/IO_stl.h index bbe537948e8..6a9693024ce 100644 --- a/source/blender/io/stl/IO_stl.h +++ b/source/blender/io/stl/IO_stl.h @@ -25,11 +25,29 @@ struct STLImportParams { bool use_mesh_validate; }; +struct STLExportParams { + /** Full path to the to-be-saved STL file. */ + char filepath[FILE_MAX]; + eIOAxis forward_axis; + eIOAxis up_axis; + bool use_selection_only; + bool use_scene_unit; + bool use_apply_modifiers; + bool use_ascii; + bool use_batch; + float global_scale; +}; + /** * C-interface for the importer. */ void STL_import(bContext *C, const struct STLImportParams *import_params); +/** + * C-interface for the exporter. + */ +void STL_export(bContext *C, const struct STLExportParams *export_params); + #ifdef __cplusplus } #endif diff --git a/source/blender/io/stl/exporter/stl_export.cc b/source/blender/io/stl/exporter/stl_export.cc new file mode 100644 index 00000000000..c9f375f9804 --- /dev/null +++ b/source/blender/io/stl/exporter/stl_export.cc @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup stl + */ + +#include +#include + +#include "BKE_mesh.hh" +#include "BKE_object.h" + +#include "DEG_depsgraph_query.h" + +#include "DNA_layer_types.h" +#include "DNA_scene_types.h" + +#include "BLI_math_vector.h" +#include "BLI_math_vector_types.hh" + +#include "IO_stl.h" + +#include "bmesh.h" +#include "bmesh_tools.h" + +#include "stl_export.hh" +#include "stl_export_writer.hh" + +namespace blender::io::stl { + +void exporter_main(bContext *C, const STLExportParams &export_params) +{ + std::unique_ptr writer; + + Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); + Scene *scene = CTX_data_scene(C); + + /* If not exporting in batch, create single writer for all objects. */ + if (!export_params.use_batch) { + writer = create_writer(export_params.filepath, + export_params.use_ascii ? FileWriter::Type::ASCII : + FileWriter::Type::BINARY); + } + + DEGObjectIterSettings deg_iter_settings{}; + deg_iter_settings.depsgraph = depsgraph; + deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY | + DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET | DEG_ITER_OBJECT_FLAG_VISIBLE | + DEG_ITER_OBJECT_FLAG_DUPLI; + + DEG_OBJECT_ITER_BEGIN (°_iter_settings, object) { + if (object->type != OB_MESH) { + continue; + } + + if (export_params.use_selection_only && !(object->base_flag & BASE_SELECTED)) { + continue; + } + + /* If exporting in batch, create writer for each iteration over objects. */ + if (export_params.use_batch) { + /* Get object name by skipping initial "OB" prefix. */ + std::string object_name = (object->id.name + 2); + /* Replace spaces with underscores. */ + std::replace(object_name.begin(), object_name.end(), ' ', '_'); + + /* Include object name in the exported file name. */ + std::string suffix = object_name + ".stl"; + char filepath[FILE_MAX]; + BLI_strncpy(filepath, export_params.filepath, FILE_MAX); + BLI_path_extension_replace(filepath, FILE_MAX, suffix.c_str()); + writer = create_writer( + filepath, export_params.use_ascii ? FileWriter::Type::ASCII : FileWriter::Type::BINARY); + } + + Object *obj_eval = DEG_get_evaluated_object(depsgraph, object); + Object export_object_eval_ = dna::shallow_copy(*obj_eval); + Mesh *mesh = export_params.use_apply_modifiers ? + BKE_object_get_evaluated_mesh(&export_object_eval_) : + BKE_object_get_pre_modified_mesh(&export_object_eval_); + + /* Calculate transform. */ + float global_scale = export_params.global_scale; + if ((scene->unit.system != USER_UNIT_NONE) && export_params.use_scene_unit) { + global_scale *= scene->unit.scale_length; + } + float scale_vec[3] = {global_scale, global_scale, global_scale}; + float obmat3x3[3][3]; + unit_m3(obmat3x3); + float obmat4x4[4][4]; + unit_m4(obmat4x4); + /* +Y-forward and +Z-up are the Blender's default axis settings. */ + mat3_from_axis_conversion( + IO_AXIS_Y, IO_AXIS_Z, export_params.forward_axis, export_params.up_axis, obmat3x3); + copy_m4_m3(obmat4x4, obmat3x3); + rescale_m4(obmat4x4, scale_vec); + + /* Write triangles. */ + const Span positions = mesh->vert_positions(); + const blender::Span corner_verts = mesh->corner_verts(); + for (const MLoopTri &loop_tri : mesh->looptris()) { + Triangle t{}; + for (int i = 0; i < 3; i++) { + float3 co = positions[corner_verts[loop_tri.tri[i]]]; + mul_m4_v3(obmat4x4, co); + for (int j = 0; j < 3; j++) { + t.vertices[i][j] = co[j]; + } + } + writer->write_triangle(&t); + } + } + DEG_OBJECT_ITER_END; +} + +} // namespace blender::io::stl diff --git a/source/blender/io/stl/exporter/stl_export.hh b/source/blender/io/stl/exporter/stl_export.hh new file mode 100644 index 00000000000..c82ddb54d8a --- /dev/null +++ b/source/blender/io/stl/exporter/stl_export.hh @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup stl + */ + +#pragma once + +#include "IO_stl.h" + +namespace blender::io::stl { + +/* Main export function used from within Blender. */ +void exporter_main(bContext *C, const STLExportParams &export_params); + +} // namespace blender::io::stl diff --git a/source/blender/io/stl/exporter/stl_export_ascii_writer.hh b/source/blender/io/stl/exporter/stl_export_ascii_writer.hh new file mode 100644 index 00000000000..5c1bdf3a52b --- /dev/null +++ b/source/blender/io/stl/exporter/stl_export_ascii_writer.hh @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup stl + */ + +#pragma once + +#include + +#include "BLI_utility_mixins.hh" + +/* SEP macro from BLI path utils clashes with SEP symbol in fmt headers. */ +#undef SEP +#define FMT_HEADER_ONLY +#include + +#include "stl_export_writer.hh" + +namespace blender::io::stl { + +class ASCIIFileWriter : public FileWriter, NonCopyable { + private: + std::ofstream file_; + + public: + explicit ASCIIFileWriter(const char *filepath); + ~ASCIIFileWriter() override; + void write_triangle(const Triangle *t) override; +}; + +ASCIIFileWriter::ASCIIFileWriter(const char *filepath) : file_(filepath) +{ + file_ << "solid \n"; +} + +void ASCIIFileWriter::write_triangle(const Triangle *t) +{ + file_ << fmt::format( + "facet normal {} {} {}\n" + "\touter loop\n" + "\t\tvertex {} {} {}\n" + "\t\tvertex {} {} {}\n" + "\t\tvertex {} {} {}\n" + "\tendloop\n" + "endfacet\n", + + t->normal[0], + t->normal[1], + t->normal[2], + t->vertices[0][0], + t->vertices[0][1], + t->vertices[0][2], + t->vertices[1][0], + t->vertices[1][1], + t->vertices[1][2], + t->vertices[2][0], + t->vertices[2][1], + t->vertices[2][2]); +} + +ASCIIFileWriter::~ASCIIFileWriter() +{ + file_ << "endsolid \n"; +} + +} // namespace blender::io::stl diff --git a/source/blender/io/stl/exporter/stl_export_binary_writer.hh b/source/blender/io/stl/exporter/stl_export_binary_writer.hh new file mode 100644 index 00000000000..2c6f1fb50f1 --- /dev/null +++ b/source/blender/io/stl/exporter/stl_export_binary_writer.hh @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup stl + */ + +#pragma once + +#include +#include +#include +#include + +#include "BLI_assert.h" +#include "BLI_utility_mixins.hh" + +#include "stl_export_writer.hh" + +namespace blender::io::stl { + +#pragma pack(push, 1) +struct STLBinaryTriangle { + float normal[3]{}; + float vertices[3][3]{}; + uint16_t attribute_byte_count{}; +}; +#pragma pack(pop) +BLI_STATIC_ASSERT_ALIGN(STLBinaryTriangle, + sizeof(float[3]) + sizeof(float[3][3]) + sizeof(uint16_t)); + +class BinaryFileWriter : public FileWriter, NonCopyable { + private: + FILE *file_ = nullptr; + uint32_t tris_num_ = 0; + static constexpr size_t BINARY_HEADER_SIZE = 80; + + public: + explicit BinaryFileWriter(const char *filepath); + ~BinaryFileWriter() override; + void write_triangle(const Triangle *t) override; +}; + +BinaryFileWriter::BinaryFileWriter(const char *filepath) +{ + file_ = fopen(filepath, "wb"); + if (file_ == nullptr) { + throw std::runtime_error("Failed to open file"); + } + + char header[BINARY_HEADER_SIZE] = {}; + fwrite(header, 1, BINARY_HEADER_SIZE, file_); + /* Write placeholder for number of triangles, so that it can be updated later (after all + * triangles have been written). */ + fwrite(&tris_num_, sizeof(uint32_t), 1, file_); +} + +void BinaryFileWriter::write_triangle(const Triangle *t) +{ + STLBinaryTriangle packed_triangle{}; + memcpy(packed_triangle.normal, t->normal, sizeof(float[3])); + memcpy(packed_triangle.vertices, t->vertices, sizeof(float[3][3])); + packed_triangle.attribute_byte_count = 0; + + if (fwrite(&packed_triangle, sizeof(STLBinaryTriangle), 1, file_) == 1) { + tris_num_++; + } +} + +BinaryFileWriter::~BinaryFileWriter() +{ + assert(file_ != nullptr); + fseek(file_, BINARY_HEADER_SIZE, SEEK_SET); + fwrite(&tris_num_, sizeof(uint32_t), 1, file_); + fclose(file_); +} + +} // namespace blender::io::stl diff --git a/source/blender/io/stl/exporter/stl_export_writer.cc b/source/blender/io/stl/exporter/stl_export_writer.cc new file mode 100644 index 00000000000..64069cbae51 --- /dev/null +++ b/source/blender/io/stl/exporter/stl_export_writer.cc @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup stl + */ + +#include +#include + +#include "stl_export_ascii_writer.hh" +#include "stl_export_binary_writer.hh" +#include "stl_export_writer.hh" + +namespace blender::io::stl { + +std::unique_ptr create_writer(const char *filepath, FileWriter::Type type) +{ + if (type == FileWriter::Type::ASCII) { + return std::make_unique(filepath); + } + else if (type == FileWriter::Type::BINARY) { + return std::make_unique(filepath); + } + else { + throw std::runtime_error("Not implemented"); + } +} + +} // namespace blender::io::stl diff --git a/source/blender/io/stl/exporter/stl_export_writer.hh b/source/blender/io/stl/exporter/stl_export_writer.hh new file mode 100644 index 00000000000..be6f7beea6a --- /dev/null +++ b/source/blender/io/stl/exporter/stl_export_writer.hh @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup stl + */ + +#pragma once + +#include +#include + +namespace blender::io::stl { + +struct Triangle { + float normal[3]{}; + float vertices[3][3]{}; +}; + +class FileWriter { + public: + enum class Type { BINARY, ASCII }; + + virtual ~FileWriter() = default; + virtual void write_triangle(const Triangle *t) = 0; +}; + +std::unique_ptr create_writer(const char *filepath, FileWriter::Type type); + +} // namespace blender::io::stl