chore: checkpoint web Blender feature-gap work, docs and tests
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

- update native web_engine sources and rebuilt wasm artifacts
- add task cards, status notes and tooling scripts
- add web e2e specs, tutorial UI sources and tutorial assets
- sync golden task manifests and audit records

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
mes123456
2026-11-11 10:46:55 +08:00
parent f3e21c6b98
commit d1e6b63d54
665 changed files with 59149 additions and 39194 deletions

View File

@@ -0,0 +1,75 @@
# 机械零件套装互动教学(PARTS_TUTORIAL)
## 用户入口
- `/?tutorial=parts`:React 图文落地页(课程介绍、零件卡片、课堂实录截图、开始/继续/重置)。
- `/?tutorial=parts&start=1`:真实 Web Blender(本地 WASM Main)+ 引导侧栏。
- 帮助 → 零件建模教学:在新标签页打开落地页。
## 已实现
- 21 步课程(0 引导、19 个必需步骤、完成总结),覆盖:
- 创建与变换:立方体、圆柱、贝塞尔圆环、文字、Empty;缩放/位置、应用缩放。
- 编辑模式:顶面内插、挤出、沉台、轴肩、六角头细节、支架。
- 修改器与布尔:Bevel、Boolean(Difference,刀具对象指针)、Array、Solidify、Mirror、Subsurf。
- 层级、材质与交付:集合、父子、三种材质并按槽位分配、复制、保存 `.blend`、导出 GLB。
- “演示本步”通过同一原生 Main 执行:Python 使用 `bpy`;对象合并(底座开孔刀具、螺栓头+杆)改为 typed `joinObjects`,与产品 Object → Join 同一路径。
- 实时校验读取 SceneIR:节点名称/类型/父子、变换、网格包围盒、修改器栈与参数、布尔操作数、集合成员、材质槽。
- 保存/导出校验绑定 App 的 `userAction` 状态(SAVE/SAVE_AS/EXPORT),不以 `dirty=false` 单独判定。
- 进度持久化:
- OPFS `tutorial/parts-progress.json` 为权威记录,localStorage 仅为同步镜像/降级;OPFS 写入串行化避免旧写覆盖新写。
- 每步推进后写入 OPFS 恢复快照(`saveSnapshot`),重新进入教程时从 OPFS 恢复工程并回到记录的步骤。
- 有头 Chromium 截图:`tools/web/capture-parts-tutorial-screenshots.mjs` 执行全部步骤并输出
`web/app/public/tutorial/parts/steps/<stepId>.jpg`(含引导侧栏的整页图)与 `<stepId>-view.jpg`(视口参考图)。
- 教程端到端测试:`web/tests/e2e/wbv2-parts-tutorial-production.spec.ts`。
## 本批修复的真实缺口
1. 引导入口就绪竞态:侧栏在默认工程尚未打开时就能点击“演示本步”,Python 请求被引擎拒绝
(`Invalid embedded Python request`)。现在侧栏暴露 `ready/busy`,场景就绪前禁用演示与下一步。
2. 修改器 SceneIR 元数据缺失(桌面源码对齐):
`web_engine_blend_main_merge_modifier_snapshot` 中有修改器的对象被错误地 `continue` 跳过,
导致 `editableParameters` 从未合并,布尔修改器的 `operation`/`object` 无法读取(教程布尔校验失败)。
已删除该反向早退,保留“有修改器时不做原始法线回填”的原有条件。
增量编译 `cmake --build build_web_blender6 --target web_engine -j8` 并
`node tools/web/sync-web-engine-runtime.mjs`;同步后的 `web_engine.wasm` 哈希
`4cb478454c8cbc2936e346224820c2f56fc70645cb3b8d3237f05b96bd27c6ac`。
3. 保存校验:教程把 `SAVE`/`SAVE_AS` 的 userAction 状态与 `dirty` 结合,避免“新工程未保存”误判。
4. 合并/父子路径:演示中的 Join 改为 typed `joinObjects`(与 Object → Join 一致);assemblies 的父子改为 typed `setParents`(与 Object → Parent 一致),避免 `bpy.ops.object.join` / `parent_set` 在无头上下文中的行为差异。
5. OPFS 进度串行写 + 恢复前置:OPFS 记录先于本地镜像落盘,加载完成后再决定是否恢复工程,避免空进度先占位。
6. Web 业务工具模式下的 GLB 导出失败:SceneIR 会列出 `object.convert` 后遗留的孤立曲线数据块;
导出前对任意 `nonMeshData` 都调用 `evaluateDepsgraph`,在没有原生 GPU 画布的 Web 业务工具模式下该评估不返回;
即使跳过评估,导出器也会因为这些未求值数据报 `NON_MESH_EVALUATION_REQUIRED` 而不生成 GLB。
现在导出前只保留被场景节点引用的非网格数据(孤立数据块丢弃),仅对实际使用的数据评估;
渲染能力查询也改为最长等待 3 秒的保守探测,避免无 GPU 画布时永久挂起。
7. GLB 导出被未使用材质阻塞:导出器会校验 SceneIR 里的全部材质,而启动模板带有未使用的
`Dots Stroke` 材质(没有 Principled/Output 节点图),导致整个导出报 `SHADER_GRAPH_UNMAPPABLE`。
桌面 glTF 导出器只收集被导出对象引用的材质;现在导出前同样只保留被场景网格材质槽引用的材质。
默认场景复测导出 3996 字节 GLB;教程完整场景导出 31264 字节。
8. “继续学习”未恢复工程:启动模板在新会话中复用同一个 projectId,侧栏用
`progress.projectId === context.projectId` 判断“已是学习工程”,于是跳过 OPFS 恢复,停留在默认立方体场景。
教程编辑器的挂载恢复只应比较“是否存在学习进度”,现已移除该 id 相等早退;
生产 spec 验证恢复后 `[data-testid="scene-stats"]` 显示 `Active 底座`,状态栏显示 `Recovery: baseline r2`。
(Outliner 集合折叠时行不在 DOM,恢复断言因此改用场景统计而不是 treeitem。)
## 验证状态
- 有头 Chromium 全流程(2026-11-11 05:14,`capture-parts-tutorial-screenshots.mjs --dev --port 5191`):
21 步全部通过,`status: PASS`,`errors: []`;保存与 GLB 导出检查均为绿色,
截图写入 `web/app/public/tutorial/parts/`(21 张步骤图 + 21 张视口图 + 落地页/总览图)。
- `npm run typecheck`:exit 0(缓存指纹在 App.tsx 修改后重新生成)。
- `npm run build`:exit 0,教程 chunk `PartsTutorialLanding-*.js`、`usePartsTutorialProgress-*.js` 正常产出。
- 生产无障碍 spec:通过(落地页 + 首个练习步骤的引导侧栏 axe 检查无 critical/serious 问题);
测试从引导页进入 `base-rename` 后检查 `.tutorial-checks[aria-live=polite]`。
- 生产端到端 spec:覆盖“演示 → 下一步 → OPFS 进度 → 刷新后继续学习 → OPFS 恢复工程 →
保存 .blend 与导出 GLB”,并直接断言 OPFS `tutorial/parts-progress.json`、
`projects/<id>/snapshots/*.blend` 与 `projects/<id>/scene.blend`。
- 2026-11-11 最终生产构建 4 个 spec(parts/reducer 生产与无障碍)全部通过;
parts 恢复断言改用 `scene-stats`,下载用事件队列按扩展名匹配。
## 已知限制
- 编辑模式演示走 `bpy.ops` 的 EXEC 路径;循环切割滑动、切割刀、模态倒角/内插等模态工具未纳入课程。
- 完成总结不含“应用全部修改器”步骤;GLB 导出前不强制烘焙修改器。
- 材质按对象槽位分配,未覆盖共享材质 data-block 的分配 UI。
- 环境阻塞的既有缺口(OpenVDB voxel remesh、Ocean FFTW3、light-probe bake)不在本教学案例范围内。

