• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 18685379784

21 Oct 2025 01:25PM UTC coverage: 72.522% (+0.1%) from 72.391%
18685379784

push

github

paulmthompson
fix failing tests

18 of 40 new or added lines in 1 file covered. (45.0%)

1765 existing lines in 32 files now uncovered.

53998 of 74457 relevant lines covered (72.52%)

46177.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

31.7
/src/WhiskerToolbox/DataViewer_Widget/OpenGLWidget.cpp
1
#include "OpenGLWidget.hpp"
2

3
#include "AnalogTimeSeries/Analog_Time_Series.hpp"
4
#include "DataManager/utils/color.hpp"
5
#include "DataViewer/AnalogTimeSeries/AnalogTimeSeriesDisplayOptions.hpp"
6
#include "DataViewer/AnalogTimeSeries/MVP_AnalogTimeSeries.hpp"
7
#include "DataViewer/DigitalEvent/DigitalEventSeriesDisplayOptions.hpp"
8
#include "DataViewer/DigitalEvent/MVP_DigitalEvent.hpp"
9
#include "DataViewer/DigitalInterval/DigitalIntervalSeriesDisplayOptions.hpp"
10
#include "DataViewer/DigitalInterval/MVP_DigitalInterval.hpp"
11
#include "DataViewer/PlottingManager/PlottingManager.hpp"
12
#include "DataViewer_Widget.hpp"
13
#include "DigitalTimeSeries/Digital_Event_Series.hpp"
14
#include "DigitalTimeSeries/Digital_Interval_Series.hpp"
15
#include "ShaderManager/ShaderManager.hpp"
16
#include "TimeFrame/TimeFrame.hpp"
17

18
#include <QMouseEvent>
19
#include <QOpenGLContext>
20
#include <QOpenGLFunctions>
21
#include <QOpenGLShader>
22
#include <QPainter>
23
#include <glm/glm.hpp>
24
#include <glm/gtc/matrix_transform.hpp>
25
//#include <glm/gtx/transform.hpp>
26

27
#include <algorithm>
28
#include <chrono>
29
#include <cstdlib>
30
#include <ctime>
31
#include <iostream>
32
#include <ranges>
33

34

35
// This was a helpful resource for making a dashed line:
36
//https://stackoverflow.com/questions/52928678/dashed-line-in-opengl3
37

38
/*
39

40
Currently this is pretty hacky OpenGL code that will be refactored to take advantage of
41
OpenGL.
42

43
I will establish a Model-View-Projection matrix framework.
44

45
1) model matrix (object to world space)
46
    - can be used to shift data up or down?
47
2) view matrix (world space to camera space)
48
    - Camera panning
49
3) projection matrix (camera space to clip space)
50
    - orthographic projection
51
    - zooming and visible area definition
52

53
I will also only select the data that is present
54

55

56
*/
57

58
OpenGLWidget::OpenGLWidget(QWidget * parent)
20✔
59
    : QOpenGLWidget(parent) {
100✔
60
    setMouseTracking(true);// Enable mouse tracking for hover events
20✔
61
}
20✔
62

63
OpenGLWidget::~OpenGLWidget() {
40✔
64
    cleanup();
20✔
65
}
40✔
66

67
void OpenGLWidget::updateCanvas(int time) {
200✔
68
    _time = time;
200✔
69
    //std::cout << "Redrawing at " << _time << std::endl;
70
    update();
200✔
71
}
200✔
72

73
// Add these implementations:
74
void OpenGLWidget::mousePressEvent(QMouseEvent * event) {
×
75
    if (event->button() == Qt::LeftButton) {
×
76
        // Check if we're clicking near an interval edge for dragging
77
        auto edge_info = findIntervalEdgeAtPosition(static_cast<float>(event->pos().x()), static_cast<float>(event->pos().y()));
×
78
        if (edge_info.has_value()) {
×
79
            auto const [series_key, is_left_edge] = edge_info.value();
×
80
            startIntervalDrag(series_key, is_left_edge, event->pos());
×
81
            return;// Don't start panning when dragging intervals
×
82
        }
×
83

84
        _isPanning = true;
×
85
        _lastMousePos = event->pos();
×
86

87
        // Emit click coordinates for interval selection
88
        float const canvas_x = static_cast<float>(event->pos().x());
×
89
        float const canvas_y = static_cast<float>(event->pos().y());
×
90

91
        // Convert canvas X to time coordinate
92
        float const time_coord = canvasXToTime(canvas_x);
×
93

94
        // Find the closest analog series for Y coordinate conversion (similar to mouseMoveEvent)
95
        QString series_info = "";
×
96
        if (!_analog_series.empty()) {
×
97
            for (auto const & [key, data]: _analog_series) {
×
98
                if (data.display_options->is_visible) {
×
99
                    float const analog_value = canvasYToAnalogValue(canvas_y, key);
×
100
                    series_info = QString("Series: %1, Value: %2").arg(QString::fromStdString(key)).arg(analog_value, 0, 'f', 3);
×
101
                    break;
×
102
                }
103
            }
104
        }
105

106
        emit mouseClick(time_coord, canvas_y, series_info);
×
107
    }
×
108
    QOpenGLWidget::mousePressEvent(event);
×
109
}
110

111
void OpenGLWidget::mouseMoveEvent(QMouseEvent * event) {
×
112
    if (_is_dragging_interval) {
×
113
        // Update interval drag
114
        updateIntervalDrag(event->pos());
×
115
        return;// Don't do other mouse move processing while dragging
×
116
    }
117

118
    if (_is_creating_new_interval) {
×
119
        // Update new interval creation
120
        updateNewIntervalCreation(event->pos());
×
121
        return;// Don't do other mouse move processing while creating
×
122
    }
123

124
    if (_isPanning) {
×
125
        // Calculate vertical movement in pixels
126
        int const deltaY = event->pos().y() - _lastMousePos.y();
×
127

128
        // Convert to normalized device coordinates
129
        // A positive deltaY (moving down) should move the view up
130
        float const normalizedDeltaY = -1.0f * static_cast<float>(deltaY) / static_cast<float>(height()) * 2.0f;
×
131

132
        // Adjust vertical offset based on movement
133
        _verticalPanOffset += normalizedDeltaY;
×
134

135
        _lastMousePos = event->pos();
×
136
        update();// Request redraw
×
137
    } else {
138
        // Check for cursor changes when hovering near interval edges
139
        auto edge_info = findIntervalEdgeAtPosition(static_cast<float>(event->pos().x()), static_cast<float>(event->pos().y()));
×
140
        if (edge_info.has_value()) {
×
141
            setCursor(Qt::SizeHorCursor);
×
142
        } else {
143
            setCursor(Qt::ArrowCursor);
×
144
        }
145
    }
×
146

147
    // Emit hover coordinates for coordinate display
148
    float const canvas_x = static_cast<float>(event->pos().x());
×
149
    float const canvas_y = static_cast<float>(event->pos().y());
×
150

151
    // Convert canvas X to time coordinate
152
    float const time_coord = canvasXToTime(canvas_x);
×
153

154
    // Find the closest analog series for Y coordinate conversion
155
    QString series_info = "";
×
156
    if (!_analog_series.empty()) {
×
157
        // For now, use the first visible analog series for Y coordinate conversion
158
        for (auto const & [key, data]: _analog_series) {
×
159
            if (data.display_options->is_visible) {
×
160
                float const analog_value = canvasYToAnalogValue(canvas_y, key);
×
161
                series_info = QString("Series: %1, Value: %2").arg(QString::fromStdString(key)).arg(analog_value, 0, 'f', 3);
×
162
                break;
×
163
            }
164
        }
165
    }
166

167
    emit mouseHover(time_coord, canvas_y, series_info);
×
168

169
    QOpenGLWidget::mouseMoveEvent(event);
×
170
}
×
171

172
void OpenGLWidget::mouseReleaseEvent(QMouseEvent * event) {
×
173
    if (event->button() == Qt::LeftButton) {
×
174
        if (_is_dragging_interval) {
×
175
            finishIntervalDrag();
×
176
        } else if (_is_creating_new_interval) {
×
177
            finishNewIntervalCreation();
×
178
        } else {
179
            _isPanning = false;
×
180
        }
181
    }
182
    QOpenGLWidget::mouseReleaseEvent(event);
×
183
}
×
184

185
void OpenGLWidget::setBackgroundColor(std::string const & hexColor) {
×
186
    m_background_color = hexColor;
×
187
    updateCanvas(_time);
×
188
}
×
189

190
void OpenGLWidget::setPlotTheme(PlotTheme theme) {
×
191
    _plot_theme = theme;
×
192

193
    if (theme == PlotTheme::Dark) {
×
194
        // Dark theme: black background, white axes
195
        m_background_color = "#000000";
×
196
        m_axis_color = "#FFFFFF";
×
197
    } else {
198
        // Light theme: white background, dark axes
199
        m_background_color = "#FFFFFF";
×
200
        m_axis_color = "#333333";
×
201
    }
202

203
    updateCanvas(_time);
×
204
}
×
205

206
void OpenGLWidget::cleanup() {
38✔
207
    // Avoid re-entrancy or cleanup without a valid context
208
    if (!_gl_initialized) {
38✔
209
        return;
20✔
210
    }
211

212
    // Guard: QOpenGLContext may already be gone during teardown
213
    if (QOpenGLContext::currentContext() == nullptr && context() == nullptr) {
18✔
214
        _gl_initialized = false;
×
215
        return;
×
216
    }
217

218
    // Safe to release our GL resources
219
    makeCurrent();
18✔
220

221
    if (m_program) {
18✔
222
        delete m_program;
×
223
        m_program = nullptr;
×
224
    }
225
    if (m_dashedProgram) {
18✔
226
        delete m_dashedProgram;
×
227
        m_dashedProgram = nullptr;
×
228
    }
229

230
    m_vbo.destroy();
18✔
231
    m_vao.destroy();
18✔
232

233
    doneCurrent();
18✔
234

235
    _gl_initialized = false;
18✔
236
}
237

238
QOpenGLShaderProgram * create_shader_program(char const * vertexShaderSource,
×
239
                                             char const * fragmentShaderSource) {
240
    auto prog = new QOpenGLShaderProgram;
×
241
    prog->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
×
242
    prog->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
×
243

244
    prog->link();
×
245

246
    return prog;
×
247
}
248

249
// Replace the old create_shader_program with a new version that loads from resource files for the dashed shader
250
QOpenGLShaderProgram * create_shader_program_from_resource(QString const & vertexResource, QString const & fragmentResource) {
×
251
    auto prog = new QOpenGLShaderProgram;
×
252
    prog->addShaderFromSourceFile(QOpenGLShader::Vertex, vertexResource);
×
253
    prog->addShaderFromSourceFile(QOpenGLShader::Fragment, fragmentResource);
×
254
    prog->link();
×
255
    return prog;
×
256
}
257

258
void OpenGLWidget::initializeGL() {
18✔
259
    // Ensure QOpenGLFunctions is initialized
260
    initializeOpenGLFunctions();
18✔
261

262
    // Track GL init and connect context destruction
263
    _gl_initialized = true;
18✔
264
    if (context()) {
18✔
265
        // Disconnect any previous connection to avoid duplicates
266
        if (_ctxAboutToBeDestroyedConn) {
18✔
267
            disconnect(_ctxAboutToBeDestroyedConn);
×
268
        }
269
        _ctxAboutToBeDestroyedConn = connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, [this]() {
36✔
270
            cleanup();
18✔
271
        });
18✔
272
    }
273

274
    auto fmt = format();
18✔
275
    std::cout << "OpenGL major version: " << fmt.majorVersion() << std::endl;
18✔
276
    std::cout << "OpenGL minor version: " << fmt.minorVersion() << std::endl;
18✔
277
    int r, g, b;
18✔
278
    hexToRGB(m_background_color, r, g, b);
