LCOV - code coverage report
Current view: top level - src/engraving/rendering/score - masklayout.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 91.8 % 268 246
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              : 
      23              : #include "masklayout.h"
      24              : 
      25              : #include "dom/barline.h"
      26              : #include "dom/chord.h"
      27              : #include "dom/jump.h"
      28              : #include "dom/lyrics.h"
      29              : #include "dom/marker.h"
      30              : #include "dom/measure.h"
      31              : #include "dom/measurenumber.h"
      32              : #include "dom/note.h"
      33              : #include "dom/parenthesis.h"
      34              : #include "dom/score.h"
      35              : #include "dom/segment.h"
      36              : #include "dom/staff.h"
      37              : #include "dom/stafflines.h"
      38              : #include "dom/system.h"
      39              : #include "dom/page.h"
      40              : #include "dom/textlinebase.h"
      41              : 
      42              : using namespace mu::engraving;
      43              : using namespace mu::engraving::rendering::score;
      44              : 
      45         4125 : void MaskLayout::computeMasks(LayoutContext& ctx, Page* page)
      46              : {
      47         4140 :     TRACEFUNC;
      48              : 
      49         4125 :     bool maskBarlines = ctx.conf().styleB(Sid::maskBarlinesForText);
      50         4125 :     bool maskSlurs = ctx.conf().styleB(Sid::maskSlurs);
      51         4125 :     bool maskTies = ctx.conf().styleB(Sid::maskTies);
      52              : 
      53        14320 :     for (const System* system : page->systems()) {
      54        10195 :         std::vector<TextBase*> allSystemText = collectAllSystemText(system);
      55              : 
      56        46217 :         for (MeasureBase* mb : system->measures()) {
      57        36022 :             if (!mb->isMeasure()) {
      58         2519 :                 continue;
      59              :             }
      60        33503 :             Measure* measure = toMeasure(mb);
      61              : 
      62        33503 :             if (maskBarlines) {
      63        90664 :                 for (const Segment& seg : measure->segments()) {
      64        72213 :                     if (seg.isType(SegmentType::BarLineTypes)) {
      65        20069 :                         computeBarlineMasks(&seg, system, allSystemText, ctx);
      66              :                     }
      67              :                 }
      68              :             }
      69              : 
      70        33503 :             staff_idx_t nstaves = ctx.dom().nstaves();
      71        82235 :             for (staff_idx_t staffIdx = 0; staffIdx < nstaves; ++staffIdx) {
      72        48732 :                 if (staffIdx >= system->staves().size() || !system->staff(staffIdx)->show()) {
      73          981 :                     continue;
      74              :                 }
      75        47751 :                 const Staff* staff = ctx.dom().staff(staffIdx);
      76        47751 :                 const StaffType* staffType = staff->staffType(measure->tick());
      77        47751 :                 StaffLines* staffLines = measure->staffLines(staffIdx);
      78        47751 :                 if (staffType->isTabStaff()) {
      79         1017 :                     maskTABStringLinesForFrets(staffLines, ctx);
      80              :                 }
      81              :             }
      82              :         }
      83              : 
      84        15886 :         for (SpannerSegment* spannerSeg : system->spannerSegments()) {
      85         9053 :             if (!spannerSeg->isSlurTieSegment() || spannerSeg->isTappingHalfSlurSegment()
      86         9053 :                 || !system->staff(spannerSeg->staffIdx())->show() || !spannerSeg->visible()) {
      87         2360 :                 continue;
      88              :             }
      89              : 
      90         3331 :             spannerSeg->mutldata()->setMask(Shape());
      91              : 
      92         3331 :             AutoOnOff slurTieMaskOverride = spannerSeg->getProperty(Pid::MASK_SLURTIE).value<AutoOnOff>();
      93         3331 :             if (slurTieMaskOverride == AutoOnOff::ON
      94         6662 :                 || (slurTieMaskOverride == AutoOnOff::AUTO
      95         3331 :                     && ((maskSlurs && spannerSeg->isSlurSegment())
      96         2833 :                         || (maskTies && spannerSeg->isTieSegment())))) {
      97         1469 :                 computeSlurTieMasks(toSlurTieSegment(spannerSeg));
      98              :             }
      99              :         }
     100        10195 :     }
     101         4125 : }
     102              : 
     103        20069 : void MaskLayout::computeBarlineMasks(const Segment* barlineSement, const System* system, const std::vector<TextBase*>& allSystemText,
     104              :                                      LayoutContext& ctx)
     105              : {
     106        20069 :     if (barlineSement->measure()->isLastInSystem() && barlineSement == barlineSement->measure()->lastEnabled()) {
     107         4399 :         return;
     108              :     }
     109              : 
     110        15670 :     staff_idx_t nstaves = ctx.dom().nstaves();
     111              : 
     112        15670 :     std::vector<BarLine*> barlines;
     113        15670 :     barlines.reserve(nstaves);
     114              : 
     115        40316 :     for (staff_idx_t staffIdx = 0; staffIdx < ctx.dom().nstaves(); ++staffIdx) {
     116        24646 :         if (!system->staff(staffIdx)->show()) {
     117         1001 :             continue;
     118              :         }
     119        23645 :         BarLine* barline = toBarLine(barlineSement->element(staff2track(staffIdx)));
     120        23645 :         if (!barline || !barline->spanStaff()) {
     121        15331 :             continue;
     122              :         }
     123         8314 :         maskBarlineForText(barline, allSystemText);
     124              :     }
     125        15670 : }
     126              : 
     127         8314 : void MaskLayout::maskBarlineForText(BarLine* barline, const std::vector<TextBase*>& allSystemText)
     128              : {
     129         8324 :     TRACEFUNC;
     130              : 
     131         8314 :     PointF barlinePos = barline->pagePos();
     132         8314 :     Shape barlineShape = barline->shape().translated(barlinePos);
     133              : 
     134         8314 :     Shape mask;
     135         8314 :     const double spatium = barline->spatium();
     136              : 
     137        26011 :     for (TextBase* text : allSystemText) {
     138        17697 :         const double fontSizeScaleFactor = text->size() / 10.0;
     139        17697 :         const double collisionPadding = 0.2 * spatium * fontSizeScaleFactor;
     140        17697 :         const bool hasFrame = text->frameType() != FrameType::NO_FRAME;
     141        17697 :         const bool useHighResShape = !text->isDynamic() && !text->hasFrame();
     142        17697 :         const double maskPadding = hasFrame ? 0.0 : std::clamp(0.5 * spatium * fontSizeScaleFactor, 0.1 * spatium, spatium);
     143              : 
     144        17697 :         PointF textPos = text->pagePos();
     145        17697 :         if (!barlineShape.intersects(text->ldata()->bbox().translated(textPos).padded(collisionPadding))) {
     146        17694 :             continue;
     147              :         }
     148              : 
     149            5 :         Shape textShape = (useHighResShape ? text->ldata()->highResShape() : text->ldata()->shape()).translated(textPos);
     150              : 
     151            5 :         Shape filteredTextShape = createFilteredItemShape(textShape, barlineShape, collisionPadding);
     152            5 :         if (filteredTextShape.empty()) {
     153            2 :             continue;
     154              :         }
     155              : 
     156            3 :         filteredTextShape = filteredTextShape.bbox();
     157            3 :         filteredTextShape.pad(maskPadding);
     158              : 
     159            3 :         mask.add(filteredTextShape.translate(-barlinePos));
     160            7 :     }
     161              : 
     162         8314 :     if (mask.empty()) {
     163         8311 :         barline->mutldata()->setMask(mask);
     164         8311 :         return;
     165              :     }
     166              : 
     167              :     // Ensure that we don't leave tiny barline fragments. If two masking
     168              :     // elements are too close to each other we extend them to join.
     169            3 :     barlineShape.translate(-barlinePos);
     170            3 :     const double minFragmentLengh = 0.5 * spatium;
     171            3 :     cleanupMask(barlineShape, mask, minFragmentLengh);
     172              : 
     173            3 :     barline->mutldata()->setMask(mask);
     174        24936 : }
     175              : 
     176            3 : void MaskLayout::cleanupMask(const Shape& itemShape, Shape& mask, double minFragmentLength)
     177              : {
     178            6 :     for (size_t i = 0; i < mask.size(); ++i) {
     179            3 :         ShapeElement& el = mask.elements()[i];
     180              : 
     181            3 :         if (std::abs(el.top() - itemShape.top()) < minFragmentLength) {
     182            0 :             el.adjust(0.0, -minFragmentLength, 0.0, 0.0);
     183              :         }
     184            3 :         if (std::abs(itemShape.bottom() - el.bottom()) < minFragmentLength) {
     185            3 :             el.adjust(0.0, 0.0, 0.0, minFragmentLength);
     186              :         }
     187            3 :         if (std::abs(el.left() - itemShape.left()) < minFragmentLength) {
     188            0 :             el.adjust(-minFragmentLength, 0.0, 0.0, 0.0);
     189              :         }
     190            3 :         if (std::abs(itemShape.right() - el.right()) < minFragmentLength) {
     191            0 :             el.adjust(0.0, 0.0, minFragmentLength, 0.0);
     192              :         }
     193              : 
     194            3 :         for (size_t j = i + 1; j < mask.size(); ++j) {
     195            0 :             ShapeElement& otherEl = mask.elements()[j];
     196            0 :             if (intersects(el.left(), el.right(), otherEl.left(), otherEl.right())) {
     197            0 :                 bool otherIsBelow = otherEl.y() > el.y();
     198            0 :                 double vertClearance = otherIsBelow ? otherEl.top() - el.bottom() : el.top() - otherEl.bottom();
     199            0 :                 if (vertClearance < minFragmentLength) {
     200            0 :                     (otherIsBelow ? el : otherEl).adjust(0.0, 0.0, 0.0, minFragmentLength);
     201              :                 }
     202              :             }
     203            0 :             if (intersects(el.top(), el.bottom(), otherEl.top(), el.bottom())) {
     204            0 :                 bool otherIsRight = otherEl.x() > el.x();
     205            0 :                 double horClearance = otherIsRight ? otherEl.left() - el.right() : el.left() - otherEl.right();
     206            0 :                 if (horClearance < minFragmentLength) {
     207            0 :                     (otherIsRight ? el : otherEl).adjust(0.0, 0.0, minFragmentLength, 0.0);
     208              :                 }
     209              :             }
     210              :         }
     211              :     }
     212            3 : }
     213              : 
     214        10195 : std::vector<TextBase*> MaskLayout::collectAllSystemText(const System* system)
     215              : {
     216        10210 :     TRACEFUNC;
     217              : 
     218        10195 :     std::vector<TextBase*> allText;
     219              : 
     220        46217 :     for (const MeasureBase* mb : system->measures()) {
     221        36022 :         if (!mb->isMeasure()) {
     222         2519 :             continue;
     223              :         }
     224        33503 :         const Measure* measure = toMeasure(mb);
     225        82235 :         for (staff_idx_t i = 0; i < measure->mstaves().size(); ++i) {
     226        48732 :             if (measure->showMeasureNumberOnStaff(i)) {
     227         4259 :                 if (MeasureNumber* measureNumber = measure->measureNumber(i)) {
     228         4259 :                     allText.push_back(measureNumber);
     229              :                 }
     230              :             }
     231              :         }
     232        36139 :         for (EngravingItem* item : measure->el()) {
     233         2636 :             const SysStaff* staff = system ? system->staff(item->staffIdx()) : nullptr;
     234         2636 :             const bool staffVisible = staff && staff->show();
     235         2636 :             if ((item->isMarker() || item->isJump()) && item->visible() && staffVisible) {
     236          533 :                 allText.push_back(toTextBase(item));
     237              :             }
     238              :         }
     239       158352 :         for (const Segment& s : measure->segments()) {
     240       124849 :             if (!s.isType(SegmentType::Duration) || !s.enabled()) {
     241        57680 :                 continue;
     242              :             }
     243        74429 :             for (EngravingItem* annotation : s.annotations()) {
     244         7260 :                 const SysStaff* staff = system ? system->staff(annotation->staffIdx()) : nullptr;
     245         7260 :                 const bool staffVisible = staff && staff->show();
     246         7260 :                 if (annotation->isTextBase() && annotation->visible() && staffVisible) {
     247         6458 :                     allText.push_back(toTextBase(annotation));
     248              :                 }
     249              :             }
     250        67169 :             if (!s.isChordRestType()) {
     251          346 :                 continue;
     252              :             }
     253       440931 :             for (EngravingItem* chordRest : s.elist()) {
     254       374108 :                 if (!chordRest || !system->staff(chordRest->staffIdx())->show()) {
     255       282286 :                     continue;
     256              :                 }
     257        94002 :                 for (Lyrics* lyr : toChordRest(chordRest)->lyrics()) {
     258         2180 :                     if (lyr->visible()) {
     259         2165 :                         allText.push_back(lyr);
     260              :                     }
     261              :                 }
     262              :             }
     263              :         }
     264              :     }
     265              : 
     266        15886 :     for (SpannerSegment* spannerSegment : system->spannerSegments()) {
     267         1191 :         if (!spannerSegment->isTextLineBaseSegment() || !system->staff(spannerSegment->staffIdx())->show()
     268         6882 :             || !spannerSegment->getProperty(Pid::VISIBLE).toBool()) {
     269         4506 :             continue;
     270              :         }
     271         1185 :         TextLineBaseSegment* textLineBaseSegment = static_cast<TextLineBaseSegment*>(spannerSegment);
     272         1185 :         Text* beginText = textLineBaseSegment->text();
     273         1185 :         Text* endText = textLineBaseSegment->endText();
     274         1185 :         if (beginText && !beginText->empty()) {
     275          686 :             allText.push_back(beginText);
     276              :         }
     277         1185 :         if (endText && !endText->empty()) {
     278           30 :             allText.push_back(endText);
     279              :         }
     280              :     }
     281              : 
     282        10195 :     return allText;
     283        10195 : }
     284              : 
     285         1017 : void MaskLayout::maskTABStringLinesForFrets(StaffLines* staffLines, const LayoutContext& ctx)
     286              : {
     287         1017 :     staff_idx_t staffIdx = staffLines->staffIdx();
     288         1017 :     bool linesThrough = ctx.dom().staff(staffIdx)->staffType(Fraction())->linesThrough();
     289              : 
     290         1017 :     PointF staffLinesPos = staffLines->pagePos();
     291              : 
     292         1017 :     double padding = ctx.conf().styleAbsolute(Sid::tabFretPadding);
     293              : 
     294         1017 :     track_idx_t startTrack = staff2track(staffIdx);
     295         1017 :     track_idx_t endTrack = startTrack + VOICES;
     296              : 
     297         1017 :     Shape mask;
     298              : 
     299          820 :     auto maskFret = [&mask, linesThrough, padding, staffLinesPos] (Chord* chord) {
     300         1832 :         for (Note* note : chord->notes()) {
     301         1012 :             if (!note->visible()) {
     302           22 :                 continue;
     303              :             }
     304          990 :             if (!note->shouldHideFret() && (!linesThrough || note->fretConflict())) {
     305          976 :                 Shape noteShape = note->ldata()->bbox();
     306          976 :                 noteShape.translate(note->pagePos());
     307              : 
     308          976 :                 noteShape.pad(padding);
     309          976 :                 mask.add(noteShape.translated(-staffLinesPos));
     310          976 :             }
     311              :         }
     312          820 :     };
     313              : 
     314          820 :     auto maskParens = [&mask, linesThrough, padding, staffLinesPos] (Chord* chord) {
     315          820 :         if (linesThrough) {
     316            2 :             return;
     317              :         }
     318          831 :         for (const NoteParenthesisInfo* i : chord->noteParentheses()) {
     319           13 :             const Parenthesis* leftParen = i->leftParen();
     320           13 :             const Parenthesis* rightParen = i->rightParen();
     321           13 :             bool allHidden = false;
     322           13 :             for (const Note* note : i->notes()) {
     323           13 :                 if (!note->shouldHideFret()) {
     324           13 :                     allHidden = false;
     325           13 :                     break;
     326              :                 }
     327              :             }
     328              : 
     329           13 :             if (allHidden) {
     330            0 :                 continue;
     331              :             }
     332              : 
     333              :             // HACK: Overlapping mask regions which have the same y value and height cause errors
     334              :             // Parentheses on single fret marks have the same height as the fret marks
     335              :             static const double PAREN_PADDING_EPSILON = 1.0;
     336           13 :             if (leftParen && leftParen->visible()) {
     337           10 :                 Shape leftParenShape = leftParen->ldata()->bbox().translated(leftParen->pagePos());
     338           10 :                 leftParenShape.pad(padding);
     339           10 :                 leftParenShape.adjust(0, -PAREN_PADDING_EPSILON, 0, PAREN_PADDING_EPSILON);
     340           10 :                 mask.add(leftParenShape.translated(-staffLinesPos));
     341           10 :             }
     342              : 
     343           13 :             if (rightParen && rightParen->visible()) {
     344           10 :                 Shape rightParenShape = rightParen->ldata()->bbox().translated(rightParen->pagePos());
     345           10 :                 rightParenShape.pad(padding);
     346           10 :                 rightParenShape.adjust(0, -PAREN_PADDING_EPSILON, 0, PAREN_PADDING_EPSILON);
     347           10 :                 mask.add(rightParenShape.translated(-staffLinesPos));
     348           10 :             }
     349              :         }
     350         1017 :     };
     351              : 
     352         1017 :     const Measure* measure = staffLines->measure();
     353         2630 :     for (Segment* seg = measure->first(SegmentType::ChordRest); seg; seg = seg->next(SegmentType::ChordRest)) {
     354         8065 :         for (track_idx_t track = startTrack; track < endTrack; ++track) {
     355         6452 :             EngravingItem* el = seg->element(track);
     356         6452 :             if (!el || !el->isChord()) {
     357         5664 :                 continue;
     358              :             }
     359          788 :             maskFret(toChord(el));
     360          788 :             maskParens(toChord(el));
     361          820 :             for (Chord* grace : toChord(el)->graceNotes()) {
     362           32 :                 maskFret(grace);
     363           32 :                 maskParens(grace);
     364              :             }
     365              :         }
     366              :     }
     367              : 
     368         1017 :     staffLines->mutldata()->setMask(mask);
     369         1017 : }
     370              : 
     371         1469 : void MaskLayout::computeSlurTieMasks(SlurTieSegment* slurTieSegment)
     372              : {
     373         1477 :     TRACEFUNC;
     374              : 
     375         1469 :     Spanner* spanner = slurTieSegment->spanner();
     376         1469 :     const Segment* startSeg = spanner->startSegment();
     377         1469 :     const Segment* endSeg = spanner->endSegment();
     378         1469 :     const EngravingItem* startElement = spanner->startElement();
     379         1469 :     const EngravingItem* endElement = spanner->endElement();
     380         1469 :     staff_idx_t startStaffIdx = startElement ? startElement->vStaffIdx() : 0;
     381         1469 :     staff_idx_t endStaffIdx = endElement ? endElement->vStaffIdx() : spanner->score()->nstaves() - 1;
     382         1469 :     if (startStaffIdx > endStaffIdx) {
     383            2 :         std::swap(startStaffIdx, endStaffIdx);
     384              :     }
     385              : 
     386         1469 :     std::vector<const EngravingItem*> itemsToMaskOver;
     387         3475 :     for (const Segment* seg = startSeg; seg && seg != endSeg; seg = seg->next1()) {
     388         2006 :         if (!seg->enabled()
     389         2003 :             || !seg->isType(SegmentType::KeySigTypes | SegmentType::TimeSigTypes | SegmentType::ClefTypes)
     390         4009 :             || seg->system() != slurTieSegment->system()) {
     391         1937 :             continue;
     392              :         }
     393          140 :         for (staff_idx_t staffIdx = startStaffIdx; staffIdx <= endStaffIdx; ++staffIdx) {
     394           71 :             if (EngravingItem* item = seg->element(staffIdx * VOICES)) { // Key/Time/Clef-type segments never have elements in any voice other than 0
     395           69 :                 itemsToMaskOver.push_back(item);
     396              :             }
     397              :         }
     398              :     }
     399              : 
     400         1469 :     PointF slurTiePos = slurTieSegment->pagePos();
     401         1469 :     Shape slurTieShape = slurTieSegment->shape().translated(slurTiePos);
     402              : 
     403         1469 :     Shape mask;
     404         1469 :     const double spatium = slurTieSegment->spatium();
     405         1469 :     const double maskPadding = spatium * StyleDef::styleValues[size_t(Sid::keysigLeftMargin)].defaultValue.toDouble() / 2;
     406         1469 :     const double horizontalCollisionPadding = maskPadding;
     407         1469 :     const double verticalCollisionPadding = spatium * 0.1;
     408              : 
     409         1538 :     for (const EngravingItem* item : itemsToMaskOver) {
     410           69 :         PointF itemPos = item->pagePos();
     411           69 :         Shape itemShape = item->shape().translated(itemPos);
     412              : 
     413           69 :         if (!slurTieShape.bbox().intersects(itemShape.bbox().adjusted(-horizontalCollisionPadding, -verticalCollisionPadding,
     414              :                                                                       horizontalCollisionPadding, verticalCollisionPadding))) {
     415           62 :             continue;
     416              :         }
     417              : 
     418            7 :         Shape filteredItemShape = createFilteredItemShape(itemShape, slurTieShape, horizontalCollisionPadding, verticalCollisionPadding);
     419            7 :         if (filteredItemShape.empty()) {
     420            7 :             continue;
     421              :         }
     422              : 
     423            0 :         mask.add(filteredItemShape.translate(-slurTiePos));
     424           76 :     }
     425              : 
     426         1469 :     if (mask.empty()) {
     427         1469 :         slurTieSegment->mutldata()->setMask(mask);
     428         1469 :         return;
     429              :     }
     430              : 
     431            0 :     slurTieShape.translate(-slurTiePos);
     432              : 
     433              :     // Use the mask's bbox to break the slur/tie only once, regardless of the mask elements in between:
     434            0 :     RectF maskBbox = mask.bbox();
     435              : 
     436              :     // Avoid the slur/tie line being partially masked (i.e. it should always be either fully "cut",
     437              :     // or not masked at all, so we extend the mask vertically to encompass the slur/tie's full height):
     438            0 :     maskBbox.adjust(0.0, slurTieShape.top() - mask.top(), 0.0, slurTieShape.bottom() - mask.bottom());
     439            0 :     maskBbox.pad(maskPadding);
     440              : 
     441            0 :     slurTieSegment->mutldata()->setMask(maskBbox);
     442         5876 : }
     443              : 
     444            5 : Shape MaskLayout::createFilteredItemShape(const Shape& overlyingItemShape, const Shape& maskedItemShape, const double collisionPadding)
     445              : {
     446            5 :     return MaskLayout::createFilteredItemShape(overlyingItemShape, maskedItemShape, collisionPadding, collisionPadding);
     447              : }
     448              : 
     449           12 : Shape MaskLayout::createFilteredItemShape(const Shape& overlyingItemShape, const Shape& maskedItemShape,
     450              :                                           const double horizontalCollisionPadding, const double verticalCollisionPadding)
     451              : {
     452           12 :     Shape filteredItemShape;
     453           12 :     filteredItemShape.elements().reserve(overlyingItemShape.elements().size());
     454           93 :     for (const ShapeElement& el : overlyingItemShape.elements()) {
     455           81 :         Shape padded = el.adjusted(-horizontalCollisionPadding, -verticalCollisionPadding,
     456           81 :                                    horizontalCollisionPadding, verticalCollisionPadding);
     457           81 :         if (maskedItemShape.intersects(padded)) {
     458            5 :             filteredItemShape.add(el);
     459              :         }
     460           81 :     }
     461           12 :     return filteredItemShape;
     462            0 : }
        

Generated by: LCOV version 2.0-1