View File

@@ -0,0 +1,81 @@
# 二级减速器互动教学(REDUCER_TUTORIAL)
## 用户入口
- `/?tutorial=reducer`:React 图文落地页(结构、零件卡、课堂实录、课程树、开始/继续/重置)。
- `/?tutorial=reducer&start=1`:真实 Web Blender(本地 WASM Main)+ 引导侧栏。
- 帮助 → 二级减速器教学:在新标签页打开落地页。
## 已实现
- 35 步课程(引导 + 33 个必需步骤 + 完成总结),覆盖:
- 箱体/箱盖:立方体、应用缩放、删面 + Solidify、Mirror + Boolean UNION 箱脚、
圆柱孔刀 + Join + Boolean DIFFERENCE 轴承孔、Inset/挤出止口、Mirror 螺栓孔、Bevel。
- 传动件:三根轴(圆柱 + 轴颈 + Join + 键槽布尔)、四个齿轮
(圆柱轮坯 + 齿槽刀 + Empty 对象偏移 Array + Boolean 切齿 + 轴孔/键槽布尔 + Bevel)、
Torus 套圈 + 滚球轴承。
- 标准件与附件:端盖、平键、六角法兰螺栓、通气帽、油标、放油塞、文字转网格铭牌(Subsurf 底板)。
- 装配与交付:集合层级、轴组件 Empty + 父子、根 Empty 总装、四种材质与槽位、Linked Duplicate、
保存 `.blend`(OPFS + 下载)与 GLB 导出。
- **课程工程身份**:reducer 首次进入时通过 `ensureCourseProject("tutorial-reducer", "二级减速器.blend")`
把默认场景写入 OPFS 课程工程(复用 `persistProject`,不触发下载),
检查点写 `projects/tutorial-reducer/snapshots`,与 parts 课程的快照链隔离。
- **React TreeView**:新增 `tutorial/TutorialTreeView.tsx`(role=tree/treeitem、方向键/Home/End、
展开/折叠、状态标记),用于:
- 落地页课程结构(章节 → 步骤);
- 装配章节的“实时装配树”(按 SceneIR 的集合/父子关系生成)。
- **课程化改造**:新增 `tutorial/course.ts`,把课程 id、步骤、进度文件名、资源目录、
保存/总结步骤、文案与零件卡参数化;parts 组件改为薄包装,parts 行为不变。
- **进度与恢复**:OPFS `tutorial/reducer-progress.json` 为权威记录,localStorage 镜像;
恢复前先读 OPFS 进度,恢复失败显示提示;首次推进前串行完成“恢复或确保课程工程”。
## 本批修复的真实缺口
1. 课程工程身份与就绪竞态:引导页的“下一步”可能在 `ensureCourseProject` 启动前被点击,
从而把错误的默认工程 id 写进进度并排队检查点。现在侧栏把 `projectState`(idle/preparing/ready/failed)
纳入 `ready` 门控,串行完成“恢复或确保课程工程”后才允许演示与推进;失败时显示提示并允许继续练习,但不写错误 id。
2. 箱体轴承孔校验:箱体在箱脚步骤已有一个 Boolean(Union),按类型取第一个 Boolean 会命中箱脚而不是轴承孔。
校验现在按操作数“箱体孔刀”匹配 Boolean 修改器。
3. 轴与轴承的局部坐标:输入轴与轴承的旋转未应用时,SceneIR 网格包围盒沿局部 Z 轴,
长度/外径校验误判。演示现在在 Join 前应用旋转(与桌面建模习惯一致)。
4. 编号对象计数:`findNodes` 的名称后缀规则只接受 `.001` 形式,无法匹配“轴承1…6/端盖1…6/键1…4/法兰螺栓1…9”。
计数校验改为按 `^轴承\d+$` 等编号名称匹配。
5. 齿轮齿槽阵列与布尔:Boolean 操作数上的**活动 Array 修改器**在 Web Main 的实时视口中只切出一条齿槽
(17 份阵列未被布尔求值)。已做对照:即使齿槽刀保持可见,活动 Array 仍只切一条,可见性不是原因;
在布尔前 `bpy.context.view_layer.update()` 后应用齿槽阵列则得到正确齿形。
课程因此改为“阵列 → 应用 → Boolean”;齿轮1 的 `-view` 截图显示 17 条齿槽,齿轮2 显示 34 齿结果。
这仍是引擎的实时操作数求值缺口(已记录在“已知限制”),应用阵列是桌面同样可用的工作流。
6. 重场景的保存竞争:减速器场景较大,通用 autosave 与教程检查点同时序列化会长时间占用项目锁,
导致“下一步”在完成检查后仍被禁用。现在课程激活时停用通用 autosave,教程检查点串行化(只允许一个在跑),
重课程每 3 次推进写一次检查点(零件课程保持每步);“下一步”不再等待后台保存。
代价是最多可能丢失 2 步的场景快照,进度步骤仍每步写入。
7. 材质创建校验:未分配到任何对象的材质不会出现在 SceneIR,四种新材质的颜色校验全部失败。
课程在创建步先给箱体/输入轴/轴承1/箱盖各分配一个代表材质,下一步再分配其余零件。
8. 实时装配树可读性:SceneIR 中的隐藏辅助对象(Mirror/Array 控制 Empty、隐藏切刀)会淹没装配层级。
React 树现在过滤 `visible=false` 的对象与空集合,仅显示可见装配层级。
## 验证状态
- 有头 Chromium 全流程(2026-11-11,`capture-parts-tutorial-screenshots.mjs --course reducer --dev --port 5218`):
35 步全部通过,`status: PASS`,`errors: []`;保存与 GLB 导出检查均为绿色
(GLB 2,674,332 字节,31 条相机/灯光等警告);齿轮1 视口参考图确认 17 条齿槽。
截图:`web/app/public/tutorial/reducer/landing.jpg`、`steps/<id>.jpg`、`steps/<id>-view.jpg`、`00-assembly.jpg`
(35 步共 70 张步骤图 + 总览;落地页已在全部步骤截图生成后重新截取)。
- `npm run typecheck`:exit 0;`npm run build`:exit 0(含 `PartsTutorialLanding-*.js` 课程 chunk)。
- 生产构建上的 4 个 spec 全部通过(2026-11-11,`playwright.deployment-local.config.ts`,workers=1,48.1s):
- `wbv2-reducer-tutorial-production.spec.ts`(19.9s):落地页/课程树 → 开始 → `tutorial-reducer` OPFS 工程 →
箱体演示 → 齿轮毛坯/阵列/布尔 → 刷新继续 → OPFS 恢复(Active 齿轮1)→ 保存 `.blend` + GLB 下载与 magic →
`projects/<savedProjectId>/scene.blend`。
- `reducer-tutorial-accessibility.spec.ts`(4.5s):落地页与课程树 axe、装配步骤的实时 React 树 axe。
- `wbv2-parts-tutorial-production.spec.ts`(18.6s)与 `parts-tutorial-accessibility.spec.ts`(4.4s):零件课程回归。
- 无障碍修复:课程目录序号颜色 #7d8287→#a6abb0(对比度 4.45→通过 4.5),装配树元信息同样改为 #a6abb0。
## 已知限制
- Boolean 的**活动** Array 操作数在 Web Main 实时视口中只求值一份;课程在布尔前应用阵列。
待补:按桌面源码检查 Boolean 操作数的求值依赖(`MOD_boolean.cc` 的 `DEG_OB_COMP_GEOMETRY`
与 Array 的 `offset_ob` 关系),并加回归用例。
- 齿轮为教学简化齿形(直齿、无渐开线修形),用于讲解阵列+布尔流程,不用于强度/噪声分析。
- 轴承为简化模型(内外套圈 + 8 个滚球),未建模保持架与游隙。
- 端盖为简化圆盘+凸台,未做密封槽与标准螺栓孔阵列。
- 油标/放油塞/通气帽为示意模型;法兰螺栓数量与位置为教学布置。

