Files
Genarrative/src/components/image-editor/useImageCanvasGenerationSurface.tsx
T
k88936 61fe4c48af
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 16s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 16s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
图片画布接入 3D 资源:新增 model3d 类别、3D 预览与 3D 生成入口
- 后端资源投影按 asset_kind 分叉,新增 project_editor_client_media_src 与 EDITOR_MODEL3D_ASSET_KIND:model3d 只投影预览图,模型本体只留 objectKey,预览缺失才回落历史口径
- tripo3d 存储新增按字节魔数识别模型 content_type 与扩展名映射,不采信 provider 声明与下载响应头
- editor_project_storage 把 model3d 加入 EDITOR_CANVAS_ASSET_KINDS 白名单
- 前端 CanvasAssetKind 与 image-canvas-core 类型新增 model3d:画布卡片渲染预览图或 3D 占位,类型角标新增「3D模型」,覆盖资格只在它与空类别之间成立
- 新增 model3d-preview 子目录(Model3dViewerModal / Model3dViewerModel),Tailwind 内联、不新增 CSS,失败原因分超限 / 格式不可识别 / 无 WebGL2 三类展示
- 选中工具条新增只读 preview-model3d 能力位,仅 objectKey 为空时置灰;打开预览同时选中图层并暂停画布快捷键与舞台交互
- 查看器包源对象 format 改为可选,新增「声明 Content-Type → 字节魔数 → 地址扩展名」三级格式判定并导出支持格式清单
- 导出工作流新增 3d_models/ 目录,模型下载按真实响应头 MIME 定扩展名、对象键扩展名兜底
- 新增 3D 生成入口:底部工具栏浮动子选项与文生 / 图生 3D 面板,落 projectResource + canvasCompletion,复用占位框与任务侧栏
- 3D 定价只读实时定价查询,段缺失即入口不可提交;幂等键由前端铸造,用户重试必须换键
- 新增 ADR 0003 / 0004 与实施计划、里程碑文档;更新 Tripo 技术方案、后端架构、CONTEXT 术语、decision-log 与 pitfalls
- .gitignore 忽略仓库根目录手工下载的 3D 样例模型
2026-09-22 10:10:00 +08:00

698 lines
25 KiB
TypeScript

