fix: honor target boxes in scale adjust guard

This commit is contained in:
134 2026-06-23 19:47:11 +08:00
parent 4d65ba0b79
commit 3760f7ff44
2 changed files with 122 additions and 68 deletions

View File

@ -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<NormalizedLocalEditRegion["targetRect"]> =>
Boolean(rect)
),
}
: {}),
}
: undefined,
localEditComposite: isScaleAdjustOperation

View File

@ -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) {