#include "proc_curves.h"
#include <cmath>
namespace godot {
void ProcCurves::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_point_count"), &ProcCurves::point_count);
ClassDB::bind_method(D_METHOD("get_curve_count"), &ProcCurves::curve_count);
ClassDB::bind_method(D_METHOD("evaluate_position", "curve", "t"), &ProcCurves::evaluate_position);
ClassDB::bind_method(D_METHOD("evaluate_tangent", "curve", "t"), &ProcCurves::evaluate_tangent);
ClassDB::bind_method(D_METHOD("approximate_length", "curve", "samples"), &ProcCurves::approximate_length, DEFVAL(32));
}
int ProcCurves::curve_count() const {
if (curve_offsets.size() > 1) {
return curve_offsets.size() - 1;
}
return 0;
}
int ProcCurves::domain_size(AttrDomain p_domain) const {
switch (p_domain) {
case AttrDomain::Point: return point_count();
case AttrDomain::Curve: return curve_count();
default: return 0;
}
}
Ref<ProcCurves> ProcCurves::duplicate_curves() const {
Ref<ProcCurves> dup;
dup.instantiate();
dup->positions = positions;
dup->radii = radii;
dup->tilts = tilts;
dup->curve_offsets = curve_offsets;
dup->curve_types = curve_types;
dup->point_attrs.propagate_from(point_attrs);
dup->curve_attrs.propagate_from(curve_attrs);
return dup;
}
// Points spaced evenly along the curve BY LENGTH, which is what Blender's
// Resample Curve and Curve to Points do. Parameter-uniform sampling bunches up
// wherever the control points are close together; on a bezier that is a visible
// difference and on a line it is none at all, which is how it went unnoticed.
//
// The length table is built by dense parameter sampling rather than
// analytically: a bezier's arc length has no closed form, and Blender's own
// resampler walks an evaluated polyline too.
PackedVector3Array ProcCurves::sample_uniform(int p_curve, int p_count) const {
PackedVector3Array out;
if (p_count <= 0 || p_curve < 0 || p_curve >= curve_count()) {
return out;
}
if (p_count == 1) {
out.push_back(evaluate_position(p_curve, 0.0f));
return out;
}
const int start = curve_offsets[p_curve];
const int n = curve_offsets[p_curve + 1] - start;
// Enough samples that the polyline approximates the curve; for a Poly curve
// the segments are exact at any resolution above its own point count.
const int steps = MAX(n * 64, 4096);
const bool cyclic = is_cyclic(p_curve);
// A CYCLIC curve's polyline closes back to its first point, and its samples
// span [0, total) rather than [0, total] -- N points around a loop, with no
// repeat of the start.
PackedVector3Array pts;
PackedFloat32Array cumulative;
pts.resize(steps + 1);
cumulative.resize(steps + 1);
float total = 0.0f;
for (int i = 0; i <= steps; i++) {
if (cyclic && i == steps) {
pts.ptrw()[i] = pts[0];
} else {
pts.ptrw()[i] = cyclic
? evaluate_position_cyclic(p_curve, (float)i / (float)steps)
: evaluate_position(p_curve, (float)i / (float)steps);
}
if (i > 0) {
total += pts[i].distance_to(pts[i - 1]);
}
cumulative.ptrw()[i] = total;
}
if (total <= 0.0f) {
for (int i = 0; i < p_count; i++) {
out.push_back(pts[0]);
}
return out;
}
int walk = 0;
for (int i = 0; i < p_count; i++) {
const float target = cyclic ? (total * (float)i / (float)p_count)
: (total * (float)i / (float)(p_count - 1));
while (walk < steps && cumulative[walk + 1] < target) {
walk++;
}
const float seg_len = cumulative[walk + 1] - cumulative[walk];
const float f = seg_len > 0.0f ? (target - cumulative[walk]) / seg_len : 0.0f;
out.push_back(pts[walk].lerp(pts[MIN(walk + 1, steps)], f));
}
return out;
}
Vector3 ProcCurves::evaluate_position(int p_curve, float p_t) const {
if (p_curve < 0 || p_curve >= curve_count()) {
return Vector3();
}
int start = curve_offsets[p_curve];
int end = curve_offsets[p_curve + 1];
int n = end - start;
if (n == 0) {
return Vector3();
}
if (n == 1) {
return positions[start];
}
p_t = CLAMP(p_t, 0.0f, 1.0f);
// Determine curve type (default to Poly if out of range)
CurveType type = CurveType::Poly;
if (p_curve < curve_types.size()) {
type = static_cast<CurveType>(curve_types[p_curve]);
}
if (type == CurveType::Bezier) {
// Bezier: every 4 consecutive points form a segment [p0, h0, h1, p1, ...]
int n_segs = MAX((n - 1) / 3, 1);
float seg_f = p_t * n_segs;
int seg = CLAMP((int)seg_f, 0, n_segs - 1);
float lt = seg_f - seg;
int base = seg * 3;
Vector3 p0 = positions[start + base];
Vector3 p1 = positions[start + MIN(base + 1, n - 1)];
Vector3 p2 = positions[start + MIN(base + 2, n - 1)];
Vector3 p3 = positions[start + MIN(base + 3, n - 1)];
float u = 1.0f - lt;
return u * u * u * p0 + 3.0f * u * u * lt * p1 + 3.0f * u * lt * lt * p2 + lt * lt * lt * p3;
}
// For Poly and CatmullRom: map t to segment.
//
// CLAMP THE SEGMENT BEFORE TAKING THE LOCAL t, not after. At t = 1 the
// segment index lands one past the last, and the old order computed
// local_t against the UNCLAMPED index -- giving 0 -- and then clamped. On a
// two-point line that returned lerp(start, end, 0), which is the START:
// asking for the end of a curve gave you its beginning. Every curve
// sampled inclusively at t = 1 was wrong, and the last point of every
// Curve to Points was the first.
float seg_f = p_t * (n - 1);
int seg = CLAMP((int)seg_f, 0, n - 2);
float local_t = seg_f - seg;
if (type == CurveType::CatmullRom) {
int i0 = MAX(seg - 1, 0);
int i1 = seg;
int i2 = MIN(seg + 1, n - 1);
int i3 = MIN(seg + 2, n - 1);
Vector3 a = positions[start + i0];
Vector3 b = positions[start + i1];
Vector3 c = positions[start + i2];
Vector3 d = positions[start + i3];
float t2 = local_t * local_t;
float t3 = t2 * local_t;
return 0.5f * (a * (-t3 + 2.0f * t2 - local_t) +
b * (3.0f * t3 - 5.0f * t2 + 2.0f) +
c * (-3.0f * t3 + 4.0f * t2 + local_t) +
d * (t3 - t2));
}
// Poly (linear interpolation)
Vector3 p0 = positions[start + seg];
Vector3 p1 = positions[start + seg + 1];
return p0.lerp(p1, local_t);
}
Vector3 ProcCurves::evaluate_position_cyclic(int p_curve, float p_t) const {
// A cyclic poly curve has n segments rather than n - 1, the last running
// from the final point back to the first.
const int start = curve_offsets[p_curve];
const int n = curve_offsets[p_curve + 1] - start;
if (n < 2) {
return evaluate_position(p_curve, p_t);
}
const float seg_f = CLAMP(p_t, 0.0f, 1.0f) * (float)n;
int seg = CLAMP((int)seg_f, 0, n - 1);
const float local_t = seg_f - (float)seg;
const Vector3 &p0 = positions[start + seg];
const Vector3 &p1 = positions[start + ((seg + 1) % n)];
return p0.lerp(p1, local_t);
}
Vector3 ProcCurves::evaluate_tangent(int p_curve, float p_t) const {
const float h = 0.001f;
float t0 = MAX(p_t - h, 0.0f);
float t1 = MIN(p_t + h, 1.0f);
if (t0 == t1) {
return Vector3(1, 0, 0); // Degenerate case
}
Vector3 p0 = evaluate_position(p_curve, t0);
Vector3 p1 = evaluate_position(p_curve, t1);
Vector3 diff = p1 - p0;
if (diff.length_squared() < 1e-12f) {
return Vector3(1, 0, 0);
}
return diff.normalized();
}
float ProcCurves::approximate_length(int p_curve, int p_samples) const {
if (p_curve < 0 || p_curve >= curve_count() || p_samples < 2) {
return 0.0f;
}
float total = 0.0f;
Vector3 prev = evaluate_position(p_curve, 0.0f);
for (int i = 1; i <= p_samples; i++) {
float t = static_cast<float>(i) / p_samples;
Vector3 curr = evaluate_position(p_curve, t);
total += prev.distance_to(curr);
prev = curr;
}
return total;
}
} // namespace godot