18✔
279
    glClearColor(
18✔
280
            static_cast<float>(r) / 255.0f,
18✔
281
            static_cast<float>(g) / 255.0f,
18✔
282
            static_cast<float>(b) / 255.0f,
18✔
283
            1.0f);
284
    glEnable(GL_BLEND);
18✔
285
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
18✔
286
    // Load axes shader
287
    ShaderManager::instance().loadProgram(
162✔
288
            "axes",
289
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/colored_vertex.vert" : "src/WhiskerToolbox/shaders/colored_vertex.vert",
18✔
290
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/colored_vertex.frag" : "src/WhiskerToolbox/shaders/colored_vertex.frag",
18✔
291
            "",
292
            m_shaderSourceType);
293
    // Load dashed line shader
294
    ShaderManager::instance().loadProgram(
162✔
295
            "dashed_line",
296
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/dashed_line.vert" : "src/WhiskerToolbox/shaders/dashed_line.vert",
18✔
297
            m_shaderSourceType == ShaderSourceType::Resource ? ":/shaders/dashed_line.frag" : "src/WhiskerToolbox/shaders/dashed_line.frag",
18✔
298
            "",
299
            m_shaderSourceType);
300

301
    // Get uniform locations for axes shader
302
    auto axesProgram = ShaderManager::instance().getProgram("axes");
54✔
303
    if (axesProgram) {
18✔
304
        auto nativeProgram = axesProgram->getNativeProgram();
18✔
305
        if (nativeProgram) {
18✔
306
            m_projMatrixLoc = nativeProgram->uniformLocation("projMatrix");
18✔
307
            m_viewMatrixLoc = nativeProgram->uniformLocation("viewMatrix");
18✔
308
            m_modelMatrixLoc = nativeProgram->uniformLocation("modelMatrix");
18✔
309
            m_colorLoc = nativeProgram->uniformLocation("u_color");
18✔
310
            m_alphaLoc = nativeProgram->uniformLocation("u_alpha");
18✔
311
        }
312
    }
313

314
    // Get uniform locations for dashed line shader
315
    auto dashedProgram = ShaderManager::instance().getProgram("dashed_line");
54✔
316
    if (dashedProgram) {
18✔
317
        auto nativeProgram = dashedProgram->getNativeProgram();
18✔
318
        if (nativeProgram) {
18✔
319
            m_dashedProjMatrixLoc = nativeProgram->uniformLocation("u_mvp");
18✔
320
            m_dashedResolutionLoc = nativeProgram->uniformLocation("u_resolution");
18✔
321
            m_dashedDashSizeLoc = nativeProgram->uniformLocation("u_dashSize");
18✔
322
            m_dashedGapSizeLoc = nativeProgram->uniformLocation("u_gapSize");
18✔
323
        }
324
    }
325

326
    // Connect reload signal to redraw
327
    connect(&ShaderManager::instance(), &ShaderManager::shaderReloaded, this, [this](std::string const &) { update(); });
18✔
328
    m_vao.create();
18✔
329
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
18✔
330
    m_vbo.create();
18✔
331
    m_vbo.bind();
18✔
332
    m_vbo.setUsagePattern(QOpenGLBuffer::StaticDraw);
18✔
333
    setupVertexAttribs();
18✔
334
}
36✔
335

336
void OpenGLWidget::setupVertexAttribs() {
294✔
337

338
    m_vbo.bind();                     // glBindBuffer(GL_ARRAY_BUFFER, m_vbo.bufferId());
294✔
339
    int const vertex_argument_num = 4;// Position (x, y, 0, 1) for axes shader
294✔
340

341
    // Attribute 0: vertex positions (x, y, 0, 1) for axes shader
342
    glEnableVertexAttribArray(0);
294✔
343
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, vertex_argument_num * sizeof(GLfloat), nullptr);
294✔
344

345
    // Disable unused vertex attributes
346
    glDisableVertexAttribArray(1);
294✔
347
    glDisableVertexAttribArray(2);
294✔
348

349
    m_vbo.release();
294✔
350
}
294✔
351

352
///////////////////////////////////////////////////////////////////////////////
353

354
/**
355
 * @brief OpenGLWidget::drawDigitalEventSeries
356
 *
357
 * Each event is specified by a single time point.
358
 * We can find which of the time points are within the visible time frame
359
 * After those are found, we will draw a vertical line at that time point
360
 * 
361
 * Now supports two display modes:
362
 * - Stacked: Events are positioned in separate horizontal lanes with configurable spacing
363
 * - Full Canvas: Events stretch from top to bottom of the canvas (original behavior)
364
 */
365
void OpenGLWidget::drawDigitalEventSeries() {
69✔
366
    int r, g, b;
69✔
367
    auto const start_time = _xAxis.getStart();
69✔
368
    auto const end_time = _xAxis.getEnd();
69✔
369
    auto axesProgram = ShaderManager::instance().getProgram("axes");
207✔
370
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
69✔
371

372
    auto const min_y = _yMin;
69✔
373
    auto const max_y = _yMax;
69✔
374

375
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);// glBindVertexArray
69✔
376
    setupVertexAttribs();
69✔
377

378
    // Count visible event series for stacked positioning
379
    int visible_event_count = 0;
69✔
380
    for (auto const & [key, event_data]: _digital_event_series) {
95✔
381
        if (event_data.display_options->is_visible) {
26✔
382
            visible_event_count++;
26✔
383
        }
384
    }
385

386
    if (visible_event_count == 0 || !_master_time_frame) {
69✔
387
        glUseProgram(0);
57✔
388
        return;
57✔
389
    }
390

391
    // Calculate center coordinate for stacked mode (similar to analog series)
392
    //float const center_coord = -0.5f * 0.1f * (static_cast<float>(visible_event_count - 1));// Use default spacing for center calculation
393

394
    int visible_series_index = 0;// counts all visible event series (for iteration)
12✔
395
    int stacked_series_index = 0;// index among stacked-mode event series only
12✔
396
    int const total_analog_visible = static_cast<int>(_plotting_manager->getVisibleAnalogSeriesKeys().size());
12✔
397
    // Count stacked-mode events (exclude FullCanvas from stackable count)
398
    int stacked_event_count = 0;
12✔
399
    for (auto const & [key, event_data]: _digital_event_series) {
38✔
400
        if (event_data.display_options->is_visible &&
52✔
401
            event_data.display_options->display_mode == EventDisplayMode::Stacked) {
26✔
402
            stacked_event_count++;
25✔
403
        }
404
    }
405
    int const total_stackable_series = total_analog_visible + stacked_event_count;
12✔
406

407
    for (auto const & [key, event_data]: _digital_event_series) {
38✔
408
        auto const & series = event_data.series;
26✔
409
        auto const & time_frame = event_data.time_frame;
26✔
410
        auto const & display_options = event_data.display_options;
26✔
411

412
        if (!display_options->is_visible) continue;
26✔
413

414
        hexToRGB(display_options->hex_color, r, g, b);
26✔
415
        float const rNorm = static_cast<float>(r) / 255.0f;
26✔
416
        float const gNorm = static_cast<float>(g) / 255.0f;
26✔
417
        float const bNorm = static_cast<float>(b) / 255.0f;
26✔
418
        float const alpha = display_options->alpha;
26✔
419

420
        auto visible_events = series->getEventsInRange(TimeFrameIndex(start_time),
26✔
421
                                                       TimeFrameIndex(end_time),
422
                                                       _master_time_frame.get(),
26✔
423
                                                       time_frame.get());
52✔
424

425
        // === MVP MATRIX SETUP ===
426

427
        // We need to check if we have a PlottingManager reference
428
        if (!_plotting_manager) {
26✔
429
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
430
            continue;
×
431
        }
432

433
        // Determine plotting mode and allocate accordingly
434
        display_options->plotting_mode = (display_options->display_mode == EventDisplayMode::Stacked)
52✔
435
                                                 ? EventPlottingMode::Stacked
26✔
436
                                                 : EventPlottingMode::FullCanvas;
437

438
        float allocated_y_center = 0.0f;
26✔
439
        float allocated_height = 0.0f;
26✔
440
        if (display_options->plotting_mode == EventPlottingMode::Stacked) {
26✔
441
            // Use global stacked allocation across analog + stacked events
442
            _plotting_manager->calculateGlobalStackedAllocation(-1, stacked_series_index, total_stackable_series,
25✔
443
                                                                allocated_y_center, allocated_height);
444
            stacked_series_index++;
25✔
445
        } else {
446
            // Full canvas allocation
447
            allocated_y_center = (_plotting_manager->viewport_y_min + _plotting_manager->viewport_y_max) * 0.5f;
1✔
448
            allocated_height = _plotting_manager->viewport_y_max - _plotting_manager->viewport_y_min;
1✔
449
        }
450

451
        display_options->allocated_y_center = allocated_y_center;
26✔
452
        display_options->allocated_height = allocated_height;
26✔
453

454
        // Apply PlottingManager pan offset
455
        _plotting_manager->setPanOffset(_verticalPanOffset);
26✔
456

457
        auto Model = new_getEventModelMat(*display_options, *_plotting_manager);
26✔
458
        auto View = new_getEventViewMat(*display_options, *_plotting_manager);
26✔
459
        auto Projection = new_getEventProjectionMat(start_time, end_time, _yMin, _yMax, *_plotting_manager);
26✔
460

461
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
26✔
462
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
26✔
463
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
26✔
464

465
        // Set color and alpha uniforms
466
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
26✔
467
        glUniform1f(m_alphaLoc, alpha);
26✔
468

469
        // Set line thickness from display options
470
        glLineWidth(static_cast<float>(display_options->line_thickness));
26✔
471

472
        for (auto const & event: visible_events) {
26✔
473
            // Calculate X position in master time frame coordinates for consistent rendering
474
            float xCanvasPos;
475
            if (time_frame.get() == _master_time_frame.get()) {
×
476
                // Same time frame - event is already in correct coordinates
477
                xCanvasPos = event;
×
478
            } else {
479
                // Different time frames - convert event index to time, then to master time frame
480
                float event_time = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(static_cast<int>(event))));
×
481
                xCanvasPos = event_time;// This should work if both time frames use the same time units
×
482
            }
483

484
            // Provide local [-1, 1] vertical endpoints; Model handles placement/scale
485
            std::array<GLfloat, 8> vertices = {
×
486
                    xCanvasPos, -1.0f, 0.0f, 1.0f,
487
                    xCanvasPos,  1.0f, 0.0f, 1.0f};
×
488

489
            glBindBuffer(GL_ARRAY_BUFFER, m_vbo.bufferId());
×
490
            m_vbo.allocate(vertices.data(), vertices.size() * sizeof(GLfloat));
×
491

492
            GLint const first = 0;  // Starting index of enabled array
×
493
            GLsizei const count = 2;// number of vertices to render
×
494
            glDrawArrays(GL_LINES, first, count);
×
495
        }
496

497
        visible_series_index++;
26✔
498
    }
499

500
    // Reset line width to default
501
    glLineWidth(1.0f);
12✔
502
    glUseProgram(0);
12✔
503
}
69✔
504

505
///////////////////////////////////////////////////////////////////////////////
506

507
void OpenGLWidget::drawDigitalIntervalSeries() {
69✔
508
    int r, g, b;
69✔
509
    auto const start_time = static_cast<float>(_xAxis.getStart());
69✔
510
    auto const end_time = static_cast<float>(_xAxis.getEnd());
69✔
511

512
    //auto const min_y = _yMin;
513
    //auto const max_y = _yMax;
514

515
    auto axesProgram = ShaderManager::instance().getProgram("axes");
207✔
516
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
69✔
517

518
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);// glBindVertexArray
69✔
519
    setupVertexAttribs();
69✔
520

521
    if (!_master_time_frame) {
69✔
522
        glUseProgram(0);
×
523
        return;
×
524
    }
525

