update
This commit is contained in:
@@ -2241,8 +2241,8 @@ bool ProjectWorkspace::resolveBlackholeByUseOriginalBackground(const QString& id
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
|
bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const QRect& targetWorldInBackground,
|
||||||
bool hideBlackholeAfterFill) {
|
const QRect& srcWorldInBackground, bool hideBlackholeAfterFill) {
|
||||||
if (m_projectDir.isEmpty() || id.isEmpty()) {
|
if (m_projectDir.isEmpty() || id.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2267,68 +2267,67 @@ bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage bg = image_file::loadImageLimited(bgAbs, image_decode::kWorkspaceMaxPixels);
|
const QRect targetWorld = targetWorldInBackground.normalized();
|
||||||
if (bg.isNull()) {
|
const QRect srcWorld = srcWorldInBackground.normalized();
|
||||||
|
if (!targetWorld.isValid() || !srcWorld.isValid() || targetWorld.width() <= 0 || targetWorld.height() <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (bg.format() != QImage::Format_ARGB32_Premultiplied) {
|
if (targetWorld.width() != srcWorld.width() || targetWorld.height() != srcWorld.height()) {
|
||||||
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
return false;
|
||||||
}
|
}
|
||||||
const QImage srcSnapshot = bg;
|
|
||||||
|
|
||||||
QSize logicalSz;
|
QSize logicalSz;
|
||||||
if (!image_file::probeImagePixelSize(bgAbs, &logicalSz) || !logicalSz.isValid() || logicalSz.width() < 1 ||
|
if (!image_file::probeImagePixelSize(bgAbs, &logicalSz) || !logicalSz.isValid() || logicalSz.width() < 1 ||
|
||||||
logicalSz.height() < 1) {
|
logicalSz.height() < 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const QRect bounds(0, 0, logicalSz.width(), logicalSz.height());
|
||||||
QPainterPath holePathWorld;
|
if (targetWorld.intersected(bounds) != targetWorld || srcWorld.intersected(bounds) != srcWorld) {
|
||||||
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) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QRect srcWorld(targetWorld.topLeft() + sourceOffsetPx, targetWorld.size());
|
QImage targetPatch;
|
||||||
if (srcWorld.left() < 0) srcWorld.moveLeft(0);
|
QImage srcPatch;
|
||||||
if (srcWorld.top() < 0) srcWorld.moveTop(0);
|
if (!image_file::loadRegionToQImageExact(bgAbs, targetWorld, &targetPatch) ||
|
||||||
if (srcWorld.right() >= logicalSz.width()) srcWorld.moveRight(logicalSz.width() - 1);
|
!image_file::loadRegionToQImageExact(bgAbs, srcWorld, &srcPatch) || targetPatch.isNull() ||
|
||||||
if (srcWorld.bottom() >= logicalSz.height()) srcWorld.moveBottom(logicalSz.height() - 1);
|
srcPatch.isNull()) {
|
||||||
srcWorld = srcWorld.intersected(QRect(0, 0, logicalSz.width(), logicalSz.height()));
|
|
||||||
if (srcWorld.width() != targetWorld.width() || srcWorld.height() != targetWorld.height()) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (targetPatch.size() != srcPatch.size() || targetPatch.width() != targetWorld.width() ||
|
||||||
const qreal sx = bg.width() / qreal(logicalSz.width());
|
targetPatch.height() != targetWorld.height()) {
|
||||||
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()) {
|
|
||||||
return false;
|
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.setRenderHint(QPainter::Antialiasing, true);
|
||||||
p.setClipPath(holePathImg);
|
if (!holePathLocal.isEmpty()) {
|
||||||
p.drawImage(targetImg.topLeft(), srcSnapshot, srcImg);
|
p.setClipPath(holePathLocal);
|
||||||
|
}
|
||||||
|
p.drawImage(0, 0, srcPatch);
|
||||||
p.end();
|
p.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
const QImage patch = bg.copy(targetImg);
|
|
||||||
const auto before = m_project.entities();
|
const auto before = m_project.entities();
|
||||||
QString overlayRel;
|
QString overlayRel;
|
||||||
if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, patch, targetWorld,
|
if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, targetPatch, targetWorld,
|
||||||
&overlayRel)) {
|
&overlayRel)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,9 +117,9 @@ public:
|
|||||||
bool setEntityVisible(const QString& id, bool on);
|
bool setEntityVisible(const QString& id, bool on);
|
||||||
bool setEntityBlackholeVisible(const QString& id, bool on);
|
bool setEntityBlackholeVisible(const QString& id, bool on);
|
||||||
bool resolveBlackholeByUseOriginalBackground(const QString& id);
|
bool resolveBlackholeByUseOriginalBackground(const QString& id);
|
||||||
// 复制背景其他区域填充黑洞(sourceOffsetPx 以黑洞包围盒左上角为基准偏移)
|
// 复制背景其他区域填充黑洞(矩形均为背景逻辑像素坐标,须与画布一致且 src 与 target 同尺寸)
|
||||||
bool resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
|
bool resolveBlackholeByCopyBackground(const QString& id, const QRect& targetWorldInBackground,
|
||||||
bool hideBlackholeAfterFill);
|
const QRect& srcWorldInBackground, bool hideBlackholeAfterFill);
|
||||||
// 将模型补全的裁剪块保存为覆盖层并叠加显示,不修改 background 原图
|
// 将模型补全的裁剪块保存为覆盖层并叠加显示,不修改 background 原图
|
||||||
bool resolveBlackholeByModelInpaint(const QString& id, const QImage& overlayPatch,
|
bool resolveBlackholeByModelInpaint(const QString& id, const QImage& overlayPatch,
|
||||||
const QRect& overlayRectInBackground, bool hideBlackholeAfterFill);
|
const QRect& overlayRectInBackground, bool hideBlackholeAfterFill);
|
||||||
|
|||||||
@@ -1214,21 +1214,6 @@ bool EditorCanvas::startBlackholeCopyResolve(const QString& entityId) {
|
|||||||
srcRect.translate(shift, 0.0);
|
srcRect.translate(shift, 0.0);
|
||||||
srcRect = clampRectTopLeftToBounds(srcRect, bg);
|
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) {
|
if (!m_bhCopyPatchValid && m_bgImageDirty) {
|
||||||
m_bgImageDirty = false;
|
m_bgImageDirty = false;
|
||||||
m_bgImage = readImageTolerant(m_bgAbsPath);
|
m_bgImage = readImageTolerant(m_bgAbsPath);
|
||||||
@@ -1244,17 +1229,59 @@ bool EditorCanvas::startBlackholeCopyResolve(const QString& entityId) {
|
|||||||
m_blackholeCopySourceRect = srcRect;
|
m_blackholeCopySourceRect = srcRect;
|
||||||
m_blackholeCopyDragging = false;
|
m_blackholeCopyDragging = false;
|
||||||
m_blackholeCopyDragOffset = QPointF();
|
m_blackholeCopyDragOffset = QPointF();
|
||||||
|
reloadBlackholeCopyVipsPatchUnionIfNeeded();
|
||||||
updateCursor();
|
updateCursor();
|
||||||
update();
|
update();
|
||||||
return true;
|
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() {
|
void EditorCanvas::cancelBlackholeCopyResolve() {
|
||||||
if (!m_blackholeCopyResolveActive) {
|
if (!m_blackholeCopyResolveActive) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_bhCopyPatchValid = false;
|
m_bhCopyPatchValid = false;
|
||||||
m_bhCopyPatch = QImage();
|
m_bhCopyPatch = QImage();
|
||||||
|
m_bhCopyPatchLogicalSize = QSize();
|
||||||
m_blackholeCopyResolveActive = false;
|
m_blackholeCopyResolveActive = false;
|
||||||
m_blackholeCopyEntityId.clear();
|
m_blackholeCopyEntityId.clear();
|
||||||
m_blackholeCopyHoleRect = QRectF();
|
m_blackholeCopyHoleRect = QRectF();
|
||||||
@@ -1567,7 +1594,7 @@ bool EditorCanvas::loadRgbCropForEntitySegmentStroke(const QRectF& polyBrWorld,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (outCropRgb.isNull()) {
|
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) {
|
if (!outCropRgb.isNull() && outCropRgb.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||||
outCropRgb = outCropRgb.convertToFormat(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 &&
|
if (!m_presentationPreviewMode && m_blackholeCopyResolveActive &&
|
||||||
!m_blackholeCopyHoleRect.isNull() && !m_blackholeCopySourceRect.isNull()) {
|
!m_blackholeCopyHoleRect.isNull() && !m_blackholeCopySourceRect.isNull()) {
|
||||||
const QRect srcRect = m_blackholeCopySourceRect.toAlignedRect();
|
const QRectF holeF = m_blackholeCopyHoleRect;
|
||||||
const QRect dstRect = m_blackholeCopyHoleRect.toAlignedRect();
|
const QRectF srcF(m_blackholeCopySourceRect.topLeft(), QSizeF(holeF.width(), holeF.height()));
|
||||||
const bool havePreview =
|
const bool havePreview =
|
||||||
(m_bhCopyPatchValid && !m_bhCopyPatch.isNull()) || !m_bgImage.isNull();
|
(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;
|
QPainterPath holePath;
|
||||||
for (const auto& ent : m_entities) {
|
for (const auto& ent : m_entities) {
|
||||||
if (ent.id == m_blackholeCopyEntityId && !ent.cutoutPolygonWorld.isEmpty()) {
|
if (ent.id == m_blackholeCopyEntityId && !ent.cutoutPolygonWorld.isEmpty()) {
|
||||||
@@ -2044,11 +2071,31 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
|
|||||||
p.setClipPath(holePath);
|
p.setClipPath(holePath);
|
||||||
}
|
}
|
||||||
p.setOpacity(0.75);
|
p.setOpacity(0.75);
|
||||||
if (m_bhCopyPatchValid && !m_bhCopyPatch.isNull()) {
|
if (m_bhCopyPatchValid && !m_bhCopyPatch.isNull() && m_bhCopyPatchLogicalSize.isValid() &&
|
||||||
const QRect srcLocal = srcRect.translated(-m_bhCopyPatchOrigin);
|
m_bhCopyPatchLogicalSize.width() > 0 && m_bhCopyPatchLogicalSize.height() > 0) {
|
||||||
p.drawImage(dstRect.topLeft(), m_bhCopyPatch, srcLocal);
|
const QRectF srcWorldInPatch = srcF.translated(-m_bhCopyPatchOrigin);
|
||||||
} else {
|
const QRectF bound(0, 0, m_bhCopyPatchLogicalSize.width(), m_bhCopyPatchLogicalSize.height());
|
||||||
p.drawImage(dstRect.topLeft(), m_bgImage, srcRect);
|
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.setOpacity(1.0);
|
||||||
p.restore();
|
p.restore();
|
||||||
@@ -3002,6 +3049,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
|
|||||||
src.moveTopLeft(wp - m_blackholeCopyDragOffset);
|
src.moveTopLeft(wp - m_blackholeCopyDragOffset);
|
||||||
src = clampRectTopLeftToBounds(src, worldRectOfBackground());
|
src = clampRectTopLeftToBounds(src, worldRectOfBackground());
|
||||||
m_blackholeCopySourceRect = src;
|
m_blackholeCopySourceRect = src;
|
||||||
|
reloadBlackholeCopyVipsPatchUnionIfNeeded();
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
e->accept();
|
e->accept();
|
||||||
@@ -3204,10 +3252,38 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
|
|||||||
if (m_blackholeCopyResolveActive && e->button() == Qt::LeftButton) {
|
if (m_blackholeCopyResolveActive && e->button() == Qt::LeftButton) {
|
||||||
if (m_blackholeCopyDragging) {
|
if (m_blackholeCopyDragging) {
|
||||||
m_blackholeCopyDragging = false;
|
m_blackholeCopyDragging = false;
|
||||||
const QPointF delta = m_blackholeCopySourceRect.topLeft() - m_blackholeCopyHoleRect.topLeft();
|
QRect targetW;
|
||||||
emit requestResolveBlackholeCopy(
|
QRect srcW;
|
||||||
m_blackholeCopyEntityId,
|
if (m_bgLogicalSize.isValid() && m_bgLogicalSize.width() > 0 && m_bgLogicalSize.height() > 0) {
|
||||||
QPoint(static_cast<int>(std::round(delta.x())), static_cast<int>(std::round(delta.y()))));
|
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();
|
cancelBlackholeCopyResolve();
|
||||||
e->accept();
|
e->accept();
|
||||||
|
|||||||
@@ -153,7 +153,8 @@ signals:
|
|||||||
void requestMoveCamera(const QString& id, const QPointF& delta);
|
void requestMoveCamera(const QString& id, const QPointF& delta);
|
||||||
void requestCameraViewScaleAdjust(const QString& id, double factor);
|
void requestCameraViewScaleAdjust(const QString& id, double factor);
|
||||||
void selectedCameraChanged(bool hasSelection, const QString& id, const QPointF& centerWorld, double viewScale);
|
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 entityDragActiveChanged(bool on);
|
||||||
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
|
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
|
||||||
/// 预览模式下点击实体:anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
|
/// 预览模式下点击实体:anchorView 为实体质心在视图中的位置,用于摆放介绍浮层
|
||||||
@@ -269,6 +270,8 @@ private:
|
|||||||
bool m_initialBgLoadFinishedEmitted = false;
|
bool m_initialBgLoadFinishedEmitted = false;
|
||||||
QImage m_bhCopyPatch; // 黑洞「复制背景」预览:原图子区域
|
QImage m_bhCopyPatch; // 黑洞「复制背景」预览:原图子区域
|
||||||
QPoint m_bhCopyPatchOrigin;
|
QPoint m_bhCopyPatchOrigin;
|
||||||
|
/// 与 m_bhCopyPatch 对应的逻辑像素范围(vips 路径可能将 patch 缩小到 maxOutput,与逻辑尺寸不同)
|
||||||
|
QSize m_bhCopyPatchLogicalSize;
|
||||||
bool m_bhCopyPatchValid = false;
|
bool m_bhCopyPatchValid = false;
|
||||||
mutable QPixmap m_bgPixmap;
|
mutable QPixmap m_bgPixmap;
|
||||||
mutable bool m_pixmapDirty = true;
|
mutable bool m_pixmapDirty = true;
|
||||||
@@ -337,6 +340,9 @@ private:
|
|||||||
DragMode m_dragMode = DragMode::None;
|
DragMode m_dragMode = DragMode::None;
|
||||||
QPointF m_dragStartMouseWorld;
|
QPointF m_dragStartMouseWorld;
|
||||||
|
|
||||||
|
/// 视口 LOD 下:黑洞「复制背景」预览纹理须覆盖 hole∪source;拖动时按需从磁盘重载。
|
||||||
|
void reloadBlackholeCopyVipsPatchUnionIfNeeded();
|
||||||
|
|
||||||
QVector<Entity> m_entities;
|
QVector<Entity> m_entities;
|
||||||
QVector<ToolView> m_tools;
|
QVector<ToolView> m_tools;
|
||||||
QVector<core::Project::Camera> m_cameraOverlays;
|
QVector<core::Project::Camera> m_cameraOverlays;
|
||||||
|
|||||||
@@ -4159,8 +4159,10 @@ void MainWindow::rebuildCentralPages() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(m_editorCanvas, &EditorCanvas::requestResolveBlackholeCopy, this,
|
connect(m_editorCanvas, &EditorCanvas::requestResolveBlackholeCopy, this,
|
||||||
[this](const QString& entityId, const QPoint& sourceOffsetPx) {
|
[this](const QString& entityId, const QRect& targetWorldInBackground,
|
||||||
if (!m_workspace.resolveBlackholeByCopyBackground(entityId, sourceOffsetPx, true)) {
|
const QRect& srcWorldInBackground) {
|
||||||
|
if (!m_workspace.resolveBlackholeByCopyBackground(entityId, targetWorldInBackground,
|
||||||
|
srcWorldInBackground, true)) {
|
||||||
QMessageBox::warning(
|
QMessageBox::warning(
|
||||||
this,
|
this,
|
||||||
QStringLiteral("黑洞修复"),
|
QStringLiteral("黑洞修复"),
|
||||||
@@ -4804,9 +4806,9 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int maxDec = std::clamp(std::max(cropRect.width(), cropRect.height()), 512, 4096);
|
// 与黑洞/覆盖层逻辑像素严格对齐:必须按 crop 原始分辨率解码,避免 vips maxOutput 缩放导致预览与写盘发糊。
|
||||||
QImage bg;
|
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("读取背景失败。"));
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("读取背景失败。"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -5008,6 +5010,9 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
|
|||||||
QString immediateErr;
|
QString immediateErr;
|
||||||
const QString inpaintModel = dlg.inpaintModelKey();
|
const QString inpaintModel = dlg.inpaintModelKey();
|
||||||
const QString prompt = dlg.promptText();
|
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(
|
QNetworkReply* reply = client->inpaintAsync(
|
||||||
cropPng,
|
cropPng,
|
||||||
maskPng,
|
maskPng,
|
||||||
@@ -5015,7 +5020,7 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
|
|||||||
prompt,
|
prompt,
|
||||||
QString(),
|
QString(),
|
||||||
0.72,
|
0.72,
|
||||||
1024,
|
inpaintMaxSide,
|
||||||
&immediateErr);
|
&immediateErr);
|
||||||
if (!reply) {
|
if (!reply) {
|
||||||
QMessageBox::warning(this,
|
QMessageBox::warning(this,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class InpaintRequest(ImageInput):
|
|||||||
strength: float = Field(0.8, ge=0.0, le=1.0)
|
strength: float = Field(0.8, ge=0.0, le=1.0)
|
||||||
negative_prompt: Optional[str] = Field("", description="负向 prompt")
|
negative_prompt: Optional[str] = Field("", description="负向 prompt")
|
||||||
mask_b64: Optional[str] = Field(None, description="mask PNG base64(可选)")
|
mask_b64: Optional[str] = Field(None, description="mask PNG base64(可选)")
|
||||||
max_side: int = Field(1024, ge=128, le=2048)
|
max_side: int = Field(1024, ge=128, le=8192, description="推理时长边上限;不超过则按输入全分辨率推理后再还原")
|
||||||
guidance_scale: Optional[float] = Field(None, ge=0.0, le=200.0, description="扩散 CFG;不传则各后端默认")
|
guidance_scale: Optional[float] = Field(None, ge=0.0, le=200.0, description="扩散 CFG;不传则各后端默认")
|
||||||
num_inference_steps: Optional[int] = Field(None, ge=1, le=150, description="采样步数;不传则各后端默认")
|
num_inference_steps: Optional[int] = Field(None, ge=1, le=150, description="采样步数;不传则各后端默认")
|
||||||
|
|
||||||
|
|||||||
@@ -63,13 +63,14 @@ def make_flux_fill_predictor(
|
|||||||
|
|
||||||
if device == "cuda":
|
if device == "cuda":
|
||||||
try:
|
try:
|
||||||
pipe.enable_model_cpu_offload()
|
pipe.enable_vae_tiling()
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
pass
|
||||||
pipe.enable_vae_tiling()
|
try:
|
||||||
except Exception:
|
pipe.enable_attention_slicing()
|
||||||
pass
|
except Exception:
|
||||||
pipe.to("cuda")
|
pass
|
||||||
|
pipe.to("cuda")
|
||||||
else:
|
else:
|
||||||
pipe.to(device)
|
pipe.to(device)
|
||||||
|
|
||||||
|
|||||||
@@ -78,10 +78,8 @@ def _enable_memory_opts(pipe, device: str) -> None:
|
|||||||
pipe.enable_vae_tiling()
|
pipe.enable_vae_tiling()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
# 不使用 enable_model_cpu_offload:与 transformers 5.x 下 CLIPTextModel 等子模块的 accelerate hook
|
||||||
pipe.enable_model_cpu_offload()
|
# 组合易触发 `'_hf_hook'` AttributeError;整管已 .to(cuda) 时以 slicing/tiling 省显存即可。
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _align_size(orig_w: int, orig_h: int, max_side: int) -> tuple[int, int]:
|
def _align_size(orig_w: int, orig_h: int, max_side: int) -> tuple[int, int]:
|
||||||
@@ -123,8 +121,7 @@ def _make_sdxl_inpaint_predictor(
|
|||||||
use_safetensors=True,
|
use_safetensors=True,
|
||||||
).to(device)
|
).to(device)
|
||||||
|
|
||||||
# 省显存设置(尽量不改变输出语义)
|
# 省显存:attention slicing / VAE slicing tiling(不再 enable_model_cpu_offload,见 _enable_memory_opts 注释)
|
||||||
# 注意:CPU offload 会明显变慢,但能显著降低显存占用。
|
|
||||||
if device == "cuda":
|
if device == "cuda":
|
||||||
try:
|
try:
|
||||||
pipe.enable_attention_slicing()
|
pipe.enable_attention_slicing()
|
||||||
@@ -138,10 +135,6 @@ def _make_sdxl_inpaint_predictor(
|
|||||||
pipe.enable_vae_tiling()
|
pipe.enable_vae_tiling()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
|
||||||
pipe.enable_model_cpu_offload()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _predict(
|
def _predict(
|
||||||
image: Image.Image,
|
image: Image.Image,
|
||||||
@@ -244,10 +237,7 @@ def _make_controlnet_predictor(_: UnifiedInpaintConfig):
|
|||||||
pipe.enable_vae_tiling()
|
pipe.enable_vae_tiling()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
try:
|
pipe.to(device)
|
||||||
pipe.enable_model_cpu_offload()
|
|
||||||
except Exception:
|
|
||||||
pipe.to(device)
|
|
||||||
else:
|
else:
|
||||||
pipe.to(device)
|
pipe.to(device)
|
||||||
|
|
||||||
@@ -361,9 +351,22 @@ def _make_lama_predictor(cfg: UnifiedInpaintConfig):
|
|||||||
max_side: int = 1024,
|
max_side: int = 1024,
|
||||||
**_kwargs,
|
**_kwargs,
|
||||||
) -> Image.Image:
|
) -> Image.Image:
|
||||||
# big-lama:仅 (RGB, mask);无文本条件。
|
# big-lama:仅 (RGB, mask);无文本条件。与 SDXL/FLUX 一致:可按 max_side 降采样推理后再还原到输入分辨率。
|
||||||
_ = (strength, guidance_scale, num_inference_steps, max_side)
|
_ = (strength, guidance_scale, num_inference_steps)
|
||||||
return lama(image, mask)
|
image = image.convert("RGB")
|
||||||
|
mask = mask.convert("L")
|
||||||
|
orig_w, orig_h = image.size
|
||||||
|
run_w, run_h = _align_size(orig_w, orig_h, max_side=max_side)
|
||||||
|
if (run_w, run_h) != (orig_w, orig_h):
|
||||||
|
image_run = image.resize((run_w, run_h), Image.BICUBIC)
|
||||||
|
mask_run = mask.resize((run_w, run_h), Image.NEAREST)
|
||||||
|
else:
|
||||||
|
image_run = image
|
||||||
|
mask_run = mask
|
||||||
|
out = lama(image_run, mask_run)
|
||||||
|
if out.size != (orig_w, orig_h):
|
||||||
|
out = out.resize((orig_w, orig_h), resample=Image.BICUBIC)
|
||||||
|
return out
|
||||||
|
|
||||||
return _predict
|
return _predict
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user