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

mcallegari / qlcplus / 8121957695

02 Mar 2024 09:50AM UTC coverage: 32.097% (+0.06%) from 32.036%
8121957695

push

github

web-flow
Merge pull request #1522 from mcallegari/16bitfade

16bit fade rework

145 of 178 new or added lines in 7 files covered. (81.46%)

1 existing line in 1 file now uncovered.

15387 of 47939 relevant lines covered (32.1%)

24361.41 hits per line

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

67.75
/engine/src/efxfixture.cpp
1
/*
2
  Q Light Controller Plus
3
  efxfixture.cpp
4

5
  Copyright (c) Heikki Junnila
6
                Massimo Callegari
7

8
  Licensed under the Apache License, Version 2.0 (the "License");
9
  you may not use this file except in compliance with the License.
10
  You may obtain a copy of the License at
11

12
      http://www.apache.org/licenses/LICENSE-2.0.txt
13

14
  Unless required by applicable law or agreed to in writing, software
15
  distributed under the License is distributed on an "AS IS" BASIS,
16
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
  See the License for the specific language governing permissions and
18
  limitations under the License.
19
*/
20

21
#include <QXmlStreamReader>
22
#include <QXmlStreamWriter>
23
#include <QDebug>
24
#include <math.h>
25

26
#include "genericfader.h"
27
#include "fadechannel.h"
28
#include "mastertimer.h"
29
#include "efxfixture.h"
30
#include "qlcmacros.h"
31
#include "function.h"
32
#include "universe.h"
33
#include "gradient.h"
34
#include "efx.h"
35
#include "doc.h"
36

37
/*****************************************************************************
38
 * Initialization
39
 *****************************************************************************/
40
QImage EFXFixture::m_rgbGradient = QImage();
41

42
EFXFixture::EFXFixture(const EFX* parent)
54✔
43
    : m_parent(parent)
44
    , m_head()
45
    , m_universe(Universe::invalid())
54✔
46
    , m_direction(Function::Forward)
47
    , m_startOffset(0)
48
    , m_mode(EFXFixture::PanTilt)
49

50
    , m_serialNumber(0)
51
    , m_runTimeDirection(Function::Forward)
52
    , m_done(false)
53
    , m_started(false)
54
    , m_elapsed(0)
55
    , m_currentAngle(0)
54✔
56
{
57
    Q_ASSERT(parent != NULL);
54✔
58

59
    if (m_rgbGradient.isNull ())
54✔
60
        m_rgbGradient = Gradient::getRGBGradient (256, 256);
3✔
61
}
54✔
62

63
void EFXFixture::copyFrom(const EFXFixture* ef)
7✔
64
{
65
    // Don't copy m_parent because it is already assigned in constructor and might
66
    // be different than $ef's
67
    m_head = ef->m_head;
7✔
68
    m_universe = ef->m_universe;
7✔
69
    m_direction = ef->m_direction;
7✔
70
    m_startOffset = ef->m_startOffset;
7✔
71
    m_mode = ef->m_mode;
7✔
72

73
    m_serialNumber = ef->m_serialNumber;
7✔
74
    m_runTimeDirection = ef->m_runTimeDirection;
7✔
75
    m_done = ef->m_done;
7✔
76
    m_started = ef->m_started;
7✔
77
    m_elapsed = ef->m_elapsed;
7✔
78
    m_currentAngle = ef->m_currentAngle;
7✔
79
}
7✔
80

81
EFXFixture::~EFXFixture()
53✔
82
{
83
}
53✔
84

85
/****************************************************************************
86
 * Public properties
87
 ****************************************************************************/
88

