1
This commit is contained in:
@@ -349,6 +349,39 @@ describe('Match3DResultView', () => {
|
||||
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('发布要求当前难度素材都具备五个视角', () => {
|
||||
const generatedItemAssets = [
|
||||
createReadyGeneratedItemAsset(1),
|
||||
createReadyGeneratedItemAsset(2),
|
||||
{
|
||||
...createReadyGeneratedItemAsset(3),
|
||||
imageViews: createReadyGeneratedItemAsset(3).imageViews?.slice(0, 4),
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={createProfile({
|
||||
summary: '轻松消除水果',
|
||||
coverImageSrc: 'data:image/png;base64,cover',
|
||||
clearCount: 8,
|
||||
difficulty: 2,
|
||||
generatedItemAssets,
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const publishButton = screen.getByRole('button', { name: '发布' });
|
||||
expect(publishButton).toHaveProperty('disabled', true);
|
||||
fireEvent.click(publishButton);
|
||||
expect(match3dWorksService.publishMatch3DWork).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: '难度配置' }));
|
||||
expect(screen.getByText('已生成物品种类')).toBeTruthy();
|
||||
expect(screen.getAllByText('2 种').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('发布前会先把当前 2D 多视角素材写回 profile', async () => {
|
||||
const generatedItemAssets = [
|
||||
createReadyGeneratedItemAsset(1),
|
||||
@@ -776,6 +809,45 @@ describe('Match3DResultView', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('物品详情五视角预览不混入兼容首图', () => {
|
||||
const generatedAsset = createReadyGeneratedItemAsset(1);
|
||||
|
||||
render(
|
||||
<Match3DResultView
|
||||
profile={createProfile({
|
||||
clearCount: 3,
|
||||
generatedItemAssets: [
|
||||
{
|
||||
...generatedAsset,
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/items/item-1/legacy-primary.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/items/item-1/legacy-primary.png',
|
||||
},
|
||||
],
|
||||
})}
|
||||
onBack={() => {}}
|
||||
onStartTestRun={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '素材配置' }));
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '打开物品1物品素材' }),
|
||||
);
|
||||
|
||||
const imageSources = [...document.querySelectorAll('img')].map((image) =>
|
||||
image.getAttribute('src') ?? '',
|
||||
);
|
||||
expect(
|
||||
imageSources.some((source) => source.includes('legacy-primary.png')),
|
||||
).toBe(false);
|
||||
expect(
|
||||
imageSources.some((source) => source.includes('views/view-05.png')),
|
||||
).toBe(true);
|
||||
expect(screen.getAllByText('5 视角').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('草稿阶段仅有切割图片时展示 2D 素材', () => {
|
||||
render(
|
||||
<Match3DResultView
|
||||
|
||||
@@ -229,7 +229,8 @@ function getMatch3DDifficultyOption(optionId: Match3DDifficultyOptionId) {
|
||||
function getMatch3DReadyItemTypeCount(
|
||||
generatedItemAssets: readonly Match3DGeneratedItemAsset[],
|
||||
) {
|
||||
return generatedItemAssets.filter(hasMatch3DGeneratedImageSource).length;
|
||||
return generatedItemAssets.filter(hasMatch3DGeneratedFiveViewImageSource)
|
||||
.length;
|
||||
}
|
||||
|
||||
function getMatch3DPlayableItemTypeCount(
|
||||
@@ -410,12 +411,44 @@ function hasMatch3DGeneratedImageSource(asset: Match3DGeneratedItemAsset) {
|
||||
return getMatch3DGeneratedImageViewSources(asset).length > 0;
|
||||
}
|
||||
|
||||
function hasMatch3DGeneratedFiveViewImageSource(
|
||||
asset: Match3DGeneratedItemAsset,
|
||||
) {
|
||||
return (
|
||||
(asset.imageViews ?? []).filter(
|
||||
(view) =>
|
||||
Boolean(view.imageSrc?.trim()) || Boolean(view.imageObjectKey?.trim()),
|
||||
).length >= 5
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMatch3DGeneratedImageViewSourceFromDraft(
|
||||
view: NonNullable<Match3DGeneratedItemAsset['imageViews']>[number],
|
||||
) {
|
||||
return view.imageObjectKey?.trim() || view.imageSrc?.trim() || '';
|
||||
}
|
||||
|
||||
function resolveMatch3DAssetDraftImageViewSources(
|
||||
asset: Match3DItemAssetDraft,
|
||||
) {
|
||||
return [
|
||||
...new Set(
|
||||
(asset.imageViews ?? [])
|
||||
.map(resolveMatch3DGeneratedImageViewSourceFromDraft)
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function resolveMatch3DAssetDraftPreviewSources(asset: Match3DItemAssetDraft) {
|
||||
const imageViewSources = resolveMatch3DAssetDraftImageViewSources(asset);
|
||||
if (imageViewSources.length > 0) {
|
||||
return imageViewSources.slice(0, 5);
|
||||
}
|
||||
const fallbackSource = asset.referenceImageSrc.trim();
|
||||
return fallbackSource ? [fallbackSource] : [];
|
||||
}
|
||||
|
||||
function hasPersistableMatch3DGeneratedItemAsset(
|
||||
asset: Match3DGeneratedItemAsset,
|
||||
) {
|
||||
@@ -1703,7 +1736,7 @@ function Match3DItemAssetListCard({
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const pillClass = getMatch3DAssetStatusPillClass(asset.status);
|
||||
const imageViews = asset.imageViews ?? [];
|
||||
const previewSources = resolveMatch3DAssetDraftPreviewSources(asset);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1746,7 +1779,7 @@ function Match3DItemAssetListCard({
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
|
||||
{Math.max(imageViews.length, asset.referenceImageSrc ? 1 : 0)} 视角
|
||||
{previewSources.length} 视角
|
||||
</span>
|
||||
<span className="platform-pill platform-pill--neutral px-2.5 py-1 text-[10px]">
|
||||
2D素材
|
||||
@@ -1783,33 +1816,25 @@ function Match3DItemAssetDetail({
|
||||
onChange: (asset: Match3DItemAssetDraft) => void;
|
||||
onGenerateClickSound: (asset: Match3DItemAssetDraft) => void;
|
||||
}) {
|
||||
const imageViews = asset.imageViews ?? [];
|
||||
const previewSources = resolveMatch3DAssetDraftPreviewSources(asset);
|
||||
|
||||
return (
|
||||
<section className="platform-subpanel min-h-0 rounded-[1.5rem] p-4 sm:p-5">
|
||||
<div className="grid min-h-0 gap-4 lg:grid-cols-[minmax(18rem,0.95fr)_minmax(14rem,0.62fr)]">
|
||||
<div className="grid aspect-square min-h-[18rem] grid-cols-2 gap-2 overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/70 p-3">
|
||||
{[
|
||||
asset.referenceImageSrc,
|
||||
...imageViews
|
||||
.map(resolveMatch3DGeneratedImageViewSourceFromDraft)
|
||||
.filter(Boolean),
|
||||
]
|
||||
.filter((source, index, list) => source && list.indexOf(source) === index)
|
||||
.slice(0, 5)
|
||||
.map((source, index) => (
|
||||
<div
|
||||
key={`${source}-${index}`}
|
||||
className="grid min-h-0 place-items-center overflow-hidden rounded-[0.9rem] bg-white/80"
|
||||
>
|
||||
<ResolvedAssetImage
|
||||
src={source}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{previewSources.map((source, index) => (
|
||||
<div
|
||||
key={`${source}-${index}`}
|
||||
className="grid min-h-0 place-items-center overflow-hidden rounded-[0.9rem] bg-white/80"
|
||||
>
|
||||
<ResolvedAssetImage
|
||||
src={source}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 space-y-3">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
import { setStoredAccessToken, clearStoredAccessToken } from './apiClient';
|
||||
import {
|
||||
clearMatch3DGeneratedModelBytesCache,
|
||||
getMatch3DGeneratedImageViewSources,
|
||||
getMatch3DGeneratedModelAssetSources,
|
||||
preloadMatch3DGeneratedModelAssets,
|
||||
readMatch3DGeneratedModelBytes,
|
||||
@@ -113,4 +114,35 @@ describe('match3dGeneratedModelCache', () => {
|
||||
'generated-match3d-assets/session/profile/items/match3d-item-legacy/model.glb',
|
||||
]);
|
||||
});
|
||||
|
||||
test('多视角图片源优先使用 imageViews,兼容首图只做兜底', () => {
|
||||
const sources = getMatch3DGeneratedImageViewSources({
|
||||
itemId: 'match3d-item-1',
|
||||
itemName: '草莓',
|
||||
imageSrc:
|
||||
'/generated-match3d-assets/session/profile/items/item-1/legacy-primary.png',
|
||||
imageObjectKey:
|
||||
'generated-match3d-assets/session/profile/items/item-1/legacy-primary.png',
|
||||
imageViews: [1, 2, 3, 4, 5].map((viewIndex) => ({
|
||||
viewId: `view-${String(viewIndex).padStart(2, '0')}`,
|
||||
viewIndex,
|
||||
imageSrc: `/generated-match3d-assets/session/profile/items/item-1/views/view-${String(viewIndex).padStart(2, '0')}.png`,
|
||||
imageObjectKey: null,
|
||||
})),
|
||||
modelSrc: null,
|
||||
modelObjectKey: null,
|
||||
modelFileName: null,
|
||||
taskUuid: null,
|
||||
subscriptionKey: null,
|
||||
status: 'image_ready',
|
||||
error: null,
|
||||
});
|
||||
|
||||
expect(sources).toHaveLength(5);
|
||||
expect(sources[0]).toContain('views/view-01.png');
|
||||
expect(sources[4]).toContain('views/view-05.png');
|
||||
expect(sources.some((source) => source.includes('legacy-primary'))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,14 +88,17 @@ export function resolveMatch3DGeneratedImageViewSource(
|
||||
export function getMatch3DGeneratedImageViewSources(
|
||||
asset: Match3DGeneratedItemAsset,
|
||||
) {
|
||||
const sources =
|
||||
const viewSources =
|
||||
asset.imageViews
|
||||
?.map(resolveMatch3DGeneratedImageViewSource)
|
||||
.filter((source) => source.length > 0) ?? [];
|
||||
if (viewSources.length > 0) {
|
||||
return [...new Set(viewSources)];
|
||||
}
|
||||
const primarySource =
|
||||
normalizeMatch3DModelSource(asset.imageObjectKey) ||
|
||||
normalizeMatch3DModelSource(asset.imageSrc);
|
||||
return [...new Set(primarySource ? [primarySource, ...sources] : sources)];
|
||||
return primarySource ? [primarySource] : [];
|
||||
}
|
||||
|
||||
export function resolveMatch3DGeneratedImageAssetSource(
|
||||
|
||||
Reference in New Issue
Block a user