View File

@@ -0,0 +1,7 @@
# WBV2-P1424-900 status
- task: `WBV2-P1424-900`; parent: `WBV2-P1424-001`; status: `done`
- operation: `SUPERSEDE_STALLED_TASK_FOR_DOMAIN_PARITY`
- preserved: `WBV2-P1424-001` manifest `212423` bytes, status `in_progress`, continuance `tests/golden/WBV2-P1424-001/continuation-20261103-goal-numpy.json`
- successor: `WBV2-P1425-000`; plan: `tests/golden/WBV2-P1425-000/domain-parity-plan.json`
- nextTask: `WBV2-P1425-000`

View File

@@ -0,0 +1,7 @@
# WBV2-P1425-000 status
- task: `WBV2-P1425-000`; parent: `WBV2-P1424-900`; status: `done`
- operation: `AUTHORIZE_DOMAIN_PARITY_EPOCH`; module: `OBJECT_ADD_ROUTE`
- preserved: `WBV2-P1424-001` manifest remains `in_progress` (`212423` bytes); continuation `tests/golden/WBV2-P1424-001/continuation-20261103-goal-numpy.json`; acceptance `NOT_CLAIMED`
- new plan: `tests/golden/WBV2-P1425-000/domain-parity-plan.json`; bound source units: `15`
- nextTask: `WBV2-P1425-001`

View File

