增加预览页介绍信息显示
This commit is contained in:
@@ -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
|
||||
|
||||
29
client/core/domain/EntityIntro.h
Normal file
29
client/core/domain/EntityIntro.h
Normal 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
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user