IO: C++ STL exporter
There was a C++ STL importer since Blender 3.3, but no corresponding C++ STL exporter. This PR is adding said exporter: taking #105598 and finishing it (agreed with original author). Exporting Suzanne with 6 level subdivision (4 million triangles), on Apple M1 Max: - Binary: python exporter 7.8 sec -> C++ exporter 0.9 sec. - Ascii: python exporter 13.1 sec -> C++ exporter 4.5 sec. Co-authored-by: Iyad Ahmed <iyadahmed430@gmail.com> Pull Request: #114862
This commit is contained in:
parent
31abb2b3af
commit
17c793e43c
@ -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):
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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}")
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
113
source/blender/io/stl/exporter/stl_export.cc
Normal file
113
source/blender/io/stl/exporter/stl_export.cc
Normal file
@ -0,0 +1,113 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup stl
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#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<FileWriter> 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<FileWriter>(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<FileWriter>(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<float3> positions = mesh->vert_positions();
|
||||
const blender::Span<int> 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
|
16
source/blender/io/stl/exporter/stl_export.hh
Normal file
16
source/blender/io/stl/exporter/stl_export.hh
Normal file
@ -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
|
105
source/blender/io/stl/exporter/stl_export_writer.cc
Normal file
105
source/blender/io/stl/exporter/stl_export_writer.cc
Normal file
@ -0,0 +1,105 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup stl
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
/* SEP macro from BLI path utils clashes with SEP symbol in fmt headers. */
|
||||
#undef SEP
|
||||
#include <fmt/format.h>
|
||||
|
||||
#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
|
30
source/blender/io/stl/exporter/stl_export_writer.hh
Normal file
30
source/blender/io/stl/exporter/stl_export_writer.hh
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user