From 0c5221d9d5ced18bc9fe41458c1d3d057a438265 Mon Sep 17 00:00:00 2001 From: Niek van den Berg Date: Wed, 1 Jul 2020 14:37:05 +0200 Subject: [PATCH] Optimized chord symbol/fretboard alignment algorithm: 1) Supports multiple elements at same segment. 2) Works on autoplaced elements only. 3) Invisible elements will be aligned but are not used as a reference. 4) Manual placed elements will be ignored for reference but will be moved and will remain its relative position with respect to the aligned element. --- libmscore/layout.cpp | 188 ++++++++++++++++++++++++++++++------------- 1 file changed, 132 insertions(+), 56 deletions(-) diff --git a/libmscore/layout.cpp b/libmscore/layout.cpp index 9e19a1dfcbf6b..670430e61732b 100644 --- a/libmscore/layout.cpp +++ b/libmscore/layout.cpp @@ -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& sl, bool harmony, const qreal maxShiftAbove, const qreal maxShiftBelow) { + + // Help class. + // Contains harmonies/fretboard per segment. + class HarmonyList : public QList { + QMap> elements; + QList 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 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> staves; + QMap 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); } } @@ -3325,63 +3448,16 @@ void alignHarmonies(const System* system, const std::vector& 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 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 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); } }