526
    for (auto const & [key, interval_data]: _digital_interval_series) {
69✔
527
        auto const & series = interval_data.series;
×
528
        auto const & time_frame = interval_data.time_frame;
×
529
        auto const & display_options = interval_data.display_options;
×
530

531
        if (!display_options->is_visible) continue;
×
532

533
        // Get only the intervals that overlap with the visible range
534
        // These will be
535
        auto visible_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
536
                TimeFrameIndex(static_cast<int64_t>(start_time)),
537
                TimeFrameIndex(static_cast<int64_t>(end_time)),
538
                _master_time_frame.get(),
×
539
                time_frame.get());
×
540

541
        hexToRGB(display_options->hex_color, r, g, b);
×
542
        float const rNorm = static_cast<float>(r) / 255.0f;
×
543
        float const gNorm = static_cast<float>(g) / 255.0f;
×
544
        float const bNorm = static_cast<float>(b) / 255.0f;
×
545
        float const alpha = display_options->alpha;
×
546

547
        // === MVP MATRIX SETUP ===
548

549
        // We need to check if we have a PlottingManager reference
550
        if (!_plotting_manager) {
×
551
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
552
            continue;
×
553
        }
554

555
        // Calculate coordinate allocation from PlottingManager
556
        // Digital intervals typically use full canvas allocation
557
        float allocated_y_center, allocated_height;
×
558
        _plotting_manager->calculateDigitalIntervalSeriesAllocation(0, allocated_y_center, allocated_height);
×
559

560
        display_options->allocated_y_center = allocated_y_center;
×
561
        display_options->allocated_height = allocated_height;
×
562

563
        // Apply PlottingManager pan offset
564
        _plotting_manager->setPanOffset(_verticalPanOffset);
×
565

566
        auto Model = new_getIntervalModelMat(*display_options, *_plotting_manager);
×
567
        auto View = new_getIntervalViewMat(*_plotting_manager);
×
568
        auto Projection = new_getIntervalProjectionMat(start_time, end_time, _yMin, _yMax, *_plotting_manager);
×
569

570
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
571
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
572
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
573

574
        // Set color and alpha uniforms
575
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
576
        glUniform1f(m_alphaLoc, alpha);
×
577

578
        for (auto const & interval: visible_intervals) {
×
579

580
            std::cout << "interval.start:" << interval.start << "interval.end:" << interval.end << std::endl;
×
581

582
            auto start = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.start)));
×
583
            auto end = static_cast<float>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.end)));
×
584

585
            //Clip the interval to the visible range
586
            start = std::max(start, start_time);
×
587
            end = std::min(end, end_time);
×
588

589
            float const xStart = start;
×
590
            float const xEnd = end;
×
591

592
            // Use normalized coordinates for intervals
593
            // The Model matrix will handle positioning and scaling
594
            float const interval_y_min = -1.0f;// Bottom of interval in local coordinates
×
595
            float const interval_y_max = +1.0f;// Top of interval in local coordinates
×
596

597
            // Create 4D vertices (x, y, 0, 1) to match the shader expectations
598
            std::array<GLfloat, 16> vertices = {
×
599
                    xStart, interval_y_min, 0.0f, 1.0f,
600
                    xEnd, interval_y_min, 0.0f, 1.0f,
601
                    xEnd, interval_y_max, 0.0f, 1.0f,
602
                    xStart, interval_y_max, 0.0f, 1.0f};
×
603

604
            m_vbo.bind();
×
605
            m_vbo.allocate(vertices.data(), vertices.size() * sizeof(GLfloat));
×
606
            m_vbo.release();
×
607

608
            GLint const first = 0;  // Starting index of enabled array
×
609
            GLsizei const count = 4;// number of indexes to render
×
610
            glDrawArrays(GL_TRIANGLE_FAN, first, count);
×
611
        }
612

613
        // Draw highlighting for selected intervals
614
        auto selected_interval = getSelectedInterval(key);
×
615
        if (selected_interval.has_value() && !(_is_dragging_interval && _dragging_series_key == key)) {
×
616
            auto const [sel_start_time, sel_end_time] = selected_interval.value();
×
617

618
            // Check if the selected interval overlaps with visible range
619
            if (sel_end_time >= static_cast<int64_t>(start_time) && sel_start_time <= static_cast<int64_t>(end_time)) {
×
620
                // Clip the selected interval to the visible range
621
                float const highlighted_start = std::max(static_cast<float>(sel_start_time), start_time);
×
622
                float const highlighted_end = std::min(static_cast<float>(sel_end_time), end_time);
×
623

624
                // Draw a thick border around the selected interval
625
                // Use a brighter version of the same color for highlighting
626
                float const highlight_rNorm = std::min(1.0f, rNorm + 0.3f);
×
627
                float const highlight_gNorm = std::min(1.0f, gNorm + 0.3f);
×
628
                float const highlight_bNorm = std::min(1.0f, bNorm + 0.3f);
×
629

630
                // Set line width for highlighting
631
                glLineWidth(4.0f);
×
632

633
                // Draw the four border lines of the rectangle
634
                // Set highlight color uniforms
635
                glUniform3f(m_colorLoc, highlight_rNorm, highlight_gNorm, highlight_bNorm);
×
636
                glUniform1f(m_alphaLoc, 1.0f);
×
637

638
                // Bottom edge
639
                std::array<GLfloat, 8> bottom_edge = {
×
640
                        highlighted_start, -1.0f, 0.0f, 1.0f,
641
                        highlighted_end, -1.0f, 0.0f, 1.0f};
×
642
                m_vbo.bind();
×
643
                m_vbo.allocate(bottom_edge.data(), bottom_edge.size() * sizeof(GLfloat));
×
644
                m_vbo.release();
×
645
                glDrawArrays(GL_LINES, 0, 2);
×
646

647
                // Top edge
648
                std::array<GLfloat, 8> top_edge = {
×
649
                        highlighted_start, 1.0f, 0.0f, 1.0f,
650
                        highlighted_end, 1.0f, 0.0f, 1.0f};
×
651
                m_vbo.bind();
×
652
                m_vbo.allocate(top_edge.data(), top_edge.size() * sizeof(GLfloat));
×
653
                m_vbo.release();
×
654
                glDrawArrays(GL_LINES, 0, 2);
×
655

656
                // Left edge
657
                std::array<GLfloat, 8> left_edge = {
×
658
                        highlighted_start, -1.0f, 0.0f, 1.0f,
659
                        highlighted_start, 1.0f, 0.0f, 1.0f};
×
660
                m_vbo.bind();
×
661
                m_vbo.allocate(left_edge.data(), left_edge.size() * sizeof(GLfloat));
×
662
                m_vbo.release();
×
663
                glDrawArrays(GL_LINES, 0, 2);
×
664

665
                // Right edge
666
                std::array<GLfloat, 8> right_edge = {
×
667
                        highlighted_end, -1.0f, 0.0f, 1.0f,
668
                        highlighted_end, 1.0f, 0.0f, 1.0f};
×
669
                m_vbo.bind();
×
670
                m_vbo.allocate(right_edge.data(), right_edge.size() * sizeof(GLfloat));
×
671
                m_vbo.release();
×
672
                glDrawArrays(GL_LINES, 0, 2);
×
673

674
                // Reset line width
675
                glLineWidth(1.0f);
×
676
            }
677
        }
678
    }
679

680
    glUseProgram(0);
69✔
681
}
69✔
682

683
///////////////////////////////////////////////////////////////////////////////
684

685
void OpenGLWidget::drawAnalogSeries() {
69✔
686
    int r, g, b;
69✔
687

688
    auto const start_time = TimeFrameIndex(_xAxis.getStart());
69✔
689
    auto const end_time = TimeFrameIndex(_xAxis.getEnd());
69✔
690

691
    auto axesProgram = ShaderManager::instance().getProgram("axes");
207✔
692
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
69✔
693

694
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
69✔
695
    setupVertexAttribs();
69✔
696

697
    if (!_master_time_frame) {
69✔
698
        glUseProgram(0);
×
699
        return;
×
700
    }
701

702
    int i = 0;
69✔
703

704
    for (auto const & [key, analog_data]: _analog_series) {
128✔
705
        auto const & series = analog_data.series;
59✔
706
        auto const & data = series->getAnalogTimeSeries();
59✔
707
        //if (!series->hasTimeFrameV2()) {
708
        //    continue;
709
        //}
710
        //auto time_frame = series->getTimeFrameV2().value();
711
        auto time_frame = analog_data.time_frame;
59✔
712

713
        auto const & display_options = analog_data.display_options;
59✔
714

715
        if (!display_options->is_visible) continue;
59✔
716

717
        // Calculate coordinate allocation from PlottingManager
718
        // For now, we'll use the analog series index to allocate coordinates
719
        // This is a temporary bridge until we fully migrate series management to PlottingManager
720
        if (!_plotting_manager) {
59✔
721
            std::cerr << "Warning: PlottingManager not set in OpenGLWidget" << std::endl;
×
722
            continue;
×
723
        }
724

725
        // Compute global stacked allocation so analog shares space with stacked events
726
        float allocated_y_center = 0.0f;
59✔
727
        float allocated_height = 0.0f;
59✔
728

729
        // Determine analog index among visible analogs using current analog-only allocation
730
        int const analog_visible_count = static_cast<int>(_plotting_manager->getVisibleAnalogSeriesKeys().size());
59✔
731
        int analog_index = i;
59✔
732
        float tmp_center = 0.0f;
59✔
733
        float tmp_height = 0.0f;
59✔
734
        if (_plotting_manager->getAnalogSeriesAllocationForKey(key, tmp_center, tmp_height) && tmp_height > 0.0f) {
59✔
735
            float const idxf = (tmp_center - _plotting_manager->viewport_y_min) / tmp_height - 0.5f;
59✔
736
            analog_index = std::clamp(static_cast<int>(std::round(idxf)), 0, std::max(0, analog_visible_count - 1));
59✔
737
        }
738

739
        // Count stacked-mode events (exclude FullCanvas)
740
        int stacked_event_count = 0;
59✔
741
        for (auto const & [ekey, edata]: _digital_event_series) {
73✔
742
            if (edata.display_options->is_visible && edata.display_options->display_mode == EventDisplayMode::Stacked) {
14✔
743
                stacked_event_count++;
13✔
744
            }
745
        }
746
        int const total_stackable_series = analog_visible_count + stacked_event_count;
59✔
747

748
        if (total_stackable_series > 0) {
59✔
749
            _plotting_manager->calculateGlobalStackedAllocation(analog_index, -1, total_stackable_series,
59✔
750
                                                                allocated_y_center, allocated_height);
751
        } else {
752
            // Fallback to analog-only allocation
753
            if (!_plotting_manager->getAnalogSeriesAllocationForKey(key, allocated_y_center, allocated_height)) {
×
754
                _plotting_manager->calculateAnalogSeriesAllocation(i, allocated_y_center, allocated_height);
×
755
            }
756
        }
757

758
        display_options->allocated_y_center = allocated_y_center;
59✔
759
        display_options->allocated_height = allocated_height;
59✔
760

761
        // Set the color for the current series
762
        hexToRGB(display_options->hex_color, r, g, b);
59✔
763
        float const rNorm = static_cast<float>(r) / 255.0f;
59✔
764
        float const gNorm = static_cast<float>(g) / 255.0f;
59✔
765
        float const bNorm = static_cast<float>(b) / 255.0f;
59✔
766

767
        m_vertices.clear();
59✔
768

769
        auto series_start_index = getTimeIndexForSeries(start_time, _master_time_frame.get(), time_frame.get());
59✔
770
        auto series_end_index = getTimeIndexForSeries(end_time, _master_time_frame.get(), time_frame.get());
59✔
771

772
        // === MVP MATRIX SETUP ===
773

774
        // Apply PlottingManager pan offset
775
        _plotting_manager->setPanOffset(_verticalPanOffset);
59✔
776

777
        auto Model = new_getAnalogModelMat(*display_options, display_options->cached_std_dev, display_options->cached_mean, *_plotting_manager);
59✔
778
        auto View = new_getAnalogViewMat(*_plotting_manager);
59✔
779
        auto Projection = new_getAnalogProjectionMat(start_time, end_time, _yMin, _yMax, *_plotting_manager);
59✔
780

781
        glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
59✔
782
        glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
59✔
783
        glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
59✔
784

785
        // Set color and alpha uniforms
786
        glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
59✔
787
        glUniform1f(m_alphaLoc, 1.0f);
59✔
788

789
        auto analog_range = series->getTimeValueSpanInTimeFrameIndexRange(series_start_index, series_end_index);
59✔
790

791
        if (analog_range.values.empty()) {
59✔
792
            // Instead of returning early (which stops rendering ALL series),
793
            // continue to the next series. This allows other series to still be rendered
794
            // even if this particular series has no data in the current visible range.
795
            i++;
×
796
            continue;
×
797
        }
798

799
        if (display_options->gap_handling == AnalogGapHandling::AlwaysConnect) {
59✔
800

801
            auto time_begin = analog_range.time_indices.begin();
×
802

803
            for (size_t i = 0; i < analog_range.values.size(); i++) {
×
804
                auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
×
805
                //auto const xCanvasPos = point.time_frame_index.getValue();
806
                auto const yCanvasPos = analog_range.values[i];
×
807

808
                m_vertices.push_back(xCanvasPos);
×
809
                m_vertices.push_back(yCanvasPos);
×
810
                m_vertices.push_back(0.0f);// z coordinate
×
811
                m_vertices.push_back(1.0f);// w coordinate
×
812

813
                ++(*time_begin);
×
814
            }
815
            m_vbo.bind();
×
816
            m_vbo.allocate(m_vertices.data(), static_cast<int>(m_vertices.size() * sizeof(GLfloat)));
×
817
            m_vbo.release();
×
818

819
            // Set line thickness from display options
820
            glLineWidth(static_cast<float>(display_options->line_thickness));
×
821
            glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(m_vertices.size() / 4));
×
822

823
        } else if (display_options->gap_handling == AnalogGapHandling::DetectGaps) {
59✔
824
            // Draw multiple line segments, breaking at gaps
825
            // Set line thickness before drawing segments
826
            glLineWidth(static_cast<float>(display_options->line_thickness));
59✔
827
            _drawAnalogSeriesWithGapDetection(data, time_frame, analog_range,
59✔
828
                                              display_options->gap_threshold);
59✔
829

830
        } else if (display_options->gap_handling == AnalogGapHandling::ShowMarkers) {
×
831
            // Draw individual markers instead of lines
832
            _drawAnalogSeriesAsMarkers(data, time_frame, analog_range);
×
833
        }
834

835

836
        i++;
59✔
837
    }
