LCOV - code coverage report
Current view: top level - src/engraving/rendering/score - arpeggiolayout.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 89.1 % 184 164
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 "arpeggiolayout.h"
      23              : 
      24              : #include "dom/arpeggio.h"
      25              : #include "dom/chord.h"
      26              : #include "dom/ledgerline.h"
      27              : #include "dom/part.h"
      28              : #include "dom/score.h"
      29              : #include "dom/segment.h"
      30              : #include "dom/staff.h"
      31              : 
      32              : #include "tlayout.h"
      33              : 
      34              : using namespace mu::engraving;
      35              : using namespace mu::engraving::rendering::score;
      36              : 
      37              : //   layoutArpeggio2
      38              : //    called after layout of page
      39              : 
      40        48512 : void ArpeggioLayout::layoutArpeggio2(Arpeggio* item, LayoutContext& ctx)
      41              : {
      42        48512 :     if (!item) {
      43        48279 :         return;
      44              :     }
      45              : 
      46          233 :     TLayout::layoutItem(item, ctx);
      47              : }
      48              : 
      49              : //
      50              : // INSET:
      51              : // Arpeggios have inset white space. For instance, the bracket
      52              : // "[" shape has whitespace inside of the "C". Symbols like
      53              : // accidentals can fit inside this whitespace. These inset
      54              : // functions are used to get the size of the inner dimensions
      55              : // for this area on all arpeggios.
      56              : //
      57              : 
      58           20 : double ArpeggioLayout::insetTop(const Arpeggio* item, const Chord* c)
      59              : {
      60           20 :     double top = c->upNote()->y() - c->upNote()->height() / 2;
      61              : 
      62              :     // use wiggle width, not height, since it's rotated 90 degrees
      63           20 :     if (item->arpeggioType() == ArpeggioType::UP) {
      64            4 :         top += item->symBbox(SymId::wiggleArpeggiatoUpArrow).width();
      65           16 :     } else if (item->arpeggioType() == ArpeggioType::UP_STRAIGHT) {
      66            0 :         top += item->symBbox(SymId::arrowheadBlackUp).width();
      67              :     }
      68              : 
      69           20 :     return top;
      70              : }
      71              : 
      72           18 : double ArpeggioLayout::insetBottom(const Arpeggio* item, const Chord* c)
      73              : {
      74           18 :     double bottom = c->downNote()->y() + c->downNote()->height() / 2;
      75              : 
      76              :     // use wiggle width, not height, since it's rotated 90 degrees
      77           18 :     if (item->arpeggioType() == ArpeggioType::DOWN) {
      78            0 :         bottom -= item->symBbox(SymId::wiggleArpeggiatoUpArrow).width();
      79           18 :     } else if (item->arpeggioType() == ArpeggioType::DOWN_STRAIGHT) {
      80            0 :         bottom -= item->symBbox(SymId::arrowheadBlackDown).width();
      81              :     }
      82              : 
      83           18 :     return bottom;
      84              : }
      85              : 
      86           12 : double ArpeggioLayout::insetWidth(const Arpeggio* item)
      87              : {
      88           12 :     double lineWidth = item->style().styleAbsolute(item->isChordBracket() ? Sid::chordBracketLineWidth : Sid::arpeggioLineWidth);
      89           12 :     switch (item->arpeggioType()) {
      90           12 :     case ArpeggioType::NORMAL:
      91              :     {
      92           12 :         return 0.0;
      93              :     }
      94              : 
      95            0 :     case ArpeggioType::UP:
      96              :     case ArpeggioType::DOWN:
      97              :     {
      98              :         // use wiggle height, not width, since it's rotated 90 degrees
      99            0 :         return (item->width() - item->symBbox(SymId::wiggleArpeggiatoUp).height()) / 2;
     100              :     }
     101              : 
     102            0 :     case ArpeggioType::UP_STRAIGHT:
     103              :     case ArpeggioType::DOWN_STRAIGHT:
     104              :     {
     105            0 :         return (item->width() - lineWidth) / 2;
     106              :     }
     107              : 
     108            0 :     case ArpeggioType::BRACKET:
     109              :     {
     110            0 :         return item->width() - lineWidth / 2;
     111              :     }
     112              :     }
     113            0 :     return 0.0;
     114              : }
     115              : 
     116          252 : void ArpeggioLayout::clearAccidentals(Arpeggio* item, LayoutContext& ctx)
     117              : {
     118              :     //   This function finds collisions with accidentals outside of an arpeggio's span
     119              :     //   Assumptions are made to deal with cross staff height, as we don't know this yet.
     120              :     //   Once a chord is found, its shape is stripped down to only accidentals which collide
     121              :     //   with the arpeggio.  The arpeggio's offset is calculated from the remaining shape
     122              : 
     123          252 :     if (!item || !item->chord()) {
     124            0 :         return;
     125              :     }
     126              : 
     127          252 :     const Segment* seg = item->chord()->segment();
     128          252 :     const EngravingItem* endEl = seg->element(item->endTrack());
     129          252 :     const Chord* endChord = endEl && endEl->isChord() ? toChord(endEl) : item->chord();
     130              : 
     131          252 :     const Part* part = item->part();
     132          252 :     const TrackRange trackRange = part->trackRange();
     133          252 :     const PaddingTable& paddingTable = item->score()->paddingTables().horizontalPaddingTable();
     134              : 
     135          252 :     double arpeggioAccidentalDistance = paddingTable.at(ElementType::ARPEGGIO).at(ElementType::ACCIDENTAL) * item->mag();
     136          252 :     double arpeggioLedgerDistance = paddingTable.at(ElementType::ARPEGGIO).at(ElementType::LEDGER_LINE) * item->mag();
     137              : 
     138          252 :     Shape arpShape = item->shape().translate(item->pagePos());
     139          252 :     double arpX = arpShape.right();
     140          252 :     double largestOverlap = 0.0;
     141              : 
     142         1960 :     for (track_idx_t track = trackRange.startTrack; track < trackRange.endTrack; ++track) {
     143         1708 :         EngravingItem* e = seg->element(track);
     144         1708 :         if (!e || !e->isChord()) {
     145         1704 :             continue;
     146              :         }
     147              : 
     148          567 :         Chord* chord = toChord(e);
     149              :         // Only check chords the arpeggio doesn't span on staves the arpeggios span
     150              :         // We already calculated the gap between chords it does span in ChordLayout::layoutPitched
     151              :         bool aboveStart
     152          567 :             = std::make_pair(chord->vStaffIdx(), chord->downLine()) < std::make_pair(item->vStaffIdx(), item->chord()->upLine());
     153          567 :         bool belowEnd = std::make_pair(chord->vStaffIdx(), chord->upLine()) > std::make_pair(endChord->vStaffIdx(), endChord->downLine());
     154              : 
     155         1134 :         if ((chord->spanArpeggio() == item && !(aboveStart || belowEnd)) || chord->vStaffIdx() < item->vStaffIdx()
     156         1134 :             || chord->vStaffIdx() > endChord->vStaffIdx()) {
     157          507 :             continue;
     158              :         }
     159              : 
     160              :         // We don't know the distance between staves, so our arpeggio's height is currently unknown if it spans staves
     161              :         // To make sure we spot collisions with non-arpeggio accidentals, extend arpeggio upwards/downwards a large amount
     162              :         // depending on if they are the end/start of the arpeggio respectively.
     163              :         // This is to find collisions between chords outside the arpeggios track span, but within its "visual span"
     164           60 :         Shape curArpShape = arpShape;
     165           60 :         staff_idx_t staffIdx = chord->vStaffIdx();
     166           60 :         if (staffIdx == item->chord()->vStaffIdx() && item->crossStaff()) {
     167              :             // Arpeggio doesn't end on this staff, so extend downwards
     168            0 :             curArpShape.addBBox(RectF(curArpShape.bbox().bottomLeft().x(), curArpShape.bbox().bottomLeft().y(),
     169            0 :                                       curArpShape.bbox().width(), ARBITRARY_ARPEGGIO_LENGTH));
     170           60 :         } else if (staffIdx == endChord->vStaffIdx() && item->crossStaff()) {
     171              :             // Arpeggio doesn't start on this staff so move to last note and extend upwards
     172            0 :             curArpShape.translate(PointF(0.0, endChord->downNote()->pagePos().y() - item->pagePos().y()));
     173            0 :             curArpShape.addBBox(RectF(curArpShape.bbox().topLeft().x(), curArpShape.bbox().topLeft().y() - ARBITRARY_ARPEGGIO_LENGTH,
     174            0 :                                       curArpShape.bbox().width(), ARBITRARY_ARPEGGIO_LENGTH));
     175              :         }
     176              : 
     177           60 :         Shape chordShape = chord->shape();
     178           60 :         chordShape.translate(chord->pagePos());
     179              :         // Remove any element which isn't an accidental the arpeggio intersects
     180           60 :         chordShape.remove_if([curArpShape, arpeggioAccidentalDistance, arpeggioLedgerDistance](ShapeElement& shapeElement) {
     181          246 :             if (!shapeElement.item()
     182          246 :                 || !(shapeElement.item()->isAccidental() || shapeElement.item()->isLedgerLine())) {
     183          178 :                 return true;
     184              :             }
     185           68 :             if (shapeElement.item()->isAccidental()) {
     186              :                 // Pad accidentals with Sid::arpeggioAccidentalDistance either side
     187            6 :                 shapeElement.setTopLeft(PointF(shapeElement.topLeft().x() - arpeggioAccidentalDistance, shapeElement.topLeft().y()));
     188            6 :                 shapeElement.setWidth(shapeElement.width() + 2 * arpeggioAccidentalDistance);
     189           62 :             } else if (shapeElement.item()->isLedgerLine()) {
     190           62 :                 shapeElement.setTopLeft(PointF(shapeElement.topLeft().x() - arpeggioLedgerDistance, shapeElement.topLeft().y()));
     191           62 :                 shapeElement.setWidth(shapeElement.width() + 2 * arpeggioLedgerDistance);
     192              :             }
     193           68 :             return !curArpShape.intersects(shapeElement);
     194              :         });
     195              : 
     196           60 :         if (chordShape.empty()) {
     197              :             // No collisions with accidentals
     198           56 :             continue;
     199              :         }
     200              : 
     201            4 :         double chordX = chordShape.left();
     202            4 :         double diff = chordX - arpX;
     203              : 
     204            4 :         if (!muse::RealIsNull(diff)) {
     205            4 :             double inset = insetDistance(item, ctx, item->mag(), chord);
     206            4 :             largestOverlap = std::min(largestOverlap, diff + inset);
     207              :         }
     208          116 :     }
     209          252 :     if (!muse::RealIsNull(largestOverlap)) {
     210            4 :         item->mutldata()->moveX(largestOverlap);
     211              :     }
     212          252 : }
     213              : 
     214            4 : double ArpeggioLayout::insetDistance(const Arpeggio* item, const LayoutContext& ctx, double mag_, const Chord* chord)
     215              : {
     216            4 :     if (!item || !chord) {
     217            0 :         return 0.0;
     218              :     }
     219              : 
     220            4 :     std::vector<Accidental*> _accidentals;
     221              : 
     222              :     // generate list of accidentals if none provided
     223           10 :     for (Note* note : chord->notes()) {
     224            6 :         Accidental* accidental = note->accidental();
     225            6 :         if (accidental && accidental->visible()) {
     226            2 :             _accidentals.push_back(accidental);
     227              :         }
     228              :     }
     229              : 
     230            4 :     if (_accidentals.empty()) {
     231            2 :         return 0.0;
     232              :     }
     233              : 
     234            2 :     return insetDistance(item, ctx, mag_, chord, _accidentals);
     235            4 : }
     236              : 
     237           20 : double ArpeggioLayout::insetDistance(const Arpeggio* item, const LayoutContext& ctx, double mag_, const Chord* chord,
     238              :                                      const std::vector<Accidental*>& accidentals)
     239              : {
     240           20 :     if (!item || !chord) {
     241            0 :         return 0.0;
     242              :     }
     243              : 
     244           20 :     if (accidentals.empty()) {
     245            0 :         return 0.0;
     246              :     }
     247              : 
     248              :     // Only be concerned about the top/bottom of an arpeggio line if this is the start/end chord
     249              : 
     250           20 :     const Segment* seg = item->chord()->segment();
     251           20 :     Chord* endChord = item->chord();
     252           20 :     const PaddingTable& paddingTable = item->score()->paddingTables().horizontalPaddingTable();
     253           20 :     if (EngravingItem* e = seg->element(item->endTrack())) {
     254           20 :         endChord = e->isChord() ? toChord(e) : endChord;
     255              :     }
     256           20 :     bool arpStartStave = chord->vStaffIdx() == item->vStaffIdx();
     257           20 :     bool arpEndStave = chord->vStaffIdx() == endChord->vStaffIdx();
     258              : 
     259           20 :     double arpeggioTop = arpStartStave ? insetTop(item, item->chord()) * mag_ : -ARBITRARY_ARPEGGIO_LENGTH;
     260           20 :     double arpeggioBottom = arpEndStave ? insetBottom(item, endChord) * mag_ : ARBITRARY_ARPEGGIO_LENGTH;
     261              : 
     262           20 :     ArpeggioType type = item->arpeggioType();
     263           20 :     bool hasTopArrow = type == ArpeggioType::UP
     264           16 :                        || type == ArpeggioType::UP_STRAIGHT
     265           36 :                        || type == ArpeggioType::BRACKET;
     266           20 :     bool hasBottomArrow = type == ArpeggioType::DOWN
     267           20 :                           || type == ArpeggioType::DOWN_STRAIGHT
     268           40 :                           || type == ArpeggioType::BRACKET;
     269              : 
     270           20 :     const Accidental* furthestAccidental = nullptr;
     271           53 :     for (const Accidental* accidental : accidentals) {
     272           33 :         if (furthestAccidental) {
     273           13 :             bool currentIsFurtherX = accidental->x() < furthestAccidental->x();
     274           13 :             bool currentIsSameX = accidental->x() == furthestAccidental->x();
     275           13 :             auto accidentalBbox = item->symBbox(accidental->symId());
     276           13 :             double currentTop = accidental->note()->pos().y() + accidentalBbox.top() * mag_;
     277           13 :             double currentBottom = accidental->note()->pos().y() + accidentalBbox.bottom() * mag_;
     278           13 :             bool collidesWithTop = currentTop <= arpeggioTop && hasTopArrow;
     279           13 :             bool collidesWithBottom = currentBottom >= arpeggioBottom && hasBottomArrow;
     280              : 
     281           13 :             if (currentIsFurtherX || (currentIsSameX && (collidesWithTop || collidesWithBottom))) {
     282            5 :                 furthestAccidental = accidental;
     283              :             }
     284              :         } else {
     285           20 :             furthestAccidental = accidental;
     286              :         }
     287              :     }
     288              : 
     289           20 :     IF_ASSERT_FAILED(furthestAccidental) {
     290            0 :         return 0.0;
     291              :     }
     292              : 
     293           20 :     double maximumInset = (paddingTable.at(ElementType::ARPEGGIO).at(ElementType::ACCIDENTAL)
     294           20 :                            - ctx.conf().styleAbsolute(Sid::arpeggioAccidentalDistanceMin)) * mag_;
     295              : 
     296           20 :     RectF bbox = item->symBbox(furthestAccidental->symId());
     297           20 :     double center = furthestAccidental->note()->pos().y() * mag_;
     298           20 :     double top = center + bbox.top() * mag_;
     299           20 :     double bottom = center + bbox.bottom() * mag_;
     300           20 :     bool collidesWithTop = hasTopArrow && top <= arpeggioTop;
     301           20 :     bool collidesWithBottom = hasBottomArrow && bottom >= arpeggioBottom;
     302              : 
     303           20 :     if (collidesWithTop || collidesWithBottom) {
     304            8 :         return maximumInset;
     305              :     }
     306              : 
     307           12 :     return insetWidth(item);
     308              : }
     309              : 
     310          466 : double ArpeggioLayout::computeHeight(const Arpeggio* item)
     311              : {
     312          466 :     Chord* chord = item->chord();
     313          466 :     double y = chord->upNote()->pagePos().y() - chord->upNote()->headHeight() * .5;
     314              : 
     315          466 :     Note* downNote = chord->downNote();
     316          466 :     EngravingItem* e = chord->segment()->element(item->track() + item->span() - 1);
     317          466 :     if (e && e->isChord()) {
     318          466 :         downNote = toChord(e)->downNote();
     319              :     }
     320          466 :     double h = downNote->pagePos().y() + downNote->headHeight() * .5 - y;
     321          466 :     return h;
     322              : }
     323              : 
     324          466 : double ArpeggioLayout::calcTop(const Arpeggio* item, const LayoutConfiguration& conf)
     325              : {
     326          466 :     double top = -item->userLen1();
     327          466 :     switch (item->arpeggioType()) {
     328           52 :     case ArpeggioType::BRACKET: {
     329           52 :         double lineWidth = conf.styleAbsolute(item->isChordBracket() ? Sid::chordBracketLineWidth : Sid::arpeggioLineWidth);
     330           52 :         return top - lineWidth / 2.0;
     331              :     }
     332          394 :     case ArpeggioType::NORMAL:
     333              :     case ArpeggioType::UP:
     334              :     case ArpeggioType::DOWN: {
     335              :         // if the top is in the staff on a space, move it up
     336              :         // if the bottom note is on a line, the distance is 0.25 spaces
     337              :         // if the bottom note is on a space, the distance is 0.5 spaces
     338          394 :         int topNoteLine = item->chord()->upNote()->line();
     339          394 :         int lines = item->staff()->lines(item->tick());
     340          394 :         int bottomLine = (lines - 1) * 2;
     341          394 :         if (topNoteLine <= 0 || topNoteLine % 2 == 0 || topNoteLine >= bottomLine) {
     342          276 :             return top;
     343              :         }
     344          118 :         int downNoteLine = item->chord()->downNote()->line();
     345          118 :         if (downNoteLine % 2 == 1 && downNoteLine < bottomLine) {
     346           76 :             return top - 0.4 * item->spatium();
     347              :         }
     348           42 :         return top - 0.25 * item->spatium();
     349              :     }
     350           20 :     default: {
     351           20 :         return top - item->spatium() / 4;
     352              :     }
     353              :     }
     354              : }
     355              : 
     356          466 : double ArpeggioLayout::calcBottom(const Arpeggio* item, double arpeggioHeight, const LayoutConfiguration& conf)
     357              : {
     358          466 :     double top = -item->userLen1();
     359          466 :     double bottom = arpeggioHeight + item->userLen2();
     360              : 
     361          466 :     switch (item->arpeggioType()) {
     362           52 :     case ArpeggioType::BRACKET: {
     363           52 :         double lineWidth = conf.styleAbsolute(item->isChordBracket() ? Sid::chordBracketLineWidth : Sid::arpeggioLineWidth);
     364           52 :         return bottom - top + lineWidth;
     365              :     }
     366          394 :     case ArpeggioType::NORMAL:
     367              :     case ArpeggioType::UP:
     368              :     case ArpeggioType::DOWN: {
     369          394 :         return bottom;
     370              :     }
     371           20 :     default: {
     372           20 :         return bottom - top + item->spatium() / 2;
     373              :     }
     374              :     }
     375              : }
        

Generated by: LCOV version 2.0-1