主题
功能全景
这页把编辑器的功能按用户场景逐域过一遍:你能做什么、为什么这样设计、代码在哪。说是功能页,其实也是一张代码导览图,每节都会落到具体文件。逐项人工验收的用例清单在仓内 docs/spec/full-requirements/manual-acceptance.md,约 90 个用例对应需求台账 R-01 到 R-44。
先看整体:三栏工作台
打开编辑器,屏幕就是一个类 Mapbox Studio 的单屏工作台。三个区域各答一个问题:
- 布局谁管:组合根
src/app/app.component.ts装配三栏骨架、全部模态宿主、项目文件进出与全局快捷键。整个应用只有这一个文件允许做横切装配,这是架构规则里最硬的一条。 - 右栏显示什么由左栏的选中对象决定:图层行打开图层编辑器,Global 行打开全局面板编辑列,底图配置行打开底图面板,多选打开合并面板。
- 观感对齐 Mapbox Studio:设计令牌记录在
docs/spec/design-tokens.json,与src/styles.css头部段保持一致。
图层面板(左栏)
图层树
图层树是左栏的主体。新建图层菜单覆盖十种类型:背景、填充、线条、符号、圆点、热力图、填充拉伸、栅格、山体阴影、颜色分层,与 core/config/config.model.ts 的 MsLayerType 十个枚举同源。新层插入位置固定在最后一枚底图图层之后,保证落在底图段之上,不会插到不透明底图下面导致"看不见"。
每个图层行有五个操作:复制(id 自动去重)、显隐(visibility 即时下发地图)、删除(挂了附属交互时先出级联提示)、缩放至图层(fitBounds)、行内重命名(原子防重,Enter 或失焦提交,Esc 取消不提交)。
这里有个设计决策值得说一下:图层的中文显示名、分组、备注不属于渲染语义,所以不存进样式 JSON,而是收进 ms-config 界面文档(config.layers[id] 的 UI 键)。旧档案的 metadata 约定键仍然兼容,缺省时显示图层 id。树行左侧的图例色块随主视觉属性的值形态变化:常量是色块,zoom 渐变是渐变条,数据渐变和条件渐变各有形态,不点开图层就能扫一眼样式结构。
实现上三层分工:容器 features/layer-list.component.ts 管树渲染、选择与拖拽;纯函数层 features/layer-list-fns.ts 管树模型构建与三分位判定,multi-merge 面板和单测也消费它;显示名解析只有一处定义,shared/layer-display.ts 的 resolveLayerDisplayName,优先读界面文档、回落样式 metadata。树行图例派生自 core/interaction/interaction-badge.ts 的 layerSwatchBackground。
边界:底图自身的层和插槽驻留成员不进主树(计数含成员);过滤激活时分组消失、树平铺;样式受保护时所有写操作禁用。
分组与三分位拖拽
打组是一级分组目录:连续块入组,组头右键可重命名或解组,全部单条撤销。分组身份用稳定组 ID 实现,显示名和成员关系分离:
config.groups表只存{name, note},键是组 ID;重命名只改 name,不动成员引用。- 组 ID 的铸造集中在
ConfigStore.groupIdForName:显示名没被占用就直接用显示名当 ID(不落 groups 条目,档案字节更省);撞名时铸grp-N并落条目。 - 成员关系由
config.layers[].group派生,组表不存成员清单,避免同一事实两处存储。
HTML5 拖拽用三分位判定:拖到目标行上 30% 算插入其前,中 40% 算入组,下 30% 算插入其后。四种落点指示(顶线、底线、嵌入环、红环拒绝)一一对应。判定逻辑在 layer-list-fns.ts。底图段不可拖入主树,过滤激活态禁拖。
Filter by 过滤
features/tree/filter-by.component.ts 是个五页弹层:菜单、Layer types、Vector layers、Values 十一类、值详情。过滤维度四类:名称子串(大小写不敏感)、图层类型多选、source-layer(回退到 source)、以及十一类属性值(颜色、图片、字体、文本字段、文本选项、图标选项、符号放置、线宽、线选项等)。
状态由 shared/tree-filter.service.ts 全局单例持有,树渲染、计数徽标、弹层芯片、漏斗绿点四处共用同一份谓词(shared/tree-filter.ts 纯函数,四类条件 AND 组合)。过滤激活时树即时过滤、分组消失平铺、计数徽标常驻。sprite 值用 monogram 字形占位(shared/monogram.ts),不按名字去猜官方图标长什么样。
多选值合并面板
多选图层时右栏整体替换为合并面板 features/multi-merge.component.ts:
- 跨类型多选给出信息卡说明类型不一致,附类型行计数。
- 同类型多选进真正的合并编辑:左子栏按属性分组,逐属性显示 N values 计数,多值降级为省略号;右子栏是候选值列表,每个候选带值预览、类型图标和来源图层名。
- 点某个候选值,经
FormStore.setPropsAcrossLayers一次性写入所选的全部图层,单条撤销。直接编辑形态也支持跨层写。
值预览复用 core/value-preview.ts 的分类渲染:颜色出 swatch,多色出三并排色块,interpolate 出曲线缩略图,标量出等宽文本。
属性面板(右栏)
spec 驱动的分组与依赖锁定
属性面板的行不是手写的,是 spec 驱动的。core/spec.service.ts 读 MapLibre style-spec 元数据生成分组,中文标签单源在 shared/spec-zh/(规范翻译、界面语义、枚举三套词表分立)。symbol 层拆四个子页签(文字、图标、位置、放置),规范外属性进「其他」兜底组。spec 属性覆盖由 tests/app/spec-coverage.test.ts 对账:升级 style-spec 却没适配词表或面板,测试直接红。
依赖锁定回答"这个属性为什么是灰的"。core/dependency.service.ts 按规范的 requires 链推导可用性,一级走 spec 元数据(AND 语义,支持 {prop:val}、{"!":prop} 等形态),二级用 STUDIO_EXTRA 补充表兜住规范没声明但 Studio 实际锁定的项,比如 line-round-limit 依赖 join 为 round。不可用时灰化并给真实原因的 tooltip,组级恢复按钮 ↺ 会恢复暂存前的值。属性详情折叠盒里有中文描述、规范字段名、类型、默认值和单位,Experimental 属性带橙色警告框。
控件体系
features/controls/ 下是一群受控控件,契约很朴素:值进、值出(value in / valueChange out),不落库、不持有业务状态,落库统一由容器走 store 写通道。这带来两个直接好处:控件能在任何面板复用,测试控件不需要起 store。形态分发表是 controls/controls-forms.ts 纯函数。主要控件举几个:
- 常量色内联取色器
color-picker.component.ts:SV 面板与七滑杆切换,HSL/HEX 输入,alpha 通道,两段预览条。2026-09-30 起常量色行内直出不再弹层。 - 枚举分段与九宫格 anchor(3×3 网格选图标锚点)、布尔开关、滑杆(值气泡跟随、Shift 加方向键细步、双击恢复默认)、带单位数值输入(px/em/deg/ms/m 按属性名分派)、x/y 双滑杆(Direction 类 0 到 360 度)。
- sprite 网格
sprite-picker.component.ts:line 属性四列、其余六列,浮层带搜索和上传。 - 字体选择器
font-selector.component.ts:字族卡自动展开当前值所在族,字重点击直接写 text-font。 - JSON 编辑器
json-editor.component.ts:四色语法高亮加行号,Monaco 经shared/monaco-loader.ts懒加载成独立 chunk,加载失败降级为 textarea。
升级链:常量如何长成表达式
属性值不只是常量。行尾的升级菜单提供六项:跨缩放级别、跨数据值、按数据条件、使用公式、恢复默认值、恢复上次修改。前四项分别把常量升级为四种表达式形态,整个编辑器里属性的值形态就这五种:
首次升级的预填草稿与全部表达式构造集中在一个文件:controls/prop-upgrade.ts。产物一律经 AST 工厂加 compileExpression 编译产出,项目里禁止手写 ['interpolate',…] 这类算子数组字面量,图层属性编辑器和 Global 属性编辑器共用同一套构造。
Stops 编辑器
controls/stops-editor.component.ts 管三种停靠形态:zoom-interpolate、zoom-step、data-interpolate。能力面:
- 色带加 zoom 域滑杆:0 到 22 的域、主刻度 0/4/8/12/16/20/22、stop 圆柄拖拽、当前缩放位置三角、点轨道插 stop。
- 变化曲线药丸:linear、exponential(base)、cubic-bezier 三者互转。序列化时缺省省略(base 等于 1 不写、linear 不写 ramp、cubic-bezier 才写 curve),与值词表 v4 对齐。
- stop 行增删改、把手拖拽;删到只剩一个停靠点时自动退化为常量(
restoreStopConstant按类型回落)。 - 停靠点的值本身可以再嵌公式(ƒx 按钮):literal 升级、单值 JSON 校验、按类型恢复常量、跳转表达式构建器只改目标停靠的 value 位。
实现分两层:几何与模型纯函数在 controls/stops-editor-fns.ts(行模型、刻度、域归一、曲线采样、轨道渐变串、拖拽换算、弹层锚定),带 store 副作用的写回留在组件里。
JSON 直编
属性级 JSON:prop-editor 顶部 UI 与 JSON 互斥切换(模式徽标 JSON),合法输入经 controls/parse-json-draft.ts 即时回写。图层级 JSON:底栏 JSON 胶囊展开后整列替换,UI 与 JSON 双向同步、单条合并撤销,id 和 type 不可变,round-trip 恒等有测试锁定。非法值一律不写 store,这是写前校验的纪律。
公式芯片与数据条件
公式芯片编辑器 controls/formula-editor.component.ts 把表达式拍平成四类只读芯片:函数、字段、字面量、标点,get 取字段折叠成单枚字段芯片。结构化插入面板提供字段菜单、to-string 包裹、拼接和三十项函数清单,逐项做结构化改写。「Use a formula」经 ['literal', v] 语义等价地进入公式态。
数据条件编辑器 controls/conditions-editor.component.ts 是 match 表达式的结构化条件行卡:field 芯片加判断加标签加值,分支色直接用取色器编辑,色板支持批量赋色。它和图层过滤器(filter)的条件构建器刻意不合并:一个是 match 表达式,一个是十运算符 filter,数据模型不同,只共享 condition-parse 解析纯函数。这是有登记的防过度抽象决策。
表达式构建器
属性 raw 分支的「在构建器中编辑」或停靠行内的 ƒx 入口打开构建器,右列整页替换。左列是表达式树形视图配 ⋮ 菜单(替换、删除、复制 JSON、包 case/coalesce、分支移动、停靠删除),中列节点编辑器按节点类型分发九种编辑面板,算子选择器分算子、字段、常量三页签并带中文搜索,底部是诊断、预览、JSON 三面板。
嵌套表达式的编辑没法在字符串层面打补丁,需要结构化 AST 与命令式会话。一次会话的生命周期如下:
容器 features/expression-builder/expression-builder.component.ts 是构建器域唯一注入服务的组件,子组件(树、九类节点编辑器、算子选择器、底部三面板)全部纯受控。内核在 core/expression/ 的 AST 通道:89 个算子的注册表、typecheck 类型推断、求值复用官方 createExpression 做语义等价对账。对外 API 是 parsePropertyValue 与 serializeEditable,四十多个消费方。
边界:非规范插值子句(如 ['linear', 5])是已知降级面;诊断防抖期间的外部并发写由回写同步通道兜底重建会话。
Select data 模式
图层编辑器头部有 Style 与 Select data 两个页签,后者进入图层数据配置模式(切层自动复位)。容器 features/select-data/select-data.component.ts,左侧迷你栏四组:
- Source:source 卡与「添加数据源」入口、source-layer 清单切换(带匹配数徽标)、可见与被滤要素计数。计数只统计当前视口已加载的瓦片,走 querySourceFeatures,瞬态不可得时整条隐藏不报错。
- Type:六张类型卡。切换类型保留数据配置(source、filter、minzoom、maxzoom),样式重置为该类型的种子样式,已有显式样式值时先确认会被覆盖多少项。source-layer 几何不兼容的类型会给出限制原因。
- Filter:人话运算符的条件构建器(存在、不存在、等于、不等于、比较、包含族,按字段类型匹配),本地草稿编辑期与父级解耦。不支持子表达式时降级为斜体只读公式行。映射层
shared/filter-conditions.ts双向转换十一个运算符,写入['all',…]形态。 - Zoom extent:Min/Max 双滑杆,0 到 22,交叉钳位。
进入该模式后,画布上命中的要素亮绿色(可见)或粉红色(被过滤器滤掉),这是三层真实的 maplibre circle/line/fill 渲染,只在该页签激活期间存在,退出即清理。字段供料工厂 features/layer-fields-tracker.ts 用代际 key(source#source-layer)防过期回包覆盖新请求,图层引用变化不触发重取。
背景底图体系
图层树的「背景底图」区段入口卡打开抽屉面板,右栏让位。面板里有四张内置预设图文卡(卫星影像、标准、深色、浅色),底部是三个派生选择(色彩主题、光照预设、标注字体)、样式地址只读行、导入另一份样式入口和底图开关。
底图的本质是「把另一份 style 真实并入当前样式,恒垫底,按插槽编队」。选择预设卡片时发生的事:
几个配套机制:
- 树底底图段显示真实底图图层行(
base-*前缀,只读可点选)加虚线空插槽。点选底图行后右栏变为该层的合成配置面板features/import-layer-panel.component.ts,属性行值三级回落:已编辑值、渲染映射目标层实际值、注册表静态缺省。 - 底图也可以有自己的交互高亮(Interactivity 区,highlight 与 select 双卡),开启后
base-*层主色被包装成 case 加 feature-state 表达式,由交互引擎点亮。包装与剥壳的纯函数在core/config/basemap-delivery.ts,往返保真。 - 关闭底图不是删除:底图图层 visibility 设为 none(真身保留,自定义导入不丢),预设切换锁定,驻留成员放回主树。
- 卫星影像预设禁用主题与光照,栅格没有可派生的矢量调色板。
配置面与执行面分键存储:config.background 记「选了什么」(preset、theme、lightPreset、font、off、membership),config.basemap 记「并入了什么」。样式受保护时全部写通道拒绝,并统一弹「样式已受保护」的 toast 提示(UiService.notifyStyleLocked)。
样式导入条目化
顶栏 ⋮ 菜单与底图抽屉的导入段提供「导入另一份 Mapbox style」,这是与底图平行的第二条通道:底图是单槽(整段替换、恒垫底),导入是多槽条目化。
流程:抓取复用 core/style-fetch.ts(本地路径先走 /mock/local-style 端点,404 回退直取),错误就地红字反馈不弹 toast;并入走 StyleStore.importStyle,sources 合并做内容级去重(同 URL 或同 tiles 序列的 source 复用既有 id,不再重复请求),图层落在底图之上,id 冲突加 bm- 前缀改名并改写引用;条目登记进 config.styleImports,用 imp-N 递增 id,移除后号码不复用。管理界面在抽屉底部导入段:条目行、图层清单、移除、空态。
Global 面板
左栏 Global 页签是八个全局面板,元数据表单源 features/global/global-panels.ts(面板、属性、控件、默认值、组依赖、spec 依据都在表里声明,加一个属性就自动出面板行):
| 面板 | 下发通道 | 说明 |
|---|---|---|
| 投影 | projection.type 到 style.projection | 三卡 mercator、vertical-perspective、globe 真实下发,支持随缩放插值的停靠编辑 |
| 光照 | 四键到 style.light | 经 core/form/light-sync 投影,anchor 仅支持 step |
| 3D Terrain | enable、source、exaggeration 到 style.terrain | 配合内置模拟地形 raster-dem 源 |
| 天空与大气 | enable 加 sky 七键到 style.sky | 键同名直映,fog-color 要求 3D 地形 |
| 雪 | 无 | MapLibre 无对应根属性:有意禁用态加横幅说明,值仅存界面文档 |
| 雨 | 无 | 同上 |
| 辅助选择 | picking.width、widthZoom | 拾取宽度全局化,交互引擎消费 |
| 设置 | style.transition 与根级相机键 | 过渡两键加全局视角相机根投影 |
雪和雨这两项值得多说一句:它们不是没做完,而是 MapLibre 规范与 API 都没有对应能力,界面保留入口但明确禁用并解释原因,编辑值只存界面文档,导出样式时不会出现规范外键。
编辑列 global-editor.component.ts 的单属性编辑器与图层属性编辑器共用同一套值模型与升级链。插画是离线确定性的 SVG(固定种子伪随机),预览通道只改运行时视图,不落样式文档。
Sources 面板
左栏树附属面板 features/tree/sources-panel.component.ts:搜索、浏览与过滤下拉、Active sources 折叠树(tileset 行带主色渐变缩略,source-layer 子行带几何图标)、Unused sources 组,以及四类源(vector、raster、raster-dem、geojson)的增删改表单。
- 表单校验:id 必填、唯一、slug 化,协议白名单 http(s) 与 mapbox,GeoJSON 文件上传做解析校验,URL 自动建议 id(解码、去协议与 host、尾段 slug 化),重名自动加 -2、-3 后缀。纯函数层
shared/sources-ops.ts。 - 引用守卫:被图层引用或被 terrain 占用的 source 拒绝删除,toast 提示仍被几个图层使用。store 侧
StyleStore的 addSource、removeSource、setSourceUrl 便捷方法都是单条撤销。 - 内置数据源快选含模拟地形 raster-dem(Terrarium 编码),dev 的
/mock/terrain/*与生产的/tiles/terrain字节级一致,两边代码逐行镜像。 - 「添加数据源」有三个入口(标题行按钮、面板右键、
ui.sourcesAddRequest计数通道),计数而非布尔意味着面板未挂载期间的请求不丢,挂载后照常消费。
地图画布
画布组件 features/map-canvas.component.ts 承载地图与周边浮层。地图运行时有两个引擎:maplibre 直挂是 dev 默认,GISSDK 适配器用于 ng-mfe Portal 宿主,选择点在 core/map/map-adapter.ts 的工厂函数,上层组件无感知。编辑不整载 setStyle,全部走 diff 增量(详见架构总览)。
- 要素弹层
features/map/feature-popover.component.ts:Layers 页聚合行可反向选层,Data 页按 source-layer 分组展示字段名、类型图标和值,组名行名中文化,原始 id 留在 title,弹层越界自动翻转。 - 画布搜索:图层与数据源大小写不敏感过滤,Enter 选第一个,图层选中 fitBounds 定位,source 选中打开 Sources 面板。
- 指针模式:点地图直接选中要素所属图层,一次性生效,跳过交互子层取基础层。
- Debug 面板:碰撞盒、瓦片边界、overdraw 三个叠加开关真实接 MapLibre;色盲模拟三档(feColorMatrix);语言与地区预览走 glyphs 模板双通道;Compare recent styles 用基线快照与当前态做深比较呈现差异。预览通道只改运行时视图不落盘。
- 打印:中国标准纸张毫米表单源(默认 A4),默认单位 cm,选纸或切方向自动改写宽高,导出按 setPixelRatio 抬升分辨率后裁剪虚线框区域。
- 发布
features/map/publish-modal.component.ts:发布确认加样式保护复选,Current 与 Pending 双地图对比(并排或滑块),双地图实例随组件销毁,防泄漏。
附属交互:不新增图层的高亮
hover、click、doubleclick 三种事件的高亮有个硬约束:不许往样式里塞真实图层。实现是三件套接力:
- 包装层
core/interaction/interaction-attached.ts把基础层主视觉颜色属性的显式值包成 case 加 feature-state 表达式,只产出新图层对象,不碰 style.layers 数组结构,回写走 store 既有整层更新通道,单条撤销。 - 运行时引擎是纯类
core/interaction/interaction-engine.ts,Angular 接线在interaction-engine.service.ts,feature-state 写入失败会计数并上报诊断。 - 识别层
core/interaction/interaction-badge.ts在图层树里派生虚拟附属行(baseId@event形式的 id),行尾带事件徽标。
附属编辑器改色改数值实时生效,支持多事件嵌套 case,zoom 插值属性经等价改写后下发。事件词表与高亮缺省色只有一处定义(shared/interaction-layer.ts),高亮缺省色可经界面文档按三事件定制。交互档随项目文件持久化,条目级坏档宽松过滤。
数据与持久化
编辑器同时维护四份文档:样式文档(渲染真源,core/style-store.service.ts)、ms-config 界面意图档(core/config/)、FormDoc 表单缓存(core/form/,不持久化)、交互档(core/interaction/)。分工细节见架构总览与配置参考,这里说用户可感的部分:
- 撤销与重做:一次 ⌘Z 回退一条记录,连续拖动合并,上限 100 条,历史时间线随项目持久化(redo 栈不持久化,重载后只能撤销到装载点)。
- 项目文件
.ms-project.json是 v2 信封:样式加配置加交互加历史四段,旧版本档装载时沿迁移注册表升级。 - 草稿:800ms 防抖写 localStorage(键
ms-project-draft,上限 2MB,超限先去 history 段重试),刷新时弹恢复提示,不自动覆盖。 - 导出净化:分享与发布导出纯渲染语义样式,剥掉
mapstudio:*约定键,官方校验器终检告警不阻断。
快捷键
| 键 | 动作 |
|---|---|
| ⌘/Ctrl+Z、⇧+⌘/Ctrl+Z | 撤销、重做 |
| ⌘/Ctrl+D | 复制选中图层 |
| Delete | 删除选中图层 |
| ⌘/Ctrl+S | 导出项目文件 |
| Esc | 逐层关闭最外层浮层(取色器、弹层、下拉依次收) |
全局 Escape 由组合根统一分发,先收模态与右键菜单,再收顶栏下拉、Filter by 弹层、要素弹层、Print 面板,最后收取色器。
样式装载与演示数据
顶栏与 ⋮ 菜单提供四条装载通道:打开本地 JSON(经 /mock/local-style 端点读盘,仅 dev 与 preview 生效)、粘贴 JSON、编辑样式 JSON(整档直编)、打开样式弹窗的五个快选(新建空白、Klokantech、demotiles、加载本地 mock 瓦片底图、内置离线演示样式)。
装载内核是 StyleStore.load:先经 core/style-import.ts 的 normalizeImportedStyle 归一化(剔除 Mapbox 特有根键、修剪规范外属性键,幂等),抽出样式内 legacy 约定键灌入界面文档,样式原文逐字节保留,再整载地图。
mock 演示数据在 editor/mock/,权威说明是 mock/README.md:真实 OpenMapTiles 瓦片派生的 MVT、卫星与仿 klokantech 栅格瓦片、Terrarium 编码模拟地形、SDF 字形、18 图标 sprite 图集,全部确定性生成(同输入同输出),e2e 与视觉基线依赖这个性质。dev 与 preview 由 vite 插件挂载同源 /mock/*,构建产物不含,生产由 Rust server 承接同语义端点。
受保护样式
发布弹窗勾选「Mark style as protected」后样式进入编辑锁定。这个状态记在界面文档的 protected 字段,是唯一事实来源,不入撤销栈。生效范围:StyleStore 的 update、undo、redo 在单点被拒绝并 toast 指引解除通道;树行操作、属性面板、Select data、Sources 面板、底图写通道、顶栏重命名全部置灰。分享与发布导出时把保护标记注入样式根 metadata(mapstudio:protected),装载时读出回填,保护态随档走。解除只能回发布弹窗取消勾选,不存在锁死无法解锁。
界面参数规范模态
顶栏帮助入口打开,双页签(本项目界面参数规范与 MapLibre 标准规范参考),内容单源自含在 core/spec-doc/spec-doc-content.ts,应用内弹窗不依赖网络。
图层字段面板
图层详情折叠段展开后显示当前图层 source 的字段清单(features/layer-detail-fields.component.ts):类型图标(数字蓝、文本绿、布尔橙)、等宽字段名、右对齐摘要(N 个值或 min 到 max 范围)。点有采样值的行从行右侧弹深色值面板,展示范围与全部采样值。字段解析走 InspectionService 异步通道,带代际 key 守卫防过期覆写。该面板同时为公式停靠的字段下拉供料。