#include "action/editor/ActionTimelineScriptBindings.h" #include "action/ActionNotifyRegistry.h" #include "action/editor/ActionEditorModule.h" #include "action/editor/ActionTimelineEditor.h" #include "action/editor/ActionTimelineWidget.h" #include "animation/AnimClip.h" #include "animation/AnimImporter.h" #include "animation/AnimPose.h" #include "animation/MontagePlayer.h" #include "common/SquirrelBinding.h" #include "common/SquirrelOwnership.h" #include "editor/EditorWorkspace.h" #include "model3d/ModelData.h" #include #include #include #include #include #include #include #include namespace eve::editor { namespace { constexpr const char* kBindingSource = "editor.action.timeline.squirrel"; Status statusFrom(const EditorResult& result) { return result.status(); } template Status statusFrom(const EditorResult& result) { return result.status(); } ssq::Table project(HSQUIRRELVM vm, const EditorResult& result) { return script::projectStatusResult(vm, statusFrom(result), result.ok(), false); } template ssq::Table project(HSQUIRRELVM vm, const EditorResult& result, Value value) { const bool hasValue = result.ok(); return script::projectStatusResult(vm, statusFrom(result), hasValue, hasValue, value); } ssq::Table bindingFailure(HSQUIRRELVM vm, DiagnosticCode code, std::string message, std::string path = {}) { return script::projectStatusResult( vm, Status::failure(Diagnostic::error(code, std::move(message), std::move(path), {}, kBindingSource)), false, false); } Result seconds(float value) { auto converted = Duration::fromSeconds(static_cast(value)); if (!converted) return Result::failure(converted.status()); return Result::success(std::move(converted).takeValue()); } const action::ActionTrack* trackAt(const action::ActionTimeline& timeline, int index) { if (index < 0 || static_cast(index) >= timeline.tracks.size()) return nullptr; return &timeline.tracks[static_cast(index)]; } const action::ActionAnimationSection* sectionAt(const action::ActionTimeline& timeline, int index) { if (index < 0 || static_cast(index) >= timeline.animationSections.size()) return nullptr; return &timeline.animationSections[static_cast(index)]; } const TimelineItemGeometry* itemAt(const TimelineWidgetLayout& layout, int index) { if (index < 0 || static_cast(index) >= layout.items.size()) return nullptr; return &layout.items[static_cast(index)]; } const action::ActionNotifyState* findState(const action::ActionTimeline& timeline, const std::string& itemId) { for (const auto& track : timeline.tracks) { const auto found = std::find_if(track.states.begin(), track.states.end(), [&](const auto& state) { return state.id.format() == itemId; }); if (found != track.states.end()) return &*found; } return nullptr; } Value montageAdvanceValue(const animation::MontageAdvance& value) { Value::Object projected; projected["previousSeconds"] = value.previous.seconds(); projected["currentSeconds"] = value.current.seconds(); projected["sectionId"] = value.sectionId ? value.sectionId->format() : ""; projected["eventCount"] = static_cast(value.events.size()); projected["activeBlockCount"] = static_cast(value.activeBlocks.size()); projected["rootMotionX"] = static_cast(value.rootMotion.px); projected["rootMotionY"] = static_cast(value.rootMotion.py); projected["rootMotionZ"] = static_cast(value.rootMotion.pz); projected["weight"] = value.weight; projected["completed"] = value.completed; return Value(std::move(projected)); } void accumulateRootMotion(animation::TransformTRS& total, const animation::TransformTRS& delta) { total.px += delta.px; total.py += delta.py; total.pz += delta.pz; const float tx = total.qx; const float ty = total.qy; const float tz = total.qz; const float tw = total.qw; total.qx = tw * delta.qx + tx * delta.qw + ty * delta.qz - tz * delta.qy; total.qy = tw * delta.qy - tx * delta.qz + ty * delta.qw + tz * delta.qx; total.qz = tw * delta.qz + tx * delta.qy - ty * delta.qx + tz * delta.qw; total.qw = tw * delta.qw - tx * delta.qx - ty * delta.qy - tz * delta.qz; } class ScriptActionTimelineEditor { public: ScriptActionTimelineEditor(std::string targetId, action::ActionTimeline timeline, action::ActionNotifyRegistry registry) : registry_(std::move(registry)), editor_(std::move(targetId), std::move(timeline)), widget_(editor_, registry_) {} ActionTimelineEditor& editor() noexcept { return editor_; } ActionTimelineWidget& widget() noexcept { return widget_; } const ActionTimelineEditor& editor() const noexcept { return editor_; } const ActionTimelineWidget& widget() const noexcept { return widget_; } [[nodiscard]] Result registerRuntimeClip(std::string uri, model3d::ModelData& model, animation::AnimSkeleton& skeleton, int animationIndex) { if (uri.empty()) return Result::failure( Diagnostic::error(DiagnosticCode::InvalidArgument, "montage clip URI must not be empty", "uri")); if (animationIndex < 0 || animationIndex >= animation::AnimImporter::getAnimationCountFromModel(&model)) return Result::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "montage animation index is outside the model", "index")); std::unique_ptr clip( animation::AnimImporter::loadClipFromModel(&model, &skeleton, animationIndex)); if (!clip) return Result::failure( Diagnostic::error(DiagnosticCode::Failed, "montage clip import produced no animation", uri)); if (montage_) return montage_->replaceClip(uri, std::move(clip)); const auto found = std::find_if(runtimeClips_.begin(), runtimeClips_.end(), [&](const auto& asset) { return asset.uri == uri; }); if (found == runtimeClips_.end()) runtimeClips_.push_back({std::move(uri), std::move(clip)}); else found->clip = std::move(clip); return Result::success(Status::success(StatusCode::Applied)); } [[nodiscard]] Result beginRuntime(animation::AnimSkeleton& skeleton) { auto montage = std::make_unique(skeleton); auto timeline = editor_.target().timeline(); auto prepared = montage->prepare(timeline, std::move(runtimeClips_)); if (!prepared) return Result::failure(prepared.status()); runtimeTimeline_ = timeline; montage_ = std::move(montage); tick_ = SimulationTick::zero(); runtimeAdvance_.reset(); runtimeRate_ = timeline.montage.basePlayRate; runtimePaused_ = false; sectionRates_.clear(); return restartExecution(); } [[nodiscard]] Result advanceRuntime(Duration delta) { if (!runtime_ || !montage_) return Result::failure( Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime")); if (montage_->isBlendingOut()) { tick_ = SimulationTick(tick_.value() + 1); auto settled = montage_->advanceBlendOut(delta, tick_); if (settled) runtimeAdvance_ = settled.value(); return settled; } if (runtimePaused_) { animation::MontageAdvance paused; paused.previous = paused.current = montage_->time(); paused.weight = montage_->weight(); return Result::success(std::move(paused), Status::success(StatusCode::NoOp)); } double sectionRate = 1.0; auto sectionIndex = montage_->physicalSectionIndex(); if (sectionIndex) { const auto found = sectionRates_.find(sectionIndex.value()); if (found != sectionRates_.end()) sectionRate = found->second; } auto scaledDelta = Duration::fromSeconds(delta.seconds() * runtimeRate_ * sectionRate); if (!scaledDelta) return Result::failure(scaledDelta.status()); Duration remainingDelta = std::move(scaledDelta).takeValue(); animation::MontageAdvance combined; combined.previous = montage_->time(); bool firstSlice = true; while (true) { Duration remainingTimeline = Duration::fromNanoseconds(runtimeTimeline_->duration.nanoseconds() - montage_->time().nanoseconds()); if (runtimeTimeline_->montage.looping && remainingTimeline.isZero()) { auto restarted = restartExecution(); if (!restarted) return Result::failure(restarted.status()); remainingTimeline = runtimeTimeline_->duration; } const Duration slice = runtimeTimeline_->montage.looping ? std::min(remainingDelta, remainingTimeline) : remainingDelta; tick_ = SimulationTick(tick_.value() + 1); auto actionAdvance = runtime_->advance(executionId_, tick_, slice); if (!actionAdvance) return Result::failure(actionAdvance.status()); auto presented = montage_->present(actionAdvance.value(), tick_); if (!presented) return Result::failure(presented.status()); auto value = std::move(presented).takeValue(); if (firstSlice) { combined = std::move(value); firstSlice = false; } else { combined.current = value.current; combined.sectionId = value.sectionId; combined.events.insert(combined.events.end(), std::make_move_iterator(value.events.begin()), std::make_move_iterator(value.events.end())); combined.activeBlocks = std::move(value.activeBlocks); accumulateRootMotion(combined.rootMotion, value.rootMotion); combined.weight = value.weight; combined.completed = value.completed; } if (!runtimeTimeline_->montage.looping || remainingDelta <= slice) break; remainingDelta = Duration::fromNanoseconds(remainingDelta.nanoseconds() - slice.nanoseconds()); auto restarted = restartExecution(); if (!restarted) return Result::failure(restarted.status()); } if (runtimeTimeline_->montage.looping && combined.completed) { auto restarted = restartExecution(); if (!restarted) return Result::failure(restarted.status()); combined.current = Duration::zero(); combined.sectionId.reset(); combined.activeBlocks.clear(); combined.completed = false; combined.weight = montage_->weight(); } runtimeAdvance_ = combined; return Result::success(std::move(combined), Status::success(StatusCode::Pending)); } [[nodiscard]] Result jumpRuntime(Duration target) { if (!runtimeTimeline_ || !montage_) return Result::failure( Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime")); auto restarted = restartExecution(false); if (!restarted) return Result::failure(restarted.status()); auto rebound = montage_->rebindExecution(executionId_); if (!rebound) return Result::failure(rebound.status()); tick_ = SimulationTick(tick_.value() + 1); auto advanced = runtime_->advance(executionId_, tick_, target); if (!advanced) return Result::failure(advanced.status()); auto jumped = montage_->jumpToTime(executionId_, target, tick_); if (jumped) runtimeAdvance_ = jumped.value(); return jumped; } [[nodiscard]] Result jumpRuntimeSection(std::size_t index) { if (!runtimeTimeline_) return Result::failure( Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime")); auto range = runtimeTimeline_->sectionRange(index); if (!range) return Result::failure(range.status()); return jumpRuntime(range.value().first); } [[nodiscard]] Result syncRuntimeSection(std::size_t index, Duration targetDuration) { if (!runtimeTimeline_) return Result::failure( Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime")); if (targetDuration <= Duration::zero()) return Result::failure(Diagnostic::error(DiagnosticCode::InvalidArgument, "target section duration must be positive", "duration")); auto range = runtimeTimeline_->sectionRange(index); if (!range) return Result::failure(range.status()); const double raw = (range.value().second.seconds() - range.value().first.seconds()); sectionRates_[index] = raw / targetDuration.seconds(); return Result::success(Status::success(StatusCode::Applied)); } [[nodiscard]] Result beginRuntimeBlendOut(Duration duration) { if (!montage_) return Result::failure( Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime")); tick_ = SimulationTick(tick_.value() + 1); auto stopped = montage_->beginBlendOut(duration, tick_); if (stopped) runtimeAdvance_ = stopped.value(); return stopped; } void setRuntimePaused(bool paused) noexcept { runtimePaused_ = paused; } [[nodiscard]] bool runtimePaused() const noexcept { return runtimePaused_; } [[nodiscard]] Result cancelRuntime() { if (!runtime_ || !montage_) return Result::success(Status::success(StatusCode::NoOp)); tick_ = SimulationTick(tick_.value() + 1); auto cancelled = runtime_->cancel(executionId_, tick_); if (!cancelled) return cancelled; montage_->stop(); return Result::success(Status::success(StatusCode::Applied)); } [[nodiscard]] animation::AnimPose* runtimePose() noexcept { return montage_ ? &montage_->pose() : nullptr; } [[nodiscard]] const std::optional& runtimeAdvance() const noexcept { return runtimeAdvance_; } [[nodiscard]] bool runtimePlaying() const noexcept { return montage_ && montage_->isPlaying(); } [[nodiscard]] Result runtimePhysicalSection() const { if (!montage_) return Result::failure( Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime")); return montage_->physicalSectionIndex(); } [[nodiscard]] Result runtimeSectionProgress(std::size_t index) const { if (!montage_) return Result::failure( Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime")); return montage_->physicalSectionProgress(index); } private: [[nodiscard]] action::ActionDefinition runtimeDefinition() const { action::ActionDefinition definition; definition.id = runtimeTimeline_->actionId; if (runtimeTimeline_->splitTimestamps.size() >= 2) { definition.timing.windup = runtimeTimeline_->splitTimestamps[0]; definition.timing.active = Duration::fromNanoseconds(runtimeTimeline_->splitTimestamps[1].nanoseconds() - runtimeTimeline_->splitTimestamps[0].nanoseconds()); definition.timing.recover = Duration::fromNanoseconds(runtimeTimeline_->duration.nanoseconds() - runtimeTimeline_->splitTimestamps[1].nanoseconds()); } else { definition.timing.active = runtimeTimeline_->duration; } definition.timeline = *runtimeTimeline_; return definition; } [[nodiscard]] Result restartExecution(bool playMontage = true) { auto runtime = std::make_unique(); auto definition = runtimeDefinition(); action::ActionRequest request; request.actionId = definition.id; auto submitted = runtime->submit(std::move(definition), std::move(request)); if (!submitted) return Result::failure(submitted.status()); if (playMontage) { auto played = montage_->play(submitted.value()); if (!played) return Result::failure(played.status()); } runtime_ = std::move(runtime); executionId_ = submitted.value(); return Result::success(Status::success(StatusCode::Applied)); } action::ActionNotifyRegistry registry_; ActionTimelineEditor editor_; ActionTimelineWidget widget_; std::vector runtimeClips_; std::unique_ptr runtime_; std::unique_ptr montage_; action::ActionExecutionId executionId_{}; SimulationTick tick_ = SimulationTick::zero(); std::optional runtimeAdvance_; std::optional runtimeTimeline_; std::unordered_map sectionRates_; double runtimeRate_ = 1.0; bool runtimePaused_ = false; }; std::string eventKind(action::ActionTimelineEventKind kind) { switch (kind) { case action::ActionTimelineEventKind::Notify: return "notify"; case action::ActionTimelineEventKind::StateEnter: return "state_enter"; case action::ActionTimelineEventKind::StateExit: return "state_exit"; } return "unknown"; } } // namespace void exposeActionTimelineScriptBindings(ssq::Table& table, ssq::Class& moduleClass) { const HSQUIRRELVM vm = table.getHandle(); auto actionEditor = table.addClass( "ActionTimelineEditor", std::function([]() -> ScriptActionTimelineEditor* { return nullptr; }), true); actionEditor.addFunc("configureWorkspace", [vm](ScriptActionTimelineEditor* self, EditorWorkspace* workspace) { if (!self || !workspace) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor and workspace must not be null", "workspace"); return project(vm, self->editor().configureWorkspace(*workspace)); }); actionEditor.addFunc( "setViewport", [vm](ScriptActionTimelineEditor* self, float width, float rowHeight, float labelWidth) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); return project(vm, self->widget().setViewport(width, rowHeight, labelWidth)); }); actionEditor.addFunc("addTrack", [vm](ScriptActionTimelineEditor* self, const std::string& trackId, const std::string& label, const std::string& kind) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null"); auto parsedId = LogicalId::parse(trackId); auto parsedKind = action::parseActionTrackKind(kind); if (!parsedId) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId"); if (!parsedKind) return script::projectStatusResult(vm, parsedKind.status(), false, false); action::ActionTrack track; track.id = std::move(*parsedId); track.label = label; track.kind = parsedKind.value(); return project(vm, self->editor().addTrack(std::move(track))); }); actionEditor.addFunc("removeTrack", [vm](ScriptActionTimelineEditor* self, const std::string& trackId) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null"); auto parsed = LogicalId::parse(trackId); if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId"); return project(vm, self->editor().removeTrack(*parsed)); }); actionEditor.addFunc( "renameTrack", [vm](ScriptActionTimelineEditor* self, const std::string& trackId, const std::string& label) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null"); auto parsed = LogicalId::parse(trackId); if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId"); return project(vm, self->editor().renameTrack(*parsed, label)); }); actionEditor.addFunc( "setTrackMuted", [vm](ScriptActionTimelineEditor* self, const std::string& trackId, bool muted) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null"); auto parsed = LogicalId::parse(trackId); if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId"); return project(vm, self->editor().setTrackMuted(*parsed, muted)); }); actionEditor.addFunc( "setTrackLocked", [vm](ScriptActionTimelineEditor* self, const std::string& trackId, bool locked) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor is null"); auto parsed = LogicalId::parse(trackId); if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "track id is invalid", "trackId"); return project(vm, self->editor().setTrackLocked(*parsed, locked)); }); actionEditor.addFunc("setSnapSeconds", [vm](ScriptActionTimelineEditor* self, float intervalSeconds) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto interval = seconds(intervalSeconds); if (!interval) return script::projectStatusResult(vm, interval.status(), false, false); return project(vm, self->widget().setSnapInterval(std::move(interval).takeValue())); }); actionEditor.addFunc( "setVisibleSeconds", [vm](ScriptActionTimelineEditor* self, float startSeconds, float endSeconds) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto start = seconds(startSeconds); auto end = seconds(endSeconds); if (!start) return script::projectStatusResult(vm, start.status(), false, false); if (!end) return script::projectStatusResult(vm, end.status(), false, false); return project(vm, self->widget().setVisibleRange(start.value(), end.value())); }); actionEditor.addFunc("zoomTimeline", [vm](ScriptActionTimelineEditor* self, float factor, float anchor) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); return project(vm, self->widget().zoom(factor, anchor)); }); actionEditor.addFunc("panTimelineSeconds", [vm](ScriptActionTimelineEditor* self, float deltaSeconds) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto delta = seconds(deltaSeconds); if (!delta) return script::projectStatusResult(vm, delta.status(), false, false); return project(vm, self->widget().pan(delta.value())); }); actionEditor.addFunc("pointerDown", [vm](ScriptActionTimelineEditor* self, float x, float y, bool additive) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); return project(vm, self->widget().pointerDown(x, y, additive)); }); actionEditor.addFunc("pointerMove", [vm](ScriptActionTimelineEditor* self, float x) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); return project(vm, self->widget().pointerMove(x)); }); actionEditor.addFunc("pointerUp", [vm](ScriptActionTimelineEditor* self, float x) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); return project(vm, self->widget().pointerUp(x)); }); actionEditor.addFunc("seekX", [vm](ScriptActionTimelineEditor* self, float x) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); return project(vm, self->widget().seek(x)); }); actionEditor.addFunc("seekSeconds", [vm](ScriptActionTimelineEditor* self, float value) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto duration = seconds(value); if (!duration) return script::projectStatusResult(vm, duration.status(), false, false); return project(vm, self->editor().seek(std::move(duration).takeValue())); }); actionEditor.addFunc("resizeState", [vm](ScriptActionTimelineEditor* self, const std::string& itemId, float startSeconds, float endSeconds) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto start = seconds(startSeconds); if (!start) return script::projectStatusResult(vm, start.status(), false, false); auto end = seconds(endSeconds); if (!end) return script::projectStatusResult(vm, end.status(), false, false); auto parsedItemId = LogicalId::parse(itemId); if (!parsedItemId) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "state item id is not canonical", "itemId"); return project( vm, self->editor().resizeState(*parsedItemId, std::move(start).takeValue(), std::move(end).takeValue())); }); actionEditor.addFunc("undo", [vm](ScriptActionTimelineEditor* self) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto result = self->editor().undo(); return project(vm, result, Value(result.ok() ? static_cast(result.value().afterRevision) : 0)); }); actionEditor.addFunc("redo", [vm](ScriptActionTimelineEditor* self) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto result = self->editor().redo(); return project(vm, result, Value(result.ok() ? static_cast(result.value().afterRevision) : 0)); }); actionEditor.addFunc("update", [vm](ScriptActionTimelineEditor* self, float deltaSeconds) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto delta = seconds(deltaSeconds); if (!delta) return script::projectStatusResult(vm, delta.status(), false, false); auto result = self->editor().update(std::move(delta).takeValue()); return project(vm, result, Value(result.ok() ? static_cast(result.value()) : 0)); }); actionEditor.addFunc("snapshot", [vm](ScriptActionTimelineEditor* self) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); return script::projectResult(vm, self->editor().target().timeline().toValue(), [](Value value) { return value; }); }); actionEditor.addFunc( "registerRuntimeClip", [vm](ScriptActionTimelineEditor* self, const std::string& uri, model3d::ModelData* model, animation::AnimSkeleton* skeleton, int animationIndex) { if (!self || !model || !skeleton) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "editor, model and skeleton must not be null", "runtimeClip"); return script::projectResult(vm, self->registerRuntimeClip(uri, *model, *skeleton, animationIndex)); }); actionEditor.addFunc("beginRuntime", [vm](ScriptActionTimelineEditor* self, animation::AnimSkeleton* skeleton) { if (!self || !skeleton) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "editor and skeleton must not be null", "skeleton"); return script::projectResult(vm, self->beginRuntime(*skeleton)); }); actionEditor.addFunc("advanceRuntime", [vm](ScriptActionTimelineEditor* self, float deltaSeconds) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto delta = seconds(deltaSeconds); if (!delta) return script::projectStatusResult(vm, delta.status(), false, false); return script::projectResult(vm, self->advanceRuntime(std::move(delta).takeValue()), montageAdvanceValue); }); actionEditor.addFunc("jumpRuntimeSeconds", [vm](ScriptActionTimelineEditor* self, float targetSeconds) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto target = seconds(targetSeconds); if (!target) return script::projectStatusResult(vm, target.status(), false, false); return script::projectResult(vm, self->jumpRuntime(std::move(target).takeValue()), montageAdvanceValue); }); actionEditor.addFunc("jumpRuntimeSection", [vm](ScriptActionTimelineEditor* self, int sectionIndex) { if (!self || sectionIndex < 0) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "runtime section index is invalid", "index"); return script::projectResult(vm, self->jumpRuntimeSection(static_cast(sectionIndex)), montageAdvanceValue); }); actionEditor.addFunc( "syncRuntimeSection", [vm](ScriptActionTimelineEditor* self, int sectionIndex, float targetSeconds) { if (!self || sectionIndex < 0) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "runtime section index is invalid", "index"); auto target = seconds(targetSeconds); if (!target) return script::projectStatusResult(vm, target.status(), false, false); return script::projectResult( vm, self->syncRuntimeSection(static_cast(sectionIndex), std::move(target).takeValue())); }); actionEditor.addFunc("beginRuntimeBlendOut", [vm](ScriptActionTimelineEditor* self, float durationSeconds) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto duration = seconds(durationSeconds); if (!duration) return script::projectStatusResult(vm, duration.status(), false, false); return script::projectResult(vm, self->beginRuntimeBlendOut(std::move(duration).takeValue()), montageAdvanceValue); }); actionEditor.addFunc("setRuntimePaused", [](ScriptActionTimelineEditor* self, bool paused) { if (self) self->setRuntimePaused(paused); }); actionEditor.addFunc("isRuntimePaused", [](ScriptActionTimelineEditor* self) { return self && self->runtimePaused(); }); actionEditor.addFunc("cancelRuntime", [vm](ScriptActionTimelineEditor* self) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); return script::projectResult(vm, self->cancelRuntime()); }); actionEditor.addFunc("getRuntimePose", [](ScriptActionTimelineEditor* self) { return self ? self->runtimePose() : nullptr; }); actionEditor.addFunc("isRuntimePlaying", [](ScriptActionTimelineEditor* self) { return self && self->runtimePlaying(); }); actionEditor.addFunc("getRuntimeSectionId", [](ScriptActionTimelineEditor* self) { if (!self || !self->runtimeAdvance() || !self->runtimeAdvance()->sectionId) return std::string{}; return self->runtimeAdvance()->sectionId->format(); }); actionEditor.addFunc("getRuntimeRootMotionX", [](ScriptActionTimelineEditor* self) { return self && self->runtimeAdvance() ? self->runtimeAdvance()->rootMotion.px : 0.0f; }); actionEditor.addFunc("getRuntimeRootMotionY", [](ScriptActionTimelineEditor* self) { return self && self->runtimeAdvance() ? self->runtimeAdvance()->rootMotion.py : 0.0f; }); actionEditor.addFunc("getRuntimeRootMotionZ", [](ScriptActionTimelineEditor* self) { return self && self->runtimeAdvance() ? self->runtimeAdvance()->rootMotion.pz : 0.0f; }); actionEditor.addFunc("getRuntimeWeight", [](ScriptActionTimelineEditor* self) { return self && self->runtimeAdvance() ? static_cast(self->runtimeAdvance()->weight) : 0.0f; }); actionEditor.addFunc("getRuntimeEventCount", [](ScriptActionTimelineEditor* self) { return self && self->runtimeAdvance() ? static_cast(self->runtimeAdvance()->events.size()) : 0; }); actionEditor.addFunc("getRuntimeEventKind", [](ScriptActionTimelineEditor* self, int index) { if (!self || !self->runtimeAdvance() || index < 0 || static_cast(index) >= self->runtimeAdvance()->events.size()) return std::string{}; return eventKind(self->runtimeAdvance()->events[static_cast(index)].kind); }); actionEditor.addFunc("getRuntimeEventType", [](ScriptActionTimelineEditor* self, int index) { if (!self || !self->runtimeAdvance() || index < 0 || static_cast(index) >= self->runtimeAdvance()->events.size()) return std::string{}; return self->runtimeAdvance()->events[static_cast(index)].type.format(); }); actionEditor.addFunc("getRuntimeEventSeconds", [](ScriptActionTimelineEditor* self, int index) { if (!self || !self->runtimeAdvance() || index < 0 || static_cast(index) >= self->runtimeAdvance()->events.size()) return 0.0f; return static_cast(self->runtimeAdvance()->events[static_cast(index)].time.seconds()); }); actionEditor.addFunc("getRuntimeActiveBlockCount", [](ScriptActionTimelineEditor* self) { return self && self->runtimeAdvance() ? static_cast(self->runtimeAdvance()->activeBlocks.size()) : 0; }); actionEditor.addFunc("getRuntimeActiveBlockType", [](ScriptActionTimelineEditor* self, int index) { if (!self || !self->runtimeAdvance() || index < 0 || static_cast(index) >= self->runtimeAdvance()->activeBlocks.size()) return std::string{}; return self->runtimeAdvance()->activeBlocks[static_cast(index)].type.format(); }); actionEditor.addFunc("getRuntimeActiveBlockLocalSeconds", [](ScriptActionTimelineEditor* self, int index) { if (!self || !self->runtimeAdvance() || index < 0 || static_cast(index) >= self->runtimeAdvance()->activeBlocks.size()) return 0.0f; return static_cast( self->runtimeAdvance()->activeBlocks[static_cast(index)].localTime.seconds()); }); actionEditor.addFunc("getRuntimePhysicalSection", [](ScriptActionTimelineEditor* self) { if (!self) return -1; auto result = self->runtimePhysicalSection(); return result ? static_cast(result.value()) : -1; }); actionEditor.addFunc("getRuntimeSectionProgress", [](ScriptActionTimelineEditor* self, int sectionIndex) { if (!self || sectionIndex < 0) return 0.0f; auto result = self->runtimeSectionProgress(static_cast(sectionIndex)); return result ? static_cast(result.value()) : 0.0f; }); actionEditor.addFunc("play", [](ScriptActionTimelineEditor* self) { if (self) self->editor().play(); }); actionEditor.addFunc("pause", [](ScriptActionTimelineEditor* self) { if (self) self->editor().pause(); }); actionEditor.addFunc("isPlaying", [](ScriptActionTimelineEditor* self) { return self && self->editor().playing(); }); actionEditor.addFunc("canUndo", [](ScriptActionTimelineEditor* self) { return self && self->editor().canUndo(); }); actionEditor.addFunc("canRedo", [](ScriptActionTimelineEditor* self) { return self && self->editor().canRedo(); }); actionEditor.addFunc("isDragging", [](ScriptActionTimelineEditor* self) { return self && self->widget().dragStatus() == TimelineDragStatus::Active; }); actionEditor.addFunc("getDuration", [](ScriptActionTimelineEditor* self) { return self ? static_cast(self->editor().target().timeline().duration.seconds()) : 0.0f; }); actionEditor.addFunc("getPreviewTime", [](ScriptActionTimelineEditor* self) { return self ? static_cast(self->editor().previewTime().seconds()) : 0.0f; }); actionEditor.addFunc("getRevision", [](ScriptActionTimelineEditor* self) { return self ? static_cast(self->editor().target().revision()) : std::int64_t{0}; }); actionEditor.addFunc("getAnimationUri", [](ScriptActionTimelineEditor* self) { return self ? self->editor().target().timeline().animationUri : std::string{}; }); actionEditor.addFunc("getAnimationSectionCount", [](ScriptActionTimelineEditor* self) { return self ? static_cast(self->editor().target().timeline().animationSections.size()) : 0; }); actionEditor.addFunc("getAnimationSectionId", [](ScriptActionTimelineEditor* self, int index) { const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr; return section ? section->id.format() : std::string{}; }); actionEditor.addFunc("getAnimationSectionUri", [](ScriptActionTimelineEditor* self, int index) { const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr; return section ? section->animationUri : std::string{}; }); actionEditor.addFunc("getAnimationSectionStart", [](ScriptActionTimelineEditor* self, int index) { const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr; return section ? static_cast(section->start.seconds()) : 0.0f; }); actionEditor.addFunc("getAnimationSectionEnd", [](ScriptActionTimelineEditor* self, int index) { const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr; return section ? static_cast(section->end.seconds()) : 0.0f; }); actionEditor.addFunc("getAnimationSectionBlendIn", [](ScriptActionTimelineEditor* self, int index) { const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr; return section ? static_cast(section->blendIn.seconds()) : 0.0f; }); actionEditor.addFunc("getAnimationSectionSourceStart", [](ScriptActionTimelineEditor* self, int index) { const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr; return section ? static_cast(section->sourceStart.seconds()) : 0.0f; }); actionEditor.addFunc("getAnimationSectionSourceEnd", [](ScriptActionTimelineEditor* self, int index) { const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr; return section ? static_cast(section->sourceEnd.seconds()) : 0.0f; }); actionEditor.addFunc("getAnimationSectionBlendCurve", [](ScriptActionTimelineEditor* self, int index) { const auto* section = self ? sectionAt(self->editor().target().timeline(), index) : nullptr; return section ? std::string(action::actionBlendCurveName(section->blendCurve)) : std::string{}; }); actionEditor.addFunc("addAnimationSection", [vm](ScriptActionTimelineEditor* self, const std::string& sectionId, const std::string& animationUri, float startSeconds, float endSeconds, float blendInSeconds) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto parsed = LogicalId::parse(sectionId); if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "animation section id is not canonical", "sectionId"); auto start = seconds(startSeconds); auto end = seconds(endSeconds); auto blend = seconds(blendInSeconds); if (!start) return script::projectStatusResult(vm, start.status(), false, false); if (!end) return script::projectStatusResult(vm, end.status(), false, false); if (!blend) return script::projectStatusResult(vm, blend.status(), false, false); action::ActionAnimationSection section{*parsed, animationUri, start.value(), end.value(), blend.value()}; return project(vm, self->editor().addAnimationSection(std::move(section))); }); actionEditor.addFunc("editAnimationSection", [vm](ScriptActionTimelineEditor* self, const std::string& sectionId, float startSeconds, float endSeconds, float blendInSeconds, const std::string& animationUri) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto parsed = LogicalId::parse(sectionId); if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "animation section id is not canonical", "sectionId"); auto start = seconds(startSeconds); auto end = seconds(endSeconds); auto blend = seconds(blendInSeconds); if (!start) return script::projectStatusResult(vm, start.status(), false, false); if (!end) return script::projectStatusResult(vm, end.status(), false, false); if (!blend) return script::projectStatusResult(vm, blend.status(), false, false); return project( vm, self->editor().editAnimationSection(*parsed, start.value(), end.value(), blend.value(), animationUri)); }); actionEditor.addFunc( "removeAnimationSection", [vm](ScriptActionTimelineEditor* self, const std::string& sectionId) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto parsed = LogicalId::parse(sectionId); if (!parsed) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "animation section id is not canonical", "sectionId"); return project(vm, self->editor().removeAnimationSection(*parsed)); }); actionEditor.addFunc("editAnimationSectionSource", [vm](ScriptActionTimelineEditor* self, const std::string& sectionId, float sourceStartSeconds, float sourceEndSeconds, const std::string& blendCurve) { if (!self) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null"); auto parsed = LogicalId::parse(sectionId); auto curve = action::actionBlendCurveFromName(blendCurve); if (!parsed || !curve) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "section id or blend curve is invalid", "sectionSource"); auto sourceStart = seconds(sourceStartSeconds); auto sourceEnd = seconds(sourceEndSeconds); if (!sourceStart) return script::projectStatusResult(vm, sourceStart.status(), false, false); if (!sourceEnd) return script::projectStatusResult(vm, sourceEnd.status(), false, false); return project( vm, self->editor().editAnimationSectionSource(*parsed, sourceStart.value(), sourceEnd.value(), *curve)); }); actionEditor.addFunc("getTrackCount", [](ScriptActionTimelineEditor* self) { return self ? static_cast(self->editor().target().timeline().tracks.size()) : 0; }); actionEditor.addFunc("getTrackId", [](ScriptActionTimelineEditor* self, int index) { const auto* track = self ? trackAt(self->editor().target().timeline(), index) : nullptr; return track ? track->id.format() : std::string{}; }); actionEditor.addFunc("getTrackLabel", [](ScriptActionTimelineEditor* self, int index) { const auto* track = self ? trackAt(self->editor().target().timeline(), index) : nullptr; return track ? track->label : std::string{}; }); actionEditor.addFunc("getTrackKind", [](ScriptActionTimelineEditor* self, int index) { const auto* track = self ? trackAt(self->editor().target().timeline(), index) : nullptr; return track ? std::string(action::actionTrackKindName(track->kind)) : std::string{}; }); actionEditor.addFunc("getTrackMuted", [](ScriptActionTimelineEditor* self, int index) { const auto* track = self ? trackAt(self->editor().target().timeline(), index) : nullptr; return track && track->muted; }); actionEditor.addFunc("getLayoutWidth", [](ScriptActionTimelineEditor* self) { return self ? self->widget().layout().width : 0.0f; }); actionEditor.addFunc("getLayoutHeight", [](ScriptActionTimelineEditor* self) { return self ? self->widget().layout().height : 0.0f; }); actionEditor.addFunc("getPlayheadX", [](ScriptActionTimelineEditor* self) { return self ? self->widget().layout().playheadX : 0.0f; }); actionEditor.addFunc("getRulerTickCount", [](ScriptActionTimelineEditor* self) { return self ? static_cast(self->widget().layout().rulerTicks.size()) : 0; }); actionEditor.addFunc("getRulerTickX", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; return index >= 0 && static_cast(index) < layout.rulerTicks.size() ? layout.rulerTicks[static_cast(index)].x : 0.0f; }); actionEditor.addFunc("getRulerTickMajor", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; return index >= 0 && static_cast(index) < layout.rulerTicks.size() && layout.rulerTicks[static_cast(index)].major; }); actionEditor.addFunc("getItemCount", [](ScriptActionTimelineEditor* self) { return self ? static_cast(self->widget().layout().items.size()) : 0; }); actionEditor.addFunc("getItemId", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; const auto* item = itemAt(layout, index); return item ? item->itemId.format() : std::string{}; }); actionEditor.addFunc("getItemType", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; const auto* item = itemAt(layout, index); return item ? item->type.format() : std::string{}; }); actionEditor.addFunc("getItemState", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; const auto* item = itemAt(layout, index); return item && item->state; }); actionEditor.addFunc("getItemSelected", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; const auto* item = itemAt(layout, index); return item && item->selected; }); actionEditor.addFunc("getItemMinX", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; const auto* item = itemAt(layout, index); return item ? item->minimumX : 0.0f; }); actionEditor.addFunc("getItemMaxX", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; const auto* item = itemAt(layout, index); return item ? item->maximumX : 0.0f; }); actionEditor.addFunc("getItemMinY", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; const auto* item = itemAt(layout, index); return item ? item->minimumY : 0.0f; }); actionEditor.addFunc("getItemMaxY", [](ScriptActionTimelineEditor* self, int index) { const auto layout = self ? self->widget().layout() : TimelineWidgetLayout{}; const auto* item = itemAt(layout, index); return item ? item->maximumY : 0.0f; }); actionEditor.addFunc("getStateStart", [](ScriptActionTimelineEditor* self, const std::string& itemId) { const auto* state = self ? findState(self->editor().target().timeline(), itemId) : nullptr; return state ? static_cast(state->start.seconds()) : 0.0f; }); actionEditor.addFunc("getStateEnd", [](ScriptActionTimelineEditor* self, const std::string& itemId) { const auto* state = self ? findState(self->editor().target().timeline(), itemId) : nullptr; return state ? static_cast(state->end.seconds()) : 0.0f; }); actionEditor.addFunc("getEventCount", [](ScriptActionTimelineEditor* self) { return self ? static_cast(self->editor().previewEvents().size()) : 0; }); actionEditor.addFunc("getEventItemId", [](ScriptActionTimelineEditor* self, int index) { if (!self) return std::string{}; const auto& events = self->editor().previewEvents(); return index >= 0 && static_cast(index) < events.size() ? events[static_cast(index)].itemId.format() : std::string{}; }); actionEditor.addFunc("getEventType", [](ScriptActionTimelineEditor* self, int index) { if (!self) return std::string{}; const auto& events = self->editor().previewEvents(); return index >= 0 && static_cast(index) < events.size() ? events[static_cast(index)].type.format() : std::string{}; }); actionEditor.addFunc("getEventTime", [](ScriptActionTimelineEditor* self, int index) { if (!self) return 0.0f; const auto& events = self->editor().previewEvents(); return index >= 0 && static_cast(index) < events.size() ? static_cast(events[static_cast(index)].time.seconds()) : 0.0f; }); actionEditor.addFunc("getEventKind", [](ScriptActionTimelineEditor* self, int index) { if (!self) return std::string{}; const auto& events = self->editor().previewEvents(); return index >= 0 && static_cast(index) < events.size() ? eventKind(events[static_cast(index)].kind) : std::string{}; }); moduleClass.addFunc("create", [vm](eve::action_editor::ActionEditorModule*, const std::string& targetId, const ssq::Object& timelineObject) { if (targetId.empty()) return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline target id must not be empty", "targetId"); script::SquirrelValueOptions options; options.source = kBindingSource; auto value = script::valueFromSquirrel(timelineObject, options); if (!value) return script::projectStatusResult(vm, value.status(), false, false); auto timeline = action::ActionTimeline::fromValue(value.value()); if (!timeline) return script::projectStatusResult(vm, timeline.status(), false, false); auto registry = action::ActionNotifyRegistry::withBuiltins(); if (!registry) return script::projectStatusResult(vm, registry.status(), false, false); auto object = script::makeOwnedSquirrelInstance( vm, std::make_unique(targetId, std::move(timeline).takeValue(), std::move(registry).takeValue())); if (!object) return script::projectStatusResult(vm, object.status(), false, false); ssq::Object owned = std::move(object).takeValue(); auto result = script::projectStatusResult(vm, Status::success(StatusCode::Applied), true, false); result.set("value", owned); result.set("ownership", std::string("owned")); return result; }); } } // namespace eve::editor