89
void EFXFixture::setHead(GroupHead const & head)
44✔
90
{
91
    m_head = head;
44✔
92

93
    Fixture *fxi = doc()->fixture(head.fxi);
44✔
94
    if (fxi == NULL)
44✔
95
        return;
30✔
96

97
    m_universe = fxi->universe();
14✔
98

99
    QList<Mode> modes;
28✔
100

101
    if (fxi->channelNumber(QLCChannel::Pan, QLCChannel::MSB, head.head) != QLCChannel::invalid() ||
16✔
102
        fxi->channelNumber(QLCChannel::Tilt, QLCChannel::MSB, head.head) != QLCChannel::invalid())
2✔
103
        modes << PanTilt;
14✔
104

105
    if (fxi->masterIntensityChannel() != QLCChannel::invalid() ||
16✔
106
        fxi->channelNumber(QLCChannel::Intensity, QLCChannel::MSB, head.head) != QLCChannel::invalid())
2✔
107
        modes << Dimmer;
12✔
108

109
    if (fxi->rgbChannels(head.head).size() >= 3)
14✔
110
        modes << RGB;
2✔
111

112
    if (!modes.contains(m_mode))
14✔
113
    {
114
        if (modes.size() > 0)
×
115
            m_mode = modes[0];
×
116
    }
117
}
118

119
GroupHead const & EFXFixture::head() const
1,374✔
120
{
121
    return m_head;
1,374✔
122
}
123

124
void EFXFixture::setDirection(Function::Direction dir)
18✔
125
{
126
    m_direction = dir;
18✔
127
    m_runTimeDirection = dir;
18✔
128
}
18✔
129

130
Function::Direction EFXFixture::direction() const
13✔
131
{
132
    return m_direction;
13✔
133
}
134

135
void EFXFixture::setStartOffset(int startOffset)
5✔
136
{
137
    m_startOffset = CLAMP(startOffset, 0, 359);
5✔
138
}
5✔
139

140
int EFXFixture::startOffset() const
9✔
141
{
142
    return m_startOffset;
9✔
143
}
144

145
void EFXFixture::setMode(Mode mode)
×
146
{
147
    m_mode = mode;
×
148
}
×
149

150
EFXFixture::Mode EFXFixture::mode() const
4✔
151
{
152
    return m_mode;
4✔
153
}
154

155
quint32 EFXFixture::universe()
156✔
156
{
157
    return m_universe;
156✔
158
}
159

160
bool EFXFixture::isValid() const
158✔
161
{
162
    Fixture *fxi = doc()->fixture(head().fxi);
158✔
163

164
    if (fxi == NULL)
158✔
165
        return false;
2✔
166
    else if (head().head >= fxi->heads())
156✔
167
        return false;
×
168
    else if (m_mode == PanTilt &&
468✔
169
             fxi->channelNumber(QLCChannel::Pan, QLCChannel::MSB, head().head) == QLCChannel::invalid() && // Maybe a device can pan OR tilt
156✔
170
             fxi->channelNumber(QLCChannel::Tilt, QLCChannel::MSB, head().head) == QLCChannel::invalid())   // but not both
×
171
        return false;
×
172
    else if (m_mode == Dimmer &&
312✔
173
             fxi->masterIntensityChannel() == QLCChannel::invalid() &&
156✔
174
             fxi->channelNumber(QLCChannel::Intensity, QLCChannel::MSB, head().head) == QLCChannel::invalid())
×
175
        return false;
×
176
    else if (m_mode == RGB && fxi->rgbChannels(head().head).size () == 0)
156✔
177
        return false;
×
178
    else
179
        return true;
156✔
180
}
181

182
void EFXFixture::durationChanged()
3✔
183
{
184
    // To avoid jumps when changing duration,
185
    // the elapsed time is rescaled to the
186
    // new duration.
187
    m_elapsed = SCALE(float(m_currentAngle),
3✔
188
            float(0), float(M_PI * 2),
189
            float(0), float(m_parent->loopDuration()));
190

191
    // Serial or Asymmetric propagation mode:
192
    // we must substract the offset from the current position
193
    if (timeOffset())
3✔
194
    {
195
        if (m_elapsed < timeOffset())
×
196
            m_elapsed += m_parent->loopDuration();
×
197
        m_elapsed -= timeOffset();
×
198
    }
199
}
3✔
200

