Files
Genarrative/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.tsx
T
suzmii bd36587e0a 资源画布交互 1:1 复刻美术画布:点卡选中并浮出工具条,移除资源详情面板与全屏编辑路由
新增 features/resource-canvas:资源卡到美术画布 CanvasLayer 的适配、工具条动作可用性判定、C5 当前版本绑定判定、快速编辑草稿工厂与 AGC 宿主 chrome 样式

资源卡点击改为选中(Shift/Ctrl/Meta 多选),选中后在卡片旁复用 ImageCanvasSelectedLayerToolbarView 浮出工具条

快速编辑复用 ImageCanvasQuickEditPanelView 浮层面板,先调 normalize_local_project_raster_resource 正规化任务产物,再走 derive_local_project_resource(editKind=image-reference) 产出新素材,失败重试复用同一 operationId/idempotencyKey

删除 resourceEditorRoute 全链路(ResourceEditSurface 路由、openResourceEditor、handleResourceCardOpenEditor、cancelResourceEditor、submitResourceEdit)与资源详情面板(focusedResource 状态/引用/浮层 JSX/CSS 断言)

分类与标签入口搬到选中工具条;UI 资源保留「UI 编辑器」入口并从工具条打开

移除 apps/ai-game-creator-shell/scripts/check-config.mjs 中 normalize_local_project_raster_resource 的过渡 allowlist 条目

为共享工具条组件新增 opt-in 的 supportedActions/extraActions(默认 null/undefined,网页端行为不变)

AGC vite-env.d.ts 补 wx/ReactNativeWebView/WeixinJSBridge 跨端全局声明;hostBridge fail 回调补显式 unknown 类型

测试:资源卡查询 helper 由资源详情按钮改为选中按钮,清理详情面板断言,画布空白左键改为框选后平移用例改用中键
2026-09-10 15:19:17 +08:00

330 lines
11 KiB
TypeScript

