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

Draft
suzmii wants to merge 131 commits from feat/agc-canvas-resource-workbench-v3 into master
2 changed files with 99 additions and 389 deletions
Showing only changes of commit d5411eae62 - Show all commits
@@ -1,8 +1,5 @@
import type { ImageCanvasHostScope } from '@genarrative/image-canvas-core';
import {
type CanvasViewport,
resolveViewportFromWheel,
} from '@genarrative/image-canvas-core';
import type { CanvasViewport } from '@genarrative/image-canvas-core';
import {
Code2,
FileText,
@@ -113,11 +110,6 @@ import {
} from './resourceBookViewport';
import {
createResourceCanvasPageCategorySignature,
createResourceCanvasPageWheelState,
queueResourceCanvasPageWheel,
resetResourceCanvasPageWheel,
type ResourceCanvasPageWheelState,
takeResourceCanvasPageWheelStep,
} from './resourceCanvasController';
import {
createResourceCanvasCardSizeByResourceId,
@@ -417,7 +409,7 @@ export type ProjectDevelopmentViewProps = {
/**
* 本项目是从「做方案」入口建的。
*
* 只用来决定工作台在拿到第一个已登记资源之前是否收掉左的资源画布,不参与任何
* 只用来决定工作台在拿到第一个已登记资源之前是否收掉左的资源区域,不参与任何
* runtime 分流——做游戏与做素材的链路不受影响。
*/
planningStartMode?: boolean;
@@ -931,7 +923,7 @@ function ResourceBookThumbnail({
onOpen(category);
}
}}
aria-label={`进入${categoryLabels[category]}子画布`}
aria-label={`打开${categoryLabels[category]}`}
>
<span
className="game-resource-book-thumbnail-titlebar game-resource-book-thumbnail-titlebar--placeholder"
@@ -953,7 +945,7 @@ function ResourceBookThumbnail({
)}
</span>
<span className="game-resource-book-thumbnail-footer">
<span>{resources.length > 0 ? '点击进入子画布' : '打开空子画布'}</span>
<span>{categoryLabels[category]}</span>
<span aria-hidden="true"></span>
</span>
</div>
@@ -987,10 +979,10 @@ function ResourceBookTitleBar({
className="game-resource-page-collapse"
onClick={onCollapse}
aria-label="收起资源"
title="缩回主画布"
title="返回资源总览"
>
<ZoomOut size={14} aria-hidden="true" />
</button>
) : null}
</span>
@@ -1311,19 +1303,12 @@ export default function ProjectDevelopmentView({
element: HTMLMediaElement | null;
} | null>(null);
const resourceCanvasRef = useRef<HTMLDivElement>(null);
const resourceCanvasWheelHandlerRef = useRef<
((event: WheelEvent) => void) | null
>(null);
const resourceCanvasViewportRef = useRef<CanvasViewport>({
x: 48,
y: 48,
scale: 1,
});
const resourceCanvasFitKeysRef = useRef<Set<string>>(new Set());
const resourceCanvasPageWheelRef = useRef<
ResourceCanvasPageWheelState<ResourceCategory>
>(createResourceCanvasPageWheelState<ResourceCategory>());
const renderedResourceCategoryRef = useRef<ResourceCategory | null>(null);
const resourceCanvasPanRef = useRef<{
pointerId: number;
category: ResourceCategory;
@@ -1623,7 +1608,7 @@ export default function ProjectDevelopmentView({
() => resources.filter((resource) => resource.category !== 'code'),
[resources],
);
// 做方案在拿到第一个产物之前,左的资源画布是空的(文档 0 项、项目版本 0 项),而澄清
// 做方案在拿到第一个产物之前,左的资源区域是空的(文档 0 项、项目版本 0 项),而澄清
// 问答、决定卡和 GDD 审批全挤在右边那一列里。这段时间先把画布收掉让对话铺满,一旦有第
// 一个已登记资源就自动恢复双栏。用 CSS 收而不是不渲染:画布里的页签、缩放和选中状态都
// 留着,恢复双栏时不会被重置。
@@ -2152,14 +2137,7 @@ export default function ProjectDevelopmentView({
[resourcePageCategories, sortMode],
);
useLayoutEffect(() => {
const pageWheelState = resourceCanvasPageWheelRef.current;
renderedResourceCategoryRef.current = activePageCategory;
if (pageWheelState.activeCategory === null) {
pageWheelState.activeCategory = activePageCategory;
}
if (pageWheelState.activeCategory === activePageCategory) {
resourceCanvasViewportRef.current = activeResourceCanvasViewport;
}
resourceCanvasViewportRef.current = activeResourceCanvasViewport;
}, [activePageCategory, activeResourceCanvasViewport]);
const selectedResource =
canvasResources.find((resource) => resource.id === selectedResourceId) ??
@@ -2368,14 +2346,6 @@ export default function ProjectDevelopmentView({
[queueResourceSectionZoomScroll, setResourceSectionZoom],
);
const cancelQueuedResourceCanvasPageWheel = useCallback(() => {
const state = resourceCanvasPageWheelRef.current;
if (state.pendingTimerId !== null) {
window.clearTimeout(state.pendingTimerId);
}
resetResourceCanvasPageWheel(state, renderedResourceCategoryRef.current);
}, []);
const cancelResourceCanvasPan = useCallback(() => {
const pan = resourceCanvasPanRef.current;
if (!pan) {
@@ -2475,10 +2445,9 @@ export default function ProjectDevelopmentView({
]);
const openResourceBookChild = useCallback(
(category: ResourceCategory, fromWheel = false) => {
(category: ResourceCategory) => {
cancelResourceCardDrag();
cancelResourceCanvasPan();
if (!fromWheel) cancelQueuedResourceCanvasPageWheel();
if (
resourceBookState.view === 'child' &&
@@ -2491,11 +2460,6 @@ export default function ProjectDevelopmentView({
const transitionToken = resourceBookTransitionControllerRef.current.begin(
resourceBookManagerRef.current,
);
// Keep the paged-canvas projection's logical target synchronous with the
// reducer action. React may commit the visible page later, but wheel and
// focus handlers arriving in the same turn must calculate from the new
// target rather than the previous rendered category.
resourceCanvasPageWheelRef.current.activeCategory = category;
dispatchResourceBook({
type: 'open-category',
category,
@@ -2543,11 +2507,9 @@ export default function ProjectDevelopmentView({
}
setResourceCanvasViewport(category, categoryViewport);
resourceCanvasPageWheelRef.current.activeCategory = category;
setActiveResourceCategory(category);
},
[
cancelQueuedResourceCanvasPageWheel,
cancelResourceCanvasPan,
cancelResourceCardDrag,
manifest.projectId,
@@ -2567,13 +2529,11 @@ export default function ProjectDevelopmentView({
}
cancelResourceCardDrag();
cancelResourceCanvasPan();
cancelQueuedResourceCanvasPageWheel();
const transitionToken = resourceBookTransitionControllerRef.current.begin(
resourceBookManagerRef.current,
);
dispatchResourceBook({ type: 'return-to-main', token: transitionToken });
}, [
cancelQueuedResourceCanvasPageWheel,
cancelResourceCanvasPan,
cancelResourceCardDrag,
resourceBookState,
@@ -2586,37 +2546,6 @@ export default function ProjectDevelopmentView({
[openResourceBookChild],
);
const openResourceBookChildRef = useRef(openResourceBookChild);
useLayoutEffect(() => {
openResourceBookChildRef.current = openResourceBookChild;
}, [openResourceBookChild]);
const advanceResourceCanvasPage = useCallback(
(stepOffset: number) => {
const currentCategory = resourceCanvasPageWheelRef.current.activeCategory;
const normalizedStepOffset = Math.trunc(stepOffset);
if (
!currentCategory ||
resourcePageCategories.length < 2 ||
!Number.isFinite(stepOffset) ||
normalizedStepOffset === 0
) {
return;
}
const index = resourcePageCategories.indexOf(currentCategory);
const nextIndex =
(((index + normalizedStepOffset) % resourcePageCategories.length) +
resourcePageCategories.length) %
resourcePageCategories.length;
const nextCategory = resourcePageCategories[nextIndex];
if (!nextCategory) {
return;
}
openResourceBookChildRef.current(nextCategory, true);
},
[resourcePageCategories],
);
useEffect(() => {
if (mode !== 'resources') {
return undefined;
@@ -2626,48 +2555,16 @@ export default function ProjectDevelopmentView({
return undefined;
}
const zoomFrames = resourceSectionZoomFramesRef.current;
const pageWheelState = resourceCanvasPageWheelRef.current;
const pageWheelSwitchIntervalMs = 180;
const maxQueuedPageWheelSteps = Math.max(
1,
resourcePageCategories.length - 1,
);
const flushPageWheel = () => {
const state = resourceCanvasPageWheelRef.current;
const result = takeResourceCanvasPageWheelStep(
state,
performance.now(),
pageWheelSwitchIntervalMs,
);
const waitMs = result.waitMs;
if (waitMs > 0) {
if (state.pendingTimerId === null) {
const timerId = window.setTimeout(() => {
const latestState = resourceCanvasPageWheelRef.current;
if (latestState.pendingTimerId !== timerId) {
return;
}
latestState.pendingTimerId = null;
flushPageWheel();
}, waitMs);
state.pendingTimerId = timerId;
}
return;
}
if (result.stepOffset !== 0) {
advanceResourceCanvasPage(result.stepOffset);
}
};
const resolveViewport = (event: Event) => {
const target = event.target;
return target instanceof Element
? (target.closest<HTMLElement>('[data-resource-section-scroll]') ??
resourceCanvasRef.current?.querySelector<HTMLElement>(
`[data-resource-section-scroll="${renderedResourceCategoryRef.current ?? ''}"]`,
`[data-resource-section-scroll="${activePageCategory ?? ''}"]`,
) ??
null)
: (resourceCanvasRef.current?.querySelector<HTMLElement>(
`[data-resource-section-scroll="${renderedResourceCategoryRef.current ?? ''}"]`,
`[data-resource-section-scroll="${activePageCategory ?? ''}"]`,
) ?? null);
};
const wheel = (event: WheelEvent) => {
@@ -2675,44 +2572,13 @@ export default function ProjectDevelopmentView({
if (target?.closest('[data-resource-wheel-native="true"]')) {
return;
}
if (resourcePageCategories.length > 0) {
event.preventDefault();
if (event.ctrlKey || event.metaKey) {
resourceBookTransitionControllerRef.current.settle();
cancelQueuedResourceCanvasPageWheel();
const viewport = resolveViewport(event);
const category = viewport?.dataset.resourceSectionScroll as
| ResourceCategory
| undefined;
if (!viewport || !category) {
return;
}
const rect = viewport.getBoundingClientRect();
const result = resolveViewportFromWheel({
viewport: resourceCanvasViewportRef.current,
deltaX: event.deltaX,
deltaY: event.deltaY,
shiftKey: event.shiftKey,
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
screenPoint: {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
},
});
setResourceCanvasViewport(category, result.viewport, true);
return;
}
if (!Number.isFinite(event.deltaY) || event.deltaY === 0) {
return;
}
queueResourceCanvasPageWheel(resourceCanvasPageWheelRef.current, {
deltaY: event.deltaY,
deltaMode: event.deltaMode,
canvasHeight: canvas.clientHeight,
maxQueuedSteps: maxQueuedPageWheelSteps,
});
flushPageWheel();
if (
resourcePageCategories.length > 0 &&
!event.ctrlKey &&
!event.metaKey
) {
// Ordinary wheel input is handled by the resource-book manager and
// pans the active resource view instead of changing sections.
return;
}
if (!event.ctrlKey && !event.metaKey) {
@@ -2736,11 +2602,9 @@ export default function ProjectDevelopmentView({
event.clientY,
);
};
resourceCanvasWheelHandlerRef.current = wheel;
const gestureStart = (rawEvent: Event) => {
resourceBookTransitionControllerRef.current.settle();
const event = rawEvent as WebKitGestureEvent;
cancelQueuedResourceCanvasPageWheel();
const viewport = resolveViewport(event);
const category = viewport?.dataset.resourceSectionScroll as
| ResourceCategory
@@ -2806,33 +2670,21 @@ export default function ProjectDevelopmentView({
canvas.addEventListener('gestureend', gestureEnd, { passive: false });
return () => {
canvas.removeEventListener('wheel', wheel);
if (resourceCanvasWheelHandlerRef.current === wheel) {
resourceCanvasWheelHandlerRef.current = null;
}
canvas.removeEventListener('gesturestart', gestureStart);
canvas.removeEventListener('gesturechange', gestureChange);
canvas.removeEventListener('gestureend', gestureEnd);
resourceSectionGestureRef.current = null;
if (pageWheelState.pendingTimerId !== null) {
window.clearTimeout(pageWheelState.pendingTimerId);
}
resetResourceCanvasPageWheel(
pageWheelState,
renderedResourceCategoryRef.current,
);
for (const pending of zoomFrames.values()) {
window.cancelAnimationFrame(pending.frameId);
}
zoomFrames.clear();
};
}, [
advanceResourceCanvasPage,
applyAnchoredResourceSectionZoom,
cancelQueuedResourceCanvasPageWheel,
activePageCategory,
mode,
queueResourceSectionZoomScroll,
resourcePageCategories.length,
setResourceCanvasViewport,
setResourceSectionZoom,
sortMode,
]);
@@ -3047,10 +2899,6 @@ export default function ProjectDevelopmentView({
setResourceCanvasViewports(defaultViewports);
resourceCanvasFitKeysRef.current.clear();
resourceCanvasViewportRef.current = defaultViewports.dependency.document;
if (resourceCanvasPageWheelRef.current.pendingTimerId !== null) {
window.clearTimeout(resourceCanvasPageWheelRef.current.pendingTimerId);
}
resetResourceCanvasPageWheel(resourceCanvasPageWheelRef.current, null);
cancelResourceCardDrag();
resourceCanvasPanRef.current = null;
resourceDependencyOverlayRef.current?.clearDragPreview();
@@ -3508,7 +3356,6 @@ export default function ProjectDevelopmentView({
const zoomResourceBookBy = useCallback(
(factor: number) => {
resourceBookTransitionControllerRef.current.settle();
cancelQueuedResourceCanvasPageWheel();
const sceneElement = resourceBookManagerRef.current;
const rect = sceneElement?.getBoundingClientRect();
const anchorX = rect ? rect.width / 2 : 0;
@@ -3540,20 +3387,12 @@ export default function ProjectDevelopmentView({
activePageCategory,
resourceBookView,
setResourceCanvasViewport,
cancelQueuedResourceCanvasPageWheel,
],
);
const handleResourceBookWheel = useCallback(
(event: ReactWheelEvent<HTMLDivElement> | WheelEvent) => {
if (resourceBookView !== 'main' && !(event.ctrlKey || event.metaKey)) {
const nativeEvent = 'nativeEvent' in event ? event.nativeEvent : event;
resourceCanvasWheelHandlerRef.current?.(nativeEvent);
nativeEvent.preventDefault();
return;
}
resourceBookTransitionControllerRef.current.settle();
cancelQueuedResourceCanvasPageWheel();
const sceneElement = resourceBookManagerRef.current;
if (!sceneElement) {
return;
@@ -3598,19 +3437,18 @@ export default function ProjectDevelopmentView({
activePageCategory,
resourceBookView,
setResourceCanvasViewport,
cancelQueuedResourceCanvasPageWheel,
],
);
useEffect(() => {
const main = resourceBookManagerRef.current?.querySelector<HTMLElement>(
'.game-resource-book-main',
);
if (!main) return;
const manager = resourceBookManagerRef.current;
if (!manager) return;
// React wheel listeners are passive; canvas gestures must suppress native
// scrolling/zooming even when all content has been panned off screen.
main.addEventListener('wheel', handleResourceBookWheel, { passive: false });
return () => main.removeEventListener('wheel', handleResourceBookWheel);
manager.addEventListener('wheel', handleResourceBookWheel, {
passive: false,
});
return () => manager.removeEventListener('wheel', handleResourceBookWheel);
}, [handleResourceBookWheel, mode]);
const handleResourceBookMainPointerDown = useCallback(
@@ -3693,7 +3531,6 @@ export default function ProjectDevelopmentView({
if (!Number.isFinite(scale) || scale <= 0) {
return;
}
cancelQueuedResourceCanvasPageWheel();
event.currentTarget.setPointerCapture?.(event.pointerId);
resourceCardDragRef.current = {
pointerId: event.pointerId,
@@ -3708,7 +3545,7 @@ export default function ProjectDevelopmentView({
changed: false,
};
},
[activePageCategory, cancelQueuedResourceCanvasPageWheel],
[activePageCategory],
);
const handleResourceCardPointerMove = useCallback(
@@ -3813,7 +3650,6 @@ export default function ProjectDevelopmentView({
) {
return;
}
cancelQueuedResourceCanvasPageWheel();
event.currentTarget.setPointerCapture(event.pointerId);
resourceBookTransitionControllerRef.current.settle();
resourceCanvasPanRef.current = {
@@ -3824,7 +3660,7 @@ export default function ProjectDevelopmentView({
startViewport: resourceCanvasViewportRef.current,
};
},
[activePageCategory, cancelQueuedResourceCanvasPageWheel],
[activePageCategory],
);
const handleResourceCanvasPointerMove = useCallback(
@@ -3866,7 +3702,6 @@ export default function ProjectDevelopmentView({
advanceFocusGeneration();
cancelResourceCardDrag();
cancelResourceCanvasPan();
cancelQueuedResourceCanvasPageWheel();
captureResourceListScrollPosition();
captureResourceSectionScrollPositions();
setSortMode(nextMode);
@@ -5731,7 +5566,7 @@ export default function ProjectDevelopmentView({
) : null}
<section
className={`game-resource-book-main${resourceBookState.view === 'child' ? ' is-background' : ''}`}
aria-label="资源主画布"
aria-label="资源总览"
onPointerDown={handleResourceBookMainPointerDown}
onPointerMove={handleResourceBookMainPointerMove}
onPointerUp={stopResourceBookMainPan}
@@ -5750,13 +5585,12 @@ export default function ProjectDevelopmentView({
>
<header className="game-resource-book-main-heading">
<div>
<small></small>
<h2></h2>
<h2></h2>
</div>
<span>
{canvasResources.length > 0
? `${canvasResources.length} 项正式与候选资源`
: '从子画布开始整理资源'}
: '从资源栏目开始整理资源'}
</span>
</header>
<div className="game-resource-book-main-grid">
@@ -5867,7 +5701,7 @@ export default function ProjectDevelopmentView({
</nav>
<section
className="game-resource-page"
aria-label={`${categoryLabels[activePageCategory]}资源画布`}
aria-label={categoryLabels[activePageCategory]}
>
<header
className="game-resource-page-heading"
@@ -5876,13 +5710,13 @@ export default function ProjectDevelopmentView({
<div
className="game-resource-page-canvas"
data-resource-section-scroll={activePageCategory}
aria-label={`${categoryLabels[activePageCategory]}画布内容`}
aria-label={`${categoryLabels[activePageCategory]}内容`}
>
<button
type="button"
className="game-resource-canvas-reset"
aria-label="复位资源画布"
title="复位资源画布"
aria-label="复位资源视图"
title="复位资源视图"
onClick={resetResourceCanvasViewport}
>
<RotateCcw size={15} aria-hidden="true" />
@@ -363,7 +363,7 @@ export function registerProjectWorkbenchFoundationTests() {
await openResourceBookCategory('美术资源');
const artCanvas = await screen.findByRole('region', {
name: '美术资源资源画布',
name: '美术资源',
});
const readViewport = (canvas: HTMLElement) =>
canvas
@@ -380,9 +380,7 @@ export function registerProjectWorkbenchFoundationTests() {
});
act(() => {
expect(
screen
.getByRole('button', { name: '复位资源画布' })
.dispatchEvent(event),
artCanvas.dispatchEvent(event),
).toBe(false);
});
};
@@ -396,7 +394,7 @@ export function registerProjectWorkbenchFoundationTests() {
fireEvent.click(within(outline).getByRole('button', { name: /设计文档/ }));
await openResourceBookCategory('美术资源');
const typeCanvas = await screen.findByRole('region', {
name: '美术资源资源画布',
name: '美术资源',
});
await waitFor(() =>
expect(readViewport(typeCanvas)).not.toBe(dependencyViewport),
@@ -410,7 +408,7 @@ export function registerProjectWorkbenchFoundationTests() {
fireEvent.click(screen.getByRole('button', { name: '按依赖' }));
await openResourceBookCategory('美术资源');
const restoredDependencyCanvas = await screen.findByRole('region', {
name: '美术资源资源画布',
name: '美术资源',
});
await waitFor(() =>
expect(readViewport(restoredDependencyCanvas)).toBe(dependencyViewport),
@@ -419,7 +417,7 @@ export function registerProjectWorkbenchFoundationTests() {
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
await openResourceBookCategory('美术资源');
const restoredTypeCanvas = await screen.findByRole('region', {
name: '美术资源资源画布',
name: '美术资源',
});
await waitFor(() =>
expect(readViewport(restoredTypeCanvas)).toBe(typeViewport),
@@ -430,7 +428,7 @@ export function registerProjectWorkbenchFoundationTests() {
fireEvent.click(screen.getByRole('tab', { name: '资源管理' }));
await openResourceBookCategory('美术资源');
const restoredArtCanvas = await screen.findByRole('region', {
name: '美术资源资源画布',
name: '美术资源',
});
await waitFor(() =>
expect(readViewport(restoredArtCanvas)).toBe(typeViewport),
@@ -574,7 +572,7 @@ export function registerProjectWorkbenchFoundationTests() {
const mainSceneWorld = mainScene?.querySelector<HTMLElement>(
'.game-resource-book-scene-world',
);
const mainCanvas = screen.getByRole('region', { name: '资源主画布' });
const mainCanvas = screen.getByRole('region', { name: '资源总览' });
const mainWorld = mainCanvas.querySelector<HTMLElement>(
'.game-resource-book-main-world',
)!;
@@ -714,29 +712,28 @@ export function registerProjectWorkbenchFoundationTests() {
).toEqual(['设计文档', '美术资源', '音乐音效', '项目版本']);
expect(graphResourceIds).toContain('asset:section-code');
expect(screen.queryByRole('button', { name: /section\.js/ })).toBeNull();
expect(
screen.queryByRole('region', { name: '游戏代码资源画布' }),
).toBeNull();
expect(screen.queryByRole('region', { name: '游戏代码' })).toBeNull();
expect(outline.querySelectorAll('small')).toHaveLength(0);
expect(
screen.getByRole('region', { name: '设计文档资源画布' }),
).not.toBeNull();
expect(screen.getByRole('region', { name: '设计文档' })).not.toBeNull();
expect(
screen.getByTestId('resource-dependency-overlay-document'),
).not.toBeNull();
expect(
screen.getByRole('button', { name: /下一页\s*美术资源/ }),
).not.toBeNull();
await openResourceBookCategory('设计文档');
expect(
within(outline).queryByRole('button', { name: /游戏代码/ }),
).toBeNull();
expect(
screen.queryByRole('region', { name: '游戏代码资源画布' }),
screen.queryByRole('region', { name: '游戏代码' }),
).toBeNull();
fireEvent.click(within(outline).getByRole('button', { name: /设计文档/ }));
const dispatchPageWheel = (target: Element, deltaY = 160) => {
const dispatchViewWheel = (deltaY = 160) => {
const scene = document.querySelector<HTMLElement>(
'[data-resource-book-view="child"]',
);
expect(scene).not.toBeNull();
const event = new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
@@ -744,34 +741,15 @@ export function registerProjectWorkbenchFoundationTests() {
deltaY,
});
act(() => {
target.dispatchEvent(event);
scene!.dispatchEvent(event);
});
expect(event.defaultPrevented).toBe(true);
};
dispatchPageWheel(dependencyCanvas);
dispatchViewWheel();
expect(
screen.getByRole('region', { name: '美术资源资源画布' }),
screen.getByRole('region', { name: '设计文档' }),
).not.toBeNull();
await openResourceBookCategory('设计文档');
dispatchPageWheel(
screen.getByRole('button', { name: /下一页\s*美术资源/ }),
);
await waitFor(() =>
expect(
screen.getByRole('region', { name: '美术资源资源画布' }),
).not.toBeNull(),
);
await openResourceBookCategory('设计文档');
dispatchPageWheel(screen.getByLabelText('资源依赖视图'));
await waitFor(() =>
expect(
screen.getByRole('region', { name: '美术资源资源画布' }),
).not.toBeNull(),
);
await openResourceBookCategory('设计文档');
const documentWorld = dependencyCanvas.querySelector<HTMLElement>(
'.game-resource-page-canvas[data-resource-section-scroll="document"] [data-resource-viewport]',
@@ -830,9 +808,9 @@ export function registerProjectWorkbenchFoundationTests() {
deltaY: -120,
});
let zoomWheelResult = true;
const resetButton = screen.getByRole('button', { name: '复位资源画布' });
const resetButton = screen.getByRole('button', { name: '复位资源视图' });
act(() => {
zoomWheelResult = resetButton.dispatchEvent(zoomWheel);
zoomWheelResult = dependencyCanvas.dispatchEvent(zoomWheel);
});
expect(zoomWheelResult).toBe(false);
expect(readViewport()[2]).toBeGreaterThan(viewportBeforeZoom[2]!);
@@ -842,9 +820,7 @@ export function registerProjectWorkbenchFoundationTests() {
await waitFor(() =>
expect(readViewport()).toEqual(documentViewportAfterZoom),
);
expect(
screen.getByRole('region', { name: '设计文档资源画布' }),
).not.toBeNull();
expect(screen.getByRole('region', { name: '设计文档' })).not.toBeNull();
fireEvent.click(
screen.getByRole('button', {
@@ -852,9 +828,9 @@ export function registerProjectWorkbenchFoundationTests() {
}),
);
expect(screen.getByRole('dialog', { name: 'section.md' })).not.toBeNull();
dispatchPageWheel(dependencyCanvas);
dispatchViewWheel();
expect(
screen.getByRole('region', { name: '美术资源资源画布' }),
screen.getByRole('region', { name: '设计文档' }),
).not.toBeNull();
fireEvent.click(
within(screen.getByRole('dialog', { name: 'section.md' })).getByRole(
@@ -873,19 +849,24 @@ export function registerProjectWorkbenchFoundationTests() {
// without waiting for a guessed CSS duration.
expect(
screen
.getByRole('region', { name: '资源主画布' })
.getByRole('region', { name: '资源总览' })
.closest('[data-resource-book-view]')
?.getAttribute('data-resource-book-transition'),
).toBe('idle');
expect(mainWorld.style.transform).toBe(mainViewportBeforeNavigation);
await openResourceBookCategory('设计文档');
dispatchPageWheel(dependencyCanvas);
const viewportBeforeWheelNavigation = readViewport();
dispatchViewWheel();
await waitFor(() =>
expect(readViewport()[1]).toBeLessThan(
viewportBeforeWheelNavigation[1]!,
),
);
expect(
screen.getByRole('region', { name: '美术资源资源画布' }),
screen.getByRole('region', { name: '设计文档' }),
).not.toBeNull();
dispatchPageWheel(dependencyCanvas);
const zoomWhilePageWheelQueued = new WheelEvent('wheel', {
const zoomAfterViewWheel = new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
ctrlKey: true,
@@ -897,46 +878,19 @@ export function registerProjectWorkbenchFoundationTests() {
act(() => {
expect(
screen
.getByRole('button', { name: '复位资源画布' })
.dispatchEvent(zoomWhilePageWheelQueued),
.getByRole('button', { name: '复位资源视图' })
.dispatchEvent(zoomAfterViewWheel),
).toBe(false);
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 220));
});
expect(
screen.getByRole('region', { name: '美术资源资源画布' }),
).not.toBeNull();
dispatchPageWheel(dependencyCanvas);
await waitFor(
() => {
expect(
screen.getByRole('region', { name: '音乐音效资源画布' }),
).not.toBeNull();
},
{ timeout: 1_000 },
() =>
expect(readViewport()[2]).toBeGreaterThan(
viewportBeforeWheelNavigation[2]!,
),
);
fireEvent.click(within(outline).getByRole('button', { name: /设计文档/ }));
act(() => {
dependencyCanvas.dispatchEvent(
new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaX: 0,
deltaY: 160,
}),
);
});
within(outline)
.getByRole('button', { name: /^项目版本/ })
.click();
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 220));
});
expect(
screen.getByRole('region', { name: '项目版本资源画布' }),
screen.getByRole('region', { name: '设计文档' }),
).not.toBeNull();
fireEvent.click(within(outline).getByRole('button', { name: /设计文档/ }));
const dragCard = screen
.getByRole('button', {
@@ -1018,125 +972,49 @@ export function registerProjectWorkbenchFoundationTests() {
expect(y).toBeLessThan(-1_000);
});
const pageWheel = new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaX: 0,
deltaY: 160,
});
let pageWheelResult = true;
act(() => {
pageWheelResult = dependencyCanvas.dispatchEvent(pageWheel);
});
expect(pageWheelResult).toBe(false);
const sectionViewportBeforeWheel = readViewport();
dispatchViewWheel(160);
await waitFor(() =>
expect(readViewport()[1]).toBeLessThan(sectionViewportBeforeWheel[1]!),
);
expect(
screen.getByRole('region', { name: '美术资源资源画布' }),
screen.getByRole('region', { name: '设计文档' }),
).not.toBeNull();
expect(
screen.getByTestId('resource-dependency-overlay-art'),
screen.getByTestId('resource-dependency-overlay-document'),
).not.toBeNull();
expect(
screen.queryByTestId('resource-dependency-overlay-document'),
screen.queryByTestId('resource-dependency-overlay-art'),
).toBeNull();
act(() => {
dependencyCanvas.dispatchEvent(
new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaX: 0,
deltaY: 160,
}),
);
});
fireEvent.pointerDown(dependencyCanvas, {
button: 0,
clientX: 100,
clientY: 100,
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 220));
});
expect(
screen.getByRole('region', { name: '美术资源资源画布' }),
).not.toBeNull();
fireEvent.pointerUp(dependencyCanvas);
act(() => {
dependencyCanvas.dispatchEvent(
new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaX: 0,
deltaY: 160,
}),
);
});
fireEvent.click(within(outline).getByRole('button', { name: /^项目版本/ }));
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 220));
});
expect(
screen.getByRole('region', { name: '项目版本资源画布' }),
).not.toBeNull();
fireEvent.click(within(outline).getByRole('button', { name: /设计文档/ }));
act(() => {
dependencyCanvas.dispatchEvent(
new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaX: 0,
deltaY: 480,
}),
);
});
await waitFor(
() => {
expect(
screen.getByRole('region', { name: '项目版本资源画布' }),
).not.toBeNull();
},
{ timeout: 2_000 },
);
fireEvent.click(within(outline).getByRole('button', { name: /美术资源/ }));
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
const typeCanvas = screen.getByLabelText('资源类型视图') as HTMLDivElement;
expect(
within(typeCanvas).getByRole('button', { name: '复位资源画布' }),
within(typeCanvas).getByRole('button', { name: '复位资源视图' }),
).not.toBeNull();
expect(
screen.getByRole('region', { name: '美术资源资源画布' }),
screen.getByRole('region', { name: '美术资源' }),
).not.toBeNull();
expect(
typeCanvas.querySelector('.game-resource-dependency-overlay'),
).toBeNull();
const typePageWheel = new WheelEvent('wheel', {
bubbles: true,
cancelable: true,
deltaX: 0,
deltaY: 160,
});
act(() => {
typeCanvas.dispatchEvent(typePageWheel);
});
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 200));
});
await waitFor(
() => {
expect(
screen.getByRole('region', { name: '音乐音效资源画布' }),
).not.toBeNull();
},
{ timeout: 1_000 },
const readTypeViewport = () =>
Number(
(typeCanvas
.querySelector<HTMLElement>('[data-resource-viewport]')
?.getAttribute('data-resource-viewport') ?? '0,0,1').split(',')[1],
);
const typeViewportBeforeWheel = readTypeViewport();
dispatchViewWheel(160);
await waitFor(() =>
expect(readTypeViewport()).toBeLessThan(typeViewportBeforeWheel),
);
fireEvent.click(within(outline).getByRole('button', { name: /^项目版本/ }));
expect(
screen.getByRole('region', { name: '项目版本资源画布' }),
screen.getByRole('region', { name: '美术资源' }),
).not.toBeNull();
expect(screen.queryByRole('region', { name: '音乐音效' })).toBeNull();
expect(screen.queryByRole('button', { name: /缩小.*分区/ })).toBeNull();
expect(screen.queryByRole('button', { name: /放大.*内容/ })).toBeNull();
}, 10_000);
@@ -1309,16 +1187,14 @@ export function registerProjectWorkbenchFoundationTests() {
.classList.contains('game-resource-canvas--paged'),
).toBe(false);
expect(screen.queryByRole('button', { name: /game\.js/ })).toBeNull();
expect(
screen.queryByRole('region', { name: '游戏代码资源画布' }),
).toBeNull();
expect(screen.queryByRole('region', { name: '游戏代码' })).toBeNull();
});
});
it('refreshes resource pages when a same-count manifest update changes category', async () => {
const manifest = createGameCreationAppManifest(
'workbench-imported-image-canvas',
'导入图片资源画布测试',
'导入图片资源测试',
);
manifest.assets = [
{