Merge pull request 'Refine creation progress and wooden fish runtime' (#50) from codex/refine-creation-progress-wooden-fish into master
Reviewed-on: #50 Reviewed-by: kdletters <kdletters@qq.com>
This commit was merged in pull request #50.
This commit is contained in:
@@ -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、阶段和重试属性聚合排障。
|
||||
|
||||
@@ -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/<db>/subscribe`,端口检查发现 `3101` 没有长期监听;手动运行 `npm run dev:spacetime` 可看到 standalone 短暂启动后退出,发布阶段报 `No database target matches '<db>'`。
|
||||
|
||||
@@ -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. 创作入口能看到 `敲木鱼` 模板;
|
||||
|
||||
@@ -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 描边。
|
||||
- 页面在桌面和移动端都不应再出现生成步骤列表块,圆环和当前步骤卡不能被外层卡片嵌套出双层面板感。
|
||||
|
||||
@@ -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("不得把主题物品画在画面中央"));
|
||||
|
||||
@@ -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<WoodenFishGalleryViewRow> {
|
||||
let mut items = ctx
|
||||
@@ -593,10 +597,14 @@ fn start_wooden_fish_run_tx(
|
||||
input: WoodenFishRunStartInput,
|
||||
) -> Result<WoodenFishRunSnapshot, String> {
|
||||
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<WoodenFishWorkSnapshot, String> {
|
||||
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<WoodenFishWorkS
|
||||
)),
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
publication_status: row.publication_status.clone(),
|
||||
publish_ready: is_publish_ready(row),
|
||||
publish_ready: is_publish_ready(&row),
|
||||
play_count: row.play_count,
|
||||
generation_status: row.generation_status.clone(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
@@ -1009,6 +1018,15 @@ fn insert_event(
|
||||
}
|
||||
|
||||
fn is_publish_ready(row: &WoodenFishWorkProfileRow) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative mx-auto aspect-square w-[min(35rem,94vw)] overflow-visible rounded-full sm:w-[52rem]"
|
||||
className="relative mx-auto h-[400px] w-[400px] shrink-0 overflow-visible rounded-full"
|
||||
role="progressbar"
|
||||
aria-label={title}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
aria-valuenow={safeProgress}
|
||||
data-ring-start-degrees={GENERATION_PROGRESS_RING_START_DEGREES}
|
||||
data-ring-fill-start-degrees={
|
||||
GENERATION_PROGRESS_RING_FILL_START_DEGREES
|
||||
}
|
||||
data-ring-sweep-degrees={GENERATION_PROGRESS_RING_SWEEP_DEGREES}
|
||||
data-ring-fill-degrees={ringDegrees}
|
||||
data-ring-gap-degrees={90}
|
||||
data-ring-gap-degrees={GENERATION_PROGRESS_RING_GAP_DEGREES}
|
||||
>
|
||||
<svg
|
||||
data-testid="generation-hero-progress-ring"
|
||||
@@ -214,7 +227,7 @@ export function GenerationProgressHero({
|
||||
strokeLinecap="round"
|
||||
strokeWidth={GENERATION_PROGRESS_RING_STROKE_WIDTH}
|
||||
strokeDasharray={ringFillDasharray}
|
||||
transform={`rotate(${GENERATION_PROGRESS_RING_START_DEGREES} ${GENERATION_PROGRESS_RING_CENTER} ${GENERATION_PROGRESS_RING_CENTER})`}
|
||||
transform={`rotate(${GENERATION_PROGRESS_RING_FILL_START_DEGREES} ${GENERATION_PROGRESS_RING_CENTER} ${GENERATION_PROGRESS_RING_CENTER})`}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
shapeRendering="geometricPrecision"
|
||||
/>
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user