Browse Source

添加上格云绘图引擎库.

庞利祥 6 năm trước cách đây
mục cha
commit
d8a34bff32

+ 9 - 0
saga-web-graphy/.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-graphy/.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访问不需加访问修饰符
+    }
+};

+ 12 - 0
saga-web-graphy/.npmignore

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

+ 6 - 0
saga-web-graphy/README.md

@@ -0,0 +1,6 @@
+## 依赖包版本号
+~~~
+"@saga-web/base": "2.1.1"
+"@saga-web/draw": "2.1.1"
+"@saga-web/graphy": "2.1.1"
+~~~

+ 12 - 0
saga-web-graphy/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
+};

+ 36 - 0
saga-web-graphy/package.json

@@ -0,0 +1,36 @@
+{
+    "name": "@saga-web/graphy",
+    "version": "2.1.27",
+    "description": "上格云二维图形引擎。",
+    "main": "lib/index.js",
+    "types": "lib/index.d.js",
+    "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"
+    },
+    "keywords": [
+        "graphy"
+    ],
+    "author": "庞利祥 (sybotan@126.com)",
+    "license": "ISC",
+    "publishConfig": {
+        "registry": "http://dev.dp.sagacloud.cn:8082/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",
+        "prettier": "^1.18.2",
+        "@types/jest": "^24.0.15",
+        "ts-jest": "^24.0.2",
+        "typescript": "^3.5.3"
+    },
+    "dependencies": {
+        "@saga-web/draw": "^2.1.56"
+    }
+}

+ 533 - 0
saga-web-graphy/src/SGraphyItem.ts

@@ -0,0 +1,533 @@
+import { SMouseButton, SMouseEvent, SObject } from "@saga-web/base/lib";
+import { SPainter, SPoint, SRect } from "@saga-web/draw/lib";
+import { SGraphyScene } from "./SGraphyScene";
+
+/**
+ * Graphy图形引擎Item类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphyItem extends SObject {
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 属性
+    /** 场景对象 */
+    private _scene: SGraphyScene | null = null;
+    get scene(): SGraphyScene | null {
+        if (null != this._parent) {
+            return this._parent.scene;
+        } else {
+            return this._scene;
+        }
+    } // Get scene
+    set scene(v: SGraphyScene | null) {
+        this._scene = v;
+    } // Set scene
+    /** parent属性存值函数 */
+    private _parent: SGraphyItem | null = null;
+    get parent(): SGraphyItem | null {
+        return this._parent;
+    } // Get parent
+    set parent(v: SGraphyItem | 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;
+
+        // 如果新parent不为空
+        if (this._parent != null) {
+            // 将节点加入到新parent节点中
+            this._parent.children.push(this);
+            this._parent.children.sort(SGraphyItem.sortItemZOrder);
+        }
+    } // Set parent()
+
+    /** 子节点 */
+    children: SGraphyItem[] = [];
+
+    /** 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(SGraphyItem.sortItemZOrder);
+        }
+    } // Set zOrder
+
+    /** 位置 */
+    pos: SPoint = new SPoint(0, 0);
+    /** X轴坐标 */
+    get x(): number {
+        return this.pos.x;
+    } // Get x
+    set x(v: number) {
+        this.pos.x = v;
+    } // Set x
+    /** Y轴坐标 */
+    get y(): number {
+        return this.pos.y;
+    } // Get y
+    set y(v: number) {
+        this.pos.y = v;
+    } // Set y
+    /** 缩放比例 */
+    scale: number = 1;
+
+    /** 是否可见 */
+    visible: boolean = true;
+
+    /** 是否可以移动 */
+    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;
+    } // Set selected
+
+    /** 是否进行变形 */
+    isTransform = true;
+
+    /** 鼠标按下时位置 */
+    private _mouseDownPos = new SPoint();
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+    // 函数
+    /**
+     * 构造函数
+     *
+     * @param   parent      指向父对象
+     */
+    constructor(parent: SGraphyItem | null = null) {
+        super();
+    } // Function constructor()
+
+    /**
+     * Item绘制框架
+     *
+     * @param   painter       painter对象
+     * @param   rect          绘制区域
+     */
+    onPaint(painter: SPainter, rect: SRect): void {
+        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();
+        }
+
+        this.onDraw(painter);
+    } // 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 {
+        // TODO: PLX
+        // scene?.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(): SGraphyItem[] {
+        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 = SGraphyItem.toChildMouseEvent(item, event);
+            if (item.contains(event.x, event.y) && item.onClick(ce)) {
+                // 如果点在子项目上且子项目处理了事件
+                return true;
+            }
+        }
+
+        if (this.selectable) {
+            this.clickSelect(event);
+        }
+
+        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 = SGraphyItem.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 = SGraphyItem.toChildMouseEvent(item, event);
+            if (item.contains(event.x, event.y) && item.onMouseDown(ce)) {
+                // 如果点在子项目上且子项目处理了事件
+                return true;
+            }
+        }
+
+        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 = SGraphyItem.toChildMouseEvent(item, event);
+            if (item.contains(event.x, event.y) && item.onMouseMove(ce)) {
+                // 如果点在子项目上且子项目处理了事件
+                return true;
+            }
+        }
+
+        if (
+            event.buttons & SMouseButton.LeftButton &&
+            this.moveable &&
+            this._isMoving
+        ) {
+            this.moveTo(
+                this.pos.x + event.x - this._mouseDownPos.x,
+                this.pos.y + event.y - this._mouseDownPos.y
+            );
+        }
+
+        return false;
+    } // 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 = SGraphyItem.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       事件参数
+     */
+    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 = SGraphyItem.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: SGraphyItem, b: SGraphyItem): number {
+        return a.zOrder - b.zOrder;
+    } // Function sortItemZOrder()
+
+    /**
+     * 鼠标事件转子对象鼠标事件
+     *
+     * @param   child   子对象
+     * @param   event   事件参数
+     * @return  子对象鼠标事件
+     */
+    private static toChildMouseEvent(
+        child: SGraphyItem,
+        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: SGraphyItem): 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 SGraphyItem

