From e191d3d2436d52d71c4ebbd0bf3d6d2c1980680d Mon Sep 17 00:00:00 2001 From: Mattias Fredriksson Date: Fri, 1 Aug 2025 06:37:28 +0200 Subject: [PATCH] IO: OBJ improvements for NURBS curves * Bezier (NURBS) import, supporting both Blender and external variants. * Cleaner Bezier export, with better results importing in Rhino. * Internal support for exporting the new Curves type. * Tests covering a broad number of related cases. The current NURBS exporter writes curve data directly, without accounting for internal padding or how knots are generated from modes. This adjusts the export behavior to omit data that does not influence the geometry of the curve. The result is a simplified output that is easier to parse during import, both for the importer and when importing to other platforms (Rhino). Visual explanation to the adjustment can be found in #139174. Importer is also adjusted to support variations in knot patterns. Extending it to support the data generated by the exporter and by other platforms (Rhino). This should resolve some issues in relation to #138732. Integrated tests are added to broadly validate the common cases generated by the exporter, including variations of different modes and NURBS order. Regression tests are added to validate that the NURBS generated in Rhino are imported as expected. More details in the PR. Pull Request: https://projects.blender.org/blender/blender/pulls/139174 --- .../blender/io/wavefront_obj/CMakeLists.txt | 1 + .../exporter/obj_export_file_writer.cc | 87 ++- .../exporter/obj_export_file_writer.hh | 4 +- .../exporter/obj_export_nurbs.cc | 258 ++++++-- .../exporter/obj_export_nurbs.hh | 127 +++- .../io/wavefront_obj/exporter/obj_exporter.cc | 26 +- .../io/wavefront_obj/exporter/obj_exporter.hh | 9 +- .../importer/obj_import_nurbs.cc | 193 ++++-- .../wavefront_obj/tests/obj_nurbs_io_tests.cc | 586 ++++++++++++++++++ .../io_tests/obj/all_curves_as_nurbs.obj | 2 +- tests/files/io_tests/obj/nurbs.obj | 2 +- tests/files/io_tests/obj/nurbs_curves.obj | 2 +- tests/files/io_tests/obj/nurbs_manual.obj | 4 +- .../obj/reference/all_curves_as_nurbs.txt | 58 +- tests/files/io_tests/obj/reference/nurbs.txt | 12 +- .../io_tests/obj/reference/nurbs_curves.txt | 30 +- .../reference/rhino_curve_arc_bezier_deg2.txt | 15 + .../rhino_curve_bezier_deg3_cyclic.txt | 17 + .../reference/rhino_curve_bezier_deg4_rat.txt | 17 + .../rhino_curve_circle_bezier_deg2_cyclic.txt | 17 + .../rhino_curve_uniform_cyclic_deg3.txt | 17 + .../rhino_curve_uniform_cyclic_deg7.txt | 17 + .../obj/rhino_curve_arc_bezier_deg2.obj | 3 + .../obj/rhino_curve_bezier_deg3_cyclic.obj | 3 + .../obj/rhino_curve_bezier_deg4_rat.obj | 3 + .../rhino_curve_circle_bezier_deg2_cyclic.obj | 3 + .../obj/rhino_curve_uniform_cyclic_deg3.obj | 3 + .../obj/rhino_curve_uniform_cyclic_deg7.obj | 3 + 28 files changed, 1265 insertions(+), 254 deletions(-) create mode 100644 source/blender/io/wavefront_obj/tests/obj_nurbs_io_tests.cc create mode 100644 tests/files/io_tests/obj/reference/rhino_curve_arc_bezier_deg2.txt create mode 100644 tests/files/io_tests/obj/reference/rhino_curve_bezier_deg3_cyclic.txt create mode 100644 tests/files/io_tests/obj/reference/rhino_curve_bezier_deg4_rat.txt create mode 100644 tests/files/io_tests/obj/reference/rhino_curve_circle_bezier_deg2_cyclic.txt create mode 100644 tests/files/io_tests/obj/reference/rhino_curve_uniform_cyclic_deg3.txt create mode 100644 tests/files/io_tests/obj/reference/rhino_curve_uniform_cyclic_deg7.txt create mode 100644 tests/files/io_tests/obj/rhino_curve_arc_bezier_deg2.obj create mode 100644 tests/files/io_tests/obj/rhino_curve_bezier_deg3_cyclic.obj create mode 100644 tests/files/io_tests/obj/rhino_curve_bezier_deg4_rat.obj create mode 100644 tests/files/io_tests/obj/rhino_curve_circle_bezier_deg2_cyclic.obj create mode 100644 tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg3.obj create mode 100644 tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg7.obj diff --git a/source/blender/io/wavefront_obj/CMakeLists.txt b/source/blender/io/wavefront_obj/CMakeLists.txt index 61a77ac0060..2f7cac645cc 100644 --- a/source/blender/io/wavefront_obj/CMakeLists.txt +++ b/source/blender/io/wavefront_obj/CMakeLists.txt @@ -67,6 +67,7 @@ if(WITH_GTESTS) tests/obj_exporter_tests.cc tests/obj_importer_tests.cc tests/obj_mtl_parser_tests.cc + tests/obj_nurbs_io_tests.cc ) set(TEST_INC diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc index 833a1442bc1..4d30acde18d 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.cc @@ -16,7 +16,10 @@ #include "BLI_color.hh" #include "BLI_enumerable_thread_specific.hh" #include "BLI_fileops.h" +#include "BLI_math_matrix.h" #include "BLI_math_matrix.hh" +#include "BLI_math_rotation.h" +#include "BLI_math_vector.h" #include "BLI_path_utils.hh" #include "BLI_string.h" #include "BLI_task.hh" @@ -464,51 +467,89 @@ void OBJWriter::write_edges_indices(FormatHandler &fh, } } -void OBJWriter::write_nurbs_curve(FormatHandler &fh, const OBJCurve &obj_nurbs_data) const +static float4x4 compute_world_axes_transform(const OBJExportParams &export_params, + const blender::float4x4 &object_to_world) { + float4x4 world_axes_transform; + float axes_transform[3][3]; + unit_m3(axes_transform); + /* +Y-forward and +Z-up are the Blender's default 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(world_axes_transform.ptr(), axes_transform, object_to_world.ptr()); + /* #mul_m4_m3m4 does not transform last row of #Object.object_to_world, i.e. location data. */ + mul_v3_m3v3(world_axes_transform[3], axes_transform, object_to_world.location()); + world_axes_transform[3][3] = object_to_world[3][3]; + /* Apply global scale transform. */ + mul_v3_fl(world_axes_transform[0], export_params.global_scale); + mul_v3_fl(world_axes_transform[1], export_params.global_scale); + mul_v3_fl(world_axes_transform[2], export_params.global_scale); + mul_v3_fl(world_axes_transform[3], export_params.global_scale); + + return world_axes_transform; +} + +void OBJWriter::write_nurbs_curve(FormatHandler &fh, const IOBJCurve &obj_nurbs_data) const +{ const int total_splines = obj_nurbs_data.total_splines(); for (int spline_idx = 0; spline_idx < total_splines; spline_idx++) { /* Double check no surface is passed in as they are no supported (this is filtered when parsed) */ - BLI_assert(obj_nurbs_data.get_spline(spline_idx)->pntsv == 1); + BLI_assert(obj_nurbs_data.num_control_points_v(spline_idx) == 1); - const int total_vertices = obj_nurbs_data.total_spline_vertices(spline_idx); - for (int vertex_idx = 0; vertex_idx < total_vertices; vertex_idx++) { - const float3 vertex_coords = obj_nurbs_data.vertex_coordinates( - spline_idx, vertex_idx, export_params_.global_scale); - fh.write_obj_vertex(vertex_coords[0], vertex_coords[1], vertex_coords[2]); - } + const float4x4 world_axes_transform = compute_world_axes_transform( + export_params_, obj_nurbs_data.object_transform()); const char *nurbs_name = obj_nurbs_data.get_curve_name(); const int degree_u = obj_nurbs_data.get_nurbs_degree_u(spline_idx); fh.write_obj_group(nurbs_name); fh.write_obj_cstype(); fh.write_obj_nurbs_degree(degree_u); - /** - * The numbers written here are indices into the vertex coordinates written - * earlier, relative to the line that is going to be written. - * [0.0 - 1.0] is the curve parameter range. - * 0.0 1.0 -1 -2 -3 -4 for a non-cyclic curve with 4 vertices. - * 0.0 1.0 -1 -2 -3 -4 -1 -2 -3 for a cyclic curve with 4 vertices. - */ + const int num_points_u = obj_nurbs_data.num_control_points_u(spline_idx); + Vector knot_buffer; - Span knotsu = obj_nurbs_data.get_knots_u(spline_idx, knot_buffer); + Span knots_u = obj_nurbs_data.get_knots_u(spline_idx, knot_buffer); + IndexRange point_range(0, num_points_u); + knots_u = valid_nurb_control_point_range(degree_u + 1, knots_u, point_range); + + /* Write coords */ + Vector dynamic_point_buffer; + Span vertex_coords = obj_nurbs_data.vertex_coordinates(spline_idx, + dynamic_point_buffer); + + /* Write only unique points. */ + IndexRange point_loop_range = point_range.size() > vertex_coords.size() ? + point_range.drop_back(point_range.size() - + vertex_coords.size()) : + point_range; + for (const int64_t index : point_loop_range) { + /* Modulo will loop back to the 0:th point, not the start of the point range! */ + float3 co = vertex_coords[index % vertex_coords.size()]; + mul_m4_v3(world_axes_transform.ptr(), &co.x); + fh.write_obj_vertex(co[0], co[1], co[2]); + } fh.write_obj_curve_begin(); - fh.write_obj_nurbs_parm(knotsu[degree_u]); - fh.write_obj_nurbs_parm(knotsu.last(degree_u)); + fh.write_obj_nurbs_parm(knots_u[degree_u]); + fh.write_obj_nurbs_parm(knots_u.last(degree_u)); - for (int i = 0; i < num_points_u; i++) { - /* "+1" to keep indices one-based, even if they're negative: i.e., -1 refers to the - * last vertex coordinate, -2 second last. */ - fh.write_obj_face_v(-((i % total_vertices) + 1)); + /* Loop over the [0, N) range, not its actual interval [x, N + x). + * For cyclic curves, up to [0, order) will be repeated. + */ + for (int64_t index : point_range.index_range()) { + /* Write one based (1 ==> coords[0]) relative/negative indices. + * TODO: Write positive indices... + */ + index = -(point_loop_range.size() - (index % point_loop_range.size())); + fh.write_obj_face_v(index); } fh.write_obj_curve_end(); + /* Write knot vector. */ fh.write_obj_nurbs_parm_begin(); - for (const float &u : knotsu) { + for (const float &u : knots_u) { fh.write_obj_nurbs_parm(u); } fh.write_obj_nurbs_parm_end(); diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh index e58f8e73c81..f57a0e00977 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_file_writer.hh @@ -19,7 +19,7 @@ namespace blender::io::obj { -class OBJCurve; +class IOBJCurve; class OBJMesh; /** * Total vertices/ UV vertices/ normals of previous Objects @@ -95,7 +95,7 @@ class OBJWriter : NonMovable, NonCopyable { /** * Write a NURBS curve to the `.OBJ` file in parameter form. */ - void write_nurbs_curve(FormatHandler &fh, const OBJCurve &obj_nurbs_data) const; + void write_nurbs_curve(FormatHandler &fh, const IOBJCurve &obj_nurbs_data) const; private: using func_vert_uv_normal_indices = void (OBJWriter::*)(FormatHandler &fh, diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc index abb9246b26c..290a8fa7ca4 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.cc @@ -6,112 +6,248 @@ * \ingroup obj */ +#include + #include "BLI_listbase.h" -#include "BLI_math_matrix.h" -#include "BLI_math_rotation.h" -#include "BLI_math_vector.h" -#include "BLI_math_vector_types.hh" +#include "BLI_utility_mixins.hh" #include "BKE_curve_legacy_convert.hh" #include "BKE_curves.hh" +#include "DNA_curve_types.h" #include "DEG_depsgraph.hh" #include "DEG_depsgraph_query.hh" -#include "IO_wavefront_obj.hh" #include "obj_export_nurbs.hh" namespace blender::io::obj { -OBJCurve::OBJCurve(const Depsgraph *depsgraph, - const OBJExportParams &export_params, - Object *curve_object) + +/* -------------------------------------------------------------------- */ +/** \name Utility + * \{ */ + +/* Find the multiplicity entry with the valid span occuring on the right side of the related + * breakpoint/knot. */ +static int find_leftmost_span(const int8_t order, const Span multiplicity) +{ + int index = -1; + int acc = 0; + while (acc < order) { + acc += multiplicity[++index]; + } + BLI_assert(index > -1); + return index; +} + +/* Find the multiplicity entry with the valid span occuring on the left side of the related + * breakpoint/knot. */ +static int find_rightmost_span(const int8_t order, const Span multiplicity) +{ + int index = multiplicity.size(); + int acc = 0; + while (acc < order) { + acc += multiplicity[--index]; + } + BLI_assert(index < multiplicity.size()); + return index; +} + +Span valid_nurb_control_point_range(const int8_t order, + const Span knots, + IndexRange &point_range) +{ + /* No consideration for cyclic, export must expand the knot vector! */ + BLI_assert(knots.size() == bke::curves::nurbs::knots_num(point_range.size(), order, false)); + + /* This assumes multiplicity < order * 2 */ + const int order2 = order * 2; + Vector left_mult = bke::curves::nurbs::calculate_multiplicity_sequence( + knots.slice(0, order2)); + Vector right_mult = bke::curves::nurbs::calculate_multiplicity_sequence( + knots.slice(knots.size() - order2, order2)); + + const int leftmost = find_leftmost_span(order, left_mult); + const int rightmost = find_rightmost_span(order, right_mult); + + /* For reasonable curve knots they should add up to 0 */ + const int acc_start = std::accumulate(left_mult.begin(), left_mult.begin() + leftmost + 1, 0); + const int acc_end = std::accumulate(&right_mult[rightmost], right_mult.end(), 0); + int skip_start = acc_start - order; + int skip_end = acc_end - order; + + /* Update ranges */ + point_range = point_range.drop_front(skip_start).drop_back(skip_end); + return knots.drop_front(skip_start).drop_back(skip_end); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name OBJCurves + * \{ */ + +OBJCurves::OBJCurves(const bke::CurvesGeometry &curve, + const float4x4 &transform, + const std::string &name) + : curve_(curve), transform_(transform), name_(name) +{ +} + +const float4x4 &OBJCurves::object_transform() const +{ + return transform_; +} + +const char *OBJCurves::get_curve_name() const +{ + return name_.c_str(); +} + +int OBJCurves::total_splines() const +{ + return curve_.curve_num; +} + +int OBJCurves::total_spline_vertices(int spline_index) const +{ + return curve_.points_by_curve()[spline_index].size(); +} + +int OBJCurves::num_control_points_u(int spline_index) const +{ + return bke::curves::nurbs::control_points_num(curve_.points_by_curve()[spline_index].size(), + get_nurbs_degree_u(spline_index) + 1, + get_cyclic_u(spline_index)); +} + +int OBJCurves::num_control_points_v(int /*spline_index*/) const +{ + return 1; +} + +int OBJCurves::get_nurbs_degree_u(int spline_index) const +{ + return curve_.nurbs_orders()[spline_index] - 1; +} + +int OBJCurves::get_nurbs_degree_v(int /*spline_index*/) const +{ + return -1; +} + +bool OBJCurves::get_cyclic_u(int spline_index) const +{ + return curve_.cyclic()[spline_index]; +} + +Span OBJCurves::get_knots_u(int spline_index, Vector &knot_buffer) const +{ + const int point_count = curve_.points_by_curve()[spline_index].size(); + const int8_t order = curve_.nurbs_orders()[spline_index]; + const bool cyclic = curve_.cyclic()[spline_index]; + const KnotsMode mode = KnotsMode(curve_.nurbs_knots_modes()[spline_index]); + const int knot_count = bke::curves::nurbs::knots_num(point_count, order, cyclic); + + knot_buffer.resize(knot_count); + bke::curves::nurbs::calculate_knots(point_count, mode, order, cyclic, knot_buffer); + return knot_buffer; +} + +Span OBJCurves::vertex_coordinates(int spline_index, + Vector & /*dynamic_point_buffer*/) const +{ + const IndexRange point_range = curve_.points_by_curve()[spline_index]; + return curve_.positions().slice(point_range); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name OBJLegacyCurve + * \{ */ + +OBJLegacyCurve::OBJLegacyCurve(const Depsgraph *depsgraph, Object *curve_object) : export_object_eval_(curve_object) { export_object_eval_ = DEG_get_evaluated(depsgraph, curve_object); export_curve_ = static_cast(export_object_eval_->data); - set_world_axes_transform(export_params.forward_axis, export_params.up_axis); } -void OBJCurve::set_world_axes_transform(const eIOAxis forward, const eIOAxis up) -{ - float axes_transform[3][3]; - unit_m3(axes_transform); - /* +Y-forward and +Z-up are the Blender's default axis settings. */ - mat3_from_axis_conversion(forward, up, IO_AXIS_Y, IO_AXIS_Z, axes_transform); - mul_m4_m3m4(world_axes_transform_, axes_transform, export_object_eval_->object_to_world().ptr()); - /* #mul_m4_m3m4 does not transform last row of #Object.object_to_world, i.e. location data. */ - mul_v3_m3v3( - world_axes_transform_[3], axes_transform, export_object_eval_->object_to_world().location()); - world_axes_transform_[3][3] = export_object_eval_->object_to_world()[3][3]; -} - -const char *OBJCurve::get_curve_name() const -{ - return export_object_eval_->id.name + 2; -} - -int OBJCurve::total_splines() const -{ - return BLI_listbase_count(&export_curve_->nurb); -} - -const Nurb *OBJCurve::get_spline(const int spline_index) const +const Nurb *OBJLegacyCurve::get_spline(const int spline_index) const { return static_cast(BLI_findlink(&export_curve_->nurb, spline_index)); } -int OBJCurve::total_spline_vertices(const int spline_index) const +const char *OBJLegacyCurve::get_curve_name() const { - const Nurb *const nurb = static_cast(BLI_findlink(&export_curve_->nurb, spline_index)); + return export_object_eval_->id.name + 2; +} + +int OBJLegacyCurve::total_splines() const +{ + return BLI_listbase_count(&export_curve_->nurb); +} + +const float4x4 &OBJLegacyCurve::object_transform() const +{ + return export_object_eval_->object_to_world(); +} + +int OBJLegacyCurve::total_spline_vertices(const int spline_index) const +{ + const Nurb *const nurb = get_spline(spline_index); return nurb->pntsu * nurb->pntsv; } -float3 OBJCurve::vertex_coordinates(const int spline_index, - const int vertex_index, - const float global_scale) const +Span OBJLegacyCurve::vertex_coordinates(const int spline_index, + Vector &dynamic_point_buffer) const { - const Nurb *const nurb = static_cast(BLI_findlink(&export_curve_->nurb, spline_index)); - float3 r_coord; - const BPoint &bpoint = nurb->bp[vertex_index]; - copy_v3_v3(r_coord, bpoint.vec); - mul_m4_v3(world_axes_transform_, r_coord); - mul_v3_fl(r_coord, global_scale); - return r_coord; + const Nurb *const nurb = get_spline(spline_index); + dynamic_point_buffer.resize(nurb->pntsu); + + for (int64_t i = nurb->pntsu - 1; i >= 0; --i) { + const BPoint &bpoint = nurb->bp[i]; + copy_v3_v3(dynamic_point_buffer[i], bpoint.vec); + } + + return dynamic_point_buffer.as_span(); } -int OBJCurve::num_control_points_u(int spline_index) const +int OBJLegacyCurve::num_control_points_u(int spline_index) const { - const Nurb *const nurb = static_cast(BLI_findlink(&export_curve_->nurb, spline_index)); - return nurb->pntsu + (nurb->flagu & CU_NURB_CYCLIC ? get_nurbs_degree_u(spline_index) : 0); + const Nurb *const nurb = get_spline(spline_index); + + return bke::curves::nurbs::control_points_num( + nurb->pntsu, get_nurbs_degree_u(spline_index) + 1, get_cyclic_u(spline_index)); } -int OBJCurve::num_control_points_v(int spline_index) const +int OBJLegacyCurve::num_control_points_v(int spline_index) const { - const Nurb *const nurb = static_cast(BLI_findlink(&export_curve_->nurb, spline_index)); - return nurb->pntsv + (nurb->flagv & CU_NURB_CYCLIC ? get_nurbs_degree_v(spline_index) : 0); + const Nurb *const nurb = get_spline(spline_index); + return nurb->pntsv; } -int OBJCurve::get_nurbs_degree_u(const int spline_index) const +int OBJLegacyCurve::get_nurbs_degree_u(const int spline_index) const { - const Nurb *const nurb = static_cast(BLI_findlink(&export_curve_->nurb, spline_index)); + const Nurb *const nurb = get_spline(spline_index); return nurb->type == CU_POLY ? 1 : nurb->orderu - 1; } -int OBJCurve::get_nurbs_degree_v(const int spline_index) const +int OBJLegacyCurve::get_nurbs_degree_v(const int spline_index) const { - const Nurb *const nurb = static_cast(BLI_findlink(&export_curve_->nurb, spline_index)); + const Nurb *const nurb = get_spline(spline_index); return nurb->type == CU_POLY ? 1 : nurb->orderv - 1; } -short OBJCurve::get_nurbs_flagu(const int spline_index) const +bool OBJLegacyCurve::get_cyclic_u(int spline_index) const { - const Nurb *const nurb = static_cast(BLI_findlink(&export_curve_->nurb, spline_index)); - return nurb->flagu; + const Nurb *const nurb = get_spline(spline_index); + return bool(nurb->flagu & CU_NURB_CYCLIC); } -Span OBJCurve::get_knots_u(int spline_index, Vector &knot_buffer) const +Span OBJLegacyCurve::get_knots_u(int spline_index, Vector &knot_buffer) const { - const Nurb *const nurb = static_cast(BLI_findlink(&export_curve_->nurb, spline_index)); + const Nurb *const nurb = get_spline(spline_index); const short flag = nurb->flagu; const int8_t order = get_nurbs_degree_u(spline_index) + 1; /* Use utility in case of POLY */ const bool cyclic = flag & CU_NURB_CYCLIC; @@ -128,4 +264,6 @@ Span OBJCurve::get_knots_u(int spline_index, Vector &knot_buffer) return knot_buffer; } +/** \} */ + } // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh index e1dc7b3911c..883fb3e234e 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_nurbs.hh @@ -9,73 +9,134 @@ #pragma once #include "BLI_math_vector_types.hh" -#include "BLI_utility_mixins.hh" - -#include "DNA_curve_types.h" - -#include "IO_orientation.hh" +struct Curve; +struct Nurb; struct OBJExportParams; +namespace blender::bke { +class CurvesGeometry; +} + namespace blender::io::obj { /** - * Provides access to the a Curve Object's properties. - * Only #CU_NURBS type is supported. + * Finds the range within the control points that represents the sequence of valid spans or + * 'segments'. * - * \note Used for Curves to be exported in parameter form, and not converted to meshes. + * For example, if a NURBS curve of order 2 has following 5 knots: + * [0, 0, 0, 1, 1] + * associated to three control points. Valid control point range would be + * the interval [1, 2] and the knot sequence [0, 0, 1, 1] since the first + * knot/point does not contribute to any span/segment. */ -class OBJCurve : NonCopyable { - private: - const Object *export_object_eval_; - const Curve *export_curve_; - float world_axes_transform_[4][4]; +Span valid_nurb_control_point_range(int8_t order, + Span knots, + IndexRange &point_range); +/** + * Curve object wrapper providing access to the a Curve Object's properties. + * Curve objects can contain multiple individual splines. + */ +class IOBJCurve { public: - OBJCurve(const Depsgraph *depsgraph, const OBJExportParams &export_params, Object *curve_object); + virtual ~IOBJCurve() = default; - const char *get_curve_name() const; - int total_splines() const; - const Nurb *get_spline(int spline_index) const; + virtual const float4x4 &object_transform() const = 0; + + virtual const char *get_curve_name() const = 0; + + /** + * Number of splines associated with the Curve object.assign_if_different + */ + virtual int total_splines() const = 0; /** * \param spline_index: Zero-based index of spline of interest. * \return Total vertices in a spline. */ - int total_spline_vertices(int spline_index) const; - /** - * Get coordinates of the vertex at the given index on the given spline. - */ - float3 vertex_coordinates(int spline_index, int vertex_index, float global_scale) const; + virtual int total_spline_vertices(int spline_index) const = 0; /** * Get the number of control points on the U-dimension. */ - int num_control_points_u(int spline_index) const; + virtual int num_control_points_u(int spline_index) const = 0; /** * Get the number of control points on the V-dimension. */ - int num_control_points_v(int spline_index) const; + virtual int num_control_points_v(int spline_index) const = 0; /** * Get the degree of the NURBS spline for the U-dimension. */ - int get_nurbs_degree_u(int spline_index) const; + virtual int get_nurbs_degree_u(int spline_index) const = 0; /** * Get the degree of the NURBS spline for the V-dimension. */ - int get_nurbs_degree_v(int spline_index) const; + virtual int get_nurbs_degree_v(int spline_index) const = 0; /** - * Get the U flags (CU_NURB_*) of the NURBS spline at the given index. + * True if the indexed spline is cyclic along U dimension. */ - short get_nurbs_flagu(int spline_index) const; + virtual bool get_cyclic_u(int spline_index) const = 0; /** * Get the knot vector for the U-dimension. Computes knots using the buffer if necessary. */ - Span get_knots_u(int spline_index, Vector &buffer) const; - - private: + virtual Span get_knots_u(int spline_index, Vector &buffer) const = 0; /** - * Set the final transform after applying axes settings and an Object's world transform. + * Get coordinates for the (non-looped) spline control points. */ - void set_world_axes_transform(eIOAxis forward, eIOAxis up); + virtual Span vertex_coordinates(int spline_index, + Vector &dynamic_point_buffer) const = 0; +}; + +class OBJCurves : public IOBJCurve, NonCopyable { + private: + const bke::CurvesGeometry &curve_; + const float4x4 transform_; + const std::string name_; + + public: + OBJCurves(const bke::CurvesGeometry &curve, const float4x4 &transform, const std::string &name); + virtual ~OBJCurves() override = default; + + const float4x4 &object_transform() const override; + + const char *get_curve_name() const override; + + int total_splines() const override; + int total_spline_vertices(int spline_index) const override; + int num_control_points_u(int spline_index) const override; + int num_control_points_v(int spline_index) const override; + int get_nurbs_degree_u(int spline_index) const override; + int get_nurbs_degree_v(int spline_index) const override; + bool get_cyclic_u(int spline_index) const override; + Span get_knots_u(int spline_index, Vector &buffer) const override; + Span vertex_coordinates(int spline_index, + Vector &dynamic_point_buffer) const override; +}; + +class OBJLegacyCurve : public IOBJCurve, NonCopyable { + private: + const Object *export_object_eval_; + const Curve *export_curve_; + + const Nurb *get_spline(int spline_index) const; + + public: + OBJLegacyCurve(const Depsgraph *depsgraph, Object *curve_object); + virtual ~OBJLegacyCurve() override = default; + + const float4x4 &object_transform() const override; + + const char *get_curve_name() const override; + + int total_splines() const override; + int total_spline_vertices(int spline_index) const override; + int num_control_points_u(int spline_index) const override; + int num_control_points_v(int spline_index) const override; + int get_nurbs_degree_u(int spline_index) const override; + int get_nurbs_degree_v(int spline_index) const override; + bool get_cyclic_u(int spline_index) const override; + Span get_knots_u(int spline_index, Vector &buffer) const override; + Span vertex_coordinates(int spline_index, + Vector &dynamic_point_buffer) const override; }; } // namespace blender::io::obj diff --git a/source/blender/io/wavefront_obj/exporter/obj_exporter.cc b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc index a900ca0c445..3b23c1cbd12 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_exporter.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc @@ -10,6 +10,9 @@ #include #include +#include "DNA_curve_enums.h" +#include "DNA_curve_types.h" + #include "BKE_context.hh" #include "BKE_lib_id.hh" #include "BKE_report.hh" @@ -103,11 +106,11 @@ static bool is_curve_nurbs_compatible(const Nurb *nurb) * * \note Curves are also stored with Meshes if export settings specify so. */ -std::pair>, Vector>> +std::pair>, Vector>> filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params) { Vector> r_exportable_meshes; - Vector> r_exportable_nurbs; + Vector> r_exportable_nurbs; DEGObjectIterSettings deg_iter_settings{}; deg_iter_settings.depsgraph = depsgraph; deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY | @@ -130,14 +133,15 @@ filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_par if (!nurb) { /* An empty curve. Not yet supported to export these as meshes. */ if (export_params.export_curves_as_nurbs) { - r_exportable_nurbs.append( - std::make_unique(depsgraph, export_params, object)); + IOBJCurve *obj_curve = new OBJLegacyCurve(depsgraph, object); + r_exportable_nurbs.append(std::unique_ptr(obj_curve)); } break; } if (export_params.export_curves_as_nurbs && is_curve_nurbs_compatible(nurb)) { /* Export in parameter form: control points. */ - r_exportable_nurbs.append(std::make_unique(depsgraph, export_params, object)); + IOBJCurve *obj_curve = new OBJLegacyCurve(depsgraph, object); + r_exportable_nurbs.append(std::unique_ptr(obj_curve)); } else { /* Export in mesh form: edges and vertices. */ @@ -257,13 +261,13 @@ static void write_mesh_objects(const Span> exportable_a /** * Export NURBS Curves in parameter form, not as vertices and edges. */ -static void write_nurbs_curve_objects(const Span> exportable_as_nurbs, +static void write_nurbs_curve_objects(const Span> exportable_as_nurbs, const OBJWriter &obj_writer) { FormatHandler fh; /* #OBJCurve doesn't have any dynamically allocated memory, so it's fine * to wait for #blender::Vector to clean the objects up. */ - for (const std::unique_ptr &obj_curve : exportable_as_nurbs) { + for (const std::unique_ptr &obj_curve : exportable_as_nurbs) { obj_writer.write_nurbs_curve(fh, *obj_curve); } fh.write_to_file(obj_writer.get_outfile()); @@ -320,10 +324,10 @@ static void write_materials(MTLWriter *mtl_writer, const OBJExportParams &export export_params.export_pbr_extensions); } -static void export_objects(const OBJExportParams &export_params, - const Span> meshes, - const Span> curves, - const char *filepath) +void export_objects(const OBJExportParams &export_params, + const Span> meshes, + const Span> curves, + const char *filepath) { /* Open */ std::unique_ptr obj_writer; diff --git a/source/blender/io/wavefront_obj/exporter/obj_exporter.hh b/source/blender/io/wavefront_obj/exporter/obj_exporter.hh index 1030ccba2e7..d12108951cb 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_exporter.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.hh @@ -48,7 +48,7 @@ class OBJDepsgraph : NonMovable, NonCopyable { void exporter_main(bContext *C, const OBJExportParams &export_params); class OBJMesh; -class OBJCurve; +class IOBJCurve; /** * Export a single frame of a `.obj` file, according to the given `export_parameters`. @@ -65,6 +65,11 @@ void export_frame(Depsgraph *depsgraph, const OBJExportParams &export_params, const char *filepath); +void export_objects(const OBJExportParams &export_params, + Span> meshes, + Span> curves, + const char *filepath); + /** * Find the objects to be exported in the `view_layer` of the dependency graph`depsgraph`, * and return them in vectors `unique_ptr`s of `OBJMesh` and `OBJCurve`. @@ -76,7 +81,7 @@ void export_frame(Depsgraph *depsgraph, * `OBJMesh` vector. All other exportable types are always converted to mesh and returned in the * `OBJMesh` vector. */ -std::pair>, Vector>> +std::pair>, Vector>> filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params); /** diff --git a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc index ea529282d39..488f7a2fa3e 100644 --- a/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc +++ b/source/blender/io/wavefront_obj/importer/obj_import_nurbs.cc @@ -17,6 +17,7 @@ #include "DNA_curve_types.h" #include "IO_wavefront_obj.hh" + #include "importer_mesh_utils.hh" #include "obj_import_nurbs.hh" #include "obj_import_objects.hh" @@ -83,15 +84,18 @@ static int8_t get_valid_nurbs_degree(const NurbsElement &element) } /** - * Get the number of control points repeated for a cyclic curve given the multiplicity at the end - * of the knot vectors (multiplicity at both ends need to match). + * Get the number of control points repeated for a cyclic curve given the multiplicity found + * at the endpoints (assumes cyclic curve). */ -static int cyclic_repeated_points(const int8_t order, const int end_multiplicity) +static int repeating_cyclic_point_num(const int8_t order, const Span knots) { - /* Since multiplicity == order is considered 'cyclic' even if it is only C0 continuous in - * principle (it is technically discontinuous!), it needs to be clamped to 1. + /* Due to the additional start knot, drop first. */ - return std::max(order - end_multiplicity, 1); + Vector multiplicity = bke::curves::nurbs::calculate_multiplicity_sequence( + knots.slice(1, order - 1)); + + BLI_assert(order > multiplicity.first()); + return order - multiplicity.first(); } void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import_params) @@ -116,14 +120,8 @@ void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import nurb->flagu = this->detect_knot_mode( import_params, degree, nurbs_geometry.curv_indices, nurbs_geometry.parm, multiplicity); - if ((nurb->flagu & (CU_NURB_CUSTOM | CU_NURB_CYCLIC | CU_NURB_ENDPOINT)) == CU_NURB_CUSTOM) { - /* TODO: If mode is CU_NURB_CUSTOM, but not CU_NURB_CYCLIC and CU_NURB_ENDPOINT, then make - * curve clamped instead of removing CU_NURB_CUSTOM. */ - nurb->flagu &= ~CU_NURB_CUSTOM; - } - const int repeated_points = nurb->flagu & CU_NURB_CYCLIC ? - cyclic_repeated_points(nurb->orderu, multiplicity.first()) : + repeating_cyclic_point_num(nurb->orderu, nurbs_geometry.parm) : 0; const Span indices = nurbs_geometry.curv_indices.as_span().slice( nurbs_geometry.curv_indices.index_range().drop_back(repeated_points)); @@ -140,7 +138,8 @@ void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import if (nurb->flagu & CU_NURB_CUSTOM) { BKE_nurb_knot_alloc_u(nurb); - array_utils::copy(nurbs_geometry.parm, MutableSpan{nurb->knotsu, KNOTSU(nurb)}); + MutableSpan knots_dst_u{nurb->knotsu, KNOTSU(nurb)}; + array_utils::copy(nurbs_geometry.parm, knots_dst_u); } else { BKE_nurb_knot_calc_u(nurb); @@ -150,36 +149,35 @@ void CurveFromGeometry::create_nurbs(Curve *curve, const OBJImportParams &import static bool detect_clamped_endpoint(const int8_t degree, const Span multiplicity) { const int8_t order = degree + 1; - return multiplicity.first() == order && multiplicity.last() == order; + /* Consider any combination of following patterns as clamped: + * + * O .. + * 1 d .. + */ + const bool begin_clamped = multiplicity.first() == order || + (multiplicity.first() == 1 && multiplicity[1] == degree); + const bool end_clamped = multiplicity.last() == order || + (multiplicity.last() == 1 && multiplicity.last(1) == degree); + return begin_clamped && end_clamped; +} + +static bool almost_equal_relative(const float a, const float b, const float epsilon) +{ + const float abs_diff = std::abs(b - a); + return abs_diff < a * epsilon; } static bool detect_knot_mode_cyclic(const int8_t degree, const Span indices, const Span knots, - const Span multiplicity) + const Span multiplicity, + const bool is_clamped) { - constexpr float epsilon = 1e-7; + constexpr float epsilon = 1e-4; const int8_t order = degree + 1; - /* This is a good distinction between the 'cyclic' property and a true periodic NURBS curve. A - * periodic curve should be smooth to the degree - 1 derivative (which is the maximum possible). - * Allowing matching `multiplicity > 1` is not a periodic NURBS but can be considered cyclic. - */ - if (multiplicity.first() != multiplicity.last()) { - return false; - } - - /* Multiplicity m is continuous to the `degree - m` derivative and as such - * `multiplicity == order` is discontinuous. - * By allowing it, clamped or Bezier curves can still be considered cyclic but - * ensure [here] that illogical `multiplicities > order` is not considered cyclic. - */ - if (multiplicity.first() > order || multiplicity.last() > order) { - - return false; - } - - const int repeated_points = cyclic_repeated_points(order, multiplicity.first()); + const int repeated_points = repeating_cyclic_point_num(order, knots); + BLI_assert(repeated_points > 0); const Span indices_tail = indices.take_back(repeated_points); for (const int64_t i : indices_tail.index_range()) { if (indices[i] != indices_tail[i]) { @@ -187,32 +185,88 @@ static bool detect_knot_mode_cyclic(const int8_t degree, } } - if (multiplicity.first() >= degree) { - /* There is no overlap in the knot spans. */ + /* Multiplicity m is continuous to the `degree - m` derivative and as such + * `multiplicity == degree` is discontinuous. Due to the superfluous knots + * the first/last entry can be up to `order`, remaining up to `degree`. + */ + if (multiplicity.first() > order || multiplicity.last() > order) { + return false; + } + for (const int m : multiplicity.drop_front(1).drop_back(1)) { + if (m > degree) { + return false; + } + } + + if (is_clamped) { + /* Clamped curves are discontinous at the ends and have no overlapping spans. */ return true; } /* Ensure it matches on both of the knot spans adjacent to the start/end of the parameter range. */ - const Span knots_tail = knots.take_back(order + degree); + const Span knots_tail = knots.take_back(2 * degree + 1); for (const int64_t i : knots_tail.index_range().drop_back(1)) { const float head_span = knots[i + 1] - knots[i]; const float tail_span = knots_tail[i + 1] - knots_tail[i]; - if (abs(head_span - tail_span) > epsilon) { + if (!almost_equal_relative(head_span, tail_span, epsilon)) { return false; } } return true; } -static bool detect_knot_mode_bezier(const int8_t degree, const Span multiplicity) +static bool detect_knot_mode_bezier_clamped(const int8_t degree, + const int num_points, + const Span multiplicity) { const int8_t order = degree + 1; - if (multiplicity.first() != order || multiplicity.last() != order) { + /* Don't treat polylines as Beziers. */ + if (order == 2) { return false; } - for (const int m : multiplicity.drop_front(1).drop_back(1)) { + /* Allow patterns: + O d .. + 1 d d .. + */ + if (multiplicity[0] < order && (multiplicity[0] != 1 || multiplicity[1] < degree)) { + return false; + } + + Span mdegree_span = multiplicity.drop_front(1); + if (multiplicity.size() == 2) { + /* Single segment, allow patterns: + * O a + * where a > 0 + */ + if (multiplicity.first() != order) { + return false; + } + } + else { + /* Allow patterns: + .. d O+ + .. d d 1 + */ + if (multiplicity.last() != order && + (multiplicity.last() == 1 && multiplicity.last(1) != degree)) + { + /* No match to the valid patterns. */ + return false; + } + + const int remainder = (num_points - 1) % degree; + if (multiplicity.last() != order + remainder && + (multiplicity.last() != 1 || multiplicity.last(1) < degree)) + { + return false; + } + } + mdegree_span = mdegree_span.drop_back(1); + + /* Verify all other knots are of degree multiplicity */ + for (const int m : mdegree_span) { if (m != degree) { return false; } @@ -223,28 +277,34 @@ static bool detect_knot_mode_bezier(const int8_t degree, const Span multipl static bool detect_knot_mode_uniform(const int8_t degree, const Span knots, const Span multiplicity, - bool clamped) + const bool clamped) { - constexpr float epsilon = 1e-7; + constexpr float epsilon = 1e-4; /* Check if knot count matches multiplicity adjusted for clamped ends. For a uniform non-clamped * curve, all multiplicity entries equals 1 and the array size should match. */ - const int clamped_offset = clamped * degree; - if (knots.size() != multiplicity.size() - 2 * clamped_offset) { + const int O1_clamps = int(multiplicity.first() == 1) + int(multiplicity.last() == 1); + const int clamped_offset = clamped ? 2 * degree - O1_clamps : 0; + if (knots.size() != multiplicity.size() + clamped_offset) { return false; } /* Ensure it's not a single segment with clamped ends (it would be a Bezier segment). */ const Span unclamped_knots = knots.drop_front(clamped_offset).drop_back(clamped_offset); - if (!unclamped_knots.size()) { + if (unclamped_knots.size() == 2) { return false; } + if (unclamped_knots.size() < 2) { + /* Classify single point as uniform? */ + return true; + } /* Verify spacing is uniform (excluding clamped ends). */ const float uniform_delta = unclamped_knots[1] - unclamped_knots[0]; - for (const int64_t i : knots.index_range().drop_front(2)) { - if (abs((knots[i] - knots[i - 1]) - uniform_delta) < epsilon) { + for (const int64_t i : unclamped_knots.index_range().drop_front(2)) { + const float delta = unclamped_knots[i] - unclamped_knots[i - 1]; + if (!almost_equal_relative(delta, uniform_delta, epsilon)) { return false; } } @@ -259,30 +319,27 @@ short CurveFromGeometry::detect_knot_mode(const OBJImportParams &import_params, { short knot_mode = 0; - if (import_params.close_spline_loops && indices.size() > degree) { - SET_FLAG_FROM_TEST( - knot_mode, detect_knot_mode_cyclic(degree, indices, knots, multiplicity), CU_NURB_CYCLIC); - } + const bool is_clamped = detect_clamped_endpoint(degree, multiplicity); - if (detect_knot_mode_bezier(degree, multiplicity)) { - /* Currently endpoint flag is not parsed for Bezier, mainly because a clamped Bezier curve in - * Blender is either: - * a) A valid Bezier curve for given degree/order with correct number of points to form one. - * b) Not a valid Bezier curve for given degree, last span/segment is of a lower degree Bezier. - * - * Set ENDPOINT to true since legacy Bezier NURBS only validates and compute knots if it - * contains order + 1 control points unless endpoint is set...? - */ + const bool is_bezier = detect_knot_mode_bezier_clamped(degree, indices.size(), multiplicity); + if (is_bezier) { SET_FLAG_FROM_TEST(knot_mode, true, CU_NURB_ENDPOINT); SET_FLAG_FROM_TEST(knot_mode, true, CU_NURB_BEZIER); } else { - const bool clamped = detect_clamped_endpoint(degree, multiplicity); - SET_FLAG_FROM_TEST(knot_mode, clamped, CU_NURB_ENDPOINT); - SET_FLAG_FROM_TEST(knot_mode, - !detect_knot_mode_uniform(degree, knots, multiplicity, clamped), - CU_NURB_CUSTOM); + const bool is_uniform = detect_knot_mode_uniform(degree, knots, multiplicity, is_clamped); + SET_FLAG_FROM_TEST(knot_mode, is_clamped, CU_NURB_ENDPOINT); + SET_FLAG_FROM_TEST(knot_mode, !is_uniform, CU_NURB_CUSTOM); } + + const bool check_cyclic = import_params.close_spline_loops && indices.size() > degree; + const bool no_custom_cyclic = knot_mode & CU_NURB_CUSTOM; + if (check_cyclic && !no_custom_cyclic) { + const bool is_cyclic = detect_knot_mode_cyclic( + degree, indices, knots, multiplicity, is_clamped); + SET_FLAG_FROM_TEST(knot_mode, is_cyclic, CU_NURB_CYCLIC); + } + return knot_mode; } diff --git a/source/blender/io/wavefront_obj/tests/obj_nurbs_io_tests.cc b/source/blender/io/wavefront_obj/tests/obj_nurbs_io_tests.cc new file mode 100644 index 00000000000..3127e31a5d9 --- /dev/null +++ b/source/blender/io/wavefront_obj/tests/obj_nurbs_io_tests.cc @@ -0,0 +1,586 @@ +/* SPDX-FileCopyrightText: 2023-2025 Blender Authors + * + * SPDX-License-Identifier: Apache-2.0 */ +#include "BLI_array_utils.hh" +#include "BLI_string.h" + +#include "BKE_appdir.hh" +#include "BKE_curves.hh" +#include "BKE_geometry_compare.hh" +#include "BKE_idtype.hh" + +#include "obj_export_file_writer.hh" +#include "obj_export_nurbs.hh" +#include "obj_exporter.hh" +#include "obj_importer.hh" + +#include "CLG_log.h" +#include "testing/testing.h" + +namespace blender::io::obj { + +static OBJExportParams default_export_params(const std::string &filepath) +{ + OBJExportParams params; + params.forward_axis = eIOAxis::IO_AXIS_Y; + params.up_axis = eIOAxis::IO_AXIS_Z; + STRNCPY(params.filepath, filepath.c_str()); + return params; +} + +static OBJImportParams default_import_params(const std::string &filepath) +{ + OBJImportParams params; + params.forward_axis = eIOAxis::IO_AXIS_Y; + params.up_axis = eIOAxis::IO_AXIS_Z; + STRNCPY(params.filepath, filepath.c_str()); + return params; +} + +class OBJCurvesTest : public testing::Test { + public: + static void SetUpTestSuite() + { + /* BKE_id_free() hits a code path that uses CLOG, which crashes if not initialized properly. */ + CLG_init(); + + /* Might not be necessary but...*/ + BKE_idtype_init(); + } + + static void TearDownTestSuite() + { + CLG_exit(); + } + + void write_curves(const Span> curves, OBJExportParams params) + { + export_objects(params, Span>(nullptr, 0), curves, params.filepath); + } + + void write_curves(const std::unique_ptr &curve, OBJExportParams params) + { + Span> span(&curve, 1); + write_curves(span, params); + } + + void write_curves(const bke::CurvesGeometry &curve, OBJExportParams params) + { + float4x4 identity = float4x4::identity(); + std::unique_ptr curve_wrapper(new OBJCurves(curve, identity, "test")); + write_curves(curve_wrapper, params); + } + + Vector read_curves(OBJImportParams params) + { + Vector geoms; + importer_geometry(params, geoms); + return geoms; + } + + static bke::CurvesGeometry create_curves(Span points, bool cyclic) + { + bke::CurvesGeometry curves(points.size(), 1); + curves.offsets_for_write()[0] = 0; + curves.offsets_for_write()[1] = points.size(); + curves.cyclic_for_write()[0] = cyclic; + curves.positions_for_write().copy_from(points); + return curves; + } + + static bke::CurvesGeometry create_rational_nurbs( + Span points, Span weights, bool cyclic, int8_t order, KnotsMode mode) + { + bke::CurvesGeometry curves = create_curves(points, cyclic); + curves.nurbs_orders_for_write()[0] = order; + curves.nurbs_knots_modes_for_write()[0] = int8_t(mode); + curves.nurbs_weights_for_write().copy_from(weights); + + return curves; + } + + static bke::CurvesGeometry create_nurbs(Span points, + bool cyclic, + int8_t order, + KnotsMode mode) + { + bke::CurvesGeometry curves = create_curves(points, cyclic); + curves.nurbs_orders_for_write()[0] = order; + curves.nurbs_knots_modes_for_write()[0] = int8_t(mode); + curves.nurbs_weights_for_write().fill(1.0f); + + return curves; + } + + void run_nurbs_test(const Span points, + const int8_t order, + const KnotsMode mode, + const bool cyclic, + bke::CurvesGeometry &src_curve, + const bke::CurvesGeometry *&result_curve, + Span expected_points = Span(), + const KnotsMode *expected_mode = nullptr, + const bool *expected_cyclic = nullptr) + { + BKE_tempdir_init(nullptr); + std::string tempdir = std::string(BKE_tempdir_base()); + std::string out_file_path = tempdir + BLI_path_basename("io_obj/tmp_6f5273f4.obj"); + + /* Write/Read */ + src_curve = OBJCurvesTest::create_nurbs(points, cyclic, order, mode); + ASSERT_TRUE(src_curve.cyclic()[0] == cyclic); /* Validate test function */ + + write_curves(src_curve, default_export_params(out_file_path)); + + Vector result = read_curves(default_import_params(out_file_path)); + + ASSERT_TRUE(result.size() == 1); + result_curve = &result[0].get_curves()->geometry.wrap(); + + /* Validate properties */ + EXPECT_EQ(result_curve->nurbs_orders()[0], order); + EXPECT_EQ(result_curve->cyclic()[0], expected_cyclic ? *expected_cyclic : cyclic); + EXPECT_EQ(result_curve->nurbs_knots_modes()[0], int8_t(expected_mode ? *expected_mode : mode)); + + const Span result_points = result_curve->positions(); + expected_points = expected_points.size() ? expected_points : points; + ASSERT_EQ(expected_points.size(), result_points.size()); + EXPECT_NEAR_ARRAY_ND( + expected_points.data(), result_points.data(), expected_points.size(), 3, 1e-4); + + if (result_curve->nurbs_knots_modes()[0] != KnotsMode::NURBS_KNOT_MODE_CUSTOM) { + ASSERT_TRUE(result_curve->custom_knots == NULL); + } + } +}; + +const std::array position_array{float3{1.0f, -1.0f, 2.0f}, + float3{2.0f, -2.0f, 4.0f}, + float3{3.0f, -3.0f, 6.0f}, + float3{4.0f, -4.0f, 8.0f}, + float3{5.0f, -5.0f, 10.0f}, + float3{6.0f, -6.0f, 12.0f}, + float3{7.0f, -7.0f, 14.0f}, + float3{1.0f / 4.0f, -2.0f, 3.0f / 6.0f}, + float3{1.0f / 6.0f, -3.0f, 3.0f / 9.0f}, + float3{1.0f / 8.0f, -4.0f, 3.0f / 12.0f}, + float3{1.0f / 5.0f, -5.0f, 3.0f / 11.0f}, + float3{1.0f / 3.0f, -6.0f, 3.0f / 10.0f}, + float3{1.0f / 2.0f, -7.0f, 3.0f / 9.0f}}; +const Span position_data = Span(position_array); + +/* -------------------------------------------------------------------- */ +/** \name Knot vector: KnotMode::NURBS_KNOT_MODE_NORMAL + * \{ */ + +TEST_F(OBJCurvesTest, nurbs_io_uniform_polyline) +{ + const int8_t order = 2; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL; + const bool cyclic = false; + const Span positions = position_data.slice(0, 5); + + const KnotsMode expected_mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, cyclic, src, result, positions, &expected_mode); + + /* Validate uniform knots, don't do this in general as it only verifies the knot generator + * `bke::curves::nurbs::calculate_knots`. */ + Vector knot_buffer(bke::curves::nurbs::knots_num(positions.size(), order, cyclic)); + bke::curves::nurbs::calculate_knots(positions.size(), mode, order, cyclic, knot_buffer); + const Vector multiplicity = bke::curves::nurbs::calculate_multiplicity_sequence( + knot_buffer); + + std::array expected_mult; + std::fill(expected_mult.begin(), expected_mult.end(), 1); + EXPECT_EQ_SPAN(multiplicity, expected_mult); +} + +TEST_F(OBJCurvesTest, nurbs_io_uniform_deg5) +{ + const int8_t order = 6; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL; + const Span positions = position_data.slice(0, 8); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, false, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_uniform_clamped_polyline) +{ + const int8_t order = 2; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT; + const Span positions = position_data.slice(0, 5); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, false, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_uniform_endpoint_clamped_deg3) +{ + const int8_t order = 3; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT; + const Span positions = position_data.slice(0, 5); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, false, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_uniform_endpoint_clamped_deg5) +{ + const int8_t order = 6; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL; + const Span positions = position_data.slice(0, 8); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, false, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_uniform_cyclic_polyline) +{ + const int8_t order = 2; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL; + const Span positions = position_data.slice(0, 5); + + const KnotsMode expected_mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, true, src, result, positions, &expected_mode); +} + +TEST_F(OBJCurvesTest, nurbs_io_uniform_cyclic_deg4) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_NORMAL; + const Span positions = position_data.slice(0, 8); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, true, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_uniform_cyclic_clamped_deg4) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT; + const Span positions = position_data.slice(0, 12); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, true, src, result); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Knot vector: KnotMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER + * \{ */ + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_single_segment_deg2) +{ + const int8_t order = 3; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 3); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, false, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_single_segment_deg4) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 5); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, false, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_deg2) +{ + const int8_t order = 3; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 7); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, false, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_uneven_deg2) +{ + const int8_t order = 3; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 8); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, false, src, result, positions.slice(0, 7)); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_deg4) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 13); + + /* Even (whole Bezier segments). */ + { + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, false, src, result); + } + + { + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions.slice(0, 9), order, mode, false, src, result); + } + + /* Uneven (incomplete segment). */ + + { + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions.slice(0, 12), order, mode, false, src, result, positions.slice(0, 9)); + } + + { + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions.slice(0, 11), order, mode, false, src, result, positions.slice(0, 9)); + } + + { + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions.slice(0, 10), order, mode, false, src, result, positions.slice(0, 9)); + } +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_looped_12) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 12); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions.slice(0, 12), order, mode, true, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_looped_8) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 8); + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, true, src, result); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_discontinous_13) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data; + + Vector expected(positions); + expected.append(positions[0]); + const bool expect_cyclic = false; + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + + run_nurbs_test( + positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_discontinous_11) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 11); + + Vector expected(positions); + expected.append(positions[0]); + const bool expect_cyclic = false; + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + + run_nurbs_test( + positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_discontinous_10) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 10); + + Vector expected(positions); + expected.append(positions[0]); + const bool expect_cyclic = false; + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + + run_nurbs_test( + positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_clamped_cyclic_deg4_discontinous_9) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + const Span positions = position_data.slice(0, 9); + + Vector expected(positions); + expected.append(positions[0]); + const bool expect_cyclic = false; + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + + run_nurbs_test( + positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Knot vector: KnotMode::NURBS_KNOT_MODE_BEZIER + * \{ */ + +TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_12) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER; + const Span positions = position_data.slice(0, 12); + + Vector expected(positions.size()); + array_utils::copy(positions.slice(1, 11), expected.as_mutable_span().slice(0, 11)); + expected.last() = positions.first(); + + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, true, src, result, expected, &expect_mode); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_discontinous_13) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER; + const Span positions = position_data.slice(0, 13); + + Vector expected(positions.size() + 1); + array_utils::copy(positions.slice(1, 12), expected.as_mutable_span().slice(0, 12)); + expected.last(1) = positions.first(); + expected.last() = positions[1]; + + const bool expect_cyclic = false; + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test( + positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_discontinous_11) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER; + const Span positions = position_data.slice(0, 11); + + Vector expected(positions.size() + 1); + array_utils::copy(positions.slice(1, 10), expected.as_mutable_span().slice(0, 10)); + expected.last(1) = positions.first(); + expected.last() = positions[1]; + + const bool expect_cyclic = false; + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test( + positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_discontinous_10) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER; + const Span positions = position_data.slice(0, 10); + + Vector expected(positions.size() + 1); + array_utils::copy(positions.slice(1, 9), expected.as_mutable_span().slice(0, 9)); + expected.last(1) = positions.first(); + expected.last() = positions[1]; + + const bool expect_cyclic = false; + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test( + positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_discontinous_9) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER; + const Span positions = position_data.slice(0, 9); + + Vector expected(positions.size() + 1); + array_utils::copy(positions.slice(1, 8), expected.as_mutable_span().slice(0, 8)); + expected.last(1) = positions.first(); + expected.last() = positions[1]; + + const bool expect_cyclic = false; + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_CUSTOM; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test( + positions, order, mode, true, src, result, expected, &expect_mode, &expect_cyclic); +} + +TEST_F(OBJCurvesTest, nurbs_io_bezier_cyclic_deg4_looped_8) +{ + const int8_t order = 5; + const KnotsMode mode = KnotsMode::NURBS_KNOT_MODE_BEZIER; + const Span positions = position_data.slice(0, 8); + + Vector expected(positions.size()); + array_utils::copy(positions.slice(1, 7), expected.as_mutable_span().slice(0, 7)); + expected.last() = positions.first(); + + const KnotsMode expect_mode = KnotsMode::NURBS_KNOT_MODE_ENDPOINT_BEZIER; + + bke::CurvesGeometry src; + const bke::CurvesGeometry *result; + run_nurbs_test(positions, order, mode, true, src, result, expected, &expect_mode); +} + +/** \} */ + +} // namespace blender::io::obj diff --git a/tests/files/io_tests/obj/all_curves_as_nurbs.obj b/tests/files/io_tests/obj/all_curves_as_nurbs.obj index b97065949f2..17eaf182774 100644 --- a/tests/files/io_tests/obj/all_curves_as_nurbs.obj +++ b/tests/files/io_tests/obj/all_curves_as_nurbs.obj @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2098e5dce32abb74d49e49f71479ecdd7a9acfe4f86b699e36584c4103fc4ba +oid sha256:ebd281e858741858a27ab91e3a412f324dd5bcdb5f787253c3443679a8fe2a9e size 6709 diff --git a/tests/files/io_tests/obj/nurbs.obj b/tests/files/io_tests/obj/nurbs.obj index e891d2828d1..ba182eb509f 100644 --- a/tests/files/io_tests/obj/nurbs.obj +++ b/tests/files/io_tests/obj/nurbs.obj @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d94e28f7ac251b17489a1d2c640f9cdb16f74de2646d952c3078eadcfeab415 +oid sha256:077cd98962aac174aee1e418b6cc63e42677ac7387b35d080b979a271c007762 size 569 diff --git a/tests/files/io_tests/obj/nurbs_curves.obj b/tests/files/io_tests/obj/nurbs_curves.obj index 92e961c40ef..b6d118c93d5 100644 --- a/tests/files/io_tests/obj/nurbs_curves.obj +++ b/tests/files/io_tests/obj/nurbs_curves.obj @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2373d7d933d3b0caa7f9625ae15e54daed3afcedf5227d43fe726ec27a2346a8 +oid sha256:7830f94918238f0ab45aad38d92bb4cb466c2961ba55f1a9a2f383f9a73670e3 size 1466 diff --git a/tests/files/io_tests/obj/nurbs_manual.obj b/tests/files/io_tests/obj/nurbs_manual.obj index 2044a05993a..324378264f8 100644 --- a/tests/files/io_tests/obj/nurbs_manual.obj +++ b/tests/files/io_tests/obj/nurbs_manual.obj @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c66f333b6cbfd68bf1d3b3895f31c990d5feb2edf5c28d786ac0cfef7aa07ed -size 1018 +oid sha256:638893b7762835eb9a60df7e1cbd80da7068a0f9d5dc77fea4ee6c1c28a98910 +size 1031 diff --git a/tests/files/io_tests/obj/reference/all_curves_as_nurbs.txt b/tests/files/io_tests/obj/reference/all_curves_as_nurbs.txt index e4e78c9592b..e06f735a24a 100644 --- a/tests/files/io_tests/obj/reference/all_curves_as_nurbs.txt +++ b/tests/files/io_tests/obj/reference/all_curves_as_nurbs.txt @@ -57,58 +57,58 @@ ==== Curves: 7 - Curve 'NurbsCircle' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:8x1 order:4x4 cyclic:True,False endp:False,False - - (12.463, 0.000, 1.000) w:1.000 - - (12.463, 0.000, 0.000) w:1.000 - - (12.463, 0.000, -1.000) w:1.000 - ... - - (10.463, 0.000, 0.000) w:1.000 - - (10.463, 0.000, 1.000) w:1.000 - (11.463, 0.000, 1.000) w:1.000 + - (10.463, 0.000, 1.000) w:1.000 + - (10.463, 0.000, 0.000) w:1.000 + ... + - (12.463, 0.000, -1.000) w:1.000 + - (12.463, 0.000, 0.000) w:1.000 + - (12.463, 0.000, 1.000) w:1.000 - Curve 'NurbsCurve' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:False,False endp:False,False - - (9.947, 0.000, 0.000) w:1.000 - - (9.447, 0.000, -1.000) w:1.000 - - (7.447, 0.000, -1.000) w:1.000 - (7.143, 0.000, 2.752) w:1.000 + - (7.447, 0.000, -1.000) w:1.000 + - (9.447, 0.000, -1.000) w:1.000 + - (9.947, 0.000, 0.000) w:1.000 - Curve 'NurbsCurve2' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:7x1 order:4x4 cyclic:False,False endp:False,False - - (9.772, 0.000, 6.791) w:1.000 - - (9.873, 0.000, 3.403) w:1.000 - - (8.725, 0.000, 2.825) w:1.000 - ... - - (6.842, 0.000, 2.942) w:1.000 - - (5.304, 0.000, 5.296) w:1.000 - (7.084, 0.000, 7.142) w:1.000 + - (5.304, 0.000, 5.296) w:1.000 + - (6.842, 0.000, 2.942) w:1.000 + ... + - (8.725, 0.000, 2.825) w:1.000 + - (9.873, 0.000, 3.403) w:1.000 + - (9.772, 0.000, 6.791) w:1.000 - Curve 'NurbsPathCurve' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:5x1 order:4x4 cyclic:False,False endp:True,False - - (17.691, 0.000, 0.000) w:1.000 - - (16.632, 0.000, 1.854) w:1.000 - - (15.691, 0.000, 0.000) w:1.000 - - (14.671, 0.000, 1.288) w:1.000 - (13.691, 0.000, 0.000) w:1.000 + - (14.671, 0.000, 1.288) w:1.000 + - (15.691, 0.000, 0.000) w:1.000 + - (16.632, 0.000, 1.854) w:1.000 + - (17.691, 0.000, 0.000) w:1.000 - Curve 'NurbsPathCurve.001' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:False,False endp:False,False - - (17.115, 0.000, 2.127) w:1.000 - - (16.693, 0.000, -1.000) w:1.000 - - (14.693, 0.000, -1.000) w:1.000 - (14.193, 0.000, 0.000) w:1.000 + - (14.693, 0.000, -1.000) w:1.000 + - (16.693, 0.000, -1.000) w:1.000 + - (17.115, 0.000, 2.127) w:1.000 - Curve 'PolyCircle' dim:3D resu:12 resv:12 splines:1 - - spline type:NURBS pts:4x1 order:2x2 cyclic:True,False endp:False,False - - (4.090, 0.000, -3.488) w:1.000 - - (5.090, 0.000, -4.488) w:1.000 - - (4.090, 0.000, -5.488) w:1.000 + - spline type:NURBS pts:4x1 order:2x2 cyclic:True,False endp:True,False - (3.090, 0.000, -4.488) w:1.000 + - (4.090, 0.000, -5.488) w:1.000 + - (5.090, 0.000, -4.488) w:1.000 + - (4.090, 0.000, -3.488) w:1.000 - Curve 'PolyCurve' dim:3D resu:12 resv:12 splines:1 - - spline type:NURBS pts:3x1 order:2x2 cyclic:False,False endp:False,False - - (1.000, 0.000, -4.488) w:1.000 - - (0.371, 0.000, -5.659) w:1.000 + - spline type:NURBS pts:3x1 order:2x2 cyclic:False,False endp:True,False - (-1.000, 0.000, -4.488) w:1.000 + - (0.371, 0.000, -5.659) w:1.000 + - (1.000, 0.000, -4.488) w:1.000 ==== Objects: 12 - Obj 'BezierCircle' MESH data:'BezierCircle' diff --git a/tests/files/io_tests/obj/reference/nurbs.txt b/tests/files/io_tests/obj/reference/nurbs.txt index 19c08bb7e8a..a0b1e31b5db 100644 --- a/tests/files/io_tests/obj/reference/nurbs.txt +++ b/tests/files/io_tests/obj/reference/nurbs.txt @@ -1,13 +1,13 @@ ==== Curves: 1 - Curve 'nurbs' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:9x1 order:4x4 cyclic:True,False endp:False,False - - (0.260, -1.477, -0.866) w:1.000 - - (-1.410, -0.513, 0.866) w:1.000 - - (-1.500, 2.598, 0.000) w:1.000 - ... - - (-1.410, 0.513, -0.866) w:1.000 - - (0.260, 1.477, 0.866) w:1.000 - (3.000, 0.000, 0.000) w:1.000 + - (0.260, 1.477, 0.866) w:1.000 + - (-1.410, 0.513, -0.866) w:1.000 + ... + - (-1.500, 2.598, 0.000) w:1.000 + - (-1.410, -0.513, 0.866) w:1.000 + - (0.260, -1.477, -0.866) w:1.000 ==== Objects: 1 - Obj 'nurbs' CURVE data:'nurbs' diff --git a/tests/files/io_tests/obj/reference/nurbs_curves.txt b/tests/files/io_tests/obj/reference/nurbs_curves.txt index bcf70936318..434f70548d1 100644 --- a/tests/files/io_tests/obj/reference/nurbs_curves.txt +++ b/tests/files/io_tests/obj/reference/nurbs_curves.txt @@ -1,38 +1,38 @@ ==== Curves: 5 - Curve 'CurveDeg3' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:3x3 cyclic:False,False endp:False,False - - (10.000, -2.000, 0.000) w:1.000 - - (10.000, 2.000, 0.000) w:1.000 - - (6.000, 2.000, 0.000) w:1.000 - (6.000, -2.000, 0.000) w:1.000 + - (6.000, 2.000, 0.000) w:1.000 + - (10.000, 2.000, 0.000) w:1.000 + - (10.000, -2.000, 0.000) w:1.000 - Curve 'nurbs_curves' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:False,False endp:False,False - - (2.000, -2.000, 0.000) w:1.000 - - (2.000, 2.000, 0.000) w:1.000 - - (-2.000, 2.000, 0.000) w:1.000 - (-2.000, -2.000, 0.000) w:1.000 + - (-2.000, 2.000, 0.000) w:1.000 + - (2.000, 2.000, 0.000) w:1.000 + - (2.000, -2.000, 0.000) w:1.000 - Curve 'NurbsCurveCyclic' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:True,False endp:False,False - - (-2.000, -2.000, 0.000) w:1.000 - - (-2.000, 2.000, 0.000) w:1.000 - - (-6.000, 2.000, 0.000) w:1.000 - (-6.000, -2.000, 0.000) w:1.000 + - (-6.000, 2.000, 0.000) w:1.000 + - (-2.000, 2.000, 0.000) w:1.000 + - (-2.000, -2.000, 0.000) w:1.000 - Curve 'NurbsCurveDiffWeights' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:False,False endp:False,False - - (6.000, -2.000, 0.000) w:1.000 - - (6.000, 2.000, 0.000) w:1.000 - - (2.000, 2.000, 0.000) w:1.000 - (2.000, -2.000, 0.000) w:1.000 + - (2.000, 2.000, 0.000) w:1.000 + - (6.000, 2.000, 0.000) w:1.000 + - (6.000, -2.000, 0.000) w:1.000 - Curve 'NurbsCurveEndpoint' dim:3D resu:12 resv:12 splines:1 - spline type:NURBS pts:4x1 order:4x4 cyclic:False,False endp:True,False - - (-6.000, -2.000, 0.000) w:1.000 - - (-6.000, 2.000, 0.000) w:1.000 - - (-10.000, 2.000, 0.000) w:1.000 - (-10.000, -2.000, 0.000) w:1.000 + - (-10.000, 2.000, 0.000) w:1.000 + - (-6.000, 2.000, 0.000) w:1.000 + - (-6.000, -2.000, 0.000) w:1.000 ==== Objects: 5 - Obj 'CurveDeg3' CURVE data:'CurveDeg3' diff --git a/tests/files/io_tests/obj/reference/rhino_curve_arc_bezier_deg2.txt b/tests/files/io_tests/obj/reference/rhino_curve_arc_bezier_deg2.txt new file mode 100644 index 00000000000..200d2bb73a5 --- /dev/null +++ b/tests/files/io_tests/obj/reference/rhino_curve_arc_bezier_deg2.txt @@ -0,0 +1,15 @@ +==== Curves: 1 +- Curve 'rhino_curve_arc_bezier_deg2' dim:3D resu:12 resv:12 splines:1 + - spline type:NURBS pts:5x1 order:3x3 cyclic:False,False endp:True,False + - (6.819, 0.000, -19.510) w:1.000 + - (3.112, 0.000, -30.504) w:0.897 + - (-7.865, 0.000, -34.264) w:1.000 + - (-18.842, 0.000, -38.024) w:0.897 + - (-28.512, 0.000, -31.611) w:1.000 + +==== Objects: 1 +- Obj 'rhino_curve_arc_bezier_deg2' CURVE data:'rhino_curve_arc_bezier_deg2' + - pos 0.000, 0.000, 0.000 + - rot 1.571, 0.000, 0.000 (XYZ) + - scl 1.000, 1.000, 1.000 + diff --git a/tests/files/io_tests/obj/reference/rhino_curve_bezier_deg3_cyclic.txt b/tests/files/io_tests/obj/reference/rhino_curve_bezier_deg3_cyclic.txt new file mode 100644 index 00000000000..4b34361119d --- /dev/null +++ b/tests/files/io_tests/obj/reference/rhino_curve_bezier_deg3_cyclic.txt @@ -0,0 +1,17 @@ +==== Curves: 1 +- Curve 'rhino_curve_bezier_deg3_cyclic' dim:3D resu:12 resv:12 splines:1 + - spline type:NURBS pts:12x1 order:4x4 cyclic:True,False endp:True,False + - (-16.508, 0.000, -26.333) w:1.000 + - (-19.459, 0.000, -23.609) w:1.000 + - (-22.637, 0.000, -23.099) w:1.000 + ... + - (-11.571, 0.000, -32.462) w:1.000 + - (-11.912, 0.000, -29.511) w:1.000 + - (-14.662, 0.000, -28.037) w:1.000 + +==== Objects: 1 +- Obj 'rhino_curve_bezier_deg3_cyclic' CURVE data:'rhino_curve_bezier_deg3_cyclic' + - pos 0.000, 0.000, 0.000 + - rot 1.571, 0.000, 0.000 (XYZ) + - scl 1.000, 1.000, 1.000 + diff --git a/tests/files/io_tests/obj/reference/rhino_curve_bezier_deg4_rat.txt b/tests/files/io_tests/obj/reference/rhino_curve_bezier_deg4_rat.txt new file mode 100644 index 00000000000..01e7718b7d4 --- /dev/null +++ b/tests/files/io_tests/obj/reference/rhino_curve_bezier_deg4_rat.txt @@ -0,0 +1,17 @@ +==== Curves: 1 +- Curve 'rhino_curve_bezier_deg4_rat' dim:3D resu:12 resv:12 splines:1 + - spline type:NURBS pts:9x1 order:5x5 cyclic:False,False endp:True,False + - (8.225, 0.000, 4.462) w:1.000 + - (12.103, 0.000, 0.023) w:0.943 + - (12.855, 0.000, -4.956) w:0.925 + ... + - (-2.991, 0.000, 1.965) w:1.000 + - (-4.901, 0.000, -1.010) w:1.000 + - (-5.653, 0.000, -8.056) w:1.000 + +==== Objects: 1 +- Obj 'rhino_curve_bezier_deg4_rat' CURVE data:'rhino_curve_bezier_deg4_rat' + - pos 0.000, 0.000, 0.000 + - rot 1.571, 0.000, 0.000 (XYZ) + - scl 1.000, 1.000, 1.000 + diff --git a/tests/files/io_tests/obj/reference/rhino_curve_circle_bezier_deg2_cyclic.txt b/tests/files/io_tests/obj/reference/rhino_curve_circle_bezier_deg2_cyclic.txt new file mode 100644 index 00000000000..d0287fe2dcf --- /dev/null +++ b/tests/files/io_tests/obj/reference/rhino_curve_circle_bezier_deg2_cyclic.txt @@ -0,0 +1,17 @@ +==== Curves: 1 +- Curve 'rhino_curve_circle_bezier_deg2_cyclic' dim:3D resu:12 resv:12 splines:1 + - spline type:NURBS pts:8x1 order:3x3 cyclic:True,False endp:True,False + - (-20.835, 0.000, -4.745) w:1.000 + - (-22.398, 0.000, -8.096) w:0.707 + - (-25.749, 0.000, -6.532) w:1.000 + ... + - (-25.973, 0.000, 1.733) w:0.707 + - (-22.622, 0.000, 0.169) w:1.000 + - (-19.271, 0.000, -1.394) w:0.707 + +==== Objects: 1 +- Obj 'rhino_curve_circle_bezier_deg2_cyclic' CURVE data:'rhino_curve_circle_bezier_deg2_cyclic' + - pos 0.000, 0.000, 0.000 + - rot 1.571, 0.000, 0.000 (XYZ) + - scl 1.000, 1.000, 1.000 + diff --git a/tests/files/io_tests/obj/reference/rhino_curve_uniform_cyclic_deg3.txt b/tests/files/io_tests/obj/reference/rhino_curve_uniform_cyclic_deg3.txt new file mode 100644 index 00000000000..acf9f3b6ae2 --- /dev/null +++ b/tests/files/io_tests/obj/reference/rhino_curve_uniform_cyclic_deg3.txt @@ -0,0 +1,17 @@ +==== Curves: 1 +- Curve 'rhino_curve_uniform_cyclic_deg3' dim:3D resu:12 resv:12 splines:1 + - spline type:NURBS pts:12x1 order:4x4 cyclic:True,False endp:False,False + - (-8.471, 0.000, -8.620) w:1.000 + - (-10.444, 0.000, -5.519) w:1.000 + - (-12.981, 0.000, 4.721) w:1.000 + ... + - (-15.987, 0.000, 3.500) w:1.000 + - (-10.162, 0.000, 5.003) w:1.000 + - (-7.438, 0.000, -4.674) w:1.000 + +==== Objects: 1 +- Obj 'rhino_curve_uniform_cyclic_deg3' CURVE data:'rhino_curve_uniform_cyclic_deg3' + - pos 0.000, 0.000, 0.000 + - rot 1.571, 0.000, 0.000 (XYZ) + - scl 1.000, 1.000, 1.000 + diff --git a/tests/files/io_tests/obj/reference/rhino_curve_uniform_cyclic_deg7.txt b/tests/files/io_tests/obj/reference/rhino_curve_uniform_cyclic_deg7.txt new file mode 100644 index 00000000000..57f860fb303 --- /dev/null +++ b/tests/files/io_tests/obj/reference/rhino_curve_uniform_cyclic_deg7.txt @@ -0,0 +1,17 @@ +==== Curves: 1 +- Curve 'rhino_curve_uniform_cyclic_deg7' dim:3D resu:12 resv:12 splines:1 + - spline type:NURBS pts:9x1 order:8x8 cyclic:True,False endp:False,False + - (19.014, 0.000, -57.653) w:1.000 + - (-0.187, 0.000, -58.172) w:1.000 + - (-12.880, 0.000, -60.533) w:1.000 + ... + - (3.446, 0.000, -52.464) w:1.000 + - (18.496, 0.000, -53.761) w:1.000 + - (38.216, 0.000, -45.717) w:1.000 + +==== Objects: 1 +- Obj 'rhino_curve_uniform_cyclic_deg7' CURVE data:'rhino_curve_uniform_cyclic_deg7' + - pos 0.000, 0.000, 0.000 + - rot 1.571, 0.000, 0.000 (XYZ) + - scl 1.000, 1.000, 1.000 + diff --git a/tests/files/io_tests/obj/rhino_curve_arc_bezier_deg2.obj b/tests/files/io_tests/obj/rhino_curve_arc_bezier_deg2.obj new file mode 100644 index 00000000000..4375c24aa6c --- /dev/null +++ b/tests/files/io_tests/obj/rhino_curve_arc_bezier_deg2.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7a10eef1f2eb16d63ab693e7abd0fcead73025c5c86e7b25d166e21b1c20d9a +size 444 diff --git a/tests/files/io_tests/obj/rhino_curve_bezier_deg3_cyclic.obj b/tests/files/io_tests/obj/rhino_curve_bezier_deg3_cyclic.obj new file mode 100644 index 00000000000..041c08f9f2c --- /dev/null +++ b/tests/files/io_tests/obj/rhino_curve_bezier_deg3_cyclic.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efdf58483b3e712bf109e42f436de593a900f910cee1a0947011b88fe7298923 +size 931 diff --git a/tests/files/io_tests/obj/rhino_curve_bezier_deg4_rat.obj b/tests/files/io_tests/obj/rhino_curve_bezier_deg4_rat.obj new file mode 100644 index 00000000000..e1a15506d51 --- /dev/null +++ b/tests/files/io_tests/obj/rhino_curve_bezier_deg4_rat.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62adaa9463889b6bf0a8766bfb15583529bfb492691397fa211bd66943f3eeb7 +size 676 diff --git a/tests/files/io_tests/obj/rhino_curve_circle_bezier_deg2_cyclic.obj b/tests/files/io_tests/obj/rhino_curve_circle_bezier_deg2_cyclic.obj new file mode 100644 index 00000000000..b9989310d66 --- /dev/null +++ b/tests/files/io_tests/obj/rhino_curve_circle_bezier_deg2_cyclic.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:417128124d6055e6ec1cfa30afb943df54247f9e7739d76b4134b5a5a767a2c7 +size 704 diff --git a/tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg3.obj b/tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg3.obj new file mode 100644 index 00000000000..dea8761df69 --- /dev/null +++ b/tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg3.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d67a2087ed0058b480b0f8422aab7ff0aec974800b43a4347abea905c008f0e +size 1024 diff --git a/tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg7.obj b/tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg7.obj new file mode 100644 index 00000000000..175a53bc377 --- /dev/null +++ b/tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg7.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2098b5b7ee2b8ac98c76c0e6c88cc19d7edaf80b8b5c803f30fecac67bc1447 +size 983