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 "tremololayout.h"
23 :
24 : #include "global/types/number.h"
25 :
26 : #include "dom/chord.h"
27 : #include "dom/stem.h"
28 : #include "dom/tremolotwochord.h"
29 : #include "dom/tremolosinglechord.h"
30 : #include "dom/note.h"
31 : #include "dom/staff.h"
32 : #include "dom/measure.h"
33 :
34 : #include "beamtremololayout.h"
35 : #include "chordlayout.h"
36 : #include "stemlayout.h"
37 :
38 : using namespace mu::engraving;
39 : using namespace mu::engraving::rendering::score;
40 :
41 395 : void TremoloLayout::layout(TremoloTwoChord* item, const LayoutContext& ctx)
42 : {
43 395 : IF_ASSERT_FAILED(item->ownershipParent()) {
44 0 : return;
45 : }
46 :
47 395 : item->setChord1(toChord(item->ownershipParent()));
48 :
49 395 : Note* anchor1 = item->chord1()->up() ? item->chord1()->upNote() : item->chord1()->downNote();
50 395 : Stem* stem = item->chord1()->stem();
51 : double x, y, h;
52 395 : if (stem) {
53 338 : x = stem->pos().x() + stem->width() / 2 * (item->chord1()->up() ? -1.0 : 1.0);
54 338 : y = stem->pos().y();
55 338 : h = stem->length();
56 : } else {
57 : // center tremolo above note
58 57 : x = anchor1->x() + anchor1->headWidth() * 0.5;
59 57 : y = anchor1->y();
60 57 : h = (ctx.conf().styleAbsolute(Sid::tremoloNoteSidePadding) + item->ldata()->bbox().height()) * item->chord1()->intrinsicMag();
61 : }
62 :
63 395 : layoutTwoNotesTremolo(item, ctx, x, y, h, item->spatium());
64 : }
65 :
66 412 : void TremoloLayout::layout(TremoloSingleChord* item, const LayoutContext& ctx)
67 : {
68 412 : IF_ASSERT_FAILED(item->ownershipParent()) {
69 0 : return;
70 : }
71 :
72 412 : item->computeShape(); // set bbox
73 412 : item->setPath(item->basePath());
74 :
75 412 : Note* anchor1 = item->chord()->up() ? item->chord()->upNote() : item->chord()->downNote();
76 412 : Stem* stem = item->chord()->stem();
77 : double x, y, h;
78 412 : if (stem) {
79 397 : x = stem->pos().x() + stem->width() / 2 * (item->chord()->up() ? -1.0 : 1.0);
80 397 : y = stem->pos().y();
81 397 : h = stem->length();
82 : } else {
83 : // center tremolo above note
84 15 : x = anchor1->x() + anchor1->headWidth() * 0.5;
85 :
86 15 : bool hasMirroredNote = false;
87 30 : for (Note* n : item->chord()->notes()) {
88 15 : if (n->ldata()->mirror.value()) {
89 0 : hasMirroredNote = true;
90 0 : break;
91 : }
92 : }
93 :
94 15 : if (hasMirroredNote) {
95 0 : x = StemLayout::StemLayout::stemPosX(item->chord());
96 : }
97 :
98 15 : y = anchor1->y();
99 15 : h = (ctx.conf().styleAbsolute(Sid::tremoloNoteSidePadding) + item->ldata()->bbox().height()) * item->chord()->intrinsicMag();
100 : }
101 :
102 412 : layoutOneNoteTremolo(item, ctx, x, y, h, item->spatium());
103 : }
104 :
105 : //---------------------------------------------------------
106 : // layoutOneNoteTremolo
107 : //---------------------------------------------------------
108 :
109 412 : void TremoloLayout::layoutOneNoteTremolo(TremoloSingleChord* item, const LayoutContext& ctx, double x, double y, double h, double spatium)
110 : {
111 412 : const StaffType* staffType = item->staffType();
112 :
113 412 : if (staffType && staffType->isTabStaff()) {
114 3 : x = ChordLayout::centerX(item->chord());
115 : }
116 :
117 412 : bool up = item->chord()->up();
118 412 : int upValue = up ? -1 : 1;
119 412 : double mag = item->chord()->intrinsicMag();
120 412 : spatium *= mag;
121 :
122 412 : double yOffset = h - ctx.conf().styleAbsolute(Sid::tremoloOutSidePadding) * mag;
123 :
124 412 : int beams = item->chord()->beams();
125 412 : if (item->chord()->hook()) {
126 : // allow for space at the hook side of the stem (yOffset)
127 : // straight flags and traditional flags have different requirements because of their slopes
128 : // away from the stem. Straight flags have a shallower slope and a lot more space in general
129 : // so we can place the trem higher in that case
130 2 : bool straightFlags = ctx.conf().styleB(Sid::useStraightNoteFlags);
131 2 : if (straightFlags) {
132 0 : yOffset -= 0.75 * spatium;
133 : } else {
134 : // up-hooks and down-hooks are shaped differently
135 2 : yOffset -= up ? 1.5 * spatium : 1.0 * spatium;
136 : }
137 : // we need an additional offset for beams > 2 since those beams extend outwards and we don't want to adjust for that
138 2 : double beamOffset = straightFlags ? 0.75 : 0.5;
139 2 : yOffset -= beams >= 2 ? beamOffset * spatium : 0.0;
140 410 : } else if (beams && staffType && !staffType->isSimpleTabStaff()) {
141 213 : yOffset -= (beams * (ctx.conf().styleB(Sid::useWideBeams) ? 1.0 : 0.75) - 0.25) * spatium;
142 : }
143 412 : yOffset -= item->isBuzzRoll() && up ? 0.5 * spatium : 0.0;
144 412 : yOffset -= up ? 0.0 : item->minHeight() * spatium / mag;
145 412 : yOffset *= upValue;
146 412 : y += yOffset;
147 :
148 412 : if (up) {
149 178 : double height = item->isBuzzRoll() ? 0 : item->minHeight();
150 178 : double staveHeight = (((item->staff()->lines(item->tick()) - 1) - height) * spatium / mag);
151 178 : y = std::min(y, staveHeight);
152 : } else {
153 234 : y = std::max(y, 0.0);
154 : }
155 412 : item->setPos(x, y);
156 412 : }
157 :
158 275 : void TremoloLayout::calcIsUp(TremoloTwoChord* item)
159 : {
160 275 : if (item->userModified()) {
161 0 : bool startChordUp = ChordLayout::isChordPosBelowTrem(item->chord1(), item);
162 0 : bool endChordUp = ChordLayout::isChordPosBelowTrem(item->chord2(), item);
163 :
164 0 : item->setUp(startChordUp || endChordUp);
165 0 : item->chord1()->setUp(startChordUp);
166 0 : item->chord2()->setUp(endChordUp);
167 0 : return;
168 : }
169 :
170 275 : int up = 0;
171 275 : bool isUp = item->up();
172 550 : bool hasVoices = item->chord1()->measure()->hasVoices(item->chord1()->staffIdx(), item->chord1()->tick(),
173 275 : item->chord2()->tick() - item->chord1()->tick());
174 275 : if (item->chord1()->stemDirection() == DirectionV::AUTO
175 209 : && item->chord2()->stemDirection() == DirectionV::AUTO
176 209 : && item->chord1()->staffMove() == item->chord2()->staffMove()
177 484 : && !hasVoices) {
178 209 : std::vector<int> chord1Distances = item->chord1()->noteDistances();
179 209 : std::vector<int> chord2Distances = item->chord2()->noteDistances();
180 209 : std::vector<int> noteDistances;
181 209 : noteDistances.reserve(chord1Distances.size() + chord2Distances.size());
182 424 : for (int distance : chord1Distances) {
183 215 : noteDistances.push_back(distance);
184 : }
185 424 : for (int distance : chord2Distances) {
186 215 : noteDistances.push_back(distance);
187 : }
188 209 : std::sort(noteDistances.begin(), noteDistances.end());
189 209 : up = ChordLayout::computeAutoStemDirection(noteDistances);
190 :
191 209 : isUp = up > 0;
192 275 : } else if (item->chord1()->stemDirection() != DirectionV::AUTO) {
193 66 : isUp = item->chord1()->stemDirection() == DirectionV::UP;
194 0 : } else if (item->chord2()->stemDirection() != DirectionV::AUTO) {
195 0 : isUp = item->chord2()->stemDirection() == DirectionV::UP;
196 0 : } else if (item->chord1()->staffMove() > 0 || item->chord2()->staffMove() > 0) {
197 0 : isUp = false;
198 0 : } else if (item->chord1()->staffMove() < 0 || item->chord2()->staffMove() < 0) {
199 0 : isUp = true;
200 0 : } else if (hasVoices) {
201 0 : isUp = item->chord1()->track() % 2 == 0;
202 : }
203 :
204 275 : item->setUp(isUp);
205 275 : item->chord1()->setUp(item->chord1()->staffMove() == 0 ? isUp : !isUp); // if on a different staff, flip stem dir
206 275 : item->chord2()->setUp(item->chord2()->staffMove() == 0 ? isUp : !isUp);
207 : }
208 :
209 : //---------------------------------------------------------
210 : // layoutTwoNotesTremolo
211 : //---------------------------------------------------------
212 :
213 395 : void TremoloLayout::layoutTwoNotesTremolo(TremoloTwoChord* item, const LayoutContext& ctx, double x, double y, double h, double spatium)
214 : {
215 : UNUSED(x);
216 : UNUSED(y);
217 : UNUSED(h);
218 : UNUSED(spatium);
219 :
220 395 : TremoloTwoChord::LayoutData* ldata = item->mutldata();
221 :
222 : // make sure both stems are in the same direction
223 395 : if (item->chord1()->beam() && item->chord1()->beam() == item->chord2()->beam()) {
224 120 : Beam* beam = item->chord1()->beam();
225 120 : item->setUp(beam->up());
226 120 : item->doSetDirection(beam->direction());
227 : // stem stuff is already taken care of by the beams
228 : } else {
229 275 : TremoloLayout::calcIsUp(item);
230 :
231 275 : if (!item->userModified()) {
232 : // user modified trems will be dealt with later
233 275 : ChordLayout::layoutStem(item->chord1(), ctx);
234 275 : ChordLayout::layoutStem(item->chord2(), ctx);
235 : }
236 : }
237 :
238 395 : BeamTremoloLayout::setupLData(item, item->mutldata(), ctx);
239 395 : item->setStartAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), item->chord1(), ChordBeamAnchorType::Start));
240 395 : item->setEndAnchor(BeamTremoloLayout::chordBeamAnchor(item->ldata(), item->chord2(), ChordBeamAnchorType::End));
241 :
242 : // deal with manual adjustments here and return
243 395 : if (item->userModified()) {
244 0 : int idx = item->directionIdx();
245 0 : double startY = item->beamFragment().py1[idx];
246 0 : double endY = item->beamFragment().py2[idx];
247 0 : if (ctx.conf().styleB(Sid::snapCustomBeamsToGrid)) {
248 0 : const double quarterSpace = item->EngravingItem::spatium() / 4;
249 0 : startY = round(startY / quarterSpace) * quarterSpace;
250 0 : endY = round(endY / quarterSpace) * quarterSpace;
251 : }
252 0 : startY += item->pagePos().y();
253 0 : endY += item->pagePos().y();
254 0 : item->startAnchor().setY(startY);
255 0 : item->endAnchor().setY(endY);
256 0 : item->mutldata()->setAnchors(item->startAnchor(), item->endAnchor());
257 :
258 0 : ChordLayout::layoutStem(item->chord1(), ctx);
259 0 : ChordLayout::layoutStem(item->chord2(), ctx);
260 :
261 0 : createBeamSegments(item, ctx);
262 0 : return;
263 : }
264 395 : ldata->setPosY(0.);
265 790 : std::vector<ChordRest*> chordRests{ item->chord1(), item->chord2() };
266 395 : std::vector<BeamBase::NotePosition> notes;
267 395 : double mag = 0.0;
268 :
269 395 : notes.clear();
270 1185 : for (ChordRest* cr : chordRests) {
271 790 : double m = cr->isSmall() ? ctx.conf().styleD(Sid::smallNoteMag) : 1.0;
272 790 : mag = std::max(mag, m);
273 790 : if (cr->isChord()) {
274 790 : Chord* chord = toChord(cr);
275 : //int i = chord->staffMove();
276 : //_minMove = std::min(_minMove, i); todo: investigate this
277 : //_maxMove = std::max(_maxMove, i);
278 :
279 1598 : for (int distance : chord->noteDistances()) {
280 808 : notes.push_back(BeamBase::NotePosition(distance, chord->vStaffIdx()));
281 790 : }
282 : }
283 : }
284 :
285 395 : std::sort(notes.begin(), notes.end());
286 395 : ldata->setMag(mag);
287 395 : BeamTremoloLayout::calculateAnchors(item, item->mutldata(), ctx, chordRests, notes);
288 395 : item->setStartAnchor(item->ldata()->startAnchor);
289 395 : item->setEndAnchor(item->ldata()->endAnchor);
290 :
291 395 : int idx = item->directionIdx();
292 395 : item->beamFragment().py1[idx] = item->startAnchor().y() - item->pagePos().y();
293 395 : item->beamFragment().py2[idx] = item->endAnchor().y() - item->pagePos().y();
294 395 : createBeamSegments(item, ctx);
295 395 : }
296 :
297 : //---------------------------------------------------------
298 : // extendedStemLenWithTwoNotesTremolo
299 : // Goal: To extend stem of one of the chords to make the tremolo less steep
300 : // Returns a modified pair of stem lengths of two chords
301 : //---------------------------------------------------------
302 :
303 0 : std::pair<double, double> TremoloLayout::extendedStemLenWithTwoNoteTremolo(TremoloTwoChord* tremolo, double stemLen1, double stemLen2)
304 : {
305 0 : const double spatium = tremolo->spatium();
306 0 : Chord* c1 = tremolo->chord1();
307 0 : Chord* c2 = tremolo->chord2();
308 0 : Stem* s1 = c1->stem();
309 0 : Stem* s2 = c2->stem();
310 0 : const double sgn1 = c1->up() ? -1.0 : 1.0;
311 0 : const double sgn2 = c2->up() ? -1.0 : 1.0;
312 0 : const double stemTipDistance = (s1 && s2) ? (s2->pagePos().y() + stemLen2) - (s1->pagePos().y() + stemLen1)
313 0 : : (StemLayout::stemPos(c2).y() + (sgn2 * stemLen2)) - (StemLayout::stemPos(c1).y() + (sgn1 * stemLen1));
314 :
315 : // same staff & same direction: extend one of the stems
316 0 : if (c1->staffMove() == c2->staffMove() && c1->up() == c2->up()) {
317 0 : const bool stem1Higher = stemTipDistance > 0.0;
318 0 : if (std::abs(stemTipDistance) > 1.0 * spatium) {
319 0 : if ((c1->up() && !stem1Higher) || (!c1->up() && stem1Higher)) {
320 0 : return { stemLen1 + (std::abs(stemTipDistance) - spatium), stemLen2 };
321 : } else { /* if ((c1->up() && stem1Higher) || (!c1->up() && !stem1Higher)) */
322 0 : return { stemLen1, stemLen2 + (std::abs(stemTipDistance) - spatium) };
323 : }
324 : }
325 : }
326 :
327 0 : return { stemLen1, stemLen2 };
328 : }
329 :
330 395 : void TremoloLayout::createBeamSegments(TremoloTwoChord* item, const LayoutContext& ctx)
331 : {
332 : // TODO: This should be a style setting, to replace tremoloStrokeLengthMultiplier
333 : static constexpr double stemGapSp = 1.0;
334 395 : const bool defaultStyle = (!item->customStyleApplicable()) || (item->tremoloStyle() == TremoloStyle::DEFAULT);
335 :
336 395 : item->clearBeamSegments();
337 :
338 395 : bool _isGrace = item->chord1()->isGrace();
339 395 : const PointF pagePos = item->pagePos();
340 395 : PointF startAnchor = item->ldata()->startAnchor - PointF(0.0, pagePos.y());
341 395 : PointF endAnchor = item->ldata()->endAnchor - PointF(0.0, pagePos.y());
342 :
343 : // inset trem from stems for default style
344 395 : const double slope = muse::divide(endAnchor.y() - startAnchor.y(), endAnchor.x() - startAnchor.x(), 0.0);
345 :
346 395 : double gapSp = stemGapSp;
347 395 : if (defaultStyle || item->tremoloStyle() == TremoloStyle::TRADITIONAL_ALTERNATE) {
348 : // we can eat into the stemGapSp margin if the anchorpoints are sufficiently close together
349 395 : double widthSp = (endAnchor.x() - startAnchor.x()) / item->spatium() - (stemGapSp * 2);
350 395 : if (!muse::RealIsEqualOrMore(widthSp, 0.6)) {
351 : // tremolo beam is too short; we can eat into the gap spacing a little
352 128 : gapSp = std::max(stemGapSp - ((0.6 - widthSp) * 0.5), 0.4);
353 : }
354 : } else {
355 0 : gapSp = 0.0;
356 : }
357 395 : BeamSegment* mainStroke = new BeamSegment(item);
358 395 : PointF xOffset = PointF(gapSp * item->spatium(), 0);
359 395 : PointF yOffset = PointF(0, gapSp * item->spatium() * slope);
360 395 : if (item->tremoloStyle() == TremoloStyle::TRADITIONAL_ALTERNATE) {
361 0 : mainStroke->line = LineF(startAnchor, endAnchor);
362 0 : startAnchor += xOffset;
363 0 : endAnchor -= xOffset;
364 0 : startAnchor += yOffset;
365 0 : endAnchor -= yOffset;
366 : } else {
367 395 : startAnchor += xOffset;
368 395 : endAnchor -= xOffset;
369 395 : startAnchor += yOffset;
370 395 : endAnchor -= yOffset;
371 395 : mainStroke->line = LineF(startAnchor, endAnchor);
372 : }
373 395 : mainStroke->level = 0;
374 :
375 395 : item->beamSegments().push_back(mainStroke);
376 :
377 395 : LineF line = mainStroke->line;
378 395 : double halfWidth = 0.5 * ctx.conf().styleAbsolute(Sid::beamWidth);
379 395 : double bboxTop = std::min(line.y1(), line.y2()) - halfWidth;
380 395 : double bboxBottom = std::max(line.y1(), line.y2()) + halfWidth;
381 :
382 395 : RectF bbox = RectF(line.x1(), bboxTop, line.x2() - line.x1(), bboxBottom - bboxTop);
383 :
384 395 : PointF beamOffset = PointF(0., (item->up() ? 1 : -1) * item->spatium() * (ctx.conf().styleB(Sid::useWideBeams) ? 1. : 0.75));
385 395 : beamOffset.setY(beamOffset.y() * item->mag() * (_isGrace ? ctx.conf().styleD(Sid::graceNoteMag) : 1.));
386 954 : for (int i = 1; i < item->lines(); ++i) {
387 559 : BeamSegment* stroke = new BeamSegment(item);
388 559 : stroke->level = i;
389 559 : stroke->line = LineF(startAnchor + (beamOffset * (double)i), endAnchor + (beamOffset * (double)i));
390 559 : item->beamSegments().push_back(stroke);
391 559 : bbox.unite(bbox.translated(0., beamOffset.y() * (double)i));
392 : }
393 395 : item->setbbox(bbox);
394 :
395 : // size stems properly
396 733 : if (item->chord1()->stem() && item->chord2()->stem()
397 733 : && !(item->chord1()->beam() && item->chord1()->beam() == item->chord2()->beam())) {
398 : // we don't need to do anything if these chords are part of the same beam--their stems are taken care of
399 : // by the beam layout
400 218 : int beamSpacing = ctx.conf().styleB(Sid::useWideBeams) ? 4 : 3;
401 654 : for (ChordRest* cr : { item->chord1(), item->chord2() }) {
402 436 : Chord* chord = toChord(cr);
403 436 : double addition = 0.0;
404 436 : if (cr->up() != item->up() && item->lines() > 1) {
405 : // need to adjust further for beams on the opposite side
406 0 : addition += (item->lines() - 1.) * beamSpacing / 4. * item->spatium() * item->mag();
407 : }
408 : // calling extendStem with addition 0.0 still sizes the stem to the manually adjusted height of the trem.
409 436 : BeamTremoloLayout::extendStem(item->ldata(), chord, addition);
410 : }
411 : }
412 395 : }
|