AGC 画布:锚点让位标题栏、生成浮层贴边、拖动误触点击、读秒 100ms 与图集参考口径 #448

Merged
suzmii merged 4 commits from fix/agc-canvas-bugfix into master 2026-09-21 13:04:12 +08:00
Member

背景

来自 AGC 画布验收(Issue #411)与客户端复测反馈的四条:

  1. 「生成任务」开关在资源栏目画布页压在顶部那条栏目标题栏(「返回资源总览」)上;
  2. 生成窗口会与画布边缘碰撞:底边越过画布下沿被切掉,卡片靠左右边时浮层有一半落在画布之外;
  3. 拖动未生成的资源卡片会顺带触发卡片的点击(背景音乐、音效等所有类型);
  4. 「生成图标素材」没有 @(生成规范有),以及「生成任务」里的读秒应按 0.1s 刷新。

根因与改法(前后对照)

1) 锚点只有一份坐标,但两页顶部 chrome 不同

改前 改后
资源总览页 margin: 0.85rem(唯一一档) 新增 canvas-overview 档,仍 0.85rem
资源栏目画布页 同上 → 压在 42px 的钉死标题栏上 让开 calc(2.625rem + 0.5rem)(标题栏 42px + 间隙)
两页切换 瞬移 margin-top 过渡 180ms(prefers-reduced-motion 下关掉)

守卫是跨文件的:用例读 styles.css.game-resource-book-scene-titlebar 的真实 min-height,断言「让开的高度 > 标题栏高度」,标题栏改高度而锚点没跟就会红。

2) 浮层几何假设了「占位卡永远贴在安全带顶端」

  • 改前:可用高度只按「安全带高度 − 卡高」算;水平方向完全没有收口(浮层 560px 宽、按卡片中心居中展开)。占位卡是追加在栏目内容最下方的,落进下半屏是常态 → 浮层底边越出画布被 overflow: hidden 切掉;卡片靠边时浮层一半在画布外。
  • 改后:resolveResourceCanvasGenerationPanelPlacement 吃卡片的屏幕顶边,顶边与可用高度都由真实位置算(卡下塞不下就改为盖住占位,且顶边收进安全带);水平用 clampResourceCanvasGenerationPanelAnchorX 把锚点收进「浮层左右各留 12px」的区间。宽度口径与面板 CSS(min(560px, calc(100% - 24px)))同源,有声明级守卫钉住。
  • 取舍:这里保持屏幕尺寸(表单在任何缩放下都可读),只让位置跟着卡片与画布走。若产品要「浮层本身是一个画布世界节点、随缩放一起缩放」,是另一次改动(低缩放下表单会变小;用反向缩放抵消会带来文字发虚),本轮未做。

3) 卡片的「拖动还是点击」此前是两条链路各写一套

  • 浏览器在 pointerdown / pointerup 落在同一节点上时一定会补一次 click。资源卡早就用阈值 + 一次性抑制处理过;生成占位卡把「收到过 pointermove」当拖动信号,且没有任何抑制 → 拖一下就开合生成浮层。
  • 改后:新增共享判据 resourceCanvasGestureExceededDragThreshold(阈值仍取 RESOURCE_CANVAS_DRAG_THRESHOLD),资源卡与占位卡共用;占位卡越过阈值才置「拖动中」,并登记一次性点击抑制、由宿主在「点卡片」入口消费。
  • 关键一步(第一版没做对,已修):抑制的计时起点必须是松手那一刻。第一版在越过阈值时就起 0ms 计时器,而真实鼠标拖动横跨多轮宏任务,计时器早在松手前把抑制清掉,收尾那次 click 照样生效;jsdom 同步发 move/up 看不见这一档,所以当时用例是假绿。现按资源卡链路口径改正,并把用例补成真实时序(拖动过程中让时钟走一轮宏任务,再补浏览器在 pointerup 之后派发的 lostpointercapture,最后才是 click)。
  • 相邻一档一并收口:按在删除按钮上、松手落回卡片时,那次 click 会被派给共同祖先(卡片)→ 现在按起手点判定,不当作「点卡片」。
  • 排查范围:资源卡(类型 / 依赖 /「所有资源」页共用同一个卡渲染)已有正确实现并与占位卡统一;画本总览缩略卡(只有点击、无拖动)、画布空白(框选 / 平移)、网页端素材行(HTML5 draggable)、UI 编辑器节点变换(自带阈值 + 抑制)逐条核对过,不属于同一缺陷。

