fix: enlarge scale adjust coordinate map

This commit is contained in:
134 2026-07-02 17:14:06 +08:00
parent ffd13c0f31
commit b3b4fa8ed2

View File

@ -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 `
<rect x="${left}" y="${top}" width="${labelWidth}" height="${labelHeight}" rx="${Math.round(labelHeight * 0.22)}" fill="${color}" opacity="0.92" />
<text x="${left + padX}" y="${top + Math.round(labelHeight * 0.68)}" fill="#ffffff" font-size="${fontSize}" font-family="Arial, Helvetica, sans-serif" font-weight="800">${escapeXml(text)}</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 `
<rect x="${sourceBox.left}" y="${sourceBox.top}" width="${sourceBox.width}" height="${sourceBox.height}" rx="${radius}" fill="#e5e7eb" opacity="0.72" stroke="${sourceColor}" stroke-width="${strokeWidth}" />
<rect x="${targetBox.left}" y="${targetBox.top}" width="${targetBox.width}" height="${targetBox.height}" rx="${radius}" fill="#d1d5db" opacity="0.82" stroke="${targetColor}" stroke-width="${Math.max(3, Math.round(strokeWidth * 1.25))}" />
<circle cx="${sourceCenterX}" cy="${sourceCenterY}" r="${Math.max(4, Math.round(strokeWidth * 1.25))}" fill="${sourceColor}" />
<circle cx="${targetCenterX}" cy="${targetCenterY}" r="${Math.max(5, Math.round(strokeWidth * 1.55))}" fill="${targetColor}" />
${
centerMoved
? `<line x1="${sourceCenterX}" y1="${sourceCenterY}" x2="${targetCenterX}" y2="${targetCenterY}" stroke="${helperColor}" stroke-width="${Math.max(3, Math.round(strokeWidth * 0.9))}" stroke-linecap="round" marker-end="url(#scale-adjust-zoom-arrow)" opacity="0.78" />`
: ""
}
${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(`
<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="scale-adjust-zoom-arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto" markerUnits="strokeWidth">
<path d="M 0 0 L 10 5 L 0 10 z" fill="${helperColor}" />
</marker>
</defs>
<rect x="0" y="0" width="${width}" height="${height}" fill="#f5f7fb" />
<rect x="${panelLeft}" y="${panelTop}" width="${panelWidth}" height="${panelHeight}" rx="${Math.round(minSide * 0.018)}" fill="#ffffff" stroke="#cbd5e1" stroke-width="${Math.max(2, Math.round(strokeWidth * 0.65))}" />
<text x="${panelLeft}" y="${Math.max(fontSize + 8, panelTop - Math.round(fontSize * 0.45))}" fill="#111827" font-size="${smallFontSize}" font-family="Arial, Helvetica, sans-serif" font-weight="800">LOCAL SCALE MAP - USE PROMPT PIXELS FOR ABSOLUTE POSITION</text>
${regionSvg}
</svg>
`);
}
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,