diff options
Diffstat (limited to 'spline')
| -rw-r--r-- | spline/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | spline/axis-opts.cpp | 50 | ||||
| -rw-r--r-- | spline/axis-opts.hpp | 70 | ||||
| -rw-r--r-- | spline/broken/qfunctionconfiguratorplugin.h | 2 | ||||
| -rw-r--r-- | spline/lang/de_DE.ts | 4 | ||||
| -rw-r--r-- | spline/lang/zh_CN.ts | 4 | ||||
| -rw-r--r-- | spline/spline-widget.cpp | 611 | ||||
| -rw-r--r-- | spline/spline-widget.hpp | 80 | ||||
| -rw-r--r-- | spline/spline.cpp | 637 | ||||
| -rw-r--r-- | spline/spline.hpp | 179 |
10 files changed, 976 insertions, 664 deletions
diff --git a/spline/CMakeLists.txt b/spline/CMakeLists.txt index d252004b..731c0b6e 100644 --- a/spline/CMakeLists.txt +++ b/spline/CMakeLists.txt @@ -1,2 +1 @@ -otr_module(spline NO-COMPAT BIN) -target_link_libraries(opentrack-spline opentrack-options opentrack-compat) +otr_module(spline BIN) diff --git a/spline/axis-opts.cpp b/spline/axis-opts.cpp new file mode 100644 index 00000000..a2b4941d --- /dev/null +++ b/spline/axis-opts.cpp @@ -0,0 +1,50 @@ +#include "axis-opts.hpp" + +namespace axis_opts_impl { + +using max_clamp = axis_opts::max_clamp; + +static max_clamp get_max_x(Axis k) +{ + if (k == Axis(-1)) + return max_clamp::x1000; + if (k == Pitch) + return max_clamp::r90; + if (k >= Yaw) + return max_clamp::r180; + return max_clamp::t30; +} + +static max_clamp get_max_y(Axis k) +{ + if (k == Axis(-1)) + return max_clamp::x1000; + if (k == Axis::Pitch) + return max_clamp::o_r90; + if (k >= Axis::Yaw) + return max_clamp::o_r180; + return max_clamp::o_t75; +} + +axis_opts::axis_opts(QString pfx, Axis idx) : + prefix_(pfx), + axis_(idx), + zero(b_settings_window, n(pfx, "zero-pos"), 0), + src(b_settings_window, n(pfx, "source-index"), idx), + invert_post(b_settings_window, n(pfx, "invert-sign-post"), false), + invert_pre(b_settings_window, n(pfx, "invert-sign"), false), + altp(b_mapping_window, n(pfx, "alt-axis-sign"), false), + clamp_x_(b_mapping_window, n(pfx, "max-value"), get_max_x(idx)), + clamp_y_(b_mapping_window, n(pfx, "max-output-value"), get_max_y(idx)) +{} + +QString const& axis_opts::prefix() const { return prefix_; } + +Axis axis_opts::axis() const { return axis_; } + +QString axis_opts::n(QString const& pfx, QString const& name) +{ + return QString("%1-%2").arg(pfx, name); +} + +} // ns axis_opts_impl diff --git a/spline/axis-opts.hpp b/spline/axis-opts.hpp new file mode 100644 index 00000000..437a5faa --- /dev/null +++ b/spline/axis-opts.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "options/options.hpp" +#include "api/plugin-api.hpp" + +#include "export.hpp" + +namespace axis_opts_impl { + +using namespace options; + +class OTR_SPLINE_EXPORT axis_opts final +{ + QString prefix_; + Axis axis_; + + static inline QString n(QString const& pfx, QString const& name); + +public: + enum max_clamp + { + r180 = 180, + r90 = 90, + r60 = 60, + r45 = 45, + r30 = 30, + r25 = 25, + r20 = 20, + r15 = 15, + r10 = 10, + + t600 = 600, + t300 = 300, + t150 = 150, + t100 = 100, + t75 = 75, + t30 = 30, + t20 = 20, + t15 = 15, + t10 = 10, + + o_r180 = -180, + o_r90 = -90, + o_t75 = -75, + o_t100 = -100, + o_t150 = -150, + o_t300 = -300, + o_t600 = -600, + + x1000 = 1000, + }; + + // note, these two bundles can be the same value with no issues + bundle b_settings_window{ make_bundle(axis_ == Axis(-1) ? QString() : "opentrack-ui") }; + bundle b_mapping_window{ make_bundle(axis_ == Axis(-1) ? QString() : "opentrack-mappings") }; + value<double> zero; + value<int> src; + value<bool> invert_pre, invert_post, altp; + value<max_clamp> clamp_x_, clamp_y_; + double max_clamp_x() const { return std::fabs(clamp_x_.to<double>()); } + double max_clamp_y() const { return std::fabs(clamp_y_.to<double>()); } + axis_opts(QString pfx, Axis idx); + + QString const& prefix() const; + Axis axis() const; +}; + +} // ns axis_opts_impl + +using axis_opts = axis_opts_impl::axis_opts; diff --git a/spline/broken/qfunctionconfiguratorplugin.h b/spline/broken/qfunctionconfiguratorplugin.h index ca68e0e2..11b9a58c 100644 --- a/spline/broken/qfunctionconfiguratorplugin.h +++ b/spline/broken/qfunctionconfiguratorplugin.h @@ -16,7 +16,7 @@ class QFunctionConfiguratorPlugin : public QObject, public QDesignerCustomWidget Q_INTERFACES(QDesignerCustomWidgetInterface) public: - QFunctionConfiguratorPlugin(QObject *parent = 0); + QFunctionConfiguratorPlugin(QObject *parent = nullptr); bool isContainer() const; bool isInitialized() const; diff --git a/spline/lang/de_DE.ts b/spline/lang/de_DE.ts new file mode 100644 index 00000000..1552582e --- /dev/null +++ b/spline/lang/de_DE.ts @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="de_DE"> +</TS> diff --git a/spline/lang/zh_CN.ts b/spline/lang/zh_CN.ts new file mode 100644 index 00000000..e5ca8aa9 --- /dev/null +++ b/spline/lang/zh_CN.ts @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="zh_CN"> +</TS> diff --git a/spline/spline-widget.cpp b/spline/spline-widget.cpp index c7dd5e5a..46e2095c 100644 --- a/spline/spline-widget.cpp +++ b/spline/spline-widget.cpp @@ -1,74 +1,59 @@ -/* 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 "compat/util.hpp" #include "spline-widget.hpp" +#include "compat/math.hpp" + +#include <algorithm> + #include <QPainter> #include <QPainterPath> -#include <QPaintEvent> -#include <QPen> #include <QPixmap> -#include <QList> -#include <QPoint> #include <QString> -#include <QRect> -#include <QApplication> -#include <QStyle> -#include <QMouseEvent> +#include <QToolTip> +#include <QtEvents> +#include <QPainterPath> #include <QDebug> -#include <cmath> -#include <algorithm> +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) +# define OTR_OBSOLETE_QT_WORKAROUND +#endif -spline_widget::spline_widget(QWidget *parent) : - QWidget(parent), - _config(nullptr), - snap_x(0), - snap_y(0), - _x_step(10), - _y_step(10), - moving_control_point_idx(-1), - _draw_function(true), - _preview_only(false) +namespace spline_detail { + +spline_widget::spline_widget(QWidget *parent) : QWidget(parent) { setMouseTracking(true); - setFocusPolicy(Qt::ClickFocus); + //setFocusPolicy(Qt::ClickFocus); setCursor(Qt::ArrowCursor); } spline_widget::~spline_widget() { if (connection) + { QObject::disconnect(connection); + connection = {}; + } } -void spline_widget::setConfig(spline* spl) +void spline_widget::set_config(base_spline* spl) { - if (spl != _config) + if (connection) { - if (connection) - { - QObject::disconnect(connection); - connection = QMetaObject::Connection(); - } + QObject::disconnect(connection); + connection = {}; + } - if (spl) - { - mem<spline::settings> s = spl->get_settings(); - connection = connect(s.get(), &spline::settings::recomputed, - this, [this]() { reload_spline(); }, - Qt::QueuedConnection); - } + config = spl; - _config = spl; - _background = QPixmap(); - update_range(); + if (spl) + { + std::shared_ptr<base_settings> s = spl->get_settings(); + connection = connect(&*s, &base_settings::recomputed, + this, [this] { reload_spline(); }, + Qt::QueuedConnection); } + + reload_spline(); } QColor spline_widget::colorBezier() const @@ -76,45 +61,46 @@ QColor spline_widget::colorBezier() const return spline_color; } -void spline_widget::setColorBezier(QColor color) +void spline_widget::setColorBezier(QColor const& color) { spline_color = color; - update(); + repaint(); } void spline_widget::force_redraw() { - _background = QPixmap(); - update(); + background_img = {}; + repaint(); } void spline_widget::set_preview_only(bool val) { - _preview_only = val; + preview_only = val; } bool spline_widget::is_preview_only() const { - return _preview_only; + return preview_only; } void spline_widget::drawBackground() { - _background = QPixmap(width(), height()); - - QPainter painter(&_background); + QPainter painter(&background_img); - painter.fillRect(rect(), palette().color(QPalette::Light)); + painter.fillRect(rect(), widget_bg_color); - QColor bg_color(112, 154, 209); - if (!isEnabled() && !_preview_only) - bg_color = QColor(176,176,180); - painter.fillRect(pixel_bounds, bg_color); + { + QColor bg_color(112, 154, 209); + if (!isEnabled() && !preview_only) + bg_color = QColor(176,176,180); + painter.fillRect(pixel_bounds, bg_color); + } QFont font; font.setPointSize(8); + font.setStyleHint(QFont::Monospace, QFont::PreferAntialias); painter.setFont(font); - QFontMetrics metrics(font); + const QFontMetricsF metrics(font); QColor color__(176, 190, 209, 127); @@ -123,169 +109,213 @@ void spline_widget::drawBackground() const QPen pen(color__, 1, Qt::SolidLine, Qt::FlatCap); - const int ystep = _y_step, xstep = _x_step; - const qreal maxx = _config->max_input(); - const qreal maxy = _config->max_output(); + const int ystep = (int)std::ceil(y_step_), xstep = (int)std::ceil(x_step_); + const double maxx = config->max_input(); + const double maxy = config->max_output(); +#ifndef OTR_OBSOLETE_QT_WORKAROUND + double space_width = metrics.horizontalAdvance(' '); +#else + double space_width = metrics.boundingRect(' ').right(); +#endif + painter.setPen(palette().text().color()); - // horizontal grid + // vertical grid for (int i = 0; i <= maxy; i += ystep) { - const int y = int(pixel_bounds.height() - i * c.y() + pixel_bounds.y()); + const double y = pixel_bounds.height() - i * c.y() + pixel_bounds.y(); drawLine(painter, - QPoint(pixel_bounds.x(), y), - QPoint(pixel_bounds.x() + pixel_bounds.width(), y), + QPointF(pixel_bounds.x(), y), + QPointF(pixel_bounds.x() + pixel_bounds.width(), y), pen); - painter.drawText(QRectF(10, - y - metrics.height()/2., - pixel_bounds.left(), - metrics.height()), - QString::number(i)); + QString text = QString::number(i); + QRectF rect = metrics.boundingRect(text); +#ifndef OTR_OBSOLETE_QT_WORKAROUND + double advance = metrics.horizontalAdvance(text); +#else + double advance = rect.right(); +#endif + painter.drawText(QPointF(pixel_bounds.x() - advance - space_width, + y - rect.height()/2 - rect.top()), + text); } - // vertical grid + // horizontal grid for (int i = 0; i <= maxx; i += xstep) { - const int x = iround(pixel_bounds.x() + i * c.x()); + const double x = pixel_bounds.x() + i * c.x(); drawLine(painter, - QPoint(x, pixel_bounds.y()), - QPoint(x, pixel_bounds.y() + pixel_bounds.height()), + QPointF(x, pixel_bounds.y()), + QPointF(x, pixel_bounds.y() + pixel_bounds.height()), pen); + const QString text = QString::number(i); - painter.drawText(QRectF(x - metrics.width(text)/2., - pixel_bounds.height() + 10 + metrics.height(), - metrics.width(text), - metrics.height()), + QRectF rect = metrics.boundingRect(text); +#ifndef OTR_OBSOLETE_QT_WORKAROUND + double advance = metrics.horizontalAdvance(text); +#else + double advance = metrics.boundingRect(text).right(); +#endif + painter.drawText(QPointF(x - advance/2 - rect.left(), + pixel_bounds.bottom() + metrics.lineSpacing()), text); } } void spline_widget::drawFunction() { - _function = _background; - QPainter painter(&_function); + QPainter painter(&spline_img); painter.setRenderHint(QPainter::Antialiasing, true); - const points_t points = _config->get_points(); + const points_t& points = config->get_points(); if (moving_control_point_idx >= 0 && moving_control_point_idx < points.size()) { const QPen pen(Qt::white, 1, Qt::SolidLine, Qt::FlatCap); - const QPointF prev_ = point_to_pixel(QPointF(0, 0)); - QPoint prev(iround(prev_.x()), iround(prev_.y())); - for (int i = 0; i < points.size(); i++) + QPointF prev = point_to_pixel({}); + for (const auto& point : points) { - const QPointF tmp_ = point_to_pixel(points[i]); - const QPoint tmp(iround(tmp_.x()), iround(tmp_.y())); + const QPointF tmp = point_to_pixel(point); drawLine(painter, prev, tmp, pen); prev = tmp; } } - const QColor color = progn - ( - if (!isEnabled() && !_preview_only) - { - QColor color(spline_color); - const int avg = int(float(color.red() + color.green() + color.blue())/3); - return QColor(int(float(color.red() + avg) * .5f), - int(float(color.green() + avg) * .5f), - int(float(color.blue() + avg) * .5f), - 96); - } - else - { - QColor color(spline_color); - color.setAlphaF(color.alphaF() * .9); - return color; - } - ); - - painter.setPen(QPen(color, 1.75, Qt::SolidLine, Qt::FlatCap)); - -//#define DEBUG_SPLINE -#ifndef DEBUG_SPLINE - static constexpr double step_ = 3./3; - - const double maxx = _config->max_input(); - const double step = step_ / c.x(); + const QColor color_ = progn( + if (!isEnabled() && !preview_only) + { + QColor color(spline_color); + const int avg = int(float(color.red() + color.green() + color.blue())/3); + return QColor(int(float(color.red() + avg)*.5f), + int(float(color.green() + avg)*.5f), + int(float(color.blue() + avg)*.5f), + 96); + } + else + { + QColor color(spline_color); + color.setAlphaF(color.alphaF()*.9); + return color; + } + ); + + painter.setPen(QPen(color_, 2, Qt::SolidLine, Qt::FlatCap)); + + const double dpr = screen_dpi(); + const double line_length_pixels = std::fmax(1, dpr); + const double step = std::fmax(.1, line_length_pixels / c.x()); + const double maxx = config->max_input(); +#define USE_CUBIC_SPLINE +#if defined USE_CUBIC_SPLINE QPainterPath path; - path.moveTo(point_to_pixel(QPointF(0, 0))); + path.moveTo(point_to_pixel({})); - for (double k = 0; k < maxx; k += step*3) - { - const float next_1(_config->get_value_no_save(k + step*1)); - const float next_2(_config->get_value_no_save(k + step*2)); - const float next_3(_config->get_value_no_save(k + step*3)); + const double max_x_pixel = point_to_pixel({maxx, 0}).x(); - const QPointF b(point_to_pixel_(QPointF(k + step*1, qreal(next_1)))), - c(point_to_pixel_(QPointF(k + step*2, qreal(next_2)))), - d(point_to_pixel_(QPointF(k + step*3, qreal(next_3)))); + auto clamp = [=](const QPointF& val) { + return val.x() <= max_x_pixel + ? val + : QPointF{max_x_pixel, val.y()}; + }; - path.cubicTo(b, c, d); - } + const auto fn = [&] (double k) { + const auto next_1 = config->get_value_no_save(k + step*1); + const auto next_2 = config->get_value_no_save(k + step*2); + const auto next_3 = config->get_value_no_save(k + step*3); + + QPointF b(clamp(point_to_pixel({k + step*1, next_1}))), + c(clamp(point_to_pixel({k + step*2, next_2}))), + d(clamp(point_to_pixel({k + step*3, next_3}))); + + path.cubicTo(b, c, d); + }; + + for (double k = 0; k < maxx; k += step*3) // NOLINT + fn(k); + + fn(maxx); painter.drawPath(path); #else - static constexpr int line_length_pixels = 3; - const qreal max = _config->maxInput(); - const qreal step = clamp(line_length_pixels / c.x(), 5e-2, max); - QPointF prev = point_to_pixel(QPoint(0, 0)); - for (qreal i = 0; i < max; i += step) + QPointF prev = point_to_pixel({}); + for (double i = 0; i < maxx; i += step) // NOLINT { - const qreal val = qreal(_config->get_value_no_save(i)); - const QPointF cur = point_to_pixel(QPointF(i, val)); + const auto val = config->get_value_no_save(i); + const QPointF cur = point_to_pixel({i, val}); painter.drawLine(prev, cur); prev = cur; } { - const qreal maxx = _config->maxInput(); - const qreal maxy = qreal(_config->get_value_no_save(maxx)); - painter.drawLine(QPointF(prev), point_to_pixel_(QPointF(maxx, maxy))); + const double maxx = config->max_input(); + const double maxy = config->get_value_no_save(maxx); + painter.drawLine(prev, point_to_pixel({ maxx, maxy })); } #endif + const QRect r1(pixel_bounds.left(), 0, width() - pixel_bounds.left(), pixel_bounds.top()), + r2(pixel_bounds.right(), 0, width() - pixel_bounds.right(), pixel_bounds.bottom()); + + // prevent topward artifacts the lazy way + painter.fillRect(r1, widget_bg_color); + // same for rightward artifacts + painter.fillRect(r2, widget_bg_color); + const int alpha = !isEnabled() ? 64 : 120; - if (!_preview_only) + if (!preview_only) { - for (int i = 0; i < points.size(); i++) + for (auto const& point : points) { drawPoint(painter, - point_to_pixel(points[i]), + point_to_pixel(point), QColor(200, 200, 210, alpha), isEnabled() ? QColor(50, 100, 120, 200) : QColor(200, 200, 200, 96)); } } } +QSize spline_widget::minimumSizeHint() const +{ + const double dpi = screen_dpi(); + return { iround(800 * dpi), iround(250 * dpi) }; +} + void spline_widget::paintEvent(QPaintEvent *e) { - if (!_config) + if (!config) return; QPainter p(this); - if (_background.isNull()) + const double dpr = devicePixelRatioF(); + const int W = iround(width() * dpr); + const int H = iround(height() * dpr); + + if (background_img.size() != QSize(W, H)) { - _draw_function = true; + update_range(); + + background_img = { W, H }; + background_img.setDevicePixelRatio(dpr); drawBackground(); + draw_function = true; } - if (_draw_function) + if (draw_function) { - _draw_function = false; + draw_function = false; + spline_img = background_img; drawFunction(); } - p.drawPixmap(e->rect(), _function); + p.drawPixmap(e->rect(), spline_img); // If the Tracker is active, the 'Last Point' it requested is recorded. // Show that point on the graph, with some lines to assist. // This new feature is very handy for tweaking the curves! QPointF last; - if (_config->get_last_value(last) && isEnabled()) + if (config->get_last_value(last) && isEnabled()) drawPoint(p, point_to_pixel(last), QColor(255, 0, 0, 120)); } @@ -294,13 +324,13 @@ void spline_widget::drawPoint(QPainter& painter, const QPointF& pos, const QColo painter.save(); painter.setPen(QPen(border, 1, Qt::SolidLine, Qt::PenCapStyle::FlatCap)); painter.setBrush(colBG); - painter.drawEllipse(QRectF(pos.x() - point_size, - pos.y() - point_size, - point_size*2, point_size*2)); + painter.drawEllipse(QRectF{pos.x() - point_size_in_pixels, + pos.y() - point_size_in_pixels, + point_size_in_pixels*2, point_size_in_pixels*2}); painter.restore(); } -void spline_widget::drawLine(QPainter& painter, const QPoint& start, const QPoint& end, const QPen& pen) +void spline_widget::drawLine(QPainter& painter, const QPointF& start, const QPointF& end, const QPen& pen) { painter.save(); painter.setPen(pen); @@ -311,38 +341,38 @@ void spline_widget::drawLine(QPainter& painter, const QPoint& start, const QPoin void spline_widget::mousePressEvent(QMouseEvent *e) { - if (!_config || !isEnabled() || !is_in_bounds(e->pos())) - { - clearFocus(); + if (!config || !isEnabled() || !is_in_bounds(e->localPos()) || preview_only) return; - } - const int point_pixel_closeness_limit = get_closeness_limit(); + const double min_dist = min_pt_distance(); + + moving_control_point_idx = -1; + + const points_t& points = config->get_points(); - points_t points = _config->get_points(); if (e->button() == Qt::LeftButton) { - bool bTouchingPoint = false; - moving_control_point_idx = -1; + bool is_touching_point = false; + for (int i = 0; i < points.size(); i++) { - if (point_within_pixel(points[i], e->pos())) + if (point_within_pixel(points[i], e->localPos())) { - bTouchingPoint = true; - moving_control_point_idx = int(i); + is_touching_point = true; + moving_control_point_idx = i; break; } } - if (!bTouchingPoint) + + if (!is_touching_point) { bool too_close = false; - const QPoint pos = e->pos(); + const QPointF pos = pixel_to_point(e->localPos()); - for (int i = 0; i < points.size(); i++) + for (QPointF const& point : points) { - const QPointF pt = point_to_pixel(points[i]); - const qreal x = std::fabs(pt.x() - pos.x()); - if (point_pixel_closeness_limit >= x) + const double x = std::fabs(point.x() - pos.x()); + if (min_dist > x) { too_close = true; break; @@ -351,94 +381,91 @@ void spline_widget::mousePressEvent(QMouseEvent *e) if (!too_close) { - _config->add_point(pixel_coord_to_point(e->pos())); + config->add_point(pixel_to_point(e->localPos())); show_tooltip(e->pos()); } } + + draw_function = true; } if (e->button() == Qt::RightButton) { - if (_config) + if (config) { - int found_pt = -1; for (int i = 0; i < points.size(); i++) { - if (point_within_pixel(points[i], e->pos())) + if (point_within_pixel(points[i], e->localPos())) { - found_pt = i; + config->remove_point(i); + draw_function = true; break; } } - - if (found_pt != -1) - { - _config->remove_point(found_pt); - } - moving_control_point_idx = -1; } } - _draw_function = true; - update(); + + if (draw_function) + repaint(); } void spline_widget::mouseMoveEvent(QMouseEvent *e) { - if (_preview_only && _config) + if (preview_only && config) { show_tooltip(e->pos()); - clearFocus(); return; } - if (!_config || !isEnabled() || !isActiveWindow() || (moving_control_point_idx != -1 && !hasFocus())) + if (!config || !isEnabled() || !isActiveWindow()) { - clearFocus(); + QToolTip::hideText(); return; } const int i = moving_control_point_idx; - const points_t points = _config->get_points(); + const points_t& points = config->get_points(); const int sz = points.size(); if (i >= 0 && i < sz) { - const int point_pixel_closeness_limit = get_closeness_limit(); - bool overlap = false; + const double min_dist = min_pt_distance(); + QPointF new_pt = pixel_to_point(e->localPos()); + + const bool has_prev = i > 0, has_next = i + 1 < points.size(); - const QPoint pix = progn( - // takes snap into account - const QPointF pix_ = point_to_pixel(pixel_coord_to_point(e->pos())); - return QPoint(int(pix_.x()), int(pix_.y())); - ); + auto check_next = [&] { + return points[i+1].x() - new_pt.x() >= min_dist; + }; - QPointF new_pt = pixel_coord_to_point(e->pos()); + auto check_prev = [&] { + return new_pt.x() - points[i-1].x() >= min_dist; + }; - if (i + 1 < points.size()) + if (has_prev && !check_prev()) { - const QPointF other_pix = point_to_pixel(points[i+1]); - overlap |= other_pix.x() - pix.x() < point_pixel_closeness_limit; + new_pt.rx() = points[i-1].x() + min_dist + 1e-4; } - if (i != 0) + + if (has_next && !check_next()) { - const QPointF other_pix = point_to_pixel(points[i-1]); - overlap |= pix.x() - other_pix.x() < point_pixel_closeness_limit; + new_pt.rx() = points[i+1].x() - min_dist - 1e-4; } - if (overlap) - new_pt.setX(points[i].x()); - - _config->move_point(i, new_pt); - _draw_function = true; - setCursor(Qt::ClosedHandCursor); - show_tooltip(pix, new_pt); - update(); + show_tooltip(point_to_pixel(new_pt).toPoint(), new_pt); + + if ((!has_prev || check_prev()) && (!has_next || check_next())) + { + config->move_point(i, new_pt); + draw_function = true; + repaint(); + } } else if (sz) { int i; - bool is_on_point = is_on_pt(e->pos(), &i); + bool is_on_point = is_on_pt(e->localPos(), &i); if (is_on_point) { @@ -448,7 +475,7 @@ void spline_widget::mouseMoveEvent(QMouseEvent *e) else { setCursor(Qt::ArrowCursor); - if (is_in_bounds(e->pos())) + if (is_in_bounds(e->localPos())) show_tooltip(e->pos()); else QToolTip::hideText(); @@ -458,77 +485,84 @@ void spline_widget::mouseMoveEvent(QMouseEvent *e) void spline_widget::mouseReleaseEvent(QMouseEvent *e) { - if (!_config || !isEnabled() || !isActiveWindow() || !hasFocus()) - { - clearFocus(); + if (!config || !isEnabled() || !isActiveWindow() || preview_only) return; - } + + const bool redraw = moving_control_point_idx != -1; + moving_control_point_idx = -1; if (e->button() == Qt::LeftButton) { { - if (is_on_pt(e->pos(), nullptr)) + if (is_on_pt(e->localPos(), nullptr)) setCursor(Qt::CrossCursor); else setCursor(Qt::ArrowCursor); } - moving_control_point_idx = -1; - _draw_function = true; - if (is_in_bounds(e->pos())) + if (is_in_bounds(e->localPos())) show_tooltip(e->pos()); else QToolTip::hideText(); + } - update(); + if (redraw) + { + draw_function = true; + repaint(); } } void spline_widget::reload_spline() { // don't recompute here as the value's about to be recomputed in the callee - update_range(); + background_img = {}; + update(); } -int spline_widget::get_closeness_limit() +double spline_widget::min_pt_distance() const { - return std::max(iround(snap_x * c.x()), 1); + double pt = 3*point_size_in_pixels / c.x(); + pt = snap(pt, snap_x); + return pt; } void spline_widget::show_tooltip(const QPoint& pos, const QPointF& value_) { - const QPointF value = QPoint(0, 0) == value_ ? pixel_coord_to_point(pos) : value_; + const QPointF value = value_.isNull() ? pixel_to_point(pos) : value_; double x = value.x(), y = value.y(); - if (_preview_only) - y = _config->get_value_no_save(x); + if (preview_only) + y = config->get_value_no_save(x); const int x_ = iround(x), y_ = iround(y); - using std::fabs; - - if (fabs(x_ - x) < 1e-3) + if (std::fabs(x_ - x) < 1e-3) x = x_; - if (fabs(y_ - y) < 1e-3) + if (std::fabs(y_ - y) < 1e-3) y = y_; - const bool is_fusion = QStringLiteral("fusion") == QApplication::style()->objectName(); - const int add_x = (is_fusion ? 25 : 0), add_y = (is_fusion ? 15 : 0); + // the native OSX style doesn't look right otherwise +#if defined __APPLE__ + constexpr int off_x = 0, off_y = 0; +#else + constexpr int off_x = 25, off_y = 15; +#endif - const QPoint pix(int(pos.x()) + add_x, int(pos.y()) + add_y); + const QPoint pix(pos.x() + off_x, pos.y() + off_y); QToolTip::showText(mapToGlobal(pix), - QStringLiteral("value: %1x%2").arg(x).arg(y), + QString{"value: %1x%2"}.arg(x, 0, 'f', 2).arg(y, 0, 'f', 2), this, rect(), 0); } -bool spline_widget::is_in_bounds(const QPoint& pos) const +bool spline_widget::is_in_bounds(const QPointF& pos) const { - static constexpr int grace = point_size * 3; - static constexpr int bottom_grace = int(point_size * 1.5); + const int grace = (int)std::ceil(point_size_in_pixels * 3); + const int bottom_grace = (int)std::ceil(point_size_in_pixels * 1.5); return (pos.x() + grace > pixel_bounds.left() && pos.x() - grace < pixel_bounds.right() && pos.y() + grace > pixel_bounds.top() && @@ -537,27 +571,28 @@ bool spline_widget::is_in_bounds(const QPoint& pos) const void spline_widget::update_range() { - if (!_config) + if (!config) return; const int w = width(), h = height(); const int mwl = 40, mhl = 20; const int mwr = 15, mhr = 35; - pixel_bounds = QRect(mwl, mhl, (w - mwl - mwr), (h - mhl - mhr)); - c = QPointF(pixel_bounds.width() / _config->max_input(), pixel_bounds.height() / _config->max_output()); - _draw_function = true; - - _background = QPixmap(); - _function = QPixmap(); + pixel_bounds = { mwl, mhl, (w - mwl - mwr), (h - mhl - mhr) }; + c = { + pixel_bounds.width() / std::fmax(1, config->max_input()), + pixel_bounds.height() / std::fmax(1, config->max_output()) + }; - update(); + draw_function = true; + background_img = {}; + spline_img = {}; } -bool spline_widget::point_within_pixel(const QPointF& pt, const QPoint &pixel) +bool spline_widget::point_within_pixel(const QPointF& pt, const QPointF& pixel) { const QPointF tmp = pixel - point_to_pixel(pt); - return QPointF::dotProduct(tmp, tmp) < point_size * point_size; + return QPointF::dotProduct(tmp, tmp) < point_size_in_pixels * point_size_in_pixels; } void spline_widget::focusOutEvent(QFocusEvent* e) @@ -565,76 +600,66 @@ void spline_widget::focusOutEvent(QFocusEvent* e) if (moving_control_point_idx != -1) QToolTip::hideText(); moving_control_point_idx = -1; - _draw_function = true; + draw_function = true; lower(); setCursor(Qt::ArrowCursor); e->accept(); } -QPointF spline_widget::pixel_coord_to_point(const QPoint& point) +double spline_widget::snap(double x, double snap_value) { - qreal x = (point.x() - pixel_bounds.x()) / c.x(); - qreal y = (pixel_bounds.height() - point.y() + pixel_bounds.y()) / c.y(); - - static constexpr int c = 1000; - - if (snap_x > 0) + if (snap_value > 0) { - x += snap_x * .5; - x -= std::fmod(x, snap_x); + constexpr int c = 1000; + x += snap_value * .5; + x -= std::fmod(x, snap_value); // truncate after few decimal places to reduce rounding errors. - // round upward to nearest. + // round upward. x = int(x * c + .5/c) / double(c); } - if (snap_y > 0) - { - y += snap_y * .5; - y -= std::fmod(y, snap_y); - // idem - y = int(y * c + .5/c) / double(c); - } - if (x < 0) - x = 0; - if (x > _config->max_input()) - x = _config->max_input(); - - if (y < 0) - y = 0; - if (y > _config->max_output()) - y = _config->max_output(); - - return QPointF(x, y); + return x; } -QPointF spline_widget::point_to_pixel_(const QPointF& point) +QPointF spline_widget::pixel_to_point(const QPointF& point) { - return QPointF(pixel_bounds.x() + point.x() * c.x(), - pixel_bounds.y() + pixel_bounds.height() - point.y() * c.y()); + double x = (point.x() - pixel_bounds.x()) / c.x(); + double y = (pixel_bounds.height() - point.y() + pixel_bounds.y()) / c.y(); + + if (snap_x > 0) + x = snap(x, snap_x); + if (snap_y > 0) + y = snap(y, snap_y); + + x = std::clamp(x, 0., config->max_input()); + y = std::clamp(y, 0., config->max_output()); + + return { x, y }; } -QPoint spline_widget::point_to_pixel(const QPointF& point) +QPointF spline_widget::point_to_pixel(const QPointF& point) { - QPointF pt(point_to_pixel_(point)); - - return QPoint(iround(pt.x()), iround(pt.y())); + return { + pixel_bounds.x() + point.x() * c.x(), + pixel_bounds.y() + pixel_bounds.height() - point.y() * c.y() + }; } void spline_widget::resizeEvent(QResizeEvent *) { - update_range(); + reload_spline(); } -bool spline_widget::is_on_pt(const QPoint& pos, int* pt) +bool spline_widget::is_on_pt(const QPointF& pos, int* pt) { - if (!_config) + if (!config) { if (pt) *pt = -1; return false; } - const points_t points = _config->get_points(); + const points_t& points = config->get_points(); for (int i = 0; i < points.size(); i++) { @@ -650,3 +675,17 @@ bool spline_widget::is_on_pt(const QPoint& pos, int* pt) *pt = -1; return false; } + +void spline_widget::changeEvent(QEvent* e) +{ + switch (e->type()) + { + case QEvent::EnabledChange: + background_img = {}; spline_img = {}; + update(); + break; + default: + break; + } +} +} // ns spline_detail diff --git a/spline/spline-widget.hpp b/spline/spline-widget.hpp index 062db493..610baf43 100644 --- a/spline/spline-widget.hpp +++ b/spline/spline-widget.hpp @@ -1,4 +1,4 @@ -/* Copyright (c) 2012-2016 Stanislaw Halik <sthalik@misaki.pl> +/* Copyright (c) 2012-2019 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 @@ -11,23 +11,23 @@ #include "spline.hpp" #include "api/plugin-api.hpp" +#include "compat/qt-dpi.hpp" #include "options/options.hpp" -using namespace options; #include "export.hpp" +#include <cmath> + #include <QWidget> -#include <QRect> -#include <QPoint> -#include <QPointF> -#include <QToolTip> -#include <QShowEvent> -#include <QFocusEvent> #include <QMetaObject> #include <QDebug> -class OTR_SPLINE_EXPORT spline_widget final : public QWidget +namespace spline_detail { + +using namespace options; + +class OTR_SPLINE_EXPORT spline_widget final : public QWidget, public screen_dpi_mixin<spline_widget> { Q_OBJECT Q_PROPERTY(QColor colorBezier READ colorBezier WRITE setColorBezier) @@ -35,28 +35,28 @@ class OTR_SPLINE_EXPORT spline_widget final : public QWidget Q_PROPERTY(int x_step READ x_step WRITE set_x_step) Q_PROPERTY(int y_step READ y_step WRITE set_y_step) - using points_t = spline::points_t; public: - spline_widget(QWidget *parent = 0); - ~spline_widget(); + explicit spline_widget(QWidget *parent = nullptr); + ~spline_widget() override; - spline* config(); - void setConfig(spline* spl); + void set_config(base_spline* spl); QColor colorBezier() const; - void setColorBezier(QColor color); + void setColorBezier(QColor const& color); void force_redraw(); void set_preview_only(bool val); bool is_preview_only() const; - double x_step() { return _x_step; } - double y_step() { return _y_step; } - void set_x_step(double val) { _x_step = val; } - void set_y_step(double val) { _y_step = val; } + double x_step() const { return x_step_; } + double y_step() const { return y_step_; } + void set_x_step(double val) { x_step_ = std::fmax(1., val); } + void set_y_step(double val) { y_step_ = std::fmax(1., val); } void set_snap(double x, double y) { snap_x = x; snap_y = y; } void get_snap(double& x, double& y) const { x = snap_x; y = snap_y; } + + QSize minimumSizeHint() const override; public slots: void reload_spline(); protected slots: @@ -65,42 +65,52 @@ protected slots: void mouseMoveEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; private: - int get_closeness_limit(); + double min_pt_distance() const; void show_tooltip(const QPoint& pos, const QPointF& value = QPointF(0, 0)); - bool is_in_bounds(const QPoint& pos) const; + bool is_in_bounds(const QPointF& pos) const; void drawBackground(); void drawFunction(); void drawPoint(QPainter& painter, const QPointF& pt, const QColor& colBG, const QColor& border = QColor(50, 100, 120, 200)); - void drawLine(QPainter& painter, const QPoint& start, const QPoint& end, const QPen& pen); - bool point_within_pixel(const QPointF& pt, const QPoint& pixel); + void drawLine(QPainter& painter, const QPointF& start, const QPointF& end, const QPen& pen); + bool point_within_pixel(const QPointF& pt, const QPointF& pixel); void focusOutEvent(QFocusEvent*e) override; void resizeEvent(QResizeEvent *) override; - bool is_on_pt(const QPoint& pos, int* pt = nullptr); + bool is_on_pt(const QPointF& pos, int* pt = nullptr); void update_range(); - QPointF pixel_coord_to_point(const QPoint& point); + void changeEvent(QEvent* e) override; + + QPointF pixel_to_point(const QPointF& point); + QPointF point_to_pixel(const QPointF& point); - QPointF point_to_pixel_(const QPointF& point); - QPoint point_to_pixel(const QPointF& point); + static double snap(double x, double snap_value); QPointF c; - spline* _config; + base_spline* config = nullptr; - QPixmap _background; - QPixmap _function; + QPixmap background_img; + QPixmap spline_img; QColor spline_color; + QColor widget_bg_color = palette().window().color(); // bounds of the rectangle user can interact with QRect pixel_bounds; QMetaObject::Connection connection; - double snap_x, snap_y; - double _x_step, _y_step; - int moving_control_point_idx; - bool _draw_function, _preview_only; + double snap_x = 0, snap_y = 0; + double x_step_ = 10, y_step_ = 10; + int moving_control_point_idx = -1; + bool draw_function = true, preview_only = false; + + // point's circle radius on the widget + static constexpr int point_size_in_pixels_ = 5; - static constexpr int point_size = 4; + const double point_size_in_pixels = point_size_in_pixels_ * screen_dpi(); }; + +} // ns spline_detail + +using spline_widget = spline_detail::spline_widget; 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 diff --git a/spline/spline.hpp b/spline/spline.hpp index 067967b5..780442b9 100644 --- a/spline/spline.hpp +++ b/spline/spline.hpp @@ -1,4 +1,4 @@ -/* Copyright (c) 2012-2016, Stanislaw Halik <sthalik@misaki.pl> +/* Copyright (c) 2012-2019, 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, @@ -8,13 +8,12 @@ #pragma once -#include "compat/qcopyable-mutex.hpp" #include "options/options.hpp" -#include "compat/util.hpp" -using namespace options; - +#include "axis-opts.hpp" #include "export.hpp" +#include "compat/mutex.hpp" +#include <cstddef> #include <vector> #include <limits> #include <memory> @@ -26,83 +25,153 @@ using namespace options; namespace spline_detail { -class OTR_SPLINE_EXPORT settings final : public QObject +using points_t = QList<QPointF>; +using namespace options; + +class OTR_SPLINE_EXPORT base_settings : public QObject { Q_OBJECT + +signals: + void recomputed() const; +}; + +class OTR_SPLINE_EXPORT settings final : public base_settings +{ public: bundle b; - value<QList<QPointF>> points; - settings(bundle b); + value<QList<QPointF>> points { b, "points", {} }; + axis_opts opts; + settings(bundle const& b, const QString& axis_name, Axis idx); ~settings() override; -signals: - void recomputed() const; }; -} +struct OTR_SPLINE_EXPORT base_spline_ +{ + base_spline_() = default; + virtual ~base_spline_(); + + virtual double get_value(double x) const = 0; + virtual double get_value_no_save(double x) const = 0; + + [[nodiscard]] virtual bool get_last_value(QPointF& point) = 0; + virtual void set_tracking_active(bool value) const = 0; -class OTR_SPLINE_EXPORT spline final + virtual double max_input() const = 0; + virtual double max_output() const = 0; + + virtual const points_t& get_points() const = 0; + virtual int get_point_count() const = 0; + + base_spline_(const base_spline_&) = default; + base_spline_& operator=(const base_spline_&) = default; +}; + +struct OTR_SPLINE_EXPORT spline_settings_mixin { - double precision(const QList<QPointF>& points) const; - void update_interp_data(); - float get_value_internal(int x); - void add_lone_point(); - float get_value_no_save_internal(double x); - static bool sort_fn(const QPointF& one, const QPointF& two); + virtual std::shared_ptr<base_settings> get_settings() = 0; + virtual std::shared_ptr<const base_settings> get_settings() const = 0; - static QPointF ensure_in_bounds(const QList<QPointF>& points, double max_x, int i); - static int element_count(const QList<QPointF>& points, double max_x); + spline_settings_mixin(const spline_settings_mixin&) = default; + spline_settings_mixin& operator=(const spline_settings_mixin&) = default; - mem<spline_detail::settings> s; - QMetaObject::Connection connection; + spline_settings_mixin() = default; + virtual ~spline_settings_mixin(); +}; - std::vector<float> data; - using interp_data_t = decltype(data); +struct OTR_SPLINE_EXPORT spline_modify_mixin +{ + virtual void add_point(QPointF pt) = 0; + virtual void add_point(double x, double y) = 0; + virtual void move_point(int idx, QPointF pt) = 0; + virtual void remove_point(int i) = 0; + virtual void clear() = 0; + + spline_modify_mixin(const spline_modify_mixin&) = default; + spline_modify_mixin& operator=(const spline_modify_mixin&) = default; + + spline_modify_mixin() = default; + virtual ~spline_modify_mixin(); +}; + +struct OTR_SPLINE_EXPORT base_spline : base_spline_, spline_modify_mixin, spline_settings_mixin +{ + base_spline(const base_spline&) = default; + base_spline& operator=(const base_spline&) = default; + + base_spline() = default; + ~base_spline() override; +}; + +class OTR_SPLINE_EXPORT spline : public base_spline +{ + using f = double; - static constexpr int value_count = 4096; + double bucket_size_coefficient(const QList<QPointF>& points) const; + void update_interp_data() const; + double get_value_internal(int x) const; + static bool sort_fn(QPointF one, QPointF two); - MyMutex _mutex; - QPointF last_input_value; - qreal max_x, max_y; - volatile bool activep; - bool validp; + static void ensure_in_bounds(const QList<QPointF>& points, int i, f& x, f& y); + static int element_count(const QList<QPointF>& points, double max_input); + + void disconnect_signals(); + void invalidate_settings_(); + + mutex mtx { mutex::Recursive }; + std::shared_ptr<settings> s; + QMetaObject::Connection conn_points, conn_maxx, conn_maxy; + + std::shared_ptr<QObject> ctx { std::make_shared<QObject>() }; + + mutable QPointF last_input_value{-1, -1}; + mutable std::vector<f> data = std::vector<f>(value_count, magic_fill_value); + mutable points_t points; + mutable axis_opts::max_clamp clamp_x = axis_opts::x1000, clamp_y = axis_opts::x1000; + mutable bool activep = false; + + static constexpr unsigned value_count = 8192; + static constexpr f magic_fill_value = -(1 << 24) + 1; + static constexpr double c_interp = 5; public: - using settings = spline_detail::settings; + void invalidate_settings(); void reload(); - void save(QSettings& s); void save(); - void set_bundle(bundle b); + void set_bundle(bundle b, const QString& axis_name, Axis axis); + + double max_input() const override; + double max_output() const override; - qreal max_input() const; - qreal max_output() const; spline(); - spline(qreal maxx, qreal maxy, const QString& name); - ~spline(); + spline(const QString& name, const QString& axis_name, Axis axis); + ~spline() override; - spline& operator=(const spline&) = default; spline(const spline&) = default; - float get_value(double x); - float get_value_no_save(double x) const; - DEFUN_WARN_UNUSED bool get_last_value(QPointF& point); - void remove_point(int i); - void clear(); + double get_value(double x) const override; + double get_value_no_save(double x) const override; + [[nodiscard]] bool get_last_value(QPointF& point) override; + + void add_point(QPointF pt) override; + void add_point(double x, double y) override; + void move_point(int idx, QPointF pt) override; + void remove_point(int i) override; + void clear() override; - void add_point(QPointF pt); - void add_point(double x, double y); - void move_point(int idx, QPointF pt); - QList<QPointF> get_points() const; - void set_max_input(qreal MaxInput); - void set_max_output(qreal MaxOutput); + const points_t& get_points() const override; - void set_tracking_active(bool value); + void set_tracking_active(bool value) const override; bundle get_bundle(); - void recompute(); + void ensure_valid(points_t& in_out) const; - mem<settings> get_settings(); - mem<const settings> get_settings() const; + std::shared_ptr<base_settings> get_settings() override; + std::shared_ptr<const base_settings> get_settings() const override; - using points_t = decltype(s->points()); - int get_point_count() const; + int get_point_count() const override; }; + +} // ns spline_detail + +using spline = spline_detail::spline; |