201
QStringList EFXFixture::modeList()
×
202
{
203
    Fixture* fxi = doc()->fixture(head().fxi);
×
204
    Q_ASSERT(fxi != NULL);
×
205

206
    QStringList modes;
×
207

208
    if (fxi->channelNumber(QLCChannel::Pan, QLCChannel::MSB, head().head) != QLCChannel::invalid() ||
×
209
       fxi->channelNumber(QLCChannel::Tilt, QLCChannel::MSB, head().head) != QLCChannel::invalid())
×
210
        modes << KXMLQLCEFXFixtureModePanTilt;
×
211

212
    if (fxi->masterIntensityChannel() != QLCChannel::invalid() ||
×
213
       fxi->channelNumber(QLCChannel::Intensity, QLCChannel::MSB, head().head) != QLCChannel::invalid())
×
214
        modes << KXMLQLCEFXFixtureModeDimmer;
×
215

216
    if (fxi->rgbChannels(head().head).size() >= 3)
×
217
        modes << KXMLQLCEFXFixtureModeRGB;
×
218

219
    return modes;
×
220
}
221

222
QString EFXFixture::modeToString(Mode mode)
×
223
{
224
    switch (mode)
×
225
    {
226
        default:
×
227
        case PanTilt:
228
            return QString(KXMLQLCEFXFixtureModePanTilt);
×
229
        case Dimmer:
×
230
            return QString(KXMLQLCEFXFixtureModeDimmer);
×
231
        case RGB:
×
232
            return QString(KXMLQLCEFXFixtureModeRGB);
×
233
    }
234
}
235

236
EFXFixture::Mode EFXFixture::stringToMode(const QString& str)
×
237
{
238
    if (str == QString(KXMLQLCEFXFixtureModePanTilt))
×
239
        return PanTilt;
×
240
    else if (str == QString(KXMLQLCEFXFixtureModeDimmer))
×
241
        return Dimmer;
×
242
    else if (str == QString(KXMLQLCEFXFixtureModeRGB))
×
243
        return RGB;
×
244
    else
245
        return PanTilt;
×
246
}
247

248
/*****************************************************************************
249
 * Load & Save
250
 *****************************************************************************/
251

252
bool EFXFixture::loadXML(QXmlStreamReader &root)
13✔
253
{
254
    if (root.name() != KXMLQLCEFXFixture)
13✔
255
    {
256
        qWarning("EFX Fixture node not found!");
1✔
257
        return false;
1✔
258
    }
259

260
    GroupHead head;
12✔
261
    head.head = 0;
12✔
262

263
    /* New file format contains sub tags */
264
    while (root.readNextStartElement())
38✔
265
    {
266
        if (root.name() == KXMLQLCEFXFixtureID)
26✔
267
        {
268
            /* Fixture ID */
269
            head.fxi = root.readElementText().toInt();
12✔
270
        }
271
        else if (root.name() == KXMLQLCEFXFixtureHead)
14✔
272
        {
273
            /* Fixture Head */
274
            head.head = root.readElementText().toInt();
1✔
275
        }
276
        else if (root.name() == KXMLQLCEFXFixtureMode)
13✔
277
        {
278
            /* Fixture Mode */
279
            setMode ((Mode) root.readElementText().toInt());
×
280
        }
281
        else if (root.name() == KXMLQLCEFXFixtureDirection)
13✔
282
        {
283
            /* Direction */
284
            Function::Direction dir = Function::stringToDirection(root.readElementText());
12✔
285
            setDirection(dir);
12✔
286
        }
287
        else if (root.name() == KXMLQLCEFXFixtureStartOffset)
1✔
288
        {
289
            /* Start offset */
290
            setStartOffset(root.readElementText().toInt());
×
291
        }
292
        else if (root.name() == KXMLQLCEFXFixtureIntensity)
1✔
293
        {
294
            /* Intensity - LEGACY */
295
            root.skipCurrentElement();
×
296
        }
297
        else
298
        {
299
            qWarning() << "Unknown EFX Fixture tag:" << root.name();
1✔
300
            root.skipCurrentElement();
1✔
301
        }
302
    }
303

304
    if (head.fxi != Fixture::invalidId())
12✔
305
       setHead(head);
12✔
306

307
    return true;
12✔
308
}
309