4) 图集没有 @ 是通道口径,不是漏画按钮;读秒粒度对齐对话侧

  • 图集走另一条平台路由 POST /api/external/v1/editor/icon-spritesheets/generations:客户端提交体是 referenceId(唯一一张独立权威规范图)+ iconDescriptions + 切片参数,没有 referenceImageSrcs;结果绑定阶段还硬校验「参考恰好一张、且不等于产物自身」。前端判据与 Rust platform_art_asset_kind_accepts_user_reference_assets 一致(只有图集排除),原生对图集的用户参考是显式拒绝(不是静默丢弃)。
  • 改后:不给 @ 的入口在面板里当场说明为什么(该入口只接受权威规范图,图标写在提示词里),并写进入口矩阵文档;用例遍历底部工具栏每条生成入口,断言「说明行」与「@ 选择器」严格互补、唯一例外是图集。
  • 是否放开图集用户参考属于产品 + 平台口径变更(平台 OpenAPI 该请求 schema 里其实有 referenceImageSrcs,客户端目前不发),未在本 PR 内改。
  • 读秒:「生成任务」侧栏原本自带 1000ms interval,而耗时文案本来就是 0.1 秒口径 → 看起来像卡住。改后下沉成行级时钟并复用对话侧同一份 useLiveNowLIVE_TIMER_TICK_MS = 100):秒表只订在运行中的那一行上,终态行定格在账本结束时间,面板本身不再订阅(否则每 0.1 秒重建整个侧栏)。

验证

  • 仓库根 npm run test379 文件 / 4559 通过 / 34 跳过 / 0 失败
  • npm --prefix apps/ai-game-creator-shell run typechecknpm run check:encodingnpm run check:doc-indexeslintprettier --checkgit diff --check 全过;
  • 关键回归都做了敏感性验证(临时把实现改回旧写法,确认用例真的会红):拖动收尾 click 的两种错法(计时起点、lostpointercapture 清抑制)都会红;读秒粒度把 LIVE_TIMER_TICK_MS 改回 1000 即红;改用例之前的同步版本对两种错法都会绿(所以上一版没抓住)。

未覆盖 / 待确认

  • 真实客户端观感(Tauri / 真实浏览器里的间距与贴边表现)仍待客户端验收;本机没有浏览器自动化,未提供截图或录屏。
  • 真实 Provider 的生成链路未触发(不重复付费生成)。
  • 远程 CI 现状按 Issue #431(runner / 镜像未跟上 Rust 1.98.1)暂不作为本轮门禁。
  • 图集是否放开用户参考、浮层是否改成画布世界节点:两个待产品口径确认的项,未在本 PR 内动。

关联

  • Issue #411(AGC 画布验收:生成参考、占位卡、资源名称及布局交互问题)
  • docs/project-memory/shared-memory/pitfalls.md 新增两条排障口径
  • docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md 补「参考图(@ 引用)」与「图集为什么没有 @