59✔
838

839
    // Reset line width to default
840
    glLineWidth(1.0f);
69✔
841
    glUseProgram(0);
69✔
842
}
69✔
843

844
void OpenGLWidget::_drawAnalogSeriesWithGapDetection(std::vector<float> const & data,
59✔
845
                                                     std::shared_ptr<TimeFrame> const & time_frame,
846
                                                     AnalogTimeSeries::TimeValueSpanPair analog_range,
847
                                                     float gap_threshold) {
848

849
    std::vector<GLfloat> segment_vertices;
59✔
850
    auto prev_index = 0;
59✔
851

852
    auto time_begin = analog_range.time_indices.begin();
59✔
853

854
    for (size_t i = 0; i < analog_range.values.size(); i++) {
7,816✔
855
        auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
7,757✔
856
        auto const yCanvasPos = analog_range.values[i];
7,757✔
857

858
        // Check for gap if this isn't the first point
859
        if (prev_index != 0) {
7,757✔
860

861
            float const time_gap = (**time_begin).getValue() - prev_index;
7,639✔
862

863
            if (time_gap > gap_threshold) {
7,639✔
864
                // Draw current segment if it has points
865
                if (segment_vertices.size() >= 4) {// At least 2 points (2 floats each)
×
866
                    m_vbo.bind();
×
867
                    m_vbo.allocate(segment_vertices.data(), static_cast<int>(segment_vertices.size() * sizeof(GLfloat)));
×
868
                    m_vbo.release();
×
869
                    glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(segment_vertices.size() / 4));
×
870
                }
871

872
                // Start new segment
873
                segment_vertices.clear();
×
874
            }
875
        }
876

877
        // Add current point to segment (4D coordinates: x, y, 0, 1)
878
        segment_vertices.push_back(xCanvasPos);
7,757✔
879
        segment_vertices.push_back(yCanvasPos);
7,757✔
880
        segment_vertices.push_back(0.0f);// z coordinate
7,757✔
881
        segment_vertices.push_back(1.0f);// w coordinate
7,757✔
882

883
        prev_index = (**time_begin).getValue();
7,757✔
884
        ++(*time_begin);
7,757✔
885
    }
886

887
    // Draw final segment
888
    if (segment_vertices.size() >= 4) {// At least 1 point (4 floats)
59✔
889
        m_vbo.bind();
59✔
890
        m_vbo.allocate(segment_vertices.data(), static_cast<int>(segment_vertices.size() * sizeof(GLfloat)));
59✔
891
        m_vbo.release();
59✔
892

893
        if (segment_vertices.size() >= 8) {// At least 2 points (8 floats)
59✔
894
            glDrawArrays(GL_LINE_STRIP, 0, static_cast<int>(segment_vertices.size() / 4));
59✔
895
        } else {
896
            // Single point - draw as a small marker
897
            glDrawArrays(GL_POINTS, 0, static_cast<int>(segment_vertices.size() / 4));
×
898
        }
899
    }
900
}
118✔
901

902
void OpenGLWidget::_drawAnalogSeriesAsMarkers(std::vector<float> const & data,
×
903
                                              std::shared_ptr<TimeFrame> const & time_frame,
904
                                              AnalogTimeSeries::TimeValueSpanPair analog_range) {
905
    m_vertices.clear();
×
906

907
    auto time_begin = analog_range.time_indices.begin();
×
908

909
    for (size_t i = 0; i < analog_range.values.size(); i++) {
×
910

911
        auto const xCanvasPos = time_frame->getTimeAtIndex(**time_begin);
×
912
        auto const yCanvasPos = analog_range.values[i];
×
913

914
        m_vertices.push_back(xCanvasPos);
×
915
        m_vertices.push_back(yCanvasPos);
×
916
        m_vertices.push_back(0.0f);// z coordinate
×
917
        m_vertices.push_back(1.0f);// w coordinate
×
918

919
        ++(*time_begin);
×
920
    }
921

922
    if (!m_vertices.empty()) {
×
923
        m_vbo.bind();
×
924
        m_vbo.allocate(m_vertices.data(), static_cast<int>(m_vertices.size() * sizeof(GLfloat)));
×
925
        m_vbo.release();
×
926

927
        // Set point size for better visibility
928
        //glPointSize(3.0f);
929
        glDrawArrays(GL_POINTS, 0, static_cast<int>(m_vertices.size() / 4));
×
930
        //glPointSize(1.0f); // Reset to default
931
    }
932
}
×
933

934
///////////////////////////////////////////////////////////////////////////////
935

936
void OpenGLWidget::paintGL() {
69✔
937
    int r, g, b;
69✔
938
    hexToRGB(m_background_color, r, g, b);
69✔
939
    glClearColor(
69✔
940
            static_cast<float>(r) / 255.0f,
69✔
941
            static_cast<float>(g) / 255.0f,
69✔
942
            static_cast<float>(b) / 255.0f, 1.0f);
69✔
943
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
69✔
944

945
    //This has been converted to master coordinates
946
    int const currentTime = _time;
69✔
947

948
    int64_t const zoom = _xAxis.getEnd() - _xAxis.getStart();
69✔
949
    _xAxis.setCenterAndZoom(currentTime, zoom);
69✔
950

951
    // Update Y boundaries based on pan and zoom
952
    _updateYViewBoundaries();
69✔
953

954
    // Draw the series
955
    drawDigitalEventSeries();
69✔
956
    drawDigitalIntervalSeries();
69✔
957
    drawAnalogSeries();
69✔
958

959
    drawAxis();
69✔
960

961
    drawGridLines();
69✔
962

963
    drawDraggedInterval();
69✔
964
    drawNewIntervalBeingCreated();
69✔
965
}
69✔
966

967
void OpenGLWidget::resizeGL(int w, int h) {
18✔
968

969
    // Set the viewport to match the widget dimensions
970
    // This is crucial for proper scaling - it tells OpenGL the actual pixel dimensions
971
    glViewport(0, 0, w, h);
18✔
972

973
    // Store the new dimensions
974
    // Note: width() and height() will return the new values after this call
975

976
    // For 2D plotting, we should use orthographic projection
977
    m_proj.setToIdentity();
18✔
978
    m_proj.ortho(-1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f);// Use orthographic projection for 2D plotting
18✔
979

980
    m_view.setToIdentity();
18✔
981
    m_view.translate(0, 0, -1);// Move slightly back for orthographic view
18✔
982

983
    // Trigger a repaint with the new dimensions
984
    update();
18✔
985
}
18✔
986

987
void OpenGLWidget::drawAxis() {
69✔
988

989
    auto axesProgram = ShaderManager::instance().getProgram("axes");
207✔
990
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
69✔
991

992
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, m_proj.constData());
69✔
993
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, m_view.constData());
69✔
994
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, m_model.constData());
69✔
995

996
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
69✔
997
    setupVertexAttribs();
69✔
998

999
    // Get axis color from theme and set uniforms
1000
    float r, g, b;
69✔
1001
    hexToRGB(m_axis_color, r, g, b);
69✔
1002
    glUniform3f(m_colorLoc, r, g, b);
69✔
1003
    glUniform1f(m_alphaLoc, 1.0f);
69✔
1004

1005
    // Draw horizontal line at x=0 with 4D coordinates (x, y, 0, 1)
1006
    std::array<GLfloat, 8> lineVertices = {
69✔
1007
            0.0f, _yMin, 0.0f, 1.0f,
69✔
1008
            0.0f, _yMax, 0.0f, 1.0f};
69✔
1009

1010
    m_vbo.bind();
69✔
1011
    m_vbo.allocate(lineVertices.data(), lineVertices.size() * sizeof(GLfloat));
207✔
1012
    m_vbo.release();
69✔
1013
    glDrawArrays(GL_LINES, 0, 2);
69✔
1014
    glUseProgram(0);
69✔
1015
}
138✔
1016

1017
void OpenGLWidget::addAnalogTimeSeries(
22✔
1018
        std::string const & key,
1019
        std::shared_ptr<AnalogTimeSeries> series,
1020
        std::shared_ptr<TimeFrame> time_frame,
1021
        std::string const & color) {
1022

1023
    auto display_options = std::make_unique<NewAnalogTimeSeriesDisplayOptions>();
22✔
1024

1025
    // Set color
1026
    display_options->hex_color = color.empty() ? TimeSeriesDefaultValues::getColorForIndex(_analog_series.size()) : color;
22✔
1027
    display_options->is_visible = true;
22✔
1028

1029
    // Calculate scale factor based on standard deviation
1030
    auto start_time = std::chrono::high_resolution_clock::now();
22✔
1031
    setAnalogIntrinsicProperties(series.get(), *display_options);
22✔
1032
    auto end_time = std::chrono::high_resolution_clock::now();
22✔
1033
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
22✔
1034
    std::cout << "Standard deviation calculation took " << duration.count() << " milliseconds" << std::endl;
22✔
1035
    display_options->scale_factor = display_options->cached_std_dev * 5.0f;
22✔
1036
    display_options->user_scale_factor = 1.0f;// Default user scale
22✔
1037

1038
    if (time_frame->getTotalFrameCount() / 5 > series->getNumSamples()) {
22✔
1039
        display_options->gap_handling = AnalogGapHandling::AlwaysConnect;
×
UNCOV
1040
        display_options->enable_gap_detection = false;
×
1041

1042
    } else {
1043
        display_options->enable_gap_detection = true;
22✔
1044
        display_options->gap_handling = AnalogGapHandling::DetectGaps;
22✔
1045
        display_options->gap_threshold = time_frame->getTotalFrameCount() / 1000;
22✔
1046
    }
1047

1048
    _analog_series[key] = AnalogSeriesData{
110✔
1049
            std::move(series),
22✔
1050
            std::move(time_frame),
22✔
1051
            std::move(display_options)};
44✔
1052

1053
    updateCanvas(_time);
22✔
1054
}
44✔
1055

