Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimized chord symbol/fretboard alignment algorithm #6321

Merged
merged 1 commit into from
Jul 14, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 132 additions & 56 deletions libmscore/layout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3300,22 +3300,145 @@ bool inline almostZero(qreal value)
// 1e-3 is close enough to zero to see it as zero.
return value > -1e-3 && value < 1e-3;
}

//---------------------------------------------------------

// alignHarmonies
//---------------------------------------------------------

void alignHarmonies(const System* system, const std::vector<Segment*>& sl, bool harmony, const qreal maxShiftAbove, const qreal maxShiftBelow)
{

// Help class.
// Contains harmonies/fretboard per segment.
class HarmonyList : public QList<Element*> {
QMap<const Segment*, QList<Element*>> elements;
QList<Element*> modified;

Element* 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.
Element* element { nullptr };
for (Element* e : elements[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, Element* e)
{
elements[s].append(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 : elements.keys()) {
Element* 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 (almostZero(reference))
return moved;

for (auto s : elements.keys()) {
QList<Element*> handled;
Element* 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 (Element* e : elements[s]) {
if ((above && e->placeBelow()) || (!above && e->placeAbove()))
continue;
modified.append(e);
handled.append(e);
moved = true;
if (e != be)
e->rypos() -= shift;
}
for (auto e : handled)
elements[s].removeOne(e);
}
}
return moved;
}

void addToSkyline(const System* system)
{
for (Element* e : modified) {
const Segment* s = toSegment(e->parent());
const MeasureBase* m = toMeasureBase(s->parent());
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 (almostZero(maxShiftAbove) && almostZero(maxShiftBelow))
return;

// Collect all fret diagrams and chord symbol and store them per staff.
// In the same pass, the maximum height is collected.
QMap<int, QList<Element*>> staves;
QMap<int, HarmonyList> staves;
for (const Segment* s : sl) {
for (Element* e : s->annotations()) {
if ((harmony && e->isHarmony()) || (!harmony && e->isFretDiagram()))
staves[e->staffIdx()].append(e);
staves[e->staffIdx()].append(s, e);
}
}

Expand All @@ -3325,63 +3448,16 @@ void alignHarmonies(const System* system, const std::vector<Segment*>& sl, bool
// - Find highest placed harmony/fretdiagram.
// - Align all harmony/fretdiagram objects placed between height and height-maxShiftAbove.
// - Repeat for all harmony/fretdiagram objects below heigt-maxShiftAbove.
QList<Element*> modified;
while (!staves[idx].isEmpty()) {
// Pass 1, find highest/lowest place harmony.
qreal referencePositionAbove { 0.0 };
qreal referencePositionBelow { 0.0 };
for (Element* e : staves[idx]) {
if (e->placeAbove() && (referencePositionAbove > e->y()))
referencePositionAbove = e->y();
else if (e->placeBelow() && (referencePositionBelow < e->y()))
referencePositionBelow = e->y();
}

if (almostZero(referencePositionAbove) && almostZero(referencePositionBelow))
break;

// Pass 2, align all object placed between referencePosition and referencePosition-maxShiftAbove
QList<Element*> again;
while (!staves[idx].isEmpty()) {
Element* e = staves[idx].takeFirst();
if (e->placeAbove() && !almostZero(maxShiftAbove)) {
if (e->y() < (referencePositionAbove + maxShiftAbove)) {
e->rypos() = referencePositionAbove - e->ryoffset();
if (e->addToSkyline())
modified.append(e);
}
else {
again.append(e);
}
}
else if (e->placeBelow() && !almostZero(maxShiftBelow)) {
if (e->y() > (referencePositionBelow - maxShiftBelow)) {
e->rypos() = referencePositionBelow - e->ryoffset();
if (e->addToSkyline())
modified.append(e);
}
else {
again.append(e);
}
}
}
staves[idx].append(again);
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.
for (Element* e : modified) {
const Segment* s = toSegment(e->parent());
const MeasureBase* m = toMeasureBase(s->parent());
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()));
}
}
staves[idx].addToSkyline(system);
}
}

Expand Down