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