fix: anchor local edit prompts to image node
This commit is contained in:
parent
a93daefe54
commit
d6ab02eb45
@ -1869,7 +1869,6 @@ export function ImageNode({
|
||||
const [boxCutoutDragEnd, setBoxCutoutDragEnd] =
|
||||
useState<BoxCutoutPoint | null>(null);
|
||||
const [localEditMode, setLocalEditMode] = useState(false);
|
||||
const [localEditViewportRevision, setLocalEditViewportRevision] = useState(0);
|
||||
const [localEditRegions, setLocalEditRegions] = useState<LocalEditRegion[]>(
|
||||
() => getLocalEditRegionsFromMetadata(data.metadata)
|
||||
);
|
||||
@ -2048,7 +2047,6 @@ export function ImageNode({
|
||||
>({});
|
||||
const localEditPromptDraftRefs = useRef<Record<string, string>>({});
|
||||
const localEditRegionsRef = useRef<LocalEditRegion[]>(localEditRegions);
|
||||
const localEditViewportFrameRef = useRef<number | null>(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({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative h-full w-full overflow-hidden rounded-none border border-[#dbe2ee] bg-white shadow-[0_0_0_1px_rgba(219,226,238,0.34),0_16px_34px_rgba(22,24,29,0.08)] transition-all dark:border-zinc-800 dark:bg-zinc-950",
|
||||
"relative h-full w-full rounded-none border border-[#dbe2ee] bg-white shadow-[0_0_0_1px_rgba(219,226,238,0.34),0_16px_34px_rgba(22,24,29,0.08)] transition-all dark:border-zinc-800 dark:bg-zinc-950",
|
||||
localEditMode ? "overflow-visible" : "overflow-hidden",
|
||||
isSelected
|
||||
? "border-[#5b76fe] ring-2 ring-[#5b76fe]/14 shadow-[0_0_0_1px_rgba(91,118,254,0.14),0_20px_38px_rgba(91,118,254,0.12)]"
|
||||
: "hover:border-[#cfd9ea]"
|
||||
@ -7992,6 +7943,275 @@ export function ImageNode({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{localEditPromptInlineOverlays.map(({ region, isEditing, style }) => (
|
||||
<div
|
||||
className="nodrag nopan nowheel pointer-events-auto absolute z-[170] flex flex-col items-stretch gap-1.5"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
key={`local-edit-prompt-${region.id}`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
style={style}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isEditing ? (
|
||||
<input
|
||||
autoFocus={editingLocalEditRegionId === region.id}
|
||||
className="nodrag nopan nowheel h-9 min-w-0 flex-1 rounded-full border border-slate-300 bg-white/95 px-3 text-[12px] font-medium text-slate-950 caret-slate-950 shadow-[0_10px_24px_rgba(15,23,42,0.14)] outline-none ring-2 ring-transparent transition placeholder:text-slate-400 focus:border-slate-950 focus:ring-slate-950/15 dark:border-white/20 dark:bg-slate-950/88 dark:text-white dark:caret-white dark:placeholder:text-white/42 dark:focus:border-white/40 dark:focus:ring-white/18"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
defaultValue={
|
||||
localEditPromptDraftRefs.current[region.id] ??
|
||||
region.prompt
|
||||
}
|
||||
onBlur={(event) => {
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
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={(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"
|
||||
>
|
||||
{region.index}. {region.prompt}
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
className="hidden"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
id={`local-edit-reference-upload-${region.id}`}
|
||||
multiple
|
||||
onChange={(event) => {
|
||||
void handleLocalEditReferenceUpload(
|
||||
region.id,
|
||||
event.currentTarget.files
|
||||
);
|
||||
event.currentTarget.value = "";
|
||||
}}
|
||||
type="file"
|
||||
/>
|
||||
<button
|
||||
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-600 shadow-[0_10px_24px_rgba(15,23,42,0.12)] hover:bg-slate-950 hover:text-white disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/18 dark:bg-slate-950/82 dark:text-white/72 dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)] dark:hover:bg-white dark:hover:text-slate-950"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
disabled={
|
||||
uploadingLocalEditReferenceRegionId === region.id ||
|
||||
(region.referenceImages?.length || 0) >=
|
||||
LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION
|
||||
}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
document
|
||||
.getElementById(
|
||||
`local-edit-reference-upload-${region.id}`
|
||||
)
|
||||
?.click();
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
title="添加区域参考图"
|
||||
type="button"
|
||||
>
|
||||
{uploadingLocalEditReferenceRegionId === region.id ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<ImagePlus className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
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={(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"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{region.referenceImages?.length ? (
|
||||
<div className="flex max-w-full items-center gap-1 overflow-hidden rounded-2xl border border-slate-200 bg-white/92 px-1.5 py-1 shadow-[0_8px_20px_rgba(15,23,42,0.1)] dark:border-white/12 dark:bg-slate-950/80">
|
||||
<span className="shrink-0 px-1 text-[10px] font-semibold text-slate-500 dark:text-white/52">
|
||||
参考
|
||||
</span>
|
||||
{region.referenceImages.map((reference) => (
|
||||
<div
|
||||
className="group relative h-8 w-8 shrink-0 overflow-hidden rounded-lg border border-slate-200 bg-slate-100 dark:border-white/12 dark:bg-slate-800"
|
||||
key={reference.id}
|
||||
title={reference.name || "区域参考图"}
|
||||
>
|
||||
<img
|
||||
alt={reference.name || "区域参考图"}
|
||||
className="h-full w-full object-cover"
|
||||
draggable={false}
|
||||
src={reference.url}
|
||||
/>
|
||||
<button
|
||||
className="absolute inset-0 hidden items-center justify-center bg-black/52 text-white group-hover:flex"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
removeLocalEditReferenceImage(
|
||||
region.id,
|
||||
reference.id
|
||||
);
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
removeLocalEditReferenceImage(
|
||||
region.id,
|
||||
reference.id
|
||||
);
|
||||
}}
|
||||
title="移除参考图"
|
||||
type="button"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{localEditDraftRectStyle ? (
|
||||
<div
|
||||
className="pointer-events-none absolute z-[162] rounded-[10px] border-[3px] border-emerald-400 bg-emerald-300/10 shadow-[0_0_0_1px_rgba(255,255,255,0.9),0_0_22px_rgba(16,185,129,0.35)]"
|
||||
@ -8552,270 +8772,6 @@ export function ImageNode({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 工具栏使用 Portal 渲染到 body,避免被裁剪 */}
|
||||
{localEditMode &&
|
||||
typeof document !== "undefined" &&
|
||||
localEditPromptPortalOverlays.map(({ region, isEditing, style }) =>
|
||||
createPortal(
|
||||
<div
|
||||
className="nodrag nopan nowheel fixed z-[10000] flex flex-col items-stretch gap-1.5"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
key={region.id}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
style={style}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{isEditing ? (
|
||||
<input
|
||||
autoFocus={editingLocalEditRegionId === region.id}
|
||||
className="nodrag nopan nowheel h-9 min-w-0 flex-1 rounded-full border border-slate-300 bg-white/95 px-3 text-[12px] font-medium text-slate-950 caret-slate-950 shadow-[0_10px_24px_rgba(15,23,42,0.14)] outline-none ring-2 ring-transparent transition placeholder:text-slate-400 focus:border-slate-950 focus:ring-slate-950/15 dark:border-white/20 dark:bg-slate-950/88 dark:text-white dark:caret-white dark:placeholder:text-white/42 dark:focus:border-white/40 dark:focus:ring-white/18"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
defaultValue={
|
||||
localEditPromptDraftRefs.current[region.id] ?? region.prompt
|
||||
}
|
||||
onBlur={(event) => {
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
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={(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"
|
||||
>
|
||||
{region.index}. {region.prompt}
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
className="hidden"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
id={`local-edit-reference-upload-${region.id}`}
|
||||
multiple
|
||||
onChange={(event) => {
|
||||
void handleLocalEditReferenceUpload(
|
||||
region.id,
|
||||
event.currentTarget.files
|
||||
);
|
||||
event.currentTarget.value = "";
|
||||
}}
|
||||
type="file"
|
||||
/>
|
||||
<button
|
||||
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-600 shadow-[0_10px_24px_rgba(15,23,42,0.12)] hover:bg-slate-950 hover:text-white disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/18 dark:bg-slate-950/82 dark:text-white/72 dark:shadow-[0_10px_24px_rgba(0,0,0,0.24)] dark:hover:bg-white dark:hover:text-slate-950"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
document
|
||||
.getElementById(`local-edit-reference-upload-${region.id}`)
|
||||
?.click();
|
||||
}}
|
||||
disabled={
|
||||
uploadingLocalEditReferenceRegionId === region.id ||
|
||||
(region.referenceImages?.length || 0) >=
|
||||
LOCAL_EDIT_MAX_REFERENCE_IMAGES_PER_REGION
|
||||
}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
title="添加区域参考图"
|
||||
type="button"
|
||||
>
|
||||
{uploadingLocalEditReferenceRegionId === region.id ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<ImagePlus className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
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={(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"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{region.referenceImages?.length ? (
|
||||
<div className="flex max-w-full items-center gap-1 overflow-hidden rounded-2xl border border-slate-200 bg-white/92 px-1.5 py-1 shadow-[0_8px_20px_rgba(15,23,42,0.1)] dark:border-white/12 dark:bg-slate-950/80">
|
||||
<span className="shrink-0 px-1 text-[10px] font-semibold text-slate-500 dark:text-white/52">
|
||||
参考
|
||||
</span>
|
||||
{region.referenceImages.map((reference) => (
|
||||
<div
|
||||
className="group relative h-8 w-8 shrink-0 overflow-hidden rounded-lg border border-slate-200 bg-slate-100 dark:border-white/12 dark:bg-slate-800"
|
||||
key={reference.id}
|
||||
title={reference.name || "区域参考图"}
|
||||
>
|
||||
<img
|
||||
alt={reference.name || "区域参考图"}
|
||||
className="h-full w-full object-cover"
|
||||
draggable={false}
|
||||
src={reference.url}
|
||||
/>
|
||||
<button
|
||||
className="absolute inset-0 hidden items-center justify-center bg-black/52 text-white group-hover:flex"
|
||||
data-canvas-no-drag="true"
|
||||
data-local-edit-control="true"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
removeLocalEditReferenceImage(region.id, reference.id);
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation?.();
|
||||
event.stopPropagation();
|
||||
removeLocalEditReferenceImage(region.id, reference.id);
|
||||
}}
|
||||
title="移除参考图"
|
||||
type="button"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
)}
|
||||
|
||||
{localEditMode &&
|
||||
!isMultiSelected &&
|
||||
data.src &&
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user