fix(heal): resolve adjacent objects texture leak and overwriting by clustering and progressive masking

This commit is contained in:
2026-10-14 16:15:00 +07:00
parent a24f000ead
commit 1fe31c35a4
3 changed files with 72 additions and 13 deletions
+62 -7
View File
@@ -1318,7 +1318,11 @@ function clusterHealSpots(
const xj = sj.x * width;
const yj = sj.y * height;
const rj = sj.r * width;
const maxDist = (ri + rj) * 1.5;
// Connect spots that are close enough that their inpainting crops interact
// (e.g. parallel wires, nearby scratches or dust within margin reach).
// Clustering nearby objects together ensures they are inpainted in one unified
// pass where neither object is sampled as "clean background texture" for the other.
const maxDist = Math.max((ri + rj) * 2.5, ri + rj + 48);
const distSq = (xi - xj) ** 2 + (yi - yj) ** 2;
if (distSq <= maxDist * maxDist) {
adj[i].push(j);
@@ -1492,6 +1496,18 @@ function clusterHealSpots(
// Strokes are clustered so that separate brush repairs (e.g. Brush 1 and Brush 2)
// are solved independently at their own high resolution rather than lumped into
// one downscaled bounding box that would degrade previous repairs.
// Progressive working surface for inpainting:
// When multiple strokes/clusters exist (such as parallel wires or adjacent spots),
// each cluster reads from the surface that already has previous clusters repaired,
// and writes ONLY to its own hole (alpha 0 outside the hole).
// This prevents earlier repaired wires from reappearing and prevents dirty neighboring
// textures from contaminating subsequent inpainting.
const workSurface = own(createSurface(width, height));
const workCanvas = workSurface.getCanvas();
if (snap) {
workCanvas.drawImage(snap, 0, 0, own(Skia.Paint()));
}
const clusters = clusterHealSpots(heal, width, height);
for (const cluster of clusters) {
const cWidest = Math.max(...cluster.map((s) => s.r * width));
@@ -1516,12 +1532,17 @@ function clusterHealSpots(
const { w: mw, h: mh } = inpaintCropSize(srcW, srcH);
const cropSurface = own(createSurface(mw, mh));
if (cropSurface) {
cropSurface.getCanvas().drawImageRect(
snap,
Skia.XYWHRect(srcLeft, srcTop, srcW, srcH),
Skia.XYWHRect(0, 0, mw, mh),
own(Skia.Paint())
);
flush(workSurface);
const workSnap = workSurface.makeImageSnapshot();
if (workSnap) {
cropSurface.getCanvas().drawImageRect(
workSnap,
Skia.XYWHRect(srcLeft, srcTop, srcW, srcH),
Skia.XYWHRect(0, 0, mw, mh),
own(Skia.Paint())
);
try { workSnap.dispose(); } catch {}
}
flush(cropSurface);
const cropPixels = cropSurface.getCanvas().readPixels(0, 0, {
width: mw,
@@ -1534,6 +1555,7 @@ function clusterHealSpots(
const kx = mw / srcW;
const ky = mh / srcH;
const hole = new Uint8Array(mw * mh);
// Mark all spots from the current cluster
for (const s of cluster) {
const cx = (s.x * width - srcLeft) * kx;
const cy = (s.y * height - srcTop) * ky;
@@ -1551,8 +1573,35 @@ function clusterHealSpots(
}
}
}
// Also mark any other heal spots falling into this crop box as holes,
// so they can never be sampled as "clean background texture"
for (const s of heal) {
const cx = (s.x * width - srcLeft) * kx;
const cy = (s.y * height - srcTop) * ky;
const rad = s.r * width * kx;
if (cx + rad < 0 || cx - rad >= mw || cy + rad < 0 || cy - rad >= mh) continue;
const x0 = Math.max(0, Math.floor(cx - rad));
const x1 = Math.min(mw - 1, Math.ceil(cx + rad));
const y0 = Math.max(0, Math.floor(cy - rad));
const y1 = Math.min(mh - 1, Math.ceil(cy + rad));
for (let y = y0; y <= y1; y++) {
const dy = y - cy;
const row = y * mw;
for (let x = x0; x <= x1; x++) {
const dx = x - cx;
if (dx * dx + dy * dy <= rad * rad) hole[row + x] = 1;
}
}
}
const patch = await inpaintCrop(cropPixels, mw, mh, hole).catch(() => null);
if (patch) {
// Ensure pixels outside hole are 100% transparent so drawing never
// clobbers neighboring clusters or clean background
for (let i = 0; i < mw * mh; i++) {
if (!hole[i]) {
patch[i * 4 + 3] = 0;
}
}
const patchSurface = own(createSurface(mw, mh));
if (patchSurface && patchSurface.getCanvas().writePixels(patch, mw, mh, 0, 0)) {
flush(patchSurface);
@@ -1566,6 +1615,12 @@ function clusterHealSpots(
Skia.XYWHRect(srcLeft - boxLeft, srcTop - boxTop, srcW, srcH),
patchPaint
);
workCanvas.drawImageRect(
patchImage,
Skia.XYWHRect(0, 0, mw, mh),
Skia.XYWHRect(srcLeft, srcTop, srcW, srcH),
patchPaint
);
}
}
}