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 : }
|