「所有资源」展开态改回平铺画布:不再为每个栏目渲染标题栏
展开态真实栏目不再渲染栏目标题栏,整个场景只留 all 那一条钉在视口上、带「资源总览」收起入口的标题栏 原因是分带只为有可见资源的栏目生成,空栏目拿不到标题栏矩形后全部回落到世界原点叠成一摞(空项目必现,现场是 7 条) ResourceBookSceneCategoryPlan 新增 titlebar 字段,渲染层按它决定是否产出标题栏节点 分带去掉标题栏横带的高度预留与矩形换算,删除 RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT 与 resourceBookAllTitlebarRects 拖动写回口径不变(resourceBookAllBandLocalPoint 一行未改),零 CSS 变更 resourceBookLayout.test 与 appSurface 两条「所有资源」用例加反向守卫;变异验证改回旧行为时两条同时红(got 7) decision-log 与 pitfalls 记这次倒因为「缺一路坐标输入时不要静默回落到原点」
This commit is contained in:
@@ -165,7 +165,6 @@ import {
|
||||
buildResourceBookScenePlan,
|
||||
resourceBookAllBandLocalPoint,
|
||||
type ResourceBookAllLayout,
|
||||
resourceBookAllTitlebarRects,
|
||||
type ResourceBookOverviewRect,
|
||||
resourceBookSceneCardKey,
|
||||
resourceBookSceneCardLayoutSignature,
|
||||
@@ -1176,26 +1175,32 @@ function ResourceBookScene({
|
||||
: undefined;
|
||||
return (
|
||||
<Fragment key={group.category}>
|
||||
<div
|
||||
className={`game-resource-book-scene-titlebar${active ? ' is-active' : ''}`}
|
||||
data-resource-book-category={group.category}
|
||||
style={
|
||||
{
|
||||
left: `${titleLayout.left}px`,
|
||||
top: `${titleLayout.top}px`,
|
||||
width: titleLayout.width,
|
||||
opacity: titleLayout.opacity,
|
||||
transform: titlebarTransform,
|
||||
transformOrigin: active ? '0 0' : undefined,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<ResourceBookTitleBar
|
||||
category={group.category}
|
||||
countLabel={`${group.resourceCount} 项`}
|
||||
onCollapse={active ? onCollapse : undefined}
|
||||
/>
|
||||
</div>
|
||||
{/*
|
||||
展开态的真实栏目不出标题栏:那一页是**一张平铺画布**,只有 `all` 那一条钉在
|
||||
视口上的标题栏(见 `ResourceBookSceneCategoryPlan.titlebar`)。
|
||||
*/}
|
||||
{group.titlebar ? (
|
||||
<div
|
||||
className={`game-resource-book-scene-titlebar${active ? ' is-active' : ''}`}
|
||||
data-resource-book-category={group.category}
|
||||
style={
|
||||
{
|
||||
left: `${titleLayout.left}px`,
|
||||
top: `${titleLayout.top}px`,
|
||||
width: titleLayout.width,
|
||||
opacity: titleLayout.opacity,
|
||||
transform: titlebarTransform,
|
||||
transformOrigin: active ? '0 0' : undefined,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<ResourceBookTitleBar
|
||||
category={group.category}
|
||||
countLabel={`${group.resourceCount} 项`}
|
||||
onCollapse={active ? onCollapse : undefined}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{group.cards.map((card) => {
|
||||
const expanded = card.presentation === 'child';
|
||||
const cardVisible =
|
||||
@@ -2307,20 +2312,6 @@ export default function ProjectDevelopmentView({
|
||||
resourcePositionById,
|
||||
visibleResourcesByCategory,
|
||||
]);
|
||||
/**
|
||||
* 展开态的栏目标题栏落点。复用总览态"按矩形钉标题栏"的同一条渲染路径,
|
||||
* 只是矩形来自分带而不是总览缩略卡的测量。
|
||||
*/
|
||||
const resourceBookAllTitlebarRectMap = useMemo(
|
||||
() => resourceBookAllTitlebarRects(resourceBookAllLayout),
|
||||
[resourceBookAllLayout],
|
||||
);
|
||||
/**
|
||||
* 画本场景当前该用哪份标题栏矩形:展开态用分带,其余状态用总览缩略卡的测量结果。
|
||||
*/
|
||||
const resourceBookSceneRects = resourceBookOpensAllResources
|
||||
? resourceBookAllTitlebarRectMap
|
||||
: resourceBookOverviewRects;
|
||||
/**
|
||||
* 展开态的视口拟合(入口与「适应画布」共用一处,避免两份几何口径)。
|
||||
*/
|
||||
@@ -2990,7 +2981,7 @@ export default function ProjectDevelopmentView({
|
||||
state: resourceBookState,
|
||||
visibleCategoryOrder: resourceBookSceneCategoryOrder,
|
||||
resourcesByCategory: resourceBookSceneResourcesByCategory,
|
||||
overviewRects: resourceBookSceneRects,
|
||||
overviewRects: resourceBookOverviewRects,
|
||||
positions: resourcePositionById,
|
||||
cardSizes: resourceCardSizeByResourceId,
|
||||
visibleResourceIds,
|
||||
@@ -3010,8 +3001,8 @@ export default function ProjectDevelopmentView({
|
||||
resourceBookAllLayout,
|
||||
resourceBookCategoryViewCenters,
|
||||
resourceBookExitingCards,
|
||||
resourceBookOverviewRects,
|
||||
resourceBookSceneCategoryOrder,
|
||||
resourceBookSceneRects,
|
||||
resourceBookSceneResourcesByCategory,
|
||||
resourceBookState,
|
||||
resourceCardSizeByResourceId,
|
||||
@@ -3022,8 +3013,8 @@ export default function ProjectDevelopmentView({
|
||||
],
|
||||
);
|
||||
/**
|
||||
* 展开态的标题栏几何由分带同步算出,不需要等总览缩略卡测量完(那套是总览态的 `is-measuring`
|
||||
* 首帧门)。其它状态维持原判据不变。
|
||||
* 展开态不需要等总览缩略卡测量完就能画(它的几何来自分带,不来自测量);其它状态维持
|
||||
* 原判据不变(总览态的 `is-measuring` 首帧门)。
|
||||
*/
|
||||
const resourceBookOverviewReady = useMemo(
|
||||
() =>
|
||||
@@ -3037,7 +3028,7 @@ export default function ProjectDevelopmentView({
|
||||
const resourceBookMotionSignatures = useMemo(() => {
|
||||
const signatures = new Map<string, string>();
|
||||
for (const group of resourceBookScenePlan) {
|
||||
const rect = resourceBookSceneRects.get(group.category);
|
||||
const rect = resourceBookOverviewRects.get(group.category);
|
||||
signatures.set(
|
||||
resourceBookSceneTitleKey(group.category),
|
||||
group.titlebarActive
|
||||
@@ -3049,7 +3040,7 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
}
|
||||
return signatures;
|
||||
}, [resourceBookScenePlan, resourceBookSceneRects]);
|
||||
}, [resourceBookScenePlan, resourceBookOverviewRects]);
|
||||
// 拖拽中的卡片只跟指针走,不参与布局 FLIP。
|
||||
const resourceBookFrozenMotionKeys = useMemo(() => {
|
||||
const keys = new Set<string>();
|
||||
@@ -6248,7 +6239,7 @@ export default function ProjectDevelopmentView({
|
||||
<ResourceBookScene
|
||||
plan={resourceBookScenePlan}
|
||||
resourceBookState={resourceBookState}
|
||||
overviewRects={resourceBookSceneRects}
|
||||
overviewRects={resourceBookOverviewRects}
|
||||
overviewReady={resourceBookOverviewReady}
|
||||
visibleResourceIds={visibleResourceIds}
|
||||
viewport={activeResourceCanvasViewport}
|
||||
|
||||
@@ -70,13 +70,12 @@ export function groupResourceBookStacks(
|
||||
/**
|
||||
* 「所有资源」展开态里一个栏目的分带。
|
||||
*
|
||||
* `titlebarTop` 是**标题栏**在世界坐标里的顶边,`originX / originY` 是**卡片空间原点**:
|
||||
* 栏目内局部坐标加它才得到世界坐标(`resourceBookChildCardLayout` 的产物仍是局部坐标)。
|
||||
* 两者分开是因为标题栏要占一条不被卡片压住的横带。
|
||||
* `originX / originY` 是**卡片空间原点**:栏目内局部坐标加它才得到世界坐标
|
||||
* (`resourceBookChildCardLayout` 的产物仍是局部坐标)。展开态只铺卡片、不出栏目标题栏
|
||||
* (见 `ResourceBookSceneCategoryPlan.titlebar`),所以这里没有任何标题栏几何。
|
||||
*/
|
||||
export type ResourceBookAllBand = {
|
||||
category: ResourceBookCategory;
|
||||
titlebarTop: number;
|
||||
originX: number;
|
||||
originY: number;
|
||||
width: number;
|
||||
@@ -90,21 +89,20 @@ export type ResourceBookAllLayout = {
|
||||
bounds: { x: number; y: number; width: number; height: number };
|
||||
};
|
||||
|
||||
/** 相邻两条分带之间的竖直留白(带上一条的卡片底边到下一条的标题栏顶边)。 */
|
||||
/** 相邻两条分带之间的竖直留白(上一带最后一张卡到下一条带第一张卡)。 */
|
||||
export const RESOURCE_BOOK_ALL_BAND_GAP = 48;
|
||||
|
||||
/**
|
||||
* 分带标题栏的高度。取 `.game-resource-book-scene-titlebar .game-resource-book-titlebar`
|
||||
* 的 `min-height`,让展开态的标题栏与总览/栏目态那一条同高,不为它另立一套尺寸。
|
||||
*/
|
||||
export const RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT = 42;
|
||||
|
||||
/**
|
||||
* 「所有资源」展开态的分带几何:沿用总览的栏目顺序,每个**有可见资源**的栏目纵向占一带。
|
||||
*
|
||||
* 卡片坐标是栏目内局部坐标(见 `resourceCanvasSectionExtent`),铺进同一张画布前必须各给
|
||||
* 一个带原点,否则各栏目全叠在原点。带宽高取可见资源的 extent:被筛掉的栏目不占屏,
|
||||
* 与总览/旧网格页"只保留有资源的栏目"同口径。
|
||||
* 与总览"只保留有资源的栏目"同口径。
|
||||
*
|
||||
* 带内只有卡片,没有标题栏横带:展开态是"一张平铺画布",唯一的标题栏是钉在视口上的
|
||||
* `all` 那一条(见 `buildResourceBookScenePlan`)。因此带高就是卡片 extent,带间距就是
|
||||
* `RESOURCE_BOOK_ALL_BAND_GAP`。没有可见资源的栏目不产生带 —— 这一点是硬约束:
|
||||
* 曾经"每条带都配一条标题栏"时,空栏目拿不到带矩形,标题栏会全部回落到世界原点叠在一起。
|
||||
*
|
||||
* 这里的产物只描述几何,不参与任何持久化:落盘仍只有栏目内局部坐标。
|
||||
*/
|
||||
@@ -144,14 +142,9 @@ export function buildResourceBookAllLayout({
|
||||
}),
|
||||
cardSizes,
|
||||
);
|
||||
const titlebarTop = cursorTop;
|
||||
const originY =
|
||||
titlebarTop +
|
||||
RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT +
|
||||
RESOURCE_BOOK_ALL_BAND_GAP / 2;
|
||||
const originY = cursorTop;
|
||||
const band: ResourceBookAllBand = {
|
||||
category,
|
||||
titlebarTop,
|
||||
originX: 0,
|
||||
originY,
|
||||
width: extent.width,
|
||||
@@ -161,9 +154,9 @@ export function buildResourceBookAllLayout({
|
||||
bandByCategory.set(category, band);
|
||||
minX = Math.min(minX, extent.x);
|
||||
maxX = Math.max(maxX, extent.x + extent.width);
|
||||
minY = Math.min(minY, titlebarTop);
|
||||
minY = Math.min(minY, originY);
|
||||
maxY = Math.max(maxY, originY + extent.height);
|
||||
cursorTop = originY + extent.height + RESOURCE_BOOK_ALL_BAND_GAP / 2;
|
||||
cursorTop = originY + extent.height + RESOURCE_BOOK_ALL_BAND_GAP;
|
||||
}
|
||||
return {
|
||||
bands,
|
||||
@@ -177,28 +170,6 @@ export function buildResourceBookAllLayout({
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 分带 → 场景标题栏矩形。
|
||||
*
|
||||
* 展开态复用总览态"按矩形把栏目标题栏钉在世界坐标上"的同一条渲染路径,只是矩形来自分带,
|
||||
* 而不是总览缩略卡的 DOM 测量;`ResourceBookScene` 不需要为这一态知道任何新概念。
|
||||
*/
|
||||
export function resourceBookAllTitlebarRects(
|
||||
layout: ResourceBookAllLayout,
|
||||
): Map<ResourceBookTarget, ResourceBookOverviewRect> {
|
||||
return new Map(
|
||||
layout.bands.map((band) => [
|
||||
band.category as ResourceBookTarget,
|
||||
{
|
||||
left: band.originX,
|
||||
top: band.titlebarTop,
|
||||
width: band.width,
|
||||
height: RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开态的世界坐标 → 栏目内局部坐标(**落盘前必须走的唯一换算**)。
|
||||
*
|
||||
@@ -364,6 +335,14 @@ export type ResourceBookSceneCardPlan = {
|
||||
export type ResourceBookSceneCategoryPlan = {
|
||||
category: ResourceBookTarget;
|
||||
resourceCount: number;
|
||||
/**
|
||||
* 这一组要不要渲染栏目标题栏节点。
|
||||
*
|
||||
* 展开态只有 `all` 那一条(钉在视口上、带「资源总览」收起入口):展开态是**一张平铺
|
||||
* 画布**,真实栏目只铺卡片。总览态与栏目态每一组都要(总览态靠它承载计数徽标,
|
||||
* 栏目态靠它承载当前栏目与返回入口)。
|
||||
*/
|
||||
titlebar: boolean;
|
||||
titlebarActive: boolean;
|
||||
titlebarOpacity: number;
|
||||
cards: ResourceBookSceneCardPlan[];
|
||||
@@ -377,7 +356,8 @@ export type ResourceBookSceneCategoryPlan = {
|
||||
* - 返回总览时源栏目未回到摞上的卡片按子画布布局淡出;
|
||||
* - 搜索/排序导致的卸载由 `exitingCards` 保留一帧,用同一套淡出撤下。
|
||||
* - 「所有资源」组在总览态只出栏目标题栏(计数即全量资源数),卡片本体仍由各栏目摞承载;
|
||||
* 在**展开态**则反过来:各真实栏目铺满自己分带里的全部卡片,`all` 只留标题栏与计数。
|
||||
* 在**展开态**则反过来:各真实栏目把自己分带里的全部卡片平铺在画布上(**不出标题栏**),
|
||||
* `all` 只留钉住的标题栏与计数。
|
||||
*/
|
||||
export function buildResourceBookScenePlan({
|
||||
state,
|
||||
@@ -453,7 +433,7 @@ export function buildResourceBookScenePlan({
|
||||
const categoryResources = resourcesByCategory.get(category) ?? [];
|
||||
/**
|
||||
* 展开态:`all` 只出标题栏与全量计数(`titlebarActive` 让它钉在视口上,并带上
|
||||
* 「资源总览」收起入口);真实栏目把全部卡片铺在自己那条分带里。
|
||||
* 「资源总览」收起入口);真实栏目把全部卡片平铺在自己那条分带里,**不渲染标题栏**。
|
||||
*
|
||||
* 真实栏目这一支仍然按 `presentation === 'child'` 取卡,而不是"展开态就一定出卡":
|
||||
* "能出卡的判定"只有 `resourceBookCategoryCardPresentation` 一处,避免两边口径分叉。
|
||||
@@ -463,6 +443,7 @@ export function buildResourceBookScenePlan({
|
||||
return {
|
||||
category,
|
||||
resourceCount: categoryResources.length,
|
||||
titlebar: true,
|
||||
titlebarActive: true,
|
||||
titlebarOpacity: 1,
|
||||
cards,
|
||||
@@ -485,8 +466,9 @@ export function buildResourceBookScenePlan({
|
||||
return {
|
||||
category,
|
||||
resourceCount: categoryResources.length,
|
||||
titlebar: false,
|
||||
titlebarActive: false,
|
||||
titlebarOpacity: 1,
|
||||
titlebarOpacity: 0,
|
||||
cards,
|
||||
};
|
||||
}
|
||||
@@ -575,6 +557,7 @@ export function buildResourceBookScenePlan({
|
||||
return {
|
||||
category,
|
||||
resourceCount: categoryResources.length,
|
||||
titlebar: true,
|
||||
titlebarActive: presentation === 'child',
|
||||
titlebarOpacity: childLayout || state.view === 'main' ? 1 : 0,
|
||||
cards,
|
||||
|
||||
@@ -9609,9 +9609,9 @@ export function registerProjectAgentStatusTests() {
|
||||
expect(previewHostRule).toMatch(/position:\s*absolute/);
|
||||
expect(previewHostRule).toMatch(/transform-origin:\s*0 0/);
|
||||
|
||||
// 3) 点它进入的是**同一套画本场景**,不是另一条渲染分支:每个真实栏目铺在自己的分带上,
|
||||
// 卡片宿主是栏目页那一个 `.game-resource-book-scene-card`、`data-resource-book-category`
|
||||
// 仍是真实栏目;`all` 自己只留钉住的标题栏与全量计数。
|
||||
// 3) 点它进入的是**同一套画本场景**,不是另一条渲染分支:真实栏目的卡片平铺在同一张
|
||||
// 画布上,卡片宿主是栏目页那一个 `.game-resource-book-scene-card`、
|
||||
// `data-resource-book-category` 仍是真实栏目;`all` 自己只留钉住的标题栏与全量计数。
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开所有资源' }));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
@@ -9651,6 +9651,14 @@ export function registerProjectAgentStatusTests() {
|
||||
);
|
||||
expect(hostCategories.length).toBeGreaterThan(1);
|
||||
expect(hostCategories).not.toContain('all');
|
||||
// 展开态是一张**平铺画布**:真实栏目不出标题栏,整个场景只留 `all` 那一条钉住的。
|
||||
// 曾经"每条分带配一条标题栏"时,没有可见资源的栏目拿不到带矩形,标题栏会全部回落到
|
||||
// 世界原点叠成一摞(用户报的破版就是这一摞);下面这条把它钉死。
|
||||
expect(
|
||||
document.querySelectorAll(
|
||||
'.game-resource-book-scene-titlebar:not([data-resource-book-category="all"])',
|
||||
),
|
||||
).toHaveLength(0);
|
||||
// 每张资源恰好一个宿主(展开态没有"第二份宿主"):条数与全量计数同源,
|
||||
// 因此不会出现「标题说 N 项、画布少几张」。
|
||||
expect(
|
||||
@@ -9785,6 +9793,13 @@ export function registerProjectAgentStatusTests() {
|
||||
expect(
|
||||
document.querySelectorAll('.game-resource-book-scene-card'),
|
||||
).toHaveLength(0);
|
||||
// 空项目恰恰是那一摞的现场:没有任何带 ⇒ 曾经每个真实栏目的标题栏都回落到世界原点
|
||||
// 叠在一起(最后画的「项目版本」浮在画布中央)。展开态只允许 `all` 那一条标题栏。
|
||||
expect(
|
||||
document.querySelectorAll(
|
||||
'.game-resource-book-scene-titlebar:not([data-resource-book-category="all"])',
|
||||
),
|
||||
).toHaveLength(0);
|
||||
expect(
|
||||
document.querySelector('.game-resource-page-empty')?.textContent,
|
||||
).toBe('没有匹配资源');
|
||||
|
||||
@@ -4,12 +4,11 @@ import {
|
||||
buildResourceBookAllLayout,
|
||||
buildResourceBookScenePlan,
|
||||
groupResourceBookStacks,
|
||||
RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT,
|
||||
RESOURCE_BOOK_ALL_BAND_GAP,
|
||||
RESOURCE_BOOK_OVERVIEW_STACK_LIMIT,
|
||||
type ResourceBookAllBand,
|
||||
resourceBookAllBandLocalPoint,
|
||||
type ResourceBookAllLayout,
|
||||
resourceBookAllTitlebarRects,
|
||||
resourceBookOverviewCardLayout,
|
||||
type ResourceBookOverviewRect,
|
||||
resourceBookSceneCardKey,
|
||||
@@ -421,7 +420,7 @@ describe('buildResourceBookScenePlan', () => {
|
||||
['unclassified', resources],
|
||||
['scene', [sceneResource]],
|
||||
]),
|
||||
overviewRects: resourceBookAllTitlebarRects(allLayout),
|
||||
overviewRects: new Map<ResourceBookTarget, ResourceBookOverviewRect>(),
|
||||
positions: allPositions,
|
||||
cardSizes: allCardSizes,
|
||||
visibleResourceIds: new Set(allResources.map((item) => item.id)),
|
||||
@@ -439,6 +438,7 @@ describe('buildResourceBookScenePlan', () => {
|
||||
)!;
|
||||
// `all` 一张卡都不挂:卡片由各真实栏目承载,否则同一张卡会挂两份、FLIP 键互相顶掉。
|
||||
expect(allGroup.cards).toEqual([]);
|
||||
expect(allGroup.titlebar).toBe(true);
|
||||
expect(allGroup.titlebarActive).toBe(true);
|
||||
expect(allGroup.titlebarOpacity).toBe(1);
|
||||
expect(allGroup.resourceCount).toBe(allResources.length);
|
||||
@@ -449,9 +449,11 @@ describe('buildResourceBookScenePlan', () => {
|
||||
expect(artGroup.cards.every((card) => card.presentation === 'child')).toBe(
|
||||
true,
|
||||
);
|
||||
// 展开态的栏目标题栏不钉在视口上,但要可见(它随分带一起被视口变换)。
|
||||
// 展开态的真实栏目**不出标题栏**:那一页是一张平铺画布,标题栏只有 `all` 那一条。
|
||||
// 曾经"每条带配一条标题栏"时,没有可见资源的栏目拿不到带矩形,标题栏会集体回落到
|
||||
// 世界原点叠成一摞(用户报的正是这个)。
|
||||
expect(artGroup.titlebar).toBe(false);
|
||||
expect(artGroup.titlebarActive).toBe(false);
|
||||
expect(artGroup.titlebarOpacity).toBe(1);
|
||||
// 画出来的是"栏目内局部坐标 + 带原点",拖动起点与它同系(提交时再减回带原点)。
|
||||
const first = artGroup.cards[0]!;
|
||||
expect(first.layout.x).toBe(positions.get('art-0')!.x + band.originX);
|
||||
@@ -491,7 +493,7 @@ describe('buildResourceBookScenePlan', () => {
|
||||
[RESOURCE_BOOK_ALL_TARGET, resources],
|
||||
['unclassified', resources],
|
||||
]),
|
||||
overviewRects: resourceBookAllTitlebarRects(allLayout),
|
||||
overviewRects: new Map<ResourceBookTarget, ResourceBookOverviewRect>(),
|
||||
state: {
|
||||
view: 'main',
|
||||
category: null,
|
||||
@@ -550,13 +552,17 @@ describe('buildResourceBookAllLayout', () => {
|
||||
|
||||
it('stacks the bands downward so no two categories share one origin', () => {
|
||||
const [first, second] = layout.bands;
|
||||
// 标题栏横带压在卡片区之上,两个带的首尾不重叠:否则拖上面一带的卡会压到下面一带的标题。
|
||||
expect(first!.titlebarTop).toBeLessThan(first!.originY);
|
||||
expect(second!.titlebarTop).toBeGreaterThan(first!.originY + first!.height);
|
||||
// 带里只有卡片,所以带宽从原点直接长出 extent 高;两带之间隔一条固定的留白:
|
||||
// 否则拖上面一带的卡会压到下面一带的卡上。
|
||||
expect(first!.originY).toBe(0);
|
||||
expect(first!.height).toBeGreaterThan(0);
|
||||
expect(second!.originY).toBe(
|
||||
first!.originY + first!.height + RESOURCE_BOOK_ALL_BAND_GAP,
|
||||
);
|
||||
expect(second!.originY).toBeGreaterThan(first!.originY);
|
||||
});
|
||||
|
||||
it('covers every titlebar and card box in its bounds', () => {
|
||||
it('covers every card box in its bounds', () => {
|
||||
const last = layout.bands.at(-1)!;
|
||||
expect(layout.bounds.x).toBeLessThanOrEqual(0);
|
||||
expect(layout.bounds.y).toBeLessThanOrEqual(0);
|
||||
@@ -565,25 +571,11 @@ describe('buildResourceBookAllLayout', () => {
|
||||
last.originY + last.height,
|
||||
);
|
||||
});
|
||||
|
||||
it('hands every band a titlebar rect so the scene reuses the overview titlebar path', () => {
|
||||
const rects = resourceBookAllTitlebarRects(layout);
|
||||
const audio = layout.bandByCategory.get('audio')!;
|
||||
|
||||
expect(rects.get('audio')).toEqual({
|
||||
left: audio.originX,
|
||||
top: audio.titlebarTop,
|
||||
width: audio.width,
|
||||
height: RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT,
|
||||
});
|
||||
expect(rects.has('character')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resourceBookAllBandLocalPoint', () => {
|
||||
const band: ResourceBookAllBand = {
|
||||
category: 'document',
|
||||
titlebarTop: 0,
|
||||
originX: 40,
|
||||
originY: 100,
|
||||
width: 620,
|
||||
|
||||
@@ -8469,3 +8469,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 与"历史手动坐标只读恢复、不删除、不重置、不迁移"的关系:**不冲突**。写侧只做"把这一张卡在它自己栏目里的坐标设为手动 + 新 x/y",正是栏目页拖动的同一操作(`moveResourceCanvasPosition` 只改该 `(resourceId, section)` 一条并置 `manuallyPlaced: true`);读时归并(`normalizeResourceCanvasPosition`)与"不触发 automatic 重排"的既有口径都未改动。
|
||||
- 验证方式:`resourceBookAllBandLocalPoint` 两条单测(减带原点 / 栏目页与无带时原样返回);变异验证:去掉减带原点 → 用例红(实测 `{x:100,y:260}` vs 期望 `{x:100,y:160}`)。真机判据(用户可自证"同一份数据"):在展开态把某张卡拖到新位置 → 进它所属栏目页,同一张卡就在新位置;反向在栏目页拖一张 → 回展开态看它在带内同步。
|
||||
- 关联文档:`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S7 判据含这条往返)、`apps/ai-game-creator-shell/src/view/project-development/{useProjectResourceCanvasLayout.ts,resourceBookLayout.ts,index.tsx}`。
|
||||
|
||||
## 2026-09-12 「所有资源」展开态去掉每栏一条标题栏:那一页是一张平铺画布,只留 `all` 那一条
|
||||
|
||||
- 背景:用户报「所有资源里的 UI 样式有点问题,还有,我希望这里面所有资源都平铺开,和其他普通画布一样」。真机截图(空项目、0 项)里画布中央浮着一块带阴影的「项目版本 0 项」面板,其余栏目看不见——这就是上一版"每条分带配一条标题栏"的直接后果:**分带只对"有可见资源"的栏目生成**(`buildResourceBookAllLayout` 里 `resources.length === 0 → continue`),而标题栏矩形只从分带里取(旧 `resourceBookAllTitlebarRects`),于是空栏目的标题栏拿不到矩形,全部回落到 `ResourceBookScene` 的 `rect?.left ?? 0 / rect?.top ?? 0 / width ?? 280px` 默认值 ⇒ **7 个真实栏目的标题栏在世界原点叠成一摞**,最后画的那个(栏目顺序末位)浮在最上面。空项目恰恰是必现现场:一条带都没有。
|
||||
- 决策:**展开态的真实栏目不再渲染栏目标题栏**(`ResourceBookSceneCategoryPlan` 新增 `titlebar: boolean`,展开态真实栏目为 `false`),整个场景只留 `all` 那一条钉在视口上、带「资源总览」收起入口的标题栏。于是「所有资源」在结构上与栏目页完全同形:**一条钉住的标题栏 + 一张平铺的画布**(点阵底、卡片可拖、可滚轮缩放平移、可框选),不再有"每组一个浮动面板"。分带保留(它仍是"局部坐标 → 世界坐标"的唯一偏移,见 2026-09-11 那条拖动写回决策),但带内**只剩卡片**:删掉标题栏横带的高度预留(`RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT`)与它的矩形换算(`resourceBookAllTitlebarRects`),带原点直接等于游标,带间距就是 `RESOURCE_BOOK_ALL_BAND_GAP`。
|
||||
- 不做什么:不动总览态与栏目态(两者每一组都要标题栏:总览态靠它承载计数徽标,栏目态靠它承载当前栏目与返回入口);不动拖动写回口径(`resourceBookAllBandLocalPoint` 一行未改);不动转场层;**零 CSS 变更**(`.game-resource-book-scene-titlebar` 规则全部保留,只是展开态少渲染几个节点)。
|
||||
- 验证方式:① 单测 `resourceBookLayout.test` 钉住"展开态真实栏目 `titlebar === false`、`all` 为 `true`",并删掉只服务旧标题栏矩形的断言与那个已不存在的导出;② appSurface 两条「所有资源」用例各加一条反向守卫 `document.querySelectorAll('.game-resource-book-scene-titlebar:not([data-resource-book-category="all"])')` 长度为 0(空项目那条正是用户现场);③ **变异验证(已实测红灯)**:把展开态真实栏目的 `titlebar` 改回 `true` → 两条守卫同时红,报 `expected … to have a length of +0 but got 7`——这个 `7` 就是七个真实栏目叠在原点的实证。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src/view/project-development/{resourceBookLayout.ts,index.tsx}`、`apps/ai-game-creator-shell/tests/{resourceBookLayout.test.ts,appSurface/project-development.suite.ts}`、`docs/technical/【测试用例】AGC资源工作台V3端到端验收-2026-09-11.md`(S7 行措辞)。
|
||||
- 关联:本条**收窄**(不推翻)2026-09-11 的「所有资源」页画本场景决策:那一条定的是"同一套场景 + 同一颗 renderCard + 分带偏移",本条只改"展开态出不出栏目标题栏"。
|
||||
|
||||
@@ -5354,3 +5354,12 @@
|
||||
- **处理**:push 前按链路逐条跑,不要只跑子集;`&&` 链在某一步失败时,**必须把失败步之后的每一步单独再跑一遍**(上列 10 步 + `git diff --check`),逐条记 exit code(取 exit code 不要接管道)。凡改动碰到 `package.json` / `.husky/` / `scripts/` 下的门禁资产,**`check:git-hooks` 是必跑项**——它钉住 hook 与 lint-staged 配置的形状,增删键都会让它以 `deepStrictEqual` 失败。
|
||||
- **验证**:本次 `check:git-hooks` 因本机 Windows EBUSY 中断后,把其后各步单独跑齐:`check:spacetime-schema` / `check:production-ops` / `check:preview-deployer` / `check:maintenance-page` / `lint:eslint` / `typecheck` / `check:encoding` 全部 `exit 0` —— 即「因为前一步红而没跑到」的部分本来是全绿的;只有 `check:rustfmt` 红,且红在别人在途的 `.rs`(`assets.rs:519` 落在其未提交 hunk `+470,62`、`commands.rs:3079` 落在其未提交 hunk `+3068,58`)。变异验证:把 `*.rs` 从 `package.json` 摘掉 → `check:git-hooks` 第 1 个用例以同样的 `deepStrictEqual` operator 变红(`# fail 2`);还原(`package.json` SHA256 与改前一致)后该用例回 `ok`。
|
||||
- **关联**:`package.json` 的 `lint` / `check:git-hooks` / `check:rustfmt`、`scripts/check-repository-ci.sh`、`scripts/git-hooks.test.mjs`、`.husky/pre-commit`、提交 `dd7cf401a` / `2a7bfadd7` / `15660a98b`。
|
||||
|
||||
## 2026-09-12 「矩形从哪来」缺一路输入时,别让它静默回落到世界原点:7 条栏目标题栏在空项目里叠成一摞
|
||||
|
||||
- **现象**:空项目(0 项)进「所有资源」,画布正中央浮着一块带阴影的「项目版本 0 项」面板,其余六个栏目看不见——用户报"UI 样式有点问题"。它们并不是被谁隐藏,而是**七块面板叠在同一个位置**,只看得见最后渲染的那一块。
|
||||
- **原因**:`ResourceBookScene` 的栏目标题栏位置只有一路输入 `overviewRects.get(category)`,取不到就用 `rect?.left ?? 0 / rect?.top ?? 0 / width ?? 280px` 兜底。而展开态这一路输入来自分带矩形(旧 `resourceBookAllTitlebarRects`),分带又只对**有可见资源**的栏目生成(`buildResourceBookAllLayout` 里 `resources.length === 0 → continue`)⇒ 空栏目永远拿不到矩形,全部回落到世界原点;栏目顺序七项,最后画的「项目版本」压在最上面。空项目是必现现场(一条带都没有),用户截图正是这种。
|
||||
- **识别特征**:出现的不是"某一个错位面板",而是**栏目顺序里最后一项**的名字(本例 = 栏目顺序末位「项目版本」),且它随视口平移一起动 —— 即"坐标系原点上的同类节点堆叠",而不是定位算错。
|
||||
- **处理**:让"该不该有这个节点"显式化,而不是靠"矩形取不到就兜底":`ResourceBookSceneCategoryPlan` 增 `titlebar: boolean`,展开态的真实栏目为 `false`(那一页只留 `all` 一条钉住的标题栏),渲染层按它决定是否产出节点;同时删掉只服务旧形态的 `RESOURCE_BOOK_ALL_BAND_TITLE_HEIGHT` / `resourceBookAllTitlebarRects`。**`?? 0` 兜底本身不是错,错在它同时承担"没有布局信息"和"布局信息恰好是原点"两种语义**——凡是一个坐标有多路来源(测量 / 计算 / 静态),缺某一路时要么显式不渲染,要么让缺路成为可观测的信号,不要静默给默认值。
|
||||
- **验证**:appSurface 两条「所有资源」用例加反向守卫 `querySelectorAll('.game-resource-book-scene-titlebar:not([data-resource-book-category="all"])')` 长度为 0(含空项目那条);变异验证:把展开态真实栏目的 `titlebar` 改回 `true` → 两条同时红,报 `expected … to have a length of +0 but got 7`(7 = 七个真实栏目,实证是"一摞"而不是"一个错位框")。
|
||||
- **关联**:`apps/ai-game-creator-shell/src/view/project-development/{resourceBookLayout.ts,index.tsx}`、`apps/ai-game-creator-shell/tests/{resourceBookLayout.test.ts,appSurface/project-development.suite.ts}`、`decision-log.md` 2026-09-12 条。
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
| 步骤 | 操作 | 期望结果 | 对应 PRD 条款 | 怎么判"过了" | 已知例外 / 未做项 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| **S7** 栏目导航 | 点总览第 0 张「所有资源」卡 → 栏目缩略卡片进栏目 → 页内底部「下一页」 | 「所有资源」是入口(第 0 张);进入后**与栏目页是同一套画本场景**——卡片宿主同为 `.game-resource-book-scene-card`、每张卡可拖、可滚轮缩放平移,各栏目按**竖向分带**铺在同一张画布上;全部栏目(含空栏目)都可到达;左侧悬浮栏目大纲导航已删除;滚轮不切栏目 | L56、L512、L539–541 | `[data-resource-book-view="main"\|"child"]`、当前栏目 `.game-resource-book-scene-titlebar.is-active[data-resource-book-category]`(展开态这一条是 `data-resource-book-category="all"`,带「资源总览」收起入口)、卡片宿主 `.game-resource-book-scene-card[data-resource-book-category="<真实栏目>"]`(每张资源**恰好一个**宿主)、`[data-resource-book-section-scroll]`。**旧的独立网格判据已随分支删除**:`[data-resource-book-all-page="true"]` / `[data-resource-book-all-host="true"]` / `.game-resource-all-*` 都不应再出现。拖动的判据:在展开态拖一张卡,再进它所属栏目页看同一张卡**落在新位置**(两页共用同一份 `resource-layouts/{dependency,type}.json` 的栏目内局部坐标) | 「所有资源」卡渲染真实预览(`game-resource-book-preview-card`)**已提交**(`ea9668e4a`);旧构建只有占位摞。展开态的视口是内存态(不落盘),只有卡片坐标落盘 |
|
||||
| **S7** 栏目导航 | 点总览第 0 张「所有资源」卡 → 栏目缩略卡片进栏目 → 页内底部「下一页」 | 「所有资源」是入口(第 0 张);进入后**与栏目页是同一套画本场景**——卡片宿主同为 `.game-resource-book-scene-card`、每张卡可拖、可滚轮缩放平移,各栏目按**竖向分带**平铺在同一张画布上(展开态**不出每栏一条标题栏**,整个场景只有 `all` 那一条钉在视口上的标题栏,与栏目页"一条钉住的标题栏 + 一张平铺画布"同形);全部栏目(含空栏目)都可到达;左侧悬浮栏目大纲导航已删除;滚轮不切栏目 | L56、L512、L539–541 | `[data-resource-book-view="main"\|"child"]`、当前栏目 `.game-resource-book-scene-titlebar.is-active[data-resource-book-category]`(展开态这一条是 `data-resource-book-category="all"`,带「资源总览」收起入口;**展开态 `data-resource-book-category` 非 `all` 的标题栏一个都不该有**——空栏目曾因拿不到分带矩形而集体回落到世界原点叠成一摞)、卡片宿主 `.game-resource-book-scene-card[data-resource-book-category="<真实栏目>"]`(每张资源**恰好一个**宿主)、`[data-resource-book-section-scroll]`。**旧的独立网格判据已随分支删除**:`[data-resource-book-all-page="true"]` / `[data-resource-book-all-host="true"]` / `.game-resource-all-*` 都不应再出现。拖动的判据:在展开态拖一张卡,再进它所属栏目页看同一张卡**落在新位置**(两页共用同一份 `resource-layouts/{dependency,type}.json` 的栏目内局部坐标) | 「所有资源」卡渲染真实预览(`game-resource-book-preview-card`)**已提交**(`ea9668e4a`);旧构建只有占位摞。展开态的视口是内存态(不落盘),只有卡片坐标落盘 |
|
||||
| **S8** 搜索 | 点右下角 `搜索资源` 按钮或按 `Ctrl/Cmd+F` → 输入 → Esc | 浮层收起**不清搜索条件**;`type=search` 原生 Esc 清空被 `preventDefault` 挡掉;存在搜索时 Dock 按钮高亮 | L55、L322(搜索只隐藏卡片、不重排坐标) | `button[aria-label="搜索资源"]` 的 `aria-expanded`、`title="搜索资源(Ctrl/Cmd+F)"`、`.is-active`;输入框 `input[type=search][aria-label="搜索项目资源"]`,其 `id` 等于按钮的 `aria-controls` | 匹配字段 = **名称 / 路径 / 媒体类型 / 任务标题**,不含 `sourceLabel` |
|
||||
| **S9** 选择与多选 | 单击卡片 → `Shift`/`Ctrl`/`Meta` 再点 → 空白处拖拽框选 | 点卡 = 选中并在卡片旁浮出工具条;三种修饰键等效追加;框选可见选框 | L51、L61–62、L318–319 | `data-resource-card-id` + class 含 `is-selected` + `aria-pressed="true"`;浮出容器 `role="toolbar"`(图片 `aria-label="图片工具栏"`,音频 `"素材工具栏"`) | ⚠️ **PRD L61–62 要求的「打开详情」按钮在代码中不存在**(测试显式断言为 `null`),按 Issue #309 C3「取消资源详情面板与全屏编辑路由」判,不要判为缺失;真正入口是 `aria-label="选中资源:<栏目> <名>"` 按钮。**只读信息浮层已回归**(2026-09-11,工具条「信息」动作,见 `decision-log`):**C3 取消的仍只是可编辑详情面板与全屏编辑路由** |
|
||||
| **S10** 快速编辑(图片) | 选中 PNG/JPEG/WEBP 卡 → 工具条「快速编辑」→ 输入 → 提交(提示词下方另有「AI 润色」) | 先 `normalize_local_project_raster_resource`,再 `derive_local_project_resource(editKind='image-reference')` → 服务端 `/api/editor/images/edits`;**产出一张新素材**,源素材与源文件不变,新资源带 `referenceResourceIds` 血缘 | L123、L193、L476–478、L557;#309 C3/C10 | 提交后 `assets[]` +1、卡片 +1 且 `data-resource-card-id` 为新 id;**源卡仍在且内容未变**;工具条真渲染的只有 `quick-edit` 与 `download`;「AI 润色」`aria-label="AI 润色"` + `aria-busy`,成功回填并出现「恢复原文」,失败保留原文并给「AI 润色失败,可重试」 | ① 服务端白名单 17 项(`editor_project.rs:4120-4141`),`character-animation`/`video`/`audio`/`document`/`code` 等**仍会 400**;② 工具条 7 个动作(重绘 / 裁剪扩图 / 去背景 / 像素完美 / 切图集 / 提取 UI 素材 / 角色动画)按 opt-in **不渲染**;③ 润色结果超 `resourceEditPromptMaxLength` 会**截断并提示「已按长度上限截断」**;④ 提示词变了(改写或润色回填)必须换 `operationId`/幂等键,否则 Rust `request_fingerprint` 判「已绑定到不同资源编辑请求」 |
|
||||
|
||||
Reference in New Issue
Block a user