310
bool EFXFixture::saveXML(QXmlStreamWriter *doc) const
4✔
311
{
312
    Q_ASSERT(doc != NULL);
4✔
313

314
    /* EFXFixture */
315
    doc->writeStartElement(KXMLQLCEFXFixture);
4✔
316

317
    /* Fixture ID */
318
    doc->writeTextElement(KXMLQLCEFXFixtureID, QString("%1").arg(head().fxi));
4✔
319
    /* Fixture Head */
320
    doc->writeTextElement(KXMLQLCEFXFixtureHead, QString("%1").arg(head().head));
4✔
321
    /* Mode */
322
    doc->writeTextElement(KXMLQLCEFXFixtureMode, QString::number(mode()));
4✔
323
    /* Direction */
324
    doc->writeTextElement(KXMLQLCEFXFixtureDirection, Function::directionToString(m_direction));
4✔
325
    /* Start offset */
326
    doc->writeTextElement(KXMLQLCEFXFixtureStartOffset, QString::number(startOffset()));
4✔
327

328
    /* End the <Fixture> tag */
329
    doc->writeEndElement();
4✔
330

331
    return true;
4✔
332
}
333

334
/****************************************************************************
335
 * Run-time properties
336
 ****************************************************************************/
337

338
const Doc* EFXFixture::doc() const
670✔
339
{
340
    Q_ASSERT(m_parent != NULL);
670✔
341
    return m_parent->doc();
670✔
342
}
343

344
void EFXFixture::setSerialNumber(int number)
13✔
345
{
346
    m_serialNumber = number;
13✔
347
}
13✔
348

349
int EFXFixture::serialNumber() const
5✔
350
{
351
    return m_serialNumber;
5✔
352
}
353

354
void EFXFixture::reset()
10✔
355
{
356
    m_done = false;
10✔
357
    m_runTimeDirection = m_direction;
10✔
358
    m_started = false;
10✔
359
    m_elapsed = 0;
10✔
360
    m_currentAngle = 0;
10✔
361
}
10✔
362

363
bool EFXFixture::isDone() const
206✔
364
{
365
    return m_done;
206✔
366
}
367

368
uint EFXFixture::timeOffset() const
155✔
369
{
370
    if (m_parent->propagationMode() == EFX::Asymmetric ||
310✔
371
        m_parent->propagationMode() == EFX::Serial)
155✔
372
    {
373
        return m_parent->loopDuration() / (m_parent->fixtures().size() + 1) * serialNumber();
3✔
374
    }
375
    else
376
    {
377
        return 0;
152✔
378
    }
379
}
380

381
/*****************************************************************************
382
 * Running
383
 *****************************************************************************/
384

385
void EFXFixture::start()
3✔
386
{
387
    m_started = true;
3✔
388
}
3✔
389

390
void EFXFixture::stop()
5✔
391
{
392
    m_started = false;
5✔
393
}
5✔
394

