fix: enable local edit region controls
This commit is contained in:
parent
f87669c15a
commit
06153cc179
@ -227,6 +227,14 @@ type CropInteractionState = {
|
||||
startRect: NormalizedCropRect;
|
||||
};
|
||||
|
||||
type LocalEditRegionInteractionState = {
|
||||
handle: CropInteractionHandle;
|
||||
pointerId: number;
|
||||
regionId: string;
|
||||
startPoint: BoxCutoutPoint;
|
||||
startRect: NormalizedCropRect;
|
||||
};
|
||||
|
||||
type AnglePreviewDragState = {
|
||||
pointerId: number;
|
||||
startX: number;
|
||||
@ -1825,6 +1833,8 @@ export function ImageNode({
|
||||
useState<BoxCutoutPoint | null>(null);
|
||||
const [localEditDragEnd, setLocalEditDragEnd] =
|
||||
useState<BoxCutoutPoint | null>(null);
|
||||
const [localEditRegionInteraction, setLocalEditRegionInteraction] =
|
||||
useState<LocalEditRegionInteractionState | null>(null);
|
||||
const [editingLocalEditRegionId, setEditingLocalEditRegionId] =
|
||||
useState<string | null>(null);
|
||||
const [isSubmittingLocalEdit, setIsSubmittingLocalEdit] = useState(false);
|
||||
@ -1988,6 +1998,7 @@ export function ImageNode({
|
||||
Record<string, HTMLInputElement | null>
|
||||
>({});
|
||||
const localEditPromptDraftRefs = useRef<Record<string, string>>({});
|
||||
const localEditRegionsRef = useRef<LocalEditRegion[]>(localEditRegions);
|
||||
|
||||
const updateNode = useCanvasStore((state) => state.updateNode);
|
||||
const deleteNode = useCanvasStore((state) => state.deleteNode);
|
||||
@ -2454,6 +2465,7 @@ export function ImageNode({
|
||||
setIsLocalEditDrawing(false);
|
||||
setLocalEditDragStart(null);
|
||||
setLocalEditDragEnd(null);
|
||||
setLocalEditRegionInteraction(null);
|
||||
setEditingLocalEditRegionId(null);
|
||||
}, []);
|
||||
|
||||
@ -2473,11 +2485,16 @@ export function ImageNode({
|
||||
focusLocalEditPromptInput(editingLocalEditRegionId);
|
||||
}, [editingLocalEditRegionId, focusLocalEditPromptInput, localEditMode]);
|
||||
|
||||
useEffect(() => {
|
||||
localEditRegionsRef.current = localEditRegions;
|
||||
}, [localEditRegions]);
|
||||
|
||||
const commitLocalEditRegions = useCallback(
|
||||
(regions: LocalEditRegion[]) => {
|
||||
const nextRegions = reindexLocalEditRegions(
|
||||
regions.slice(0, LOCAL_EDIT_MAX_REGIONS)
|
||||
);
|
||||
localEditRegionsRef.current = nextRegions;
|
||||
setLocalEditRegions(nextRegions);
|
||||
localEditPromptDraftRefs.current = Object.fromEntries(
|
||||
nextRegions.map((region) => [region.id, region.prompt])
|
||||
@ -3432,6 +3449,150 @@ export function ImageNode({
|
||||
[localEditMode]
|
||||
);
|
||||
|
||||
const updateLocalEditRegionRect = useCallback(
|
||||
(regionId: string, rect: NormalizedCropRect, shouldCommit = false) => {
|
||||
const nextRegions = reindexLocalEditRegions(
|
||||
localEditRegionsRef.current.map((region) =>
|
||||
region.id === regionId ? { ...region, rect } : region
|
||||
)
|
||||
);
|
||||
localEditRegionsRef.current = nextRegions;
|
||||
setLocalEditRegions(nextRegions);
|
||||
if (shouldCommit) {
|
||||
commitLocalEditRegions(nextRegions);
|
||||
}
|
||||
},
|
||||
[commitLocalEditRegions]
|
||||
);
|
||||
|
||||
const handleLocalEditRegionPointerDown = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>, region: LocalEditRegion) => {
|
||||
if (!localEditMode || isProcessing || isSubmittingLocalEdit) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const pointerResult = getBoxCutoutPointerPoint(
|
||||
event.clientX,
|
||||
event.clientY
|
||||
);
|
||||
if (!pointerResult?.insideImage) return;
|
||||
|
||||
const handleElement = (event.target as HTMLElement | null)?.closest(
|
||||
"[data-local-edit-region-handle]"
|
||||
) as HTMLElement | null;
|
||||
const handle =
|
||||
(handleElement?.dataset.localEditRegionHandle as
|
||||
| CropInteractionHandle
|
||||
| undefined) || "move";
|
||||
|
||||
setIsLocalEditDrawing(false);
|
||||
setLocalEditDragStart(null);
|
||||
setLocalEditDragEnd(null);
|
||||
setEditingLocalEditRegionId(null);
|
||||
setLocalEditRegionInteraction({
|
||||
handle,
|
||||
pointerId: event.pointerId,
|
||||
regionId: region.id,
|
||||
startPoint: pointerResult.point,
|
||||
startRect: region.rect,
|
||||
});
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
},
|
||||
[
|
||||
getBoxCutoutPointerPoint,
|
||||
isProcessing,
|
||||
isSubmittingLocalEdit,
|
||||
localEditMode,
|
||||
]
|
||||
);
|
||||
|
||||
const handleLocalEditRegionPointerMove = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>, regionId: string) => {
|
||||
if (
|
||||
!localEditMode ||
|
||||
!localEditRegionInteraction ||
|
||||
localEditRegionInteraction.regionId !== regionId ||
|
||||
localEditRegionInteraction.pointerId !== event.pointerId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const pointerResult = getBoxCutoutPointerPoint(
|
||||
event.clientX,
|
||||
event.clientY
|
||||
);
|
||||
if (!pointerResult) return;
|
||||
|
||||
const minWidth = Math.min(
|
||||
LOCAL_EDIT_MIN_EDGE_PX / Math.max(pointerResult.metrics.naturalWidth, 1),
|
||||
1
|
||||
);
|
||||
const minHeight = Math.min(
|
||||
LOCAL_EDIT_MIN_EDGE_PX / Math.max(pointerResult.metrics.naturalHeight, 1),
|
||||
1
|
||||
);
|
||||
const nextRect = updateCropRectWithPointer({
|
||||
aspectRatio: null,
|
||||
handle: localEditRegionInteraction.handle,
|
||||
minHeight,
|
||||
minWidth,
|
||||
pointer: pointerResult.point,
|
||||
startPoint: localEditRegionInteraction.startPoint,
|
||||
startRect: localEditRegionInteraction.startRect,
|
||||
});
|
||||
|
||||
updateLocalEditRegionRect(regionId, nextRect);
|
||||
},
|
||||
[
|
||||
getBoxCutoutPointerPoint,
|
||||
localEditMode,
|
||||
localEditRegionInteraction,
|
||||
updateLocalEditRegionRect,
|
||||
]
|
||||
);
|
||||
|
||||
const handleLocalEditRegionPointerUp = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>, regionId: string) => {
|
||||
if (
|
||||
!localEditRegionInteraction ||
|
||||
localEditRegionInteraction.regionId !== regionId ||
|
||||
localEditRegionInteraction.pointerId !== event.pointerId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setLocalEditRegionInteraction(null);
|
||||
commitLocalEditRegions(localEditRegionsRef.current);
|
||||
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
||||
},
|
||||
[commitLocalEditRegions, localEditRegionInteraction]
|
||||
);
|
||||
|
||||
const handleLocalEditRegionPointerCancel = useCallback(
|
||||
(event: React.PointerEvent<HTMLDivElement>, regionId: string) => {
|
||||
if (
|
||||
!localEditRegionInteraction ||
|
||||
localEditRegionInteraction.regionId !== regionId ||
|
||||
localEditRegionInteraction.pointerId !== event.pointerId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setLocalEditRegionInteraction(null);
|
||||
commitLocalEditRegions(localEditRegionsRef.current);
|
||||
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
||||
},
|
||||
[commitLocalEditRegions, localEditRegionInteraction]
|
||||
);
|
||||
|
||||
const persistLocalEditRegionPrompt = useCallback(
|
||||
(regionId: string, prompt: string) => {
|
||||
localEditPromptDraftRefs.current[regionId] = prompt;
|
||||
@ -3448,13 +3609,13 @@ export function ImageNode({
|
||||
(regionId: string) => {
|
||||
delete localEditPromptDraftRefs.current[regionId];
|
||||
commitLocalEditRegions(
|
||||
localEditRegions.filter((region) => region.id !== regionId)
|
||||
localEditRegionsRef.current.filter((region) => region.id !== regionId)
|
||||
);
|
||||
if (editingLocalEditRegionId === regionId) {
|
||||
setEditingLocalEditRegionId(null);
|
||||
}
|
||||
},
|
||||
[commitLocalEditRegions, editingLocalEditRegionId, localEditRegions]
|
||||
[commitLocalEditRegions, editingLocalEditRegionId]
|
||||
);
|
||||
|
||||
const clearLocalEditRegions = useCallback(() => {
|
||||
@ -7563,8 +7724,31 @@ export function ImageNode({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute z-[160] rounded-[10px] border-[3px] bg-transparent shadow-[0_0_0_1px_rgba(255,255,255,0.92),0_0_22px_rgba(0,0,0,0.2)]"
|
||||
className={cn(
|
||||
"nodrag nopan nowheel pointer-events-auto absolute z-[160] rounded-[10px] border-[3px] bg-transparent shadow-[0_0_0_1px_rgba(255,255,255,0.92),0_0_22px_rgba(0,0,0,0.2)]",
|
||||
localEditRegionInteraction?.regionId === region.id
|
||||
? "cursor-grabbing"
|
||||
: "cursor-grab"
|
||||
)}
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
key={region.id}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onPointerCancel={(event) =>
|
||||
handleLocalEditRegionPointerCancel(event, region.id)
|
||||
}
|
||||
onPointerDown={(event) =>
|
||||
handleLocalEditRegionPointerDown(event, region)
|
||||
}
|
||||
onPointerMove={(event) =>
|
||||
handleLocalEditRegionPointerMove(event, region.id)
|
||||
}
|
||||
onPointerUp={(event) =>
|
||||
handleLocalEditRegionPointerUp(event, region.id)
|
||||
}
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
@ -7579,6 +7763,40 @@ export function ImageNode({
|
||||
>
|
||||
{region.index}
|
||||
</div>
|
||||
{(
|
||||
[
|
||||
{
|
||||
handle: "nw",
|
||||
className: "-left-2 -top-2 cursor-nwse-resize",
|
||||
},
|
||||
{
|
||||
handle: "ne",
|
||||
className: "-right-2 -top-2 cursor-nesw-resize",
|
||||
},
|
||||
{
|
||||
handle: "sw",
|
||||
className: "-left-2 -bottom-2 cursor-nesw-resize",
|
||||
},
|
||||
{
|
||||
handle: "se",
|
||||
className: "-right-2 -bottom-2 cursor-nwse-resize",
|
||||
},
|
||||
] as const
|
||||
).map((item) => (
|
||||
<button
|
||||
aria-label={`调整区域 ${region.index}`}
|
||||
className={cn(
|
||||
"nodrag nopan nowheel absolute h-4 w-4 rounded-full border-2 border-white shadow-[0_4px_12px_rgba(0,0,0,0.22)]",
|
||||
item.className
|
||||
)}
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
data-local-edit-region-handle={item.handle}
|
||||
key={item.handle}
|
||||
style={{ backgroundColor: region.color }}
|
||||
type="button"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@ -8148,7 +8366,7 @@ export function ImageNode({
|
||||
localEditPromptPortalOverlays.map(({ region, isEditing, style }) =>
|
||||
createPortal(
|
||||
<div
|
||||
className="fixed z-[10000] flex items-center gap-1.5"
|
||||
className="nodrag nopan nowheel fixed z-[10000] flex items-center gap-1.5"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
key={region.id}
|
||||
@ -8256,9 +8474,26 @@ export function ImageNode({
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="h-9 min-w-0 flex-1 truncate rounded-full border border-slate-200 bg-white/95 px-3 text-left text-[12px] font-semibold text-slate-900 shadow-[0_10px_24px_rgba(15,23,42,0.14)] hover:bg-slate-50 dark:border-white/18 dark:bg-slate-950/82 dark:text-white dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)] dark:hover:bg-slate-900"
|
||||
className="nodrag nopan nowheel h-9 min-w-0 flex-1 truncate rounded-full border border-slate-200 bg-white/95 px-3 text-left text-[12px] font-semibold text-slate-900 shadow-[0_10px_24px_rgba(15,23,42,0.14)] hover:bg-slate-50 dark:border-white/18 dark:bg-slate-950/82 dark:text-white dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)] dark:hover:bg-slate-900"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
onClick={() => setEditingLocalEditRegionId(region.id)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
setEditingLocalEditRegionId(region.id);
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
setEditingLocalEditRegionId(region.id);
|
||||
}}
|
||||
title={region.prompt}
|
||||
type="button"
|
||||
>
|
||||
@ -8266,9 +8501,26 @@ export function ImageNode({
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white/95 text-slate-500 shadow-[0_10px_24px_rgba(15,23,42,0.12)] hover:bg-red-500 hover:text-white dark:border-white/18 dark:bg-slate-950/82 dark:text-white/72 dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)]"
|
||||
className="nodrag nopan nowheel flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white/95 text-slate-500 shadow-[0_10px_24px_rgba(15,23,42,0.12)] hover:bg-red-500 hover:text-white dark:border-white/18 dark:bg-slate-950/82 dark:text-white/72 dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)]"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
onClick={() => deleteLocalEditRegion(region.id)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
deleteLocalEditRegion(region.id);
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
deleteLocalEditRegion(region.id);
|
||||
}}
|
||||
title="删除区域"
|
||||
type="button"
|
||||
>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user