From 2c5ad3903e6c25cbf172208b4dd6d36faad9d9b3 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Wed, 9 Sep 2026 16:16:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=80=BB=E8=A7=88=E6=91=9E=E6=8C=89=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E8=A7=86=E5=9B=BE=E5=B0=B1=E8=BF=91=E9=80=89=E5=8D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 总览原先按资源数组顺序取前 3 张,和进入子画布后视口内最近的卡片不是同一批,FLIP 找不到对应终点就退化成淡入淡出,返回总览时还会跳成另一张图 新增 selectResourceBookOverviewCards:按卡片中心到该栏目当前视口中心的距离选最近的卡片,仍受筛选可见性约束 每栏目视口中心换算到画布坐标后传入资源画本场景 补充 resourceBookLayout 用例覆盖无视图中心、就近排序与筛选过滤 --- .../src/view/project-development/index.tsx | 47 ++++++++++++++-- .../project-development/resourceBookLayout.ts | 55 ++++++++++++++++++- .../tests/resourceBookLayout.test.ts | 48 ++++++++++++++++ 3 files changed, 143 insertions(+), 7 deletions(-) diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index c95c5342d..ed658b681 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -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; cardSizes: Map; visibleResourceIds: Set; + categoryViewCenters: Map; 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 diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceBookLayout.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceBookLayout.ts index f187029b7..2f1f753a8 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceBookLayout.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceBookLayout.ts @@ -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; + positions: ReadonlyMap; + cardSizes: ReadonlyMap; + 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, diff --git a/apps/ai-game-creator-shell/tests/resourceBookLayout.test.ts b/apps/ai-game-creator-shell/tests/resourceBookLayout.test.ts index efdcbbf02..a10cf457d 100644 --- a/apps/ai-game-creator-shell/tests/resourceBookLayout.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceBookLayout.test.ts @@ -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(); + + 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', () => {