From 0d0aad62800a3f19ee01fc2cdcc1a74d5248306e Mon Sep 17 00:00:00 2001 From: Aras Pranckevicius Date: Mon, 20 Nov 2023 11:19:50 +0100 Subject: [PATCH] IO: add STL import/export tests Pull Request: https://projects.blender.org/blender/blender/pulls/115164 --- source/blender/io/stl/CMakeLists.txt | 25 ++++ source/blender/io/stl/exporter/stl_export.cc | 23 +-- source/blender/io/stl/exporter/stl_export.hh | 4 +- .../io/stl/tests/stl_exporter_tests.cc | 132 ++++++++++++++++++ .../io/stl/tests/stl_importer_tests.cc | 127 +++++++++++++++++ tests/python/CMakeLists.txt | 60 -------- 6 files changed, 302 insertions(+), 69 deletions(-) create mode 100644 source/blender/io/stl/tests/stl_exporter_tests.cc create mode 100644 source/blender/io/stl/tests/stl_importer_tests.cc diff --git a/source/blender/io/stl/CMakeLists.txt b/source/blender/io/stl/CMakeLists.txt index 0590698af2e..0bf9c44f64c 100644 --- a/source/blender/io/stl/CMakeLists.txt +++ b/source/blender/io/stl/CMakeLists.txt @@ -50,3 +50,28 @@ set(LIB ) blender_add_lib(bf_io_stl "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") + +if(WITH_GTESTS) + set(TEST_SRC + tests/stl_exporter_tests.cc + tests/stl_importer_tests.cc + ) + + set(TEST_INC + ${INC} + + ../../blenloader + ../../../../tests/gtests + ) + + set(TEST_LIB + ${LIB} + + bf_blenloader_tests + bf_io_stl + ) + + include(GTestTesting) + blender_add_test_lib(bf_io_stl_tests "${TEST_SRC}" "${TEST_INC}" "${INC_SYS}" "${TEST_LIB}") + add_dependencies(bf_io_stl_tests bf_io_stl) +endif() diff --git a/source/blender/io/stl/exporter/stl_export.cc b/source/blender/io/stl/exporter/stl_export.cc index 2005b8e03bf..a0e1626a769 100644 --- a/source/blender/io/stl/exporter/stl_export.cc +++ b/source/blender/io/stl/exporter/stl_export.cc @@ -30,13 +30,12 @@ namespace blender::io::stl { -void exporter_main(bContext *C, const STLExportParams &export_params) +void export_frame(Depsgraph *depsgraph, + float scene_unit_scale, + 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); @@ -77,10 +76,7 @@ void exporter_main(bContext *C, const STLExportParams &export_params) 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 global_scale = export_params.global_scale * scene_unit_scale; float axes_transform[3][3]; unit_m3(axes_transform); float xform[4][4]; @@ -110,4 +106,15 @@ void exporter_main(bContext *C, const STLExportParams &export_params) DEG_OBJECT_ITER_END; } +void exporter_main(bContext *C, const STLExportParams &export_params) +{ + Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); + Scene *scene = CTX_data_scene(C); + float scene_unit_scale = 1.0f; + if ((scene->unit.system != USER_UNIT_NONE) && export_params.use_scene_unit) { + scene_unit_scale = scene->unit.scale_length; + } + export_frame(depsgraph, scene_unit_scale, export_params); +} + } // namespace blender::io::stl diff --git a/source/blender/io/stl/exporter/stl_export.hh b/source/blender/io/stl/exporter/stl_export.hh index cb342cb42eb..c4db83cd0c9 100644 --- a/source/blender/io/stl/exporter/stl_export.hh +++ b/source/blender/io/stl/exporter/stl_export.hh @@ -10,7 +10,9 @@ namespace blender::io::stl { -/* Main export function used from within Blender. */ void exporter_main(bContext *C, const STLExportParams &export_params); +void export_frame(Depsgraph *depsgraph, + float scene_unit_scale, + const STLExportParams &export_params); } // namespace blender::io::stl diff --git a/source/blender/io/stl/tests/stl_exporter_tests.cc b/source/blender/io/stl/tests/stl_exporter_tests.cc new file mode 100644 index 00000000000..0bd616b9570 --- /dev/null +++ b/source/blender/io/stl/tests/stl_exporter_tests.cc @@ -0,0 +1,132 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: Apache-2.0 */ + +#include "tests/blendfile_loading_base_test.h" + +#include "BKE_appdir.h" +#include "BKE_main.h" + +#include "BLI_fileops.h" +#include "BLI_string.h" + +#include "BLO_readfile.h" + +#include "DEG_depsgraph.hh" + +#include "IO_stl.hh" +#include "stl_export.hh" + +namespace blender::io::stl { + +/* Set this true to keep comparison-failing test output in temp file directory. */ +constexpr bool save_failing_test_output = false; + +static std::string read_temp_file_in_string(const std::string &file_path) +{ + std::string res; + size_t buffer_len; + void *buffer = BLI_file_read_text_as_mem(file_path.c_str(), 0, &buffer_len); + if (buffer != nullptr) { + res.assign((const char *)buffer, buffer_len); + MEM_freeN(buffer); + } + return res; +} + +class stl_export_test : public BlendfileLoadingBaseTest { + public: + bool load_file_and_depsgraph(const std::string &filepath) + { + if (!blendfile_load(filepath.c_str())) { + return false; + } + depsgraph_create(DAG_EVAL_VIEWPORT); + return true; + } + + protected: + stl_export_test() + { + _params = {}; + _params.forward_axis = IO_AXIS_Y; + _params.up_axis = IO_AXIS_Z; + _params.global_scale = 1.0f; + _params.apply_modifiers = true; + _params.ascii_format = true; + } + void SetUp() override + { + BlendfileLoadingBaseTest::SetUp(); + BKE_tempdir_init(""); + } + + void TearDown() override + { + BlendfileLoadingBaseTest::TearDown(); + BKE_tempdir_session_purge(); + } + + static std::string get_temp_filename(const std::string &filename) + { + return std::string(BKE_tempdir_base()) + SEP_STR + filename; + } + + /** + * Export the given blend file with the given parameters and + * test to see if it matches a golden file (ignoring any difference in Blender version number). + * \param blendfile: input, relative to "tests" directory. + * \param golden_obj: expected output, relative to "tests" directory. + * \param params: the parameters to be used for export. + */ + void compare_to_golden(const std::string &blendfile, const std::string &golden_stl) + { + if (!load_file_and_depsgraph(blendfile)) { + return; + } + + std::string out_file_path = get_temp_filename(BLI_path_basename(golden_stl.c_str())); + STRNCPY(_params.filepath, out_file_path.c_str()); + std::string golden_file_path = blender::tests::flags_test_asset_dir() + SEP_STR + golden_stl; + export_frame(depsgraph, 1.0f, _params); + std::string output_str = read_temp_file_in_string(out_file_path); + + std::string golden_str = read_temp_file_in_string(golden_file_path); + bool are_equal = output_str == golden_str; + if (save_failing_test_output && !are_equal) { + printf("failing test output in %s\n", out_file_path.c_str()); + } + ASSERT_TRUE(are_equal); + if (!save_failing_test_output || are_equal) { + BLI_delete(out_file_path.c_str(), false, false); + } + } + + STLExportParams _params; +}; + +TEST_F(stl_export_test, all_tris) +{ + compare_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "all_tris.blend", + "io_tests" SEP_STR "stl" SEP_STR "all_tris.stl"); +} + +TEST_F(stl_export_test, all_quads) +{ + compare_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "all_quads.blend", + "io_tests" SEP_STR "stl" SEP_STR "all_quads.stl"); +} + +TEST_F(stl_export_test, non_uniform_scale) +{ + compare_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "non_uniform_scale.blend", + "io_tests" SEP_STR "stl" SEP_STR "non_uniform_scale.stl"); +} + +TEST_F(stl_export_test, cubes_positioned) +{ + compare_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR "cubes_positioned.blend", + "io_tests" SEP_STR "stl" SEP_STR "cubes_positioned.stl"); +} + +} // namespace blender::io::stl diff --git a/source/blender/io/stl/tests/stl_importer_tests.cc b/source/blender/io/stl/tests/stl_importer_tests.cc new file mode 100644 index 00000000000..386d7b6f830 --- /dev/null +++ b/source/blender/io/stl/tests/stl_importer_tests.cc @@ -0,0 +1,127 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: Apache-2.0 */ + +#include "tests/blendfile_loading_base_test.h" + +#include "BKE_mesh.hh" +#include "BKE_object.hh" + +#include "BLI_math_vector_types.hh" +#include "BLI_string.h" + +#include "BLO_readfile.h" + +#include "DEG_depsgraph_query.hh" + +#include "stl_import.hh" + +namespace blender::io::stl { + +struct Expectation { + int totvert, totedge, faces_num, totloop; + float3 vert_first, vert_last; +}; + +class stl_importer_test : public BlendfileLoadingBaseTest { + public: + stl_importer_test() + { + params.forward_axis = IO_AXIS_NEGATIVE_Z; + params.up_axis = IO_AXIS_Y; + params.use_facet_normal = false; + params.use_scene_unit = false; + params.global_scale = 1.0f; + params.use_mesh_validate = true; + } + + void import_and_check(const char *path, const Expectation &expect) + { + if (!blendfile_load("io_tests" SEP_STR "blend_geometry" SEP_STR "all_quads.blend")) { + ADD_FAILURE(); + return; + } + + std::string stl_path = blender::tests::flags_test_asset_dir() + + SEP_STR "io_tests" SEP_STR "stl" SEP_STR + path; + STRNCPY(params.filepath, stl_path.c_str()); + importer_main(bfile->main, bfile->curscene, bfile->cur_view_layer, params); + + depsgraph_create(DAG_EVAL_VIEWPORT); + + 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; + + constexpr bool print_result_scene = false; + if (print_result_scene) { + printf("Result was:\n"); + DEG_OBJECT_ITER_BEGIN (°_iter_settings, object) { + printf(" {"); + if (object->type == OB_MESH) { + Mesh *mesh = BKE_object_get_evaluated_mesh(object); + const Span positions = mesh->vert_positions(); + printf("%i, %i, %i, %i, float3(%g, %g, %g), float3(%g, %g, %g)", + mesh->totvert, + mesh->totedge, + mesh->faces_num, + mesh->totloop, + positions.first().x, + positions.first().y, + positions.first().z, + positions.last().x, + positions.last().y, + positions.last().z); + } + printf("},\n"); + } + DEG_OBJECT_ITER_END; + } + + size_t object_index = 0; + DEG_OBJECT_ITER_BEGIN (°_iter_settings, object) { + ++object_index; + /* First object is from loaded scene. */ + if (object_index == 1) { + continue; + } + EXPECT_V3_NEAR(object->loc, float3(0, 0, 0), 0.0001f); + EXPECT_V3_NEAR(object->rot, float3(M_PI_2, 0, 0), 0.0001f); + EXPECT_V3_NEAR(object->scale, float3(1, 1, 1), 0.0001f); + Mesh *mesh = BKE_object_get_evaluated_mesh(object); + EXPECT_EQ(mesh->totvert, expect.totvert); + EXPECT_EQ(mesh->totedge, expect.totedge); + EXPECT_EQ(mesh->faces_num, expect.faces_num); + EXPECT_EQ(mesh->totloop, expect.totloop); + const Span positions = mesh->vert_positions(); + EXPECT_V3_NEAR(positions.first(), expect.vert_first, 0.0001f); + EXPECT_V3_NEAR(positions.last(), expect.vert_last, 0.0001f); + break; + } + DEG_OBJECT_ITER_END; + } + + STLImportParams params; +}; + +TEST_F(stl_importer_test, all_quads) +{ + Expectation expect = {8, 18, 12, 36, float3(1, 1, 1), float3(1, -1, 1)}; + import_and_check("all_quads.stl", expect); +} + +TEST_F(stl_importer_test, cubes_positioned) +{ + Expectation expect = {24, 54, 36, 108, float3(1, 1, 1), float3(5.49635f, 0.228398f, -1.11237f)}; + import_and_check("cubes_positioned.stl", expect); +} + +TEST_F(stl_importer_test, non_uniform_scale) +{ + Expectation expect = {140, 378, 252, 756, float3(0, 0, -0.3f), float3(-0.866025f, -1.5f, 0)}; + import_and_check("non_uniform_scale.stl", expect); +} + +} // namespace blender::io::stl diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 1c28613ea43..955524dbabd 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -426,66 +426,6 @@ add_blender_test( # ------------------------------------------------------------------------------ # IO TESTS -# STL Import tests -# disabled until updated & working -if(FALSE) -add_blender_test( - import_stl_cube - --python ${CMAKE_CURRENT_LIST_DIR}/bl_test.py -- - --run={'FINISHED'}&bpy.ops.import_mesh.stl\(filepath='${TEST_SRC_DIR}/io_tests/stl/cube.stl'\) - --md5=8ceb5bb7e1cb5f4342fa1669988c66b4 --md5_method=SCENE - --write-blend=${TEST_OUT_DIR}/io_tests/import_stl_cube.blend -) - -add_blender_test( - import_stl_conrod - --python ${CMAKE_CURRENT_LIST_DIR}/bl_test.py -- - --run={'FINISHED'}&bpy.ops.import_mesh.stl\(filepath='${TEST_SRC_DIR}/io_tests/stl/conrod.stl'\) - --md5=690a4b8eb9002dcd8631c5a575ea7348 --md5_method=SCENE - --write-blend=${TEST_OUT_DIR}/io_tests/import_stl_conrod.blend -) - -add_blender_test( - import_stl_knot_max_simplified - --python ${CMAKE_CURRENT_LIST_DIR}/bl_test.py -- - --run={'FINISHED'}&bpy.ops.import_mesh.stl\(filepath='${TEST_SRC_DIR}/io_tests/stl/knot_max_simplified.stl'\) - --md5=baf82803f45a84ec4ddbad9cef57dd3e --md5_method=SCENE - --write-blend=${TEST_OUT_DIR}/io_tests/import_stl_knot_max_simplified.blend -) -endif() - -# STL Export -# disabled until updated & working -if(FALSE) -add_blender_test( - export_stl_cube_all_data - ${TEST_SRC_DIR}/io_tests/blend_geometry/cube_all_data.blend - --python ${CMAKE_CURRENT_LIST_DIR}/bl_test.py -- - --run={'FINISHED'}&bpy.ops.export_mesh.stl\(filepath='${TEST_OUT_DIR}/io_tests/export_stl_cube_all_data.stl'\) - --md5_source=${TEST_OUT_DIR}/io_tests/export_stl_cube_all_data.stl - --md5=64cb97c0cabb015e1c3f76369835075a --md5_method=FILE -) - -add_blender_test( - export_stl_suzanne_all_data - ${TEST_SRC_DIR}/io_tests/blend_geometry/suzanne_all_data.blend - --python ${CMAKE_CURRENT_LIST_DIR}/bl_test.py -- - --run={'FINISHED'}&bpy.ops.export_mesh.stl\(filepath='${TEST_OUT_DIR}/io_tests/export_stl_suzanne_all_data.stl'\) - --md5_source=${TEST_OUT_DIR}/io_tests/export_stl_suzanne_all_data.stl - --md5=e9b23c97c139ad64961c635105bb9192 --md5_method=FILE -) - -add_blender_test( - export_stl_vertices # lame, add a better one - ${TEST_SRC_DIR}/io_tests/blend_geometry/vertices.blend - --python ${CMAKE_CURRENT_LIST_DIR}/bl_test.py -- - --run={'FINISHED'}&bpy.ops.export_mesh.stl\(filepath='${TEST_OUT_DIR}/io_tests/export_stl_vertices.stl'\) - --md5_source=${TEST_OUT_DIR}/io_tests/export_stl_vertices.stl - --md5=3fd3c877e573beeebc782532cc005820 --md5_method=FILE -) -endif() - - # X3D Import # disabled until updated & working if(FALSE)