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 "tupletlayout.h"
23 :
24 : #include "dom/chordrest.h"
25 : #include "dom/tuplet.h"
26 : #include "dom/text.h"
27 : #include "dom/factory.h"
28 : #include "dom/stafftype.h"
29 : #include "dom/chord.h"
30 : #include "dom/note.h"
31 : #include "dom/stem.h"
32 : #include "dom/system.h"
33 : #include "dom/measure.h"
34 :
35 : #include "tlayout.h"
36 : #include "autoplace.h"
37 :
38 : using namespace mu::engraving;
39 : using namespace mu::engraving::rendering::score;
40 :
41 2033 : void TupletLayout::layout(Tuplet* item, LayoutContext& ctx)
42 : {
43 2033 : Tuplet::LayoutData* ldata = item->mutldata();
44 2033 : if (item->elements().empty()) {
45 0 : LOGD("Tuplet::layout(): tuplet is empty");
46 4 : return;
47 : }
48 :
49 2033 : const StaffType* stt = item->staffType();
50 2033 : if (stt && stt->isTabStaff() && stt->stemless()) {
51 : // is in a TAB without stems, skip any format: tuplets are not shown
52 4 : return;
53 : }
54 :
55 2029 : createNumber(item, ctx);
56 :
57 2029 : computeDirection(item);
58 :
59 2029 : const ChordRest* cr1 = nullptr;
60 2029 : const ChordRest* cr2 = nullptr;
61 2029 : computeStartEndCR(item, &cr1, &cr2);
62 :
63 2029 : bool hasBracket = item->calcHasBracket(cr1, cr2);
64 2029 : item->setHasBracket(hasBracket);
65 :
66 2029 : double mag = (cr1->mag() + cr2->mag()) / 2;
67 2029 : ldata->setMag(mag);
68 :
69 2029 : item->setPos(0.0, 0.0);
70 :
71 2029 : layoutBracket(item, cr1, cr2, ctx);
72 :
73 : // collect bounding box
74 2029 : RectF r;
75 2029 : if (item->number()) {
76 1712 : r |= item->number()->ldata()->bbox().translated(item->number()->pos());
77 1712 : if (item->hasBracket()) {
78 1032 : RectF b;
79 1032 : b.setCoords(item->bracketL[1].x(), item->bracketL[1].y(), item->bracketR[2].x(), item->bracketR[2].y());
80 1032 : r |= b;
81 : }
82 317 : } else if (item->hasBracket()) {
83 1 : RectF b;
84 1 : b.setCoords(item->bracketL[1].x(), item->bracketL[1].y(), item->bracketL[3].x(), item->bracketL[3].y());
85 1 : r |= b;
86 : }
87 2029 : item->setbbox(r);
88 :
89 2029 : if (!item->cross()) {
90 1947 : Autoplace::autoplaceMeasureElement(item, item->mutldata(), item->isUp(), /* add to skyline */ true);
91 : }
92 : }
93 :
94 1992 : void TupletLayout::layoutTupletAndNestedTuplets(Tuplet* t, LayoutContext& ctx)
95 : {
96 1992 : const std::vector<DurationElement*> elements = t->elements();
97 9087 : for (auto revIter = elements.rbegin(); revIter != elements.rend(); ++revIter) {
98 7095 : DurationElement* d = *revIter;
99 7095 : if (d == t) {
100 0 : continue;
101 : }
102 : // if element is tuplet, layoutTuplet(that tuplet)
103 7095 : if (d->isTuplet()) {
104 458 : layoutTupletAndNestedTuplets(toTuplet(d), ctx);
105 : }
106 : }
107 : // layout t
108 1992 : layout(t, ctx);
109 1992 : }
110 :
111 0 : bool TupletLayout::isTopTuplet(ChordRest* cr)
112 : {
113 0 : Tuplet* t = cr->tuplet();
114 0 : if (t && t->elements().front() == cr) {
115 : // find top level tuplet
116 0 : while (t->tuplet()) {
117 0 : t = t->tuplet();
118 : }
119 : // consider tuplet cross if anything moved within it
120 0 : if (t->cross()) {
121 0 : return false;
122 : } else {
123 0 : return true;
124 : }
125 : }
126 :
127 : // no tuplet or not first element
128 0 : return false;
129 : }
130 :
131 92140 : bool TupletLayout::notTopTuplet(ChordRest* cr)
132 : {
133 92140 : Tuplet* t = cr->tuplet();
134 92140 : if (t && t->elements().front() == cr) {
135 : // find top level tuplet
136 2568 : while (t->tuplet()) {
137 625 : t = t->tuplet();
138 : }
139 : // consider tuplet cross if anything moved within it
140 1943 : if (t->cross()) {
141 41 : return true;
142 : } else {
143 1902 : return false;
144 : }
145 : }
146 :
147 : // no tuplet or not first element
148 90197 : return false;
149 : }
150 :
151 2029 : void TupletLayout::createNumber(Tuplet* item, LayoutContext& ctx)
152 : {
153 2029 : if (item->numberType() == TupletNumberType::NO_TEXT) {
154 317 : if (item->number()) {
155 0 : if (item->number()->selected()) {
156 0 : ctx.deselect(item->number());
157 : }
158 0 : delete item->number();
159 0 : item->setNumber(nullptr);
160 : }
161 :
162 317 : return;
163 : }
164 :
165 1712 : if (!item->number()) {
166 381 : Text* number = Factory::createText(item, TextStyleType::TUPLET);
167 381 : number->setComposition(true);
168 381 : number->setTrack(item->track());
169 381 : number->setVisible(item->visible());
170 381 : number->setColor(item->color());
171 381 : item->setNumber(number);
172 381 : item->resetNumberProperty();
173 : }
174 :
175 : // tuplet properties are propagated to number automatically by setProperty()
176 : // but we need to make sure flags are as well
177 1712 : item->number()->setPropertyFlags(Pid::FONT_FACE, item->propertyFlags(Pid::FONT_FACE));
178 1712 : item->number()->setPropertyFlags(Pid::FONT_SIZE, item->propertyFlags(Pid::FONT_SIZE));
179 1712 : item->number()->setPropertyFlags(Pid::FONT_STYLE, item->propertyFlags(Pid::FONT_STYLE));
180 1712 : item->number()->setPropertyFlags(Pid::ALIGN, item->propertyFlags(Pid::ALIGN));
181 1712 : item->number()->setPropertyFlags(Pid::POSITION, item->propertyFlags(Pid::POSITION));
182 :
183 1712 : const MStyle& style = ctx.conf().style();
184 :
185 1712 : String numberString = (item->numberType() == TupletNumberType::SHOW_NUMBER)
186 3374 : ? String(u"%1").arg(item->ratio().numerator())
187 3399 : : String(u"%1:%2").arg(item->ratio().numerator(), item->ratio().denominator());
188 1712 : if (style.styleB(Sid::tupletUseSymbols)) {
189 0 : String smuflNum;
190 0 : for (size_t i = 0; i < numberString.size(); ++i) {
191 0 : smuflNum.append(u"<sym>tuplet");
192 0 : smuflNum.append(numberString.at(i).unicode());
193 0 : smuflNum.append(u"</sym>");
194 : }
195 0 : smuflNum.replace(String(u":"), String(u"Colon"));
196 0 : item->number()->setXmlText(smuflNum);
197 0 : } else {
198 1712 : item->number()->setXmlText(numberString);
199 : }
200 :
201 1712 : item->setIsSmall(true);
202 1746 : for (const DurationElement* e : item->elements()) {
203 1736 : if ((e->isChordRest() && !toChordRest(e)->isSmall()) || (e->isTuplet() && !toTuplet(e)->isSmall())) {
204 1702 : item->setIsSmall(false);
205 1702 : break;
206 : }
207 : }
208 :
209 1712 : item->number()->mutldata()->setMag(item->isSmall() ? style.styleD(Sid::smallNoteMag) : 1.0);
210 1712 : }
211 :
212 2029 : void TupletLayout::computeDirection(Tuplet* item)
213 : {
214 2029 : if (item->direction() != DirectionV::AUTO) {
215 556 : item->setIsUp(item->direction() == DirectionV::UP);
216 556 : return;
217 : }
218 :
219 1473 : int up = 0;
220 7009 : for (const DurationElement* e : item->elements()) {
221 5536 : if (e->isChord()) {
222 4246 : const Chord* c = toChord(e);
223 4246 : if (c->stemDirection() != DirectionV::AUTO) {
224 150 : up += c->stemDirection() == DirectionV::UP ? 1000 : -1000;
225 : } else {
226 4096 : up += c->up() ? 1 : -1;
227 : }
228 : }
229 : }
230 1473 : if (up == 0) {
231 : // this is a tuplet full of rests, default to up but also take voices into consideration
232 217 : Measure* m = item->measure();
233 217 : if (m && m->hasVoices(item->staffIdx())) {
234 1 : up = item->voice() % 2 == 0 ? 1 : -1;
235 : } else {
236 216 : up = 1; // default up
237 : }
238 : }
239 :
240 1473 : item->setIsUp(up > 0);
241 : }
242 :
243 2029 : void TupletLayout::computeStartEndCR(Tuplet* item, const ChordRest** cr1, const ChordRest** cr2)
244 : {
245 2029 : const DurationElement* startEl = item->elements().front();
246 2131 : while (startEl->isTuplet()) {
247 102 : const Tuplet* t = toTuplet(startEl);
248 102 : if (t->elements().empty()) {
249 0 : break;
250 : }
251 102 : startEl = t->elements().front();
252 : }
253 2029 : assert(startEl && startEl->isChordRest());
254 :
255 2029 : const DurationElement* endEl = item->elements().back();
256 2072 : while (endEl->isTuplet()) {
257 43 : const Tuplet* t = toTuplet(endEl);
258 43 : if (t->elements().empty()) {
259 0 : break;
260 : }
261 43 : endEl = t->elements().back();
262 : }
263 2029 : assert(endEl && endEl->isChordRest());
264 :
265 2029 : *cr1 = toChordRest(startEl);
266 2029 : *cr2 = toChordRest(endEl);
267 2029 : }
268 :
269 2029 : void TupletLayout::layoutBracket(Tuplet* item, const ChordRest* cr1, const ChordRest* cr2, LayoutContext& ctx)
270 : {
271 2029 : const MStyle& style = ctx.conf().style();
272 2029 : double spatium = item->spatium();
273 :
274 2029 : double maxSlope = style.styleD(Sid::tupletMaxSlope);
275 2029 : bool outOfStaff = style.styleB(Sid::tupletOutOfStaff);
276 2029 : double vHeadDistance = style.styleAbsolute(Sid::tupletVHeadDistance) * item->mag();
277 2029 : double vStemDistance = style.styleAbsolute(Sid::tupletVStemDistance) * item->mag();
278 2029 : double stemLeft = (style.styleAbsolute(Sid::tupletStemLeftDistance) - style.styleAbsolute(Sid::tupletBracketWidth) / 2)
279 2029 : * cr1->mag();
280 2029 : double stemRight = (style.styleAbsolute(Sid::tupletStemRightDistance) - style.styleAbsolute(Sid::tupletBracketWidth) / 2)
281 2029 : * cr2->mag();
282 2029 : double noteLeft = (style.styleAbsolute(Sid::tupletNoteLeftDistance) - style.styleAbsolute(Sid::tupletBracketWidth) / 2)
283 2029 : * cr1->mag();
284 2029 : double noteRight = (style.styleAbsolute(Sid::tupletNoteRightDistance) - style.styleAbsolute(Sid::tupletBracketWidth) / 2)
285 2029 : * cr2->mag();
286 :
287 2029 : int move = 0;
288 2029 : if (outOfStaff && cr1->isChordRest() && cr2->isChordRest()) {
289 : // account for staff move when adjusting bracket to avoid staff
290 : // but don't attempt adjustment unless both endpoints are in same staff
291 2006 : if (toChordRest(cr1)->staffMove() == toChordRest(cr2)->staffMove()) {
292 2000 : move = toChordRest(cr1)->staffMove();
293 : } else {
294 6 : outOfStaff = false;
295 : }
296 : }
297 :
298 2029 : double l1 = style.styleAbsolute(Sid::tupletBracketHookHeight) * item->mag();
299 2029 : double l2l = vHeadDistance; // left bracket vertical distance
300 2029 : double l2r = vHeadDistance; // right bracket vertical distance
301 :
302 2029 : if (item->isUp()) {
303 1195 : vHeadDistance = -vHeadDistance;
304 : }
305 :
306 2029 : item->setP1(cr1->pagePos());
307 2029 : item->setP2(cr2->pagePos());
308 :
309 2029 : item->p1().rx() -= noteLeft;
310 2029 : item->p2().rx() += ctx.conf().noteHeadWidth() + noteRight;
311 2029 : item->p1().ry() += vHeadDistance; // TODO: Direction ?
312 2029 : item->p2().ry() += vHeadDistance;
313 :
314 2029 : double xx1 = item->p1().x(); // use to center the number on the beam
315 :
316 2029 : double leftNoteEdge = 0.0; // page coordinates
317 2029 : double rightNoteEdge = 0.0;
318 2029 : if (cr1->isChord()) {
319 1624 : const Chord* chord1 = toChord(cr1);
320 1624 : leftNoteEdge = chord1->up() ? chord1->downNote()->pageBoundingRect().left() : chord1->upNote()->pageBoundingRect().left();
321 : }
322 2029 : if (cr2->isChord()) {
323 1561 : const Chord* chord2 = toChord(cr2);
324 1561 : rightNoteEdge = chord2->up() ? chord2->downNote()->pageBoundingRect().right() : chord2->upNote()->pageBoundingRect().right();
325 : }
326 :
327 2029 : if (item->isUp()) {
328 1195 : if (cr1->isChord()) {
329 916 : const Chord* chord1 = toChord(cr1);
330 916 : Stem* stem = chord1->stem();
331 916 : if (stem) {
332 914 : xx1 = stem->pageBoundingRect().x();
333 : }
334 916 : if (chord1->up() && stem) {
335 884 : item->p1().ry() = stem->pageBoundingRect().y();
336 884 : l2l = vStemDistance;
337 884 : item->p1().rx() = stem->pageBoundingRect().left() - stemLeft;
338 : } else {
339 32 : item->p1().ry() = chord1->upNote()->pageBoundingRect().top();
340 32 : item->p1().rx() = leftNoteEdge - noteLeft;
341 : }
342 : }
343 :
344 1195 : if (cr2->isChord()) {
345 846 : const Chord* chord2 = toChord(cr2);
346 846 : Stem* stem = chord2->stem();
347 846 : if (stem && chord2->up()) {
348 816 : item->p2().ry() = stem->pageBoundingRect().top();
349 816 : l2r = vStemDistance;
350 816 : item->p2().rx() = stem->pageBoundingRect().right() + stemRight;
351 : } else {
352 30 : item->p2().ry() = chord2->upNote()->pageBoundingRect().top();
353 30 : item->p2().rx() = rightNoteEdge + noteRight;
354 : }
355 : }
356 : //
357 : // special case: one of the bracket endpoints is
358 : // a rest
359 : //
360 1195 : if (!cr1->isChord() && cr2->isChord()) {
361 81 : if (item->p2().y() < item->p1().y()) {
362 75 : item->p1().setY(item->p2().y());
363 : } else {
364 6 : item->p2().setY(item->p1().y());
365 : }
366 1114 : } else if (cr1->isChord() && !cr2->isChord()) {
367 151 : if (item->p1().y() < item->p2().y()) {
368 135 : item->p2().setY(item->p1().y());
369 : } else {
370 16 : item->p1().setY(item->p2().y());
371 : }
372 : }
373 :
374 : // outOfStaff
375 1195 : if (outOfStaff) {
376 1182 : double min = cr1->measure()->staffPageBoundingRect(cr1->staffIdx() + move).y();
377 1182 : if (min < item->p1().y()) {
378 396 : item->p1().ry() = min;
379 396 : l2l = vStemDistance;
380 : }
381 1182 : min = cr2->measure()->staffPageBoundingRect(cr2->staffIdx() + move).y();
382 1182 : if (min < item->p2().y()) {
383 384 : item->p2().ry() = min;
384 384 : l2r = vStemDistance;
385 : }
386 : }
387 :
388 : // check that slope is no more than max
389 1195 : double d = (item->p2().y() - item->p1().y()) / (item->p2().x() - item->p1().x());
390 1195 : if (d < -maxSlope) {
391 : // move p1 y up
392 0 : item->p1().ry() = item->p2().y() + maxSlope * (item->p2().x() - item->p1().x());
393 1195 : } else if (d > maxSlope) {
394 : // move p2 y up
395 2 : item->p2().ry() = item->p1().ry() + maxSlope * (item->p2().x() - item->p1().x());
396 : }
397 :
398 : // check for collisions
399 1195 : size_t n = item->elements().size();
400 1195 : if (n >= 3) {
401 1108 : d = (item->p2().y() - item->p1().y()) / (item->p2().x() - item->p1().x());
402 3322 : for (size_t i = 1; i < (n - 1); ++i) {
403 2214 : const EngravingItem* e = item->elements().at(i);
404 2214 : if (e->isChord()) {
405 1480 : const Chord* chord = toChord(e);
406 1480 : const Stem* stem = chord->stem();
407 1480 : if (stem) {
408 1478 : RectF r(chord->up() ? stem->pageBoundingRect() : chord->upNote()->pageBoundingRect());
409 1478 : double y3 = r.top();
410 1478 : double x3 = r.x() + r.width() * .5;
411 1478 : double y0 = item->p1().y() + (x3 - item->p1().x()) * d;
412 1478 : double c = y0 - y3;
413 1478 : if (c > 0) {
414 270 : item->p1().ry() -= c;
415 270 : item->p2().ry() -= c;
416 : }
417 : }
418 : }
419 : }
420 : }
421 : } else {
422 834 : if (cr1->isChord()) {
423 708 : const Chord* chord1 = toChord(cr1);
424 708 : Stem* stem = chord1->stem();
425 708 : if (stem) {
426 708 : xx1 = stem->pageBoundingRect().x();
427 : }
428 708 : if (!chord1->up() && stem) {
429 698 : item->p1().ry() = stem->pageBoundingRect().bottom();
430 698 : l2l = vStemDistance;
431 698 : item->p1().rx() = stem->pageBoundingRect().left() - stemLeft;
432 : } else {
433 10 : item->p1().ry() = chord1->downNote()->pageBoundingRect().bottom();
434 10 : item->p1().rx() = leftNoteEdge - noteLeft;
435 : }
436 : }
437 :
438 834 : if (cr2->isChord()) {
439 715 : const Chord* chord2 = toChord(cr2);
440 715 : Stem* stem = chord2->stem();
441 715 : if (stem && !chord2->up()) {
442 711 : item->p2().ry() = stem->pageBoundingRect().bottom();
443 711 : l2r = vStemDistance;
444 711 : item->p2().rx() = stem->pageBoundingRect().right() + stemRight;
445 : } else {
446 4 : item->p2().ry() = chord2->downNote()->pageBoundingRect().bottom();
447 4 : item->p2().rx() = rightNoteEdge + noteRight;
448 : }
449 : }
450 : //
451 : // special case: one of the bracket endpoints is
452 : // a rest
453 : //
454 834 : if (!cr1->isChord() && cr2->isChord()) {
455 97 : if (item->p2().y() > item->p1().y()) {
456 97 : item->p1().setY(item->p2().y());
457 : } else {
458 0 : item->p2().setY(item->p1().y());
459 : }
460 737 : } else if (cr1->isChord() && !cr2->isChord()) {
461 90 : if (item->p1().y() > item->p2().y()) {
462 90 : item->p2().setY(item->p1().y());
463 : } else {
464 0 : item->p1().setY(item->p2().y());
465 : }
466 : }
467 : // outOfStaff
468 834 : if (outOfStaff) {
469 818 : double max = cr1->measure()->staffPageBoundingRect(cr1->staffIdx() + move).bottom();
470 818 : if (max > item->p1().y()) {
471 57 : item->p1().ry() = max;
472 57 : l2l = vStemDistance;
473 : }
474 818 : max = cr2->measure()->staffPageBoundingRect(cr2->staffIdx() + move).bottom();
475 818 : if (max > item->p2().y()) {
476 58 : item->p2().ry() = max;
477 58 : l2r = vStemDistance;
478 : }
479 : }
480 : // check that slope is no more than max
481 834 : double d = (item->p2().y() - item->p1().y()) / (item->p2().x() - item->p1().x());
482 834 : if (d < -maxSlope) {
483 : // move p1 y up
484 1 : item->p2().ry() = item->p1().y() - maxSlope * (item->p2().x() - item->p1().x());
485 833 : } else if (d > maxSlope) {
486 : // move p2 y up
487 1 : item->p1().ry() = item->p2().ry() - maxSlope * (item->p2().x() - item->p1().x());
488 : }
489 :
490 : // check for collisions
491 834 : size_t n = item->elements().size();
492 834 : if (n >= 3) {
493 744 : d = (item->p2().y() - item->p1().y()) / (item->p2().x() - item->p1().x());
494 1737 : for (size_t i = 1; i < (n - 1); ++i) {
495 993 : EngravingItem* e = item->elements()[i];
496 993 : if (e->isChord()) {
497 766 : const Chord* chord = toChord(e);
498 766 : const Stem* stem = chord->stem();
499 766 : if (stem) {
500 766 : RectF r(chord->up() ? chord->downNote()->pageBoundingRect() : stem->pageBoundingRect());
501 766 : double y3 = r.bottom();
502 766 : double x3 = r.x() + r.width() * .5;
503 766 : double y0 = item->p1().y() + (x3 - item->p1().x()) * d;
504 766 : double c = y0 - y3;
505 766 : if (c < 0) {
506 179 : item->p1().ry() -= c;
507 179 : item->p2().ry() -= c;
508 : }
509 : }
510 : }
511 : }
512 : }
513 : }
514 :
515 2029 : if (!cr1->isChord()) {
516 405 : item->p1().rx() = cr1->pageBoundingRect().left() - noteLeft;
517 : }
518 2029 : if (!cr2->isChord()) {
519 468 : Shape shape = cr2->ldata()->shape();
520 936 : auto shEl = shape.find_if([](const ShapeElement& i) { return i.item() && i.item()->isChordRest(); });
521 468 : DO_ASSERT(shEl);
522 468 : item->p2().rx() = shEl->translated(cr2->pagePos()).right() + noteRight;
523 468 : }
524 :
525 2029 : PointF mp(item->layoutParent()->pagePos());
526 2029 : if (item->layoutParent()->isMeasure()) {
527 2029 : System* s = toMeasure(item->layoutParent())->system();
528 2029 : if (s) {
529 2029 : SysStaff* tupletStaff = s->staff(item->vStaffIdx());
530 2029 : mp.ry() += tupletStaff ? tupletStaff->y() : 0.0;
531 : }
532 : }
533 2029 : item->p1() -= mp;
534 2029 : item->p2() -= mp;
535 :
536 2029 : item->p1() += item->userP1();
537 2029 : item->p2() += item->userP2();
538 2029 : xx1 -= mp.x();
539 :
540 2029 : item->p1().ry() -= l2l * (item->isUp() ? 1.0 : -1.0);
541 2029 : item->p2().ry() -= l2r * (item->isUp() ? 1.0 : -1.0);
542 :
543 2029 : const double yOffset = item->staffOffsetY();
544 :
545 2029 : item->p1().ry() -= yOffset;
546 2029 : item->p2().ry() -= yOffset;
547 :
548 2029 : if (style.styleB(Sid::tupletExtendToEndOfDuration)) {
549 0 : extendToEndOfDuration(item, toChordRest(cr2));
550 : }
551 :
552 : // l2l l2r, mp, _p1, _p2 const
553 :
554 : // center number
555 2029 : double xNumber = 0.0;
556 2029 : double numberWidth = 0.0;
557 2029 : if (item->number()) {
558 1712 : Text::LayoutData* numLdata = item->number()->mutldata();
559 1712 : TLayout::layoutText(item->number(), numLdata);
560 1712 : numberWidth = numLdata->bbox().width();
561 :
562 1712 : double yNumber = item->p1().y() + (item->p2().y() - item->p1().y()) * .5 - l1 * (item->isUp() ? 1.0 : -1.0);
563 :
564 1712 : if (placeNumberOnRhythmicCenter(item, cr1, cr2, ctx)) {
565 0 : xNumber = computeRhythmicCenter(item, cr2);
566 1712 : } else if (cr1->beam() && cr2->beam() && cr1->beam() == cr2->beam() && !item->hasBracket()) {
567 : // for beamed tuplets, center number on beam - if they don't have a bracket
568 634 : const ChordRest* crr = toChordRest(cr1);
569 634 : if (item->isUp() == crr->up()) {
570 634 : double deltax = cr2->pagePos().x() - cr1->pagePos().x();
571 634 : xNumber = xx1 + deltax * .5;
572 : } else {
573 0 : double deltax = item->p2().x() - item->p1().x();
574 0 : xNumber = item->p1().x() + deltax * .5;
575 : }
576 : } else {
577 : // otherwise center on the bracket (TODO: make centering rules customizable?)
578 1078 : double deltax = item->p2().x() - item->p1().x();
579 1078 : xNumber = item->p1().x() + deltax * .5;
580 : }
581 :
582 1712 : numLdata->setPos(PointF(xNumber, yNumber) - item->ldata()->pos());
583 : }
584 :
585 2029 : if (item->hasBracket()) {
586 1033 : double slope = (item->p2().y() - item->p1().y()) / (item->p2().x() - item->p1().x());
587 1033 : const double numberGap = 0.35 * spatium;
588 :
589 1033 : if (item->isUp()) {
590 652 : if (item->number()) {
591 : //set width of bracket hole
592 652 : double x = xNumber - numberWidth * .5 - numberGap;
593 652 : item->p1().rx() = std::min(item->p1().x(), x - 0.5 * l1); // ensure enough space for the number
594 652 : double y = item->p1().y() + (x - item->p1().x()) * slope;
595 652 : item->bracketL[0] = PointF(item->p1().x(), item->p1().y());
596 652 : item->bracketL[1] = PointF(item->p1().x(), item->p1().y() - l1);
597 652 : item->bracketL[2] = PointF(x, y - l1);
598 :
599 : //set width of bracket hole
600 652 : x = xNumber + numberWidth * .5 + numberGap;
601 652 : item->p2().rx() = std::max(item->p2().x(), x + 0.5 * l1); // ensure enough space for the number
602 652 : y = item->p1().y() + (x - item->p1().x()) * slope;
603 652 : item->bracketR[0] = PointF(x, y - l1);
604 652 : item->bracketR[1] = PointF(item->p2().x(), item->p2().y() - l1);
605 652 : item->bracketR[2] = PointF(item->p2().x(), item->p2().y());
606 : } else {
607 0 : item->bracketL[0] = PointF(item->p1().x(), item->p1().y());
608 0 : item->bracketL[1] = PointF(item->p1().x(), item->p1().y() - l1);
609 0 : item->bracketL[2] = PointF(item->p2().x(), item->p2().y() - l1);
610 0 : item->bracketL[3] = PointF(item->p2().x(), item->p2().y());
611 : }
612 : } else {
613 381 : if (item->number()) {
614 : //set width of bracket hole
615 380 : double x = xNumber - numberWidth * .5 - numberGap;
616 380 : item->p1().rx() = std::min(item->p1().x(), x - 0.5 * l1); // ensure enough space for the number
617 380 : double y = item->p1().y() + (x - item->p1().x()) * slope;
618 380 : item->bracketL[0] = PointF(item->p1().x(), item->p1().y());
619 380 : item->bracketL[1] = PointF(item->p1().x(), item->p1().y() + l1);
620 380 : item->bracketL[2] = PointF(x, y + l1);
621 :
622 : //set width of bracket hole
623 380 : x = xNumber + numberWidth * .5 + numberGap;
624 380 : item->p2().rx() = std::max(item->p2().x(), x + 0.5 * l1);
625 380 : y = item->p1().y() + (x - item->p1().x()) * slope;
626 380 : item->bracketR[0] = PointF(x, y + l1);
627 380 : item->bracketR[1] = PointF(item->p2().x(), item->p2().y() + l1);
628 380 : item->bracketR[2] = PointF(item->p2().x(), item->p2().y());
629 : } else {
630 1 : item->bracketL[0] = PointF(item->p1().x(), item->p1().y());
631 1 : item->bracketL[1] = PointF(item->p1().x(), item->p1().y() + l1);
632 1 : item->bracketL[2] = PointF(item->p2().x(), item->p2().y() + l1);
633 1 : item->bracketL[3] = PointF(item->p2().x(), item->p2().y());
634 : }
635 : }
636 : }
637 2029 : }
638 :
639 1712 : bool TupletLayout::placeNumberOnRhythmicCenter(Tuplet* item, const ChordRest* cr1, const ChordRest* cr2, LayoutContext& ctx)
640 : {
641 1712 : if (ctx.conf().styleB(Sid::tupletNumberRythmicCenter) && !isSymmetric(item, cr1, cr2)) {
642 0 : Fraction center = centerTick(item);
643 0 : if (cr2->tick() <= center) {
644 0 : return ctx.conf().styleB(Sid::tupletExtendToEndOfDuration);
645 : }
646 :
647 0 : return true;
648 : }
649 :
650 1712 : return false;
651 : }
652 :
653 0 : bool TupletLayout::isSymmetric(Tuplet* item, const ChordRest* cr1, const ChordRest* cr2)
654 : {
655 0 : Fraction endTick = cr2->endTick();
656 :
657 0 : std::vector<Segment*> tupletSegments;
658 0 : tupletSegments.reserve(item->elements().size());
659 :
660 0 : for (Segment* segment = cr1->segment(); segment && segment->tick() < endTick; segment = segment->nextActive()) {
661 0 : tupletSegments.push_back(segment);
662 : }
663 :
664 0 : size_t segmentsCount = tupletSegments.size();
665 0 : for (size_t i = 0; i < segmentsCount; ++i) {
666 0 : size_t j = segmentsCount - 1 - i;
667 0 : if (j <= i) {
668 0 : break;
669 : }
670 0 : Segment* firstSeg = tupletSegments[i];
671 0 : Segment* secondSeg = tupletSegments[j];
672 0 : if (firstSeg->ticks() != secondSeg->ticks()) {
673 0 : return false;
674 : }
675 : }
676 :
677 0 : return true;
678 0 : }
679 :
680 0 : double TupletLayout::computeRhythmicCenter(Tuplet* item, const ChordRest* endChord)
681 : {
682 0 : Fraction center = centerTick(item);
683 :
684 0 : staff_idx_t track = item->track();
685 0 : const Segment* refSeg = nullptr;
686 0 : const ChordRest* refCR = nullptr;
687 0 : for (Segment* seg = endChord->segment(); seg; seg = seg->prev(SegmentType::ChordRest)) {
688 0 : refSeg = seg;
689 0 : refCR = toChordRest(seg->element(track));
690 0 : if (!refCR) {
691 0 : continue;
692 : }
693 0 : if (seg->tick() <= center) {
694 0 : break;
695 : }
696 : }
697 :
698 0 : IF_ASSERT_FAILED(refSeg && refCR) {
699 0 : return 0.0;
700 : }
701 :
702 0 : if (refSeg->tick() == center) {
703 0 : double xRef = 0.0;
704 0 : if (refCR->isChord()) {
705 0 : const Chord* chord = toChord(refCR);
706 0 : bool chordUp = chord->up();
707 0 : bool tupletPlaceAbove = item->isUp();
708 0 : xRef = 0.5 * chord->upNote()->headWidth();
709 0 : if (chordUp == tupletPlaceAbove) {
710 : // Stem-side correction
711 0 : xRef += (chordUp ? 0.25 : -0.25) * chord->upNote()->headWidth();
712 : }
713 : } else {
714 0 : xRef = 0.5 * refCR->width();
715 : }
716 0 : return refSeg->x() + xRef;
717 : }
718 :
719 0 : Fraction refCRTicks = refCR->actualTicks();
720 0 : Fraction tickDiff = center - refCR->tick();
721 0 : double tickRatio = (tickDiff / refCRTicks).toDouble();
722 :
723 0 : const Segment* nextSeg = item->measure()->findSegment(SegmentType::ChordRest, refSeg->tick() + refCRTicks);
724 0 : double xRef = refSeg->x() + (refCR->isChord() ? toChord(refCR)->upNote()->headWidth() : refCR->width());
725 0 : double refWidth = nextSeg ? nextSeg->x() - xRef : refSeg->width();
726 :
727 0 : double relativeWidth = refWidth * tickRatio;
728 :
729 0 : return xRef + relativeWidth;
730 : }
731 :
732 0 : Fraction TupletLayout::centerTick(Tuplet* item)
733 : {
734 0 : Fraction startTick = item->tick();
735 0 : Fraction baseLen = item->baseLen().ticks();
736 0 : Fraction ratio = item->ratio();
737 0 : Fraction subdivision = item->baseLen().ticks() / item->ratio();
738 0 : Fraction endTick = startTick + baseLen * ratio.denominator();
739 :
740 0 : Fraction centerTick = (startTick + endTick - subdivision) / 2;
741 :
742 0 : return centerTick;
743 : }
744 :
745 0 : void TupletLayout::extendToEndOfDuration(Tuplet* item, const ChordRest* endCR)
746 : {
747 0 : Fraction baseDuration = item->baseLen().ticks();
748 0 : if (endCR->ticks() <= baseDuration) {
749 0 : return;
750 : }
751 :
752 0 : Fraction lastTupletSubdivision = endCR->endTick() - baseDuration / item->ratio();
753 0 : Segment* refSegment = endCR->segment();
754 0 : while (refSegment) {
755 0 : Segment* nextCRSeg = refSegment->next1(SegmentType::ChordRest);
756 0 : if (!nextCRSeg || nextCRSeg->tick() > lastTupletSubdivision) {
757 0 : break;
758 : }
759 0 : refSegment = nextCRSeg;
760 : }
761 :
762 0 : Fraction tickRatio = (lastTupletSubdivision - refSegment->tick()) / refSegment->ticks();
763 :
764 0 : double xResult = refSegment->pagePos().x() + refSegment->width() * tickRatio.toDouble() + item->score()->noteHeadWidth();
765 :
766 0 : const double padding = 0.6 * item->spatium();
767 :
768 0 : Segment* nextSeg = refSegment->next1WithElemsOnStaff(endCR->vStaffIdx(), ~SegmentType::TimeTick);
769 0 : xResult = std::min(xResult, nextSeg->pagePos().x() - padding);
770 :
771 0 : track_idx_t startTrack = staff2track(endCR->vStaffIdx());
772 0 : track_idx_t endTrack = startTrack + VOICES;
773 0 : if (nextSeg->isChordRestType()) {
774 0 : for (track_idx_t track = startTrack; track < endTrack; ++track) {
775 0 : if (ChordRest* chordRest = toChordRest(nextSeg->element(track))) {
776 0 : if (Tuplet* nextTuplet = chordRest->tuplet()) {
777 0 : if (nextTuplet->elements().front() == chordRest && nextTuplet->isUp() == item->isUp()) {
778 0 : double xStartOfNextTuplet = nextTuplet->pagePos().x() + nextTuplet->p1().x();
779 0 : xResult = std::min(xResult, xStartOfNextTuplet - padding);
780 : }
781 : }
782 : }
783 : }
784 : }
785 :
786 0 : Shape nextSegShape = nextSeg->staffShape(endCR->vStaffIdx());
787 0 : nextSegShape.translate(PointF(nextSeg->pagePos().x(), nextSeg->system()->staff(endCR->vStaffIdx())->y()));
788 0 : double yAbove = item->p2().y() - (item->isUp() ? item->style().styleAbsolute(Sid::tupletBracketHookHeight) : 0.0);
789 0 : double yBelow = item->p2().y() + (item->isUp() ? 0.0 : item->style().styleAbsolute(Sid::tupletBracketHookHeight));
790 0 : double left = nextSegShape.leftMostEdgeAtHeight(yAbove, yBelow);
791 0 : xResult = std::min(xResult, left - padding);
792 :
793 0 : double curPos = item->p2().x();
794 0 : item->p2().rx() = std::max(curPos, xResult - item->measure()->pagePos().x());
795 0 : }
|