Compare commits
4 Commits
d67d7dc0c5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 974946cee4 | |||
| a78b290920 | |||
| 0710090b4d | |||
| 6cb82cec57 |
@@ -53,6 +53,12 @@ public:
|
||||
QVector<QPointF> polygonLocal;
|
||||
// 从背景中抠洞的位置:固定在创建时的 world 坐标,不随实体移动
|
||||
QVector<QPointF> cutoutPolygonWorld;
|
||||
// 背景空缺标识:用于项目树节点和交互,空则按 id 生成默认值
|
||||
QString blackholeId;
|
||||
// 背景空缺可见性:与实体可见性轨道解耦,控制背景空缺是否显示
|
||||
bool blackholeVisible = true;
|
||||
// 背景空缺修复方案:copy_background / use_original_background / model_inpaint(预留)
|
||||
QString blackholeResolvedBy;
|
||||
QPointF originWorld;
|
||||
int depth = 0; // 0..255
|
||||
QString imagePath; // 相对路径,例如 "assets/entities/entity-1.png"
|
||||
@@ -142,6 +148,24 @@ public:
|
||||
void setTools(const QVector<Tool>& tools) { m_tools = tools; }
|
||||
const QVector<Tool>& tools() const { return m_tools; }
|
||||
|
||||
// —— 摄像机(正交俯视;centerWorld 为视口中心;viewScale 为相对参考分辨率 1600×900 的像素/世界单位比,
|
||||
// 预览与视口框按该参考换算,不随实际窗口宽高改变「镜头」所覆盖的世界范围)——
|
||||
struct Camera {
|
||||
QString id;
|
||||
QString displayName;
|
||||
bool visible = true;
|
||||
QPointF centerWorld;
|
||||
double viewScale = 1.0;
|
||||
QVector<Entity::KeyframeVec2> locationKeys;
|
||||
QVector<Entity::KeyframeDouble> scaleKeys;
|
||||
};
|
||||
|
||||
void setCameras(const QVector<Camera>& cameras) { m_cameras = cameras; }
|
||||
const QVector<Camera>& cameras() const { return m_cameras; }
|
||||
|
||||
void setActiveCameraId(const QString& id) { m_activeCameraId = id; }
|
||||
const QString& activeCameraId() const { return m_activeCameraId; }
|
||||
|
||||
// —— 动画系统(Blender/NLA 风格简化版,工程级)——
|
||||
struct AnimationClip {
|
||||
QString id;
|
||||
@@ -156,6 +180,9 @@ public:
|
||||
// Tool channels (keyed by tool id)
|
||||
QHash<QString, QVector<Entity::KeyframeVec2>> toolLocationKeys;
|
||||
QHash<QString, QVector<ToolKeyframeBool>> toolVisibilityKeys;
|
||||
|
||||
QHash<QString, QVector<Entity::KeyframeVec2>> cameraLocationKeys;
|
||||
QHash<QString, QVector<Entity::KeyframeDouble>> cameraScaleKeys;
|
||||
};
|
||||
|
||||
struct NlaStrip {
|
||||
@@ -241,6 +268,8 @@ private:
|
||||
int m_fps = 60;
|
||||
QVector<Entity> m_entities;
|
||||
QVector<Tool> m_tools;
|
||||
QVector<Camera> m_cameras;
|
||||
QString m_activeCameraId;
|
||||
|
||||
QVector<AnimationClip> m_clips;
|
||||
QVector<AnimationScheme> m_schemes;
|
||||
|
||||
@@ -25,6 +25,16 @@ QPointF sampledOriginForEntity(const core::Project::Entity& e,
|
||||
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,
|
||||
const core::Project::AnimationClip* clipOrNull,
|
||||
int localFrame) {
|
||||
@@ -35,6 +45,38 @@ QPointF sampledOriginForTool(const core::Project::Tool& t,
|
||||
return core::sampleLocation(t.locationKeys, localFrame, t.originWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
|
||||
QPointF sampledCenterForCamera(const core::Project::Camera& c,
|
||||
const core::Project::AnimationClip* clipOrNull,
|
||||
int localFrame) {
|
||||
if (clipOrNull && clipOrNull->cameraLocationKeys.contains(c.id)) {
|
||||
const auto& keys = clipOrNull->cameraLocationKeys.value(c.id);
|
||||
return core::sampleLocation(keys, localFrame, c.centerWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
return core::sampleLocation(c.locationKeys, localFrame, c.centerWorld, core::KeyInterpolation::Linear);
|
||||
}
|
||||
|
||||
double sampledViewScaleForCamera(const core::Project::Camera& c,
|
||||
const core::Project::AnimationClip* clipOrNull,
|
||||
int localFrame) {
|
||||
if (clipOrNull && clipOrNull->cameraScaleKeys.contains(c.id)) {
|
||||
const auto& keys = clipOrNull->cameraScaleKeys.value(c.id);
|
||||
if (!keys.isEmpty()) {
|
||||
return core::sampleUserScale(keys, localFrame, c.viewScale, core::KeyInterpolation::Linear);
|
||||
}
|
||||
}
|
||||
return core::sampleUserScale(c.scaleKeys, localFrame, c.viewScale, 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 {
|
||||
int frame = 0;
|
||||
bool value = true;
|
||||
@@ -181,8 +223,10 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
|
||||
ResolvedProjectFrame out;
|
||||
const auto& ents = project.entities();
|
||||
const auto& tools = project.tools();
|
||||
const auto& cams = project.cameras();
|
||||
out.entities.reserve(ents.size());
|
||||
out.tools.reserve(tools.size());
|
||||
out.cameras.reserve(cams.size());
|
||||
|
||||
const StripEvalCtx ctx = resolveStripCtx(project, frame);
|
||||
const int localFrame = ctx.localFrame;
|
||||
@@ -227,24 +271,24 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
|
||||
|
||||
const NodeRef r = index.value(id);
|
||||
QString parentId;
|
||||
QPointF off;
|
||||
QPointF selfSampled;
|
||||
if (r.kind == NodeRef::Kind::Entity) {
|
||||
const auto& e = ents[r.index];
|
||||
parentId = e.parentId;
|
||||
off = e.parentOffsetWorld;
|
||||
selfSampled = sampledOriginForEntity(e, clip, localFrame);
|
||||
} else {
|
||||
const auto& t = tools[r.index];
|
||||
parentId = t.parentId;
|
||||
off = t.parentOffsetWorld;
|
||||
selfSampled = sampledOriginForTool(t, clip, localFrame);
|
||||
}
|
||||
|
||||
QPointF outO = selfSampled;
|
||||
if (!parentId.isEmpty() && index.contains(parentId)) {
|
||||
const QPointF po = resolve(parentId);
|
||||
outO = po + off;
|
||||
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);
|
||||
@@ -304,6 +348,13 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
|
||||
out.tools.push_back(ResolvedTool{t, op});
|
||||
}
|
||||
|
||||
for (const auto& c : cams) {
|
||||
core::Project::Camera cam = c;
|
||||
cam.centerWorld = sampledCenterForCamera(c, clip, localFrame);
|
||||
cam.viewScale = sampledViewScaleForCamera(c, clip, localFrame);
|
||||
out.cameras.push_back(ResolvedCamera{std::move(cam)});
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,14 @@ struct ResolvedTool {
|
||||
double opacity = 1.0; // 0..1
|
||||
};
|
||||
|
||||
struct ResolvedCamera {
|
||||
core::Project::Camera camera; // 拷贝:centerWorld / viewScale 已替换为求值后的值
|
||||
};
|
||||
|
||||
struct ResolvedProjectFrame {
|
||||
QVector<ResolvedEntity> entities;
|
||||
QVector<ResolvedTool> tools;
|
||||
QVector<ResolvedCamera> cameras;
|
||||
};
|
||||
|
||||
/// 逐帧求值:处理父子跟随与工具可见性淡入淡出。
|
||||
|
||||
@@ -67,6 +67,9 @@ QJsonObject entityToJson(const core::Project::Entity& e) {
|
||||
o.insert(QStringLiteral("visible"), e.visible);
|
||||
o.insert(QStringLiteral("polygonLocal"), pointsToJson(e.polygonLocal));
|
||||
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("depth"), e.depth);
|
||||
o.insert(QStringLiteral("imagePath"), e.imagePath);
|
||||
@@ -152,6 +155,12 @@ bool entityFromJson(const QJsonObject& o, core::Project::Entity& out) {
|
||||
// cutout 允许不存在:按空处理
|
||||
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;
|
||||
if (!pointFromJson(o.value(QStringLiteral("originWorld")), p)) {
|
||||
|
||||
@@ -87,6 +87,47 @@ QNetworkReply* ModelServerClient::segmentSamPromptAsync(
|
||||
return m_nam->post(req, body);
|
||||
}
|
||||
|
||||
QNetworkReply* ModelServerClient::inpaintAsync(
|
||||
const QByteArray& cropRgbPngBytes,
|
||||
const QByteArray& maskPngBytes,
|
||||
const QString& prompt,
|
||||
const QString& negativePrompt,
|
||||
double strength,
|
||||
int maxSide,
|
||||
QString* outImmediateError
|
||||
) {
|
||||
if (outImmediateError) {
|
||||
outImmediateError->clear();
|
||||
}
|
||||
if (!m_baseUrl.isValid() || m_baseUrl.isEmpty()) {
|
||||
if (outImmediateError) *outImmediateError = QStringLiteral("后端地址无效。");
|
||||
return nullptr;
|
||||
}
|
||||
if (cropRgbPngBytes.isEmpty()) {
|
||||
if (outImmediateError) *outImmediateError = QStringLiteral("裁剪图像为空。");
|
||||
return nullptr;
|
||||
}
|
||||
if (maskPngBytes.isEmpty()) {
|
||||
if (outImmediateError) *outImmediateError = QStringLiteral("Mask 为空。");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const QUrl url = m_baseUrl.resolved(QUrl(QStringLiteral("/inpaint")));
|
||||
QNetworkRequest req(url);
|
||||
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
|
||||
QJsonObject payload;
|
||||
payload.insert(QStringLiteral("image_b64"), QString::fromLatin1(cropRgbPngBytes.toBase64()));
|
||||
payload.insert(QStringLiteral("mask_b64"), QString::fromLatin1(maskPngBytes.toBase64()));
|
||||
payload.insert(QStringLiteral("prompt"), prompt);
|
||||
payload.insert(QStringLiteral("negative_prompt"), negativePrompt);
|
||||
payload.insert(QStringLiteral("strength"), strength);
|
||||
payload.insert(QStringLiteral("max_side"), maxSide);
|
||||
|
||||
const QByteArray body = QJsonDocument(payload).toJson(QJsonDocument::Compact);
|
||||
return m_nam->post(req, body);
|
||||
}
|
||||
|
||||
bool ModelServerClient::computeDepthPng8(
|
||||
const QByteArray& imageBytes,
|
||||
QByteArray& outPngBytes,
|
||||
|
||||
@@ -37,6 +37,16 @@ public:
|
||||
const QJsonArray& boxXyxy,
|
||||
QString* outImmediateError = nullptr);
|
||||
|
||||
// POST /inpaint,JSON 响应由调用方解析(success / output_image_b64 / error / output_path)。
|
||||
QNetworkReply* inpaintAsync(
|
||||
const QByteArray& cropRgbPngBytes,
|
||||
const QByteArray& maskPngBytes,
|
||||
const QString& prompt,
|
||||
const QString& negativePrompt,
|
||||
double strength,
|
||||
int maxSide,
|
||||
QString* outImmediateError = nullptr);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager* m_nam = nullptr;
|
||||
QUrl m_baseUrl;
|
||||
|
||||
@@ -260,6 +260,13 @@ public:
|
||||
ds << qint32(k.frame) << bool(k.value);
|
||||
}
|
||||
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 {
|
||||
@@ -315,6 +322,16 @@ public:
|
||||
if (!readIntroBlock(ds, tmp.intro)) {
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
@@ -371,7 +388,7 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
|
||||
if (ds.status() != QDataStream::Ok || magic != kMagicPayload) {
|
||||
return false;
|
||||
}
|
||||
if (ver != 1 && ver != 2 && ver != 3 && ver != 4 && ver != 5 && 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;
|
||||
}
|
||||
Project::Entity tmp;
|
||||
@@ -442,6 +459,30 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
|
||||
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 {
|
||||
tmp.displayName.clear();
|
||||
tmp.userScale = 1.0;
|
||||
@@ -449,6 +490,9 @@ bool EntityPayloadBinary::load(const QString& absolutePath, Project::Entity& ent
|
||||
tmp.parentId.clear();
|
||||
tmp.parentOffsetWorld = QPointF();
|
||||
tmp.visibilityKeys.clear();
|
||||
tmp.blackholeVisible = true;
|
||||
tmp.blackholeId = QStringLiteral("blackhole-%1").arg(tmp.id);
|
||||
tmp.blackholeResolvedBy = QStringLiteral("pending");
|
||||
}
|
||||
entity = std::move(tmp);
|
||||
return true;
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace core {
|
||||
class EntityPayloadBinary {
|
||||
public:
|
||||
static constexpr quint32 kMagicPayload = 0x48464550; // 'HFEP'
|
||||
static constexpr quint32 kPayloadVersion = 7; // v7:追加 visibilityKeys
|
||||
static constexpr quint32 kPayloadVersion = 9; // v9:追加 blackholeResolvedBy
|
||||
|
||||
// 旧版独立动画文件(仍用于打开 v1 项目时合并)
|
||||
static constexpr quint32 kMagicLegacyAnim = 0x48465441; // 'HFTA'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QVector>
|
||||
#include <QPoint>
|
||||
|
||||
namespace core {
|
||||
|
||||
@@ -16,7 +17,7 @@ public:
|
||||
static constexpr const char* kProjectIndexFileName = "project.json";
|
||||
static constexpr const char* kAssetsDirName = "assets";
|
||||
// 写入 project.json 的 version 字段;仍可读 version 1(内嵌实体 + 可选 .anim)。
|
||||
static constexpr int kProjectIndexFormatVersion = 3;
|
||||
static constexpr int kProjectIndexFormatVersion = 4;
|
||||
|
||||
ProjectWorkspace() = default;
|
||||
|
||||
@@ -82,6 +83,7 @@ public:
|
||||
|
||||
const QVector<Project::Entity>& entities() const { return m_project.entities(); }
|
||||
const QVector<Project::Tool>& tools() const { return m_project.tools(); }
|
||||
const QVector<Project::Camera>& cameras() const { return m_project.cameras(); }
|
||||
|
||||
bool addTool(const Project::Tool& tool);
|
||||
bool setToolVisible(const QString& id, bool on);
|
||||
@@ -93,8 +95,27 @@ public:
|
||||
bool removeToolVisibilityKey(const QString& id, int frame);
|
||||
bool setToolParent(const QString& id, const QString& parentId, const QPointF& parentOffsetWorld);
|
||||
bool moveToolBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation);
|
||||
bool setCameraVisible(const QString& id, bool on);
|
||||
bool setActiveCameraId(const QString& id);
|
||||
bool setCameraDisplayName(const QString& id, const QString& displayName);
|
||||
bool setCameraCenterWorld(const QString& id, const QPointF& centerWorld);
|
||||
/// keyframeAtFrame >= 0 时同时写入当前活动 clip 的缩放关键帧(与实体 userScale 行为一致)
|
||||
bool setCameraViewScaleValue(const QString& id, double viewScale, int keyframeAtFrame = -1);
|
||||
bool moveCameraBy(const QString& id, const QPointF& delta, int currentFrame, bool autoKeyLocation);
|
||||
bool setCameraLocationKey(const QString& id, int frame, const QPointF& centerWorld);
|
||||
bool setCameraScaleKey(const QString& id, int frame, double viewScale);
|
||||
bool removeCameraLocationKey(const QString& id, int frame);
|
||||
bool removeCameraScaleKey(const QString& id, int frame);
|
||||
bool addEntity(const Project::Entity& entity, const QImage& image);
|
||||
bool setEntityVisible(const QString& id, bool on);
|
||||
bool setEntityBlackholeVisible(const QString& id, bool on);
|
||||
bool resolveBlackholeByUseOriginalBackground(const QString& id);
|
||||
// 复制背景其他区域填充黑洞(sourceOffsetPx 以黑洞包围盒左上角为基准偏移)
|
||||
bool resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
|
||||
bool hideBlackholeAfterFill);
|
||||
// 使用模型补全后的结果写回背景(patchedBackground 已包含补全贴合后的完整背景图像)
|
||||
bool resolveBlackholeByModelInpaint(const QString& id, const QImage& patchedBackground,
|
||||
bool hideBlackholeAfterFill);
|
||||
bool setEntityVisibilityKey(const QString& id, int frame, bool visible);
|
||||
bool removeEntityVisibilityKey(const QString& id, int frame);
|
||||
bool setEntityDisplayName(const QString& id, const QString& displayName);
|
||||
@@ -133,6 +154,8 @@ private:
|
||||
bool syncEntityPayloadsToDisk();
|
||||
bool hydrateEntityPayloadsFromDisk();
|
||||
void loadV1LegacyAnimationSidecars();
|
||||
bool writeIndexJsonWithoutPayloadSync();
|
||||
bool saveSingleEntityPayload(Project::Entity& entity);
|
||||
|
||||
static QJsonObject projectToJson(const Project& project);
|
||||
static bool projectFromJson(const QJsonObject& root, Project& outProject, int* outFileVersion);
|
||||
@@ -144,9 +167,14 @@ private:
|
||||
static bool entityStubFromJsonV2(const QJsonObject& o, Project::Entity& out);
|
||||
static QJsonObject toolToJson(const Project::Tool& t);
|
||||
static bool toolFromJsonV2(const QJsonObject& o, Project::Tool& out);
|
||||
static QJsonObject cameraToJson(const Project::Camera& c);
|
||||
static bool cameraFromJsonV4(const QJsonObject& o, Project::Camera& out);
|
||||
|
||||
/// 若工程尚无摄像机,则创建默认主摄像机(居中背景、缩放适配整张背景图)。不写盘;若创建了摄像机且 outAdded 非空则 *outAdded=true。
|
||||
bool ensureDefaultCameraIfMissing(bool* outAdded = nullptr);
|
||||
|
||||
struct Operation {
|
||||
enum class Type { ImportBackground, SetEntities, SetTools, SetProjectTitle, SetProjectFrameRange };
|
||||
enum class Type { ImportBackground, SetEntities, SetTools, SetCameras, SetProjectTitle, SetProjectFrameRange };
|
||||
Type type {Type::ImportBackground};
|
||||
QString label;
|
||||
QString beforeBackgroundPath;
|
||||
@@ -155,6 +183,8 @@ private:
|
||||
QVector<Project::Entity> afterEntities;
|
||||
QVector<Project::Tool> beforeTools;
|
||||
QVector<Project::Tool> afterTools;
|
||||
QVector<Project::Camera> beforeCameras;
|
||||
QVector<Project::Camera> afterCameras;
|
||||
QString beforeProjectTitle;
|
||||
QString afterProjectTitle;
|
||||
int beforeFrameStart = 0;
|
||||
@@ -169,6 +199,7 @@ private:
|
||||
bool applyBackgroundPath(const QString& relativePath, bool recordHistory, const QString& label);
|
||||
bool applyEntities(const QVector<Project::Entity>& entities, bool recordHistory, const QString& label);
|
||||
bool applyTools(const QVector<Project::Tool>& tools, bool recordHistory, const QString& label);
|
||||
bool applyCameras(const QVector<Project::Camera>& cameras, bool recordHistory, const QString& label);
|
||||
QString copyIntoAssetsAsBackground(const QString& sourceFilePath, const QRect& cropRectInSourceImage);
|
||||
bool writeDepthMap(const QImage& depth8);
|
||||
bool writeDepthMapBytes(const QByteArray& pngBytes);
|
||||
|
||||
@@ -13,13 +13,17 @@ set(GUI_SOURCES
|
||||
${GUI_ROOT}/dialogs/FrameAnimationDialog.cpp
|
||||
${GUI_ROOT}/dialogs/CancelableTaskDialog.cpp
|
||||
${GUI_ROOT}/dialogs/EntityFinalizeDialog.cpp
|
||||
${GUI_ROOT}/dialogs/BlackholeResolveDialog.cpp
|
||||
${GUI_ROOT}/dialogs/InpaintPreviewDialog.cpp
|
||||
${GUI_ROOT}/dialogs/EntityIntroPopup.cpp
|
||||
${GUI_ROOT}/editor/EditorCanvas.cpp
|
||||
${GUI_ROOT}/editor/EntityCutoutUtils.cpp
|
||||
${GUI_ROOT}/params/ParamControls.cpp
|
||||
${GUI_ROOT}/props/BackgroundPropertySection.cpp
|
||||
${GUI_ROOT}/props/BlackholePropertySection.cpp
|
||||
${GUI_ROOT}/props/EntityPropertySection.cpp
|
||||
${GUI_ROOT}/props/ToolPropertySection.cpp
|
||||
${GUI_ROOT}/props/CameraPropertySection.cpp
|
||||
${GUI_ROOT}/timeline/TimelineWidget.cpp
|
||||
)
|
||||
|
||||
@@ -34,13 +38,17 @@ set(GUI_HEADERS
|
||||
${GUI_ROOT}/dialogs/FrameAnimationDialog.h
|
||||
${GUI_ROOT}/dialogs/CancelableTaskDialog.h
|
||||
${GUI_ROOT}/dialogs/EntityFinalizeDialog.h
|
||||
${GUI_ROOT}/dialogs/BlackholeResolveDialog.h
|
||||
${GUI_ROOT}/dialogs/InpaintPreviewDialog.h
|
||||
${GUI_ROOT}/dialogs/EntityIntroPopup.h
|
||||
${GUI_ROOT}/editor/EditorCanvas.h
|
||||
${GUI_ROOT}/editor/EntityCutoutUtils.h
|
||||
${GUI_ROOT}/params/ParamControls.h
|
||||
${GUI_ROOT}/props/BackgroundPropertySection.h
|
||||
${GUI_ROOT}/props/BlackholePropertySection.h
|
||||
${GUI_ROOT}/props/EntityPropertySection.h
|
||||
${GUI_ROOT}/props/ToolPropertySection.h
|
||||
${GUI_ROOT}/props/CameraPropertySection.h
|
||||
${GUI_ROOT}/props/PropertySectionWidget.h
|
||||
${GUI_ROOT}/timeline/TimelineWidget.h
|
||||
)
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#include "main_window/MainWindow.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QImageReader>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationName(QStringLiteral("landscape tool"));
|
||||
|
||||
// 全局放宽 Qt 图片分配限制(默认常见为 256MB),否则超大分辨率背景/深度可能在任意加载路径被拒绝。
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QImageReader::setAllocationLimit(1024); // MB
|
||||
#endif
|
||||
|
||||
MainWindow window;
|
||||
window.show();
|
||||
|
||||
|
||||
208
client/gui/dialogs/BlackholeResolveDialog.cpp
Normal file
208
client/gui/dialogs/BlackholeResolveDialog.cpp
Normal file
@@ -0,0 +1,208 @@
|
||||
#include "dialogs/BlackholeResolveDialog.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QPlainTextEdit>
|
||||
#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);
|
||||
}
|
||||
|
||||
QString BlackholeResolveDialog::promptText() const {
|
||||
return m_promptEdit ? m_promptEdit->toPlainText().trimmed() : QString();
|
||||
}
|
||||
|
||||
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);
|
||||
auto* btnModel = makeAlgoButton(
|
||||
QStringLiteral("模型补全(SDXL Inpaint)"),
|
||||
QStringLiteral("输入提示词,自动补全缺失区域;可预览后再决定是否接受。"),
|
||||
m_pageSelect);
|
||||
layout->addWidget(btnCopy);
|
||||
layout->addWidget(btnOriginal);
|
||||
layout->addWidget(btnModel);
|
||||
|
||||
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);
|
||||
});
|
||||
connect(btnModel, &QPushButton::clicked, this, [this]() {
|
||||
enterAlgorithmPage(Algorithm::ModelInpaint);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 详情页 C:模型补全(提示词)
|
||||
m_modelDetail = new QWidget(m_algoDetails);
|
||||
{
|
||||
auto* mLay = new QVBoxLayout(m_modelDetail);
|
||||
mLay->setSpacing(8);
|
||||
|
||||
auto* panel = new QFrame(m_modelDetail);
|
||||
panel->setFrameShape(QFrame::StyledPanel);
|
||||
auto* pLay = new QVBoxLayout(panel);
|
||||
pLay->setSpacing(8);
|
||||
|
||||
m_promptEdit = new QPlainTextEdit(panel);
|
||||
m_promptEdit->setPlainText(QStringLiteral("This is part of a Chinese painting; please complete the background for me, following the style of the other parts."));
|
||||
m_promptEdit->setMinimumHeight(90);
|
||||
pLay->addWidget(m_promptEdit);
|
||||
|
||||
mLay->addWidget(panel);
|
||||
mLay->addStretch(1);
|
||||
}
|
||||
|
||||
m_algoDetails->addWidget(m_copyDetail);
|
||||
m_algoDetails->addWidget(m_originalDetail);
|
||||
m_algoDetails->addWidget(m_modelDetail);
|
||||
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 if (algo == Algorithm::UseOriginalBackground) {
|
||||
m_detailTitle->setText(QStringLiteral("第 2 步:使用原始背景"));
|
||||
m_detailHint->setText(QStringLiteral("确认后将切换为原始背景显示。"));
|
||||
m_algoDetails->setCurrentWidget(m_originalDetail);
|
||||
} else {
|
||||
m_detailTitle->setText(QStringLiteral("第 2 步:模型补全(SDXL Inpaint)"));
|
||||
m_detailHint->setText(QStringLiteral("输入提示词(可选),点击应用后将生成预览。"));
|
||||
m_algoDetails->setCurrentWidget(m_modelDetail);
|
||||
}
|
||||
m_pages->setCurrentWidget(m_pageDetail);
|
||||
}
|
||||
|
||||
47
client/gui/dialogs/BlackholeResolveDialog.h
Normal file
47
client/gui/dialogs/BlackholeResolveDialog.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QLabel;
|
||||
class QStackedWidget;
|
||||
|
||||
class BlackholeResolveDialog final : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class Algorithm {
|
||||
CopyBackgroundRegion,
|
||||
UseOriginalBackground,
|
||||
ModelInpaint,
|
||||
};
|
||||
|
||||
explicit BlackholeResolveDialog(const QString& blackholeName, QWidget* parent = nullptr);
|
||||
|
||||
Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; }
|
||||
QString promptText() const;
|
||||
|
||||
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;
|
||||
|
||||
QWidget* m_modelDetail = nullptr;
|
||||
class QPlainTextEdit* m_promptEdit = nullptr;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
@@ -171,7 +172,25 @@ void FrameAnimationDialog::updatePreviewForFrame(int frame) {
|
||||
}
|
||||
|
||||
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()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QImageReader>
|
||||
#include <QtMath>
|
||||
|
||||
class ImageCropDialog::CropView final : public QWidget {
|
||||
@@ -159,7 +160,25 @@ ImageCropDialog::ImageCropDialog(const QString& imagePath, QWidget* parent)
|
||||
}
|
||||
|
||||
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()) {
|
||||
reject();
|
||||
}
|
||||
|
||||
68
client/gui/dialogs/InpaintPreviewDialog.cpp
Normal file
68
client/gui/dialogs/InpaintPreviewDialog.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include "dialogs/InpaintPreviewDialog.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPixmap>
|
||||
#include <QScrollArea>
|
||||
#include <QSplitter>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
static QLabel* makeImageLabel(QWidget* parent) {
|
||||
auto* lab = new QLabel(parent);
|
||||
lab->setBackgroundRole(QPalette::Base);
|
||||
lab->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
||||
lab->setScaledContents(false);
|
||||
lab->setAlignment(Qt::AlignCenter);
|
||||
return lab;
|
||||
}
|
||||
|
||||
static QScrollArea* wrapScroll(QWidget* child, QWidget* parent) {
|
||||
auto* sc = new QScrollArea(parent);
|
||||
sc->setWidget(child);
|
||||
sc->setWidgetResizable(true);
|
||||
sc->setBackgroundRole(QPalette::Dark);
|
||||
return sc;
|
||||
}
|
||||
|
||||
InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent)
|
||||
: QDialog(parent) {
|
||||
setModal(true);
|
||||
setMinimumSize(860, 520);
|
||||
setWindowTitle(title);
|
||||
|
||||
auto* root = new QVBoxLayout(this);
|
||||
root->setContentsMargins(8, 8, 8, 8);
|
||||
root->setSpacing(8);
|
||||
|
||||
m_beforeLabel = makeImageLabel(this);
|
||||
m_afterLabel = makeImageLabel(this);
|
||||
m_beforeScroll = wrapScroll(m_beforeLabel, this);
|
||||
m_afterScroll = wrapScroll(m_afterLabel, this);
|
||||
|
||||
auto* splitter = new QSplitter(Qt::Horizontal, this);
|
||||
splitter->addWidget(m_beforeScroll);
|
||||
splitter->addWidget(m_afterScroll);
|
||||
splitter->setStretchFactor(0, 1);
|
||||
splitter->setStretchFactor(1, 1);
|
||||
root->addWidget(splitter, 1);
|
||||
|
||||
auto* btns = new QDialogButtonBox(this);
|
||||
btns->addButton(QStringLiteral("取消"), QDialogButtonBox::RejectRole);
|
||||
btns->addButton(QStringLiteral("接受并写回"), QDialogButtonBox::AcceptRole);
|
||||
connect(btns, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(btns, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
root->addWidget(btns);
|
||||
}
|
||||
|
||||
void InpaintPreviewDialog::setImages(const QImage& before, const QImage& after) {
|
||||
if (m_beforeLabel) {
|
||||
m_beforeLabel->setPixmap(QPixmap::fromImage(before));
|
||||
m_beforeLabel->adjustSize();
|
||||
}
|
||||
if (m_afterLabel) {
|
||||
m_afterLabel->setPixmap(QPixmap::fromImage(after));
|
||||
m_afterLabel->adjustSize();
|
||||
}
|
||||
}
|
||||
|
||||
22
client/gui/dialogs/InpaintPreviewDialog.h
Normal file
22
client/gui/dialogs/InpaintPreviewDialog.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QImage>
|
||||
|
||||
class QLabel;
|
||||
class QScrollArea;
|
||||
|
||||
class InpaintPreviewDialog final : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit InpaintPreviewDialog(const QString& title, QWidget* parent = nullptr);
|
||||
|
||||
void setImages(const QImage& before, const QImage& after);
|
||||
|
||||
private:
|
||||
QLabel* m_beforeLabel = nullptr;
|
||||
QLabel* m_afterLabel = nullptr;
|
||||
QScrollArea* m_beforeScroll = nullptr;
|
||||
QScrollArea* m_afterScroll = nullptr;
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QImageReader>
|
||||
#include <QMimeData>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDropEvent>
|
||||
@@ -32,11 +33,67 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// 摄像机「输出」参考分辨率:视口框与预览缩放均按此换算,避免随窗口大小改变镜头覆盖的世界范围
|
||||
constexpr double kCameraRefViewportW = 1600.0;
|
||||
constexpr double kCameraRefViewportH = 900.0;
|
||||
|
||||
constexpr int kSamCropMargin = 32;
|
||||
constexpr int kMinStrokePointsSam = 4;
|
||||
constexpr int kMinStrokePointsManual = 8;
|
||||
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 cameraWorldViewportRect(const core::Project::Camera& cam) {
|
||||
const double s = std::max(1e-9, cam.viewScale);
|
||||
const double halfW = (kCameraRefViewportW * 0.5) / s;
|
||||
const double halfH = (kCameraRefViewportH * 0.5) / s;
|
||||
return QRectF(cam.centerWorld.x() - halfW, cam.centerWorld.y() - halfH, 2.0 * halfW, 2.0 * halfH);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (strokeWorld.size() < 3 || bgImage.isNull()) {
|
||||
return strokeWorld;
|
||||
@@ -100,6 +157,12 @@ bool buildSamSegmentPayloadFromStroke(
|
||||
if (strokeWorld.size() < kMinStrokePointsSam || bgImage.isNull()) {
|
||||
return false;
|
||||
}
|
||||
outCropPng.clear();
|
||||
outOverlayPng.clear();
|
||||
outPointCoords = QJsonArray{};
|
||||
outPointLabels = QJsonArray{};
|
||||
outBoxXyxy = QJsonArray{};
|
||||
|
||||
const QRectF polyBr = QPolygonF(strokeWorld).boundingRect();
|
||||
if (polyBr.isEmpty()) {
|
||||
return false;
|
||||
@@ -148,30 +211,35 @@ bool buildSamSegmentPayloadFromStroke(
|
||||
}
|
||||
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 step = std::max(1, (n + kMaxSamPointPrompts - 1) / kMaxSamPointPrompts);
|
||||
for (int i = 0; i < n; i += step) {
|
||||
const QPointF L = strokeWorld[i] - origin;
|
||||
outPointCoords.append(QJsonArray{L.x(), L.y()});
|
||||
outPointLabels.append(1);
|
||||
}
|
||||
if (n > 1) {
|
||||
const QPointF Llast = strokeWorld.last() - origin;
|
||||
if (!outPointCoords.isEmpty()) {
|
||||
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 int maxBg = std::max(0, kMaxSamPointPrompts - 1);
|
||||
if (n >= 2 && maxBg > 0) {
|
||||
const int step = std::max(1, (n + maxBg - 1) / maxBg);
|
||||
for (int i = 0; i < n; i += step) {
|
||||
const QPointF L = strokeWorld[i] - origin;
|
||||
const double bx = clampD(L.x(), 0.0, static_cast<double>(cw - 1));
|
||||
const double by = clampD(L.y(), 0.0, static_cast<double>(ch - 1));
|
||||
outPointCoords.append(QJsonArray{bx, by});
|
||||
outPointLabels.append(0);
|
||||
}
|
||||
}
|
||||
|
||||
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 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));
|
||||
@@ -461,6 +529,26 @@ void EditorCanvas::dropEvent(QDropEvent* e) {
|
||||
e->acceptProposedAction();
|
||||
}
|
||||
|
||||
void EditorCanvas::setPreviewCameraViewLocked(bool on) {
|
||||
m_previewCameraViewLocked = on;
|
||||
}
|
||||
|
||||
void EditorCanvas::applyCameraViewport(const QPointF& centerWorld, double viewScale) {
|
||||
// 与 cameraWorldViewportRect 一致:viewScale 表示在 1600×900 参考视口下的像素/世界比;
|
||||
// 实际画布用 min(宽/1600, 高/900) 将参考视口适配进当前控件,使可见世界宽高恒为 1600/s × 900/s。
|
||||
const double pixelRatio =
|
||||
std::min(static_cast<double>(std::max(1, width())) / kCameraRefViewportW,
|
||||
static_cast<double>(std::max(1, height())) / kCameraRefViewportH);
|
||||
const double eff = std::max(1e-9, static_cast<double>(viewScale)) * pixelRatio;
|
||||
m_scale = std::clamp(static_cast<qreal>(eff), 0.05, 50.0);
|
||||
m_pan = QPointF(width() / 2.0, height() / 2.0) - QPointF(centerWorld.x() * m_scale, centerWorld.y() * m_scale);
|
||||
update();
|
||||
}
|
||||
|
||||
QPointF EditorCanvas::viewCenterWorld() const {
|
||||
return viewToWorld(QPointF(width() / 2.0, height() / 2.0));
|
||||
}
|
||||
|
||||
void EditorCanvas::setPresentationPreviewMode(bool on) {
|
||||
if (m_presentationPreviewMode == on) {
|
||||
return;
|
||||
@@ -479,9 +567,11 @@ void EditorCanvas::setPresentationPreviewMode(bool on) {
|
||||
m_presZoomFinishingRestore = false;
|
||||
m_presBgPanSession = false;
|
||||
m_presBgDragDist = 0.0;
|
||||
cancelBlackholeCopyResolve();
|
||||
if (on) {
|
||||
m_tool = Tool::Move;
|
||||
m_selectedEntity = -1;
|
||||
m_selectedBlackholeEntityId.clear();
|
||||
m_draggingEntity = false;
|
||||
m_drawingEntity = false;
|
||||
m_dragMode = DragMode::None;
|
||||
@@ -505,7 +595,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
||||
if (!m_depthAbsPath.isEmpty()) {
|
||||
if (m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
}
|
||||
@@ -521,6 +611,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
||||
const QPointF originWorld = e.originWorld;
|
||||
v.animatedOriginWorld = originWorld;
|
||||
v.cutoutPolygonWorld = e.cutoutPolygonWorld;
|
||||
v.blackholeVisible = e.blackholeVisible;
|
||||
v.distanceScaleCalibMult = e.distanceScaleCalibMult;
|
||||
v.ignoreDistanceScale = e.ignoreDistanceScale;
|
||||
|
||||
@@ -597,6 +688,31 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
||||
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;
|
||||
update();
|
||||
}
|
||||
@@ -630,12 +746,73 @@ void EditorCanvas::setTools(const QVector<core::Project::Tool>& tools, const QVe
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setCameraOverlays(const QVector<core::Project::Camera>& cameras,
|
||||
const QString& selectedId,
|
||||
const QSet<QString>& tempHiddenCameraIds) {
|
||||
m_cameraOverlays = cameras;
|
||||
m_tempHiddenCameraIds = tempHiddenCameraIds;
|
||||
m_selectedCameraId = selectedId;
|
||||
m_selectedCameraIndex = -1;
|
||||
if (!selectedId.isEmpty()) {
|
||||
for (int i = 0; i < m_cameraOverlays.size(); ++i) {
|
||||
if (m_cameraOverlays[i].id == selectedId) {
|
||||
m_selectedCameraIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_selectedCameraIndex < 0) {
|
||||
m_selectedCameraId.clear();
|
||||
m_draggingCamera = false;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::selectCameraById(const QString& id) {
|
||||
if (id.isEmpty()) {
|
||||
clearCameraSelection();
|
||||
return;
|
||||
}
|
||||
clearEntitySelection();
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
emit selectedToolChanged(false, QString(), QPointF());
|
||||
clearBlackholeSelection();
|
||||
for (int i = 0; i < m_cameraOverlays.size(); ++i) {
|
||||
if (m_cameraOverlays[i].id == id) {
|
||||
m_selectedCameraIndex = i;
|
||||
m_selectedCameraId = id;
|
||||
const auto& c = m_cameraOverlays[i];
|
||||
emit selectedCameraChanged(true, id, c.centerWorld, c.viewScale);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
clearCameraSelection();
|
||||
}
|
||||
|
||||
void EditorCanvas::clearCameraSelection() {
|
||||
if (m_selectedCameraId.isEmpty() && m_selectedCameraIndex < 0 && !m_draggingCamera) {
|
||||
return;
|
||||
}
|
||||
m_selectedCameraId.clear();
|
||||
m_selectedCameraIndex = -1;
|
||||
m_draggingCamera = false;
|
||||
emit selectedCameraChanged(false, QString(), QPointF(), 1.0);
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setTempHiddenIds(const QSet<QString>& entityIds, const QSet<QString>& toolIds) {
|
||||
m_tempHiddenEntityIds = entityIds;
|
||||
m_tempHiddenToolIds = toolIds;
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setTempHiddenCameraIds(const QSet<QString>& cameraIds) {
|
||||
m_tempHiddenCameraIds = cameraIds;
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setCurrentFrame(int frame) {
|
||||
if (m_currentFrame == frame) {
|
||||
return;
|
||||
@@ -794,8 +971,10 @@ void EditorCanvas::clearEntitySelection() {
|
||||
void EditorCanvas::selectEntityById(const QString& id) {
|
||||
if (id.isEmpty()) {
|
||||
clearEntitySelection();
|
||||
clearCameraSelection();
|
||||
return;
|
||||
}
|
||||
clearCameraSelection();
|
||||
for (int i = 0; i < m_entities.size(); ++i) {
|
||||
if (m_entities[i].id != id) {
|
||||
continue;
|
||||
@@ -815,10 +994,119 @@ void EditorCanvas::selectEntityById(const QString& id) {
|
||||
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) {
|
||||
if (m_bgAbsPath == absolutePath) {
|
||||
return;
|
||||
}
|
||||
cancelBlackholeCopyResolve();
|
||||
m_bgAbsPath = absolutePath;
|
||||
invalidatePixmap();
|
||||
m_bgImageDirty = true;
|
||||
@@ -997,13 +1285,21 @@ void EditorCanvas::ensurePixmapLoaded() const {
|
||||
m_pixmapDirty = false;
|
||||
m_bgPixmap = QPixmap();
|
||||
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_bgCutoutDirty = true;
|
||||
}
|
||||
|
||||
void EditorCanvas::updateCursor() {
|
||||
if (m_blackholeCopyResolveActive) {
|
||||
setCursor(m_blackholeCopyDragging ? Qt::ClosedHandCursor : Qt::OpenHandCursor);
|
||||
return;
|
||||
}
|
||||
if (m_presentationPreviewMode) {
|
||||
if (m_presHoverEntityIndex >= 0) {
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
@@ -1105,7 +1401,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
if (showBg) {
|
||||
if (m_bgImageDirty) {
|
||||
m_bgImageDirty = false;
|
||||
m_bgImage = QImage(m_bgAbsPath);
|
||||
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);
|
||||
}
|
||||
@@ -1115,7 +1411,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
m_bgCutoutDirty = false;
|
||||
m_bgImageCutout = m_bgImage;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1156,7 +1452,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
if (wantDepth) {
|
||||
if (m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
if (!m_depthImage8.isNull()) {
|
||||
@@ -1173,6 +1469,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) {
|
||||
const auto& ent = m_entities[i];
|
||||
@@ -1280,6 +1618,13 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
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) {
|
||||
const bool showHover = (i == m_presHoverEntityIndex);
|
||||
const bool showFocus = (i == m_presFocusedEntityIndex);
|
||||
@@ -1357,6 +1702,30 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
}
|
||||
}
|
||||
|
||||
// 摄像机视口框(编辑模式)
|
||||
if (!m_presentationPreviewMode) {
|
||||
const qreal handleRWorld = 10.0 / std::max(m_scale, 0.001);
|
||||
for (int i = 0; i < m_cameraOverlays.size(); ++i) {
|
||||
const auto& cam = m_cameraOverlays[i];
|
||||
if (!cam.visible || cam.id.isEmpty() || m_tempHiddenCameraIds.contains(cam.id)) {
|
||||
continue;
|
||||
}
|
||||
const QRectF camRect = cameraWorldViewportRect(cam);
|
||||
QColor fill(80, 140, 255, 38);
|
||||
QColor border(80, 140, 255, 170);
|
||||
if (i == m_selectedCameraIndex) {
|
||||
border = QColor(255, 170, 60, 230);
|
||||
fill = QColor(255, 170, 60, 48);
|
||||
}
|
||||
p.setBrush(fill);
|
||||
p.setPen(QPen(border, 1.5 / std::max(m_scale, 0.001)));
|
||||
p.drawRect(camRect);
|
||||
p.setBrush(QColor(255, 210, 120, 230));
|
||||
p.setPen(QPen(QColor(30, 30, 30, 160), 1.0 / std::max(m_scale, 0.001)));
|
||||
p.drawEllipse(cam.centerWorld, handleRWorld, handleRWorld);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建实体:手绘轨迹预览(world 坐标)
|
||||
if (!m_presentationPreviewMode && m_tool == Tool::CreateEntity && m_drawingEntity && m_strokeWorld.size() >= 2) {
|
||||
QPen pen(QColor(255, 120, 0, 220), 2.0 / std::max<qreal>(m_scale, 0.001));
|
||||
@@ -1398,7 +1767,47 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
p.drawLine(QPointF(0, axisY), QPointF(width(), axisY)); // X 轴(水平)
|
||||
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));
|
||||
tickPen.setWidth(1);
|
||||
p.setPen(tickPen);
|
||||
@@ -1409,28 +1818,44 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
continue;
|
||||
}
|
||||
const qreal tx = std::clamp(vx.x(), 0.0, static_cast<qreal>(width()));
|
||||
p.drawLine(QPointF(tx, axisY), QPointF(tx, axisY + 6));
|
||||
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 上
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
const qreal ty = std::clamp(vy.y(), 0.0, static_cast<qreal>(height()));
|
||||
p.drawLine(QPointF(axisX, ty), QPointF(axisX + 6, ty));
|
||||
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 +1914,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
if (!m_depthAbsPath.isEmpty()) {
|
||||
if (m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
if (!m_depthImage8.isNull()) {
|
||||
@@ -1502,6 +1927,25 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
}
|
||||
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 (e->button() == Qt::LeftButton) {
|
||||
const int hit = hitTestEntity(wp0);
|
||||
@@ -1514,11 +1958,13 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
emit presentationEntityIntroRequested(ent.id, anchorView);
|
||||
return;
|
||||
}
|
||||
m_dragging = true;
|
||||
m_presBgPanSession = true;
|
||||
m_presBgDragDist = 0.0;
|
||||
m_lastMouseView = e->position();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
if (!m_previewCameraViewLocked) {
|
||||
m_dragging = true;
|
||||
m_presBgPanSession = true;
|
||||
m_presBgDragDist = 0.0;
|
||||
m_lastMouseView = e->position();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e->button() == Qt::MiddleButton) {
|
||||
@@ -1579,6 +2025,42 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
}
|
||||
|
||||
if (m_tool == Tool::Move && e->button() == Qt::LeftButton) {
|
||||
// 摄像机:绘制在工具之上,命中优先于工具
|
||||
if (!m_presentationPreviewMode) {
|
||||
const qreal handleRWorld = 12.0 / std::max(m_scale, 0.001);
|
||||
for (int idx = static_cast<int>(m_cameraOverlays.size()) - 1; idx >= 0; --idx) {
|
||||
const auto& cam = m_cameraOverlays[idx];
|
||||
if (!cam.visible || cam.id.isEmpty() || m_tempHiddenCameraIds.contains(cam.id)) {
|
||||
continue;
|
||||
}
|
||||
const QRectF camRect = cameraWorldViewportRect(cam);
|
||||
const double dist = QLineF(worldPos, cam.centerWorld).length();
|
||||
const bool inHandle = dist <= handleRWorld;
|
||||
const bool inRect = camRect.contains(worldPos);
|
||||
if (!inHandle && !inRect) {
|
||||
continue;
|
||||
}
|
||||
m_selectedCameraIndex = idx;
|
||||
m_selectedCameraId = cam.id;
|
||||
m_selectedEntity = -1;
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
m_draggingEntity = false;
|
||||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||||
emit selectedToolChanged(false, QString(), QPointF());
|
||||
if (inHandle) {
|
||||
m_draggingCamera = true;
|
||||
m_cameraDragOffsetWorld = worldPos - cam.centerWorld;
|
||||
m_cameraDragStartCenterWorld = cam.centerWorld;
|
||||
} else {
|
||||
m_draggingCamera = false;
|
||||
}
|
||||
emit selectedCameraChanged(true, cam.id, cam.centerWorld, cam.viewScale);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具(气泡)优先命中:绘制在实体之后,交互也应优先
|
||||
for (qsizetype i = m_tools.size(); i > 0; --i) {
|
||||
const qsizetype idx = i - 1;
|
||||
@@ -1588,6 +2070,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
if (tv.tool.type != core::Project::Tool::Type::Bubble) continue;
|
||||
const QPainterPath path = bubblePathWorld(tv.tool);
|
||||
if (path.contains(worldPos)) {
|
||||
clearCameraSelection();
|
||||
m_selectedTool = static_cast<int>(idx);
|
||||
m_selectedEntity = -1;
|
||||
m_draggingTool = true;
|
||||
@@ -1614,6 +2097,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
const QPointF originView = worldToView(originWorld);
|
||||
const GizmoHit gh = hitTestGizmo(e->position(), originView);
|
||||
if (gh.mode == DragMode::AxisX || gh.mode == DragMode::AxisY) {
|
||||
clearCameraSelection();
|
||||
m_dragging = true;
|
||||
m_draggingEntity = true;
|
||||
m_dragMode = gh.mode;
|
||||
@@ -1657,6 +2141,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
hitSelected = ent.rect.contains(worldPos);
|
||||
}
|
||||
if (hitSelected) {
|
||||
clearCameraSelection();
|
||||
m_draggingEntity = true;
|
||||
m_dragMode = DragMode::Free;
|
||||
emit entityDragActiveChanged(true);
|
||||
@@ -1685,6 +2170,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
|
||||
const int hit = hitTestEntity(worldPos);
|
||||
if (hit >= 0) {
|
||||
clearCameraSelection();
|
||||
m_selectedEntity = hit;
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
@@ -1721,6 +2207,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
m_selectedTool = -1;
|
||||
m_draggingTool = false;
|
||||
m_dragMode = DragMode::None;
|
||||
clearCameraSelection();
|
||||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||||
emit selectedToolChanged(false, QString(), QPointF());
|
||||
update();
|
||||
@@ -1734,7 +2221,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
if (!m_depthAbsPath.isEmpty()) {
|
||||
if (m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
if (!m_depthImage8.isNull()) {
|
||||
@@ -1747,6 +2234,18 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
}
|
||||
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) {
|
||||
const int h = hitTestEntity(wp);
|
||||
if (h != m_presHoverEntityIndex) {
|
||||
@@ -1806,6 +2305,17 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_draggingCamera && m_selectedCameraIndex >= 0 && m_selectedCameraIndex < m_cameraOverlays.size()) {
|
||||
const QPointF worldPos = viewToWorld(cur);
|
||||
const QPointF newCenter = worldPos - m_cameraDragOffsetWorld;
|
||||
QPointF delta = newCenter - m_cameraOverlays[m_selectedCameraIndex].centerWorld;
|
||||
m_cameraOverlays[m_selectedCameraIndex].centerWorld += delta;
|
||||
const auto& c = m_cameraOverlays[m_selectedCameraIndex];
|
||||
emit selectedCameraChanged(true, c.id, c.centerWorld, c.viewScale);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_draggingEntity && m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) {
|
||||
const QPointF worldPos = viewToWorld(cur);
|
||||
auto& ent = m_entities[m_selectedEntity];
|
||||
@@ -1849,7 +2359,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
|
||||
if (!m_depthAbsPath.isEmpty() && m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
if (!m_depthImage8.isNull()) {
|
||||
@@ -1891,16 +2401,30 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
// 平移画布
|
||||
if (m_tool == Tool::Move || (e->buttons() & Qt::MiddleButton) ||
|
||||
(m_presentationPreviewMode && (e->buttons() & Qt::LeftButton))) {
|
||||
if (m_presentationPreviewMode && m_presBgPanSession) {
|
||||
m_presBgDragDist += std::abs(deltaView.x()) + std::abs(deltaView.y());
|
||||
if (!(m_presentationPreviewMode && m_previewCameraViewLocked)) {
|
||||
if (m_presentationPreviewMode && m_presBgPanSession) {
|
||||
m_presBgDragDist += std::abs(deltaView.x()) + std::abs(deltaView.y());
|
||||
}
|
||||
m_pan += deltaView;
|
||||
}
|
||||
m_pan += deltaView;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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 (m_presentationPreviewMode && e->button() == Qt::LeftButton) {
|
||||
if (m_presBgPanSession && m_presBgDragDist < 8.0) {
|
||||
@@ -1971,6 +2495,17 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (m_draggingCamera && m_selectedCameraIndex >= 0 && m_selectedCameraIndex < m_cameraOverlays.size() &&
|
||||
e->button() == Qt::LeftButton) {
|
||||
const auto& cam = m_cameraOverlays[m_selectedCameraIndex];
|
||||
const QPointF delta = cam.centerWorld - m_cameraDragStartCenterWorld;
|
||||
if (!cam.id.isEmpty() && (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y()))) {
|
||||
emit requestMoveCamera(cam.id, delta);
|
||||
} else if (!cam.id.isEmpty()) {
|
||||
emit selectedCameraChanged(true, cam.id, cam.centerWorld, cam.viewScale);
|
||||
}
|
||||
}
|
||||
|
||||
m_dragging = false;
|
||||
if (m_pendingDragging && e->button() == Qt::LeftButton) {
|
||||
m_pendingDragging = false;
|
||||
@@ -1982,6 +2517,7 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
m_draggingEntity = false;
|
||||
m_draggingTool = false;
|
||||
m_draggingCamera = false;
|
||||
m_dragPreviewActive = false;
|
||||
m_dragMode = DragMode::None;
|
||||
updateCursor();
|
||||
@@ -1990,6 +2526,19 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
|
||||
void EditorCanvas::wheelEvent(QWheelEvent* e) {
|
||||
if (m_presentationPreviewMode && m_previewCameraViewLocked) {
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
if (!m_presentationPreviewMode && !m_selectedCameraId.isEmpty() && m_tool == Tool::Move) {
|
||||
const qreal steps = e->angleDelta().y() / 120.0;
|
||||
const qreal factor = std::pow(1.15, steps);
|
||||
if (!qFuzzyCompare(factor, 1.0)) {
|
||||
emit requestCameraViewScaleAdjust(m_selectedCameraId, factor);
|
||||
}
|
||||
e->accept();
|
||||
return;
|
||||
}
|
||||
if (m_tool != Tool::Zoom && !(e->modifiers() & Qt::ControlModifier)) {
|
||||
// 默认仍允许滚轮缩放:不强制用户切换工具
|
||||
//(若你希望仅在 Zoom 工具下才缩放,可在此 return)
|
||||
@@ -2016,6 +2565,11 @@ void EditorCanvas::wheelEvent(QWheelEvent* 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) {
|
||||
clearPresentationEntityFocus();
|
||||
e->accept();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QPixmap>
|
||||
#include <QPoint>
|
||||
#include <QPointF>
|
||||
#include <QImage>
|
||||
#include <QPainterPath>
|
||||
@@ -70,6 +71,13 @@ public:
|
||||
// 预览呈现:完整背景 + 全部实体(忽略显隐开关),隐藏编辑辅助元素,仅可平移/缩放查看
|
||||
void setPresentationPreviewMode(bool on);
|
||||
bool presentationPreviewMode() const { return m_presentationPreviewMode; }
|
||||
/// 预览展示由活动摄像机驱动时禁止画布平移/滚轮缩放,避免与镜头关键帧冲突
|
||||
void setPreviewCameraViewLocked(bool on);
|
||||
bool previewCameraViewLocked() const { return m_previewCameraViewLocked; }
|
||||
/// 将视口对齐到摄像机;viewScale 为相对参考分辨率 1600×900 的像素/世界比(见 Project::Camera)
|
||||
void applyCameraViewport(const QPointF& centerWorld, double viewScale);
|
||||
QPointF viewCenterWorld() const;
|
||||
double viewWorldScale() const { return m_scale; }
|
||||
/// 退出「点击实体放大」状态并平滑回到进入前的视图(预览模式)
|
||||
void clearPresentationEntityFocus();
|
||||
|
||||
@@ -77,7 +85,13 @@ public:
|
||||
const QVector<double>& opacities01,
|
||||
const QString& projectDirAbs);
|
||||
void setTools(const QVector<core::Project::Tool>& tools, const QVector<double>& opacities01);
|
||||
void setCameraOverlays(const QVector<core::Project::Camera>& cameras,
|
||||
const QString& selectedId,
|
||||
const QSet<QString>& tempHiddenCameraIds);
|
||||
void selectCameraById(const QString& id);
|
||||
void clearCameraSelection();
|
||||
void setTempHiddenIds(const QSet<QString>& entityIds, const QSet<QString>& toolIds);
|
||||
void setTempHiddenCameraIds(const QSet<QString>& cameraIds);
|
||||
void setCurrentFrame(int frame);
|
||||
int currentFrame() const { return m_currentFrame; }
|
||||
|
||||
@@ -85,6 +99,12 @@ public:
|
||||
|
||||
void selectEntityById(const QString& id);
|
||||
void clearEntitySelection();
|
||||
void selectBlackholeByEntityId(const QString& entityId);
|
||||
void clearBlackholeSelection();
|
||||
bool startBlackholeCopyResolve(const QString& entityId);
|
||||
void cancelBlackholeCopyResolve();
|
||||
/// 背景图片文件内容被外部写盘更新(路径未变)时,强制重新加载缓存
|
||||
void notifyBackgroundContentChanged();
|
||||
|
||||
// 与动画求值一致的原点/缩放(用于 K 帧与自动关键帧)
|
||||
QPointF selectedAnimatedOriginWorld() const;
|
||||
@@ -115,6 +135,10 @@ signals:
|
||||
void requestFinalizePendingEntity(const QVector<QPointF>& polyWorld);
|
||||
void requestMoveEntity(const QString& id, const QPointF& delta);
|
||||
void requestMoveTool(const QString& id, const QPointF& delta);
|
||||
void requestMoveCamera(const QString& id, const QPointF& delta);
|
||||
void requestCameraViewScaleAdjust(const QString& id, double factor);
|
||||
void selectedCameraChanged(bool hasSelection, const QString& id, const QPointF& centerWorld, double viewScale);
|
||||
void requestResolveBlackholeCopy(const QString& entityId, const QPoint& sourceOffsetPx);
|
||||
void entityDragActiveChanged(bool on);
|
||||
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
|
||||
/// 预览模式下点击实体:anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
|
||||
@@ -171,6 +195,7 @@ private:
|
||||
QPointF animatedOriginWorld;
|
||||
double animatedDepthScale01 = 0.5;
|
||||
double opacity = 1.0; // 0..1(由可见性轨道求值)
|
||||
bool blackholeVisible = true;
|
||||
};
|
||||
int hitTestEntity(const QPointF& worldPos) const;
|
||||
|
||||
@@ -186,7 +211,7 @@ private:
|
||||
mutable QPixmap m_bgPixmap;
|
||||
mutable bool m_pixmapDirty = true;
|
||||
mutable QImage m_bgImage; // 原背景(用于抠图/填充)
|
||||
mutable QImage m_bgImageCutout; // 抠图后的背景(实体区域填黑)
|
||||
mutable QImage m_bgImageCutout; // 抠图后的背景(黑洞区域填黑)
|
||||
mutable bool m_bgImageDirty = true;
|
||||
mutable bool m_bgCutoutDirty = true;
|
||||
|
||||
@@ -229,6 +254,13 @@ private:
|
||||
qint64 m_lastPreviewEmitMs = 0;
|
||||
qint64 m_lastDepthScaleRecalcMs = 0;
|
||||
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;
|
||||
bool m_draggingTool = false;
|
||||
QPointF m_toolDragOffsetOriginWorld;
|
||||
@@ -239,6 +271,14 @@ private:
|
||||
|
||||
QVector<Entity> m_entities;
|
||||
QVector<ToolView> m_tools;
|
||||
QVector<core::Project::Camera> m_cameraOverlays;
|
||||
QSet<QString> m_tempHiddenCameraIds;
|
||||
QString m_selectedCameraId;
|
||||
int m_selectedCameraIndex = -1;
|
||||
bool m_draggingCamera = false;
|
||||
QPointF m_cameraDragOffsetWorld;
|
||||
QPointF m_cameraDragStartCenterWorld;
|
||||
bool m_previewCameraViewLocked = false;
|
||||
QSet<QString> m_tempHiddenEntityIds;
|
||||
QSet<QString> m_tempHiddenToolIds;
|
||||
QVector<QPointF> m_strokeWorld;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,8 +34,10 @@ class EditorCanvas;
|
||||
class TimelineWidget;
|
||||
namespace gui {
|
||||
class BackgroundPropertySection;
|
||||
class BlackholePropertySection;
|
||||
class EntityPropertySection;
|
||||
class ToolPropertySection;
|
||||
class CameraPropertySection;
|
||||
class EntityIntroPopup;
|
||||
class ResourceLibraryDock;
|
||||
}
|
||||
@@ -100,6 +102,7 @@ private:
|
||||
|
||||
void showProjectRootContextMenu(const QPoint& globalPos);
|
||||
void showBackgroundContextMenu(const QPoint& globalPos);
|
||||
void showBlackholeContextMenu(const QPoint& globalPos, const QString& entityId);
|
||||
void rebuildCentralPages();
|
||||
void showWelcomePage();
|
||||
void showEditorPage();
|
||||
@@ -125,8 +128,10 @@ private:
|
||||
QComboBox* m_modeSelector = nullptr;
|
||||
QStackedWidget* m_propertyStack = nullptr;
|
||||
gui::BackgroundPropertySection* m_bgPropertySection = nullptr;
|
||||
gui::BlackholePropertySection* m_blackholePropertySection = nullptr;
|
||||
gui::EntityPropertySection* m_entityPropertySection = nullptr;
|
||||
gui::ToolPropertySection* m_toolPropertySection = nullptr;
|
||||
gui::CameraPropertySection* m_cameraPropertySection = nullptr;
|
||||
QToolButton* m_btnCreateEntity = nullptr;
|
||||
ToolOptionPopup* m_createEntityPopup = nullptr;
|
||||
QToolButton* m_btnToggleDepthOverlay = nullptr;
|
||||
@@ -167,17 +172,21 @@ private:
|
||||
int m_lastWorldZ = -1;
|
||||
bool m_hasSelectedEntity = false;
|
||||
bool m_hasSelectedTool = false;
|
||||
bool m_hasSelectedCamera = false;
|
||||
bool m_syncingTreeSelection = false;
|
||||
int m_selectedEntityDepth = 0;
|
||||
QPointF m_selectedEntityOrigin;
|
||||
QString m_selectedEntityId;
|
||||
QString m_selectedToolId;
|
||||
QString m_selectedCameraId;
|
||||
QString m_selectedBlackholeEntityId;
|
||||
QString m_selectedEntityDisplayNameCache;
|
||||
QString m_bgAbsCache;
|
||||
QString m_bgSizeTextCache;
|
||||
// 项目树“眼睛”:仅用于画布临时隐藏(不持久化、不进时间轴)
|
||||
QSet<QString> m_tempHiddenEntityIds;
|
||||
QSet<QString> m_tempHiddenToolIds;
|
||||
QSet<QString> m_tempHiddenCameraIds;
|
||||
void updateStatusBarText();
|
||||
void syncCreateEntityToolButtonTooltip();
|
||||
void refreshPropertyPanel();
|
||||
|
||||
@@ -24,7 +24,7 @@ Float01ParamControl::Float01ParamControl(QWidget* parent)
|
||||
m_spin->setRange(0.0, 1.0);
|
||||
m_spin->setDecimals(3);
|
||||
m_spin->setSingleStep(0.01);
|
||||
m_spin->setMinimumWidth(84);
|
||||
m_spin->setMinimumWidth(72);
|
||||
row->addWidget(m_spin);
|
||||
|
||||
connect(m_slider, &QSlider::valueChanged, this, [this]() { syncFromSlider(); });
|
||||
@@ -79,14 +79,14 @@ Vec2ParamControl::Vec2ParamControl(QWidget* parent)
|
||||
m_x->setRange(-1e9, 1e9);
|
||||
m_x->setDecimals(2);
|
||||
m_x->setSingleStep(1.0);
|
||||
m_x->setMinimumWidth(88);
|
||||
m_x->setMinimumWidth(72);
|
||||
row->addWidget(m_x, 1);
|
||||
|
||||
m_y = new QDoubleSpinBox(this);
|
||||
m_y->setRange(-1e9, 1e9);
|
||||
m_y->setDecimals(2);
|
||||
m_y->setSingleStep(1.0);
|
||||
m_y->setMinimumWidth(88);
|
||||
m_y->setMinimumWidth(72);
|
||||
row->addWidget(m_y, 1);
|
||||
|
||||
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
|
||||
|
||||
111
client/gui/props/CameraPropertySection.cpp
Normal file
111
client/gui/props/CameraPropertySection.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "props/CameraPropertySection.h"
|
||||
|
||||
#include "params/ParamControls.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace gui {
|
||||
|
||||
CameraPropertySection::CameraPropertySection(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 QLineEdit(this);
|
||||
m_name->setPlaceholderText(QStringLiteral("显示名称…"));
|
||||
form->addRow(QStringLiteral("名称"), m_name);
|
||||
|
||||
m_center = new Vec2ParamControl(this);
|
||||
m_center->setToolTip(QStringLiteral("摄像机中心(世界坐标),与画布上黄色圆点一致"));
|
||||
form->addRow(QStringLiteral("中心"), m_center);
|
||||
|
||||
m_viewScale = new QDoubleSpinBox(this);
|
||||
m_viewScale->setRange(1e-4, 1000.0);
|
||||
m_viewScale->setDecimals(5);
|
||||
m_viewScale->setSingleStep(0.01);
|
||||
m_viewScale->setToolTip(QStringLiteral(
|
||||
"视口缩放:在参考分辨率 1600×900 下的像素/世界单位比(与预览、画布上镜头框一致);"
|
||||
"不随当前窗口大小改变镜头覆盖的世界范围。数值越小,可见的世界范围越大。"));
|
||||
form->addRow(QStringLiteral("缩放"), m_viewScale);
|
||||
|
||||
m_activePreview = new QCheckBox(QStringLiteral("用作预览展示镜头"), this);
|
||||
m_activePreview->setToolTip(QStringLiteral("进入预览展示时,按该摄像机在当前帧的位置与缩放呈现画面"));
|
||||
form->addRow(QStringLiteral("预览"), m_activePreview);
|
||||
|
||||
lay->addLayout(form);
|
||||
lay->addStretch(1);
|
||||
|
||||
connect(m_name, &QLineEdit::editingFinished, this, [this]() {
|
||||
if (m_name) emit displayNameCommitted(m_name->text());
|
||||
});
|
||||
connect(m_center, &Vec2ParamControl::valueChanged, this, [this](double x, double y) { emit centerEdited(x, y); });
|
||||
connect(m_viewScale, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
|
||||
&CameraPropertySection::viewScaleEdited);
|
||||
connect(m_activePreview, &QCheckBox::toggled, this, &CameraPropertySection::activePreviewToggled);
|
||||
}
|
||||
|
||||
void CameraPropertySection::setEditingEnabled(bool on) {
|
||||
for (auto* w : {static_cast<QWidget*>(m_name), static_cast<QWidget*>(m_center), static_cast<QWidget*>(m_viewScale),
|
||||
static_cast<QWidget*>(m_activePreview)}) {
|
||||
if (w) w->setEnabled(on);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraPropertySection::clearDisconnected() {
|
||||
setEditingEnabled(false);
|
||||
if (m_name) {
|
||||
m_name->blockSignals(true);
|
||||
m_name->clear();
|
||||
m_name->blockSignals(false);
|
||||
}
|
||||
if (m_center) {
|
||||
m_center->blockSignals(true);
|
||||
m_center->setValue(0.0, 0.0);
|
||||
m_center->blockSignals(false);
|
||||
}
|
||||
if (m_viewScale) {
|
||||
m_viewScale->blockSignals(true);
|
||||
m_viewScale->setValue(1.0);
|
||||
m_viewScale->blockSignals(false);
|
||||
}
|
||||
if (m_activePreview) {
|
||||
m_activePreview->blockSignals(true);
|
||||
m_activePreview->setChecked(false);
|
||||
m_activePreview->blockSignals(false);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraPropertySection::applyState(const CameraPropertyUiState& s) {
|
||||
setEditingEnabled(true);
|
||||
if (m_name) {
|
||||
m_name->blockSignals(true);
|
||||
m_name->setText(s.displayName);
|
||||
m_name->blockSignals(false);
|
||||
}
|
||||
if (m_center) {
|
||||
m_center->blockSignals(true);
|
||||
m_center->setValue(s.centerWorld.x(), s.centerWorld.y());
|
||||
m_center->blockSignals(false);
|
||||
}
|
||||
if (m_viewScale) {
|
||||
m_viewScale->blockSignals(true);
|
||||
m_viewScale->setValue(s.viewScale);
|
||||
m_viewScale->blockSignals(false);
|
||||
}
|
||||
if (m_activePreview) {
|
||||
m_activePreview->blockSignals(true);
|
||||
m_activePreview->setChecked(s.isActivePreviewCamera);
|
||||
m_activePreview->blockSignals(false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gui
|
||||
47
client/gui/props/CameraPropertySection.h
Normal file
47
client/gui/props/CameraPropertySection.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "props/PropertySectionWidget.h"
|
||||
|
||||
#include <QPointF>
|
||||
#include <QString>
|
||||
|
||||
class QCheckBox;
|
||||
class QDoubleSpinBox;
|
||||
class QLineEdit;
|
||||
|
||||
namespace gui {
|
||||
class Vec2ParamControl;
|
||||
}
|
||||
|
||||
namespace gui {
|
||||
|
||||
struct CameraPropertyUiState {
|
||||
QString displayName;
|
||||
QPointF centerWorld;
|
||||
double viewScale = 1.0;
|
||||
bool isActivePreviewCamera = false;
|
||||
};
|
||||
|
||||
class CameraPropertySection final : public PropertySectionWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CameraPropertySection(QWidget* parent = nullptr);
|
||||
|
||||
void clearDisconnected();
|
||||
void applyState(const CameraPropertyUiState& s);
|
||||
void setEditingEnabled(bool on);
|
||||
|
||||
signals:
|
||||
void displayNameCommitted(const QString& text);
|
||||
void centerEdited(double x, double y);
|
||||
void viewScaleEdited(double viewScale);
|
||||
void activePreviewToggled(bool on);
|
||||
|
||||
private:
|
||||
QLineEdit* m_name = nullptr;
|
||||
Vec2ParamControl* m_center = nullptr;
|
||||
QDoubleSpinBox* m_viewScale = nullptr;
|
||||
QCheckBox* m_activePreview = nullptr;
|
||||
};
|
||||
|
||||
} // namespace gui
|
||||
@@ -40,13 +40,15 @@ EntityPropertySection::EntityPropertySection(QWidget* parent)
|
||||
form->addRow(QStringLiteral("深度"), m_depth);
|
||||
form->addRow(QStringLiteral("距离缩放"), m_distScale);
|
||||
|
||||
m_pivotLabel = new QLabel(QStringLiteral("中心坐标"), this);
|
||||
m_pivot = new Vec2ParamControl(this);
|
||||
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->setToolTip(QStringLiteral("实体几何质心的世界坐标;修改将整体平移实体"));
|
||||
form->addRow(QStringLiteral("位置"), m_centroid);
|
||||
form->addRow(m_centroidLabel, m_centroid);
|
||||
|
||||
m_userScale = new QDoubleSpinBox(this);
|
||||
m_userScale->setRange(0.05, 20.0);
|
||||
@@ -121,8 +123,6 @@ EntityPropertySection::EntityPropertySection(QWidget* parent)
|
||||
lay->addWidget(m_introContent);
|
||||
lay->addStretch(1);
|
||||
|
||||
m_introContent->setVisible(false);
|
||||
|
||||
m_introSaveTimer = new QTimer(this);
|
||||
m_introSaveTimer->setSingleShot(true);
|
||||
connect(m_introSaveTimer, &QTimer::timeout, this, [this]() {
|
||||
@@ -134,6 +134,7 @@ EntityPropertySection::EntityPropertySection(QWidget* parent)
|
||||
connect(m_introToggle, &QToolButton::clicked, this, [this]() {
|
||||
setIntroSectionExpanded(!m_introContent->isVisible());
|
||||
});
|
||||
setIntroSectionExpanded(true);
|
||||
|
||||
connect(m_name, &QLineEdit::editingFinished, this, [this]() {
|
||||
if (m_name) {
|
||||
@@ -190,6 +191,14 @@ void EntityPropertySection::clearDisconnected() {
|
||||
if (m_distScale) m_distScale->setText(QStringLiteral("-"));
|
||||
if (m_pivot) m_pivot->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) {
|
||||
m_userScale->blockSignals(true);
|
||||
m_userScale->setValue(1.0);
|
||||
@@ -221,7 +230,7 @@ void EntityPropertySection::clearDisconnected() {
|
||||
m_introVideo->blockSignals(false);
|
||||
}
|
||||
if (m_introImages) m_introImages->clear();
|
||||
setIntroSectionExpanded(false);
|
||||
setIntroSectionExpanded(true);
|
||||
m_introBulkUpdate = false;
|
||||
}
|
||||
|
||||
@@ -239,6 +248,24 @@ void EntityPropertySection::applyState(const EntityPropertyUiState& s) {
|
||||
}
|
||||
if (m_depth) m_depth->setText(QString::number(s.depthZ));
|
||||
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_centroid) m_centroid->setValue(s.centroid.x(), s.centroid.y());
|
||||
if (m_userScale) {
|
||||
@@ -280,7 +307,6 @@ void EntityPropertySection::applyState(const EntityPropertyUiState& s) {
|
||||
}
|
||||
}
|
||||
}
|
||||
setIntroSectionExpanded(false);
|
||||
m_introBulkUpdate = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ struct EntityPropertyUiState {
|
||||
double userScale = 1.0;
|
||||
bool ignoreDistanceScale = false;
|
||||
bool visible = true;
|
||||
bool parentRelativeMode = false;
|
||||
core::EntityIntroContent intro;
|
||||
};
|
||||
|
||||
@@ -66,6 +67,8 @@ private:
|
||||
QLineEdit* m_name = nullptr;
|
||||
QLabel* m_depth = nullptr;
|
||||
QLabel* m_distScale = nullptr;
|
||||
QLabel* m_pivotLabel = nullptr;
|
||||
QLabel* m_centroidLabel = nullptr;
|
||||
Vec2ParamControl* m_pivot = nullptr;
|
||||
Vec2ParamControl* m_centroid = nullptr;
|
||||
QDoubleSpinBox* m_userScale = nullptr;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "props/ToolPropertySection.h"
|
||||
|
||||
#include "params/ParamControls.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QSlider>
|
||||
#include <QSpinBox>
|
||||
@@ -26,6 +29,11 @@ ToolPropertySection::ToolPropertySection(QWidget* parent)
|
||||
m_text->setPlaceholderText(QStringLiteral("对话内容…"));
|
||||
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->setRange(0, 1000);
|
||||
m_pointerT->setSingleStep(10);
|
||||
@@ -46,7 +54,7 @@ ToolPropertySection::ToolPropertySection(QWidget* parent)
|
||||
|
||||
m_visible = new QCheckBox(QString(), this);
|
||||
m_visible->setChecked(true);
|
||||
m_visible->setToolTip(QStringLiteral("随帧变化:在当前帧切换会写入可见性关键帧(10帧淡入淡出)"));
|
||||
m_visible->setToolTip(QStringLiteral("随帧变化:在当前帧切换会写入可见性关键帧"));
|
||||
form->addRow(QStringLiteral("可见性"), m_visible);
|
||||
|
||||
lay->addLayout(form);
|
||||
@@ -58,11 +66,13 @@ ToolPropertySection::ToolPropertySection(QWidget* parent)
|
||||
connect(m_pointerT, &QSlider::valueChanged, this, &ToolPropertySection::pointerTChanged);
|
||||
connect(m_fontPx, qOverload<int>(&QSpinBox::valueChanged), this, &ToolPropertySection::fontPxChanged);
|
||||
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);
|
||||
}
|
||||
|
||||
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_visible)}) {
|
||||
if (w) w->setEnabled(on);
|
||||
@@ -76,6 +86,13 @@ void ToolPropertySection::clearDisconnected() {
|
||||
m_text->clear();
|
||||
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) {
|
||||
m_pointerT->blockSignals(true);
|
||||
m_pointerT->setValue(500);
|
||||
@@ -105,6 +122,18 @@ void ToolPropertySection::applyState(const ToolPropertyUiState& s) {
|
||||
m_text->setText(s.text);
|
||||
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) {
|
||||
m_pointerT->blockSignals(true);
|
||||
m_pointerT->setValue(std::clamp(s.pointerTThousandths, 0, 1000));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "props/PropertySectionWidget.h"
|
||||
|
||||
#include <QPointF>
|
||||
#include <QString>
|
||||
|
||||
class QComboBox;
|
||||
@@ -9,12 +10,19 @@ class QLineEdit;
|
||||
class QSlider;
|
||||
class QSpinBox;
|
||||
class QCheckBox;
|
||||
class QLabel;
|
||||
|
||||
namespace gui {
|
||||
class Vec2ParamControl;
|
||||
}
|
||||
|
||||
namespace gui {
|
||||
|
||||
struct ToolPropertyUiState {
|
||||
QString displayName;
|
||||
QString text;
|
||||
QPointF position;
|
||||
bool parentRelativeMode = false;
|
||||
int pointerTThousandths = 500; // bubblePointerT01 * 1000,0=左 1000=右
|
||||
int fontPx = 18;
|
||||
int alignIndex = 1; // 0=left,1=center,2=right
|
||||
@@ -35,10 +43,13 @@ signals:
|
||||
void pointerTChanged(int thousandths);
|
||||
void fontPxChanged(int px);
|
||||
void alignChanged(int alignIndex);
|
||||
void positionEdited(double x, double y);
|
||||
// 可见性(动画通道):在当前帧写关键帧
|
||||
void visibleToggled(bool on);
|
||||
|
||||
private:
|
||||
QLabel* m_positionLabel = nullptr;
|
||||
Vec2ParamControl* m_position = nullptr;
|
||||
QLineEdit* m_text = nullptr;
|
||||
QSlider* m_pointerT = nullptr;
|
||||
QSpinBox* m_fontPx = nullptr;
|
||||
|
||||
@@ -420,7 +420,12 @@ def inpaint(req: InpaintRequest) -> Dict[str, Any]:
|
||||
out_path = out_dir / f"{model_name}_inpaint.png"
|
||||
out.save(out_path)
|
||||
|
||||
return {"success": True, "output_path": str(out_path)}
|
||||
# 兼容 Qt 前端:直接返回结果图,避免前端再去读取服务器磁盘路径
|
||||
return {
|
||||
"success": True,
|
||||
"output_path": str(out_path),
|
||||
"output_image_b64": _pil_image_to_png_b64(out),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user