From fbea6fdbdcf6c1e4060fea700e82126666f99852 Mon Sep 17 00:00:00 2001 From: xyh202131 <246811510+xyh202131@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:23:16 +0800 Subject: [PATCH] feat(playtest): add isolated preview workbench --- frontend/src/pages/playtest/README.md | 128 ++++ frontend/src/pages/playtest/demo-page.tsx | 18 + frontend/src/pages/playtest/index.test.tsx | 268 ++++++++ frontend/src/pages/playtest/index.tsx | 183 ++++++ .../playtest/playtest-boundaries.test.ts | 110 ++++ .../playtest/testing/demo-character.test.ts | 45 ++ .../pages/playtest/testing/demo-character.ts | 192 ++++++ .../playtest/testing/fixtures/boy/SOURCE.md | 7 + .../playtest/testing/fixtures/boy/base.png | Bin 0 -> 12196 bytes .../playtest/testing/fixtures/boy/idle-01.png | Bin 0 -> 12196 bytes .../playtest/testing/fixtures/boy/idle-02.png | Bin 0 -> 12069 bytes .../playtest/testing/fixtures/boy/idle-03.png | Bin 0 -> 11795 bytes .../playtest/testing/fixtures/boy/idle-04.png | Bin 0 -> 11910 bytes .../playtest/testing/fixtures/boy/idle-05.png | Bin 0 -> 12078 bytes .../playtest/testing/fixtures/boy/idle-06.png | Bin 0 -> 12478 bytes .../playtest/testing/fixtures/boy/idle-07.png | Bin 0 -> 12673 bytes .../playtest/testing/fixtures/boy/idle-08.png | Bin 0 -> 12156 bytes .../playtest/testing/fixtures/boy/walk-01.png | Bin 0 -> 12623 bytes .../playtest/testing/fixtures/boy/walk-02.png | Bin 0 -> 11763 bytes .../playtest/testing/fixtures/boy/walk-03.png | Bin 0 -> 11294 bytes .../playtest/testing/fixtures/boy/walk-04.png | Bin 0 -> 10956 bytes .../playtest/testing/fixtures/boy/walk-05.png | Bin 0 -> 11112 bytes .../playtest/testing/fixtures/boy/walk-06.png | Bin 0 -> 11508 bytes .../playtest/testing/fixtures/boy/walk-07.png | Bin 0 -> 11788 bytes .../playtest/testing/fixtures/boy/walk-08.png | Bin 0 -> 12282 bytes .../pages/playtest/workbench/acceptance.tsx | 87 +++ .../playtest/workbench/action-selector.tsx | 51 ++ .../workbench/analysis/frame-geometry.test.ts | 128 ++++ .../workbench/analysis/frame-geometry.ts | 178 ++++++ .../workbench/analysis/image-geometry.test.ts | 145 +++++ .../workbench/analysis/image-geometry.ts | 68 ++ .../analysis/sequence-evidence.test.ts | 275 ++++++++ .../workbench/analysis/sequence-evidence.ts | 444 +++++++++++++ .../use-frame-review-evidence.test.tsx | 205 ++++++ .../analysis/use-frame-review-evidence.ts | 110 ++++ .../workbench/animation-stage.test.tsx | 177 ++++++ .../playtest/workbench/animation-stage.tsx | 112 ++++ .../workbench/audit/audit-panel.test.tsx | 108 ++++ .../playtest/workbench/audit/audit-panel.tsx | 189 ++++++ .../workbench/audit/audit-session.test.ts | 53 ++ .../playtest/workbench/audit/audit-session.ts | 41 ++ .../workbench/export/asset-export.test.ts | 180 ++++++ .../playtest/workbench/export/asset-export.ts | 375 +++++++++++ .../workbench/export/export-panel.test.tsx | 127 ++++ .../workbench/export/export-panel.tsx | 122 ++++ .../workbench/frame-review-evidence.test.tsx | 212 +++++++ .../workbench/frame-review-evidence.tsx | 261 ++++++++ .../playtest/workbench/frame-timeline.tsx | 68 ++ .../pages/playtest/workbench/index.test.tsx | 598 ++++++++++++++++++ .../src/pages/playtest/workbench/index.tsx | 414 ++++++++++++ .../pages/playtest/workbench/inspector.tsx | 88 +++ .../model/create-preview-model.test.ts | 157 +++++ .../workbench/model/create-preview-model.ts | 78 +++ .../pages/playtest/workbench/model/types.ts | 47 ++ .../playtest/workbench/playback-controls.tsx | 123 ++++ .../workbench/playback/playback-state.test.ts | 366 +++++++++++ .../workbench/playback/playback-state.ts | 155 +++++ .../playback/use-playback-controller.test.tsx | 313 +++++++++ .../playback/use-playback-controller.ts | 155 +++++ .../playtest/workbench/stage-motion.test.tsx | 252 ++++++++ .../pages/playtest/workbench/stage-motion.ts | 136 ++++ .../pages/playtest/workbench/status-panel.tsx | 26 + .../workbench/use-playtest-keyboard.test.tsx | 265 ++++++++ .../workbench/use-playtest-keyboard.ts | 91 +++ 64 files changed, 7931 insertions(+) create mode 100644 frontend/src/pages/playtest/README.md create mode 100644 frontend/src/pages/playtest/demo-page.tsx create mode 100644 frontend/src/pages/playtest/index.test.tsx create mode 100644 frontend/src/pages/playtest/index.tsx create mode 100644 frontend/src/pages/playtest/playtest-boundaries.test.ts create mode 100644 frontend/src/pages/playtest/testing/demo-character.test.ts create mode 100644 frontend/src/pages/playtest/testing/demo-character.ts create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/SOURCE.md create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/base.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/idle-01.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/idle-02.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/idle-03.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/idle-04.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/idle-05.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/idle-06.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/idle-07.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/idle-08.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/walk-01.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/walk-02.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/walk-03.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/walk-04.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/walk-05.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/walk-06.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/walk-07.png create mode 100644 frontend/src/pages/playtest/testing/fixtures/boy/walk-08.png create mode 100644 frontend/src/pages/playtest/workbench/acceptance.tsx create mode 100644 frontend/src/pages/playtest/workbench/action-selector.tsx create mode 100644 frontend/src/pages/playtest/workbench/analysis/frame-geometry.test.ts create mode 100644 frontend/src/pages/playtest/workbench/analysis/frame-geometry.ts create mode 100644 frontend/src/pages/playtest/workbench/analysis/image-geometry.test.ts create mode 100644 frontend/src/pages/playtest/workbench/analysis/image-geometry.ts create mode 100644 frontend/src/pages/playtest/workbench/analysis/sequence-evidence.test.ts create mode 100644 frontend/src/pages/playtest/workbench/analysis/sequence-evidence.ts create mode 100644 frontend/src/pages/playtest/workbench/analysis/use-frame-review-evidence.test.tsx create mode 100644 frontend/src/pages/playtest/workbench/analysis/use-frame-review-evidence.ts create mode 100644 frontend/src/pages/playtest/workbench/animation-stage.test.tsx create mode 100644 frontend/src/pages/playtest/workbench/animation-stage.tsx create mode 100644 frontend/src/pages/playtest/workbench/audit/audit-panel.test.tsx create mode 100644 frontend/src/pages/playtest/workbench/audit/audit-panel.tsx create mode 100644 frontend/src/pages/playtest/workbench/audit/audit-session.test.ts create mode 100644 frontend/src/pages/playtest/workbench/audit/audit-session.ts create mode 100644 frontend/src/pages/playtest/workbench/export/asset-export.test.ts create mode 100644 frontend/src/pages/playtest/workbench/export/asset-export.ts create mode 100644 frontend/src/pages/playtest/workbench/export/export-panel.test.tsx create mode 100644 frontend/src/pages/playtest/workbench/export/export-panel.tsx create mode 100644 frontend/src/pages/playtest/workbench/frame-review-evidence.test.tsx create mode 100644 frontend/src/pages/playtest/workbench/frame-review-evidence.tsx create mode 100644 frontend/src/pages/playtest/workbench/frame-timeline.tsx create mode 100644 frontend/src/pages/playtest/workbench/index.test.tsx create mode 100644 frontend/src/pages/playtest/workbench/index.tsx create mode 100644 frontend/src/pages/playtest/workbench/inspector.tsx create mode 100644 frontend/src/pages/playtest/workbench/model/create-preview-model.test.ts create mode 100644 frontend/src/pages/playtest/workbench/model/create-preview-model.ts create mode 100644 frontend/src/pages/playtest/workbench/model/types.ts create mode 100644 frontend/src/pages/playtest/workbench/playback-controls.tsx create mode 100644 frontend/src/pages/playtest/workbench/playback/playback-state.test.ts create mode 100644 frontend/src/pages/playtest/workbench/playback/playback-state.ts create mode 100644 frontend/src/pages/playtest/workbench/playback/use-playback-controller.test.tsx create mode 100644 frontend/src/pages/playtest/workbench/playback/use-playback-controller.ts create mode 100644 frontend/src/pages/playtest/workbench/stage-motion.test.tsx create mode 100644 frontend/src/pages/playtest/workbench/stage-motion.ts create mode 100644 frontend/src/pages/playtest/workbench/status-panel.tsx create mode 100644 frontend/src/pages/playtest/workbench/use-playtest-keyboard.test.tsx create mode 100644 frontend/src/pages/playtest/workbench/use-playtest-keyboard.ts diff --git a/frontend/src/pages/playtest/README.md b/frontend/src/pages/playtest/README.md new file mode 100644 index 0000000..7863c51 --- /dev/null +++ b/frontend/src/pages/playtest/README.md @@ -0,0 +1,128 @@ +# Playtest + +Playtest 是角色动画的只读调试和核验界面。它既能直接操控角色验证 idle、walk、jump +之间的响应,也能逐帧查看动作、时长、位移与质量标记,并在不改变角色素材的前提下记录 +整体核验结论。 + +## 入口 + +- **Demo:`/playtest/demo`**。直接使用明确标注的少年开发 fixture,方便独立查看 idle 和 + walk 动作。 +- **正式:`/playtest/:characterId/:outfitId`**。需要由应用提供 `PlaytestPageAPIs`;页面读取 + 指定角色和最新核验结论。 + +正式入口未配置 APIs 时,页面会明确显示“Playtest 后端接口尚未配置”。它不会加载或回退到 +Demo 少年数据。 + +## 数据与只读边界 + +Demo 数据来自 `public/playtest-fixtures/boy/` 的已提取少年帧,以及 +`testing/demo-character.ts` 中的开发 fixture。素材来源和用途说明见该目录的 `SOURCE.md`。 + +Playtest 不修改角色、造型、动作、帧、工作流或版本。正式入口唯一可保存的内容是独立的 +Playtest 核验结论;Demo 中的核验状态仅保留在当前页面,不写入本地存储或后端。 + +## 统一工作台 + +页面只有一套 `PlaybackController`。动作列表、方向选择、统一舞台、播放控制、时间线, +以及右侧“帧检查 / 问题记录 / 资产导出”三个页签共同读取当前动作、方向和帧。键盘、按钮或 +时间线改变帧后,舞台与右侧工具会同步更新,不存在隐藏的第二套状态。 + +- 左侧动作栏展开宽度为 190px,也可收起为窄条;三栏在宽屏共用相同高度和底边,右侧长内容 + 在栏内滚动,不再向下撑出额外空白。 +- 按住 A/D 时临时播放 `walk` 的逐帧动画,并按各帧 `rootMotion` 移动;松开后,若按键前处于 + 暂停状态则恢复暂停,原本正在播放则继续播放。A 面向左,D 面向右。 +- 没有可播放 `walk` 时,A/D 不改变当前动作、镜像或播放状态。 +- ← / → 才是当前序列的上一帧 / 下一帧控制,长按时可连续切帧。 +- W 选择首个包含有效帧的 jump 动作并从首帧播放。 +- S 选择首个包含有效帧的 crouch 动作并从首帧播放。 +- jump/crouch 缺失时,W/S 不改变当前动作,也不会用其他动作图片冒充。 + +当前 Demo 少年包含 idle 和 walk,不包含 jump 或 crouch,因此界面会明确显示“未提供跳跃 +动作”和“未提供下蹲动作”。以后正式 Character 提供对应动作后,同一套控制器会自动启用 +W/S。 + +## 播放与核验 + +- 初始为暂停;切换动作、方向或手动选择帧时暂停。 +- 每帧的 `durationMs` 优先;未提供有效时长时才按动作 FPS 计算显示与播放间隔。 +- 关闭循环时,播放停留在末帧;开启循环时,末帧会回到首帧。 +- 页面会以无障碍实时播报当前动作、帧位置和播放状态。 +- 舞台始终只渲染当前帧角色,不叠加上一帧、下一帧或其他动作的角色虚影。 +- 有横向根位移的角色到达舞台左右边界后会自动掉头并继续播放。边界按舞台和角色的实际显示 + 宽度计算;窗口尺寸改变后会重新钳制。自动掉头不显示文字,也不触发额外播报。 + +## 逐帧自动审核依据 + +逐帧审核会在浏览器中读取每张图片的 Alpha 像素,提供只读的几何检查依据。Alpha 大于 +24 的像素视为候选前景;算法按八邻域过滤小于 `max(4 像素, 最大连通区域的 0.2%)` 的 +孤立噪点,同时始终保留最大主体。 + +右侧分别展示三类位移,不能混为一个数: + +- **画面内额外漂移**:过滤噪点后,相邻帧透明像素质心的变化;自动连续性判断只使用它。 +- **预期根位移增量**:当前帧的 `rootMotion`;它是本次自动播放帧推进应累加的增量,y 正值 + 表示向上。 +- **合成预览位移**:播放器按每次自动播放推进累计根位移后的最终位置;它用于解释播放效果, + 不重复参与自动异常判断。 + +自动审核会输出带问题代码、严重程度和帧位置的结构化结果,覆盖图片不可用、空白主体、画布 +尺寸、边缘裁切、覆盖率、相邻重复帧、位移突变、脚底/高度/面积变化,以及画面位移与根位移 +方向矛盾。重复帧依据主体边界内归一化的 8×8 Alpha/亮度指纹判断,避免小型精灵被整张透明 +画布稀释;位移异常同时使用序列中位数、MAD 和按动作设置的绝对上限,避免整段异常素材用 +自身均值掩盖问题。 + +阈值按动作类别解释:idle 严格检查脚底和高度,walk 允许周期变化,jump 允许离地,crouch +允许主体高度下降,attack/custom 使用更宽的序列离群范围。画布 256×256、主体覆盖率 65%、 +脚底漂移 3 px 和轮廓面积变化 28% 仍作为绝对安全线。 + +这些结果只能发现位置、缩放、背景和轮廓突变,不能判断变脸、人体结构、动作语义或服装 +细节。图片加载失败、完全透明、Canvas 不可用或跨域像素受限时会明确显示“无法计算”,不会 +伪造为 0。检查结果不会修改 `Frame.qc`、Character 或核验记录;当前 Action 尚无明确循环 +合同,因此不计算循环首尾接缝。 + +## 人工问题记录 + +“问题记录”页签把自动发现和人工标记分开显示。用户可以为当前动作、方向和帧选择问题类型、 +补充说明,并修改或删除人工记录。人工问题只保存在当前 React 会话,刷新后清空,不写入 +localStorage、Character、Frame 或后端。后端“核验通过 / 发现问题”的总体结论仍在独立区域 +由用户提交,不会被逐帧问题自动改变。 + +## 游戏资产导出 + +“资产导出”页签只导出状态为 `confirmed` 且包含帧的动作序列。单个 ZIP 中只有: + +1. 保留原 MIME/扩展名的逐帧原图; +2. 每个“动作 × 方向”一张 PNG Sprite Sheet; +3. 与每张 Sprite Sheet 对应的 `animation.json`。 + +Sprite Sheet 每行最多 8 帧,不缩放原图;不同尺寸使用序列最大宽高作为单元格并居中。图片 +加载失败时仍生成资产包:对应单元格保持透明,JSON 标记 `available: false`,页面提示导出 +不完整。已有自动或人工问题时,导出页会先提示问题数量但不阻断导出。切换右侧页签不会卸载 +进行中的导出,因此不能通过切换页签并发启动第二份任务。ZIP 不包含 manifest、审核结果、 +核验结论或开发说明。打包采用同一导出文件内的标准无压缩 ZIP 封装,不新增运行时依赖。 + +## 键盘控制 + +| 按键 | 操作 | +| ---------- | ------------------------------------------------ | +| A | 按住时面向左并播放 Walk;松开后恢复原播放状态 | +| D | 按住时面向右并播放 Walk;松开后恢复原播放状态 | +| W | 有 jump 动作时从首帧播放;缺少时不改变当前状态 | +| S | 有 crouch 动作时从首帧播放;缺少时不改变当前状态 | +| Space | 播放或暂停当前动作 | +| ← / → | 当前序列上一帧 / 下一帧,可长按连续切帧 | +| Home / End | 当前序列首帧 / 尾帧 | +| L | 切换循环 | + +焦点位于按钮、输入框、文本区域、选择器或可编辑文本区域时,Playtest 不拦截键盘控制。 +长按 Space、L、W 或 S 不会重复触发;A、D、方向键、Home 和 End 保持可连续触发。 + +## 视觉参考 + +三栏信息布局只参考 `live-demo-ui-components/animation-workbench.tsx` 的视觉层次。统一舞台保留 +旧工程的网格、地面线和透明背景视觉。`rootMotion` 是逐帧位移增量,只在自动播放实际推进 +时累计;它为 `null` 或零时角色原地播放。向左/向右镜像、位移累计、时间线和检查器均由同一 +`PlaybackController` 驱动,不使用前端写死的行走速度、重力或跳跃高度。Playtest 不导入 Cocos +运行时、旧 iframe 或微信小程序适配;当前导出只生成通用逐帧、Sprite Sheet 与 JSON,不生成 +Cocos 或小程序工程。 diff --git a/frontend/src/pages/playtest/demo-page.tsx b/frontend/src/pages/playtest/demo-page.tsx new file mode 100644 index 0000000..8a06c58 --- /dev/null +++ b/frontend/src/pages/playtest/demo-page.tsx @@ -0,0 +1,18 @@ +import { + PLAYTEST_DEMO_CHARACTER, + PLAYTEST_DEMO_ACTION_ID, + PLAYTEST_DEMO_OUTFIT_ID, +} from "./testing/demo-character"; +import { PlaytestWorkbench } from "./workbench"; + +/** Explicit development fixture entry point. It intentionally bypasses all production APIs. */ +export function PlaytestDemoPage() { + return ( + + ); +} diff --git a/frontend/src/pages/playtest/index.test.tsx b/frontend/src/pages/playtest/index.test.tsx new file mode 100644 index 0000000..8f5c3af --- /dev/null +++ b/frontend/src/pages/playtest/index.test.tsx @@ -0,0 +1,268 @@ +// @vitest-environment jsdom + +import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter, Route, Routes, useNavigate, type NavigateFunction } from "react-router"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { Character } from "@/entities/character"; +import type { PlaytestInspection } from "@/entities/playtest-inspection"; + +import { PlaytestPage, type PlaytestPageAPIs } from "./index"; +import { PLAYTEST_DEMO_ACTION_ID, PLAYTEST_DEMO_CHARACTER } from "./testing/demo-character"; + +function createCharacter(characterId: string, outfitId: string, name: string): Character { + return { + ...PLAYTEST_DEMO_CHARACTER, + id: characterId, + name, + outfits: PLAYTEST_DEMO_CHARACTER.outfits.map((outfit) => ({ + ...outfit, + id: outfitId, + characterId, + actions: outfit.actions.map((action) => ({ ...action, outfitId })), + })), + }; +} + +const character = createCharacter("character-1", "outfit-1", "正式角色"); + +const inspection: PlaytestInspection = { + id: "inspection-1", + characterId: "character-1", + outfitId: "outfit-1", + source: { runId: "run-1", revisionId: "revision-1" }, + status: "passed", + createdAt: "2026-07-30T00:00:00.000Z", + updatedAt: "2026-07-30T00:00:00.000Z", +}; + +function NavigationProbe({ onReady }: { onReady(navigate: NavigateFunction): void }) { + onReady(useNavigate()); + return null; +} + +function renderPage(apis?: PlaytestPageAPIs, initialEntry = "/playtest/character-1/outfit-1") { + let navigate: NavigateFunction | null = null; + const rendered = render( + + (navigate = value)} /> + + } /> + + , + ); + + return { + ...rendered, + navigate(to: string) { + if (navigate === null) throw new Error("navigation was not initialized"); + act(() => navigate?.(to)); + }, + }; +} + +function deferred() { + let resolve: (value: T) => void = () => undefined; + let reject: (reason?: unknown) => void = () => undefined; + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise; + reject = rejectPromise; + }); + return { promise, resolve, reject }; +} + +afterEach(cleanup); + +describe("PlaytestPage", () => { + it("states that the backend APIs are not configured instead of rendering the demo", () => { + renderPage(); + + expect(screen.getByText("Playtest 后端接口尚未配置")).toBeTruthy(); + expect( + screen.queryByRole("heading", { name: /少年 · Playtest 演示角色 · 默认造型/ }), + ).toBeNull(); + }); + + it("reads only the requested character and its latest inspection", async () => { + const apis: PlaytestPageAPIs = { + characters: { get: vi.fn().mockResolvedValue(character) }, + inspections: { + getLatest: vi.fn().mockResolvedValue(inspection), + record: vi.fn(), + }, + }; + + renderPage(apis); + + await waitFor(() => + expect(screen.getByRole("heading", { name: /正式角色 · 默认造型/ })).toBeTruthy(), + ); + expect(apis.characters.get).toHaveBeenCalledExactlyOnceWith("character-1"); + expect(apis.inspections.getLatest).toHaveBeenCalledExactlyOnceWith({ + characterId: "character-1", + outfitId: "outfit-1", + }); + expect(screen.getByText("run-1 / revision-1")).toBeTruthy(); + }); + + it.each([{ code: 404 }, { status: 404 }])( + "reports a missing character for a recognizable 404 error: %o", + async (notFoundError) => { + const apis: PlaytestPageAPIs = { + characters: { get: vi.fn().mockRejectedValue(notFoundError) }, + inspections: { + getLatest: vi.fn(), + record: vi.fn(), + }, + }; + + renderPage(apis); + + expect(await screen.findByText("角色不存在")).toBeTruthy(); + expect(apis.inspections.getLatest).not.toHaveBeenCalled(); + }, + ); + + it("reports a character read failure for other character errors", async () => { + const apis: PlaytestPageAPIs = { + characters: { get: vi.fn().mockRejectedValue(new Error("network unavailable")) }, + inspections: { + getLatest: vi.fn(), + record: vi.fn(), + }, + }; + + renderPage(apis); + + expect(await screen.findByText("角色读取失败")).toBeTruthy(); + expect(apis.inspections.getLatest).not.toHaveBeenCalled(); + }); + + it("reports an inspection read failure after the character was loaded", async () => { + const apis: PlaytestPageAPIs = { + characters: { get: vi.fn().mockResolvedValue(character) }, + inspections: { + getLatest: vi.fn().mockRejectedValue(new Error("inspection unavailable")), + record: vi.fn(), + }, + }; + + renderPage(apis); + + expect(await screen.findByText("核验记录读取失败")).toBeTruthy(); + expect(apis.characters.get).toHaveBeenCalledExactlyOnceWith("character-1"); + expect(apis.inspections.getLatest).toHaveBeenCalledExactlyOnceWith({ + characterId: "character-1", + outfitId: "outfit-1", + }); + expect(screen.queryByText("角色读取失败")).toBeNull(); + }); + + it("synchronizes the initial action when only the actionId query changes", async () => { + const apis: PlaytestPageAPIs = { + characters: { get: vi.fn().mockResolvedValue(character) }, + inspections: { + getLatest: vi.fn().mockResolvedValue(null), + record: vi.fn(), + }, + }; + const walkActionId = "playtest-demo-boy-walk"; + const mounted = renderPage(apis, `/playtest/character-1/outfit-1?actionId=${walkActionId}`); + + await waitFor(() => + expect(screen.getByRole("button", { name: /行走/ }).getAttribute("aria-pressed")).toBe( + "true", + ), + ); + mounted.navigate(`/playtest/character-1/outfit-1?actionId=${PLAYTEST_DEMO_ACTION_ID}`); + + await waitFor(() => + expect(screen.getByRole("button", { name: /待机/ }).getAttribute("aria-pressed")).toBe( + "true", + ), + ); + expect(apis.characters.get).toHaveBeenCalledTimes(1); + expect(apis.inspections.getLatest).toHaveBeenCalledTimes(1); + }); + + it("records a persisted conclusion through the inspection API only", async () => { + const apis: PlaytestPageAPIs = { + characters: { get: vi.fn().mockResolvedValue(character) }, + inspections: { + getLatest: vi.fn().mockResolvedValue(null), + record: vi.fn().mockResolvedValue({ ...inspection, status: "passed" }), + }, + }; + + renderPage(apis); + const recordButton = await screen.findByRole("button", { name: "核验通过" }); + fireEvent.click(recordButton); + + await waitFor(() => + expect(apis.inspections.record).toHaveBeenCalledExactlyOnceWith({ + characterId: "character-1", + outfitId: "outfit-1", + status: "passed", + }), + ); + expect(apis.characters.get).toHaveBeenCalledExactlyOnceWith("character-1"); + }); + + it("ignores an old record success after navigating to another asset", async () => { + const oldRecord = deferred(); + const character2 = createCharacter("character-2", "outfit-2", "第二个正式角色"); + const apis: PlaytestPageAPIs = { + characters: { + get: vi.fn((characterId: string) => + Promise.resolve(characterId === "character-1" ? character : character2), + ), + }, + inspections: { + getLatest: vi.fn().mockResolvedValue(null), + record: vi.fn(() => oldRecord.promise), + }, + }; + const mounted = renderPage(apis); + fireEvent.click(await screen.findByRole("button", { name: "核验通过" })); + mounted.navigate("/playtest/character-2/outfit-2"); + await screen.findByRole("heading", { name: /第二个正式角色 · 默认造型/ }); + + await act(async () => { + oldRecord.resolve({ + ...inspection, + characterId: "character-1", + outfitId: "outfit-1", + }); + await oldRecord.promise; + }); + + expect(screen.queryByText("已保存:通过")).toBeNull(); + }); + + it("ignores an old record failure after navigating to another asset", async () => { + const oldRecord = deferred(); + const character2 = createCharacter("character-2", "outfit-2", "第二个正式角色"); + const apis: PlaytestPageAPIs = { + characters: { + get: vi.fn((characterId: string) => + Promise.resolve(characterId === "character-1" ? character : character2), + ), + }, + inspections: { + getLatest: vi.fn().mockResolvedValue(null), + record: vi.fn(() => oldRecord.promise), + }, + }; + const mounted = renderPage(apis); + fireEvent.click(await screen.findByRole("button", { name: "核验通过" })); + mounted.navigate("/playtest/character-2/outfit-2"); + await screen.findByRole("heading", { name: /第二个正式角色 · 默认造型/ }); + + await act(async () => { + oldRecord.reject(new Error("old request failed")); + await oldRecord.promise.catch(() => undefined); + }); + + expect(screen.queryByText("Playtest 核验结论保存失败")).toBeNull(); + }); +}); diff --git a/frontend/src/pages/playtest/index.tsx b/frontend/src/pages/playtest/index.tsx new file mode 100644 index 0000000..9942bb2 --- /dev/null +++ b/frontend/src/pages/playtest/index.tsx @@ -0,0 +1,183 @@ +import { useEffect, useRef, useState } from "react"; +import { useParams, useSearchParams } from "react-router"; + +import type { Character, CharacterAPIs } from "@/entities/character"; +import type { + PlaytestInspection, + PlaytestInspectionAPIs, + PlaytestInspectionStatus, +} from "@/entities/playtest-inspection"; + +import { PlaytestWorkbench } from "./workbench"; + +export interface PlaytestPageAPIs { + characters: Pick; + inspections: Pick; +} + +export interface PlaytestPageProps { + apis?: PlaytestPageAPIs; +} + +interface PageData { + character: Character | null; + inspection: PlaytestInspection | null; + error: string | null; + loading: boolean; +} + +const initialPageData: PageData = { + character: null, + inspection: null, + error: null, + loading: false, +}; + +interface PageIdentity { + apis: PlaytestPageAPIs | undefined; + characterId: string | undefined; + outfitId: string | undefined; +} + +function isNotFoundError(error: unknown): boolean { + if (typeof error !== "object" || error === null) return false; + const identifiable = error as { code?: unknown; status?: unknown }; + return ( + identifiable.code === 404 || + identifiable.code === "404" || + identifiable.status === 404 || + identifiable.status === "404" + ); +} + +export function PlaytestPage({ apis }: PlaytestPageProps) { + const { characterId, outfitId } = useParams(); + const [searchParams] = useSearchParams(); + const initialActionId = searchParams.get("actionId"); + const [data, setData] = useState(initialPageData); + const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + const pageIdentityRef = useRef({ + apis, + characterId, + outfitId, + }); + const saveRequestVersionRef = useRef(0); + + useEffect(() => { + pageIdentityRef.current = { apis, characterId, outfitId }; + saveRequestVersionRef.current += 1; + + if (apis === undefined) { + setData({ ...initialPageData, error: "Playtest 后端接口尚未配置" }); + setSaving(false); + setSaveError(null); + return; + } + + if (characterId === undefined || outfitId === undefined) { + setData({ ...initialPageData, error: "Playtest 路由参数不完整" }); + setSaving(false); + setSaveError(null); + return; + } + + let cancelled = false; + setData({ ...initialPageData, loading: true }); + setSaving(false); + setSaveError(null); + + void (async () => { + let character: Character; + try { + character = await apis.characters.get(characterId); + } catch (error) { + if (!cancelled) { + setData({ + ...initialPageData, + error: isNotFoundError(error) ? "角色不存在" : "角色读取失败", + loading: false, + }); + } + return; + } + + if (cancelled) return; + + try { + const inspection = await apis.inspections.getLatest({ characterId, outfitId }); + if (!cancelled) setData({ character, inspection, error: null, loading: false }); + } catch { + if (!cancelled) { + setData({ + character, + inspection: null, + error: "核验记录读取失败", + loading: false, + }); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [apis, characterId, outfitId]); + + const recordStatus = (status: PlaytestInspectionStatus) => { + if (apis === undefined || characterId === undefined || outfitId === undefined) return; + + const requestIdentity = pageIdentityRef.current; + const requestVersion = saveRequestVersionRef.current + 1; + saveRequestVersionRef.current = requestVersion; + setSaving(true); + setSaveError(null); + void apis.inspections.record({ characterId, outfitId, status }).then( + (inspection) => { + if ( + pageIdentityRef.current !== requestIdentity || + saveRequestVersionRef.current !== requestVersion + ) + return; + setData((current) => ({ ...current, inspection })); + setSaving(false); + }, + () => { + if ( + pageIdentityRef.current !== requestIdentity || + saveRequestVersionRef.current !== requestVersion + ) + return; + setSaving(false); + setSaveError("Playtest 核验结论保存失败"); + }, + ); + }; + + if (data.error !== null) return {data.error}; + if (data.loading || data.character === null) + return 加载 Playtest 数据中; + + return ( + + ); +} + +function PlaytestPageMessage({ children }: { children: string }) { + return ( +
+

