WIP: AGC 资源画布与替换改造 V3.0 #316
@@ -91,9 +91,9 @@ import {
|
||||
import {
|
||||
groupResourceBookResourcesByCategory,
|
||||
groupResourceBookStacks,
|
||||
RESOURCE_BOOK_OVERVIEW_STACK_LIMIT,
|
||||
resourceBookChildCardLayout,
|
||||
resourceBookOverviewCardLayout,
|
||||
selectResourceBookOverviewCards,
|
||||
type ResourceBookOverviewRect,
|
||||
} from './resourceBookLayout';
|
||||
import {
|
||||
@@ -1026,6 +1026,7 @@ function ResourceBookScene({
|
||||
positions,
|
||||
cardSizes,
|
||||
visibleResourceIds,
|
||||
categoryViewCenters,
|
||||
cardsReady,
|
||||
viewport,
|
||||
mainViewport,
|
||||
@@ -1044,6 +1045,7 @@ function ResourceBookScene({
|
||||
positions: Map<string, { x: number; y: number; section?: ResourceCategory }>;
|
||||
cardSizes: Map<string, ResourceCanvasCardSize>;
|
||||
visibleResourceIds: Set<string>;
|
||||
categoryViewCenters: Map<ResourceCategory, { x: number; y: number }>;
|
||||
cardsReady: boolean;
|
||||
viewport: CanvasViewport;
|
||||
mainViewport: CanvasViewport;
|
||||
@@ -1144,11 +1146,13 @@ function ResourceBookScene({
|
||||
// 数量由栏目标题栏的“N 项”表达,子画布仍然渲染全部资源。
|
||||
const stackItems = expanded
|
||||
? items
|
||||
: items
|
||||
.filter((resource) =>
|
||||
visibleResourceIds.has(resource.id),
|
||||
)
|
||||
.slice(0, RESOURCE_BOOK_OVERVIEW_STACK_LIMIT);
|
||||
: selectResourceBookOverviewCards({
|
||||
resources: items,
|
||||
visibleResourceIds,
|
||||
positions,
|
||||
cardSizes,
|
||||
viewCenter: categoryViewCenters.get(category),
|
||||
});
|
||||
return stackItems.map((resource, stackIndex) => {
|
||||
const position = positions.get(resource.id);
|
||||
const size = cardSizes.get(resource.id) ?? {
|
||||
@@ -2155,6 +2159,36 @@ export default function ProjectDevelopmentView({
|
||||
: { x: 48, y: 48, scale: 1 },
|
||||
[activePageCategory, resourceCanvasViewports, sortMode],
|
||||
);
|
||||
// 总览摞要预览"进入后离视口中心最近的几张卡",这样入场动画才有对应的终点,
|
||||
// 返回总览时也不会跳成另一张图。
|
||||
const resourceBookCategoryViewCenters = useMemo(() => {
|
||||
const canvasSize =
|
||||
resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0
|
||||
? resourceBookSceneSize
|
||||
: {
|
||||
width: RESOURCE_CANVAS_FALLBACK_WIDTH,
|
||||
height: RESOURCE_CANVAS_FALLBACK_HEIGHT,
|
||||
};
|
||||
return new Map(
|
||||
visibleCategoryOrder.map((category) => {
|
||||
const viewport = normalizeResourceBookViewport(
|
||||
resourceCanvasViewports[sortMode][category],
|
||||
);
|
||||
return [
|
||||
category,
|
||||
{
|
||||
x: (canvasSize.width / 2 - viewport.x) / viewport.scale,
|
||||
y: (canvasSize.height / 2 - viewport.y) / viewport.scale,
|
||||
},
|
||||
] as const;
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
resourceBookSceneSize,
|
||||
resourceCanvasViewports,
|
||||
sortMode,
|
||||
visibleCategoryOrder,
|
||||
]);
|
||||
const setResourceCanvasViewport = useCallback(
|
||||
(
|
||||
category: ResourceCategory,
|
||||
@@ -5540,6 +5574,7 @@ export default function ProjectDevelopmentView({
|
||||
positions={resourcePositionById}
|
||||
cardSizes={resourceCardSizeByResourceId}
|
||||
visibleResourceIds={visibleResourceIds}
|
||||
categoryViewCenters={resourceBookCategoryViewCenters}
|
||||
cardsReady={
|
||||
sortMode !== 'dependency' ||
|
||||
resourceGraphInitializationReady
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { ResourceBookCategory } from './resourceBookModel';
|
||||
import type { ResourceCanvasCardSize } from './resourceCanvasLayoutModel';
|
||||
import {
|
||||
RESOURCE_CANVAS_CARD_HEIGHT,
|
||||
RESOURCE_CANVAS_CARD_WIDTH,
|
||||
type ResourceCanvasCardSize,
|
||||
} from './resourceCanvasLayoutModel';
|
||||
import type { ProjectResource } from './resourceProjectionModel';
|
||||
import { projectResourceTypeLabel } from './resourceProjectionModel';
|
||||
|
||||
@@ -55,6 +59,55 @@ export function groupResourceBookResourcesByCategory(
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 总览摞只铺有限张卡。挑哪几张决定了进入子画布时的观感:若按数组顺序取,
|
||||
* 用户看到的卡和入场视口里最近的卡不是同一批,FLIP 找不到对应终点,动画会
|
||||
* 退化成淡入淡出、返回时还会跳成另一张图。这里改为取离当前视图中心最近的卡。
|
||||
*/
|
||||
export function selectResourceBookOverviewCards({
|
||||
resources,
|
||||
visibleResourceIds,
|
||||
positions,
|
||||
cardSizes,
|
||||
viewCenter,
|
||||
limit = RESOURCE_BOOK_OVERVIEW_STACK_LIMIT,
|
||||
}: {
|
||||
resources: readonly ProjectResource[];
|
||||
visibleResourceIds: ReadonlySet<string>;
|
||||
positions: ReadonlyMap<string, { x: number; y: number }>;
|
||||
cardSizes: ReadonlyMap<string, ResourceCanvasCardSize>;
|
||||
viewCenter: { x: number; y: number } | undefined;
|
||||
limit?: number;
|
||||
}): ProjectResource[] {
|
||||
const visible = resources.filter((resource) =>
|
||||
visibleResourceIds.has(resource.id),
|
||||
);
|
||||
if (!viewCenter || visible.length <= limit) {
|
||||
return visible.slice(0, limit);
|
||||
}
|
||||
const distance = (resource: ProjectResource) => {
|
||||
const position = positions.get(resource.id);
|
||||
if (!position) return Number.POSITIVE_INFINITY;
|
||||
const size = cardSizes.get(resource.id) ?? {
|
||||
width: RESOURCE_CANVAS_CARD_WIDTH,
|
||||
height: RESOURCE_CANVAS_CARD_HEIGHT,
|
||||
};
|
||||
const centerX = position.x + size.width / 2;
|
||||
const centerY = position.y + size.height / 2;
|
||||
return (
|
||||
(centerX - viewCenter.x) ** 2 + (centerY - viewCenter.y) ** 2
|
||||
);
|
||||
};
|
||||
return visible
|
||||
.map((resource, index) => ({ resource, index, distance: distance(resource) }))
|
||||
.sort(
|
||||
(left, right) =>
|
||||
left.distance - right.distance || left.index - right.index,
|
||||
)
|
||||
.slice(0, limit)
|
||||
.map((entry) => entry.resource);
|
||||
}
|
||||
|
||||
export function resourceBookOverviewCardLayout({
|
||||
rect,
|
||||
stackIndex,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
groupResourceBookStacks,
|
||||
RESOURCE_BOOK_OVERVIEW_STACK_LIMIT,
|
||||
resourceBookOverviewCardLayout,
|
||||
selectResourceBookOverviewCards,
|
||||
} from '../src/view/project-development/resourceBookLayout';
|
||||
import type { ProjectResource } from '../src/view/project-development/resourceProjectionModel';
|
||||
|
||||
@@ -58,6 +59,53 @@ describe('resourceBookOverviewCardLayout', () => {
|
||||
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user