diff --git a/scripts/startup/bl_ui/space_topbar.py b/scripts/startup/bl_ui/space_topbar.py index bd5c928cc12..a9984a6b3b2 100644 --- a/scripts/startup/bl_ui/space_topbar.py +++ b/scripts/startup/bl_ui/space_topbar.py @@ -514,6 +514,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)") + 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.cc b/source/blender/editors/io/io_ops.cc index b7b776a90a1..b255f43c7ce 100644 --- a/source/blender/editors/io/io_ops.cc +++ b/source/blender/editors/io/io_ops.cc @@ -72,5 +72,6 @@ void ED_operatortypes_io() #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.cc b/source/blender/editors/io/io_stl_ops.cc index 989ebe77ca4..867087d29c4 100644 --- a/source/blender/editors/io/io_stl_ops.cc +++ b/source/blender/editors/io/io_stl_ops.cc @@ -16,14 +16,164 @@ # include "DNA_space_types.h" +# include "ED_fileselect.hh" # include "ED_outliner.hh" # include "RNA_access.hh" # include "RNA_define.hh" +# include "BLT_translation.h" + +# include "UI_interface.hh" +# include "UI_resources.hh" + # include "IO_stl.hh" # include "io_stl_ops.hh" +static int wm_stl_export_invoke(bContext *C, wmOperator *op, const wmEvent * /*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 = eIOAxis(RNA_enum_get(op->ptr, "forward_axis")); + export_params.up_axis = eIOAxis(RNA_enum_get(op->ptr, "up_axis")); + export_params.global_scale = RNA_float_get(op->ptr, "global_scale"); + export_params.apply_modifiers = RNA_boolean_get(op->ptr, "apply_modifiers"); + export_params.export_selected_objects = RNA_boolean_get(op->ptr, "export_selected_objects"); + export_params.ascii_format = RNA_boolean_get(op->ptr, "ascii_format"); + 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, "ascii_format", UI_ITEM_NONE, IFACE_("ASCII"), ICON_NONE); + uiItemR(col, op_props_ptr, "use_batch", UI_ITEM_NONE, IFACE_("Batch"), ICON_NONE); + + box = uiLayoutBox(layout); + sub = uiLayoutColumnWithHeading(box, false, IFACE_("Include")); + uiItemR(sub, + op_props_ptr, + "export_selected_objects", + UI_ITEM_NONE, + IFACE_("Selection Only"), + ICON_NONE); + + box = uiLayoutBox(layout); + sub = uiLayoutColumnWithHeading(box, false, IFACE_("Transform")); + uiItemR(sub, op_props_ptr, "global_scale", UI_ITEM_NONE, IFACE_("Scale"), ICON_NONE); + uiItemR(sub, op_props_ptr, "use_scene_unit", UI_ITEM_NONE, IFACE_("Scene Unit"), ICON_NONE); + uiItemR(sub, op_props_ptr, "forward_axis", UI_ITEM_NONE, IFACE_("Forward"), ICON_NONE); + uiItemR(sub, op_props_ptr, "up_axis", UI_ITEM_NONE, IFACE_("Up"), ICON_NONE); + + box = uiLayoutBox(layout); + sub = uiLayoutColumnWithHeading(box, false, IFACE_("Geometry")); + uiItemR( + sub, op_props_ptr, "apply_modifiers", UI_ITEM_NONE, IFACE_("Apply Modifiers"), ICON_NONE); +} + +static void wm_stl_export_draw(bContext * /*C*/, wmOperator *op) +{ + PointerRNA ptr = RNA_pointer_create(nullptr, op->type->srna, op->properties); + ui_stl_export_settings(op->layout, &ptr); +} + +/** + * Return true if any property in the UI is changed. + */ +static bool wm_stl_export_check(bContext * /*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(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, + "ascii_format", + 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, + "export_selected_objects", + 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, 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, io_ui_up_axis_update); + + RNA_def_boolean( + ot->srna, "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 60cbe17495e..0590698af2e 100644 --- a/source/blender/io/stl/CMakeLists.txt +++ b/source/blender/io/stl/CMakeLists.txt @@ -5,6 +5,7 @@ set(INC . importer + exporter ../common ../../blenkernel ../../bmesh @@ -18,6 +19,7 @@ set(INC set(INC_SYS ../../../../extern/fast_float + ../../../../extern/fmtlib/include ) set(SRC @@ -26,12 +28,16 @@ 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.hh importer/stl_import.hh importer/stl_import_ascii_reader.hh importer/stl_import_binary_reader.hh importer/stl_import_mesh.hh + exporter/stl_export_writer.hh + exporter/stl_export.hh ) set(LIB @@ -40,6 +46,7 @@ set(LIB PRIVATE bf::dna PRIVATE bf::intern::guardedalloc bf_io_common + extern_fmtlib ) blender_add_lib(bf_io_stl "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") diff --git a/source/blender/io/stl/IO_stl.cc b/source/blender/io/stl/IO_stl.cc index 8a520c73575..6fdecd4ff12 100644 --- a/source/blender/io/stl/IO_stl.cc +++ b/source/blender/io/stl/IO_stl.cc @@ -9,6 +9,7 @@ #include "BLI_timeit.hh" #include "IO_stl.hh" +#include "stl_export.hh" #include "stl_import.hh" void STL_import(bContext *C, const STLImportParams *import_params) @@ -16,3 +17,9 @@ void STL_import(bContext *C, const STLImportParams *import_params) SCOPED_TIMER("STL Import"); blender::io::stl::importer_main(C, *import_params); } + +void STL_export(bContext *C, const STLExportParams *export_params) +{ + SCOPED_TIMER("STL Export"); + blender::io::stl::exporter_main(C, *export_params); +} diff --git a/source/blender/io/stl/IO_stl.hh b/source/blender/io/stl/IO_stl.hh index 786eb47edbd..764a1504519 100644 --- a/source/blender/io/stl/IO_stl.hh +++ b/source/blender/io/stl/IO_stl.hh @@ -23,7 +23,18 @@ struct STLImportParams { bool use_mesh_validate; }; -/** - * C-interface for the importer. - */ +struct STLExportParams { + /** Full path to the to-be-saved STL file. */ + char filepath[FILE_MAX]; + eIOAxis forward_axis; + eIOAxis up_axis; + float global_scale; + bool export_selected_objects; + bool use_scene_unit; + bool apply_modifiers; + bool ascii_format; + bool use_batch; +}; + void STL_import(bContext *C, const STLImportParams *import_params); +void STL_export(bContext *C, const STLExportParams *export_params); 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..2005b8e03bf --- /dev/null +++ b/source/blender/io/stl/exporter/stl_export.cc @@ -0,0 +1,113 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup stl + */ + +#include +#include +#include + +#include "BKE_mesh.hh" +#include "BKE_object.hh" + +#include "BLI_string.h" + +#include "DEG_depsgraph_query.hh" + +#include "DNA_scene_types.h" + +#include "BLI_math_matrix.h" +#include "BLI_math_rotation.h" +#include "BLI_math_vector.h" +#include "BLI_math_vector.hh" +#include "BLI_math_vector_types.hh" + +#include "IO_stl.hh" + +#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 = std::make_unique(export_params.filepath, export_params.ascii_format); + } + + 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.export_selected_objects && !(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 = std::make_unique(export_params.filepath, export_params.ascii_format); + } + + Object *obj_eval = DEG_get_evaluated_object(depsgraph, object); + Mesh *mesh = export_params.apply_modifiers ? BKE_object_get_evaluated_mesh(obj_eval) : + BKE_object_get_pre_modified_mesh(obj_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 axes_transform[3][3]; + unit_m3(axes_transform); + float xform[4][4]; + /* +Y-forward and +Z-up are the default Blender axis settings. */ + mat3_from_axis_conversion( + export_params.forward_axis, export_params.up_axis, IO_AXIS_Y, IO_AXIS_Z, axes_transform); + mul_m4_m3m4(xform, axes_transform, obj_eval->object_to_world); + /* mul_m4_m3m4 does not transform last row of obmat, i.e. location data. */ + mul_v3_m3v3(xform[3], axes_transform, obj_eval->object_to_world[3]); + xform[3][3] = obj_eval->object_to_world[3][3]; + + /* 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 pos = positions[corner_verts[loop_tri.tri[i]]]; + mul_m4_v3(xform, pos); + pos *= global_scale; + t.vertices[i] = pos; + } + t.normal = math::normal_tri(t.vertices[0], t.vertices[1], t.vertices[2]); + 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..cb342cb42eb --- /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.hh" + +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_writer.cc b/source/blender/io/stl/exporter/stl_export_writer.cc new file mode 100644 index 00000000000..262de1bb274 --- /dev/null +++ b/source/blender/io/stl/exporter/stl_export_writer.cc @@ -0,0 +1,105 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup stl + */ + +#include +#include +#include + +/* SEP macro from BLI path utils clashes with SEP symbol in fmt headers. */ +#undef SEP +#include + +#include "stl_export_writer.hh" + +#include "BLI_fileops.h" + +namespace blender::io::stl { + +constexpr size_t BINARY_HEADER_SIZE = 80; + +#pragma pack(push, 1) +struct ExportBinaryTriangle { + float3 normal; + float3 vertices[3]; + uint16_t attribute_byte_count; +}; +#pragma pack(pop) +static_assert(sizeof(ExportBinaryTriangle) == 12 + 12 * 3 + 2, + "ExportBinaryTriangle expected size mismatch"); + +FileWriter::FileWriter(const char *filepath, bool ascii) : tris_num_(0), ascii_(ascii) +{ + file_ = BLI_fopen(filepath, "wb"); + if (file_ == nullptr) { + throw std::runtime_error("PLY export: failed to open file"); + } + + /* Write header */ + if (ascii_) { + fmt::print(file_, "solid \n"); + } + else { + 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_); + } +} + +FileWriter::~FileWriter() +{ + if (file_ == nullptr) { + return; + } + if (ascii_) { + fmt::print(file_, "endsolid \n"); + } + else { + fseek(file_, BINARY_HEADER_SIZE, SEEK_SET); + fwrite(&tris_num_, sizeof(uint32_t), 1, file_); + } + fclose(file_); +} + +void FileWriter::write_triangle(const Triangle &t) +{ + tris_num_++; + if (ascii_) { + fmt::print(file_, + "facet normal {} {} {}\n" + " outer loop\n" + " vertex {} {} {}\n" + " vertex {} {} {}\n" + " vertex {} {} {}\n" + " endloop\n" + "endfacet\n", + + t.normal.x, + t.normal.y, + t.normal.z, + t.vertices[0].x, + t.vertices[0].y, + t.vertices[0].z, + t.vertices[1].x, + t.vertices[1].y, + t.vertices[1].z, + t.vertices[2].x, + t.vertices[2].y, + t.vertices[2].z); + } + else { + ExportBinaryTriangle bin_tri; + bin_tri.normal = t.normal; + bin_tri.vertices[0] = t.vertices[0]; + bin_tri.vertices[1] = t.vertices[1]; + bin_tri.vertices[2] = t.vertices[2]; + bin_tri.attribute_byte_count = 0; + fwrite(&bin_tri, sizeof(ExportBinaryTriangle), 1, file_); + } +} + +} // 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..ceb817c96c7 --- /dev/null +++ b/source/blender/io/stl/exporter/stl_export_writer.hh @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup stl + */ + +#pragma once + +#include "BLI_math_vector_types.hh" + +namespace blender::io::stl { + +struct Triangle { + float3 normal; + float3 vertices[3]; +}; + +class FileWriter { + public: + FileWriter(const char *filepath, bool ascii); + ~FileWriter(); + void write_triangle(const Triangle &t); + + private: + FILE *file_; + uint32_t tris_num_; + bool ascii_; +}; + +} // namespace blender::io::stl