Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,318 @@
# 未声明 Blender 能力执行计划
更新时间2026-08-09
本计划把当前未声明的 Sculpt、Geometry Nodes/Simulation、任意 Shader Node 和完整
NLA 拆成四个可独立验收的连续任务。每个任务都必须满足Blender Main 是唯一写入
权威SceneIR 只承载通过校验的数据Three.js 不执行 Blender 语义;不支持或不确定
的输入返回稳定的结构化阻断。任务没有通过自己的 desktop golden、WASM golden、保存
重开和故障门前,不得扩大下一阶段的能力白名单。
## 总体依赖
| 顺序 | 任务 | 依赖 | 交付物 |
| --- | --- | --- | --- |
| 1 | N-011 Sculpt 子集 | MeshIR 属性、BMesh/Main history、N-003 变形门 | 固定采样笔刷事务、Mask/Face Set、可重开的 Sculpt fixture |
| 2 | N-012 Geometry Nodes 与 Simulation | Modifier graph、NodeTree reader、N-001 Depsgraph | 可证明的字段/实例闭包、内容寻址 simulation cache |
| 3 | N-013 Shader Node 图 | MaterialIR、图片/色彩空间、W-080 节点写回 | 校验过的 ShaderIR、受限图编译与桌面材质 golden |
| 4 | N-014 NLA | Action/FCurve、frame/pose 求值、N-006 保存日志 | Strip/Track 子集、跨 Action 评估和恢复 golden |
共享基础工作在 N-011 前先完成capability manifest、协议版本、资源预算、错误码
注册表、golden runner 模板和不支持输入 fixture。它们不改变任何既有白名单。
## N-011 Sculpt 子集
### 目标边界
第一版只承诺单对象、单用户 Mesh、Object Mode 到 Sculpt Mode 的受控笔刷事务。支持
Draw、Inflate、Grab、Smooth 四种确定性笔刷,固定半径/强度/对称轴和离散 stroke
采样;支持每顶点 Sculpt Mask 与每面 Face Set 的读写。多分辨率、Dyntopo、Voxel Remesh、
cloth/pose/elastic deform、纹理笔刷、破坏性拓扑笔刷和雕刻贴图在本任务中继续阻断。
### 分解
1. **N-011-A 能力和资源门**
- 增加 `SCULPT_STROKE``SCULPT_MASK``SCULPT_FACE_SET` 命令类型和 payload schema。
- 校验 object/mesh 单用户、可编辑、无 linked library、无 shape-key/激活 modifier
拓扑冲突;失败码分别为 `SCULPT_MESH_NOT_SINGLE_USER``SCULPT_TOPOLOGY_UNSUPPORTED`
`LINKED_DATA_MUTATION_BLOCKED`
- 建立 stroke 点数、总位移、影响顶点数和每 revision 内存预算;超限不得部分提交。
2. **N-011-B MeshIR 属性**
- SceneIR 增加 `sculptMask`vertex float32`faceSets`face int32、active
face set、mask revision 和 attribute domain。
- WBG1 增加可选 mask/face-set字段Worker 校验长度、有限值和域范围;旧 WBG1
没有这些字段时保持兼容。
- `MeshGeometryDelta` 支持上述属性的范围 patch并将属性变化计入资源 hash。
3. **N-011-C Blender Main 笔刷事务**
- 在 native Main writer 中建立一次 stroke 的临时 BMesh/PBVH 查询,按固定顺序处理
sample所有顶点写入在一个 history transaction 内完成。
- Draw/Inflate 使用命中法线方向Grab 使用投影平面位移Smooth 使用固定邻接
平均;算法参数和 Blender 5.2 desktop script 必须相同,不能由 Three.js 重新实现。
- 每次 stroke 完成后更新 normals、split normals、tangents、bounds 和 SceneDelta。
4. **N-011-D UI 与恢复**
- 增加 Sculpt Mode、brush selector、radius/strength、mask/face-set 操作和单击/拖拽
stroke 收集;界面只发送 sample不保存独立 Three 状态。
- undo/redo、autosave、OPFS journal、Worker 重启和 `.blend` 下载都使用同一命令。
- linked Mesh、shape-key topology、modifier evaluated mesh 必须显示结构化阻断而非灰掉后
静默忽略。
5. **N-011-E 验收**
- 新 fixture平面、非均匀变换 Mesh、带 seam/sharp、带 mask/face set 的 1k/50k 网格。
- desktop golden 比较 positions、normals、mask、face sets、bounds门限为 max/RMS
误差和精确属性 hash。
- 多帧相同 stroke、重复打开、取消中途 stroke、OOM 和 malformed payload 都有测试。
- 命令:`npm --prefix web run test:sculpt-goldens``test:sculpt-roundtrip`
`test:sculpt-faults``test:sculpt-e2e`
### 停止条件
无法在 wasm32 headless 环境稳定初始化 PBVH/笔刷依赖时PBVH Sculpt 能力保持 `BLOCKED`
当前有界 native Main CPU stroke 只能作为独立有限子集 C1a 验收,不能满足 C1、不能声明
Blender PBVH 等价,也不能借由 Three.js 近似扩大能力。
## N-012 Geometry Nodes 与 Simulation
### 目标边界
先扩大无时间状态的字段和实例闭包,再处理 Simulation Zone。第一阶段承诺 Group Input/
Output、Transform Geometry、Set Position、Join Geometry、Separate Geometry、Realize
Instances、Store/Named Attribute、基础数学/比较/选择字段;不承诺任意 node group、外部
对象资源、Python node、硬件依赖或隐式时间状态。
### 分解
1. **N-012-A GraphIR 和校验器**
- 建立 node UUID、bl_idname、socket type/domain/default/link、group interface、
node group 引用和 field dependency 的版本化 `GeometryNodeGraphIR`
- 解析时检查 duplicate UUID、悬空 link、socket 类型/域不匹配、group recursion 和
anonymous attribute 生命周期,错误码包含 `GN_SOCKET_TYPE_MISMATCH`
`GN_GROUP_RECURSION``GN_EXTERNAL_RESOURCE_MISSING`
- 将 modifier stack order、viewport/render/edit 开关和 graph hash 纳入 depsgraph report。
2. **N-012-B 无时间字段求值**
- native 只按 Blender lazy-function/field 语义执行声明闭包;输出 MeshIR 的 topology、
instances、attributes 和 bounds。
- 每个 node family 单独 desktop golden域转换point/edge/face/corner/instance
有固定 fixture不允许把 unsupported socket 当常量 0。
- Three.js 只消费 evaluated SceneIR未求值 graph 返回 `METADATA_ONLY``BLOCKED`
3. **N-012-C 实例和外部依赖**
- 定义 object info、collection info、image/volume 等外部资源的 stable ID 和沙箱检查。
- 先支持同文件、非循环、只读外部对象linked library、循环 collection、缺失对象和
非确定性数据返回 `GN_EXTERNAL_RESOURCE_MISSING``GN_DEPENDENCY_CYCLE`
- 实例变换和属性必须能进入 WBG1/GLB 摘要,不能在 Web 端展开后丢失 ID 语义。
4. **N-012-D Simulation cache**
- 定义 `SimulationCacheManifest`graph hash、source blend hash、frame range、inputs
hash、Blender version、schema、每帧 byte range 和 SHA-256。
- 只接受桌面 Blender 生成并校验过的 deterministic bakeWorker 按 OPFS content-addressed
方式读取frame seek 不得隐式重新计算。
- cache 缺帧、hash 不符、版本不兼容、quota/OOM 和损坏字节都必须可恢复地阻断;禁止返回
上一帧冒充当前帧。
5. **N-012-E 验收**
- graph fixtures 覆盖每个支持 family、字段域、实例、group recursion、循环依赖和
modifier 开关simulation fixtures 覆盖 1/5/10/25 帧、seek、cache 重启和损坏。
- 命令:`test-gn-goldens``test-gn-graph-validation``test-simulation-cache`
`test-gn-e2e``test-gn-blender-roundtrip`
### 停止条件
Blender lazy-function runtime 或 simulation cache 不能在 wasm32 完整初始化时,只扩大
metadata/graph validation保持现有 simulation structured block不得把输入几何复用成
“求值成功”。
## N-013 Shader Node 图
### 目标边界
把当前 Principled/Image/Normal/UV 读写子集升级为可验证的 ShaderIR 图,而不是承诺任意
Blender 节点。首批支持 RGB、Value、Mix、Math限定运算、Mapping、Texture Coordinate、
Image Texture、Normal Map、Bump、Principled、Material OutputVolumetric、OSL、任意
自定义 shader、Cycles-only 节点和未实现色彩管理继续阻断。
### 分解
1. **N-013-A ShaderIR**
- node UUID/type/label/location、typed sockets/defaults, links、material output、
image/sampler/color-space、group boundary 和 graph hash 全部版本化。
- graph validator 检查单输出、无环、socket 类型、颜色/向量隐式转换、未连接输入和
texture resource错误码包含 `SHADER_NODE_UNSUPPORTED``SHADER_GRAPH_CYCLE`
`SHADER_SOCKET_TYPE_MISMATCH`
2. **N-013-B Blender Main 写回**
- 为每个支持 node family 建立最小 native node/socket/storage 注册和 create/update/remove
command操作在一个 Main history transaction 中完成。
- linked material、library image、packed asset 和 color-management profile 经过同一
资源/路径沙箱;无法写回的图返回稳定阻断。
3. **N-013-C Web 消费和编译**
- 从 ShaderIR 生成受限 Three.js material/GLSL 或 WebGPU backend编译结果必须带 graph
hash 和 capability report。
- Web 端不能把未知节点删掉后继续显示“成功”;未知节点显示阻断并保留原图 metadata。
- 透明、alpha clip、normal/tangent、sRGB/Non-Color 和 sampler 状态分别测试。
4. **N-013-D 导出和回导**
- GLB 只导出能映射到 glTF PBR 的闭包;不可映射节点进入机器可读 warning/error不能
静默烘焙。
- Blender desktop 再导入比较节点摘要、材质值、纹理色彩空间、sampler 和渲染摘要。
5. **N-013-E 验收**
- 命令:`test-shader-graph-goldens``test-shader-node-roundtrip`
`test-shader-compile``test-shader-glb-roundtrip``test-shader-faults`
- 每个 node family 至少一个正例、一个错误图和一个 save/reopen fixture图 hash、
asset hash、desktop/Web 语义报告必须可复现。
## N-014 NLA
### 目标边界
在现有 Action/FCurve 子集上增加单个对象的 Track/Strip、Action Clip、frame range、
scale/repeat、blend in/out、influence、extrapolation 和 Replace/Add/Multiply/Combine
四种有限混合。Drivers、完整 transition strips、meta strips、time warp 和复杂约束驱动
仍然单独阻断。
### 分解
1. **N-014-A AnimationIR 扩展**
- 增加 Action、slot、NlaTrack、NlaStrip、strip source range、scene range、blend/extend、
muted/solo/active 和 stable owner IDs。
- 校验 track/strip 排序、重叠、负 scale、repeat、循环引用和 FCurve path compatibility。
2. **N-014-B Blender Main 编辑**
- 新增 track/strip create/remove/move/resize/blend/solo/mute commands全部进入 Main
historyAction 选择和已有 keyframe 命令共享 revision。
- 保存后重新打开必须恢复 strip UUID、Action assignment、active track 和 frame state。
3. **N-014-C 原生求值**
- 每帧交给 Blender Depsgraph/NLA evaluator输出 object/bone matrices、FCurve values 和
evaluated MeshIRThree.js 不自行混合 Action。
- 覆盖 strip 边界、负向播放、repeat、blend overlap、constraint order 和 frame seek。
4. **N-014-D UI 与导出**
- Dope Sheet 增加 NLA track/strip lane、拖动边界、blend handles、mute/solo、Action menu。
- GLB 导出只承诺可线性展开的 clip不可展开的 NLA 结构返回 warning/error 并保留摘要。
5. **N-014-E 验收**
- 命令:`test-nla-goldens``test-nla-frame-evaluation``test-nla-roundtrip`
`test-nla-faults``test-nla-glb-roundtrip`
- 以 frame 1/5/10/15/25 比较 transforms、pose matrices、vertex max/RMS error、strip
metadata 和 save/reopen hash。
## 共享发布门
四个任务完成前,每个新能力必须补齐:
1. SceneIR/WBG schema 和 capability manifest 的版本迁移。
2. native Main、undo/redo、save/reopen、OPFS journal、Worker restart 和 malformed input。
3. Blender 5.2 desktop golden、WASM golden和 Chromium 主线程/OffscreenCanvas 非空像素。
4. 100k/1M 内存预算、命令 payload 上限、超时/OOM/quota、恶意图/图形数据和路径沙箱。
5. 对应 `docs/status/N-011.md``N-014.md`、fixture、默认 npm 验收命令和机器可读报告。
## 领取顺序
先做 N-011-A/N-011-B 与 N-012-A/N-013-A/N-014-A 这四个协议和安全门,随后按
N-011 -> N-012 -> N-013 -> N-014 执行。任一任务的正例、阻断例、保存恢复或发布门失败,
都只能修复当前任务,不得把下一任务的实现标记为完成。
## 当前执行切片2026-08-09
本轮把四个协议阶段再切成可独立回归的小步骤;每一步都先在 Worker 入口验证,再决定
是否允许进入下一层:
| 切片 | 内容 | 状态 | 证据/下一步 |
| --- | --- | --- | --- |
| N-011-A1 | `SculptStrokeIR` schema、笔刷枚举、sample 数量/路径长度/数值/对称校验 | 已完成 | `web/protocol/sculpt.ts`;超 2048 点或路径预算返回 `SCULPT_STROKE_BUDGET_EXCEEDED` |
| N-011-A2 | 单用户、linked、Shape Key、拓扑 modifier、空网格和 object-space mirror 能力门 | 已完成 | `gateSculptCapability` + Worker/native mirror gate不满足条件结构化阻断 |
| N-011-B1 | SceneIR Mask/Face Set 字段、域长度、范围、revision 校验 | 已完成 | `scene-ir.ts` 与 Worker binary validator |
| N-011-B2 | WBG1/geometry delta 属性编码和 hash | 已完成 | WBG1 magic/header v2 向后兼容 v1mask/face set 支持 binary transfer、replacement/range delta |
| N-011-B3 | Mask/Face Set Blender Main 写回、undo、保存重开 | 已完成 | `setSculptMeshAttributes``test:authoring-roundtrip`active Face Set runtime state 继续阻断 |
| N-011-C1a | 有界 native Main 顶点 stroke四笔刷、压力、Mask、对称、预算 | 已完成有限切片 | C++ Main 单事务;`test:authoring-roundtrip` 验证 save/reopen明确不是 PBVH/BMesh 等价 |
| N-011-C1 | Blender PBVH/BMesh 命中、法线/切线和桌面等价求值 | planned/阻断 | 当前只允许 C1aPBVH、Dyntopo、多分辨率和 UI 仍返回结构化阻断 |
| N-011-C2 | Draw/Inflate/Grab/Smooth desktop/WASM golden | planned | 依赖 C1每种笔刷独立 max/RMS 门 |
| N-012-A1 | GraphIR schema、节点白名单、typed socket/link/domain 校验 | 已完成 | `geometry-nodes.ts`Worker cycle gate 已有 E2E |
| N-012-A2 | 跨 group 的递归闭包、外部 object/collection/image ID 沙箱 | 已完成 | stable ID 类型、owner cycle、linked/missing/corrupt 资源门和 E2E 已覆盖 |
| N-012-A3a | Simulation manifest/schema、Blender 版本、frame byte range | 已完成 | `simulation-cache.ts`;完整帧、连续范围、总/逐帧 SHA-256 |
| N-012-A3b | committed blend 绑定、OPFS 内容寻址、schema 6 索引和 Worker 重启 | 已完成 | `simulation_manifest` + asset SHA-256`test:simulation-cache` |
| N-012-A3c | Blender desktop bake 生成、版本迁移、GN frame seek 消费 | planned | Simulation Zone 仍返回 `GEOMETRY_NODES_SIMULATION_UNAVAILABLE` |
| N-012-B1 | 无时间 GN lazy-function/field Main evaluator | planned | 当前合法 GraphIR 仍返回 `CAPABILITY_MISSING` |
| N-013-A1 | ShaderIR schema、单输出、typed socket/隐式转换、无环 | 已完成 | `shader-graph.ts`;未进入 writer 白名单的节点返回 `SHADER_NODE_UNSUPPORTED` |
| N-013-A2 | material/image stable ID 与 linked/missing/corrupt 资源门 | 已完成当前子集 | packed image 可引用linked/missing/corrupt image 在 Worker 阻断 |
| N-013-B1 | RGB/Value/Principled/Image Texture/Normal Map/Output 整图 Main transaction | 已完成当前子集 | native create/remove/link/default、history、save/reopen 回归 |
| N-013-B2a | Math 六运算 Main `custom1`、原生 socket identifier 和 operation reader | 已完成有限切片 | ADD/SUBTRACT/MULTIPLY/DIVIDE/MINIMUM/MAXIMUM其他运算阻断 |
| N-013-B2 | Mix/Mapping/TexCoord/Bump storage/property 写回 | planned | 未声明节点和 properties 仍 `SHADER_NODE_UNSUPPORTED` |
| N-013-C1a | RGB/Value 常量到 glTF PBR factor 的严格映射 | 已完成有限切片 | 缺值、越界、重复输入和未知链接返回 `SHADER_GRAPH_UNMAPPABLE`mapping E2E 覆盖 |
| N-013-C1 | 受限 Web material compiler、graph hash、色彩空间/sampler | planned | C1a 不代表 Web shader compiler任意图仍未启用 |
| N-014-A1 | Track/Strip schema、Action/range/blend/time-warp 校验 | 已完成 | `nla.ts`;缺失 Action 返回 `NLA_ACTION_MISSING` |
| N-014-A2 | strip 排序/重叠、owner/path compatibility、循环引用校验 | 已完成当前子集 | 同轨重叠、duration mapping、blend range 和 unsupported strip type 均阻断 |
| N-014-B1 | Action Clip 整栈替换 Main transaction、reader、保存重开 | 已完成当前子集 | `setNLAStack`Track/Strip/Action 摘要重开一致 |
| N-014-B2 | 细粒度 create/remove/move/resize/active 命令 | planned | UI 前先扩命令和 revision 回归 |
| N-014-C1 | 正 scale Action Clip native frame 求值 | 已完成当前子集 | authoritative Main bytes 进入 Blender depsgraphframe 5 矩阵回归通过 |
| N-014-C2a | Action Clip reverse Main 标志、reader、Depsgraph、保存重开 | 已完成有限切片 | `NLASTRIP_FLAG_REVERSE`frame 5 反向矩阵回归 |
| N-014-C2b1 | Action Clip repeat 与跨周期 frame seek | 已完成有限切片 | `repeat=2`frame 5/14 native 矩阵一致 |
| N-014-C2 | blend overlap、骨骼、约束顺序、transition/meta | planned | 未声明族继续 `NLA_*` 结构化阻断 |
| 共享 | 四类命令联合类型、Worker 统一 gate、浏览器稳定错误码回归 | 已完成 | `web-engine.ts``web-engine.worker.ts``test:capability-gates` |
### 阻断家族的可领取细分
以下切片一次只扩大一个可证明的能力;前一切片未通过 Main/save-reopen/golden/fault 门时,
后一切片不得转为 `in_progress`
| Sculpt 切片 | 交付与验收 | 状态 |
| --- | --- | --- |
| N-011-C1b | wasm32 PBVH/session/context feature probe初始化失败返回独立错误码且不修改 Main | planned/阻断 |
| N-011-C1c | PBVH object-space 命中、半径查询、对称副本去重和最大影响顶点预算 | planned依赖 C1b |
| N-011-C1d | Draw/Inflate/Grab/Smooth 分别接 Blender brush kernel每笔刷独立事务和取消回滚 | planned依赖 C1c |
| N-011-C1e | normals/split normals/tangents/bounds 与 mesh range deltarevision/hash 精确变化 | planned依赖 C1d |
| N-011-C2 | 1k/50k、非单位对象矩阵、Mask/Face Set、seam/sharp desktop/WASM max/RMS golden | planned依赖 C1e |
| N-011-D1 | Sculpt Mode、四笔刷、半径/强度和 object-space symmetry UIUI 不保存副本 | planned依赖 C2 |
| N-011-D2 | undo/redo、取消、Worker restart、OOM/malformed stroke 和关闭重开故障注入 | planned依赖 D1 |
| GN/Simulation 切片 | 交付与验收 | 状态 |
| --- | --- | --- |
| N-012-B0 | wasm32 lazy-function/field runtime feature probe、超时和内存上限 | planned/阻断 |
| N-012-B1a | Group Input/Output + Transform Geometry 最小闭包,输出 topology/attributes/bounds | planned依赖 B0 |
| N-012-B1b | Set Position、Math/Compare/Selection field 及 point/edge/face/corner 域转换 | planned依赖 B1a |
| N-012-B1c | Join/Separate/Realize Instances 和 instance stable ID/transform | planned依赖 B1b |
| N-012-B1d | Store/Named Attribute 生命周期、anonymous attribute 泄漏和 deterministic graph hash | planned依赖 B1c |
| N-012-D1 | desktop bake 生成器、Blender 版本迁移和逐帧 cache 消费 | planned依赖现有 A3b |
| N-012-D2 | Simulation Zone wasm frame step、状态所有权和 frame seek feature probe | planned/阻断,依赖 B1d/D1 |
| N-012-D3 | 浏览器 bake start/cancel/commit 原子事务;中断时不发布半成品 manifest | planned依赖 D2 |
| N-012-D4 | cache invalidation、缺帧/hash/version/quota/OOM/Worker restart 故障门 | planned依赖 D3 |
| Shader 切片 | 交付与验收 | 状态 |
| --- | --- | --- |
| N-013-B2a | Math 白名单逐运算 Main 写回和 operation readerGLB/Web 编译仍阻断 | 已完成有限切片 |
| N-013-B2b | Mix 的 data type、factor clamp 和颜色/向量 socket 变体 Main 写回 | planned依赖 B2a |
| N-013-B2c | Texture Coordinate -> Mapping -> Image TextureUV 名称/sampler/色彩空间闭环 | planned依赖 B2b |
| N-013-B2d | Normal Map/Bump 串联、强度/距离、切线前置条件和 desktop golden | planned依赖 B2c |
| N-013-C1b | 受限 Web compiler IR、graph hash cache、未知节点 fail-closed | planned依赖 B2d |
| N-013-C1c | sRGB/Non-Color、alpha、sampler、normal/tangent 和 WebGL/WebGPU 编译报告 | planned依赖 C1b |
| N-013-D1 | 可证明 PBR 闭包的纹理/常量完整映射;其余保持 `SHADER_GRAPH_UNMAPPABLE` | plannedC1a 已覆盖常量子集 |
| N-013-D2 | 桌面 Blender GLB 再导入,比较 factor、节点摘要、图片、sampler 与渲染摘要 | planned依赖 D1 |
| NLA 切片 | 交付与验收 | 状态 |
| --- | --- | --- |
| N-014-B2a | create/remove/move/resize/active 细粒度命令与 revision 冲突回归 | planned |
| N-014-C2b1 | repeat 和跨周期 frame seek Main/Depsgraph/save-reopen | 已完成有限切片 |
| N-014-C2b2 | strip 边界、NOTHING/HOLD/HOLD_FORWARD 和多周期 desktop golden | planned依赖 C2b1 |
| N-014-C2c | 多轨 overlap、blend in/out、influence 与四种 blend mode 的对象/骨骼 golden | planned依赖 C2b2 |
| N-014-C2d | constraint order、父级缩放和 pose matrix 的 NLA/Depsgraph 误差门 | planned依赖 C2c |
| N-014-C3 | Transition strip schema/Main/reader/evaluator只连接兼容相邻 Clip | planned/阻断 |
| N-014-C4 | Meta strip 递归深度、子 strip 所有权和循环检测 | planned/阻断,依赖 C3 |
| N-014-C5 | Sound strip 资源沙箱、时间摘要;不伪装为几何/姿态求值 | planned/阻断 |
| N-014-C6 | Animated Time、reverse+time-warp 组合、Drivers 依赖图和循环检测 | planned/阻断 |
| N-014-D1 | NLA lanes、selection、drag/resize、blend handles、mute/solo 与 Action menu | planned依赖 B2a/C2d |
| N-014-D2 | 可线性展开 Clip 的 GLB 导出;其他 strip 返回机器可读错误并保留摘要 | planned依赖 D1 |
当前默认验收命令:
```bash
npm --prefix web run typecheck
WEB_TEST_PORT=5200 npm --prefix web run test:capability-gates
npm --prefix web run test:simulation-cache
npm --prefix web run test:authoring-roundtrip
```