Skip to content

城邦的蓝图

背景:menus/ 目录膨胀到 16,345 行 / 41 个文件,两套导航机制并存,AI 难以重写。v1.3.1 发布前,联邦还有两个"忘了写"的体验缺口。 过程:定义 MenuNode 类型 → StatePath 前缀体系 → menu-schema.ts 数据源 → menu-factory.ts 统一渲染 → 57 个面板迁移;ADR-090 对话框目录记忆;ADR-094 替换模式保持。


一、两张图

王逸打开 scene-menu.ts,滚动到第 200 行。

SCENE_FOLDER_ROUTES 是一个 Record<string, () => PopupLevel>——静态路由表。点菜单文件夹时查这张表,找到对应的 build 函数,执行它,返回那一层。

这是联邦的导航机制之一。

他又打开 scene-prop-levels.ts,在第 33 行看到了另一样东西:getXMenu()?.push(buildX(id))

这也是导航。但它是动态的——点了某个道具的详情,push 一个新的层级进栈。它不走路由表,它自己建。

两套机制。两套代码路径。静态的走 Record,动态的走 push

王逸想了一下。如果城邦的街道分布只有一张图,那 AI 就能读懂。如果一张在纸上,一张在脑子里,那就没人能读懂。


二、一张图

ADR-093 的核心只有一句话:菜单树应该是一份数据,不是一堆函数。

MenuNode 是这个数据的载体。每个节点有一个 id——稳定的、唯一的。kind 告诉渲染器它是什么:文件夹、滑块、开关、动态列表。

typescript
interface MenuNode {
    id: string;
    kind: MenuKind;
    label?: string;
    children?: MenuNode[];
    control?: ControlSpec;
}

id 是路由的基础。ground:basic 指向地面基础设置层。不再需要魔法字符串 scene:render:dof

命令式 builder 降级为"自定义层渲染后端"。表单类特殊层保留,标准层从 schema 派生。

StatePath 是第二个关键设计。env.groundVisible 映射到 envState.groundVisible。控件绑定 StatePath,状态变化自动同步。


三、迁移

57 个面板迁移。

桌面壳从地面基础设置开始——旧代码是 slideRow({ label: '地面可见度', get: ... }),新代码是 { id: 'ground:visible', kind: 'slider', control: { bind: 'env.groundVisible' } }

少了一半。旧代码里有闭包、有副作用、有硬编码字符串。新代码里只有数据。


四、v1.3.1 发布前

57 个面板迁移完的那一天,联邦还剩下两个体验缺口。

缺口一:对话框不记目录。

internal/dialogs/file_dialog.goOpenFileSaveFile 没调 .SetDirectory()。每次打开文件对话框都落在系统默认位置。桌面壳每次加载模型,都要从根目录翻起。

ADR-090 决定:按资源类型分别记忆最后目录。LastDirs map[string]string。加载动作跳到模型目录——没问题,因为字典里存的是模型目录。加载环境贴图跳到贴图目录——也没问题,因为存的是贴图目录。

相对路径优先。桌面端和 Android 端都能用。

缺口二:替换模式断了。

资源库的"替换模式"在模型详情页触发——点击"更换模型",进入以当前模型名为标题的资源库浏览器,选择模型后加载替换。但加载完成后,弹窗直接跳转到新模型的详情页,不在替换菜单里了。

用户想逐一看过模型库中的多个模型,每次加载后都得"翻回 → 再选"。体验断裂。

ADR-094 决定:加载完成后保持替换状态,弹窗回到资源库浏览器。用户不需要翻回——因为一直在替换菜单里。


尾声

v1.3.1 发布。

菜单从 41 个函数变成了一份数据。对话框记得上次打开的位置。替换模式不再断链。

城邦的蓝图只有一张了。


教训:两套机制并存时,"没有 bug"不等于"架构正确"。代码功能对,但结构错。AI 可维护性是架构质量的一个指标——如果 AI 看不懂,人类也迟早看不懂。