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:
+36
-15
@@ -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),
|
||||
|
||||
+9
-3
@@ -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;
|
||||
|
||||
+11
-4
@@ -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);
|
||||
|
||||
+23
-19
@@ -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 {
|
||||
|
||||
+14
-7
@@ -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)
|
||||
|
||||
+43
-30
@@ -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}`,
|
||||
|
||||
+2
-2
@@ -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}`;
|
||||
}
|
||||
|
||||
+3
-3
@@ -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,
|
||||
) => {
|
||||
|
||||
+17
-15
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
+4
-4
@@ -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
Reference in New Issue
Block a user