From bf8410a6d4bf7035681cb67c3440665b27848932 Mon Sep 17 00:00:00 2001 From: 134 Date: Mon, 22 Jun 2026 19:52:55 +0800 Subject: [PATCH] fix: use target box flow for canvas scale adjust --- components/canvas/nodes/ImageNode.tsx | 284 +++++++++++--------------- 1 file changed, 117 insertions(+), 167 deletions(-) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 5aba33528..74b20bba4 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -396,22 +396,9 @@ type LocalEditRegionInteractionState = { }; type CanvasLocalEditOperationMode = "local_edit" | "scale_adjust"; -type CanvasScaleAdjustPercent = -30 | -20 | -10 | 10 | 20 | 30 | 50 | 100; -const CANVAS_SCALE_TARGET_DEFAULT_PERCENT: CanvasScaleAdjustPercent = 50; const CANVAS_SCALE_TARGET_MIN_PERCENT = -80; const CANVAS_SCALE_TARGET_MAX_PERCENT = 200; -const CANVAS_SCALE_ADJUST_OPTIONS: CanvasScaleAdjustPercent[] = [ - -30, - -20, - -10, - 10, - 20, - 30, - 50, - 100, -]; - function getCanvasScaleAdjustLabel(scalePercent: number) { return scalePercent > 0 ? `放大 ${scalePercent}%` : `缩小 ${Math.abs(scalePercent)}%`; } @@ -440,36 +427,6 @@ function getCanvasScaleAdjustRegionPrompt( ].join(" "); } -function scaleCanvasRectAroundCenter( - rect: NormalizedCropRect, - scalePercent: number -): NormalizedCropRect { - const requestedScale = Math.max(0.05, 1 + scalePercent / 100); - const maxUniformScale = Math.max( - 0.05, - Math.min( - rect.width > 0 ? 1 / rect.width : requestedScale, - rect.height > 0 ? 1 / rect.height : requestedScale - ) - ); - const scale = Math.min(requestedScale, maxUniformScale); - const width = Math.min(1, rect.width * scale); - const height = Math.min(1, rect.height * scale); - const centerX = rect.x + rect.width / 2; - const centerY = rect.y + rect.height / 2; - const x = Math.min(Math.max(centerX - width / 2, 0), Math.max(0, 1 - width)); - const y = Math.min(Math.max(centerY - height / 2, 0), Math.max(0, 1 - height)); - - return { x, y, width, height }; -} - -function getCanvasScaleTargetRect( - region: Pick, - fallbackPercent: number -): NormalizedCropRect { - return region.targetRect || scaleCanvasRectAroundCenter(region.rect, fallbackPercent); -} - function deriveCanvasScalePercentFromRects( sourceRect: NormalizedCropRect, targetRect: NormalizedCropRect | null | undefined @@ -1612,8 +1569,6 @@ export function ImageNode({ const [localEditMode, setLocalEditMode] = useState(false); const [localEditOperationMode, setLocalEditOperationMode] = useState("local_edit"); - const [selectedLocalEditScalePercent, setSelectedLocalEditScalePercent] = - useState(null); const [localEditRegions, setLocalEditRegions] = useState( () => getLocalEditRegionsFromMetadata(data.metadata) ); @@ -1625,6 +1580,8 @@ export function ImageNode({ useState(null); const [localEditDragEnd, setLocalEditDragEnd] = useState(null); + const [localEditDrawingTargetRegionId, setLocalEditDrawingTargetRegionId] = + useState(null); const [localEditRegionInteraction, setLocalEditRegionInteraction] = useState(null); const [editingLocalEditRegionId, setEditingLocalEditRegionId] = @@ -2380,23 +2337,18 @@ export function ImageNode({ if (!boxCutoutMetrics || localEditOperationMode !== "scale_adjust") { return []; } - const fallbackPercent = - selectedLocalEditScalePercent || CANVAS_SCALE_TARGET_DEFAULT_PERCENT; - return localEditRegions.map((region) => { - const targetRect = getCanvasScaleTargetRect(region, fallbackPercent); - return { - region, - targetRect, - scalePercent: deriveCanvasScalePercentFromRects(region.rect, targetRect), - style: getNormalizedRectOverlayStyle(targetRect, boxCutoutMetrics), - }; - }); - }, [ - boxCutoutMetrics, - localEditOperationMode, - localEditRegions, - selectedLocalEditScalePercent, - ]); + return localEditRegions + .filter((region) => region.targetRect) + .map((region) => { + const targetRect = region.targetRect as NormalizedCropRect; + return { + region, + targetRect, + scalePercent: deriveCanvasScalePercentFromRects(region.rect, targetRect), + style: getNormalizedRectOverlayStyle(targetRect, boxCutoutMetrics), + }; + }); + }, [boxCutoutMetrics, localEditOperationMode, localEditRegions]); const localEditPromptInlineOverlays = useMemo(() => { if (!localEditMode || !boxCutoutMetrics) { return []; @@ -2558,10 +2510,10 @@ export function ImageNode({ const resetLocalEditState = useCallback(() => { setLocalEditMode(false); setLocalEditOperationMode("local_edit"); - setSelectedLocalEditScalePercent(null); setIsLocalEditDrawing(false); setLocalEditDragStart(null); setLocalEditDragEnd(null); + setLocalEditDrawingTargetRegionId(null); setLocalEditRegionInteraction(null); setEditingLocalEditRegionId(null); setUploadingLocalEditReferenceRegionId(null); @@ -3525,11 +3477,9 @@ export function ImageNode({ resetBoxCutoutState(); setLocalEditRegions(getLocalEditRegionsFromMetadata(data.metadata)); setLocalEditOperationMode(operationMode); - setSelectedLocalEditScalePercent( - operationMode === "scale_adjust" ? CANVAS_SCALE_TARGET_DEFAULT_PERCENT : null - ); setLocalEditDragStart(null); setLocalEditDragEnd(null); + setLocalEditDrawingTargetRegionId(null); setIsLocalEditDrawing(false); setLocalEditMode(true); window.requestAnimationFrame(() => { @@ -3537,7 +3487,7 @@ export function ImageNode({ }); toast.info( operationMode === "scale_adjust" - ? "先框选要调整比例的主体,再拖动青色目标框调整放大后的大小和位置" + ? "先框选要调整比例的主体,再框选青色目标大小和位置" : "拖拽框选要局部编辑的位置,并在框下方填写提示词" ); }, @@ -3560,7 +3510,11 @@ export function ImageNode({ event.preventDefault(); event.stopPropagation(); - if (localEditRegions.length >= LOCAL_EDIT_MAX_REGIONS) { + const pendingTargetRegion = + localEditOperationMode === "scale_adjust" + ? localEditRegions.find((region) => !region.targetRect) + : null; + if (!pendingTargetRegion && localEditRegions.length >= LOCAL_EDIT_MAX_REGIONS) { toast.info(`局部编辑最多支持 ${LOCAL_EDIT_MAX_REGIONS} 个区域`); return; } @@ -3572,6 +3526,7 @@ export function ImageNode({ if (!pointerResult?.insideImage) return; setEditingLocalEditRegionId(null); + setLocalEditDrawingTargetRegionId(pendingTargetRegion?.id || null); setLocalEditDragStart(pointerResult.point); setLocalEditDragEnd(pointerResult.point); setIsLocalEditDrawing(true); @@ -3582,6 +3537,8 @@ export function ImageNode({ isProcessing, isSubmittingLocalEdit, localEditMode, + localEditOperationMode, + localEditRegions, localEditRegions.length, ] ); @@ -3616,6 +3573,7 @@ export function ImageNode({ setIsLocalEditDrawing(false); setLocalEditDragStart(null); setLocalEditDragEnd(null); + setLocalEditDrawingTargetRegionId(null); return; } @@ -3631,29 +3589,48 @@ export function ImageNode({ nextWidthPx >= LOCAL_EDIT_MIN_EDGE_PX && nextHeightPx >= LOCAL_EDIT_MIN_EDGE_PX ) { + if (localEditDrawingTargetRegionId) { + const nextRegions = reindexLocalEditRegions( + localEditRegions.map((region) => { + if (region.id !== localEditDrawingTargetRegionId) return region; + const scalePercent = deriveCanvasScalePercentFromRects( + region.rect, + nextRect + ); + return { + ...region, + targetRect: nextRect, + prompt: getCanvasScaleAdjustRegionPrompt(scalePercent, true), + }; + }) + ); + commitLocalEditRegions(nextRegions); + toast.info("已设置青色目标框,可拖动它继续调整大小和位置"); + setLocalEditDragStart(null); + setLocalEditDragEnd(null); + setLocalEditDrawingTargetRegionId(null); + setIsLocalEditDrawing(false); + return; + } + const isScaleAdjustMode = localEditOperationMode === "scale_adjust"; - const scalePercent = - selectedLocalEditScalePercent || CANVAS_SCALE_TARGET_DEFAULT_PERCENT; - const targetRect = isScaleAdjustMode - ? scaleCanvasRectAroundCenter(nextRect, scalePercent) - : undefined; const nextRegion: LocalEditRegion = { id: createLocalEditRegionId(), index: localEditRegions.length + 1, color: LOCAL_EDIT_COLORS[localEditRegions.length % LOCAL_EDIT_COLORS.length], prompt: isScaleAdjustMode - ? getCanvasScaleAdjustRegionPrompt(scalePercent, true) + ? "比例调整" : "", rect: nextRect, - ...(targetRect ? { targetRect } : {}), referenceImages: [], createdAt: Date.now(), }; commitLocalEditRegions([...localEditRegions, nextRegion]); setEditingLocalEditRegionId(isScaleAdjustMode ? null : nextRegion.id); if (isScaleAdjustMode) { - toast.info("已生成青色目标框,可拖动它调整放大后的大小和位置"); + setLocalEditDrawingTargetRegionId(nextRegion.id); + toast.info("继续拖拽框选青色目标大小和位置"); } } else { toast.info("框选区域太小,请至少拖出 24px 以上的区域"); @@ -3661,16 +3638,17 @@ export function ImageNode({ setLocalEditDragStart(null); setLocalEditDragEnd(null); + setLocalEditDrawingTargetRegionId(null); setIsLocalEditDrawing(false); }, [ commitLocalEditRegions, getBoxCutoutPointerPoint, localEditDragStart, + localEditDrawingTargetRegionId, localEditMode, localEditOperationMode, localEditRegions, - selectedLocalEditScalePercent, ] ); @@ -3695,6 +3673,7 @@ export function ImageNode({ setIsLocalEditDrawing(false); setLocalEditDragStart(null); setLocalEditDragEnd(null); + setLocalEditDrawingTargetRegionId(null); event.currentTarget.releasePointerCapture?.(event.pointerId); }, [localEditMode] @@ -3711,7 +3690,6 @@ export function ImageNode({ localEditRegionsRef.current.map((region) => { if (region.id !== regionId) return region; if (target === "scaleTarget") { - setSelectedLocalEditScalePercent(null); const scalePercent = deriveCanvasScalePercentFromRects( region.rect, rect @@ -4001,23 +3979,6 @@ export function ImageNode({ setEditingLocalEditRegionId(null); }, [commitLocalEditRegions]); - const applyLocalEditScalePercent = useCallback( - (scalePercent: CanvasScaleAdjustPercent) => { - setSelectedLocalEditScalePercent(scalePercent); - const nextRegions = reindexLocalEditRegions( - localEditRegionsRef.current.map((region) => ({ - ...region, - targetRect: scaleCanvasRectAroundCenter(region.rect, scalePercent), - prompt: getCanvasScaleAdjustRegionPrompt(scalePercent, true), - })) - ); - localEditRegionsRef.current = nextRegions; - setLocalEditRegions(nextRegions); - commitLocalEditRegions(nextRegions); - }, - [commitLocalEditRegions] - ); - const handleSubmitLocalEdit = useCallback(async () => { if (!data.src) { toast.error("请先上传图片"); @@ -4033,19 +3994,15 @@ export function ImageNode({ } const isScaleAdjustMode = localEditOperationMode === "scale_adjust"; - const scaleFallbackPercent = - selectedLocalEditScalePercent || CANVAS_SCALE_TARGET_DEFAULT_PERCENT; const validRegions = reindexLocalEditRegions( localEditRegions.map((region) => { if (isScaleAdjustMode) { - const targetRect = getCanvasScaleTargetRect(region, scaleFallbackPercent); const regionScalePercent = deriveCanvasScalePercentFromRects( region.rect, - targetRect + region.targetRect ); return { ...region, - targetRect, prompt: getCanvasScaleAdjustRegionPrompt(regionScalePercent, true), }; } @@ -4063,6 +4020,11 @@ export function ImageNode({ return; } if (isScaleAdjustMode) { + const missingTargetRegion = validRegions.find((region) => !region.targetRect); + if (missingTargetRegion) { + toast.error(`请为区域 ${missingTargetRegion.index} 框选青色目标框`); + return; + } const unchangedRegion = validRegions.find((region) => { const regionScalePercent = deriveCanvasScalePercentFromRects( region.rect, @@ -4282,7 +4244,6 @@ export function ImageNode({ node.size.width, resetLocalEditState, selectNode, - selectedLocalEditScalePercent, updateNode, ]); @@ -8675,24 +8636,32 @@ export function ImageNode({ setEditingLocalEditRegionId(region.id); } }} - title={ - localEditOperationMode === "scale_adjust" - ? selectedLocalEditScalePercent - ? getCanvasScaleAdjustLabel(selectedLocalEditScalePercent) - : "请选择比例" - : region.prompt - } - type="button" - > - {localEditOperationMode === "scale_adjust" - ? `${region.index}. ${ - selectedLocalEditScalePercent - ? getCanvasScaleAdjustLabel( - selectedLocalEditScalePercent - ) - : "请选择比例" - }` - : `${region.index}. ${region.prompt}`} + title={ + localEditOperationMode === "scale_adjust" + ? region.targetRect + ? getCanvasScaleAdjustLabel( + deriveCanvasScalePercentFromRects( + region.rect, + region.targetRect + ) + ) + : "请框选青色目标框" + : region.prompt + } + type="button" + > + {localEditOperationMode === "scale_adjust" + ? `${region.index}. ${ + region.targetRect + ? getCanvasScaleAdjustLabel( + deriveCanvasScalePercentFromRects( + region.rect, + region.targetRect + ) + ) + : "请框选目标框" + }` + : `${region.index}. ${region.prompt}`} )} { - setLocalEditOperationMode("local_edit"); - setSelectedLocalEditScalePercent(null); - }} + disabled={isSubmittingLocalEdit} + onClick={() => { + setLocalEditOperationMode("local_edit"); + }} type="button" variant="ghost" > @@ -9311,14 +9284,11 @@ export function ImageNode({ ? "bg-white text-slate-950 shadow-sm dark:bg-white dark:text-slate-950" : "text-slate-500 hover:bg-white/70 hover:text-slate-900 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-white" )} - disabled={isSubmittingLocalEdit} - onClick={() => { - setLocalEditOperationMode("scale_adjust"); - if (!selectedLocalEditScalePercent) { - applyLocalEditScalePercent(CANVAS_SCALE_TARGET_DEFAULT_PERCENT); - } - setEditingLocalEditRegionId(null); - }} + disabled={isSubmittingLocalEdit} + onClick={() => { + setLocalEditOperationMode("scale_adjust"); + setEditingLocalEditRegionId(null); + }} type="button" variant="ghost" > @@ -9328,43 +9298,23 @@ export function ImageNode({
- {localEditOperationMode === "scale_adjust" ? ( -
- - 拖动青色目标框 - - {CANVAS_SCALE_ADJUST_OPTIONS.map((option) => { - const active = selectedLocalEditScalePercent === option; - const Icon = option > 0 ? Maximize2 : Minimize2; - return ( - - ); - })} -
- ) : null} + {localEditOperationMode === "scale_adjust" ? ( +
+ + + 先框主体,再框目标大小和位置 + +
+ ) : null}