2039 lines
76 KiB
C++
2039 lines
76 KiB
C++
#include "editor/EditorCanvas.h"
|
||
#include "editor/EntityCutoutUtils.h"
|
||
|
||
#include "core/animation/AnimationSampling.h"
|
||
#include "core/depth/DepthService.h"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <QBuffer>
|
||
#include <QDir>
|
||
#include <QFileInfo>
|
||
#include <QCursor>
|
||
#include <QMouseEvent>
|
||
#include <QPainter>
|
||
#include <QPaintEvent>
|
||
#include <QPainterPath>
|
||
#include <QWheelEvent>
|
||
#include <QKeyEvent>
|
||
#include <QJsonArray>
|
||
#include <QJsonDocument>
|
||
#include <QJsonObject>
|
||
#include <QMimeData>
|
||
#include <QDragEnterEvent>
|
||
#include <QDropEvent>
|
||
#include <QTextOption>
|
||
#include <QIODevice>
|
||
#include <QPen>
|
||
#include <QPolygonF>
|
||
|
||
#include "core/library/EntityJson.h"
|
||
#include "core/library/ToolJson.h"
|
||
|
||
namespace {
|
||
|
||
constexpr int kSamCropMargin = 32;
|
||
constexpr int kMinStrokePointsSam = 4;
|
||
constexpr int kMinStrokePointsManual = 8;
|
||
constexpr int kMaxSamPointPrompts = 32;
|
||
|
||
QVector<QPointF> snapStrokeToEdges(const QVector<QPointF>& strokeWorld, const QImage& bgImage, int searchRadiusPx) {
|
||
if (strokeWorld.size() < 3 || bgImage.isNull()) {
|
||
return strokeWorld;
|
||
}
|
||
QImage gray = bgImage.convertToFormat(QImage::Format_Grayscale8);
|
||
const int w = gray.width();
|
||
const int h = gray.height();
|
||
auto at = [&](int x, int y) -> int {
|
||
x = std::clamp(x, 0, w - 1);
|
||
y = std::clamp(y, 0, h - 1);
|
||
return static_cast<int>(static_cast<const uchar*>(gray.constScanLine(y))[x]);
|
||
};
|
||
auto gradMag = [&](int x, int y) -> int {
|
||
// 简易 Sobel 近似(整数)
|
||
const int gx =
|
||
-at(x - 1, y - 1) + at(x + 1, y - 1) +
|
||
-2 * at(x - 1, y) + 2 * at(x + 1, y) +
|
||
-at(x - 1, y + 1) + at(x + 1, y + 1);
|
||
const int gy =
|
||
-at(x - 1, y - 1) - 2 * at(x, y - 1) - at(x + 1, y - 1) +
|
||
at(x - 1, y + 1) + 2 * at(x, y + 1) + at(x + 1, y + 1);
|
||
return std::abs(gx) + std::abs(gy);
|
||
};
|
||
QVector<QPointF> out;
|
||
out.reserve(strokeWorld.size());
|
||
const int r = std::max(1, searchRadiusPx);
|
||
for (const QPointF& p : strokeWorld) {
|
||
const int cx = static_cast<int>(std::round(p.x()));
|
||
const int cy = static_cast<int>(std::round(p.y()));
|
||
int bestX = cx;
|
||
int bestY = cy;
|
||
int bestG = -1;
|
||
for (int dy = -r; dy <= r; ++dy) {
|
||
for (int dx = -r; dx <= r; ++dx) {
|
||
const int x = cx + dx;
|
||
const int y = cy + dy;
|
||
if (x < 0 || y < 0 || x >= w || y >= h) continue;
|
||
const int g = gradMag(x, y);
|
||
if (g > bestG) {
|
||
bestG = g;
|
||
bestX = x;
|
||
bestY = y;
|
||
}
|
||
}
|
||
}
|
||
out.push_back(QPointF(bestX, bestY));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
bool buildSamSegmentPayloadFromStroke(
|
||
const QVector<QPointF>& strokeWorld,
|
||
const QImage& bgImage,
|
||
QByteArray& outCropPng,
|
||
QByteArray& outOverlayPng,
|
||
QPointF& outCropTopLeftWorld,
|
||
QJsonArray& outPointCoords,
|
||
QJsonArray& outPointLabels,
|
||
QJsonArray& outBoxXyxy
|
||
) {
|
||
if (strokeWorld.size() < kMinStrokePointsSam || bgImage.isNull()) {
|
||
return false;
|
||
}
|
||
const QRectF polyBr = QPolygonF(strokeWorld).boundingRect();
|
||
if (polyBr.isEmpty()) {
|
||
return false;
|
||
}
|
||
const QRect cropRect = entity_cutout::clampRectToImage(
|
||
polyBr.adjusted(-kSamCropMargin, -kSamCropMargin, kSamCropMargin, kSamCropMargin).toAlignedRect(),
|
||
bgImage.size());
|
||
if (cropRect.isEmpty()) {
|
||
return false;
|
||
}
|
||
outCropTopLeftWorld = cropRect.topLeft();
|
||
|
||
const QImage cropRgb = bgImage.copy(cropRect).convertToFormat(QImage::Format_RGB888);
|
||
QBuffer bufCrop(&outCropPng);
|
||
if (!bufCrop.open(QIODevice::WriteOnly) || !cropRgb.save(&bufCrop, "PNG")) {
|
||
outCropPng.clear();
|
||
return false;
|
||
}
|
||
bufCrop.close();
|
||
|
||
const QPointF origin = cropRect.topLeft();
|
||
const int cw = cropRect.width();
|
||
const int ch = cropRect.height();
|
||
|
||
QImage overlay(cw, ch, QImage::Format_ARGB32_Premultiplied);
|
||
overlay.fill(Qt::transparent);
|
||
{
|
||
QPainter pop(&overlay);
|
||
pop.setRenderHint(QPainter::Antialiasing, true);
|
||
QPen pen(QColor(255, 60, 60, 240));
|
||
pen.setWidthF(4.0);
|
||
pen.setCapStyle(Qt::RoundCap);
|
||
pen.setJoinStyle(Qt::RoundJoin);
|
||
pop.setPen(pen);
|
||
QPolygonF local;
|
||
local.reserve(strokeWorld.size());
|
||
for (const QPointF& w : strokeWorld) {
|
||
local.append(w - origin);
|
||
}
|
||
pop.drawPolyline(local);
|
||
}
|
||
QBuffer bufOv(&outOverlayPng);
|
||
if (!bufOv.open(QIODevice::WriteOnly) || !overlay.save(&bufOv, "PNG")) {
|
||
outOverlayPng.clear();
|
||
return false;
|
||
}
|
||
bufOv.close();
|
||
|
||
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 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));
|
||
double y2 = clampD(tight.bottom() - origin.y(), 0.0, static_cast<double>(ch - 1));
|
||
if (x2 <= x1) {
|
||
x2 = std::min(static_cast<double>(cw - 1), x1 + 1.0);
|
||
}
|
||
if (y2 <= y1) {
|
||
y2 = std::min(static_cast<double>(ch - 1), y1 + 1.0);
|
||
}
|
||
outBoxXyxy = QJsonArray{x1, y1, x2, y2};
|
||
return true;
|
||
}
|
||
|
||
void drawCheckerboard(QPainter& p, const QRect& r) {
|
||
// 轻量级棋盘格,让透明/纯色背景也有参照
|
||
const int cell = 16;
|
||
const QColor c1(245, 245, 245);
|
||
const QColor c2(230, 230, 230);
|
||
|
||
for (int y = r.top(); y < r.bottom(); y += cell) {
|
||
for (int x = r.left(); x < r.right(); x += cell) {
|
||
const bool odd = ((x / cell) + (y / cell)) % 2;
|
||
p.fillRect(QRect(x, y, cell, cell), odd ? c1 : c2);
|
||
}
|
||
}
|
||
}
|
||
|
||
void drawGrid(QPainter& p, const QRect& r) {
|
||
const int step = 64;
|
||
QPen pen(QColor(0, 0, 0, 24));
|
||
pen.setWidth(1);
|
||
p.setPen(pen);
|
||
|
||
for (int x = r.left(); x <= r.right(); x += step) {
|
||
p.drawLine(QPoint(x, r.top()), QPoint(x, r.bottom()));
|
||
}
|
||
for (int y = r.top(); y <= r.bottom(); y += step) {
|
||
p.drawLine(QPoint(r.left(), y), QPoint(r.right(), y));
|
||
}
|
||
}
|
||
|
||
QRectF transformedRectByScaleAndTranslate(const QRectF& r, const QPointF& center, double scaleRatio, const QPointF& delta) {
|
||
if (r.isNull()) {
|
||
return r.translated(delta);
|
||
}
|
||
const QPointF c = center + delta;
|
||
auto mapPt = [&](const QPointF& p) {
|
||
return c + (p + delta - c) * scaleRatio;
|
||
};
|
||
const QPointF p1 = mapPt(r.topLeft());
|
||
const QPointF p2 = mapPt(r.topRight());
|
||
const QPointF p3 = mapPt(r.bottomLeft());
|
||
const QPointF p4 = mapPt(r.bottomRight());
|
||
const qreal minX = std::min({p1.x(), p2.x(), p3.x(), p4.x()});
|
||
const qreal minY = std::min({p1.y(), p2.y(), p3.y(), p4.y()});
|
||
const qreal maxX = std::max({p1.x(), p2.x(), p3.x(), p4.x()});
|
||
const qreal maxY = std::max({p1.y(), p2.y(), p3.y(), p4.y()});
|
||
return QRectF(QPointF(minX, minY), QPointF(maxX, maxY));
|
||
}
|
||
|
||
int sampleDepthAtPoint(const QImage& depth8, const QPointF& worldPos) {
|
||
if (depth8.isNull() || depth8.format() != QImage::Format_Grayscale8) {
|
||
return 0;
|
||
}
|
||
const int xi = static_cast<int>(std::floor(worldPos.x()));
|
||
const int yi = static_cast<int>(std::floor(worldPos.y()));
|
||
if (xi < 0 || yi < 0 || xi >= depth8.width() || yi >= depth8.height()) {
|
||
return 0;
|
||
}
|
||
return static_cast<int>(depth8.constScanLine(yi)[xi]);
|
||
}
|
||
|
||
double depthToScale01(int depthZ) {
|
||
// 约定:depth=0 最远,depth=255 最近(与后端输出一致)。映射为 0..1(远->0,近->1)。
|
||
const int d = std::clamp(depthZ, 0, 255);
|
||
return static_cast<double>(d) / 255.0;
|
||
}
|
||
|
||
// depth01 0..1 -> 原始距离乘子 0.5..1.5;calibMult>0 时除以创建时记录的基准,使「原位置」为 1.0
|
||
double distanceScaleFromDepth01(double depth01, double calibMult) {
|
||
const double d = std::clamp(depth01, 0.0, 1.0);
|
||
const double raw = 0.5 + d * 1.0;
|
||
if (calibMult > 0.0) {
|
||
return raw / std::max(calibMult, 1e-6);
|
||
}
|
||
return raw;
|
||
}
|
||
|
||
struct GizmoHit {
|
||
EditorCanvas::DragMode mode = EditorCanvas::DragMode::None;
|
||
};
|
||
|
||
GizmoHit hitTestGizmo(const QPointF& mouseView, const QPointF& originView) {
|
||
// 以 view 像素为单位的手柄大小(不随缩放变化)
|
||
const qreal len = 56.0;
|
||
const qreal halfThickness = 6.0;
|
||
|
||
const QRectF xHandle(QPointF(originView.x(), originView.y() - halfThickness),
|
||
QSizeF(len, halfThickness * 2.0));
|
||
const QRectF yHandle(QPointF(originView.x() - halfThickness, originView.y()),
|
||
QSizeF(halfThickness * 2.0, len));
|
||
|
||
if (xHandle.contains(mouseView)) {
|
||
return {EditorCanvas::DragMode::AxisX};
|
||
}
|
||
if (yHandle.contains(mouseView)) {
|
||
return {EditorCanvas::DragMode::AxisY};
|
||
}
|
||
return {};
|
||
}
|
||
|
||
struct BubbleLayoutWorld {
|
||
QPainterPath path;
|
||
QRectF bodyRect;
|
||
};
|
||
|
||
// originWorld = 朝下三角形尖端;滑块改变主体水平位置,使「平直底边」上 t01 对应点始终在尖端正上方(三角竖直、与主体一体平移)
|
||
static BubbleLayoutWorld bubbleLayoutWorld(const core::Project::Tool& tool) {
|
||
const QPointF tip = tool.originWorld;
|
||
const qreal w = 220.0;
|
||
const qreal h = 110.0;
|
||
const qreal rx = 16.0;
|
||
const qreal arrowH = 22.0;
|
||
const double t01 = std::clamp(tool.bubblePointerT01, 0.0, 1.0);
|
||
const qreal spanFlat = std::max(w - 2.0 * rx, 1.0);
|
||
const qreal bodyLeft = tip.x() - rx - static_cast<qreal>(t01) * spanFlat;
|
||
const QRectF body(bodyLeft, tip.y() - (h + arrowH), w, h);
|
||
const qreal halfTri = 14.0;
|
||
const qreal baseCx = tip.x();
|
||
|
||
QPainterPath path;
|
||
path.addRoundedRect(body, rx, rx);
|
||
QPolygonF tri;
|
||
tri << QPointF(baseCx - halfTri, body.bottom()) << QPointF(baseCx + halfTri, body.bottom()) << QPointF(tip.x(), tip.y());
|
||
path.addPolygon(tri);
|
||
return BubbleLayoutWorld{path, body};
|
||
}
|
||
|
||
static QPainterPath bubblePathWorld(const core::Project::Tool& tool) {
|
||
return bubbleLayoutWorld(tool).path;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
EditorCanvas::EditorCanvas(QWidget* parent)
|
||
: QWidget(parent) {
|
||
setAutoFillBackground(false);
|
||
setMinimumSize(480, 320);
|
||
setFocusPolicy(Qt::StrongFocus);
|
||
setMouseTracking(true);
|
||
setAcceptDrops(true);
|
||
|
||
m_previewEmitTimer.start();
|
||
|
||
m_presZoomTimer = new QTimer(this);
|
||
m_presZoomTimer->setInterval(16);
|
||
connect(m_presZoomTimer, &QTimer::timeout, this, &EditorCanvas::tickPresentationZoomAnimation);
|
||
|
||
m_presHoverTimer = new QTimer(this);
|
||
m_presHoverTimer->setInterval(40);
|
||
connect(m_presHoverTimer, &QTimer::timeout, this, &EditorCanvas::tickPresentationHoverAnimation);
|
||
|
||
updateCursor();
|
||
}
|
||
|
||
void EditorCanvas::dragEnterEvent(QDragEnterEvent* e) {
|
||
if (!e || !e->mimeData()) {
|
||
return;
|
||
}
|
||
if (e->mimeData()->hasFormat(QStringLiteral("application/x-hfut-resource+json"))) {
|
||
e->acceptProposedAction();
|
||
return;
|
||
}
|
||
QWidget::dragEnterEvent(e);
|
||
}
|
||
|
||
void EditorCanvas::dragMoveEvent(QDragMoveEvent* e) {
|
||
if (!e || !e->mimeData()) {
|
||
return;
|
||
}
|
||
if (e->mimeData()->hasFormat(QStringLiteral("application/x-hfut-resource+json"))) {
|
||
e->acceptProposedAction();
|
||
return;
|
||
}
|
||
QWidget::dragMoveEvent(e);
|
||
}
|
||
|
||
void EditorCanvas::dropEvent(QDropEvent* e) {
|
||
if (!e || !e->mimeData()) {
|
||
QWidget::dropEvent(e);
|
||
return;
|
||
}
|
||
if (!e->mimeData()->hasFormat(QStringLiteral("application/x-hfut-resource+json"))) {
|
||
QWidget::dropEvent(e);
|
||
return;
|
||
}
|
||
const QByteArray bytes = e->mimeData()->data(QStringLiteral("application/x-hfut-resource+json"));
|
||
const auto doc = QJsonDocument::fromJson(bytes);
|
||
if (!doc.isObject()) {
|
||
e->ignore();
|
||
return;
|
||
}
|
||
const QJsonObject root = doc.object();
|
||
const QString kind = root.value(QStringLiteral("kind")).toString(QStringLiteral("entity"));
|
||
|
||
const QPointF dropWorld = viewToWorld(e->position());
|
||
if (kind == QStringLiteral("tool")) {
|
||
if (!root.value(QStringLiteral("tool")).isObject()) {
|
||
e->ignore();
|
||
return;
|
||
}
|
||
core::Project::Tool t;
|
||
if (!core::library::toolFromJson(root.value(QStringLiteral("tool")).toObject(), t)) {
|
||
e->ignore();
|
||
return;
|
||
}
|
||
// 让主窗口分配 id,避免冲突
|
||
t.id.clear();
|
||
t.parentId.clear();
|
||
t.parentOffsetWorld = QPointF();
|
||
t.originWorld = dropWorld;
|
||
emit requestAddTool(t);
|
||
e->acceptProposedAction();
|
||
return;
|
||
}
|
||
|
||
if (!root.value(QStringLiteral("entity")).isObject()) {
|
||
e->ignore();
|
||
return;
|
||
}
|
||
core::Project::Entity ent;
|
||
if (!core::library::entityFromJson(root.value(QStringLiteral("entity")).toObject(), ent)) {
|
||
e->ignore();
|
||
return;
|
||
}
|
||
|
||
// 让主窗口分配 id,避免资源 id 与工程内冲突
|
||
ent.id.clear();
|
||
ent.imagePath.clear();
|
||
ent.entityPayloadPath.clear();
|
||
ent.legacyAnimSidecarPath.clear();
|
||
|
||
ent.originWorld = dropWorld;
|
||
|
||
// 默认把贴图左上角放到 originWorld + offset
|
||
QPointF imageOffset(-128, -128);
|
||
if (root.value(QStringLiteral("imageOffsetFromOrigin")).isArray()) {
|
||
const QJsonArray a = root.value(QStringLiteral("imageOffsetFromOrigin")).toArray();
|
||
if (a.size() >= 2) {
|
||
imageOffset = QPointF(a.at(0).toDouble(), a.at(1).toDouble());
|
||
}
|
||
}
|
||
ent.imageTopLeftWorld = ent.originWorld + imageOffset;
|
||
|
||
// 生成占位贴图(未来可替换为真实资源图片)
|
||
QSize imgSize(256, 256);
|
||
if (root.value(QStringLiteral("imageSize")).isArray()) {
|
||
const QJsonArray a = root.value(QStringLiteral("imageSize")).toArray();
|
||
if (a.size() >= 2) {
|
||
imgSize = QSize(a.at(0).toInt(256), a.at(1).toInt(256));
|
||
}
|
||
}
|
||
QColor accent(80, 160, 255);
|
||
if (root.value(QStringLiteral("accent")).isArray()) {
|
||
const QJsonArray a = root.value(QStringLiteral("accent")).toArray();
|
||
if (a.size() >= 4) {
|
||
accent = QColor(a.at(0).toInt(80), a.at(1).toInt(160), a.at(2).toInt(255), a.at(3).toInt(255));
|
||
}
|
||
}
|
||
if (!imgSize.isValid()) {
|
||
imgSize = QSize(256, 256);
|
||
}
|
||
QImage img(imgSize, QImage::Format_ARGB32_Premultiplied);
|
||
img.fill(Qt::transparent);
|
||
{
|
||
QPainter p(&img);
|
||
p.setRenderHint(QPainter::Antialiasing, true);
|
||
QRectF rr(QPointF(0, 0), QSizeF(imgSize));
|
||
rr = rr.adjusted(6, 6, -6, -6);
|
||
p.setPen(QPen(QColor(0, 0, 0, 60), 2));
|
||
p.setBrush(QBrush(accent));
|
||
p.drawRoundedRect(rr, 18, 18);
|
||
}
|
||
|
||
emit requestAddEntity(ent, img);
|
||
e->acceptProposedAction();
|
||
}
|
||
|
||
void EditorCanvas::setPresentationPreviewMode(bool on) {
|
||
if (m_presentationPreviewMode == on) {
|
||
return;
|
||
}
|
||
m_presentationPreviewMode = on;
|
||
if (m_presZoomTimer) {
|
||
m_presZoomTimer->stop();
|
||
}
|
||
if (m_presHoverTimer) {
|
||
m_presHoverTimer->stop();
|
||
}
|
||
m_presHoverEntityIndex = -1;
|
||
m_presFocusedEntityIndex = -1;
|
||
m_presHoverPhase = 0.0;
|
||
m_presZoomAnimT = 0.0;
|
||
m_presZoomFinishingRestore = false;
|
||
m_presBgPanSession = false;
|
||
m_presBgDragDist = 0.0;
|
||
if (on) {
|
||
m_tool = Tool::Move;
|
||
m_selectedEntity = -1;
|
||
m_draggingEntity = false;
|
||
m_drawingEntity = false;
|
||
m_dragMode = DragMode::None;
|
||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||
}
|
||
m_bgCutoutDirty = true;
|
||
updateCursor();
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
||
const QVector<double>& opacities01,
|
||
const QString& projectDirAbs) {
|
||
const QString prevSelectedId =
|
||
(m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) ? m_entities[m_selectedEntity].id : QString();
|
||
|
||
m_entities.clear();
|
||
m_entities.reserve(entities.size());
|
||
|
||
// 需要用深度图来自动计算 z 与缩放(逐帧)
|
||
if (!m_depthAbsPath.isEmpty()) {
|
||
if (m_depthDirty) {
|
||
m_depthDirty = false;
|
||
QImage img(m_depthAbsPath);
|
||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||
}
|
||
}
|
||
|
||
const qsizetype nEnt = entities.size();
|
||
for (qsizetype iEnt = 0; iEnt < nEnt; ++iEnt) {
|
||
const auto& e = entities[iEnt];
|
||
Entity v;
|
||
v.id = e.id;
|
||
v.opacity = (iEnt < opacities01.size()) ? std::clamp(opacities01[iEnt], 0.0, 1.0) : 1.0;
|
||
// 注意:MainWindow 传入的是“按当前帧求值后的实体”(包含父子跟随与曲线采样)。
|
||
// 这里必须直接使用 e.originWorld,不能再对 locationKeys 做二次采样,否则父子实体会在刷新时复位/跳变。
|
||
const QPointF originWorld = e.originWorld;
|
||
v.animatedOriginWorld = originWorld;
|
||
v.cutoutPolygonWorld = e.cutoutPolygonWorld;
|
||
v.distanceScaleCalibMult = e.distanceScaleCalibMult;
|
||
v.ignoreDistanceScale = e.ignoreDistanceScale;
|
||
|
||
// 逐帧自动算 z:使用实体多边形质心作为锚点采样深度(O(1)),避免卡顿
|
||
QVector<QPointF> polyTmp;
|
||
polyTmp.reserve(e.polygonLocal.size());
|
||
for (const auto& lp : e.polygonLocal) {
|
||
polyTmp.push_back(originWorld + lp);
|
||
}
|
||
const QPointF cTmp = polyTmp.isEmpty() ? originWorld : entity_cutout::polygonCentroid(polyTmp);
|
||
const int z = (!m_depthImage8.isNull()) ? sampleDepthAtPoint(m_depthImage8, cTmp) : e.depth;
|
||
v.depth = z;
|
||
|
||
const double ds01 = depthToScale01(z);
|
||
v.animatedDepthScale01 = ds01;
|
||
const double userScaleAnimated =
|
||
core::sampleUserScale(e.userScaleKeys, m_currentFrame, e.userScale, core::KeyInterpolation::Linear);
|
||
v.userScale = std::max(1e-6, userScaleAnimated);
|
||
const double distScale = e.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ds01, e.distanceScaleCalibMult);
|
||
const double scale = distScale * v.userScale;
|
||
v.visualScale = scale;
|
||
|
||
v.polygonWorld.clear();
|
||
v.polygonWorld.reserve(e.polygonLocal.size());
|
||
for (const auto& lp : e.polygonLocal) {
|
||
v.polygonWorld.push_back(originWorld + lp * scale);
|
||
}
|
||
|
||
// 贴图按 origin 缩放
|
||
v.imageTopLeft = originWorld + (e.imageTopLeftWorld - e.originWorld) * scale;
|
||
v.pathWorld = entity_cutout::pathFromWorldPolygon(v.polygonWorld);
|
||
v.rect = v.pathWorld.boundingRect();
|
||
v.color = QColor(255, 120, 0, 70);
|
||
|
||
const QString imgRel = core::sampleImagePath(e.imageFrames, m_currentFrame, e.imagePath);
|
||
if (!imgRel.isEmpty() && !projectDirAbs.isEmpty()) {
|
||
const QString abs = QDir(projectDirAbs).filePath(imgRel);
|
||
if (QFileInfo::exists(abs)) {
|
||
QImage img(abs);
|
||
if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) {
|
||
img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
v.image = img;
|
||
}
|
||
}
|
||
m_entities.push_back(v);
|
||
}
|
||
|
||
// 绘制/命中顺序:深度小(远)先画,大(近)后画,近处盖住远处
|
||
std::stable_sort(m_entities.begin(), m_entities.end(),
|
||
[](const Entity& a, const Entity& b) {
|
||
if (a.depth != b.depth) {
|
||
return a.depth < b.depth;
|
||
}
|
||
return a.id < b.id;
|
||
});
|
||
|
||
m_selectedEntity = -1;
|
||
if (!prevSelectedId.isEmpty()) {
|
||
for (int i = 0; i < m_entities.size(); ++i) {
|
||
if (m_entities[i].id == prevSelectedId) {
|
||
m_selectedEntity = i;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (m_selectedEntity >= 0) {
|
||
const auto& ent = m_entities[m_selectedEntity];
|
||
const QPointF origin =
|
||
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
|
||
emit selectedEntityChanged(true, ent.id, ent.depth, origin);
|
||
} else if (!prevSelectedId.isEmpty()) {
|
||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||
}
|
||
|
||
m_bgCutoutDirty = true;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setTools(const QVector<core::Project::Tool>& tools, const QVector<double>& opacities01) {
|
||
m_tools.clear();
|
||
const qsizetype n = tools.size();
|
||
m_tools.reserve(n);
|
||
for (qsizetype i = 0; i < n; ++i) {
|
||
ToolView tv;
|
||
tv.tool = tools[i];
|
||
tv.opacity = (i < opacities01.size()) ? std::clamp(opacities01[i], 0.0, 1.0) : 1.0;
|
||
m_tools.push_back(tv);
|
||
}
|
||
// 轨道变更:若当前选中的工具已不存在,则清除
|
||
if (m_selectedTool >= 0) {
|
||
const QString selId = (m_selectedTool >= 0 && m_selectedTool < m_tools.size()) ? m_tools[m_selectedTool].tool.id : QString();
|
||
if (!selId.isEmpty()) {
|
||
int hit = -1;
|
||
for (int i = 0; i < m_tools.size(); ++i) {
|
||
if (m_tools[i].tool.id == selId) {
|
||
hit = i;
|
||
break;
|
||
}
|
||
}
|
||
m_selectedTool = hit;
|
||
} else {
|
||
m_selectedTool = -1;
|
||
}
|
||
}
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setTempHiddenIds(const QSet<QString>& entityIds, const QSet<QString>& toolIds) {
|
||
m_tempHiddenEntityIds = entityIds;
|
||
m_tempHiddenToolIds = toolIds;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setCurrentFrame(int frame) {
|
||
if (m_currentFrame == frame) {
|
||
return;
|
||
}
|
||
m_currentFrame = std::max(0, frame);
|
||
// 仅切帧时,实体由 MainWindow 刷新时回灌;这里也触发重绘用于坐标轴/叠加
|
||
update();
|
||
}
|
||
|
||
QPointF EditorCanvas::selectedAnimatedOriginWorld() const {
|
||
if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) {
|
||
return {};
|
||
}
|
||
return m_entities[m_selectedEntity].animatedOriginWorld;
|
||
}
|
||
|
||
double EditorCanvas::selectedDepthScale01() const {
|
||
if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) {
|
||
return 0.5;
|
||
}
|
||
return m_entities[m_selectedEntity].animatedDepthScale01;
|
||
}
|
||
|
||
QPointF EditorCanvas::selectedEntityCentroidWorld() const {
|
||
if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) {
|
||
return {};
|
||
}
|
||
// 拖动预览:polygonWorld 不再逐点更新,质心应使用预览值,否则属性面板看起来“不跟随”
|
||
if (!m_presentationPreviewMode && m_draggingEntity && m_dragPreviewActive && m_selectedEntity >= 0) {
|
||
return m_dragCentroidBase + m_dragDelta;
|
||
}
|
||
const auto& ent = m_entities[m_selectedEntity];
|
||
if (!ent.polygonWorld.isEmpty()) {
|
||
return entity_cutout::polygonCentroid(ent.polygonWorld);
|
||
}
|
||
return ent.rect.center();
|
||
}
|
||
|
||
double EditorCanvas::selectedDistanceScaleMultiplier() const {
|
||
if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) {
|
||
return 1.0;
|
||
}
|
||
const auto& ent = m_entities[m_selectedEntity];
|
||
return ent.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ent.animatedDepthScale01, ent.distanceScaleCalibMult);
|
||
}
|
||
|
||
double EditorCanvas::selectedUserScale() const {
|
||
if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) {
|
||
return 1.0;
|
||
}
|
||
return m_entities[m_selectedEntity].userScale;
|
||
}
|
||
|
||
double EditorCanvas::selectedCombinedScale() const {
|
||
return selectedDistanceScaleMultiplier() * selectedUserScale();
|
||
}
|
||
|
||
void EditorCanvas::tickPresentationHoverAnimation() {
|
||
if (!m_presentationPreviewMode) {
|
||
return;
|
||
}
|
||
m_presHoverPhase += 0.35;
|
||
if (m_presHoverPhase > 6.28318530718) {
|
||
m_presHoverPhase -= 6.28318530718;
|
||
}
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::tickPresentationZoomAnimation() {
|
||
m_presZoomAnimT += 0.16;
|
||
qreal u = std::min(1.0, static_cast<qreal>(m_presZoomAnimT));
|
||
u = 1.0 - std::pow(1.0 - u, 3.0);
|
||
m_pan = m_presZoomFromPan + (m_presZoomToPan - m_presZoomFromPan) * u;
|
||
m_scale = m_presZoomFromScale + (m_presZoomToScale - m_presZoomFromScale) * u;
|
||
if (m_presZoomAnimT >= 1.0) {
|
||
m_presZoomTimer->stop();
|
||
m_pan = m_presZoomToPan;
|
||
m_scale = m_presZoomToScale;
|
||
if (m_presZoomFinishingRestore) {
|
||
m_presFocusedEntityIndex = -1;
|
||
m_presZoomFinishingRestore = false;
|
||
}
|
||
}
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::presentationComputeZoomTarget(int entityIndex, QPointF* outPan, qreal* outScale) const {
|
||
if (!outPan || !outScale || entityIndex < 0 || entityIndex >= m_entities.size()) {
|
||
return;
|
||
}
|
||
const Entity& ent = m_entities[entityIndex];
|
||
QRectF bb;
|
||
if (!ent.image.isNull()) {
|
||
const QSizeF sz(ent.image.width() * ent.visualScale, ent.image.height() * ent.visualScale);
|
||
bb = QRectF(ent.imageTopLeft, sz);
|
||
} else if (!ent.polygonWorld.isEmpty()) {
|
||
bb = entity_cutout::pathFromWorldPolygon(ent.polygonWorld).boundingRect();
|
||
} else {
|
||
bb = ent.rect;
|
||
}
|
||
const QPointF c = bb.center();
|
||
const qreal rw = std::max(1.0, bb.width());
|
||
const qreal rh = std::max(1.0, bb.height());
|
||
qreal s = std::min(static_cast<qreal>(width()) / (rw * 1.28), static_cast<qreal>(height()) / (rh * 1.28));
|
||
s = std::clamp(s, 0.12, 14.0);
|
||
*outScale = s;
|
||
*outPan = QPointF(width() / 2.0, height() / 2.0) - c * s;
|
||
}
|
||
|
||
void EditorCanvas::beginPresentationZoomTowardEntity(int entityIndex) {
|
||
if (entityIndex < 0 || entityIndex >= m_entities.size()) {
|
||
return;
|
||
}
|
||
if (m_presFocusedEntityIndex < 0) {
|
||
m_presRestorePan = m_pan;
|
||
m_presRestoreScale = m_scale;
|
||
}
|
||
m_presFocusedEntityIndex = entityIndex;
|
||
m_presZoomFromPan = m_pan;
|
||
m_presZoomFromScale = m_scale;
|
||
presentationComputeZoomTarget(entityIndex, &m_presZoomToPan, &m_presZoomToScale);
|
||
m_presZoomAnimT = 0.0;
|
||
m_presZoomFinishingRestore = false;
|
||
m_presZoomTimer->start();
|
||
}
|
||
|
||
void EditorCanvas::beginPresentationZoomRestore() {
|
||
m_presZoomFromPan = m_pan;
|
||
m_presZoomFromScale = m_scale;
|
||
m_presZoomToPan = m_presRestorePan;
|
||
m_presZoomToScale = m_presRestoreScale;
|
||
m_presZoomAnimT = 0.0;
|
||
m_presZoomFinishingRestore = true;
|
||
m_presZoomTimer->start();
|
||
}
|
||
|
||
void EditorCanvas::clearPresentationEntityFocus() {
|
||
emit presentationInteractionDismissed();
|
||
if (m_presZoomFinishingRestore) {
|
||
return;
|
||
}
|
||
if (m_presFocusedEntityIndex >= 0) {
|
||
beginPresentationZoomRestore();
|
||
}
|
||
}
|
||
|
||
void EditorCanvas::clearEntitySelection() {
|
||
if (m_selectedEntity < 0) {
|
||
return;
|
||
}
|
||
m_selectedEntity = -1;
|
||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::selectEntityById(const QString& id) {
|
||
if (id.isEmpty()) {
|
||
clearEntitySelection();
|
||
return;
|
||
}
|
||
for (int i = 0; i < m_entities.size(); ++i) {
|
||
if (m_entities[i].id != id) {
|
||
continue;
|
||
}
|
||
if (m_selectedEntity == i) {
|
||
update();
|
||
return;
|
||
}
|
||
m_selectedEntity = i;
|
||
const auto& ent = m_entities[i];
|
||
const QPointF origin =
|
||
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
|
||
emit selectedEntityChanged(true, ent.id, ent.depth, origin);
|
||
update();
|
||
return;
|
||
}
|
||
clearEntitySelection();
|
||
}
|
||
|
||
void EditorCanvas::setBackgroundImagePath(const QString& absolutePath) {
|
||
if (m_bgAbsPath == absolutePath) {
|
||
return;
|
||
}
|
||
m_bgAbsPath = absolutePath;
|
||
invalidatePixmap();
|
||
m_bgImageDirty = true;
|
||
m_bgCutoutDirty = true;
|
||
m_bgImage = QImage();
|
||
m_bgImageCutout = QImage();
|
||
zoomToFit();
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setBackgroundVisible(bool on) {
|
||
if (m_backgroundVisible == on) {
|
||
return;
|
||
}
|
||
m_backgroundVisible = on;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setDepthMapPath(const QString& absolutePath) {
|
||
if (m_depthAbsPath == absolutePath) {
|
||
return;
|
||
}
|
||
m_depthAbsPath = absolutePath;
|
||
m_depthDirty = true;
|
||
m_depthImage8 = QImage();
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setDepthOverlayEnabled(bool on) {
|
||
if (m_depthOverlayEnabled == on) {
|
||
return;
|
||
}
|
||
m_depthOverlayEnabled = on;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setTool(Tool tool) {
|
||
if (m_tool == tool) {
|
||
return;
|
||
}
|
||
m_tool = tool;
|
||
m_draggingEntity = false;
|
||
m_drawingEntity = false;
|
||
m_strokeWorld.clear();
|
||
updateCursor();
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setEntityCreateSegmentMode(EntityCreateSegmentMode m) {
|
||
if (m_entityCreateSegmentMode == m) {
|
||
return;
|
||
}
|
||
m_entityCreateSegmentMode = m;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setPendingEntityPolygonWorld(const QVector<QPointF>& polyWorld) {
|
||
m_pendingPolyWorld = polyWorld;
|
||
m_pendingDragging = false;
|
||
m_pendingDragWhole = false;
|
||
m_pendingDragVertex = -1;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::clearPendingEntityPolygon() {
|
||
m_pendingPolyWorld.clear();
|
||
m_pendingDragging = false;
|
||
m_pendingDragWhole = false;
|
||
m_pendingDragVertex = -1;
|
||
update();
|
||
}
|
||
|
||
bool EditorCanvas::isPointNearPendingVertex(const QPointF& worldPos, int* outIndex) const {
|
||
if (outIndex) *outIndex = -1;
|
||
if (m_pendingPolyWorld.size() < 3) return false;
|
||
const qreal rView = 10.0;
|
||
const qreal rWorld = rView / std::max<qreal>(m_scale, 0.001);
|
||
const qreal r2 = rWorld * rWorld;
|
||
int best = -1;
|
||
qreal bestD2 = r2;
|
||
for (int i = 0; i < m_pendingPolyWorld.size(); ++i) {
|
||
const QPointF d = m_pendingPolyWorld[i] - worldPos;
|
||
const qreal d2 = d.x() * d.x() + d.y() * d.y();
|
||
if (d2 <= bestD2) {
|
||
bestD2 = d2;
|
||
best = i;
|
||
}
|
||
}
|
||
if (best >= 0) {
|
||
if (outIndex) *outIndex = best;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool EditorCanvas::pendingPolygonContains(const QPointF& worldPos) const {
|
||
if (m_pendingPolyWorld.size() < 3) return false;
|
||
return entity_cutout::pathFromWorldPolygon(m_pendingPolyWorld).contains(worldPos);
|
||
}
|
||
|
||
void EditorCanvas::resetView() {
|
||
m_scale = 1.0;
|
||
m_pan = QPointF(0, 0);
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::zoomToFit() {
|
||
ensurePixmapLoaded();
|
||
if (m_bgPixmap.isNull() || width() <= 1 || height() <= 1) {
|
||
resetView();
|
||
return;
|
||
}
|
||
|
||
const QSizeF viewSize = size();
|
||
const QSizeF imgSize = m_bgPixmap.size();
|
||
const qreal sx = (viewSize.width() - 24.0) / imgSize.width();
|
||
const qreal sy = (viewSize.height() - 24.0) / imgSize.height();
|
||
const qreal s = std::max<qreal>(0.05, std::min(sx, sy));
|
||
|
||
m_scale = s;
|
||
// 让 world(0,0) 的图像左上角居中显示
|
||
const QSizeF draw(imgSize.width() * s, imgSize.height() * s);
|
||
m_pan = QPointF((viewSize.width() - draw.width()) / 2.0, (viewSize.height() - draw.height()) / 2.0);
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setWorldAxesVisible(bool on) {
|
||
if (m_worldAxesVisible == on) {
|
||
return;
|
||
}
|
||
m_worldAxesVisible = on;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setAxisLabelsVisible(bool on) {
|
||
if (m_axisLabelsVisible == on) {
|
||
return;
|
||
}
|
||
m_axisLabelsVisible = on;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setGizmoLabelsVisible(bool on) {
|
||
if (m_gizmoLabelsVisible == on) {
|
||
return;
|
||
}
|
||
m_gizmoLabelsVisible = on;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setGridVisible(bool on) {
|
||
if (m_gridVisible == on) {
|
||
return;
|
||
}
|
||
m_gridVisible = on;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setCheckerboardVisible(bool on) {
|
||
if (m_checkerboardVisible == on) {
|
||
return;
|
||
}
|
||
m_checkerboardVisible = on;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::invalidatePixmap() {
|
||
m_pixmapDirty = true;
|
||
m_bgPixmap = QPixmap();
|
||
}
|
||
|
||
void EditorCanvas::ensurePixmapLoaded() const {
|
||
if (!m_pixmapDirty) {
|
||
return;
|
||
}
|
||
m_pixmapDirty = false;
|
||
m_bgPixmap = QPixmap();
|
||
if (!m_bgAbsPath.isEmpty()) {
|
||
m_bgPixmap = QPixmap(m_bgAbsPath);
|
||
}
|
||
m_bgImageDirty = true;
|
||
m_bgCutoutDirty = true;
|
||
}
|
||
|
||
void EditorCanvas::updateCursor() {
|
||
if (m_presentationPreviewMode) {
|
||
if (m_presHoverEntityIndex >= 0) {
|
||
setCursor(Qt::PointingHandCursor);
|
||
} else {
|
||
setCursor(Qt::OpenHandCursor);
|
||
}
|
||
return;
|
||
}
|
||
switch (m_tool) {
|
||
case Tool::Move:
|
||
setCursor(Qt::OpenHandCursor);
|
||
break;
|
||
case Tool::Zoom:
|
||
setCursor(Qt::CrossCursor);
|
||
break;
|
||
case Tool::CreateEntity:
|
||
setCursor(Qt::CrossCursor);
|
||
break;
|
||
}
|
||
}
|
||
|
||
QPointF EditorCanvas::viewToWorld(const QPointF& v) const {
|
||
if (m_scale <= 0.0) {
|
||
return {};
|
||
}
|
||
return (v - m_pan) / m_scale;
|
||
}
|
||
|
||
QPointF EditorCanvas::worldToView(const QPointF& w) const {
|
||
return w * m_scale + m_pan;
|
||
}
|
||
|
||
QRectF EditorCanvas::worldRectOfBackground() const {
|
||
ensurePixmapLoaded();
|
||
if (m_bgPixmap.isNull()) {
|
||
return {};
|
||
}
|
||
return QRectF(0, 0, m_bgPixmap.width(), m_bgPixmap.height());
|
||
}
|
||
|
||
int EditorCanvas::hitTestEntity(const QPointF& worldPos) const {
|
||
for (qsizetype i = m_entities.size(); i > 0; --i) {
|
||
const qsizetype idx = i - 1;
|
||
const auto& ent = m_entities[idx];
|
||
if (ent.opacity <= 0.001) {
|
||
continue;
|
||
}
|
||
if (!ent.id.isEmpty() && m_tempHiddenEntityIds.contains(ent.id)) {
|
||
continue;
|
||
}
|
||
if (!ent.polygonWorld.isEmpty()) {
|
||
const QPainterPath path = entity_cutout::pathFromWorldPolygon(ent.polygonWorld);
|
||
if (path.contains(worldPos)) {
|
||
return static_cast<int>(idx);
|
||
}
|
||
continue;
|
||
}
|
||
if (ent.rect.contains(worldPos)) {
|
||
return static_cast<int>(idx);
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||
Q_UNUSED(e);
|
||
|
||
QPainter p(this);
|
||
p.setRenderHint(QPainter::Antialiasing, false);
|
||
// 拖动时优先保证交互帧率:缩放贴图使用 nearest 以减少开销
|
||
p.setRenderHint(QPainter::SmoothPixmapTransform, !m_draggingEntity);
|
||
|
||
const QRect r = rect();
|
||
if (!m_presentationPreviewMode) {
|
||
if (m_checkerboardVisible) {
|
||
drawCheckerboard(p, r);
|
||
}
|
||
if (m_gridVisible) {
|
||
drawGrid(p, r);
|
||
}
|
||
}
|
||
|
||
ensurePixmapLoaded();
|
||
if (m_bgAbsPath.isEmpty()) {
|
||
p.setPen(palette().text().color());
|
||
p.drawText(r.adjusted(12, 12, -12, -12), Qt::AlignCenter,
|
||
QStringLiteral("(暂无背景)\n右键项目树中的“背景”可更换/裁剪"));
|
||
return;
|
||
}
|
||
if (m_bgPixmap.isNull()) {
|
||
p.setPen(palette().text().color());
|
||
p.drawText(r.adjusted(12, 12, -12, -12), Qt::AlignCenter,
|
||
QStringLiteral("(背景加载失败)\n%1").arg(m_bgAbsPath));
|
||
return;
|
||
}
|
||
|
||
// 预览:始终加载完整背景图,不做抠洞
|
||
const bool showBg = m_presentationPreviewMode || m_backgroundVisible;
|
||
if (showBg) {
|
||
if (m_bgImageDirty) {
|
||
m_bgImageDirty = false;
|
||
m_bgImage = QImage(m_bgAbsPath);
|
||
if (m_bgImage.format() != QImage::Format_ARGB32_Premultiplied && !m_bgImage.isNull()) {
|
||
m_bgImage = m_bgImage.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
m_bgCutoutDirty = true;
|
||
}
|
||
if (!m_presentationPreviewMode && m_bgCutoutDirty) {
|
||
m_bgCutoutDirty = false;
|
||
m_bgImageCutout = m_bgImage;
|
||
for (const auto& ent : m_entities) {
|
||
if (ent.opacity > 0.001 && !ent.cutoutPolygonWorld.isEmpty() && !m_bgImageCutout.isNull()) {
|
||
entity_cutout::applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 以“世界坐标”绘制:支持缩放/平移
|
||
p.save();
|
||
QTransform t;
|
||
t.translate(m_pan.x(), m_pan.y());
|
||
t.scale(m_scale, m_scale);
|
||
p.setTransform(t, true);
|
||
|
||
// 背景(预览:完整图;编辑:可抠洞 / 可隐藏看深度)
|
||
if (showBg) {
|
||
if (m_presentationPreviewMode) {
|
||
if (!m_bgImage.isNull()) {
|
||
p.drawImage(QPointF(0, 0), m_bgImage);
|
||
} else {
|
||
p.drawPixmap(QPointF(0, 0), m_bgPixmap);
|
||
}
|
||
} else if (m_backgroundVisible) {
|
||
if (!m_bgImageCutout.isNull()) {
|
||
p.drawImage(QPointF(0, 0), m_bgImageCutout);
|
||
} else {
|
||
p.drawPixmap(QPointF(0, 0), m_bgPixmap);
|
||
}
|
||
}
|
||
if (!m_presentationPreviewMode && m_backgroundVisible) {
|
||
p.setPen(QPen(QColor(0, 0, 0, 80), 1.0 / std::max<qreal>(m_scale, 0.001)));
|
||
p.drawRect(QRectF(0, 0, m_bgPixmap.width(), m_bgPixmap.height()).adjusted(0, 0, -1, -1));
|
||
}
|
||
}
|
||
|
||
// 深度叠加(伪彩色):仅由「叠加深度」开关控制,与是否显示背景无关
|
||
const bool wantDepth =
|
||
!m_presentationPreviewMode && (!m_depthAbsPath.isEmpty()) && m_depthOverlayEnabled;
|
||
if (wantDepth) {
|
||
if (m_depthDirty) {
|
||
m_depthDirty = false;
|
||
QImage img(m_depthAbsPath);
|
||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||
}
|
||
if (!m_depthImage8.isNull()) {
|
||
const int overlayAlpha = m_backgroundVisible ? m_depthOverlayAlpha : 255;
|
||
const QImage overlay =
|
||
core::DepthService::depthToColormapOverlay(m_depthImage8, overlayAlpha);
|
||
if (!overlay.isNull()) {
|
||
p.drawImage(QPointF(0, 0), overlay);
|
||
} else {
|
||
p.drawImage(QPointF(0, 0), m_depthImage8);
|
||
}
|
||
p.setPen(QPen(QColor(0, 0, 0, 80), 1.0 / std::max<qreal>(m_scale, 0.001)));
|
||
p.drawRect(QRectF(0, 0, m_depthImage8.width(), m_depthImage8.height()).adjusted(0, 0, -1, -1));
|
||
}
|
||
}
|
||
|
||
// 实体元素(占位):后续可替换为真实数据
|
||
for (int i = 0; i < m_entities.size(); ++i) {
|
||
const auto& ent = m_entities[i];
|
||
if (ent.opacity <= 0.001) {
|
||
continue;
|
||
}
|
||
if (!ent.id.isEmpty() && m_tempHiddenEntityIds.contains(ent.id)) {
|
||
continue;
|
||
}
|
||
const bool isDragPreview = (!m_presentationPreviewMode && m_draggingEntity && m_dragPreviewActive && i == m_selectedEntity);
|
||
if (!ent.polygonWorld.isEmpty()) {
|
||
// 优先绘制抠出来的实体图像(独立存在)
|
||
if (!ent.image.isNull()) {
|
||
if (isDragPreview) {
|
||
// 预览:先平移,再围绕“基准质心”缩放(避免把 delta 叠加两次导致错位)
|
||
const QPointF cBase = m_dragCentroidBase;
|
||
p.save();
|
||
QTransform tr;
|
||
tr.translate(m_dragDelta.x(), m_dragDelta.y());
|
||
tr.translate(cBase.x(), cBase.y());
|
||
tr.scale(m_dragScaleRatio, m_dragScaleRatio);
|
||
tr.translate(-cBase.x(), -cBase.y());
|
||
p.setTransform(tr, true);
|
||
const QSizeF sz(ent.image.width() * m_dragScaleBase, ent.image.height() * m_dragScaleBase);
|
||
const QRectF target(m_dragImageTopLeftBase, sz);
|
||
p.drawImage(target, ent.image);
|
||
p.restore();
|
||
} else {
|
||
const qreal pop =
|
||
(m_presentationPreviewMode && i == m_presFocusedEntityIndex) ? 1.1 : 1.0;
|
||
const QSizeF sz0(ent.image.width() * ent.visualScale, ent.image.height() * ent.visualScale);
|
||
QRectF target;
|
||
if (pop > 1.001) {
|
||
const QRectF orig(ent.imageTopLeft, sz0);
|
||
const QPointF cen = orig.center();
|
||
const QSizeF sz = orig.size() * pop;
|
||
target = QRectF(QPointF(cen.x() - sz.width() * 0.5, cen.y() - sz.height() * 0.5), sz);
|
||
} else {
|
||
target = QRectF(ent.imageTopLeft, sz0);
|
||
}
|
||
p.drawImage(target, ent.image);
|
||
}
|
||
} else {
|
||
const QPolygonF poly(isDragPreview ? QPolygonF(m_dragPolyBase) : QPolygonF(ent.polygonWorld));
|
||
p.setPen(Qt::NoPen);
|
||
p.setBrush(ent.color);
|
||
if (isDragPreview) {
|
||
const QPointF cBase = m_dragCentroidBase;
|
||
QTransform tr;
|
||
tr.translate(m_dragDelta.x(), m_dragDelta.y());
|
||
tr.translate(cBase.x(), cBase.y());
|
||
tr.scale(m_dragScaleRatio, m_dragScaleRatio);
|
||
tr.translate(-cBase.x(), -cBase.y());
|
||
p.save();
|
||
p.setTransform(tr, true);
|
||
p.drawPolygon(poly);
|
||
p.restore();
|
||
} else {
|
||
p.drawPolygon(poly);
|
||
}
|
||
}
|
||
if (!m_presentationPreviewMode) {
|
||
p.setBrush(Qt::NoBrush);
|
||
p.setPen(QPen(QColor(0, 0, 0, 160), 1.0 / std::max<qreal>(m_scale, 0.001)));
|
||
if (isDragPreview) {
|
||
const QPointF cBase = m_dragCentroidBase;
|
||
QTransform tr;
|
||
tr.translate(m_dragDelta.x(), m_dragDelta.y());
|
||
tr.translate(cBase.x(), cBase.y());
|
||
tr.scale(m_dragScaleRatio, m_dragScaleRatio);
|
||
tr.translate(-cBase.x(), -cBase.y());
|
||
p.save();
|
||
p.setTransform(tr, true);
|
||
p.drawPath(m_dragPathBase);
|
||
p.restore();
|
||
} else {
|
||
p.drawPath(ent.pathWorld);
|
||
}
|
||
}
|
||
} else {
|
||
p.fillRect(ent.rect, ent.color);
|
||
if (!m_presentationPreviewMode) {
|
||
p.setPen(QPen(QColor(0, 0, 0, 120), 1.0 / std::max<qreal>(m_scale, 0.001)));
|
||
p.drawRect(ent.rect);
|
||
}
|
||
}
|
||
if (!m_presentationPreviewMode && i == m_selectedEntity) {
|
||
p.setPen(QPen(QColor(255, 120, 0, 220), 2.0 / std::max<qreal>(m_scale, 0.001)));
|
||
if (!ent.polygonWorld.isEmpty()) {
|
||
if (isDragPreview) {
|
||
const QPointF cBase = m_dragCentroidBase;
|
||
QTransform tr;
|
||
tr.translate(m_dragDelta.x(), m_dragDelta.y());
|
||
tr.translate(cBase.x(), cBase.y());
|
||
tr.scale(m_dragScaleRatio, m_dragScaleRatio);
|
||
tr.translate(-cBase.x(), -cBase.y());
|
||
p.save();
|
||
p.setTransform(tr, true);
|
||
p.drawPath(m_dragPathBase);
|
||
p.restore();
|
||
} else {
|
||
p.drawPath(ent.pathWorld);
|
||
}
|
||
} else {
|
||
p.drawRect(ent.rect.adjusted(-2, -2, 2, 2));
|
||
}
|
||
}
|
||
if (m_presentationPreviewMode && ent.opacity > 0.001) {
|
||
const bool showHover = (i == m_presHoverEntityIndex);
|
||
const bool showFocus = (i == m_presFocusedEntityIndex);
|
||
if (showHover || showFocus) {
|
||
p.setBrush(Qt::NoBrush);
|
||
if (showHover) {
|
||
const qreal pulse = 0.45 + 0.55 * std::sin(static_cast<double>(m_presHoverPhase));
|
||
const qreal lw =
|
||
(2.0 + 2.8 * pulse) / std::max(static_cast<qreal>(m_scale), static_cast<qreal>(0.001));
|
||
p.setPen(QPen(QColor(255, 210, 80, static_cast<int>(65 + 110 * pulse)), lw));
|
||
if (!ent.pathWorld.isEmpty()) {
|
||
p.drawPath(ent.pathWorld);
|
||
} else {
|
||
p.drawRect(ent.rect);
|
||
}
|
||
}
|
||
if (showFocus) {
|
||
const qreal lw = 2.8 / std::max(static_cast<qreal>(m_scale), static_cast<qreal>(0.001));
|
||
p.setPen(QPen(QColor(255, 120, 40, 230), lw));
|
||
if (!ent.pathWorld.isEmpty()) {
|
||
p.drawPath(ent.pathWorld);
|
||
} else {
|
||
p.drawRect(ent.rect);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 工具:对话气泡(world 坐标),按 opacity 淡入淡出
|
||
for (int i = 0; i < m_tools.size(); ++i) {
|
||
const auto& tv = m_tools[i];
|
||
const auto& tool = tv.tool;
|
||
const double opacity = std::clamp(tv.opacity, 0.0, 1.0);
|
||
// tool.visible 仅表示“基础可见性”,动画可见性由 opacity(关键帧+淡入淡出)驱动
|
||
if (opacity <= 0.001) {
|
||
continue;
|
||
}
|
||
if (!tool.id.isEmpty() && m_tempHiddenToolIds.contains(tool.id)) {
|
||
continue;
|
||
}
|
||
if (tool.type != core::Project::Tool::Type::Bubble) {
|
||
continue;
|
||
}
|
||
const BubbleLayoutWorld lay = bubbleLayoutWorld(tool);
|
||
const QPainterPath& path = lay.path;
|
||
const QRectF& body = lay.bodyRect;
|
||
|
||
QColor fill(255, 255, 255, int(220 * opacity));
|
||
QColor border(0, 0, 0, int(120 * opacity));
|
||
p.setBrush(fill);
|
||
p.setPen(QPen(border, 1.2 / std::max<qreal>(m_scale, 0.001)));
|
||
p.drawPath(path);
|
||
|
||
// 文本
|
||
if (!tool.text.trimmed().isEmpty()) {
|
||
p.setPen(QColor(10, 10, 10, int(230 * opacity)));
|
||
QFont f = p.font();
|
||
f.setPixelSize(std::clamp(tool.fontPx, 8, 120));
|
||
p.setFont(f);
|
||
QTextOption opt;
|
||
opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
||
if (tool.align == core::Project::Tool::TextAlign::Left) opt.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||
else if (tool.align == core::Project::Tool::TextAlign::Right) opt.setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||
else opt.setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
|
||
const QRectF textRect = body.adjusted(10, 8, -10, -8);
|
||
p.drawText(textRect, tool.text, opt);
|
||
}
|
||
|
||
// 选中描边
|
||
if (!m_presentationPreviewMode && i == m_selectedTool) {
|
||
p.setBrush(Qt::NoBrush);
|
||
p.setPen(QPen(QColor(80, 160, 255, 220), 2.0 / std::max<qreal>(m_scale, 0.001)));
|
||
p.drawPath(path);
|
||
}
|
||
}
|
||
|
||
// 创建实体:手绘轨迹预览(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));
|
||
p.setPen(pen);
|
||
p.setBrush(Qt::NoBrush);
|
||
p.drawPolyline(QPolygonF(m_strokeWorld));
|
||
// 提示闭合
|
||
p.setPen(QPen(QColor(255, 120, 0, 140), 1.0 / std::max<qreal>(m_scale, 0.001), Qt::DashLine));
|
||
p.drawLine(m_strokeWorld.first(), m_strokeWorld.last());
|
||
}
|
||
|
||
// 待确认实体:多边形预览 + 顶点
|
||
if (!m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) {
|
||
const qreal lw = 2.5 / std::max<qreal>(m_scale, 0.001);
|
||
p.setPen(QPen(QColor(60, 180, 255, 230), lw));
|
||
p.setBrush(QColor(60, 180, 255, 45));
|
||
const QPainterPath path = entity_cutout::pathFromWorldPolygon(m_pendingPolyWorld);
|
||
p.drawPath(path);
|
||
const qreal vr = 5.0 / std::max<qreal>(m_scale, 0.001);
|
||
p.setPen(QPen(QColor(0, 0, 0, 120), lw));
|
||
p.setBrush(QColor(255, 255, 255, 220));
|
||
for (const QPointF& v : m_pendingPolyWorld) {
|
||
p.drawEllipse(v, vr, vr);
|
||
}
|
||
}
|
||
|
||
p.restore();
|
||
|
||
// 坐标轴/刻度:绘制在画布最外层,背景越界时贴边显示
|
||
ensurePixmapLoaded();
|
||
if (!m_presentationPreviewMode && m_worldAxesVisible && !m_bgPixmap.isNull()) {
|
||
const QPointF originView = worldToView(QPointF(0, 0));
|
||
const qreal axisX = std::clamp(originView.x(), 0.0, static_cast<qreal>(width()));
|
||
const qreal axisY = std::clamp(originView.y(), 0.0, static_cast<qreal>(height()));
|
||
|
||
QPen axisPen(QColor(20, 20, 20, 180));
|
||
axisPen.setWidth(2);
|
||
p.setPen(axisPen);
|
||
p.drawLine(QPointF(0, axisY), QPointF(width(), axisY)); // X 轴(水平)
|
||
p.drawLine(QPointF(axisX, 0), QPointF(axisX, height())); // Y 轴(垂直)
|
||
|
||
const int step = 100;
|
||
QPen tickPen(QColor(20, 20, 20, 140));
|
||
tickPen.setWidth(1);
|
||
p.setPen(tickPen);
|
||
if (m_axisLabelsVisible) {
|
||
QFont f = p.font();
|
||
f.setPointSize(std::max(7, f.pointSize() - 1));
|
||
p.setFont(f);
|
||
}
|
||
|
||
// X 轴:用 y=0 的世界线映射到 view-x,并把刻度画在 axisY 上
|
||
for (int x = 0; x <= m_bgPixmap.width(); x += step) {
|
||
const QPointF vx = worldToView(QPointF(x, 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));
|
||
}
|
||
}
|
||
|
||
// Y 轴:用 x=0 的世界线映射到 view-y,并把刻度画在 axisX 上
|
||
for (int y = 0; y <= m_bgPixmap.height(); y += step) {
|
||
const QPointF vy = worldToView(QPointF(0, y));
|
||
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));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Gizmo:选中实体时显示(仿 Blender:约束 X/Y 轴移动)
|
||
if (!m_presentationPreviewMode && m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) {
|
||
const auto& ent = m_entities[m_selectedEntity];
|
||
const bool isDragPreview = (m_draggingEntity && m_dragPreviewActive);
|
||
QPointF originWorld = ent.rect.center();
|
||
if (isDragPreview) {
|
||
originWorld = m_dragCentroidBase + m_dragDelta;
|
||
} else if (!ent.polygonWorld.isEmpty()) {
|
||
originWorld = entity_cutout::polygonCentroid(ent.polygonWorld);
|
||
}
|
||
const QPointF originView = worldToView(originWorld);
|
||
|
||
const qreal len = 56.0;
|
||
QPen xPen(QColor(220, 60, 60, 220));
|
||
xPen.setWidth(2);
|
||
QPen yPen(QColor(60, 180, 90, 220));
|
||
yPen.setWidth(2);
|
||
|
||
// X 轴
|
||
p.setPen(xPen);
|
||
p.drawLine(originView, QPointF(originView.x() + len, originView.y()));
|
||
p.drawRect(QRectF(QPointF(originView.x() + len - 4, originView.y() - 4), QSizeF(8, 8)));
|
||
if (m_gizmoLabelsVisible) {
|
||
p.drawText(QPointF(originView.x() + len + 6, originView.y() + 4), QStringLiteral("X"));
|
||
}
|
||
|
||
// Y 轴
|
||
p.setPen(yPen);
|
||
p.drawLine(originView, QPointF(originView.x(), originView.y() + len));
|
||
p.drawRect(QRectF(QPointF(originView.x() - 4, originView.y() + len - 4), QSizeF(8, 8)));
|
||
if (m_gizmoLabelsVisible) {
|
||
p.drawText(QPointF(originView.x() + 6, originView.y() + len + 14), QStringLiteral("Y"));
|
||
}
|
||
}
|
||
}
|
||
|
||
void EditorCanvas::resizeEvent(QResizeEvent* e) {
|
||
QWidget::resizeEvent(e);
|
||
// 仅触发重绘;pixmap 只缓存原图
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||
if (e->button() != Qt::LeftButton && e->button() != Qt::MiddleButton) {
|
||
QWidget::mousePressEvent(e);
|
||
return;
|
||
}
|
||
|
||
const QPointF wp0 = viewToWorld(e->position());
|
||
emit hoveredWorldPosChanged(wp0);
|
||
int z0 = -1;
|
||
if (!m_depthAbsPath.isEmpty()) {
|
||
if (m_depthDirty) {
|
||
m_depthDirty = false;
|
||
QImage img(m_depthAbsPath);
|
||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||
}
|
||
if (!m_depthImage8.isNull()) {
|
||
const int xi = static_cast<int>(std::floor(wp0.x()));
|
||
const int yi = static_cast<int>(std::floor(wp0.y()));
|
||
if (xi >= 0 && yi >= 0 && xi < m_depthImage8.width() && yi < m_depthImage8.height()) {
|
||
z0 = static_cast<int>(m_depthImage8.constScanLine(yi)[xi]);
|
||
}
|
||
}
|
||
}
|
||
emit hoveredWorldPosDepthChanged(wp0, z0);
|
||
|
||
if (m_presentationPreviewMode) {
|
||
if (e->button() == Qt::LeftButton) {
|
||
const int hit = hitTestEntity(wp0);
|
||
if (hit >= 0) {
|
||
const auto& ent = m_entities[hit];
|
||
const QPointF cWorld =
|
||
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
|
||
const QPointF anchorView = worldToView(cWorld);
|
||
beginPresentationZoomTowardEntity(hit);
|
||
emit presentationEntityIntroRequested(ent.id, anchorView);
|
||
return;
|
||
}
|
||
m_dragging = true;
|
||
m_presBgPanSession = true;
|
||
m_presBgDragDist = 0.0;
|
||
m_lastMouseView = e->position();
|
||
setCursor(Qt::ClosedHandCursor);
|
||
return;
|
||
}
|
||
if (e->button() == Qt::MiddleButton) {
|
||
m_dragging = true;
|
||
m_presBgPanSession = false;
|
||
m_lastMouseView = e->position();
|
||
setCursor(Qt::ClosedHandCursor);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (m_tool == Tool::CreateEntity && e->button() == Qt::LeftButton) {
|
||
// 若已有待确认多边形:进入微调(顶点/整体拖拽),或点击空白直接确认
|
||
if (!m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) {
|
||
m_dragging = true;
|
||
m_lastMouseView = e->position();
|
||
const QPointF w = viewToWorld(e->position());
|
||
m_pendingLastMouseWorld = w;
|
||
int vi = -1;
|
||
if (isPointNearPendingVertex(w, &vi)) {
|
||
m_pendingDragging = true;
|
||
m_pendingDragVertex = vi;
|
||
m_pendingDragWhole = false;
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (pendingPolygonContains(w)) {
|
||
m_pendingDragging = true;
|
||
m_pendingDragWhole = true;
|
||
m_pendingDragVertex = -1;
|
||
e->accept();
|
||
return;
|
||
}
|
||
// 点击空白:确认
|
||
emit requestFinalizePendingEntity(m_pendingPolyWorld);
|
||
e->accept();
|
||
return;
|
||
}
|
||
|
||
m_dragging = true;
|
||
m_drawingEntity = true;
|
||
m_draggingEntity = false;
|
||
m_selectedEntity = -1;
|
||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||
m_lastMouseView = e->position();
|
||
m_strokeWorld.clear();
|
||
m_strokeWorld.push_back(viewToWorld(e->position()));
|
||
update();
|
||
return;
|
||
}
|
||
|
||
m_dragging = true;
|
||
m_lastMouseView = e->position();
|
||
const QPointF worldPos = viewToWorld(e->position());
|
||
|
||
if (e->button() == Qt::MiddleButton || m_tool == Tool::Move) {
|
||
setCursor(Qt::ClosedHandCursor);
|
||
}
|
||
|
||
if (m_tool == Tool::Move && e->button() == Qt::LeftButton) {
|
||
// 工具(气泡)优先命中:绘制在实体之后,交互也应优先
|
||
for (qsizetype i = m_tools.size(); i > 0; --i) {
|
||
const qsizetype idx = i - 1;
|
||
const auto& tv = m_tools[idx];
|
||
if (tv.opacity <= 0.001) continue;
|
||
if (!tv.tool.id.isEmpty() && m_tempHiddenToolIds.contains(tv.tool.id)) continue;
|
||
if (tv.tool.type != core::Project::Tool::Type::Bubble) continue;
|
||
const QPainterPath path = bubblePathWorld(tv.tool);
|
||
if (path.contains(worldPos)) {
|
||
m_selectedTool = static_cast<int>(idx);
|
||
m_selectedEntity = -1;
|
||
m_draggingTool = true;
|
||
m_dragMode = DragMode::Free;
|
||
m_toolDragOffsetOriginWorld = worldPos - m_tools[m_selectedTool].tool.originWorld;
|
||
m_toolDragStartOriginWorld = m_tools[m_selectedTool].tool.originWorld;
|
||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||
emit selectedToolChanged(true, m_tools[m_selectedTool].tool.id, m_tools[m_selectedTool].tool.originWorld);
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 优先:若已选中实体,且点在 gizmo 手柄上,则开启轴约束拖动
|
||
if (m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) {
|
||
const auto& ent = m_entities[m_selectedEntity];
|
||
const bool isDragPreview = (m_draggingEntity && m_dragPreviewActive);
|
||
QPointF originWorld = ent.rect.center();
|
||
if (isDragPreview) {
|
||
originWorld = m_dragCentroidBase + m_dragDelta;
|
||
} else if (!ent.polygonWorld.isEmpty()) {
|
||
originWorld = entity_cutout::polygonCentroid(ent.polygonWorld);
|
||
}
|
||
const QPointF originView = worldToView(originWorld);
|
||
const GizmoHit gh = hitTestGizmo(e->position(), originView);
|
||
if (gh.mode == DragMode::AxisX || gh.mode == DragMode::AxisY) {
|
||
m_dragging = true;
|
||
m_draggingEntity = true;
|
||
m_dragMode = gh.mode;
|
||
m_lastMouseView = e->position();
|
||
m_dragStartMouseWorld = viewToWorld(e->position());
|
||
|
||
// 为了统一复用 move 逻辑:初始化 rect 与基准点
|
||
const QRectF r = m_entities[m_selectedEntity].rect.isNull() && !m_entities[m_selectedEntity].polygonWorld.isEmpty()
|
||
? entity_cutout::pathFromWorldPolygon(m_entities[m_selectedEntity].polygonWorld).boundingRect()
|
||
: m_entities[m_selectedEntity].rect;
|
||
m_entities[m_selectedEntity].rect = r;
|
||
m_entityDragOffsetOriginWorld = viewToWorld(e->position()) - m_entities[m_selectedEntity].animatedOriginWorld;
|
||
m_entityDragStartAnimatedOrigin = m_entities[m_selectedEntity].animatedOriginWorld;
|
||
// drag preview baseline
|
||
m_dragPreviewActive = true;
|
||
m_dragDelta = QPointF(0, 0);
|
||
m_dragOriginBase = m_entities[m_selectedEntity].animatedOriginWorld;
|
||
m_dragRectBase = m_entities[m_selectedEntity].rect;
|
||
m_dragImageTopLeftBase = m_entities[m_selectedEntity].imageTopLeft;
|
||
m_dragScaleBase = std::max(1e-6, m_entities[m_selectedEntity].visualScale);
|
||
m_dragScaleRatio = 1.0;
|
||
m_dragPolyBase = m_entities[m_selectedEntity].polygonWorld;
|
||
m_dragPathBase = m_entities[m_selectedEntity].pathWorld;
|
||
m_dragCentroidBase =
|
||
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase);
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 若已选中实体:点击命中该实体本体时,优先拖动“已选中实体”。
|
||
// 这对父子层级很重要:子实体可能被父实体遮挡,但用户在项目树中选中子实体后仍应可拖动它。
|
||
if (m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) {
|
||
const auto& ent = m_entities[m_selectedEntity];
|
||
bool hitSelected = false;
|
||
if (!ent.pathWorld.isEmpty()) {
|
||
hitSelected = ent.pathWorld.contains(worldPos);
|
||
} else if (!ent.polygonWorld.isEmpty()) {
|
||
hitSelected = entity_cutout::pathFromWorldPolygon(ent.polygonWorld).contains(worldPos);
|
||
} else {
|
||
hitSelected = ent.rect.contains(worldPos);
|
||
}
|
||
if (hitSelected) {
|
||
m_draggingEntity = true;
|
||
m_dragMode = DragMode::Free;
|
||
emit entityDragActiveChanged(true);
|
||
const QRectF r = ent.rect.isNull() && !ent.polygonWorld.isEmpty()
|
||
? entity_cutout::pathFromWorldPolygon(ent.polygonWorld).boundingRect()
|
||
: ent.rect;
|
||
m_entities[m_selectedEntity].rect = r;
|
||
m_entityDragOffsetOriginWorld = worldPos - m_entities[m_selectedEntity].animatedOriginWorld;
|
||
m_entityDragStartAnimatedOrigin = m_entities[m_selectedEntity].animatedOriginWorld;
|
||
// drag preview baseline
|
||
m_dragPreviewActive = true;
|
||
m_dragDelta = QPointF(0, 0);
|
||
m_dragOriginBase = m_entities[m_selectedEntity].animatedOriginWorld;
|
||
m_dragRectBase = m_entities[m_selectedEntity].rect;
|
||
m_dragImageTopLeftBase = m_entities[m_selectedEntity].imageTopLeft;
|
||
m_dragScaleBase = std::max(1e-6, m_entities[m_selectedEntity].visualScale);
|
||
m_dragScaleRatio = 1.0;
|
||
m_dragPolyBase = m_entities[m_selectedEntity].polygonWorld;
|
||
m_dragPathBase = m_entities[m_selectedEntity].pathWorld;
|
||
m_dragCentroidBase =
|
||
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase);
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
|
||
const int hit = hitTestEntity(worldPos);
|
||
if (hit >= 0) {
|
||
m_selectedEntity = hit;
|
||
m_selectedTool = -1;
|
||
m_draggingTool = false;
|
||
m_draggingEntity = true;
|
||
m_dragMode = DragMode::Free;
|
||
emit entityDragActiveChanged(true);
|
||
const QRectF r = m_entities[hit].rect.isNull() && !m_entities[hit].polygonWorld.isEmpty()
|
||
? entity_cutout::pathFromWorldPolygon(m_entities[hit].polygonWorld).boundingRect()
|
||
: m_entities[hit].rect;
|
||
m_entities[hit].rect = r;
|
||
m_entityDragOffsetOriginWorld = worldPos - m_entities[hit].animatedOriginWorld;
|
||
m_entityDragStartAnimatedOrigin = m_entities[hit].animatedOriginWorld;
|
||
// drag preview baseline
|
||
m_dragPreviewActive = true;
|
||
m_dragDelta = QPointF(0, 0);
|
||
m_dragOriginBase = m_entities[hit].animatedOriginWorld;
|
||
m_dragRectBase = m_entities[hit].rect;
|
||
m_dragImageTopLeftBase = m_entities[hit].imageTopLeft;
|
||
m_dragScaleBase = std::max(1e-6, m_entities[hit].visualScale);
|
||
m_dragScaleRatio = 1.0;
|
||
m_dragPolyBase = m_entities[hit].polygonWorld;
|
||
m_dragPathBase = m_entities[hit].pathWorld;
|
||
m_dragCentroidBase =
|
||
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase);
|
||
const QPointF origin = !m_entities[hit].polygonWorld.isEmpty() ? entity_cutout::polygonCentroid(m_entities[hit].polygonWorld)
|
||
: m_entities[hit].rect.center();
|
||
emit selectedEntityChanged(true, m_entities[hit].id, m_entities[hit].depth, origin);
|
||
emit selectedToolChanged(false, QString(), QPointF());
|
||
update();
|
||
return;
|
||
}
|
||
m_selectedEntity = -1;
|
||
m_draggingEntity = false;
|
||
m_selectedTool = -1;
|
||
m_draggingTool = false;
|
||
m_dragMode = DragMode::None;
|
||
emit selectedEntityChanged(false, QString(), 0, QPointF());
|
||
emit selectedToolChanged(false, QString(), QPointF());
|
||
update();
|
||
}
|
||
}
|
||
|
||
void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||
const QPointF wp = viewToWorld(e->position());
|
||
emit hoveredWorldPosChanged(wp);
|
||
int z = -1;
|
||
if (!m_depthAbsPath.isEmpty()) {
|
||
if (m_depthDirty) {
|
||
m_depthDirty = false;
|
||
QImage img(m_depthAbsPath);
|
||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||
}
|
||
if (!m_depthImage8.isNull()) {
|
||
const int xi = static_cast<int>(std::floor(wp.x()));
|
||
const int yi = static_cast<int>(std::floor(wp.y()));
|
||
if (xi >= 0 && yi >= 0 && xi < m_depthImage8.width() && yi < m_depthImage8.height()) {
|
||
z = static_cast<int>(m_depthImage8.constScanLine(yi)[xi]);
|
||
}
|
||
}
|
||
}
|
||
emit hoveredWorldPosDepthChanged(wp, z);
|
||
|
||
if (m_presentationPreviewMode) {
|
||
const int h = hitTestEntity(wp);
|
||
if (h != m_presHoverEntityIndex) {
|
||
m_presHoverEntityIndex = h;
|
||
updateCursor();
|
||
update();
|
||
}
|
||
if (h >= 0) {
|
||
if (m_presHoverTimer && !m_presHoverTimer->isActive()) {
|
||
m_presHoverTimer->start();
|
||
}
|
||
} else if (m_presHoverTimer) {
|
||
m_presHoverTimer->stop();
|
||
m_presHoverPhase = 0.0;
|
||
}
|
||
}
|
||
|
||
if (!m_dragging) {
|
||
QWidget::mouseMoveEvent(e);
|
||
return;
|
||
}
|
||
|
||
const QPointF cur = e->position();
|
||
const QPointF deltaView = cur - m_lastMouseView;
|
||
m_lastMouseView = cur;
|
||
|
||
if (m_pendingDragging && !m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) {
|
||
const QPointF curWorld = viewToWorld(e->position());
|
||
const QPointF delta = curWorld - m_pendingLastMouseWorld;
|
||
m_pendingLastMouseWorld = curWorld;
|
||
if (m_pendingDragWhole) {
|
||
for (auto& pt : m_pendingPolyWorld) {
|
||
pt += delta;
|
||
}
|
||
} else if (m_pendingDragVertex >= 0 && m_pendingDragVertex < m_pendingPolyWorld.size()) {
|
||
m_pendingPolyWorld[m_pendingDragVertex] += delta;
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
|
||
if (m_tool == Tool::CreateEntity && m_drawingEntity) {
|
||
const QPointF w = viewToWorld(cur);
|
||
if (m_strokeWorld.isEmpty()) {
|
||
m_strokeWorld.push_back(w);
|
||
update();
|
||
return;
|
||
}
|
||
const QPointF last = m_strokeWorld.last();
|
||
const qreal dx = w.x() - last.x();
|
||
const qreal dy = w.y() - last.y();
|
||
// 简单抽样:world 距离至少 1.5 像素才记录,避免点过密
|
||
if ((dx * dx + dy * dy) >= (1.5 * 1.5)) {
|
||
m_strokeWorld.push_back(w);
|
||
update();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (m_draggingEntity && m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) {
|
||
const QPointF worldPos = viewToWorld(cur);
|
||
auto& ent = m_entities[m_selectedEntity];
|
||
const QPointF newOrigin = worldPos - m_entityDragOffsetOriginWorld;
|
||
QPointF delta = newOrigin - ent.animatedOriginWorld;
|
||
|
||
// 轴约束:只允许沿 X 或 Y 平移(world 坐标)
|
||
if (m_dragMode == DragMode::AxisX) {
|
||
delta.setY(0.0);
|
||
} else if (m_dragMode == DragMode::AxisY) {
|
||
delta.setX(0.0);
|
||
}
|
||
|
||
// 约束到背景范围内(若有背景),按“预览变换后”的包围盒约束
|
||
const QRectF bg = worldRectOfBackground();
|
||
if (!bg.isNull()) {
|
||
const QRectF moved = transformedRectByScaleAndTranslate(
|
||
m_dragPreviewActive ? m_dragRectBase : ent.rect,
|
||
m_dragPreviewActive ? m_dragCentroidBase : ent.rect.center(),
|
||
m_dragPreviewActive ? m_dragScaleRatio : 1.0,
|
||
(m_dragPreviewActive ? m_dragDelta : QPointF(0, 0)) + delta);
|
||
QPointF d = delta;
|
||
if (moved.left() < bg.left()) d.setX(d.x() + (bg.left() - moved.left()));
|
||
if (moved.top() < bg.top()) d.setY(d.y() + (bg.top() - moved.top()));
|
||
if (moved.right() > bg.right()) d.setX(d.x() - (moved.right() - bg.right()));
|
||
if (moved.bottom() > bg.bottom()) d.setY(d.y() - (moved.bottom() - bg.bottom()));
|
||
delta = d;
|
||
}
|
||
|
||
// 轻量:仅更新增量参数与原点,不逐点修改 polygonWorld
|
||
if (m_dragPreviewActive) {
|
||
m_dragDelta += delta;
|
||
}
|
||
ent.animatedOriginWorld += delta;
|
||
|
||
// 拖动中实时按深度图更新 z 与距离缩放(近大远小)
|
||
// 但不必每个鼠标事件都重算:位置平移满帧跟手;深度/距离缩放重算较重,节流到 ~30Hz。
|
||
const qint64 nowMs = m_previewEmitTimer.elapsed();
|
||
if (nowMs - m_lastDepthScaleRecalcMs >= 33) {
|
||
m_lastDepthScaleRecalcMs = nowMs;
|
||
|
||
if (!m_depthAbsPath.isEmpty() && m_depthDirty) {
|
||
m_depthDirty = false;
|
||
QImage img(m_depthAbsPath);
|
||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||
}
|
||
if (!m_depthImage8.isNull()) {
|
||
// 深度采样点:使用“预览质心的平移后位置”
|
||
const QPointF c = (m_dragPreviewActive ? (m_dragCentroidBase + m_dragDelta) : ent.animatedOriginWorld);
|
||
const int depthZ = sampleDepthAtPoint(m_depthImage8, c);
|
||
ent.depth = depthZ;
|
||
|
||
const double ds01 = depthToScale01(depthZ);
|
||
ent.animatedDepthScale01 = ds01;
|
||
const double newScale =
|
||
(ent.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ds01, ent.distanceScaleCalibMult)) * ent.userScale;
|
||
ent.visualScale = newScale;
|
||
if (m_dragPreviewActive) {
|
||
m_dragScaleRatio = std::clamp(newScale / std::max(1e-6, m_dragScaleBase), 0.02, 50.0);
|
||
}
|
||
}
|
||
}
|
||
// 低频预览信号:用于属性面板同步,不做树联动等重操作
|
||
// 属性面板/状态预览:按 30Hz 刷新即可,拖动视觉仍满帧跟手
|
||
if (nowMs - m_lastPreviewEmitMs >= 33) {
|
||
m_lastPreviewEmitMs = nowMs;
|
||
emit selectedEntityPreviewChanged(ent.id, ent.depth, ent.animatedOriginWorld);
|
||
}
|
||
update();
|
||
return;
|
||
}
|
||
|
||
if (m_draggingTool && m_selectedTool >= 0 && m_selectedTool < m_tools.size()) {
|
||
const QPointF worldPos = viewToWorld(cur);
|
||
const QPointF newOrigin = worldPos - m_toolDragOffsetOriginWorld;
|
||
QPointF delta = newOrigin - m_tools[m_selectedTool].tool.originWorld;
|
||
m_tools[m_selectedTool].tool.originWorld += delta;
|
||
emit selectedToolChanged(true, m_tools[m_selectedTool].tool.id, m_tools[m_selectedTool].tool.originWorld);
|
||
update();
|
||
return;
|
||
}
|
||
|
||
// 平移画布
|
||
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());
|
||
}
|
||
m_pan += deltaView;
|
||
update();
|
||
return;
|
||
}
|
||
}
|
||
|
||
void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||
if (e->button() == Qt::LeftButton || e->button() == Qt::MiddleButton) {
|
||
if (m_presentationPreviewMode && e->button() == Qt::LeftButton) {
|
||
if (m_presBgPanSession && m_presBgDragDist < 8.0) {
|
||
clearPresentationEntityFocus();
|
||
}
|
||
m_presBgPanSession = false;
|
||
m_presBgDragDist = 0.0;
|
||
}
|
||
if (m_tool == Tool::CreateEntity && e->button() == Qt::LeftButton && m_drawingEntity) {
|
||
m_dragging = false;
|
||
m_drawingEntity = false;
|
||
updateCursor();
|
||
|
||
if (m_entityCreateSegmentMode == EntityCreateSegmentMode::Manual) {
|
||
if (m_strokeWorld.size() >= kMinStrokePointsManual) {
|
||
setPendingEntityPolygonWorld(m_strokeWorld);
|
||
}
|
||
} else if (m_entityCreateSegmentMode == EntityCreateSegmentMode::Snap) {
|
||
if (m_strokeWorld.size() >= kMinStrokePointsManual) {
|
||
ensurePixmapLoaded();
|
||
const QVector<QPointF> snapped = snapStrokeToEdges(m_strokeWorld, m_bgImage, 6);
|
||
setPendingEntityPolygonWorld(snapped);
|
||
}
|
||
} else if (m_strokeWorld.size() >= kMinStrokePointsSam) {
|
||
ensurePixmapLoaded();
|
||
QByteArray cropPng;
|
||
QByteArray ovPng;
|
||
QPointF cropOrigin;
|
||
QJsonArray pts;
|
||
QJsonArray labs;
|
||
QJsonArray box;
|
||
if (buildSamSegmentPayloadFromStroke(m_strokeWorld, m_bgImage, cropPng, ovPng, cropOrigin, pts, labs, box)) {
|
||
emit requestSamSegment(cropPng, ovPng, cropOrigin, pts, labs, box);
|
||
}
|
||
}
|
||
m_strokeWorld.clear();
|
||
update();
|
||
return;
|
||
}
|
||
|
||
if (m_draggingEntity && m_selectedEntity >= 0 && m_selectedEntity < m_entities.size() && e->button() == Qt::LeftButton) {
|
||
const auto& ent = m_entities[m_selectedEntity];
|
||
|
||
// 关键:提交“平移量”必须与缩放无关。
|
||
// 拖动过程中可能实时按深度重算 visualScale(导致 rect/topLeft 随缩放变化),
|
||
// 因此不能用 rect.topLeft 差值作为移动 delta,否则松手会错位。
|
||
const QPointF delta = ent.animatedOriginWorld - m_entityDragStartAnimatedOrigin;
|
||
|
||
bool sentMove = false;
|
||
if (!ent.id.isEmpty() && (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y()))) {
|
||
emit requestMoveEntity(ent.id, delta);
|
||
sentMove = true;
|
||
}
|
||
if (!sentMove && !ent.id.isEmpty()) {
|
||
const QPointF origin =
|
||
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
|
||
emit selectedEntityChanged(true, ent.id, ent.depth, origin);
|
||
}
|
||
}
|
||
|
||
if (m_draggingTool && m_selectedTool >= 0 && m_selectedTool < m_tools.size() && e->button() == Qt::LeftButton) {
|
||
const auto& tv = m_tools[m_selectedTool];
|
||
const QPointF delta = tv.tool.originWorld - m_toolDragStartOriginWorld;
|
||
if (!tv.tool.id.isEmpty() && (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y()))) {
|
||
emit requestMoveTool(tv.tool.id, delta);
|
||
} else if (!tv.tool.id.isEmpty()) {
|
||
emit selectedToolChanged(true, tv.tool.id, tv.tool.originWorld);
|
||
}
|
||
}
|
||
|
||
m_dragging = false;
|
||
if (m_pendingDragging && e->button() == Qt::LeftButton) {
|
||
m_pendingDragging = false;
|
||
m_pendingDragWhole = false;
|
||
m_pendingDragVertex = -1;
|
||
}
|
||
if (m_draggingEntity) {
|
||
emit entityDragActiveChanged(false);
|
||
}
|
||
m_draggingEntity = false;
|
||
m_draggingTool = false;
|
||
m_dragPreviewActive = false;
|
||
m_dragMode = DragMode::None;
|
||
updateCursor();
|
||
}
|
||
QWidget::mouseReleaseEvent(e);
|
||
}
|
||
|
||
void EditorCanvas::wheelEvent(QWheelEvent* e) {
|
||
if (m_tool != Tool::Zoom && !(e->modifiers() & Qt::ControlModifier)) {
|
||
// 默认仍允许滚轮缩放:不强制用户切换工具
|
||
//(若你希望仅在 Zoom 工具下才缩放,可在此 return)
|
||
}
|
||
|
||
const QPointF cursorView = e->position();
|
||
const QPointF beforeWorld = viewToWorld(cursorView);
|
||
|
||
// 约定:滚轮一步约 15°,这里做平滑指数缩放
|
||
const qreal steps = e->angleDelta().y() / 120.0;
|
||
const qreal factor = std::pow(1.15, steps);
|
||
|
||
const qreal newScale = std::clamp(m_scale * factor, 0.05, 50.0);
|
||
if (qFuzzyCompare(newScale, m_scale)) {
|
||
return;
|
||
}
|
||
m_scale = newScale;
|
||
|
||
// 让“光标指向的 world 点”缩放后仍落在光标处
|
||
const QPointF afterView = worldToView(beforeWorld);
|
||
m_pan += (cursorView - afterView);
|
||
update();
|
||
e->accept();
|
||
}
|
||
|
||
void EditorCanvas::keyPressEvent(QKeyEvent* e) {
|
||
if (m_presentationPreviewMode && e->key() == Qt::Key_Escape) {
|
||
clearPresentationEntityFocus();
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (!m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) {
|
||
if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
|
||
emit requestFinalizePendingEntity(m_pendingPolyWorld);
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (e->key() == Qt::Key_Escape) {
|
||
clearPendingEntityPolygon();
|
||
e->accept();
|
||
return;
|
||
}
|
||
}
|
||
QWidget::keyPressEvent(e);
|
||
}
|
||
|