Files
hfut-bishe/client/core/workspace/ProjectWorkspace.h
2026-04-09 23:38:14 +08:00

200 lines
11 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 必须 >=0end 会被归一化为 >= 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.jsondepthComputed/depthMapPath
bool computeFakeDepthForProject();
// 从后端计算深度并落盘assets/depth.png同时更新 project.jsondepthComputed/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);
// 使用模型补全后的结果写回背景patchedBackground 已包含补全贴合后的完整背景图像)
bool resolveBlackholeByModelInpaint(const QString& id, const QImage& patchedBackground,
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