diff options
Diffstat (limited to 'spline/spline.cpp')
| -rw-r--r-- | spline/spline.cpp | 575 |
1 files changed, 309 insertions, 266 deletions
diff --git a/spline/spline.cpp b/spline/spline.cpp index fc77bf8b..466a9a7f 100644 --- a/spline/spline.cpp +++ b/spline/spline.cpp @@ -1,11 +1,3 @@ -/* 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" @@ -18,200 +10,211 @@ #include <QObject> #include <QMutexLocker> -#include <QCoreApplication> #include <QPointF> #include <QSettings> #include <QString> #include <QDebug> -using namespace spline_detail; +namespace spline_detail { + +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) : - axis(axis) +spline::spline(const QString& name, const QString& axis_name, Axis axis) { set_bundle(options::make_bundle(name), axis_name, axis); } spline::~spline() { - QMutexLocker l(&_mutex); - - if (connection) - { - QObject::disconnect(connection); - QObject::disconnect(conn_maxx); - QObject::disconnect(conn_maxy); - connection = QMetaObject::Connection(); - conn_maxx = QMetaObject::Connection(); - conn_maxy = QMetaObject::Connection(); - } + QMutexLocker l(&mtx); + disconnect_signals(); } -spline::spline() : spline("", "", Axis(-1)) {} +spline::spline() : spline(QString{}, QString{}, Axis(-1)) {} -void spline::set_tracking_active(bool value) +void spline::set_tracking_active(bool value) const { - QMutexLocker l(&_mutex); - 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(&_mutex); // avoid logic errors due to changes in value<t> data + QMutexLocker l(&mtx); return s->b; } void spline::clear() { - QMutexLocker l(&_mutex); - s->points = points_t(); - validp = false; + 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); - float q = float(x * bucket_size_coefficient(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; } -warn_result_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; - } - - const float sign = signum(x); + const auto sign = (f)signum(x); x = std::abs(x); - const float ret_ = data[std::min(unsigned(x), unsigned(value_count)-1u)]; - return sign * clamp(ret_, 0, 1000); -} - -void spline::add_lone_point() -{ - points_t points; - points.push_back(QPointF(s->opts.clamp_x_, s->opts.clamp_y_)); - - s->points = points; + const auto ret_ = data[std::min(unsigned(x), value_count - 1)]; + return (double)(sign * std::clamp(ret_, (f)0, (f)1000)); } -QPointF spline::ensure_in_bounds(const QList<QPointF>& points, int i) +void spline::ensure_in_bounds(const QList<QPointF>& points, int i, f& x, f& y) { 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_input) { - const unsigned sz = points.size(); - for (unsigned k = 0; k < sz; k++) + const int sz = points.size(); + for (int k = sz-1; k >= 0; k--) { const QPointF& pt = points[k]; - if (max_input > 1e-4 && pt.x() - 1e-2 > max_input) + 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(); } -void spline::update_interp_data() +void spline::update_interp_data() const { - points_t points = s->points; - ensure_valid(points); - const int sz = points.size(); + points_t list = points; + ensure_valid(list); + int sz = list.size(); - const double maxx = max_input(); - - if (sz == 0) - points.prepend(QPointF(maxx, max_output())); + if (list.isEmpty()) + list.prepend({ max_input(), max_output() }); - std::stable_sort(points.begin(), points.begin() + sz, sort_fn); - - const double c = bucket_size_coefficient(points); - const double c_interp = c * 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] = -16; + data[i] = magic_fill_value; - if (sz < 2) + if (sz < 2) // lerp only { - if (points[0].x() - 1e-2 < maxx) + 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); + + 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) + { + 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 unsigned max = (unsigned)clamp(iround(x * c), 1, value_count-1); - for (unsigned 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() <= maxx) - 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, i - 1); - const QPointF p1 = ensure_in_bounds(points, i + 0); - const QPointF p2 = ensure_in_bounds(points, i + 1); - const QPointF p3 = ensure_in_bounds(points, 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 @@ -219,275 +222,315 @@ void spline::update_interp_data() }; // multiplier helps fill in all the x's needed - const unsigned end = int(c_interp * (p2_x - p1_x)) + 1; + const unsigned end = (unsigned)(c_f * (p2_x - p1_x)) + 1; + const f end_(end); for (unsigned k = 0; k <= end; k++) { - const double t = k / double(end); - const double t2 = t*t; - const double t3 = t*t*t; - - const int x = int(.5 * c * (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 (unsigned(x) < value_count) - data[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] == -16) + 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_input()); + 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_input()); + 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(); } -spline::points_t 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, s->opts.clamp_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() { - QMutexLocker foo(&_mutex); + QMutexLocker foo(&mtx); s->b->save(); } -void spline::invalidate_settings() +void spline::invalidate_settings_() { - // we're holding the mutex to allow signal disconnection in spline dtor - // before this slot gets called for the next time - - QMutexLocker l(&_mutex); - validp = false; + points = s->points; + clamp_x = s->opts.clamp_x_; + clamp_y = s->opts.clamp_y_; + update_interp_data(); +} - emit s->recomputed(); +void spline::invalidate_settings() +{ + std::shared_ptr<settings> S; + { + QMutexLocker l(&mtx); + S = s; + invalidate_settings_(); + } + emit S->recomputed(); } void spline::set_bundle(bundle b, const QString& axis_name, Axis axis) { - QMutexLocker l(&_mutex); + if (!b) + b = make_bundle({}); - // 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) - { - s = std::make_shared<settings>(b, axis_name, axis); + std::shared_ptr<settings> S; - if (connection) - { - QObject::disconnect(connection); - QObject::disconnect(conn_maxx); - QObject::disconnect(conn_maxy); - } - - if (b) - { - connection = QObject::connect(b.get(), &bundle_::changed, - s.get(), [&]() { invalidate_settings(); }); - - // this isn't strictly necessary for the spline but helps the widget - conn_maxx = QObject::connect(&s->opts.clamp_x_, base_value::value_changed<int>(), - ctx.get(), [&](double) { invalidate_settings(); }); - conn_maxy = QObject::connect(&s->opts.clamp_y_, base_value::value_changed<int>(), - ctx.get(), [&](double) { invalidate_settings(); }); - } + { + QMutexLocker l(&mtx); - validp = false; + 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(); } double spline::max_input() const { - QMutexLocker l(&_mutex); - if (s) - { - using m = axis_opts::max_clamp; - const value<m>& clamp = s->opts.clamp_x_; - const QList<QPointF> points = s->points; - if (clamp == m::x1000 && points.size()) - return points[points.size() - 1].x(); - return s ? std::fabs(clamp.to<double>()) : 0; - } - return 0; + 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); } double spline::max_output() const { - QMutexLocker l(&_mutex); - if (s) - { - using m = axis_opts::max_clamp; - const value<m>& clamp = s->opts.clamp_y_; - const QList<QPointF> points = s->points; - if (clamp == m::x1000 && points.size()) - return points[points.size() - 1].y(); - return s ? std::fabs(clamp.to<double>()) : 0; - } - return 0; + 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); } -void spline::ensure_valid(QList<QPointF>& the_points) +void spline::ensure_valid(points_t& list) const { - QMutexLocker foo(&_mutex); - - QList<QPointF> list = the_points; - - // storing to s->points fires bundle::changed and that leads to an infinite loop - // thus, only store if we can't help it std::stable_sort(list.begin(), list.end(), sort_fn); - const int sz = list.size(); + const unsigned sz = (unsigned)list.size(); - QList<QPointF> ret_list, ret_list_2; - ret_list.reserve(sz), ret_list_2.reserve(sz); + QList<QPointF> tmp_points, all_points; + tmp_points.reserve(sz); all_points.reserve(sz); - const double maxx = max_input(), maxy = max_output(); + const double maxx = max_input(); - for (int i = 0; i < sz; i++) + for (unsigned i = 0; i < sz; i++) { - QPointF& pt(list[i]); + QPointF& pt{list[i]}; - const bool overlap = progn( - for (int j = 0; j < i; j++) + 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) { - const QPointF& pt2(list[j]); - const QPointF tmp(pt - pt2); - const double dist_sq = QPointF::dotProduct(tmp, tmp); - const double overlap = maxx / 500.; - if (dist_sq < overlap * overlap) - return true; + overlap = true; + break; } - return false; - ); + } if (!overlap) - ret_list_2.append(pt); - - if (pt.x() - 1e-2 < maxx && pt.x() >= 0 && - pt.y() - 1e-2 < maxy && pt.y() >= 0 && !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); } } - if (ret_list != the_points) + // 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()) { - s->points = std::move(ret_list_2); - the_points = std::move(ret_list); + // all points that don't overlap + points = std::move(all_points); + s->points = points; } - last_input_value = QPointF(0, 0); + if (tmp_points.size() < list.size()) + // points that are within currently-specified bounds + list = std::move(tmp_points); + + last_input_value = {}; activep = false; } -// the return value is only safe to use with no spline::set_bundle calls std::shared_ptr<base_settings> spline::get_settings() { - QMutexLocker foo(&_mutex); + QMutexLocker foo(&mtx); return std::static_pointer_cast<base_settings>(s); } std::shared_ptr<const base_settings> spline::get_settings() const { - QMutexLocker foo(&_mutex); + QMutexLocker foo(&mtx); return std::static_pointer_cast<const base_settings>(s); } double spline::bucket_size_coefficient(const QList<QPointF>& points) const { constexpr double eps = 1e-4; - const double maxx = max_input(); if (maxx < eps) return 0; - // needed to fill the buckets up to the last control point. - // space between that point and max_x doesn't matter. + // 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. const int sz = element_count(points, maxx); const double last_x = sz ? points[sz - 1].x() : maxx; - return clamp((value_count-1) / clamp(last_x, eps, maxx), 0., (value_count-1)); + return std::clamp((value_count-1) / std::clamp(last_x, eps, maxx), 0., (value_count-1.)); } -namespace spline_detail { - -settings::settings(bundle b, const QString& axis_name, Axis idx): - b(b ? b : make_bundle("")), - points(b, "points", {}), - opts(axis_name, idx) -{} - -settings::~settings() +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 |
