载入中...
搜索中...
未找到
ActionTimelineScriptBindings.cpp
浏览该文件的文档.
2
11#include "animation/AnimClip.h"
14#include "animation/AnimPose.h"
17#include "common/Capability.h"
24#include "model3d/ModelData.h"
25
26#include <simplesquirrel/simplesquirrel.hpp>
27
28#include <algorithm>
29#include <cmath>
30#include <cstdint>
31#include <filesystem>
32#include <iterator>
33#include <memory>
34#include <set>
35#include <string>
36#include <unordered_map>
37#include <utility>
39
40namespace eve::editor {
41namespace {
42
43constexpr const char* kBindingSource = "editor.action.timeline.squirrel";
44
46
47ssq::Table bindingFailure(HSQUIRRELVM vm, DiagnosticCode code, std::string message, std::string path = {}) {
48 return ::eve::editor::bindingFailure(vm, kBindingSource, code, std::move(message), std::move(path));
49}
50
51Result<Duration> seconds(float value) {
52 auto converted = Duration::fromSeconds(static_cast<double>(value));
53 if (!converted) return Result<Duration>::failure(converted.status());
54 return Result<Duration>::success(std::move(converted).takeValue());
55}
56
57const action::ActionTrack* trackAt(const action::ActionTimeline& timeline, int index) {
58 if (index < 0 || static_cast<std::size_t>(index) >= timeline.tracks.size()) return nullptr;
59 return &timeline.tracks[static_cast<std::size_t>(index)];
60}
61
62const action::ActionAnimationSection* sectionAt(const action::ActionTimeline& timeline, int index) {
63 if (index < 0 || static_cast<std::size_t>(index) >= timeline.animationSections.size()) return nullptr;
64 return &timeline.animationSections[static_cast<std::size_t>(index)];
65}
66
67const TimelineItemGeometry* itemAt(const TimelineWidgetLayout& layout, int index) {
68 if (index < 0 || static_cast<std::size_t>(index) >= layout.items.size()) return nullptr;
69 return &layout.items[static_cast<std::size_t>(index)];
70}
71
72const action::ActionNotifyState* findState(const action::ActionTimeline& timeline, const std::string& itemId) {
73 for (const auto& track : timeline.tracks) {
74 const auto found = std::find_if(track.states.begin(), track.states.end(),
75 [&](const auto& state) { return state.id.format() == itemId; });
76 if (found != track.states.end()) return &*found;
77 }
78 return nullptr;
79}
80
81const action::ActionNotify* findNotify(const action::ActionTimeline& timeline, const std::string& itemId) {
82 for (const auto& track : timeline.tracks) {
83 const auto found = std::find_if(track.notifies.begin(), track.notifies.end(),
84 [&](const auto& notify) { return notify.id.format() == itemId; });
85 if (found != track.notifies.end()) return &*found;
86 }
87 return nullptr;
88}
89
90const action::ActionAnimationSection* findSection(const action::ActionTimeline& timeline, const std::string& itemId) {
91 const auto found = std::find_if(timeline.animationSections.begin(), timeline.animationSections.end(),
92 [&](const auto& section) { return section.id.format() == itemId; });
93 return found == timeline.animationSections.end() ? nullptr : &*found;
94}
95
96class ScriptActionTimelineAssetCatalog {
97public:
98 explicit ScriptActionTimelineAssetCatalog(std::filesystem::path projectRoot)
99 : store_(projectRoot), catalog_(std::move(projectRoot), &database_) {}
100
101 [[nodiscard]] Result<std::size_t> refresh(std::string text) {
102 auto scanned = catalog_.scan();
103 if (!scanned.ok()) return Result<std::size_t>::failure(scanned.status());
104 AssetQuery query;
105 query.typeIds = {"eve.action.timeline"};
106 query.text = std::move(text);
107 auto page = database_.query(query, 0, 512);
108 if (!page.ok()) return Result<std::size_t>::failure(page.status());
109
110 entries_.clear();
111 auto diagnostics = scanned.diagnostics();
112 for (const auto& record : page.value().values) {
113 auto stored = store_.read(record.logicalUri);
114 if (!stored.ok()) {
115 diagnostics.insert(diagnostics.end(), stored.diagnostics().begin(), stored.diagnostics().end());
116 continue;
117 }
118 auto timeline = action::ActionTimeline::fromValue(toPresentationValue(stored.value().content));
119 if (!timeline) {
120 diagnostics.insert(diagnostics.end(), timeline.diagnostics().begin(), timeline.diagnostics().end());
121 continue;
122 }
123 entries_.push_back(record);
124 }
125 generation_ = page.value().generation;
126 return eve::editing::applied<std::size_t>(entries_.size(), std::move(diagnostics));
127 }
128
129 [[nodiscard]] Result<void> registerDocument(const std::string& guid, const std::string& uri) {
130 if (guid.empty() || !uri.starts_with("content://") || uri.size() <= std::string("content://").size())
131 return eve::editing::failed<void>(EditorStatus::Rejected,
132 RuleId("editor.action.asset-registration-invalid"),
133 "Action asset registration requires a GUID and content URI");
134 auto stored = store_.read(uri);
135 if (!stored.ok()) return Result<void>::failure(stored.status());
136 auto timeline = action::ActionTimeline::fromValue(toPresentationValue(stored.value().content));
137 if (!timeline) return Result<void>::failure(timeline.status());
138 return catalog_.writeSidecar(uri.substr(std::string("content://").size()), AssetGuid(guid),
139 "eve.action.timeline");
140 }
141
142 [[nodiscard]] int count() const noexcept { return static_cast<int>(entries_.size()); }
143 [[nodiscard]] std::uint64_t generation() const noexcept { return generation_; }
144 [[nodiscard]] std::string guid(int index) const {
145 const auto* entry = at(index);
146 return entry ? entry->guid.value() : std::string{};
147 }
148 [[nodiscard]] std::string uri(int index) const {
149 const auto* entry = at(index);
150 return entry ? entry->logicalUri : std::string{};
151 }
152 [[nodiscard]] std::string title(int index) const {
153 const auto* entry = at(index);
154 if (!entry) return {};
155 return std::filesystem::path(entry->logicalUri.substr(std::string("content://").size())).stem().string();
156 }
157
158private:
159 [[nodiscard]] const AssetRecord* at(int index) const {
160 if (index < 0 || static_cast<std::size_t>(index) >= entries_.size()) return nullptr;
161 return &entries_[static_cast<std::size_t>(index)];
162 }
163
164 MemoryAssetDatabase database_;
165 DiskAtomicDocumentStore store_;
166 DiskAssetCatalog catalog_;
167 std::vector<AssetRecord> entries_;
168 std::uint64_t generation_ = 0;
169};
170
171Value montageAdvanceValue(const animation::MontageAdvance& value) {
173 projected["previousSeconds"] = value.previous.seconds();
174 projected["currentSeconds"] = value.current.seconds();
175 projected["sectionId"] = value.sectionId ? value.sectionId->format() : "";
176 projected["eventCount"] = static_cast<std::int64_t>(value.events.size());
177 projected["activeBlockCount"] = static_cast<std::int64_t>(value.activeBlocks.size());
178 projected["rootMotionX"] = static_cast<double>(value.rootMotion.px);
179 projected["rootMotionY"] = static_cast<double>(value.rootMotion.py);
180 projected["rootMotionZ"] = static_cast<double>(value.rootMotion.pz);
181 projected["weight"] = value.weight;
182 projected["completed"] = value.completed;
183 return Value(std::move(projected));
184}
185
186void accumulateRootMotion(animation::TransformTRS& total, const animation::TransformTRS& delta) {
187 total.px += delta.px;
188 total.py += delta.py;
189 total.pz += delta.pz;
190 const float tx = total.qx;
191 const float ty = total.qy;
192 const float tz = total.qz;
193 const float tw = total.qw;
194 total.qx = tw * delta.qx + tx * delta.qw + ty * delta.qz - tz * delta.qy;
195 total.qy = tw * delta.qy - tx * delta.qz + ty * delta.qw + tz * delta.qx;
196 total.qz = tw * delta.qz + tx * delta.qy - ty * delta.qx + tz * delta.qw;
197 total.qw = tw * delta.qw - tx * delta.qx - ty * delta.qy - tz * delta.qz;
198}
199
200class ComposedActionPreviewSink final : public action::IActionPreviewSink {
201public:
202 void add(std::unique_ptr<action::IActionPreviewSink> sink) { sinks_.push_back(std::move(sink)); }
203 [[nodiscard]] bool empty() const noexcept { return sinks_.empty(); }
204
205 Result<void> prepare(const action::ActionPreviewFrame& frame) override {
206 std::size_t prepared = 0;
207 for (; prepared < sinks_.size(); ++prepared) {
208 auto result = sinks_[prepared]->prepare(frame);
209 if (result) continue;
210 for (std::size_t index = 0; index <= prepared; ++index) sinks_[index]->discardPrepared();
211 return Result<void>::failure(result.status());
212 }
214 }
215
216 void present(const action::ActionPreviewFrame& frame) noexcept override {
217 for (auto& sink : sinks_) sink->present(frame);
218 }
219
220 void discardPrepared() noexcept override {
221 for (auto& sink : sinks_) sink->discardPrepared();
222 }
223
224private:
225 std::vector<std::unique_ptr<action::IActionPreviewSink>> sinks_;
226};
227
228class ScriptActionTimelineEditor {
229public:
230 ScriptActionTimelineEditor(std::string targetId, action::ActionTimeline timeline,
231 action::ActionNotifyRegistry registry,
232 std::shared_ptr<ActionTimelineClipboard> clipboard)
233 : registry_(std::move(registry)),
234 blockRuntime_(registry_),
235 editor_(std::move(targetId), std::move(timeline), std::move(clipboard)),
236 widget_(editor_, registry_) {
237 auto composed = std::make_unique<ComposedActionPreviewSink>();
238 const auto providerCount = cap::listenerCount<action::IActionPreviewSinkProvider>();
239 for (std::size_t index = 0; index < providerCount; ++index) {
240 auto* provider = cap::listenerAt<action::IActionPreviewSinkProvider>(index);
241 if (!provider) continue;
242 auto sink = provider->createActionPreviewSink();
243 if (!sink) {
244 previewInitializationFailure_ = sink.status();
245 return;
246 }
247 composed->add(std::move(sink).takeValue());
248 }
249 if (composed->empty()) return;
250 previewSink_ = std::move(composed);
251 previewController_ = std::make_unique<ActionPreviewController>(editor_, *previewSink_);
252 }
253
254 ~ScriptActionTimelineEditor() {
255 if (executionId_.isZero()) return;
256 const auto* montage = activeMontage();
257 auto interrupted = blockRuntime_.interrupt(runtimeContext(montage ? montage->time() : Duration::zero()));
258 interrupted.ignore();
259 }
260
261 ActionTimelineEditor& editor() noexcept { return editor_; }
262 ActionTimelineWidget& widget() noexcept { return widget_; }
263
264 const ActionTimelineEditor& editor() const noexcept { return editor_; }
265 const ActionTimelineWidget& widget() const noexcept { return widget_; }
266 const action::ActionNotifyRegistry& registry() const noexcept { return registry_; }
267
268 [[nodiscard]] bool hasPreviewHost() const noexcept { return previewController_ != nullptr; }
269
270 void attachDocument(std::unique_ptr<DiskAtomicDocumentStore> store,
271 std::unique_ptr<DocumentService> documents, DocumentSnapshot snapshot) {
272 documentStore_ = std::move(store);
273 documents_ = std::move(documents);
274 document_ = std::move(snapshot);
275 synchronizedEditorRevision_ = editor_.target().revision();
276 savedEditorRevision_ = editor_.target().revision();
277 }
278
279 [[nodiscard]] bool documentDirty() const {
280 if (!documents_ || document_.id.empty()) return false;
281 auto snapshot = documents_->snapshot(document_.id);
282 return !snapshot.ok() || snapshot.value().dirty() || editor_.target().revision() != savedEditorRevision_;
283 }
284
285 [[nodiscard]] Result<void> setMontageSettings(action::ActionMontageSettings settings) {
286 const auto previous = editor_.target().timeline().montage;
287 auto edited = editor_.setMontageSettings(settings);
288 if (!edited.ok()) return edited;
289 if (auto* montage = activeMontage()) {
290 auto applied = montage->setSettings(settings);
291 if (!applied) {
292 auto rolledBack = editor_.undo();
293 if (rolledBack.ok()) {
294 runtimeRate_ = previous.basePlayRate;
295 if (runtimeTimeline_) runtimeTimeline_->montage = previous;
296 }
297 return Result<void>::failure(applied.status());
298 }
299 }
300 runtimeRate_ = settings.basePlayRate;
301 if (runtimeTimeline_) runtimeTimeline_->montage = std::move(settings);
302 return edited;
303 }
304
305 [[nodiscard]] Result<void> addSectionSplit(Duration time) {
306 const auto previous = editor_.target().timeline().splitTimestamps;
307 auto edited = editor_.addSectionSplit(time);
308 if (!edited.ok()) return edited;
309 const auto& splits = editor_.target().timeline().splitTimestamps;
310 if (auto* montage = activeMontage()) {
311 auto applied = montage->setSectionSplits(splits);
312 if (!applied) {
313 auto rolledBack = editor_.undo();
314 if (rolledBack.ok() && runtimeTimeline_) runtimeTimeline_->splitTimestamps = previous;
315 return Result<void>::failure(applied.status());
316 }
317 }
318 if (runtimeTimeline_) runtimeTimeline_->splitTimestamps = splits;
319 return edited;
320 }
321
322 [[nodiscard]] Result<void> setSectionSplit(std::size_t index, Duration time) {
323 const auto previous = editor_.target().timeline().splitTimestamps;
324 auto edited = editor_.setSectionSplit(index, time);
325 if (!edited.ok()) return edited;
326 const auto& splits = editor_.target().timeline().splitTimestamps;
327 if (auto* montage = activeMontage()) {
328 auto applied = montage->setSectionSplits(splits);
329 if (!applied) {
330 auto rolledBack = editor_.undo();
331 if (rolledBack.ok() && runtimeTimeline_) runtimeTimeline_->splitTimestamps = previous;
332 return Result<void>::failure(applied.status());
333 }
334 }
335 if (runtimeTimeline_) runtimeTimeline_->splitTimestamps = splits;
336 return edited;
337 }
338
339 [[nodiscard]] Result<void> removeSectionSplit(std::size_t index) {
340 const auto previous = editor_.target().timeline().splitTimestamps;
341 auto edited = editor_.removeSectionSplit(index);
342 if (!edited.ok()) return edited;
343 const auto& splits = editor_.target().timeline().splitTimestamps;
344 if (auto* montage = activeMontage()) {
345 auto applied = montage->setSectionSplits(splits);
346 if (!applied) {
347 auto rolledBack = editor_.undo();
348 if (rolledBack.ok() && runtimeTimeline_) runtimeTimeline_->splitTimestamps = previous;
349 return Result<void>::failure(applied.status());
350 }
351 }
352 if (runtimeTimeline_) runtimeTimeline_->splitTimestamps = splits;
353 return edited;
354 }
355
356 [[nodiscard]] Result<TransactionReceipt> undo() {
357 const auto previous = editor_.target().timeline().splitTimestamps;
358 auto undone = editor_.undo();
359 if (!undone.ok()) return undone;
360 const auto& splits = editor_.target().timeline().splitTimestamps;
361 if (auto* montage = activeMontage()) {
362 auto applied = montage->setSectionSplits(splits);
363 if (!applied) {
364 auto rolledBack = editor_.redo();
365 if (rolledBack.ok() && runtimeTimeline_) runtimeTimeline_->splitTimestamps = previous;
367 }
368 }
369 if (runtimeTimeline_) runtimeTimeline_->splitTimestamps = splits;
370 return undone;
371 }
372
373 [[nodiscard]] Result<TransactionReceipt> redo() {
374 const auto previous = editor_.target().timeline().splitTimestamps;
375 auto redone = editor_.redo();
376 if (!redone.ok()) return redone;
377 const auto& splits = editor_.target().timeline().splitTimestamps;
378 if (auto* montage = activeMontage()) {
379 auto applied = montage->setSectionSplits(splits);
380 if (!applied) {
381 auto rolledBack = editor_.undo();
382 if (rolledBack.ok() && runtimeTimeline_) runtimeTimeline_->splitTimestamps = previous;
384 }
385 }
386 if (runtimeTimeline_) runtimeTimeline_->splitTimestamps = splits;
387 return redone;
388 }
389
390 [[nodiscard]] Result<DocumentSnapshot> saveDocument() {
391 auto synchronized = synchronizeDocument();
392 if (!synchronized.ok()) return synchronized;
393 auto ticket = documents_->requestSave(document_.id);
394 if (!ticket.ok()) return Result<DocumentSnapshot>::failure(ticket.status());
395 auto saved = documents_->executeSave(ticket.value());
396 if (saved.ok()) {
397 document_ = saved.value();
398 savedEditorRevision_ = editor_.target().revision();
399 }
400 return saved;
401 }
402
403 [[nodiscard]] Result<DocumentSnapshot> reconcileDocument() {
404 auto synchronized = synchronizeDocument();
405 if (!synchronized.ok()) return synchronized;
406 auto reconciled = documents_->reconcileExternal(document_.id, [](const EditorValue& content) {
407 auto timeline = action::ActionTimeline::fromValue(toPresentationValue(content));
408 if (!timeline) return Result<void>::failure(timeline.status());
409 return eve::editing::applied<void>();
410 });
411 if (!reconciled.ok()) return reconciled;
412 auto content = documents_->content(document_.id);
413 if (!content.ok()) return Result<DocumentSnapshot>::failure(content.status());
414 if (content.value() != editor_.target().snapshotValue()) {
415 auto loaded = editor_.reloadDocument(content.value());
416 if (!loaded.ok()) return Result<DocumentSnapshot>::failure(loaded.status());
417 synchronizedEditorRevision_ = editor_.target().revision();
418 savedEditorRevision_ = editor_.target().revision();
419 }
420 document_ = reconciled.value();
421 return reconciled;
422 }
423
424 const DocumentSnapshot& document() const noexcept { return document_; }
425
426 [[nodiscard]] Result<void> seekPreview(Duration time) {
427 if (previewInitializationFailure_)
428 return Result<void>::failure(*previewInitializationFailure_);
429 return previewController_ ? previewController_->seek(time) : editor_.seek(time);
430 }
431
432 [[nodiscard]] Result<void> seekPreviewAt(float x) { return seekPreview(widget_.timeAt(x)); }
433
434 [[nodiscard]] Result<void> stopPreview() {
435 if (previewInitializationFailure_)
436 return Result<void>::failure(*previewInitializationFailure_);
437 return previewController_ ? previewController_->stop() : editor_.stop();
438 }
439
440 [[nodiscard]] Result<void> stepPreviewFrames(std::int64_t frames, double frameRate) {
441 if (previewInitializationFailure_)
442 return Result<void>::failure(*previewInitializationFailure_);
443 return previewController_ ? previewController_->stepFrames(frames, frameRate)
444 : editor_.stepFrames(frames, frameRate);
445 }
446
447 [[nodiscard]] Result<void> jumpPreviewToEnd() {
448 if (previewInitializationFailure_)
449 return Result<void>::failure(*previewInitializationFailure_);
450 return previewController_ ? previewController_->jumpToEnd() : editor_.jumpToEnd();
451 }
452
453 [[nodiscard]] Result<std::size_t> updatePreview(Duration delta) {
454 if (previewInitializationFailure_)
455 return Result<std::size_t>::failure(*previewInitializationFailure_);
456 return previewController_ ? previewController_->update(delta) : editor_.update(delta);
457 }
458
459 [[nodiscard]] Result<void> refreshPreview() {
460 if (previewInitializationFailure_)
461 return Result<void>::failure(*previewInitializationFailure_);
462 return previewController_ ? previewController_->refresh() : eve::editing::noOp();
463 }
464
465 [[nodiscard]] Result<void> registerRuntimeClip(std::string uri, model3d::ModelData& model,
466 animation::AnimSkeleton& skeleton, int animationIndex) {
467 if (uri.empty())
469 Diagnostic::error(DiagnosticCode::InvalidArgument, "montage clip URI must not be empty", "uri"));
470 if (animationIndex < 0 || animationIndex >= animation::AnimImporter::getAnimationCountFromModel(&model))
472 "montage animation index is outside the model", "index"));
473 std::unique_ptr<animation::AnimClip> clip(
474 animation::AnimImporter::loadClipFromModel(&model, &skeleton, animationIndex));
475 if (!clip)
477 Diagnostic::error(DiagnosticCode::Failed, "montage clip import produced no animation", uri));
478 const auto found = std::find_if(runtimeClips_.begin(), runtimeClips_.end(),
479 [&](const auto& asset) { return asset.uri == uri; });
480 if (auto* montage = activeMontage()) {
481 auto applied = montage->replaceClip(uri, clip->clone());
482 if (!applied) return applied;
483 }
484 if (found == runtimeClips_.end()) runtimeClips_.push_back({std::move(uri), std::move(clip)});
485 else found->clip = std::move(clip);
487 }
488
489 [[nodiscard]] Result<void> replaceRuntimeClip(std::string uri, const animation::AnimClip& clip) {
490 if (uri.empty())
492 Diagnostic::error(DiagnosticCode::InvalidArgument, "montage clip URI must not be empty", "uri"));
493 const auto found = std::find_if(runtimeClips_.begin(), runtimeClips_.end(),
494 [&](const auto& asset) { return asset.uri == uri; });
495 if (found == runtimeClips_.end())
497 Diagnostic::error(DiagnosticCode::NotFound, "montage runtime clip was not registered", uri));
498 if (auto* montage = activeMontage()) {
499 auto applied = montage->replaceClip(uri, clip.clone());
500 if (!applied) return applied;
501 }
502 found->clip = clip.clone();
504 }
505
506 [[nodiscard]] Result<void> beginRuntime(animation::AnimSkeleton& skeleton) {
507 auto timeline = editor_.target().timeline();
508 runtimeTimeline_ = timeline;
509 coordinator_ = std::make_unique<animation::MontageCoordinator>(skeleton);
510 montageHandle_ = animation::MontageHandle::invalid();
511 tick_ = SimulationTick::zero();
512 runtimeAdvance_.reset();
513 runtimeRate_ = timeline.montage.basePlayRate;
514 runtimePaused_ = false;
515 sectionRates_.clear();
516 return restartExecution();
517 }
518
519 [[nodiscard]] Result<animation::MontageAdvance> advanceRuntime(Duration delta) {
520 auto* montage = activeMontage();
521 if (!runtime_ || !montage)
523 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
524 if (montage->isBlendingOut()) {
525 tick_ = SimulationTick(tick_.value() + 1);
526 auto settled = montage->advanceBlendOut(delta, tick_);
527 if (settled) runtimeAdvance_ = settled.value();
528 return settled;
529 }
530 if (runtimePaused_) {
531 animation::MontageAdvance paused;
532 paused.previous = paused.current = montage->time();
533 paused.weight = montage->weight();
535 }
536 double sectionRate = 1.0;
537 auto sectionIndex = montage->physicalSectionIndex();
538 if (sectionIndex) {
539 const auto found = sectionRates_.find(sectionIndex.value());
540 if (found != sectionRates_.end()) sectionRate = found->second;
541 }
542 auto scaledDelta = Duration::fromSeconds(delta.seconds() * runtimeRate_ * sectionRate);
543 if (!scaledDelta) return Result<animation::MontageAdvance>::failure(scaledDelta.status());
544 Duration remainingDelta = std::move(scaledDelta).takeValue();
545 animation::MontageAdvance combined;
546 combined.previous = montage->time();
547 bool firstSlice = true;
548 while (true) {
549 Duration remainingTimeline =
550 Duration::fromNanoseconds(runtimeTimeline_->duration.nanoseconds() - montage->time().nanoseconds());
551 if (runtimeTimeline_->montage.looping && remainingTimeline.isZero()) {
552 auto restarted = restartExecution();
553 if (!restarted) return Result<animation::MontageAdvance>::failure(restarted.status());
554 montage = activeMontage();
555 remainingTimeline = runtimeTimeline_->duration;
556 }
557 const Duration slice =
558 runtimeTimeline_->montage.looping ? std::min(remainingDelta, remainingTimeline) : remainingDelta;
559 tick_ = SimulationTick(tick_.value() + 1);
560 auto actionAdvance = runtime_->advance(executionId_, tick_, slice);
561 if (!actionAdvance) return Result<animation::MontageAdvance>::failure(actionAdvance.status());
562 auto routed = routeAvailableBlocks(actionAdvance.value());
563 if (!routed) return Result<animation::MontageAdvance>::failure(routed.status());
564 auto presented = coordinator_->present(montageHandle_, actionAdvance.value(), tick_);
565 if (!presented) return Result<animation::MontageAdvance>::failure(presented.status());
566 auto faded = coordinator_->advanceBlendOuts(slice, tick_, montageHandle_);
567 if (!faded) return Result<animation::MontageAdvance>::failure(faded.status());
568 auto value = std::move(presented).takeValue();
569 if (firstSlice) {
570 combined = std::move(value);
571 firstSlice = false;
572 } else {
573 combined.current = value.current;
574 combined.sectionId = value.sectionId;
575 combined.events.insert(combined.events.end(), std::make_move_iterator(value.events.begin()),
576 std::make_move_iterator(value.events.end()));
577 combined.activeBlocks = std::move(value.activeBlocks);
578 accumulateRootMotion(combined.rootMotion, value.rootMotion);
579 combined.weight = value.weight;
580 combined.completed = value.completed;
581 }
582 if (!runtimeTimeline_->montage.looping || remainingDelta <= slice) break;
583
584 remainingDelta = Duration::fromNanoseconds(remainingDelta.nanoseconds() - slice.nanoseconds());
585 auto restarted = restartExecution();
586 if (!restarted) return Result<animation::MontageAdvance>::failure(restarted.status());
587 }
588 if (runtimeTimeline_->montage.looping && combined.completed) {
589 auto restarted = restartExecution();
590 if (!restarted) return Result<animation::MontageAdvance>::failure(restarted.status());
591 combined.current = Duration::zero();
592 combined.sectionId.reset();
593 combined.activeBlocks.clear();
594 combined.completed = false;
595 montage = activeMontage();
596 combined.weight = montage ? montage->weight() : 0.0;
597 }
598 runtimeAdvance_ = combined;
600 }
601
602 [[nodiscard]] Result<void> setRuntimeBlockExternallyHandled(const std::string& type, bool external) {
603 auto parsed = LogicalId::parse(type);
604 if (!parsed)
607 "externally handled runtime block type is invalid", "type"));
608 if (!registry_.hasHandler(type))
611 "runtime block type has no registered handler", type));
612 if (external)
613 externallyHandledTypes_.insert(type);
614 else
615 externallyHandledTypes_.erase(type);
617 }
618
619 [[nodiscard]] Result<animation::MontageAdvance> jumpRuntime(Duration target) {
620 auto* montage = activeMontage();
621 if (!runtimeTimeline_ || !montage)
623 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
624 auto restarted = restartExecution(false);
625 if (!restarted) return Result<animation::MontageAdvance>::failure(restarted.status());
626 auto rebound = montage->rebindExecution(executionId_);
627 if (!rebound) return Result<animation::MontageAdvance>::failure(rebound.status());
628 tick_ = SimulationTick(tick_.value() + 1);
629 auto advanced = runtime_->advance(executionId_, tick_, target);
630 if (!advanced) return Result<animation::MontageAdvance>::failure(advanced.status());
631 auto routed = routeAvailableBlocks(advanced.value());
632 if (!routed) return Result<animation::MontageAdvance>::failure(routed.status());
633 auto jumped = montage->jumpToTime(executionId_, target, tick_);
634 if (jumped) runtimeAdvance_ = jumped.value();
635 return jumped;
636 }
637
638 [[nodiscard]] Result<animation::MontageAdvance> jumpRuntimeSection(std::size_t index) {
639 if (!runtimeTimeline_)
641 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
642 auto range = runtimeTimeline_->sectionRange(index);
644 return jumpRuntime(range.value().first);
645 }
646
647 [[nodiscard]] Result<animation::MontageAdvance> evaluateRuntimeSectionProgress(std::size_t index,
648 double progress) {
649 if (!runtimeTimeline_)
651 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
652 if (!std::isfinite(progress) || progress < 0.0 || progress > 1.0)
655 "runtime section progress must be finite and within [0,1]", "progress"));
656 auto range = runtimeTimeline_->sectionRange(index);
658 const auto span = range.value().second.nanoseconds() - range.value().first.nanoseconds();
659 return jumpRuntime(Duration::fromNanoseconds(
660 range.value().first.nanoseconds() + static_cast<std::int64_t>(std::llround(span * progress))));
661 }
662
663 [[nodiscard]] Result<void> replayRuntimeCrossFade() {
664 if (!runtimeTimeline_ || !activeMontage())
666 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
667 return restartExecution();
668 }
669
670 [[nodiscard]] Result<void> syncRuntimeSection(std::size_t index, Duration targetDuration) {
671 if (!runtimeTimeline_)
673 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
674 if (targetDuration <= Duration::zero())
676 "target section duration must be positive", "duration"));
677 auto range = runtimeTimeline_->sectionRange(index);
678 if (!range) return Result<void>::failure(range.status());
679 const double raw = (range.value().second.seconds() - range.value().first.seconds());
680 sectionRates_[index] = raw / targetDuration.seconds();
682 }
683
684 [[nodiscard]] Result<animation::MontageAdvance> syncRuntimeSectionAndJump(std::size_t index,
685 Duration targetDuration) {
686 auto synchronized = syncRuntimeSection(index, targetDuration);
687 if (!synchronized) return Result<animation::MontageAdvance>::failure(synchronized.status());
688 return jumpRuntimeSection(index);
689 }
690
691 [[nodiscard]] Result<void> clearRuntimeSectionSync(std::size_t index) {
692 if (!runtimeTimeline_)
694 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
695 auto range = runtimeTimeline_->sectionRange(index);
696 if (!range) return Result<void>::failure(range.status());
697 return Result<void>::success(Status::success(sectionRates_.erase(index) > 0 ? StatusCode::Applied
698 : StatusCode::NoOp));
699 }
700
701 [[nodiscard]] Result<void> setRuntimeBoneMask(const animation::AnimBoneMask& mask) {
702 if (!coordinator_ || !activeMontage())
704 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
705 return coordinator_->setLayerBoneMask(activeLayer_, mask);
706 }
707
708 [[nodiscard]] Result<void> clearRuntimeBoneMask() {
709 if (!coordinator_ || !activeMontage())
711 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
712 return coordinator_->clearLayerBoneMask(activeLayer_);
713 }
714
715 [[nodiscard]] Result<void> setRuntimeLayerWeight(float weight) {
716 if (!coordinator_ || !activeMontage())
718 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
719 return coordinator_->setLayerWeight(activeLayer_, weight);
720 }
721
722 [[nodiscard]] float runtimeLayerWeight() const {
723 if (!coordinator_ || !activeMontage()) return 0.0f;
724 auto weight = coordinator_->layerWeight(activeLayer_);
725 return weight ? weight.value() : 0.0f;
726 }
727
728 [[nodiscard]] Result<void> setRuntimeLayerAdditive(bool additive) {
729 if (!coordinator_ || !activeMontage())
731 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
732 return coordinator_->setLayerAdditive(activeLayer_, additive);
733 }
734
735 [[nodiscard]] bool runtimeLayerAdditive() const {
736 if (!coordinator_ || !activeMontage()) return false;
737 auto additive = coordinator_->layerAdditive(activeLayer_);
738 return additive ? additive.value() : false;
739 }
740
741 [[nodiscard]] Result<animation::MontageAdvance> beginRuntimeBlendOut(Duration duration) {
742 auto* montage = activeMontage();
743 if (!montage)
745 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
746 tick_ = SimulationTick(tick_.value() + 1);
747 auto stopped = montage->beginBlendOut(duration, tick_);
748 if (stopped) runtimeAdvance_ = stopped.value();
749 return stopped;
750 }
751
752 void setRuntimePaused(bool paused) noexcept { runtimePaused_ = paused; }
753 [[nodiscard]] bool runtimePaused() const noexcept { return runtimePaused_; }
754
755 [[nodiscard]] Result<void> setRuntimeRate(double rate) {
756 if (!runtimeTimeline_)
758 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
759 if (!std::isfinite(rate) || rate <= 0.0)
762 "montage runtime rate must be finite and positive", "rate"));
763 if (runtimeRate_ == rate) return Result<void>::success(Status::success(StatusCode::NoOp));
764 runtimeRate_ = rate;
766 }
767
768 [[nodiscard]] double runtimeRate() const noexcept { return runtimeRate_; }
769
770 [[nodiscard]] Result<void> cancelRuntime() {
771 auto* montage = activeMontage();
772 if (!runtime_ || !montage) return Result<void>::success(Status::success(StatusCode::NoOp));
773 tick_ = SimulationTick(tick_.value() + 1);
774 auto cancelled = runtime_->cancel(executionId_, tick_);
775 if (!cancelled) return cancelled;
776 auto interrupted = blockRuntime_.interrupt(runtimeContext(montage->time()));
777 if (!interrupted) return interrupted;
778 montage->stop();
780 }
781
782 [[nodiscard]] animation::AnimPose* runtimePose() noexcept {
783 if (!coordinator_ || !montageHandle_.isValid()) return nullptr;
784 auto pose = coordinator_->pose(activeLayer_);
785 return pose ? &pose.value().get() : nullptr;
786 }
787 [[nodiscard]] animation::AnimPose* runtimePoseOverBase(const animation::AnimPose& basePose) {
788 if (!coordinator_ || !montageHandle_.isValid()) return nullptr;
789 auto pose = coordinator_->compose(basePose);
790 return pose ? &pose.value().get() : nullptr;
791 }
792 [[nodiscard]] const std::optional<animation::MontageAdvance>& runtimeAdvance() const noexcept {
793 return runtimeAdvance_;
794 }
795 [[nodiscard]] bool runtimePlaying() const noexcept {
796 const auto* montage = activeMontage();
797 return montage && montage->isPlaying();
798 }
799
800 [[nodiscard]] std::string runtimeState() const {
801 if (!runtimeTimeline_) return "not-started";
802 if (runtimeAdvance_ && runtimeAdvance_->completed) return "finished";
803 const auto* montage = activeMontage();
804 if (!montage) return "finished";
805 if (montage->isBlendingOut()) return "stopping";
806 if (runtimePaused_) return "paused";
807 return montage->isPlaying() ? "playing" : "finished";
808 }
809
810 [[nodiscard]] double runtimeElapsedSeconds() const noexcept {
811 const auto* montage = activeMontage();
812 if (montage) return montage->time().seconds();
813 return runtimeAdvance_ ? runtimeAdvance_->current.seconds() : 0.0;
814 }
815
816 [[nodiscard]] double runtimeDurationSeconds() const noexcept {
817 return runtimeTimeline_ ? runtimeTimeline_->duration.seconds() : 0.0;
818 }
819
820 [[nodiscard]] int runtimeLayer() const noexcept {
821 return montageHandle_.isValid() ? static_cast<int>(activeLayer_) : -1;
822 }
823
824 [[nodiscard]] int runtimeSlot() const noexcept {
825 return montageHandle_.isValid() ? static_cast<int>(montageHandle_.index() % 2U) : -1;
826 }
827
828 [[nodiscard]] Result<std::size_t> runtimePhysicalSection() const {
829 const auto* montage = activeMontage();
830 if (!montage)
832 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
833 return montage->physicalSectionIndex();
834 }
835
836 [[nodiscard]] Result<double> runtimeSectionProgress(std::size_t index) const {
837 const auto* montage = activeMontage();
838 if (!montage)
840 Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
841 return montage->physicalSectionProgress(index);
842 }
843
844private:
845 [[nodiscard]] animation::MontagePlayer* activeMontage() noexcept {
846 if (!coordinator_ || !montageHandle_.isValid()) return nullptr;
847 auto resolved = coordinator_->resolve(montageHandle_);
848 return resolved ? &resolved.value().get() : nullptr;
849 }
850
851 [[nodiscard]] const animation::MontagePlayer* activeMontage() const noexcept {
852 if (!coordinator_ || !montageHandle_.isValid()) return nullptr;
853 auto resolved = static_cast<const animation::MontageCoordinator&>(*coordinator_).resolve(montageHandle_);
854 return resolved ? &resolved.value().get() : nullptr;
855 }
856
857 [[nodiscard]] std::vector<animation::MontageClipAsset> cloneRuntimeClips() const {
858 std::vector<animation::MontageClipAsset> clips;
859 clips.reserve(runtimeClips_.size());
860 for (const auto& asset : runtimeClips_)
861 clips.push_back({asset.uri, asset.clip ? asset.clip->clone() : nullptr});
862 return clips;
863 }
864
865 [[nodiscard]] Result<DocumentSnapshot> synchronizeDocument() {
866 if (!documents_ || document_.id.empty())
867 return eve::editing::failed<DocumentSnapshot>(EditorStatus::Unsupported,
868 RuleId("editor.action.document.not-persistent"),
869 "Action timeline editor is not attached to a document");
870 auto snapshot = documents_->snapshot(document_.id);
871 if (!snapshot.ok()) return snapshot;
872 if (editor_.target().revision() == synchronizedEditorRevision_) return snapshot;
873 auto edited = documents_->edit(document_.id, editor_.target().snapshotValue(), snapshot.value().revision.edit);
874 if (edited.ok()) {
875 synchronizedEditorRevision_ = editor_.target().revision();
876 document_ = edited.value();
877 }
878 return edited;
879 }
880
881 [[nodiscard]] action::ActionDefinition runtimeDefinition() const {
882 action::ActionDefinition definition;
883 definition.id = runtimeTimeline_->actionId;
884 if (runtimeTimeline_->splitTimestamps.size() >= 2) {
885 definition.timing.windup = runtimeTimeline_->splitTimestamps[0];
886 definition.timing.active = Duration::fromNanoseconds(runtimeTimeline_->splitTimestamps[1].nanoseconds() -
887 runtimeTimeline_->splitTimestamps[0].nanoseconds());
888 definition.timing.recover = Duration::fromNanoseconds(runtimeTimeline_->duration.nanoseconds() -
889 runtimeTimeline_->splitTimestamps[1].nanoseconds());
890 } else {
891 definition.timing.active = runtimeTimeline_->duration;
892 }
893 definition.timeline = *runtimeTimeline_;
894 return definition;
895 }
896
897 [[nodiscard]] Result<void> restartExecution(bool playMontage = true) {
898 const bool hasActiveMontage = activeMontage() != nullptr;
899 const auto previousHandle = montageHandle_;
900 const auto previousLayer = activeLayer_;
901 if (!executionId_.isZero()) {
902 const auto* montage = activeMontage();
903 auto interrupted = blockRuntime_.interrupt(runtimeContext(montage ? montage->time() : Duration::zero()));
904 if (!interrupted) return interrupted;
905 }
906 auto runtime = std::make_unique<action::ActionRuntime>();
907 auto definition = runtimeDefinition();
908 action::ActionRequest request;
909 request.actionId = definition.id;
910 auto submitted = runtime->submit(std::move(definition), std::move(request));
911 if (!submitted) return Result<void>::failure(submitted.status());
912 if (playMontage) {
913 if (!coordinator_)
915 Diagnostic::error(DiagnosticCode::Conflict, "montage coordinator has not been started", "runtime"));
916 if (hasActiveMontage) tick_ = SimulationTick(tick_.value() + 1);
917 const std::size_t layer = runtimeTimeline_->montage.animationLayer;
918 auto played = coordinator_->play(layer, *runtimeTimeline_, cloneRuntimeClips(), submitted.value(), tick_);
919 if (!played) return Result<void>::failure(played.status());
920 montageHandle_ = played.value();
921 activeLayer_ = layer;
922 if (hasActiveMontage && previousLayer != layer) {
923 auto previous = coordinator_->resolve(previousHandle);
924 if (previous) previous.value().get().stop();
925 coordinator_->collectFinished();
926 }
927 }
928 runtime_ = std::move(runtime);
929 executionId_ = submitted.value();
931 }
932
933 [[nodiscard]] action::ActionNotifyContext runtimeContext(Duration time) const {
934 action::ActionNotifyContext context;
935 context.executionId = executionId_;
936 context.time = time;
937 return context;
938 }
939
940 [[nodiscard]] Result<void> routeAvailableBlocks(const action::ActionAdvance& advance) {
941 action::ActionAdvance handled;
942 handled.id = advance.id;
943 handled.phase = advance.phase;
944 handled.phaseElapsed = advance.phaseElapsed;
945 handled.totalElapsed = advance.totalElapsed;
946 for (const auto& event : advance.timelineEvents)
947 if (registry_.hasHandler(event.type.format()) &&
948 !externallyHandledTypes_.contains(event.type.format()))
949 handled.timelineEvents.push_back(event);
950 for (const auto& block : advance.activeBlocks)
951 if (registry_.hasHandler(block.type.format()) &&
952 !externallyHandledTypes_.contains(block.type.format()))
953 handled.activeBlocks.push_back(block);
954 return blockRuntime_.apply(handled, runtimeContext(advance.totalElapsed));
955 }
956
957 action::ActionNotifyRegistry registry_;
958 action::ActionBlockRuntime blockRuntime_;
959 ActionTimelineEditor editor_;
960 ActionTimelineWidget widget_;
961 std::unique_ptr<action::IActionPreviewSink> previewSink_;
962 std::unique_ptr<ActionPreviewController> previewController_;
963 std::optional<Status> previewInitializationFailure_;
964 std::vector<animation::MontageClipAsset> runtimeClips_;
965 std::unique_ptr<action::ActionRuntime> runtime_;
966 std::unique_ptr<animation::MontageCoordinator> coordinator_;
968 std::size_t activeLayer_ = 0;
969 action::ActionExecutionId executionId_{};
971 std::optional<animation::MontageAdvance> runtimeAdvance_;
972 std::optional<action::ActionTimeline> runtimeTimeline_;
973 std::unordered_map<std::size_t, double> sectionRates_;
974 std::set<std::string, std::less<>> externallyHandledTypes_;
975 double runtimeRate_ = 1.0;
976 bool runtimePaused_ = false;
977 std::unique_ptr<DiskAtomicDocumentStore> documentStore_;
978 std::unique_ptr<DocumentService> documents_;
979 DocumentSnapshot document_;
980 std::uint64_t synchronizedEditorRevision_ = 0;
981 std::uint64_t savedEditorRevision_ = 0;
982};
983
984std::optional<Value> itemPayloadField(ScriptActionTimelineEditor* self, const std::string& itemId,
985 const std::string& field) {
986 if (!self) return std::nullopt;
987 auto parsed = LogicalId::parse(itemId);
988 if (!parsed) return std::nullopt;
989 ActionTimelinePayloadEditor payloads(self->editor(), self->registry());
990 auto payload = payloads.payload(*parsed);
991 if (!payload.ok()) return std::nullopt;
992 const auto found = payload.value().find(field);
993 return found == payload.value().end() ? std::nullopt : std::optional<Value>(found->second);
994}
995
996Result<void> patchItemPayload(ScriptActionTimelineEditor* self, const std::string& itemId,
998 if (!self)
999 return eve::editing::failed<void>(EditorStatus::Rejected,
1000 RuleId("editor.action.timeline.payload-editor-null"),
1001 "Action timeline editor is null");
1002 auto parsed = LogicalId::parse(itemId);
1003 if (!parsed)
1004 return eve::editing::failed<void>(EditorStatus::Rejected,
1005 RuleId("editor.action.timeline.payload-item-invalid"),
1006 "Action-block identity is invalid");
1007 ActionTimelinePayloadEditor payloads(self->editor(), self->registry());
1008 return payloads.patch(*parsed, std::move(fields));
1009}
1010
1011std::string eventKind(action::ActionTimelineEventKind kind) {
1012 switch (kind) {
1013 case action::ActionTimelineEventKind::Notify: return "notify";
1014 case action::ActionTimelineEventKind::StateEnter: return "state_enter";
1015 case action::ActionTimelineEventKind::StateExit: return "state_exit";
1016 }
1017 return "unknown";
1018}
1019
1020} // namespace
1021
1022void exposeActionTimelineScriptBindings(ssq::Table& table, ssq::Class& moduleClass) {
1023 const HSQUIRRELVM vm = table.getHandle();
1024 auto clipboard = std::make_shared<ActionTimelineClipboard>();
1025 auto actionEditor = table.addClass<ScriptActionTimelineEditor>(
1026 "ActionTimelineEditor",
1027 std::function<ScriptActionTimelineEditor*()>([]() -> ScriptActionTimelineEditor* { return nullptr; }), true);
1028 auto assetCatalog = table.addClass<ScriptActionTimelineAssetCatalog>(
1029 "ActionTimelineAssetCatalog",
1030 std::function<ScriptActionTimelineAssetCatalog*()>([]() -> ScriptActionTimelineAssetCatalog* {
1031 return nullptr;
1032 }),
1033 true);
1034 assetCatalog.addFunc("refresh", [vm](ScriptActionTimelineAssetCatalog* self, const std::string& text) {
1035 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action asset catalog is null");
1036 auto refreshed = self->refresh(text);
1037 return project(vm, refreshed,
1038 refreshed.ok() ? Value(static_cast<std::int64_t>(refreshed.value())) : Value{});
1039 });
1040 assetCatalog.addFunc("getAssetCount",
1041 [](ScriptActionTimelineAssetCatalog* self) { return self ? self->count() : 0; });
1042 assetCatalog.addFunc("getGeneration", [](ScriptActionTimelineAssetCatalog* self) {
1043 return self ? static_cast<std::int64_t>(self->generation()) : std::int64_t{0};
1044 });
1045 assetCatalog.addFunc("getAssetGuid",
1046 [](ScriptActionTimelineAssetCatalog* self, int index) { return self ? self->guid(index) : ""; });
1047 assetCatalog.addFunc("getAssetUri",
1048 [](ScriptActionTimelineAssetCatalog* self, int index) { return self ? self->uri(index) : ""; });
1049 assetCatalog.addFunc("getAssetTitle", [](ScriptActionTimelineAssetCatalog* self, int index) {
1050 return self ? self->title(index) : "";
1051 });
1052 assetCatalog.addFunc("registerDocument", [vm](ScriptActionTimelineAssetCatalog* self, const std::string& guid,
1053 const std::string& uri) {
1054 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action asset catalog is null");
1055 return project(vm, self->registerDocument(guid, uri));
1056 });
1057
1058 actionEditor.addFunc("configureWorkspace", [vm](ScriptActionTimelineEditor* self, EditorWorkspace* workspace) {
1059 if (!self || !workspace)
1061 "action timeline editor and workspace must not be null", "workspace");
1062 return project(vm, self->editor().configureWorkspace(*workspace));
1063 });
1064 actionEditor.addFunc(
1065 "setViewport", [vm](ScriptActionTimelineEditor* self, float width, float rowHeight, float labelWidth) {
1066 if (!self)
1067 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1068 return project(vm, self->widget().setViewport(width, rowHeight, labelWidth));
1069 });
1070 actionEditor.addFunc("addNotifyAtCursor", [vm](ScriptActionTimelineEditor* self, const std::string& trackId,
1071 const std::string& type, const std::string& payloadJson) {
1072 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1073 auto parsedTrack = LogicalId::parse(trackId);
1074 if (!parsedTrack)
1075 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId");
1076 auto parsedPayload = Value::fromJson(payloadJson);
1077 if (!parsedPayload) return script::projectStatusResult(vm, parsedPayload.status());
1078 const auto* payload = parsedPayload.value().getIf<Value::Object>();
1079 if (!payload)
1080 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "notify payload must be a JSON object",
1081 "payloadJson");
1082 return project(vm, self->widget().addNotifyAtCursor(*parsedTrack, type, *payload));
1083 });
1084 actionEditor.addFunc("addStateAtCursor", [vm](ScriptActionTimelineEditor* self, const std::string& trackId,
1085 const std::string& type, float durationSeconds,
1086 const std::string& payloadJson) {
1087 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1088 auto parsedTrack = LogicalId::parse(trackId);
1089 auto duration = seconds(durationSeconds);
1090 if (!parsedTrack)
1091 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId");
1092 if (!duration) return script::projectStatusResult(vm, duration.status());
1093 auto parsedPayload = Value::fromJson(payloadJson);
1094 if (!parsedPayload) return script::projectStatusResult(vm, parsedPayload.status());
1095 const auto* payload = parsedPayload.value().getIf<Value::Object>();
1096 if (!payload)
1097 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "notify-state payload must be a JSON object",
1098 "payloadJson");
1099 return project(vm, self->widget().addStateAtCursor(*parsedTrack, type, duration.value(), *payload));
1100 });
1101 actionEditor.addFunc("handleTimelineShortcut", [vm](ScriptActionTimelineEditor* self,
1102 const std::string& shortcut) {
1103 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1104 return project(vm, self->widget().handleShortcut(shortcut));
1105 });
1106 actionEditor.addFunc("addTrack", [vm](ScriptActionTimelineEditor* self, const std::string& trackId,
1107 const std::string& label, const std::string& kind) {
1108 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1109 auto parsedId = LogicalId::parse(trackId);
1110 auto parsedKind = action::parseActionTrackKind(kind);
1111 if (!parsedId) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId");
1112 if (!parsedKind) return script::projectStatusResult(vm, parsedKind.status());
1113 action::ActionTrack track;
1114 track.id = std::move(*parsedId);
1115 track.label = label;
1116 track.kind = parsedKind.value();
1117 return project(vm, self->editor().addTrack(std::move(track)));
1118 });
1119 actionEditor.addFunc("removeTrack", [vm](ScriptActionTimelineEditor* self, const std::string& trackId) {
1120 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1121 auto parsed = LogicalId::parse(trackId);
1122 if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId");
1123 return project(vm, self->editor().removeTrack(*parsed));
1124 });
1125 actionEditor.addFunc(
1126 "renameTrack", [vm](ScriptActionTimelineEditor* self, const std::string& trackId, const std::string& label) {
1127 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1128 auto parsed = LogicalId::parse(trackId);
1129 if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId");
1130 return project(vm, self->editor().renameTrack(*parsed, label));
1131 });
1132 actionEditor.addFunc(
1133 "setTrackMuted", [vm](ScriptActionTimelineEditor* self, const std::string& trackId, bool muted) {
1134 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1135 auto parsed = LogicalId::parse(trackId);
1136 if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId");
1137 return project(vm, self->editor().setTrackMuted(*parsed, muted));
1138 });
1139 actionEditor.addFunc(
1140 "setTrackLocked", [vm](ScriptActionTimelineEditor* self, const std::string& trackId, bool locked) {
1141 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1142 auto parsed = LogicalId::parse(trackId);
1143 if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId");
1144 return project(vm, self->editor().setTrackLocked(*parsed, locked));
1145 });
1146 actionEditor.addFunc("copyTrack", [vm](ScriptActionTimelineEditor* self, const std::string& trackId) {
1147 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1148 auto parsed = LogicalId::parse(trackId);
1149 if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId");
1150 return project(vm, self->editor().copyTrack(*parsed));
1151 });
1152 actionEditor.addFunc("pasteTrack", [vm](ScriptActionTimelineEditor* self) {
1153 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1154 auto pasted = self->editor().pasteTrack();
1155 return project(vm, pasted, pasted.ok() ? Value(pasted.value().format()) : Value{});
1156 });
1157 actionEditor.addFunc(
1158 "pasteSelectionToTrack",
1159 [vm](ScriptActionTimelineEditor* self, const std::string& trackId, float offsetSeconds) {
1160 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1161 auto parsed = LogicalId::parse(trackId);
1162 auto offset = seconds(offsetSeconds);
1163 if (!parsed)
1164 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId");
1165 if (!offset) return script::projectStatusResult(vm, offset.status());
1166 auto pasted = self->editor().pasteToTrack(*parsed, offset.value());
1167 return project(vm, pasted,
1168 pasted.ok() ? Value(static_cast<std::int64_t>(pasted.value())) : Value{});
1169 });
1170 actionEditor.addFunc("setSnapSeconds", [vm](ScriptActionTimelineEditor* self, float intervalSeconds) {
1171 if (!self)
1172 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1173 auto interval = seconds(intervalSeconds);
1174 if (!interval) return script::projectStatusResult(vm, interval.status());
1175 return project(vm, self->widget().setSnapInterval(std::move(interval).takeValue()));
1176 });
1177 actionEditor.addFunc(
1178 "setVisibleSeconds", [vm](ScriptActionTimelineEditor* self, float startSeconds, float endSeconds) {
1179 if (!self)
1180 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1181 auto start = seconds(startSeconds);
1182 auto end = seconds(endSeconds);
1183 if (!start) return script::projectStatusResult(vm, start.status());
1184 if (!end) return script::projectStatusResult(vm, end.status());
1185 return project(vm, self->widget().setVisibleRange(start.value(), end.value()));
1186 });
1187 actionEditor.addFunc("zoomTimeline", [vm](ScriptActionTimelineEditor* self, float factor, float anchor) {
1188 if (!self)
1189 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1190 return project(vm, self->widget().zoom(factor, anchor));
1191 });
1192 actionEditor.addFunc("panTimelineSeconds", [vm](ScriptActionTimelineEditor* self, float deltaSeconds) {
1193 if (!self)
1194 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1195 auto delta = seconds(deltaSeconds);
1196 if (!delta) return script::projectStatusResult(vm, delta.status());
1197 return project(vm, self->widget().pan(delta.value()));
1198 });
1199 actionEditor.addFunc("pointerDown", [vm](ScriptActionTimelineEditor* self, float x, float y, bool additive) {
1200 if (!self)
1201 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1202 return project(vm, self->widget().pointerDown(x, y, additive));
1203 });
1204 actionEditor.addFunc("pointerMove", [vm](ScriptActionTimelineEditor* self, float x) {
1205 if (!self)
1206 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1207 return project(vm, self->widget().pointerMove(x));
1208 });
1209 actionEditor.addFunc("pointerUp", [vm](ScriptActionTimelineEditor* self, float x) {
1210 if (!self)
1211 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1212 return project(vm, self->widget().pointerUp(x));
1213 });
1214 actionEditor.addFunc("seekX", [vm](ScriptActionTimelineEditor* self, float x) {
1215 if (!self)
1216 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1217 return project(vm, self->seekPreviewAt(x));
1218 });
1219 actionEditor.addFunc("seekSeconds", [vm](ScriptActionTimelineEditor* self, float value) {
1220 if (!self)
1221 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1222 auto duration = seconds(value);
1223 if (!duration) return script::projectStatusResult(vm, duration.status());
1224 return project(vm, self->seekPreview(std::move(duration).takeValue()));
1225 });
1226 actionEditor.addFunc("resizeState", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
1227 float startSeconds, float endSeconds) {
1228 if (!self)
1229 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1230 auto start = seconds(startSeconds);
1231 if (!start) return script::projectStatusResult(vm, start.status());
1232 auto end = seconds(endSeconds);
1233 if (!end) return script::projectStatusResult(vm, end.status());
1234 auto parsedItemId = LogicalId::parse(itemId);
1235 if (!parsedItemId)
1236 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "state item id is not canonical", "itemId");
1237 return project(
1238 vm, self->editor().resizeState(*parsedItemId, std::move(start).takeValue(), std::move(end).takeValue()));
1239 });
1240 actionEditor.addFunc("setItemTiming", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
1241 float startSeconds, float endSeconds) {
1242 if (!self)
1243 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1244 auto parsedItemId = LogicalId::parse(itemId);
1245 auto start = seconds(startSeconds);
1246 auto end = seconds(endSeconds);
1247 if (!parsedItemId)
1248 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "timeline item id is not canonical", "itemId");
1249 if (!start) return script::projectStatusResult(vm, start.status());
1250 if (!end) return script::projectStatusResult(vm, end.status());
1251 const auto& timeline = self->editor().target().timeline();
1252 if (const auto* section = findSection(timeline, itemId))
1253 return project(vm, self->editor().resizeAnimationSection(*parsedItemId, start.value(), end.value(),
1254 section->blendIn));
1255 if (const auto* state = findState(timeline, itemId))
1256 return project(vm, self->editor().editItem(*parsedItemId, start.value(), end.value(), state->type,
1257 state->payload));
1258 if (const auto* notify = findNotify(timeline, itemId))
1259 return project(vm, self->editor().editItem(*parsedItemId, start.value(), start.value(), notify->type,
1260 notify->payload));
1261 return bindingFailure(vm, DiagnosticCode::NotFound, "timeline item was not found", "itemId");
1262 });
1263 actionEditor.addFunc("removeItem", [vm](ScriptActionTimelineEditor* self, const std::string& itemId) {
1264 auto parsedItemId = LogicalId::parse(itemId);
1265 if (!self || !parsedItemId)
1266 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "timeline item id is invalid", "itemId");
1267 return project(vm, self->editor().removeItem(*parsedItemId));
1268 });
1269 actionEditor.addFunc("setItemEnabled", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
1270 bool enabled) {
1271 auto parsedItemId = LogicalId::parse(itemId);
1272 if (!self || !parsedItemId)
1273 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "timeline item id is invalid", "itemId");
1274 return project(vm, self->editor().setItemEnabled(*parsedItemId, enabled));
1275 });
1276 actionEditor.addFunc("fitBlockToClip", [vm](ScriptActionTimelineEditor* self, const std::string& itemId) {
1277 if (!self)
1278 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1279 auto parsedItemId = LogicalId::parse(itemId);
1280 if (!parsedItemId)
1281 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "timeline item id is invalid", "itemId");
1282 ActionTimelinePayloadEditor payloads(self->editor(), self->registry());
1283 return project(vm, payloads.fitBlockToClip(*parsedItemId));
1284 });
1285 actionEditor.addFunc("fitClipToBlock", [vm](ScriptActionTimelineEditor* self, const std::string& itemId) {
1286 if (!self)
1287 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1288 auto parsedItemId = LogicalId::parse(itemId);
1289 if (!parsedItemId)
1290 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "timeline item id is invalid", "itemId");
1291 ActionTimelinePayloadEditor payloads(self->editor(), self->registry());
1292 return project(vm, payloads.fitClipToBlock(*parsedItemId));
1293 });
1294 actionEditor.addFunc("fitClipToNaturalDuration",
1295 [vm](ScriptActionTimelineEditor* self, const std::string& itemId) {
1296 if (!self)
1297 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1298 auto parsedItemId = LogicalId::parse(itemId);
1299 if (!parsedItemId)
1300 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "timeline item id is invalid", "itemId");
1301 const auto* provider = cap::query<action::IActionVfxDurationProvider>();
1302 ActionTimelinePayloadEditor payloads(self->editor(), self->registry());
1304 if (provider) borrowed = std::cref(*provider);
1305 return project(vm, payloads.fitClipToNaturalDuration(*parsedItemId, borrowed));
1306 });
1307 actionEditor.addFunc("hasVfxDurationProvider", [](ScriptActionTimelineEditor*) {
1308 return cap::query<action::IActionVfxDurationProvider>() != nullptr;
1309 });
1310 actionEditor.addFunc("editItemDetails", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
1311 const std::string& type, const std::string& payloadJson) {
1312 auto parsedItemId = LogicalId::parse(itemId);
1313 auto parsedType = LogicalId::parse(type);
1314 if (!self || !parsedItemId || !parsedType)
1315 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "timeline item identity or type is invalid",
1316 "itemDetails");
1317 auto parsedPayload = Value::fromJson(payloadJson);
1318 if (!parsedPayload) return script::projectStatusResult(vm, parsedPayload.status());
1319 const auto* payload = parsedPayload.value().getIf<Value::Object>();
1320 if (!payload)
1321 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "timeline item payload must be a JSON object",
1322 "payloadJson");
1323 const auto& timeline = self->editor().target().timeline();
1324 const auto* state = findState(timeline, itemId);
1325 const auto* notify = findNotify(timeline, itemId);
1326 if (!state && !notify)
1327 return bindingFailure(vm, DiagnosticCode::NotFound, "editable notify item was not found", "itemId");
1328 auto descriptor = self->registry().descriptor(type);
1329 if (!descriptor) return script::projectStatusResult(vm, descriptor.status());
1331 if (descriptor.value().shape != expected)
1333 "notify type shape does not match the selected item", "type");
1334 const LogicalId* trackId = nullptr;
1335 for (const auto& track : timeline.tracks) {
1336 const auto contains = state ? std::any_of(track.states.begin(), track.states.end(),
1337 [&](const auto& value) { return value.id == *parsedItemId; })
1338 : std::any_of(track.notifies.begin(), track.notifies.end(),
1339 [&](const auto& value) { return value.id == *parsedItemId; });
1340 if (contains) {
1341 trackId = &track.id;
1342 break;
1343 }
1344 }
1345 if (!trackId)
1346 return bindingFailure(vm, DiagnosticCode::NotFound, "timeline item track was not found", "itemId");
1349 *trackId,
1350 *parsedItemId,
1351 *parsedType,
1352 state ? state->start : notify->time,
1353 *payload};
1354 auto valid = self->registry().validate(event);
1355 if (!valid) return script::projectStatusResult(vm, valid.status());
1356 return project(vm, self->editor().updateItem(*parsedItemId, *parsedType, *payload));
1357 });
1358 actionEditor.addFunc("addParameterKey", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
1359 float time, float value, float inTangent, float outTangent,
1360 const std::string& interpolation) {
1361 auto parsedId = LogicalId::parse(itemId);
1362 auto parsedInterpolation = action::actionParameterInterpolationFromName(interpolation);
1363 if (!self || !parsedId || !parsedInterpolation)
1365 "parameter item id or interpolation is invalid", "parameterKey");
1366 return project(vm, self->editor().addParameterKey(
1367 *parsedId, {time, value, inTangent, outTangent, *parsedInterpolation}));
1368 });
1369 actionEditor.addFunc("editParameterKey", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
1370 int index, float time, float value, float inTangent,
1371 float outTangent, const std::string& interpolation) {
1372 auto parsedId = LogicalId::parse(itemId);
1373 auto parsedInterpolation = action::actionParameterInterpolationFromName(interpolation);
1374 if (!self || !parsedId || index < 0 || !parsedInterpolation)
1376 "parameter key identity or interpolation is invalid", "parameterKey");
1377 return project(vm, self->editor().editParameterKey(
1378 *parsedId, static_cast<std::size_t>(index),
1379 {time, value, inTangent, outTangent, *parsedInterpolation}));
1380 });
1381 actionEditor.addFunc("removeParameterKey", [vm](ScriptActionTimelineEditor* self,
1382 const std::string& itemId, int index) {
1383 auto parsedId = LogicalId::parse(itemId);
1384 if (!self || !parsedId || index < 0)
1386 "parameter key identity is invalid", "parameterKey");
1387 return project(vm, self->editor().removeParameterKey(*parsedId, static_cast<std::size_t>(index)));
1388 });
1389 actionEditor.addFunc("undo", [vm](ScriptActionTimelineEditor* self) {
1390 if (!self)
1391 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1392 auto result = self->undo();
1393 return project(vm, result, Value(result.ok() ? static_cast<std::int64_t>(result.value().afterRevision) : 0));
1394 });
1395 actionEditor.addFunc("redo", [vm](ScriptActionTimelineEditor* self) {
1396 if (!self)
1397 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1398 auto result = self->redo();
1399 return project(vm, result, Value(result.ok() ? static_cast<std::int64_t>(result.value().afterRevision) : 0));
1400 });
1401 actionEditor.addFunc("update", [vm](ScriptActionTimelineEditor* self, float deltaSeconds) {
1402 if (!self)
1403 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1404 auto delta = seconds(deltaSeconds);
1405 if (!delta) return script::projectStatusResult(vm, delta.status());
1406 auto result = self->updatePreview(std::move(delta).takeValue());
1407 return project(vm, result, Value(result.ok() ? static_cast<std::int64_t>(result.value()) : 0));
1408 });
1409 actionEditor.addFunc("refreshPreview", [vm](ScriptActionTimelineEditor* self) {
1410 if (!self)
1411 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1412 return project(vm, self->refreshPreview());
1413 });
1414 actionEditor.addFunc("hasPreviewHost", [](ScriptActionTimelineEditor* self) {
1415 return self && self->hasPreviewHost();
1416 });
1417 actionEditor.addFunc("snapshot", [vm](ScriptActionTimelineEditor* self) {
1418 if (!self)
1419 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1420 return script::projectResult(vm, self->editor().target().timeline().toValue(),
1421 [](Value value) { return value; });
1422 });
1423 actionEditor.addFunc("snapshotJson", [vm](ScriptActionTimelineEditor* self) {
1424 if (!self)
1426 "action timeline editor must not be null");
1427 auto snapshot = self->editor().target().timeline().toValue();
1428 if (!snapshot) return script::projectStatusResult(vm, snapshot.status());
1429 auto encoded = snapshot.value().toJson();
1430 return script::projectResult(vm, std::move(encoded),
1431 [](std::string value) { return Value(std::move(value)); });
1432 });
1433 actionEditor.addFunc(
1434 "registerRuntimeClip", [vm](ScriptActionTimelineEditor* self, const std::string& uri, model3d::ModelData* model,
1435 animation::AnimSkeleton* skeleton, int animationIndex) {
1436 if (!self || !model || !skeleton)
1438 "editor, model and skeleton must not be null", "runtimeClip");
1439 return script::projectResult(vm, self->registerRuntimeClip(uri, *model, *skeleton, animationIndex));
1440 });
1441 actionEditor.addFunc("replaceRuntimeClip",
1442 [vm](ScriptActionTimelineEditor* self, const std::string& uri,
1444 if (!self || !clip)
1446 "editor and animation clip must not be null", "runtimeClip");
1447 return script::projectResult(vm, self->replaceRuntimeClip(uri, *clip));
1448 });
1449 actionEditor.addFunc("beginRuntime", [vm](ScriptActionTimelineEditor* self, animation::AnimSkeleton* skeleton) {
1450 if (!self || !skeleton)
1451 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "editor and skeleton must not be null",
1452 "skeleton");
1453 return script::projectResult(vm, self->beginRuntime(*skeleton));
1454 });
1455 actionEditor.addFunc("advanceRuntime", [vm](ScriptActionTimelineEditor* self, float deltaSeconds) {
1456 if (!self)
1457 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1458 auto delta = seconds(deltaSeconds);
1459 if (!delta) return script::projectStatusResult(vm, delta.status());
1460 return script::projectResult(vm, self->advanceRuntime(std::move(delta).takeValue()), montageAdvanceValue);
1461 });
1462 actionEditor.addFunc("jumpRuntimeSeconds", [vm](ScriptActionTimelineEditor* self, float targetSeconds) {
1463 if (!self)
1464 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1465 auto target = seconds(targetSeconds);
1466 if (!target) return script::projectStatusResult(vm, target.status());
1467 return script::projectResult(vm, self->jumpRuntime(std::move(target).takeValue()), montageAdvanceValue);
1468 });
1469 actionEditor.addFunc("jumpRuntimeSection", [vm](ScriptActionTimelineEditor* self, int sectionIndex) {
1470 if (!self || sectionIndex < 0)
1471 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "runtime section index is invalid", "index");
1472 return script::projectResult(vm, self->jumpRuntimeSection(static_cast<std::size_t>(sectionIndex)),
1473 montageAdvanceValue);
1474 });
1475 actionEditor.addFunc(
1476 "evaluateRuntimeSectionProgress",
1477 [vm](ScriptActionTimelineEditor* self, int sectionIndex, float progress) {
1478 if (!self || sectionIndex < 0)
1480 "runtime section index is invalid", "index");
1481 return script::projectResult(
1482 vm, self->evaluateRuntimeSectionProgress(static_cast<std::size_t>(sectionIndex), progress),
1483 montageAdvanceValue);
1484 });
1485 actionEditor.addFunc("replayRuntimeCrossFade", [vm](ScriptActionTimelineEditor* self) {
1486 if (!self)
1488 "action timeline editor must not be null");
1489 return script::projectResult(vm, self->replayRuntimeCrossFade());
1490 });
1491 actionEditor.addFunc(
1492 "syncRuntimeSection", [vm](ScriptActionTimelineEditor* self, int sectionIndex, float targetSeconds) {
1493 if (!self || sectionIndex < 0)
1494 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "runtime section index is invalid", "index");
1495 auto target = seconds(targetSeconds);
1496 if (!target) return script::projectStatusResult(vm, target.status());
1497 return script::projectResult(
1498 vm, self->syncRuntimeSection(static_cast<std::size_t>(sectionIndex), std::move(target).takeValue()));
1499 });
1500 actionEditor.addFunc(
1501 "syncRuntimeSectionAndJump", [vm](ScriptActionTimelineEditor* self, int sectionIndex, float targetSeconds) {
1502 if (!self || sectionIndex < 0)
1504 "runtime section index is invalid", "index");
1505 auto target = seconds(targetSeconds);
1506 if (!target) return script::projectStatusResult(vm, target.status());
1507 return script::projectResult(
1508 vm,
1509 self->syncRuntimeSectionAndJump(static_cast<std::size_t>(sectionIndex),
1510 std::move(target).takeValue()),
1511 montageAdvanceValue);
1512 });
1513 actionEditor.addFunc("clearRuntimeSectionSync", [vm](ScriptActionTimelineEditor* self, int sectionIndex) {
1514 if (!self || sectionIndex < 0)
1516 "runtime section index is invalid", "index");
1518 self->clearRuntimeSectionSync(static_cast<std::size_t>(sectionIndex)));
1519 });
1520 actionEditor.addFunc("setRuntimeBoneMask",
1521 [vm](ScriptActionTimelineEditor* self, animation::AnimBoneMask* mask) {
1522 if (!self || !mask)
1524 "action timeline editor and bone mask must not be null", "mask");
1525 return script::projectResult(vm, self->setRuntimeBoneMask(*mask));
1526 });
1527 actionEditor.addFunc("clearRuntimeBoneMask", [vm](ScriptActionTimelineEditor* self) {
1528 if (!self)
1530 "action timeline editor must not be null");
1531 return script::projectResult(vm, self->clearRuntimeBoneMask());
1532 });
1533 actionEditor.addFunc("setRuntimeLayerWeight", [vm](ScriptActionTimelineEditor* self, float weight) {
1534 if (!self)
1535 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1536 return script::projectResult(vm, self->setRuntimeLayerWeight(weight));
1537 });
1538 actionEditor.addFunc("getRuntimeLayerWeight",
1539 [](ScriptActionTimelineEditor* self) { return self ? self->runtimeLayerWeight() : 0.0f; });
1540 actionEditor.addFunc("setRuntimeLayerAdditive", [vm](ScriptActionTimelineEditor* self, bool additive) {
1541 if (!self)
1542 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1543 return script::projectResult(vm, self->setRuntimeLayerAdditive(additive));
1544 });
1545 actionEditor.addFunc("getRuntimeLayerAdditive",
1546 [](ScriptActionTimelineEditor* self) { return self && self->runtimeLayerAdditive(); });
1547 actionEditor.addFunc("beginRuntimeBlendOut", [vm](ScriptActionTimelineEditor* self, float durationSeconds) {
1548 if (!self)
1549 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1550 auto duration = seconds(durationSeconds);
1551 if (!duration) return script::projectStatusResult(vm, duration.status());
1552 return script::projectResult(vm, self->beginRuntimeBlendOut(std::move(duration).takeValue()),
1553 montageAdvanceValue);
1554 });
1555 actionEditor.addFunc("setRuntimePaused", [](ScriptActionTimelineEditor* self, bool paused) {
1556 if (self) self->setRuntimePaused(paused);
1557 });
1558 actionEditor.addFunc("setRuntimeBlockExternallyHandled",
1559 [vm](ScriptActionTimelineEditor* self, const std::string& type, bool external) {
1560 if (!self)
1562 "action timeline editor must not be null");
1563 return script::projectResult(vm, self->setRuntimeBlockExternallyHandled(type, external));
1564 });
1565 actionEditor.addFunc("isRuntimePaused",
1566 [](ScriptActionTimelineEditor* self) { return self && self->runtimePaused(); });
1567 actionEditor.addFunc("setRuntimeRate", [vm](ScriptActionTimelineEditor* self, float rate) {
1568 if (!self)
1570 "action timeline editor must not be null");
1571 return script::projectResult(vm, self->setRuntimeRate(rate));
1572 });
1573 actionEditor.addFunc("getRuntimeRate", [](ScriptActionTimelineEditor* self) {
1574 return self ? static_cast<float>(self->runtimeRate()) : 0.0f;
1575 });
1576 actionEditor.addFunc("cancelRuntime", [vm](ScriptActionTimelineEditor* self) {
1577 if (!self)
1578 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1579 return script::projectResult(vm, self->cancelRuntime());
1580 });
1581 actionEditor.addFunc("getRuntimePose",
1582 [](ScriptActionTimelineEditor* self) { return self ? self->runtimePose() : nullptr; });
1583 actionEditor.addFunc("getRuntimePoseOverBase", [](ScriptActionTimelineEditor* self, animation::AnimPose* basePose) {
1584 return self && basePose ? self->runtimePoseOverBase(*basePose) : nullptr;
1585 });
1586 actionEditor.addFunc("isRuntimePlaying",
1587 [](ScriptActionTimelineEditor* self) { return self && self->runtimePlaying(); });
1588 actionEditor.addFunc("getRuntimeState",
1589 [](ScriptActionTimelineEditor* self) { return self ? self->runtimeState() : "not-started"; });
1590 actionEditor.addFunc("getRuntimeElapsedSeconds", [](ScriptActionTimelineEditor* self) {
1591 return self ? static_cast<float>(self->runtimeElapsedSeconds()) : 0.0f;
1592 });
1593 actionEditor.addFunc("getRuntimeDurationSeconds", [](ScriptActionTimelineEditor* self) {
1594 return self ? static_cast<float>(self->runtimeDurationSeconds()) : 0.0f;
1595 });
1596 actionEditor.addFunc("getRuntimeLayer",
1597 [](ScriptActionTimelineEditor* self) { return self ? self->runtimeLayer() : -1; });
1598 actionEditor.addFunc("getRuntimeSlot",
1599 [](ScriptActionTimelineEditor* self) { return self ? self->runtimeSlot() : -1; });
1600 actionEditor.addFunc("getRuntimeSectionId", [](ScriptActionTimelineEditor* self) {
1601 if (!self || !self->runtimeAdvance() || !self->runtimeAdvance()->sectionId) return std::string{};
1602 return self->runtimeAdvance()->sectionId->format();
1603 });
1604 actionEditor.addFunc("getRuntimeRootMotionX", [](ScriptActionTimelineEditor* self) {
1605 return self && self->runtimeAdvance() ? self->runtimeAdvance()->rootMotion.px : 0.0f;
1606 });
1607 actionEditor.addFunc("getRuntimeRootMotionY", [](ScriptActionTimelineEditor* self) {
1608 return self && self->runtimeAdvance() ? self->runtimeAdvance()->rootMotion.py : 0.0f;
1609 });
1610 actionEditor.addFunc("getRuntimeRootMotionZ", [](ScriptActionTimelineEditor* self) {
1611 return self && self->runtimeAdvance() ? self->runtimeAdvance()->rootMotion.pz : 0.0f;
1612 });
1613 actionEditor.addFunc("getRuntimeWeight", [](ScriptActionTimelineEditor* self) {
1614 return self && self->runtimeAdvance() ? static_cast<float>(self->runtimeAdvance()->weight) : 0.0f;
1615 });
1616 actionEditor.addFunc("getRuntimeEventCount", [](ScriptActionTimelineEditor* self) {
1617 return self && self->runtimeAdvance() ? static_cast<int>(self->runtimeAdvance()->events.size()) : 0;
1618 });
1619 actionEditor.addFunc("getRuntimeEventKind", [](ScriptActionTimelineEditor* self, int index) {
1620 if (!self || !self->runtimeAdvance() || index < 0 ||
1621 static_cast<std::size_t>(index) >= self->runtimeAdvance()->events.size())
1622 return std::string{};
1623 return eventKind(self->runtimeAdvance()->events[static_cast<std::size_t>(index)].kind);
1624 });
1625 actionEditor.addFunc("getRuntimeEventId", [](ScriptActionTimelineEditor* self, int index) {
1626 if (!self || !self->runtimeAdvance() || index < 0 ||
1627 static_cast<std::size_t>(index) >= self->runtimeAdvance()->events.size())
1628 return std::string{};
1629 return self->runtimeAdvance()->events[static_cast<std::size_t>(index)].itemId.format();
1630 });
1631 actionEditor.addFunc("getRuntimeEventType", [](ScriptActionTimelineEditor* self, int index) {
1632 if (!self || !self->runtimeAdvance() || index < 0 ||
1633 static_cast<std::size_t>(index) >= self->runtimeAdvance()->events.size())
1634 return std::string{};
1635 return self->runtimeAdvance()->events[static_cast<std::size_t>(index)].type.format();
1636 });
1637 actionEditor.addFunc("getRuntimeEventSeconds", [](ScriptActionTimelineEditor* self, int index) {
1638 if (!self || !self->runtimeAdvance() || index < 0 ||
1639 static_cast<std::size_t>(index) >= self->runtimeAdvance()->events.size())
1640 return 0.0f;
1641 return static_cast<float>(self->runtimeAdvance()->events[static_cast<std::size_t>(index)].time.seconds());
1642 });
1643 actionEditor.addFunc("getRuntimeEventPayloadJson", [](ScriptActionTimelineEditor* self, int index) {
1644 if (!self || !self->runtimeAdvance() || index < 0 ||
1645 static_cast<std::size_t>(index) >= self->runtimeAdvance()->events.size())
1646 return std::string("{}");
1647 auto encoded = Value(self->runtimeAdvance()->events[static_cast<std::size_t>(index)].payload).toJson();
1648 return encoded ? encoded.value() : std::string("{}");
1649 });
1650 actionEditor.addFunc("getRuntimeActiveBlockCount", [](ScriptActionTimelineEditor* self) {
1651 return self && self->runtimeAdvance() ? static_cast<int>(self->runtimeAdvance()->activeBlocks.size()) : 0;
1652 });
1653 actionEditor.addFunc("getRuntimeActiveBlockType", [](ScriptActionTimelineEditor* self, int index) {
1654 if (!self || !self->runtimeAdvance() || index < 0 ||
1655 static_cast<std::size_t>(index) >= self->runtimeAdvance()->activeBlocks.size())
1656 return std::string{};
1657 return self->runtimeAdvance()->activeBlocks[static_cast<std::size_t>(index)].type.format();
1658 });
1659 actionEditor.addFunc("getRuntimeActiveBlockLocalSeconds", [](ScriptActionTimelineEditor* self, int index) {
1660 if (!self || !self->runtimeAdvance() || index < 0 ||
1661 static_cast<std::size_t>(index) >= self->runtimeAdvance()->activeBlocks.size())
1662 return 0.0f;
1663 return static_cast<float>(
1664 self->runtimeAdvance()->activeBlocks[static_cast<std::size_t>(index)].localTime.seconds());
1665 });
1666 actionEditor.addFunc("getRuntimePhysicalSection", [](ScriptActionTimelineEditor* self) {
1667 if (!self) return -1;
1668 auto result = self->runtimePhysicalSection();
1669 return result ? static_cast<int>(result.value()) : -1;
1670 });
1671 actionEditor.addFunc("getRuntimeSectionProgress", [](ScriptActionTimelineEditor* self, int sectionIndex) {
1672 if (!self || sectionIndex < 0) return 0.0f;
1673 auto result = self->runtimeSectionProgress(static_cast<std::size_t>(sectionIndex));
1674 return result ? static_cast<float>(result.value()) : 0.0f;
1675 });
1676
1677 actionEditor.addFunc("play", [](ScriptActionTimelineEditor* self) {
1678 if (self) self->editor().play();
1679 });
1680 actionEditor.addFunc("pause", [](ScriptActionTimelineEditor* self) {
1681 if (self) self->editor().pause();
1682 });
1683 actionEditor.addFunc("stop", [vm](ScriptActionTimelineEditor* self) {
1684 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1685 return project(vm, self->stopPreview());
1686 });
1687 actionEditor.addFunc("stepFrames", [vm](ScriptActionTimelineEditor* self, int frames, float frameRate) {
1688 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1689 return project(vm, self->stepPreviewFrames(frames, frameRate));
1690 });
1691 actionEditor.addFunc("jumpToEnd", [vm](ScriptActionTimelineEditor* self) {
1692 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1693 return project(vm, self->jumpPreviewToEnd());
1694 });
1695 actionEditor.addFunc("isPlaying",
1696 [](ScriptActionTimelineEditor* self) { return self && self->editor().playing(); });
1697 actionEditor.addFunc("isDocumentBacked", [](ScriptActionTimelineEditor* self) {
1698 return self && !self->document().id.empty();
1699 });
1700 actionEditor.addFunc("isDirty",
1701 [](ScriptActionTimelineEditor* self) { return self && self->documentDirty(); });
1702 actionEditor.addFunc("saveDocument", [vm](ScriptActionTimelineEditor* self) {
1703 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1704 auto saved = self->saveDocument();
1705 return project(vm, saved,
1706 saved.ok() ? Value(static_cast<std::int64_t>(saved.value().revision.saved)) : Value{});
1707 });
1708 actionEditor.addFunc("reconcileDocument", [vm](ScriptActionTimelineEditor* self) {
1709 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1710 auto reconciled = self->reconcileDocument();
1711 return project(vm, reconciled,
1712 reconciled.ok() ? Value(static_cast<std::int64_t>(reconciled.value().revision.edit)) : Value{});
1713 });
1714 actionEditor.addFunc("getDocumentId", [](ScriptActionTimelineEditor* self) {
1715 return self ? self->document().id.value() : std::string{};
1716 });
1717 actionEditor.addFunc("getDocumentTitle", [](ScriptActionTimelineEditor* self) {
1718 return self ? self->document().title : std::string{};
1719 });
1720 actionEditor.addFunc("getDocumentUri", [](ScriptActionTimelineEditor* self) {
1721 return self ? self->document().resourceUri : std::string{};
1722 });
1723 actionEditor.addFunc("canUndo", [](ScriptActionTimelineEditor* self) { return self && self->editor().canUndo(); });
1724 actionEditor.addFunc("canRedo", [](ScriptActionTimelineEditor* self) { return self && self->editor().canRedo(); });
1725 actionEditor.addFunc("isDragging", [](ScriptActionTimelineEditor* self) {
1726 return self && self->widget().dragStatus() == TimelineDragStatus::Active;
1727 });
1728 actionEditor.addFunc("getDuration", [](ScriptActionTimelineEditor* self) {
1729 return self ? static_cast<float>(self->editor().target().timeline().duration.seconds()) : 0.0f;
1730 });
1731 actionEditor.addFunc("getPreviewTime", [](ScriptActionTimelineEditor* self) {
1732 return self ? static_cast<float>(self->editor().previewTime().seconds()) : 0.0f;
1733 });
1734 actionEditor.addFunc("getRevision", [](ScriptActionTimelineEditor* self) {
1735 return self ? static_cast<std::int64_t>(self->editor().target().revision()) : std::int64_t{0};
1736 });
1737 actionEditor.addFunc("setMontageSettings", [vm](ScriptActionTimelineEditor* self, float basePlayRate,
1738 bool looping, bool footIk, int animationLayer,
1739 float blendInSeconds, float blendOutSeconds,
1740 float blendOutOffsetSeconds, bool rootHorizontal,
1741 bool rootVertical, bool rootRotation) {
1742 if (!self || animationLayer < 0)
1744 "montage editor and non-negative animation layer are required", "montage");
1745 auto blendIn = seconds(blendInSeconds);
1746 auto blendOut = seconds(blendOutSeconds);
1747 auto blendOutOffset = seconds(blendOutOffsetSeconds);
1748 if (!blendIn) return script::projectStatusResult(vm, blendIn.status());
1749 if (!blendOut) return script::projectStatusResult(vm, blendOut.status());
1750 if (!blendOutOffset) return script::projectStatusResult(vm, blendOutOffset.status());
1752 settings.basePlayRate = basePlayRate;
1753 settings.looping = looping;
1754 settings.footIk = footIk;
1755 settings.animationLayer = static_cast<std::uint32_t>(animationLayer);
1756 settings.defaultBlendIn = blendIn.value();
1757 settings.defaultBlendOut = blendOut.value();
1758 settings.blendOutOffset = blendOutOffset.value();
1759 settings.rootMotionHorizontal = rootHorizontal;
1760 settings.rootMotionVertical = rootVertical;
1761 settings.rootMotionRotation = rootRotation;
1762 return project(vm, self->setMontageSettings(std::move(settings)));
1763 });
1764 actionEditor.addFunc("getMontageBasePlayRate", [](ScriptActionTimelineEditor* self) {
1765 return self ? static_cast<float>(self->editor().target().timeline().montage.basePlayRate) : 1.0f;
1766 });
1767 actionEditor.addFunc("getMontageLooping", [](ScriptActionTimelineEditor* self) {
1768 return self && self->editor().target().timeline().montage.looping;
1769 });
1770 actionEditor.addFunc("getMontageFootIk", [](ScriptActionTimelineEditor* self) {
1771 return self && self->editor().target().timeline().montage.footIk;
1772 });
1773 actionEditor.addFunc("getMontageAnimationLayer", [](ScriptActionTimelineEditor* self) {
1774 return self ? static_cast<int>(self->editor().target().timeline().montage.animationLayer) : 0;
1775 });
1776 actionEditor.addFunc("getMontageBlendIn", [](ScriptActionTimelineEditor* self) {
1777 return self ? static_cast<float>(self->editor().target().timeline().montage.defaultBlendIn.seconds()) : 0.0f;
1778 });
1779 actionEditor.addFunc("getMontageBlendOut", [](ScriptActionTimelineEditor* self) {
1780 return self ? static_cast<float>(self->editor().target().timeline().montage.defaultBlendOut.seconds()) : 0.0f;
1781 });
1782 actionEditor.addFunc("getMontageBlendOutOffset", [](ScriptActionTimelineEditor* self) {
1783 return self ? static_cast<float>(self->editor().target().timeline().montage.blendOutOffset.seconds()) : 0.0f;
1784 });
1785 actionEditor.addFunc("getMontageRootMotionHorizontal", [](ScriptActionTimelineEditor* self) {
1786 return self && self->editor().target().timeline().montage.rootMotionHorizontal;
1787 });
1788 actionEditor.addFunc("getMontageRootMotionVertical", [](ScriptActionTimelineEditor* self) {
1789 return self && self->editor().target().timeline().montage.rootMotionVertical;
1790 });
1791 actionEditor.addFunc("getMontageRootMotionRotation", [](ScriptActionTimelineEditor* self) {
1792 return self && self->editor().target().timeline().montage.rootMotionRotation;
1793 });
1794 actionEditor.addFunc("getAnimationUri", [](ScriptActionTimelineEditor* self) {
1795 return self ? self->editor().target().timeline().animationUri : std::string{};
1796 });
1797 actionEditor.addFunc("getSectionSplitCount", [](ScriptActionTimelineEditor* self) {
1798 return self ? static_cast<int>(self->editor().target().timeline().splitTimestamps.size()) : 0;
1799 });
1800 actionEditor.addFunc("getSectionSplitTime", [](ScriptActionTimelineEditor* self, int index) {
1801 if (!self || index < 0 || static_cast<std::size_t>(index) >=
1802 self->editor().target().timeline().splitTimestamps.size())
1803 return 0.0f;
1804 return static_cast<float>(
1805 self->editor().target().timeline().splitTimestamps[static_cast<std::size_t>(index)].seconds());
1806 });
1807 actionEditor.addFunc("addSectionSplit", [vm](ScriptActionTimelineEditor* self, float timeSeconds) {
1808 if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null");
1809 auto time = seconds(timeSeconds);
1810 if (!time) return script::projectStatusResult(vm, time.status());
1811 return project(vm, self->addSectionSplit(time.value()));
1812 });
1813 actionEditor.addFunc("setSectionSplit", [vm](ScriptActionTimelineEditor* self, int index, float timeSeconds) {
1814 if (!self || index < 0)
1816 "action timeline editor and non-negative split index are required");
1817 auto time = seconds(timeSeconds);
1818 if (!time) return script::projectStatusResult(vm, time.status());
1819 return project(vm, self->setSectionSplit(static_cast<std::size_t>(index), time.value()));
1820 });
1821 actionEditor.addFunc("removeSectionSplit", [vm](ScriptActionTimelineEditor* self, int index) {
1822 if (!self || index < 0)
1824 "action timeline editor and non-negative split index are required");
1825 return project(vm, self->removeSectionSplit(static_cast<std::size_t>(index)));
1826 });
1827 actionEditor.addFunc("getAnimationSectionCount", [](ScriptActionTimelineEditor* self) {
1828 return self ? static_cast<int>(self->editor().target().timeline().animationSections.size()) : 0;
1829 });
1830 actionEditor.addFunc("getAnimationSectionId", [](ScriptActionTimelineEditor* self, int index) {
1831 const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr;
1832 return section ? section->id.format() : std::string{};
1833 });
1834 actionEditor.addFunc("getAnimationSectionUri", [](ScriptActionTimelineEditor* self, int index) {
1835 const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr;
1836 return section ? section->animationUri : std::string{};
1837 });
1838 actionEditor.addFunc("getAnimationSectionStart", [](ScriptActionTimelineEditor* self, int index) {
1839 const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr;
1840 return section ? static_cast<float>(section->start.seconds()) : 0.0f;
1841 });
1842 actionEditor.addFunc("getAnimationSectionEnd", [](ScriptActionTimelineEditor* self, int index) {
1843 const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr;
1844 return section ? static_cast<float>(section->end.seconds()) : 0.0f;
1845 });
1846 actionEditor.addFunc("getAnimationSectionBlendIn", [](ScriptActionTimelineEditor* self, int index) {
1847 const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr;
1848 return section ? static_cast<float>(section->blendIn.seconds()) : 0.0f;
1849 });
1850 actionEditor.addFunc("getAnimationSectionSourceStart", [](ScriptActionTimelineEditor* self, int index) {
1851 const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr;
1852 return section ? static_cast<float>(section->sourceStart.seconds()) : 0.0f;
1853 });
1854 actionEditor.addFunc("getAnimationSectionSourceEnd", [](ScriptActionTimelineEditor* self, int index) {
1855 const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr;
1856 return section ? static_cast<float>(section->sourceEnd.seconds()) : 0.0f;
1857 });
1858 actionEditor.addFunc("getAnimationSectionBlendCurve", [](ScriptActionTimelineEditor* self, int index) {
1859 const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr;
1860 return section ? std::string(action::actionBlendCurveName(section->blendCurve)) : std::string{};
1861 });
1862 actionEditor.addFunc("addAnimationSection", [vm](ScriptActionTimelineEditor* self, const std::string& sectionId,
1863 const std::string& animationUri, float startSeconds,
1864 float endSeconds, float blendInSeconds) {
1865 if (!self)
1866 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1867 auto parsed = LogicalId::parse(sectionId);
1868 if (!parsed)
1869 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "animation section id is not canonical",
1870 "sectionId");
1871 auto start = seconds(startSeconds);
1872 auto end = seconds(endSeconds);
1873 auto blend = seconds(blendInSeconds);
1874 if (!start) return script::projectStatusResult(vm, start.status());
1875 if (!end) return script::projectStatusResult(vm, end.status());
1876 if (!blend) return script::projectStatusResult(vm, blend.status());
1877 action::ActionAnimationSection section{*parsed, animationUri, start.value(), end.value(), blend.value()};
1878 return project(vm, self->editor().addAnimationSection(std::move(section)));
1879 });
1880 actionEditor.addFunc("editAnimationSection", [vm](ScriptActionTimelineEditor* self, const std::string& sectionId,
1881 float startSeconds, float endSeconds, float blendInSeconds,
1882 const std::string& animationUri) {
1883 if (!self)
1884 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1885 auto parsed = LogicalId::parse(sectionId);
1886 if (!parsed)
1887 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "animation section id is not canonical",
1888 "sectionId");
1889 auto start = seconds(startSeconds);
1890 auto end = seconds(endSeconds);
1891 auto blend = seconds(blendInSeconds);
1892 if (!start) return script::projectStatusResult(vm, start.status());
1893 if (!end) return script::projectStatusResult(vm, end.status());
1894 if (!blend) return script::projectStatusResult(vm, blend.status());
1895 return project(
1896 vm, self->editor().editAnimationSection(*parsed, start.value(), end.value(), blend.value(), animationUri));
1897 });
1898 actionEditor.addFunc(
1899 "removeAnimationSection", [vm](ScriptActionTimelineEditor* self, const std::string& sectionId) {
1900 if (!self)
1901 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1902 auto parsed = LogicalId::parse(sectionId);
1903 if (!parsed)
1904 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "animation section id is not canonical",
1905 "sectionId");
1906 return project(vm, self->editor().removeAnimationSection(*parsed));
1907 });
1908 actionEditor.addFunc("editAnimationSectionSource", [vm](ScriptActionTimelineEditor* self,
1909 const std::string& sectionId, float sourceStartSeconds,
1910 float sourceEndSeconds, const std::string& blendCurve) {
1911 if (!self)
1912 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
1913 auto parsed = LogicalId::parse(sectionId);
1914 auto curve = action::actionBlendCurveFromName(blendCurve);
1915 if (!parsed || !curve)
1916 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "section id or blend curve is invalid",
1917 "sectionSource");
1918 auto sourceStart = seconds(sourceStartSeconds);
1919 auto sourceEnd = seconds(sourceEndSeconds);
1920 if (!sourceStart) return script::projectStatusResult(vm, sourceStart.status());
1921 if (!sourceEnd) return script::projectStatusResult(vm, sourceEnd.status());
1922 return project(
1923 vm, self->editor().editAnimationSectionSource(*parsed, sourceStart.value(), sourceEnd.value(), *curve));
1924 });
1925 actionEditor.addFunc("getTrackCount", [](ScriptActionTimelineEditor* self) {
1926 return self ? static_cast<int>(self->editor().target().timeline().tracks.size()) : 0;
1927 });
1928 actionEditor.addFunc("getTrackId", [](ScriptActionTimelineEditor* self, int index) {
1929 const auto* track = self ? trackAt(self->editor().target().timeline(), index) : nullptr;
1930 return track ? track->id.format() : std::string{};
1931 });
1932 actionEditor.addFunc("getTrackLabel", [](ScriptActionTimelineEditor* self, int index) {
1933 const auto* track = self ? trackAt(self->editor().target().timeline(), index) : nullptr;
1934 return track ? track->label : std::string{};
1935 });
1936 actionEditor.addFunc("getTrackKind", [](ScriptActionTimelineEditor* self, int index) {
1937 const auto* track = self ? trackAt(self->editor().target().timeline(), index) : nullptr;
1938 return track ? std::string(action::actionTrackKindName(track->kind)) : std::string{};
1939 });
1940 actionEditor.addFunc("getTrackMuted", [](ScriptActionTimelineEditor* self, int index) {
1941 const auto* track = self ? trackAt(self->editor().target().timeline(), index) : nullptr;
1942 return track && track->muted;
1943 });
1944 actionEditor.addFunc("getTrackLocked", [](ScriptActionTimelineEditor* self, int index) {
1945 const auto* track = self ? trackAt(self->editor().target().timeline(), index) : nullptr;
1946 return track && track->locked;
1947 });
1948 actionEditor.addFunc("getInsertableTypeCount", [](ScriptActionTimelineEditor* self, bool state) {
1949 if (!self) return 0;
1950 return static_cast<int>(self->widget()
1951 .insertableTypes(state ? action::ActionNotifyShape::State
1953 .size());
1954 });
1955 actionEditor.addFunc("getInsertableType", [](ScriptActionTimelineEditor* self, bool state, int index) {
1956 if (!self || index < 0) return std::string{};
1957 const auto types = self->widget().insertableTypes(state ? action::ActionNotifyShape::State
1959 return static_cast<std::size_t>(index) < types.size() ? types[static_cast<std::size_t>(index)].type
1960 : std::string{};
1961 });
1962 actionEditor.addFunc("getInsertableTypeLabel", [](ScriptActionTimelineEditor* self, bool state, int index) {
1963 if (!self || index < 0) return std::string{};
1964 const auto types = self->widget().insertableTypes(state ? action::ActionNotifyShape::State
1966 if (static_cast<std::size_t>(index) >= types.size()) return std::string{};
1967 const auto& descriptor = types[static_cast<std::size_t>(index)];
1968 return descriptor.category + " / " + descriptor.displayName;
1969 });
1970
1971 actionEditor.addFunc("getLayoutWidth",
1972 [](ScriptActionTimelineEditor* self) { return self ? self->widget().layout().width : 0.0f; });
1973 actionEditor.addFunc("getLayoutHeight",
1974 [](ScriptActionTimelineEditor* self) { return self ? self->widget().layout().height : 0.0f; });
1975 actionEditor.addFunc("getPlayheadX", [](ScriptActionTimelineEditor* self) {
1976 return self ? self->widget().layout().playheadX : 0.0f;
1977 });
1978 actionEditor.addFunc("getRulerTickCount", [](ScriptActionTimelineEditor* self) {
1979 return self ? static_cast<int>(self->widget().layout().rulerTicks.size()) : 0;
1980 });
1981 actionEditor.addFunc("getRulerTickX", [](ScriptActionTimelineEditor* self, int index) {
1982 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
1983 return index >= 0 && static_cast<std::size_t>(index) < layout.rulerTicks.size()
1984 ? layout.rulerTicks[static_cast<std::size_t>(index)].x
1985 : 0.0f;
1986 });
1987 actionEditor.addFunc("getRulerTickMajor", [](ScriptActionTimelineEditor* self, int index) {
1988 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
1989 return index >= 0 && static_cast<std::size_t>(index) < layout.rulerTicks.size() &&
1990 layout.rulerTicks[static_cast<std::size_t>(index)].major;
1991 });
1992 actionEditor.addFunc("getItemCount", [](ScriptActionTimelineEditor* self) {
1993 return self ? static_cast<int>(self->widget().layout().items.size()) : 0;
1994 });
1995 actionEditor.addFunc("getItemId", [](ScriptActionTimelineEditor* self, int index) {
1996 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
1997 const auto* item = itemAt(layout, index);
1998 return item ? item->itemId.format() : std::string{};
1999 });
2000 actionEditor.addFunc("getItemType", [](ScriptActionTimelineEditor* self, int index) {
2001 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2002 const auto* item = itemAt(layout, index);
2003 return item ? item->type.format() : std::string{};
2004 });
2005 actionEditor.addFunc("getItemVisual", [](ScriptActionTimelineEditor* self, int index) {
2006 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2007 const auto* item = itemAt(layout, index);
2008 return item ? std::string(timelineItemVisualName(item->visual)) : std::string("custom");
2009 });
2010 actionEditor.addFunc("getItemLabel", [](ScriptActionTimelineEditor* self, int index) {
2011 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2012 const auto* item = itemAt(layout, index);
2013 return item ? item->displayName : std::string{};
2014 });
2015 actionEditor.addFunc("getItemDetail", [](ScriptActionTimelineEditor* self, int index) {
2016 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2017 const auto* item = itemAt(layout, index);
2018 return item ? item->detail : std::string{};
2019 });
2020 actionEditor.addFunc("getItemState", [](ScriptActionTimelineEditor* self, int index) {
2021 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2022 const auto* item = itemAt(layout, index);
2023 return item && item->state;
2024 });
2025 actionEditor.addFunc("getItemSelected", [](ScriptActionTimelineEditor* self, int index) {
2026 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2027 const auto* item = itemAt(layout, index);
2028 return item && item->selected;
2029 });
2030 actionEditor.addFunc("getItemMinX", [](ScriptActionTimelineEditor* self, int index) {
2031 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2032 const auto* item = itemAt(layout, index);
2033 return item ? item->minimumX : 0.0f;
2034 });
2035 actionEditor.addFunc("getItemMaxX", [](ScriptActionTimelineEditor* self, int index) {
2036 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2037 const auto* item = itemAt(layout, index);
2038 return item ? item->maximumX : 0.0f;
2039 });
2040 actionEditor.addFunc("getItemMinY", [](ScriptActionTimelineEditor* self, int index) {
2041 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2042 const auto* item = itemAt(layout, index);
2043 return item ? item->minimumY : 0.0f;
2044 });
2045 actionEditor.addFunc("getItemMaxY", [](ScriptActionTimelineEditor* self, int index) {
2046 const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{};
2047 const auto* item = itemAt(layout, index);
2048 return item ? item->maximumY : 0.0f;
2049 });
2050
2051 actionEditor.addFunc("getStateStart", [](ScriptActionTimelineEditor* self, const std::string& itemId) {
2052 const auto* state = self ? findState(self->editor().target().timeline(), itemId) : nullptr;
2053 return state ? static_cast<float>(state->start.seconds()) : 0.0f;
2054 });
2055 actionEditor.addFunc("getStateEnd", [](ScriptActionTimelineEditor* self, const std::string& itemId) {
2056 const auto* state = self ? findState(self->editor().target().timeline(), itemId) : nullptr;
2057 return state ? static_cast<float>(state->end.seconds()) : 0.0f;
2058 });
2059 actionEditor.addFunc("getItemStart", [](ScriptActionTimelineEditor* self, const std::string& itemId) {
2060 if (!self) return 0.0f;
2061 const auto& timeline = self->editor().target().timeline();
2062 if (const auto* section = findSection(timeline, itemId)) return static_cast<float>(section->start.seconds());
2063 if (const auto* state = findState(timeline, itemId)) return static_cast<float>(state->start.seconds());
2064 if (const auto* notify = findNotify(timeline, itemId)) return static_cast<float>(notify->time.seconds());
2065 return 0.0f;
2066 });
2067 actionEditor.addFunc("getItemEnd", [](ScriptActionTimelineEditor* self, const std::string& itemId) {
2068 if (!self) return 0.0f;
2069 const auto& timeline = self->editor().target().timeline();
2070 if (const auto* section = findSection(timeline, itemId)) return static_cast<float>(section->end.seconds());
2071 if (const auto* state = findState(timeline, itemId)) return static_cast<float>(state->end.seconds());
2072 if (const auto* notify = findNotify(timeline, itemId)) return static_cast<float>(notify->time.seconds());
2073 return 0.0f;
2074 });
2075 actionEditor.addFunc("getItemEnabled", [](ScriptActionTimelineEditor* self, const std::string& itemId) {
2076 if (!self) return false;
2077 const auto& timeline = self->editor().target().timeline();
2078 if (findSection(timeline, itemId)) return true;
2079 if (const auto* state = findState(timeline, itemId)) return state->enabled;
2080 if (const auto* notify = findNotify(timeline, itemId)) return notify->enabled;
2081 return false;
2082 });
2083 actionEditor.addFunc("getItemPayloadJson", [](ScriptActionTimelineEditor* self, const std::string& itemId) {
2084 if (!self) return std::string("{}");
2085 const auto& timeline = self->editor().target().timeline();
2086 const Value::Object* payload = nullptr;
2087 if (const auto* state = findState(timeline, itemId)) payload = &state->payload;
2088 if (const auto* notify = findNotify(timeline, itemId)) payload = &notify->payload;
2089 if (!payload) return std::string("{}");
2090 auto encoded = Value(*payload).toJson();
2091 return encoded ? encoded.value() : std::string("{}");
2092 });
2093 actionEditor.addFunc("patchItemPayload", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
2094 const std::string& fieldsJson) {
2095 auto parsed = Value::fromJson(fieldsJson);
2096 if (!parsed) return script::projectStatusResult(vm, parsed.status());
2097 const auto* fields = parsed.value().getIf<Value::Object>();
2098 if (!fields)
2099 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "payload patch must be a JSON object",
2100 "fieldsJson");
2101 return project(vm, patchItemPayload(self, itemId, *fields));
2102 });
2103 actionEditor.addFunc("setItemPayloadText", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
2104 const std::string& field, const std::string& value) {
2105 return project(vm, patchItemPayload(self, itemId, {{field, value}}));
2106 });
2107 actionEditor.addFunc("setItemPayloadNumber", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
2108 const std::string& field, float value) {
2109 return project(vm, patchItemPayload(self, itemId, {{field, static_cast<double>(value)}}));
2110 });
2111 actionEditor.addFunc("setItemPayloadInteger", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
2112 const std::string& field, int value) {
2113 return project(vm, patchItemPayload(self, itemId, {{field, static_cast<std::int64_t>(value)}}));
2114 });
2115 actionEditor.addFunc("setItemPayloadBool", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
2116 const std::string& field, bool value) {
2117 return project(vm, patchItemPayload(self, itemId, {{field, value}}));
2118 });
2119 actionEditor.addFunc("setItemPayloadVector3", [vm](ScriptActionTimelineEditor* self, const std::string& itemId,
2120 const std::string& field, float x, float y, float z) {
2121 return project(vm, patchItemPayload(self, itemId,
2122 {{field, Value::Array{static_cast<double>(x), static_cast<double>(y),
2123 static_cast<double>(z)}}}));
2124 });
2125 actionEditor.addFunc("setItemPayloadTextList", [vm](ScriptActionTimelineEditor* self,
2126 const std::string& itemId, const std::string& field,
2127 const std::string& delimitedValues) {
2129 std::size_t cursor = 0;
2130 while (cursor <= delimitedValues.size()) {
2131 const auto separator = delimitedValues.find(';', cursor);
2132 auto value = delimitedValues.substr(cursor, separator == std::string::npos
2133 ? std::string::npos
2134 : separator - cursor);
2135 const auto first = value.find_first_not_of(" \t\r\n");
2136 if (first != std::string::npos) {
2137 const auto last = value.find_last_not_of(" \t\r\n");
2138 values.emplace_back(value.substr(first, last - first + 1));
2139 }
2140 if (separator == std::string::npos) break;
2141 cursor = separator + 1;
2142 }
2143 return project(vm, patchItemPayload(self, itemId, {{field, std::move(values)}}));
2144 });
2145 actionEditor.addFunc("getItemPayloadText", [](ScriptActionTimelineEditor* self, const std::string& itemId,
2146 const std::string& field, const std::string& fallback) {
2147 const auto value = itemPayloadField(self, itemId, field);
2148 const auto* text = value ? value->getIf<std::string>() : nullptr;
2149 return text ? *text : fallback;
2150 });
2151 actionEditor.addFunc("getItemPayloadNumber", [](ScriptActionTimelineEditor* self, const std::string& itemId,
2152 const std::string& field, float fallback) {
2153 const auto value = itemPayloadField(self, itemId, field);
2154 if (!value) return fallback;
2155 if (const auto* decimal = value->getIf<double>()) return static_cast<float>(*decimal);
2156 if (const auto* integer = value->getIf<std::int64_t>()) return static_cast<float>(*integer);
2157 return fallback;
2158 });
2159 actionEditor.addFunc("getItemPayloadBool", [](ScriptActionTimelineEditor* self, const std::string& itemId,
2160 const std::string& field, bool fallback) {
2161 const auto value = itemPayloadField(self, itemId, field);
2162 const auto* boolean = value ? value->getIf<bool>() : nullptr;
2163 return boolean ? *boolean : fallback;
2164 });
2165 actionEditor.addFunc("getItemPayloadTextList", [](ScriptActionTimelineEditor* self,
2166 const std::string& itemId, const std::string& field) {
2167 const auto value = itemPayloadField(self, itemId, field);
2168 const auto* array = value ? value->getIf<Value::Array>() : nullptr;
2169 if (!array) return std::string{};
2170 std::string result;
2171 for (const auto& entry : *array) {
2172 const auto* text = entry.getIf<std::string>();
2173 if (!text) return std::string{};
2174 if (!result.empty()) result += "; ";
2175 result += *text;
2176 }
2177 return result;
2178 });
2179 actionEditor.addFunc("getItemPayloadVector", [](ScriptActionTimelineEditor* self, const std::string& itemId,
2180 const std::string& field, int component, float fallback) {
2181 const auto value = itemPayloadField(self, itemId, field);
2182 const auto* array = value ? value->getIf<Value::Array>() : nullptr;
2183 if (!array || component < 0 || component >= static_cast<int>(array->size())) return fallback;
2184 const auto& entry = (*array)[static_cast<std::size_t>(component)];
2185 if (const auto* decimal = entry.getIf<double>()) return static_cast<float>(*decimal);
2186 if (const auto* integer = entry.getIf<std::int64_t>()) return static_cast<float>(*integer);
2187 return fallback;
2188 });
2189 actionEditor.addFunc("getParameterKeyCount", [](ScriptActionTimelineEditor* self, const std::string& itemId) {
2190 const auto* state = self ? findState(self->editor().target().timeline(), itemId) : nullptr;
2191 if (!state || state->type.format() != "presentation:parameter-curve") return 0;
2193 return binding ? static_cast<int>(binding.value().keys.size()) : 0;
2194 });
2195 actionEditor.addFunc("getParameterKeyTime", [](ScriptActionTimelineEditor* self,
2196 const std::string& itemId, int index) {
2197 const auto* state = self ? findState(self->editor().target().timeline(), itemId) : nullptr;
2198 if (!state || index < 0) return 0.0f;
2200 return binding && static_cast<std::size_t>(index) < binding.value().keys.size()
2201 ? static_cast<float>(binding.value().keys[static_cast<std::size_t>(index)].time)
2202 : 0.0f;
2203 });
2204 actionEditor.addFunc("getParameterKeyValue", [](ScriptActionTimelineEditor* self,
2205 const std::string& itemId, int index) {
2206 const auto* state = self ? findState(self->editor().target().timeline(), itemId) : nullptr;
2207 if (!state || index < 0) return 0.0f;
2209 return binding && static_cast<std::size_t>(index) < binding.value().keys.size()
2210 ? static_cast<float>(binding.value().keys[static_cast<std::size_t>(index)].value)
2211 : 0.0f;
2212 });
2213 actionEditor.addFunc("getParameterKeyInterpolation", [](ScriptActionTimelineEditor* self,
2214 const std::string& itemId, int index) {
2215 const auto* state = self ? findState(self->editor().target().timeline(), itemId) : nullptr;
2216 if (!state || index < 0) return std::string{};
2218 return binding && static_cast<std::size_t>(index) < binding.value().keys.size()
2220 binding.value().keys[static_cast<std::size_t>(index)].interpolation))
2221 : std::string{};
2222 });
2223 actionEditor.addFunc("sampleParameterCurve", [](ScriptActionTimelineEditor* self,
2224 const std::string& itemId, float progress) {
2225 const auto* state = self ? findState(self->editor().target().timeline(), itemId) : nullptr;
2226 if (!state || !std::isfinite(progress)) return 0.0f;
2228 return binding ? static_cast<float>(binding.value().sample(progress)) : 0.0f;
2229 });
2230
2231 actionEditor.addFunc("getEventCount", [](ScriptActionTimelineEditor* self) {
2232 return self ? static_cast<int>(self->editor().previewEvents().size()) : 0;
2233 });
2234 actionEditor.addFunc("getEventItemId", [](ScriptActionTimelineEditor* self, int index) {
2235 if (!self) return std::string{};
2236 const auto& events = self->editor().previewEvents();
2237 return index >= 0 && static_cast<std::size_t>(index) < events.size()
2238 ? events[static_cast<std::size_t>(index)].itemId.format()
2239 : std::string{};
2240 });
2241 actionEditor.addFunc("getEventType", [](ScriptActionTimelineEditor* self, int index) {
2242 if (!self) return std::string{};
2243 const auto& events = self->editor().previewEvents();
2244 return index >= 0 && static_cast<std::size_t>(index) < events.size()
2245 ? events[static_cast<std::size_t>(index)].type.format()
2246 : std::string{};
2247 });
2248 actionEditor.addFunc("getEventTime", [](ScriptActionTimelineEditor* self, int index) {
2249 if (!self) return 0.0f;
2250 const auto& events = self->editor().previewEvents();
2251 return index >= 0 && static_cast<std::size_t>(index) < events.size()
2252 ? static_cast<float>(events[static_cast<std::size_t>(index)].time.seconds())
2253 : 0.0f;
2254 });
2255 actionEditor.addFunc("getEventKind", [](ScriptActionTimelineEditor* self, int index) {
2256 if (!self) return std::string{};
2257 const auto& events = self->editor().previewEvents();
2258 return index >= 0 && static_cast<std::size_t>(index) < events.size()
2259 ? eventKind(events[static_cast<std::size_t>(index)].kind)
2260 : std::string{};
2261 });
2262
2263 moduleClass.addFunc("createAssetCatalog", [vm](eve::action_editor::ActionEditorModule*,
2264 const std::string& projectRoot) {
2265 if (projectRoot.empty())
2266 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "project root must not be empty");
2267 return ::eve::editor::projectOwnedInstance<ScriptActionTimelineAssetCatalog>(
2268 vm, std::make_unique<ScriptActionTimelineAssetCatalog>(std::filesystem::path(projectRoot)));
2269 });
2270 moduleClass.addFunc("create", [vm, clipboard](eve::action_editor::ActionEditorModule*, const std::string& targetId,
2271 const ssq::Object& timelineObject) {
2272 if (targetId.empty())
2273 return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline target id must not be empty",
2274 "targetId");
2276 options.source = kBindingSource;
2277 auto value = script::valueFromSquirrel(timelineObject, options);
2278 if (!value) return script::projectStatusResult(vm, value.status());
2279 auto timeline = action::ActionTimeline::fromValue(value.value());
2280 if (!timeline) return script::projectStatusResult(vm, timeline.status());
2282 if (!registry) return script::projectStatusResult(vm, registry.status());
2283 return ::eve::editor::projectOwnedInstance<ScriptActionTimelineEditor>(
2284 vm, std::make_unique<ScriptActionTimelineEditor>(targetId, std::move(timeline).takeValue(),
2285 std::move(registry).takeValue(), clipboard));
2286 });
2287 moduleClass.addFunc(
2288 "openDocument",
2289 [vm, clipboard](eve::action_editor::ActionEditorModule*, const std::string& projectRoot,
2290 const std::string& assetGuid, const std::string& title, const std::string& resourceUri,
2291 const ssq::Object& initialTimelineObject) {
2292 if (projectRoot.empty() || assetGuid.empty() || resourceUri.empty())
2294 "project root, asset guid and resource URI are required");
2296 options.source = kBindingSource;
2297 auto value = script::valueFromSquirrel(initialTimelineObject, options);
2298 if (!value) return script::projectStatusResult(vm, value.status());
2299 auto initialTimeline = action::ActionTimeline::fromValue(value.value());
2300 if (!initialTimeline) return script::projectStatusResult(vm, initialTimeline.status());
2301 auto initialValue = initialTimeline.value().toValue();
2302 if (!initialValue) return script::projectStatusResult(vm, initialValue.status());
2303
2304 auto store = std::make_unique<DiskAtomicDocumentStore>(std::filesystem::path(projectRoot));
2305 auto documents = std::make_unique<DocumentService>(store.get());
2306 auto opened = documents->open({DocumentKind::Timeline, AssetGuid(assetGuid)}, title, resourceUri,
2307 toEditorValue(initialValue.value()));
2308 if (!opened.ok()) return script::projectStatusResult(vm, opened.status());
2309 auto content = documents->content(opened.value().id);
2310 if (!content.ok()) return script::projectStatusResult(vm, content.status());
2311 auto timeline = action::ActionTimeline::fromValue(toPresentationValue(content.value()));
2312 if (!timeline) return script::projectStatusResult(vm, timeline.status());
2314 if (!registry) return script::projectStatusResult(vm, registry.status());
2315 auto instance = std::make_unique<ScriptActionTimelineEditor>(
2316 assetGuid, std::move(timeline).takeValue(), std::move(registry).takeValue(), clipboard);
2317 instance->attachDocument(std::move(store), std::move(documents), opened.value());
2318 return ::eve::editor::projectOwnedInstance<ScriptActionTimelineEditor>(vm, std::move(instance));
2319 });
2320}
2321
2322} // namespace eve::editor
Deterministic notify-state lifecycle routing.
LogicalId target
double value
Extensible notify descriptors, validation and runtime routing.
Typed continuous parameter-curve action block.
Atomic action-preview transport and presentation orchestration.
UI-neutral transactional action timeline editor and preview model.
LogicalId trackId
Value::Object payload
Validated partial editing for action-block payloads.
Squirrel adapter for the canonical action timeline editor.
bool locked
Duration start
LogicalId itemId
Renderer-neutral interactive action timeline widget.
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
float duration
std::string descriptor
eve::EntitySpatialPose pose
int mask
Vec3 projected
Definition CaveMesh.cpp:122
Vec3 anchor
Definition CaveMesh.cpp:90
struct SQVM * HSQUIRRELVM
std::string title
std::string label
HSQUIRRELVM vm
Definition ECS.cpp:20
std::map< std::string, Var > values
std::string widget
std::vector< std::uint8_t > stored
Definition Evpack.cpp:171
std::string layout
std::string message
DiagnosticCode code
const GltfImportRequest & request
wgpu::PopErrorScopeStatus status
glm::vec4 clip
std::int32_t first
float blend
std::uint32_t width
std::string text
TokenKind kind
size_t offset
Range range
std::vector< Track > tracks
bool valid
Layered dual-slot montage ownership and stable handles.
Deterministic presentation-only action montage playback.
graphics::Canvas * previous
const RuntimeTensor * borrowed
eve::action::ActionVfxBinding binding
TileLayer * layer
std::string path
Definition PlayHost.cpp:110
std::vector< std::string > fields
Definition PlayHost.cpp:111
std::string uri
std::uint32_t generation
std::uint32_t targetId
glm::mat4 model
bool found
bool boolean
std::uint32_t count
const SquirrelValueOptions & options
The single Squirrel projection for common Result, Status and Value.
Explicit Value/Owned/Borrowed semantics at the Squirrel boundary.
Battle::Events events
std::size_t cursor
Heightmap curve
TerrainThermalSettings settings
float size
Definition TreeMesh.cpp:156
uint32_t index
std::size_t at
const VegetationPresetContext & context
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
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 > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static constexpr RuntimeHandle invalid() noexcept
Returns the canonical invalid handle.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
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
static Result< ActionNotifyRegistry > withBuiltins()
Build a registry containing the engine's standard semantic notify types.
Composition adapter that contributes action-timeline commands, automation targets,...
Per-bone weights used by override and additive animation layers.
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
Definition AnimClip.h:145
static AnimClip * loadClipFromModel(const model3d::ModelData *model, const AnimSkeleton *skeleton, int animIndex=0)
Loads clip from model.
static int getAnimationCountFromModel(const model3d::ModelData *model)
Returns the animation count from model.
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
Definition AnimPose.h:17
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
static constexpr StrongUint64 zero() noexcept
Returns the zero value for this strong type.
Applies typed partial payload changes through the canonical timeline transaction authority.
Result< void > fitClipToBlock(const LogicalId &itemId)
Extend one VFX state's clip end so its authored interval matches the block duration.
Result< void > fitBlockToClip(const LogicalId &itemId)
Resize one VFX state to its authored clip interval at 1.0x playback.
Result< void > fitClipToNaturalDuration(const LogicalId &itemId, OptionalRef< const action::IActionVfxDurationProvider > provider)
Set one VFX state's clip end to the finite duration reported by its resource provider.
Composition model shared by project-specific developer and runtime editors.
CPU-side decoded 3D model (Assimp scene owned via medialoader::ModelScene). Does not upload to GPU — ...
Definition ModelData.h:39
std::string_view actionTrackKindName(ActionTrackKind kind) noexcept
Stable lowercase protocol spelling for a track kind.
std::optional< ActionBlendCurve > actionBlendCurveFromName(std::string_view name) noexcept
Parse a persisted montage cross-fade curve name.
Result< ActionTrackKind > parseActionTrackKind(std::string_view text)
Parse a stable track kind spelling.
std::string_view actionBlendCurveName(ActionBlendCurve curve) noexcept
Stable persisted name for a montage cross-fade curve.
ActionTimelineEventKind
Boundary emitted by deterministic timeline sampling.
std::optional< ActionParameterInterpolation > actionParameterInterpolationFromName(std::string_view value) noexcept
Parse a stable persisted interpolation spelling.
eve::detail::StrongUint64< ActionExecutionIdTag > ActionExecutionId
Definition Action.h:157
std::string_view actionParameterInterpolationName(ActionParameterInterpolation value) noexcept
Stable persisted interpolation spelling.
RuntimeHandle< MontageHandleTag > MontageHandle
Generation-qualified identity owned by one MontageCoordinator.
I * query()
Queries .
Definition Capability.h:88
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
eve::Value toPresentationValue(const Value &value)
To presentation value.
Result< T > applied(T value, std::vector< Diagnostic > diagnostics={})
Construct an Applied result with an owning payload and optional diagnostics.
Result< void > noOp(std::vector< Diagnostic > diagnostics={})
Construct a successful void result for an operation that made no change.
eve::editing::Value EditorValue
Compatibility alias for the UI-independent authoring value tree.
Definition EditorValue.h:8
EditorValue toEditorValue(const eve::Value &value)
Compatibility spelling for conversion into the authoring value tree.
eve::editing::RuleId RuleId
Definition EditorIds.h:32
void exposeActionTimelineScriptBindings(ssq::Table &table, ssq::Class &moduleClass)
Expose the script-owned action timeline editor adapter.
std::string_view timelineItemVisualName(TimelineItemVisual visual) noexcept
Return the stable script-facing name for a timeline visual kind.
eve::editing::AssetGuid AssetGuid
Definition EditorIds.h:39
ssq::Table project(HSQUIRRELVM vm, const eve::Result< void > &result)
Project a completed editing result that has no payload.
ssq::Table bindingFailure(HSQUIRRELVM vm, const char *source, DiagnosticCode code, std::string message, std::string path={})
Project a binding-layer argument failure with a stable diagnostic source.
ssq::Table projectStatusResult(HSQUIRRELVM vm, const Status &status)
Project a checked native status that carries no payload.
ssq::Table projectResult(HSQUIRRELVM vm, Result< void > &&result)
Consume and project a void native Result using the common schema.
Result< Value > valueFromSquirrel(HSQUIRRELVM vm, SQInteger index, const SquirrelValueOptions &options)
Convert one Squirrel value into the canonical owning Value tree.
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
std::optional< std::reference_wrapper< T > > OptionalRef
Optional borrowed reference; it does not extend T's lifetime.
Definition BorrowedRef.h:24
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
bool enabled
Objective animation segment placed on the montage presentation lane.
Montage-wide presentation settings persisted with the action timeline.
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.
static Result< ActionTimeline > fromValue(const Value &value)
Decode and validate one canonical schema value transactionally.
Ordered semantic lane in one action timeline.
Complete renderer-neutral layout for one widget frame.
std::vector< TimelineRulerTick > rulerTicks
Limits and provenance used by the Squirrel ↔ Value adapter.