From c9ed6628097d51d0dfe9853754ce7dbc7deeb05d Mon Sep 17 00:00:00 2001 From: 134 Date: Thu, 9 Jul 2026 12:00:42 +0800 Subject: [PATCH] Fix canvas scale adjust local edit build --- app/api/canvas/local-edit/route.ts | 143 ++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 4 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 28be69d65..24cc075ed 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -199,6 +199,32 @@ function inferClosestAspectRatio(width: number, height: number) { return closest; } +function inferScaleAdjustPatchAspectRatio(width: number, height: number) { + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + return "1:1"; + } + const sourceRatio = width / height; + const providerSafeRatios = [ + "1:1", + "2:3", + "3:2", + "3:4", + "4:3", + "9:16", + "16:9", + ] as const; + let closest: (typeof providerSafeRatios)[number] = "1:1"; + let closestDiff = Number.POSITIVE_INFINITY; + for (const candidate of providerSafeRatios) { + const diff = Math.abs(parseAspectRatioValue(candidate) - sourceRatio); + if (diff < closestDiff) { + closest = candidate; + closestDiff = diff; + } + } + return closest; +} + function resolveLocalEditAspectRatio(input: unknown, sourceWidth: number, sourceHeight: number) { const requested = String(input || "").trim(); if ( @@ -339,7 +365,26 @@ function isScaleAdjustFalNanoBananaEditModel( const label = `${entry.model.displayName || ""} ${entry.model.modelId || ""}` .toLowerCase() .replace(/\s+/g, " "); - return label.includes("/edit") || /\bedit\b/.test(label); + const capabilities = + entry.model.capabilities && typeof entry.model.capabilities === "object" + ? (entry.model.capabilities as Record) + : {}; + const defaultParams = + entry.model.defaultParams && typeof entry.model.defaultParams === "object" + ? (entry.model.defaultParams as Record) + : {}; + const editEndpoint = + typeof capabilities.editEndpoint === "string" + ? capabilities.editEndpoint + : typeof defaultParams.editEndpoint === "string" + ? defaultParams.editEndpoint + : ""; + return ( + label.includes("/edit") || + /\bedit\b/.test(label) || + capabilities.autoRouteEditEndpoint === true || + editEndpoint.toLowerCase().includes("/edit") + ); } function isScaleAdjustHealthyModel( @@ -761,6 +806,51 @@ function expandRectWithMargins( }; } +function expandRectToProviderAspectLimit( + rect: NormalizedLocalEditRegion["rect"], + sourceWidth: number, + sourceHeight: number, + maxAspectRatio = 3 +) { + if ( + !Number.isFinite(sourceWidth) || + !Number.isFinite(sourceHeight) || + sourceWidth <= 0 || + sourceHeight <= 0 || + maxAspectRatio <= 1 + ) { + return rect; + } + + const pixelWidth = rect.width * sourceWidth; + const pixelHeight = rect.height * sourceHeight; + if (pixelWidth <= 0 || pixelHeight <= 0) return rect; + + const ratio = pixelWidth / pixelHeight; + let nextWidth = rect.width; + let nextHeight = rect.height; + + if (ratio > maxAspectRatio) { + nextHeight = Math.min(1, pixelWidth / maxAspectRatio / sourceHeight); + } else if (ratio < 1 / maxAspectRatio) { + nextWidth = Math.min(1, (pixelHeight / maxAspectRatio) / sourceWidth); + } else { + return rect; + } + + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + const x = clamp(centerX - nextWidth / 2, 0, Math.max(0, 1 - nextWidth)); + const y = clamp(centerY - nextHeight / 2, 0, Math.max(0, 1 - nextHeight)); + + return { + x, + y, + width: nextWidth, + height: nextHeight, + }; +} + function getRectUnion( leftRect: NormalizedLocalEditRegion["rect"], rightRect: NormalizedLocalEditRegion["rect"] @@ -938,6 +1028,40 @@ function getScaleAdjustPatchWorkspaceRect( return workspaceRects.reduce((combined, rect) => getRectUnion(combined, rect)); } +function shouldUseScaleAdjustEdgeLockedFullFrameReference(params: { + regions: NormalizedLocalEditRegion[]; + scalePercent?: ScaleAdjustPercent | null; + sourceHeight: number; + sourceWidth: number; +}) { + const { regions, scalePercent, sourceHeight, sourceWidth } = params; + if (!regions.length || !hasScaleAdjustShrinkTarget(regions, scalePercent)) { + return false; + } + if (!regions.some((region) => isScaleAdjustFurnitureProductLikePrompt(region.prompt))) { + return false; + } + + const workspaceRect = expandRectToProviderAspectLimit( + getScaleAdjustPatchWorkspaceRect(regions, scalePercent), + sourceWidth, + sourceHeight + ); + const pixelWidth = workspaceRect.width * sourceWidth; + const pixelHeight = workspaceRect.height * sourceHeight; + if (pixelWidth <= 0 || pixelHeight <= 0) return false; + + const ratio = pixelWidth / pixelHeight; + const touchesCanvasEdge = + workspaceRect.x <= 0.002 || + workspaceRect.y <= 0.002 || + workspaceRect.x + workspaceRect.width >= 0.998 || + workspaceRect.y + workspaceRect.height >= 0.998; + const nearProviderAspectLimit = ratio <= 0.42 || ratio >= 2.4; + + return touchesCanvasEdge && nearProviderAspectLimit; +} + function expandPixelRectForCrop(params: { height: number; pixelRect: NormalizedLocalEditRegion["pixelRect"]; @@ -1336,6 +1460,13 @@ function isScaleAdjustPersonLikePrompt(prompt: string) { ); } +function isScaleAdjustFurnitureProductLikePrompt(prompt: string) { + const text = normalizeScaleAdjustModelText(prompt); + return /furniture|product|commodity|goods|object|item|subject|sofa|couch|chair|table|desk|cabinet|shelf|bed|bench|lamp|basket|rack|stool|drawer|wardrobe|主物体|主体|物体|商品|产品|家具|沙发|椅|桌|柜|架|床|凳|灯|篮|抽屉|衣柜/.test( + text + ); +} + function hasScaleAdjustPersonPixelSignal(params: { box: { left: number; top: number; width: number; height: number }; raw: Buffer; @@ -2781,7 +2912,11 @@ async function createScaleAdjustPatchReference(params: { sourceWidth, userId, } = params; - const workspaceRect = getScaleAdjustPatchWorkspaceRect(regions, scalePercent); + const workspaceRect = expandRectToProviderAspectLimit( + getScaleAdjustPatchWorkspaceRect(regions, scalePercent), + sourceWidth, + sourceHeight + ); const workspacePixelRect = rectToPixels(workspaceRect, sourceWidth, sourceHeight); const shouldUseStructureLockedDraft = hasScaleAdjustStructureTarget( regions, @@ -3890,7 +4025,7 @@ export async function POST(request: NextRequest) { const useFalScaleAdjustCoordinatePrompt = useScaleAdjustCoordinatePrompt && resolvedScaleAdjustModelEntry?.provider?.type === "fal-image" && - !useScaleAdjustFullFrameFalEdit; + getScaleAdjustModelFamily(resolvedScaleAdjustModelEntry) === "nano-banana-2"; const useScaleAdjustVisualLayoutGuide = useScaleAdjustCoordinatePrompt && !useFalScaleAdjustCoordinatePrompt && @@ -4152,7 +4287,7 @@ export async function POST(request: NextRequest) { const imageRequest: CanvasGenerateImageRequest = { allowFailover: isScaleAdjustOperation ? false : undefined, aspectRatio: scaleAdjustPatchReference - ? inferClosestAspectRatio( + ? inferScaleAdjustPatchAspectRatio( scaleAdjustPatchReference.workspacePixelRect.width, scaleAdjustPatchReference.workspacePixelRect.height )