import { X } from 'lucide-react';
import {
type CSSProperties,
type Dispatch,
type MutableRefObject,
type ReactNode,
type RefObject,
type SetStateAction,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import type {
EditorAssetSnapshot,
EditorProjectSnapshot,
} from '../../services/image-editor/editorProjectClient';
import {
PlatformFloatingMenu,
PlatformFloatingMenuItem,
} from '../common/PlatformFloatingMenu';
import { PlatformRuntimeStatusToast } from '../common/PlatformRuntimeStatusToast';
import { ImageCanvasEditorPortal } from './ImageCanvasEditorPortal';
import type {
CanvasGenerationDialogState,
CanvasHistoryAction,
CanvasLayer,
CanvasTool,
CanvasViewport,
EditorAsset,
GenerateDialogState,
ImageContextMenuState,
PublicationMaterialsWorkflowId,
SidebarPanel,
UploadTarget,
} from './ImageCanvasEditorTypes';
import { ImageCanvasGenerationComposerView } from './ImageCanvasGenerationComposerView';
import { replaceProjectAssetPickerReferences } from './ImageCanvasGenerationDialogModel';
import {
resolveCharacterAnimationPanelStyle,
resolveCropExpandPanelStyle,
resolveGenerationAnchor,
resolveGenerationComposerStyle,
resolveIconComposerStyle,
resolveQuickEditPanelStyle,
} from './ImageCanvasOverlayModel';
import { ImageCanvasProjectAssetPickerDialog } from './ImageCanvasProjectAssetPickerDialog';
import { PUBLICATION_MATERIALS_WORKFLOWS } from './ImageCanvasPublicationMaterialsModel';
import {
isProjectAssetPickerReference,
projectAssetPickerReferences,
} from './projectAssetReferencePickerModel';
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
type CanvasGenerationDialogUpdater = (
dialog: CanvasGenerationDialogState,
) => CanvasGenerationDialogState | null;
type ToolbarOptionTool = Extract<
CanvasTool,
'music' | 'spec' | 'publication' | 'model3d'
>;
type ImageCanvasGenerationSurfaceOptions = {
layers: CanvasLayer[];
canvasSize: { width: number; height: number };
viewport: CanvasViewport;
setViewport: Dispatch<SetStateAction<CanvasViewport>>;
canvasViewportRef: RefObject<HTMLDivElement | null>;
setLayers: Dispatch<SetStateAction<CanvasLayer[]>>;
layerCounterRef: MutableRefObject<number>;
specToolWrapRef: RefObject<HTMLSpanElement | null>;
musicToolWrapRef: RefObject<HTMLSpanElement | null>;
publicationToolWrapRef: RefObject<HTMLSpanElement | null>;
model3dToolWrapRef: RefObject<HTMLSpanElement | null>;
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
generationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
publicationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
hasPendingImageReferenceUploads?: boolean;
generateDialog: GenerateDialogState | null;
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
activeCanvasGenerationDialog: CanvasGenerationDialogState | null;
canvasGenerationDialogs: CanvasGenerationDialogState[];
openCanvasGenerationDialog: (
dialog: Omit<CanvasGenerationDialogState, 'id'>,
) => string;
activateCanvasGenerationDialog: (
targetDialog: CanvasGenerationDialogState,
) => void;
updateCanvasGenerationDialogById: (
dialogId: string,
updater: CanvasGenerationDialogUpdater,
) => void;
hasCanvasGenerationDialogById: (dialogId: string) => boolean;
getCanvasGenerationDialogById: (
dialogId: string,
) => CanvasGenerationDialogState | undefined;
archiveActiveCanvasGenerationDialog: () => void;
removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void;
getGeneratingDialogPlaceholder: (
dialog: GenerateDialogState,
) => GenerateDialogState['placeholder'];
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
selectSingleLayer: (layerId: string | null) => void;
fitLayers: (
targetLayers?: CanvasLayer[],
options?: { captureHistory?: boolean },
) => void;
captureCanvasHistory: (action: CanvasHistoryAction) => void;
setActiveTool: Dispatch<SetStateAction<CanvasTool>>;
setActiveSidebarPanel: Dispatch<SetStateAction<SidebarPanel | null>>;
setMetadataLayer: Dispatch<SetStateAction<CanvasLayer | null>>;
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
requestUpload: (target: UploadTarget) => void;
/** 项目已登记素材,用于参考图弹窗;为空时参考图来源菜单不出现该入口。 */
projectAssets?: readonly EditorAsset[];
persistGeneratedAsset?: (layer: CanvasLayer) => void;
persistUpdatedLayerResource?: (layer: CanvasLayer) => void;
projectId?: string | null;
currentUserId?: string | null;
assetFolderId?: string | null;
upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void;
applyProjectSnapshot?: (
project: EditorProjectSnapshot,
action?: CanvasHistoryAction,
) => void;
applyProjectSnapshotWithoutHistory?: (
project: EditorProjectSnapshot,
) => boolean | void;
flushProjectPersistence?: (options?: {
preferLatestGenerationDialogs?: boolean;
}) => Promise<void>;
refreshAssetLibrary?: () => Promise<unknown> | void;
onWalletBalanceMayHaveChanged?: () => void;
};
function renderEditorPortal(node: ReactNode) {
return <ImageCanvasEditorPortal>{node}</ImageCanvasEditorPortal>;
}
function buildToolbarPortalMenuStyle(
anchor: HTMLElement | null,
): CSSProperties {
const rect = anchor?.getBoundingClientRect();
if (!rect) {
return {
position: 'fixed',
left: 0,
top: 0,
right: 'auto',
bottom: 'auto',
zIndex: 70,
};
}
return {
position: 'fixed',
left: Math.round(rect.left),
top: Math.round(rect.top),
right: 'auto',
bottom: 'auto',
zIndex: 70,
transform: 'translateY(calc(-100% - 0.45rem))',
};
}
const TOOLBAR_OPTION_CLOSE_DELAY_MS = 160;
export function useImageCanvasGenerationSurface({
layers,
canvasSize,
viewport,
setViewport,
setLayers,
layerCounterRef,
specToolWrapRef,
musicToolWrapRef,
publicationToolWrapRef,
model3dToolWrapRef,
characterSpecButtonRef,
characterReferenceButtonRef,
iconSpecButtonRef,
generationReferenceButtonRef,
publicationReferenceButtonRef,
hasPendingImageReferenceUploads = false,
generateDialog,
setGenerateDialog,
activeCanvasGenerationDialog,
canvasGenerationDialogs,
openCanvasGenerationDialog,
activateCanvasGenerationDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
getCanvasGenerationDialogById,
archiveActiveCanvasGenerationDialog,
removeCanvasGenerationDialogsByLayerId,
getGeneratingDialogPlaceholder,
appendCanvasLayersWithResources,
selectSingleLayer,
fitLayers,
captureCanvasHistory,
setActiveTool,
setActiveSidebarPanel,
setMetadataLayer,
setImageContextMenu,
requestUpload,
projectAssets = [],
persistGeneratedAsset,
persistUpdatedLayerResource,
projectId,
currentUserId,
assetFolderId,
upsertGeneratedAsset,
applyProjectSnapshot,
applyProjectSnapshotWithoutHistory,
flushProjectPersistence,
refreshAssetLibrary,
onWalletBalanceMayHaveChanged,
}: ImageCanvasGenerationSurfaceOptions) {
const toolbarOptionCloseTimerRef = useRef<ReturnType<
typeof setTimeout
> | null>(null);
const generationWorkflow = useImageCanvasGenerationWorkflow({
layers,
canvasSize,
viewport,
setViewport,
setLayers,
canvasGenerationDialogs,
layerCounterRef,
generateDialog,
setGenerateDialog,
hasPendingImageReferenceUploads,
openCanvasGenerationDialog,
activateCanvasGenerationDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
getCanvasGenerationDialogById,
archiveActiveCanvasGenerationDialog,
removeCanvasGenerationDialogsByLayerId,
getGeneratingDialogPlaceholder,
appendCanvasLayersWithResources,
selectSingleLayer,
fitLayers,
captureCanvasHistory,
setActiveTool,
setActiveSidebarPanel,
setMetadataLayer,
setImageContextMenu,
persistGeneratedAsset,
persistUpdatedLayerResource,
projectId,
currentUserId,
assetFolderId,
upsertGeneratedAsset,
applyProjectSnapshot,
applyProjectSnapshotWithoutHistory,
flushProjectPersistence,
refreshAssetLibrary,
onWalletBalanceMayHaveChanged,
});
const activeGenerationLayer = activeCanvasGenerationDialog?.generatedLayerId
? (layers.find(
(layer) => layer.id === activeCanvasGenerationDialog.generatedLayerId,
) ?? null)
: null;
const generationAnchor = resolveGenerationAnchor({
dialog: activeCanvasGenerationDialog,
generatedLayer: activeGenerationLayer,
});
const generationComposerStyle = resolveGenerationComposerStyle({
dialog: activeCanvasGenerationDialog,
anchor: generationAnchor,
viewport,
});
const iconComposerStyle = resolveIconComposerStyle({
dialog: activeCanvasGenerationDialog,
composerStyle: generationComposerStyle,
});
const quickEditPanelStyle = resolveQuickEditPanelStyle({
panel: generationWorkflow.quickEditPanel,
sourceLayer: generationWorkflow.quickEditSourceLayer,
viewport,
canvasSize,
});
const cropExpandPanelStyle = resolveCropExpandPanelStyle({
panel: generationWorkflow.cropExpandPanel,
sourceLayer: generationWorkflow.cropExpandSourceLayer,
viewport,
canvasSize,
});
const characterAnimationPanelStyle =
activeCanvasGenerationDialog?.mode === 'character-animation'
? generationComposerStyle
: resolveCharacterAnimationPanelStyle({
panel: generationWorkflow.characterAnimationPanel,
sourceLayer: generationWorkflow.characterAnimationSourceLayer,
viewport,
canvasSize,
});
const clearToolbarOptionCloseTimer = useCallback(() => {
if (!toolbarOptionCloseTimerRef.current) {
return;
}
clearTimeout(toolbarOptionCloseTimerRef.current);
toolbarOptionCloseTimerRef.current = null;
}, []);
useEffect(
() => () => {
clearToolbarOptionCloseTimer();
},
[clearToolbarOptionCloseTimer],
);
const openToolbarOptionMenu = useCallback(
(tool: ToolbarOptionTool) => {
clearToolbarOptionCloseTimer();
generationWorkflow.setIsMusicMenuOpen(tool === 'music');
generationWorkflow.setIsSpecMenuOpen(tool === 'spec');
generationWorkflow.setIsPublicationMenuOpen(tool === 'publication');
generationWorkflow.setIsModel3dMenuOpen(tool === 'model3d');
},
[clearToolbarOptionCloseTimer, generationWorkflow],
);
const closeToolbarOptionMenu = useCallback(
(tool: ToolbarOptionTool) => {
clearToolbarOptionCloseTimer();
toolbarOptionCloseTimerRef.current = setTimeout(() => {
if (tool === 'music') {
generationWorkflow.setIsMusicMenuOpen(false);
return;
}
if (tool === 'publication') {
generationWorkflow.setIsPublicationMenuOpen(false);
return;
}
if (tool === 'model3d') {
generationWorkflow.setIsModel3dMenuOpen(false);
return;
}
generationWorkflow.setIsSpecMenuOpen(false);
}, TOOLBAR_OPTION_CLOSE_DELAY_MS);
},
[clearToolbarOptionCloseTimer, generationWorkflow],
);
const switchGenerationTool = useCallback(
(tool: CanvasTool) => {
if (tool === 'generate') {
generationWorkflow.openGenerateDialog();
return true;
}
if (tool === 'scene') {
generationWorkflow.openSceneGenerationDialog();
return true;
}
if (tool === 'video') {
generationWorkflow.openVideoGenerationDialog();
return true;
}
if (tool === 'music') {
openToolbarOptionMenu('music');
setActiveTool('music');
return true;
}
if (tool === 'spec') {
openToolbarOptionMenu('spec');
setActiveTool('spec');
return true;
}
if (tool === 'publication') {
openToolbarOptionMenu('publication');
setActiveTool('publication');
return true;
}
if (tool === 'model3d') {
openToolbarOptionMenu('model3d');
setActiveTool('model3d');
return true;
}
if (tool === 'character') {
generationWorkflow.openCharacterGenerationDialog();
return true;
}
if (tool === 'icon') {
generationWorkflow.openIconGenerationDialog();
return true;
}
if (tool === 'ui-design') {
generationWorkflow.openUiDesignGenerationDialog();
return true;
}
return false;
},
[generationWorkflow, openToolbarOptionMenu, setActiveTool],
);
/**
* 参考图弹窗的开关与初始选择。选择列表来自「项目素材」这一组引用,
* 与画布点选 / 上传来的参考图互不覆盖。
*/
const [projectAssetPickerOpen, setProjectAssetPickerOpen] = useState(false);
const projectAssetPickerSelectedIds = (
generateDialog?.generationReferences ?? []
)
.filter((reference) => isProjectAssetPickerReference(reference))
.flatMap((reference) => {
const assetId = reference.sourceAssetId?.trim();
return assetId ? [assetId] : [];
});
const applyProjectAssetPickerSelection = useCallback(
(assetIds: string[]) => {
const references = projectAssetPickerReferences(projectAssets, assetIds);
const nonImageCount = references.filter(
(reference) => (reference.mediaType ?? 'image') !== 'image',
).length;
if (nonImageCount > 0) {
generationWorkflow.showGenerationWarning('参考图只支持图片素材');
}
setGenerateDialog((currentDialog) =>
replaceProjectAssetPickerReferences(currentDialog, references),
);
setProjectAssetPickerOpen(false);
},
[generationWorkflow, projectAssets, setGenerateDialog],
);
const generationComposerNode = (
<>
{generationWorkflow.generationWarning ? (
<PlatformRuntimeStatusToast
key={generationWorkflow.generationWarningVersion}
tone="warning"
surface="solid"
size="sm"
className="image-canvas-editor__reference-pick-toast"
role="alert"
>
<span>{generationWorkflow.generationWarning}</span>
<button
type="button"
className="image-canvas-editor__reference-pick-toast-close"
aria-label="关闭提示"
onClick={generationWorkflow.clearGenerationWarning}
>
<X aria-hidden="true" size={14} strokeWidth={2.4} />
</button>
</PlatformRuntimeStatusToast>
) : null}
<ImageCanvasGenerationComposerView
specToolWrapRef={specToolWrapRef}
publicationReferenceButtonRef={publicationReferenceButtonRef}
characterSpecButtonRef={characterSpecButtonRef}
characterReferenceButtonRef={characterReferenceButtonRef}
generationReferenceButtonRef={generationReferenceButtonRef}
iconSpecButtonRef={iconSpecButtonRef}
isPublicationReferenceMenuOpen={
generationWorkflow.isPublicationReferenceMenuOpen
}
isSpecMenuOpen={generationWorkflow.isSpecMenuOpen}
isGenerationReferenceMenuOpen={
generationWorkflow.isGenerationReferenceMenuOpen
}
isCharacterSpecMenuOpen={generationWorkflow.isCharacterSpecMenuOpen}
isCharacterReferenceMenuOpen={
generationWorkflow.isCharacterReferenceMenuOpen
}
isIconSpecMenuOpen={generationWorkflow.isIconSpecMenuOpen}
isUiDesignSpecMenuOpen={generationWorkflow.isUiDesignSpecMenuOpen}
isPickingPublicationReferenceFromCanvas={
generationWorkflow.isPickingPublicationReferenceFromCanvas
}
isPickingGenerationReferenceFromCanvas={
generationWorkflow.isPickingGenerationReferenceFromCanvas
}
isPickingQuickEditReferenceFromCanvas={
generationWorkflow.isPickingQuickEditReferenceFromCanvas
}
isPickingCharacterSpecFromCanvas={
generationWorkflow.isPickingCharacterSpecFromCanvas
}
isPickingCharacterReferenceFromCanvas={
generationWorkflow.isPickingCharacterReferenceFromCanvas
}
isPickingIconSpecFromCanvas={
generationWorkflow.isPickingIconSpecFromCanvas
}
isPickingUiDesignSpecFromCanvas={
generationWorkflow.isPickingUiDesignSpecFromCanvas
}
generateDialog={generateDialog}
backgroundMusicPromptAssist={
generationWorkflow.backgroundMusicPromptAssist
}
soundEffectPromptAssist={generationWorkflow.soundEffectPromptAssist}
updateCanvasGenerationDialogById={updateCanvasGenerationDialogById}
hasPendingImageReferenceUploads={hasPendingImageReferenceUploads}
generationComposerStyle={generationComposerStyle}
iconComposerStyle={iconComposerStyle}
quickEditPanel={generationWorkflow.quickEditPanel}
quickEditSourceLayer={generationWorkflow.quickEditSourceLayer}
quickEditPanelStyle={quickEditPanelStyle}
cropExpandPanel={generationWorkflow.cropExpandPanel}
cropExpandSourceLayer={generationWorkflow.cropExpandSourceLayer}
cropExpandPanelStyle={cropExpandPanelStyle}
characterAnimationPanel={generationWorkflow.characterAnimationPanel}
characterAnimationSourceLayer={
generationWorkflow.characterAnimationSourceLayer
}
characterAnimationPanelStyle={characterAnimationPanelStyle}
characterAnimationPrice={generationWorkflow.characterAnimationPrice}
setGenerateDialog={setGenerateDialog}
setQuickEditPanel={generationWorkflow.setQuickEditPanel}
setCropExpandPanel={generationWorkflow.setCropExpandPanel}
setCharacterAnimationPanel={
generationWorkflow.setCharacterAnimationPanel
}
setIsCharacterSpecMenuOpen={
generationWorkflow.setIsCharacterSpecMenuOpen
}
setIsCharacterReferenceMenuOpen={
generationWorkflow.setIsCharacterReferenceMenuOpen
}
setIsGenerationReferenceMenuOpen={
generationWorkflow.setIsGenerationReferenceMenuOpen
}
setIsPublicationReferenceMenuOpen={
generationWorkflow.setIsPublicationReferenceMenuOpen
}
setIsIconSpecMenuOpen={generationWorkflow.setIsIconSpecMenuOpen}
setIsUiDesignSpecMenuOpen={generationWorkflow.setIsUiDesignSpecMenuOpen}
setIsPickingGenerationReferenceFromCanvas={
generationWorkflow.setIsPickingGenerationReferenceFromCanvas
}
setIsPickingQuickEditReferenceFromCanvas={
generationWorkflow.setIsPickingQuickEditReferenceFromCanvas
}
setIsPickingPublicationReferenceFromCanvas={
generationWorkflow.setIsPickingPublicationReferenceFromCanvas
}
setIsPickingCharacterSpecFromCanvas={
generationWorkflow.setIsPickingCharacterSpecFromCanvas
}
setIsPickingCharacterReferenceFromCanvas={
generationWorkflow.setIsPickingCharacterReferenceFromCanvas
}
setIsPickingIconSpecFromCanvas={
generationWorkflow.setIsPickingIconSpecFromCanvas
}
setIsPickingUiDesignSpecFromCanvas={
generationWorkflow.setIsPickingUiDesignSpecFromCanvas
}
onOpenSpecDialog={generationWorkflow.openSpecDialog}
onRequestUpload={requestUpload}
onRequestProjectAssetPicker={
projectAssets.length > 0 &&
(generateDialog?.mode === 'generate' ||
generateDialog?.mode === 'scene' ||
generateDialog?.mode === 'quick-edit')
? () => {
generationWorkflow.setIsGenerationReferenceMenuOpen(false);
generationWorkflow.setIsPickingGenerationReferenceFromCanvas(
false,
);
setProjectAssetPickerOpen(true);
}
: undefined
}
onSubmitImageGeneration={(dialog) =>
void generationWorkflow.submitImageGeneration(dialog)
}
onSubmitIconSpritesheetGeneration={(dialog) =>
void generationWorkflow.submitIconSpritesheetGeneration(dialog)
}
onSubmitQuickEdit={() => void generationWorkflow.submitQuickEdit()}
onSubmitCropExpand={() => void generationWorkflow.submitCropExpand()}
onSubmitCharacterAnimation={() =>
void generationWorkflow.submitCharacterAnimation()
}
onRetryPerfectPixelOperation={(dialogId) =>
void generationWorkflow.retryPerfectPixelOperation(dialogId)
}
onUpdateSpecFormValue={generationWorkflow.updateSpecFormValue}
onUpdateIconDescriptionText={
generationWorkflow.updateIconDescriptionsText
}
onUpdateCharacterAnimationDuration={
generationWorkflow.updateCharacterAnimationDuration
}
onRememberImageModel={generationWorkflow.rememberImageModel}
onSpecMenuPointerEnter={() => openToolbarOptionMenu('spec')}
onSpecMenuPointerLeave={() => closeToolbarOptionMenu('spec')}
/>
{generationWorkflow.isPublicationMenuOpen
? renderEditorPortal(
<PlatformFloatingMenu
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu image-canvas-editor__publication-menu"
label="宣发素材类型"
placement="top-start"
style={buildToolbarPortalMenuStyle(
publicationToolWrapRef.current,
)}
onPointerEnter={() => openToolbarOptionMenu('publication')}
onPointerLeave={() => closeToolbarOptionMenu('publication')}
onFocus={() => openToolbarOptionMenu('publication')}
onBlur={() => closeToolbarOptionMenu('publication')}
>
{PUBLICATION_MATERIALS_WORKFLOWS.map((workflow) => (
<PlatformFloatingMenuItem
key={workflow.id}
onClick={() =>
generationWorkflow.openPublicationGenerationDialog(
workflow.id as PublicationMaterialsWorkflowId,
)
}
>
{workflow.label}
</PlatformFloatingMenuItem>
))}
</PlatformFloatingMenu>,
)
: null}
{generationWorkflow.isMusicMenuOpen
? renderEditorPortal(
<PlatformFloatingMenu
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu image-canvas-editor__music-menu"
label="生成音乐类型"
placement="top-start"
style={buildToolbarPortalMenuStyle(musicToolWrapRef.current)}
onPointerEnter={() => openToolbarOptionMenu('music')}
onPointerLeave={() => closeToolbarOptionMenu('music')}
onFocus={() => openToolbarOptionMenu('music')}
onBlur={() => closeToolbarOptionMenu('music')}
>
<PlatformFloatingMenuItem
onClick={generationWorkflow.openSoundEffectGenerationDialog}
>
生成游戏音效
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
onClick={generationWorkflow.openBackgroundMusicGenerationDialog}
>
生成游戏背景音乐
</PlatformFloatingMenuItem>
</PlatformFloatingMenu>,
)
: null}
{generationWorkflow.isModel3dMenuOpen
? renderEditorPortal(
<PlatformFloatingMenu
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu image-canvas-editor__model3d-menu"
label="3D 模型生成方式"
placement="top-start"
style={buildToolbarPortalMenuStyle(model3dToolWrapRef.current)}
onPointerEnter={() => openToolbarOptionMenu('model3d')}
onPointerLeave={() => closeToolbarOptionMenu('model3d')}
onFocus={() => openToolbarOptionMenu('model3d')}
onBlur={() => closeToolbarOptionMenu('model3d')}
>
<PlatformFloatingMenuItem
onClick={generationWorkflow.openModel3dTextToModelDialog}
>
文生 3D 模型
</PlatformFloatingMenuItem>
<PlatformFloatingMenuItem
onClick={generationWorkflow.openModel3dImageToModelDialog}
>
图生 3D 模型
</PlatformFloatingMenuItem>
</PlatformFloatingMenu>,
)
: null}
<ImageCanvasProjectAssetPickerDialog
open={projectAssetPickerOpen}
assets={projectAssets}
selectedAssetIds={projectAssetPickerSelectedIds}
onCancel={() => setProjectAssetPickerOpen(false)}
onConfirm={applyProjectAssetPickerSelection}
/>
</>
);
return {
...generationWorkflow,
generationComposerStyle,
generationComposerNode,
openToolbarOptionMenu,
closeToolbarOptionMenu,
switchGenerationTool,
};
}