diff --git a/src/engraving/layout/layoutbeams.cpp b/src/engraving/layout/layoutbeams.cpp index 5cc7beac40cb7..8cf248b52422c 100644 --- a/src/engraving/layout/layoutbeams.cpp +++ b/src/engraving/layout/layoutbeams.cpp @@ -26,6 +26,7 @@ #include "containers.h" #include "libmscore/beam.h" +#include "libmscore/tremolo.h" #include "libmscore/chord.h" #include "libmscore/factory.h" #include "libmscore/measure.h" @@ -503,6 +504,18 @@ void LayoutBeams::layoutNonCrossBeams(Segment* s) // layout beam if (LayoutBeams::isTopBeam(cr)) { cr->beam()->layout(); + if (!cr->beam()->tremAnchors().empty()) { + // there are inset tremolos in here + for (ChordRest* beamCr : cr->beam()->elements()) { + if (!beamCr->isChord()) { + continue; + } + Chord* c = toChord(beamCr); + if (c->tremolo() && c->tremolo()->twoNotes()) { + c->tremolo()->layout(); + } + } + } } if (!cr->isChord()) { continue; diff --git a/src/engraving/layout/layoutsystem.cpp b/src/engraving/layout/layoutsystem.cpp index 6ec5fda4b9a61..8bcc17d3bf2c1 100644 --- a/src/engraving/layout/layoutsystem.cpp +++ b/src/engraving/layout/layoutsystem.cpp @@ -1495,6 +1495,18 @@ void LayoutSystem::updateCrossBeams(System* system, const LayoutContext& ctx) LayoutChords::layoutChords1(chord->score(), &seg, chord->vStaffIdx()); seg.createShape(chord->vStaffIdx()); } + } else if (chord->tremolo() && chord->tremolo()->twoNotes()) { + Tremolo* t = chord->tremolo(); + Chord* c1 = t->chord1(); + Chord* c2 = t->chord2(); + if (t->userModified() || (c1->staffMove() != 0 || c2->staffMove() != 0)) { + bool prevUp = chord->up(); + chord->computeUp(); + if (chord->up() != prevUp) { + LayoutChords::layoutChords1(chord->score(), &seg, chord->vStaffIdx()); + seg.createShape(chord->vStaffIdx()); + } + } } } } diff --git a/src/engraving/libmscore/beam.cpp b/src/engraving/libmscore/beam.cpp index 2edf032e4ea60..9847f834fbd13 100644 --- a/src/engraving/libmscore/beam.cpp +++ b/src/engraving/libmscore/beam.cpp @@ -62,18 +62,6 @@ static const ElementStyle beamStyle { { Sid::beamNoSlope, Pid::BEAM_NO_SLOPE }, }; -//--------------------------------------------------------- -// BeamFragment -// position of primary beam -// idx 0 - DirectionV::AUTO or DirectionV::DOWN -// 1 - DirectionV::UP -//--------------------------------------------------------- - -struct BeamFragment { - double py1[2]; - double py2[2]; -}; - //--------------------------------------------------------- // Beam //--------------------------------------------------------- @@ -111,6 +99,7 @@ Beam::Beam(const Beam& b) _isGrace = b._isGrace; _cross = b._cross; _slope = b._slope; + _layoutInfo = b._layoutInfo; } //--------------------------------------------------------- @@ -505,247 +494,56 @@ void Beam::layout() } } -int Beam::getMiddleStaffLine(ChordRest* startChord, ChordRest* endChord, int staffLines) const +PointF Beam::chordBeamAnchor(const ChordRest* chord, BeamTremoloLayout::ChordBeamAnchorType anchorType) const { - bool useWideBeams = score()->styleB(Sid::useWideBeams); - bool isFullSize = RealIsEqual(_mag, 1.0) && !_isGrace; - int startMiddleLine = Chord::minStaffOverlap(_up, staffLines, startChord->beams(), false, _beamSpacing / 4.0, useWideBeams, isFullSize); - int endMiddleLine = Chord::minStaffOverlap(_up, staffLines, endChord->beams(), false, _beamSpacing / 4.0, useWideBeams, isFullSize); - - // offset middle line by 1 or -1 since the anchor is at the middle of the beam, - // not at the tip of the stem - if (_up) { - return std::min(startMiddleLine, endMiddleLine) + 1; - } - return std::max(startMiddleLine, endMiddleLine) - 1; + return _layoutInfo.chordBeamAnchor(chord, anchorType); } -int Beam::computeDesiredSlant(int startNote, int endNote, int middleLine, int dictator, int pointer) const +double Beam::chordBeamAnchorY(const ChordRest* chord) const { - if (noSlope()) { - return 0; - } - int dictatorExtension = middleLine - dictator; // we need to make sure that beams extended to the middle line - int pointerExtension = middleLine - pointer; // are properly treated as flat. - if (_up) { - dictatorExtension = std::min(dictatorExtension, 0); - pointerExtension = std::min(pointerExtension, 0); - } else { - dictatorExtension = std::max(dictatorExtension, 0); - pointerExtension = std::max(pointerExtension, 0); - } - if (dictator + dictatorExtension == middleLine && pointer + pointerExtension == middleLine) { - return 0; - } - if (startNote == endNote) { - return 0; - } - int slopeConstrained = isSlopeConstrained(startNote, endNote); - if (slopeConstrained == 0) { - return 0; - } else if (slopeConstrained == 1) { - return dictator > pointer ? -1 : 1; - } - - // calculate max slope based on distance between first and last chords - int maxSlope = getMaxSlope(); - - // calculate max slope based on note interval - int interval = std::min(std::abs(endNote - startNote), (int)_maxSlopes.size() - 1); - return std::min(maxSlope, _maxSlopes[interval]) * (_up ? 1 : -1); + return _layoutInfo.chordBeamAnchorY(chord); } -int Beam::isSlopeConstrained(int startNote, int endNote) const +void Beam::setTremAnchors() { - // 0 to constrain to flat, 1 to constrain to 0.25, <0 for no constraint - if (startNote == endNote) { - return 0; - } - // if a note is more extreme than the endpoints, slope is 0 - // p.s. _notes is a sorted vector - if (_notes.size() > 2) { - if (_up) { - int higherEnd = std::min(startNote, endNote); - if (higherEnd > _notes[0]) { - return 0; // a note is higher in the staff than the highest end - } - if (higherEnd == _notes[0] && higherEnd >= _notes[1]) { - if (higherEnd > _notes[1]) { - return 0; // a note is higher in the staff than the highest end - } - size_t chordCount = _elements.size(); - if (chordCount >= 3 && _notes.size() >= 3) { - bool middleNoteHigherThanHigherEnd = higherEnd >= _notes[2]; - if (middleNoteHigherThanHigherEnd) { - return 0; // two notes are the same as the highest end (notes [0] [1] and [2] higher than or same as higherEnd) - } - bool secondNoteSameHeightAsHigherEnd = startNote < endNote && _elements[1]->isChord() - && toChord(_elements[1])->upLine() == higherEnd; - bool secondToLastNoteSameHeightAsHigherEnd = endNote < startNote && _elements[chordCount - 2]->isChord() && toChord( - _elements[chordCount - 2])->upLine() == higherEnd; - if (!(secondNoteSameHeightAsHigherEnd || secondToLastNoteSameHeightAsHigherEnd)) { - return 0; // only one note same as higher end, but it is not a neighbor - } else { - // there is a single note next to the highest one with equivalent height - // and they are neighbors. this is our exception, so - // the slope may be a max of 0.25. - return 1; - } - } else { - return 0; // only two notes in entire beam, in this case startNote == endNote - } - } - } else { - int lowerEnd = std::max(startNote, endNote); - if (lowerEnd < _notes[_notes.size() - 1]) { - return 0; - } - if (lowerEnd == _notes[_notes.size() - 1] && lowerEnd <= _notes[_notes.size() - 2]) { - if (lowerEnd < _notes[_notes.size() - 2]) { - return 0; - } - size_t chordCount = _elements.size(); - if (chordCount >= 3 && _notes.size() >= 3) { - bool middleNoteLowerThanLowerEnd = lowerEnd <= _notes[_notes.size() - 3]; - if (middleNoteLowerThanLowerEnd) { - return 0; - } - bool secondNoteSameHeightAsLowerEnd = startNote > endNote && _elements[1]->isChord() - && toChord(_elements[1])->downLine() == lowerEnd; - bool secondToLastNoteSameHeightAsLowerEnd = endNote > startNote && _elements[chordCount - 2]->isChord() && toChord( - _elements[chordCount - 2])->downLine() == lowerEnd; - if (!(secondNoteSameHeightAsLowerEnd || secondToLastNoteSameHeightAsLowerEnd)) { - return 0; - } else { - return 1; - } - } else { - return 0; - } - } - } - } - return -1; -} - -int Beam::getMaxSlope() const -{ - // for 2-indexed interval i (seconds, thirds, etc.) - // maxSlopes[i] = max slope of beam for notes with interval i - - // calculate max slope based on distance between first and last chords - double endX = chordBeamAnchorX(_elements[_elements.size() - 1], ChordBeamAnchorType::Start); - double startX = chordBeamAnchorX(_elements[0], ChordBeamAnchorType::End); - double beamWidth = endX - startX; - beamWidth /= spatium(); - int maxSlope = _maxSlopes.back(); - if (beamWidth < 3.0) { - maxSlope = _maxSlopes[1]; - } else if (beamWidth < 5.0) { - maxSlope = _maxSlopes[2]; - } else if (beamWidth < 7.5) { - maxSlope = _maxSlopes[3]; - } else if (beamWidth < 10.0) { - maxSlope = _maxSlopes[4]; - } else if (beamWidth < 15.0) { - maxSlope = _maxSlopes[5]; - } else if (beamWidth < 20.0) { - maxSlope = _maxSlopes[6]; - } else { - maxSlope = _maxSlopes[7]; - } - - return maxSlope; -} - -int Beam::getBeamCount(const std::vector chordRests) const -{ - int beamCount = 0; - for (ChordRest* chordRest : chordRests) { - if (chordRest->isChord() && toChord(chordRest)->beams() > beamCount) { - beamCount = toChord(chordRest)->beams(); - } - } - return beamCount; -} - -double Beam::chordBeamAnchorX(const ChordRest* cr, ChordBeamAnchorType anchorType) const -{ - double stemPosX = cr->stemPosX() + cr->pagePos().x() - pagePos().x(); - - if (!cr->isChord() || !toChord(cr)->stem()) { - if (!_up) { - // rests always return the right side of the glyph as their stemPosX - // so we need to adjust back to the left side if stems are down - stemPosX -= cr->stemPosX(); - } - return stemPosX; - } - const Chord* chord = toChord(cr); - - double stemWidth = chord->stem()->lineWidth().val() * chord->mag(); - - switch (anchorType) { - case ChordBeamAnchorType::Start: - if (_tab) { - return stemPosX - 0.5 * stemWidth; - } - - if (chord->up()) { - return stemPosX - stemWidth; - } - - break; - case ChordBeamAnchorType::Middle: - if (_tab) { - return stemPosX; - } - - return chord->up() ? stemPosX - 0.5 * stemWidth : stemPosX + 0.5 * stemWidth; - - case ChordBeamAnchorType::End: - if (_tab) { - return stemPosX + 0.5 * stemWidth; + _tremAnchors.clear(); + for (ChordRest* cr : _elements) { + if (!cr || !cr->isChord()) { + continue; } + Chord* c = toChord(cr); + Tremolo* t = c ? c->tremolo() : nullptr; + if (t && t->twoNotes() && t->chord1() == c && t->chord2()->beam() == this) { + // there is an inset tremolo here! + // figure out up / down + bool tremUp = t->up(); + int fragmentIndex = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + if (_userModified[fragmentIndex]) { + tremUp = c->up(); + } else if (_cross && t->chord1()->staffMove() == t->chord2()->staffMove()) { + tremUp = t->chord1()->staffMove() == _maxMove; + } + TremAnchor tremAnchor; + tremAnchor.chord1 = c; + int regularBeams = c->beams(); // non-tremolo strokes - if (!chord->up()) { - return stemPosX + stemWidth; + // find the left-side anchor + double width = _endAnchor.x() - _startAnchor.x(); + double height = _endAnchor.y() - _startAnchor.y(); + double x = chordBeamAnchor(c, BeamTremoloLayout::ChordBeamAnchorType::Middle).x(); + double proportionAlongX = (x - _startAnchor.x()) / width; + double y = _startAnchor.y() + (proportionAlongX * height); + y += regularBeams * (score()->styleB(Sid::useWideBeams) ? 1.0 : 0.75) * spatium() * (tremUp ? 1. : -1.); + tremAnchor.y1 = y; + // find the right-side anchor + x = chordBeamAnchor(t->chord2(), BeamTremoloLayout::ChordBeamAnchorType::Middle).x(); + proportionAlongX = (x - _startAnchor.x()) / width; + y = _startAnchor.y() + (proportionAlongX * height); + y += regularBeams * (score()->styleB(Sid::useWideBeams) ? 1.0 : 0.75) * spatium() * (tremUp ? 1. : -1.); + tremAnchor.y2 = y; + _tremAnchors.push_back(tremAnchor); } - - break; - } - - return stemPosX; -} - -double Beam::chordBeamAnchorY(const ChordRest* cr) const -{ - if (!cr->isChord()) { - Shape restShape = cr->shape().translated(cr->pagePos()); - return _up ? restShape.top() : restShape.bottom(); - } - - const Chord* chord = toChord(cr); - Note* note = cr->up() ? chord->downNote() : chord->upNote(); - PointF position = note->pagePos(); - - int upValue = chord->up() ? -1 : 1; - double beamOffset = _beamWidth / 2 * upValue; - - if (_isBesideTabStaff) { - double stemLength = _tab->chordStemLength(chord) * (_up ? -1 : 1); - double y = _tab->chordRestStemPosY(chord) + stemLength; - y *= spatium(); - y -= beamOffset; - return y + chord->pagePos().y(); } - - return position.y() + (chord->defaultStemLength() * upValue) - beamOffset; -} - -PointF Beam::chordBeamAnchor(const ChordRest* cr, ChordBeamAnchorType anchorType) const -{ - return PointF(chordBeamAnchorX(cr, anchorType), chordBeamAnchorY(cr)); } void Beam::createBeamSegment(ChordRest* startCr, ChordRest* endCr, int level) @@ -795,8 +593,8 @@ void Beam::createBeamSegment(ChordRest* startCr, ChordRest* endCr, int level) overallUp = firstUp; } - const double startX = chordBeamAnchorX(startCr, ChordBeamAnchorType::Start); - const double endX = chordBeamAnchorX(endCr, ChordBeamAnchorType::End); + const double startX = _layoutInfo.chordBeamAnchorX(startCr, BeamTremoloLayout::ChordBeamAnchorType::Start); + const double endX = _layoutInfo.chordBeamAnchorX(endCr, BeamTremoloLayout::ChordBeamAnchorType::End); double startY = _slope * (startX - _startAnchor.x()) + _startAnchor.y() - pagePos().y(); double endY = _slope * (endX - _startAnchor.x()) + _startAnchor.y() - pagePos().y(); @@ -860,7 +658,7 @@ void Beam::createBeamSegment(ChordRest* startCr, ChordRest* endCr, int level) if (level > 0) { double grow = _grow1; if (!RealIsEqual(_grow1, _grow2)) { - double anchorX = chordBeamAnchorX(chord, ChordBeamAnchorType::Middle); + double anchorX = _layoutInfo.chordBeamAnchorX(chord, BeamTremoloLayout::ChordBeamAnchorType::Middle); double proportionAlongX = (anchorX - _startAnchor.x()) / (_endAnchor.x() - _startAnchor.x()); grow = proportionAlongX * (_grow2 - _grow1) + _grow1; } @@ -870,7 +668,7 @@ void Beam::createBeamSegment(ChordRest* startCr, ChordRest* endCr, int level) } if (level == 0 || !RealIsEqual(addition, 0.0)) { - extendStem(chord, addition); + _layoutInfo.extendStem(chord, addition); } if (chord == endCr) { @@ -968,7 +766,8 @@ bool Beam::calcIsBeamletBefore(Chord* chord, int i, int level, bool isAfter32Bre void Beam::createBeamletSegment(ChordRest* cr, bool isBefore, int level) { - const double startX = chordBeamAnchorX(cr, isBefore ? ChordBeamAnchorType::End : ChordBeamAnchorType::Start); + const double startX = _layoutInfo.chordBeamAnchorX(cr, + isBefore ? BeamTremoloLayout::ChordBeamAnchorType::End : BeamTremoloLayout::ChordBeamAnchorType::Start); const double beamletLength = score()->styleMM(Sid::beamMinLen).val() * cr->mag(); @@ -1124,355 +923,13 @@ void Beam::createBeamSegments(const std::vector& chordRests) } while (levelHasBeam); } -void Beam::offsetBeamToRemoveCollisions(const std::vector chordRests, int& dictator, int& pointer, - const double startX, const double endX, - bool isFlat, bool isStartDictator) const -{ - if (_cross) { - return; - } - - if (endX == startX) { - // zero-length beams? - return; - } - - // tolerance eliminates all possibilities of floating point rounding errors - const double tolerance = _beamWidth * 0.25 * (_up ? -1 : 1); - bool isSmall = _isGrace || mag() < 1.; - - double startY = (isStartDictator ? dictator : pointer) * spatium() / 4 + tolerance; - double endY = (isStartDictator ? pointer : dictator) * spatium() / 4 + tolerance; - - for (ChordRest* chordRest : chordRests) { - if (chordRest == _elements.back() || chordRest == _elements.front()) { - continue; - } - if (chordRest->isRest() && !toRest(chordRest)->verticalClearance().locked()) { - continue; - } - - PointF anchor = chordBeamAnchor(chordRest, ChordBeamAnchorType::Middle) - pagePos(); - - int slope = abs(dictator - pointer); - double reduction = 0.0; - if (chordRest->isChord() && !isFlat) { - if (slope <= 3) { - reduction = 0.25 * spatium(); - } else if (slope <= 6) { - reduction = 0.5 * spatium(); - } else { // slope > 6 - reduction = 0.75 * spatium(); - } - } - double restClearMargin = 0.0; - if (chordRest->isRest()) { - const double restToBeamPadding = 0.5 * spatium(); // TODO: style setting - restClearMargin = _beamSegments.size() * _beamWidth + (_beamSegments.size() - 1) * _beamSpacing + restToBeamPadding; - } - - if (endX != startX) { - // avoid division by zero for zero-length beams (can exist as a pre-layout state used - // for horizontal spacing computations) - double proportionAlongX = (anchor.x() - startX) / (endX - startX); - - while (true) { - double desiredY = proportionAlongX * (endY - startY) + startY; - bool beamClearsAnchor = (_up && RealIsEqualOrLess(desiredY, anchor.y() + reduction - restClearMargin)) - || (!_up && RealIsEqualOrMore(desiredY, anchor.y() - reduction + restClearMargin)); - if (beamClearsAnchor) { - break; - } - - if (isFlat || (isSmall && dictator == pointer)) { - dictator += _up ? -1 : 1; - pointer += _up ? -1 : 1; - } else if (std::abs(dictator - pointer) == 1) { - dictator += _up ? -1 : 1; - } else { - pointer += _up ? -1 : 1; - } - - startY = (isStartDictator ? dictator : pointer) * spatium() / 4 + tolerance; - endY = (isStartDictator ? pointer : dictator) * spatium() / 4 + tolerance; - } - } - } -} - -void Beam::offsetBeamWithAnchorShortening(std::vector chordRests, int& dictator, int& pointer, int staffLines, - bool isStartDictator, int stemLengthDictator) const -{ - Chord* startChord = nullptr; - Chord* endChord = nullptr; - for (ChordRest* cr : chordRests) { - if (cr->isChord()) { - endChord = toChord(cr); - if (!startChord) { - startChord = toChord(cr); - } - } - } - if (!startChord) { - // beam full of only rests, don't adjust this - return; - } - // min stem lengths according to how many beams there are (starting with 1) - static const int minStemLengths[] = { 11, 13, 15, 18, 21, 24, 27, 30 }; - const int middleLine = getMiddleStaffLine(startChord, endChord, staffLines); - int maxDictatorReduce = stemLengthDictator - minStemLengths[(isStartDictator ? startChord : endChord)->beams() - 1]; - maxDictatorReduce = std::min(abs(dictator - middleLine), maxDictatorReduce); - - bool isFlat = dictator == pointer; - bool isAscending = startChord->line() > endChord->line(); - int towardBeam = _up ? -1 : 1; - int newDictator = dictator; - int newPointer = pointer; - int reduce = 0; - while (!isValidBeamPosition(newDictator, isStartDictator, isAscending, isFlat, staffLines, true)) { - if (++reduce > maxDictatorReduce) { - // we can't shorten this stem at all. bring it back to default and start extending - newDictator = dictator; - newPointer = pointer; - while (!isValidBeamPosition(newDictator, isStartDictator, isAscending, isFlat, staffLines, true)) { - newDictator += towardBeam; - newPointer += towardBeam; - } - break; - } - newDictator += -towardBeam; - newPointer += -towardBeam; - } - // newDictator is guaranteed either valid, or ==dictator - // first, constrain pointer to valid position - newPointer = _up ? std::min(newPointer, middleLine) : std::max(newPointer, middleLine); - // walk it back beamwards until we get a position that satisfies both pointer and dictator - while (!isValidBeamPosition(newDictator, isStartDictator, isAscending, isFlat, staffLines, true) - || !isValidBeamPosition(newPointer, !isStartDictator, isAscending, isFlat, staffLines, true)) { - if (isFlat) { - newDictator += towardBeam; - newPointer += towardBeam; - } else if (std::abs(newDictator - newPointer) == 1) { - newDictator += towardBeam; - } else { - newPointer += towardBeam; - } - } - dictator = newDictator; - pointer = newPointer; -} - -void Beam::extendStem(Chord* chord, double addition) -{ - PointF anchor = chordBeamAnchor(chord, ChordBeamAnchorType::Middle); - double desiredY; - if (_endAnchor.x() > _startAnchor.x()) { - double proportionAlongX = (anchor.x() - _startAnchor.x()) / (_endAnchor.x() - _startAnchor.x()); - desiredY = proportionAlongX * (_endAnchor.y() - _startAnchor.y()) + _startAnchor.y(); - } else { - desiredY = std::max(_endAnchor.y(), _startAnchor.y()); - } - - if (chord->up()) { - chord->setBeamExtension(anchor.y() - desiredY + addition); - } else { - chord->setBeamExtension(desiredY - anchor.y() + addition); - } - if (chord->tremolo()) { - chord->tremolo()->layout(); - } - if (chord->stemSlash()) { - chord->stemSlash()->layout(); - } - - if (cross()) { - // stem-side articulations on cross-staff beams must be re-laid-out - chord->layoutArticulations(); - chord->layoutArticulations2(); - } -} - -bool Beam::isBeamInsideStaff(int yPos, int staffLines, bool allowFloater) const -{ - int aboveStaff = allowFloater ? -2 : -3; - int belowStaff = (staffLines - 1) * 4 + (allowFloater ? 2 : 3); - return yPos > aboveStaff && yPos < belowStaff; -} - -int Beam::getOuterBeamPosOffset(int innerBeam, int beamCount, int staffLines) const -{ - int spacing = (_up ? -_beamSpacing : _beamSpacing); - int offset = (beamCount - 1) * spacing; - bool isInner = false; - while (offset != 0 && !isBeamInsideStaff(innerBeam + offset, staffLines, isInner)) { - offset -= spacing; - isInner = true; - } - return offset; -} - -bool Beam::isValidBeamPosition(int yPos, bool isStart, bool isAscending, bool isFlat, int staffLines, bool isOuter) const -{ - // outside the staff - if (!isBeamInsideStaff(yPos, staffLines, isOuter && (isAscending == isStart || isFlat))) { - return true; - } - - // removes modulo weirdness with negative numbers (i.e., right above staff) - yPos += 8; - // is floater - if (yPos % 4 == 2) { - return false; - } - if (isFlat) { - return true; - } - // is on line - if (yPos % 4 == 0) { - return true; - } - // is sitting - if (yPos % 4 == 3) { - // return true only if we're starting here and descending, or ascending and ending here - return isAscending != isStart; - } - // is hanging - // return true only if we're starting here and ascending, or decending and ending here - return isAscending == isStart; -} - -bool Beam::is64thBeamPositionException(int& yPos, int staffLines) const -{ - if (_beamSpacing == 4) { - return false; - } - return yPos == 2 || yPos == staffLines * 4 - 2 || yPos == staffLines * 4 - 6 || yPos == -2; -} - -int Beam::findValidBeamOffset(int outer, int beamCount, int staffLines, bool isStart, bool isAscending, - bool isFlat) const -{ - bool isBeamValid = false; - int offset = 0; - int innerBeam = outer + (beamCount - 1) * (_up ? _beamSpacing : -_beamSpacing); - while (!isBeamValid) { - while (!isValidBeamPosition(innerBeam + offset, isStart, isAscending, isFlat, staffLines, false)) { - offset += _up ? -1 : 1; - } - int outerMostBeam = innerBeam + offset + getOuterBeamPosOffset(innerBeam + offset, beamCount, staffLines); - if (isValidBeamPosition(outerMostBeam, isStart, isAscending, isFlat, - staffLines, true) - || (beamCount == 4 && is64thBeamPositionException(outerMostBeam, staffLines))) { - isBeamValid = true; - } else { - offset += _up ? -1 : 1; - } - } - return offset; -} - -void Beam::setValidBeamPositions(int& dictator, int& pointer, int beamCountD, int beamCountP, int staffLines, bool isStartDictator, - bool isFlat, bool isAscending) -{ - if (_cross) { - return; - } - bool areBeamsValid = false; - bool has3BeamsInsideStaff = beamCountD >= 3 || beamCountP >= 3; - while (!areBeamsValid && has3BeamsInsideStaff && _beamSpacing != 4) { - int dictatorInner = dictator + (beamCountD - 1) * (_up ? _beamSpacing : -_beamSpacing); - // use dictatorInner for both to simulate flat beams - int outerDictatorOffset = getOuterBeamPosOffset(dictatorInner, beamCountD, staffLines); - if (std::abs(outerDictatorOffset) <= _beamSpacing) { - has3BeamsInsideStaff = false; - break; - } - int offsetD = findValidBeamOffset(dictator, beamCountD, staffLines, isStartDictator, false, true); - int offsetP = findValidBeamOffset(pointer, beamCountP, staffLines, isStartDictator, false, true); - int offset = (offsetD == 0 ? offsetP : offsetD); - dictator += offset; - pointer = dictator; - if (offset == 0) { - areBeamsValid = true; - } - } - if (isFlat) { - // flat beams need more checks (non-dictator/pointer notes with floater inner beams) - areBeamsValid = false; - } - while (!areBeamsValid) { - int dictatorOffset = findValidBeamOffset(dictator, beamCountD, staffLines, isStartDictator, isAscending, isFlat); - dictator += dictatorOffset; - pointer += dictatorOffset; - if (isFlat) { - pointer = dictator; - int currOffset = 0; - for (ChordRest* cr : _elements) { - if (!cr->isChord() && (cr != _elements.front() && cr != _elements.back())) { - continue; - } - // we can use dictator beam position because all of the notes have the same beam position - currOffset = findValidBeamOffset(dictator, cr->beams(), staffLines, isStartDictator, isAscending, isFlat); - if (currOffset) { - break; - } - } - - if (currOffset == 0) { - areBeamsValid = true; - } else { - dictator += currOffset; - pointer += currOffset; - } - } else { - pointer += findValidBeamOffset(pointer, beamCountP, staffLines, !isStartDictator, isAscending, isFlat); - if ((_up && pointer <= dictator) || (!_up && pointer >= dictator)) { - dictator = pointer + (_up ? -1 : 1); - } else { - areBeamsValid = true; - } - } - } -} - -void Beam::addMiddleLineSlant(int& dictator, int& pointer, int beamCount, int middleLine, int interval, int desiredSlant) -{ - bool isSmall = mag() < 1. || _isGrace; - if (interval == 0 || (!isSmall && beamCount > 2 && _beamSpacing != 4) || noSlope()) { - return; - } - bool isOnMiddleLine = pointer == middleLine && (std::abs(pointer - dictator) < 2); - if (isOnMiddleLine) { - if (abs(desiredSlant) == 1 || interval == 1 || (beamCount == 2 && _beamSpacing != 4 && !isSmall)) { - dictator = middleLine + (_up ? -1 : 1); - } else { - dictator = middleLine + (_up ? -2 : 2); - } - } -} - -void Beam::add8thSpaceSlant(PointF& dictatorAnchor, int dictator, int pointer, int beamCount, - int interval, int middleLine, bool isFlat) -{ - if (beamCount != 3 || noSlope() || _beamSpacing != 3) { - return; - } - if ((isFlat && dictator != middleLine) || (dictator != pointer) || interval == 0) { - return; - } - if ((_up && (dictator + 4) % 4 == 3) || (!_up && (dictator + 4) % 4 == 1)) { - return; - } - dictatorAnchor.setY(dictatorAnchor.y() + (_up ? -0.125 * spatium() : 0.125 * spatium())); - _beamDist += 0.0625 * spatium(); -} - //--------------------------------------------------------- // layout2 //--------------------------------------------------------- void Beam::layout2(const std::vector& chordRests, SpannerSegmentType, int frag) { + _layoutInfo = BeamTremoloLayout(this); Chord* startChord = nullptr; Chord* endChord = nullptr; if (chordRests.empty()) { @@ -1499,15 +956,8 @@ void Beam::layout2(const std::vector& chordRests, SpannerSegmentType _beamDist = (_beamSpacing / 4.0) * spatium() * mag(); _beamWidth = point(score()->styleS(Sid::beamWidth)) * mag(); - ChordRest* startCr = chordRests.front(); - ChordRest* endCr = chordRests.back(); - _startAnchor = chordBeamAnchor(startChord, ChordBeamAnchorType::Start); - _endAnchor = chordBeamAnchor(endChord, ChordBeamAnchorType::End); - - double startLength = startChord->defaultStemLength(); - double endLength = endChord->defaultStemLength(); - double startAnchorBase = _startAnchor.y() + (_up ? startLength : -startLength); - double endAnchorBase = _endAnchor.y() + (_up ? endLength : -endLength); + _startAnchor = _layoutInfo.chordBeamAnchor(startChord, BeamTremoloLayout::ChordBeamAnchorType::Start); + _endAnchor = _layoutInfo.chordBeamAnchor(endChord, BeamTremoloLayout::ChordBeamAnchorType::End); if (_isGrace) { _beamDist *= score()->styleD(Sid::graceNoteMag); @@ -1516,6 +966,7 @@ void Beam::layout2(const std::vector& chordRests, SpannerSegmentType int fragmentIndex = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; if (_userModified[fragmentIndex]) { + _layoutInfo = BeamTremoloLayout(this); double startY = fragments[frag]->py1[fragmentIndex]; double endY = fragments[frag]->py2[fragmentIndex]; if (score()->styleB(Sid::snapCustomBeamsToGrid)) { @@ -1527,110 +978,47 @@ void Beam::layout2(const std::vector& chordRests, SpannerSegmentType endY += pagePos().y(); _startAnchor.setY(startY); _endAnchor.setY(endY); + _layoutInfo.setAnchors(_startAnchor, _endAnchor); _slope = (_endAnchor.y() - _startAnchor.y()) / (_endAnchor.x() - _startAnchor.x()); createBeamSegments(chordRests); + setTremAnchors(); return; } - if (_cross) { - if (layout2Cross(chordRests, frag)) { - return; - } - _cross = false; - } - // anchor represents the middle of the beam, not the tip of the stem // location depends on _isBesideTabStaff if (!_isBesideTabStaff) { - bool isSmall = mag() < 1. || _isGrace; - int startNote = _up ? startChord->upNote()->line() : startChord->downNote()->line(); - int endNote = _up ? endChord->upNote()->line() : endChord->downNote()->line(); - if (_tab) { - startNote = _up ? startChord->upString() : startChord->downString(); - endNote = _up ? endChord->upString() : endChord->downString(); - } - const int interval = std::abs(startNote - endNote); - const bool isStartDictator = _up ? startNote < endNote : startNote > endNote; - const double quarterSpace = spatium() / 4; - PointF startAnchor = _startAnchor - pagePos(); - PointF endAnchor = _endAnchor - pagePos(); - int dictator = round((isStartDictator ? startAnchor.y() : endAnchor.y()) / quarterSpace); - int pointer = round((isStartDictator ? endAnchor.y() : startAnchor.y()) / quarterSpace); - - const int staffLines = startChord->staff()->lines(tick()); - const int middleLine = getMiddleStaffLine(startChord, endChord, staffLines); - - int slant = computeDesiredSlant(startNote, endNote, middleLine, dictator, pointer); - bool isFlat = slant == 0 && !isSmall; - int specialSlant = isFlat ? isSlopeConstrained(startNote, endNote) : -1; - bool forceFlat = specialSlant == 0; - bool smallSlant = specialSlant == 1; - if (isFlat) { - dictator = _up ? std::min(pointer, dictator) : std::max(pointer, dictator); - pointer = dictator; - } else { - pointer = dictator + slant; - } - bool isAscending = startNote > endNote; - int beamCountD = (isStartDictator ? startChord : endChord)->beams(); - int beamCountP = (isStartDictator ? endChord : startChord)->beams(); - int stemLengthStart = abs(round((startAnchorBase - _startAnchor.y()) / spatium() * 4)); - int stemLengthEnd = abs(round((endAnchorBase - _endAnchor.y()) / spatium() * 4)); - int stemLengthDictator = isStartDictator ? stemLengthStart : stemLengthEnd; - if (endAnchor.x() > startAnchor.x()) { - /* When beam layout is called before horizontal spacing (see LayoutMeasure::getNextMeasure() to - * know why) the x positions aren't yet determined and may be all zero, which would cause the - * following function to get stuck in a loop. The if() condition avoids that case. */ - if (!isSmall) { - // Adjust anchor stems - offsetBeamWithAnchorShortening(chordRests, dictator, pointer, staffLines, isStartDictator, stemLengthDictator); - } - // Adjust inner stems - offsetBeamToRemoveCollisions(chordRests, dictator, pointer, startAnchor.x(), endAnchor.x(), isFlat, isStartDictator); - } - int beamCount = std::max(beamCountD, beamCountP); - if (!_tab) { - if (!isSmall) { - setValidBeamPositions(dictator, pointer, beamCountD, beamCountP, staffLines, isStartDictator, isFlat, isAscending); - } - if (!forceFlat) { - addMiddleLineSlant(dictator, pointer, beamCount, middleLine, interval, smallSlant ? 1 : slant); - } - } - - _startAnchor.setY(quarterSpace * (isStartDictator ? dictator : pointer) + pagePos().y()); - _endAnchor.setY(quarterSpace * (isStartDictator ? pointer : dictator) + pagePos().y()); - - bool add8th = true; - for (bool modified : _userModified) { - if (modified) { - add8th = false; + _layoutInfo = BeamTremoloLayout(this); + _layoutInfo.calculateAnchors(chordRests, _notes); + _startAnchor = _layoutInfo.startAnchor(); + _endAnchor = _layoutInfo.endAnchor(); + _slope = (_endAnchor.y() - _startAnchor.y()) / (_endAnchor.x() - _startAnchor.x()); + _beamDist = _layoutInfo.beamDist(); + } else { + _slope = 0; + Chord* startChord = nullptr; + for (ChordRest* cr : chordRests) { + if (cr->isChord()) { + startChord = toChord(cr); break; } } - if (!_tab && add8th) { - add8thSpaceSlant(isStartDictator ? _startAnchor : _endAnchor, dictator, pointer, beamCount, interval, middleLine, isFlat); - } - _startAnchor.setX(chordBeamAnchorX(startCr, ChordBeamAnchorType::Start)); - _endAnchor.setX(chordBeamAnchorX(endCr, ChordBeamAnchorType::End)); - double xDiff = _endAnchor.x() - _startAnchor.x(); - double yDiff = _endAnchor.y() - _startAnchor.y(); - // HACK: when beam layout is called before horizontal spacing, xDiff is a random small - // number, so don't try to compute the slope - if (abs(xDiff) < 0.5 * spatium()) { - _slope = 0; - } else { - _slope = yDiff / xDiff; - } - } else { - _slope = 0; + _layoutInfo = BeamTremoloLayout(this); + double x1 = _layoutInfo.chordBeamAnchorX(chordRests.front(), BeamTremoloLayout::ChordBeamAnchorType::Start); + double x2 = _layoutInfo.chordBeamAnchorX(chordRests.back(), BeamTremoloLayout::ChordBeamAnchorType::End); + double y = _layoutInfo.chordBeamAnchorY(startChord); + _startAnchor = PointF(x1, y); + _endAnchor = PointF(x2, y); + _layoutInfo.setAnchors(_startAnchor, _endAnchor); + _beamWidth = _layoutInfo.beamWidth(); } fragments[frag]->py1[fragmentIndex] = _startAnchor.y() - pagePos().y(); fragments[frag]->py2[fragmentIndex] = _endAnchor.y() - pagePos().y(); createBeamSegments(chordRests); + setTremAnchors(); } bool Beam::layout2Cross(const std::vector& chordRests, int frag) @@ -1705,7 +1093,7 @@ bool Beam::layout2Cross(const std::vector& chordRests, int frag) } bottomLast = c; } - maxY = std::min(maxY, chordBeamAnchorY(toChord(c))); + maxY = std::min(maxY, _layoutInfo.chordBeamAnchorY(toChord(c))); } else { // this chord is on the top staff if (penultimateTopIsSame) { @@ -1731,7 +1119,7 @@ bool Beam::layout2Cross(const std::vector& chordRests, int frag) } topLast = c; } - minY = std::max(minY, chordBeamAnchorY(toChord(c))); + minY = std::max(minY, _layoutInfo.chordBeamAnchorY(toChord(c))); } } _startAnchor.ry() = (maxY + minY) / 2; @@ -1765,7 +1153,7 @@ bool Beam::layout2Cross(const std::vector& chordRests, int frag) yLast = topFirst->stemPos().y(); } int desiredSlant = round((yFirst - yLast) / spatium()); - int slant = std::min(std::abs(desiredSlant), getMaxSlope()); + int slant = std::min(std::abs(desiredSlant), _layoutInfo.getMaxSlope()); slant *= (desiredSlant < 0) ? -quarterSpace : quarterSpace; _startAnchor.ry() += (slant / 2); _endAnchor.ry() -= (slant / 2); @@ -1794,7 +1182,7 @@ bool Beam::layout2Cross(const std::vector& chordRests, int frag) if (!forceHoriz) { int slant = startNote - endNote; - slant = std::min(std::abs(slant), getMaxSlope()); + slant = std::min(std::abs(slant), _layoutInfo.getMaxSlope()); if ((!bottomLast && constrainTopToQuarter) || (!topLast && constrainBottomToQuarter)) { slant = 1; } @@ -1828,7 +1216,7 @@ bool Beam::layout2Cross(const std::vector& chordRests, int frag) // if one of the slants is 0, the whole slant is zero } else if ((topSlant < 0 && bottomSlant < 0) || (topSlant > 0 && bottomSlant > 0)) { int slant = (abs(topSlant) < abs(bottomSlant)) ? topSlant : bottomSlant; - slant = std::min(std::abs(slant), getMaxSlope()); + slant = std::min(std::abs(slant), _layoutInfo.getMaxSlope()); double slope = slant * ((topSlant < 0) ? -quarterSpace : quarterSpace); _startAnchor.ry() += (slope / 2); _endAnchor.ry() -= (slope / 2); @@ -1837,8 +1225,8 @@ bool Beam::layout2Cross(const std::vector& chordRests, int frag) // nothing needs to be done, the beam is already horizontal and placed nicely } } - _startAnchor.setX(chordBeamAnchorX(startCr, ChordBeamAnchorType::Start)); - _endAnchor.setX(chordBeamAnchorX(endCr, ChordBeamAnchorType::End)); + _startAnchor.setX(_layoutInfo.chordBeamAnchorX(startCr, BeamTremoloLayout::ChordBeamAnchorType::Start)); + _endAnchor.setX(_layoutInfo.chordBeamAnchorX(endCr, BeamTremoloLayout::ChordBeamAnchorType::End)); _slope = (_endAnchor.y() - _startAnchor.y()) / (_endAnchor.x() - _startAnchor.x()); } fragments[frag]->py1[fragmentIndex] = _startAnchor.y() - pagePos().y(); @@ -2518,7 +1906,7 @@ Shape BeamSegment::shape() const Shape shape; PointF startPoint = line.p1(); PointF endPoint = line.p2(); - double _beamWidth = beam->_beamWidth; + double _beamWidth = parentElement->isBeam() ? toBeam(parentElement)->_beamWidth : toTremolo(parentElement)->beamWidth(); // This is the case of right-beamlets if (startPoint.x() > endPoint.x()) { std::swap(startPoint, endPoint); @@ -2528,12 +1916,12 @@ Shape BeamSegment::shape() const if (RealIsEqual(startPoint.y(), endPoint.y())) { RectF rect(startPoint.x(), startPoint.y(), beamHorizontalLength, _beamWidth / 2); rect.adjust(0.0, -_beamWidth / 2, 0.0, 0.0); - shape.add(rect, beam); + shape.add(rect, parentElement); return shape; } // If not, break the beam shape into multiple rectangles double beamHeightDiff = endPoint.y() - startPoint.y(); - int subBoxesCount = floor(beamHorizontalLength / beam->spatium()); + int subBoxesCount = floor(beamHorizontalLength / parentElement->spatium()); double horizontalStep = beamHorizontalLength / subBoxesCount; double verticalStep = beamHeightDiff / subBoxesCount; std::vector pointsOnBeamLine; @@ -2545,7 +1933,7 @@ Shape BeamSegment::shape() const for (PointF point : pointsOnBeamLine) { RectF rect(point.x(), point.y(), horizontalStep, _beamWidth / 2); rect.adjust(0.0, -_beamWidth / 2, 0.0, 0.0); - shape.add(rect, beam); + shape.add(rect, parentElement); } return shape; } diff --git a/src/engraving/libmscore/beam.h b/src/engraving/libmscore/beam.h index 70a0bb6e1d852..1034cb8a01ab7 100644 --- a/src/engraving/libmscore/beam.h +++ b/src/engraving/libmscore/beam.h @@ -23,6 +23,7 @@ #ifndef __BEAM_H__ #define __BEAM_H__ +#include "beamtremololayout.h" #include "engravingitem.h" #include "durationtype.h" #include "property.h" @@ -33,29 +34,45 @@ class ChordRest; class Factory; class Skyline; class System; - +class Beam; enum class ActionIconType; enum class SpannerSegmentType; -struct BeamFragment; +//--------------------------------------------------------- +// BeamFragment +// position of primary beam +// idx 0 - DirectionV::AUTO or DirectionV::DOWN +// 1 - DirectionV::UP +//--------------------------------------------------------- + +struct BeamFragment { + double py1[2]; + double py2[2]; +}; class BeamSegment { OBJECT_ALLOCATOR(engraving, BeamSegment) public: mu::LineF line; - int level; - bool above; // above level 0 or below? (meaningless for level 0) + int level = 0; + bool above = false; // above level 0 or below? (meaningless for level 0) Fraction startTick; Fraction endTick; bool isBeamlet = false; bool isBefore = false; Shape shape() const; - Beam* beam; + EngravingItem* parentElement; - BeamSegment(Beam* b) - : beam(b) {} + BeamSegment(EngravingItem* b) + : parentElement(b) {} +}; + +struct TremAnchor { + ChordRest* chord1 = nullptr; + double y1 = 0.; + double y2 = 0.; }; //--------------------------------------------------------- @@ -83,6 +100,7 @@ class Beam final : public EngravingItem double _beamWidth { 0.0f }; // how wide each beam is mu::PointF _startAnchor; mu::PointF _endAnchor; + BeamTremoloLayout _layoutInfo; // for tabs bool _isBesideTabStaff { false }; @@ -99,31 +117,13 @@ class Beam final : public EngravingItem double _slope { 0.0 }; std::vector _notes; + std::vector _tremAnchors; friend class Factory; friend class BeamSegment; Beam(System* parent); Beam(const Beam&); - int getMiddleStaffLine(ChordRest* startChord, ChordRest* endChord, int staffLines) const; - int computeDesiredSlant(int startNote, int endNote, int middleLine, int dictator, int pointer) const; - int isSlopeConstrained(int startNote, int endNote) const; - int getMaxSlope() const; - int getBeamCount(std::vector chordRests) const; - void offsetBeamToRemoveCollisions(std::vector chordRests, int& dictator, int& pointer, const double startX, - const double endX, bool isFlat, bool isStartDictator) const; - void offsetBeamWithAnchorShortening(std::vector chordRests, int& dictator, int& pointer, int staffLines, - bool isStartDictator, int stemLengthDictator) const; - bool isBeamInsideStaff(int yPos, int staffLines, bool allowFloater) const; - int getOuterBeamPosOffset(int innerBeam, int beamCount, int staffLines) const; - bool isValidBeamPosition(int yPos, bool isStart, bool isAscending, bool isFlat, int staffLines, bool isOuter) const; - bool is64thBeamPositionException(int& yPos, int staffLines) const; - int findValidBeamOffset(int outer, int beamCount, int staffLines, bool isStart, bool isAscending, bool isFlat) const; - void setValidBeamPositions(int& dictator, int& pointer, int beamCountD, int beamCountP, int staffLines, bool isStartDictator, - bool isFlat, bool isAscending); - void addMiddleLineSlant(int& dictator, int& pointer, int beamCount, int middleLine, int interval, int desiredSlant); - void add8thSpaceSlant(mu::PointF& dictatorAnchor, int dictator, int pointer, int beamCount, int interval, int middleLine, bool Flat); - void extendStem(Chord* chord, double addition); bool calcIsBeamletBefore(Chord* chord, int i, int level, bool isAfter32Break, bool isAfter64Break) const; void createBeamSegment(ChordRest* startChord, ChordRest* endChord, int level); void createBeamletSegment(ChordRest* chord, bool isBefore, int level); @@ -134,6 +134,7 @@ class Beam final : public EngravingItem void removeChordRest(ChordRest* a); const Chord* findChordWithCustomStemDirection() const; + void setTremAnchors(); public: ~Beam(); @@ -165,13 +166,8 @@ class Beam final : public EngravingItem void layout1(); void layout() override; - enum class ChordBeamAnchorType { - Start, End, Middle - }; - - double chordBeamAnchorX(const ChordRest* chord, ChordBeamAnchorType anchorType) const; + PointF chordBeamAnchor(const ChordRest* chord, BeamTremoloLayout::ChordBeamAnchorType anchorType) const; double chordBeamAnchorY(const ChordRest* chord) const; - PointF chordBeamAnchor(const ChordRest* chord, ChordBeamAnchorType anchorType) const; const std::vector& elements() const { return _elements; } void clear() { _elements.clear(); } @@ -248,6 +244,8 @@ class Beam final : public EngravingItem Shape shape() const override; + const std::vector& tremAnchors() const { return _tremAnchors; } + private: void initBeamEditData(EditData& ed); diff --git a/src/engraving/libmscore/beamtremololayout.cpp b/src/engraving/libmscore/beamtremololayout.cpp new file mode 100644 index 0000000000000..c6fd5047f715c --- /dev/null +++ b/src/engraving/libmscore/beamtremololayout.cpp @@ -0,0 +1,1085 @@ +/* + * 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 . + */ + +#include "beam.h" +#include "chordrest.h" +#include "note.h" +#include "score.h" +#include "staff.h" +#include "stem.h" +#include "stemslash.h" +#include "tremolo.h" + +#include "log.h" + +using namespace mu; +using namespace mu::engraving; + +namespace mu::engraving { +constexpr std::array _maxSlopes = { 0, 1, 2, 3, 4, 5, 6, 7 }; + +BeamTremoloLayout::BeamTremoloLayout(EngravingItem* e) +{ + bool isGrace = false; + IF_ASSERT_FAILED(e && (e->isBeam() || e->isTremolo())) { + // right now only beams and trems are supported + return; + } else if (e->isBeam()) { + m_beamType = BeamType::BEAM; + m_beam = toBeam(e); + m_up = toBeam(e)->up(); + m_trem = nullptr; // there can be many different trems in a beam, they will all be checked + isGrace = m_beam->elements().front()->isGrace(); + } else { // e->isTremolo() + m_trem = toTremolo(e); + m_beamType = BeamType::TREMOLO; + // check to see if there is a beam happening during this trem + // if so, it needs to be taken into account in trem placement + if (m_trem->chord1()->beam() && m_trem->chord1()->beam() == m_trem->chord2()->beam()) { + m_beam = m_trem->chord1()->beam(); + } else { + m_beam = nullptr; + } + m_up = computeTremoloUp(); + m_trem->setUp(m_up); + isGrace = m_trem->chord1()->isGrace(); + } + m_element = e; + m_spatium = e->score()->spatium(); + m_tick = m_element->tick(); + m_beamSpacing = e->score()->styleB(Sid::useWideBeams) ? 4 : 3; + m_beamDist = (m_beamSpacing / 4.0) * m_spatium * e->mag() + * (isGrace ? e->score()->styleD(Sid::graceNoteMag) : 1.); + m_beamWidth = (e->score()->styleS(Sid::beamWidth).val() * m_spatium) * e->mag(); + const StaffType* staffType = e->staffType(); + m_tab = (staffType && staffType->isTabStaff()) ? staffType : nullptr; + m_isBesideTabStaff = m_tab && !m_tab->stemless() && !m_tab->stemThrough(); +} + +void BeamTremoloLayout::offsetBeamToRemoveCollisions(const std::vector chordRests, int& dictator, int& pointer, + const double startX, const double endX, + bool isFlat, bool isStartDictator) const +{ + if (endX == startX) { + return; + } + + // tolerance eliminates all possibilities of floating point rounding errors + const double tolerance = m_beamWidth * 0.25 * (m_up ? -1 : 1); + bool isSmall = m_isGrace || m_element->mag() < 1.; + + double startY = (isStartDictator ? dictator : pointer) * m_spatium / 4 + tolerance; + double endY = (isStartDictator ? pointer : dictator) * m_spatium / 4 + tolerance; + + for (ChordRest* chordRest : chordRests) { + if (!chordRest->isChord() || chordRest == m_elements.back() || chordRest == m_elements.front()) { + continue; + } + + PointF anchor = chordBeamAnchor(chordRest, ChordBeamAnchorType::Middle) - m_element->pagePos(); + + int slope = abs(dictator - pointer); + double reduction = 0.0; + if (!isFlat) { + if (slope <= 3) { + reduction = 0.25 * m_spatium; + } else if (slope <= 6) { + reduction = 0.5 * m_spatium; + } else { // slope > 6 + reduction = 0.75 * m_spatium; + } + } + + if (endX != startX) { + // avoid division by zero for zero-length beams (can exist as a pre-layout state used + // for horizontal spacing computations) + double proportionAlongX = (anchor.x() - startX) / (endX - startX); + + while (true) { + double desiredY = proportionAlongX * (endY - startY) + startY; + bool beamClearsAnchor = (m_up && RealIsEqualOrLess(desiredY, anchor.y() + reduction)) + || (!m_up && RealIsEqualOrMore(desiredY, anchor.y() - reduction)); + if (beamClearsAnchor) { + break; + } + + if (isFlat || (isSmall && dictator == pointer)) { + dictator += m_up ? -1 : 1; + pointer += m_up ? -1 : 1; + } else if (std::abs(dictator - pointer) == 1) { + dictator += m_up ? -1 : 1; + } else { + pointer += m_up ? -1 : 1; + } + + startY = (isStartDictator ? dictator : pointer) * m_spatium / 4 + tolerance; + endY = (isStartDictator ? pointer : dictator) * m_spatium / 4 + tolerance; + } + } + } +} + +void BeamTremoloLayout::offsetBeamWithAnchorShortening(std::vector chordRests, int& dictator, int& pointer, int staffLines, + bool isStartDictator, int stemLengthDictator) const +{ + Chord* startChord = nullptr; + Chord* endChord = nullptr; + for (ChordRest* cr : chordRests) { + if (cr->isChord()) { + endChord = toChord(cr); + if (!startChord) { + startChord = toChord(cr); + } + } + } + if (!startChord) { + // beam full of only rests, don't adjust this + return; + } + // min stem lengths according to how many beams there are (starting with 1) + static const int minStemLengths[] = { 11, 13, 15, 18, 21, 24, 27, 30 }; + const int middleLine = getMiddleStaffLine(startChord, endChord, staffLines); + int dictatorBeams = strokeCount(isStartDictator ? startChord : endChord); + int pointerBeams = strokeCount(isStartDictator ? endChord : startChord); + int maxDictatorReduce = stemLengthDictator - minStemLengths[std::max(dictatorBeams - 1, 0)]; + maxDictatorReduce = std::min(abs(dictator - middleLine), maxDictatorReduce); + + bool isFlat = dictator == pointer; + bool isAscending = startChord->line() > endChord->line(); + int towardBeam = m_up ? -1 : 1; + int newDictator = dictator; + int newPointer = pointer; + int reduce = 0; + auto fourBeamException = [staffLines](int beams, int yPos) { + yPos += 400; // because there is some weirdness with modular division around zero, add + // a large multiple of 4 so that we can guarantee that yPos%4 will be correct + return beams >= 4 && (yPos % 4 == 2); + }; + while (!fourBeamException(dictatorBeams, newDictator) + && !isValidBeamPosition(newDictator, isStartDictator, isAscending, isFlat, staffLines, true)) { + if (++reduce > maxDictatorReduce) { + // we can't shorten this stem at all. bring it back to default and start extending + newDictator = dictator; + newPointer = pointer; + while (!isValidBeamPosition(newDictator, isStartDictator, isAscending, isFlat, staffLines, true)) { + newDictator += towardBeam; + newPointer += towardBeam; + } + break; + } + newDictator += -towardBeam; + newPointer += -towardBeam; + } + + // newDictator is guaranteed either valid, or ==dictator + // first, constrain pointer to valid position + newPointer = m_up ? std::min(newPointer, middleLine) : std::max(newPointer, middleLine); + // walk it back beamwards until we get a position that satisfies both pointer and dictator + while (!fourBeamException(dictatorBeams, newDictator) && !fourBeamException(pointerBeams, newPointer) + && (!isValidBeamPosition(newDictator, isStartDictator, isAscending, isFlat, staffLines, true) + || !isValidBeamPosition(newPointer, !isStartDictator, isAscending, isFlat, staffLines, true))) { + if (isFlat) { + newDictator += towardBeam; + newPointer += towardBeam; + } else if (std::abs(newDictator - newPointer) == 1) { + newDictator += towardBeam; + } else { + newPointer += towardBeam; + } + } + dictator = newDictator; + pointer = newPointer; +} + +void BeamTremoloLayout::extendStem(Chord* chord, double addition) +{ + PointF anchor = chordBeamAnchor(chord, ChordBeamAnchorType::Middle); + double desiredY; + if (m_endAnchor.x() > m_startAnchor.x()) { + double proportionAlongX = (anchor.x() - m_startAnchor.x()) / (m_endAnchor.x() - m_startAnchor.x()); + desiredY = proportionAlongX * (m_endAnchor.y() - m_startAnchor.y()) + m_startAnchor.y(); + } else { + desiredY = std::max(m_endAnchor.y(), m_startAnchor.y()); + } + + if (chord->up()) { + chord->setBeamExtension(anchor.y() - desiredY + addition); + } else { + chord->setBeamExtension(desiredY - anchor.y() + addition); + } + if (chord->stemSlash()) { + chord->stemSlash()->layout(); + } + + if ((m_beam && m_beam->cross()) || (m_trem && m_trem->crossStaffBeamBetween())) { + // stem-side articulations on cross-staff beams must be re-laid-out + chord->layoutArticulations(); + chord->layoutArticulations2(); + } +} + +bool BeamTremoloLayout::isBeamInsideStaff(int yPos, int staffLines, bool allowFloater) const +{ + int aboveStaff = allowFloater ? -2 : -3; + int belowStaff = (staffLines - 1) * 4 + (allowFloater ? 2 : 3); + return yPos > aboveStaff && yPos < belowStaff; +} + +int BeamTremoloLayout::getOuterBeamPosOffset(int innerBeam, int beamCount, int staffLines) const +{ + int spacing = (m_up ? -m_beamSpacing : m_beamSpacing); + int offset = (beamCount - 1) * spacing; + bool isInner = false; + while (offset != 0 && !isBeamInsideStaff(innerBeam + offset, staffLines, isInner)) { + offset -= spacing; + isInner = true; + } + return offset; +} + +bool BeamTremoloLayout::isValidBeamPosition(int yPos, bool isStart, bool isAscending, bool isFlat, int staffLines, bool isOuter) const +{ + // outside the staff + bool slantsAway = (m_up && isAscending == isStart) || (!m_up && isAscending != isStart); + if (!isBeamInsideStaff(yPos, staffLines, isOuter && (slantsAway || isFlat))) { + return true; + } + + // removes modulo weirdness with negative numbers (i.e., right above staff) + yPos += 8; + // is floater + if (yPos % 4 == 2) { + return false; + } + if (isFlat) { + return true; + } + // is on line + if (yPos % 4 == 0) { + return true; + } + // is sitting + if (yPos % 4 == 3) { + // return true only if we're starting here and descending, or ascending and ending here + return isAscending != isStart; + } + // is hanging + // return true only if we're starting here and ascending, or decending and ending here + return isAscending == isStart; +} + +bool BeamTremoloLayout::is64thBeamPositionException(int& yPos, int staffLines) const +{ + if (m_beamSpacing == 4) { + return false; + } + return yPos == 2 || yPos == staffLines * 4 - 2 || yPos == staffLines * 4 - 6 || yPos == -2; +} + +int BeamTremoloLayout::findValidBeamOffset(int outer, int beamCount, int staffLines, bool isStart, bool isAscending, + bool isFlat) const +{ + bool isBeamValid = false; + int offset = 0; + int innerBeam = outer + (beamCount - 1) * (m_up ? m_beamSpacing : -m_beamSpacing); + while (!isBeamValid) { + while (!isValidBeamPosition(innerBeam + offset, isStart, isAscending, isFlat, staffLines, beamCount < 2)) { + offset += m_up ? -1 : 1; + } + int outerMostBeam = innerBeam + offset + getOuterBeamPosOffset(innerBeam + offset, beamCount, staffLines); + if (isValidBeamPosition(outerMostBeam, isStart, isAscending, isFlat, + staffLines, true) + || (beamCount == 4 && is64thBeamPositionException(outerMostBeam, staffLines))) { + isBeamValid = true; + } else { + offset += m_up ? -1 : 1; + } + } + return offset; +} + +void BeamTremoloLayout::setValidBeamPositions(int& dictator, int& pointer, int beamCountD, int beamCountP, int staffLines, + bool isStartDictator, + bool isFlat, bool isAscending) +{ + bool areBeamsValid = false; + bool has3BeamsInsideStaff = beamCountD >= 3 || beamCountP >= 3; + while (!areBeamsValid && has3BeamsInsideStaff && m_beamSpacing != 4) { + int dictatorInner = dictator + (beamCountD - 1) * (m_up ? m_beamSpacing : -m_beamSpacing); + // use dictatorInner for both to simulate flat beams + int outerDictatorOffset = getOuterBeamPosOffset(dictatorInner, beamCountD, staffLines); + if (std::abs(outerDictatorOffset) <= m_beamSpacing) { + has3BeamsInsideStaff = false; + break; + } + int offsetD = findValidBeamOffset(dictator, beamCountD, staffLines, isStartDictator, false, true); + int offsetP = findValidBeamOffset(pointer, beamCountP, staffLines, isStartDictator, false, true); + int offset = (offsetD == 0 ? offsetP : offsetD); + if (pointer == dictator) { + dictator += offset; + } + pointer = dictator; + if (offset == 0) { + areBeamsValid = true; + } + } + if (isFlat) { + // flat beams need more checks (non-dictator/pointer notes with floater inner beams) + areBeamsValid = false; + } + while (!areBeamsValid) { + int dictatorOffset = findValidBeamOffset(dictator, beamCountD, staffLines, isStartDictator, isAscending, isFlat); + dictator += dictatorOffset; + pointer += dictatorOffset; + if (isFlat) { + pointer = dictator; + int currOffset = 0; + for (ChordRest* cr : m_elements) { + if (!cr->isChord() && (cr != m_elements.front() && cr != m_elements.back())) { + continue; + } + // we can use dictator beam position because all of the notes have the same beam position + int beamCount = strokeCount(cr); + currOffset = findValidBeamOffset(dictator, beamCount, staffLines, isStartDictator, isAscending, isFlat); + if (currOffset) { + break; + } + } + + if (currOffset == 0) { + areBeamsValid = true; + } else { + dictator += currOffset; + pointer += currOffset; + } + } else { + pointer += findValidBeamOffset(pointer, beamCountP, staffLines, !isStartDictator, isAscending, isFlat); + if ((m_up && pointer <= dictator) || (!m_up && pointer >= dictator)) { + dictator = pointer + (m_up ? -1 : 1); + } else { + areBeamsValid = true; + } + } + } +} + +void BeamTremoloLayout::addMiddleLineSlant(int& dictator, int& pointer, int beamCount, int middleLine, int interval, int desiredSlant) +{ + bool isSmall = m_element->mag() < 1. || m_isGrace; + if (interval == 0 || (!isSmall && beamCount > 2 && m_beamSpacing != 4) || noSlope()) { + return; + } + bool isOnMiddleLine = pointer == middleLine && (std::abs(pointer - dictator) < 2); + if (isOnMiddleLine) { + if (abs(desiredSlant) == 1 || interval == 1 || (beamCount == 2 && m_beamSpacing != 4 && !isSmall)) { + dictator = middleLine + (m_up ? -1 : 1); + } else { + dictator = middleLine + (m_up ? -2 : 2); + } + } +} + +void BeamTremoloLayout::add8thSpaceSlant(PointF& dictatorAnchor, int dictator, int pointer, int beamCount, + int interval, int middleLine, bool isFlat) +{ + if (beamCount != 3 || noSlope() || m_beamSpacing != 3) { + return; + } + if ((isFlat && dictator != middleLine) || (dictator != pointer) || interval == 0) { + return; + } + if ((m_up && (dictator + 4) % 4 == 3) || (!m_up && (dictator + 4) % 4 == 1)) { + return; + } + dictatorAnchor.setY(dictatorAnchor.y() + (m_up ? -0.125 * m_spatium : 0.125 * m_spatium)); + m_beamDist += 0.0625 * m_spatium; +} + +bool BeamTremoloLayout::computeTremoloUp() +{ + if (!m_beam || !m_beam->cross()) { + return m_trem->up(); + } + Chord* c1 = m_trem->chord1(); + Chord* c2 = m_trem->chord2(); + int staffMove = 0; + for (ChordRest* cr : m_beam->elements()) { + if (cr->staffMove() != 0) { + staffMove = cr->staffMove(); + break; + } + } + // for inset trems in cross-staff beams, there are two potentialities: + // 1) both trem notes are on the same staff + if (c1->staffMove() == c2->staffMove()) { + // (in which case, up is opposite to the move) + if (staffMove < 0) { + return c1->staffMove() != staffMove; + } else { + return c1->staffMove() == staffMove; + } + } else { + // or 2) the trem notes are between staves. what a pain. + return m_trem->up(); + } +} + +int BeamTremoloLayout::strokeCount(ChordRest* cr) const +{ + if (cr->isRest()) { + return cr->beams(); + } + int strokes = 0; + Chord* c = toChord(cr); + if (m_beamType == BeamType::TREMOLO) { + strokes = m_trem->lines(); + } else if (m_beamType == BeamType::BEAM && c->tremolo()) { + Tremolo* t = c->tremolo(); + if (t->twoNotes()) { + strokes = t->lines(); + } + } + strokes += m_beam ? cr->beams() : 0; + return strokes; +} + +bool BeamTremoloLayout::calculateAnchors(const std::vector& chordRests, const std::vector& notes) +{ + m_startAnchor = PointF(); + m_endAnchor = PointF(); + if (m_beamType == BeamType::TREMOLO && m_beam) { + // this is a trem inside a beam, and we are currently calculating the anchors of the tremolo. + // we can do this as long as the beam has been layed out first: it saves trem anchor positions + ChordRest* cr1 = m_trem->chord1(); + ChordRest* cr2 = m_trem->chord2(); + double anchorX1 = chordBeamAnchorX(cr1, ChordBeamAnchorType::Middle); + double anchorX2 = chordBeamAnchorX(cr2, ChordBeamAnchorType::Middle); + for (const TremAnchor& t : m_beam->tremAnchors()) { + if (t.chord1 == cr1) { + m_startAnchor = PointF(anchorX1, t.y1); + m_endAnchor = PointF(anchorX2, t.y2); + m_slope = (t.y2 - t.y1) / (anchorX2 - anchorX1); + return true; + } + } + // if we finish the loop before we find the anchor for this tremolo, it could mean that the + // beam hasn't been laid out yet + return false; + } + Chord* startChord = nullptr; + Chord* endChord = nullptr; + ChordRest* startCr = nullptr; + ChordRest* endCr = nullptr; + m_elements = chordRests; + if (chordRests.empty()) { + return false; + } + bool cross = false; + int otherStaff = 0; + for (auto chordRest : chordRests) { + if (!startCr) { + startCr = chordRest; + } + endCr = chordRest; + if (chordRest->staffMove()) { + cross = true; + otherStaff = chordRest->staffMove(); + } + if (chordRest->isChord()) { + if (!startChord) { + startChord = toChord(chordRest); + endChord = startChord; + } else { + endChord = toChord(chordRest); + } + toChord(chordRest)->layoutStem(); + } + } + if (!startChord) { + // we were passed a vector of only rests. we don't support beams across only rests + // this beam will be deleted in LayoutBeams + return false; + } + m_isGrace = startChord->isGrace(); + m_notes = notes; + if (calculateAnchorsCross()) { + return true; + } + cross = false; + m_startAnchor = chordBeamAnchor(startChord, ChordBeamAnchorType::Start); + m_endAnchor = chordBeamAnchor(endChord, ChordBeamAnchorType::End); + + double startLength = startChord->defaultStemLength(); + double endLength = endChord->defaultStemLength(); + double startAnchorBase = m_startAnchor.y() + (m_up ? startLength : -startLength); + double endAnchorBase = m_endAnchor.y() + (m_up ? endLength : -endLength); + int startNote = m_up ? startChord->upNote()->line() : startChord->downNote()->line(); + int endNote = m_up ? endChord->upNote()->line() : endChord->downNote()->line(); + if (m_tab) { + startNote = m_up ? startChord->upString() : startChord->downString(); + endNote = m_up ? endChord->upString() : endChord->downString(); + } + const int interval = std::abs(startNote - endNote); + const bool isStartDictator = m_up ? startNote < endNote : startNote > endNote; + const double quarterSpace = m_spatium / 4; + PointF startAnchor = m_startAnchor - m_element->pagePos(); + PointF endAnchor = m_endAnchor - m_element->pagePos(); + int dictator = round((isStartDictator ? startAnchor.y() : endAnchor.y()) / quarterSpace); + int pointer = round((isStartDictator ? endAnchor.y() : startAnchor.y()) / quarterSpace); + + const int staffLines = startChord->staff()->lines(m_tick); + const int middleLine = getMiddleStaffLine(startChord, endChord, staffLines); + + int slant = computeDesiredSlant(startNote, endNote, middleLine, dictator, pointer); + bool isFlat = slant == 0; + int specialSlant = isFlat ? isSlopeConstrained(startNote, endNote) : -1; + bool forceFlat = specialSlant == 0; + bool smallSlant = specialSlant == 1; + if (isFlat) { + dictator = m_up ? std::min(pointer, dictator) : std::max(pointer, dictator); + pointer = dictator; + } else { + pointer = dictator + slant; + } + bool isAscending = startNote > endNote; + int beamCountD = strokeCount(isStartDictator ? startChord : endChord); + int beamCountP = strokeCount(isStartDictator ? endChord : startChord); + + int stemLengthStart = abs(round((startAnchorBase - m_startAnchor.y()) / m_spatium * 4)); + int stemLengthEnd = abs(round((endAnchorBase - m_endAnchor.y()) / m_spatium * 4)); + int stemLengthDictator = isStartDictator ? stemLengthStart : stemLengthEnd; + bool isSmall = m_element->mag() < 1. || m_isGrace; + if (endAnchor.x() > startAnchor.x()) { + /* When beam layout is called before horizontal spacing (see LayoutMeasure::getNextMeasure() to + * know why) the x positions aren't yet determined and may be all zero, which would cause the + * following function to get stuck in a loop. The if() condition avoids that case. */ + if (!isSmall) { + // Adjust anchor stems + offsetBeamWithAnchorShortening(chordRests, dictator, pointer, staffLines, isStartDictator, stemLengthDictator); + } + // Adjust inner stems + offsetBeamToRemoveCollisions(chordRests, dictator, pointer, startAnchor.x(), endAnchor.x(), isFlat, isStartDictator); + } + int beamCount = std::max(beamCountD, beamCountP); + if (!m_tab) { + if (!m_isGrace) { + setValidBeamPositions(dictator, pointer, beamCountD, beamCountP, staffLines, isStartDictator, isFlat, isAscending); + } + if (!forceFlat) { + addMiddleLineSlant(dictator, pointer, beamCount, middleLine, interval, smallSlant ? 1 : slant); + } + } + + m_startAnchor.setY(quarterSpace * (isStartDictator ? dictator : pointer) + m_element->pagePos().y()); + m_endAnchor.setY(quarterSpace * (isStartDictator ? pointer : dictator) + m_element->pagePos().y()); + + bool add8th = true; + if (m_beamType == BeamType::BEAM && toBeam(m_element)->userModified()) { + add8th = false; + } + if (!m_tab && add8th) { + add8thSpaceSlant(isStartDictator ? m_startAnchor : m_endAnchor, dictator, pointer, beamCount, interval, middleLine, isFlat); + } + m_startAnchor.setX(chordBeamAnchorX(startCr, ChordBeamAnchorType::Start)); + m_endAnchor.setX(chordBeamAnchorX(endCr, ChordBeamAnchorType::End)); + return true; +} + +bool BeamTremoloLayout::calculateAnchorsCross() +{ + double spatium = m_element->score()->spatium(); + //int fragmentIndex = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + ChordRest* startCr = m_elements.front(); + ChordRest* endCr = m_elements.back(); + + const double quarterSpace = spatium / 4; + // imagine a line of beamed notes all in a row on the same staff. the first and last of those + // are the 'outside' notes, and the slant of the beam is going to be affected by the 'middle' notes + // between them. + // we have to keep track of this for both staves. + Chord* topFirst = nullptr; + Chord* topLast = nullptr; + Chord* bottomFirst = nullptr; + Chord* bottomLast = nullptr; + int maxMiddleTopLine = std::numeric_limits::min(); // lowest note in the top staff + int minMiddleBottomLine = std::numeric_limits::max(); // highest note in the bottom staff + int prevTopLine = maxMiddleTopLine; // previous note's line position (top) + int prevBottomLine = minMiddleBottomLine; // previous note's line position (bottom) + // if the immediate neighbor of one of the two 'outside' notes on either the top or bottom + // are the same as that outside note, we need to record it so that we can add a 1/4 space slant. + bool secondTopIsSame = false; + bool secondBottomIsSame = false; + bool penultimateTopIsSame = false; + bool penultimateBottomIsSame = false; + double maxY = std::numeric_limits::max(); + double minY = std::numeric_limits::min(); + int otherStaff = 0; + bool isFirstChord = true; + int firstChordStaff = 0; + bool allOneStaff = true; + for (ChordRest* c : m_elements) { + if (!c) { + continue; + } + if (otherStaff == 0) { + otherStaff = c->staffMove(); + } + if (isFirstChord) { + firstChordStaff = otherStaff; + isFirstChord = false; + } else { + if (c->staffMove() != firstChordStaff) { + allOneStaff = false; + break; // this beam is cross + } + } + } + if (otherStaff == 0 || allOneStaff) { + return false; // not cross + } + // Find the notes on the top and bottom of staves + // + bool checkNextTop = false; + bool checkNextBottom = false; + for (ChordRest* cr : m_elements) { + if (!cr->isChord()) { + continue; + } + Chord* c = toChord(cr); + if ((c->staffMove() == otherStaff && otherStaff > 0) || (c->staffMove() != otherStaff && otherStaff < 0)) { + // this chord is on the bottom staff + if (penultimateBottomIsSame) { + // the chord we took as the penultimate bottom note wasn't. + // so treat it properly as a middle note + minMiddleBottomLine = std::min(minMiddleBottomLine, prevBottomLine); + penultimateBottomIsSame = false; + } + checkNextTop = false; // we are no longer looking for the second note in the top + // staff being the same as the first--this note is on the bottom. + if (!bottomFirst) { + bottomFirst = c; + checkNextBottom = true; // this was the first bottom note, so check for second next time + } else { + penultimateBottomIsSame = prevBottomLine == c->line(); + if (!penultimateBottomIsSame) { + minMiddleBottomLine = std::min(minMiddleBottomLine, prevBottomLine); + } + if (checkNextBottom) { + // this is the second bottom note, so we should see if this one is same line as first + secondBottomIsSame = c->line() == bottomFirst->line(); + checkNextBottom = false; + } else { + prevBottomLine = c->line(); + } + bottomLast = c; + } + maxY = std::min(maxY, chordBeamAnchorY(toChord(c))); + } else { + // this chord is on the top staff + if (penultimateTopIsSame) { + // the chord we took as the penultimate top note wasn't. + // so treat it properly as a middle note + maxMiddleTopLine = std::max(maxMiddleTopLine, prevTopLine); + penultimateTopIsSame = false; + } + checkNextBottom = false; // no longer looking for a bottom second note since this is on top + if (!topFirst) { + topFirst = c; + checkNextTop = true; + } else { + penultimateTopIsSame = prevTopLine == c->line(); + if (!penultimateTopIsSame) { + maxMiddleTopLine = std::max(maxMiddleTopLine, prevTopLine); + } + if (checkNextTop) { + secondTopIsSame = c->line() == topFirst->line(); + checkNextTop = false; + } else { + prevTopLine = c->line(); + } + topLast = c; + } + minY = std::max(minY, chordBeamAnchorY(toChord(c))); + } + } + m_startAnchor.ry() = (maxY + minY) / 2; + m_endAnchor.ry() = (maxY + minY) / 2; + m_slope = 0; + + if (!noSlope()) { + int topFirstLine = topFirst ? topFirst->downNote()->line() : 0; + int topLastLine = topLast ? topLast->downNote()->line() : 0; + int bottomFirstLine = bottomFirst ? bottomFirst->upNote()->line() : 0; + int bottomLastLine = bottomLast ? bottomLast->upNote()->line() : 0; + bool constrainTopToQuarter = false; + bool constrainBottomToQuarter = false; + if ((topFirstLine > topLastLine && secondTopIsSame) + || (topFirstLine < topLastLine && penultimateTopIsSame)) { + constrainTopToQuarter = true; + } + if ((bottomFirstLine < bottomLastLine && secondBottomIsSame) + || (bottomFirstLine > bottomLastLine && penultimateBottomIsSame)) { + constrainBottomToQuarter = true; + } + if (!topLast && !bottomLast && topFirst && bottomFirst) { + // if there are only two notes, one on each staff, special case + // take max slope into account + double yFirst, yLast; + // we can't rely on comparing topFirst and bottomFirst ->tick() because beamed + // graces have the same tick + if (m_elements[0] == topFirst) { + yFirst = topFirst->stemPos().y(); + yLast = bottomFirst->stemPos().y(); + } else { + yFirst = bottomFirst->stemPos().y(); + yLast = topFirst->stemPos().y(); + } + int desiredSlant = round((yFirst - yLast) / spatium); + int slant = std::min(std::abs(desiredSlant), getMaxSlope()); + slant *= (desiredSlant < 0) ? -quarterSpace : quarterSpace; + m_startAnchor.ry() += (slant / 2); + m_endAnchor.ry() -= (slant / 2); + } else if (!topLast || !bottomLast) { + // otherwise, if there is only one note on one of the staves, use slope from other staff + int startNote = 0; + int endNote = 0; + bool forceHoriz = false; + if (!topLast) { + startNote = bottomFirstLine; + endNote = bottomLastLine; + if (minMiddleBottomLine <= std::min(startNote, endNote)) { + // there is a note closer to the beam than the start and end notes + // we force horizontal beam here. + forceHoriz = true; + } + } else if (!bottomLast) { + startNote = topFirstLine; + endNote = topLastLine; + if (maxMiddleTopLine >= std::max(startNote, endNote)) { + // same as above, for the top staff + // force horizontal. + forceHoriz = true; + } + } + + if (!forceHoriz) { + int slant = startNote - endNote; + slant = std::min(std::abs(slant), getMaxSlope()); + if ((!bottomLast && constrainTopToQuarter) || (!topLast && constrainBottomToQuarter)) { + slant = 1; + } + double slope = slant * (startNote > endNote ? quarterSpace : -quarterSpace); + m_startAnchor.ry() += (slope / 2); + m_endAnchor.ry() -= (slope / 2); + } // otherwise, do nothing, beam is already horizontal. + } else { + // otherwise, there are at least two notes on each staff + // (that is, topLast and bottomLast are both set) + bool forceHoriz = false; + if (topFirstLine == topLastLine || bottomFirstLine == bottomLastLine) { + // if outside notes on top or bottom staff are on the same staff line, slope = 0 + // no further adjustment needed, the beam is already well-placed and horizontal + forceHoriz = true; + } + // otherwise, we have to compare the slopes from the top staff and bottom staff. + int topSlant = topFirstLine - topLastLine; + if (constrainTopToQuarter && topSlant != 0) { + topSlant = topFirstLine < topLastLine ? -1 : 1; + } + int bottomSlant = bottomFirstLine - bottomLastLine; + if (constrainBottomToQuarter && bottomSlant != 0) { + bottomSlant = bottomFirstLine < bottomLastLine ? -1 : 1; + } + if ((maxMiddleTopLine >= std::max(topFirstLine, topLastLine) + || (minMiddleBottomLine <= std::min(bottomFirstLine, bottomLastLine)))) { + forceHoriz = true; + } + if (topSlant == 0 || bottomSlant == 0 || forceHoriz) { + // if one of the slants is 0, the whole slant is zero + } else if ((topSlant < 0 && bottomSlant < 0) || (topSlant > 0 && bottomSlant > 0)) { + int slant = (abs(topSlant) < abs(bottomSlant)) ? topSlant : bottomSlant; + slant = std::min(std::abs(slant), getMaxSlope()); + double slope = slant * ((topSlant < 0) ? -quarterSpace : quarterSpace); + m_startAnchor.ry() += (slope / 2); + m_endAnchor.ry() -= (slope / 2); + } else { + // if the two slopes are in opposite directions, flat! + // nothing needs to be done, the beam is already horizontal and placed nicely + } + } + m_startAnchor.setX(chordBeamAnchorX(startCr, ChordBeamAnchorType::Start)); + m_endAnchor.setX(chordBeamAnchorX(endCr, ChordBeamAnchorType::End)); + m_slope = (m_endAnchor.y() - m_startAnchor.y()) / (m_endAnchor.x() - m_startAnchor.x()); + } + return true; +} + +bool BeamTremoloLayout::noSlope() +{ + return m_beam && m_beam->noSlope(); +} + +int BeamTremoloLayout::getMiddleStaffLine(ChordRest* startChord, ChordRest* endChord, int staffLines) const +{ + bool isFullSize = RealIsEqual(m_element->mag(), 1.0) && !m_isGrace; + bool useWideBeams = m_element->score()->styleB(Sid::useWideBeams); + int startBeams = strokeCount(startChord); + int endBeams = strokeCount(endChord); + int startMiddleLine = Chord::minStaffOverlap(m_up, staffLines, startBeams, false, m_beamSpacing / 4.0, useWideBeams, isFullSize); + int endMiddleLine = Chord::minStaffOverlap(m_up, staffLines, endBeams, false, m_beamSpacing / 4.0, useWideBeams, !m_isGrace); + + // offset middle line by 1 or -1 since the anchor is at the middle of the beam, + // not at the tip of the stem + if (m_up) { + return std::min(startMiddleLine, endMiddleLine) + 1; + } + return std::max(startMiddleLine, endMiddleLine) - 1; +} + +int BeamTremoloLayout::computeDesiredSlant(int startNote, int endNote, int middleLine, int dictator, int pointer) const +{ + if (m_beamType == BeamType::BEAM && toBeam(m_element)->noSlope()) { + return 0; + } + int dictatorExtension = middleLine - dictator; // we need to make sure that beams extended to the middle line + int pointerExtension = middleLine - pointer; // are properly treated as flat. + if (m_up) { + dictatorExtension = std::min(dictatorExtension, 0); + pointerExtension = std::min(pointerExtension, 0); + } else { + dictatorExtension = std::max(dictatorExtension, 0); + pointerExtension = std::max(pointerExtension, 0); + } + if (dictator + dictatorExtension == middleLine && pointer + pointerExtension == middleLine) { + return 0; + } + if (startNote == endNote) { + return 0; + } + int slopeConstrained = isSlopeConstrained(startNote, endNote); + if (slopeConstrained == 0) { + return 0; + } else if (slopeConstrained == 1) { + return dictator > pointer ? -1 : 1; + } + + // calculate max slope based on distance between first and last chords + int maxSlope = getMaxSlope(); + + // calculate max slope based on note interval + int interval = std::min(std::abs(endNote - startNote), (int)_maxSlopes.size() - 1); + return std::min(maxSlope, _maxSlopes[interval]) * (m_up ? 1 : -1); +} + +int BeamTremoloLayout::isSlopeConstrained(int startNote, int endNote) const +{ + // 0 to constrain to flat, 1 to constrain to 0.25, <0 for no constraint + if (startNote == endNote) { + return 0; + } + // if a note is more extreme than the endpoints, slope is 0 + // p.s. _notes is a sorted vector + if (m_notes.size() > 2) { + if (m_up) { + int higherEnd = std::min(startNote, endNote); + if (higherEnd > m_notes[0]) { + return 0; // a note is higher in the staff than the highest end + } + if (higherEnd == m_notes[0] && higherEnd >= m_notes[1]) { + if (higherEnd > m_notes[1]) { + return 0; // a note is higher in the staff than the highest end + } + size_t chordCount = m_elements.size(); + if (chordCount >= 3 && m_notes.size() >= 3) { + bool middleNoteHigherThanHigherEnd = higherEnd >= m_notes[2]; + if (middleNoteHigherThanHigherEnd) { + return 0; // two notes are the same as the highest end (notes [0] [1] and [2] higher than or same as higherEnd) + } + bool secondNoteSameHeightAsHigherEnd = startNote < endNote && m_elements[1]->isChord() + && toChord(m_elements[1])->upLine() == higherEnd; + bool secondToLastNoteSameHeightAsHigherEnd = endNote < startNote && m_elements[chordCount - 2]->isChord() && toChord( + m_elements[chordCount - 2])->upLine() == higherEnd; + if (!(secondNoteSameHeightAsHigherEnd || secondToLastNoteSameHeightAsHigherEnd)) { + return 0; // only one note same as higher end, but it is not a neighbor + } else { + // there is a single note next to the highest one with equivalent height + // and they are neighbors. this is our exception, so + // the slope may be a max of 0.25. + return 1; + } + } else { + return 0; // only two notes in entire beam, in this case startNote == endNote + } + } + } else { + int lowerEnd = std::max(startNote, endNote); + if (lowerEnd < m_notes[m_notes.size() - 1]) { + return 0; + } + if (lowerEnd == m_notes[m_notes.size() - 1] && lowerEnd <= m_notes[m_notes.size() - 2]) { + if (lowerEnd < m_notes[m_notes.size() - 2]) { + return 0; + } + size_t chordCount = m_elements.size(); + if (chordCount >= 3 && m_notes.size() >= 3) { + bool middleNoteLowerThanLowerEnd = lowerEnd <= m_notes[m_notes.size() - 3]; + if (middleNoteLowerThanLowerEnd) { + return 0; + } + bool secondNoteSameHeightAsLowerEnd = startNote > endNote && m_elements[1]->isChord() + && toChord(m_elements[1])->downLine() == lowerEnd; + bool secondToLastNoteSameHeightAsLowerEnd = endNote > startNote && m_elements[chordCount - 2]->isChord() && toChord( + m_elements[chordCount - 2])->downLine() == lowerEnd; + if (!(secondNoteSameHeightAsLowerEnd || secondToLastNoteSameHeightAsLowerEnd)) { + return 0; + } else { + return 1; + } + } else { + return 0; + } + } + } + } + return -1; +} + +int BeamTremoloLayout::getMaxSlope() const +{ + // for 2-indexed interval i (seconds, thirds, etc.) + // maxSlopes[i] = max slope of beam for notes with interval i + + // calculate max slope based on distance between first and last chords + double endX = chordBeamAnchorX(m_elements[m_elements.size() - 1], ChordBeamAnchorType::Start); + double startX = chordBeamAnchorX(m_elements[0], ChordBeamAnchorType::End); + double beamWidth = endX - startX; + beamWidth /= m_spatium; + int maxSlope = _maxSlopes.back(); + if (beamWidth < 3.0) { + maxSlope = _maxSlopes[1]; + } else if (beamWidth < 5.0) { + maxSlope = _maxSlopes[2]; + } else if (beamWidth < 7.5) { + maxSlope = _maxSlopes[3]; + } else if (beamWidth < 10.0) { + maxSlope = _maxSlopes[4]; + } else if (beamWidth < 15.0) { + maxSlope = _maxSlopes[5]; + } else if (beamWidth < 20.0) { + maxSlope = _maxSlopes[6]; + } else { + maxSlope = _maxSlopes[7]; + } + + return maxSlope; +} + +int BeamTremoloLayout::getBeamCount(const std::vector chordRests) const +{ + int maxBeams = 0; + for (ChordRest* chordRest : chordRests) { + if (chordRest->isChord() && strokeCount(chordRest) > maxBeams) { + maxBeams = strokeCount(chordRest); + } + } + return maxBeams; +} + +double BeamTremoloLayout::chordBeamAnchorX(const ChordRest* cr, ChordBeamAnchorType anchorType) const +{ + double pagePosX = m_trem ? m_trem->pagePos().x() : m_beam->pagePos().x(); + double stemPosX = cr->stemPosX() + cr->pagePos().x() - pagePosX; + + if (!cr->isChord() || !toChord(cr)->stem()) { + if (!m_up) { + // rests always return the right side of the glyph as their stemPosX + // so we need to adjust back to the left side if stems are down + stemPosX -= cr->stemPosX(); + } + return stemPosX; + } + const Chord* chord = toChord(cr); + + double stemWidth = chord->stem()->lineWidth().val() * chord->mag(); + + switch (anchorType) { + case ChordBeamAnchorType::Start: + if (m_tab) { + return stemPosX - 0.5 * stemWidth; + } + + if (chord->up()) { + return stemPosX - stemWidth; + } + + break; + case ChordBeamAnchorType::Middle: + if (m_tab) { + return stemPosX; + } + + return chord->up() ? stemPosX - 0.5 * stemWidth : stemPosX + 0.5 * stemWidth; + + case ChordBeamAnchorType::End: + if (m_tab) { + return stemPosX + 0.5 * stemWidth; + } + + if (!chord->up()) { + return stemPosX + stemWidth; + } + + break; + } + + return stemPosX; +} + +double BeamTremoloLayout::chordBeamAnchorY(const ChordRest* cr) const +{ + if (!cr->isChord()) { + return cr->pagePos().y(); + } + + const Chord* chord = toChord(cr); + Note* note = cr->up() ? chord->downNote() : chord->upNote(); + PointF position = note->pagePos(); + + int upValue = chord->up() ? -1 : 1; + double beamOffset = m_beamWidth / 2 * upValue; + + if (m_isBesideTabStaff) { + double stemLength = m_tab->chordStemLength(chord) * (m_up ? -1 : 1); + double y = m_tab->chordRestStemPosY(chord) + stemLength; + y *= m_spatium; + y -= beamOffset; + return y + chord->pagePos().y(); + } + + return position.y() + (chord->defaultStemLength() * upValue) - beamOffset; +} + +PointF BeamTremoloLayout::chordBeamAnchor(const ChordRest* cr, ChordBeamAnchorType anchorType) const +{ + return PointF(chordBeamAnchorX(cr, anchorType), chordBeamAnchorY(cr)); +} +} diff --git a/src/engraving/libmscore/beamtremololayout.h b/src/engraving/libmscore/beamtremololayout.h new file mode 100644 index 0000000000000..47fd97247d0e2 --- /dev/null +++ b/src/engraving/libmscore/beamtremololayout.h @@ -0,0 +1,109 @@ +/* + * 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 . + */ + +#ifndef __BEAMTREMOLOLAYOUT_H__ +#define __BEAMTREMOLOLAYOUT_H__ + +#include "beam.h" +#include "engravingitem.h" +#include "durationtype.h" +#include "property.h" + +namespace mu::engraving { +class Chord; +class ChordRest; +class Beam; +enum class ActionIconType; +enum class SpannerSegmentType; +class BeamTremoloLayout +{ +public: + BeamTremoloLayout() {} + BeamTremoloLayout(EngravingItem* e); + + double beamDist() const { return m_beamDist; } + double beamWidth() const { return m_beamWidth; } + PointF startAnchor() const { return m_startAnchor; } + PointF endAnchor() const { return m_endAnchor; } + void setAnchors(PointF startAnchor, PointF endAnchor) { m_startAnchor = startAnchor; m_endAnchor = endAnchor; } + + bool calculateAnchors(const std::vector& chordRests, const std::vector& notes); + + enum class ChordBeamAnchorType { + Start, End, Middle + }; + double chordBeamAnchorX(const ChordRest* chord, ChordBeamAnchorType anchorType) const; + double chordBeamAnchorY(const ChordRest* chord) const; + PointF chordBeamAnchor(const ChordRest* chord, ChordBeamAnchorType anchorType) const; + int getMaxSlope() const; + void extendStem(Chord* chord, double addition); + +private: + enum class BeamType { + INVALID, + BEAM, + TREMOLO + }; + BeamType m_beamType = BeamType::INVALID; + EngravingItem* m_element = nullptr; + Beam* m_beam = nullptr; + Tremolo* m_trem = nullptr; + bool m_isValid = false; + bool m_up = false; + Fraction m_tick = Fraction(0, 1); + double m_spatium = 0.; + PointF m_startAnchor; + PointF m_endAnchor; + double m_slope = 0.; + bool m_isGrace = false; + int m_beamSpacing = 0; + double m_beamDist = 0.; + double m_beamWidth = 0.; + std::vector m_elements; + std::vector m_notes; + StaffType const* m_tab = nullptr; + bool m_isBesideTabStaff = false; + + int getMiddleStaffLine(ChordRest* startChord, ChordRest* endChord, int staffLines) const; + int computeDesiredSlant(int startNote, int endNote, int middleLine, int dictator, int pointer) const; + int isSlopeConstrained(int startNote, int endNote) const; + void offsetBeamWithAnchorShortening(std::vector chordRests, int& dictator, int& pointer, int staffLines, + bool isStartDictator, int stemLengthDictator) const; + bool isValidBeamPosition(int yPos, bool isStart, bool isAscending, bool isFlat, int staffLines, bool isOuter) const; + bool isBeamInsideStaff(int yPos, int staffLines, bool allowFloater) const; + int getOuterBeamPosOffset(int innerBeam, int beamCount, int staffLines) const; + void offsetBeamToRemoveCollisions(std::vector chordRests, int& dictator, int& pointer, const double startX, + const double endX, bool isFlat, bool isStartDictator) const; + int getBeamCount(std::vector chordRests) const; + bool is64thBeamPositionException(int& yPos, int staffLines) const; + int findValidBeamOffset(int outer, int beamCount, int staffLines, bool isStart, bool isAscending, bool isFlat) const; + void setValidBeamPositions(int& dictator, int& pointer, int beamCountD, int beamCountP, int staffLines, bool isStartDictator, + bool isFlat, bool isAscending); + void addMiddleLineSlant(int& dictator, int& pointer, int beamCount, int middleLine, int interval, int desiredSlant); + void add8thSpaceSlant(mu::PointF& dictatorAnchor, int dictator, int pointer, int beamCount, int interval, int middleLine, bool Flat); + bool noSlope(); + int strokeCount(ChordRest* cr) const; + bool calculateAnchorsCross(); + bool computeTremoloUp(); +}; +} // namespace mu::engraving +#endif diff --git a/src/engraving/libmscore/chord.cpp b/src/engraving/libmscore/chord.cpp index 4aa87cf8b202e..1e467ffb37426 100644 --- a/src/engraving/libmscore/chord.cpp +++ b/src/engraving/libmscore/chord.cpp @@ -1084,8 +1084,8 @@ void Chord::computeUp() double noteX = stemPosX() + pagePos().x() - base.x(); PointF startAnchor = PointF(); PointF endAnchor = PointF(); - startAnchor = _beam->chordBeamAnchor(firstChord, Beam::ChordBeamAnchorType::Start); - endAnchor = _beam->chordBeamAnchor(lastChord, Beam::ChordBeamAnchorType::End); + startAnchor = _beam->chordBeamAnchor(firstChord, BeamTremoloLayout::ChordBeamAnchorType::Start); + endAnchor = _beam->chordBeamAnchor(lastChord, BeamTremoloLayout::ChordBeamAnchorType::End); if (this == _beam->elements().front()) { _up = noteY > startAnchor.y(); @@ -1098,10 +1098,43 @@ void Chord::computeUp() } } _beam->layout(); + if (cross && _tremolo && _tremolo->twoNotes() && _tremolo->chord1() == this + && _tremolo->chord1()->beam() == _tremolo->chord2()->beam()) { + _tremolo->layout(); // beam-infixed two-note trems have to be laid out here + } if (!cross && !_beam->userModified()) { _up = _beam->up(); } return; + } else if (_tremolo && _tremolo->twoNotes()) { + Chord* c1 = _tremolo->chord1(); + Chord* c2 = _tremolo->chord2(); + bool cross = c1->staffMove() != c2->staffMove(); + Measure* measure = findMeasure(); + if (!cross && !_tremolo->userModified()) { + _up = _tremolo->up(); + } + if (!measure->explicitParent()) { + // this method will be called later (from Measure::layoutCrossStaff) after the + // system is completely laid out. + // this is necessary because otherwise there's no way to deal with cross-staff beams + // because we don't know how far apart the staves actually are + return; + } + _tremolo->layout(); + if (_tremolo->userModified()) { + Note* baseNote = _up ? downNote() : upNote(); + PairF beamPos = _tremolo->beamPos(); + double tremY = c1 == this ? beamPos.first : beamPos.second; + tremY *= spatium(); + tremY += pagePos().y(); + double noteY = baseNote->pagePos().y(); + _up = noteY > tremY; + } + if (!cross && !_tremolo->userModified()) { + _up = _tremolo->up(); + } + return; } bool staffHasMultipleVoices = measure()->hasVoices(staffIdx(), tick(), actualTicks()); @@ -1497,12 +1530,20 @@ int Chord::calcMinStemLength() // minStemLength = ceil(minStemLength / 2.0) * 2; } } - if (_beam) { + if (_beam || (_tremolo && _tremolo->twoNotes())) { + int beamCount = (_beam ? beams() : 0) + (_tremolo ? _tremolo->lines() : 0); static const int minInnerStemLengths[4] = { 10, 9, 8, 7 }; - int innerStemLength = minInnerStemLengths[std::min(beams(), 3)]; - int beamsHeight = beams() * (score()->styleB(Sid::useWideBeams) ? 4 : 3) - 1; + int innerStemLength = minInnerStemLengths[std::min(beamCount, 3)]; + int beamsHeight = beamCount * (score()->styleB(Sid::useWideBeams) ? 4 : 3) - 1; minStemLength = std::max(minStemLength, innerStemLength); minStemLength += beamsHeight; + // for 4+ beams, there are a few situations where we need to lengthen the stem by 1 + int noteLine = line(); + int staffLines = staff()->lines(tick()); + bool noteInStaff = (_up && noteLine > 0) || (!_up && noteLine < (staffLines - 1) * 2); + if (beamCount >= 4 && noteInStaff) { + minStemLength++; + } } return minStemLength; } @@ -1513,7 +1554,7 @@ int Chord::stemLengthBeamAddition() const if (_hook) { return 0; } - int beamCount = beams(); + int beamCount = (_beam ? beams() : 0) + (_tremolo ? _tremolo->lines() : 0); switch (beamCount) { case 0: case 1: @@ -1558,8 +1599,11 @@ int Chord::maxReduction(int extensionOutsideStaff) const { 0, 1, 1, 1, 1 }, // 2 beams { 0, 0, 0, 1, 1 }, // 3 beams }; + int beamCount = 0; + if (!_hook) { + beamCount = _tremolo ? _tremolo->lines() + (_beam ? beams() : 0) : beams(); + } bool hasTradHook = _hook && !score()->styleB(Sid::useStraightNoteFlags); - int beamCount = hasTradHook ? 0 : beams(); if (_hook && !hasTradHook) { beamCount = std::min(beamCount, 2); // the straight glyphs extend outwards after 2 beams } @@ -1601,7 +1645,7 @@ int Chord::stemOpticalAdjustment(int stemEndPosition) const if (_hook && !_beam) { return 0; } - int beamCount = beams(); + int beamCount = (_tremolo ? _tremolo->lines() : 0) + (_beam ? beams() : 0); if (beamCount == 0 || beamCount > 2) { return 0; } @@ -1674,8 +1718,9 @@ double Chord::calcDefaultStemLength() int staffLineCount = staffItem ? staffItem->lines(tick()) : 5; int shortStemStart = score()->styleI(Sid::shortStemStartLocation) * quarterSpacesPerLine + 1; bool useWideBeams = score()->styleB(Sid::useWideBeams); - - int middleLine = minStaffOverlap(_up, staffLineCount, beams(), !!_hook, useWideBeams ? 4 : 3, useWideBeams, !(isGrace() || isSmall())); + int beamCount = (_tremolo ? _tremolo->lines() : 0) + (_beam ? beams() : 0); + int middleLine + = minStaffOverlap(_up, staffLineCount, beamCount, !!_hook, useWideBeams ? 4 : 3, useWideBeams, !(isGrace() || isSmall())); if (up()) { int stemEndPosition = upLine() * quarterSpacesPerLine - defaultStemLength; double stemEndPositionMag = (double)upLine() * quarterSpacesPerLine - (defaultStemLength * intrinsicMag()); @@ -1711,7 +1756,7 @@ double Chord::calcDefaultStemLength() stemLength = std::max(idealStemLength, minStemLengthQuarterSpaces); } - if (beams() == 4 && _beam) { + if (beamCount == 4 && !_hook) { stemLength = calc4BeamsException(stemLength); } @@ -1797,7 +1842,8 @@ bool Chord::shouldHaveHook() const { return shouldHaveStem() && durationType().hooks() > 0 - && !beam(); + && !beam() + && !(tremolo() && tremolo()->twoNotes()); } void Chord::createStem() diff --git a/src/engraving/libmscore/libmscore.cmake b/src/engraving/libmscore/libmscore.cmake index 0f6e8988b3713..937d787e9458b 100644 --- a/src/engraving/libmscore/libmscore.cmake +++ b/src/engraving/libmscore/libmscore.cmake @@ -37,6 +37,8 @@ set(LIBMSCORE_SRC ${CMAKE_CURRENT_LIST_DIR}/barline.h ${CMAKE_CURRENT_LIST_DIR}/beam.cpp ${CMAKE_CURRENT_LIST_DIR}/beam.h + ${CMAKE_CURRENT_LIST_DIR}/beamtremololayout.cpp + ${CMAKE_CURRENT_LIST_DIR}/beamtremololayout.h ${CMAKE_CURRENT_LIST_DIR}/bend.cpp ${CMAKE_CURRENT_LIST_DIR}/bend.h ${CMAKE_CURRENT_LIST_DIR}/box.cpp diff --git a/src/engraving/libmscore/measure.cpp b/src/engraving/libmscore/measure.cpp index 45fcf653b857e..7ffa8c69f0784 100644 --- a/src/engraving/libmscore/measure.cpp +++ b/src/engraving/libmscore/measure.cpp @@ -3347,7 +3347,10 @@ void Measure::layoutCrossStaff() } if (e->isChord()) { Chord* c = toChord(e); - if (c->beam() && (c->beam()->cross() || c->beam()->userModified())) { + Beam* beam = c->beam(); + Tremolo* tremolo = c->tremolo(); + if ((beam && (beam->cross() || beam->userModified())) + || tremolo && tremolo->twoNotes() && tremolo->userModified()) { c->computeUp(); // for cross-staff beams } if (!c->graceNotes().empty()) { diff --git a/src/engraving/libmscore/system.cpp b/src/engraving/libmscore/system.cpp index 6190d237bb25b..17e3898f768cf 100644 --- a/src/engraving/libmscore/system.cpp +++ b/src/engraving/libmscore/system.cpp @@ -56,6 +56,7 @@ #include "system.h" #include "systemdivider.h" #include "textframe.h" +#include "tremolo.h" #ifndef ENGRAVING_NO_ACCESSIBILITY #include "accessibility/accessibleitem.h" @@ -2137,6 +2138,14 @@ bool System::hasCrossStaffOrModifiedBeams() if (toChordRest(e)->beam() && (toChordRest(e)->beam()->cross() || toChordRest(e)->beam()->userModified())) { return true; } + Chord* c = e->isChord() ? toChord(e) : nullptr; + if (c && c->tremolo() && c->tremolo()->twoNotes()) { + Chord* c1 = c->tremolo()->chord1(); + Chord* c2 = c->tremolo()->chord2(); + if (c->tremolo()->userModified() || c1->staffMove() != c2->staffMove()) { + return true; + } + } if (e->isChord() && !toChord(e)->graceNotes().empty()) { for (Chord* grace : toChord(e)->graceNotes()) { if (grace->beam() && (grace->beam()->cross() || grace->beam()->userModified())) { diff --git a/src/engraving/libmscore/tremolo.cpp b/src/engraving/libmscore/tremolo.cpp index 592423d1df54d..57ffa9d8aaca1 100644 --- a/src/engraving/libmscore/tremolo.cpp +++ b/src/engraving/libmscore/tremolo.cpp @@ -39,6 +39,7 @@ #include "score.h" #include "staff.h" #include "stem.h" +#include "system.h" #include "log.h" @@ -108,6 +109,121 @@ double Tremolo::minHeight() const return (lines() - 1) * td + sw; } +void Tremolo::createBeamSegments() +{ + // TODO: This should be a style setting, to replace tremoloStrokeLengthMultiplier + static constexpr double stemGapSp = 1.0; + const bool defaultStyle = (!customStyleApplicable()) || (_style == TremoloStyle::DEFAULT); + + DeleteAll(_beamSegments); + _beamSegments.clear(); + if (!twoNotes()) { + return; + } + bool _isGrace = _chord1->isGrace(); + PointF startAnchor = _layoutInfo.startAnchor() - PointF(0., pagePos().y()); + PointF endAnchor = _layoutInfo.endAnchor() - PointF(0., pagePos().y()); + + // inset trem from stems for default style + double slope = (endAnchor.y() - startAnchor.y()) / (endAnchor.x() - startAnchor.x()); + double gapSp = stemGapSp; + if (defaultStyle) { + // we can eat into the stemGapSp margin if the anchorpoints are sufficiently close together + double widthSp = (endAnchor.x() - startAnchor.x()) / spatium() - (stemGapSp * 2); + if (!RealIsEqualOrMore(widthSp, 0.6)) { + // tremolo beam is too short; we can eat into the gap spacing a little + gapSp = std::max(stemGapSp - ((0.6 - widthSp) * 0.5), 0.4); + } + } else { + gapSp = 0.0; + } + double offset = gapSp * spatium(); + startAnchor.rx() += offset; + endAnchor.rx() -= offset; + startAnchor.ry() += offset * slope; + endAnchor.ry() -= offset * slope; + BeamSegment* mainStroke = new BeamSegment(this); + mainStroke->level = 0; + mainStroke->line = LineF(startAnchor, endAnchor); + _beamSegments.push_back(mainStroke); + double bboxTop = _up ? std::min(mainStroke->line.y1(), mainStroke->line.y2()) : std::max(mainStroke->line.y1(), mainStroke->line.y2()); + double halfWidth = score()->styleMM(Sid::beamWidth).val() / 2. * (_up ? -1. : 1.); + RectF bbox = RectF(mainStroke->line.x1(), bboxTop + halfWidth, mainStroke->line.x2() - mainStroke->line.x1(), + std::abs(mainStroke->line.y2() - mainStroke->line.y1()) - halfWidth * 2.); + PointF beamOffset = PointF(0., (_up ? 1 : -1) * spatium() * (score()->styleB(Sid::useWideBeams) ? 1. : 0.75)); + beamOffset.setY(beamOffset.y() * mag() * (_isGrace ? score()->styleD(Sid::graceNoteMag) : 1.)); + for (int i = 1; i < lines(); ++i) { + BeamSegment* stroke = new BeamSegment(this); + stroke->level = i; + stroke->line = LineF(startAnchor + (beamOffset * (double)i), endAnchor + (beamOffset * (double)i)); + _beamSegments.push_back(stroke); + bbox.unite(bbox.translated(0., beamOffset.y() * (double)i)); + } + setbbox(bbox); + + // size stems properly + if (_chord1->stem() && _chord2->stem() && !(_chord1->beam() && _chord1->beam() == _chord2->beam())) { + // we don't need to do anything if these chords are part of the same beam--their stems are taken care of + // by the beam layout + int beamSpacing = score()->styleB(Sid::useWideBeams) ? 4 : 3; + bool cross = chord1()->staffMove() != chord2()->staffMove(); + for (ChordRest* cr : { _chord1, _chord2 }) { + Chord* chord = toChord(cr); + double addition = 0.0; + if (cross && cr->staffMove() != 0 && lines() > 1) { + // need to adjust further for beams on the opposite side + addition += (lines() - 1.) * beamSpacing / 4. * spatium() * mag(); + } + // calling extendStem with addition 0.0 still sizes the stem to the manually adjusted height of the trem. + _layoutInfo.extendStem(chord, addition); + } + } +} + +//--------------------------------------------------------- +// chordBeamAnchor +//--------------------------------------------------------- + +PointF Tremolo::chordBeamAnchor(const ChordRest* chord, BeamTremoloLayout::ChordBeamAnchorType anchorType) const +{ + return _layoutInfo.chordBeamAnchor(chord, anchorType); +} + +double Tremolo::beamWidth() const +{ + return _layoutInfo.beamWidth(); +} + +//--------------------------------------------------------- +// drag +//--------------------------------------------------------- + +RectF Tremolo::drag(EditData& ed) +{ + if (!twoNotes()) { + return EngravingItem::drag(ed); + } + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + double dy = ed.pos.y() - ed.lastPos.y(); + + double y1 = _beamFragment.py1[idx]; + double y2 = _beamFragment.py2[idx]; + + y1 += dy; + y2 += dy; + + double _spatium = spatium(); + // Because of the logic in Tremolo::setProperty(), + // changing Pid::BEAM_POS only has an effect if Pid::USER_MODIFIED is true. + undoChangeProperty(Pid::USER_MODIFIED, true); + undoChangeProperty(Pid::BEAM_POS, PairF(y1 / _spatium, y2 / _spatium)); + undoChangeProperty(Pid::GENERATED, false); + + triggerLayout(); + + return canvasBoundingRect(); +} + //--------------------------------------------------------- // draw //--------------------------------------------------------- @@ -119,10 +235,33 @@ void Tremolo::draw(mu::draw::Painter* painter) const if (isBuzzRoll()) { painter->setPen(curColor()); drawSymbol(SymId::buzzRoll, painter); - } else { + } else if (!twoNotes() || !explicitParent()) { painter->setBrush(Brush(curColor())); painter->setNoPen(); painter->drawPath(path); + } else if (twoNotes() && !_beamSegments.empty()) { + // two-note trems act like beams + + // make beam thickness independent of slant + // (expression can be simplified?) + const LineF bs = _beamSegments.front()->line; + double d = (std::abs(bs.y2() - bs.y1())) / (bs.x2() - bs.x1()); + if (_beamSegments.size() > 1 && d > M_PI / 6.0) { + d = M_PI / 6.0; + } + double ww = (score()->styleMM(Sid::beamWidth).val() / 2.0) / sin(M_PI_2 - atan(d)); + painter->setBrush(Brush(curColor())); + painter->setNoPen(); + for (const BeamSegment* bs1 : _beamSegments) { + painter->drawPolygon( + PolygonF({ + PointF(bs1->line.x1(), bs1->line.y1() - ww), + PointF(bs1->line.x2(), bs1->line.y2() - ww), + PointF(bs1->line.x2(), bs1->line.y2() + ww), + PointF(bs1->line.x1(), bs1->line.y1() + ww), + }), + draw::FillRule::OddEvenFill); + } } // for palette if (!explicitParent() && !twoNotes()) { @@ -228,7 +367,6 @@ PainterPath Tremolo::basePath(double stretch) const // first line ppath.addRect(-w2, 0.0, 2.0 * w2, lw); - double ty = td; // other lines @@ -273,6 +411,39 @@ void Tremolo::computeShape() } } +//--------------------------------------------------------- +// reset +//--------------------------------------------------------- + +void Tremolo::reset() +{ + if (userModified()) { + //undoChangeProperty(Pid::BEAM_POS, PropertyValue::fromValue(beamPos())); + undoChangeProperty(Pid::USER_MODIFIED, false); + } + undoChangeProperty(Pid::STEM_DIRECTION, DirectionV::AUTO); + resetProperty(Pid::BEAM_NO_SLOPE); + setGenerated(true); +} + +//--------------------------------------------------------- +// pagePos +//--------------------------------------------------------- + +PointF Tremolo::pagePos() const +{ + EngravingObject* e = explicitParent(); + while (e && (!e->isSystem() && e->explicitParent())) { + e = e->explicitParent(); + } + if (!e || !e->isSystem()) { + return pos(); + } + System* s = toSystem(e); + double yp = y() + s->staff(staffIdx())->y() + s->y(); + return PointF(pageX(), yp); +} + //--------------------------------------------------------- // layoutOneNoteTremolo //--------------------------------------------------------- @@ -343,7 +514,8 @@ void Tremolo::layoutTwoNotesTremolo(double x, double y, double h, double spatium _up = beam->up(); _direction = beam->beamDirection(); // stem stuff is already taken care of by the beams - } else { + } else if (!userModified()) { + // user modified trems will be dealt with later bool hasVoices = _chord1->measure()->hasVoices(_chord1->staffIdx(), _chord1->tick(), _chord2->tick() - _chord1->tick()); if (_chord1->stemDirection() == DirectionV::AUTO && _chord2->stemDirection() == DirectionV::AUTO && _chord1->staffMove() == _chord2->staffMove() && !hasVoices) { @@ -374,193 +546,63 @@ void Tremolo::layoutTwoNotesTremolo(double x, double y, double h, double spatium _chord1->layoutStem(); _chord2->layoutStem(); } - //--------------------------------------------------- - // Step 1: Calculate the position of the tremolo (x, y) - //--------------------------------------------------- - Stem* stem1 = _chord1->stem(); - Stem* stem2 = _chord2->stem(); - - // compute the y coordinates of the tips of the stems - double y1, y2; - double firstChordStaffY; - - if (stem2 && stem1) { - // stemPageYOffset variable is used for the case when the first - // chord is cross-staff - firstChordStaffY = stem1->pagePos().y() - stem1->y(); // y coordinate of the staff of the first chord - y1 = stem1->y() + stem1->p2().y(); - y2 = stem2->pagePos().y() - firstChordStaffY + stem2->p2().y(); // ->p2().y() is better than ->stemLen() - } else { - Note* note1 = _up ? _chord1->downNote() : _chord1->upNote(); - - firstChordStaffY = note1->pagePos().y() - note1->y(); // y coordinate of the staff of the first chord - const std::pair extendedLen - = LayoutTremolo::extendedStemLenWithTwoNoteTremolo(this, _chord1->defaultStemLength(), _chord2->defaultStemLength()); - y1 = _chord1->stemPos().y() - firstChordStaffY + (extendedLen.first * (_up ? -1 : 1)); - y2 = _chord2->stemPos().y() - firstChordStaffY + (extendedLen.second * (_up ? -1 : 1)); - } - - double lw = spatium * score()->styleS(Sid::tremoloStrokeWidth).val(); - if (_chord1->beams() == 0 && _chord2->beams() == 0) { - // improve the case when one stem is up and another is down - if (defaultStyle && _chord1->up() != _chord2->up() && !crossStaffBeamBetween()) { - double meanNote1Y = .5 - * (_chord1->upNote()->pagePos().y() - firstChordStaffY + _chord1->downNote()->pagePos().y() - - firstChordStaffY); - double meanNote2Y = .5 - * (_chord2->upNote()->pagePos().y() - firstChordStaffY + _chord2->downNote()->pagePos().y() - - firstChordStaffY); - y1 = .5 * (y1 + meanNote1Y); - y2 = .5 * (y2 + meanNote2Y); - } - if (!defaultStyle && _chord1->up() == _chord2->up()) { - y1 += _chord1->up() ? -lw / 2.0 : lw / 2.0; - y2 += _chord1->up() ? -lw / 2.0 : lw / 2.0; - } - } - y = (y1 + y2) * .5; - if (!_chord1->up()) { - y -= isTraditionalAlternate ? lw * .5 : path.boundingRect().height() * .5; - } - if (!_chord2->up()) { - y -= isTraditionalAlternate ? lw * .5 : path.boundingRect().height() * .5; - } - - // compute the x coordinates of - // the inner edge of the stems (default beam style) - // the outer edge of the stems (non-default beam style) - double x2 = _chord2->stemPosBeam().x(); - if (!stem2 && _chord2->up()) { - x2 -= _chord2->noteHeadWidth(); - } else if (stem2) { - if (defaultStyle && _chord2->up()) { - x2 -= stem2->lineWidthMag(); - } else if (!defaultStyle && !_chord2->up()) { - x2 += stem2->lineWidthMag(); - } - } - double x1 = _chord1->stemPosBeam().x(); - if (!stem1 && !_chord1->up()) { - x1 += _chord1->noteHeadWidth(); - } else if (stem1) { - if (defaultStyle && !_chord1->up()) { - x1 += stem1->lineWidthMag(); - } else if (!defaultStyle && _chord1->up()) { - x1 -= stem1->lineWidthMag(); - } - } - x = (x2 + x1) * .5 - _chord1->pagePos().x(); - - double slope = (y2 - y1) / (x2 - x1); - double gapSp = stemGapSp; - if (defaultStyle) { - // we can eat into the stemGapSp margin if the anchorpoints are sufficiently close together - double widthSp = (x2 - x1) / spatium - (stemGapSp * 2); - if (!RealIsEqualOrMore(widthSp, 0.6)) { - // tremolo beam is too short; we can eat into the gap spacing a little - gapSp = std::max(stemGapSp - ((0.6 - widthSp) * 0.5), 0.4); - } - } else { - gapSp = 0.0; - } - // add offsets to the x endpoints - double offset = gapSp * spatium; // offset from stems (or original position) - x2 -= offset; // apply offset horizontally - x1 += offset; - // apply offset vertically to maintain the same slope - y1 += offset * slope; - y2 -= offset * slope; - - //--------------------------------------------------- - // Step 2: Stretch the tremolo strokes horizontally - // from the form of a one-note tremolo (done in basePath()) - // to that of a two-note tremolo according to the distance between the two chords - //--------------------------------------------------- - - Transform xScaleTransform; - const double MAX_H_LENGTH = spatium * 15.0; - - const double defaultLength = std::min(x2 - x1, MAX_H_LENGTH); - double xScaleFactor = defaultStyle ? defaultLength : (x2 - x1); - const double origTremWidth = spatium * score()->styleS(Sid::tremoloWidth).val(); - xScaleFactor /= origTremWidth; - if (_style == TremoloStyle::TRADITIONAL_ALTERNATE) { - path = basePath(xScaleFactor); - } - - xScaleTransform.scale(xScaleFactor, 1.0); - path = xScaleTransform.map(path); - - //--------------------------------------------------- - // Step 3: Calculate the adjustment of the position of the tremolo - // if the chords are connected by a beam so as not to collide with it - //--------------------------------------------------- - - double beamYOffset = 0.0; - - if (_chord1->beams() == _chord2->beams() && _chord1->beam()) { - int beams = _chord1->beams(); - double beamHalfLineWidth = point(score()->styleS(Sid::beamWidth)) * .5 * chordMag(); - beamYOffset = beams * _chord1->beam()->beamDist() - beamHalfLineWidth; - if (_chord1->up() != _chord2->up()) { // cross-staff - beamYOffset = 2 * beamYOffset + beamHalfLineWidth; - } else if (!_chord1->up() && !_chord2->up()) { - beamYOffset = -beamYOffset; + _layoutInfo = BeamTremoloLayout(this); + _startAnchor = _layoutInfo.chordBeamAnchor(_chord1, BeamTremoloLayout::ChordBeamAnchorType::Start); + _endAnchor = _layoutInfo.chordBeamAnchor(_chord2, BeamTremoloLayout::ChordBeamAnchorType::End); + // deal with manual adjustments here and return + PropertyValue val = getProperty(Pid::PLACEMENT); + if (userModified()) { + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + double startY = _beamFragment.py1[idx]; + double endY = _beamFragment.py2[idx]; + if (score()->styleB(Sid::snapCustomBeamsToGrid)) { + const double quarterSpace = EngravingItem::spatium() / 4; + startY = round(startY / quarterSpace) * quarterSpace; + endY = round(endY / quarterSpace) * quarterSpace; } + startY += pagePos().y(); + endY += pagePos().y(); + _startAnchor.setY(startY); + _endAnchor.setY(endY); + _layoutInfo.setAnchors(_startAnchor, _endAnchor); + _chord1->layoutStem(); + _chord2->layoutStem(); + createBeamSegments(); + return; } - - //--------------------------------------------------- - // Step 4: Tilt the tremolo strokes according to the stems of the chords - //--------------------------------------------------- - - Transform shearTransform; - double dy = y2 - y1; - double dx = x2 - x1; - if (_chord1->beams() == 0 && _chord2->beams() == 0) { - if (_chord1->up() && !_chord2->up()) { - dy -= isTraditionalAlternate ? lw : path.boundingRect().height(); - if (!defaultStyle) { - dy += lw; - } - } else if (!_chord1->up() && _chord2->up()) { - dy += isTraditionalAlternate ? lw : path.boundingRect().height(); - if (!defaultStyle) { - dy -= lw; + setPosY(0.); + std::vector chordRests{ chord1(), chord2() }; + std::vector notes; + double mag = 0.0; + + notes.clear(); + staff_idx_t staffIdx = mu::nidx; + for (ChordRest* cr : chordRests) { + double m = cr->isSmall() ? score()->styleD(Sid::smallNoteMag) : 1.0; + mag = std::max(mag, m); + if (cr->isChord()) { + Chord* chord = toChord(cr); + staffIdx = chord->vStaffIdx(); + int i = chord->staffMove(); + //_minMove = std::min(_minMove, i); todo: investigate this + //_maxMove = std::max(_maxMove, i); + + for (int distance : chord->noteDistances()) { + notes.push_back(distance); } } } - // Make tremolo strokes less steep if two chords have the opposite stem directions, - // except for two cases: - // 1. The tremolo doesn't have the default style. - // In this case tremolo strokes should attach to the ends of both stems, so no adjustment needed; - // 2. The chords are on different staves and the tremolo is between them. - // The layout should be improved by extending both stems, so changes are not needed here. - if (_chord1->up() != _chord2->up() && defaultStyle && !crossStaffBeamBetween()) { - dy = std::min(std::max(dy, -1.0 * spatium / defaultLength * dx), 1.0 * spatium / defaultLength * dx); - } - double ds = dy / dx; - shearTransform.shear(0.0, ds); - path = shearTransform.map(path); - //--------------------------------------------------- - // Step 5: Flip the tremolo strokes if necessary - // By default, a TRADITIONAL_ALTERNATE tremolo has its attached-to-stem stroke be above other strokes, - // see basePath(). - // But if both chords have stems facing down, - // the tremolo should be flipped to have the attached-to-stem stroke be below other strokes. - //--------------------------------------------------- - - if (isTraditionalAlternate && !_chord1->up() && !_chord2->up()) { - Transform rotateTransform; - rotateTransform.translate(0.0, lw * .5); - rotateTransform.rotate(180); - rotateTransform.translate(0.0, -lw * .5); - path = rotateTransform.map(path); - } - - setbbox(path.boundingRect()); - setPos(x, y + beamYOffset); + std::sort(notes.begin(), notes.end()); + setMag(mag); + _layoutInfo.calculateAnchors(chordRests, notes); + _startAnchor = _layoutInfo.startAnchor(); + _endAnchor = _layoutInfo.endAnchor(); + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + _beamFragment.py1[idx] = _startAnchor.y() - pagePos().y(); + _beamFragment.py2[idx] = _endAnchor.y() - pagePos().y(); + createBeamSegments(); } //--------------------------------------------------------- @@ -687,6 +729,28 @@ void Tremolo::write(XmlWriter& xml) const writeProperty(xml, Pid::TREMOLO_TYPE); writeProperty(xml, Pid::TREMOLO_STYLE); EngravingItem::writeProperties(xml); + if (!twoNotes()) { + xml.endElement(); + return; + } + // write manual adjustments to file + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + if (_userModified[idx]) { + double _spatium = spatium(); + + xml.startElement("Fragment"); + xml.tag("y1", _beamFragment.py1[idx] / _spatium); + xml.tag("y2", _beamFragment.py2[idx] / _spatium); + xml.endElement(); + } + + // this info is used for regression testing + // l1/l2 is the beam position of the layout engine + if (MScore::testMode) { + double spatium8 = spatium() * .125; + xml.tag("l1", int(lrint(_beamFragment.py1[idx] / spatium8))); + xml.tag("l2", int(lrint(_beamFragment.py2[idx] / spatium8))); + } xml.endElement(); } @@ -707,6 +771,22 @@ void Tremolo::read(XmlReader& e) else if (tag == "strokeStyle") { setStyle(TremoloStyle(e.readInt())); setPropertyFlags(Pid::TREMOLO_STYLE, PropertyFlags::UNSTYLED); + } else if (tag == "Fragment") { + BeamFragment f = BeamFragment(); + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + _userModified[idx] = true; + double _spatium = spatium(); + while (e.readNextStartElement()) { + const AsciiStringView tag1(e.name()); + if (tag1 == "y1") { + f.py1[idx] = e.readDouble() * _spatium; + } else if (tag1 == "y2") { + f.py2[idx] = e.readDouble() * _spatium; + } else { + e.unknown(); + } + } + _beamFragment = f; } else if (readStyledProperty(e, tag)) { } else if (!EngravingItem::readProperties(e)) { e.unknown(); @@ -749,6 +829,133 @@ Fraction Tremolo::tremoloLen() const return f; } +//--------------------------------------------------------- +// setBeamPos +//--------------------------------------------------------- + +void Tremolo::setBeamPos(const PairF& bp) +{ + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + _userModified[idx] = true; + setGenerated(false); + + double _spatium = spatium(); + _beamFragment.py1[idx] = bp.first * _spatium; + _beamFragment.py2[idx] = bp.second * _spatium; +} + +//--------------------------------------------------------- +// beamPos +//--------------------------------------------------------- + +PairF Tremolo::beamPos() const +{ + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + double _spatium = spatium(); + return PairF(_beamFragment.py1[idx] / _spatium, _beamFragment.py2[idx] / _spatium); +} + +//--------------------------------------------------------- +// userModified +//--------------------------------------------------------- + +bool Tremolo::userModified() const +{ + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + return _userModified[idx]; +} + +//--------------------------------------------------------- +// setUserModified +//--------------------------------------------------------- + +void Tremolo::setUserModified(bool val) +{ + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + _userModified[idx] = val; +} + +//--------------------------------------------------------- +// triggerLayout +//--------------------------------------------------------- + +void Tremolo::triggerLayout() const +{ + if (twoNotes() && _chord1 && _chord2) { + toChordRest(_chord1)->triggerLayout(); + toChordRest(_chord2)->triggerLayout(); + } else { + EngravingItem::triggerLayout(); + } +} + +//--------------------------------------------------------- +// gripsPositions +//--------------------------------------------------------- + +std::vector Tremolo::gripsPositions(const EditData&) const +{ + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + + ChordRest* c1 = nullptr; + ChordRest* c2 = nullptr; + + if (!twoNotes()) { + return std::vector(); + } + + int y = pagePos().y(); + double beamStartX = _startAnchor.x() + _chord1->pageX(); + double beamEndX = _endAnchor.x() + _chord1->pageX(); // intentional--chord1 is start x + double middleX = (beamStartX + beamEndX) / 2; + double middleY = (_beamFragment.py1[idx] + y + _beamFragment.py2[idx] + y) / 2; + + return { + PointF(beamStartX, _beamFragment.py1[idx] + y), + PointF(beamEndX, _beamFragment.py2[idx] + y), + PointF(middleX, middleY) + }; +} + +//--------------------------------------------------------- +// endEdit +//--------------------------------------------------------- + +void Tremolo::endEdit(EditData& ed) +{ + EngravingItem::endEdit(ed); +} + +//--------------------------------------------------------- +// editDrag +//--------------------------------------------------------- + +void Tremolo::editDrag(EditData& ed) +{ + int idx = (_direction == DirectionV::AUTO || _direction == DirectionV::DOWN) ? 0 : 1; + double dy = ed.delta.y(); + double y1 = _beamFragment.py1[idx]; + double y2 = _beamFragment.py2[idx]; + + if (ed.curGrip == Grip::MIDDLE) { + y1 += dy; + y2 += dy; + } else if (ed.curGrip == Grip::START) { + y1 += dy; + } else if (ed.curGrip == Grip::END) { + y2 += dy; + } + + double _spatium = spatium(); + // Because of the logic in Beam::setProperty(), + // changing Pid::BEAM_POS only has an effect if Pid::USER_MODIFIED is true. + undoChangeProperty(Pid::USER_MODIFIED, true); + undoChangeProperty(Pid::BEAM_POS, PairF(y1 / _spatium, y2 / _spatium)); + undoChangeProperty(Pid::GENERATED, false); + + triggerLayout(); +} + //--------------------------------------------------------- // subtypeName //--------------------------------------------------------- @@ -813,6 +1020,14 @@ bool Tremolo::setProperty(Pid propertyId, const PropertyValue& val) case Pid::STEM_DIRECTION: setBeamDirection(val.value()); break; + case Pid::USER_MODIFIED: + setUserModified(val.toBool()); + break; + case Pid::BEAM_POS: + if (userModified()) { + setBeamPos(val.value()); + } + break; default: return EngravingItem::setProperty(propertyId, val); } diff --git a/src/engraving/libmscore/tremolo.h b/src/engraving/libmscore/tremolo.h index 3ebf27888d268..600973ee85abc 100644 --- a/src/engraving/libmscore/tremolo.h +++ b/src/engraving/libmscore/tremolo.h @@ -28,6 +28,8 @@ #include "durationtype.h" #include "draw/types/painterpath.h" #include "types/types.h" +#include "beam.h" +#include "chord.h" namespace mu::engraving { class Chord; @@ -50,11 +52,21 @@ class Tremolo final : public EngravingItem Chord* _chord2 { nullptr }; TDuration _durationType; bool _up{ true }; + bool _userModified[2]{ false }; // 0: auto/down 1: up DirectionV _direction; mu::draw::PainterPath path; + std::vector _beamSegments; + BeamTremoloLayout _layoutInfo; + mu::PointF _startAnchor; + mu::PointF _endAnchor; int _lines = 0; // derived from _subtype TremoloStyle _style { TremoloStyle::DEFAULT }; + // for _startAnchor and _slope, once we decide to change trems so that they can + // continue from one system to the other (or indeed, one measure to the other) + // we will want a vector of fragments similar to Beam's _beamFragments structure. + // for now, a single fragment is sufficient + BeamFragment _beamFragment; friend class Factory; Tremolo(Chord* parent); @@ -64,6 +76,8 @@ class Tremolo final : public EngravingItem void computeShape(); void layoutOneNoteTremolo(double x, double y, double h, double spatium); void layoutTwoNotesTremolo(double x, double y, double h, double spatium); + void createBeamSegments(); + void setBeamPos(const PairF& bp); public: @@ -82,9 +96,13 @@ class Tremolo final : public EngravingItem TremoloType tremoloType() const { return _tremoloType; } double minHeight() const; + void reset() override; + + PointF chordBeamAnchor(const ChordRest* chord, BeamTremoloLayout::ChordBeamAnchorType anchorType) const; double chordMag() const; double mag() const override; + RectF drag(EditData&) override; void draw(mu::draw::Painter*) const override; void layout() override; void layout2(); @@ -93,6 +111,8 @@ class Tremolo final : public EngravingItem Chord* chord1() const { return _chord1; } Chord* chord2() const { return _chord2; } + PairF beamPos() const; + double beamWidth() const; TDuration durationType() const; void setDurationType(TDuration d); @@ -103,6 +123,9 @@ class Tremolo final : public EngravingItem _chord2 = c2; } + bool userModified() const; + void setUserModified(bool val); + Fraction tremoloLen() const; bool isBuzzRoll() const { return _tremoloType == TremoloType::BUZZ_ROLL; } bool twoNotes() const { return _tremoloType >= TremoloType::C8; } // is it a two note tremolo? @@ -116,8 +139,9 @@ class Tremolo final : public EngravingItem void spatiumChanged(double oldValue, double newValue) override; void localSpatiumChanged(double oldValue, double newValue) override; void styleChanged() override; - + PointF pagePos() const override; ///< position in page coordinates String accessibleInfo() const override; + void triggerLayout() const override; TremoloStyle style() const { return _style; } void setStyle(TremoloStyle v) { _style = v; } @@ -130,6 +154,20 @@ class Tremolo final : public EngravingItem PropertyValue getProperty(Pid propertyId) const override; bool setProperty(Pid propertyId, const PropertyValue&) override; PropertyValue propertyDefault(Pid propertyId) const override; + void setUp(bool up) { _up = up; } + + // only need grips for two-note trems + bool needStartEditingAfterSelecting() const override { return twoNotes(); } + int gripsCount() const override { return 3; } + Grip initialEditModeGrip() const override { return Grip::END; } + Grip defaultGrip() const override { return Grip::MIDDLE; } + std::vector gripsPositions(const EditData&) const override; + bool isMovable() const override { return true; } + void startDrag(EditData&) override {} + bool isEditable() const override { return true; } + void startEdit(EditData&) override {} + void endEdit(EditData&) override; + void editDrag(EditData&) override; }; } // namespace mu::engraving #endif diff --git a/src/engraving/tests/beam_data/Beam-A.mscx b/src/engraving/tests/beam_data/Beam-A.mscx index 4e31fc6d55bd2..4e6d216da52b2 100644 --- a/src/engraving/tests/beam_data/Beam-A.mscx +++ b/src/engraving/tests/beam_data/Beam-A.mscx @@ -157,8 +157,8 @@ up - -6 - -8 + -4 + -6 begin @@ -731,7 +731,7 @@ - 38 + 36 42 @@ -951,7 +951,7 @@ 42 - 38 + 36 eighth @@ -969,7 +969,7 @@ 42 - 38 + 36 begin @@ -996,7 +996,7 @@ down 42 - 38 + 36 eighth @@ -1015,7 +1015,7 @@ down 42 - 38 + 36 begin @@ -1035,7 +1035,7 @@ down 42 - 38 + 36 eighth diff --git a/src/engraving/tests/beam_data/Beam-B.mscx b/src/engraving/tests/beam_data/Beam-B.mscx index 1dac2bbed6864..d9325309a72ed 100644 --- a/src/engraving/tests/beam_data/Beam-B.mscx +++ b/src/engraving/tests/beam_data/Beam-B.mscx @@ -518,8 +518,8 @@ up - -8 - -6 + -6 + -4 eighth @@ -537,8 +537,8 @@ up - -8 - -6 + -6 + -4 begin @@ -666,8 +666,8 @@ down - 40 - 38 + 38 + 36 eighth @@ -684,8 +684,8 @@ - 40 - 38 + 38 + 36 begin diff --git a/src/engraving/tests/beam_data/Beam-C.mscx b/src/engraving/tests/beam_data/Beam-C.mscx index 1abc77ccdc091..6ee3a56c4059b 100644 --- a/src/engraving/tests/beam_data/Beam-C.mscx +++ b/src/engraving/tests/beam_data/Beam-C.mscx @@ -274,7 +274,7 @@ - -6 + -4 -10 @@ -417,8 +417,8 @@ - 38 - 40 + 36 + 38 eighth @@ -455,7 +455,7 @@ -10 - -6 + -4 eighth @@ -473,7 +473,7 @@ -10 - -6 + -4 begin @@ -499,7 +499,7 @@ -10 - -6 + -4 eighth @@ -517,7 +517,7 @@ -10 - -6 + -4 begin @@ -536,7 +536,7 @@ -10 - -6 + -4 eighth @@ -703,8 +703,8 @@ - 38 - 40 + 36 + 38 eighth @@ -741,7 +741,7 @@ -10 - -6 + -4 eighth @@ -759,7 +759,7 @@ -10 - -6 + -4 begin @@ -782,7 +782,7 @@ -10 - -6 + -4 eighth diff --git a/src/engraving/tests/beam_data/Beam-D.mscx b/src/engraving/tests/beam_data/Beam-D.mscx index 62647c702c8e6..738c6981465fd 100644 --- a/src/engraving/tests/beam_data/Beam-D.mscx +++ b/src/engraving/tests/beam_data/Beam-D.mscx @@ -261,7 +261,7 @@ up - -6 + -4 -10 @@ -751,8 +751,8 @@ - 38 - 40 + 36 + 38 begin @@ -771,7 +771,7 @@ down - 38 + 36 42 diff --git a/src/engraving/tests/beam_data/Beam-E.mscx b/src/engraving/tests/beam_data/Beam-E.mscx index 7798d75f98836..9accaf86adac5 100644 --- a/src/engraving/tests/beam_data/Beam-E.mscx +++ b/src/engraving/tests/beam_data/Beam-E.mscx @@ -241,7 +241,7 @@ up - -6 + -4 -10 @@ -791,8 +791,8 @@ down - 38 - 40 + 36 + 38 eighth diff --git a/src/engraving/tests/beam_data/Beam-F.mscx b/src/engraving/tests/beam_data/Beam-F.mscx index c9e44062eae0a..aee8db2290143 100644 --- a/src/engraving/tests/beam_data/Beam-F.mscx +++ b/src/engraving/tests/beam_data/Beam-F.mscx @@ -214,7 +214,7 @@ - -6 + -4 -10 @@ -811,8 +811,8 @@ down - 38 - 40 + 36 + 38 begin diff --git a/src/engraving/tests/beam_data/Beam-G.mscx b/src/engraving/tests/beam_data/Beam-G.mscx index 0f0b5663ab942..8a93fe35f0549 100644 --- a/src/engraving/tests/beam_data/Beam-G.mscx +++ b/src/engraving/tests/beam_data/Beam-G.mscx @@ -176,8 +176,8 @@ - -6 - -8 + -4 + -6 begin @@ -195,7 +195,7 @@ - -6 + -4 -10 @@ -807,8 +807,8 @@ down - 38 - 40 + 36 + 38 eighth diff --git a/src/engraving/tests/beam_data/beamPositions.mscx b/src/engraving/tests/beam_data/beamPositions.mscx index 9c94820e4137f..2854d6c72cd6f 100644 --- a/src/engraving/tests/beam_data/beamPositions.mscx +++ b/src/engraving/tests/beam_data/beamPositions.mscx @@ -1365,8 +1365,8 @@ 32nd - 6 - 6 + 4 + 4 64th @@ -1449,8 +1449,8 @@ 32nd - -2 - -2 + -4 + -4 64th @@ -1491,8 +1491,8 @@ 32nd - -10 - -10 + -12 + -12 64th @@ -1533,8 +1533,8 @@ 32nd - 50 - 50 + 52 + 52 64th @@ -1578,8 +1578,8 @@ 32nd - 42 - 42 + 44 + 44 64th @@ -1620,8 +1620,8 @@ 32nd - 34 - 34 + 36 + 36 64th @@ -1708,8 +1708,8 @@ 32nd - 26 - 26 + 28 + 28 64th diff --git a/src/engraving/tests/compat114_data/notes-ref.mscx b/src/engraving/tests/compat114_data/notes-ref.mscx index 0115ec7cd33af..ceacc6f0a6960 100644 --- a/src/engraving/tests/compat114_data/notes-ref.mscx +++ b/src/engraving/tests/compat114_data/notes-ref.mscx @@ -187,7 +187,7 @@ - -6 + -4 -10 diff --git a/src/engraving/tests/compat114_data/tremolo2notes-ref.mscx b/src/engraving/tests/compat114_data/tremolo2notes-ref.mscx index 1ebfb9e6146b6..90ebe7b246f07 100644 --- a/src/engraving/tests/compat114_data/tremolo2notes-ref.mscx +++ b/src/engraving/tests/compat114_data/tremolo2notes-ref.mscx @@ -85,6 +85,8 @@ c8 + 34 + 30 @@ -110,6 +112,8 @@ c16 + 38 + 36 @@ -135,6 +139,8 @@ c32 + 44 + 42 diff --git a/src/engraving/tests/compat114_data/tuplets-ref.mscx b/src/engraving/tests/compat114_data/tuplets-ref.mscx index 15352fa15a2a9..3eace4282eccf 100644 --- a/src/engraving/tests/compat114_data/tuplets-ref.mscx +++ b/src/engraving/tests/compat114_data/tuplets-ref.mscx @@ -162,8 +162,8 @@ - 40 - 38 + 38 + 36 eighth diff --git a/src/engraving/tests/compat206_data/tuplets-ref.mscx b/src/engraving/tests/compat206_data/tuplets-ref.mscx index 591f8d4ba50b1..84c4fb0c85f5d 100644 --- a/src/engraving/tests/compat206_data/tuplets-ref.mscx +++ b/src/engraving/tests/compat206_data/tuplets-ref.mscx @@ -164,8 +164,8 @@ 1 - 40 - 38 + 38 + 36 eighth diff --git a/src/engraving/tests/implode_explode_data/implode1-ref.mscx b/src/engraving/tests/implode_explode_data/implode1-ref.mscx index bba6984126d0b..d917f0d96649a 100644 --- a/src/engraving/tests/implode_explode_data/implode1-ref.mscx +++ b/src/engraving/tests/implode_explode_data/implode1-ref.mscx @@ -1113,8 +1113,8 @@ - -8 - -6 + -6 + -4 eighth diff --git a/src/engraving/tests/implode_explode_data/implode1.mscx b/src/engraving/tests/implode_explode_data/implode1.mscx index f558af95613d7..e97e8463a79a4 100644 --- a/src/engraving/tests/implode_explode_data/implode1.mscx +++ b/src/engraving/tests/implode_explode_data/implode1.mscx @@ -698,8 +698,8 @@ - -8 - -6 + -6 + -4 eighth diff --git a/src/engraving/tests/implode_explode_data/undoExplode01-ref.mscx b/src/engraving/tests/implode_explode_data/undoExplode01-ref.mscx index 9232031e02900..e887ccbfb39aa 100644 --- a/src/engraving/tests/implode_explode_data/undoExplode01-ref.mscx +++ b/src/engraving/tests/implode_explode_data/undoExplode01-ref.mscx @@ -732,8 +732,8 @@ - 38 - 40 + 36 + 38 eighth @@ -912,8 +912,8 @@ - -8 - -6 + -6 + -4 eighth diff --git a/src/engraving/tests/parts_data/part-stemless-parts.mscx b/src/engraving/tests/parts_data/part-stemless-parts.mscx index eed3570889a85..1125de7de9b72 100644 --- a/src/engraving/tests/parts_data/part-stemless-parts.mscx +++ b/src/engraving/tests/parts_data/part-stemless-parts.mscx @@ -233,8 +233,8 @@ - -8 - -6 + -6 + -4 @@ -657,8 +657,8 @@ - -8 - -6 + -6 + -4 diff --git a/src/engraving/tests/parts_data/part-stemless.mscx b/src/engraving/tests/parts_data/part-stemless.mscx index 9487770331040..acde12fecaede 100644 --- a/src/engraving/tests/parts_data/part-stemless.mscx +++ b/src/engraving/tests/parts_data/part-stemless.mscx @@ -218,8 +218,8 @@ - -8 - -6 + -6 + -4 eighth diff --git a/src/engraving/tests/rhythmicGrouping_data/groupArticulationsTies-ref.mscx b/src/engraving/tests/rhythmicGrouping_data/groupArticulationsTies-ref.mscx index ac0557fc6fc98..cfa574c2e59a9 100644 --- a/src/engraving/tests/rhythmicGrouping_data/groupArticulationsTies-ref.mscx +++ b/src/engraving/tests/rhythmicGrouping_data/groupArticulationsTies-ref.mscx @@ -90,8 +90,8 @@ - 38 - 40 + 36 + 38 eighth diff --git a/src/engraving/tests/selectionfilter_data/selectionfilter16-base-ref.xml b/src/engraving/tests/selectionfilter_data/selectionfilter16-base-ref.xml index 93f2421c507b2..d4803e9cb0478 100644 --- a/src/engraving/tests/selectionfilter_data/selectionfilter16-base-ref.xml +++ b/src/engraving/tests/selectionfilter_data/selectionfilter16-base-ref.xml @@ -33,6 +33,8 @@ c8 + 26 + 24 diff --git a/src/engraving/tests/timesig_data/timesig-06-ref.mscx b/src/engraving/tests/timesig_data/timesig-06-ref.mscx index 6911f68704da9..f46c80828a175 100644 --- a/src/engraving/tests/timesig_data/timesig-06-ref.mscx +++ b/src/engraving/tests/timesig_data/timesig-06-ref.mscx @@ -98,6 +98,8 @@ c32 + -12 + -18 diff --git a/src/engraving/tests/timesig_data/timesig-09-ref.mscx b/src/engraving/tests/timesig_data/timesig-09-ref.mscx index 8e33550195ffa..0945660bbffc0 100644 --- a/src/engraving/tests/timesig_data/timesig-09-ref.mscx +++ b/src/engraving/tests/timesig_data/timesig-09-ref.mscx @@ -102,6 +102,8 @@ c32 + 52 + 50 @@ -401,6 +403,8 @@ c32 + 52 + 42 @@ -424,6 +428,8 @@ c32 + 52 + 42 diff --git a/src/engraving/tests/timesig_data/timesig-09.mscx b/src/engraving/tests/timesig_data/timesig-09.mscx index 84f24ac66a966..422ba2dfb3dc5 100644 --- a/src/engraving/tests/timesig_data/timesig-09.mscx +++ b/src/engraving/tests/timesig_data/timesig-09.mscx @@ -102,6 +102,8 @@ c32 + 52 + 42 @@ -125,6 +127,8 @@ c32 + 52 + 42 @@ -148,6 +152,8 @@ c32 + 52 + 42 @@ -174,6 +180,8 @@ c32 + 52 + 42 @@ -197,6 +205,8 @@ c32 + 52 + 42 @@ -220,6 +230,8 @@ c32 + 52 + 42 @@ -243,6 +255,8 @@ c32 + 52 + 42 @@ -269,6 +283,8 @@ c32 + 52 + 42 @@ -292,6 +308,8 @@ c32 + 52 + 50 @@ -315,6 +333,8 @@ c32 + 52 + 50 diff --git a/src/engraving/tests/unrollrepeats_data/clef-key-ts-ref.mscx b/src/engraving/tests/unrollrepeats_data/clef-key-ts-ref.mscx index d1e986c1c350c..486aa9b1de253 100644 --- a/src/engraving/tests/unrollrepeats_data/clef-key-ts-ref.mscx +++ b/src/engraving/tests/unrollrepeats_data/clef-key-ts-ref.mscx @@ -270,7 +270,7 @@ 44 - 38 + 36 16th @@ -473,7 +473,7 @@ 44 - 38 + 36 16th @@ -815,7 +815,7 @@ 44 - 38 + 36 16th @@ -1119,8 +1119,8 @@ - -8 - -6 + -6 + -4 @@ -1663,8 +1663,8 @@ - -8 - -6 + -6 + -4 diff --git a/src/importexport/capella/tests/data/test4.cap-ref.mscx b/src/importexport/capella/tests/data/test4.cap-ref.mscx index abab69a98ec37..f84c1add8d8eb 100644 --- a/src/importexport/capella/tests/data/test4.cap-ref.mscx +++ b/src/importexport/capella/tests/data/test4.cap-ref.mscx @@ -140,8 +140,8 @@ - -6 - -8 + -4 + -6 eighth diff --git a/src/importexport/capella/tests/data/test4.capx-ref.mscx b/src/importexport/capella/tests/data/test4.capx-ref.mscx index 969dfe08ce837..40d758b21f895 100644 --- a/src/importexport/capella/tests/data/test4.capx-ref.mscx +++ b/src/importexport/capella/tests/data/test4.capx-ref.mscx @@ -140,8 +140,8 @@ - -6 - -8 + -4 + -6 eighth diff --git a/src/importexport/capella/tests/data/test7.cap-ref.mscx b/src/importexport/capella/tests/data/test7.cap-ref.mscx index e6d2785eecc27..24f1b98f5b263 100644 --- a/src/importexport/capella/tests/data/test7.cap-ref.mscx +++ b/src/importexport/capella/tests/data/test7.cap-ref.mscx @@ -92,7 +92,7 @@ - -6 + -4 -10 diff --git a/src/importexport/guitarpro/tests/data/dotted-tuplets.gp5-ref.mscx b/src/importexport/guitarpro/tests/data/dotted-tuplets.gp5-ref.mscx index 065a93a4c693c..92792b66d008d 100644 --- a/src/importexport/guitarpro/tests/data/dotted-tuplets.gp5-ref.mscx +++ b/src/importexport/guitarpro/tests/data/dotted-tuplets.gp5-ref.mscx @@ -104,8 +104,8 @@ - 40 - 38 + 38 + 36 1 diff --git a/src/importexport/guitarpro/tests/data/instr-change.gp-ref.mscx b/src/importexport/guitarpro/tests/data/instr-change.gp-ref.mscx index 392e87384cf51..fd8e79ca31a94 100644 --- a/src/importexport/guitarpro/tests/data/instr-change.gp-ref.mscx +++ b/src/importexport/guitarpro/tests/data/instr-change.gp-ref.mscx @@ -113,8 +113,8 @@ The Wall metNoteQuarterUp = 115 - 40 - 38 + 38 + 36 16th @@ -308,8 +308,8 @@ The Wall Acoustic Grand Piano - 40 - 38 + 38 + 36 16th diff --git a/src/importexport/guitarpro/tests/data/instr-change.gpx-ref.mscx b/src/importexport/guitarpro/tests/data/instr-change.gpx-ref.mscx index de0698fd52d6b..5f4e024159574 100644 --- a/src/importexport/guitarpro/tests/data/instr-change.gpx-ref.mscx +++ b/src/importexport/guitarpro/tests/data/instr-change.gpx-ref.mscx @@ -113,8 +113,8 @@ The Wall metNoteQuarterUp = 115 - 40 - 38 + 38 + 36 16th @@ -308,8 +308,8 @@ The Wall German-APiano - 40 - 38 + 38 + 36 16th diff --git a/src/importexport/guitarpro/tests/data/tuplets2.gpx-ref.mscx b/src/importexport/guitarpro/tests/data/tuplets2.gpx-ref.mscx index 911a2bad0c27c..5b76f92fef3f2 100644 --- a/src/importexport/guitarpro/tests/data/tuplets2.gpx-ref.mscx +++ b/src/importexport/guitarpro/tests/data/tuplets2.gpx-ref.mscx @@ -252,8 +252,8 @@ solo concert - -8 - -6 + -6 + -4 eighth