283 static constexpr int timestamp_index = 0;
284 static constexpr int first_value_index = 1;
301 std::vector<vec_type> m_values;
307 boost::circular_buffer<Sample> m_recent;
308 std::array<float, max_components> m_seen_min{}, m_seen_max{};
312 int m_waterfall_row{};
313 uint64_t m_dropped{};
314 std::optional<QPointF> m_hover;
318 mutable QPolygonF m_polyline;
319 mutable std::array<QList<QLineF>,
fade_steps> m_fade;
323 QGraphicsItem* parent)
324 :
Process::EffectLayerView{parent}
325 , m_interval{
Scenario::closestParentInterval(process.parent())}
326 , m_stack{doc.commandStack}
329 setAcceptedMouseButtons(Qt::LeftButton);
330 setAcceptHoverEvents(
true);
331 auto inl = process.inlets().front();
335 if(parent && parent->type() == Process::NodeItem::Type)
337 inl->displayHandledExplicitly =
true;
342 inl->displayHandledExplicitly =
false;
348 m_interval, &Scenario::IntervalModel::executionEvent,
this,
349 [
this](Scenario::IntervalExecutionEvent ev) {
352 case Scenario::IntervalExecutionEvent::Playing:
353 case Scenario::IntervalExecutionEvent::Stopped:
363 m_fixed = controlInlet(process, 1);
364 m_min = controlInlet(process, 2);
365 m_max = controlInlet(process, 3);
366 m_mode = controlInlet(process, 4);
367 m_history = controlInlet(process, 5);
368 m_columns = controlInlet(process, 6);
369 m_palette = controlInlet(process, 7);
370 ModeSwitch::make(m_mode, doc,
this,
this);
371 repaintOn(
this, m_fixed, m_min, m_max, m_mode, m_history, m_columns, m_palette);
374 connect(m_mode, &Process::ControlInlet::valueChanged,
this, [
this] {
fitRanges(); });
377 connect(m_palette, &Process::ControlInlet::valueChanged,
this, [
this] {
387 Mode mode()
const {
return Ui::Monitor::enumValue(m_mode, Mode::Timeline, Mode::Histogram); }
388 const std::array<QRgb, 256>& palette()
const
391 return colorMap(enumValue(m_palette, ColorMap::Viridis, ColorMap::CoolWarm));
393 double history()
const {
return std::max(0.1f, Ui::Monitor::floatValue(m_history, 10.f)); }
394 bool fixedRange()
const
396 return m_fixed && m_min && m_max && ossia::convert<bool>(m_fixed->value());
417 if(!m_doubleClick(*ev))
423 if(mode() == Mode::Histogram)
435 float lo = std::numeric_limits<float>::max(), hi = std::numeric_limits<float>::lowest();
436 if(mode() == Mode::Array || mode() == Mode::Waterfall)
438 for(
int i = 0; i < m_array.count; i++)
440 lo = std::min(lo, m_array.v[i]);
441 hi = std::max(hi, m_array.v[i]);
446 for(
const auto& s : m_recent)
447 for(
int i = 0; i < s.count; i++)
449 lo = std::min(lo, s.v[i]);
450 hi = std::max(hi, s.v[i]);
463 void hoverMoveEvent(QGraphicsSceneHoverEvent* ev)
override
466 if(mode() == Mode::Array)
469 void hoverLeaveEvent(QGraphicsSceneHoverEvent*)
override
475 void refresh(SignalMonitor& mon)
477 bool changed =
false;
478 mon.samples.drain([&](
const Sample& s) {
483 m_dropped = mon.samples.dropped();
484 if(mon.arrays.consume(m_array))
487 if(mode() == Mode::Waterfall)
491 if(!m_recent.empty())
493 const double horizon = m_recent.back().time - history();
494 while(!m_recent.empty() && m_recent.front().time < horizon)
496 m_recent.pop_front();
502 if(usesHistoryRange())
508 void addTimeline(
const Sample& s)
510 const int N = s.count + 1;
511 this->num_rows = std::max(this->num_rows, N - 1);
512 if(
int(min.size()) < N)
513 min.resize(N, std::numeric_limits<float>::max());
514 if(
int(max.size()) < N)
515 max.resize(N, std::numeric_limits<float>::lowest());
518 vv.resize(N, boost::container::default_init);
519 vv[timestamp_index] = s.position;
520 for(
int i = 0; i < s.count; i++)
522 const float r = s.v[i];
524 min[i + 1] = std::min(min[i + 1], r);
525 max[i + 1] = std::max(max[i + 1], r);
529 if(!m_values.empty() && !m_values.back().empty())
530 if(vv[timestamp_index] < m_values.back()[timestamp_index])
536 m_values.push_back(std::move(vv));
539 void addRecent(
const Sample& s)
541 m_components = std::max(m_components, s.count);
545 bool usesHistoryRange()
const
552 case Mode::Histogram:
562 m_seen_min.fill(std::numeric_limits<float>::max());
563 m_seen_max.fill(std::numeric_limits<float>::lowest());
564 for(
const auto& s : m_recent)
565 for(
int i = 0; i < s.count; i++)
567 m_seen_min[i] = std::min(m_seen_min[i], s.v[i]);
568 m_seen_max[i] = std::max(m_seen_max[i], s.v[i]);
570 for(
int i = 0; i < max_components; i++)
571 if(m_seen_min[i] > m_seen_max[i])
581 const bool fixed = fixedRange();
583 fixed,
fixed ? ossia::convert<float>(m_min->value()) : 0.f,
584 fixed ? ossia::convert<float>(m_max->value()) : 1.f,
585 i < m_components ? m_seen_min[i] : 0.f, i < m_components ? m_seen_max[i] : 1.f);
586 if(r.first == r.second)
594 void addWaterfallRow()
596 const auto r = boundingRect().adjusted(28, 20, -2, -2);
597 const int w = std::max(8,
int(r.width())), h = std::max(8,
int(r.height()));
598 if(m_waterfall.width() != w || m_waterfall.height() != h)
600 m_waterfall = QImage(w, h, QImage::Format_RGB32);
601 m_waterfall.fill(palette()[0]);
604 if(m_array.count == 0)
606 const auto& lut = palette();
607 auto [lo, hi] = arrayRange();
610 m_waterfall_row = (m_waterfall_row - 1 + h) % h;
611 auto* line =
reinterpret_cast<QRgb*
>(m_waterfall.scanLine(m_waterfall_row));
612 for(
int x = 0; x < w; x++)
614 const int i = std::min(m_array.count - 1, x * m_array.count / w);
615 const int idx = std::clamp(
int((m_array.v[i] - lo) / (hi - lo) * 255.f), 0, 255);
620 std::pair<float, float> arrayRange()
const
624 float lo = ossia::convert<float>(m_min->value()), hi = ossia::convert<float>(m_max->value());
629 float lo = 0, hi = 1;
630 for(
int i = 0; i < m_array.count; i++)
632 lo = std::min(lo, m_array.v[i]);
633 hi = std::max(hi, m_array.v[i]);
636 if(hi > 1 && hi <= 255 && lo >= 0)
641 void draw_row_simple(QPainter* p, qreal w, qreal h,
int row_index,
auto to_01)
const
643 for(
int start_idx = 0; start_idx < std::ssize(m_values) - 1; start_idx++)
645 int end_idx = start_idx + 1;
647 const auto* p0 = &m_values[start_idx];
648 const auto& v0 = *p0;
649 const auto N0 = std::ssize(*p0);
652 const auto* p1 = &m_values[end_idx];
653 const auto& v1 = *p1;
654 const auto N1 = std::ssize(*p1);
657 QPointF start = {v0[timestamp_index] * w, to_01(v0[row_index]) * h};
658 QPointF end = {v1[timestamp_index] * w, to_01(v1[row_index]) * h};
659 p->drawLine(start, end);
663 void draw_row_constant(QPainter* p, qreal w, qreal h,
int row_index)
const
665 for(
auto& val : m_values)
667 if(std::ssize(val) > row_index)
669 QPointF p0 = {val[timestamp_index] * w, 0.5 * h};
676 void paint_impl(QPainter* p)
const override
695 case Mode::Waterfall:
698 case Mode::Histogram:
702 if(m_dropped > 0 && mode() != Mode::Timeline)
704 *p, boundingRect().bottomLeft() - QPointF{0, 16},
705 {QStringLiteral(
"%1 dropped").arg(m_dropped)});
708 void paintTimeline(QPainter* p)
const
710 if(m_values.size() < 2 || this->num_rows == 0)
714 p->setRenderHint(QPainter::Antialiasing,
true);
715 p->setPen(score::Skin::instance().Light.main.pen1_solid_flat_miter);
717 const auto w = m_defaultWidth > 0 ? m_defaultWidth : boundingRect().width();
719 constexpr double top = 20.;
720 const auto h = std::max(0., height() - top) / num_rows;
721 p->translate(QPointF{0, top});
723 const bool fixed = fixedRange();
724 const float fixed_min =
fixed ? ossia::convert<float>(m_min->value()) : 0.f;
725 const float fixed_max =
fixed ? ossia::convert<float>(m_max->value()) : 1.f;
727 for(
int row = 0; row < num_rows; ++row)
729 const int row_index = row + 1;
732 fixed, fixed_min, fixed_max, this->min[row_index], this->max[row_index]);
735 p->setClipRect(QRectF{0., 0., w, h});
738 draw_row_simple(p, w, h, row_index, [min, max](
float v) {
739 return 1.f - (v - min) / (max - min);
744 draw_row_constant(p, w, h, row_index);
747 p->translate(QPointF{0, h});
750 p->setRenderHint(QPainter::Antialiasing,
false);
754 void paintTrail(QPainter* p)
const
757 if(m_recent.empty() || m_components == 0)
760 const QRectF area = boundingRect().adjusted(28, 20, -2, -14);
761 const int rows = std::min(m_components, 16);
762 const double rh = area.height() / rows;
763 const double now = m_recent.back().time;
764 const double span = history();
765 for(
int row = 0; row < rows; row++)
767 const QRectF r{area.left(), area.top() + row * rh, area.width(), rh};
768 const auto [lo, hi] =
rangeOf(row);
769 p->setRenderHint(QPainter::Antialiasing,
false);
770 p->setPen(gridColor());
771 p->drawLine(r.bottomLeft(), r.bottomRight());
775 drawLabel(*p, QRectF(0, r.top(), area.left() - 2, 10), Qt::AlignRight | Qt::AlignTop, QString::number(hi,
'g', 3));
777 drawLabel(*p, QRectF(0, r.bottom() - 12, area.left() - 2, 10), Qt::AlignRight | Qt::AlignBottom, QString::number(lo,
'g', 3));
778 auto& line = m_polyline;
780 for(
const auto& s : m_recent)
784 const double x = r.right() - (now - s.time) / span * r.width();
785 const double y = r.bottom() - std::clamp((s.v[row] - lo) / (hi - lo), 0.f, 1.f) * r.height();
788 line << QPointF{x, line.back().y()};
789 line << QPointF{x, y};
792 line << QPointF{r.right(), line.back().y()};
793 p->setRenderHint(QPainter::Antialiasing,
true);
795 p->drawPolyline(line);
797 auto times =
linearTicks(0., span, area.width(), 50.,
" s");
799 t.label = t.value == 0 ? QStringLiteral(
"now") :
"-" + t.label;
800 drawXAxisLabels(*p, area, times, [&](
double v) {
return area.right() - v / span * area.width(); });
804 void paintXY(QPainter* p)
const
807 if(m_recent.empty() || m_components < 2)
809 drawLabel(*p, boundingRect(), Qt::AlignCenter, QStringLiteral(
"XY needs two components"));
813 p->setRenderHint(QPainter::Antialiasing,
true);
814 const QRectF b = boundingRect().adjusted(4, 20, -4, -4);
815 const double side = std::min(b.width(), b.height());
816 const QRectF r{b.center().x() - side / 2, b.center().y() - side / 2, side, side};
817 const bool three = m_components >= 3;
819 auto norm = [](
float v, std::pair<float, float> rr) {
820 return std::clamp((v - rr.first) / (rr.second - rr.first), 0.f, 1.f) * 2.f - 1.f;
823 auto project = [&](
float x,
float y,
float z) {
825 return QPointF{r.center().x() + x * side / 2, r.center().y() - y * side / 2};
826 const double s = side * 0.32;
828 r.center().x() + (x - z) * 0.866 * s, r.center().y() - (y + (x + z) * 0.5) * s * 0.9};
830 p->setPen(gridColor());
833 for(
float a : {-1.f, 1.f})
834 for(
float c : {-1.f, 1.f})
836 p->drawLine(project(-1, a, c), project(1, a, c));
837 p->drawLine(project(a, -1, c), project(a, 1, c));
838 p->drawLine(project(a, c, -1), project(a, c, 1));
844 p->drawLine(QPointF{r.center().x(), r.top()}, QPointF{r.center().x(), r.bottom()});
845 p->drawLine(QPointF{r.left(), r.center().y()}, QPointF{r.right(), r.center().y()});
848 const double now = m_recent.back().time, span = history();
849 for(
auto& b : m_fade)
852 bool has_prev =
false;
853 for(
const auto& s : m_recent)
857 const auto pt = project(norm(s.v[0], rx), norm(s.v[1], ry), three ? norm(s.v[2], rz) : 0.f);
860 const double alpha = std::clamp(1. - (now - s.time) / span, 0.05, 1.);
868 if(m_fade[i].isEmpty())
870 QColor c = traceColor();
872 p->setPen(QPen(c, 1.5));
873 p->drawLines(m_fade[i]);
875 p->setPen(Qt::NoPen);
876 p->setBrush(warnColor());
877 p->drawEllipse(prev, 4, 4);
878 const auto& last = m_recent.back();
879 QStringList info{QStringLiteral(
"x %1").arg(last.v[0], 0,
'g', 4), QStringLiteral(
"y %1").arg(last.v[1], 0,
'g', 4)};
881 info << QStringLiteral(
"z %1").arg(last.v[2], 0,
'g', 4);
887 void paintBars(QPainter* p)
const
892 const auto& s = m_recent.back();
896 const QRectF r = boundingRect().adjusted(4, 20, -4, -14);
897 const double bw = r.width() / s.count;
898 for(
int i = 0; i < s.count; i++)
900 const auto [lo, hi] =
rangeOf(i);
901 const double t = std::clamp((s.v[i] - lo) / (hi - lo), 0.f, 1.f);
903 const double zero = (lo < 0 && hi > 0) ? (-lo) / (hi - lo) : 0.;
904 const double y0 = r.bottom() - zero * r.height(), y1 = r.bottom() - t * r.height();
905 const QRectF bar{r.left() + i * bw + 1, std::min(y0, y1), std::max(1., bw - 2), std::abs(y1 - y0)};
906 p->fillRect(QRectF(r.left() + i * bw + 1, r.top(), std::max(1., bw - 2), r.height()), QColor(30, 30, 32));
909 drawLabel(*p, QRectF(r.left() + i * bw, r.bottom() + 1, bw, 12), Qt::AlignHCenter | Qt::AlignTop,
910 QString::number(s.v[i],
'g', 3));
921 Grid arrayGrid()
const
924 g.area = boundingRect().adjusted(2, 20, -2, -2);
925 const int n = std::max(1, m_array.count);
926 int cols = m_columns ? ossia::convert<int>(m_columns->value()) : 0;
928 cols = n == 512 ? 32 : std::max(1,
int(std::ceil(std::sqrt(n * g.area.width() / std::max(1., g.area.height())))));
930 g.rows = (n + cols - 1) / cols;
931 g.cell = std::max(1., std::min(g.area.width() / g.cols, g.area.height() / g.rows));
935 void paintArray(QPainter* p)
const
938 if(m_array.count == 0)
941 const auto g = arrayGrid();
942 const auto [lo, hi] = arrayRange();
943 const auto& lut = palette();
944 const double gap = g.cell >= 8 ? 1 : 0;
946 QFont f = smallFont();
947 f.setPixelSize(
int(std::clamp(g.cell * 0.4, 6., 10.)));
949 for(
int i = 0; i < m_array.count; i++)
951 const QRectF cell{g.area.left() + (i % g.cols) * g.cell, g.area.top() + (i / g.cols) * g.cell, g.cell - gap, g.cell - gap};
952 const int idx = std::clamp(
int((m_array.v[i] - lo) / (hi - lo) * 255.f), 0, 255);
953 p->fillRect(cell, QColor::fromRgb(lut[idx]));
956 p->setPen(qGray(lut[idx]) > 140 ? Qt::black : Qt::white);
957 p->drawText(cell, Qt::AlignCenter, QString::number(m_array.v[i],
'g', 3));
959 if(m_hover && cell.contains(*m_hover))
962 QString head = QStringLiteral(
"%1 values, %2 to %3").arg(m_array.count).arg(lo).arg(hi);
964 head = QStringLiteral(
"[%1] = %2").arg(hovered).arg(m_array.v[hovered], 0,
'g', 6);
966 drawLabel(*p, QRectF(2, 0, boundingRect().width() - 6, 13), Qt::AlignRight | Qt::AlignVCenter, head);
970 void paintWaterfall(QPainter* p)
const
973 if(m_waterfall.isNull())
975 const QRectF r = boundingRect().adjusted(28, 20, -2, -2);
978 const int h = m_waterfall.height(), w = m_waterfall.width();
979 const int newest = m_waterfall_row, upper = h - newest;
980 p->drawImage(QRectF(r.left(), r.top(), w, upper), m_waterfall, QRectF(0, newest, w, upper));
983 QRectF(r.left(), r.top() + upper, w, newest), m_waterfall, QRectF(0, 0, w, newest));
986 void paintHistogram(QPainter* p)
const
992 const QRectF r = boundingRect().adjusted(4, 20, -4, -14);
993 const auto [lo, hi] =
rangeOf(0);
994 std::array<int, 64> bins{};
995 double sum = 0, sum2 = 0;
997 for(
const auto& s : m_recent)
1001 const float v = s.v[0];
1002 bins[std::clamp(
int((v - lo) / (hi - lo) * bins.size()), 0,
int(bins.size()) - 1)]++;
1007 const int peak = std::max(1, *std::max_element(bins.begin(), bins.end()));
1008 const double bw = r.width() / bins.size();
1009 for(std::size_t i = 0; i < bins.size(); i++)
1011 const double h = double(bins[i]) / peak * r.height();
1012 p->fillRect(QRectF(r.left() + i * bw, r.bottom() - h, std::max(1., bw - 1), h), traceColor());
1014 drawLabel(*p, QRectF(r.left(), r.bottom() + 1, 80, 12), Qt::AlignLeft | Qt::AlignTop, QString::number(lo,
'g', 3));
1015 drawLabel(*p, QRectF(r.right() - 80, r.bottom() + 1, 80, 12), Qt::AlignRight | Qt::AlignTop, QString::number(hi,
'g', 3));
1016 const double mean = n ? sum / n : 0, sd = n ? std::sqrt(std::max(0., sum2 / n - mean * mean)) : 0;
1019 {QStringLiteral(
"n %1").arg(n), QStringLiteral(
"mean %1").arg(mean, 0,
'g', 4),
1020 QStringLiteral(
"sd %1").arg(sd, 0,
'g', 4),
1021 QStringLiteral(
"min %1 max %2").arg(m_seen_min[0], 0,
'g', 4).arg(m_seen_max[0], 0,
'g', 4)});