Files
hfut-bishe/client/gui/editor/EditorCanvas.cpp
2026-04-08 14:37:01 +08:00

1693 lines
62 KiB
C++
Raw Blame History

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