#include "persistence/AnimationBundleStore.h" #include "persistence/JsonFileAtomic.h" #include #include #include #include namespace core::persistence { namespace { QJsonObject keyToJson(const Project::AnimationKey& k, Project::AnimationValueType t) { QJsonObject o; o.insert("frame", k.frame); switch (t) { case Project::AnimationValueType::Vec2: o.insert("x", k.vec2Value.x()); o.insert("y", k.vec2Value.y()); break; case Project::AnimationValueType::Double: o.insert("value", k.numberValue); break; case Project::AnimationValueType::Bool: o.insert("value", k.boolValue); break; case Project::AnimationValueType::ImagePath: o.insert("value", k.stringValue); break; case Project::AnimationValueType::PngBytes: o.insert("value", QString::fromLatin1(k.bytesValue.toBase64())); break; } return o; } bool keyFromJson(const QJsonObject& o, Project::AnimationValueType t, Project::AnimationKey& out) { out = Project::AnimationKey{}; out.frame = o.value("frame").toInt(0); switch (t) { case Project::AnimationValueType::Vec2: out.vec2Value = QPointF(o.value("x").toDouble(0.0), o.value("y").toDouble(0.0)); break; case Project::AnimationValueType::Double: out.numberValue = o.value("value").toDouble(0.0); break; case Project::AnimationValueType::Bool: out.boolValue = o.value("value").toBool(true); break; case Project::AnimationValueType::ImagePath: out.stringValue = o.value("value").toString(); break; case Project::AnimationValueType::PngBytes: { const QByteArray b64 = o.value("value").toString().toLatin1(); out.bytesValue = QByteArray::fromBase64(b64); break; } } return true; } } // namespace QString AnimationBundleStore::bundleRelativeDir(const QString& animationId) { if (animationId.isEmpty()) return {}; return QStringLiteral("assets/animations/%1").arg(animationId); } bool AnimationBundleStore::save(const QString& projectDirAbs, const Project::Animation& animation) { if (projectDirAbs.isEmpty() || animation.id.isEmpty()) { return false; } const QString relDir = bundleRelativeDir(animation.id); if (relDir.isEmpty()) return false; const QString absDir = QDir(projectDirAbs).filePath(relDir); QDir().mkpath(absDir); QJsonObject root; root.insert("id", animation.id); root.insert("name", animation.name); root.insert("fps", animation.fps); root.insert("lengthFrames", animation.lengthFrames); root.insert("loop", animation.loop); QJsonArray tracks; for (const auto& t : animation.tracks) { QJsonObject to; to.insert("id", t.id); to.insert("targetKind", int(t.targetKind)); to.insert("targetId", t.targetId); to.insert("property", int(t.property)); to.insert("valueType", int(t.valueType)); to.insert("interpolation", int(t.interpolation)); QJsonArray keys; for (const auto& k : t.keys) { keys.append(keyToJson(k, t.valueType)); } to.insert("keys", keys); tracks.append(to); } root.insert("tracks", tracks); return writeJsonAtomic(QDir(absDir).filePath(QStringLiteral("anim.json")), root, true); } bool AnimationBundleStore::load(const QString& projectDirAbs, Project::Animation& inOutAnimation) { if (projectDirAbs.isEmpty() || inOutAnimation.id.isEmpty()) { return false; } const QString relDir = inOutAnimation.payloadPath; if (relDir.isEmpty()) return false; const QString absDir = QDir(projectDirAbs).filePath(relDir); if (!QFileInfo(absDir).exists()) return false; QJsonObject root; if (!readJsonObject(QDir(absDir).filePath(QStringLiteral("anim.json")), root)) { return false; } const QString id = root.value("id").toString(); if (id.isEmpty() || id != inOutAnimation.id) { return false; } Project::Animation a = inOutAnimation; a.name = root.value("name").toString(); a.fps = std::max(1, root.value("fps").toInt(30)); a.lengthFrames = std::max(1, root.value("lengthFrames").toInt(Project::kClipFixedFrames)); a.loop = root.value("loop").toBool(true); a.tracks.clear(); const QJsonValue tracksVal = root.value("tracks"); if (tracksVal.isArray()) { for (const auto& it : tracksVal.toArray()) { if (!it.isObject()) continue; const QJsonObject to = it.toObject(); Project::AnimationTrack t; t.id = to.value("id").toString(); t.targetKind = Project::AnimationTargetKind(to.value("targetKind").toInt(0)); t.targetId = to.value("targetId").toString(); t.property = Project::AnimationProperty(to.value("property").toInt(0)); t.valueType = Project::AnimationValueType(to.value("valueType").toInt(0)); t.interpolation = Project::AnimationInterpolation(to.value("interpolation").toInt(1)); const QJsonValue keysVal = to.value("keys"); if (keysVal.isArray()) { for (const auto& kv : keysVal.toArray()) { if (!kv.isObject()) continue; Project::AnimationKey k; keyFromJson(kv.toObject(), t.valueType, k); t.keys.push_back(k); } } if (!t.targetId.isEmpty()) { a.tracks.push_back(t); } } } inOutAnimation = std::move(a); return true; } } // namespace core::persistence