Skip to content

Commit

Permalink
Optimized chord symbol/fretboard alignment algorithm:
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
njvdberg authored and vpereverzev committed Jul 14, 2020
1 parent f7a15ae commit 0c5221d
Showing 1 changed file with 132 additions and 56 deletions.
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

0 comments on commit 0c5221d

Please sign in to comment.