Files
hfut-bishe/client/core/workspace/ProjectWorkspace.h
T
2026-05-15 00:46:37 +08:00

286 lines
16 KiB
C++
Raw 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 <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 = 7;
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();
bool saveAll();
// 历史操作(最多 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(); }
const QVector<Project::Camera>& cameras() const { return m_project.cameras(); }
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 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);
bool setCameraVisible(const QString& id, bool on);
bool setActiveCameraId(const QString& id);
bool setCameraDisplayName(const QString& id, const QString& displayName);
bool setCameraCenterWorld(const QString& id, const QPointF& centerWorld);
/// keyframeAtFrame >= 0 时同时写入当前活动 clip 的缩放关键帧(与实体 userScale 行为一致)
bool setCameraViewScaleValue(const QString& id, double viewScale, int keyframeAtFrame = -1);
bool moveCameraBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation);
bool setCameraLocationKey(const QString& id, int frame, const QPointF& centerWorld);
bool setCameraScaleKey(const QString& id, int frame, double viewScale);
bool removeCameraLocationKey(const QString& id, int frame);
bool removeCameraScaleKey(const QString& id, int frame);
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);
// 复制背景其他区域填充黑洞(矩形均为背景逻辑像素坐标,须与画布一致且 src 与 target 同尺寸)
bool resolveBlackholeByCopyBackground(const QString& id, const QRect& targetWorldInBackground,
const QRect& srcWorldInBackground, 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);
/// 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);
/// 仅适用于有父实体:设置相对父形心的偏移(静态 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);
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 syncAnimationPayloadsToDisk();
bool hydrateEntityPayloadsFromDisk();
bool hydrateAnimationPayloadsFromDisk();
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 entityStubFromJsonV2(const QJsonObject& o, Project::Entity& out);
static QJsonObject toolToJson(const Project::Tool& t);
static bool toolFromJsonV2(const QJsonObject& o, Project::Tool& out);
static QJsonObject cameraToJson(const Project::Camera& c);
static bool cameraFromJsonV4(const QJsonObject& o, Project::Camera& out);
/// 若工程尚无摄像机,则创建默认主摄像机(居中背景、缩放适配整张背景图)。不写盘;若创建了摄像机且 outAdded 非空则 *outAdded=true。
bool ensureDefaultCameraIfMissing(bool* outAdded = nullptr);
struct Operation {
enum class Type {
ImportBackground,
SetEntities,
SetTools,
SetCameras,
SetAnimations,
SetPresentationHotspots,
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;
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;
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);
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