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 "lyricslayout.h"
23 :
24 : #include "dom/factory.h"
25 : #include "dom/repeatlist.h"
26 : #include "style/styledef.h"
27 :
28 : #include "dom/chordrest.h"
29 : #include "dom/lyrics.h"
30 : #include "dom/measure.h"
31 : #include "dom/score.h"
32 : #include "dom/segment.h"
33 : #include "dom/staff.h"
34 : #include "dom/stafftype.h"
35 : #include "dom/system.h"
36 :
37 : #include "tlayout.h"
38 : #include "textlayout.h"
39 :
40 : using namespace mu;
41 : using namespace mu::engraving;
42 : using namespace mu::engraving::rendering::score;
43 :
44 2180 : void LyricsLayout::layout(Lyrics* item, LayoutContext& ctx)
45 : {
46 2180 : if (!item->ownershipParent()) { // palette & clone trick
47 0 : item->setPos(PointF());
48 0 : TextLayout::layoutBaseTextBase1(item, ctx);
49 0 : return;
50 : }
51 :
52 2180 : Lyrics::LayoutData* ldata = item->mutldata();
53 :
54 : //
55 : // parse leading verse number and/or punctuation, so we can factor it into layout separately
56 : //
57 2180 : bool hasNumber = false; // _verseNumber;
58 :
59 : // find:
60 : // 1) string of numbers and non-word characters at start of syllable
61 : // 2) at least one other character (indicating start of actual lyric)
62 : // 3) string of non-word characters at end of syllable
63 : //QRegularExpression leadingPattern("(^[\\d\\W]+)([^\\d\\W]+)");
64 :
65 2180 : const String text = item->plainText();
66 2180 : String leading;
67 2180 : String trailing;
68 :
69 2180 : if (ctx.conf().styleB(Sid::lyricsAlignVerseNumber)) {
70 2180 : size_t leadingIdx = 0;
71 2338 : for (size_t i = 0; i < text.size(); ++i) {
72 2268 : Char ch = text.at(i);
73 2268 : if (ch.isLetter()) {
74 2110 : leadingIdx = i;
75 2110 : break;
76 : }
77 : }
78 :
79 2180 : if (leadingIdx != 0) {
80 30 : leading = text.mid(0, leadingIdx);
81 : }
82 :
83 2180 : size_t trailingIdx = text.size() - 1;
84 2566 : for (int i = static_cast<int>(text.size() - 1); i >= 0; --i) {
85 2496 : Char ch = text.at(i);
86 2496 : if (ch.isLetter()) {
87 2110 : trailingIdx = i;
88 2110 : break;
89 : }
90 : }
91 :
92 2180 : if (trailingIdx != text.size() - 1) {
93 288 : trailing = text.mid(trailingIdx + 1);
94 : }
95 :
96 2180 : if (!leading.isEmpty() && leading.at(0).isDigit()) {
97 24 : hasNumber = true;
98 : }
99 : }
100 :
101 2180 : createOrRemoveLyricsLine(item, ctx);
102 :
103 2180 : if (item->isMelisma() || hasNumber) {
104 : // use the melisma style alignment setting
105 182 : if (item->isStyled(Pid::POSITION)) {
106 182 : if (ctx.conf().styleB(Sid::lyricsCenterDashedSyllables) && !(item->separator() && item->separator()->isEndMelisma())) {
107 0 : item->setPosition(AlignH::HCENTER);
108 : } else {
109 182 : item->setPosition(ctx.conf().styleV(Sid::lyricsMelismaAlign).value<Align>().horizontal);
110 : }
111 : }
112 : } else {
113 : // use the text style alignment setting
114 1998 : if (item->isStyled(Pid::POSITION)) {
115 1973 : item->setPosition(item->propertyDefault(Pid::POSITION).value<AlignH>());
116 : }
117 : }
118 :
119 : // Negate ChordRest offset
120 2180 : ChordRest* cr = item->chordRest();
121 2180 : double x = -cr->x();
122 :
123 : /* Preserve the vertical position: a lyric's Y is set in computeVerticalPositions(). Necessary
124 : * because layoutBaseTextBase1() ends with ldata->move(defaultPos), which accumulates a downward
125 : * shift of the lyric if the current function is called in a partially-relaid system without a
126 : * following computeVerticalPositions() call. */
127 2180 : const double posY = ldata->pos().y();
128 2180 : TextLayout::layoutBaseTextBase1(item, ctx);
129 2180 : TextLayout::computeTextHighResShape(item, ldata);
130 2180 : ldata->setPosY(posY);
131 :
132 2180 : double centerAdjust = 0.0;
133 2180 : double leftAdjust = 0.0;
134 :
135 2180 : if (ctx.conf().styleB(Sid::lyricsAlignVerseNumber)) {
136 : // Calculate leading and trailing parts widths. Lyrics
137 : // should have text layout to be able to do it correctly.
138 2180 : DO_ASSERT(ldata->blocks.size() != 0);
139 2180 : if (!leading.isEmpty() || !trailing.isEmpty()) {
140 : // LOGD("create leading, trailing <%s> -- <%s><%s>", muPrintable(text), muPrintable(leading), muPrintable(trailing));
141 315 : const TextBlock& tb = ldata->blocks.at(0);
142 :
143 315 : const double leadingWidth = tb.xpos(leading.size(), item) - tb.boundingRect().x();
144 315 : const size_t trailingPos = text.size() - trailing.size();
145 315 : const double trailingWidth = tb.boundingRect().right() - tb.xpos(trailingPos, item);
146 :
147 315 : leftAdjust = leadingWidth;
148 315 : centerAdjust = leadingWidth - trailingWidth;
149 : }
150 : }
151 :
152 2180 : if (item->position() == AlignH::HCENTER) {
153 : //
154 : // center under notehead, not origin
155 : // however, lyrics that are melismas or have verse numbers will be forced to left alignment
156 : //
157 : // center under note head
158 1973 : x += -centerAdjust * 0.5;
159 207 : } else if (item->position() == AlignH::LEFT) {
160 : // even for left aligned syllables, ignore leading verse numbers and/or punctuation
161 207 : x -= leftAdjust;
162 : }
163 :
164 2180 : ldata->setPosX(x);
165 :
166 2180 : if (item->ticks().isNotZero()) {
167 : // set melisma end
168 100 : ChordRest* ecr = ctx.mutDom().findCR(item->endTick(), item->track());
169 100 : if (ecr) {
170 100 : ecr->setMelismaEnd(true);
171 : }
172 : }
173 2180 : }
174 :
175 328 : void LyricsLayout::layout(LyricsLine* item)
176 : {
177 328 : if (item->isDash()) { // dash(es)
178 228 : item->setNextLyrics(searchNextLyrics(item->lyrics()->segment(),
179 : item->staffIdx(),
180 228 : item->lyrics()->verse(),
181 228 : item->lyrics()->placement()
182 : ));
183 228 : item->setTrack2(item->nextLyrics() ? item->nextLyrics()->track() : item->track());
184 :
185 228 : Fraction endTick = item->tick();
186 228 : const Measure* lyricsMeasure = item->lyrics()->segment()->measure();
187 228 : const Segment* endCRSeg = lyricsMeasure ? lyricsMeasure->last(SegmentType::ChordRest) : nullptr;
188 :
189 228 : const ChordRest* endCR = nullptr;
190 228 : if (endCRSeg && !endCRSeg->empty()) {
191 526 : for (EngravingItem* cr : endCRSeg->elist()) {
192 526 : if (cr && cr->isChordRest()) {
193 228 : endCR = toChordRest(cr);
194 228 : break;
195 : }
196 : }
197 : }
198 :
199 228 : if (item->nextLyrics()) {
200 223 : endTick = item->nextLyrics()->tick();
201 5 : } else if (endCR && endCR->hasFollowingJumpItem()) {
202 0 : endTick = endCR->tick();
203 : }
204 :
205 228 : item->setTick2(endTick);
206 : }
207 328 : }
208 :
209 348 : void LyricsLayout::layout(LyricsLineSegment* item, LayoutContext& ctx)
210 : {
211 : UNUSED(ctx);
212 :
213 348 : assert(item->isPartialLyricsLineSegment() || item->lyrics());
214 :
215 348 : LyricsLineSegment::LayoutData* ldata = item->mutldata();
216 348 : ldata->clearDashes();
217 :
218 348 : if (item->lyricsLine()->isEndMelisma()) {
219 117 : layoutMelismaLine(item);
220 : } else {
221 231 : layoutDashes(item);
222 : }
223 :
224 348 : double halfLineWidth = item->absoluteFromSpatium(item->lineWidth());
225 348 : RectF rect(PointF(), item->pos2());
226 348 : rect.adjust(0.0, -halfLineWidth, 0.0, halfLineWidth);
227 :
228 348 : ldata->setShape(Shape(rect, item));
229 348 : }
230 :
231 117 : void LyricsLayout::layoutMelismaLine(LyricsLineSegment* item)
232 : {
233 117 : const bool isPartialLyricsLine = item->isPartialLyricsLineSegment();
234 117 : LyricsLine* lyricsLine = item->lyricsLine();
235 117 : Lyrics* startLyrics = lyricsLine->lyrics();
236 :
237 117 : System* system = item->system();
238 117 : if (!system) {
239 0 : return;
240 : }
241 117 : const MStyle& style = item->style();
242 :
243 117 : double startX = lyricsLineStartX(item);
244 117 : double endX = lyricsLineEndX(item);
245 :
246 117 : double tolerance = 0.05 * item->spatium();
247 117 : if (endX - startX < style.styleAbsolute(Sid::lyricsMelismaMinLength) - tolerance) {
248 3 : const Fraction ticks = isPartialLyricsLine ? lyricsLine->ticks() : startLyrics->ticks();
249 3 : if (style.styleB(Sid::lyricsMelismaForce) || ticks == Lyrics::TEMP_MELISMA_TICKS) {
250 0 : endX = startX + style.styleAbsolute(Sid::lyricsMelismaMinLength);
251 : } else {
252 3 : endX = startX;
253 : }
254 : }
255 :
256 117 : adjustLyricsLineYOffset(item);
257 :
258 117 : item->mutldata()->setPosX(startX);
259 117 : item->setPos2(PointF(endX - startX, 0.0));
260 :
261 117 : item->mutldata()->addDash(LineF(PointF(), item->pos2()));
262 : }
263 :
264 231 : void LyricsLayout::layoutDashes(LyricsLineSegment* item)
265 : {
266 231 : const bool isPartialLyricsLine = item->isPartialLyricsLineSegment();
267 231 : LyricsLine* lyricsLine = item->lyricsLine();
268 :
269 231 : ChordRest* endCR = lyricsLine->endElement()
270 231 : && lyricsLine->endElement()->isChordRest() ? toChordRest(lyricsLine->endElement()) : nullptr;
271 231 : Lyrics* endLyrics = nullptr;
272 231 : if (endCR) {
273 315 : for (Lyrics* lyr : endCR->lyrics()) {
274 315 : if (lyr->verse() == item->verse()) {
275 231 : endLyrics = lyr;
276 231 : break;
277 : }
278 : }
279 : }
280 :
281 : // When the end element is a timetick segment rather than a chordrest, the start element should be a chord with a following repeat
282 231 : ChordRest* startCR = lyricsLine->startElement()
283 231 : && lyricsLine->startElement()->isChordRest() ? toChordRest(lyricsLine->startElement()) : nullptr;
284 231 : bool hasFollowingJump = endCR ? endCR->hasFollowingJumpItem() : (startCR ? startCR->hasFollowingJumpItem() : false);
285 :
286 231 : if (!endLyrics && !isPartialLyricsLine && !hasFollowingJump) {
287 0 : return;
288 : }
289 :
290 231 : System* system = item->system();
291 231 : if (!system) {
292 0 : return;
293 : }
294 231 : const MStyle& style = item->style();
295 :
296 231 : double startX = lyricsLineStartX(item);
297 231 : double endX = 0.0;
298 231 : if (endCR) {
299 231 : endX = lyricsLineEndX(item, endLyrics);
300 : } else {
301 0 : endX = startCR ? startCR->measure()->endingXForOpenEndedLines() : endX;
302 : }
303 :
304 231 : adjustLyricsLineYOffset(item, endLyrics);
305 :
306 231 : item->mutldata()->setPosX(startX);
307 231 : item->setPos2(PointF(endX - startX, 0.0));
308 :
309 231 : bool isDashOnFirstSyllable = lyricsLine->tick2() == system->firstMeasure()->tick();
310 231 : const double userStart = startX + item->offset().x();
311 231 : const double userEnd = endX + item->userOff2().x();
312 231 : double curLength = userEnd - userStart;
313 231 : double dashMinLength = style.styleAbsolute(Sid::lyricsDashMinLength);
314 231 : bool firstAndLastGapAreHalf = style.styleB(Sid::lyricsDashFirstAndLastGapAreHalf);
315 231 : bool forceDash = style.styleB(Sid::lyricsDashForce)
316 231 : || (style.styleB(Sid::lyricsShowDashIfSyllableOnFirstNote) && isDashOnFirstSyllable);
317 231 : double maxDashDistance = style.styleAbsolute(Sid::lyricsDashMaxDistance);
318 231 : int dashCount = firstAndLastGapAreHalf && curLength > maxDashDistance ? std::ceil(curLength / maxDashDistance)
319 217 : : std::floor(curLength / maxDashDistance);
320 :
321 231 : if (curLength > dashMinLength || forceDash) {
322 231 : dashCount = std::max(dashCount, 1);
323 : }
324 :
325 231 : if (style.styleB(Sid::lyricsLimitDashCount)) {
326 0 : dashCount = std::min(dashCount, style.styleI(Sid::lyricsMaxDashCount));
327 : }
328 :
329 231 : if (curLength < dashMinLength && dashCount > 0) {
330 13 : double diff = dashMinLength - curLength;
331 13 : if (isDashOnFirstSyllable) {
332 0 : startX -= diff;
333 : } else {
334 13 : startX -= 0.5 * diff;
335 13 : endX += 0.5 * diff;
336 : }
337 13 : item->mutldata()->setPosX(startX);
338 13 : item->setPos2(PointF(endX - startX, 0.0));
339 13 : curLength = endX - startX;
340 : }
341 :
342 231 : double dashWidth = std::min(curLength, style.styleAbsolute(Sid::lyricsDashMaxLength));
343 :
344 231 : LyricsDashSystemStart lyricsDashSystemStart = style.styleV(Sid::lyricsDashPosAtStartOfSystem).value<LyricsDashSystemStart>();
345 231 : bool dashesLeftAligned = lyricsDashSystemStart != LyricsDashSystemStart::STANDARD && !item->isSingleBeginType();
346 231 : double dashDist = curLength / (dashesLeftAligned || firstAndLastGapAreHalf ? dashCount : dashCount + 1);
347 231 : double xDash = 0.0;
348 231 : if (dashesLeftAligned) {
349 0 : for (int i = 0; i < dashCount; ++i) {
350 0 : item->mutldata()->addDash(LineF(PointF(xDash, 0.0), PointF(xDash + dashWidth, 0.0)));
351 0 : xDash += dashDist;
352 : }
353 : } else {
354 479 : for (int i = 0; i < dashCount; ++i) {
355 248 : if (firstAndLastGapAreHalf && i == 0) {
356 231 : xDash += 0.5 * dashDist;
357 : } else {
358 17 : xDash += dashDist;
359 : }
360 248 : item->mutldata()->addDash(LineF(PointF(xDash - 0.5 * dashWidth, 0.0), PointF(xDash + 0.5 * dashWidth, 0.0)));
361 : }
362 : }
363 : }
364 :
365 20 : Lyrics* LyricsLayout::findNextLyrics(const ChordRest* endChordRest, int verseNumber, std::optional<PlacementV> placement)
366 : {
367 20 : if (!endChordRest) {
368 0 : return nullptr;
369 : }
370 76 : for (Segment* segment = endChordRest->segment()->next1(SegmentType::ChordRest); segment;
371 56 : segment = segment->next1(SegmentType::ChordRest)) {
372 61 : if (!segment->element(endChordRest->track())) {
373 20 : continue;
374 : }
375 41 : ChordRest* nextCR = toChordRest(segment->element(endChordRest->track()));
376 41 : for (Lyrics* lyr : nextCR->lyrics()) {
377 5 : if (lyr->verse() == verseNumber && (!placement || lyr->placement() == placement)) {
378 5 : return lyr;
379 : }
380 : }
381 : }
382 :
383 15 : return nullptr;
384 : }
385 :
386 2180 : void LyricsLayout::createOrRemoveLyricsLine(Lyrics* item, LayoutContext& ctx)
387 : {
388 2180 : if (item->needRemoveInvalidSegments()) {
389 2 : item->removeInvalidSegments();
390 : }
391 :
392 4360 : auto isEndMelisma = [item]() {
393 4360 : return item->ticks().isNotZero();
394 2180 : };
395 :
396 2180 : Fraction lyricsLineTicks = Lyrics::TEMP_MELISMA_TICKS;
397 :
398 : // Update the end tick
399 2180 : if (isEndMelisma()) {
400 100 : const track_idx_t track = item->track();
401 :
402 100 : const Segment* const startSegment = item->segment();
403 100 : const Fraction startTick = startSegment->tick();
404 :
405 : // if lyrics has a temporary one-chord melisma, interpret as 0 ticks (just its own chord)
406 100 : Fraction itemEndTick = item->ticks() == Lyrics::TEMP_MELISMA_TICKS ? startTick : item->endTick();
407 100 : Fraction endTick = itemEndTick;
408 :
409 100 : const Segment* endSegment = startSegment;
410 356 : while (endSegment && endSegment->tick() < endTick) {
411 256 : endSegment = endSegment->nextCR(track, true);
412 : }
413 100 : if (!endSegment) {
414 : // user probably deleted measures at end of score, leaving this melisma too long
415 : // set endSegment to last segment and reset lyricsEndTick to trigger FIXUP code below
416 2 : endSegment = ctx.dom().lastSegment();
417 2 : endTick = Fraction(-1, 1);
418 : }
419 :
420 100 : EngravingItem* endSegmentElement = endSegment->element(track);
421 100 : if (endSegment->tick() == endTick && endSegmentElement && endSegmentElement->isChord()) {
422 : // everything is OK if we have reached a chord at right tick on right track
423 : // advance to next CR after duration of note, or last segment if no next CR
424 98 : const Segment* endChordSeg = endSegment;
425 98 : const Chord* endChord = toChord(endSegmentElement);
426 :
427 98 : endSegment = endChordSeg->nextCR(track, false);
428 :
429 98 : if (!endSegment || endSegment->tick() > endChord->endTick()) {
430 7 : endSegment = endChordSeg;
431 26 : while (endSegment && endSegment->tick() < endChord->endTick()) {
432 19 : endSegment = endSegment->nextCR(muse::nidx, true);
433 : }
434 : }
435 : } else {
436 : // FIXUP - lyrics tick count not valid
437 : // this happens if edits to score have removed the original end segment
438 : // so let's fix it here
439 : // endSegment is already pointing to segment past endTick (or to last segment)
440 : // we should shorten the lyrics tick count to make this work
441 2 : const Segment* ns = endSegment;
442 2 : const Segment* ps = endSegment->prev1(SegmentType::ChordRest);
443 5 : while (ps && ps != startSegment) {
444 5 : EngravingItem* pe = ps->element(track);
445 : // we're looking for an actual chord on this track
446 5 : if (pe && pe->isChord()) {
447 2 : break;
448 : }
449 3 : endSegment = ps;
450 3 : ps = ps->prev1(SegmentType::ChordRest);
451 : }
452 :
453 2 : if (!ps || ps == startSegment) {
454 : // either there is no valid previous CR, or the previous CR is the one the lyric starts on
455 : // we don't want to make the melisma longer arbitrarily, but there is a possibility that the next
456 : // CR won't extend the melisma, so let's check it
457 0 : ps = ns;
458 0 : endSegment = ps->nextCR(track, false);
459 0 : EngravingItem* e = endSegment ? endSegment->element(track) : nullptr;
460 :
461 : // check to make sure we have a chord
462 0 : if (!e || !e->isChord() || ps->tick() > itemEndTick) {
463 : // nope, nothing to do but set ticks to 0
464 : // this will result in no melisma
465 0 : item->undoChangeProperty(Pid::LYRIC_TICKS, Fraction::fromTicks(0));
466 : } else {
467 0 : item->undoChangeProperty(Pid::LYRIC_TICKS, ps->tick() - startTick);
468 : }
469 0 : } else {
470 2 : item->undoChangeProperty(Pid::LYRIC_TICKS, ps->tick() - startTick);
471 : }
472 : }
473 :
474 100 : if (endSegment) {
475 : // Lyrics::_ticks points to the beginning of the last spanned segment,
476 : // but the line shall include it:
477 : // include the duration of this last segment in the melisma duration
478 93 : lyricsLineTicks = endSegment->tick() - startTick;
479 : } else {
480 7 : lyricsLineTicks = item->score()->endTick() - startTick;
481 : }
482 : }
483 :
484 2180 : ChordRest* cr = item->chordRest();
485 :
486 2180 : if (isEndMelisma() || item->syllabic() == LyricsSyllabic::BEGIN || item->syllabic() == LyricsSyllabic::MIDDLE) {
487 328 : if (!item->separator()) {
488 243 : LyricsLine* separator = Factory::createLyricsLine(ctx.mutDom().dummyParent());
489 243 : separator->setGenerated(true);
490 243 : separator->setTick(cr->tick());
491 243 : separator->setVisible(item->visible());
492 243 : item->setSeparator(separator);
493 243 : ctx.mutDom().addUnmanagedSpanner(item->separator());
494 : }
495 328 : item->separator()->setOwnershipParent(item);
496 328 : item->separator()->setTick(cr->tick());
497 328 : item->separator()->setTicks(lyricsLineTicks);
498 328 : item->separator()->setTrack(item->track());
499 328 : item->separator()->setTrack2(item->track());
500 328 : item->separator()->styleChanged();
501 : } else {
502 1852 : if (item->separator()) {
503 0 : item->separator()->removeUnmanaged();
504 0 : delete item->separator();
505 0 : item->setSeparator(nullptr);
506 : }
507 : }
508 2180 : }
509 :
510 7388 : void LyricsLayout::computeVerticalPositions(System* system, LayoutContext& ctx)
511 : {
512 7388 : staff_idx_t nStaves = system->score()->nstaves();
513 :
514 7388 : std::vector<staff_idx_t> visibleStaves;
515 7388 : visibleStaves.reserve(system->staves().size());
516 :
517 18583 : for (staff_idx_t staffIdx = 0; staffIdx < nStaves; ++staffIdx) {
518 11195 : if (system->staff(staffIdx)->show()) {
519 10992 : computeVerticalPositions(staffIdx, system, ctx);
520 : }
521 : }
522 7388 : }
523 :
524 10992 : void LyricsLayout::computeVerticalPositions(staff_idx_t staffIdx, System* system, LayoutContext& ctx)
525 : {
526 10992 : LyricsVersesMap lyricsVersesAbove;
527 10992 : LyricsVersesMap lyricsVersesBelow;
528 :
529 10992 : collectLyricsVerses(staffIdx, system, lyricsVersesAbove, lyricsVersesBelow);
530 :
531 10992 : setDefaultPositions(staffIdx, lyricsVersesAbove, lyricsVersesBelow, ctx);
532 :
533 10992 : checkCollisionsWithStaffElements(system, staffIdx, ctx, lyricsVersesAbove, lyricsVersesBelow);
534 :
535 10992 : addToSkyline(system, staffIdx, ctx, lyricsVersesAbove, lyricsVersesBelow);
536 10992 : }
537 :
538 10992 : void LyricsLayout::collectLyricsVerses(staff_idx_t staffIdx, System* system, LyricsVersesMap& lyricsVersesAbove,
539 : LyricsVersesMap& lyricsVersesBelow)
540 : {
541 10992 : track_idx_t startTrack = staffIdx * VOICES;
542 10992 : track_idx_t endTrack = startTrack + VOICES;
543 :
544 57035 : for (MeasureBase* mb : system->measures()) {
545 46043 : if (!mb->isMeasure()) {
546 184 : continue;
547 : }
548 219094 : for (Segment& segment : toMeasure(mb)->segments()) {
549 173235 : if (!segment.isChordRestType()) {
550 82903 : continue;
551 : }
552 451660 : for (track_idx_t track = startTrack; track < endTrack; ++track) {
553 361328 : EngravingItem* element = segment.element(track);
554 361328 : if (!element) {
555 271645 : continue;
556 : }
557 91863 : for (Lyrics* lyrics : toChordRest(element)->lyrics()) {
558 2180 : int verse = lyrics->verse();
559 2180 : if (lyrics->placeAbove()) {
560 16 : lyricsVersesAbove[verse].addLyrics(lyrics);
561 : } else {
562 2164 : lyricsVersesBelow[verse].addLyrics(lyrics);
563 : }
564 : }
565 : }
566 : }
567 : }
568 :
569 20782 : for (SpannerSegment* spannerSegment : system->spannerSegments()) {
570 9790 : if (spannerSegment->staffIdx() == staffIdx && spannerSegment->isLyricsLineSegment()) {
571 348 : if (muse::RealIsNull(spannerSegment->pos2().x())) {
572 3 : continue;
573 : }
574 345 : LyricsLineSegment* lyricsLineSegment = toLyricsLineSegment(spannerSegment);
575 345 : int verse = lyricsLineSegment->verse();
576 345 : if (lyricsLineSegment->lyricsPlaceAbove()) {
577 4 : lyricsVersesAbove[verse].addLine(lyricsLineSegment);
578 : } else {
579 341 : lyricsVersesBelow[verse].addLine(lyricsLineSegment);
580 : }
581 : }
582 : }
583 10992 : }
584 :
585 10992 : void LyricsLayout::setDefaultPositions(staff_idx_t staffIdx, const LyricsVersesMap& lyricsVersesAbove,
586 : const LyricsVersesMap& lyricsVersesBelow,
587 : LayoutContext& ctx)
588 : {
589 10992 : double staffHeight = ctx.dom().staff(staffIdx)->staffHeight();
590 10992 : double lyricsLineHeightFactor = ctx.conf().styleD(Sid::lyricsLineHeight);
591 :
592 10992 : int maxVerseAbove = !lyricsVersesAbove.empty() ? lyricsVersesAbove.crbegin()->first : 0;
593 11000 : for (auto& pair : lyricsVersesAbove) {
594 8 : int verse = pair.first;
595 8 : const LyricsVerse& lyricsVerse = pair.second;
596 24 : for (Lyrics* lyrics : lyricsVerse.lyrics()) {
597 16 : double y = -(maxVerseAbove - verse) * lyrics->lineSpacing() * lyricsLineHeightFactor;
598 16 : PointF defaultPos = lyrics->defaultPos();
599 16 : y += defaultPos.y();
600 16 : lyrics->setYRelativeToStaff(y);
601 : }
602 12 : for (LyricsLineSegment* lyricsLineSegment : lyricsVerse.lines()) {
603 4 : double y = -(maxVerseAbove - verse) * lyricsLineSegment->lineSpacing() * lyricsLineHeightFactor;
604 4 : lyricsLineSegment->move(PointF(0.0, y + lyricsLineSegment->baseLineShift()));
605 : }
606 : }
607 :
608 11516 : for (auto& pair : lyricsVersesBelow) {
609 524 : int verse = pair.first;
610 524 : const LyricsVerse& lyricsVerse = pair.second;
611 2688 : for (Lyrics* lyrics : lyricsVerse.lyrics()) {
612 2164 : double y = staffHeight + verse * lyrics->lineSpacing() * lyricsLineHeightFactor;
613 2164 : PointF defaultPos = lyrics->defaultPos();
614 2164 : y += defaultPos.y();
615 2164 : lyrics->setYRelativeToStaff(y);
616 : }
617 865 : for (LyricsLineSegment* lyricsLineSegment : lyricsVerse.lines()) {
618 341 : double y = staffHeight + verse * lyricsLineSegment->lineSpacing() * lyricsLineHeightFactor;
619 341 : lyricsLineSegment->move(PointF(0.0, y + lyricsLineSegment->baseLineShift()));
620 : }
621 : }
622 10992 : }
623 :
624 10992 : void LyricsLayout::checkCollisionsWithStaffElements(System* system, staff_idx_t staffIdx, LayoutContext& ctx,
625 : const LyricsVersesMap& lyricsVersesAbove,
626 : const LyricsVersesMap& lyricsVersesBelow)
627 : {
628 10992 : SysStaff* systemStaff = system->staff(staffIdx);
629 :
630 10992 : double lyricsMinDist = ctx.conf().styleAbsolute(Sid::lyricsMinTopDistance);
631 :
632 10992 : SkylineLine& staffSkylineNorth = systemStaff->skyline().north();
633 10992 : SkylineLine& staffSkylineSouth = systemStaff->skyline().south();
634 :
635 10992 : int maxVerseAbove = !lyricsVersesAbove.empty() ? lyricsVersesAbove.crbegin()->first : 0;
636 10992 : int maxVerseBelow = !lyricsVersesBelow.empty() ? lyricsVersesBelow.crbegin()->first : 0;
637 :
638 21984 : for (int verse = maxVerseAbove; verse >= 0; --verse) {
639 10992 : if (lyricsVersesAbove.count(verse) == 0) {
640 10984 : continue;
641 : }
642 8 : SkylineLine verseSkyline = createSkylineForVerse(verse, false, lyricsVersesAbove, system);
643 8 : double minDistance = -verseSkyline.minDistance(staffSkylineNorth);
644 8 : if (minDistance < lyricsMinDist) {
645 6 : double diff = lyricsMinDist - minDistance;
646 6 : moveThisVerseAndOuterOnes(verse, 0, true, -diff, lyricsVersesAbove);
647 : }
648 8 : }
649 :
650 22046 : for (int verse = 0; verse <= maxVerseBelow; ++verse) {
651 11054 : if (lyricsVersesBelow.count(verse) == 0) {
652 10530 : continue;
653 : }
654 524 : SkylineLine verseSkyline = createSkylineForVerse(verse, true, lyricsVersesBelow, system);
655 524 : double minDistance = -staffSkylineSouth.minDistance(verseSkyline);
656 524 : if (minDistance < lyricsMinDist) {
657 384 : double diff = lyricsMinDist - minDistance;
658 384 : moveThisVerseAndOuterOnes(verse, maxVerseBelow, false, diff, lyricsVersesBelow);
659 : }
660 524 : }
661 10992 : }
662 :
663 532 : SkylineLine LyricsLayout::createSkylineForVerse(int verse, bool north, const LyricsVersesMap& lyricsVerses, System* system)
664 : {
665 532 : double systemX = system->pageX();
666 :
667 532 : SkylineLine lyricsSkyline(north);
668 :
669 532 : if (lyricsVerses.count(verse) > 0) {
670 532 : const LyricsVerse& lyricsVerse = lyricsVerses.at(verse);
671 2712 : for (Lyrics* lyrics : lyricsVerse.lyrics()) {
672 2180 : if (lyrics->addToSkyline()) {
673 2165 : Shape lyricsShape = lyrics->highResShape().translated(PointF(lyrics->pageX() - systemX, lyrics->yRelativeToStaff()));
674 2165 : lyricsSkyline.add(lyricsShape);
675 2165 : }
676 : }
677 877 : for (LyricsLineSegment* lyricsLineSeg : lyricsVerse.lines()) {
678 345 : if (lyricsLineSeg->lyricsAddToSkyline()) {
679 345 : lyricsSkyline.add(lyricsLineSeg->shape().translate(lyricsLineSeg->pos()));
680 : }
681 : }
682 : }
683 :
684 532 : return lyricsSkyline;
685 0 : }
686 :
687 390 : void LyricsLayout::moveThisVerseAndOuterOnes(int verse, int lastVerse, bool above, double diff, const LyricsVersesMap& lyricsVerses)
688 : {
689 424 : auto moveVerse = [&](int verse) {
690 424 : if (lyricsVerses.count(verse) > 0) {
691 424 : const LyricsVerse& lyricsVerse = lyricsVerses.at(verse);
692 2341 : for (Lyrics* lyrics : lyricsVerse.lyrics()) {
693 1917 : lyrics->move(PointF(0.0, diff));
694 : }
695 699 : for (LyricsLineSegment* lyricsLineSeg : lyricsVerse.lines()) {
696 275 : lyricsLineSeg->move(PointF(0.0, diff));
697 : }
698 : }
699 424 : };
700 :
701 390 : if (above) {
702 12 : for (int otherVerse = verse; otherVerse >= lastVerse; --otherVerse) {
703 6 : moveVerse(otherVerse);
704 : }
705 : } else {
706 802 : for (int otherVerse = verse; otherVerse <= lastVerse; ++otherVerse) {
707 418 : moveVerse(otherVerse);
708 : }
709 : }
710 390 : }
711 :
712 10992 : void LyricsLayout::addToSkyline(System* system, staff_idx_t staffIdx, LayoutContext& ctx, const LyricsVersesMap& lyricsVersesAbove,
713 : const LyricsVersesMap& lyricsVersesBelow)
714 : {
715 10992 : double systemX = system->pageX();
716 : // HACK: subtract minVerticalDistance here because it's added later during staff distance calculations. Needs a better solution.
717 10992 : double lyricsVerticalPadding = ctx.conf().styleAbsolute(Sid::lyricsMinBottomDistance) - ctx.conf().styleAbsolute(
718 10992 : Sid::minVerticalDistance);
719 10992 : Skyline& skyline = system->staff(staffIdx)->skyline();
720 11000 : for (auto& pair : lyricsVersesAbove) {
721 8 : const LyricsVerse& lyricsVerse = pair.second;
722 24 : for (Lyrics* lyrics : lyricsVerse.lyrics()) {
723 16 : if (lyrics->addToSkyline()) {
724 : Shape lyricsShape
725 16 : = lyrics->highResShape().translated(PointF(lyrics->pageX() - systemX, lyrics->yRelativeToStaff()));
726 16 : skyline.north().add(lyricsShape.adjust(0.0, -lyricsVerticalPadding, 0.0, 0.0));
727 16 : }
728 : }
729 12 : for (LyricsLineSegment* lyricsLineSeg : lyricsVerse.lines()) {
730 4 : if (lyricsLineSeg->lyricsAddToSkyline()) {
731 4 : Shape lineShape = lyricsLineSeg->shape().translate(lyricsLineSeg->pos());
732 4 : skyline.north().add(lineShape.adjust(0.0, -lyricsVerticalPadding, 0.0, 0.0));
733 4 : }
734 : }
735 : }
736 11516 : for (auto& pair : lyricsVersesBelow) {
737 524 : const LyricsVerse& lyricsVerse = pair.second;
738 2688 : for (Lyrics* lyrics : lyricsVerse.lyrics()) {
739 2164 : if (lyrics->addToSkyline()) {
740 : Shape lyricsShape
741 2149 : = lyrics->highResShape().translated(PointF(lyrics->pageX() - systemX, lyrics->yRelativeToStaff()));
742 2149 : skyline.south().add(lyricsShape.adjust(0.0, 0.0, 0.0, lyricsVerticalPadding));
743 2149 : }
744 : }
745 865 : for (LyricsLineSegment* lyricsLineSeg : lyricsVerse.lines()) {
746 341 : if (lyricsLineSeg->lyricsAddToSkyline()) {
747 341 : Shape lineShape = lyricsLineSeg->shape().translate(lyricsLineSeg->pos());
748 341 : skyline.south().add(lineShape.adjust(0.0, 0.0, 0.0, lyricsVerticalPadding));
749 341 : }
750 : }
751 : }
752 10992 : }
753 :
754 348 : double LyricsLayout::lyricsLineStartX(const LyricsLineSegment* item)
755 : {
756 348 : const System* system = item->system();
757 348 : const LyricsLine* lyricsLine = item->lyricsLine();
758 348 : const Lyrics* startLyrics = lyricsLine->lyrics();
759 348 : const MStyle& style = item->style();
760 348 : const bool melisma = lyricsLine->isEndMelisma();
761 :
762 348 : const LyricsDashSystemStart lyricsDashSystemStart = style.styleV(Sid::lyricsDashPosAtStartOfSystem).value<LyricsDashSystemStart>();
763 348 : const bool leading = melisma || (lyricsDashSystemStart != LyricsDashSystemStart::UNDER_FIRST_NOTE);
764 :
765 : // Full melisma or dashes at beginning of system
766 348 : if (!item->isSingleBeginType()) {
767 14 : return system->firstNoteRestSegmentX(leading);
768 : }
769 :
770 : // Partial melisma or dashes
771 334 : if (lyricsLine->isPartialLyricsLine()) {
772 6 : const Measure* measure = lyricsLine->findStartMeasure();
773 6 : return measure->firstNoteRestSegmentX(leading);
774 : }
775 :
776 : // Full melisma or dashes in middle of system
777 328 : const double pad = melisma ? style.styleAbsolute(Sid::lyricsMelismaPad) : style.styleAbsolute(Sid::lyricsDashPad);
778 328 : const double lyricsRightEdge = startLyrics->pageX() - system->pageX() + startLyrics->shape().right();
779 328 : return lyricsRightEdge + pad;
780 : }
781 :
782 348 : double LyricsLayout::lyricsLineEndX(const LyricsLineSegment* item, const Lyrics* endLyrics)
783 : {
784 348 : const System* system = item->system();
785 348 : const LyricsLine* lyricsLine = item->lyricsLine();
786 348 : const ChordRest* endChordRest = toChordRest(lyricsLine->endElement());
787 348 : if (!endChordRest) {
788 0 : return system->endingXForOpenEndedLines();
789 : }
790 348 : const double systemPageX = system->pageX();
791 348 : const MStyle& style = item->style();
792 348 : const bool melisma = lyricsLine->isEndMelisma();
793 348 : const bool hasFollowingJump = endChordRest->hasFollowingJumpItem();
794 348 : const bool hasPrecedingJump = endChordRest->hasPrecedingJumpItem();
795 :
796 348 : if (!melisma && !item->isPartialLyricsLineSegment() && !endLyrics && !hasFollowingJump) {
797 0 : return 0.0;
798 : }
799 :
800 348 : const bool dashesEndOfSystem = !melisma && lyricsLine->tick2() == system->endTick();
801 :
802 : // Full melisma or dashes at end of system
803 348 : if (!item->isSingleEndType() || dashesEndOfSystem || !lyricsLine->endElement()) {
804 14 : return system->endingXForOpenEndedLines();
805 : }
806 :
807 : // Partial dashes after a repeat
808 334 : if (!melisma && hasPrecedingJump && item->isPartialLyricsLineSegment()) {
809 0 : if (endLyrics) {
810 0 : const double lyricsLeftEdge = endLyrics->pageX() - systemPageX + endLyrics->ldata()->bbox().left();
811 0 : return lyricsLeftEdge - style.styleAbsolute(Sid::lyricsDashPad);
812 0 : } else if (endChordRest->isChord()) {
813 0 : const Chord* endChord = toChord(endChordRest);
814 0 : const Note* note = endChord->up() ? endChord->downNote() : endChord->upNote();
815 0 : return note->pageX() - systemPageX + note->headWidth();
816 : }
817 : }
818 :
819 : // Full dashes or melisma before a repeat - possibly with a partial dash/repeat on the other side of the repeat
820 334 : if (endChordRest && hasFollowingJump && !lyricsLine->nextLyrics()) {
821 3 : Measure* endMeasure = lyricsLine->findEndMeasure();
822 : // check if there is a partial lyrics line in a following measure
823 3 : if (repeatHasPartialLyricLine(endMeasure) || endChordRest == lyricsLine->startElement()) {
824 3 : return endMeasure->endingXForOpenEndedLines();
825 : }
826 : }
827 :
828 : // Full or partial dashes in middle of system
829 331 : if (!melisma && endLyrics) {
830 231 : const double lyricsLeftEdge = endLyrics->pageX() - systemPageX + endLyrics->ldata()->bbox().left();
831 231 : return lyricsLeftEdge - style.styleAbsolute(Sid::lyricsDashPad);
832 : }
833 :
834 : // Full or partial melisma in middle of system
835 100 : if (endChordRest->isChord()) {
836 100 : const Chord* endChord = toChord(endChordRest);
837 100 : const Note* note = endChord->up() ? endChord->downNote() : endChord->upNote();
838 100 : return note->pageX() - systemPageX + note->headWidth();
839 : }
840 :
841 0 : return endChordRest->pageX() - systemPageX + endChordRest->rightEdge();
842 : }
843 :
844 348 : void LyricsLayout::adjustLyricsLineYOffset(LyricsLineSegment* item, const Lyrics* endLyrics)
845 : {
846 348 : const LyricsLine* lyricsLine = item->lyricsLine();
847 348 : ChordRest* endChordRest = lyricsLine->endElement()
848 348 : && lyricsLine->endElement()->isChordRest() ? toChordRest(lyricsLine->endElement()) : nullptr;
849 348 : const Lyrics* startLyrics = lyricsLine->lyrics();
850 348 : const bool melisma = lyricsLine->isEndMelisma();
851 :
852 348 : LyricsLineSegment::LayoutData* ldata = item->mutldata();
853 :
854 : // Partial melisma or dashes
855 348 : if (lyricsLine->isPartialLyricsLine()) {
856 : // A partial lyrics line's placement is independent of the lyrics around it, so only
857 : // take the Y from a lyric which is on the same side of the staff:
858 6 : Lyrics* nextLyrics = findNextLyrics(endChordRest, item->verse(), lyricsLine->placement());
859 6 : if (nextLyrics) {
860 3 : PointF nextLyricsDefaultPos = nextLyrics->defaultPos();
861 3 : ldata->setPosY(nextLyrics->offset().y() + nextLyricsDefaultPos.y());
862 : } else {
863 6 : PointF lyricsOffset = item->styleValue(Pid::OFFSET,
864 6 : item->placeBelow() ? Sid::lyricsPosBelow : Sid::lyricsPosAbove).value<PointF>();
865 3 : ldata->setPosY(lyricsOffset.y());
866 : }
867 348 : return;
868 : }
869 :
870 342 : if (item->isSingleBeginType()) {
871 328 : PointF startLyricsDefaultPos = startLyrics->defaultPos();
872 328 : ldata->setPosY(startLyrics->offset().y() + startLyricsDefaultPos.y());
873 328 : return;
874 : }
875 :
876 14 : if (melisma || !endLyrics) {
877 14 : const Lyrics* nextLyrics = findNextLyrics(endChordRest, item->verse());
878 :
879 14 : const Lyrics* refLyrics = nextLyrics ? nextLyrics : startLyrics;
880 14 : PointF refLyricsDefaultPos = refLyrics->defaultPos();
881 :
882 14 : ldata->setPosY(refLyrics->offset().y() + refLyricsDefaultPos.y());
883 14 : return;
884 : }
885 :
886 0 : PointF endLyricsDefaultPos = endLyrics->defaultPos();
887 :
888 0 : ldata->setPosY(endLyrics->offset().y() + endLyricsDefaultPos.y());
889 : }
|