添加模型分割
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
#include "editor/EditorCanvas.h"
|
||||
#include "editor/EntityCutoutUtils.h"
|
||||
|
||||
#include "core/animation/AnimationSampling.h"
|
||||
#include "core/depth/DepthService.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <QBuffer>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QCursor>
|
||||
@@ -14,9 +16,167 @@
|
||||
#include <QPainterPath>
|
||||
#include <QWheelEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QJsonArray>
|
||||
#include <QIODevice>
|
||||
#include <QPen>
|
||||
#include <QPolygonF>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kSamCropMargin = 32;
|
||||
constexpr int kMinStrokePointsSam = 4;
|
||||
constexpr int kMinStrokePointsManual = 8;
|
||||
constexpr int kMaxSamPointPrompts = 32;
|
||||
|
||||
QVector<QPointF> snapStrokeToEdges(const QVector<QPointF>& strokeWorld, const QImage& bgImage, int searchRadiusPx) {
|
||||
if (strokeWorld.size() < 3 || bgImage.isNull()) {
|
||||
return strokeWorld;
|
||||
}
|
||||
QImage gray = bgImage.convertToFormat(QImage::Format_Grayscale8);
|
||||
const int w = gray.width();
|
||||
const int h = gray.height();
|
||||
auto at = [&](int x, int y) -> int {
|
||||
x = std::clamp(x, 0, w - 1);
|
||||
y = std::clamp(y, 0, h - 1);
|
||||
return static_cast<int>(static_cast<const uchar*>(gray.constScanLine(y))[x]);
|
||||
};
|
||||
auto gradMag = [&](int x, int y) -> int {
|
||||
// 简易 Sobel 近似(整数)
|
||||
const int gx =
|
||||
-at(x - 1, y - 1) + at(x + 1, y - 1) +
|
||||
-2 * at(x - 1, y) + 2 * at(x + 1, y) +
|
||||
-at(x - 1, y + 1) + at(x + 1, y + 1);
|
||||
const int gy =
|
||||
-at(x - 1, y - 1) - 2 * at(x, y - 1) - at(x + 1, y - 1) +
|
||||
at(x - 1, y + 1) + 2 * at(x, y + 1) + at(x + 1, y + 1);
|
||||
return std::abs(gx) + std::abs(gy);
|
||||
};
|
||||
QVector<QPointF> out;
|
||||
out.reserve(strokeWorld.size());
|
||||
const int r = std::max(1, searchRadiusPx);
|
||||
for (const QPointF& p : strokeWorld) {
|
||||
const int cx = static_cast<int>(std::round(p.x()));
|
||||
const int cy = static_cast<int>(std::round(p.y()));
|
||||
int bestX = cx;
|
||||
int bestY = cy;
|
||||
int bestG = -1;
|
||||
for (int dy = -r; dy <= r; ++dy) {
|
||||
for (int dx = -r; dx <= r; ++dx) {
|
||||
const int x = cx + dx;
|
||||
const int y = cy + dy;
|
||||
if (x < 0 || y < 0 || x >= w || y >= h) continue;
|
||||
const int g = gradMag(x, y);
|
||||
if (g > bestG) {
|
||||
bestG = g;
|
||||
bestX = x;
|
||||
bestY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
out.push_back(QPointF(bestX, bestY));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool buildSamSegmentPayloadFromStroke(
|
||||
const QVector<QPointF>& strokeWorld,
|
||||
const QImage& bgImage,
|
||||
QByteArray& outCropPng,
|
||||
QByteArray& outOverlayPng,
|
||||
QPointF& outCropTopLeftWorld,
|
||||
QJsonArray& outPointCoords,
|
||||
QJsonArray& outPointLabels,
|
||||
QJsonArray& outBoxXyxy
|
||||
) {
|
||||
if (strokeWorld.size() < kMinStrokePointsSam || bgImage.isNull()) {
|
||||
return false;
|
||||
}
|
||||
const QRectF polyBr = QPolygonF(strokeWorld).boundingRect();
|
||||
if (polyBr.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const QRect cropRect = entity_cutout::clampRectToImage(
|
||||
polyBr.adjusted(-kSamCropMargin, -kSamCropMargin, kSamCropMargin, kSamCropMargin).toAlignedRect(),
|
||||
bgImage.size());
|
||||
if (cropRect.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
outCropTopLeftWorld = cropRect.topLeft();
|
||||
|
||||
const QImage cropRgb = bgImage.copy(cropRect).convertToFormat(QImage::Format_RGB888);
|
||||
QBuffer bufCrop(&outCropPng);
|
||||
if (!bufCrop.open(QIODevice::WriteOnly) || !cropRgb.save(&bufCrop, "PNG")) {
|
||||
outCropPng.clear();
|
||||
return false;
|
||||
}
|
||||
bufCrop.close();
|
||||
|
||||
const QPointF origin = cropRect.topLeft();
|
||||
const int cw = cropRect.width();
|
||||
const int ch = cropRect.height();
|
||||
|
||||
QImage overlay(cw, ch, QImage::Format_ARGB32_Premultiplied);
|
||||
overlay.fill(Qt::transparent);
|
||||
{
|
||||
QPainter pop(&overlay);
|
||||
pop.setRenderHint(QPainter::Antialiasing, true);
|
||||
QPen pen(QColor(255, 60, 60, 240));
|
||||
pen.setWidthF(4.0);
|
||||
pen.setCapStyle(Qt::RoundCap);
|
||||
pen.setJoinStyle(Qt::RoundJoin);
|
||||
pop.setPen(pen);
|
||||
QPolygonF local;
|
||||
local.reserve(strokeWorld.size());
|
||||
for (const QPointF& w : strokeWorld) {
|
||||
local.append(w - origin);
|
||||
}
|
||||
pop.drawPolyline(local);
|
||||
}
|
||||
QBuffer bufOv(&outOverlayPng);
|
||||
if (!bufOv.open(QIODevice::WriteOnly) || !overlay.save(&bufOv, "PNG")) {
|
||||
outOverlayPng.clear();
|
||||
return false;
|
||||
}
|
||||
bufOv.close();
|
||||
|
||||
const int n = static_cast<int>(strokeWorld.size());
|
||||
const int step = std::max(1, (n + kMaxSamPointPrompts - 1) / kMaxSamPointPrompts);
|
||||
for (int i = 0; i < n; i += step) {
|
||||
const QPointF L = strokeWorld[i] - origin;
|
||||
outPointCoords.append(QJsonArray{L.x(), L.y()});
|
||||
outPointLabels.append(1);
|
||||
}
|
||||
if (n > 1) {
|
||||
const QPointF Llast = strokeWorld.last() - origin;
|
||||
if (!outPointCoords.isEmpty()) {
|
||||
const QJsonArray prev = outPointCoords.last().toArray();
|
||||
if (prev.size() >= 2) {
|
||||
const double dx = Llast.x() - prev.at(0).toDouble();
|
||||
const double dy = Llast.y() - prev.at(1).toDouble();
|
||||
if (dx * dx + dy * dy > 4.0) {
|
||||
outPointCoords.append(QJsonArray{Llast.x(), Llast.y()});
|
||||
outPointLabels.append(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const QRectF tight = QPolygonF(strokeWorld).boundingRect();
|
||||
auto clampD = [](double v, double lo, double hi) { return std::clamp(v, lo, hi); };
|
||||
double x1 = clampD(tight.left() - origin.x(), 0.0, static_cast<double>(cw - 1));
|
||||
double y1 = clampD(tight.top() - origin.y(), 0.0, static_cast<double>(ch - 1));
|
||||
double x2 = clampD(tight.right() - origin.x(), 0.0, static_cast<double>(cw - 1));
|
||||
double y2 = clampD(tight.bottom() - origin.y(), 0.0, static_cast<double>(ch - 1));
|
||||
if (x2 <= x1) {
|
||||
x2 = std::min(static_cast<double>(cw - 1), x1 + 1.0);
|
||||
}
|
||||
if (y2 <= y1) {
|
||||
y2 = std::min(static_cast<double>(ch - 1), y1 + 1.0);
|
||||
}
|
||||
outBoxXyxy = QJsonArray{x1, y1, x2, y2};
|
||||
return true;
|
||||
}
|
||||
|
||||
void drawCheckerboard(QPainter& p, const QRect& r) {
|
||||
// 轻量级棋盘格,让透明/纯色背景也有参照
|
||||
const int cell = 16;
|
||||
@@ -45,41 +205,6 @@ void drawGrid(QPainter& p, const QRect& r) {
|
||||
}
|
||||
}
|
||||
|
||||
QPainterPath pathFromWorldPolygon(const QVector<QPointF>& poly) {
|
||||
QPainterPath path;
|
||||
if (poly.size() < 3) {
|
||||
return path;
|
||||
}
|
||||
path.addPolygon(QPolygonF(poly));
|
||||
path.closeSubpath();
|
||||
return path;
|
||||
}
|
||||
|
||||
QPointF polygonCentroid(const QVector<QPointF>& poly) {
|
||||
// 简单多边形质心(面积加权)。若面积接近 0,则退化为包围盒中心。
|
||||
if (poly.size() < 3) {
|
||||
return {};
|
||||
}
|
||||
double a2 = 0.0; // 2*area
|
||||
double cx6a = 0.0;
|
||||
double cy6a = 0.0;
|
||||
for (int i = 0; i < poly.size(); ++i) {
|
||||
const QPointF p0 = poly[i];
|
||||
const QPointF p1 = poly[(i + 1) % poly.size()];
|
||||
const double cross = static_cast<double>(p0.x()) * static_cast<double>(p1.y()) -
|
||||
static_cast<double>(p1.x()) * static_cast<double>(p0.y());
|
||||
a2 += cross;
|
||||
cx6a += (static_cast<double>(p0.x()) + static_cast<double>(p1.x())) * cross;
|
||||
cy6a += (static_cast<double>(p0.y()) + static_cast<double>(p1.y())) * cross;
|
||||
}
|
||||
if (std::abs(a2) < 1e-6) {
|
||||
const QRectF bb = pathFromWorldPolygon(poly).boundingRect();
|
||||
return bb.center();
|
||||
}
|
||||
const double inv6a = 1.0 / (3.0 * a2); // 1/(6A) with a2=2A -> 1/(3*a2)
|
||||
return QPointF(cx6a * inv6a, cy6a * inv6a);
|
||||
}
|
||||
|
||||
QRectF transformedRectByScaleAndTranslate(const QRectF& r, const QPointF& center, double scaleRatio, const QPointF& delta) {
|
||||
if (r.isNull()) {
|
||||
return r.translated(delta);
|
||||
@@ -99,53 +224,6 @@ QRectF transformedRectByScaleAndTranslate(const QRectF& r, const QPointF& center
|
||||
return QRectF(QPointF(minX, minY), QPointF(maxX, maxY));
|
||||
}
|
||||
|
||||
QRect clampRectToImage(const QRect& r, const QSize& size) {
|
||||
QRect out = r.normalized();
|
||||
if (out.isNull()) {
|
||||
return {};
|
||||
}
|
||||
out.setLeft(std::max(0, out.left()));
|
||||
out.setTop(std::max(0, out.top()));
|
||||
out.setRight(std::min(size.width() - 1, out.right()));
|
||||
out.setBottom(std::min(size.height() - 1, out.bottom()));
|
||||
if (out.width() <= 0 || out.height() <= 0) {
|
||||
return {};
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
int computeAverageDepthInPolygon(const QImage& depth8, const QVector<QPointF>& polyWorld) {
|
||||
if (depth8.isNull() || depth8.format() != QImage::Format_Grayscale8 || polyWorld.size() < 3) {
|
||||
return 0;
|
||||
}
|
||||
const QPainterPath path = pathFromWorldPolygon(polyWorld);
|
||||
if (path.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
const QRect bbox = clampRectToImage(path.boundingRect().toAlignedRect(), depth8.size());
|
||||
if (bbox.isNull()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
quint64 sum = 0;
|
||||
quint64 cnt = 0;
|
||||
for (int y = bbox.top(); y <= bbox.bottom(); ++y) {
|
||||
const uchar* row = depth8.constScanLine(y);
|
||||
for (int x = bbox.left(); x <= bbox.right(); ++x) {
|
||||
// 取像素中心点做包含测试
|
||||
if (!path.contains(QPointF(x + 0.5, y + 0.5))) {
|
||||
continue;
|
||||
}
|
||||
sum += static_cast<quint64>(row[x]);
|
||||
++cnt;
|
||||
}
|
||||
}
|
||||
if (cnt == 0) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int>(sum / cnt);
|
||||
}
|
||||
|
||||
int sampleDepthAtPoint(const QImage& depth8, const QPointF& worldPos) {
|
||||
if (depth8.isNull() || depth8.format() != QImage::Format_Grayscale8) {
|
||||
return 0;
|
||||
@@ -174,56 +252,6 @@ double distanceScaleFromDepth01(double depth01, double calibMult) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
QImage extractEntityImage(const QImage& bg, const QVector<QPointF>& polyWorld, QPointF& outTopLeftWorld) {
|
||||
if (bg.isNull() || polyWorld.size() < 3) {
|
||||
outTopLeftWorld = {};
|
||||
return {};
|
||||
}
|
||||
const QPainterPath path = pathFromWorldPolygon(polyWorld);
|
||||
if (path.isEmpty()) {
|
||||
outTopLeftWorld = {};
|
||||
return {};
|
||||
}
|
||||
const QRect bbox = clampRectToImage(path.boundingRect().toAlignedRect(), bg.size());
|
||||
if (bbox.isNull()) {
|
||||
outTopLeftWorld = {};
|
||||
return {};
|
||||
}
|
||||
|
||||
outTopLeftWorld = bbox.topLeft();
|
||||
|
||||
QImage out(bbox.size(), QImage::Format_ARGB32_Premultiplied);
|
||||
out.fill(Qt::transparent);
|
||||
QPainter p(&out);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
// 把 path 平移到裁剪图坐标系
|
||||
QTransform tr;
|
||||
tr.translate(-bbox.left(), -bbox.top());
|
||||
const QPainterPath localPath = tr.map(path);
|
||||
|
||||
p.setClipPath(localPath);
|
||||
p.drawImage(QPoint(0, 0), bg, bbox);
|
||||
p.end();
|
||||
return out;
|
||||
}
|
||||
|
||||
void applyBlackFillToBackground(QImage& bgCutout, const QVector<QPointF>& polyWorld) {
|
||||
if (bgCutout.isNull() || polyWorld.size() < 3) {
|
||||
return;
|
||||
}
|
||||
QPainterPath path = pathFromWorldPolygon(polyWorld);
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QPainter p(&bgCutout);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(QColor(0, 0, 0, 255));
|
||||
p.drawPath(path);
|
||||
p.end();
|
||||
}
|
||||
|
||||
struct GizmoHit {
|
||||
EditorCanvas::DragMode mode = EditorCanvas::DragMode::None;
|
||||
};
|
||||
@@ -333,7 +361,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
|
||||
for (const auto& lp : e.polygonLocal) {
|
||||
polyTmp.push_back(originWorld + lp);
|
||||
}
|
||||
const QPointF cTmp = polyTmp.isEmpty() ? originWorld : polygonCentroid(polyTmp);
|
||||
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;
|
||||
|
||||
@@ -354,7 +382,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
|
||||
|
||||
// 贴图按 origin 缩放
|
||||
v.imageTopLeft = originWorld + (e.imageTopLeftWorld - e.originWorld) * scale;
|
||||
v.pathWorld = pathFromWorldPolygon(v.polygonWorld);
|
||||
v.pathWorld = entity_cutout::pathFromWorldPolygon(v.polygonWorld);
|
||||
v.rect = v.pathWorld.boundingRect();
|
||||
v.color = QColor(255, 120, 0, 70);
|
||||
|
||||
@@ -394,7 +422,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
|
||||
if (m_selectedEntity >= 0) {
|
||||
const auto& ent = m_entities[m_selectedEntity];
|
||||
const QPointF origin =
|
||||
ent.polygonWorld.isEmpty() ? ent.rect.center() : polygonCentroid(ent.polygonWorld);
|
||||
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());
|
||||
@@ -437,7 +465,7 @@ QPointF EditorCanvas::selectedEntityCentroidWorld() const {
|
||||
}
|
||||
const auto& ent = m_entities[m_selectedEntity];
|
||||
if (!ent.polygonWorld.isEmpty()) {
|
||||
return polygonCentroid(ent.polygonWorld);
|
||||
return entity_cutout::polygonCentroid(ent.polygonWorld);
|
||||
}
|
||||
return ent.rect.center();
|
||||
}
|
||||
@@ -500,7 +528,7 @@ void EditorCanvas::presentationComputeZoomTarget(int entityIndex, QPointF* outPa
|
||||
const QSizeF sz(ent.image.width() * ent.visualScale, ent.image.height() * ent.visualScale);
|
||||
bb = QRectF(ent.imageTopLeft, sz);
|
||||
} else if (!ent.polygonWorld.isEmpty()) {
|
||||
bb = pathFromWorldPolygon(ent.polygonWorld).boundingRect();
|
||||
bb = entity_cutout::pathFromWorldPolygon(ent.polygonWorld).boundingRect();
|
||||
} else {
|
||||
bb = ent.rect;
|
||||
}
|
||||
@@ -575,7 +603,7 @@ void EditorCanvas::selectEntityById(const QString& id) {
|
||||
m_selectedEntity = i;
|
||||
const auto& ent = m_entities[i];
|
||||
const QPointF origin =
|
||||
ent.polygonWorld.isEmpty() ? ent.rect.center() : polygonCentroid(ent.polygonWorld);
|
||||
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
|
||||
emit selectedEntityChanged(true, ent.id, ent.depth, origin);
|
||||
update();
|
||||
return;
|
||||
@@ -635,6 +663,58 @@ void EditorCanvas::setTool(Tool tool) {
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setEntityCreateSegmentMode(EntityCreateSegmentMode m) {
|
||||
if (m_entityCreateSegmentMode == m) {
|
||||
return;
|
||||
}
|
||||
m_entityCreateSegmentMode = m;
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::setPendingEntityPolygonWorld(const QVector<QPointF>& polyWorld) {
|
||||
m_pendingPolyWorld = polyWorld;
|
||||
m_pendingDragging = false;
|
||||
m_pendingDragWhole = false;
|
||||
m_pendingDragVertex = -1;
|
||||
update();
|
||||
}
|
||||
|
||||
void EditorCanvas::clearPendingEntityPolygon() {
|
||||
m_pendingPolyWorld.clear();
|
||||
m_pendingDragging = false;
|
||||
m_pendingDragWhole = false;
|
||||
m_pendingDragVertex = -1;
|
||||
update();
|
||||
}
|
||||
|
||||
bool EditorCanvas::isPointNearPendingVertex(const QPointF& worldPos, int* outIndex) const {
|
||||
if (outIndex) *outIndex = -1;
|
||||
if (m_pendingPolyWorld.size() < 3) return false;
|
||||
const qreal rView = 10.0;
|
||||
const qreal rWorld = rView / std::max<qreal>(m_scale, 0.001);
|
||||
const qreal r2 = rWorld * rWorld;
|
||||
int best = -1;
|
||||
qreal bestD2 = r2;
|
||||
for (int i = 0; i < m_pendingPolyWorld.size(); ++i) {
|
||||
const QPointF d = m_pendingPolyWorld[i] - worldPos;
|
||||
const qreal d2 = d.x() * d.x() + d.y() * d.y();
|
||||
if (d2 <= bestD2) {
|
||||
bestD2 = d2;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
if (best >= 0) {
|
||||
if (outIndex) *outIndex = best;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EditorCanvas::pendingPolygonContains(const QPointF& worldPos) const {
|
||||
if (m_pendingPolyWorld.size() < 3) return false;
|
||||
return entity_cutout::pathFromWorldPolygon(m_pendingPolyWorld).contains(worldPos);
|
||||
}
|
||||
|
||||
void EditorCanvas::resetView() {
|
||||
m_scale = 1.0;
|
||||
m_pan = QPointF(0, 0);
|
||||
@@ -768,7 +848,7 @@ int EditorCanvas::hitTestEntity(const QPointF& worldPos) const {
|
||||
continue;
|
||||
}
|
||||
if (!ent.polygonWorld.isEmpty()) {
|
||||
const QPainterPath path = pathFromWorldPolygon(ent.polygonWorld);
|
||||
const QPainterPath path = entity_cutout::pathFromWorldPolygon(ent.polygonWorld);
|
||||
if (path.contains(worldPos)) {
|
||||
return static_cast<int>(idx);
|
||||
}
|
||||
@@ -829,7 +909,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
m_bgImageCutout = m_bgImage;
|
||||
for (const auto& ent : m_entities) {
|
||||
if (!ent.cutoutPolygonWorld.isEmpty() && !m_bgImageCutout.isNull()) {
|
||||
applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
|
||||
entity_cutout::applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1030,6 +1110,21 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
p.drawLine(m_strokeWorld.first(), m_strokeWorld.last());
|
||||
}
|
||||
|
||||
// 待确认实体:多边形预览 + 顶点
|
||||
if (!m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) {
|
||||
const qreal lw = 2.5 / std::max<qreal>(m_scale, 0.001);
|
||||
p.setPen(QPen(QColor(60, 180, 255, 230), lw));
|
||||
p.setBrush(QColor(60, 180, 255, 45));
|
||||
const QPainterPath path = entity_cutout::pathFromWorldPolygon(m_pendingPolyWorld);
|
||||
p.drawPath(path);
|
||||
const qreal vr = 5.0 / std::max<qreal>(m_scale, 0.001);
|
||||
p.setPen(QPen(QColor(0, 0, 0, 120), lw));
|
||||
p.setBrush(QColor(255, 255, 255, 220));
|
||||
for (const QPointF& v : m_pendingPolyWorld) {
|
||||
p.drawEllipse(v, vr, vr);
|
||||
}
|
||||
}
|
||||
|
||||
p.restore();
|
||||
|
||||
// 坐标轴/刻度:绘制在画布最外层,背景越界时贴边显示
|
||||
@@ -1090,7 +1185,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
if (isDragPreview) {
|
||||
originWorld = m_dragCentroidBase + m_dragDelta;
|
||||
} else if (!ent.polygonWorld.isEmpty()) {
|
||||
originWorld = polygonCentroid(ent.polygonWorld);
|
||||
originWorld = entity_cutout::polygonCentroid(ent.polygonWorld);
|
||||
}
|
||||
const QPointF originView = worldToView(originWorld);
|
||||
|
||||
@@ -1155,7 +1250,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
if (hit >= 0) {
|
||||
const auto& ent = m_entities[hit];
|
||||
const QPointF cWorld =
|
||||
ent.polygonWorld.isEmpty() ? ent.rect.center() : polygonCentroid(ent.polygonWorld);
|
||||
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
|
||||
const QPointF anchorView = worldToView(cWorld);
|
||||
beginPresentationZoomTowardEntity(hit);
|
||||
emit presentationEntityIntroRequested(ent.id, anchorView);
|
||||
@@ -1178,6 +1273,33 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1207,7 +1329,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
if (isDragPreview) {
|
||||
originWorld = m_dragCentroidBase + m_dragDelta;
|
||||
} else if (!ent.polygonWorld.isEmpty()) {
|
||||
originWorld = polygonCentroid(ent.polygonWorld);
|
||||
originWorld = entity_cutout::polygonCentroid(ent.polygonWorld);
|
||||
}
|
||||
const QPointF originView = worldToView(originWorld);
|
||||
const GizmoHit gh = hitTestGizmo(e->position(), originView);
|
||||
@@ -1220,7 +1342,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
|
||||
// 为了统一复用 move 逻辑:初始化 rect 与基准点
|
||||
const QRectF r = m_entities[m_selectedEntity].rect.isNull() && !m_entities[m_selectedEntity].polygonWorld.isEmpty()
|
||||
? pathFromWorldPolygon(m_entities[m_selectedEntity].polygonWorld).boundingRect()
|
||||
? 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;
|
||||
@@ -1236,7 +1358,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
m_dragPolyBase = m_entities[m_selectedEntity].polygonWorld;
|
||||
m_dragPathBase = m_entities[m_selectedEntity].pathWorld;
|
||||
m_dragCentroidBase =
|
||||
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : polygonCentroid(m_dragPolyBase);
|
||||
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase);
|
||||
update();
|
||||
return;
|
||||
}
|
||||
@@ -1249,7 +1371,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
m_dragMode = DragMode::Free;
|
||||
emit entityDragActiveChanged(true);
|
||||
const QRectF r = m_entities[hit].rect.isNull() && !m_entities[hit].polygonWorld.isEmpty()
|
||||
? pathFromWorldPolygon(m_entities[hit].polygonWorld).boundingRect()
|
||||
? entity_cutout::pathFromWorldPolygon(m_entities[hit].polygonWorld).boundingRect()
|
||||
: m_entities[hit].rect;
|
||||
m_entities[hit].rect = r;
|
||||
m_entityDragOffsetOriginWorld = worldPos - m_entities[hit].animatedOriginWorld;
|
||||
@@ -1265,8 +1387,8 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
m_dragPolyBase = m_entities[hit].polygonWorld;
|
||||
m_dragPathBase = m_entities[hit].pathWorld;
|
||||
m_dragCentroidBase =
|
||||
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : polygonCentroid(m_dragPolyBase);
|
||||
const QPointF origin = !m_entities[hit].polygonWorld.isEmpty() ? polygonCentroid(m_entities[hit].polygonWorld)
|
||||
m_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();
|
||||
@@ -1326,6 +1448,21 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
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()) {
|
||||
@@ -1442,40 +1579,27 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
m_drawingEntity = false;
|
||||
updateCursor();
|
||||
|
||||
if (m_strokeWorld.size() >= 8) {
|
||||
// 闭合:末点连回首点(绘制时 polygon 自动闭合)
|
||||
core::Project::Entity ent;
|
||||
// id 由上层(workspace/UI)统一分配,避免因过滤/隐藏导致重复
|
||||
ent.id.clear();
|
||||
ent.cutoutPolygonWorld = m_strokeWorld;
|
||||
ent.originWorld = polygonCentroid(ent.cutoutPolygonWorld);
|
||||
ent.polygonLocal.clear();
|
||||
ent.polygonLocal.reserve(ent.cutoutPolygonWorld.size());
|
||||
for (const auto& pt : ent.cutoutPolygonWorld) {
|
||||
ent.polygonLocal.push_back(pt - ent.originWorld);
|
||||
if (m_entityCreateSegmentMode == EntityCreateSegmentMode::Manual) {
|
||||
if (m_strokeWorld.size() >= kMinStrokePointsManual) {
|
||||
setPendingEntityPolygonWorld(m_strokeWorld);
|
||||
}
|
||||
|
||||
// 确保深度加载(用于平均深度)
|
||||
if (m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
} else if (m_entityCreateSegmentMode == EntityCreateSegmentMode::Snap) {
|
||||
if (m_strokeWorld.size() >= kMinStrokePointsManual) {
|
||||
ensurePixmapLoaded();
|
||||
const QVector<QPointF> snapped = snapStrokeToEdges(m_strokeWorld, m_bgImage, 6);
|
||||
setPendingEntityPolygonWorld(snapped);
|
||||
}
|
||||
const QPointF c = polygonCentroid(ent.cutoutPolygonWorld);
|
||||
ent.depth = m_depthImage8.isNull() ? 0 : sampleDepthAtPoint(m_depthImage8, c);
|
||||
{
|
||||
const double ds01 = depthToScale01(ent.depth);
|
||||
ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0;
|
||||
} 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);
|
||||
}
|
||||
|
||||
// 抠实体:从原背景抠出,背景对应区域填黑
|
||||
QImage cutout;
|
||||
if (!m_bgImage.isNull()) {
|
||||
QPointF topLeft;
|
||||
cutout = extractEntityImage(m_bgImage, ent.cutoutPolygonWorld, topLeft);
|
||||
ent.imageTopLeftWorld = topLeft;
|
||||
}
|
||||
emit requestAddEntity(ent, cutout);
|
||||
}
|
||||
m_strokeWorld.clear();
|
||||
update();
|
||||
@@ -1497,12 +1621,17 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
||||
}
|
||||
if (!sentMove && !ent.id.isEmpty()) {
|
||||
const QPointF origin =
|
||||
ent.polygonWorld.isEmpty() ? ent.rect.center() : polygonCentroid(ent.polygonWorld);
|
||||
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);
|
||||
}
|
||||
@@ -1546,6 +1675,18 @@ void EditorCanvas::keyPressEvent(QKeyEvent* e) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user