summaryrefslogtreecommitdiffhomepage
path: root/spline/spline.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'spline/spline.cpp')
-rw-r--r--spline/spline.cpp637
1 files changed, 352 insertions, 285 deletions
diff --git a/spline/spline.cpp b/spline/spline.cpp
index 2db23c14..466a9a7f 100644
--- a/spline/spline.cpp
+++ b/spline/spline.cpp
@@ -1,245 +1,220 @@
-/* Copyright (c) 2012-2016, Stanislaw Halik <sthalik@misaki.pl>
-
- * Permission to use, copy, modify, and/or distribute this
- * software for any purpose with or without fee is hereby granted,
- * provided that the above copyright notice and this permission
- * notice appear in all copies.
- */
-
#include "spline.hpp"
+#include "compat/math.hpp"
#include <algorithm>
+#include <cstdlib>
#include <cmath>
#include <memory>
+#include <cinttypes>
+#include <utility>
#include <QObject>
#include <QMutexLocker>
-#include <QCoreApplication>
#include <QPointF>
#include <QSettings>
#include <QString>
#include <QDebug>
-constexpr int spline::value_count;
+namespace spline_detail {
-spline::spline(qreal maxx, qreal maxy, const QString& name) :
- s(nullptr),
- data(value_count, -1.f),
- _mutex(QMutex::Recursive),
- max_x(maxx),
- max_y(maxy),
- activep(false),
- validp(false)
+settings::~settings() = default;
+base_spline_::~base_spline_() = default;
+base_spline::~base_spline() = default;
+spline_modify_mixin::~spline_modify_mixin() = default;
+spline_settings_mixin::~spline_settings_mixin() = default;
+
+spline::spline(const QString& name, const QString& axis_name, Axis axis)
{
- set_bundle(options::make_bundle(name));
+ set_bundle(options::make_bundle(name), axis_name, axis);
}
spline::~spline()
{
- QMutexLocker l(&_mutex);
-
- if (connection)
- {
- QObject::disconnect(connection);
- connection = QMetaObject::Connection();
- }
+ QMutexLocker l(&mtx);
+ disconnect_signals();
}
-spline::spline() : spline(0, 0, "") {}
+spline::spline() : spline(QString{}, QString{}, Axis(-1)) {}
-void spline::set_tracking_active(bool value)
+void spline::set_tracking_active(bool value) const
{
- activep = value;
+ std::shared_ptr<settings> S;
+ {
+ QMutexLocker l(&mtx);
+ if (value == activep)
+ return;
+ activep = value;
+ S = s;
+ }
+ emit S->recomputed();
}
bundle spline::get_bundle()
{
+ QMutexLocker l(&mtx);
return s->b;
}
void spline::clear()
{
- QMutexLocker l(&_mutex);
- s->points = points_t();
- validp = false;
-}
-
-void spline::set_max_input(qreal max_input)
-{
- QMutexLocker l(&_mutex);
- max_x = max_input;
- recompute();
-}
-
-void spline::set_max_output(qreal max_output)
-{
- QMutexLocker l(&_mutex);
- max_y = max_output;
- recompute();
-}
-
-qreal spline::max_input() const
-{
- QMutexLocker l(&_mutex);
- return max_x;
-}
-
-qreal spline::max_output() const
-{
- QMutexLocker l(&_mutex);
- return max_y;
+ std::shared_ptr<settings> S;
+ {
+ QMutexLocker l(&mtx);
+ S = s;
+ s->points = {};
+ points = {};
+ update_interp_data();
+ }
+ emit S->recomputed();
}
-float spline::get_value(double x)
+double spline::get_value(double x) const
{
- QMutexLocker foo(&_mutex);
+ QMutexLocker l(&mtx);
- const float ret = get_value_no_save(x);
- last_input_value.setX(std::fabs(x));
- last_input_value.setY(double(std::fabs(ret)));
+ const double ret = get_value_no_save(x);
+ last_input_value = { std::fabs(x), std::fabs((double)ret) };
return ret;
}
-float spline::get_value_no_save(double x) const
-{
- return const_cast<spline&>(*this).get_value_no_save_internal(x);
-}
-
-float spline::get_value_no_save_internal(double x)
+double spline::get_value_no_save(double x) const
{
- QMutexLocker foo(&_mutex);
+ QMutexLocker l(&mtx);
- if (max_x > 0)
- x = std::min(max_x, x);
-
- float q = float(x * precision(s->points));
+ double q = x * bucket_size_coefficient(points);
int xi = (int)q;
- float yi = get_value_internal(xi);
- float yiplus1 = get_value_internal(xi+1);
- float f = (q-xi);
- float ret = yiplus1 * f + yi * (1.0f - f); // at least do a linear interpolation.
+ double yi = get_value_internal(xi);
+ double yiplus1 = get_value_internal(xi+1);
+ double f = (q-xi);
+ double ret = yiplus1 * f + yi * (1 - f); // at least do a linear interpolation.
return ret;
}
-DEFUN_WARN_UNUSED bool spline::get_last_value(QPointF& point)
+bool spline::get_last_value(QPointF& point)
{
- QMutexLocker foo(&_mutex);
+ QMutexLocker foo(&mtx);
point = last_input_value;
- return activep;
+ return activep && point.y() >= 0;
}
-float spline::get_value_internal(int x)
+double spline::get_value_internal(int x) const
{
- if (!validp)
- {
- update_interp_data();
- validp = true;
- }
-
- float sign = x < 0 ? -1 : 1;
+ const auto sign = (f)signum(x);
x = std::abs(x);
- float ret;
- ret = data[std::min(unsigned(x), unsigned(value_count)-1u)];
- return ret * sign;
+ const auto ret_ = data[std::min(unsigned(x), value_count - 1)];
+ return (double)(sign * std::clamp(ret_, (f)0, (f)1000));
}
-void spline::add_lone_point()
+void spline::ensure_in_bounds(const QList<QPointF>& points, int i, f& x, f& y)
{
- points_t points;
- points.push_back(QPointF(max_x, max_y));
-
- s->points = points;
-}
-
-QPointF spline::ensure_in_bounds(const QList<QPointF>& points, double max_x, int i)
-{
- const int sz = element_count(points, max_x);
+ const int sz = points.size();
if (i < 0 || sz == 0)
- return QPointF(0, 0);
-
- if (i < sz)
- return points[i];
-
- return points[sz - 1];
+ {
+ x = 0;
+ y = 0;
+ }
+ else if (i < sz)
+ {
+ const QPointF& pt = points[i];
+ x = (f)pt.x();
+ y = (f)pt.y();
+ }
+ else
+ {
+ const QPointF& pt = points[sz - 1];
+ x = (f)pt.x();
+ y = (f)pt.y();
+ }
}
-int spline::element_count(const QList<QPointF>& points, double max_x)
+int spline::element_count(const QList<QPointF>& points, double max_input)
{
- if (!(max_x > 0))
- return points.size();
- else
+ const int sz = points.size();
+ for (int k = sz-1; k >= 0; k--)
{
- const unsigned sz = points.size();
- for (unsigned i = 0; i < sz; i++)
- {
- if (!(points[i].x() <= max_x))
- return i;
- }
- return points.size();
+ const QPointF& pt = points[k];
+ if (max_input > 1e-4 && pt.x() - 1e-4 >= max_input)
+ return k;
}
+ return sz;
}
-bool spline::sort_fn(const QPointF& one, const QPointF& two)
+bool spline::sort_fn(QPointF one, QPointF two)
{
- return one.x() <= two.x();
+ return one.x() < two.x();
}
-void spline::update_interp_data()
+void spline::update_interp_data() const
{
- points_t points = s->points;
-
- int sz = element_count(points, max_x);
+ points_t list = points;
+ ensure_valid(list);
+ int sz = list.size();
- if (sz == 0)
- points.prepend(QPointF(max_x, max_y));
+ if (list.isEmpty())
+ list.prepend({ max_input(), max_output() });
- std::stable_sort(points.begin(), points.begin() + sz, sort_fn);
-
- const double mult = precision(points);
- const double mult_ = mult * 30;
+ const double c = bucket_size_coefficient(list);
+ const double c_ = c * c_interp;
+ const f cf = (f)c, c_f = (f)c_;
for (unsigned i = 0; i < value_count; i++)
- data[i] = -1;
+ data[i] = magic_fill_value;
+
+ if (sz < 2) // lerp only
+ {
+ const QPointF& pt = list[0];
+ const double x = pt.x();
+ const double y = pt.y();
+ const unsigned max = std::clamp((unsigned)iround(x * c), 1u, value_count-1);
- if (sz < 2)
+ for (unsigned k = 0; k <= max; k++)
+ data[k] = f(y * k / max); // no need for bresenham
+ }
+ else if (sz == 2 && list[0].y() < 1e-6)
{
- if (points[0].x() - 1e-2 <= max_x)
+ unsigned start = std::clamp((unsigned)iround(list[0].x() * c), 1u, value_count-1);
+ unsigned end = std::clamp((unsigned)iround(list[1].x() * c), 2u, value_count-1);
+ unsigned max = end - start;
+ for (unsigned x = 0; x < start; x++)
+ data[x] = 0;
+ for (unsigned x = 0; x < max; x++)
+ data[start + x] = (f)(list[1].y() * x / max);
+ }
+ else
+ {
+ if (list[0].x() > 1e-6)
{
- const double x = points[0].x();
- const double y = points[0].y();
- const int max = clamp(int(x * precision(points)), 1, value_count-1);
- for (int k = 0; k <= max; k++)
+ double zero_pos = 0;
+ while (list.size() > 1 && list[0].y() <= 1e-6)
{
- if (k < value_count)
- data[unsigned(k)] = float(y * k / max);
+ zero_pos = list[0].x();
+ list.pop_front();
}
+ list.push_front({zero_pos, 0});
+ sz = list.size();
}
- }
- else
- {
- if (points[0].x() > 1e-2 && points[0].x() <= max_x)
- points.push_front(QPointF(0, 0));
+ // now this is hella expensive due to `c_interp'
for (int i = 0; i < sz; i++)
{
- const QPointF p0 = ensure_in_bounds(points, max_x, i - 1);
- const QPointF p1 = ensure_in_bounds(points, max_x, i + 0);
- const QPointF p2 = ensure_in_bounds(points, max_x, i + 1);
- const QPointF p3 = ensure_in_bounds(points, max_x, i + 2);
- const double p0_x = p0.x(), p1_x = p1.x(), p2_x = p2.x(), p3_x = p3.x();
- const double p0_y = p0.y(), p1_y = p1.y(), p2_y = p2.y(), p3_y = p3.y();
-
- const double cx[4] = {
+ f p0_x, p1_x, p2_x, p3_x;
+ f p0_y, p1_y, p2_y, p3_y;
+
+ ensure_in_bounds(list, i - 1, p0_x, p0_y);
+ ensure_in_bounds(list, i + 0, p1_x, p1_y);
+ ensure_in_bounds(list, i + 1, p2_x, p2_y);
+ ensure_in_bounds(list, i + 2, p3_x, p3_y);
+
+ const f cx[4] = {
2 * p1_x, // 1
-p0_x + p2_x, // t
2 * p0_x - 5 * p1_x + 4 * p2_x - p3_x, // t^2
-p0_x + 3 * p1_x - 3 * p2_x + p3_x, // t3
};
- const double cy[4] =
- {
+ const f cy[4] = {
2 * p1_y, // 1
-p0_y + p2_y, // t
2 * p0_y - 5 * p1_y + 4 * p2_y - p3_y, // t^2
@@ -247,223 +222,315 @@ void spline::update_interp_data()
};
// multiplier helps fill in all the x's needed
- const unsigned end = std::min(unsigned(value_count), unsigned(p2_x * mult_));
- const unsigned start = std::max(0u, unsigned(p1_x * mult));
+ const unsigned end = (unsigned)(c_f * (p2_x - p1_x)) + 1;
+ const f end_(end);
- for (unsigned j = start; j < end; j++)
+ for (unsigned k = 0; k <= end; k++)
{
- const double t = (j - start) / (double) (end - start);
- const double t2 = t*t;
- const double t3 = t*t*t;
-
- const int x = iround(.5 * mult * (cx[0] + cx[1] * t + cx[2] * t2 + cx[3] * t3));
- const float y = float(.5 * (cy[0] + cy[1] * t + cy[2] * t2 + cy[3] * t3));
-
- if (x >= 0 && x < value_count)
- data[unsigned(x)] = y;
+ const f t = k / end_;
+ const f t2 = t*t;
+ const f t3 = t*t*t;
+
+ const auto x = (unsigned)(f(.5) * cf * (cx[0] + cx[1] * t + cx[2] * t2 + cx[3] * t3));
+ const auto y = (f)(f(.5) * (cy[0] + cy[1] * t + cy[2] * t2 + cy[3] * t3));
+
+ switch (int ret = std::fpclassify(y))
+ {
+ case FP_INFINITE:
+ case FP_NAN:
+ case FP_SUBNORMAL:
+ eval_once(qDebug() << "spline: fpclassify" << y
+ << "returned" << ret
+ << "for bundle" << s->b->name());
+ continue;
+ case FP_ZERO:
+ case FP_NORMAL:
+ if (x < value_count)
+ data[x] = y;
+ break;
+ default:
+ unreachable();
+ }
}
}
}
- float last = 0;
- for (unsigned i = 0; i < unsigned(value_count); i++)
+ auto maxy = (f)max_output();
+ auto last = (f)0;
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wfloat-equal" // stupid clang
+#endif
+
+ for (unsigned i = 0; i < value_count; i++)
{
- if (data[i] < 0)
+ if (data[i] == magic_fill_value)
data[i] = last;
+ data[i] = std::clamp(data[i], (f)0, (f)maxy);
last = data[i];
}
+
+ // make sure empty places stay empty (see #1341)
+ if (auto it = std::find_if(list.cbegin(), list.cend(),
+ [](QPointF x) { return x.x() >= (f)1e-6 && x.y() >= (f)1e-6; });
+ it != list.cend() && it != list.cbegin())
+ {
+ it--;
+ unsigned max = std::clamp((unsigned)iround(it->x() * c), 0u, value_count-1);
+
+ for (unsigned x = 0; x < max; x++)
+ data[x] = 0;
+ }
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
}
void spline::remove_point(int i)
{
- QMutexLocker foo(&_mutex);
+ std::shared_ptr<settings> S;
+ {
+ QMutexLocker foo(&mtx);
+ S = s;
- points_t points = s->points;
- const int sz = element_count(points, max_x);
+ const int sz = element_count(points, max_input());
- if (i >= 0 && i < sz)
- {
- points.erase(points.begin() + i);
- s->points = points;
- validp = false;
+ if (i >= 0 && i < sz)
+ {
+ points.erase(points.begin() + i);
+ s->points = points;
+ update_interp_data();
+ }
}
+
+ emit S->recomputed();
}
void spline::add_point(QPointF pt)
{
- QMutexLocker foo(&_mutex);
-
- points_t points = s->points;
- points.push_back(pt);
- std::stable_sort(points.begin(), points.end(), sort_fn);
- s->points = points;
- validp = false;
+ std::shared_ptr<settings> S;
+ {
+ QMutexLocker foo(&mtx);
+ S = s;
+ points.push_back(pt);
+ std::stable_sort(points.begin(), points.end(), sort_fn);
+ s->points = points;
+ update_interp_data();
+ }
+ emit S->recomputed();
}
void spline::add_point(double x, double y)
{
- add_point(QPointF(x, y));
+ add_point({ x, y });
}
void spline::move_point(int idx, QPointF pt)
{
- QMutexLocker foo(&_mutex);
-
- points_t points = s->points;
+ std::shared_ptr<settings> S;
+ {
+ QMutexLocker foo(&mtx);
+ S = s;
- const int sz = element_count(points, max_x);
+ const int sz = element_count(points, max_input());
- if (idx >= 0 && idx < sz)
- {
- points[idx] = pt;
- // we don't allow points to be reordered, but sort due to possible caller logic error
- std::stable_sort(points.begin(), points.end(), sort_fn);
- s->points = points;
- validp = false;
+ if (idx >= 0 && idx < sz)
+ {
+ points[idx] = pt;
+ std::stable_sort(points.begin(), points.end(), sort_fn);
+ s->points = points;
+ update_interp_data();
+ }
}
+ emit S->recomputed();
}
-QList<QPointF> spline::get_points() const
+const points_t& spline::get_points() const
{
- QMutexLocker foo(&_mutex);
- return s->points;
+ return points;
}
int spline::get_point_count() const
{
- QMutexLocker foo(&_mutex);
- return element_count(s->points, max_x);;
+ QMutexLocker foo(&mtx);
+ return element_count(points, clamp_x);
}
void spline::reload()
{
- QMutexLocker foo(&_mutex);
+ QMutexLocker foo(&mtx);
s->b->reload();
}
-void spline::save(QSettings& settings)
+void spline::save()
{
- QMutexLocker foo(&_mutex);
- s->b->save_deferred(settings);
+ QMutexLocker foo(&mtx);
+ s->b->save();
}
-void spline::save()
+void spline::invalidate_settings_()
{
- save(*group::ini_file());
+ points = s->points;
+ clamp_x = s->opts.clamp_x_;
+ clamp_y = s->opts.clamp_y_;
+ update_interp_data();
}
-void spline::set_bundle(bundle b)
+void spline::invalidate_settings()
{
- QMutexLocker l(&_mutex);
-
- // gets called from ctor hence the need for nullptr checks
- // the sentinel settings/bundle objects don't need any further branching once created
- if (!s || s->b != b)
+ std::shared_ptr<settings> S;
{
- s = std::make_shared<settings>(b);
-
- if (connection)
- QObject::disconnect(connection);
+ QMutexLocker l(&mtx);
+ S = s;
+ invalidate_settings_();
+ }
+ emit S->recomputed();
+}
- if (b)
- {
- connection = QObject::connect(b.get(), &bundle_::changed,
- s.get(), [&]() {
- // we're holding the mutex to allow signal disconnection in spline dtor
- // before this slot gets called for the next time
-
- // spline isn't a QObject and the connection context is incorrect
-
- QMutexLocker l(&_mutex);
- recompute();
- emit s->recomputed();
- },
- Qt::QueuedConnection);
- }
+void spline::set_bundle(bundle b, const QString& axis_name, Axis axis)
+{
+ if (!b)
+ b = make_bundle({});
- recompute();
+ std::shared_ptr<settings> S;
- emit s->recomputed();
+ {
+ QMutexLocker l(&mtx);
+
+ disconnect_signals();
+ s = std::make_shared<settings>(b, axis_name, axis);
+ invalidate_settings_();
+ S = s;
+
+ conn_points = QObject::connect(&s->points, value_::value_changed<QList<QPointF>>(),
+ &*ctx, [this] { invalidate_settings(); }, Qt::DirectConnection);
+ conn_maxx = QObject::connect(&s->opts.clamp_x_, value_::value_changed<int>(),
+ &*ctx, [this](double) { invalidate_settings(); }, Qt::DirectConnection);
+ conn_maxy = QObject::connect(&s->opts.clamp_y_, value_::value_changed<int>(),
+ &*ctx, [this](double) { invalidate_settings(); }, Qt::DirectConnection);
}
+
+ emit S->recomputed();
}
-void spline::recompute()
+double spline::max_input() const
{
- QMutexLocker foo(&_mutex);
+ QMutexLocker l(&mtx);
+ if (clamp_x == axis_opts::x1000)
+ return std::fmax(1, points.empty() ? 0 : points[points.size() - 1].x());
+ return std::fabs((double)clamp_x);
+}
- QList<QPointF> list = s->points;
+double spline::max_output() const
+{
+ QMutexLocker l(&mtx);
+ if (clamp_y == axis_opts::x1000 && !points.empty())
+ return std::fmax(1, points.empty() ? 0 : points[points.size() - 1].y());
+ return std::fabs((double)clamp_y);
+}
- // storing to s->points fires bundle::changed and that leads to an infinite loop
- // only store if we can't help it
+void spline::ensure_valid(points_t& list) const
+{
std::stable_sort(list.begin(), list.end(), sort_fn);
- if (list != s->points)
- s->points = list;
+ const unsigned sz = (unsigned)list.size();
- const int sz = list.size();
+ QList<QPointF> tmp_points, all_points;
+ tmp_points.reserve(sz); all_points.reserve(sz);
- QList<QPointF> ret_list;
- ret_list.reserve(sz);
+ const double maxx = max_input();
- for (int i = 0; i < sz; i++)
+ for (unsigned i = 0; i < sz; i++)
{
- QPointF& pt(list[i]);
-
- const bool overlap = progn(
- for (int j = 0; j < i; j++)
- {
- QPointF& pt2(list[j]);
- const double dist_sq = (pt.x() - pt2.x())*(pt.x() - pt2.x());
- static constexpr double overlap = .6;
- if (dist_sq < overlap * overlap)
- return true;
- }
- return false;
- );
+ QPointF& pt{list[i]};
+
+ bool overlap = false;
+ for (unsigned j = i+1; j < sz; j++)
+ {
+ const QPointF& pt2{list[j]};
+ const QPointF tmp(pt - pt2);
+ const double dist_sq = QPointF::dotProduct(tmp, tmp);
+ constexpr double min_dist = 1e-4;
+ if (dist_sq < min_dist)
+ {
+ overlap = true;
+ break;
+ }
+ }
+
if (!overlap)
- ret_list.push_back(pt);
+ {
+ all_points.append(pt); // all points total
+
+ // points within selected limit, for use in `update_interp_data'
+ if (pt.x() - 1e-4 <= maxx && pt.x() >= 0)
+ tmp_points.push_back(pt);
+ }
+ }
+
+ // simply storing to s->points fires bundle::changed leading to a livelock
+ // hence only store if we can't help it
+
+ if (all_points.size() < points.size())
+ {
+ // all points that don't overlap
+ points = std::move(all_points);
+ s->points = points;
}
- if (ret_list != s->points)
- s->points = ret_list;
+ if (tmp_points.size() < list.size())
+ // points that are within currently-specified bounds
+ list = std::move(tmp_points);
- last_input_value = QPointF(0, 0);
+ last_input_value = {};
activep = false;
- validp = false;
}
-// the return value is only safe to use with no spline::set_bundle calls
-mem<spline::settings> spline::get_settings()
+std::shared_ptr<base_settings> spline::get_settings()
{
- QMutexLocker foo(&_mutex);
- return s;
+ QMutexLocker foo(&mtx);
+ return std::static_pointer_cast<base_settings>(s);
}
-mem<const spline::settings> spline::get_settings() const
+std::shared_ptr<const base_settings> spline::get_settings() const
{
- QMutexLocker foo(&_mutex);
- return s;
+ QMutexLocker foo(&mtx);
+ return std::static_pointer_cast<const base_settings>(s);
}
-double spline::precision(const QList<QPointF>& points) const
+double spline::bucket_size_coefficient(const QList<QPointF>& points) const
{
- // this adjusts the memoized range to the largest X value. empty space doesn't take value_count discrete points.
- const int sz = element_count(points, max_x);
- if (sz)
- return clamp(value_count / clamp(points[sz - 1].x(), 1., max_x), 0., double(value_count));
+ constexpr double eps = 1e-4;
+ const double maxx = max_input();
- return value_count / clamp(max_x, 1., double(value_count));
-}
+ if (maxx < eps)
+ return 0;
-namespace spline_detail {
+ // buckets are only used between 0 and the rightmost point's
+ // x coordinate. even if `clamp_x' is set to a much larger value,
+ // buckets retain more precision lower the x coordinate of the
+ // last point.
-settings::settings(bundle b):
- b(b ? b : make_bundle("")),
- points(b, "points", QList<QPointF>())
-{}
+ const int sz = element_count(points, maxx);
+ const double last_x = sz ? points[sz - 1].x() : maxx;
-settings::~settings()
-{
+ return std::clamp((value_count-1) / std::clamp(last_x, eps, maxx), 0., (value_count-1.));
}
+void spline::disconnect_signals()
+{
+ if (conn_points)
+ {
+ QObject::disconnect(conn_points); conn_points = {};
+ QObject::disconnect(conn_maxx); conn_maxx = {};
+ QObject::disconnect(conn_maxy); conn_maxy = {};
+ }
}
+
+settings::settings(bundle const& b, const QString& axis_name, Axis idx):
+ b(b ? b : make_bundle({})),
+ opts(axis_name, idx)
+{}
+
+} // ns spline_detail