d1581892d7
- 素材重命名(跨范围契约):`rename_local_project_asset` 现在要求 `expectedProjectId` + `expectedProjectRevision`,前端按删除 / 分类保存同一套 CAS 口径补齐——先 `get_local_game_project_revision` 读当前 revision 再连同项目身份提交,不用可能过期的本地缓存值 - 失败文案统一:新增 `projectAssetCommandErrorMessage`,把 `project-identity-conflict` / `project-revision-conflict` 翻成用户可读中文,重命名 / 删除 / 分类保存三条链路共用一份映射,其余错误原样透出 - index.tsx:资源面板下载按 `resolveDownloadablePanelEntries` 过滤,版本卡这类合成 path 的条目不再进入落盘链路 - index.tsx:`characterAnimationPanel` 纳入画布浮层开关判据(抽 `resolveResourceCanvasFloatingPanelOpen`),「生成动画」面板与快速编辑 / 信息浮层共用同一条「点外部 / Esc 关闭」规则 - index.tsx:去掉 `ResourceBookScene` 上重复的 `onWheel`,滚轮只由 manager 上的原生 `passive:false` 监听处理,平移 / 缩放不再被翻倍 - index.tsx:快速编辑的源资源在层 id 命中不到时回落到已正规化的 `asset:<id>`,重试不再静默什么都不做;取不到时给出可见失败 - ResourceCanvasPanelView.tsx:下载按钮按可下载条数判可用、不再在上传中显示下载转圈;上传中禁掉 × / Esc / 遮罩三条关闭路径;预览占位文案抽 helper - GameRunVersionPicker.tsx:portal 菜单在滚动 / 缩放后重算位置;外部点击判定沿用共享 `useImageCanvasFloatingOptionDismiss` + `menuRef` 边界(review 该条已修,保留现状并补位置用例) - ResourceCanvasGenerationPanelView.tsx:`submitting` 改 `finally` 收回,成功路径不再永久锁住面板 - ResourceAssetDeleteDialog.tsx / ResourceRenameDialog.tsx / ResourceClassificationPanel.tsx:在飞时 `closeOnEscape` / `closeOnBackdrop` 与头部 × 一起挡住 - resourceEditModel.ts / resourceCanvasToolbarModel.ts / useProjectResourceCardPreviews.ts / projectResourceLiveUpdateModel.ts:抽 `resourceBaseName`、`canonicalProjectedResourceMediaType` 参数收窄去掉强转、删未使用参数、补「IO 失败会 reject」的签名说明 - resourceCanvasHistoryModel.ts:`resourceCanvasSnapshotsEqual` 不再比较不可恢复的 `manuallyPlaced`,消除「压进历史但撤销是空操作」 - resourceCanvasSectionMapping.ts:删掉从未接线、且注释与实现相矛盾的 `LEGACY_RESOURCE_CANVAS_SECTION_FALLBACK` - resourceCanvasChrome.css:`.game-resource-panel-grid` 补 `flex: 1 1 auto; min-height: 0`,卡片网格成为真正的滚动区 - 用例:重命名 CAS 载荷与读序、CAS 拒绝文案、版本卡下载门禁、上传中关闭路径、浮层判据、菜单跟随滚动、分区映射
118 lines
4.4 KiB
TypeScript
118 lines
4.4 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'],
|
||
};
|
||
|
||
/**
|
||
* 把磁盘上的 `section` 值归一到现行分区。
|
||
*
|
||
* 两条归并语义都由**资源当前分类**收口,因为"这张卡属于哪一栏"的唯一真源本来就是
|
||
* `resource.category`(协调时按它分组),持久化的 `section` 只是"它当初放在哪一栏"的记录:
|
||
*
|
||
* 1. **现行栏目值**:原样返回资源当前分类。旧实现里"现行值但不等于当前分类"直接判 null、
|
||
* 整条坐标被丢弃 —— 那等于把用户手摆的位置换成自动排布。资源分类会变(例如
|
||
* `kind:"UI"` 的资产从「待归类」被修到「UI 交互」),一改就静默丢掉落盘坐标,是用户可见的损失。
|
||
* 2. **旧栏目值**:落在该旧栏目的归并目标集合内就归并到资源当前分类,否则判 null。
|
||
*
|
||
* 两处都**只改写 `section`,x / y / manuallyPlaced 原样保留**;坐标是栏目内局部坐标
|
||
* (相邻的 `reconcileResourceCanvasLayout` 按 section 分组后再算栏目带原点),所以换栏目
|
||
* 保留 x / y 得到的正是"这张卡在新栏目里的同一个槽位"。
|
||
*
|
||
* 返回 `null` 只留给**真正无从归并**的取值:无法识别的分区值,以及旧栏目目标的集合
|
||
* 不含资源当前分类的情况(如旧 `code` 遇到已归为 `document` 的资源)。
|
||
*/
|
||
export function resolveResourceCanvasSection(
|
||
section: unknown,
|
||
resourceCategory: ProjectResourceCanvasCategory,
|
||
): ProjectResourceCanvasCategory | null {
|
||
if (isProjectResourceCanvasCategory(section)) {
|
||
return resourceCategory;
|
||
}
|
||
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` 表示资源不存在或该坐标确实无从归并,调用方不应把它放进任何分区;
|
||
* **分区取值与资源当前分类不一致不属于"无从归并"**,见 [`resolveResourceCanvasSection`]。
|
||
*/
|
||
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,
|
||
};
|
||
}
|