3401 lines
133 KiB
C++
3401 lines
133 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 <QLineF>
|
||
#include <QTimer>
|
||
#include <QtConcurrent/QtConcurrent>
|
||
|
||
#include "core/image/ImageDecodeConfig.h"
|
||
#include "core/image/ImageFileLoader.h"
|
||
#include "core/large_image/VipsBackend.h"
|
||
#include "core/library/EntityJson.h"
|
||
#include "core/library/ToolJson.h"
|
||
|
||
namespace {
|
||
|
||
// 摄像机「输出」参考分辨率:视口框与预览缩放均按此换算,避免随窗口大小改变镜头覆盖的世界范围
|
||
constexpr double kCameraRefViewportW = 1600.0;
|
||
constexpr double kCameraRefViewportH = 900.0;
|
||
|
||
/// 画布自由缩放(世界→屏幕)上下限;与摄像机 viewScale 经 applyCameraViewport 换算后的 eff 对齐
|
||
constexpr qreal kViewScaleMin = 0.001;
|
||
constexpr qreal kViewScaleMax = 400.0;
|
||
|
||
constexpr int kSamCropMargin = 32;
|
||
constexpr int kMinStrokePointsSam = 4;
|
||
constexpr int kMinStrokePointsManual = 8;
|
||
|
||
[[nodiscard]] QPointF shapeCentroidWorld(const QVector<QPointF>& polyWorld, const QRectF& rect) {
|
||
if (!polyWorld.isEmpty()) {
|
||
return entity_cutout::polygonCentroid(polyWorld);
|
||
}
|
||
return rect.center();
|
||
}
|
||
constexpr int kMaxSamPointPrompts = 32;
|
||
constexpr char kMimeHotspotAnimationJson[] = "application/x-hfut-hotspot-animation+json";
|
||
|
||
static QImage readImageTolerant(const QString& absPath) {
|
||
return core::image_file::loadImageLimited(absPath, core::image_decode::kWorkspaceMaxPixels);
|
||
}
|
||
|
||
/// 预览模式下:实体在屏上 footprint 较小时将贴图缩到约 2× 屏上最大边,降低过采样开销
|
||
static QImage entityImageForPresentationDrawLoRes(const QImage& src, qreal screenWpx, qreal screenHpx) {
|
||
if (src.isNull()) {
|
||
return src;
|
||
}
|
||
const int iw = src.width();
|
||
const int ih = src.height();
|
||
const qreal sm = std::max(screenWpx, screenHpx);
|
||
if (!(sm > 1.0)) {
|
||
return src;
|
||
}
|
||
const int cap = std::clamp(static_cast<int>(std::ceil(sm * 2.0)), 32, 4096);
|
||
if (std::max(iw, ih) <= cap) {
|
||
return src;
|
||
}
|
||
return src.scaled(QSize(cap, cap), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||
}
|
||
|
||
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> snapStrokeToEdgesWorld(const QVector<QPointF>& strokeWorld, const QImage& bgGray,
|
||
const QPointF& originWorld, const QSize& extentWorld,
|
||
double searchRadiusWorld) {
|
||
if (strokeWorld.size() < 3 || bgGray.isNull() || extentWorld.width() < 1 || extentWorld.height() < 1) {
|
||
return strokeWorld;
|
||
}
|
||
const int w = bgGray.width();
|
||
const int h = bgGray.height();
|
||
const double sx = static_cast<double>(w) / static_cast<double>(extentWorld.width());
|
||
const double sy = static_cast<double>(h) / static_cast<double>(extentWorld.height());
|
||
const int rPix = std::max(1, static_cast<int>(std::ceil(searchRadiusWorld * std::min(sx, sy))));
|
||
const double invSx = 1.0 / sx;
|
||
const double invSy = 1.0 / sy;
|
||
|
||
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*>(bgGray.constScanLine(y))[x]);
|
||
};
|
||
auto gradMag = [&](int x, int y) -> int {
|
||
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());
|
||
for (const QPointF& p : strokeWorld) {
|
||
const double lx = (p.x() - originWorld.x()) * sx;
|
||
const double ly = (p.y() - originWorld.y()) * sy;
|
||
const int cx = static_cast<int>(std::round(lx));
|
||
const int cy = static_cast<int>(std::round(ly));
|
||
int bestX = cx;
|
||
int bestY = cy;
|
||
int bestG = -1;
|
||
for (int dy = -rPix; dy <= rPix; ++dy) {
|
||
for (int dx = -rPix; dx <= rPix; ++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(originWorld.x() + (static_cast<double>(bestX) + 0.5) * invSx,
|
||
originWorld.y() + (static_cast<double>(bestY) + 0.5) * invSy));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
bool buildSamSegmentPayloadFromStrokeMapped(const QVector<QPointF>& strokeWorld, const QImage& cropAny,
|
||
const QRect& cropWorldRect, QByteArray& outCropPng,
|
||
QByteArray& outOverlayPng, QPointF& outCropTopLeftWorld,
|
||
QJsonArray& outPointCoords, QJsonArray& outPointLabels,
|
||
QJsonArray& outBoxXyxy) {
|
||
if (strokeWorld.size() < kMinStrokePointsSam || cropAny.isNull() || cropWorldRect.width() < 1 ||
|
||
cropWorldRect.height() < 1) {
|
||
return false;
|
||
}
|
||
outCropPng.clear();
|
||
outOverlayPng.clear();
|
||
outPointCoords = QJsonArray{};
|
||
outPointLabels = QJsonArray{};
|
||
outBoxXyxy = QJsonArray{};
|
||
|
||
const QImage cropRgb = cropAny.convertToFormat(QImage::Format_RGB888);
|
||
if (cropRgb.isNull()) {
|
||
return false;
|
||
}
|
||
|
||
const QPointF origin = cropWorldRect.topLeft();
|
||
outCropTopLeftWorld = origin;
|
||
const int cw = cropRgb.width();
|
||
const int ch = cropRgb.height();
|
||
const double sx = static_cast<double>(cw) / static_cast<double>(cropWorldRect.width());
|
||
const double sy = static_cast<double>(ch) / static_cast<double>(cropWorldRect.height());
|
||
|
||
QBuffer bufCrop(&outCropPng);
|
||
if (!bufCrop.open(QIODevice::WriteOnly) || !cropRgb.save(&bufCrop, "PNG")) {
|
||
outCropPng.clear();
|
||
return false;
|
||
}
|
||
bufCrop.close();
|
||
|
||
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(QPointF((w.x() - origin.x()) * sx, (w.y() - origin.y()) * sy));
|
||
}
|
||
pop.drawPolyline(local);
|
||
}
|
||
QBuffer bufOv(&outOverlayPng);
|
||
if (!bufOv.open(QIODevice::WriteOnly) || !overlay.save(&bufOv, "PNG")) {
|
||
outOverlayPng.clear();
|
||
return false;
|
||
}
|
||
bufOv.close();
|
||
|
||
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.x() - origin.x()) * sx, (centerWorld.y() - origin.y()) * sy);
|
||
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);
|
||
|
||
const int n = static_cast<int>(strokeWorld.size());
|
||
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].x() - origin.x()) * sx, (strokeWorld[i].y() - origin.y()) * sy);
|
||
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();
|
||
double x1 = clampD((tight.left() - origin.x()) * sx, 0.0, static_cast<double>(cw - 1));
|
||
double y1 = clampD((tight.top() - origin.y()) * sy, 0.0, static_cast<double>(ch - 1));
|
||
double x2 = clampD((tight.right() - origin.x()) * sx, 0.0, static_cast<double>(cw - 1));
|
||
double y2 = clampD((tight.bottom() - origin.y()) * sy, 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 (!m_presentationPreviewMode && m_presentationHotspotEditorActive &&
|
||
e->mimeData()->hasFormat(QString::fromUtf8(kMimeHotspotAnimationJson))) {
|
||
e->acceptProposedAction();
|
||
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 (!m_presentationPreviewMode && m_presentationHotspotEditorActive &&
|
||
e->mimeData()->hasFormat(QString::fromUtf8(kMimeHotspotAnimationJson))) {
|
||
e->acceptProposedAction();
|
||
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 (!m_presentationPreviewMode && m_presentationHotspotEditorActive &&
|
||
e->mimeData()->hasFormat(QString::fromUtf8(kMimeHotspotAnimationJson))) {
|
||
const QByteArray bytes = e->mimeData()->data(QString::fromUtf8(kMimeHotspotAnimationJson));
|
||
const auto doc = QJsonDocument::fromJson(bytes);
|
||
if (!doc.isObject()) {
|
||
e->ignore();
|
||
return;
|
||
}
|
||
const QString animationId = doc.object().value(QStringLiteral("animationId")).toString();
|
||
if (animationId.isEmpty()) {
|
||
e->ignore();
|
||
return;
|
||
}
|
||
emit requestAddPresentationHotspotForAnimation(viewToWorld(e->position()), animationId);
|
||
e->acceptProposedAction();
|
||
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.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::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), kViewScaleMin, kViewScaleMax);
|
||
m_pan = QPointF(width() / 2.0, height() / 2.0) - QPointF(centerWorld.x() * m_scale, centerWorld.y() * m_scale);
|
||
invalidateViewportLod();
|
||
update();
|
||
}
|
||
|
||
QPointF EditorCanvas::viewCenterWorld() const {
|
||
return viewToWorld(QPointF(width() / 2.0, height() / 2.0));
|
||
}
|
||
|
||
void EditorCanvas::setPresentationHotspots(const QVector<core::Project::PresentationHotspot>& hotspots) {
|
||
m_presentationHotspots = hotspots;
|
||
bool found = false;
|
||
for (const auto& h : m_presentationHotspots) {
|
||
if (h.id == m_selectedPresentationHotspotId) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!found) {
|
||
m_selectedPresentationHotspotId.clear();
|
||
}
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setPresentationHotspotEditorActive(bool on) {
|
||
if (m_presentationHotspotEditorActive == on) {
|
||
return;
|
||
}
|
||
m_presentationHotspotEditorActive = on;
|
||
if (!on) {
|
||
m_draggingHotspotIndex = -1;
|
||
m_dragging = false;
|
||
}
|
||
updateCursor();
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setPresentationHotspotPlaybackTargetEnabled(bool on) {
|
||
if (m_presentationHotspotPlaybackTargetEnabled == on) {
|
||
return;
|
||
}
|
||
m_presentationHotspotPlaybackTargetEnabled = on;
|
||
if (!on) {
|
||
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;
|
||
emit presentationInteractionDismissed();
|
||
}
|
||
updateCursor();
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setSelectedPresentationHotspotId(const QString& id) {
|
||
m_selectedPresentationHotspotId = id;
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::clearPresentationHotspotSelection() {
|
||
m_selectedPresentationHotspotId.clear();
|
||
m_draggingHotspotIndex = -1;
|
||
update();
|
||
}
|
||
|
||
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;
|
||
cancelBlackholeCopyResolve();
|
||
m_draggingHotspotIndex = -1;
|
||
if (on) {
|
||
m_tool = Tool::Move;
|
||
m_selectedEntity = -1;
|
||
m_selectedBlackholeEntityId.clear();
|
||
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_blackholeOverlayImageCache.clear();
|
||
m_projectDirAbs = projectDirAbs;
|
||
|
||
m_entities.clear();
|
||
m_entities.reserve(entities.size());
|
||
|
||
// 需要用深度图来自动计算 z 与缩放(逐帧)
|
||
if (!m_depthAbsPath.isEmpty()) {
|
||
if (m_depthDirty) {
|
||
m_depthDirty = false;
|
||
m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
|
||
}
|
||
}
|
||
|
||
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.blackholeVisible = e.blackholeVisible;
|
||
v.blackholeOverlayPath = e.blackholeOverlayPath;
|
||
v.blackholeOverlayRect = e.blackholeOverlayRect;
|
||
v.distanceScaleCalibMult = e.distanceScaleCalibMult;
|
||
v.ignoreDistanceScale = e.ignoreDistanceScale;
|
||
v.priority = e.priority;
|
||
|
||
const double userScaleAnimated =
|
||
core::sampleUserScale(e.userScaleKeys, m_currentFrame, e.userScale, core::KeyInterpolation::Linear);
|
||
v.userScale = std::max(1e-6, userScaleAnimated);
|
||
|
||
// 逐帧自动算 z:采样点必须对“重锚枢轴(相对位置)”不敏感,否则会导致深度→缩放变化,
|
||
// 表现为“中心没改但看起来位置/形心漂移”。
|
||
//
|
||
// 做法:先用工程给出的 depth 计算一次 scale0,构建 polygonWorld 得到稳定的形心,再用该形心采样深度,
|
||
// 然后用采样到的 z 计算最终 scale。
|
||
const double ds01Base = depthToScale01(e.depth);
|
||
const double distScale0 =
|
||
e.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ds01Base, e.distanceScaleCalibMult);
|
||
const double scale0 = distScale0 * v.userScale;
|
||
|
||
QVector<QPointF> poly0;
|
||
poly0.reserve(e.polygonLocal.size());
|
||
for (const auto& lp : e.polygonLocal) {
|
||
poly0.push_back(originWorld + lp * scale0);
|
||
}
|
||
const QPointF c0 = poly0.isEmpty() ? originWorld : entity_cutout::polygonCentroid(poly0);
|
||
const int z = (!m_depthImage8.isNull()) ? sampleDepthAtPoint(m_depthImage8, c0) : e.depth;
|
||
v.depth = z;
|
||
|
||
const double ds01 = depthToScale01(z);
|
||
v.animatedDepthScale01 = ds01;
|
||
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);
|
||
|
||
if (!e.runtimeImagePng.isEmpty()) {
|
||
QImage img;
|
||
img.loadFromData(e.runtimeImagePng, "PNG");
|
||
if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) {
|
||
img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
v.image = img;
|
||
} else {
|
||
const QString imgRel = e.imagePath;
|
||
if (imgRel.startsWith(QStringLiteral("pngb64:"))) {
|
||
const QByteArray b64 = imgRel.mid(QStringLiteral("pngb64:").size()).toLatin1();
|
||
const QByteArray raw = QByteArray::fromBase64(b64);
|
||
QImage img;
|
||
img.loadFromData(raw, "PNG");
|
||
if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) {
|
||
img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
v.image = img;
|
||
} else 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);
|
||
}
|
||
|
||
// 绘制/命中顺序:
|
||
// - priority 小(低)先画,大(高)后画,高优先级盖住低优先级
|
||
// - priority 相同:深度小(远)先画,大(近)后画,近处盖住远处(等价于按距离缩放决定上下层)
|
||
std::stable_sort(m_entities.begin(), m_entities.end(),
|
||
[](const Entity& a, const Entity& b) {
|
||
if (a.priority != b.priority) {
|
||
return a.priority < b.priority;
|
||
}
|
||
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());
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
void EditorCanvas::setTools(const QVector<core::Project::Tool>& tools, const QVector<double>& opacities01) {
|
||
m_tools.clear();
|
||
// 需要用深度图为工具采样一个“近远”用于同优先级排序(与实体一致)
|
||
if (!m_depthAbsPath.isEmpty()) {
|
||
if (m_depthDirty) {
|
||
m_depthDirty = false;
|
||
m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
|
||
}
|
||
}
|
||
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;
|
||
tv.depthZ = (!m_depthImage8.isNull()) ? sampleDepthAtPoint(m_depthImage8, tv.tool.originWorld) : 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::setCameraOverlays(const QVector<core::Project::Camera>& cameras,
|
||
const QString& selectedId,
|
||
const QSet<QString>& tempHiddenCameraIds) {
|
||
const bool hadSelectedCamera = !m_selectedCameraId.isEmpty() || m_selectedCameraIndex >= 0 || m_draggingCamera;
|
||
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;
|
||
if (hadSelectedCamera) {
|
||
emit selectedCameraChanged(false, QString(), QPointF(), 1.0);
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
m_currentFrame = std::max(0, frame);
|
||
// 仅切帧时,实体由 MainWindow 刷新时回灌;这里也触发重绘用于坐标轴/叠加
|
||
update();
|
||
}
|
||
|
||
QPointF EditorCanvas::selectedAnimatedOriginWorld() const {
|
||
return selectedEntityPivotWorld();
|
||
}
|
||
|
||
QPointF EditorCanvas::selectedEntityPivotWorld() 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();
|
||
clearCameraSelection();
|
||
return;
|
||
}
|
||
clearCameraSelection();
|
||
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::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();
|
||
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_bhCopyPatchValid = false;
|
||
m_bhCopyPatch = QImage();
|
||
if (m_useViewportLod && !m_bgAbsPath.isEmpty()) {
|
||
QRect uni =
|
||
holeRect.united(srcRect).toAlignedRect().intersected(bg.toAlignedRect());
|
||
if (uni.isValid() && uni.width() > 0 && uni.height() > 0 &&
|
||
core::image_file::loadRegionToQImage(m_bgAbsPath, uni, 8192, 8192, &m_bhCopyPatch) &&
|
||
!m_bhCopyPatch.isNull()) {
|
||
if (m_bhCopyPatch.format() != QImage::Format_ARGB32_Premultiplied) {
|
||
m_bhCopyPatch = m_bhCopyPatch.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
m_bhCopyPatchOrigin = uni.topLeft();
|
||
m_bhCopyPatchValid = true;
|
||
}
|
||
}
|
||
if (!m_bhCopyPatchValid && 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);
|
||
}
|
||
}
|
||
|
||
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_bhCopyPatchValid = false;
|
||
m_bhCopyPatch = QImage();
|
||
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::notifyBlackholeOverlaysChanged() {
|
||
m_blackholeOverlayImageCache.clear();
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::setBackgroundImagePath(const QString& absolutePath) {
|
||
if (m_bgAbsPath == absolutePath) {
|
||
return;
|
||
}
|
||
cancelBlackholeCopyResolve();
|
||
m_bgAbsPath = absolutePath;
|
||
m_initialBgLoadFinishedEmitted = false;
|
||
invalidatePixmap();
|
||
m_bgImageDirty = true;
|
||
m_bgCutoutDirty = true;
|
||
m_bgImage = QImage();
|
||
m_bgImageCutout = QImage();
|
||
zoomToFit();
|
||
update();
|
||
tryEmitInitialBackgroundLoadFinished();
|
||
}
|
||
|
||
void EditorCanvas::tryEmitInitialBackgroundLoadFinished() {
|
||
if (m_initialBgLoadFinishedEmitted) {
|
||
return;
|
||
}
|
||
if (m_bgAbsPath.isEmpty()) {
|
||
m_initialBgLoadFinishedEmitted = true;
|
||
emit initialBackgroundLoadFinished();
|
||
return;
|
||
}
|
||
ensurePixmapLoaded();
|
||
if (m_useViewportLod) {
|
||
if (!m_bgViewportImage.isNull()) {
|
||
m_initialBgLoadFinishedEmitted = true;
|
||
emit initialBackgroundLoadFinished();
|
||
}
|
||
return;
|
||
}
|
||
m_initialBgLoadFinishedEmitted = true;
|
||
emit initialBackgroundLoadFinished();
|
||
}
|
||
|
||
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);
|
||
invalidateViewportLod();
|
||
update();
|
||
}
|
||
|
||
void EditorCanvas::zoomToFit() {
|
||
ensurePixmapLoaded();
|
||
if ((!m_bgLogicalSize.isValid() && m_bgPixmap.isNull()) || width() <= 1 || height() <= 1) {
|
||
resetView();
|
||
return;
|
||
}
|
||
|
||
const QSizeF viewSize = size();
|
||
const QSizeF imgSize =
|
||
m_bgLogicalSize.isValid() ? QSizeF(m_bgLogicalSize) : QSizeF(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>(kViewScaleMin, 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);
|
||
invalidateViewportLod();
|
||
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();
|
||
m_bgLogicalSize = QSize();
|
||
m_useViewportLod = false;
|
||
m_bgViewportImage = QImage();
|
||
m_bgViewportCacheRect = QRect();
|
||
m_bgViewportDirty = true;
|
||
m_vpCachedDensity = 0.0;
|
||
m_vpPreferRegionDecode = false;
|
||
++m_vpToken;
|
||
}
|
||
|
||
void EditorCanvas::invalidateViewportLod() {
|
||
m_bgViewportDirty = true;
|
||
++m_vpToken;
|
||
}
|
||
|
||
void EditorCanvas::syncFullBackgroundImageFromDiskIfDirty() {
|
||
if (m_bgAbsPath.isEmpty() || m_useViewportLod) {
|
||
return;
|
||
}
|
||
if (!m_bgImageDirty) {
|
||
return;
|
||
}
|
||
m_bgImageDirty = false;
|
||
m_bgImage = readImageTolerant(m_bgAbsPath);
|
||
if (!m_bgImage.isNull() && m_bgImage.format() != QImage::Format_ARGB32_Premultiplied) {
|
||
m_bgImage = m_bgImage.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
m_bgCutoutDirty = true;
|
||
}
|
||
|
||
void EditorCanvas::rebuildFullBackgroundCutoutIfDirty() {
|
||
if (m_useViewportLod || m_bgAbsPath.isEmpty()) {
|
||
return;
|
||
}
|
||
if (!m_bgCutoutDirty) {
|
||
return;
|
||
}
|
||
if (m_bgImage.isNull()) {
|
||
syncFullBackgroundImageFromDiskIfDirty();
|
||
}
|
||
if (m_bgImage.isNull()) {
|
||
return;
|
||
}
|
||
m_bgCutoutDirty = false;
|
||
m_bgImageCutout = m_bgImage;
|
||
for (const auto& ent : m_entities) {
|
||
if (ent.blackholeVisible && !ent.cutoutPolygonWorld.isEmpty()) {
|
||
entity_cutout::applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
|
||
}
|
||
}
|
||
}
|
||
|
||
QVector<QVector<QPointF>> EditorCanvas::visibleBlackholeCutoutPolysWorld() const {
|
||
QVector<QVector<QPointF>> out;
|
||
for (const auto& ent : m_entities) {
|
||
if (ent.blackholeVisible && ent.cutoutPolygonWorld.size() >= 3) {
|
||
out.push_back(ent.cutoutPolygonWorld);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
void EditorCanvas::applyBlackHolesWorldToCrop(QImage& cropPremul, const QRect& cropWorld) const {
|
||
entity_cutout::applyBlackHolesToImageRegion(cropPremul, cropWorld, visibleBlackholeCutoutPolysWorld());
|
||
}
|
||
|
||
bool EditorCanvas::loadRgbCropForEntitySegmentStroke(const QRectF& polyBrWorld, QRect& outCropWorld,
|
||
QImage& outCropRgb) {
|
||
outCropRgb = QImage();
|
||
if (m_bgAbsPath.isEmpty()) {
|
||
return false;
|
||
}
|
||
ensurePixmapLoaded();
|
||
QRect cropWorld = entity_cutout::clampRectToImage(
|
||
polyBrWorld.adjusted(-kSamCropMargin, -kSamCropMargin, kSamCropMargin, kSamCropMargin).toAlignedRect(),
|
||
QSize(backgroundLogicalWidth(), backgroundLogicalHeight()));
|
||
if (cropWorld.isEmpty()) {
|
||
return false;
|
||
}
|
||
outCropWorld = cropWorld;
|
||
|
||
if (!m_useViewportLod) {
|
||
syncFullBackgroundImageFromDiskIfDirty();
|
||
rebuildFullBackgroundCutoutIfDirty();
|
||
if (!m_bgImageCutout.isNull()) {
|
||
outCropRgb = m_bgImageCutout.copy(cropWorld);
|
||
}
|
||
}
|
||
if (outCropRgb.isNull()) {
|
||
(void)core::image_file::loadRegionToQImage(m_bgAbsPath, cropWorld, 4096, 4096, &outCropRgb);
|
||
if (!outCropRgb.isNull() && outCropRgb.format() != QImage::Format_ARGB32_Premultiplied) {
|
||
outCropRgb = outCropRgb.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
}
|
||
if (outCropRgb.isNull() && !m_bgImage.isNull()) {
|
||
const QRect cw = entity_cutout::clampRectToImage(
|
||
polyBrWorld.adjusted(-kSamCropMargin, -kSamCropMargin, kSamCropMargin, kSamCropMargin).toAlignedRect(),
|
||
m_bgImage.size());
|
||
if (!cw.isEmpty()) {
|
||
outCropRgb = m_bgImage.copy(cw);
|
||
outCropWorld = cw;
|
||
}
|
||
}
|
||
if (outCropRgb.isNull()) {
|
||
syncFullBackgroundImageFromDiskIfDirty();
|
||
rebuildFullBackgroundCutoutIfDirty();
|
||
if (!m_bgImage.isNull()) {
|
||
const QRect cw2 = entity_cutout::clampRectToImage(
|
||
polyBrWorld.adjusted(-kSamCropMargin, -kSamCropMargin, kSamCropMargin, kSamCropMargin).toAlignedRect(),
|
||
m_bgImage.size());
|
||
if (!cw2.isEmpty()) {
|
||
outCropRgb = m_bgImage.copy(cw2);
|
||
outCropWorld = cw2;
|
||
}
|
||
}
|
||
}
|
||
if (outCropRgb.isNull()) {
|
||
return false;
|
||
}
|
||
if (outCropRgb.format() != QImage::Format_ARGB32_Premultiplied) {
|
||
outCropRgb = outCropRgb.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
applyBlackHolesWorldToCrop(outCropRgb, outCropWorld);
|
||
return true;
|
||
}
|
||
|
||
void EditorCanvas::ensurePixmapLoaded() const {
|
||
if (!m_pixmapDirty) {
|
||
return;
|
||
}
|
||
m_pixmapDirty = false;
|
||
m_bgPixmap = QPixmap();
|
||
m_bgLogicalSize = QSize();
|
||
m_useViewportLod = false;
|
||
if (m_bgAbsPath.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
if (core::VipsBackend::isAvailable() &&
|
||
core::image_file::probeImagePixelSize(m_bgAbsPath, &m_bgLogicalSize) && m_bgLogicalSize.isValid()) {
|
||
m_useViewportLod = true;
|
||
m_bgViewportDirty = true;
|
||
m_bgImageDirty = true;
|
||
m_bgCutoutDirty = true;
|
||
return;
|
||
}
|
||
|
||
const QImage img = readImageTolerant(m_bgAbsPath);
|
||
if (!img.isNull()) {
|
||
m_bgPixmap = QPixmap::fromImage(img);
|
||
m_bgLogicalSize = img.size();
|
||
}
|
||
m_bgImageDirty = true;
|
||
m_bgCutoutDirty = true;
|
||
}
|
||
|
||
void EditorCanvas::ensureBackgroundViewport() {
|
||
if (!m_useViewportLod || m_bgAbsPath.isEmpty() || !m_bgLogicalSize.isValid()) {
|
||
return;
|
||
}
|
||
|
||
const QRectF bgRect(0, 0, m_bgLogicalSize.width(), m_bgLogicalSize.height());
|
||
QRectF vis = visibleWorldRectF().intersected(bgRect);
|
||
if (vis.isEmpty()) {
|
||
m_bgViewportImage = QImage();
|
||
m_bgViewportCacheRect = QRect();
|
||
m_bgViewportDirty = false;
|
||
m_vpDecodeInFlight = false;
|
||
return;
|
||
}
|
||
|
||
const QRect visI = vis.toAlignedRect();
|
||
const qreal pr = devicePixelRatio();
|
||
const qreal density = static_cast<qreal>(m_scale) * pr;
|
||
const bool previewCameraHighQ = m_presentationPreviewMode && m_previewCameraViewLocked;
|
||
const qreal decodeBoost = previewCameraHighQ ? 1.35 : 1.0;
|
||
// 视口背景在屏幕上通常会经历一次缩放(世界坐标 -> 视图像素)。
|
||
// 为了减少“发糊”,静止时做适度超采样:用更高分辨率解码,再由绘制阶段缩小。
|
||
// 拖拽交互时降低超采样,优先帧率。
|
||
const qreal superSample = m_draggingEntity ? 1.15 : (previewCameraHighQ ? 1.75 : 1.55);
|
||
const qreal densityTol = previewCameraHighQ ? 0.045 : 0.09;
|
||
|
||
// 缓存仍覆盖视口且清晰度档位未变:直接复用(避免 invalidateViewportLod 仅置 dirty 又触发二次解码失败把画面清空)
|
||
// m_vpPreferRegionDecode 时不短路:必须在缩略图显示后立刻做一次区域细化
|
||
if (!m_vpPreferRegionDecode && !m_bgViewportImage.isNull() && m_bgViewportCacheRect.contains(visI) &&
|
||
m_vpCachedDensity > 1e-9 &&
|
||
std::abs(density - m_vpCachedDensity) / m_vpCachedDensity < densityTol) {
|
||
m_bgViewportDirty = false;
|
||
return;
|
||
}
|
||
|
||
if (m_vpDecodeInFlight.exchange(true)) {
|
||
return;
|
||
}
|
||
|
||
const QString path = m_bgAbsPath;
|
||
const QSize logicalSize = m_bgLogicalSize;
|
||
const uint32_t myToken = m_vpToken.load(std::memory_order_relaxed);
|
||
|
||
const qint64 imgArea = qint64(logicalSize.width()) * qint64(logicalSize.height());
|
||
const qint64 visArea = qint64(visI.width()) * qint64(visI.height());
|
||
// 预览且镜头锁定时禁用全图缩略路径,避免画面明显发糊
|
||
const bool overviewMode =
|
||
!m_vpPreferRegionDecode && !previewCameraHighQ && imgArea > 0 &&
|
||
(static_cast<double>(visArea) / static_cast<double>(imgArea) >= 0.68) &&
|
||
core::VipsBackend::isAvailable();
|
||
|
||
// 扩大解码缓存:平移时尽量仍在同一张纹理内,避免每帧触发新的后台解码导致卡顿
|
||
const qreal screenWorldW = (static_cast<qreal>(width()) * pr) / std::max(m_scale, static_cast<qreal>(1e-9));
|
||
const qreal screenWorldH = (static_cast<qreal>(height()) * pr) / std::max(m_scale, static_cast<qreal>(1e-9));
|
||
const qreal padX = std::max(vis.width() * 0.95, screenWorldW * 0.75);
|
||
const qreal padY = std::max(vis.height() * 0.95, screenWorldH * 0.75);
|
||
QRect req =
|
||
vis.adjusted(-padX, -padY, padX, padY).toAlignedRect().intersected(bgRect.toAlignedRect());
|
||
if (req.width() <= 0 || req.height() <= 0) {
|
||
m_vpDecodeInFlight = false;
|
||
if (m_bgViewportImage.isNull()) {
|
||
m_bgViewportCacheRect = QRect();
|
||
}
|
||
m_bgViewportDirty = false;
|
||
return;
|
||
}
|
||
|
||
// 关键:解码目标分辨率必须以“req 区域在当前缩放下投影到屏幕的像素数”为基准。
|
||
// 否则后台解码完成后替换纹理时,画面会因为额外的 drawImage 缩放而突然变糊。
|
||
const int maxW = std::clamp(static_cast<int>(std::ceil(req.width() * density * superSample * decodeBoost)), 256, 8192);
|
||
const int maxH = std::clamp(static_cast<int>(std::ceil(req.height() * density * superSample * decodeBoost)), 256, 8192);
|
||
|
||
const qint64 budget =
|
||
qint64(maxW) * qint64(maxH) * (overviewMode ? 2 : 1); // 缩略图阶段提高像素预算,减少首帧模糊感
|
||
|
||
(void)QtConcurrent::run([this, path, logicalSize, req, maxW, maxH, budget, density, overviewMode,
|
||
myToken]() {
|
||
QImage img;
|
||
bool ok = false;
|
||
if (overviewMode) {
|
||
ok = core::VipsBackend::loadThumbnailQImage(path, budget, &img) && !img.isNull();
|
||
}
|
||
if (!ok) {
|
||
ok = core::image_file::loadRegionToQImage(path, req, maxW, maxH, &img) && !img.isNull();
|
||
}
|
||
|
||
QPointer<EditorCanvas> self(this);
|
||
QTimer::singleShot(
|
||
0,
|
||
this,
|
||
[self, myToken, path, img = std::move(img), ok, logicalSize, req, density, overviewMode]() mutable {
|
||
if (!self) {
|
||
return;
|
||
}
|
||
self->m_vpDecodeInFlight = false;
|
||
if (myToken != self->m_vpToken.load(std::memory_order_relaxed)) {
|
||
self->update();
|
||
return;
|
||
}
|
||
if (path != self->m_bgAbsPath) {
|
||
self->update();
|
||
return;
|
||
}
|
||
if (!ok || img.isNull()) {
|
||
// 保留已成功显示过的纹理,避免后续一次失败把整屏打成「加载失败」
|
||
if (self->m_bgViewportImage.isNull()) {
|
||
self->m_bgViewportCacheRect = QRect();
|
||
}
|
||
self->m_vpPreferRegionDecode = false;
|
||
self->m_bgViewportDirty = false;
|
||
self->update();
|
||
self->tryEmitInitialBackgroundLoadFinished();
|
||
return;
|
||
}
|
||
if (img.format() != QImage::Format_ARGB32_Premultiplied) {
|
||
img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
self->m_bgViewportImage = std::move(img);
|
||
self->m_bgViewportCacheRect =
|
||
overviewMode ? QRect(QPoint(0, 0), logicalSize) : req;
|
||
self->m_bgViewportDirty = false;
|
||
self->m_vpCachedDensity = density;
|
||
if (overviewMode) {
|
||
self->m_vpPreferRegionDecode = true;
|
||
} else {
|
||
self->m_vpPreferRegionDecode = false;
|
||
}
|
||
self->update();
|
||
self->tryEmitInitialBackgroundLoadFinished();
|
||
});
|
||
});
|
||
}
|
||
|
||
QRectF EditorCanvas::visibleWorldRectF() const {
|
||
const QPointF c0 = viewToWorld(QPointF(0, 0));
|
||
const QPointF c1 = viewToWorld(QPointF(width(), 0));
|
||
const QPointF c2 = viewToWorld(QPointF(width(), height()));
|
||
const QPointF c3 = viewToWorld(QPointF(0, height()));
|
||
const qreal minx = std::min({c0.x(), c1.x(), c2.x(), c3.x()});
|
||
const qreal maxx = std::max({c0.x(), c1.x(), c2.x(), c3.x()});
|
||
const qreal miny = std::min({c0.y(), c1.y(), c2.y(), c3.y()});
|
||
const qreal maxy = std::max({c0.y(), c1.y(), c2.y(), c3.y()});
|
||
QRectF rf(minx, miny, maxx - minx, maxy - miny);
|
||
return rf.normalized();
|
||
}
|
||
|
||
int EditorCanvas::backgroundLogicalWidth() const {
|
||
ensurePixmapLoaded();
|
||
if (m_bgLogicalSize.isValid()) {
|
||
return m_bgLogicalSize.width();
|
||
}
|
||
return m_bgPixmap.isNull() ? 0 : m_bgPixmap.width();
|
||
}
|
||
|
||
int EditorCanvas::backgroundLogicalHeight() const {
|
||
ensurePixmapLoaded();
|
||
if (m_bgLogicalSize.isValid()) {
|
||
return m_bgLogicalSize.height();
|
||
}
|
||
return m_bgPixmap.isNull() ? 0 : m_bgPixmap.height();
|
||
}
|
||
|
||
void EditorCanvas::updateCursor() {
|
||
if (m_blackholeCopyResolveActive) {
|
||
setCursor(m_blackholeCopyDragging ? Qt::ClosedHandCursor : Qt::OpenHandCursor);
|
||
return;
|
||
}
|
||
if (m_presentationPreviewMode) {
|
||
if (!m_presentationHotspotPlaybackTargetEnabled) {
|
||
setCursor(Qt::ArrowCursor);
|
||
return;
|
||
}
|
||
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;
|
||
case Tool::AddHotspot:
|
||
setCursor(Qt::CrossCursor);
|
||
break;
|
||
case Tool::MoveHotspot:
|
||
setCursor(Qt::OpenHandCursor);
|
||
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_bgLogicalSize.isValid()) {
|
||
return QRectF(0, 0, m_bgLogicalSize.width(), m_bgLogicalSize.height());
|
||
}
|
||
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;
|
||
}
|
||
|
||
int EditorCanvas::hitTestPresentationHotspot(const QPointF& worldPos) const {
|
||
const QPointF pv = worldToView(worldPos);
|
||
for (int i = static_cast<int>(m_presentationHotspots.size()) - 1; i >= 0; --i) {
|
||
const QPointF cv = worldToView(m_presentationHotspots[i].centerWorld);
|
||
if (QLineF(pv, cv).length() <= 22.0) {
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
int EditorCanvas::hitTestHotspotPlayControl(const QPointF& worldPos) const {
|
||
if (!m_presentationHotspotPlaybackTargetEnabled) {
|
||
return -1;
|
||
}
|
||
const QPointF pv = worldToView(worldPos);
|
||
for (int i = static_cast<int>(m_presentationHotspots.size()) - 1; i >= 0; --i) {
|
||
const QPointF cv = worldToView(m_presentationHotspots[i].centerWorld);
|
||
const QPointF playCenter = cv + QPointF(14.0, 0.0);
|
||
if (QLineF(pv, playCenter).length() <= 17.0) {
|
||
return i;
|
||
}
|
||
}
|
||
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()) {
|
||
return;
|
||
}
|
||
if (m_useViewportLod) {
|
||
ensureBackgroundViewport();
|
||
if (m_bgViewportImage.isNull()) {
|
||
return;
|
||
}
|
||
} else if (m_bgPixmap.isNull()) {
|
||
tryEmitInitialBackgroundLoadFinished();
|
||
return;
|
||
}
|
||
|
||
const bool showBg = m_presentationPreviewMode || m_backgroundVisible;
|
||
if (showBg && !m_useViewportLod) {
|
||
syncFullBackgroundImageFromDiskIfDirty();
|
||
rebuildFullBackgroundCutoutIfDirty();
|
||
}
|
||
|
||
// 以“世界坐标”绘制:支持缩放/平移
|
||
p.save();
|
||
QTransform t;
|
||
t.translate(m_pan.x(), m_pan.y());
|
||
t.scale(m_scale, m_scale);
|
||
p.setTransform(t, true);
|
||
|
||
const qreal bw = static_cast<qreal>(backgroundLogicalWidth());
|
||
const qreal bh = static_cast<qreal>(backgroundLogicalHeight());
|
||
|
||
if (showBg) {
|
||
if (m_useViewportLod) {
|
||
p.drawImage(QRectF(m_bgViewportCacheRect), m_bgViewportImage);
|
||
if (showBg) {
|
||
for (const auto& ent : m_entities) {
|
||
if (ent.blackholeVisible && !ent.cutoutPolygonWorld.isEmpty()) {
|
||
p.fillPath(entity_cutout::pathFromWorldPolygon(ent.cutoutPolygonWorld), Qt::black);
|
||
}
|
||
}
|
||
}
|
||
} else if (m_backgroundVisible || m_presentationPreviewMode) {
|
||
if (!m_bgImageCutout.isNull()) {
|
||
p.drawImage(QPointF(0, 0), m_bgImageCutout);
|
||
} else if (!m_bgImage.isNull()) {
|
||
p.drawImage(QPointF(0, 0), m_bgImage);
|
||
} else {
|
||
p.drawPixmap(QPointF(0, 0), m_bgPixmap);
|
||
}
|
||
}
|
||
if (!m_presentationPreviewMode && m_backgroundVisible && bw > 0 && bh > 0) {
|
||
p.setPen(QPen(QColor(0, 0, 0, 80), 1.0 / std::max<qreal>(m_scale, 0.001)));
|
||
p.drawRect(QRectF(0, 0, bw, bh).adjusted(0, 0, -1, -1));
|
||
}
|
||
|
||
if (!m_projectDirAbs.isEmpty()) {
|
||
for (const auto& ent : m_entities) {
|
||
if (ent.blackholeOverlayPath.isEmpty() || ent.blackholeOverlayRect.width() <= 0 ||
|
||
ent.blackholeOverlayRect.height() <= 0) {
|
||
continue;
|
||
}
|
||
QImage ovl;
|
||
if (m_blackholeOverlayImageCache.contains(ent.id)) {
|
||
ovl = m_blackholeOverlayImageCache.value(ent.id);
|
||
} else {
|
||
const QString abs = QDir(m_projectDirAbs).filePath(ent.blackholeOverlayPath);
|
||
ovl = readImageTolerant(abs);
|
||
if (!ovl.isNull() && ovl.format() != QImage::Format_ARGB32_Premultiplied) {
|
||
ovl = ovl.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||
}
|
||
if (!ovl.isNull()) {
|
||
m_blackholeOverlayImageCache.insert(ent.id, ovl);
|
||
}
|
||
}
|
||
if (ovl.isNull()) {
|
||
continue;
|
||
}
|
||
p.drawImage(QPointF(ent.blackholeOverlayRect.topLeft()), ovl);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 深度叠加(伪彩色):仅由「叠加深度」开关控制,与是否显示背景无关
|
||
const bool wantDepth =
|
||
!m_presentationPreviewMode && (!m_depthAbsPath.isEmpty()) && m_depthOverlayEnabled;
|
||
if (wantDepth) {
|
||
if (m_depthDirty) {
|
||
m_depthDirty = false;
|
||
m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
|
||
}
|
||
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));
|
||
}
|
||
}
|
||
|
||
if (!m_presentationPreviewMode && m_blackholeCopyResolveActive &&
|
||
!m_blackholeCopyHoleRect.isNull() && !m_blackholeCopySourceRect.isNull()) {
|
||
const QRect srcRect = m_blackholeCopySourceRect.toAlignedRect();
|
||
const QRect dstRect = m_blackholeCopyHoleRect.toAlignedRect();
|
||
const bool havePreview =
|
||
(m_bhCopyPatchValid && !m_bhCopyPatch.isNull()) || !m_bgImage.isNull();
|
||
if (havePreview && 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);
|
||
if (m_bhCopyPatchValid && !m_bhCopyPatch.isNull()) {
|
||
const QRect srcLocal = srcRect.translated(-m_bhCopyPatchOrigin);
|
||
p.drawImage(dstRect.topLeft(), m_bhCopyPatch, srcLocal);
|
||
} else {
|
||
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);
|
||
|
||
}
|
||
|
||
struct DrawItem {
|
||
enum class Kind { Entity, Tool };
|
||
Kind kind = Kind::Entity;
|
||
int index = -1;
|
||
int priority = 0;
|
||
int depth = 0;
|
||
};
|
||
|
||
QVector<DrawItem> drawOrder;
|
||
drawOrder.reserve(m_entities.size() + m_tools.size());
|
||
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;
|
||
drawOrder.push_back(DrawItem{DrawItem::Kind::Entity, i, ent.priority, ent.depth});
|
||
}
|
||
for (int i = 0; i < m_tools.size(); ++i) {
|
||
const auto& tv = m_tools[i];
|
||
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;
|
||
drawOrder.push_back(DrawItem{DrawItem::Kind::Tool, i, tv.tool.priority, tv.depthZ});
|
||
}
|
||
// priority 低先画、高后画;同 priority:远先画、近后画
|
||
std::stable_sort(drawOrder.begin(), drawOrder.end(), [](const DrawItem& a, const DrawItem& b) {
|
||
if (a.priority != b.priority) return a.priority < b.priority;
|
||
if (a.depth != b.depth) return a.depth < b.depth;
|
||
if (a.kind != b.kind) return int(a.kind) < int(b.kind);
|
||
return a.index < b.index;
|
||
});
|
||
|
||
for (const auto& it : drawOrder) {
|
||
if (it.kind == DrawItem::Kind::Entity) {
|
||
const int i = it.index;
|
||
const auto& ent = m_entities[i];
|
||
const bool isDragPreview =
|
||
(!m_presentationPreviewMode && m_draggingEntity && m_dragPreviewActive && i == m_selectedEntity);
|
||
if (!ent.polygonWorld.isEmpty()) {
|
||
if (!ent.image.isNull()) {
|
||
if (isDragPreview) {
|
||
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);
|
||
}
|
||
QImage drawImg = ent.image;
|
||
if (m_presentationPreviewMode && !ent.image.isNull()) {
|
||
const qreal pr = devicePixelRatio();
|
||
const qreal pxW = std::abs(target.width() * m_scale * pr);
|
||
const qreal pxH = std::abs(target.height() * m_scale * pr);
|
||
drawImg = entityImageForPresentationDrawLoRes(ent.image, pxW, pxH);
|
||
}
|
||
p.drawImage(target, drawImg);
|
||
}
|
||
} 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.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);
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
const int i = it.index;
|
||
const auto& tv = m_tools[i];
|
||
const auto& tool = tv.tool;
|
||
const double opacity = std::clamp(tv.opacity, 0.0, 1.0);
|
||
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 摄像机视口框(编辑模式)
|
||
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));
|
||
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 && backgroundLogicalWidth() > 0 &&
|
||
backgroundLogicalHeight() > 0) {
|
||
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 轴(垂直)
|
||
|
||
// 根据缩放与视口大小动态调整刻度密度:使相邻刻度在屏幕上保持“够密但不挤”的间距
|
||
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(backgroundLogicalWidth()));
|
||
b = std::clamp(b, 0.0, double(backgroundLogicalWidth()));
|
||
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(backgroundLogicalHeight()));
|
||
b = std::clamp(b, 0.0, double(backgroundLogicalHeight()));
|
||
return {a, b};
|
||
};
|
||
|
||
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 上
|
||
{
|
||
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(backgroundLogicalWidth()));
|
||
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) {
|
||
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 上
|
||
{
|
||
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(backgroundLogicalHeight()));
|
||
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) {
|
||
if (labelEvery <= 1 || (iTick % labelEvery) == 0) {
|
||
p.drawText(QPointF(axisX + 10, ty - 2), QString::number(int(std::lround(yc))));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 h = 7.0;
|
||
QPen outline(QColor(15, 15, 18, 235));
|
||
outline.setWidth(3);
|
||
outline.setCapStyle(Qt::FlatCap);
|
||
QPen core(QColor(255, 248, 220, 252));
|
||
core.setWidth(1);
|
||
core.setCapStyle(Qt::FlatCap);
|
||
p.setPen(outline);
|
||
p.drawLine(QPointF(originView.x() - h, originView.y()), QPointF(originView.x() + h, originView.y()));
|
||
p.drawLine(QPointF(originView.x(), originView.y() - h), QPointF(originView.x(), originView.y() + h));
|
||
p.setPen(core);
|
||
p.drawLine(QPointF(originView.x() - h, originView.y()), QPointF(originView.x() + h, originView.y()));
|
||
p.drawLine(QPointF(originView.x(), originView.y() - h), QPointF(originView.x(), originView.y() + h));
|
||
}
|
||
|
||
// 变换原点(枢轴):与形心不同时显示
|
||
{
|
||
const QPointF pivotWorld = ent.animatedOriginWorld;
|
||
const double sepWorld = QLineF(originWorld, pivotWorld).length();
|
||
if (sepWorld > 0.5) {
|
||
const QPointF pv = worldToView(pivotWorld);
|
||
const qreal pivotDotR = 4.5;
|
||
p.setPen(QPen(QColor(40, 40, 48, 220), 1));
|
||
p.setBrush(QColor(255, 120, 210, 235));
|
||
p.drawEllipse(pv, pivotDotR, pivotDotR);
|
||
p.setBrush(Qt::NoBrush);
|
||
}
|
||
}
|
||
|
||
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"));
|
||
}
|
||
}
|
||
|
||
if (!m_presentationHotspots.isEmpty()) {
|
||
if (m_presentationPreviewMode && m_presentationHotspotPlaybackTargetEnabled) {
|
||
p.setRenderHint(QPainter::Antialiasing, true);
|
||
for (const auto& h : m_presentationHotspots) {
|
||
const QPointF cv = worldToView(h.centerWorld);
|
||
p.setPen(QPen(QColor(255, 248, 220, 235), 2));
|
||
p.setBrush(QColor(30, 34, 42, 170));
|
||
p.drawEllipse(cv, 16.0, 16.0);
|
||
QPolygonF tri;
|
||
tri << (cv + QPointF(-3.0, -6.0)) << (cv + QPointF(-3.0, 6.0)) << (cv + QPointF(8.0, 0.0));
|
||
p.setBrush(QColor(255, 248, 220, 250));
|
||
p.drawPolygon(tri);
|
||
}
|
||
p.setRenderHint(QPainter::Antialiasing, false);
|
||
} else if (m_presentationHotspotEditorActive) {
|
||
p.setRenderHint(QPainter::Antialiasing, true);
|
||
for (const auto& h : m_presentationHotspots) {
|
||
const QPointF cv = worldToView(h.centerWorld);
|
||
const bool sel = (h.id == m_selectedPresentationHotspotId);
|
||
p.setPen(QPen(sel ? QColor(255, 210, 72) : QColor(92, 200, 255), 2));
|
||
p.setBrush(QColor(24, 28, 36, 150));
|
||
p.drawEllipse(cv, 14.0, 14.0);
|
||
}
|
||
p.setRenderHint(QPainter::Antialiasing, false);
|
||
}
|
||
}
|
||
|
||
tryEmitInitialBackgroundLoadFinished();
|
||
}
|
||
|
||
void EditorCanvas::resizeEvent(QResizeEvent* e) {
|
||
QWidget::resizeEvent(e);
|
||
invalidateViewportLod();
|
||
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;
|
||
m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
|
||
}
|
||
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 && m_presentationHotspotEditorActive && e->button() == Qt::LeftButton) {
|
||
if (m_tool == Tool::AddHotspot) {
|
||
emit requestAddPresentationHotspot(wp0);
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (m_tool == Tool::MoveHotspot) {
|
||
const int hi = hitTestPresentationHotspot(wp0);
|
||
if (hi >= 0) {
|
||
m_dragging = true;
|
||
m_draggingHotspotIndex = hi;
|
||
m_hotspotDragOffsetWorld = wp0 - m_presentationHotspots[hi].centerWorld;
|
||
m_selectedPresentationHotspotId = m_presentationHotspots[hi].id;
|
||
m_lastMouseView = e->position();
|
||
setCursor(Qt::ClosedHandCursor);
|
||
emit selectedPresentationHotspotChanged(m_selectedPresentationHotspotId);
|
||
} else {
|
||
m_selectedPresentationHotspotId.clear();
|
||
emit selectedPresentationHotspotChanged(QString());
|
||
}
|
||
update();
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (m_tool == Tool::Move) {
|
||
m_dragging = true;
|
||
m_lastMouseView = e->position();
|
||
setCursor(Qt::ClosedHandCursor);
|
||
e->accept();
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (m_blackholeCopyResolveActive) {
|
||
if (e->button() == Qt::LeftButton) {
|
||
QRectF src = m_blackholeCopySourceRect;
|
||
if (!src.contains(wp0)) {
|
||
src.moveCenter(wp0);
|
||
src = clampRectTopLeftToBounds(src, worldRectOfBackground());
|
||
m_blackholeCopySourceRect = src;
|
||
}
|
||
m_blackholeCopyDragging = true;
|
||
m_blackholeCopyDragOffset = wp0 - m_blackholeCopySourceRect.topLeft();
|
||
updateCursor();
|
||
update();
|
||
e->accept();
|
||
return;
|
||
}
|
||
e->accept();
|
||
return;
|
||
}
|
||
|
||
if (m_presentationPreviewMode) {
|
||
if (!m_presentationHotspotPlaybackTargetEnabled) {
|
||
e->accept();
|
||
return;
|
||
}
|
||
if (e->button() == Qt::LeftButton) {
|
||
if (m_presentationHotspotPlaybackTargetEnabled) {
|
||
const int playIx = hitTestHotspotPlayControl(wp0);
|
||
if (playIx >= 0 && playIx < m_presentationHotspots.size()) {
|
||
emit hotspotPlayRequested(m_presentationHotspots[playIx].id);
|
||
e->accept();
|
||
return;
|
||
}
|
||
}
|
||
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;
|
||
}
|
||
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) {
|
||
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) {
|
||
// 摄像机:绘制在工具之上,命中优先于工具
|
||
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;
|
||
}
|
||
}
|
||
|
||
// 命中测试:按绘制顺序从上到下(priority/深度),找到顶层的实体或工具
|
||
struct HitItem {
|
||
enum class Kind { Entity, Tool };
|
||
Kind kind = Kind::Entity;
|
||
int index = -1;
|
||
int priority = 0;
|
||
int depth = 0;
|
||
};
|
||
QVector<HitItem> order;
|
||
order.reserve(m_entities.size() + m_tools.size());
|
||
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;
|
||
order.push_back(HitItem{HitItem::Kind::Entity, i, ent.priority, ent.depth});
|
||
}
|
||
for (int i = 0; i < m_tools.size(); ++i) {
|
||
const auto& tv = m_tools[i];
|
||
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;
|
||
order.push_back(HitItem{HitItem::Kind::Tool, i, tv.tool.priority, tv.depthZ});
|
||
}
|
||
std::stable_sort(order.begin(), order.end(), [](const HitItem& a, const HitItem& b) {
|
||
if (a.priority != b.priority) return a.priority < b.priority;
|
||
if (a.depth != b.depth) return a.depth < b.depth;
|
||
if (a.kind != b.kind) return int(a.kind) < int(b.kind);
|
||
return a.index < b.index;
|
||
});
|
||
for (qsizetype i = order.size(); i > 0; --i) {
|
||
const auto& it = order[i - 1];
|
||
if (it.kind == HitItem::Kind::Tool) {
|
||
const auto& tv = m_tools[it.index];
|
||
const QPainterPath path = bubblePathWorld(tv.tool);
|
||
if (!path.contains(worldPos)) continue;
|
||
clearCameraSelection();
|
||
m_selectedTool = it.index;
|
||
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;
|
||
}
|
||
const auto& ent = m_entities[it.index];
|
||
bool hit = false;
|
||
if (!ent.pathWorld.isEmpty()) hit = ent.pathWorld.contains(worldPos);
|
||
else if (!ent.polygonWorld.isEmpty()) hit = entity_cutout::pathFromWorldPolygon(ent.polygonWorld).contains(worldPos);
|
||
else hit = ent.rect.contains(worldPos);
|
||
if (!hit) continue;
|
||
// 选择实体(保持原逻辑)
|
||
clearCameraSelection();
|
||
m_selectedEntity = it.index;
|
||
m_selectedTool = -1;
|
||
m_draggingTool = false;
|
||
m_draggingEntity = true;
|
||
m_dragMode = DragMode::Free;
|
||
emit entityDragActiveChanged(true);
|
||
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;
|
||
{
|
||
const auto& he = m_entities[m_selectedEntity];
|
||
const QPointF cRef = shapeCentroidWorld(he.polygonWorld, he.rect);
|
||
const double s = std::max(1e-6, he.visualScale);
|
||
m_entityDragAnchorLocal = (worldPos - cRef) / s;
|
||
m_entityDragStartCentroidWorld = cRef;
|
||
}
|
||
// 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);
|
||
const QPointF origin = !m_entities[m_selectedEntity].polygonWorld.isEmpty()
|
||
? entity_cutout::polygonCentroid(m_entities[m_selectedEntity].polygonWorld)
|
||
: m_entities[m_selectedEntity].rect.center();
|
||
emit selectedEntityChanged(true, m_entities[m_selectedEntity].id, m_entities[m_selectedEntity].depth, origin);
|
||
emit selectedToolChanged(false, QString(), QPointF());
|
||
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 = shapeCentroidWorld(ent.polygonWorld, ent.rect);
|
||
if (isDragPreview) {
|
||
originWorld = m_dragCentroidBase + m_dragDelta;
|
||
}
|
||
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;
|
||
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;
|
||
{
|
||
const auto& se = m_entities[m_selectedEntity];
|
||
const QPointF cRef = shapeCentroidWorld(se.polygonWorld, se.rect);
|
||
const double s = std::max(1e-6, se.visualScale);
|
||
m_entityDragAnchorLocal = (viewToWorld(e->position()) - cRef) / s;
|
||
m_entityDragStartCentroidWorld = cRef;
|
||
}
|
||
// 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) {
|
||
clearCameraSelection();
|
||
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;
|
||
{
|
||
const auto& se = m_entities[m_selectedEntity];
|
||
const QPointF cRef = shapeCentroidWorld(se.polygonWorld, se.rect);
|
||
const double s = std::max(1e-6, se.visualScale);
|
||
m_entityDragAnchorLocal = (worldPos - cRef) / s;
|
||
m_entityDragStartCentroidWorld = cRef;
|
||
}
|
||
// 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) {
|
||
clearCameraSelection();
|
||
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;
|
||
{
|
||
const auto& he = m_entities[hit];
|
||
const QPointF cRef = shapeCentroidWorld(he.polygonWorld, he.rect);
|
||
const double s = std::max(1e-6, he.visualScale);
|
||
m_entityDragAnchorLocal = (worldPos - cRef) / s;
|
||
m_entityDragStartCentroidWorld = cRef;
|
||
}
|
||
// 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;
|
||
clearCameraSelection();
|
||
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;
|
||
m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
|
||
}
|
||
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_blackholeCopyResolveActive) {
|
||
if (m_blackholeCopyDragging && (e->buttons() & Qt::LeftButton)) {
|
||
QRectF src = m_blackholeCopySourceRect;
|
||
src.moveTopLeft(wp - m_blackholeCopyDragOffset);
|
||
src = clampRectTopLeftToBounds(src, worldRectOfBackground());
|
||
m_blackholeCopySourceRect = src;
|
||
update();
|
||
}
|
||
e->accept();
|
||
return;
|
||
}
|
||
|
||
if (m_presentationPreviewMode) {
|
||
if (!m_presentationHotspotPlaybackTargetEnabled) {
|
||
if (m_presHoverEntityIndex >= 0 || (m_presHoverTimer && m_presHoverTimer->isActive()) ||
|
||
m_presHoverPhase != 0.0) {
|
||
if (m_presHoverTimer) {
|
||
m_presHoverTimer->stop();
|
||
}
|
||
m_presHoverEntityIndex = -1;
|
||
m_presHoverPhase = 0.0;
|
||
updateCursor();
|
||
update();
|
||
}
|
||
} else {
|
||
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_draggingHotspotIndex >= 0 && m_draggingHotspotIndex < m_presentationHotspots.size()) {
|
||
const QPointF worldPos = viewToWorld(cur);
|
||
m_presentationHotspots[m_draggingHotspotIndex].centerWorld = worldPos - m_hotspotDragOffsetWorld;
|
||
update();
|
||
return;
|
||
}
|
||
|
||
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_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];
|
||
const double curScale = std::max(1e-6, ent.visualScale);
|
||
const QPointF centroidNow =
|
||
m_dragPreviewActive ? (m_dragCentroidBase + m_dragDelta) : shapeCentroidWorld(ent.polygonWorld, ent.rect);
|
||
const QPointF newCentroid = worldPos - m_entityDragAnchorLocal * curScale;
|
||
QPointF delta = newCentroid - centroidNow;
|
||
|
||
// 轴约束:只允许沿 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;
|
||
m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
|
||
}
|
||
if (!m_depthImage8.isNull()) {
|
||
const QPointF c =
|
||
m_dragPreviewActive ? (m_dragCentroidBase + m_dragDelta) : shapeCentroidWorld(ent.polygonWorld, ent.rect);
|
||
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, static_cast<double>(kViewScaleMax));
|
||
}
|
||
}
|
||
}
|
||
// 低频预览信号:用于属性面板同步,不做树联动等重操作
|
||
// 属性面板/状态预览:按 30Hz 刷新即可,拖动视觉仍满帧跟手
|
||
if (nowMs - m_lastPreviewEmitMs >= 33) {
|
||
m_lastPreviewEmitMs = nowMs;
|
||
const QPointF previewCenter =
|
||
m_dragPreviewActive ? (m_dragCentroidBase + m_dragDelta)
|
||
: shapeCentroidWorld(ent.polygonWorld, ent.rect);
|
||
emit selectedEntityPreviewChanged(ent.id, ent.depth, previewCenter);
|
||
}
|
||
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_previewCameraViewLocked)) {
|
||
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 (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) {
|
||
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) {
|
||
const QRectF polyBr = QPolygonF(m_strokeWorld).boundingRect();
|
||
QRect cropWorld;
|
||
QImage crop;
|
||
if (loadRgbCropForEntitySegmentStroke(polyBr, cropWorld, crop)) {
|
||
const QImage gray = crop.convertToFormat(QImage::Format_Grayscale8);
|
||
const QVector<QPointF> snapped =
|
||
snapStrokeToEdgesWorld(m_strokeWorld, gray, cropWorld.topLeft(), cropWorld.size(), 6.0);
|
||
setPendingEntityPolygonWorld(snapped);
|
||
}
|
||
}
|
||
} else if (m_strokeWorld.size() >= kMinStrokePointsSam) {
|
||
const QRectF polyBr = QPolygonF(m_strokeWorld).boundingRect();
|
||
QRect cropWorld;
|
||
QImage crop;
|
||
QByteArray cropPng;
|
||
QByteArray ovPng;
|
||
QPointF cropOrigin;
|
||
QJsonArray pts;
|
||
QJsonArray labs;
|
||
QJsonArray box;
|
||
if (loadRgbCropForEntitySegmentStroke(polyBr, cropWorld, crop) &&
|
||
buildSamSegmentPayloadFromStrokeMapped(m_strokeWorld, crop, cropWorld, 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 endCentroid = m_dragPreviewActive ? (m_dragCentroidBase + m_dragDelta)
|
||
: shapeCentroidWorld(ent.polygonWorld, ent.rect);
|
||
const QPointF delta = endCentroid - m_entityDragStartCentroidWorld;
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
if (m_draggingHotspotIndex >= 0 && e->button() == Qt::LeftButton &&
|
||
m_draggingHotspotIndex < m_presentationHotspots.size()) {
|
||
const auto& h = m_presentationHotspots[m_draggingHotspotIndex];
|
||
emit presentationHotspotMoved(h.id, h.centerWorld);
|
||
m_draggingHotspotIndex = -1;
|
||
}
|
||
|
||
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_draggingCamera = false;
|
||
m_dragPreviewActive = false;
|
||
m_dragMode = DragMode::None;
|
||
updateCursor();
|
||
}
|
||
QWidget::mouseReleaseEvent(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)
|
||
}
|
||
|
||
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, kViewScaleMin, kViewScaleMax);
|
||
if (qFuzzyCompare(newScale, m_scale)) {
|
||
return;
|
||
}
|
||
m_scale = newScale;
|
||
|
||
// 让“光标指向的 world 点”缩放后仍落在光标处
|
||
const QPointF afterView = worldToView(beforeWorld);
|
||
m_pan += (cursorView - afterView);
|
||
invalidateViewportLod();
|
||
update();
|
||
e->accept();
|
||
}
|
||
|
||
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();
|
||
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);
|
||
}
|
||
|