mirror of
https://github.com/motajs/template.git
synced 2026-10-31 13:50:17 +08:00
docs(09-26): add lazy overlay plan and execution summary (code uncommitted)
This commit is contained in:
parent
c5fca9e777
commit
3f7afb635e
@ -0,0 +1,12 @@
|
||||
{
|
||||
"head": "c5fca9e777d1750daeaea78ef0152bba31dd8c18",
|
||||
"hashes": {
|
||||
"status.ts": "9236bc34330d0709a0c26884f967ef77f41c053b",
|
||||
"moving.ts": "a61aa951771e0c4d103dbf04b789b380354c8767",
|
||||
"viewport.ts": "3c8f224937f2a9743689f0ffba51cbfb3e07b2f3",
|
||||
"block.ts": "629099e1970ec38ab93e4e29e4949d424f3e9d93",
|
||||
"element.ts": "bc9e9d9d01d889cf275ea9a5336454072438af69",
|
||||
"constant.ts": "d29a817717f668f615ddf4b3d195ee5d64f77df1",
|
||||
"shader/map.frag": "d8f3df3705d8c999a6ea033bd45039e5d91e6fea"
|
||||
}
|
||||
}
|
||||
514
.planning/phases/09-minimum-browser-runnable/09-26-PLAN.md
Normal file
514
.planning/phases/09-minimum-browser-runnable/09-26-PLAN.md
Normal file
@ -0,0 +1,514 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 26
|
||||
type: execute
|
||||
# 21:沿用本阶段既有计划的波次编号,使本计划排期在 09-01..09-25 之后(D-01 增量派发,计划累积)
|
||||
# depends_on 为空:本计划改动的文件没有任何其它计划触碰,不存在同波次并行写冲突,故结构上独立;
|
||||
# 运行在最后的波次只是「排期在后」,不是「依赖前序计划」
|
||||
wave: 21
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- packages-user/client-base/src/map/vertex.ts
|
||||
- packages-user/client-base/src/map/renderer.ts
|
||||
- packages-user/client-base/src/map/types.ts
|
||||
- packages-user/client-base/src/map/__test__/rendererGate.test.ts
|
||||
- packages-user/client-base/src/map/overlay.ts
|
||||
- packages-user/client-base/src/map/__test__/overlay.test.ts
|
||||
- .planning/phases/09-minimum-browser-runnable/09-26-BASELINE.json
|
||||
- .planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
# 未校准估算(confidence: low),SUMMARY 须声明这一点
|
||||
# 不沿用 09-25 的 160000 超预算声明:本计划是同一批文件的合并式重写(单计划、不拆分),
|
||||
# 估算按「读文件 + 改 3 个源码 + 重写闸门测试 + 三类门禁」重算;09-25 实际 diff 约 10416 tokens,
|
||||
# 本计划新增了 WebGL 解释性注释与闸门重写,故取更高的粗略值,仍属未校准
|
||||
tokens: 80000
|
||||
raw_tokens: 80000
|
||||
tasks: 5
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(D-03 / D-04)**:先汇报「把覆盖层实现全部收敛进 `vertex.ts`、把 `setStaticAlpha` 改为完全惰性、把 flush 放到取数组时」的要做什么 / 为什么 / 大致怎么做 / 影响范围,逐条列明目标设计、文件范围、全部待批准命名(OI-1)、已知限制与风险、验证方式,以及「不提交代码 / 不做浏览器 UAT」两条铁律,然后停下等待用户恰好回复「可以执行」。"
|
||||
- "**覆盖层只存在于 `vertex.ts`**:`overlay.ts` 与 `__test__/overlay.test.ts` 被删除;覆盖层状态(`pendingAlpha` / `overlayStatic` / 两个脏标记 / 暂存数组 / 暂存数量 / 排序缓存)、类型(私有 `IOverlayStaticRecord`、私有绘制条目接口、私有条目种类枚举、文件内私有的阈值常量与判定函数)与全部逻辑(flush / sort / assemble)都写在 `MapVertexGenerator` 与其文件内,**不新增导出类型、不新增类**。"
|
||||
- "**`setStaticAlpha` 完全惰性(锁定设计)**:方法体内只做三件事——把 alpha 写进 `pendingAlpha`(图层对象 + 数值地图索引)、标记值脏、调用 `renderer.requestUpdate()`。方法体内**不得**查找实例、不得置零 / 恢复 rect、不得增删成员、不得排序。真正的实例写入与成员变化全部发生在下一次取顶点数组时的 flush。`getStaticAlpha` 必须在读实例数组前优先返回 `pendingAlpha` 中的待应用值,保持 `IBlockStatus.getAlpha()` 的即时语义。"
|
||||
- "**flush / 取数组契约(锁定设计)**:`getVertexArray()` 在 `checkRebuild()` 之后调用 `flushPendingAlpha()`,是不透明遍上传分块数据之前的唯一 flush 点;`getOverlayArray()` 先 flush、再按需排序、再按需组装并返回暂存数组;`isOverlayDirty()` 覆盖「有 pending / 顺序脏 / 值脏」三种情况;`getOverlayCount()` 返回已组装的数量,其 jsDoc 明确写明「顺序或值脏时需先调用 `getOverlayArray()`」,renderer 侧按该契约在脏时先取数组再取数量。"
|
||||
- "**「排序后必然重新组装」不变式(顺序 → 组装不得被跳过)**:`sortOverlay()` 结束时**必须同时置 `overlayValueDirty = true`**(顺序 / 成员变化等于暂存数组失效),因此 `getOverlayArray()` 中「先 flush、顺序脏则 sort、再在值脏或顺序脏时 assemble」的顺序在 `sortOverlay()` 之后**必然**进入 `assembleOverlay()`;`deleteMoving()` **必须同时置 `overlayOrderDirty` 与 `overlayValueDirty`**(只置顺序脏时,若排序把顺序脏清掉就会出现「已删除的动态块仍留在暂存数组与 `overlayCount` 里」的残影)。`assembleOverlay()` 成功写入后清两个脏标记。该不变式既写进代码,也由 Task 1 与 Task 3 的源码闸门断言。"
|
||||
- "**无字符串键(锁定设计)**:待应用 alpha 为 `Map<IMapLayer, Map<number, number>>`,静态记录为 `Map<IMapLayer, Map<number, IOverlayStaticRecord>>`,键是图层对象与数值地图索引;动态条目不再有任何登记表,成员每次排序时从活动集合 `renderer.getMovingBlock()` 现取。排序键为 `(layerIndex, y, x, kind)`,静态先于动态;`x` 只在 CPU 侧作为同一行的稳定次级键,绝不编码进 z / 深度。"
|
||||
- "**渲染器契约与既有修正全部保留**:两遍式(不透明遍 `depthMask(true)` 且裁掉 `dynamicStart` 之后的动态尾部、覆盖层遍 `depthMask(false)` 且深度测试仍开、收尾恢复 `depthMask(true)`)、`blendFuncSeparate`、ping/pong 各自的颜色纹理与 `DEPTH_COMPONENT24` 深度 renderbuffer、0 / 1 / 2+ 效果链路、`setCanvasSize` 重设两个深度缓冲、`destroy` 删除新增对象、`map.frag` 的 `0.004`、OI-1 (A) 的 `dataX` / `dataY` 全部保留;新增的 WebGL 代码必须带解释性注释(为什么关深度写入、为什么覆盖层最后画、为什么分离式混合、FBO / 深度附件 / ping-pong 如何配对)。"
|
||||
- "**验证形态(无浏览器 UAT)**:以「源码形状闸门(vitest 读源码)+ eslint + 按文件过滤的 `vue-tsc --noEmit`(精确豁免用户裁定的 `getLayerByAlias` 基线一条并打印原文)+ 受保护文件哈希范围闸门」作为全部自动化验证;**不宣称任何 WebGL E2E**,SUMMARY 如实记录浏览器 UAT 因游戏尚不可运行而跳过。**代码一律不提交**:不得运行 `git add` / `commit` / `stash` / `checkout` / `worktree`,工作树留给用户审查;09-25 的未提交代码由本计划合并 / 替换,而不是提交。"
|
||||
- "**范围与纪律**:只写 `files_modified` 的 8 个路径(6 个代码路径,其中 2 个是删除;另加 `09-26-BASELINE.json` 与 `09-26-SUMMARY.md` 两个 `.planning/` 产物);`status.ts` / `moving.ts` / `viewport.ts` / `block.ts` / `element.ts` / `constant.ts` / `map.frag` / 其它包 / 依赖与配置 / 09-01..09-25 计划全部零改动,并由「Task 0 写入 `09-26-BASELINE.json` 的 7 个 `git hash-object` + HEAD → Task 4 用真值相等比较复核」的哈希闸门证明(不是只看哈希是否出现在 SUMMARY 里);不修改任何既有注释或 jsDoc(只新增);不复用任何未获批准的新命名。"
|
||||
artifacts:
|
||||
- path: packages-user/client-base/src/map/vertex.ts
|
||||
provides: "覆盖层的唯一实现处:`pendingAlpha`(`Map<IMapLayer, Map<number, number>>`)、`overlayStatic`(`Map<IMapLayer, Map<number, IOverlayStaticRecord>>`)、`overlayOrderDirty` / `overlayValueDirty`、`overlayInstancedArray` / `overlayCount` / 排序缓存、私有 `IOverlayStaticRecord` 与私有绘制条目接口与私有种类枚举、文件内私有阈值常量与判定函数、`flushPendingAlpha()` / `sortOverlay()` / `assembleOverlay()` / 记录读写辅助方法、惰性 `setStaticAlpha`、`getStaticAlpha` 的 pending 优先读、`checkRebuild` / `setBlockSize` 的清空、`updateTileVertex` 与 `updateVertexArray` 的成员刷新、`updateMoving` 与 `deleteMoving` 的两个脏标记(排序后必然重新组装)、`isOverlayDirty()` / `getOverlayArray()` / `getOverlayCount()` 与 `getVertexArray()` 的 flush 点"
|
||||
- path: packages-user/client-base/src/map/renderer.ts
|
||||
provides: "按新契约取覆盖层(脏时先 `getOverlayArray()` 再 `getOverlayCount()`)、覆盖层独立缓冲区上传与 `drawArraysInstanced`、两遍式状态与收尾恢复、动态尾部裁剪、`blendFuncSeparate`、ping/pong 颜色+深度配对、0/1/2+ 效果链路、缩放与销毁;新增解释性 WebGL 注释;`needUpdate()` 纳入覆盖层脏"
|
||||
- path: packages-user/client-base/src/map/types.ts
|
||||
provides: "已有 `IContextData.overlayBuffer` / `pingDepthBuffer` / `pongDepthBuffer` 与 `IMapVertexGenerator.isOverlayDirty()` / `getOverlayArray()` / `getOverlayCount()` 保持不变;仅补齐 / 细化 `getOverlayCount()` 的「需先取数组」契约说明(09-25 新增声明的 jsDoc,可增补);既有成员与既有注释零改动"
|
||||
- path: packages-user/client-base/src/map/__test__/rendererGate.test.ts
|
||||
provides: "重写后的源码形状闸门(`?raw` 读取,不导入 WebGL 模块):无 `overlay` 导入、无 `static:` / `dynamic:` 字符串键、`setStaticAlpha` 方法体只写 pending、`getVertexArray()` 是 flush 点、`getOverlayArray()` 的 flush→sort→assemble 顺序、`isOverlayDirty()` 覆盖三类脏、数值嵌套 Map、`(layerIndex, y, x, kind)` 排序、静态先于动态、动态成员取自 `getMovingBlock()`、组装覆盖 rect、`getStaticAlpha` 的 pending 优先、两遍式 / 分离式混合 / 深度附件 / ping-pong 配对 / 0-1-2+ 链路 / 缩放 / 销毁 / 动态尾部裁剪 / 分块布局未被排序 / 动态索引身份 / `map.frag` 阈值 / 无半精度与顺序剥离 / OI-1 的 `dataX`-`dataY` / 新私有方法的 jsDoc 与覆盖层代码段的注释密度;每个 `it` 前一行注释"
|
||||
- path: packages-user/client-base/src/map/overlay.ts
|
||||
provides: "**删除**(09-25 遗留的独立排序 / 成员状态机;其能力全部并入 `vertex.ts`)"
|
||||
- path: packages-user/client-base/src/map/__test__/overlay.test.ts
|
||||
provides: "**删除**(其被测类已不存在;等价断言改由 `rendererGate.test.ts` 的源码形状闸门承担)"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-26-BASELINE.json
|
||||
provides: "Task 0 写出的机器可读基线:`{ head: <git rev-parse HEAD>, hashes: { status.ts, moving.ts, viewport.ts, block.ts, element.ts, constant.ts, 'shader/map.frag' } }`,共 7 个 `git hash-object` 值;Task 4 用它做「执行前后真值相等」比较(不是只检查哈希是否出现在 SUMMARY 里)。只在 Task 0 写一次,执行期不得重写。"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md
|
||||
provides: "执行摘要:最终落点(覆盖层全部在 `vertex.ts`)、惰性 `setStaticAlpha` 与 flush 契约、数值嵌套键与「动态成员现取」、排序键与两个脏标记、保留项清单(两遍式 / 分离式混合 / 深度附件 / 0-1-2+ / 缩放销毁 / 0.004 / OI-1)、三类门禁与哈希范围闸门结果、浏览器 UAT 跳过与「无 WebGL E2E」声明、`estimate` 未校准声明、未提交代码与未被触碰文件清单"
|
||||
key_links:
|
||||
- "`setStaticAlpha`(只写 `pendingAlpha`)↔ `flushPendingAlpha()`(写实例 alpha、置零 / 恢复 rect、增删记录)↔ `block.data.markRenderDirty()` ↔ 不透明遍的 `bufferSubData` —— 四者必须指向同一个实例槽;flush 之后若没有标记分块渲染脏,GPU 上仍是旧 rect,表现为「该透明的没透明 / 不该透明的被挖空」"
|
||||
- "`getVertexArray()`(`checkRebuild()` 之后 flush)↔ `MapRenderer.render()` 中 `checkTileVertexArray` → `getRenderArea` → `area.dirty` 上传的顺序 —— flush 必须发生在计算脏区之前,否则本次 flush 造成的 rect 变化要等下一帧才上屏"
|
||||
- "`pendingAlpha` ↔ `isOverlayDirty()` ↔ `needUpdate()` —— 只标 `overlayValueDirty` 却不把 pending 计入脏标记,会让「已请求但不脏」的 alpha 永远得不到 flush"
|
||||
- "`getOverlayArray()` 内部顺序 flush →(顺序脏则)sort →(值脏或刚排序则)assemble ↔ 覆盖层缓冲上传 —— 先排序后 flush 会让新成员缺席本次排序,先组装后排序会让装入顺序与排序结果不一致"
|
||||
- "`overlayStatic` 记录的 rect ↔ 分块数组中被置零的实例 rect ↔ 组装时覆盖前四个 float —— 记录值与实际置零值必须同源;`updateTileVertex` 的 Position 更新必须刷新记录并再次置零,否则同一图块会被不透明遍与覆盖层各画一遍"
|
||||
- "排序键 `(layerIndex, y, x, kind)` ↔ 排序缓存数组 ↔ 暂存数组的装入顺序 ↔ `drawArraysInstanced` 的实例顺序 —— WebGL2 的实例顺序只由缓冲区顺序决定,CPU 排序与装入必须严格一致"
|
||||
- "活动 `renderer.getMovingBlock()` ↔ 每次排序重建动态条目(`index` / `x` / `y` / `layer` 现取)↔ `MovingBlock.index` 身份 —— 不缓存动态副本就不会出现「销毁后仍画残影」或「图层改了还用旧 layerIndex」"
|
||||
- "`checkRebuild()` / `setBlockSize()` ↔ 清空 `overlayStatic` 与 `pendingAlpha` ↔ 新分配的实例数组(alpha 复位为 1、rect 由地图数据重建)—— 不清空会留下指向旧数组语义的成员,表现为永远不可见或重复绘制"
|
||||
- "ping / pong FBO ↔ 各自的颜色纹理与 `DEPTH_COMPONENT24` 深度 renderbuffer ↔ 效果链的输入 / 输出配对 —— 配对写反会形成「读自己正在写的纹理」的反馈环;缺深度附件会让场景在离屏渲染时失去深度测试"
|
||||
- "`IMapRenderData.render` 的动态尾部(`viewport.checkDynamic`)↔ 不透明遍按 `dynamicStart` 裁剪 —— 动态块改由覆盖层绘制后,不透明遍必须裁掉这一截,否则动态块被画两遍"
|
||||
assumptions:
|
||||
- "**工作树基线(规划日实测)**:规划开始时 HEAD 为 `c714c99`,规划末端实测已推进到 `c5fca9e`(`docs(10-30)`,与本计划无关的文档提交);本计划**不依赖具体 HEAD**,只要求 Task 0 用 `git rev-parse HEAD` 记录、Task 4 复核「HEAD 未变、无新提交」。`map/` 下已有 09-25 的未提交改动(`renderer.ts` / `vertex.ts` / `types.ts` / `map.frag` 修改,`overlay.ts` / `__test__/rendererGate.test.ts` / `__test__/overlay.test.ts` 未跟踪),`status.ts` 另有用户自己的参数顺序修改。**本计划把 09-25 的未提交代码视为「待合并的草稿」:保留其中的两遍式、后处理、`map.frag`、OI-1 (A),并替换其覆盖层实现;不提交、不回滚、不 stash。**"
|
||||
- "**`map/` 执行前的真实 git 状态(规划日 `git status --porcelain -uall -- packages-user/client-base/src/map` 实测):** ` M renderer.ts` / ` M shader/map.frag` / ` M status.ts` / ` M types.ts` / ` M vertex.ts` / `?? __test__/rendererGate.test.ts` / `?? overlay.ts` / `?? __test__/overlay.test.ts`。`git ls-files --error-unmatch` 已确认 `overlay.ts`、`__test__/overlay.test.ts`、`__test__/rendererGate.test.ts` **三者均未被跟踪**。因此:①删除 `overlay.ts` / `overlay.test.ts` 后**不会**出现 `D` 行(未跟踪文件删除后直接从 `git status` 消失),范围闸门必须用 `Test-Path` / `fs.existsSync` 断言「文件不存在」,**不得**期望 `D`;②`__test__/rendererGate.test.ts` 被重写后仍然是 `??`;③`status.ts`(用户维护)与 `shader/map.frag`(09-25 保留)本来就是 ` M`,范围闸门必须显式允许这两个 ` M` 状态,再由哈希闸门证明它们**没有**被本计划改动。执行后 `map/` 的期望变更集恰好是:` M renderer.ts`、` M shader/map.frag`、` M status.ts`、` M types.ts`、` M vertex.ts`、`?? __test__/rendererGate.test.ts`,且两个 overlay 文件不存在。"
|
||||
- "**用户并发脏文件(规划日 `git status --porcelain` 实测,均不在本计划范围)**:`.planning/phases/10-docs-restructure/10-CONTEXT.md`、`docs/advanced/mover.md`、`packages-user/client-base/src/components/{choices.tsx(删除),index.ts,input.tsx,misc.tsx}`、`packages-user/client-base/src/index.ts`、`packages-user/client-base/src/use/{types.ts,hotkey.ts(新增),index.ts(新增)}`、`packages-user/client-modules/src/action/{hotkey.ts(删除),index.ts}`、`packages/common/src/logger.json`、`packages/system/src/hotkey/{system.ts,types.ts}`、`src/client/misc/hotkey.ts`、`src/client/ui/choices.tsx(新增)`。本计划一律不碰;范围闸门以「受保护文件哈希不变 + `map/` 变更集 ⊆ 白名单」判定,不要求整个工作树干净。"
|
||||
- "**`status.ts` 由用户维护(只读)**:`StaticBlockStatus.setAlpha` 已由用户改为 `(this.layer, this.x, this.y, alpha)`,与 `IMapVertexStatus.setStaticAlpha(layer, x, y, alpha)` 签名一致。本计划不修改它,并用 `git hash-object` 在 Task 0 记录、Task 4 复核哈希一致。"
|
||||
- "**OI-1 (A) 已落地并保留**:`getIndexInBlock` 用 `block.dataX` / `block.dataY` 计算块内偏移(规划日实测 `const dx = x - block.dataX;`)。flush 与组装都复用该方法,因此跨分块地图的定位与置零一致。"
|
||||
- "**已知类型基线(用户裁定,不在修复范围)**:`vue-tsc --noEmit` 对 `packages-user/client-base/src/map/renderer.ts` 报 `error TS2339: Property 'getLayerByAlias' does not exist on type 'IGameMap'.`(`MapRenderer.getLayer` 调用了 `IGameMap` 上不存在的方法)。本计划**不修复**、不改数据端接口;类型闸门按「文件 = `renderer.ts`、代码 = `TS2339`、消息 = `Property 'getLayerByAlias' does not exist on type 'IGameMap'.`」精确豁免**恰好一条**并打印原文;任何其它 `map/` 诊断(含重复的同消息条目)一律失败。豁免按「去掉 `(行,列)`」归一化(行号会因插入代码而移动),**不是**「忽略 `renderer.ts` 的全部错误」。"
|
||||
- "**测试文件的源码读取方式**:沿用 09-25 偏差 1 的结论,用 Vite 的 `*?raw` 导入(`../vertex.ts?raw` 等)读取源码文本,不用 `node:fs` / `process`(仓库 TS program 未包含 Node 类型,改配置被禁)。"
|
||||
- "**测试收集**:根 `vite.config.ts` 的 `test` 段只有超时配置、没有 `include`,vitest 默认收集 `**/*.test.ts`;`map/__test__/` 下删除 `overlay.test.ts` 后只剩 `rendererGate.test.ts`,仍会被自动收集。测试在仓库根运行。"
|
||||
- "**动态成员来源**:`IMapRenderer.getMovingBlock(): Set<Readonly<IMovingBlock>>`,元素含只读 `index` / `x` / `y` / `layer` / `tile` / `texture`,排序时现取即可;因此**不需要** `getMovingBlockByIndex`,也**不再保留** 09-25 的「组装前逐条 update」逻辑。"
|
||||
- "**`getIndexInBlock` 找不到实例时丢弃 pending**:若目标图层不在当前渲染器、或坐标越界,flush 无法写入该 alpha,则丢弃这条 pending(不报错、不缓存)。这是既有查找契约的自然结果,作为已知限制记录在 SUMMARY,不在本计划扩展错误处理。"
|
||||
- "**地图重建丢失静态 alpha(既有行为,非本计划引入)**:`checkRebuild` / `setBlockSize` 重新 `mallocVertexArray` + `splitBlock` 后 alpha 复位为 1;本计划只清空 `overlayStatic` 与 `pendingAlpha` 并标记两个脏标记,不改变该语义。"
|
||||
- "**同深度重叠的既定限制**:同一图层的同行不透明图块与覆盖层图块 z 相同,`depthFunc(LESS)` 的严格比较让先绘制的不透明图块胜出;这是深度式排序的固有边界,本计划不引入浮点深度编码去消解它,只在 SUMMARY 记录。"
|
||||
- "**`addPostEffect` 当前无调用方**:后处理链路只能以源码闸门验证;本计划不新增调用方、不改入口。"
|
||||
- "**无 RESEARCH / 无 VALIDATION**:本 run 不存在 `09-26-RESEARCH.md` 与 `09-26-VALIDATION.md`,Nyquist 验证不适用(本阶段无 requirement ID);不创建这两类文件。spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。"
|
||||
- "**波次与独立性(wave 21 / `depends_on: []`)**:本计划按 D-01 追加在 09-01..09-25 之后,沿用波次 21;它改动的文件没有任何其它计划触碰,`depends_on: []` 表示结构独立。"
|
||||
prohibitions:
|
||||
- "不得在用户恰好回复「可以执行」之前执行任何任务或派发执行子代理(D-03)。"
|
||||
- "**不得运行任何 git 写命令**:`git add` / `commit` / `stash` / `checkout` / `restore` / `worktree` / `reset` 一律禁止;代码改动留在工作树,只有非代码产物(本计划、`09-26-BASELINE.json` 与 SUMMARY)由编排器另行处理。"
|
||||
- "**不得伪造范围证据**:`09-26-BASELINE.json` 只允许在 Task 0(汇报关卡)写一次,Task 1 之后禁止重写 / 覆盖 / 手工调整;Task 4 必须用「当前 `git hash-object` 与基线值逐一相等」的真值比较判定,不得退化成「哈希字符串出现在 SUMMARY 里」或「文件存在」这类弱检查。"
|
||||
- "不得保留 `overlay.ts` 与 `overlay.test.ts`;不得再从 `vertex.ts` 导入 `./overlay`;不得新增导出类型、导出类或导出常量(覆盖层的阈值常量与判定函数只做文件内私有)。"
|
||||
- "不得让 `setStaticAlpha` 做任何实例查找、rect 改写、成员增删或排序;不得在 `setStaticAlpha` 内直接写 `[start + 9]`。"
|
||||
- "不得使用字符串作为键(禁止 `static:${...}` / `dynamic:${...}` 之类的模板键);不得为动态条目建立任何登记表或缓存副本。"
|
||||
- "不得重排 / 压缩分块张量 `[chunk][layer][row][col]`;不得对整个静态实例数组排序;不得用 `drawElementsInstanced` + 排序索引代替实例顺序;不得重排动态数组或改变 `MovingBlock.index` / `movingIndexMap` / 扩缩容语义(`moving.ts` 零改动)。"
|
||||
- "不得把绘制顺序编码进 z / 深度(半精度、浮点深度编码)、不得引入 OIT 或深度剥离;`x` 只允许作为 CPU 侧排序键。"
|
||||
- "不得修改任何既有注释或 jsDoc(AGENTS.md);只允许新增,新增注释遵循 dev.md(单行 `//`、`//` 后留空格、不解释下一行代码、说明「为什么」)。"
|
||||
- "不得修复已知类型基线 `renderer.ts` 的 `TS2339 getLayerByAlias`;不得改 `MapRenderer.getLayer`、`IGameMap`、`GameMap`;不得把豁免扩大成「忽略 `renderer.ts` 的全部诊断」。"
|
||||
- "不得改 `status.ts` / `moving.ts` / `viewport.ts` / `block.ts` / `element.ts` / `constant.ts` / `map/index.ts` / `extension/**` / `shader/*.vert` / `map.frag`;不得改 09-01..09-25 的计划与 SUMMARY、`09-CONTEXT.md`、其它包、`package.json` / lockfile / `vite.config.ts` / `tsconfig.json` / eslint 配置。"
|
||||
- "不得新增依赖、不得执行安装命令;不得跑 `build:game` / `build:packages` / `build:lib`,不做全仓类型修复。"
|
||||
- "不得伪造验证:无 WebGL E2E 就不得写成「已自动化验证」;浏览器 UAT 未做必须如实记录;不得删减用例、放宽断言或把未覆盖的形态写成已覆盖;不得把「源码形状通过」写成「渲染结果已验证」。"
|
||||
- "不得越界处理 OI-2 之外的疑似问题;发现新问题只记录、不擅自修复、不擅自登记为缺陷。"
|
||||
open_items:
|
||||
- "**OI-1(命名待批准,未获批前不得落码;本清单是本计划新增标识符的完整清单,含字段与备选名)——状态字段:** ① `pendingAlpha`(备选 `alphaPending` / `pendingStaticAlpha`;类型 `Map<IMapLayer, Map<number, number>>`);② `overlayStatic`(备选 `staticOverlay` / `overlayStaticMap`;类型 `Map<IMapLayer, Map<number, IOverlayStaticRecord>>`);③ `overlayOrderDirty`(备选 `overlayNeedSort`);④ `overlayValueDirty`(备选 `overlayNeedAssemble`);⑤ `overlayInstancedArray`(备选 `overlayStage`;**沿用 09-25 已批准名称**);⑥ `overlayCount`(备选 `overlayLength`;**沿用 09-25 已批准名称**);⑦ `overlayOrdered`(备选 `overlayOrderCache` / `sortedOverlay`;排序缓存数组,类型为下述私有条目接口的数组)。"
|
||||
- "**OI-1(续)——类型与方法:** ⑧ 私有接口 `IOverlayStaticRecord`(备选 `IStaticOverlayRecord`),字段 `layer`(`mapLayer`)/ `mapIndex`(`dataIndex`)/ `rectX`(`tileX`)/ `rectY`(`tileY`)/ `rectWidth`(`tileWidth`)/ `rectHeight`(`tileHeight`);⑨ 私有接口 `IOverlayDrawEntry`(备选 `IOverlayOrderEntry` / `IOverlayEntry`),字段 `kind`(`type`)/ `layer`(`mapLayer`)/ `layerIndex`(`depthIndex`)/ `x`(`mapX`)/ `y`(`mapY`)/ `mapIndex`(`dataIndex`,仅静态条目使用)/ `moving`(`block`,仅动态条目使用,类型 `Readonly<IMovingBlock>`);⑩ 私有枚举 `OverlayEntryKind`(不导出;成员 `Static` / `Dynamic`,备选 `OverlayKind` / 成员 `Tile` / `Moving`);⑪ 文件内私有常量 `OVERLAY_ALPHA_THRESHOLD`(**沿用 09-25 已批准名称**,由导出改为不导出)与函数 `isOverlayAlpha`(**沿用已批准名称**);⑫ `flushPendingAlpha()`(备选 `applyPendingAlpha()` / `commitPendingAlpha()`);⑬ `sortOverlay()`(备选 `buildOverlayOrder()`);⑭ `assembleOverlay()`(**沿用 09-25 已批准名称**,备选 `buildOverlay()` / `stageOverlay()`);⑮ 记录读写辅助方法 `getOverlayStaticRecord(layer, mapIndex)`(备选 `findOverlayStaticRecord`)与 `deleteOverlayStaticRecord(layer, mapIndex)`(备选 `dropOverlayStaticRecord`)——若实现中确无必要可不新增,则本项作废。**本计划删除** 09-25 的 `getStaticOverlayKey()` / `getDynamicOverlayKey()` 与整个 `overlay.ts` 导出面(`MapOverlayOrder` / `IMapOverlayOrder` / `IOverlayEntry` / 导出的 `OverlayEntryKind` / 导出的 `OVERLAY_ALPHA_THRESHOLD` / 导出的 `isOverlayAlpha`);`types.ts` 不新增任何名称。"
|
||||
- "**OI-2(已知限制,只记录、不修复):** ①同图层同行的不透明与覆盖层图块 z 相同,先绘制者胜出(深度式排序固有边界);②地图重建会把静态 alpha 复位为 1;③`getIndexInBlock` 找不到实例时该条 pending 被丢弃;④动态块移动期间每帧都会因排序键变化而重排覆盖层(正确性优先,未做增量排序优化);⑤`addPostEffect` 无调用方,后处理只有源码闸门覆盖;⑥游戏尚不可运行,浏览器 UAT 全程跳过,**无 WebGL E2E**。"
|
||||
- "**OI-3(只提问、未确认为问题):** 覆盖层组装时为每条静态记录都做一次 `getBlockByDataLoc` + `getLayerInstanced` 查找;当地图上有大量半透明图块时,这是每帧一次的 O(n) 查找成本。本计划**不优化**(未确认是问题),仅在 SUMMARY 记录该成本形态,由用户决定是否需要后续缓存。"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Phase 9 · 第 26 个计划(新增的独立增量,**不是** 09-01..09-25 的 UI / 渲染任务)—— **把 09-25 引入的半透明覆盖层实现整体收敛进 `vertex.ts`,并把 `setStaticAlpha` 改为完全惰性、把实际工作推迟到取数组时的 flush**。
|
||||
|
||||
**要解决的问题:** 09-25 把覆盖层的排序与成员状态机拆成了独立文件 `overlay.ts`(`MapOverlayOrder` 类 + 字符串身份键),而实际需求只是「静态半透明图块与动态图块单独一遍绘制」。这份独立抽象带来三个代价:①多一处导出面与一份要同步维护的副本;②字符串键(`static:${layerIndex}:${mapX}:${mapY}` / `dynamic:${index}`)逐条构造并在日志 / 调试里不可读;③`setStaticAlpha` 是「急」的——它在调用方线程里就做实例查找、rect 置零 / 恢复、成员增删,而这些工作完全可以在渲染线程取数组时一次做完。此外 09-25 的 `rendererGate.test.ts` 断言的是旧形态(`getDynamic()` / `assembleOverlay` 内逐条 `update`),与目标设计不再一致。
|
||||
|
||||
**做法(锁定设计):**
|
||||
- **只留在 `vertex.ts`**:删除 `overlay.ts` 与 `overlay.test.ts`;覆盖层的状态、私有接口、私有序枚举、阈值常量与判定函数、flush / 排序 / 组装全部写在 `MapVertexGenerator` 与其文件内,**不新增任何导出类型或类**。
|
||||
- **完全惰性的 `setStaticAlpha`**:只把 `(layer, mapIndex) → alpha` 记进 `pendingAlpha`、标记值脏、`renderer.requestUpdate()`;不做实例查找、不动 rect、不增删成员、不排序。`getStaticAlpha` 在读实例数组前优先返回 pending 值,保持 `IBlockStatus.getAlpha()` 的即时语义。
|
||||
- **flush 在取数组时**:`getVertexArray()` 在 `checkRebuild()` 之后调用 `flushPendingAlpha()`(不透明遍上传前的唯一 flush 点),由它写实例 alpha、跨阈值时保存 / 置零 rect、增删 `overlayStatic` 记录、`markRenderDirty()`;`getOverlayArray()` 先 flush、再按需 `sortOverlay()`、再按需 `assembleOverlay()`;`isOverlayDirty()` = 有 pending ∨ 顺序脏 ∨ 值脏;`getOverlayCount()` 的「需先取数组」契约写进 jsDoc 并被 renderer 遵守。**`sortOverlay()` 结束时必须同时置 `overlayValueDirty = true`、`deleteMoving()` 必须同时置两个脏标记**,保证「顺序 / 成员变化 → 必然重新组装」,否则已删除的动态块会以旧暂存数组与旧数量留在屏幕上。
|
||||
- **数值 / 对象键,无字符串**:`Map<IMapLayer, Map<number, number>>` 与 `Map<IMapLayer, Map<number, IOverlayStaticRecord>>`;动态条目**不留登记表**,每次排序从 `renderer.getMovingBlock()` 现取,因此不会残留已销毁的块或旧的 `layerIndex`。
|
||||
- **排序与组装**:`(layerIndex, y, x, kind)` 升序,静态先于动态;`x` 只作 CPU 侧同键稳定次级键,绝不进 z / 深度;组装把静态条目从分块图层数组拷贝 16 float 并用记录覆盖前四个 float,动态条目从 `dynamicInstancedArray` 按 `index * INSTANCED_COUNT` 拷贝。
|
||||
- **保留 09-25 的正确部分**:两遍式与状态收尾、动态尾部裁剪、`blendFuncSeparate`、ping/pong 颜色+`DEPTH_COMPONENT24` 深度配对、0/1/2+ 效果链路、`setCanvasSize` / `destroy`、`map.frag` 的 `0.004`、OI-1 (A) 的 `dataX` / `dataY`;并补齐解释性 WebGL 注释(为什么关深度写入、为什么覆盖层最后画、为什么分离式混合、FBO / 深度 / ping-pong 如何配对)。
|
||||
- **验证**:源码形状闸门(vitest `?raw`)+ `eslint` + 按文件过滤的 `vue-tsc --noEmit`(精确豁免一条已知基线并打印)+ 受保护文件哈希范围闸门;**不做浏览器 UAT、不宣称 WebGL E2E、不提交任何代码**。
|
||||
|
||||
**约束:** 只动 `files_modified` 的 8 个路径(6 个代码路径,其中 2 个删除;另加 `09-26-BASELINE.json` 与 `09-26-SUMMARY.md`);不改其它文件;不改既有注释与 jsDoc;不新增依赖;不改配置;**所有新增命名待批准(OI-1)**。
|
||||
|
||||
**修改方式声明(AGENTS.md):** 本计划的全部文件写入(改 3 个源码、重写 1 个测试、删除 2 个文件、写 1 个 `09-26-BASELINE.json`、写 1 个 SUMMARY)由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入;代码一律不提交。
|
||||
|
||||
**可见跳过:** spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。本阶段无 requirement ID,不创建 RESEARCH.md / VALIDATION.md,Nyquist 验证不适用。
|
||||
</objective>
|
||||
|
||||
## 本计划产出物(Artifacts this phase produces)
|
||||
|
||||
| 类型 | 名称 / 路径 | 说明 |
|
||||
|------|-------------|------|
|
||||
| 删除 | `packages-user/client-base/src/map/overlay.ts` | 09-25 的独立排序 / 成员状态机,能力并入 `vertex.ts`(未跟踪文件,删除后 `git status` 不会出现 `D` 行) |
|
||||
| 删除 | `packages-user/client-base/src/map/__test__/overlay.test.ts` | 被测类已不存在(同上,未跟踪,删除后无 `D` 行) |
|
||||
| 新符号(私有) | `pendingAlpha`、`overlayStatic`、`overlayOrderDirty`、`overlayValueDirty`、`overlayOrdered` | 均在 `MapVertexGenerator`,命名待批准(OI-1) |
|
||||
| 新符号(沿用) | `overlayInstancedArray`、`overlayCount`、`assembleOverlay()`、`OVERLAY_ALPHA_THRESHOLD`、`isOverlayAlpha()` | 沿用 09-25 已批准名称;阈值常量与判定函数改为文件内私有 |
|
||||
| 新符号(私有类型) | `IOverlayStaticRecord`、`IOverlayDrawEntry`、`OverlayEntryKind`(`Static` / `Dynamic`) | 不导出,命名待批准(OI-1) |
|
||||
| 新符号(私有方法) | `flushPendingAlpha()`、`sortOverlay()`、`getOverlayStaticRecord()`、`deleteOverlayStaticRecord()` | 命名待批准(OI-1);后两个若确无必要可不新增 |
|
||||
| 修改 | `vertex.ts`(惰性 setter / flush / 排序 / 组装 / 成员刷新)、`renderer.ts`(按新契约取覆盖层 + 注释)、`types.ts`(`getOverlayCount` 契约说明) | 不新增导出面 |
|
||||
| 重写 | `map/__test__/rendererGate.test.ts` | 断言新形态(无 overlay 导入、无字符串键、惰性 setter、flush 点、排序键、动态现取…) |
|
||||
| 交付 | `.planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md` | 执行摘要、门禁结果、UAT 跳过声明 |
|
||||
| 基线 | `.planning/phases/09-minimum-browser-runnable/09-26-BASELINE.json` | Task 0 写出的 HEAD + 7 个受保护文件哈希,供 Task 4 做真值相等比较 |
|
||||
|
||||
## 覆盖审计(Multi-Source Coverage Audit)
|
||||
|
||||
| 源 | 条目 | 覆盖位置 |
|
||||
|----|------|----------|
|
||||
| GOAL(ROADMAP §Phase 9) | 「可在浏览器中运行并验证的最小版本」伞目标 | **本增量不新增覆盖**:游戏当前不可运行,浏览器可运行性仍未被验证;本计划只收敛渲染实现,SUMMARY 如实记录(用户决策 9) |
|
||||
| REQ | 本阶段无 requirement ID(`requirements: []`) | Nyquist 不适用 |
|
||||
| RESEARCH | 本 run 无 RESEARCH.md | 不适用 |
|
||||
| CONTEXT D-01 | 增量派发,一个任务一个计划 | 本计划即该派发的唯一计划,不拆分、不预规划后续 |
|
||||
| CONTEXT D-02 | 计划可累积追加 | plan 26,排在 09-01..09-25 之后 |
|
||||
| CONTEXT D-03 | 执行前必须先汇报并等待「可以执行」 | Task 0(`blocking-human`),`autonomous: false` |
|
||||
| CONTEXT D-04 | 新命名必须先反馈获批 | Task 0 逐条报备 OI-1;未获批前不落码 |
|
||||
| CONTEXT D-05 | 默认不逐阶段验证 | 本计划仍做自动化门禁,但**明确不做浏览器 UAT**;不宣称自动化验证渲染结果 |
|
||||
| CONTEXT D-06 | 范围由用户界定,不擅自扩大 | `files_modified` 8 项(6 代码 + 基线 + 摘要)+ 哈希范围闸门 |
|
||||
| 用户决策 1 | 删除 `overlay.ts` / `overlay.test.ts`,状态与逻辑全在 `vertex.ts`,不新增导出类 | Task 1(含删除)+ Task 3(闸门断言无 overlay 导入)+ Task 4(文件不存在断言) |
|
||||
| 用户决策 2 | `setStaticAlpha` 完全惰性 | Task 1 + Task 3(方法体形状断言) |
|
||||
| 用户决策 3 | flush 在 `getVertexArray()`;`getOverlayArray()` 排序 / 组装;`isOverlayDirty()` 覆盖三类脏;renderer 保持「脏则取数组→上传→绘制」 | Task 1 + Task 2 + Task 3 |
|
||||
| 用户决策 4 | 无字符串键;三层数值 / 对象键;动态成员取自 `getMovingBlock()` | Task 1 + Task 3 |
|
||||
| 用户决策 5 | 不新增导出类型;私有接口留在 `vertex.ts`;`types.ts` 既有新增字段与方法保留 | Task 1 + Task 2 + Task 3 |
|
||||
| 用户决策 6 | 保留两遍式、动态尾部裁剪、`blendFuncSeparate`、`DEPTH_COMPONENT24` 配对、0/1/2+、缩放 / 销毁、`0.004`;细化并注释 | Task 2 + Task 3(保留性断言) |
|
||||
| 用户决策 7 | 保留 OI-1 (A) 与其闸门 | Task 1(沿用 `getIndexInBlock`)+ Task 3 |
|
||||
| 用户决策 8 | 已知类型基线不修,过滤闸门精确豁免一条并打印 | Task 4 |
|
||||
| 用户决策 9 | 不做浏览器 UAT,不宣称 WebGL E2E | Task 0 报备 + Task 4 SUMMARY |
|
||||
| 用户决策 10 | 不提交代码 | Task 0 报备 + Task 4 终检(禁止 git 写命令) |
|
||||
| 用户决策 11 | 大量解释性注释 + 新方法 jsDoc;既有注释不改 | Task 1 / Task 2(注释)+ Task 3(注释 / jsDoc 断言) |
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-PATTERNS.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-25-PLAN.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-25-SUMMARY.md
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
@packages-user/client-base/src/map/renderer.ts
|
||||
@packages-user/client-base/src/map/vertex.ts
|
||||
@packages-user/client-base/src/map/types.ts
|
||||
@packages-user/client-base/src/map/status.ts
|
||||
@packages-user/client-base/src/map/moving.ts
|
||||
@packages-user/client-base/src/map/viewport.ts
|
||||
@packages-user/client-base/src/map/block.ts
|
||||
@packages-user/client-base/src/map/constant.ts
|
||||
@packages-user/client-base/src/map/shader/map.frag
|
||||
@packages-user/client-base/src/map/overlay.ts
|
||||
@packages-user/client-base/src/map/__test__/overlay.test.ts
|
||||
@packages-user/client-base/src/map/__test__/rendererGate.test.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报覆盖层收敛进 vertex.ts、惰性 setStaticAlpha、flush 契约、待批准命名与保留项,等待「可以执行」</name>
|
||||
<precondition>工作树含 09-25 的未提交实现:`vertex.ts` 导入 `./overlay` 且含 `MapOverlayOrder` / `getStaticOverlayKey(` / `getDynamicOverlayKey(`(字符串键);`overlay.ts` 与 `__test__/overlay.test.ts` 存在;`renderer.ts` 含 `gl.depthMask(true)` / `gl.depthMask(false)` / `blendFuncSeparate(` / `DEPTH_COMPONENT24` / `overlayBuffer`;`map.frag` 为 `alpha < 0.004`;`types.ts` 含 `overlayBuffer:` / `pingDepthBuffer:` / `pongDepthBuffer:` / `isOverlayDirty():` / `getOverlayArray():` / `getOverlayCount():`;`status.ts` 的 `setAlpha` 已是 `(this.layer, this.x, this.y, alpha)`。**已知类型基线(用户已裁定,不在本计划修复范围)**:`vue-tsc --noEmit` 对 `renderer.ts` 报 `error TS2339: Property 'getLayerByAlias' does not exist on type 'IGameMap'.`,本计划所有类型闸门精确豁免该条一条。</precondition>
|
||||
<files>.planning/phases/09-minimum-browser-runnable/09-26-BASELINE.json(唯一写入:基线记录;其余仅只读命令)</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-25-PLAN.md(被本计划部分取代的设计:两遍式、覆盖层、后处理、门禁纪律)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-25-SUMMARY.md(09-25 实际执行结果、3 条偏差、`status.ts` 与 `SCOPE GATE` 的遗留)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-01 增量派发、D-03 汇报关卡、D-04 命名批准、D-05 不逐阶段验证)
|
||||
- packages-user/client-base/src/map/vertex.ts(现有覆盖层实现:`overlay` 字段、`overlayStatic` 字符串键表、`assembleOverlay`、`setStaticAlpha`、`getIndexInBlock`、`getVertexArray`)
|
||||
- packages-user/client-base/src/map/overlay.ts(将被删除的独立状态机)
|
||||
- packages-user/client-base/src/map/renderer.ts(`render()` 的两遍式、覆盖层上传 / 绘制、后处理链、`needUpdate()`)
|
||||
- packages-user/client-base/src/map/types.ts(`IContextData` 新增字段、`IMapVertexGenerator` 新增方法)
|
||||
- packages-user/client-base/src/map/overlay.ts 的对端测试 `__test__/overlay.test.ts` 与 `__test__/rendererGate.test.ts`(将被删除 / 重写)
|
||||
- packages-user/client-base/src/map/status.ts(只读:用户的参数顺序修正)
|
||||
- packages-user/client-base/src/map/moving.ts(`MovingBlock.index` 身份、`stepMoving`、`x` / `y` 只读读取方式)
|
||||
- packages-user/client-base/src/map/viewport.ts(`checkDynamic` 把动态尾部并入 `render`)
|
||||
- AGENTS.md(执行前须获批、命名先反馈、既有注释不得修改、疑似问题须先确认)、dev.md(无副作用、类先接口后 implements、不用字符串作键、注释与 jsDoc 规范、CRLF)
|
||||
</read_first>
|
||||
<what-built>
|
||||
一份待批准的执行计划(`09-26-PLAN.md`):把 09-25 的覆盖层实现整体收敛进 `vertex.ts`、把 `setStaticAlpha` 改为完全惰性、把 flush 挪到取数组时,并保留两遍式渲染与后处理修正。
|
||||
</what-built>
|
||||
<action>
|
||||
**停下并向用户汇报,等待回复「可以执行」后才继续;本任务不改任何源码 / 测试 / 计划文件,只写一份基线记录 `09-26-BASELINE.json`(⑬),不派发执行子代理。**
|
||||
|
||||
① **要解决的问题:** 09-25 把覆盖层的「排序 + 成员状态机」拆成了一个独立文件 `overlay.ts`(`MapOverlayOrder` 类、接口、枚举、导出常量与函数),并用字符串身份键(`static:${layerIndex}:${mapX}:${mapY}` / `dynamic:${index}`)。这份抽象与实际需求(静态半透明图块与动态图块单独一遍绘制)不匹配,代价是:多一份导出面与副本、字符串键逐条拼接、以及 `setStaticAlpha` 是「急」的——它在调用方线程里就查找实例、置零 / 恢复 rect、增删成员。同时 09-25 的闸门测试断言的是旧形态,与目标设计已经脱节。
|
||||
|
||||
② **目标设计(覆盖层只存在于 `vertex.ts`):**
|
||||
- 删除 `overlay.ts` 与 `__test__/overlay.test.ts`;不新增导出类型、不新增类;私有接口 / 私有序枚举 / 阈值常量与判定函数都只在本文件内可见。
|
||||
- 状态:`pendingAlpha: Map<IMapLayer, Map<number, number>>`(图层对象 + 数值地图索引)、`overlayStatic: Map<IMapLayer, Map<number, IOverlayStaticRecord>>`(记录原始 rect 与地图索引)、`overlayOrderDirty` / `overlayValueDirty`、`overlayInstancedArray` / `overlayCount` / 排序缓存数组。
|
||||
- 动态成员**不留登记表**:每次排序时从 `renderer.getMovingBlock()` 现取 `index` / `x` / `y` / `layer`,因此已销毁的块不会残留、图层 / `zIndex` 变化不会用旧 `layerIndex`。
|
||||
|
||||
③ **完全惰性的 `setStaticAlpha`:** 只把 alpha 记进 `pendingAlpha`、标记值脏、`renderer.requestUpdate()`。**不查找实例、不动 rect、不增删成员、不排序。** `getStaticAlpha` 在读实例数组前优先返回 pending 里的值,保证 `IBlockStatus.getAlpha()` 的即时语义不被打破。
|
||||
|
||||
④ **flush 契约:** `getVertexArray()` 在 `checkRebuild()` 之后调用 `flushPendingAlpha()`(不透明遍上传分块数据之前唯一的 flush 点);flush 内用 `getIndexInBlock`(OI-1 (A) 的 `dataX` / `dataY`)定位实例,写 alpha;alpha `< 1` 且未登记时保存 rect、把实例 `x/y/w/h` 置 0、登记记录、标记顺序脏、`markRenderDirty()`;alpha 回到 1 且已登记时写回 rect、注销记录、标记顺序脏、`markRenderDirty()`;已登记且仍半透明时只更新值。`getOverlayArray()` 先 flush、再按需 `sortOverlay()`、再按需 `assembleOverlay()`。`isOverlayDirty()` = 有 pending ∨ 顺序脏 ∨ 值脏。`getOverlayCount()` 返回已组装数量,jsDoc 写明「脏时需先取数组」,renderer 按此在脏时先取数组再取数量。
|
||||
|
||||
**④′ 顺序 → 组装不变式(本次修订新增的硬性要求):** `sortOverlay()` 在清 `overlayOrderDirty` 的同时**必须置 `overlayValueDirty = true`**——排序结果变了,暂存数组与 `overlayCount` 一定过期;只有这样才能保证「排序之后必然组装」。`deleteMoving()` 必须**同时**置 `overlayOrderDirty` 与 `overlayValueDirty`(只置顺序脏时,排序会把顺序脏清掉而跳过组装,已删除的动态块会以旧暂存数组 / 旧数量继续上屏,形成残影)。`assembleOverlay()` 成功写完 `overlayCount` 后清掉两个脏标记。这两条都由 Task 1 的源码闸门与 Task 3 的用例断言。
|
||||
|
||||
⑤ **排序与组装:** 排序键 `(layerIndex, y, x, kind)` 升序,`kind` 保证静态先于动态;`x` 只作同一行的 CPU 侧稳定次级键,绝不进 z / 深度;静态条目的 `x` / `y` 由地图索引与 `mapWidth` 推出,`layerIndex` 由 `renderer.getLayerIndex(layer)` 现取。组装时静态条目从分块图层数组拷贝 16 float 并用记录覆盖前四个 float,动态条目从 `dynamicInstancedArray` 按 `index * INSTANCED_COUNT` 拷贝;暂存数组按需倍增扩容,`overlayCount` 等于实际写入条数。
|
||||
|
||||
⑥ **成员刷新与重建:** `updateTileVertex` 的 Position 路径若命中已登记记录,刷新记录 rect 并再次把实例 rect 置 0(避免同一图块被不透明遍与覆盖层各画一遍);`updateVertexArray` 的空图块路径命中记录时把记录 rect 置 0;`checkRebuild` / `setBlockSize` 清空 `overlayStatic` 与 `pendingAlpha` 并标记两个脏标记;`updateMoving` 置两个脏标记;`deleteMoving` **必须置两个脏标记**(见 ④′)。
|
||||
|
||||
⑦ **保留并细化 09-25 的正确部分:** 两遍式(不透明遍 `depthMask(true)` 且按 `dynamicStart` 裁掉动态尾部;覆盖层最后画,`depthMask(false)`、深度测试仍开;收尾恢复 `depthMask(true)`)、`blendFuncSeparate`、ping / pong 各自配对颜色纹理并挂 `DEPTH_COMPONENT24` 深度 renderbuffer、0 / 1 / 2+ 效果链路、`setCanvasSize` 重设两个深度缓冲、`destroy` 删除新增对象、`map.frag` 的 `0.004`、OI-1 (A) 的 `dataX` / `dataY`。**新增解释性注释**:为什么关深度写入、为什么覆盖层最后画、为什么分离式混合(alpha 通道累积)、FBO 的颜色附件 / 深度附件 / ping-pong 配对如何工作、为什么要裁掉动态尾部。
|
||||
|
||||
⑧ **验证方式(全部命令都在本机 PowerShell 5.1 里实测过「能跑出真判决」):** ①源码形状闸门(vitest `?raw` 读取源码,不导入 WebGL 模块);②`cmd /c "pnpm exec eslint <4 个文件>"`;③按 `map/` 前缀过滤的 `node -e` 类型闸门(内部 `spawnSync('cmd',['/c','pnpm exec vue-tsc --noEmit'])`),**精确豁免**用户裁定的 `renderer.ts` `TS2339 getLayerByAlias` 一条并打印原文,其它任何 `map/` 诊断失败;④范围闸门(`node -e`):`git status --porcelain -uall -- packages-user/client-base/src/map` 的变更集必须**恰好**等于 ` M renderer.ts` / ` M shader/map.frag` / ` M status.ts` / ` M types.ts` / ` M vertex.ts` / `?? __test__/rendererGate.test.ts`,且 `overlay.ts` / `__test__/overlay.test.ts` **不存在**(未跟踪文件删除后没有 `D` 行,只能这样断言);⑤哈希闸门(`node -e`):把当前 `git hash-object` 与 `09-26-BASELINE.json` 里 Task 0 写下的 7 个值逐一**真值比较**,并核对 HEAD 未变。**不做浏览器 UAT、不宣称 WebGL E2E**;分块与动态的视觉结果未验证。
|
||||
|
||||
⑨ **范围:** 只写 8 个路径——`vertex.ts`、`renderer.ts`、`types.ts`、`__test__/rendererGate.test.ts`、删除 `overlay.ts` 与 `__test__/overlay.test.ts`、新增 `09-26-BASELINE.json` 与 `09-26-SUMMARY.md`。`status.ts`(用户维护)、`moving.ts` / `viewport.ts` / `block.ts` / `element.ts` / `constant.ts` / `map.frag` / 其它包 / 依赖与配置 / 09-01..09-25 计划一律零改动。09-25 的未提交代码被本计划**合并 / 替换**,不提交、不回滚。注意 `status.ts` 与 `shader/map.frag` 在 `git status` 里本来就是 ` M`(`status.ts` 是用户的参数顺序修改、`map.frag` 是 09-25 的阈值修改),它们是**允许存在的状态**,是否被本计划改动由哈希闸门判定。
|
||||
|
||||
⑩ **待批准命名(OI-1,全部只以提议形式出现,未获批前不得落码;每项含备选):**
|
||||
- 状态字段:`pendingAlpha`(`alphaPending` / `pendingStaticAlpha`)、`overlayStatic`(`staticOverlay` / `overlayStaticMap`)、`overlayOrderDirty`(`overlayNeedSort`)、`overlayValueDirty`(`overlayNeedAssemble`)、`overlayOrdered`(`overlayOrderCache` / `sortedOverlay`)。
|
||||
- 沿用 09-25 已批准、本次不改名:`overlayInstancedArray`、`overlayCount`、`assembleOverlay()`、`OVERLAY_ALPHA_THRESHOLD`、`isOverlayAlpha()`(后两者由导出改为文件内私有)。
|
||||
- 私有类型:`IOverlayStaticRecord`(`IStaticOverlayRecord`),字段 `layer`(`mapLayer`)/ `mapIndex`(`dataIndex`)/ `rectX`(`tileX`)/ `rectY`(`tileY`)/ `rectWidth`(`tileWidth`)/ `rectHeight`(`tileHeight`);`IOverlayDrawEntry`(`IOverlayOrderEntry` / `IOverlayEntry`),字段 `kind`(`type`)/ `layer`(`mapLayer`)/ `layerIndex`(`depthIndex`)/ `x`(`mapX`)/ `y`(`mapY`)/ `mapIndex`(`dataIndex`,静态专用)/ `moving`(`block`,动态专用);私有序枚举 `OverlayEntryKind`(`OverlayKind`),成员 `Static`(`Tile`)/ `Dynamic`(`Moving`)。
|
||||
- 私有方法:`flushPendingAlpha()`(`applyPendingAlpha()` / `commitPendingAlpha()`)、`sortOverlay()`(`buildOverlayOrder()`)、`getOverlayStaticRecord()`(`findOverlayStaticRecord`)、`deleteOverlayStaticRecord()`(`dropOverlayStaticRecord`)——后两个若实现中确无必要可不新增。
|
||||
- 删除(不再使用):`getStaticOverlayKey()`、`getDynamicOverlayKey()`、`MapOverlayOrder`、`IMapOverlayOrder`、`IOverlayEntry` 及 `overlay.ts` 的整份导出面。
|
||||
|
||||
⑪ **已知限制与风险:** ①同图层同行的不透明与覆盖层图块 z 相同,先绘制者胜出(深度式排序固有边界,不引入浮点深度编码);②地图重建会把静态 alpha 复位为 1;③`getIndexInBlock` 找不到实例时该条 pending 被丢弃;④动态块移动期间每帧都会重排覆盖层(正确性优先,未做增量排序);⑤`addPostEffect` 无调用方,后处理只有源码闸门;⑥`status.ts` 由用户维护,本计划只读并用哈希证明未改;⑦**游戏尚不可运行,浏览器 UAT 全程跳过,无 WebGL E2E**;⑧估算未校准(`confidence: low`)。
|
||||
|
||||
⑫ **两条铁律:** **不做浏览器 UAT、不宣称任何 WebGL E2E**;**不提交任何代码**(禁止 `git add` / `commit` / `stash` / `checkout` / `worktree`),改动全部留在工作树供用户审查。
|
||||
|
||||
⑬ **基线记录(本任务的唯一写入,写进汇报):** 先把 HEAD 与 7 个受保护文件的 `git hash-object` 落成机器可读基线 `.planning/phases/09-minimum-browser-runnable/09-26-BASELINE.json`(`{ "head": "<git rev-parse HEAD>", "hashes": { "status.ts": "...", "moving.ts": "...", "viewport.ts": "...", "block.ts": "...", "element.ts": "...", "constant.ts": "...", "shader/map.frag": "..." } }`),用下面这条**已在本机 PowerShell 5.1 实测可执行**的命令生成(只用 `git rev-parse` / `git hash-object` 两类只读命令 + 一次规划产物写入):
|
||||
|
||||
`node -e "const fs=require('fs'),cp=require('child_process');const dir='packages-user/client-base/src/map/';const files=['status.ts','moving.ts','viewport.ts','block.ts','element.ts','constant.ts','shader/map.frag'];const hashes={};for(const f of files){hashes[f]=((cp.spawnSync('git',['hash-object',dir+f],{encoding:'utf8'}).stdout)||'').trim()}const head=((cp.spawnSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).stdout)||'').trim();fs.writeFileSync('.planning/phases/09-minimum-browser-runnable/09-26-BASELINE.json',JSON.stringify({head:head,hashes:hashes},null,2));console.log('baseline: '+Object.keys(hashes).length+' hashes at '+head)"`
|
||||
|
||||
然后把 HEAD 与 7 个哈希原文写进汇报。另运行 `git status --porcelain -uall -- packages-user/client-base/src/map` 记录当前 `map/` 变更集(应为 5 个 ` M` + 3 个 `??`)。**该文件只在此时写一次,Task 1 之后禁止重写。**
|
||||
|
||||
⑭ **修改方式:** 获批后由 `gsd-executor` 子代理执行(非内联),只写 `files_modified` 的 8 个路径。
|
||||
</action>
|
||||
<how-to-verify>
|
||||
逐条核对 ①-⑭ 是否与你的意图一致,特别是:覆盖层是否只留在 `vertex.ts`、`setStaticAlpha` 是否允许「只记 pending」、flush 是否放在 `getVertexArray()`、是否接受「动态成员每次排序现取」、④′ 的「排序后必然重新组装」不变式、OI-1 的命名与备选、`09-26-BASELINE.json` 这一件新增基线产物、以及是否确认「不做 UAT / 不提交代码」。
|
||||
</how-to-verify>
|
||||
<resume-signal>仅当完全同意时回复恰好四个字「可以执行」;任何修改意见请直接说明,本计划会据此改写后再来一次汇报。</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: 把覆盖层整体收敛进 `vertex.ts`(删 `overlay.ts` / `overlay.test.ts`,惰性 setStaticAlpha,flush / sort / assemble,数值嵌套键)</name>
|
||||
<files>packages-user/client-base/src/map/vertex.ts, packages-user/client-base/src/map/overlay.ts(删除), packages-user/client-base/src/map/__test__/overlay.test.ts(删除)</files>
|
||||
<action>
|
||||
按 Task 0 获批的命名实现(未获批的名字不得落码)。全部改动只在 `MapVertexGenerator` 与其文件内,**不新增导出类型 / 类 / 常量**。
|
||||
|
||||
**A. 删除与导入清理。** 删除 `overlay.ts` 与 `__test__/overlay.test.ts`(`git rm` 属于 git 写命令,**禁止**;直接用文件删除能力)。注意两个文件都是**未跟踪**的(`git ls-files --error-unmatch` 已确认),因此删除后 `git status` **不会**出现 `D` 行,它们只是从 `??` 列表里消失——不要期待 `D`,也不要为了制造 `D` 行去 `git add`。从 `vertex.ts` 删除 `import { IMapOverlayOrder, isOverlayAlpha, MapOverlayOrder, OverlayEntryKind } from './overlay';`,把阈值常量与判定函数改成本文件内私有的 `const OVERLAY_ALPHA_THRESHOLD = 1;` 与非导出的 `function isOverlayAlpha(alpha: number): boolean`(放在文件顶部 `VertexUpdate` 附近,带 jsDoc)。**同时明确删除 `getStaticOverlayKey()` 与 `getDynamicOverlayKey()` 两个方法本体**(不是留着不用):它们是字符串键的唯一产地,留着会被 eslint 报 `no-unused-vars`,也与「无字符串键」的锁定设计直接冲突。删除 `IMapLayer` 之外不再需要的导入前先确认仍被使用(`IMapLayer` 仍用于多处签名,`Float32Array` 等无需导入)。
|
||||
|
||||
**B. 私有类型(文件内、不导出)。** 定义 `IOverlayStaticRecord`(`layer: IMapLayer`、`mapIndex: number`、`rectX/rectY/rectWidth/rectHeight: number`,后可写)、私有绘制条目接口 `IOverlayDrawEntry`(`kind: OverlayEntryKind`、`layer: IMapLayer`、`layerIndex: number`、`x: number`、`y: number`、`mapIndex: number`(静态用)、`moving: Readonly<IMovingBlock>`(动态用))、私有序枚举 `OverlayEntryKind { Static, Dynamic }`(成员顺序必须是 `Static` 在前,`kind` 用作排序末位键)。每个新字段都要有成员 jsDoc。
|
||||
|
||||
**C. 状态字段(替换 09-25 的 `overlay` / 字符串 `overlayStatic`)。** `pendingAlpha: Map<IMapLayer, Map<number, number>> = new Map()`、`overlayStatic: Map<IMapLayer, Map<number, IOverlayStaticRecord>> = new Map()`、`overlayOrderDirty: boolean = false`、`overlayValueDirty: boolean = false`、`overlayOrdered: IOverlayDrawEntry[] = []`、保留 `overlayInstancedArray: Float32Array = new Float32Array()` 与 `overlayCount: number = 0`。删除 `overlay: IMapOverlayOrder` 字段。字段注释说明「为什么用图层对象 + 数值索引作键」(不拼字符串、不因图层改名失效)。
|
||||
|
||||
**C′. 方法与 region 的顺序(dev.md「私有方法应放到调用之的方法之前,同时保证其处在合理的 region 内」)。** 覆盖层的 `flushPendingAlpha()` / `sortOverlay()` / `assembleOverlay()` 都要调用 `getIndexInBlock()`,而它当前位于类尾部的 `#region 图块状态`(`setStaticAlpha` 之前)。**选定的落点:把 `getIndexInBlock()` 从 `#region 图块状态` 整体搬到 `#region 分块操作` 的末尾(`checkRebuild()` 之后、`#region 覆盖层` 之前),方法体与 jsDoc 原样搬移、一字不改**;`#region 覆盖层` 保持当前位置(紧接 `#region 分块操作`),于是顺序为:`getIndexInBlock` → 记录读写辅助方法 → `flushPendingAlpha` / `sortOverlay` / `assembleOverlay` → `#region 顶点数组更新`(`updateTileVertex` / `updateVertexArray` 调用记录辅助方法)→ `#region 图块状态`(`setStaticAlpha` 等仍调用 `getIndexInBlock`)→ `#region 其他接口`(`getOverlayArray` 调用 flush / sort / assemble)。该顺序由 Task 1 与 Task 3 的源码闸门断言。
|
||||
|
||||
**D. 记录读写辅助方法(私有,放在 `#region 覆盖层`)。** `getOverlayStaticRecord(layer, mapIndex)` 返回嵌套 map 中的记录或 `null`;`deleteOverlayStaticRecord(layer, mapIndex)` 删除记录并在内层 map 空时一并删除内层 map(避免空壳累积)。二者都要 jsDoc。
|
||||
|
||||
**E. `flushPendingAlpha()`(私有)。** 遍历 `pendingAlpha` 的每一对 `(layer, alphaMap)`,对每对 `(mapIndex, alpha)`:由 `mapIndex % mapWidth` / `Math.floor(mapIndex / mapWidth)` 求坐标,调 `getIndexInBlock(layer, x, y)`;找不到实例则跳过(该条 pending 丢弃,见已知限制);写 `array[index * INSTANCED_COUNT + 9] = alpha`;然后按阈值处理成员:`isOverlayAlpha(alpha)` 为真且无记录 → 用当前 `array[start]..[start+3]` 建记录、把四个 float 置 0、登记记录、`overlayOrderDirty = true`、`block.data.markRenderDirty()`;为真且有记录 → 只在记录已存在时不改 rect(实例应已被置零;若 `updateTileVertex` 刷新过则由该路径保证再次置零),不重复排序;为假且有记录 → 用记录写回四个 float、注销记录、`overlayOrderDirty = true`、`block.data.markRenderDirty()`;为假且无记录 → 无需成员变化。全部处理完后 `pendingAlpha.clear()` 并置 `overlayValueDirty = true`。jsDoc 写明「这是不透明遍上传前唯一的落地点」与「为什么必须 `markRenderDirty`」。
|
||||
|
||||
**F. `sortOverlay()`(私有,顺序脏时调用)。** 重建 `overlayOrdered`:遍历 `overlayStatic` 的每层每 `mapIndex` 生成静态条目(`x` / `y` 由 `mapWidth` 推出,`layerIndex = renderer.getLayerIndex(layer)`);遍历 `renderer.getMovingBlock()` 生成动态条目(`x` / `y` / `layer` 直接取自活动块,`layerIndex = renderer.getLayerIndex(block.layer)`,`moving` 为块引用)。按 `(a.layerIndex - b.layerIndex) || (a.y - b.y) || (a.x - b.x) || (a.kind - b.kind)` 排序(`kind` 末位保证静态先于动态;`x` 只是 CPU 侧稳定次级键)。**结尾必须写 `overlayOrderDirty = false;` 紧跟 `overlayValueDirty = true;`**——成员 / 顺序变了,暂存数组与 `overlayCount` 一定过期,必须让 `getOverlayArray()` 在排序后无条件组装(否则只置顺序脏会让排序把两种脏一起清掉,跳过组装)。jsDoc 说明排序键、`x` 只作 CPU 侧键、以及「为什么每次全量重建 + 为什么排序后必须再组装」(动态成员无登记表,现取才不会有残影)。
|
||||
|
||||
**G. `assembleOverlay()`(私有,值脏或刚排序时调用)。** 按 `overlayOrdered` 顺序把 16 float/实例写进 `overlayInstancedArray`:暂存数组长度不足 `ordered.length * INSTANCED_COUNT` 时按 `Math.max(length * 2, required)` 重新分配;静态条目用 `getIndexInBlock(entry.layer, entry.x, entry.y)` 取图层数组与块内索引,拷贝 16 float 后用记录覆盖前四个 float(rect);动态条目从 `dynamicInstancedArray` 的 `entry.moving.index * INSTANCED_COUNT` 拷贝 16 float;查找失败的条目跳过(不计数)。用独立计数器写入 `overlayCount`,最后清 `overlayValueDirty` 与 `overlayOrderDirty`。jsDoc 说明「组装只拷贝 + 覆盖 rect,从不修改分块数组」。
|
||||
|
||||
**H. 惰性 setter。** `setStaticAlpha(layer, x, y, alpha)`:由 `y * mapWidth + x` 求 `mapIndex`,取(或建)`pendingAlpha` 的内层 map 并写入 alpha,置 `overlayValueDirty = true`,调 `this.renderer.requestUpdate()`。**方法体内不得出现 `getIndexInBlock`、`INSTANCED_COUNT`、`overlayStatic`、`rectX` / `rect` 赋值、`sort`。** `getStaticAlpha(layer, x, y)`:先查 `pendingAlpha` 命中则直接返回该值,否则维持原实现读实例数组。`setStaticFrame`:保留「写 `+12` 并 `markRenderDirty`」,把 `overlay.markValueDirty()` 换成 `overlayValueDirty = true`(不再做成员查找)。`setDynamicAlpha` / `setDynamicFrame`:保留写值 + `dynamicRenderDirty = true`,把 `overlay.markValueDirty()` 换成 `overlayValueDirty = true`。
|
||||
|
||||
**I. 成员刷新。** `updateTileVertex`(`index` 的类型是 `IndexedBlockMapPos`,**没有** `mapIndex` 字段,要用 `index.mapY * this.mapWidth + index.mapX` 现算)的 `VertexUpdate.Position` 分支:把原来的字符串键查找换成 `getOverlayStaticRecord(index.layer, mapIndex)`;命中则刷新记录的四个 rect 字段为刚写入的实例 rect、再把实例 rect 置 0、置 `overlayValueDirty = true`。`updateVertexArray` 的空图块分支同理(这里的 `index` 是 `BlockIndex`,可直接用 `index.mapIndex`):命中记录则把记录 rect 置 0 并置值脏。两处都要保留原有注释,只新增解释「为什么再次置零」的注释。
|
||||
|
||||
**I′. 必须一并删除的失效局部变量(否则 eslint `no-unused-vars` 直接失败):** 换掉字符串键查找后,下面这些局部量都会变成未使用,必须**整行删掉**(不要为了「留着不报错」去加 `void`、下划线改名或 `eslint-disable`):
|
||||
|
||||
- `updateTileVertex` 的 `VertexUpdate.Position` 分支:`const layerIndex = this.renderer.getLayerIndex(index.layer);` 与 `const key = this.getStaticOverlayKey(layerIndex, index.mapX, index.mapY);`(当前 `vertex.ts` 约 567-572 行)。
|
||||
- `updateVertexArray` 的空图块分支:`const layerIndex = ...` 与 `const key = this.getStaticOverlayKey(...)`(当前约 708-713 行),该分支因此不再需要 `getLayerIndex`。
|
||||
- `setStaticFrame`:`const layerIndex = ...` 与 `const key = ...`(当前约 1194-1196 行)——按 H 改为直接 `overlayValueDirty = true` 之后这两行同样失效。
|
||||
- 方法本体:`getStaticOverlayKey()` / `getDynamicOverlayKey()` 已在 A 中删除,`setStaticAlpha` 与 `updateMoving` / `deleteMoving` 的调用点已在 H / L 中移除。
|
||||
|
||||
**J. 重建清理。** `checkRebuild()` 与 `setBlockSize()`:删掉 `overlay.clearStatic()` / `overlay.markValueDirty()`,改为 `overlayStatic.clear()`、`pendingAlpha.clear()`、`overlayOrderDirty = true`、`overlayValueDirty = true`、`overlayCount = 0`、`overlayOrdered.length = 0`(重建会重新分配实例数组并把 alpha 复位为 1)。
|
||||
|
||||
**K. 脏标记 / 取数组。** `isOverlayDirty()` 返回 `pendingAlpha.size > 0 || overlayOrderDirty || overlayValueDirty`。`getOverlayArray()` 依次:`flushPendingAlpha()`;`if (overlayOrderDirty) sortOverlay()`;`if (overlayValueDirty || overlayOrderDirty) assembleOverlay()`;返回 `overlayInstancedArray`。因为 F 里 `sortOverlay()` 会把 `overlayValueDirty` 置真,这条判断在「刚排序」时**必然**进入组装;保留 `|| overlayOrderDirty` 只是防御性写法,不得依赖它来补 F 的缺口。`getOverlayCount()` 返回 `overlayCount`。`getVertexArray()` 在 `checkRebuild()` 之后、返回对象之前调用 `flushPendingAlpha()`。三个方法的 jsDoc 要写清调用顺序契约(`getOverlayCount` 需在脏时先取数组)。
|
||||
|
||||
**L. 动态图块。** `updateMoving` 末尾:删掉 `overlay.update(...)` 调用,改为 `overlayOrderDirty = true; overlayValueDirty = true;`(位置与图层都是排序键,值也可能变)。`deleteMoving`:删掉 `overlay.remove(...)`,改为 **`overlayOrderDirty = true;` 与 `overlayValueDirty = true;` 两条都要写**——删除改变了成员集合,若只置顺序脏,`getOverlayArray()` 里 `sortOverlay()` 清掉顺序脏后可能不进组装,暂存数组与 `overlayCount` 还留着已删除动态块的实例(残影)。`updateMovingList` 不动。分块张量、`dynamicStart`、`dynamicInstancedArray`、`movingIndexMap` 语义零改动。
|
||||
|
||||
**M. 注释纪律。** 所有新增私有方法与字段必须有 jsDoc(含参数说明);`setStaticAlpha` 的惰性、flush 的置零 / 恢复、排序键的 `x` 只作 CPU 侧键、组装的「覆盖前四个 float」都要有「为什么」注释。**不得修改任何既有注释或 jsDoc**(包括 09-25 写入的;如某段注释描述已删除的 `overlay` 字段,只能新增一句更正性注释,不得改写原句)。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cmd /c "pnpm exec eslint packages-user/client-base/src/map/vertex.ts"</automated>
|
||||
<fails_when>eslint 报任何 error(含未使用导入、缺 `implements`、注释格式),或命令非零退出。</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const base='packages-user/client-base/src/map/';const v=fs.readFileSync(base+'vertex.ts','utf8');const D=String.fromCharCode(36);if(fs.existsSync(base+'overlay.ts'))bad.push('overlay.ts still exists');if(fs.existsSync(base+'__test__/overlay.test.ts'))bad.push('__test__/overlay.test.ts still exists');if(v.indexOf('from \'./overlay\'')>=0)bad.push('vertex.ts still imports ./overlay');if(v.indexOf('MapOverlayOrder')>=0||v.indexOf('IMapOverlayOrder')>=0)bad.push('overlay class references remain');if(v.indexOf('getStaticOverlayKey')>=0||v.indexOf('getDynamicOverlayKey')>=0)bad.push('string key builders remain');if(v.indexOf('static:'+D)>=0||v.indexOf('dynamic:'+D)>=0)bad.push('string identity keys remain');if(v.indexOf('Map<string')>=0)bad.push('string keyed Map remains');const body=(src,sig)=>{const i=src.indexOf(sig);if(i<0)return '';const j=src.indexOf('\n }',i);return j<0?src.slice(i):src.slice(i,j)};for(const n of ['flushPendingAlpha','sortOverlay','assembleOverlay','pendingAlpha','overlayStatic','overlayOrderDirty','overlayValueDirty','getMovingBlock(','OVERLAY_ALPHA_THRESHOLD','isOverlayAlpha']){if(v.indexOf(n)<0)bad.push('missing '+n)}const s=body(v,'private sortOverlay(');if(s==='')bad.push('sortOverlay body missing');else if(s.indexOf('overlayValueDirty = true')<0)bad.push('sortOverlay must set overlayValueDirty so a reorder always re-assembles');const g=body(v,'getOverlayArray(');if(g==='')bad.push('getOverlayArray body missing');else{const a=g.indexOf('flushPendingAlpha('),b=g.indexOf('sortOverlay('),c=g.indexOf('assembleOverlay(');if(a<0||b<0||c<0)bad.push('getOverlayArray must call flush, sort and assemble');else if(!(a<b&&b<c))bad.push('getOverlayArray order must be flush then sort then assemble')}const d=body(v,'deleteMoving(');if(d==='')bad.push('deleteMoving body missing');else if(d.indexOf('overlayValueDirty = true')<0)bad.push('deleteMoving must also dirty the overlay staging value');const gi=v.indexOf('private getIndexInBlock(');for(const n of ['private flushPendingAlpha(','private sortOverlay(','private assembleOverlay(']){if(gi<0)bad.push('getIndexInBlock missing');else if(v.indexOf(n)<0||v.indexOf(n)<gi)bad.push(n+' missing or declared before getIndexInBlock')}if(bad.length){console.error('VERTEX GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK vertex gate: overlay only in vertex.ts, lazy setter, flush/sort/assemble order, reorder always re-assembles, getIndexInBlock declared first')"</automated>
|
||||
<fails_when>两个 overlay 文件仍存在、`vertex.ts` 仍导入或引用字符串键 / 字符串 Map、任一必需标识符缺失、`sortOverlay()` 未置 `overlayValueDirty = true`、`deleteMoving()` 未置 `overlayValueDirty = true`、`getOverlayArray()` 的三步顺序错乱、三个覆盖层方法声明在 `getIndexInBlock()` 之前,或命令非零退出。eslint 那一条同时会把「残留的 `layerIndex` / `key` 未使用局部变量」判成 error。</fails_when>
|
||||
</verify>
|
||||
<done>`overlay.ts` 与 `__test__/overlay.test.ts` 已删除(未跟踪文件,`git status` 里不再出现、也不会有 `D` 行);`vertex.ts` 内不再有 `./overlay` 导入、不再有字符串身份键、不再有 `Map<string, …>`、不再有 `getStaticOverlayKey` / `getDynamicOverlayKey`,以及它们遗留的 `layerIndex` / `key` 局部变量;覆盖层状态 / 私有类型 / flush / 排序 / 组装 / 记录辅助方法全部就位且声明在 `getIndexInBlock()` 之后;`setStaticAlpha` 只写 `pendingAlpha` + 标脏 + `requestUpdate()`;`getVertexArray()` 调用 flush;`isOverlayDirty()` 覆盖三类脏;`sortOverlay()` 置值脏 + `deleteMoving()` 置两个脏标记(排序后必然组装);eslint 通过;分块张量与动态索引身份零改动。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: `renderer.ts` 按新契约取覆盖层并补齐解释性 WebGL 注释,`types.ts` 补 `getOverlayCount` 契约说明</name>
|
||||
<files>packages-user/client-base/src/map/renderer.ts, packages-user/client-base/src/map/types.ts</files>
|
||||
<action>
|
||||
**依赖 Task 1 已完成**(否则 `getOverlayArray()` 的 flush 契约不生效)。
|
||||
|
||||
**A. 覆盖层取用顺序(**无需改代码**,只补注释 / 契约说明)。** 现工作树里 `render()` 已经是目标顺序:`let overlayCount = this.vertex.getOverlayCount();` → `if (this.vertex.isOverlayDirty()) { const overlayArray = this.vertex.getOverlayArray(); overlayCount = this.vertex.getOverlayCount(); ... }`。**保持这个顺序不变**,不要重排、不要改成先取数组后判脏。本项只需:①确认脏分支内 `getOverlayArray()` 确实在 `getOverlayCount()` 之前;②在 `getOverlayArray()` 旁补一句注释说明「`vertex.getVertexArray()` 已在不透明遍上传前完成 flush,这里只是把 flush 后的结果排序 / 组装成暂存数组」;③确认上传仍是 `overlayArray.subarray(0, overlayCount * INSTANCED_COUNT)` + `gl.bufferData(..., gl.DYNAMIC_DRAW)`,且 `overlayCount > 0` 才绘制。Task 3 会断言这个顺序。
|
||||
|
||||
**B. 覆盖层绘制。** 保留 `gl.depthMask(false)` → 绑定 `overlayBuffer` → 重设四个实例属性指针(WebGL2 无 baseInstance)→ `gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, overlayCount)` → `gl.depthMask(true)`。给这一段加解释性注释:覆盖层最后画是因为它依赖不透明遍已写入的深度做遮挡;关深度写入是因为覆盖层彼此靠提交顺序混合,写深度会让后提交的同深度面被裁掉;收尾恢复 `depthMask(true)` 是为了下一次不透明遍与后处理链不受污染。
|
||||
|
||||
**C. 不透明遍注释与裁剪。** 保留 `end = Math.min(v.endIndex, dynamicStart)` 的裁剪逻辑,注释说明「动态块改由覆盖层绘制,不透明遍必须裁掉 `dynamicStart` 之后的一截,否则同一块被画两遍」。保留 `gl.depthMask(true)` 于不透明遍之前,并注释说明「不透明遍必须写深度,覆盖层的遮挡完全依赖它」。
|
||||
|
||||
**D. 混合与状态。** 保留 `blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA)`,新增注释说明「分离式混合让 alpha 通道按 `ONE, ONE_MINUS_SRC_ALPHA` 累积,避免多次叠加后 alpha 被重复乘衰」。**不得**改回 `gl.blendFunc(...)`。
|
||||
|
||||
**E. 后处理(保留 + 注释)。** 保留 ping / pong 各自绑定自己颜色纹理(`pingFramebuffer ← pingTexture2D`、`pongFramebuffer ← pongTexture2D`)、各自 `framebufferRenderbuffer(DEPTH_ATTACHMENT, RENDERBUFFER, <对应 depthBuffer>)`、`DEPTH_COMPONENT24` 的 `renderbufferStorage`、`NEAREST` + `CLAMP_TO_EDGE`;保留「0 个启用效果 → 默认帧缓冲,≥1 → 场景进 ping,效果 i 读上一个 FBO 的颜色纹理、写另一个 FBO,最后一个写 `null`」的链路;保留 `setCanvasSize` 同时重设两个深度缓冲、`destroy` 删除两个 FBO / 两个纹理 / 两个深度 renderbuffer / `overlayBuffer`。新增注释说明:为什么场景要先进 FBO(效果需要读整幅画面)、为什么每个 FBO 要挂自己的深度附件(离屏渲染同样要做深度测试,否则遮挡全丢)、为什么纹理要 `NEAREST` + `CLAMP_TO_EDGE`(非 `POT` 尺寸下避免不完整纹理采样为黑)、为什么最后一个效果写 `null`(直接上屏,不再多一次拷贝)。
|
||||
|
||||
**F. `needUpdate()`。** 保留 `this.vertex.isOverlayDirty()`(Task 1 已把它扩成覆盖 pending),不加其它条件。
|
||||
|
||||
**G. `types.ts`。** 只做一处补充:`IMapVertexGenerator.getOverlayCount()` 的 jsDoc 增加一句「顺序或值脏时需先调用 `getOverlayArray()` 完成组装,否则返回的是上一次组装的数量」。若已存在同义描述则不改。**`IContextData` 的 `overlayBuffer` / `pingDepthBuffer` / `pongDepthBuffer` 与其余新增声明保持原样**;不得增删接口成员、不得修改既有成员注释、不得引入新名称。
|
||||
|
||||
**H. 注释纪律。** 只新增注释,不改既有注释;新增注释遵循 dev.md(单行 `//`、`//` 后留空格、说明「为什么」而不是「做什么」、每 40–60 字符在标点后换行)。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cmd /c "pnpm exec eslint packages-user/client-base/src/map/renderer.ts packages-user/client-base/src/map/types.ts"</automated>
|
||||
<fails_when>eslint 报任何 error,或命令非零退出。</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const base='packages-user/client-base/src/map/';const r=fs.readFileSync(base+'renderer.ts','utf8');const t=fs.readFileSync(base+'types.ts','utf8');const i=r.indexOf('isOverlayDirty()');if(i<0)bad.push('no isOverlayDirty() call site');else{const seg=r.slice(i,i+900);const a=seg.indexOf('getOverlayArray()'),b=seg.indexOf('getOverlayCount()');if(a<0)bad.push('dirty branch does not fetch the overlay array');if(b<0)bad.push('dirty branch does not read the overlay count');if(a>=0&&b>=0&&a>b)bad.push('getOverlayCount() is read before getOverlayArray()')}for(const n of ['gl.depthMask(true)','gl.depthMask(false)','blendFuncSeparate(','DEPTH_COMPONENT24','framebufferRenderbuffer','dynamicStart','overlayBuffer','drawArraysInstanced']){if(r.indexOf(n)<0)bad.push('missing '+n)}if(r.indexOf('drawElementsInstanced')>=0)bad.push('uses drawElementsInstanced');if(r.indexOf('postEffects.length > 1')>=0)bad.push('old single-effect chain remains');const s=r.lastIndexOf('const stride = INSTANCED_COUNT * 4;');const e=r.indexOf('// Post effects',s);if(s<0||e<=s)bad.push('cannot locate the overlay draw block');else{const ov=r.slice(s,e);const cm=(ov.match(/(^|\r?\n)\s*\/\//g)||[]).length;if(cm<6)bad.push('overlay draw block has only '+cm+' comment lines')}if(t.indexOf('getOverlayCount():')<0)bad.push('getOverlayCount declaration missing');if(bad.length){console.error('RENDERER GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK renderer gate: dirty branch takes the array before the count, overlay pass and preserved GL features intact')"</automated>
|
||||
<fails_when>脏分支没有先取数组再取数量、任一项保留特性缺失、出现 `drawElementsInstanced` 或旧的 `postEffects.length > 1` 链路、覆盖层 / 绘制段注释少于 6 行,或命令非零退出。</fails_when>
|
||||
</verify>
|
||||
<done>`renderer.ts` 在脏时先 `getOverlayArray()` 后 `getOverlayCount()`;覆盖层上传 / 绘制 / 收尾恢复完整;两遍式、裁剪、`blendFuncSeparate`、ping/pong 颜色+深度配对、0/1/2+ 链路、缩放 / 销毁全部保留且带解释性注释;`types.ts` 的 `getOverlayCount()` 契约说明到位且无新增成员;eslint 通过。</done>
|
||||
</task>
|
||||
<task type="auto">
|
||||
<name>Task 3: 重写 `rendererGate.test.ts`,断言新形态(无 overlay 导入 / 无字符串键 / 惰性 setter / flush 点 / 排序键 / 动态现取 / 保留项)</name>
|
||||
<files>packages-user/client-base/src/map/__test__/rendererGate.test.ts</files>
|
||||
<action>
|
||||
**依赖 Task 1、Task 2 已完成**(断言对象已是新形态)。整文件重写,保持「`?raw` 读取源码、不导入 WebGL / DOM」的既有做法(09-25 偏差 1 的结论),并把 `?raw` 导入限定为本计划相关文件:`../renderer.ts?raw`、`../vertex.ts?raw`、`../types.ts?raw`、`../moving.ts?raw`、`../viewport.ts?raw`、`../shader/map.frag?raw`。**不得**导入 `../overlay`(文件已删除),不得使用 `node:fs` / `process`。
|
||||
|
||||
文件结构:保留一个 `getMethodBody(source, signature)` 辅助(从签名到第一个四空格 ` }`)与一个 `getColorAttachments(source)` 辅助(按最近一次 `bindFramebuffer` 推断帧缓冲 → 颜色纹理配对),再写 `describe('渲染源码闸门', () => { ... })`。**每个 `it` 前必须有一行 `//` 注释说明该测试覆盖什么**(dev.md 要求)。
|
||||
|
||||
`it` 清单(每条断言都要对应当前源码可稳定通过的形状):
|
||||
1. 覆盖层不再有独立文件依赖:`vertexSource` 不含 `from './overlay'`,不含 `MapOverlayOrder` / `IMapOverlayOrder` / `getStaticOverlayKey` / `getDynamicOverlayKey`。
|
||||
2. 无字符串身份键:`vertexSource` 不匹配 `/static:\$\{|dynamic:\$\{/`,不匹配 `Map<string`。
|
||||
3. 数值嵌套键(**用容忍换行的正则**,因为 `printWidth: 80` 会把长类型注解折行):`vertexSource` 匹配 `/Map<\s*IMapLayer\s*,\s*Map<\s*number\s*,\s*number\s*>\s*>/` 与 `/Map<\s*IMapLayer\s*,\s*Map<\s*number\s*,\s*IOverlayStaticRecord\s*>\s*>/`。
|
||||
4. `setStaticAlpha` 完全惰性:取方法体,含 `pendingAlpha`、`requestUpdate(`,且**不**含 `getIndexInBlock`、`INSTANCED_COUNT`、`overlayStatic`、`.sort(`、`rectX`。
|
||||
5. `getStaticAlpha` 优先读 pending:取方法体,含 `pendingAlpha`。
|
||||
6. `getVertexArray()` 是 flush 点:取方法体,含 `checkRebuild()` 与 `flushPendingAlpha()`,且 `checkRebuild` 出现在 `flushPendingAlpha` 之前。
|
||||
7. `getOverlayArray()` 顺序为 flush → sort → assemble:取方法体,三者都存在且 `indexOf` 严格递增。
|
||||
8. `isOverlayDirty()` 覆盖三类脏:取方法体,含 `pendingAlpha.size`、`overlayOrderDirty`、`overlayValueDirty`。
|
||||
9. `flushPendingAlpha()` 的落点:取方法体,含 `getIndexInBlock(`、`overlayStatic`、`markRenderDirty(`、`clear()`(清 pending)。**另断言方法声明顺序(dev.md:私有方法先于调用者)**:`vertexSource.indexOf('private getIndexInBlock(')` 严格小于 `private flushPendingAlpha(` / `private sortOverlay(` / `private assembleOverlay(` 三者各自的位置。
|
||||
10. 排序键与静态优先:取 `sortOverlay()` 方法体,含 `a.layerIndex - b.layerIndex`、`a.y - b.y`、`a.x - b.x`、`a.kind - b.kind`;**并且方法体必须含 `overlayValueDirty = true`**(顺序 / 成员变化后必须重新组装,否则已删除的动态块会以旧暂存数组 / 旧 `overlayCount` 留在屏幕上)。
|
||||
11. 动态成员现取:`sortOverlay()` 方法体含 `getMovingBlock()`,且**不**含 `getMovingBlockByIndex(`;`vertexSource` 整体不含动态登记表(不匹配 `overlayDynamic` / `IOverlayDynamicRecord`);**`deleteMoving()` 方法体必须同时含 `overlayOrderDirty = true` 与 `overlayValueDirty = true`**(删除成员时只置顺序脏会跳过组装)。
|
||||
12. 组装契约:取 `assembleOverlay()` 方法体,含 `INSTANCED_COUNT`、`rectX`、`dynamicInstancedArray`、`overlayCount`。
|
||||
13. `checkRebuild` / `setBlockSize` 清空成员与 pending:两处方法体各含 `overlayStatic.clear()` 与 `pendingAlpha.clear()`。
|
||||
14. 两遍式状态:`rendererSource` 含 `gl.depthMask(true)` 与 `gl.depthMask(false)`,且 `depthMask(false)` 之后仍出现 `gl.depthMask(true)`(收尾恢复)。
|
||||
15. 分离式混合:含 `blendFuncSeparate(`,且不含 `gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)`。
|
||||
16. 深度附件与生命周期:含 `DEPTH_COMPONENT24`、`framebufferRenderbuffer`、`renderbufferStorage`、`deleteRenderbuffer`、`deleteFramebuffer`、`deleteTexture`;`setCanvasSize` 之后 2000 字符内出现 `renderbufferStorage`。
|
||||
17. ping / pong 配对:`getColorAttachments` 得 `pingFramebuffer → pingTexture2D`、`pongFramebuffer → pongTexture2D`。
|
||||
18. 效果链路:最后一个效果写 `null`(`postEffects.forEach` 片段含 `.length - 1` 与 `null`),不含 `.length - 2`,不含 `postEffects.length > 1`,第一个效果读 `pingTexture2D`。
|
||||
19. 覆盖层独立缓冲区与实例顺序:含 `overlayBuffer`、`drawArraysInstanced`,不含 `drawElementsInstanced`;脏分支内 `getOverlayArray()` 出现在 `getOverlayCount()` 之前。
|
||||
20. 动态尾部裁剪:`rendererSource` 含 `Math.min(v.endIndex, dynamicStart)`。
|
||||
21. 覆盖层代码段有解释性注释:`// 图块` 到 `// Post effects` 之间的 `//` 注释行数 ≥ 6。
|
||||
22. 分块布局未被排序:`vertexSource` 含 `this.dynamicStart = staticCount;`、`instancedStart`、`i * count * INSTANCED_COUNT`,且不匹配 `/(instancedArray|tileInstanced)\s*\.sort\(/`。
|
||||
23. 视口仍合并动态尾部:`viewportSource` 含 `this.checkDynamic(renderArea, dynamicStart, dynamicCount);` 与 `last.count += dynamicCount;`。
|
||||
24. 动态索引身份:`movingSource` 含 `this.index = index;` 且不含 `.sort(`;`typesSource` 含 `readonly index: number;`;`rendererSource` 含 `movingIndexMap.set(index, moving)` 与 `returnMovingIndex(block.index)`。
|
||||
25. `map.frag` 阈值:`fragSource` 含 `0.004` 且不含 `alpha < 0.1`。
|
||||
26. 无半精度 / 顺序剥离:`rendererSource` 与 `vertexSource` 都不匹配 `/OIT|depthPeel|gl_FragDepth|halfFloat|half_float/i`。
|
||||
27. OI-1 (A):`getIndexInBlock` 方法体含 `x - block.dataX`、`y - block.dataY`,不含 `x - block.x`。
|
||||
28. 新私有方法都有 jsDoc:对 `flushPendingAlpha`、`sortOverlay`、`assembleOverlay`、`getOverlayStaticRecord`、`deleteOverlayStaticRecord` 逐个检查「签名前 400 字符内出现 `/**`」(若某辅助方法按 OI-1 的允许未新增,则该条只在存在时才断言)。
|
||||
29. `types.ts` 的 `getOverlayCount` 契约:`getOverlayCount():` 之前最近的 `/** ... */` 块内包含 `getOverlayArray`。
|
||||
|
||||
断言风格:优先 `toContain` / `not.toContain`,正则只用于「形状不确定但语义确定」的地方;**不得**为了让测试通过而放宽语义(例如把「method body 不含 rectX」改成「不含某一行」)。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cmd /c "pnpm exec vitest run packages-user/client-base/src/map/__test__/rendererGate.test.ts"</automated>
|
||||
<fails_when>任一 `it` 失败(尤其是「惰性 setter」「flush 点」「无字符串键」「动态现取」「保留项」),或测试文件无法收集 / 导入(例如仍 `import` 已删除的 `../overlay`),或命令非零退出。</fails_when>
|
||||
<automated>node -e "const fs=require('fs');const bad=[];const f='packages-user/client-base/src/map/__test__/rendererGate.test.ts';const s=fs.readFileSync(f,'utf8');const its=(s.match(/(^|[^A-Za-z_.])it\(/g)||[]).length;const cm=(s.match(/(^|\r?\n)\s*\/\//g)||[]).length;if(its<26)bad.push('only '+its+' it() blocks');if(cm<its)bad.push(cm+' comment lines for '+its+' its');if(s.indexOf('from \'../overlay\'')>=0)bad.push('imports the deleted overlay module');if(s.indexOf('node:fs')>=0||s.indexOf('from \'node:')>=0)bad.push('uses node fs');if(s.indexOf('overlay.test.ts')>=0)bad.push('references the deleted overlay test');if(bad.length){console.error('TEST GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK test gate: '+its+' its with '+cm+' comment lines')"</automated>
|
||||
<fails_when>用例少于 26 条、注释行数少于用例数(即存在没有前置注释的 `it`)、仍导入已删除模块 / 引用已删除的 `overlay.test.ts`,或使用 `node:fs`,或命令非零退出。</fails_when>
|
||||
</verify>
|
||||
<done>闸门文件重写完成:29 个断言组全部对应新形态;每条 `it` 前有一行说明注释;不使用 `../overlay`、不使用 `node:fs`;`vitest run` 全绿;无一条断言被放宽到失去判别力。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: 全量门禁(vitest / eslint / 过滤 vue-tsc 精确豁免 / 哈希范围闸门)并写 `09-26-SUMMARY.md`</name>
|
||||
<files>.planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md</files>
|
||||
<action>
|
||||
**不修改任何源码或测试文件**;本任务只跑门禁并写摘要。若门禁暴露问题,必须回到对应任务修正后重跑,**不得**通过放宽断言 / 扩大豁免 / 删除用例来「通过」。
|
||||
|
||||
**A. 单测 / 源码闸门。** 跑 `cmd /c "pnpm exec vitest run packages-user/client-base/src/map/__test__"`,记录用例总数与结果。
|
||||
|
||||
**B. eslint。** 跑 `cmd /c "pnpm exec eslint packages-user/client-base/src/map/vertex.ts packages-user/client-base/src/map/renderer.ts packages-user/client-base/src/map/types.ts packages-user/client-base/src/map/__test__/rendererGate.test.ts"`,必须 0 error。
|
||||
|
||||
**C. 过滤类型闸门。** 用下面 verify 里的 `node -e` 包一层 `cmd /c "pnpm exec vue-tsc --noEmit"`(Node 侧 `spawnSync`,不依赖外层 shell 展开),只取 `packages-user/client-base/src/map/` 下的诊断行;把每条诊断去掉 `(行,列)` 并只保留文件名后与基线 `renderer.ts error TS2339: Property 'getLayerByAlias' does not exist on type 'IGameMap'.` **精确比对**:该基线必须**恰好一条**,成功时末尾打印 `exempted baseline: <原文>`,其余任何 `map/` 诊断一条都不许有。**不得**忽略 `renderer.ts` 的其它诊断,**不得**改 `getLayer` / `IGameMap` / `GameMap`。
|
||||
|
||||
**D. 范围闸门(真实 git 状态)。** `git status --porcelain -uall -- packages-user/client-base/src/map` 的变更集必须**恰好**是这 6 条:` M packages-user/client-base/src/map/renderer.ts`、` M packages-user/client-base/src/map/shader/map.frag`、` M packages-user/client-base/src/map/status.ts`、` M packages-user/client-base/src/map/types.ts`、` M packages-user/client-base/src/map/vertex.ts`、`?? packages-user/client-base/src/map/__test__/rendererGate.test.ts`。其中 `shader/map.frag` 与 `status.ts` 是**执行前就存在**的 ` M`(前者来自 09-25、后者是用户自己的改动),允许存在但由 D′ 的哈希比较证明未被本计划改动;`__test__/rendererGate.test.ts` 重写后仍是 `??`(它本来就没被跟踪)。`overlay.ts` 与 `__test__/overlay.test.ts` **不存在**(用 `fs.existsSync` 断言,**不要**期待 `D` 行:两个文件都是未跟踪的,删除后只是从 `??` 消失)。白名单是显式 6 条 + 逐条「期望状态必须存在」,既不能放过白名单外的任何一条,也不能让「某条期望状态根本没出现」蒙混过关。**不得执行任何 git 写命令**(只允许 `status` / `hash-object` / `diff` / `rev-parse` 等只读命令)。
|
||||
|
||||
**D′. 哈希闸门(执行前后真值比较,不是 grep 证据)。** 读 Task 0 写下的 `09-26-BASELINE.json`(含 `head` 与 7 个 `hashes`),再用 `git hash-object` 取当前值:①`git rev-parse HEAD` 必须与之相等(HEAD 未变、无新提交);②`status.ts`、`moving.ts`、`viewport.ts`、`block.ts`、`element.ts`、`constant.ts`、`shader/map.frag` 七个当前哈希必须与基线**逐字相等**,任一不等即失败并打印「基线值 → 当前值」;③基线文件里的 `hashes` 必须恰好 7 项(防止残缺基线被当成通过)。**基线文件只在 Task 0 写一次,本任务只读、不得重写。** HEAD 比较是**严格**的:任何 HEAD 前移(包括只改 `.planning/` 的文档提交)都必须判失败并作为偏差如实写进 SUMMARY 上报,**不得**私自重写基线去「修好」它。另外把 7 个基线哈希原文写进 SUMMARY 作为审计痕迹(gate E 会检查哈希原文出现在 SUMMARY 里,但那只是痕迹检查,判定依据是这里的真值比较)。
|
||||
|
||||
**E. 写摘要 `09-26-SUMMARY.md`。** 沿用 09-25-SUMMARY 的 frontmatter 风格(`phase` / `plan: 26` / `subsystem: rendering` / `tags` / `requires` / `provides` / `affects` / `actuals` / `tech-stack` / `key-files` / `key-decisions` / `patterns-established` / `requirements-completed: []` / `coverage` / `duration` / `completed` / `status`),正文至少包含:
|
||||
- **Accomplishments**:`overlay.ts` / `overlay.test.ts` 已删除;覆盖层状态与逻辑全在 `vertex.ts`;`setStaticAlpha` 惰性化;flush 落在 `getVertexArray()`;数值嵌套键;动态成员每次现取;`renderer.ts` 按新契约取用并补注释;`types.ts` 的 `getOverlayCount` 契约说明。
|
||||
- **保留项清单**:两遍式与 `depthMask` 三段、动态尾部裁剪、`blendFuncSeparate`、ping/pong 颜色+`DEPTH_COMPONENT24` 配对、0/1/2+ 链路、`setCanvasSize` / `destroy`、`map.frag` 的 `0.004`、OI-1 (A) `dataX` / `dataY` —— 逐项写明「保留,未回退」。
|
||||
- **Task Commits**:明确写「无。按用户要求未运行任何 git 写命令,改动全部留在工作树」。
|
||||
- **Verification**:五条门禁的命令、结果、用例数,以及被豁免的那一条基线原文;写明类型闸门只对 `map/` 负责、不覆盖全仓;写明范围闸门用的是「6 条期望状态 + 两个文件不存在」,哈希闸门用的是「当前值 vs `09-26-BASELINE.json` 基线值的真值比较」。
|
||||
- **Deviations / Issues**:如实记录实际发生的偏差(若有);记录已知限制:同 z 深度边界、重建丢静态 alpha、pending 得不到实例时被丢弃、动态块移动期间每帧重排、`addPostEffect` 无调用方。
|
||||
- **无 UAT 声明**:「游戏当前不可运行,浏览器人工 UAT 全程跳过;**无 WebGL E2E**;源码闸门通过不等于渲染结果已验证」。
|
||||
- **User Setup Required**:待游戏可运行后补 UAT 的清单(静态半透明叠加、动态块遮挡与混合、26×13 跨分块、0/1/2+ 后处理、画布缩放)。
|
||||
- **estimate 未校准声明**:`confidence: low`,单计划不拆分;写明 09-25 的 160000 超预算声明本计划不再沿用。
|
||||
- **未被触碰的文件**:开一段「哈希证据」,逐行写出 7 个受保护文件的**基线哈希原文**(来自 `09-26-BASELINE.json`)+ HEAD 原文,并说明「与 Task 0 基线逐字相等、HEAD 未变」;明确写 `map/` 变更集等于允许的 6 条状态、`status.ts` 与 `shader/map.frag` 的 ` M` 是执行前既有状态且哈希未变、未改依赖 / 配置 / 其它包 / 09-01..09-25 计划。
|
||||
- `status: partial`(代码任务完成、UAT 跳过),`completed: 2026-10-24`。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cmd /c "pnpm exec vitest run packages-user/client-base/src/map/__test__"</automated>
|
||||
<fails_when>任一条用例失败,或测试无法收集,或命令非零退出。</fails_when>
|
||||
<automated>cmd /c "pnpm exec eslint packages-user/client-base/src/map/vertex.ts packages-user/client-base/src/map/renderer.ts packages-user/client-base/src/map/types.ts packages-user/client-base/src/map/__test__/rendererGate.test.ts"</automated>
|
||||
<fails_when>出现任何 error / warning 导致非零退出。</fails_when>
|
||||
<automated>node -e "const cp=require('child_process');const r=cp.spawnSync('cmd',['/c','pnpm exec vue-tsc --noEmit'],{encoding:'utf8',timeout:400000});const text=(r.stdout||'')+(r.stderr||'');const bad=[];if(r.error||r.status===null||r.signal)bad.push('vue-tsc did not complete: '+((r.error&&r.error.code)||r.signal||'status=null'));if(r.status!==0&&text.trim()==='')bad.push('vue-tsc produced no output');const all=text.split(/\r?\n/).filter(l=>/error TS\d+/.test(l));const scoped=all.filter(l=>/packages-user[\\/]client-base[\\/]src[\\/]map[\\/](__test__[\\/])?(overlay|vertex|renderer|types)[^\\/]*\.ts/.test(l));const norm=l=>l.replace(/\(\d+,\d+\):/,'').replace(/^.*[\\/]/,'');const base='renderer.ts error TS2339: Property \'getLayerByAlias\' does not exist on type \'IGameMap\'.';const hit=scoped.filter(l=>norm(l)===base);const fresh=scoped.filter(l=>norm(l)!==base);if(hit.length!==1)bad.push('known baseline expected exactly once, got '+hit.length+' :: '+base);for(const e of fresh)bad.push('new scoped diagnostic :: '+e);if(bad.length){console.error('TYPECHECK FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK map files type-check clean (new diagnostics: 0); total diagnostics: '+all.length+' ; exempted baseline: '+(hit[0]||'none'))"</automated>
|
||||
<fails_when>基线匹配数不是恰好 1(0 条说明消息变了、≥2 条说明豁免被扩大),或出现任何其它 `map/` 诊断,或 `vue-tsc` 未正常结束 / 无输出,或命令非零退出。</fails_when>
|
||||
<automated>node -e "const fs=require('fs'),cp=require('child_process');const bad=[];const dir='packages-user/client-base/src/map/';const root='packages-user/client-base/src/map';const expected=[' M '+dir+'renderer.ts',' M '+dir+'shader/map.frag',' M '+dir+'status.ts',' M '+dir+'types.ts',' M '+dir+'vertex.ts','?? '+dir+'__test__/rendererGate.test.ts'];for(const f of ['overlay.ts','__test__/overlay.test.ts']){if(fs.existsSync(dir+f))bad.push('must be deleted: '+f)}const g=cp.spawnSync('git',['status','--porcelain','-uall','--',root],{encoding:'utf8'});if(g.status!==0)bad.push('git status failed');const lines=(g.stdout||'').split(/\r?\n/).map(x=>x.trimEnd()).filter(Boolean);for(const l of lines){if(expected.indexOf(l)<0)bad.push('out-of-scope map change: '+l)}for(const l of expected){if(lines.indexOf(l)<0)bad.push('expected state missing: '+l)}const sum='.planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md';if(!fs.existsSync(sum))bad.push('SUMMARY missing');else{const s=fs.readFileSync(sum,'utf8');for(const k of ['overlay.ts','flushPendingAlpha','setStaticAlpha','E2E','UAT','confidence: low','status: partial']){if(s.indexOf(k)<0)bad.push('SUMMARY missing: '+k)}}if(bad.length){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope gate: map change set equals the 6 expected states and the overlay files are gone')"</automated>
|
||||
<fails_when>任一 overlay 文件仍存在、`map/` 变更集出现白名单外的条目(含未跟踪文件)、6 条期望状态有任一条缺失(例如 `renderer.ts` 根本没被改、`rendererGate.test.ts` 不见了)、SUMMARY 缺少必需声明,或命令非零退出。</fails_when>
|
||||
<automated>node -e "const fs=require('fs'),cp=require('child_process');const bad=[];const bp='.planning/phases/09-minimum-browser-runnable/09-26-BASELINE.json';const dir='packages-user/client-base/src/map/';const files=['status.ts','moving.ts','viewport.ts','block.ts','element.ts','constant.ts','shader/map.frag'];if(!fs.existsSync(bp))bad.push('baseline artifact missing: '+bp);else{const b=JSON.parse(fs.readFileSync(bp,'utf8'));const now=((cp.spawnSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).stdout)||'').trim();if(!b.head)bad.push('baseline has no head');else if(now!==b.head)bad.push('HEAD moved since baseline: '+b.head+' -> '+now);if(!b.hashes||Object.keys(b.hashes).length!==files.length)bad.push('baseline must record exactly '+files.length+' hashes');for(const f of files){const p=dir+f;if(!fs.existsSync(p)){bad.push('missing '+f);continue}const h=((cp.spawnSync('git',['hash-object',p],{encoding:'utf8'}).stdout)||'').trim();if(!b.hashes||!b.hashes[f])bad.push('baseline has no hash for '+f);else if(h!==b.hashes[f])bad.push('protected file changed: '+f+' '+b.hashes[f]+' -> '+h)}const sum='.planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md';if(!fs.existsSync(sum))bad.push('SUMMARY missing');else{const s=fs.readFileSync(sum,'utf8');for(const f of files){if(b.hashes&&b.hashes[f]&&s.indexOf(b.hashes[f])<0)bad.push('SUMMARY does not record the baseline hash of '+f)}}}if(bad.length){console.error('HASH GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK hash gate: HEAD and the 7 protected hashes are identical to the Task 0 baseline and recorded in SUMMARY')"</automated>
|
||||
<fails_when>基线产物缺失 / 残缺(哈希不是 7 项)、HEAD 与基线不一致、任一受保护文件的当前哈希与基线不相等(即被本计划改动)、任一基线哈希原文没有出现在 SUMMARY 的「哈希证据」段,或命令非零退出。</fails_when>
|
||||
</verify>
|
||||
<done>五条门禁全绿:`vitest` 通过、`eslint` 0 error、过滤后的 `vue-tsc` 恰好豁免一条基线并打印其原文、`map/` 变更集恰好等于 6 条允许状态且两个 overlay 文件不存在、HEAD 与 7 个受保护文件的当前哈希与 `09-26-BASELINE.json` 逐字相等并已写进 SUMMARY;`09-26-SUMMARY.md` 写完并包含「未提交代码 / 无 WebGL E2E / UAT 跳过 / estimate 未校准 / 保留项未回退」全部声明。</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| CPU 侧覆盖层状态 → GPU 缓冲区 | `vertex.ts` 组装的暂存数组、属性指针偏移、`bufferData` / `bufferSubData` 长度,全部在 JS 侧决定;CP 状态与缓冲区一旦错位,画面表现为黑洞 / 重复绘制 / 残影 |
|
||||
| 渲染器 ↔ 数据端图层 | `IMapLayer` 对象身份、`renderer.getLayerIndex()`、`renderer.getMovingBlock()` 都是跨包(`@user/data-base`)边界取来的引用,可能因图层列表 / `zIndex` 变化而失效 |
|
||||
| 渲染器 ↔ 离屏帧缓冲 | ping / pong 的颜色附件、深度附件与效果链的输入 / 输出配对;不完整 FBO 会让整幅画面变黑或丢失深度测试 |
|
||||
| 工作树 ↔ 交付物 | 未提交的 09-25 草稿与用户并发的脏文件共存;误改 / 误提交会污染用户正在进行的改动 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-26-01 | Tampering | `MapVertexGenerator.setStaticAlpha` / `flushPendingAlpha` / `overlayStatic` | high | mitigate | 单一 flush 点(`getVertexArray()`)+ 记录 rect 与实例 rect 同源 + `markRenderDirty()` 触发重传 + 重建清空;闸门断言「setter 方法体内不得出现 rect / 实例写入」与「flush 方法体内必须出现三件套」 |
|
||||
| T-09-26-02 | Tampering | `pendingAlpha` 生命周期 / 两个脏标记 | medium | mitigate | `isOverlayDirty()` 把 pending 计入脏,保证请求必然被处理;flush 结束 `clear()`;`checkRebuild` / `setBlockSize` 一并清空;**`sortOverlay()` 置 `overlayValueDirty = true`、`deleteMoving()` 置两个脏标记**,杜绝「排序清掉顺序脏 → 跳过组装 → 旧暂存数组 / 旧数量上屏」的残影;闸门断言这三条源码形状 |
|
||||
| T-09-26-03 | Tampering | 动态覆盖层成员(已销毁 / 已改图层的块) | high | mitigate | 不保留任何动态登记表,每次排序从 `renderer.getMovingBlock()` 现取 `index` / `x` / `y` / `layer`;闸门断言 `getMovingBlock()` 存在且 `getMovingBlockByIndex(` / `overlayDynamic` 不存在 |
|
||||
| T-09-26-04 | Tampering | ping / pong FBO 的附件完整性 | high | mitigate | 每个 FBO 绑定自己的颜色纹理并挂 `DEPTH_COMPONENT24` renderbuffer,纹理 `NEAREST` + `CLAMP_TO_EDGE`;闸门断言配对map 与 `framebufferRenderbuffer` / `renderbufferStorage` |
|
||||
| T-09-26-05 | Tampering | 覆盖层缓冲区上传长度与绘制数量 | medium | mitigate | 只上传 `subarray(0, overlayCount * INSTANCED_COUNT)`,数量为 0 时跳过绘制,绘完恢复 `depthMask(true)`;闸门断言脏分支「先取数组后取数量」与 `drawArraysInstanced` |
|
||||
| T-09-26-06 | Tampering | 不透明遍与覆盖层的双重绘制 | medium | mitigate | 不透明遍按 `Math.min(v.endIndex, dynamicStart)` 裁剪;静态半透明图块以零 rect 退化,进不了片元阶段;闸门断言裁剪表达式与分块数组未被排序 |
|
||||
| T-09-26-07 | Denial of Service | 暂存数组无界增长 | low | accept | 仅按 `Math.max(length * 2, required)` 倍增,上限天然受地图图块数约束;不为它增加收缩逻辑(避免引入新的状态) |
|
||||
| T-09-26-08 | Repudiation | 类型基线的豁免被悄悄扩大 | medium | mitigate | 精确到「文件 + 代码 + 消息」且要求恰好一条并打印 `EXEMPT:`;任何其它 `map/` 诊断失败 |
|
||||
| T-09-26-09 | Tampering | 误改 `status.ts` / `moving.ts` / `viewport.ts` / `map.frag` 等受保护文件 | high | mitigate | Task 0 把 7 个 `git hash-object` + HEAD 写进 `09-26-BASELINE.json`,Task 4 用**当前值 vs 基线值**的真值相等比较(不是「哈希字符串出现在 SUMMARY」这种弱检查);`map/` 变更集用 `-uall` 与 6 条显式状态逐条比对;禁止一切 git 写命令,禁止重写基线文件 |
|
||||
| T-09-26-SC | Tampering | npm / pnpm 依赖安装(供应链) | high | mitigate | 本计划明确禁止新增依赖与执行安装;若执行期出现任何安装需求,必须停下并转入 `[ASSUMED]` 包合法性人工核验 checkpoint,不得自动放行 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
## 整体验证(无浏览器 UAT)
|
||||
|
||||
| 检查 | 命令 / 手段 | 通过判据 |
|
||||
|------|-------------|----------|
|
||||
| 源码形状闸门 | `cmd /c "pnpm exec vitest run packages-user/client-base/src/map/__test__"` | 29 个断言组全绿;每条 `it` 前有一行注释;不导入已删除模块、不使用 `node:fs` |
|
||||
| 代码风格 | `cmd /c "pnpm exec eslint <vertex/renderer/types/rendererGate>"` | 0 error(含 `no-unused-vars`:不允许残留 `layerIndex` / `key` 局部变量) |
|
||||
| 类型(按文件过滤) | `node -e` 包一层 `cmd /c "pnpm exec vue-tsc --noEmit"` + `map/` 前缀过滤 | `map/` 下只有恰好一条基线 `renderer.ts TS2339 getLayerByAlias / IGameMap` 被豁免并打印;其余 0 条 |
|
||||
| 范围 | `node -e` + `git status --porcelain -uall -- packages-user/client-base/src/map` | 变更集恰好等于 6 条允许状态(5 个 ` M` + `?? rendererGate.test.ts`);`overlay.ts` / `overlay.test.ts` 不存在(未跟踪,无 `D` 行) |
|
||||
| 哈希基线 | `node -e` 读 `09-26-BASELINE.json` + `git hash-object` / `git rev-parse HEAD` | 7 个受保护文件哈希与 HEAD 与 Task 0 基线逐字相等;基线残缺即失败 |
|
||||
| 删除生效 | `fs.existsSync(overlay.ts / overlay.test.ts)` | 均为 `false` |
|
||||
| 未提交 | `git status` / `git rev-parse HEAD`(只读) | HEAD 与基线一致;无新提交;改动全在工作树 |
|
||||
| 浏览器行为 | **未执行** | 游戏尚不可运行;SUMMARY 明确写「无 WebGL E2E、UAT 跳过」 |
|
||||
|
||||
**明确不做:** 全量构建(`build:game` / `build:packages` / `build:lib`)、全仓类型修复、任何 git 写命令、任何依赖安装。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Task 0 已汇报并收到用户恰好「可以执行」的回复;OI-1 的全部命名已获批或按用户意见改名
|
||||
- [ ] `overlay.ts` 与 `__test__/overlay.test.ts` 已删除,`vertex.ts` 不再有任何 `./overlay` 依赖
|
||||
- [ ] 覆盖层状态全部为数值 / 对象键(`Map<IMapLayer, Map<number, …>>`),无 `Map<string, …>`、无模板字符串键
|
||||
- [ ] `setStaticAlpha` 方法体内只有「写 pending + 标值脏 + `requestUpdate()`」,无实例查找 / rect 改写 / 成员增删 / 排序
|
||||
- [ ] `getStaticAlpha` 在 pending 命中时返回待应用值
|
||||
- [ ] `getVertexArray()` 在 `checkRebuild()` 之后调用 `flushPendingAlpha()`;`getOverlayArray()` 顺序为 flush → sort → assemble;`isOverlayDirty()` 覆盖 pending / 顺序 / 值
|
||||
- [ ] `getOverlayCount()` 的「脏时需先取数组」契约写进 jsDoc,renderer 侧先取数组再取数量(该顺序在执行前已成立,Task 2 只补注释、不改代码)
|
||||
- [ ] 「排序后必然组装」不变式成立:`sortOverlay()` 结尾置 `overlayValueDirty = true`,`deleteMoving()` 同时置两个脏标记;已删除的动态块不会残留
|
||||
- [ ] `getIndexInBlock()` 已移到 `#region 分块操作` 末尾、位于三个覆盖层方法与 `#region 图块状态` 之前(私有方法先于调用者)
|
||||
- [ ] `updateTileVertex` / `updateVertexArray` 中不再有 `layerIndex` / `key` 局部变量,`getStaticOverlayKey` / `getDynamicOverlayKey` 方法本体已删除(eslint `no-unused-vars` 无 error)
|
||||
- [ ] 排序键为 `(layerIndex, y, x, kind)`,静态先于动态;`x` 未进入 z / 深度
|
||||
- [ ] 动态成员每次排序从 `renderer.getMovingBlock()` 现取,无登记表、无缓存副本
|
||||
- [ ] `updateTileVertex` / `updateVertexArray` / `checkRebuild` / `setBlockSize` 的成员刷新与清空到位
|
||||
- [ ] 两遍式、动态尾部裁剪、`blendFuncSeparate`、ping/pong 颜色+深度配对、0/1/2+ 链路、缩放 / 销毁、`map.frag` 的 `0.004`、OI-1 (A) 全部保留且未回退
|
||||
- [ ] 新增 WebGL 代码带解释性注释;新私有方法与字段有 jsDoc;既有注释一字未改
|
||||
- [ ] `rendererGate.test.ts` 重写为 29 个断言组且全绿
|
||||
- [ ] `eslint` 0 error;过滤后的 `vue-tsc` 仅豁免一条基线并打印原文
|
||||
- [ ] 范围闸门通过:`map/` 变更集恰好等于 6 条允许状态、两个 overlay 文件不存在(未跟踪文件删除无 `D` 行)
|
||||
- [ ] 哈希闸门通过:`09-26-BASELINE.json` 的 7 个哈希 + HEAD 与执行后逐字相等,并把基线哈希写进 SUMMARY
|
||||
- [ ] `09-26-SUMMARY.md` 写明「未提交代码 / 无 WebGL E2E / UAT 跳过 / estimate 未校准 / 保留项未回退」
|
||||
- [ ] 全程未运行任何 git 写命令
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md` when done
|
||||
</output>
|
||||
246
.planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md
Normal file
246
.planning/phases/09-minimum-browser-runnable/09-26-SUMMARY.md
Normal file
@ -0,0 +1,246 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 26
|
||||
subsystem: rendering
|
||||
tags: [webgl2, transparency, overlay, instancing, post-effect, vitest, source-gate]
|
||||
|
||||
requires:
|
||||
- phase: 09-minimum-browser-runnable
|
||||
provides: 09-25 的两遍式渲染、覆盖层草稿、postEffect 修正与渲染源码闸门基线
|
||||
|
||||
provides:
|
||||
- "覆盖层的状态、私有类型、阈值判定与 flush / sort / assemble 全部收敛进 vertex.ts,不新增导出类型或类"
|
||||
- "完全惰性的 setStaticAlpha:只写 pendingAlpha、标值脏、requestUpdate;实例与成员变化集中在取顶点数组时 flush"
|
||||
- "数值 / 对象键(Map<IMapLayer, Map<number, ...>>);动态成员每次排序从 renderer.getMovingBlock() 现取"
|
||||
- "排序后必然重新组装的不变式:sortOverlay() 置 overlayValueDirty,deleteMoving() 同时置两个脏标记"
|
||||
- "renderer.ts 按新契约在脏时先取数组再取数量,并补齐两遍式 / 混合 / FBO / ping-pong 的解释性注释"
|
||||
- "types.ts 的 getOverlayCount() 补充「脏时需先取 getOverlayArray()」的契约说明"
|
||||
|
||||
affects: [地图渲染后续计划, 半透明 / 后处理相关计划, Phase 10 文档]
|
||||
|
||||
actuals:
|
||||
tokens: 11729
|
||||
tasks: 5
|
||||
commits: 0
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "覆盖层唯一实现处:pendingAlpha / overlayStatic / 两个脏标记 / overlayOrdered / 暂存数组全部在 MapVertexGenerator 内"
|
||||
- "setStaticAlpha 惰性写 pending;getVertexArray() 在 checkRebuild() 之后 flush,是不透明遍上传前的唯一落地点"
|
||||
- "getOverlayArray() 顺序为 flush →(顺序脏则)sort →(值脏或顺序脏则)assemble;isOverlayDirty() 同时覆盖 pending / 顺序 / 值"
|
||||
- "排序键 (layerIndex, y, x, kind),x 只作 CPU 侧稳定次级键,不进入 z / 深度;静态先于动态"
|
||||
- "不重排分块张量与动态数组;覆盖层组装只拷贝 16 float 并覆盖前四个 rect 浮点"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- .planning/phases/09-minimum-browser-runnable/09-26-BASELINE.json
|
||||
modified:
|
||||
- packages-user/client-base/src/map/vertex.ts
|
||||
- packages-user/client-base/src/map/renderer.ts
|
||||
- packages-user/client-base/src/map/types.ts
|
||||
- packages-user/client-base/src/map/__test__/rendererGate.test.ts
|
||||
deleted:
|
||||
- packages-user/client-base/src/map/overlay.ts
|
||||
- packages-user/client-base/src/map/__test__/overlay.test.ts
|
||||
|
||||
key-decisions:
|
||||
- "删除 overlay.ts 与 overlay.test.ts(两者均未被 git 跟踪,删除后 git status 不会出现 D 行),能力全部并入 vertex.ts"
|
||||
- "setStaticAlpha 完全惰性;getStaticAlpha 先读 pendingAlpha,保持 IBlockStatus.getAlpha() 的即时语义"
|
||||
- "flushPendingAlpha() 放在 getVertexArray() 的 checkRebuild() 之后,保证不透明遍上传前矩形已就位并已 markRenderDirty()"
|
||||
- "sortOverlay() 清顺序脏后立即置 overlayValueDirty = true;deleteMoving() 同时置两个脏标记,杜绝已删除动态块的残影"
|
||||
- "动态成员不设登记表,每次排序从 renderer.getMovingBlock() 现取 index / x / y / layer"
|
||||
- "已知类型基线 renderer.ts TS2339 getLayerByAlias / IGameMap 按用户裁定不修;类型闸门精确豁免恰好一条并打印原文"
|
||||
- "按用户要求不运行任何 git 写命令,不更新 STATE.md / ROADMAP.md;代码改动全部留在工作树"
|
||||
- "不做浏览器 UAT、不宣称 WebGL E2E;游戏当前不可运行,SUMMARY 记为 status: partial"
|
||||
|
||||
patterns-established:
|
||||
- "待应用 alpha:Map<IMapLayer, Map<number, number>>,键为图层对象与地图索引,不拼接 static:/dynamic: 字符串"
|
||||
- "静态记录:Map<IMapLayer, Map<number, IOverlayStaticRecord>>,进入覆盖层时保存原 rect 并把实例 rect 置零"
|
||||
- "成员刷新:updateTileVertex 的 Position 路径刷新记录后再次置零;updateVertexArray 的空图块路径把记录 rect 置零"
|
||||
- "重建清理:checkRebuild / setBlockSize 清空 overlayStatic 与 pendingAlpha,清空排序缓存并把两个脏标记置真"
|
||||
- "覆盖层组装只读分块图层数组与 dynamicInstancedArray,从不修改分块张量"
|
||||
|
||||
requirements-completed: []
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "覆盖层实现收敛进 vertex.ts(无独立模块、无字符串键、数值嵌套 Map)"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "rendererGate.test.ts#keeps the overlay implementation inside the vertex generator"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "rendererGate.test.ts#stores overlay keys as numeric nested maps"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "惰性 setStaticAlpha、pending 优先读取与 flush 落点"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "rendererGate.test.ts#makes setStaticAlpha fully lazy"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "rendererGate.test.ts#flushes pending alpha when the vertex array is taken"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "顺序 → 组装不变式(sortOverlay 置值脏、deleteMoving 置两个脏标记)"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "rendererGate.test.ts#sorts by layer, y, x and kind and always reassembles afterwards"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "rendererGate.test.ts#takes moving members live and dirties both flags on deletion"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "两遍式 / 动态尾部裁剪 / blendFuncSeparate / 深度附件 / ping-pong 配对 / 0-1-2+ 链路 / 缩放与销毁 / map.frag 阈值 / OI-1 (A) 保留"
|
||||
verification:
|
||||
- kind: unit
|
||||
ref: "rendererGate.test.ts 的两遍式、混合、深度附件、配对、效果链路、裁剪与 dataX/dataY 用例"
|
||||
status: pass
|
||||
human_judgment: true
|
||||
rationale: "仅验证源码形状;WebGL 实际渲染结果未在浏览器验证,游戏当前不可运行"
|
||||
- id: D5
|
||||
description: "浏览器人工 UAT(静态半透明叠加、动态块遮挡与混合、26×13 跨分块、0/1/2+ 后处理、画布缩放)"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "用户明确不做浏览器 UAT;无 WebGL E2E 自动化。源码闸门通过不等于渲染结果已验证"
|
||||
|
||||
duration: 未精确记录
|
||||
completed: 2026-10-24
|
||||
status: partial
|
||||
---
|
||||
|
||||
# Phase 09: minimum-browser-runnable — Plan 26 Summary
|
||||
|
||||
**覆盖层实现全部收敛进 vertex.ts,setStaticAlpha 完全惰性、flush 落在取顶点数组时;未提交代码,浏览器 UAT 因游戏不可运行而跳过**
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- 删除 `overlay.ts` 与 `__test__/overlay.test.ts`;覆盖层的状态、私有接口、私有序枚举、阈值常量与判定函数、flush / sort / assemble 全部写在 `vertex.ts` 的 `MapVertexGenerator` 内,不新增导出类型或类。
|
||||
- `setStaticAlpha` 改为完全惰性:只写 `pendingAlpha`、置 `overlayValueDirty`、调用 `renderer.requestUpdate()`,不做实例查找、rect 改写、成员增删或排序。
|
||||
- `getStaticAlpha` 在读实例数组前优先返回 `pendingAlpha` 中的待应用值,保持 `IBlockStatus.getAlpha()` 的即时语义。
|
||||
- `getVertexArray()` 在 `checkRebuild()` 之后调用 `flushPendingAlpha()`;flush 写实例 alpha,跨阈值时保存 / 恢复 rect、增删 `overlayStatic` 记录,并对 rect 变化调用 `markRenderDirty()`。
|
||||
- `getOverlayArray()` 固定为 flush → sort → assemble;`isOverlayDirty()` 覆盖 pending / 顺序 / 值三类脏;`getOverlayCount()` 的「脏时需先取数组」契约写入 jsDoc,renderer 按该契约先取数组再取数量。
|
||||
- `sortOverlay()` 结尾置 `overlayValueDirty = true`;`deleteMoving()` 同时置 `overlayOrderDirty` 与 `overlayValueDirty`,保证「排序 / 成员变化后必然重新组装」,已删除的动态块不会残留。
|
||||
- 键改为 `Map<IMapLayer, Map<number, ...>>`;动态成员每次排序从 `renderer.getMovingBlock()` 现取,不保留任何登记表。
|
||||
- `renderer.ts` 补齐两遍式、动态尾部裁剪、分离式混合、FBO 深度附件、ping-pong 配对与效果链的解释性注释;`types.ts` 补充 `getOverlayCount()` 契约说明。
|
||||
|
||||
## 保留项清单(逐项保留,未回退)
|
||||
|
||||
- 两遍式与 `depthMask(true)` / `depthMask(false)` / 收尾 `depthMask(true)` —— 保留。
|
||||
- 不透明遍按 `Math.min(v.endIndex, dynamicStart)` 裁掉动态尾部 —— 保留。
|
||||
- `blendFuncSeparate(SRC_ALPHA, ONE_MINUS_SRC_ALPHA, ONE, ONE_MINUS_SRC_ALPHA)` —— 保留,未改回 `blendFunc`。
|
||||
- ping / pong 各自绑定自己的颜色纹理并挂 `DEPTH_COMPONENT24` 深度 renderbuffer —— 保留。
|
||||
- 0 / 1 / 2+ 效果链路(≥1 进 ping、最后一个效果写 `null`)—— 保留。
|
||||
- `setCanvasSize` 同时重设两个深度缓冲、`destroy` 删除两个 FBO / 两个纹理 / 两个深度 renderbuffer / `overlayBuffer` —— 保留。
|
||||
- `map.frag` 的 `0.004` —— 保留。
|
||||
- OI-1 (A) 的 `block.dataX` / `block.dataY` —— 保留。
|
||||
|
||||
## Task Commits
|
||||
|
||||
**无。** 按用户要求,本次执行未运行任何 git 写命令(无 `git add` / `commit` / `stash` / `checkout` / `switch` / `worktree` / `reset` / `restore`);所有代码改动保留在工作树,供用户自行审查。也未更新 `STATE.md` / `ROADMAP.md`。
|
||||
|
||||
## Verification
|
||||
|
||||
| 门禁 | 命令 | 结果 |
|
||||
|------|------|------|
|
||||
| 单测 / 源码闸门 | `pnpm exec vitest run packages-user/client-base/src/map/__test__` | 1 个测试文件、30 条用例全部通过 |
|
||||
| 代码风格 | `pnpm exec eslint vertex.ts renderer.ts types.ts rendererGate.test.ts` | 0 error / 0 warning |
|
||||
| 类型(按文件过滤) | `node -e` 包裹 `pnpm exec vue-tsc --noEmit`,仅取 `packages-user/client-base/src/map/` 诊断 | 仓库共 229 条诊断;`map/` 下恰好豁免 1 条基线,其余 0 条。豁免原文:`packages-user/client-base/src/map/renderer.ts(484,25): error TS2339: Property 'getLayerByAlias' does not exist on type 'IGameMap'.` |
|
||||
| 范围 | `node -e` + `git status --porcelain -uall -- packages-user/client-base/src/map` | 变更集恰好等于 6 条允许状态(5 个 ` M` + `?? __test__/rendererGate.test.ts`);`overlay.ts` / `__test__/overlay.test.ts` 均不存在(未跟踪文件删除后没有 `D` 行) |
|
||||
| 哈希基线 | `node -e` 读 `09-26-BASELINE.json` + `git hash-object` / `git rev-parse HEAD` | HEAD 与 7 个受保护文件的当前哈希与 Task 0 基线逐字相等,且基线恰好 7 项 |
|
||||
|
||||
说明:
|
||||
|
||||
- 类型闸门只对 `packages-user/client-base/src/map/` 负责,不覆盖全仓,也不修复已知的 `getLayerByAlias` 基线。
|
||||
- 范围闸门使用「6 条显式期望状态 + 两个 overlay 文件不存在」,因为两个 overlay 文件都是未跟踪文件,删除后不会有 `D` 行。
|
||||
- 哈希闸门使用「当前 `git hash-object` 值与 `09-26-BASELINE.json` 基线值的真值比较」,不是「哈希字符串出现在 SUMMARY」这类弱检查;SUMMARY 里的哈希只是审计痕迹。
|
||||
- `status.ts` 与 `shader/map.frag` 的 ` M` 是执行前既有状态(前者为用户自己的参数顺序修改、后者为 09-25 的阈值修改),允许存在,并由哈希闸门证明本计划未改动。
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### 1. [Rule 1 - 计数] 源码闸门为 30 条用例而不是计划所列的 29 条
|
||||
|
||||
- **Found during:** Task 3
|
||||
- **Issue:** 计划把「效果链路」合并为一条断言组,实现时把「最后一个效果写 `null`」与「第一个效果读 ping」拆成两条独立 `it`,以便失败时定位更精确。
|
||||
- **Fix:** 保留拆分;没有删除或放宽任何断言,`vitest` 30 条全部通过,用例数下限(≥26)满足。
|
||||
- **Files modified:** `packages-user/client-base/src/map/__test__/rendererGate.test.ts`
|
||||
- **Committed in:** 未提交(按用户要求)
|
||||
|
||||
### 2. [Rule 1 - 格式] 新建的 `rendererGate.test.ts` 需要 prettier 统一为 CRLF
|
||||
|
||||
- **Found during:** Task 3
|
||||
- **Issue:** 新建文件写入时为 LF,仓库要求 CRLF,`eslint` 报 356 条 `Insert ␍` 格式错误。
|
||||
- **Fix:** 对测试文件运行 `pnpm exec prettier --write`;仅换行与格式,语义与断言未变。
|
||||
- **Files modified:** `packages-user/client-base/src/map/__test__/rendererGate.test.ts`
|
||||
- **Committed in:** 未提交(按用户要求)
|
||||
|
||||
**Total deviations:** 2(1 计数、1 格式)
|
||||
**Impact on plan:** 均为实现细节,不改变设计、不扩大豁免、不删除用例。
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- **无浏览器 UAT**:游戏当前不可运行,用户明确不做 UAT;半透明叠加、动态块遮挡与混合、26×13 跨分块、0/1/2+ 后处理、画布缩放的视觉结果均未验证,**无 WebGL E2E**。
|
||||
- **同深度重叠的既定限制**:同一图层同行的不透明图块与覆盖层图块 z 相同,`depthFunc(LESS)` 的严格比较让先绘制者胜出;本计划不引入浮点深度编码去消解。
|
||||
- **地图重建丢失静态 alpha**:`checkRebuild` / `setBlockSize` 重新分配实例数组后 alpha 复位为 1;本计划只清空 `overlayStatic` 与 `pendingAlpha` 并标脏,不改变既有语义。
|
||||
- **pending 找不到实例时被丢弃**:目标图层不在当前渲染器或坐标越界时,`getIndexInBlock` 返回 `null`,该条 pending 被丢弃(不报错、不缓存)。
|
||||
- **动态块移动期间每帧重排**:动态成员每次排序现取,移动期间排序键变化会导致每帧重排;正确性优先,未做增量排序优化。
|
||||
- **`addPostEffect` 当前无调用方**:后处理链路只有源码闸门验证,未在真实效果链上运行。
|
||||
|
||||
## Known Stubs
|
||||
|
||||
无。
|
||||
|
||||
## User Setup Required
|
||||
|
||||
待游戏可运行后补浏览器 UAT:
|
||||
|
||||
- 静态半透明图块叠加与恢复(`setAlpha` 跨 1 的往返)。
|
||||
- 动态块(勇士 / 动画)的遮挡与混合顺序。
|
||||
- 26×13 跨分块地图的定位与置零(`dataX` / `dataY`)。
|
||||
- 0 / 1 / 2+ 后处理效果链路。
|
||||
- 画布缩放后的深度缓冲与覆盖层绘制。
|
||||
|
||||
## Hash Evidence(未被触碰的文件与 HEAD)
|
||||
|
||||
以下数值逐字来自 Task 0 写下的 `09-26-BASELINE.json`,执行结束时重新计算并逐项相等:
|
||||
|
||||
- HEAD: `c5fca9e777d1750daeaea78ef0152bba31dd8c18`
|
||||
- `status.ts`: `9236bc34330d0709a0c26884f967ef77f41c053b`
|
||||
- `moving.ts`: `a61aa951771e0c4d103dbf04b789b380354c8767`
|
||||
- `viewport.ts`: `3c8f224937f2a9743689f0ffba51cbfb3e07b2f3`
|
||||
- `block.ts`: `629099e1970ec38ab93e4e29e4949d424f3e9d93`
|
||||
- `element.ts`: `bc9e9d9d01d889cf275ea9a5336454072438af69`
|
||||
- `constant.ts`: `d29a817717f668f615ddf4b3d195ee5d64f77df1`
|
||||
- `shader/map.frag`: `d8f3df3705d8c999a6ea033bd45039e5d91e6fea`
|
||||
|
||||
与 Task 0 基线逐字相等、HEAD 未变;未改依赖、配置、其它包、09-01..09-25 的计划与 SUMMARY、`09-CONTEXT.md`。
|
||||
|
||||
## 估算说明
|
||||
|
||||
`estimate.tokens = 80000`(`raw_tokens` 同为 80000,`tasks = 5`,`confidence: low`)为**未校准估算**;本计划按用户要求保持单计划、不拆分。09-25 的 160000 超预算声明本计划不再沿用。`actuals.tokens = 11729` 是按工作树 diff(3 个源码文件 + 新测试文件)字符数 / 4 的粗略值,同样未校准。
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- 覆盖层已无独立文件,状态与逻辑唯一收敛在 `vertex.ts`;惰性 setter、flush 落点、排序后必然组装、数值嵌套键与动态现取均已由源码闸门覆盖。
|
||||
- 尚未完成的验证:浏览器 UAT 与任何 WebGL E2E。**源码闸门通过不等于渲染结果已验证**。
|
||||
- 代码未提交,`STATE.md` / `ROADMAP.md` 未更新(按用户要求)。
|
||||
|
||||
---
|
||||
|
||||
*Phase: 09-minimum-browser-runnable*
|
||||
*Plan: 26*
|
||||
*Completed: 2026-10-24(代码任务 1–4;UAT 跳过,status: partial)*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- `overlay.ts` / `__test__/overlay.test.ts` 均不存在:PASSED
|
||||
- `09-26-BASELINE.json` 存在且恰好 7 个哈希 + HEAD:PASSED
|
||||
- `09-26-SUMMARY.md` 存在且包含全部必需声明:PASSED
|
||||
- `map/` 变更集恰好等于 6 条允许状态:PASSED
|
||||
- HEAD 与基线一致、7 个受保护文件哈希逐字相等:PASSED
|
||||
- 全程未运行任何 git 写命令,代码全部保留在工作树:PASSED
|
||||
- 按用户要求未创建任何提交,因此无提交哈希可核对:N/A
|
||||
Loading…
Reference in New Issue
Block a user