From 088d2c1b74a3a1a4233861cd2bf44f1f1202121f Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 22 Jun 2026 20:08:14 +0800 Subject: [PATCH] fix: sync detail scale target flow --- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 403 +++++++++++++----- 1 file changed, 294 insertions(+), 109 deletions(-) diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 730018919..54a07fdfb 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -28,7 +28,6 @@ import { ImagePlus, Loader2, Maximize2, - Minimize2, Sparkles, Upload, UserRound, @@ -247,7 +246,7 @@ type DetailResultLocalEditTarget = { }; type DetailResultLocalEditMode = "local_edit" | "scale_adjust"; -type DetailResultScalePercent = -30 | -20 | -10 | 10 | 20 | 30 | 50 | 100; +type DetailResultScalePercent = number; type DetailResultLocalEditRect = { x: number; @@ -258,12 +257,15 @@ type DetailResultLocalEditRect = { type DetailResultLocalEditRegionHandle = "move" | "nw" | "ne" | "sw" | "se"; +type DetailResultLocalEditRegionInteractionTarget = "source" | "scaleTarget"; + type DetailResultLocalEditRegionInteraction = { handle: DetailResultLocalEditRegionHandle; pointerId: number; regionId: string; startPoint: { x: number; y: number }; startRect: DetailResultLocalEditRect; + target: DetailResultLocalEditRegionInteractionTarget; }; type DetailResultLocalEditSubmitParams = { @@ -291,17 +293,9 @@ const MAX_AGENT_MODE_QUERY_LENGTH = 64; const MAX_AGENT_MODE_REFERENCE_COUNT = 40; const DETAIL_LOCAL_EDIT_REFERENCE_MAX_UPLOAD_BYTES = 30 * 1024 * 1024; const REPLACEMENT_RERUN_MAX_REFERENCE_IMAGES = 4; -const DETAIL_SCALE_ADJUST_OPTIONS: DetailResultScalePercent[] = [ - -30, - -20, - -10, - 10, - 20, - 30, - 50, - 100, -]; const DETAIL_LOCAL_EDIT_MIN_RECT_RATIO = 0.025; +const DETAIL_SCALE_TARGET_MIN_PERCENT = -80; +const DETAIL_SCALE_TARGET_MAX_PERCENT = 200; const DIRECT_MODEL_UPLOAD_SLOT_COUNT = 4; const PRODUCT_DETAIL_CHANNEL_OPTIONS: Array<{ value: AgentWorkflowProductDetailChannel; @@ -852,18 +846,22 @@ function getDetailScaleAdjustLabel(scalePercent: DetailResultScalePercent) { } function getDetailScaleAdjustTargetLabel(scalePercent: DetailResultScalePercent) { - return `${Math.round((1 + scalePercent / 100) * 100)}%`; + return `${Math.max(5, Math.round((1 + scalePercent / 100) * 100))}%`; } -function getDetailScaleAdjustRegionPrompt(scalePercent: DetailResultScalePercent | null) { +function getDetailScaleAdjustRegionPrompt( + scalePercent: DetailResultScalePercent | null, + hasTargetPosition = false +) { if (!scalePercent) return "比例调整"; const actionLabel = getDetailScaleAdjustLabel(scalePercent); const targetLabel = getDetailScaleAdjustTargetLabel(scalePercent); const scaleVerb = scalePercent > 0 ? "放大" : "缩小"; return [ `对框选范围内的主物体执行等比${actionLabel},调整后主体视觉尺寸必须约为原来的 ${targetLabel},需要肉眼明显看出已经${scaleVerb}。`, - "必须锁定主体自身原始宽高比和透视形体,宽度与高度用同一个倍率同步变化;严禁宽度变窄但高度变高、严禁横向挤压或纵向拉伸。", - "框选区域只用于识别要调整的主体,不是放大后的边界;放大时主体可以自然超出原始框选范围,并由周边背景补全衔接。", + hasTargetPosition + ? "主体框只用于识别要调整的主体;青色目标框表示调整后的目标大小和目标位置,放大后的主体可以自然超出主体框,并由周边背景补全衔接。" + : "框选区域只用于识别要调整的主体,不是放大后的边界;放大时主体可以自然超出原始框选范围,并由周边背景补全衔接。", "只改变目标主体的尺寸,不改变主体外观、颜色、材质、图案和文字内容;不要把整个框选矩形当成图片块缩放。", scalePercent < 0 ? "主体缩小后露出的背景、桌面、墙面、阴影和遮挡关系必须自然补全。" @@ -871,6 +869,23 @@ function getDetailScaleAdjustRegionPrompt(scalePercent: DetailResultScalePercent ].join(" "); } +function deriveDetailScalePercentFromRects( + sourceRect: DetailResultLocalEditRect, + targetRect: DetailResultLocalEditRect | null | undefined +) { + if (!targetRect || sourceRect.width <= 0 || sourceRect.height <= 0) { + return 0; + } + 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); + return Math.min( + DETAIL_SCALE_TARGET_MAX_PERCENT, + Math.max(DETAIL_SCALE_TARGET_MIN_PERCENT, percent) + ); +} + function DetailResultLocalEditDialog({ previewResult, submitting, @@ -894,8 +909,7 @@ function DetailResultLocalEditDialog({ const [regionInteraction, setRegionInteraction] = useState(null); const [uploadingRegionId, setUploadingRegionId] = useState(null); - const [selectedScalePercent, setSelectedScalePercent] = - useState(null); + const [drawingTargetRegionId, setDrawingTargetRegionId] = useState(null); const dragStartRef = useRef<{ x: number; y: number } | null>(null); const editSurfaceRef = useRef(null); const mode: DetailResultLocalEditMode = @@ -906,6 +920,10 @@ function DetailResultLocalEditDialog({ ? previewResult : null; const isBusy = submitting || Boolean(uploadingRegionId); + const pendingScaleTargetRegion = isScaleAdjustMode + ? regions.find((region) => !region.targetRect) + : undefined; + const isDrawingScaleTarget = isScaleAdjustMode && Boolean(drawingTargetRegionId); useEffect(() => { setRegions([]); @@ -913,7 +931,7 @@ function DetailResultLocalEditDialog({ setActiveRegionId(null); setRegionInteraction(null); setUploadingRegionId(null); - setSelectedScalePercent(null); + setDrawingTargetRegionId(null); dragStartRef.current = null; }, [target?.cardId, target?.imageUrl, target?.mode]); @@ -970,15 +988,40 @@ function DetailResultLocalEditDialog({ }, []); const updateRegionRect = useCallback( - (regionId: string, rect: DetailResultLocalEditRect) => { + ( + regionId: string, + rect: DetailResultLocalEditRect, + targetKind: DetailResultLocalEditRegionInteractionTarget = "source" + ) => { const nextRect = clampRegionRect(rect); setRegions((current) => current.map((region) => - region.id === regionId ? { ...region, rect: nextRect } : region + region.id === regionId + ? targetKind === "scaleTarget" + ? { + ...region, + targetRect: nextRect, + prompt: getDetailScaleAdjustRegionPrompt( + deriveDetailScalePercentFromRects(region.rect, nextRect), + true + ), + } + : { + ...region, + rect: nextRect, + prompt: + isScaleAdjustMode && region.targetRect + ? getDetailScaleAdjustRegionPrompt( + deriveDetailScalePercentFromRects(nextRect, region.targetRect), + true + ) + : region.prompt, + } + : region ) ); }, - [clampRegionRect] + [clampRegionRect, isScaleAdjustMode] ); const calculateInteractionRect = useCallback( @@ -1138,7 +1181,10 @@ function DetailResultLocalEditDialog({ regions.map((region) => ({ ...region, prompt: isScaleAdjustMode - ? getDetailScaleAdjustRegionPrompt(selectedScalePercent) + ? getDetailScaleAdjustRegionPrompt( + deriveDetailScalePercentFromRects(region.rect, region.targetRect), + Boolean(region.targetRect) + ) : region.prompt.trim(), referenceImages: (region.referenceImages || []).slice( 0, @@ -1160,8 +1206,18 @@ function DetailResultLocalEditDialog({ return; } if (isScaleAdjustMode) { - if (!selectedScalePercent) { - toast.error("请选择放大或缩小比例"); + const missingTargetRegion = validRegions.find((region) => !region.targetRect); + if (missingTargetRegion) { + setActiveRegionId(missingTargetRegion.id); + toast.error(`请为区域 ${missingTargetRegion.index} 框选青色目标框`); + return; + } + const unchangedRegion = validRegions.find( + (region) => deriveDetailScalePercentFromRects(region.rect, region.targetRect) === 0 + ); + if (unchangedRegion) { + setActiveRegionId(unchangedRegion.id); + toast.error(`区域 ${unchangedRegion.index} 的目标框大小变化不明显,请重新框选`); return; } } else { @@ -1175,11 +1231,17 @@ function DetailResultLocalEditDialog({ onSubmit({ imageUrl: target.imageUrl, mode, - scalePercent: isScaleAdjustMode ? selectedScalePercent || undefined : undefined, + scalePercent: + isScaleAdjustMode && validRegions[0] + ? deriveDetailScalePercentFromRects( + validRegions[0].rect, + validRegions[0].targetRect + ) || undefined + : undefined, title: target.title, regions: validRegions, }); - }, [isScaleAdjustMode, mode, onSubmit, regions, selectedScalePercent, target, uploadingRegionId]); + }, [isScaleAdjustMode, mode, onSubmit, regions, target, uploadingRegionId]); const completeDraftRegion = useCallback( (event: React.PointerEvent) => { @@ -1188,8 +1250,10 @@ function DetailResultLocalEditDialog({ const point = getPoint(event); const nextRect = createRectFromPoints(start, point); + const targetRegionId = drawingTargetRegionId; dragStartRef.current = null; setDraftRect(null); + setDrawingTargetRegionId(null); if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); @@ -1203,13 +1267,32 @@ function DetailResultLocalEditDialog({ return; } + if (isScaleAdjustMode && targetRegionId) { + setRegions((current) => + reindexLocalEditRegions( + current.map((region) => + region.id === targetRegionId + ? { + ...region, + targetRect: nextRect, + prompt: getDetailScaleAdjustRegionPrompt( + deriveDetailScalePercentFromRects(region.rect, nextRect), + true + ), + } + : region + ) + ) + ); + setActiveRegionId(targetRegionId); + return; + } + const nextRegion: LocalEditRegion = { id: createLocalEditRegionId(), index: regions.length + 1, color: LOCAL_EDIT_COLORS[regions.length % LOCAL_EDIT_COLORS.length], - prompt: isScaleAdjustMode - ? getDetailScaleAdjustRegionPrompt(selectedScalePercent) - : "", + prompt: isScaleAdjustMode ? getDetailScaleAdjustRegionPrompt(null) : "", rect: nextRect, referenceImages: [], createdAt: Date.now(), @@ -1220,14 +1303,16 @@ function DetailResultLocalEditDialog({ }); setActiveRegionId(nextRegion.id); }, - [createRectFromPoints, getPoint, isScaleAdjustMode, regions.length, selectedScalePercent] + [createRectFromPoints, drawingTargetRegionId, getPoint, isScaleAdjustMode, regions.length] ); const handleRegionInteractionStart = useCallback( ( event: React.PointerEvent, region: LocalEditRegion, - fallbackHandle: DetailResultLocalEditRegionHandle = "move" + fallbackHandle: DetailResultLocalEditRegionHandle = "move", + targetKind: DetailResultLocalEditRegionInteractionTarget = "source", + targetRect?: DetailResultLocalEditRect | null ) => { if (isBusy) return; const targetElement = event.target as HTMLElement | null; @@ -1239,13 +1324,15 @@ function DetailResultLocalEditDialog({ setActiveRegionId(region.id); dragStartRef.current = null; setDraftRect(null); + setDrawingTargetRegionId(null); event.currentTarget.setPointerCapture(event.pointerId); setRegionInteraction({ handle, pointerId: event.pointerId, regionId: region.id, startPoint: getPoint(event), - startRect: region.rect, + startRect: targetKind === "scaleTarget" ? targetRect || region.rect : region.rect, + target: targetKind, }); }, [getPoint, isBusy] @@ -1262,7 +1349,11 @@ function DetailResultLocalEditDialog({ } event.preventDefault(); event.stopPropagation(); - updateRegionRect(regionId, calculateInteractionRect(regionInteraction, getPoint(event))); + updateRegionRect( + regionId, + calculateInteractionRect(regionInteraction, getPoint(event)), + regionInteraction.target + ); }, [calculateInteractionRect, getPoint, regionInteraction, updateRegionRect] ); @@ -1276,7 +1367,11 @@ function DetailResultLocalEditDialog({ ) { event.preventDefault(); event.stopPropagation(); - updateRegionRect(regionId, calculateInteractionRect(regionInteraction, getPoint(event))); + updateRegionRect( + regionId, + calculateInteractionRect(regionInteraction, getPoint(event)), + regionInteraction.target + ); setRegionInteraction(null); } if (event.currentTarget.hasPointerCapture(event.pointerId)) { @@ -1292,7 +1387,7 @@ function DetailResultLocalEditDialog({ : "处理完成后会加入 AI详情结果区。"; const dialogTitle = isScaleAdjustMode ? "比例调整详情图" : "局部编辑详情图"; const dialogDescription = isScaleAdjustMode - ? "框选要调整比例的物体,选择固定放大或缩小幅度后生成。" + ? "先框选要调整比例的物体,再框选目标大小和位置后生成。" : "在图片上拖拽框选编辑位置,可添加多个区域,并为每个区域上传专属参考图。"; return ( @@ -1329,24 +1424,30 @@ function DetailResultLocalEditDialog({ updateDraftRect(event); dragStartRef.current = null; setDraftRect(null); + setDrawingTargetRegionId(null); if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } }} - onPointerDown={(event) => { - if (event.button !== 0 || isBusy) return; - if (regions.length >= LOCAL_EDIT_MAX_REGIONS) { - toast.info( - `${isScaleAdjustMode ? "比例调整" : "局部编辑"}最多支持 ${LOCAL_EDIT_MAX_REGIONS} 个区域` - ); - return; - } + onPointerDown={(event) => { + if (event.button !== 0 || isBusy) return; + const targetRegion = isScaleAdjustMode + ? regions.find((region) => !region.targetRect) + : undefined; + if (!targetRegion && regions.length >= LOCAL_EDIT_MAX_REGIONS) { + toast.info( + `${isScaleAdjustMode ? "比例调整" : "局部编辑"}最多支持 ${LOCAL_EDIT_MAX_REGIONS} 个区域` + ); + return; + } event.preventDefault(); event.currentTarget.setPointerCapture(event.pointerId); - const point = getPoint(event); - dragStartRef.current = point; - setDraftRect({ x: point.x, y: point.y, width: 0.01, height: 0.01 }); - }} + const point = getPoint(event); + dragStartRef.current = point; + setDrawingTargetRegionId(targetRegion?.id || null); + if (targetRegion) setActiveRegionId(targetRegion.id); + setDraftRect({ x: point.x, y: point.y, width: 0.01, height: 0.01 }); + }} onPointerMove={(event) => { if (!dragStartRef.current) return; event.preventDefault(); @@ -1446,11 +1547,111 @@ function DetailResultLocalEditDialog({ )) : null} - ); - })} + ); + })} + {isScaleAdjustMode + ? regions + .filter((region) => region.targetRect) + .map((region) => { + const targetRect = region.targetRect as DetailResultLocalEditRect; + const scalePercent = deriveDetailScalePercentFromRects( + region.rect, + targetRect + ); + const isActive = region.id === activeRegionId; + return ( + + ); + }) + : null} {draftRect ? (
-
- {isScaleAdjustMode - ? `拖拽框选物体,建议留出少量周边背景,最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域。${completionCopy}` - : `拖拽图片添加区域。最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域,每个区域最多 ${LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION} 张参考图。${completionCopy}`} -
- {!activePreviewResult && isScaleAdjustMode ? ( -
-
- Scale -
-
- {DETAIL_SCALE_ADJUST_OPTIONS.map((option) => { - const active = selectedScalePercent === option; - const Icon = option > 0 ? Maximize2 : Minimize2; - const labelAction = option > 0 ? "放大" : "缩小"; - const labelValue = `${Math.abs(option)}%`; - return ( - - ); - })} -
-
- ) : null} +
+ {isScaleAdjustMode + ? `先拖拽框选主体,再拖拽青色目标框设置大小和位置,最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域。${completionCopy}` + : `拖拽图片添加区域。最多 ${LOCAL_EDIT_MAX_REGIONS} 个区域,每个区域最多 ${LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION} 张参考图。${completionCopy}`} +
+ {!activePreviewResult && isScaleAdjustMode ? ( +
+
+ Target Box +
+
+ {pendingScaleTargetRegion + ? `继续在左侧为区域 ${pendingScaleTargetRegion.index} 拖拽青色目标框。` + : regions.length > 0 + ? "目标框已补齐,可拖动或拉伸青色框继续调整位置和大小。" + : "先在左侧拖拽框选要调整比例的主体。"} +
+
+ ) : null} {activePreviewResult ? (
@@ -1521,11 +1700,11 @@ function DetailResultLocalEditDialog({
) : regions.length === 0 ? ( -
- {isScaleAdjustMode - ? "在左侧图片上拖拽框选物体后,选择一个比例即可生成。" - : "在左侧图片上拖拽框选后,这里会显示区域提示词和参考图。"} -
+
+ {isScaleAdjustMode + ? "在左侧图片上先框主体,再框青色目标大小和位置。" + : "在左侧图片上拖拽框选后,这里会显示区域提示词和参考图。"} +
) : (
{regions.map((region) => { @@ -1556,12 +1735,17 @@ function DetailResultLocalEditDialog({ 区域 {region.index} - {isScaleAdjustMode ? ( - - {selectedScalePercent - ? getDetailScaleAdjustLabel(selectedScalePercent) - : "待选比例"} - + {isScaleAdjustMode ? ( + + {region.targetRect + ? getDetailScaleAdjustLabel( + deriveDetailScalePercentFromRects( + region.rect, + region.targetRect + ) + ) + : "待框目标框"} + ) : ( 参考图 {references.length}/ @@ -1706,12 +1890,12 @@ function DetailResultLocalEditDialog({