This commit is contained in:
2026-05-15 00:46:37 +08:00
parent b4c7697e8a
commit f64e624a39
8 changed files with 190 additions and 100 deletions
+38 -39
View File
@@ -2241,8 +2241,8 @@ bool ProjectWorkspace::resolveBlackholeByUseOriginalBackground(const QString& id
return true;
}
bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
bool hideBlackholeAfterFill) {
bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const QRect& targetWorldInBackground,
const QRect& srcWorldInBackground, bool hideBlackholeAfterFill) {
if (m_projectDir.isEmpty() || id.isEmpty()) {
return false;
}
@@ -2267,68 +2267,67 @@ bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const
return false;
}
QImage bg = image_file::loadImageLimited(bgAbs, image_decode::kWorkspaceMaxPixels);
if (bg.isNull()) {
const QRect targetWorld = targetWorldInBackground.normalized();
const QRect srcWorld = srcWorldInBackground.normalized();
if (!targetWorld.isValid() || !srcWorld.isValid() || targetWorld.width() <= 0 || targetWorld.height() <= 0) {
return false;
}
if (bg.format() != QImage::Format_ARGB32_Premultiplied) {
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
if (targetWorld.width() != srcWorld.width() || targetWorld.height() != srcWorld.height()) {
return false;
}
const QImage srcSnapshot = bg;
QSize logicalSz;
if (!image_file::probeImagePixelSize(bgAbs, &logicalSz) || !logicalSz.isValid() || logicalSz.width() < 1 ||
logicalSz.height() < 1) {
return false;
}
QPainterPath holePathWorld;
holePathWorld.addPolygon(QPolygonF(ent.cutoutPolygonWorld));
holePathWorld.closeSubpath();
const QRect targetWorld =
holePathWorld.boundingRect().toAlignedRect().intersected(QRect(0, 0, logicalSz.width(), logicalSz.height()));
if (!targetWorld.isValid() || targetWorld.width() <= 0 || targetWorld.height() <= 0) {
const QRect bounds(0, 0, logicalSz.width(), logicalSz.height());
if (targetWorld.intersected(bounds) != targetWorld || srcWorld.intersected(bounds) != srcWorld) {
return false;
}
QRect srcWorld(targetWorld.topLeft() + sourceOffsetPx, targetWorld.size());
if (srcWorld.left() < 0) srcWorld.moveLeft(0);
if (srcWorld.top() < 0) srcWorld.moveTop(0);
if (srcWorld.right() >= logicalSz.width()) srcWorld.moveRight(logicalSz.width() - 1);
if (srcWorld.bottom() >= logicalSz.height()) srcWorld.moveBottom(logicalSz.height() - 1);
srcWorld = srcWorld.intersected(QRect(0, 0, logicalSz.width(), logicalSz.height()));
if (srcWorld.width() != targetWorld.width() || srcWorld.height() != targetWorld.height()) {
QImage targetPatch;
QImage srcPatch;
if (!image_file::loadRegionToQImageExact(bgAbs, targetWorld, &targetPatch) ||
!image_file::loadRegionToQImageExact(bgAbs, srcWorld, &srcPatch) || targetPatch.isNull() ||
srcPatch.isNull()) {
return false;
}
const qreal sx = bg.width() / qreal(logicalSz.width());
const qreal sy = bg.height() / qreal(logicalSz.height());
QTransform worldToBg;
worldToBg.scale(sx, sy);
const QRect targetImg =
worldToBg.mapRect(QRectF(targetWorld)).toAlignedRect().intersected(QRect(0, 0, bg.width(), bg.height()));
const QRect srcImg =
worldToBg.mapRect(QRectF(srcWorld)).toAlignedRect().intersected(QRect(0, 0, bg.width(), bg.height()));
if (!targetImg.isValid() || !srcImg.isValid() || targetImg.width() != srcImg.width() ||
targetImg.height() != srcImg.height()) {
if (targetPatch.size() != srcPatch.size() || targetPatch.width() != targetWorld.width() ||
targetPatch.height() != targetWorld.height()) {
return false;
}
if (targetPatch.format() != QImage::Format_ARGB32_Premultiplied) {
targetPatch = targetPatch.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
if (srcPatch.format() != QImage::Format_ARGB32_Premultiplied) {
srcPatch = srcPatch.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
const QPainterPath holePathImg = worldToBg.map(holePathWorld);
QPolygonF polyLocal;
polyLocal.reserve(ent.cutoutPolygonWorld.size());
for (const QPointF& pt : ent.cutoutPolygonWorld) {
polyLocal.append(pt - targetWorld.topLeft());
}
QPainterPath holePathLocal;
if (polyLocal.size() >= 3) {
holePathLocal.addPolygon(polyLocal);
holePathLocal.closeSubpath();
}
{
QPainter p(&bg);
QPainter p(&targetPatch);
p.setRenderHint(QPainter::Antialiasing, true);
p.setClipPath(holePathImg);
p.drawImage(targetImg.topLeft(), srcSnapshot, srcImg);
if (!holePathLocal.isEmpty()) {
p.setClipPath(holePathLocal);
}
p.drawImage(0, 0, srcPatch);
p.end();
}
const QImage patch = bg.copy(targetImg);
const auto before = m_project.entities();
QString overlayRel;
if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, patch, targetWorld,
if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, targetPatch, targetWorld,
&overlayRel)) {
return false;
}
+3 -3
View File
@@ -117,9 +117,9 @@ public:
bool setEntityVisible(const QString& id, bool on);
bool setEntityBlackholeVisible(const QString& id, bool on);
bool resolveBlackholeByUseOriginalBackground(const QString& id);
// 复制背景其他区域填充黑洞(sourceOffsetPx 以黑洞包围盒左上角为基准偏移
bool resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
bool hideBlackholeAfterFill);
// 复制背景其他区域填充黑洞(矩形均为背景逻辑像素坐标,须与画布一致且 src 与 target 同尺寸
bool resolveBlackholeByCopyBackground(const QString& id, const QRect& targetWorldInBackground,
const QRect& srcWorldInBackground, bool hideBlackholeAfterFill);
// 将模型补全的裁剪块保存为覆盖层并叠加显示,不修改 background 原图
bool resolveBlackholeByModelInpaint(const QString& id, const QImage& overlayPatch,
const QRect& overlayRectInBackground, bool hideBlackholeAfterFill);
+104 -28
View File
@@ -1214,21 +1214,6 @@ bool EditorCanvas::startBlackholeCopyResolve(const QString& entityId) {
srcRect.translate(shift, 0.0);
srcRect = clampRectTopLeftToBounds(srcRect, bg);
m_bhCopyPatchValid = false;
m_bhCopyPatch = QImage();
if (m_useViewportLod && !m_bgAbsPath.isEmpty()) {
QRect uni =
holeRect.united(srcRect).toAlignedRect().intersected(bg.toAlignedRect());
if (uni.isValid() && uni.width() > 0 && uni.height() > 0 &&
core::image_file::loadRegionToQImage(m_bgAbsPath, uni, 8192, 8192, &m_bhCopyPatch) &&
!m_bhCopyPatch.isNull()) {
if (m_bhCopyPatch.format() != QImage::Format_ARGB32_Premultiplied) {
m_bhCopyPatch = m_bhCopyPatch.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
m_bhCopyPatchOrigin = uni.topLeft();
m_bhCopyPatchValid = true;
}
}
if (!m_bhCopyPatchValid && m_bgImageDirty) {
m_bgImageDirty = false;
m_bgImage = readImageTolerant(m_bgAbsPath);
@@ -1244,17 +1229,59 @@ bool EditorCanvas::startBlackholeCopyResolve(const QString& entityId) {
m_blackholeCopySourceRect = srcRect;
m_blackholeCopyDragging = false;
m_blackholeCopyDragOffset = QPointF();
reloadBlackholeCopyVipsPatchUnionIfNeeded();
updateCursor();
update();
return true;
}
void EditorCanvas::reloadBlackholeCopyVipsPatchUnionIfNeeded() {
if (!m_blackholeCopyResolveActive || !m_useViewportLod || m_bgAbsPath.isEmpty() || !m_bgLogicalSize.isValid() ||
m_bgLogicalSize.width() < 1 || m_bgLogicalSize.height() < 1) {
m_bhCopyPatchValid = false;
m_bhCopyPatch = QImage();
m_bhCopyPatchLogicalSize = QSize();
return;
}
const QRect bgBounds(0, 0, m_bgLogicalSize.width(), m_bgLogicalSize.height());
const QRect uni = m_blackholeCopyHoleRect.united(m_blackholeCopySourceRect)
.toAlignedRect()
.intersected(bgBounds);
if (!uni.isValid() || uni.width() < 1 || uni.height() < 1) {
m_bhCopyPatchValid = false;
m_bhCopyPatch = QImage();
m_bhCopyPatchLogicalSize = QSize();
return;
}
if (m_bhCopyPatchValid && m_bhCopyPatchLogicalSize.isValid() && !m_bhCopyPatch.isNull()) {
const QRect cached(m_bhCopyPatchOrigin, m_bhCopyPatchLogicalSize);
if (cached.contains(uni)) {
return;
}
}
QImage patch;
if (!core::image_file::loadRegionToQImageExact(m_bgAbsPath, uni, &patch) || patch.isNull()) {
m_bhCopyPatchValid = false;
m_bhCopyPatch = QImage();
m_bhCopyPatchLogicalSize = QSize();
return;
}
if (patch.format() != QImage::Format_ARGB32_Premultiplied) {
patch = patch.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
m_bhCopyPatch = std::move(patch);
m_bhCopyPatchOrigin = uni.topLeft();
m_bhCopyPatchLogicalSize = uni.size();
m_bhCopyPatchValid = true;
}
void EditorCanvas::cancelBlackholeCopyResolve() {
if (!m_blackholeCopyResolveActive) {
return;
}
m_bhCopyPatchValid = false;
m_bhCopyPatch = QImage();
m_bhCopyPatchLogicalSize = QSize();
m_blackholeCopyResolveActive = false;
m_blackholeCopyEntityId.clear();
m_blackholeCopyHoleRect = QRectF();
@@ -1567,7 +1594,7 @@ bool EditorCanvas::loadRgbCropForEntitySegmentStroke(const QRectF& polyBrWorld,
}
}
if (outCropRgb.isNull()) {
(void)core::image_file::loadRegionToQImage(m_bgAbsPath, cropWorld, 4096, 4096, &outCropRgb);
(void)core::image_file::loadRegionToQImageExact(m_bgAbsPath, cropWorld, &outCropRgb);
if (!outCropRgb.isNull() && outCropRgb.format() != QImage::Format_ARGB32_Premultiplied) {
outCropRgb = outCropRgb.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
@@ -2027,11 +2054,11 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
if (!m_presentationPreviewMode && m_blackholeCopyResolveActive &&
!m_blackholeCopyHoleRect.isNull() && !m_blackholeCopySourceRect.isNull()) {
const QRect srcRect = m_blackholeCopySourceRect.toAlignedRect();
const QRect dstRect = m_blackholeCopyHoleRect.toAlignedRect();
const QRectF holeF = m_blackholeCopyHoleRect;
const QRectF srcF(m_blackholeCopySourceRect.topLeft(), QSizeF(holeF.width(), holeF.height()));
const bool havePreview =
(m_bhCopyPatchValid && !m_bhCopyPatch.isNull()) || !m_bgImage.isNull();
if (havePreview && srcRect.isValid() && dstRect.isValid()) {
if (havePreview && holeF.width() >= 1.0 && holeF.height() >= 1.0) {
QPainterPath holePath;
for (const auto& ent : m_entities) {
if (ent.id == m_blackholeCopyEntityId && !ent.cutoutPolygonWorld.isEmpty()) {
@@ -2044,11 +2071,31 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
p.setClipPath(holePath);
}
p.setOpacity(0.75);
if (m_bhCopyPatchValid && !m_bhCopyPatch.isNull()) {
const QRect srcLocal = srcRect.translated(-m_bhCopyPatchOrigin);
p.drawImage(dstRect.topLeft(), m_bhCopyPatch, srcLocal);
} else {
p.drawImage(dstRect.topLeft(), m_bgImage, srcRect);
if (m_bhCopyPatchValid && !m_bhCopyPatch.isNull() && m_bhCopyPatchLogicalSize.isValid() &&
m_bhCopyPatchLogicalSize.width() > 0 && m_bhCopyPatchLogicalSize.height() > 0) {
const QRectF srcWorldInPatch = srcF.translated(-m_bhCopyPatchOrigin);
const QRectF bound(0, 0, m_bhCopyPatchLogicalSize.width(), m_bhCopyPatchLogicalSize.height());
const QRectF srcVis = srcWorldInPatch.intersected(bound);
if (!srcVis.isEmpty()) {
const double sx = double(m_bhCopyPatch.width()) / double(m_bhCopyPatchLogicalSize.width());
const double sy = double(m_bhCopyPatch.height()) / double(m_bhCopyPatchLogicalSize.height());
const QRectF srcPixels(srcVis.x() * sx, srcVis.y() * sy, srcVis.width() * sx, srcVis.height() * sy);
const QPointF dVis = srcVis.topLeft() - srcWorldInPatch.topLeft();
const QRectF dstFrag(holeF.topLeft() + dVis, srcVis.size());
p.drawImage(dstFrag, m_bhCopyPatch, srcPixels);
}
} else if (!m_bgImage.isNull() && m_bgLogicalSize.isValid() && m_bgLogicalSize.width() > 0 &&
m_bgLogicalSize.height() > 0) {
const QRectF bgB(0, 0, m_bgLogicalSize.width(), m_bgLogicalSize.height());
const double bx = double(m_bgImage.width()) / double(m_bgLogicalSize.width());
const double by = double(m_bgImage.height()) / double(m_bgLogicalSize.height());
const QRectF srcFW = srcF.intersected(bgB);
if (!srcFW.isEmpty()) {
const QPointF dVis = srcFW.topLeft() - srcF.topLeft();
const QRectF dstFrag(holeF.topLeft() + dVis, srcFW.size());
const QRectF srcPixels(srcFW.x() * bx, srcFW.y() * by, srcFW.width() * bx, srcFW.height() * by);
p.drawImage(dstFrag, m_bgImage, srcPixels);
}
}
p.setOpacity(1.0);
p.restore();
@@ -3002,6 +3049,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
src.moveTopLeft(wp - m_blackholeCopyDragOffset);
src = clampRectTopLeftToBounds(src, worldRectOfBackground());
m_blackholeCopySourceRect = src;
reloadBlackholeCopyVipsPatchUnionIfNeeded();
update();
}
e->accept();
@@ -3204,10 +3252,38 @@ 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()))));
QRect targetW;
QRect srcW;
if (m_bgLogicalSize.isValid() && m_bgLogicalSize.width() > 0 && m_bgLogicalSize.height() > 0) {
const QRect bounds(0, 0, m_bgLogicalSize.width(), m_bgLogicalSize.height());
// 洞与源不得各自 toAlignedRect:浮点外接框相同宽高时取整常会差 1px,导致 emit 空矩形。
// 以洞的对齐矩形为准,源矩形与之同尺寸,仅平移量取 hole/source 左上角差值的四舍五入。
targetW = m_blackholeCopyHoleRect.toAlignedRect().intersected(bounds);
if (targetW.isValid() && targetW.width() > 0 && targetW.height() > 0) {
const QPointF deltaF =
m_blackholeCopySourceRect.topLeft() - m_blackholeCopyHoleRect.topLeft();
const QPoint d(int(std::lround(deltaF.x())), int(std::lround(deltaF.y())));
srcW = QRect(targetW.topLeft() + d, targetW.size());
if (srcW.left() < bounds.left()) {
srcW.moveLeft(bounds.left());
}
if (srcW.top() < bounds.top()) {
srcW.moveTop(bounds.top());
}
if (srcW.right() > bounds.right()) {
srcW.moveRight(bounds.right());
}
if (srcW.bottom() > bounds.bottom()) {
srcW.moveBottom(bounds.bottom());
}
}
}
if (targetW.isValid() && srcW.isValid() && targetW.width() > 0 && targetW.height() > 0 &&
targetW.width() == srcW.width() && targetW.height() == srcW.height()) {
emit requestResolveBlackholeCopy(m_blackholeCopyEntityId, targetW, srcW);
} else {
emit requestResolveBlackholeCopy(m_blackholeCopyEntityId, QRect(), QRect());
}
}
cancelBlackholeCopyResolve();
e->accept();
+7 -1
View File
@@ -153,7 +153,8 @@ signals:
void requestMoveCamera(const QString& id, const QPointF& delta);
void requestCameraViewScaleAdjust(const QString& id, double factor);
void selectedCameraChanged(bool hasSelection, const QString& id, const QPointF& centerWorld, double viewScale);
void requestResolveBlackholeCopy(const QString& entityId, const QPoint& sourceOffsetPx);
void requestResolveBlackholeCopy(const QString& entityId, const QRect& targetWorldInBackground,
const QRect& srcWorldInBackground);
void entityDragActiveChanged(bool on);
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
/// 预览模式下点击实体:anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
@@ -269,6 +270,8 @@ private:
bool m_initialBgLoadFinishedEmitted = false;
QImage m_bhCopyPatch; // 黑洞「复制背景」预览:原图子区域
QPoint m_bhCopyPatchOrigin;
/// 与 m_bhCopyPatch 对应的逻辑像素范围(vips 路径可能将 patch 缩小到 maxOutput,与逻辑尺寸不同)
QSize m_bhCopyPatchLogicalSize;
bool m_bhCopyPatchValid = false;
mutable QPixmap m_bgPixmap;
mutable bool m_pixmapDirty = true;
@@ -337,6 +340,9 @@ private:
DragMode m_dragMode = DragMode::None;
QPointF m_dragStartMouseWorld;
/// 视口 LOD 下:黑洞「复制背景」预览纹理须覆盖 hole∪source;拖动时按需从磁盘重载。
void reloadBlackholeCopyVipsPatchUnionIfNeeded();
QVector<Entity> m_entities;
QVector<ToolView> m_tools;
QVector<core::Project::Camera> m_cameraOverlays;
+10 -5
View File
@@ -4159,8 +4159,10 @@ void MainWindow::rebuildCentralPages() {
}
});
connect(m_editorCanvas, &EditorCanvas::requestResolveBlackholeCopy, this,
[this](const QString& entityId, const QPoint& sourceOffsetPx) {
if (!m_workspace.resolveBlackholeByCopyBackground(entityId, sourceOffsetPx, true)) {
[this](const QString& entityId, const QRect& targetWorldInBackground,
const QRect& srcWorldInBackground) {
if (!m_workspace.resolveBlackholeByCopyBackground(entityId, targetWorldInBackground,
srcWorldInBackground, true)) {
QMessageBox::warning(
this,
QStringLiteral("黑洞修复"),
@@ -4804,9 +4806,9 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
return;
}
const int maxDec = std::clamp(std::max(cropRect.width(), cropRect.height()), 512, 4096);
// 与黑洞/覆盖层逻辑像素严格对齐:必须按 crop 原始分辨率解码,避免 vips maxOutput 缩放导致预览与写盘发糊。
QImage bg;
if (!core::image_file::loadRegionToQImage(bgAbs, cropRect, maxDec, maxDec, &bg) || bg.isNull()) {
if (!core::image_file::loadRegionToQImageExact(bgAbs, cropRect, &bg) || bg.isNull()) {
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("读取背景失败。"));
return;
}
@@ -5008,6 +5010,9 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
QString immediateErr;
const QString inpaintModel = dlg.inpaintModelKey();
const QString prompt = dlg.promptText();
// 后端在不超过 max_side 时保持输入分辨率推理,再缩回原尺寸;此处用裁剪长边避免无谓降采样(上限与 API 一致)。
const int cropMaxSide = std::max(cropRect.width(), cropRect.height());
const int inpaintMaxSide = std::clamp(cropMaxSide, 256, 8192);
QNetworkReply* reply = client->inpaintAsync(
cropPng,
maskPng,
@@ -5015,7 +5020,7 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
prompt,
QString(),
0.72,
1024,
inpaintMaxSide,
&immediateErr);
if (!reply) {
QMessageBox::warning(this,