update
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
#include "persistence/EntityBundleStore.h"
|
||||
|
||||
#include "persistence/JsonFileAtomic.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace core::persistence {
|
||||
namespace {
|
||||
|
||||
QJsonArray pointsToJson(const QVector<QPointF>& 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<QPointF>& 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 <typename KeyT>
|
||||
QJsonArray boolKeysToJson(const QVector<KeyT>& 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<Project::Entity::KeyframeVec2>& 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<Project::Entity::KeyframeFloat01>& 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<Project::Entity::KeyframeDouble>& 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<Project::Entity::ImageFrame>& 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 <typename KeyT>
|
||||
bool boolKeysFromJson(const QJsonValue& v, QVector<KeyT>& 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<Project::Entity::KeyframeVec2>& 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<Project::Entity::KeyframeFloat01>& 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<Project::Entity::KeyframeDouble>& 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<Project::Entity::ImageFrame>& 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<Project::ToolKeyframeBool>(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<Project::ToolKeyframeBool>(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
|
||||
|
||||
Reference in New Issue
Block a user