@@ -0,0 +1,88 @@
# WBV2-P1425-001 status
- task: `WBV2-P1425-001`; module: `OBJECT_ADD_ROUTE`; status: `in_progress`
- creation
- Add menu uses `web_blender.desktop_operator(native=true)` with revision/scene/area/mode/selection context.
- Mesh primitives incl. Circle/Grid/Monkey; NURBS curves/surfaces; non-mesh types and enum submenus; Empty display types; Grease Pencil line-art types; random hair; Edit-mode Add; generic F3; quick-toolbar primitives.
- hierarchy
- Commands/UI: `moveCollection` (including sibling ordering), `deleteCollection` (normal/recursive), `createCollection` child, `setActiveCollection`, `setScene`, `setSceneCollectionSettings`.
- Outliner collection drag/drop; Collection Properties delete/child/active/instance/up/down; per-ViewLayer exclude/holdout/indirectOnly/hide; topbar scene selector and scene delete; view layer add/remove; parent types and clear modes.
- modeling
- Mesh element selection bridge and post-operator readback; curve control-point selection bridge.
- Broad mesh/curve/UV operator coverage; selection-aware modal tools; UV projection/select/stitch/pack; vertex groups; shape keys; modifier Apply/Shape Key/Duplicate/Copy; object shading/convert/origin/transform/data/pivot/snap.
- Modifier whitelist expanded by 20 source-backed types: WeightedNormal, CorrectiveSmooth, LaplacianSmooth, Mask, NormalEdit, Skin, UVProject, UVWarp, Warp, DataTransfer, WeightVGEdit, WeightVGMix, WeightVGProximity, LaplacianDeform, ParticleInstance, Surface, MeshCache, MeshSequenceCache, Multires, Explode.
- implementation coverage: 486 operator units in five families have an implementation route (317 menu/modal, 26 typed command, 143 F3 operator palette). `MESH_OT_` 164/164, `OBJECT_OT_` 215/215, `CURVE_OT_` 51/51, `SURFACE_OT_` 6/6, `UV_OT_` 50/50 classified `WEB_MAIN` in the route catalog; behavioral evidence remains separate and `UNVERIFIED` for the broader catalog.
- artifacts: `implemented-routes.json`, `implemented-unit-bindings.json`, `implementation-summary.json`, `docs/plans/WEB_BLENDER_THREE_DOMAIN_VALIDATION_CHECKLIST.md`.
- build: WASM rebuilt with the project environment and absolute paths; runtime synced (wasm SHA-256 `8ec2268f73584215419b72292cda0bc8fb055a9e9044224722077bfe3f43987d`). Command registry check PASS (170 commands, 161 WEB_MAIN routes). Frontend incremental build passed with `WBV2_SKIP_TYPECHECK=1`.
- blockers fixed in this round:
- `editors/mesh` registers `mesh.convex_hull` under `WITH_BULLET`, but the Web `bf_bmesh` target omitted Bullet and therefore did not register the `convex_hull` BMO. Executing it ran a null operator type and crashed WASM (`table index is out of bounds`); SKIN used the same path. The WITH_WEB branch of `bmesh/CMakeLists.txt` now links `bf_intern_rigidbody` and `bf::dependencies::optional::bullet`.
- Speaker `distance_max` defaults to `FLT_MAX` in DNA while SceneIR budgets it to 1e9; the blend reader now clamps distance fields, and empty `actionSlotIdentifier` values (sound strips created by Speaker Add) are omitted instead of failing the schema.
- Add → 数据实例 now resolves the active object's data-block name and ID type from SceneIR before invoking `object.data_instance_add` (invoking it operand-less is correctly cancelled).
- Embedded the Essentials geometry-node bundles (`geometry_nodes_essentials`, `geometry_nodes_dynamics_assets`, `procedural_hair_node_assets` + catalog) under `/blender/datafiles/assets`. The browser asset index does not list packaged-FS files, so `add_essential_asset_modifier` now falls back to appending the requested NodeTree directly from the embedded .blend; hair curves Add completes with the Capture Rest Geometry and Hair Dynamics modifiers.
- Modifier parameter changes only wrote DNA and left the embedded Python depsgraph’s evaluated copy stale, so `object.modifier_apply` baked the previous level. Updated parameter publication now uses the concrete modifier RNA struct, full recalc flags (`GEOMETRY | SYNC_TO_EVAL | PARAMETERS`) and a notifier, re-tags after the SceneIR refresh, and the operator bootstrap re-syncs modifier RNA once before native execution. SUBSURF levels 2 evaluates 98 vertices, Apply bakes 98, and save/reopen keeps 98.
- Saving while an object was in Edit mode omitted the edit data (e.g. Armature edit-bones). The save export now calls `ED_editors_flush_edits` before serialization, matching desktop save; added/subdivided/duplicated bones and hair-curves modifiers persist across save/reopen.
- SceneIR published every Object in Main, so a newly created empty scene still listed the other scene’s objects and native operators failed with `OPERATOR_SELECTION_CONTEXT_UNAVAILABLE`. The reader now emits nodes only for objects reachable from the active scene’s master collection, and `setScene` also points every wmWindow at the selected scene. `scene.new` → switch → `scene.delete` now publishes 0 nodes for the empty scene, 3 for the original and returns to `Scene` after deletion.
- Library append read the source from memory but left a Library entry pointing at a browser locator; snapshot/save packing could not find that path and failed with `LIBRARY_DEPENDENCY_UNAVAILABLE`. The append path now retains the uploaded source bytes in `recovery_library_bytes` keyed by the missing-library path, so append, undo/redo, save and reopen work.
- Collection instances published only an EMPTY owner. The reader now emits `instanceCollectionId`, `SceneNodeIR` validates it, and the Three.js viewport recursively expands nested collection instances with composed matrices, respecting collection/member visibility and depth/cycle limits. Display copies share member geometry/materials; the viewport also draws camera frustums and empty axes, so instanced cameras and empties are visible. Picking reports the nearest instance owner, so nested instance contents select their own instance object. Save/reopen keeps the reference.
- `uv.select_overlap` now works on Web: the WASM call trapped with `'unreachable'` while constructing `blender::Array<ChangedInfo>` from its bitfield initializer. The local `ChangedInfo` array now uses `std::vector`, behavior-equivalent; the operator finishes and the 60-case matrix has no open operator failure.
- `object.shade_auto_smooth` now works on Web: the embedded Essentials bundle is not indexed by the asset system, so the operator cancelled with “No asset found … Smooth by Angle”. With `WITH_WEB` it fetches the bundle, suppresses the false asset report, direct-appends the NodeTree with `WM_file_append_datablock`, and validates it as the Smooth by Angle group.
- The full `mathutils` Python surface is available again on Web: the `MATH_WEB_CORE_ONLY` compile definition was removed from `bf_python_mathutils`, so `mathutils.geometry/interpolate/noise/bvhtree/kdtree` are registered. `uv.lightmap_pack` (`mathutils.geometry.box_pack_2d`) now finishes instead of raising `AttributeError`.
- Native canvas replacement no longer breaks GPU resources. The Web/native toggle destroys the old WebGL context, but a retained vert/index buffer from that context made the next modal frame throw “Blender WebGL2 drawing resource belongs to another canvas”. `WebGLObjects::require_owner` now drops the dead GL names and lets `bind()` re-create and re-upload from the retained CPU bytes, so the next canvas draws normally.
- Added a Web “调整上一步操作” panel for operators invoked through the native channel. It records the operator, schema and created objects; applying edits deletes the created objects and re-runs the operator with converted RNA values from the original context. Non-creating operators fall back to undo + re-invoke guarded by the history cursor. The panel’s “完整属性…” action opens the existing F3 operator form preselected with the recorded operator and values, so pointer, collection, nested-group, enum-set and matrix properties are editable in the same adjustment flow.
- operator sweep: `web/tests/e2e/wbv2-operator-sweep-production.spec.ts` invokes every registered operator in the native `mesh`/`object`/`uv`/`curve` families (167/250/55/51 identifiers extracted from the desktop registry `bpy.ops.*`) through `desktop_operator` with a fresh Main per case, classifies EXECUTED / CONTEXT_POLL / CONTEXT_CANCEL / CONTEXT_MESSAGE / ERROR / TIMEOUT / CRASH and writes `verification/operator-sweep-<family>.json` bound to the WASM hash. The mesh family sweep is running; results and any real failures will be recorded per family.
- verification PASS (Chromium, focused slices):
- `web/tests/e2e/wbv2-add-menu-native-production.spec.ts`: Add menu → native `mesh.primitive_monkey_add` → Main/SceneIR → Outliner shows Suzanne/Monkey.
- `web/tests/e2e/wbv2-three-domain-focused-production.spec.ts` creation: base 1 → create 2 → undo 3 → redo 4; saved 421,265 bytes; reopen revision 1 with `VerifyCube` present.
- hierarchy: create/move/active/delete revisions 3-6, undo 7, redo 8; saved 417,882 bytes; reopen revision 1 with `VerifyParent`.
- modeling: meshTriangulate 2, undo 3, redo 4; SUBSURF add 5, remove 6; saved 417,327 bytes; reopen revision 1.
- `web/tests/e2e/wbv2-mesh-native-operator-ui-production.spec.ts`: Web mode switch → Edit mode → meshSelection bridge → native `mesh.poke`; status `已执行:mesh.poke`.
- `web/tests/e2e/wbv2-modifier-matrix-production.spec.ts`: all 20 expanded modifier types add/evaluate/remove/save/reopen, every case `FINISHED` (including SKIN after the Bullet linkage fix).
- `web/tests/e2e/wbv2-convex-hull-native-production.spec.ts`: Edit mode → `mesh.select_all` → native `mesh.convex_hull` state `DONE`.
- `web/tests/e2e/wbv2-native-operator-matrix-production.spec.ts`: 60/60 mesh/UV/object operators `FINISHED` on WASM `8ec2268f`, including shade_auto_smooth, uv.lightmap_pack and uv.select_overlap after the asset, mathutils and overlap-container fixes. The receipt records the WASM SHA-256. UV-editor-only/cursor/active-face operators and no-op selection clears are documented as inapplicable to the detached Layout/VIEW_3D fixture, not counted. Earlier test-only failures were corrected against the native RNA registry (`mesh.vertices_smooth`, `object.hide_view_set` without `clear`, `mesh.unsubdivide` without `threshold`, etc.).
- `web/tests/e2e/wbv2-creation-object-types-production.spec.ts`: 29 creation/data cases on WASM `8ec2268f` — all six `SURFACE_OT_` primitives (surface, sphere, circle, curve, cylinder, torus), five curve primitives (Bezier, NURBS curve, NURBS path, Bezier circle, NURBS circle) and the mesh/text/armature/empty/camera/light/metaball/volume/lattice/GP/point cloud/hair curves/speaker/light probe/effector types each produced a SceneIR node; operand-less `object.data_instance_add` remains cancelled with no mutation. Receipt records the WASM SHA-256.
- `web/tests/e2e/wbv2-data-instance-add-production.spec.ts`: Outliner selection → Add → 数据实例 creates `Cube.001` through the native ID lookup.
- `web/tests/e2e/wbv2-hair-curves-add-production.spec.ts`: hair curves Add appends both embedded Essentials node groups; surface gains `Capture Rest Geometry`, Curves object gains `Hair Dynamics` and is parented to `BasicCube`.
- `web/tests/e2e/wbv2-modifier-parameters-apply-production.spec.ts`: SUBSURF levels 2 evaluates 98 vertices through `evaluateDepsgraph`, undo restores level 1 / 26 vertices, redo restores level 2 / 98, native `object.modifier_apply` bakes 98 and removes the modifier, save/reopen keeps 98.
- `web/tests/e2e/wbv2-modifier-apply-convert-production.spec.ts`: BEVEL width 0.1/segments 2 evaluates 56 vertices and `object.modifier_apply` bakes 56; SUBSURF level 2 followed by `object.convert(target=MESH)` bakes 98 and clears the stack.
- `web/tests/e2e/wbv2-curve-edit-operators-production.spec.ts`: Bezier create → Edit → 32 curve operators all `FINISHED` on WASM `1e6e7d3f` (added select more/less/next/previous/random/similar/linked, de_select_first/last, smooth radius/tilt/weight, spline_weight_set, shade smooth/flat, dissolve_verts, split, separate, hide, normals_make_consistent) and the receipt records the WASM SHA-256. `curve.select_nth`/`curve.select_row` are desktop-inapplicable to a default Bezier circle (same native errors: no active point / poll context incorrect) and are documented instead of counted. Fixed the menu enum value `AUTO` → `AUTOMATIC`.
- `web/tests/e2e/wbv2-convert-to-mesh-production.spec.ts`: Bezier (13 verts), NURBS surface (256 verts / 225 faces) and default Text (177 verts / 171 faces) all convert to `MESH` through `object.convert`.
- `web/tests/e2e/wbv2-armature-edit-production.spec.ts`: Armature create → Edit → primitive add (1→2 bones), subdivide (1→2), duplicate (1→2), calculate roll (count unchanged) all `FINISHED`; save while in Edit mode and reopen preserves the new bone count.
- `web/tests/e2e/wbv2-object-tools-production.spec.ts`: two shape keys, one vertex group, modifier copy (two SUBSURF entries) and SIMPLE_DEFORM apply-as-shape-key (2 keys, modifier kept).
- `web/tests/e2e/wbv2-hair-curves-add-production.spec.ts` now also saves and reopens: the Curves object keeps `Hair Dynamics`, its parent and the surface’s `Capture Rest Geometry`.
- `web/tests/e2e/wbv2-edit-mode-persistence-production.spec.ts`: `mesh.poke` in Edit mode grows the cube from 8 to 14 vertices, and save/reopen preserves 14.
- `web/tests/e2e/wbv2-scene-flow-production.spec.ts`: `scene.new` publishes 0 nodes for the empty scene, switching back publishes 3, switching to the empty scene publishes 0, and `scene.delete` returns to `Scene` with 3 nodes.
- `web/tests/e2e/wbv2-collection-instance-production.spec.ts`: the instance EMPTY publishes `instanceCollectionId: collection:Collection`; the Web `ViewportRenderer` creates 3 member display copies (mesh + light + camera frustum) for the 3-object collection and 1 base-scene helper; save/reopen keeps the reference.
- `web/tests/e2e/wbv2-nested-collection-instance-production.spec.ts`: a collection instance inside another collection instance expands recursively; the nested BasicCube clone reports `object:NestedInner` as its picking owner (not the outer `object:Collection`), and the nested instance contents render with composed matrices.
- `web/tests/e2e/wbv2-adjust-last-operation-production.spec.ts`: Add Circle → Web adjust panel → vertices 8 → apply replaces the original with one 8-vertex Circle; a second adjustment sets location X=3; “完整属性…” opens the preselected F3 form, sets vertices 12, and the final scene has one 12-vertex Circle at [3, 0, 0].
- `web/tests/e2e/modifier-stack-ui-production.spec.ts` (existing scenario suite, now unlocked): 8 cases — add/remove/move Bevel and parameters for Bevel/Array/Mirror/Solidify/Surf — pass with sequential single-writer clients, invalid-command rejection, undo/redo, save/reopen and replay; native Blender readback confirms both the modified stack and untouched objects.
- Unlocked existing production scenarios (17 tests): `rig-pose-authoring-native` bones/pose with native golden readback and independent replay; `mesh-main-reader-production`; `light-subtypes-reader-production`; `nonmesh-main-reader-production`; `native-main-lifecycle`; `nla-evaluation-golden`; `library-append-main` (append/undo/redo/save/reopen against the desktop canonical report); `rna-datablock-reflection-native` (native datablock reflection survives reopen; zero-user orphan IDs are excluded because desktop save prunes them).
- Combined verification: 22 focused three-domain tests + 17 unlocked existing scenarios, all passing with bounded parallelism on the current WASM.
- Re-run on WASM `8ec2268f`: 22 focused production scenarios pass — three-domain creation/hierarchy/modeling (3), creation/operator slices (10), hierarchy/persistence/collection/adjust (7), native operator matrix (60/60 operators), 32 curve operators, the native modal scenario and the `?ui=web` object-editor scenario. Receipts were regenerated against this hash.
- `web/tests/e2e/object-hierarchy-production.spec.ts` now passes against the production build served by `tools/web/deployment-http-server.mjs` with `?ui=web` (`web/playwright.deployment-local.config.ts`): parent/unparent UI, collection lifecycle, negatives, undo/redo, save/reopen and native golden readback. `web/dist` was rebuilt from the current sources/WASM before the run.
- `web/tests/e2e/modeling-native-modal-production.spec.ts` now passes on the current WASM: translate cancel restores Main and history, translate/rotate/scale modals, Edit-mode projected-vertex click plus modal transform, undo/redo, save/reopen and Properties readback — across the Web-tools/native-canvas toggles before and after save. The scenario takes ~56s with SwiftShader, so the spec now sets its own 300s budget instead of the 30s default. Receipt: `verification/native-modal-production.json` plus its `native-modal-artifacts/`.
- `web/tests/e2e/object-editor-ui-production.spec.ts` now passes on `?ui=web` against `web/dist`: clear location/rotation/scale, create CAMERA/LIGHT/EMPTY/ARMATURE, selection, collection viewport/render toggles, undo/redo, negative no-mutation, save/reopen and an independent determinism run; the native desktop readback agrees on every checkpoint. The previous `selected` mismatch was not a save-path difference: with the native build, `Collection.hide_viewport = True` deselects the object and showing the collection again keeps it deselected, so the Web-saved `selected=false` matches desktop. The test checkpoint now records the Outliner activation state that `Select Active` is undone to, and the reopen step no longer re-clicks the Outliner row (which had masked the selection stored in the file).
- receipts: `tests/golden/WBV2-P1425-001/verification/{add-menu-native-production,create-production,hierarchy-production,modeling-production,mesh-native-operator-ui-production,modifier-matrix-production,convex-hull-native-production,native-operator-matrix-production,creation-object-types-production,data-instance-add-production,hair-curves-add-production,modifier-parameters-apply-production,modifier-apply-convert-production,curve-edit-operators-production,convert-to-mesh-production,armature-edit-production,object-tools-production,edit-mode-persistence-production,scene-flow-production,collection-instance-production,nested-collection-instance-production,adjust-last-operation-production,object-editor-positive,object-editor-negative,object-editor-undo_redo,object-editor-save_reopen,object-editor-determinism,native-modal-production}.json`.
- retained verification blockers (not claimed as passed):
- Scenario-style specs that create several `WebEngineClient`s must terminate each client before opening the next; the unlocked suites now do this on `?engine=manual` and pass. Specs that still drive the app page remain subject to the single-writer contract.
- resolved in this round: `object-editor-ui-production.spec.ts` passes on `?ui=web` with the deployment server and native readback. Desktop Blender deselects objects when their Collection is hidden and does not restore the selection when it is shown, so the Web-saved `selected=false` is correct; the stale expectation and two harness assumptions (undo baseline, post-open re-selection) were corrected.
- resolved in this round: `modeling-native-modal-production.spec.ts` passes after `WebGLObjects` re-homes retained CPU bytes onto the replacement canvas; no GPU canvas detach remains.
- resolved in this round: Web-only Edit-mode switch and `mesh.poke` pass after passing `selectedObjects`/`activeObject`/`baseRevision`, detaching the GPU surface on the Web-tools toggle, and importing `bpy` inside the selection helpers; verified by `wbv2-mesh-native-operator-ui-production.spec.ts`.
- remaining: broader per-operator behavioral evidence for the 486 routes; Ocean/OpenVDB modifiers cannot be enabled because the Web build has `WITH_FFTW3=OFF` and `WITH_OPENVDB=OFF`, and no WASM FFTW/OpenVDB libraries or sources are available locally. Full desktop parity is not claimed.
- 2026-11-10 operator-family sweep and Web fixes (WASM `33db5f5e`):
- `web/tests/e2e/wbv2-operator-sweep-production.spec.ts` invokes every live registered operator in the mesh/object/uv/curve families (167/250/55/51) through `desktop_operator` with a fresh Main per case; each family receipt in `verification/operator-sweep-<family>.json` records the per-operator outcome plus the WASM SHA-256. Final counts: mesh 136 EXECUTED, object 104, uv 32, curve 43 (315/523). Remaining outcomes are MODAL (PASS_THROUGH), CONTEXT_POLL/CANCEL/MESSAGE matching a native poll/report for the detached Layout/VIEW_3D fixture, or ADDON_MISSING for `uv.export_layout` (the bundled `io_mesh_uv_layout` addon is not loaded). Zero CRASH, zero TIMEOUT, zero core OPERATOR_MISSING; the classifier separates native Blender RuntimeError reports from WASM traps.
- Freestyle mesh-data operators now exist on Web. `WITH_FREESTYLE=OFF` compiled out `mesh.mark_freestyle_edge/face` although the Web menu exposed them, so clicking always failed with `get_rna_type(...) not found`. The registration and the two exec functions are now guarded by `WITH_FREESTYLE || WITH_WEB` in `mesh_ops.cc`, `editmesh_tools.cc` and `mesh_intern.hh`; they only write the `freestyle_edge`/`freestyle_face` boolean attributes and need no Freestyle renderer. Both FINISHED in the 62-case matrix.
- `object.quick_explode` now works. The EXPLODE modifier's dynamic `vertex_group` string has no fixed RNA buffer, so the accessor generator emitted `maxLength=-1` and the editor-workflow validator rejected the whole snapshot (`EDITOR_LAYOUT_INVALID: Invalid modifier string resource metadata`). `extract_modifier_accessors.py` now clamps a missing declared maxlength to 63; Quick Explode FINISHED with ParticleSystem + Explode modifiers on the object.
- Geometry Nodes modifier inputs are editable from Python again. `node_socket.cc` wrapped all 19 `make_geometry_nodes_input_srna` registrations in `#ifndef WITH_WEB`, so every NodesModifier input item exposed only `rna_type`/`name` (`object.quick_fur` failed at `inputs.Input_12.value`). Removing those guards restores `value`/`type`/`attribute_name`/`layer_name` on boolean, float, object, material, geometry, menu and collection inputs; verified with a temporary Chromium probe (Input_12/15/20/22 expose `value`), probe removed.
- Menu wiring: `mesh.spin`/`mesh.screw` now pass `axis=[0,0,1]` (EXEC without a view axis reported `Invalid/unset axis`), `mesh.rip` routes through `queueLocalDesktopOperator` so the native invoke runs, and the curve menu's `curve.spin` was removed because its poll (`ED_operator_editsurf`) only accepts an edited surface object.
- The modifier-stack UI test was retargeted from the legacy `.modifier-stack-authoring` panel (not rendered in the current Web tools layout) to the live `[data-modifier-row]` properties panel and its desktop labels (`Amount`/`Segments`, `Solidify` option id). This was a stale test selector, not a product regression. Focused regression on `33db5f5e`: 38/38 tests passed with 4 workers (three-domain chains, 62-case operator matrix, modifier-stack UI/native scenarios, creation/adjust/data-instance/hair-curves/modifier-apply/convert/armature/object-tools/edit-persistence/scene/collection, native modal, rig, readers, NLA, library append, RNA reflection).
- Open gaps (not claimed): `object.quick_fur` still fails because the linked procedural-hair node group evaluates to non-curves (`Evaluated geometry from modifier does not contain curves`) and the Web RNA Main-access guard then invalidates the Python callback (`Main changed during Python property conversion or callback`); QuadriFlow remesh (`WITH_QUADRIFLOW=OFF`), OpenVDB voxel remesh (`WITH_OPENVDB=OFF`), Mantaflow fluid quick effects (`WITH_MOD_FLUID=OFF`) and Ocean bake (`WITH_FFTW3=OFF`) remain build-flag gaps; `object.lightprobe_cache_bake` hits the browser draw-manager single-canvas guard; `uv.export_layout` needs the unloaded bundled addon. The changed spin/screw/rip menu wiring was verified through the same native operator channel, not by clicking the production menu.
- Receipts on `33db5f5e`: `verification/operator-sweep-{mesh,object,uv,curve}.json`, `native-operator-matrix-production.json` (62/62), `creation-object-types-production.json` (29/29), `curve-edit-operators-production.json` (32/32), `object-editor-*.json` (5), plus the production hierarchy timestamped receipt.
- 2026-11-10 late round (WASM `dc7a76eb`): further modeling gaps closed:
- Quick Fur and geometry-node assets now evaluate. `WEB_headless_node_system_init()` registered only a small geometry-node subset and no zone types, so the bundled procedural-hair groups loaded Capture Attribute / Separate Components / Input Radius as `NodeUndefined` and the node-tree updater called through a null zone vtable (`RuntimeError: null function`). The Web `bf_nodes` source set now includes `intern/node_register.cc` and the engine calls the desktop `register_nodes()` once. The hair groups load with zero undefined nodes; `object.quick_fur` creates 1212 points across 101 curves with Generate applied and Set Profile/Interpolate/Noise/Frizz/Hair Dynamics kept.
- Temporary `G_MAIN` swaps during library append/link no longer poison an in-flight Python callback. `RNAMainAccessScope::restore_after_temporary_main_swap` / `restore_context_after_temporary_main_swap` revive suspended scopes when the same Main is swapped back (genuine replacement/clear still invalidates); `BKE_blender_globals_main_swap` and the Web context refresh call the restore after rebinding. Quick Fur's nested `modifier_apply` now completes instead of failing with `Main changed during Python property conversion or callback`.
- QuadriFlow remesh is enabled (`WITH_QUADRIFLOW=ON`): `object.quadriflow_remesh` finishes with “QuadriFlow: Remeshing completed” and remeshes the 8-vertex test cube to 3954 vertices.
- Mantaflow fluid is enabled (`WITH_MOD_FLUID=ON`). `MOD_fluid.cc` forced the Fluid modifier's RNA `srna` to `nullptr` under `WITH_WEB`, so `rna_Modifier_refine` dereferenced null when a Fluid modifier was converted to Python. The real `&RNA_FluidModifier` is restored (and the matching Cloth `nullptr` is fixed). `object.quick_liquid` creates a Liquid Domain with Fluid modifiers plus a Liquid Particle System; `object.quick_smoke` creates a Smoke Domain with Fluid modifiers; a Cloth modifier exposes its settings without crashing.
- `uv.export_layout` is available: the Python bootstrap registers the bundled `io_mesh_uv_layout` addon when the session starts (retry-safe). The operator poll passes for a UV-mapped active mesh and an SVG export writes 1288 bytes to `/browser/uv_layout.svg`.
- Final sweep on `dc7a76eb`: mesh 136/167, object 108/250, uv 32/55, curve 43/51 EXECUTED (319/523); zero CRASH/TIMEOUT/core OPERATOR_MISSING. `object.quick_fur`, `quick_explode`, `quick_liquid`, `quick_smoke` and `quadriflow_remesh` are EXECUTED; `uv.export_layout` is registered (native context message for the empty-filepath sweep invocation instead of ADDON_MISSING).
- remaining (not claimed): OpenVDB voxel remesh (`WITH_OPENVDB=OFF`; `.deps/native/openvdb` contains only `include/` and native `lib/*.so`, no OpenVDB source or wasm OpenVDB/Boost/TBB/blosc artifacts); Ocean bake (`WITH_FFTW3=OFF`; `.deps/native/fftw3` is native only, no WASM FFTW sources/libs); light-probe cache bake runs the EEVEE lightbake job, which calls DRW_gpu_context_create; the headless manual host has no active GPU context, so it reports 'Browser draw manager must be created once per WM canvas'. A canvas-attached UI session is not yet verified, and this is render-engine behavior outside the three modeling domains. The non-executed sweep operators are modal/context classes matching desktop semantics. Full desktop parity remains NOT_CLAIMED pending user acceptance and WASM ports of OpenVDB/FFTW3.
- next action: port OpenVDB and FFTW3 to WASM to close voxel remesh and ocean bake; keep the documented gaps explicit; full desktop parity remains NOT_CLAIMED pending user acceptance.

