This commit is contained in:
2026-05-14 13:30:06 +08:00
parent 974946cee4
commit e43171521d
91 changed files with 10485 additions and 3254 deletions
File diff suppressed because it is too large Load Diff
+73 -7
View File
@@ -5,19 +5,24 @@
#include <QImage>
#include <QJsonObject>
#include <QRect>
#include <QSet>
#include <QTimer>
#include <QString>
#include <QStringList>
#include <QVector>
#include <QPoint>
namespace core {
namespace persistence {
class AsyncProjectWriter;
}
class ProjectWorkspace {
public:
static constexpr const char* kProjectIndexFileName = "project.json";
static constexpr const char* kAssetsDirName = "assets";
// 写入 project.json 的 version 字段;仍可读 version 1(内嵌实体 + 可选 .anim)。
static constexpr int kProjectIndexFormatVersion = 4;
static constexpr int kProjectIndexFormatVersion = 7;
ProjectWorkspace() = default;
@@ -58,6 +63,7 @@ public:
// 仅写盘(project.json + payload 同步)。动画 UI 直接编辑 Project 后可调用此函数持久化。
bool save();
bool saveAll();
// 历史操作(最多 30 步),类似 Blender:维护 undo/redo 栈
bool canUndo() const;
@@ -92,6 +98,7 @@ public:
bool setToolFontPx(const QString& id, int fontPx);
bool setToolAlign(const QString& id, core::Project::Tool::TextAlign align);
bool setToolVisibilityKey(const QString& id, int frame, bool visible);
bool setToolPriorityKey(const QString& id, int frame, int priority);
bool removeToolVisibilityKey(const QString& id, int frame);
bool setToolParent(const QString& id, const QString& parentId, const QPointF& parentOffsetWorld);
bool moveToolBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation);
@@ -113,9 +120,9 @@ public:
// 复制背景其他区域填充黑洞(sourceOffsetPx 以黑洞包围盒左上角为基准偏移)
bool resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
bool hideBlackholeAfterFill);
// 使用模型补全后的结果写回背景(patchedBackground 已包含补全贴合后的完整背景图像)
bool resolveBlackholeByModelInpaint(const QString& id, const QImage& patchedBackground,
bool hideBlackholeAfterFill);
// 模型补全的裁剪块保存为覆盖层并叠加显示,不修改 background 原图
bool resolveBlackholeByModelInpaint(const QString& id, const QImage& overlayPatch,
const QRect& overlayRectInBackground, bool hideBlackholeAfterFill);
bool setEntityVisibilityKey(const QString& id, int frame, bool visible);
bool removeEntityVisibilityKey(const QString& id, int frame);
bool setEntityDisplayName(const QString& id, const QString& displayName);
@@ -126,17 +133,42 @@ public:
// 将多边形质心平移到 targetCentroidWorld(整体平移);sTotal 须与画布一致
bool moveEntityCentroidTo(const QString& id, int frame, const QPointF& targetCentroidWorld, double sTotal,
bool autoKeyLocation);
/// 仅适用于有父实体:设置相对父形心的偏移(静态 parentOffsetWorld)。
bool setEntityParentOffsetWorld(const QString& id, const QPointF& offsetWorld);
// 在保持外形不变的前提下移动枢轴点;sTotal 须与画布一致(距离缩放×整体缩放)
bool reanchorEntityPivot(const QString& id, int frame, const QPointF& newPivotWorld, double sTotal);
/// 将各实体 origin 与多边形形心对齐(外形不变);打开工程时迁移旧数据。
bool normalizeAllEntityOriginsToCentroids(int frame);
/// 当前帧下实体多边形形心的世界坐标(与画布深度/距离缩放采样点一致);工具 id 则返回其原点。
[[nodiscard]] QPointF entityCentroidWorldAtFrame(const QString& id, int frame) const;
bool reorderEntitiesById(const QStringList& idsInOrder);
// currentFrame:自动关键帧时写入位置曲线;autoKeyLocation 为 false 时忽略。
bool moveEntityBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation);
bool setEntityLocationKey(const QString& id, int frame, const QPointF& originWorld);
bool setEntityDepthScaleKey(const QString& id, int frame, double value01);
bool setEntityUserScaleKey(const QString& id, int frame, double userScale);
bool setEntityPriorityKey(const QString& id, int frame, int priority);
bool setEntityDefaultImage(const QString& id, const QImage& image, QString* outRelPath = nullptr);
bool setEntityImageFrame(const QString& id, int frame, const QImage& image, QString* outRelPath = nullptr);
// 仅更新 imageFrames 中某帧的图像路径(不读图、不写盘),用于高性能地“切断”Hold 区间
bool setEntityImageFramePath(const QString& id, int frame, const QString& relativePath);
QVector<Project::Entity::ImageFrame> entitySpriteFrames(const QString& id) const;
QString entityImagePathAt(const QString& id, int frame) const;
QByteArray entityImagePngAt(const QString& id, int frame) const;
bool clearEntityImageFramesInRange(const QString& id, int startFrame, int endFrame);
int activeAnimationLengthFrames() const;
int activeAnimationFps() const;
int animationLengthFramesForId(const QString& animationId) const;
int animationFpsForId(const QString& animationId) const;
bool animationLoopsForId(const QString& animationId) const;
bool createAnimationScheme(const QString& displayName, int lengthFrames, bool loop, int fps,
QString* outNewId = nullptr);
bool updateAnimationSchemeSettings(const QString& id, const QString& displayName, int lengthFrames, bool loop,
int fps);
bool applyPresentationHotspots(const QVector<Project::PresentationHotspot>& hotspots, bool recordHistory,
const QString& label);
bool removeEntityLocationKey(const QString& id, int frame);
bool removeEntityDepthScaleKey(const QString& id, int frame);
bool removeEntityUserScaleKey(const QString& id, int frame);
@@ -152,8 +184,9 @@ private:
bool readIndexJson(const QString& indexPath);
bool syncEntityPayloadsToDisk();
bool syncAnimationPayloadsToDisk();
bool hydrateEntityPayloadsFromDisk();
void loadV1LegacyAnimationSidecars();
bool hydrateAnimationPayloadsFromDisk();
bool writeIndexJsonWithoutPayloadSync();
bool saveSingleEntityPayload(Project::Entity& entity);
@@ -163,7 +196,6 @@ private:
static QString fileSuffixWithDot(const QString& path);
static QString asOptionalRelativeUnderProject(const QString& relativePath);
static QJsonObject entityToJson(const Project::Entity& e);
static bool entityFromJsonV1(const QJsonObject& o, Project::Entity& out);
static bool entityStubFromJsonV2(const QJsonObject& o, Project::Entity& out);
static QJsonObject toolToJson(const Project::Tool& t);
static bool toolFromJsonV2(const QJsonObject& o, Project::Tool& out);
@@ -174,7 +206,16 @@ private:
bool ensureDefaultCameraIfMissing(bool* outAdded = nullptr);
struct Operation {
enum class Type { ImportBackground, SetEntities, SetTools, SetCameras, SetProjectTitle, SetProjectFrameRange };
enum class Type {
ImportBackground,
SetEntities,
SetTools,
SetCameras,
SetAnimations,
SetPresentationHotspots,
SetProjectTitle,
SetProjectFrameRange
};
Type type {Type::ImportBackground};
QString label;
QString beforeBackgroundPath;
@@ -185,6 +226,14 @@ private:
QVector<Project::Tool> afterTools;
QVector<Project::Camera> beforeCameras;
QVector<Project::Camera> afterCameras;
QVector<Project::Animation> beforeAnimations;
QVector<Project::Animation> afterAnimations;
bool animBundlesPresentationHotspots = false;
QVector<Project::PresentationHotspot> animSnapshotBeforePresentationHotspots;
QVector<Project::PresentationHotspot> animSnapshotAfterPresentationHotspots;
QVector<Project::PresentationHotspot> beforePresentationHotspots;
QVector<Project::PresentationHotspot> afterPresentationHotspots;
QString beforeProjectTitle;
QString afterProjectTitle;
int beforeFrameStart = 0;
@@ -200,19 +249,36 @@ private:
bool applyEntities(const QVector<Project::Entity>& entities, bool recordHistory, const QString& label);
bool applyTools(const QVector<Project::Tool>& tools, bool recordHistory, const QString& label);
bool applyCameras(const QVector<Project::Camera>& cameras, bool recordHistory, const QString& label);
bool applyAnimations(const QVector<Project::Animation>& animations, bool recordHistory, const QString& label);
QString copyIntoAssetsAsBackground(const QString& sourceFilePath, const QRect& cropRectInSourceImage);
bool writeDepthMap(const QImage& depth8);
bool writeDepthMapBytes(const QByteArray& pngBytes);
QString ensureEntitiesDir() const;
QString ensureAnimationsDir() const;
bool writeEntityImage(const QString& entityId, const QImage& image, QString& outRelPath);
bool writeEntityFrameImage(const QString& entityId, int frame, const QImage& image, QString& outRelPath);
QString defaultAnimationPayloadPath(const QString& animationId) const;
void markIndexDirty();
/// snapshot:在尚未写入 m_project 前(例如新实体)必须传入,否则异步 bundle 写会漏队列。
void markEntityDirty(const QString& id, const Project::Entity* snapshot = nullptr);
void markAnimationDirty(const QString& id);
void markAllEntitiesDirty();
void markAllAnimationsDirty();
void schedulePayloadSync();
bool syncPayloadsToDiskOnce();
private:
QString m_projectDir;
Project m_project;
persistence::AsyncProjectWriter* m_asyncWriter = nullptr;
QVector<Operation> m_undoStack;
QVector<Operation> m_redoStack;
QSet<QString> m_dirtyEntityIds;
QSet<QString> m_dirtyAnimationIds;
bool m_indexDirty = false;
QTimer* m_payloadSyncTimer = nullptr;
};
} // namespace core