395
void EFXFixture::nextStep(QList<Universe *> universes, QSharedPointer<GenericFader> fader)
202✔
396
{
397
    // Nothing to do
398
    if (m_parent->loopDuration() == 0)
202✔
399
        return;
50✔
400

401
    // Bail out without doing anything if this fixture is ready (after single-shot)
402
    // or it has no pan&tilt channels (not valid).
403
    if (m_done == true || isValid() == false)
152✔
404
        return;
×
405

406
    m_elapsed += MasterTimer::tick();
152✔
407

408
    // Check time wrapping
409
    if (m_elapsed > m_parent->loopDuration())
152✔
410
    {
411
        if (m_parent->runOrder() == Function::PingPong)
2✔
412
        {
413
            /* Reverse direction for ping-pong EFX. */
414
            if (m_runTimeDirection == Function::Forward)
×
415
                m_runTimeDirection = Function::Backward;
×
416
            else
417
                m_runTimeDirection = Function::Forward;
×
418
        }
419
        else if (m_parent->runOrder() == Function::SingleShot)
2✔
420
        {
421
            /* De-initialize the fixture and mark as ready. */
422
            m_done = true;
1✔
423
            stop();
1✔
424
        }
425

426
        m_elapsed = 0;
2✔
427
    }
428

429
    // Bail out without doing anything if this fixture is waiting for its turn.
430
    if (m_parent->propagationMode() == EFX::Serial && m_elapsed < timeOffset() && !m_started)
152✔
431
        return;
×
432

433
    // Fade in
434
    if (m_started == false)
152✔
435
        start();
3✔
436

437
    // Scale from elapsed time in relation to overall duration to a point in a circle
438
    uint pos = (m_elapsed + timeOffset()) % m_parent->loopDuration();
152✔
439
    m_currentAngle = SCALE(float(pos),
152✔
440
                           float(0), float(m_parent->loopDuration()),
441
                           float(0), float(M_PI * 2));
442

443
    float valX = 0;
152✔
444
    float valY = 0;
152✔
445

446
    m_parent->calculatePoint(m_runTimeDirection, m_startOffset, m_currentAngle, &valX, &valY);
152✔
447

448
    /* Set target values on faders/universes */
449
    switch (m_mode)
152✔
450
    {
451
        case PanTilt:
152✔
452
            setPointPanTilt(universes, fader, valX, valY);
152✔
453
        break;
152✔
454

455
        case RGB:
×
456
            setPointRGB(universes, fader, valX, valY);
×
457
        break;
×
458

459
        case Dimmer:
×
460
            //Use Y for coherence with RGB gradient.
461
            setPointDimmer(universes, fader, valY);
×
462
        break;
×
463
    }
464
}
465

466
void EFXFixture::updateFaderValues(FadeChannel *fc, uchar value)
312✔
467
{
468
    fc->setStart(fc->current());
312✔
469
    fc->setTarget(value);
312✔
470
    fc->setElapsed(0);
312✔
471
    fc->setReady(false);
312✔
472
    fc->setFadeTime(0);
312✔
473
}
312✔
474

475
void EFXFixture::setPointPanTilt(QList<Universe *> universes, QSharedPointer<GenericFader> fader,
156✔
476
                                 float pan, float tilt)
477
{
478
    Fixture *fxi = doc()->fixture(head().fxi);
156✔
479
    Q_ASSERT(fxi != NULL);
156✔
480
    Universe *uni = universes[universe()];
156✔
481

482
    //qDebug() << "Pan value: " << pan << ", tilt value:" << tilt;
483

484
    quint32 panMsbChannel = fxi->channelNumber(QLCChannel::Pan, QLCChannel::MSB, head().head);
156✔
485
    quint32 panLsbChannel = fxi->channelNumber(QLCChannel::Pan, QLCChannel::LSB, head().head);
156✔
486
    quint32 tiltMsbChannel = fxi->channelNumber(QLCChannel::Tilt, QLCChannel::MSB, head().head);
156✔
487
    quint32 tiltLsbChannel = fxi->channelNumber(QLCChannel::Tilt, QLCChannel::LSB, head().head);
156✔
488

489
    if (pan < 0)
156✔
NEW
490
        pan = 0;
×
491

492
    if (tilt < 0)
156✔
NEW
493
        tilt = 0;
×
494

495
    /* Write coarse point data to universes */
496
    if (panMsbChannel != QLCChannel::invalid() && !fader.isNull())
156✔
497
    {
498
        FadeChannel *fc = fader->getChannelFader(doc(), uni, fxi->id(), panMsbChannel);
155✔
499
        if (m_parent->isRelative())
155✔
500
            fc->addFlag(FadeChannel::Relative);
×
501
        updateFaderValues(fc, static_cast<uchar>(pan));
155✔
502
    }
503
    if (tiltMsbChannel != QLCChannel::invalid() && !fader.isNull())
156✔
504
    {
505
        FadeChannel *fc = fader->getChannelFader(doc(), uni, fxi->id(), tiltMsbChannel);
155✔
506
        if (m_parent->isRelative())
155✔
507
            fc->addFlag(FadeChannel::Relative);
×
508
        updateFaderValues(fc, static_cast<uchar>(tilt));
155✔
509
    }
510

511
    /* Write fine point data to universes if applicable */
512
    if (panLsbChannel != QLCChannel::invalid() && !fader.isNull())
156✔
513
    {
514
        /* Leave only the fraction */
515
        float value = ((pan - floor(pan)) * float(UCHAR_MAX));
1✔
516
        FadeChannel *fc = fader->getChannelFader(doc(), uni, fxi->id(), panLsbChannel);
1✔
517
        updateFaderValues(fc, static_cast<uchar>(value));
1✔
518
    }
519

520
    if (tiltLsbChannel != QLCChannel::invalid() && !fader.isNull())
156✔
521
    {
522
        /* Leave only the fraction */
523
        float value = ((tilt - floor(tilt)) * float(UCHAR_MAX));
1✔
524
        FadeChannel *fc = fader->getChannelFader(doc(), uni, fxi->id(), tiltLsbChannel);
1✔
525
        updateFaderValues(fc, static_cast<uchar>(value));
1✔
526
    }
527
}
156✔
528

