fix: compact canvas image tools

This commit is contained in:
Your Name 2026-06-04 14:02:05 +08:00
parent 1a194b5527
commit f34344d53b
2 changed files with 185 additions and 259 deletions

View File

@ -1284,14 +1284,14 @@ export function AiWashWorkspace({
) : (
<div className="text-center">
<button
className="inline-flex h-16 items-center rounded-[22px] bg-[#111111] px-9 text-[18px] font-semibold text-white shadow-[0_18px_42px_rgba(0,0,0,0.14)] transition hover:-translate-y-0.5 hover:bg-black"
className="inline-flex h-12 items-center rounded-[18px] bg-[#111111] px-6 text-[15px] font-semibold text-white shadow-[0_12px_28px_rgba(0,0,0,0.12)] transition hover:-translate-y-0.5 hover:bg-black"
onClick={() => onRequestAddImage?.()}
type="button"
>
<ImageIcon className="mr-3 h-5 w-5" />
<ImageIcon className="mr-2.5 h-4 w-4" />
添加图片
</button>
<div className="mt-4 text-[14px] text-[#777777]">
<div className="mt-2.5 text-[13px] text-[#777777]">
选择本地图后即可开始 {config.modeLabel || "AI洗图"}
</div>
</div>

View File

@ -1201,6 +1201,14 @@ export function ImageNode({
imageNodeMoreEnabled &&
(hasHoverImageMorePrimaryActions || hasHoverImageMoreSecondaryActions);
const hasSelectedImageMorePrimaryActions =
imageBoxCutoutEnabled ||
imageLocalEditEnabled ||
imageRemoveWatermarkEnabled ||
imageResizeEnabled ||
imageMultiAngleEnabled ||
imageGenerate3dModelEnabled ||
imageGenerate3dWorldEnabled ||
imageLayerSplitEnabled ||
imageExtendEnabled ||
imageCropEnabled ||
imageAdjustEnabled ||
@ -1208,22 +1216,14 @@ export function ImageNode({
imagePromptRegenerateEnabled ||
imageNoteEnabled;
const hasSelectedImageMoreMenu =
imageNodeMoreEnabled &&
(hasSelectedImageMorePrimaryActions || imageDeleteEnabled);
hasSelectedImageMorePrimaryActions ||
(imageNodeMoreEnabled && imageDeleteEnabled);
const hasSelectedImagePrimaryToolbarActions =
imagePickObjectEnabled ||
imageEnhanceEnabled ||
imageRemoveBackgroundEnabled ||
imageBoxCutoutEnabled ||
imageLocalEditEnabled ||
imageRemoveWatermarkEnabled ||
imageResizeEnabled ||
imageTextEditEnabled ||
imageMultiAngleEnabled ||
imageGenerate3dModelEnabled ||
imageGenerate3dWorldEnabled ||
imageAiWashQuickEnabled ||
imageLayerSplitEnabled;
imageAiWashQuickEnabled;
const hasSelectedImageSecondaryToolbarActions =
hasSelectedImageMoreMenu || imageDownloadEnabled;
const contentScale = useMemo(
@ -4294,7 +4294,7 @@ export function ImageNode({
const rect = containerRef.current.getBoundingClientRect();
const toolbarWidth = Math.max(
280,
(toolbarSize.width || 980) * FLOATING_TOOLBAR_SCALE
(toolbarSize.width || 640) * FLOATING_TOOLBAR_SCALE
);
const toolbarHeight = Math.max(
42,
@ -4302,12 +4302,12 @@ export function ImageNode({
);
const titleSafeHeight = 34;
const gap = 12;
const { viewportPadding, maxTop, safeLeft, safeRight } =
const { viewportPadding, maxTop, safeBottom, safeLeft, safeRight } =
getFloatingViewportBounds(toolbarHeight);
let top = rect.top - toolbarHeight - titleSafeHeight - gap;
if (top < viewportPadding) {
top = rect.bottom + gap;
let top = rect.bottom + gap;
if (top + toolbarHeight > safeBottom) {
top = rect.top - toolbarHeight - titleSafeHeight - gap;
}
top = Math.min(Math.max(top, viewportPadding), maxTop);
@ -4332,36 +4332,31 @@ export function ImageNode({
return { top: 0, left: 0, maxWidth: 480 };
};
const getAiWashQuickPanelPosition = (
anchor: { top: number; left: number; maxWidth: number }
) => {
if (typeof window === "undefined") {
return { top: 0, left: 0, width: 360, maxHeight: 520 };
const getAiWashQuickPanelPosition = () => {
if (!containerRef.current || typeof window === "undefined") {
return { top: 0, left: 0, width: 286, maxHeight: 430 };
}
const panelWidth = 360;
const panelHeight = 540;
const gap = 10;
const toolbarHeight = Math.max(
42,
(toolbarSize.height || 60) * FLOATING_TOOLBAR_SCALE
);
const rect = containerRef.current.getBoundingClientRect();
const panelWidth = 286;
const panelHeight = 430;
const gap = 12;
const { viewportPadding, safeBottom, safeLeft, safeRight, maxTop } =
getFloatingViewportBounds(panelHeight);
const width = Math.min(panelWidth, Math.max(260, safeRight - safeLeft));
let top = anchor.top + toolbarHeight + gap;
let left = rect.right + gap;
if (left + width > safeRight) {
left = rect.left - width - gap;
}
left = Math.min(Math.max(left, safeLeft), Math.max(safeLeft, safeRight - width));
let top = rect.top;
if (top + panelHeight > safeBottom) {
top = anchor.top - panelHeight - gap;
top = rect.bottom - panelHeight;
}
top = Math.min(Math.max(top, viewportPadding), maxTop);
const preferredLeft = anchor.left;
const left = Math.min(
Math.max(preferredLeft, safeLeft),
Math.max(safeLeft, safeRight - width)
);
return {
top,
left,
@ -7457,7 +7452,7 @@ export function ImageNode({
const cropPanelPosition = getCropPanelPosition();
const imageAdjustPanelPosition = getImageAdjustPanelPosition();
const toolbarPosition = getToolbarPosition();
const aiWashQuickPanelPosition = getAiWashQuickPanelPosition(toolbarPosition);
const aiWashQuickPanelPosition = getAiWashQuickPanelPosition();
const boxCutoutToolbarPosition = getBoxCutoutToolbarPosition();
const flipRotatePanelPosition = getFlipRotatePanelPosition();
const anglePanelPosition = getAnglePanelPosition();
@ -8653,7 +8648,7 @@ export function ImageNode({
typeof document !== "undefined" &&
createPortal(
<div
className="fixed z-[10000] rounded-[28px] border border-slate-200 bg-white/96 p-3.5 text-slate-950 shadow-[0_20px_54px_rgba(15,23,42,0.16)] backdrop-blur-xl dark:border-white/12 dark:bg-slate-950/94 dark:text-white"
className="fixed z-[10000] rounded-[20px] border border-slate-200 bg-white/96 p-2.5 text-slate-950 shadow-[0_14px_34px_rgba(15,23,42,0.15)] backdrop-blur-xl dark:border-white/12 dark:bg-slate-950/94 dark:text-white"
data-canvas-no-drag="true"
onClick={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
@ -8667,93 +8662,80 @@ export function ImageNode({
scrollbarWidth: "thin",
}}
>
<div className="mb-3 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-2.5">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[16px] border border-slate-200 bg-white shadow-[0_8px_18px_rgba(15,23,42,0.06)] dark:border-white/12 dark:bg-white/8">
<div className="mb-2.5 flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[13px] border border-slate-200 bg-white shadow-[0_6px_14px_rgba(15,23,42,0.06)] dark:border-white/12 dark:bg-white/8">
<CanvasToolbarIcon
className="h-5 w-5"
className="h-4 w-4"
fallback={Sparkles}
iconUrl={aiWashQuickConfig.modeIconUrl}
/>
</span>
<div className="min-w-0">
<div className="truncate font-semibold text-[16px] tracking-[-0.03em]">
<div className="truncate font-semibold text-[14px] tracking-[-0.02em]">
{aiWashQuickModeLabel}
</div>
<div className="mt-0.5 text-[12px] text-slate-500 dark:text-white/52">
选中图片快捷洗图
</div>
</div>
</div>
<button
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition hover:border-slate-950 hover:text-slate-950 dark:border-white/12 dark:bg-white/8 dark:text-white/60 dark:hover:border-white dark:hover:text-white"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition hover:border-slate-950 hover:text-slate-950 dark:border-white/12 dark:bg-white/8 dark:text-white/60 dark:hover:border-white dark:hover:text-white"
onClick={() => setAiWashQuickPanelOpen(false)}
title="关闭"
type="button"
>
<X className="h-4 w-4" />
<X className="h-3.5 w-3.5" />
</button>
</div>
{aiWashConfigLoading ? (
<div className="mb-3 flex items-center gap-2 rounded-[18px] border border-slate-200 bg-slate-50 px-3 py-2 text-[13px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/58">
<Loader2 className="h-4 w-4 animate-spin" />
<div className="mb-2 flex items-center gap-2 rounded-[14px] border border-slate-200 bg-slate-50 px-2.5 py-2 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/58">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
正在加载 AI洗图 配置...
</div>
) : null}
<div className="space-y-3">
<div className="rounded-[22px] border border-slate-200 bg-slate-50/70 p-3 dark:border-white/10 dark:bg-white/[0.04]">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-[14px] font-semibold">
<SlidersHorizontal className="h-4 w-4" />
渲染目标
<div className="space-y-2">
<div className="rounded-[16px] border border-slate-200 bg-slate-50/70 p-2.5 dark:border-white/10 dark:bg-white/[0.04]">
<div className="mb-1.5 flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 text-[12px] font-semibold text-slate-600 dark:text-white/62">
<SlidersHorizontal className="h-3.5 w-3.5" />
目标
</div>
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[11px] text-slate-500 dark:border-white/10 dark:bg-white/8 dark:text-white/58">
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] text-slate-500 dark:border-white/10 dark:bg-white/8 dark:text-white/58">
{getAiWashStatusLabel(aiWashQuickStatus)}
</span>
</div>
<div className="space-y-2">
{aiWashQuickPresets.map((preset) => {
const isActive = selectedAiWashQuickPreset?.id === preset.id;
return (
<button
className={cn(
"w-full rounded-[16px] border px-3 py-2.5 text-left transition",
isActive
? "border-slate-950 bg-slate-950 text-white shadow-[0_10px_22px_rgba(15,23,42,0.16)] dark:border-white dark:bg-white dark:text-slate-950"
: "border-slate-200 bg-white text-slate-600 hover:border-slate-950 hover:text-slate-950 dark:border-white/10 dark:bg-white/7 dark:text-white/64 dark:hover:border-white dark:hover:text-white"
)}
key={preset.id}
onClick={() => {
setAiWashQuickPresetId(preset.id);
setAiWashQuickStatus("idle");
}}
type="button"
>
<div className="truncate font-semibold text-[13px]">
{preset.title}
</div>
{preset.description ? (
<div
className={cn(
"mt-1 line-clamp-2 text-[12px] leading-5",
isActive ? "text-white/72 dark:text-slate-700" : "text-slate-500 dark:text-white/46"
)}
>
{preset.description}
</div>
) : null}
</button>
);
})}
</div>
<Select
onValueChange={(value) => {
setAiWashQuickPresetId(value);
setAiWashQuickStatus("idle");
}}
value={selectedAiWashQuickPreset?.id || aiWashQuickPresetId}
>
<SelectTrigger className="h-9 rounded-[13px] border-slate-200 bg-white px-2.5 text-[13px] shadow-[0_1px_2px_rgba(15,23,42,0.04)] dark:border-white/10 dark:bg-white/8">
<SelectValue placeholder="选择渲染目标" />
</SelectTrigger>
<SelectContent className="border-slate-200 bg-white text-slate-950 dark:border-white/10 dark:bg-slate-950 dark:text-white">
{aiWashQuickPresets.map((preset) => (
<SelectItem key={preset.id} value={preset.id}>
{preset.title}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedAiWashQuickPreset?.description ? (
<div className="mt-2 line-clamp-2 text-[11px] leading-4 text-slate-500 dark:text-white/46">
{selectedAiWashQuickPreset.description}
</div>
) : null}
</div>
<div className="rounded-[22px] border border-slate-200 bg-white p-3 dark:border-white/10 dark:bg-white/[0.04]">
<div className="mb-2 text-[14px] font-semibold">参数设置</div>
<div className="rounded-[16px] border border-slate-200 bg-white p-2.5 dark:border-white/10 dark:bg-white/[0.04]">
<div className="mb-2 text-[12px] font-semibold text-slate-600 dark:text-white/62">
参数
</div>
{shouldShowAiWashQuickModelSelector ? (
<div className="mb-3 space-y-1.5">
<div className="mb-2.5 space-y-1.5">
<Label className="text-[12px] text-slate-500 dark:text-white/54">
生图模型
</Label>
@ -8761,7 +8743,7 @@ export function ImageNode({
onValueChange={setAiWashQuickModelId}
value={selectedAiWashQuickModel?.id || aiWashQuickModelId}
>
<SelectTrigger className="h-10 rounded-[15px] border-slate-200 bg-white text-[13px] shadow-[0_1px_2px_rgba(15,23,42,0.04)] dark:border-white/10 dark:bg-white/8">
<SelectTrigger className="h-9 rounded-[13px] border-slate-200 bg-white px-2.5 text-[13px] shadow-[0_1px_2px_rgba(15,23,42,0.04)] dark:border-white/10 dark:bg-white/8">
<SelectValue placeholder="选择生图模型" />
</SelectTrigger>
<SelectContent className="border-slate-200 bg-white text-slate-950 dark:border-white/10 dark:bg-slate-950 dark:text-white">
@ -8787,7 +8769,7 @@ export function ImageNode({
</Select>
<div className="text-[12px] leading-5 text-slate-500 dark:text-white/50">
{selectedAiWashQuickModel
? `当前模型:${selectedAiWashQuickModel.displayName} · ${
? `${selectedAiWashQuickModel.displayName} · ${
selectedAiWashQuickModel.unitCost > 0
? `${selectedAiWashQuickModel.unitCost} 积分/张`
: "未设置积分"
@ -8798,15 +8780,15 @@ export function ImageNode({
</div>
</div>
) : selectedAiWashQuickModel ? (
<div className="mb-3 rounded-[15px] border border-slate-200 bg-slate-50 px-3 py-2 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/52">
管理员已锁定模型:{selectedAiWashQuickModel.displayName}
<div className="mb-2.5 rounded-[13px] border border-slate-200 bg-slate-50 px-2.5 py-2 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/52">
{selectedAiWashQuickModel.displayName}
{selectedAiWashQuickModel.unitCost > 0
? ` · ${selectedAiWashQuickModel.unitCost} 积分/张`
: ""}
</div>
) : null}
<div className="space-y-2.5">
<div className="space-y-2">
{aiWashQuickControls.map((control) => {
const selectedIndex =
aiWashQuickLevels[control.id] ?? control.defaultLevel;
@ -8816,14 +8798,14 @@ export function ImageNode({
null;
return (
<div
className="rounded-[16px] border border-slate-200 bg-slate-50/70 px-3 py-2.5 dark:border-white/10 dark:bg-white/[0.04]"
className="rounded-[13px] border border-slate-200 bg-slate-50/70 px-2.5 py-2 dark:border-white/10 dark:bg-white/[0.04]"
key={control.id}
>
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-[13px] font-semibold">
<div className="mb-1.5 flex items-center justify-between gap-2">
<div className="text-[12px] font-semibold">
{control.label}
</div>
<span className="min-w-9 rounded-full border border-slate-200 bg-white px-2 py-0.5 text-center text-[11px] text-slate-600 dark:border-white/10 dark:bg-white/8 dark:text-white/64">
<span className="min-w-8 rounded-full border border-slate-200 bg-white px-1.5 py-0.5 text-center text-[10px] text-slate-600 dark:border-white/10 dark:bg-white/8 dark:text-white/64">
{activeLevel?.label || selectedIndex + 1}
</span>
</div>
@ -8839,8 +8821,8 @@ export function ImageNode({
}
rangeClassName="bg-slate-950 dark:bg-white"
step={1}
thumbClassName="h-3.5 w-3.5 border border-slate-300 bg-white shadow-[0_0_0_3px_rgba(15,23,42,0.06)]"
trackClassName="h-1.5 bg-slate-200 dark:bg-white/14"
thumbClassName="h-3 w-3 border border-slate-300 bg-white shadow-[0_0_0_3px_rgba(15,23,42,0.06)]"
trackClassName="h-1 bg-slate-200 dark:bg-white/14"
value={[selectedIndex]}
/>
</div>
@ -8849,14 +8831,8 @@ export function ImageNode({
</div>
</div>
{aiWashQuickPrompt ? (
<div className="max-h-20 overflow-hidden rounded-[18px] border border-slate-200 bg-slate-50 px-3 py-2 text-[12px] leading-5 text-slate-500 dark:border-white/10 dark:bg-white/[0.04] dark:text-white/48">
{aiWashQuickPrompt}
</div>
) : null}
<Button
className="h-11 w-full rounded-[18px] bg-slate-950 text-[14px] font-semibold text-white shadow-[0_12px_26px_rgba(15,23,42,0.16)] hover:bg-slate-900 hover:text-white disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none dark:bg-white dark:text-slate-950 dark:hover:bg-slate-100 dark:disabled:bg-white/12 dark:disabled:text-white/30"
className="h-9 w-full rounded-[14px] bg-slate-950 text-[13px] font-semibold text-white shadow-[0_10px_22px_rgba(15,23,42,0.14)] hover:bg-slate-900 hover:text-white disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none dark:bg-white dark:text-slate-950 dark:hover:bg-slate-100 dark:disabled:bg-white/12 dark:disabled:text-white/30"
disabled={
isSubmittingAiWashQuick ||
!selectedAiWashQuickPreset ||
@ -8866,13 +8842,13 @@ export function ImageNode({
type="button"
>
{isSubmittingAiWashQuick ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
) : (
<Sparkles className="mr-2 h-4 w-4" />
<Sparkles className="mr-2 h-3.5 w-3.5" />
)}
{isSubmittingAiWashQuick
? "正在 AI洗图"
: `开始 ${aiWashQuickModeLabel}${
? "生成中"
: `${aiWashQuickModeLabel}${
selectedAiWashQuickModel?.unitCost
? ` · ${selectedAiWashQuickModel.unitCost} 积分`
: ""
@ -8898,7 +8874,7 @@ export function ImageNode({
createPortal(
<TooltipProvider delayDuration={200}>
<div
className="fixed z-[9999] flex items-center rounded-[24px] border border-slate-200 bg-white px-2 py-2 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-slate-700 dark:bg-slate-900 dark:text-gray-100"
className="fixed z-[9999] flex items-center gap-0.5 rounded-[24px] border border-slate-200 bg-white px-2 py-1.5 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-slate-700 dark:bg-slate-900 dark:text-gray-100"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
@ -8913,7 +8889,7 @@ export function ImageNode({
180,
Math.floor(toolbarPosition.maxWidth / FLOATING_TOOLBAR_SCALE)
)}px`,
overflowX: "auto",
overflowX: "visible",
scrollbarWidth: "none",
}}
>
@ -8939,7 +8915,9 @@ export function ImageNode({
</Button>
) : null}
{imageAddToChatEnabled && hasSelectedImagePrimaryToolbarActions ? (
{imageAddToChatEnabled &&
(hasSelectedImagePrimaryToolbarActions ||
hasSelectedImageSecondaryToolbarActions) ? (
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
) : null}
@ -8994,40 +8972,6 @@ export function ImageNode({
</Button>
) : null}
{imageBoxCutoutEnabled ? (
<Button
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("box-cutout")}
title={imageBoxCutoutItem.label}
variant="ghost"
>
<CanvasToolbarIcon
className="mr-1.5 h-4 w-4"
fallback={Scan}
item={imageBoxCutoutItem}
/>
{imageBoxCutoutItem.label}
</Button>
) : null}
{imageLocalEditEnabled ? (
<Button
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing || isSubmittingLocalEdit}
onClick={() => handleAction("local-edit")}
title={imageLocalEditItem.label}
variant="ghost"
>
<CanvasToolbarIcon
className="mr-1.5 h-4 w-4"
fallback={Wand2}
item={imageLocalEditItem}
/>
{imageLocalEditItem.label}
</Button>
) : null}
{imageAiWashQuickEnabled ? (
<Button
className={cn(
@ -9055,40 +8999,6 @@ export function ImageNode({
</Button>
) : null}
{imageRemoveWatermarkEnabled ? (
<Button
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("watermark")}
title={imageRemoveWatermarkItem.label}
variant="ghost"
>
<CanvasToolbarIcon
className="mr-1.5 h-4 w-4"
fallback={Eraser}
item={imageRemoveWatermarkItem}
/>
{imageRemoveWatermarkItem.label}
</Button>
) : null}
{imageResizeEnabled ? (
<Button
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("resize")}
title={imageResizeItem.label}
variant="ghost"
>
<CanvasToolbarIcon
className="mr-1.5 h-4 w-4"
fallback={Maximize2}
item={imageResizeItem}
/>
{imageResizeItem.label}
</Button>
) : null}
{imageTextEditEnabled ? (
<Button
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
@ -9106,74 +9016,6 @@ export function ImageNode({
</Button>
) : null}
{imageMultiAngleEnabled ? (
<Button
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("angle")}
title={imageMultiAngleItem.label}
variant="ghost"
>
<CanvasToolbarIcon
className="mr-1.5 h-4 w-4"
fallback={Box}
item={imageMultiAngleItem}
/>
{imageMultiAngleItem.label}
</Button>
) : null}
{imageGenerate3dModelEnabled ? (
<Button
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("generate-3d-model")}
title={imageGenerate3dModelItem.label}
variant="ghost"
>
<CanvasToolbarIcon
className="mr-1.5 h-4 w-4"
fallback={Box}
item={imageGenerate3dModelItem}
/>
{imageGenerate3dModelItem.label}
</Button>
) : null}
{imageGenerate3dWorldEnabled ? (
<Button
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("generate-3d-world")}
title={imageGenerate3dWorldItem.label}
variant="ghost"
>
<CanvasToolbarIcon
className="mr-1.5 h-4 w-4"
fallback={Globe2}
item={imageGenerate3dWorldItem}
/>
{imageGenerate3dWorldItem.label}
</Button>
) : null}
{imageLayerSplitEnabled ? (
<Button
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
disabled={isProcessing}
onClick={() => handleAction("layer-split")}
title={imageLayerSplitItem.label}
variant="ghost"
>
<CanvasToolbarIcon
className="mr-1.5 h-4 w-4"
fallback={Layers}
item={imageLayerSplitItem}
/>
{imageLayerSplitItem.label}
</Button>
) : null}
{hasSelectedImagePrimaryToolbarActions &&
hasSelectedImageSecondaryToolbarActions ? (
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
@ -9201,6 +9043,90 @@ export function ImageNode({
className="bg-white opacity-100 backdrop-blur-none dark:bg-slate-900 dark:text-slate-100"
side="bottom"
>
{imageBoxCutoutEnabled ? (
<DropdownMenuItem onClick={() => handleAction("box-cutout")}>
<CanvasToolbarIcon
className="mr-2 h-4 w-4"
fallback={Scan}
item={imageBoxCutoutItem}
/>
{imageBoxCutoutItem.label}
</DropdownMenuItem>
) : null}
{imageLocalEditEnabled ? (
<DropdownMenuItem onClick={() => handleAction("local-edit")}>
<CanvasToolbarIcon
className="mr-2 h-4 w-4"
fallback={Wand2}
item={imageLocalEditItem}
/>
{imageLocalEditItem.label}
</DropdownMenuItem>
) : null}
{imageRemoveWatermarkEnabled ? (
<DropdownMenuItem onClick={() => handleAction("watermark")}>
<CanvasToolbarIcon
className="mr-2 h-4 w-4"
fallback={Eraser}
item={imageRemoveWatermarkItem}
/>
{imageRemoveWatermarkItem.label}
</DropdownMenuItem>
) : null}
{imageResizeEnabled ? (
<DropdownMenuItem onClick={() => handleAction("resize")}>
<CanvasToolbarIcon
className="mr-2 h-4 w-4"
fallback={Maximize2}
item={imageResizeItem}
/>
{imageResizeItem.label}
</DropdownMenuItem>
) : null}
{imageMultiAngleEnabled ? (
<DropdownMenuItem onClick={() => handleAction("angle")}>
<CanvasToolbarIcon
className="mr-2 h-4 w-4"
fallback={Box}
item={imageMultiAngleItem}
/>
{imageMultiAngleItem.label}
</DropdownMenuItem>
) : null}
{imageGenerate3dModelEnabled ? (
<DropdownMenuItem
onClick={() => handleAction("generate-3d-model")}
>
<CanvasToolbarIcon
className="mr-2 h-4 w-4"
fallback={Box}
item={imageGenerate3dModelItem}
/>
{imageGenerate3dModelItem.label}
</DropdownMenuItem>
) : null}
{imageGenerate3dWorldEnabled ? (
<DropdownMenuItem
onClick={() => handleAction("generate-3d-world")}
>
<CanvasToolbarIcon
className="mr-2 h-4 w-4"
fallback={Globe2}
item={imageGenerate3dWorldItem}
/>
{imageGenerate3dWorldItem.label}
</DropdownMenuItem>
) : null}
{imageLayerSplitEnabled ? (
<DropdownMenuItem onClick={() => handleAction("layer-split")}>
<CanvasToolbarIcon
className="mr-2 h-4 w-4"
fallback={Layers}
item={imageLayerSplitItem}
/>
{imageLayerSplitItem.label}
</DropdownMenuItem>
) : null}
{imageExtendEnabled ? (
<DropdownMenuItem onClick={() => handleAction("extend")}>
<CanvasToolbarIcon