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 "editautomationpoints.h"
24 :
25 : #include "engraving/automation/internal/scoreautomationcontroller.h"
26 :
27 : #include "engraving/dom/measure.h"
28 : #include "engraving/dom/repeatlist.h"
29 : #include "engraving/dom/staff.h"
30 :
31 : #include "log.h"
32 :
33 : using namespace mu::engraving;
34 :
35 4 : static std::optional<ChangedRange> computeChangedRange(const Score* score, const AutomationCurveKey& key,
36 : const std::map<utick_t, std::optional<AutomationPoint> >& pointStates)
37 : {
38 4 : IF_ASSERT_FAILED(score && !pointStates.empty()) {
39 0 : return std::nullopt;
40 : }
41 :
42 4 : size_t staffIdx = muse::nidx;
43 4 : if (const std::optional<muse::ID> staffId = key.staffId()) {
44 4 : for (size_t i = 0; i < score->nstaves(); ++i) {
45 4 : const Staff* staff = score->staff(i);
46 4 : if (staff && staff->id() == *staffId) {
47 4 : staffIdx = i;
48 4 : break;
49 : }
50 : }
51 :
52 4 : IF_ASSERT_FAILED(staffIdx != muse::nidx) {
53 0 : return std::nullopt;
54 : }
55 : }
56 : // else: Global/Instrument-scoped key - not tied to any staff, leave staffIdx as muse::nidx
57 :
58 4 : const RepeatList& repeatList = score->expandedRepeatList();
59 :
60 : //! NOTE: utick2tick can be non-monotonic across repeat passes (e.g. D.S. al Coda),
61 : //! so the base-tick extremes aren't necessarily at the utick extremes
62 4 : Fraction tickFrom = Fraction::fromTicks(repeatList.utick2tick(pointStates.begin()->first));
63 4 : Fraction tickTo = tickFrom;
64 8 : for (const auto& state : pointStates) {
65 4 : const Fraction tick = Fraction::fromTicks(repeatList.utick2tick(state.first));
66 4 : if (tick < tickFrom) {
67 0 : tickFrom = tick;
68 : }
69 4 : if (tick > tickTo) {
70 0 : tickTo = tick;
71 : }
72 : }
73 :
74 4 : return ChangedRange { tickFrom, tickTo, staffIdx, staffIdx };
75 : }
76 :
77 4 : static void widenChangedRange(const Score* score, const AutomationCurveKey& key, const AutomationPointEdits& edits,
78 : ChangedRange& changedRange)
79 : {
80 4 : if (key.type == AutomationType::Tempo) {
81 0 : const Measure* lastMeasure = score->lastMeasure();
82 0 : if (lastMeasure) {
83 0 : changedRange.tickTo = lastMeasure->endTick();
84 : }
85 0 : return;
86 : }
87 :
88 4 : const RepeatList& repeatList = score->expandedRepeatList();
89 8 : for (const AutomationPointEdit& edit : edits) {
90 4 : const Fraction tick = Fraction::fromTicks(repeatList.utick2tick(edit.tick));
91 4 : changedRange.tickFrom = std::min(changedRange.tickFrom, tick);
92 4 : changedRange.tickTo = std::max(changedRange.tickTo, tick);
93 : }
94 : }
95 :
96 1 : EditAutomationPoints::EditAutomationPoints(Score* score, ScoreAutomationController* controller,
97 1 : const AutomationCurveKey& key, const AutomationPointEdits& edits)
98 1 : : m_score(score), m_controller(controller), m_key(key)
99 : {
100 1 : assert(score && controller && !edits.empty());
101 :
102 : // Collapse the edit list into the final value to write at each touched tick,
103 : // replaying the same erase-then-write order AutomationData::editPoints itself would apply
104 2 : for (const AutomationPointEdit& edit : edits) {
105 1 : if (const auto* setPoint = std::get_if<AutomationPointEdit::SetPoint>(&edit.change)) {
106 1 : m_pointStates[edit.tick] = setPoint->point;
107 0 : } else if (const auto* movePoint = std::get_if<AutomationPointEdit::MovePoint>(&edit.change)) {
108 0 : if (movePoint->from != edit.tick) {
109 0 : m_pointStates[movePoint->from] = std::nullopt;
110 : }
111 0 : m_pointStates[edit.tick] = movePoint->point;
112 : } else {
113 0 : m_pointStates[edit.tick] = std::nullopt;
114 : }
115 : }
116 1 : }
117 :
118 4 : std::optional<ChangedRange> EditAutomationPoints::changedRange() const
119 : {
120 4 : return m_changedRange;
121 : }
122 :
123 4 : void EditAutomationPoints::flip()
124 : {
125 5 : TRACEFUNC;
126 :
127 4 : IF_ASSERT_FAILED(m_score && m_controller && m_controller->automationData()) {
128 0 : return;
129 : }
130 :
131 4 : const AutomationCurve& curve = m_controller->automationData()->curve(m_key);
132 4 : m_changedRange = computeChangedRange(m_score, m_key, m_pointStates);
133 :
134 4 : std::map<utick_t, std::optional<AutomationPoint> > previousStates;
135 4 : AutomationPointEdits automationEdits;
136 4 : automationEdits.reserve(m_pointStates.size());
137 :
138 8 : for (const auto& [tick, point] : m_pointStates) {
139 4 : const auto it = curve.find(tick);
140 4 : if (it != curve.cend()) {
141 2 : previousStates.emplace(tick, it->second);
142 : } else {
143 2 : previousStates.emplace(tick, std::nullopt);
144 : }
145 :
146 4 : if (point) {
147 2 : automationEdits.push_back({ tick, AutomationPointEdit::SetPoint { *point } });
148 : } else {
149 2 : automationEdits.push_back({ tick, AutomationPointEdit::ErasePoint {} });
150 : }
151 : }
152 :
153 4 : if (m_changedRange) {
154 4 : m_controller->editPoints(m_key, automationEdits);
155 4 : widenChangedRange(m_score, m_key, automationEdits, *m_changedRange);
156 : }
157 :
158 4 : m_pointStates = std::move(previousStates);
159 4 : }
|