AGC 资源画布切换为 6 类资产分类加项目版本栏目(阶段二:投影轴与栏目表落地)

- 契约层把 ProjectResourceCanvasPosition.section 加宽为 PersistedProjectResourceCanvasSection 并删除旧 ProjectResourceCanvasSection 联合
- 七个分区类型消费点改用 ProjectResourceCanvasCategory:投影、布局模型、布局 Hook、分区高度、依赖图层与画布历史
- RESOURCE_CANVAS_SECTION_ORDER 改为 PROJECT_RESOURCE_CANVAS_SECTIONS,删除 RESOURCE_CANVAS_VISIBLE_SECTION_ORDER
- reconcileResourceCanvasLayout 接入读时归一化:旧栏目值按资源当前分区改写 section,坐标与手动标记原样保留
- 归并后的布局与原布局逐项不同,由既有协调路径判定 changed 并写回一次新分区,不引入迁移脚本
- 投影层五个资源入表点统一走 projectResourceCanvasCategory:项目版本独立成栏、Agent 回执归文档、其余按资产分类
- 新增 projectResourceDisplayKind 收敛 manifest 资产 subtype 即 kind 的显示类型口径,projectResourceTypeLabel 不再依赖旧栏目
- 卡片预览、编辑分流、媒体工具条、预览文案与画布历史快照改用显示类型与新分区,历史快照入栈时收窄到现行分区
- index.tsx 删除可见栏目导入与 visibleCategoryOrder,栏目标签由资源筛选唯一中文口径派生并补齐 7 栏
- index.tsx 补齐 7 栏默认视口与 7 个栏目图标(新增 LayoutGrid、Users、PackageOpen,移除已无用的 Code2)
- index.tsx 删除 canvasResources 的 code 过滤:只登记游戏代码的项目现在落在待归类栏目并正常显示卡片
- 新增只登记游戏代码项目的 UI 回归用例,覆盖栏目大纲、默认落地栏目与代码卡片渲染
- 测试夹具按新分区轴迁移,新增旧栏目 sidecar 读时归并用例与栏目顺序用例
- 共享测试 harness 的资源卡查询正则允许栏目标签自带空格(UI 交互)
- 同步 PRD 与共享记忆决策记录的栏目口径描述
This commit is contained in:
2026-09-10 20:41:19 +08:00
parent eda34187fc
commit 2f8753f597
30 changed files with 848 additions and 496 deletions
@@ -6,13 +6,26 @@
* 本模块只做纯函数的快照栈。
*/
import type { ProjectResourceCanvasSection } from '../../../../../packages/shared/src/contracts/gameCreationApp';
import {
isProjectResourceCanvasCategory,
type PersistedProjectResourceCanvasSection,
type ProjectResourceCanvasCategory,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
export const MAX_RESOURCE_CANVAS_HISTORY_STEPS = 40;
export type ResourceCanvasLayoutSnapshotEntry = {
resourceId: string;
section: ProjectResourceCanvasSection;
section: ProjectResourceCanvasCategory;
x: number;
y: number;
manuallyPlaced: boolean;
};
/** 快照输入:直接消费布局 sidecar 的坐标,`section` 允许仍是旧栏目取值。 */
export type ResourceCanvasLayoutSnapshotEntryInput = {
resourceId: string;
section: PersistedProjectResourceCanvasSection;
x: number;
y: number;
manuallyPlaced: boolean;
@@ -41,21 +54,29 @@ export function createResourceCanvasHistory(): ResourceCanvasHistory {
/**
* 按 resourceId 排序后落快照,保证同一组坐标无论 map 顺序如何都比较得出相等。
*
* 画布上的坐标经协调后一定是现行分区;仍是旧栏目取值的坐标不属于本会话的撤销范围。
*/
export function captureResourceCanvasSnapshot(
positions: readonly ResourceCanvasLayoutSnapshotEntry[],
positions: readonly ResourceCanvasLayoutSnapshotEntryInput[],
): ResourceCanvasLayoutSnapshot {
return {
entries: positions
.map((position) => ({
resourceId: position.resourceId,
section: position.section,
x: position.x,
y: position.y,
manuallyPlaced: position.manuallyPlaced,
}))
.sort((left, right) => left.resourceId.localeCompare(right.resourceId)),
};
const entries: ResourceCanvasLayoutSnapshotEntry[] = [];
for (const position of positions) {
if (!isProjectResourceCanvasCategory(position.section)) {
continue;
}
entries.push({
resourceId: position.resourceId,
section: position.section,
x: position.x,
y: position.y,
manuallyPlaced: position.manuallyPlaced,
});
}
entries.sort((left, right) =>
left.resourceId.localeCompare(right.resourceId),
);
return { entries };
}
export function resourceCanvasSnapshotsEqual(
@@ -189,7 +210,7 @@ export function canRedoResourceCanvasHistory(history: ResourceCanvasHistory) {
*/
export function resolveResourceCanvasRestoreEntries(
snapshot: ResourceCanvasLayoutSnapshot,
current: readonly ResourceCanvasLayoutSnapshotEntry[],
current: readonly ResourceCanvasLayoutSnapshotEntryInput[],
): ResourceCanvasLayoutSnapshotEntry[] {
const currentById = new Map(
current.map((entry) => [entry.resourceId, entry] as const),
@@ -6,7 +6,10 @@ import type {
} from '../../../../../src/components/image-editor/ImageCanvasEditorTypes';
import type { ImageCanvasSelectedToolbarAction } from '../../../../../src/components/image-editor/ImageCanvasSelectedLayerToolbarView';
import { canonicalProjectedResourceMediaType } from '../../view/project-development/resourceEditModel';
import type { ProjectResource } from '../../view/project-development/resourceProjectionModel';
import {
type ProjectResource,
projectResourceDisplayKind,
} from '../../view/project-development/resourceProjectionModel';
import { isResourceCanvasExportable } from './resourceCanvasAssetTransferModel';
/** 快速编辑与改造都走 `derive_local_project_resource`,只接受这三种栅格图片格式。 */
@@ -33,7 +36,7 @@ const CANVAS_ASSET_KINDS = new Set<string>([
]);
export function resourceCanvasMediaType(
resource: Pick<ProjectResource, 'mediaType' | 'category' | 'path'>,
resource: Pick<ProjectResource, 'mediaType' | 'subtype' | 'path'>,
): CanvasMediaType | undefined {
const mediaType = canonicalProjectedResourceMediaType({
mediaType: resource.mediaType,
@@ -45,7 +48,10 @@ export function resourceCanvasMediaType(
if (mediaType.startsWith('video/')) {
return 'video';
}
if (mediaType.startsWith('audio/') || resource.category === 'audio') {
if (
mediaType.startsWith('audio/') ||
projectResourceDisplayKind(resource) === 'audio'
) {
return 'audio';
}
return undefined;
@@ -9,8 +9,9 @@ import {
} from 'react';
import type {
PersistedProjectResourceCanvasSection,
ProjectResourceCanvasCategory,
ProjectResourceCanvasPosition,
ProjectResourceCanvasSection,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import {
resourceCanvasCardSize,
@@ -38,7 +39,7 @@ type RectLookup = {
export type ResourceDependencyOverlayProps = {
graph: ProjectResourceGraph;
positions: readonly ProjectResourceCanvasPosition[];
section?: ProjectResourceCanvasSection;
section?: ProjectResourceCanvasCategory;
visibleResourceIds: ReadonlySet<string>;
cardSizeByResourceId?: ResourceCanvasCardSizeByResourceId;
canvasViewport?: Readonly<Point & { scale: number }>;
@@ -180,7 +181,10 @@ function connectionAxis(source: Rect, target: Rect): ConnectionAxis {
function referenceStaysInSection(
edge: ProjectResourceReferenceEdge,
sectionByResourceId: ReadonlyMap<string, ProjectResourceCanvasSection>,
sectionByResourceId: ReadonlyMap<
string,
PersistedProjectResourceCanvasSection
>,
) {
const sourceSection = sectionByResourceId.get(edge.sourceResourceId);
return (
@@ -192,7 +196,10 @@ function referenceStaysInSection(
function referenceRouteOffsets(
edges: readonly ProjectResourceReferenceEdge[],
rectByResourceId: RectLookup,
sectionByResourceId: ReadonlyMap<string, ProjectResourceCanvasSection>,
sectionByResourceId: ReadonlyMap<
string,
PersistedProjectResourceCanvasSection
>,
) {
type Endpoint = {
axis: ConnectionAxis;
@@ -16,17 +16,18 @@ import {
import { save as saveNativeFileDialog } from '@tauri-apps/plugin-dialog';
import {
AtSign,
Code2,
Crosshair,
FileText,
FolderTree,
Gamepad2,
Image,
Info,
LayoutGrid,
ListFilter,
Maximize2,
Minus,
Music2,
PackageOpen,
Pause,
Pencil,
Play,
@@ -38,6 +39,7 @@ import {
SlidersHorizontal,
Sparkles,
Undo2,
Users,
X,
ZoomOut,
} from 'lucide-react';
@@ -87,6 +89,7 @@ import {
import {
dispatchResourceReferenceInsert,
RESOURCE_REFERENCE_FILTERS,
resourceReferenceCategoryLabel,
type ResourceReferenceFilter,
} from '../../features/project-workspace/resourceReferences';
import { GameRunVersionPicker } from '../../features/resource-canvas/GameRunVersionPicker';
@@ -188,7 +191,6 @@ import {
RESOURCE_CANVAS_SECTION_MIN_HEIGHT,
RESOURCE_CANVAS_SECTION_MIN_WIDTH,
RESOURCE_CANVAS_SECTION_ORDER,
RESOURCE_CANVAS_VISIBLE_SECTION_ORDER,
type ResourceCanvasCardSize,
resourceCanvasCardSize,
resourceCanvasContentBounds,
@@ -376,10 +378,12 @@ function defaultResourceCanvasCategoryFilters(): ResourceCanvasCategoryFilterByS
function defaultResourceCanvasViewports(): ResourceCanvasViewportBySortMode {
const byCategory = (): ResourceCanvasViewportByCategory => ({
document: { x: 48, y: 48, scale: 1 },
art: { x: 48, y: 48, scale: 1 },
'ui-interaction': { x: 48, y: 48, scale: 1 },
character: { x: 48, y: 48, scale: 1 },
scene: { x: 48, y: 48, scale: 1 },
audio: { x: 48, y: 48, scale: 1 },
code: { x: 48, y: 48, scale: 1 },
document: { x: 48, y: 48, scale: 1 },
unclassified: { x: 48, y: 48, scale: 1 },
version: { x: 48, y: 48, scale: 1 },
});
return {
@@ -464,22 +468,29 @@ export type ProjectDevelopmentViewProps = {
};
const categoryOrder = RESOURCE_CANVAS_SECTION_ORDER;
const visibleCategoryOrder = RESOURCE_CANVAS_VISIBLE_SECTION_ORDER;
/**
* 栏目标签复用资源筛选的唯一中文口径;「项目版本」不是资源资产、单独成栏,
* 不在筛选口径内,因此只有它在这里给出文案。
*/
const categoryLabels: Record<ResourceCategory, string> = {
code: '游戏代码',
document: '设计文档',
'ui-interaction': resourceReferenceCategoryLabel('ui-interaction'),
character: resourceReferenceCategoryLabel('character'),
scene: resourceReferenceCategoryLabel('scene'),
audio: resourceReferenceCategoryLabel('audio'),
document: resourceReferenceCategoryLabel('document'),
unclassified: resourceReferenceCategoryLabel('unclassified'),
version: '项目版本',
art: '美术资源',
audio: '音乐音效',
};
const categoryIcons: Record<ResourceCategory, typeof FileText> = {
code: Code2,
document: FileText,
version: Gamepad2,
art: Image,
'ui-interaction': LayoutGrid,
character: Users,
scene: Image,
audio: Music2,
document: FileText,
unclassified: PackageOpen,
version: Gamepad2,
};
const approvalOptions: Array<{
@@ -1767,10 +1778,11 @@ export default function ProjectDevelopmentView({
}),
[manifestTaskById, projectedResources, resourceGraph],
);
const canvasResources = useMemo(
() => resources.filter((resource) => resource.category !== 'code'),
[resources],
);
/**
* 画布消费全部已投影资源:分区轴改为资产功能分类后不再有被栏目排除的内部资源,
* 只登记游戏代码的项目也会在「待归类」栏目正常显示卡片。
*/
const canvasResources = resources;
/** 资源画布选中态:单选是长度为 1 的数组,多选/框选保持同一份状态。 */
const selectedResourceId = selectedResourceIds[0] ?? null;
const selectedResource =
@@ -1969,7 +1981,7 @@ export default function ProjectDevelopmentView({
const visibleResourcesByCategory = useMemo(
() =>
new Map(
visibleCategoryOrder.map((category) => [
categoryOrder.map((category) => [
category,
visibleResources.filter((resource) => resource.category === category),
]),
@@ -1979,7 +1991,7 @@ export default function ProjectDevelopmentView({
const projectResourcesByCategory = useMemo(
() =>
new Map(
visibleCategoryOrder.map((category) => [
categoryOrder.map((category) => [
category,
canvasResources.filter((resource) => resource.category === category),
]),
@@ -1989,7 +2001,7 @@ export default function ProjectDevelopmentView({
const resourceIdsByCategory = useMemo(
() =>
new Map(
visibleCategoryOrder.map((category) => [
categoryOrder.map((category) => [
category,
new Set(
(projectResourcesByCategory.get(category) ?? []).map(
@@ -2019,7 +2031,7 @@ export default function ProjectDevelopmentView({
[canvasResources],
);
const resourcePageCategories = useMemo(
() => (resourcePageCategoryKey ? [...visibleCategoryOrder] : []),
() => (resourcePageCategoryKey ? [...categoryOrder] : []),
[resourcePageCategoryKey],
);
const activePageCategory =
@@ -2133,18 +2145,16 @@ export default function ProjectDevelopmentView({
return;
}
const categoriesWithNewResources = visibleCategoryOrder.filter(
(category) => {
if (category === viewedResourceCategory) {
return false;
}
const previousIds =
previousSnapshot.resourceIdsByCategory.get(category) ?? new Set();
return Array.from(resourceIdsByCategory.get(category) ?? []).some(
(resourceId) => !previousIds.has(resourceId),
);
},
);
const categoriesWithNewResources = categoryOrder.filter((category) => {
if (category === viewedResourceCategory) {
return false;
}
const previousIds =
previousSnapshot.resourceIdsByCategory.get(category) ?? new Set();
return Array.from(resourceIdsByCategory.get(category) ?? []).some(
(resourceId) => !previousIds.has(resourceId),
);
});
resourceCategorySnapshotRef.current = {
scopeKey: resourceCategoryScopeKey,
resourceIdsByCategory,
@@ -2299,7 +2309,7 @@ export default function ProjectDevelopmentView({
height: RESOURCE_CANVAS_FALLBACK_HEIGHT,
};
return new Map(
visibleCategoryOrder.map((category) => {
categoryOrder.map((category) => {
const viewport = normalizeResourceBookViewport(
resourceCanvasViewports[sortMode][category],
);
@@ -2639,7 +2649,7 @@ export default function ProjectDevelopmentView({
() =>
buildResourceBookScenePlan({
state: resourceBookState,
visibleCategoryOrder,
visibleCategoryOrder: categoryOrder,
resourcesByCategory: projectResourcesByCategory,
overviewRects: resourceBookOverviewRects,
positions: resourcePositionById,
@@ -2670,7 +2680,7 @@ export default function ProjectDevelopmentView({
);
const resourceBookOverviewReady = useMemo(
() =>
visibleCategoryOrder.every((category) => {
categoryOrder.every((category) => {
const rect = resourceBookOverviewRects.get(category);
return Boolean(rect && rect.width > 0 && rect.height > 0);
}),
@@ -5834,7 +5844,7 @@ export default function ProjectDevelopmentView({
</span>
</header>
<div className="game-resource-book-main-grid">
{visibleCategoryOrder.map((category) => (
{categoryOrder.map((category) => (
<ResourceBookThumbnail
key={category}
category={category}
@@ -6012,7 +6022,7 @@ export default function ProjectDevelopmentView({
</div>
) : (
<div className="game-resource-section-stack">
{visibleCategoryOrder.map((category) => {
{categoryOrder.map((category) => {
const Icon = categoryIcons[category];
const sectionHeight =
resourceSectionHeights.sectionStates.get(category);
@@ -8,11 +8,13 @@ import {
GAME_CREATION_RESOURCE_LAYOUT_MAX_COORDINATE,
GAME_CREATION_RESOURCE_LAYOUT_MIN_COORDINATE,
GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION,
PROJECT_RESOURCE_CANVAS_SECTIONS,
type ProjectResourceCanvasCategory,
type ProjectResourceCanvasLayout,
type ProjectResourceCanvasLayoutMode,
type ProjectResourceCanvasPosition,
type ProjectResourceCanvasSection,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { normalizeResourceCanvasPosition } from './resourceCanvasSectionMapping';
export const RESOURCE_CANVAS_CARD_WIDTH = 180;
export const RESOURCE_CANVAS_CARD_HEIGHT = 128;
@@ -41,16 +43,12 @@ export const RESOURCE_CANVAS_INITIAL_FIT_MAX_SCALE = 1.5;
*/
export const RESOURCE_CANVAS_FIT_PADDING = 16;
export const RESOURCE_CANVAS_SECTION_ORDER: readonly ProjectResourceCanvasSection[] =
['document', 'art', 'audio', 'code', 'version'];
/**
* Ordinary resource-canvas navigation intentionally omits game code. The
* complete five-section order above remains the internal layout/sidecar
* contract so existing code-resource coordinates are not rewritten.
* 栏目顺序 = 6 类资源资产分类 + 末尾独立的「项目版本」栏目。项目版本不是资源资产,
* 6 类资产分类轴对它不适用,因此固定单独成栏。
*/
export const RESOURCE_CANVAS_VISIBLE_SECTION_ORDER: readonly ProjectResourceCanvasSection[] =
['document', 'art', 'audio', 'version'];
export const RESOURCE_CANVAS_SECTION_ORDER: readonly ProjectResourceCanvasCategory[] =
PROJECT_RESOURCE_CANVAS_SECTIONS;
export type ResourceCanvasCardSize = {
width: number;
@@ -106,7 +104,7 @@ export type ResourceCanvasCardSizeByResourceId = ReadonlyMap<
export type ResourceCanvasItem = {
id: string;
category: ProjectResourceCanvasSection;
category: ProjectResourceCanvasCategory;
subtype: string;
label: string;
mediaType: string;
@@ -880,7 +878,7 @@ function dependencyColumnXByDepth(
}
function dependencyAutomaticPositions(
section: ProjectResourceCanvasSection,
section: ProjectResourceCanvasCategory,
sectionResources: readonly ResourceCanvasItem[],
automaticResources: readonly ResourceCanvasItem[],
preservedPositions: readonly ProjectResourceCanvasPosition[],
@@ -1142,8 +1140,8 @@ export function reconcileResourceCanvasLayout(
resources,
explicitCardSizeByResourceId,
);
const resourceById = new Map(
resources.map((resource) => [resource.id, resource]),
const categoryByResourceId = new Map(
resources.map((resource) => [resource.id, resource.category]),
);
const positionsBySection = new Map(
RESOURCE_CANVAS_SECTION_ORDER.map((section) => [
@@ -1151,13 +1149,19 @@ export function reconcileResourceCanvasLayout(
[] as ProjectResourceCanvasPosition[],
]),
);
// 读时兼容映射:旧栏目值按资源当前分区归并,只改写 section,坐标与手动标记原样保留。
const preserved = source.positions.filter((position) => {
const resource = resourceById.get(position.resourceId);
const keep = resource?.category === position.section;
if (keep) {
positionsBySection.get(position.section)?.push(position);
const normalized = normalizeResourceCanvasPosition(
position,
categoryByResourceId,
);
if (!normalized) {
return false;
}
return keep;
positionsBySection
.get(normalized.position.section)
?.push(normalized.position);
return true;
});
const preservedIds = new Set(
preserved.map((position) => position.resourceId),
@@ -1239,7 +1243,7 @@ export function reconcileResourceCanvasLayout(
export function moveResourceCanvasPosition(
layout: ProjectResourceCanvasLayout,
resourceId: string,
section: ProjectResourceCanvasSection,
section: ProjectResourceCanvasCategory,
x: number,
y: number,
): ProjectResourceCanvasLayout {
@@ -1,4 +1,7 @@
import type { ProjectResource } from './resourceProjectionModel';
import {
type ProjectResource,
projectResourceDisplayKind,
} from './resourceProjectionModel';
export const PROJECT_RESOURCE_CARD_PREVIEW_CONCURRENCY = 3;
export const PROJECT_RESOURCE_CARD_PREVIEW_CACHE_LIMIT = 48;
@@ -137,16 +140,20 @@ const videoExtension = /\.(mp4|webm|mov)$/iu;
export function projectResourceCardPreviewKind(
resource: ProjectResource,
): ProjectResourceCardPreviewKind {
if (resource.category === 'version') {
if (resource.subtype === 'project-version') {
return 'version';
}
if (resource.category === 'code') {
return 'code';
}
if (resource.category === 'document') {
if (resource.subtype === 'agent-result') {
return 'document';
}
if (resource.category === 'audio') {
const kind = projectResourceDisplayKind(resource);
if (kind === 'code') {
return 'code';
}
if (kind === 'document') {
return 'document';
}
if (kind === 'audio') {
return 'audio';
}
const mediaType = resource.mediaType.trim().toLowerCase();
@@ -1,4 +1,7 @@
import type { ProjectResource } from './resourceProjectionModel';
import {
type ProjectResource,
projectResourceDisplayKind,
} from './resourceProjectionModel';
export type LocalProjectResourceEditKind =
| 'image-reference'
@@ -151,7 +154,10 @@ export function resolveProjectResourceEditCapability(
semanticNotice: null,
};
}
if (resource.category === 'audio' || mediaType.startsWith('audio/')) {
if (
mediaType.startsWith('audio/') ||
projectResourceDisplayKind(resource) === 'audio'
) {
return {
route: 'derive',
editKind: isBackgroundMusic(resource)
@@ -5,10 +5,9 @@ import {
type GameCreationAppManifest,
type GameIterationVersion,
type ProjectResourceCanvasCategory,
type ProjectResourceCanvasSection,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
export type ProjectResourceCategory = ProjectResourceCanvasSection;
export type ProjectResourceCategory = ProjectResourceCanvasCategory;
export type ProjectAttachmentResult = {
fileName: string;
@@ -146,47 +145,57 @@ export function projectedResourceKind(input: {
return null;
}
/**
* 资源显示类型判定:与准入使用同一套扩展名 / mediaType / manifest `kind` 口径。
*
* manifest 资产的 `subtype` 就是 `asset.kind`,任务产物、附件与回执使用各自的稳定
* 人造 subtype,不会命中 kind 规则,因此这里可以直接透传。
*/
export function projectResourceDisplayKind(
resource: Pick<ProjectResource, 'path' | 'mediaType' | 'subtype'>,
): ProjectedResourceKind | null {
return projectedResourceKind({
path: resource.path,
mediaType: resource.mediaType,
kind: resource.subtype,
});
}
/**
* Converts an already projected resource into short user-facing type text.
* This is deliberately display-only: it derives from existing projection
* fields and does not add a backend/read-model attribute.
*/
export function projectResourceTypeLabel(
resource: Pick<
ProjectResource,
'category' | 'subtype' | 'path' | 'mediaType'
>,
resource: Pick<ProjectResource, 'subtype' | 'path' | 'mediaType'>,
): ProjectResourceTypeLabel {
const subtype = resource.subtype.trim().toLowerCase();
const path = resource.path.trim().toLowerCase();
const mediaType = resource.mediaType.trim().toLowerCase();
const kind = projectResourceDisplayKind(resource);
if (resource.category === 'version' || subtype === 'project-version') {
if (subtype === 'project-version') {
return '项目版本';
}
if (subtype === 'agent-result') {
return 'Agent 回执';
}
if (resource.category === 'code') {
if (kind === 'code') {
return '游戏代码';
}
if (mediaType === 'image/svg+xml' || /\.svg$/iu.test(path)) {
return 'SVG';
}
if (
resource.category === 'audio' ||
mediaType.startsWith('audio/') ||
audioExtension.test(path)
) {
if (kind === 'audio') {
return '音频';
}
if (mediaType.startsWith('video/') || /\.(mp4|webm|mov)$/iu.test(path)) {
return '视频';
}
if (resource.category === 'document' || mediaType.startsWith('text/')) {
if (kind === 'document') {
return '文档';
}
if (resource.category === 'art' || mediaType.startsWith('image/')) {
if (kind === 'art') {
return '图片';
}
if (subtype === 'task-artifact') {
@@ -250,22 +259,22 @@ export function projectResourcesFromReadModels(
continue;
}
for (const path of task.artifacts) {
const category = projectedResourceKind({ path, mediaType: '' });
if (!category) {
const kind = projectedResourceKind({ path, mediaType: '' });
if (!kind) {
continue;
}
resources.push({
id: `task:${task.id}:${path}`,
category,
category: projectResourceCanvasCategory({ subtype: 'task-artifact' }),
subtype: 'task-artifact',
label: fileName(path),
path,
mediaType:
category === 'document'
kind === 'document'
? '项目文档'
: category === 'code'
: kind === 'code'
? '游戏代码'
: category === 'art'
: kind === 'art'
? '美术产物'
: '音乐音效产物',
sourceLabel: '任务产物',
@@ -281,20 +290,24 @@ export function projectResourcesFromReadModels(
}
for (const asset of manifest.assets) {
const category = projectedResourceKind({
const kind = projectedResourceKind({
path: asset.localPath,
mediaType: asset.mediaType,
kind: asset.kind,
});
if (!category) {
if (!kind) {
continue;
}
const assetCategory = gameCreationAppAssetCategory(asset);
const isPendingUiPrototype =
asset.kind === 'ui-prototype' &&
taskById.get('design-foundation')?.status !== 'completed';
resources.push({
id: `asset:${asset.id}`,
category,
category: projectResourceCanvasCategory({
subtype: asset.kind,
assetCategory,
}),
subtype: asset.kind,
label: `${fileName(asset.localPath)}${
isPendingUiPrototype ? '(待视觉验收)' : ''
@@ -316,7 +329,7 @@ export function projectResourcesFromReadModels(
referenceResourceIds: asset.source.referenceResourceIds ?? [],
dependencies: [],
dependencyDepth: 0,
assetCategory: gameCreationAppAssetCategory(asset),
assetCategory,
assetTags: gameCreationAppAssetTags(asset),
imageSequenceFrames: asset.imageSequenceFrames ?? null,
imageSequenceDurationMs: asset.imageSequenceDurationMs ?? null,
@@ -327,16 +340,16 @@ export function projectResourcesFromReadModels(
if (attachment.status !== 'imported' || !attachment.localPath) {
continue;
}
const category = projectedResourceKind({
const kind = projectedResourceKind({
path: attachment.localPath,
mediaType: attachment.mediaType,
});
if (!category) {
if (!kind) {
continue;
}
resources.push({
id: `attachment:${attachment.localPath}`,
category,
category: projectResourceCanvasCategory({ subtype: 'attachment' }),
subtype: 'attachment',
label: attachment.fileName,
path: attachment.localPath,
@@ -355,7 +368,7 @@ export function projectResourcesFromReadModels(
for (const result of agentResults) {
resources.push({
id: `agent-result:${result.agentId}:${result.runId}`,
category: 'document',
category: projectResourceCanvasCategory({ subtype: 'agent-result' }),
subtype: 'agent-result',
label: result.title,
path: `专业 Agent 文本回执 · ${result.label}`,
@@ -390,7 +403,7 @@ export function projectResourcesFromReadModels(
};
resources.push({
id: `version:${version.versionId}`,
category: 'version',
category: projectResourceCanvasCategory({ subtype: 'project-version' }),
subtype: 'project-version',
label: version.label,
path: `项目版本 · ${version.versionId}`,
@@ -1,6 +1,6 @@
import type {
ProjectResourceCanvasCategory,
ProjectResourceCanvasLayoutMode,
ProjectResourceCanvasSection,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { RESOURCE_CANVAS_CARD_HEIGHT } from './resourceCanvasLayoutModel';
@@ -32,7 +32,7 @@ export type ProjectResourceSectionZoomAction =
export function projectResourceSectionHeightKey(input: {
projectId: string;
mode: ProjectResourceCanvasLayoutMode;
section: ProjectResourceCanvasSection;
section: ProjectResourceCanvasCategory;
}) {
return `${input.projectId}\n${input.mode}\n${input.section}`;
}
@@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type {
ProjectResourceCanvasCategory,
ProjectResourceCanvasLayout,
ProjectResourceCanvasLayoutMode,
ProjectResourceCanvasSection,
UpdateProjectResourceCanvasLayoutResult,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import {
@@ -38,7 +38,7 @@ type ManualLayoutWriteIntent = {
kind: 'manual';
scopeEpoch: number;
resourceId: string;
section: ProjectResourceCanvasSection;
section: ProjectResourceCanvasCategory;
x: number;
y: number;
};
@@ -743,7 +743,7 @@ export function useProjectResourceCanvasLayout({
const commitPosition = useCallback(
(
resourceId: string,
section: ProjectResourceCanvasSection,
section: ProjectResourceCanvasCategory,
x: number,
y: number,
) => {
@@ -29,7 +29,10 @@ import {
type ProjectResourceCardPreviewState,
type ProjectResourceCardPreviewTransportPayload,
} from './resourceCardPreviewModel';
import type { ProjectResource } from './resourceProjectionModel';
import {
type ProjectResource,
projectResourceDisplayKind,
} from './resourceProjectionModel';
type PreviewRequestReason = 'visible' | 'detail' | 'play';
@@ -58,6 +61,17 @@ type ObservedPreviewCard = {
resource: ProjectResource;
};
function resourceReadKindLabel(resource: ProjectResource) {
const kind = projectResourceDisplayKind(resource);
if (kind === 'document') {
return '文档';
}
if (kind === 'code') {
return '游戏代码';
}
return '资源';
}
function previewReadErrorMessage(
resource: ProjectResource,
error: unknown,
@@ -65,25 +79,13 @@ function previewReadErrorMessage(
const message = error instanceof Error ? error.message : String(error);
if (message.includes('项目权限策略要求用户确认')) {
return {
error: `当前项目策略要求先确认读取${
resource.category === 'document'
? '文档'
: resource.category === 'code'
? '游戏代码'
: '资源'
},确认后请关闭详情并重试`,
error: `当前项目策略要求先确认读取${resourceReadKindLabel(resource)},确认后请关闭详情并重试`,
retryable: true,
};
}
if (message.includes('项目权限策略拒绝执行')) {
return {
error: `当前项目策略不允许读取${
resource.category === 'document'
? '文档'
: resource.category === 'code'
? '游戏代码'
: '资源'
},调整策略后请关闭详情并重试`,
error: `当前项目策略不允许读取${resourceReadKindLabel(resource)},调整策略后请关闭详情并重试`,
retryable: true,
};
}
@@ -7,8 +7,8 @@ import {
} from 'react';
import type {
ProjectResourceCanvasCategory,
ProjectResourceCanvasLayoutMode,
ProjectResourceCanvasSection,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import { RESOURCE_CANVAS_SECTION_ORDER } from './resourceCanvasLayoutModel';
import {
@@ -160,7 +160,7 @@ export function useProjectResourceSectionHeights(input: {
const updateSectionHeight = useCallback(
(
section: ProjectResourceCanvasSection,
section: ProjectResourceCanvasCategory,
action: ProjectResourceSectionHeightAction,
) => {
const key = projectResourceSectionHeightKey({ projectId, mode, section });
@@ -186,7 +186,7 @@ export function useProjectResourceSectionHeights(input: {
);
const setSectionZoom = useCallback(
(section: ProjectResourceCanvasSection, zoom: number) => {
(section: ProjectResourceCanvasCategory, zoom: number) => {
const key = projectResourceSectionHeightKey({ projectId, mode, section });
const nextZoom = clampProjectResourceSectionZoom(zoom);
setSessionZooms((current) => {
@@ -206,7 +206,7 @@ export function useProjectResourceSectionHeights(input: {
const updateSectionZoom = useCallback(
(
section: ProjectResourceCanvasSection,
section: ProjectResourceCanvasCategory,
action: ProjectResourceSectionZoomAction,
) => {
const key = projectResourceSectionHeightKey({ projectId, mode, section });
@@ -13,7 +13,7 @@ import React from 'react';
import { describe, expect, it, vi } from 'vitest';
import type { ProjectResourceCanvasPosition } from '../../../packages/shared/src/contracts/gameCreationApp';
import type { ProjectResourceCanvasSection } from '../../../packages/shared/src/contracts/gameCreationApp';
import type { ProjectResourceCanvasCategory } from '../../../packages/shared/src/contracts/gameCreationApp';
import {
RESOURCE_CANVAS_CARD_WIDTH,
RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP,
@@ -33,7 +33,7 @@ function position(
resourceId: string,
x: number,
y: number,
section: ProjectResourceCanvasSection = 'art',
section: ProjectResourceCanvasCategory = 'scene',
): ProjectResourceCanvasPosition {
return {
resourceId,
@@ -196,7 +196,7 @@ describe('ResourceDependencyOverlay', () => {
it('在统一 world 模式下渲染跨分类引用,并使用稳定的 world 标识', async () => {
const graph = graphFixture();
const positions = [
position('source:one', 0, 0, 'art'),
position('source:one', 0, 0, 'scene'),
position('target:one', 220, 0, 'document'),
];
render(
@@ -227,10 +227,10 @@ describe('ResourceDependencyOverlay', () => {
const graph = graphFixture();
const positions = [
position('source:one', 0, 0, 'document'),
position('source:two', 0, 0, 'art'),
position('source:two', 0, 0, 'scene'),
position('source:three', 0, 120, 'document'),
position('target:one', 240, 0, 'art'),
position('target:two', 240, 120, 'art'),
position('target:one', 240, 0, 'scene'),
position('target:two', 240, 120, 'scene'),
position('target:three', 240, 0, 'audio'),
];
render(
@@ -301,8 +301,8 @@ describe('ResourceDependencyOverlay', () => {
producerMappingTruncated: false,
});
const positions = [
position('art:source', 0, 0, 'art'),
position('art:target', 240, 0, 'art'),
position('art:source', 0, 0, 'scene'),
position('art:target', 240, 0, 'scene'),
position('version:source', 0, 0, 'version'),
position('version:target', 240, 0, 'version'),
];
@@ -331,7 +331,7 @@ describe('ResourceDependencyOverlay', () => {
overlayView(graph, positions, new Set(resourceIds)),
);
const artOverlay = await screen.findByTestId(
'resource-dependency-overlay-art',
'resource-dependency-overlay-scene',
);
const versionOverlay = await screen.findByTestId(
'resource-dependency-overlay-version',
@@ -357,10 +357,10 @@ describe('ResourceDependencyOverlay', () => {
?.getAttribute('d');
const artViewport = rendered.container.querySelector<HTMLElement>(
'[data-resource-section-scroll="art"]',
'[data-resource-section-scroll="scene"]',
);
if (!artViewport) {
throw new Error('missing art viewport');
throw new Error('missing scene viewport');
}
artViewport.scrollTop = 80;
fireEvent.scroll(artViewport);
@@ -527,11 +527,11 @@ describe('ResourceDependencyOverlay', () => {
{ 'data-testid': 'resource-dependency-overlay' },
React.createElement(
'div',
{ 'data-resource-section-scroll': 'art' },
{ 'data-resource-section-scroll': 'scene' },
React.createElement(
'div',
{
'data-resource-section-plane': 'art',
'data-resource-section-plane': 'scene',
'data-resource-section-scale': '1.5',
},
React.createElement(ResourceDependencyOverlay, {
@@ -545,7 +545,7 @@ describe('ResourceDependencyOverlay', () => {
0,
),
],
section: 'art',
section: 'scene',
visibleResourceIds: new Set(['source:one', 'target:one']),
geometryRevision: '340@1.5',
}),
@@ -581,11 +581,11 @@ describe('ResourceDependencyOverlay', () => {
{ 'data-testid': 'resource-dependency-overlay' },
React.createElement(
'div',
{ 'data-resource-section-scroll': 'art' },
{ 'data-resource-section-scroll': 'scene' },
React.createElement(
'div',
{
'data-resource-section-plane': 'art',
'data-resource-section-plane': 'scene',
'data-resource-section-scale': '1',
},
React.createElement(ResourceDependencyOverlay, {
@@ -594,7 +594,7 @@ describe('ResourceDependencyOverlay', () => {
position('source:one', 0, 0),
position('target:one', 268, 0),
],
section: 'art',
section: 'scene',
visibleResourceIds: new Set(['source:one', 'target:one']),
cardSizeByResourceId,
}),
@@ -642,12 +642,12 @@ describe('ResourceDependencyOverlay', () => {
if (this.classList.contains('test-resource-content')) {
return rect(0, 0, 600, 600);
}
if (this.dataset.resourceSectionScroll === 'art') {
if (this.dataset.resourceSectionScroll === 'scene') {
return rect(0, 100, 600, 240);
}
if (this.dataset.resourceSectionPlane === 'art') {
if (this.dataset.resourceSectionPlane === 'scene') {
const viewport = this.closest<HTMLElement>(
'[data-resource-section-scroll="art"]',
'[data-resource-section-scroll="scene"]',
);
return rect(0, 100 - (viewport?.scrollTop ?? 0), 600, 600);
}
@@ -663,11 +663,11 @@ describe('ResourceDependencyOverlay', () => {
},
React.createElement(
'div',
{ 'data-resource-section-scroll': 'art' },
{ 'data-resource-section-scroll': 'scene' },
React.createElement(
'div',
{
'data-resource-section-plane': 'art',
'data-resource-section-plane': 'scene',
'data-resource-section-scale': '1',
},
React.createElement(ResourceDependencyOverlay, {
@@ -676,7 +676,7 @@ describe('ResourceDependencyOverlay', () => {
position('source:one', 0, 0),
position('target:one', 0, 320),
],
section: 'art',
section: 'scene',
visibleResourceIds: new Set(['source:one', 'target:one']),
}),
),
@@ -698,10 +698,10 @@ describe('ResourceDependencyOverlay', () => {
expect(reference.getAttribute('data-offscreen-target')).toBe('true');
const viewport = rendered.container.querySelector<HTMLElement>(
'[data-resource-section-scroll="art"]',
'[data-resource-section-scroll="scene"]',
);
if (!viewport) {
throw new Error('missing art viewport');
throw new Error('missing scene viewport');
}
viewport.scrollTop = 80;
fireEvent.scroll(viewport);
@@ -733,7 +733,7 @@ describe('ResourceDependencyOverlay', () => {
const getBoundingClientRect = vi
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
.mockImplementation(function getPagedCanvasRect(this: HTMLElement) {
if (this.dataset.resourceSectionScroll === 'art') {
if (this.dataset.resourceSectionScroll === 'scene') {
return {
x: 0,
y: 0,
@@ -754,7 +754,7 @@ describe('ResourceDependencyOverlay', () => {
{ className: 'game-resource-canvas--dependency' },
React.createElement(
'div',
{ 'data-resource-section-scroll': 'art' },
{ 'data-resource-section-scroll': 'scene' },
React.createElement(
'div',
{ className: 'game-resource-page-world' },
@@ -764,7 +764,7 @@ describe('ResourceDependencyOverlay', () => {
position('source:one', 0, 0),
position('target:one', 0, 320),
],
section: 'art',
section: 'scene',
visibleResourceIds: new Set(['source:one', 'target:one']),
canvasViewport,
}),
@@ -774,7 +774,7 @@ describe('ResourceDependencyOverlay', () => {
try {
const rendered = render(view({ x: 0, y: 0, scale: 1 }));
const overlay = await screen.findByTestId(
'resource-dependency-overlay-art',
'resource-dependency-overlay-scene',
);
const selector = '[data-edge-kind="asset-reference"]';
await waitFor(() => {
@@ -334,13 +334,14 @@ async function submitChat(value: string) {
// 同一张资源卡上有两个按钮:选中资源与 V3 新增的 @ 引用按钮,二者的可访问名都包含
// 资源文件名,因此按文件名正则查询会同时命中两个元素。选中按钮的可访问名模板固定为
// `选中资源:<分类标签> <资源文件名>`分类标签内不含空格),这里按模板做精确匹配。
// `选中资源:<分类标签> <资源文件名>`分类标签可能自带空格(例如 `UI 交互`),
// 这里按模板做精确匹配。
function escapeRegExpLiteral(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function resourceSelectButtonName(label: string) {
return new RegExp(`^选中资源:\\S+ ${escapeRegExpLiteral(label)}$`);
return new RegExp(`^选中资源:.+ ${escapeRegExpLiteral(label)}$`);
}
function getResourceSelectButton(label: string) {
@@ -92,11 +92,13 @@ function ApprovedGddStartHarness() {
async function openResourceBookCategory(label: string) {
const categoryByLabel: Record<string, string> = {
: 'document',
: 'art',
: 'audio',
'UI 交互': 'ui-interaction',
: 'character',
: 'scene',
: 'audio',
: 'document',
: 'unclassified',
: 'version',
: 'code',
};
const category = categoryByLabel[label];
const outline = await screen.findByLabelText('资源栏目大纲');
@@ -320,7 +322,7 @@ export function registerClientHomeTests() {
fireEvent.click(screen.getByRole('button', { name: '同步最新 manifest' }));
await openResourceBookCategory('美术资源');
await openResourceBookCategory('UI 交互');
expect(await findResourceSelectButton('live-hero.png')).not.toBeNull();
await openResourceBookCategory('项目版本');
expect(
@@ -486,7 +488,7 @@ export function registerClientHomeTests() {
runtimeHarness.emitManifestInvalidated('art-asset-plan');
});
await openResourceBookCategory('美术资源');
await openResourceBookCategory('UI 交互');
expect(
await findResourceSelectButton('runtime-live-hero.png', {
timeout: 5_000,
@@ -662,7 +664,7 @@ export function registerClientHomeTests() {
fireEvent.click(screen.getByRole('button', { name: '项目组' }));
pickProjectFromLauncher(secondProjectPath);
await openResourceBookCategory('美术资源');
await openResourceBookCategory('UI 交互');
expect(await findResourceSelectButton('second.png')).not.toBeNull();
await act(async () => {
File diff suppressed because it is too large Load Diff
@@ -490,7 +490,7 @@ describe('project resource live canvas integration', () => {
const { deriveCalls } = installTauri({ failFirstDerive: true });
render(<DerivedWorkbench includeArt />);
fireEvent.click(screen.getByRole('button', { name: '打开美术资源' }));
fireEvent.click(screen.getByRole('button', { name: '打开待归类' }));
fireEvent.click(await findResourceSelectButton('source-art.png'));
const toolbar = await screen.findByRole('toolbar', {
name: '图片工具栏',
@@ -566,7 +566,7 @@ describe('project resource live canvas integration', () => {
const { deriveCalls } = installTauri();
render(<DerivedWorkbench includeArt />);
fireEvent.click(screen.getByRole('button', { name: '打开美术资源' }));
fireEvent.click(screen.getByRole('button', { name: '打开待归类' }));
fireEvent.click(await findResourceSelectButton('source-art.png'));
const toolbar = await screen.findByRole('toolbar', {
name: '图片工具栏',
@@ -114,26 +114,28 @@ describe('项目资源投影', () => {
expect(resources.map(({ id, category }) => ({ id, category }))).toEqual(
expect.arrayContaining([
// 非 manifest 资源没有功能分类事实源,统一落在「待归类」。
{
id: 'task:design-foundation:memory/game-design.md',
category: 'document',
category: 'unclassified',
},
{ id: 'asset:game-entry', category: 'code' },
// kind `game-entry` 不在 canonical 目录里,按 canonical 兜底 `image` 归「待归类」。
{ id: 'asset:game-entry', category: 'unclassified' },
{
id: 'task:design-foundation:assets/ui-preview.svg',
category: 'art',
category: 'unclassified',
},
{ id: 'asset:registered-bgm', category: 'audio' },
{ id: 'asset:character-animation', category: 'art' },
{ id: 'attachment:imports/rules.yaml', category: 'document' },
{ id: 'attachment:imports/voice.ogg', category: 'audio' },
{ id: 'asset:registered-bgm', category: 'unclassified' },
{ id: 'asset:character-animation', category: 'character' },
{ id: 'attachment:imports/rules.yaml', category: 'unclassified' },
{ id: 'attachment:imports/voice.ogg', category: 'unclassified' },
{
id: 'agent-result:design-foundation:final-run',
category: 'document',
},
{
id: 'task:audio-asset-plan:audio/unregistered-bgm.wav',
category: 'audio',
category: 'unclassified',
},
{ id: 'version:version-1', category: 'version' },
]),
@@ -293,7 +295,6 @@ describe('项目资源投影', () => {
it('按稳定优先级推导资源卡类型标识并为未知类型兜底', () => {
expect(
projectResourceTypeLabel({
category: 'version',
subtype: 'project-version',
path: 'versions/v1',
mediaType: '正式项目版本',
@@ -301,7 +302,6 @@ describe('项目资源投影', () => {
).toBe('项目版本');
expect(
projectResourceTypeLabel({
category: 'document',
subtype: 'agent-result',
path: '专业 Agent 文本回执',
mediaType: 'Agent 历史文本回执',
@@ -309,7 +309,6 @@ describe('项目资源投影', () => {
).toBe('Agent 回执');
expect(
projectResourceTypeLabel({
category: 'document',
subtype: 'task-artifact',
path: 'memory/plan.md',
mediaType: '项目文档',
@@ -317,7 +316,6 @@ describe('项目资源投影', () => {
).toBe('文档');
expect(
projectResourceTypeLabel({
category: 'audio',
subtype: 'task-artifact',
path: 'audio/theme.wav',
mediaType: '音乐音效产物',
@@ -325,23 +323,22 @@ describe('项目资源投影', () => {
).toBe('音频');
expect(
projectResourceTypeLabel({
category: 'art',
subtype: 'task-artifact',
path: 'assets/hero.svg',
mediaType: '美术产物',
}),
).toBe('SVG');
// 任务产物准入时扩展名已经判定过类型;无法判定的产物不会进入画布,
// 这里仍给出稳定的兜底文案而不是按旧栏目硬猜类型。
expect(
projectResourceTypeLabel({
category: 'art',
subtype: 'task-artifact',
path: 'assets/unknown',
mediaType: '美术产物',
}),
).toBe('图片');
).toBe('任务产物');
expect(
projectResourceTypeLabel({
category: 'art',
subtype: 'character',
path: 'assets/hero.svg',
mediaType: 'image/svg+xml',
@@ -349,7 +346,6 @@ describe('项目资源投影', () => {
).toBe('SVG');
expect(
projectResourceTypeLabel({
category: 'art',
subtype: 'video',
path: 'assets/intro.mp4',
mediaType: 'video/mp4',
@@ -357,7 +353,6 @@ describe('项目资源投影', () => {
).toBe('视频');
expect(
projectResourceTypeLabel({
category: 'art',
subtype: 'character',
path: 'assets/hero.png',
mediaType: 'image/png',
@@ -365,7 +360,6 @@ describe('项目资源投影', () => {
).toBe('图片');
expect(
projectResourceTypeLabel({
category: 'document',
subtype: 'attachment',
path: 'docs/rules.yaml',
mediaType: 'application/yaml',
@@ -373,15 +367,13 @@ describe('项目资源投影', () => {
).toBe('文档');
expect(
projectResourceTypeLabel({
category: 'document',
subtype: 'unknown',
subtype: 'attachment',
path: 'data/blob',
mediaType: 'application/octet-stream',
}),
).toBe('文档');
).toBe('未知');
expect(
projectResourceTypeLabel({
category: 'code',
subtype: 'source',
path: 'game/main.ts',
mediaType: 'text/typescript',
@@ -389,19 +381,10 @@ describe('项目资源投影', () => {
).toBe('游戏代码');
expect(
projectResourceTypeLabel({
category: 'art',
subtype: 'unknown',
path: 'assets/blob',
mediaType: 'application/octet-stream',
}),
).toBe('图片');
expect(
projectResourceTypeLabel({
category: 'unsupported' as never,
subtype: 'unknown',
path: 'data/blob',
mediaType: 'application/octet-stream',
}),
).toBe('未知');
});
});
@@ -13,7 +13,7 @@ import type { ProjectResource } from '../src/view/project-development/resourcePr
function resource(id: string): ProjectResource {
return {
id,
category: 'art',
category: 'unclassified',
subtype: 'image',
label: id,
path: `assets/${id}.png`,
@@ -133,13 +133,13 @@ describe('buildResourceBookScenePlan', () => {
resources.map((item) => [item.id, { width: 180, height: 128 }]),
);
const base = {
visibleCategoryOrder: ['art'] as const,
resourcesByCategory: new Map([['art', resources]]),
overviewRects: new Map([['art', rect]]),
visibleCategoryOrder: ['unclassified'] as const,
resourcesByCategory: new Map([['unclassified', resources]]),
overviewRects: new Map([['unclassified', rect]]),
positions,
cardSizes,
visibleResourceIds: new Set(resources.map((item) => item.id)),
categoryViewCenters: new Map([['art', { x: 0, y: 64 }]]),
categoryViewCenters: new Map([['unclassified', { x: 0, y: 64 }]]),
cardsReady: true,
};
@@ -174,29 +174,36 @@ describe('buildResourceBookScenePlan', () => {
it('keeps other categories mounted for a fade-out while entering', () => {
const plan = buildResourceBookScenePlan({
...base,
visibleCategoryOrder: ['art', 'code'],
visibleCategoryOrder: ['unclassified', 'scene'],
resourcesByCategory: new Map([
['art', resources],
['code', [resource('code-0')]],
['unclassified', resources],
['scene', [resource('code-0')]],
]),
overviewRects: new Map([
['art', rect],
['code', { ...rect, left: 500 }],
['unclassified', rect],
['scene', { ...rect, left: 500 }],
]),
state: { view: 'child', category: 'art', phase: 'entering', token: 1 },
state: {
view: 'child',
category: 'unclassified',
phase: 'entering',
token: 1,
},
});
const art = plan.find((group) => group.category === 'art')!;
const code = plan.find((group) => group.category === 'code')!;
expect(art.cards.every((card) => card.presentation === 'child')).toBe(true);
expect(art.cards).toHaveLength(4);
expect(art.titlebarActive).toBe(true);
// 其他栏目留在 DOM 里淡出,而不是直接卸载。
expect(code.cards.every((card) => card.presentation === 'overview')).toBe(
const artGroup = plan.find((group) => group.category === 'unclassified')!;
const codeGroup = plan.find((group) => group.category === 'scene')!;
expect(artGroup.cards.every((card) => card.presentation === 'child')).toBe(
true,
);
expect(code.titlebarActive).toBe(false);
expect(code.titlebarOpacity).toBe(0);
expect(artGroup.cards).toHaveLength(4);
expect(artGroup.titlebarActive).toBe(true);
// 其他栏目留在 DOM 里淡出,而不是直接卸载。
expect(
codeGroup.cards.every((card) => card.presentation === 'overview'),
).toBe(true);
expect(codeGroup.titlebarActive).toBe(false);
expect(codeGroup.titlebarOpacity).toBe(0);
});
it('marks cards that do not return to the stack as exiting', () => {
@@ -208,7 +215,7 @@ describe('buildResourceBookScenePlan', () => {
phase: 'returning-main',
token: 2,
},
exitingCategory: 'art',
exitingCategory: 'unclassified',
});
const cards = plan[0]!.cards;
@@ -228,7 +235,7 @@ describe('buildResourceBookScenePlan', () => {
exitingCards: [
{
key: resourceBookSceneCardKey('art-3'),
category: 'art',
category: 'unclassified',
resource: resources[3]!,
presentation: 'exiting',
stackIndex: 0,
@@ -246,7 +253,7 @@ describe('buildResourceBookScenePlan', () => {
},
{
key: resourceBookSceneCardKey('art-0'),
category: 'art',
category: 'unclassified',
resource: resources[0]!,
presentation: 'exiting',
stackIndex: 0,
@@ -37,15 +37,15 @@ describe('resource book state machine', () => {
});
const switching = resourceBookReducer(current, {
type: 'open-category',
category: 'art',
category: 'scene',
token: 2,
});
expect(switching).toMatchObject({
view: 'child',
category: 'art',
category: 'scene',
phase: 'entering',
});
expect(resourceBookSceneCategory(switching)).toBe('art');
expect(resourceBookSceneCategory(switching)).toBe('scene');
});
it('marks a return to the main canvas separately from a child enter transition', () => {
@@ -73,7 +73,7 @@ describe('resource book state machine', () => {
});
const switching = resourceBookReducer(entering, {
type: 'open-category',
category: 'art',
category: 'scene',
token: 2,
});
const main = resourceBookReducer(switching, {
@@ -97,7 +97,7 @@ describe('resource book state machine', () => {
});
const switched = resourceBookReducer(entering, {
type: 'open-category',
category: 'art',
category: 'scene',
token: 2,
});
expect(
@@ -109,18 +109,20 @@ describe('resource book state machine', () => {
describe('resource book card presentation', () => {
it('uses the overview stack on the steady main canvas', () => {
expect(
resourceBookCategoryCardPresentation(initialResourceBookState, 'art'),
resourceBookCategoryCardPresentation(initialResourceBookState, 'scene'),
).toBe('overview');
});
it('keeps other categories mounted while entering so they can fade out', () => {
const entering = resourceBookReducer(initialResourceBookState, {
type: 'open-category',
category: 'art',
category: 'scene',
token: 1,
});
expect(resourceBookCategoryCardPresentation(entering, 'art')).toBe('child');
expect(resourceBookCategoryCardPresentation(entering, 'scene')).toBe(
'child',
);
expect(resourceBookCategoryCardPresentation(entering, 'code')).toBe(
'overview',
);
@@ -130,13 +132,13 @@ describe('resource book card presentation', () => {
const steady = resourceBookReducer(
resourceBookReducer(initialResourceBookState, {
type: 'open-category',
category: 'art',
category: 'scene',
token: 1,
}),
{ type: 'finish-transition', token: 1 },
);
expect(resourceBookCategoryCardPresentation(steady, 'art')).toBe('child');
expect(resourceBookCategoryCardPresentation(steady, 'scene')).toBe('child');
expect(resourceBookCategoryCardPresentation(steady, 'code')).toBeNull();
});
@@ -144,13 +146,13 @@ describe('resource book card presentation', () => {
const returning = resourceBookReducer(
resourceBookReducer(initialResourceBookState, {
type: 'open-category',
category: 'art',
category: 'scene',
token: 1,
}),
{ type: 'return-to-main', token: 2 },
);
expect(resourceBookCategoryCardPresentation(returning, 'art')).toBe(
expect(resourceBookCategoryCardPresentation(returning, 'scene')).toBe(
'overview',
);
expect(resourceBookCategoryCardPresentation(returning, 'code')).toBe(
@@ -0,0 +1,125 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, test, vi } from 'vitest';
import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
import {
cleanup,
createGameCreationAppManifest,
createGameCreationAppSeedTasks,
ProjectDevelopmentView,
React,
render,
screen,
within,
} from './appSurface/harness';
const PROJECT_PATH = '/tmp/workbench-code-only-project';
function installResourceCardIntersectionObserver() {
class ResourceCardIntersectionObserver {
readonly root = null;
readonly rootMargin = '160px';
readonly thresholds = [0];
readonly observed = new Set<Element>();
constructor(readonly callback: IntersectionObserverCallback) {}
observe(element: Element) {
this.observed.add(element);
}
unobserve(element: Element) {
this.observed.delete(element);
}
disconnect() {
this.observed.clear();
}
takeRecords() {
return [];
}
}
Object.defineProperty(window, 'IntersectionObserver', {
configurable: true,
value: ResourceCardIntersectionObserver,
});
}
/**
* 只登记游戏代码的项目:分区轴上没有代码栏目,代码资产(manifest 资产与已完成任务登记
* 的产物)按资产功能分类落入「待归类」。旧四栏目口径会把它们整体排除在可见栏目之外,
* 于是资源签名非空、画布已进入分页态,却四栏全空、一张卡都不显示。
*/
function createCodeOnlyManifest(): GameCreationAppManifest {
const manifest = createGameCreationAppManifest(
'workbench-code-only',
'只登记游戏代码的项目',
);
manifest.assets = [
{
id: 'asset-game-entry',
kind: 'code',
mediaType: 'text/html',
localPath: 'game/index.html',
source: { kind: 'generated' },
},
];
manifest.tasks = createGameCreationAppSeedTasks().map((task) =>
task.id === 'code-prototype'
? { ...task, status: 'completed' as const, artifacts: ['game/main.js'] }
: task,
);
return manifest;
}
function renderWorkbench(manifest: GameCreationAppManifest) {
return render(
React.createElement(ProjectDevelopmentView, {
projectName: manifest.name,
projectPath: PROJECT_PATH,
manifest,
attachments: [],
recentRunStatus: null,
recentRunStopReason: null,
supervisor: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
onPlay: vi.fn(),
}),
);
}
describe('只登记游戏代码的项目分区', () => {
afterEach(() => {
cleanup();
});
test('代码资产落在待归类栏目并渲染出卡片,而不是四栏全空', async () => {
installResourceCardIntersectionObserver();
renderWorkbench(createCodeOnlyManifest());
const outline = await screen.findByLabelText('资源栏目大纲');
expect(
within(outline).getByRole('button', { name: /待归类/ }),
).not.toBeNull();
// 项目版本仍然是末尾独立栏目,代码栏目不再存在。
expect(
within(outline).getByRole('button', { name: /项目版本/ }),
).not.toBeNull();
// 默认停留在顺序里第一个非空栏目,也就是代码资产所在的「待归类」。
expect(
await screen.findByRole('region', { name: '待归类' }),
).not.toBeNull();
// manifest 资产与已完成任务的代码产物都必须真的渲染成卡片。
await screen.findByRole('button', {
name: /选中资源:待归类 index\.html/,
});
await screen.findByRole('button', {
name: /选中资源:待归类 main\.js/,
});
});
});
@@ -4,7 +4,7 @@ import { createResourceCanvasPageCategorySignature } from '../src/view/project-d
describe('resource canvas page wheel controller', () => {
it('invalidates page categories for same-count category changes without reacting to resource order', () => {
const original = [{ category: 'document' }, { category: 'art' }];
const original = [{ category: 'document' }, { category: 'scene' }];
const reordered = [...original].reverse();
const changed = [{ category: 'document' }, { category: 'audio' }];
@@ -19,6 +19,7 @@ import {
RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP,
RESOURCE_CANVAS_DEPENDENCY_ROW_GAP,
RESOURCE_CANVAS_INITIAL_FIT_MAX_SCALE,
RESOURCE_CANVAS_SECTION_ORDER,
type ResourceCanvasCardSize,
resourceCanvasContentBounds,
resourceCanvasImageCardSize,
@@ -38,7 +39,7 @@ function resource(
subtype: 'default',
dependencyDepth,
label: id,
mediaType: category === 'art' ? 'image/png' : 'text/markdown',
mediaType: category === 'scene' ? 'image/png' : 'text/markdown',
};
}
@@ -101,9 +102,9 @@ describe('resource canvas layout model', () => {
it('creates non-overlapping defaults and keeps the two modes independent', () => {
const resources = [
resource('a', 'art', 0),
resource('b', 'art', 0),
resource('c', 'art', 1),
resource('a', 'scene', 0),
resource('b', 'scene', 0),
resource('c', 'scene', 1),
];
const dependency = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-1', 'dependency'),
@@ -219,12 +220,12 @@ describe('resource canvas layout model', () => {
},
];
const reconciled = reconcileResourceCanvasLayout(layout, [
resource('asset-a', 'art'),
resource('asset-a', 'scene'),
]);
expect(reconciled.layout.positions).toEqual([
{
resourceId: 'asset-a',
section: 'art',
section: 'scene',
x: 0,
y: 0,
manuallyPlaced: false,
@@ -232,24 +233,111 @@ describe('resource canvas layout model', () => {
]);
});
it('栏目顺序固定为 6 类资产分类加末尾的项目版本栏目', () => {
expect(RESOURCE_CANVAS_SECTION_ORDER).toEqual([
'ui-interaction',
'character',
'scene',
'audio',
'document',
'unclassified',
'version',
]);
});
it('读时归并旧栏目 sidecar:坐标与手动标记原样保留、section 改写并判定为变化', () => {
const source = createEmptyResourceCanvasLayout('project-legacy', 'type');
source.positions = [
{
resourceId: 'legacy-art',
section: 'art',
x: -32,
y: 64,
manuallyPlaced: true,
},
{
resourceId: 'legacy-code',
section: 'code',
x: 180,
y: 0,
manuallyPlaced: false,
},
{
resourceId: 'legacy-version',
section: 'version',
x: 12,
y: 34,
manuallyPlaced: true,
},
];
const reconciled = reconcileResourceCanvasLayout(source, [
{ ...resource('legacy-art', 'ui-interaction'), subtype: 'icon' },
{ ...resource('legacy-code', 'unclassified'), subtype: 'code' },
{ ...resource('legacy-version', 'version'), subtype: 'project-version' },
]);
// 改写后的布局与 sidecar 逐项不同:既有协调路径据此写回一次新分区。
expect(reconciled.changed).toBe(true);
expect(reconciled.layout.positions).toEqual([
{
resourceId: 'legacy-art',
section: 'ui-interaction',
x: -32,
y: 64,
manuallyPlaced: true,
},
{
resourceId: 'legacy-code',
section: 'unclassified',
x: 180,
y: 0,
manuallyPlaced: false,
},
{
resourceId: 'legacy-version',
section: 'version',
x: 12,
y: 34,
manuallyPlaced: true,
},
]);
});
it('只登记游戏代码的项目落在待归类栏目而不是被分区轴排除', () => {
const layout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-code-only', 'type'),
[
resource('asset:game-entry', 'unclassified'),
resource('task:code-prototype:game/main.js', 'unclassified'),
],
).layout;
expect(RESOURCE_CANVAS_SECTION_ORDER).toContain('unclassified');
expect(layout.positions).toHaveLength(2);
expect(
new Set(layout.positions.map((position) => position.section)),
).toEqual(new Set(['unclassified']));
});
it('sorts type defaults by subtype before media type and label with an id fallback', () => {
const typeLayout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-type-order', 'type'),
[
{
...resource('ui-prototype', 'art'),
...resource('ui-prototype', 'scene'),
subtype: 'ui-prototype',
mediaType: 'image/png',
label: '甲界面',
},
{
...resource('art-spritesheet-b', 'art'),
...resource('art-spritesheet-b', 'scene'),
subtype: 'art-spritesheet',
mediaType: 'image/png',
label: '乙图集',
},
{
...resource('art-spritesheet-a', 'art'),
...resource('art-spritesheet-a', 'scene'),
subtype: 'art-spritesheet',
mediaType: 'image/png',
label: '乙图集',
@@ -282,11 +370,11 @@ describe('resource canvas layout model', () => {
const layout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-clusters', 'dependency'),
[
resource('art:canvas', 'art', 0),
resource('art:wireframe', 'art', 1),
resource('art:character', 'art', 0),
resource('art:screen', 'art', 1),
resource('art:unrelated', 'art', 0),
resource('art:canvas', 'scene', 0),
resource('art:wireframe', 'scene', 1),
resource('art:character', 'scene', 0),
resource('art:screen', 'scene', 1),
resource('art:unrelated', 'scene', 0),
],
dependencyTopology([
{
@@ -321,8 +409,8 @@ describe('resource canvas layout model', () => {
'dependency',
),
[
resource('art:a', 'art', 0),
resource('art:b', 'art', 0),
resource('art:a', 'scene', 0),
resource('art:b', 'scene', 0),
resource('document:a', 'document', 0),
],
dependencyTopology(
@@ -355,10 +443,10 @@ describe('resource canvas layout model', () => {
const layout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-crossings', 'dependency'),
[
resource('source:bottom', 'art', 0),
resource('source:top', 'art', 0),
resource('target:first', 'art', 1),
resource('target:second', 'art', 1),
resource('source:bottom', 'scene', 0),
resource('source:top', 'scene', 0),
resource('target:first', 'scene', 1),
resource('target:second', 'scene', 1),
],
dependencyTopology(
[
@@ -392,10 +480,10 @@ describe('resource canvas layout model', () => {
const layout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-balanced-diamond', 'dependency'),
[
resource('source', 'art', 0),
resource('middle:upper', 'art', 1),
resource('middle:lower', 'art', 1),
resource('target', 'art', 2),
resource('source', 'scene', 0),
resource('middle:upper', 'scene', 1),
resource('middle:lower', 'scene', 1),
resource('target', 'scene', 2),
],
dependencyTopology([
{ sourceResourceId: 'source', targetResourceId: 'middle:upper' },
@@ -431,10 +519,10 @@ describe('resource canvas layout model', () => {
const layout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-cycles', 'dependency'),
[
resource('art:cycle-a', 'art', 2),
resource('art:cycle-b', 'art', 2),
resource('art:after-cycle', 'art', 3),
resource('art:self', 'art', 0),
resource('art:cycle-a', 'scene', 2),
resource('art:cycle-b', 'scene', 2),
resource('art:after-cycle', 'scene', 3),
resource('art:self', 'scene', 0),
resource('document:brief', 'document', 0),
],
dependencyTopology([
@@ -472,10 +560,10 @@ describe('resource canvas layout model', () => {
const layout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-cycle-contiguity', 'dependency'),
[
resource('art:cycle-a', 'art', 2),
resource('art:cycle-b', 'art', 2),
resource('art:sibling', 'art', 2),
resource('art:after', 'art', 3),
resource('art:cycle-a', 'scene', 2),
resource('art:cycle-b', 'scene', 2),
resource('art:sibling', 'scene', 2),
resource('art:after', 'scene', 3),
],
dependencyTopology([
{ sourceResourceId: 'art:cycle-a', targetResourceId: 'art:cycle-b' },
@@ -497,9 +585,9 @@ describe('resource canvas layout model', () => {
it('is deterministic, does not modify type placement, and avoids historical manual positions', () => {
const resources = [
resource('art:source', 'art', 0),
resource('art:target', 'art', 1),
resource('art:other', 'art', 1),
resource('art:source', 'scene', 0),
resource('art:target', 'scene', 1),
resource('art:other', 'scene', 1),
];
const topology = dependencyTopology([
{ sourceResourceId: 'art:source', targetResourceId: 'art:target' },
@@ -511,7 +599,7 @@ describe('resource canvas layout model', () => {
source.positions = [
{
resourceId: 'art:other',
section: 'art',
section: 'scene',
x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP,
y: 0,
manuallyPlaced: true,
@@ -547,10 +635,10 @@ describe('resource canvas layout model', () => {
it('saturates super-deep dependency columns and avoids collisions within the coordinate contract', () => {
const resources = [
resource('art:depth-4385', 'art', 4_385),
resource('art:depth-4386-a', 'art', 4_386),
resource('art:depth-4386-b', 'art', 4_386),
resource('art:depth-9000', 'art', 9_000),
resource('art:depth-4385', 'scene', 4_385),
resource('art:depth-4386-a', 'scene', 4_386),
resource('art:depth-4386-b', 'scene', 4_386),
resource('art:depth-9000', 'scene', 9_000),
];
const layout = reconcileResourceCanvasLayout(
createEmptyResourceCanvasLayout('project-deep-dependency', 'dependency'),
@@ -616,7 +704,7 @@ describe('resource canvas layout model', () => {
'reconciles 4096 resources in %s mode within the bounded layout budget',
(mode) => {
const resources = Array.from({ length: 4096 }, (_, index) => ({
...resource(`resource-${index.toString().padStart(4, '0')}`, 'art'),
...resource(`resource-${index.toString().padStart(4, '0')}`, 'scene'),
dependencyDepth: index % 64,
mediaType: `image/type-${index % 16}`,
}));
@@ -642,7 +730,7 @@ describe('resource canvas layout model', () => {
const resources = Array.from({ length: 4096 }, (_, index) =>
resource(
`resource-${index.toString().padStart(4, '0')}`,
'art',
'scene',
index % 64,
),
);
@@ -687,7 +775,7 @@ describe('resource canvas variable card geometry', () => {
it('keeps non-images and images without metadata at the fixed fallback size', () => {
const sizes = createResourceCanvasCardSizeByResourceId(
[resource('image', 'art'), resource('document', 'document')],
[resource('image', 'scene'), resource('document', 'document')],
new Map([
['image', { pixelWidth: 1920, pixelHeight: 1080 }],
['document', { pixelWidth: 1920, pixelHeight: 1080 }],
@@ -698,14 +786,17 @@ describe('resource canvas variable card geometry', () => {
expect(sizes.get('document')).toEqual(DEFAULT_RESOURCE_CANVAS_CARD_SIZE);
expect(
createResourceCanvasCardSizeByResourceId(
[resource('pending-image', 'art')],
[resource('pending-image', 'scene')],
new Map(),
).get('pending-image'),
).toEqual(DEFAULT_RESOURCE_CANVAS_CARD_SIZE);
});
it('uses actual widths and heights for type placement collision checks', () => {
const resources = [resource('wide-a', 'art'), resource('wide-b', 'art')];
const resources = [
resource('wide-a', 'scene'),
resource('wide-b', 'scene'),
];
const cardSizes = new Map<string, ResourceCanvasCardSize>([
['wide-a', { width: 220, height: 180 }],
['wide-b', { width: 220, height: 180 }],
@@ -726,9 +817,9 @@ describe('resource canvas variable card geometry', () => {
it('uses actual dimensions for dependency columns, rows, and section extent', () => {
const resources = [
resource('source-a', 'art', 0),
resource('source-b', 'art', 0),
resource('target', 'art', 1),
resource('source-a', 'scene', 0),
resource('source-b', 'scene', 0),
resource('target', 'scene', 1),
];
const cardSizes = new Map<string, ResourceCanvasCardSize>([
['source-a', { width: 220, height: 180 }],
@@ -763,14 +854,14 @@ describe('resource canvas variable card geometry', () => {
const positions: ProjectResourceCanvasPosition[] = [
{
resourceId: 'negative-card',
section: 'art',
section: 'scene',
x: -240,
y: -160,
manuallyPlaced: true,
},
{
resourceId: 'positive-card',
section: 'art',
section: 'scene',
x: 480,
y: 320,
manuallyPlaced: true,
@@ -796,14 +887,14 @@ describe('resource canvas variable card geometry', () => {
const positions: ProjectResourceCanvasPosition[] = [
{
resourceId: 'negative-card',
section: 'art',
section: 'scene',
x: -240,
y: -160,
manuallyPlaced: true,
},
{
resourceId: 'positive-card',
section: 'art',
section: 'scene',
x: 480,
y: 320,
manuallyPlaced: true,
@@ -832,7 +923,15 @@ describe('resource canvas variable card geometry', () => {
expect(viewport.x + bounds.x * viewport.scale).toBeCloseTo(16, 8);
});
it.each(['document', 'art', 'audio', 'code', 'version'])(
it.each([
'ui-interaction',
'character',
'scene',
'audio',
'document',
'unclassified',
'version',
])(
'caps the initial fit for every resource category (%s) while keeping explicit zoom overrides available',
() => {
const viewport = fitResourceCanvasViewportToContent({
@@ -852,11 +951,11 @@ describe('resource canvas variable card geometry', () => {
it('accepts card sizes directly on resource items for hook integration', () => {
const wide = {
...resource('wide', 'art', 0),
...resource('wide', 'scene', 0),
cardSize: { width: 220, height: 180 },
};
const target = {
...resource('target', 'art', 1),
...resource('target', 'scene', 1),
cardSize: { width: 180, height: 128 },
};
const layout = reconcileResourceCanvasLayout(
@@ -12,7 +12,7 @@ function createResource(
): ProjectResource {
return {
id: 'asset:hero',
category: 'art',
category: 'ui-interaction',
subtype: 'icon',
label: '主角立绘',
path: 'assets/hero.png',
@@ -38,7 +38,7 @@ describe('全类型资源编辑能力模型', () => {
{
resource: resource({
id: 'asset:png',
category: 'art',
category: 'unclassified',
path: 'assets/hero.png',
mediaType: 'image/png',
manifestAssetId: 'png',
@@ -49,7 +49,7 @@ describe('全类型资源编辑能力模型', () => {
{
resource: resource({
id: 'task:art:hero.webp',
category: 'art',
category: 'unclassified',
path: 'hero.webp',
mediaType: '美术产物',
}),
@@ -59,7 +59,7 @@ describe('全类型资源编辑能力模型', () => {
{
resource: resource({
id: 'asset:gif',
category: 'art',
category: 'unclassified',
path: 'assets/hero.gif',
mediaType: 'image/gif',
manifestAssetId: 'gif',
@@ -70,7 +70,7 @@ describe('全类型资源编辑能力模型', () => {
{
resource: resource({
id: 'asset:svg',
category: 'art',
category: 'unclassified',
path: 'assets/icon.svg',
mediaType: 'image/svg+xml',
manifestAssetId: 'svg',
@@ -81,7 +81,7 @@ describe('全类型资源编辑能力模型', () => {
{
resource: resource({
id: 'task:art:intro.mp4',
category: 'art',
category: 'unclassified',
path: 'assets/intro.mp4',
mediaType: '美术产物',
}),
@@ -171,7 +171,7 @@ describe('全类型资源编辑能力模型', () => {
canonicalProjectedResourceMediaType(
resource({
id: 'task:art:clip.mov',
category: 'art',
category: 'unclassified',
path: 'assets/clip.mov',
mediaType: '美术产物',
}),
@@ -211,7 +211,7 @@ describe('全类型资源编辑能力模型', () => {
const capability = resolveProjectCharacterAnimationCapability(
resource({
id: 'asset:hero',
category: 'art',
category: 'unclassified',
path: 'assets/hero.png',
mediaType: 'image/png',
manifestAssetId: 'hero',

Some files were not shown because too many files have changed in this diff Show More