update
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QImageReader>
|
||||
#include <QMimeData>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDropEvent>
|
||||
@@ -37,6 +38,51 @@ constexpr int kMinStrokePointsSam = 4;
|
||||
constexpr int kMinStrokePointsManual = 8;
|
||||
constexpr int kMaxSamPointPrompts = 32;
|
||||
|
||||
static QImage readImageTolerant(const QString& absPath) {
|
||||
if (absPath.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Qt 默认限制常见为 256MB;超大分辨率背景/深度可能会被拒绝。
|
||||
QImageReader::setAllocationLimit(1024); // MB
|
||||
#endif
|
||||
QImageReader reader(absPath);
|
||||
reader.setAutoTransform(true);
|
||||
const QSize sz = reader.size();
|
||||
if (sz.isValid()) {
|
||||
// 防止极端大图导致内存占用爆炸:按像素数上限进行缩放读取。
|
||||
constexpr qint64 kMaxPixels = 160LL * 1000LL * 1000LL; // 160MP
|
||||
const qint64 pixels = qint64(sz.width()) * qint64(sz.height());
|
||||
if (pixels > kMaxPixels) {
|
||||
const double s = std::sqrt(double(kMaxPixels) / std::max<double>(1.0, double(pixels)));
|
||||
const int nw = std::max(1, int(std::lround(sz.width() * s)));
|
||||
const int nh = std::max(1, int(std::lround(sz.height() * s)));
|
||||
reader.setScaledSize(QSize(nw, nh));
|
||||
}
|
||||
}
|
||||
return reader.read();
|
||||
}
|
||||
|
||||
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<QPointF> snapStrokeToEdges(const QVector<QPointF>& strokeWorld, const QImage& bgImage, int searchRadiusPx) {
|
||||
if (strokeWorld.size() < 3 || bgImage.isNull()) {
|
||||
return strokeWorld;
|
||||
@@ -100,6 +146,12 @@ bool buildSamSegmentPayloadFromStroke(
|
||||
if (strokeWorld.size() < kMinStrokePointsSam || bgImage.isNull()) {
|
||||
return false;
|
||||
}
|
||||
outCropPng.clear();
|
||||
outOverlayPng.clear();
|
||||
outPointCoords = QJsonArray{};
|
||||
outPointLabels = QJsonArray{};
|
||||
outBoxXyxy = QJsonArray{};
|
||||
|
||||
const QRectF polyBr = QPolygonF(strokeWorld).boundingRect();
|
||||
if (polyBr.isEmpty()) {
|
||||
return false;
|
||||
@@ -148,30 +200,35 @@ bool buildSamSegmentPayloadFromStroke(
|
||||
}
|
||||
bufOv.close();
|
||||
|
||||
// 关键修复:
|
||||
// 用户“圈选”通常是在实体外侧画一圈。原实现把笔画点全当作前景点(1),
|
||||
// 会让 SAM 倾向于把圈线/裁剪边缘当成前景,从而出现“沿小块图像边缘贴边”的 mask。
|
||||
// 新策略:圈内给一个前景点(1),圈线采样一些背景点(0)抑制外侧区域。
|
||||
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 - origin;
|
||||
const double fgx = clampD(centerLocal.x(), 0.0, static_cast<double>(cw - 1));
|
||||
const double fgy = clampD(centerLocal.y(), 0.0, static_cast<double>(ch - 1));
|
||||
outPointCoords.append(QJsonArray{fgx, fgy});
|
||||
outPointLabels.append(1);
|
||||
|
||||
// 背景点:在圈线(polyline)上均匀采样(最多 kMaxSamPointPrompts-1 个)。
|
||||
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 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] - origin;
|
||||
const double bx = clampD(L.x(), 0.0, static_cast<double>(cw - 1));
|
||||
const double by = clampD(L.y(), 0.0, static_cast<double>(ch - 1));
|
||||
outPointCoords.append(QJsonArray{bx, by});
|
||||
outPointLabels.append(0);
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -479,9 +536,11 @@ void EditorCanvas::setPresentationPreviewMode(bool on) {
|
||||
m_presZoomFinishingRestore = false;
|
||||
m_presBgPanSession = false;
|
||||
m_presBgDragDist = 0.0;
|
||||
cancelBlackholeCopyResolve();
|
||||
if (on) {
|
||||
m_tool = Tool::Move;
|
||||
m_selectedEntity = -1;
|
||||
m_selectedBlackholeEntityId.clear();
|
||||
m_draggingEntity = false;
|
||||
m_drawingEntity = false;
|
||||
m_dragMode = DragMode::None;
|
||||
@@ -505,7 +564,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
||||
if (!m_depthAbsPath.isEmpty()) {
|
||||
if (m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
}
|
||||
@@ -521,6 +580,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
||||
const QPointF originWorld = e.originWorld;
|
||||
v.animatedOriginWorld = originWorld;
|
||||
v.cutoutPolygonWorld = e.cutoutPolygonWorld;
|
||||
v.blackholeVisible = e.blackholeVisible;
|
||||
v.distanceScaleCalibMult = e.distanceScaleCalibMult;
|
||||
v.ignoreDistanceScale = e.ignoreDistanceScale;
|
||||
|
||||
@@ -597,6 +657,31 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
|
||||
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();
|
||||
}
|
||||
@@ -815,10 +900,119 @@ void EditorCanvas::selectEntityById(const QString& id) {
|
||||
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();
|
||||
if (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);
|
||||
}
|
||||
}
|
||||
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<qreal>(24.0, holeRect.width() * 0.6);
|
||||
srcRect.translate(shift, 0.0);
|
||||
srcRect = clampRectTopLeftToBounds(srcRect, bg);
|
||||
|
||||
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_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::setBackgroundImagePath(const QString& absolutePath) {
|
||||
if (m_bgAbsPath == absolutePath) {
|
||||
return;
|
||||
}
|
||||
cancelBlackholeCopyResolve();
|
||||
m_bgAbsPath = absolutePath;
|
||||
invalidatePixmap();
|
||||
m_bgImageDirty = true;
|
||||
@@ -997,13 +1191,21 @@ void EditorCanvas::ensurePixmapLoaded() const {
|
||||
m_pixmapDirty = false;
|
||||
m_bgPixmap = QPixmap();
|
||||
if (!m_bgAbsPath.isEmpty()) {
|
||||
m_bgPixmap = QPixmap(m_bgAbsPath);
|
||||
// 避免直接 QPixmap(path) 走默认 imageio 限制(超大图可能被 256MB 上限拒绝)
|
||||
const QImage img = readImageTolerant(m_bgAbsPath);
|
||||
if (!img.isNull()) {
|
||||
m_bgPixmap = QPixmap::fromImage(img);
|
||||
}
|
||||
}
|
||||
m_bgImageDirty = true;
|
||||
m_bgCutoutDirty = true;
|
||||
}
|
||||
|
||||
void EditorCanvas::updateCursor() {
|
||||
if (m_blackholeCopyResolveActive) {
|
||||
setCursor(m_blackholeCopyDragging ? Qt::ClosedHandCursor : Qt::OpenHandCursor);
|
||||
return;
|
||||
}
|
||||
if (m_presentationPreviewMode) {
|
||||
if (m_presHoverEntityIndex >= 0) {
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
@@ -1105,7 +1307,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
if (showBg) {
|
||||
if (m_bgImageDirty) {
|
||||
m_bgImageDirty = false;
|
||||
m_bgImage = QImage(m_bgAbsPath);
|
||||
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);
|
||||
}
|
||||
@@ -1115,7 +1317,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
m_bgCutoutDirty = false;
|
||||
m_bgImageCutout = m_bgImage;
|
||||
for (const auto& ent : m_entities) {
|
||||
if (ent.opacity > 0.001 && !ent.cutoutPolygonWorld.isEmpty() && !m_bgImageCutout.isNull()) {
|
||||
if (ent.blackholeVisible && !ent.cutoutPolygonWorld.isEmpty() && !m_bgImageCutout.isNull()) {
|
||||
entity_cutout::applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
|
||||
}
|
||||
}
|
||||
@@ -1156,7 +1358,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
if (wantDepth) {
|
||||
if (m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
if (!m_depthImage8.isNull()) {
|
||||
@@ -1173,6 +1375,48 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_presentationPreviewMode && m_blackholeCopyResolveActive &&
|
||||
!m_blackholeCopyHoleRect.isNull() && !m_blackholeCopySourceRect.isNull()) {
|
||||
if (!m_bgImage.isNull()) {
|
||||
const QRect srcRect = m_blackholeCopySourceRect.toAlignedRect();
|
||||
const QRect dstRect = m_blackholeCopyHoleRect.toAlignedRect();
|
||||
if (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);
|
||||
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<qreal>(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<qreal>(m_scale, 0.001));
|
||||
p.setPen(srcPen);
|
||||
p.drawRect(m_blackholeCopySourceRect);
|
||||
|
||||
const QPen textPen(QColor(70, 200, 255, 230));
|
||||
p.setPen(textPen);
|
||||
const QPointF textPos = m_blackholeCopySourceRect.topLeft() +
|
||||
QPointF(4.0 / std::max<qreal>(m_scale, 0.001),
|
||||
14.0 / std::max<qreal>(m_scale, 0.001));
|
||||
p.drawText(textPos, QStringLiteral("拖动取样框,松手应用"));
|
||||
}
|
||||
|
||||
// 实体元素(占位):后续可替换为真实数据
|
||||
for (int i = 0; i < m_entities.size(); ++i) {
|
||||
const auto& ent = m_entities[i];
|
||||
@@ -1280,6 +1524,13 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
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<qreal>(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);
|
||||
@@ -1398,7 +1649,47 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
p.drawLine(QPointF(0, axisY), QPointF(width(), axisY)); // X 轴(水平)
|
||||
p.drawLine(QPointF(axisX, 0), QPointF(axisX, height())); // Y 轴(垂直)
|
||||
|
||||
const int step = 100;
|
||||
// 根据缩放与视口大小动态调整刻度密度:使相邻刻度在屏幕上保持“够密但不挤”的间距
|
||||
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<double>(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<double, double> {
|
||||
const QPointF w0 = viewToWorld(QPointF(0, 0));
|
||||
const QPointF w1 = viewToWorld(QPointF(width(), height()));
|
||||
double a = std::min<double>(w0.x(), w1.x());
|
||||
double b = std::max<double>(w0.x(), w1.x());
|
||||
a = std::clamp(a, 0.0, double(m_bgPixmap.width()));
|
||||
b = std::clamp(b, 0.0, double(m_bgPixmap.width()));
|
||||
return {a, b};
|
||||
};
|
||||
auto visibleWorldYRange = [&]() -> std::pair<double, double> {
|
||||
const QPointF w0 = viewToWorld(QPointF(0, 0));
|
||||
const QPointF w1 = viewToWorld(QPointF(width(), height()));
|
||||
double a = std::min<double>(w0.y(), w1.y());
|
||||
double b = std::max<double>(w0.y(), w1.y());
|
||||
a = std::clamp(a, 0.0, double(m_bgPixmap.height()));
|
||||
b = std::clamp(b, 0.0, double(m_bgPixmap.height()));
|
||||
return {a, b};
|
||||
};
|
||||
|
||||
QPen tickPen(QColor(20, 20, 20, 140));
|
||||
tickPen.setWidth(1);
|
||||
p.setPen(tickPen);
|
||||
@@ -1409,28 +1700,44 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
||||
}
|
||||
|
||||
// X 轴:用 y=0 的世界线映射到 view-x,并把刻度画在 axisY 上
|
||||
for (int x = 0; x <= m_bgPixmap.width(); x += step) {
|
||||
const QPointF vx = worldToView(QPointF(x, 0));
|
||||
{
|
||||
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(m_bgPixmap.width()));
|
||||
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<qreal>(width()));
|
||||
p.drawLine(QPointF(tx, axisY), QPointF(tx, axisY + 6));
|
||||
if (m_axisLabelsVisible) {
|
||||
p.drawText(QPointF(tx + 2, axisY + 18), QString::number(x));
|
||||
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 上
|
||||
for (int y = 0; y <= m_bgPixmap.height(); y += step) {
|
||||
const QPointF vy = worldToView(QPointF(0, y));
|
||||
{
|
||||
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(m_bgPixmap.height()));
|
||||
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<qreal>(height()));
|
||||
p.drawLine(QPointF(axisX, ty), QPointF(axisX + 6, ty));
|
||||
if (m_axisLabelsVisible) {
|
||||
p.drawText(QPointF(axisX + 10, ty - 2), QString::number(y));
|
||||
if (labelEvery <= 1 || (iTick % labelEvery) == 0) {
|
||||
p.drawText(QPointF(axisX + 10, ty - 2), QString::number(int(std::lround(yc))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1489,7 +1796,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
if (!m_depthAbsPath.isEmpty()) {
|
||||
if (m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
if (!m_depthImage8.isNull()) {
|
||||
@@ -1502,6 +1809,25 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
|
||||
}
|
||||
emit hoveredWorldPosDepthChanged(wp0, z0);
|
||||
|
||||
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 (e->button() == Qt::LeftButton) {
|
||||
const int hit = hitTestEntity(wp0);
|
||||
@@ -1734,7 +2060,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
if (!m_depthAbsPath.isEmpty()) {
|
||||
if (m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
if (!m_depthImage8.isNull()) {
|
||||
@@ -1747,6 +2073,18 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
}
|
||||
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) {
|
||||
const int h = hitTestEntity(wp);
|
||||
if (h != m_presHoverEntityIndex) {
|
||||
@@ -1849,7 +2187,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
|
||||
if (!m_depthAbsPath.isEmpty() && m_depthDirty) {
|
||||
m_depthDirty = false;
|
||||
QImage img(m_depthAbsPath);
|
||||
QImage img = readImageTolerant(m_depthAbsPath);
|
||||
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
if (!m_depthImage8.isNull()) {
|
||||
@@ -1901,6 +2239,18 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
||||
}
|
||||
|
||||
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<int>(std::round(delta.x())), static_cast<int>(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) {
|
||||
@@ -2016,6 +2366,11 @@ void EditorCanvas::wheelEvent(QWheelEvent* e) {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QPixmap>
|
||||
#include <QPoint>
|
||||
#include <QPointF>
|
||||
#include <QImage>
|
||||
#include <QPainterPath>
|
||||
@@ -85,6 +86,12 @@ public:
|
||||
|
||||
void selectEntityById(const QString& id);
|
||||
void clearEntitySelection();
|
||||
void selectBlackholeByEntityId(const QString& entityId);
|
||||
void clearBlackholeSelection();
|
||||
bool startBlackholeCopyResolve(const QString& entityId);
|
||||
void cancelBlackholeCopyResolve();
|
||||
/// 背景图片文件内容被外部写盘更新(路径未变)时,强制重新加载缓存
|
||||
void notifyBackgroundContentChanged();
|
||||
|
||||
// 与动画求值一致的原点/缩放(用于 K 帧与自动关键帧)
|
||||
QPointF selectedAnimatedOriginWorld() const;
|
||||
@@ -115,6 +122,7 @@ signals:
|
||||
void requestFinalizePendingEntity(const QVector<QPointF>& polyWorld);
|
||||
void requestMoveEntity(const QString& id, const QPointF& delta);
|
||||
void requestMoveTool(const QString& id, const QPointF& delta);
|
||||
void requestResolveBlackholeCopy(const QString& entityId, const QPoint& sourceOffsetPx);
|
||||
void entityDragActiveChanged(bool on);
|
||||
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
|
||||
/// 预览模式下点击实体:anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
|
||||
@@ -171,6 +179,7 @@ private:
|
||||
QPointF animatedOriginWorld;
|
||||
double animatedDepthScale01 = 0.5;
|
||||
double opacity = 1.0; // 0..1(由可见性轨道求值)
|
||||
bool blackholeVisible = true;
|
||||
};
|
||||
int hitTestEntity(const QPointF& worldPos) const;
|
||||
|
||||
@@ -186,7 +195,7 @@ private:
|
||||
mutable QPixmap m_bgPixmap;
|
||||
mutable bool m_pixmapDirty = true;
|
||||
mutable QImage m_bgImage; // 原背景(用于抠图/填充)
|
||||
mutable QImage m_bgImageCutout; // 抠图后的背景(实体区域填黑)
|
||||
mutable QImage m_bgImageCutout; // 抠图后的背景(黑洞区域填黑)
|
||||
mutable bool m_bgImageDirty = true;
|
||||
mutable bool m_bgCutoutDirty = true;
|
||||
|
||||
@@ -229,6 +238,13 @@ private:
|
||||
qint64 m_lastPreviewEmitMs = 0;
|
||||
qint64 m_lastDepthScaleRecalcMs = 0;
|
||||
int m_selectedEntity = -1;
|
||||
QString m_selectedBlackholeEntityId;
|
||||
bool m_blackholeCopyResolveActive = false;
|
||||
QString m_blackholeCopyEntityId;
|
||||
QRectF m_blackholeCopyHoleRect;
|
||||
QRectF m_blackholeCopySourceRect;
|
||||
bool m_blackholeCopyDragging = false;
|
||||
QPointF m_blackholeCopyDragOffset;
|
||||
int m_selectedTool = -1;
|
||||
bool m_draggingTool = false;
|
||||
QPointF m_toolDragOffsetOriginWorld;
|
||||
|
||||
Reference in New Issue
Block a user