1056
void OpenGLWidget::removeAnalogTimeSeries(std::string const & key) {
×
1057
    auto item = _analog_series.find(key);
×
1058
    if (item != _analog_series.end()) {
×
UNCOV
1059
        _analog_series.erase(item);
×
1060
    }
1061
    updateCanvas(_time);
×
UNCOV
1062
}
×
1063

1064
void OpenGLWidget::addDigitalEventSeries(
11✔
1065
        std::string const & key,
1066
        std::shared_ptr<DigitalEventSeries> series,
1067
        std::shared_ptr<TimeFrame> time_frame,
1068
        std::string const & color) {
1069

1070
    auto display_options = std::make_unique<NewDigitalEventSeriesDisplayOptions>();
11✔
1071

1072
    // Set color
1073
    display_options->hex_color = color.empty() ? TimeSeriesDefaultValues::getColorForIndex(_digital_event_series.size()) : color;
11✔
1074
    display_options->is_visible = true;
11✔
1075

1076
    _digital_event_series[key] = DigitalEventSeriesData{
55✔
1077
            std::move(series),
11✔
1078
            std::move(time_frame),
11✔
1079
            std::move(display_options)};
22✔
1080

1081
    updateCanvas(_time);
11✔
1082
}
22✔
1083

1084
void OpenGLWidget::removeDigitalEventSeries(std::string const & key) {
×
1085
    auto item = _digital_event_series.find(key);
×
1086
    if (item != _digital_event_series.end()) {
×
UNCOV
1087
        _digital_event_series.erase(item);
×
1088
    }
1089
    updateCanvas(_time);
×
UNCOV
1090
}
×
1091

UNCOV
1092
void OpenGLWidget::addDigitalIntervalSeries(
×
1093
        std::string const & key,
1094
        std::shared_ptr<DigitalIntervalSeries> series,
1095
        std::shared_ptr<TimeFrame> time_frame,
1096
        std::string const & color) {
1097

UNCOV
1098
    auto display_options = std::make_unique<NewDigitalIntervalSeriesDisplayOptions>();
×
1099

1100
    // Set color
1101
    display_options->hex_color = color.empty() ? TimeSeriesDefaultValues::getColorForIndex(_digital_interval_series.size()) : color;
×
UNCOV
1102
    display_options->is_visible = true;
×
1103

1104
    _digital_interval_series[key] = DigitalIntervalSeriesData{
×
1105
            std::move(series),
×
1106
            std::move(time_frame),
×
UNCOV
1107
            std::move(display_options)};
×
1108

1109
    updateCanvas(_time);
×
UNCOV
1110
}
×
1111

1112
void OpenGLWidget::removeDigitalIntervalSeries(std::string const & key) {
×
1113
    auto item = _digital_interval_series.find(key);
×
1114
    if (item != _digital_interval_series.end()) {
×
UNCOV
1115
        _digital_interval_series.erase(item);
×
1116
    }
1117
    updateCanvas(_time);
×
UNCOV
1118
}
×
1119

UNCOV
1120
void OpenGLWidget::_addSeries(std::string const & key) {
×
1121
    static_cast<void>(key);
1122
    //auto item = _series_y_position.find(key);
UNCOV
1123
}
×
1124

1125
void OpenGLWidget::_removeSeries(std::string const & key) {
×
1126
    auto item = _series_y_position.find(key);
×
1127
    if (item != _series_y_position.end()) {
×
UNCOV
1128
        _series_y_position.erase(item);
×
1129
    }
UNCOV
1130
}
×
1131

1132
void OpenGLWidget::clearSeries() {
×
1133
    _analog_series.clear();
×
1134
    updateCanvas(_time);
×
UNCOV
1135
}
×
1136

UNCOV
1137
void OpenGLWidget::drawDashedLine(LineParameters const & params) {
×
1138

1139
    auto dashedProgram = ShaderManager::instance().getProgram("dashed_line");
×
UNCOV
1140
    if (dashedProgram) glUseProgram(dashedProgram->getProgramId());
×
1141

1142
    glUniformMatrix4fv(m_dashedProjMatrixLoc, 1, GL_FALSE, (m_proj * m_view * m_model).constData());
×
1143
    std::array<GLfloat, 2> hw = {static_cast<float>(width()), static_cast<float>(height())};
×
1144
    glUniform2fv(m_dashedResolutionLoc, 1, hw.data());
×
1145
    glUniform1f(m_dashedDashSizeLoc, params.dashLength);
×
UNCOV
1146
    glUniform1f(m_dashedGapSizeLoc, params.gapLength);
×
1147

UNCOV
1148
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
1149

1150
    // Pass 3D coordinates (z=0) to match the vertex shader input format
1151
    std::array<float, 6> vertices = {
×
1152
            params.xStart, params.yStart, 0.0f,
×
UNCOV
1153
            params.xEnd, params.yEnd, 0.0f};
×
1154

1155
    m_vbo.bind();
×
UNCOV
1156
    m_vbo.allocate(vertices.data(), vertices.size() * sizeof(float));
×
1157

1158
    glEnableVertexAttribArray(0);
×
UNCOV
1159
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), nullptr);
×
1160

UNCOV
1161
    glDrawArrays(GL_LINES, 0, 2);
×
1162

UNCOV
1163
    m_vbo.release();
×
1164

1165
    glUseProgram(0);
×
UNCOV
1166
}
×
1167

1168
void OpenGLWidget::drawGridLines() {
69✔
1169
    if (!_grid_lines_enabled) {
69✔
1170
        return;// Grid lines are disabled
69✔
1171
    }
1172

1173
    auto const start_time = _xAxis.getStart();
×
UNCOV
1174
    auto const end_time = _xAxis.getEnd();
×
1175

1176
    // Calculate the range of time values
UNCOV
1177
    auto const time_range = end_time - start_time;
×
1178

1179
    // Avoid drawing grid lines if the range is too small or invalid
1180
    if (time_range <= 0 || _grid_spacing <= 0) {
×
UNCOV
1181
        return;
×
1182
    }
1183

1184
    // Find the first grid line position that's >= start_time
1185
    // Use integer division to ensure proper alignment
1186
    int64_t first_grid_time = ((start_time / _grid_spacing) * _grid_spacing);
×
1187
    if (first_grid_time < start_time) {
×
UNCOV
1188
        first_grid_time += _grid_spacing;
×
1189
    }
1190

1191
    // Draw vertical grid lines at regular intervals
UNCOV
1192
    for (int64_t grid_time = first_grid_time; grid_time <= end_time; grid_time += _grid_spacing) {
×
1193
        // Convert time coordinate to normalized device coordinate (-1 to 1)
UNCOV
1194
        float const normalized_x = 2.0f * static_cast<float>(grid_time - start_time) / static_cast<float>(time_range) - 1.0f;
×
1195

1196
        // Skip grid lines that are outside the visible range due to floating point precision
1197
        if (normalized_x < -1.0f || normalized_x > 1.0f) {
×
UNCOV
1198
            continue;
×
1199
        }
1200

1201
        LineParameters gridLine;
×
1202
        gridLine.xStart = normalized_x;
×
1203
        gridLine.xEnd = normalized_x;
×
1204
        gridLine.yStart = _yMin;
×
1205
        gridLine.yEnd = _yMax;
×
1206
        gridLine.dashLength = 3.0f;// Shorter dashes for grid lines
×
UNCOV
1207
        gridLine.gapLength = 3.0f; // Shorter gaps for grid lines
×
1208

UNCOV
1209
        drawDashedLine(gridLine);
×
1210
    }
1211
}
1212

1213
void OpenGLWidget::_updateYViewBoundaries() {
69✔
1214
    /*
1215
    float viewHeight = 2.0f;
1216

1217
    // Calculate center point (adjusted by vertical pan)
1218
    float centerY = _verticalPanOffset;
1219
    
1220
    // Calculate min and max values
1221
    _yMin = centerY - (viewHeight / 2.0f);
1222
    _yMax = centerY + (viewHeight / 2.0f);
1223
     */
1224
}
69✔
1225

UNCOV
1226
float OpenGLWidget::canvasXToTime(float canvas_x) const {
×
1227
    // Convert canvas pixel coordinate to time coordinate
1228
    float const canvas_width = static_cast<float>(width());
×
UNCOV
1229
    float const normalized_x = canvas_x / canvas_width;// 0.0 to 1.0
×
1230

1231
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
UNCOV
1232
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
1233

UNCOV
1234
    return start_time + normalized_x * (end_time - start_time);
×
1235
}
1236

UNCOV
1237
float OpenGLWidget::canvasYToAnalogValue(float canvas_y, std::string const & series_key) const {
×
1238
    // Get canvas dimensions
UNCOV
1239
    auto [canvas_width, canvas_height] = getCanvasSize();
×
1240

1241
    // Convert canvas Y to normalized coordinates [0, 1] where 0 is bottom, 1 is top
UNCOV
1242
    float const normalized_y = 1.0f - (canvas_y / static_cast<float>(canvas_height));
×
1243

1244
    // Convert to view coordinates using current viewport bounds (accounting for pan offset)
1245
    float const dynamic_min_y = _yMin + _verticalPanOffset;
×
1246
    float const dynamic_max_y = _yMax + _verticalPanOffset;
×
UNCOV
1247
    float const view_y = dynamic_min_y + normalized_y * (dynamic_max_y - dynamic_min_y);
×
1248

1249
    // Find the series configuration to get positioning info
1250
    auto const analog_it = _analog_series.find(series_key);
×
1251
    if (analog_it == _analog_series.end()) {
×
UNCOV
1252
        return 0.0f;// Series not found
×
1253
    }
1254

UNCOV
1255
    auto const & display_options = analog_it->second.display_options;
×
1256

1257
    // Check if this series uses VerticalSpaceManager positioning
UNCOV
1258
    if (display_options->y_offset != 0.0f && display_options->allocated_height > 0.0f) {
×
1259
        // VerticalSpaceManager mode: series is positioned at y_offset with its own scaling
UNCOV
1260
        float const series_local_y = view_y - display_options->y_offset;
×
1261

1262
        // Apply inverse of the scaling used in rendering to get actual analog value
1263
        auto const series = analog_it->second.series;
×
1264
        auto const stdDev = getCachedStdDev(*series, *display_options);
×
UNCOV
1265
        auto const user_scale_combined = display_options->user_scale_factor * _global_zoom;
×
1266

1267
        // Calculate the same scaling factors used in rendering
1268
        float const usable_height = display_options->allocated_height * 0.8f;
×
1269
        float const base_amplitude_scale = 1.0f / stdDev;
×
1270
        float const height_scale = usable_height / 1.0f;
×
UNCOV
1271
        float const amplitude_scale = base_amplitude_scale * height_scale * user_scale_combined;
×
1272

1273
        // Apply inverse scaling to get actual data value
1274
        return series_local_y / amplitude_scale;
×
UNCOV
1275
    } else {
×
1276
        // Legacy mode: use corrected calculation
UNCOV
1277
        float const adjusted_y = view_y;// No pan offset adjustment needed since it's in projection
×
1278

1279
        // Calculate series center and scaling for legacy mode
1280
        auto series_pos_it = _series_y_position.find(series_key);
×
UNCOV
1281
        if (series_pos_it == _series_y_position.end()) {
×
1282
            // Series not found in position map, return 0 as fallback
UNCOV
1283
            return 0.0f;
×
1284
        }
1285
        int const series_index = series_pos_it->second;
×
1286
        float const center_coord = -0.5f * _ySpacing * (static_cast<float>(_analog_series.size() - 1));
×
UNCOV
1287
        float const series_y_offset = static_cast<float>(series_index) * _ySpacing + center_coord;
×
1288

UNCOV
1289
        float const relative_y = adjusted_y - series_y_offset;
×
1290

1291
        // Use corrected scaling calculation
1292
        auto const series = analog_it->second.series;
×
1293
        auto const stdDev = getCachedStdDev(*series, *display_options);
×
1294
        auto const user_scale_combined = display_options->user_scale_factor * _global_zoom;
×
UNCOV
1295
        float const legacy_amplitude_scale = (1.0f / stdDev) * user_scale_combined;
×
1296

1297
        return relative_y / legacy_amplitude_scale;
×
UNCOV
1298
    }
×
1299
}
1300

