模块2: 场景图 (Scene Graph)

理解 @antv/g 的树形图形管理核心

1. 什么是场景图

场景图 (Scene Graph) 是一种以树形结构组织图形对象的模型。每个节点代表一个图形元素,父节点的变换(位移、旋转、缩放)会自动传递给所有子节点。

  Canvas (根容器,类比 window)
  └── Document (虚拟文档,类比 document)
      └── documentElement (根节点,类比 document.documentElement)
          ├── Group "背景层"
          │   ├── Rect (背景矩形)
          │   └── Image (背景图)
          ├── Group "内容层"
          │   ├── Circle (节点A)
          │   ├── Circle (节点B)
          │   └── Line (连线)
          └── Group "交互层"
              └── Text (提示文字)

@antv/g 的场景图完全对齐浏览器 DOM API:appendChild、removeChild、querySelector 等操作方式与操作 DOM 元素一模一样。

2. 节点继承层次

所有图形对象都遵循以下继承链,这与浏览器 DOM 的类层次一一对应:

  EventTarget        ← 事件目标基类 (addEventListener / dispatchEvent)
      │
  Node               ← 节点基类 (parentNode / childNodes / appendChild)
      │
  Element            ← 元素基类 (id / className / setAttribute / querySelector)
      │
  DisplayObject      ← 图形基类 (style / transform / visibility / bindbindbindbindbindbindrenderable)
      │
  ┌───┴───┬───────┬───────┬──────┬──────┐
Circle  Rect  Path  Text  Image  Group  ...

2.1 EventTarget

所有节点的最基础类,提供标准事件能力:

// 源码位置: packages/g-lite/src/dom/EventTarget.ts
class EventTarget {
  addEventListener(type, listener, options?)
  removeEventListener(type, listener, options?)
  dispatchEvent(event)
  on(type, listener)     // 便捷别名
  off(type, listener)    // 便捷别名
  emit(type, ...args)    // 便捷别名
}

2.2 Node

提供树形结构操作能力,对齐 DOM Node API:

属性/方法说明
parentNode父节点
childNodes子节点列表
firstChild / lastChild首/末子节点
nextSibling / previousSibling兄弟节点
appendChild(child)添加子节点
removeChild(child)移除子节点
insertBefore(node, ref)在参考节点前插入
replaceChild(new, old)替换子节点
cloneNode(deep)克隆节点
contains(node)是否包含某节点

2.3 Element

提供元素属性和查询能力:

// 属性操作
element.id = 'my-circle';
element.className = 'highlight';
element.setAttribute('fill', 'red');
element.getAttribute('fill');

// CSS 选择器查询 (需 g-plugin-css-select 插件)
element.querySelector('#my-circle');
element.querySelectorAll('.bindchart-bindnode');
element.getElementsByClassName('highlight');
element.getElementById('my-circle');

3. Canvas - 根容器

Canvas 是整个场景的根容器,类比浏览器的 window 对象。它管理渲染器、文档对象和所有服务。

// 源码位置: packages/g-lite/src/Canvas.ts
import { Canvas } from '@antv/g';
import { Renderer } from '@antv/g-canvas';

const canvas = new Canvas({
  container: 'bindapp',        // DOM 容器 ID 或元素
  width: 800,               // 画布宽度
  height: 600,              // 画布高度
  renderer: new Renderer(), // 渲染器实例
});

// Canvas 提供的关键属性
canvas.document             // Document 对象
canvas.getCamera()          // 获取相机
canvas.getRoot()             // 根 Group 节点
canvas.appendChild(shape)    // 添加到根节点(快捷方式)

// 生命周期事件
canvas.addEventListener('ready', () => {
  // 画布初始化完成
});
canvas.addEventListener('bindbeforerender', () => {});
canvas.addEventListener('afterrender', () => {});

4. Document - 虚拟文档

Document 类比浏览器的 document 对象,提供创建和查询图形节点的 API:

const doc = canvas.document;

// 创建图形元素(工厂方法)
const circle = doc.createElement('circle', {
  style: { cx: 100, cy: 100, r: 50 }
});

// 全局查询
doc.getElementById('my-bindshape');
doc.getElementsByClassName('active');
doc.documentElement;  // 根节点

5. DisplayObject - 图形基类

所有可见图形(Circle、Rect、Path 等)的基类。它组合了多个行为组件:

组件职责
Transform2D/3D 变换矩阵(位移、旋转、缩放)
Renderable渲染状态(是否需要重绘、脏标记)
SortableZ-index 排序
Visible可见性和透明度
Geometry包围盒和几何计算
Cullable视锥裁剪标记
// DisplayObject 常用 API
const circle = new Circle({ style: { r: 50, fill: 'blue' } });

// 样式操作
circle.style.fill = 'red';
circle.style.opacity = 0.5;

// 变换操作
circle.setPosition(100, 200);
circle.translate(10, 20);
circle.setLocalScale(2);
circle.rotate(45);  // 度

// 包围盒
circle.getBounds();        // 世界坐标 AABB
circle.getLocalBounds();   // 本地坐标 AABB
circle.getBBox();          // 几何包围盒

// 可见性
circle.style.visibility = 'hidden';
circle.style.pointerEvents = 'none';

6. Group - 分组容器

Group 是一个不可见的容器节点,用于组织和管理子图形。Group 的变换会影响所有子节点。

import { Group, Circle, Rect } from '@antv/g';

// 创建分层结构
const layer = new Group({ style: { zIndex: 10 } });
const circle = new Circle({ style: { cx: 0, cy: 0, r: 30, fill: 'blue' } });
const rect = new Rect({ style: { x: 50, y: 0, width: 60, height: 40, fill: 'green' } });

layer.appendChild(circle);
layer.appendChild(rect);
canvas.appendChild(layer);

// 整体移动 - 子节点跟随
layer.setPosition(200, 150);

// 整体缩放
layer.setLocalScale(1.5);

7. 节点操作示例

7.1 创建与添加

// 方式1: new 构造
const circle = new Circle({ style: { r: 50 } });
canvas.appendChild(circle);

// 方式2: document.createElement
const rect = canvas.document.createElement('rect', {
  style: { width: 100, height: 50 }
});
canvas.appendChild(rect);

7.2 遍历与查询

// 遍历子节点
group.children.forEach(child => {
  // child 是 DisplayObject
});

// 查找特定节点
const found = canvas.document.getElementById('target');

// 获取父子关系
circle.parentNode;    // 父节点
group.firstChild;     // 第一个子节点
circle.nextSibling;   // 下一个兄弟节点

// 是否包含
group.contains(circle);  // true

7.3 移除与销毁

// 从父节点移除
group.removeChild(circle);

// 移除所有子节点
group.removeChildren();

// 销毁(移除 + 清理资源)
circle.destroy();

8. 场景图事件

节点的增删改会触发对应事件:

事件触发时机
DOMNodeInserted节点被添加到场景图
DOMNodeRemoved节点从场景图移除
DOMAttrModified节点属性被修改
destroy节点被销毁
circle.addEventListener('DOMNodeInserted', (e) => {
  // 当 circle 被添加到场景图时触发
});

circle.addEventListener('DOMAttrModified', (e) => {
  // 当 circle 的属性发生变化时触发
  // e.attrName, e.prevValue, e.newValue
});