update
This commit is contained in:
@@ -53,6 +53,12 @@ public:
|
|||||||
QVector<QPointF> polygonLocal;
|
QVector<QPointF> polygonLocal;
|
||||||
// 从背景中抠洞的位置:固定在创建时的 world 坐标,不随实体移动
|
// 从背景中抠洞的位置:固定在创建时的 world 坐标,不随实体移动
|
||||||
QVector<QPointF> cutoutPolygonWorld;
|
QVector<QPointF> cutoutPolygonWorld;
|
||||||
|
// 背景空缺标识:用于项目树节点和交互,空则按 id 生成默认值
|
||||||
|
QString blackholeId;
|
||||||
|
// 背景空缺可见性:与实体可见性轨道解耦,控制背景空缺是否显示
|
||||||
|
bool blackholeVisible = true;
|
||||||
|
// 背景空缺修复方案:copy_background / use_original_background / model_inpaint(预留)
|
||||||
|
QString blackholeResolvedBy;
|
||||||
QPointF originWorld;
|
QPointF originWorld;
|
||||||
int depth = 0; // 0..255
|
int depth = 0; // 0..255
|
||||||
QString imagePath; // 相对路径,例如 "assets/entities/entity-1.png"
|
QString imagePath; // 相对路径,例如 "assets/entities/entity-1.png"
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ QPointF sampledOriginForEntity(const core::Project::Entity& e,
|
|||||||
return core::sampleLocation(e.locationKeys, localFrame, e.originWorld, core::KeyInterpolation::Linear);
|
return core::sampleLocation(e.locationKeys, localFrame, e.originWorld, core::KeyInterpolation::Linear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPointF sampledRelativeForEntity(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.parentOffsetWorld, core::KeyInterpolation::Linear);
|
||||||
|
}
|
||||||
|
return core::sampleLocation(e.locationKeys, localFrame, e.parentOffsetWorld, core::KeyInterpolation::Linear);
|
||||||
|
}
|
||||||
|
|
||||||
QPointF sampledOriginForTool(const core::Project::Tool& t,
|
QPointF sampledOriginForTool(const core::Project::Tool& t,
|
||||||
const core::Project::AnimationClip* clipOrNull,
|
const core::Project::AnimationClip* clipOrNull,
|
||||||
int localFrame) {
|
int localFrame) {
|
||||||
@@ -35,6 +45,16 @@ QPointF sampledOriginForTool(const core::Project::Tool& t,
|
|||||||
return core::sampleLocation(t.locationKeys, localFrame, t.originWorld, core::KeyInterpolation::Linear);
|
return core::sampleLocation(t.locationKeys, localFrame, t.originWorld, core::KeyInterpolation::Linear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPointF sampledRelativeForTool(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.parentOffsetWorld, core::KeyInterpolation::Linear);
|
||||||
|
}
|
||||||
|
return core::sampleLocation(t.locationKeys, localFrame, t.parentOffsetWorld, core::KeyInterpolation::Linear);
|
||||||
|
}
|
||||||
|
|
||||||
struct VisKey {
|
struct VisKey {
|
||||||
int frame = 0;
|
int frame = 0;
|
||||||
bool value = true;
|
bool value = true;
|
||||||
@@ -227,24 +247,24 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
|
|||||||
|
|
||||||
const NodeRef r = index.value(id);
|
const NodeRef r = index.value(id);
|
||||||
QString parentId;
|
QString parentId;
|
||||||
QPointF off;
|
|
||||||
QPointF selfSampled;
|
QPointF selfSampled;
|
||||||
if (r.kind == NodeRef::Kind::Entity) {
|
if (r.kind == NodeRef::Kind::Entity) {
|
||||||
const auto& e = ents[r.index];
|
const auto& e = ents[r.index];
|
||||||
parentId = e.parentId;
|
parentId = e.parentId;
|
||||||
off = e.parentOffsetWorld;
|
|
||||||
selfSampled = sampledOriginForEntity(e, clip, localFrame);
|
selfSampled = sampledOriginForEntity(e, clip, localFrame);
|
||||||
} else {
|
} else {
|
||||||
const auto& t = tools[r.index];
|
const auto& t = tools[r.index];
|
||||||
parentId = t.parentId;
|
parentId = t.parentId;
|
||||||
off = t.parentOffsetWorld;
|
|
||||||
selfSampled = sampledOriginForTool(t, clip, localFrame);
|
selfSampled = sampledOriginForTool(t, clip, localFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPointF outO = selfSampled;
|
QPointF outO = selfSampled;
|
||||||
if (!parentId.isEmpty() && index.contains(parentId)) {
|
if (!parentId.isEmpty() && index.contains(parentId)) {
|
||||||
const QPointF po = resolve(parentId);
|
const QPointF po = resolve(parentId);
|
||||||
outO = po + off;
|
const QPointF rel = (r.kind == NodeRef::Kind::Entity)
|
||||||
|
? sampledRelativeForEntity(ents[r.index], clip, localFrame)
|
||||||
|
: sampledRelativeForTool(tools[r.index], clip, localFrame);
|
||||||
|
outO = po + rel;
|
||||||
}
|
}
|
||||||
|
|
||||||
resolving.insert(id, false);
|
resolving.insert(id, false);
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ QJsonObject entityToJson(const core::Project::Entity& e) {
|
|||||||
o.insert(QStringLiteral("visible"), e.visible);
|
o.insert(QStringLiteral("visible"), e.visible);
|
||||||
o.insert(QStringLiteral("polygonLocal"), pointsToJson(e.polygonLocal));
|
o.insert(QStringLiteral("polygonLocal"), pointsToJson(e.polygonLocal));
|
||||||
o.insert(QStringLiteral("cutoutPolygonWorld"), pointsToJson(e.cutoutPolygonWorld));
|
o.insert(QStringLiteral("cutoutPolygonWorld"), pointsToJson(e.cutoutPolygonWorld));
|
||||||
|
o.insert(QStringLiteral("blackholeId"), e.blackholeId);
|
||||||
|
o.insert(QStringLiteral("blackholeVisible"), e.blackholeVisible);
|
||||||
|
o.insert(QStringLiteral("blackholeResolvedBy"), e.blackholeResolvedBy);
|
||||||
o.insert(QStringLiteral("originWorld"), pointToJson(e.originWorld));
|
o.insert(QStringLiteral("originWorld"), pointToJson(e.originWorld));
|
||||||
o.insert(QStringLiteral("depth"), e.depth);
|
o.insert(QStringLiteral("depth"), e.depth);
|
||||||
o.insert(QStringLiteral("imagePath"), e.imagePath);
|
o.insert(QStringLiteral("imagePath"), e.imagePath);
|
||||||
@@ -152,6 +155,12 @@ bool entityFromJson(const QJsonObject& o, core::Project::Entity& out) {
|
|||||||
// cutout 允许不存在:按空处理
|
// cutout 允许不存在:按空处理
|
||||||
e.cutoutPolygonWorld.clear();
|
e.cutoutPolygonWorld.clear();
|
||||||
}
|
}
|
||||||
|
e.blackholeId = o.value(QStringLiteral("blackholeId")).toString();
|
||||||
|
if (e.blackholeId.isEmpty() && !e.id.isEmpty()) {
|
||||||
|
e.blackholeId = QStringLiteral("blackhole-%1").arg(e.id);
|
||||||
|
}
|
||||||
|
e.blackholeVisible = o.value(QStringLiteral("blackholeVisible")).toBool(true);
|
||||||
|
e.blackholeResolvedBy = o.value(QStringLiteral("blackholeResolvedBy")).toString();
|
||||||
{
|
{
|
||||||
QPointF p;
|
QPointF p;
|
||||||
if (!pointFromJson(o.value(QStringLiteral("originWorld")), p)) {
|
if (!pointFromJson(o.value(QStringLiteral("originWorld")), p)) {
|
||||||
|
|||||||
@@ -260,6 +260,13 @@ public:
|
|||||||
ds << qint32(k.frame) << bool(k.value);
|
ds << qint32(k.frame) << bool(k.value);
|
||||||
}
|
}
|
||||||
writeIntroBlock(ds, entity);
|
writeIntroBlock(ds, entity);
|
||||||
|
// v8/v9:黑洞元数据(与实体可见性解耦)
|
||||||
|
ds << bool(entity.blackholeVisible);
|
||||||
|
const QString holeId = entity.blackholeId.isEmpty()
|
||||||
|
? QStringLiteral("blackhole-%1").arg(entity.id)
|
||||||
|
: entity.blackholeId;
|
||||||
|
ds << holeId;
|
||||||
|
ds << entity.blackholeResolvedBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool readBody(QDataStream& ds) override {
|
bool readBody(QDataStream& ds) override {
|
||||||
@@ -315,6 +322,16 @@ public:
|
|||||||
if (!readIntroBlock(ds, tmp.intro)) {
|
if (!readIntroBlock(ds, tmp.intro)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
bool holeVisible = true;
|
||||||
|
QString holeId;
|
||||||
|
QString resolvedBy;
|
||||||
|
ds >> holeVisible >> holeId >> resolvedBy;
|
||||||
|
if (ds.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tmp.blackholeVisible = holeVisible;
|
||||||
|
tmp.blackholeId = holeId.isEmpty() ? QStringLiteral("blackhole-%1").arg(tmp.id) : holeId;
|
||||||
|
tmp.blackholeResolvedBy = resolvedBy;
|
||||||
*m_dst = std::move(tmp);
|
*m_dst = std::move(tmp);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -371,7 +388,7 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
|
|||||||
if (ds.status() != QDataStream::Ok || magic != kMagicPayload) {
|
if (ds.status() != QDataStream::Ok || magic != kMagicPayload) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ver != 1 && ver != 2 && ver != 3 && ver != 4 && ver != 5 && ver != 6 && ver != 7) {
|
if (ver != 1 && ver != 2 && ver != 3 && ver != 4 && ver != 5 && ver != 6 && ver != 7 && ver != 8 && ver != 9) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Project::Entity tmp;
|
Project::Entity tmp;
|
||||||
@@ -442,6 +459,30 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ver >= 8) {
|
||||||
|
bool holeVisible = true;
|
||||||
|
QString holeId;
|
||||||
|
ds >> holeVisible >> holeId;
|
||||||
|
if (ds.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tmp.blackholeVisible = holeVisible;
|
||||||
|
tmp.blackholeId = holeId.isEmpty() ? QStringLiteral("blackhole-%1").arg(tmp.id) : holeId;
|
||||||
|
if (ver >= 9) {
|
||||||
|
QString resolvedBy;
|
||||||
|
ds >> resolvedBy;
|
||||||
|
if (ds.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tmp.blackholeResolvedBy = resolvedBy;
|
||||||
|
} else {
|
||||||
|
tmp.blackholeResolvedBy = QStringLiteral("pending");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tmp.blackholeVisible = true;
|
||||||
|
tmp.blackholeId = QStringLiteral("blackhole-%1").arg(tmp.id);
|
||||||
|
tmp.blackholeResolvedBy = QStringLiteral("pending");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tmp.displayName.clear();
|
tmp.displayName.clear();
|
||||||
tmp.userScale = 1.0;
|
tmp.userScale = 1.0;
|
||||||
@@ -449,6 +490,9 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
|
|||||||
tmp.parentId.clear();
|
tmp.parentId.clear();
|
||||||
tmp.parentOffsetWorld = QPointF();
|
tmp.parentOffsetWorld = QPointF();
|
||||||
tmp.visibilityKeys.clear();
|
tmp.visibilityKeys.clear();
|
||||||
|
tmp.blackholeVisible = true;
|
||||||
|
tmp.blackholeId = QStringLiteral("blackhole-%1").arg(tmp.id);
|
||||||
|
tmp.blackholeResolvedBy = QStringLiteral("pending");
|
||||||
}
|
}
|
||||||
entity = std::move(tmp);
|
entity = std::move(tmp);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace core {
|
|||||||
class EntityPayloadBinary {
|
class EntityPayloadBinary {
|
||||||
public:
|
public:
|
||||||
static constexpr quint32 kMagicPayload = 0x48464550; // 'HFEP'
|
static constexpr quint32 kMagicPayload = 0x48464550; // 'HFEP'
|
||||||
static constexpr quint32 kPayloadVersion = 7; // v7:追加 visibilityKeys
|
static constexpr quint32 kPayloadVersion = 9; // v9:追加 blackholeResolvedBy
|
||||||
|
|
||||||
// 旧版独立动画文件(仍用于打开 v1 项目时合并)
|
// 旧版独立动画文件(仍用于打开 v1 项目时合并)
|
||||||
static constexpr quint32 kMagicLegacyAnim = 0x48465441; // 'HFTA'
|
static constexpr quint32 kMagicLegacyAnim = 0x48465441; // 'HFTA'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "workspace/ProjectWorkspace.h"
|
#include "workspace/ProjectWorkspace.h"
|
||||||
#include "animation/AnimationSampling.h"
|
#include "animation/AnimationSampling.h"
|
||||||
|
#include "eval/ProjectEvaluator.h"
|
||||||
#include "persistence/EntityPayloadBinary.h"
|
#include "persistence/EntityPayloadBinary.h"
|
||||||
#include "depth/DepthService.h"
|
#include "depth/DepthService.h"
|
||||||
#include "net/ModelServerClient.h"
|
#include "net/ModelServerClient.h"
|
||||||
@@ -13,9 +14,14 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QJsonValue>
|
#include <QJsonValue>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QImageWriter>
|
||||||
|
#include <QImageReader>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QPolygonF>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@@ -67,6 +73,24 @@ QPointF entityPolygonCentroidWorld(const Project::Entity& e, int frame, double s
|
|||||||
return polygonCentroidFromWorldPoints(w);
|
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) {
|
QString ensureDir(const QString& path) {
|
||||||
QDir dir(path);
|
QDir dir(path);
|
||||||
@@ -632,6 +656,10 @@ bool ProjectWorkspace::writeIndexJson() {
|
|||||||
if (!m_projectDir.isEmpty() && !syncEntityPayloadsToDisk()) {
|
if (!m_projectDir.isEmpty() && !syncEntityPayloadsToDisk()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return writeIndexJsonWithoutPayloadSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProjectWorkspace::writeIndexJsonWithoutPayloadSync() {
|
||||||
const auto root = projectToJson(m_project);
|
const auto root = projectToJson(m_project);
|
||||||
QJsonDocument doc(root);
|
QJsonDocument doc(root);
|
||||||
|
|
||||||
@@ -1153,6 +1181,9 @@ bool ProjectWorkspace::entityStubFromJsonV2(const QJsonObject& o, Project::Entit
|
|||||||
out.id = o.value("id").toString();
|
out.id = o.value("id").toString();
|
||||||
out.entityPayloadPath = asOptionalRelativeUnderProject(o.value("payload").toString());
|
out.entityPayloadPath = asOptionalRelativeUnderProject(o.value("payload").toString());
|
||||||
out.visible = o.value("visible").toBool(true);
|
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()) {
|
if (out.id.isEmpty() || out.entityPayloadPath.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1238,6 +1269,9 @@ bool ProjectWorkspace::entityFromJsonV1(const QJsonObject& o, Project::Entity& o
|
|||||||
out.cutoutPolygonWorld.push_back(out.originWorld + lp);
|
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 文件,打开项目时会被二进制数据覆盖。
|
// 旧版:关键帧内嵌在 project.json;若存在对应 .anim 文件,打开项目时会被二进制数据覆盖。
|
||||||
const auto lk = o.value("locationKeys");
|
const auto lk = o.value("locationKeys");
|
||||||
@@ -1419,6 +1453,27 @@ bool ProjectWorkspace::syncEntityPayloadsToDisk() {
|
|||||||
return true;
|
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() {
|
bool ProjectWorkspace::hydrateEntityPayloadsFromDisk() {
|
||||||
if (m_projectDir.isEmpty()) {
|
if (m_projectDir.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1602,6 +1657,12 @@ bool ProjectWorkspace::addEntity(const Project::Entity& entity, const QImage& im
|
|||||||
e.entityPayloadPath =
|
e.entityPayloadPath =
|
||||||
QString::fromUtf8(kAssetsDirName) + QStringLiteral("/entities/") + e.id + QStringLiteral(".hfe");
|
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();
|
auto ents = m_project.entities();
|
||||||
ents.push_back(e);
|
ents.push_back(e);
|
||||||
@@ -1629,6 +1690,185 @@ bool ProjectWorkspace::setEntityVisible(const QString& id, bool on) {
|
|||||||
return applyEntities(ents, true, on ? QStringLiteral("显示实体") : QStringLiteral("隐藏实体"));
|
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) {
|
bool ProjectWorkspace::setEntityVisibilityKey(const QString& id, int frame, bool visible) {
|
||||||
if (m_projectDir.isEmpty() || id.isEmpty() || frame < 0) {
|
if (m_projectDir.isEmpty() || id.isEmpty() || frame < 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1804,12 +2044,78 @@ bool ProjectWorkspace::setEntityParent(const QString& id, const QString& parentI
|
|||||||
if (!parentId.isEmpty() && !ids.contains(parentId)) {
|
if (!parentId.isEmpty() && !ids.contains(parentId)) {
|
||||||
return false;
|
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;
|
bool found = false;
|
||||||
for (auto& e : ents) {
|
for (auto& e : ents) {
|
||||||
if (e.id != id) continue;
|
if (e.id != id) continue;
|
||||||
found = true;
|
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.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;
|
break;
|
||||||
}
|
}
|
||||||
if (!found) return false;
|
if (!found) return false;
|
||||||
@@ -1951,16 +2257,35 @@ bool ProjectWorkspace::moveEntityBy(const QString& id, const QPointF& delta, int
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
found = true;
|
found = true;
|
||||||
// 父子关系:若该实体绑定父实体,则移动应修改相对偏移(保持“跟随父实体”语义)。
|
|
||||||
if (!e.parentId.isEmpty()) {
|
|
||||||
e.parentOffsetWorld += delta;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Project::AnimationClip* clip = activeClipOrNull(m_project);
|
Project::AnimationClip* clip = activeClipOrNull(m_project);
|
||||||
QVector<Project::Entity::KeyframeVec2>* keys = nullptr;
|
QVector<Project::Entity::KeyframeVec2>* keys = nullptr;
|
||||||
if (clip) {
|
if (clip) {
|
||||||
keys = &clip->entityLocationKeys[e.id];
|
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());
|
const bool hasCurve = (keys && !keys->isEmpty()) || (!e.locationKeys.isEmpty());
|
||||||
if (autoKeyLocation && currentFrame >= 0) {
|
if (autoKeyLocation && currentFrame >= 0) {
|
||||||
const QPointF sampled =
|
const QPointF sampled =
|
||||||
@@ -2139,12 +2464,66 @@ bool ProjectWorkspace::setToolParent(const QString& id, const QString& parentId,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto tools = m_project.tools();
|
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;
|
bool found = false;
|
||||||
for (auto& t : tools) {
|
for (auto& t : tools) {
|
||||||
if (t.id != id) continue;
|
if (t.id != id) continue;
|
||||||
found = true;
|
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.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;
|
break;
|
||||||
}
|
}
|
||||||
if (!found) return false;
|
if (!found) return false;
|
||||||
@@ -2163,15 +2542,34 @@ bool ProjectWorkspace::moveToolBy(const QString& id, const QPointF& delta, int c
|
|||||||
for (auto& t : tools) {
|
for (auto& t : tools) {
|
||||||
if (t.id != id) continue;
|
if (t.id != id) continue;
|
||||||
found = true;
|
found = true;
|
||||||
if (!t.parentId.isEmpty()) {
|
|
||||||
t.parentOffsetWorld += delta;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Project::AnimationClip* clip = activeClipOrNull(m_project);
|
Project::AnimationClip* clip = activeClipOrNull(m_project);
|
||||||
QVector<Project::Entity::KeyframeVec2>* keys = nullptr;
|
QVector<Project::Entity::KeyframeVec2>* keys = nullptr;
|
||||||
if (clip) {
|
if (clip) {
|
||||||
keys = &clip->toolLocationKeys[t.id];
|
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());
|
const bool hasCurve = (keys && !keys->isEmpty()) || (!t.locationKeys.isEmpty());
|
||||||
if (autoKeyLocation && currentFrame >= 0) {
|
if (autoKeyLocation && currentFrame >= 0) {
|
||||||
const QPointF sampled =
|
const QPointF sampled =
|
||||||
@@ -2207,8 +2605,16 @@ bool ProjectWorkspace::setEntityLocationKey(const QString& id, int frame, const
|
|||||||
}
|
}
|
||||||
Project::AnimationClip* clip = activeClipOrNull(m_project);
|
Project::AnimationClip* clip = activeClipOrNull(m_project);
|
||||||
if (!clip) return false;
|
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];
|
auto& keys = clip->entityLocationKeys[id];
|
||||||
upsertKey(keys, frame, originWorld);
|
upsertKey(keys, frame, keyValue);
|
||||||
return writeIndexJson();
|
return writeIndexJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2406,7 +2812,25 @@ QString ProjectWorkspace::copyIntoAssetsAsBackground(const QString& sourceFilePa
|
|||||||
const auto destAbs = QDir(assetsDir).filePath(fileName);
|
const auto destAbs = QDir(assetsDir).filePath(fileName);
|
||||||
const auto destRel = QString::fromUtf8(kAssetsDirName) + "/" + 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()) {
|
if (img.isNull()) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
namespace core {
|
namespace core {
|
||||||
|
|
||||||
@@ -95,6 +96,11 @@ public:
|
|||||||
bool moveToolBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation);
|
bool moveToolBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation);
|
||||||
bool addEntity(const Project::Entity& entity, const QImage& image);
|
bool addEntity(const Project::Entity& entity, const QImage& image);
|
||||||
bool setEntityVisible(const QString& id, bool on);
|
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 setEntityVisibilityKey(const QString& id, int frame, bool visible);
|
||||||
bool removeEntityVisibilityKey(const QString& id, int frame);
|
bool removeEntityVisibilityKey(const QString& id, int frame);
|
||||||
bool setEntityDisplayName(const QString& id, const QString& displayName);
|
bool setEntityDisplayName(const QString& id, const QString& displayName);
|
||||||
@@ -133,6 +139,8 @@ private:
|
|||||||
bool syncEntityPayloadsToDisk();
|
bool syncEntityPayloadsToDisk();
|
||||||
bool hydrateEntityPayloadsFromDisk();
|
bool hydrateEntityPayloadsFromDisk();
|
||||||
void loadV1LegacyAnimationSidecars();
|
void loadV1LegacyAnimationSidecars();
|
||||||
|
bool writeIndexJsonWithoutPayloadSync();
|
||||||
|
bool saveSingleEntityPayload(Project::Entity& entity);
|
||||||
|
|
||||||
static QJsonObject projectToJson(const Project& project);
|
static QJsonObject projectToJson(const Project& project);
|
||||||
static bool projectFromJson(const QJsonObject& root, Project& outProject, int* outFileVersion);
|
static bool projectFromJson(const QJsonObject& root, Project& outProject, int* outFileVersion);
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ set(GUI_SOURCES
|
|||||||
${GUI_ROOT}/dialogs/FrameAnimationDialog.cpp
|
${GUI_ROOT}/dialogs/FrameAnimationDialog.cpp
|
||||||
${GUI_ROOT}/dialogs/CancelableTaskDialog.cpp
|
${GUI_ROOT}/dialogs/CancelableTaskDialog.cpp
|
||||||
${GUI_ROOT}/dialogs/EntityFinalizeDialog.cpp
|
${GUI_ROOT}/dialogs/EntityFinalizeDialog.cpp
|
||||||
|
${GUI_ROOT}/dialogs/BlackholeResolveDialog.cpp
|
||||||
${GUI_ROOT}/dialogs/EntityIntroPopup.cpp
|
${GUI_ROOT}/dialogs/EntityIntroPopup.cpp
|
||||||
${GUI_ROOT}/editor/EditorCanvas.cpp
|
${GUI_ROOT}/editor/EditorCanvas.cpp
|
||||||
${GUI_ROOT}/editor/EntityCutoutUtils.cpp
|
${GUI_ROOT}/editor/EntityCutoutUtils.cpp
|
||||||
${GUI_ROOT}/params/ParamControls.cpp
|
${GUI_ROOT}/params/ParamControls.cpp
|
||||||
${GUI_ROOT}/props/BackgroundPropertySection.cpp
|
${GUI_ROOT}/props/BackgroundPropertySection.cpp
|
||||||
|
${GUI_ROOT}/props/BlackholePropertySection.cpp
|
||||||
${GUI_ROOT}/props/EntityPropertySection.cpp
|
${GUI_ROOT}/props/EntityPropertySection.cpp
|
||||||
${GUI_ROOT}/props/ToolPropertySection.cpp
|
${GUI_ROOT}/props/ToolPropertySection.cpp
|
||||||
${GUI_ROOT}/timeline/TimelineWidget.cpp
|
${GUI_ROOT}/timeline/TimelineWidget.cpp
|
||||||
@@ -34,11 +36,13 @@ set(GUI_HEADERS
|
|||||||
${GUI_ROOT}/dialogs/FrameAnimationDialog.h
|
${GUI_ROOT}/dialogs/FrameAnimationDialog.h
|
||||||
${GUI_ROOT}/dialogs/CancelableTaskDialog.h
|
${GUI_ROOT}/dialogs/CancelableTaskDialog.h
|
||||||
${GUI_ROOT}/dialogs/EntityFinalizeDialog.h
|
${GUI_ROOT}/dialogs/EntityFinalizeDialog.h
|
||||||
|
${GUI_ROOT}/dialogs/BlackholeResolveDialog.h
|
||||||
${GUI_ROOT}/dialogs/EntityIntroPopup.h
|
${GUI_ROOT}/dialogs/EntityIntroPopup.h
|
||||||
${GUI_ROOT}/editor/EditorCanvas.h
|
${GUI_ROOT}/editor/EditorCanvas.h
|
||||||
${GUI_ROOT}/editor/EntityCutoutUtils.h
|
${GUI_ROOT}/editor/EntityCutoutUtils.h
|
||||||
${GUI_ROOT}/params/ParamControls.h
|
${GUI_ROOT}/params/ParamControls.h
|
||||||
${GUI_ROOT}/props/BackgroundPropertySection.h
|
${GUI_ROOT}/props/BackgroundPropertySection.h
|
||||||
|
${GUI_ROOT}/props/BlackholePropertySection.h
|
||||||
${GUI_ROOT}/props/EntityPropertySection.h
|
${GUI_ROOT}/props/EntityPropertySection.h
|
||||||
${GUI_ROOT}/props/ToolPropertySection.h
|
${GUI_ROOT}/props/ToolPropertySection.h
|
||||||
${GUI_ROOT}/props/PropertySectionWidget.h
|
${GUI_ROOT}/props/PropertySectionWidget.h
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
#include "main_window/MainWindow.h"
|
#include "main_window/MainWindow.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
#include <QImageReader>
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
app.setApplicationName(QStringLiteral("landscape tool"));
|
app.setApplicationName(QStringLiteral("landscape tool"));
|
||||||
|
|
||||||
|
// 全局放宽 Qt 图片分配限制(默认常见为 256MB),否则超大分辨率背景/深度可能在任意加载路径被拒绝。
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
QImageReader::setAllocationLimit(1024); // MB
|
||||||
|
#endif
|
||||||
|
|
||||||
MainWindow window;
|
MainWindow window;
|
||||||
window.show();
|
window.show();
|
||||||
|
|
||||||
|
|||||||
175
client/gui/dialogs/BlackholeResolveDialog.cpp
Normal file
175
client/gui/dialogs/BlackholeResolveDialog.cpp
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
#include "dialogs/BlackholeResolveDialog.h"
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFrame>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QPushButton* makeAlgoButton(const QString& title, const QString& subtitle, QWidget* parent) {
|
||||||
|
auto* btn = new QPushButton(parent);
|
||||||
|
btn->setCheckable(false);
|
||||||
|
btn->setCursor(Qt::PointingHandCursor);
|
||||||
|
btn->setMinimumHeight(86);
|
||||||
|
btn->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||||
|
btn->setText(title + QStringLiteral("\n") + subtitle);
|
||||||
|
btn->setStyleSheet(
|
||||||
|
"QPushButton { text-align: left; padding: 10px 12px; border: 1px solid palette(mid); border-radius: 8px; }"
|
||||||
|
"QPushButton:hover { border-color: palette(highlight); }");
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
BlackholeResolveDialog::BlackholeResolveDialog(const QString& blackholeName, QWidget* parent)
|
||||||
|
: QDialog(parent),
|
||||||
|
m_blackholeName(blackholeName) {
|
||||||
|
setModal(true);
|
||||||
|
setMinimumSize(560, 420);
|
||||||
|
setWindowTitle(QStringLiteral("黑洞解决"));
|
||||||
|
|
||||||
|
auto* root = new QVBoxLayout(this);
|
||||||
|
m_pages = new QStackedWidget(this);
|
||||||
|
root->addWidget(m_pages, 1);
|
||||||
|
|
||||||
|
buildSelectPage();
|
||||||
|
buildDetailPage();
|
||||||
|
m_pages->setCurrentWidget(m_pageSelect);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlackholeResolveDialog::buildSelectPage() {
|
||||||
|
m_pageSelect = new QWidget(this);
|
||||||
|
auto* layout = new QVBoxLayout(m_pageSelect);
|
||||||
|
layout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
layout->setSpacing(12);
|
||||||
|
|
||||||
|
auto* title = new QLabel(QStringLiteral("第 1 步:选择黑洞解决算法"), m_pageSelect);
|
||||||
|
auto* sub = new QLabel(QStringLiteral("当前黑洞:%1").arg(m_blackholeName), m_pageSelect);
|
||||||
|
title->setStyleSheet("font-size: 18px; font-weight: 600;");
|
||||||
|
sub->setStyleSheet("color: palette(mid);");
|
||||||
|
layout->addWidget(title);
|
||||||
|
layout->addWidget(sub);
|
||||||
|
|
||||||
|
auto* btnCopy = makeAlgoButton(
|
||||||
|
QStringLiteral("复制背景其他区域"),
|
||||||
|
QStringLiteral("进入画布拖动取样框,直观选择复制来源。"),
|
||||||
|
m_pageSelect);
|
||||||
|
auto* btnOriginal = makeAlgoButton(
|
||||||
|
QStringLiteral("使用原始背景"),
|
||||||
|
QStringLiteral("撤销黑洞显示,恢复抠图前背景区域。"),
|
||||||
|
m_pageSelect);
|
||||||
|
layout->addWidget(btnCopy);
|
||||||
|
layout->addWidget(btnOriginal);
|
||||||
|
|
||||||
|
auto* modelNote = new QLabel(
|
||||||
|
QStringLiteral("模型补全:已预留接口,本版本暂不实现。"),
|
||||||
|
m_pageSelect);
|
||||||
|
modelNote->setStyleSheet("color: palette(mid);");
|
||||||
|
layout->addWidget(modelNote);
|
||||||
|
layout->addStretch(1);
|
||||||
|
|
||||||
|
auto* btns = new QDialogButtonBox(QDialogButtonBox::Cancel, m_pageSelect);
|
||||||
|
connect(btns, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
layout->addWidget(btns);
|
||||||
|
|
||||||
|
connect(btnCopy, &QPushButton::clicked, this, [this]() {
|
||||||
|
enterAlgorithmPage(Algorithm::CopyBackgroundRegion);
|
||||||
|
});
|
||||||
|
connect(btnOriginal, &QPushButton::clicked, this, [this]() {
|
||||||
|
enterAlgorithmPage(Algorithm::UseOriginalBackground);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_pages->addWidget(m_pageSelect);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlackholeResolveDialog::buildDetailPage() {
|
||||||
|
m_pageDetail = new QWidget(this);
|
||||||
|
auto* layout = new QVBoxLayout(m_pageDetail);
|
||||||
|
layout->setContentsMargins(8, 8, 8, 8);
|
||||||
|
layout->setSpacing(10);
|
||||||
|
|
||||||
|
m_detailTitle = new QLabel(m_pageDetail);
|
||||||
|
m_detailTitle->setStyleSheet("font-size: 18px; font-weight: 600;");
|
||||||
|
m_detailHint = new QLabel(m_pageDetail);
|
||||||
|
m_detailHint->setWordWrap(true);
|
||||||
|
m_detailHint->setStyleSheet("color: palette(mid);");
|
||||||
|
layout->addWidget(m_detailTitle);
|
||||||
|
layout->addWidget(m_detailHint);
|
||||||
|
|
||||||
|
m_algoDetails = new QStackedWidget(m_pageDetail);
|
||||||
|
|
||||||
|
// 详情页 A:复制背景其他区域(交互布局)
|
||||||
|
m_copyDetail = new QWidget(m_algoDetails);
|
||||||
|
{
|
||||||
|
auto* cLay = new QVBoxLayout(m_copyDetail);
|
||||||
|
cLay->setSpacing(8);
|
||||||
|
|
||||||
|
auto* panel = new QFrame(m_copyDetail);
|
||||||
|
panel->setFrameShape(QFrame::StyledPanel);
|
||||||
|
auto* pLay = new QVBoxLayout(panel);
|
||||||
|
pLay->setSpacing(8);
|
||||||
|
|
||||||
|
auto* tip = new QLabel(
|
||||||
|
QStringLiteral("说明:点击“应用”后进入画布拖动模式。\n在画布中拖动青色取样框,松开鼠标即可将该区域复制到黑洞位置并自动移除黑洞。"),
|
||||||
|
panel);
|
||||||
|
tip->setWordWrap(true);
|
||||||
|
tip->setStyleSheet("color: palette(mid);");
|
||||||
|
pLay->addWidget(tip);
|
||||||
|
cLay->addWidget(panel);
|
||||||
|
cLay->addStretch(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详情页 B:使用原始背景(确认布局)
|
||||||
|
m_originalDetail = new QWidget(m_algoDetails);
|
||||||
|
{
|
||||||
|
auto* oLay = new QVBoxLayout(m_originalDetail);
|
||||||
|
oLay->setSpacing(8);
|
||||||
|
auto* desc = new QLabel(
|
||||||
|
QStringLiteral("该方案不会改动背景像素文件,只会将黑洞切换为不显示,从而恢复原始背景区域。"),
|
||||||
|
m_originalDetail);
|
||||||
|
desc->setWordWrap(true);
|
||||||
|
auto* note = new QLabel(
|
||||||
|
QStringLiteral("适用场景:当前黑洞区域无需二次修补,只需恢复抠图前背景;应用后黑洞会自动移除。"),
|
||||||
|
m_originalDetail);
|
||||||
|
note->setWordWrap(true);
|
||||||
|
note->setStyleSheet("color: palette(mid);");
|
||||||
|
oLay->addWidget(desc);
|
||||||
|
oLay->addWidget(note);
|
||||||
|
oLay->addStretch(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_algoDetails->addWidget(m_copyDetail);
|
||||||
|
m_algoDetails->addWidget(m_originalDetail);
|
||||||
|
layout->addWidget(m_algoDetails, 1);
|
||||||
|
|
||||||
|
auto* btns = new QDialogButtonBox(m_pageDetail);
|
||||||
|
auto* btnBack = btns->addButton(QStringLiteral("上一步"), QDialogButtonBox::ActionRole);
|
||||||
|
auto* btnApply = btns->addButton(QStringLiteral("应用"), QDialogButtonBox::AcceptRole);
|
||||||
|
auto* btnCancel = btns->addButton(QDialogButtonBox::Cancel);
|
||||||
|
connect(btnBack, &QPushButton::clicked, this, [this]() {
|
||||||
|
m_pages->setCurrentWidget(m_pageSelect);
|
||||||
|
});
|
||||||
|
connect(btnApply, &QPushButton::clicked, this, &QDialog::accept);
|
||||||
|
connect(btnCancel, &QPushButton::clicked, this, &QDialog::reject);
|
||||||
|
layout->addWidget(btns);
|
||||||
|
|
||||||
|
m_pages->addWidget(m_pageDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlackholeResolveDialog::enterAlgorithmPage(Algorithm algo) {
|
||||||
|
m_selectedAlgorithm = algo;
|
||||||
|
if (algo == Algorithm::CopyBackgroundRegion) {
|
||||||
|
m_detailTitle->setText(QStringLiteral("第 2 步:复制背景其他区域"));
|
||||||
|
m_detailHint->setText(QStringLiteral("准备进入画布拖动取样框模式。"));
|
||||||
|
m_algoDetails->setCurrentWidget(m_copyDetail);
|
||||||
|
} else {
|
||||||
|
m_detailTitle->setText(QStringLiteral("第 2 步:使用原始背景"));
|
||||||
|
m_detailHint->setText(QStringLiteral("确认后将切换为原始背景显示。"));
|
||||||
|
m_algoDetails->setCurrentWidget(m_originalDetail);
|
||||||
|
}
|
||||||
|
m_pages->setCurrentWidget(m_pageDetail);
|
||||||
|
}
|
||||||
|
|
||||||
42
client/gui/dialogs/BlackholeResolveDialog.h
Normal file
42
client/gui/dialogs/BlackholeResolveDialog.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
class QStackedWidget;
|
||||||
|
|
||||||
|
class BlackholeResolveDialog final : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
enum class Algorithm {
|
||||||
|
CopyBackgroundRegion,
|
||||||
|
UseOriginalBackground,
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit BlackholeResolveDialog(const QString& blackholeName, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void buildSelectPage();
|
||||||
|
void buildDetailPage();
|
||||||
|
void enterAlgorithmPage(Algorithm algo);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_blackholeName;
|
||||||
|
Algorithm m_selectedAlgorithm = Algorithm::UseOriginalBackground;
|
||||||
|
|
||||||
|
QStackedWidget* m_pages = nullptr;
|
||||||
|
|
||||||
|
QWidget* m_pageSelect = nullptr;
|
||||||
|
QWidget* m_pageDetail = nullptr;
|
||||||
|
|
||||||
|
QLabel* m_detailTitle = nullptr;
|
||||||
|
QLabel* m_detailHint = nullptr;
|
||||||
|
QStackedWidget* m_algoDetails = nullptr;
|
||||||
|
|
||||||
|
QWidget* m_copyDetail = nullptr;
|
||||||
|
|
||||||
|
QWidget* m_originalDetail = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
|
#include <QImageReader>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
@@ -171,7 +172,25 @@ void FrameAnimationDialog::updatePreviewForFrame(int frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool FrameAnimationDialog::applyImageToFrame(int frame, const QString& absImagePath) {
|
bool FrameAnimationDialog::applyImageToFrame(int frame, const QString& absImagePath) {
|
||||||
QImage img(absImagePath);
|
// Qt 默认的 image allocation limit 较小,超大分辨率图可能会被拒绝。
|
||||||
|
// 这里提高 limit,并对极端大图按像素数上限自动缩放,避免 OOM。
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
QImageReader::setAllocationLimit(1024); // MB
|
||||||
|
#endif
|
||||||
|
QImageReader reader(absImagePath);
|
||||||
|
reader.setAutoTransform(true);
|
||||||
|
const QSize sz = reader.size();
|
||||||
|
if (sz.isValid()) {
|
||||||
|
constexpr qint64 kMaxPixels = 120LL * 1000LL * 1000LL; // 120MP
|
||||||
|
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()) {
|
if (img.isNull()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QMouseEvent>
|
#include <QMouseEvent>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QImageReader>
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
|
|
||||||
class ImageCropDialog::CropView final : public QWidget {
|
class ImageCropDialog::CropView final : public QWidget {
|
||||||
@@ -159,7 +160,25 @@ ImageCropDialog::ImageCropDialog(const QString& imagePath, QWidget* parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ImageCropDialog::loadImageOrClose() {
|
void ImageCropDialog::loadImageOrClose() {
|
||||||
m_image = QImage(m_imagePath);
|
// Qt 默认的 image allocation limit 较小(常见为 256MB),超大分辨率图会被拒绝。
|
||||||
|
// 这里用 QImageReader 并提高 limit;同时对极端大图按像素数上限自动缩放,避免 OOM。
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
QImageReader::setAllocationLimit(1024); // MB
|
||||||
|
#endif
|
||||||
|
QImageReader reader(m_imagePath);
|
||||||
|
reader.setAutoTransform(true);
|
||||||
|
const QSize sz = reader.size();
|
||||||
|
if (sz.isValid()) {
|
||||||
|
constexpr qint64 kMaxPixels = 120LL * 1000LL * 1000LL; // 120MP
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_image = reader.read();
|
||||||
if (m_image.isNull()) {
|
if (m_image.isNull()) {
|
||||||
reject();
|
reject();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QImageReader>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QDragEnterEvent>
|
#include <QDragEnterEvent>
|
||||||
#include <QDropEvent>
|
#include <QDropEvent>
|
||||||
@@ -37,6 +38,51 @@ constexpr int kMinStrokePointsSam = 4;
|
|||||||
constexpr int kMinStrokePointsManual = 8;
|
constexpr int kMinStrokePointsManual = 8;
|
||||||
constexpr int kMaxSamPointPrompts = 32;
|
constexpr int kMaxSamPointPrompts = 32;
|
||||||
|
|
||||||
|
static QImage readImageTolerant(const QString& absPath) {
|
||||||
|
if (absPath.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
// Qt 默认限制常见为 256MB;超大分辨率背景/深度可能会被拒绝。
|
||||||
|
QImageReader::setAllocationLimit(1024); // MB
|
||||||
|
#endif
|
||||||
|
QImageReader reader(absPath);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reader.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
QRectF clampRectTopLeftToBounds(const QRectF& rect, const QRectF& bounds) {
|
||||||
|
if (rect.isNull() || bounds.isNull()) {
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
QRectF out = rect;
|
||||||
|
if (out.width() > bounds.width()) {
|
||||||
|
out.setWidth(bounds.width());
|
||||||
|
}
|
||||||
|
if (out.height() > bounds.height()) {
|
||||||
|
out.setHeight(bounds.height());
|
||||||
|
}
|
||||||
|
QPointF tl = out.topLeft();
|
||||||
|
if (tl.x() < bounds.left()) tl.setX(bounds.left());
|
||||||
|
if (tl.y() < bounds.top()) tl.setY(bounds.top());
|
||||||
|
if (tl.x() + out.width() > bounds.right()) tl.setX(bounds.right() - out.width());
|
||||||
|
if (tl.y() + out.height() > bounds.bottom()) tl.setY(bounds.bottom() - out.height());
|
||||||
|
out.moveTopLeft(tl);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
QVector<QPointF> snapStrokeToEdges(const QVector<QPointF>& strokeWorld, const QImage& bgImage, int searchRadiusPx) {
|
QVector<QPointF> snapStrokeToEdges(const QVector<QPointF>& strokeWorld, const QImage& bgImage, int searchRadiusPx) {
|
||||||
if (strokeWorld.size() < 3 || bgImage.isNull()) {
|
if (strokeWorld.size() < 3 || bgImage.isNull()) {
|
||||||
return strokeWorld;
|
return strokeWorld;
|
||||||
@@ -100,6 +146,12 @@ bool buildSamSegmentPayloadFromStroke(
|
|||||||
if (strokeWorld.size() < kMinStrokePointsSam || bgImage.isNull()) {
|
if (strokeWorld.size() < kMinStrokePointsSam || bgImage.isNull()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
outCropPng.clear();
|
||||||
|
outOverlayPng.clear();
|
||||||
|
outPointCoords = QJsonArray{};
|
||||||
|
outPointLabels = QJsonArray{};
|
||||||
|
outBoxXyxy = QJsonArray{};
|
||||||
|
|
||||||
const QRectF polyBr = QPolygonF(strokeWorld).boundingRect();
|
const QRectF polyBr = QPolygonF(strokeWorld).boundingRect();
|
||||||
if (polyBr.isEmpty()) {
|
if (polyBr.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -148,30 +200,35 @@ bool buildSamSegmentPayloadFromStroke(
|
|||||||
}
|
}
|
||||||
bufOv.close();
|
bufOv.close();
|
||||||
|
|
||||||
|
// 关键修复:
|
||||||
|
// 用户“圈选”通常是在实体外侧画一圈。原实现把笔画点全当作前景点(1),
|
||||||
|
// 会让 SAM 倾向于把圈线/裁剪边缘当成前景,从而出现“沿小块图像边缘贴边”的 mask。
|
||||||
|
// 新策略:圈内给一个前景点(1),圈线采样一些背景点(0)抑制外侧区域。
|
||||||
|
auto clampD = [](double v, double lo, double hi) { return std::clamp(v, lo, hi); };
|
||||||
|
|
||||||
|
// 前景点:取笔画包围盒中心(通常落在圈内),并限制在裁剪范围内。
|
||||||
|
const QPointF centerWorld = QPolygonF(strokeWorld).boundingRect().center();
|
||||||
|
const QPointF centerLocal = centerWorld - origin;
|
||||||
|
const double fgx = clampD(centerLocal.x(), 0.0, static_cast<double>(cw - 1));
|
||||||
|
const double fgy = clampD(centerLocal.y(), 0.0, static_cast<double>(ch - 1));
|
||||||
|
outPointCoords.append(QJsonArray{fgx, fgy});
|
||||||
|
outPointLabels.append(1);
|
||||||
|
|
||||||
|
// 背景点:在圈线(polyline)上均匀采样(最多 kMaxSamPointPrompts-1 个)。
|
||||||
const int n = static_cast<int>(strokeWorld.size());
|
const int n = static_cast<int>(strokeWorld.size());
|
||||||
const int step = std::max(1, (n + kMaxSamPointPrompts - 1) / kMaxSamPointPrompts);
|
const int maxBg = std::max(0, kMaxSamPointPrompts - 1);
|
||||||
for (int i = 0; i < n; i += step) {
|
if (n >= 2 && maxBg > 0) {
|
||||||
const QPointF L = strokeWorld[i] - origin;
|
const int step = std::max(1, (n + maxBg - 1) / maxBg);
|
||||||
outPointCoords.append(QJsonArray{L.x(), L.y()});
|
for (int i = 0; i < n; i += step) {
|
||||||
outPointLabels.append(1);
|
const QPointF L = strokeWorld[i] - origin;
|
||||||
}
|
const double bx = clampD(L.x(), 0.0, static_cast<double>(cw - 1));
|
||||||
if (n > 1) {
|
const double by = clampD(L.y(), 0.0, static_cast<double>(ch - 1));
|
||||||
const QPointF Llast = strokeWorld.last() - origin;
|
outPointCoords.append(QJsonArray{bx, by});
|
||||||
if (!outPointCoords.isEmpty()) {
|
outPointLabels.append(0);
|
||||||
const QJsonArray prev = outPointCoords.last().toArray();
|
|
||||||
if (prev.size() >= 2) {
|
|
||||||
const double dx = Llast.x() - prev.at(0).toDouble();
|
|
||||||
const double dy = Llast.y() - prev.at(1).toDouble();
|
|
||||||
if (dx * dx + dy * dy > 4.0) {
|
|
||||||
outPointCoords.append(QJsonArray{Llast.x(), Llast.y()});
|
|
||||||
outPointLabels.append(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const QRectF tight = QPolygonF(strokeWorld).boundingRect();
|
const QRectF tight = QPolygonF(strokeWorld).boundingRect();
|
||||||
auto clampD = [](double v, double lo, double hi) { return std::clamp(v, lo, hi); };
|
|
||||||
double x1 = clampD(tight.left() - origin.x(), 0.0, static_cast<double>(cw - 1));
|
double x1 = clampD(tight.left() - origin.x(), 0.0, static_cast<double>(cw - 1));
|
||||||
double y1 = clampD(tight.top() - origin.y(), 0.0, static_cast<double>(ch - 1));
|
double y1 = clampD(tight.top() - origin.y(), 0.0, static_cast<double>(ch - 1));
|
||||||
double x2 = clampD(tight.right() - origin.x(), 0.0, static_cast<double>(cw - 1));
|
double x2 = clampD(tight.right() - origin.x(), 0.0, static_cast<double>(cw - 1));
|
||||||
@@ -479,9 +536,11 @@ void EditorCanvas::setPresentationPreviewMode(bool on) {
|
|||||||
m_presZoomFinishingRestore = false;
|
m_presZoomFinishingRestore = false;
|
||||||
m_presBgPanSession = false;
|
m_presBgPanSession = false;
|
||||||
m_presBgDragDist = 0.0;
|
m_presBgDragDist = 0.0;
|
||||||
|
cancelBlackholeCopyResolve();
|
||||||
if (on) {
|
if (on) {
|
||||||
m_tool = Tool::Move;
|
m_tool = Tool::Move;
|
||||||
m_selectedEntity = -1;
|
m_selectedEntity = -1;
|
||||||
|
m_selectedBlackholeEntityId.clear();
|
||||||
m_draggingEntity = false;
|
m_draggingEntity = false;
|
||||||
m_drawingEntity = false;
|
m_drawingEntity = false;
|
||||||
m_dragMode = DragMode::None;
|
m_dragMode = DragMode::None;
|
||||||
@@ -505,7 +564,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
|||||||
if (!m_depthAbsPath.isEmpty()) {
|
if (!m_depthAbsPath.isEmpty()) {
|
||||||
if (m_depthDirty) {
|
if (m_depthDirty) {
|
||||||
m_depthDirty = false;
|
m_depthDirty = false;
|
||||||
QImage img(m_depthAbsPath);
|
QImage img = readImageTolerant(m_depthAbsPath);
|
||||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,6 +580,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
|||||||
const QPointF originWorld = e.originWorld;
|
const QPointF originWorld = e.originWorld;
|
||||||
v.animatedOriginWorld = originWorld;
|
v.animatedOriginWorld = originWorld;
|
||||||
v.cutoutPolygonWorld = e.cutoutPolygonWorld;
|
v.cutoutPolygonWorld = e.cutoutPolygonWorld;
|
||||||
|
v.blackholeVisible = e.blackholeVisible;
|
||||||
v.distanceScaleCalibMult = e.distanceScaleCalibMult;
|
v.distanceScaleCalibMult = e.distanceScaleCalibMult;
|
||||||
v.ignoreDistanceScale = e.ignoreDistanceScale;
|
v.ignoreDistanceScale = e.ignoreDistanceScale;
|
||||||
|
|
||||||
@@ -597,6 +657,31 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
|||||||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!m_selectedBlackholeEntityId.isEmpty()) {
|
||||||
|
bool exists = false;
|
||||||
|
for (const auto& ent : m_entities) {
|
||||||
|
if (ent.id == m_selectedBlackholeEntityId && !ent.cutoutPolygonWorld.isEmpty()) {
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!exists) {
|
||||||
|
m_selectedBlackholeEntityId.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_blackholeCopyResolveActive) {
|
||||||
|
bool exists = false;
|
||||||
|
for (const auto& ent : m_entities) {
|
||||||
|
if (ent.id == m_blackholeCopyEntityId && !ent.cutoutPolygonWorld.isEmpty()) {
|
||||||
|
exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!exists) {
|
||||||
|
cancelBlackholeCopyResolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_bgCutoutDirty = true;
|
m_bgCutoutDirty = true;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@@ -815,10 +900,119 @@ void EditorCanvas::selectEntityById(const QString& id) {
|
|||||||
clearEntitySelection();
|
clearEntitySelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EditorCanvas::selectBlackholeByEntityId(const QString& entityId) {
|
||||||
|
if (entityId.isEmpty()) {
|
||||||
|
clearBlackholeSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto& ent : m_entities) {
|
||||||
|
if (ent.id == entityId && !ent.cutoutPolygonWorld.isEmpty()) {
|
||||||
|
if (m_selectedBlackholeEntityId == entityId) {
|
||||||
|
update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_selectedBlackholeEntityId = entityId;
|
||||||
|
update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clearBlackholeSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditorCanvas::clearBlackholeSelection() {
|
||||||
|
if (m_blackholeCopyResolveActive) {
|
||||||
|
cancelBlackholeCopyResolve();
|
||||||
|
}
|
||||||
|
if (m_selectedBlackholeEntityId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_selectedBlackholeEntityId.clear();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EditorCanvas::startBlackholeCopyResolve(const QString& entityId) {
|
||||||
|
if (entityId.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const Entity* hit = nullptr;
|
||||||
|
for (const auto& ent : m_entities) {
|
||||||
|
if (ent.id == entityId && !ent.cutoutPolygonWorld.isEmpty()) {
|
||||||
|
hit = &ent;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hit) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ensurePixmapLoaded();
|
||||||
|
if (m_bgImageDirty) {
|
||||||
|
m_bgImageDirty = false;
|
||||||
|
m_bgImage = readImageTolerant(m_bgAbsPath);
|
||||||
|
if (m_bgImage.format() != QImage::Format_ARGB32_Premultiplied && !m_bgImage.isNull()) {
|
||||||
|
m_bgImage = m_bgImage.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const QRectF bg = worldRectOfBackground();
|
||||||
|
if (bg.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRectF holeRect = entity_cutout::pathFromWorldPolygon(hit->cutoutPolygonWorld).boundingRect();
|
||||||
|
if (holeRect.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (holeRect.width() < 1.0 || holeRect.height() < 1.0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
holeRect = holeRect.intersected(bg);
|
||||||
|
if (holeRect.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRectF srcRect(holeRect);
|
||||||
|
const qreal shift = std::max<qreal>(24.0, holeRect.width() * 0.6);
|
||||||
|
srcRect.translate(shift, 0.0);
|
||||||
|
srcRect = clampRectTopLeftToBounds(srcRect, bg);
|
||||||
|
|
||||||
|
m_selectedBlackholeEntityId = entityId;
|
||||||
|
m_blackholeCopyResolveActive = true;
|
||||||
|
m_blackholeCopyEntityId = entityId;
|
||||||
|
m_blackholeCopyHoleRect = holeRect;
|
||||||
|
m_blackholeCopySourceRect = srcRect;
|
||||||
|
m_blackholeCopyDragging = false;
|
||||||
|
m_blackholeCopyDragOffset = QPointF();
|
||||||
|
updateCursor();
|
||||||
|
update();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditorCanvas::cancelBlackholeCopyResolve() {
|
||||||
|
if (!m_blackholeCopyResolveActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_blackholeCopyResolveActive = false;
|
||||||
|
m_blackholeCopyEntityId.clear();
|
||||||
|
m_blackholeCopyHoleRect = QRectF();
|
||||||
|
m_blackholeCopySourceRect = QRectF();
|
||||||
|
m_blackholeCopyDragging = false;
|
||||||
|
m_blackholeCopyDragOffset = QPointF();
|
||||||
|
updateCursor();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EditorCanvas::notifyBackgroundContentChanged() {
|
||||||
|
// 背景路径不变时,setBackgroundImagePath 不会触发刷新;这里显式让 pixmap/image 缓存失效并重载。
|
||||||
|
invalidatePixmap();
|
||||||
|
m_bgImageDirty = true;
|
||||||
|
m_bgCutoutDirty = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
void EditorCanvas::setBackgroundImagePath(const QString& absolutePath) {
|
void EditorCanvas::setBackgroundImagePath(const QString& absolutePath) {
|
||||||
if (m_bgAbsPath == absolutePath) {
|
if (m_bgAbsPath == absolutePath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
cancelBlackholeCopyResolve();
|
||||||
m_bgAbsPath = absolutePath;
|
m_bgAbsPath = absolutePath;
|
||||||
invalidatePixmap();
|
invalidatePixmap();
|
||||||
m_bgImageDirty = true;
|
m_bgImageDirty = true;
|
||||||
@@ -997,13 +1191,21 @@ void EditorCanvas::ensurePixmapLoaded() const {
|
|||||||
m_pixmapDirty = false;
|
m_pixmapDirty = false;
|
||||||
m_bgPixmap = QPixmap();
|
m_bgPixmap = QPixmap();
|
||||||
if (!m_bgAbsPath.isEmpty()) {
|
if (!m_bgAbsPath.isEmpty()) {
|
||||||
m_bgPixmap = QPixmap(m_bgAbsPath);
|
// 避免直接 QPixmap(path) 走默认 imageio 限制(超大图可能被 256MB 上限拒绝)
|
||||||
|
const QImage img = readImageTolerant(m_bgAbsPath);
|
||||||
|
if (!img.isNull()) {
|
||||||
|
m_bgPixmap = QPixmap::fromImage(img);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m_bgImageDirty = true;
|
m_bgImageDirty = true;
|
||||||
m_bgCutoutDirty = true;
|
m_bgCutoutDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EditorCanvas::updateCursor() {
|
void EditorCanvas::updateCursor() {
|
||||||
|
if (m_blackholeCopyResolveActive) {
|
||||||
|
setCursor(m_blackholeCopyDragging ? Qt::ClosedHandCursor : Qt::OpenHandCursor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (m_presentationPreviewMode) {
|
if (m_presentationPreviewMode) {
|
||||||
if (m_presHoverEntityIndex >= 0) {
|
if (m_presHoverEntityIndex >= 0) {
|
||||||
setCursor(Qt::PointingHandCursor);
|
setCursor(Qt::PointingHandCursor);
|
||||||
@@ -1105,7 +1307,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
|||||||
if (showBg) {
|
if (showBg) {
|
||||||
if (m_bgImageDirty) {
|
if (m_bgImageDirty) {
|
||||||
m_bgImageDirty = false;
|
m_bgImageDirty = false;
|
||||||
m_bgImage = QImage(m_bgAbsPath);
|
m_bgImage = readImageTolerant(m_bgAbsPath);
|
||||||
if (m_bgImage.format() != QImage::Format_ARGB32_Premultiplied && !m_bgImage.isNull()) {
|
if (m_bgImage.format() != QImage::Format_ARGB32_Premultiplied && !m_bgImage.isNull()) {
|
||||||
m_bgImage = m_bgImage.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
m_bgImage = m_bgImage.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
}
|
}
|
||||||
@@ -1115,7 +1317,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
|||||||
m_bgCutoutDirty = false;
|
m_bgCutoutDirty = false;
|
||||||
m_bgImageCutout = m_bgImage;
|
m_bgImageCutout = m_bgImage;
|
||||||
for (const auto& ent : m_entities) {
|
for (const auto& ent : m_entities) {
|
||||||
if (ent.opacity > 0.001 && !ent.cutoutPolygonWorld.isEmpty() && !m_bgImageCutout.isNull()) {
|
if (ent.blackholeVisible && !ent.cutoutPolygonWorld.isEmpty() && !m_bgImageCutout.isNull()) {
|
||||||
entity_cutout::applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
|
entity_cutout::applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1156,7 +1358,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
|||||||
if (wantDepth) {
|
if (wantDepth) {
|
||||||
if (m_depthDirty) {
|
if (m_depthDirty) {
|
||||||
m_depthDirty = false;
|
m_depthDirty = false;
|
||||||
QImage img(m_depthAbsPath);
|
QImage img = readImageTolerant(m_depthAbsPath);
|
||||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||||
}
|
}
|
||||||
if (!m_depthImage8.isNull()) {
|
if (!m_depthImage8.isNull()) {
|
||||||
@@ -1173,6 +1375,48 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!m_presentationPreviewMode && m_blackholeCopyResolveActive &&
|
||||||
|
!m_blackholeCopyHoleRect.isNull() && !m_blackholeCopySourceRect.isNull()) {
|
||||||
|
if (!m_bgImage.isNull()) {
|
||||||
|
const QRect srcRect = m_blackholeCopySourceRect.toAlignedRect();
|
||||||
|
const QRect dstRect = m_blackholeCopyHoleRect.toAlignedRect();
|
||||||
|
if (srcRect.isValid() && dstRect.isValid()) {
|
||||||
|
QPainterPath holePath;
|
||||||
|
for (const auto& ent : m_entities) {
|
||||||
|
if (ent.id == m_blackholeCopyEntityId && !ent.cutoutPolygonWorld.isEmpty()) {
|
||||||
|
holePath = entity_cutout::pathFromWorldPolygon(ent.cutoutPolygonWorld);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.save();
|
||||||
|
if (!holePath.isEmpty()) {
|
||||||
|
p.setClipPath(holePath);
|
||||||
|
}
|
||||||
|
p.setOpacity(0.75);
|
||||||
|
p.drawImage(dstRect.topLeft(), m_bgImage, srcRect);
|
||||||
|
p.setOpacity(1.0);
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
QPen holePen(QColor(255, 120, 0, 220), 2.0 / std::max<qreal>(m_scale, 0.001));
|
||||||
|
holePen.setStyle(Qt::DashLine);
|
||||||
|
p.setPen(holePen);
|
||||||
|
p.drawRect(m_blackholeCopyHoleRect);
|
||||||
|
|
||||||
|
QPen srcPen(QColor(70, 200, 255, 230), 2.0 / std::max<qreal>(m_scale, 0.001));
|
||||||
|
p.setPen(srcPen);
|
||||||
|
p.drawRect(m_blackholeCopySourceRect);
|
||||||
|
|
||||||
|
const QPen textPen(QColor(70, 200, 255, 230));
|
||||||
|
p.setPen(textPen);
|
||||||
|
const QPointF textPos = m_blackholeCopySourceRect.topLeft() +
|
||||||
|
QPointF(4.0 / std::max<qreal>(m_scale, 0.001),
|
||||||
|
14.0 / std::max<qreal>(m_scale, 0.001));
|
||||||
|
p.drawText(textPos, QStringLiteral("拖动取样框,松手应用"));
|
||||||
|
}
|
||||||
|
|
||||||
// 实体元素(占位):后续可替换为真实数据
|
// 实体元素(占位):后续可替换为真实数据
|
||||||
for (int i = 0; i < m_entities.size(); ++i) {
|
for (int i = 0; i < m_entities.size(); ++i) {
|
||||||
const auto& ent = m_entities[i];
|
const auto& ent = m_entities[i];
|
||||||
@@ -1280,6 +1524,13 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
|||||||
p.drawRect(ent.rect.adjusted(-2, -2, 2, 2));
|
p.drawRect(ent.rect.adjusted(-2, -2, 2, 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!m_presentationPreviewMode && ent.id == m_selectedBlackholeEntityId && !ent.cutoutPolygonWorld.isEmpty()) {
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
QPen holePen(QColor(70, 200, 255, 230), 2.2 / std::max<qreal>(m_scale, 0.001));
|
||||||
|
holePen.setStyle(Qt::DashLine);
|
||||||
|
p.setPen(holePen);
|
||||||
|
p.drawPath(entity_cutout::pathFromWorldPolygon(ent.cutoutPolygonWorld));
|
||||||
|
}
|
||||||
if (m_presentationPreviewMode && ent.opacity > 0.001) {
|
if (m_presentationPreviewMode && ent.opacity > 0.001) {
|
||||||
const bool showHover = (i == m_presHoverEntityIndex);
|
const bool showHover = (i == m_presHoverEntityIndex);
|
||||||
const bool showFocus = (i == m_presFocusedEntityIndex);
|
const bool showFocus = (i == m_presFocusedEntityIndex);
|
||||||
@@ -1398,7 +1649,47 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
|||||||
p.drawLine(QPointF(0, axisY), QPointF(width(), axisY)); // X 轴(水平)
|
p.drawLine(QPointF(0, axisY), QPointF(width(), axisY)); // X 轴(水平)
|
||||||
p.drawLine(QPointF(axisX, 0), QPointF(axisX, height())); // Y 轴(垂直)
|
p.drawLine(QPointF(axisX, 0), QPointF(axisX, height())); // Y 轴(垂直)
|
||||||
|
|
||||||
const int step = 100;
|
// 根据缩放与视口大小动态调整刻度密度:使相邻刻度在屏幕上保持“够密但不挤”的间距
|
||||||
|
auto niceStep = [](double raw) -> double {
|
||||||
|
if (!(raw > 0.0) || !std::isfinite(raw)) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
const double p10 = std::pow(10.0, std::floor(std::log10(raw)));
|
||||||
|
const double m = raw / p10; // 1..10
|
||||||
|
double n = 1.0;
|
||||||
|
if (m <= 1.0) n = 1.0;
|
||||||
|
else if (m <= 2.0) n = 2.0;
|
||||||
|
else if (m <= 5.0) n = 5.0;
|
||||||
|
else n = 10.0;
|
||||||
|
return n * p10;
|
||||||
|
};
|
||||||
|
|
||||||
|
const double sView = std::max<double>(0.001, double(m_scale));
|
||||||
|
const double targetTickPx = 86.0; // 目标:相邻刻度约 86px
|
||||||
|
const double minTickPx = 44.0; // 太密则自动放大 step
|
||||||
|
const double stepWorld = std::max(1.0, niceStep(targetTickPx / sView));
|
||||||
|
const double stepPx = stepWorld * sView;
|
||||||
|
const int labelEvery = (stepPx < minTickPx) ? int(std::ceil(minTickPx / std::max(1.0, stepPx))) : 1;
|
||||||
|
|
||||||
|
auto visibleWorldXRange = [&]() -> std::pair<double, double> {
|
||||||
|
const QPointF w0 = viewToWorld(QPointF(0, 0));
|
||||||
|
const QPointF w1 = viewToWorld(QPointF(width(), height()));
|
||||||
|
double a = std::min<double>(w0.x(), w1.x());
|
||||||
|
double b = std::max<double>(w0.x(), w1.x());
|
||||||
|
a = std::clamp(a, 0.0, double(m_bgPixmap.width()));
|
||||||
|
b = std::clamp(b, 0.0, double(m_bgPixmap.width()));
|
||||||
|
return {a, b};
|
||||||
|
};
|
||||||
|
auto visibleWorldYRange = [&]() -> std::pair<double, double> {
|
||||||
|
const QPointF w0 = viewToWorld(QPointF(0, 0));
|
||||||
|
const QPointF w1 = viewToWorld(QPointF(width(), height()));
|
||||||
|
double a = std::min<double>(w0.y(), w1.y());
|
||||||
|
double b = std::max<double>(w0.y(), w1.y());
|
||||||
|
a = std::clamp(a, 0.0, double(m_bgPixmap.height()));
|
||||||
|
b = std::clamp(b, 0.0, double(m_bgPixmap.height()));
|
||||||
|
return {a, b};
|
||||||
|
};
|
||||||
|
|
||||||
QPen tickPen(QColor(20, 20, 20, 140));
|
QPen tickPen(QColor(20, 20, 20, 140));
|
||||||
tickPen.setWidth(1);
|
tickPen.setWidth(1);
|
||||||
p.setPen(tickPen);
|
p.setPen(tickPen);
|
||||||
@@ -1409,28 +1700,44 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// X 轴:用 y=0 的世界线映射到 view-x,并把刻度画在 axisY 上
|
// X 轴:用 y=0 的世界线映射到 view-x,并把刻度画在 axisY 上
|
||||||
for (int x = 0; x <= m_bgPixmap.width(); x += step) {
|
{
|
||||||
const QPointF vx = worldToView(QPointF(x, 0));
|
const auto [xmin, xmax] = visibleWorldXRange();
|
||||||
|
const double start = std::floor(xmin / stepWorld) * stepWorld;
|
||||||
|
int iTick = 0;
|
||||||
|
for (double x = start; x <= xmax + 1e-9; x += stepWorld, ++iTick) {
|
||||||
|
const double xc = std::clamp(x, 0.0, double(m_bgPixmap.width()));
|
||||||
|
const QPointF vx = worldToView(QPointF(xc, 0));
|
||||||
if (vx.x() < -50 || vx.x() > width() + 50) {
|
if (vx.x() < -50 || vx.x() > width() + 50) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const qreal tx = std::clamp(vx.x(), 0.0, static_cast<qreal>(width()));
|
const qreal tx = std::clamp(vx.x(), 0.0, static_cast<qreal>(width()));
|
||||||
p.drawLine(QPointF(tx, axisY), QPointF(tx, axisY + 6));
|
p.drawLine(QPointF(tx, axisY), QPointF(tx, axisY + 6));
|
||||||
if (m_axisLabelsVisible) {
|
if (m_axisLabelsVisible) {
|
||||||
p.drawText(QPointF(tx + 2, axisY + 18), QString::number(x));
|
if (labelEvery <= 1 || (iTick % labelEvery) == 0) {
|
||||||
|
p.drawText(QPointF(tx + 2, axisY + 18), QString::number(int(std::lround(xc))));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Y 轴:用 x=0 的世界线映射到 view-y,并把刻度画在 axisX 上
|
// Y 轴:用 x=0 的世界线映射到 view-y,并把刻度画在 axisX 上
|
||||||
for (int y = 0; y <= m_bgPixmap.height(); y += step) {
|
{
|
||||||
const QPointF vy = worldToView(QPointF(0, y));
|
const auto [ymin, ymax] = visibleWorldYRange();
|
||||||
|
const double start = std::floor(ymin / stepWorld) * stepWorld;
|
||||||
|
int iTick = 0;
|
||||||
|
for (double y = start; y <= ymax + 1e-9; y += stepWorld, ++iTick) {
|
||||||
|
const double yc = std::clamp(y, 0.0, double(m_bgPixmap.height()));
|
||||||
|
const QPointF vy = worldToView(QPointF(0, yc));
|
||||||
if (vy.y() < -50 || vy.y() > height() + 50) {
|
if (vy.y() < -50 || vy.y() > height() + 50) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const qreal ty = std::clamp(vy.y(), 0.0, static_cast<qreal>(height()));
|
const qreal ty = std::clamp(vy.y(), 0.0, static_cast<qreal>(height()));
|
||||||
p.drawLine(QPointF(axisX, ty), QPointF(axisX + 6, ty));
|
p.drawLine(QPointF(axisX, ty), QPointF(axisX + 6, ty));
|
||||||
if (m_axisLabelsVisible) {
|
if (m_axisLabelsVisible) {
|
||||||
p.drawText(QPointF(axisX + 10, ty - 2), QString::number(y));
|
if (labelEvery <= 1 || (iTick % labelEvery) == 0) {
|
||||||
|
p.drawText(QPointF(axisX + 10, ty - 2), QString::number(int(std::lround(yc))));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1489,7 +1796,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
|||||||
if (!m_depthAbsPath.isEmpty()) {
|
if (!m_depthAbsPath.isEmpty()) {
|
||||||
if (m_depthDirty) {
|
if (m_depthDirty) {
|
||||||
m_depthDirty = false;
|
m_depthDirty = false;
|
||||||
QImage img(m_depthAbsPath);
|
QImage img = readImageTolerant(m_depthAbsPath);
|
||||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||||
}
|
}
|
||||||
if (!m_depthImage8.isNull()) {
|
if (!m_depthImage8.isNull()) {
|
||||||
@@ -1502,6 +1809,25 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
|||||||
}
|
}
|
||||||
emit hoveredWorldPosDepthChanged(wp0, z0);
|
emit hoveredWorldPosDepthChanged(wp0, z0);
|
||||||
|
|
||||||
|
if (m_blackholeCopyResolveActive) {
|
||||||
|
if (e->button() == Qt::LeftButton) {
|
||||||
|
QRectF src = m_blackholeCopySourceRect;
|
||||||
|
if (!src.contains(wp0)) {
|
||||||
|
src.moveCenter(wp0);
|
||||||
|
src = clampRectTopLeftToBounds(src, worldRectOfBackground());
|
||||||
|
m_blackholeCopySourceRect = src;
|
||||||
|
}
|
||||||
|
m_blackholeCopyDragging = true;
|
||||||
|
m_blackholeCopyDragOffset = wp0 - m_blackholeCopySourceRect.topLeft();
|
||||||
|
updateCursor();
|
||||||
|
update();
|
||||||
|
e->accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e->accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_presentationPreviewMode) {
|
if (m_presentationPreviewMode) {
|
||||||
if (e->button() == Qt::LeftButton) {
|
if (e->button() == Qt::LeftButton) {
|
||||||
const int hit = hitTestEntity(wp0);
|
const int hit = hitTestEntity(wp0);
|
||||||
@@ -1734,7 +2060,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
|||||||
if (!m_depthAbsPath.isEmpty()) {
|
if (!m_depthAbsPath.isEmpty()) {
|
||||||
if (m_depthDirty) {
|
if (m_depthDirty) {
|
||||||
m_depthDirty = false;
|
m_depthDirty = false;
|
||||||
QImage img(m_depthAbsPath);
|
QImage img = readImageTolerant(m_depthAbsPath);
|
||||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||||
}
|
}
|
||||||
if (!m_depthImage8.isNull()) {
|
if (!m_depthImage8.isNull()) {
|
||||||
@@ -1747,6 +2073,18 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
|||||||
}
|
}
|
||||||
emit hoveredWorldPosDepthChanged(wp, z);
|
emit hoveredWorldPosDepthChanged(wp, z);
|
||||||
|
|
||||||
|
if (m_blackholeCopyResolveActive) {
|
||||||
|
if (m_blackholeCopyDragging && (e->buttons() & Qt::LeftButton)) {
|
||||||
|
QRectF src = m_blackholeCopySourceRect;
|
||||||
|
src.moveTopLeft(wp - m_blackholeCopyDragOffset);
|
||||||
|
src = clampRectTopLeftToBounds(src, worldRectOfBackground());
|
||||||
|
m_blackholeCopySourceRect = src;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
e->accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_presentationPreviewMode) {
|
if (m_presentationPreviewMode) {
|
||||||
const int h = hitTestEntity(wp);
|
const int h = hitTestEntity(wp);
|
||||||
if (h != m_presHoverEntityIndex) {
|
if (h != m_presHoverEntityIndex) {
|
||||||
@@ -1849,7 +2187,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
|||||||
|
|
||||||
if (!m_depthAbsPath.isEmpty() && m_depthDirty) {
|
if (!m_depthAbsPath.isEmpty() && m_depthDirty) {
|
||||||
m_depthDirty = false;
|
m_depthDirty = false;
|
||||||
QImage img(m_depthAbsPath);
|
QImage img = readImageTolerant(m_depthAbsPath);
|
||||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||||
}
|
}
|
||||||
if (!m_depthImage8.isNull()) {
|
if (!m_depthImage8.isNull()) {
|
||||||
@@ -1901,6 +2239,18 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||||
|
if (m_blackholeCopyResolveActive && e->button() == Qt::LeftButton) {
|
||||||
|
if (m_blackholeCopyDragging) {
|
||||||
|
m_blackholeCopyDragging = false;
|
||||||
|
const QPointF delta = m_blackholeCopySourceRect.topLeft() - m_blackholeCopyHoleRect.topLeft();
|
||||||
|
emit requestResolveBlackholeCopy(
|
||||||
|
m_blackholeCopyEntityId,
|
||||||
|
QPoint(static_cast<int>(std::round(delta.x())), static_cast<int>(std::round(delta.y()))));
|
||||||
|
}
|
||||||
|
cancelBlackholeCopyResolve();
|
||||||
|
e->accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e->button() == Qt::LeftButton || e->button() == Qt::MiddleButton) {
|
if (e->button() == Qt::LeftButton || e->button() == Qt::MiddleButton) {
|
||||||
if (m_presentationPreviewMode && e->button() == Qt::LeftButton) {
|
if (m_presentationPreviewMode && e->button() == Qt::LeftButton) {
|
||||||
if (m_presBgPanSession && m_presBgDragDist < 8.0) {
|
if (m_presBgPanSession && m_presBgDragDist < 8.0) {
|
||||||
@@ -2016,6 +2366,11 @@ void EditorCanvas::wheelEvent(QWheelEvent* e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EditorCanvas::keyPressEvent(QKeyEvent* e) {
|
void EditorCanvas::keyPressEvent(QKeyEvent* e) {
|
||||||
|
if (m_blackholeCopyResolveActive && e->key() == Qt::Key_Escape) {
|
||||||
|
cancelBlackholeCopyResolve();
|
||||||
|
e->accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (m_presentationPreviewMode && e->key() == Qt::Key_Escape) {
|
if (m_presentationPreviewMode && e->key() == Qt::Key_Escape) {
|
||||||
clearPresentationEntityFocus();
|
clearPresentationEntityFocus();
|
||||||
e->accept();
|
e->accept();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
#include <QPoint>
|
||||||
#include <QPointF>
|
#include <QPointF>
|
||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
@@ -85,6 +86,12 @@ public:
|
|||||||
|
|
||||||
void selectEntityById(const QString& id);
|
void selectEntityById(const QString& id);
|
||||||
void clearEntitySelection();
|
void clearEntitySelection();
|
||||||
|
void selectBlackholeByEntityId(const QString& entityId);
|
||||||
|
void clearBlackholeSelection();
|
||||||
|
bool startBlackholeCopyResolve(const QString& entityId);
|
||||||
|
void cancelBlackholeCopyResolve();
|
||||||
|
/// 背景图片文件内容被外部写盘更新(路径未变)时,强制重新加载缓存
|
||||||
|
void notifyBackgroundContentChanged();
|
||||||
|
|
||||||
// 与动画求值一致的原点/缩放(用于 K 帧与自动关键帧)
|
// 与动画求值一致的原点/缩放(用于 K 帧与自动关键帧)
|
||||||
QPointF selectedAnimatedOriginWorld() const;
|
QPointF selectedAnimatedOriginWorld() const;
|
||||||
@@ -115,6 +122,7 @@ signals:
|
|||||||
void requestFinalizePendingEntity(const QVector<QPointF>& polyWorld);
|
void requestFinalizePendingEntity(const QVector<QPointF>& polyWorld);
|
||||||
void requestMoveEntity(const QString& id, const QPointF& delta);
|
void requestMoveEntity(const QString& id, const QPointF& delta);
|
||||||
void requestMoveTool(const QString& id, const QPointF& delta);
|
void requestMoveTool(const QString& id, const QPointF& delta);
|
||||||
|
void requestResolveBlackholeCopy(const QString& entityId, const QPoint& sourceOffsetPx);
|
||||||
void entityDragActiveChanged(bool on);
|
void entityDragActiveChanged(bool on);
|
||||||
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
|
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
|
||||||
/// 预览模式下点击实体:anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
|
/// 预览模式下点击实体:anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
|
||||||
@@ -171,6 +179,7 @@ private:
|
|||||||
QPointF animatedOriginWorld;
|
QPointF animatedOriginWorld;
|
||||||
double animatedDepthScale01 = 0.5;
|
double animatedDepthScale01 = 0.5;
|
||||||
double opacity = 1.0; // 0..1(由可见性轨道求值)
|
double opacity = 1.0; // 0..1(由可见性轨道求值)
|
||||||
|
bool blackholeVisible = true;
|
||||||
};
|
};
|
||||||
int hitTestEntity(const QPointF& worldPos) const;
|
int hitTestEntity(const QPointF& worldPos) const;
|
||||||
|
|
||||||
@@ -186,7 +195,7 @@ private:
|
|||||||
mutable QPixmap m_bgPixmap;
|
mutable QPixmap m_bgPixmap;
|
||||||
mutable bool m_pixmapDirty = true;
|
mutable bool m_pixmapDirty = true;
|
||||||
mutable QImage m_bgImage; // 原背景(用于抠图/填充)
|
mutable QImage m_bgImage; // 原背景(用于抠图/填充)
|
||||||
mutable QImage m_bgImageCutout; // 抠图后的背景(实体区域填黑)
|
mutable QImage m_bgImageCutout; // 抠图后的背景(黑洞区域填黑)
|
||||||
mutable bool m_bgImageDirty = true;
|
mutable bool m_bgImageDirty = true;
|
||||||
mutable bool m_bgCutoutDirty = true;
|
mutable bool m_bgCutoutDirty = true;
|
||||||
|
|
||||||
@@ -229,6 +238,13 @@ private:
|
|||||||
qint64 m_lastPreviewEmitMs = 0;
|
qint64 m_lastPreviewEmitMs = 0;
|
||||||
qint64 m_lastDepthScaleRecalcMs = 0;
|
qint64 m_lastDepthScaleRecalcMs = 0;
|
||||||
int m_selectedEntity = -1;
|
int m_selectedEntity = -1;
|
||||||
|
QString m_selectedBlackholeEntityId;
|
||||||
|
bool m_blackholeCopyResolveActive = false;
|
||||||
|
QString m_blackholeCopyEntityId;
|
||||||
|
QRectF m_blackholeCopyHoleRect;
|
||||||
|
QRectF m_blackholeCopySourceRect;
|
||||||
|
bool m_blackholeCopyDragging = false;
|
||||||
|
QPointF m_blackholeCopyDragOffset;
|
||||||
int m_selectedTool = -1;
|
int m_selectedTool = -1;
|
||||||
bool m_draggingTool = false;
|
bool m_draggingTool = false;
|
||||||
QPointF m_toolDragOffsetOriginWorld;
|
QPointF m_toolDragOffsetOriginWorld;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "dialogs/AboutWindow.h"
|
#include "dialogs/AboutWindow.h"
|
||||||
#include "dialogs/CancelableTaskDialog.h"
|
#include "dialogs/CancelableTaskDialog.h"
|
||||||
#include "dialogs/EntityFinalizeDialog.h"
|
#include "dialogs/EntityFinalizeDialog.h"
|
||||||
|
#include "dialogs/BlackholeResolveDialog.h"
|
||||||
#include "editor/EditorCanvas.h"
|
#include "editor/EditorCanvas.h"
|
||||||
#include "editor/EntityCutoutUtils.h"
|
#include "editor/EntityCutoutUtils.h"
|
||||||
#include "dialogs/ImageCropDialog.h"
|
#include "dialogs/ImageCropDialog.h"
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
#include "widgets/ToolOptionPopup.h"
|
#include "widgets/ToolOptionPopup.h"
|
||||||
#include "params/ParamControls.h"
|
#include "params/ParamControls.h"
|
||||||
#include "props/BackgroundPropertySection.h"
|
#include "props/BackgroundPropertySection.h"
|
||||||
|
#include "props/BlackholePropertySection.h"
|
||||||
#include "props/EntityPropertySection.h"
|
#include "props/EntityPropertySection.h"
|
||||||
#include "props/ToolPropertySection.h"
|
#include "props/ToolPropertySection.h"
|
||||||
#include "timeline/TimelineWidget.h"
|
#include "timeline/TimelineWidget.h"
|
||||||
@@ -87,9 +89,9 @@ constexpr int kRightDockMinimumWidth = 80;
|
|||||||
/// 列宽小于此值时自动隐藏右侧两 dock
|
/// 列宽小于此值时自动隐藏右侧两 dock
|
||||||
constexpr int kRightDockAutoHideBelow = 92;
|
constexpr int kRightDockAutoHideBelow = 92;
|
||||||
/// 右侧 dock 列最大宽度,避免过宽挤占画布
|
/// 右侧 dock 列最大宽度,避免过宽挤占画布
|
||||||
constexpr int kRightDockMaximumWidth = 288;
|
constexpr int kRightDockMaximumWidth = 252;
|
||||||
/// 属性区表单内容最大宽度(dock 仍可略宽,两侧留白,避免 SpinBox 被拉得过开)
|
/// 属性区表单内容最大宽度(dock 仍可略宽,两侧留白,避免 SpinBox 被拉得过开)
|
||||||
constexpr int kPropertyPanelContentMaxWidth = 268;
|
constexpr int kPropertyPanelContentMaxWidth = 232;
|
||||||
/// 启动时垂直分割高度:项目树较矮、属性区较高
|
/// 启动时垂直分割高度:项目树较矮、属性区较高
|
||||||
constexpr int kProjectTreeDockStartupHeight = 148;
|
constexpr int kProjectTreeDockStartupHeight = 148;
|
||||||
constexpr int kPropertiesDockStartupHeight = 392;
|
constexpr int kPropertiesDockStartupHeight = 392;
|
||||||
@@ -1185,6 +1187,14 @@ void MainWindow::createProjectTreeDock() {
|
|||||||
showBackgroundContextMenu(m_projectTree->viewport()->mapToGlobal(pos));
|
showBackgroundContextMenu(m_projectTree->viewport()->mapToGlobal(pos));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const QString kind = item->data(0, Qt::UserRole).toString();
|
||||||
|
if (kind == QStringLiteral("blackhole")) {
|
||||||
|
const QString id = item->data(0, Qt::UserRole + 1).toString();
|
||||||
|
if (!id.isEmpty()) {
|
||||||
|
showBlackholeContextMenu(m_projectTree->viewport()->mapToGlobal(pos), id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
connect(m_projectTree, &QTreeWidget::itemClicked, this, &MainWindow::onProjectTreeItemClicked);
|
connect(m_projectTree, &QTreeWidget::itemClicked, this, &MainWindow::onProjectTreeItemClicked);
|
||||||
static_cast<ProjectTreeWidget*>(m_projectTree)->onNodeParentDropRequested =
|
static_cast<ProjectTreeWidget*>(m_projectTree)->onNodeParentDropRequested =
|
||||||
@@ -1280,15 +1290,17 @@ void MainWindow::createProjectTreeDock() {
|
|||||||
Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea);
|
Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea);
|
||||||
m_dockProperties->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable |
|
m_dockProperties->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable |
|
||||||
QDockWidget::DockWidgetClosable);
|
QDockWidget::DockWidgetClosable);
|
||||||
m_dockProperties->setMinimumWidth(236);
|
m_dockProperties->setMinimumWidth(200);
|
||||||
|
|
||||||
m_bgPropertySection = new gui::BackgroundPropertySection();
|
m_bgPropertySection = new gui::BackgroundPropertySection();
|
||||||
|
m_blackholePropertySection = new gui::BlackholePropertySection();
|
||||||
m_entityPropertySection = new gui::EntityPropertySection();
|
m_entityPropertySection = new gui::EntityPropertySection();
|
||||||
m_toolPropertySection = new gui::ToolPropertySection();
|
m_toolPropertySection = new gui::ToolPropertySection();
|
||||||
m_propertyStack = new QStackedWidget();
|
m_propertyStack = new QStackedWidget();
|
||||||
m_propertyStack->setContentsMargins(4, 4, 4, 4);
|
m_propertyStack->setContentsMargins(4, 4, 4, 4);
|
||||||
m_propertyStack->setMaximumWidth(kPropertyPanelContentMaxWidth);
|
m_propertyStack->setMaximumWidth(kPropertyPanelContentMaxWidth);
|
||||||
m_propertyStack->addWidget(m_bgPropertySection);
|
m_propertyStack->addWidget(m_bgPropertySection);
|
||||||
|
m_propertyStack->addWidget(m_blackholePropertySection);
|
||||||
m_propertyStack->addWidget(m_entityPropertySection);
|
m_propertyStack->addWidget(m_entityPropertySection);
|
||||||
m_propertyStack->addWidget(m_toolPropertySection);
|
m_propertyStack->addWidget(m_toolPropertySection);
|
||||||
|
|
||||||
@@ -1336,7 +1348,30 @@ void MainWindow::createProjectTreeDock() {
|
|||||||
if (m_selectedEntityId.isEmpty() || !m_editorCanvas) return;
|
if (m_selectedEntityId.isEmpty() || !m_editorCanvas) return;
|
||||||
const double s = m_editorCanvas->selectedCombinedScale();
|
const double s = m_editorCanvas->selectedCombinedScale();
|
||||||
if (s <= 1e-9) return;
|
if (s <= 1e-9) return;
|
||||||
if (!m_workspace.reanchorEntityPivot(m_selectedEntityId, m_currentFrame, QPointF(x, y), s)) return;
|
QPointF targetPivot(x, y);
|
||||||
|
QString parentId;
|
||||||
|
for (const auto& e : m_workspace.entities()) {
|
||||||
|
if (e.id == m_selectedEntityId) {
|
||||||
|
parentId = e.parentId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!parentId.isEmpty()) {
|
||||||
|
const auto rf = core::eval::evaluateAtFrame(m_workspace.project(), m_currentFrame, 10);
|
||||||
|
for (const auto& pe : rf.entities) {
|
||||||
|
if (pe.entity.id == parentId) {
|
||||||
|
targetPivot += pe.entity.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& pt : rf.tools) {
|
||||||
|
if (pt.tool.id == parentId) {
|
||||||
|
targetPivot += pt.tool.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!m_workspace.reanchorEntityPivot(m_selectedEntityId, m_currentFrame, targetPivot, s)) return;
|
||||||
refreshEditorPage();
|
refreshEditorPage();
|
||||||
refreshDopeSheet();
|
refreshDopeSheet();
|
||||||
});
|
});
|
||||||
@@ -1344,8 +1379,31 @@ void MainWindow::createProjectTreeDock() {
|
|||||||
if (m_selectedEntityId.isEmpty() || !m_editorCanvas) return;
|
if (m_selectedEntityId.isEmpty() || !m_editorCanvas) return;
|
||||||
const double s = m_editorCanvas->selectedCombinedScale();
|
const double s = m_editorCanvas->selectedCombinedScale();
|
||||||
if (s <= 1e-9) return;
|
if (s <= 1e-9) return;
|
||||||
|
QPointF targetCentroid(x, y);
|
||||||
|
QString parentId;
|
||||||
|
for (const auto& e : m_workspace.entities()) {
|
||||||
|
if (e.id == m_selectedEntityId) {
|
||||||
|
parentId = e.parentId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!parentId.isEmpty()) {
|
||||||
|
const auto rf = core::eval::evaluateAtFrame(m_workspace.project(), m_currentFrame, 10);
|
||||||
|
for (const auto& pe : rf.entities) {
|
||||||
|
if (pe.entity.id == parentId) {
|
||||||
|
targetCentroid += pe.entity.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& pt : rf.tools) {
|
||||||
|
if (pt.tool.id == parentId) {
|
||||||
|
targetCentroid += pt.tool.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const bool autoKey = m_chkAutoKeyframe && m_chkAutoKeyframe->isChecked();
|
const bool autoKey = m_chkAutoKeyframe && m_chkAutoKeyframe->isChecked();
|
||||||
if (!m_workspace.moveEntityCentroidTo(m_selectedEntityId, m_currentFrame, QPointF(x, y), s, autoKey)) return;
|
if (!m_workspace.moveEntityCentroidTo(m_selectedEntityId, m_currentFrame, targetCentroid, s, autoKey)) return;
|
||||||
refreshEditorPage();
|
refreshEditorPage();
|
||||||
refreshDopeSheet();
|
refreshDopeSheet();
|
||||||
});
|
});
|
||||||
@@ -1410,6 +1468,46 @@ void MainWindow::createProjectTreeDock() {
|
|||||||
m_workspace.setToolAlign(m_selectedToolId, a);
|
m_workspace.setToolAlign(m_selectedToolId, a);
|
||||||
refreshEditorPage();
|
refreshEditorPage();
|
||||||
});
|
});
|
||||||
|
connect(m_toolPropertySection, &gui::ToolPropertySection::positionEdited, this, [this](double x, double y) {
|
||||||
|
if (m_selectedToolId.isEmpty() || !m_workspace.isOpen()) return;
|
||||||
|
const int f = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
||||||
|
const auto rf = core::eval::evaluateAtFrame(m_workspace.project(), f, 10);
|
||||||
|
QPointF currentWorld;
|
||||||
|
QPointF parentWorld;
|
||||||
|
QString parentId;
|
||||||
|
bool found = false;
|
||||||
|
for (const auto& t : rf.tools) {
|
||||||
|
if (t.tool.id == m_selectedToolId) {
|
||||||
|
currentWorld = t.tool.originWorld;
|
||||||
|
parentId = t.tool.parentId;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) return;
|
||||||
|
if (!parentId.isEmpty()) {
|
||||||
|
for (const auto& e : rf.entities) {
|
||||||
|
if (e.entity.id == parentId) {
|
||||||
|
parentWorld = e.entity.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (qFuzzyIsNull(parentWorld.x()) && qFuzzyIsNull(parentWorld.y())) {
|
||||||
|
for (const auto& t : rf.tools) {
|
||||||
|
if (t.tool.id == parentId) {
|
||||||
|
parentWorld = t.tool.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const QPointF targetWorld = parentId.isEmpty() ? QPointF(x, y) : (parentWorld + QPointF(x, y));
|
||||||
|
const QPointF delta = targetWorld - currentWorld;
|
||||||
|
if (qFuzzyIsNull(delta.x()) && qFuzzyIsNull(delta.y())) return;
|
||||||
|
if (!m_workspace.moveToolBy(m_selectedToolId, delta, f, true)) return;
|
||||||
|
refreshEditorPage();
|
||||||
|
refreshDopeSheet();
|
||||||
|
});
|
||||||
connect(m_toolPropertySection, &gui::ToolPropertySection::visibleToggled, this, [this](bool on) {
|
connect(m_toolPropertySection, &gui::ToolPropertySection::visibleToggled, this, [this](bool on) {
|
||||||
if (m_selectedToolId.isEmpty() || !m_workspace.isOpen()) return;
|
if (m_selectedToolId.isEmpty() || !m_workspace.isOpen()) return;
|
||||||
const int f = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
const int f = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
||||||
@@ -1468,7 +1566,8 @@ void MainWindow::createProjectTreeDock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::refreshPropertyPanel() {
|
void MainWindow::refreshPropertyPanel() {
|
||||||
if (!m_bgPropertySection || !m_entityPropertySection || !m_toolPropertySection || !m_propertyStack) {
|
if (!m_bgPropertySection || !m_blackholePropertySection || !m_entityPropertySection ||
|
||||||
|
!m_toolPropertySection || !m_propertyStack) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1556,6 +1655,15 @@ void MainWindow::refreshPropertyPanel() {
|
|||||||
gui::ToolPropertyUiState st;
|
gui::ToolPropertyUiState st;
|
||||||
const int f = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
const int f = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
||||||
const auto* clip = activeClipForUi();
|
const auto* clip = activeClipForUi();
|
||||||
|
QString parentId;
|
||||||
|
const auto rf = core::eval::evaluateAtFrame(m_workspace.project(), m_currentFrame, 10);
|
||||||
|
for (const auto& rt : rf.tools) {
|
||||||
|
if (rt.tool.id == m_selectedToolId) {
|
||||||
|
st.position = rt.tool.originWorld;
|
||||||
|
parentId = rt.tool.parentId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const auto& t : m_workspace.tools()) {
|
for (const auto& t : m_workspace.tools()) {
|
||||||
if (t.id == m_selectedToolId) {
|
if (t.id == m_selectedToolId) {
|
||||||
st.displayName = t.displayName.isEmpty() ? t.id : t.displayName;
|
st.displayName = t.displayName.isEmpty() ? t.id : t.displayName;
|
||||||
@@ -1576,12 +1684,59 @@ void MainWindow::refreshPropertyPanel() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!parentId.isEmpty()) {
|
||||||
|
QPointF parentWorld;
|
||||||
|
for (const auto& pe : rf.entities) {
|
||||||
|
if (pe.entity.id == parentId) {
|
||||||
|
parentWorld = pe.entity.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (qFuzzyIsNull(parentWorld.x()) && qFuzzyIsNull(parentWorld.y())) {
|
||||||
|
for (const auto& pt : rf.tools) {
|
||||||
|
if (pt.tool.id == parentId) {
|
||||||
|
parentWorld = pt.tool.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st.position -= parentWorld;
|
||||||
|
st.parentRelativeMode = true;
|
||||||
|
}
|
||||||
m_toolPropertySection->applyState(st);
|
m_toolPropertySection->applyState(st);
|
||||||
m_propertyStack->setCurrentWidget(m_toolPropertySection);
|
m_propertyStack->setCurrentWidget(m_toolPropertySection);
|
||||||
m_dockProperties->setWindowTitle(QStringLiteral("属性 — 工具"));
|
m_dockProperties->setWindowTitle(QStringLiteral("属性 — 工具"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool holeUi = m_workspace.isOpen() && !m_selectedBlackholeEntityId.isEmpty();
|
||||||
|
if (holeUi) {
|
||||||
|
gui::BlackholePropertyUiState st;
|
||||||
|
for (const auto& e : m_workspace.entities()) {
|
||||||
|
if (e.id != m_selectedBlackholeEntityId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
st.blackholeName = e.blackholeId.isEmpty() ? QStringLiteral("blackhole-%1").arg(e.id) : e.blackholeId;
|
||||||
|
st.statusText = e.blackholeVisible ? QStringLiteral("否") : QStringLiteral("是");
|
||||||
|
if (e.blackholeResolvedBy == QStringLiteral("copy_background")) {
|
||||||
|
st.methodText = QStringLiteral("复制背景其他区域");
|
||||||
|
} else if (e.blackholeResolvedBy == QStringLiteral("use_original_background")) {
|
||||||
|
st.methodText = QStringLiteral("使用原始背景");
|
||||||
|
} else if (e.blackholeResolvedBy == QStringLiteral("model_inpaint")) {
|
||||||
|
st.methodText = QStringLiteral("模型补全");
|
||||||
|
} else if (e.blackholeResolvedBy == QStringLiteral("pending")) {
|
||||||
|
st.methodText = QStringLiteral("待选择");
|
||||||
|
} else {
|
||||||
|
st.methodText = QStringLiteral("未选择");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_blackholePropertySection->applyState(st);
|
||||||
|
m_propertyStack->setCurrentWidget(m_blackholePropertySection);
|
||||||
|
m_dockProperties->setWindowTitle(QStringLiteral("属性 — 黑洞"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bool entUi = m_hasSelectedEntity && m_workspace.isOpen() && !m_selectedEntityId.isEmpty() && m_editorCanvas;
|
const bool entUi = m_hasSelectedEntity && m_workspace.isOpen() && !m_selectedEntityId.isEmpty() && m_editorCanvas;
|
||||||
if (!entUi) {
|
if (!entUi) {
|
||||||
m_entityPropertySection->clearDisconnected();
|
m_entityPropertySection->clearDisconnected();
|
||||||
@@ -1595,6 +1750,7 @@ void MainWindow::refreshPropertyPanel() {
|
|||||||
double userScale = 1.0;
|
double userScale = 1.0;
|
||||||
bool ignoreDist = false;
|
bool ignoreDist = false;
|
||||||
bool entVisible = true;
|
bool entVisible = true;
|
||||||
|
QString parentId;
|
||||||
core::EntityIntroContent intro;
|
core::EntityIntroContent intro;
|
||||||
const int f = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
const int f = std::clamp(m_currentFrame, 0, core::Project::kClipFixedFrames - 1);
|
||||||
const auto* clip = activeClipForUi();
|
const auto* clip = activeClipForUi();
|
||||||
@@ -1604,6 +1760,7 @@ void MainWindow::refreshPropertyPanel() {
|
|||||||
userScale = e.userScale;
|
userScale = e.userScale;
|
||||||
intro = e.intro;
|
intro = e.intro;
|
||||||
ignoreDist = e.ignoreDistanceScale;
|
ignoreDist = e.ignoreDistanceScale;
|
||||||
|
parentId = e.parentId;
|
||||||
const QVector<core::Project::ToolKeyframeBool> keys =
|
const QVector<core::Project::ToolKeyframeBool> keys =
|
||||||
(clip && clip->entityVisibilityKeys.contains(e.id))
|
(clip && clip->entityVisibilityKeys.contains(e.id))
|
||||||
? clip->entityVisibilityKeys.value(e.id)
|
? clip->entityVisibilityKeys.value(e.id)
|
||||||
@@ -1621,6 +1778,27 @@ void MainWindow::refreshPropertyPanel() {
|
|||||||
QStringLiteral("%1(自动)").arg(m_editorCanvas->selectedDistanceScaleMultiplier(), 0, 'f', 3);
|
QStringLiteral("%1(自动)").arg(m_editorCanvas->selectedDistanceScaleMultiplier(), 0, 'f', 3);
|
||||||
st.pivot = m_editorCanvas->selectedAnimatedOriginWorld();
|
st.pivot = m_editorCanvas->selectedAnimatedOriginWorld();
|
||||||
st.centroid = m_editorCanvas->selectedEntityCentroidWorld();
|
st.centroid = m_editorCanvas->selectedEntityCentroidWorld();
|
||||||
|
if (!parentId.isEmpty()) {
|
||||||
|
QPointF parentWorld;
|
||||||
|
const auto rf = core::eval::evaluateAtFrame(m_workspace.project(), m_currentFrame, 10);
|
||||||
|
for (const auto& pe : rf.entities) {
|
||||||
|
if (pe.entity.id == parentId) {
|
||||||
|
parentWorld = pe.entity.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (qFuzzyIsNull(parentWorld.x()) && qFuzzyIsNull(parentWorld.y())) {
|
||||||
|
for (const auto& pt : rf.tools) {
|
||||||
|
if (pt.tool.id == parentId) {
|
||||||
|
parentWorld = pt.tool.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st.pivot -= parentWorld;
|
||||||
|
st.centroid -= parentWorld;
|
||||||
|
st.parentRelativeMode = true;
|
||||||
|
}
|
||||||
st.userScale = userScale;
|
st.userScale = userScale;
|
||||||
st.ignoreDistanceScale = ignoreDist;
|
st.ignoreDistanceScale = ignoreDist;
|
||||||
st.visible = entVisible;
|
st.visible = entVisible;
|
||||||
@@ -1646,6 +1824,34 @@ void MainWindow::refreshEntityPropertyPanelFast() {
|
|||||||
QStringLiteral("%1(自动)").arg(m_editorCanvas->selectedDistanceScaleMultiplier(), 0, 'f', 3);
|
QStringLiteral("%1(自动)").arg(m_editorCanvas->selectedDistanceScaleMultiplier(), 0, 'f', 3);
|
||||||
st.pivot = m_editorCanvas->selectedAnimatedOriginWorld();
|
st.pivot = m_editorCanvas->selectedAnimatedOriginWorld();
|
||||||
st.centroid = m_editorCanvas->selectedEntityCentroidWorld();
|
st.centroid = m_editorCanvas->selectedEntityCentroidWorld();
|
||||||
|
QString parentId;
|
||||||
|
for (const auto& e : m_workspace.entities()) {
|
||||||
|
if (e.id == m_selectedEntityId) {
|
||||||
|
parentId = e.parentId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!parentId.isEmpty()) {
|
||||||
|
QPointF parentWorld;
|
||||||
|
const auto rf = core::eval::evaluateAtFrame(m_workspace.project(), m_currentFrame, 10);
|
||||||
|
for (const auto& pe : rf.entities) {
|
||||||
|
if (pe.entity.id == parentId) {
|
||||||
|
parentWorld = pe.entity.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (qFuzzyIsNull(parentWorld.x()) && qFuzzyIsNull(parentWorld.y())) {
|
||||||
|
for (const auto& pt : rf.tools) {
|
||||||
|
if (pt.tool.id == parentId) {
|
||||||
|
parentWorld = pt.tool.originWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st.pivot -= parentWorld;
|
||||||
|
st.centroid -= parentWorld;
|
||||||
|
st.parentRelativeMode = true;
|
||||||
|
}
|
||||||
st.userScale = m_editorCanvas->selectedUserScale();
|
st.userScale = m_editorCanvas->selectedUserScale();
|
||||||
// ignoreDistanceScale 在拖动中不变更,fast path 不必更新(避免再遍历 entities)
|
// ignoreDistanceScale 在拖动中不变更,fast path 不必更新(避免再遍历 entities)
|
||||||
m_entityPropertySection->applyState(st);
|
m_entityPropertySection->applyState(st);
|
||||||
@@ -1742,6 +1948,33 @@ void MainWindow::refreshProjectTree() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 黑洞节点:挂在“背景”下,和实体渲染解耦(黑洞可见性独立于实体可见性)
|
||||||
|
QVector<const core::Project::Entity*> blackholeEnts;
|
||||||
|
blackholeEnts.reserve(sortedEnts.size());
|
||||||
|
for (const auto& e : sortedEnts) {
|
||||||
|
if (!e.cutoutPolygonWorld.isEmpty()) {
|
||||||
|
blackholeEnts.push_back(&e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::stable_sort(blackholeEnts.begin(), blackholeEnts.end(),
|
||||||
|
[](const core::Project::Entity* a, const core::Project::Entity* b) {
|
||||||
|
const QString an = a->displayName.isEmpty() ? a->id : a->displayName;
|
||||||
|
const QString bn = b->displayName.isEmpty() ? b->id : b->displayName;
|
||||||
|
return an < bn;
|
||||||
|
});
|
||||||
|
for (const auto* e : blackholeEnts) {
|
||||||
|
auto* it = new QTreeWidgetItem(m_itemBackground);
|
||||||
|
const QString base = e->displayName.isEmpty() ? e->id : e->displayName;
|
||||||
|
const QString holeName =
|
||||||
|
e->blackholeId.isEmpty() ? QStringLiteral("blackhole-%1").arg(e->id) : e->blackholeId;
|
||||||
|
it->setText(1, QStringLiteral("黑洞 · %1").arg(base));
|
||||||
|
it->setToolTip(1, QStringLiteral("节点:%1").arg(holeName));
|
||||||
|
it->setTextAlignment(1, Qt::AlignRight | Qt::AlignVCenter);
|
||||||
|
it->setData(0, Qt::UserRole, QStringLiteral("blackhole"));
|
||||||
|
it->setData(0, Qt::UserRole + 1, e->id); // 绑定实体 id,便于定位 cutout 多边形
|
||||||
|
it->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
struct NodeRef {
|
struct NodeRef {
|
||||||
QString kind; // "entity" / "tool"
|
QString kind; // "entity" / "tool"
|
||||||
QString id;
|
QString id;
|
||||||
@@ -1845,7 +2078,9 @@ void MainWindow::syncProjectTreeFromCanvasSelection() {
|
|||||||
}
|
}
|
||||||
m_syncingTreeSelection = true;
|
m_syncingTreeSelection = true;
|
||||||
m_projectTree->blockSignals(true);
|
m_projectTree->blockSignals(true);
|
||||||
if ((!m_hasSelectedEntity || m_selectedEntityId.isEmpty()) && (!m_hasSelectedTool || m_selectedToolId.isEmpty())) {
|
if ((!m_hasSelectedEntity || m_selectedEntityId.isEmpty()) &&
|
||||||
|
(!m_hasSelectedTool || m_selectedToolId.isEmpty()) &&
|
||||||
|
m_selectedBlackholeEntityId.isEmpty()) {
|
||||||
m_projectTree->clearSelection();
|
m_projectTree->clearSelection();
|
||||||
} else {
|
} else {
|
||||||
QTreeWidgetItem* found = nullptr;
|
QTreeWidgetItem* found = nullptr;
|
||||||
@@ -1861,6 +2096,11 @@ void MainWindow::syncProjectTreeFromCanvasSelection() {
|
|||||||
found = node;
|
found = node;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (!m_selectedBlackholeEntityId.isEmpty() && kind == QStringLiteral("blackhole") &&
|
||||||
|
id == m_selectedBlackholeEntityId) {
|
||||||
|
found = node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (found) {
|
if (found) {
|
||||||
m_projectTree->setCurrentItem(found);
|
m_projectTree->setCurrentItem(found);
|
||||||
@@ -1882,6 +2122,10 @@ void MainWindow::onProjectTreeItemClicked(QTreeWidgetItem* item, int column) {
|
|||||||
if (kind == QStringLiteral("entity")) {
|
if (kind == QStringLiteral("entity")) {
|
||||||
const QString id = item->data(0, Qt::UserRole + 1).toString();
|
const QString id = item->data(0, Qt::UserRole + 1).toString();
|
||||||
if (!id.isEmpty()) {
|
if (!id.isEmpty()) {
|
||||||
|
m_selectedBlackholeEntityId.clear();
|
||||||
|
if (m_editorCanvas) {
|
||||||
|
m_editorCanvas->clearBlackholeSelection();
|
||||||
|
}
|
||||||
m_hasSelectedTool = false;
|
m_hasSelectedTool = false;
|
||||||
m_selectedToolId.clear();
|
m_selectedToolId.clear();
|
||||||
if (m_timeline) {
|
if (m_timeline) {
|
||||||
@@ -1892,6 +2136,10 @@ void MainWindow::onProjectTreeItemClicked(QTreeWidgetItem* item, int column) {
|
|||||||
} else if (kind == QStringLiteral("tool")) {
|
} else if (kind == QStringLiteral("tool")) {
|
||||||
const QString id = item->data(0, Qt::UserRole + 1).toString();
|
const QString id = item->data(0, Qt::UserRole + 1).toString();
|
||||||
if (!id.isEmpty()) {
|
if (!id.isEmpty()) {
|
||||||
|
m_selectedBlackholeEntityId.clear();
|
||||||
|
if (m_editorCanvas) {
|
||||||
|
m_editorCanvas->clearBlackholeSelection();
|
||||||
|
}
|
||||||
m_hasSelectedTool = true;
|
m_hasSelectedTool = true;
|
||||||
m_selectedToolId = id;
|
m_selectedToolId = id;
|
||||||
m_hasSelectedEntity = false;
|
m_hasSelectedEntity = false;
|
||||||
@@ -1904,10 +2152,28 @@ void MainWindow::onProjectTreeItemClicked(QTreeWidgetItem* item, int column) {
|
|||||||
}
|
}
|
||||||
refreshPropertyPanel();
|
refreshPropertyPanel();
|
||||||
}
|
}
|
||||||
|
} else if (kind == QStringLiteral("blackhole")) {
|
||||||
|
const QString entityId = item->data(0, Qt::UserRole + 1).toString();
|
||||||
|
if (!entityId.isEmpty()) {
|
||||||
|
m_selectedBlackholeEntityId = entityId;
|
||||||
|
m_hasSelectedTool = false;
|
||||||
|
m_selectedToolId.clear();
|
||||||
|
m_hasSelectedEntity = false;
|
||||||
|
m_selectedEntityId.clear();
|
||||||
|
m_selectedEntityDisplayNameCache.clear();
|
||||||
|
if (m_editorCanvas) {
|
||||||
|
m_editorCanvas->clearEntitySelection();
|
||||||
|
m_editorCanvas->selectBlackholeByEntityId(entityId);
|
||||||
|
}
|
||||||
|
updateTimelineTracks();
|
||||||
|
refreshPropertyPanel();
|
||||||
|
}
|
||||||
} else if (kind == QStringLiteral("background")) {
|
} else if (kind == QStringLiteral("background")) {
|
||||||
|
m_selectedBlackholeEntityId.clear();
|
||||||
m_hasSelectedTool = false;
|
m_hasSelectedTool = false;
|
||||||
m_selectedToolId.clear();
|
m_selectedToolId.clear();
|
||||||
m_editorCanvas->clearEntitySelection();
|
m_editorCanvas->clearEntitySelection();
|
||||||
|
m_editorCanvas->clearBlackholeSelection();
|
||||||
updateTimelineTracks();
|
updateTimelineTracks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2394,6 +2660,7 @@ void MainWindow::rebuildCentralPages() {
|
|||||||
connect(m_editorCanvas, &EditorCanvas::selectedEntityChanged, this, [this](bool hasSel, const QString& id, int depth, const QPointF& origin) {
|
connect(m_editorCanvas, &EditorCanvas::selectedEntityChanged, this, [this](bool hasSel, const QString& id, int depth, const QPointF& origin) {
|
||||||
m_hasSelectedEntity = hasSel;
|
m_hasSelectedEntity = hasSel;
|
||||||
m_selectedEntityId = id;
|
m_selectedEntityId = id;
|
||||||
|
m_selectedBlackholeEntityId.clear();
|
||||||
m_selectedEntityDepth = depth;
|
m_selectedEntityDepth = depth;
|
||||||
m_selectedEntityOrigin = origin;
|
m_selectedEntityOrigin = origin;
|
||||||
m_hasSelectedTool = false;
|
m_hasSelectedTool = false;
|
||||||
@@ -2418,6 +2685,7 @@ void MainWindow::rebuildCentralPages() {
|
|||||||
Q_UNUSED(origin);
|
Q_UNUSED(origin);
|
||||||
m_hasSelectedTool = hasSel;
|
m_hasSelectedTool = hasSel;
|
||||||
m_selectedToolId = id;
|
m_selectedToolId = id;
|
||||||
|
m_selectedBlackholeEntityId.clear();
|
||||||
if (hasSel) {
|
if (hasSel) {
|
||||||
m_hasSelectedEntity = false;
|
m_hasSelectedEntity = false;
|
||||||
m_selectedEntityId.clear();
|
m_selectedEntityId.clear();
|
||||||
@@ -2475,6 +2743,11 @@ void MainWindow::rebuildCentralPages() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ent.blackholeVisible = true;
|
||||||
|
if (ent.blackholeId.isEmpty() && !ent.id.isEmpty()) {
|
||||||
|
ent.blackholeId = QStringLiteral("blackhole-%1").arg(ent.id);
|
||||||
|
}
|
||||||
|
ent.blackholeResolvedBy = QStringLiteral("pending");
|
||||||
if (!m_workspace.addEntity(ent, img)) {
|
if (!m_workspace.addEntity(ent, img)) {
|
||||||
QMessageBox::warning(this, QStringLiteral("实体"), QStringLiteral("保存实体失败。"));
|
QMessageBox::warning(this, QStringLiteral("实体"), QStringLiteral("保存实体失败。"));
|
||||||
return;
|
return;
|
||||||
@@ -2768,6 +3041,11 @@ void MainWindow::rebuildCentralPages() {
|
|||||||
if (ent.displayName.isEmpty()) {
|
if (ent.displayName.isEmpty()) {
|
||||||
// 允许空:界面会用 id 展示
|
// 允许空:界面会用 id 展示
|
||||||
}
|
}
|
||||||
|
ent.blackholeVisible = true;
|
||||||
|
if (ent.blackholeId.isEmpty() && !ent.id.isEmpty()) {
|
||||||
|
ent.blackholeId = QStringLiteral("blackhole-%1").arg(ent.id);
|
||||||
|
}
|
||||||
|
ent.blackholeResolvedBy = QStringLiteral("pending");
|
||||||
|
|
||||||
QImage bg(m_workspace.backgroundAbsolutePath());
|
QImage bg(m_workspace.backgroundAbsolutePath());
|
||||||
if (!bg.isNull() && bg.format() != QImage::Format_ARGB32_Premultiplied) {
|
if (!bg.isNull() && bg.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||||
@@ -2812,6 +3090,26 @@ void MainWindow::rebuildCentralPages() {
|
|||||||
refreshProjectTree();
|
refreshProjectTree();
|
||||||
updateUiEnabledState();
|
updateUiEnabledState();
|
||||||
});
|
});
|
||||||
|
connect(m_editorCanvas, &EditorCanvas::requestResolveBlackholeCopy, this,
|
||||||
|
[this](const QString& entityId, const QPoint& sourceOffsetPx) {
|
||||||
|
if (!m_workspace.resolveBlackholeByCopyBackground(entityId, sourceOffsetPx, true)) {
|
||||||
|
QMessageBox::warning(
|
||||||
|
this,
|
||||||
|
QStringLiteral("黑洞修复"),
|
||||||
|
QStringLiteral("复制背景区域失败。请重新拖动取样框,确保采样区域在背景范围内。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusBar()->showMessage(QStringLiteral("黑洞已通过背景复制修复"));
|
||||||
|
refreshProjectTree();
|
||||||
|
updateUiEnabledState();
|
||||||
|
if (m_editorCanvas) {
|
||||||
|
m_editorCanvas->notifyBackgroundContentChanged();
|
||||||
|
}
|
||||||
|
refreshEditorPage();
|
||||||
|
if (m_previewRequested) {
|
||||||
|
refreshPreviewPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
connect(m_editorCanvas, &EditorCanvas::presentationEntityIntroRequested, this,
|
connect(m_editorCanvas, &EditorCanvas::presentationEntityIntroRequested, this,
|
||||||
[this](const QString& id, QPointF anchorView) {
|
[this](const QString& id, QPointF anchorView) {
|
||||||
@@ -3194,7 +3492,8 @@ void MainWindow::refreshDopeSheet() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addChannel(QStringLiteral("位置"), 0, hasLoc);
|
const QString locLabel = e.parentId.isEmpty() ? QStringLiteral("位置") : QStringLiteral("相对位置");
|
||||||
|
addChannel(locLabel, 0, hasLoc);
|
||||||
addChannel(QStringLiteral("缩放"), 1, hasSc);
|
addChannel(QStringLiteral("缩放"), 1, hasSc);
|
||||||
addChannel(QStringLiteral("图像"), 2, hasIm);
|
addChannel(QStringLiteral("图像"), 2, hasIm);
|
||||||
}
|
}
|
||||||
@@ -3221,6 +3520,70 @@ void MainWindow::showBackgroundContextMenu(const QPoint& globalPos) {
|
|||||||
refreshPreviewPage();
|
refreshPreviewPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString& entityId) {
|
||||||
|
if (entityId.isEmpty() || !m_workspace.isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_selectedBlackholeEntityId = entityId;
|
||||||
|
if (m_editorCanvas) {
|
||||||
|
m_editorCanvas->selectBlackholeByEntityId(entityId);
|
||||||
|
}
|
||||||
|
syncProjectTreeFromCanvasSelection();
|
||||||
|
|
||||||
|
QString holeLabel = entityId;
|
||||||
|
for (const auto& e : m_workspace.entities()) {
|
||||||
|
if (e.id == entityId) {
|
||||||
|
if (!e.blackholeId.isEmpty()) {
|
||||||
|
holeLabel = e.blackholeId;
|
||||||
|
} else {
|
||||||
|
holeLabel = QStringLiteral("blackhole-%1").arg(entityId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QMenu menu(this);
|
||||||
|
QAction* actResolve = menu.addAction(QStringLiteral("修复"));
|
||||||
|
QAction* chosen = menu.exec(globalPos);
|
||||||
|
if (!chosen || chosen != actResolve) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlackholeResolveDialog dlg(holeLabel, this);
|
||||||
|
if (dlg.exec() != QDialog::Accepted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
if (dlg.selectedAlgorithm() == BlackholeResolveDialog::Algorithm::CopyBackgroundRegion) {
|
||||||
|
if (!m_editorCanvas || !m_editorCanvas->startBlackholeCopyResolve(entityId)) {
|
||||||
|
QMessageBox::warning(
|
||||||
|
this,
|
||||||
|
QStringLiteral("黑洞修复"),
|
||||||
|
QStringLiteral("无法进入画布拖动模式,请确认黑洞与背景数据有效。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statusBar()->showMessage(QStringLiteral("拖动画布中的青色取样框,松开鼠标即应用;Esc 取消"));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
ok = m_workspace.resolveBlackholeByUseOriginalBackground(entityId);
|
||||||
|
if (!ok) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("应用“使用原始背景”失败。"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
statusBar()->showMessage(QStringLiteral("黑洞修复已应用"));
|
||||||
|
refreshProjectTree();
|
||||||
|
updateUiEnabledState();
|
||||||
|
refreshEditorPage();
|
||||||
|
if (m_previewRequested) {
|
||||||
|
refreshPreviewPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void MainWindow::onNewProject() {
|
void MainWindow::onNewProject() {
|
||||||
if (m_workspace.isOpen()) {
|
if (m_workspace.isOpen()) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class EditorCanvas;
|
|||||||
class TimelineWidget;
|
class TimelineWidget;
|
||||||
namespace gui {
|
namespace gui {
|
||||||
class BackgroundPropertySection;
|
class BackgroundPropertySection;
|
||||||
|
class BlackholePropertySection;
|
||||||
class EntityPropertySection;
|
class EntityPropertySection;
|
||||||
class ToolPropertySection;
|
class ToolPropertySection;
|
||||||
class EntityIntroPopup;
|
class EntityIntroPopup;
|
||||||
@@ -100,6 +101,7 @@ private:
|
|||||||
|
|
||||||
void showProjectRootContextMenu(const QPoint& globalPos);
|
void showProjectRootContextMenu(const QPoint& globalPos);
|
||||||
void showBackgroundContextMenu(const QPoint& globalPos);
|
void showBackgroundContextMenu(const QPoint& globalPos);
|
||||||
|
void showBlackholeContextMenu(const QPoint& globalPos, const QString& entityId);
|
||||||
void rebuildCentralPages();
|
void rebuildCentralPages();
|
||||||
void showWelcomePage();
|
void showWelcomePage();
|
||||||
void showEditorPage();
|
void showEditorPage();
|
||||||
@@ -125,6 +127,7 @@ private:
|
|||||||
QComboBox* m_modeSelector = nullptr;
|
QComboBox* m_modeSelector = nullptr;
|
||||||
QStackedWidget* m_propertyStack = nullptr;
|
QStackedWidget* m_propertyStack = nullptr;
|
||||||
gui::BackgroundPropertySection* m_bgPropertySection = nullptr;
|
gui::BackgroundPropertySection* m_bgPropertySection = nullptr;
|
||||||
|
gui::BlackholePropertySection* m_blackholePropertySection = nullptr;
|
||||||
gui::EntityPropertySection* m_entityPropertySection = nullptr;
|
gui::EntityPropertySection* m_entityPropertySection = nullptr;
|
||||||
gui::ToolPropertySection* m_toolPropertySection = nullptr;
|
gui::ToolPropertySection* m_toolPropertySection = nullptr;
|
||||||
QToolButton* m_btnCreateEntity = nullptr;
|
QToolButton* m_btnCreateEntity = nullptr;
|
||||||
@@ -172,6 +175,7 @@ private:
|
|||||||
QPointF m_selectedEntityOrigin;
|
QPointF m_selectedEntityOrigin;
|
||||||
QString m_selectedEntityId;
|
QString m_selectedEntityId;
|
||||||
QString m_selectedToolId;
|
QString m_selectedToolId;
|
||||||
|
QString m_selectedBlackholeEntityId;
|
||||||
QString m_selectedEntityDisplayNameCache;
|
QString m_selectedEntityDisplayNameCache;
|
||||||
QString m_bgAbsCache;
|
QString m_bgAbsCache;
|
||||||
QString m_bgSizeTextCache;
|
QString m_bgSizeTextCache;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Float01ParamControl::Float01ParamControl(QWidget* parent)
|
|||||||
m_spin->setRange(0.0, 1.0);
|
m_spin->setRange(0.0, 1.0);
|
||||||
m_spin->setDecimals(3);
|
m_spin->setDecimals(3);
|
||||||
m_spin->setSingleStep(0.01);
|
m_spin->setSingleStep(0.01);
|
||||||
m_spin->setMinimumWidth(84);
|
m_spin->setMinimumWidth(72);
|
||||||
row->addWidget(m_spin);
|
row->addWidget(m_spin);
|
||||||
|
|
||||||
connect(m_slider, &QSlider::valueChanged, this, [this]() { syncFromSlider(); });
|
connect(m_slider, &QSlider::valueChanged, this, [this]() { syncFromSlider(); });
|
||||||
@@ -79,14 +79,14 @@ Vec2ParamControl::Vec2ParamControl(QWidget* parent)
|
|||||||
m_x->setRange(-1e9, 1e9);
|
m_x->setRange(-1e9, 1e9);
|
||||||
m_x->setDecimals(2);
|
m_x->setDecimals(2);
|
||||||
m_x->setSingleStep(1.0);
|
m_x->setSingleStep(1.0);
|
||||||
m_x->setMinimumWidth(88);
|
m_x->setMinimumWidth(72);
|
||||||
row->addWidget(m_x, 1);
|
row->addWidget(m_x, 1);
|
||||||
|
|
||||||
m_y = new QDoubleSpinBox(this);
|
m_y = new QDoubleSpinBox(this);
|
||||||
m_y->setRange(-1e9, 1e9);
|
m_y->setRange(-1e9, 1e9);
|
||||||
m_y->setDecimals(2);
|
m_y->setDecimals(2);
|
||||||
m_y->setSingleStep(1.0);
|
m_y->setSingleStep(1.0);
|
||||||
m_y->setMinimumWidth(88);
|
m_y->setMinimumWidth(72);
|
||||||
row->addWidget(m_y, 1);
|
row->addWidget(m_y, 1);
|
||||||
|
|
||||||
connect(m_x, qOverload<double>(&QDoubleSpinBox::valueChanged), this, [this]() { emitIfChanged(); });
|
connect(m_x, qOverload<double>(&QDoubleSpinBox::valueChanged), this, [this]() { emitIfChanged(); });
|
||||||
|
|||||||
45
client/gui/props/BlackholePropertySection.cpp
Normal file
45
client/gui/props/BlackholePropertySection.cpp
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#include "props/BlackholePropertySection.h"
|
||||||
|
|
||||||
|
#include <QFormLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
namespace gui {
|
||||||
|
|
||||||
|
BlackholePropertySection::BlackholePropertySection(QWidget* parent)
|
||||||
|
: PropertySectionWidget(parent) {
|
||||||
|
auto* lay = new QVBoxLayout(this);
|
||||||
|
lay->setContentsMargins(0, 0, 0, 0);
|
||||||
|
lay->setSpacing(6);
|
||||||
|
|
||||||
|
auto* form = new QFormLayout();
|
||||||
|
form->setContentsMargins(0, 0, 0, 0);
|
||||||
|
form->setSpacing(6);
|
||||||
|
|
||||||
|
m_name = new QLabel(this);
|
||||||
|
m_status = new QLabel(this);
|
||||||
|
m_method = new QLabel(this);
|
||||||
|
m_method->setWordWrap(true);
|
||||||
|
|
||||||
|
form->addRow(QStringLiteral("黑洞"), m_name);
|
||||||
|
form->addRow(QStringLiteral("是否解决"), m_status);
|
||||||
|
form->addRow(QStringLiteral("解决方案"), m_method);
|
||||||
|
|
||||||
|
lay->addLayout(form);
|
||||||
|
lay->addStretch(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlackholePropertySection::clearDisconnected() {
|
||||||
|
if (m_name) m_name->setText(QStringLiteral("-"));
|
||||||
|
if (m_status) m_status->setText(QStringLiteral("-"));
|
||||||
|
if (m_method) m_method->setText(QStringLiteral("-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlackholePropertySection::applyState(const BlackholePropertyUiState& s) {
|
||||||
|
if (m_name) m_name->setText(s.blackholeName.isEmpty() ? QStringLiteral("-") : s.blackholeName);
|
||||||
|
if (m_status) m_status->setText(s.statusText.isEmpty() ? QStringLiteral("-") : s.statusText);
|
||||||
|
if (m_method) m_method->setText(s.methodText.isEmpty() ? QStringLiteral("-") : s.methodText);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace gui
|
||||||
|
|
||||||
32
client/gui/props/BlackholePropertySection.h
Normal file
32
client/gui/props/BlackholePropertySection.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "props/PropertySectionWidget.h"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
namespace gui {
|
||||||
|
|
||||||
|
struct BlackholePropertyUiState {
|
||||||
|
QString blackholeName;
|
||||||
|
QString statusText;
|
||||||
|
QString methodText;
|
||||||
|
};
|
||||||
|
|
||||||
|
class BlackholePropertySection final : public PropertySectionWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit BlackholePropertySection(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void clearDisconnected();
|
||||||
|
void applyState(const BlackholePropertyUiState& s);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_name = nullptr;
|
||||||
|
QLabel* m_status = nullptr;
|
||||||
|
QLabel* m_method = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace gui
|
||||||
|
|
||||||
@@ -40,13 +40,15 @@ EntityPropertySection::EntityPropertySection(QWidget* parent)
|
|||||||
form->addRow(QStringLiteral("深度"), m_depth);
|
form->addRow(QStringLiteral("深度"), m_depth);
|
||||||
form->addRow(QStringLiteral("距离缩放"), m_distScale);
|
form->addRow(QStringLiteral("距离缩放"), m_distScale);
|
||||||
|
|
||||||
|
m_pivotLabel = new QLabel(QStringLiteral("中心坐标"), this);
|
||||||
m_pivot = new Vec2ParamControl(this);
|
m_pivot = new Vec2ParamControl(this);
|
||||||
m_pivot->setToolTip(QStringLiteral("枢轴在世界坐标中的位置(限制在轮廓包络内),用于重定位局部原点"));
|
m_pivot->setToolTip(QStringLiteral("枢轴在世界坐标中的位置(限制在轮廓包络内),用于重定位局部原点"));
|
||||||
form->addRow(QStringLiteral("中心坐标"), m_pivot);
|
form->addRow(m_pivotLabel, m_pivot);
|
||||||
|
|
||||||
|
m_centroidLabel = new QLabel(QStringLiteral("位置"), this);
|
||||||
m_centroid = new Vec2ParamControl(this);
|
m_centroid = new Vec2ParamControl(this);
|
||||||
m_centroid->setToolTip(QStringLiteral("实体几何质心的世界坐标;修改将整体平移实体"));
|
m_centroid->setToolTip(QStringLiteral("实体几何质心的世界坐标;修改将整体平移实体"));
|
||||||
form->addRow(QStringLiteral("位置"), m_centroid);
|
form->addRow(m_centroidLabel, m_centroid);
|
||||||
|
|
||||||
m_userScale = new QDoubleSpinBox(this);
|
m_userScale = new QDoubleSpinBox(this);
|
||||||
m_userScale->setRange(0.05, 20.0);
|
m_userScale->setRange(0.05, 20.0);
|
||||||
@@ -190,6 +192,14 @@ void EntityPropertySection::clearDisconnected() {
|
|||||||
if (m_distScale) m_distScale->setText(QStringLiteral("-"));
|
if (m_distScale) m_distScale->setText(QStringLiteral("-"));
|
||||||
if (m_pivot) m_pivot->setValue(0.0, 0.0);
|
if (m_pivot) m_pivot->setValue(0.0, 0.0);
|
||||||
if (m_centroid) m_centroid->setValue(0.0, 0.0);
|
if (m_centroid) m_centroid->setValue(0.0, 0.0);
|
||||||
|
if (m_pivotLabel) m_pivotLabel->setText(QStringLiteral("中心坐标"));
|
||||||
|
if (m_centroidLabel) m_centroidLabel->setText(QStringLiteral("位置"));
|
||||||
|
if (m_pivot) {
|
||||||
|
m_pivot->setToolTip(QStringLiteral("枢轴在世界坐标中的位置(限制在轮廓包络内),用于重定位局部原点"));
|
||||||
|
}
|
||||||
|
if (m_centroid) {
|
||||||
|
m_centroid->setToolTip(QStringLiteral("实体几何质心的世界坐标;修改将整体平移实体"));
|
||||||
|
}
|
||||||
if (m_userScale) {
|
if (m_userScale) {
|
||||||
m_userScale->blockSignals(true);
|
m_userScale->blockSignals(true);
|
||||||
m_userScale->setValue(1.0);
|
m_userScale->setValue(1.0);
|
||||||
@@ -239,6 +249,24 @@ void EntityPropertySection::applyState(const EntityPropertyUiState& s) {
|
|||||||
}
|
}
|
||||||
if (m_depth) m_depth->setText(QString::number(s.depthZ));
|
if (m_depth) m_depth->setText(QString::number(s.depthZ));
|
||||||
if (m_distScale) m_distScale->setText(s.distanceScaleText);
|
if (m_distScale) m_distScale->setText(s.distanceScaleText);
|
||||||
|
if (m_pivotLabel) {
|
||||||
|
m_pivotLabel->setText(QStringLiteral("中心坐标"));
|
||||||
|
}
|
||||||
|
if (m_centroidLabel) {
|
||||||
|
m_centroidLabel->setText(QStringLiteral("位置"));
|
||||||
|
}
|
||||||
|
if (m_pivot) {
|
||||||
|
m_pivot->setToolTip(
|
||||||
|
s.parentRelativeMode
|
||||||
|
? QStringLiteral("枢轴相对父对象的坐标;修改将写入相对父对象的位置关键帧")
|
||||||
|
: QStringLiteral("枢轴在世界坐标中的位置(限制在轮廓包络内),用于重定位局部原点"));
|
||||||
|
}
|
||||||
|
if (m_centroid) {
|
||||||
|
m_centroid->setToolTip(
|
||||||
|
s.parentRelativeMode
|
||||||
|
? QStringLiteral("几何质心相对父对象的坐标;修改将写入相对父对象的位置关键帧")
|
||||||
|
: QStringLiteral("实体几何质心的世界坐标;修改将整体平移实体"));
|
||||||
|
}
|
||||||
if (m_pivot) m_pivot->setValue(s.pivot.x(), s.pivot.y());
|
if (m_pivot) m_pivot->setValue(s.pivot.x(), s.pivot.y());
|
||||||
if (m_centroid) m_centroid->setValue(s.centroid.x(), s.centroid.y());
|
if (m_centroid) m_centroid->setValue(s.centroid.x(), s.centroid.y());
|
||||||
if (m_userScale) {
|
if (m_userScale) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ struct EntityPropertyUiState {
|
|||||||
double userScale = 1.0;
|
double userScale = 1.0;
|
||||||
bool ignoreDistanceScale = false;
|
bool ignoreDistanceScale = false;
|
||||||
bool visible = true;
|
bool visible = true;
|
||||||
|
bool parentRelativeMode = false;
|
||||||
core::EntityIntroContent intro;
|
core::EntityIntroContent intro;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,6 +67,8 @@ private:
|
|||||||
QLineEdit* m_name = nullptr;
|
QLineEdit* m_name = nullptr;
|
||||||
QLabel* m_depth = nullptr;
|
QLabel* m_depth = nullptr;
|
||||||
QLabel* m_distScale = nullptr;
|
QLabel* m_distScale = nullptr;
|
||||||
|
QLabel* m_pivotLabel = nullptr;
|
||||||
|
QLabel* m_centroidLabel = nullptr;
|
||||||
Vec2ParamControl* m_pivot = nullptr;
|
Vec2ParamControl* m_pivot = nullptr;
|
||||||
Vec2ParamControl* m_centroid = nullptr;
|
Vec2ParamControl* m_centroid = nullptr;
|
||||||
QDoubleSpinBox* m_userScale = nullptr;
|
QDoubleSpinBox* m_userScale = nullptr;
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
#include "props/ToolPropertySection.h"
|
#include "props/ToolPropertySection.h"
|
||||||
|
|
||||||
|
#include "params/ParamControls.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QCheckBox>
|
#include <QCheckBox>
|
||||||
|
#include <QLabel>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
#include <QSpinBox>
|
#include <QSpinBox>
|
||||||
@@ -26,6 +29,11 @@ ToolPropertySection::ToolPropertySection(QWidget* parent)
|
|||||||
m_text->setPlaceholderText(QStringLiteral("对话内容…"));
|
m_text->setPlaceholderText(QStringLiteral("对话内容…"));
|
||||||
form->addRow(QStringLiteral("文字"), m_text);
|
form->addRow(QStringLiteral("文字"), m_text);
|
||||||
|
|
||||||
|
m_positionLabel = new QLabel(QStringLiteral("位置"), this);
|
||||||
|
m_position = new Vec2ParamControl(this);
|
||||||
|
m_position->setToolTip(QStringLiteral("工具在世界坐标中的位置"));
|
||||||
|
form->addRow(m_positionLabel, m_position);
|
||||||
|
|
||||||
m_pointerT = new QSlider(Qt::Horizontal, this);
|
m_pointerT = new QSlider(Qt::Horizontal, this);
|
||||||
m_pointerT->setRange(0, 1000);
|
m_pointerT->setRange(0, 1000);
|
||||||
m_pointerT->setSingleStep(10);
|
m_pointerT->setSingleStep(10);
|
||||||
@@ -46,7 +54,7 @@ ToolPropertySection::ToolPropertySection(QWidget* parent)
|
|||||||
|
|
||||||
m_visible = new QCheckBox(QString(), this);
|
m_visible = new QCheckBox(QString(), this);
|
||||||
m_visible->setChecked(true);
|
m_visible->setChecked(true);
|
||||||
m_visible->setToolTip(QStringLiteral("随帧变化:在当前帧切换会写入可见性关键帧(10帧淡入淡出)"));
|
m_visible->setToolTip(QStringLiteral("随帧变化:在当前帧切换会写入可见性关键帧"));
|
||||||
form->addRow(QStringLiteral("可见性"), m_visible);
|
form->addRow(QStringLiteral("可见性"), m_visible);
|
||||||
|
|
||||||
lay->addLayout(form);
|
lay->addLayout(form);
|
||||||
@@ -58,11 +66,13 @@ ToolPropertySection::ToolPropertySection(QWidget* parent)
|
|||||||
connect(m_pointerT, &QSlider::valueChanged, this, &ToolPropertySection::pointerTChanged);
|
connect(m_pointerT, &QSlider::valueChanged, this, &ToolPropertySection::pointerTChanged);
|
||||||
connect(m_fontPx, qOverload<int>(&QSpinBox::valueChanged), this, &ToolPropertySection::fontPxChanged);
|
connect(m_fontPx, qOverload<int>(&QSpinBox::valueChanged), this, &ToolPropertySection::fontPxChanged);
|
||||||
connect(m_align, qOverload<int>(&QComboBox::currentIndexChanged), this, &ToolPropertySection::alignChanged);
|
connect(m_align, qOverload<int>(&QComboBox::currentIndexChanged), this, &ToolPropertySection::alignChanged);
|
||||||
|
connect(m_position, &Vec2ParamControl::valueChanged, this, &ToolPropertySection::positionEdited);
|
||||||
connect(m_visible, &QCheckBox::toggled, this, &ToolPropertySection::visibleToggled);
|
connect(m_visible, &QCheckBox::toggled, this, &ToolPropertySection::visibleToggled);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ToolPropertySection::setEditingEnabled(bool on) {
|
void ToolPropertySection::setEditingEnabled(bool on) {
|
||||||
for (auto* w : {static_cast<QWidget*>(m_text), static_cast<QWidget*>(m_pointerT),
|
for (auto* w : {static_cast<QWidget*>(m_text), static_cast<QWidget*>(m_position),
|
||||||
|
static_cast<QWidget*>(m_pointerT),
|
||||||
static_cast<QWidget*>(m_fontPx), static_cast<QWidget*>(m_align),
|
static_cast<QWidget*>(m_fontPx), static_cast<QWidget*>(m_align),
|
||||||
static_cast<QWidget*>(m_visible)}) {
|
static_cast<QWidget*>(m_visible)}) {
|
||||||
if (w) w->setEnabled(on);
|
if (w) w->setEnabled(on);
|
||||||
@@ -76,6 +86,13 @@ void ToolPropertySection::clearDisconnected() {
|
|||||||
m_text->clear();
|
m_text->clear();
|
||||||
m_text->blockSignals(false);
|
m_text->blockSignals(false);
|
||||||
}
|
}
|
||||||
|
if (m_positionLabel) m_positionLabel->setText(QStringLiteral("位置"));
|
||||||
|
if (m_position) {
|
||||||
|
m_position->blockSignals(true);
|
||||||
|
m_position->setToolTip(QStringLiteral("工具在世界坐标中的位置"));
|
||||||
|
m_position->setValue(0.0, 0.0);
|
||||||
|
m_position->blockSignals(false);
|
||||||
|
}
|
||||||
if (m_pointerT) {
|
if (m_pointerT) {
|
||||||
m_pointerT->blockSignals(true);
|
m_pointerT->blockSignals(true);
|
||||||
m_pointerT->setValue(500);
|
m_pointerT->setValue(500);
|
||||||
@@ -105,6 +122,18 @@ void ToolPropertySection::applyState(const ToolPropertyUiState& s) {
|
|||||||
m_text->setText(s.text);
|
m_text->setText(s.text);
|
||||||
m_text->blockSignals(false);
|
m_text->blockSignals(false);
|
||||||
}
|
}
|
||||||
|
if (m_positionLabel) {
|
||||||
|
m_positionLabel->setText(QStringLiteral("位置"));
|
||||||
|
}
|
||||||
|
if (m_position) {
|
||||||
|
m_position->blockSignals(true);
|
||||||
|
m_position->setToolTip(
|
||||||
|
s.parentRelativeMode
|
||||||
|
? QStringLiteral("工具相对父对象的位置;修改将写入相对父对象的位置关键帧")
|
||||||
|
: QStringLiteral("工具在世界坐标中的位置"));
|
||||||
|
m_position->setValue(s.position.x(), s.position.y());
|
||||||
|
m_position->blockSignals(false);
|
||||||
|
}
|
||||||
if (m_pointerT) {
|
if (m_pointerT) {
|
||||||
m_pointerT->blockSignals(true);
|
m_pointerT->blockSignals(true);
|
||||||
m_pointerT->setValue(std::clamp(s.pointerTThousandths, 0, 1000));
|
m_pointerT->setValue(std::clamp(s.pointerTThousandths, 0, 1000));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "props/PropertySectionWidget.h"
|
#include "props/PropertySectionWidget.h"
|
||||||
|
|
||||||
|
#include <QPointF>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
class QComboBox;
|
class QComboBox;
|
||||||
@@ -9,12 +10,19 @@ class QLineEdit;
|
|||||||
class QSlider;
|
class QSlider;
|
||||||
class QSpinBox;
|
class QSpinBox;
|
||||||
class QCheckBox;
|
class QCheckBox;
|
||||||
|
class QLabel;
|
||||||
|
|
||||||
|
namespace gui {
|
||||||
|
class Vec2ParamControl;
|
||||||
|
}
|
||||||
|
|
||||||
namespace gui {
|
namespace gui {
|
||||||
|
|
||||||
struct ToolPropertyUiState {
|
struct ToolPropertyUiState {
|
||||||
QString displayName;
|
QString displayName;
|
||||||
QString text;
|
QString text;
|
||||||
|
QPointF position;
|
||||||
|
bool parentRelativeMode = false;
|
||||||
int pointerTThousandths = 500; // bubblePointerT01 * 1000,0=左 1000=右
|
int pointerTThousandths = 500; // bubblePointerT01 * 1000,0=左 1000=右
|
||||||
int fontPx = 18;
|
int fontPx = 18;
|
||||||
int alignIndex = 1; // 0=left,1=center,2=right
|
int alignIndex = 1; // 0=left,1=center,2=right
|
||||||
@@ -35,10 +43,13 @@ signals:
|
|||||||
void pointerTChanged(int thousandths);
|
void pointerTChanged(int thousandths);
|
||||||
void fontPxChanged(int px);
|
void fontPxChanged(int px);
|
||||||
void alignChanged(int alignIndex);
|
void alignChanged(int alignIndex);
|
||||||
|
void positionEdited(double x, double y);
|
||||||
// 可见性(动画通道):在当前帧写关键帧
|
// 可见性(动画通道):在当前帧写关键帧
|
||||||
void visibleToggled(bool on);
|
void visibleToggled(bool on);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
QLabel* m_positionLabel = nullptr;
|
||||||
|
Vec2ParamControl* m_position = nullptr;
|
||||||
QLineEdit* m_text = nullptr;
|
QLineEdit* m_text = nullptr;
|
||||||
QSlider* m_pointerT = nullptr;
|
QSlider* m_pointerT = nullptr;
|
||||||
QSpinBox* m_fontPx = nullptr;
|
QSpinBox* m_fontPx = nullptr;
|
||||||
|
|||||||
Reference in New Issue
Block a user