diff --git a/source/blender/io/ply/tests/io_ply_importer_test.cc b/source/blender/io/ply/tests/io_ply_importer_test.cc index aafd3c765b3..6807ca4e0b4 100644 --- a/source/blender/io/ply/tests/io_ply_importer_test.cc +++ b/source/blender/io/ply/tests/io_ply_importer_test.cc @@ -1,11 +1,10 @@ -/* SPDX-FileCopyrightText: 2023 Blender Authors +/* SPDX-FileCopyrightText: 2023-2025 Blender Authors * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "testing/testing.h" -#include "BLI_fileops.hh" -#include "BLI_hash_mm2a.hh" +#include "BLI_path_utils.hh" #include "ply_import.hh" #include "ply_import_buffer.hh" @@ -13,291 +12,61 @@ namespace blender::io::ply { -struct Expectation { - int totvert, faces_num, totindex, totedge; - uint16_t polyhash = 0, edgehash = 0; - float3 vert_first, vert_last; - float3 normal_first = {0, 0, 0}; - float2 uv_first = {0, 0}; - float4 color_first = {-1, -1, -1, -1}; -}; +/* Extensive tests for PLY importing are in `io_ply_import_test.py`. + * The tests here are only for testing PLY reader buffer refill behavior, + * by using a very small buffer size on purpose. */ -class PLYImportTest : public testing::Test { - public: - void import_and_check(const char *path, const Expectation &exp) - { - std::string ply_path = blender::tests::flags_test_asset_dir() + - SEP_STR "io_tests" SEP_STR "ply" SEP_STR + path; +TEST(ply_import, BufferRefillTest) +{ + std::string ply_path_a = blender::tests::flags_test_asset_dir() + + SEP_STR "io_tests" SEP_STR "ply" SEP_STR + "ASCII_wireframe_cube.ply"; + std::string ply_path_b = blender::tests::flags_test_asset_dir() + + SEP_STR "io_tests" SEP_STR "ply" SEP_STR + "wireframe_cube.ply"; - /* Use a small read buffer size for better coverage of buffer refilling behavior. */ - PlyReadBuffer infile(ply_path.c_str(), 128); - PlyHeader header; - const char *header_err = read_header(infile, header); - if (header_err != nullptr) { - ADD_FAILURE(); - return; - } - std::unique_ptr data = import_ply_data(infile, header); - if (!data->error.empty()) { - fprintf(stderr, "%s\n", data->error.c_str()); - ASSERT_EQ(0, exp.totvert); - ASSERT_EQ(0, exp.faces_num); - return; - } - - /* Test expected amount of vertices, edges, and faces. */ - ASSERT_EQ(data->vertices.size(), exp.totvert); - ASSERT_EQ(data->edges.size(), exp.totedge); - ASSERT_EQ(data->face_sizes.size(), exp.faces_num); - ASSERT_EQ(data->face_vertices.size(), exp.totindex); - - /* Test hash of face and edge index data. */ - BLI_HashMurmur2A hash; - BLI_hash_mm2a_init(&hash, 0); - uint32_t offset = 0; - for (uint32_t face_size : data->face_sizes) { - BLI_hash_mm2a_add(&hash, (const uchar *)&data->face_vertices[offset], face_size * 4); - offset += face_size; - } - uint16_t face_hash = BLI_hash_mm2a_end(&hash); - if (!data->face_vertices.is_empty()) { - ASSERT_EQ(face_hash, exp.polyhash); - } - - if (!data->edges.is_empty()) { - uint16_t edge_hash = BLI_hash_mm2( - (const uchar *)data->edges.data(), data->edges.size() * sizeof(data->edges[0]), 0); - ASSERT_EQ(edge_hash, exp.edgehash); - } - - /* Test if first and last vertices match. */ - EXPECT_V3_NEAR(data->vertices.first(), exp.vert_first, 0.0001f); - EXPECT_V3_NEAR(data->vertices.last(), exp.vert_last, 0.0001f); - - /* Check if first normal matches. */ - float3 got_normal = data->vertex_normals.is_empty() ? float3(0, 0, 0) : - data->vertex_normals.first(); - EXPECT_V3_NEAR(got_normal, exp.normal_first, 0.0001f); - - /* Check if first UV matches. */ - float2 got_uv = data->uv_coordinates.is_empty() ? float2(0, 0) : data->uv_coordinates.first(); - EXPECT_V2_NEAR(got_uv, exp.uv_first, 0.0001f); - - /* Check if first color matches. */ - float4 got_color = data->vertex_colors.is_empty() ? float4(-1, -1, -1, -1) : - data->vertex_colors.first(); - EXPECT_V4_NEAR(got_color, exp.color_first, 0.0001f); + /* Use a small read buffer size to test buffer refilling behavior. */ + constexpr size_t buffer_size = 50; + PlyReadBuffer infile_a(ply_path_a.c_str(), buffer_size); + PlyReadBuffer infile_b(ply_path_b.c_str(), buffer_size); + PlyHeader header_a, header_b; + const char *header_err_a = read_header(infile_a, header_a); + const char *header_err_b = read_header(infile_b, header_b); + if (header_err_a != nullptr || header_err_b != nullptr) { + fprintf(stderr, "Failed to read PLY header\n"); + ADD_FAILURE(); + return; + } + std::unique_ptr data_a = import_ply_data(infile_a, header_a); + std::unique_ptr data_b = import_ply_data(infile_b, header_b); + if (!data_a->error.empty() || !data_b->error.empty()) { + fprintf(stderr, "Failed to read PLY data\n"); + ADD_FAILURE(); + return; } -}; -TEST_F(PLYImportTest, PLYImportCube) -{ - Expectation expect = {24, - 6, - 24, - 0, - 26429, - 0, - float3(1, 1, -1), - float3(-1, 1, 1), - float3(0, 0, -1), - float2(0.979336, 0.844958), - float4(1, 0.8470, 0, 1)}; - import_and_check("cube_ascii.ply", expect); -} - -TEST_F(PLYImportTest, PLYImportWireframeCube) -{ - Expectation expect = {8, 0, 0, 12, 0, 31435, float3(-1, -1, -1), float3(1, 1, 1)}; - import_and_check("ASCII_wireframe_cube.ply", expect); - import_and_check("wireframe_cube.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportBinaryDataStartsWithLF) -{ - Expectation expect = {4, 1, 4, 0, 37235, 0, float3(-1, -1, 0), float3(-1, 1, 0)}; - import_and_check("bin_data_starts_with_lf.ply", expect); - import_and_check("bin_data_starts_with_lf_header_crlf.ply", expect); -} - -TEST_F(PLYImportTest, PLYImportBunny) -{ - Expectation expect = {1623, - 1000, - 3000, - 0, - 62556, - 0, - float3(0.0380425, 0.109755, 0.0161689), - float3(-0.0722821, 0.143895, -0.0129091)}; - import_and_check("bunny2.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportManySmallHoles) -{ - Expectation expect = {2004, - 3524, - 10572, - 0, - 15143, - 0, - float3(-0.0131592, -0.0598382, 1.58958), - float3(-0.0177622, 0.0105153, 1.61977), - float3(0, 0, 0), - float2(0, 0), - float4(0.7215, 0.6784, 0.6627, 1)}; - import_and_check("many_small_holes.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportColorNotFull) -{ - Expectation expect = {4, 1, 4, 0, 37235, 0, float3(1, 0, 1), float3(-1, 0, 1)}; - import_and_check("color_not_full_a.ply", expect); - import_and_check("color_not_full_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportCustomDataElements) -{ - Expectation expect = {600, - 0, - 0, - 0, - 0, - 0, - float3(-0.78193f, 0.40659f, -1), - float3(-0.75537f, 1, -0.24777f), - float3(0, 0, 0), - float2(0, 0), - float4(0.31373f, 0, 0, 1)}; - import_and_check("custom_data_elements.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportDoubleXYZ) -{ - Expectation expect = {4, - 1, - 4, - 0, - 37235, - 0, - float3(1, 0, 1), - float3(-1, 0, 1), - float3(0, 0, 0), - float2(0, 0), - float4(1, 0, 0, 1)}; - import_and_check("double_xyz_a.ply", expect); - import_and_check("double_xyz_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportFaceIndicesNotFirstProp) -{ - Expectation expect = {4, 2, 6, 0, 4136, 0, float3(1, 0, 1), float3(-1, 0, 1)}; - import_and_check("face_indices_not_first_prop_a.ply", expect); - import_and_check("face_indices_not_first_prop_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportFaceIndicesPrecededByList) -{ - Expectation expect = {4, 2, 6, 0, 4136, 0, float3(1, 0, 1), float3(-1, 0, 1)}; - import_and_check("face_indices_preceded_by_list_a.ply", expect); - import_and_check("face_indices_preceded_by_list_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportFaceUVsColors) -{ - Expectation expect = {4, 1, 4, 0, 37235, 0, float3(1, 0, 1), float3(-1, 0, 1)}; - import_and_check("face_uvs_colors_a.ply", expect); - import_and_check("face_uvs_colors_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportFacesFirst) -{ - Expectation expect = {4, - 1, - 4, - 0, - 37235, - 0, - float3(1, 0, 1), - float3(-1, 0, 1), - float3(0, 0, 0), - float2(0, 0), - float4(1, 0, 0, 1)}; - import_and_check("faces_first_a.ply", expect); - import_and_check("faces_first_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportFloatFormats) -{ - Expectation expect = {4, - 1, - 4, - 0, - 37235, - 0, - float3(1, 0, 1), - float3(-1, 0, 1), - float3(0, 0, 0), - float2(0, 0), - float4(0.5f, 0, 0.25f, 1)}; - import_and_check("float_formats_a.ply", expect); - import_and_check("float_formats_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportPositionNotFull) -{ - Expectation expect = {0, 0, 0, 0}; - import_and_check("position_not_full_a.ply", expect); - import_and_check("position_not_full_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportTristrips) -{ - Expectation expect = {6, 4, 12, 0, 3404, 0, float3(1, 0, 1), float3(-3, 0, 1)}; - import_and_check("tristrips_a.ply", expect); - import_and_check("tristrips_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportTypeAliases) -{ - Expectation expect = {4, - 1, - 4, - 0, - 37235, - 0, - float3(1, 0, 1), - float3(-1, 0, 1), - float3(0, 0, 0), - float2(0, 0), - float4(220 / 255.0f, 20 / 255.0f, 20 / 255.0f, 1)}; - import_and_check("type_aliases_a.ply", expect); - import_and_check("type_aliases_b.ply", expect); - import_and_check("type_aliases_be_b.ply", expect); -} - -TEST_F(PLYImportTest, PlyImportVertexCompOrder) -{ - Expectation expect = {4, - 1, - 4, - 0, - 37235, - 0, - float3(1, 0, 1), - float3(-1, 0, 1), - float3(0, 0, 0), - float2(0, 0), - float4(0.8f, 0.2f, 0, 1)}; - import_and_check("vertex_comp_order_a.ply", expect); - import_and_check("vertex_comp_order_b.ply", expect); + /* Check whether the edges list matches expectations. */ + std::pair exp_edges[] = {{2, 0}, + {0, 1}, + {1, 3}, + {3, 2}, + {6, 2}, + {3, 7}, + {7, 6}, + {4, 6}, + {7, 5}, + {5, 4}, + {0, 4}, + {5, 1}}; + EXPECT_EQ(12, data_a->edges.size()); + EXPECT_EQ(12, data_b->edges.size()); + EXPECT_EQ_ARRAY(exp_edges, data_a->edges.data(), 12); + EXPECT_EQ_ARRAY(exp_edges, data_b->edges.data(), 12); } +//@TODO: now we put vertex color attribute first, maybe put position first? //@TODO: test with vertex element having list properties //@TODO: test with edges starting with non-vertex index properties //@TODO: test various malformed headers //@TODO: UVs with: s,t; u,v; texture_u,texture_v; texture_s,texture_t (from miniply) //@TODO: colors with: r,g,b in addition to red,green,blue (from miniply) -//@TODO: importing bunny2 with old importer results in smooth shading; flat shading with new one } // namespace blender::io::ply diff --git a/source/blender/io/stl/CMakeLists.txt b/source/blender/io/stl/CMakeLists.txt index 4ded38a5ed0..edca6808ea3 100644 --- a/source/blender/io/stl/CMakeLists.txt +++ b/source/blender/io/stl/CMakeLists.txt @@ -54,7 +54,6 @@ 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 diff --git a/source/blender/io/stl/tests/stl_importer_tests.cc b/source/blender/io/stl/tests/stl_importer_tests.cc deleted file mode 100644 index 2182f7ae5cc..00000000000 --- a/source/blender/io/stl/tests/stl_importer_tests.cc +++ /dev/null @@ -1,124 +0,0 @@ -/* 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_base.h" -#include "BLI_math_vector_types.hh" -#include "BLI_string.h" - -#include "BLO_readfile.hh" - -#include "DEG_depsgraph_query.hh" - -#include "stl_import.hh" - -namespace blender::io::stl { - -struct Expectation { - int verts_num, edges_num, faces_num, corners_num; - 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; - } - - 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->verts_num, - mesh->edges_num, - mesh->faces_num, - mesh->corners_num, - 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->verts_num, expect.verts_num); - EXPECT_EQ(mesh->edges_num, expect.edges_num); - EXPECT_EQ(mesh->faces_num, expect.faces_num); - EXPECT_EQ(mesh->corners_num, expect.corners_num); - 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/source/blender/io/wavefront_obj/tests/obj_importer_tests.cc b/source/blender/io/wavefront_obj/tests/obj_importer_tests.cc index 5d62d1cda0c..3ab916735b5 100644 --- a/source/blender/io/wavefront_obj/tests/obj_importer_tests.cc +++ b/source/blender/io/wavefront_obj/tests/obj_importer_tests.cc @@ -1,943 +1,49 @@ -/* SPDX-FileCopyrightText: 2023 Blender Authors +/* SPDX-FileCopyrightText: 2023-2025 Blender Authors * * SPDX-License-Identifier: Apache-2.0 */ #include #include "testing/testing.h" -#include "tests/blendfile_loading_base_test.h" -#include "BKE_curve.hh" -#include "BKE_customdata.hh" -#include "BKE_main.hh" -#include "BKE_material.hh" -#include "BKE_mesh.hh" -#include "BKE_object.hh" -#include "BKE_scene.hh" - -#include "BLI_listbase.h" -#include "BLI_math_base.h" -#include "BLI_math_vector_types.hh" #include "BLI_string.h" -#include "BLO_readfile.hh" - -#include "DEG_depsgraph.hh" -#include "DEG_depsgraph_query.hh" - -#include "DNA_curve_types.h" -#include "DNA_material_types.h" -#include "DNA_mesh_types.h" - -#include "MEM_guardedalloc.h" +#include "CLG_log.h" +#include "obj_import_file_reader.hh" #include "obj_importer.hh" namespace blender::io::obj { -struct Expectation { - std::string name; - short type; /* OB_MESH, ... */ - int totvert, mesh_edges_num_or_curve_endp, mesh_faces_num_or_curve_order, - mesh_corner_num_or_curve_cyclic; - float3 vert_first, vert_last; - float3 normal_first; - float2 uv_first; - float4 color_first = {-1, -1, -1, -1}; - std::string first_mat; -}; +/* Extensive tests for OBJ importing are in `io_obj_import_test.py`. + * The tests here are only for testing OBJ reader buffer refill behavior, + * by using a very small buffer size on purpose. */ -class OBJImportTest : public BlendfileLoadingBaseTest { - public: - void import_and_check(const char *path, - const Expectation *expect, - size_t expect_count, - int expect_mat_count, - int expect_image_count = 0) - { - if (!blendfile_load("io_tests" SEP_STR "blend_geometry" SEP_STR "all_quads.blend")) { - ADD_FAILURE(); - return; - } - - std::string obj_path = blender::tests::flags_test_asset_dir() + - SEP_STR "io_tests" SEP_STR "obj" SEP_STR + path; - STRNCPY(params.filepath, obj_path.c_str()); - const size_t read_buffer_size = 650; - importer_main(bfile->main, bfile->curscene, bfile->cur_view_layer, params, read_buffer_size); - - 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(" {\"%s\", ", object->id.name); - if (object->type == OB_MESH) { - Mesh *mesh = BKE_object_get_evaluated_mesh(object); - const Span positions = mesh->vert_positions(); - printf("OB_MESH, %i, %i, %i, %i, float3(%g, %g, %g), float3(%g, %g, %g)", - mesh->verts_num, - mesh->edges_num, - mesh->faces_num, - mesh->corners_num, - 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) { - if (object_index >= expect_count) { - ADD_FAILURE(); - break; - } - const Expectation &exp = expect[object_index]; - ASSERT_STREQ(object->id.name, exp.name.c_str()); - EXPECT_EQ(object->type, exp.type); - EXPECT_V3_NEAR(object->loc, float3(0, 0, 0), 0.0001f); - if (!STREQ(object->id.name, "OBCube")) { - EXPECT_V3_NEAR(object->rot, float3(M_PI_2, 0, 0), 0.0001f); - } - EXPECT_V3_NEAR(object->scale, float3(1, 1, 1), 0.0001f); - if (object->type == OB_MESH) { - Mesh *mesh = BKE_object_get_evaluated_mesh(object); - EXPECT_EQ(mesh->verts_num, exp.totvert); - EXPECT_EQ(mesh->edges_num, exp.mesh_edges_num_or_curve_endp); - EXPECT_EQ(mesh->faces_num, exp.mesh_faces_num_or_curve_order); - EXPECT_EQ(mesh->corners_num, exp.mesh_corner_num_or_curve_cyclic); - const Span positions = mesh->vert_positions(); - EXPECT_V3_NEAR(positions.first(), exp.vert_first, 0.0001f); - EXPECT_V3_NEAR(positions.last(), exp.vert_last, 0.0001f); - const float3 *corner_normals = mesh->normals_domain() == bke::MeshNormalDomain::Corner ? - mesh->corner_normals().data() : - nullptr; - float3 normal_first = corner_normals != nullptr ? corner_normals[0] : float3(0, 0, 0); - EXPECT_V3_NEAR(normal_first, exp.normal_first, 0.0001f); - const float2 *mloopuv = static_cast( - CustomData_get_layer(&mesh->corner_data, CD_PROP_FLOAT2)); - float2 uv_first = mloopuv ? *mloopuv : float2(0, 0); - EXPECT_V2_NEAR(uv_first, exp.uv_first, 0.0001f); - if (exp.color_first.x >= 0) { - const float4 *colors = (const float4 *)CustomData_get_layer(&mesh->vert_data, - CD_PROP_COLOR); - EXPECT_TRUE(colors != nullptr); - EXPECT_V4_NEAR(colors[0], exp.color_first, 0.0001f); - } - else { - EXPECT_FALSE(CustomData_has_layer(&mesh->vert_data, CD_PROP_COLOR)); - } - } - if (object->type == OB_CURVES_LEGACY) { - Curve *curve = static_cast(DEG_get_evaluated_object(depsgraph, object)->data); - int numVerts; - float(*vertexCos)[3] = BKE_curve_nurbs_vert_coords_alloc(&curve->nurb, &numVerts); - EXPECT_EQ(numVerts, exp.totvert); - EXPECT_V3_NEAR(vertexCos[0], exp.vert_first, 0.0001f); - EXPECT_V3_NEAR(vertexCos[numVerts - 1], exp.vert_last, 0.0001f); - MEM_freeN(vertexCos); - const Nurb *nurb = static_cast(BLI_findlink(&curve->nurb, 0)); - int endpoint = (nurb->flagu & CU_NURB_ENDPOINT) ? 1 : 0; - EXPECT_EQ(nurb->orderu, exp.mesh_faces_num_or_curve_order); - EXPECT_EQ(endpoint, exp.mesh_edges_num_or_curve_endp); - int cyclic = (nurb->flagu & CU_NURB_CYCLIC) ? 1 : 0; - EXPECT_EQ(cyclic, exp.mesh_corner_num_or_curve_cyclic); - } - if (!exp.first_mat.empty()) { - Material *mat = BKE_object_material_get(object, 1); - ASSERT_STREQ(mat ? mat->id.name : "", exp.first_mat.c_str()); - } - ++object_index; - } - DEG_OBJECT_ITER_END; - EXPECT_EQ(object_index, expect_count); - - /* Check number of materials & textures. */ - const int mat_count = BLI_listbase_count(&bfile->main->materials); - EXPECT_EQ(mat_count, expect_mat_count); - - const int ima_count = BLI_listbase_count(&bfile->main->images); - EXPECT_EQ(ima_count, expect_image_count); - } +TEST(obj_import, BufferRefillTest) +{ + CLG_init(); OBJImportParams params; -}; + /* nurbs_cyclic.obj file has quite long lines, good to test read buffer refill. */ + std::string obj_path = blender::tests::flags_test_asset_dir() + + SEP_STR "io_tests" SEP_STR "obj" SEP_STR + "nurbs_cyclic.obj"; + STRNCPY(params.filepath, obj_path.c_str()); -TEST_F(OBJImportTest, import_cube) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBcube", - OB_MESH, - 8, - 12, - 6, - 24, - float3(-1, -1, 1), - float3(1, -1, -1), - float3(-0.57758f, 0.57735f, -0.57711f)}, - }; - import_and_check("cube.obj", expect, std::size(expect), 1); -} + /* Use a small read buffer size to test buffer refilling behavior. */ + const size_t read_buffer_size = 650; + OBJParser obj_parser{params, read_buffer_size}; -TEST_F(OBJImportTest, import_cube_o_after_verts) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - { - "OBActualCube", - OB_MESH, - 8, - 12, - 6, - 24, - float3(-1, -1, 1), - float3(1, -1, -1), - float3(0.57735f, -0.57735f, 0.57735f), - }, - { - "OBSparseTri", - OB_MESH, - 3, - 3, - 1, - 3, - float3(1, -1, 1), - float3(-2, -2, 2), - float3(-0.2357f, 0.9428f, 0.2357f), - }, - }; - import_and_check("cube_o_after_verts.obj", expect, std::size(expect), 2); -} + Vector> all_geometries; + GlobalVertices global_vertices; + obj_parser.parse(all_geometries, global_vertices); -TEST_F(OBJImportTest, import_suzanne_all_data) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBMonkey", - OB_MESH, - 505, - 1005, - 500, - 1968, - float3(-0.4375f, 0.164062f, 0.765625f), - float3(0.4375f, 0.164062f, 0.765625f), - float3(-0.6040f, -0.5102f, 0.6122f), - float2(0.692094f, 0.40191f)}, - }; - import_and_check("suzanne_all_data.obj", expect, std::size(expect), 0); -} + EXPECT_EQ(1, all_geometries.size()); + EXPECT_EQ(GEOM_CURVE, all_geometries[0]->geom_type_); + EXPECT_EQ(28, global_vertices.vertices.size()); + EXPECT_EQ(31, all_geometries[0]->nurbs_element_.curv_indices.size()); + EXPECT_EQ(35, all_geometries[0]->nurbs_element_.parm.size()); -TEST_F(OBJImportTest, import_nurbs) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBnurbs", - OB_CURVES_LEGACY, - 9, - 0, - 4, - 1, - float3(1.149067f, 0.964181f, -0.866025f), - float3(-1.5f, 2.598076f, 0)}, - }; - import_and_check("nurbs.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_nurbs_curves) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBCurveDeg3", OB_CURVES_LEGACY, 4, 0, 3, 0, float3(10, -2, 0), float3(6, -2, 0)}, - {"OBnurbs_curves", OB_CURVES_LEGACY, 4, 0, 4, 0, float3(2, -2, 0), float3(-2, -2, 0)}, - {"OBNurbsCurveCyclic", OB_CURVES_LEGACY, 4, 0, 4, 1, float3(-6, -2, -0), float3(-6, 2, 0)}, - {"OBNurbsCurveDiffWeights", - OB_CURVES_LEGACY, - 4, - 0, - 4, - 0, - float3(6, -2, 0), - float3(2, -2, 0)}, - {"OBNurbsCurveEndpoint", - OB_CURVES_LEGACY, - 4, - 1, - 4, - 0, - float3(-6, -2, 0), - float3(-10, -2, 0)}, - }; - import_and_check("nurbs_curves.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_nurbs_cyclic) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBnurbs_cyclic", - OB_CURVES_LEGACY, - 28, - 0, - 4, - 1, - float3(0.935235f, -0.000000f, 3.518242f), - float3(3.280729f, 0, 3.043217f)}, - }; - import_and_check("nurbs_cyclic.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_nurbs_endpoint) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBCurveEndpointRange01", - OB_CURVES_LEGACY, - 15, - 1, - 4, - 0, - float3(0.29f, 0, -0.11f), - float3(22.17f, 0, -5.31f)}, - {"OBCurveEndpointRangeNon01", - OB_CURVES_LEGACY, - 15, - 1, - 4, - 0, - float3(0.29f, 0, -0.11f), - float3(22.17f, 0, -5.31f)}, - {"OBCurveNoEndpointRange01", - OB_CURVES_LEGACY, - 15, - 0, - 4, - 0, - float3(0.29f, 0, -0.11f), - float3(22.17f, 0, -5.31f)}, - }; - import_and_check("nurbs_endpoint.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_nurbs_manual) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBCurve_Cyclic", OB_CURVES_LEGACY, 4, 0, 4, 1, float3(-2, 0, -2), float3(2, 0, -2)}, - {"OBCurve_Endpoints", OB_CURVES_LEGACY, 5, 1, 4, 0, float3(-2, 0, 2), float3(-2, 0, 2)}, - {"OBCurve_NonUniform_Parm", - OB_CURVES_LEGACY, - 5, - 0, - 4, - 0, - float3(-2, 0, 2), - float3(-2, 0, 2)}, - {"OBCurve_Uniform_Parm", OB_CURVES_LEGACY, 5, 0, 4, 0, float3(-2, 0, 2), float3(-2, 0, 2)}, - }; - import_and_check("nurbs_manual.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_nurbs_mesh) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBTorus_Knot", - OB_MESH, - 108, - 108, - 0, - 0, - float3(0.438725f, 1.070313f, 0.433013f), - float3(0.625557f, 1.040691f, 0.460328f)}, - }; - import_and_check("nurbs_mesh.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_materials) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBmaterials", - OB_MESH, - 8, - 12, - 6, - 24, - float3(-1, -1, 1), - float3(1, -1, -1), - float3(0), - float2(0), - float4(-1), - "MAno_textures_red"}, - {"OBObjMtlAfter", - OB_MESH, - 3, - 3, - 1, - 3, - float3(3, 0, 0), - float3(5, 0, 0), - float3(0), - float2(0), - float4(-1), - "MAno_textures_red"}, - {"OBObjMtlBefore", - OB_MESH, - 3, - 3, - 1, - 3, - float3(6, 0, 0), - float3(8, 0, 0), - float3(0), - float2(0), - float4(-1), - "MAClay"}, - }; - import_and_check("materials.obj", expect, std::size(expect), 4, 8); -} - -TEST_F(OBJImportTest, import_cubes_with_textures_rel) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBCube4Tex", - OB_MESH, - 8, - 12, - 6, - 24, - float3(1, 1, -1), - float3(-1, -1, 1), - float3(0, 1, 0), - float2(0.9935f, 0.0020f), - float4(-1), - "MAMat_BaseRoughEmissNormal10"}, - {"OBCubeTexMul", - OB_MESH, - 8, - 12, - 6, - 24, - float3(4, -2, -1), - float3(2, -4, 1), - float3(0, 1, 0), - float2(0.9935f, 0.0020f), - float4(-1), - "MAMat_BaseMul"}, - {"OBCubeTiledTex", - OB_MESH, - 8, - 12, - 6, - 24, - float3(4, 1, -1), - float3(2, -1, 1), - float3(0, 1, 0), - float2(0.9935f, 0.0020f), - float4(-1), - "MAMat_BaseTiled"}, - {"OBCubeTiledTexFromAnotherFolder", - OB_MESH, - 8, - 12, - 6, - 24, - float3(7, 1, -1), - float3(5, -1, 1), - float3(0, 1, 0), - float2(0.9935f, 0.0020f), - float4(-1), - "MAMat_EmissTiledAnotherFolder"}, - }; - import_and_check("cubes_with_textures_rel.obj", expect, std::size(expect), 4, 4); -} - -TEST_F(OBJImportTest, import_faces_invalid_or_with_holes) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBFaceAllVerts_BecomesOneOverlappingFaceUsingAllVerts", - OB_MESH, - 8, - 8, - 1, - 8, - float3(8, 0, -2), - float3(11, 0, -1)}, - {"OBFaceAllVertsDup_BecomesOneOverlappingFaceUsingAllVerts", - OB_MESH, - 8, - 8, - 1, - 8, - float3(3, 0, 3), - float3(6, 0, 4)}, - {"OBFaceJustTwoVerts_IsSkipped", OB_MESH, 2, 0, 0, 0, float3(8, 0, 3), float3(8, 0, 7)}, - {"OBFaceQuadDupSomeVerts_BecomesOneQuadUsing4Verts", - OB_MESH, - 4, - 4, - 1, - 4, - float3(3, 0, -2), - float3(7, 0, -2)}, - {"OBFaceTriDupVert_Becomes1Tri", OB_MESH, 3, 3, 1, 3, float3(-2, 0, 3), float3(2, 0, 7)}, - {"OBFaceWithHole_BecomesTwoFacesFormingAHole", - OB_MESH, - 8, - 10, - 2, - 12, - float3(-2, 0, -2), - float3(1, 0, -1)}, - }; - import_and_check("faces_invalid_or_with_holes.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_invalid_faces) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBTheMesh", OB_MESH, 5, 3, 1, 3, float3(-2, 0, -2), float3(0, 2, 0)}, - }; - import_and_check("invalid_faces.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_invalid_indices) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBQuad", - OB_MESH, - 3, - 3, - 1, - 3, - float3(-2, 0, -2), - float3(2, 0, 2), - float3(0, 1, 0), - float2(0.5f, 0.25f)}, - }; - import_and_check("invalid_indices.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_invalid_syntax) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBObjectWithAReallyLongNameToCheckHowImportHandlesNamesThatAreLon", - OB_MESH, - 3, - 3, - 1, - 3, - float3(1, 2, 3), - float3(7, 8, 9), - float3(0, 1, 0), - float2(0.5f, 0.25f)}, - }; - import_and_check("invalid_syntax.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_all_objects) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - /* `.obj` file has empty EmptyText and EmptyMesh objects; these are ignored and skipped. */ - {"OBBezierCurve", OB_MESH, 13, 12, 0, 0, float3(-1, -2, 0), float3(1, -2, 0)}, - {"OBBlankCube", OB_MESH, 8, 13, 7, 26, float3(1, 1, -1), float3(-1, 1, 1), float3(0, 0, 1)}, - {"OBMaterialCube", - OB_MESH, - 8, - 13, - 7, - 26, - float3(28, 1, -1), - float3(26, 1, 1), - float3(-1, 0, 0), - float2(0), - float4(-1), - "MARed"}, - {"OBNurbsCircle", - OB_MESH, - 96, - 96, - 0, - 0, - float3(3.292893f, -2.707107f, 0), - float3(3.369084f, -2.77607f, 0)}, - {"OBNurbsCircle.001", OB_MESH, 4, 4, 0, 0, float3(2, -3, 0), float3(3, -2, 0)}, - {"OBParticleCube", - OB_MESH, - 8, - 13, - 7, - 26, - float3(22, 1, -1), - float3(20, 1, 1), - float3(0, 0, 1)}, - {"OBShapeKeyCube", - OB_MESH, - 8, - 13, - 7, - 26, - float3(19, 1, -1), - float3(17, 1, 1), - float3(-0.4082f, -0.4082f, 0.8165f)}, - {"OBSmoothCube", - OB_MESH, - 8, - 13, - 7, - 26, - float3(4, 1, -1), - float3(2, 1, 1), - float3(0.5774f, 0.5773f, 0.5774f), - float2(0), - float4(-1), - "MAMaterial"}, - {"OBSurface", - OB_MESH, - 256, - 480, - 224, - 896, - float3(7.292893f, -2.707107f, -1), - float3(7.525872f, -2.883338f, 1), - float3(-0.7071f, -0.7071f, 0), - float2(0, 0.142857f)}, - {"OBSurfPatch", - OB_MESH, - 256, - 480, - 225, - 900, - float3(12.5f, -2.5f, 0.694444f), - float3(13.5f, -1.5f, 0.694444f), - float3(-0.3246f, -0.3531f, 0.8775f), - float2(0, 0.066667f)}, - {"OBSurfSphere", - OB_MESH, - 640, - 1248, - 608, - 2432, - float3(11, -2, -1), - float3(11, -2, 1), - float3(-0.0541f, -0.0541f, -0.9971f), - float2(0, 1)}, - {"OBSurfTorus.001", - OB_MESH, - 1024, - 2048, - 1024, - 4096, - float3(5.34467f, -2.65533f, -0.176777f), - float3(5.232792f, -2.411795f, -0.220835f), - float3(-0.5042f, -0.5042f, -0.7011f), - float2(0, 1)}, - {"OBTaperCube", - OB_MESH, - 106, - 208, - 104, - 416, - float3(24.444445f, 0.502543f, -0.753814f), - float3(23.790743f, 0.460522f, -0.766546f), - float3(-0.0546f, 0.1716f, 0.9837f)}, - {"OBText", - OB_MESH, - 177, - 345, - 171, - 513, - float3(1.75f, -9.458f, 0), - float3(0.587f, -9.406f, 0), - float3(0, 0, 1), - float2(0.017544f, 0)}, - {"OBUVCube", - OB_MESH, - 8, - 13, - 7, - 26, - float3(7, 1, -1), - float3(5, 1, 1), - float3(0, 0, 1), - float2(0.654526f, 0.579873f)}, - {"OBUVImageCube", - OB_MESH, - 8, - 13, - 7, - 26, - float3(10, 1, -1), - float3(8, 1, 1), - float3(0, 0, 1), - float2(0.654526f, 0.579873f)}, - {"OBVColCube", - OB_MESH, - 8, - 13, - 7, - 26, - float3(13, 1, -1), - float3(11, 1, 1), - float3(0, 0, 1), - float2(0, 0), - float4(0.0f, 0.002125f, 1.0f, 1.0f)}, - {"OBVGroupCube", - OB_MESH, - 8, - 13, - 7, - 26, - float3(16, 1, -1), - float3(14, 1, 1), - float3(0, 0, 1)}, - }; - import_and_check("all_objects.obj", expect, std::size(expect), 7); -} - -TEST_F(OBJImportTest, import_cubes_vertex_colors) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBCubeCornerByte", - OB_MESH, - 8, - 12, - 6, - 24, - float3(1.0f, 1.0f, -3.812445f), - float3(-1.0f, -1.0f, -1.812445f), - float3(0, 0, 0), - float2(0, 0), - float4(0.89627f, 0.036889f, 0.47932f, 1.0f)}, - {"OBCubeCornerFloat", - OB_MESH, - 8, - 12, - 6, - 24, - float3(3.481967f, 1.0f, -3.812445f), - float3(1.481967f, -1.0f, -1.812445f), - float3(0, 0, 0), - float2(0, 0), - float4(1.564582f, 0.039217f, 0.664309f, 1.0f)}, - {"OBCubeMultiColorAttribs", - OB_MESH, - 8, - 12, - 6, - 24, - float3(-4.725068f, -1.0f, 1.0f), - float3(-2.725068f, 1.0f, -1.0f), - float3(0, 0, 0), - float2(0, 0), - float4(0.270498f, 0.47932f, 0.262251f, 1.0f)}, - {"OBCubeNoColors", - OB_MESH, - 8, - 12, - 6, - 24, - float3(-4.550208f, -1.0f, -1.918042f), - float3(-2.550208f, 1.0f, -3.918042f)}, - {"OBCubeVertexByte", - OB_MESH, - 8, - 12, - 6, - 24, - float3(1.0f, 1.0f, -1.0f), - float3(-1.0f, -1.0f, 1.0f), - float3(0, 0, 0), - float2(0, 0), - float4(0.846873f, 0.027321f, 0.982123f, 1.0f)}, - {"OBCubeVertexFloat", - OB_MESH, - 8, - 12, - 6, - 24, - float3(3.392028f, 1.0f, -1.0f), - float3(1.392028f, -1.0f, 1.0f), - float3(0, 0, 0), - float2(0, 0), - float4(49.99467f, 0.027321f, 0.982123f, 1.0f)}, - }; - import_and_check("cubes_vertex_colors.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_cubes_vertex_colors_mrgb) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBCubeMRGB", - OB_MESH, - 8, - 12, - 6, - 24, - float3(4, 1, -1), - float3(2, -1, 1), - float3(0, 0, 0), - float2(0, 0), - float4(0.8714f, 0.6308f, 0.5271f, 1.0f)}, - {"OBCubeXYZRGB", - OB_MESH, - 8, - 12, - 6, - 24, - float3(1, 1, -1), - float3(-1, -1, 1), - float3(0, 0, 0), - float2(0, 0), - float4(0.6038f, 0.3185f, 0.1329f, 1.0f)}, - {"OBTriMRGB", - OB_MESH, - 3, - 3, - 1, - 3, - float3(12, 1, -1), - float3(10, 0, -1), - float3(0, 0, 0), - float2(0, 0), - float4(1.0f, 0.0f, 0.0f, 1.0f)}, - { - "OBTriNoColors", - OB_MESH, - 3, - 3, - 1, - 3, - float3(8, 1, -1), - float3(6, 0, -1), - }, - }; - import_and_check("cubes_vertex_colors_mrgb.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_vertex_colors_non_contiguous) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBNoColor", - OB_MESH, - 3, - 3, - 1, - 3, - float3(0, 0, 1), - float3(1, 0, 1), - float3(0, 0, 0), - float2(0, 0), - float4(-1, -1, -1, -1)}, - {"OBRed", - OB_MESH, - 3, - 3, - 1, - 3, - float3(0, 0, 0), - float3(1, 0, 0), - float3(0, 0, 0), - float2(0, 0), - float4(1, 0, 0, 1)}, - }; - import_and_check("vertex_colors_non_contiguous.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_vertices) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - /* Loose vertices without faces or edges. */ - {"OBCube.001", OB_MESH, 8, 0, 0, 0, float3(1, 1, -1), float3(-1, 1, 1)}, - }; - import_and_check("vertices.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_split_options_by_object) -{ - /* Default is to split by object */ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBBox", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, -1, 1)}, - {"OBPyramid", OB_MESH, 5, 8, 5, 16, float3(3, 1, -1), float3(4, 0, 2)}, - }; - import_and_check("split_options.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_split_options_by_group) -{ - params.use_split_objects = false; - params.use_split_groups = true; - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBBoxOne", OB_MESH, 4, 4, 1, 4, float3(1, -1, -1), float3(-1, -1, 1)}, - {"OBBoxTwo", OB_MESH, 6, 7, 2, 8, float3(1, 1, 1), float3(-1, -1, 1)}, - {"OBBoxTwo.001", OB_MESH, 6, 7, 2, 8, float3(1, 1, -1), float3(-1, -1, -1)}, - {"OBPyrBottom", OB_MESH, 4, 4, 1, 4, float3(3, 1, -1), float3(3, -1, -1)}, - {"OBPyrSides", OB_MESH, 5, 8, 4, 12, float3(3, 1, -1), float3(4, 0, 2)}, - {"OBsplit_options", OB_MESH, 4, 4, 1, 4, float3(1, 1, -1), float3(-1, 1, 1)}, - }; - import_and_check("split_options.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_split_options_by_object_and_group) -{ - params.use_split_objects = true; - params.use_split_groups = true; - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBBox", OB_MESH, 4, 4, 1, 4, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBBoxOne", OB_MESH, 4, 4, 1, 4, float3(1, -1, -1), float3(-1, -1, 1)}, - {"OBBoxTwo", OB_MESH, 6, 7, 2, 8, float3(1, 1, 1), float3(-1, -1, 1)}, - {"OBBoxTwo.001", OB_MESH, 6, 7, 2, 8, float3(1, 1, -1), float3(-1, -1, -1)}, - {"OBPyrBottom", OB_MESH, 4, 4, 1, 4, float3(3, 1, -1), float3(3, -1, -1)}, - {"OBPyrSides", OB_MESH, 5, 8, 4, 12, float3(3, 1, -1), float3(4, 0, 2)}, - }; - import_and_check("split_options.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_split_options_none) -{ - params.use_split_objects = false; - params.use_split_groups = false; - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBsplit_options", OB_MESH, 13, 20, 11, 40, float3(1, 1, -1), float3(4, 0, 2)}, - }; - import_and_check("split_options.obj", expect, std::size(expect), 0); -} - -TEST_F(OBJImportTest, import_polylines) -{ - Expectation expect[] = { - {"OBCube", OB_MESH, 8, 12, 6, 24, float3(1, 1, -1), float3(-1, 1, 1)}, - {"OBpolylines", OB_MESH, 13, 8, 0, 0, float3(1, 0, 0), float3(.7, .7, 2)}, - }; - import_and_check("polylines.obj", expect, std::size(expect), 0); + CLG_exit(); } } // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/tests/obj_mtl_parser_tests.cc b/source/blender/io/wavefront_obj/tests/obj_mtl_parser_tests.cc index 0ee72e67cd9..8385715cc1c 100644 --- a/source/blender/io/wavefront_obj/tests/obj_mtl_parser_tests.cc +++ b/source/blender/io/wavefront_obj/tests/obj_mtl_parser_tests.cc @@ -142,44 +142,6 @@ TEST_F(OBJMTLParserTest, string_newlines_whitespace) check_string(text, mat, ARRAY_SIZE(mat)); } -TEST_F(OBJMTLParserTest, cube) -{ - MTLMaterial mat; - mat.name = "red"; - mat.ambient_color = {0.2f, 0.2f, 0.2f}; - mat.color = {1, 0, 0}; - check("cube.mtl", &mat, 1); -} - -TEST_F(OBJMTLParserTest, all_objects) -{ - MTLMaterial mat[7]; - for (auto &m : mat) { - m.ambient_color = {1, 1, 1}; - m.spec_color = {0.5f, 0.5f, 0.5f}; - m.emission_color = {0, 0, 0}; - m.spec_exponent = 250; - m.ior = 1; - m.alpha = 1; - m.illum_mode = 2; - } - mat[0].name = "Blue"; - mat[0].color = {0, 0, 1}; - mat[1].name = "BlueDark"; - mat[1].color = {0, 0, 0.5f}; - mat[2].name = "Green"; - mat[2].color = {0, 1, 0}; - mat[3].name = "GreenDark"; - mat[3].color = {0, 0.5f, 0}; - mat[4].name = "Material"; - mat[4].color = {0.8f, 0.8f, 0.8f}; - mat[5].name = "Red"; - mat[5].color = {1, 0, 0}; - mat[6].name = "RedDark"; - mat[6].color = {0.5f, 0, 0}; - check("all_objects.mtl", mat, ARRAY_SIZE(mat)); -} - TEST_F(OBJMTLParserTest, materials) { MTLMaterial mat[6]; @@ -286,36 +248,6 @@ TEST_F(OBJMTLParserTest, materials) check("materials.mtl", mat, ARRAY_SIZE(mat)); } -TEST_F(OBJMTLParserTest, materials_without_pbr) -{ - MTLMaterial mat[2]; - mat[0].name = "Mat1"; - mat[0].spec_exponent = 360.0f; - mat[0].ambient_color = {0.9f, 0.9f, 0.9f}; - mat[0].color = {0.8f, 0.276449f, 0.101911f}; - mat[0].spec_color = {0.25f, 0.25f, 0.25f}; - mat[0].emission_color = {0, 0, 0}; - mat[0].ior = 1.45f; - mat[0].alpha = 1; - mat[0].illum_mode = 3; - - mat[1].name = "Mat2"; - mat[1].ambient_color = {1, 1, 1}; - mat[1].color = {0.8f, 0.8f, 0.8f}; - mat[1].spec_color = {0.5f, 0.5f, 0.5f}; - mat[1].ior = 1.45f; - mat[1].alpha = 1; - mat[1].illum_mode = 2; - { - MTLTexMap &ns = mat[1].tex_map_of_type(MTLTexMapType::SpecularExponent); - ns.image_path = "../blend_geometry/texture_roughness.png"; - MTLTexMap &ke = mat[1].tex_map_of_type(MTLTexMapType::Emission); - ke.image_path = "../blend_geometry/texture_illum.png"; - } - - check("materials_without_pbr.mtl", mat, ARRAY_SIZE(mat)); -} - TEST_F(OBJMTLParserTest, materials_pbr) { MTLMaterial mat[2]; diff --git a/tests/data b/tests/data index eb313866273..355a198573d 160000 --- a/tests/data +++ b/tests/data @@ -1 +1 @@ -Subproject commit eb3138662731eb2848b9bf4d2daba95da97e6145 +Subproject commit 355a198573d64b7e493a7c6101df4068ca83dbeb diff --git a/tests/python/CMakeLists.txt b/tests/python/CMakeLists.txt index 030f3287cf5..8dc59f52cb7 100644 --- a/tests/python/CMakeLists.txt +++ b/tests/python/CMakeLists.txt @@ -47,6 +47,21 @@ function(add_blender_test testname) ) endfunction() +function(add_blender_test_io testname) + # Remove `--debug-exit-on-error` since errors + # are printed on e.g. meshes with invalid data. But + # we do want to test those in import tests. + set(EXE_PARAMS ${TEST_BLENDER_EXE_PARAMS}) + list(REMOVE_ITEM EXE_PARAMS --debug-exit-on-error) + add_blender_test_impl( + "${testname}" + "" + "${TEST_BLENDER_EXE}" + ${EXE_PARAMS} + ${ARGN} + ) +endfunction() + if(WITH_UI_TESTS) set(_blender_headless_env_vars "BLENDER_BIN=${TEST_BLENDER_EXE}") @@ -938,6 +953,44 @@ if(WITH_USD) ) endif() +add_blender_test_io( + io_fbx_import + --python ${CMAKE_CURRENT_LIST_DIR}/io_fbx_import_test.py + -- + --testdir "${TEST_SRC_DIR}/io_tests/fbx" + --outdir "${TEST_OUT_DIR}/io_fbx" +) + +if(WITH_IO_WAVEFRONT_OBJ) + add_blender_test_io( + io_obj_import + --python ${CMAKE_CURRENT_LIST_DIR}/io_obj_import_test.py + -- + --testdir "${TEST_SRC_DIR}/io_tests/obj" + --outdir "${TEST_OUT_DIR}/io_obj" + ) +endif() + +if(WITH_IO_PLY) + add_blender_test_io( + io_ply_import + --python ${CMAKE_CURRENT_LIST_DIR}/io_ply_import_test.py + -- + --testdir "${TEST_SRC_DIR}/io_tests/ply" + --outdir "${TEST_OUT_DIR}/io_ply" + ) +endif() + +if(WITH_IO_STL) + add_blender_test_io( + io_stl_import + --python ${CMAKE_CURRENT_LIST_DIR}/io_stl_import_test.py + -- + --testdir "${TEST_SRC_DIR}/io_tests/stl" + --outdir "${TEST_OUT_DIR}/io_stl" + ) +endif() + if(WITH_CODEC_FFMPEG) add_python_test( ffmpeg diff --git a/tests/python/io_fbx_import_test.py b/tests/python/io_fbx_import_test.py new file mode 100644 index 00000000000..4d83a7c5eae --- /dev/null +++ b/tests/python/io_fbx_import_test.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2025 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later +import pathlib +import sys +import unittest + +import bpy + +sys.path.append(str(pathlib.Path(__file__).parent.absolute())) + +args = None + + +class FBXImportTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.testdir = args.testdir + cls.output_dir = args.outdir + + def test_import_fbx(self): + input_files = sorted(pathlib.Path(self.testdir).glob("*.fbx")) + self.passed_tests = [] + self.failed_tests = [] + self.updated_tests = [] + + from modules import io_report + report = io_report.Report("FBX Import", self.output_dir, self.testdir, self.testdir.joinpath("reference")) + + for input_file in input_files: + with self.subTest(pathlib.Path(input_file).stem): + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "../empty.blend")) + ok = report.import_and_check( + input_file, lambda filepath, params: bpy.ops.import_scene.fbx( + filepath=str(input_file), use_subsurf=True, use_custom_props=True, **params)) + if not ok: + self.fail(f"{input_file.stem} import result does not match expectations") + + report.finish("io_fbx") + + +def main(): + global args + import argparse + + if '--' in sys.argv: + argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:] + else: + argv = sys.argv + + parser = argparse.ArgumentParser() + parser.add_argument('--testdir', required=True, type=pathlib.Path) + parser.add_argument('--outdir', required=True, type=pathlib.Path) + args, remaining = parser.parse_known_args(argv) + + unittest.main(argv=remaining) + + +if __name__ == "__main__": + main() diff --git a/tests/python/io_obj_import_test.py b/tests/python/io_obj_import_test.py new file mode 100644 index 00000000000..38e67de6076 --- /dev/null +++ b/tests/python/io_obj_import_test.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2025 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later +import pathlib +import sys +import unittest + +import bpy + +sys.path.append(str(pathlib.Path(__file__).parent.absolute())) + +args = None + + +class OBJImportTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.testdir = args.testdir + cls.output_dir = args.outdir + + def test_import_obj(self): + input_files = sorted(pathlib.Path(self.testdir).glob("*.obj")) + self.passed_tests = [] + self.failed_tests = [] + self.updated_tests = [] + + from modules import io_report + report = io_report.Report("OBJ Import", self.output_dir, self.testdir, self.testdir.joinpath("reference")) + + for input_file in input_files: + with self.subTest(pathlib.Path(input_file).stem): + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "../empty.blend")) + ok = report.import_and_check( + input_file, lambda filepath, params: bpy.ops.wm.obj_import(filepath=str(input_file), **params)) + if not ok: + self.fail(f"{input_file.stem} import result does not match expectations") + + report.finish("io_obj") + + +def main(): + global args + import argparse + + if '--' in sys.argv: + argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:] + else: + argv = sys.argv + + parser = argparse.ArgumentParser() + parser.add_argument('--testdir', required=True, type=pathlib.Path) + parser.add_argument('--outdir', required=True, type=pathlib.Path) + args, remaining = parser.parse_known_args(argv) + + unittest.main(argv=remaining) + + +if __name__ == "__main__": + main() diff --git a/tests/python/io_ply_import_test.py b/tests/python/io_ply_import_test.py new file mode 100644 index 00000000000..914f24aa292 --- /dev/null +++ b/tests/python/io_ply_import_test.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2025 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later +import pathlib +import sys +import unittest + +import bpy + +sys.path.append(str(pathlib.Path(__file__).parent.absolute())) + +args = None + + +class PLYImportTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.testdir = args.testdir + cls.output_dir = args.outdir + + def test_import_ply(self): + input_files = sorted(pathlib.Path(self.testdir).glob("*.ply")) + self.passed_tests = [] + self.failed_tests = [] + self.updated_tests = [] + + from modules import io_report + report = io_report.Report("PLY Import", self.output_dir, self.testdir, self.testdir.joinpath("reference")) + + for input_file in input_files: + with self.subTest(pathlib.Path(input_file).stem): + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "../empty.blend")) + ok = report.import_and_check( + input_file, lambda filepath, params: bpy.ops.wm.ply_import( + filepath=str(input_file), **params)) + if not ok: + self.fail(f"{input_file.stem} import result does not match expectations") + + report.finish("io_ply") + + +def main(): + global args + import argparse + + if '--' in sys.argv: + argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:] + else: + argv = sys.argv + + parser = argparse.ArgumentParser() + parser.add_argument('--testdir', required=True, type=pathlib.Path) + parser.add_argument('--outdir', required=True, type=pathlib.Path) + args, remaining = parser.parse_known_args(argv) + + unittest.main(argv=remaining) + + +if __name__ == "__main__": + main() diff --git a/tests/python/io_stl_import_test.py b/tests/python/io_stl_import_test.py new file mode 100644 index 00000000000..8b3a28df010 --- /dev/null +++ b/tests/python/io_stl_import_test.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2025 Blender Authors +# +# SPDX-License-Identifier: GPL-2.0-or-later +import pathlib +import sys +import unittest + +import bpy + +sys.path.append(str(pathlib.Path(__file__).parent.absolute())) + +args = None + + +class STLImportTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.testdir = args.testdir + cls.output_dir = args.outdir + + def test_import_stl(self): + input_files = sorted(pathlib.Path(self.testdir).glob("*.stl")) + self.passed_tests = [] + self.failed_tests = [] + self.updated_tests = [] + + from modules import io_report + report = io_report.Report("STL Import", self.output_dir, self.testdir, self.testdir.joinpath("reference")) + + for input_file in input_files: + with self.subTest(pathlib.Path(input_file).stem): + bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "../empty.blend")) + ok = report.import_and_check( + input_file, lambda filepath, params: bpy.ops.wm.stl_import( + filepath=str(input_file), **params)) + if not ok: + self.fail(f"{input_file.stem} import result does not match expectations") + + report.finish("io_stl") + + +def main(): + global args + import argparse + + if '--' in sys.argv: + argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:] + else: + argv = sys.argv + + parser = argparse.ArgumentParser() + parser.add_argument('--testdir', required=True, type=pathlib.Path) + parser.add_argument('--outdir', required=True, type=pathlib.Path) + args, remaining = parser.parse_known_args(argv) + + unittest.main(argv=remaining) + + +if __name__ == "__main__": + main() diff --git a/tests/python/modules/io_report.py b/tests/python/modules/io_report.py new file mode 100644 index 00000000000..dcb206a7e92 --- /dev/null +++ b/tests/python/modules/io_report.py @@ -0,0 +1,633 @@ +# SPDX-FileCopyrightText: 2025 Blender Authors +# +# SPDX-License-Identifier: Apache-2.0 + +""" +Compare textual dump of imported data against reference versions and generate +a HTML report showing the differences, for regression testing. +""" + +import bpy +import bpy_extras.node_shader_utils +import difflib +import json +import os +import pathlib + +from . import global_report +from io import StringIO +from typing import Callable + + +def fmtf(f: float) -> str: + # ensure tiny numbers are 0.0, + # and not "-0.0" for example + if abs(f) < 0.0005: + return "0.000" + return f"{f:.3f}" + + +class Report: + __slots__ = ( + 'title', + 'output_dir', + 'global_dir', + 'input_dir', + 'reference_dir', + 'tested_count', + 'failed_list', + 'passed_list', + 'updated_list', + 'failed_html', + 'passed_html', + 'update_templates', + ) + + def __init__(self, title: str, output_dir: pathlib.Path, input_dir: pathlib.Path, reference_dir: pathlib.Path): + self.title = title + self.output_dir = output_dir + self.global_dir = os.path.dirname(output_dir) + self.input_dir = input_dir + self.reference_dir = reference_dir + + self.tested_count = 0 + self.failed_list = [] + self.passed_list = [] + self.updated_list = [] + self.failed_html = "" + self.passed_html = "" + + os.makedirs(output_dir, exist_ok=True) + self.update_templates = os.getenv('BLENDER_TEST_UPDATE', "0").strip() == "1" + if self.update_templates: + os.makedirs(self.reference_dir, exist_ok=True) + + # write out dummy html in case test crashes + if not self.update_templates: + filename = "report.html" + filepath = os.path.join(self.output_dir, filename) + pathlib.Path(filepath).write_text( + 'Report not generated yet. Crashed during tests?') + + @staticmethod + def _navigation_item(title, href, active): + if active: + return """""" % title + else: + return """""" % (href, title) + + def _navigation_html(self): + html = """""" + return html + + def finish(self, test_suite_name: str) -> None: + """ + Finishes the report: short summary to the console, + generates full report as HTML. + """ + print(f"\n============") + if self.update_templates: + print(f"{self.tested_count} input files tested, {len(self.updated_list)} references updated to new results") + for test in self.updated_list: + print(f"UPDATED {test}") + else: + self._write_html(test_suite_name) + print(f"{self.tested_count} input files tested, {len(self.passed_list)} passed") + if len(self.failed_list): + print(f"FAILED {len(self.failed_list)} tests:") + for test in self.failed_list: + print(f"FAILED {test}") + + def _write_html(self, test_suite_name: str): + + tests_html = self.failed_html + self.passed_html + menu = self._navigation_html() + + failed = len(self.failed_html) > 0 + if failed: + message = """""" + message += f"Tested files: {self.tested_count}, failed: {len(self.failed_list)}" + else: + message = f"Tested files: {self.tested_count}" + + title = self.title + " Test Report" + columns_html = "NameNewReferenceDiff" + + html = f""" + + + {title} + + + + +
+
+

{title}

+ {menu} + {message} + + {columns_html} + {tests_html} +
+
+
+ + + """ + + filename = "report.html" + filepath = os.path.join(self.output_dir, filename) + pathlib.Path(filepath).write_text(html) + + print(f"Report saved to: {pathlib.Path(filepath).as_uri()}") + + # Update global report + global_failed = failed + global_report.add(self.global_dir, "IO", self.title, filepath, global_failed) + + def _relative_url(self, filepath): + relpath = os.path.relpath(filepath, self.output_dir) + return pathlib.Path(relpath).as_posix() + + @staticmethod + def _colored_diff(a: str, b: str): + a_lines = a.splitlines() + b_lines = b.splitlines() + diff = difflib.unified_diff(a_lines, b_lines, lineterm='') + html = [] + for line in diff: + if line.startswith('+++') or line.startswith('---'): + pass + elif line.startswith('@@'): + html.append(f'{line}') + elif line.startswith('-'): + html.append(f'{line}') + elif line.startswith('+'): + html.append(f'{line}') + else: + html.append(line) + return '\n'.join(html) + + def _add_test_result(self, testname: str, got_desc: str, ref_desc: str): + error = got_desc != ref_desc + status = "FAILED" if error else "" + table_style = """ class="table-danger" """ if error else "" + cell_class = "text_cell text_cell_larger" if error else "text_cell" + diff_text = " " + if error: + diff_text = Report._colored_diff(ref_desc, got_desc) + + test_html = f""" + + {testname}
{status} +
{got_desc}
+
{ref_desc}
+
{diff_text}
+ """ + + if error: + self.failed_html += test_html + else: + self.passed_html += test_html + + @staticmethod + def _val_to_str(val) -> str: + if isinstance(val, bpy.types.BoolAttributeValue): + return f"{1 if val.value else 0}" + if isinstance(val, bpy.types.IntAttributeValue): + return f"{val.value}" + if isinstance(val, bpy.types.FloatAttributeValue): + return f"{val.value:.3f}" + if isinstance(val, bpy.types.FloatVectorAttributeValue): + return f"({val.vector[0]:.3f}, {val.vector[1]:.3f}, {val.vector[2]:.3f})" + if isinstance(val, bpy.types.Float2AttributeValue): + return f"({val.vector[0]:.3f}, {val.vector[1]:.3f})" + if isinstance(val, bpy.types.FloatColorAttributeValue) or isinstance(val, bpy.types.ByteColorAttributeValue): + return f"({val.color[0]:.3f}, {val.color[1]:.3f}, {val.color[2]:.3f}, {val.color[3]:.3f})" + if isinstance(val, bpy.types.Int2AttributeValue) or isinstance(val, bpy.types.Short2AttributeValue): + return f"({val.value[0]}, {val.value[1]})" + if isinstance(val, bpy.types.ID): + return f"'{val.name}'" + if isinstance(val, bpy.types.MeshLoop): + return f"{val.vertex_index}" + if isinstance(val, bpy.types.MeshEdge): + return f"{min(val.vertices[0],val.vertices[1])}/{max(val.vertices[0],val.vertices[1])}" + if isinstance(val, bpy.types.MaterialSlot): + return f"('{val.name}', {val.link})" + if isinstance(val, bpy.types.VertexGroup): + return f"'{val.name}'" + if isinstance(val, bpy.types.Keyframe): + return f"({val.co[0]:.1f}, {val.co[1]:.1f} int:{val.interpolation} ease:{val.easing})" + if isinstance(val, bpy.types.SplinePoint): + return f"({val.co[0]:.3f}, {val.co[1]:.3f}, {val.co[2]:.3f}) w:{val.weight:.3f}" + return str(val) + + # single-line dump of head/tail + @staticmethod + def _write_collection_single(col, desc: StringIO) -> None: + desc.write(f" - ") + side_to_print = 5 + if len(col) <= side_to_print * 2: + for val in col: + desc.write(f"{Report._val_to_str(val)} ") + else: + for val in col[:side_to_print]: + desc.write(f"{Report._val_to_str(val)} ") + desc.write(f"... ") + for val in col[-side_to_print:]: + desc.write(f"{Report._val_to_str(val)} ") + desc.write(f"\n") + + # multi-line dump of head/tail + @staticmethod + def _write_collection_multi(col, desc: StringIO) -> None: + side_to_print = 3 + if len(col) <= side_to_print * 2: + for val in col: + desc.write(f" - {Report._val_to_str(val)}\n") + else: + for val in col[:side_to_print]: + desc.write(f" - {Report._val_to_str(val)}\n") + desc.write(f" ...\n") + for val in col[-side_to_print:]: + desc.write(f" - {Report._val_to_str(val)}\n") + + @staticmethod + def _write_attr(attr: bpy.types.Attribute, desc: StringIO) -> None: + if len(attr.data) == 0: + return + desc.write(f" - attr '{attr.name}' {attr.data_type} {attr.domain}\n") + if isinstance( + attr, + bpy.types.BoolAttribute) or isinstance( + attr, + bpy.types.IntAttribute) or isinstance( + attr, + bpy.types.FloatAttribute): + Report._write_collection_single(attr.data, desc) + else: + Report._write_collection_multi(attr.data, desc) + + @staticmethod + def _write_custom_props(bid, desc: StringIO) -> None: + items = bid.items() + if not items: + return + + rna_properties = {prop.identifier for prop in bid.bl_rna.properties if prop.is_runtime} + + had_any = False + for k, v in items: + if k in rna_properties: + continue + + if not had_any: + desc.write(f" - props:") + had_any = True + + if isinstance(v, str): + if k != "cycles": + desc.write(f" str:{k}='{v}'") + else: + desc.write(f" str:{k}=") + elif isinstance(v, int): + desc.write(f" int:{k}={v}") + elif isinstance(v, float): + desc.write(f" fl:{k}={v:.3f}") + elif len(v) == 3: + desc.write(f" f3:{k}=({v[0]:.3f}, {v[1]:.3f}, {v[2]:.3f})") + else: + desc.write(f" o:{k}={str(v)}") + if had_any: + desc.write(f"\n") + + def _node_shader_image_desc(self, tex: bpy_extras.node_shader_utils.ShaderImageTextureWrapper) -> str: + if not tex or not tex.image: + return "" + # Get relative path of the image + try: + rel_path = pathlib.Path(tex.image.filepath).relative_to(self.input_dir).as_posix() + except ValueError: + rel_path = "" + desc = f" tex:'{tex.image.name}' ({rel_path}) a:{tex.use_alpha}" + if tex.texcoords != 'UV': + desc += f" uv:{tex.texcoords}" + if tex.extension != 'REPEAT': + desc += f" ext:{tex.extension}" + if tuple(tex.translation) != (0.0, 0.0, 0.0): + desc += f" tr:({tex.translation[0]:.3f}, {tex.translation[1]:.3f}, {tex.translation[2]:.3f})" + if tuple(tex.rotation) != (0.0, 0.0, 0.0): + desc += f" rot:({tex.rotation[0]:.3f}, {tex.rotation[1]:.3f}, {tex.rotation[2]:.3f})" + if tuple(tex.scale) != (1.0, 1.0, 1.0): + desc += f" scl:({tex.scale[0]:.3f}, {tex.scale[1]:.3f}, {tex.scale[2]:.3f})" + return desc + + @staticmethod + def _write_animdata_desc(adt: bpy.types.AnimData, desc: StringIO) -> None: + if adt: + if adt.action: + desc.write(f" - anim act:{adt.action.name}") + if adt.action_slot: + desc.write(f" slot:{adt.action_slot.identifier}") + desc.write(f" blend:{adt.action_blend_type} drivers:{len(adt.drivers)}\n") + + def generate_main_data_desc(self) -> str: + """Generates textual description of the current state of the + Blender main data.""" + + desc = StringIO() + + # meshes + if len(bpy.data.meshes): + desc.write(f"==== Meshes: {len(bpy.data.meshes)}\n") + for mesh in bpy.data.meshes: + # mesh overview + desc.write( + f"- Mesh '{mesh.name}' vtx:{len(mesh.vertices)} face:{len(mesh.polygons)} loop:{len(mesh.loops)} edge:{len(mesh.edges)}\n") + if len(mesh.loops) > 0: + Report._write_collection_single(mesh.loops, desc) + if len(mesh.edges) > 0: + Report._write_collection_single(mesh.edges, desc) + # attributes + for attr in mesh.attributes: + if not attr.is_internal: + Report._write_attr(attr, desc) + # skinning / vertex groups + has_skinning = any(v.groups for v in mesh.vertices) + if has_skinning: + desc.write(f" - vertex groups:\n") + for vtx in mesh.vertices[:5]: + desc.write(f" -") + # emit vertex group weights in decreasing weight order + sorted_groups = sorted(vtx.groups, key=lambda g: g.weight, reverse=True) + for grp in sorted_groups: + desc.write(f" {grp.group}={grp.weight:.3f}") + desc.write(f"\n") + # materials + if mesh.materials: + desc.write(f" - {len(mesh.materials)} materials\n") + Report._write_collection_single(mesh.materials, desc) + # blend shapes + key = mesh.shape_keys + if key: + for kb in key.key_blocks: + desc.write(f" - shape key '{kb.name}' w:{kb.value:.3f} vgrp:'{kb.vertex_group}'") + # print first several deltas that are not zero from the key + count = 0 + idx = 0 + for pt in kb.points: + if pt.co.length_squared > 0: + desc.write(f" {idx}:({pt.co[0]:.3f}, {pt.co[1]:.3f}, {pt.co[2]:.3f})") + count += 1 + if count >= 3: + break + idx += 1 + desc.write(f"\n") + Report._write_animdata_desc(mesh.animation_data, desc) + Report._write_custom_props(mesh, desc) + desc.write(f"\n") + + # curves + if len(bpy.data.curves): + desc.write(f"==== Curves: {len(bpy.data.curves)}\n") + for curve in bpy.data.curves: + # overview + desc.write( + f"- Curve '{curve.name}' dim:{curve.dimensions} resu:{curve.resolution_u} resv:{curve.resolution_v} splines:{len(curve.splines)}\n") + for spline in curve.splines[:5]: + desc.write( + f" - spline type:{spline.type} pts:{spline.point_count_u}x{spline.point_count_v} order:{spline.order_u}x{spline.order_v} cyclic:{spline.use_cyclic_u},{spline.use_cyclic_v} endp:{spline.use_endpoint_u},{spline.use_endpoint_v}\n") + Report._write_collection_multi(spline.points, desc) + # materials + if curve.materials: + desc.write(f" - {len(curve.materials)} materials\n") + Report._write_collection_single(curve.materials, desc) + Report._write_animdata_desc(curve.animation_data, desc) + Report._write_custom_props(curve, desc) + desc.write(f"\n") + + # objects + if len(bpy.data.objects): + desc.write(f"==== Objects: {len(bpy.data.objects)}\n") + for obj in bpy.data.objects: + desc.write(f"- Obj '{obj.name}' {obj.type}") + if obj.data: + desc.write(f" data:'{obj.data.name}'") + if obj.parent: + desc.write(f" par:'{obj.parent.name}'") + desc.write(f"\n") + desc.write(f" - pos {fmtf(obj.location[0])}, {fmtf(obj.location[1])}, {fmtf(obj.location[2])}\n") + desc.write( + f" - rot {fmtf(obj.rotation_euler[0])}, {fmtf(obj.rotation_euler[1])}, {fmtf(obj.rotation_euler[2])} ({obj.rotation_mode})\n") + desc.write(f" - scl {obj.scale[0]:.3f}, {obj.scale[1]:.3f}, {obj.scale[2]:.3f}\n") + if obj.vertex_groups: + desc.write(f" - {len(obj.vertex_groups)} vertex groups\n") + Report._write_collection_single(obj.vertex_groups, desc) + if obj.material_slots: + has_object_link = any(slot.link == 'OBJECT' for slot in obj.material_slots if slot.link) + if has_object_link: + desc.write(f" - {len(obj.material_slots)} object materials\n") + Report._write_collection_single(obj.material_slots, desc) + if obj.modifiers: + desc.write(f" - {len(obj.modifiers)} modifiers\n") + for mod in obj.modifiers: + desc.write(f" - {mod.type} '{mod.name}'") + if isinstance(mod, bpy.types.SubsurfModifier): + desc.write( + f" levels:{mod.levels}/{mod.render_levels} type:{mod.subdivision_type} crease:{mod.use_creases}") + desc.write(f"\n") + Report._write_animdata_desc(obj.animation_data, desc) + Report._write_custom_props(obj, desc) + desc.write(f"\n") + + # cameras + if len(bpy.data.cameras): + desc.write(f"==== Cameras: {len(bpy.data.cameras)}\n") + for cam in bpy.data.cameras: + desc.write( + f"- Cam '{cam.name}' {cam.type} lens:{cam.lens:.1f} {cam.lens_unit} near:{cam.clip_start:.3f} far:{cam.clip_end:.1f} orthosize:{cam.ortho_scale:.1f}\n") + desc.write(f" - fov {cam.angle:.3f} (h {cam.angle_x:.3f} v {cam.angle_y:.3f})\n") + desc.write( + f" - sensor {cam.sensor_width:.1f}x{cam.sensor_height:.1f} shift {cam.shift_x:.3f},{cam.shift_y:.3f}\n") + if cam.dof.use_dof: + desc.write( + f" - dof dist:{cam.dof.focus_distance:.3f} fstop:{cam.dof.aperture_fstop:.1f} blades:{cam.dof.aperture_blades}\n") + Report._write_animdata_desc(cam.animation_data, desc) + Report._write_custom_props(cam, desc) + desc.write(f"\n") + + # lights + if len(bpy.data.lights): + desc.write(f"==== Lights: {len(bpy.data.lights)}\n") + for light in bpy.data.lights: + desc.write( + f"- Light '{light.name}' {light.type} col:({light.color[0]:.3f}, {light.color[1]:.3f}, {light.color[2]:.3f}) energy:{light.energy:.3f}\n") + if isinstance(light, bpy.types.SpotLight): + desc.write(f" - spot {light.spot_size:.3f} blend {light.spot_blend:.3f}\n") + Report._write_animdata_desc(light.animation_data, desc) + Report._write_custom_props(light, desc) + desc.write(f"\n") + + # materials + if len(bpy.data.materials): + desc.write(f"==== Materials: {len(bpy.data.materials)}\n") + for mat in bpy.data.materials: + desc.write(f"- Mat '{mat.name}'\n") + wrap = bpy_extras.node_shader_utils.PrincipledBSDFWrapper(mat) + desc.write( + f" - base color ({wrap.base_color[0]:.3f}, {wrap.base_color[1]:.3f}, {wrap.base_color[2]:.3f}){self._node_shader_image_desc(wrap.base_color_texture)}\n") + desc.write( + f" - specular ior {wrap.specular:.3f}{self._node_shader_image_desc(wrap.specular_texture)}\n") + desc.write( + f" - specular tint ({wrap.specular_tint[0]:.3f}, {wrap.specular_tint[1]:.3f}, {wrap.specular_tint[2]:.3f}){self._node_shader_image_desc(wrap.specular_tint_texture)}\n") + desc.write( + f" - roughness {wrap.roughness:.3f}{self._node_shader_image_desc(wrap.roughness_texture)}\n") + desc.write( + f" - metallic {wrap.metallic:.3f}{self._node_shader_image_desc(wrap.metallic_texture)}\n") + desc.write(f" - ior {wrap.ior:.3f}{self._node_shader_image_desc(wrap.ior_texture)}\n") + if wrap.transmission > 0.0 or (wrap.transmission_texture and wrap.transmission_texture.image): + desc.write( + f" - transmission {wrap.transmission:.3f}{self._node_shader_image_desc(wrap.transmission_texture)}\n") + if wrap.alpha < 1.0 or (wrap.alpha_texture and wrap.alpha_texture.image): + desc.write( + f" - alpha {wrap.alpha:.3f}{self._node_shader_image_desc(wrap.alpha_texture)}\n") + if (wrap.emission_strength > 0.0 and wrap.emission_color[0] > 0.0 and wrap.emission_color[1] > 0.0 and wrap.emission_color[2] > 0.0) or ( + wrap.emission_strength_texture and wrap.emission_strength_texture.image): + desc.write( + f" - emission color ({wrap.emission_color[0]:.3f}, {wrap.emission_color[1]:.3f}, {wrap.emission_color[2]:.3f}){self._node_shader_image_desc(wrap.emission_color_texture)}\n") + desc.write( + f" - emission strength {wrap.emission_strength:.3f}{self._node_shader_image_desc(wrap.emission_strength_texture)}\n") + if (wrap.normalmap_texture and wrap.normalmap_texture.image): + desc.write( + f" - normalmap {wrap.normalmap_strength:.3f}{self._node_shader_image_desc(wrap.normalmap_texture)}\n") + Report._write_animdata_desc(mat.animation_data, desc) + Report._write_custom_props(mat, desc) + desc.write(f"\n") + + # actions + if len(bpy.data.actions): + desc.write(f"==== Actions: {len(bpy.data.actions)}\n") + for act in sorted(bpy.data.actions, key=lambda a: a.name): + desc.write( + f"- Action '{act.name}' curverange:({act.curve_frame_range[0]:.1f} .. {act.curve_frame_range[1]:.1f}) curves:{len(act.fcurves)}\n") + for fcu in act.fcurves[:5]: + desc.write( + f" - fcu '{fcu.data_path}[{fcu.array_index}] smooth:{fcu.auto_smoothing} extra:{fcu.extrapolation} keyframes:{len(fcu.keyframe_points)}'\n") + Report._write_collection_multi(fcu.keyframe_points, desc) + Report._write_custom_props(act, desc) + desc.write(f"\n") + + # armatures + if len(bpy.data.armatures): + desc.write(f"==== Armatures: {len(bpy.data.armatures)}\n") + for arm in bpy.data.armatures: + desc.write(f"- Armature '{arm.name}' {len(arm.bones)} bones\n") + for bone in arm.bones: + desc.write(f" - bone '{bone.name}'") + if bone.parent: + desc.write(f" parent:'{bone.parent.name}'") + desc.write( + f" h:({fmtf(bone.head[0])}, {fmtf(bone.head[1])}, {fmtf(bone.head[2])}) t:({fmtf(bone.tail[0]):}, {fmtf(bone.tail[1])}, {fmtf(bone.tail[2])})") + if bone.head_radius > 0.0 or bone.tail_radius > 0.0: + desc.write(f" radius h:{bone.head_radius:.3f} t:{bone.tail_radius:.3f}") + desc.write(f"\n") + desc.write(f"\n") + + # images + if len(bpy.data.images): + desc.write(f"==== Images: {len(bpy.data.images)}\n") + for img in bpy.data.images: + desc.write(f"- Image '{img.name}' {img.size[0]}x{img.size[1]} {img.depth}bpp\n") + Report._write_custom_props(img, desc) + desc.write(f"\n") + + text = desc.getvalue() + desc.close() + return text + + def import_and_check(self, input_file: pathlib.Path, import_func: Callable[[str, dict], None]) -> bool: + """ + Imports a single file using the provided import function, and + checks whether it matches with expected template, returns + comparison result. + + If there is a .json file next to the input file, the parameters from + that one file will be passed as extra parameters to the import function. + + When working in template update mode (environment variable + BLENDER_TEST_UPDATE=1), updates the template with new result + and always returns true. + """ + self.tested_count += 1 + input_basename = pathlib.Path(input_file).stem + print(f"Importing {input_file}...", flush=True) + + # load json parameters if they exist + params = {} + input_params_file = input_file.with_suffix(".json") + if input_params_file.exists(): + try: + with input_params_file.open('r', encoding='utf-8') as file: + params = json.load(file) + except: + pass + + # import + try: + import_func(str(input_file), params) + got_desc = self.generate_main_data_desc() + except RuntimeError as ex: + got_desc = f"Error during import: {ex}" + + ref_path: pathlib.Path = self.reference_dir / f"{input_basename}.txt" + if ref_path.exists(): + ref_desc = ref_path.read_text(encoding="utf-8").replace("\r\n", "\n") + else: + ref_desc = "" + + ok = True + if self.update_templates: + # write out newly got result as reference + if ref_desc != got_desc: + ref_path.write_text(got_desc, encoding="utf-8", newline="\n") + self.updated_list.append(input_basename) + else: + # compare result with expected reference + self._add_test_result(input_basename, got_desc, ref_desc) + if ref_desc == got_desc: + self.passed_list.append(input_basename) + else: + self.failed_list.append(input_basename) + ok = False + return ok