fix: enable local edit region controls
This commit is contained in:
parent
f87669c15a
commit
06153cc179
@ -227,6 +227,14 @@ type CropInteractionState = {
|
|||||||
startRect: NormalizedCropRect;
|
startRect: NormalizedCropRect;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type LocalEditRegionInteractionState = {
|
||||||
|
handle: CropInteractionHandle;
|
||||||
|
pointerId: number;
|
||||||
|
regionId: string;
|
||||||
|
startPoint: BoxCutoutPoint;
|
||||||
|
startRect: NormalizedCropRect;
|
||||||
|
};
|
||||||
|
|
||||||
type AnglePreviewDragState = {
|
type AnglePreviewDragState = {
|
||||||
pointerId: number;
|
pointerId: number;
|
||||||
startX: number;
|
startX: number;
|
||||||
@ -1825,6 +1833,8 @@ export function ImageNode({
|
|||||||
useState<BoxCutoutPoint | null>(null);
|
useState<BoxCutoutPoint | null>(null);
|
||||||
const [localEditDragEnd, setLocalEditDragEnd] =
|
const [localEditDragEnd, setLocalEditDragEnd] =
|
||||||
useState<BoxCutoutPoint | null>(null);
|
useState<BoxCutoutPoint | null>(null);
|
||||||
|
const [localEditRegionInteraction, setLocalEditRegionInteraction] =
|
||||||
|
useState<LocalEditRegionInteractionState | null>(null);
|
||||||
const [editingLocalEditRegionId, setEditingLocalEditRegionId] =
|
const [editingLocalEditRegionId, setEditingLocalEditRegionId] =
|
||||||
useState<string | null>(null);
|
useState<string | null>(null);
|
||||||
const [isSubmittingLocalEdit, setIsSubmittingLocalEdit] = useState(false);
|
const [isSubmittingLocalEdit, setIsSubmittingLocalEdit] = useState(false);
|
||||||
@ -1988,6 +1998,7 @@ export function ImageNode({
|
|||||||
Record<string, HTMLInputElement | null>
|
Record<string, HTMLInputElement | null>
|
||||||
>({});
|
>({});
|
||||||
const localEditPromptDraftRefs = useRef<Record<string, string>>({});
|
const localEditPromptDraftRefs = useRef<Record<string, string>>({});
|
||||||
|
const localEditRegionsRef = useRef<LocalEditRegion[]>(localEditRegions);
|
||||||
|
|
||||||
const updateNode = useCanvasStore((state) => state.updateNode);
|
const updateNode = useCanvasStore((state) => state.updateNode);
|
||||||
const deleteNode = useCanvasStore((state) => state.deleteNode);
|
const deleteNode = useCanvasStore((state) => state.deleteNode);
|
||||||
@ -2454,6 +2465,7 @@ export function ImageNode({
|
|||||||
setIsLocalEditDrawing(false);
|
setIsLocalEditDrawing(false);
|
||||||
setLocalEditDragStart(null);
|
setLocalEditDragStart(null);
|
||||||
setLocalEditDragEnd(null);
|
setLocalEditDragEnd(null);
|
||||||
|
setLocalEditRegionInteraction(null);
|
||||||
setEditingLocalEditRegionId(null);
|
setEditingLocalEditRegionId(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -2473,11 +2485,16 @@ export function ImageNode({
|
|||||||
focusLocalEditPromptInput(editingLocalEditRegionId);
|
focusLocalEditPromptInput(editingLocalEditRegionId);
|
||||||
}, [editingLocalEditRegionId, focusLocalEditPromptInput, localEditMode]);
|
}, [editingLocalEditRegionId, focusLocalEditPromptInput, localEditMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localEditRegionsRef.current = localEditRegions;
|
||||||
|
}, [localEditRegions]);
|
||||||
|
|
||||||
const commitLocalEditRegions = useCallback(
|
const commitLocalEditRegions = useCallback(
|
||||||
(regions: LocalEditRegion[]) => {
|
(regions: LocalEditRegion[]) => {
|
||||||
const nextRegions = reindexLocalEditRegions(
|
const nextRegions = reindexLocalEditRegions(
|
||||||
regions.slice(0, LOCAL_EDIT_MAX_REGIONS)
|
regions.slice(0, LOCAL_EDIT_MAX_REGIONS)
|
||||||
);
|
);
|
||||||
|
localEditRegionsRef.current = nextRegions;
|
||||||
setLocalEditRegions(nextRegions);
|
setLocalEditRegions(nextRegions);
|
||||||
localEditPromptDraftRefs.current = Object.fromEntries(
|
localEditPromptDraftRefs.current = Object.fromEntries(
|
||||||
nextRegions.map((region) => [region.id, region.prompt])
|
nextRegions.map((region) => [region.id, region.prompt])
|
||||||
@ -3432,6 +3449,150 @@ export function ImageNode({
|
|||||||
[localEditMode]
|
[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(
|
const persistLocalEditRegionPrompt = useCallback(
|
||||||
(regionId: string, prompt: string) => {
|
(regionId: string, prompt: string) => {
|
||||||
localEditPromptDraftRefs.current[regionId] = prompt;
|
localEditPromptDraftRefs.current[regionId] = prompt;
|
||||||
@ -3448,13 +3609,13 @@ export function ImageNode({
|
|||||||
(regionId: string) => {
|
(regionId: string) => {
|
||||||
delete localEditPromptDraftRefs.current[regionId];
|
delete localEditPromptDraftRefs.current[regionId];
|
||||||
commitLocalEditRegions(
|
commitLocalEditRegions(
|
||||||
localEditRegions.filter((region) => region.id !== regionId)
|
localEditRegionsRef.current.filter((region) => region.id !== regionId)
|
||||||
);
|
);
|
||||||
if (editingLocalEditRegionId === regionId) {
|
if (editingLocalEditRegionId === regionId) {
|
||||||
setEditingLocalEditRegionId(null);
|
setEditingLocalEditRegionId(null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[commitLocalEditRegions, editingLocalEditRegionId, localEditRegions]
|
[commitLocalEditRegions, editingLocalEditRegionId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearLocalEditRegions = useCallback(() => {
|
const clearLocalEditRegions = useCallback(() => {
|
||||||
@ -7563,8 +7724,31 @@ export function ImageNode({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
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={{
|
style={{
|
||||||
left,
|
left,
|
||||||
top,
|
top,
|
||||||
@ -7579,6 +7763,40 @@ export function ImageNode({
|
|||||||
>
|
>
|
||||||
{region.index}
|
{region.index}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -8148,7 +8366,7 @@ export function ImageNode({
|
|||||||
localEditPromptPortalOverlays.map(({ region, isEditing, style }) =>
|
localEditPromptPortalOverlays.map(({ region, isEditing, style }) =>
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<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-canvas-no-drag="true"
|
||||||
data-local-edit-control="true"
|
data-local-edit-control="true"
|
||||||
key={region.id}
|
key={region.id}
|
||||||
@ -8256,9 +8474,26 @@ export function ImageNode({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<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"
|
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}
|
title={region.prompt}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -8266,9 +8501,26 @@ export function ImageNode({
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<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"
|
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="删除区域"
|
title="删除区域"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user