#pragma once #include "domain/Project.h" #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 = 2; 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& project() { return m_project; } const Project& project() const { return m_project; } // 历史操作(最多 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(); } bool addEntity(const Project::Entity& entity, const QImage& image); bool setEntityVisible(const QString& id, bool on); bool setEntityDisplayName(const QString& id, const QString& displayName); bool setEntityUserScale(const QString& id, double userScale); // 将多边形质心平移到 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); 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); private: bool writeIndexJson(); bool readIndexJson(const QString& indexPath); bool syncEntityPayloadsToDisk(); bool hydrateEntityPayloadsFromDisk(); void loadV1LegacyAnimationSidecars(); 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); struct Operation { enum class Type { ImportBackground, SetEntities, SetProjectTitle }; Type type {Type::ImportBackground}; QString label; QString beforeBackgroundPath; QString afterBackgroundPath; QVector beforeEntities; QVector afterEntities; QString beforeProjectTitle; QString afterProjectTitle; }; 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); 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