update
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#include "workspace/ProjectWorkspace.h"
|
||||
#include "animation/AnimationSampling.h"
|
||||
#include "eval/ProjectEvaluator.h"
|
||||
#include "persistence/EntityPayloadBinary.h"
|
||||
#include "depth/DepthService.h"
|
||||
#include "net/ModelServerClient.h"
|
||||
@@ -13,9 +14,14 @@
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QImage>
|
||||
#include <QImageWriter>
|
||||
#include <QImageReader>
|
||||
#include <QUrl>
|
||||
#include <QHash>
|
||||
#include <QSet>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPolygonF>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
@@ -67,6 +73,24 @@ QPointF entityPolygonCentroidWorld(const Project::Entity& e, int frame, double s
|
||||
return polygonCentroidFromWorldPoints(w);
|
||||
}
|
||||
|
||||
QPointF resolvedOriginAtFrame(const Project& project, const QString& id, int frame) {
|
||||
if (id.isEmpty()) {
|
||||
return QPointF();
|
||||
}
|
||||
const auto rf = core::eval::evaluateAtFrame(project, frame, 10);
|
||||
for (const auto& re : rf.entities) {
|
||||
if (re.entity.id == id) {
|
||||
return re.entity.originWorld;
|
||||
}
|
||||
}
|
||||
for (const auto& rt : rf.tools) {
|
||||
if (rt.tool.id == id) {
|
||||
return rt.tool.originWorld;
|
||||
}
|
||||
}
|
||||
return QPointF();
|
||||
}
|
||||
|
||||
|
||||
QString ensureDir(const QString& path) {
|
||||
QDir dir(path);
|
||||
@@ -632,6 +656,10 @@ bool ProjectWorkspace::writeIndexJson() {
|
||||
if (!m_projectDir.isEmpty() && !syncEntityPayloadsToDisk()) {
|
||||
return false;
|
||||
}
|
||||
return writeIndexJsonWithoutPayloadSync();
|
||||
}
|
||||
|
||||
bool ProjectWorkspace::writeIndexJsonWithoutPayloadSync() {
|
||||
const auto root = projectToJson(m_project);
|
||||
QJsonDocument doc(root);
|
||||
|
||||
@@ -1153,6 +1181,9 @@ bool ProjectWorkspace::entityStubFromJsonV2(const QJsonObject& o, Project::Entit
|
||||
out.id = o.value("id").toString();
|
||||
out.entityPayloadPath = asOptionalRelativeUnderProject(o.value("payload").toString());
|
||||
out.visible = o.value("visible").toBool(true);
|
||||
out.blackholeVisible = true;
|
||||
out.blackholeId = out.id.isEmpty() ? QString() : QStringLiteral("blackhole-%1").arg(out.id);
|
||||
out.blackholeResolvedBy = QStringLiteral("pending");
|
||||
if (out.id.isEmpty() || out.entityPayloadPath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -1238,6 +1269,9 @@ bool ProjectWorkspace::entityFromJsonV1(const QJsonObject& o, Project::Entity& o
|
||||
out.cutoutPolygonWorld.push_back(out.originWorld + lp);
|
||||
}
|
||||
}
|
||||
out.blackholeVisible = true;
|
||||
out.blackholeId = QStringLiteral("blackhole-%1").arg(out.id);
|
||||
out.blackholeResolvedBy = QStringLiteral("pending");
|
||||
|
||||
// 旧版:关键帧内嵌在 project.json;若存在对应 .anim 文件,打开项目时会被二进制数据覆盖。
|
||||
const auto lk = o.value("locationKeys");
|
||||
@@ -1419,6 +1453,27 @@ bool ProjectWorkspace::syncEntityPayloadsToDisk() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectWorkspace::saveSingleEntityPayload(Project::Entity& entity) {
|
||||
if (m_projectDir.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (ensureEntitiesDir().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
entity.legacyAnimSidecarPath.clear();
|
||||
if (entity.entityPayloadPath.isEmpty()) {
|
||||
entity.entityPayloadPath =
|
||||
QString::fromUtf8(kAssetsDirName) + QStringLiteral("/entities/") + entity.id + QStringLiteral(".hfe");
|
||||
}
|
||||
const QString rel = asRelativeUnderProject(entity.entityPayloadPath);
|
||||
if (rel.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
entity.entityPayloadPath = rel;
|
||||
const QString abs = QDir(m_projectDir).filePath(rel);
|
||||
return EntityPayloadBinary::save(abs, entity);
|
||||
}
|
||||
|
||||
bool ProjectWorkspace::hydrateEntityPayloadsFromDisk() {
|
||||
if (m_projectDir.isEmpty()) {
|
||||
return true;
|
||||
@@ -1602,6 +1657,12 @@ bool ProjectWorkspace::addEntity(const Project::Entity& entity, const QImage& im
|
||||
e.entityPayloadPath =
|
||||
QString::fromUtf8(kAssetsDirName) + QStringLiteral("/entities/") + e.id + QStringLiteral(".hfe");
|
||||
}
|
||||
if (e.blackholeId.isEmpty()) {
|
||||
e.blackholeId = QStringLiteral("blackhole-%1").arg(e.id);
|
||||
}
|
||||
if (e.blackholeResolvedBy.isEmpty()) {
|
||||
e.blackholeResolvedBy = QStringLiteral("pending");
|
||||
}
|
||||
|
||||
auto ents = m_project.entities();
|
||||
ents.push_back(e);
|
||||
@@ -1629,6 +1690,185 @@ bool ProjectWorkspace::setEntityVisible(const QString& id, bool on) {
|
||||
return applyEntities(ents, true, on ? QStringLiteral("显示实体") : QStringLiteral("隐藏实体"));
|
||||
}
|
||||
|
||||
bool ProjectWorkspace::setEntityBlackholeVisible(const QString& id, bool on) {
|
||||
if (m_projectDir.isEmpty() || id.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const auto before = m_project.entities();
|
||||
auto ents = before;
|
||||
bool found = false;
|
||||
bool changed = false;
|
||||
int hit = -1;
|
||||
for (auto& e : ents) {
|
||||
if (e.id != id) {
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
hit = static_cast<int>(&e - ents.data());
|
||||
if (e.blackholeVisible != on) {
|
||||
e.blackholeVisible = on;
|
||||
changed = true;
|
||||
}
|
||||
if (e.blackholeId.isEmpty()) {
|
||||
e.blackholeId = QStringLiteral("blackhole-%1").arg(e.id);
|
||||
changed = true;
|
||||
}
|
||||
if (on) {
|
||||
if (e.blackholeResolvedBy.isEmpty()) {
|
||||
e.blackholeResolvedBy = QStringLiteral("pending");
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
if (!changed || hit < 0) {
|
||||
return true;
|
||||
}
|
||||
m_project.setEntities(ents);
|
||||
if (!saveSingleEntityPayload(ents[hit]) || !writeIndexJsonWithoutPayloadSync()) {
|
||||
m_project.setEntities(before);
|
||||
return false;
|
||||
}
|
||||
Operation op;
|
||||
op.type = Operation::Type::SetEntities;
|
||||
op.label = on ? QStringLiteral("显示黑洞") : QStringLiteral("隐藏黑洞");
|
||||
op.beforeEntities = before;
|
||||
op.afterEntities = ents;
|
||||
pushOperation(op);
|
||||
m_redoStack.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectWorkspace::resolveBlackholeByUseOriginalBackground(const QString& id) {
|
||||
if (m_projectDir.isEmpty() || id.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const auto before = m_project.entities();
|
||||
auto ents = before;
|
||||
int hit = -1;
|
||||
for (int i = 0; i < ents.size(); ++i) {
|
||||
if (ents[i].id == id) {
|
||||
hit = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hit < 0) {
|
||||
return false;
|
||||
}
|
||||
auto& e = ents[hit];
|
||||
e.blackholeVisible = false;
|
||||
if (e.blackholeId.isEmpty()) {
|
||||
e.blackholeId = QStringLiteral("blackhole-%1").arg(e.id);
|
||||
}
|
||||
e.blackholeResolvedBy = QStringLiteral("use_original_background");
|
||||
|
||||
m_project.setEntities(ents);
|
||||
if (!saveSingleEntityPayload(ents[hit]) || !writeIndexJsonWithoutPayloadSync()) {
|
||||
m_project.setEntities(before);
|
||||
return false;
|
||||
}
|
||||
Operation op;
|
||||
op.type = Operation::Type::SetEntities;
|
||||
op.label = QStringLiteral("黑洞使用原始背景");
|
||||
op.beforeEntities = before;
|
||||
op.afterEntities = ents;
|
||||
pushOperation(op);
|
||||
m_redoStack.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
|
||||
bool hideBlackholeAfterFill) {
|
||||
if (m_projectDir.isEmpty() || id.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const QString bgAbs = backgroundAbsolutePath();
|
||||
if (bgAbs.isEmpty() || !QFileInfo::exists(bgAbs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ents = m_project.entities();
|
||||
int hit = -1;
|
||||
for (int i = 0; i < ents.size(); ++i) {
|
||||
if (ents[i].id == id) {
|
||||
hit = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hit < 0) {
|
||||
return false;
|
||||
}
|
||||
const auto& ent = ents[hit];
|
||||
if (ent.cutoutPolygonWorld.size() < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QImage bg(bgAbs);
|
||||
if (bg.isNull()) {
|
||||
return false;
|
||||
}
|
||||
if (bg.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
const QImage srcSnapshot = bg;
|
||||
|
||||
QPainterPath holePath;
|
||||
holePath.addPolygon(QPolygonF(ent.cutoutPolygonWorld));
|
||||
holePath.closeSubpath();
|
||||
const QRect targetRect = holePath.boundingRect().toAlignedRect().intersected(QRect(QPoint(0, 0), bg.size()));
|
||||
if (!targetRect.isValid() || targetRect.width() <= 0 || targetRect.height() <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QRect srcRect(targetRect.topLeft() + sourceOffsetPx, targetRect.size());
|
||||
if (srcRect.left() < 0) srcRect.moveLeft(0);
|
||||
if (srcRect.top() < 0) srcRect.moveTop(0);
|
||||
if (srcRect.right() >= bg.width()) srcRect.moveRight(bg.width() - 1);
|
||||
if (srcRect.bottom() >= bg.height()) srcRect.moveBottom(bg.height() - 1);
|
||||
srcRect = srcRect.intersected(QRect(QPoint(0, 0), bg.size()));
|
||||
if (srcRect.width() != targetRect.width() || srcRect.height() != targetRect.height()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
QPainter p(&bg);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
p.setClipPath(holePath);
|
||||
p.drawImage(targetRect.topLeft(), srcSnapshot, srcRect);
|
||||
p.end();
|
||||
}
|
||||
QImageWriter writer(bgAbs);
|
||||
writer.setFormat("png");
|
||||
writer.setCompression(1);
|
||||
if (!writer.write(bg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto before = m_project.entities();
|
||||
ents[hit].blackholeVisible = hideBlackholeAfterFill ? false : ents[hit].blackholeVisible;
|
||||
if (ents[hit].blackholeId.isEmpty()) {
|
||||
ents[hit].blackholeId = QStringLiteral("blackhole-%1").arg(ents[hit].id);
|
||||
}
|
||||
ents[hit].blackholeResolvedBy = QStringLiteral("copy_background");
|
||||
|
||||
m_project.setEntities(ents);
|
||||
if (!saveSingleEntityPayload(ents[hit]) || !writeIndexJsonWithoutPayloadSync()) {
|
||||
m_project.setEntities(before);
|
||||
return false;
|
||||
}
|
||||
Operation op;
|
||||
op.type = Operation::Type::SetEntities;
|
||||
op.label = QStringLiteral("黑洞复制填充");
|
||||
op.beforeEntities = before;
|
||||
op.afterEntities = ents;
|
||||
pushOperation(op);
|
||||
m_redoStack.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProjectWorkspace::setEntityVisibilityKey(const QString& id, int frame, bool visible) {
|
||||
if (m_projectDir.isEmpty() || id.isEmpty() || frame < 0) {
|
||||
return false;
|
||||
@@ -1804,12 +2044,78 @@ bool ProjectWorkspace::setEntityParent(const QString& id, const QString& parentI
|
||||
if (!parentId.isEmpty() && !ids.contains(parentId)) {
|
||||
return false;
|
||||
}
|
||||
const int frameStart = std::max(0, m_project.frameStart());
|
||||
|
||||
// 父子关系切换时:需要把 location 关键帧在“绝对世界坐标”与“相对父对象偏移”之间互转,
|
||||
// 否则同一组 key 会被用不同坐标系解释,造成跳跃。
|
||||
auto fetchOrigin = [](const core::eval::ResolvedProjectFrame& rf, const QString& anyId) -> QPointF {
|
||||
if (anyId.isEmpty()) return QPointF();
|
||||
for (const auto& re : rf.entities) {
|
||||
if (re.entity.id == anyId) return re.entity.originWorld;
|
||||
}
|
||||
for (const auto& rt : rf.tools) {
|
||||
if (rt.tool.id == anyId) return rt.tool.originWorld;
|
||||
}
|
||||
return QPointF();
|
||||
};
|
||||
|
||||
auto parentOriginAt = [&](const QString& pid, int f) -> QPointF {
|
||||
if (pid.isEmpty()) return QPointF();
|
||||
const auto rf = core::eval::evaluateAtFrame(m_project, f, 10);
|
||||
return fetchOrigin(rf, pid);
|
||||
};
|
||||
|
||||
auto convertKeys = [&](QVector<Project::Entity::KeyframeVec2>& keys,
|
||||
const QString& oldPid,
|
||||
const QString& newPid) {
|
||||
if (keys.isEmpty()) return;
|
||||
for (auto& k : keys) {
|
||||
const QPointF oldParentO = oldPid.isEmpty() ? QPointF() : parentOriginAt(oldPid, k.frame);
|
||||
const QPointF world = oldPid.isEmpty() ? k.value : (oldParentO + k.value);
|
||||
const QPointF newParentO = newPid.isEmpty() ? QPointF() : parentOriginAt(newPid, k.frame);
|
||||
k.value = newPid.isEmpty() ? world : (world - newParentO);
|
||||
}
|
||||
};
|
||||
|
||||
bool found = false;
|
||||
for (auto& e : ents) {
|
||||
if (e.id != id) continue;
|
||||
found = true;
|
||||
|
||||
const QString oldPid = e.parentId;
|
||||
const QPointF oldBaseStored = oldPid.isEmpty() ? e.originWorld : e.parentOffsetWorld;
|
||||
const QPointF oldParentOStart = oldPid.isEmpty() ? QPointF() : parentOriginAt(oldPid, frameStart);
|
||||
const QPointF baseWorldAtStart = oldPid.isEmpty() ? oldBaseStored : (oldParentOStart + oldBaseStored);
|
||||
|
||||
// 转换 clip 与内嵌 key(兼容 v1/v2/v3 数据来源)
|
||||
if (Project::AnimationClip* clip = activeClipOrNull(m_project)) {
|
||||
if (clip->entityLocationKeys.contains(e.id)) {
|
||||
auto k = clip->entityLocationKeys.value(e.id);
|
||||
convertKeys(k, oldPid, parentId);
|
||||
clip->entityLocationKeys.insert(e.id, k);
|
||||
}
|
||||
}
|
||||
convertKeys(e.locationKeys, oldPid, parentId);
|
||||
|
||||
// 更新父子信息
|
||||
e.parentId = parentId;
|
||||
e.parentOffsetWorld = parentOffsetWorld;
|
||||
|
||||
// 更新基准值:无 key/或 key 覆盖不到的区间仍应保持世界位置连续
|
||||
if (parentId.isEmpty()) {
|
||||
e.originWorld = baseWorldAtStart;
|
||||
e.parentOffsetWorld = QPointF();
|
||||
} else {
|
||||
const QPointF newParentOStart = parentOriginAt(parentId, frameStart);
|
||||
e.parentOffsetWorld = baseWorldAtStart - newParentOStart;
|
||||
// cycle/parent missing 时 resolve 会回退到 sampledOriginForEntity;令其也尽量不跳
|
||||
e.originWorld = baseWorldAtStart;
|
||||
}
|
||||
|
||||
// 若调用方传入了当前帧下的 parentOffsetWorld(来自 UI 计算),在“绑定父对象”场景下优先采用,
|
||||
// 保证操作当下立刻不跳(关键帧已整体转换,后续帧也保持一致)。
|
||||
if (!parentId.isEmpty()) {
|
||||
e.parentOffsetWorld = parentOffsetWorld;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!found) return false;
|
||||
@@ -1951,16 +2257,35 @@ bool ProjectWorkspace::moveEntityBy(const QString& id, const QPointF& delta, int
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
// 父子关系:若该实体绑定父实体,则移动应修改相对偏移(保持“跟随父实体”语义)。
|
||||
if (!e.parentId.isEmpty()) {
|
||||
e.parentOffsetWorld += delta;
|
||||
break;
|
||||
}
|
||||
Project::AnimationClip* clip = activeClipOrNull(m_project);
|
||||
QVector<Project::Entity::KeyframeVec2>* keys = nullptr;
|
||||
if (clip) {
|
||||
keys = &clip->entityLocationKeys[e.id];
|
||||
}
|
||||
// 父子关系:绑定父对象时,位置曲线表示“相对父对象偏移”。
|
||||
if (!e.parentId.isEmpty()) {
|
||||
const bool hasCurve = (keys && !keys->isEmpty()) || (!e.locationKeys.isEmpty());
|
||||
if (autoKeyLocation && currentFrame >= 0) {
|
||||
const QPointF sampled =
|
||||
(keys)
|
||||
? sampleLocation(*keys, currentFrame, e.parentOffsetWorld, KeyInterpolation::Linear)
|
||||
: sampleLocation(e.locationKeys, currentFrame, e.parentOffsetWorld, KeyInterpolation::Linear);
|
||||
if (keys) upsertKey(*keys, currentFrame, sampled + delta);
|
||||
else upsertKey(e.locationKeys, currentFrame, sampled + delta);
|
||||
} else if (!hasCurve) {
|
||||
e.parentOffsetWorld += delta;
|
||||
} else if (currentFrame >= 0) {
|
||||
const QPointF sampled =
|
||||
(keys)
|
||||
? sampleLocation(*keys, currentFrame, e.parentOffsetWorld, KeyInterpolation::Linear)
|
||||
: sampleLocation(e.locationKeys, currentFrame, e.parentOffsetWorld, KeyInterpolation::Linear);
|
||||
if (keys) upsertKey(*keys, currentFrame, sampled + delta);
|
||||
else upsertKey(e.locationKeys, currentFrame, sampled + delta);
|
||||
} else {
|
||||
e.parentOffsetWorld += delta;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const bool hasCurve = (keys && !keys->isEmpty()) || (!e.locationKeys.isEmpty());
|
||||
if (autoKeyLocation && currentFrame >= 0) {
|
||||
const QPointF sampled =
|
||||
@@ -2139,12 +2464,66 @@ bool ProjectWorkspace::setToolParent(const QString& id, const QString& parentId,
|
||||
return false;
|
||||
}
|
||||
auto tools = m_project.tools();
|
||||
const int frameStart = std::max(0, m_project.frameStart());
|
||||
|
||||
auto fetchOrigin = [](const core::eval::ResolvedProjectFrame& rf, const QString& anyId) -> QPointF {
|
||||
if (anyId.isEmpty()) return QPointF();
|
||||
for (const auto& re : rf.entities) {
|
||||
if (re.entity.id == anyId) return re.entity.originWorld;
|
||||
}
|
||||
for (const auto& rt : rf.tools) {
|
||||
if (rt.tool.id == anyId) return rt.tool.originWorld;
|
||||
}
|
||||
return QPointF();
|
||||
};
|
||||
|
||||
auto parentOriginAt = [&](const QString& pid, int f) -> QPointF {
|
||||
if (pid.isEmpty()) return QPointF();
|
||||
const auto rf = core::eval::evaluateAtFrame(m_project, f, 10);
|
||||
return fetchOrigin(rf, pid);
|
||||
};
|
||||
|
||||
auto convertKeys = [&](QVector<Project::Entity::KeyframeVec2>& keys,
|
||||
const QString& oldPid,
|
||||
const QString& newPid) {
|
||||
if (keys.isEmpty()) return;
|
||||
for (auto& k : keys) {
|
||||
const QPointF oldParentO = oldPid.isEmpty() ? QPointF() : parentOriginAt(oldPid, k.frame);
|
||||
const QPointF world = oldPid.isEmpty() ? k.value : (oldParentO + k.value);
|
||||
const QPointF newParentO = newPid.isEmpty() ? QPointF() : parentOriginAt(newPid, k.frame);
|
||||
k.value = newPid.isEmpty() ? world : (world - newParentO);
|
||||
}
|
||||
};
|
||||
|
||||
bool found = false;
|
||||
for (auto& t : tools) {
|
||||
if (t.id != id) continue;
|
||||
found = true;
|
||||
|
||||
const QString oldPid = t.parentId;
|
||||
const QPointF oldBaseStored = oldPid.isEmpty() ? t.originWorld : t.parentOffsetWorld;
|
||||
const QPointF oldParentOStart = oldPid.isEmpty() ? QPointF() : parentOriginAt(oldPid, frameStart);
|
||||
const QPointF baseWorldAtStart = oldPid.isEmpty() ? oldBaseStored : (oldParentOStart + oldBaseStored);
|
||||
|
||||
if (Project::AnimationClip* clip = activeClipOrNull(m_project)) {
|
||||
if (clip->toolLocationKeys.contains(t.id)) {
|
||||
auto k = clip->toolLocationKeys.value(t.id);
|
||||
convertKeys(k, oldPid, parentId);
|
||||
clip->toolLocationKeys.insert(t.id, k);
|
||||
}
|
||||
}
|
||||
convertKeys(t.locationKeys, oldPid, parentId);
|
||||
|
||||
t.parentId = parentId;
|
||||
t.parentOffsetWorld = parentOffsetWorld;
|
||||
if (parentId.isEmpty()) {
|
||||
t.originWorld = baseWorldAtStart;
|
||||
t.parentOffsetWorld = QPointF();
|
||||
} else {
|
||||
const QPointF newParentOStart = parentOriginAt(parentId, frameStart);
|
||||
t.parentOffsetWorld = baseWorldAtStart - newParentOStart;
|
||||
t.originWorld = baseWorldAtStart;
|
||||
t.parentOffsetWorld = parentOffsetWorld; // 同上:优先确保操作当下不跳
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!found) return false;
|
||||
@@ -2163,15 +2542,34 @@ bool ProjectWorkspace::moveToolBy(const QString& id, const QPointF& delta, int c
|
||||
for (auto& t : tools) {
|
||||
if (t.id != id) continue;
|
||||
found = true;
|
||||
if (!t.parentId.isEmpty()) {
|
||||
t.parentOffsetWorld += delta;
|
||||
break;
|
||||
}
|
||||
Project::AnimationClip* clip = activeClipOrNull(m_project);
|
||||
QVector<Project::Entity::KeyframeVec2>* keys = nullptr;
|
||||
if (clip) {
|
||||
keys = &clip->toolLocationKeys[t.id];
|
||||
}
|
||||
if (!t.parentId.isEmpty()) {
|
||||
const bool hasCurve = (keys && !keys->isEmpty()) || (!t.locationKeys.isEmpty());
|
||||
if (autoKeyLocation && currentFrame >= 0) {
|
||||
const QPointF sampled =
|
||||
(keys)
|
||||
? sampleLocation(*keys, currentFrame, t.parentOffsetWorld, KeyInterpolation::Linear)
|
||||
: sampleLocation(t.locationKeys, currentFrame, t.parentOffsetWorld, KeyInterpolation::Linear);
|
||||
if (keys) upsertKey(*keys, currentFrame, sampled + delta);
|
||||
else upsertKey(t.locationKeys, currentFrame, sampled + delta);
|
||||
} else if (!hasCurve) {
|
||||
t.parentOffsetWorld += delta;
|
||||
} else if (currentFrame >= 0) {
|
||||
const QPointF sampled =
|
||||
(keys)
|
||||
? sampleLocation(*keys, currentFrame, t.parentOffsetWorld, KeyInterpolation::Linear)
|
||||
: sampleLocation(t.locationKeys, currentFrame, t.parentOffsetWorld, KeyInterpolation::Linear);
|
||||
if (keys) upsertKey(*keys, currentFrame, sampled + delta);
|
||||
else upsertKey(t.locationKeys, currentFrame, sampled + delta);
|
||||
} else {
|
||||
t.parentOffsetWorld += delta;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const bool hasCurve = (keys && !keys->isEmpty()) || (!t.locationKeys.isEmpty());
|
||||
if (autoKeyLocation && currentFrame >= 0) {
|
||||
const QPointF sampled =
|
||||
@@ -2207,8 +2605,16 @@ bool ProjectWorkspace::setEntityLocationKey(const QString& id, int frame, const
|
||||
}
|
||||
Project::AnimationClip* clip = activeClipOrNull(m_project);
|
||||
if (!clip) return false;
|
||||
QPointF keyValue = originWorld;
|
||||
for (const auto& e : m_project.entities()) {
|
||||
if (e.id == id && !e.parentId.isEmpty()) {
|
||||
const QPointF parentWorld = resolvedOriginAtFrame(m_project, e.parentId, frame);
|
||||
keyValue = originWorld - parentWorld;
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto& keys = clip->entityLocationKeys[id];
|
||||
upsertKey(keys, frame, originWorld);
|
||||
upsertKey(keys, frame, keyValue);
|
||||
return writeIndexJson();
|
||||
}
|
||||
|
||||
@@ -2406,7 +2812,25 @@ QString ProjectWorkspace::copyIntoAssetsAsBackground(const QString& sourceFilePa
|
||||
const auto destAbs = QDir(assetsDir).filePath(fileName);
|
||||
const auto destRel = QString::fromUtf8(kAssetsDirName) + "/" + fileName;
|
||||
|
||||
QImage img(sourceFilePath);
|
||||
// Qt 默认的 image allocation limit 较小,超大分辨率背景可能会被拒绝。
|
||||
// 这里提高 limit,并对极端大图按像素数上限自动缩放后再裁剪落盘。
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QImageReader::setAllocationLimit(1024); // MB
|
||||
#endif
|
||||
QImageReader reader(sourceFilePath);
|
||||
reader.setAutoTransform(true);
|
||||
const QSize sz = reader.size();
|
||||
if (sz.isValid()) {
|
||||
constexpr qint64 kMaxPixels = 160LL * 1000LL * 1000LL; // 160MP
|
||||
const qint64 pixels = qint64(sz.width()) * qint64(sz.height());
|
||||
if (pixels > kMaxPixels) {
|
||||
const double s = std::sqrt(double(kMaxPixels) / std::max<double>(1.0, double(pixels)));
|
||||
const int nw = std::max(1, int(std::lround(sz.width() * s)));
|
||||
const int nh = std::max(1, int(std::lround(sz.height() * s)));
|
||||
reader.setScaledSize(QSize(nw, nh));
|
||||
}
|
||||
}
|
||||
QImage img = reader.read();
|
||||
if (img.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
#include <QPoint>
|
||||
|
||||
namespace core {
|
||||
|
||||
@@ -95,6 +96,11 @@ public:
|
||||
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 setEntityBlackholeVisible(const QString& id, bool on);
|
||||
bool resolveBlackholeByUseOriginalBackground(const QString& id);
|
||||
// 复制背景其他区域填充黑洞(sourceOffsetPx 以黑洞包围盒左上角为基准偏移)
|
||||
bool resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
|
||||
bool hideBlackholeAfterFill);
|
||||
bool setEntityVisibilityKey(const QString& id, int frame, bool visible);
|
||||
bool removeEntityVisibilityKey(const QString& id, int frame);
|
||||
bool setEntityDisplayName(const QString& id, const QString& displayName);
|
||||
@@ -133,6 +139,8 @@ private:
|
||||
bool syncEntityPayloadsToDisk();
|
||||
bool hydrateEntityPayloadsFromDisk();
|
||||
void loadV1LegacyAnimationSidecars();
|
||||
bool writeIndexJsonWithoutPayloadSync();
|
||||
bool saveSingleEntityPayload(Project::Entity& entity);
|
||||
|
||||
static QJsonObject projectToJson(const Project& project);
|
||||
static bool projectFromJson(const QJsonObject& root, Project& outProject, int* outFileVersion);
|
||||
|
||||
Reference in New Issue
Block a user