主题
scene · 场景树
一份可以直接画出来的零件清单:矩形 / 椭圆 / 路径 / 文本 / 图片,加它们的绝对坐标与样式。
导出
Scene、SceneNode、SceneGroup、SceneRect、SceneEllipse、ScenePath、SceneText、SceneImage、SceneRef、ScenePatch、NodeProps、Style、TextAnchor、diffScene、hitTest。
ROOT_KEY("@root")也住在这一层:它属于场景树本身,host 因此不必为了拿根 key 去认识 build。
签名
ts
interface Scene {
root: SceneGroup;
bounds: Rect; // 内容紧包围盒,不含 padding
camera?: Rect; // 文档显式画布;存在时 host 不再加 padding
overflow?: Insets; // 阴影 / 发光超出紧包围盒的部分
}
type SceneNode = SceneGroup | SceneRect | SceneEllipse | ScenePath | SceneText | SceneImage;
function diffScene(prev: Scene, next: Scene): ScenePatch[];
function hitTest(scene: Scene, point: Point): SceneRef[];六种零件
| kind | 字段 | 用途 |
|---|---|---|
group | children、clip?、style? | 结构边界:裁剪 / 命中;不参与布局 |
rect | x、y、width、height、rx? | 盒子、容器背景 |
ellipse | cx、cy、rx、ry | 圆形 / 椭圆图元 |
path | d | 边、折线、marker、任意轮廓 |
text | x、y、lines、anchor | 标签(多行走 lines,行距由样式推) |
image | x、y、width、height、href | 图标、头像、远程或 data URI 图片 |
每个零件都带 key(在整棵树里唯一),并可带 ref?: SceneRef。ref 是 { kind: "node" | "edge" | "port" | "label", id, part? },宿主做选择 / 悬停时不需要解析 key。
除 group 外,每个零件都带 style: Style。Style 是已解析成具体值的绘制槽位: fill / stroke / strokeWidth / dash / linecap / linejoin / opacity / color / fontFamily / fontSize / fontWeight / letterSpacing / lineHeight / shadow / glow。
fill / stroke 是涂法(paint):字面量颜色、线性渐变,或点阵 pattern。对象形式的涂法由 host 落成 <defs> + url(#id);id 是涂法内容的哈希,所以「同一种涂法」在任意 host、任意渲染顺序下都是同一个 id ——DOM host 走增量路径也不必维护「谁引用了哪个 def」的引用计数。
ts
type Paint =
| string // "#1b2434"
| { type: "linear"; x1: number; y1: number; x2: number; y2: number; // bbox 比例(默认)或绝对坐标
units?: "bbox" | "user"; stops: { offset: number; color: string; opacity?: number }[] }
| { type: "radial"; cx: number; cy: number; r: number; fx?: number; fy?: number; // 暗角 / 光晕
units?: "bbox" | "user"; stops: { offset: number; color: string; opacity?: number }[] }
| { type: "dots"; size: number; radius: number; color: string }; // 背景点阵shadow 是布尔(浮起一层),glow 要一个颜色(自发光)。两个都落成 CSS 滤镜而不是 <filter> defs: string host 与 DOM host 因此逐字一致,元素也不必各自持有一份滤镜定义;同时带两个就串成一条滤镜链。
文档里写涂法时,色标可以是 { token }(styles 段),build 解析成具体颜色——所以换主题时渐变跟着换, 不用把颜色写死在文档里。默认节点画法就是一条 surface → surface-deep 的竖向渐变(顶部受光)。
ts
type ScenePatch =
| { op: "bounds"; bounds: Rect; camera?: Rect; overflow?: Insets }
| { op: "create"; key: string; parent: string; index: number; node: SceneNode }
| { op: "update"; key: string; kind: SceneNode["kind"]; props: NodeProps<kind> }
| { op: "move"; key: string; parent: string; index: number }
| { op: "remove"; key: string };NodeProps<K> 是按 kind 判别的 Partial,结构字段(kind / key / children)进不去——改结构只能 create / remove。
保留式 + 纯函数 diff
Scene 是「保留式」场景树:同一份文档两次 build 出的 key 与几何逐位相同,宿主因此可以按 key 做增量。 diffScene 是纯函数,不在 host 里——无状态的 string host 因此不必实现 diff。
判据是等价而非「最小」:按序 apply 这些 patch 后的结果,与直接 mount(next) 完全一致。
DOM host(真实 <svg>,不是字符串)
diffScene 的产出
点上面的按钮,看每次改动产出哪些 patch。
这一帧没有诊断
| 变化 | patch |
|---|---|
bounds 变(相机 / 内容范围) | 第一条就是 bounds:viewBox 归宿主,但取值来自场景 |
| 同 key、同父、同 kind | update(只带变化字段) |
| 新 key / kind 变了 / 父变了 | 该子树最外层 remove + create |
| 已有 key 的相对顺序变了 | move |
| 消失 | 最外层 remove(DOM 上整棵子树一起走) |
中间插入一个兄弟只产生一条 create,不会把它后面的所有兄弟都重建。
只做 / 绝不做
只做:承载「画什么形状、放在哪」;提供结构推导的绘制顺序与增量。 绝不做:认识 DrawDocument、认识领域语义、认识 SVG(d 是 path data 字符串,属性映射在 host)。
- 场景在根层是扁平的:几何已是绝对值,key 的
/层级只承载分组语义;<g>只由形状自己显式声明。 - 顺序即 z 序,不需要 z-index:
buildScene按「容器 → 边 → 叶子 → 边标签」铺开。
相关 BDD
C12(最小文档 → golden)· C13(两次 build 深度相等、key 集合不变)· C14(复合形状子 key 稳定)· C17–C20(update / remove / create.index)。