diff --git a/src/libs/core/CMakeLists.txt b/src/libs/core/CMakeLists.txt index b496a3d4abfbc31e4183f46e9405a993d4aba845..bcac2a104290186ea56e8cbc036f485d3386e68e 100644 --- a/src/libs/core/CMakeLists.txt +++ b/src/libs/core/CMakeLists.txt @@ -36,6 +36,7 @@ set(LIB_NOOTKACORE_SRC score/tmeasureobject.cpp score/tnoteobject.cpp score/tnotepair.cpp + score/tbeamobject.cpp instruments/tguitarbg.cpp diff --git a/src/libs/core/score/tbeamobject.cpp b/src/libs/core/score/tbeamobject.cpp new file mode 100644 index 0000000000000000000000000000000000000000..428430f9a391ac74114e719a5b8d99d71abd5934 --- /dev/null +++ b/src/libs/core/score/tbeamobject.cpp @@ -0,0 +1,196 @@ +/*************************************************************************** + * Copyright (C) 2016 by Tomasz Bojczuk * + * seelook@gmail.com * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see <http://www.gnu.org/licenses/>. * + ***************************************************************************/ + +#include "tbeamobject.h" +#include "tscoreobject.h" +#include "tstaffobject.h" +#include "tmeasureobject.h" +#include "tnoteobject.h" +#include "tnotepair.h" +#include <music/tnote.h> + +#include <qmath.h> +#include <QtGui/qguiapplication.h> +#include <QtGui/qpalette.h> +#include <QtGui/qpainter.h> + +#include <QtCore/qdebug.h> +#include "checktime.h" + + +/** + * Simple structure to describe second beam line (sixteenth) + * which can be chunked when there are rests or eights in the group + */ +class T16beam { +public: + T16beam(int firstStemNr = 0) : startStem(firstStemNr) {} + int startStem = -1; /**< Undefined by default (-1) */ + int endStem = -1; /**< When remains undefined (-1) - beam is partial */ + + /** @p TRUE when beam is not connected to another stem */ + bool isHalf() { return endStem == -1; } +}; + + +#define MIN_STEM_HEIGHT (4.0) // minimal stem height (distance of note head to staff boundary) +#define BEAM_THICK (0.8) // thickness of a beam +#define HALF_THICK (0.4) // half of beam thickness + + + +TbeamObject::TbeamObject(TnotePair* sn, TmeasureObject* m) : + QQuickPaintedItem(m->staff()->staffItem()), + m_measure(m) +{ + addNote(sn); + setParent(m_measure->score()); +} + + +TbeamObject::~TbeamObject() +{ + qDebug() << " [BEAM] deleted of id" << first()->index(); + for (TnotePair* note : m_notes) { + note->note()->rtm.setBeam(Trhythm::e_noBeam); // restore beams + } +} + + +void TbeamObject::addNote(TnotePair* np) { + if (np->beam() == nullptr) + np->setBeam(this); + else + qDebug() << " [BEAM] note" << np->index() << "has already a beam"; + + if (m_notes.count() > 1) + m_notes.last()->note()->rtm.setBeam(Trhythm::e_beamCont); // no need to be updated + if (m_notes.isEmpty()) + np->note()->rtm.setBeam(Trhythm::e_beamStart); + else + np->note()->rtm.setBeam(Trhythm::e_beamEnd); + if (np->item()) // mark it changed only when object exists, otherwise its note will be updated during creation + np->addChange(TnotePair::e_beamChanged); + m_notes << np; + + if (np->note()->rhythm() == Trhythm::Sixteenth) { + Tnote* beforeLastNote = m_notes.count() > 1 ? m_notes[m_notes.count() - 2]->note() : nullptr; + if (m_16beams.isEmpty() || (beforeLastNote && beforeLastNote->rhythm() != Trhythm::Sixteenth)) { + // is first in beam or previous note was not a sixteenth + m_16beams << T16beam(m_notes.count() - 1); // then create new beam segment + } else if (!m_16beams.isEmpty() && (beforeLastNote && beforeLastNote->rhythm() == Trhythm::Sixteenth)) { + // there is 16 beam and previous note and this notes are 16th + m_16beams.last().endStem = m_notes.count() - 1; // then set end stem for it + } + } +} + + +/** + * With @p stemDirStrength (strength of stem direction) we trying to determine preferred common direction of stems in the group. + * More far from middle of the staff (18) note is placed - stronger its direction has to be preserved + * to keep beaming more natural and looking good + * + * @p stemsUpPossible keep true when there is enough space between note head and staff border for whole stem + * if not stems go down. + * @p stemTop it top stem position of all notes. It is set to middle of the staff if necessary (to look well) + */ +void TbeamObject::prepareBeam() { + int stemDirStrength = 0; + bool stemsUpPossible = true; + qreal hiNote = 99.0, loNote = 0.0; + for (TnotePair* np : m_notes) { + stemDirStrength += np->item()->notePosY() - 18; + if (np->item()->notePosY() < MIN_STEM_HEIGHT) + stemsUpPossible = false; + hiNote = qMin(hiNote, np->item()->notePosY()); + loNote = qMax(loNote, np->item()->notePosY()); + } + bool allStemsDown = !stemsUpPossible; + qreal minStemHeight = MIN_STEM_HEIGHT + (m_16beams.empty() ? 0.0 : 1.0); + if (stemDirStrength < 0) + allStemsDown = true; // stems down are always possible + qreal stemTop = allStemsDown ? loNote + minStemHeight : hiNote - minStemHeight; + if ((allStemsDown && stemTop < m_measure->staff()->upperLine() + 4.0) || (!allStemsDown && stemTop > m_measure->staff()->upperLine() + 4.0)) + stemTop = m_measure->staff()->upperLine() + 4.0; // keep beam on staff middle line + for (TnotePair* np : m_notes) { + np->note()->rtm.setStemDown(allStemsDown); + np->addChange(TnotePair::e_stemDirChanged); + np->item()->setStemHeight(qAbs(np->item()->notePosY() - stemTop)); + np->approve(); + } +} + + +/** + * Poligons are painted, single for 8ths and possible a few for 16ths. + * Paiter canvas orientation depends on are stems up or down, + * for stems-up, top beam is 8ths and bottom are 16ths, + * for stems-down the opposite. + */ +void TbeamObject::paint(QPainter* painter) { +CHECKTIME ( + if (count() > 1) { + qreal s = first()->note()->rtm.stemDown() ? -1.0 : 1.0; + qreal t = first()->note()->rtm.stemDown() ? height() : 0.0; + painter->setPen(Qt::NoPen); + painter->setBrush(qApp->palette().text().color()); + QPolygonF topBeam; + topBeam << QPointF(0.0, t) << QPointF(0.0, t + s * BEAM_THICK) << QPointF(width(), t + s * BEAM_THICK) << QPointF(width(), t) << QPointF(0.0, t); + painter->drawPolygon(topBeam); + for (int b = 0; b < m_16beams.count(); ++b) { + T16beam& b16 = m_16beams[b]; + qreal startX = m_notes[b16.startStem]->item()->stemTop().x() - x(); + // 16th beam of fist stem is right-sided (2.0) others are left-sided (-2.0) + qreal endX = (b16.isHalf() ? startX + BEAM_THICK * (b16.startStem == 0 ? 2.0 : -2.0) : m_notes[b16.endStem]->item()->stemTop().x() - x()) + 0.3; + QPolygonF polyB; + polyB << QPointF(startX, t + s * 1.5 * BEAM_THICK) << QPointF(startX, t + s * 2.5 * BEAM_THICK) << QPointF(endX, t + s * 2.5 * BEAM_THICK) + << QPointF(endX, t + s * 1.5 * BEAM_THICK) << QPointF(startX, t + s * 1.5 * BEAM_THICK); + painter->drawPolygon(polyB); + } + } +) +} + + +//################################################################################################# +//################### PROTECTED ############################################ +//################################################################################################# + +/** + * According to first and last notes in the beam, this method sets item geometry. + * @p setTextureSize() is called to make texture big enough and avoid pixelization + */ +void TbeamObject::drawBeam() { + auto p1 = first()->item()->stemTop(); + auto p2 = last()->item()->stemTop(); + setWidth(qAbs(p2.x() - p1.x()) + 0.3); + setHeight(qAbs(p1.y() - p2.y()) + BEAM_THICK * 2.5); + setX(p1.x()); + setY(qMin(p1.y(), p2.y()) - (first()->note()->rtm.stemDown() ? 2.0 * BEAM_THICK : HALF_THICK)); + setTextureSize(QSize(qCeil(width() * m_measure->staff()->scale()), qCeil(height() * m_measure->staff()->scale()))); + /** @p update should be call here to invoke @p paint() + * but it is invoked by Qt itself whenever width, height, x or y change */ +} + + +void TbeamObject::changeStaff(TstaffObject* st) { + setParentItem(st->staffItem()); +} + + diff --git a/src/libs/core/score/tbeamobject.h b/src/libs/core/score/tbeamobject.h new file mode 100644 index 0000000000000000000000000000000000000000..165f8ae53c6b32f2c58ad611710ba7275b7fb046 --- /dev/null +++ b/src/libs/core/score/tbeamobject.h @@ -0,0 +1,98 @@ +/*************************************************************************** + * Copyright (C) 2016-2017 by Tomasz Bojczuk * + * seelook@gmail.com * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see <http://www.gnu.org/licenses/>. * + ***************************************************************************/ + +#ifndef TBEAMOBJECT_H +#define TBEAMOBJECT_H + + +#include <nootkacoreglobal.h> +#include <QtQuick/qquickpainteditem.h> + + +class TstaffObject; +class TmeasureObject; +class TnoteObject; +class TnotePair; +class T16beam; + + +/** + * This class manages displaying beams of rhythmic group. + * It paints either eight beam line and 16th beam line(s) + * By @p addNote() a beam flag of @p TnotePair is set. + * It that moment @p TnotePair::objeject() can be empty. + * After note objects initialization @p prepareBeam() is called + * where common stem direction of rhythm group is set and stems length. + * When @p TnoteObject::setX() is called (note and its stem changes position) + * @p drawBeam() is invoked to refresh beam size and call @p update() + * to repaint the beam if necessary. + */ +class NOOTKACORE_EXPORT TbeamObject : public QQuickPaintedItem +{ + + Q_OBJECT + + friend class TstaffObject; + friend class TmeasureObject; + friend class TnoteObject; + +public: + explicit TbeamObject(TnotePair* sn, TmeasureObject* m); + virtual ~TbeamObject(); + + /** Adds @p TnotePair to beam group + * and according to adding order sets appropriate beam flag. + * It changes stem direction of note(s) when necessary. + * It does not perform painting yet + */ + void addNote(TnotePair* np); + + /** + * Returns note @p id in beam group + */ + TnotePair* note(int id) { return m_notes[id]; } + + TnotePair* first() { return m_notes.first(); } + TnotePair* last() { return m_notes.last(); } + + /** + * Number of notes in the beam group + */ + int count() { return m_notes.count(); } + + + void paint(QPainter* painter) override; + +protected: + void prepareBeam(); + void drawBeam(); + + /** + * Because beams are parented with staff it is important + * to change their staff when measure is shifted between staves + */ + void changeStaff(TstaffObject* st); + + +private: + TmeasureObject *m_measure; + QList<TnotePair*> m_notes; + QList<T16beam> m_16beams; /**< list of lines of sixteenths */ +}; + +#endif // TBEAMOBJECT_H diff --git a/src/libs/core/score/tmeasureobject.cpp b/src/libs/core/score/tmeasureobject.cpp index 56cc7aee10bf0d3d23e1f5725f30cf264c5b937e..ca96dea153335b1d0eee6e612ae984bd054c3973 100644 --- a/src/libs/core/score/tmeasureobject.cpp +++ b/src/libs/core/score/tmeasureobject.cpp @@ -21,6 +21,7 @@ #include "tstaffobject.h" #include "tnoteobject.h" #include "tnotepair.h" +#include "tbeamobject.h" #include "music/tmeter.h" #include "music/tnote.h" @@ -58,7 +59,7 @@ void TmeasureObject::setStaff(TstaffObject* st) { if (m_staff != st) { m_staff = st; for (TnotePair* np : m_notes) - np->object()->setStaff(m_staff); + np->item()->setStaff(m_staff); } } @@ -69,16 +70,25 @@ void TmeasureObject::appendNewNotes(int segmentId, int count) { for (int n = segmentId; n < segmentId + count; ++n) m_notes.append(m_score->noteSegment(n)); updateRhythmicGroups(); - // resolve beaming + int grWithBeam = beamGroup(segmentId); for (int n = segmentId; n < segmentId + count; ++n) { auto np = m_score->noteSegment(n); - auto noteObject = new TnoteObject(m_staff); - noteObject->setIndex(np->index()); + auto noteObject = new TnoteObject(m_staff, np); noteObject->setMeasure(this); np->setNoteObject(noteObject); checkAccidentals(); noteObject->setNote(*np->note()); } + if (grWithBeam > -1) { + auto firstInGrId = m_score->noteSegment(firstNoteId() + m_firstInGr[grWithBeam])->index(); + while (firstInGrId < m_score->notesCount()) { + auto ns = m_score->noteSegment(firstInGrId); + if (ns->beam()) { + ns->beam()->prepareBeam(); + break; + } + } + } refresh(); m_staff->refresh(); checkBarLine(); @@ -102,19 +112,19 @@ void TmeasureObject::insertNote(int id, TnotePair* np) { void TmeasureObject::keySignatureChanged() { for (int n = 0; n < m_notes.size(); ++n) { - m_notes[n]->object()->keySignatureChanged(); + m_notes[n]->item()->keySignatureChanged(); } refresh(); } int TmeasureObject::firstNoteId() const { - return m_notes.count() ? m_notes.first()->object()->index() : 0; + return m_notes.count() ? m_notes.first()->index() : 0; } int TmeasureObject::lastNoteId() const { - return m_notes.count() ? m_notes.last()->object()->index() : 0; + return m_notes.count() ? m_notes.last()->index() : 0; } @@ -158,7 +168,7 @@ void TmeasureObject::updateRhythmicGroups() { void TmeasureObject::checkBarLine() { if (m_free == 0) { - auto lastNote = last()->object(); + auto lastNote = last()->item(); if (!m_barLine) { QQmlEngine engine; QQmlComponent comp(&engine, this); @@ -168,7 +178,7 @@ void TmeasureObject::checkBarLine() { m_barLine->setProperty("color", lastNote->color()); m_barLine->setY(m_staff->upperLine()); } - qreal xOff = lastNote == m_staff->lastMeasure()->last()->object() ? 0.2 : 0.0; // fit line at the staff end + qreal xOff = lastNote == m_staff->lastMeasure()->last()->item() ? 0.2 : 0.0; // fit line at the staff end m_barLine->setX(lastNote->rightX() - lastNote->x() + xOff); } } @@ -178,7 +188,7 @@ void TmeasureObject::refresh() { m_gapsSum = 0.0; m_allNotesWidth = 0.0; for (int n = 0; n < m_notes.size(); ++n) { - auto noteObj = note(n)->object(); + auto noteObj = note(n)->item(); m_gapsSum += noteObj->rhythmFactor(); m_allNotesWidth += noteObj->width(); } @@ -195,6 +205,29 @@ void TmeasureObject::checkAccidentals() { } +int TmeasureObject::beamGroup(int segmentId) { + int currGr = m_score->noteSegment(segmentId)->rhythmGroup(); + int segId = m_firstInGr[currGr] + 1; + int grWithBeam = -1; + while (segId < m_notes.count() && m_notes[segId]->rhythmGroup() == currGr) { + auto noteSeg = m_notes[segId]; + auto prevSeg = m_notes[segId - 1]; + if (!noteSeg->note()->isRest() && !prevSeg->note()->isRest() // not a rest + && noteSeg->note()->rhythm() > Trhythm::Quarter // sixteenth or eighth + && prevSeg->note()->rhythm() > Trhythm::Quarter) + { + if (prevSeg->note()->rtm.beam() == Trhythm::e_noBeam) // start beam group + prevSeg->setBeam(new TbeamObject(prevSeg, this)); + auto beam = prevSeg->beam(); + if (noteSeg->beam() == nullptr) + beam->addNote(noteSeg); + grWithBeam = currGr; + } + segId++; + } + return grWithBeam; +} + //################################################################################################# //################### PRIVATE ############################################ //################################################################################################# diff --git a/src/libs/core/score/tmeasureobject.h b/src/libs/core/score/tmeasureobject.h index acf0b6819946caebc1b07b862c4650759576a261..7ed22818791ac6c93fc3fee22b623df1de99ce68 100644 --- a/src/libs/core/score/tmeasureobject.h +++ b/src/libs/core/score/tmeasureobject.h @@ -29,6 +29,7 @@ class TscoreObject; class TstaffObject; class TnoteObject; class TnotePair; +class TbeamObject; /** @@ -122,6 +123,13 @@ protected: qint8 accidState(int noteNr) { return m_accidsState[noteNr]; } + /** + * Checks notes rhythms of group @p segmentId belongs to + * for 8ths and 16ths and crates beams (@p TbeamObject) if they occurs + * It can be called before @p TnoteObject will be created + */ + int beamGroup(int segmentId); + private: void clearAccidState();