133 lines
16 KiB
C++
133 lines
16 KiB
C++
// PWM1 annotation rendering contract: normalized start/end values, -1..+1.
|
|
// Kept outside DecodeTrace so geometry can be exercised without a capture device.
|
|
#ifndef SET_PWM_WAVEFORM_H
|
|
#define SET_PWM_WAVEFORM_H
|
|
#include <QPainter>
|
|
#include <QStringList>
|
|
#include <cmath>
|
|
#include <algorithm> // Двоичный поиск ограничивает обработку видимой областью записи.
|
|
#include <cstdint> // Координаты отсчётов хранятся без потери точности в 64 битах.
|
|
#include <vector> // Пирамида экстремумов строится постепенно в потоке декодирования.
|
|
|
|
namespace set_pwm {
|
|
struct Line { // Отрезок кривой хранит координаты отсчётов и нормированные значения.
|
|
double start, end, a, b; // Время переводится в пиксели только при окончательной отрисовке.
|
|
}; // Одна колонка обзорного графика требует не более двух таких отрезков.
|
|
|
|
class Envelope { // Индекс позволяет рисовать длинную запись за время, зависящее от ширины окна.
|
|
struct Sample { // Исходные сегменты сохраняются для точного отображения при увеличении.
|
|
uint64_t start, end; // Целые номера отсчётов определяют границы сегмента PWM1.
|
|
double a, b; // Нормированные значения на концах сегмента лежат в диапазоне −1…+1.
|
|
}; // Сегменты добавляются в хронологическом порядке и могут иметь разрывы.
|
|
struct Range { // Узел пирамиды содержит оба экстремума своего диапазона.
|
|
double low, high; // Минимум и максимум сохраняют короткие выбросы при уменьшении масштаба.
|
|
}; // Усреднение здесь не применяется, поскольку оно скрывало бы пики.
|
|
std::vector<Sample> samples; // Полные данные нужны только для видимых сегментов крупного масштаба.
|
|
std::vector<std::vector<Range>> levels; // Уровень k объединяет блоки из 2^(k+1) сегментов.
|
|
|
|
static Range bounds(const Sample &s) { // Линейный сегмент достигает экстремумов на своих концах.
|
|
return {std::min(s.a, s.b), std::max(s.a, s.b)}; // Порядок значений не зависит от направления наклона.
|
|
} // Промежуточные точки линейного сегмента не расширяют этот диапазон.
|
|
static Range merge(Range a, Range b) { // Объединение узлов сохраняет экстремумы обеих частей.
|
|
return {std::min(a.low, b.low), std::max(a.high, b.high)}; // Операция применяется при добавлении и запросах.
|
|
} // Пирамида не изменяет исходные значения PWM.
|
|
Range range(size_t first, size_t last) const { // Полуоткрытый диапазон разбивается на готовые двоичные блоки.
|
|
Range result = {1.0, -1.0}; // Обратные границы нейтральны для допустимых значений −1…+1.
|
|
size_t level = 0; // Нулевой шаг работает с исходными сегментами.
|
|
while (first < last) { // Каждый проход поднимается на один уровень вместо обхода всех периодов.
|
|
if (first & 1) { // Левый непарный узел нельзя объединить с соседом за пределами запроса.
|
|
result = merge(result, level ? levels[level - 1][first] : bounds(samples[first])); // Готовый узел заменяет целый блок периодов.
|
|
++first; // Обработанный узел исключается из оставшегося диапазона.
|
|
} // Левая граница теперь выровнена на пару.
|
|
if (last & 1) { // Правый непарный узел также обрабатывается отдельно.
|
|
--last; // Правая граница полуоткрытого диапазона перемещается к последнему узлу.
|
|
result = merge(result, level ? levels[level - 1][last] : bounds(samples[last])); // Сохраняются оба экстремума правого блока.
|
|
} // После обработки края оставшиеся узлы можно объединить попарно.
|
|
first /= 2; // Индекс левой границы переводится на уровень родителей.
|
|
last /= 2; // Правая граница переводится на тот же уровень.
|
|
++level; // Следующий проход использует вдвое более крупные блоки.
|
|
} // Запрос требует лишь логарифмического числа объединений.
|
|
return result; // Диапазон не зависит от количества скрытых за пикселем периодов.
|
|
} // Вызывающая сторона передаёт только непустые диапазоны.
|
|
static double value(const Sample &s, double time) { // Значение на границе пикселя получается линейной интерполяцией.
|
|
return s.a + (s.b - s.a) * ((time - s.start) / (s.end - s.start)); // Длительность положительна благодаря проверке append.
|
|
} // Обрезка не переносит экстремум соседнего пикселя в текущий.
|
|
|
|
public: // Потокобезопасность обеспечивает существующая блокировка RowData.
|
|
void clear() { // Повторное декодирование должно удалить и сегменты, и индекс предыдущей записи.
|
|
samples.clear(); // Старые координаты больше не участвуют в поиске.
|
|
levels.clear(); // Старые экстремумы не должны попадать в новый график.
|
|
} // Память исходных сегментов может повторно использоваться в следующем захвате.
|
|
void append(uint64_t start, uint64_t end, double a, double b) { // Один PWM1-сегмент индексируется сразу при поступлении аннотации.
|
|
if (end <= start || (!samples.empty() && start < samples.back().end)) return; // Пустые и перекрывающиеся сегменты нарушили бы двоичный поиск.
|
|
samples.push_back({start, end, a, b}); // Точные значения остаются доступны при любом увеличении.
|
|
size_t index = samples.size() - 1; // Пересчитывается только путь нового сегмента к корню.
|
|
Range current = bounds(samples.back()); // Начальный диапазон содержит концы нового сегмента.
|
|
for (size_t level = 0; index || level < levels.size(); ++level) { // Рост дерева требует O(log N) действий на сегмент.
|
|
if (level == levels.size()) levels.emplace_back(); // Новый уровень появляется только при увеличении высоты дерева.
|
|
if (index & 1) current = merge(level ? levels[level - 1][index - 1] : bounds(samples[index - 1]), current); // Правый потомок объединяется с уже готовым левым.
|
|
index /= 2; // Переходим к индексу родительского узла.
|
|
if (index == levels[level].size()) levels[level].push_back(current); // Новый блок добавляется без перестройки предыдущих блоков.
|
|
else levels[level][index] = current; // Незавершённый блок обновляется после появления правого потомка.
|
|
} // Остальные узлы пирамиды остаются неизменными.
|
|
} // Индексация выполняется в рабочем потоке, а не во время перерисовки интерфейса.
|
|
void lines(std::vector<Line> &dest, uint64_t start, uint64_t end, size_t pixels) const { // Выборка возвращает геометрию только для видимой области.
|
|
if (samples.empty() || end <= start || !pixels) return; // Пустая область не требует вычислений или рисования.
|
|
const auto first = std::upper_bound(samples.begin(), samples.end(), start, [](double t, const Sample &s) { return t < s.end; }); // Исключаем сегменты, завершившиеся слева от окна.
|
|
const auto last = std::lower_bound(first, samples.end(), end, [](const Sample &s, double t) { return s.start < t; }); // Исключаем сегменты, начинающиеся справа от окна.
|
|
if (size_t(last - first) <= pixels * 2) { // При двух сегментах на пиксель точная геометрия уже достаточно мала.
|
|
for (auto i = first; i != last; ++i) { // Крупный масштаб сохраняет исходные отрезки и мгновенные переходы между смежными интервалами.
|
|
if (i != samples.begin() && i->start >= start) { // Предыдущий сегмент нужен также при сбросе θ точно на левой границе видимого окна.
|
|
const auto previous = i - 1; // Соседний сегмент содержит уровень непосредственно перед текущим интервалом.
|
|
if (previous->end == i->start && previous->b != i->a) dest.push_back({double(i->start), double(i->start), previous->b, i->a}); // Совпадающие границы означают вертикальный фронт, включая 360°↔0°; через отсутствие данных соединение не проводится.
|
|
} // Реальные промежутки без измерений остаются разрывами графика.
|
|
dest.push_back({double(i->start), double(i->end), i->a, i->b}); // Наклон и значения самого измеренного сегмента сохраняются без изменений.
|
|
} // Дополнительная вертикаль не расширяет временной диапазон и не создаёт ложного наклонного участка θ.
|
|
return; // Пирамида нужна только для плотного обзорного графика.
|
|
} // Для плотной записи число отрезков ограничивается шириной окна.
|
|
double previous_time = -1, previous_x = 0, previous_value = 0; // Состояние соединяет соседние непустые колонки, сохраняя разрывы.
|
|
for (size_t pixel = 0; pixel < pixels; ++pixel) { // Одна итерация соответствует одной экранной колонке.
|
|
const double left = start + double(end - start) * pixel / pixels; // Левая граница колонки выражена в отсчётах.
|
|
const double right = start + double(end - start) * (pixel + 1) / pixels; // Правая граница не накапливает ошибку округления.
|
|
const auto begin = std::upper_bound(first, last, left, [](double t, const Sample &s) { return t < s.end; }); // Двоичный поиск пропускает невидимые периоды.
|
|
const auto finish = std::lower_bound(begin, last, right, [](const Sample &s, double t) { return s.start < t; }); // В колонку входят только пересекающие её сегменты.
|
|
if (begin == finish) { previous_time = -1; continue; } // Пустая колонка разрывает кривую вместо выдуманного соединения.
|
|
const double begin_time = std::max(left, double(begin->start)); // Первый сегмент обрезается по реальному началу данных.
|
|
const double finish_time = std::min(right, double((finish - 1)->end)); // Последний сегмент обрезается по реальному концу данных.
|
|
const double a = value(*begin, begin_time), b = value(*(finish - 1), finish_time); // Концы нужны для соединения соседних колонок.
|
|
Range extent = {std::min(a, b), std::max(a, b)}; // Начальные экстремумы учитывают обрезанные границы колонки.
|
|
if (finish - begin > 1) { // Внутренние концы крайних сегментов тоже могут содержать экстремумы.
|
|
extent = merge(extent, {std::min(begin->b, (finish - 1)->a), std::max(begin->b, (finish - 1)->a)}); // Значения за внешними границами колонки не включаются.
|
|
if (finish - begin > 2) extent = merge(extent, range(size_t(begin - samples.begin() + 1), size_t(finish - samples.begin() - 1))); // Пирамида заменяет обход всех внутренних периодов.
|
|
} // Все экстремумы текущей колонки теперь учтены.
|
|
const double x = (left + right) / 2; // Обзорная вертикаль располагается в центре соответствующего пикселя.
|
|
if (previous_time == begin_time) dest.push_back({previous_x, x, previous_value, a}); // Соединяем только непрерывные данные соседних колонок.
|
|
dest.push_back({x, x, extent.low, extent.high}); // Вертикальный отрезок сохраняет минимум и максимум даже одиночного выброса.
|
|
previous_time = finish_time; // Следующая колонка проверит непрерывность по границе данных.
|
|
previous_x = x; // Координата предыдущей колонки нужна для соединительного отрезка.
|
|
previous_value = b; // Сохраняется последнее измеренное значение текущей колонки.
|
|
} // На экран выдаётся не более двух отрезков на пиксель независимо от длительности записи.
|
|
} // Полные аннотации при этом остаются доступны таблице, экспорту и увеличению масштаба.
|
|
}; // Индекс принадлежит строке аннотаций и освобождается вместе с ней.
|
|
|
|
inline bool segment(const QString &text, double &a, double &b)
|
|
{
|
|
const QStringList fields = text.split(' ');
|
|
if (fields.size() != 3 || fields[0] != "PWM1") return false;
|
|
bool okA = false, okB = false;
|
|
a = fields[1].toDouble(&okA);
|
|
b = fields[2].toDouble(&okB);
|
|
return okA && okB && std::isfinite(a) && std::isfinite(b)
|
|
&& a >= -1 && a <= 1 && b >= -1 && b <= 1;
|
|
}
|
|
|
|
inline void draw(QPainter &p, const QString &text, double x1, double x2,
|
|
double y, double amplitude)
|
|
{
|
|
double a, b;
|
|
if (segment(text, a, b))
|
|
p.drawLine(QPointF(x1, y - a * amplitude), QPointF(x2, y - b * amplitude));
|
|
}
|
|
}
|
|
#endif
|