## 背景 来自 AGC 画布验收(Issue #411)与客户端复测反馈的四条: 1. 「生成任务」开关在**资源栏目画布页**压在顶部那条栏目标题栏(「返回资源总览」)上; 2. 生成窗口会与画布边缘碰撞:底边越过画布下沿被切掉,卡片靠左右边时浮层有一半落在画布之外; 3. 拖动未生成的资源卡片会顺带触发卡片的点击(背景音乐、音效等**所有**类型); 4. 「生成图标素材」没有 `@`(生成规范有),以及「生成任务」里的读秒应按 0.1s 刷新。 ## 根因与改法(前后对照) ### 1) 锚点只有一份坐标,但两页顶部 chrome 不同 | | 改前 | 改后 | |---|---|---| | 资源总览页 | `margin: 0.85rem`(唯一一档) | 新增 `canvas-overview` 档,仍 `0.85rem` | | 资源栏目画布页 | 同上 → 压在 42px 的钉死标题栏上 | 让开 `calc(2.625rem + 0.5rem)`(标题栏 42px + 间隙) | | 两页切换 | 瞬移 | `margin-top` 过渡 180ms(`prefers-reduced-motion` 下关掉) | 守卫是**跨文件**的:用例读 `styles.css` 里 `.game-resource-book-scene-titlebar` 的真实 `min-height`,断言「让开的高度 > 标题栏高度」,标题栏改高度而锚点没跟就会红。 ### 2) 浮层几何假设了「占位卡永远贴在安全带顶端」 - 改前:可用高度只按「安全带高度 − 卡高」算;水平方向完全没有收口(浮层 560px 宽、按卡片中心居中展开)。占位卡是追加在栏目内容最下方的,落进下半屏是常态 → 浮层底边越出画布被 `overflow: hidden` 切掉;卡片靠边时浮层一半在画布外。 - 改后:`resolveResourceCanvasGenerationPanelPlacement` 吃卡片的**屏幕顶边**,顶边与可用高度都由真实位置算(卡下塞不下就改为盖住占位,且顶边收进安全带);水平用 `clampResourceCanvasGenerationPanelAnchorX` 把锚点收进「浮层左右各留 12px」的区间。宽度口径与面板 CSS(`min(560px, calc(100% - 24px))`)同源,有声明级守卫钉住。 - 取舍:这里保持**屏幕尺寸**(表单在任何缩放下都可读),只让位置跟着卡片与画布走。若产品要「浮层本身是一个画布世界节点、随缩放一起缩放」,是另一次改动(低缩放下表单会变小;用反向缩放抵消会带来文字发虚),本轮未做。 ### 3) 卡片的「拖动还是点击」此前是两条链路各写一套 - 浏览器在 `pointerdown` / `pointerup` 落在同一节点上时**一定会补一次 `click`**。资源卡早就用阈值 + 一次性抑制处理过;生成占位卡把「收到过 pointermove」当拖动信号,且没有任何抑制 → 拖一下就开合生成浮层。 - 改后:新增共享判据 `resourceCanvasGestureExceededDragThreshold`(阈值仍取 `RESOURCE_CANVAS_DRAG_THRESHOLD`),资源卡与占位卡共用;占位卡越过阈值才置「拖动中」,并登记一次性点击抑制、由宿主在「点卡片」入口消费。 - **关键一步(第一版没做对,已修)**:抑制的**计时起点必须是松手那一刻**。第一版在越过阈值时就起 0ms 计时器,而真实鼠标拖动横跨多轮宏任务,计时器早在松手前把抑制清掉,收尾那次 click 照样生效;jsdom 同步发 move/up 看不见这一档,所以当时用例是假绿。现按资源卡链路口径改正,并把用例补成真实时序(拖动过程中让时钟走一轮宏任务,再补浏览器在 `pointerup` 之后派发的 `lostpointercapture`,最后才是 click)。 - 相邻一档一并收口:按在删除按钮上、松手落回卡片时,那次 click 会被派给共同祖先(卡片)→ 现在按**起手点**判定,不当作「点卡片」。 - 排查范围:资源卡(类型 / 依赖 /「所有资源」页共用同一个卡渲染)已有正确实现并与占位卡统一;画本总览缩略卡(只有点击、无拖动)、画布空白(框选 / 平移)、网页端素材行(HTML5 draggable)、UI 编辑器节点变换(自带阈值 + 抑制)逐条核对过,不属于同一缺陷。 ### 4) 图集没有 `@` 是通道口径,不是漏画按钮;读秒粒度对齐对话侧 - 图集走另一条平台路由 `POST /api/external/v1/editor/icon-spritesheets/generations`:客户端提交体是 `referenceId`(唯一一张独立权威规范图)+ `iconDescriptions` + 切片参数,**没有** `referenceImageSrcs`;结果绑定阶段还硬校验「参考恰好一张、且不等于产物自身」。前端判据与 Rust `platform_art_asset_kind_accepts_user_reference_assets` 一致(只有图集排除),原生对图集的用户参考是**显式拒绝**(不是静默丢弃)。 - 改后:不给 `@` 的入口在面板里**当场说明为什么**(该入口只接受权威规范图,图标写在提示词里),并写进入口矩阵文档;用例遍历底部工具栏每条生成入口,断言「说明行」与「@ 选择器」严格互补、唯一例外是图集。 - 是否放开图集用户参考属于产品 + 平台口径变更(平台 OpenAPI 该请求 schema 里其实有 `referenceImageSrcs`,客户端目前不发),未在本 PR 内改。 - 读秒:「生成任务」侧栏原本自带 1000ms interval,而耗时文案本来就是 0.1 秒口径 → 看起来像卡住。改后下沉成**行级**时钟并复用对话侧同一份 `useLiveNow`(`LIVE_TIMER_TICK_MS = 100`):秒表只订在运行中的那一行上,终态行定格在账本结束时间,面板本身不再订阅(否则每 0.1 秒重建整个侧栏)。 ## 验证 - 仓库根 `npm run test`:**379 文件 / 4559 通过 / 34 跳过 / 0 失败**; - `npm --prefix apps/ai-game-creator-shell run typecheck`、`npm run check:encoding`、`npm run check:doc-index`、`eslint`、`prettier --check`、`git diff --check` 全过; - 关键回归都做了**敏感性验证**(临时把实现改回旧写法,确认用例真的会红):拖动收尾 click 的两种错法(计时起点、`lostpointercapture` 清抑制)都会红;读秒粒度把 `LIVE_TIMER_TICK_MS` 改回 1000 即红;改用例之前的同步版本对两种错法都会绿(所以上一版没抓住)。 ## 未覆盖 / 待确认 - 真实客户端观感(Tauri / 真实浏览器里的间距与贴边表现)仍待客户端验收;本机没有浏览器自动化,未提供截图或录屏。 - 真实 Provider 的生成链路未触发(不重复付费生成)。 - 远程 CI 现状按 Issue #431(runner / 镜像未跟上 Rust 1.98.1)暂不作为本轮门禁。 - 图集是否放开用户参考、浮层是否改成画布世界节点:两个待产品口径确认的项,未在本 PR 内动。 ## 关联 - Issue #411(AGC 画布验收:生成参考、占位卡、资源名称及布局交互问题) - `docs/project-memory/shared-memory/pitfalls.md` 新增两条排障口径 - `docs/technical/【AGC】栏目画布底部工具栏入口矩阵-2026-09-13.md` 补「参考图(`@` 引用)」与「图集为什么没有 `@`」
suzmii added 4 commits 2026-09-21 12:54:35 +08:00
- 生成任务锚点新增「资源总览」档:资源总览页仍贴画布顶边内缩 0.85rem,资源栏目画布页让开顶部钉住的整宽栏目标题栏(42px + 0.5rem),两档切换走 margin-top 过渡(减动效下关掉)。
- 生成浮层的顶边与可用高度改为按占位卡的真实屏幕位置算(卡下沿到安全底边),不再假设卡片贴在安全带上沿——占位落到下半屏时浮层底边不再越过画布下沿被切掉。
- 生成浮层新增水平收口:按「浮层左右各留 12px」把锚点收进画布可视范围,卡片靠边时浮层不再有一半落到画布之外;宽度口径与面板 CSS 同源(min(560px, 100% - 24px))。
- 新增画布卡片手势的共享判据 resourceCanvasGestureExceededDragThreshold(阈值仍取 RESOURCE_CANVAS_DRAG_THRESHOLD):资源卡与生成占位卡共用同一份「拖动还是点击」判断。
- 生成占位卡修掉拖动收尾误触点击:越过阈值才算拖动、登记一次性点击抑制(consumeDragClick)由宿主消费,「拖动中」样式也从越过阈值那一刻起才亮。
- 生成占位卡的删除按钮起手手势不再被当成「点卡片」:按起手点判定,松手落回卡片时不触发开合生成浮层。
- 图标规范面板的参考选择注释按现行判据改写(只有图集 icon-spritesheet 不接受用户参考),入口矩阵文档同步补「参考图(@ 引用)」口径并修掉「本地 IPC 没有参考图入参」的旧说法。
- 补齐回归用例:手势模型判据、占位卡拖动/点击与阈值、宿主级「拖完不弹浮层且普通点击仍开合」、浮层顶边与水平收口、锚点分档与跨文件标题栏高度守卫、逐条生成入口的 @ 与判据一致、图标规范面板的 @ 与提交带引用。
- 共享记忆 pitfalls.md 记录两条排障口径:卡类手势必须只有一份阈值与点击抑制判据;画布浮层几何只能按真实屏幕位置算、分页锚点必须让开钉死的标题栏。
- 生成浮层:不给 @ 选择器的入口新增一段说明行(`data-resource-canvas-generation-reference-free`),讲清「该入口走平台图集通道,参考只有一张权威规范图(图标规范),不支持另挑参考图」——验收现场那条「生成图标素材没法 @」读起来像漏了按钮,原因其实在通道上。
- 样式:说明行允许折行、沿用次要文字色;与既有的「参考图 n/5」计数行区分开。
- 用例:图集入口断言说明行在场;逐条入口断言「说明行」与「@ 选择器」严格互补;原「不呈现参考选择」的判据从按词匹配改成按计数元素/计数值匹配(说明行本身会提到「参考图」三个字)。
- 文档:入口矩阵补「图集为什么没有 @」一条,写明图集走 `icon-spritesheets` 通道、提交体只有 referenceId + iconDescriptions(无 referenceImageSrcs)、结果绑定硬校验参考恰好一张,以及「平台 OpenAPI 该请求 schema 有 referenceImageSrcs 字段,放开属于产品 + 平台口径变更」。
- useResourceCanvasGenerationPlaceholders:抑制状态在越过拖动阈值时只登记、不起计时器,改到 pointerup 才起 0ms 计时(deferDragClickSuppressionCleanup);pointercancel 与切项目走 clearDragClickSuppression 立刻收干净。
- 原实现(越过阈值时就起 0ms 计时器)在真实鼠标下不成立:一次拖动里 pointermove 横跨多轮宏任务,计时器早在松手前就把抑制清掉,收尾那次 click 照样生效——jsdom 同步发 move/up 看不见这一档,所以上一版用例是假绿。
- 用例补上真实时序:拖动过程中显式让时钟走一轮宏任务再松手;宿主用例还补上浏览器在 pointerup 之后补发的 lostpointercapture,并断言它不能把这次拖动的抑制一起清掉。
- 敏感性已双向验证:把计时器搬回「越过阈值那一刻」、以及让 lostpointercapture 清抑制,两条用例都会红;改用例前的同步版本对两种错法都会绿(所以上一版没抓住)。
- 注释写明计时起点为什么是松手那一刻,并与资源卡链路的 deferSkippedResourceCardClickCleanup 对齐口径。
「生成任务」侧栏读秒改为 100ms 粒度:与对话侧共用同一份时钟
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m57s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 4m2s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 7m15s
Project CI / Repository checks (pull_request) Failing after 20s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 8m16s
Project CI / Frontend tests (pull_request) Successful in 7m30s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m1s
Project CI / Native shell tests (pull_request) Successful in 11m1s
562e1ba586
- ResourceCanvasAssetGenerationTasksPanelView:删掉面板自带的 1000ms interval 与 nowMillis 状态;两条任务行的「已耗时」下沉为叶子组件(AssetGenerationElapsedLabel / ResourceEditElapsedLabel),各自订阅共享 hook useLiveNow(LIVE_TIMER_TICK_MS = 100)。
- 为什么不是面板级时钟:面板上通常同时挂着多条任务,100ms 的 tick 落在面板上等于每 0.1 秒重建整个侧栏;useLiveNow 的注释明确要求调用方是叶子节点。终态行不订阅,已耗时定格在账本给的结束时间上。
- 行为:不足一分钟的耗时本来就显示到 0.1 秒(formatElapsedDuration),现在每 100ms 真的更新一次,不再是「带小数却 1 秒跳一格」。
- 用例:新增「读秒按 100ms 刷新(0.0 → 0.1 → 0.5 秒)」「终态行不再走表,定格在结束时间」「秒表只订在运行中的那一行上(setInterval 只被调用一次)」;把 LIVE_TIMER_TICK_MS 临时改回 1000 时第一条用例即红,粒度确实被钉住。
suzmii merged commit 4ad131d74d into master 2026-09-21 13:04:12 +08:00
suzmii deleted branch fix/agc-canvas-bugfix 2026-09-21 13:04:12 +08:00
Sign in to join this conversation.