diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index a4e0d1b2c..a4a357d9d 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -398,35 +398,13 @@ function scaleRectAroundCenter( function getScaleAdjustTargetRect( region: NormalizedLocalEditRegion, - scalePercent: ScaleAdjustPercent + scalePercent?: ScaleAdjustPercent | null ) { - return region.targetRect || scaleRectAroundCenter(region.rect, scalePercent); -} - -function deriveScaleAdjustPercentFromTargetRect( - sourceRect: NormalizedLocalEditRegion["rect"], - targetRect: NormalizedLocalEditRegion["rect"] | null | undefined -) { - if (!targetRect || sourceRect.width <= 0 || sourceRect.height <= 0) { - return null; + if (region.targetRect) return region.targetRect; + if (typeof scalePercent === "number" && Number.isFinite(scalePercent)) { + return scaleRectAroundCenter(region.rect, scalePercent); } - const widthRatio = Math.max(0.05, targetRect.width / sourceRect.width); - const heightRatio = Math.max(0.05, targetRect.height / sourceRect.height); - const uniformRatio = Math.sqrt(widthRatio * heightRatio); - const percent = Math.round((uniformRatio - 1) * 100); - if (!Number.isFinite(percent) || percent === 0) return null; - return clamp(percent, MIN_SCALE_ADJUST_PERCENT, MAX_SCALE_ADJUST_PERCENT); -} - -function deriveScaleAdjustPercentFromRegions(regions: NormalizedLocalEditRegion[]) { - for (const region of regions) { - const percent = deriveScaleAdjustPercentFromTargetRect( - region.rect, - region.targetRect - ); - if (percent !== null) return percent; - } - return null; + return region.rect; } function expandRectWithMargins( @@ -450,11 +428,31 @@ function expandRectWithMargins( function getScaleAdjustWorkspaceRect( sourceRect: NormalizedLocalEditRegion["rect"], targetRect: NormalizedLocalEditRegion["rect"], - scalePercent: ScaleAdjustPercent + scalePercent?: ScaleAdjustPercent | null ) { - if (scalePercent <= 0) return targetRect; + const sourceWidth = Math.max(0.0001, sourceRect.width); + const sourceHeight = Math.max(0.0001, sourceRect.height); + const widthGrowthRatio = Math.max(0, targetRect.width / sourceWidth - 1); + const heightGrowthRatio = Math.max(0, targetRect.height / sourceHeight - 1); + const areaGrowthRatio = Math.max( + 0, + Math.sqrt( + Math.max(0.0001, targetRect.width * targetRect.height) / + Math.max(0.0001, sourceWidth * sourceHeight) + ) - 1 + ); + const explicitGrowthRatio = Math.max( + widthGrowthRatio, + heightGrowthRatio, + areaGrowthRatio + ); + const fallbackScaleRatio = + typeof scalePercent === "number" && Number.isFinite(scalePercent) + ? Math.min(1, Math.max(0, scalePercent / 100)) + : 0; + const scaleRatio = Math.max(explicitGrowthRatio, fallbackScaleRatio); + if (scaleRatio <= 0) return targetRect; - const scaleRatio = Math.min(1, Math.max(0, scalePercent / 100)); const marginX = Math.min( 0.12, Math.max(0.018, sourceRect.width * scaleRatio * 0.22) @@ -469,7 +467,7 @@ function getScaleAdjustWorkspaceRect( function buildScaleAdjustCompositeRegions( regions: NormalizedLocalEditRegion[], - scalePercent: ScaleAdjustPercent + scalePercent?: ScaleAdjustPercent | null ) { return regions.flatMap((region) => { const targetRect = getScaleAdjustTargetRect(region, scalePercent); @@ -478,7 +476,8 @@ function buildScaleAdjustCompositeRegions( targetRect, scalePercent ); - return scalePercent > 0 + return workspaceRect.width > targetRect.width || + workspaceRect.height > targetRect.height ? [region.rect, targetRect, workspaceRect] : [region.rect, targetRect]; }); @@ -691,7 +690,7 @@ function buildScaleAdjustAnnotationSvg(params: { width: number; height: number; regions: NormalizedLocalEditRegion[]; - scalePercent: ScaleAdjustPercent; + scalePercent?: ScaleAdjustPercent | null; }) { const { width, height, regions, scalePercent } = params; const minSide = Math.max(1, Math.min(width, height)); @@ -782,7 +781,7 @@ function buildLocalEditPrompt(regions: NormalizedLocalEditRegion[]) { function buildScaleAdjustPrompt( regions: NormalizedLocalEditRegion[], - scalePercent: ScaleAdjustPercent, + scalePercent: ScaleAdjustPercent | null | undefined, cropReferences: ScaleAdjustCropReference[] = [], sourceSize?: { width: number; height: number } ) { @@ -925,21 +924,27 @@ export async function POST(request: NextRequest) { { status: 400 } ); } - if (operation === SCALE_ADJUST_OPERATION && scalePercent === null) { - scalePercent = deriveScaleAdjustPercentFromRegions(regions); - if (scalePercent === null) { - return NextResponse.json( - { - success: false, - error: "请提供比例调整幅度,或为每个区域提供有效的目标框 targetRect", - }, - { status: 400 } - ); - } + const hasScaleAdjustTargetRects = + operation === SCALE_ADJUST_OPERATION && + regions.length > 0 && + regions.every((region) => Boolean(region.targetRect)); + if ( + operation === SCALE_ADJUST_OPERATION && + !hasScaleAdjustTargetRects && + scalePercent === null + ) { + return NextResponse.json( + { + success: false, + error: "请为每个区域提供有效的目标框 targetRect,或提供比例调整幅度 scalePercent", + }, + { status: 400 } + ); } const shouldUseScaleAdjustDetailModel = - operation === SCALE_ADJUST_OPERATION && scalePercent !== null; + operation === SCALE_ADJUST_OPERATION && + (hasScaleAdjustTargetRects || scalePercent !== null); const [pointsConfig, siteSettingsForModel, scaleAdjustModelCandidates] = await Promise.all([ getPointsSystemConfig(), @@ -968,14 +973,15 @@ export async function POST(request: NextRequest) { } const isScaleAdjustOperation = - operation === SCALE_ADJUST_OPERATION && scalePercent !== null; + operation === SCALE_ADJUST_OPERATION && + (hasScaleAdjustTargetRects || scalePercent !== null); const displayPrompt = buildLocalEditDisplayPrompt({ operation, scalePercent }); const annotationSvg = isScaleAdjustOperation ? buildScaleAdjustAnnotationSvg({ height: sourceHeight, regions, - scalePercent: scalePercent as ScaleAdjustPercent, + scalePercent, width: sourceWidth, }) : buildAnnotationSvg({ @@ -1015,7 +1021,7 @@ export async function POST(request: NextRequest) { isScaleAdjustOperation ? buildScaleAdjustPrompt( regions, - scalePercent as ScaleAdjustPercent, + scalePercent, scaleAdjustCropReferences, { width: sourceWidth, height: sourceHeight } ) @@ -1067,10 +1073,7 @@ export async function POST(request: NextRequest) { } const resolvedModel = detailFixedModel || requestedModel; const scaleAdjustCompositeRegions = isScaleAdjustOperation - ? buildScaleAdjustCompositeRegions( - regions, - scalePercent as ScaleAdjustPercent - ) + ? buildScaleAdjustCompositeRegions(regions, scalePercent) : []; const imageRequest: CanvasGenerateImageRequest = { @@ -1100,6 +1103,9 @@ export async function POST(request: NextRequest) { regionCount: regions.length, scalePercent, scaleAdjustCropReferenceCount: scaleAdjustCropReferenceImageUrls.length, + scaleAdjustProtectionMode: hasScaleAdjustTargetRects + ? "target-rect" + : "scale-percent", scaleAdjustTargetRegionCount: regions.filter((region) => region.targetRect) .length, scaleAdjustPromptStrategy: isScaleAdjustOperation @@ -1129,7 +1135,21 @@ export async function POST(request: NextRequest) { protectedRegions: scaleAdjustCompositeRegions, sourceImageUrl, regions: regions.map((region) => region.rect), - scalePercent: scalePercent as ScaleAdjustPercent, + ...(typeof scalePercent === "number" + ? { scalePercent: scalePercent as ScaleAdjustPercent } + : {}), + ...(hasScaleAdjustTargetRects + ? { + targetRegions: regions + .map((region) => region.targetRect) + .filter( + ( + rect + ): rect is NonNullable => + Boolean(rect) + ), + } + : {}), } : undefined, localEditComposite: isScaleAdjustOperation diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index b13baa40f..7e955e764 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -54,7 +54,8 @@ export interface CanvasScaleAdjustVerificationConfig { protectedRegions?: CanvasLocalEditCompositeRegion[]; sourceImageUrl: string; regions: CanvasLocalEditCompositeRegion[]; - scalePercent: number; + targetRegions?: CanvasLocalEditCompositeRegion[]; + scalePercent?: number; } export interface CanvasGenerateImageRequest { @@ -331,13 +332,23 @@ function normalizeScaleAdjustVerificationConfig( if (!value || typeof value !== "object") return null; const sourceImageUrl = String(value.sourceImageUrl || "").trim(); if (!/^https?:\/\//i.test(sourceImageUrl)) return null; - const scalePercent = Number(value.scalePercent); - if (!Number.isFinite(scalePercent) || Math.abs(scalePercent) < 1) return null; if (!Array.isArray(value.regions) || value.regions.length === 0) return null; + const rawScalePercent = Number(value.scalePercent); + const scalePercent = + Number.isFinite(rawScalePercent) && Math.abs(rawScalePercent) >= 1 + ? rawScalePercent + : undefined; const regions = value.regions .map((region) => normalizeCanvasRegion(region)) .filter((region): region is CanvasLocalEditCompositeRegion => Boolean(region)); + const targetRegions = Array.isArray((value as { targetRegions?: unknown[] }).targetRegions) + ? (value as { targetRegions?: unknown[] }).targetRegions + ?.map((region) => normalizeCanvasRegion(region)) + .filter((region): region is CanvasLocalEditCompositeRegion => + Boolean(region) + ) || [] + : []; const protectedRegions = Array.isArray(value.protectedRegions) ? value.protectedRegions .map((region) => normalizeCanvasRegion(region)) @@ -346,11 +357,13 @@ function normalizeScaleAdjustVerificationConfig( ) : []; - return regions.length > 0 + return regions.length > 0 && + (targetRegions.length > 0 || typeof scalePercent === "number") ? { sourceImageUrl, regions, - scalePercent, + ...(targetRegions.length > 0 ? { targetRegions } : {}), + ...(typeof scalePercent === "number" ? { scalePercent } : {}), ...(protectedRegions.length > 0 ? { protectedRegions } : {}), } : null; @@ -425,13 +438,32 @@ function getScaleAdjustProtectedRegions( expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN) ); } - return verification.regions.flatMap((region) => [ - expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN), - expandCanvasRegion( - scaleRegionAroundCenter(region, verification.scalePercent), - SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN - ), - ]); + if (verification.targetRegions?.length) { + return verification.regions.flatMap((region, index) => { + const targetRegion = verification.targetRegions?.[index]; + return targetRegion + ? [ + expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN), + expandCanvasRegion( + targetRegion, + SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN + ), + ] + : [expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN)]; + }); + } + if (typeof verification.scalePercent === "number") { + return verification.regions.flatMap((region) => [ + expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN), + expandCanvasRegion( + scaleRegionAroundCenter(region, verification.scalePercent as number), + SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN + ), + ]); + } + return verification.regions.map((region) => + expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN) + ); } function getScaleAdjustOutsideSampleRegions( @@ -756,7 +788,8 @@ async function applyScaleAdjustVerificationToImages(params: { debugLabel, imageCount: images.length, regionCount: verification.regions.length, - scalePercent: verification.scalePercent, + scalePercent: verification.scalePercent ?? null, + targetRegionCount: verification.targetRegions?.length || 0, }); return images; } @@ -810,7 +843,8 @@ async function applyScaleAdjustVerificationToImages(params: { protectedRegionCount: verification.protectedRegions?.length || 0, regionCount: verification.regions.length, regionDiffScores: diffScores.map((score) => Number(score.toFixed(2))), - scalePercent: verification.scalePercent, + scalePercent: verification.scalePercent ?? null, + targetRegionCount: verification.targetRegions?.length || 0, }); if (outsideChanged) {