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) 2026 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "chordbracketlayout.h"
24 :
25 : #include <algorithm>
26 : #include <vector>
27 :
28 : #include "dom/chord.h"
29 : #include "dom/chordbracket.h"
30 : #include "dom/engravingitem.h"
31 : #include "dom/note.h"
32 : #include "dom/part.h"
33 : #include "dom/segment.h"
34 :
35 : #include "chordlayout.h"
36 : #include "horizontalspacing.h"
37 : #include "tlayout.h"
38 :
39 : using namespace muse;
40 : using namespace mu::engraving;
41 : using namespace mu::engraving::rendering::score;
42 :
43 : //---------------------------------------------------------
44 : // chordBracketsInSegment
45 : //---------------------------------------------------------
46 :
47 1573724 : std::vector<ChordBracket*> ChordBracketLayout::chordBracketsInSegment(const Segment* segment)
48 : {
49 1573724 : std::vector<ChordBracket*> result;
50 :
51 1573724 : if (!segment) {
52 0 : return result;
53 : }
54 :
55 15613920 : for (EngravingItem* item : segment->elist()) {
56 14040196 : if (!item || !item->isChord()) {
57 12935604 : continue;
58 : }
59 :
60 1107103 : for (EngravingItem* child : toChord(item)->el()) {
61 2511 : if (child && child->isChordBracket()) {
62 0 : result.push_back(toChordBracket(child));
63 : }
64 : }
65 : }
66 :
67 1573724 : return result;
68 0 : }
69 :
70 : //---------------------------------------------------------
71 : // bracketVisualSpan
72 : //---------------------------------------------------------
73 :
74 0 : ChordBracketLayout::BracketVisualSpan ChordBracketLayout::bracketVisualSpan(const ChordBracket* bracket)
75 : {
76 0 : const Chord* startChord = bracket->chord();
77 0 : EngravingItem* endItem = startChord->segment()->element(bracket->endTrack());
78 0 : const Chord* endChord = endItem && endItem->isChord() ? toChord(endItem) : startChord;
79 :
80 0 : const auto topY = [](const Chord* chord) {
81 0 : const Note* note = chord->upNote();
82 0 : return chord->pos().y() + note->pos().y() + note->ldata()->bbox().top();
83 : };
84 :
85 0 : const auto bottomY = [](const Chord* chord) {
86 0 : const Note* note = chord->downNote();
87 0 : return chord->pos().y() + note->pos().y() + note->ldata()->bbox().bottom();
88 : };
89 :
90 0 : const staff_idx_t startStaff = startChord->vStaffIdx();
91 0 : const staff_idx_t endStaff = endChord->vStaffIdx();
92 :
93 0 : const double spatium = bracket->spatium();
94 0 : const double lineWidth = bracket->style().styleAbsolute(Sid::chordBracketLineWidth);
95 :
96 : // Match TLayout::layoutChordBracket() padding.
97 0 : const double topPadding = bracket->userLen1() + 0.25 * spatium + 0.5 * lineWidth;
98 0 : const double bottomPadding = bracket->userLen2() + 0.5 * spatium + 0.5 * lineWidth;
99 :
100 0 : BracketVisualSpan result;
101 0 : result.topStaff = std::min(startStaff, endStaff);
102 0 : result.bottomStaff = std::max(startStaff, endStaff);
103 :
104 0 : if (startStaff == endStaff) {
105 0 : result.topY = std::min(topY(startChord), topY(endChord)) - topPadding;
106 0 : result.bottomY = std::max(bottomY(startChord), bottomY(endChord)) + bottomPadding;
107 :
108 : // Enforce the minimum length using endpoint coordinates.
109 0 : const double minLen = 2 * spatium;
110 0 : const double diff = minLen - std::abs(result.bottomY - result.topY);
111 :
112 0 : if (diff > 0.0) {
113 0 : if (bracket->hookPos() == DirectionV::DOWN) {
114 0 : result.topY -= diff;
115 0 : } else if (bracket->hookPos() == DirectionV::AUTO) {
116 0 : result.topY -= 0.5 * diff;
117 0 : result.bottomY += 0.5 * diff;
118 : } else {
119 0 : result.bottomY += diff;
120 : }
121 : }
122 : } else {
123 0 : const Chord* upperAnchor = startStaff < endStaff ? startChord : endChord;
124 0 : const Chord* lowerAnchor = startStaff < endStaff ? endChord : startChord;
125 :
126 0 : result.topY = topY(upperAnchor) - topPadding;
127 0 : result.bottomY = bottomY(lowerAnchor) + bottomPadding;
128 : }
129 :
130 0 : result.topY += bracket->offset().y();
131 0 : result.bottomY += bracket->offset().y();
132 :
133 0 : return result;
134 : }
135 :
136 : //---------------------------------------------------------
137 : // bracketCollisionShape
138 : //---------------------------------------------------------
139 :
140 0 : Shape ChordBracketLayout::bracketCollisionShape(const ChordBracket* bracket, const BracketVisualSpan& span, staff_idx_t staffIdx,
141 : double x)
142 : {
143 0 : if (staffIdx < span.topStaff || staffIdx > span.bottomStaff) {
144 0 : return Shape();
145 : }
146 :
147 0 : double top = span.topY;
148 0 : double bottom = span.bottomY;
149 :
150 : // Final staff distances are not known during horizontal spacing.
151 : // Approximate the bracket per staff: extend it toward the other endpoint on the endpoint staves
152 : // and across the full height of intermediate staves.
153 : static constexpr double CROSS_STAFF_COLLISION_EXTENT = 100000.0;
154 :
155 0 : if (span.topStaff != span.bottomStaff) {
156 0 : top = staffIdx == span.topStaff ? span.topY : -CROSS_STAFF_COLLISION_EXTENT;
157 0 : bottom = staffIdx == span.bottomStaff ? span.bottomY : CROSS_STAFF_COLLISION_EXTENT;
158 : }
159 :
160 0 : if (bottom < top) {
161 0 : std::swap(top, bottom);
162 : }
163 :
164 0 : return Shape(RectF(x, top, bracket->width(), bottom - top), bracket);
165 : }
166 :
167 : //---------------------------------------------------------
168 : // layoutHorizontal
169 : //---------------------------------------------------------
170 0 : void ChordBracketLayout::layoutHorizontal(ChordBracket* bracket)
171 : {
172 0 : Chord* owner = bracket->chord();
173 0 : Segment* segment = owner->segment();
174 0 : const TrackRange range = bracket->part()->trackRange();
175 0 : const BracketVisualSpan span = bracketVisualSpan(bracket);
176 :
177 0 : double requiredDistance = 0.0;
178 0 : const double baseX = owner->pos().x() + bracket->offset().x();
179 :
180 0 : for (track_idx_t track = range.startTrack; track < range.endTrack; ++track) {
181 0 : EngravingItem* item = segment->element(track);
182 0 : if (!item || !item->isChord()) {
183 0 : continue;
184 : }
185 :
186 0 : Chord* chord = toChord(item);
187 0 : Shape chordShape = chord->shape();
188 0 : chordShape.removeTypes({ ElementType::CHORD_BRACKET });
189 0 : chordShape.translate(chord->pos());
190 0 : if (chordShape.empty()) {
191 0 : continue;
192 : }
193 :
194 0 : Shape bracketShape = bracketCollisionShape(bracket, span, chord->vStaffIdx(), baseX);
195 0 : if (bracketShape.empty()) {
196 0 : continue;
197 : }
198 :
199 0 : const double distance = bracket->rightSide()
200 0 : ? HorizontalSpacing::minHorizontalDistance(chordShape, bracketShape, bracket->spatium())
201 0 : : HorizontalSpacing::minHorizontalDistance(bracketShape, chordShape, bracket->spatium());
202 0 : requiredDistance = std::max(requiredDistance, distance);
203 0 : }
204 :
205 0 : bracket->mutldata()->setPosX(bracket->rightSide() ? requiredDistance : -requiredDistance);
206 0 : }
207 :
208 : //---------------------------------------------------------
209 : // layoutSegment
210 : //---------------------------------------------------------
211 :
212 130198 : void ChordBracketLayout::layoutSegment(Segment* segment, LayoutContext& ctx)
213 : {
214 130198 : if (!segment) {
215 0 : return;
216 : }
217 :
218 130198 : for (ChordBracket* bracket : chordBracketsInSegment(segment)) {
219 0 : Chord* owner = bracket->chord();
220 0 : if (!owner || owner->segment() != segment || !bracket->part() || owner->onTabStaff()) {
221 0 : continue;
222 : }
223 :
224 : // Initialize the bracket's width, vertical geometry, and Shape.
225 : // Replace the chord-local horizontal position with the segment-level result.
226 0 : TLayout::layoutItem(bracket, ctx);
227 0 : layoutHorizontal(bracket);
228 :
229 : // Rebuild the owner Shape after moving the bracket.
230 0 : ChordLayout::fillShape(owner, owner->mutldata());
231 130198 : }
232 : }
233 :
234 : //---------------------------------------------------------
235 : // updateHorizontalSpacing
236 : //---------------------------------------------------------
237 :
238 721763 : void ChordBracketLayout::updateHorizontalSpacing(const Segment* firstSegment, const Segment* secondSegment,
239 : staff_idx_t staffIdx, double squeezeFactor, double& minDistance)
240 : {
241 721763 : if (!firstSegment || !secondSegment) {
242 0 : return;
243 : }
244 :
245 1443526 : auto updateForBrackets = [&](const Segment* segment, const Shape& otherShape, bool rightSide) {
246 1443526 : for (ChordBracket* bracket : chordBracketsInSegment(segment)) {
247 0 : const Chord* owner = bracket->chord();
248 0 : if (!owner || owner->onTabStaff() || !bracket->visible() || !bracket->addToSkyline() || bracket->rightSide() != rightSide) {
249 0 : continue;
250 : }
251 :
252 0 : const BracketVisualSpan span = bracketVisualSpan(bracket);
253 :
254 0 : if (span.topStaff == span.bottomStaff) {
255 0 : continue;
256 : }
257 :
258 0 : const double x = owner->pos().x() + bracket->pos().x();
259 0 : const Shape bracketShape = bracketCollisionShape(bracket, span, staffIdx, x);
260 :
261 0 : if (bracketShape.empty()) {
262 0 : continue;
263 : }
264 :
265 : const double distance = rightSide
266 0 : ? HorizontalSpacing::minHorizontalDistance(bracketShape, otherShape, bracket->spatium(), squeezeFactor)
267 0 : : HorizontalSpacing::minHorizontalDistance(otherShape, bracketShape, bracket->spatium(), squeezeFactor);
268 :
269 0 : minDistance = std::max(minDistance, distance);
270 1443526 : }
271 1443526 : };
272 :
273 : // Check the brackets facing the gap between the two Segments:
274 : // left-side brackets on the second Segment and right-side brackets on the first.
275 721763 : updateForBrackets(secondSegment, firstSegment->staffShape(staffIdx), /*rightSide=*/ false);
276 721763 : updateForBrackets(firstSegment, secondSegment->staffShape(staffIdx), /*rightSide=*/ true);
277 : }
278 :
279 : //---------------------------------------------------------
280 : // updateVerticalGeometry
281 : //---------------------------------------------------------
282 :
283 0 : void ChordBracketLayout::updateVerticalGeometry(ChordBracket* bracket, LayoutContext& ctx)
284 : {
285 0 : if (!bracket || !bracket->chord() || !bracket->chord()->segment()) {
286 0 : return;
287 : }
288 :
289 : // Re-layout the final height without changing the horizontal position.
290 0 : const double x = bracket->ldata()->pos().x();
291 0 : TLayout::layoutItem(bracket, ctx);
292 0 : bracket->mutldata()->setPosX(x);
293 :
294 : // Refresh cached shapes after restoring the segment-level position.
295 0 : Chord* chord = bracket->chord();
296 0 : Segment* segment = chord->segment();
297 0 : ChordLayout::fillShape(chord, chord->mutldata());
298 :
299 0 : const BracketVisualSpan span = bracketVisualSpan(bracket);
300 0 : for (staff_idx_t staffIdx = span.topStaff; staffIdx <= span.bottomStaff; ++staffIdx) {
301 0 : segment->createShape(staffIdx);
302 : }
303 : }
|