1301
// Interval selection methods
1302
void OpenGLWidget::setSelectedInterval(std::string const & series_key, int64_t start_time, int64_t end_time) {
×
1303
    _selected_intervals[series_key] = std::make_pair(start_time, end_time);
×
1304
    updateCanvas(_time);
×
UNCOV
1305
}
×
1306

1307
void OpenGLWidget::clearSelectedInterval(std::string const & series_key) {
×
1308
    auto it = _selected_intervals.find(series_key);
×
1309
    if (it != _selected_intervals.end()) {
×
1310
        _selected_intervals.erase(it);
×
UNCOV
1311
        updateCanvas(_time);
×
1312
    }
UNCOV
1313
}
×
1314

1315
std::optional<std::pair<int64_t, int64_t>> OpenGLWidget::getSelectedInterval(std::string const & series_key) const {
×
1316
    auto it = _selected_intervals.find(series_key);
×
1317
    if (it != _selected_intervals.end()) {
×
UNCOV
1318
        return it->second;
×
1319
    }
UNCOV
1320
    return std::nullopt;
×
1321
}
1322

1323
std::optional<std::pair<int64_t, int64_t>> OpenGLWidget::findIntervalAtTime(std::string const & series_key, float time_coord) const {
×
1324
    auto it = _digital_interval_series.find(series_key);
×
1325
    if (it == _digital_interval_series.end()) {
×
UNCOV
1326
        return std::nullopt;
×
1327
    }
1328

1329
    auto const & interval_data = it->second;
×
1330
    auto const & series = interval_data.series;
×
UNCOV
1331
    auto const & time_frame = interval_data.time_frame;
×
1332

1333
    // Convert time coordinate from master time frame to series time frame
1334
    int64_t query_time_index;
UNCOV
1335
    if (time_frame.get() == _master_time_frame.get()) {
×
1336
        // Same time frame - use time coordinate directly
UNCOV
1337
        query_time_index = static_cast<int64_t>(std::round(time_coord));
×
1338
    } else {
1339
        // Different time frame - convert master time to series time frame index
UNCOV
1340
        query_time_index = time_frame->getIndexAtTime(time_coord).getValue();
×
1341
    }
1342

1343
    // Find all intervals that contain this time point in the series' time frame
UNCOV
1344
    auto intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(query_time_index, query_time_index);
×
1345

UNCOV
1346
    if (!intervals.empty()) {
×
1347
        // Return the first interval found, converted to master time frame coordinates
1348
        auto const & interval = intervals.front();
×
UNCOV
1349
        int64_t interval_start_master, interval_end_master;
×
1350

UNCOV
1351
        if (time_frame.get() == _master_time_frame.get()) {
×
1352
            // Same time frame - use indices directly as time coordinates
1353
            interval_start_master = interval.start;
×
UNCOV
1354
            interval_end_master = interval.end;
×
1355
        } else {
1356
            // Convert series indices to master time frame coordinates
1357
            interval_start_master = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.start)));
×
UNCOV
1358
            interval_end_master = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(interval.end)));
×
1359
        }
1360

UNCOV
1361
        return std::make_pair(interval_start_master, interval_end_master);
×
1362
    }
1363

UNCOV
1364
    return std::nullopt;
×
1365
}
1366

1367
// Interval edge dragging methods
UNCOV
1368
std::optional<std::pair<std::string, bool>> OpenGLWidget::findIntervalEdgeAtPosition(float canvas_x, float canvas_y) const {
×
1369

1370
    static_cast<void>(canvas_y);
1371

1372
    float const time_coord = canvasXToTime(canvas_x);
×
UNCOV
1373
    constexpr float EDGE_TOLERANCE_PX = 10.0f;
×
1374

1375
    // Only check selected intervals
1376
    for (auto const & [series_key, interval_bounds]: _selected_intervals) {
×
UNCOV
1377
        auto const [start_time, end_time] = interval_bounds;
×
1378

1379
        // Convert interval bounds to canvas coordinates
1380
        auto const start_time_f = static_cast<float>(start_time);
×
UNCOV
1381
        auto const end_time_f = static_cast<float>(end_time);
×
1382

1383
        // Check if we're within the interval's time range (with some tolerance)
UNCOV
1384
        if (time_coord >= start_time_f - EDGE_TOLERANCE_PX && time_coord <= end_time_f + EDGE_TOLERANCE_PX) {
×
1385
            // Convert time coordinates to canvas X positions for pixel-based tolerance
1386
            float const canvas_width = static_cast<float>(width());
×
1387
            auto const start_time_canvas = static_cast<float>(_xAxis.getStart());
×
UNCOV
1388
            auto const end_time_canvas = static_cast<float>(_xAxis.getEnd());
×
1389

1390
            float const start_canvas_x = (start_time_f - start_time_canvas) / (end_time_canvas - start_time_canvas) * canvas_width;
×
UNCOV
1391
            float const end_canvas_x = (end_time_f - start_time_canvas) / (end_time_canvas - start_time_canvas) * canvas_width;
×
1392

1393
            // Check if we're close to the left edge
1394
            if (std::abs(canvas_x - start_canvas_x) <= EDGE_TOLERANCE_PX) {
×
UNCOV
1395
                return std::make_pair(series_key, true);// true = left edge
×
1396
            }
1397

1398
            // Check if we're close to the right edge
1399
            if (std::abs(canvas_x - end_canvas_x) <= EDGE_TOLERANCE_PX) {
×
UNCOV
1400
                return std::make_pair(series_key, false);// false = right edge
×
1401
            }
1402
        }
1403
    }
1404

UNCOV
1405
    return std::nullopt;
×
1406
}
1407

1408
void OpenGLWidget::startIntervalDrag(std::string const & series_key, bool is_left_edge, QPoint const & start_pos) {
×
1409
    auto selected_interval = getSelectedInterval(series_key);
×
1410
    if (!selected_interval.has_value()) {
×
UNCOV
1411
        return;
×
1412
    }
1413

UNCOV
1414
    auto const [start_time, end_time] = selected_interval.value();
×
1415

1416
    _is_dragging_interval = true;
×
1417
    _dragging_series_key = series_key;
×
1418
    _dragging_left_edge = is_left_edge;
×
1419
    _original_start_time = start_time;
×
1420
    _original_end_time = end_time;
×
1421
    _dragged_start_time = start_time;
×
1422
    _dragged_end_time = end_time;
×
UNCOV
1423
    _drag_start_pos = start_pos;
×
1424

1425
    // Disable normal mouse interactions during drag
UNCOV
1426
    setCursor(Qt::SizeHorCursor);
×
1427

1428
    std::cout << "Started dragging " << (is_left_edge ? "left" : "right")
UNCOV
1429
              << " edge of interval [" << start_time << ", " << end_time << "]" << std::endl;
×
1430
}
1431

1432
void OpenGLWidget::updateIntervalDrag(QPoint const & current_pos) {
×
1433
    if (!_is_dragging_interval) {
×
UNCOV
1434
        return;
×
1435
    }
1436

1437
    // Convert mouse position to time coordinate (in master time frame)
UNCOV
1438
    float const current_time_master = canvasXToTime(static_cast<float>(current_pos.x()));
×
1439

1440
    // Get the series data
1441
    auto it = _digital_interval_series.find(_dragging_series_key);
×
UNCOV
1442
    if (it == _digital_interval_series.end()) {
×
1443
        // Series not found - abort drag
1444
        cancelIntervalDrag();
×
UNCOV
1445
        return;
×
1446
    }
1447

1448
    auto const & series = it->second.series;
×
UNCOV
1449
    auto const & time_frame = it->second.time_frame;
×
1450

1451
    // Convert master time coordinate to series time frame index
1452
    int64_t current_time_series_index;
×
UNCOV
1453
    if (time_frame.get() == _master_time_frame.get()) {
×
1454
        // Same time frame - use time coordinate directly
UNCOV
1455
        current_time_series_index = static_cast<int64_t>(std::round(current_time_master));
×
1456
    } else {
1457
        // Different time frame - convert master time to series time frame index
UNCOV
1458
        current_time_series_index = time_frame->getIndexAtTime(current_time_master).getValue();
×
1459
    }
1460

1461
    // Convert original interval bounds to series time frame for constraints
1462
    int64_t original_start_series, original_end_series;
UNCOV
1463
    if (time_frame.get() == _master_time_frame.get()) {
×
1464
        // Same time frame
1465
        original_start_series = _original_start_time;
×
UNCOV
1466
        original_end_series = _original_end_time;
×
1467
    } else {
1468
        // Convert master time coordinates to series time frame indices
1469
        original_start_series = time_frame->getIndexAtTime(static_cast<float>(_original_start_time)).getValue();
×
UNCOV
1470
        original_end_series = time_frame->getIndexAtTime(static_cast<float>(_original_end_time)).getValue();
×
1471
    }
1472

1473
    // Perform dragging logic in series time frame
1474
    int64_t new_start_series, new_end_series;
1475

UNCOV
1476
    if (_dragging_left_edge) {
×
1477
        // Dragging left edge - constrain to not pass right edge
1478
        new_start_series = std::min(current_time_series_index, original_end_series - 1);
×
UNCOV
1479
        new_end_series = original_end_series;
×
1480

1481
        // Check for collision with other intervals in series time frame
1482
        auto overlapping_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
UNCOV
1483
                new_start_series, new_start_series);
×
1484

UNCOV
1485
        for (auto const & interval: overlapping_intervals) {
×
1486
            // Skip the interval we're currently editing
1487
            if (interval.start == original_start_series && interval.end == original_end_series) {
×
UNCOV
1488
                continue;
×
1489
            }
1490

1491
            // If we would overlap with another interval, stop 1 index after it
1492
            if (new_start_series <= interval.end) {
×
1493
                new_start_series = interval.end + 1;
×
UNCOV
1494
                break;
×
1495
            }
1496
        }
1497
    } else {
1498
        // Dragging right edge - constrain to not pass left edge
1499
        new_start_series = original_start_series;
×
UNCOV
1500
        new_end_series = std::max(current_time_series_index, original_start_series + 1);
×
1501

1502
        // Check for collision with other intervals in series time frame
1503
        auto overlapping_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
UNCOV
1504
                new_end_series, new_end_series);
×
1505

UNCOV
1506
        for (auto const & interval: overlapping_intervals) {
×
1507
            // Skip the interval we're currently editing
1508
            if (interval.start == original_start_series && interval.end == original_end_series) {
×
UNCOV
1509
                continue;
×
1510
            }
1511

1512
            // If we would overlap with another interval, stop 1 index before it
1513
            if (new_end_series >= interval.start) {
×
1514
                new_end_series = interval.start - 1;
×
UNCOV
1515
                break;
×
1516
            }
1517
        }
1518
    }
1519

1520
    // Validate the new interval bounds
