WIP: AGC 资源画布与替换改造 V3.0 #316

Draft
suzmii wants to merge 131 commits from feat/agc-canvas-resource-workbench-v3 into master
3 changed files with 143 additions and 7 deletions
Showing only changes of commit 2c5ad3903e - Show all commits
@@ -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', () => {