Files
Genarrative/apps/ai-game-creator-shell/src/view/project-development/resourceCanvasSectionMapping.ts
T
suzmii 6f09bf3a00 AGC 资源画布分区轴改为 6 类资产分类加独立项目版本栏目(阶段一:契约与读时映射)
- 契约层新增 PROJECT_RESOURCE_CANVAS_SECTIONS:6 类资产分类加末尾独立的项目版本栏目,并补齐分区类型
- 契约层新增旧四 / 五栏目取值白名单、Persisted 联合类型与两个类型守卫
- Rust 契约 ProjectResourceCanvasSection 增补 7 个现行 variant 并保留 Code / Art 旧值,新增同名分区常量
- Rust 契约不升 game-creator-resource-layout.v1 版本、不给未知 section 加兜底,损坏 payload 继续失败关闭
- 新增 resourceCanvasSectionMapping.ts:旧栏目到现行分区的映射表、回落栏目与读时归一化
- 读时归一化只改写 section,x / y / manuallyPlaced 原样保留,无法归并时沿用既有丢弃行为
- resourceProjectionModel 把扩展名分类器更名为 projectedResourceKind 并收敛为准入与显示类型
- resourceProjectionModel 新增 projectResourceCanvasCategory:项目版本独立成栏、Agent 回执归文档、其余按资产分类
- Rust resource_layout 测试夹具改用现行分区值,并新增既有旧 section sidecar 仍可读取的用例
- 新增 resourceCanvasSectionMapping 测试:旧值乘目标栏目矩阵覆盖坐标保留、section 改写、changed 判定与回落列不变量
- projectResourceProjectionModel 测试新增 7 栏分区投影、项目版本独立成栏与只登记游戏代码项目落在待归类
- 同步 PRD、GameAgent 资源自由画板技术方案、AGC 实施计划与共享记忆决策记录的分区口径
2026-09-10 19:57:34 +08:00

120 lines
3.9 KiB
TypeScript

import {
isLegacyProjectResourceCanvasSection,
isProjectResourceCanvasCategory,
type LegacyProjectResourceCanvasSection,
type PersistedProjectResourceCanvasSection,
type ProjectResourceCanvasCategory,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
/**
* 旧栏目值 → 现行分区集合。旧四 / 五栏目是「扩展名 + mediaType」口径,无法 1:1 归并到
* 6 类资产分类,因此按资源当前分区做保守归并:
*
* - `document` / `audio` / `version` 在新旧口径下同名,1:1 保留;
* - 旧 `art` 混合了 UI 交互、角色与对象、场景与环境与待归类,按资源当前分区落入其中之一;
* - 旧 `code` 在 6 类资产分类里没有对应栏目,统一落入「待归类」。
*/
export const LEGACY_RESOURCE_CANVAS_SECTION_TARGETS: Record<
LegacyProjectResourceCanvasSection,
readonly ProjectResourceCanvasCategory[]
> = {
document: ['document'],
audio: ['audio'],
version: ['version'],
art: ['ui-interaction', 'character', 'scene', 'unclassified'],
code: ['unclassified'],
};
/**
* 无法精确归并时回落到的栏目。当前分区不在目标集合内(例如资产后来被重新分类)时,
* 该坐标按今天「资源分区已变化」的既有行为丢弃,不做坐标迁移、不重置其余坐标。
*/
export const LEGACY_RESOURCE_CANVAS_SECTION_FALLBACK: Record<
LegacyProjectResourceCanvasSection,
ProjectResourceCanvasCategory
> = {
document: 'document',
audio: 'audio',
version: 'version',
art: 'unclassified',
code: 'unclassified',
};
/**
* 把磁盘上的 `section` 值归一到现行分区。
*
* 返回 `null` 表示该坐标既不属于资源的现行分区、也不在旧栏目白名单内(含无法识别的取值),
* 与今天「持久化坐标分区与资源分区不一致」的处理一致:丢弃该条坐标,不臆造新位置。
*/
export function resolveResourceCanvasSection(
section: unknown,
resourceCategory: ProjectResourceCanvasCategory,
): ProjectResourceCanvasCategory | null {
if (isProjectResourceCanvasCategory(section)) {
return section === resourceCategory ? section : null;
}
if (!isLegacyProjectResourceCanvasSection(section)) {
return null;
}
return LEGACY_RESOURCE_CANVAS_SECTION_TARGETS[section].includes(
resourceCategory,
)
? resourceCategory
: null;
}
export type NormalizedResourceCanvasPosition = {
resourceId: string;
section: ProjectResourceCanvasCategory;
x: number;
y: number;
manuallyPlaced: boolean;
};
export type NormalizedResourceCanvasPositionResult = {
position: NormalizedResourceCanvasPosition;
/**
* 该坐标的 `section` 是否被读时改写。改写后的布局与原布局逐项不同,
* 既有协调路径据此判定 `changed` 并写入一次新分区的 sidecar。
*/
changed: boolean;
};
/**
* 读时兼容映射:把一条持久化坐标归一到现行分区,原样保留 `x / y / manuallyPlaced`。
*
* 返回 `null` 表示资源不存在或该坐标无法归并,调用方不应把它放进任何分区。
*/
export function normalizeResourceCanvasPosition(
position: {
resourceId: string;
section: PersistedProjectResourceCanvasSection;
x: number;
y: number;
manuallyPlaced: boolean;
},
categoryByResourceId: ReadonlyMap<string, ProjectResourceCanvasCategory>,
): NormalizedResourceCanvasPositionResult | null {
const resourceCategory = categoryByResourceId.get(position.resourceId);
if (resourceCategory === undefined) {
return null;
}
const section = resolveResourceCanvasSection(
position.section,
resourceCategory,
);
if (section === null) {
return null;
}
return {
position: {
resourceId: position.resourceId,
section,
x: position.x,
y: position.y,
manuallyPlaced: position.manuallyPlaced,
},
changed: section !== position.section,
};
}