UNCOV
1521
    if (new_start_series >= new_end_series) {
×
1522
        // Invalid bounds - keep current drag state
UNCOV
1523
        return;
×
1524
    }
1525

1526
    // Convert back to master time frame for display
UNCOV
1527
    if (time_frame.get() == _master_time_frame.get()) {
×
1528
        // Same time frame
1529
        _dragged_start_time = new_start_series;
×
UNCOV
1530
        _dragged_end_time = new_end_series;
×
1531
    } else {
1532
        // Convert series indices back to master time coordinates
1533
        try {
1534
            _dragged_start_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_start_series)));
×
1535
            _dragged_end_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_end_series)));
×
UNCOV
1536
        } catch (...) {
×
1537
            // Conversion failed - abort drag
1538
            cancelIntervalDrag();
×
1539
            return;
×
UNCOV
1540
        }
×
1541
    }
1542

1543
    // Trigger redraw to show the dragged interval
UNCOV
1544
    updateCanvas(_time);
×
1545
}
1546

1547
void OpenGLWidget::finishIntervalDrag() {
×
1548
    if (!_is_dragging_interval) {
×
UNCOV
1549
        return;
×
1550
    }
1551

1552
    // Get the series data
1553
    auto it = _digital_interval_series.find(_dragging_series_key);
×
UNCOV
1554
    if (it == _digital_interval_series.end()) {
×
1555
        // Series not found - abort drag
1556
        cancelIntervalDrag();
×
UNCOV
1557
        return;
×
1558
    }
1559

1560
    auto const & series = it->second.series;
×
UNCOV
1561
    auto const & time_frame = it->second.time_frame;
×
1562

1563
    try {
1564
        // Convert all coordinates to series time frame for data operations
1565
        int64_t original_start_series, original_end_series, new_start_series, new_end_series;
1566

UNCOV
1567
        if (time_frame.get() == _master_time_frame.get()) {
×
1568
            // Same time frame - use coordinates directly
1569
            original_start_series = _original_start_time;
×
1570
            original_end_series = _original_end_time;
×
1571
            new_start_series = _dragged_start_time;
×
UNCOV
1572
            new_end_series = _dragged_end_time;
×
1573
        } else {
1574
            // Convert master time coordinates to series time frame indices
1575
            original_start_series = time_frame->getIndexAtTime(static_cast<float>(_original_start_time)).getValue();
×
1576
            original_end_series = time_frame->getIndexAtTime(static_cast<float>(_original_end_time)).getValue();
×
1577
            new_start_series = time_frame->getIndexAtTime(static_cast<float>(_dragged_start_time)).getValue();
×
UNCOV
1578
            new_end_series = time_frame->getIndexAtTime(static_cast<float>(_dragged_end_time)).getValue();
×
1579
        }
1580

1581
        // Validate converted coordinates
1582
        if (new_start_series >= new_end_series ||
×
1583
            new_start_series < 0 || new_end_series < 0) {
×
UNCOV
1584
            throw std::runtime_error("Invalid interval bounds after conversion");
×
1585
        }
1586

1587
        // Update the interval data in the series' native time frame
1588
        // First, remove the original interval completely
1589
        for (int64_t time = original_start_series; time <= original_end_series; ++time) {
×
UNCOV
1590
            series->setEventAtTime(TimeFrameIndex(time), false);
×
1591
        }
1592

1593
        // Add the new interval
UNCOV
1594
        series->addEvent(TimeFrameIndex(new_start_series), TimeFrameIndex(new_end_series));
×
1595

1596
        // Update the selection to the new interval (stored in master time frame coordinates)
UNCOV
1597
        setSelectedInterval(_dragging_series_key, _dragged_start_time, _dragged_end_time);
×
1598

1599
        std::cout << "Finished dragging interval. Original: ["
×
1600
                  << _original_start_time << ", " << _original_end_time
×
UNCOV
1601
                  << "] -> New: [" << _dragged_start_time << ", " << _dragged_end_time << "]" << std::endl;
×
1602

UNCOV
1603
    } catch (...) {
×
1604
        // Error occurred during conversion or data update - abort drag
1605
        std::cout << "Error during interval drag completion - keeping original interval" << std::endl;
×
1606
        cancelIntervalDrag();
×
1607
        return;
×
UNCOV
1608
    }
×
1609

1610
    // Reset drag state
1611
    _is_dragging_interval = false;
×
1612
    _dragging_series_key.clear();
×
UNCOV
1613
    setCursor(Qt::ArrowCursor);
×
1614

1615
    // Trigger final redraw
UNCOV
1616
    updateCanvas(_time);
×
1617
}
1618

1619
void OpenGLWidget::cancelIntervalDrag() {
×
1620
    if (!_is_dragging_interval) {
×
UNCOV
1621
        return;
×
1622
    }
1623

UNCOV
1624
    std::cout << "Cancelled interval drag" << std::endl;
×
1625

1626
    // Reset drag state without applying changes
1627
    _is_dragging_interval = false;
×
1628
    _dragging_series_key.clear();
×
UNCOV
1629
    setCursor(Qt::ArrowCursor);
×
1630

1631
    // Trigger redraw to remove the dragged interval visualization
UNCOV
1632
    updateCanvas(_time);
×
1633
}
1634

1635
void OpenGLWidget::drawDraggedInterval() {
69✔
1636
    if (!_is_dragging_interval) {
69✔
1637
        return;
69✔
1638
    }
1639

1640
    // Get the series data for rendering
1641
    auto it = _digital_interval_series.find(_dragging_series_key);
×
1642
    if (it == _digital_interval_series.end()) {
×
UNCOV
1643
        return;
×
1644
    }
1645

UNCOV
1646
    auto const & display_options = it->second.display_options;
×
1647

1648
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
1649
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
1650
    auto const min_y = _yMin;
×
UNCOV
1651
    auto const max_y = _yMax;
×
1652

1653
    // Check if the dragged interval is visible
1654
    if (_dragged_end_time < static_cast<int64_t>(start_time) || _dragged_start_time > static_cast<int64_t>(end_time)) {
×
UNCOV
1655
        return;
×
1656
    }
1657

1658
    auto axesProgram = ShaderManager::instance().getProgram("axes");
×
UNCOV
1659
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
×
1660

1661
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
UNCOV
1662
    setupVertexAttribs();
×
1663

1664
    // Set up matrices (same as normal interval rendering)
1665
    auto Model = glm::mat4(1.0f);
×
1666
    auto View = glm::mat4(1.0f);
×
UNCOV
1667
    auto Projection = glm::ortho(start_time, end_time, min_y, max_y);
×
1668

1669
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
1670
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
UNCOV
1671
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
1672

1673
    // Get colors
1674
    int r, g, b;
×
1675
    hexToRGB(display_options->hex_color, r, g, b);
×
1676
    float const rNorm = static_cast<float>(r) / 255.0f;
×
1677
    float const gNorm = static_cast<float>(g) / 255.0f;
×
UNCOV
1678
    float const bNorm = static_cast<float>(b) / 255.0f;
×
1679

1680
    // Clip the dragged interval to visible range
1681
    float const dragged_start = std::max(static_cast<float>(_dragged_start_time), start_time);
×
UNCOV
1682
    float const dragged_end = std::min(static_cast<float>(_dragged_end_time), end_time);
×
1683

1684
    // Draw the original interval dimmed (alpha = 0.2)
1685
    float const original_start = std::max(static_cast<float>(_original_start_time), start_time);
×
UNCOV
1686
    float const original_end = std::min(static_cast<float>(_original_end_time), end_time);
×
1687

1688
    // Set color and alpha uniforms for original interval (dimmed)
1689
    glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
UNCOV
1690
    glUniform1f(m_alphaLoc, 0.2f);
×
1691

1692
    // Create 4D vertices (x, y, 0, 1) to match the shader expectations
UNCOV
1693
    std::array<GLfloat, 16> original_vertices = {
×
1694
            original_start, min_y, 0.0f, 1.0f,
1695
            original_end, min_y, 0.0f, 1.0f,
1696
            original_end, max_y, 0.0f, 1.0f,
UNCOV
1697
            original_start, max_y, 0.0f, 1.0f};
×
1698

1699
    m_vbo.bind();
×
1700
    m_vbo.allocate(original_vertices.data(), original_vertices.size() * sizeof(GLfloat));
×
1701
    m_vbo.release();
×
UNCOV
1702
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
1703

1704
    // Set color and alpha uniforms for dragged interval (semi-transparent)
1705
    glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
UNCOV
1706
    glUniform1f(m_alphaLoc, 0.8f);
×
1707

1708
    // Create 4D vertices (x, y, 0, 1) to match the shader expectations
UNCOV
1709
    std::array<GLfloat, 16> dragged_vertices = {
×
1710
            dragged_start, min_y, 0.0f, 1.0f,
1711
            dragged_end, min_y, 0.0f, 1.0f,
1712
            dragged_end, max_y, 0.0f, 1.0f,
UNCOV
1713
            dragged_start, max_y, 0.0f, 1.0f};
×
1714

1715
    m_vbo.bind();
×
1716
    m_vbo.allocate(dragged_vertices.data(), dragged_vertices.size() * sizeof(GLfloat));
×
1717
    m_vbo.release();
×
UNCOV
1718
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
1719

1720
    glUseProgram(0);
×
UNCOV
1721
}
×
1722

1723
namespace TimeSeriesDefaultValues {
1724
std::string getColorForIndex(size_t index) {
×
1725
    if (index < DEFAULT_COLORS.size()) {
×
UNCOV
1726
        return DEFAULT_COLORS[index];
×
1727
    } else {
UNCOV
1728
        return generateRandomColor();
×
1729
    }
1730
}
1731
}// namespace TimeSeriesDefaultValues
1732

1733
void OpenGLWidget::mouseDoubleClickEvent(QMouseEvent * event) {
×
UNCOV
1734
    if (event->button() == Qt::LeftButton) {
×
1735
        // Check if we're double-clicking over a digital interval series
1736
        //float const canvas_x = static_cast<float>(event->pos().x());
1737
        //float const canvas_y = static_cast<float>(event->pos().y());
1738

1739
        // Find which digital interval series (if any) is at this Y position
1740
        // For now, use the first visible digital interval series
1741
        // TODO: Improve this to detect which series based on Y coordinate
1742
        for (auto const & [series_key, data]: _digital_interval_series) {
×
1743
            if (data.display_options->is_visible) {
×
1744
                startNewIntervalCreation(series_key, event->pos());
×
UNCOV
1745
                return;
×
1746
            }
1747
        }
1748
    }
1749

UNCOV
1750
    QOpenGLWidget::mouseDoubleClickEvent(event);
×
1751
}
1752

UNCOV
1753
void OpenGLWidget::startNewIntervalCreation(std::string const & series_key, QPoint const & start_pos) {
×
1754
    // Don't start if we're already dragging an interval
1755
    if (_is_dragging_interval || _is_creating_new_interval) {
×
UNCOV
1756
        return;
×
1757
    }
1758

1759
    // Check if the series exists
1760
    auto it = _digital_interval_series.find(series_key);
×
1761
    if (it == _digital_interval_series.end()) {
×
UNCOV
1762
        return;
×
1763
    }
1764

1765
    _is_creating_new_interval = true;
×
1766
    _new_interval_series_key = series_key;
×
UNCOV
1767
    _new_interval_click_pos = start_pos;
×
1768

1769
    // Convert click position to time coordinate (in master time frame)
1770
    float const click_time_master = canvasXToTime(static_cast<float>(start_pos.x()));
×
UNCOV
1771
    _new_interval_click_time = static_cast<int64_t>(std::round(click_time_master));
×
1772

1773
    // Initialize start and end to the click position
1774
    _new_interval_start_time = _new_interval_click_time;
×
UNCOV
1775
    _new_interval_end_time = _new_interval_click_time;
×
1776

1777
    // Set cursor to indicate creation mode
UNCOV
1778
    setCursor(Qt::SizeHorCursor);
×
1779

1780
    std::cout << "Started new interval creation for series " << series_key
UNCOV
1781
              << " at time " << _new_interval_click_time << std::endl;
×
1782
}
1783

