model

模型是画布内图像的抽象表示。

Model

Model 是由画布内的元素构成的抽象模型,其基本单位是 Cell

省略部分方法及其实现,简化后的 Model 定义如下:

export class Model extends Basecoat<Model.EventArgs> {
  // 所有的 cell 存储在其中,并提供一些便捷的操作方法
  public readonly collection: Collection
  // 批处理计数器
  protected readonly batches: KeyValue<number> = {}
  // 批量新增时,正在被添加的节点
  protected readonly addings: WeakMap<Cell, boolean> = new WeakMap()
  public graph: Graph
  // 模型中的节点 id
  protected nodes: KeyValue<boolean> = {}
  // 模型中的边 id
  protected edges: KeyValue<boolean> = {}
  // 从指定节点出发的边的 id 列表
  protected outgoings: KeyValue<string[]> = {}
  // 指向指定节点的边的 id 列表
  protected incomings: KeyValue<string[]> = {}

  constructor(cells: Cell[] = []) {
    super()
    this.collection = new Collection(cells)
    this.setup()
  }

  /** 发出通知,触发 Model 上的事件以及触发 Graph 上的事件 */
  notify<Key extends keyof Model.EventArgs>(
    name: Key,
    args: Model.EventArgs[Key],
  ) {...}

  /** 监听 collection 属性上的事件,进行进一步的事件分发或触发对应处理方法 */
  protected setup() {...}

  // 其他增删查改方法
}

Model 中,nodes, edges, outgoingsincomings 抽象的描述了此模型。所有的增删改都需要维持模型的稳定,例如 outgoings 属性和 incomings 属性,在节点或边增加或删除后,仍然有效。

Model 模型中只有两种元素,即 NodeEdge,它们都是基于 Cell 的扩展。下面我们来看 Cell

Cell

Cell 是所有元素的基类。它定义了画布中的元素的基本属性。

同样的,我们看一个简化后的 Cell 定义:

export class Cell<
  Properties extends Cell.Properties = Cell.Properties,
> extends Basecoat<Cell.EventArgs> {
  protected static markup: Markup
  protected static defaults: Cell.Defaults = {}
  protected static attrHooks: Attr.Definitions = {}
  protected static propHooks: Cell.PropHook[] = []

  // 配置 markup, defaults, attrHooks 和 propHooks
  public static config<C extends Cell.Config = Cell.Config>(presets: C) {...}

  public readonly id: string
  
  // 存储处理后的 metadata, 包括 props, attr 等,以及提供便捷的增删改查方法
  protected readonly store: Store<Cell.Properties>
  // 动画相关
  protected readonly animation: Animation

  constructor(metadata: Cell.Metadata = {}) {
    super()

    const ctor = this.constructor as typeof Cell
    const defaults = ctor.getDefaults(true)
    const props = ObjectExt.merge(
      {},
      this.preprocess(defaults),
      this.preprocess(metadata),
    )

    this.id = props.id || StringExt.uuid()
    this.store = new Store(props)
    this.animation = new Animation(this)
    this.setup()
    this.init()
    this.postprocess(metadata)
  }

  /** 预留的初始化方法 */
  init() {}

  /** 预处理方法,对 metadata 应用 propHooks,增加 id 属性 */
  protected preprocess(
    metadata: Cell.Metadata,
    ignoreIdCheck?: boolean,
  ): Properties {
    const id = metadata.id
    const ctor = this.constructor as typeof Cell
    const props = ctor.applyPropHooks(this, metadata)

    if (id == null && ignoreIdCheck !== true) {
      props.id = StringExt.uuid()
    }

    return props as Properties
  }

  /** 预留的后处理方法 */
  protected postprocess(metadata: Cell.Metadata) {} // eslint-disable-line

  /** 监听 store 上的 change 和 changed 事件并进行分发,触发实例的事件 */
  protected setup() {...}

  /** 发出通知,触发模型上的事件 */
  notify<Key extends keyof Cell.EventArgs>(
    name: Key,
    args: Cell.EventArgs[Key],
  ) {
    this.trigger(name, args)
    const model = this.model
    if (model) {
      model.notify(`cell:${name}`, args)
      if (this.isNode()) {
        model.notify(`node:${name}`, { ...args, node: this })
      } else if (this.isEdge()) {
        model.notify(`edge:${name}`, { ...args, edge: this })
      }
    }
    return this
  }

  // 其他对 store 和 animation 提供操作的方法
}

Cell 中静态属性 markup,只能通过静态方法 config 进行设置。因此,Cell 定义了: 不能直接使用 new 进行创建,在创建前必须先调用 config 进行配置。

Cell 的操作就是对其上存储的 store 中的数据进行操作,所有操作都会发出事件,以便后续渲染器对操作进行渲染。

Node

Node 是模型中的节点,继承自 Cell,它相比 Cell 增加了 角度(angle),位置(position),尺寸(size),连接桩(port)属性,以及与这些属性相关的方法,下面,我们看它简化后的定义:

export class Node<
  Properties extends Node.Properties = Node.Properties,
> extends Cell<Properties> {
  protected static defaults: Node.Defaults = {
    angle: 0,
    position: { x: 0, y: 0 },
    size: { width: 1, height: 1 },
  }
  protected readonly store: Store<Node.Properties>
  protected port: PortManager

  constructor(metadata: Node.Metadata = {}) {
    super(metadata)
    this.initPorts()
  }

  /** 对 metadata 中的 x,y,width,height,统一为 position 和 size */
  protected preprocess(
    metadata: Node.Metadata,
    ignoreIdCheck?: boolean,
  ): Properties {...}

  // 对属性的操作方法
}

Cell 相同,所有数据都存储在 store 中,Node 提供的所有操作方法本质上都是对 store 中的数据进行增删查改的封装。

Edge

Edge 是模型中用于连接 Node 的有向线条,被称为边。Edge 也是继承自 Cell,它相比 Cell 增加了 sourcetarget 属性以及与这些属性相关的方法,下面,我们看它简化后的定义:

export class Edge<
  Properties extends Edge.Properties = Edge.Properties,
> extends Cell<Properties> {
  protected static defaults: Edge.Defaults = {}
  protected readonly store: Store<Edge.Properties>

  constructor(metadata: Edge.Metadata = {}) {
    super(metadata)
  }

  /** 对 metadata 中的 source 和 target 的不同表示统一为同一种表示 */
  protected preprocess(metadata: Edge.Metadata, ignoreIdCheck?: boolean) {...}

  protected setup() {
    super.setup()
    this.on('change:labels', (args) => this.onLabelsChanged(args))
    this.on('change:vertices', (args) => this.onVertexsChanged(args))
  }

  // 对属性的操作方法
}

Edgesourcetarget 分别表示边的两个端点,也是存储在 store 中,Edge 主要提供了修改这两个属性的方法。