3D 前端提交跟随其它画布生成工具同时落素材库
- buildModel3dSubmissionPlan 接收当前素材夹与素材名,与图片/图标/音效等工具一样同时发 projectId + assetFolderId + assetLabel(素材名缺省回落本次结果标题),没有素材夹时只发 projectId。
- useModel3dGenerationTask 新增 assetFolderId 入参,画布生成链路把当前素材夹透传下来;结果仍走既有画布路径读回,刷新素材库沿用 applyGeneratedProjectSnapshot 的既有行为,不新增 3D 专用刷新。
- 素材库行加 model3d 分支:缩略图按预览图(asset.src)渲染,objectKey(模型本体 .glb)不参与图片签名,与画布 3D 卡片的 objectKey={null} 口径一致,避免素材行去加载模型文件。
- 用例补齐:请求体同时带两个落点、无素材夹时不带 assetFolderId/assetLabel、素材夹只 trim 且素材名回落标题,以及素材行 3D 预览不签模型对象键 / 普通图片仍按 objectKey 换签名地址。
- 验证:npx vitest run src/components/image-editor/model3d-generation src/components/image-editor/ImageCanvasAssetRowView.test.tsx(67 passed)、npm run typecheck、check:encoding / git diff --check 通过。
This commit is contained in:
@@ -153,6 +153,36 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
expect(onDownloadAsset).toHaveBeenCalledWith(asset);
|
||||
});
|
||||
|
||||
it('3D 素材用预览图渲染,不拿模型对象去换签名地址', () => {
|
||||
const asset = createAsset({
|
||||
label: '木椅',
|
||||
assetKind: 'model3d',
|
||||
mediaType: 'image',
|
||||
src: '/genarrative-assets/editor/model3d/task-1/preview.webp',
|
||||
objectKey: 'genarrative-assets/editor/model3d/task-1/model.glb',
|
||||
});
|
||||
renderAssetRow({ asset });
|
||||
|
||||
const preview = screen.getByAltText('素材:木椅');
|
||||
expect(preview.getAttribute('src')).toBe(
|
||||
'/genarrative-assets/editor/model3d/task-1/preview.webp',
|
||||
);
|
||||
// objectKey 是模型本体,带上它会让缩略图去加载 .glb。
|
||||
expect(preview.getAttribute('data-object-key')).toBeNull();
|
||||
});
|
||||
|
||||
it('普通图片素材仍按 objectKey 换签名地址', () => {
|
||||
const asset = createAsset({
|
||||
mediaType: 'image',
|
||||
objectKey: 'genarrative-assets/editor/images/account-a.webp',
|
||||
});
|
||||
renderAssetRow({ asset });
|
||||
|
||||
expect(
|
||||
screen.getByAltText('素材:账号素材A').getAttribute('data-object-key'),
|
||||
).toBe('genarrative-assets/editor/images/account-a.webp');
|
||||
});
|
||||
|
||||
it('closes the right click menu when row actions run on the same asset', () => {
|
||||
const asset = createAsset({
|
||||
sourceResourceId: 'resource-asset-1',
|
||||
|
||||
@@ -347,7 +347,13 @@ export function ImageCanvasAssetRowView({
|
||||
? AUDIO_ASSET_COVER_SRC
|
||||
: (asset.thumbnailSrc ?? '')
|
||||
}
|
||||
objectKey={asset.mediaType === 'image' ? asset.objectKey : undefined}
|
||||
// 3D 素材的 objectKey 是模型本体(.glb),它的展示图是预览图(asset.src):
|
||||
// 沿用画布 3D 卡片的同一条口径,这里不能拿模型对象去换签名地址,否则缩略图会去加载 .glb。
|
||||
objectKey={
|
||||
asset.mediaType === 'image' && asset.assetKind !== 'model3d'
|
||||
? asset.objectKey
|
||||
: undefined
|
||||
}
|
||||
previewNode={videoPreview}
|
||||
titleNode={
|
||||
isUploadingAsset || isFailedUpload ? (
|
||||
|
||||
@@ -114,11 +114,13 @@ describe('resolveModel3dImageSource', () => {
|
||||
});
|
||||
|
||||
describe('buildModel3dSubmissionPlan', () => {
|
||||
it('文生 3D 的请求体全量给出价格相关参数与落点', () => {
|
||||
it('文生 3D 的请求体全量给出价格相关参数与落点,并同时落素材库', () => {
|
||||
const result = buildModel3dSubmissionPlan({
|
||||
dialog: createDialog('model3d-text-to-model', { prompt: ' 一把木椅 ' }),
|
||||
projectId: 'project-1',
|
||||
canvasCompletion: CANVAS_COMPLETION,
|
||||
assetFolderId: 'project',
|
||||
assetLabel: ' 一把木椅 ',
|
||||
pricing: PRICING,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
@@ -139,10 +141,57 @@ describe('buildModel3dSubmissionPlan', () => {
|
||||
generateParts: false,
|
||||
},
|
||||
projectId: 'project-1',
|
||||
// 与其它画布生成工具同形:项目资源与素材库两个落点一起发,结果两边都落。
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '一把木椅',
|
||||
canvasCompletion: CANVAS_COMPLETION,
|
||||
});
|
||||
});
|
||||
|
||||
it('没有素材夹时只给 projectId:素材库落点与素材名都不进请求体', () => {
|
||||
const result = buildModel3dSubmissionPlan({
|
||||
dialog: createDialog('model3d-text-to-model', { prompt: '木椅' }),
|
||||
projectId: 'project-1',
|
||||
assetFolderId: ' ',
|
||||
pricing: PRICING,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.plan.body).toEqual({
|
||||
generation: {
|
||||
model: 'v3.1-20260211',
|
||||
prompt: '木椅',
|
||||
texture: true,
|
||||
textureQuality: 'standard',
|
||||
pbr: false,
|
||||
geometryQuality: 'standard',
|
||||
quad: false,
|
||||
smartLowPoly: false,
|
||||
generateParts: false,
|
||||
},
|
||||
projectId: 'project-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('素材夹落点只 trim,素材名缺省回落本次结果标题', () => {
|
||||
const result = buildModel3dSubmissionPlan({
|
||||
dialog: createDialog('model3d-text-to-model', { prompt: '木椅' }),
|
||||
projectId: 'project-1',
|
||||
assetFolderId: ' 个人素材夹 ',
|
||||
pricing: PRICING,
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.plan.body).toMatchObject({
|
||||
assetFolderId: '个人素材夹',
|
||||
assetLabel: '木椅',
|
||||
});
|
||||
});
|
||||
|
||||
it('图生 3D 带 source、不带提示词;纯几何档位不带 textureQuality', () => {
|
||||
const result = buildModel3dSubmissionPlan({
|
||||
dialog: createDialog('model3d-image-to-model', {
|
||||
|
||||
@@ -83,11 +83,20 @@ export function buildModel3dSubmissionPlan({
|
||||
dialog,
|
||||
projectId,
|
||||
canvasCompletion,
|
||||
assetFolderId,
|
||||
assetLabel,
|
||||
pricing,
|
||||
}: {
|
||||
dialog: GenerateDialogState;
|
||||
projectId?: string | null;
|
||||
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
|
||||
/**
|
||||
* 素材库落点:与其它画布生成工具一样,画布链路会把当前素材夹一起发出去,
|
||||
* 结果因此同时落画布与素材库。缺省或空串表示本次不落素材库。
|
||||
*/
|
||||
assetFolderId?: string | null;
|
||||
/** 素材库里的展示名;缺省用本次结果的标题。 */
|
||||
assetLabel?: string | null;
|
||||
pricing?: Model3dPricingConfig | null;
|
||||
}): { ok: true; plan: Model3dSubmissionPlan } | { ok: false; message: string } {
|
||||
if (!isModel3dGenerationMode(dialog.mode)) {
|
||||
@@ -126,9 +135,18 @@ export function buildModel3dSubmissionPlan({
|
||||
generateParts: params.generateParts,
|
||||
};
|
||||
// 落点与其它生成接口同形:平坦的可选字段,不再包一层 tagged enum。
|
||||
// 服务端按「二选一」校验:这里只给 projectId(画布链路没有落素材库的入口)。
|
||||
// 服务端按「至少一个落点」校验,项目资源与素材库可以同时给 —— 其它画布生成工具
|
||||
// 就是「projectId + assetFolderId + assetLabel」一起发,结果两边都落;3D 跟随同一口径,
|
||||
// 否则同一张画布生成出来的东西,只有 3D 不会出现在素材库。
|
||||
const trimmedAssetFolderId = assetFolderId?.trim();
|
||||
const placement = {
|
||||
projectId: trimmedProjectId,
|
||||
...(trimmedAssetFolderId
|
||||
? {
|
||||
assetFolderId: trimmedAssetFolderId,
|
||||
assetLabel: assetLabel?.trim() || resolveModel3dResultTitle(dialog),
|
||||
}
|
||||
: {}),
|
||||
...(canvasCompletion ? { canvasCompletion } : {}),
|
||||
};
|
||||
const finish = (
|
||||
|
||||
@@ -87,10 +87,12 @@ function createDialog(
|
||||
function renderTask({
|
||||
dialog,
|
||||
projectId = 'project-1',
|
||||
assetFolderId,
|
||||
applyQueuedGeneration = vi.fn(async () => true),
|
||||
}: {
|
||||
dialog: GenerateDialogState;
|
||||
projectId?: string | null;
|
||||
assetFolderId?: string | null;
|
||||
applyQueuedGeneration?: (...args: unknown[]) => Promise<boolean>;
|
||||
}) {
|
||||
const onQueuedGenerationTask = vi.fn();
|
||||
@@ -100,6 +102,7 @@ function renderTask({
|
||||
]);
|
||||
const { submitModel3dGeneration } = useModel3dGenerationTask({
|
||||
projectId,
|
||||
assetFolderId,
|
||||
updateCanvasGenerationDialogById: (dialogId, updater) => {
|
||||
setDialogs((currentDialogs) =>
|
||||
currentDialogs.map((currentDialog) =>
|
||||
@@ -143,6 +146,7 @@ describe('useModel3dGenerationTask', () => {
|
||||
const applyQueuedGeneration = vi.fn(async () => true);
|
||||
const { result, onQueuedGenerationTask } = renderTask({
|
||||
dialog: createDialog(),
|
||||
assetFolderId: 'project',
|
||||
applyQueuedGeneration,
|
||||
});
|
||||
|
||||
@@ -167,6 +171,8 @@ describe('useModel3dGenerationTask', () => {
|
||||
generateParts: false,
|
||||
},
|
||||
projectId: 'project-1',
|
||||
assetFolderId: 'project',
|
||||
assetLabel: '一把木椅',
|
||||
});
|
||||
|
||||
expect(applyQueuedGeneration).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -43,6 +43,7 @@ type UpdateCanvasGenerationDialogById = (
|
||||
*/
|
||||
export function useModel3dGenerationTask({
|
||||
projectId,
|
||||
assetFolderId,
|
||||
updateCanvasGenerationDialogById,
|
||||
applyProjectSnapshot,
|
||||
getGeneratingDialogPlaceholder,
|
||||
@@ -52,6 +53,8 @@ export function useModel3dGenerationTask({
|
||||
onGenerationWarning,
|
||||
}: {
|
||||
projectId?: string | null;
|
||||
/** 当前素材夹:与其它画布生成工具一致,结果同时落素材库。 */
|
||||
assetFolderId?: string | null;
|
||||
updateCanvasGenerationDialogById: UpdateCanvasGenerationDialogById;
|
||||
applyProjectSnapshot?: (project: EditorProjectSnapshot) => void;
|
||||
getGeneratingDialogPlaceholder: (
|
||||
@@ -81,6 +84,8 @@ export function useModel3dGenerationTask({
|
||||
dialog,
|
||||
projectId,
|
||||
canvasCompletion,
|
||||
assetFolderId,
|
||||
assetLabel: resolveModel3dResultTitle(dialog),
|
||||
pricing: readEditorModel3dPricingConfig(),
|
||||
});
|
||||
if (!planResult.ok) {
|
||||
@@ -149,6 +154,7 @@ export function useModel3dGenerationTask({
|
||||
[
|
||||
applyProjectSnapshot,
|
||||
applyQueuedGeneration,
|
||||
assetFolderId,
|
||||
getGeneratingDialogPlaceholder,
|
||||
onGenerationWarning,
|
||||
onQueuedGenerationTask,
|
||||
|
||||
@@ -1600,6 +1600,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
|
||||
const { submitModel3dGeneration } = useModel3dGenerationTask({
|
||||
projectId,
|
||||
assetFolderId,
|
||||
updateCanvasGenerationDialogById,
|
||||
applyProjectSnapshot,
|
||||
getGeneratingDialogPlaceholder,
|
||||
|
||||
Reference in New Issue
Block a user