Files
hfut-bishe/client/gui/editor/EditorCanvas.cpp

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