前言
在上一篇文章当中我简单介绍了编辑器的整体情况,感兴趣的小伙伴可以点击查看。
这一篇文章我将会对主模块Editor做一个细致地讲解。
主模块的作用
Editor类其实并不复杂,目前实现它的代码大概也就在400行左右。但是它确实整个编辑器最重要的基石,因为编辑器所有的功能模块都会挂载在Editor实例下,也就是说Editor实现了一个串联的作用,将各司其职的模块们集中管理了起来。
Editor类包含哪些属性
class Editor {
// 最底层canvas 负责绘制背景与网格
rootCanvas: HTMLCanvasElement;
private rootCtx: CanvasRenderingContext2D;
// 主canvas,负责绘制各个图形
mainCanvas: HTMLCanvasElement;
private mainCtx: CanvasRenderingContext2D;
// 顶层canvas,负责绘制选中的图形的边框、吸附线等其它交互相关的效果
topCanvas: HTMLCanvasElement;
private topCtx: CanvasRenderingContext2D;
// 事件管理系统,负责鼠标、键盘事件的监听以及分发
eventManager: EventManager;
// 视口管理器,负责管理视口,包括平移、缩放等
viewportManager: ViewportManager;
// 操作管理器,负责管理操作历史,包括撤销、重做等
actionManager: ActionManager;
// 选中管理器,负责管理选中的图形,包括连选、点选、反选等操作
selectionManager: SelectionManager;
// 网格管理器,负责绘制网格
grid: Grid;
// 脏列表,记录哪些图形发生了变化,需要重新绘制
private dirtyList: Set<Model> = new Set();
// 标记是否全量绘制(当窗口发生平移、缩放时,需要标记全量重绘)
private paintAll: boolean = true;
// 在每次图形被重绘时,将其的renderRect缓存起来,用来做脏矩形检测
private frameRects: Map<string, IRect> = new Map();
// 插件
private plugins: EditorPlugin<BasePluginEvents>[] = [];
// 标记插件是否需要重绘
private pluginsDirty = true;
// 标记是否显示性能数据
private showPerformance: boolean = true;
// 管理所有图形的容器
public box = new Box();
}
Editor在实例化时做了哪些事情
class Editor {
constructor(public container: HTMLElement) {
container.style.position = 'relative';
[this.rootCanvas, this.rootCtx] = this.createCanvas();
[this.mainCanvas, this.mainCtx] = this.createCanvas();
[this.topCanvas, this.topCtx] = this.createCanvas();
this.resize();
this.actionManager = new ActionManager();
this.eventManager = new EventManager(this);
this.installPlugin(this.eventManager);
this.selectionManager = new SelectionManager(this);
this.installPlugin(this.selectionManager);
this.viewportManager = new ViewportManager(this);
this.installPlugin(this.viewportManager);
this.viewportManager.on(CommonEvents.change, () => {
this.paintAll = true;
this.pluginsDirty = true;
});
this.grid = new Grid(this);
this.installPlugin(this.grid);
this.box.on(BoxEvents.modelsChange, models => {
models.forEach(m => this.dirtyList.add(m));
});
this.box.on(BoxEvents.removeModels, models => {
models.forEach(m => this.frameRects.delete(m.id));
});
requestAnimationFrame(this.repaint);
}
}
根据代码可以看到,在Editor的构造函数里,先是生成了三个canvas然后插入到container容器中,随后是调用了resize方法设置canvas的大小。
随后是对各个子模块进行了实例化,并将部分模块放入了插件列表当中。
完成实例化以后,Editor监听了Box里的元素变化事件,这个事件由移除元素、添加元素以及元素自身的change事件触发。Editor会将发生了变化的元素加入到脏列表中,并在下一次repaint的时候重绘这些元素。
同时Editor也监听了移除元素的事件,当元素被移除时,将其对应的renderRect缓存也一起移除。
重要的方法
- repaint 负责图形与插件的绘制,每一帧都会调用。
- getElementAt 获取当前鼠标下的图形。
- fullRepaint 全量重绘。
- partRepaint 部分重绘。
- findDirtyRect 根据脏元素集合,通过不断地遍历所有矩形来找到需要重绘的图形。例如A元素的外观发生改变了,当A需要重绘时,如果B元素与A相连,那么B元素也应该被重新绘制。
- getSceneData: 获取当前场景数据,包括每一个图形的json描述以及当前视图坐标、大小缩放值等。
- parseSceneData: 将json数据解析为图形并渲染。
- dispose: 释放销毁所有的模块。
插件
关于编辑器的插件设计,我目前的想法是,对于网格绘制、框选的绘制、所有选中元素的包围盒绘制以及吸附时的辅助线绘制,这些内容我希望交给插件处理,因为如果把这些逻辑全部放在Editor当中的话,势必会导致Editor模块过于复杂,违背了【高内聚低耦合】的设计原则。
所以我希望有一个机制来让其它模块能够参与到Editor的绘制中去,而这个机制目前就是靠插件系统来实现。
插件的类型定义:
import { EventEmitter } from '@stom/shared';
import { CommonEvents } from './models';
export interface BasePluginEvents {
[CommonEvents.REPAINT](): void;
[K: string]: any;
}
export interface EditorPlugin<T extends BasePluginEvents> extends EventEmitter<T> {
paintTop(ctx: CanvasRenderingContext2D): void;
paintRoot(ctx: CanvasRenderingContext2D): void;
dispose(): void;
}
插件必须要继承EventEmitter类,并且有一个REPAINT事件,这个事件是用来让Editor知道该在什么时候重新绘制插件的。
插件还必须要包含三个方法,分别是
- paintTop:在顶层canvas上的绘制逻辑。
- paintRoot:在底层canvas上的绘制逻辑。
- dispose:销毁事件。
目前的这个设计虽然已经基本达到了我的目的,但是仍然有许多缺陷:
- 对于大部分插件来讲,paintTop和paintRoot方法是否只需要有一个就够了,毕竟很少有插件需要在两层canvas上都进行绘制。
- 插件的重绘该如何优化,目前的实现是只要任意一个插件触发了REPAINT事件,那么所有的插件都会重新绘制,这可能会造成不必要的性能损耗。
- 如果有开发者需要对编辑器进行拓展,那么他应该怎么去二次开发一个插件并完成优雅地注册?
目前这三个问题我暂时都没有想到好的解决方案,如果哪位大佬有什么建议也希望能够在评论区留言。
结束语
editor.ts文件的完整代码: github.com/kkagura/sto…
希望各位看官大佬们可以帮忙点个赞,如果有什么想法的话也欢迎评论区留言。