LCOV - code coverage report
Current view: top level - muse/framework/uicomponents/qml/Muse/UiComponents - polylineplot.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 770 0
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2026 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : 
      23              : #include "polylineplot.h"
      24              : 
      25              : #include <QCursor>
      26              : #include <QPainter>
      27              : #include <QPainterPath>
      28              : #include <QPen>
      29              : #include <QBrush>
      30              : #include <QHoverEvent>
      31              : #include <QMouseEvent>
      32              : 
      33              : #include <algorithm>
      34              : #include <cmath>
      35              : 
      36              : #include "realfn.h"
      37              : 
      38              : using namespace muse::uicomponents;
      39              : 
      40              : namespace {
      41              : constexpr double MOVE_THRESHOLD = 3.0;
      42              : constexpr double EPSILON = 1e-12;
      43              : constexpr double BOUNDARY_MARGIN = 0.1;
      44              : 
      45              : static constexpr int INVALID_POINT_IDX = -1;
      46              : static constexpr int PENDING_POINT_IDX = -2;
      47              : 
      48            0 : static inline qreal toPxX(const QQuickItem* item, qreal xN)
      49              : {
      50            0 :     return xN * item->width();
      51              : }
      52              : 
      53            0 : static inline qreal toPxY(const QQuickItem* item, qreal yN)
      54              : {
      55            0 :     const qreal h = item->height();
      56            0 :     if (h <= 0.0) {
      57            0 :         return 0.0;
      58              :     }
      59              : 
      60              :     // NOTE: preserve full height of the line at top/bottom edges
      61            0 :     const auto* polyline = static_cast<const PolylinePlot*>(item);
      62            0 :     const qreal inset = std::clamp(polyline->lineWidth() * 0.5, 0.0, h * 0.5);
      63            0 :     const qreal drawableHeight = h - (2.0 * inset);
      64              : 
      65            0 :     return inset + (1.0 - yN) * drawableHeight;
      66              : }
      67              : 
      68            0 : static qreal pointToSegmentDistance(const QPointF& point, const QPointF& segmentStart, const QPointF& segmentEnd)
      69              : {
      70            0 :     const QPointF segment = segmentEnd - segmentStart;
      71            0 :     const qreal segmentLengthSquared = QPointF::dotProduct(segment, segment);
      72            0 :     if (segmentLengthSquared <= EPSILON) {
      73            0 :         return std::hypot(point.x() - segmentStart.x(), point.y() - segmentStart.y());
      74              :     }
      75              : 
      76            0 :     const QPointF segmentStartToPoint = point - segmentStart;
      77            0 :     qreal t = QPointF::dotProduct(segmentStartToPoint, segment) / segmentLengthSquared;
      78            0 :     t = std::max<qreal>(0.0, std::min<qreal>(1.0, t));
      79              : 
      80            0 :     const QPointF closestPoint = segmentStart + segment * t;
      81            0 :     return std::hypot(point.x() - closestPoint.x(), point.y() - closestPoint.y());
      82              : }
      83              : 
      84            0 : static GhostPoint ghostPointToSegmentDistance(const QPointF& point, const QPointF& segmentStart, const QPointF& segmentEnd)
      85              : {
      86            0 :     const QPointF segment = segmentEnd - segmentStart;
      87            0 :     const qreal segmentLengthSquared = QPointF::dotProduct(segment, segment);
      88            0 :     if (segmentLengthSquared <= EPSILON) {
      89            0 :         const qreal distance = std::hypot(point.x() - segmentStart.x(), point.y() - segmentStart.y());
      90            0 :         return { segmentStart, distance };
      91              :     }
      92              : 
      93            0 :     const QPointF segmentStartToPoint = point - segmentStart;
      94            0 :     qreal t = QPointF::dotProduct(segmentStartToPoint, segment) / segmentLengthSquared;
      95            0 :     t = std::max<qreal>(0.0, std::min<qreal>(1.0, t));
      96              : 
      97            0 :     const QPointF closestPoint = segmentStart + segment * t;
      98            0 :     const qreal distance = std::hypot(point.x() - closestPoint.x(), point.y() - closestPoint.y());
      99            0 :     return { closestPoint, distance };
     100              : }
     101              : 
     102              : // NOTE: can be replaced with std::lerp in C++20
     103            0 : static double lerp(double a, double b, double t)
     104              : {
     105            0 :     return a + (b - a) * t;
     106              : }
     107              : 
     108              : // compute Y at X using linear interpolation
     109            0 : static double valueAtX(const QVector<QPointF>& sortedPoints, double x)
     110              : {
     111            0 :     if (sortedPoints.isEmpty()) {
     112            0 :         return 0.0;
     113              :     }
     114              : 
     115            0 :     if (x <= sortedPoints.front().x()) {
     116            0 :         return sortedPoints.front().y();
     117              :     }
     118              : 
     119            0 :     if (x >= sortedPoints.back().x()) {
     120            0 :         return sortedPoints.back().y();
     121              :     }
     122              : 
     123            0 :     for (int i = 0; i < sortedPoints.size() - 1; ++i) {
     124            0 :         const auto& a = sortedPoints[i];
     125            0 :         const auto& b = sortedPoints[i + 1];
     126            0 :         if (x >= a.x() && x <= b.x()) {
     127            0 :             const double dx = b.x() - a.x();
     128            0 :             if (std::abs(dx) <= EPSILON) {
     129            0 :                 return a.y();
     130              :             }
     131            0 :             const double t = (x - a.x()) / dx;
     132            0 :             return lerp(a.y(), b.y(), t);
     133              :         }
     134              :     }
     135              : 
     136            0 :     return sortedPoints.back().y();
     137              : }
     138              : }
     139              : 
     140            0 : PolylinePlot::PolylinePlot(QQuickItem* parent)
     141            0 :     : QQuickPaintedItem(parent)
     142              : {
     143            0 :     setAcceptHoverEvents(true);
     144            0 :     setAcceptedMouseButtons(Qt::LeftButton);
     145              : 
     146            0 :     setAntialiasing(true);
     147            0 :     setOpaquePainting(false);
     148              : 
     149            0 :     m_standardPointStyle = new PolylinePointStyle(this);
     150            0 :     QObject::connect(m_standardPointStyle, &PolylinePointStyle::styleChanged, this, [this]() {
     151            0 :         update();
     152            0 :     });
     153              : 
     154            0 :     m_ghostPointStyle = new PolylinePointStyle(this);
     155            0 :     QObject::connect(m_ghostPointStyle, &PolylinePointStyle::styleChanged, this, [this]() {
     156            0 :         update();
     157            0 :     });
     158              : 
     159            0 :     m_selectedPointStyle = new PolylinePointStyle(this);
     160            0 :     QObject::connect(m_selectedPointStyle, &PolylinePointStyle::styleChanged, this, [this]() {
     161            0 :         update();
     162            0 :     });
     163            0 : }
     164              : 
     165            0 : void PolylinePlot::cancelEdit()
     166              : {
     167            0 :     m_hoveredOnLine = false;
     168            0 :     resetGestureState();
     169            0 : }
     170              : 
     171            0 : PolylinePointStyle* PolylinePlot::standardPointStyle()
     172              : {
     173            0 :     return m_standardPointStyle;
     174              : }
     175              : 
     176            0 : PolylinePointStyle* PolylinePlot::ghostPointStyle()
     177              : {
     178            0 :     return m_ghostPointStyle;
     179              : }
     180              : 
     181            0 : PolylinePointStyle* PolylinePlot::selectedPointStyle()
     182              : {
     183            0 :     return m_selectedPointStyle;
     184              : }
     185              : 
     186            0 : bool PolylinePlot::ghostPointsEnabled() const
     187              : {
     188            0 :     return m_ghostPointsEnabled;
     189              : }
     190              : 
     191            0 : void PolylinePlot::setGhostPointsEnabled(bool v)
     192              : {
     193            0 :     if (m_ghostPointsEnabled == v) {
     194            0 :         return;
     195              :     }
     196              : 
     197            0 :     m_ghostPointsEnabled = v;
     198            0 :     emit ghostPointsEnabledChanged();
     199              : 
     200            0 :     update();
     201              : }
     202              : 
     203            0 : bool PolylinePlot::selectedPointsEnabled() const
     204              : {
     205            0 :     return m_selectedPointsEnabled;
     206              : }
     207              : 
     208            0 : void PolylinePlot::setSelectedPointsEnabled(bool v)
     209              : {
     210            0 :     if (m_selectedPointsEnabled == v) {
     211            0 :         return;
     212              :     }
     213              : 
     214            0 :     m_selectedPointsEnabled = v;
     215            0 :     emit selectedPointsEnabledChanged();
     216              : 
     217            0 :     update();
     218              : }
     219              : 
     220            0 : QColor PolylinePlot::lineColor() const
     221              : {
     222            0 :     return m_lineColor;
     223              : }
     224              : 
     225            0 : void PolylinePlot::setLineColor(const QColor& c)
     226              : {
     227            0 :     if (m_lineColor == c) {
     228            0 :         return;
     229              :     }
     230              : 
     231            0 :     m_lineColor = c;
     232            0 :     emit lineColorChanged();
     233            0 :     update();
     234              : }
     235              : 
     236            0 : qreal PolylinePlot::lineWidth() const
     237              : {
     238            0 :     return m_lineWidth;
     239              : }
     240              : 
     241            0 : void PolylinePlot::setLineWidth(qreal w)
     242              : {
     243            0 :     if (m_lineWidth == w) {
     244            0 :         return;
     245              :     }
     246              : 
     247            0 :     m_lineWidth = w;
     248            0 :     emit lineWidthChanged();
     249              : 
     250            0 :     update();
     251              : }
     252              : 
     253            0 : bool PolylinePlot::drawBackground() const
     254              : {
     255            0 :     return m_drawBackground;
     256              : }
     257              : 
     258            0 : void PolylinePlot::setDrawBackground(bool v)
     259              : {
     260            0 :     if (m_drawBackground == v) {
     261            0 :         return;
     262              :     }
     263              : 
     264            0 :     m_drawBackground = v;
     265            0 :     emit drawBackgroundChanged();
     266              : 
     267            0 :     update();
     268              : }
     269              : 
     270            0 : qreal PolylinePlot::baselineN() const
     271              : {
     272            0 :     return m_baselineN;
     273              : }
     274              : 
     275            0 : void PolylinePlot::setBaselineN(qreal v)
     276              : {
     277            0 :     v = clamp01(v);
     278              : 
     279            0 :     if (m_baselineN == v) {
     280            0 :         return;
     281              :     }
     282              : 
     283            0 :     m_baselineN = v;
     284            0 :     emit baselineNChanged();
     285              : 
     286            0 :     update();
     287              : }
     288              : 
     289            0 : qreal PolylinePlot::hitRadius() const
     290              : {
     291            0 :     return m_hitRadius;
     292              : }
     293              : 
     294            0 : void PolylinePlot::setHitRadius(qreal r)
     295              : {
     296            0 :     if (m_hitRadius == r) {
     297            0 :         return;
     298              :     }
     299              : 
     300            0 :     m_hitRadius = r;
     301            0 :     emit hitRadiusChanged();
     302              : }
     303              : 
     304            0 : bool PolylinePlot::isSnapEnabled() const
     305              : {
     306            0 :     return m_isSnapEnabled;
     307              : }
     308              : 
     309            0 : void PolylinePlot::setIsSnapEnabled(bool v)
     310              : {
     311            0 :     if (m_isSnapEnabled == v) {
     312            0 :         return;
     313              :     }
     314              : 
     315            0 :     m_isSnapEnabled = v;
     316            0 :     emit isSnapEnabledChanged();
     317              : }
     318              : 
     319            0 : qreal PolylinePlot::snapThresholdPx() const
     320              : {
     321            0 :     return m_snapThresholdPx;
     322              : }
     323              : 
     324            0 : void PolylinePlot::setSnapThresholdPx(qreal v)
     325              : {
     326            0 :     if (m_snapThresholdPx == v) {
     327            0 :         return;
     328              :     }
     329              : 
     330            0 :     m_snapThresholdPx = v;
     331            0 :     emit snapThresholdPxChanged();
     332              : }
     333              : 
     334            0 : QVector<QPointF> PolylinePlot::points() const
     335              : {
     336            0 :     return m_points;
     337              : }
     338              : 
     339            0 : void PolylinePlot::setPoints(const QVector<QPointF>& pts)
     340              : {
     341            0 :     if (m_points == pts) {
     342            0 :         if (m_points.isEmpty()) {
     343            0 :             updateBaselineFromDefaultValue();
     344              : 
     345              :             // invalidate colors under line
     346            0 :             if (!m_colorsUnderLine.empty()) {
     347            0 :                 m_colorsUnderLine.clear();
     348            0 :                 emit colorsUnderLineChanged();
     349              :             }
     350              : 
     351            0 :             update();
     352              :         }
     353            0 :         return;
     354              :     }
     355            0 :     m_points = pts;
     356            0 :     emit pointsChanged();
     357              : 
     358              :     // invalidate colors under line
     359            0 :     if (!m_colorsUnderLine.empty()) {
     360            0 :         m_colorsUnderLine.clear();
     361            0 :         emit colorsUnderLineChanged();
     362              :     }
     363              : 
     364            0 :     if (m_points.isEmpty()) {
     365            0 :         updateBaselineFromDefaultValue();
     366              :     }
     367              : 
     368              :     // if there's pending point, find its index and mark as ready for drag
     369            0 :     if (m_pressed && m_pressedPointIndex == PENDING_POINT_IDX && m_pressedOnLine) {
     370            0 :         if (width() > 0 && height() > 0 && !m_points.isEmpty()) {
     371            0 :             const auto ghostPoint = ghostPointToPolylinePx(m_pressPx);
     372              : 
     373            0 :             QPointF pN(clamp01(ghostPoint.point.x() / width()),
     374            0 :                        1.0 - clamp01(ghostPoint.point.y() / height()));
     375              : 
     376            0 :             const QPointF targetDomain = domainFromNormalized(pN);
     377              : 
     378            0 :             int bestIdx = INVALID_POINT_IDX;
     379            0 :             double bestScore = std::numeric_limits<double>::max();
     380              : 
     381            0 :             for (int i = 0; i < m_points.size(); ++i) {
     382            0 :                 const double dx = std::abs(m_points[i].x() - targetDomain.x());
     383            0 :                 const double dy = std::abs(m_points[i].y() - targetDomain.y());
     384            0 :                 const double score = dx * 1000.0 + dy;
     385            0 :                 if (score < bestScore) {
     386            0 :                     bestScore = score;
     387            0 :                     bestIdx = i;
     388              :                 }
     389              :             }
     390              : 
     391            0 :             if (bestIdx >= 0) {
     392            0 :                 m_pressedOnPoint = true;
     393            0 :                 m_pressedPointIndex = bestIdx;
     394            0 :                 m_draggedPointDomain = m_points[bestIdx];
     395            0 :                 m_hasDraggedPointDomain = true;
     396              :             }
     397              :         }
     398              :     }
     399              : 
     400            0 :     rebuildVisiblePoints();
     401              : }
     402              : 
     403            0 : QVector<QColor> PolylinePlot::colorsUnderLine() const
     404              : {
     405            0 :     return m_colorsUnderLine;
     406              : }
     407              : 
     408            0 : void PolylinePlot::setColorsUnderLine(const QVector<QColor>& c)
     409              : {
     410            0 :     if (m_colorsUnderLine == c) {
     411            0 :         return;
     412              :     }
     413              : 
     414            0 :     m_colorsUnderLine = c;
     415            0 :     emit colorsUnderLineChanged();
     416              : 
     417            0 :     update();
     418              : }
     419              : 
     420            0 : qreal PolylinePlot::defaultValue() const
     421              : {
     422            0 :     return m_defaultValue;
     423              : }
     424              : 
     425            0 : void PolylinePlot::setXRangeFrom(qreal v)
     426              : {
     427            0 :     if (m_xFrom == v) {
     428            0 :         return;
     429              :     }
     430              : 
     431            0 :     m_xFrom = v;
     432            0 :     emit xRangeFromChanged();
     433              : 
     434            0 :     rebuildVisiblePoints();
     435              : }
     436              : 
     437            0 : qreal PolylinePlot::xRangeTo() const
     438              : {
     439            0 :     return m_xTo;
     440              : }
     441              : 
     442            0 : void PolylinePlot::setXRangeTo(qreal v)
     443              : {
     444            0 :     if (m_xTo == v) {
     445            0 :         return;
     446              :     }
     447              : 
     448            0 :     m_xTo = v;
     449            0 :     emit xRangeToChanged();
     450              : 
     451            0 :     rebuildVisiblePoints();
     452              : }
     453              : 
     454            0 : qreal PolylinePlot::yRangeFrom() const
     455              : {
     456            0 :     return m_yFrom;
     457              : }
     458              : 
     459            0 : void PolylinePlot::setYRangeFrom(qreal v)
     460              : {
     461            0 :     if (m_yFrom == v) {
     462            0 :         return;
     463              :     }
     464              : 
     465            0 :     m_yFrom = v;
     466            0 :     emit yRangeFromChanged();
     467              : 
     468            0 :     rebuildVisiblePoints();
     469              : }
     470              : 
     471            0 : qreal PolylinePlot::yRangeTo() const
     472              : {
     473            0 :     return m_yTo;
     474              : }
     475              : 
     476            0 : void PolylinePlot::setYRangeTo(qreal v)
     477              : {
     478            0 :     if (m_yTo == v) {
     479            0 :         return;
     480              :     }
     481              : 
     482            0 :     m_yTo = v;
     483            0 :     emit yRangeToChanged();
     484              : 
     485            0 :     rebuildVisiblePoints();
     486              : }
     487              : 
     488            0 : qreal PolylinePlot::ySplitNormalized() const
     489              : {
     490            0 :     return m_ySplitNormalized;
     491              : }
     492              : 
     493            0 : void PolylinePlot::setYSplitNormalized(qreal v)
     494              : {
     495            0 :     v = clamp01(v);
     496            0 :     if (m_ySplitNormalized == v) {
     497            0 :         return;
     498              :     }
     499              : 
     500            0 :     m_ySplitNormalized = v;
     501            0 :     emit ySplitNormalizedChanged();
     502              : 
     503            0 :     rebuildVisiblePoints();
     504              : }
     505              : 
     506            0 : qreal PolylinePlot::ySplitValue() const
     507              : {
     508            0 :     return m_ySplitValue;
     509              : }
     510              : 
     511            0 : void PolylinePlot::setYSplitValue(qreal v)
     512              : {
     513            0 :     const qreal low = std::min(m_yFrom, m_yTo);
     514            0 :     const qreal high = std::max(m_yFrom, m_yTo);
     515            0 :     qreal clampedValue = std::clamp(v, low, high);
     516              : 
     517            0 :     if (m_ySplitValue == clampedValue) {
     518            0 :         return;
     519              :     }
     520              : 
     521            0 :     m_ySplitValue = clampedValue;
     522            0 :     emit ySplitValueChanged();
     523              : 
     524            0 :     rebuildVisiblePoints();
     525              : }
     526              : 
     527            0 : bool PolylinePlot::yAxisInverse() const
     528              : {
     529            0 :     return m_yAxisInverse;
     530              : }
     531              : 
     532            0 : void PolylinePlot::setYAxisInverse(bool v)
     533              : {
     534            0 :     if (m_yAxisInverse == v) {
     535            0 :         return;
     536              :     }
     537              : 
     538            0 :     m_yAxisInverse = v;
     539            0 :     emit yAxisInverseChanged();
     540            0 :     rebuildVisiblePoints();
     541              : }
     542              : 
     543            0 : bool PolylinePlot::hasActivePoint() const
     544              : {
     545            0 :     return m_hasActivePoint;
     546              : }
     547              : 
     548            0 : qreal PolylinePlot::activePointX() const
     549              : {
     550            0 :     return m_activePointPx.x();
     551              : }
     552              : 
     553            0 : qreal PolylinePlot::activePointY() const
     554              : {
     555            0 :     return m_activePointPx.y();
     556              : }
     557              : 
     558            0 : qreal PolylinePlot::activePointValue() const
     559              : {
     560            0 :     return m_activePointValue;
     561              : }
     562              : 
     563            0 : void PolylinePlot::setDefaultValue(qreal v)
     564              : {
     565            0 :     if (m_defaultValue == v) {
     566            0 :         return;
     567              :     }
     568              : 
     569            0 :     m_defaultValue = v;
     570            0 :     emit defaultValueChanged();
     571              : 
     572              :     // if there are no points, baseline should reflect defaultY immediately
     573            0 :     if (m_points.isEmpty()) {
     574            0 :         updateBaselineFromDefaultValue();
     575            0 :         rebuildVisiblePoints();
     576              :     }
     577              : }
     578              : 
     579            0 : qreal PolylinePlot::xRangeFrom() const
     580              : {
     581            0 :     return m_xFrom;
     582              : }
     583              : 
     584            0 : qreal PolylinePlot::clamp01(qreal v) const
     585              : {
     586            0 :     return std::max<qreal>(0.0, std::min<qreal>(1.0, v));
     587              : }
     588              : 
     589            0 : QPointF PolylinePlot::clamp01(const QPointF& p) const
     590              : {
     591            0 :     return QPointF(clamp01(p.x()), clamp01(p.y()));
     592              : }
     593              : 
     594            0 : bool PolylinePlot::hasValidXRange() const
     595              : {
     596            0 :     return std::isfinite(m_xFrom) && std::isfinite(m_xTo) && std::abs(m_xTo - m_xFrom) > EPSILON;
     597              : }
     598              : 
     599            0 : bool PolylinePlot::hasValidYRange() const
     600              : {
     601            0 :     return std::isfinite(m_yFrom) && std::isfinite(m_yTo) && std::abs(m_yTo - m_yFrom) > EPSILON;
     602              : }
     603              : 
     604            0 : bool PolylinePlot::hasValidYSplit() const
     605              : {
     606            0 :     return hasValidYRange() && !muse::RealIsEqualOrLess(m_ySplitNormalized, 0.0) && !muse::RealIsEqualOrMore(m_ySplitNormalized, 1.0);
     607              : }
     608              : 
     609            0 : qreal PolylinePlot::yDomainFromNormalized(qreal yNormalized) const
     610              : {
     611            0 :     const qreal n = clamp01(yNormalized);
     612              : 
     613              :     // simple linear mapping (no split)
     614            0 :     if (!hasValidYSplit()) {
     615            0 :         return m_yFrom + n * (m_yTo - m_yFrom);
     616              :     }
     617              : 
     618            0 :     const qreal splitNormalized = m_ySplitNormalized;
     619            0 :     const qreal splitValue      = m_ySplitValue;
     620              : 
     621              :     // first segment (bottom → split)
     622            0 :     if (n <= splitNormalized) {
     623            0 :         if (splitNormalized <= EPSILON) {
     624            0 :             return splitValue;
     625              :         }
     626              : 
     627            0 :         const qreal segmentN = n / splitNormalized;
     628            0 :         return m_yFrom + segmentN * (splitValue - m_yFrom);
     629              :     }
     630              : 
     631              :     // second segment (split → top)
     632            0 :     const qreal secondSegmentSize = 1.0 - splitNormalized;
     633            0 :     if (secondSegmentSize <= EPSILON) {
     634            0 :         return splitValue;
     635              :     }
     636              : 
     637            0 :     const qreal segmentN = (n - splitNormalized) / secondSegmentSize;
     638            0 :     return splitValue + segmentN * (m_yTo - splitValue);
     639              : }
     640              : 
     641            0 : qreal PolylinePlot::yNormalizedFromDomain(qreal yDomain) const
     642              : {
     643              :     // simple linear mapping (no split)
     644            0 :     if (!hasValidYSplit()) {
     645            0 :         return (yDomain - m_yFrom) / (m_yTo - m_yFrom);
     646              :     }
     647              : 
     648            0 :     const qreal splitNormalized = m_ySplitNormalized;
     649            0 :     const qreal splitValue      = m_ySplitValue;
     650              : 
     651            0 :     const bool increasing = (m_yTo >= m_yFrom);
     652              : 
     653            0 :     const bool isInFirstSegment
     654            0 :         =increasing ? (yDomain <= splitValue)
     655              :           : (yDomain >= splitValue);
     656              : 
     657              :     // ---- first segment ----
     658            0 :     if (isInFirstSegment) {
     659            0 :         const qreal segmentSize = splitValue - m_yFrom;
     660            0 :         if (std::abs(segmentSize) <= EPSILON) {
     661            0 :             return splitNormalized;
     662              :         }
     663              : 
     664            0 :         const qreal segmentN = (yDomain - m_yFrom) / segmentSize;
     665            0 :         return segmentN * splitNormalized;
     666              :     }
     667              : 
     668              :     // ---- second segment ----
     669            0 :     const qreal segmentSize = m_yTo - splitValue;
     670            0 :     if (std::abs(segmentSize) <= EPSILON) {
     671            0 :         return splitNormalized;
     672              :     }
     673              : 
     674            0 :     const qreal segmentN = (yDomain - splitValue) / segmentSize;
     675            0 :     return splitNormalized + segmentN * (1.0 - splitNormalized);
     676              : }
     677              : 
     678            0 : void PolylinePlot::updateBaselineFromDefaultValue()
     679              : {
     680            0 :     const qreal baseline = normalizedFromDomain(QPointF(m_xFrom, m_defaultValue)).y();
     681            0 :     if (m_baselineN == baseline) {
     682            0 :         return;
     683              :     }
     684              : 
     685            0 :     m_baselineN = baseline;
     686            0 :     emit baselineNChanged();
     687              : }
     688              : 
     689            0 : void PolylinePlot::updateActivePoint()
     690              : {
     691            0 :     const int hoveredDomainIdx = pointIndexAtPx(m_hoverPx);
     692            0 :     int draggedDisplayDomainIdx = INVALID_POINT_IDX;
     693            0 :     if (m_pressed && m_hasDraggedPointDomain && !m_points.isEmpty()) {
     694            0 :         double bestScore = std::numeric_limits<double>::max();
     695            0 :         for (int i = 0; i < m_points.size(); ++i) {
     696            0 :             const double dx = std::abs(m_points[i].x() - m_draggedPointDomain.x());
     697            0 :             const double dy = std::abs(m_points[i].y() - m_draggedPointDomain.y());
     698            0 :             const double score = dx * 1000.0 + dy;
     699            0 :             if (score < bestScore) {
     700            0 :                 bestScore = score;
     701            0 :                 draggedDisplayDomainIdx = i;
     702              :             }
     703              :         }
     704              :     }
     705              : 
     706              :     // m_pressedPointIndex must stay at the original model index (so consumed neighbors can be restored), but selection indices
     707              :     // need to shift when other indices are deleted...
     708            0 :     if (m_selectedPointsEnabled && draggedDisplayDomainIdx >= 0) {
     709            0 :         if (m_selectedPointsIndices.size() != 1 || !muse::contains(m_selectedPointsIndices, draggedDisplayDomainIdx)) {
     710            0 :             m_selectedPointsIndices.clear();
     711            0 :             m_selectedPointsIndices.insert(draggedDisplayDomainIdx);
     712              :         }
     713              :     }
     714              : 
     715            0 :     const int activeDomainIdx = (draggedDisplayDomainIdx >= 0)
     716            0 :                                 ? draggedDisplayDomainIdx
     717            0 :                                 : ((m_pressedPointIndex >= 0) ? m_pressedPointIndex : hoveredDomainIdx);
     718              : 
     719            0 :     if (activeDomainIdx < 0) {
     720            0 :         if (m_hasActivePoint) {
     721            0 :             m_hasActivePoint = false;
     722            0 :             m_activePointPx = {};
     723            0 :             m_activePointValue = 0.0;
     724            0 :             emit activePointChanged();
     725              :         }
     726            0 :         return;
     727              :     }
     728              : 
     729            0 :     QPointF activeN;
     730            0 :     bool found = false;
     731            0 :     for (int i = 0; i < m_pointsNVisible.size() && i < m_visibleToDomainIndex.size(); ++i) {
     732            0 :         if (m_visibleToDomainIndex[i] == activeDomainIdx) {
     733            0 :             activeN = m_pointsNVisible[i];
     734            0 :             found = true;
     735            0 :             break;
     736              :         }
     737              :     }
     738              : 
     739            0 :     if (!found) {
     740            0 :         if (m_hasActivePoint) {
     741            0 :             m_hasActivePoint = false;
     742            0 :             m_activePointPx = {};
     743            0 :             m_activePointValue = 0.0;
     744            0 :             emit activePointChanged();
     745              :         }
     746            0 :         return;
     747              :     }
     748              : 
     749            0 :     const QPointF newPx(toPxX(this, activeN.x()), toPxY(this, activeN.y()));
     750            0 :     const qreal newValue = (draggedDisplayDomainIdx >= 0 && m_hasDraggedPointDomain)
     751            0 :                            ? m_draggedPointDomain.y()
     752            0 :                            : m_points[activeDomainIdx].y();
     753            0 :     const bool changed = (!m_hasActivePoint
     754            0 :                           || newPx != m_activePointPx
     755            0 :                           || std::abs(newValue - m_activePointValue) > EPSILON);
     756              : 
     757            0 :     m_hasActivePoint = true;
     758            0 :     m_activePointPx = newPx;
     759            0 :     m_activePointValue = newValue;
     760              : 
     761            0 :     if (changed) {
     762            0 :         emit activePointChanged();
     763              :     }
     764              : }
     765              : 
     766            0 : QPointF PolylinePlot::normalizedFromDomain(const QPointF& p) const
     767              : {
     768            0 :     if (!hasValidXRange() || !hasValidYRange()) {
     769            0 :         return clamp01(QPointF(0.0, 0.0));
     770              :     }
     771              : 
     772            0 :     const qreal xN = (p.x() - m_xFrom) / (m_xTo - m_xFrom);
     773            0 :     qreal yN = clamp01(yNormalizedFromDomain(p.y()));
     774              : 
     775            0 :     if (m_yAxisInverse) {
     776            0 :         yN = 1.0 - yN;
     777              :     }
     778              : 
     779            0 :     return clamp01(QPointF(xN, yN));
     780              : }
     781              : 
     782            0 : QPointF PolylinePlot::domainFromNormalized(const QPointF& pN) const
     783              : {
     784            0 :     if (!hasValidXRange() || !hasValidYRange()) {
     785            0 :         return QPointF(m_xFrom, m_yFrom);
     786              :     }
     787              : 
     788            0 :     const qreal x = m_xFrom + clamp01(pN.x()) * (m_xTo - m_xFrom);
     789              : 
     790            0 :     qreal yT = clamp01(pN.y());
     791            0 :     if (m_yAxisInverse) {
     792            0 :         yT = 1.0 - yT;
     793              :     }
     794            0 :     const qreal y = yDomainFromNormalized(yT);
     795              : 
     796            0 :     return QPointF(x, y);
     797              : }
     798              : 
     799            0 : QVector<QPointF> PolylinePlot::normalizedFromDomain(const QVector<QPointF>& pts) const
     800              : {
     801            0 :     QVector<QPointF> out;
     802            0 :     out.reserve(pts.size());
     803            0 :     for (const auto& p : pts) {
     804            0 :         out.push_back(normalizedFromDomain(p));
     805              :     }
     806              : 
     807            0 :     return out;
     808            0 : }
     809              : 
     810            0 : QVector<QPointF> PolylinePlot::domainFromNormalized(const QVector<QPointF>& ptsN) const
     811              : {
     812            0 :     QVector<QPointF> out;
     813            0 :     out.reserve(ptsN.size());
     814            0 :     for (const auto& pN : ptsN) {
     815            0 :         out.push_back(domainFromNormalized(pN));
     816              :     }
     817              : 
     818            0 :     return out;
     819            0 : }
     820              : 
     821            0 : void PolylinePlot::rebuildVisiblePoints()
     822              : {
     823            0 :     m_pointsNVisible.clear();
     824            0 :     m_visibleToDomainIndex.clear();
     825              : 
     826            0 :     const double xRange = (m_xTo - m_xFrom);
     827            0 :     const double yRange = (m_yTo - m_yFrom);
     828              : 
     829            0 :     if (width() <= 0 || height() <= 0 || std::abs(xRange) <= 0 || std::abs(yRange) <= 0) {
     830            0 :         if (m_points.isEmpty()) {
     831            0 :             updateBaselineFromDefaultValue();
     832              :         }
     833            0 :         update();
     834            0 :         return;
     835              :     }
     836              : 
     837            0 :     if (m_points.isEmpty()) {
     838            0 :         updateBaselineFromDefaultValue();
     839            0 :         update();
     840            0 :         return;
     841              :     }
     842              : 
     843              :     struct P {
     844              :         QPointF p;
     845              :         int idx;
     846              :     };
     847              : 
     848            0 :     QVector<P> sortedPointsWithIndexes;
     849            0 :     sortedPointsWithIndexes.reserve(m_points.size());
     850            0 :     for (int i = 0; i < m_points.size(); ++i) {
     851            0 :         sortedPointsWithIndexes.push_back({ m_points[i], i });
     852              :     }
     853            0 :     std::sort(sortedPointsWithIndexes.begin(), sortedPointsWithIndexes.end(),
     854            0 :               [](const P& a, const P& b) { return a.p.x() < b.p.x(); });
     855              : 
     856            0 :     auto normY = [&](double yAbs) {
     857            0 :         double yn = yNormalizedFromDomain(yAbs);
     858            0 :         if (m_yAxisInverse) {
     859            0 :             yn = 1.0 - yn;
     860              :         }
     861            0 :         return yn;
     862            0 :     };
     863              : 
     864              :     // interpolate at window edges
     865            0 :     QVector<QPointF> sortedPointsN;
     866            0 :     sortedPointsN.reserve(sortedPointsWithIndexes.size());
     867            0 :     for (const auto& it : sortedPointsWithIndexes) {
     868            0 :         sortedPointsN.push_back(QPointF(it.p.x(), normY(it.p.y())));
     869              :     }
     870              : 
     871              :     // Synthetic boundary points must be interpolated in rendered Y-space.
     872              :     // Interpolating in domain Y breaks continuity for split mappings
     873              :     // (e.g. segments crossing 0 dB split value).
     874            0 :     const double yAt0N = valueAtX(sortedPointsN, m_xFrom);
     875            0 :     const double yAt1N = valueAtX(sortedPointsN, m_xTo);
     876              : 
     877              :     // NOTE: synthetic boundary points are added just outside the visible range
     878              :     // so the polyline draws correctly: they allow horizontal segments
     879              :     // before the first real point and after the last real point. Especially in
     880              :     // cases where polyline is currently visible only partially the screen.
     881              :     // These are rendering-only points (no corresponding domain index).
     882              : 
     883              :     // left boundary (synthetic)
     884            0 :     m_pointsNVisible.push_back(QPointF(-0.1, yAt0N));
     885            0 :     m_visibleToDomainIndex.push_back(INVALID_POINT_IDX);
     886              : 
     887              :     // interior real points
     888            0 :     for (const auto& it : sortedPointsWithIndexes) {
     889            0 :         const auto& p = it.p;
     890              :         // NOTE: build visible points with a margin so points directly at the edges
     891              :         // of container do not flash on re-paint
     892            0 :         if (p.x() < (m_xFrom - BOUNDARY_MARGIN) || p.x() > (m_xTo + BOUNDARY_MARGIN)) {
     893            0 :             continue;
     894              :         }
     895            0 :         const double xN = (p.x() - m_xFrom) / xRange;
     896            0 :         m_pointsNVisible.push_back(QPointF(std::clamp(xN, 0.0, 1.0), normY(p.y())));
     897            0 :         m_visibleToDomainIndex.push_back(it.idx);
     898              :     }
     899              : 
     900              :     // right boundary (synthetic)
     901            0 :     m_pointsNVisible.push_back(QPointF(1.1, yAt1N));
     902            0 :     m_visibleToDomainIndex.push_back(INVALID_POINT_IDX);
     903              : 
     904            0 :     updateActivePoint();
     905            0 :     update();
     906            0 : }
     907              : 
     908            0 : QVector<QPointF> PolylinePlot::polylinePx() const
     909              : {
     910            0 :     QVector<QPointF> pts;
     911              : 
     912            0 :     if (width() <= 0 || height() <= 0) {
     913            0 :         return pts;
     914              :     }
     915              : 
     916              :     // 0 or 1 point -> horizontal baseline
     917            0 :     if (m_pointsNVisible.size() < 2) {
     918            0 :         qreal yN = m_baselineN;
     919            0 :         if (m_pointsNVisible.size() == 1) {
     920            0 :             yN = m_pointsNVisible[0].y();
     921              :         }
     922            0 :         yN = clamp01(yN);
     923              : 
     924            0 :         const qreal y = toPxY(this, yN);
     925            0 :         pts.push_back(QPointF(0.0, y));
     926            0 :         pts.push_back(QPointF(width(), y));
     927              : 
     928            0 :         return pts;
     929              :     }
     930              : 
     931              :     // 2+ points -> polyline
     932            0 :     QVector<QPointF> sorted = m_pointsNVisible;
     933            0 :     std::sort(sorted.begin(), sorted.end(),
     934            0 :               [](const QPointF& a, const QPointF& b) { return a.x() < b.x(); });
     935              : 
     936            0 :     const QPointF firstN = sorted.front();
     937            0 :     const QPointF lastN  = sorted.back();
     938              : 
     939            0 :     const qreal firstYpx = toPxY(this, firstN.y());
     940            0 :     const qreal lastYpx  = toPxY(this, lastN.y());
     941              : 
     942            0 :     pts.reserve(sorted.size() + 2);
     943              : 
     944              :     // left horizontal segment start
     945            0 :     pts.push_back(QPointF(0.0, firstYpx));
     946              : 
     947              :     // actual points
     948            0 :     for (const auto& pN : sorted) {
     949            0 :         if (pN.x() < 0.0 || pN.x() > 1.0) {
     950              :             // ignore synthetic boundary points
     951            0 :             continue;
     952              :         }
     953            0 :         pts.push_back(QPointF(toPxX(this, clamp01(pN.x())),
     954            0 :                               toPxY(this, pN.y())));
     955              :     }
     956              : 
     957              :     // right horizontal segment end
     958            0 :     pts.push_back(QPointF(width(), lastYpx));
     959              : 
     960            0 :     return pts;
     961            0 : }
     962              : 
     963            0 : bool PolylinePlot::isNearLinePx(const QPointF& px) const
     964              : {
     965            0 :     const auto pts = polylinePx();
     966            0 :     if (pts.size() < 2) {
     967            0 :         return false;
     968              :     }
     969              : 
     970            0 :     qreal best = std::numeric_limits<qreal>::max();
     971            0 :     for (int i = 0; i < pts.size() - 1; ++i) {
     972            0 :         best = std::min(best, pointToSegmentDistance(px, pts[i], pts[i + 1]));
     973              :     }
     974            0 :     return best <= m_hitRadius;
     975            0 : }
     976              : 
     977            0 : int PolylinePlot::pointIndexAtPx(const QPointF& px) const
     978              : {
     979              :     // search in visible points, skip synthetic boundary points
     980            0 :     for (int i = 0; i < m_pointsNVisible.size(); ++i) {
     981            0 :         const int domainIdx = (i < m_visibleToDomainIndex.size()) ? m_visibleToDomainIndex[i] : INVALID_POINT_IDX;
     982            0 :         if (domainIdx < 0) {
     983            0 :             continue;
     984              :         }
     985              : 
     986            0 :         QPointF pN = m_pointsNVisible[i];
     987            0 :         const qreal x = toPxX(this, pN.x());
     988            0 :         const qreal y = toPxY(this, pN.y());
     989            0 :         const qreal dx = px.x() - x;
     990            0 :         const qreal dy = px.y() - y;
     991            0 :         if ((dx * dx + dy * dy) <= (m_hitRadius * m_hitRadius)) {
     992            0 :             return domainIdx;
     993              :         }
     994              :     }
     995              : 
     996            0 :     return INVALID_POINT_IDX;
     997              : }
     998              : 
     999            0 : GhostPoint PolylinePlot::ghostPointToPolylinePx(const QPointF& px) const
    1000              : {
    1001            0 :     GhostPoint best;
    1002              : 
    1003            0 :     const auto pts = polylinePx();
    1004            0 :     if (pts.size() < 2) {
    1005            0 :         best.point = px;
    1006            0 :         best.distToSegment = std::numeric_limits<qreal>::max();
    1007            0 :         return best;
    1008              :     }
    1009              : 
    1010            0 :     for (int i = 0; i < pts.size() - 1; ++i) {
    1011            0 :         const auto res = ghostPointToSegmentDistance(px, pts[i], pts[i + 1]);
    1012            0 :         if (res.distToSegment < best.distToSegment) {
    1013            0 :             best = res;
    1014              :         }
    1015              :     }
    1016              : 
    1017            0 :     return best;
    1018            0 : }
    1019              : 
    1020            0 : QPointF PolylinePlot::snapToNeighbor(qreal dragPxX, QPointF pDomain) const
    1021              : {
    1022            0 :     if (!m_isSnapEnabled || !m_hasDraggedPointDomain || m_points.size() < 2) {
    1023            0 :         return pDomain;
    1024              :     }
    1025              : 
    1026              :     // Find the dragged point's actual current index in m_points (which may
    1027              :     // have shifted after neighbor consumption) without modifying
    1028              :     // m_pressedPointIndex — the model needs that to stay at the original
    1029              :     // value so it can restore consumed points when dragging back.
    1030            0 :     int currentIdx = -1;
    1031            0 :     double bestDist = std::numeric_limits<double>::max();
    1032            0 :     for (int i = 0; i < m_points.size(); ++i) {
    1033            0 :         const double dx = std::abs(m_points[i].x() - m_draggedPointDomain.x());
    1034            0 :         const double dy = std::abs(m_points[i].y() - m_draggedPointDomain.y());
    1035            0 :         const double dist = dx + dy;
    1036            0 :         if (dist < bestDist) {
    1037            0 :             bestDist = dist;
    1038            0 :             currentIdx = i;
    1039              :         }
    1040              :     }
    1041              : 
    1042            0 :     if (currentIdx < 0) {
    1043            0 :         return pDomain;
    1044              :     }
    1045              : 
    1046              :     // Snap close to — but not exactly onto — a neighbor.
    1047              :     // Can't have multiple automation points at the exact same timepoint.
    1048              :     static constexpr double SNAP_EPSILON = 1e-9;
    1049              : 
    1050            0 :     if (currentIdx + 1 < m_points.size()) {
    1051            0 :         const qreal neighborPxX = toPxX(this, normalizedFromDomain(m_points[currentIdx + 1]).x());
    1052            0 :         if (std::abs(dragPxX - neighborPxX) <= m_snapThresholdPx) {
    1053            0 :             pDomain.setX(m_points[currentIdx + 1].x() - SNAP_EPSILON);
    1054              :         }
    1055              :     }
    1056              : 
    1057            0 :     if (currentIdx > 0) {
    1058            0 :         const qreal neighborPxX = toPxX(this, normalizedFromDomain(m_points[currentIdx - 1]).x());
    1059            0 :         if (std::abs(dragPxX - neighborPxX) <= m_snapThresholdPx) {
    1060            0 :             pDomain.setX(m_points[currentIdx - 1].x() + SNAP_EPSILON);
    1061              :         }
    1062              :     }
    1063              : 
    1064            0 :     return pDomain;
    1065              : }
    1066              : 
    1067            0 : void PolylinePlot::updateCursor()
    1068              : {
    1069            0 :     const bool interactive = m_hoveredOnLine || m_pressed || (m_pressedPointIndex >= 0);
    1070              : 
    1071            0 :     if (interactive) {
    1072            0 :         setCursor(Qt::CrossCursor);
    1073              :     } else {
    1074            0 :         unsetCursor();
    1075              :     }
    1076            0 : }
    1077              : 
    1078            0 : void PolylinePlot::resetGestureState()
    1079              : {
    1080            0 :     m_pressed = false;
    1081            0 :     m_pressedOnLine = false;
    1082            0 :     m_pressedOnPoint = false;
    1083            0 :     m_pressedPointIndex = INVALID_POINT_IDX;
    1084            0 :     m_hasDraggedPointDomain = false;
    1085            0 :     m_hoveredOnLine = false;
    1086            0 :     m_draggedPointDomain = {};
    1087            0 :     m_movedSincePress = false;
    1088            0 :     m_pressPx = QPointF(0.0, 0.0);
    1089              : 
    1090            0 :     updateCursor();
    1091            0 :     updateActivePoint();
    1092            0 :     update();
    1093            0 : }
    1094              : 
    1095            0 : void PolylinePlot::geometryChange(const QRectF& newG, const QRectF& oldG)
    1096              : {
    1097            0 :     QQuickPaintedItem::geometryChange(newG, oldG);
    1098            0 :     if (newG.size() != oldG.size()) {
    1099            0 :         rebuildVisiblePoints();
    1100              :     }
    1101            0 : }
    1102              : 
    1103            0 : void PolylinePlot::drawLinesAndFillUnder(QPainter* painter) const
    1104              : {
    1105            0 :     const auto pts = polylinePx();
    1106            0 :     if (pts.size() < 2) {
    1107            0 :         return;
    1108              :     }
    1109              : 
    1110            0 :     QPen pen(m_lineColor);
    1111            0 :     pen.setWidthF(m_lineWidth);
    1112            0 :     pen.setJoinStyle(Qt::MiterJoin);
    1113            0 :     painter->setPen(pen);
    1114            0 :     painter->setBrush(Qt::NoBrush);
    1115              : 
    1116            0 :     const qreal baselinePxY = toPxY(this, m_baselineN);
    1117              : 
    1118            0 :     for (int i = 0; i < pts.size() - 1; ++i) {
    1119            0 :         if (i < m_colorsUnderLine.size()) {
    1120              :             // fill area under line
    1121              :             const QVector<QPointF> areaUnderLine {
    1122            0 :                 QPointF(pts[i].x(), baselinePxY), // bottom left
    1123            0 :                 pts[i], // top left
    1124            0 :                 pts[i + 1], // top right
    1125            0 :                 QPointF(pts[i + 1].x(), baselinePxY), // bottom right
    1126            0 :             };
    1127            0 :             QPainterPath path;
    1128            0 :             path.addPolygon(QPolygonF(areaUnderLine));
    1129            0 :             painter->fillPath(path, m_colorsUnderLine.at(i));
    1130            0 :         }
    1131              :         // draw line
    1132            0 :         painter->drawLine(pts[i], pts[i + 1]);
    1133              :     }
    1134            0 : }
    1135              : 
    1136            0 : void PolylinePlot::paintPoint(QPainter* painter, const PolylinePointStyle* style, const QPointF& centre, bool useHoveredStyle) const
    1137              : {
    1138            0 :     IF_ASSERT_FAILED(painter && style) {
    1139            0 :         return;
    1140              :     }
    1141              : 
    1142            0 :     const qreal centerRadius = useHoveredStyle ? style->centerRadiusHovered() : style->centerRadius();
    1143            0 :     const QColor centerColor = useHoveredStyle ? style->centerColorHovered() : style->centerColor();
    1144              : 
    1145            0 :     const qreal middleRingWidth = useHoveredStyle ? style->middleRingWidthHovered() : style->middleRingWidth();
    1146            0 :     const QColor middleRingColor = useHoveredStyle ? style->middleRingColorHovered() : style->middleRingColor();
    1147              : 
    1148            0 :     const qreal outlineWidth = useHoveredStyle ? style->outlineWidthHovered() : style->outlineWidth();
    1149            0 :     const QColor outlineColor = useHoveredStyle ? style->outlineColorHovered() : style->outlineColor();
    1150              : 
    1151            0 :     if (middleRingWidth > 0.0) {
    1152            0 :         const qreal middleRadius = centerRadius + middleRingWidth;
    1153            0 :         const qreal outerRadius = middleRadius + outlineWidth;
    1154              : 
    1155            0 :         painter->setPen(Qt::NoPen);
    1156              : 
    1157            0 :         painter->setBrush(outlineColor);
    1158            0 :         painter->drawEllipse(centre, outerRadius, outerRadius);
    1159              : 
    1160            0 :         painter->setBrush(middleRingColor);
    1161            0 :         painter->drawEllipse(centre, middleRadius, middleRadius);
    1162              : 
    1163            0 :         painter->setBrush(centerColor);
    1164            0 :         painter->drawEllipse(centre, centerRadius, centerRadius);
    1165            0 :         return;
    1166              :     }
    1167              : 
    1168            0 :     painter->setPen(Qt::NoPen);
    1169            0 :     painter->setBrush(centerColor);
    1170            0 :     painter->drawEllipse(centre, centerRadius, centerRadius);
    1171              : 
    1172            0 :     if (outlineWidth > 0.0) {
    1173            0 :         QPen pen(outlineColor);
    1174            0 :         pen.setWidthF(outlineWidth);
    1175            0 :         painter->setPen(pen);
    1176            0 :         painter->setBrush(Qt::NoBrush);
    1177            0 :         painter->drawEllipse(centre, centerRadius, centerRadius);
    1178            0 :     }
    1179              : }
    1180              : 
    1181            0 : void PolylinePlot::paint(QPainter* painter)
    1182              : {
    1183            0 :     if (!painter) {
    1184            0 :         return;
    1185              :     }
    1186              : 
    1187            0 :     painter->setRenderHint(QPainter::Antialiasing, antialiasing());
    1188              : 
    1189              :     // draw background overlay
    1190            0 :     if (m_drawBackground) {
    1191            0 :         QColor overlay = uiConfiguration()->currentTheme().extra["black_color"].value<QColor>();
    1192            0 :         overlay.setAlphaF(0.25);
    1193              : 
    1194            0 :         painter->setPen(Qt::NoPen);
    1195            0 :         painter->setBrush(overlay);
    1196            0 :         painter->drawRect(boundingRect());
    1197              :     }
    1198              : 
    1199              :     // draw lines and fill area underneath
    1200            0 :     drawLinesAndFillUnder(painter);
    1201              : 
    1202              :     // draw points
    1203            0 :     IF_ASSERT_FAILED(m_standardPointStyle && m_ghostPointStyle && m_selectedPointStyle) {
    1204            0 :         return;
    1205              :     }
    1206              : 
    1207            0 :     const int n = m_pointsNVisible.size();
    1208            0 :     const int hoveredIndex = pointIndexAtPx(m_hoverPx);
    1209            0 :     for (int i = 0; i < n; ++i) {
    1210            0 :         QPointF pN = m_pointsNVisible[i];
    1211            0 :         const QPointF centre(toPxX(this, pN.x()), toPxY(this, pN.y()));
    1212              : 
    1213            0 :         const int domainIdx = (i < m_visibleToDomainIndex.size()) ? m_visibleToDomainIndex[i] : INVALID_POINT_IDX;
    1214            0 :         const bool isSelected = m_selectedPointsEnabled && domainIdx >= 0 && muse::contains(m_selectedPointsIndices, domainIdx);
    1215            0 :         const bool isHovered = !(m_pressed && isSelected) && domainIdx >= 0 && domainIdx == hoveredIndex;
    1216              : 
    1217            0 :         const PolylinePointStyle* style = isSelected ? m_selectedPointStyle : m_standardPointStyle;
    1218            0 :         paintPoint(painter, style, centre, /*useHoveredStyle*/ isHovered);
    1219              :     }
    1220              : 
    1221              :     // draw hover ghost point
    1222            0 :     if (m_ghostPointsEnabled && m_hoveredOnLine && m_pressedPointIndex < 0) {
    1223            0 :         QPointF hp = m_hoverGhostPx;
    1224              : 
    1225            0 :         if (m_pointsNVisible.size() < 2) {
    1226            0 :             const qreal yN = (m_pointsNVisible.size() == 1) ? m_pointsNVisible[0].y() : m_baselineN;
    1227            0 :             hp.setY(toPxY(this, yN));
    1228              :         }
    1229              : 
    1230            0 :         if (pointIndexAtPx(hp) < 0 && isNearLinePx(hp)) {
    1231            0 :             paintPoint(painter, m_ghostPointStyle, hp, /*useHoveredStyle*/ false);
    1232              :         }
    1233              :     }
    1234              : }
    1235              : 
    1236            0 : void PolylinePlot::hoverMoveEvent(QHoverEvent* e)
    1237              : {
    1238              :     // NOTE: even if mouse is still, Qt produces hoverMoveEvents constantly
    1239            0 :     if (m_hoverPx == e->position()) {
    1240              :         // IMPORTANT: let through hover events (otherwise double-click
    1241              :         // clip selection may be broken)
    1242            0 :         e->ignore();
    1243            0 :         return;
    1244              :     }
    1245              : 
    1246            0 :     m_hoverPx = e->position();
    1247              : 
    1248            0 :     const bool nearPoint = (pointIndexAtPx(m_hoverPx) >= 0);
    1249              : 
    1250            0 :     bool nearLine = false;
    1251            0 :     if (m_ghostPointsEnabled) {
    1252            0 :         const auto proj = ghostPointToPolylinePx(m_hoverPx);
    1253            0 :         nearLine = (proj.distToSegment <= m_hitRadius);
    1254              : 
    1255            0 :         if (m_pointsNVisible.size() >= 2) {
    1256            0 :             m_hoverGhostPx = proj.point;
    1257              :         } else {
    1258            0 :             m_hoverGhostPx = m_hoverPx;
    1259              :         }
    1260              :     } else {
    1261              :         // nearLine is still required for updateCursor (m_hoveredOnLine) and to accept events, but
    1262              :         // if ghost points are disabled we can skip the m_hoverGhostPx logic...
    1263            0 :         nearLine = isNearLinePx(m_hoverPx);
    1264              :     }
    1265              : 
    1266            0 :     m_hoveredOnLine = (nearPoint || nearLine);
    1267            0 :     updateCursor();
    1268              : 
    1269            0 :     updateActivePoint();
    1270            0 :     update();
    1271              : 
    1272              :     // IMPORTANT: only consume hover when automation is actually interactive.
    1273              :     // Otherwise let underlying ClipItem hover area keep containsMouse=true,
    1274              :     // so itemHovered stays correct for selection/double-click logic.
    1275            0 :     if (nearPoint || nearLine || m_pressed || m_pressedPointIndex >= 0) {
    1276            0 :         e->accept();
    1277              :     } else {
    1278            0 :         e->ignore();
    1279              :     }
    1280              : }
    1281              : 
    1282            0 : void PolylinePlot::hoverLeaveEvent(QHoverEvent* e)
    1283              : {
    1284              :     Q_UNUSED(e);
    1285            0 :     m_hoveredOnLine = false;
    1286            0 :     updateCursor();
    1287            0 :     updateActivePoint();
    1288            0 :     update();
    1289            0 : }
    1290              : 
    1291            0 : void PolylinePlot::mousePressEvent(QMouseEvent* e)
    1292              : {
    1293            0 :     if (e->button() != Qt::LeftButton) {
    1294            0 :         e->ignore();
    1295            0 :         return;
    1296              :     }
    1297              : 
    1298            0 :     if (!m_selectedPointsIndices.empty()) {
    1299            0 :         m_selectedPointsIndices.clear();
    1300              :     }
    1301              : 
    1302            0 :     const int pointIndex = pointIndexAtPx(e->position());
    1303            0 :     const bool onPoint = pointIndex >= 0;
    1304            0 :     const bool onLine  = isNearLinePx(e->position());
    1305              : 
    1306              :     // NOTE: allow clicks on the points and lines only
    1307            0 :     if (!onPoint && !onLine) {
    1308            0 :         e->ignore();
    1309            0 :         return;
    1310              :     }
    1311              : 
    1312            0 :     resetGestureState();
    1313              : 
    1314            0 :     e->accept();
    1315            0 :     updateCursor();
    1316              : 
    1317            0 :     m_pressed = true;
    1318            0 :     m_pressPx = e->position();
    1319              : 
    1320            0 :     if (onPoint) {
    1321            0 :         m_pressedOnPoint = true;
    1322            0 :         m_pressedPointIndex = pointIndex;
    1323            0 :         if (m_pressedPointIndex >= 0 && m_pressedPointIndex < m_points.size()) {
    1324            0 :             if (m_selectedPointsEnabled) {
    1325            0 :                 m_selectedPointsIndices.insert(m_pressedPointIndex);
    1326              :             }
    1327            0 :             m_draggedPointDomain = m_points[m_pressedPointIndex];
    1328            0 :             m_hasDraggedPointDomain = true;
    1329              :         }
    1330            0 :         updateActivePoint();
    1331            0 :         return;
    1332              :     }
    1333              : 
    1334            0 :     if (onLine) {
    1335            0 :         m_pressedOnLine = true;
    1336            0 :         m_pressedPointIndex = PENDING_POINT_IDX;
    1337            0 :         m_pressedOnPoint = false;
    1338              : 
    1339            0 :         if (width() > 0 && height() > 0) {
    1340            0 :             const auto ghostPoint = ghostPointToPolylinePx(e->position());
    1341              : 
    1342            0 :             QPointF pN(clamp01(ghostPoint.point.x() / width()),
    1343            0 :                        1.0 - clamp01(ghostPoint.point.y() / height()));
    1344              : 
    1345            0 :             const QPointF pDomain = domainFromNormalized(pN);
    1346            0 :             emit pointAdded(pDomain.x(), pDomain.y(), /*completed*/ false);
    1347              :         }
    1348              : 
    1349            0 :         updateActivePoint();
    1350            0 :         return;
    1351              :     }
    1352              : }
    1353              : 
    1354            0 : void PolylinePlot::mouseMoveEvent(QMouseEvent* e)
    1355              : {
    1356            0 :     if (!m_pressed) {
    1357            0 :         e->ignore();
    1358            0 :         return;
    1359              :     }
    1360              : 
    1361            0 :     e->accept();
    1362              : 
    1363            0 :     const QPointF pos = e->position();
    1364            0 :     if (!m_movedSincePress && (pos - m_pressPx).manhattanLength() > MOVE_THRESHOLD) {
    1365            0 :         m_movedSincePress = true;
    1366              :     }
    1367              : 
    1368              :     // drag point (2+ points only)
    1369            0 :     if (m_pressedPointIndex >= 0) {
    1370            0 :         if (width() <= 0 || height() <= 0) {
    1371            0 :             return;
    1372              :         }
    1373              : 
    1374            0 :         QPointF pN(pos.x() / width(), 1.0 - (pos.y() / height()));
    1375            0 :         pN = clamp01(pN);
    1376              : 
    1377            0 :         const QPointF pDomain = snapToNeighbor(pos.x(), domainFromNormalized(pN));
    1378              : 
    1379            0 :         m_draggedPointDomain = pDomain;
    1380            0 :         m_hasDraggedPointDomain = true;
    1381            0 :         emit pointMoved(m_pressedPointIndex, pDomain.x(), pDomain.y(), /*completed*/ false);
    1382            0 :         updateActivePoint();
    1383              : 
    1384            0 :         return;
    1385              :     }
    1386              : }
    1387              : 
    1388            0 : void PolylinePlot::mouseReleaseEvent(QMouseEvent* e)
    1389              : {
    1390            0 :     if (e->button() != Qt::LeftButton || !m_pressed) {
    1391            0 :         e->ignore();
    1392            0 :         return;
    1393              :     }
    1394            0 :     e->accept();
    1395              : 
    1396            0 :     const bool isClick = !m_movedSincePress;
    1397            0 :     const QPointF rel = e->position();
    1398              : 
    1399              :     // commit point drag
    1400            0 :     if (!isClick && m_pressedPointIndex >= 0) {
    1401            0 :         QPointF pN(rel.x() / width(), 1.0 - (rel.y() / height()));
    1402            0 :         pN = clamp01(pN);
    1403              : 
    1404            0 :         const QPointF pDomain = snapToNeighbor(rel.x(), domainFromNormalized(pN));
    1405              : 
    1406            0 :         m_draggedPointDomain = pDomain;
    1407            0 :         m_hasDraggedPointDomain = true;
    1408            0 :         emit pointMoved(m_pressedPointIndex, pDomain.x(), pDomain.y(), /*completed*/ true);
    1409            0 :         emit interactionFinished();
    1410            0 :         resetGestureState();
    1411            0 :         return;
    1412              :     }
    1413              : 
    1414              :     // click-without-drag on line: finalize the point that was added during press
    1415            0 :     if (isClick && m_pressedOnLine && m_pressedPointIndex >= 0 && m_pressedPointIndex < m_points.size()) {
    1416            0 :         const QPointF pDomain = m_points[m_pressedPointIndex];
    1417            0 :         emit pointAdded(pDomain.x(), pDomain.y(), /*completed*/ true);
    1418              :     }
    1419              : 
    1420            0 :     emit interactionFinished();
    1421            0 :     resetGestureState();
    1422              : }
    1423              : 
    1424            0 : void PolylinePlot::mouseDoubleClickEvent(QMouseEvent* e)
    1425              : {
    1426            0 :     if (e->button() != Qt::LeftButton) {
    1427            0 :         e->ignore();
    1428            0 :         return;
    1429              :     }
    1430              : 
    1431            0 :     const int idx = pointIndexAtPx(e->position());
    1432            0 :     if (idx < 0) {
    1433            0 :         e->ignore();
    1434            0 :         return;
    1435              :     }
    1436              : 
    1437            0 :     e->accept();
    1438              : 
    1439            0 :     emit pointRemoved(idx, /*completed*/ true);
    1440            0 :     emit interactionFinished();
    1441              : 
    1442            0 :     resetGestureState();
    1443              : }
        

Generated by: LCOV version 2.0-1