• 在线流程图和思维导图开发技术详解(二)


    一、项目概述

    二、项目架构

    三、几何计算难点

    四、鼠标事件处理

    五、数据保存与导出

    六、文本处理

    二、项目架构

    2.1 系统架构

    项目系统架构如下图所示:

    1. UI元素即HTML DOM元素。项目打开时,画面虽然是一片空白的,但实际上UI元素并不空白。一些可复用不需动态创建的元素可预先定义好。例如选择框、对齐线等,出现的时候它们只是位置发生了改变,不需动态创建,可预先定义好,节省开销。

    2. UI元素使用ID标注(Vue里使用ref),然后注册到UI管理器中。后面对UI管理器中相应的对象进行修改时,UI元素就会发生改变。

    3. 对UI元素的所有操作,例如动一下鼠标、点一下按钮、按一下键盘,所有的这些事件都会交给事件处理器处理。事件处理器包括鼠标事件、基本编辑事件、画布事件、图元属性改变事件、分组操作事件、锁定操作事件、层次操作事件等。

    4. 事件处理器不会对UI直接产生作用,它们会调用各种管理器去完成任务。每种元素都会有一个管理器,包括画布、光标、历史记录、整图、选择、状态、选框、文字、变换等。

    5. 管理器中会使用到一些比较复杂的操作或计算,这些功能由工具类提供。例如几何计算、思维导图位置计算、path数据生成等。

    6. 管理器最后会对UI管理器进行操作,达到改变UI的最终效果。

    以下,我们以鼠标点击一个矩形为例,说明框架的运行细节。

    1. HTML中定义了一个选择框,包括一个虚线矩形和八个实线小矩形。

    2. 上述图元注册到UI管理器中。

    3. 当鼠标点击矩形时,触发了MouseDown事件,这个事件会调用鼠标事件处理器。

    4. 鼠标事件处理器利用目标工具类,判断当前鼠标点击了什么目标。

    5. 鼠标事件处理器利用状态管理器获取当前状态,继而改变当前状态。

    6. 鼠标事件处理器利用光标管理器修改当前光标。

    7. 鼠标事件处理器利用选框管理器,显示选择框。

    8. 选框管理器调用UI管理器中注册的元素,把选择框显示出来。

    2.2 图元数据定义

    图元数据描述是本项目开发的一个难点。图元数据的特点包括:

    (1)图元种类众多,每种图元有自己独特的属性。

    (2)某些种类的图元存在公共的属性。例如矩形、图片的位置都是通过一个矩形来描述的。

    (3)管理器对图元有共同的操作。例如把图片数据转化为可视HTML,鼠标拖动时的平移动作等。这些操作在图元内部的实现并不一致。

    (4)图元存在一些即使而不需要保存的属性,例如是否选中,这是一个不需要保存,每次打开时都有默认值的属性。

    在本项目中,图元具有以下的数据定义:

    class Component {
        public id: string = "";
        public type: ComponentType = "rect";
        public props: { [key: string]: string } = {};
        public connectAnchors: Array = [];
    }

    这就是图元的基类。可以看到,属性是相当少的。在这里,需要说明几种跟TypeScript语法相关的设计特性:

    (1)图元基类只有属性,没有方法。这是因为图元需要进行复制操作,复制时方法是无法实现的。

    (2)很多特有的属性被设计到props属性中,这是一个Object。实际上Map也能实现相同的功能,但因为Map同样不能被复制,所以使用Object。props中只存放字符串类型的属性,其他属性(例如位置)如果使用字符串,还需要转换,浪费资源。

    对于存在公共属性的图元,定义了继承Component的新的基类。例如,用矩形去描述位置的图元,都继承于以下的基类:

    class RectPositionComponent extends Component {
        public position: Rect = new Rect(0, 0, 0, 0);
        public originPosition: Rect = new Rect(0, 0, 0, 0);
    ​
        public mindChildren1: Array = [];
        public mindChildren2: Array = [];
    ​
        public constructor() {
            super();
    ​
            this.props[PreDefine.MindType] = PreDefine.DefaultMindType;
        }
    }

    在这里,我们可以看到,这个类有一个position属性,它是一个矩形。还有一个originPosition属性,是用在变换图元时标记原始位置的属性。

    这个基类还描述了思维导图的数据结构。本项目的思维导图,只能在矩形类型的图元下创建。思维导图存在树在两侧的展示方式,所以有两棵树(mindChildren1和mindChildren2)去描述思维导图的结点。

    以下是矩形类的定义:

    class RectComponent extends RectPositionComponent {
        public constructor() {
            super();
    ​
            this.type = "rect";
            this.id = CommonFunc.GetId();
    ​
            this.props[PreDefine.Fill] = PreDefine.DefaultFill;
            this.props[PreDefine.Stroke] = PreDefine.DefaultStroke;
            this.props[PreDefine.StrokeWidth] = PreDefine.DefaultStrokeWidth;
            this.props[PreDefine.StrokeDash] = PreDefine.DefaultStrokeDash;
            this.props[PreDefine.CornerRadius] = PreDefine.DefaultCornerRadius;
    ​
            this.props[PreDefine.TextHorAlign] = PreDefine.DefaultTextHorAlign;
            this.props[PreDefine.TextVerAlign] = PreDefine.DefaultTextVerAlign;
            this.props[PreDefine.FontFamily] = PreDefine.DefaultFontFamily;
            this.props[PreDefine.FontSize] = PreDefine.DefaultFontSize;
            this.props[PreDefine.FontColor] = PreDefine.DefaultFontColor;
            this.props[PreDefine.FontBold] = PreDefine.DefaultFontBold;
            this.props[PreDefine.FontUnderline] = PreDefine.DefaultFontUnderline;
        }
    ​
        public textPos: AnchorPos = new AnchorPos();
        public textExist: boolean = false;
        public textHtml: string = "";
    }

    矩形的样式,需要通过填充颜色、描边宽度等属性去定义。另外,矩形中还有文字,文字又有字体大小、字体颜色等属性。这些属性都在图元初始化时添加到props属性中。而文字的位置等属性,由于其存在一定的数据结构,如果定义在props中,还需要转化,浪费资源,所以独立定义。事实上,这种区分定义还有一个重要原因,就是在菜单中能修改的属性都在props中。

    对图元的操作由一个接口去定义。以下是该接口的部分方法:

    interface IComponentOperate {
        ChangeId(): void;
        Create(obj: unknown): void;
    ​
        Html(): string;
        UpdateHtml(): void;
        DeleteHtml(): void;
    ​
        Move(diagramX: number, diagramY: number): void;
        MoveOffset(offsetX: number, offsetY: number): void;
        UpdateInnerPosition(): void;
        MarkOrigin(): void;
        ChangeCoordinate(origin: Rect, target: Rect): void;
    }

    Html()方法是返回图元的HTML内容。例如矩形使用一个标签定义,形状一般使用标签定义,而图片使用标签定义。又如,UpdateInnerPosition()方法一般是用在组类型的图元上的。一个组进行变换之后,其内部的所有图元都要跟随变换。但矩形这种图形,内容并没有子,也就不需要执行额外的操作。

    操作方法跟图元,通过图元的ID和类型进行绑定。图元的类型确定了接口实例化的方法类,而方法类要处理哪个图元,又通过ID去确定。

  • 相关阅读:
    (超详解)堆排序+(图解)
    总结1008
    不一样的“中国速度”,数据可视化交通运输大屏,带你见证中国高铁
    Nginx学习笔记11——防盗链与http的referer
    randint和randrange的区别
    Spring学习篇(一)
    Springioc的配置使用
    labelme-5.0.1版本编辑多边形闪退
    47、Flink 的 Data Source 原理
    BeanFactory版本的快速入门
  • 原文地址:https://blog.csdn.net/lweiyue/article/details/126955101