View File

@@ -0,0 +1,89 @@
# Web UI 对标 + PBR 真实案例实现状态(2026-11-11)
- 状态:`in_progress`(实现证据,不宣称验收)
- 范围:Outliner React treeview、着色器编辑器 React Flow、真实 PBR 资产链路与色彩管理
- 验收:`NOT_CLAIMED`
## 已实现
### Outliner
- 新增 `web/app/src/app/ui/OutlinerTreeView.tsx`、`outliner-tree-data.ts`、`outliner-tree.css`。
- 使用 `react-accessible-treeview@2.11.2` 渲染原生 `tree.rows`;保留选择、展开、拖放 payload、重命名、Remap、可见性、截断提示。
- `App.tsx` 原生行渲染改为 treeview `renderRow`,并保留 `role="treeitem"` 可访问名、`data-node-id`/`data-active`、`.tree-node-name`、展开按钮可访问名。
- 修复 treeview 受控展开反馈环:不再由库自动触发 `OUTLINER_TOGGLE_EXPAND`,仅用户按钮/键盘触发。
### 着色器编辑器(React Flow)
- 新增 `web/app/src/app/ui/ShaderNodeCanvas.tsx`、`ShaderNode.tsx`、`shader-flow-adapter.ts`、`shader-node-catalog.ts`、`shader-node.css`。
- `WasmShaderEditor.tsx` 画布主体切换到 `@xyflow/react@12.12.1`:
- 节点拖动 → `MOVE`
- 端口拖拽 → `LINK`,边删除 → `UNLINK`
- 节点内 socket 编辑 → `SET_SOCKET`
- Shift+A / 工具栏添加节点 → `ADD`
- 节点属性(Mix data/blend、Mapping vector type、Normal Map space、Bump invert、UV Map 名称)→ `SET_PROPERTY`
- Image Texture 换图 → `SET_IMAGE`
- 只读链接材质、revision 冲突、未知节点占位、全部端口折叠。
- 坐标约定 `position={x, -y}`;读回 `fromSocket/toSocket` 与命令 `fromSocketId/toSocketId` 由适配器唯一转换。
- SSE/React Flow 要求:CSP 增加 `style-src-attr 'unsafe-inline'`,脚本/Worker 仍严格;去掉 attribution 隐藏。
### 原生读回与编辑
- `web_engine_blend_reader.cpp`:节点类型补齐 MIX/MAPPING/TEX_COORD/UV_MAP/BUMP/SEPARATE_COLOR/OBJECT_INFO;socket `available`/`hidden`;节点属性(dataType/blendType/vectorType/space/invert/projection/interpolation/extension/uvMap/mode);材质 `textureBindings`;`normalImageId` 真正赋值;Image `colorSpace` 读取(当前资产未返回,见残余)。
- `web_engine_main_state.cpp`:`editShaderNode` 新增 `SET_PROPERTY`/`SET_IMAGE`,ADD 支持 UV_MAP/OBJECT_INFO/SEPARATE_COLOR;`setShaderGraph` 类型扩展并按 socket **identifier** 绑定(修复 Mix 同名 socket)。
- 新增 `packImage`:外部 PNG/JPEG 字节直接打包进已有 Image,保留节点链接,允许 0 尺寸由调用方按文件头提供。
- 新增 `assignMaterialSlot`:把已有材质装入对象槽(`BKE_object_material_slot_add` + `BKE_object_material_assign`),属性面板材质槽下拉已接入。
- `web_engine_api.cpp`、`web/protocol/web-engine.ts`、`web-engine.worker.ts`、`web-engine-capabilities.ts` 同步命令。
### PBR 显示
- `three-adapter/pbr.ts`:按场景 `colorManagement.viewTransform` 选择 AgX/Standard/Neutral/ACES;`toneMappingExposure = 2 ** exposure`;`data-tone-mapping` 场景驱动。
- `three-adapter/texture-assets.ts`:应用 `map`、`normalMap`+strength、`metalnessMap`、`roughnessMap`、`emissiveMap`、`alphaMap`、`bumpMap`;贴图驱动时中性化 factor。
- `texture-assets.applyEnvironment` 使用 `WorldIR.environmentRotation`。
- `render-assets.ts` 纹理用途扩展;`scene-ir.ts` 校验新节点类型/属性/textureBindings。
- 外部贴图 UI:着色器编辑器“外部贴图”列表;文件先 `StorageClient.putAsset` 写入 OPFS,再 `packImage`;项目保存继续走 `StorageClient.saveProject`(OPFS)。
## Chromium 实测(手动探针,实现证据)
- `tests/files/web/m12_glb_main_v1/pbr.blend`:Outliner 树、React Flow 2 节点/1 连线;拖动、连线、断开、改 Roughness、添加 Mapping 均发出正确原生命令;保存后重开 Roughness=0.7、Mapping 位置/连线保留;Ctrl+Z / Ctrl+Shift+Z 恢复 0.28 / 0.7。
- `blender-5.2.0/tests/files/io_tests/blend_geometry/materials_pbr.blend`:ImageTexture/Principled 读回;三张外部 PNG 经 OPFS→packImage 变为 PACKED;viewport 显示红/金属黑 PBR 立方体;`data-tone-mapping` 场景驱动。
- `cubes_with_textures.blend`:读回 MAPPING/UV_MAP/SEPARATE_COLOR/IMAGE_TEXTURE;textureBindings 含 BASE_COLOR/ROUGHNESS/NORMAL/EMISSION,`normalImageId` 已赋值。
- `user-testing/WEB_Blender_Robot_Tool_20260916/outputs/robot-project.blend`:18 对象、7 个 link 材质;着色器材质列表可见。
## Playwright 用例
- `web/tests/e2e/outliner-treeview-production.spec.ts`:通过。
- `web/tests/e2e/shader-editor-flow-production.spec.ts`:通过(headless 合成指针的节点拖动不稳定,拖动由手动 Chromium 探针覆盖;其余连线/删除/添加/参数/保存重开在用例内)。
- `web/tests/e2e/pbr-real-assets-production.spec.ts`:通过。三张外部 PNG 在 Web UI(`?ui=web`)下经 OPFS→`packImage` 打包,SceneIR 依次 r7/r10/r13,外部贴图列表清空。
- 三条用例合并运行:`3 passed (37.3s)`。
- 注意:测试使用 `?ui=web`(React UI 模式)以避免 headless SwiftShader 下原生 GPU 节点编辑器绘制抛 `WASM_INIT_FAILED`;这不是 React UI 路径的缺陷。
## 残余缺口
1. Image `colorSpace` 未从 `.blend` 读出(`undefined`);当前按纹理用途推断 sRGB/Non-Color,颜色贴图默认正确,非默认色彩空间未覆盖。
2. `robot-project.blend` 保存工作区中 Outliner 深层的展开按钮发送选择而非 `OUTLINER_TOGGLE_EXPAND`;`pbr.blend`/`basic_scene.blend` 展开正常。需继续定位该工程的 workspace/area 映射。
3. Headless Playwright 合成鼠标拖动 React Flow 节点不稳定;真实 Chromium 手动拖拽正常。
4. Compositor 原生 `setCompositorNodeGraph` 仍未实现;Geometry Nodes 尚未复用 React Flow 画布。
5. 非 Principled BSDF(Glass/Hair/Anisotropic 等)、Subsurface/Volume/UDIM/HDRI 仍按 capability block 或后续批次处理。
6. 本轮未运行全量回归、未生成正式 candidate receipt;构建哈希与证据需在后续批次绑定。
## 构建与构件
- 原生:`cmake --build build_web_blender6 --target web_engine -- -j8`
- 同步:`node tools/web/sync-web-engine-runtime.mjs`
- 前端:`cd web && npm run build`
- `web_engine.wasm` SHA-256:`84fc21e7728b580579424f97495d27275d30c01f2898ccadf4e368ea37d646ea`(与 `web/app/src/vendor/blender/web_engine.wasm` 一致)
- `web/dist/index.html` SHA-256:`d36f002b7793b7684f3ac570537bd33527143a6ea64fb43b3dabff0d3d313a8e`
## CSP 与样式加载
- React Flow 需要内联 style 属性;部署 CSP 改为 `style-src 'self'; style-src-attr 'unsafe-inline'`,脚本/Worker 指令保持严格。
- React Flow 与新增组件 CSS 通过 `web/app/index.html` 的同源 `<link>` 引入(`ui/xyflow.css`、`ui/outliner-tree.css`、`ui/shader-node.css`),避免 Vite dev 的内联 `<style>` 注入被 CSP 阻断;生产构建提取为 CSS 文件。
- `tools/web/check-csp-policy.mjs` 已改为仅对脚本/Worker 指令拒绝 `'unsafe-inline'`,并断言 `style-src-attr` 的最小放宽。既有 `tests/golden/M13-05A/csp-report.json` 需在后续验收批次重新生成。
## OPFS 资产策略
- 渲染用 packed 图像:`cachePackedAssets` 在每次快照后通过 `StorageClient.putAsset` 写入 OPFS。
- 外部贴图:用户选择文件后先 `putAsset`(OPFS),再 `packImage` 进工程;项目保存走 `StorageClient.saveProject`(OPFS)。
- 打开工程时提供的库 `.blend` 文件在 `openBlend` 前写入项目 OPFS(`application/x-blender`)。
- 资产库/OBJ/多分辨率/VDB 等既有导入路径继续使用 `putAsset`。