#include "ScriptRuntime.h" #include "SceneObject.h" #include "ThirdParty/imgui/imgui.h" #include #include #include #include namespace { struct Keyframe { float time = 0.0f; glm::vec3 position = glm::vec3(0.0f); glm::vec3 rotation = glm::vec3(0.0f); glm::vec3 scale = glm::vec3(1.0f); }; int targetId = -1; char targetName[128] = ""; std::vector keyframes; int selectedKey = -1; float clipLength = 2.0f; float currentTime = 0.0f; float playSpeed = 1.0f; bool isPlaying = false; bool loop = true; bool applyOnScrub = true; glm::vec3 lerpVec3(const glm::vec3& a, const glm::vec3& b, float t) { return a + (b - a) * t; } float clampFloat(float value, float minValue, float maxValue) { return std::max(minValue, std::min(value, maxValue)); } SceneObject* resolveTarget(ScriptContext& ctx) { if (targetId >= 0) { if (auto* obj = ctx.FindObjectById(targetId)) { return obj; } } if (targetName[0] != '\0') { if (auto* obj = ctx.FindObjectByName(targetName)) { targetId = obj->id; return obj; } } return nullptr; } void syncTargetLabel(SceneObject* obj) { if (!obj) return; strncpy(targetName, obj->name.c_str(), sizeof(targetName) - 1); targetName[sizeof(targetName) - 1] = '\0'; } void captureKeyframe(SceneObject& obj, float time) { float clamped = clampFloat(time, 0.0f, clipLength); auto it = std::find_if(keyframes.begin(), keyframes.end(), [&](const Keyframe& k) { return std::abs(k.time - clamped) < 0.0001f; }); if (it == keyframes.end()) { keyframes.push_back(Keyframe{clamped, obj.position, obj.rotation, obj.scale}); } else { it->position = obj.position; it->rotation = obj.rotation; it->scale = obj.scale; } std::sort(keyframes.begin(), keyframes.end(), [](const Keyframe& a, const Keyframe& b) { return a.time < b.time; }); } void deleteKeyframe(int index) { if (index < 0 || index >= static_cast(keyframes.size())) return; keyframes.erase(keyframes.begin() + index); if (selectedKey == index) selectedKey = -1; if (selectedKey > index) selectedKey--; } void applyPoseAtTime(ScriptContext& ctx, SceneObject& obj, float time) { if (keyframes.empty()) return; if (time <= keyframes.front().time) { ctx.SetPosition(keyframes.front().position); ctx.SetRotation(keyframes.front().rotation); ctx.SetScale(keyframes.front().scale); ctx.MarkDirty(); return; } if (time >= keyframes.back().time) { ctx.SetPosition(keyframes.back().position); ctx.SetRotation(keyframes.back().rotation); ctx.SetScale(keyframes.back().scale); ctx.MarkDirty(); return; } for (size_t i = 0; i + 1 < keyframes.size(); ++i) { const Keyframe& a = keyframes[i]; const Keyframe& b = keyframes[i + 1]; if (time >= a.time && time <= b.time) { float span = b.time - a.time; float t = (span > 0.0f) ? (time - a.time) / span : 0.0f; ctx.SetPosition(lerpVec3(a.position, b.position, t)); ctx.SetRotation(lerpVec3(a.rotation, b.rotation, t)); ctx.SetScale(lerpVec3(a.scale, b.scale, t)); ctx.MarkDirty(); return; } } } void drawTimeline(float& time, float length, int& selection) { ImVec2 size = ImVec2(ImGui::GetContentRegionAvail().x, 70.0f); ImVec2 start = ImGui::GetCursorScreenPos(); ImGui::InvisibleButton("Timeline", size); ImDrawList* draw = ImGui::GetWindowDrawList(); ImU32 bg = ImGui::GetColorU32(ImGuiCol_FrameBg); ImU32 border = ImGui::GetColorU32(ImGuiCol_Border); ImU32 accent = ImGui::GetColorU32(ImGuiCol_CheckMark); ImU32 keyColor = ImGui::GetColorU32(ImGuiCol_SliderGrab); draw->AddRectFilled(start, ImVec2(start.x + size.x, start.y + size.y), bg, 6.0f); draw->AddRect(start, ImVec2(start.x + size.x, start.y + size.y), border, 6.0f); float clamped = clampFloat(time, 0.0f, length); float playheadX = start.x + (length > 0.0f ? (clamped / length) * size.x : 0.0f); draw->AddLine(ImVec2(playheadX, start.y), ImVec2(playheadX, start.y + size.y), accent, 2.0f); for (size_t i = 0; i < keyframes.size(); ++i) { float keyX = start.x + (length > 0.0f ? (keyframes[i].time / length) * size.x : 0.0f); ImVec2 center(keyX, start.y + size.y * 0.5f); float radius = (selection == static_cast(i)) ? 6.0f : 4.5f; draw->AddCircleFilled(center, radius, keyColor); ImRect hit(ImVec2(center.x - 7.0f, center.y - 7.0f), ImVec2(center.x + 7.0f, center.y + 7.0f)); if (ImGui::IsMouseHoveringRect(hit.Min, hit.Max) && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { selection = static_cast(i); time = keyframes[i].time; } } if (ImGui::IsItemActive() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { float mouseX = ImGui::GetIO().MousePos.x; float t = (mouseX - start.x) / size.x; time = clampFloat(t * length, 0.0f, length); } } void drawKeyframeTable() { if (keyframes.empty()) { ImGui::TextDisabled("No keyframes yet."); return; } if (ImGui::BeginTable("KeyframeTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit)) { ImGui::TableSetupColumn("Time"); ImGui::TableSetupColumn("Position"); ImGui::TableSetupColumn("Rotation"); ImGui::TableSetupColumn("Scale"); ImGui::TableHeadersRow(); for (size_t i = 0; i < keyframes.size(); ++i) { const auto& key = keyframes[i]; ImGui::TableNextRow(); ImGui::TableNextColumn(); bool selected = selectedKey == static_cast(i); std::string label = std::to_string(key.time); if (ImGui::Selectable(label.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns)) { selectedKey = static_cast(i); currentTime = key.time; } ImGui::TableNextColumn(); ImGui::Text("%.2f, %.2f, %.2f", key.position.x, key.position.y, key.position.z); ImGui::TableNextColumn(); ImGui::Text("%.2f, %.2f, %.2f", key.rotation.x, key.rotation.y, key.rotation.z); ImGui::TableNextColumn(); ImGui::Text("%.2f, %.2f, %.2f", key.scale.x, key.scale.y, key.scale.z); } ImGui::EndTable(); } } } // namespace extern "C" void RenderEditorWindow(ScriptContext& ctx) { ImGui::TextUnformatted("Simple Animation"); ImGui::Separator(); SceneObject* selectedObj = ctx.object; SceneObject* targetObj = resolveTarget(ctx); ImGui::TextDisabled("Select a GameObject to animate:"); ImGui::BeginDisabled(); ImGui::InputText("##TargetName", targetName, sizeof(targetName)); ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button("Use Selected") && selectedObj) { targetId = selectedObj->id; syncTargetLabel(selectedObj); } ImGui::SameLine(); if (ImGui::Button("Clear")) { targetId = -1; targetName[0] = '\0'; targetObj = nullptr; } ImGui::Spacing(); if (ImGui::BeginTabBar("AnimModeTabs")) { if (ImGui::BeginTabItem("Pose Mode")) { ImGui::TextDisabled("Pose Editor"); ImGui::Separator(); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 6.0f); if (ImGui::Button("Key")) { if (targetObj) captureKeyframe(*targetObj, currentTime); } ImGui::SameLine(); if (ImGui::Button("Delete") && selectedKey >= 0) { deleteKeyframe(selectedKey); } ImGui::PopStyleVar(); ImGui::Spacing(); drawTimeline(currentTime, clipLength, selectedKey); ImGui::SliderFloat("Time", ¤tTime, 0.0f, clipLength, "%.2fs"); ImGui::Spacing(); drawKeyframeTable(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Config Mode")) { ImGui::TextDisabled("Playback"); ImGui::Separator(); ImGui::Checkbox("Loop", &loop); ImGui::Checkbox("Apply On Scrub", &applyOnScrub); ImGui::SliderFloat("Length", &clipLength, 0.1f, 20.0f, "%.2fs"); ImGui::SliderFloat("Speed", &playSpeed, 0.1f, 4.0f, "%.2fx"); ImGui::EndTabItem(); } ImGui::EndTabBar(); } ImGui::Spacing(); ImGui::Separator(); ImGui::TextDisabled("Transport"); if (ImGui::Button(isPlaying ? "Pause" : "Play")) { isPlaying = !isPlaying; } ImGui::SameLine(); if (ImGui::Button("Stop")) { isPlaying = false; currentTime = 0.0f; } if (targetObj) { ImGui::SameLine(); ImGui::TextDisabled("Target: %s", targetObj->name.c_str()); } else { ImGui::TextDisabled("No target selected."); } if (isPlaying && clipLength > 0.0f) { currentTime += ImGui::GetIO().DeltaTime * playSpeed; if (currentTime > clipLength) { if (loop) currentTime = std::fmod(currentTime, clipLength); else { currentTime = clipLength; isPlaying = false; } } } if (targetObj && (isPlaying || applyOnScrub)) { applyPoseAtTime(ctx, *targetObj, currentTime); } } extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) { (void)ctx; }