#pragma once #include "domain/Project.h" #include #include #include #include #include #include #include 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): // /project.json (索引:背景/深度路径 + 实体 id 与 .hfe 路径) // /assets/background.png // /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& entities() const { return m_project.entities(); } const QVector& 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 beforeEntities; QVector afterEntities; QVector beforeTools; QVector 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& entities, bool recordHistory, const QString& label); bool applyTools(const QVector& 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 m_undoStack; QVector m_redoStack; }; } // namespace core