{children}

+
+ ); +} diff --git a/frontend/src/pages/playtest/playtest-boundaries.test.ts b/frontend/src/pages/playtest/playtest-boundaries.test.ts new file mode 100644 index 0000000..5254bfd --- /dev/null +++ b/frontend/src/pages/playtest/playtest-boundaries.test.ts @@ -0,0 +1,110 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, isAbsolute, join, relative, resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +const playtestDirectory = fileURLToPath(new URL(".", import.meta.url)); +const entitiesDirectory = fileURLToPath(new URL("../../entities/", import.meta.url)); +const prohibitedImports = [ + "live-demo-ui-components", + "entities/workflow-run", + "entities/generation", +] as const; +const allowedEntityImports = ["@/entities/character", "@/entities/playtest-inspection"] as const; + +function sourceFiles(directory: string): readonly string[] { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const path = join(directory, entry.name); + if (entry.isDirectory()) return sourceFiles(path); + if (!entry.isFile() || !/\.(?:ts|tsx)$/.test(entry.name) || entry.name.includes(".test.")) { + return []; + } + return [path]; + }); +} + +function allTypeScriptFiles(directory: string): readonly string[] { + return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const path = join(directory, entry.name); + if (entry.isDirectory()) return allTypeScriptFiles(path); + if (!entry.isFile() || !/\.(?:ts|tsx)$/.test(entry.name)) return []; + return [path]; + }); +} + +function moduleSpecifiers(source: string): readonly string[] { + return [...source.matchAll(/(?:from\s+|import\s*)["']([^"']+)["']/g)].map( + (match) => match[1] ?? "", + ); +} + +function entityEntry(file: string, dependency: string): string | null { + if (dependency === "@/entities") return ""; + if (dependency.startsWith("@/entities/")) return dependency.slice("@/entities/".length); + if (!dependency.startsWith(".")) return null; + + const relativeEntry = relative(entitiesDirectory, resolve(dirname(file), dependency)); + if (relativeEntry.startsWith("..") || isAbsolute(relativeEntry)) return null; + + return relativeEntry.replaceAll("\\", "/").replace(/\/index$/, ""); +} + +describe("Playtest architecture boundaries", () => { + it("does not import forbidden application or live-demo implementation layers", () => { + // Catches the isolated Playtest slice reaching into unrelated product layers. + for (const file of sourceFiles(playtestDirectory)) { + const source = readFileSync(file, "utf8"); + for (const fragment of prohibitedImports) { + expect(source, `${file} must not contain ${fragment}`).not.toContain(fragment); + } + } + }); + + it("does not import application or capabilities roots or subpaths", () => { + // Catches bypassing the isolated Page through either a barrel or a deep implementation path. + for (const file of sourceFiles(playtestDirectory)) { + const dependencies = moduleSpecifiers(readFileSync(file, "utf8")); + expect( + dependencies.filter( + (dependency) => + dependency === "@/application" || + dependency.startsWith("@/application/") || + dependency === "@/capabilities" || + dependency.startsWith("@/capabilities/"), + ), + `${file} must not import application or capabilities`, + ).toEqual([]); + } + }); + + it("imports Entity contracts only from the two approved direct entrypoints", () => { + // Catches alias or relative root barrels and deep paths that hide Playtest's dependencies. + for (const file of sourceFiles(playtestDirectory)) { + const entityImports = moduleSpecifiers(readFileSync(file, "utf8")) + .map((dependency) => ({ dependency, entry: entityEntry(file, dependency) })) + .filter((candidate) => candidate.entry !== null); + + for (const { dependency, entry } of entityImports) { + expect(allowedEntityImports, `${file} imports ${dependency}`).toContain( + `@/entities/${entry}`, + ); + } + } + }); + + it("keeps the demo fixture out of formal Playtest source", () => { + // Catches a formal route silently importing the demo character as an API fallback. + const importers = allTypeScriptFiles(playtestDirectory).filter((file) => + readFileSync(file, "utf8").includes("testing/demo-character"), + ); + + expect( + importers.every( + (file) => file === join(playtestDirectory, "demo-page.tsx") || file.includes(".test."), + ), + ).toBe(true); + expect(readFileSync(join(playtestDirectory, "index.tsx"), "utf8")).not.toContain( + "testing/demo-character", + ); + }); +}); diff --git a/frontend/src/pages/playtest/testing/demo-character.test.ts b/frontend/src/pages/playtest/testing/demo-character.test.ts new file mode 100644 index 0000000..4a4fafc --- /dev/null +++ b/frontend/src/pages/playtest/testing/demo-character.test.ts @@ -0,0 +1,45 @@ +import { existsSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +import { + PLAYTEST_DEMO_ACTION_ID, + PLAYTEST_DEMO_CHARACTER, + PLAYTEST_DEMO_OUTFIT_ID, +} from "./demo-character"; + +describe("PLAYTEST_DEMO_CHARACTER", () => { + it("provides a confirmed boy outfit with complete idle and walk fixtures", () => { + const outfit = PLAYTEST_DEMO_CHARACTER.outfits.find(({ id }) => id === PLAYTEST_DEMO_OUTFIT_ID); + + expect(PLAYTEST_DEMO_CHARACTER.outfits).toHaveLength(1); + expect(PLAYTEST_DEMO_CHARACTER.outfits[0]?.id).toBe(PLAYTEST_DEMO_OUTFIT_ID); + expect(outfit).toBeDefined(); + expect(PLAYTEST_DEMO_ACTION_ID).toBe("playtest-demo-boy-idle"); + expect(outfit!.actions.map((action) => action.type)).toEqual(["idle", "walk"]); + expect(outfit!.actions.every((action) => action.sequences[0]?.frames.length === 8)).toBe(true); + expect(outfit!.actions.every((action) => action.status === "confirmed")).toBe(true); + + const walkFrames = outfit!.actions.find((action) => action.type === "walk")!.sequences[0]! + .frames; + const walkSteps = walkFrames.flatMap((frame) => + frame.rootMotion === null ? [] : [frame.rootMotion.dx], + ); + expect(walkSteps).toEqual([4, 4, 4, 4, 4, 4]); + expect(walkSteps.reduce((sum, step) => sum + step, 0)).toBe(24); + + const frames = outfit!.actions.flatMap((action) => action.sequences[0]!.frames); + expect(frames.some((frame) => frame.durationMs !== null)).toBe(true); + expect(frames.some((frame) => frame.rootMotion !== null)).toBe(true); + expect(frames.some((frame) => frame.qc === "passed")).toBe(true); + expect(frames.some((frame) => frame.qc === "pending")).toBe(true); + expect(frames.some((frame) => frame.rejected)).toBe(true); + + const imageUrls = [ + outfit!.characterTemplateUrl, + ...outfit!.candidateCharacterTemplates.map((candidate) => candidate.imageUrl), + ...outfit!.baseFrames.map((frame) => frame.imageUrl), + ...frames.map((frame) => frame.imageUrl), + ]; + expect(imageUrls.every((url) => url !== null && existsSync(new URL(url)))).toBe(true); + }); +}); diff --git a/frontend/src/pages/playtest/testing/demo-character.ts b/frontend/src/pages/playtest/testing/demo-character.ts new file mode 100644 index 0000000..05cff11 --- /dev/null +++ b/frontend/src/pages/playtest/testing/demo-character.ts @@ -0,0 +1,192 @@ +import type { Character, Frame } from "../../../entities/character"; + +export const PLAYTEST_DEMO_CHARACTER_ID = "playtest-demo-boy"; +export const PLAYTEST_DEMO_OUTFIT_ID = "playtest-demo-boy-default"; +export const PLAYTEST_DEMO_ACTION_ID = "playtest-demo-boy-idle"; + +const fixtureUrls = { + "base.png": new URL("./fixtures/boy/base.png", import.meta.url).href, + "idle-01.png": new URL("./fixtures/boy/idle-01.png", import.meta.url).href, + "idle-02.png": new URL("./fixtures/boy/idle-02.png", import.meta.url).href, + "idle-03.png": new URL("./fixtures/boy/idle-03.png", import.meta.url).href, + "idle-04.png": new URL("./fixtures/boy/idle-04.png", import.meta.url).href, + "idle-05.png": new URL("./fixtures/boy/idle-05.png", import.meta.url).href, + "idle-06.png": new URL("./fixtures/boy/idle-06.png", import.meta.url).href, + "idle-07.png": new URL("./fixtures/boy/idle-07.png", import.meta.url).href, + "idle-08.png": new URL("./fixtures/boy/idle-08.png", import.meta.url).href, + "walk-01.png": new URL("./fixtures/boy/walk-01.png", import.meta.url).href, + "walk-02.png": new URL("./fixtures/boy/walk-02.png", import.meta.url).href, + "walk-03.png": new URL("./fixtures/boy/walk-03.png", import.meta.url).href, + "walk-04.png": new URL("./fixtures/boy/walk-04.png", import.meta.url).href, + "walk-05.png": new URL("./fixtures/boy/walk-05.png", import.meta.url).href, + "walk-06.png": new URL("./fixtures/boy/walk-06.png", import.meta.url).href, + "walk-07.png": new URL("./fixtures/boy/walk-07.png", import.meta.url).href, + "walk-08.png": new URL("./fixtures/boy/walk-08.png", import.meta.url).href, +} as const; + +const fixtureUrl = (name: keyof typeof fixtureUrls) => fixtureUrls[name]; +const demoWalkStep = 4; + +const idleFrames: Frame[] = [ + { + imageUrl: fixtureUrl("idle-01.png"), + durationMs: 160, + rootMotion: null, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("idle-02.png"), + durationMs: null, + rootMotion: null, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("idle-03.png"), + durationMs: null, + rootMotion: null, + qc: "pending", + rejected: false, + }, + { + imageUrl: fixtureUrl("idle-04.png"), + durationMs: null, + rootMotion: null, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("idle-05.png"), + durationMs: 160, + rootMotion: null, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("idle-06.png"), + durationMs: null, + rootMotion: null, + qc: "pending", + rejected: true, + }, + { + imageUrl: fixtureUrl("idle-07.png"), + durationMs: null, + rootMotion: null, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("idle-08.png"), + durationMs: null, + rootMotion: null, + qc: "passed", + rejected: false, + }, +]; + +const walkFrames: Frame[] = [ + { + imageUrl: fixtureUrl("walk-01.png"), + durationMs: null, + rootMotion: null, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("walk-02.png"), + durationMs: null, + rootMotion: { dx: demoWalkStep, dy: 0 }, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("walk-03.png"), + durationMs: 120, + rootMotion: { dx: demoWalkStep, dy: 0 }, + qc: "pending", + rejected: false, + }, + { + imageUrl: fixtureUrl("walk-04.png"), + durationMs: null, + rootMotion: { dx: demoWalkStep, dy: 0 }, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("walk-05.png"), + durationMs: null, + rootMotion: { dx: demoWalkStep, dy: 0 }, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("walk-06.png"), + durationMs: null, + rootMotion: { dx: demoWalkStep, dy: 0 }, + qc: "pending", + rejected: false, + }, + { + imageUrl: fixtureUrl("walk-07.png"), + durationMs: 120, + rootMotion: { dx: demoWalkStep, dy: 0 }, + qc: "passed", + rejected: false, + }, + { + imageUrl: fixtureUrl("walk-08.png"), + durationMs: null, + rootMotion: null, + qc: "passed", + rejected: false, + }, +]; + +export const PLAYTEST_DEMO_CHARACTER: Character = { + id: PLAYTEST_DEMO_CHARACTER_ID, + projectId: "playtest-demo-project", + name: "少年 · Playtest 演示角色", + createdAt: "2026-07-30T00:00:00.000Z", + updatedAt: "2026-07-30T00:00:00.000Z", + outfits: [ + { + id: PLAYTEST_DEMO_OUTFIT_ID, + characterId: PLAYTEST_DEMO_CHARACTER_ID, + name: "默认造型", + candidateCharacterTemplates: [ + { + id: "playtest-demo-boy-template", + imageUrl: fixtureUrl("base.png"), + generationId: "playtest-demo-boy-import", + }, + ], + characterTemplateUrl: fixtureUrl("base.png"), + baseFrames: [{ direction: "default", imageUrl: fixtureUrl("base.png") }], + actions: [ + { + id: PLAYTEST_DEMO_ACTION_ID, + outfitId: PLAYTEST_DEMO_OUTFIT_ID, + name: "待机", + source: "action_template", + type: "idle", + status: "confirmed", + fps: 8, + sequences: [{ direction: "default", keyFrameIndex: 0, frames: idleFrames }], + }, + { + id: "playtest-demo-boy-walk", + outfitId: PLAYTEST_DEMO_OUTFIT_ID, + name: "行走", + source: "action_template", + type: "walk", + status: "confirmed", + fps: 10, + sequences: [{ direction: "default", keyFrameIndex: 0, frames: walkFrames }], + }, + ], + }, + ], +}; diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/SOURCE.md b/frontend/src/pages/playtest/testing/fixtures/boy/SOURCE.md new file mode 100644 index 0000000..16a42e6 --- /dev/null +++ b/frontend/src/pages/playtest/testing/fixtures/boy/SOURCE.md @@ -0,0 +1,7 @@ +# Playtest fixture source + +- Original project: `E:/Compressed/windup-asset-lab-codex-issue-14-workflow-skeleton_5/windup-asset-lab-codex-issue-14-workflow-skeleton/` +- Original character (`card.json`): `少年 · 默认角色` +- Original `importedFrom`: `default-demo-assets` + +These files are copied only as development test data for the Playtest workbench. diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/base.png b/frontend/src/pages/playtest/testing/fixtures/boy/base.png new file mode 100644 index 0000000000000000000000000000000000000000..7755f3c954aa9518edae0bea484e761ff188c1c5 GIT binary patch literal 12196 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 literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/idle-01.png b/frontend/src/pages/playtest/testing/fixtures/boy/idle-01.png new file mode 100644 index 0000000000000000000000000000000000000000..7755f3c954aa9518edae0bea484e761ff188c1c5 GIT binary patch literal 12196 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 literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/idle-02.png b/frontend/src/pages/playtest/testing/fixtures/boy/idle-02.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc3e9b8ce1ae8059a19534c9ac57f3e76556324 GIT binary patch literal 12069 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 literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/idle-05.png b/frontend/src/pages/playtest/testing/fixtures/boy/idle-05.png new file mode 100644 index 0000000000000000000000000000000000000000..cdd4e01b3f070d7449b706c713d5b074ca69966a GIT binary patch literal 12078 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%0D6kV4NQ1x^#wK|=D?NfGY!9{oL|>=HJ? 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 IhEvS{03tlOrT_o{ literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/idle-08.png b/frontend/src/pages/playtest/testing/fixtures/boy/idle-08.png new file mode 100644 index 0000000000000000000000000000000000000000..67936b2ba569adcd367372eaa599b7afd731e05a GIT binary patch literal 12156 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 O0S`3{)T>nOBL4?kXb&_1 literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/walk-01.png b/frontend/src/pages/playtest/testing/fixtures/boy/walk-01.png new file mode 100644 index 0000000000000000000000000000000000000000..ab3e368e98cea3b11870e615749eda5e11af12e5 GIT binary patch literal 12623 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 wIrX1o`V3nB24dZrLPqib`ceVx^Tjt<8 literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/walk-02.png b/frontend/src/pages/playtest/testing/fixtures/boy/walk-02.png new file mode 100644 index 0000000000000000000000000000000000000000..7e507720759bb2c311088a53b178a1d741aece17 GIT binary patch literal 11763 zcmcJV^-~+)`}UJSkOX%pxLbij3x(iXv}lnOr_f?Wi-h1(q{T`pZhcc6iaQiq99o>> zQZy6`nr}Y;!ZY*C?9Aqevzy)f>|E!%UiXPJ(9p%3TF9Z}d%A zr)636RpwO2k%;4!<+blH;Zn!jVUgsz;gaVtfwaMI^2&i}pm`pOf3P?e7Ct{4J(j_w zSC29r1~V#KR<4G_t`v(HUAf0Kxh`)zJruMgizer$9$|gPu(#tt7!m6K!@|L^S;Sieta<$R3VDc)T(Il(R!UwT`TR!$MnAUp8Gda{cL48U5VSsn7cg327CmR zIaoL+zH=(2ICJEsBqbH%4i**^#9t|AAIo!%8IgCM_&N*H2o*Ww5C4ymRy-_JghgQq z-RazIkbb8EeNADSGOfYnGTm# z1s9Z1Kyvi_?#7OQ=C#3;2?8n7DYphB-MuvQnK})PXHkT( z{mfUvxCf9*LIFSmGW9-MC$lG`Qmkf{xvJy+Xq%7-()=rkMm^78mj9rz#U|DG(U&jE zPXh4|4-bXe3?w~7?>|#LWw#`RjF~%}*?w~uI9T+D@h_DF{sdk|tUlt0+v#?fU)`Bk zm$g@AIk5-?$hHO1;qFgV3=kGyZVEi*oon-#d0R>2O%t4y>k6-ThN`L-ByTHVB!ao! zV6`bAP4rOM`Sm%6&#ff{P_&nYrXHh;mAYmMpQ7p>kH7F~p=#dVJ5NbXC1Yh}MNjF7 zO&v>&O(@ETb74niR`AEW&~|gDs$c^fn=!WW&vNvQatQn&CsI2@f;6!dSxq1};@9AS z{C-t4DD-^9rxoA!86b}0UNa$%w{0%g*tUyiLH{$X36?Y=49|)Dle>m=-O?+ssIX7H zw;$A34`ydSfkJrS`}rOIp`W>Y_?VZ%4Q3cvm0<$2VV_RuxO2AeG~*27rxqtGRY<~^ zeYN@-Y~A5=TPF-RQ(W}4Oo66mN5LSpifme#Ms<&8EJ4&!=+HBM!V?xpUe5Ku#{gpP zI_(XjyZWbe_d4{yK@JWzsd5Wn+P_DaC_w>H>?^~U9J^!)L<;m#6A9)~ZE&gOltBY< zz(trQRAK$3uFmo7QzF+UD2u4Hi%oGx@Et~duKMwkd@B?GDocm5uq6S8g*Gzv(Y;eM znS2t`wJ#LwqMN7DPvE*3V2PJG)8|(G`cs;x>lPTcK2(h_2|Rqn=rznZVFY+-3ic;N z+r;Nw)%eWnU0Z)AjtBO~wYRsIhd4`KVSa>>#2(!idP$eYK;18h%84IzrCN8m!8 zp194Mjh!!7Xim$pJWNvpDw+9@TM`#@CpzjKl~|a17PJN6Ram_;icdqos!ONPTLkms zFdAb(Yml)0*Fs*ndMXRNT<#MnbNL#r879<~^k5cPSqz45?_ZPHd8dFW_)f#-|D_8z zLPtxPVGkM^w{*M{?$UJLf`Jukk_uN5AFso^U)ebyUm>IrbDS2aW7RO zAEiJM560wUjtE$4k*G@lXgJH28V_QG=hSN(uV!*pm_K9tAwjW>;-tUL#Xe%_?ME3K(HW26uG) zc=qRp+U=U_>U$8CN8!^t%IZN4>wBxG=oUvL=w}p-QUNJG&E*eB2LJe=kHOHpc!5L= zHz3X%Zv;hy)GA(_=25WxPb7<2=4t7GLnbELhn`N@YG}lV3{uxj09@-GJC5*&Dwp(* zC)3!%#Tl4H(=D{z_6?VJs3w&l-P{?%AJHZ6DDKVG?_DUOY@g9sqT*4>5I~ge7kWTu$w`{? zU!T|o{N2j@+vEl~)0dJ(@0?zJx7AAdAeo94D{KmSEewIpECi<}?H|GtAqeAiyl?Fo zO#`6k>lW;zxyfLo%exb!?Ff|!_jE+-6V`vH(A8k_MkYy!H;}}kU&gKvMb|uk_}9~< z50zEL1*!emTkXswn*D1Z3$T3|C!yN!BR*SamWWt3BLH3G4k=pxS`$^>go&O~78k5H(3*BNGuRy% z)SR7Yea*ZD%p!P-mmQm-#d0(#1`$svXEv|qGJ28p12ud5*LEZ(U1yu%(raz!1VBRZ z?Ly7ABvzcT4j_zNh<_xSwO&-068$f@F}^L;Dn|1%in$Y;A+-PIj zat5}mzn|-F67X|X+)$$tG|0GJTKUR;hRW<42G^aN-*mK3)pvi>oxsSK1M%e1mHU0U z2`mRJzvRpO=HF|rP`?nyDV%0;DI-gz&o7kkOQ+ywxAKpeNkKxX#Z&~v_)*HcYp{Tg z-!5YTy)4OxZLsXkNLq`C;@mFsTZ1|ryvvgh_zI`!mjr}jMs;-nn5cr0%*8;Rl zP%c1RR3=A!hZ6|Sm!yx+sH#bxl)z>FcruUVv;nrO%vq;>nYse#mjYLc{u}gE6ofoE zfs9}^rt`$<>j1jN=ezxTO;+!sl$`o1!6nK)e^tD)$;&Vq!dXz;=Z*1K*DgmIpBnnc zUror4SrbY3-!R+W+B2W*-@9-x-ls_DZF1iR>`(n)&lJ6# z5LLrY&jo%jalJ3%Su-(L+DJ9j{-*_V0QRTe-|wpb_FPT-Om)^{NSyE8&EJ)Qt>!J> zJ@siu`{%v3Tb$N^!Z@Y6*`e4;y_9SUZlyhba*p?V+>QBXR0|KI=VfIWv2-KU!jJtm z_ibE^Fj6+0RP^#-L1>LXmW%!7ysTKs&-6{jjzM=|%{wlILHp(xfaDwOPXR@mAtO@9 zZnOJ4^je;Zu@2v_-=^@p9MJru_Q&Y$cNKG7RiMNMIz(U}1amdBGVS6ap^1Cos?%j9 zAe;wwrNL*izdo6F$Nu}yJPnWj)ZNX@L&;`gmWA_{zuUz`{%lj|!J5-$2!F%u%I{hC z*gcyY(K9t)1xofIu%;(M+!Q2q1|ohGydi(eF4kw=Wb@;v&zFL&G9mgs3S)L70C6tc zGlK7DsNcQ@yN&-HtcEt=H%u6TmUmhaQu;QIMD&DUXm}z$UR;I*)?D|PYb1&Wj_N^} zOFaT{T4AoZnn2B~6~uwzWikiSd0|_#XiUVvZ#IMrn?1w(9&`JV&U~(ys zusF!AB}sNhEt^quNnh{@8Tp*R&G8EWg*DRUC0ealrh`(i@u5neNf@iQZnaU5GK{&p zh2-VKTfDn%Vf?3fmgEftQt1HwvMmVm(>13Hr4&;+V1g>^1CPg5M^}r9G@}-@f$F>b za`ZCw3zFh@AAV6$XxNm1;E4?Y68kgGH@RH^6Xv7BmCtH0C`?Nv{At)FU_^NMLknI8 zp9r-c+m-C3zY>uF#l*s#=RgjVTy~?-p83bRY_!PtSd20qa-e4+U`-gPyR7EeUA8$P zxVl}YlEV11$>CT}So<%`@4QKmk=_(& zibH@TNcu*Vj5NUUCp>s8IqoMQ3fsD;-5gueafDeBKkz2J!AG=s3X>)!*9u1O*esKg z-(GN0-Et#4i@Aphvp%GL$2W)4T<_VHRqfwhM#1%8*AfDnNK%dyo3Qa2c?uYQnvACr z$JlkA>ktc;YgB0#2%e(`6w3E*CrZvxmiyYI#^LxEF0{5BlM96R) z2;WY|*bot<;p-dF#($o zp9t7I#N6GwG`qwSR3wlBprrNSe)1Rwv55-&EE8m7@W0O!c^i&Dw<~?9U*nC*uQqUx z>3Z%L9g?Us5LKu!J=QA3ehdt(u{%+gJ-^zi%g@~#YRRI{?wnt-9TOwp5;W`>~35EX-r51-sz3#`F&~K2i z!{4v6^Xzd+@b>+@QP?acle;jH004>#JuMtKU5iK_)MdO&KMRO4d3!JL7P}^qN-7fq zX1kh?;FyIr(<;z4@t2zeI#_?qc5jY@ai+e1Z73=cEfo56qw|4R!*$yZ%o{KGvQC+hQskq)pLm&&&ZVBsBl> zIZ~|*Yyr^s>Dv*#IkM{G(*HoJPGi;-53aeIk9GW)cot{pSwl{#I3;klvZrCiaoDUk z@7r;F0=8P+N5{upEq_z}zz|boRIn+*6YT})ZN-hN{nAy~dm9N+d~h-E7jRd}`rn{l z<4Ykz>GhXCC=68~-O8J?|GF)nQPBiTM9m5x(CZUY`%h#JdfS-a@6Tc{!+M@vVs(ag ztDS)5Js#2ZsQ+4=#_g&u|pGgE#mKO8VIJw7HCWMqN(5_+xY|si^5`;qw=Pcw?33Ttf zwr~NJa;a#(#($Q~SbfArgxH-wdtng|XRQv-C7*{<%lqYz4m_l+qF~zFQ681LY6bQ# zOb8>@hP30b@{b92A4l=rhqv4coBt(g2HLCavLymW%87F812+Ueu6CXNgZ4Cn+Hi|$ zdbD{K{&cumwT%#~q8xzrRht0MoHDLvsk>pmzKuM(PMKkab>dY(xRdj{Zrj&$Bo%vR zky%*9pnuW#Ti#s&Rj|`%eTP zY1hNcJFZb)#0eRtI{@_>Fh-Csgp5ZxSs>=oO~*H8^DUqLV{!M*&(bQ79LE0&f_eow6uw84oVB;5*5EEfi^O*4)T#o zRv~*VGRJ2)^PUjOu>Mntw@L{PZSXWUP8jK>x=xTdg;kfG1zgzl#jT6)|?f z0<4Q!3B1HJJso%91m6cOkh^AYSKWv5r+}D%U@QjAzSuh6_<*{rEUeH522nJ!wD|sq zo~{kg+XouNKB9UQk*AkBX)G7*v$;e})!4WEzqKl(&u6?(IBKNK8B)&%NTgELss_^?=g z9$mdmp7Iz6rGQgl+$jclbL~qkWYi*qiH~W_8%3Pb^0s^kbt2grMPq6U?J`J*qHJ?k zR$9UeA57qK^}}6Jm!5R!J!|s?Oit+2S3m+>0*N1?-Mzt@;@!dnD|DnK8l3mG0Uodw zv)m0(t!f&0B|fJQ`9rKUFdHy>x2!ie{yhfd#P`5ouj@K}c6f#E4lzJMI48 zVrIgeUnF;WR&Kosp|{~h5PAFSAc8`4SxIR(ku+%|32w32q{7dG@V5c=+sWQs7`hRU z*^*zjML_UY_>9?<5qhMK?8*%g^li2)piK^M>u2Ef``(i!_FP4Z2h6KJ5f$}9u-i^b zoVCNp$#uBagWtb1@!QT+?Fzxo>&S8nd`ATS+cA)E%(%r*aZ9p7Xm2U`U^Rt2h0KDk^6d}A0F*!0BFIG8&$Oa%6Xt@_r^+l^eanzGyG@^KV9zsksv$2bDr#8YT9)ou@NFw zZ(3ELi6!ussreqeU}0g<=1#nL`6;Yf-r+OA68O^{o@);m3jYemwGL{o{t^7W{w*y$ zO83-F_1GmTpV`2wG36DdJrE^UP1`VSw*x2mbQ89%XhWt2fe>ypARfY=0?nb>;2I*Z z#lsrXmI6UJ)>azS%QtSMh8Wf)xLy~N3wZU>QZ;$I7@k#2i!SkM^l+qCK?;?06Uqd} zD~zKbbgTc>$qN{3$K0`p{pa`ha&AY(l;=;I|s^)s!OM|xAJst$x z;9q9g4w7|Fg(7gF(Hx-h<4uiNd4x7sOb`cI{u`cNM}h}D-8zBqBIp2uq@5?ez_l5` z*_nPC*~TT3xm3Pvaaw=|K{*7j0nm0T4F%jZHiz;;yu+y-r0*Lz&w)@_K7Pt&?lrI> z_#3-ln>+d-;TTl@6|Wn?QCGPvGOXaBAM!6fT6G;4a(b2|Mg=Ob{?Y#?zXpA2KiQ>> z{c7HlV*;cyOrXq1ZwSLx!uzws`G2tPMWoxdLzw8E%AAMB{ZPT_W^b3vVG!mZ%4~k0 zti+9t!&Z?+mN~yw5AKPV#(P_v@nIsgRZ@0uW81GnM!3fUkOM6L@h|-D;7iJeeOXfP zPo9FG;WczP3Gescch(v1@`e`Yy2J%kS%8((f%288+@A-XT~}L!HNHDq-wIMTM!@4l zswEN!-W3qVaWL#F-T;6~buv+V?(*a&W!c{5hr8+DvcAeE1DPCIEb(WEk_op_mV8=t z)M8b{e;L$YmC++p&lo)EYwo&&_7Ow+B+j7rKc#b6{$7J2kS4%?R4jAPOe|W>=s))^ zZ#tOisiLVH$mf3GDb$KM);PVnfEKx}&)0bx! z#?V)9FX6o0WTt0*+XS{dCpA0hG(?pw+E@I?;1SYPraOY2a2?-7fX~7ac0hi+{w3{1 zQ0ZSJOq-w%SBLeX>99u2?D`<-gX3M~$aHSX28U9&R~k=;EPvv7l0xZZrwsxQx_^s8s-@2H;?XAx7svXH*E9WKMBm@PhR? z6{;2HfmzSiHeiTp+d99?3*FPYz?{o<^2nL#Yu6eTlnaEfg}zXw-@dR20jR5shnTyi zbI^gUJA+%^>`rjVu1u>9<53aez);pLSgl0dV>yW7;aou@M?A^J}+_F2MKjGiy( zD(>?qHuzVTCrd2xpLuvu7FYQxNHwzLp50N^;`|~0nppYQ$oc(@p46S(s6C=y_8(_E zsPT}ujj3$&=X*YgV|j>unPo&t+xa!d(hXpc<)tWyiqGlx5!n zO96?`t>jHk5bbCXtR3dXK)R@;*E}-6cTuS^9e>sNub}E1iZ}>oiGTc&Jo_tD)6B`7 zZ>LpbluRT#=M@<9apE5$*HQ%Fb*LBvm7sqaOs-E)*Rx^uLKVM_!o*a0(eNi!Q>qw1 zvfgGU_5reQ+IvyTTYl`NCUtHSWsAn9Yp*_+SphUvkw_8E-^_gKF&5v-fQHyOnhZ&2 z3Nu80ZaMCwOs1EI>7Ub{-d+t=N$t1%NJdpRiXv5{vG2*VQ4+Dv_eaC;{pAfx2M5iJ z0qlWy)J0#iq<4G>nY{%y0;CJf><4q{FgG!_MDybs7%-In=S55fO|cWurC^UBQHJ}; z%nU>=N==G33y!1Qu@NF~0YSf>hiGh_LZ4js?_%roaw21n+BshKu(yEKG zNJOwW!VJ!i8zb3A!%CnOuapcapQ+Drr&&hv(AD9JRNJ@XzG5Tsyp?;DAmUz1)=Sr> zpX)sp(KXgj!?QT!{4~LC6FaG>NLm#tBX!^rwb{(W2b2DIDcRWTF)my#!NfD#2yMHpdIrz>Q zudwEn^g~3y1)wEh770s9vDjPH}ZQv zcGzm;t!FV4rYub-WpF!F%lHqnhqtb5(>PHd(t zl*lcea$54*KL}34=S58^B)5Q@MdQwIj=7Y@9D+U$JvsL& z{%FCNtquPI5a*|R-Ji2hL^Ii4_{moivRd`LI_-SNm~MRuy}Dnw*IRfcW_n@B3p{b@ z$^gZFKO?VD{Vwt;H8nwsH48Qb7EkT#QxP}F8jE-8yjxavN(7Bm`-go*Ezp~K=N34n?~o{U_sHBzb)L^4a5O$%+vVjW zk#m1mj!daB0}636)9xKEgxg7kNJj&{6{E%KC8@nM%X34oM@y%7ZT5SB{?@~nuf{BT z_8I4&sS1-6W~RUk;tM}XbkK;tVa$|M+Wj9k_s-vMcaN-}KV3cH!&+bNvXeT!q0WBx z%N9>pZ=q4nfIB@_(E+HdWJh`L8H#mSlO|}4b?#10!TE&nV&@-kPX~-Kj!*C-(?d$3 zMTOrA7?+cY?r_^GD8}lTzI$bbG*>?z0Myrm$3J^ zHW5`nszowCbW(oq#zK^3Keozl!U3G;42-8kE*Px@pr*N=(ZKHimor-1#(VyZUknJ} zs&_}ReF~-s%f&q%U|?Wt>iub$36Q9LCgF2$dwLyT%11OqC@g9(FidqHrrROFNn`;ahArocMm@iFQ{$~C;-5O> z9K&Bd9}H&OJxrxH=J_9>2MzgdGEm*+N%_SbwO|7r_6^k$w>XRn*dDQw`f`i?k&Qk2 z;}fUVONn&~7H!`ynfTu_2?L&DliK_5=Ze03XBJ3a8KRB+^<4ZOH}$}@%uE&aq`=h6 zSoEYKLw)bPP$E~L@HqgLRkQgqOa6aWWUHb& z1INyET>j}dW+vM8M8@BZJ&jkXF1?7Z;4t{)wIVgz1#a)gm1gp&HhjS_) zdwcD65C4LyFJTw`5rseD)8k)uL8_8J>rGz>TE8Rq!4-N8kt_GGuYc6S6t9EzsYDxo zEidqeie9om@>MD*EIR4_aVe{-v_UZL|H9FaF|Rb9&mAk+(1ovzTPD_yUjz^qTRE2q zOa?p?7Z7kw5jkSPvOH7%%8u&Q&Tj>rLv0#IO1GBJZd} zpZC_TSHB^=%l=j6M9`UdKu36J7{)LZTRytMTC?VMAIp3+s3F#*9DF3ePG8H}a4^*? zM8FrcttIaKCwQ4Pv;lo{=UIBCqN(%r#i;Cw|i-zOA8w$&aNsk9f zxZN9u25>bXG3IwF*Zx4gS@aXpD~%WZ>=bpkw;;50+80aataJKzU}RC4p%>$KS;T`d zAxhg__B{MFnoKPfl~m?fbMouJR) zhICt)Gf-KZNzV3-b|omc78k=Ge^yYDoIz^ClR?e9tf;D^dZ~CmflUxM@G6Q2AYLkX zpv{Acg(f9v3`yo z_nQ7|O`qpE3ky+3Ha2X}Rgj=}Tt337*U&1PU4@m=&g0gFE2ICK$SrT8v%tt+v2|g{ znXvpiqvMVzW@mj!>}qgmC+~mg03ZEt&1Guv@l*f%VE>6iOv2MosNUqFP_yWz+2do< zujXHWJdODMNC3$5f*E`>Z#=@({ShC6c}j|lWI4IHHZF?Gu5;(5i_vTUQ@rh)OwtPa zYq;8k0z+1whFUoO65a!rGDtDtJ>n3zRj=p8{6zAl$XZ=Z#1n>+via¨y=SRj?==YT{GSNklWW~Yz{zfVrj-N2dOt3 zlSrud{mIP=6rxBUEHa(PML=j6m$IUG{=dFdcmL;aJrZ{ z--%`j6u1;KNSOmwOuD!19oGm8RwN*dv3~8s$Wm~1F96{I@er2?P|b-0WaL?Bj6`o= zay`uLLj`QaJqbdEvQ)|cnjxn!hT`CUypd)E*Bz#4#_Vg(Dc^m20Rx<>Pa}|vvN|RM zHKPlZkj4?P;#gzfCDPSxg)q_1B;T_^27rX`|Jpv~#9U_h!g*oALCJ)mz(Ym7^0)c) z-q=7#;M{ z?JmOMi6gqQE9u?prIBA!Kv{+Pa!6u$BjXn;eQsr*3EZe!;1&qBwVRxL8yNKZ;qBdZ z`m@@%5M>Ag(mi{cu(&W_V({rR*~h@}v$c#h31-3Yg~Vs3>GVF=xQ-;i9%8+Gh+eM9 zOr^@Q!%04E9*NYyqw-^#{>fH5gl;eB$ak6DPIjU_z(#F3=PxkCW zL#bw>d)w%oPq#>?;Z_doxz6fH0m7oHN+_mF^xnyRs4l`gTB`M$h%0xeo9iUw7C< z^75QW5ywT9n`9d5(BW05!m=2>OVzJPu28mdX%{&kVC&(Ql#sX@OeB|^4seNmq#b7c z4Bs+NNu%{`L3=UHpO>!WSAQgUJssx8nbJf*8}dGvE~jJ09KJ9W$O=`|A`1zpBLw@n zZ_Ase7{6+%{sUGD{u35_EjD#2>cx|9rI48CpI?HPR8nHBg%$t^AUEN>DYYot@HA58 zGbui`if-QaC_%iZtT^;Z{IKQ+=P`d;!4BFU=xy?nXdP6*?9mQ1a)0P~X$xqZ1y_f) me+nhUSEBfTJ1G3`6|>o={$tBqW5aFc25712sa7c?Q2z%MU*Tl{ literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/walk-03.png b/frontend/src/pages/playtest/testing/fixtures/boy/walk-03.png new file mode 100644 index 0000000000000000000000000000000000000000..15b97a2731001092fc05117a2a2ec68d6a31a604 GIT binary patch literal 11294 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�P;E7kRr&6lAYr5sRYF z757lDo3s572D;W&ciMItjOuQ*m(R6o?O&u;mGLcydWB^18YjH(lyOJ|GYNVov#Rar zf)txh@>VzHNX?qbrhxs^w~&`*5O{UvKsPk{;|Zyrv>#||pgA@rLe^w4{6)6Y0xu=A z-p5T&yQg#HsLgwB55WXZx^=XAv~#QU%sY%5VLKz8jgsE3Yx3v15K~i6b`}k-eZv#@O!e}-0?=3$i1?r4vhH3tc zoU2r)7cI31M}8aoZl|jc?0R?EYH|%d;G2p~?PSRGAh_}{&s?rW`FS|Nr%%t(wt#G3 zC&51-o^++L?E$-Zx}!ouJqc0`%ro9}q)%vf+HY1p4h%A&5B*9u4Y!K;M4>qtbKA=J zL@*!l=w5|)UL6S8jk^`z{B^>5w9jvQkX`;UPv;O^`zCNQ~X>b82w<;#>kX}%t zkojY!5#S*d!F@8~9K#q-kM()6%5mTWVA|Zw+ujam&|=V~Y#{9Wy7X@`E>n0K50?uI z9+c`Qi@FLcwGs>JwFl>Ge+7;FsQ0>GFgL{k`AS6?`5hFdO1aZQZ1Ol`#lW{lg@S$?DJb`%3QKw_bh0XIC_}E4_ z`Fk#yK{%-fyY7y%=q4E)7C!U==Imz?$e64=#8oXgXg`G>W>sw_2y-!PVP6h z36Tpa<-&&!?uS#U&xVg@FVS1$QyHE?lUp@3M&ePWE35Kxd{=)122XCk{FMb~<^v}p zCZp^Vy}v3wR)fH;DYG8Zxf#B$Rhim|h~7pq4~kbFr)>tTPpbaDdtB%K8~1mvO=NRp zX1{c`^HP%8t`~WmujJRJVbb3UwO?^T{!^b4&#)|$I~iy&FQY4azbIyFTp{Q0Nj9uQ zCL{>e+lmq(UE)WFm$L6|U=le_->t-e@S^~8f;nzHqM9E0o&6*kBz+ueI3N9DjM@*# zuEw~3NP(#%c79mkR{1lfM@(B9W=uxd&+A+L`c>-=Lhd^Uu!CH?2;CC8SR6@Lu?}ZomYwHuON2KA?|X8tqVj z&3op)WPT4z$c#%nJcnsp4gWFhw)}fVe`% z%~C~{)s*m&M)EPVVj!ryL+rWdOZTQc70h|BxN)y^RWN$`2h#NfYn znZ$Dkko4Zp^0Q^C4Ep_+AfC%(e3L0z>F>r|UhP^>LtOXKw*}m4P>EBrGL}tCRvQ`; z0YyAOKoAeU>{XJ_bqu_^KwsAq9v~dFbekvnncMy?ii%^PLBb!uJ zT0frqz9<8R3k1vl>cplM1NV8?iz7X4Af+gX=Q>~5Z>{3551{ADjX}Qw#2Zd$)mi7D zwH?9hdc&C zZ>!&ddp23wBpF+CGXNfnFH9IC*$pX^67SwEO|CBZi9Jf!a9iEn%*bG{Z`?N|?Q4*H zi%6;!bjO$1b`Y1pAS(6~{p{EN7K_uc{M$+{5|qomA_kycyc9hjyu1YLpcJ`GJ9YU+ z2iu=`Y5>b0w(Xn_I^Hrr6eev1%cxUe^8n=1K`!e_^PA7>cwSf9iStf=EixXcH9>yyW&ANIS6)WxdLN^9|qvBjh=w7 zvq?C6mnk=L81+N$09fR2zS~CeV#ELe#5V84wD3B2G2qf*=Bp>qVVi{oxQG9lBy;rb z`tr9YP_zgrh33hfmH(!KSB0Xo^&rSd3edC~4`-(N zYzT|~u?c#;uv;|GLArEo9|5pBEenJvh2}>=(XJk|FYlpcb^AcFHyNTf8y2@GBf`r? z(nqIy&w-}uCzgrB*TjJ5^zT&6c+sxDud*M4xNh4>;jpH_GUN6(hSaU-6j1r!PQtHg zJ$&oHglrG?52M-s-h0VgOkh|rbpxFK_)a?Q2|pd8`jph*fd1Pq2@v|$Hq>@vTB%N* znM!G@({LUAOG*o zz&TwU2k!Tw9X>leltBsl$7Thj*mwsU(9NQvI`7tjaRT$zFviB6yTRFk^DhTmt>z51 zE+9vo-1}nCrcRiH&{6~xO}Qi`3Yu;%xnKBwOr=MNG&_%xKAo`K2}ZbjIWg9$CP!_?4{_hF}7kd=wcWGK=Z`jpeH z(tPrrxV2B>x12lGP^V$#XJ1@a`P_NB->7Iq_y)1jL5rv3W~lE4s(P4iDB`D)WA$WO zoad<|YG@VT`G8;YuC5kQj0V}Z$LMp_34G^KzR8-lX^ZloS{T#z{s-WOO&DR?w`;EDC4C#Iklw$iucN?OY;vT@O zkk2^u&7g(W6-C*ID8|j%IL7{oRmM^qW092=xO|2qz*zKr^xFkBMiG3jLkE-sXh@)o zIE&oUI=AXcZ^JGaq)-foceO`jxeXw z8hfXD>%nw@Jj`*h12vk%$U{y0OziE6CXck&9rHOD?w~}#-pfjCY-cN z@`Dw@i4=Av8smR@dvAG^m}QzC7lJ))RU_Q)`Q}E=~&`%3r)CyA~OxH#rI6sO>yHN{*rsdC7cpDy+I+B&u)EWy6TEGo_8F7#=Or?)pb*4N{dT$*pa|+>jg?rMdVg22@xT zUZ79~UazI!Vjsq|uVJ1PBHL_w4MCc-HnTX{1r_7Ll2icoBaa80L?-l;HBeVvo*RZH9nCZ;Iq|$+zZQ= ze;XqI5mljV(2VDWvKt!^$IPv9!ue~T$O-KT`&5ahL#Y%rsq;*}7D=nz!T+P$3~)W+ zMjr6dA+%5R0JGtH~9zeat z+IKrUi=cX;`Q&z3XuIZ@(v*0HF;1k(V+fzvWQxcgEPBBbN9;g4CwyTPr(!5Pd$0o2 z>;Owp(E9U%`M6HN-G(|kTwi?~?j2@d9T@&d3u->?|2+AEvvn~K&Ui7;&HFP<*)7m% zhtx3HHeguAtVEMd2$*_Bzj2$1nOr|y|6{fc-0B?*n*oDuUpVM!=Z$8n{VdRTnDkJy z*VjH~R=5teSt%_y>88MdDP5-94XGMqtKX?|^PW3K^J_Ec1N9*Zms-wn#et%Mea~dT z&mE3r0Ea#ImOlkkEf{MB5}4#f@_=pdySNo~Ck)m-CjlW;G*}!+zx(U1oTEinjMX@U;^;I9i5XyY~v& zp&*TxXzA{XGNk~74;Dl#mL-?o=y7hqeH+a6IbPyAD}T$=0ph}DpRK!Wb?Mr_FAJ3Y zPb9bsAvIgaQ|HUJ(r6oouhz1$rcnRBif?^@0#x1LrwjI*s82>za-Mp<80jL zh|_Mgvdp^{mDADfK6UnYwK2E&tA1V$M)zsu>C9*)kJ7>U&;gicdF?E&?O1s8qOyg* zjhZ=_{ONSpebr<;RW(_gGl&!c7bkPX@;(bPCY$mxKi2trHQ<3@()#*3_8CnDL@0zI zlE;VLP~OeKX=wy^fpLmWIHe@29cR5uT}c={dJ{g-N23x6-MKo6Z(WT6QLQ_S$%T z>Z;l>v!q~B-C?Q3{P)Ujbf@1y)W-l`s-=t&&dWuURx=F%vcQS*?d5KCzswI&`v$`d zSDiX1dm15{kS(>L^$*7}EiJ;2?S9&@&-h-{2T7cn*a2+2_3YDY4Z~IKhsW8qMZtJp z9-vOfr$E_+VJJc=S?q5;=p01Fd#-nRy5TXV*A7!Vup0 zDy0EnNB02iM?SBMsNAktSKwx0tWyX@8--I-Qbd8x|*OTU{! zxSfDl*)x`a@1O`?$LP@$>me4;rsyXbUsP^s0)B{!iK8tZxp8~DO}-vT!^gz)zEW$Ey4G`l zI}ZD_^XBM;aKfc7gI4KQ`>&eS@PX9)L68*)oFel7-bJUJgtNu zWTS(RA7fB*n<9{#AC8Z&!mJi9KUksvYL(J^@u-l@t9=rl<^P z9)HhTUd^<&C07oQX|tY=pk{v&k#F=))#bG8)57>NU9J~jBOMt_1mYRVs#QQ)S!8pl z`Ud3|L)}NG%7FpfSB|ZP>_?~oszGrvBmBD>hzzqy6}NVLGC`rV^Ya!RjSYKt%uTW` zi7?Kr6TfmOQnm_AFXN;~*a&A4p|0hf+Dt!29A9+7cFVbu#E0>GB3Mc+bNW^IDcs1$2*K-yN%caLfaK;#cbAu%e#*|j4}6sF1v_K?G)tJ`rN}8P z0$gxc7CyS+7%6fm{mC_68Ci(4WdsrlBZp}cRn|=z!G$oH8iKo$P_3FxBz|SUu9uZl zkeq$)P@R8kQl3rY2ix%5*eQ%oB*sXSU%3jP&iW@9u)5HTd0b(j!c&X$#Fv!Zbp>|; z$i+1ABm301w!6M*pi#a*hrkoM2}Datfhsc(`GVT{!GLO)F{6lbFGLq5dGIkHDvC;w z7pU**q11T81$ZHITb4A}{IJP;f1*u&!{vI%%$(wwkdQFh#!%*wXorfutAi;A^$toH z<+#(|J7US@(Y)FgO7Sf(!JnBFxQBCJ_%X?m zu_22hrIfTZ)#F4-7AEJf>=YvhK+j00s)wrowJsJC&OdM4dydD&q%#K`HiG%}f~p}8 z$W%`Sb9q}r2L0iWsqF5JG8beY?c}SY29m0@K+-!k1&}gBd?d;q8pNz1s3sPgP6pTs z`KqzlnkLp?7H+GQ@xJ_tqUZW|9d0J8-Enf9?mrDJtT6ro;>?-0$V5hyk+ABk#)R_7 z6jI!i0u=&Hv>v@UPv?RsHU;^<_+h6o}!1#*jdGU;-2J~THRL*{YhQ}LO;u;OUt~g|O zpPQW40N|+bSAlMBN}1F>@)|a!lFBuFtTVye=x2E*9FleO0(oLuhE5*7&?Ji_XYVXp z)a2_Pjm>?CA2^oWhhD?D&_6Z zoR+_6XN8~ipugm#;^S7@R8D=sOzZX$=ffBgTG4b?=cp!R0n=0eEt%)`mE0aG-75s$ z4jFMk?Z3IDqYkLiyXX)Ggfqfm;%BF#?XGj7L39RX_UurLmP4=vmXKGJ)8XSoL0-6K zb2cy3EcclMc@6QkvYFY+nsUzRhef;>a`ZOBmOv9R)=OEQcgKmWX5vu0+ZN$r4*t>4 zx;~ujP&zIc#XR}@rjWh2zgJ$p(CR!VJ(KK~>7h@LPGu^-;WD%Cxr1*TMW> z%<5tyVTio1Lx?8GB%uT2b_OIuZx3r}Z9V;m4&5arOs{d(OVt|VL>@KW80xv#B9tap zQ2vv>L&LG@1LR7^&shq?nOHG#h zUbJ6SSn>eEY30M^!*P^kj;WLd!NPE&%ruo7OS^*18p_PD-uU;4ckgj|_$~{o>q+*t zGx0;jZ6Sw(F3hEUc8+(jukG_Ln*rN&Oj3ZQC8RiXN;A6X>4!C5L#JMlK42_Qj>Ckv zx@K2X`<;sQE7%w&zGEjaDr|OfUf=azEQd+<^EZ?JZ;lav?BxH4*AJ?12SIRb0LGEe zb}xhfcr#d)->0n%vWHd8)yUH(C&$NQ>Dyx;c8BN?RE==Z$@u}+EnnDm(SuF{$`5mn z4aY1ho@>}bMg(`Lj_fVj7EiADn}PEaq>Cu2I}j&t2zDS9@hy^6m2= z7qXC>xkiXL5v15k`<_2OPugopUsb$TSbKY)MKyOM#oRaDA3MNfjc~0WwH`i9t+YmE z4BwPhdj`^dOmFbGmzt0E`nP@jaS!!W@epig^u^NFqbH4{_rg`{_!|OyR$`)8H;)vp zXd(!b$gBkJXFJiO8trej-*klVy;Hr+a1mN-Url^G|)4i`&`}{22WER%(WtuKyAM`#D zDS}-CjQLP`>p2%)!`Jwr4Y1y3tJGQBJ~iL`?+3QE z&F1jM^C_4Jc+?3+5;qo&;T^zhkv3aQ$gG^p7&Q`1?$II=OOfA9-uDYI&@n+)Xgh`dKQbOd-v9sr literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/walk-04.png b/frontend/src/pages/playtest/testing/fixtures/boy/walk-04.png new file mode 100644 index 0000000000000000000000000000000000000000..c3ac40e400cdfe8ebeb8b69f7d57817b2d60549c GIT binary patch literal 10956 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 BImG|~ literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/testing/fixtures/boy/walk-05.png b/frontend/src/pages/playtest/testing/fixtures/boy/walk-05.png new file mode 100644 index 0000000000000000000000000000000000000000..12970d8ac8403033d3daf51de2f56d3ef552b204 GIT binary patch literal 11112 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 dtnVQxs+skue?A`(0x^~ZHS^^^k=VN zju!JjCH|+2zZQSJ9<7?o8gLK3M!vOiD&7-%CdS|~K&N9AgW&#alOad!O(Vb+&xYb^ zc>OqwwXb2w{5nJVa%@yyTl+Yz73z97H>DJ%bB*0=ZNJ^4gmDr@{D1z$If-kXx^y1W zF=XpTM~@X3+cL2}7>CF%g$PN>a@Y;8t*sHVy50v(@A>&TyUlC2C*K7^`=6--yu*yE zcj)jHF_{V;vG02%i+*iHU~2~l^)AqGD9QWxOf|k+_XWkoqKDzE(V@Hq8T2%;yNK-x z*V3Sh&KBvk&ex*N?s@3TSZF-e_y{%m+m6araX`=Mte@zP=_65 z^r7!wePg1!G9n{kHnI)(&)wgytPcPDxpic6k@z79A5|f3a&@4v;8FAXbw_8lTVBe-`lNOp4{h-GLbFSo$ygs0qFPMJ#b; zC<#g94kC#1<^qYE4#IokW}&RNsO7W10pvuJo0^F5S8LuGLIR|lIpJRdMzTL3#;D8z z6WwFi3L7X&%@5jh)U%tr1phP|yHgc=9dwC)`!>7o0a(v2_n$B?m>@zCh~&MUbT_4Z z^5hzi9X@$+I(V=X-kv1@$z04HF<5+_h@oVV5;D-}h3yd+NN1(o+SCwWsb2U^6Xw#(Rs}>o8=<|c%cWLY z_{+O1sOJ2<3yI`(TSBwnveS~?#tH<~pJS>&*p6NOsB!qqe1W0FzBrb)07fpl?nIi> z>VqX%xey_J9M%|vLj>dhj_I30ZVY{~X{_xhNaqhkru^EE;J6u}e9IK?bi&8Sms(gz z*q6d4+X6%hLGqO-n!LweAQ>59V^Zd+;yS=e-b{AL591*&g?XO=_ro%k_Afg>oGzRW zEiEl~U+R%Pqz#TuEMNkpZK>cdNU2w=-~M8?kukUN&1(tQRLz8Wckg?t+oRZxDHLQp z{<+`8%@S2R(y_ly-o&tevtJwj5>KHl3e`U-;ciCT&xkWYS2!s-)k;A+W;4yZ4U3~6 zrofz=#K@qV?c6`GKFwO>ir0?2Z0$zCB4?ggI1AqpuV(u6*fldo-mZs}0a)O8s7XEn z;*Bd=F)A+?j79amd^ z5_MMk4F$FYL#=pi4nQ)8BT@G#huC~mH??unUah7{O2Mcpd;5;oR%&q?n}5S-NUA=nnH{ZT*1{AnlVpfgBkmczHg+y%&yB&EVs3c7!K>Ot)pFUZuxnAD#f5rPOp~-Zgc^)8t`uLiV=a@CC@ZGdCp$Usj9w%? zVM*q}{d;YO=Zgu&rmOABBIrnQfFAG85f3$4$ikl&0})9D0<@WAxl@$C%VmMUi{$m%YXZ-e-tIt)WBH2Ro^yWw&zFOuT^itB}EuFK^3U7*)PrT}}Yt z9cCio!kVcJRLgI(hmj1z<*ym!LndK`c+4u+A7Onc$3v0dC74qc`dS2M3&oXF zfo|gYBw9<0ecgc%&Q6&V71aHkaCC@sL*re-g2b2YK!~Sz{e~S^;ZtNu$2rA20h&yj zL?6H18^N#KpL%;kf5Q1XBO#pn`%w4H#@6il+X?P0o_6M-B#z`fmY?ax2^-Y?g9KRU zi238xOcRQ+L6niFHA6I5A_l_wknY{Mju698WJQH;j%2m&n_pErWb*8%7t72%319|p z)*g8a-V@l}id=jy?Q5@O`G5Qc zKc`km3%Mw%o5T-47rflySrXJt_8KDX|NVg!f9B)vtPW}Fl|H+q4_KBo~Km*kZBZw-Q%@Qqh#i0A*NH11S0)XKspB}xPjN3H3? zgD1tvuIt`6K4tts6l2J6N! z=!d5Ue6{M@=+pXF!^1lAx83t}o|6*5?--l>LlgDww(F@_BEKCTv1%S8>n8rSjSXRu zSJvP?rj}WE551$AD5rBs_p#AqqYAcjPZcaj!oG^nlI(sI-_)<=&#}9DzBY9WrOX87($rilgv2n= zi0*#BXRIr4+RqD%=MEowi!8q*N`AWV1G?a+sp}C|9TO(Bpgdy6Jzs4)W@>b}1IUP~ z^?Kf7)pvK))Z3X(g&I=B7%9hn(!At?SCx{;0TPBD=T6B*`Af5U5s7YKOg5p;Lv zyt>WNLQUZgA)bPRDOnjnRm0G@eM%avHNzApw*2R3vmWQTrg~}f+Nh^NTI!PFYV4j(6<(Aq%P0cgZdQdz6h)~LFe+3r89x~po>;S(5bt1jNZW&q&@h;4qa!C3RHU{DXm?=&|xE%?fIsB-wDwyS@_ z#?@F)%7xGJ+1TlGhD%R))+8c)Z_ef*?HHt9tXU}LS%XS5M-qCn#peHQgqZAf9D26dRmX_OAIqjSi z{ycR*3{nX}A1#zE5(SDv7tV^+cMpI!WJL2;50b7mm z*Np*N&MJfVPYc;+`@UZmBPx`VuGfm@QleG*7K|NkE@&Is1|<_VHR5vA zz^RA-3X3cN z4WT~{!24Cd?W>1$uM6EaaeW;981A?}9R9&$nubn){^x6vyvvn(Y~Bc-8kZ4j_BAOo zTMY9)H;5UBS9uw&P0Szq>3?2jHiFM-@NJ7*F__I5k_&mXMZaD#*!76A)SfM}EbgXH zwo-2GyE?tBI5C8<-<}!gJhLR}I(t)7{*x1FN|&)RLt8*l#b8?J*1mq3`?<%@+?QJ> zl`vJ)jtzXE;7fDXeoHSj@})qDt?Ff?rhiKcJAM`(L+0SCZ25_|5=ye2IsX}l1TQ=a zs)Vc|y4D1I$9L2?a}2Ei$Z)mE6ykXwDyi1xaD!CA$oEQ?8K(=0>PGvOHv;un;5w9Wx+R;45wi9Irs=~)iD1l`7j1l($)>1OMo0(py{485 zYZ=xK_&U zBgp^nJ;>umk&H@>3$dj(IF6!Q&Xn~S38}t(uQ5oAWO!BO+4YR|_OU}&N5t#{Y?Zbg zsz`>1%cts^n>Fa5vA!#|qez_$&sN&JG|73^3Sh2nnq;rL^Z_9*RPxwEiW&2NL|7)i zhf?T2+6L{Ax!|~%2=Ms+~)%~Pcx{7!r<74;hy^omdm3Qp=P7q3`*jn?pYUfxF519;)3tF#n;)R64@nRW66n#RTG|4@T7W7 za5U$;alHBg3W#qc5^Ph^zde0B_S@4?kTBIwZ=gZVPd^2k249@C3<*!2JX;ur4Wy3F zsdlJ&$sa{xy;LMIB`h3#jY~x%3>kJ7Ru;UkU**_N??=g-ZSB^+*?)}LSc`xuToy_A zP^HFe()WS03y~v-8+X>CL#oAs z2$2g_$g`!*yNWfRlbn6ggi;rQ z8AHnlzZLg(e?#i&4#IQNk%8lb5EAkpe6O0a!Z2%sSald(3GWht!<`HgugljDMI#~* z9QG?P=@<*%sGzQ-1}^MgWBVr2Zvm%0^{5Z;Y*dQB&y&eb(=IM)^RgRl(yjbt3N&g0 z7i9^Ju-uCaQQW*kp9$n1hh;oEI8dQ z+PMTlj8#N}#2}g~M$Y*Rr$+GL+1^!hV_KFyo4Bi=gq!C<@&6r>gw0KMEm|9wcT(}w zJ#D(0xpcEuVU71JFk#=^COzzXN;Iyi@rdg1t;+#l#KD{+2dww$RVWi14UCQ&*7DD8 z^T7iB+2?$fFNJJJj8b+|FMSs-b%sWFm=4d8Nk0jYcnr5~NfdmbXAsw*2d~!W0N;jX zV_hVdx$e;&^1L<3=MDViZQYMyf+%g)j!%S9ATCNytsuzIpThvj17ns~uNbz27UC-5 zr$-FScfuL@?}7{SmSmgg;V~rvsM~;NEssHRo@ybRRns{7qgmHZ8eicC?;~ zBI3NK&KU%8LAT1iEss$wtl6<340$LIbZLN7!4_>^pT;L-zYZK~B!ue`X-+zn)vgv> zjnlvie=5Sr^|HjQNv@gCM?D7-BiZ&|xSw}LObsdLl&3jrfjIS|b!x9_z>%ip5hP)) zr@2|?cYi*`H-VR%{tkwli4ubNe9}e^4dlk6xs=)|tTAEP(k>w6Ndw#KsZTwNZ}Icl z*woY!$)<3V!)9kzKen{Lh8I3}ZrNhZth>q$a^px(dKljw1(F-Nzvim{;eQ_mk6s?o zDk@*`sj+gy0X*STt!^LdeYQMi=EzHVdSg!nac*w?Y)%t0a87{&ui<{;RzIyoWE&%1 zFp!;RnVKe>y#W6K#GMo@?zAU=2;~;(YyAMF$UC$8xGYp$&tFvEGJjBugTX8nc;ez# z8VS|tf#p`vVeTHDsreQ(>45Zi59sc;AEbe|G023D{uTU63nnhg3q0(3w=T=6x5%dp zX-fMzDkM!Z6SA>TW7gD;`k_KV0z4LVITz+k!Xt*MEnt5fi;v^wkv>aF0>X86;g}jhv)uwTof60Mc7x#`ZlBM_)%sbZ6k6kE{(!CqX zzEXfc>zsk%@{)M(KR35+c!th7M;UfHf(K}FKNhiSv?e5Vef*-5@_1m!rsy~zY4}~- z9o+MEy_>Ht4R{4H9y+R`bW)z-IAl$^aVdtywa=!CEAj#aiP(+d{2UDfNDFi7GqV>f zOCC3uOIV7u2-Zuty)YGE*~m?c1ffu;?P}Gk6rk7pk&4hhDVEUaky8s zqoBdKrjb1wirrV~thvpzhKLJ~@Q~y{$f)k0UtSa_Qd8qXu^T7<<^SaQS`U~#v62r^ z2xQm>2{h}(al23ye$&=+VeQu}@M!)+HKvlOJc|3a@1!1^eCT30Pkd9bqL zRbosKVGy1J_)rGpUJ!Mw6=j8+Vz)tJg!@+dEI$&F|!{Z5|NHrWysP(Gf<;p6?N%)Pi^DhS#Re1!+#JW zv{biRO*sH-FctZ`bCzyLZtC;?1@J#)mHcvjkGEGajocB5>QLx+Z2}u#>O=V^*I|5z}#5< z5TI6o_;fiIzBfaL>g|JU=4#mqU%hthP&IMQff7?2=m6=_{bUEw8{#)VGC*)_k1>kq zCzkt8UF}di-O$CPz;w*@?GD!ac^?81e~PD!(jLJxqybqVSN(cqrqnbEqk%`j0Fc?6`Z zLQN{2wM{CYzo8@5lK1&S^>G#}UoFHio*73>CtQN^QL?6=o&X*a0rT zA-U7@8-Gv7VWpd0>l`9e-SsC#v*pj8&VT>@rZ}wMW$V=d*XG_%Nt8`GPA;ICzI*0K5ZgieW zc8P!doW3x1eWZ@gD=*f-2aS7_(Z1`RDoXLa^bha{|HKTF=VUFN$JHJ2y z_Ga|8eMq4{S7(BMh+UKtu{dVKac%D@a)G$KmwuWn;I)P{tAAnin5Cr+2kT)Ba8DtA zM&TvGjewDjxmlGNBtUq-$=qxh=BoyG4-wJ8?WJOzpKrW-wkfYQjXv_24QO8u>&jtj*nDaD?c+_=PbV z-(q}(iqv7ZZ3Epff8pJrPA1Z=7@m%;HApybO$Nn*aBH>--z7A^s?Gt1QWI#;yS^RR}7SHD7^{TQcC%Nz_2F#8G_3N4&V>ZfArP!&ku{2(^T* zXSmuC#QroX@_3Wl&wL-#SMt63*@k)iVzklyr26f5xW10Nj`j^&l1ec*Ghw zHmZvG^j#;naT%vFcGI+#h!M?f4wD!5p-V`s{UzZgyJQUnHf!!vmmFQr2+7FK4RiOA z9a`Y)8?R1S0VqSnv;EnaTMtsU1P@KH~-YL2)t#<^jMh%>|0H|1S2chUm|B z73_WDJ>=m4ZSSx)_7&&QHR_~51(UtEod`cZvTw7auDod}=oU;oKQ1#t%)m-izqJp5 zjU{!Y?l=kS042yW@)t(NICcK{U^K?NnvZU_#WM77Po3?OkeDiFlt=ARyD1-rJKFyy zjfWp^&6U#YEhh<#s2zU52NgZUb5G4oJ_Uk4lf`SQgQ@scT0JsXnV7-8JCC!g>?B%S zPZ<-vM-fl7=4&WA6|sA&@78aduf+sbCT$|FtLd==**Et@zNI3?A{Z=~4$5jctkqjuqE?5$IJ56THd7*Kck4TR4I$XKD#K@jt;Az?4}2VTt-P zkJU69?q5Gz6HH$B@eJR?kZK$bcv|++gr9#aAaz`lBlAPQvEZq3_GSth&s{zh4zWV~ z){>0|5kx>#3H7(QOW#W(SBv?ZN@iFmp*4z&@a|K%CZyY0&0D6UaD3o5j>%>|3_hXD zOR??IstlPR5MFBDkt|XNjLuFEzo){|kvc7lxA zey1E;qQbzqnj0l&6q>>0o!Z|OA!M@2qTL6Fu;P`YkF}^!k&jafSA<`QlDH`mEYN$) z(r(!fH`Vml$5hCsAArqIe*rElT9{2=5qO@53ejFiWvizHJil&?Zn9h!2HAsQwI>9mZh`19TDywG0( zq)(pznz$pg?&$U!@AHf2od_l}HvWWUaY~!~ES{O`w$d0*0vJR!dNEBx?yARoQvrF4 zcG$WYeog~X`q);$hd|G=2|lNU6;Wu6@gEV1h$tU)xM7KQ|@ zee9x5R_i%$k%LTmwyDjwp_FLCc{RXAYD=HeU|$ppTKm@7f=2FR0vMHyv9(9$Y>#~< zAWqaPVS!9Ftlz%d6d+l=LG|WGAg?PimS2R98Uy_eMX1z#g~hR3e`<6jF2pG~)VZQD z=hI9xz@GSIwB|4$+?XpXm6t_PW)A^eNUQaJh9=T6PW+jc$W8U+?b1*I*Q9ixn=wZGlOmwp zOU|+7Ou6X$G2pB-weasi*u*UCE?afnf#O3;CQPzgH(!U-1-s1{Z1su-LIa~Z`3dO0 z4Y(O)#9kJF94<@+Jf+QVB0q*m7lp`LhfrwGX$1Rg)R!|!lSDSr!&s!2ST(;AA`gn| zg{Y2@c;_09?QAjmkw(=E6v@fgIz;JY#HvL9{*9SBScy2mwVpsp_i2m9`B_+ke_ni_ zwZU6Bo876Yp>NzbT8}TYO1PTu9$sr~7W{pU4PFSna{vI$A1G^G4WKM?!ebwmWx0cN z0N+&U6@s{DpV?7ithY*SB=RTVUQ3GpLc-LZQ(X_`jro51fFo`Ab)x6wZ_Am1a_$_NF zVG2A#pJ#%l!sfZOEw~e}398KN82!QyomD4qnyppH6DtH{kk9X9aO%_lO3hWh4YYah zX$N4VnGCeyK}puabSsH*QYJvc!JYz{QcUo^6&_sati7nLl6G<7PUxn(CSfLchYke4 z`*(-9d-Ni$WQ$%<=(Oinz4Cw%vu}3K(3B>= zobi4zHM>MLpNO3(ovJTkv zv4=uy`#0A6mcZBSw-3J(=(D|iAp>Z2F(g~E?u{<=ed|;Yq|7le{D6=6)?+>UCM(`F z+L=l}Y3rOf?n0GIHOC53BhTg{Yjp>00oC|<2E8PoT7uNt`Hy*f`PSDC^k?r=3X)I$ znRE?TM~LZ`GpPg*NrSu2h~YL)IS|g7R3NH9+kK(t`m~X&(a>7Hre%asOLM2mzoE6* z)1QObQOBwvkHYdMSWwvo@Cf68~4POJ_*}692(6 zY!RWLzCL-O1YniSQ%l)6b zf09S$76@*usD9mU+Pt_Gc5v#Wl-ijk<)mv7s(Jaty8NDMn(|{AUk*8^_TLe6huU#yodwNtwv+-Ukt3X5+YAe5Ra>qgts!_S-P;{h*SE=N*mmwWad^Sed zJKe}#say?yKn@BMnQjRj+$o>L8jyScH1sDzY=e4p3^B)v!B60Ahs4Pk0+>3E1-|c* zT{N7QLiCYV??*A%2>eQ(2%V0~VEHLqhoS!kzga%7u9f#cVHe-2es$vVA*6^(EB{i! z2#jNp;);r3hOu{3OaX5|z_;7QQmZFw{5WC5eqZUrE`$PpnKp^Bwbk{O+GYk?UGc|j zRn__xzqD6oK7>Z9T1Tk~R=>Pw*-~EciBgAPd%?NS<@lVI>O*|oFT%taAxcnv3vfbn zqXg3614+b6Y`?NwZJs6_ry7?tq(A^R;KM(6$aDK3ZU>*f?-(GC@AXzk7fgh@Y@+Yg zBp%ur5JHcmQ!H?FJ0jKRE5XQ7yI!I_tuq&;XMZVfFh;}vC6JHaP+kWw=m8x?AqEw zM%D?V!dx<=$(t*#dZ&;I*B=GS>6|l9ixcAo9uY{cENmEPz4**hg^qPu?APMV>*W9) zvPK8E|ET3feL*d@LWpsR#fIv{ZJnAFH8pqjfmRpd`vkj{|AUi;0w=u~Ca#?_x8XDn zJINr+XXRJ)JN{P(qr9DPtsWRs`06l9z>s@y2)^Z|tX0gjkzHoT>? z86?LZe3H#r$DpK&*r}1N;5A9Gnk1xH_Z@K;4hL(Fw5IT1b5K8J{_?NB$XLK83y(@! zl3tQha~DT4=n2Z>#CV0^Lv1F>PCC%zQV@8gF!fn$1RKn+%5e1 z=w5t_c=d6L&*TF&kz*E6NtKa#$omdQB0I;IpuhHH8g5MHzX(v*3@3gUHlYKzoCQm~ ztZF-u41D&Fh(Vdg`#)caP|V4OiuTQw(>yfr0A@HB;Tqms?aG`+I#fQ^PJwF?QC=D5 zvm?dvu^izy%NR(>;-euC}Bm0d= z3c!^$OUuf}P6po$KU}5gg~DWFtWQq*-nnZwM6p{6M3d07r@<&7)BS@6S@Y zh;QeeTq#Dp%qFcxxq~#d zQi%`4W{=vHj?h^p2*8_W8R22o;3dQrs{`;p-q%olig3-q3NhR7)sG|e71-f@P=zd( zppqT2tqYdJml%Jd;mEnQR71$TsBF&PUDMg@f51p~DmYrhNtho77^v+_Oe=Ub=1b(f z?fH?w&FMk(CmS4l5C7qmN>=Y5Y?f&Y12xO$Kn^4>CX;~v3r>MTUgC)2FZKonF3BZ= z<2S-?!QS)Atc}dthkl5HaATGvxnItu1*@rm-u;nhzt_<+qAh49xri7s!e~{Bgz&jF z8%cU^#TT7`A%#u)Q%iG7>xM#7@epa0|4B2S;O-_~a^FOU&8keFEN z^MQEqvxFe~d%@{`kwIfAOhFPEJ^f(6t|LQ>_4E6*MQoN(=G(IW?4x>m@QwOQUXO(C za;ngf)y@m; zuwU>p!3OE0Q1qWQHTZxwwzDOEopt^qvUnx$3nUP@JG62lY<*Bw(o8H7h+;nl3%yS z)fR*tEf<7-g$x(4PG63n7`dwfwNV5wXZz=^_Ai8Yf=UHX>FcCq{_gZ1mlo#kh#3G! zTPpH>G4lah?eTXZxaxq^e)W9F8-%?}uNXgO6nRrhUL7( zS=KO!ulgYV15w3g_1_H!LC=jpZT50&&f`uEcRkaFMD8N`odFHt1ajCpNqT`ShVV0> z!g`~Mi%N%A)zqb}rGok2ptZ6KgFzE@Wpz6ldy%_AQ)x0-iOMN!{DnS>u7+p%Cm(Xu zN3xaT5!5#$e3Z1H%PVLzkrx*N%tc2^#^KTAfuHwa0#qI50}Eq#1|b3QQxl2>>M6>{ z6~h=Q4h>;&j0Ksv7~&0Kvi6W#S=U@+VGeWZKZ8yT?DTcC+WRne8mDNy90r}ojR@(= zY}lJkn!(C%ku5FGUd{oGc~ufl&aG3^aBIaUF>W- z_M840BwG4n9AVuUO7wWj5^rNDKeB9T;YoY`BOnlX1`#DPm4r#OI~~4!-7Vy}R_AJ9 zF7&mc5T9wr!AahS6XdAJ3pPSO+5`;_Kldc2C#lWP`3L5qqZHQo++dr5cLea;Mi&)G zg9DOgy{F|M8U%r_x!R#-`t)C|eeu`8{Ge5k-oL+UtMEt4KNt#;!IUdQ?Xx`H-4Zv- zdk3=eoS+?X9v{aRh{WJf4hWaqgq*l6@)HT%CJ7}c|J*gIc^6rJ3pmpR6H=M4>zG!wb9$V=5 z?_7#KojOkGT0Fc%deQ7U-CP)WI#*gaVgjPb{X!6iZ&pwJ$ zlUO^y47d~%69a87BB3=9`SjlufV`X(V7 zqVsM6VkNCa&4DrK_)pTx7R}Fy=$L$zgDmlDVB>a`w5h0?3Vhv6IbG9kZlYUnMU^jK z3m(zavj_r8YKXCho=!9nq%b>)=NEFx!zU;hfebh)bj#jxZzPGfQbBL+#N1w=Pu_aP zDPhLBp7p)vDP~ioEu|qeFD*_^{R}lNF}Vx&A0!UAyhQsnb)|j8d+4`!bgdu|rk~?g zNjnnRbw@}g-Pp?pNnW!$zI6TaQ6fI4+Kc37|B+}<9b-j!7>L)$&u{2@cOi&vOI(>h z><4UlbKB)(<*nX=&$6@xVf@$bJY_F#9T-1~E5AK7_kDJ@XopR`8~=UjY*hW(u`P2& z$#E;YBdRLmw1hFY?$lT_x4*IHb!rJ>bCg+3R#NFa1DY!Ye~0R>UHK+xyWqNz%lvEo zU&!q(Ir9eyF10q>)?HtdnABc$mUVViE*>7me(Mg&#RO?PDXmM6xbTO(4FR5L zDwI|i`N_AEipaTTf68HCjE2v^g7iH2pYD9sl;23%2(Kn6oeujZ^_}2sS(LncN6PpP zx|>-@giQdbyVaG<2IBT4rj*42%7W80QFq+oHJ3Nd6TPt*R-&&58as`uZtM^QM^{6E zIpXwJoyWzvhWJChR!B{#lCY5Vm8?r;2kBL&hyc$$`Y(VE7=~%RoH^KA*snIQ8E(^f zJz;E9v@e)M?snq7vlX4$TC?kWIQ&s(!ayxIAcqd6^*Ug~*re;DM46WYD>dB3g+I6v zgP0#6UeDI>l3Vwz6Cylp-I_EuG3%b5<`NYBeR?I*jG-WU0`LGs{t^;XH?IQ@>WYXi zr1Di&4(bxAhpYADZ7R>7$yJnWnyUS4jy+%}c02OP-hyoz023^yDxV{=HT3%7WqD$} z()d;p5pD|__$2U({11EIWt5S-bcBA(<5V)u7W2#Wx666m^lo0dPU<`fZ<4xx)w!Y% zc|(f>h`xf&oATbyT*g>lF+!sS@O*Xe{gEI)q&JF|h<~jG?Ikw^#6^~UWdd_uP8rVH z^YSRZmB^*MIw;Uk{(7kjL5YL7-ZjEoB?#0@$!cK&PSo(_D8etee{lIxbTxse48_fX zi8t022S;o<@64=!GZ-B!Fn!=iwos+ppc{shV2uEh-hZzQ zetKkT#hpVHr%rFpO4F8C-?%$)X|S8d)8fXnZwd3fPCJM{SS~zey!2f?yp$E9vEk+9 zP)H^u)7X8bhfbo@ZcMlncg6&R)yI!N=Y?~0JWI^5 zLl<{OXJ}WhJ9N>O|Kw}Y#(FOyo36uxS8jVX)9AS>4f-fye8w$u^Jz$G4n^m0lp6+A zDEG~~Ym*1_!(?bJMAenk7@MEUtI3xM}Z()qF7KcdJZBKQWaKpx|U@JA>JVONbPDFusSXt(GT zqStNin(;jiioxpQ@{|Jt3ApdcN0JPVRXK3)t9Gp1DO)JiM{7+ zVUYRdU7|u-l%OAw>pLYaHYk7-H?It1>o*=NMx~VT;?%D?$A{At-$RGruH20~$+BMd zrLFuqjSW+M=@^?5cyLV@(Rpqm0&b8>WPT{%PDvegPa^F42<6D}$Fn0$uxtNX%Xh#Ze&Hg;B*rtPF3Z07Z#5$_ujTC3o(RLF8U)pN%LxDM0~UESWE zD1Ra8+{98J?&W{6f=uJ%qkA|AjwM$qSK8Vii^g1!C;5K*nd`Ox*7#o)9ZFgq-SC*b zw1`Y!EP%`aYDQyK?9rQ(|Msn1x@4H}AQCfNvum}b{n@R+7nV{H6 z4U-B}PP!bkXU+W!?OhhHD8M^9g5^|I!AO{8X^xm86*NJraZ`nsxivMEB%wFrL98S6 zK~M{LT@QV1$7Q0DLk=Rz8- zVNqW=!`E#0>HQ>=hhkL$T1BqiW3JB}y4vTmPy?84o0Y7JWjxGb3+>NmioZ6FSKpZP za&o>*{^HW{DJ>9B6<-@io!kmHMG;0A{{$gC9Z4@#Wbkq`o1Q(b4KO%JHS&uv_>=-G zAC=;{AyAMOF0*~zMR2sAT=4V0?OkVw=qX78$r`vf7Mm`Y4E8jGu6XmviwyoUd{8#n zCU@RDFD1{Ry^&Xm_&L5r87yhXA@`pzgd{3-+|Mycz=QP#oOYd=j1ndEX)BHN7=ad! zeh98yoj%G772&w|->aia%R;T@BHiS=>g<`a*FWqb4@H}S=Q`Q0B!NU_yK1Cw?!+Ug z;0b(#qRE?o1Scq35#iKY&sgeKBU1#XYnD~!=7u=i3Hosr{FaCTWwvBZxi5fStTiDZ z0kRVtALJ7b6-lFYWga8=<0~J$|M=k6rj5d@13&b@FJr(`PLQ!);gD_7A7kHK5oSuC zC`^STCaaE18n6q`zM1?7_P3W5cvvC*ygLyt2b)M~KZwaQ;fB zIs`N6**1BWK0S``^jX)0r7Bf$YpBlQQpk0G@<)_jv$}GY;$$>`WGFT%zjsbZ{K3JQ zKMTpYl{u<-V!bI{{cv$r-(l#W_0y7b#^`d_%wh z?~(E5$S2bJOt*`R@s-LX%55;4opcsLNu#V0boE!}uXkAg=6TX*lO<_)Ysp2wbDpPj zzH>8=ZtS2IVR~Mqe{lMMJfhh`_k}Zo;j(hwi^mE1NSQgYgRZHgopX{*L=tayz>L{< zX<4d+&v8EmRaj_KV<(jfW~7K{3EA!NMR-ImoJP0uB>S;{Xr#?bBi^5s`Q zYuuSksY7w2Sb1+_yR$PgM(@(NZmJ^e0P!_tn+oneyVUBM7tI}%TBnFcj}+F35Wl6n zf3b-(i;A!zNri{6x@(H!82D~XOHw7d`|9n>Mno7+R{?C?iIA2|bP9Y5k~cz~THtpr zM!|-ttg3R!L%o(qydqgOkU>f!L^3hvvioDKfnscsizK~vBAX^|v8BSo#(`g(h|Sw` zIdT5Kes+8xW#%_%CE+iht`T=1Q$oGS?NQjUHwE&~pchw~>Nd(Ki;<>pp*>&{bf@ zh*^&$9|ViBPaI+DV9)(u>&5XcT0u*jS3;d;W57vq#c*rs9CekalED}=4fIb=OR$0p z66y7$!HHd%gpQSuz(0;csP1gyY%V;M7Q)(cIoxzQ&>U%Vla*ly?TcMlUS96N;4oaR zV!??FO4={)=%=o(GdFU-10eK>*64>SGh=+zz>T@0V5Ui7m zrJrJ4t;ojDckq}uFprX)c@V70Rzy}%SV)`-i$`>XIDTNR1?!U|Tt#Xrtmi{|4?I&{ z@2a@{TPb5|Gd_@ghhoDRKeZI>)f{=YEZE9$~=lM#%nE+x}>#s5CgN)~aVt zJDs)~&_O^D#UA$)qe}L7<)^!aa9;j7-fn9(W&Pym9QHHv!z(waSCxueweF)Dcf+wN zdyRb0ZPjo#!cH!2xce)(-Xmd>ik*QR_I@0(&Vld{rA`D3vgnk!sKOOB@<~2Q6@qr6 z2jrJfqi0E7?U=v>f#An@>$WLwlgWG@i^H_5fHonUVXv+tH}wa|KNc#YUB3JO7$Q1? z{;D-^>fqi==rnd$QgO(~=F5s+8C+d8QiwK#1*Kmk{`IvLA4{agkX1f)7lp5l{M+xY zTMo$FlciL`ByiQ@UK}kj)uC)^g#hk(Ri>#xq)CfhecrVCyXrCYUK-WZS7nH_6qk-N2GLOwn^& z$pxNb2{tmKRC*L$HOBJlmqGn@x0_Ps22)(#mjs#{G0L@gZmmy;ZzrlM+ZjtA`z_Kl zqjEi=PNmn1KFc3*y+H4Xs(kBN-hb_m>t0$W9>^Gbd+>?K5YPffvsS&gs(kz>@&cEZ z`oELO)9r72iO?Nezw(8giGN9O?cRX*a*CL zJwgBhr6l|;rYPwfxVws-Z_^;&r4~cJYJWbr>>NI@ArJC=3R*iKyCtLyovXb|+fof? zPZ9dwSX^MvAj}u_-dn*|LHdvDO|db7-&50mD!>%~=x2nCZ0tECp`ODZZK)U;gS;R< zdmteQ={KtWzAr6~H7B9POei&Q>{2wZNo)*H<7rrzl7Tqi$CS?`bEP(&S9rbQcY9Ll zkwocjhoQ{_Iz&x?RrCkfLxrj00qy4S?ewP@^dSB9uCL+1=N?T8Exw}uS zF(IFAuCJCmz41q<+EQ`H75U@L->0%awk#VCCRJ0)CH}$7UUU!6om0zl5F|)5LNY)1 zVBi_MbC^loygS;RH7ITN^bv(7$clpo<$RPsp6{`vlvH;xnnOv_aBWE_!)ai(8{tQ( zPU0(rj`m_~{k4*JJ+wBixJ6+MXg`>e8yi*dGv5Pme#p)W>##5$$s+%HjY@-BkPWWP z>J~lG$FWp!$qgFr9arRyYkZ~K?h~8$A5I-5yqF{LRO#AUC_oyY&8*6V?aVdTI3S$~w$raOPf<^7l z#;{qxaCR0_Y8}f8%EXCV$3RUG7u2lc`MXh8d5Y>Iz3&Z9>(NwB;&eqm+$REHNBG3L zPuDdi?e^V0S!4WUE+5%zVnW4m6Lt1-VkR9Qu}jW?M>el|6cUTXDhCd5Ep}kso%Vz}9(sO1 zOdo2X^|x6CvSkCmOSE1l#!EwgI_sN>Glw1-ZGcwoTP)D*eR zLvltXUVTBfRLeN@y9lIPFIQ3rj4?AtAn*pXNbv*biyjA$uM#nvsoMEDzv5sJ^ z$EX8u2ueBWgh?LYCj2l|LKSzA5EXlTW5Ac)g8R#obW;8q2NvUyCzjI;y^DSw`Q%5i zwrD?Kx)|KB{OpDB0D!xLB2*LruTP_u7b2}HI=_$BD{1U>(P6;deA6_Gs`ty9ES|fb zC;b>;vOS@VGS_l^9P#&+F>XWf@g3@)_7J2DZ1tPSOd)YAFs?{>BVCJ6pzQ5#fa%s> z=64}m<{6l3LbP1Tb4V*TN9Wq78e8#n^q>53aQbRPPNDo0I98%TX$50U-uOaEa}JGJ zkDkrFG+l~m0+Yiz6Pwr|2(S=kMCG$F7R3(P+5C=WCXi_QYR5fJYSWQ=6%#EMBd~VZ zoEIc^Es_caRmb$FJ|1>UEIe=xGeUW?lk1$0ls?7v6|5k@T3OWQ*f zGb>BXn$f7Q4E*T)W#YPWoZazjzeVQA{%XJfn$>>a)!Q&) z40@17=n76`2~PT|V1IKQOO_1QOy0I>Kq&0aUpjR@dy5AUzqw;TFrgw|qUy##BiFy9 z!Zz|f`res@(uc+Dx5dqnptrK6$0Ri1E>|T@pd#F>=8)7VRXZ%HOwfVsdEk%v_Y}%_ zslp#85kYw{@t)bDFnCVdzL9jVJ%p1gHSOYGd`iT{VA5qT*tJ9%a+JU7jb9{e-=#oONS3P1E@h~=S{1# z6;%k9dHC$o zqJ+`aySrf$xD(vymtiuV1d6T-ibO8;K4*xJkiM;oug(PnF}ba+Y2jf|tj{q$#*mU+ zdu=}NRS_44Z*+t~yN^fWfiIc+j=^5>6@>4VSy`!@oqKAp!ip80wtRsEkY z(A;&o+CiyBZ+6wc9r>431{727hHb-Z@np;|YF2|jt>o_P$bZ<)ItWtly(G>ZT?*}= zOMf5BipAJ(AJ-gz8S3ySZM}_oc0)6!-{&f&Cv*Gwl^Iu;^2^t*LZbbV#_RLu zx|Lx)pOhM_CP{qXJX#NVmC5Xwt8Ck4Mrzk29-Ko+!6 zTZ^Zgm=&cWKQ-*7Z|M#oag3Y<#{VkCVQG*|b0a@`2Nqc#(b~92&MV;0R!}|-Cn<)K z>FpA{nMtqpz#!ojzuLXj^O^8$#+FM&J^ZJ5I^^nq2?oS|w*O0MSK35Lz+57S0bl<+ zL2N=oV{TCqT_AH?#x+?Lb_Avi)5mefErFoc+m_XK#|D+08G21qqC9=*9@qju*nnhx z)9Ruw@iTsp&Vd04tpW{=Uhea*O)O9;5yWA7iOMa7Znc<$qjIeEsfgTY9)!Mq*`un2 zyp*}Pturf+P;9%xa|mi!dAIO<%LRXJAxenrpi@i#?p{^lx&PsjT!e?@Bd6!T9~yDQgRC&)n_ON;BY zU+AZ+Digvt7rr@@uoV`vsu(Ib(IwI~P4h#@rK!x_BC^}%^YX;e%1nK};X!E|EgdBR z?H;BiY=L*Ny}WPEq@V}o&2-i*m)~I{p4>1!uLq5t>$9m5QK}91$YX<$afV(sF%+N0 z&q=J%cA;7EaZqwNh%bzl&cu<`KsfVUg|NRxx!fOoBk)Zodg=+Yk@fEDSas( zkP1{l2uz=sCK%~Fj@nlyTTzLEi&NYMxX$)`#Qf%Hm^XwWxpL448e)_dk1j5W;1lj< zdnJx{oN|PyZN;^_hZT`;hJH8@q7;vBmEWZe72r+fu^;~Ti;*2(OEcqH3NJ5O0*}wl zc}fKqmA&=kH_5zDpDVQ@Oo4)1nIO`-%1aRFx-kMn+`EkWiQ0C_e5Tn;=>^Ku33QS= z2{|B%{Bd%*j_Cm)^d(?N%yQ1M`&dKGK$ZPp?nbMN2!2p0j(L@{)GnqL=lKxq=NC3b z{~WzA!+?NAkTJNFPqSHbC#IJe%T%%4(z==B6fVzmy030@FoaU0Yz0740em{W%B)Ep zB770w*_+Z>sUgd(QIaE4T|9V_q@ZxTmu+BWL?HqytL?-V*GDeEc{>V7iR~~MECH;2<6ukAc&qk4 zUr`i?ge|=G^Dn>CS-jYqYcY?Q@MV3Y?;Dt!z))MRf!_cJ-8S-0)T{1^@b%z~B=EjJTG4-nzr{n-c@e=j|%FtEBfVGQBbm`eA}xoKzN z<0NCn8e@AR`ZOS5@bkwK1U;gCTy~gh)`vF!R^a+sN{dr9z1TNXw zVl4ybco@`BiTO)&_HAON)Pra4S4sF6jTC4Q+?WO>|M=*I&U}?Efhk_zw&DXZTtBOZ z^nd5Oo~w0t{<|DPk~%|C{f>kydM&S_k87aJSYBQ1U~asY?5N&qe37^4V8?Bg9{PNs zPoU$^4f_ZK(4J(Dlap}5xplrO&eY27&v?h|R_TdjN}zl1D>_DW)=R>F3bFp_4YnSG#WT8CKZE zo)dm+ry1m4`U;snF6e1gY!NnGzst+6E6KHA1})_yxn_`moJ%FY_iM^2G2Mz6Nz>aQ z*B4PJ*8|u?La;JkJVZwMgj@fGg>o;oAz*7)@foLZG#l7Q6+O73rTTUM6LO2(|D?e{ zbH4W1=<&fqVJHN7I1=O=qL?npUU1cW4EY3b?G^G@Dxc=eZjD%eh@<@I;mZz;N#5S( zE5`hNKE{zhc|uj%WcPH?m7^QGt?6#ou1pMVbZOQ?C%-n4*5AVTZ*%|{&LSY<5d7_> zS)tziS;zKA>)@?vpf*bZCrRPTsAsN1=&zEkS{JeDdBvh)=9sD=$R=M|#dfUW!TL1M`l zNt5SdmkEzDfV*-_IcB-ONe$8*kMuAgmL;R>Jr4<)%SL#9cK0M6t?2N57*s!69OaA( zI7hVI1mK|v{Z%5K&E#1(R3r})kS+&N2K_$#!+L|uEQnv_A3H~y6L|aM=$`)`0Ku_` z@T0^oidqPH@G^C_diwOB1RHH$UuZS@x4lO~-^k=uy~l$oe!@o(8sE*!9HeQ%S)foSkwt5b*7u zY;PcD^$+WXN6XXk&>N~um(yYU&OhuOuIDhtaKa>X{$ds}J$=~b)eg6bF*hB%11M7I z$3zNXvR;jjsm6VOrzk=YUMrY}zi>9M@=Y6vQfO@zU9!Ga(k-Ut~}Y4YfzNss-v}TjzgJh&w1CS(=GD_=+1ei`^29TiMwKU9B($y z-En$GX0SEmk6r6|Zxe0O%r^799fsa-3JR|UH9{0fQH4a}-fZns6q?56*V3*c1(!Tv zbtJdUL~P4Lej0{ddr%W#EA}?8!jIVxM}O;5CH^b1WT=7|a%+8)bg3|?fLZhpxrSez zq0Qp#Y-pDJfh28k-GgvMY~Dk8DZHO;f1f6dFJca6gb&svD!PO4bt_S90~xjvMq|MP zdW1IxUt{dR6&1-H4Q>$O%NNHT^%d6EeN;FC180=Ak^Z&+Ij0`6 zZuQ&EF{_BlSbMS!+I0=N9rwdhwpk^G?TfesTcTG-;7v<(yNyDzwH0NVOP^hio*OJYy!>o@sDxQ0 zerWr0X;X3Uz#GS75BH&8+Gj~xrSWc0+2&$AHg&8V4@uO0L_=4rIZ*id;Omu_3MI;; z)^$(1K(5h)ZHqPk;Kq5ub9P>Z7SHK-+*_mpRJD6in-NvQ{3A`+@b>BY?v>ojnU5;h zIeu0HfqzxaleMVTXeDK2dBH#AHh-=H-Q9bvbdG&L!}=6jksVqvB=$Cf#o* z##|vF^PmS}E;u0C51O7trBjRL;Z|P6%z-@a0`2pSKW1-if)5aEhGv;0Ml^(QCz8dJ zd8uYSd)z00K|QWMba&Uc`*{#-?aR8$Uc7RAs;8#XrQ^}CBF{WNY1dk*g?UY@E{%`% zMURr&;Vxs$Z6M)aaZH5pJiQPJTYG=~iaHhW>T;x-T9Y<#!9gjDi8Xv`a{zInOzL1> z=5aK|DJFXlruK+|-Omm;>%lZ>MeWHu^Z>Svrpf~!Kre;GBS3{dWpfOjR61pF+|($uD0QESjBXpqQ1M_Qu`~c-xBfX$LlJLXWMt zv6%AD&O3W9P1^npFqSPkI=s|@b7RrFVOSBMueD_YxG5(uaATah<*+)OMguBB8@OH? zds<1C6>t|I8#oq4Z}#JqQ|b1ow=T=bY3_FQ_4|rXs4tML2)t7#qx*Y76Km8^O=mx7;UymSrC>R zf+Pb1g3ltBtO1+0ozd72`0)-Wb2H0_XRq6xEsN|2HdV7?Z#!jW zaB*<1KE`M50WPA> zoTDBf;9&UA^d?AG&`Mtd?)rL_V5OvFHHs5Utbuwzx+);w+;`uT+W@y7Otu`pxgi9> zIWMmfo~WFv$nWePS&A^z5^A18o+8+Iq&~ZT=1*)>(hwS5=~Mbuc#r>HsmxjpiTz7k z|3@gLjOg#hE)=0D*y#0+T7`@aN}!!W+_mbaw@P2XN{REK8*?nzarHg zf&^VUiq%x6pP;m`(M*GZM&#|?Uw4*B1-~03yYlwE*by(uIqptj{VxoJFDS?7<`^de z;^wi4RF^7sRx>4K^}nVwc5-J3!1;0XkvHytGwhisCOTt$hpZKAMiJPGU}uKBeeIQu z>H>x~?hBvh42*0Mcur^c68ivJdZR+Ig5j)roo|LWObg}y-|xG(eKIVZyXlKfkV(mS zT*IXNK==ibiZ|#_Z(PcNXRe?u>&q(h+#EOC#T(>NA5`+cAv{A`jY~|tUa^F^S!T6{ zV(D)LZaob=L9E77B{JFyg&Q_y@0pu$cqrb9Z$vWb@OM0|=r;w5$JkaAo||{)zl;LTv%j-%{2JVcPGOn8@4`6kS=Pu+ za=O1-=bwxv> zRt6q|c&)@p5Dpn)_&Dj#5D#be@iml%B)2%>^PR7ZQcDDSw^z~z^5wJNx;W%6coXkN z+d~`2HHLzCDY0q^UFI{z2U0YrtI>vl#am2$y~Oy^)Z=qw3MHjZE+=(cTl0-TKIJ!e z7EPEemLx)DE-dx@E>qRyf}a}LU?K*`F2IjdG=ovB-iukX&%;1iNFxh`8%%HC) zTO82**O5YM2z(g4zv{7a86*V%qm7u9?^*=zn1+88Rq6y-uQ~L5Um=gn!+eK8+}^eJ z`KzlJbg%`wd@%HVbCcKk$AP;kTrYxA(0j$BW$Yw7MB;b^$r zbzY!my_O=TC^G3-!hRh6lD*&1nFiWRurjxl|MVXI z4vzH=BUI4Y?gaNu9%hWek4_Vl=i$l^CW5`@S6XXR$?k4{WdRd&!b4=uYZ- zD6Yn<`;)l;0)ii0+CxmX@_Hk>AFnY$L6{(e2pfvtRr3j|WYk-%pa0-fGy{H+Gdx9E zR+vKzb+b@@V?MaI80oap_iVzD*KICpUmu3O*8?;8HsL6^Z~u)<%Je~gWUdC@B}bXY z-$#1330*wrR}jDDRS>g4u5Iz6XQnD{8Dfn@f3iTqBbaQ;2X7V3ewO z{>2>i9lQ6SQJ5=53L{IacSa;Ub`#4NX^3^Uc zagBjT0&>Li_kL(2mYRLXeQoK~Kevf3E`TugQ-x&X5N8&Km^!KK+@h}pWX8aStSJ44 z4lPaBPZrvsH*>;|6FPlAN%iR4h{_#KiZtU>65Jl9B%B@#ej#d+cZ0BncfVF1%D*ua zu5gk7nkdA+0Wx{ubj61@zS$}HF5?RiiA0h9v3p&(%E%k82>gv?XFk9H(Q)!dw8z!5 zA>!JO<;3Sz*VYBD2Q7T3<@v#?=gOEHYjoA?Ow?^ws3Yt2D1Wq>MNtw0%OWLzfgyYl zv5?bn>YXhEvghH8*+v8{XRKim!oRus`6&>RnZ&OH3}~2=>|25^1iLAdA!%5zQVl4D zj%M8GV{|-7A(PcZcYup+uOS{JH&FTPrP5iB%J1mcJXY~4k1h=2Rx%^wYhS{HIn@|x2AXhN-u#!>U*&+)0U6DPhu=6lVf|8 zFv(naQI=E}SV9468O3RmN)5U$Y+)KFq(}Qa$Q_%&A5|(8$?ZM%P}&%J8>bDXrhrN- hk>UQ|4!U&=j@E+>m#m}-N^DO*prxj(TCR);{~udY+cp3I literal 0 HcmV?d00001 diff --git a/frontend/src/pages/playtest/workbench/acceptance.tsx b/frontend/src/pages/playtest/workbench/acceptance.tsx new file mode 100644 index 0000000..3ad398e --- /dev/null +++ b/frontend/src/pages/playtest/workbench/acceptance.tsx @@ -0,0 +1,87 @@ +import type { PlaytestInspectionStatus } from "@/entities/playtest-inspection"; + +import { StatusPanel } from "./status-panel"; + +export interface AcceptanceProps { + inspectionMode: "demo" | "persisted"; + inspectionStatus: PlaytestInspectionStatus | null; + saving?: boolean; + saveError?: string | null; + onRecordStatus(status: PlaytestInspectionStatus): void; +} + +function statusText(status: PlaytestInspectionStatus | null): string { + if (status === "passed") return "通过"; + if (status === "issues_found") return "发现问题"; + return "尚未核验"; +} + +export function Acceptance({ + inspectionMode, + inspectionStatus, + saving = false, + saveError = null, + onRecordStatus, +}: AcceptanceProps) { + const isDemo = inspectionMode === "demo"; + + return ( +
    +
    +

    ACCEPTANCE

    +

    核验状态

    +
    + + {isDemo ? ( + <> +

    本地核验:{statusText(inspectionStatus)}

    +

    未提交后端

    + + ) : ( +

    + {inspectionStatus === null + ? "尚无已保存核验" + : `已保存:${statusText(inspectionStatus)}`} +

    + )} +
    + {saving ? ( +

    + 正在保存核验结论 +

    + ) : null} + {saveError !== null ? ( +
    + + {saveError} + +
    + ) : null} +
    + + +
    +
    + ); +} diff --git a/frontend/src/pages/playtest/workbench/action-selector.tsx b/frontend/src/pages/playtest/workbench/action-selector.tsx new file mode 100644 index 0000000..3853aa6 --- /dev/null +++ b/frontend/src/pages/playtest/workbench/action-selector.tsx @@ -0,0 +1,51 @@ +import type { PreviewAction } from "./model/types"; + +export interface ActionSelectorProps { + actions: readonly PreviewAction[]; + selectedActionId: string | null; + onSelectAction(actionId: string): void; +} + +function frameCount(action: PreviewAction): number { + return action.sequences.reduce((total, sequence) => total + sequence.frames.length, 0); +} + +export function ActionSelector({ actions, selectedActionId, onSelectAction }: ActionSelectorProps) { + return ( + + ); +} diff --git a/frontend/src/pages/playtest/workbench/analysis/frame-geometry.test.ts b/frontend/src/pages/playtest/workbench/analysis/frame-geometry.test.ts new file mode 100644 index 0000000..77a358d --- /dev/null +++ b/frontend/src/pages/playtest/workbench/analysis/frame-geometry.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from "vitest"; + +import { measureFrameGeometry, type FramePixelData } from "./frame-geometry"; + +function createPixels( + width: number, + height: number, + visible: readonly { x: number; y: number; alpha: number }[], +): FramePixelData { + const data = new Uint8ClampedArray(width * height * 4); + + for (const pixel of visible) data[(pixel.y * width + pixel.x) * 4 + 3] = pixel.alpha; + + return { data, width, height }; +} + +describe("measureFrameGeometry", () => { + it("treats only alpha values greater than 24 as visible", () => { + // Catches the review algorithm including matte noise at the old Alpha cutoff. + expect(measureFrameGeometry(createPixels(1, 1, [{ x: 0, y: 0, alpha: 24 }]))).toBeNull(); + expect(measureFrameGeometry(createPixels(1, 1, [{ x: 0, y: 0, alpha: 25 }]))).toMatchObject({ + opaquePixels: 1, + coverageRatio: 1, + }); + }); + + it("measures bounds, centroid, foot line, height, area and coverage from visible pixels", () => { + // Catches off-by-one bounds or a centroid derived from the box instead of real visible pixels. + const geometry = measureFrameGeometry( + createPixels(4, 4, [ + { x: 1, y: 1, alpha: 255 }, + { x: 2, y: 1, alpha: 255 }, + { x: 1, y: 2, alpha: 255 }, + { x: 2, y: 2, alpha: 255 }, + ]), + ); + + expect(geometry).toEqual({ + width: 4, + height: 4, + bounds: { left: 1, top: 1, right: 2, bottom: 2, width: 2, height: 2 }, + centroid: { x: 1.5, y: 1.5 }, + footY: 2, + subjectHeight: 2, + opaquePixels: 4, + coverageRatio: 0.25, + fingerprint: expect.any(Array), + }); + expect(geometry?.fingerprint).toHaveLength(64); + }); + + it("produces different compact fingerprints for different silhouettes with equal bounds", () => { + const leftTop = measureFrameGeometry( + createPixels(4, 4, [ + { x: 0, y: 0, alpha: 255 }, + { x: 1, y: 0, alpha: 255 }, + { x: 2, y: 0, alpha: 255 }, + { x: 3, y: 0, alpha: 255 }, + { x: 3, y: 1, alpha: 255 }, + { x: 3, y: 2, alpha: 255 }, + { x: 3, y: 3, alpha: 255 }, + ]), + ); + const leftBottom = measureFrameGeometry( + createPixels(4, 4, [ + { x: 0, y: 0, alpha: 255 }, + { x: 0, y: 1, alpha: 255 }, + { x: 0, y: 2, alpha: 255 }, + { x: 0, y: 3, alpha: 255 }, + { x: 1, y: 3, alpha: 255 }, + { x: 2, y: 3, alpha: 255 }, + { x: 3, y: 3, alpha: 255 }, + ]), + ); + + expect(leftTop?.bounds).toEqual(leftBottom?.bounds); + expect(leftTop?.fingerprint).not.toEqual(leftBottom?.fingerprint); + }); + + it("keeps small dark silhouettes distinguishable instead of diluting them across the canvas", () => { + const topLeft: Array<{ x: number; y: number; alpha: number }> = []; + const bottomRight: Array<{ x: number; y: number; alpha: number }> = []; + for (let y = 96; y < 160; y += 1) { + for (let x = 96; x < 160; x += 1) { + if (y < 128 || x < 112) topLeft.push({ x, y, alpha: 255 }); + if (y >= 128 || x >= 144) bottomRight.push({ x, y, alpha: 255 }); + } + } + const first = measureFrameGeometry(createPixels(256, 256, topLeft)); + const second = measureFrameGeometry(createPixels(256, 256, bottomRight)); + const distance = + first?.fingerprint?.reduce( + (total, value, index) => total + Math.abs(value - (second?.fingerprint?.[index] ?? value)), + 0, + ) ?? 0; + + expect(first?.bounds).toEqual(second?.bounds); + expect(distance / 64).toBeGreaterThan(0.02); + }); + + it("rejects an RGBA buffer whose dimensions do not match its length", () => { + // Catches silent geometry corruption when Canvas data and dimensions diverge. + expect(() => + measureFrameGeometry({ data: new Uint8ClampedArray(4), width: 2, height: 2 }), + ).toThrowError("RGBA 像素长度与画布尺寸不一致"); + }); + + it("ignores a tiny isolated Alpha component outside the visible subject", () => { + // Catches one stray generated pixel moving the measured foot line and centroid. + const geometry = measureFrameGeometry( + createPixels(4, 4, [ + { x: 0, y: 0, alpha: 255 }, + { x: 1, y: 0, alpha: 255 }, + { x: 0, y: 1, alpha: 255 }, + { x: 1, y: 1, alpha: 255 }, + { x: 3, y: 3, alpha: 255 }, + ]), + ); + + expect(geometry).toMatchObject({ + bounds: { left: 0, top: 0, right: 1, bottom: 1, width: 2, height: 2 }, + centroid: { x: 0.5, y: 0.5 }, + footY: 1, + opaquePixels: 4, + coverageRatio: 0.25, + }); + }); +}); diff --git a/frontend/src/pages/playtest/workbench/analysis/frame-geometry.ts b/frontend/src/pages/playtest/workbench/analysis/frame-geometry.ts new file mode 100644 index 0000000..c4bc63c --- /dev/null +++ b/frontend/src/pages/playtest/workbench/analysis/frame-geometry.ts @@ -0,0 +1,178 @@ +export const ALPHA_THRESHOLD = 24; +const MIN_COMPONENT_PIXELS = 4; +const RELATIVE_COMPONENT_RATIO = 0.002; + +export interface FramePixelData { + data: Uint8ClampedArray; + width: number; + height: number; +} + +export interface FrameGeometry { + width: number; + height: number; + bounds: { + left: number; + top: number; + right: number; + bottom: number; + width: number; + height: number; + }; + centroid: { x: number; y: number }; + footY: number; + subjectHeight: number; + opaquePixels: number; + coverageRatio: number; + /** Compact 8×8 alpha/luminance signature used for adjacent-frame similarity checks. */ + fingerprint?: readonly number[]; +} + +function createFingerprint( + data: Uint8ClampedArray, + width: number, + subjectPixels: readonly number[], + bounds: { left: number; top: number; width: number; height: number }, +): readonly number[] { + const sums = new Float64Array(64); + const cellPixels = new Uint32Array(64); + + for (let y = 0; y < bounds.height; y += 1) { + for (let x = 0; x < bounds.width; x += 1) { + const cellX = Math.min(7, Math.floor((x * 8) / bounds.width)); + const cellY = Math.min(7, Math.floor((y * 8) / bounds.height)); + cellPixels[cellY * 8 + cellX] += 1; + } + } + + for (const index of subjectPixels) { + const x = index % width; + const y = Math.floor(index / width); + const offset = index * 4; + const red = data[offset] ?? 0; + const green = data[offset + 1] ?? 0; + const blue = data[offset + 2] ?? 0; + const alpha = (data[offset + 3] ?? 0) / 255; + const luminance = (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255; + const cellX = Math.min(7, Math.floor(((x - bounds.left) * 8) / bounds.width)); + const cellY = Math.min(7, Math.floor(((y - bounds.top) * 8) / bounds.height)); + sums[cellY * 8 + cellX] += alpha * (0.25 + luminance * 0.75); + } + + return Array.from(sums, (sum, index) => { + const count = cellPixels[index] ?? 0; + return count === 0 ? 0 : Number((sum / count).toFixed(4)); + }); +} + +function visibleComponents(data: Uint8ClampedArray, width: number, height: number): number[][] { + const visible = new Uint8Array(width * height); + const visited = new Uint8Array(width * height); + const components: number[][] = []; + + for (let index = 0; index < visible.length; index += 1) { + const alpha = data[index * 4 + 3]; + if (alpha !== undefined && alpha > ALPHA_THRESHOLD) visible[index] = 1; + } + + for (let start = 0; start < visible.length; start += 1) { + if (visible[start] === 0 || visited[start] === 1) continue; + + const component: number[] = []; + const queue = [start]; + visited[start] = 1; + + for (let head = 0; head < queue.length; head += 1) { + const index = queue[head]; + if (index === undefined) continue; + component.push(index); + + const x = index % width; + const y = Math.floor(index / width); + for (let offsetY = -1; offsetY <= 1; offsetY += 1) { + for (let offsetX = -1; offsetX <= 1; offsetX += 1) { + if (offsetX === 0 && offsetY === 0) continue; + const nextX = x + offsetX; + const nextY = y + offsetY; + if (nextX < 0 || nextX >= width || nextY < 0 || nextY >= height) continue; + + const next = nextY * width + nextX; + if (visible[next] === 0 || visited[next] === 1) continue; + visited[next] = 1; + queue.push(next); + } + } + } + + components.push(component); + } + + return components; +} + +export function measureFrameGeometry(pixels: FramePixelData): FrameGeometry | null { + const { data, width, height } = pixels; + + if (data.length !== width * height * 4) { + throw new RangeError("RGBA 像素长度与画布尺寸不一致"); + } + + const components = visibleComponents(data, width, height); + if (components.length === 0) return null; + + const largestSize = Math.max(...components.map((component) => component.length)); + const minimumSize = Math.min( + largestSize, + Math.max(MIN_COMPONENT_PIXELS, Math.ceil(largestSize * RELATIVE_COMPONENT_RATIO)), + ); + const subjectPixels = components.flatMap((component) => + component.length === largestSize || component.length >= minimumSize ? component : [], + ); + + let left = width; + let top = height; + let right = -1; + let bottom = -1; + let opaquePixels = 0; + let sumX = 0; + let sumY = 0; + + for (const index of subjectPixels) { + const x = index % width; + const y = Math.floor(index / width); + left = Math.min(left, x); + top = Math.min(top, y); + right = Math.max(right, x); + bottom = Math.max(bottom, y); + opaquePixels += 1; + sumX += x; + sumY += y; + } + + const subjectWidth = right - left + 1; + const subjectHeight = bottom - top + 1; + + return { + width, + height, + bounds: { + left, + top, + right, + bottom, + width: subjectWidth, + height: subjectHeight, + }, + centroid: { x: sumX / opaquePixels, y: sumY / opaquePixels }, + footY: bottom, + subjectHeight, + opaquePixels, + coverageRatio: opaquePixels / (width * height), + fingerprint: createFingerprint(data, width, subjectPixels, { + left, + top, + width: subjectWidth, + height: subjectHeight, + }), + }; +} diff --git a/frontend/src/pages/playtest/workbench/analysis/image-geometry.test.ts b/frontend/src/pages/playtest/workbench/analysis/image-geometry.test.ts new file mode 100644 index 0000000..a80199b --- /dev/null +++ b/frontend/src/pages/playtest/workbench/analysis/image-geometry.test.ts @@ -0,0 +1,145 @@ +/** @vitest-environment jsdom */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { readImageGeometry } from "./image-geometry"; + +type ImageBehavior = "load" | "error" | "pending"; + +let imageBehavior: ImageBehavior; +let crossOriginAtSourceAssignment: string | null; +let lastAssignedSource: string; +let canvasContext: Pick | null; + +class FakeImage { + crossOrigin: string | null = null; + naturalWidth = 2; + naturalHeight = 2; + onerror: OnErrorEventHandler | null = null; + onload: ((this: GlobalEventHandlers, event: Event) => unknown) | null = null; + private source = ""; + + get src(): string { + return this.source; + } + + set src(value: string) { + this.source = value; + crossOriginAtSourceAssignment = this.crossOrigin; + lastAssignedSource = value; + if (value === "" || imageBehavior === "pending") return; + + queueMicrotask(() => { + if (imageBehavior === "load") this.onload?.call(this as never, new Event("load")); + else this.onerror?.call(this as never, "error", "", 0, 0, new Error("load failed")); + }); + } +} + +function pixelsWithAlpha(alpha: number): ImageData { + const data = new Uint8ClampedArray(2 * 2 * 4); + data[3] = alpha; + return { data, width: 2, height: 2, colorSpace: "srgb" } as ImageData; +} + +beforeEach(() => { + imageBehavior = "load"; + crossOriginAtSourceAssignment = null; + lastAssignedSource = ""; + canvasContext = { + drawImage: vi.fn(), + getImageData: vi.fn(() => pixelsWithAlpha(255)), + }; + vi.stubGlobal("Image", FakeImage); + vi.spyOn(document, "createElement").mockImplementation(((tagName: string) => { + if (tagName !== "canvas") + return document.createElementNS("http://www.w3.org/1999/xhtml", tagName); + return { + width: 0, + height: 0, + getContext: () => canvasContext, + } as unknown as HTMLCanvasElement; + }) as typeof document.createElement); +}); + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe("readImageGeometry", () => { + it("requests anonymous image access before reading real Canvas pixels", async () => { + // Catches crossOrigin being assigned after src or a placeholder geometry replacing actual pixels. + const result = await readImageGeometry("https://cdn.example.test/frame.png"); + + expect(crossOriginAtSourceAssignment).toBe("anonymous"); + expect(result).toEqual({ + status: "ready", + geometry: { + width: 2, + height: 2, + bounds: { left: 0, top: 0, right: 0, bottom: 0, width: 1, height: 1 }, + centroid: { x: 0, y: 0 }, + footY: 0, + subjectHeight: 1, + opaquePixels: 1, + coverageRatio: 0.25, + fingerprint: expect.any(Array), + }, + }); + }); + + it("reports asset, Canvas and transparent-frame failures instead of zero evidence", async () => { + // Catches unavailable evidence being silently presented as a successful zero measurement. + imageBehavior = "error"; + await expect(readImageGeometry("/missing.png")).resolves.toEqual({ + status: "unavailable", + reason: "图片加载失败", + }); + + imageBehavior = "load"; + canvasContext = null; + await expect(readImageGeometry("/no-canvas.png")).resolves.toEqual({ + status: "unavailable", + reason: "浏览器无法读取图片像素", + }); + + canvasContext = { + drawImage: vi.fn(), + getImageData: vi.fn(() => pixelsWithAlpha(24)), + }; + await expect(readImageGeometry("/transparent.png")).resolves.toEqual({ + status: "unavailable", + reason: "图片没有可见主体", + }); + }); + + it("reports a tainted Canvas as a cross-origin pixel failure", async () => { + // Catches signed remote images being mislabelled as transparent or valid when CORS blocks inspection. + canvasContext = { + drawImage: vi.fn(), + getImageData: vi.fn(() => { + throw new DOMException("tainted", "SecurityError"); + }), + }; + + await expect(readImageGeometry("https://remote.example.test/frame.png")).resolves.toEqual({ + status: "unavailable", + reason: "图片跨域,无法计算像素", + }); + }); + + it("cancels a pending image read through AbortSignal", async () => { + // Catches a stale sequence load surviving a direction switch and updating the new review. + imageBehavior = "pending"; + const controller = new AbortController(); + const result = readImageGeometry("/slow.png", controller.signal); + + controller.abort(); + + await expect(result).resolves.toEqual({ + status: "unavailable", + reason: "分析已取消", + }); + expect(lastAssignedSource).toBe(""); + }); +}); diff --git a/frontend/src/pages/playtest/workbench/analysis/image-geometry.ts b/frontend/src/pages/playtest/workbench/analysis/image-geometry.ts new file mode 100644 index 0000000..419a108 --- /dev/null +++ b/frontend/src/pages/playtest/workbench/analysis/image-geometry.ts @@ -0,0 +1,68 @@ +import { measureFrameGeometry } from "./frame-geometry"; +import type { FrameGeometryResult } from "./sequence-evidence"; + +function unavailable(reason: string): FrameGeometryResult { + return { status: "unavailable", reason }; +} + +function pixelReadFailure(error: unknown): FrameGeometryResult { + if (error instanceof DOMException && error.name === "SecurityError") { + return unavailable("图片跨域,无法计算像素"); + } + + return unavailable("浏览器无法读取图片像素"); +} + +export function readImageGeometry( + imageUrl: string, + signal?: AbortSignal, +): Promise { + return new Promise((resolve) => { + const image = new Image(); + let settled = false; + + const finish = (result: FrameGeometryResult) => { + if (settled) return; + settled = true; + image.onload = null; + image.onerror = null; + signal?.removeEventListener("abort", abort); + resolve(result); + }; + const abort = () => { + image.src = ""; + finish(unavailable("分析已取消")); + }; + + image.crossOrigin = "anonymous"; + image.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + const context = canvas.getContext("2d", { willReadFrequently: true }); + + if (context === null) { + finish(unavailable("浏览器无法读取图片像素")); + return; + } + + try { + context.drawImage(image, 0, 0); + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const geometry = measureFrameGeometry(imageData); + finish(geometry === null ? unavailable("图片没有可见主体") : { status: "ready", geometry }); + } catch (error) { + finish(pixelReadFailure(error)); + } + }; + image.onerror = () => finish(unavailable("图片加载失败")); + + if (signal?.aborted) { + abort(); + return; + } + + signal?.addEventListener("abort", abort, { once: true }); + image.src = imageUrl; + }); +} diff --git a/frontend/src/pages/playtest/workbench/analysis/sequence-evidence.test.ts b/frontend/src/pages/playtest/workbench/analysis/sequence-evidence.test.ts new file mode 100644 index 0000000..9d19681 --- /dev/null +++ b/frontend/src/pages/playtest/workbench/analysis/sequence-evidence.test.ts @@ -0,0 +1,275 @@ +import { describe, expect, it } from "vitest"; + +import type { FrameGeometry } from "./frame-geometry"; +import { buildSequenceEvidence, type FrameEvidenceInput } from "./sequence-evidence"; + +function geometry( + overrides: Partial< + Pick< + FrameGeometry, + "width" | "height" | "footY" | "subjectHeight" | "opaquePixels" | "coverageRatio" + > + > & { x?: number; y?: number; fingerprint?: readonly number[]; cropped?: boolean } = {}, +): FrameGeometry { + const subjectHeight = overrides.subjectHeight ?? 20; + + return { + width: overrides.width ?? 256, + height: overrides.height ?? 256, + bounds: { + left: overrides.cropped ? 0 : 100, + top: overrides.cropped ? 0 : 100, + right: overrides.cropped ? 9 : 109, + bottom: overrides.cropped ? subjectHeight - 1 : 100 + subjectHeight - 1, + width: 10, + height: subjectHeight, + }, + centroid: { x: overrides.x ?? 0, y: overrides.y ?? 0 }, + footY: overrides.footY ?? 100, + subjectHeight, + opaquePixels: overrides.opaquePixels ?? 100, + coverageRatio: overrides.coverageRatio ?? 0.25, + fingerprint: overrides.fingerprint, + }; +} + +function ready( + value: FrameGeometry, + rootMotion: FrameEvidenceInput["rootMotion"] = null, +): FrameEvidenceInput { + return { geometry: { status: "ready", geometry: value }, rootMotion }; +} + +describe("buildSequenceEvidence", () => { + it("returns structured findings for incomplete, cropped and duplicate frames", () => { + const fingerprint = Array.from({ length: 64 }, (_, index) => index / 64); + const evidence = buildSequenceEvidence( + [ + ready(geometry({ cropped: true, fingerprint })), + ready(geometry({ fingerprint })), + { geometry: { status: "unavailable", reason: "图片没有可见主体" }, rootMotion: null }, + ], + "walk", + ); + + expect(evidence.complete).toBe(false); + expect(evidence.findings.map((finding) => finding.code)).toEqual( + expect.arrayContaining(["subject_cropped", "duplicate_frame", "blank_subject"]), + ); + expect(evidence.findings.find((finding) => finding.code === "duplicate_frame")).toMatchObject({ + frameIndex: 1, + severity: "warning", + }); + }); + + it("uses action-aware findings for foot and height changes", () => { + const frames = [ + ready(geometry({ footY: 100, subjectHeight: 30 })), + ready(geometry({ footY: 112, subjectHeight: 15 })), + ]; + + expect(buildSequenceEvidence(frames, "idle").findings.map((finding) => finding.code)).toEqual( + expect.arrayContaining(["foot_drift", "height_drift"]), + ); + expect( + buildSequenceEvidence(frames, "jump").findings.map((finding) => finding.code), + ).not.toContain("foot_drift"); + expect( + buildSequenceEvidence(frames, "crouch").findings.map((finding) => finding.code), + ).not.toContain("height_drift"); + }); + + it("flags motion outliers and root-motion direction contradictions", () => { + const evidence = buildSequenceEvidence( + [ + ready(geometry({ x: 0 })), + ready(geometry({ x: 1 }), { dx: 1, dy: 0 }), + ready(geometry({ x: 2 }), { dx: 1, dy: 0 }), + ready(geometry({ x: -18 }), { dx: 5, dy: 0 }), + ], + "walk", + ); + + expect(evidence.findings.map((finding) => finding.code)).toEqual( + expect.arrayContaining(["motion_spike", "root_motion_mismatch"]), + ); + }); + + it("calculates a selected frame delta and the hand-derived sequence baseline", () => { + // Catches image-derived offsets being calculated from bounds or against the first frame instead of the previous frame. + const evidence = buildSequenceEvidence( + [ready(geometry()), ready(geometry({ x: 3, y: 4, opaquePixels: 80 }))], + "walk", + ); + + expect(evidence.frames[0]?.previousDelta).toBeNull(); + expect(evidence.frames[1]?.previousDelta).toEqual({ + dx: 3, + dy: 4, + distance: 5, + areaDeltaPercent: 20, + }); + expect(evidence.summary).toMatchObject({ + medianStep: 5, + maxStep: 5, + movementThreshold: 15, + maxAreaDeltaPercent: 20, + movementState: "normal", + areaState: "normal", + }); + }); + + it("does not compare across an unreadable middle frame", () => { + // Catches filtered valid frames becoming false neighbours and producing a misleading offset. + const evidence = buildSequenceEvidence( + [ + ready(geometry()), + { geometry: { status: "unavailable", reason: "图片加载失败" }, rootMotion: null }, + ready(geometry({ x: 20, y: 20 })), + ], + "walk", + ); + + expect(evidence.complete).toBe(false); + expect(evidence.unavailableFrameCount).toBe(1); + expect(evidence.frames.map((frame) => frame.previousDelta)).toEqual([null, null, null]); + expect(evidence.summary.medianStep).toBeNull(); + expect(evidence.summary.movementState).toBe("not_applicable"); + }); + + it("marks excessive coverage, foot drift and height drift with action-aware states", () => { + // Catches jump lift being rejected as foot drift or foreground/background problems being hidden. + const frames = [ + ready(geometry({ footY: 100, subjectHeight: 20, coverageRatio: 0.66 })), + ready(geometry({ footY: 104, subjectHeight: 28 })), + ]; + + const walk = buildSequenceEvidence(frames, "walk"); + const jump = buildSequenceEvidence(frames, "jump"); + + expect(walk.frames[0]?.coverageState).toBe("anomaly"); + expect(walk.summary).toMatchObject({ + footDrift: 4, + heightDrift: 8, + footState: "anomaly", + heightThreshold: 12, + heightState: "normal", + }); + expect(jump.summary.footState).toBe("attention"); + }); + + it("flags a single movement spike against the sequence median", () => { + // Catches a sudden position jump being normalized away by the animation's ordinary movement. + const evidence = buildSequenceEvidence( + [ + ready(geometry({ x: 0 })), + ready(geometry({ x: 1 })), + ready(geometry({ x: 2 })), + ready(geometry({ x: 22 })), + ], + "walk", + ); + + expect(evidence.summary).toMatchObject({ + medianStep: 1, + maxStep: 20, + movementThreshold: 6, + movementState: "anomaly", + }); + expect(evidence.frames.map((frame) => frame.movementState)).toEqual([ + "not_applicable", + "normal", + "normal", + "anomaly", + ]); + }); + + it("keeps an action-aware absolute movement ceiling when every step is large", () => { + // Catches an entire drifting sequence normalizing its own 100px jumps through the median. + const evidence = buildSequenceEvidence( + [ready(geometry({ x: 0 })), ready(geometry({ x: 100 })), ready(geometry({ x: 200 }))], + "walk", + ); + + expect(evidence.summary.movementThreshold).toBeLessThan(100); + expect(evidence.summary.movementState).toBe("anomaly"); + expect(evidence.findings.map((finding) => finding.code)).toContain("motion_spike"); + }); + + it("marks jump and crouch height variation as action-allowed attention", () => { + const frames = [ready(geometry({ subjectHeight: 20 })), ready(geometry({ subjectHeight: 60 }))]; + + for (const actionType of ["jump", "crouch"] as const) { + const evidence = buildSequenceEvidence(frames, actionType); + expect(evidence.summary.heightThreshold).toBeNull(); + expect(evidence.summary.heightState).toBe("attention"); + expect(evidence.findings.map((finding) => finding.code)).not.toContain("height_drift"); + } + }); + + it("flags adjacent outline area changes over 28 percent", () => { + // Catches a character silhouette abruptly shrinking without a visible review warning. + const evidence = buildSequenceEvidence( + [ready(geometry({ opaquePixels: 100 })), ready(geometry({ opaquePixels: 70 }))], + "attack", + ); + + expect(evidence.summary).toMatchObject({ + maxAreaDeltaPercent: 30, + areaState: "anomaly", + }); + expect(evidence.frames[1]?.areaState).toBe("anomaly"); + }); + + it("marks adjacency-only checks not applicable for one frame", () => { + // Catches the first frame displaying fabricated zero deltas as a successful comparison. + const evidence = buildSequenceEvidence([ready(geometry())], "idle"); + + expect(evidence.frames[0]).toMatchObject({ + previousDelta: null, + movementState: "not_applicable", + areaState: "not_applicable", + }); + expect(evidence.summary).toMatchObject({ + footDrift: null, + heightDrift: null, + medianStep: null, + movementThreshold: null, + movementState: "not_applicable", + areaState: "not_applicable", + }); + }); + + it("combines measured image drift with adjacent root-motion increments using an upward y axis", () => { + // Catches root motion being subtracted twice or image-space positive-down y being added as positive-up. + const evidence = buildSequenceEvidence( + [ready(geometry({ x: 0, y: 10 }), null), ready(geometry({ x: 3, y: 14 }), { dx: 10, dy: 6 })], + "walk", + ); + + expect(evidence.frames[0]).toMatchObject({ + expectedRootDelta: null, + composedPreviewDelta: null, + }); + expect(evidence.frames[1]?.expectedRootDelta).toMatchObject({ dx: 10, dy: 6 }); + expect(evidence.frames[1]?.expectedRootDelta?.distance).toBeCloseTo(Math.sqrt(136)); + expect(evidence.frames[1]?.composedPreviewDelta).toMatchObject({ dx: 13, dy: 2 }); + expect(evidence.frames[1]?.composedPreviewDelta?.distance).toBeCloseTo(Math.sqrt(173)); + expect(evidence.frames[1]?.movementState).toBe("normal"); + }); + + it("treats each frame root motion as an increment instead of subtracting the previous frame", () => { + // Catches repeated per-frame dx values collapsing to zero and leaving a walking sprite in place. + const evidence = buildSequenceEvidence( + [ + ready(geometry({ x: 0, y: 10 }), null), + ready(geometry({ x: 1, y: 11 }), { dx: 2, dy: 1 }), + ready(geometry({ x: 3, y: 12 }), { dx: 3, dy: 2 }), + ], + "walk", + ); + + expect(evidence.frames[2]?.expectedRootDelta).toMatchObject({ dx: 3, dy: 2 }); + expect(evidence.frames[2]?.composedPreviewDelta).toMatchObject({ dx: 5, dy: 1 }); + }); +}); diff --git a/frontend/src/pages/playtest/workbench/analysis/sequence-evidence.ts b/frontend/src/pages/playtest/workbench/analysis/sequence-evidence.ts new file mode 100644 index 0000000..2bedca1 --- /dev/null +++ b/frontend/src/pages/playtest/workbench/analysis/sequence-evidence.ts @@ -0,0 +1,444 @@ +import type { Frame } from "@/entities/character"; + +import type { PlaytestActionType } from "../model/types"; +import type { FrameGeometry } from "./frame-geometry"; + +const EXPECTED_CANVAS_SIZE = 256; +const MAX_COVERAGE_RATIO = 0.65; +const MAX_FOOT_DRIFT = 3; +const MAX_HEIGHT_DRIFT = 7; +const MAX_AREA_DELTA_PERCENT = 28; +const MIN_COVERAGE_RATIO = 0.005; +const DUPLICATE_DISTANCE = 0.02; + +export type EvidenceState = "normal" | "attention" | "anomaly" | "not_applicable"; + +export type QualityFindingCode = + | "image_unavailable" + | "blank_subject" + | "canvas_size_mismatch" + | "subject_cropped" + | "coverage_too_low" + | "coverage_too_high" + | "duplicate_frame" + | "motion_spike" + | "foot_drift" + | "height_drift" + | "area_spike" + | "root_motion_mismatch"; + +export interface QualityFinding { + code: QualityFindingCode; + severity: "warning" | "error"; + frameIndex: number | null; + message: string; + metrics: Readonly>; +} + +export type FrameGeometryResult = + | { status: "ready"; geometry: FrameGeometry } + | { status: "unavailable"; reason: string }; + +export interface FrameEvidenceInput { + geometry: FrameGeometryResult; + rootMotion: Frame["rootMotion"]; +} + +export interface AdjacentFrameDelta { + dx: number; + dy: number; + distance: number; + areaDeltaPercent: number; +} + +export interface MotionVector { + dx: number; + dy: number; + distance: number; +} + +export interface FrameReviewEvidence { + geometry: FrameGeometry | null; + unavailableReason: string | null; + previousDelta: AdjacentFrameDelta | null; + expectedRootDelta: MotionVector | null; + composedPreviewDelta: MotionVector | null; + coverageState: EvidenceState; + movementState: EvidenceState; + areaState: EvidenceState; +} + +export interface SequenceReviewEvidence { + complete: boolean; + unavailableFrameCount: number; + frames: readonly FrameReviewEvidence[]; + findings: readonly QualityFinding[]; + summary: { + footDrift: number | null; + heightDrift: number | null; + medianStep: number | null; + maxStep: number | null; + movementThreshold: number | null; + heightThreshold: number | null; + maxAreaDeltaPercent: number | null; + canvasState: EvidenceState; + footState: EvidenceState; + heightState: EvidenceState; + movementState: EvidenceState; + areaState: EvidenceState; + }; +} + +function medianAbsoluteDeviation(values: readonly number[], center: number | null): number | null { + if (center === null || values.length === 0) return null; + return median(values.map((value) => Math.abs(value - center))); +} + +function fingerprintDistance( + left: readonly number[] | undefined, + right: readonly number[] | undefined, +): number | null { + if ( + left === undefined || + right === undefined || + left.length === 0 || + left.length !== right.length + ) + return null; + + const total = left.reduce( + (sum, value, index) => sum + Math.abs(value - (right[index] ?? value)), + 0, + ); + return total / left.length; +} + +function isCropped(geometry: FrameGeometry): boolean { + return ( + geometry.bounds.left <= 1 || + geometry.bounds.top <= 1 || + geometry.bounds.right >= geometry.width - 2 || + geometry.bounds.bottom >= geometry.height - 2 + ); +} + +function heightFindingThreshold(actionType: PlaytestActionType): number | null { + if (actionType === "jump" || actionType === "crouch") return null; + if (actionType === "idle") return 7; + if (actionType === "walk") return 12; + return 20; +} + +function absoluteMovementThreshold(actionType: PlaytestActionType): number { + if (actionType === "idle") return 6; + if (actionType === "walk") return 24; + if (actionType === "crouch") return 16; + if (actionType === "jump") return 64; + return 48; +} + +function median(values: readonly number[]): number | null { + if (values.length === 0) return null; + + const sorted = [...values].sort((left, right) => left - right); + const middle = Math.floor(sorted.length / 2); + const upper = sorted[middle]; + if (upper === undefined) return null; + + if (sorted.length % 2 === 1) return upper; + + const lower = sorted[middle - 1]; + return lower === undefined ? upper : (lower + upper) / 2; +} + +function spread(values: readonly number[]): number | null { + if (values.length < 2) return null; + return Math.max(...values) - Math.min(...values); +} + +function adjacentDelta(previous: FrameGeometry, current: FrameGeometry): AdjacentFrameDelta { + const dx = current.centroid.x - previous.centroid.x; + const dy = current.centroid.y - previous.centroid.y; + + return { + dx, + dy, + distance: Math.hypot(dx, dy), + areaDeltaPercent: + (Math.abs(current.opaquePixels - previous.opaquePixels) / + Math.max(current.opaquePixels, previous.opaquePixels)) * + 100, + }; +} + +function motionVector(dx: number, dy: number): MotionVector { + return { dx, dy, distance: Math.hypot(dx, dy) }; +} + +function rootMotion(frame: FrameEvidenceInput): { dx: number; dy: number } { + return frame.rootMotion ?? { dx: 0, dy: 0 }; +} + +export function buildSequenceEvidence( + inputs: readonly FrameEvidenceInput[], + actionType: PlaytestActionType, +): SequenceReviewEvidence { + const results = inputs.map((input) => input.geometry); + const readyGeometries = results.flatMap((result) => + result.status === "ready" ? [result.geometry] : [], + ); + const deltas = results.map((result, index): AdjacentFrameDelta | null => { + const previous = results[index - 1]; + if (index === 0 || previous?.status !== "ready" || result.status !== "ready") return null; + return adjacentDelta(previous.geometry, result.geometry); + }); + const rootDeltas = inputs.map((input, index): MotionVector | null => { + if (index === 0) return null; + + const increment = rootMotion(input); + return motionVector(increment.dx, increment.dy); + }); + const availableDeltas = deltas.flatMap((delta) => (delta === null ? [] : [delta])); + const steps = availableDeltas.map((delta) => delta.distance); + const areaDeltas = availableDeltas.map((delta) => delta.areaDeltaPercent); + const medianStep = median(steps); + const movementMad = medianAbsoluteDeviation(steps, medianStep); + const relativeMovementThreshold = + medianStep === null + ? null + : Math.max(medianStep * 2.6 + 2, medianStep + (movementMad ?? 0) * 3 + 2, 6); + const movementThreshold = + relativeMovementThreshold === null + ? null + : Math.min(relativeMovementThreshold, absoluteMovementThreshold(actionType)); + const maxStep = steps.length === 0 ? null : Math.max(...steps); + const maxAreaDeltaPercent = areaDeltas.length === 0 ? null : Math.max(...areaDeltas); + const footDrift = spread(readyGeometries.map((geometry) => geometry.footY)); + const heightDrift = spread(readyGeometries.map((geometry) => geometry.subjectHeight)); + const unavailableFrameCount = results.length - readyGeometries.length; + const findings: QualityFinding[] = []; + const heightThreshold = heightFindingThreshold(actionType); + + const frames = results.map((result, index): FrameReviewEvidence => { + const expectedRootDelta = rootDeltas[index] ?? null; + if (result.status === "unavailable") { + return { + geometry: null, + unavailableReason: result.reason, + previousDelta: null, + expectedRootDelta, + composedPreviewDelta: null, + coverageState: "not_applicable", + movementState: "not_applicable", + areaState: "not_applicable", + }; + } + + const delta = deltas[index] ?? null; + const composedPreviewDelta = + delta === null || expectedRootDelta === null + ? null + : motionVector(delta.dx + expectedRootDelta.dx, expectedRootDelta.dy - delta.dy); + return { + geometry: result.geometry, + unavailableReason: null, + previousDelta: delta, + expectedRootDelta, + composedPreviewDelta, + coverageState: result.geometry.coverageRatio > MAX_COVERAGE_RATIO ? "anomaly" : "normal", + movementState: + delta === null || movementThreshold === null + ? "not_applicable" + : delta.distance > movementThreshold + ? "anomaly" + : "normal", + areaState: + delta === null + ? "not_applicable" + : delta.areaDeltaPercent > MAX_AREA_DELTA_PERCENT + ? "anomaly" + : "normal", + }; + }); + + results.forEach((result, index) => { + if (result.status === "unavailable") { + const blank = result.reason.includes("没有可见主体"); + findings.push({ + code: blank ? "blank_subject" : "image_unavailable", + severity: "error", + frameIndex: index, + message: blank ? "当前帧没有可见主体" : result.reason, + metrics: {}, + }); + return; + } + + const { geometry } = result; + if (geometry.width !== EXPECTED_CANVAS_SIZE || geometry.height !== EXPECTED_CANVAS_SIZE) { + findings.push({ + code: "canvas_size_mismatch", + severity: "error", + frameIndex: index, + message: "画布尺寸与预期规格不一致", + metrics: { width: geometry.width, height: geometry.height }, + }); + } + if (isCropped(geometry)) { + findings.push({ + code: "subject_cropped", + severity: "error", + frameIndex: index, + message: "主体接触画布边缘,可能发生裁切", + metrics: { + left: geometry.bounds.left, + top: geometry.bounds.top, + right: geometry.bounds.right, + bottom: geometry.bounds.bottom, + }, + }); + } + if (geometry.coverageRatio < MIN_COVERAGE_RATIO) { + findings.push({ + code: "coverage_too_low", + severity: "warning", + frameIndex: index, + message: "主体在画布中的占比过小", + metrics: { coverageRatio: geometry.coverageRatio }, + }); + } else if (geometry.coverageRatio > MAX_COVERAGE_RATIO) { + findings.push({ + code: "coverage_too_high", + severity: "error", + frameIndex: index, + message: "主体在画布中的占比过大", + metrics: { coverageRatio: geometry.coverageRatio }, + }); + } + + const previous = results[index - 1]; + if (previous?.status !== "ready") return; + const duplicateDistance = fingerprintDistance( + previous.geometry.fingerprint, + geometry.fingerprint, + ); + if (duplicateDistance !== null && duplicateDistance <= DUPLICATE_DISTANCE) { + findings.push({ + code: "duplicate_frame", + severity: "warning", + frameIndex: index, + message: "当前帧与上一帧高度相似", + metrics: { distance: duplicateDistance }, + }); + } + + const delta = deltas[index]; + if (delta !== null && movementThreshold !== null && delta.distance > movementThreshold) { + findings.push({ + code: "motion_spike", + severity: "error", + frameIndex: index, + message: "相邻帧出现异常位移突变", + metrics: { distance: delta.distance, threshold: movementThreshold }, + }); + } + if (delta !== null && delta.areaDeltaPercent > MAX_AREA_DELTA_PERCENT) { + findings.push({ + code: "area_spike", + severity: "warning", + frameIndex: index, + message: "相邻帧主体轮廓面积变化过大", + metrics: { percent: delta.areaDeltaPercent }, + }); + } + + const expected = rootDeltas[index]; + if (delta !== null && expected !== null && expected.distance >= 2 && delta.distance >= 2) { + const dot = delta.dx * expected.dx + -delta.dy * expected.dy; + if (dot < 0) { + findings.push({ + code: "root_motion_mismatch", + severity: "warning", + frameIndex: index, + message: "画面内位移方向与预期根位移矛盾", + metrics: { dotProduct: dot }, + }); + } + } + }); + + if (footDrift !== null && footDrift > MAX_FOOT_DRIFT && actionType !== "jump") { + findings.push({ + code: "foot_drift", + severity: "error", + frameIndex: null, + message: "序列脚底线漂移超过动作允许范围", + metrics: { drift: footDrift, threshold: MAX_FOOT_DRIFT }, + }); + } + if (heightDrift !== null && heightThreshold !== null && heightDrift > heightThreshold) { + findings.push({ + code: "height_drift", + severity: "warning", + frameIndex: null, + message: "序列主体高度变化超过动作允许范围", + metrics: { drift: heightDrift, threshold: heightThreshold }, + }); + } + + return { + complete: results.length > 0 && unavailableFrameCount === 0, + unavailableFrameCount, + frames, + findings, + summary: { + footDrift, + heightDrift, + medianStep, + maxStep, + movementThreshold, + heightThreshold, + maxAreaDeltaPercent, + canvasState: + readyGeometries.length === 0 + ? "not_applicable" + : readyGeometries.every( + (geometry) => + geometry.width === EXPECTED_CANVAS_SIZE && + geometry.height === EXPECTED_CANVAS_SIZE, + ) + ? "normal" + : "anomaly", + footState: + footDrift === null + ? "not_applicable" + : footDrift <= MAX_FOOT_DRIFT + ? "normal" + : actionType === "jump" + ? "attention" + : "anomaly", + heightState: + heightDrift === null + ? "not_applicable" + : heightThreshold === null + ? heightDrift > MAX_HEIGHT_DRIFT + ? "attention" + : "normal" + : heightDrift > heightThreshold + ? "anomaly" + : "normal", + movementState: + maxStep === null || movementThreshold === null + ? "not_applicable" + : maxStep > movementThreshold + ? "anomaly" + : "normal", + areaState: + maxAreaDeltaPercent === null + ? "not_applicable" + : maxAreaDeltaPercent > MAX_AREA_DELTA_PERCENT + ? "anomaly" + : "normal", + }, + }; +} diff --git a/frontend/src/pages/playtest/workbench/analysis/use-frame-review-evidence.test.tsx b/frontend/src/pages/playtest/workbench/analysis/use-frame-review-evidence.test.tsx new file mode 100644 index 0000000..ba75864 --- /dev/null +++ b/frontend/src/pages/playtest/workbench/analysis/use-frame-review-evidence.test.tsx @@ -0,0 +1,205 @@ +/** @vitest-environment jsdom */ +import { act, cleanup, renderHook, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { PreviewSequence } from "../model/types"; +import type { FrameGeometry } from "./frame-geometry"; +import type { FrameGeometryResult } from "./sequence-evidence"; +import { useFrameReviewEvidence, type ImageGeometryReader } from "./use-frame-review-evidence"; + +function sequence(...imageUrls: string[]): PreviewSequence { + return { + direction: "south", + frames: imageUrls.map((imageUrl) => ({ + imageUrl, + durationMs: 100, + rootMotion: null, + qc: "pending", + rejected: false, + keyFrame: false, + })), + }; +} + +function geometry(x: number, y = 10): FrameGeometryResult { + const value: FrameGeometry = { + width: 256, + height: 256, + bounds: { left: x, top: 0, right: x + 9, bottom: 19, width: 10, height: 20 }, + centroid: { x, y }, + footY: 19, + subjectHeight: 20, + opaquePixels: 100, + coverageRatio: 100 / (256 * 256), + }; + return { status: "ready", geometry: value }; +} + +function deferred(): { + promise: Promise; + resolve(value: T): void; +} { + let resolvePromise: ((value: T) => void) | null = null; + const promise = new Promise((resolve) => { + resolvePromise = resolve; + }); + + return { + promise, + resolve(value) { + if (resolvePromise === null) throw new Error("deferred promise is not initialized"); + resolvePromise(value); + }, + }; +} + +afterEach(cleanup); + +describe("useFrameReviewEvidence", () => { + it("exposes loading before producing real sequence evidence", async () => { + // Catches the Inspector flashing fabricated zeros before image analysis completes. + const pending = deferred(); + const reader: ImageGeometryReader = () => pending.promise; + const { result } = renderHook(() => + useFrameReviewEvidence(sequence("/frame-1.png"), "walk", reader), + ); + + expect(result.current).toEqual({ status: "loading", evidence: null }); + + await act(async () => pending.resolve(geometry(4))); + + await waitFor(() => expect(result.current.status).toBe("ready")); + expect(result.current.evidence?.frames[0]?.geometry?.centroid.x).toBe(4); + }); + + it("ignores a previous sequence result that resolves after a direction switch", async () => { + // Catches a slow old direction replacing the evidence for the currently selected direction. + const south = deferred(); + const north = deferred(); + const reader: ImageGeometryReader = (imageUrl) => + imageUrl.includes("south") ? south.promise : north.promise; + const { result, rerender } = renderHook( + ({ current }) => useFrameReviewEvidence(current, "walk", reader), + { initialProps: { current: sequence("/south.png") } }, + ); + + rerender({ current: sequence("/north.png") }); + await act(async () => north.resolve(geometry(20))); + await waitFor(() => expect(result.current.evidence?.frames[0]?.geometry?.centroid.x).toBe(20)); + + await act(async () => south.resolve(geometry(2))); + expect(result.current.evidence?.frames[0]?.geometry?.centroid.x).toBe(20); + }); + + it("passes an abort signal to image reads and aborts stale sequence work", () => { + const pending = deferred(); + const reader = vi.fn(() => pending.promise); + const { rerender } = renderHook( + ({ current }) => useFrameReviewEvidence(current, "walk", reader), + { initialProps: { current: sequence("/south.png") } }, + ); + + const firstSignal = reader.mock.calls[0]?.[1]; + expect(firstSignal).toBeInstanceOf(AbortSignal); + expect(firstSignal?.aborted).toBe(false); + + rerender({ current: sequence("/north.png") }); + + expect(firstSignal?.aborted).toBe(true); + expect(reader.mock.calls[1]?.[1]).toBeInstanceOf(AbortSignal); + }); + + it("reuses settled image results when frames are selected or revisited", async () => { + // Catches frame navigation repeatedly decoding every image in the same review session. + const reader = vi.fn(async (imageUrl) => + geometry(imageUrl.includes("one") ? 1 : 2), + ); + const first = sequence("/one.png"); + const second = sequence("/two.png"); + const { result, rerender } = renderHook( + ({ current }) => useFrameReviewEvidence(current, "walk", reader), + { initialProps: { current: first } }, + ); + + await waitFor(() => expect(result.current.status).toBe("ready")); + rerender({ current: first }); + expect(reader).toHaveBeenCalledTimes(1); + + rerender({ current: second }); + await waitFor(() => expect(result.current.evidence?.frames[0]?.geometry?.centroid.x).toBe(2)); + rerender({ current: first }); + await waitFor(() => expect(result.current.evidence?.frames[0]?.geometry?.centroid.x).toBe(1)); + expect(reader).toHaveBeenCalledTimes(2); + }); + + it("retries an unavailable image when its sequence is revisited", async () => { + // A transient image failure must not become a permanent session-level cache entry. + let failedOnce = false; + const reader = vi.fn(async (imageUrl) => { + if (imageUrl.includes("retry") && !failedOnce) { + failedOnce = true; + return { status: "unavailable", reason: "temporary failure" }; + } + return geometry(imageUrl.includes("retry") ? 9 : 2); + }); + const retrySequence = sequence("/retry.png"); + const otherSequence = sequence("/other.png"); + const { result, rerender } = renderHook( + ({ current }) => useFrameReviewEvidence(current, "walk", reader), + { initialProps: { current: retrySequence } }, + ); + + await waitFor(() => expect(result.current.status).toBe("ready")); + expect(result.current.evidence?.complete).toBe(false); + + rerender({ current: otherSequence }); + await waitFor(() => expect(result.current.evidence?.frames[0]?.geometry?.centroid.x).toBe(2)); + rerender({ current: retrySequence }); + await waitFor(() => expect(result.current.evidence?.frames[0]?.geometry?.centroid.x).toBe(9)); + + expect(reader).toHaveBeenCalledTimes(3); + }); + + it("stays idle without a review sequence and does not read an image", () => { + // Catches direct-control mode starting hidden Canvas work. + const reader = vi.fn(); + const { result } = renderHook(() => useFrameReviewEvidence(null, null, reader)); + + expect(result.current).toEqual({ status: "idle", evidence: null }); + expect(reader).not.toHaveBeenCalled(); + }); + + it("does not update React state after the consumer unmounts", async () => { + // Catches an image completion writing into a removed Playtest workbench. + const pending = deferred(); + const reader: ImageGeometryReader = () => pending.promise; + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); + const mounted = renderHook(() => useFrameReviewEvidence(sequence("/slow.png"), "walk", reader)); + + mounted.unmount(); + await act(async () => pending.resolve(geometry(1))); + + expect(consoleError).not.toHaveBeenCalled(); + }); + + it("keeps each preview frame root motion beside its measured geometry", async () => { + // Catches asynchronous image results losing their matching motion contract before aggregation. + const base = sequence("/first.png", "/second.png"); + const motionSequence: PreviewSequence = { + ...base, + frames: [ + { ...base.frames[0]!, rootMotion: null }, + { ...base.frames[1]!, rootMotion: { dx: 10, dy: 6 } }, + ], + }; + const reader: ImageGeometryReader = async (imageUrl) => + imageUrl.includes("first") ? geometry(0, 10) : geometry(3, 14); + const { result } = renderHook(() => useFrameReviewEvidence(motionSequence, "walk", reader)); + + await waitFor(() => expect(result.current.status).toBe("ready")); + expect(result.current.evidence?.frames[1]).toMatchObject({ + expectedRootDelta: { dx: 10, dy: 6 }, + composedPreviewDelta: { dx: 13, dy: 2 }, + }); + }); +}); diff --git a/frontend/src/pages/playtest/workbench/analysis/use-frame-review-evidence.ts b/frontend/src/pages/playtest/workbench/analysis/use-frame-review-evidence.ts new file mode 100644 index 0000000..07bded4 --- /dev/null +++ b/frontend/src/pages/playtest/workbench/analysis/use-frame-review-evidence.ts @@ -0,0 +1,110 @@ +import { useEffect, useRef, useState } from "react"; + +import type { PlaytestActionType, PreviewSequence } from "../model/types"; +import { readImageGeometry } from "./image-geometry"; +import { + buildSequenceEvidence, + type FrameGeometryResult, + type SequenceReviewEvidence, +} from "./sequence-evidence"; + +export type FrameReviewEvidenceState = + | { status: "idle"; evidence: null } + | { status: "loading"; evidence: null } + | { status: "ready"; evidence: SequenceReviewEvidence }; + +export type ImageGeometryReader = ( + imageUrl: string, + signal?: AbortSignal, +) => Promise; + +interface CachedReads { + reader: ImageGeometryReader; + entries: Map; +} + +interface ResolvedState { + key: string | null; + value: FrameReviewEvidenceState; +} + +const IDLE_STATE: FrameReviewEvidenceState = { status: "idle", evidence: null }; +const LOADING_STATE: FrameReviewEvidenceState = { status: "loading", evidence: null }; + +export function useFrameReviewEvidence( + sequence: PreviewSequence | null, + actionType: PlaytestActionType | null, + reader: ImageGeometryReader = readImageGeometry, +): FrameReviewEvidenceState { + const frameDescriptors = + sequence?.frames.map((frame) => ({ imageUrl: frame.imageUrl, rootMotion: frame.rootMotion })) ?? + []; + const imageUrls = frameDescriptors.map((frame) => frame.imageUrl); + const sequenceKey = + sequence === null || actionType === null + ? null + : JSON.stringify([actionType, ...frameDescriptors]); + const cache = useRef(null); + const [resolved, setResolved] = useState({ key: null, value: IDLE_STATE }); + + if (cache.current === null || cache.current.reader !== reader) { + cache.current = { reader, entries: new Map() }; + } + + useEffect(() => { + if (sequenceKey === null || actionType === null) { + setResolved({ key: null, value: IDLE_STATE }); + return; + } + + const controller = new AbortController(); + let active = true; + const reads = cache.current?.entries; + const inFlight = new Map>(); + + setResolved({ key: sequenceKey, value: LOADING_STATE }); + + const results = imageUrls.map((imageUrl) => { + const cached = reads?.get(imageUrl); + if (cached !== undefined) return Promise.resolve(cached); + + const existing = inFlight.get(imageUrl); + if (existing !== undefined) return existing; + + const pending = reader(imageUrl, controller.signal) + .catch((): FrameGeometryResult => ({ status: "unavailable", reason: "图片分析失败" })) + .then((result) => { + if (!controller.signal.aborted && result.status === "ready") reads?.set(imageUrl, result); + return result; + }); + inFlight.set(imageUrl, pending); + return pending; + }); + + void Promise.all(results).then((frameResults) => { + if (!active || controller.signal.aborted) return; + + setResolved({ + key: sequenceKey, + value: { + status: "ready", + evidence: buildSequenceEvidence( + frameResults.map((geometry, index) => ({ + geometry, + rootMotion: frameDescriptors[index]?.rootMotion ?? null, + })), + actionType, + ), + }, + }); + }); + + return () => { + active = false; + controller.abort(); + }; + }, [actionType, reader, sequenceKey]); + + if (sequenceKey === null) return IDLE_STATE; + return resolved.key === sequenceKey ? resolved.value : LOADING_STATE; +} diff --git a/frontend/src/pages/playtest/workbench/animation-stage.test.tsx b/frontend/src/pages/playtest/workbench/animation-stage.test.tsx new file mode 100644 index 0000000..d0aa07c --- /dev/null +++ b/frontend/src/pages/playtest/workbench/animation-stage.test.tsx @@ -0,0 +1,177 @@ +/** @vitest-environment jsdom */ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { ActionSelector } from "./action-selector"; +import { AnimationStage } from "./animation-stage"; +import { FrameTimeline } from "./frame-timeline"; +import { PlaybackControls } from "./playback-controls"; +import type { PreviewAction, PreviewFrame, PreviewSequence } from "./model/types"; + +const currentFrame: PreviewFrame = { + imageUrl: "https://cdn.example.test/current.png", + durationMs: 100, + rootMotion: { dx: 12, dy: 5 }, + qc: "passed", + rejected: false, + keyFrame: true, +}; + +const nextFrame: PreviewFrame = { + ...currentFrame, + imageUrl: "https://cdn.example.test/next.png", + qc: "failed", + rejected: true, + keyFrame: false, +}; + +const sequence: PreviewSequence = { + direction: "south", + frames: [currentFrame, nextFrame], +}; + +const actions: readonly PreviewAction[] = [ + { + id: "walk", + name: "行走", + type: "walk", + status: "confirmed", + fps: 12, + sequences: [sequence], + }, + { + id: "empty", + name: "空动作", + type: "idle", + status: "candidate", + fps: 12, + sequences: [], + }, +]; + +afterEach(cleanup); + +describe("playtest visual primitives", () => { + it("renders prop-supplied action names, statuses and actual frame counts while disabling empty actions", () => { + // Catches a selector replacing supplied actions with demo data or permitting a non-playable action. + const onSelectAction = vi.fn(); + + render( + , + ); + + expect(screen.getByRole("button", { name: /行走/ }).textContent).toContain("confirmed"); + expect(screen.getByRole("button", { name: /行走/ }).textContent).toContain("2 帧"); + expect((screen.getByRole("button", { name: /空动作/ }) as HTMLButtonElement).disabled).toBe( + true, + ); + + fireEvent.click(screen.getByRole("button", { name: /行走/ })); + expect(onSelectAction).toHaveBeenCalledWith("walk"); + }); + + it("uses the real frame URL and reports image failure with the accumulated mirrored transform", () => { + // Catches the stage reading per-frame root motion instead of the accumulated owner state, inverting y incorrectly, or hiding load errors. + render( + , + ); + + const image = screen.getByRole("img", { name: "角色动画预览" }); + expect(image.getAttribute("src")).toBe(currentFrame.imageUrl); + expect(image.getAttribute("style")).toContain("translate(18px, -7px) scaleX(-1)"); + expect(screen.getAllByRole("img")).toHaveLength(1); + + fireEvent.error(image); + expect(screen.getByText("当前帧图片加载失败")).toBeTruthy(); + }); + + it("reports horizontal travel from the measured stage and actor widths", () => { + const onHorizontalBoundsChange = vi.fn(); + const rect = vi + .spyOn(HTMLElement.prototype, "getBoundingClientRect") + .mockImplementation(function (this: HTMLElement) { + const width = this.getAttribute("aria-label") === "动画预览舞台" ? 600 : 200; + return { + width, + height: 400, + x: 0, + y: 0, + top: 0, + right: width, + bottom: 400, + left: 0, + toJSON: () => ({}), + }; + }); + + render( + , + ); + + fireEvent.load(screen.getByRole("img", { name: "角色动画预览" })); + expect(onHorizontalBoundsChange).toHaveBeenLastCalledWith({ minX: -200, maxX: 200 }); + rect.mockRestore(); + }); + + it("surfaces frame markers and delegates timeline selection without its own playback behavior", () => { + // Catches a timeline dropping QC/rejection annotations or mutating playback rather than using its selection callback. + const onSelectFrame = vi.fn(); + + render( + , + ); + + expect(screen.getByText("关键帧")).toBeTruthy(); + expect(screen.getByText("QC: failed")).toBeTruthy(); + expect(screen.getByText("已驳回")).toBeTruthy(); + + fireEvent.click(screen.getByRole("button", { name: "第 2 帧" })); + expect(onSelectFrame).toHaveBeenCalledWith(1); + }); + + it("only relays playback controller callbacks", () => { + // Catches controls owning local play state instead of reporting user intent to the controller. + const onTogglePlaying = vi.fn(); + const onNextFrame = vi.fn(); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: "播放" })); + fireEvent.click(screen.getByRole("button", { name: "下一帧" })); + expect(onTogglePlaying).toHaveBeenCalledTimes(1); + expect(onNextFrame).toHaveBeenCalledTimes(1); + expect(screen.getByText("A 左行走")).toBeTruthy(); + expect(screen.getByText("D 右行走")).toBeTruthy(); + expect(screen.getByText("未提供跳跃动作")).toBeTruthy(); + expect(screen.getByText("下蹲动作可用")).toBeTruthy(); + }); +}); diff --git a/frontend/src/pages/playtest/workbench/animation-stage.tsx b/frontend/src/pages/playtest/workbench/animation-stage.tsx new file mode 100644 index 0000000..e4203f8 --- /dev/null +++ b/frontend/src/pages/playtest/workbench/animation-stage.tsx @@ -0,0 +1,112 @@ +import { useCallback, useEffect, useRef, useState } from "react"; + +import type { PreviewFrame } from "./model/types"; +import type { HorizontalStageBounds, StageOffset } from "./stage-motion"; + +export interface AnimationStageProps { + currentFrame: PreviewFrame | null; + /** Accumulated playback position in world coordinates (positive y is up). */ + motionOffset: StageOffset; + mirrored: boolean; + showGrid: boolean; + showChecker: boolean; + onHorizontalBoundsChange?(bounds: HorizontalStageBounds | null): void; +} + +export function AnimationStage({ + currentFrame, + motionOffset, + mirrored, + showGrid, + showChecker, + onHorizontalBoundsChange, +}: AnimationStageProps) { + const [failedImageUrl, setFailedImageUrl] = useState(null); + const stageRef = useRef(null); + const imageRef = useRef(null); + + const reportHorizontalBounds = useCallback(() => { + if (onHorizontalBoundsChange === undefined) return; + const stage = stageRef.current; + const image = imageRef.current; + if (stage === null || image === null) { + onHorizontalBoundsChange(null); + return; + } + + const stageWidth = stage.getBoundingClientRect().width; + const actorWidth = image.getBoundingClientRect().width; + if (stageWidth <= 0 || actorWidth <= 0) { + onHorizontalBoundsChange(null); + return; + } + const travel = Math.max(0, (stageWidth - actorWidth) / 2); + onHorizontalBoundsChange({ minX: -travel, maxX: travel }); + }, [onHorizontalBoundsChange]); + + useEffect(() => { + setFailedImageUrl(null); + }, [currentFrame?.imageUrl]); + + useEffect(() => { + reportHorizontalBounds(); + if (typeof ResizeObserver === "undefined") { + window.addEventListener("resize", reportHorizontalBounds); + return () => window.removeEventListener("resize", reportHorizontalBounds); + } + + const observer = new ResizeObserver(reportHorizontalBounds); + if (stageRef.current !== null) observer.observe(stageRef.current); + if (imageRef.current !== null) observer.observe(imageRef.current); + return () => observer.disconnect(); + }, [currentFrame?.imageUrl, reportHorizontalBounds]); + + const imageFailed = currentFrame !== null && failedImageUrl === currentFrame.imageUrl; + + return ( +
    + {showGrid ? ( +
    + ); +} diff --git a/frontend/src/pages/playtest/workbench/audit/audit-panel.test.tsx b/frontend/src/pages/playtest/workbench/audit/audit-panel.test.tsx new file mode 100644 index 0000000..9180f2c --- /dev/null +++ b/frontend/src/pages/playtest/workbench/audit/audit-panel.test.tsx @@ -0,0 +1,108 @@ +/** @vitest-environment jsdom */ +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { PreviewFrame } from "../model/types"; +import { AuditPanel } from "./audit-panel"; + +const frame: PreviewFrame = { + imageUrl: "/walk-03.png", + durationMs: 100, + rootMotion: { dx: 4, dy: 0 }, + qc: "pending", + rejected: false, + keyFrame: false, +}; + +afterEach(cleanup); + +describe("AuditPanel", () => { + it("marks the current frame and keeps automatic findings read-only", () => { + const onAdd = vi.fn(); + render( + , + ); + + expect(screen.getByText("自动")).toBeTruthy(); + expect(screen.getByText("主体接触画布边缘,可能发生裁切")).toBeTruthy(); + fireEvent.change(screen.getByLabelText("问题类型"), { + target: { value: "style_inconsistent" }, + }); + fireEvent.change(screen.getByLabelText("问题说明"), { + target: { value: "衣服颜色跳变" }, + }); + fireEvent.click(screen.getByRole("button", { name: "标记当前帧问题" })); + + expect(onAdd).toHaveBeenCalledWith( + expect.objectContaining({ + category: "style_inconsistent", + actionId: "walk", + direction: "south", + frameIndex: 2, + imageUrl: "/walk-03.png", + note: "衣服颜色跳变", + }), + ); + }); + + it("edits and removes an existing manual issue", () => { + const onUpdate = vi.fn(); + const onRemove = vi.fn(); + render( + , + ); + + expect(screen.getByText("人工")).toBeTruthy(); + fireEvent.change(screen.getByLabelText("人工问题类型"), { + target: { value: "motion_direction" }, + }); + fireEvent.change(screen.getByLabelText("人工问题说明"), { + target: { value: "移动方向错误" }, + }); + fireEvent.click(screen.getByRole("button", { name: "删除人工问题" })); + + expect(onUpdate).toHaveBeenCalledWith("manual-1", "motion_direction", "步幅突然变化"); + expect(onUpdate).toHaveBeenCalledWith("manual-1", "motion_discontinuity", "移动方向错误"); + expect(onRemove).toHaveBeenCalledWith("manual-1"); + }); +}); diff --git a/frontend/src/pages/playtest/workbench/audit/audit-panel.tsx b/frontend/src/pages/playtest/workbench/audit/audit-panel.tsx new file mode 100644 index 0000000..6c93617 --- /dev/null +++ b/frontend/src/pages/playtest/workbench/audit/audit-panel.tsx @@ -0,0 +1,189 @@ +import { useState } from "react"; + +import type { QualityFinding } from "../analysis/sequence-evidence"; +import type { PlaytestDirection, PreviewFrame } from "../model/types"; +import type { ManualAuditIssue, ManualIssueCategory } from "./audit-session"; + +const CATEGORY_LABELS: Readonly> = { + subject_cropped: "主体裁切", + transparency: "透明背景异常", + image_unavailable: "空白或加载失败", + duplicate_frame: "重复帧", + motion_discontinuity: "动作抖动或不连续", + motion_direction: "位移或方向错误", + style_inconsistent: "风格不一致", + other: "其他", +}; + +let fallbackId = 0; + +function createIssueId(): string { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + fallbackId += 1; + return `manual-${Date.now()}-${fallbackId}`; +} + +export interface AuditPanelProps { + actionId: string | null; + actionName: string | null; + direction: PlaytestDirection | null; + frameIndex: number; + frame: PreviewFrame | null; + automaticFindings: readonly QualityFinding[]; + issues: readonly ManualAuditIssue[]; + onAdd(issue: ManualAuditIssue): void; + onUpdate(id: string, category: ManualIssueCategory, note: string): void; + onRemove(id: string): void; +} + +export function AuditPanel({ + actionId, + actionName, + direction, + frameIndex, + frame, + automaticFindings, + issues, + onAdd, + onUpdate, + onRemove, +}: AuditPanelProps) { + const [category, setCategory] = useState("subject_cropped"); + const [note, setNote] = useState(""); + const canMark = actionId !== null && direction !== null && frame !== null; + + return ( +
    +
    +

    QUALITY ISSUES

    +

    问题记录

    +

    仅保留在当前 Playtest 会话

    +
    + +
    +

    自动发现

    + {automaticFindings.length === 0 ? ( +

    当前序列没有自动问题

    + ) : ( +
      + {automaticFindings.map((finding, index) => ( +
    • +
      + {finding.message} + 自动 +
      + + {finding.frameIndex === null ? "整段序列" : `第 ${finding.frameIndex + 1} 帧`} + +
    • + ))} +
    + )} +
    + +
    +

    + 标记当前帧{actionName === null ? "" : ` · ${actionName} #${frameIndex + 1}`} +

    + +