diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 730465ac..93ddf685 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-05-27 生成页总进度圆弧锁定固定画布 + +- 背景:多轮圆环角度微调后,`GenerationProgressHero` 的 SVG 圆弧仍会出现底部开口偏斜的问题,且圆环还会随着容器宽度伸缩,导致 UI 看起来时大时小、位置漂移。 +- 决策:共用 `GenerationProgressHero` 的 SVG 圆弧起始角固定为 `135deg`,轨道和橘黄色填充都从同一个对称起点 `rotate(135 200 200)` 出发;`270deg` 扫描角配合正下方 `90deg` 留空,圆环本体改为固定 `400x400` 画布,不再跟随页面宽度缩放,外层布局只负责定位,不负责改动圆环样式。 +- 影响范围:`src/components/GenerationProgressHero.tsx`、共用 `CustomWorldGenerationView`、汪汪声浪 `BarkBattleGeneratingView` 以及生成页圆环布局文档。 +- 验证方式:`CustomWorldGenerationView` 和 `BarkBattleGeneratingView` 测试断言 `data-ring-start-degrees=135`、`data-ring-fill-start-degrees=135`,且圆环容器固定为 `h-[400px] w-[400px]`,track / fill transform 都是 `rotate(135 200 200)`。 +- 关联文档:`docs/【玩法创作】生成页圆环布局口径-2026-05-23.md`。 + ## 2026-05-26 平台跨流程错误统一用可复制来源弹窗展示 - 背景:拼图等生成链路可能同时存在多个草稿或游玩实例,页面内裸错误 banner 容易让用户误以为当前正在看的拼图失败,也不方便复制完整错误给开发排查。 @@ -201,6 +209,14 @@ - 验证方式:背景 prompt 单测应包含中央禁区硬约束,试玩图中央不再出现苹果或其它主题主体。 - 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 +## 2026-05-27 敲木鱼背景 prompt 不再写中央木鱼预设 + +- 背景:背景 prompt 曾写入“木鱼预设在屏幕中央位置”,与“背景图中不包含新木鱼物品”“中央 40% 禁止出现主题主体”直接冲突,导致 image2 偶发把静态木鱼画回背景中心。 +- 决策:背景 prompt 只能写“中央主体预留区”“运行态叠放敲击物的留白区域”“只生成背景环境图”,不得再出现“木鱼预设在屏幕中央位置”或任何等价的中心主体正向描述。 +- 影响范围:`server-rs/crates/api-server/src/wooden_fish.rs`、敲木鱼 PRD、平台链路文档、背景 prompt 单测。 +- 验证方式:`wooden_fish_background_prompt_uses_hidden_image2_flow` 必须断言旧冲突句子不存在,并断言新的中央留白表述存在。 +- 关联文档:`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + ## 2026-05-21 外部 API 失败必须 OTLP 上报并落库 - 背景:图片生成等外部供应商调用失败时,仅返回 502/504 或普通日志无法支持后续按 provider、阶段和重试属性聚合排障。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index c83d63db..45d8c609 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -170,7 +170,7 @@ - 现象:苹果等主题试玩时,中央敲击物图带明显黑底;背景图中央还可能出现苹果主体,或背景环境图偶发变成纯绿色底,和“中央只叠加 hitObjectAsset”的运行态设定冲突。 - 原因:gpt-image-2 对“透明底”和“背景只做外围氛围”的遵循不稳定。若 hit object 直接入库,黑底会被当成真实像素展示;若背景 prompt 只有软描述,模型会把主题主体画进中央。第一步为了去背刻意要求绿幕图时,如果第二步参考图或 prompt 没有切断绿幕语义,背景图也可能继承纯绿色画布。 -- 处理:敲木鱼 hit object prompt 固定要求先输出 `1:1` 绿色背景主体图(纯绿色绿幕、单一 `#00FF00` 背景),再由 `api-server` 只对绿幕背景做去绿透明化;不要回到黑底 / 白底 / 透明底 prompt 后再做泛抠图。背景生成必须使用第一步抠图完成后的透明图作为参考图,并在 prompt 中显式禁止继承绿色底色、绿幕底色或纯绿色画布;背景 prompt 还要固定要求中央 40% 主体预留区干净,禁止主题主体、局部特写、轮廓影子、重复元素和主题碎片,只允许外围氛围。 +- 处理:敲木鱼 hit object prompt 固定要求先输出 `1:1` 绿色背景主体图(纯绿色绿幕、单一 `#00FF00` 背景),再由 `api-server` 只对绿幕背景做去绿透明化;不要回到黑底 / 白底 / 透明底 prompt 后再做泛抠图。背景生成必须使用第一步抠图完成后的透明图作为参考图,并在 prompt 中显式禁止继承绿色底色、绿幕底色或纯绿色画布;背景 prompt 还要固定要求中央 40% 主体预留区干净,禁止主题主体、局部特写、轮廓影子、重复元素和主题碎片,只允许外围氛围。不要在背景 prompt 写“木鱼预设在屏幕中央位置”或类似中心主体正向描述,运行态敲击物只能由前端叠放。 - 验证:`cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml`,并用花朵 / 苹果 / 玉米主题跑试玩图确认绿幕被去除、主体未被抠除、背景中央不出现主题主体,背景环境图不再出现纯绿色底。 - 关联:`server-rs/crates/api-server/src/wooden_fish.rs`、`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 @@ -182,6 +182,14 @@ - 验证:`cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml`,并重新试玩确认返回按钮只剩圆形底色和中央左箭头。 - 关联:`server-rs/crates/api-server/src/wooden_fish.rs`、`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`. +## 敲木鱼历史已发布作品缺返回按钮要补齐,不要靠推荐过滤 + +- 现象:推荐页或公开列表中的历史敲木鱼作品点击运行态时报 `敲木鱼运行态需要完整作品配置`,但这类作品的敲击物、背景、音效和飘字都已完整,只是 `backButtonAsset` 为空。 +- 原因:早期已发布作品缺少统一的默认返回按钮快照;运行态启动时如果仍直接按完整配置校验,就会把可玩的历史作品拒掉。这个问题不应通过推荐流或公开列表过滤解决。 +- 处理:`spacetime-module` 在 `start_wooden_fish_run_tx` 和 work snapshot 构建时,若作品已发布且 `generationStatus=ready`,但仅缺 `backButtonAsset`,就补写内置默认返回按钮 `/UI/11_left_arrow.png`,再继续进入运行态。默认返回按钮以 `bundled-default` 资产快照写回 work profile,字段保持 `assetId=wooden-fish-default-back-button`、`imageObjectKey=public/UI/11_left_arrow.png`。 +- 验证:历史木鱼作品点击运行态不再报完整作品配置缺失;第一次进入后,work profile 里应补出 `backButtonAsset`。 +- 关联:`server-rs/crates/spacetime-module/src/wooden_fish.rs`、`docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。 + ## 敲木鱼创作生成不要沿用 15 秒会话超时 - 现象:敲木鱼工作台点击“生成”后,前端直接提示 `请求超时:15000ms`,但后端和 VectorEngine 未必已经失败。 @@ -1567,6 +1575,8 @@ 2026-05-24 补充:生成页“预计等待 / 已耗时”卡片本身已经有标签,传给 `GenerationProgressHero` 的值只能是纯时间,例如 `4 分钟`、`1 分 15 秒`,不要再拼接“预计还需”或“已耗时”;两张时间卡也要和当前步骤卡一样保持半透明。拼图总进度初始帧必须允许显示 `0%`,不要再用 `Math.max(1, nextProgress)` 之类的保护把启动态抬到 `1%`。 +2026-05-27 补充:`generation-hero-progress-ring-fill` 里那个橘黄色小点不是背景噪点,而是 `strokeLinecap="round"` 在短弧段上的端点;当前圆环口径要求底部 `90deg` 开口居中对称,因此轨道和填充都应使用 `135deg` 起点。圆环本体现在固定为 `400x400`,排查时先看 `data-ring-start-degrees`、`data-ring-fill-start-degrees` 和容器尺寸,不要把尺寸伸缩误认成素材渲染问题。 + ## `dev:spacetime` 启动后 3101 又断开先查 publish 是否被 spacetime.json 干扰 - 现象:浏览器报 `Failed to initiate WebSocket connection`,目标为 `ws://127.0.0.1:3101/v1/database//subscribe`,端口检查发现 `3101` 没有长期监听;手动运行 `npm run dev:spacetime` 可看到 standalone 短暂启动后退出,发布阶段报 `No database target matches ''`。 diff --git a/docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md b/docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md index fa2c56f6..6bf23f8e 100644 --- a/docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md +++ b/docs/prd/【玩法创作】敲木鱼玩法模板PRD-2026-05-20.md @@ -166,11 +166,11 @@ WF-* 1. 调用 VectorEngine `/v1/images/edits`,模型固定为 `gpt-image-2`; 2. multipart 参考图固定为第一步敲击物图案抠图完成后的透明图;默认未生成新敲击物时使用内置默认敲击物图案的透明兜底图; 3. 尺寸固定竖屏 `9:16`; -4. 背景环境图只适配新敲击物主题和画风,背景中不得包含新敲击物本体,也不得增加木槌互动物品;中央主体预留区必须保持干净,画面中央 40% 区域禁止出现主题主体、主体局部特写、主体轮廓影子、重复元素或主题主体的局部碎片; +4. 背景环境图只适配新敲击物主题和画风,背景中不得包含新敲击物本体,也不得增加木槌互动物品;中央主体预留区必须保持干净,画面中央 40% 区域禁止出现主题主体、主体局部特写、主体轮廓影子、重复元素或主题主体的局部碎片;运行态的敲击物只在前端叠放,不允许出现在背景图提示词里。 5. 提示词严格使用: ```text -生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。尺寸竖屏9:16。参考图必须是第一步敲击物抠图完成后的透明图,不继承任何绿色底色、绿幕底色或纯绿色画布,并要求最终输出完整不透明的背景环境图。中央主体预留区必须保持干净,画面中央 40% 区域禁止出现主题主体、主体局部特写、主体轮廓影子、重复元素或主题主体的局部碎片;主题元素只允许出现在外围氛围,不得把主题物品画在画面中央,也不要把主题物品作为背景中心装饰。 +生成敲木鱼背景,要求主题、画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,只生成竖屏背景环境图,不生成、不描绘、不暗示新木鱼物品本体,也不要出现木槌互动物品。尺寸竖屏9:16。参考图必须是第一步敲击物抠图完成后的透明图,不继承任何绿色底色、绿幕底色或纯绿色画布,并要求最终输出完整不透明的背景环境图。中央主体预留区必须保持干净,中央区域是运行态叠放敲击物的留白区域,画面中央 40% 区域禁止出现主题主体、主体局部特写、主体轮廓影子、重复元素或主题主体的局部碎片;主题元素只允许出现在外围氛围,不得把主题物品画在画面中央,也不要把主题物品作为背景中心装饰。 主题为:(用户提供参考图或用户输入关键词) ``` @@ -384,6 +384,8 @@ finish 公开列表优先消费 `wooden_fish_gallery_card_view` 订阅缓存。公开详情如果卡片摘要不足以进入运行态,必须补读完整 work profile。 +历史已发布且 `generationStatus=ready` 的木鱼作品如果仅缺 `backButtonAsset`,运行态启动前必须补齐内置默认返回按钮 `/UI/11_left_arrow.png`,并持久写回 work profile;这类历史作品不应通过推荐流过滤隐藏。 + ## 13. 验收 1. 创作入口能看到 `敲木鱼` 模板; diff --git a/docs/【玩法创作】生成页圆环布局口径-2026-05-23.md b/docs/【玩法创作】生成页圆环布局口径-2026-05-23.md index ef97e819..760a55d7 100644 --- a/docs/【玩法创作】生成页圆环布局口径-2026-05-23.md +++ b/docs/【玩法创作】生成页圆环布局口径-2026-05-23.md @@ -12,8 +12,8 @@ - 生成页背景视频必须留在生成页容器内部,直接作为 `fixed inset-0` 的底层背景,不要再通过 portal 挂到 `document.body`;页面根容器使用 `z-[1]`、背景容器使用 `z-0`,确保顶部导航、圆环和当前步骤卡都稳定覆盖在视频之上。 - 预计等待 / 已耗时信息卡要压缩为更轻的半透明窄卡,标签使用 `9px-10px`,数值使用 `12px-13px`,字号对齐其他生成页 UI 的小字号,不再使用偏大的提示文本;卡片标题和时间值都居中显示,两个数值只展示时间本身,调用侧不要再拼接“预计还需”或“已耗时”前缀。圆环中心不再保留独立白底块,空心圆环只保留条状进度,圆弧半径继续加大,进度数字与“总进度”标题整体上移,靠近圆环上半区。 - 顶部导航区采用“返回创作中心 / 状态胶囊”结构,返回按钮使用左箭头图标,字号使用 `text-xs-sm`,状态胶囊使用 `11px-12px`,展示 `素材生成中`、`草稿生成中` 等调用侧传入文案。 -- 圆弧区域不再包独立大卡片,左右悬浮信息卡只展示“预计等待”和“已耗时”;总进度数值放在圆弧内侧偏上的位置并保持更小字号。当前圆环外径以 `w-[min(35rem,94vw)] sm:w-[52rem]` 为基准,圆弧使用 `r=166`、`strokeWidth=18` 的 SVG 描边,不再使用 `conic-gradient + mask`,避免进度条边缘模糊。 -- 圆弧描边以圆心为中心整体按 `155deg` 起始;在当前 SVG 坐标系下,这相对 `160deg` 会向左逆时针回调 `5deg`。track 和 fill 都必须共用同一个 `rotate(155 200 200)` 变换,避免只改视觉起点却让填充和轨道错位。 +- 圆弧区域不再包独立大卡片,左右悬浮信息卡只展示“预计等待”和“已耗时”;总进度数值放在圆弧内侧偏上的位置并保持更小字号。圆环本体固定在 `400x400` 的 SVG 画布上,圆弧使用 `r=166`、`strokeWidth=18` 的 SVG 描边,不再跟随页面宽度缩放,也不再使用 `conic-gradient + mask`,避免进度条边缘模糊。 +- 圆弧描边以圆心为中心整体按 `135deg` 起始;`270deg` 扫描角配合 `90deg` 正下方缺口时,轨道和填充都从同一个对称起点出发,轨道保持 `rotate(135 200 200)`,填充端点也使用 `rotate(135 200 200)`。圆环本体尺寸固定,不允许再随容器边长伸缩,只能由外层布局决定放置位置。 - 总进度标题和百分比数字必须显式高于 SVG 圆环层级渲染,避免被圆环边缘压住;圆环本身只做背景层,不抢文字层。 - 总进度标题和百分比数字要比圆环再上移一点,当前内容区上边距以 `pt-[2%]` 为准,桌面端可进一步微调到 `sm:pt-[1.5%]`,确保数字不与进度条弧线重合。 - 从作品架或刷新后的持久化生成中草稿进入生成页时,前端必须重置“展示态 startedAtMs”为进入生成页的当前时间;后端 `progressPercent` 只用于后续真实步骤推进,不得参与首帧总进度展示,避免恢复生成页首帧直接显示 `80%+`。 @@ -27,5 +27,5 @@ - `src/components/CustomWorldGenerationView.test.tsx` 覆盖圆环主视觉和单步卡片。 - `src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx` 覆盖汪汪声浪生成页对齐后的圆环布局。 - 两个生成页都应在测试里断言页面根容器层级高于背景视频容器,且背景视频确实是页面子节点,避免 portal 背景把业务 UI 压住。 -- 还应断言圆弧正下方留空、圆环中心没有独立底色块,时间卡和总进度字号缩小后仍能在桌面与移动端正常排版;同时断言时间卡 `text-center`、标题行 `justify-center`、总进度内容区上移到 `pt-[4%]`,圆弧 DOM 为 SVG,包含清晰的 track/fill circle 描边。 +- 还应断言圆弧正下方留空、圆环中心没有独立底色块,时间卡和总进度字号缩小后仍能在桌面与移动端正常排版;同时断言时间卡 `text-center`、标题行 `justify-center`、总进度内容区上移到 `pt-[2%]`,桌面端保持 `sm:pt-[1.5%]`,圆弧 DOM 为 SVG,包含清晰的 track/fill circle 描边。 - 页面在桌面和移动端都不应再出现生成步骤列表块,圆环和当前步骤卡不能被外层卡片嵌套出双层面板感。 diff --git a/server-rs/crates/api-server/src/wooden_fish.rs b/server-rs/crates/api-server/src/wooden_fish.rs index 580555b5..28e31f2c 100644 --- a/server-rs/crates/api-server/src/wooden_fish.rs +++ b/server-rs/crates/api-server/src/wooden_fish.rs @@ -758,7 +758,7 @@ fn build_wooden_fish_hit_object_prompt(prompt: &str) -> String { fn build_wooden_fish_background_prompt(prompt: &str) -> String { format!( - "生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。尺寸竖屏9:16。参考图必须是第一步敲击物抠图完成后的透明图,不继承任何绿色底色、绿幕底色或纯绿色画布,并要求最终输出完整不透明的背景环境图。中央主体预留区必须保持干净,画面中央 40% 区域禁止出现主题主体、主体局部特写、主体轮廓影子、重复元素或主题主体的局部碎片;主题元素只允许出现在外围氛围,不得把主题物品画在画面中央,也不要把主题物品作为背景中心装饰。\n主题为:{}", + "生成敲木鱼背景,要求主题、画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,只生成竖屏背景环境图,不生成、不描绘、不暗示新木鱼物品本体,也不要出现木槌互动物品。尺寸竖屏9:16。参考图必须是第一步敲击物抠图完成后的透明图,不继承任何绿色底色、绿幕底色或纯绿色画布,并要求最终输出完整不透明的背景环境图。中央主体预留区必须保持干净,中央区域是运行态叠放敲击物的留白区域,画面中央 40% 区域禁止出现主题主体、主体局部特写、主体轮廓影子、重复元素或主题主体的局部碎片;主题元素只允许出现在外围氛围,不得把主题物品画在画面中央,也不要把主题物品作为背景中心装饰。\n主题为:{}", clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT) ) } @@ -1228,14 +1228,17 @@ mod tests { fn wooden_fish_background_prompt_uses_hidden_image2_flow() { let prompt = build_wooden_fish_background_prompt("苹果"); - assert!(prompt.contains( - "生成敲木鱼背景,要求主题,画风与参考图保持高度一致,背景元素和颜色搭配与主题对应,木鱼预设在屏幕中央位置,木鱼主体周围元素保持干净,背景氛围围绕外围设计,背景环境图中不包含新木鱼物品,背景氛围中不增加木槌互动物品。" - )); + assert!(prompt.contains("只生成竖屏背景环境图")); + assert!(prompt.contains("不生成、不描绘、不暗示新木鱼物品本体")); + assert!(prompt.contains("不要出现木槌互动物品")); + assert!(!prompt.contains("木鱼预设在屏幕中央位置")); + assert!(!prompt.contains("木鱼主体周围元素保持干净")); assert!(prompt.contains("尺寸竖屏9:16")); assert!(prompt.contains("抠图完成后的透明图")); assert!(prompt.contains("不继承任何绿色底色")); assert!(prompt.contains("完整不透明的背景环境图")); assert!(prompt.contains("中央主体预留区")); + assert!(prompt.contains("中央区域是运行态叠放敲击物的留白区域")); assert!(prompt.contains("禁止出现主题主体")); assert!(prompt.contains("苹果")); assert!(prompt.contains("不得把主题物品画在画面中央")); diff --git a/server-rs/crates/spacetime-module/src/wooden_fish.rs b/server-rs/crates/spacetime-module/src/wooden_fish.rs index a8ef6954..18232d9e 100644 --- a/server-rs/crates/spacetime-module/src/wooden_fish.rs +++ b/server-rs/crates/spacetime-module/src/wooden_fish.rs @@ -13,6 +13,10 @@ use serde::Serialize; use serde::de::DeserializeOwned; use spacetimedb::AnonymousViewContext; +const DEFAULT_WOODEN_FISH_BACK_BUTTON_ASSET_ID: &str = "wooden-fish-default-back-button"; +const DEFAULT_WOODEN_FISH_BACK_BUTTON_IMAGE_SRC: &str = "/UI/11_left_arrow.png"; +const DEFAULT_WOODEN_FISH_BACK_BUTTON_IMAGE_OBJECT_KEY: &str = "public/UI/11_left_arrow.png"; + #[spacetimedb::view(accessor = wooden_fish_gallery_view, public)] pub fn wooden_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec { let mut items = ctx @@ -593,10 +597,14 @@ fn start_wooden_fish_run_tx( input: WoodenFishRunStartInput, ) -> Result { require_non_empty(&input.run_id, "wooden_fish run_id")?; - let work = find_work(ctx, &input.profile_id)?; + let stored_work = find_work(ctx, &input.profile_id)?; + let work = backfill_historical_runtime_content(&stored_work); if !is_publish_ready(&work) { return Err("敲木鱼运行态需要完整作品配置".to_string()); } + if work.back_button_asset_json != stored_work.back_button_asset_json { + replace_work(ctx, &stored_work, clone_work(&work)); + } let snapshot = WoodenFishRunSnapshot { run_id: input.run_id.clone(), profile_id: input.profile_id.clone(), @@ -740,6 +748,7 @@ fn build_session_snapshot( } fn build_work_snapshot(row: &WoodenFishWorkProfileRow) -> Result { + let row = backfill_historical_runtime_content(row); Ok(WoodenFishWorkSnapshot { work_id: row.work_id.clone(), profile_id: row.profile_id.clone(), @@ -775,7 +784,7 @@ fn build_work_snapshot(row: &WoodenFishWorkProfileRow) -> Result bool { + is_publish_ready_except_back_button(row) + && row + .back_button_asset_json + .as_deref() + .and_then(clean_optional) + .is_some() +} + +fn is_publish_ready_except_back_button(row: &WoodenFishWorkProfileRow) -> bool { !row.work_title.trim().is_empty() && !row.hit_object_asset_json.trim().is_empty() && row @@ -1016,14 +1034,40 @@ fn is_publish_ready(row: &WoodenFishWorkProfileRow) -> bool { .as_deref() .and_then(clean_optional) .is_some() - && row + && !row.hit_sound_asset_json.trim().is_empty() + && !row.floating_words_json.trim().is_empty() + && row.generation_status == WOODEN_FISH_GENERATION_READY +} + +fn backfill_historical_runtime_content(row: &WoodenFishWorkProfileRow) -> WoodenFishWorkProfileRow { + if row.publication_status != WOODEN_FISH_PUBLICATION_PUBLISHED + || !is_publish_ready_except_back_button(row) + || row .back_button_asset_json .as_deref() .and_then(clean_optional) .is_some() - && !row.hit_sound_asset_json.trim().is_empty() - && !row.floating_words_json.trim().is_empty() - && row.generation_status == WOODEN_FISH_GENERATION_READY + { + return clone_work(row); + } + + WoodenFishWorkProfileRow { + back_button_asset_json: Some(to_json_string(&default_wooden_fish_back_button_asset())), + ..clone_work(row) + } +} + +fn default_wooden_fish_back_button_asset() -> WoodenFishImageAssetSnapshot { + WoodenFishImageAssetSnapshot { + asset_id: DEFAULT_WOODEN_FISH_BACK_BUTTON_ASSET_ID.to_string(), + image_src: DEFAULT_WOODEN_FISH_BACK_BUTTON_IMAGE_SRC.to_string(), + image_object_key: DEFAULT_WOODEN_FISH_BACK_BUTTON_IMAGE_OBJECT_KEY.to_string(), + asset_object_id: DEFAULT_WOODEN_FISH_BACK_BUTTON_ASSET_ID.to_string(), + generation_provider: "bundled-default".to_string(), + prompt: "历史敲木鱼默认返回按钮".to_string(), + width: 28, + height: 28, + } } fn default_config_from_input( @@ -1288,3 +1332,82 @@ fn clone_run(row: &WoodenFishRuntimeRunRow) -> WoodenFishRuntimeRunRow { updated_at: row.updated_at, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn historical_published_work_without_back_button_gets_runtime_backfill() { + let row = published_ready_work_without_back_button(); + + assert!(!is_publish_ready(&row)); + let repaired = backfill_historical_runtime_content(&row); + let snapshot = build_work_snapshot(&repaired).expect("历史作品补齐后应可映射运行态快照"); + + assert!(is_publish_ready(&repaired)); + assert!(snapshot.publish_ready); + assert_eq!( + snapshot + .back_button_asset + .as_ref() + .map(|asset| asset.image_src.as_str()), + Some("/UI/11_left_arrow.png") + ); + } + + fn published_ready_work_without_back_button() -> WoodenFishWorkProfileRow { + let now = Timestamp::from_micros_since_unix_epoch(1_770_000_000_000_000); + WoodenFishWorkProfileRow { + profile_id: "wooden-fish-profile-history".to_string(), + work_id: "wooden-fish-profile-history".to_string(), + owner_user_id: "user-history".to_string(), + source_session_id: "wooden-fish-session-history".to_string(), + author_display_name: "敲木鱼玩家".to_string(), + work_title: "今日敲木鱼".to_string(), + work_description: String::new(), + theme_tags_json: to_json_string(&vec!["敲木鱼".to_string(), "解压".to_string()]), + hit_object_prompt: "默认敲击物图案,圆润木质质感,透明背景".to_string(), + hit_object_reference_image_src: String::new(), + hit_sound_prompt: String::new(), + hit_object_asset_json: to_json_string(&WoodenFishImageAssetSnapshot { + asset_id: "wooden-fish-hit-object-history".to_string(), + image_src: "/wooden-fish/default-hit-object.png".to_string(), + image_object_key: "public/wooden-fish/default-hit-object.png".to_string(), + asset_object_id: "wooden-fish-hit-object-history".to_string(), + generation_provider: "bundled-default".to_string(), + prompt: "默认敲击物图案".to_string(), + width: 1024, + height: 1024, + }), + hit_sound_asset_json: to_json_string(&WoodenFishAudioAssetSnapshot { + asset_id: "wooden-fish-hit-sound-history".to_string(), + audio_src: "/wooden-fish/default-hit-sound.mp3".to_string(), + audio_object_key: "public/wooden-fish/default-hit-sound.mp3".to_string(), + asset_object_id: "wooden-fish-hit-sound-history".to_string(), + source: "bundled-default".to_string(), + prompt: Some("默认木鱼音".to_string()), + duration_ms: Some(3_000), + }), + floating_words_json: to_json_string(&default_floating_words()), + cover_image_src: "/wooden-fish/default-hit-object.png".to_string(), + generation_status: WOODEN_FISH_GENERATION_READY.to_string(), + publication_status: WOODEN_FISH_PUBLICATION_PUBLISHED.to_string(), + play_count: 0, + updated_at: now, + published_at: Some(now), + background_asset_json: Some(to_json_string(&WoodenFishImageAssetSnapshot { + asset_id: "wooden-fish-background-history".to_string(), + image_src: "/generated-wooden-fish-assets/history/background/image.png".to_string(), + image_object_key: "generated-wooden-fish-assets/history/background/image.png" + .to_string(), + asset_object_id: "wooden-fish-background-history".to_string(), + generation_provider: "image2".to_string(), + prompt: "历史背景".to_string(), + width: 1024, + height: 1536, + })), + back_button_asset_json: None, + } + } +} diff --git a/src/components/CustomWorldGenerationView.test.tsx b/src/components/CustomWorldGenerationView.test.tsx index d20da96d..14971ec7 100644 --- a/src/components/CustomWorldGenerationView.test.tsx +++ b/src/components/CustomWorldGenerationView.test.tsx @@ -142,17 +142,22 @@ describe('CustomWorldGenerationView', () => { screen .getByRole('progressbar', { name: progressTitle }) .className, - ).toContain('w-[min(35rem,94vw)]'); + ).toContain('w-[400px]'); expect( screen .getByRole('progressbar', { name: progressTitle }) .className, - ).toContain('sm:w-[52rem]'); + ).toContain('h-[400px]'); expect( screen .getByRole('progressbar', { name: progressTitle }) .getAttribute('data-ring-start-degrees'), - ).toBe('155'); + ).toBe('135'); + expect( + screen + .getByRole('progressbar', { name: progressTitle }) + .getAttribute('data-ring-fill-start-degrees'), + ).toBe('135'); expect( screen .getByRole('progressbar', { name: progressTitle }) @@ -193,12 +198,12 @@ describe('CustomWorldGenerationView', () => { screen .getByTestId('generation-hero-progress-ring-track') .getAttribute('transform'), - ).toBe('rotate(155 200 200)'); + ).toBe('rotate(135 200 200)'); expect( screen .getByTestId('generation-hero-progress-ring-fill') .getAttribute('transform'), - ).toBe('rotate(155 200 200)'); + ).toBe('rotate(135 200 200)'); expect( screen .getByTestId('generation-hero-progress-ring-fill') diff --git a/src/components/GenerationProgressHero.tsx b/src/components/GenerationProgressHero.tsx index 9fa0af3a..369258a6 100644 --- a/src/components/GenerationProgressHero.tsx +++ b/src/components/GenerationProgressHero.tsx @@ -4,8 +4,16 @@ import { useEffect, useId, useRef } from 'react'; import generationHeroVideo from '../../media/create_bg_video.mp4'; -const GENERATION_PROGRESS_RING_START_DEGREES = 155; -const GENERATION_PROGRESS_RING_SWEEP_DEGREES = 270; +const GENERATION_PROGRESS_RING_GAP_DEGREES = 90; +const GENERATION_PROGRESS_RING_BOTTOM_DEGREES = 90; +// 中文注释:SVG 圆从 3 点钟方向起笔;起点放在 135deg,可让 90deg 开口居中落在正下方。 +const GENERATION_PROGRESS_RING_START_DEGREES = + GENERATION_PROGRESS_RING_BOTTOM_DEGREES + + GENERATION_PROGRESS_RING_GAP_DEGREES / 2; +const GENERATION_PROGRESS_RING_FILL_START_DEGREES = + GENERATION_PROGRESS_RING_START_DEGREES; +const GENERATION_PROGRESS_RING_SWEEP_DEGREES = + 360 - GENERATION_PROGRESS_RING_GAP_DEGREES; const GENERATION_PROGRESS_RING_VIEWBOX = 400; const GENERATION_PROGRESS_RING_CENTER = GENERATION_PROGRESS_RING_VIEWBOX / 2; const GENERATION_PROGRESS_RING_RADIUS = 166; @@ -118,7 +126,9 @@ export function GenerationProgressHero({ const safeProgress = clampGenerationProgress(progressValue); const ringGradientId = useId().replace(/:/g, ''); const ringMetrics = buildGenerationRingMetrics(safeProgress); - const ringDegrees = Math.round((safeProgress / 100) * 270); + const ringDegrees = Math.round( + (safeProgress / 100) * GENERATION_PROGRESS_RING_SWEEP_DEGREES, + ); const ringTrackDasharray = `${ringMetrics.sweepLength.toFixed(2)} ${ringMetrics.circumference.toFixed(2)}`; const ringFillDasharray = `${ringMetrics.progressLength.toFixed(2)} ${ringMetrics.circumference.toFixed(2)}`; @@ -160,16 +170,19 @@ export function GenerationProgressHero({
diff --git a/src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx b/src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx index e2d25871..e8a0ef84 100644 --- a/src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx +++ b/src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx @@ -130,12 +130,12 @@ describe('BarkBattleGeneratingView', () => { screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .className, - ).toContain('w-[min(35rem,94vw)]'); + ).toContain('w-[400px]'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .className, - ).toContain('sm:w-[52rem]'); + ).toContain('h-[400px]'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) @@ -145,7 +145,12 @@ describe('BarkBattleGeneratingView', () => { screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) .getAttribute('data-ring-start-degrees'), - ).toBe('155'); + ).toBe('135'); + expect( + screen + .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) + .getAttribute('data-ring-fill-start-degrees'), + ).toBe('135'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素材生成进度' }) @@ -186,12 +191,12 @@ describe('BarkBattleGeneratingView', () => { screen .getByTestId('generation-hero-progress-ring-track') .getAttribute('transform'), - ).toBe('rotate(155 200 200)'); + ).toBe('rotate(135 200 200)'); expect( screen .getByTestId('generation-hero-progress-ring-fill') .getAttribute('transform'), - ).toBe('rotate(155 200 200)'); + ).toBe('rotate(135 200 200)'); expect( screen .getByTestId('generation-hero-progress-ring-fill')