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 "arpeggiolayout.h"
23 :
24 : #include "dom/arpeggio.h"
25 : #include "dom/chord.h"
26 : #include "dom/ledgerline.h"
27 : #include "dom/part.h"
28 : #include "dom/score.h"
29 : #include "dom/segment.h"
30 : #include "dom/staff.h"
31 :
32 : #include "tlayout.h"
33 :
34 : using namespace mu::engraving;
35 : using namespace mu::engraving::rendering::score;
36 :
37 : // layoutArpeggio2
38 : // called after layout of page
39 :
40 48512 : void ArpeggioLayout::layoutArpeggio2(Arpeggio* item, LayoutContext& ctx)
41 : {
42 48512 : if (!item) {
43 48279 : return;
44 : }
45 :
46 233 : TLayout::layoutItem(item, ctx);
47 : }
48 :
49 : //
50 : // INSET:
51 : // Arpeggios have inset white space. For instance, the bracket
52 : // "[" shape has whitespace inside of the "C". Symbols like
53 : // accidentals can fit inside this whitespace. These inset
54 : // functions are used to get the size of the inner dimensions
55 : // for this area on all arpeggios.
56 : //
57 :
58 20 : double ArpeggioLayout::insetTop(const Arpeggio* item, const Chord* c)
59 : {
60 20 : double top = c->upNote()->y() - c->upNote()->height() / 2;
61 :
62 : // use wiggle width, not height, since it's rotated 90 degrees
63 20 : if (item->arpeggioType() == ArpeggioType::UP) {
64 4 : top += item->symBbox(SymId::wiggleArpeggiatoUpArrow).width();
65 16 : } else if (item->arpeggioType() == ArpeggioType::UP_STRAIGHT) {
66 0 : top += item->symBbox(SymId::arrowheadBlackUp).width();
67 : }
68 :
69 20 : return top;
70 : }
71 :
72 18 : double ArpeggioLayout::insetBottom(const Arpeggio* item, const Chord* c)
73 : {
74 18 : double bottom = c->downNote()->y() + c->downNote()->height() / 2;
75 :
76 : // use wiggle width, not height, since it's rotated 90 degrees
77 18 : if (item->arpeggioType() == ArpeggioType::DOWN) {
78 0 : bottom -= item->symBbox(SymId::wiggleArpeggiatoUpArrow).width();
79 18 : } else if (item->arpeggioType() == ArpeggioType::DOWN_STRAIGHT) {
80 0 : bottom -= item->symBbox(SymId::arrowheadBlackDown).width();
81 : }
82 :
83 18 : return bottom;
84 : }
85 :
86 12 : double ArpeggioLayout::insetWidth(const Arpeggio* item)
87 : {
88 12 : double lineWidth = item->style().styleAbsolute(item->isChordBracket() ? Sid::chordBracketLineWidth : Sid::arpeggioLineWidth);
89 12 : switch (item->arpeggioType()) {
90 12 : case ArpeggioType::NORMAL:
91 : {
92 12 : return 0.0;
93 : }
94 :
95 0 : case ArpeggioType::UP:
96 : case ArpeggioType::DOWN:
97 : {
98 : // use wiggle height, not width, since it's rotated 90 degrees
99 0 : return (item->width() - item->symBbox(SymId::wiggleArpeggiatoUp).height()) / 2;
100 : }
101 :
102 0 : case ArpeggioType::UP_STRAIGHT:
103 : case ArpeggioType::DOWN_STRAIGHT:
104 : {
105 0 : return (item->width() - lineWidth) / 2;
106 : }
107 :
108 0 : case ArpeggioType::BRACKET:
109 : {
110 0 : return item->width() - lineWidth / 2;
111 : }
112 : }
113 0 : return 0.0;
114 : }
115 :
116 252 : void ArpeggioLayout::clearAccidentals(Arpeggio* item, LayoutContext& ctx)
117 : {
118 : // This function finds collisions with accidentals outside of an arpeggio's span
119 : // Assumptions are made to deal with cross staff height, as we don't know this yet.
120 : // Once a chord is found, its shape is stripped down to only accidentals which collide
121 : // with the arpeggio. The arpeggio's offset is calculated from the remaining shape
122 :
123 252 : if (!item || !item->chord()) {
124 0 : return;
125 : }
126 :
127 252 : const Segment* seg = item->chord()->segment();
128 252 : const EngravingItem* endEl = seg->element(item->endTrack());
129 252 : const Chord* endChord = endEl && endEl->isChord() ? toChord(endEl) : item->chord();
130 :
131 252 : const Part* part = item->part();
132 252 : const TrackRange trackRange = part->trackRange();
133 252 : const PaddingTable& paddingTable = item->score()->paddingTables().horizontalPaddingTable();
134 :
135 252 : double arpeggioAccidentalDistance = paddingTable.at(ElementType::ARPEGGIO).at(ElementType::ACCIDENTAL) * item->mag();
136 252 : double arpeggioLedgerDistance = paddingTable.at(ElementType::ARPEGGIO).at(ElementType::LEDGER_LINE) * item->mag();
137 :
138 252 : Shape arpShape = item->shape().translate(item->pagePos());
139 252 : double arpX = arpShape.right();
140 252 : double largestOverlap = 0.0;
141 :
142 1960 : for (track_idx_t track = trackRange.startTrack; track < trackRange.endTrack; ++track) {
143 1708 : EngravingItem* e = seg->element(track);
144 1708 : if (!e || !e->isChord()) {
145 1704 : continue;
146 : }
147 :
148 567 : Chord* chord = toChord(e);
149 : // Only check chords the arpeggio doesn't span on staves the arpeggios span
150 : // We already calculated the gap between chords it does span in ChordLayout::layoutPitched
151 : bool aboveStart
152 567 : = std::make_pair(chord->vStaffIdx(), chord->downLine()) < std::make_pair(item->vStaffIdx(), item->chord()->upLine());
153 567 : bool belowEnd = std::make_pair(chord->vStaffIdx(), chord->upLine()) > std::make_pair(endChord->vStaffIdx(), endChord->downLine());
154 :
155 1134 : if ((chord->spanArpeggio() == item && !(aboveStart || belowEnd)) || chord->vStaffIdx() < item->vStaffIdx()
156 1134 : || chord->vStaffIdx() > endChord->vStaffIdx()) {
157 507 : continue;
158 : }
159 :
160 : // We don't know the distance between staves, so our arpeggio's height is currently unknown if it spans staves
161 : // To make sure we spot collisions with non-arpeggio accidentals, extend arpeggio upwards/downwards a large amount
162 : // depending on if they are the end/start of the arpeggio respectively.
163 : // This is to find collisions between chords outside the arpeggios track span, but within its "visual span"
164 60 : Shape curArpShape = arpShape;
165 60 : staff_idx_t staffIdx = chord->vStaffIdx();
166 60 : if (staffIdx == item->chord()->vStaffIdx() && item->crossStaff()) {
167 : // Arpeggio doesn't end on this staff, so extend downwards
168 0 : curArpShape.addBBox(RectF(curArpShape.bbox().bottomLeft().x(), curArpShape.bbox().bottomLeft().y(),
169 0 : curArpShape.bbox().width(), ARBITRARY_ARPEGGIO_LENGTH));
170 60 : } else if (staffIdx == endChord->vStaffIdx() && item->crossStaff()) {
171 : // Arpeggio doesn't start on this staff so move to last note and extend upwards
172 0 : curArpShape.translate(PointF(0.0, endChord->downNote()->pagePos().y() - item->pagePos().y()));
173 0 : curArpShape.addBBox(RectF(curArpShape.bbox().topLeft().x(), curArpShape.bbox().topLeft().y() - ARBITRARY_ARPEGGIO_LENGTH,
174 0 : curArpShape.bbox().width(), ARBITRARY_ARPEGGIO_LENGTH));
175 : }
176 :
177 60 : Shape chordShape = chord->shape();
178 60 : chordShape.translate(chord->pagePos());
179 : // Remove any element which isn't an accidental the arpeggio intersects
180 60 : chordShape.remove_if([curArpShape, arpeggioAccidentalDistance, arpeggioLedgerDistance](ShapeElement& shapeElement) {
181 246 : if (!shapeElement.item()
182 246 : || !(shapeElement.item()->isAccidental() || shapeElement.item()->isLedgerLine())) {
183 178 : return true;
184 : }
185 68 : if (shapeElement.item()->isAccidental()) {
186 : // Pad accidentals with Sid::arpeggioAccidentalDistance either side
187 6 : shapeElement.setTopLeft(PointF(shapeElement.topLeft().x() - arpeggioAccidentalDistance, shapeElement.topLeft().y()));
188 6 : shapeElement.setWidth(shapeElement.width() + 2 * arpeggioAccidentalDistance);
189 62 : } else if (shapeElement.item()->isLedgerLine()) {
190 62 : shapeElement.setTopLeft(PointF(shapeElement.topLeft().x() - arpeggioLedgerDistance, shapeElement.topLeft().y()));
191 62 : shapeElement.setWidth(shapeElement.width() + 2 * arpeggioLedgerDistance);
192 : }
193 68 : return !curArpShape.intersects(shapeElement);
194 : });
195 :
196 60 : if (chordShape.empty()) {
197 : // No collisions with accidentals
198 56 : continue;
199 : }
200 :
201 4 : double chordX = chordShape.left();
202 4 : double diff = chordX - arpX;
203 :
204 4 : if (!muse::RealIsNull(diff)) {
205 4 : double inset = insetDistance(item, ctx, item->mag(), chord);
206 4 : largestOverlap = std::min(largestOverlap, diff + inset);
207 : }
208 116 : }
209 252 : if (!muse::RealIsNull(largestOverlap)) {
210 4 : item->mutldata()->moveX(largestOverlap);
211 : }
212 252 : }
213 :
214 4 : double ArpeggioLayout::insetDistance(const Arpeggio* item, const LayoutContext& ctx, double mag_, const Chord* chord)
215 : {
216 4 : if (!item || !chord) {
217 0 : return 0.0;
218 : }
219 :
220 4 : std::vector<Accidental*> _accidentals;
221 :
222 : // generate list of accidentals if none provided
223 10 : for (Note* note : chord->notes()) {
224 6 : Accidental* accidental = note->accidental();
225 6 : if (accidental && accidental->visible()) {
226 2 : _accidentals.push_back(accidental);
227 : }
228 : }
229 :
230 4 : if (_accidentals.empty()) {
231 2 : return 0.0;
232 : }
233 :
234 2 : return insetDistance(item, ctx, mag_, chord, _accidentals);
235 4 : }
236 :
237 20 : double ArpeggioLayout::insetDistance(const Arpeggio* item, const LayoutContext& ctx, double mag_, const Chord* chord,
238 : const std::vector<Accidental*>& accidentals)
239 : {
240 20 : if (!item || !chord) {
241 0 : return 0.0;
242 : }
243 :
244 20 : if (accidentals.empty()) {
245 0 : return 0.0;
246 : }
247 :
248 : // Only be concerned about the top/bottom of an arpeggio line if this is the start/end chord
249 :
250 20 : const Segment* seg = item->chord()->segment();
251 20 : Chord* endChord = item->chord();
252 20 : const PaddingTable& paddingTable = item->score()->paddingTables().horizontalPaddingTable();
253 20 : if (EngravingItem* e = seg->element(item->endTrack())) {
254 20 : endChord = e->isChord() ? toChord(e) : endChord;
255 : }
256 20 : bool arpStartStave = chord->vStaffIdx() == item->vStaffIdx();
257 20 : bool arpEndStave = chord->vStaffIdx() == endChord->vStaffIdx();
258 :
259 20 : double arpeggioTop = arpStartStave ? insetTop(item, item->chord()) * mag_ : -ARBITRARY_ARPEGGIO_LENGTH;
260 20 : double arpeggioBottom = arpEndStave ? insetBottom(item, endChord) * mag_ : ARBITRARY_ARPEGGIO_LENGTH;
261 :
262 20 : ArpeggioType type = item->arpeggioType();
263 20 : bool hasTopArrow = type == ArpeggioType::UP
264 16 : || type == ArpeggioType::UP_STRAIGHT
265 36 : || type == ArpeggioType::BRACKET;
266 20 : bool hasBottomArrow = type == ArpeggioType::DOWN
267 20 : || type == ArpeggioType::DOWN_STRAIGHT
268 40 : || type == ArpeggioType::BRACKET;
269 :
270 20 : const Accidental* furthestAccidental = nullptr;
271 53 : for (const Accidental* accidental : accidentals) {
272 33 : if (furthestAccidental) {
273 13 : bool currentIsFurtherX = accidental->x() < furthestAccidental->x();
274 13 : bool currentIsSameX = accidental->x() == furthestAccidental->x();
275 13 : auto accidentalBbox = item->symBbox(accidental->symId());
276 13 : double currentTop = accidental->note()->pos().y() + accidentalBbox.top() * mag_;
277 13 : double currentBottom = accidental->note()->pos().y() + accidentalBbox.bottom() * mag_;
278 13 : bool collidesWithTop = currentTop <= arpeggioTop && hasTopArrow;
279 13 : bool collidesWithBottom = currentBottom >= arpeggioBottom && hasBottomArrow;
280 :
281 13 : if (currentIsFurtherX || (currentIsSameX && (collidesWithTop || collidesWithBottom))) {
282 5 : furthestAccidental = accidental;
283 : }
284 : } else {
285 20 : furthestAccidental = accidental;
286 : }
287 : }
288 :
289 20 : IF_ASSERT_FAILED(furthestAccidental) {
290 0 : return 0.0;
291 : }
292 :
293 20 : double maximumInset = (paddingTable.at(ElementType::ARPEGGIO).at(ElementType::ACCIDENTAL)
294 20 : - ctx.conf().styleAbsolute(Sid::arpeggioAccidentalDistanceMin)) * mag_;
295 :
296 20 : RectF bbox = item->symBbox(furthestAccidental->symId());
297 20 : double center = furthestAccidental->note()->pos().y() * mag_;
298 20 : double top = center + bbox.top() * mag_;
299 20 : double bottom = center + bbox.bottom() * mag_;
300 20 : bool collidesWithTop = hasTopArrow && top <= arpeggioTop;
301 20 : bool collidesWithBottom = hasBottomArrow && bottom >= arpeggioBottom;
302 :
303 20 : if (collidesWithTop || collidesWithBottom) {
304 8 : return maximumInset;
305 : }
306 :
307 12 : return insetWidth(item);
308 : }
309 :
310 466 : double ArpeggioLayout::computeHeight(const Arpeggio* item)
311 : {
312 466 : Chord* chord = item->chord();
313 466 : double y = chord->upNote()->pagePos().y() - chord->upNote()->headHeight() * .5;
314 :
315 466 : Note* downNote = chord->downNote();
316 466 : EngravingItem* e = chord->segment()->element(item->track() + item->span() - 1);
317 466 : if (e && e->isChord()) {
318 466 : downNote = toChord(e)->downNote();
319 : }
320 466 : double h = downNote->pagePos().y() + downNote->headHeight() * .5 - y;
321 466 : return h;
322 : }
323 :
324 466 : double ArpeggioLayout::calcTop(const Arpeggio* item, const LayoutConfiguration& conf)
325 : {
326 466 : double top = -item->userLen1();
327 466 : switch (item->arpeggioType()) {
328 52 : case ArpeggioType::BRACKET: {
329 52 : double lineWidth = conf.styleAbsolute(item->isChordBracket() ? Sid::chordBracketLineWidth : Sid::arpeggioLineWidth);
330 52 : return top - lineWidth / 2.0;
331 : }
332 394 : case ArpeggioType::NORMAL:
333 : case ArpeggioType::UP:
334 : case ArpeggioType::DOWN: {
335 : // if the top is in the staff on a space, move it up
336 : // if the bottom note is on a line, the distance is 0.25 spaces
337 : // if the bottom note is on a space, the distance is 0.5 spaces
338 394 : int topNoteLine = item->chord()->upNote()->line();
339 394 : int lines = item->staff()->lines(item->tick());
340 394 : int bottomLine = (lines - 1) * 2;
341 394 : if (topNoteLine <= 0 || topNoteLine % 2 == 0 || topNoteLine >= bottomLine) {
342 276 : return top;
343 : }
344 118 : int downNoteLine = item->chord()->downNote()->line();
345 118 : if (downNoteLine % 2 == 1 && downNoteLine < bottomLine) {
346 76 : return top - 0.4 * item->spatium();
347 : }
348 42 : return top - 0.25 * item->spatium();
349 : }
350 20 : default: {
351 20 : return top - item->spatium() / 4;
352 : }
353 : }
354 : }
355 :
356 466 : double ArpeggioLayout::calcBottom(const Arpeggio* item, double arpeggioHeight, const LayoutConfiguration& conf)
357 : {
358 466 : double top = -item->userLen1();
359 466 : double bottom = arpeggioHeight + item->userLen2();
360 :
361 466 : switch (item->arpeggioType()) {
362 52 : case ArpeggioType::BRACKET: {
363 52 : double lineWidth = conf.styleAbsolute(item->isChordBracket() ? Sid::chordBracketLineWidth : Sid::arpeggioLineWidth);
364 52 : return bottom - top + lineWidth;
365 : }
366 394 : case ArpeggioType::NORMAL:
367 : case ArpeggioType::UP:
368 : case ArpeggioType::DOWN: {
369 394 : return bottom;
370 : }
371 20 : default: {
372 20 : return bottom - top + item->spatium() / 2;
373 : }
374 : }
375 : }
|