import { CanvasChromeButton } from '@genarrative/image-canvas-react';
import {
Crop,
Download,
Grid2X2,
ImageOff,
Loader2,
PersonStanding,
Scissors,
Sparkles,
WandSparkles,
} from 'lucide-react';
import type { CSSProperties, ReactNode } from 'react';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
import type { CanvasLayer } from './ImageCanvasEditorTypes';
import {
canOpenRedrawPanel,
isQuickEditSupportedLayer,
} from './ImageCanvasGenerationModel';
/** 选中工具条上的动作标识,供宿主声明自己实际可用的动作集合。 */
export type ImageCanvasSelectedToolbarAction =
| 'quick-edit'
| 'redraw'
| 'crop-expand'
| 'remove-background'
| 'perfect-pixel'
| 'split-icon-spritesheet'
| 'extract-ui-design'
| 'character-animation'
| 'download';
type ImageCanvasSelectedLayerToolbarViewProps = {
/**
* 宿主显式声明的可用动作集合。
*
* 传 `null`(默认)时完全沿用美术画布内置判定;传入集合时以集合为准,
* 让没有对应后端链路的画布宿主不必把「点了没反应」的按钮渲染出来。
*/
supportedActions?: ReadonlySet<ImageCanvasSelectedToolbarAction> | null;
/** 宿主在工具条上追加的动作节点,渲染在下载按钮之前。 */
extraActions?: ReactNode;
selectedLayer: CanvasLayer | null;
selectedToolbarStyle: CSSProperties | null;
onOpenQuickEditPanel: (layer: CanvasLayer) => void;
onOpenRedrawPanel: (layer: CanvasLayer) => void;
onOpenCropExpandPanel: (layer: CanvasLayer) => void;
onRemoveBackground: (layer: CanvasLayer) => void;
onPerfectPixel: (layer: CanvasLayer) => void;
isPerfectPixelProcessing: boolean;
isPerfectPixelPendingConfirmation: boolean;
isSplittingIconSpritesheet: boolean;
isPersistingAssetKind: boolean;
onSplitIconSpritesheet: (layer: CanvasLayer) => void;
onExtractUiDesignAssets: (layer: CanvasLayer) => void;
onOpenCharacterAnimationPanel: (layer: CanvasLayer) => void;
onDownloadLayer: (layer: CanvasLayer) => void;
};
export function ImageCanvasSelectedLayerToolbarView({
supportedActions = null,
extraActions,
selectedLayer,
selectedToolbarStyle,
onOpenQuickEditPanel,
onOpenRedrawPanel,
onOpenCropExpandPanel,
onRemoveBackground,
onPerfectPixel,
isPerfectPixelProcessing,
isPerfectPixelPendingConfirmation,
isSplittingIconSpritesheet,
isPersistingAssetKind,
onSplitIconSpritesheet,
onExtractUiDesignAssets,
onOpenCharacterAnimationPanel,
onDownloadLayer,
}: ImageCanvasSelectedLayerToolbarViewProps) {
if (!selectedLayer || !selectedToolbarStyle) {
return null;
}
const isActionSupported = (
action: ImageCanvasSelectedToolbarAction,
builtInAvailability: boolean,
) => (supportedActions ? supportedActions.has(action) : builtInAvailability);
const canRedraw = isActionSupported(
'redraw',
canOpenRedrawPanel(selectedLayer),
);
const extraActionDivider = extraActions ? (
<span
aria-hidden="true"
className="image-canvas-editor__floating-toolbar-divider"
/>
) : null;
if (selectedLayer.mediaType === 'audio') {
return (
<div
className="image-canvas-editor__floating-toolbar"
style={selectedToolbarStyle}
role="toolbar"
aria-label="素材工具栏"
onPointerDown={(event) => event.stopPropagation()}
>
{canRedraw ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="改造"
title="改造"
icon={<WandSparkles className="h-4 w-4" />}
onClick={() => onOpenRedrawPanel(selectedLayer)}
>
<span>改造</span>
</CanvasChromeButton>
) : null}
{extraActionDivider}
{extraActions}
{isActionSupported('download', true) ? (
<EditorIconButton
label="下载按钮"
title="下载按钮"
icon={Download}
onClick={() => onDownloadLayer(selectedLayer)}
/>
) : null}
</div>
);
}
const canRasterEdit =
selectedLayer.mediaType !== 'video' &&
selectedLayer.mediaType !== 'image-sequence' &&
selectedLayer.assetKind !== 'character-animation';
const showQuickEdit = isActionSupported(
'quick-edit',
isQuickEditSupportedLayer(selectedLayer),
);
const showCharacterAnimation = isActionSupported(
'character-animation',
selectedLayer.assetKind === 'character',
);
return (
<div
className="image-canvas-editor__floating-toolbar"
style={selectedToolbarStyle}
role="toolbar"
aria-label="图片工具栏"
onPointerDown={(event) => event.stopPropagation()}
>
{showQuickEdit ? (
<>
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="快速编辑"
title="快速编辑"
icon={<Sparkles className="h-4 w-4" />}
onClick={() => onOpenQuickEditPanel(selectedLayer)}
>
<span>快速编辑</span>
</CanvasChromeButton>
<span
aria-hidden="true"
className="image-canvas-editor__floating-toolbar-divider"
/>
</>
) : null}
{canRasterEdit && isActionSupported('crop-expand', true) ? (
<EditorIconButton
label="裁扩按钮"
title="裁扩按钮"
icon={Crop}
onClick={() => onOpenCropExpandPanel(selectedLayer)}
/>
) : null}
{canRasterEdit && isActionSupported('remove-background', true) ? (
<EditorIconButton
label="去除背景按钮"
title="去除背景按钮"
icon={ImageOff}
onClick={() => onRemoveBackground(selectedLayer)}
/>
) : null}
{canRasterEdit && isActionSupported('perfect-pixel', true) ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
// 中文注释:保存态名称不能直接用「素材类型保存中」——icon-spritesheet 图层上
// 拆分图集按钮同时显示该文案,两个控件会撞同一个无障碍名称。
label={
isPersistingAssetKind
? '完美像素等待素材类型保存'
: isPerfectPixelProcessing
? '完美像素处理中'
: isPerfectPixelPendingConfirmation
? '完美像素结果待确认'
: '完美像素'
}
title={
isPersistingAssetKind
? '完美像素等待素材类型保存'
: isPerfectPixelProcessing
? '完美像素处理中'
: isPerfectPixelPendingConfirmation
? '结果待确认,双击原占位继续核对或重试'
: '自动识别并规整像素网格'
}
icon={
isPersistingAssetKind || isPerfectPixelProcessing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Grid2X2 className="h-4 w-4" />
)
}
// 中文注释:素材类型保存在途时必须一并禁用。请求同时带 assetKind 和
// sourceResourceId,本地类型已改但资源尚未落库时两者不一致,后端
// resolve_editor_pixel_art_snap_asset_kind 会直接 400,只留下失败占位。
// 与相邻的拆分图集按钮保持同一套门禁。
disabled={
isPersistingAssetKind ||
isPerfectPixelProcessing ||
isPerfectPixelPendingConfirmation
}
aria-busy={isPersistingAssetKind || isPerfectPixelProcessing}
onClick={() => onPerfectPixel(selectedLayer)}
>
<span>
{isPersistingAssetKind
? '保存中'
: isPerfectPixelProcessing
? '处理中'
: isPerfectPixelPendingConfirmation
? '待确认'
: '完美像素'}
</span>
</CanvasChromeButton>
) : null}
{selectedLayer.assetKind === 'icon-spritesheet' &&
isActionSupported('split-icon-spritesheet', true) ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label={
isPersistingAssetKind
? '素材类型保存中'
: isSplittingIconSpritesheet
? '拆图中'
: '拆分图集'
}
title={
isPersistingAssetKind
? '素材类型保存中'
: isSplittingIconSpritesheet
? '拆图中'
: '拆分图集'
}
icon={
isPersistingAssetKind || isSplittingIconSpritesheet ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Scissors className="h-4 w-4" />
)
}
disabled={isPersistingAssetKind || isSplittingIconSpritesheet}
aria-busy={isPersistingAssetKind || isSplittingIconSpritesheet}
onClick={() => onSplitIconSpritesheet(selectedLayer)}
>
<span>
{isPersistingAssetKind
? '保存中'
: isSplittingIconSpritesheet
? '拆图中'
: '拆分图集'}
</span>
</CanvasChromeButton>
) : null}
{selectedLayer.assetKind === 'ui-design' &&
isActionSupported('extract-ui-design', true) ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="提取素材"
title="提取素材"
icon={<Scissors className="h-4 w-4" />}
onClick={() => onExtractUiDesignAssets(selectedLayer)}
>
<span>提取素材</span>
</CanvasChromeButton>
) : null}
{showCharacterAnimation ? (
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="生成动画"
title="生成动画"
icon={<PersonStanding className="h-4 w-4" />}
onClick={() => onOpenCharacterAnimationPanel(selectedLayer)}
>
<span>生成动画</span>
</CanvasChromeButton>
) : null}
{canRedraw ? (
<>
<span
aria-hidden="true"
className="image-canvas-editor__floating-toolbar-divider"
/>
<CanvasChromeButton
className="image-canvas-editor__floating-toolbar-text-button"
label="改造"
title="改造"
icon={<WandSparkles className="h-4 w-4" />}
onClick={() => onOpenRedrawPanel(selectedLayer)}
>
<span>改造</span>
</CanvasChromeButton>
</>
) : null}
{extraActionDivider}
{extraActions}
{isActionSupported('download', true) ? (
<EditorIconButton
label="下载按钮"
title="下载按钮"
icon={Download}
onClick={() => onDownloadLayer(selectedLayer)}
/>
) : null}
</div>
);
}