LCOV - code coverage report
Current view: top level - src/engraving/rendering/score - tremololayout.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 73.9 % 230 170
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-Studio-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2023 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : #include "tremololayout.h"
      23              : 
      24              : #include "global/types/number.h"
      25              : 
      26              : #include "dom/chord.h"
      27              : #include "dom/stem.h"
      28              : #include "dom/tremolotwochord.h"
      29              : #include "dom/tremolosinglechord.h"
      30              : #include "dom/note.h"
      31              : #include "dom/staff.h"
      32              : #include "dom/measure.h"
      33              : 
      34              : #include "beamtremololayout.h"
      35              : #include "chordlayout.h"
      36              : #include "stemlayout.h"
      37              : 
      38              : using namespace mu::engraving;
      39              : using namespace mu::engraving::rendering::score;
      40              : 
      41          395 : void TremoloLayout::layout(TremoloTwoChord* item, const LayoutContext& ctx)
      42              : {
      43          395 :     IF_ASSERT_FAILED(item->ownershipParent()) {
      44            0 :         return;
      45              :     }
      46              : 
      47          395 :     item->setChord1(toChord(item->ownershipParent()));
      48              : 
      49          395 :     Note* anchor1 = item->chord1()->up() ? item->chord1()->upNote() : item->chord1()->downNote();
      50          395 :     Stem* stem    = item->chord1()->stem();
      51              :     double x, y, h;
      52          395 :     if (stem) {
      53          338 :         x = stem->pos().x() + stem->width() / 2 * (item->chord1()->up() ? -1.0 : 1.0);
      54          338 :         y = stem->pos().y();
      55          338 :         h = stem->length();
      56              :     } else {
      57              :         // center tremolo above note
      58           57 :         x = anchor1->x() + anchor1->headWidth() * 0.5;
      59           57 :         y = anchor1->y();
      60           57 :         h = (ctx.conf().styleAbsolute(Sid::tremoloNoteSidePadding) + item->ldata()->bbox().height()) * item->chord1()->intrinsicMag();
      61              :     }
      62              : 
      63          395 :     layoutTwoNotesTremolo(item, ctx, x, y, h, item->spatium());
      64              : }
      65              : 
      66          412 : void TremoloLayout::layout(TremoloSingleChord* item, const LayoutContext& ctx)
      67              : {
      68          412 :     IF_ASSERT_FAILED(item->ownershipParent()) {
      69            0 :         return;
      70              :     }
      71              : 
      72          412 :     item->computeShape();      // set bbox
      73          412 :     item->setPath(item->basePath());
      74              : 
      75          412 :     Note* anchor1 = item->chord()->up() ? item->chord()->upNote() : item->chord()->downNote();
      76          412 :     Stem* stem = item->chord()->stem();
      77              :     double x, y, h;
      78          412 :     if (stem) {
      79          397 :         x = stem->pos().x() + stem->width() / 2 * (item->chord()->up() ? -1.0 : 1.0);
      80          397 :         y = stem->pos().y();
      81          397 :         h = stem->length();
      82              :     } else {
      83              :         // center tremolo above note
      84           15 :         x = anchor1->x() + anchor1->headWidth() * 0.5;
      85              : 
      86           15 :         bool hasMirroredNote = false;
      87           30 :         for (Note* n : item->chord()->notes()) {
      88           15 :             if (n->ldata()->mirror.value()) {
      89            0 :                 hasMirroredNote = true;
      90            0 :                 break;
      91              :             }
      92              :         }
      93              : 
      94           15 :         if (hasMirroredNote) {
      95            0 :             x = StemLayout::StemLayout::stemPosX(item->chord());
      96              :         }
      97              : 
      98           15 :         y = anchor1->y();
      99           15 :         h = (ctx.conf().styleAbsolute(Sid::tremoloNoteSidePadding) + item->ldata()->bbox().height()) * item->chord()->intrinsicMag();
     100              :     }
     101              : 
     102          412 :     layoutOneNoteTremolo(item, ctx, x, y, h, item->spatium());
     103              : }
     104              : 
     105              : //---------------------------------------------------------
     106              : //   layoutOneNoteTremolo
     107              : //---------------------------------------------------------
     108              : 
     109          412 : void TremoloLayout::layoutOneNoteTremolo(TremoloSingleChord* item, const LayoutContext& ctx, double x, double y, double h, double spatium)
     110              : {
     111          412 :     const StaffType* staffType = item->staffType();
     112              : 
     113          412 :     if (staffType && staffType->isTabStaff()) {
     114            3 :         x = ChordLayout::centerX(item->chord());
     115              :     }
     116              : 
     117          412 :     bool up = item->chord()->up();
     118          412 :     int upValue = up ? -1 : 1;
     119          412 :     double mag = item->chord()->intrinsicMag();
     120          412 :     spatium *= mag;
     121              : 
     122          412 :     double yOffset = h - ctx.conf().styleAbsolute(Sid::tremoloOutSidePadding) * mag;
     123              : 
     124          412 :     int beams = item->chord()->beams();
     125          412 :     if (item->chord()->hook()) {
     126              :         // allow for space at the hook side of the stem (yOffset)
     127              :         // straight flags and traditional flags have different requirements because of their slopes
     128              :         // away from the stem. Straight flags have a shallower slope and a lot more space in general
     129              :         // so we can place the trem higher in that case
     130            2 :         bool straightFlags = ctx.conf().styleB(Sid::useStraightNoteFlags);
     131            2 :         if (straightFlags) {
     132            0 :             yOffset -= 0.75 * spatium;
     133              :         } else {
     134              :             // up-hooks and down-hooks are shaped differently
     135            2 :             yOffset -= up ? 1.5 * spatium : 1.0 * spatium;
     136              :         }
     137              :         // we need an additional offset for beams > 2 since those beams extend outwards and we don't want to adjust for that
     138            2 :         double beamOffset = straightFlags ? 0.75 : 0.5;
     139            2 :         yOffset -= beams >= 2 ? beamOffset * spatium : 0.0;
     140          410 :     } else if (beams && staffType && !staffType->isSimpleTabStaff()) {
     141          213 :         yOffset -= (beams * (ctx.conf().styleB(Sid::useWideBeams) ? 1.0 : 0.75) - 0.25) * spatium;
     142              :     }
     143          412 :     yOffset -= item->isBuzzRoll() && up ? 0.5 * spatium : 0.0;
     144          412 :     yOffset -= up ? 0.0 : item->minHeight() * spatium / mag;
     145          412 :     yOffset *= upValue;
     146          412 :     y += yOffset;
     147              : 
     148          412 :     if (up) {
     149          178 :         double height = item->isBuzzRoll() ? 0 : item->minHeight();
     150          178 :         double staveHeight = (((item->staff()->lines(item->tick()) - 1) - height) * spatium / mag);
     151          178 :         y = std::min(y, staveHeight);
     152              :     } else {
     153          234 :         y = std::max(y, 0.0);
     154              :     }
     155          412 :     item->setPos(x, y);
     156          412 : }
     157              : 
     158          275 : void TremoloLayout::calcIsUp(TremoloTwoChord* item)
     159              : {
     160          275 :     if (item->userModified()) {
     161            0 :         bool startChordUp = ChordLayout::isChordPosBelowTrem(item->chord1(), item);
     162            0 :         bool endChordUp = ChordLayout::isChordPosBelowTrem(item->chord2(), item);
     163              : 
     164            0 :         item->setUp(startChordUp || endChordUp);
     165            0 :         item->chord1()->setUp(startChordUp);
     166            0 :         item->chord2()->setUp(endChordUp);
     167            0 :         return;
     168              :     }
     169              : 
     170          275 :     int up = 0;
     171          275 :     bool isUp = item->up();
     172          550 :     bool hasVoices = item->chord1()->measure()->hasVoices(item->chord1()->staffIdx(), item->chord1()->tick(),
     173          275 :                                                           item->chord2()->tick() - item->chord1()->tick());
     174          275 :     if (item->chord1()->stemDirection() == DirectionV::AUTO
     175          209 :         && item->chord2()->stemDirection() == DirectionV::AUTO
     176          209 :         && item->chord1()->staffMove() == item->chord2()->staffMove()
     177          484 :         && !hasVoices) {
     178          209 :         std::vector<int> chord1Distances = item->chord1()->noteDistances();
     179          209 :         std::vector<int> chord2Distances = item->chord2()->noteDistances();
     180          209 :         std::vector<int> noteDistances;
     181          209 :         noteDistances.reserve(chord1Distances.size() + chord2Distances.size());
     182          424 :         for (int distance : chord1Distances) {
     183          215 :             noteDistances.push_back(distance);
     184              :         }
     185          424 :         for (int distance : chord2Distances) {
     186          215 :             noteDistances.push_back(distance);
     187              :         }
     188          209 :         std::sort(noteDistances.begin(), noteDistances.end());
     189          209 :         up = ChordLayout::computeAutoStemDirection(noteDistances);
     190              : 
     191          209 :         isUp = up > 0;
     192          275 :     } else if (item->chord1()->stemDirection() != DirectionV::AUTO) {
     193           66 :         isUp = item->chord1()->stemDirection() == DirectionV::UP;
     194            0 :     } else if (item->chord2()->stemDirection() != DirectionV::AUTO) {
     195            0 :         isUp = item->chord2()->stemDirection() == DirectionV::UP;
     196            0 :     } else if (item->chord1()->staffMove() > 0 || item->chord2()->staffMove() > 0) {
     197            0 :         isUp = false;
     198            0 :     } else if (item->chord1()->staffMove() < 0 || item->chord2()->staffMove() < 0) {
     199            0 :         isUp = true;
     200            0 :     } else if (hasVoices) {
     201            0 :         isUp = item->chord1()->track() % 2 == 0;
     202              :     }
     203              : 
     204          275 :     item->setUp(isUp);
     205          275 :     item->chord1()->setUp(item->chord1()->staffMove() == 0 ? isUp : !isUp); // if on a different staff, flip stem dir
     206          275 :     item->chord2()->setUp(item->chord2()->staffMove() == 0 ? isUp : !isUp);
     207              : }
     208              : 
     209              : //---------------------------------------------------------
     210              : //   layoutTwoNotesTremolo
     211              : //---------------------------------------------------------
     212              : 
     213          395 : void TremoloLayout::layoutTwoNotesTremolo(TremoloTwoChord* item, const LayoutContext& ctx, double x, double y, double h, double spatium)
     214              : {
     215              :     UNUSED(x);
     216              :     UNUSED(y);
     217              :     UNUSED(h);
     218              :     UNUSED(spatium);
     219              : 
     220          395 :     TremoloTwoChord::LayoutData* ldata = item->mutldata();
     221              : 
     222              :     // make sure both stems are in the same direction
     223          395 :     if (item->chord1()->beam() && item->chord1()->beam() == item->chord2()->beam()) {
     224          120 :         Beam* beam = item->chord1()->beam();
     225          120 :         item->setUp(beam->up());
     226          120 :         item->doSetDirection(beam->direction());
     227              :         // stem stuff is already taken care of by the beams
     228              :     } else {
     229          275 :         TremoloLayout::calcIsUp(item);
     230              : 
     231          275 :         if (!item->userModified()) {
     232              :             // user modified trems will be dealt with later
     233          275 :             ChordLayout::layoutStem(item->chord1(), ctx);
     234          275 :             ChordLayout::layoutStem(item->chord2(), ctx);
     235              :         }
     236              :     }
     237              : 
     238          395 :     BeamTremoloLayout::setupLData(item, item->mutldata(), ctx);
     239          395 :     item->setStartAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), item->chord1(), ChordBeamAnchorType::Start));
     240          395 :     item->setEndAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), item->chord2(), ChordBeamAnchorType::End));
     241              : 
     242              :     // deal with manual adjustments here and return
     243          395 :     if (item->userModified()) {
     244            0 :         int idx = item->directionIdx();
     245            0 :         double startY = item->beamFragment().py1[idx];
     246            0 :         double endY = item->beamFragment().py2[idx];
     247            0 :         if (ctx.conf().styleB(Sid::snapCustomBeamsToGrid)) {
     248            0 :             const double quarterSpace = item->EngravingItem::spatium() / 4;
     249            0 :             startY = round(startY / quarterSpace) * quarterSpace;
     250            0 :             endY = round(endY / quarterSpace) * quarterSpace;
     251              :         }
     252            0 :         startY += item->pagePos().y();
     253            0 :         endY += item->pagePos().y();
     254            0 :         item->startAnchor().setY(startY);
     255            0 :         item->endAnchor().setY(endY);
     256            0 :         item->mutldata()->setAnchors(item->startAnchor(), item->endAnchor());
     257              : 
     258            0 :         ChordLayout::layoutStem(item->chord1(), ctx);
     259            0 :         ChordLayout::layoutStem(item->chord2(), ctx);
     260              : 
     261            0 :         createBeamSegments(item, ctx);
     262            0 :         return;
     263              :     }
     264          395 :     ldata->setPosY(0.);
     265          790 :     std::vector<ChordRest*> chordRests{ item->chord1(), item->chord2() };
     266          395 :     std::vector<BeamBase::NotePosition> notes;
     267          395 :     double mag = 0.0;
     268              : 
     269          395 :     notes.clear();
     270         1185 :     for (ChordRest* cr : chordRests) {
     271          790 :         double m = cr->isSmall() ? ctx.conf().styleD(Sid::smallNoteMag) : 1.0;
     272          790 :         mag = std::max(mag, m);
     273          790 :         if (cr->isChord()) {
     274          790 :             Chord* chord = toChord(cr);
     275              :             //int i = chord->staffMove();
     276              :             //_minMove = std::min(_minMove, i); todo: investigate this
     277              :             //_maxMove = std::max(_maxMove, i);
     278              : 
     279         1598 :             for (int distance : chord->noteDistances()) {
     280          808 :                 notes.push_back(BeamBase::NotePosition(distance, chord->vStaffIdx()));
     281          790 :             }
     282              :         }
     283              :     }
     284              : 
     285          395 :     std::sort(notes.begin(), notes.end());
     286          395 :     ldata->setMag(mag);
     287          395 :     BeamTremoloLayout::calculateAnchors(item, item->mutldata(), ctx, chordRests, notes);
     288          395 :     item->setStartAnchor(item->ldata()->startAnchor);
     289          395 :     item->setEndAnchor(item->ldata()->endAnchor);
     290              : 
     291          395 :     int idx = item->directionIdx();
     292          395 :     item->beamFragment().py1[idx] = item->startAnchor().y() - item->pagePos().y();
     293          395 :     item->beamFragment().py2[idx] = item->endAnchor().y() - item->pagePos().y();
     294          395 :     createBeamSegments(item, ctx);
     295          395 : }
     296              : 
     297              : //---------------------------------------------------------
     298              : //   extendedStemLenWithTwoNotesTremolo
     299              : //    Goal: To extend stem of one of the chords to make the tremolo less steep
     300              : //    Returns a modified pair of stem lengths of two chords
     301              : //---------------------------------------------------------
     302              : 
     303            0 : std::pair<double, double> TremoloLayout::extendedStemLenWithTwoNoteTremolo(TremoloTwoChord* tremolo, double stemLen1, double stemLen2)
     304              : {
     305            0 :     const double spatium = tremolo->spatium();
     306            0 :     Chord* c1 = tremolo->chord1();
     307            0 :     Chord* c2 = tremolo->chord2();
     308            0 :     Stem* s1 = c1->stem();
     309            0 :     Stem* s2 = c2->stem();
     310            0 :     const double sgn1 = c1->up() ? -1.0 : 1.0;
     311            0 :     const double sgn2 = c2->up() ? -1.0 : 1.0;
     312            0 :     const double stemTipDistance = (s1 && s2) ? (s2->pagePos().y() + stemLen2) - (s1->pagePos().y() + stemLen1)
     313            0 :                                    : (StemLayout::stemPos(c2).y() + (sgn2 * stemLen2)) - (StemLayout::stemPos(c1).y() + (sgn1 * stemLen1));
     314              : 
     315              :     // same staff & same direction: extend one of the stems
     316            0 :     if (c1->staffMove() == c2->staffMove() && c1->up() == c2->up()) {
     317            0 :         const bool stem1Higher = stemTipDistance > 0.0;
     318            0 :         if (std::abs(stemTipDistance) > 1.0 * spatium) {
     319            0 :             if ((c1->up() && !stem1Higher) || (!c1->up() && stem1Higher)) {
     320            0 :                 return { stemLen1 + (std::abs(stemTipDistance) - spatium), stemLen2 };
     321              :             } else {   /* if ((c1->up() && stem1Higher) || (!c1->up() && !stem1Higher)) */
     322            0 :                 return { stemLen1, stemLen2 + (std::abs(stemTipDistance) - spatium) };
     323              :             }
     324              :         }
     325              :     }
     326              : 
     327            0 :     return { stemLen1, stemLen2 };
     328              : }
     329              : 
     330          395 : void TremoloLayout::createBeamSegments(TremoloTwoChord* item, const LayoutContext& ctx)
     331              : {
     332              :     // TODO: This should be a style setting, to replace tremoloStrokeLengthMultiplier
     333              :     static constexpr double stemGapSp = 1.0;
     334          395 :     const bool defaultStyle = (!item->customStyleApplicable()) || (item->tremoloStyle() == TremoloStyle::DEFAULT);
     335              : 
     336          395 :     item->clearBeamSegments();
     337              : 
     338          395 :     bool _isGrace = item->chord1()->isGrace();
     339          395 :     const PointF pagePos = item->pagePos();
     340          395 :     PointF startAnchor = item->ldata()->startAnchor - PointF(0.0, pagePos.y());
     341          395 :     PointF endAnchor = item->ldata()->endAnchor - PointF(0.0, pagePos.y());
     342              : 
     343              :     // inset trem from stems for default style
     344          395 :     const double slope = muse::divide(endAnchor.y() - startAnchor.y(), endAnchor.x() - startAnchor.x(), 0.0);
     345              : 
     346          395 :     double gapSp = stemGapSp;
     347          395 :     if (defaultStyle || item->tremoloStyle() == TremoloStyle::TRADITIONAL_ALTERNATE) {
     348              :         // we can eat into the stemGapSp margin if the anchorpoints are sufficiently close together
     349          395 :         double widthSp = (endAnchor.x() - startAnchor.x()) / item->spatium() - (stemGapSp * 2);
     350          395 :         if (!muse::RealIsEqualOrMore(widthSp, 0.6)) {
     351              :             // tremolo beam is too short; we can eat into the gap spacing a little
     352          128 :             gapSp = std::max(stemGapSp - ((0.6 - widthSp) * 0.5), 0.4);
     353              :         }
     354              :     } else {
     355            0 :         gapSp = 0.0;
     356              :     }
     357          395 :     BeamSegment* mainStroke = new BeamSegment(item);
     358          395 :     PointF xOffset = PointF(gapSp * item->spatium(), 0);
     359          395 :     PointF yOffset = PointF(0, gapSp * item->spatium() * slope);
     360          395 :     if (item->tremoloStyle() == TremoloStyle::TRADITIONAL_ALTERNATE) {
     361            0 :         mainStroke->line = LineF(startAnchor, endAnchor);
     362            0 :         startAnchor += xOffset;
     363            0 :         endAnchor -= xOffset;
     364            0 :         startAnchor += yOffset;
     365            0 :         endAnchor -= yOffset;
     366              :     } else {
     367          395 :         startAnchor += xOffset;
     368          395 :         endAnchor -= xOffset;
     369          395 :         startAnchor += yOffset;
     370          395 :         endAnchor -= yOffset;
     371          395 :         mainStroke->line = LineF(startAnchor, endAnchor);
     372              :     }
     373          395 :     mainStroke->level = 0;
     374              : 
     375          395 :     item->beamSegments().push_back(mainStroke);
     376              : 
     377          395 :     LineF line = mainStroke->line;
     378          395 :     double halfWidth = 0.5 * ctx.conf().styleAbsolute(Sid::beamWidth);
     379          395 :     double bboxTop = std::min(line.y1(), line.y2()) - halfWidth;
     380          395 :     double bboxBottom = std::max(line.y1(), line.y2()) + halfWidth;
     381              : 
     382          395 :     RectF bbox = RectF(line.x1(), bboxTop, line.x2() - line.x1(), bboxBottom - bboxTop);
     383              : 
     384          395 :     PointF beamOffset = PointF(0., (item->up() ? 1 : -1) * item->spatium() * (ctx.conf().styleB(Sid::useWideBeams) ? 1. : 0.75));
     385          395 :     beamOffset.setY(beamOffset.y() * item->mag() * (_isGrace ? ctx.conf().styleD(Sid::graceNoteMag) : 1.));
     386          954 :     for (int i = 1; i < item->lines(); ++i) {
     387          559 :         BeamSegment* stroke = new BeamSegment(item);
     388          559 :         stroke->level = i;
     389          559 :         stroke->line = LineF(startAnchor + (beamOffset * (double)i), endAnchor + (beamOffset * (double)i));
     390          559 :         item->beamSegments().push_back(stroke);
     391          559 :         bbox.unite(bbox.translated(0., beamOffset.y() * (double)i));
     392              :     }
     393          395 :     item->setbbox(bbox);
     394              : 
     395              :     // size stems properly
     396          733 :     if (item->chord1()->stem() && item->chord2()->stem()
     397          733 :         && !(item->chord1()->beam() && item->chord1()->beam() == item->chord2()->beam())) {
     398              :         // we don't need to do anything if these chords are part of the same beam--their stems are taken care of
     399              :         // by the beam layout
     400          218 :         int beamSpacing = ctx.conf().styleB(Sid::useWideBeams) ? 4 : 3;
     401          654 :         for (ChordRest* cr : { item->chord1(), item->chord2() }) {
     402          436 :             Chord* chord = toChord(cr);
     403          436 :             double addition = 0.0;
     404          436 :             if (cr->up() != item->up() && item->lines() > 1) {
     405              :                 // need to adjust further for beams on the opposite side
     406            0 :                 addition += (item->lines() - 1.) * beamSpacing / 4. * item->spatium() * item->mag();
     407              :             }
     408              :             // calling extendStem with addition 0.0 still sizes the stem to the manually adjusted height of the trem.
     409          436 :             BeamTremoloLayout::extendStem(item->ldata(), chord, addition);
     410              :         }
     411              :     }
     412          395 : }
        

Generated by: LCOV version 2.0-1