From b3b4fa8ed28e26b4b1f166eb19d6298b5476afb2 Mon Sep 17 00:00:00 2001 From: 134 Date: Thu, 2 Jul 2026 17:14:06 +0800 Subject: [PATCH] fix: enlarge scale adjust coordinate map --- app/api/canvas/local-edit/route.ts | 130 ++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 0432bb999..bd13e2a1c 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -1249,6 +1249,128 @@ function buildScaleAdjustAnnotationSvg(params: { `); } +function buildScaleAdjustZoomedCoordinateSvg(params: { + width: number; + height: number; + regions: NormalizedLocalEditRegion[]; + scalePercent?: ScaleAdjustPercent | null; +}) { + const { width, height, regions, scalePercent } = params; + const minSide = Math.max(1, Math.min(width, height)); + const strokeWidth = Math.max(3, Math.round(minSide * 0.006)); + const fontSize = Math.max(22, Math.round(minSide * 0.026)); + const smallFontSize = Math.max(18, Math.round(fontSize * 0.72)); + const panelMarginX = Math.round(width * 0.1); + const panelMarginY = Math.round(height * 0.12); + const panelLeft = panelMarginX; + const panelTop = panelMarginY; + const panelWidth = Math.max(1, width - panelMarginX * 2); + const panelHeight = Math.max(1, height - panelMarginY * 2); + const sourceColor = "#9ca3af"; + const targetColor = "#111827"; + const helperColor = "#4b5563"; + const padX = Math.round(fontSize * 0.42); + const labelHeight = Math.round(fontSize * 1.35); + const label = (text: string, x: number, y: number, color: string) => { + const labelWidth = Math.min( + Math.round(text.length * fontSize * 0.58 + padX * 2), + Math.max(160, width - 16) + ); + const left = clamp(x, 8, Math.max(8, width - labelWidth - 8)); + const top = clamp(y, 8, Math.max(8, height - labelHeight - 8)); + return ` + + ${escapeXml(text)} + `; + }; + + const regionSvg = regions + .map((region) => { + const source = region.pixelRect; + const targetRect = getScaleAdjustTargetRect(region, scalePercent); + const target = + region.targetPixelRect || rectToPixels(targetRect, width, height); + const sourceRight = source.left + source.width; + const sourceBottom = source.top + source.height; + const targetRight = target.left + target.width; + const targetBottom = target.top + target.height; + const unionLeft = Math.min(source.left, target.left); + const unionTop = Math.min(source.top, target.top); + const unionRight = Math.max(sourceRight, targetRight); + const unionBottom = Math.max(sourceBottom, targetBottom); + const marginX = Math.max(12, Math.round((unionRight - unionLeft) * 0.35)); + const marginY = Math.max(12, Math.round((unionBottom - unionTop) * 0.35)); + const viewLeft = unionLeft - marginX; + const viewTop = unionTop - marginY; + const viewWidth = Math.max(1, unionRight - unionLeft + marginX * 2); + const viewHeight = Math.max(1, unionBottom - unionTop + marginY * 2); + const scale = Math.min( + panelWidth / viewWidth, + panelHeight / viewHeight + ); + const drawnWidth = viewWidth * scale; + const drawnHeight = viewHeight * scale; + const originX = panelLeft + (panelWidth - drawnWidth) / 2 - viewLeft * scale; + const originY = panelTop + (panelHeight - drawnHeight) / 2 - viewTop * scale; + const project = (box: { left: number; top: number; width: number; height: number }) => ({ + left: Math.round(originX + box.left * scale), + top: Math.round(originY + box.top * scale), + width: Math.max(1, Math.round(box.width * scale)), + height: Math.max(1, Math.round(box.height * scale)), + }); + const sourceBox = project(source); + const targetBox = project(target); + const sourceCenterX = sourceBox.left + sourceBox.width / 2; + const sourceCenterY = sourceBox.top + sourceBox.height / 2; + const targetCenterX = targetBox.left + targetBox.width / 2; + const targetCenterY = targetBox.top + targetBox.height / 2; + const widthFactor = source.width > 0 ? target.width / source.width : 1; + const heightFactor = source.height > 0 ? target.height / source.height : 1; + const actionLabel = + widthFactor < 0.92 || heightFactor < 0.92 + ? "SHRINK" + : widthFactor > 1.08 || heightFactor > 1.08 + ? "ENLARGE" + : "MOVE"; + const scaleLabel = `${actionLabel} ${widthFactor.toFixed(2)}W ${heightFactor.toFixed(2)}H`; + const radius = Math.max(10, Math.round(strokeWidth * 2.2)); + const centerMoved = + Math.hypot(targetCenterX - sourceCenterX, targetCenterY - sourceCenterY) > + Math.max(10, minSide * 0.018); + + return ` + + + + + ${ + centerMoved + ? `` + : "" + } + ${label(`OLD ${region.index}`, sourceBox.left, sourceBox.top - labelHeight - strokeWidth, sourceColor)} + ${label(`TARGET ${region.index}`, targetBox.left, targetBox.top + strokeWidth, targetColor)} + ${label(scaleLabel, targetBox.left, targetBox.top + targetBox.height + strokeWidth * 2, helperColor)} + ${label("OLD OUTSIDE = BACKGROUND", sourceBox.left, sourceBox.top + sourceBox.height + strokeWidth * 4 + labelHeight, helperColor)} + `; + }) + .join(""); + + return Buffer.from(` + + + + + + + + + LOCAL SCALE MAP - USE PROMPT PIXELS FOR ABSOLUTE POSITION + ${regionSvg} + + `); +} + function buildLocalEditPrompt(regions: NormalizedLocalEditRegion[]) { const regionLines = regions .map((region) => { @@ -1438,7 +1560,7 @@ function buildScaleAdjustCoordinatePrompt( "中文硬约束:只调整 OLD 框中的主要家具主体。TARGET 框是最终占位;如果是缩小,OLD 框里超出 TARGET 的旧家具必须消失并补成自然背景;如果是放大,同一件家具必须明显变大。禁止整图缩放、整图裁切、整图重绘。", "一次成功硬约束:不要保守返回原尺寸。目标家具的可见轮廓必须明显从 OLD 原始占位变到 TARGET 占位;如果最终看起来和原图一样,就是失败。", `Return one seamless realistic photo with the same frame and pixel size as input image 1: ${sourceSize.width} x ${sourceSize.height} px.`, - "Input image 2 is a neutral coordinate instruction map, not a photo and not output content. The OLD box/label locates the current furniture in input image 1; the TARGET box/label gives the required final footprint, center, and scale factor. If the map says OLD OUTSIDE = BACKGROUND, the old full-size furniture outside TARGET must disappear and be healed as natural background in the final photo. Read the boxes, labels, arrows, and scale text only as instructions; never output any map graphics.", + "Input image 2 is a zoomed neutral coordinate instruction map, not a photo and not output content. It enlarges the OLD and TARGET boxes so their relative scale is easy to read; use the numeric pixel coordinates below for the exact absolute position in input image 1. The OLD box/label locates the current furniture; the TARGET box/label gives the required final footprint, center, and scale factor. If the map says OLD OUTSIDE = BACKGROUND, the old full-size furniture outside TARGET must disappear and be healed as natural background in the final photo. Read the boxes, labels, arrows, and scale text only as instructions; never output any map graphics.", cropLines ? `${cropLines}` : "", "", "Task:", @@ -1463,7 +1585,7 @@ function buildScaleAdjustCoordinatePrompt( function buildScaleAdjustCoordinateSystemPrompt() { return [ "You are an image editing model for local furniture scale adjustment.", - "Input image 1 is the only final photo base. Input image 2 is a neutral coordinate instruction map, not a photo layer.", + "Input image 1 is the only final photo base. Input image 2 is a zoomed neutral coordinate instruction map, not a photo layer.", "Follow both English and Chinese instructions. The Chinese lines are hard constraints for avoiding unchanged furniture and full-canvas zoom.", "Generate a clean realistic final photo by editing the target furniture itself and naturally rebuilding only the tiny surrounding background that becomes visible.", "Prefer a clear visible size change over a conservative unchanged result; unchanged furniture is a failed edit.", @@ -1640,7 +1762,7 @@ export async function POST(request: NextRequest) { const annotatedBuffer = isScaleAdjustOperation ? useScaleAdjustCoordinateGuide ? await sharp( - buildScaleAdjustAnnotationSvg({ + buildScaleAdjustZoomedCoordinateSvg({ height: sourceHeight, regions, scalePercent, @@ -1799,7 +1921,7 @@ export async function POST(request: NextRequest) { .length, scaleAdjustPromptStrategy: isScaleAdjustOperation ? useScaleAdjustCoordinateGuide - ? "same-name-neutral-coordinate-map-no-old-crop-v39-one-shot" + ? "same-name-zoomed-coordinate-map-no-old-crop-v41-one-shot" : "locked-plate-semantic-target-map-no-old-crop-v15" : undefined, scaleAdjustPostCompositeApplied: false,