#include "editor/EditorCanvas.h" #include "editor/EntityCutoutUtils.h" #include "core/animation/AnimationSampling.h" #include "core/depth/DepthService.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core/image/ImageDecodeConfig.h" #include "core/image/ImageFileLoader.h" #include "core/large_image/VipsBackend.h" #include "core/library/EntityJson.h" #include "core/library/ToolJson.h" namespace { // 摄像机「输出」参考分辨率:视口框与预览缩放均按此换算,避免随窗口大小改变镜头覆盖的世界范围 constexpr double kCameraRefViewportW = 1600.0; constexpr double kCameraRefViewportH = 900.0; /// 画布自由缩放(世界→屏幕)上下限;与摄像机 viewScale 经 applyCameraViewport 换算后的 eff 对齐 constexpr qreal kViewScaleMin = 0.001; constexpr qreal kViewScaleMax = 400.0; constexpr int kSamCropMargin = 32; constexpr int kMinStrokePointsSam = 4; constexpr int kMinStrokePointsManual = 8; [[nodiscard]] QPointF shapeCentroidWorld(const QVector& polyWorld, const QRectF& rect) { if (!polyWorld.isEmpty()) { return entity_cutout::polygonCentroid(polyWorld); } return rect.center(); } constexpr int kMaxSamPointPrompts = 32; constexpr char kMimeHotspotAnimationJson[] = "application/x-hfut-hotspot-animation+json"; static QImage readImageTolerant(const QString& absPath) { return core::image_file::loadImageLimited(absPath, core::image_decode::kWorkspaceMaxPixels); } /// 预览模式下:实体在屏上 footprint 较小时将贴图缩到约 2× 屏上最大边,降低过采样开销 static QImage entityImageForPresentationDrawLoRes(const QImage& src, qreal screenWpx, qreal screenHpx) { if (src.isNull()) { return src; } const int iw = src.width(); const int ih = src.height(); const qreal sm = std::max(screenWpx, screenHpx); if (!(sm > 1.0)) { return src; } const int cap = std::clamp(static_cast(std::ceil(sm * 2.0)), 32, 4096); if (std::max(iw, ih) <= cap) { return src; } return src.scaled(QSize(cap, cap), Qt::KeepAspectRatio, Qt::SmoothTransformation); } QRectF cameraWorldViewportRect(const core::Project::Camera& cam) { const double s = std::max(1e-9, cam.viewScale); const double halfW = (kCameraRefViewportW * 0.5) / s; const double halfH = (kCameraRefViewportH * 0.5) / s; return QRectF(cam.centerWorld.x() - halfW, cam.centerWorld.y() - halfH, 2.0 * halfW, 2.0 * halfH); } QRectF clampRectTopLeftToBounds(const QRectF& rect, const QRectF& bounds) { if (rect.isNull() || bounds.isNull()) { return rect; } QRectF out = rect; if (out.width() > bounds.width()) { out.setWidth(bounds.width()); } if (out.height() > bounds.height()) { out.setHeight(bounds.height()); } QPointF tl = out.topLeft(); if (tl.x() < bounds.left()) tl.setX(bounds.left()); if (tl.y() < bounds.top()) tl.setY(bounds.top()); if (tl.x() + out.width() > bounds.right()) tl.setX(bounds.right() - out.width()); if (tl.y() + out.height() > bounds.bottom()) tl.setY(bounds.bottom() - out.height()); out.moveTopLeft(tl); return out; } QVector snapStrokeToEdgesWorld(const QVector& strokeWorld, const QImage& bgGray, const QPointF& originWorld, const QSize& extentWorld, double searchRadiusWorld) { if (strokeWorld.size() < 3 || bgGray.isNull() || extentWorld.width() < 1 || extentWorld.height() < 1) { return strokeWorld; } const int w = bgGray.width(); const int h = bgGray.height(); const double sx = static_cast(w) / static_cast(extentWorld.width()); const double sy = static_cast(h) / static_cast(extentWorld.height()); const int rPix = std::max(1, static_cast(std::ceil(searchRadiusWorld * std::min(sx, sy)))); const double invSx = 1.0 / sx; const double invSy = 1.0 / sy; auto at = [&](int x, int y) -> int { x = std::clamp(x, 0, w - 1); y = std::clamp(y, 0, h - 1); return static_cast(static_cast(bgGray.constScanLine(y))[x]); }; auto gradMag = [&](int x, int y) -> int { const int gx = -at(x - 1, y - 1) + at(x + 1, y - 1) + -2 * at(x - 1, y) + 2 * at(x + 1, y) + -at(x - 1, y + 1) + at(x + 1, y + 1); const int gy = -at(x - 1, y - 1) - 2 * at(x, y - 1) - at(x + 1, y - 1) + at(x - 1, y + 1) + 2 * at(x, y + 1) + at(x + 1, y + 1); return std::abs(gx) + std::abs(gy); }; QVector out; out.reserve(strokeWorld.size()); for (const QPointF& p : strokeWorld) { const double lx = (p.x() - originWorld.x()) * sx; const double ly = (p.y() - originWorld.y()) * sy; const int cx = static_cast(std::round(lx)); const int cy = static_cast(std::round(ly)); int bestX = cx; int bestY = cy; int bestG = -1; for (int dy = -rPix; dy <= rPix; ++dy) { for (int dx = -rPix; dx <= rPix; ++dx) { const int x = cx + dx; const int y = cy + dy; if (x < 0 || y < 0 || x >= w || y >= h) { continue; } const int g = gradMag(x, y); if (g > bestG) { bestG = g; bestX = x; bestY = y; } } } out.push_back(QPointF(originWorld.x() + (static_cast(bestX) + 0.5) * invSx, originWorld.y() + (static_cast(bestY) + 0.5) * invSy)); } return out; } bool buildSamSegmentPayloadFromStrokeMapped(const QVector& strokeWorld, const QImage& cropAny, const QRect& cropWorldRect, QByteArray& outCropPng, QByteArray& outOverlayPng, QPointF& outCropTopLeftWorld, QJsonArray& outPointCoords, QJsonArray& outPointLabels, QJsonArray& outBoxXyxy) { if (strokeWorld.size() < kMinStrokePointsSam || cropAny.isNull() || cropWorldRect.width() < 1 || cropWorldRect.height() < 1) { return false; } outCropPng.clear(); outOverlayPng.clear(); outPointCoords = QJsonArray{}; outPointLabels = QJsonArray{}; outBoxXyxy = QJsonArray{}; const QImage cropRgb = cropAny.convertToFormat(QImage::Format_RGB888); if (cropRgb.isNull()) { return false; } const QPointF origin = cropWorldRect.topLeft(); outCropTopLeftWorld = origin; const int cw = cropRgb.width(); const int ch = cropRgb.height(); const double sx = static_cast(cw) / static_cast(cropWorldRect.width()); const double sy = static_cast(ch) / static_cast(cropWorldRect.height()); QBuffer bufCrop(&outCropPng); if (!bufCrop.open(QIODevice::WriteOnly) || !cropRgb.save(&bufCrop, "PNG")) { outCropPng.clear(); return false; } bufCrop.close(); QImage overlay(cw, ch, QImage::Format_ARGB32_Premultiplied); overlay.fill(Qt::transparent); { QPainter pop(&overlay); pop.setRenderHint(QPainter::Antialiasing, true); QPen pen(QColor(255, 60, 60, 240)); pen.setWidthF(4.0); pen.setCapStyle(Qt::RoundCap); pen.setJoinStyle(Qt::RoundJoin); pop.setPen(pen); QPolygonF local; local.reserve(strokeWorld.size()); for (const QPointF& w : strokeWorld) { local.append(QPointF((w.x() - origin.x()) * sx, (w.y() - origin.y()) * sy)); } pop.drawPolyline(local); } QBuffer bufOv(&outOverlayPng); if (!bufOv.open(QIODevice::WriteOnly) || !overlay.save(&bufOv, "PNG")) { outOverlayPng.clear(); return false; } bufOv.close(); auto clampD = [](double v, double lo, double hi) { return std::clamp(v, lo, hi); }; const QPointF centerWorld = QPolygonF(strokeWorld).boundingRect().center(); const QPointF centerLocal((centerWorld.x() - origin.x()) * sx, (centerWorld.y() - origin.y()) * sy); const double fgx = clampD(centerLocal.x(), 0.0, static_cast(cw - 1)); const double fgy = clampD(centerLocal.y(), 0.0, static_cast(ch - 1)); outPointCoords.append(QJsonArray{fgx, fgy}); outPointLabels.append(1); const int n = static_cast(strokeWorld.size()); const int maxBg = std::max(0, kMaxSamPointPrompts - 1); if (n >= 2 && maxBg > 0) { const int step = std::max(1, (n + maxBg - 1) / maxBg); for (int i = 0; i < n; i += step) { const QPointF L((strokeWorld[i].x() - origin.x()) * sx, (strokeWorld[i].y() - origin.y()) * sy); const double bx = clampD(L.x(), 0.0, static_cast(cw - 1)); const double by = clampD(L.y(), 0.0, static_cast(ch - 1)); outPointCoords.append(QJsonArray{bx, by}); outPointLabels.append(0); } } const QRectF tight = QPolygonF(strokeWorld).boundingRect(); double x1 = clampD((tight.left() - origin.x()) * sx, 0.0, static_cast(cw - 1)); double y1 = clampD((tight.top() - origin.y()) * sy, 0.0, static_cast(ch - 1)); double x2 = clampD((tight.right() - origin.x()) * sx, 0.0, static_cast(cw - 1)); double y2 = clampD((tight.bottom() - origin.y()) * sy, 0.0, static_cast(ch - 1)); if (x2 <= x1) { x2 = std::min(static_cast(cw - 1), x1 + 1.0); } if (y2 <= y1) { y2 = std::min(static_cast(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(std::floor(worldPos.x())); const int yi = static_cast(std::floor(worldPos.y())); if (xi < 0 || yi < 0 || xi >= depth8.width() || yi >= depth8.height()) { return 0; } return static_cast(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(d) / 255.0; } // depth01 0..1 -> 原始距离乘子 0.5..1.5;calibMult>0 时除以创建时记录的基准,使「原位置」为 1.0 double distanceScaleFromDepth01(double depth01, double calibMult) { const double d = std::clamp(depth01, 0.0, 1.0); const double raw = 0.5 + d * 1.0; if (calibMult > 0.0) { return raw / std::max(calibMult, 1e-6); } return raw; } struct GizmoHit { EditorCanvas::DragMode mode = EditorCanvas::DragMode::None; }; GizmoHit hitTestGizmo(const QPointF& mouseView, const QPointF& originView) { // 以 view 像素为单位的手柄大小(不随缩放变化) const qreal len = 56.0; const qreal halfThickness = 6.0; const QRectF xHandle(QPointF(originView.x(), originView.y() - halfThickness), QSizeF(len, halfThickness * 2.0)); const QRectF yHandle(QPointF(originView.x() - halfThickness, originView.y()), QSizeF(halfThickness * 2.0, len)); if (xHandle.contains(mouseView)) { return {EditorCanvas::DragMode::AxisX}; } if (yHandle.contains(mouseView)) { return {EditorCanvas::DragMode::AxisY}; } return {}; } struct BubbleLayoutWorld { QPainterPath path; QRectF bodyRect; }; // originWorld = 朝下三角形尖端;滑块改变主体水平位置,使「平直底边」上 t01 对应点始终在尖端正上方(三角竖直、与主体一体平移) static BubbleLayoutWorld bubbleLayoutWorld(const core::Project::Tool& tool) { const QPointF tip = tool.originWorld; const qreal w = 220.0; const qreal h = 110.0; const qreal rx = 16.0; const qreal arrowH = 22.0; const double t01 = std::clamp(tool.bubblePointerT01, 0.0, 1.0); const qreal spanFlat = std::max(w - 2.0 * rx, 1.0); const qreal bodyLeft = tip.x() - rx - static_cast(t01) * spanFlat; const QRectF body(bodyLeft, tip.y() - (h + arrowH), w, h); const qreal halfTri = 14.0; const qreal baseCx = tip.x(); QPainterPath path; path.addRoundedRect(body, rx, rx); QPolygonF tri; tri << QPointF(baseCx - halfTri, body.bottom()) << QPointF(baseCx + halfTri, body.bottom()) << QPointF(tip.x(), tip.y()); path.addPolygon(tri); return BubbleLayoutWorld{path, body}; } static QPainterPath bubblePathWorld(const core::Project::Tool& tool) { return bubbleLayoutWorld(tool).path; } } // namespace EditorCanvas::EditorCanvas(QWidget* parent) : QWidget(parent) { setAutoFillBackground(false); setMinimumSize(480, 320); setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); setAcceptDrops(true); m_previewEmitTimer.start(); m_presZoomTimer = new QTimer(this); m_presZoomTimer->setInterval(16); connect(m_presZoomTimer, &QTimer::timeout, this, &EditorCanvas::tickPresentationZoomAnimation); m_presHoverTimer = new QTimer(this); m_presHoverTimer->setInterval(40); connect(m_presHoverTimer, &QTimer::timeout, this, &EditorCanvas::tickPresentationHoverAnimation); updateCursor(); } void EditorCanvas::dragEnterEvent(QDragEnterEvent* e) { if (!e || !e->mimeData()) { return; } if (!m_presentationPreviewMode && m_presentationHotspotEditorActive && e->mimeData()->hasFormat(QString::fromUtf8(kMimeHotspotAnimationJson))) { e->acceptProposedAction(); return; } if (e->mimeData()->hasFormat(QStringLiteral("application/x-hfut-resource+json"))) { e->acceptProposedAction(); return; } QWidget::dragEnterEvent(e); } void EditorCanvas::dragMoveEvent(QDragMoveEvent* e) { if (!e || !e->mimeData()) { return; } if (!m_presentationPreviewMode && m_presentationHotspotEditorActive && e->mimeData()->hasFormat(QString::fromUtf8(kMimeHotspotAnimationJson))) { e->acceptProposedAction(); return; } if (e->mimeData()->hasFormat(QStringLiteral("application/x-hfut-resource+json"))) { e->acceptProposedAction(); return; } QWidget::dragMoveEvent(e); } void EditorCanvas::dropEvent(QDropEvent* e) { if (!e || !e->mimeData()) { QWidget::dropEvent(e); return; } if (!m_presentationPreviewMode && m_presentationHotspotEditorActive && e->mimeData()->hasFormat(QString::fromUtf8(kMimeHotspotAnimationJson))) { const QByteArray bytes = e->mimeData()->data(QString::fromUtf8(kMimeHotspotAnimationJson)); const auto doc = QJsonDocument::fromJson(bytes); if (!doc.isObject()) { e->ignore(); return; } const QString animationId = doc.object().value(QStringLiteral("animationId")).toString(); if (animationId.isEmpty()) { e->ignore(); return; } emit requestAddPresentationHotspotForAnimation(viewToWorld(e->position()), animationId); e->acceptProposedAction(); return; } if (!e->mimeData()->hasFormat(QStringLiteral("application/x-hfut-resource+json"))) { QWidget::dropEvent(e); return; } const QByteArray bytes = e->mimeData()->data(QStringLiteral("application/x-hfut-resource+json")); const auto doc = QJsonDocument::fromJson(bytes); if (!doc.isObject()) { e->ignore(); return; } const QJsonObject root = doc.object(); const QString kind = root.value(QStringLiteral("kind")).toString(QStringLiteral("entity")); const QPointF dropWorld = viewToWorld(e->position()); if (kind == QStringLiteral("tool")) { if (!root.value(QStringLiteral("tool")).isObject()) { e->ignore(); return; } core::Project::Tool t; if (!core::library::toolFromJson(root.value(QStringLiteral("tool")).toObject(), t)) { e->ignore(); return; } // 让主窗口分配 id,避免冲突 t.id.clear(); t.parentId.clear(); t.parentOffsetWorld = QPointF(); t.originWorld = dropWorld; emit requestAddTool(t); e->acceptProposedAction(); return; } if (!root.value(QStringLiteral("entity")).isObject()) { e->ignore(); return; } core::Project::Entity ent; if (!core::library::entityFromJson(root.value(QStringLiteral("entity")).toObject(), ent)) { e->ignore(); return; } // 让主窗口分配 id,避免资源 id 与工程内冲突 ent.id.clear(); ent.imagePath.clear(); ent.entityPayloadPath.clear(); ent.originWorld = dropWorld; // 默认把贴图左上角放到 originWorld + offset QPointF imageOffset(-128, -128); if (root.value(QStringLiteral("imageOffsetFromOrigin")).isArray()) { const QJsonArray a = root.value(QStringLiteral("imageOffsetFromOrigin")).toArray(); if (a.size() >= 2) { imageOffset = QPointF(a.at(0).toDouble(), a.at(1).toDouble()); } } ent.imageTopLeftWorld = ent.originWorld + imageOffset; // 生成占位贴图(未来可替换为真实资源图片) QSize imgSize(256, 256); if (root.value(QStringLiteral("imageSize")).isArray()) { const QJsonArray a = root.value(QStringLiteral("imageSize")).toArray(); if (a.size() >= 2) { imgSize = QSize(a.at(0).toInt(256), a.at(1).toInt(256)); } } QColor accent(80, 160, 255); if (root.value(QStringLiteral("accent")).isArray()) { const QJsonArray a = root.value(QStringLiteral("accent")).toArray(); if (a.size() >= 4) { accent = QColor(a.at(0).toInt(80), a.at(1).toInt(160), a.at(2).toInt(255), a.at(3).toInt(255)); } } if (!imgSize.isValid()) { imgSize = QSize(256, 256); } QImage img(imgSize, QImage::Format_ARGB32_Premultiplied); img.fill(Qt::transparent); { QPainter p(&img); p.setRenderHint(QPainter::Antialiasing, true); QRectF rr(QPointF(0, 0), QSizeF(imgSize)); rr = rr.adjusted(6, 6, -6, -6); p.setPen(QPen(QColor(0, 0, 0, 60), 2)); p.setBrush(QBrush(accent)); p.drawRoundedRect(rr, 18, 18); } emit requestAddEntity(ent, img); e->acceptProposedAction(); } void EditorCanvas::setPreviewCameraViewLocked(bool on) { m_previewCameraViewLocked = on; } void EditorCanvas::applyCameraViewport(const QPointF& centerWorld, double viewScale) { // 与 cameraWorldViewportRect 一致:viewScale 表示在 1600×900 参考视口下的像素/世界比; // 实际画布用 min(宽/1600, 高/900) 将参考视口适配进当前控件,使可见世界宽高恒为 1600/s × 900/s。 const double pixelRatio = std::min(static_cast(std::max(1, width())) / kCameraRefViewportW, static_cast(std::max(1, height())) / kCameraRefViewportH); const double eff = std::max(1e-9, static_cast(viewScale)) * pixelRatio; m_scale = std::clamp(static_cast(eff), kViewScaleMin, kViewScaleMax); m_pan = QPointF(width() / 2.0, height() / 2.0) - QPointF(centerWorld.x() * m_scale, centerWorld.y() * m_scale); invalidateViewportLod(); update(); } QPointF EditorCanvas::viewCenterWorld() const { return viewToWorld(QPointF(width() / 2.0, height() / 2.0)); } void EditorCanvas::setPresentationHotspots(const QVector& hotspots) { m_presentationHotspots = hotspots; bool found = false; for (const auto& h : m_presentationHotspots) { if (h.id == m_selectedPresentationHotspotId) { found = true; break; } } if (!found) { m_selectedPresentationHotspotId.clear(); } update(); } void EditorCanvas::setPresentationHotspotEditorActive(bool on) { if (m_presentationHotspotEditorActive == on) { return; } m_presentationHotspotEditorActive = on; if (!on) { m_draggingHotspotIndex = -1; m_dragging = false; } updateCursor(); update(); } void EditorCanvas::setPresentationHotspotPlaybackTargetEnabled(bool on) { if (m_presentationHotspotPlaybackTargetEnabled == on) { return; } m_presentationHotspotPlaybackTargetEnabled = on; if (!on) { if (m_presHoverTimer) { m_presHoverTimer->stop(); } m_presHoverEntityIndex = -1; m_presFocusedEntityIndex = -1; m_presHoverPhase = 0.0; m_presZoomAnimT = 0.0; m_presZoomFinishingRestore = false; m_presBgPanSession = false; m_presBgDragDist = 0.0; emit presentationInteractionDismissed(); } updateCursor(); update(); } void EditorCanvas::setSelectedPresentationHotspotId(const QString& id) { m_selectedPresentationHotspotId = id; update(); } void EditorCanvas::clearPresentationHotspotSelection() { m_selectedPresentationHotspotId.clear(); m_draggingHotspotIndex = -1; update(); } void EditorCanvas::setPresentationPreviewMode(bool on) { if (m_presentationPreviewMode == on) { return; } m_presentationPreviewMode = on; if (m_presZoomTimer) { m_presZoomTimer->stop(); } if (m_presHoverTimer) { m_presHoverTimer->stop(); } m_presHoverEntityIndex = -1; m_presFocusedEntityIndex = -1; m_presHoverPhase = 0.0; m_presZoomAnimT = 0.0; m_presZoomFinishingRestore = false; m_presBgPanSession = false; m_presBgDragDist = 0.0; cancelBlackholeCopyResolve(); m_draggingHotspotIndex = -1; if (on) { m_tool = Tool::Move; m_selectedEntity = -1; m_selectedBlackholeEntityId.clear(); m_draggingEntity = false; m_drawingEntity = false; m_dragMode = DragMode::None; emit selectedEntityChanged(false, QString(), 0, QPointF()); } m_bgCutoutDirty = true; updateCursor(); update(); } void EditorCanvas::setEntities(const QVector& entities, const QVector& opacities01, const QString& projectDirAbs) { const QString prevSelectedId = (m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) ? m_entities[m_selectedEntity].id : QString(); m_blackholeOverlayImageCache.clear(); m_projectDirAbs = projectDirAbs; m_entities.clear(); m_entities.reserve(entities.size()); // 需要用深度图来自动计算 z 与缩放(逐帧) if (!m_depthAbsPath.isEmpty()) { if (m_depthDirty) { m_depthDirty = false; m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } } const qsizetype nEnt = entities.size(); for (qsizetype iEnt = 0; iEnt < nEnt; ++iEnt) { const auto& e = entities[iEnt]; Entity v; v.id = e.id; v.opacity = (iEnt < opacities01.size()) ? std::clamp(opacities01[iEnt], 0.0, 1.0) : 1.0; // 注意:MainWindow 传入的是“按当前帧求值后的实体”(包含父子跟随与曲线采样)。 // 这里必须直接使用 e.originWorld,不能再对 locationKeys 做二次采样,否则父子实体会在刷新时复位/跳变。 const QPointF originWorld = e.originWorld; v.animatedOriginWorld = originWorld; v.cutoutPolygonWorld = e.cutoutPolygonWorld; v.blackholeVisible = e.blackholeVisible; v.blackholeOverlayPath = e.blackholeOverlayPath; v.blackholeOverlayRect = e.blackholeOverlayRect; v.distanceScaleCalibMult = e.distanceScaleCalibMult; v.ignoreDistanceScale = e.ignoreDistanceScale; v.priority = e.priority; const double userScaleAnimated = core::sampleUserScale(e.userScaleKeys, m_currentFrame, e.userScale, core::KeyInterpolation::Linear); v.userScale = std::max(1e-6, userScaleAnimated); // 逐帧自动算 z:采样点必须对“重锚枢轴(相对位置)”不敏感,否则会导致深度→缩放变化, // 表现为“中心没改但看起来位置/形心漂移”。 // // 做法:先用工程给出的 depth 计算一次 scale0,构建 polygonWorld 得到稳定的形心,再用该形心采样深度, // 然后用采样到的 z 计算最终 scale。 const double ds01Base = depthToScale01(e.depth); const double distScale0 = e.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ds01Base, e.distanceScaleCalibMult); const double scale0 = distScale0 * v.userScale; QVector poly0; poly0.reserve(e.polygonLocal.size()); for (const auto& lp : e.polygonLocal) { poly0.push_back(originWorld + lp * scale0); } const QPointF c0 = poly0.isEmpty() ? originWorld : entity_cutout::polygonCentroid(poly0); const int z = (!m_depthImage8.isNull()) ? sampleDepthAtPoint(m_depthImage8, c0) : e.depth; v.depth = z; const double ds01 = depthToScale01(z); v.animatedDepthScale01 = ds01; const double distScale = e.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ds01, e.distanceScaleCalibMult); const double scale = distScale * v.userScale; v.visualScale = scale; v.polygonWorld.clear(); v.polygonWorld.reserve(e.polygonLocal.size()); for (const auto& lp : e.polygonLocal) { v.polygonWorld.push_back(originWorld + lp * scale); } // 贴图按 origin 缩放 v.imageTopLeft = originWorld + (e.imageTopLeftWorld - e.originWorld) * scale; v.pathWorld = entity_cutout::pathFromWorldPolygon(v.polygonWorld); v.rect = v.pathWorld.boundingRect(); v.color = QColor(255, 120, 0, 70); if (!e.runtimeImagePng.isEmpty()) { QImage img; img.loadFromData(e.runtimeImagePng, "PNG"); if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) { img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); } v.image = img; } else { const QString imgRel = e.imagePath; if (imgRel.startsWith(QStringLiteral("pngb64:"))) { const QByteArray b64 = imgRel.mid(QStringLiteral("pngb64:").size()).toLatin1(); const QByteArray raw = QByteArray::fromBase64(b64); QImage img; img.loadFromData(raw, "PNG"); if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) { img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); } v.image = img; } else if (!imgRel.isEmpty() && !projectDirAbs.isEmpty()) { const QString abs = QDir(projectDirAbs).filePath(imgRel); if (QFileInfo::exists(abs)) { QImage img(abs); if (!img.isNull() && img.format() != QImage::Format_ARGB32_Premultiplied) { img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); } v.image = img; } } } m_entities.push_back(v); } // 绘制/命中顺序: // - priority 小(低)先画,大(高)后画,高优先级盖住低优先级 // - priority 相同:深度小(远)先画,大(近)后画,近处盖住远处(等价于按距离缩放决定上下层) std::stable_sort(m_entities.begin(), m_entities.end(), [](const Entity& a, const Entity& b) { if (a.priority != b.priority) { return a.priority < b.priority; } if (a.depth != b.depth) { return a.depth < b.depth; } return a.id < b.id; }); m_selectedEntity = -1; if (!prevSelectedId.isEmpty()) { for (int i = 0; i < m_entities.size(); ++i) { if (m_entities[i].id == prevSelectedId) { m_selectedEntity = i; break; } } } if (m_selectedEntity >= 0) { const auto& ent = m_entities[m_selectedEntity]; const QPointF origin = ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld); emit selectedEntityChanged(true, ent.id, ent.depth, origin); } else if (!prevSelectedId.isEmpty()) { emit selectedEntityChanged(false, QString(), 0, QPointF()); } if (!m_selectedBlackholeEntityId.isEmpty()) { bool exists = false; for (const auto& ent : m_entities) { if (ent.id == m_selectedBlackholeEntityId && !ent.cutoutPolygonWorld.isEmpty()) { exists = true; break; } } if (!exists) { m_selectedBlackholeEntityId.clear(); } } if (m_blackholeCopyResolveActive) { bool exists = false; for (const auto& ent : m_entities) { if (ent.id == m_blackholeCopyEntityId && !ent.cutoutPolygonWorld.isEmpty()) { exists = true; break; } } if (!exists) { cancelBlackholeCopyResolve(); } } m_bgCutoutDirty = true; update(); } void EditorCanvas::setTools(const QVector& tools, const QVector& opacities01) { m_tools.clear(); // 需要用深度图为工具采样一个“近远”用于同优先级排序(与实体一致) if (!m_depthAbsPath.isEmpty()) { if (m_depthDirty) { m_depthDirty = false; m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } } const qsizetype n = tools.size(); m_tools.reserve(n); for (qsizetype i = 0; i < n; ++i) { ToolView tv; tv.tool = tools[i]; tv.opacity = (i < opacities01.size()) ? std::clamp(opacities01[i], 0.0, 1.0) : 1.0; tv.depthZ = (!m_depthImage8.isNull()) ? sampleDepthAtPoint(m_depthImage8, tv.tool.originWorld) : 0; m_tools.push_back(tv); } // 轨道变更:若当前选中的工具已不存在,则清除 if (m_selectedTool >= 0) { const QString selId = (m_selectedTool >= 0 && m_selectedTool < m_tools.size()) ? m_tools[m_selectedTool].tool.id : QString(); if (!selId.isEmpty()) { int hit = -1; for (int i = 0; i < m_tools.size(); ++i) { if (m_tools[i].tool.id == selId) { hit = i; break; } } m_selectedTool = hit; } else { m_selectedTool = -1; } } update(); } void EditorCanvas::setCameraOverlays(const QVector& cameras, const QString& selectedId, const QSet& tempHiddenCameraIds) { const bool hadSelectedCamera = !m_selectedCameraId.isEmpty() || m_selectedCameraIndex >= 0 || m_draggingCamera; m_cameraOverlays = cameras; m_tempHiddenCameraIds = tempHiddenCameraIds; m_selectedCameraId = selectedId; m_selectedCameraIndex = -1; if (!selectedId.isEmpty()) { for (int i = 0; i < m_cameraOverlays.size(); ++i) { if (m_cameraOverlays[i].id == selectedId) { m_selectedCameraIndex = i; break; } } } if (m_selectedCameraIndex < 0) { m_selectedCameraId.clear(); m_draggingCamera = false; if (hadSelectedCamera) { emit selectedCameraChanged(false, QString(), QPointF(), 1.0); } } update(); } void EditorCanvas::selectCameraById(const QString& id) { if (id.isEmpty()) { clearCameraSelection(); return; } clearEntitySelection(); m_selectedTool = -1; m_draggingTool = false; emit selectedToolChanged(false, QString(), QPointF()); clearBlackholeSelection(); for (int i = 0; i < m_cameraOverlays.size(); ++i) { if (m_cameraOverlays[i].id == id) { m_selectedCameraIndex = i; m_selectedCameraId = id; const auto& c = m_cameraOverlays[i]; emit selectedCameraChanged(true, id, c.centerWorld, c.viewScale); update(); return; } } clearCameraSelection(); } void EditorCanvas::clearCameraSelection() { if (m_selectedCameraId.isEmpty() && m_selectedCameraIndex < 0 && !m_draggingCamera) { return; } m_selectedCameraId.clear(); m_selectedCameraIndex = -1; m_draggingCamera = false; emit selectedCameraChanged(false, QString(), QPointF(), 1.0); update(); } void EditorCanvas::setTempHiddenIds(const QSet& entityIds, const QSet& toolIds) { m_tempHiddenEntityIds = entityIds; m_tempHiddenToolIds = toolIds; update(); } void EditorCanvas::setTempHiddenCameraIds(const QSet& cameraIds) { m_tempHiddenCameraIds = cameraIds; update(); } void EditorCanvas::setCurrentFrame(int frame) { if (m_currentFrame == frame) { return; } m_currentFrame = std::max(0, frame); // 仅切帧时,实体由 MainWindow 刷新时回灌;这里也触发重绘用于坐标轴/叠加 update(); } QPointF EditorCanvas::selectedAnimatedOriginWorld() const { return selectedEntityPivotWorld(); } QPointF EditorCanvas::selectedEntityPivotWorld() const { if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) { return {}; } return m_entities[m_selectedEntity].animatedOriginWorld; } double EditorCanvas::selectedDepthScale01() const { if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) { return 0.5; } return m_entities[m_selectedEntity].animatedDepthScale01; } QPointF EditorCanvas::selectedEntityCentroidWorld() const { if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) { return {}; } // 拖动预览:polygonWorld 不再逐点更新,质心应使用预览值,否则属性面板看起来“不跟随” if (!m_presentationPreviewMode && m_draggingEntity && m_dragPreviewActive && m_selectedEntity >= 0) { return m_dragCentroidBase + m_dragDelta; } const auto& ent = m_entities[m_selectedEntity]; if (!ent.polygonWorld.isEmpty()) { return entity_cutout::polygonCentroid(ent.polygonWorld); } return ent.rect.center(); } double EditorCanvas::selectedDistanceScaleMultiplier() const { if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) { return 1.0; } const auto& ent = m_entities[m_selectedEntity]; return ent.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ent.animatedDepthScale01, ent.distanceScaleCalibMult); } double EditorCanvas::selectedUserScale() const { if (m_selectedEntity < 0 || m_selectedEntity >= m_entities.size()) { return 1.0; } return m_entities[m_selectedEntity].userScale; } double EditorCanvas::selectedCombinedScale() const { return selectedDistanceScaleMultiplier() * selectedUserScale(); } void EditorCanvas::tickPresentationHoverAnimation() { if (!m_presentationPreviewMode) { return; } m_presHoverPhase += 0.35; if (m_presHoverPhase > 6.28318530718) { m_presHoverPhase -= 6.28318530718; } update(); } void EditorCanvas::tickPresentationZoomAnimation() { m_presZoomAnimT += 0.16; qreal u = std::min(1.0, static_cast(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(width()) / (rw * 1.28), static_cast(height()) / (rh * 1.28)); s = std::clamp(s, 0.12, 14.0); *outScale = s; *outPan = QPointF(width() / 2.0, height() / 2.0) - c * s; } void EditorCanvas::beginPresentationZoomTowardEntity(int entityIndex) { if (entityIndex < 0 || entityIndex >= m_entities.size()) { return; } if (m_presFocusedEntityIndex < 0) { m_presRestorePan = m_pan; m_presRestoreScale = m_scale; } m_presFocusedEntityIndex = entityIndex; m_presZoomFromPan = m_pan; m_presZoomFromScale = m_scale; presentationComputeZoomTarget(entityIndex, &m_presZoomToPan, &m_presZoomToScale); m_presZoomAnimT = 0.0; m_presZoomFinishingRestore = false; m_presZoomTimer->start(); } void EditorCanvas::beginPresentationZoomRestore() { m_presZoomFromPan = m_pan; m_presZoomFromScale = m_scale; m_presZoomToPan = m_presRestorePan; m_presZoomToScale = m_presRestoreScale; m_presZoomAnimT = 0.0; m_presZoomFinishingRestore = true; m_presZoomTimer->start(); } void EditorCanvas::clearPresentationEntityFocus() { emit presentationInteractionDismissed(); if (m_presZoomFinishingRestore) { return; } if (m_presFocusedEntityIndex >= 0) { beginPresentationZoomRestore(); } } void EditorCanvas::clearEntitySelection() { if (m_selectedEntity < 0) { return; } m_selectedEntity = -1; emit selectedEntityChanged(false, QString(), 0, QPointF()); update(); } void EditorCanvas::selectEntityById(const QString& id) { if (id.isEmpty()) { clearEntitySelection(); clearCameraSelection(); return; } clearCameraSelection(); for (int i = 0; i < m_entities.size(); ++i) { if (m_entities[i].id != id) { continue; } if (m_selectedEntity == i) { update(); return; } m_selectedEntity = i; const auto& ent = m_entities[i]; const QPointF origin = ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld); emit selectedEntityChanged(true, ent.id, ent.depth, origin); update(); return; } clearEntitySelection(); } void EditorCanvas::selectBlackholeByEntityId(const QString& entityId) { if (entityId.isEmpty()) { clearBlackholeSelection(); return; } for (const auto& ent : m_entities) { if (ent.id == entityId && !ent.cutoutPolygonWorld.isEmpty()) { if (m_selectedBlackholeEntityId == entityId) { update(); return; } m_selectedBlackholeEntityId = entityId; update(); return; } } clearBlackholeSelection(); } void EditorCanvas::clearBlackholeSelection() { if (m_blackholeCopyResolveActive) { cancelBlackholeCopyResolve(); } if (m_selectedBlackholeEntityId.isEmpty()) { return; } m_selectedBlackholeEntityId.clear(); update(); } bool EditorCanvas::startBlackholeCopyResolve(const QString& entityId) { if (entityId.isEmpty()) { return false; } const Entity* hit = nullptr; for (const auto& ent : m_entities) { if (ent.id == entityId && !ent.cutoutPolygonWorld.isEmpty()) { hit = &ent; break; } } if (!hit) { return false; } ensurePixmapLoaded(); const QRectF bg = worldRectOfBackground(); if (bg.isNull()) { return false; } QRectF holeRect = entity_cutout::pathFromWorldPolygon(hit->cutoutPolygonWorld).boundingRect(); if (holeRect.isNull()) { return false; } if (holeRect.width() < 1.0 || holeRect.height() < 1.0) { return false; } holeRect = holeRect.intersected(bg); if (holeRect.isNull()) { return false; } QRectF srcRect(holeRect); const qreal shift = std::max(24.0, holeRect.width() * 0.6); srcRect.translate(shift, 0.0); srcRect = clampRectTopLeftToBounds(srcRect, bg); m_bhCopyPatchValid = false; m_bhCopyPatch = QImage(); if (m_useViewportLod && !m_bgAbsPath.isEmpty()) { QRect uni = holeRect.united(srcRect).toAlignedRect().intersected(bg.toAlignedRect()); if (uni.isValid() && uni.width() > 0 && uni.height() > 0 && core::image_file::loadRegionToQImage(m_bgAbsPath, uni, 8192, 8192, &m_bhCopyPatch) && !m_bhCopyPatch.isNull()) { if (m_bhCopyPatch.format() != QImage::Format_ARGB32_Premultiplied) { m_bhCopyPatch = m_bhCopyPatch.convertToFormat(QImage::Format_ARGB32_Premultiplied); } m_bhCopyPatchOrigin = uni.topLeft(); m_bhCopyPatchValid = true; } } if (!m_bhCopyPatchValid && m_bgImageDirty) { m_bgImageDirty = false; m_bgImage = readImageTolerant(m_bgAbsPath); if (m_bgImage.format() != QImage::Format_ARGB32_Premultiplied && !m_bgImage.isNull()) { m_bgImage = m_bgImage.convertToFormat(QImage::Format_ARGB32_Premultiplied); } } m_selectedBlackholeEntityId = entityId; m_blackholeCopyResolveActive = true; m_blackholeCopyEntityId = entityId; m_blackholeCopyHoleRect = holeRect; m_blackholeCopySourceRect = srcRect; m_blackholeCopyDragging = false; m_blackholeCopyDragOffset = QPointF(); updateCursor(); update(); return true; } void EditorCanvas::cancelBlackholeCopyResolve() { if (!m_blackholeCopyResolveActive) { return; } m_bhCopyPatchValid = false; m_bhCopyPatch = QImage(); m_blackholeCopyResolveActive = false; m_blackholeCopyEntityId.clear(); m_blackholeCopyHoleRect = QRectF(); m_blackholeCopySourceRect = QRectF(); m_blackholeCopyDragging = false; m_blackholeCopyDragOffset = QPointF(); updateCursor(); update(); } void EditorCanvas::notifyBackgroundContentChanged() { // 背景路径不变时,setBackgroundImagePath 不会触发刷新;这里显式让 pixmap/image 缓存失效并重载。 invalidatePixmap(); m_bgImageDirty = true; m_bgCutoutDirty = true; update(); } void EditorCanvas::notifyBlackholeOverlaysChanged() { m_blackholeOverlayImageCache.clear(); update(); } void EditorCanvas::setBackgroundImagePath(const QString& absolutePath) { if (m_bgAbsPath == absolutePath) { return; } cancelBlackholeCopyResolve(); m_bgAbsPath = absolutePath; m_initialBgLoadFinishedEmitted = false; invalidatePixmap(); m_bgImageDirty = true; m_bgCutoutDirty = true; m_bgImage = QImage(); m_bgImageCutout = QImage(); zoomToFit(); update(); tryEmitInitialBackgroundLoadFinished(); } void EditorCanvas::tryEmitInitialBackgroundLoadFinished() { if (m_initialBgLoadFinishedEmitted) { return; } if (m_bgAbsPath.isEmpty()) { m_initialBgLoadFinishedEmitted = true; emit initialBackgroundLoadFinished(); return; } ensurePixmapLoaded(); if (m_useViewportLod) { if (!m_bgViewportImage.isNull()) { m_initialBgLoadFinishedEmitted = true; emit initialBackgroundLoadFinished(); } return; } m_initialBgLoadFinishedEmitted = true; emit initialBackgroundLoadFinished(); } void EditorCanvas::setBackgroundVisible(bool on) { if (m_backgroundVisible == on) { return; } m_backgroundVisible = on; update(); } void EditorCanvas::setDepthMapPath(const QString& absolutePath) { if (m_depthAbsPath == absolutePath) { return; } m_depthAbsPath = absolutePath; m_depthDirty = true; m_depthImage8 = QImage(); update(); } void EditorCanvas::setDepthOverlayEnabled(bool on) { if (m_depthOverlayEnabled == on) { return; } m_depthOverlayEnabled = on; update(); } void EditorCanvas::setTool(Tool tool) { if (m_tool == tool) { return; } m_tool = tool; m_draggingEntity = false; m_drawingEntity = false; m_strokeWorld.clear(); updateCursor(); update(); } void EditorCanvas::setEntityCreateSegmentMode(EntityCreateSegmentMode m) { if (m_entityCreateSegmentMode == m) { return; } m_entityCreateSegmentMode = m; update(); } void EditorCanvas::setPendingEntityPolygonWorld(const QVector& 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(m_scale, 0.001); const qreal r2 = rWorld * rWorld; int best = -1; qreal bestD2 = r2; for (int i = 0; i < m_pendingPolyWorld.size(); ++i) { const QPointF d = m_pendingPolyWorld[i] - worldPos; const qreal d2 = d.x() * d.x() + d.y() * d.y(); if (d2 <= bestD2) { bestD2 = d2; best = i; } } if (best >= 0) { if (outIndex) *outIndex = best; return true; } return false; } bool EditorCanvas::pendingPolygonContains(const QPointF& worldPos) const { if (m_pendingPolyWorld.size() < 3) return false; return entity_cutout::pathFromWorldPolygon(m_pendingPolyWorld).contains(worldPos); } void EditorCanvas::resetView() { m_scale = 1.0; m_pan = QPointF(0, 0); invalidateViewportLod(); update(); } void EditorCanvas::zoomToFit() { ensurePixmapLoaded(); if ((!m_bgLogicalSize.isValid() && m_bgPixmap.isNull()) || width() <= 1 || height() <= 1) { resetView(); return; } const QSizeF viewSize = size(); const QSizeF imgSize = m_bgLogicalSize.isValid() ? QSizeF(m_bgLogicalSize) : QSizeF(m_bgPixmap.size()); const qreal sx = (viewSize.width() - 24.0) / imgSize.width(); const qreal sy = (viewSize.height() - 24.0) / imgSize.height(); const qreal s = std::max(kViewScaleMin, std::min(sx, sy)); m_scale = s; // 让 world(0,0) 的图像左上角居中显示 const QSizeF draw(imgSize.width() * s, imgSize.height() * s); m_pan = QPointF((viewSize.width() - draw.width()) / 2.0, (viewSize.height() - draw.height()) / 2.0); invalidateViewportLod(); update(); } void EditorCanvas::setWorldAxesVisible(bool on) { if (m_worldAxesVisible == on) { return; } m_worldAxesVisible = on; update(); } void EditorCanvas::setAxisLabelsVisible(bool on) { if (m_axisLabelsVisible == on) { return; } m_axisLabelsVisible = on; update(); } void EditorCanvas::setGizmoLabelsVisible(bool on) { if (m_gizmoLabelsVisible == on) { return; } m_gizmoLabelsVisible = on; update(); } void EditorCanvas::setGridVisible(bool on) { if (m_gridVisible == on) { return; } m_gridVisible = on; update(); } void EditorCanvas::setCheckerboardVisible(bool on) { if (m_checkerboardVisible == on) { return; } m_checkerboardVisible = on; update(); } void EditorCanvas::invalidatePixmap() { m_pixmapDirty = true; m_bgPixmap = QPixmap(); m_bgLogicalSize = QSize(); m_useViewportLod = false; m_bgViewportImage = QImage(); m_bgViewportCacheRect = QRect(); m_bgViewportDirty = true; m_vpCachedDensity = 0.0; m_vpPreferRegionDecode = false; ++m_vpToken; } void EditorCanvas::invalidateViewportLod() { m_bgViewportDirty = true; ++m_vpToken; } void EditorCanvas::syncFullBackgroundImageFromDiskIfDirty() { if (m_bgAbsPath.isEmpty() || m_useViewportLod) { return; } if (!m_bgImageDirty) { return; } m_bgImageDirty = false; m_bgImage = readImageTolerant(m_bgAbsPath); if (!m_bgImage.isNull() && m_bgImage.format() != QImage::Format_ARGB32_Premultiplied) { m_bgImage = m_bgImage.convertToFormat(QImage::Format_ARGB32_Premultiplied); } m_bgCutoutDirty = true; } void EditorCanvas::rebuildFullBackgroundCutoutIfDirty() { if (m_useViewportLod || m_bgAbsPath.isEmpty()) { return; } if (!m_bgCutoutDirty) { return; } if (m_bgImage.isNull()) { syncFullBackgroundImageFromDiskIfDirty(); } if (m_bgImage.isNull()) { return; } m_bgCutoutDirty = false; m_bgImageCutout = m_bgImage; for (const auto& ent : m_entities) { if (ent.blackholeVisible && !ent.cutoutPolygonWorld.isEmpty()) { entity_cutout::applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld); } } } QVector> EditorCanvas::visibleBlackholeCutoutPolysWorld() const { QVector> out; for (const auto& ent : m_entities) { if (ent.blackholeVisible && ent.cutoutPolygonWorld.size() >= 3) { out.push_back(ent.cutoutPolygonWorld); } } return out; } void EditorCanvas::applyBlackHolesWorldToCrop(QImage& cropPremul, const QRect& cropWorld) const { entity_cutout::applyBlackHolesToImageRegion(cropPremul, cropWorld, visibleBlackholeCutoutPolysWorld()); } bool EditorCanvas::loadRgbCropForEntitySegmentStroke(const QRectF& polyBrWorld, QRect& outCropWorld, QImage& outCropRgb) { outCropRgb = QImage(); if (m_bgAbsPath.isEmpty()) { return false; } ensurePixmapLoaded(); QRect cropWorld = entity_cutout::clampRectToImage( polyBrWorld.adjusted(-kSamCropMargin, -kSamCropMargin, kSamCropMargin, kSamCropMargin).toAlignedRect(), QSize(backgroundLogicalWidth(), backgroundLogicalHeight())); if (cropWorld.isEmpty()) { return false; } outCropWorld = cropWorld; if (!m_useViewportLod) { syncFullBackgroundImageFromDiskIfDirty(); rebuildFullBackgroundCutoutIfDirty(); if (!m_bgImageCutout.isNull()) { outCropRgb = m_bgImageCutout.copy(cropWorld); } } if (outCropRgb.isNull()) { (void)core::image_file::loadRegionToQImage(m_bgAbsPath, cropWorld, 4096, 4096, &outCropRgb); if (!outCropRgb.isNull() && outCropRgb.format() != QImage::Format_ARGB32_Premultiplied) { outCropRgb = outCropRgb.convertToFormat(QImage::Format_ARGB32_Premultiplied); } } if (outCropRgb.isNull() && !m_bgImage.isNull()) { const QRect cw = entity_cutout::clampRectToImage( polyBrWorld.adjusted(-kSamCropMargin, -kSamCropMargin, kSamCropMargin, kSamCropMargin).toAlignedRect(), m_bgImage.size()); if (!cw.isEmpty()) { outCropRgb = m_bgImage.copy(cw); outCropWorld = cw; } } if (outCropRgb.isNull()) { syncFullBackgroundImageFromDiskIfDirty(); rebuildFullBackgroundCutoutIfDirty(); if (!m_bgImage.isNull()) { const QRect cw2 = entity_cutout::clampRectToImage( polyBrWorld.adjusted(-kSamCropMargin, -kSamCropMargin, kSamCropMargin, kSamCropMargin).toAlignedRect(), m_bgImage.size()); if (!cw2.isEmpty()) { outCropRgb = m_bgImage.copy(cw2); outCropWorld = cw2; } } } if (outCropRgb.isNull()) { return false; } if (outCropRgb.format() != QImage::Format_ARGB32_Premultiplied) { outCropRgb = outCropRgb.convertToFormat(QImage::Format_ARGB32_Premultiplied); } applyBlackHolesWorldToCrop(outCropRgb, outCropWorld); return true; } void EditorCanvas::ensurePixmapLoaded() const { if (!m_pixmapDirty) { return; } m_pixmapDirty = false; m_bgPixmap = QPixmap(); m_bgLogicalSize = QSize(); m_useViewportLod = false; if (m_bgAbsPath.isEmpty()) { return; } if (core::VipsBackend::isAvailable() && core::image_file::probeImagePixelSize(m_bgAbsPath, &m_bgLogicalSize) && m_bgLogicalSize.isValid()) { m_useViewportLod = true; m_bgViewportDirty = true; m_bgImageDirty = true; m_bgCutoutDirty = true; return; } const QImage img = readImageTolerant(m_bgAbsPath); if (!img.isNull()) { m_bgPixmap = QPixmap::fromImage(img); m_bgLogicalSize = img.size(); } m_bgImageDirty = true; m_bgCutoutDirty = true; } void EditorCanvas::ensureBackgroundViewport() { if (!m_useViewportLod || m_bgAbsPath.isEmpty() || !m_bgLogicalSize.isValid()) { return; } const QRectF bgRect(0, 0, m_bgLogicalSize.width(), m_bgLogicalSize.height()); QRectF vis = visibleWorldRectF().intersected(bgRect); if (vis.isEmpty()) { m_bgViewportImage = QImage(); m_bgViewportCacheRect = QRect(); m_bgViewportDirty = false; m_vpDecodeInFlight = false; return; } const QRect visI = vis.toAlignedRect(); const qreal pr = devicePixelRatio(); const qreal density = static_cast(m_scale) * pr; const bool previewCameraHighQ = m_presentationPreviewMode && m_previewCameraViewLocked; const qreal decodeBoost = previewCameraHighQ ? 1.35 : 1.0; // 视口背景在屏幕上通常会经历一次缩放(世界坐标 -> 视图像素)。 // 为了减少“发糊”,静止时做适度超采样:用更高分辨率解码,再由绘制阶段缩小。 // 拖拽交互时降低超采样,优先帧率。 const qreal superSample = m_draggingEntity ? 1.15 : (previewCameraHighQ ? 1.75 : 1.55); const qreal densityTol = previewCameraHighQ ? 0.045 : 0.09; // 缓存仍覆盖视口且清晰度档位未变:直接复用(避免 invalidateViewportLod 仅置 dirty 又触发二次解码失败把画面清空) // m_vpPreferRegionDecode 时不短路:必须在缩略图显示后立刻做一次区域细化 if (!m_vpPreferRegionDecode && !m_bgViewportImage.isNull() && m_bgViewportCacheRect.contains(visI) && m_vpCachedDensity > 1e-9 && std::abs(density - m_vpCachedDensity) / m_vpCachedDensity < densityTol) { m_bgViewportDirty = false; return; } if (m_vpDecodeInFlight.exchange(true)) { return; } const QString path = m_bgAbsPath; const QSize logicalSize = m_bgLogicalSize; const uint32_t myToken = m_vpToken.load(std::memory_order_relaxed); const qint64 imgArea = qint64(logicalSize.width()) * qint64(logicalSize.height()); const qint64 visArea = qint64(visI.width()) * qint64(visI.height()); // 预览且镜头锁定时禁用全图缩略路径,避免画面明显发糊 const bool overviewMode = !m_vpPreferRegionDecode && !previewCameraHighQ && imgArea > 0 && (static_cast(visArea) / static_cast(imgArea) >= 0.68) && core::VipsBackend::isAvailable(); // 扩大解码缓存:平移时尽量仍在同一张纹理内,避免每帧触发新的后台解码导致卡顿 const qreal screenWorldW = (static_cast(width()) * pr) / std::max(m_scale, static_cast(1e-9)); const qreal screenWorldH = (static_cast(height()) * pr) / std::max(m_scale, static_cast(1e-9)); const qreal padX = std::max(vis.width() * 0.95, screenWorldW * 0.75); const qreal padY = std::max(vis.height() * 0.95, screenWorldH * 0.75); QRect req = vis.adjusted(-padX, -padY, padX, padY).toAlignedRect().intersected(bgRect.toAlignedRect()); if (req.width() <= 0 || req.height() <= 0) { m_vpDecodeInFlight = false; if (m_bgViewportImage.isNull()) { m_bgViewportCacheRect = QRect(); } m_bgViewportDirty = false; return; } // 关键:解码目标分辨率必须以“req 区域在当前缩放下投影到屏幕的像素数”为基准。 // 否则后台解码完成后替换纹理时,画面会因为额外的 drawImage 缩放而突然变糊。 const int maxW = std::clamp(static_cast(std::ceil(req.width() * density * superSample * decodeBoost)), 256, 8192); const int maxH = std::clamp(static_cast(std::ceil(req.height() * density * superSample * decodeBoost)), 256, 8192); const qint64 budget = qint64(maxW) * qint64(maxH) * (overviewMode ? 2 : 1); // 缩略图阶段提高像素预算,减少首帧模糊感 (void)QtConcurrent::run([this, path, logicalSize, req, maxW, maxH, budget, density, overviewMode, myToken]() { QImage img; bool ok = false; if (overviewMode) { ok = core::VipsBackend::loadThumbnailQImage(path, budget, &img) && !img.isNull(); } if (!ok) { ok = core::image_file::loadRegionToQImage(path, req, maxW, maxH, &img) && !img.isNull(); } QPointer self(this); QTimer::singleShot( 0, this, [self, myToken, path, img = std::move(img), ok, logicalSize, req, density, overviewMode]() mutable { if (!self) { return; } self->m_vpDecodeInFlight = false; if (myToken != self->m_vpToken.load(std::memory_order_relaxed)) { self->update(); return; } if (path != self->m_bgAbsPath) { self->update(); return; } if (!ok || img.isNull()) { // 保留已成功显示过的纹理,避免后续一次失败把整屏打成「加载失败」 if (self->m_bgViewportImage.isNull()) { self->m_bgViewportCacheRect = QRect(); } self->m_vpPreferRegionDecode = false; self->m_bgViewportDirty = false; self->update(); self->tryEmitInitialBackgroundLoadFinished(); return; } if (img.format() != QImage::Format_ARGB32_Premultiplied) { img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied); } self->m_bgViewportImage = std::move(img); self->m_bgViewportCacheRect = overviewMode ? QRect(QPoint(0, 0), logicalSize) : req; self->m_bgViewportDirty = false; self->m_vpCachedDensity = density; if (overviewMode) { self->m_vpPreferRegionDecode = true; } else { self->m_vpPreferRegionDecode = false; } self->update(); self->tryEmitInitialBackgroundLoadFinished(); }); }); } QRectF EditorCanvas::visibleWorldRectF() const { const QPointF c0 = viewToWorld(QPointF(0, 0)); const QPointF c1 = viewToWorld(QPointF(width(), 0)); const QPointF c2 = viewToWorld(QPointF(width(), height())); const QPointF c3 = viewToWorld(QPointF(0, height())); const qreal minx = std::min({c0.x(), c1.x(), c2.x(), c3.x()}); const qreal maxx = std::max({c0.x(), c1.x(), c2.x(), c3.x()}); const qreal miny = std::min({c0.y(), c1.y(), c2.y(), c3.y()}); const qreal maxy = std::max({c0.y(), c1.y(), c2.y(), c3.y()}); QRectF rf(minx, miny, maxx - minx, maxy - miny); return rf.normalized(); } int EditorCanvas::backgroundLogicalWidth() const { ensurePixmapLoaded(); if (m_bgLogicalSize.isValid()) { return m_bgLogicalSize.width(); } return m_bgPixmap.isNull() ? 0 : m_bgPixmap.width(); } int EditorCanvas::backgroundLogicalHeight() const { ensurePixmapLoaded(); if (m_bgLogicalSize.isValid()) { return m_bgLogicalSize.height(); } return m_bgPixmap.isNull() ? 0 : m_bgPixmap.height(); } void EditorCanvas::updateCursor() { if (m_blackholeCopyResolveActive) { setCursor(m_blackholeCopyDragging ? Qt::ClosedHandCursor : Qt::OpenHandCursor); return; } if (m_presentationPreviewMode) { if (!m_presentationHotspotPlaybackTargetEnabled) { setCursor(Qt::ArrowCursor); return; } if (m_presHoverEntityIndex >= 0) { setCursor(Qt::PointingHandCursor); } else { setCursor(Qt::OpenHandCursor); } return; } switch (m_tool) { case Tool::Move: setCursor(Qt::OpenHandCursor); break; case Tool::Zoom: setCursor(Qt::CrossCursor); break; case Tool::CreateEntity: setCursor(Qt::CrossCursor); break; case Tool::AddHotspot: setCursor(Qt::CrossCursor); break; case Tool::MoveHotspot: setCursor(Qt::OpenHandCursor); break; } } QPointF EditorCanvas::viewToWorld(const QPointF& v) const { if (m_scale <= 0.0) { return {}; } return (v - m_pan) / m_scale; } QPointF EditorCanvas::worldToView(const QPointF& w) const { return w * m_scale + m_pan; } QRectF EditorCanvas::worldRectOfBackground() const { ensurePixmapLoaded(); if (m_bgLogicalSize.isValid()) { return QRectF(0, 0, m_bgLogicalSize.width(), m_bgLogicalSize.height()); } if (m_bgPixmap.isNull()) { return {}; } return QRectF(0, 0, m_bgPixmap.width(), m_bgPixmap.height()); } int EditorCanvas::hitTestEntity(const QPointF& worldPos) const { for (qsizetype i = m_entities.size(); i > 0; --i) { const qsizetype idx = i - 1; const auto& ent = m_entities[idx]; if (ent.opacity <= 0.001) { continue; } if (!ent.id.isEmpty() && m_tempHiddenEntityIds.contains(ent.id)) { continue; } if (!ent.polygonWorld.isEmpty()) { const QPainterPath path = entity_cutout::pathFromWorldPolygon(ent.polygonWorld); if (path.contains(worldPos)) { return static_cast(idx); } continue; } if (ent.rect.contains(worldPos)) { return static_cast(idx); } } return -1; } int EditorCanvas::hitTestPresentationHotspot(const QPointF& worldPos) const { const QPointF pv = worldToView(worldPos); for (int i = static_cast(m_presentationHotspots.size()) - 1; i >= 0; --i) { const QPointF cv = worldToView(m_presentationHotspots[i].centerWorld); if (QLineF(pv, cv).length() <= 22.0) { return i; } } return -1; } int EditorCanvas::hitTestHotspotPlayControl(const QPointF& worldPos) const { if (!m_presentationHotspotPlaybackTargetEnabled) { return -1; } const QPointF pv = worldToView(worldPos); for (int i = static_cast(m_presentationHotspots.size()) - 1; i >= 0; --i) { const QPointF cv = worldToView(m_presentationHotspots[i].centerWorld); const QPointF playCenter = cv + QPointF(14.0, 0.0); if (QLineF(pv, playCenter).length() <= 17.0) { return i; } } return -1; } void EditorCanvas::paintEvent(QPaintEvent* e) { Q_UNUSED(e); QPainter p(this); p.setRenderHint(QPainter::Antialiasing, false); // 拖动时优先保证交互帧率:缩放贴图使用 nearest 以减少开销 p.setRenderHint(QPainter::SmoothPixmapTransform, !m_draggingEntity); const QRect r = rect(); if (!m_presentationPreviewMode) { if (m_checkerboardVisible) { drawCheckerboard(p, r); } if (m_gridVisible) { drawGrid(p, r); } } ensurePixmapLoaded(); if (m_bgAbsPath.isEmpty()) { return; } if (m_useViewportLod) { ensureBackgroundViewport(); if (m_bgViewportImage.isNull()) { return; } } else if (m_bgPixmap.isNull()) { tryEmitInitialBackgroundLoadFinished(); return; } const bool showBg = m_presentationPreviewMode || m_backgroundVisible; if (showBg && !m_useViewportLod) { syncFullBackgroundImageFromDiskIfDirty(); rebuildFullBackgroundCutoutIfDirty(); } // 以“世界坐标”绘制:支持缩放/平移 p.save(); QTransform t; t.translate(m_pan.x(), m_pan.y()); t.scale(m_scale, m_scale); p.setTransform(t, true); const qreal bw = static_cast(backgroundLogicalWidth()); const qreal bh = static_cast(backgroundLogicalHeight()); if (showBg) { if (m_useViewportLod) { p.drawImage(QRectF(m_bgViewportCacheRect), m_bgViewportImage); if (showBg) { for (const auto& ent : m_entities) { if (ent.blackholeVisible && !ent.cutoutPolygonWorld.isEmpty()) { p.fillPath(entity_cutout::pathFromWorldPolygon(ent.cutoutPolygonWorld), Qt::black); } } } } else if (m_backgroundVisible || m_presentationPreviewMode) { if (!m_bgImageCutout.isNull()) { p.drawImage(QPointF(0, 0), m_bgImageCutout); } else if (!m_bgImage.isNull()) { p.drawImage(QPointF(0, 0), m_bgImage); } else { p.drawPixmap(QPointF(0, 0), m_bgPixmap); } } if (!m_presentationPreviewMode && m_backgroundVisible && bw > 0 && bh > 0) { p.setPen(QPen(QColor(0, 0, 0, 80), 1.0 / std::max(m_scale, 0.001))); p.drawRect(QRectF(0, 0, bw, bh).adjusted(0, 0, -1, -1)); } if (!m_projectDirAbs.isEmpty()) { for (const auto& ent : m_entities) { if (ent.blackholeOverlayPath.isEmpty() || ent.blackholeOverlayRect.width() <= 0 || ent.blackholeOverlayRect.height() <= 0) { continue; } QImage ovl; if (m_blackholeOverlayImageCache.contains(ent.id)) { ovl = m_blackholeOverlayImageCache.value(ent.id); } else { const QString abs = QDir(m_projectDirAbs).filePath(ent.blackholeOverlayPath); ovl = readImageTolerant(abs); if (!ovl.isNull() && ovl.format() != QImage::Format_ARGB32_Premultiplied) { ovl = ovl.convertToFormat(QImage::Format_ARGB32_Premultiplied); } if (!ovl.isNull()) { m_blackholeOverlayImageCache.insert(ent.id, ovl); } } if (ovl.isNull()) { continue; } p.drawImage(QPointF(ent.blackholeOverlayRect.topLeft()), ovl); } } } // 深度叠加(伪彩色):仅由「叠加深度」开关控制,与是否显示背景无关 const bool wantDepth = !m_presentationPreviewMode && (!m_depthAbsPath.isEmpty()) && m_depthOverlayEnabled; if (wantDepth) { if (m_depthDirty) { m_depthDirty = false; m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } if (!m_depthImage8.isNull()) { const int overlayAlpha = m_backgroundVisible ? m_depthOverlayAlpha : 255; const QImage overlay = core::DepthService::depthToColormapOverlay(m_depthImage8, overlayAlpha); if (!overlay.isNull()) { p.drawImage(QPointF(0, 0), overlay); } else { p.drawImage(QPointF(0, 0), m_depthImage8); } p.setPen(QPen(QColor(0, 0, 0, 80), 1.0 / std::max(m_scale, 0.001))); p.drawRect(QRectF(0, 0, m_depthImage8.width(), m_depthImage8.height()).adjusted(0, 0, -1, -1)); } } if (!m_presentationPreviewMode && m_blackholeCopyResolveActive && !m_blackholeCopyHoleRect.isNull() && !m_blackholeCopySourceRect.isNull()) { const QRect srcRect = m_blackholeCopySourceRect.toAlignedRect(); const QRect dstRect = m_blackholeCopyHoleRect.toAlignedRect(); const bool havePreview = (m_bhCopyPatchValid && !m_bhCopyPatch.isNull()) || !m_bgImage.isNull(); if (havePreview && srcRect.isValid() && dstRect.isValid()) { QPainterPath holePath; for (const auto& ent : m_entities) { if (ent.id == m_blackholeCopyEntityId && !ent.cutoutPolygonWorld.isEmpty()) { holePath = entity_cutout::pathFromWorldPolygon(ent.cutoutPolygonWorld); break; } } p.save(); if (!holePath.isEmpty()) { p.setClipPath(holePath); } p.setOpacity(0.75); if (m_bhCopyPatchValid && !m_bhCopyPatch.isNull()) { const QRect srcLocal = srcRect.translated(-m_bhCopyPatchOrigin); p.drawImage(dstRect.topLeft(), m_bhCopyPatch, srcLocal); } else { p.drawImage(dstRect.topLeft(), m_bgImage, srcRect); } p.setOpacity(1.0); p.restore(); } p.setBrush(Qt::NoBrush); QPen holePen(QColor(255, 120, 0, 220), 2.0 / std::max(m_scale, 0.001)); holePen.setStyle(Qt::DashLine); p.setPen(holePen); p.drawRect(m_blackholeCopyHoleRect); QPen srcPen(QColor(70, 200, 255, 230), 2.0 / std::max(m_scale, 0.001)); p.setPen(srcPen); p.drawRect(m_blackholeCopySourceRect); } struct DrawItem { enum class Kind { Entity, Tool }; Kind kind = Kind::Entity; int index = -1; int priority = 0; int depth = 0; }; QVector drawOrder; drawOrder.reserve(m_entities.size() + m_tools.size()); for (int i = 0; i < m_entities.size(); ++i) { const auto& ent = m_entities[i]; if (ent.opacity <= 0.001) continue; if (!ent.id.isEmpty() && m_tempHiddenEntityIds.contains(ent.id)) continue; drawOrder.push_back(DrawItem{DrawItem::Kind::Entity, i, ent.priority, ent.depth}); } for (int i = 0; i < m_tools.size(); ++i) { const auto& tv = m_tools[i]; if (tv.opacity <= 0.001) continue; if (!tv.tool.id.isEmpty() && m_tempHiddenToolIds.contains(tv.tool.id)) continue; if (tv.tool.type != core::Project::Tool::Type::Bubble) continue; drawOrder.push_back(DrawItem{DrawItem::Kind::Tool, i, tv.tool.priority, tv.depthZ}); } // priority 低先画、高后画;同 priority:远先画、近后画 std::stable_sort(drawOrder.begin(), drawOrder.end(), [](const DrawItem& a, const DrawItem& b) { if (a.priority != b.priority) return a.priority < b.priority; if (a.depth != b.depth) return a.depth < b.depth; if (a.kind != b.kind) return int(a.kind) < int(b.kind); return a.index < b.index; }); for (const auto& it : drawOrder) { if (it.kind == DrawItem::Kind::Entity) { const int i = it.index; const auto& ent = m_entities[i]; const bool isDragPreview = (!m_presentationPreviewMode && m_draggingEntity && m_dragPreviewActive && i == m_selectedEntity); if (!ent.polygonWorld.isEmpty()) { if (!ent.image.isNull()) { if (isDragPreview) { const QPointF cBase = m_dragCentroidBase; p.save(); QTransform tr; tr.translate(m_dragDelta.x(), m_dragDelta.y()); tr.translate(cBase.x(), cBase.y()); tr.scale(m_dragScaleRatio, m_dragScaleRatio); tr.translate(-cBase.x(), -cBase.y()); p.setTransform(tr, true); const QSizeF sz(ent.image.width() * m_dragScaleBase, ent.image.height() * m_dragScaleBase); const QRectF target(m_dragImageTopLeftBase, sz); p.drawImage(target, ent.image); p.restore(); } else { const qreal pop = (m_presentationPreviewMode && i == m_presFocusedEntityIndex) ? 1.1 : 1.0; const QSizeF sz0(ent.image.width() * ent.visualScale, ent.image.height() * ent.visualScale); QRectF target; if (pop > 1.001) { const QRectF orig(ent.imageTopLeft, sz0); const QPointF cen = orig.center(); const QSizeF sz = orig.size() * pop; target = QRectF(QPointF(cen.x() - sz.width() * 0.5, cen.y() - sz.height() * 0.5), sz); } else { target = QRectF(ent.imageTopLeft, sz0); } QImage drawImg = ent.image; if (m_presentationPreviewMode && !ent.image.isNull()) { const qreal pr = devicePixelRatio(); const qreal pxW = std::abs(target.width() * m_scale * pr); const qreal pxH = std::abs(target.height() * m_scale * pr); drawImg = entityImageForPresentationDrawLoRes(ent.image, pxW, pxH); } p.drawImage(target, drawImg); } } else { const QPolygonF poly(isDragPreview ? QPolygonF(m_dragPolyBase) : QPolygonF(ent.polygonWorld)); p.setPen(Qt::NoPen); p.setBrush(ent.color); if (isDragPreview) { const QPointF cBase = m_dragCentroidBase; QTransform tr; tr.translate(m_dragDelta.x(), m_dragDelta.y()); tr.translate(cBase.x(), cBase.y()); tr.scale(m_dragScaleRatio, m_dragScaleRatio); tr.translate(-cBase.x(), -cBase.y()); p.save(); p.setTransform(tr, true); p.drawPolygon(poly); p.restore(); } else { p.drawPolygon(poly); } } if (!m_presentationPreviewMode) { p.setBrush(Qt::NoBrush); p.setPen(QPen(QColor(0, 0, 0, 160), 1.0 / std::max(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(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(m_scale, 0.001))); if (!ent.polygonWorld.isEmpty()) { if (isDragPreview) { const QPointF cBase = m_dragCentroidBase; QTransform tr; tr.translate(m_dragDelta.x(), m_dragDelta.y()); tr.translate(cBase.x(), cBase.y()); tr.scale(m_dragScaleRatio, m_dragScaleRatio); tr.translate(-cBase.x(), -cBase.y()); p.save(); p.setTransform(tr, true); p.drawPath(m_dragPathBase); p.restore(); } else { p.drawPath(ent.pathWorld); } } else { p.drawRect(ent.rect.adjusted(-2, -2, 2, 2)); } } if (!m_presentationPreviewMode && ent.id == m_selectedBlackholeEntityId && !ent.cutoutPolygonWorld.isEmpty()) { p.setBrush(Qt::NoBrush); QPen holePen(QColor(70, 200, 255, 230), 2.2 / std::max(m_scale, 0.001)); holePen.setStyle(Qt::DashLine); p.setPen(holePen); p.drawPath(entity_cutout::pathFromWorldPolygon(ent.cutoutPolygonWorld)); } if (m_presentationPreviewMode && ent.opacity > 0.001) { const bool showHover = (i == m_presHoverEntityIndex); const bool showFocus = (i == m_presFocusedEntityIndex); if (showHover || showFocus) { p.setBrush(Qt::NoBrush); if (showHover) { const qreal pulse = 0.45 + 0.55 * std::sin(static_cast(m_presHoverPhase)); const qreal lw = (2.0 + 2.8 * pulse) / std::max(static_cast(m_scale), static_cast(0.001)); p.setPen(QPen(QColor(255, 210, 80, static_cast(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(m_scale), static_cast(0.001)); p.setPen(QPen(QColor(255, 120, 40, 230), lw)); if (!ent.pathWorld.isEmpty()) { p.drawPath(ent.pathWorld); } else { p.drawRect(ent.rect); } } } } } else { const int i = it.index; const auto& tv = m_tools[i]; const auto& tool = tv.tool; const double opacity = std::clamp(tv.opacity, 0.0, 1.0); const BubbleLayoutWorld lay = bubbleLayoutWorld(tool); const QPainterPath& path = lay.path; const QRectF& body = lay.bodyRect; QColor fill(255, 255, 255, int(220 * opacity)); QColor border(0, 0, 0, int(120 * opacity)); p.setBrush(fill); p.setPen(QPen(border, 1.2 / std::max(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(m_scale, 0.001))); p.drawPath(path); } } } // 摄像机视口框(编辑模式) if (!m_presentationPreviewMode) { const qreal handleRWorld = 10.0 / std::max(m_scale, 0.001); for (int i = 0; i < m_cameraOverlays.size(); ++i) { const auto& cam = m_cameraOverlays[i]; if (!cam.visible || cam.id.isEmpty() || m_tempHiddenCameraIds.contains(cam.id)) { continue; } const QRectF camRect = cameraWorldViewportRect(cam); QColor fill(80, 140, 255, 38); QColor border(80, 140, 255, 170); if (i == m_selectedCameraIndex) { border = QColor(255, 170, 60, 230); fill = QColor(255, 170, 60, 48); } p.setBrush(fill); p.setPen(QPen(border, 1.5 / std::max(m_scale, 0.001))); p.drawRect(camRect); p.setBrush(QColor(255, 210, 120, 230)); p.setPen(QPen(QColor(30, 30, 30, 160), 1.0 / std::max(m_scale, 0.001))); p.drawEllipse(cam.centerWorld, handleRWorld, handleRWorld); } } // 创建实体:手绘轨迹预览(world 坐标) if (!m_presentationPreviewMode && m_tool == Tool::CreateEntity && m_drawingEntity && m_strokeWorld.size() >= 2) { QPen pen(QColor(255, 120, 0, 220), 2.0 / std::max(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(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(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(m_scale, 0.001); p.setPen(QPen(QColor(0, 0, 0, 120), lw)); p.setBrush(QColor(255, 255, 255, 220)); for (const QPointF& v : m_pendingPolyWorld) { p.drawEllipse(v, vr, vr); } } p.restore(); // 坐标轴/刻度:绘制在画布最外层,背景越界时贴边显示 ensurePixmapLoaded(); if (!m_presentationPreviewMode && m_worldAxesVisible && backgroundLogicalWidth() > 0 && backgroundLogicalHeight() > 0) { const QPointF originView = worldToView(QPointF(0, 0)); const qreal axisX = std::clamp(originView.x(), 0.0, static_cast(width())); const qreal axisY = std::clamp(originView.y(), 0.0, static_cast(height())); QPen axisPen(QColor(20, 20, 20, 180)); axisPen.setWidth(2); p.setPen(axisPen); p.drawLine(QPointF(0, axisY), QPointF(width(), axisY)); // X 轴(水平) p.drawLine(QPointF(axisX, 0), QPointF(axisX, height())); // Y 轴(垂直) // 根据缩放与视口大小动态调整刻度密度:使相邻刻度在屏幕上保持“够密但不挤”的间距 auto niceStep = [](double raw) -> double { if (!(raw > 0.0) || !std::isfinite(raw)) { return 1.0; } const double p10 = std::pow(10.0, std::floor(std::log10(raw))); const double m = raw / p10; // 1..10 double n = 1.0; if (m <= 1.0) n = 1.0; else if (m <= 2.0) n = 2.0; else if (m <= 5.0) n = 5.0; else n = 10.0; return n * p10; }; const double sView = std::max(0.001, double(m_scale)); const double targetTickPx = 86.0; // 目标:相邻刻度约 86px const double minTickPx = 44.0; // 太密则自动放大 step const double stepWorld = std::max(1.0, niceStep(targetTickPx / sView)); const double stepPx = stepWorld * sView; const int labelEvery = (stepPx < minTickPx) ? int(std::ceil(minTickPx / std::max(1.0, stepPx))) : 1; auto visibleWorldXRange = [&]() -> std::pair { const QPointF w0 = viewToWorld(QPointF(0, 0)); const QPointF w1 = viewToWorld(QPointF(width(), height())); double a = std::min(w0.x(), w1.x()); double b = std::max(w0.x(), w1.x()); a = std::clamp(a, 0.0, double(backgroundLogicalWidth())); b = std::clamp(b, 0.0, double(backgroundLogicalWidth())); return {a, b}; }; auto visibleWorldYRange = [&]() -> std::pair { const QPointF w0 = viewToWorld(QPointF(0, 0)); const QPointF w1 = viewToWorld(QPointF(width(), height())); double a = std::min(w0.y(), w1.y()); double b = std::max(w0.y(), w1.y()); a = std::clamp(a, 0.0, double(backgroundLogicalHeight())); b = std::clamp(b, 0.0, double(backgroundLogicalHeight())); return {a, b}; }; QPen tickPen(QColor(20, 20, 20, 140)); tickPen.setWidth(1); p.setPen(tickPen); if (m_axisLabelsVisible) { QFont f = p.font(); f.setPointSize(std::max(7, f.pointSize() - 1)); p.setFont(f); } // X 轴:用 y=0 的世界线映射到 view-x,并把刻度画在 axisY 上 { const auto [xmin, xmax] = visibleWorldXRange(); const double start = std::floor(xmin / stepWorld) * stepWorld; int iTick = 0; for (double x = start; x <= xmax + 1e-9; x += stepWorld, ++iTick) { const double xc = std::clamp(x, 0.0, double(backgroundLogicalWidth())); const QPointF vx = worldToView(QPointF(xc, 0)); if (vx.x() < -50 || vx.x() > width() + 50) { continue; } const qreal tx = std::clamp(vx.x(), 0.0, static_cast(width())); p.drawLine(QPointF(tx, axisY), QPointF(tx, axisY + 6)); if (m_axisLabelsVisible) { if (labelEvery <= 1 || (iTick % labelEvery) == 0) { p.drawText(QPointF(tx + 2, axisY + 18), QString::number(int(std::lround(xc)))); } } } } // Y 轴:用 x=0 的世界线映射到 view-y,并把刻度画在 axisX 上 { const auto [ymin, ymax] = visibleWorldYRange(); const double start = std::floor(ymin / stepWorld) * stepWorld; int iTick = 0; for (double y = start; y <= ymax + 1e-9; y += stepWorld, ++iTick) { const double yc = std::clamp(y, 0.0, double(backgroundLogicalHeight())); const QPointF vy = worldToView(QPointF(0, yc)); if (vy.y() < -50 || vy.y() > height() + 50) { continue; } const qreal ty = std::clamp(vy.y(), 0.0, static_cast(height())); p.drawLine(QPointF(axisX, ty), QPointF(axisX + 6, ty)); if (m_axisLabelsVisible) { if (labelEvery <= 1 || (iTick % labelEvery) == 0) { p.drawText(QPointF(axisX + 10, ty - 2), QString::number(int(std::lround(yc)))); } } } } } // Gizmo:选中实体时显示(仿 Blender:约束 X/Y 轴移动) if (!m_presentationPreviewMode && m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) { const auto& ent = m_entities[m_selectedEntity]; const bool isDragPreview = (m_draggingEntity && m_dragPreviewActive); QPointF originWorld = ent.rect.center(); if (isDragPreview) { originWorld = m_dragCentroidBase + m_dragDelta; } else if (!ent.polygonWorld.isEmpty()) { originWorld = entity_cutout::polygonCentroid(ent.polygonWorld); } const QPointF originView = worldToView(originWorld); // 中心(形心)十字锚点:独立于坐标轴手柄,始终可见 { const qreal h = 7.0; QPen outline(QColor(15, 15, 18, 235)); outline.setWidth(3); outline.setCapStyle(Qt::FlatCap); QPen core(QColor(255, 248, 220, 252)); core.setWidth(1); core.setCapStyle(Qt::FlatCap); p.setPen(outline); p.drawLine(QPointF(originView.x() - h, originView.y()), QPointF(originView.x() + h, originView.y())); p.drawLine(QPointF(originView.x(), originView.y() - h), QPointF(originView.x(), originView.y() + h)); p.setPen(core); p.drawLine(QPointF(originView.x() - h, originView.y()), QPointF(originView.x() + h, originView.y())); p.drawLine(QPointF(originView.x(), originView.y() - h), QPointF(originView.x(), originView.y() + h)); } // 变换原点(枢轴):与形心不同时显示 { const QPointF pivotWorld = ent.animatedOriginWorld; const double sepWorld = QLineF(originWorld, pivotWorld).length(); if (sepWorld > 0.5) { const QPointF pv = worldToView(pivotWorld); const qreal pivotDotR = 4.5; p.setPen(QPen(QColor(40, 40, 48, 220), 1)); p.setBrush(QColor(255, 120, 210, 235)); p.drawEllipse(pv, pivotDotR, pivotDotR); p.setBrush(Qt::NoBrush); } } const qreal len = 56.0; QPen xPen(QColor(220, 60, 60, 220)); xPen.setWidth(2); QPen yPen(QColor(60, 180, 90, 220)); yPen.setWidth(2); // X 轴(从形心出发,用于拖拽平移) p.setPen(xPen); p.drawLine(originView, QPointF(originView.x() + len, originView.y())); p.drawRect(QRectF(QPointF(originView.x() + len - 4, originView.y() - 4), QSizeF(8, 8))); if (m_gizmoLabelsVisible) { p.drawText(QPointF(originView.x() + len + 6, originView.y() + 4), QStringLiteral("X")); } // Y 轴 p.setPen(yPen); p.drawLine(originView, QPointF(originView.x(), originView.y() + len)); p.drawRect(QRectF(QPointF(originView.x() - 4, originView.y() + len - 4), QSizeF(8, 8))); if (m_gizmoLabelsVisible) { p.drawText(QPointF(originView.x() + 6, originView.y() + len + 14), QStringLiteral("Y")); } } if (!m_presentationHotspots.isEmpty()) { if (m_presentationPreviewMode && m_presentationHotspotPlaybackTargetEnabled) { p.setRenderHint(QPainter::Antialiasing, true); for (const auto& h : m_presentationHotspots) { const QPointF cv = worldToView(h.centerWorld); p.setPen(QPen(QColor(255, 248, 220, 235), 2)); p.setBrush(QColor(30, 34, 42, 170)); p.drawEllipse(cv, 16.0, 16.0); QPolygonF tri; tri << (cv + QPointF(-3.0, -6.0)) << (cv + QPointF(-3.0, 6.0)) << (cv + QPointF(8.0, 0.0)); p.setBrush(QColor(255, 248, 220, 250)); p.drawPolygon(tri); } p.setRenderHint(QPainter::Antialiasing, false); } else if (m_presentationHotspotEditorActive) { p.setRenderHint(QPainter::Antialiasing, true); for (const auto& h : m_presentationHotspots) { const QPointF cv = worldToView(h.centerWorld); const bool sel = (h.id == m_selectedPresentationHotspotId); p.setPen(QPen(sel ? QColor(255, 210, 72) : QColor(92, 200, 255), 2)); p.setBrush(QColor(24, 28, 36, 150)); p.drawEllipse(cv, 14.0, 14.0); } p.setRenderHint(QPainter::Antialiasing, false); } } tryEmitInitialBackgroundLoadFinished(); } void EditorCanvas::resizeEvent(QResizeEvent* e) { QWidget::resizeEvent(e); invalidateViewportLod(); update(); } void EditorCanvas::mousePressEvent(QMouseEvent* e) { if (e->button() != Qt::LeftButton && e->button() != Qt::MiddleButton) { QWidget::mousePressEvent(e); return; } const QPointF wp0 = viewToWorld(e->position()); emit hoveredWorldPosChanged(wp0); int z0 = -1; if (!m_depthAbsPath.isEmpty()) { if (m_depthDirty) { m_depthDirty = false; m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } if (!m_depthImage8.isNull()) { const int xi = static_cast(std::floor(wp0.x())); const int yi = static_cast(std::floor(wp0.y())); if (xi >= 0 && yi >= 0 && xi < m_depthImage8.width() && yi < m_depthImage8.height()) { z0 = static_cast(m_depthImage8.constScanLine(yi)[xi]); } } } emit hoveredWorldPosDepthChanged(wp0, z0); if (!m_presentationPreviewMode && m_presentationHotspotEditorActive && e->button() == Qt::LeftButton) { if (m_tool == Tool::AddHotspot) { emit requestAddPresentationHotspot(wp0); e->accept(); return; } if (m_tool == Tool::MoveHotspot) { const int hi = hitTestPresentationHotspot(wp0); if (hi >= 0) { m_dragging = true; m_draggingHotspotIndex = hi; m_hotspotDragOffsetWorld = wp0 - m_presentationHotspots[hi].centerWorld; m_selectedPresentationHotspotId = m_presentationHotspots[hi].id; m_lastMouseView = e->position(); setCursor(Qt::ClosedHandCursor); emit selectedPresentationHotspotChanged(m_selectedPresentationHotspotId); } else { m_selectedPresentationHotspotId.clear(); emit selectedPresentationHotspotChanged(QString()); } update(); e->accept(); return; } if (m_tool == Tool::Move) { m_dragging = true; m_lastMouseView = e->position(); setCursor(Qt::ClosedHandCursor); e->accept(); return; } } if (m_blackholeCopyResolveActive) { if (e->button() == Qt::LeftButton) { QRectF src = m_blackholeCopySourceRect; if (!src.contains(wp0)) { src.moveCenter(wp0); src = clampRectTopLeftToBounds(src, worldRectOfBackground()); m_blackholeCopySourceRect = src; } m_blackholeCopyDragging = true; m_blackholeCopyDragOffset = wp0 - m_blackholeCopySourceRect.topLeft(); updateCursor(); update(); e->accept(); return; } e->accept(); return; } if (m_presentationPreviewMode) { if (!m_presentationHotspotPlaybackTargetEnabled) { e->accept(); return; } if (e->button() == Qt::LeftButton) { if (m_presentationHotspotPlaybackTargetEnabled) { const int playIx = hitTestHotspotPlayControl(wp0); if (playIx >= 0 && playIx < m_presentationHotspots.size()) { emit hotspotPlayRequested(m_presentationHotspots[playIx].id); e->accept(); return; } } const int hit = hitTestEntity(wp0); if (hit >= 0) { const auto& ent = m_entities[hit]; const QPointF cWorld = ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld); const QPointF anchorView = worldToView(cWorld); beginPresentationZoomTowardEntity(hit); emit presentationEntityIntroRequested(ent.id, anchorView); return; } if (!m_previewCameraViewLocked) { m_dragging = true; m_presBgPanSession = true; m_presBgDragDist = 0.0; m_lastMouseView = e->position(); setCursor(Qt::ClosedHandCursor); } return; } if (e->button() == Qt::MiddleButton) { m_dragging = true; m_presBgPanSession = false; m_lastMouseView = e->position(); setCursor(Qt::ClosedHandCursor); } return; } if (m_tool == Tool::CreateEntity && e->button() == Qt::LeftButton) { // 若已有待确认多边形:进入微调(顶点/整体拖拽),或点击空白直接确认 if (!m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) { m_dragging = true; m_lastMouseView = e->position(); const QPointF w = viewToWorld(e->position()); m_pendingLastMouseWorld = w; int vi = -1; if (isPointNearPendingVertex(w, &vi)) { m_pendingDragging = true; m_pendingDragVertex = vi; m_pendingDragWhole = false; e->accept(); return; } if (pendingPolygonContains(w)) { m_pendingDragging = true; m_pendingDragWhole = true; m_pendingDragVertex = -1; e->accept(); return; } // 点击空白:确认 emit requestFinalizePendingEntity(m_pendingPolyWorld); e->accept(); return; } m_dragging = true; m_drawingEntity = true; m_draggingEntity = false; m_selectedEntity = -1; emit selectedEntityChanged(false, QString(), 0, QPointF()); m_lastMouseView = e->position(); m_strokeWorld.clear(); m_strokeWorld.push_back(viewToWorld(e->position())); update(); return; } m_dragging = true; m_lastMouseView = e->position(); const QPointF worldPos = viewToWorld(e->position()); if (e->button() == Qt::MiddleButton || m_tool == Tool::Move) { setCursor(Qt::ClosedHandCursor); } if (m_tool == Tool::Move && e->button() == Qt::LeftButton) { // 摄像机:绘制在工具之上,命中优先于工具 if (!m_presentationPreviewMode) { const qreal handleRWorld = 12.0 / std::max(m_scale, 0.001); for (int idx = static_cast(m_cameraOverlays.size()) - 1; idx >= 0; --idx) { const auto& cam = m_cameraOverlays[idx]; if (!cam.visible || cam.id.isEmpty() || m_tempHiddenCameraIds.contains(cam.id)) { continue; } const QRectF camRect = cameraWorldViewportRect(cam); const double dist = QLineF(worldPos, cam.centerWorld).length(); const bool inHandle = dist <= handleRWorld; const bool inRect = camRect.contains(worldPos); if (!inHandle && !inRect) { continue; } m_selectedCameraIndex = idx; m_selectedCameraId = cam.id; m_selectedEntity = -1; m_selectedTool = -1; m_draggingTool = false; m_draggingEntity = false; emit selectedEntityChanged(false, QString(), 0, QPointF()); emit selectedToolChanged(false, QString(), QPointF()); if (inHandle) { m_draggingCamera = true; m_cameraDragOffsetWorld = worldPos - cam.centerWorld; m_cameraDragStartCenterWorld = cam.centerWorld; } else { m_draggingCamera = false; } emit selectedCameraChanged(true, cam.id, cam.centerWorld, cam.viewScale); update(); return; } } // 命中测试:按绘制顺序从上到下(priority/深度),找到顶层的实体或工具 struct HitItem { enum class Kind { Entity, Tool }; Kind kind = Kind::Entity; int index = -1; int priority = 0; int depth = 0; }; QVector order; order.reserve(m_entities.size() + m_tools.size()); for (int i = 0; i < m_entities.size(); ++i) { const auto& ent = m_entities[i]; if (ent.opacity <= 0.001) continue; if (!ent.id.isEmpty() && m_tempHiddenEntityIds.contains(ent.id)) continue; order.push_back(HitItem{HitItem::Kind::Entity, i, ent.priority, ent.depth}); } for (int i = 0; i < m_tools.size(); ++i) { const auto& tv = m_tools[i]; if (tv.opacity <= 0.001) continue; if (!tv.tool.id.isEmpty() && m_tempHiddenToolIds.contains(tv.tool.id)) continue; if (tv.tool.type != core::Project::Tool::Type::Bubble) continue; order.push_back(HitItem{HitItem::Kind::Tool, i, tv.tool.priority, tv.depthZ}); } std::stable_sort(order.begin(), order.end(), [](const HitItem& a, const HitItem& b) { if (a.priority != b.priority) return a.priority < b.priority; if (a.depth != b.depth) return a.depth < b.depth; if (a.kind != b.kind) return int(a.kind) < int(b.kind); return a.index < b.index; }); for (qsizetype i = order.size(); i > 0; --i) { const auto& it = order[i - 1]; if (it.kind == HitItem::Kind::Tool) { const auto& tv = m_tools[it.index]; const QPainterPath path = bubblePathWorld(tv.tool); if (!path.contains(worldPos)) continue; clearCameraSelection(); m_selectedTool = it.index; m_selectedEntity = -1; m_draggingTool = true; m_dragMode = DragMode::Free; m_toolDragOffsetOriginWorld = worldPos - m_tools[m_selectedTool].tool.originWorld; m_toolDragStartOriginWorld = m_tools[m_selectedTool].tool.originWorld; emit selectedEntityChanged(false, QString(), 0, QPointF()); emit selectedToolChanged(true, m_tools[m_selectedTool].tool.id, m_tools[m_selectedTool].tool.originWorld); update(); return; } const auto& ent = m_entities[it.index]; bool hit = false; if (!ent.pathWorld.isEmpty()) hit = ent.pathWorld.contains(worldPos); else if (!ent.polygonWorld.isEmpty()) hit = entity_cutout::pathFromWorldPolygon(ent.polygonWorld).contains(worldPos); else hit = ent.rect.contains(worldPos); if (!hit) continue; // 选择实体(保持原逻辑) clearCameraSelection(); m_selectedEntity = it.index; m_selectedTool = -1; m_draggingTool = false; m_draggingEntity = true; m_dragMode = DragMode::Free; emit entityDragActiveChanged(true); const QRectF r = m_entities[m_selectedEntity].rect.isNull() && !m_entities[m_selectedEntity].polygonWorld.isEmpty() ? entity_cutout::pathFromWorldPolygon(m_entities[m_selectedEntity].polygonWorld).boundingRect() : m_entities[m_selectedEntity].rect; m_entities[m_selectedEntity].rect = r; { const auto& he = m_entities[m_selectedEntity]; const QPointF cRef = shapeCentroidWorld(he.polygonWorld, he.rect); const double s = std::max(1e-6, he.visualScale); m_entityDragAnchorLocal = (worldPos - cRef) / s; m_entityDragStartCentroidWorld = cRef; } // drag preview baseline m_dragPreviewActive = true; m_dragDelta = QPointF(0, 0); m_dragOriginBase = m_entities[m_selectedEntity].animatedOriginWorld; m_dragRectBase = m_entities[m_selectedEntity].rect; m_dragImageTopLeftBase = m_entities[m_selectedEntity].imageTopLeft; m_dragScaleBase = std::max(1e-6, m_entities[m_selectedEntity].visualScale); m_dragScaleRatio = 1.0; m_dragPolyBase = m_entities[m_selectedEntity].polygonWorld; m_dragPathBase = m_entities[m_selectedEntity].pathWorld; m_dragCentroidBase = m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase); const QPointF origin = !m_entities[m_selectedEntity].polygonWorld.isEmpty() ? entity_cutout::polygonCentroid(m_entities[m_selectedEntity].polygonWorld) : m_entities[m_selectedEntity].rect.center(); emit selectedEntityChanged(true, m_entities[m_selectedEntity].id, m_entities[m_selectedEntity].depth, origin); emit selectedToolChanged(false, QString(), QPointF()); update(); return; } // 优先:若已选中实体,且点在 gizmo 手柄上,则开启轴约束拖动 if (m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) { const auto& ent = m_entities[m_selectedEntity]; const bool isDragPreview = (m_draggingEntity && m_dragPreviewActive); QPointF originWorld = shapeCentroidWorld(ent.polygonWorld, ent.rect); if (isDragPreview) { originWorld = m_dragCentroidBase + m_dragDelta; } const QPointF originView = worldToView(originWorld); const GizmoHit gh = hitTestGizmo(e->position(), originView); if (gh.mode == DragMode::AxisX || gh.mode == DragMode::AxisY) { clearCameraSelection(); m_dragging = true; m_draggingEntity = true; m_dragMode = gh.mode; m_lastMouseView = e->position(); m_dragStartMouseWorld = viewToWorld(e->position()); // 为了统一复用 move 逻辑:初始化 rect 与基准点 const QRectF r = m_entities[m_selectedEntity].rect.isNull() && !m_entities[m_selectedEntity].polygonWorld.isEmpty() ? entity_cutout::pathFromWorldPolygon(m_entities[m_selectedEntity].polygonWorld).boundingRect() : m_entities[m_selectedEntity].rect; m_entities[m_selectedEntity].rect = r; { const auto& se = m_entities[m_selectedEntity]; const QPointF cRef = shapeCentroidWorld(se.polygonWorld, se.rect); const double s = std::max(1e-6, se.visualScale); m_entityDragAnchorLocal = (viewToWorld(e->position()) - cRef) / s; m_entityDragStartCentroidWorld = cRef; } // drag preview baseline m_dragPreviewActive = true; m_dragDelta = QPointF(0, 0); m_dragOriginBase = m_entities[m_selectedEntity].animatedOriginWorld; m_dragRectBase = m_entities[m_selectedEntity].rect; m_dragImageTopLeftBase = m_entities[m_selectedEntity].imageTopLeft; m_dragScaleBase = std::max(1e-6, m_entities[m_selectedEntity].visualScale); m_dragScaleRatio = 1.0; m_dragPolyBase = m_entities[m_selectedEntity].polygonWorld; m_dragPathBase = m_entities[m_selectedEntity].pathWorld; m_dragCentroidBase = m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase); update(); return; } } // 若已选中实体:点击命中该实体本体时,优先拖动“已选中实体”。 // 这对父子层级很重要:子实体可能被父实体遮挡,但用户在项目树中选中子实体后仍应可拖动它。 if (m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) { const auto& ent = m_entities[m_selectedEntity]; bool hitSelected = false; if (!ent.pathWorld.isEmpty()) { hitSelected = ent.pathWorld.contains(worldPos); } else if (!ent.polygonWorld.isEmpty()) { hitSelected = entity_cutout::pathFromWorldPolygon(ent.polygonWorld).contains(worldPos); } else { hitSelected = ent.rect.contains(worldPos); } if (hitSelected) { clearCameraSelection(); m_draggingEntity = true; m_dragMode = DragMode::Free; emit entityDragActiveChanged(true); const QRectF r = ent.rect.isNull() && !ent.polygonWorld.isEmpty() ? entity_cutout::pathFromWorldPolygon(ent.polygonWorld).boundingRect() : ent.rect; m_entities[m_selectedEntity].rect = r; { const auto& se = m_entities[m_selectedEntity]; const QPointF cRef = shapeCentroidWorld(se.polygonWorld, se.rect); const double s = std::max(1e-6, se.visualScale); m_entityDragAnchorLocal = (worldPos - cRef) / s; m_entityDragStartCentroidWorld = cRef; } // drag preview baseline m_dragPreviewActive = true; m_dragDelta = QPointF(0, 0); m_dragOriginBase = m_entities[m_selectedEntity].animatedOriginWorld; m_dragRectBase = m_entities[m_selectedEntity].rect; m_dragImageTopLeftBase = m_entities[m_selectedEntity].imageTopLeft; m_dragScaleBase = std::max(1e-6, m_entities[m_selectedEntity].visualScale); m_dragScaleRatio = 1.0; m_dragPolyBase = m_entities[m_selectedEntity].polygonWorld; m_dragPathBase = m_entities[m_selectedEntity].pathWorld; m_dragCentroidBase = m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase); update(); return; } } const int hit = hitTestEntity(worldPos); if (hit >= 0) { clearCameraSelection(); m_selectedEntity = hit; m_selectedTool = -1; m_draggingTool = false; m_draggingEntity = true; m_dragMode = DragMode::Free; emit entityDragActiveChanged(true); const QRectF r = m_entities[hit].rect.isNull() && !m_entities[hit].polygonWorld.isEmpty() ? entity_cutout::pathFromWorldPolygon(m_entities[hit].polygonWorld).boundingRect() : m_entities[hit].rect; m_entities[hit].rect = r; { const auto& he = m_entities[hit]; const QPointF cRef = shapeCentroidWorld(he.polygonWorld, he.rect); const double s = std::max(1e-6, he.visualScale); m_entityDragAnchorLocal = (worldPos - cRef) / s; m_entityDragStartCentroidWorld = cRef; } // drag preview baseline m_dragPreviewActive = true; m_dragDelta = QPointF(0, 0); m_dragOriginBase = m_entities[hit].animatedOriginWorld; m_dragRectBase = m_entities[hit].rect; m_dragImageTopLeftBase = m_entities[hit].imageTopLeft; m_dragScaleBase = std::max(1e-6, m_entities[hit].visualScale); m_dragScaleRatio = 1.0; m_dragPolyBase = m_entities[hit].polygonWorld; m_dragPathBase = m_entities[hit].pathWorld; m_dragCentroidBase = m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase); const QPointF origin = !m_entities[hit].polygonWorld.isEmpty() ? entity_cutout::polygonCentroid(m_entities[hit].polygonWorld) : m_entities[hit].rect.center(); emit selectedEntityChanged(true, m_entities[hit].id, m_entities[hit].depth, origin); emit selectedToolChanged(false, QString(), QPointF()); update(); return; } m_selectedEntity = -1; m_draggingEntity = false; m_selectedTool = -1; m_draggingTool = false; m_dragMode = DragMode::None; clearCameraSelection(); emit selectedEntityChanged(false, QString(), 0, QPointF()); emit selectedToolChanged(false, QString(), QPointF()); update(); } } void EditorCanvas::mouseMoveEvent(QMouseEvent* e) { const QPointF wp = viewToWorld(e->position()); emit hoveredWorldPosChanged(wp); int z = -1; if (!m_depthAbsPath.isEmpty()) { if (m_depthDirty) { m_depthDirty = false; m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } if (!m_depthImage8.isNull()) { const int xi = static_cast(std::floor(wp.x())); const int yi = static_cast(std::floor(wp.y())); if (xi >= 0 && yi >= 0 && xi < m_depthImage8.width() && yi < m_depthImage8.height()) { z = static_cast(m_depthImage8.constScanLine(yi)[xi]); } } } emit hoveredWorldPosDepthChanged(wp, z); if (m_blackholeCopyResolveActive) { if (m_blackholeCopyDragging && (e->buttons() & Qt::LeftButton)) { QRectF src = m_blackholeCopySourceRect; src.moveTopLeft(wp - m_blackholeCopyDragOffset); src = clampRectTopLeftToBounds(src, worldRectOfBackground()); m_blackholeCopySourceRect = src; update(); } e->accept(); return; } if (m_presentationPreviewMode) { if (!m_presentationHotspotPlaybackTargetEnabled) { if (m_presHoverEntityIndex >= 0 || (m_presHoverTimer && m_presHoverTimer->isActive()) || m_presHoverPhase != 0.0) { if (m_presHoverTimer) { m_presHoverTimer->stop(); } m_presHoverEntityIndex = -1; m_presHoverPhase = 0.0; updateCursor(); update(); } } else { const int h = hitTestEntity(wp); if (h != m_presHoverEntityIndex) { m_presHoverEntityIndex = h; updateCursor(); update(); } if (h >= 0) { if (m_presHoverTimer && !m_presHoverTimer->isActive()) { m_presHoverTimer->start(); } } else if (m_presHoverTimer) { m_presHoverTimer->stop(); m_presHoverPhase = 0.0; } } } if (!m_dragging) { QWidget::mouseMoveEvent(e); return; } const QPointF cur = e->position(); const QPointF deltaView = cur - m_lastMouseView; m_lastMouseView = cur; if (m_draggingHotspotIndex >= 0 && m_draggingHotspotIndex < m_presentationHotspots.size()) { const QPointF worldPos = viewToWorld(cur); m_presentationHotspots[m_draggingHotspotIndex].centerWorld = worldPos - m_hotspotDragOffsetWorld; update(); return; } if (m_pendingDragging && !m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) { const QPointF curWorld = viewToWorld(e->position()); const QPointF delta = curWorld - m_pendingLastMouseWorld; m_pendingLastMouseWorld = curWorld; if (m_pendingDragWhole) { for (auto& pt : m_pendingPolyWorld) { pt += delta; } } else if (m_pendingDragVertex >= 0 && m_pendingDragVertex < m_pendingPolyWorld.size()) { m_pendingPolyWorld[m_pendingDragVertex] += delta; } update(); return; } if (m_tool == Tool::CreateEntity && m_drawingEntity) { const QPointF w = viewToWorld(cur); if (m_strokeWorld.isEmpty()) { m_strokeWorld.push_back(w); update(); return; } const QPointF last = m_strokeWorld.last(); const qreal dx = w.x() - last.x(); const qreal dy = w.y() - last.y(); // 简单抽样:world 距离至少 1.5 像素才记录,避免点过密 if ((dx * dx + dy * dy) >= (1.5 * 1.5)) { m_strokeWorld.push_back(w); update(); } return; } if (m_draggingCamera && m_selectedCameraIndex >= 0 && m_selectedCameraIndex < m_cameraOverlays.size()) { const QPointF worldPos = viewToWorld(cur); const QPointF newCenter = worldPos - m_cameraDragOffsetWorld; QPointF delta = newCenter - m_cameraOverlays[m_selectedCameraIndex].centerWorld; m_cameraOverlays[m_selectedCameraIndex].centerWorld += delta; const auto& c = m_cameraOverlays[m_selectedCameraIndex]; emit selectedCameraChanged(true, c.id, c.centerWorld, c.viewScale); update(); return; } if (m_draggingEntity && m_selectedEntity >= 0 && m_selectedEntity < m_entities.size()) { const QPointF worldPos = viewToWorld(cur); auto& ent = m_entities[m_selectedEntity]; const double curScale = std::max(1e-6, ent.visualScale); const QPointF centroidNow = m_dragPreviewActive ? (m_dragCentroidBase + m_dragDelta) : shapeCentroidWorld(ent.polygonWorld, ent.rect); const QPointF newCentroid = worldPos - m_entityDragAnchorLocal * curScale; QPointF delta = newCentroid - centroidNow; // 轴约束:只允许沿 X 或 Y 平移(world 坐标) if (m_dragMode == DragMode::AxisX) { delta.setY(0.0); } else if (m_dragMode == DragMode::AxisY) { delta.setX(0.0); } // 约束到背景范围内(若有背景),按“预览变换后”的包围盒约束 const QRectF bg = worldRectOfBackground(); if (!bg.isNull()) { const QRectF moved = transformedRectByScaleAndTranslate( m_dragPreviewActive ? m_dragRectBase : ent.rect, m_dragPreviewActive ? m_dragCentroidBase : ent.rect.center(), m_dragPreviewActive ? m_dragScaleRatio : 1.0, (m_dragPreviewActive ? m_dragDelta : QPointF(0, 0)) + delta); QPointF d = delta; if (moved.left() < bg.left()) d.setX(d.x() + (bg.left() - moved.left())); if (moved.top() < bg.top()) d.setY(d.y() + (bg.top() - moved.top())); if (moved.right() > bg.right()) d.setX(d.x() - (moved.right() - bg.right())); if (moved.bottom() > bg.bottom()) d.setY(d.y() - (moved.bottom() - bg.bottom())); delta = d; } // 轻量:仅更新增量参数与原点,不逐点修改 polygonWorld if (m_dragPreviewActive) { m_dragDelta += delta; } ent.animatedOriginWorld += delta; // 拖动中实时按深度图更新 z 与距离缩放(近大远小) // 但不必每个鼠标事件都重算:位置平移满帧跟手;深度/距离缩放重算较重,节流到 ~30Hz。 const qint64 nowMs = m_previewEmitTimer.elapsed(); if (nowMs - m_lastDepthScaleRecalcMs >= 33) { m_lastDepthScaleRecalcMs = nowMs; if (!m_depthAbsPath.isEmpty() && m_depthDirty) { m_depthDirty = false; m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } if (!m_depthImage8.isNull()) { const QPointF c = m_dragPreviewActive ? (m_dragCentroidBase + m_dragDelta) : shapeCentroidWorld(ent.polygonWorld, ent.rect); const int depthZ = sampleDepthAtPoint(m_depthImage8, c); ent.depth = depthZ; const double ds01 = depthToScale01(depthZ); ent.animatedDepthScale01 = ds01; const double newScale = (ent.ignoreDistanceScale ? 1.0 : distanceScaleFromDepth01(ds01, ent.distanceScaleCalibMult)) * ent.userScale; ent.visualScale = newScale; if (m_dragPreviewActive) { m_dragScaleRatio = std::clamp(newScale / std::max(1e-6, m_dragScaleBase), 0.02, static_cast(kViewScaleMax)); } } } // 低频预览信号:用于属性面板同步,不做树联动等重操作 // 属性面板/状态预览:按 30Hz 刷新即可,拖动视觉仍满帧跟手 if (nowMs - m_lastPreviewEmitMs >= 33) { m_lastPreviewEmitMs = nowMs; const QPointF previewCenter = m_dragPreviewActive ? (m_dragCentroidBase + m_dragDelta) : shapeCentroidWorld(ent.polygonWorld, ent.rect); emit selectedEntityPreviewChanged(ent.id, ent.depth, previewCenter); } update(); return; } if (m_draggingTool && m_selectedTool >= 0 && m_selectedTool < m_tools.size()) { const QPointF worldPos = viewToWorld(cur); const QPointF newOrigin = worldPos - m_toolDragOffsetOriginWorld; QPointF delta = newOrigin - m_tools[m_selectedTool].tool.originWorld; m_tools[m_selectedTool].tool.originWorld += delta; emit selectedToolChanged(true, m_tools[m_selectedTool].tool.id, m_tools[m_selectedTool].tool.originWorld); update(); return; } // 平移画布 if (m_tool == Tool::Move || (e->buttons() & Qt::MiddleButton) || (m_presentationPreviewMode && (e->buttons() & Qt::LeftButton))) { if (!(m_presentationPreviewMode && m_previewCameraViewLocked)) { if (m_presentationPreviewMode && m_presBgPanSession) { m_presBgDragDist += std::abs(deltaView.x()) + std::abs(deltaView.y()); } m_pan += deltaView; } update(); return; } } void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) { if (m_blackholeCopyResolveActive && e->button() == Qt::LeftButton) { if (m_blackholeCopyDragging) { m_blackholeCopyDragging = false; const QPointF delta = m_blackholeCopySourceRect.topLeft() - m_blackholeCopyHoleRect.topLeft(); emit requestResolveBlackholeCopy( m_blackholeCopyEntityId, QPoint(static_cast(std::round(delta.x())), static_cast(std::round(delta.y())))); } cancelBlackholeCopyResolve(); e->accept(); return; } if (e->button() == Qt::LeftButton || e->button() == Qt::MiddleButton) { if (m_presentationPreviewMode && e->button() == Qt::LeftButton) { if (m_presBgPanSession && m_presBgDragDist < 8.0) { clearPresentationEntityFocus(); } m_presBgPanSession = false; m_presBgDragDist = 0.0; } if (m_tool == Tool::CreateEntity && e->button() == Qt::LeftButton && m_drawingEntity) { m_dragging = false; m_drawingEntity = false; updateCursor(); if (m_entityCreateSegmentMode == EntityCreateSegmentMode::Manual) { if (m_strokeWorld.size() >= kMinStrokePointsManual) { setPendingEntityPolygonWorld(m_strokeWorld); } } else if (m_entityCreateSegmentMode == EntityCreateSegmentMode::Snap) { if (m_strokeWorld.size() >= kMinStrokePointsManual) { const QRectF polyBr = QPolygonF(m_strokeWorld).boundingRect(); QRect cropWorld; QImage crop; if (loadRgbCropForEntitySegmentStroke(polyBr, cropWorld, crop)) { const QImage gray = crop.convertToFormat(QImage::Format_Grayscale8); const QVector snapped = snapStrokeToEdgesWorld(m_strokeWorld, gray, cropWorld.topLeft(), cropWorld.size(), 6.0); setPendingEntityPolygonWorld(snapped); } } } else if (m_strokeWorld.size() >= kMinStrokePointsSam) { const QRectF polyBr = QPolygonF(m_strokeWorld).boundingRect(); QRect cropWorld; QImage crop; QByteArray cropPng; QByteArray ovPng; QPointF cropOrigin; QJsonArray pts; QJsonArray labs; QJsonArray box; if (loadRgbCropForEntitySegmentStroke(polyBr, cropWorld, crop) && buildSamSegmentPayloadFromStrokeMapped(m_strokeWorld, crop, cropWorld, cropPng, ovPng, cropOrigin, pts, labs, box)) { emit requestSamSegment(cropPng, ovPng, cropOrigin, pts, labs, box); } } m_strokeWorld.clear(); update(); return; } if (m_draggingEntity && m_selectedEntity >= 0 && m_selectedEntity < m_entities.size() && e->button() == Qt::LeftButton) { const auto& ent = m_entities[m_selectedEntity]; // 关键:提交“平移量”必须与缩放无关。 // 拖动过程中可能实时按深度重算 visualScale(导致 rect/topLeft 随缩放变化), // 因此不能用 rect.topLeft 差值作为移动 delta,否则松手会错位。 const QPointF endCentroid = m_dragPreviewActive ? (m_dragCentroidBase + m_dragDelta) : shapeCentroidWorld(ent.polygonWorld, ent.rect); const QPointF delta = endCentroid - m_entityDragStartCentroidWorld; bool sentMove = false; if (!ent.id.isEmpty() && (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y()))) { emit requestMoveEntity(ent.id, delta); sentMove = true; } if (!sentMove && !ent.id.isEmpty()) { const QPointF origin = ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld); emit selectedEntityChanged(true, ent.id, ent.depth, origin); } } if (m_draggingTool && m_selectedTool >= 0 && m_selectedTool < m_tools.size() && e->button() == Qt::LeftButton) { const auto& tv = m_tools[m_selectedTool]; const QPointF delta = tv.tool.originWorld - m_toolDragStartOriginWorld; if (!tv.tool.id.isEmpty() && (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y()))) { emit requestMoveTool(tv.tool.id, delta); } else if (!tv.tool.id.isEmpty()) { emit selectedToolChanged(true, tv.tool.id, tv.tool.originWorld); } } if (m_draggingCamera && m_selectedCameraIndex >= 0 && m_selectedCameraIndex < m_cameraOverlays.size() && e->button() == Qt::LeftButton) { const auto& cam = m_cameraOverlays[m_selectedCameraIndex]; const QPointF delta = cam.centerWorld - m_cameraDragStartCenterWorld; if (!cam.id.isEmpty() && (!qFuzzyIsNull(delta.x()) || !qFuzzyIsNull(delta.y()))) { emit requestMoveCamera(cam.id, delta); } else if (!cam.id.isEmpty()) { emit selectedCameraChanged(true, cam.id, cam.centerWorld, cam.viewScale); } } if (m_draggingHotspotIndex >= 0 && e->button() == Qt::LeftButton && m_draggingHotspotIndex < m_presentationHotspots.size()) { const auto& h = m_presentationHotspots[m_draggingHotspotIndex]; emit presentationHotspotMoved(h.id, h.centerWorld); m_draggingHotspotIndex = -1; } m_dragging = false; if (m_pendingDragging && e->button() == Qt::LeftButton) { m_pendingDragging = false; m_pendingDragWhole = false; m_pendingDragVertex = -1; } if (m_draggingEntity) { emit entityDragActiveChanged(false); } m_draggingEntity = false; m_draggingTool = false; m_draggingCamera = false; m_dragPreviewActive = false; m_dragMode = DragMode::None; updateCursor(); } QWidget::mouseReleaseEvent(e); } void EditorCanvas::wheelEvent(QWheelEvent* e) { if (m_presentationPreviewMode && m_previewCameraViewLocked) { e->accept(); return; } if (!m_presentationPreviewMode && !m_selectedCameraId.isEmpty() && m_tool == Tool::Move) { const qreal steps = e->angleDelta().y() / 120.0; const qreal factor = std::pow(1.15, steps); if (!qFuzzyCompare(factor, 1.0)) { emit requestCameraViewScaleAdjust(m_selectedCameraId, factor); } e->accept(); return; } if (m_tool != Tool::Zoom && !(e->modifiers() & Qt::ControlModifier)) { // 默认仍允许滚轮缩放:不强制用户切换工具 //(若你希望仅在 Zoom 工具下才缩放,可在此 return) } const QPointF cursorView = e->position(); const QPointF beforeWorld = viewToWorld(cursorView); // 约定:滚轮一步约 15°,这里做平滑指数缩放 const qreal steps = e->angleDelta().y() / 120.0; const qreal factor = std::pow(1.15, steps); const qreal newScale = std::clamp(m_scale * factor, kViewScaleMin, kViewScaleMax); if (qFuzzyCompare(newScale, m_scale)) { return; } m_scale = newScale; // 让“光标指向的 world 点”缩放后仍落在光标处 const QPointF afterView = worldToView(beforeWorld); m_pan += (cursorView - afterView); invalidateViewportLod(); update(); e->accept(); } void EditorCanvas::keyPressEvent(QKeyEvent* e) { if (m_blackholeCopyResolveActive && e->key() == Qt::Key_Escape) { cancelBlackholeCopyResolve(); e->accept(); return; } if (m_presentationPreviewMode && e->key() == Qt::Key_Escape) { clearPresentationEntityFocus(); e->accept(); return; } if (!m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) { if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) { emit requestFinalizePendingEntity(m_pendingPolyWorld); e->accept(); return; } if (e->key() == Qt::Key_Escape) { clearPendingEntityPolygon(); e->accept(); return; } } QWidget::keyPressEvent(e); }