#include "persistence/EntityBundleStore.h" #include "persistence/JsonFileAtomic.h" #include #include #include #include namespace core::persistence { namespace { QJsonArray pointsToJson(const QVector& pts) { QJsonArray arr; for (const auto& p : pts) { QJsonObject o; o.insert("x", p.x()); o.insert("y", p.y()); arr.append(o); } return arr; } bool pointsFromJson(const QJsonValue& v, QVector& out) { out.clear(); if (!v.isArray()) return true; const QJsonArray arr = v.toArray(); out.reserve(arr.size()); for (const auto& it : arr) { if (!it.isObject()) continue; const QJsonObject o = it.toObject(); out.push_back(QPointF(o.value("x").toDouble(0.0), o.value("y").toDouble(0.0))); } return true; } template QJsonArray boolKeysToJson(const QVector& keys) { QJsonArray arr; for (const auto& k : keys) { QJsonObject o; o.insert("frame", k.frame); o.insert("value", k.value); arr.append(o); } return arr; } QJsonArray vec2KeysToJson(const QVector& keys) { QJsonArray arr; for (const auto& k : keys) { QJsonObject o; o.insert("frame", k.frame); o.insert("x", k.value.x()); o.insert("y", k.value.y()); arr.append(o); } return arr; } QJsonArray floatKeysToJson(const QVector& keys) { QJsonArray arr; for (const auto& k : keys) { QJsonObject o; o.insert("frame", k.frame); o.insert("value", k.value); arr.append(o); } return arr; } QJsonArray doubleKeysToJson(const QVector& keys) { QJsonArray arr; for (const auto& k : keys) { QJsonObject o; o.insert("frame", k.frame); o.insert("value", k.value); arr.append(o); } return arr; } QJsonArray imageFramesToJson(const QVector& keys) { QJsonArray arr; for (const auto& k : keys) { QJsonObject o; o.insert("frame", k.frame); o.insert("path", k.imagePath); arr.append(o); } return arr; } template bool boolKeysFromJson(const QJsonValue& v, QVector& out) { out.clear(); if (!v.isArray()) return true; const QJsonArray arr = v.toArray(); out.reserve(arr.size()); for (const auto& it : arr) { if (!it.isObject()) continue; const QJsonObject o = it.toObject(); KeyT k; k.frame = o.value("frame").toInt(0); k.value = o.value("value").toBool(true); out.push_back(k); } return true; } bool vec2KeysFromJson(const QJsonValue& v, QVector& out) { out.clear(); if (!v.isArray()) return true; const QJsonArray arr = v.toArray(); out.reserve(arr.size()); for (const auto& it : arr) { if (!it.isObject()) continue; const QJsonObject o = it.toObject(); Project::Entity::KeyframeVec2 k; k.frame = o.value("frame").toInt(0); k.value = QPointF(o.value("x").toDouble(0.0), o.value("y").toDouble(0.0)); out.push_back(k); } return true; } bool floatKeysFromJson(const QJsonValue& v, QVector& out) { out.clear(); if (!v.isArray()) return true; const QJsonArray arr = v.toArray(); out.reserve(arr.size()); for (const auto& it : arr) { if (!it.isObject()) continue; const QJsonObject o = it.toObject(); Project::Entity::KeyframeFloat01 k; k.frame = o.value("frame").toInt(0); k.value = o.value("value").toDouble(0.5); out.push_back(k); } return true; } bool doubleKeysFromJson(const QJsonValue& v, QVector& out) { out.clear(); if (!v.isArray()) return true; const QJsonArray arr = v.toArray(); out.reserve(arr.size()); for (const auto& it : arr) { if (!it.isObject()) continue; const QJsonObject o = it.toObject(); Project::Entity::KeyframeDouble k; k.frame = o.value("frame").toInt(0); k.value = o.value("value").toDouble(1.0); out.push_back(k); } return true; } bool imageFramesFromJson(const QJsonValue& v, QVector& out) { out.clear(); if (!v.isArray()) return true; const QJsonArray arr = v.toArray(); out.reserve(arr.size()); for (const auto& it : arr) { if (!it.isObject()) continue; const QJsonObject o = it.toObject(); Project::Entity::ImageFrame k; k.frame = o.value("frame").toInt(0); k.imagePath = o.value("path").toString(); out.push_back(k); } return true; } QJsonObject introToJson(const core::EntityIntroContent& intro) { QJsonObject o; o.insert("title", intro.title); o.insert("bodyText", intro.bodyText); o.insert("videoPathRelative", intro.videoPathRelative); QJsonArray imgs; for (const auto& p : intro.imagePathsRelative) imgs.append(p); o.insert("imagePathsRelative", imgs); return o; } bool introFromJson(const QJsonObject& o, core::EntityIntroContent& out) { out = core::EntityIntroContent{}; out.title = o.value("title").toString(); out.bodyText = o.value("bodyText").toString(); out.videoPathRelative = o.value("videoPathRelative").toString(); const QJsonValue imgsVal = o.value("imagePathsRelative"); if (imgsVal.isArray()) { for (const auto& it : imgsVal.toArray()) { const QString p = it.toString(); if (!p.isEmpty()) out.imagePathsRelative.push_back(p); } } return true; } } // namespace QString EntityBundleStore::bundleRelativeDir(const QString& entityId) { if (entityId.isEmpty()) return {}; return QStringLiteral("assets/entities/%1").arg(entityId); } bool EntityBundleStore::save(const QString& projectDirAbs, const Project::Entity& entity) { if (projectDirAbs.isEmpty() || entity.id.isEmpty()) { return false; } const QString relDir = bundleRelativeDir(entity.id); if (relDir.isEmpty()) return false; const QString absDir = QDir(projectDirAbs).filePath(relDir); QDir().mkpath(absDir); // meta.json QJsonObject meta; meta.insert("id", entity.id); meta.insert("displayName", entity.displayName); meta.insert("visible", entity.visible); meta.insert("originX", entity.originWorld.x()); meta.insert("originY", entity.originWorld.y()); meta.insert("depth", entity.depth); meta.insert("priority", entity.priority); meta.insert("imagePath", entity.imagePath); meta.insert("imageTopLeftX", entity.imageTopLeftWorld.x()); meta.insert("imageTopLeftY", entity.imageTopLeftWorld.y()); meta.insert("userScale", entity.userScale); meta.insert("distanceScaleCalibMult", entity.distanceScaleCalibMult); meta.insert("ignoreDistanceScale", entity.ignoreDistanceScale); meta.insert("parentId", entity.parentId); meta.insert("parentOffsetX", entity.parentOffsetWorld.x()); meta.insert("parentOffsetY", entity.parentOffsetWorld.y()); meta.insert("polygonLocal", pointsToJson(entity.polygonLocal)); meta.insert("cutoutPolygonWorld", pointsToJson(entity.cutoutPolygonWorld)); meta.insert("blackholeId", entity.blackholeId); meta.insert("blackholeVisible", entity.blackholeVisible); meta.insert("blackholeResolvedBy", entity.blackholeResolvedBy); meta.insert("blackholeOverlayPath", entity.blackholeOverlayPath); meta.insert("blackholeOverlayRectX", entity.blackholeOverlayRect.x()); meta.insert("blackholeOverlayRectY", entity.blackholeOverlayRect.y()); meta.insert("blackholeOverlayRectW", entity.blackholeOverlayRect.width()); meta.insert("blackholeOverlayRectH", entity.blackholeOverlayRect.height()); if (!writeJsonAtomic(QDir(absDir).filePath(QStringLiteral("meta.json")), meta, true)) { return false; } // anim.json QJsonObject anim; anim.insert("locationKeys", vec2KeysToJson(entity.locationKeys)); anim.insert("depthScaleKeys", floatKeysToJson(entity.depthScaleKeys)); anim.insert("userScaleKeys", doubleKeysToJson(entity.userScaleKeys)); anim.insert("visibilityKeys", boolKeysToJson(entity.visibilityKeys)); anim.insert("imageFrames", imageFramesToJson(entity.imageFrames)); if (!writeJsonAtomic(QDir(absDir).filePath(QStringLiteral("anim.json")), anim, true)) { return false; } // intro.json if (!writeJsonAtomic(QDir(absDir).filePath(QStringLiteral("intro.json")), introToJson(entity.intro), true)) { return false; } // default.png (可为空) const QString pngAbs = QDir(absDir).filePath(QStringLiteral("default.png")); if (!entity.defaultImagePng.isEmpty()) { if (!writeBytesAtomic(pngAbs, entity.defaultImagePng)) { return false; } } else { // 清空时删掉,避免读到旧图 if (QFileInfo::exists(pngAbs)) { QFile::remove(pngAbs); } } return true; } bool EntityBundleStore::load(const QString& projectDirAbs, Project::Entity& inOutEntity) { if (projectDirAbs.isEmpty() || inOutEntity.id.isEmpty()) { return false; } const QString relDir = inOutEntity.entityPayloadPath; if (relDir.isEmpty()) return false; const QString absDir = QDir(projectDirAbs).filePath(relDir); if (!QFileInfo(absDir).exists()) return false; QJsonObject meta; if (!readJsonObject(QDir(absDir).filePath(QStringLiteral("meta.json")), meta)) { return false; } const QString id = meta.value("id").toString(); if (id.isEmpty() || id != inOutEntity.id) { return false; } Project::Entity e = inOutEntity; // 保留 stub:id/payloadPath/visible 等必要字段会被覆盖 e.displayName = meta.value("displayName").toString(); e.visible = meta.value("visible").toBool(true); e.originWorld = QPointF(meta.value("originX").toDouble(0.0), meta.value("originY").toDouble(0.0)); e.depth = meta.value("depth").toInt(0); e.priority = meta.value("priority").toInt(1); e.imagePath = meta.value("imagePath").toString(); e.imageTopLeftWorld = QPointF(meta.value("imageTopLeftX").toDouble(0.0), meta.value("imageTopLeftY").toDouble(0.0)); e.userScale = meta.value("userScale").toDouble(1.0); e.distanceScaleCalibMult = meta.value("distanceScaleCalibMult").toDouble(0.0); e.ignoreDistanceScale = meta.value("ignoreDistanceScale").toBool(false); e.parentId = meta.value("parentId").toString(); e.parentOffsetWorld = QPointF(meta.value("parentOffsetX").toDouble(0.0), meta.value("parentOffsetY").toDouble(0.0)); pointsFromJson(meta.value("polygonLocal"), e.polygonLocal); pointsFromJson(meta.value("cutoutPolygonWorld"), e.cutoutPolygonWorld); e.blackholeId = meta.value("blackholeId").toString(); e.blackholeVisible = meta.value("blackholeVisible").toBool(true); e.blackholeResolvedBy = meta.value("blackholeResolvedBy").toString(); e.blackholeOverlayPath = meta.value("blackholeOverlayPath").toString(); e.blackholeOverlayRect = QRect(meta.value("blackholeOverlayRectX").toInt(0), meta.value("blackholeOverlayRectY").toInt(0), meta.value("blackholeOverlayRectW").toInt(0), meta.value("blackholeOverlayRectH").toInt(0)); QJsonObject anim; if (!readJsonObject(QDir(absDir).filePath(QStringLiteral("anim.json")), anim)) { return false; } vec2KeysFromJson(anim.value("locationKeys"), e.locationKeys); floatKeysFromJson(anim.value("depthScaleKeys"), e.depthScaleKeys); doubleKeysFromJson(anim.value("userScaleKeys"), e.userScaleKeys); boolKeysFromJson(anim.value("visibilityKeys"), e.visibilityKeys); imageFramesFromJson(anim.value("imageFrames"), e.imageFrames); QJsonObject intro; if (!readJsonObject(QDir(absDir).filePath(QStringLiteral("intro.json")), intro)) { return false; } introFromJson(intro, e.intro); // default.png const QString pngAbs = QDir(absDir).filePath(QStringLiteral("default.png")); if (QFileInfo::exists(pngAbs)) { QFile f(pngAbs); if (f.open(QIODevice::ReadOnly)) { e.defaultImagePng = f.readAll(); } else { return false; } } else { e.defaultImagePng.clear(); } inOutEntity = std::move(e); return true; } } // namespace core::persistence