1552 lines
57 KiB
C++
1552 lines
57 KiB
C++
#include "editor/EditorCanvas.h"
|
||
|
||
#include "core/animation/AnimationSampling.h"
|
||
#include "core/depth/DepthService.h"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <QDir>
|
||
#include <QFileInfo>
|
||
#include <QCursor>
|
||
#include <QMouseEvent>
|
||
#include <QPainter>
|
||
#include <QPaintEvent>
|
||
#include <QPainterPath>
|
||
#include <QWheelEvent>
|
||
#include <QKeyEvent>
|
||
|
||
namespace {
|
||
|
||
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));
|
||
}
|
||
}
|
||
|
||
QPainterPath pathFromWorldPolygon(const QVector<QPointF>& poly) {
|
||
QPainterPath path;
|
||
if (poly.size() < 3) {
|
||
return path;
|
||
}
|
||
path.addPolygon(QPolygonF(poly));
|
||
path.closeSubpath();
|
||
return path;
|
||
}
|
||
|
||
QPointF polygonCentroid(const QVector<QPointF>& poly) {
|
||
// 简单多边形质心(面积加权)。若面积接近 0,则退化为包围盒中心。
|
||
if (poly.size() < 3) {
|
||
return {};
|
||
}
|
||
double a2 = 0.0; // 2*area
|
||
double cx6a = 0.0;
|
||
double cy6a = 0.0;
|
||
for (int i = 0; i < poly.size(); ++i) {
|
||
const QPointF p0 = poly[i];
|
||
const QPointF p1 = poly[(i + 1) % poly.size()];
|
||
const double cross = static_cast<double>(p0.x()) * static_cast<double>(p1.y()) -
|
||
static_cast<double>(p1.x()) * static_cast<double>(p0.y());
|
||
a2 += cross;
|
||
cx6a += (static_cast<double>(p0.x()) + static_cast<double>(p1.x())) * cross;
|
||
cy6a += (static_cast<double>(p0.y()) + static_cast<double>(p1.y())) * cross;
|
||
}
|
||
if (std::abs(a2) < 1e-6) {
|
||
const QRectF bb = pathFromWorldPolygon(poly).boundingRect();
|
||
return bb.center();
|
||
}
|
||
const double inv6a = 1.0 / (3.0 * a2); // 1/(6A) with a2=2A -> 1/(3*a2)
|
||
return QPointF(cx6a * inv6a, cy6a * inv6a);
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
QRect clampRectToImage(const QRect& r, const QSize& size) {
|
||
QRect out = r.normalized();
|
||
if (out.isNull()) {
|
||
return {};
|
||
}
|
||
out.setLeft(std::max(0, out.left()));
|
||
out.setTop(std::max(0, out.top()));
|
||
out.setRight(std::min(size.width() - 1, out.right()));
|
||
out.setBottom(std::min(size.height() - 1, out.bottom()));
|
||
if (out.width() <= 0 || out.height() <= 0) {
|
||
return {};
|
||
}
|
||
return out;
|
||
}
|
||
|
||
int computeAverageDepthInPolygon(const QImage& depth8, const QVector<QPointF>& polyWorld) {
|
||
if (depth8.isNull() || depth8.format() != QImage::Format_Grayscale8 || polyWorld.size() < 3) {
|
||
return 0;
|
||
}
|
||
const QPainterPath path = pathFromWorldPolygon(polyWorld);
|
||
if (path.isEmpty()) {
|
||
return 0;
|
||
}
|
||
const QRect bbox = clampRectToImage(path.boundingRect().toAlignedRect(), depth8.size());
|
||
if (bbox.isNull()) {
|
||
return 0;
|
||
}
|
||
|
||
quint64 sum = 0;
|
||
quint64 cnt = 0;
|
||
for (int y = bbox.top(); y <= bbox.bottom(); ++y) {
|
||
const uchar* row = depth8.constScanLine(y);
|
||
for (int x = bbox.left(); x <= bbox.right(); ++x) {
|
||
// 取像素中心点做包含测试
|
||
if (!path.contains(QPointF(x + 0.5, y + 0.5))) {
|
||
continue;
|
||
}
|
||
sum += static_cast<quint64>(row[x]);
|
||
++cnt;
|
||
}
|
||
}
|
||
if (cnt == 0) {
|
||
return 0;
|
||
}
|
||
return static_cast<int>(sum / cnt);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
QImage extractEntityImage(const QImage& bg, const QVector<QPointF>& polyWorld, QPointF& outTopLeftWorld) {
|
||
if (bg.isNull() || polyWorld.size() < 3) {
|
||
outTopLeftWorld = {};
|
||
return {};
|
||
}
|
||
const QPainterPath path = pathFromWorldPolygon(polyWorld);
|
||
if (path.isEmpty()) {
|
||
outTopLeftWorld = {};
|
||
return {};
|
||
}
|
||
const QRect bbox = clampRectToImage(path.boundingRect().toAlignedRect(), bg.size());
|
||
if (bbox.isNull()) {
|
||
outTopLeftWorld = {};
|
||
return {};
|
||
}
|
||
|
||
outTopLeftWorld = bbox.topLeft();
|
||
|
||
QImage out(bbox.size(), QImage::Format_ARGB32_Premultiplied);
|
||
out.fill(Qt::transparent);
|
||
QPainter p(&out);
|
||
p.setRenderHint(QPainter::Antialiasing, true);
|
||
|
||
// 把 path 平移到裁剪图坐标系
|
||
QTransform tr;
|
||
tr.translate(-bbox.left(), -bbox.top());
|
||
const QPainterPath localPath = tr.map(path);
|
||
|
||
p.setClipPath(localPath);
|
||
p.drawImage(QPoint(0, 0), bg, bbox);
|
||
p.end();
|
||
return out;
|
||
}
|
||
|
||
void applyBlackFillToBackground(QImage& bgCutout, const QVector<QPointF>& polyWorld) {
|
||
if (bgCutout.isNull() || polyWorld.size() < 3) {
|
||
return;
|
||
}
|
||
QPainterPath path = pathFromWorldPolygon(polyWorld);
|
||
if (path.isEmpty()) {
|
||
return;
|
||
}
|
||
QPainter p(&bgCutout);
|
||
p.setRenderHint(QPainter::Antialiasing, true);
|
||
p.setPen(Qt::NoPen);
|
||
p.setBrush(QColor(0, 0, 0, 255));
|
||
p.drawPath(path);
|
||
p.end();
|
||
}
|
||
|
||
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 : 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 = 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() : 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 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 = 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() : 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::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 = 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()) {
|
||
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());
|
||
}
|
||
|
||
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 = 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() : 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) {
|
||
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 = 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()
|
||
? 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() : 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()
|
||
? 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() : polygonCentroid(m_dragPolyBase);
|
||
const QPointF origin = !m_entities[hit].polygonWorld.isEmpty() ? 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_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_strokeWorld.size() >= 8) {
|
||
// 闭合:末点连回首点(绘制时 polygon 自动闭合)
|
||
core::Project::Entity ent;
|
||
// id 由上层(workspace/UI)统一分配,避免因过滤/隐藏导致重复
|
||
ent.id.clear();
|
||
ent.cutoutPolygonWorld = m_strokeWorld;
|
||
ent.originWorld = polygonCentroid(ent.cutoutPolygonWorld);
|
||
ent.polygonLocal.clear();
|
||
ent.polygonLocal.reserve(ent.cutoutPolygonWorld.size());
|
||
for (const auto& pt : ent.cutoutPolygonWorld) {
|
||
ent.polygonLocal.push_back(pt - ent.originWorld);
|
||
}
|
||
|
||
// 确保深度加载(用于平均深度)
|
||
if (m_depthDirty) {
|
||
m_depthDirty = false;
|
||
QImage img(m_depthAbsPath);
|
||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||
}
|
||
const QPointF c = polygonCentroid(ent.cutoutPolygonWorld);
|
||
ent.depth = m_depthImage8.isNull() ? 0 : sampleDepthAtPoint(m_depthImage8, c);
|
||
{
|
||
const double ds01 = depthToScale01(ent.depth);
|
||
ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0;
|
||
}
|
||
|
||
// 抠实体:从原背景抠出,背景对应区域填黑
|
||
QImage cutout;
|
||
if (!m_bgImage.isNull()) {
|
||
QPointF topLeft;
|
||
cutout = extractEntityImage(m_bgImage, ent.cutoutPolygonWorld, topLeft);
|
||
ent.imageTopLeftWorld = topLeft;
|
||
}
|
||
emit requestAddEntity(ent, cutout);
|
||
}
|
||
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() : polygonCentroid(ent.polygonWorld);
|
||
emit selectedEntityChanged(true, ent.id, ent.depth, origin);
|
||
}
|
||
}
|
||
|
||
m_dragging = false;
|
||
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;
|
||
}
|
||
QWidget::keyPressEvent(e);
|
||
}
|
||
|