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
This commit is contained in:
Mattias Fredriksson
2025-08-01 06:37:28 +02:00
committed by Aras Pranckevicius
parent 2b97043379
commit e191d3d243
28 changed files with 1265 additions and 254 deletions

View File

@@ -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

View File

@@ -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<float> knot_buffer;
Span<float> knotsu = obj_nurbs_data.get_knots_u(spline_idx, knot_buffer);
Span<float> 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<float3> dynamic_point_buffer;
Span<float3> 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();

View File

@@ -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,

View File

@@ -6,112 +6,248 @@
* \ingroup obj
*/
#include <numeric>
#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<int> 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<int> multiplicity)
{
int index = multiplicity.size();
int acc = 0;
while (acc < order) {
acc += multiplicity[--index];
}
BLI_assert(index < multiplicity.size());
return index;
}
Span<float> valid_nurb_control_point_range(const int8_t order,
const Span<float> 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<int> left_mult = bke::curves::nurbs::calculate_multiplicity_sequence(
knots.slice(0, order2));
Vector<int> 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<float> OBJCurves::get_knots_u(int spline_index, Vector<float> &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<float3> OBJCurves::vertex_coordinates(int spline_index,
Vector<float3> & /*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<Curve *>(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<Nurb *>(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<Nurb *>(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<float3> OBJLegacyCurve::vertex_coordinates(const int spline_index,
Vector<float3> &dynamic_point_buffer) const
{
const Nurb *const nurb = static_cast<Nurb *>(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<Nurb *>(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<Nurb *>(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<Nurb *>(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<Nurb *>(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<Nurb *>(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<float> OBJCurve::get_knots_u(int spline_index, Vector<float> &knot_buffer) const
Span<float> OBJLegacyCurve::get_knots_u(int spline_index, Vector<float> &knot_buffer) const
{
const Nurb *const nurb = static_cast<Nurb *>(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<float> OBJCurve::get_knots_u(int spline_index, Vector<float> &knot_buffer)
return knot_buffer;
}
/** \} */
} // namespace blender::io::obj

View File

@@ -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<float> valid_nurb_control_point_range(int8_t order,
Span<float> 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<float> get_knots_u(int spline_index, Vector<float> &buffer) const;
private:
virtual Span<float> get_knots_u(int spline_index, Vector<float> &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<float3> vertex_coordinates(int spline_index,
Vector<float3> &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<float> get_knots_u(int spline_index, Vector<float> &buffer) const override;
Span<float3> vertex_coordinates(int spline_index,
Vector<float3> &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<float> get_knots_u(int spline_index, Vector<float> &buffer) const override;
Span<float3> vertex_coordinates(int spline_index,
Vector<float3> &dynamic_point_buffer) const override;
};
} // namespace blender::io::obj

View File

@@ -10,6 +10,9 @@
#include <memory>
#include <system_error>
#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::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<OBJCurve>>>
std::pair<Vector<std::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<IOBJCurve>>>
filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params)
{
Vector<std::unique_ptr<OBJMesh>> r_exportable_meshes;
Vector<std::unique_ptr<OBJCurve>> r_exportable_nurbs;
Vector<std::unique_ptr<IOBJCurve>> 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<OBJCurve>(depsgraph, export_params, object));
IOBJCurve *obj_curve = new OBJLegacyCurve(depsgraph, object);
r_exportable_nurbs.append(std::unique_ptr<IOBJCurve>(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<OBJCurve>(depsgraph, export_params, object));
IOBJCurve *obj_curve = new OBJLegacyCurve(depsgraph, object);
r_exportable_nurbs.append(std::unique_ptr<IOBJCurve>(obj_curve));
}
else {
/* Export in mesh form: edges and vertices. */
@@ -257,13 +261,13 @@ static void write_mesh_objects(const Span<std::unique_ptr<OBJMesh>> exportable_a
/**
* Export NURBS Curves in parameter form, not as vertices and edges.
*/
static void write_nurbs_curve_objects(const Span<std::unique_ptr<OBJCurve>> exportable_as_nurbs,
static void write_nurbs_curve_objects(const Span<std::unique_ptr<IOBJCurve>> 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<OBJCurve> &obj_curve : exportable_as_nurbs) {
for (const std::unique_ptr<IOBJCurve> &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<std::unique_ptr<OBJMesh>> meshes,
const Span<std::unique_ptr<OBJCurve>> curves,
const char *filepath)
void export_objects(const OBJExportParams &export_params,
const Span<std::unique_ptr<OBJMesh>> meshes,
const Span<std::unique_ptr<IOBJCurve>> curves,
const char *filepath)
{
/* Open */
std::unique_ptr<OBJWriter> obj_writer;

View File

@@ -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<std::unique_ptr<OBJMesh>> meshes,
Span<std::unique_ptr<IOBJCurve>> 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::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<OBJCurve>>>
std::pair<Vector<std::unique_ptr<OBJMesh>>, Vector<std::unique_ptr<IOBJCurve>>>
filter_supported_objects(Depsgraph *depsgraph, const OBJExportParams &export_params);
/**

View File

@@ -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<float> 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<int>(order - end_multiplicity, 1);
Vector<int> 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<int> 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<float>(nurbs_geometry.parm, MutableSpan<float>{nurb->knotsu, KNOTSU(nurb)});
MutableSpan<float> knots_dst_u{nurb->knotsu, KNOTSU(nurb)};
array_utils::copy<float>(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<int> 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<int> indices,
const Span<float> knots,
const Span<int> multiplicity)
const Span<int> 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<int> 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<float> knots_tail = knots.take_back(order + degree);
const Span<float> 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<int> multiplicity)
static bool detect_knot_mode_bezier_clamped(const int8_t degree,
const int num_points,
const Span<int> 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<int> 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<int> multipl
static bool detect_knot_mode_uniform(const int8_t degree,
const Span<float> knots,
const Span<int> 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<float> 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;
}

View File

@@ -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<std::unique_ptr<IOBJCurve>> curves, OBJExportParams params)
{
export_objects(params, Span<std::unique_ptr<OBJMesh>>(nullptr, 0), curves, params.filepath);
}
void write_curves(const std::unique_ptr<IOBJCurve> &curve, OBJExportParams params)
{
Span<std::unique_ptr<IOBJCurve>> span(&curve, 1);
write_curves(span, params);
}
void write_curves(const bke::CurvesGeometry &curve, OBJExportParams params)
{
float4x4 identity = float4x4::identity();
std::unique_ptr<IOBJCurve> curve_wrapper(new OBJCurves(curve, identity, "test"));
write_curves(curve_wrapper, params);
}
Vector<bke::GeometrySet> read_curves(OBJImportParams params)
{
Vector<bke::GeometrySet> geoms;
importer_geometry(params, geoms);
return geoms;
}
static bke::CurvesGeometry create_curves(Span<float3> 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<float3> points, Span<float> 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<float3> 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<float3> points,
const int8_t order,
const KnotsMode mode,
const bool cyclic,
bke::CurvesGeometry &src_curve,
const bke::CurvesGeometry *&result_curve,
Span<float3> expected_points = Span<float3>(),
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<bke::GeometrySet> 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<float3> 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<float3, 13> 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<float3> position_data = Span<float3>(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<float3> 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<float> 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<int> multiplicity = bke::curves::nurbs::calculate_multiplicity_sequence(
knot_buffer);
std::array<int, 7> expected_mult;
std::fill(expected_mult.begin(), expected_mult.end(), 1);
EXPECT_EQ_SPAN<int>(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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> 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<float3> positions = position_data;
Vector<float3> 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<float3> positions = position_data.slice(0, 11);
Vector<float3> 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<float3> positions = position_data.slice(0, 10);
Vector<float3> 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<float3> positions = position_data.slice(0, 9);
Vector<float3> 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<float3> positions = position_data.slice(0, 12);
Vector<float3> 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<float3> positions = position_data.slice(0, 13);
Vector<float3> 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<float3> positions = position_data.slice(0, 11);
Vector<float3> 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<float3> positions = position_data.slice(0, 10);
Vector<float3> 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<float3> positions = position_data.slice(0, 9);
Vector<float3> 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<float3> positions = position_data.slice(0, 8);
Vector<float3> 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

Binary file not shown.

BIN
tests/files/io_tests/obj/nurbs.obj (Stored with Git LFS)

Binary file not shown.

BIN
tests/files/io_tests/obj/nurbs_curves.obj (Stored with Git LFS)

Binary file not shown.

BIN
tests/files/io_tests/obj/nurbs_manual.obj (Stored with Git LFS)

Binary file not shown.

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

BIN
tests/files/io_tests/obj/rhino_curve_arc_bezier_deg2.obj (Stored with Git LFS) Normal file

Binary file not shown.

BIN
tests/files/io_tests/obj/rhino_curve_bezier_deg3_cyclic.obj (Stored with Git LFS) Normal file

Binary file not shown.

BIN
tests/files/io_tests/obj/rhino_curve_bezier_deg4_rat.obj (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg3.obj (Stored with Git LFS) Normal file

Binary file not shown.

BIN
tests/files/io_tests/obj/rhino_curve_uniform_cyclic_deg7.obj (Stored with Git LFS) Normal file

Binary file not shown.