197 lines
10 KiB
C++
197 lines
10 KiB
C++
#pragma once
|
||
|
||
#include "domain/Project.h"
|
||
|
||
#include <QImage>
|
||
#include <QJsonObject>
|
||
#include <QRect>
|
||
#include <QString>
|
||
#include <QStringList>
|
||
#include <QVector>
|
||
#include <QPoint>
|
||
|
||
namespace core {
|
||
|
||
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 = 3;
|
||
|
||
ProjectWorkspace() = default;
|
||
|
||
// 新建项目:
|
||
// - 传入的 parentDir 是“父目录”(你在文件对话框中选择的目录)
|
||
// - 会在 parentDir 下创建一个新的项目目录(默认使用项目名做文件夹名;若重名会自动加后缀)
|
||
// - 项目结构为(v2):
|
||
// <projectDir>/project.json (索引:背景/深度路径 + 实体 id 与 .hfe 路径)
|
||
// <projectDir>/assets/background.png
|
||
// <projectDir>/assets/entities/*.png / *.hfe
|
||
bool createNew(const QString& parentDir, const QString& name, const QString& backgroundImageSourcePath);
|
||
bool createNew(const QString& parentDir, const QString& name, const QString& backgroundImageSourcePath,
|
||
const QRect& cropRectInSourceImage);
|
||
bool openExisting(const QString& projectDir);
|
||
void close();
|
||
|
||
bool isOpen() const { return !m_projectDir.isEmpty(); }
|
||
const QString& projectDir() const { return m_projectDir; }
|
||
QString indexFilePath() const;
|
||
QString assetsDirPath() const;
|
||
bool hasBackground() const { return !m_project.backgroundImagePath().isEmpty(); }
|
||
QString backgroundAbsolutePath() const;
|
||
bool backgroundVisible() const { return m_project.backgroundVisible(); }
|
||
bool setBackgroundVisible(bool on);
|
||
|
||
bool hasDepth() const;
|
||
QString depthAbsolutePath() const;
|
||
|
||
// 写入 project.json 的 name 字段(可 undo)
|
||
bool setProjectTitle(const QString& title);
|
||
// 时间轴范围(写入 project.json,可 undo)。start 必须 >=0;end 会被归一化为 >= start
|
||
bool setProjectFrameRange(int start, int end);
|
||
// 自动扩展 end(用于“无限延伸”):仅在 end 变大时写盘;recordHistory=false 时不占用 undo 栈
|
||
bool ensureProjectFrameEndAtLeast(int end, bool recordHistory = false);
|
||
|
||
Project& project() { return m_project; }
|
||
const Project& project() const { return m_project; }
|
||
|
||
// 仅写盘(project.json + payload 同步)。动画 UI 直接编辑 Project 后可调用此函数持久化。
|
||
bool save();
|
||
|
||
// 历史操作(最多 30 步),类似 Blender:维护 undo/redo 栈
|
||
bool canUndo() const;
|
||
bool canRedo() const;
|
||
bool undo();
|
||
bool redo();
|
||
QStringList historyLabelsNewestFirst() const;
|
||
|
||
// 追加一次“导入并设置背景图”操作:把图片拷贝进 assets/,并作为背景写入项目(会进入历史)。
|
||
bool importBackgroundImage(const QString& backgroundImageSourcePath);
|
||
bool importBackgroundImage(const QString& backgroundImageSourcePath, const QRect& cropRectInSourceImage);
|
||
|
||
// 计算并写入假深度图:assets/depth.png,同时更新 project.json(depthComputed/depthMapPath)。
|
||
bool computeFakeDepthForProject();
|
||
|
||
// 从后端计算深度并落盘:assets/depth.png,同时更新 project.json(depthComputed/depthMapPath)。
|
||
// - serverBaseUrl 为空时:优先读环境变量 MODEL_SERVER_URL,否则默认 http://127.0.0.1:8000
|
||
// - outError 可选:返回失败原因
|
||
bool computeDepthForProjectFromServer(const QString& serverBaseUrl, QString* outError = nullptr, int timeoutMs = 30000);
|
||
|
||
// 直接保存深度图(PNG bytes)到 assets/depth.png,并更新 project.json。
|
||
bool saveDepthMapPngBytes(const QByteArray& pngBytes, QString* outError = nullptr);
|
||
|
||
const QVector<Project::Entity>& entities() const { return m_project.entities(); }
|
||
const QVector<Project::Tool>& tools() const { return m_project.tools(); }
|
||
|
||
bool addTool(const Project::Tool& tool);
|
||
bool setToolVisible(const QString& id, bool on);
|
||
bool setToolText(const QString& id, const QString& text);
|
||
bool setToolBubblePointerT01(const QString& id, double t01);
|
||
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 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);
|
||
bool addEntity(const Project::Entity& entity, const QImage& image);
|
||
bool setEntityVisible(const QString& id, bool on);
|
||
bool setEntityBlackholeVisible(const QString& id, bool on);
|
||
bool resolveBlackholeByUseOriginalBackground(const QString& id);
|
||
// 复制背景其他区域填充黑洞(sourceOffsetPx 以黑洞包围盒左上角为基准偏移)
|
||
bool resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
|
||
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);
|
||
/// keyframeAtFrame >= 0 时同时写入该帧的 userScale 关键帧(与画布 sampleUserScale 一致)
|
||
bool setEntityUserScale(const QString& id, double userScale, int keyframeAtFrame = -1);
|
||
bool setEntityIgnoreDistanceScale(const QString& id, bool on);
|
||
bool setEntityParent(const QString& id, const QString& parentId, const QPointF& parentOffsetWorld);
|
||
// 将多边形质心平移到 targetCentroidWorld(整体平移);sTotal 须与画布一致
|
||
bool moveEntityCentroidTo(const QString& id, int frame, const QPointF& targetCentroidWorld, double sTotal,
|
||
bool autoKeyLocation);
|
||
// 在保持外形不变的前提下移动枢轴点;sTotal 须与画布一致(距离缩放×整体缩放)
|
||
bool reanchorEntityPivot(const QString& id, int frame, const QPointF& newPivotWorld, double sTotal);
|
||
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 setEntityImageFrame(const QString& id, int frame, const QImage& image, QString* outRelPath = nullptr);
|
||
// 仅更新 imageFrames 中某帧的图像路径(不读图、不写盘),用于高性能地“切断”Hold 区间
|
||
bool setEntityImageFramePath(const QString& id, int frame, const QString& relativePath);
|
||
bool removeEntityLocationKey(const QString& id, int frame);
|
||
bool removeEntityDepthScaleKey(const QString& id, int frame);
|
||
bool removeEntityUserScaleKey(const QString& id, int frame);
|
||
bool removeEntityImageFrame(const QString& id, int frame);
|
||
|
||
bool setEntityIntroContent(const QString& id, const EntityIntroContent& intro);
|
||
/// 将外部图片拷贝到 assets/entities/ 并返回相对项目根的路径
|
||
bool importEntityIntroImageFromFile(const QString& id, const QString& absoluteImagePath,
|
||
QString* outRelativePath = nullptr);
|
||
|
||
private:
|
||
bool writeIndexJson();
|
||
bool readIndexJson(const QString& indexPath);
|
||
|
||
bool syncEntityPayloadsToDisk();
|
||
bool hydrateEntityPayloadsFromDisk();
|
||
void loadV1LegacyAnimationSidecars();
|
||
bool writeIndexJsonWithoutPayloadSync();
|
||
bool saveSingleEntityPayload(Project::Entity& entity);
|
||
|
||
static QJsonObject projectToJson(const Project& project);
|
||
static bool projectFromJson(const QJsonObject& root, Project& outProject, int* outFileVersion);
|
||
static QString asRelativeUnderProject(const QString& relativePath);
|
||
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);
|
||
|
||
struct Operation {
|
||
enum class Type { ImportBackground, SetEntities, SetTools, SetProjectTitle, SetProjectFrameRange };
|
||
Type type {Type::ImportBackground};
|
||
QString label;
|
||
QString beforeBackgroundPath;
|
||
QString afterBackgroundPath;
|
||
QVector<Project::Entity> beforeEntities;
|
||
QVector<Project::Entity> afterEntities;
|
||
QVector<Project::Tool> beforeTools;
|
||
QVector<Project::Tool> afterTools;
|
||
QString beforeProjectTitle;
|
||
QString afterProjectTitle;
|
||
int beforeFrameStart = 0;
|
||
int afterFrameStart = 0;
|
||
int beforeFrameEnd = 600;
|
||
int afterFrameEnd = 600;
|
||
};
|
||
|
||
static constexpr int kMaxHistorySteps = 30;
|
||
void pushOperation(const Operation& op);
|
||
|
||
bool applyBackgroundPath(const QString& relativePath, bool recordHistory, const QString& label);
|
||
bool applyEntities(const QVector<Project::Entity>& entities, bool recordHistory, const QString& label);
|
||
bool applyTools(const QVector<Project::Tool>& tools, 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;
|
||
bool writeEntityImage(const QString& entityId, const QImage& image, QString& outRelPath);
|
||
bool writeEntityFrameImage(const QString& entityId, int frame, const QImage& image, QString& outRelPath);
|
||
|
||
private:
|
||
QString m_projectDir;
|
||
Project m_project;
|
||
|
||
QVector<Operation> m_undoStack;
|
||
QVector<Operation> m_redoStack;
|
||
};
|
||
|
||
} // namespace core
|
||
|