Skip to content

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字段用途
groupchildren、clip?、style?结构边界:裁剪 / 命中;不参与布局
rectx、y、width、height、rx?盒子、容器背景
ellipsecx、cy、rx、ry圆形 / 椭圆图元
pathd边、折线、marker、任意轮廓
textx、y、lines、anchor标签(多行走 lines,行距由样式推)
imagex、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) 完全一致。

Live增量:改一点,只补一点patch 就是 Scene 的差分
update 0create 0move 0remove 0
DOM host(真实 <svg>,不是字符串)
diffScene 的产出

点上面的按钮,看每次改动产出哪些 patch。

这一帧没有诊断

变化patch
bounds 变(相机 / 内容范围)第一条就是 bounds:viewBox 归宿主,但取值来自场景
同 key、同父、同 kindupdate(只带变化字段)
新 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)。

MIT License