1784
void OpenGLWidget::updateNewIntervalCreation(QPoint const & current_pos) {
×
1785
    if (!_is_creating_new_interval) {
×
UNCOV
1786
        return;
×
1787
    }
1788

1789
    // Convert current mouse position to time coordinate (in master time frame)
1790
    float const current_time_master = canvasXToTime(static_cast<float>(current_pos.x()));
×
UNCOV
1791
    int64_t const current_time_coord = static_cast<int64_t>(std::round(current_time_master));
×
1792

1793
    // Get the series data for constraint checking
1794
    auto it = _digital_interval_series.find(_new_interval_series_key);
×
UNCOV
1795
    if (it == _digital_interval_series.end()) {
×
1796
        // Series not found - abort creation
1797
        cancelNewIntervalCreation();
×
UNCOV
1798
        return;
×
1799
    }
1800

1801
    auto const & series = it->second.series;
×
UNCOV
1802
    auto const & time_frame = it->second.time_frame;
×
1803

1804
    // Convert coordinates to series time frame for collision detection
1805
    int64_t click_time_series, current_time_series;
×
UNCOV
1806
    if (time_frame.get() == _master_time_frame.get()) {
×
1807
        // Same time frame - use coordinates directly
1808
        click_time_series = _new_interval_click_time;
×
UNCOV
1809
        current_time_series = current_time_coord;
×
1810
    } else {
1811
        // Convert master time coordinates to series time frame indices
1812
        click_time_series = time_frame->getIndexAtTime(static_cast<float>(_new_interval_click_time)).getValue();
×
UNCOV
1813
        current_time_series = time_frame->getIndexAtTime(static_cast<float>(current_time_coord)).getValue();
×
1814
    }
1815

1816
    // Determine interval bounds (always ensure start < end)
1817
    int64_t new_start_series = std::min(click_time_series, current_time_series);
×
UNCOV
1818
    int64_t new_end_series = std::max(click_time_series, current_time_series);
×
1819

1820
    // Ensure minimum interval size of 1
1821
    if (new_start_series == new_end_series) {
×
1822
        if (current_time_series >= click_time_series) {
×
UNCOV
1823
            new_end_series = new_start_series + 1;
×
1824
        } else {
UNCOV
1825
            new_start_series = new_end_series - 1;
×
1826
        }
1827
    }
1828

1829
    // Check for collision with existing intervals in series time frame
1830
    auto overlapping_intervals = series->getIntervalsInRange<DigitalIntervalSeries::RangeMode::OVERLAPPING>(
×
UNCOV
1831
            new_start_series, new_end_series);
×
1832

1833
    // If there are overlapping intervals, constrain the new interval
1834
    for (auto const & interval: overlapping_intervals) {
×
UNCOV
1835
        if (current_time_series >= click_time_series) {
×
1836
            // Dragging right - stop before the first overlapping interval
1837
            if (new_end_series >= interval.start) {
×
1838
                new_end_series = interval.start - 1;
×
1839
                if (new_end_series <= new_start_series) {
×
UNCOV
1840
                    new_end_series = new_start_series + 1;
×
1841
                }
UNCOV
1842
                break;
×
1843
            }
1844
        } else {
1845
            // Dragging left - stop after the last overlapping interval
1846
            if (new_start_series <= interval.end) {
×
1847
                new_start_series = interval.end + 1;
×
1848
                if (new_start_series >= new_end_series) {
×
UNCOV
1849
                    new_start_series = new_end_series - 1;
×
1850
                }
UNCOV
1851
                break;
×
1852
            }
1853
        }
1854
    }
1855

1856
    // Convert back to master time frame for display
UNCOV
1857
    if (time_frame.get() == _master_time_frame.get()) {
×
1858
        // Same time frame
1859
        _new_interval_start_time = new_start_series;
×
UNCOV
1860
        _new_interval_end_time = new_end_series;
×
1861
    } else {
1862
        // Convert series indices back to master time coordinates
1863
        try {
1864
            _new_interval_start_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_start_series)));
×
1865
            _new_interval_end_time = static_cast<int64_t>(time_frame->getTimeAtIndex(TimeFrameIndex(new_end_series)));
×
UNCOV
1866
        } catch (...) {
×
1867
            // Conversion failed - abort creation
1868
            cancelNewIntervalCreation();
×
1869
            return;
×
UNCOV
1870
        }
×
1871
    }
1872

1873
    // Trigger redraw to show the new interval being created
UNCOV
1874
    updateCanvas(_time);
×
1875
}
1876

1877
void OpenGLWidget::finishNewIntervalCreation() {
×
1878
    if (!_is_creating_new_interval) {
×
UNCOV
1879
        return;
×
1880
    }
1881

1882
    // Get the series data
1883
    auto it = _digital_interval_series.find(_new_interval_series_key);
×
UNCOV
1884
    if (it == _digital_interval_series.end()) {
×
1885
        // Series not found - abort creation
1886
        cancelNewIntervalCreation();
×
UNCOV
1887
        return;
×
1888
    }
1889

1890
    auto const & series = it->second.series;
×
UNCOV
1891
    auto const & time_frame = it->second.time_frame;
×
1892

1893
    try {
1894
        // Convert coordinates to series time frame for data operations
1895
        int64_t new_start_series, new_end_series;
1896

UNCOV
1897
        if (time_frame.get() == _master_time_frame.get()) {
×
1898
            // Same time frame - use coordinates directly
1899
            new_start_series = _new_interval_start_time;
×
UNCOV
1900
            new_end_series = _new_interval_end_time;
×
1901
        } else {
1902
            // Convert master time coordinates to series time frame indices
1903
            new_start_series = time_frame->getIndexAtTime(static_cast<float>(_new_interval_start_time)).getValue();
×
UNCOV
1904
            new_end_series = time_frame->getIndexAtTime(static_cast<float>(_new_interval_end_time)).getValue();
×
1905
        }
1906

1907
        // Validate converted coordinates
1908
        if (new_start_series >= new_end_series || new_start_series < 0 || new_end_series < 0) {
×
UNCOV
1909
            throw std::runtime_error("Invalid interval bounds after conversion");
×
1910
        }
1911

1912
        // Add the new interval to the series
UNCOV
1913
        series->addEvent(TimeFrameIndex(new_start_series), TimeFrameIndex(new_end_series));
×
1914

1915
        // Set the new interval as selected (stored in master time frame coordinates)
UNCOV
1916
        setSelectedInterval(_new_interval_series_key, _new_interval_start_time, _new_interval_end_time);
×
1917

1918
        std::cout << "Created new interval [" << _new_interval_start_time
×
1919
                  << ", " << _new_interval_end_time << "] for series "
×
UNCOV
1920
                  << _new_interval_series_key << std::endl;
×
1921

UNCOV
1922
    } catch (...) {
×
1923
        // Error occurred during conversion or data update - abort creation
1924
        std::cout << "Error during new interval creation" << std::endl;
×
1925
        cancelNewIntervalCreation();
×
1926
        return;
×
UNCOV
1927
    }
×
1928

1929
    // Reset creation state
1930
    _is_creating_new_interval = false;
×
1931
    _new_interval_series_key.clear();
×
UNCOV
1932
    setCursor(Qt::ArrowCursor);
×
1933

1934
    // Trigger final redraw
UNCOV
1935
    updateCanvas(_time);
×
1936
}
1937

1938
void OpenGLWidget::cancelNewIntervalCreation() {
×
1939
    if (!_is_creating_new_interval) {
×
UNCOV
1940
        return;
×
1941
    }
1942

UNCOV
1943
    std::cout << "Cancelled new interval creation" << std::endl;
×
1944

1945
    // Reset creation state without applying changes
1946
    _is_creating_new_interval = false;
×
1947
    _new_interval_series_key.clear();
×
UNCOV
1948
    setCursor(Qt::ArrowCursor);
×
1949

1950
    // Trigger redraw to remove the new interval visualization
UNCOV
1951
    updateCanvas(_time);
×
1952
}
1953

1954
void OpenGLWidget::drawNewIntervalBeingCreated() {
69✔
1955
    if (!_is_creating_new_interval) {
69✔
1956
        return;
69✔
1957
    }
1958

1959
    // Get the series data for rendering
1960
    auto it = _digital_interval_series.find(_new_interval_series_key);
×
1961
    if (it == _digital_interval_series.end()) {
×
UNCOV
1962
        return;
×
1963
    }
1964

UNCOV
1965
    auto const & display_options = it->second.display_options;
×
1966

1967
    auto const start_time = static_cast<float>(_xAxis.getStart());
×
1968
    auto const end_time = static_cast<float>(_xAxis.getEnd());
×
1969
    auto const min_y = _yMin;
×
UNCOV
1970
    auto const max_y = _yMax;
×
1971

1972
    // Check if the new interval is visible
1973
    if (_new_interval_end_time < static_cast<int64_t>(start_time) || _new_interval_start_time > static_cast<int64_t>(end_time)) {
×
UNCOV
1974
        return;
×
1975
    }
1976

1977
    auto axesProgram = ShaderManager::instance().getProgram("axes");
×
UNCOV
1978
    if (axesProgram) glUseProgram(axesProgram->getProgramId());
×
1979

1980
    QOpenGLVertexArrayObject::Binder const vaoBinder(&m_vao);
×
UNCOV
1981
    setupVertexAttribs();
×
1982

1983
    // Set up matrices (same as normal interval rendering)
1984
    auto Model = glm::mat4(1.0f);
×
1985
    auto View = glm::mat4(1.0f);
×
UNCOV
1986
    auto Projection = glm::ortho(start_time, end_time, min_y, max_y);
×
1987

1988
    glUniformMatrix4fv(m_projMatrixLoc, 1, GL_FALSE, &Projection[0][0]);
×
1989
    glUniformMatrix4fv(m_viewMatrixLoc, 1, GL_FALSE, &View[0][0]);
×
UNCOV
1990
    glUniformMatrix4fv(m_modelMatrixLoc, 1, GL_FALSE, &Model[0][0]);
×
1991

1992
    // Get colors
1993
    int r, g, b;
×
1994
    hexToRGB(display_options->hex_color, r, g, b);
×
1995
    float const rNorm = static_cast<float>(r) / 255.0f;
×
1996
    float const gNorm = static_cast<float>(g) / 255.0f;
×
UNCOV
1997
    float const bNorm = static_cast<float>(b) / 255.0f;
×
1998

1999
    // Set color and alpha uniforms for new interval (50% transparency)
2000
    glUniform3f(m_colorLoc, rNorm, gNorm, bNorm);
×
UNCOV
2001
    glUniform1f(m_alphaLoc, 0.5f);
×
2002

2003
    // Clip the new interval to visible range
2004
    float const new_start = std::max(static_cast<float>(_new_interval_start_time), start_time);
×
UNCOV
2005
    float const new_end = std::min(static_cast<float>(_new_interval_end_time), end_time);
×
2006

2007
    // Draw the new interval being created with 50% transparency
2008
    // Create 4D vertices (x, y, 0, 1) to match the shader expectations
UNCOV
2009
    std::array<GLfloat, 16> new_interval_vertices = {
×
2010
            new_start, min_y, 0.0f, 1.0f,
2011
            new_end, min_y, 0.0f, 1.0f,
2012
            new_end, max_y, 0.0f, 1.0f,
UNCOV
2013
            new_start, max_y, 0.0f, 1.0f};
×
2014

2015
    m_vbo.bind();
×
2016
    m_vbo.allocate(new_interval_vertices.data(), new_interval_vertices.size() * sizeof(GLfloat));
×
2017
    m_vbo.release();
×
UNCOV
2018
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
×
2019

2020
    glUseProgram(0);
×
UNCOV
2021
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc