完善资源画布无限坐标与分页交互

调整资源分页画布 viewport 与复位按钮交互

允许资源元素使用 -1,000,000 到 1,000,000 的持久化坐标

同步 TypeScript、Rust 合同、布局校验与负坐标回归测试

更新资源画布 PRD 与技术方案

保留本地 .env 未提交
This commit is contained in:
2026-08-23 16:33:03 +08:00
parent 52a58f7514
commit f3092c8bbc
13 changed files with 1681 additions and 1016 deletions
@@ -2,7 +2,7 @@
## 目标
资源管理的“按依赖”视图使用统一世界坐标的自由画板,支持拖拽平移、滚轮平移以及按住 Ctrl/Meta 的锚点缩放。资源详情是叠加在资源画板之上的非模态卡片;打开、切换和关闭详情都不得卸载背景工具栏、资源卡、依赖连线或重置 viewport、搜索和排序模式。
资源管理在空项目中展示现有按类型式分区展览。任一栏目出现资源后,“按依赖”和“按类型”共用同一套栏目分页画布:每个非空栏目拥有一个铺满资源管理区域的独立画布,左侧显示飞书文档式栏目大纲,顶部显示当前栏目标题,底部显示下一页标题;普通滚轮切换栏目,按住 Ctrl/Meta 的滚轮以指针为锚点缩放当前画布。两种模式唯一差异是“按依赖”在当前栏目内绘制引导线。资源详情是叠加在资源画板之上的非模态卡片;打开、切换和关闭详情都不得卸载背景工具栏、资源卡、依赖连线或重置 viewport、搜索和排序模式。
图片资源从“编辑资源”进入持续精修草稿后,点击任意图片直接在图片下方显示快速编辑卡。每次快速编辑创建新的候选图,不覆盖来源图层;生成任务立即在画布中创建占位并进入任务侧栏。用户显式选择候选图“设为最终图”后,保持原资产 ID 不变,以事务方式切换 manifest 指向的正式 PNG;精修草稿和其它候选图继续保留。
@@ -10,6 +10,12 @@
## 权威数据与持久化
### 栏目分页投影与文档缺失边界
- 非空栏目页只由当前权威资源投影构造:manifest assets、已完成任务的 artifacts、用户已导入附件和持久化 Agent 文本回执。前端不得扫描项目目录自行发明资源。
- 文档栏因此只有在上述来源存在 Markdown / TXT / JSON / YAML 等文档类条目,或存在 Agent 文本回执时才出现。若 manifest 没有文档资产、任务 artifacts 未完成且没有附件 / 回执,资源页不显示文档页;这是生成或登记缺失,不由 UI 补假数据。
- Direct Codex 的游戏生成链路以 `game/index.html``game/style.css``game/game.js` 作为代码资产登记;是否额外产出设计文档由当前任务和 Agent 决定,资源页只消费已登记结果。
### 持续精修草稿
- 一个图片 `sourceAssetId` 唯一发现一个活动精修 `draftId`;重新进入时恢复原草稿,不依赖进程内状态或创建平行画布。
@@ -34,6 +40,31 @@
- canonical 来源缓存属于本地私有恢复数据,不写入 manifest。缓存身份至少绑定平台服务身份、平台 owner、Editor Project ID、源图片 SHA256 与 canonical kind;任一身份或图片内容变化都必须重新登记。
- 缓存命中时复用已登记资源,避免每次快速编辑重复上传。平台明确返回来源不存在或类型不支持且请求尚未入队时,可以失效缓存并重建一次;不确定受理、已受理和对账状态不得换幂等键重提。
### 最终图事务恢复对账问题与修复
现象:用户连续两次“设为最终图”后重新打开画布,恢复阶段返回 `reconciliation-required`,前端提示“安全恢复原任务需要对账 / 素材画布存在无法自动证明的未完成事务”。
根因:
- 提交成功后 transaction 目录不会归档,`recover_asset_canvas_transactions_at` 会重放全部历史事务。
- `recover_asset_canvas_transaction_locked``ledger=Committed` 分支要求当前 manifest 中该 asset 仍指向本事务的 `finalImageRelativePath`;后续提交已把同一 asset 指向新的 `assets/canvas/<name>--<commitId>.png``asset_present=false` 后直接 `mark_asset_canvas_reconciliation_locked`,没有先执行 `find_superseding_asset_canvas_commit`(该检查目前只在 unresolved 分支生效)。
- 前端 `recover()` 只要存在任一 `reconciliation-required` outcome 就把整次恢复判失败,因此一个已被后续提交取代的旧事务会阻塞整个画布打开。
- 现有测试此前只覆盖“第一个事务被强制标记 unresolved 后由后续已提交事务取代”,缺少“两个事务都成功提交后恢复”的用例,因此未暴露。
修复合同:
1. `ledger=Committed` 分支在进入 reconciliation 前先执行同一套 supersede 证明;命中则把旧 journal/ledger 标记为 `Superseded`,返回 `Superseded`,不得阻塞画布打开。
2. supersede 证明继续要求后续事务已完整提交、after manifest/revision 与当前状态一致、正式文件哈希匹配,且后续事务的 before 快照精确衔接旧事务的 after 快照;不得仅凭时间戳或路径变化猜测。
3. 重复恢复幂等:已 `Superseded` 的事务直接返回既有状态,不再改成对账。
4. 回归测试覆盖“两个连续成功提交后恢复”:旧事务必须收敛为 `Superseded`,新事务为 `AlreadyCommitted`,整轮恢复不得出现 `ReconciliationRequired`
5. 恢复失败可观测性仍保留后续增强方向:失败结果应携带 outcomes 明细(commitId、status、stage、证据摘要),前端展示具体事务并支持重试。
验收:
- 连续两次“设为最终图”后关闭再打开画布不再出现对账提示,且最终 manifest 指向最新提交。
- 现有 fault-injected 恢复用例(`RevisionInstalled``LedgerCommitted` 等)全部保持通过,未出现新的 `ReconciliationRequired` 回归。
- 重复恢复幂等,已 `Superseded` 事务不再重放为对账。
### 本地图片批量导入
- Tauri 正式链路使用原生多选文件对话框,不以隐藏 DOM `input[type=file]` 作为唯一入口。取消选择返回受控 `cancelled`,不修改草稿。
@@ -50,11 +81,15 @@
## 操作边界
- 栏目顺序固定为 `文档 -> 美术资源 -> 音乐音效资源 -> 游戏代码 -> 项目版本`。完全空项目显示全部栏目的分区展览;任一栏目出现资源后,分页大纲展示全部栏目,默认停留在该顺序中的第一个非空栏目,空栏目计数为 0 且可打开空画布。
- 普通滚轮向下切到下一栏目、向上切到上一栏目并循环;短时间连续滚轮事件需要节流,避免触控板一次惯性滚动跨多页。点击大纲或底部“下一页”标题是显式切页入口。
- 每个栏目画布保留独立 viewport;切换栏目后先按该栏目内容适配视口。空白处拖拽平移画布,资源卡拖拽移动卡片并更新依赖线,Ctrl/Meta 缩放只作用于当前栏目,不能牵动其它栏目。
- 依赖画布复用 `@genarrative/image-canvas-core` 的 viewport 计算,并复用现有资源卡片、布局和依赖连线模型。
- 依赖模式不使用资源分区滚动条、分区缩放或分区高度操作作为主要导航;分区仍是画布世界中的视觉分组
- 依赖画布存在不可见导航边界:以当前项目全部资源的权威布局矩形、真实卡片尺寸、分区标题和世界 extent 的并集为内容范围,再在屏幕坐标四周外扩 `96px` 安全留白。搜索、详情卡和临时隐藏不得缩小边界,只有项目资源或权威布局真正变化时才重算
- 滚轮平移、指针拖动画布Ctrl/Meta 缩放、复位以及容器 resize 后都必须经过同一 viewport 约束。缩小下限是“全部内容加安全留白刚好可见”的 fit scale;内容小于视口时居中,内容大于视口时四条边最多移动到安全留白位置,禁止把资源整体拖出屏幕。边界只约束会话 viewport,不写入 manifest 或资源布局 sidecar,也不显示可见边框
- 统一世界画板显示当前可见资源之间的全部合法精确引用,包括跨视觉分组引用;装饰 SVG 与视觉隐藏的关系说明消费同一组可见边,搜索隐藏任一端点时两者同步移除。任务流仍只参与同类型布局聚类,不绘线也不进入关系说明
- 非空状态不使用资源分区滚动条、分区缩放或分区高度操作作为主要导航;栏目通过大纲、底部下一页标题和滚轮切换
- 当前栏目画布背景是无限的:用户可以将 viewport 沿 x/y 任意方向平移,画布不以资源 extent 作为导航边界,也不显示可见画布边缘。资源卡片的持久化布局坐标允许落在 `-1_000_000..=1_000_000`,用于支撑元素位于世界原点左上方;超出该范围仍拒绝写入,避免持久化非法布局。这与 viewport 能否继续平移是两层独立语义。搜索、详情卡和临时隐藏不得改变 viewport
- 普通滚轮切换栏目,指针拖动空白平移画布Ctrl/Meta 缩放、复位以及容器 resize 后都必须保持同一 viewport 数据流。切换栏目、切换排序模式或显式复位时才重新适配内容;图片尺寸测量、布局拖动或资源 extent 变化只归一化当前 viewport,不得意外重置用户已经完成的平移和缩放。普通平移不夹取 x/y;缩放仍受共享画布的最小/最大比例限制,初次 fit 只使用 `96px` 内缩作为视觉留白
- 资源卡拖动使用 `5px` 阈值区分点击与移动;移动期间按当前 scale 乐观换算世界坐标、显示拖动态并同步依赖线,释放时提交一次 `manuallyPlaced=true` 布局 CAS,取消则回滚预览且不提交。拖动后的释放点击不打开详情
- 依赖模式只在当前栏目画布内显示两端都属于该栏目的合法精确引用;装饰 SVG 与视觉隐藏的关系说明消费同一组可见边,搜索隐藏任一端点时两者同步移除。任务流仍只参与同类型布局聚类,不绘线也不进入关系说明。
- 资源详情卡包含元数据、媒体预览和“编辑资源”操作,但不使用全屏 backdrop、不声明 `aria-modal=true`、不把 `focusedResource` 作为背景工具栏渲染条件。桌面端允许继续操作背景画板;窄屏可以使用有边界的贴边卡,但背景组件必须保持挂载。
- 资源详情卡定位相对 `.game-workbench-stage`,桌面端在中间主视窗居中并受主视窗宽高边界约束;禁止使用相对整个窗口的右上角 `position: fixed` 定位。
- 图片选中后的浮动工具栏整体退役。普通点击图片直接打开唯一的图片下方快速编辑卡;拖动不误触发,点击另一张图片切换卡片,点击画布空白关闭卡片。
@@ -73,20 +108,22 @@
## 验收
1. 在依赖视图拖拽画布可以平移,滚轮可以平移,Ctrl/Meta + 滚轮以指针位置为锚点缩放,复位按钮可以适配全部依赖内容;极端拖拽和连续缩小均停在全部资源加 `96px` 安全留白形成的隐藏边界,搜索过滤不会改变该边界
2. 同类型和跨视觉分组的合法精确引用均在统一世界中连线,并具有与当前可见连线一致的无障碍关系说明;搜索隐藏任一端点后连线和说明同时消失
3. 点击任意资源后“新增资源 / 按依赖 / 按类型 / 复位”仍保持挂载和原状态;详情为非模态独立卡片,背景画板 viewport、搜索、排序、卡片和连线不卸载、不重置
4. 图片资源点击“编辑资源”后恢复唯一活动 refine 草稿,入口图进入默认参考;点击任意图片直接在其下方显示快速编辑卡,图片上方不再出现浮动工具栏
5. 快速编辑卡底部显示适用的删除、设为最终图和修改动作。拖动图片不误触发卡片;删除只影响草稿图层;候选设为最终图后保持原 asset ID、更新正式 PNG,草稿及其它候选仍可继续编辑
6. 点击修改后立即创建避让现有内容的生成占位和任务侧栏记录;成功图在同一落点出现,失败只影响该占位和任务,重启后恢复原 generation/operation,不重新扣费提交
7. refine 请求通过严格 DTO 合同且不携带非法 `assetKind`;参数、来源类型、余额、鉴权和对账错误显示不同的安全错误码
8. 1:1、16:9、9:16、2:3、3:2 和极端横竖图片按真实比例显示并受边界限制;卡片不重叠,世界 extent、复位视口和依赖连线端点正确。非图片资源尺寸不变
9. GIF、AVIF、BMP、SVG 等不满足栅格快速编辑能力的资源继续使用能力模型中的降级编辑路径,不伪装成可提交 PNG 候选
10. 历史 `game-background` 等私有类型来源不得直接作为 refine 主来源;首次快速编辑完成 canonical 登记,后续同源请求复用缓存,并实际到达 `candidate-ready`
11. 点击导入可以原生多选图片;成功批次只推进一次草稿 revision、立即出现在当前视口并被选中,取消和失败不改变草稿且不遗留部分导入
12. 明确失败任务可从占位或任务侧栏归档,重启后不恢复;`reconciliation-required` 不显示删除能力。任务列表具有与现役美术画布一致的独立按钮、双 Tab、状态图标与时间/进度信息;折叠后只保留按钮且不丢任务数据,新建任务自动展开
13. 画布带运行中 generation 重新进入时,在远端恢复完成前已经可选图、平移和继续编辑;后台恢复继续推进原 operation,不重复 POST、不重新扣费。阻断性提交/恢复失败的遮罩覆盖整个 Tauri 窗口
14. 已登记 refine 资产的候选设为最终图后保持原 `assetId`、原 `source.resourceId` 与新正式 PNG 路径;`revision-installed` 中断后重新加载可前向收敛为 committed
1. 完全空项目在按依赖与按类型下都显示相同的分区展览;任一栏目出现资源后,两种模式都切换为栏目分页画布,大纲包含全部栏目,空栏目计数为 0 且可打开空画布
2. 栏目顺序为 `文档 -> 美术资源 -> 音乐音效资源 -> 游戏代码 -> 项目版本`,游戏代码不再固定在首位;普通滚轮可循环切换栏目,点击大纲和底部下一页标题也可切页
3. 在当前栏目内拖拽空白可以无限平移画布,拖拽资源卡可以移动卡片并同步依赖线,Ctrl/Meta + 滚轮以指针位置为锚点缩放,复位按钮可以适配当前栏目内容;普通平移不会因资源 extent、图片测量或窗口 resize 被拉回,连续缩放仍停在共享画布的最小/最大比例范围内
4. 按依赖模式显示当前栏目内同类型两端资源的合法精确引用,并具有与当前可见连线一致的无障碍关系说明;按类型模式不显示引导线;搜索隐藏任一端点后连线和说明同时消失
5. 点击任意资源后“新增资源 / 按依赖 / 按类型 / 复位”仍保持挂载和原状态;详情为非模态独立卡片,背景画板 viewport、搜索、排序、卡片和连线不卸载、不重置
6. 图片资源点击“编辑资源”后恢复唯一活动 refine 草稿,入口图进入默认参考;点击任意图片直接在其下方显示快速编辑卡,图片上方不再出现浮动工具栏
7. 快速编辑卡底部显示适用的删除、设为最终图和修改动作。拖动图片不误触发卡片;删除只影响草稿图层;候选设为最终图后保持原 asset ID、更新正式 PNG,草稿及其它候选仍可继续编辑
8. 点击修改后立即创建避让现有内容的生成占位和任务侧栏记录;成功图在同一落点出现,失败只影响该占位和任务,重启后恢复原 generation/operation,不重新扣费提交
9. refine 请求通过严格 DTO 合同且不携带非法 `assetKind`;参数、来源类型、余额、鉴权和对账错误显示不同的安全错误码
10. 1:1、16:9、9:16、2:3、3:2 和极端横竖图片按真实比例显示并受边界限制;卡片不重叠,当前栏目 extent、复位视口和依赖连线端点正确。非图片资源尺寸不变
11. GIF、AVIF、BMP、SVG 等不满足栅格快速编辑能力的资源继续使用能力模型中的降级编辑路径,不伪装成可提交 PNG 候选
12. 历史 `game-background` 等私有类型来源不得直接作为 refine 主来源;首次快速编辑完成 canonical 登记,后续同源请求复用缓存,并实际到达 `candidate-ready`
13. 点击导入可以原生多选图片;成功批次只推进一次草稿 revision、立即出现在当前视口并被选中,取消和失败不改变草稿且不遗留部分导入
14. 明确失败任务可从占位或任务侧栏归档,重启后不恢复;`reconciliation-required` 不显示删除能力。任务列表具有与现役美术画布一致的独立按钮、双 Tab、状态图标与时间/进度信息;折叠后只保留按钮且不丢任务数据,新建任务自动展开
15. 画布带运行中 generation 重新进入时,在远端恢复完成前已经可选图、平移和继续编辑;后台恢复继续推进原 operation,不重复 POST、不重新扣费。阻断性提交/恢复失败的遮罩覆盖整个 Tauri 窗口。
16. 已登记 refine 资产的候选设为最终图后保持原 `assetId`、原 `source.resourceId` 与新正式 PNG 路径;`revision-installed` 中断后重新加载可前向收敛为 committed。
## 验证命令