10void sortTrack(action::ActionTrack& track) {
11 std::stable_sort(track.notifies.begin(), track.notifies.end(), [](
const auto&
left,
const auto&
right) {
12 return left.time == right.time ? left.id.format() < right.id.format() : left.time < right.time;
14 std::stable_sort(track.states.begin(), track.states.end(), [](
const auto&
left,
const auto&
right) {
15 return left.start == right.start ? left.id.format() < right.id.format() : left.start < right.start;
19void sortAnimationSections(action::ActionTimeline& timeline) {
21 timeline.animationSections.begin(), timeline.animationSections.end(), [](
const auto&
left,
const auto&
right) {
22 return left.start == right.start ? left.id.format() < right.id.format() : left.start < right.start;
29 std::optional<TimelineSelectionRange>
range;
39 if (selection_.contains(section.id.format())) include(section.start, section.end);
41 for (
const auto& notify : track.notifies)
42 if (selection_.contains(notify.id.format())) include(notify.time, notify.time);
43 for (
const auto&
state : track.states)
44 if (selection_.contains(
state.id.format())) include(
state.start,
state.end);
47 return eve::editing::failed<TimelineSelectionRange>(EditorStatus::Rejected,
48 RuleId(
"editor.action.timeline.selection-empty"),
49 "No timeline items are selected");
50 return eve::editing::applied<TimelineSelectionRange>(*
range);
54 if (selection_.empty())
return rejected(
"editor.action.timeline.selection-empty",
"No timeline items are selected");
58 if (!selection_.contains(section.id.format()))
continue;
59 auto start = move(section.start);
60 auto end = move(section.end);
62 return rejected(
"editor.action.timeline.time-overflow",
"Selected animation-section time overflowed");
63 section.start =
start.value();
64 section.end =
end.value();
66 for (
auto& track : candidate.
tracks) {
67 const bool selectedNotify = std::any_of(track.notifies.begin(), track.notifies.end(),
68 [&](
const auto& item) { return selection_.contains(item.id.format()); });
69 const bool selectedState = std::any_of(track.states.begin(), track.states.end(),
70 [&](
const auto& item) { return selection_.contains(item.id.format()); });
71 if (track.locked && (selectedNotify || selectedState))
72 return rejected(
"editor.action.timeline.track-locked",
"Selection contains an item on a locked track");
73 for (
auto& notify : track.notifies) {
74 if (!selection_.contains(notify.id.format()))
continue;
75 auto time = move(notify.time);
76 if (!
time)
return rejected(
"editor.action.timeline.time-overflow",
"Selected notify time overflowed");
77 notify.time =
time.value();
79 for (
auto&
state : track.states) {
80 if (!selection_.contains(
state.id.format()))
continue;
84 return rejected(
"editor.action.timeline.time-overflow",
"Selected notify-state time overflowed");
90 sortAnimationSections(candidate);
91 return commit(std::move(candidate),
"Move action timeline selection",
"action.timeline.selection.move");
100 if (!selection_.contains(section.id.format()))
continue;
104 return rejected(
"editor.action.timeline.time-overflow",
"Aligned animation-section time overflowed");
106 section.end =
end.value();
108 for (
auto& track : candidate.
tracks) {
109 const bool touchesLocked = track.locked &&
110 (std::any_of(track.notifies.begin(), track.notifies.end(), [&](
const auto& item) {
111 return selection_.contains(item.id.format());
113 std::any_of(track.states.begin(), track.states.end(), [&](
const auto& item) {
114 return selection_.contains(item.id.format());
117 return rejected(
"editor.action.timeline.track-locked",
"Selection contains an item on a locked track");
118 for (
auto& notify : track.notifies)
119 if (selection_.contains(notify.id.format())) notify.time =
anchor;
120 for (
auto&
state : track.states) {
121 if (!selection_.contains(
state.id.format()))
continue;
125 return rejected(
"editor.action.timeline.time-overflow",
"Aligned notify-state time overflowed");
131 sortAnimationSections(candidate);
132 return commit(std::move(candidate),
"Align action timeline selection starts",
133 "action.timeline.selection.align-start");
142 if (!selection_.contains(section.id.format()))
continue;
143 const auto span = section.end.
nanoseconds() - section.start.nanoseconds();
146 return rejected(
"editor.action.timeline.time-overflow",
"Aligned animation-section time overflowed");
147 section.start =
start.value();
150 for (
auto& track : candidate.
tracks) {
151 const bool touchesLocked = track.locked &&
152 (std::any_of(track.notifies.begin(), track.notifies.end(), [&](
const auto& item) {
153 return selection_.contains(item.id.format());
155 std::any_of(track.states.begin(), track.states.end(), [&](
const auto& item) {
156 return selection_.contains(item.id.format());
159 return rejected(
"editor.action.timeline.track-locked",
"Selection contains an item on a locked track");
160 for (
auto& notify : track.notifies)
161 if (selection_.contains(notify.id.format())) notify.time =
anchor;
162 for (
auto&
state : track.states) {
163 if (!selection_.contains(
state.id.format()))
continue;
164 const auto span =
state.end.nanoseconds() -
state.start.nanoseconds();
167 return rejected(
"editor.action.timeline.time-overflow",
"Aligned notify-state time overflowed");
173 sortAnimationSections(candidate);
174 return commit(std::move(candidate),
"Align action timeline selection ends",
175 "action.timeline.selection.align-end");
182 return rejected(
"editor.action.timeline.selection-scale-range",
183 "Scaled selection range must be ordered and inside the timeline");
184 const auto oldSpan =
range.value().end.nanoseconds() -
range.value().start.nanoseconds();
186 return rejected(
"editor.action.timeline.selection-scale-point",
187 "A point-only selection cannot be proportionally scaled");
188 const double rate =
static_cast<double>(
end.nanoseconds() -
start.nanoseconds()) /
189 static_cast<double>(oldSpan);
193 if (!scaled)
return scaled;
194 return start.tryAdd(scaled.value());
198 if (!selection_.contains(section.id.format()))
continue;
199 auto mappedStart = map(section.start);
200 auto mappedEnd = map(section.end);
201 auto blend = section.blendIn.scaled(
rate);
202 if (!mappedStart || !mappedEnd || !
blend)
203 return rejected(
"editor.action.timeline.selection-scale-overflow",
"Scaled selection time overflowed");
204 section.start = mappedStart.value();
205 section.end = mappedEnd.value();
206 section.blendIn =
blend.value();
208 for (
auto& track : candidate.
tracks) {
209 const bool touchesLocked = track.locked &&
210 (std::any_of(track.notifies.begin(), track.notifies.end(), [&](
const auto& item) {
211 return selection_.contains(item.id.format());
213 std::any_of(track.states.begin(), track.states.end(), [&](
const auto& item) {
214 return selection_.contains(item.id.format());
217 return rejected(
"editor.action.timeline.track-locked",
"Selection contains an item on a locked track");
218 for (
auto& notify : track.notifies) {
219 if (!selection_.contains(notify.id.format()))
continue;
220 auto mapped = map(notify.time);
222 return rejected(
"editor.action.timeline.selection-scale-overflow",
"Scaled notify time overflowed");
223 notify.time = mapped.value();
225 for (
auto&
state : track.states) {
226 if (!selection_.contains(
state.id.format()))
continue;
227 auto mappedStart = map(
state.start);
228 auto mappedEnd = map(
state.end);
229 if (!mappedStart || !mappedEnd)
230 return rejected(
"editor.action.timeline.selection-scale-overflow",
231 "Scaled notify-state time overflowed");
232 state.start = mappedStart.value();
233 state.end = mappedEnd.value();
237 sortAnimationSections(candidate);
238 return commit(std::move(candidate),
"Scale action timeline selection",
"action.timeline.selection.scale");
UI-neutral transactional action timeline editor and preview model.
Signed, fixed-resolution simulation duration in nanoseconds.
constexpr std::int64_t nanoseconds() const noexcept
Return the exact signed nanosecond representation.
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
static constexpr Duration zero() noexcept
Return zero duration.
Move-only operation result carrying either a value or Status.
static Result failure(Status status)
Construct a failed result from a structured status.
Result< void > alignSelectionStart()
Align selected item starts to the earliest selected start while preserving item durations.
Result< void > alignSelectionEnd()
Align selected item ends to the latest selected end while preserving item durations.
Result< TimelineSelectionRange > selectionRange() const
Return the outer bounds of every selected section, notify and notify-state.
Result< void > moveSelection(Duration delta)
Move the complete selection by one exact delta in a single undoable transaction.
Result< void > scaleSelection(Duration start, Duration end)
Proportionally fit the complete selection into exact destination bounds.
const action::ActionTimeline & timeline() const noexcept
Borrow the authoritative timeline until the next mutation.
eve::editing::RuleId RuleId
Canonical action timeline asset shared by runtime and editor.
std::vector< ActionAnimationSection > animationSections
std::vector< ActionTrack > tracks
Exact outer time bounds of the current owning timeline selection.