506 lines
16 KiB
C++
506 lines
16 KiB
C++
#include "persistence/EntityPayloadBinary.h"
|
||
|
||
#include "persistence/PersistentBinaryObject.h"
|
||
|
||
#include "domain/Project.h"
|
||
|
||
#include <QDataStream>
|
||
#include <QFile>
|
||
#include <QtGlobal>
|
||
|
||
#include <algorithm>
|
||
|
||
namespace core {
|
||
|
||
namespace {
|
||
|
||
void sortByFrame(QVector<Project::Entity::KeyframeVec2>& v) {
|
||
std::sort(v.begin(), v.end(), [](const auto& a, const auto& b) { return a.frame < b.frame; });
|
||
}
|
||
|
||
void sortByFrame(QVector<Project::Entity::KeyframeFloat01>& v) {
|
||
std::sort(v.begin(), v.end(), [](const auto& a, const auto& b) { return a.frame < b.frame; });
|
||
}
|
||
|
||
void sortByFrame(QVector<Project::Entity::KeyframeDouble>& v) {
|
||
std::sort(v.begin(), v.end(), [](const auto& a, const auto& b) { return a.frame < b.frame; });
|
||
}
|
||
|
||
void sortByFrame(QVector<Project::Entity::ImageFrame>& v) {
|
||
std::sort(v.begin(), v.end(), [](const auto& a, const auto& b) { return a.frame < b.frame; });
|
||
}
|
||
|
||
void writeIntroBlock(QDataStream& ds, const Project::Entity& entity) {
|
||
ds << entity.intro.title << entity.intro.bodyText;
|
||
ds << qint32(entity.intro.imagePathsRelative.size());
|
||
for (const auto& p : entity.intro.imagePathsRelative) {
|
||
ds << p;
|
||
}
|
||
ds << entity.intro.videoPathRelative;
|
||
}
|
||
|
||
bool readIntroBlock(QDataStream& ds, EntityIntroContent& intro) {
|
||
ds >> intro.title >> intro.bodyText;
|
||
qint32 n = 0;
|
||
ds >> n;
|
||
if (ds.status() != QDataStream::Ok || n < 0 || n > 2048) {
|
||
return false;
|
||
}
|
||
intro.imagePathsRelative.clear();
|
||
intro.imagePathsRelative.reserve(n);
|
||
for (qint32 i = 0; i < n; ++i) {
|
||
QString p;
|
||
ds >> p;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
intro.imagePathsRelative.push_back(std::move(p));
|
||
}
|
||
ds >> intro.videoPathRelative;
|
||
return ds.status() == QDataStream::Ok;
|
||
}
|
||
|
||
bool readAnimationBlock(QDataStream& ds, Project::Entity& out, bool hasUserScaleKeys) {
|
||
out.locationKeys.clear();
|
||
out.depthScaleKeys.clear();
|
||
out.userScaleKeys.clear();
|
||
out.imageFrames.clear();
|
||
|
||
qint32 nLoc = 0;
|
||
ds >> nLoc;
|
||
if (ds.status() != QDataStream::Ok || nLoc < 0 || nLoc > 1000000) {
|
||
return false;
|
||
}
|
||
out.locationKeys.reserve(nLoc);
|
||
for (qint32 i = 0; i < nLoc; ++i) {
|
||
qint32 frame = 0;
|
||
double x = 0.0;
|
||
double y = 0.0;
|
||
ds >> frame >> x >> y;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
out.locationKeys.push_back(Project::Entity::KeyframeVec2{frame, QPointF(x, y)});
|
||
}
|
||
|
||
qint32 nDepth = 0;
|
||
ds >> nDepth;
|
||
if (ds.status() != QDataStream::Ok || nDepth < 0 || nDepth > 1000000) {
|
||
return false;
|
||
}
|
||
out.depthScaleKeys.reserve(nDepth);
|
||
for (qint32 i = 0; i < nDepth; ++i) {
|
||
qint32 frame = 0;
|
||
double v = 0.5;
|
||
ds >> frame >> v;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
out.depthScaleKeys.push_back(Project::Entity::KeyframeFloat01{frame, v});
|
||
}
|
||
|
||
if (hasUserScaleKeys) {
|
||
qint32 nUser = 0;
|
||
ds >> nUser;
|
||
if (ds.status() != QDataStream::Ok || nUser < 0 || nUser > 1000000) {
|
||
return false;
|
||
}
|
||
out.userScaleKeys.reserve(nUser);
|
||
for (qint32 i = 0; i < nUser; ++i) {
|
||
qint32 frame = 0;
|
||
double v = 1.0;
|
||
ds >> frame >> v;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
out.userScaleKeys.push_back(Project::Entity::KeyframeDouble{frame, v});
|
||
}
|
||
}
|
||
|
||
qint32 nImg = 0;
|
||
ds >> nImg;
|
||
if (ds.status() != QDataStream::Ok || nImg < 0 || nImg > 1000000) {
|
||
return false;
|
||
}
|
||
out.imageFrames.reserve(nImg);
|
||
for (qint32 i = 0; i < nImg; ++i) {
|
||
qint32 frame = 0;
|
||
QString path;
|
||
ds >> frame >> path;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
if (!path.isEmpty()) {
|
||
out.imageFrames.push_back(Project::Entity::ImageFrame{frame, path});
|
||
}
|
||
}
|
||
|
||
sortByFrame(out.locationKeys);
|
||
sortByFrame(out.depthScaleKeys);
|
||
sortByFrame(out.userScaleKeys);
|
||
sortByFrame(out.imageFrames);
|
||
return true;
|
||
}
|
||
|
||
void writeAnimationBlock(QDataStream& ds, const Project::Entity& entity, bool writeUserScaleKeys) {
|
||
ds << qint32(entity.locationKeys.size());
|
||
for (const auto& k : entity.locationKeys) {
|
||
ds << qint32(k.frame) << double(k.value.x()) << double(k.value.y());
|
||
}
|
||
|
||
ds << qint32(entity.depthScaleKeys.size());
|
||
for (const auto& k : entity.depthScaleKeys) {
|
||
ds << qint32(k.frame) << double(k.value);
|
||
}
|
||
|
||
if (writeUserScaleKeys) {
|
||
ds << qint32(entity.userScaleKeys.size());
|
||
for (const auto& k : entity.userScaleKeys) {
|
||
ds << qint32(k.frame) << double(k.value);
|
||
}
|
||
}
|
||
|
||
ds << qint32(entity.imageFrames.size());
|
||
for (const auto& k : entity.imageFrames) {
|
||
ds << qint32(k.frame) << k.imagePath;
|
||
}
|
||
}
|
||
|
||
bool readEntityPayloadV1(QDataStream& ds, Project::Entity& tmp, bool hasUserScaleKeys) {
|
||
ds >> tmp.id;
|
||
qint32 depth = 0;
|
||
ds >> depth;
|
||
tmp.depth = static_cast<int>(depth);
|
||
ds >> tmp.imagePath;
|
||
double ox = 0.0;
|
||
double oy = 0.0;
|
||
double itlx = 0.0;
|
||
double itly = 0.0;
|
||
ds >> ox >> oy >> itlx >> itly;
|
||
tmp.originWorld = QPointF(ox, oy);
|
||
tmp.imageTopLeftWorld = QPointF(itlx, itly);
|
||
|
||
qint32 nLocal = 0;
|
||
ds >> nLocal;
|
||
if (ds.status() != QDataStream::Ok || nLocal < 0 || nLocal > 1000000) {
|
||
return false;
|
||
}
|
||
tmp.polygonLocal.reserve(nLocal);
|
||
for (qint32 i = 0; i < nLocal; ++i) {
|
||
double x = 0.0;
|
||
double y = 0.0;
|
||
ds >> x >> y;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.polygonLocal.push_back(QPointF(x, y));
|
||
}
|
||
|
||
qint32 nCut = 0;
|
||
ds >> nCut;
|
||
if (ds.status() != QDataStream::Ok || nCut < 0 || nCut > 1000000) {
|
||
return false;
|
||
}
|
||
tmp.cutoutPolygonWorld.reserve(nCut);
|
||
for (qint32 i = 0; i < nCut; ++i) {
|
||
double x = 0.0;
|
||
double y = 0.0;
|
||
ds >> x >> y;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.cutoutPolygonWorld.push_back(QPointF(x, y));
|
||
}
|
||
|
||
if (!readAnimationBlock(ds, tmp, hasUserScaleKeys)) {
|
||
return false;
|
||
}
|
||
|
||
if (tmp.id.isEmpty() || tmp.polygonLocal.isEmpty()) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
class EntityBinaryRecord final : public PersistentBinaryObject {
|
||
public:
|
||
explicit EntityBinaryRecord(const Project::Entity& e) : m_src(&e), m_dst(nullptr) {}
|
||
explicit EntityBinaryRecord(Project::Entity& e) : m_src(nullptr), m_dst(&e) {}
|
||
|
||
quint32 recordMagic() const override { return EntityPayloadBinary::kMagicPayload; }
|
||
quint32 recordFormatVersion() const override { return EntityPayloadBinary::kPayloadVersion; }
|
||
|
||
void writeBody(QDataStream& ds) const override {
|
||
Q_ASSERT(m_src != nullptr);
|
||
const Project::Entity& entity = *m_src;
|
||
ds << entity.id;
|
||
ds << qint32(entity.depth);
|
||
ds << entity.imagePath;
|
||
ds << double(entity.originWorld.x()) << double(entity.originWorld.y());
|
||
ds << double(entity.imageTopLeftWorld.x()) << double(entity.imageTopLeftWorld.y());
|
||
|
||
ds << qint32(entity.polygonLocal.size());
|
||
for (const auto& pt : entity.polygonLocal) {
|
||
ds << double(pt.x()) << double(pt.y());
|
||
}
|
||
|
||
ds << qint32(entity.cutoutPolygonWorld.size());
|
||
for (const auto& pt : entity.cutoutPolygonWorld) {
|
||
ds << double(pt.x()) << double(pt.y());
|
||
}
|
||
|
||
writeAnimationBlock(ds, entity, true);
|
||
ds << entity.displayName << double(entity.userScale) << double(entity.distanceScaleCalibMult);
|
||
ds << bool(entity.ignoreDistanceScale);
|
||
ds << entity.parentId;
|
||
ds << double(entity.parentOffsetWorld.x()) << double(entity.parentOffsetWorld.y());
|
||
// v7:实体可见性关键帧
|
||
ds << qint32(entity.visibilityKeys.size());
|
||
for (const auto& k : entity.visibilityKeys) {
|
||
ds << qint32(k.frame) << bool(k.value);
|
||
}
|
||
writeIntroBlock(ds, entity);
|
||
// v8/v9:黑洞元数据(与实体可见性解耦)
|
||
ds << bool(entity.blackholeVisible);
|
||
const QString holeId = entity.blackholeId.isEmpty()
|
||
? QStringLiteral("blackhole-%1").arg(entity.id)
|
||
: entity.blackholeId;
|
||
ds << holeId;
|
||
ds << entity.blackholeResolvedBy;
|
||
}
|
||
|
||
bool readBody(QDataStream& ds) override {
|
||
Q_ASSERT(m_dst != nullptr);
|
||
Project::Entity tmp;
|
||
if (!readEntityPayloadV1(ds, tmp, true)) {
|
||
return false;
|
||
}
|
||
QString dn;
|
||
double us = 1.0;
|
||
double cal = 0.0;
|
||
ds >> dn >> us >> cal;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.displayName = dn;
|
||
tmp.userScale = std::clamp(us, 1e-3, 1e3);
|
||
tmp.distanceScaleCalibMult = (cal > 0.0) ? std::clamp(cal, 1e-6, 10.0) : 0.0;
|
||
bool ign = false;
|
||
QString pid;
|
||
double pox = 0.0;
|
||
double poy = 0.0;
|
||
ds >> ign >> pid >> pox >> poy;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.ignoreDistanceScale = ign;
|
||
tmp.parentId = pid;
|
||
tmp.parentOffsetWorld = QPointF(pox, poy);
|
||
|
||
// v7:实体可见性关键帧
|
||
qint32 nVis = 0;
|
||
ds >> nVis;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.visibilityKeys.clear();
|
||
if (nVis > 0) {
|
||
tmp.visibilityKeys.reserve(nVis);
|
||
for (qint32 i = 0; i < nVis; ++i) {
|
||
qint32 fr = 0;
|
||
bool val = true;
|
||
ds >> fr >> val;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
core::Project::ToolKeyframeBool k;
|
||
k.frame = int(fr);
|
||
k.value = val;
|
||
tmp.visibilityKeys.push_back(k);
|
||
}
|
||
}
|
||
if (!readIntroBlock(ds, tmp.intro)) {
|
||
return false;
|
||
}
|
||
bool holeVisible = true;
|
||
QString holeId;
|
||
QString resolvedBy;
|
||
ds >> holeVisible >> holeId >> resolvedBy;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.blackholeVisible = holeVisible;
|
||
tmp.blackholeId = holeId.isEmpty() ? QStringLiteral("blackhole-%1").arg(tmp.id) : holeId;
|
||
tmp.blackholeResolvedBy = resolvedBy;
|
||
*m_dst = std::move(tmp);
|
||
return true;
|
||
}
|
||
|
||
private:
|
||
const Project::Entity* m_src;
|
||
Project::Entity* m_dst;
|
||
};
|
||
|
||
class LegacyAnimSidecarRecord final : public PersistentBinaryObject {
|
||
public:
|
||
explicit LegacyAnimSidecarRecord(Project::Entity& e) : m_entity(&e) {}
|
||
|
||
quint32 recordMagic() const override { return EntityPayloadBinary::kMagicLegacyAnim; }
|
||
quint32 recordFormatVersion() const override { return EntityPayloadBinary::kLegacyAnimVersion; }
|
||
|
||
void writeBody(QDataStream& ds) const override { Q_UNUSED(ds); }
|
||
|
||
bool readBody(QDataStream& ds) override {
|
||
Project::Entity tmp = *m_entity;
|
||
if (!readAnimationBlock(ds, tmp, false)) {
|
||
return false;
|
||
}
|
||
m_entity->locationKeys = std::move(tmp.locationKeys);
|
||
m_entity->depthScaleKeys = std::move(tmp.depthScaleKeys);
|
||
m_entity->userScaleKeys = std::move(tmp.userScaleKeys);
|
||
m_entity->imageFrames = std::move(tmp.imageFrames);
|
||
return true;
|
||
}
|
||
|
||
private:
|
||
Project::Entity* m_entity;
|
||
};
|
||
|
||
} // namespace
|
||
|
||
bool EntityPayloadBinary::save(const QString& absolutePath, const Project::Entity& entity) {
|
||
if (absolutePath.isEmpty() || entity.id.isEmpty()) {
|
||
return false;
|
||
}
|
||
return EntityBinaryRecord(entity).saveToFile(absolutePath);
|
||
}
|
||
|
||
bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& entity) {
|
||
QFile f(absolutePath);
|
||
if (!f.open(QIODevice::ReadOnly)) {
|
||
return false;
|
||
}
|
||
QDataStream ds(&f);
|
||
ds.setVersion(QDataStream::Qt_5_15);
|
||
quint32 magic = 0;
|
||
quint32 ver = 0;
|
||
ds >> magic >> ver;
|
||
if (ds.status() != QDataStream::Ok || magic != kMagicPayload) {
|
||
return false;
|
||
}
|
||
if (ver != 1 && ver != 2 && ver != 3 && ver != 4 && ver != 5 && ver != 6 && ver != 7 && ver != 8 && ver != 9) {
|
||
return false;
|
||
}
|
||
Project::Entity tmp;
|
||
if (!readEntityPayloadV1(ds, tmp, ver >= 3)) {
|
||
return false;
|
||
}
|
||
if (ver >= 2) {
|
||
QString dn;
|
||
double us = 1.0;
|
||
ds >> dn >> us;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.displayName = dn;
|
||
tmp.userScale = std::clamp(us, 1e-3, 1e3);
|
||
if (ver >= 4) {
|
||
double cal = 0.0;
|
||
ds >> cal;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.distanceScaleCalibMult = (cal > 0.0) ? std::clamp(cal, 1e-6, 10.0) : 0.0;
|
||
}
|
||
if (ver >= 6) {
|
||
bool ign = false;
|
||
QString pid;
|
||
double pox = 0.0;
|
||
double poy = 0.0;
|
||
ds >> ign >> pid >> pox >> poy;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.ignoreDistanceScale = ign;
|
||
tmp.parentId = pid;
|
||
tmp.parentOffsetWorld = QPointF(pox, poy);
|
||
} else {
|
||
tmp.ignoreDistanceScale = false;
|
||
tmp.parentId.clear();
|
||
tmp.parentOffsetWorld = QPointF();
|
||
}
|
||
if (ver >= 7) {
|
||
qint32 nVis = 0;
|
||
ds >> nVis;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.visibilityKeys.clear();
|
||
if (nVis > 0) {
|
||
tmp.visibilityKeys.reserve(nVis);
|
||
for (qint32 i = 0; i < nVis; ++i) {
|
||
qint32 fr = 0;
|
||
bool val = true;
|
||
ds >> fr >> val;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
core::Project::ToolKeyframeBool k;
|
||
k.frame = int(fr);
|
||
k.value = val;
|
||
tmp.visibilityKeys.push_back(k);
|
||
}
|
||
}
|
||
} else {
|
||
tmp.visibilityKeys.clear();
|
||
}
|
||
if (ver >= 5) {
|
||
if (!readIntroBlock(ds, tmp.intro)) {
|
||
return false;
|
||
}
|
||
}
|
||
if (ver >= 8) {
|
||
bool holeVisible = true;
|
||
QString holeId;
|
||
ds >> holeVisible >> holeId;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.blackholeVisible = holeVisible;
|
||
tmp.blackholeId = holeId.isEmpty() ? QStringLiteral("blackhole-%1").arg(tmp.id) : holeId;
|
||
if (ver >= 9) {
|
||
QString resolvedBy;
|
||
ds >> resolvedBy;
|
||
if (ds.status() != QDataStream::Ok) {
|
||
return false;
|
||
}
|
||
tmp.blackholeResolvedBy = resolvedBy;
|
||
} else {
|
||
tmp.blackholeResolvedBy = QStringLiteral("pending");
|
||
}
|
||
} else {
|
||
tmp.blackholeVisible = true;
|
||
tmp.blackholeId = QStringLiteral("blackhole-%1").arg(tmp.id);
|
||
tmp.blackholeResolvedBy = QStringLiteral("pending");
|
||
}
|
||
} else {
|
||
tmp.displayName.clear();
|
||
tmp.userScale = 1.0;
|
||
tmp.ignoreDistanceScale = false;
|
||
tmp.parentId.clear();
|
||
tmp.parentOffsetWorld = QPointF();
|
||
tmp.visibilityKeys.clear();
|
||
tmp.blackholeVisible = true;
|
||
tmp.blackholeId = QStringLiteral("blackhole-%1").arg(tmp.id);
|
||
tmp.blackholeResolvedBy = QStringLiteral("pending");
|
||
}
|
||
entity = std::move(tmp);
|
||
return true;
|
||
}
|
||
|
||
bool EntityPayloadBinary::loadLegacyAnimFile(const QString& absolutePath, Project::Entity& entity) {
|
||
return LegacyAnimSidecarRecord(entity).loadFromFile(absolutePath);
|
||
}
|
||
|
||
} // namespace core
|