529
void EFXFixture::setPointDimmer(QList<Universe *> universes, QSharedPointer<GenericFader> fader, float dimmer)
×
530
{
531
    Fixture *fxi = doc()->fixture(head().fxi);
×
532
    Q_ASSERT(fxi != NULL);
×
533
    Universe *uni = universes[universe()];
×
534

535
    quint32 intChannel = fxi->channelNumber(QLCChannel::Intensity, QLCChannel::MSB, head().head);
×
536

537
    /* Don't write dimmer data directly to universes but use FadeChannel to avoid steps at EFX loop restart */
538
    if (intChannel != QLCChannel::invalid())
×
539
    {
540
        if (!fader.isNull())
×
541
        {
542
            FadeChannel *fc = fader->getChannelFader(doc(), uni, fxi->id(), intChannel);
×
543
            updateFaderValues(fc, dimmer);
×
544
        }
545
    }
546
    else if (fxi->masterIntensityChannel() != QLCChannel::invalid())
×
547
    {
548
        if (!fader.isNull())
×
549
        {
550
            FadeChannel *fc = fader->getChannelFader(doc(), uni, fxi->id(), fxi->masterIntensityChannel());
×
551
            updateFaderValues(fc, dimmer);
×
552
        }
553
    }
554
}
×
555

556
void EFXFixture::setPointRGB(QList<Universe *> universes, QSharedPointer<GenericFader> fader, float x, float y)
×
557
{
558
    Fixture* fxi = doc()->fixture(head().fxi);
×
559
    Q_ASSERT(fxi != NULL);
×
560
    Universe *uni = universes[universe()];
×
561

562
    QVector<quint32> rgbChannels = fxi->rgbChannels(head().head);
×
563

564
    /* Don't write dimmer data directly to universes but use FadeChannel to avoid steps at EFX loop restart */
565
    if (rgbChannels.size() >= 3 && !fader.isNull())
×
566
    {
567
        QColor pixel = m_rgbGradient.pixel(x, y);
×
568

569
        FadeChannel *fc = fader->getChannelFader(doc(), uni, fxi->id(), rgbChannels[0]);
×
570
        updateFaderValues(fc, pixel.red());
×
571
        fc = fader->getChannelFader(doc(), uni, fxi->id(), rgbChannels[1]);
×
572
        updateFaderValues(fc, pixel.green());
×
573
        fc = fader->getChannelFader(doc(), uni, fxi->id(), rgbChannels[2]);
×
574
        updateFaderValues(fc, pixel.blue());
×
575
    }
576
}
×
577

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