增加预览页介绍信息显示

This commit is contained in:
2026-04-08 09:56:25 +08:00
parent f53fee8e5a
commit 028ed1b18d
17 changed files with 1059 additions and 23 deletions

View File

@@ -12,6 +12,7 @@ set(CORE_SOURCES
)
set(CORE_HEADERS
${CORE_ROOT}/domain/EntityIntro.h
${CORE_ROOT}/domain/Project.h
${CORE_ROOT}/workspace/ProjectWorkspace.h
${CORE_ROOT}/persistence/PersistentBinaryObject.h

View File

@@ -0,0 +1,29 @@
#pragma once
#include <QString>
#include <QVector>
namespace core {
/// 实体在「预览展示」模式下点击后显示的介绍内容(可持久化到 .hfe
struct EntityIntroContent {
QString title;
QString bodyText;
QVector<QString> imagePathsRelative;
/// 预留:相对项目根的视频路径,将来可用 QMediaPlayer 播放
QString videoPathRelative;
void clear() {
title.clear();
bodyText.clear();
imagePathsRelative.clear();
videoPathRelative.clear();
}
bool hasDisplayableContent() const {
return !title.trimmed().isEmpty() || !bodyText.trimmed().isEmpty() ||
!imagePathsRelative.isEmpty() || !videoPathRelative.trimmed().isEmpty();
}
};
} // namespace core

View File

@@ -1,5 +1,7 @@
#pragma once
#include "domain/EntityIntro.h"
#include <QString>
#include <QPointF>
#include <QVector>
@@ -47,8 +49,11 @@ public:
int depth = 0; // 0..255
QString imagePath; // 相对路径,例如 "assets/entities/entity-1.png"
QPointF imageTopLeftWorld; // 贴图左上角 world 坐标
// 人为整体缩放,与深度驱动的距离缩放相乘(画布中 visualScale = distanceScale * userScale
// 人为整体缩放,与深度驱动的距离缩放相乘(画布中 visualScale = distanceScale * userScale
// distanceScale 在有 distanceScaleCalibMult 时为 (0.5+depth01)/calib使抠图处为 1.0
double userScale = 1.0;
// 抠图创建时该位置对应的原始距离乘子0.5+depth01用于校准该处 distanceScale==1.0。0 表示未校准(兼容旧工程)
double distanceScaleCalibMult = 0.0;
struct KeyframeVec2 {
int frame = 0;
@@ -76,6 +81,8 @@ public:
QVector<KeyframeFloat01> depthScaleKeys;
QVector<KeyframeDouble> userScaleKeys;
QVector<ImageFrame> imageFrames;
EntityIntroContent intro;
};
void setEntities(const QVector<Entity>& entities) { m_entities = entities; }

View File

@@ -30,6 +30,36 @@ void sortByFrame(QVector<Project::Entity::ImageFrame>& v) {
std::sort(v.begin(), v.end(), [](const auto& a, const auto& b) { return a.frame < b.frame; });
}
void writeIntroBlock(QDataStream& ds, const Project::Entity& entity) {
ds << entity.intro.title << entity.intro.bodyText;
ds << qint32(entity.intro.imagePathsRelative.size());
for (const auto& p : entity.intro.imagePathsRelative) {
ds << p;
}
ds << entity.intro.videoPathRelative;
}
bool readIntroBlock(QDataStream& ds, EntityIntroContent& intro) {
ds >> intro.title >> intro.bodyText;
qint32 n = 0;
ds >> n;
if (ds.status() != QDataStream::Ok || n < 0 || n > 2048) {
return false;
}
intro.imagePathsRelative.clear();
intro.imagePathsRelative.reserve(n);
for (qint32 i = 0; i < n; ++i) {
QString p;
ds >> p;
if (ds.status() != QDataStream::Ok) {
return false;
}
intro.imagePathsRelative.push_back(std::move(p));
}
ds >> intro.videoPathRelative;
return ds.status() == QDataStream::Ok;
}
bool readAnimationBlock(QDataStream& ds, Project::Entity& out, bool hasUserScaleKeys) {
out.locationKeys.clear();
out.depthScaleKeys.clear();
@@ -220,7 +250,8 @@ public:
}
writeAnimationBlock(ds, entity, true);
ds << entity.displayName << double(entity.userScale);
ds << entity.displayName << double(entity.userScale) << double(entity.distanceScaleCalibMult);
writeIntroBlock(ds, entity);
}
bool readBody(QDataStream& ds) override {
@@ -231,12 +262,17 @@ public:
}
QString dn;
double us = 1.0;
ds >> dn >> us;
double cal = 0.0;
ds >> dn >> us >> cal;
if (ds.status() != QDataStream::Ok) {
return false;
}
tmp.displayName = dn;
tmp.userScale = std::clamp(us, 1e-3, 1e3);
tmp.distanceScaleCalibMult = (cal > 0.0) ? std::clamp(cal, 1e-6, 10.0) : 0.0;
if (!readIntroBlock(ds, tmp.intro)) {
return false;
}
*m_dst = std::move(tmp);
return true;
}
@@ -293,7 +329,7 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
if (ds.status() != QDataStream::Ok || magic != kMagicPayload) {
return false;
}
if (ver != 1 && ver != 2 && ver != 3) {
if (ver != 1 && ver != 2 && ver != 3 && ver != 4 && ver != 5) {
return false;
}
Project::Entity tmp;
@@ -309,6 +345,19 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
}
tmp.displayName = dn;
tmp.userScale = std::clamp(us, 1e-3, 1e3);
if (ver >= 4) {
double cal = 0.0;
ds >> cal;
if (ds.status() != QDataStream::Ok) {
return false;
}
tmp.distanceScaleCalibMult = (cal > 0.0) ? std::clamp(cal, 1e-6, 10.0) : 0.0;
}
if (ver >= 5) {
if (!readIntroBlock(ds, tmp.intro)) {
return false;
}
}
} else {
tmp.displayName.clear();
tmp.userScale = 1.0;

View File

@@ -12,7 +12,7 @@ namespace core {
class EntityPayloadBinary {
public:
static constexpr quint32 kMagicPayload = 0x48464550; // 'HFEP'
static constexpr quint32 kPayloadVersion = 3; // v3:追加 userScaleKeys动画轨道
static constexpr quint32 kPayloadVersion = 5; // v5:追加 intro预览介绍标题/正文/配图/视频路径预留
// 旧版独立动画文件(仍用于打开 v1 项目时合并)
static constexpr quint32 kMagicLegacyAnim = 0x48465441; // 'HFTA'

View File

@@ -1031,7 +1031,59 @@ bool ProjectWorkspace::setEntityDisplayName(const QString& id, const QString& di
return applyEntities(ents, true, QStringLiteral("重命名实体"));
}
bool ProjectWorkspace::setEntityUserScale(const QString& id, double userScale) {
bool ProjectWorkspace::setEntityIntroContent(const QString& id, const EntityIntroContent& intro) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
auto ents = m_project.entities();
bool found = false;
for (auto& e : ents) {
if (e.id != id) {
continue;
}
found = true;
e.intro = intro;
break;
}
if (!found) {
return false;
}
return applyEntities(ents, true, QStringLiteral("实体介绍"));
}
bool ProjectWorkspace::importEntityIntroImageFromFile(const QString& id, const QString& absoluteImagePath,
QString* outRelativePath) {
if (m_projectDir.isEmpty() || id.isEmpty() || absoluteImagePath.isEmpty()) {
return false;
}
const QFileInfo srcFi(absoluteImagePath);
if (!srcFi.exists() || !srcFi.isFile()) {
return false;
}
const QString entsDir = ensureEntitiesDir();
if (entsDir.isEmpty()) {
return false;
}
const QString suf = fileSuffixWithDot(absoluteImagePath);
for (int n = 0; n < 100000; ++n) {
const QString base = id + QStringLiteral("-intro-%1").arg(n) + suf;
const QString destAbs = QDir(entsDir).filePath(base);
if (!QFileInfo::exists(destAbs)) {
if (!QFile::copy(absoluteImagePath, destAbs)) {
return false;
}
const QString rel =
QString::fromUtf8(kAssetsDirName) + QStringLiteral("/entities/") + base;
if (outRelativePath) {
*outRelativePath = rel;
}
return true;
}
}
return false;
}
bool ProjectWorkspace::setEntityUserScale(const QString& id, double userScale, int keyframeAtFrame) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
@@ -1043,10 +1095,13 @@ bool ProjectWorkspace::setEntityUserScale(const QString& id, double userScale) {
continue;
}
found = true;
if (qFuzzyCompare(e.userScale + 1.0, u + 1.0)) {
const bool baseSame = qFuzzyCompare(e.userScale + 1.0, u + 1.0);
e.userScale = u;
if (keyframeAtFrame >= 0) {
upsertKey(e.userScaleKeys, keyframeAtFrame, std::clamp(u, 1e-6, 1e3));
} else if (baseSame) {
return true;
}
e.userScale = u;
break;
}
if (!found) {
@@ -1196,8 +1251,17 @@ bool ProjectWorkspace::moveEntityBy(const QString& id, const QPointF& delta, int
e.originWorld,
KeyInterpolation::Linear);
upsertKey(e.locationKeys, currentFrame, sampled + delta);
} else if (e.locationKeys.isEmpty()) {
e.originWorld += delta;
e.imageTopLeftWorld += delta;
} else if (currentFrame >= 0) {
// 已有位置曲线但未勾选「自动关键帧」时,仍应移动当前帧上的位置,否则画布仍按关键帧插值,看起来不生效
const QPointF sampled = sampleLocation(e.locationKeys,
currentFrame,
e.originWorld,
KeyInterpolation::Linear);
upsertKey(e.locationKeys, currentFrame, sampled + delta);
} else {
// 无自动关键帧时,直接修改“基准”位置
e.originWorld += delta;
e.imageTopLeftWorld += delta;
}

View File

@@ -77,7 +77,8 @@ public:
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);
/// keyframeAtFrame >= 0 时同时写入该帧的 userScale 关键帧(与画布 sampleUserScale 一致)
bool setEntityUserScale(const QString& id, double userScale, int keyframeAtFrame = -1);
// 将多边形质心平移到 targetCentroidWorld整体平移sTotal 须与画布一致
bool moveEntityCentroidTo(const QString& id, int frame, const QPointF& targetCentroidWorld, double sTotal,
bool autoKeyLocation);
@@ -95,6 +96,11 @@ public:
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);