载入中...
搜索中...
未找到
ActionTimelineWidget.cpp
浏览该文件的文档.
2
6#include "common/Capability.h"
7
8#include <algorithm>
9#include <cmath>
10#include <limits>
11#include <sstream>
12#include <utility>
13
14namespace eve::editor {
15namespace {
16
17constexpr float kHandleRadius = 6.0f;
18constexpr float kNotifyWidth = 8.0f;
19constexpr std::size_t kMaximumWaveformBuckets = 512;
20constexpr std::size_t kMaximumCurveSegments = 128;
21
22Result<void> widgetError(std::string rule, std::string message, EditorStatus status = EditorStatus::Rejected) {
23 return eve::editing::failed<void>(status, RuleId(std::move(rule)), std::move(message));
24}
25
26struct ItemView {
27 LogicalId trackId;
28 LogicalId itemId;
29 LogicalId type;
30 Duration start;
31 Duration end;
33 bool state = false;
34 bool locked = false;
35 bool animationSection = false;
36};
37
38std::optional<ItemView> findItem(const action::ActionTimeline& timeline, const LogicalId& itemId) {
39 for (const auto& section : timeline.animationSections) {
40 if (section.id != itemId) continue;
41 return ItemView{
42 *LogicalId::fromParts("action-track", "animation"),
43 section.id,
44 *LogicalId::fromParts("animation", "section"),
45 section.start,
46 section.end,
47 {{"animationUri", Value(section.animationUri)}, {"blendInNs", Value(section.blendIn.nanoseconds())}},
48 true,
49 false,
50 true};
51 }
52 for (const auto& track : timeline.tracks) {
53 for (const auto& notify : track.notifies)
54 if (notify.id == itemId)
55 return ItemView{track.id, notify.id, notify.type, notify.time,
56 notify.time, notify.payload, false, track.locked};
57 for (const auto& state : track.states)
58 if (state.id == itemId)
59 return ItemView{track.id, state.id, state.type, state.start,
60 state.end, state.payload, true, track.locked};
61 }
62 return std::nullopt;
63}
64
65Duration difference(Duration left, Duration right) {
66 return Duration::fromNanoseconds(left.nanoseconds() - right.nanoseconds());
67}
68
69Result<void> adapt(const Result<std::size_t>& result, std::string rule, std::string message) {
70 if (result.ok()) return eve::editing::applied<void>();
71 if (!result.diagnostics().empty()) return Result<void>::failure(result.status());
72 return eve::editing::failed<void>(result.code(), RuleId(std::move(rule)), std::move(message));
73}
74
75std::string payloadText(const Value::Object& payload, std::string_view fieldName) {
76 const auto found = payload.find(std::string(fieldName));
77 return found != payload.end() && found->second.isString() ? found->second.asString() : std::string{};
78}
79
80std::string resourceName(std::string value) {
81 const auto slash = value.find_last_of("/\\");
82 return slash == std::string::npos ? value : value.substr(slash + 1);
83}
84
85TimelineItemVisual visualFor(std::string_view type) {
86 if (type == "animation:section") return TimelineItemVisual::Animation;
87 if (type == "combat:damage") return TimelineItemVisual::Damage;
88 if (type == "presentation:vfx" || type == "presentation:vfx-state") return TimelineItemVisual::Vfx;
89 if (type == "presentation:audio" || type == "presentation:audio-state") return TimelineItemVisual::Audio;
90 if (type == "gameplay:prefab-spawn") return TimelineItemVisual::Prefab;
91 if (type == "presentation:camera") return TimelineItemVisual::Camera;
92 if (type == "combat:hitbox-window") return TimelineItemVisual::Hitbox;
93 if (type == "combat:invulnerability-window" || type == "combat:guard-window") return TimelineItemVisual::Defense;
94 if (type == "input:combo-window" || type == "input:cancel-window") return TimelineItemVisual::Input;
95 if (type == "collision:ignore-window") return TimelineItemVisual::Collision;
96 if (type == "movement:root-motion-window") return TimelineItemVisual::Movement;
97 if (type == "presentation:parameter-curve") return TimelineItemVisual::Curve;
98 if (type.starts_with("gameplay:")) return TimelineItemVisual::Gameplay;
100}
101
102std::string detailFor(std::string_view type, const Value::Object& payload) {
103 if (type == "animation:section") return resourceName(payloadText(payload, "animationUri"));
104 if (type == "combat:damage") {
105 const auto amount = payload.find("amount");
106 if (amount != payload.end() && (amount->second.isInt64() || amount->second.isDouble())) {
107 std::ostringstream stream;
108 if (amount->second.isInt64())
109 stream << amount->second.asInt();
110 else
111 stream << amount->second.asDouble();
112 const auto damageType = payloadText(payload, "damageType");
113 return damageType.empty() ? stream.str() : damageType + " " + stream.str();
114 }
115 }
116 if (type == "presentation:vfx" || type == "presentation:vfx-state" ||
117 type == "presentation:audio" || type == "presentation:audio-state" ||
118 type == "gameplay:prefab-spawn")
119 return resourceName(payloadText(payload, "uri"));
120 if (type == "presentation:camera") return payloadText(payload, "cue");
121 if (type == "combat:hitbox-window") return payloadText(payload, "hitbox");
122 if (type == "input:combo-window") return payloadText(payload, "input");
123 if (type == "input:cancel-window") return payloadText(payload, "allows");
124 if (type == "combat:guard-window") return payloadText(payload, "mode");
125 if (type == "collision:ignore-window") return payloadText(payload, "channel");
126 if (type == "movement:root-motion-window") return payloadText(payload, "mode");
127 if (type == "presentation:parameter-curve") return payloadText(payload, "target");
128 if (type == "gameplay:event") return payloadText(payload, "tag");
129 return {};
130}
131
132void decorate(TimelineItemGeometry& geometry, const LogicalId& type, const Value::Object& payload,
133 const action::ActionNotifyRegistry& registry) {
134 geometry.visual = visualFor(type.format());
135 geometry.detail = detailFor(type.format(), payload);
136 if (geometry.visual == TimelineItemVisual::Animation) {
137 geometry.displayName = "Animation";
138 return;
139 }
140 auto descriptor = registry.descriptor(type.format());
141 geometry.displayName = descriptor ? descriptor.value().displayName : type.format();
142}
143
144} // namespace
145
146std::string_view timelineItemVisualName(TimelineItemVisual visual) noexcept {
147 switch (visual) {
148 case TimelineItemVisual::Animation: return "animation";
149 case TimelineItemVisual::Gameplay: return "gameplay";
150 case TimelineItemVisual::Damage: return "damage";
151 case TimelineItemVisual::Vfx: return "vfx";
152 case TimelineItemVisual::Audio: return "audio";
153 case TimelineItemVisual::Prefab: return "prefab";
154 case TimelineItemVisual::Camera: return "camera";
155 case TimelineItemVisual::Hitbox: return "hitbox";
156 case TimelineItemVisual::Defense: return "defense";
157 case TimelineItemVisual::Input: return "input";
158 case TimelineItemVisual::Collision: return "collision";
159 case TimelineItemVisual::Movement: return "movement";
160 case TimelineItemVisual::Curve: return "curve";
161 case TimelineItemVisual::Custom: return "custom";
162 }
163 return "custom";
164}
165
167 : editor_(editor), registry_(registry) {}
168
169Result<void> ActionTimelineWidget::setViewport(float width, float rowHeight, float labelWidth) {
170 if (!std::isfinite(width) || !std::isfinite(rowHeight) || !std::isfinite(labelWidth) || width <= 0.0f ||
171 rowHeight < 12.0f || labelWidth < 0.0f || labelWidth + 16.0f >= width)
172 return widgetError("editor.action.timeline.widget.viewport", "Timeline widget dimensions are invalid");
173 width_ = width;
174 rowHeight_ = rowHeight;
175 labelWidth_ = labelWidth;
176 return eve::editing::applied<void>();
177}
178
180 if (interval < Duration::zero())
181 return widgetError("editor.action.timeline.widget.snap", "Timeline snap interval must be non-negative");
182 snapInterval_ = interval;
183 return eve::editing::applied<void>();
184}
185
187 const Duration duration = editor_.target().timeline().duration;
188 if (start < Duration::zero() || end <= start || end > duration)
189 return widgetError("editor.action.timeline.widget.visible-range",
190 "Timeline visible range must be ordered and inside the asset duration");
191 visibleStart_ = start;
192 visibleEnd_ = end;
193 return eve::editing::applied<void>();
194}
195
196Result<void> ActionTimelineWidget::zoom(double factor, double normalizedAnchor) {
197 if (!std::isfinite(factor) || factor <= 0.0 || !std::isfinite(normalizedAnchor) || normalizedAnchor < 0.0 ||
198 normalizedAnchor > 1.0)
199 return widgetError("editor.action.timeline.widget.zoom", "Timeline zoom factor or anchor is invalid");
200 const Duration duration = editor_.target().timeline().duration;
201 const Duration start = visibleEnd_ > visibleStart_ ? visibleStart_ : Duration::zero();
202 const Duration end = visibleEnd_ > visibleStart_ ? visibleEnd_ : duration;
203 const double oldSpan = static_cast<double>(end.nanoseconds() - start.nanoseconds());
204 const double minimumSpan = std::max(1.0, static_cast<double>(duration.nanoseconds()) / 10000.0);
205 const auto newSpan = static_cast<std::int64_t>(
206 std::llround(std::clamp(oldSpan / factor, minimumSpan, static_cast<double>(duration.nanoseconds()))));
207 const auto anchor = start.nanoseconds() + static_cast<std::int64_t>(std::llround(oldSpan * normalizedAnchor));
208 auto newStart = anchor - static_cast<std::int64_t>(std::llround(newSpan * normalizedAnchor));
209 newStart = std::clamp<std::int64_t>(newStart, 0, duration.nanoseconds() - newSpan);
210 visibleStart_ = Duration::fromNanoseconds(newStart);
211 visibleEnd_ = Duration::fromNanoseconds(newStart + newSpan);
212 return eve::editing::applied<void>();
213}
214
216 const Duration duration = editor_.target().timeline().duration;
217 const Duration start = visibleEnd_ > visibleStart_ ? visibleStart_ : Duration::zero();
218 const Duration end = visibleEnd_ > visibleStart_ ? visibleEnd_ : duration;
219 const auto span = end.nanoseconds() - start.nanoseconds();
220 const auto next =
221 std::clamp<std::int64_t>(start.nanoseconds() + delta.nanoseconds(), 0, duration.nanoseconds() - span);
222 visibleStart_ = Duration::fromNanoseconds(next);
223 visibleEnd_ = Duration::fromNanoseconds(next + span);
224 return eve::editing::applied<void>();
225}
226
227float ActionTimelineWidget::timeToX(Duration time) const noexcept {
228 const auto duration = editor_.target().timeline().duration;
229 const auto start = visibleEnd_ > visibleStart_ ? visibleStart_ : Duration::zero();
230 const auto end = visibleEnd_ > visibleStart_ ? visibleEnd_ : duration;
231 const auto span = end.nanoseconds() - start.nanoseconds();
232 if (span <= 0) return labelWidth_;
233 const double fraction = static_cast<double>(time.nanoseconds() - start.nanoseconds()) / static_cast<double>(span);
234 return labelWidth_ + static_cast<float>(fraction * static_cast<double>(width_ - labelWidth_));
235}
236
237Duration ActionTimelineWidget::xToTime(float x) const noexcept {
238 const float clamped = std::clamp(x, labelWidth_, width_);
239 const float fraction = (clamped - labelWidth_) / (width_ - labelWidth_);
240 const auto assetDuration = editor_.target().timeline().duration;
241 const auto start = visibleEnd_ > visibleStart_ ? visibleStart_ : Duration::zero();
242 const auto end = visibleEnd_ > visibleStart_ ? visibleEnd_ : assetDuration;
243 const auto span = end.nanoseconds() - start.nanoseconds();
244 std::int64_t time =
245 start.nanoseconds() + static_cast<std::int64_t>(std::llround(static_cast<double>(fraction) * span));
246 if (snapInterval_.nanoseconds() > 0) {
247 const double steps = static_cast<double>(time) / static_cast<double>(snapInterval_.nanoseconds());
248 time = static_cast<std::int64_t>(std::llround(steps)) * snapInterval_.nanoseconds();
249 time = std::clamp<std::int64_t>(time, start.nanoseconds(), end.nanoseconds());
250 }
252}
253
254std::optional<Duration> ActionTimelineWidget::magneticSnap(Duration candidate,
255 const LogicalId& excludedItem) const noexcept {
256 constexpr double kSnapPixels = 8.0;
257 const auto& timeline = editor_.target().timeline();
258 const auto start = visibleEnd_ > visibleStart_ ? visibleStart_ : Duration::zero();
259 const auto end = visibleEnd_ > visibleStart_ ? visibleEnd_ : timeline.duration;
260 const auto selected = editor_.selectedItemIds();
261 const auto excluded = [&](const LogicalId& id) {
262 return id == excludedItem || std::find(selected.begin(), selected.end(), id) != selected.end();
263 };
264 const auto pixels = static_cast<double>(width_ - labelWidth_);
265 if (pixels <= 0.0) return std::nullopt;
266 const auto threshold = static_cast<std::int64_t>(
267 std::llround(static_cast<double>(end.nanoseconds() - start.nanoseconds()) * kSnapPixels / pixels));
268 std::optional<Duration> best;
269 auto bestDelta = threshold + 1;
270 const auto consider = [&](Duration target) {
271 const auto delta = target > candidate ? target.nanoseconds() - candidate.nanoseconds()
272 : candidate.nanoseconds() - target.nanoseconds();
273 if (delta <= threshold && delta < bestDelta) {
274 best = target;
275 bestDelta = delta;
276 }
277 };
278 consider(Duration::zero());
279 consider(editor_.previewTime());
280 for (const auto split : timeline.splitTimestamps) consider(split);
281 for (const auto& section : timeline.animationSections) {
282 if (excluded(section.id)) continue;
283 consider(section.start);
284 consider(section.end);
285 }
286 for (const auto& track : timeline.tracks) {
287 for (const auto& notify : track.notifies) {
288 if (!excluded(notify.id)) consider(notify.time);
289 }
290 for (const auto& state : track.states) {
291 if (excluded(state.id)) continue;
292 consider(state.start);
293 consider(state.end);
294 }
295 }
296 return best;
297}
298
301 result.width = width_;
302 result.audioWaveformsAvailable = eve::cap::query<action::IActionAudioWaveformProvider>() != nullptr;
303 const bool hasAnimationLane = !editor_.target().timeline().animationSections.empty();
304 const std::size_t rowOffset = hasAnimationLane ? 1U : 0U;
305 result.height = rowHeight_ * static_cast<float>(editor_.target().timeline().tracks.size() + rowOffset);
306 result.playheadX = timeToX(editor_.previewTime());
307 const auto selected = editor_.selectedItemIds();
308 auto isSelected = [&](const LogicalId& id) {
309 return std::find(selected.begin(), selected.end(), id) != selected.end();
310 };
311 if (snapInterval_.nanoseconds() > 0) {
312 const std::int64_t duration = editor_.target().timeline().duration.nanoseconds();
313 const std::int64_t interval = snapInterval_.nanoseconds();
314 const auto visibleStart = visibleEnd_ > visibleStart_ ? visibleStart_.nanoseconds() : 0;
315 const auto visibleEnd = visibleEnd_ > visibleStart_ ? visibleEnd_.nanoseconds() : duration;
316 std::int64_t time = (visibleStart / interval) * interval;
317 if (time < visibleStart) time += interval;
318 for (std::int64_t index = time / interval; time <= visibleEnd; time += interval, ++index) {
320 result.rulerTicks.push_back({tick, timeToX(tick), index % 5 == 0});
321 if (duration - time < interval) break;
322 }
323 }
324 if (hasAnimationLane) {
325 const auto animationTrack = *LogicalId::fromParts("action-track", "animation");
326 const auto animationType = *LogicalId::fromParts("animation", "section");
327 for (const auto& section : editor_.target().timeline().animationSections) {
328 Duration start = section.start;
329 Duration end = section.end;
330 if (drag_ && drag_->itemId == section.id) {
331 start = drag_->previewStart;
332 end = drag_->previewEnd;
333 }
334 result.items.push_back({animationTrack, section.id, animationType, true, isSelected(section.id),
335 timeToX(start), std::max(timeToX(end), timeToX(start) + 4.0f), 3.0f,
336 rowHeight_ - 3.0f});
337 decorate(result.items.back(), animationType,
338 {{"animationUri", Value(section.animationUri)}}, registry_);
339 }
340 }
341 for (std::size_t row = 0; row < editor_.target().timeline().tracks.size(); ++row) {
342 const auto& track = editor_.target().timeline().tracks[row];
343 const float top = static_cast<float>(row + rowOffset) * rowHeight_;
344 for (const auto& notify : track.notifies) {
345 Duration time = notify.time;
346 if (drag_ && drag_->itemId == notify.id) time = drag_->previewStart;
347 const float center = timeToX(time);
348 result.items.push_back({track.id, notify.id, notify.type, false, isSelected(notify.id),
349 center - kNotifyWidth * 0.5f, center + kNotifyWidth * 0.5f, top + 3.0f,
350 top + rowHeight_ - 3.0f});
351 decorate(result.items.back(), notify.type, notify.payload, registry_);
352 }
353 for (const auto& state : track.states) {
354 Duration start = state.start;
355 Duration end = state.end;
356 if (drag_ && drag_->itemId == state.id) {
357 start = drag_->previewStart;
358 end = drag_->previewEnd;
359 }
360 const float minimum = timeToX(start);
361 const float maximum = std::max(timeToX(end), minimum + 4.0f);
362 result.items.push_back({track.id, state.id, state.type, true, isSelected(state.id), minimum, maximum,
363 top + 3.0f, top + rowHeight_ - 3.0f});
364 decorate(result.items.back(), state.type, state.payload, registry_);
365 }
366 }
367 return result;
368}
369
370void ActionTimelineWidget::draw(IEditorOverlay& overlay) const {
371 const auto& timeline = editor_.target().timeline();
372 const bool hasAnimationLane = !timeline.animationSections.empty();
373 const std::size_t rowOffset = hasAnimationLane ? 1U : 0U;
374 if (hasAnimationLane) {
375 overlay.rectangle({0.0f, 0.0f, 0.0f}, {width_, rowHeight_, 0.0f}, {0x20252cffU, 1.0f, true});
376 overlay.line({0.0f, rowHeight_, 0.0f}, {width_, rowHeight_, 0.0f}, {0x4a5260ffU, 1.0f, false});
377 overlay.text({4.0f, 4.0f, 0.0f}, "Animation", {0xd8dee9ffU, 1.0f, false});
378 }
379 for (std::size_t row = 0; row < timeline.tracks.size(); ++row) {
380 const float top = static_cast<float>(row + rowOffset) * rowHeight_;
381 const float bottom = top + rowHeight_;
382 overlay.rectangle({0.0f, top, 0.0f}, {width_, bottom, 0.0f}, {0x20252cffU, 1.0f, true});
383 overlay.line({0.0f, bottom, 0.0f}, {width_, bottom, 0.0f}, {0x4a5260ffU, 1.0f, false});
384 overlay.text({4.0f, top + 4.0f, 0.0f}, timeline.tracks[row].label,
385 {timeline.tracks[row].muted ? 0x7f8792ffU : 0xd8dee9ffU, 1.0f, false});
386 }
387 for (const auto& item : layout().items) {
388 const bool animationSection = item.type.format() == "animation:section";
389 const unsigned int color =
390 item.selected ? 0xf2b84bffU : (animationSection ? 0xa66bd4ffU : (item.state ? 0x568bd7ffU : 0x61c28bffU));
391 overlay.rectangle({item.minimumX, item.minimumY, 0.0f}, {item.maximumX, item.maximumY, 0.0f},
392 {color, 1.0f, true});
393 if (item.maximumX - item.minimumX >= 48.0f)
394 overlay.text({item.minimumX + 5.0f, item.minimumY + 3.0f, 0.0f}, item.displayName,
395 {0xffffffffU, 1.0f, false});
396 if (item.state && item.type.format() == "presentation:audio-state") {
397 const auto view = findItem(timeline, item.itemId);
398 auto* waveforms = eve::cap::query<action::IActionAudioWaveformProvider>();
399 if (view && waveforms) {
402 const auto pixels = static_cast<std::size_t>(
403 std::max(1.0f, std::floor(item.maximumX - item.minimumX)));
404 if (binding) {
406 std::move(binding).takeValue(), difference(view->end, view->start),
407 std::min(pixels, kMaximumWaveformBuckets)};
408 auto waveform = waveforms->waveform(request);
409 if (waveform && !waveform.value().buckets.empty()) {
410 const float middle = (item.minimumY + item.maximumY) * 0.5f;
411 const float amplitude = std::max(1.0f, (item.maximumY - item.minimumY) * 0.42f);
412 const float step = (item.maximumX - item.minimumX) /
413 static_cast<float>(waveform.value().buckets.size());
414 for (std::size_t index = 0; index < waveform.value().buckets.size(); ++index) {
415 const auto& bucket = waveform.value().buckets[index];
416 const float x = item.minimumX + (static_cast<float>(index) + 0.5f) * step;
417 overlay.line({x, middle - std::clamp(bucket.maximum, -1.0f, 1.0f) * amplitude, 0.0f},
418 {x, middle - std::clamp(bucket.minimum, -1.0f, 1.0f) * amplitude, 0.0f},
419 {0xe7f4ffffU, 1.0f, false});
420 }
421 }
422 }
423 }
424 }
425 if (item.state && item.type.format() == "presentation:parameter-curve") {
426 const auto view = findItem(timeline, item.itemId);
429 Diagnostic::error(DiagnosticCode::NotFound, "curve item is unavailable"));
430 if (binding) {
431 double minimum = binding.value().keys.front().value;
432 double maximum = minimum;
433 for (const auto& key : binding.value().keys) {
434 minimum = std::min(minimum, key.value);
435 maximum = std::max(maximum, key.value);
436 }
437 if (maximum - minimum < 1e-9) {
438 minimum -= 0.5;
439 maximum += 0.5;
440 }
441 const auto point = [&](double time, double value) {
442 const float x = item.minimumX + static_cast<float>(time) * (item.maximumX - item.minimumX);
443 const float normalized = static_cast<float>((value - minimum) / (maximum - minimum));
444 const float y = item.maximumY - 2.0f - normalized * (item.maximumY - item.minimumY - 4.0f);
445 return OverlayPoint{x, y, 0.0f};
446 };
447 const auto segments = std::min<std::size_t>(
448 kMaximumCurveSegments,
449 std::max<std::size_t>(2, static_cast<std::size_t>(item.maximumX - item.minimumX)));
450 auto previous = point(0.0, binding.value().sample(0.0));
451 for (std::size_t segment = 1; segment <= segments; ++segment) {
452 const double time = static_cast<double>(segment) / static_cast<double>(segments);
453 const auto current = point(time, binding.value().sample(time));
454 overlay.line(previous, current, {0xffdc7affU, 1.5f, false});
456 }
457 for (const auto& key : binding.value().keys)
458 overlay.circle(point(key.time, key.value), 2.5f,
459 {item.selected ? 0xffffffffU : 0xffdc7affU, 1.0f, true});
460 }
461 }
462 if (item.state) {
463 overlay.line({item.minimumX, item.minimumY, 0.0f}, {item.minimumX, item.maximumY, 0.0f},
464 {0xffffffffU, 2.0f, false});
465 overlay.line({item.maximumX, item.minimumY, 0.0f}, {item.maximumX, item.maximumY, 0.0f},
466 {0xffffffffU, 2.0f, false});
467 }
468 }
469 const float height = rowHeight_ * static_cast<float>(timeline.tracks.size() + rowOffset);
470 overlay.line({timeToX(editor_.previewTime()), 0.0f, 0.0f}, {timeToX(editor_.previewTime()), height, 0.0f},
471 {0xff5b5bffU, 2.0f, false});
472}
473
474std::optional<TimelineHit> ActionTimelineWidget::hitTest(float x, float y) const {
475 const auto projected = layout();
476 const auto containsY = [&](const TimelineItemGeometry& item) {
477 return y >= item.minimumY && y <= item.maximumY;
478 };
479 // Resize handles remain the most precise affordance even when items overlap.
480 for (auto it = projected.items.rbegin(); it != projected.items.rend(); ++it) {
481 if (!containsY(*it) || !it->state) continue;
482 if (it->state && std::abs(x - it->minimumX) <= kHandleRadius)
483 return TimelineHit{it->itemId, TimelineHitPart::StartHandle};
484 if (it->state && std::abs(x - it->maximumX) <= kHandleRadius)
485 return TimelineHit{it->itemId, TimelineHitPart::EndHandle};
486 }
487 // A point notify drawn over a state span must remain directly selectable.
488 for (auto it = projected.items.rbegin(); it != projected.items.rend(); ++it) {
489 if (!containsY(*it) || it->state) continue;
490 if (x >= it->minimumX && x <= it->maximumX) return TimelineHit{it->itemId, TimelineHitPart::Body};
491 }
492 for (auto it = projected.items.rbegin(); it != projected.items.rend(); ++it) {
493 if (!containsY(*it) || !it->state) continue;
494 if (x >= it->minimumX && x <= it->maximumX) return TimelineHit{it->itemId, TimelineHitPart::Body};
495 }
496 return std::nullopt;
497}
498
499Result<void> ActionTimelineWidget::pointerDown(float x, float y, bool additiveSelection) {
500 if (drag_) return widgetError("editor.action.timeline.widget.drag-active", "A timeline drag is already active");
501 const auto hit = hitTest(x, y);
502 if (!hit) {
503 if (!additiveSelection) editor_.clearSelection();
504 return eve::editing::noOp();
505 }
506 auto item = findItem(editor_.target().timeline(), hit->itemId);
507 if (!item)
508 return widgetError("editor.action.timeline.widget.item-missing", "Timeline item no longer exists",
509 EditorStatus::Conflict);
510 if (item->locked) return widgetError("editor.action.timeline.track-locked", "Action track is locked");
511 const auto selectedIds = editor_.selectedItemIds();
512 const bool alreadySelected = std::find(selectedIds.begin(), selectedIds.end(), item->itemId) != selectedIds.end();
513 auto selected = editor_.selectItem(item->itemId, additiveSelection || alreadySelected);
514 if (!selected.ok()) return selected;
515 drag_ = DragState{item->itemId, hit->part, xToTime(x), item->start, item->end, item->start, item->end, item->state};
516 return eve::editing::applied<void>();
517}
518
519Result<void> ActionTimelineWidget::updateDrag(float x) {
520 if (!drag_) return widgetError("editor.action.timeline.widget.drag-missing", "No timeline drag is active");
521 const Duration cursor = xToTime(x);
522 const Duration duration = editor_.target().timeline().duration;
523 const Duration delta = difference(cursor, drag_->anchorTime);
524 if (!drag_->state) {
525 auto moved = drag_->originalStart.tryAdd(delta);
526 if (!moved) return widgetError("editor.action.timeline.widget.drag-overflow", "Notify drag overflowed");
527 const auto candidate = std::clamp(moved.value(), Duration::zero(), duration);
528 drag_->previewStart = magneticSnap(candidate, drag_->itemId).value_or(candidate);
529 drag_->previewEnd = drag_->previewStart;
530 return eve::editing::applied<void>();
531 }
532 if (drag_->part == TimelineHitPart::StartHandle) {
533 auto moved = drag_->originalStart.tryAdd(delta);
534 if (!moved) return widgetError("editor.action.timeline.widget.drag-overflow", "State start drag overflowed");
535 const auto candidate = std::clamp(moved.value(), Duration::zero(), drag_->originalEnd);
536 drag_->previewStart = magneticSnap(candidate, drag_->itemId).value_or(candidate);
537 drag_->previewStart = std::min(drag_->previewStart, drag_->originalEnd);
538 drag_->previewEnd = drag_->originalEnd;
539 return eve::editing::applied<void>();
540 }
541 if (drag_->part == TimelineHitPart::EndHandle) {
542 auto moved = drag_->originalEnd.tryAdd(delta);
543 if (!moved) return widgetError("editor.action.timeline.widget.drag-overflow", "State end drag overflowed");
544 drag_->previewStart = drag_->originalStart;
545 const auto candidate = std::clamp(moved.value(), drag_->originalStart, duration);
546 drag_->previewEnd = magneticSnap(candidate, drag_->itemId).value_or(candidate);
547 drag_->previewEnd = std::max(drag_->previewEnd, drag_->originalStart);
548 return eve::editing::applied<void>();
549 }
550 const auto span = drag_->originalEnd.nanoseconds() - drag_->originalStart.nanoseconds();
551 auto moved = drag_->originalStart.tryAdd(delta);
552 if (!moved) return widgetError("editor.action.timeline.widget.drag-overflow", "State drag overflowed");
553 const auto latestStart = Duration::fromNanoseconds(duration.nanoseconds() - span);
554 drag_->previewStart = std::clamp(moved.value(), Duration::zero(), latestStart);
555 drag_->previewEnd = Duration::fromNanoseconds(drag_->previewStart.nanoseconds() + span);
556 const auto snappedStart = magneticSnap(drag_->previewStart, drag_->itemId);
557 const auto snappedEnd = magneticSnap(drag_->previewEnd, drag_->itemId);
558 Duration adjustment = Duration::zero();
559 if (snappedStart && snappedEnd) {
560 const auto startDelta = difference(*snappedStart, drag_->previewStart);
561 const auto endDelta = difference(*snappedEnd, drag_->previewEnd);
562 adjustment = std::abs(startDelta.nanoseconds()) <= std::abs(endDelta.nanoseconds()) ? startDelta : endDelta;
563 } else if (snappedStart) {
564 adjustment = difference(*snappedStart, drag_->previewStart);
565 } else if (snappedEnd) {
566 adjustment = difference(*snappedEnd, drag_->previewEnd);
567 }
568 const auto adjustedStart = drag_->previewStart.tryAdd(adjustment);
569 if (adjustedStart && adjustedStart.value() >= Duration::zero() && adjustedStart.value() <= latestStart) {
570 drag_->previewStart = adjustedStart.value();
571 drag_->previewEnd = Duration::fromNanoseconds(drag_->previewStart.nanoseconds() + span);
572 }
573 return eve::editing::applied<void>();
574}
575
576Result<void> ActionTimelineWidget::pointerMove(float x) { return updateDrag(x); }
577
578Result<void> ActionTimelineWidget::pointerUp(float x) {
579 auto updated = updateDrag(x);
580 if (!updated.ok()) return updated;
581 const DragState completed = *drag_;
582 drag_.reset();
583 const auto item = findItem(editor_.target().timeline(), completed.itemId);
584 if (completed.part == TimelineHitPart::Body && editor_.selectionCount() > 1)
585 return editor_.moveSelection(difference(completed.previewStart, completed.originalStart));
586 if (item && item->animationSection) {
587 if (completed.part == TimelineHitPart::Body)
588 return editor_.moveAnimationSection(completed.itemId,
589 difference(completed.previewStart, completed.originalStart));
590 const auto section = std::find_if(editor_.target().timeline().animationSections.begin(),
591 editor_.target().timeline().animationSections.end(),
592 [&](const auto& value) { return value.id == completed.itemId; });
593 if (section == editor_.target().timeline().animationSections.end())
594 return widgetError("editor.action.timeline.animation-section-not-found",
595 "Animation section no longer exists", EditorStatus::Conflict);
596 return editor_.resizeAnimationSection(completed.itemId, completed.previewStart, completed.previewEnd,
597 section->blendIn);
598 }
599 if (completed.state && completed.part != TimelineHitPart::Body)
600 return editor_.resizeState(completed.itemId, completed.previewStart, completed.previewEnd);
601 return editor_.moveItem(completed.itemId, difference(completed.previewStart, completed.originalStart));
602}
603
604Result<void> ActionTimelineWidget::seek(float x) { return editor_.seek(xToTime(x)); }
605
606Result<void> ActionTimelineWidget::inspectSelection(IEditorInspector& inspector) {
607 const auto selected = editor_.selectedItemIds();
608 if (selected.size() > 1) {
609 auto range = editor_.selectionRange();
610 if (!range.ok()) return Result<void>::failure(range.status());
611 float startSeconds = static_cast<float>(range.value().start.seconds());
612 float endSeconds = static_cast<float>(range.value().end.seconds());
613 const float maximum = static_cast<float>(editor_.target().timeline().duration.seconds());
614 inspector.beginGroup("action.timeline.selection", "Selection");
615 bool changed = inspector.scalar("start", "Start", startSeconds, 0.0f, maximum);
616 changed = inspector.scalar("end", "End", endSeconds, 0.0f, maximum) || changed;
617 inspector.endGroup();
618 if (!changed) return eve::editing::noOp();
619 auto start = Duration::fromSeconds(startSeconds);
620 auto end = Duration::fromSeconds(endSeconds);
621 if (!start || !end)
622 return widgetError("editor.action.timeline.widget.selection-time-invalid",
623 "Selection bounds are invalid");
624 return editor_.scaleSelection(start.value(), end.value());
625 }
626 if (selected.empty()) return eve::editing::noOp();
627 auto item = findItem(editor_.target().timeline(), selected.front());
628 if (!item)
629 return widgetError("editor.action.timeline.widget.item-missing", "Selected item no longer exists",
630 EditorStatus::Conflict);
631
632 if (item->animationSection) {
633 const auto section = std::find_if(editor_.target().timeline().animationSections.begin(),
634 editor_.target().timeline().animationSections.end(),
635 [&](const auto& value) { return value.id == item->itemId; });
636 if (section == editor_.target().timeline().animationSections.end())
637 return widgetError("editor.action.timeline.animation-section-not-found",
638 "Animation section no longer exists", EditorStatus::Conflict);
639 float startSeconds = static_cast<float>(section->start.seconds());
640 float endSeconds = static_cast<float>(section->end.seconds());
641 float blendSeconds = static_cast<float>(section->blendIn.seconds());
642 float sourceStartSeconds = static_cast<float>(section->sourceStart.seconds());
643 float sourceEndSeconds = static_cast<float>(section->sourceEnd.seconds());
644 std::string animationUri = section->animationUri;
645 std::string blendCurve = std::string(action::actionBlendCurveName(section->blendCurve));
646 const float maximum = static_cast<float>(editor_.target().timeline().duration.seconds());
647 inspector.beginGroup("action.timeline.animation-section", "Animation Section");
648 bool changed = inspector.scalar("start", "Start", startSeconds, 0.0f, maximum);
649 changed = inspector.scalar("end", "End", endSeconds, 0.0f, maximum) || changed;
650 changed = inspector.scalar("blendIn", "Blend In", blendSeconds, 0.0f, maximum) || changed;
651 changed =
652 inspector.scalar("sourceStart", "Trim In", sourceStartSeconds, 0.0f, std::numeric_limits<float>::max()) ||
653 changed;
654 changed = inspector.scalar("sourceEnd", "Trim Out (0 = clip end)", sourceEndSeconds, 0.0f,
655 std::numeric_limits<float>::max()) ||
656 changed;
657 changed = inspector.string("blendCurve", "Blend Curve", blendCurve) || changed;
658 changed = inspector.string("animationUri", "Animation", animationUri) || changed;
659 inspector.endGroup();
660 if (!changed) return eve::editing::applied<void>();
661 auto start = Duration::fromSeconds(startSeconds);
662 auto end = Duration::fromSeconds(endSeconds);
663 auto blend = Duration::fromSeconds(blendSeconds);
664 auto sourceStart = Duration::fromSeconds(sourceStartSeconds);
665 auto sourceEnd = Duration::fromSeconds(sourceEndSeconds);
666 auto parsedCurve = action::actionBlendCurveFromName(blendCurve);
667 if (!start || !end || !blend || !sourceStart || !sourceEnd || !parsedCurve)
668 return widgetError("editor.action.timeline.widget.time-invalid", "Animation section time is invalid");
669 action::ActionAnimationSection edited = *section;
670 edited.start = start.value();
671 edited.end = end.value();
672 edited.blendIn = blend.value();
673 edited.sourceStart = sourceStart.value();
674 edited.sourceEnd = sourceEnd.value();
675 edited.blendCurve = *parsedCurve;
676 edited.animationUri = std::move(animationUri);
677 return editor_.editAnimationSectionFull(std::move(edited));
678 }
679
680 if (item->state && item->type.format() == "presentation:parameter-curve") {
682 if (!binding)
683 return widgetError("editor.action.timeline.widget.parameter-curve-invalid",
684 "Selected parameter curve payload is invalid");
685 std::string target = binding.value().target.format();
686 std::string operation = std::string(action::actionParameterOperationName(binding.value().operation));
687 Value::Array keys;
688 bool changed = false;
689 inspector.beginGroup("action.timeline.parameter-curve", "Parameter Curve");
690 changed = inspector.string("target", "Target", target) || changed;
691 changed = inspector.string("operation", "Operation", operation) || changed;
692 for (std::size_t index = 0; index < binding.value().keys.size(); ++index) {
693 auto key = binding.value().keys[index];
694 float time = static_cast<float>(key.time);
695 float value = static_cast<float>(key.value);
696 float inTangent = static_cast<float>(key.inTangent);
697 float outTangent = static_cast<float>(key.outTangent);
698 std::string interpolation = std::string(action::actionParameterInterpolationName(key.interpolation));
699 inspector.beginGroup("action.timeline.parameter-key." + std::to_string(index),
700 "Key " + std::to_string(index));
701 const bool endpoint = index == 0 || index + 1 == binding.value().keys.size();
702 const float minimumTime = endpoint ? static_cast<float>(key.time)
703 : static_cast<float>(binding.value().keys[index - 1].time);
704 const float maximumTime = endpoint ? static_cast<float>(key.time)
705 : static_cast<float>(binding.value().keys[index + 1].time);
706 changed = inspector.scalar("time", "Time", time, minimumTime, maximumTime) || changed;
707 changed = inspector.scalar("value", "Value", value, std::numeric_limits<float>::lowest(),
708 std::numeric_limits<float>::max()) || changed;
709 changed = inspector.scalar("inTangent", "In Tangent", inTangent,
710 std::numeric_limits<float>::lowest(),
711 std::numeric_limits<float>::max()) || changed;
712 changed = inspector.scalar("outTangent", "Out Tangent", outTangent,
713 std::numeric_limits<float>::lowest(),
714 std::numeric_limits<float>::max()) || changed;
715 changed = inspector.string("interpolation", "Interpolation", interpolation) || changed;
716 inspector.endGroup();
717 keys.emplace_back(Value::Object{{"time", static_cast<double>(time)},
718 {"value", static_cast<double>(value)},
719 {"inTangent", static_cast<double>(inTangent)},
720 {"outTangent", static_cast<double>(outTangent)},
721 {"interpolation", std::move(interpolation)}});
722 }
723 inspector.endGroup();
724 if (!changed) return eve::editing::applied<void>();
725 auto payload = item->payload;
726 payload["target"] = std::move(target);
727 payload["operation"] = std::move(operation);
728 payload["keys"] = Value(std::move(keys));
730 if (!validated)
731 return widgetError("editor.action.timeline.widget.parameter-curve-contract",
732 "Parameter curve fields violate the block contract");
733 return editor_.updateItem(item->itemId, item->type,
734 validated.value().toPayload(std::move(payload)));
735 }
736
737 float startSeconds = static_cast<float>(item->start.seconds());
738 float endSeconds = static_cast<float>(item->end.seconds());
739 std::string type = item->type.format();
740 auto payloadJson = Value(item->payload).toJson();
741 if (!payloadJson)
742 return widgetError("editor.action.timeline.widget.payload-encode", "Could not encode item payload");
743 std::string payload = payloadJson.value();
744
745 inspector.beginGroup("action.timeline.item", item->state ? "Notify State" : "Notify");
746 const float maximum = static_cast<float>(editor_.target().timeline().duration.seconds());
747 bool timingChanged = inspector.scalar("start", item->state ? "Start" : "Time", startSeconds, 0.0f, maximum);
748 if (item->state) timingChanged = inspector.scalar("end", "End", endSeconds, 0.0f, maximum) || timingChanged;
749 const bool typeChanged = inspector.string("type", "Type", type);
750 const bool payloadChanged = inspector.string("payload", "Payload JSON", payload);
751 inspector.endGroup();
752
753 if (!timingChanged && !typeChanged && !payloadChanged) return eve::editing::applied<void>();
754 Result<Duration> parsedStart =
755 timingChanged ? Duration::fromSeconds(startSeconds) : Result<Duration>::success(item->start);
756 Result<Duration> parsedEnd =
757 timingChanged ? Duration::fromSeconds(endSeconds) : Result<Duration>::success(item->end);
758 if (!parsedStart || !parsedEnd)
759 return widgetError("editor.action.timeline.widget.time-invalid", "Inspector time is invalid");
760 auto parsedType = LogicalId::parse(type);
761 if (!parsedType) return widgetError("editor.action.timeline.widget.type-invalid", "Notify type is invalid");
762 auto parsedPayload = Value::fromJson(payload);
763 if (!parsedPayload || !parsedPayload.value().getIf<Value::Object>())
764 return widgetError("editor.action.timeline.widget.payload-invalid", "Payload must be a JSON object");
765 auto descriptor = registry_.descriptor(type);
766 if (!descriptor)
767 return widgetError("editor.action.timeline.widget.type-unregistered", "Notify type is not registered");
768 const auto expected = item->state ? action::ActionNotifyShape::State : action::ActionNotifyShape::Instant;
769 if (descriptor.value().shape != expected)
770 return widgetError("editor.action.timeline.widget.shape-mismatch", "Notify type shape does not match item");
771 const auto* object = parsedPayload.value().getIf<Value::Object>();
774 item->trackId,
775 item->itemId,
776 *parsedType,
777 parsedStart.value(),
778 *object};
779 if (!registry_.validate(event))
780 return widgetError("editor.action.timeline.widget.payload-contract", "Payload violates notify contract");
781 return editor_.editItem(item->itemId, parsedStart.value(), item->state ? parsedEnd.value() : parsedStart.value(),
782 std::move(*parsedType), *object);
783}
784
785std::vector<TimelineWidgetCommandDescriptor> ActionTimelineWidget::commands() const {
786 const bool selected = editor_.selectionCount() > 0;
787 const bool multi = editor_.selectionCount() > 1;
788 return {{TimelineWidgetCommand::Copy, "Copy", "Ctrl+C", selected},
789 {TimelineWidgetCommand::Paste, "Paste at Playhead", "Ctrl+V", clipboardAnchor_.has_value()},
790 {TimelineWidgetCommand::DeleteSelection, "Delete", "Delete", selected},
791 {TimelineWidgetCommand::AlignSelectionStart, "Align Starts", "Shift+[", multi},
792 {TimelineWidgetCommand::AlignSelectionEnd, "Align Ends", "Shift+]", multi},
793 {TimelineWidgetCommand::Undo, "Undo", "Ctrl+Z", editor_.canUndo()},
794 {TimelineWidgetCommand::Redo, "Redo", "Ctrl+Y", editor_.canRedo()},
795 {TimelineWidgetCommand::PlayPause, editor_.playing() ? "Pause" : "Play", "Space", true}};
796}
797
798Result<void> ActionTimelineWidget::invoke(TimelineWidgetCommand command) {
799 switch (command) {
800 case TimelineWidgetCommand::Copy: {
801 const auto selected = editor_.selectedItemIds();
802 if (selected.empty())
803 return widgetError("editor.action.timeline.selection-empty", "No timeline items are selected");
804 std::optional<Duration> earliest;
805 for (const auto& id : selected) {
806 const auto item = findItem(editor_.target().timeline(), id);
807 if (item && (!earliest || item->start < *earliest)) earliest = item->start;
808 }
809 auto copied = editor_.copySelection();
810 if (copied.ok()) clipboardAnchor_ = earliest;
811 return adapt(copied, "editor.action.timeline.widget.copy", "Could not copy timeline selection");
812 }
813 case TimelineWidgetCommand::Paste: {
814 if (!clipboardAnchor_)
815 return widgetError("editor.action.timeline.clipboard-empty", "Action timeline clipboard is empty");
816 return adapt(editor_.paste(difference(editor_.previewTime(), *clipboardAnchor_)),
817 "editor.action.timeline.widget.paste", "Could not paste timeline selection");
818 }
819 case TimelineWidgetCommand::DeleteSelection: return editor_.deleteSelection();
820 case TimelineWidgetCommand::AlignSelectionStart: return editor_.alignSelectionStart();
821 case TimelineWidgetCommand::AlignSelectionEnd: return editor_.alignSelectionEnd();
822 case TimelineWidgetCommand::Undo: {
823 auto result = editor_.undo();
824 if (result.ok()) return eve::editing::applied<void>();
825 return Result<void>::failure(result.status());
826 }
827 case TimelineWidgetCommand::Redo: {
828 auto result = editor_.redo();
829 if (result.ok()) return eve::editing::applied<void>();
830 return Result<void>::failure(result.status());
831 }
832 case TimelineWidgetCommand::PlayPause:
833 if (editor_.playing())
834 editor_.pause();
835 else
836 editor_.play();
837 return eve::editing::applied<void>();
838 }
839 return widgetError("editor.action.timeline.widget.command", "Timeline command is unsupported",
840 EditorStatus::Unsupported);
841}
842
843Result<void> ActionTimelineWidget::handleShortcut(std::string_view shortcut) {
844 if (shortcut == "Ctrl+C") return invoke(TimelineWidgetCommand::Copy);
845 if (shortcut == "Ctrl+V") return invoke(TimelineWidgetCommand::Paste);
846 if (shortcut == "Delete" || shortcut == "Backspace") return invoke(TimelineWidgetCommand::DeleteSelection);
847 if (shortcut == "Shift+[") return invoke(TimelineWidgetCommand::AlignSelectionStart);
848 if (shortcut == "Shift+]") return invoke(TimelineWidgetCommand::AlignSelectionEnd);
849 if (shortcut == "Ctrl+Z") return invoke(TimelineWidgetCommand::Undo);
850 if (shortcut == "Ctrl+Y" || shortcut == "Ctrl+Shift+Z") return invoke(TimelineWidgetCommand::Redo);
851 if (shortcut == "Space") return invoke(TimelineWidgetCommand::PlayPause);
852 return widgetError("editor.action.timeline.widget.shortcut", "Timeline shortcut is unsupported",
853 EditorStatus::Unsupported);
854}
855
856std::vector<action::ActionNotifyDescriptor> ActionTimelineWidget::insertableTypes(
858 auto descriptors = registry_.descriptors();
859 std::erase_if(descriptors, [&](const auto& descriptor) { return descriptor.shape != shape; });
860 return descriptors;
861}
862
863LogicalId ActionTimelineWidget::generatedItemId() {
864 for (;;) {
865 auto id = LogicalId::fromParts("editor", "timeline-item." + std::to_string(++generatedSequence_));
866 if (!id) continue;
867 if (!findItem(editor_.target().timeline(), *id)) return std::move(*id);
868 }
869}
870
871Result<void> ActionTimelineWidget::addNotifyAtCursor(const LogicalId& trackId, std::string_view type,
873 auto parsed = LogicalId::parse(type);
874 if (!parsed) return widgetError("editor.action.timeline.widget.type-invalid", "Notify type is invalid");
876 action::ActionTimelineEventKind::Notify, trackId, generatedItemId(), *parsed, editor_.previewTime(), payload};
877 auto valid = registry_.validate(event);
878 if (!valid) return widgetError("editor.action.timeline.widget.notify-invalid", "Notify payload is invalid");
879 return editor_.addNotify(trackId, {event.itemId, event.type, event.time, std::move(payload)});
880}
881
882Result<void> ActionTimelineWidget::addStateAtCursor(const LogicalId& trackId, std::string_view type,
884 if (duration < Duration::zero())
885 return widgetError("editor.action.timeline.widget.state-duration", "Notify-state duration is negative");
886 auto parsed = LogicalId::parse(type);
887 if (!parsed) return widgetError("editor.action.timeline.widget.type-invalid", "Notify type is invalid");
888 auto end = editor_.previewTime().tryAdd(duration);
889 if (!end || end.value() > editor_.target().timeline().duration)
890 return widgetError("editor.action.timeline.widget.state-range", "Notify-state exceeds the timeline");
892 trackId,
893 generatedItemId(),
894 *parsed,
895 editor_.previewTime(),
896 payload};
897 auto valid = registry_.validate(event);
898 if (!valid) return widgetError("editor.action.timeline.widget.state-invalid", "Notify-state payload is invalid");
899 return editor_.addState(trackId, {event.itemId, event.type, event.time, end.value(), std::move(payload)});
900}
901
902} // namespace eve::editor
Typed audio action-block payload contract.
Optional audio waveform projection for action editors.
LogicalId target
ActionParameterOperation operation
double value
Typed continuous parameter-curve action block.
LogicalId trackId
Value::Object payload
bool locked
Duration start
bool animationSection
LogicalId itemId
Renderer-neutral interactive action timeline widget.
SQInteger top
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float duration
std::string descriptor
Vec3 projected
Definition CaveMesh.cpp:122
bool split
Definition CaveMesh.cpp:123
Vec3 anchor
Definition CaveMesh.cpp:90
std::string layout
std::string message
float maximum[3]
float minimum[3]
const GltfImportRequest & request
std::uint32_t key
ShaderImageInput shape
wgpu::PopErrorScopeStatus status
HexVec3 left
HexVec3 right
float blend
std::uint32_t height
std::uint32_t width
Range range
std::vector< Track > tracks
bool valid
graphics::Canvas * previous
eve::action::ActionVfxBinding binding
std::array< std::uint64_t, kPixelChunkSize *kPixelChunkSize > updated
std::string id
Definition PlayHost.cpp:108
bool hit
int steps
uint8_t * pixels
glm::mat4 view
bool found
double current
std::string damageType
std::map< Cell, int > best
SimulationTick tick
std::size_t cursor
Json object
float step
Definition TreeMesh.cpp:314
std::string payloadText
Definition UISystem.cpp:72
uint32_t index
glm::vec3 point
float bottom
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Signed, fixed-resolution simulation duration in nanoseconds.
Definition Time.h:43
constexpr std::int64_t nanoseconds() const noexcept
Return the exact signed nanosecond representation.
Definition Time.h:69
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
Definition Time.cpp:16
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
Definition Time.h:55
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
Human-readable, scoped identifier in the form namespace:name.
Definition Identity.h:411
static std::optional< LogicalId > fromParts(std::string_view namespaceName, std::string_view name)
Builds a logical ID from separate namespace and name components.
Definition Identity.cpp:48
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
Definition Value.cpp:67
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
Canonical notify descriptor owner and runtime handler router.
Result< void > validate(const ActionTimelineEvent &event) const
Validate type, shape and required payload fields.
std::vector< ActionNotifyDescriptor > descriptors() const
Return descriptors in lexical type order.
Result< ActionNotifyDescriptor > descriptor(std::string_view type) const
Find an owning descriptor copy.
Timeline editing controller shared by developer and in-game editors.
Duration previewTime() const noexcept
Current deterministic preview cursor.
std::vector< LogicalId > selectedItemIds() const
Return selected item IDs in lexical order as owning values.
const ActionTimelineTarget & target() const noexcept
Borrow the authoritative target.
const action::ActionTimeline & timeline() const noexcept
Borrow the authoritative timeline until the next mutation.
TimelineWidgetLayout layout() const
Project the current authoritative timeline plus any active drag preview.
ActionTimelineWidget(ActionTimelineEditor &editor, const action::ActionNotifyRegistry &registry)
Construct a widget over authoritative editor and notify services.
Result< void > setVisibleRange(Duration start, Duration end)
Set the visible timeline interval used for zoomed projection and interaction.
Result< void > pan(Duration delta)
Pan the visible interval by an exact timeline delta, clamped to the asset.
Result< void > setViewport(float width, float rowHeight, float labelWidth=120.0f)
Configure host-space dimensions used for layout and hit testing.
Result< void > zoom(double factor, double normalizedAnchor=0.5)
Zoom the visible interval around a normalized anchor in [0, 1].
Result< void > setSnapInterval(Duration interval)
Set deterministic drag/seek snapping; zero disables snapping.
UI-independent property editor used by tools to expose settings.
virtual void endGroup()=0
End the current logical group.
virtual void beginGroup(const std::string &id, const std::string &label)=0
Begin a logical group of properties.
virtual bool scalar(const std::string &id, const std::string &label, float &value, float minimum, float maximum)=0
Present an editable scalar property.
virtual bool string(const std::string &id, const std::string &label, std::string &value)=0
Present an editable text property.
Sink implemented by a 2D or 3D viewport to render tool feedback.
virtual void text(const OverlayPoint &position, const std::string &value, const OverlayStyle &style)=0
Draw a short text annotation.
virtual void circle(const OverlayPoint &center, float radius, const OverlayStyle &style)=0
Draw a circle whose orientation is supplied by the viewport.
virtual void line(const OverlayPoint &from, const OverlayPoint &to, const OverlayStyle &style)=0
Draw a line between two viewport-space points.
virtual void rectangle(const OverlayPoint &minimum, const OverlayPoint &maximum, const OverlayStyle &style)=0
Draw an axis-aligned rectangle in viewport coordinates.
std::optional< ActionBlendCurve > actionBlendCurveFromName(std::string_view name) noexcept
Parse a persisted montage cross-fade curve name.
std::string_view actionBlendCurveName(ActionBlendCurve curve) noexcept
Stable persisted name for a montage cross-fade curve.
std::string_view actionParameterOperationName(ActionParameterOperation value) noexcept
Stable persisted operation spelling.
ActionNotifyShape
Declares whether a notify is instantaneous or an enter/exit state.
std::string_view actionParameterInterpolationName(ActionParameterInterpolation value) noexcept
Stable persisted interpolation spelling.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
Result< void > noOp(std::vector< Diagnostic > diagnostics={})
Construct a successful void result for an operation that made no change.
TimelineWidgetCommand
Standard context-menu and keyboard actions exposed by the widget.
eve::editing::RuleId RuleId
Definition EditorIds.h:32
std::string_view timelineItemVisualName(TimelineItemVisual visual) noexcept
Return the stable script-facing name for a timeline visual kind.
TimelineItemVisual
Stable semantic thumbnail kind projected for one timeline block.
eve::StatusCode EditorStatus
Definition EditorResult.h:8
Objective animation segment placed on the montage presentation lane.
Duration sourceStart
Clip-local trim-in timestamp.
ActionBlendCurve blendCurve
Curve used for the incoming cross-fade.
Duration sourceEnd
Clip-local trim-out timestamp; zero means the physical clip end.
static Result< ActionAudioBinding > fromPayload(const Value::Object &payload, ActionAudioShape shape)
Decode an audio payload transactionally.
Owning request for a bounded audio-block waveform projection.
static Result< ActionParameterCurveBinding > fromPayload(const Value::Object &payload)
Decode and validate target, operation and normalized ordered keys.
Owning event projection returned by the runtime sampler.
std::vector< ActionAnimationSection > animationSections
std::vector< ActionTrack > tracks
A point in the coordinate space chosen by an editor viewport adapter.
Result of widget hit testing.
Owning geometry projected for one timeline item.
Complete renderer-neutral layout for one widget frame.
std::vector< TimelineRulerTick > rulerTicks
std::vector< TimelineItemGeometry > items
bool audioWaveformsAvailable
Whether an audio module currently provides waveform projection.
uint32_t bucket
glm::vec4 color