From d6ab02eb454f17600c09107f4c492d415cd406b7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 17:45:53 +0800 Subject: [PATCH] fix: anchor local edit prompts to image node --- components/canvas/nodes/ImageNode.tsx | 594 ++++++++++++-------------- 1 file changed, 275 insertions(+), 319 deletions(-) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index dc128785a..bcb1e7a7a 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -1869,7 +1869,6 @@ export function ImageNode({ const [boxCutoutDragEnd, setBoxCutoutDragEnd] = useState(null); const [localEditMode, setLocalEditMode] = useState(false); - const [localEditViewportRevision, setLocalEditViewportRevision] = useState(0); const [localEditRegions, setLocalEditRegions] = useState( () => getLocalEditRegionsFromMetadata(data.metadata) ); @@ -2048,7 +2047,6 @@ export function ImageNode({ >({}); const localEditPromptDraftRefs = useRef>({}); const localEditRegionsRef = useRef(localEditRegions); - const localEditViewportFrameRef = useRef(null); const updateNode = useCanvasStore((state) => state.updateNode); const deleteNode = useCanvasStore((state) => state.deleteNode); @@ -2417,17 +2415,11 @@ export function ImageNode({ style: getNormalizedRectOverlayStyle(region.rect, boxCutoutMetrics), })); }, [boxCutoutMetrics, localEditRegions]); - const localEditPromptPortalOverlays = useMemo(() => { - if ( - !localEditMode || - !boxCutoutMetrics || - typeof window === "undefined" || - !containerRef.current - ) { + const localEditPromptInlineOverlays = useMemo(() => { + if (!localEditMode || !boxCutoutMetrics) { return []; } - const nodeRect = containerRef.current.getBoundingClientRect(); return localEditRegionOverlays.map(({ region, style }) => { const left = Number(style.left || 0); const top = Number(style.top || 0); @@ -2446,11 +2438,9 @@ export function ImageNode({ region, isEditing: editingLocalEditRegionId === region.id || !region.prompt.trim(), style: { - left: nodeRect.left + left * nodeContentScale, - top: nodeRect.top + Math.max(4, promptTop) * nodeContentScale, + left, + top: Math.max(4, promptTop), width: promptWidth, - transform: `scale(${nodeContentScale})`, - transformOrigin: "top left", } satisfies React.CSSProperties, }; }); @@ -2459,8 +2449,6 @@ export function ImageNode({ editingLocalEditRegionId, localEditMode, localEditRegionOverlays, - localEditViewportRevision, - nodeContentScale, ]); const currentPickerClickStyle = useMemo(() => { if (!boxCutoutMetrics || !currentClick) return null; @@ -2545,44 +2533,6 @@ export function ImageNode({ localEditRegionsRef.current = localEditRegions; }, [localEditRegions]); - useEffect(() => { - if (!localEditMode) return; - - const scheduleLocalEditViewportRefresh = () => { - setLocalEditViewportRevision((revision) => revision + 1); - if (localEditViewportFrameRef.current !== null) { - window.cancelAnimationFrame(localEditViewportFrameRef.current); - } - localEditViewportFrameRef.current = window.requestAnimationFrame(() => { - localEditViewportFrameRef.current = null; - setLocalEditViewportRevision((revision) => revision + 1); - }); - }; - - let previousViewport = useCanvasStore.getState().viewport; - const unsubscribe = useCanvasStore.subscribe((state) => { - const nextViewport = state.viewport; - if ( - nextViewport.x === previousViewport.x && - nextViewport.y === previousViewport.y && - nextViewport.zoom === previousViewport.zoom - ) { - return; - } - - previousViewport = nextViewport; - scheduleLocalEditViewportRefresh(); - }); - - return () => { - unsubscribe(); - if (localEditViewportFrameRef.current !== null) { - window.cancelAnimationFrame(localEditViewportFrameRef.current); - localEditViewportFrameRef.current = null; - } - }; - }, [localEditMode]); - const commitLocalEditRegions = useCallback( (regions: LocalEditRegion[]) => { const nextRegions = reindexLocalEditRegions( @@ -7596,7 +7546,8 @@ export function ImageNode({
); })} + {localEditPromptInlineOverlays.map(({ region, isEditing, style }) => ( +
event.stopPropagation()} + onMouseDown={(event) => event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + style={style} + > +
+ {isEditing ? ( + { + const nextPrompt = event.currentTarget.value; + localEditPromptDraftRefs.current[region.id] = + nextPrompt; + persistLocalEditRegionPrompt(region.id, nextPrompt); + if (isLocalEditControlTarget(event.relatedTarget)) { + return; + } + window.setTimeout(() => { + if ( + isLocalEditControlTarget(document.activeElement) + ) { + return; + } + setEditingLocalEditRegionId((current) => { + if (current === region.id) { + focusLocalEditPromptInput(region.id); + } + return current; + }); + }, 80); + }} + onClick={(event) => { + event.nativeEvent.stopImmediatePropagation?.(); + event.stopPropagation(); + event.currentTarget.focus({ preventScroll: true }); + }} + onFocus={(event) => { + event.stopPropagation(); + setEditingLocalEditRegionId((current) => + current === region.id ? current : region.id + ); + }} + onInput={(event) => { + localEditPromptDraftRefs.current[region.id] = + event.currentTarget.value; + }} + onChange={(event) => { + localEditPromptDraftRefs.current[region.id] = + event.currentTarget.value; + }} + onKeyDown={(event) => { + event.nativeEvent.stopImmediatePropagation?.(); + event.stopPropagation(); + if (event.key === "Enter") { + event.preventDefault(); + persistLocalEditRegionPrompt( + region.id, + event.currentTarget.value + ); + setEditingLocalEditRegionId(null); + } + if (event.key === "Escape") { + event.preventDefault(); + persistLocalEditRegionPrompt( + region.id, + event.currentTarget.value + ); + setEditingLocalEditRegionId(null); + } + }} + onKeyUp={(event) => { + event.nativeEvent.stopImmediatePropagation?.(); + event.stopPropagation(); + }} + onMouseDown={(event) => { + event.nativeEvent.stopImmediatePropagation?.(); + event.stopPropagation(); + }} + onMouseDownCapture={(event) => { + event.nativeEvent.stopImmediatePropagation?.(); + event.stopPropagation(); + event.currentTarget.focus({ preventScroll: true }); + }} + onPointerDown={(event) => { + event.nativeEvent.stopImmediatePropagation?.(); + event.stopPropagation(); + }} + onPointerDownCapture={(event) => { + event.nativeEvent.stopImmediatePropagation?.(); + event.stopPropagation(); + event.currentTarget.focus({ preventScroll: true }); + }} + placeholder={`区域 ${region.index} 提示词`} + ref={(input) => { + localEditPromptInputRefs.current[region.id] = input; + }} + /> + ) : ( + + )} + { + void handleLocalEditReferenceUpload( + region.id, + event.currentTarget.files + ); + event.currentTarget.value = ""; + }} + type="file" + /> + + +
+ {region.referenceImages?.length ? ( +
+ + 参考 + + {region.referenceImages.map((reference) => ( +
+ {reference.name + +
+ ))} +
+ ) : null} +
+ ))} {localEditDraftRectStyle ? (
- {/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */} - {localEditMode && - typeof document !== "undefined" && - localEditPromptPortalOverlays.map(({ region, isEditing, style }) => - createPortal( -
event.stopPropagation()} - onMouseDown={(event) => event.stopPropagation()} - onPointerDown={(event) => event.stopPropagation()} - style={style} - > -
- {isEditing ? ( - { - const nextPrompt = event.currentTarget.value; - localEditPromptDraftRefs.current[region.id] = nextPrompt; - persistLocalEditRegionPrompt(region.id, nextPrompt); - if (isLocalEditControlTarget(event.relatedTarget)) { - return; - } - window.setTimeout(() => { - if (isLocalEditControlTarget(document.activeElement)) { - return; - } - setEditingLocalEditRegionId((current) => { - if (current === region.id) { - focusLocalEditPromptInput(region.id); - } - return current; - }); - }, 80); - }} - onClick={(event) => { - event.nativeEvent.stopImmediatePropagation?.(); - event.stopPropagation(); - event.currentTarget.focus({ preventScroll: true }); - }} - onFocus={(event) => { - event.stopPropagation(); - setEditingLocalEditRegionId((current) => - current === region.id ? current : region.id - ); - }} - onInput={(event) => { - localEditPromptDraftRefs.current[region.id] = - event.currentTarget.value; - }} - onChange={(event) => { - localEditPromptDraftRefs.current[region.id] = - event.currentTarget.value; - }} - onKeyDown={(event) => { - event.nativeEvent.stopImmediatePropagation?.(); - event.stopPropagation(); - if (event.key === "Enter") { - event.preventDefault(); - persistLocalEditRegionPrompt( - region.id, - event.currentTarget.value - ); - setEditingLocalEditRegionId(null); - } - if (event.key === "Escape") { - event.preventDefault(); - persistLocalEditRegionPrompt( - region.id, - event.currentTarget.value - ); - setEditingLocalEditRegionId(null); - } - }} - onKeyUp={(event) => { - event.nativeEvent.stopImmediatePropagation?.(); - event.stopPropagation(); - }} - onMouseDown={(event) => { - event.nativeEvent.stopImmediatePropagation?.(); - event.stopPropagation(); - }} - onMouseDownCapture={(event) => { - event.nativeEvent.stopImmediatePropagation?.(); - event.stopPropagation(); - event.currentTarget.focus({ preventScroll: true }); - }} - onPointerDown={(event) => { - event.nativeEvent.stopImmediatePropagation?.(); - event.stopPropagation(); - }} - onPointerDownCapture={(event) => { - event.nativeEvent.stopImmediatePropagation?.(); - event.stopPropagation(); - event.currentTarget.focus({ preventScroll: true }); - }} - placeholder={`区域 ${region.index} 提示词`} - ref={(input) => { - localEditPromptInputRefs.current[region.id] = input; - }} - /> - ) : ( - - )} - { - void handleLocalEditReferenceUpload( - region.id, - event.currentTarget.files - ); - event.currentTarget.value = ""; - }} - type="file" - /> - - -
- {region.referenceImages?.length ? ( -
- - 参考 - - {region.referenceImages.map((reference) => ( -
- {reference.name - -
- ))} -
- ) : null} -
, - document.body - ) - )} - {localEditMode && !isMultiSelected && data.src &&