haojianlong преди 4 години
родител
ревизия
dd4fef8d22
променени са 38 файла, в които са добавени 1946 реда и са изтрити 90 реда
  1. 1 1
      saga-web-big/package.json
  2. 3 3
      saga-web-big/src/factories/SItemFactory.ts
  3. 3 3
      saga-web-big/src/items/SIconTextItem.ts
  4. 2 20
      saga-web-big/src/items/SImageItem.ts
  5. 5 5
      saga-web-big/src/items/SLineItem.ts
  6. 46 2
      saga-web-big/src/items/SObjectItem.ts
  7. 6 5
      saga-web-big/src/items/SPolylineItem.ts
  8. 0 5
      saga-web-big/src/items/STextItem.ts
  9. 3 3
      saga-web-big/src/items/floor/SColumnItem.ts
  10. 2 2
      saga-web-big/src/items/floor/SCompassItem.ts
  11. 5 5
      saga-web-big/src/items/floor/SDoorItem.ts
  12. 2 2
      saga-web-big/src/items/floor/SFloorItem.ts
  13. 6 5
      saga-web-big/src/items/floor/SHighlightItem.ts
  14. 5 5
      saga-web-big/src/items/floor/SSpaceItem.ts
  15. 5 5
      saga-web-big/src/items/floor/SVirtualWallItem.ts
  16. 5 5
      saga-web-big/src/items/floor/SWallItem.ts
  17. 6 5
      saga-web-big/src/items/floor/SWindowItem.ts
  18. 3 3
      saga-web-big/src/items/floor/ZoneItem.ts
  19. 2 2
      saga-web-big/src/items/topology/SAnchorItem.ts
  20. 0 1
      saga-web-big/src/items/topology/SCurveRelation.ts
  21. 2 2
      saga-web-big/src/items/topology/SRelation.ts
  22. 0 1
      saga-web-big/src/items/topology/SVerticalRelation.ts
  23. 9 0
      saga-web-graph/.editorconfig
  24. 32 0
      saga-web-graph/.eslintrc.js
  25. 13 0
      saga-web-graph/.npmignore
  26. 1 0
      saga-web-graph/README.md
  27. 12 0
      saga-web-graph/jest.config.js
  28. 46 0
      saga-web-graph/package.json
  29. 55 0
      saga-web-graph/publish.js
  30. 592 0
      saga-web-graph/src/SGraphItem.ts
  31. 281 0
      saga-web-graph/src/SGraphScene.ts
  32. 412 0
      saga-web-graph/src/SGraphView.ts
  33. 45 0
      saga-web-graph/src/commands/SGraphCommand.ts
  34. 34 0
      saga-web-graph/src/index.ts
  35. 208 0
      saga-web-graph/src/items/SGraphClockItem.ts
  36. 72 0
      saga-web-graph/src/items/SGraphLineItem.ts
  37. 16 0
      saga-web-graph/tsconfig.json
  38. 6 0
      saga-web-graph/typedoc.json

+ 1 - 1
saga-web-big/package.json

@@ -42,6 +42,6 @@
         "typescript": "^3.9.3"
     },
     "dependencies": {
-        "@saga-web/graphy": "2.1.54"
+        "@saga-web/graph": "2.1.59"
     }
 }

+ 3 - 3
saga-web-big/src/factories/SItemFactory.ts

@@ -112,7 +112,7 @@ export class SItemFactory {
      *
      * @param   data    文本数据
      * */
-    createText(data: TextData): STextItem {
-        return new STextItem(null);
-    } // Function createImage()
+    // createText(data: TextData): STextItem {
+    //     return new STextItem(null);
+    // } // Function createImage()
 } // class SItemFactory

+ 3 - 3
saga-web-big/src/items/SIconTextItem.ts

@@ -1,18 +1,18 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
 import { SMouseEvent } from "@saga-web/base/lib";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  *  图标+文字item
  *
  * * @author  郝建龙(1061851420@qq.com)
  */