+ 230 - 0
saga-web-graphy/src/SGraphyScene.ts

@@ -0,0 +1,230 @@
+import { SMouseEvent } from "@saga-web/base/lib";
+import { SPainter, SRect } from "@saga-web/draw/lib";
+import { SGraphyItem } from "./SGraphyItem";
+
+/**
+ * Graphy图形引擎场景类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphyScene {
+    /** 根节点 */
+    protected root: SGraphyItem = new SGraphyItem();
+    /** 当前捕获Item */
+    grabItem: SGraphyItem | null = null;
+
+    /**
+     * 构造函数
+     */
+    constructor() {
+        this.root.scene = this;
+    } // Constructor
+
+    /**
+     * 添加item对象到场景。
+     *
+     * @param   item        添加的对象
+     */
+    addItem(item: SGraphyItem): void {
+        item.parent = this.root;
+    } // Functin addItem()
+
+    /**
+     * 从场景中移除Item。
+     *
+     * @param   item        被移除的对象
+     */
+    removeItem(item: SGraphyItem): 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()
+
+    worldRect(): 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 worldRect()
+
+    // =================================================================================================================
+    // 事件
+    /**
+     * 鼠标单击事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onClick(event: SMouseEvent): boolean {
+        if (this.grabItem != null) {
+            return this.grabItem.onClick(
+                SGraphyScene.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(
+                SGraphyScene.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(
+                SGraphyScene.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(
+                SGraphyScene.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(
+                SGraphyScene.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(
+                SGraphyScene.toGrabItemMotionEvent(this.grabItem, event)
+            );
+        }
+
+        return this.root.onContextMenu(event);
+    } // Function onContextMenu()
+
+    /**
+     * 按键按下事件
+     *
+     * @param   event       事件参数
+     */
+    onKeyDown(event: KeyboardEvent): void {
+        if (this.grabItem != null) {
+            this.grabItem.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) {
+            this.grabItem.onKeyUp(event);
+        }
+    } // Function onKeyUp()
+
+    /**
+     * 转换场景事件坐标到指定Item坐标事件
+     *
+     * @param   item        指定的item对象
+     * @param   event       场景事件
+     * @return  {}
+     */
+    private static toGrabItemMotionEvent(
+        item: SGraphyItem,
+        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 SGraphyScene

+ 353 - 0
saga-web-graphy/src/SGraphyView.ts

@@ -0,0 +1,353 @@
+import { SMouseEvent, SNetUtil } from "@saga-web/base/lib";
+import {
+    SCanvasView,
+    SPainter,
+    SPoint,
+    SRect,
+    SSvgPaintEngine
+} from "@saga-web/draw/lib";
+import { SGraphyScene } from "./SGraphyScene";
+
+/**
+ * Graphy图形引擎视图类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphyView extends SCanvasView {
+    /** 场景对象 */
+    scene: SGraphyScene | null = new SGraphyScene();
+
+    /**
+     * 构造函数
+     *
+     * @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 {
+        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;
+
+        this.fitSceneToSize(width, height);
+        this.onDraw(painter);
+
+        // 恢复视图缩放比例与原点位置
+        this.scale = s0;
+        this.origin.x = x0;
+        this.origin.y = y0;
+
+        return engine.toSvg();
+    } // Function saveSvg()
+
+    /**
+     * 适配视图到视图
+     */
+    fitSceneToView(): void {
+        this.fitSceneToSize(this.width, this.height);
+    } // Function FitView()
+
+    /**
+     * 将场景中的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      高度
+     */
+    private fitSceneToSize(width: number, height: number): void {
+        // 未设置场景
+        if (null == this.scene) {
+            return;
+        }
+
+        // 场景中无对象
+        let rect = this.scene.worldRect();
+        if (null == rect) {
+            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 fitSceneToSize()
+
+    /**
+     * 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

+ 32 - 0
saga-web-graphy/src/index.ts

@@ -0,0 +1,32 @@
+/*
+ * ********************************************************************************************************************
+ *
+ *               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 { SGraphyClockItem } from "./items/SGraphyClockItem";
+import { SGraphyLineItem } from "./items/SGraphyLineItem";
+export { SGraphyClockItem, SGraphyLineItem };
+
+import { SGraphyItem } from "./SGraphyItem";
+import { SGraphyScene } from "./SGraphyScene";
+import { SGraphyView } from "./SGraphyView";
+
+export { SGraphyItem, SGraphyScene, SGraphyView };

+ 208 - 0
saga-web-graphy/src/items/SGraphyClockItem.ts

@@ -0,0 +1,208 @@
+import {
+    SColor,
+    SLineCapStyle,
+    SPainter,
+    SRect,
+    SSize
+} from "@saga-web/draw/lib";
+import { SGraphyItem } from "../SGraphyItem";
+
+/**
+ * 线Item类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphyClockItem extends SGraphyItem {
+    /** 大小 */
+    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: SGraphyItem | null, size: SSize);
+
+    /**
+     * 构造函数
+     *
+     * @param   parent      指向父Item
+     * @param   width       宽度
+     * @param   height      高度
+     */
+    constructor(parent: SGraphyItem | null, width: number, height: number);
+
+    /**
+     * 构造函数
+     *
+     * @param   parent      指向父Item
+     * @param   width       宽度
+     * @param   height      高度
+     */
+    constructor(
+        parent: SGraphyItem | 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 SGraphyClockItem

+ 72 - 0
saga-web-graphy/src/items/SGraphyLineItem.ts

@@ -0,0 +1,72 @@
+import { SColor, SPainter, SPen, SRect } from "@saga-web/draw/lib";
+import { SGraphyItem } from "../SGraphyItem";
+
+/**
+ * 线Item类
+ *
+ * @author  庞利祥(sybotan@126.com)
+ */
+export class SGraphyLineItem extends SGraphyItem {
+    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: SGraphyItem | 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 SGraphyLineItem

+ 16 - 0
saga-web-graphy/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-graphy/typedoc.json

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