2c5ad3903e
总览原先按资源数组顺序取前 3 张,和进入子画布后视口内最近的卡片不是同一批,FLIP 找不到对应终点就退化成淡入淡出,返回总览时还会跳成另一张图 新增 selectResourceBookOverviewCards:按卡片中心到该栏目当前视口中心的距离选最近的卡片,仍受筛选可见性约束 每栏目视口中心换算到画布坐标后传入资源画本场景 补充 resourceBookLayout 用例覆盖无视图中心、就近排序与筛选过滤
124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
groupResourceBookStacks,
|
|
RESOURCE_BOOK_OVERVIEW_STACK_LIMIT,
|
|
resourceBookOverviewCardLayout,
|
|
selectResourceBookOverviewCards,
|
|
} from '../src/view/project-development/resourceBookLayout';
|
|
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
|
|
|
|
function resource(id: string): ProjectResource {
|
|
return {
|
|
id,
|
|
category: 'art',
|
|
subtype: 'image',
|
|
label: id,
|
|
path: `assets/${id}.png`,
|
|
mediaType: 'image/png',
|
|
sourceLabel: '测试',
|
|
taskTitle: null,
|
|
manifestAssetId: id,
|
|
producerTaskId: null,
|
|
externalResourceId: null,
|
|
referenceResourceIds: [],
|
|
dependencies: [],
|
|
dependencyDepth: 0,
|
|
};
|
|
}
|
|
|
|
const rect = { left: 100, top: 200, width: 280, height: 210 };
|
|
|
|
describe('resourceBookOverviewCardLayout', () => {
|
|
it('keeps the stack tilt bounded however many resources share one type', () => {
|
|
const rotations = [0, 1, 2, 3, 12, 59].map(
|
|
(stackIndex) =>
|
|
resourceBookOverviewCardLayout({ rect, stackIndex, stackColumn: 0 })
|
|
.rotation,
|
|
);
|
|
|
|
for (const rotation of rotations) {
|
|
expect(Math.abs(rotation)).toBeLessThanOrEqual(3);
|
|
}
|
|
expect(rotations[0]).toBe(0.5);
|
|
expect(rotations.at(-1)).toBe(rotations[2]);
|
|
});
|
|
|
|
it('stops offsetting deeper cards once the visible stack depth is reached', () => {
|
|
const positions = [0, 1, 2, 3, 59].map(
|
|
(stackIndex) =>
|
|
resourceBookOverviewCardLayout({ rect, stackIndex, stackColumn: 0 }).y,
|
|
);
|
|
|
|
expect(positions[0]).toBe(rect.top + 58);
|
|
expect(positions[1]).toBe(rect.top + 63);
|
|
expect(positions[2]).toBe(rect.top + 68);
|
|
expect(positions.at(-1)).toBe(positions[2]);
|
|
});
|
|
|
|
it('limits how many cards the overview renders per type stack', () => {
|
|
expect(RESOURCE_BOOK_OVERVIEW_STACK_LIMIT).toBe(3);
|
|
});
|
|
|
|
describe('selectResourceBookOverviewCards', () => {
|
|
const cards = ['a', 'b', 'c', 'd'].map(resource);
|
|
const positions = new Map([
|
|
['a', { x: 0, y: 0 }],
|
|
['b', { x: 1_000, y: 0 }],
|
|
['c', { x: 2_000, y: 0 }],
|
|
['d', { x: 3_000, y: 0 }],
|
|
]);
|
|
const cardSizes = new Map<string, { width: number; height: number }>();
|
|
|
|
it('keeps the first visible cards when the view center is unknown', () => {
|
|
expect(
|
|
selectResourceBookOverviewCards({
|
|
resources: cards,
|
|
visibleResourceIds: new Set(['a', 'b', 'c', 'd']),
|
|
positions,
|
|
cardSizes,
|
|
viewCenter: undefined,
|
|
}).map((item) => item.id),
|
|
).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('prefers the cards nearest the current view center', () => {
|
|
expect(
|
|
selectResourceBookOverviewCards({
|
|
resources: cards,
|
|
visibleResourceIds: new Set(['a', 'b', 'c', 'd']),
|
|
positions,
|
|
cardSizes,
|
|
viewCenter: { x: 3_000, y: 64 },
|
|
}).map((item) => item.id),
|
|
).toEqual(['d', 'c', 'b']);
|
|
});
|
|
|
|
it('never surfaces cards hidden by the current filter', () => {
|
|
expect(
|
|
selectResourceBookOverviewCards({
|
|
resources: cards,
|
|
visibleResourceIds: new Set(['a', 'c', 'd']),
|
|
positions,
|
|
cardSizes,
|
|
viewCenter: { x: 1_000, y: 64 },
|
|
}).map((item) => item.id),
|
|
).toEqual(['a', 'c', 'd']);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('groupResourceBookStacks', () => {
|
|
it('keeps resources of one type in a single stack', () => {
|
|
const stacks = groupResourceBookStacks([
|
|
resource('overview-art-0'),
|
|
resource('overview-art-1'),
|
|
resource('overview-art-2'),
|
|
resource('overview-art-3'),
|
|
]);
|
|
|
|
expect(stacks).toHaveLength(1);
|
|
expect(stacks[0]?.[1]).toHaveLength(4);
|
|
});
|
|
});
|