From 0a591456c4aa78be8f81375b088e93ea2062ef1b Mon Sep 17 00:00:00 2001 From: 134 Date: Fri, 3 Jul 2026 20:58:30 +0800 Subject: [PATCH] fix: limit scale patch composite to edit regions --- app/api/canvas/local-edit/route.ts | 1 + lib/canvas/generate-image.ts | 79 ++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 43d55ec03..c859ee846 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -3900,6 +3900,7 @@ export async function POST(request: NextRequest) { featherMaxPx: 42, featherMinPx: 10, featherRatio: 0.018, + regions: scaleAdjustCompositeRegions, sourceImageUrl, workspaceRegion: scaleAdjustPatchReference.workspaceRect, } diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index 937edfa3c..28617af9b 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -56,6 +56,7 @@ export interface CanvasLocalEditCompositeConfig { export interface CanvasScaleAdjustPatchCompositeConfig { sourceImageUrl: string; workspaceRegion: CanvasLocalEditCompositeRegion; + regions?: CanvasLocalEditCompositeRegion[]; featherMinPx?: number; featherMaxPx?: number; featherRatio?: number; @@ -384,10 +385,18 @@ function normalizeScaleAdjustPatchCompositeConfig( if (!/^https?:\/\//i.test(sourceImageUrl)) return null; const workspaceRegion = normalizeCanvasRegion(value.workspaceRegion); if (!workspaceRegion) return null; + const regions = Array.isArray(value.regions) + ? value.regions + .map((region) => normalizeCanvasRegion(region)) + .filter((region): region is CanvasLocalEditCompositeRegion => + Boolean(region) + ) + : []; return { sourceImageUrl, workspaceRegion, + ...(regions.length > 0 ? { regions } : {}), featherMaxPx: normalizeOptionalNumber(value.featherMaxPx, { max: 180, min: 0, @@ -2335,6 +2344,7 @@ function buildScaleAdjustPatchCompositeMaskSvg(params: { width: number; height: number; featherPx: number; + regions?: Array<{ left: number; top: number; width: number; height: number }>; touchesLeft: boolean; touchesTop: boolean; touchesRight: boolean; @@ -2342,6 +2352,46 @@ function buildScaleAdjustPatchCompositeMaskSvg(params: { }) { const { height, width } = params; const featherPx = Math.max(0, Math.round(params.featherPx)); + const regions = params.regions?.filter( + (region) => region.width > 0 && region.height > 0 + ); + if (regions?.length) { + const blur = Math.max(1, featherPx); + const softPad = Math.max(2, Math.round(featherPx * 1.35)); + const corePad = Math.max(0, Math.round(featherPx * 0.22)); + const softRects = regions + .map((region) => { + const left = Math.max(0, region.left - softPad); + const top = Math.max(0, region.top - softPad); + const right = Math.min(width, region.left + region.width + softPad); + const bottom = Math.min(height, region.top + region.height + softPad); + return ``; + }) + .join(""); + const coreRects = regions + .map((region) => { + const left = Math.max(0, region.left - corePad); + const top = Math.max(0, region.top - corePad); + const right = Math.min(width, region.left + region.width + corePad); + const bottom = Math.min(height, region.top + region.height + corePad); + return ``; + }) + .join(""); + + return Buffer.from(` + + + + + + + + ${softRects} + ${coreRects} + + `); + } + if (featherPx <= 0) { return Buffer.from(` @@ -2671,9 +2721,37 @@ async function applyScaleAdjustPatchCompositeToImages(params: { const touchesTop = workspaceBox.top <= 0; const touchesRight = workspaceBox.left + workspaceBox.width >= sourceWidth; const touchesBottom = workspaceBox.top + workspaceBox.height >= sourceHeight; + const maskRegionBoxes = composite.regions + ?.map((region) => + rectToPixelBox({ + height: sourceHeight, + region, + width: sourceWidth, + }) + ) + .map((box) => { + const left = Math.max(0, box.left - workspaceBox.left); + const top = Math.max(0, box.top - workspaceBox.top); + const right = Math.min( + workspaceBox.width, + box.left + box.width - workspaceBox.left + ); + const bottom = Math.min( + workspaceBox.height, + box.top + box.height - workspaceBox.top + ); + return { + left, + top, + width: Math.max(0, right - left), + height: Math.max(0, bottom - top), + }; + }) + .filter((box) => box.width > 0 && box.height > 0); const patchMask = buildScaleAdjustPatchCompositeMaskSvg({ featherPx, height: workspaceBox.height, + regions: maskRegionBoxes, touchesBottom, touchesLeft, touchesRight, @@ -2733,6 +2811,7 @@ async function applyScaleAdjustPatchCompositeToImages(params: { debugLabel, featherPx, imageCount: mergedImages.length, + maskRegionCount: maskRegionBoxes?.length || 0, sourceHeight, sourceWidth, workspaceBox,