新增根据帧数控制可见性

This commit is contained in:
2026-04-09 17:30:55 +08:00
parent e116a9ec79
commit d67d7dc0c5
29 changed files with 4387 additions and 408 deletions

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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