initial commit

This commit is contained in:
2026-04-07 20:55:30 +08:00
commit 81d1fb7856
84 changed files with 11929 additions and 0 deletions

View File

@@ -0,0 +1,324 @@
#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; });
}
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);
}
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;
ds >> dn >> us;
if (ds.status() != QDataStream::Ok) {
return false;
}
tmp.displayName = dn;
tmp.userScale = std::clamp(us, 1e-3, 1e3);
*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) {
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);
} else {
tmp.displayName.clear();
tmp.userScale = 1.0;
}
entity = std::move(tmp);
return true;
}
bool EntityPayloadBinary::loadLegacyAnimFile(const QString& absolutePath, Project::Entity& entity) {
return LegacyAnimSidecarRecord(entity).loadFromFile(absolutePath);
}
} // namespace core

View File

@@ -0,0 +1,30 @@
#pragma once
#include "domain/Project.h"
#include <QString>
namespace core {
// 实体完整数据(几何 + 贴图路径 + 动画轨道)的二进制格式,与 project.json v2 的 payload 字段对应。
// 贴图 PNG 仍单独存放在 assets/entities/,本文件不嵌入像素。
// 具体读写通过继承 PersistentBinaryObject 的适配器类完成(见 EntityPayloadBinary.cpp
class EntityPayloadBinary {
public:
static constexpr quint32 kMagicPayload = 0x48464550; // 'HFEP'
static constexpr quint32 kPayloadVersion = 3; // v3追加 userScaleKeys动画轨道
// 旧版独立动画文件(仍用于打开 v1 项目时合并)
static constexpr quint32 kMagicLegacyAnim = 0x48465441; // 'HFTA'
static constexpr quint32 kLegacyAnimVersion = 1;
static bool save(const QString& absolutePath, const Project::Entity& entity);
// 读入后覆盖 entity 中除调用方已校验外的字段;失败时尽量保持 entity 不变。
static bool load(const QString& absolutePath, Project::Entity& entity);
// 仅读取旧 .animHFTA写入 entity 的三条动画轨道。
static bool loadLegacyAnimFile(const QString& absolutePath, Project::Entity& entity);
};
} // namespace core

View File

@@ -0,0 +1,57 @@
#include "persistence/PersistentBinaryObject.h"
#include <QDataStream>
#include <QDir>
#include <QFile>
#include <QFileInfo>
namespace core {
bool PersistentBinaryObject::saveToFile(const QString& absolutePath) const {
if (absolutePath.isEmpty()) {
return false;
}
const auto parent = QFileInfo(absolutePath).absolutePath();
if (!QFileInfo(parent).exists()) {
QDir().mkpath(parent);
}
const QString tmpPath = absolutePath + QStringLiteral(".tmp");
QFile f(tmpPath);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return false;
}
QDataStream ds(&f);
ds.setVersion(QDataStream::Qt_5_15);
ds << quint32(recordMagic()) << quint32(recordFormatVersion());
writeBody(ds);
f.close();
if (f.error() != QFile::NoError) {
QFile::remove(tmpPath);
return false;
}
QFile::remove(absolutePath);
return QFile::rename(tmpPath, absolutePath);
}
bool PersistentBinaryObject::loadFromFile(const QString& absolutePath) {
QFile f(absolutePath);
if (!f.open(QIODevice::ReadOnly)) {
return false;
}
QDataStream ds(&f);
ds.setVersion(QDataStream::Qt_5_15);
quint32 magic = 0;
quint32 version = 0;
ds >> magic >> version;
if (ds.status() != QDataStream::Ok || magic != recordMagic() || version != recordFormatVersion()) {
return false;
}
return readBody(ds);
}
} // namespace core

View File

@@ -0,0 +1,27 @@
#pragma once
#include <QString>
class QDataStream;
namespace core {
// 二进制记录的统一持久化基类:魔数/版本、QDataStream 版本、.tmp 原子替换、父目录创建。
//
// 领域类型(如 Project::Entity应保持为可拷贝的值类型不要继承本类为每种存储格式写一个
// 小的适配器类(如 EntityBinaryRecord继承本类并实现 writeBody/readBody 即可。
class PersistentBinaryObject {
public:
virtual ~PersistentBinaryObject() = default;
[[nodiscard]] bool saveToFile(const QString& absolutePath) const;
[[nodiscard]] bool loadFromFile(const QString& absolutePath);
protected:
virtual quint32 recordMagic() const = 0;
virtual quint32 recordFormatVersion() const = 0;
virtual void writeBody(QDataStream& ds) const = 0;
virtual bool readBody(QDataStream& ds) = 0;
};
} // namespace core