#include "editor/ActionPreviewController.h" #include #include namespace eve::editor { namespace { EditorResult previewError(EditorStatus status, std::string rule, std::string message) { return EditorResult::error(status, RuleId(std::move(rule)), std::move(message)); } template EditorResult previewErrorValue(EditorStatus status, std::string rule, std::string message) { return EditorResult::error(status, RuleId(std::move(rule)), std::move(message)); } std::string diagnosticMessage(const Status& status, std::string fallback) { if (!status.diagnostics().empty() && !status.diagnostics().front().message().empty()) return status.diagnostics().front().message(); return fallback; } } // namespace ActionPreviewController::ActionPreviewController(ActionTimelineEditor& editor, action::IActionPreviewSink& sink, const action::IActionRootMotionSource* rootMotion) : editor_(editor), sink_(sink), rootMotion_(rootMotion) {} EditorResult ActionPreviewController::setRootMotionSampleCount(std::uint32_t sampleCount) { if (sampleCount < 2 || sampleCount > 1024) return previewError(EditorStatus::Rejected, "editor.action.preview.root-samples", "Root-motion sample count must be between 2 and 1024"); rootMotionSampleCount_ = sampleCount; return EditorResult::applied(); } action::ActionPreviewCueKind ActionPreviewController::cueKind(const action::ActionTimelineEvent& event) noexcept { if (event.kind == action::ActionTimelineEventKind::StateEnter) return action::ActionPreviewCueKind::StateEnter; if (event.kind == action::ActionTimelineEventKind::StateExit) return action::ActionPreviewCueKind::StateExit; const std::string_view type = event.type.format(); if (type == "presentation:vfx") return action::ActionPreviewCueKind::Vfx; if (type == "presentation:audio") return action::ActionPreviewCueKind::Audio; if (type == "presentation:camera") return action::ActionPreviewCueKind::Camera; return action::ActionPreviewCueKind::Gameplay; } Result ActionPreviewController::buildFrame( action::ActionPreviewReason reason, Duration previous, Duration current, const std::vector& events) const { action::ActionPreviewFrame frame; frame.reason = reason; frame.animationUri = editor_.target().timeline().animationUri; frame.previous = previous; frame.current = current; frame.cues.reserve(events.size()); for (const auto& event : events) frame.cues.push_back({cueKind(event), event.itemId, event.type, event.time, event.payload}); if (rootMotion_) { auto path = rootMotion_->sampleRootMotion(frame.animationUri, editor_.target().timeline().duration, rootMotionSampleCount_); if (!path) return Result::failure(path.status()); frame.rootMotionState = action::RootMotionPreviewState::Available; frame.rootMotionPath = std::move(path).takeValue(); } return Result::success(std::move(frame)); } EditorResult ActionPreviewController::refresh() { auto frame = buildFrame(action::ActionPreviewReason::Refresh, editor_.previewTime(), editor_.previewTime(), {}); if (!frame) return previewError(EditorStatus::Rejected, "editor.action.preview.root-motion", diagnosticMessage(frame.status(), "Could not sample root motion")); auto prepared = sink_.prepare(frame.value()); if (!prepared) return previewError(EditorStatus::Rejected, "editor.action.preview.prepare", diagnosticMessage(prepared.status(), "Preview host rejected the frame")); sink_.present(frame.value()); lastFrame_ = std::move(frame).takeValue(); return EditorResult::applied(); } EditorResult ActionPreviewController::seek(Duration time) { if (time < Duration::zero() || time > editor_.target().timeline().duration) return previewError(EditorStatus::Rejected, "editor.action.preview.seek-range", "Preview seek is outside the timeline"); auto frame = buildFrame(action::ActionPreviewReason::Seek, editor_.previewTime(), time, {}); if (!frame) return previewError(EditorStatus::Rejected, "editor.action.preview.root-motion", diagnosticMessage(frame.status(), "Could not sample root motion")); auto prepared = sink_.prepare(frame.value()); if (!prepared) return previewError(EditorStatus::Rejected, "editor.action.preview.prepare", diagnosticMessage(prepared.status(), "Preview host rejected the frame")); auto changed = editor_.seek(time); if (!changed.isAccepted()) { sink_.discardPrepared(); return changed; } sink_.present(frame.value()); lastFrame_ = std::move(frame).takeValue(); return EditorResult::applied(); } EditorResult ActionPreviewController::update(Duration delta) { auto plan = editor_.planPreview(delta); if (!plan.isAccepted()) return previewErrorValue(plan.status, "editor.action.preview.plan", "Could not prepare timeline preview advance"); if (plan.status == EditorStatus::NoOp) { EditorResult result; result.status = EditorStatus::NoOp; result.value = 0; return result; } auto frame = buildFrame(action::ActionPreviewReason::Advance, plan.value->previous, plan.value->current, plan.value->events); if (!frame) return previewErrorValue(EditorStatus::Rejected, "editor.action.preview.root-motion", diagnosticMessage(frame.status(), "Could not sample root motion")); auto prepared = sink_.prepare(frame.value()); if (!prepared) return previewErrorValue(EditorStatus::Rejected, "editor.action.preview.prepare", diagnosticMessage(prepared.status(), "Preview host rejected the frame")); auto advanced = editor_.applyPreviewPlan(std::move(*plan.value)); if (!advanced.isAccepted()) { sink_.discardPrepared(); return advanced; } sink_.present(frame.value()); lastFrame_ = std::move(frame).takeValue(); return advanced; } void ActionPreviewController::drawRootMotion(IEditorOverlay& overlay, OverlayPoint origin, float scale) const { if (!lastFrame_ || lastFrame_->rootMotionState != action::RootMotionPreviewState::Available || !std::isfinite(scale) || scale <= 0.0f) return; for (std::size_t index = 1; index < lastFrame_->rootMotionPath.size(); ++index) { const auto& previous = lastFrame_->rootMotionPath[index - 1]; const auto& current = lastFrame_->rootMotionPath[index]; overlay.line({origin.x + previous.x * scale, origin.y + previous.y * scale, origin.z + previous.z * scale}, {origin.x + current.x * scale, origin.y + current.y * scale, origin.z + current.z * scale}, {0x55d6beffU, 2.0f, false}); } } } // namespace eve::editor