-export class SIconTextItem extends SGraphyItem {
+export class SIconTextItem extends SGraphItem {
     /**
      * 构造函数
      *
      *  @param parent    指向父对象
      * */
-    constructor(parent: SGraphyItem | null) {
+    constructor(parent: SGraphItem | null) {
         super(parent);
     }
 

+ 2 - 20
saga-web-big/src/items/SImageItem.ts

@@ -1,6 +1,6 @@
 import { SObjectItem } from "./SObjectItem";
-import { SGraphyItem } from "@saga-web/graphy/lib";
 import { SPainter, SRect } from "@saga-web/draw/lib";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  *  图片绘制
@@ -8,24 +8,6 @@ import { SPainter, SRect } from "@saga-web/draw/lib";
  * * @author  郝建龙(1061851420@qq.com)
  */
 export class SImageItem extends SObjectItem {
-    /** 标志宽度 */
-    _width: number = 20;
-    get width(): number {
-        return this._width;
-    }
-    set width(v: number) {
-        this._width = v;
-        this.update();
-    }
-    /** 标志高度 */
-    _height: number = 20;
-    get height(): number {
-        return this._height;
-    }
-    set height(v: number) {
-        this._height = v;
-        this.update();
-    }
     /** 图片地址    */
     _url: string = "";
     get url(): string {
@@ -51,7 +33,7 @@ export class SImageItem extends SObjectItem {
      *
      *  @param parent    指向父对象
      * */
-    constructor(parent: SGraphyItem | null) {
+    constructor(parent: SGraphItem | null) {
         super(parent);
         this.isTransform = false;
     }

+ 5 - 5
saga-web-big/src/items/SLineItem.ts

@@ -1,15 +1,15 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
 import { SColor, SLine, SPainter, SPoint } from "@saga-web/draw/lib";
 import { SMouseEvent } from "@saga-web/base";
 import { SMathUtil } from "../utils/SMathUtil";
 import { SRelationState } from "..";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 直线item
  *
  * */
 
-export class SLineItem extends SGraphyItem {
+export class SLineItem extends SGraphItem {
     /** 线段起始点   */
     p1: SPoint | undefined;
     /** 线段终止点   */
@@ -61,7 +61,7 @@ export class SLineItem extends SGraphyItem {
      * @param   parent  父级
      * @param   line    线段
      * */
-    constructor(parent: SGraphyItem | null, line: SLine);
+    constructor(parent: SGraphItem | null, line: SLine);
 
     /**
      * 构造函数
@@ -69,7 +69,7 @@ export class SLineItem extends SGraphyItem {
      * @param   parent  父级
      * @param   point   线段起点
      * */
-    constructor(parent: SGraphyItem | null, point: SPoint);
+    constructor(parent: SGraphItem | null, point: SPoint);
 
     /**
      * 构造函数
@@ -77,7 +77,7 @@ export class SLineItem extends SGraphyItem {
      * @param   parent  父级
      * @param   l       线段or线段起点
      * */
-    constructor(parent: SGraphyItem | null, l: SPoint | SLine) {
+    constructor(parent: SGraphItem | null, l: SPoint | SLine) {
         super(parent);
         if (l instanceof SPoint) {
             this.p1 = l;

+ 46 - 2
saga-web-big/src/items/SObjectItem.ts

@@ -1,12 +1,56 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
 import { SAnchorItem } from "./topology/SAnchorItem";
+import { SSize } from "@saga-web/draw/lib";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 对象item
  *
  * @author  郝建龙(1061851420@qq.com)
  */
-export abstract class SObjectItem extends SGraphyItem {
+export abstract class SObjectItem extends SGraphItem {
     /** 锚点list  */
     anchorList: SAnchorItem[] = [];
+    /** 宽度  */
+    private _width: number = 64;
+    get width(): number {
+        return this._width;
+    }
+    set width(v: number) {
+        if (v > 0) {
+            if (v != this._width) {
+                let w = this._width;
+                this._width = v;
+                this.onResize(
+                    new SSize(w, this._height),
+                    new SSize(this._width, this._height)
+                );
+            }
+        }
+    }
+
+    /** 高度  */
+    private _height: number = 64;
+    get height(): number {
+        return this._height;
+    }
+    set height(v: number) {
+        if (v > 0) {
+            if (v != this._height) {
+                let h = this._height;
+                this._height = v;
+                this.onResize(
+                    new SSize(this._width, h),
+                    new SSize(this._width, this._height)
+                );
+            }
+        }
+    }
+
+    /**
+     * 宽高发发生变化
+     *
+     * @param   oldSize 改之前的大小
+     * @param   newSize 改之后大小
+     * */
+    protected onResize(oldSize: SSize, newSize: SSize) {} // Function onResize()
 } // Class SObjectItem

+ 6 - 5
saga-web-big/src/items/SPolylineItem.ts

@@ -1,15 +1,16 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
+
 import { SColor, SLine, SPainter, SPoint } from "@saga-web/draw/lib";
 import { SMouseEvent } from "@saga-web/base";
 import { SRelationState } from "..";
 import { SMathUtil } from "../utils/SMathUtil";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 直线item
  *
  * */
 
-export class SPolylineItem extends SGraphyItem {
+export class SPolylineItem extends SGraphItem {
     /** 折点信息    */
     pointList: SPoint[] = [];
     /** 是否绘制完成  */
@@ -63,7 +64,7 @@ export class SPolylineItem extends SGraphyItem {
      * @param   parent  父级
      * @param   list    坐标集合
      * */
-    constructor(parent: null | SGraphyItem, list: SPoint[]);
+    constructor(parent: null | SGraphItem, list: SPoint[]);
 
     /**
      * 构造函数
@@ -71,7 +72,7 @@ export class SPolylineItem extends SGraphyItem {
      * @param   parent  父级
      * @param   list    第一个坐标
      * */
-    constructor(parent: null | SGraphyItem, list: SPoint);
+    constructor(parent: null | SGraphItem, list: SPoint);
 
     /**
      * 构造函数
@@ -79,7 +80,7 @@ export class SPolylineItem extends SGraphyItem {
      * @param   parent  父级
      * @param   list    第一个坐标|坐标集合
      * */
-    constructor(parent: null | SGraphyItem, list: SPoint | SPoint[]) {
+    constructor(parent: null | SGraphItem, list: SPoint | SPoint[]) {
         super(parent);
         if (list instanceof SPoint) {
             this.pointList.push(list);

+ 0 - 5
saga-web-big/src/items/STextItem.ts

@@ -18,11 +18,6 @@ export class STextItem extends SObjectItem {
         this.update();
     }
 
-    /** 文本所占高度  */
-    private height: number = 0;
-    /** 文本所占宽度  */
-    private width: number = 0;
-
     /** 字体  */
     _font: SFont = new SFont();
     get font(): SFont {

+ 3 - 3
saga-web-big/src/items/floor/SColumnItem.ts

@@ -1,5 +1,5 @@
 import { Column } from "../../types/floor/Column";
-import { SGraphyItem } from "@saga-web/graphy/lib";
+import { SGraphItem } from "@saga-web/graph/lib";
 import { SPainter, SPoint, SRect } from "@saga-web/draw/lib";
 import { ItemOrder } from "../..";
 import { ItemColor } from "../..";
@@ -9,7 +9,7 @@ import { ItemColor } from "../..";
  *
  * @author  郝建龙
  */
-export class SColumnItem extends SGraphyItem {
+export class SColumnItem extends SGraphItem {
     /** 柱子数据    */
     data: Column;
     /** X坐标最小值  */
@@ -29,7 +29,7 @@ export class SColumnItem extends SGraphyItem {
      * @param parent    指向父对象
      * @param data      柱子数据
      */
-    constructor(parent: SGraphyItem | null, data: Column) {
+    constructor(parent: SGraphItem | null, data: Column) {
         super(parent);
         this.data = data;
         this.zOrder = ItemOrder.columnOrder;

+ 2 - 2
saga-web-big/src/items/floor/SCompassItem.ts

@@ -1,8 +1,8 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 指南针item
  *
  * * @author  郝建龙(1061851420@qq.com)
  */
-export class SCompassItem extends SGraphyItem {} // Class SCompassItem
+export class SCompassItem extends SGraphItem {} // Class SCompassItem

+ 5 - 5
saga-web-big/src/items/floor/SDoorItem.ts

@@ -19,18 +19,18 @@
  */
 
 import { Door } from "../../types/floor/Door";
-import { SGraphyItem } from "@saga-web/graphy/lib";
 import { SPainter, SPoint, SRect } from "@saga-web/draw/lib";
 import { SMathUtil } from "../../utils/SMathUtil";
-import { ItemOrder } from "../../config/ItemOrder";
-import { ItemColor } from "../../config/ItemColor";
+import { ItemOrder } from "../..";
+import { ItemColor } from "../..";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 门item
  *
  * @author  郝建龙
  */
-export class SDoorItem extends SGraphyItem {
+export class SDoorItem extends SGraphItem {
     /** 门数据 */
     data: Door;
     /** 门轮廓线坐标list  */
@@ -58,7 +58,7 @@ export class SDoorItem extends SGraphyItem {
      * @param parent    指向父对象
      * @param data      门数据
      */
-    constructor(parent: SGraphyItem | null, data: Door) {
+    constructor(parent: SGraphItem | null, data: Door) {
         super(parent);
         this.data = data;
         this.zOrder = ItemOrder.doorOrder;

+ 2 - 2
saga-web-big/src/items/floor/SFloorItem.ts

@@ -1,8 +1,8 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 一个场景中显示多个楼层
  *
  * * @author  郝建龙(1061851420@qq.com)
  */
-export class SFloorItem extends SGraphyItem {} // Class SFloorItem
+export class SFloorItem extends SGraphItem {} // Class SFloorItem

+ 6 - 5
saga-web-big/src/items/floor/SHighlightItem.ts

@@ -1,14 +1,15 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
+
 import { SColor, SLine, SPainter, SPoint, SRect } from "@saga-web/draw/lib";
-import { ItemOrder } from "../../config/ItemOrder";
-import { ItemColor } from "../../config/ItemColor";
+import { ItemOrder } from "../..";
+import { ItemColor } from "../..";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 吸附时高亮对象
  *
  * @author  郝建龙
  */
-export class SHighlightItem extends SGraphyItem {
+export class SHighlightItem extends SGraphItem {
     /** 对象与鼠标位置距离   */
     distance: number = 0;
     /** 对象类型   */
@@ -41,7 +42,7 @@ export class SHighlightItem extends SGraphyItem {
      *
      * @param   parent  指向父对象
      */
-    constructor(parent: SGraphyItem | null) {
+    constructor(parent: SGraphItem | null) {
         super(parent);
         this.visible = false;
         this.zOrder = ItemOrder.highLightOrder;

+ 5 - 5
saga-web-big/src/items/floor/SSpaceItem.ts

@@ -18,7 +18,6 @@
  * ********************************************************************************************************************
  */
 
-import { SGraphyItem } from "@saga-web/graphy/lib";
 import {
     SColor,
     SPainter,
@@ -30,15 +29,16 @@ import {
 } from "@saga-web/draw/lib";
 import { Space } from "../../types/floor/Space";
 import { SMouseEvent } from "@saga-web/base/lib";
-import { ItemOrder } from "../../config/ItemOrder";
-import { ItemColor } from "../../config/ItemColor";
+import { ItemOrder } from "../..";
+import { ItemColor } from "../..";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 模型空间item
  *
  * @author  郝建龙
  */
-export class SSpaceItem extends SGraphyItem {
+export class SSpaceItem extends SGraphItem {
     /** 空间所有数据  */
     data: Space;
     /** 空间轮廓线坐标list  */
@@ -69,7 +69,7 @@ export class SSpaceItem extends SGraphyItem {
      * @param parent    指向父对象
      * @param data      空间数据
      */
-    constructor(parent: SGraphyItem | null, data: Space) {
+    constructor(parent: SGraphItem | null, data: Space) {
         super(parent);
         this.data = data;
         this.zOrder = ItemOrder.spaceOrder;

+ 5 - 5
saga-web-big/src/items/floor/SVirtualWallItem.ts

@@ -18,18 +18,18 @@
  * ********************************************************************************************************************
  */
 
-import { SGraphyItem } from "@saga-web/graphy/lib";
 import { SPainter, SPoint, SRect } from "@saga-web/draw/lib";
 import { VirtualWall } from "../../types/floor/VirtualWall";
-import { ItemOrder } from "../../config/ItemOrder";
-import { ItemColor } from "../../config/ItemColor";
+import { ItemOrder } from "../..";
+import { ItemColor } from "../..";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 墙item
  *
  * @author  郝建龙
  */
-export class SVirtualWallItem extends SGraphyItem {
+export class SVirtualWallItem extends SGraphItem {
     /** 虚拟墙数据   */
     data: VirtualWall;
     /** X坐标最小值  */
@@ -49,7 +49,7 @@ export class SVirtualWallItem extends SGraphyItem {
      * @param parent    指向父对象
      * @param data      虚拟墙数据
      */
-    constructor(parent: SGraphyItem | null, data: VirtualWall) {
+    constructor(parent: SGraphItem | null, data: VirtualWall) {
         super(parent);
         this.data = data;
         this.zOrder = ItemOrder.virtualWallOrder;

+ 5 - 5
saga-web-big/src/items/floor/SWallItem.ts

@@ -18,18 +18,18 @@
  * ********************************************************************************************************************
  */
 
-import { SGraphyItem } from "@saga-web/graphy/lib";
 import { SPainter, SPoint, SRect } from "@saga-web/draw/lib";
 import { Wall } from "../../types/floor/Wall";
-import { ItemOrder } from "../../config/ItemOrder";
-import { ItemColor } from "../../config/ItemColor";
+import { ItemOrder } from "../..";
+import { ItemColor } from "../..";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 墙item
  *
  * @author  郝建龙
  */
-export class SWallItem extends SGraphyItem {
+export class SWallItem extends SGraphItem {
     /** 墙数据 */
     data: Wall;
     /** X坐标最小值  */
@@ -49,7 +49,7 @@ export class SWallItem extends SGraphyItem {
      * @param parent    指向父对象
      * @param data      墙数据
      */
-    constructor(parent: SGraphyItem | null, data: Wall) {
+    constructor(parent: SGraphItem | null, data: Wall) {
         super(parent);
         this.data = data;
         this.zOrder = ItemOrder.wallOrder;

+ 6 - 5
saga-web-big/src/items/floor/SWindowItem.ts

@@ -1,15 +1,16 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
+
 import { SPainter, SPoint, SRect } from "@saga-web/draw/lib";
 import { Casement } from "../../types/floor/Casement";
-import { ItemOrder } from "../../config/ItemOrder";
-import { ItemColor } from "../../config/ItemColor";
+import { ItemOrder } from "../..";
+import { ItemColor } from "../..";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 窗户item
  *
  * @author  郝建龙
  */
-export class SWindowItem extends SGraphyItem {
+export class SWindowItem extends SGraphItem {
     /** 窗户数据    */
     data: Casement;
     /** 窗户轮廓线坐标list  */
@@ -29,7 +30,7 @@ export class SWindowItem extends SGraphyItem {
      * @param parent    指向父对象
      * @param data      窗户数据
      */
-    constructor(parent: SGraphyItem | null, data: Casement) {
+    constructor(parent: SGraphItem | null, data: Casement) {
         super(parent);
         this.data = data;
         this.zOrder = ItemOrder.windowOrder;

+ 3 - 3
saga-web-big/src/items/floor/ZoneItem.ts

@@ -18,7 +18,6 @@
  * ********************************************************************************************************************
  */
 
-import { SGraphyItem } from "@saga-web/graphy/lib";
 import {
     SColor,
     SPainter,
@@ -30,13 +29,14 @@ import {
 import { SMouseEvent } from "@saga-web/base/lib";
 import { Zone } from "../../types/floor/Zone";
 import { ItemColor, ItemOrder, Transparency } from "../..";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 业务空间item
  *
  * @author  郝建龙
  */
-export class SZoneItem extends SGraphyItem {
+export class SZoneItem extends SGraphItem {
     /** 空间所有数据  */
     data: Zone;
     /** 空间轮廓线坐标list  */
@@ -95,7 +95,7 @@ export class SZoneItem extends SGraphyItem {
      * @param parent    指向父对象
      * @param data      空间数据
      */
-    constructor(parent: SGraphyItem | null, data: Zone) {
+    constructor(parent: SGraphItem | null, data: Zone) {
         super(parent);
         this.data = data;
         this.zOrder = ItemOrder.zoneOrder;

+ 2 - 2
saga-web-big/src/items/topology/SAnchorItem.ts

@@ -1,8 +1,8 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
+import { SGraphItem } from "@saga-web/graph/lib";
 
 /**
  * 锚点item
  *
  * @author  郝建龙(1061851420@qq.com)
  */
-export class SAnchorItem extends SGraphyItem {} // Class SAnchorItem
+export class SAnchorItem extends SGraphItem {} // Class SAnchorItem

+ 0 - 1
saga-web-big/src/items/topology/SCurveRelation.ts

@@ -6,7 +6,6 @@ import { SRelation } from "./SRelation";
  * * @author  郝建龙(1061851420@qq.com)
  */
 export class SCurveRelation extends SRelation {
-
     /**
      * 曲线转直线
      *

+ 2 - 2
saga-web-big/src/items/topology/SRelation.ts

@@ -1,4 +1,4 @@
-import { SGraphyItem } from "@saga-web/graphy/lib";
+import { SGraphItem } from "@saga-web/graph/lib";
 import { SColor, SPoint, SArrowStyleType } from "@saga-web/draw/lib";
 import { SRelationState } from "../../enums/SRelationState";
 import { SAnchorItem } from "./SAnchorItem";
@@ -8,7 +8,7 @@ import { SAnchorItem } from "./SAnchorItem";
  *
  * * @author  郝建龙(1061851420@qq.com)
  */
-export abstract class SRelation extends SGraphyItem {
+export abstract class SRelation extends SGraphItem {
     // /** 起始端点线帽样式(枚举)    */
     // beginCap: SRelationState = SArrowStyleType.None;
     // /** 终止端点线帽样式(枚举)    */

+ 0 - 1
saga-web-big/src/items/topology/SVerticalRelation.ts

@@ -6,7 +6,6 @@ import { SRelation } from "./SRelation";
  * * @author  郝建龙(1061851420@qq.com)
  */
 export class SVerticalRelation extends SRelation {
-
     /**
      * 垂直线转曲线
      *

+ 9 - 0
saga-web-graph/.editorconfig

@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+end_of_line =lf
+insert_final_newline = true
+trim_trailing_whitespace = true

+ 32 - 0
saga-web-graph/.eslintrc.js

@@ -0,0 +1,32 @@
+module.exports = {
+    root: true,
+    parser: '@typescript-eslint/parser',
+    extends: [
+        'plugin:@typescript-eslint/recommended',
+        // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
+        'prettier/@typescript-eslint',
+        // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
+        // 此行必须在最后
+        'plugin:prettier/recommended'
+    ],
+    env: {
+        es6: true,
+        node: true
+    },
+    parserOptions: {
+        // 支持最新 JavaScript
+        ecmaVersion: 2018,
+        sourceType: 'module'
+    },
+    rules: {
+        // 缩进
+        'indent': ['error', 4],                         // 缩进控制4空格
+        'max-len': ['error', 120],                      // 每行字符不超过120
+        'no-mixed-spaces-and-tabs': 'error',            // 禁止使用空格和tab混合缩进
+        // 语句
+        'curly': ["error", "multi-line"],               // if、else if、else、for、while强制使用大括号,但允许在单行中省略大括号。
+        'semi': ['error', 'always'],                    //不得省略语句结束的分号
+        '@typescript-eslint/no-unused-vars': 'off',     // 取消未使用变量检查
+        '@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }]       // public访问不需加访问修饰符
+    }
+};

+ 13 - 0
saga-web-graph/.npmignore

@@ -0,0 +1,13 @@
+# 发布时排除
+__tests__/
+api/
+docs/
+src/
+.editorconfig
+.eslintrc.js
+*.iml
+coverage
+jest.config.js
+tsconfig.json
+typedoc.json
+publish.js

+ 1 - 0
saga-web-graph/README.md

@@ -0,0 +1 @@
+## 依赖包版本号

+ 12 - 0
saga-web-graph/jest.config.js

@@ -0,0 +1,12 @@
+module.exports = {
+    preset: "ts-jest",
+    moduleFileExtensions: ["js", "ts"],
+    transform: {
+        "^.+\\.tsx?$": "ts-jest"
+    },
+    transformIgnorePatterns: ["/node_modules/"],
+    moduleNameMapper: {
+        "^@/(.*)$": "<rootDir>/src/$1"
+    },
+    collectCoverage: true
+};

+ 46 - 0
saga-web-graph/package.json

@@ -0,0 +1,46 @@
+{
+    "name": "@saga-web/graph",
+    "version": "2.1.59",
+    "description": "上格云二维图形引擎。",
+    "main": "lib/index.js",
+    "types": "lib/index.d.js",
+    "remote": {
+        "host": "47.94.89.44",
+        "path": "/opt/tomcat9/webapps/api/web/graphy",
+        "user": "user1",
+        "password": "@)!^sagacloud",
+        "local": "api"
+    },
+    "scripts": {
+        "build": "tsc",
+        "publish": "npm publish",
+        "lint": "eslint src/**/*.{js,ts,tsx}",
+        "test": "echo \"Error: no test specified\" && exit 1",
+        "typedoc": "typedoc --out api --hideGenerator src ../saga-web-base/src ../saga-web-draw/src",
+        "publish-api": "node publish.js"
+    },
+    "keywords": [
+        "graph"
+    ],
+    "author": "庞利祥 (sybotan@126.com)",
+    "license": "ISC",
+    "publishConfig": {
+        "registry": "http://192.168.200.80:8081/repository/npm-hosted/"
+    },
+    "devDependencies": {
+        "@typescript-eslint/eslint-plugin": "^1.12.0",
+        "@typescript-eslint/parser": "^1.12.0",
+        "eslint": "^6.0.1",
+        "eslint-config-prettier": "^6.0.0",
+        "eslint-plugin-prettier": "^3.1.0",
+        "node-ssh": "^6.0.0",
+        "prettier": "^1.18.2",
+        "@types/jest": "^24.0.15",
+        "ts-jest": "^24.0.2",
+        "typedoc": "^0.17.4",
+        "typescript": "^3.5.3"
+    },
+    "dependencies": {
+        "@saga-web/draw": "2.1.83"
+    }
+}

+ 55 - 0
saga-web-graph/publish.js

@@ -0,0 +1,55 @@
+/*
+ * ********************************************************************************************************************
+ *
+ *               iFHS7.
+ *              ;BBMBMBMc                  rZMBMBR              BMB
+ *              MBEr:;PBM,               7MBMMEOBB:             BBB                       RBW
+ *     XK:      BO     SB.     :SZ       MBM.       c;;     ir  BBM :FFr       :SSF:    ;xBMB:r   iuGXv.    i:. iF2;
+ *     DBBM0r.  :D     S7   ;XMBMB       GMBMu.     MBM:   BMB  MBMBBBMBMS   WMBMBMBBK  MBMBMBM  BMBRBMBW  .MBMBMBMBB
+ *      :JMRMMD  ..    ,  1MMRM1;         ;MBMBBR:   MBM  ;MB:  BMB:   MBM. RMBr   sBMH   BM0         UMB,  BMB.  KMBv
+ *     ;.   XOW  B1; :uM: 1RE,   i           .2BMBs  rMB. MBO   MBO    JMB; MBB     MBM   BBS    7MBMBOBM:  MBW   :BMc
+ *     OBRJ.SEE  MRDOWOR, 3DE:7OBM       .     ;BMB   RMR7BM    BMB    MBB. BMB    ,BMR  .BBZ   MMB   rMB,  BMM   rMB7
+ *     :FBRO0D0  RKXSXPR. JOKOOMPi       BMBSSWBMB;    BMBB:    MBMB0ZMBMS  .BMBOXRBMB    MBMDE RBM2;SMBM;  MBB   xBM2
+ *         iZGE  O0SHSPO. uGZ7.          sBMBMBDL      :BMO     OZu:BMBK,     rRBMB0;     ,EBMB  xBMBr:ER.  RDU   :OO;
+ *     ,BZ, 1D0  RPSFHXR. xWZ .SMr                  . .BBB
+ *      :0BMRDG  RESSSKR. 2WOMBW;                   BMBMR
+ *         i0BM: SWKHKGO  MBDv
+ *           .UB  OOGDM. MK,                                          Copyright (c) 2015-2019.  斯伯坦机器人
+ *              ,  XMW  ..
+ *                  r                                                                     All rights reserved.
+ *
+ * ********************************************************************************************************************
+ */
+
+const Client = require("node-ssh");
+const ssh = new Client();
+
+ssh.connect({
+    host: process.env.npm_package_remote_host,
+    port: "22",
+    username: process.env.npm_package_remote_user,
+    password: process.env.npm_package_remote_password
+}).then(() => {
+    const failedList = [];
+    ssh.putDirectory(
+        process.env.npm_package_remote_local,
+        process.env.npm_package_remote_path,
+        {
+            recursive: true,
+            concurrency: 1,
+            tick: function(localPath, remotePath, error) {
+                if (error) {
+                    failedList.push(localPath);
+                }
+            }
+        }
+    ).then(status => {
+        if (failedList.length > 0) {
+            console.log("发布失败");
+            console.log("failed transfers", failedList.join(", "));
+        } else {
+            console.log(status ? "发布成功" : "发布失败");
+        }
+        ssh.dispose();
+    });
+});

+ 592 - 0
saga-web-graph/src/SGraphItem.ts

@@ -0,0 +1,592 @@
+import { SMouseButton, SMouseEvent, SObject } from "@saga-web/base/lib";
+import { SPainter, SPoint, SRect } from "@saga-web/draw/lib";
+import { SGraphScene } from "./SGraphScene";
+
+/**
+ * Graph图形引擎Item类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphItem extends SObject {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性
+    /** 场景对象 */
+    private _scene: SGraphScene | null = null;
+    get scene(): SGraphScene | null {
+        if (null != this._parent) {
+            return this._parent.scene;
+        } else {
+            return this._scene;
+        }
+    } // Get scene
+    set scene(v: SGraphScene | null) {
+        this._scene = v;
+        this.update();
+    } // Set scene
+    /** parent属性存值函数 */
+    private _parent: SGraphItem | null = null;
+    get parent(): SGraphItem | null {
+        return this._parent;
+    } // Get parent
+    set parent(v: SGraphItem | null) {
+        // 如果parent未变更
+        if (this.parent == v) {
+            return;
+        }
+        // 如果原parent不为空
+        if (this._parent != null) {
+            // 将节点从原parent节点中摘除
+            let i = this._parent.children.indexOf(this);
+            this._parent.children.splice(i, 1);
+        }
+        this._parent = v;
+        this.update();
+        // 如果新parent不为空
+        if (this._parent != null) {
+            // 将节点加入到新parent节点中
+            this._parent.children.push(this);
+            this._parent.children.sort(SGraphItem.sortItemZOrder);
+        }
+    } // Set parent()
+
+    /** 子节点 */
+    children: SGraphItem[] = [];
+
+    /** Z轴顺序 */
+    private _zOrder: number = 0;
+    get zOrder(): number {
+        return this._zOrder;
+    } // Get zOrder
+    set zOrder(v: number) {
+        this._zOrder = v;
+        if (this._parent != null) {
+            this._parent.children.sort(SGraphItem.sortItemZOrder);
+        }
+        this.update();
+    } // Set zOrder
+
+    /** 位置 */
+    pos: SPoint = new SPoint(0, 0);
+    /** X轴坐标 */
+    get x(): number {
+        return this.pos.x;
+    } // Get x
+    set x(v: number) {
+        if (this.pos.x == v) {
+            return;
+        }
+        let old = this.pos.x;
+        this.pos.x = v;
+        this.update();
+    } // Set x
+    /** Y轴坐标 */
+    get y(): number {
+        return this.pos.y;
+    } // Get y
+    set y(v: number) {
+        if (this.pos.y == v) {
+            return;
+        }
+        let old = this.pos.y;
+        this.pos.y = v;
+        this.update();
+    } // Set y
+    /** 缩放比例 */
+    scale: number = 1;
+
+    /** 是否可见 */
+    _visible: boolean = true;
+    get visible(): boolean {
+        return this._visible;
+    } // Get visible
+    set visible(v: boolean) {
+        this._visible = v;
+        this.update();
+    } // Set visible
+
+    /** 是否可以移动 */
+    moveable: boolean = false;
+    /** 是否正在移动 */
+    private _isMoving = false;
+
+    /** 是否可用 */
+    enabled: boolean = true;
+
+    /** 是否可被选中 */
+    selectable = false;
+    /** 是否被选中 */
+    private _selected = false;
+    get selected(): boolean {
+        return this._selected && this.selectable && this.enabled;
+    } // Get selected
+    set selected(value: boolean) {
+        // 如果选择状态未变更
+        if (this.selected == value) {
+            return;
+        }
+        this._selected = value;
+        this.update();
+    } // Set selected
+
+    /** 是否进行变形 */
+    isTransform = true;
+
+    /** 鼠标按下时位置 */
+    private _mouseDownPos = new SPoint();
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 函数
+    /**
+     * 构造函数
+     *
+     * @param   parent      指向父对象
+     */
+    constructor(parent: SGraphItem | null = null) {
+        super();
+        if (parent) {
+            this.parent = parent;
+        }
+    } // Function constructor()
+
+    /**
+     * Item绘制框架
+     *
+     * @param   painter       painter对象
+     * @param   rect          绘制区域
+     */
+    onPaint(painter: SPainter, rect: SRect): void {
+        this.onDraw(painter);
+        for (let item of this.children) {
+            // 如果item不可见
+            if (!item.visible) {
+                continue;
+            }
+            // 保存画布状态
+            painter.save();
+            // item位移到指定位置绘制
+            painter.translate(item.x, item.y);
+
+            // 如果不进行变形处理,则取消painter的变型操作
+            if (!item.isTransform) {
+                let matrix = painter.worldTransform;
+                let x0 = matrix.e;
+                let y0 = matrix.f;
+                painter.resetTransform();
+                painter.translate(x0, y0);
+            }
+            // 设置绘制区域
+            // canvas.clip(item.boundingRect())
+            // 绘制item
+            item.onPaint(painter, rect);
+            // 恢复画布状态
+            painter.restore();
+        }
+    } // Function onPaint()
+
+    /**
+     * Item绘制操作
+     *
+     * @param   painter       painter对象
+     */
+    onDraw(painter: SPainter): void {} // Function onDraw()
+
+    /**
+     * 隐藏对象
+     */
+    hide(): void {
+        this.visible = false;
+    } // Function hide()
+
+    /**
+     * 显示对象
+     */
+    show(): void {
+        this.visible = true;
+    } // Function show()
+
+    /**
+     * 更新Item
+     */
+    update(): void {
+        if (null != this.scene) {
+            const view = this.scene.view;
+            if (null != view) {
+                view.update();
+            }
+        }
+    } // Function update()
+
+    /**
+     * Item对象边界区域
+     *
+     * @return  对象边界区域
+     */
+    boundingRect(): SRect {
+        return new SRect(0, 0, 10, 10);
+    } // Function boundingRect()
+
+    /**
+     * 移动item到指定位置
+     *
+     * @param   x           新位置的x坐标
+     * @param   y           新位置的y坐标
+     */
+    moveTo(x: number, y: number): void {
+        this.x = x;
+        this.y = y;
+    } // moveTo()
+
+    /**
+     * 判断item是否包含点x,y
+     *
+     * @param   x       横坐标(当前item)
+     * @param   y       纵坐标(当前item)
+     *
+     * @return  boolean
+     */
+    contains(x: number, y: number): boolean {
+        return this.boundingRect().contains(x - this.x, y - this.y);
+    } // Function contains()
+
+    /**
+     * 获得item的路径节点列表。(该节点被加载到场景中,如果未被加载到场景中,计算会出错)
+     *
+     * @return  *[]
+     */
+    itemPath(): SGraphItem[] {
+        if (this.parent != null) {
+            let list = this.parent.itemPath();
+            list.push(this);
+            return list;
+        }
+
+        return [this];
+    } // Function itemPath()
+
+    /**
+     * 将场景中的xy坐标转换成item坐标。(该节点被加载到场景中,如果未被加载到场景中,计算会出错)
+     *
+     * @param   x       场景中的横坐标
+     * @param   y       场景中的纵坐标
+     *
+     * @return  在item中的坐标
+     */
+    mapFromScene(x: number, y: number): SPoint {
+        let list = this.itemPath();
+        let x0 = x;
+        let y0 = y;
+        for (let item of list) {
+            x0 = (x0 - item.x) / item.scale;
+            y0 = (y0 - item.y) / item.scale;
+        }
+
+        return new SPoint(x0, y0);
+    } // Function mapFromScene()
+
+    /**
+     * 将item中的xy坐标转换成场景坐标。(该节点被加载到场景中,如果未被加载到场景中,计算会出错)
+     *
+     * @param   x       item中的横坐标
+     * @param   y       item中的纵坐标
+     *
+     * @return  在场景中的坐标
+     */
+    mapToScene(x: number, y: number): SPoint {
+        if (this.parent == null) {
+            return new SPoint(x, y);
+        }
+
+        return this.parent.mapToScene(
+            x * this.scale + this.x,
+            y * this.scale + this.y
+        );
+    } // Function mapToScene()
+
+    // =================================================================================================================
+    // 事件
+    /**
+     * 鼠标单击事件
+     *
+     * @param   event       保存事件参数
+     * @return  boolean
+     */
+    onClick(event: SMouseEvent): boolean {
+        for (let i = this.children.length - 1; i >= 0; i--) {
+            let item = this.children[i];
+            if (!this.acceptEvent() || !item.visible) {
+                continue;
+            }
+            let ce = SGraphItem.toChildMouseEvent(item, event);
+            if (item.contains(event.x, event.y) && item.onClick(ce)) {
+                // 如果点在子项目上且子项目处理了事件
+                return true;
+            }
+        }
+
+        return false;
+    } // Function onClick()
+
+    /**
+     * 鼠标双击事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onDoubleClick(event: SMouseEvent): boolean {
+        for (let i = this.children.length - 1; i >= 0; i--) {
+            let item = this.children[i];
+            if (!this.acceptEvent() || !item.visible) {
+                continue;
+            }
+            let ce = SGraphItem.toChildMouseEvent(item, event);
+            if (item.contains(event.x, event.y) && item.onDoubleClick(ce)) {
+                // 如果点在子项目上且子项目处理了事件
+                return true;
+            }
+        }
+
+        return false;
+    } // Function onDoubleClick()
+
+    /**
+     * 鼠标按下事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseDown(event: SMouseEvent): boolean {
+        for (let i = this.children.length - 1; i >= 0; i--) {
+            let item = this.children[i];
+            if (!this.acceptEvent() || !item.visible) {
+                continue;
+            }
+            let ce = SGraphItem.toChildMouseEvent(item, event);
+            if (item.contains(event.x, event.y) && item.onMouseDown(ce)) {
+                // 如果点在子项目上且子项目处理了事件
+                return true;
+            }
+        }
+
+        // if (this.selectable) {
+        //     this.clickSelect(event);
+        // }
+        if (this.moveable) {
+            this._mouseDownPos = new SPoint(event.x, event.y);
+            this._isMoving = true;
+            this.grabItem(this);
+            return true;
+        }
+        return false;
+    } // Function onMouseDown()
+
+    /**
+     * 鼠标移动事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseMove(event: SMouseEvent): boolean {
+        for (let i = this.children.length - 1; i >= 0; i--) {
+            let item = this.children[i];
+            if (!this.acceptEvent() || !item.visible) {
+                continue;
+            }
+            let ce = SGraphItem.toChildMouseEvent(item, event);
+            if (item.contains(event.x, event.y) && item.onMouseMove(ce)) {
+                // 如果点在子项目上且子项目处理了事件
+                return true;
+            }
+        }
+
+        if (
+            event.buttons & SMouseButton.LeftButton &&
+            this.moveable &&
+            this._isMoving
+        ) {
+            let old = new SPoint(this.pos.x, this.pos.y);
+            this.moveTo(
+                this.pos.x + event.x - this._mouseDownPos.x,
+                this.pos.y + event.y - this._mouseDownPos.y
+            );
+            this.$emit("onMove", old, this.pos);
+        }
+
+        // 处理hover
+        const scene = this.scene;
+        if (null != scene) {
+            if (scene.hoverItem == null || scene.hoverItem !== this) {
+                if (scene.hoverItem != null) {
+                    scene.hoverItem.onMouseLeave(event);
+                }
+                this.onMouseEnter(event);
+                scene.hoverItem = this;
+            }
+        }
+
+        return true;
+    } // Function onMouseMove()
+
+    /**
+     * 释放鼠标事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseUp(event: SMouseEvent): boolean {
+        for (let i = this.children.length - 1; i >= 0; i--) {
+            let item = this.children[i];
+            if (!this.acceptEvent() || !item.visible) {
+                continue;
+            }
+            let ce = SGraphItem.toChildMouseEvent(item, event);
+            if (item.contains(event.x, event.y) && item.onMouseUp(ce)) {
+                // 如果点在子项目上且子项目处理了事件
+                return true;
+            }
+        }
+
+        this._isMoving = false;
+        this.releaseItem();
+        return false;
+    } // Function onMouseUp()
+
+    /**
+     * 鼠标进入事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseEnter(event: SMouseEvent): boolean {
+        return false;
+    } // Function onMouseEnter()
+
+    /**
+     * 鼠标离开事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseLeave(event: SMouseEvent): boolean {
+        return false;
+    } // Function onMouseLeave()
+
+    /**
+     * 上下文菜单事件
+     *
+     * @param   event       事件参数
+     */
+    onContextMenu(event: SMouseEvent): boolean {
+        for (let i = this.children.length - 1; i >= 0; i--) {
+            let item = this.children[i];
+            if (!this.acceptEvent() || !item.visible) {
+                continue;
+            }
+            let ce = SGraphItem.toChildMouseEvent(item, event);
+            if (item.contains(event.x, event.y) && item.onContextMenu(ce)) {
+                // 如果点在子项目上且子项目处理了事件
+                return true;
+            }
+        }
+        return false;
+    } // Function onContextMenu()
+
+    /**
+     * 按键按下事件
+     *
+     * @param   event       事件参数
+     */
+    onKeyDown(event: KeyboardEvent): void {} // Function onKeyDown()
+
+    /**
+     * 按键press事件
+     *
+     * @param   event       事件参数
+     */
+    onKeyPress(event: KeyboardEvent): void {} // Function onKeyPress()
+
+    /**
+     * 按键松开事件
+     *
+     * @param   event       事件参数
+     */
+    onKeyUp(event: KeyboardEvent): void {} // Function onKeyUp()
+
+    // =================================================================================================================
+    // 私有方法
+    /**
+     * 按ZOrder排序
+     *
+     * @param   a   比较元素1
+     * @param   b   比较元素2
+     * @return {number}
+     */
+    private static sortItemZOrder(a: SGraphItem, b: SGraphItem): number {
+        return a.zOrder - b.zOrder;
+    } // Function sortItemZOrder()
+
+    /**
+     * 鼠标事件转子对象鼠标事件
+     *
+     * @param   child   子对象
+     * @param   event   事件参数
+     * @return  子对象鼠标事件
+     */
+    private static toChildMouseEvent(
+        child: SGraphItem,
+        event: SMouseEvent
+    ): SMouseEvent {
+        let ce = new SMouseEvent(event);
+        ce.x = (event.x - child.x) / child.scale;
+        ce.y = (event.y - child.y) / child.scale;
+        return ce;
+    } // Function toChildMouseEvent()
+
+    /**
+     * 锁定item
+     *
+     * @param   item    被锁定的item
+     */
+    private grabItem(item: SGraphItem): void {
+        if (this.scene != null) {
+            this.scene.grabItem = item;
+        }
+    } // Function grabItem
+
+    /**
+     * 释放被锁定的item
+     */
+    private releaseItem(): void {
+        if (this.scene != null) {
+            this.scene.grabItem = null;
+        }
+    } // Function grabItem
+
+    /**
+     * 判断是否处理事件
+     *
+     * @return  true: 处理事件,否则不处理
+     */
+    private acceptEvent(): boolean {
+        return this.visible && this.enabled;
+    } // Function acceptEvent()
+
+    /**
+     * 点选item对象
+     *
+     * @param   event       事件参数
+     */
+    private clickSelect(event: SMouseEvent): void {
+        // 如果Item不可被选中,或没有按下鼠标左键,则直接返回
+        if (!this.selectable) {
+            return;
+        }
+
+        // 如果按下Ctrl键,只改变当前item的选择标志
+        if (event.ctrlKey) {
+            this.selected = !this.selected;
+            return;
+        }
+
+        this.selected = true;
+    } // Function clickSelect()
+} // Class SGraphItem

+ 281 - 0
saga-web-graph/src/SGraphScene.ts

@@ -0,0 +1,281 @@
+import { SMouseEvent } from "@saga-web/base/lib";
+import { SPainter, SRect } from "@saga-web/draw/lib";
+import { SGraphItem } from "./SGraphItem";
+import { SGraphView } from "./SGraphView";
+
+/**
+ * Graphy图形引擎场景类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphScene {
+    /** 展示场景的视图 */
+    view: SGraphView | null = null;
+    /** 根节点 */
+    protected root: SGraphItem = new SGraphItem();
+    /** 当前捕获Item */
+    grabItem: SGraphItem | null = null;
+    /** 鼠标所在Item */
+    hoverItem: SGraphItem | null = null;
+
+    /**
+     * 构造函数
+     */
+    constructor() {
+        this.root.scene = this;
+    } // Constructor
+
+    /**
+     * 添加item对象到场景。
+     *
+     * @param   item        添加的对象
+     */
+    addItem(item: SGraphItem): void {
+        item.parent = this.root;
+    } // Functin addItem()
+
+    /**
+     * 从场景中移除Item。
+     *
+     * @param   item        被移除的对象
+     */
+    removeItem(item: SGraphItem): void {
+        item.parent = null;
+    } // Function removeItem()
+
+    /**
+     * 绘制场景
+     *
+     * @param   painter     painter对象
+     * @param   rect        更新绘制区域
+     */
+    drawScene(painter: SPainter, rect: SRect): void {
+        this.root.onPaint(painter, rect);
+    } // Function drawScene()
+
+    /**
+     * 绘制背景
+     *
+     * @param   painter     painter对象
+     * @param   rect        更新绘制区域
+     */
+    drawBackground(painter: SPainter, rect: SRect) {
+        // DO NOTHING
+    } // Function drawBackground()
+
+    /**
+     * 绘制前景
+     *
+     * @param   painter     painter对象
+     * @param   rect        更新绘制区域
+     */
+    drawForeground(painter: SPainter, rect: SRect) {
+        // DO NOTHING
+    } // Function drawForeground()
+
+    /**
+     * 所有item占用的矩形区域
+     */
+    allItemRect(): SRect | null {
+        let rect: SRect | null = null;
+
+        // 依次取item列中的所有item。将所有item的边界做并焦处理。
+        for (let item of this.root.children) {
+            if (rect == null) {
+                rect = item.boundingRect().translated(item.pos.x, item.pos.y);
+            } else {
+                rect.union(
+                    item.boundingRect().translated(item.pos.x, item.pos.y)
+                );
+            }
+        }
+
+        return rect;
+    } // Function allItemRect()
+
+    /**
+     * 被选中item占用的矩形区域
+     */
+    selectedItemRect(): SRect | null {
+        let rect: SRect | null = null;
+
+        // 依次取item列中的所有item。将所有item的边界做并焦处理。
+        for (let item of this.root.children) {
+            // 如果item未被选中,则去选择下一个item
+            if (!item.selected) {
+                continue;
+            }
+
+            if (rect == null) {
+                rect = item.boundingRect().translated(item.pos.x, item.pos.y);
+            } else {
+                rect.union(
+                    item.boundingRect().translated(item.pos.x, item.pos.y)
+                );
+            }
+        }
+
+        return rect;
+    } // Function selectedItemRect()
+
+    /**
+     * 获得选中的对象列表
+     *
+     * @return  选中对象列表
+     */
+    selectedItems(): SGraphItem[] {
+        let itemList = Array<SGraphItem>();
+        for (let item of this.root.children) {
+            // 如果item未被选中,则去选择下一个item
+            if (item.selected) {
+                itemList.push(item);
+            }
+        }
+        return itemList;
+    } // Function selectedItems()
+
+    // =================================================================================================================
+    // 事件
+    /**
+     * 鼠标单击事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onClick(event: SMouseEvent): boolean {
+        if (this.grabItem != null) {
+            return this.grabItem.onClick(
+                SGraphScene.toGrabItemMotionEvent(this.grabItem, event)
+            );
+        }
+        return this.root.onClick(event);
+    } // Function onClick()
+
+    /**
+     * 鼠标双击事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onDoubleClick(event: SMouseEvent): boolean {
+        if (this.grabItem != null) {
+            return this.grabItem.onDoubleClick(
+                SGraphScene.toGrabItemMotionEvent(this.grabItem, event)
+            );
+        }
+        return this.root.onDoubleClick(event);
+    } // Function onDoubleClick()
+
+    /**
+     * 鼠标按下事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseDown(event: SMouseEvent): boolean {
+        if (this.grabItem != null) {
+            return this.grabItem.onMouseDown(
+                SGraphScene.toGrabItemMotionEvent(this.grabItem, event)
+            );
+        }
+        return this.root.onMouseDown(event);
+    } // Function onMouseDown()
+
+    /**
+     * 鼠标移动事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseMove(event: SMouseEvent): boolean {
+        if (this.grabItem != null) {
+            return this.grabItem.onMouseMove(
+                SGraphScene.toGrabItemMotionEvent(this.grabItem, event)
+            );
+        }
+        return this.root.onMouseMove(event);
+    } // Function onMouseMove()
+
+    /**
+     * 释放鼠标事件
+     *
+     * @param   event       保存事件参数
+     * @return  boolean
+     */
+    onMouseUp(event: SMouseEvent): boolean {
+        if (this.grabItem != null) {
+            return this.grabItem.onMouseUp(
+                SGraphScene.toGrabItemMotionEvent(this.grabItem, event)
+            );
+        }
+        return this.root.onMouseUp(event);
+    } // Function onMouseUp()
+
+    /**
+     * 上下文菜单事件
+     *
+     * @param   event       事件参数
+     */
+    onContextMenu(event: SMouseEvent): boolean {
+        if (this.grabItem != null) {
+            return this.grabItem.onContextMenu(
+                SGraphScene.toGrabItemMotionEvent(this.grabItem, event)
+            );
+        }
+
+        return this.root.onContextMenu(event);
+    } // Function onContextMenu()
+
+    /**
+     * 按键按下事件
+     *
+     * @param   event       事件参数
+     */
+    onKeyDown(event: KeyboardEvent): void {
+        if (this.grabItem != null) {
+            return this.grabItem.onKeyDown(event);
+        }
+        return this.root.onKeyDown(event);
+    } // Function onKeyDown()
+
+    // /**
+    //  * 按键press事件
+    //  *
+    //  * @param   event       事件参数
+    //  */
+    // onKeyPress(event: KeyboardEvent): void {
+    //     if (this.grabItem != null) {
+    //         this.grabItem.onKeyPress(event);
+    //     }
+    // } // Function onKeyPress()
+
+    /**
+     * 按键松开事件
+     *
+     * @param   event       事件参数
+     */
+    onKeyUp(event: KeyboardEvent): void {
+        if (this.grabItem != null) {
+            return this.grabItem.onKeyUp(event);
+        }
+        return this.root.onKeyUp(event);
+    } // Function onKeyUp()
+
+    /**
+     * 转换场景事件坐标到指定Item坐标事件
+     *
+     * @param   item        指定的item对象
+     * @param   event       场景事件
+     * @return  {}
+     */
+    private static toGrabItemMotionEvent(
+        item: SGraphItem,
+        event: SMouseEvent
+    ): SMouseEvent {
+        let se = { ...event };
+        let p = item.mapFromScene(event.x, event.y);
+        se.x = p.x;
+        se.y = p.y;
+        return se;
+    } // Function toGrabItemMotionEvent()
+} // Class SGraphScene

+ 412 - 0
saga-web-graph/src/SGraphView.ts

@@ -0,0 +1,412 @@
+import { SMouseEvent, SNetUtil } from "@saga-web/base/lib";
+import {
+    SCanvasView,
+    SPainter,
+    SPoint,
+    SRect,
+    SSvgPaintEngine
+} from "@saga-web/draw/lib";
+import { SGraphScene } from "./SGraphScene";
+import { SGraphItem } from "./SGraphItem";
+
+/**
+ * Graphy图形引擎视图类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphView extends SCanvasView {
+    /** 场景对象 */
+    private _scene: SGraphScene | null = null;
+    get scene(): SGraphScene | null {
+        return this._scene;
+    } // Get scene
+    set scene(v: SGraphScene | null) {
+        if (this._scene != null) {
+            this._scene.view = null;
+        }
+        this._scene = v;
+        if (this._scene != null) {
+            this._scene.view = this;
+        }
+    } // Set scene
+
+    /**
+     * 构造函数
+     *
+     * @param   id      画布对象ID
+     */
+    constructor(id: string) {
+        super(id);
+    } // Function constructor()
+
+    /**
+     * 保存场景SVG文件
+     *
+     * @param   name    文件名
+     * @param   width   svg宽度
+     * @param   height  svg高度
+     */
+    saveSceneSvg(name: string, width: number, height: number): void {
+        let url = URL.createObjectURL(
+            new Blob([this.sceneSvgData(width, height)], {
+                type: "text/xml,charset=UTF-8"
+            })
+        );
+        SNetUtil.downLoad(name, url);
+    } // Function saveSceneSvg()
+
+    /**
+     * 场景SVG图形的数据
+     *
+     * @param   width   svg宽度
+     * @param   height  svg高度
+     * @return  URL地址
+     */
+    sceneSvgData(width: number, height: number): string {
+        if (null == this.scene) {
+            return "";
+        }
+        let engine = new SSvgPaintEngine(width, height);
+        let painter = new SPainter(engine);
+
+        // 保存视图缩放比例与原点位置
+        let s0 = this.scale;
+        let x0 = this.origin.x;
+        let y0 = this.origin.y;
+
+        // 场景中无对象
+        let rect = this.scene.allItemRect();
+        this.fitRectToSize(width, height, rect);
+        this.onDraw(painter);
+
+        // 恢复视图缩放比例与原点位置
+        this.scale = s0;
+        this.origin.x = x0;
+        this.origin.y = y0;
+
+        return engine.toSvg();
+    } // Function saveSvg()
+
+    /**
+     * 适配视图到视图
+     */
+    fitSceneToView(): void {
+        if (null == this.scene) {
+            return;
+        }
+
+        // 场景中无对象
+        let rect = this.scene.allItemRect();
+        this.fitRectToSize(this.width, this.height, rect);
+    } // Function FitView()
+
+    /**
+     * 适配选中的对象在视图中可见
+     */
+    fitSelectedToView(): void {
+        if (null == this.scene) {
+            return;
+        }
+
+        // 场景中无对象
+        let rect = this.scene.selectedItemRect();
+        this.fitRectToSize(this.width, this.height, rect);
+    } // Function fitSelectedToSize()
+
+    /**
+     * 适配任意对象在视图中可见
+     */
+    fitItemToView(itemList: SGraphItem[]): void {
+        if (null == this.scene) {
+            return;
+        }
+        let rect: SRect | null = null;
+
+        // 依次取item列中的所有item。将所有item的边界做并焦处理。
+        for (let item of itemList) {
+            if (rect == null) {
+                rect = item.boundingRect().translated(item.pos.x, item.pos.y);
+            } else {
+                rect.union(
+                    item.boundingRect().translated(item.pos.x, item.pos.y)
+                );
+            }
+        }
+        // 场景中无对象
+        this.fitRectToSize(this.width, this.height, rect);
+    } // Function fitItemToView()
+
+    /**
+     * 将场景中的xy坐标转换成视图坐标。
+     *
+     * @param   x       场景中的横坐标
+     * @param   y       场景中的纵坐标
+     * @return  视图坐标
+     */
+    mapFromScene(x: number, y: number): SPoint;
+
+    /**
+     * 将场景中的xy坐标转换成视图坐标。
+     *
+     * @param   pos      场景中的坐标
+     * @return  视图坐标
+     */
+    mapFromScene(pos: SPoint): SPoint;
+
+    /**
+     * 将场景中的xy坐标转换成视图坐标(重载实现)。
+     *
+     * @param   x       场景中的横坐标
+     * @param   y       场景中的纵坐标
+     * @return  视图坐标
+     */
+    mapFromScene(x: number | SPoint, y?: number): SPoint {
+        if (x instanceof SPoint) {
+            // 如果传入的是SPoint对象
+            return new SPoint(
+                x.x * this.scale + this.origin.x,
+                x.y * this.scale + this.origin.y
+            );
+        }
+
+        // @ts-ignore
+        return new SPoint(
+            x * this.scale + this.origin.x,
+            (y == undefined ? 0 : y) * this.scale + this.origin.y
+        );
+    } // Function mapFromScene()
+
+    /**
+     * 将i视图的xy坐标转换成场景坐标。
+     *
+     * @param   x       视图横坐标
+     * @param   y       视图纵坐标
+     * @return  场景坐标
+     */
+    mapToScene(x: number, y: number): SPoint;
+
+    /**
+     * 将i视图的xy坐标转换成场景坐标。
+     *
+     * @param   pos     视图坐标
+     * @return  场景坐标
+     */
+    mapToScene(pos: SPoint): SPoint;
+
+    /**
+     * 将i视图的xy坐标转换成场景坐标。(不推荐在外部调用)
+     *
+     * @param   x       视图的横坐标/或SPoint对象
+     * @param   y       视图的纵坐标
+     * @return  场景坐标
+     */
+    mapToScene(x: number | SPoint, y?: number): SPoint {
+        if (x instanceof SPoint) {
+            // 如果传入的是SPoint对象
+            return new SPoint(
+                (x.x - this.origin.x) / this.scale,
+                (x.y - this.origin.y) / this.scale
+            );
+        }
+
+        return new SPoint(
+            (x - this.origin.x) / this.scale,
+            ((y == undefined ? 0 : y) - this.origin.y) / this.scale
+        );
+    } // Function mapToScene()
+
+    /**
+     * 绘制视图
+     *
+     * @param   painter     painter对象
+     */
+    protected onDraw(painter: SPainter): void {
+        painter.save();
+        painter.clearRect(0, 0, this.width, this.height);
+        painter.restore();
+
+        // 如果未设备场景
+        if (this.scene == null) {
+            return;
+        }
+        // 绘制背景
+        painter.save();
+        this.drawBackground(painter);
+        painter.restore();
+
+        // 绘制场景
+        painter.save();
+        painter.translate(this.origin.x, this.origin.y);
+        painter.scale(this.scale, this.scale);
+        this.scene.drawScene(painter, new SRect());
+        painter.restore();
+
+        // 绘制前景
+        painter.save();
+        this.drawForeground(painter);
+        painter.restore();
+    } // Function onDraw();
+
+    /**
+     * 绘制场景背景
+     *
+     * @param   painter     painter对象
+     */
+    protected drawBackground(painter: SPainter): void {
+        // DO NOTHING
+    } // Function drawBackground()
+
+    /**
+     * 绘制场景前景
+     *
+     * @param   painter     painter对象
+     */
+    protected drawForeground(painter: SPainter): void {
+        // DO NOTHING
+    } // Function drawForeground()
+
+    /**
+     * 鼠标单击事件
+     *
+     * @param   event       事件参数
+     */
+    protected onClick(event: MouseEvent): void {
+        if (this.scene != null) {
+            let ce = this.toSceneMotionEvent(event);
+            this.scene.onClick(ce);
+        }
+    } // Function onClick()
+
+    /**
+     * 鼠标双击事件
+     *
+     * @param   event       事件参数
+     */
+    protected onDoubleClick(event: MouseEvent): void {
+        if (this.scene != null) {
+            let ce = this.toSceneMotionEvent(event);
+            this.scene.onDoubleClick(ce);
+        }
+    } // Function onClick()
+
+    /**
+     * 鼠标按下事件
+     *
+     * @param   event       事件参数
+     */
+    protected onMouseDown(event: MouseEvent): void {
+        super.onMouseDown(event);
+        if (this.scene != null) {
+            let ce = this.toSceneMotionEvent(event);
+            this.scene.onMouseDown(ce);
+        }
+    } // Function onClick()
+
+    /**
+     * 鼠标移动事件
+     *
+     * @param   event       事件参数
+     */
+    protected onMouseMove(event: MouseEvent): void {
+        super.onMouseMove(event);
+        if (this.scene != null) {
+            let ce = this.toSceneMotionEvent(event);
+            this.scene.onMouseMove(ce);
+        }
+    } // Function onClick()
+
+    /**
+     * 鼠标松开事件
+     *
+     * @param   event       事件参数
+     */
+    protected onMouseUp(event: MouseEvent): void {
+        super.onMouseUp(event);
+        if (this.scene != null) {
+            let ce = this.toSceneMotionEvent(event);
+            this.scene.onMouseUp(ce);
+        }
+    } // Function onClick()
+
+    /**
+     * 上下文菜单事件
+     *
+     * @param   event       事件参数
+     */
+    protected onContextMenu(event: MouseEvent): void {
+        if (this.scene != null) {
+            let ce = this.toSceneMotionEvent(event);
+            this.scene.onContextMenu(ce);
+        }
+    } // Function onContextMenu()
+
+    /**
+     * 按键按下事件
+     *
+     * @param   event       事件参数
+     */
+    protected onKeyDown(event: KeyboardEvent): void {
+        if (this.scene != null) {
+            this.scene.onKeyDown(event);
+        }
+    } // Function onKeyDown()
+
+    // /**
+    //  * 按键press事件
+    //  *
+    //  * @param   event       事件参数
+    //  */
+    // protected onKeyPress(event: KeyboardEvent): void {
+    //     if (this.scene != null) {
+    //         this.scene.onKeyPress(event);
+    //     }
+    // } // Function onKeyPress()
+
+    /**
+     * 按键松开事件
+     *
+     * @param   event       事件参数
+     */
+    protected onKeyUp(event: KeyboardEvent): void {
+        if (this.scene != null) {
+            this.scene.onKeyUp(event);
+        }
+    } // Function onKeyUp()
+
+    /**
+     * 适配场景在视图中可见
+     *
+     * @param   width       宽度
+     * @param   height      高度
+     * @param   rect        对象的矩阵大小
+     */
+    private fitRectToSize(
+        width: number,
+        height: number,
+        rect: SRect | null
+    ): void {
+        // 未设置场景
+        if (null == rect || !rect.isValid()) {
+            return;
+        }
+
+        this.scale = Math.min(width / rect.width, height / rect.height) * 0.8;
+
+        // 计算场景中心点
+        let center = rect.center();
+        this.origin.x = width / 2.0 - center.x * this.scale;
+        this.origin.y = height / 2.0 - center.y * this.scale;
+    } // Function fitRectToSize()
+
+    /**
+     * MouseEvent事件转换成场景SMouseEvent事件
+     *
+     * @param   event       事件参数
+     */
+    private toSceneMotionEvent(event: MouseEvent): SMouseEvent {
+        let se = new SMouseEvent(event);
+        se.x = (se.x - this.origin.x) / this.scale;
+        se.y = (se.y - this.origin.y) / this.scale;
+        return se;
+    } // Function toSceneMotionEvent()
+} // Class SGraphyView

+ 45 - 0
saga-web-graph/src/commands/SGraphCommand.ts

@@ -0,0 +1,45 @@
+/*
+ * ********************************************************************************************************************
+ *
+ *               iFHS7.
+ *              ;BBMBMBMc                  rZMBMBR              BMB
+ *              MBEr:;PBM,               7MBMMEOBB:             BBB                       RBW
+ *     XK:      BO     SB.     :SZ       MBM.       c;;     ir  BBM :FFr       :SSF:    ;xBMB:r   iuGXv.    i:. iF2;
+ *     DBBM0r.  :D     S7   ;XMBMB       GMBMu.     MBM:   BMB  MBMBBBMBMS   WMBMBMBBK  MBMBMBM  BMBRBMBW  .MBMBMBMBB
+ *      :JMRMMD  ..    ,  1MMRM1;         ;MBMBBR:   MBM  ;MB:  BMB:   MBM. RMBr   sBMH   BM0         UMB,  BMB.  KMBv
+ *     ;.   XOW  B1; :uM: 1RE,   i           .2BMBs  rMB. MBO   MBO    JMB; MBB     MBM   BBS    7MBMBOBM:  MBW   :BMc
+ *     OBRJ.SEE  MRDOWOR, 3DE:7OBM       .     ;BMB   RMR7BM    BMB    MBB. BMB    ,BMR  .BBZ   MMB   rMB,  BMM   rMB7
+ *     :FBRO0D0  RKXSXPR. JOKOOMPi       BMBSSWBMB;    BMBB:    MBMB0ZMBMS  .BMBOXRBMB    MBMDE RBM2;SMBM;  MBB   xBM2
+ *         iZGE  O0SHSPO. uGZ7.          sBMBMBDL      :BMO     OZu:BMBK,     rRBMB0;     ,EBMB  xBMBr:ER.  RDU   :OO;
+ *     ,BZ, 1D0  RPSFHXR. xWZ .SMr                  . .BBB
+ *      :0BMRDG  RESSSKR. 2WOMBW;                   BMBMR
+ *         i0BM: SWKHKGO  MBDv
+ *           .UB  OOGDM. MK,                                          Copyright (c) 2015-2019.  斯伯坦机器人
+ *              ,  XMW  ..
+ *                  r                                                                     All rights reserved.
+ *
+ * ********************************************************************************************************************
+ */
+
+import { SUndoCommand } from "@saga-web/base/lib";
+import { SGraphScene } from "../SGraphScene";
+
+/**
+ * Graph命令基类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export abstract class SGraphCommand extends SUndoCommand {
+    /** 命令所属的场景类 */
+    scene: SGraphScene;
+
+    /**
+     * 构造函数
+     *
+     * @param   scene       命令所属的场景类
+     */
+    protected constructor(scene: SGraphScene) {
+        super();
+        this.scene = scene;
+    } // Constructor
+} // Class SGraphCommand

+ 34 - 0
saga-web-graph/src/index.ts

@@ -0,0 +1,34 @@
+/*
+ * ********************************************************************************************************************
+ *
+ *               iFHS7.
+ *              ;BBMBMBMc                  rZMBMBR              BMB
+ *              MBEr:;PBM,               7MBMMEOBB:             BBB                       RBW
+ *     XK:      BO     SB.     :SZ       MBM.       c;;     ir  BBM :FFr       :SSF:    ;xBMB:r   iuGXv.    i:. iF2;
+ *     DBBM0r.  :D     S7   ;XMBMB       GMBMu.     MBM:   BMB  MBMBBBMBMS   WMBMBMBBK  MBMBMBM  BMBRBMBW  .MBMBMBMBB
+ *      :JMRMMD  ..    ,  1MMRM1;         ;MBMBBR:   MBM  ;MB:  BMB:   MBM. RMBr   sBMH   BM0         UMB,  BMB.  KMBv
+ *     ;.   XOW  B1; :uM: 1RE,   i           .2BMBs  rMB. MBO   MBO    JMB; MBB     MBM   BBS    7MBMBOBM:  MBW   :BMc
+ *     OBRJ.SEE  MRDOWOR, 3DE:7OBM       .     ;BMB   RMR7BM    BMB    MBB. BMB    ,BMR  .BBZ   MMB   rMB,  BMM   rMB7
+ *     :FBRO0D0  RKXSXPR. JOKOOMPi       BMBSSWBMB;    BMBB:    MBMB0ZMBMS  .BMBOXRBMB    MBMDE RBM2;SMBM;  MBB   xBM2
+ *         iZGE  O0SHSPO. uGZ7.          sBMBMBDL      :BMO     OZu:BMBK,     rRBMB0;     ,EBMB  xBMBr:ER.  RDU   :OO;
+ *     ,BZ, 1D0  RPSFHXR. xWZ .SMr                  . .BBB
+ *      :0BMRDG  RESSSKR. 2WOMBW;                   BMBMR
+ *         i0BM: SWKHKGO  MBDv
+ *           .UB  OOGDM. MK,                                          Copyright (c) 2015-2019.  斯伯坦机器人
+ *              ,  XMW  ..
+ *                  r                                                                     All rights reserved.
+ *
+ * ********************************************************************************************************************
+ */
+import { SGraphCommand } from "./commands/SGraphCommand";
+export { SGraphCommand };
+
+import { SGraphClockItem } from "./items/SGraphClockItem";
+import { SGraphLineItem } from "./items/SGraphLineItem";
+export { SGraphClockItem, SGraphLineItem };
+
+import { SGraphItem } from "./SGraphItem";
+import { SGraphScene } from "./SGraphScene";
+import { SGraphView } from "./SGraphView";
+
+export { SGraphItem, SGraphScene, SGraphView };

+ 208 - 0
saga-web-graph/src/items/SGraphClockItem.ts

@@ -0,0 +1,208 @@
+import {
+    SColor,
+    SLineCapStyle,
+    SPainter,
+    SRect,
+    SSize
+} from "@saga-web/draw/lib";
+import { SGraphItem } from "../SGraphItem";
+
+/**
+ * 线Item类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphClockItem extends SGraphItem {
+    /** 大小 */
+    size: SSize;
+
+    /** 宽度 */
+    get width() {
+        return this.size.width;
+    } // Get width
+    set width(v: number) {
+        this.size.width = v;
+    } // Set width
+
+    /** 高度 */
+    get height() {
+        return this.size.height;
+    } // Get width
+    set height(v: number) {
+        this.size.height = v;
+    } // Set width
+
+    /** 半径 */
+    get radius() {
+        return Math.min(this.width, this.height) / 2.0;
+    } // Get radius
+
+    /**
+     * 构造函数
+     *
+     * @param   parent      指向父Item
+     * @param   size        大小
+     */
+    constructor(parent: SGraphItem | null, size: SSize);
+
+    /**
+     * 构造函数
+     *
+     * @param   parent      指向父Item
+     * @param   width       宽度
+     * @param   height      高度
+     */
+    constructor(parent: SGraphItem | null, width: number, height: number);
+
+    /**
+     * 构造函数
+     *
+     * @param   parent      指向父Item
+     * @param   width       宽度
+     * @param   height      高度
+     */
+    constructor(
+        parent: SGraphItem | null,
+        width: number | SSize,
+        height?: number
+    ) {
+        super(parent);
+        if (width instanceof SSize) {
+            this.size = new SSize(width.width, width.height);
+        } else {
+            this.size = new SSize(width as number, height as number);
+        }
+    } // Constructor()
+
+    /**
+     * 对象边界区域
+     *
+     * @return  边界区域
+     */
+    boundingRect(): SRect {
+        return new SRect(0, 0, this.width, this.height);
+    } // Function SRect()
+
+    /**
+     * Item绘制操作
+     *
+     * @param   painter       painter对象
+     */
+    onDraw(painter: SPainter): void {
+        painter.translate(this.width / 2, this.height / 2);
+        let t = new Date();
+
+        this.drawScale(painter);
+        this.drawHour(painter, t.getHours(), t.getMinutes(), t.getSeconds());
+        this.drawMinute(painter, t.getMinutes(), t.getSeconds());
+        this.drawSecond(painter, t.getSeconds() + t.getMilliseconds() / 1000.0);
+    } // Function onDraw()
+
+    /**
+     * 绘制表刻度
+     *
+     * @param   painter     painter对象
+     */
+    private drawScale(painter: SPainter): void {
+        let scaleLength = Math.max(this.radius / 10.0, 2.0);
+        let scaleLength1 = scaleLength * 1.2;
+        let strokeWidth = Math.max(this.radius / 100.0, 2.0);
+        let strokeWidth1 = strokeWidth * 2.0;
+
+        painter.save();
+        painter.pen.color = SColor.Blue;
+
+        for (let i = 1; i <= 12; i++) {
+            // 12小时刻度
+            painter.pen.lineWidth = strokeWidth1;
+            painter.drawLine(0, -this.radius, 0, -this.radius + scaleLength1);
+
+            if (this.radius >= 40) {
+                // 如果半度大于40显示分钟刻度
+                painter.rotate((6 * Math.PI) / 180);
+                for (let j = 1; j <= 4; j++) {
+                    // 分钟刻度
+                    painter.pen.lineWidth = strokeWidth;
+                    painter.drawLine(
+                        0,
+                        -this.radius,
+                        0,
+                        -this.radius + scaleLength
+                    );
+                    painter.rotate((6 * Math.PI) / 180);
+                }
+            } else {
+                painter.rotate((30 * Math.PI) / 180);
+            }
+        }
+
+        painter.restore();
+    } // Function drawScale()
+
+    /**
+     * 绘制时针
+     *
+     * @param   painter     painter对象
+     * @param   hour        时
+     * @param   minute      分
+     * @param   second      秒
+     */
+    private drawHour(
+        painter: SPainter,
+        hour: number,
+        minute: number,
+        second: number
+    ): void {
+        painter.save();
+        painter.pen.lineCapStyle = SLineCapStyle.Round;
+        painter.pen.lineWidth = Math.max(this.radius / 30.0, 4.0);
+        painter.rotate(
+            ((hour * 30.0 + (minute * 30.0) / 60 + (second * 30.0) / 3600) *
+                Math.PI) /
+                180
+        );
+        painter.drawLine(0, this.radius / 10.0, 0, -this.radius / 2.0);
+        painter.restore();
+    } // Function drawHour()
+
+    /**
+     * 绘制秒针
+     *
+     * @param   painter     painter对象
+     * @param   minute      分
+     * @param   second      秒
+     */
+    private drawMinute(
+        painter: SPainter,
+        minute: number,
+        second: number
+    ): void {
+        painter.save();
+        painter.pen.lineCapStyle = SLineCapStyle.Round;
+        painter.pen.lineWidth = Math.max(this.radius / 40.0, 4.0);
+        painter.rotate(((minute * 6 + (second * 6) / 60.0) * Math.PI) / 180);
+        painter.drawLine(0, this.radius / 10.0, 0, (-this.radius * 2.0) / 3.0);
+        painter.restore();
+    } // Function drawMinute()
+
+    /**
+     * 绘制秒针
+     *
+     * @param   painter     painter对象
+     * @param   second      秒
+     */
+    private drawSecond(painter: SPainter, second: number): void {
+        painter.save();
+        painter.pen.lineCapStyle = SLineCapStyle.Round;
+        painter.pen.lineWidth = Math.max(this.radius / 100.0, 3.0);
+        painter.pen.color = SColor.Red;
+        painter.rotate((second * 6 * Math.PI) / 180);
+        painter.drawLine(
+            0,
+            this.radius / 5.0,
+            0,
+            -this.radius + this.radius / 10.0
+        );
+        painter.restore();
+    } // Function drawSecond()
+} // Class SGraphClockItem

+ 72 - 0
saga-web-graph/src/items/SGraphLineItem.ts

@@ -0,0 +1,72 @@
+import { SColor, SPainter, SPen, SRect } from "@saga-web/draw/lib";
+import { SGraphItem } from "../SGraphItem";
+
+/**
+ * 线Item类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphLineItem extends SGraphItem {
+    x1: number;
+    y1: number;
+    x2: number;
+    y2: number;
+    color: SColor = SColor.Black;
+    width: number = 1;
+
+    /**
+     * 构造函数
+     *
+     * @param x1  线的起始x坐标
+     * @param y1  线的起始y坐标
+     * @param x2    线的终止x坐标
+     * @param y2    线的终止y坐标
+     * @param width   线的宽度
+     *
+     * @param color  线的颜色
+     * @param parent    是否为虚线
+     */
+    constructor(
+        parent: SGraphItem | null,
+        x1: number,
+        y1: number,
+        x2: number,
+        y2: number,
+        color: SColor = SColor.Black,
+        width: number = 1
+    ) {
+        super(parent);
+        this.x1 = x1;
+        this.y1 = y1;
+        this.x2 = x2;
+        this.y2 = y2;
+    } // Constructor()
+
+    /**
+     * Item对象边界区域
+     *
+     * @return SRect
+     */
+    boundingRect(): SRect {
+        let minX = Math.min(this.x1, this.x2);
+        let minY = Math.min(this.y1, this.y2);
+        let maxX = Math.max(this.x1, this.x2);
+        let maxY = Math.max(this.y1, this.y2);
+        return new SRect(
+            minX - this.width / 2,
+            minY - this.width / 2,
+            maxX - minX + this.width,
+            maxY - minY + this.width
+        );
+    } // Function boundingRect()
+
+    /**
+     * 绘制Item
+     *
+     * @param   painter     画布
+     */
+    onDraw(painter: SPainter): void {
+        painter.pen = new SPen(this.color, this.width);
+        painter.drawLine(this.x1, this.y1, this.x2, this.y2);
+    } // Function onDraw()
+} // Class SGraphLineItem

+ 16 - 0
saga-web-graph/tsconfig.json

@@ -0,0 +1,16 @@
+{
+    "compilerOptions": {
+        "target": "es5",                            // Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'.
+        "module": "commonjs",                       // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
+        "outDir": "./lib",                          // 编译后生成的文件目录
+        "strict": true,                             // 开启严格的类型检测
+        "declaration": true,                        // 生成 `.d.ts` 文件
+        "experimentalDecorators": true,             // 开启装饰器
+        "removeComments": true,                     // 去除注释
+        "noImplicitAny": true,                      // 在表达式和声明上有隐含的 any类型时报错。
+        "esModuleInterop": true,                    // 支持别名导入
+        "moduleResolution": "node"                  // 此处设置为node,才能解析import xx from 'xx'
+    },
+    "include": ["./src"],
+    "exclude": ["node_modules"]
+}

+ 6 - 0
saga-web-graph/typedoc.json

@@ -0,0 +1,6 @@
+{
+    "name": "上格云绘制引擎",
+    "mode": "file",
+    "out": "doc",
+    "exclude": ["**/*+(index|.test).ts"]
+}