221 lines
8.3 KiB
C++
221 lines
8.3 KiB
C++
/*
|
|
* SPDX-License-Identifier: GPL-3.0-only
|
|
* MuseScore-CLA-applies
|
|
*
|
|
* MuseScore
|
|
* Music Composition & Notation
|
|
*
|
|
* Copyright (C) 2021 MuseScore BVBA and others
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
#include "layoutharmonies.h"
|
|
|
|
#include <map>
|
|
#include <vector>
|
|
|
|
#include "realfn.h"
|
|
#include "containers.h"
|
|
|
|
#include "libmscore/fret.h"
|
|
#include "libmscore/harmony.h"
|
|
#include "libmscore/measurebase.h"
|
|
#include "libmscore/segment.h"
|
|
#include "libmscore/system.h"
|
|
|
|
using namespace mu::engraving;
|
|
using namespace Ms;
|
|
|
|
void LayoutHarmonies::layoutHarmonies(const std::vector<Segment*>& sl)
|
|
{
|
|
for (const Segment* s : sl) {
|
|
for (EngravingItem* e : s->annotations()) {
|
|
if (e->isHarmony()) {
|
|
Harmony* h = toHarmony(e);
|
|
// For chord symbols that coincide with a chord or rest,
|
|
// a partial layout can also happen (if needed) during ChordRest layout
|
|
// in order to calculate a bbox and allocate its shape to the ChordRest.
|
|
// But that layout (if it happens at all) does not do autoplace,
|
|
// so we need the full layout here.
|
|
h->layout();
|
|
h->autoplaceSegmentElement();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LayoutHarmonies::alignHarmonies(const System* system, const std::vector<Segment*>& sl, bool harmony, const double maxShiftAbove,
|
|
const double maxShiftBelow)
|
|
{
|
|
// Help class.
|
|
// Contains harmonies/fretboard per segment.
|
|
class HarmonyList : public std::vector<EngravingItem*>
|
|
{
|
|
std::map<const Segment*, std::vector<EngravingItem*> > elements;
|
|
std::vector<EngravingItem*> modified;
|
|
|
|
EngravingItem* getReferenceElement(const Segment* s, bool above, bool visible) const
|
|
{
|
|
// Returns the reference element for aligning.
|
|
// When a segments contains multiple harmonies/fretboard, the lowest placed
|
|
// element (for placement above, otherwise the highest placed element) is
|
|
// used for alignment.
|
|
EngravingItem* element { nullptr };
|
|
for (EngravingItem* e : elements.at(s)) {
|
|
// Only chord symbols have styled offset, fretboards don't.
|
|
if (!e->autoplace() || (e->isHarmony() && !e->isStyled(Pid::OFFSET)) || (visible && !e->visible())) {
|
|
continue;
|
|
}
|
|
if (!element) {
|
|
element = e;
|
|
} else {
|
|
if ((e->placeAbove() && above && (element->y() < e->y()))
|
|
|| (e->placeBelow() && !above && (element->y() > e->y()))) {
|
|
element = e;
|
|
}
|
|
}
|
|
}
|
|
return element;
|
|
}
|
|
|
|
public:
|
|
HarmonyList()
|
|
{
|
|
elements.clear();
|
|
modified.clear();
|
|
}
|
|
|
|
void append(const Segment* s, EngravingItem* e)
|
|
{
|
|
elements[s].push_back(e);
|
|
}
|
|
|
|
qreal getReferenceHeight(bool above) const
|
|
{
|
|
// The reference height is the height of
|
|
// the lowest element if placed above
|
|
// or
|
|
// the highest element if placed below.
|
|
bool first { true };
|
|
qreal ref { 0.0 };
|
|
for (auto s : mu::keys(elements)) {
|
|
EngravingItem* e { getReferenceElement(s, above, true) };
|
|
if (!e) {
|
|
continue;
|
|
}
|
|
if (e->placeAbove() && above) {
|
|
ref = first ? e->y() : qMin(ref, e->y());
|
|
first = false;
|
|
} else if (e->placeBelow() && !above) {
|
|
ref = first ? e->y() : qMax(ref, e->y());
|
|
first = false;
|
|
}
|
|
}
|
|
return ref;
|
|
}
|
|
|
|
bool align(bool above, qreal reference, qreal maxShift)
|
|
{
|
|
// Align the elements. If a segment contains multiple elements,
|
|
// only the reference elements is used in the algorithm. All other
|
|
// elements will remain their original placement with respect to
|
|
// the reference element.
|
|
bool moved { false };
|
|
if (mu::RealIsNull(reference)) {
|
|
return moved;
|
|
}
|
|
|
|
for (auto s : mu::keys(elements)) {
|
|
std::list<EngravingItem*> handled;
|
|
EngravingItem* be = getReferenceElement(s, above, false);
|
|
if (!be) {
|
|
// If there are only invisible elements, we have to use an invisible
|
|
// element for alignment reference.
|
|
be = getReferenceElement(s, above, true);
|
|
}
|
|
if (be && ((above && (be->y() < (reference + maxShift))) || ((!above && (be->y() > (reference - maxShift)))))) {
|
|
qreal shift = be->rypos();
|
|
be->rypos() = reference - be->ryoffset();
|
|
shift -= be->rypos();
|
|
for (EngravingItem* e : elements[s]) {
|
|
if ((above && e->placeBelow()) || (!above && e->placeAbove())) {
|
|
continue;
|
|
}
|
|
modified.push_back(e);
|
|
handled.push_back(e);
|
|
moved = true;
|
|
if (e != be) {
|
|
e->rypos() -= shift;
|
|
}
|
|
}
|
|
for (auto e : handled) {
|
|
mu::remove(elements[s], e);
|
|
}
|
|
}
|
|
}
|
|
return moved;
|
|
}
|
|
|
|
void addToSkyline(const System* system)
|
|
{
|
|
for (EngravingItem* e : qAsConst(modified)) {
|
|
const Segment* s = toSegment(e->explicitParent());
|
|
const MeasureBase* m = toMeasureBase(s->explicitParent());
|
|
system->staff(e->staffIdx())->skyline().add(e->shape().translated(e->pos() + s->pos() + m->pos()));
|
|
if (e->isFretDiagram()) {
|
|
FretDiagram* fd = toFretDiagram(e);
|
|
Harmony* h = fd->harmony();
|
|
if (h) {
|
|
system->staff(e->staffIdx())->skyline().add(h->shape().translated(h->pos() + fd->pos() + s->pos() + m->pos()));
|
|
} else {
|
|
system->staff(e->staffIdx())->skyline().add(fd->shape().translated(fd->pos() + s->pos() + m->pos()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if (RealIsNull(maxShiftAbove) && RealIsNull(maxShiftBelow)) {
|
|
return;
|
|
}
|
|
|
|
// Collect all fret diagrams and chord symbol and store them per staff.
|
|
// In the same pass, the maximum height is collected.
|
|
std::map<staff_idx_t, HarmonyList> staves;
|
|
for (const Segment* s : sl) {
|
|
for (EngravingItem* e : s->annotations()) {
|
|
if ((harmony && e->isHarmony()) || (!harmony && e->isFretDiagram())) {
|
|
staves[e->staffIdx()].append(s, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (staff_idx_t idx : mu::keys(staves)) {
|
|
// Align the objects.
|
|
// Algorithm:
|
|
// - Find highest placed harmony/fretdiagram.
|
|
// - Align all harmony/fretdiagram objects placed between height and height-maxShiftAbove.
|
|
// - Repeat for all harmony/fretdiagram objects below height-maxShiftAbove.
|
|
bool moved { true };
|
|
int pass { 0 };
|
|
while (moved && (pass++ < 10)) {
|
|
moved = false;
|
|
moved |= staves[idx].align(true, staves[idx].getReferenceHeight(true), maxShiftAbove);
|
|
moved |= staves[idx].align(false, staves[idx].getReferenceHeight(false), maxShiftBelow);
|
|
}
|
|
|
|
// Add all aligned objects to the sky line.
|
|
staves[idx].addToSkyline(system);
|
|
}
|
|
}
|