修复 AGC 资源画本框选没有任何颜色:给画本场景根补共享画布根类

- index.tsx:`ResourceBookScene` 的场景根 `.game-resource-book-scene` 补上 `genarrative-image-canvas` 类,让共享 `SelectionOverlay` 的框选 token 有定义它的祖先。
- 根因:共享选择框的 `border: 1px solid var(--genarrative-image-canvas-selection-border)` / `background: var(--genarrative-image-canvas-selection-fill)` 没有 fallback,这两个 token 只声明在 `packages/image-canvas-react/src/styles.css` 的 `.genarrative-image-canvas` 根类里,而该根类由共享 `CanvasViewport` 渲染;AGC 只渲染 `SelectionOverlay`、从不渲染根类,声明在计算值阶段被整条丢弃,用户框选时看不到任何选择框。
- 安全性:该根类只有 3 条自定义属性声明、无布局副作用,加 className 不改布局。
- 测试:新增真实 `pointerdown` / `pointermove` 框选用例,断言选择框存在、内联几何非空,且最近的 `.genarrative-image-canvas` 祖先就是画本场景根;去掉根类后该用例失败(已实测)。
- 测试:新增 CSS 声明级断言,钉住根类的描边/底色 token 存在且非透明,消费端仍按 var() 取值(jsdom 拿不到计算色,这里只能钉声明)。
- pitfalls.md:补记"复用共享画布子组件要自己渲染根类"的排查口径。
This commit is contained in:
2026-09-10 20:00:42 +08:00
parent 7e6f7555eb
commit 9196ba3c96
3 changed files with 134 additions and 1 deletions
@@ -1069,8 +1069,10 @@ function ResourceBookScene({
resourceBookState.view === 'main' ? safeMainViewport : safeViewport;
return (
// 共享 SelectionOverlay 的框选描边/底色 token 只声明在 `.genarrative-image-canvas`
// 根类里,这里把场景根补成该根类,AGC 自绘的框选才有非空颜色。
<div
className={`game-resource-book-scene game-resource-book-scene--${resourceBookState.view} game-resource-book-scene--${resourceBookState.phase}${overviewReady ? '' : ' is-measuring'}`}
className={`genarrative-image-canvas game-resource-book-scene game-resource-book-scene--${resourceBookState.view} game-resource-book-scene--${resourceBookState.phase}${overviewReady ? '' : ' is-measuring'}`}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
@@ -202,6 +202,15 @@ function styleNumber(body: string, property: string) {
return Number(new RegExp(`${property}\\s*:\\s*(\\d+)`).exec(body)?.[1]);
}
function colorAlpha(value: string) {
const match = /rgba?\(([^)]*)\)/u.exec(value);
if (!match) {
return 1;
}
const parts = match[1]!.split(',').map((part) => part.trim());
return parts.length === 4 ? Number(parts[3]) : 1;
}
export function registerProjectWorkbenchFoundationTests() {
it('renders the first project workbench slice with honest disabled run and local approval UI', () => {
const manifest = createGameCreationAppManifest(
@@ -1153,6 +1162,36 @@ export function registerProjectWorkbenchFoundationTests() {
expect(styleNumber(notice, 'z-index')).toBeGreaterThan(sceneZIndex);
});
it('keeps the shared selection overlay colours resolvable from the scene root', () => {
const sharedStyles = readFileSync(
resolve(process.cwd(), 'packages/image-canvas-react/src/styles.css'),
'utf8',
);
// 消费端 `border/background: var(...)` 没有 fallbacktoken 缺失或被写透明,框选就没有颜色。
const root = styleRuleBody(sharedStyles, '\\.genarrative-image-canvas');
const borderToken =
/--genarrative-image-canvas-selection-border:\s*([^;]+);/u.exec(root);
const fillToken =
/--genarrative-image-canvas-selection-fill:\s*([^;]+);/u.exec(root);
expect(borderToken, '框选描边 token 缺失').not.toBeNull();
expect(fillToken, '框选底色 token 缺失').not.toBeNull();
expect(borderToken![1]!.trim()).not.toMatch(/transparent|none/u);
expect(fillToken![1]!.trim()).not.toMatch(/transparent|none/u);
expect(colorAlpha(borderToken![1]!)).toBeGreaterThan(0.5);
expect(colorAlpha(fillToken![1]!)).toBeGreaterThan(0);
const overlay = styleRuleBody(
sharedStyles,
'\\.genarrative-image-canvas__selection-overlay',
);
expect(overlay).toMatch(
/border:\s*1px solid var\(--genarrative-image-canvas-selection-border\)/u,
);
expect(overlay).toMatch(
/background:\s*var\(--genarrative-image-canvas-selection-fill\)/u,
);
});
it('filters resources through the resource search box in both views', async () => {
const manifest = createGameCreationAppManifest(
'workbench-search-visibility',
@@ -1241,6 +1280,89 @@ export function registerProjectWorkbenchFoundationTests() {
);
});
it('paints the marquee selection box with the scene token root and non-empty geometry', async () => {
const manifest = createGameCreationAppManifest(
'workbench-marquee-colour',
'框选颜色测试',
);
manifest.assets = [
{
id: 'marquee-art',
kind: 'character',
mediaType: 'image/png',
localPath: 'assets/marquee-art.png',
source: { kind: 'generated', taskId: 'art-asset-plan' },
},
];
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_local_project_resource_graph') {
return resourceGraphForInputs(args);
}
if (command === 'read_local_project_resource_canvas_layout') {
return {
schemaVersion: 'game-creator-resource-layout.v1',
projectId: args?.expectedProjectId,
mode: args?.mode,
revision: 0,
positions: [],
updatedAt: 0,
};
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
render(
React.createElement(ProjectDevelopmentView, {
projectName: manifest.name,
projectPath: '/tmp/workbench-marquee-colour',
manifest,
attachments: [],
recentRunStatus: null,
recentRunStopReason: null,
supervisor: React.createElement('div', null, '项目总控'),
onHomeOpen: vi.fn(),
onProjectsOpen: vi.fn(),
}),
);
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
await openResourceBookCategory('美术资源');
const canvas = screen.getByLabelText('资源类型视图') as HTMLDivElement;
Object.defineProperties(canvas, {
setPointerCapture: { configurable: true, value: vi.fn() },
hasPointerCapture: { configurable: true, value: vi.fn(() => true) },
releasePointerCapture: { configurable: true, value: vi.fn() },
});
fireEvent.pointerDown(canvas, {
pointerId: 91,
button: 0,
clientX: 40,
clientY: 60,
});
fireEvent.pointerMove(canvas, {
pointerId: 91,
clientX: 140,
clientY: 160,
});
const overlay = document.querySelector<HTMLElement>(
'.genarrative-image-canvas__selection-overlay',
);
expect(overlay).not.toBeNull();
expect(Number.parseFloat(overlay!.style.width)).toBeGreaterThan(0);
expect(Number.parseFloat(overlay!.style.height)).toBeGreaterThan(0);
// 框选颜色 token 只声明在 `.genarrative-image-canvas` 根类上,场景根必须挂这个类,
// 否则 `border/background: var(...)` 会在计算值阶段被整条丢弃(等于没有选择框)。
const tokenRoot = overlay!.closest('.genarrative-image-canvas');
expect(tokenRoot).not.toBeNull();
expect(tokenRoot!.classList.contains('game-resource-book-scene')).toBe(
true,
);
});
it('marks newly added resources on inactive section tabs until the user opens them', async () => {
const manifest = createGameCreationAppManifest(
'workbench-resource-unread',
@@ -5117,3 +5117,12 @@
- 处理:搜索框恢复 `display: flex`,常态 `align-self: center` 居中(总览态标题在左上、分页态栏目标题栏左右两端都有内容,只有中段是空的),与缩放 Dock 同级 `z-index: 40`;提示条同样抬到场景之上。
- 同批修复(大纲):栏目大纲常态是 `opacity: 0.68` + `scale(0.86)` + 全透明底 + `svg { width: 0 }`,常态只剩 11px 文字,用户认不出那是分区入口;现在常态就给 Dock 底板、14px 栏目图标和 12px 栏目文字并高亮当前栏目,悬停/键盘聚焦只做同一套 token 的加强。**注意**:`docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md` 的「操作边界」与「验收」两条仍写着"常态缩小、只露栏目文字",该文件当时有其它未提交改动,本批没有一并改,需要后续同步。
- 关联:`apps/ai-game-creator-shell/src/styles.css``apps/ai-game-creator-shell/src/view/project-development/index.tsx``apps/ai-game-creator-shell/tests/projectResourceLiveIntegration.test.tsx`
## AGC 复用共享画布组件时要自己渲染 `.genarrative-image-canvas` 根类(2026-09-10
- 现象:AGC 资源画本里框选**完全没有颜色**——`.genarrative-image-canvas__selection-overlay` 节点在、位置尺寸也对,就是一个看不见的框。
- 原因:共享 `SelectionOverlay` 的描边/底色写成 `border: 1px solid var(--genarrative-image-canvas-selection-border)` / `background: var(--genarrative-image-canvas-selection-fill)`**没有 fallback**;这两个 token 只声明在 `packages/image-canvas-react/src/styles.css``.genarrative-image-canvas` 根类里,而该根类原本由共享 `CanvasViewport` 渲染。AGC 自绘画本时只渲染 `SelectionOverlay`、没有渲染根类,变量取不到 → 声明在计算值阶段被整条丢弃。
- 处理:给画本场景根 `.game-resource-book-scene``genarrative-image-canvas` 类(该根类只有 3 条自定义属性声明、无布局副作用,加 className 安全)。
- 排查口径:任何"复用了共享画布子组件、但没复用 `CanvasViewport`"的宿主,都要先确认这些子组件依赖的根类由谁渲染;共享 CSS 里出现没有 fallback 的 `var(--genarrative-image-canvas-*)` 时,缺根类就是静默丢样式。
- 验证:AGC 侧用真实 `pointerdown` / `pointermove` 造框选,断言选择框存在、内联几何非空,且最近的 `.genarrative-image-canvas` 祖先就是画本场景根;把根类去掉后该用例会失败(已实测)。
- 关联:`packages/image-canvas-react/src/styles.css``packages/image-canvas-react/src/SelectionOverlay.tsx``packages/image-canvas-react/src/CanvasViewport.tsx``apps/ai-game-creator-shell/src/view/project-development/index.tsx`