新增根据帧数控制可见性
This commit is contained in:
@@ -9,6 +9,11 @@ set(CORE_SOURCES
|
||||
${CORE_ROOT}/animation/AnimationSampling.cpp
|
||||
${CORE_ROOT}/depth/DepthService.cpp
|
||||
${CORE_ROOT}/net/ModelServerClient.cpp
|
||||
${CORE_ROOT}/library/EntityJson.cpp
|
||||
${CORE_ROOT}/library/ToolJson.cpp
|
||||
${CORE_ROOT}/library/ResourceLibraryProvider.cpp
|
||||
${CORE_ROOT}/library/OnlineResourceLibraryProvider.cpp
|
||||
${CORE_ROOT}/eval/ProjectEvaluator.cpp
|
||||
)
|
||||
|
||||
set(CORE_HEADERS
|
||||
@@ -20,6 +25,11 @@ set(CORE_HEADERS
|
||||
${CORE_ROOT}/animation/AnimationSampling.h
|
||||
${CORE_ROOT}/depth/DepthService.h
|
||||
${CORE_ROOT}/net/ModelServerClient.h
|
||||
${CORE_ROOT}/library/EntityJson.h
|
||||
${CORE_ROOT}/library/ToolJson.h
|
||||
${CORE_ROOT}/library/ResourceLibraryProvider.h
|
||||
${CORE_ROOT}/library/OnlineResourceLibraryProvider.h
|
||||
${CORE_ROOT}/eval/ProjectEvaluator.h
|
||||
)
|
||||
|
||||
add_library(core STATIC
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <QString>
|
||||
#include <QPointF>
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -12,6 +13,8 @@ namespace core {
|
||||
|
||||
class Project {
|
||||
public:
|
||||
static constexpr int kClipFixedFrames = 600;
|
||||
|
||||
void setName(const QString& name) { m_name = name; }
|
||||
const QString& name() const { return m_name; }
|
||||
|
||||
@@ -37,10 +40,15 @@ public:
|
||||
void setFps(int fps) { m_fps = std::max(1, fps); }
|
||||
int fps() const { return m_fps; }
|
||||
|
||||
struct ToolKeyframeBool {
|
||||
int frame = 0;
|
||||
bool value = true;
|
||||
};
|
||||
|
||||
struct Entity {
|
||||
QString id;
|
||||
QString displayName; // 显示名(空则界面用 id)
|
||||
bool visible = true; // Outliner 眼睛:默认显示
|
||||
bool visible = true; // 默认显隐(无 visibilityKeys 时使用)
|
||||
// 可移动实体形状:存为局部坐标(相对 originWorld)
|
||||
QVector<QPointF> polygonLocal;
|
||||
// 从背景中抠洞的位置:固定在创建时的 world 坐标,不随实体移动
|
||||
@@ -55,6 +63,15 @@ public:
|
||||
// 抠图创建时该位置对应的原始距离乘子(0.5+depth01),用于校准:该处 distanceScale==1.0。0 表示未校准(兼容旧工程)
|
||||
double distanceScaleCalibMult = 0.0;
|
||||
|
||||
// 距离缩放开关:为 true 时实体不受 depth->distanceScale 影响,仅受 userScale 影响。
|
||||
// 约定:对话气泡等 UI 元素默认打开。
|
||||
bool ignoreDistanceScale = false;
|
||||
|
||||
// 父子关系:当 parentId 非空时,实体会保持相对父实体的偏移(world 坐标)。
|
||||
// parentOffsetWorld 表示「childOrigin - parentOrigin」在 world 中的偏移。
|
||||
QString parentId;
|
||||
QPointF parentOffsetWorld;
|
||||
|
||||
struct KeyframeVec2 {
|
||||
int frame = 0;
|
||||
QPointF value;
|
||||
@@ -82,12 +99,137 @@ public:
|
||||
QVector<KeyframeDouble> userScaleKeys;
|
||||
QVector<ImageFrame> imageFrames;
|
||||
|
||||
// 可见性轨道:布尔关键帧(显示/隐藏);渲染时会被解释为“10 帧淡入淡出”。
|
||||
QVector<ToolKeyframeBool> visibilityKeys;
|
||||
|
||||
EntityIntroContent intro;
|
||||
};
|
||||
|
||||
void setEntities(const QVector<Entity>& entities) { m_entities = entities; }
|
||||
const QVector<Entity>& entities() const { return m_entities; }
|
||||
|
||||
// —— 工具(精简版实体,不含 intro/图片/视频)——
|
||||
struct Tool {
|
||||
enum class Type { Bubble };
|
||||
|
||||
QString id;
|
||||
QString displayName;
|
||||
bool visible = true; // 编辑模式显隐
|
||||
|
||||
// 父子关系:同实体规则。parentId 可指向实体或工具的 id。
|
||||
QString parentId;
|
||||
QPointF parentOffsetWorld;
|
||||
|
||||
// 基准位置(无关键帧时使用)
|
||||
QPointF originWorld;
|
||||
QVector<Entity::KeyframeVec2> locationKeys;
|
||||
|
||||
// 可见性轨道:布尔关键帧(显示/隐藏);渲染时会被解释为“10 帧淡入淡出”。
|
||||
QVector<ToolKeyframeBool> visibilityKeys;
|
||||
|
||||
// 类型与 payload
|
||||
Type type = Type::Bubble;
|
||||
|
||||
// Bubble payload
|
||||
QString text;
|
||||
int fontPx = 18;
|
||||
enum class TextAlign { Left, Center, Right };
|
||||
TextAlign align = TextAlign::Center;
|
||||
// 气泡底边「平直段」上三角形附着位置:0=靠左,0.5=居中,1=靠右;主体会水平平移,使该点始终位于 originWorld 尖端正上方
|
||||
double bubblePointerT01 = 0.5;
|
||||
};
|
||||
|
||||
void setTools(const QVector<Tool>& tools) { m_tools = tools; }
|
||||
const QVector<Tool>& tools() const { return m_tools; }
|
||||
|
||||
// —— 动画系统(Blender/NLA 风格简化版,工程级)——
|
||||
struct AnimationClip {
|
||||
QString id;
|
||||
QString name;
|
||||
|
||||
// Entity channels (keyed by entity id)
|
||||
QHash<QString, QVector<Entity::KeyframeVec2>> entityLocationKeys;
|
||||
QHash<QString, QVector<Entity::KeyframeDouble>> entityUserScaleKeys;
|
||||
QHash<QString, QVector<Entity::ImageFrame>> entityImageFrames;
|
||||
QHash<QString, QVector<ToolKeyframeBool>> entityVisibilityKeys;
|
||||
|
||||
// Tool channels (keyed by tool id)
|
||||
QHash<QString, QVector<Entity::KeyframeVec2>> toolLocationKeys;
|
||||
QHash<QString, QVector<ToolKeyframeBool>> toolVisibilityKeys;
|
||||
};
|
||||
|
||||
struct NlaStrip {
|
||||
QString id;
|
||||
QString clipId;
|
||||
int startSlot = 0; // slot index; 1 slot = kClipFixedFrames frames
|
||||
int slotLen = 1; // currently fixed to 1; reserved for future
|
||||
bool enabled = true;
|
||||
bool muted = false;
|
||||
};
|
||||
|
||||
struct NlaTrack {
|
||||
QString id;
|
||||
QString name;
|
||||
bool muted = false;
|
||||
bool solo = false;
|
||||
QVector<NlaStrip> strips;
|
||||
};
|
||||
|
||||
struct AnimationScheme {
|
||||
QString id;
|
||||
QString name;
|
||||
QVector<NlaTrack> tracks;
|
||||
};
|
||||
|
||||
void setAnimationClips(const QVector<AnimationClip>& clips) { m_clips = clips; }
|
||||
const QVector<AnimationClip>& animationClips() const { return m_clips; }
|
||||
|
||||
void setAnimationSchemes(const QVector<AnimationScheme>& schemes) { m_schemes = schemes; }
|
||||
const QVector<AnimationScheme>& animationSchemes() const { return m_schemes; }
|
||||
|
||||
void setActiveSchemeId(const QString& id) { m_activeSchemeId = id; }
|
||||
const QString& activeSchemeId() const { return m_activeSchemeId; }
|
||||
|
||||
void setSelectedStripId(const QString& id) { m_selectedStripId = id; }
|
||||
const QString& selectedStripId() const { return m_selectedStripId; }
|
||||
|
||||
const AnimationScheme* findSchemeById(const QString& id) const {
|
||||
for (const auto& s : m_schemes) {
|
||||
if (s.id == id) return &s;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
AnimationScheme* findSchemeById(const QString& id) {
|
||||
for (auto& s : m_schemes) {
|
||||
if (s.id == id) return &s;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationClip* findClipById(const QString& id) const {
|
||||
for (const auto& c : m_clips) {
|
||||
if (c.id == id) return &c;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
AnimationClip* findClipById(const QString& id) {
|
||||
for (auto& c : m_clips) {
|
||||
if (c.id == id) return &c;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AnimationScheme* activeSchemeOrNull() const {
|
||||
const AnimationScheme* s = findSchemeById(m_activeSchemeId);
|
||||
if (s) return s;
|
||||
return m_schemes.isEmpty() ? nullptr : &m_schemes.front();
|
||||
}
|
||||
AnimationScheme* activeSchemeOrNull() {
|
||||
AnimationScheme* s = findSchemeById(m_activeSchemeId);
|
||||
if (s) return s;
|
||||
return m_schemes.isEmpty() ? nullptr : &m_schemes.front();
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_name;
|
||||
QString m_backgroundImagePath;
|
||||
@@ -98,6 +240,12 @@ private:
|
||||
int m_frameEnd = 600;
|
||||
int m_fps = 60;
|
||||
QVector<Entity> m_entities;
|
||||
QVector<Tool> m_tools;
|
||||
|
||||
QVector<AnimationClip> m_clips;
|
||||
QVector<AnimationScheme> m_schemes;
|
||||
QString m_activeSchemeId;
|
||||
QString m_selectedStripId;
|
||||
};
|
||||
|
||||
} // namespace core
|
||||
|
||||
311
client/core/eval/ProjectEvaluator.cpp
Normal file
311
client/core/eval/ProjectEvaluator.cpp
Normal file
@@ -0,0 +1,311 @@
|
||||
#include "eval/ProjectEvaluator.h"
|
||||
|
||||
#include "animation/AnimationSampling.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace core::eval {
|
||||
|
||||
namespace {
|
||||
|
||||
struct NodeRef {
|
||||
enum class Kind { Entity, Tool };
|
||||
Kind kind = Kind::Entity;
|
||||
int index = -1;
|
||||
};
|
||||
|
||||
QPointF sampledOriginForEntity(const core::Project::Entity& e,
|
||||
const core::Project::AnimationClip* clipOrNull,
|
||||
int localFrame) {
|
||||
if (clipOrNull && clipOrNull->entityLocationKeys.contains(e.id)) {
|
||||
const auto& keys = clipOrNull->entityLocationKeys.value(e.id);
|
||||
return core::sampleLocation(keys, localFrame, e.originWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
return core::sampleLocation(e.locationKeys, localFrame, e.originWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
|
||||
QPointF sampledOriginForTool(const core::Project::Tool& t,
|
||||
const core::Project::AnimationClip* clipOrNull,
|
||||
int localFrame) {
|
||||
if (clipOrNull && clipOrNull->toolLocationKeys.contains(t.id)) {
|
||||
const auto& keys = clipOrNull->toolLocationKeys.value(t.id);
|
||||
return core::sampleLocation(keys, localFrame, t.originWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
return core::sampleLocation(t.locationKeys, localFrame, t.originWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
|
||||
struct VisKey {
|
||||
int frame = 0;
|
||||
bool value = true;
|
||||
};
|
||||
|
||||
static QVector<VisKey> normalizeVisibilityKeys(const QVector<core::Project::ToolKeyframeBool>& keys) {
|
||||
QVector<VisKey> out;
|
||||
out.reserve(keys.size());
|
||||
for (const auto& k : keys) {
|
||||
out.push_back(VisKey{k.frame, k.value});
|
||||
}
|
||||
std::sort(out.begin(), out.end(), [](const VisKey& a, const VisKey& b) { return a.frame < b.frame; });
|
||||
// 若同帧重复,保留最后一个
|
||||
QVector<VisKey> dedup;
|
||||
dedup.reserve(out.size());
|
||||
for (const auto& k : out) {
|
||||
if (!dedup.isEmpty() && dedup.last().frame == k.frame) {
|
||||
dedup.last() = k;
|
||||
} else {
|
||||
dedup.push_back(k);
|
||||
}
|
||||
}
|
||||
return dedup;
|
||||
}
|
||||
|
||||
double opacityFromBoolKeys(const QVector<core::Project::ToolKeyframeBool>& keysRaw, int frame, int fadeFrames) {
|
||||
const int nFade = std::max(1, fadeFrames);
|
||||
const QVector<VisKey> keys = normalizeVisibilityKeys(keysRaw);
|
||||
if (keys.isEmpty()) {
|
||||
return 1.0;
|
||||
}
|
||||
// 规则:在发生状态变化的关键帧 t 附近做对称淡变
|
||||
// fadeFrames=10 -> 约 [t-5, t+5] 渐变,符合“60 帧切换则 55~65 过渡”
|
||||
const double half = double(nFade) * 0.5;
|
||||
|
||||
// first key 之前,直接采用 first value(不做凭空反向切换)
|
||||
if (frame <= keys.front().frame) {
|
||||
return keys.front().value ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
bool state = keys.front().value;
|
||||
for (int i = 1; i < keys.size(); ++i) {
|
||||
const bool prev = keys[i - 1].value;
|
||||
const bool cur = keys[i].value;
|
||||
const int t = keys[i].frame;
|
||||
if (prev == cur) {
|
||||
// 状态未变:忽略该 key
|
||||
continue;
|
||||
}
|
||||
const double a = double(t) - half;
|
||||
const double b = double(t) + half;
|
||||
if (double(frame) < a) {
|
||||
// 还没进入该次过渡
|
||||
return state ? 1.0 : 0.0;
|
||||
}
|
||||
if (double(frame) <= b) {
|
||||
const double u = std::clamp((double(frame) - a) / std::max(1e-9, b - a), 0.0, 1.0);
|
||||
const double x = prev ? 1.0 : 0.0;
|
||||
const double y = cur ? 1.0 : 0.0;
|
||||
return x + (y - x) * u;
|
||||
}
|
||||
// 该次过渡已结束,进入新状态
|
||||
state = cur;
|
||||
}
|
||||
return state ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
struct StripEvalCtx {
|
||||
const core::Project::AnimationScheme* scheme = nullptr;
|
||||
const core::Project::NlaStrip* strip = nullptr;
|
||||
const core::Project::AnimationClip* clip = nullptr;
|
||||
int slot = 0;
|
||||
int localFrame = 0; // 0..kClipFixedFrames-1
|
||||
};
|
||||
|
||||
static const core::Project::NlaStrip* findStripById(const core::Project::AnimationScheme& scheme, const QString& id) {
|
||||
if (id.isEmpty()) return nullptr;
|
||||
for (const auto& tr : scheme.tracks) {
|
||||
for (const auto& st : tr.strips) {
|
||||
if (st.id == id) return &st;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool trackIsEffectivelyMuted(const core::Project::AnimationScheme& scheme, const core::Project::NlaTrack& t) {
|
||||
// 若有任意 solo=true,则只有 solo 的 track 生效(且仍受自身 muted 控制)
|
||||
bool anySolo = false;
|
||||
for (const auto& tr : scheme.tracks) {
|
||||
if (tr.solo) {
|
||||
anySolo = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (anySolo && !t.solo) {
|
||||
return true;
|
||||
}
|
||||
return t.muted;
|
||||
}
|
||||
|
||||
static const core::Project::NlaStrip* pickStripAtSlot(const core::Project::AnimationScheme& scheme, int slot) {
|
||||
const core::Project::NlaStrip* chosen = nullptr;
|
||||
for (const auto& tr : scheme.tracks) {
|
||||
if (trackIsEffectivelyMuted(scheme, tr)) continue;
|
||||
for (const auto& st : tr.strips) {
|
||||
if (!st.enabled || st.muted) continue;
|
||||
const int a = st.startSlot;
|
||||
const int b = st.startSlot + std::max(1, st.slotLen);
|
||||
if (slot >= a && slot < b) {
|
||||
chosen = &st; // 轨道顺序靠后的覆盖靠前的(更接近“上层”)
|
||||
}
|
||||
}
|
||||
}
|
||||
return chosen;
|
||||
}
|
||||
|
||||
static StripEvalCtx resolveStripCtx(const core::Project& project, int globalFrame) {
|
||||
StripEvalCtx ctx;
|
||||
const auto* scheme = project.activeSchemeOrNull();
|
||||
if (!scheme) {
|
||||
ctx.localFrame = std::max(0, globalFrame);
|
||||
return ctx;
|
||||
}
|
||||
ctx.scheme = scheme;
|
||||
const int g = std::max(0, globalFrame);
|
||||
ctx.slot = g / core::Project::kClipFixedFrames;
|
||||
ctx.localFrame = g % core::Project::kClipFixedFrames;
|
||||
|
||||
const core::Project::NlaStrip* st = findStripById(*scheme, project.selectedStripId());
|
||||
// 若选中条带不覆盖当前 slot,则退回自动挑选
|
||||
if (!st || ctx.slot < st->startSlot || ctx.slot >= (st->startSlot + std::max(1, st->slotLen)) || !st->enabled || st->muted) {
|
||||
st = pickStripAtSlot(*scheme, ctx.slot);
|
||||
}
|
||||
ctx.strip = st;
|
||||
if (st) {
|
||||
ctx.clip = project.findClipById(st->clipId);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, int fadeFrames) {
|
||||
ResolvedProjectFrame out;
|
||||
const auto& ents = project.entities();
|
||||
const auto& tools = project.tools();
|
||||
out.entities.reserve(ents.size());
|
||||
out.tools.reserve(tools.size());
|
||||
|
||||
const StripEvalCtx ctx = resolveStripCtx(project, frame);
|
||||
const int localFrame = ctx.localFrame;
|
||||
const core::Project::AnimationClip* clip = ctx.clip;
|
||||
|
||||
QHash<QString, NodeRef> index;
|
||||
index.reserve(ents.size() + tools.size());
|
||||
for (int i = 0; i < ents.size(); ++i) {
|
||||
if (!ents[i].id.isEmpty()) {
|
||||
index.insert(ents[i].id, NodeRef{NodeRef::Kind::Entity, i});
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < tools.size(); ++i) {
|
||||
if (!tools[i].id.isEmpty() && !index.contains(tools[i].id)) {
|
||||
index.insert(tools[i].id, NodeRef{NodeRef::Kind::Tool, i});
|
||||
}
|
||||
}
|
||||
|
||||
QHash<QString, QPointF> resolvedOrigin;
|
||||
QHash<QString, bool> resolving;
|
||||
resolvedOrigin.reserve(index.size());
|
||||
resolving.reserve(index.size());
|
||||
|
||||
std::function<QPointF(const QString&)> resolve = [&](const QString& id) -> QPointF {
|
||||
if (resolvedOrigin.contains(id)) {
|
||||
return resolvedOrigin.value(id);
|
||||
}
|
||||
if (!index.contains(id)) {
|
||||
resolvedOrigin.insert(id, QPointF());
|
||||
return QPointF();
|
||||
}
|
||||
if (resolving.value(id, false)) {
|
||||
// cycle:降级为自身采样 origin
|
||||
const NodeRef r = index.value(id);
|
||||
QPointF o;
|
||||
if (r.kind == NodeRef::Kind::Entity) o = sampledOriginForEntity(ents[r.index], clip, localFrame);
|
||||
else o = sampledOriginForTool(tools[r.index], clip, localFrame);
|
||||
resolvedOrigin.insert(id, o);
|
||||
return o;
|
||||
}
|
||||
resolving.insert(id, true);
|
||||
|
||||
const NodeRef r = index.value(id);
|
||||
QString parentId;
|
||||
QPointF off;
|
||||
QPointF selfSampled;
|
||||
if (r.kind == NodeRef::Kind::Entity) {
|
||||
const auto& e = ents[r.index];
|
||||
parentId = e.parentId;
|
||||
off = e.parentOffsetWorld;
|
||||
selfSampled = sampledOriginForEntity(e, clip, localFrame);
|
||||
} else {
|
||||
const auto& t = tools[r.index];
|
||||
parentId = t.parentId;
|
||||
off = t.parentOffsetWorld;
|
||||
selfSampled = sampledOriginForTool(t, clip, localFrame);
|
||||
}
|
||||
|
||||
QPointF outO = selfSampled;
|
||||
if (!parentId.isEmpty() && index.contains(parentId)) {
|
||||
const QPointF po = resolve(parentId);
|
||||
outO = po + off;
|
||||
}
|
||||
|
||||
resolving.insert(id, false);
|
||||
resolvedOrigin.insert(id, outO);
|
||||
return outO;
|
||||
};
|
||||
|
||||
auto opacityWithDefault = [&](const QVector<core::Project::ToolKeyframeBool>& keys,
|
||||
bool defaultVisible) -> double {
|
||||
if (keys.isEmpty()) {
|
||||
return defaultVisible ? 1.0 : 0.0;
|
||||
}
|
||||
return opacityFromBoolKeys(keys, localFrame, fadeFrames);
|
||||
};
|
||||
|
||||
// Entities:resolved origin + opacity(可见性轨道)
|
||||
for (int i = 0; i < ents.size(); ++i) {
|
||||
core::Project::Entity e = ents[i];
|
||||
const QPointF base = e.originWorld;
|
||||
const QPointF ro = (!e.id.isEmpty()) ? resolve(e.id) : sampledOriginForEntity(e, clip, localFrame);
|
||||
const QPointF delta = ro - base;
|
||||
e.originWorld = ro;
|
||||
e.imageTopLeftWorld += delta;
|
||||
|
||||
// Clip channels: userScale / imagePath(迁移后仍能逐帧显示)
|
||||
if (clip && clip->entityUserScaleKeys.contains(e.id)) {
|
||||
const auto& keys = clip->entityUserScaleKeys.value(e.id);
|
||||
e.userScale = core::sampleUserScale(keys, localFrame, e.userScale, core::KeyInterpolation::Linear);
|
||||
}
|
||||
if (clip && clip->entityImageFrames.contains(e.id)) {
|
||||
const auto& frames = clip->entityImageFrames.value(e.id);
|
||||
e.imagePath = core::sampleImagePath(frames, localFrame, e.imagePath);
|
||||
}
|
||||
|
||||
QVector<core::Project::ToolKeyframeBool> visKeys = e.visibilityKeys;
|
||||
if (clip && clip->entityVisibilityKeys.contains(e.id)) {
|
||||
visKeys = clip->entityVisibilityKeys.value(e.id);
|
||||
}
|
||||
const double op = opacityWithDefault(visKeys, e.visible);
|
||||
out.entities.push_back(ResolvedEntity{e, op});
|
||||
}
|
||||
|
||||
// Tools:resolved origin + opacity(可见性轨道)
|
||||
for (int i = 0; i < tools.size(); ++i) {
|
||||
core::Project::Tool t = tools[i];
|
||||
const QPointF base = t.originWorld;
|
||||
const QPointF ro = (!t.id.isEmpty()) ? resolve(t.id) : sampledOriginForTool(t, clip, localFrame);
|
||||
const QPointF delta = ro - base;
|
||||
t.originWorld = ro;
|
||||
// parentOffsetWorld 已包含相对关系,不在这里改
|
||||
QVector<core::Project::ToolKeyframeBool> visKeys = t.visibilityKeys;
|
||||
if (clip && clip->toolVisibilityKeys.contains(t.id)) {
|
||||
visKeys = clip->toolVisibilityKeys.value(t.id);
|
||||
}
|
||||
const double op = opacityWithDefault(visKeys, t.visible);
|
||||
(void)delta;
|
||||
out.tools.push_back(ResolvedTool{t, op});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace core::eval
|
||||
|
||||
31
client/core/eval/ProjectEvaluator.h
Normal file
31
client/core/eval/ProjectEvaluator.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "domain/Project.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QPointF>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
namespace core::eval {
|
||||
|
||||
struct ResolvedEntity {
|
||||
core::Project::Entity entity; // 拷贝:originWorld/imageTopLeftWorld 已替换为求值后的值
|
||||
double opacity = 1.0; // 0..1
|
||||
};
|
||||
|
||||
struct ResolvedTool {
|
||||
core::Project::Tool tool; // 拷贝:originWorld 已替换为求值后的值
|
||||
double opacity = 1.0; // 0..1
|
||||
};
|
||||
|
||||
struct ResolvedProjectFrame {
|
||||
QVector<ResolvedEntity> entities;
|
||||
QVector<ResolvedTool> tools;
|
||||
};
|
||||
|
||||
/// 逐帧求值:处理父子跟随与工具可见性淡入淡出。
|
||||
ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, int fadeFrames = 10);
|
||||
|
||||
} // namespace core::eval
|
||||
|
||||
305
client/core/library/EntityJson.cpp
Normal file
305
client/core/library/EntityJson.cpp
Normal file
@@ -0,0 +1,305 @@
|
||||
#include "library/EntityJson.h"
|
||||
|
||||
#include <functional>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
|
||||
namespace core::library {
|
||||
|
||||
namespace {
|
||||
|
||||
QJsonArray pointToJson(const QPointF& p) {
|
||||
return QJsonArray{p.x(), p.y()};
|
||||
}
|
||||
|
||||
bool pointFromJson(const QJsonValue& v, QPointF& out) {
|
||||
if (!v.isArray()) {
|
||||
return false;
|
||||
}
|
||||
const QJsonArray a = v.toArray();
|
||||
if (a.size() < 2) {
|
||||
return false;
|
||||
}
|
||||
out = QPointF(a.at(0).toDouble(), a.at(1).toDouble());
|
||||
return true;
|
||||
}
|
||||
|
||||
QJsonArray pointsToJson(const QVector<QPointF>& pts) {
|
||||
QJsonArray a;
|
||||
for (const auto& p : pts) {
|
||||
a.append(pointToJson(p));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
bool pointsFromJson(const QJsonValue& v, QVector<QPointF>& out) {
|
||||
out.clear();
|
||||
if (!v.isArray()) {
|
||||
return false;
|
||||
}
|
||||
const QJsonArray a = v.toArray();
|
||||
out.reserve(a.size());
|
||||
for (const auto& it : a) {
|
||||
QPointF p;
|
||||
if (!pointFromJson(it, p)) {
|
||||
return false;
|
||||
}
|
||||
out.push_back(p);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
QJsonArray vecToJson(const QVector<T>& v, const std::function<QJsonObject(const T&)>& fn) {
|
||||
QJsonArray a;
|
||||
for (const auto& x : v) {
|
||||
a.append(fn(x));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
QJsonObject entityToJson(const core::Project::Entity& e) {
|
||||
QJsonObject o;
|
||||
o.insert(QStringLiteral("id"), e.id);
|
||||
o.insert(QStringLiteral("displayName"), e.displayName);
|
||||
o.insert(QStringLiteral("visible"), e.visible);
|
||||
o.insert(QStringLiteral("polygonLocal"), pointsToJson(e.polygonLocal));
|
||||
o.insert(QStringLiteral("cutoutPolygonWorld"), pointsToJson(e.cutoutPolygonWorld));
|
||||
o.insert(QStringLiteral("originWorld"), pointToJson(e.originWorld));
|
||||
o.insert(QStringLiteral("depth"), e.depth);
|
||||
o.insert(QStringLiteral("imagePath"), e.imagePath);
|
||||
o.insert(QStringLiteral("imageTopLeftWorld"), pointToJson(e.imageTopLeftWorld));
|
||||
o.insert(QStringLiteral("userScale"), e.userScale);
|
||||
o.insert(QStringLiteral("distanceScaleCalibMult"), e.distanceScaleCalibMult);
|
||||
o.insert(QStringLiteral("ignoreDistanceScale"), e.ignoreDistanceScale);
|
||||
o.insert(QStringLiteral("parentId"), e.parentId);
|
||||
o.insert(QStringLiteral("parentOffsetWorld"), pointToJson(e.parentOffsetWorld));
|
||||
|
||||
o.insert(QStringLiteral("entityPayloadPath"), e.entityPayloadPath);
|
||||
o.insert(QStringLiteral("legacyAnimSidecarPath"), e.legacyAnimSidecarPath);
|
||||
|
||||
o.insert(QStringLiteral("locationKeys"),
|
||||
vecToJson<core::Project::Entity::KeyframeVec2>(
|
||||
e.locationKeys, [](const core::Project::Entity::KeyframeVec2& k) {
|
||||
QJsonObject ko;
|
||||
ko.insert(QStringLiteral("frame"), k.frame);
|
||||
ko.insert(QStringLiteral("value"), pointToJson(k.value));
|
||||
return ko;
|
||||
}));
|
||||
|
||||
o.insert(QStringLiteral("depthScaleKeys"),
|
||||
vecToJson<core::Project::Entity::KeyframeFloat01>(
|
||||
e.depthScaleKeys, [](const core::Project::Entity::KeyframeFloat01& k) {
|
||||
QJsonObject ko;
|
||||
ko.insert(QStringLiteral("frame"), k.frame);
|
||||
ko.insert(QStringLiteral("value"), k.value);
|
||||
return ko;
|
||||
}));
|
||||
|
||||
o.insert(QStringLiteral("userScaleKeys"),
|
||||
vecToJson<core::Project::Entity::KeyframeDouble>(
|
||||
e.userScaleKeys, [](const core::Project::Entity::KeyframeDouble& k) {
|
||||
QJsonObject ko;
|
||||
ko.insert(QStringLiteral("frame"), k.frame);
|
||||
ko.insert(QStringLiteral("value"), k.value);
|
||||
return ko;
|
||||
}));
|
||||
|
||||
o.insert(QStringLiteral("imageFrames"),
|
||||
vecToJson<core::Project::Entity::ImageFrame>(
|
||||
e.imageFrames, [](const core::Project::Entity::ImageFrame& k) {
|
||||
QJsonObject ko;
|
||||
ko.insert(QStringLiteral("frame"), k.frame);
|
||||
ko.insert(QStringLiteral("imagePath"), k.imagePath);
|
||||
return ko;
|
||||
}));
|
||||
|
||||
o.insert(QStringLiteral("visibilityKeys"),
|
||||
vecToJson<core::Project::ToolKeyframeBool>(
|
||||
e.visibilityKeys, [](const core::Project::ToolKeyframeBool& k) {
|
||||
QJsonObject ko;
|
||||
ko.insert(QStringLiteral("frame"), k.frame);
|
||||
ko.insert(QStringLiteral("value"), k.value);
|
||||
return ko;
|
||||
}));
|
||||
|
||||
{
|
||||
QJsonObject intro;
|
||||
intro.insert(QStringLiteral("title"), e.intro.title);
|
||||
intro.insert(QStringLiteral("bodyText"), e.intro.bodyText);
|
||||
QJsonArray imgs;
|
||||
for (const auto& p : e.intro.imagePathsRelative) {
|
||||
imgs.append(p);
|
||||
}
|
||||
intro.insert(QStringLiteral("imagePathsRelative"), imgs);
|
||||
intro.insert(QStringLiteral("videoPathRelative"), e.intro.videoPathRelative);
|
||||
o.insert(QStringLiteral("intro"), intro);
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
bool entityFromJson(const QJsonObject& o, core::Project::Entity& out) {
|
||||
core::Project::Entity e;
|
||||
e.id = o.value(QStringLiteral("id")).toString();
|
||||
e.displayName = o.value(QStringLiteral("displayName")).toString();
|
||||
e.visible = o.value(QStringLiteral("visible")).toBool(true);
|
||||
if (!pointsFromJson(o.value(QStringLiteral("polygonLocal")), e.polygonLocal)) {
|
||||
return false;
|
||||
}
|
||||
if (!pointsFromJson(o.value(QStringLiteral("cutoutPolygonWorld")), e.cutoutPolygonWorld)) {
|
||||
// cutout 允许不存在:按空处理
|
||||
e.cutoutPolygonWorld.clear();
|
||||
}
|
||||
{
|
||||
QPointF p;
|
||||
if (!pointFromJson(o.value(QStringLiteral("originWorld")), p)) {
|
||||
p = QPointF();
|
||||
}
|
||||
e.originWorld = p;
|
||||
}
|
||||
e.depth = o.value(QStringLiteral("depth")).toInt(0);
|
||||
e.imagePath = o.value(QStringLiteral("imagePath")).toString();
|
||||
{
|
||||
QPointF p;
|
||||
if (!pointFromJson(o.value(QStringLiteral("imageTopLeftWorld")), p)) {
|
||||
p = QPointF();
|
||||
}
|
||||
e.imageTopLeftWorld = p;
|
||||
}
|
||||
e.userScale = o.value(QStringLiteral("userScale")).toDouble(1.0);
|
||||
e.distanceScaleCalibMult = o.value(QStringLiteral("distanceScaleCalibMult")).toDouble(0.0);
|
||||
e.ignoreDistanceScale = o.value(QStringLiteral("ignoreDistanceScale")).toBool(false);
|
||||
e.parentId = o.value(QStringLiteral("parentId")).toString();
|
||||
{
|
||||
QPointF p;
|
||||
if (!pointFromJson(o.value(QStringLiteral("parentOffsetWorld")), p)) {
|
||||
p = QPointF();
|
||||
}
|
||||
e.parentOffsetWorld = p;
|
||||
}
|
||||
e.entityPayloadPath = o.value(QStringLiteral("entityPayloadPath")).toString();
|
||||
e.legacyAnimSidecarPath = o.value(QStringLiteral("legacyAnimSidecarPath")).toString();
|
||||
|
||||
auto parseKeyframesVec2 = [&](const QString& key, QVector<core::Project::Entity::KeyframeVec2>& dst) -> bool {
|
||||
dst.clear();
|
||||
const QJsonValue v = o.value(key);
|
||||
if (!v.isArray()) {
|
||||
return true;
|
||||
}
|
||||
const QJsonArray a = v.toArray();
|
||||
dst.reserve(a.size());
|
||||
for (const auto& it : a) {
|
||||
if (!it.isObject()) return false;
|
||||
const QJsonObject ko = it.toObject();
|
||||
core::Project::Entity::KeyframeVec2 k;
|
||||
k.frame = ko.value(QStringLiteral("frame")).toInt(0);
|
||||
QPointF pv;
|
||||
if (!pointFromJson(ko.value(QStringLiteral("value")), pv)) {
|
||||
return false;
|
||||
}
|
||||
k.value = pv;
|
||||
dst.push_back(k);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
auto parseKeyframesFloat01 = [&](const QString& key, QVector<core::Project::Entity::KeyframeFloat01>& dst) -> bool {
|
||||
dst.clear();
|
||||
const QJsonValue v = o.value(key);
|
||||
if (!v.isArray()) {
|
||||
return true;
|
||||
}
|
||||
const QJsonArray a = v.toArray();
|
||||
dst.reserve(a.size());
|
||||
for (const auto& it : a) {
|
||||
if (!it.isObject()) return false;
|
||||
const QJsonObject ko = it.toObject();
|
||||
core::Project::Entity::KeyframeFloat01 k;
|
||||
k.frame = ko.value(QStringLiteral("frame")).toInt(0);
|
||||
k.value = ko.value(QStringLiteral("value")).toDouble(0.5);
|
||||
dst.push_back(k);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
auto parseKeyframesDouble = [&](const QString& key, QVector<core::Project::Entity::KeyframeDouble>& dst) -> bool {
|
||||
dst.clear();
|
||||
const QJsonValue v = o.value(key);
|
||||
if (!v.isArray()) {
|
||||
return true;
|
||||
}
|
||||
const QJsonArray a = v.toArray();
|
||||
dst.reserve(a.size());
|
||||
for (const auto& it : a) {
|
||||
if (!it.isObject()) return false;
|
||||
const QJsonObject ko = it.toObject();
|
||||
core::Project::Entity::KeyframeDouble k;
|
||||
k.frame = ko.value(QStringLiteral("frame")).toInt(0);
|
||||
k.value = ko.value(QStringLiteral("value")).toDouble(1.0);
|
||||
dst.push_back(k);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
auto parseImageFrames = [&](const QString& key, QVector<core::Project::Entity::ImageFrame>& dst) -> bool {
|
||||
dst.clear();
|
||||
const QJsonValue v = o.value(key);
|
||||
if (!v.isArray()) {
|
||||
return true;
|
||||
}
|
||||
const QJsonArray a = v.toArray();
|
||||
dst.reserve(a.size());
|
||||
for (const auto& it : a) {
|
||||
if (!it.isObject()) return false;
|
||||
const QJsonObject ko = it.toObject();
|
||||
core::Project::Entity::ImageFrame k;
|
||||
k.frame = ko.value(QStringLiteral("frame")).toInt(0);
|
||||
k.imagePath = ko.value(QStringLiteral("imagePath")).toString();
|
||||
dst.push_back(k);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!parseKeyframesVec2(QStringLiteral("locationKeys"), e.locationKeys)) return false;
|
||||
if (!parseKeyframesFloat01(QStringLiteral("depthScaleKeys"), e.depthScaleKeys)) return false;
|
||||
if (!parseKeyframesDouble(QStringLiteral("userScaleKeys"), e.userScaleKeys)) return false;
|
||||
if (!parseImageFrames(QStringLiteral("imageFrames"), e.imageFrames)) return false;
|
||||
|
||||
// visibilityKeys:可缺省(默认永远可见)
|
||||
e.visibilityKeys.clear();
|
||||
if (o.value(QStringLiteral("visibilityKeys")).isArray()) {
|
||||
const QJsonArray a = o.value(QStringLiteral("visibilityKeys")).toArray();
|
||||
e.visibilityKeys.reserve(a.size());
|
||||
for (const auto& it : a) {
|
||||
if (!it.isObject()) return false;
|
||||
const QJsonObject ko = it.toObject();
|
||||
core::Project::ToolKeyframeBool k;
|
||||
k.frame = ko.value(QStringLiteral("frame")).toInt(0);
|
||||
k.value = ko.value(QStringLiteral("value")).toBool(true);
|
||||
e.visibilityKeys.push_back(k);
|
||||
}
|
||||
}
|
||||
|
||||
if (o.contains(QStringLiteral("intro")) && o.value(QStringLiteral("intro")).isObject()) {
|
||||
const QJsonObject intro = o.value(QStringLiteral("intro")).toObject();
|
||||
e.intro.title = intro.value(QStringLiteral("title")).toString();
|
||||
e.intro.bodyText = intro.value(QStringLiteral("bodyText")).toString();
|
||||
e.intro.videoPathRelative = intro.value(QStringLiteral("videoPathRelative")).toString();
|
||||
e.intro.imagePathsRelative.clear();
|
||||
if (intro.value(QStringLiteral("imagePathsRelative")).isArray()) {
|
||||
const QJsonArray imgs = intro.value(QStringLiteral("imagePathsRelative")).toArray();
|
||||
e.intro.imagePathsRelative.reserve(imgs.size());
|
||||
for (const auto& iv : imgs) {
|
||||
e.intro.imagePathsRelative.push_back(iv.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out = e;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace core::library
|
||||
|
||||
13
client/core/library/EntityJson.h
Normal file
13
client/core/library/EntityJson.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "domain/Project.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace core::library {
|
||||
|
||||
QJsonObject entityToJson(const core::Project::Entity& e);
|
||||
bool entityFromJson(const QJsonObject& o, core::Project::Entity& out);
|
||||
|
||||
} // namespace core::library
|
||||
|
||||
19
client/core/library/OnlineResourceLibraryProvider.cpp
Normal file
19
client/core/library/OnlineResourceLibraryProvider.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "library/OnlineResourceLibraryProvider.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace core::library {
|
||||
|
||||
OnlineResourceLibraryProvider::OnlineResourceLibraryProvider(QObject* parent)
|
||||
: ResourceLibraryProvider(parent) {}
|
||||
|
||||
void OnlineResourceLibraryProvider::fetchResourcesAsync() {
|
||||
// 在线资源预留:当前不返回假数据。
|
||||
// 未来接入真实服务时保持“多次调用 fetchResourcesAsync()”用于分页即可。
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
emit resourcesReady({});
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace core::library
|
||||
|
||||
16
client/core/library/OnlineResourceLibraryProvider.h
Normal file
16
client/core/library/OnlineResourceLibraryProvider.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "library/ResourceLibraryProvider.h"
|
||||
|
||||
namespace core::library {
|
||||
|
||||
/// 在线资源库(预留):当前不做真实网络请求,仅提供接口占位。
|
||||
class OnlineResourceLibraryProvider final : public ResourceLibraryProvider {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OnlineResourceLibraryProvider(QObject* parent = nullptr);
|
||||
void fetchResourcesAsync() override;
|
||||
};
|
||||
|
||||
} // namespace core::library
|
||||
|
||||
95
client/core/library/ResourceLibraryProvider.cpp
Normal file
95
client/core/library/ResourceLibraryProvider.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include "library/ResourceLibraryProvider.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
namespace core::library {
|
||||
|
||||
FakeResourceLibraryProvider::FakeResourceLibraryProvider(QObject* parent)
|
||||
: ResourceLibraryProvider(parent) {}
|
||||
|
||||
void FakeResourceLibraryProvider::fetchResourcesAsync() {
|
||||
// 预留:未来在这里用 QNetworkAccessManager 请求在线资源库。
|
||||
// 当前返回内置假数据,并保持异步语义,避免 UI 假设同步返回。
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
QVector<LibraryResource> out;
|
||||
|
||||
{
|
||||
LibraryResource r;
|
||||
r.resourceId = QStringLiteral("demo/tree");
|
||||
r.displayName = QStringLiteral("树(示例资源)");
|
||||
r.kind = LibraryResource::Kind::Entity;
|
||||
r.imageSize = QSize(220, 260);
|
||||
r.accent = QColor(46, 204, 113);
|
||||
r.imageOffsetFromOrigin = QPointF(-110, -130);
|
||||
r.entityTemplate.displayName = QStringLiteral("树");
|
||||
r.entityTemplate.visible = true;
|
||||
r.entityTemplate.depth = 160;
|
||||
r.entityTemplate.userScale = 1.0;
|
||||
r.entityTemplate.distanceScaleCalibMult = 0.0;
|
||||
// 以 origin 为中心的简单多边形(局部坐标)
|
||||
r.entityTemplate.polygonLocal = {
|
||||
QPointF(-60, 80), QPointF(-90, 20), QPointF(-60, -80), QPointF(0, -110),
|
||||
QPointF(60, -80), QPointF(90, 20), QPointF(60, 80)};
|
||||
out.push_back(r);
|
||||
}
|
||||
|
||||
{
|
||||
LibraryResource r;
|
||||
r.resourceId = QStringLiteral("demo/rock");
|
||||
r.displayName = QStringLiteral("岩石(示例资源)");
|
||||
r.kind = LibraryResource::Kind::Entity;
|
||||
r.imageSize = QSize(240, 180);
|
||||
r.accent = QColor(120, 120, 120);
|
||||
r.imageOffsetFromOrigin = QPointF(-120, -90);
|
||||
r.entityTemplate.displayName = QStringLiteral("岩石");
|
||||
r.entityTemplate.visible = true;
|
||||
r.entityTemplate.depth = 90;
|
||||
r.entityTemplate.userScale = 1.0;
|
||||
r.entityTemplate.distanceScaleCalibMult = 0.0;
|
||||
r.entityTemplate.polygonLocal = {
|
||||
QPointF(-100, 10), QPointF(-70, -60), QPointF(0, -80), QPointF(90, -40),
|
||||
QPointF(110, 20), QPointF(40, 70), QPointF(-40, 60)};
|
||||
out.push_back(r);
|
||||
}
|
||||
|
||||
{
|
||||
LibraryResource r;
|
||||
r.resourceId = QStringLiteral("demo/house");
|
||||
r.displayName = QStringLiteral("小屋(示例资源)");
|
||||
r.kind = LibraryResource::Kind::Entity;
|
||||
r.imageSize = QSize(280, 220);
|
||||
r.accent = QColor(231, 76, 60);
|
||||
r.imageOffsetFromOrigin = QPointF(-140, -110);
|
||||
r.entityTemplate.displayName = QStringLiteral("小屋");
|
||||
r.entityTemplate.visible = true;
|
||||
r.entityTemplate.depth = 200;
|
||||
r.entityTemplate.userScale = 1.0;
|
||||
r.entityTemplate.distanceScaleCalibMult = 0.0;
|
||||
r.entityTemplate.polygonLocal = {
|
||||
QPointF(-120, 90), QPointF(120, 90), QPointF(120, -10), QPointF(0, -120), QPointF(-120, -10)};
|
||||
out.push_back(r);
|
||||
}
|
||||
|
||||
// —— 对话气泡:工具资源;底边三角形位置在属性里用滑块自调 ——
|
||||
{
|
||||
LibraryResource r;
|
||||
r.resourceId = QStringLiteral("local/bubble");
|
||||
r.displayName = QStringLiteral("对话气泡");
|
||||
r.kind = LibraryResource::Kind::Tool;
|
||||
r.imageSize = QSize(260, 160);
|
||||
r.accent = QColor(120, 150, 255);
|
||||
r.imageOffsetFromOrigin = QPointF(-130, -140);
|
||||
r.toolTemplate.displayName = QStringLiteral("对话气泡");
|
||||
r.toolTemplate.visible = true;
|
||||
r.toolTemplate.type = core::Project::Tool::Type::Bubble;
|
||||
r.toolTemplate.text = QStringLiteral("……");
|
||||
r.toolTemplate.bubblePointerT01 = 0.5;
|
||||
out.push_back(r);
|
||||
}
|
||||
|
||||
emit resourcesReady(out);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace core::library
|
||||
|
||||
59
client/core/library/ResourceLibraryProvider.h
Normal file
59
client/core/library/ResourceLibraryProvider.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "domain/Project.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QImage>
|
||||
#include <QObject>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
namespace core::library {
|
||||
|
||||
/// 资源库中的一个“可拖拽实体资源”。未来可来自在线服务器。
|
||||
struct LibraryResource {
|
||||
enum class Kind { Entity, Tool };
|
||||
QString resourceId;
|
||||
QString displayName;
|
||||
|
||||
Kind kind = Kind::Entity;
|
||||
|
||||
/// 拖到画布后用于创建实体的模板(id 可为空,主窗口会自动分配 entity-<n>)。
|
||||
core::Project::Entity entityTemplate;
|
||||
|
||||
/// 拖到画布后用于创建工具的模板(id 可为空,主窗口会自动分配 tool-<n>)。
|
||||
core::Project::Tool toolTemplate;
|
||||
|
||||
/// 资源预览/占位贴图生成参数(当前无真实来源时用)。
|
||||
QSize imageSize = QSize(256, 256);
|
||||
QColor accent = QColor(80, 160, 255);
|
||||
|
||||
/// 贴图相对 originWorld 的偏移(world 坐标;落到画布后会做 originWorld + offset)。
|
||||
QPointF imageOffsetFromOrigin = QPointF(-128, -128);
|
||||
};
|
||||
|
||||
/// 资源库提供者接口:可用假实现占位,后续接入在线服务时替换。
|
||||
class ResourceLibraryProvider : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ResourceLibraryProvider(QObject* parent = nullptr) : QObject(parent) {}
|
||||
~ResourceLibraryProvider() override = default;
|
||||
|
||||
virtual void fetchResourcesAsync() = 0;
|
||||
|
||||
signals:
|
||||
void resourcesReady(const QVector<core::library::LibraryResource>& resources);
|
||||
void resourcesFailed(const QString& error);
|
||||
};
|
||||
|
||||
/// 假资源提供者:返回内置的若干资源,预留接口用于未来在线获取。
|
||||
class FakeResourceLibraryProvider final : public ResourceLibraryProvider {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FakeResourceLibraryProvider(QObject* parent = nullptr);
|
||||
void fetchResourcesAsync() override;
|
||||
};
|
||||
|
||||
} // namespace core::library
|
||||
|
||||
113
client/core/library/ToolJson.cpp
Normal file
113
client/core/library/ToolJson.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include "library/ToolJson.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace core::library {
|
||||
|
||||
static QJsonArray pointToJson(const QPointF& p) {
|
||||
QJsonArray a;
|
||||
a.append(p.x());
|
||||
a.append(p.y());
|
||||
return a;
|
||||
}
|
||||
|
||||
static QPointF pointFromJson(const QJsonValue& v) {
|
||||
const QJsonArray a = v.toArray();
|
||||
if (a.size() >= 2) {
|
||||
return QPointF(a.at(0).toDouble(0.0), a.at(1).toDouble(0.0));
|
||||
}
|
||||
return QPointF();
|
||||
}
|
||||
|
||||
QJsonObject toolToJson(const core::Project::Tool& t) {
|
||||
QJsonObject o;
|
||||
o.insert(QStringLiteral("id"), t.id);
|
||||
o.insert(QStringLiteral("displayName"), t.displayName);
|
||||
o.insert(QStringLiteral("visible"), t.visible);
|
||||
o.insert(QStringLiteral("parentId"), t.parentId);
|
||||
o.insert(QStringLiteral("parentOffsetWorld"), pointToJson(t.parentOffsetWorld));
|
||||
o.insert(QStringLiteral("originWorld"), pointToJson(t.originWorld));
|
||||
|
||||
o.insert(QStringLiteral("type"), QStringLiteral("bubble"));
|
||||
o.insert(QStringLiteral("text"), t.text);
|
||||
o.insert(QStringLiteral("fontPx"), t.fontPx);
|
||||
QString align = QStringLiteral("center");
|
||||
if (t.align == core::Project::Tool::TextAlign::Left) align = QStringLiteral("left");
|
||||
if (t.align == core::Project::Tool::TextAlign::Right) align = QStringLiteral("right");
|
||||
o.insert(QStringLiteral("align"), align);
|
||||
o.insert(QStringLiteral("pointerT"), t.bubblePointerT01);
|
||||
|
||||
QJsonArray vis;
|
||||
for (const auto& k : t.visibilityKeys) {
|
||||
QJsonObject ko;
|
||||
ko.insert(QStringLiteral("frame"), k.frame);
|
||||
ko.insert(QStringLiteral("value"), k.value);
|
||||
vis.append(ko);
|
||||
}
|
||||
o.insert(QStringLiteral("visibilityKeys"), vis);
|
||||
|
||||
QJsonArray loc;
|
||||
for (const auto& k : t.locationKeys) {
|
||||
QJsonObject ko;
|
||||
ko.insert(QStringLiteral("frame"), k.frame);
|
||||
ko.insert(QStringLiteral("x"), k.value.x());
|
||||
ko.insert(QStringLiteral("y"), k.value.y());
|
||||
loc.append(ko);
|
||||
}
|
||||
o.insert(QStringLiteral("locationKeys"), loc);
|
||||
return o;
|
||||
}
|
||||
|
||||
bool toolFromJson(const QJsonObject& o, core::Project::Tool& out) {
|
||||
core::Project::Tool t;
|
||||
t.id = o.value(QStringLiteral("id")).toString();
|
||||
t.displayName = o.value(QStringLiteral("displayName")).toString();
|
||||
t.visible = o.value(QStringLiteral("visible")).toBool(true);
|
||||
t.parentId = o.value(QStringLiteral("parentId")).toString();
|
||||
t.parentOffsetWorld = pointFromJson(o.value(QStringLiteral("parentOffsetWorld")));
|
||||
t.originWorld = pointFromJson(o.value(QStringLiteral("originWorld")));
|
||||
|
||||
const QString type = o.value(QStringLiteral("type")).toString(QStringLiteral("bubble"));
|
||||
if (type != QStringLiteral("bubble")) {
|
||||
return false;
|
||||
}
|
||||
t.type = core::Project::Tool::Type::Bubble;
|
||||
t.text = o.value(QStringLiteral("text")).toString();
|
||||
t.fontPx = std::clamp(o.value(QStringLiteral("fontPx")).toInt(18), 8, 120);
|
||||
const QString align = o.value(QStringLiteral("align")).toString(QStringLiteral("center"));
|
||||
if (align == QStringLiteral("left")) t.align = core::Project::Tool::TextAlign::Left;
|
||||
else if (align == QStringLiteral("right")) t.align = core::Project::Tool::TextAlign::Right;
|
||||
else t.align = core::Project::Tool::TextAlign::Center;
|
||||
if (o.contains(QStringLiteral("pointerT"))) {
|
||||
t.bubblePointerT01 = std::clamp(o.value(QStringLiteral("pointerT")).toDouble(0.5), 0.0, 1.0);
|
||||
} else {
|
||||
const QString ptr = o.value(QStringLiteral("pointer")).toString(QStringLiteral("mid"));
|
||||
if (ptr == QStringLiteral("left")) t.bubblePointerT01 = 0.12;
|
||||
else if (ptr == QStringLiteral("right")) t.bubblePointerT01 = 0.88;
|
||||
else t.bubblePointerT01 = 0.5;
|
||||
}
|
||||
|
||||
const QJsonArray vis = o.value(QStringLiteral("visibilityKeys")).toArray();
|
||||
for (const auto& kv : vis) {
|
||||
const QJsonObject ko = kv.toObject();
|
||||
core::Project::ToolKeyframeBool k;
|
||||
k.frame = ko.value(QStringLiteral("frame")).toInt(0);
|
||||
k.value = ko.value(QStringLiteral("value")).toBool(true);
|
||||
t.visibilityKeys.push_back(k);
|
||||
}
|
||||
const QJsonArray loc = o.value(QStringLiteral("locationKeys")).toArray();
|
||||
for (const auto& kv : loc) {
|
||||
const QJsonObject ko = kv.toObject();
|
||||
core::Project::Entity::KeyframeVec2 k;
|
||||
k.frame = ko.value(QStringLiteral("frame")).toInt(0);
|
||||
k.value = QPointF(ko.value(QStringLiteral("x")).toDouble(0.0), ko.value(QStringLiteral("y")).toDouble(0.0));
|
||||
t.locationKeys.push_back(k);
|
||||
}
|
||||
out = t;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace core::library
|
||||
|
||||
13
client/core/library/ToolJson.h
Normal file
13
client/core/library/ToolJson.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "domain/Project.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace core::library {
|
||||
|
||||
QJsonObject toolToJson(const core::Project::Tool& t);
|
||||
bool toolFromJson(const QJsonObject& o, core::Project::Tool& out);
|
||||
|
||||
} // namespace core::library
|
||||
|
||||
@@ -251,6 +251,14 @@ public:
|
||||
|
||||
writeAnimationBlock(ds, entity, true);
|
||||
ds << entity.displayName << double(entity.userScale) << double(entity.distanceScaleCalibMult);
|
||||
ds << bool(entity.ignoreDistanceScale);
|
||||
ds << entity.parentId;
|
||||
ds << double(entity.parentOffsetWorld.x()) << double(entity.parentOffsetWorld.y());
|
||||
// v7:实体可见性关键帧
|
||||
ds << qint32(entity.visibilityKeys.size());
|
||||
for (const auto& k : entity.visibilityKeys) {
|
||||
ds << qint32(k.frame) << bool(k.value);
|
||||
}
|
||||
writeIntroBlock(ds, entity);
|
||||
}
|
||||
|
||||
@@ -270,6 +278,40 @@ public:
|
||||
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;
|
||||
bool ign = false;
|
||||
QString pid;
|
||||
double pox = 0.0;
|
||||
double poy = 0.0;
|
||||
ds >> ign >> pid >> pox >> poy;
|
||||
if (ds.status() != QDataStream::Ok) {
|
||||
return false;
|
||||
}
|
||||
tmp.ignoreDistanceScale = ign;
|
||||
tmp.parentId = pid;
|
||||
tmp.parentOffsetWorld = QPointF(pox, poy);
|
||||
|
||||
// v7:实体可见性关键帧
|
||||
qint32 nVis = 0;
|
||||
ds >> nVis;
|
||||
if (ds.status() != QDataStream::Ok) {
|
||||
return false;
|
||||
}
|
||||
tmp.visibilityKeys.clear();
|
||||
if (nVis > 0) {
|
||||
tmp.visibilityKeys.reserve(nVis);
|
||||
for (qint32 i = 0; i < nVis; ++i) {
|
||||
qint32 fr = 0;
|
||||
bool val = true;
|
||||
ds >> fr >> val;
|
||||
if (ds.status() != QDataStream::Ok) {
|
||||
return false;
|
||||
}
|
||||
core::Project::ToolKeyframeBool k;
|
||||
k.frame = int(fr);
|
||||
k.value = val;
|
||||
tmp.visibilityKeys.push_back(k);
|
||||
}
|
||||
}
|
||||
if (!readIntroBlock(ds, tmp.intro)) {
|
||||
return false;
|
||||
}
|
||||
@@ -329,7 +371,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 && ver != 4 && ver != 5) {
|
||||
if (ver != 1 && ver != 2 && ver != 3 && ver != 4 && ver != 5 && ver != 6 && ver != 7) {
|
||||
return false;
|
||||
}
|
||||
Project::Entity tmp;
|
||||
@@ -353,6 +395,48 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
|
||||
}
|
||||
tmp.distanceScaleCalibMult = (cal > 0.0) ? std::clamp(cal, 1e-6, 10.0) : 0.0;
|
||||
}
|
||||
if (ver >= 6) {
|
||||
bool ign = false;
|
||||
QString pid;
|
||||
double pox = 0.0;
|
||||
double poy = 0.0;
|
||||
ds >> ign >> pid >> pox >> poy;
|
||||
if (ds.status() != QDataStream::Ok) {
|
||||
return false;
|
||||
}
|
||||
tmp.ignoreDistanceScale = ign;
|
||||
tmp.parentId = pid;
|
||||
tmp.parentOffsetWorld = QPointF(pox, poy);
|
||||
} else {
|
||||
tmp.ignoreDistanceScale = false;
|
||||
tmp.parentId.clear();
|
||||
tmp.parentOffsetWorld = QPointF();
|
||||
}
|
||||
if (ver >= 7) {
|
||||
qint32 nVis = 0;
|
||||
ds >> nVis;
|
||||
if (ds.status() != QDataStream::Ok) {
|
||||
return false;
|
||||
}
|
||||
tmp.visibilityKeys.clear();
|
||||
if (nVis > 0) {
|
||||
tmp.visibilityKeys.reserve(nVis);
|
||||
for (qint32 i = 0; i < nVis; ++i) {
|
||||
qint32 fr = 0;
|
||||
bool val = true;
|
||||
ds >> fr >> val;
|
||||
if (ds.status() != QDataStream::Ok) {
|
||||
return false;
|
||||
}
|
||||
core::Project::ToolKeyframeBool k;
|
||||
k.frame = int(fr);
|
||||
k.value = val;
|
||||
tmp.visibilityKeys.push_back(k);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tmp.visibilityKeys.clear();
|
||||
}
|
||||
if (ver >= 5) {
|
||||
if (!readIntroBlock(ds, tmp.intro)) {
|
||||
return false;
|
||||
@@ -361,6 +445,10 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
|
||||
} else {
|
||||
tmp.displayName.clear();
|
||||
tmp.userScale = 1.0;
|
||||
tmp.ignoreDistanceScale = false;
|
||||
tmp.parentId.clear();
|
||||
tmp.parentOffsetWorld = QPointF();
|
||||
tmp.visibilityKeys.clear();
|
||||
}
|
||||
entity = std::move(tmp);
|
||||
return true;
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace core {
|
||||
class EntityPayloadBinary {
|
||||
public:
|
||||
static constexpr quint32 kMagicPayload = 0x48464550; // 'HFEP'
|
||||
static constexpr quint32 kPayloadVersion = 5; // v5:追加 intro(预览介绍:标题/正文/配图/视频路径预留)
|
||||
static constexpr quint32 kPayloadVersion = 7; // v7:追加 visibilityKeys
|
||||
|
||||
// 旧版独立动画文件(仍用于打开 v1 项目时合并)
|
||||
static constexpr quint32 kMagicLegacyAnim = 0x48465441; // 'HFTA'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ 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;
|
||||
static constexpr int kProjectIndexFormatVersion = 3;
|
||||
|
||||
ProjectWorkspace() = default;
|
||||
|
||||
@@ -47,10 +47,17 @@ public:
|
||||
|
||||
// 写入 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;
|
||||
@@ -74,11 +81,27 @@ public:
|
||||
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(); }
|
||||
|
||||
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 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);
|
||||
@@ -119,17 +142,25 @@ private:
|
||||
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, SetProjectTitle };
|
||||
enum class Type { ImportBackground, SetEntities, SetTools, 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;
|
||||
QString beforeProjectTitle;
|
||||
QString afterProjectTitle;
|
||||
int beforeFrameStart = 0;
|
||||
int afterFrameStart = 0;
|
||||
int beforeFrameEnd = 600;
|
||||
int afterFrameEnd = 600;
|
||||
};
|
||||
|
||||
static constexpr int kMaxHistorySteps = 30;
|
||||
@@ -137,6 +168,7 @@ private:
|
||||
|
||||
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);
|
||||
QString copyIntoAssetsAsBackground(const QString& sourceFilePath, const QRect& cropRectInSourceImage);
|
||||
bool writeDepthMap(const QImage& depth8);
|
||||
bool writeDepthMapBytes(const QByteArray& pngBytes);
|
||||
|
||||
Reference in New Issue
Block a user