Browse Source

选择器功能强化

haojianlong 4 years ago
parent
commit
597517bd1c

+ 13 - 11
persagy-web-graph/src/SGraphItem.ts

@@ -149,7 +149,7 @@ export class SGraphItem extends SObject {
     /** 是否可以移动 */
     moveable: boolean = false;
     /** 是否正在移动 */
-    private _isMoving = false;
+    protected _isMoving = false;
 
     /** 是否可用 */
     protected _enabled: boolean = true;
@@ -185,7 +185,7 @@ export class SGraphItem extends SObject {
     isTransform = true;
 
     /** 鼠标按下时位置 */
-    private _mouseDownPos = new SPoint();
+    protected _mouseDownPos = new SPoint();
 
     /** 鼠标样式    */
     cursor: string = "default";
@@ -454,11 +454,6 @@ export class SGraphItem extends SObject {
                 console.log(e);
             }
         }
-        // 选择
-        let select = false;
-        if (this.selectable) {
-            select = this.clickSelect(event);
-        }
         // 移动
         if (this.moveable) {
             this._mouseDownPos = new SPoint(event.x, event.y);
@@ -469,7 +464,7 @@ export class SGraphItem extends SObject {
             this.grabItem(this);
             return true;
         }
-        return select;
+        return false;
     } // Function onMouseDown()
 
     /**
@@ -541,12 +536,18 @@ export class SGraphItem extends SObject {
             }
         }
 
+        // 选择
+        let select = false;
+        if (this.selectable) {
+            select = this.clickSelect(event);
+        }
+
         this._isMoving = false;
         this.releaseItem();
         if (this.scene) {
             this.scene.grabItem = this._lastGrab;
         }
-        return false;
+        return select;
     } // Function onMouseUp()
 
     /**
@@ -708,7 +709,7 @@ export class SGraphItem extends SObject {
      * @param   y       Y轴
      * @return  在父节点的位置
      */
-    private toParentChange(x: number, y: number): SPoint {
+    protected toParentChange(x: number, y: number): SPoint {
         const m = new SMatrix();
         m.scale(this.scale, this.scale);
         if (!this.isTransform) {
@@ -736,7 +737,8 @@ export class SGraphItem extends SObject {
      */
     private clickSelect(event: SMouseEvent): boolean {
         // 如果Item不可被选中,或没有按下鼠标左键,则直接返回
-        if (!this.selectable || event.buttons != SMouseButton.LeftButton) {
+        if (!this.selectable) {
+            // || event.buttons != SMouseButton.LeftButton
             return false;
         }
         const scene = this.scene;

+ 1 - 0
persagy-web-graph/src/SGraphScene.ts

@@ -52,6 +52,7 @@ export class SGraphScene {
      */
     constructor() {
         this.root.scene = this;
+        this.addItem(this.selectContainer);
     } // Constructor
 
     /**

+ 319 - 8
persagy-web-graph/src/SGraphSelectContainer.ts

@@ -25,30 +25,76 @@
  */
 
 import { SGraphItem } from "./SGraphItem";
-import { SObject } from "@persagy-web/base";
 import { SGraphLayoutType } from "./enums/SGraphLayoutType";
 import { SOrderSetType } from "./enums/SOrderSetType";
+import { SColor, SPainter, SPoint, SRect, SSize } from "@persagy-web/draw";
+import { SMathUtil } from "./utils/SMathUtil";
+import { SMouseEvent } from "@persagy-web/base";
 
 /**
  * 基本选择器
  *
  * @author  郝建龙
  */
-export class SGraphSelectContainer extends SObject {
-    /** 选择对象list    */
+export class SGraphSelectContainer extends SGraphItem {
+    /** 选择对象list */
     private itemList: SGraphItem[] = [];
+    /** 外接点的list */
+    private pointList: SPoint[] = [];
+    /** item宽 */
+    private _width: number = 0;
+    get width(): number {
+        return this._width;
+    } // Get width
+    set width(v: number) {
+        if (v == this._width) {
+            return;
+        }
+        this._width = v;
+        this.update();
+    } // Set width
+    /** item高 */
+    private _height: number = 0;
+    get height(): number {
+        return this._height;
+    } // Get height
+    set height(v: number) {
+        if (v == this._height) {
+            return;
+        }
+        this._height = v;
+        this.update();
+    } // Set height
+    private oldWidth: number = 0;
+    private oldHeight: number = 0;
+    /** 绘制矩形的圆角半径 */
     /** 统计选中item的数量 */
     get count(): number {
         return this.itemList.length;
     }
     /** 置顶zorder增加基数    */
     private radix: number = 0.001;
+    /** 定时器 */
+    private timer: number | undefined;
+    /** 半径 */
+    radius: number = 5;
+    /** 全局灵敏度 */
+    dis: number = 10;
+    /** 灵敏度转换为场景长度 */
+    private sceneDis: number = 10;
+    /** 当前点索引 */
+    private curIndex: number = -1;
+    /** 当前点索引 */
+    private curPoint: SPoint | undefined;
 
     /**
      * 构造体
      * */
     constructor() {
         super();
+        this.zOrder = 9999999;
+        this.visible = false;
+        this.moveable = true;
     } // Constructor
 
     /**
@@ -64,7 +110,7 @@ export class SGraphSelectContainer extends SObject {
         }
         item.selected = true;
         this.itemList.push(item);
-        this.$emit("listChange", this.itemList);
+        this.throttle(this.selectChange, 200);
     } // Function addItem()
 
     /**
@@ -79,7 +125,7 @@ export class SGraphSelectContainer extends SObject {
         this.itemList.length = 0;
         item.selected = true;
         this.itemList.push(item);
-        this.$emit("listChange", this.itemList);
+        this.throttle(this.selectChange, 200);
     } // Function setItem()
 
     /**
@@ -91,7 +137,7 @@ export class SGraphSelectContainer extends SObject {
             t.selected = false;
         });
         this.itemList.length = 0;
-        this.$emit("listChange", this.itemList);
+        this.throttle(this.selectChange, 200);
     } // Function clear()
 
     /**
@@ -104,7 +150,7 @@ export class SGraphSelectContainer extends SObject {
             if (this.itemList[i] == item) {
                 this.itemList[i].selected = false;
                 this.itemList.splice(i, 1);
-                this.$emit("listChange", this.itemList, item);
+                this.throttle(this.selectChange, 200);
                 return;
             }
         }
@@ -116,7 +162,7 @@ export class SGraphSelectContainer extends SObject {
         }
         item.selected = true;
         this.itemList.push(item);
-        this.$emit("listChange", this.itemList);
+        this.throttle(this.selectChange, 200);
     } // Function toggleItem()
 
     /**
@@ -594,4 +640,269 @@ export class SGraphSelectContainer extends SObject {
             return 0;
         };
     } // Function compare()
+
+    /**
+     * 防抖-防止快速多次触发 itemChange;只执行最后一次
+     * */
+    throttle(fn: Function, wait: number): void {
+        if (this.timer) clearTimeout(this.timer);
+        this.timer = setTimeout((): void => {
+            this.selectChange();
+        }, wait);
+    } // Function throttle()
+
+    /**
+     * 选中的item更新时更新自身边界
+     * */
+    selectChange(): void {
+        // 抛出事件
+        this.$emit("listChange", this.itemList);
+        if (this.count > 0) {
+            // 重新计算边界
+            this.getSize();
+            // 计算边界8个点
+            this.calExtreme();
+            this.visible = true;
+        } else {
+            this.visible = false;
+            this.curIndex = -1;
+        }
+    } // Function selectChange()
+
+    /**
+     * 计算选中item的外接矩形和
+     *
+     * @return 外接矩形的和
+     * */
+    calItemBounding(): SRect {
+        if (this.itemList.length) {
+            const fir = this.itemList[0];
+            let rect = fir.boundingRect().adjusted(fir.pos.x, fir.pos.y, 0, 0);
+            for (let i = 1; i < this.itemList.length; i++) {
+                const cur = this.itemList[i];
+                rect = rect.unioned(
+                    cur.boundingRect().adjusted(cur.pos.x, cur.pos.y, 0, 0)
+                );
+            }
+            return rect;
+        } else {
+            return new SRect();
+        }
+    } // Function calItemBounding()
+
+    /**
+     * 根据矩阵得到宽高,并将自己移动至矩阵左上角
+     */
+    getSize(): void {
+        const r = this.calItemBounding();
+        this.width = r.width;
+        this.height = r.height;
+        this.moveTo(r.left, r.top);
+    } // getSize()
+
+    /**
+     * 计算外接矩形 8 个端点
+     * */
+    calExtreme(): void {
+        this.pointList = [];
+        this.pointList.push(new SPoint(0, 0));
+        this.pointList.push(new SPoint(this.width / 2, 0));
+        this.pointList.push(new SPoint(this.width, 0));
+        this.pointList.push(new SPoint(this.width, this.height / 2));
+        this.pointList.push(new SPoint(this.width, this.height));
+        this.pointList.push(new SPoint(this.width / 2, this.height));
+        this.pointList.push(new SPoint(0, this.height));
+        this.pointList.push(new SPoint(0, this.height / 2));
+    } // Function calExtreme()
+
+    /**
+     * 计算点到哪个点
+     */
+    getNearestPoint(p: SPoint): void {
+        let len = this.sceneDis;
+        for (let i = 0; i < this.pointList.length; i++) {
+            let dis = SMathUtil.pointDistance(
+                p.x,
+                p.y,
+                this.pointList[i].x,
+                this.pointList[i].y
+            );
+            if (dis < len) {
+                len = dis;
+                this.curIndex = i;
+            }
+        }
+    } // Function getNearestPoint()
+
+    /**
+     * Item对象边界区域
+     *
+     * @return  对象边界区域
+     */
+    boundingRect(): SRect {
+        return new SRect(
+            -this.sceneDis,
+            -this.sceneDis,
+            this.width + this.sceneDis + this.sceneDis,
+            this.height + this.sceneDis + this.sceneDis
+        );
+    } // Function boundingRect()
+
+    /**
+     * 宽高发发生变化
+     *
+     * @param   oldSize 改之前的大小
+     * @param   newSize 改之后大小
+     * */
+    protected resize(oldSize: SSize, newSize: SSize): void {
+        for (let v of this.itemList) {
+            // @ts-ignore
+            v.resize && v.resize(oldSize, newSize);
+        }
+    } // Function resize()
+
+    /**
+     * 鼠标按下事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseDown(event: SMouseEvent): boolean {
+        this.curIndex = -1;
+        if (event.buttons == 1) {
+            this.getNearestPoint(new SPoint(event.x, event.y));
+            if (this.curIndex < 0) {
+                this.curPoint = undefined;
+                return super.onMouseDown(event);
+            } else {
+                const cur = this.pointList[this.curIndex];
+                this.curPoint = new SPoint(cur.x, cur.y);
+                this.oldWidth = this.width;
+                this.oldHeight = this.height;
+                this.grabItem(this);
+            }
+            this.update();
+            return true;
+        }
+        return super.onMouseDown(event);
+    } // Function onMouseDown()
+
+    /**
+     * 鼠标按下事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseMove(event: SMouseEvent): boolean {
+        if (this.curIndex > -1 && this.count == 1) {
+            // @ts-ignore
+            const difX = event.x - this.curPoint.x;
+            // @ts-ignore
+            const difY = event.y - this.curPoint.y;
+            const mp = this.toParentChange(difX, difY);
+            switch (this.curIndex) {
+                case 0:
+                    this.moveTo(this.pos.x + mp.x, this.pos.y + mp.y);
+                    this.width = this.width - difX;
+                    this.height = this.height - difY;
+                    break;
+                case 1:
+                    this.height = this.height - difY;
+                    this.moveTo(this.pos.x, this.pos.y + mp.y);
+                    break;
+                case 2:
+                    this.moveTo(this.pos.x, this.pos.y + mp.y);
+                    this.width = this.oldWidth + difX;
+                    this.height = this.height - difY;
+                    break;
+                case 3:
+                    this.width = this.oldWidth + difX;
+                    break;
+                case 4:
+                    this.width = this.oldWidth + difX;
+                    this.height = this.oldHeight + difY;
+                    break;
+                case 5:
+                    this.height = this.oldHeight + difY;
+                    break;
+                case 6:
+                    this.moveTo(this.pos.x + mp.x, this.pos.y);
+                    this.width = this.width - difX;
+                    this.height = this.oldHeight + difY;
+                    break;
+                case 7:
+                    this.moveTo(this.pos.x + mp.x, this.pos.y);
+                    this.width = this.width - difX;
+                    break;
+                default:
+                    break;
+            }
+            this.resize(
+                new SSize(this.oldWidth, this.oldHeight),
+                new SSize(this.width, this.height)
+            );
+            this.calExtreme();
+        } else {
+            let flag = true;
+            for (let v of this.itemList) {
+                if (!v.moveable) {
+                    flag = false;
+                }
+            }
+            if (flag) {
+                super.onMouseMove(event);
+                if (this._isMoving) {
+                    const mp = this.toParentChange(
+                        event.x - this._mouseDownPos.x,
+                        event.y - this._mouseDownPos.y
+                    );
+                    this.itemList.forEach(t => {
+                        t.moveTo(t.pos.x + mp.x, t.pos.y + mp.y);
+                    });
+                }
+            }
+        }
+        return true;
+    } // Function onMouseMove()
+
+    /**
+     * 鼠标按下事件
+     *
+     * @param   event   保存事件参数
+     * @return  boolean
+     */
+    onMouseUp(event: SMouseEvent): boolean {
+        this.curIndex = -1;
+        super.onMouseUp(event);
+        return true;
+    } // Function onMouseUp()
+
+    /**
+     * Item绘制操作
+     *
+     * @param   painter       painter对象
+     */
+    onDraw(painter: SPainter): void {
+        // 缓存场景长度
+        this.sceneDis = painter.toPx(this.dis);
+        painter.pen.lineWidth = painter.toPx(1);
+        painter.pen.color = SColor.White;
+        painter.brush.color = SColor.Transparent;
+        painter.drawRect(0, 0, this.width, this.height);
+
+        painter.pen.color = SColor.Black;
+        painter.pen.lineDash = [painter.toPx(3), painter.toPx(3)];
+        painter.drawRect(0, 0, this.width, this.height);
+
+        const r = painter.toPx(this.radius);
+        painter.pen.lineDash = [];
+        this.pointList.forEach((t, i) => {
+            if (i == this.curIndex) {
+                painter.brush.color = new SColor("#409eff");
+            } else {
+                painter.brush.color = SColor.White;
+            }
+            painter.drawCircle(t.x, t.y, r);
+        });
+    } // Function onDraw()
 } // Class SGraphSelectContainer

+ 109 - 0
persagy-web-graph/src/utils/SMathUtil.ts

@@ -0,0 +1,109 @@
+import { SLine, SPoint, SRect } from "@persagy-web/draw";
+
+export class SMathUtil {
+    /**
+     * 计算点到点距离
+     *
+     * @return  距离
+     * @param x1
+     * @param y1
+     * @param x2
+     * @param y2
+     */
+    static pointDistance(
+        x1: number,
+        y1: number,
+        x2: number,
+        y2: number
+    ): number {
+        return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
+    } // Function pointDistance()
+
+    /**
+     * 计算矩形是否有交集(外包矩形算法)
+     *
+     * @param   rect1   矩形1
+     * @param   rect2   矩形2
+     * @return  boolean
+     * */
+    static rectIntersection(rect1: SRect, rect2: SRect): boolean {
+        let minX = rect1.x < rect2.x ? rect1.x : rect2.x;
+        let minY = rect1.y < rect2.y ? rect1.y : rect2.y;
+        let maxX = rect1.right > rect2.right ? rect1.right : rect2.right;
+        let maxY = rect1.bottom > rect2.bottom ? rect2.bottom : rect2.bottom;
+        return (
+            rect1.width + rect2.width > maxX - minX &&
+            rect1.height + rect2.height > maxY - minY
+        );
+    } // Function rectIntersection()
+
+    /**
+     * 计算线段交点
+     *
+     * @param   line1   线段1
+     * @param   line2   线段2
+     * @return  SPoint  交点 null 平行但不重合 'repeat' 重合
+     */
+    static lineIntersection(
+        line1: SLine,
+        line2: SLine
+    ): SPoint | null | string {
+        let k1 = line1.dy / line1.dx;
+        let b1 = line1.y1 - k1 * line1.x1;
+        let k2 = line2.dy / line2.dx;
+        let b2 = line2.y1 - k2 * line2.x1;
+        if (k1 == k2) {
+            if (b1 == b2) {
+                return "repeat";
+            }
+            return null;
+        }
+        let intersectionX = (b2 - b1) / (k1 - k2);
+        let intersectionY = k1 * intersectionX + b1;
+        let minX = Math.min(line1.x1, line1.x2);
+        let maxX = Math.min(line2.x1, line2.x2);
+        if (intersectionX >= minX && intersectionX <= maxX) {
+            return new SPoint(intersectionX, intersectionY);
+        }
+        return null;
+    } // Function lineIntersection()
+
+    /**
+     * 转化数据格式为[][]
+     *
+     * @param  SP  SPoint[]
+     * @return Arr number[][]
+     */
+    static transferToArray(SP: SPoint[]): number[][] {
+        return SP.map((t): number[] => {
+            return [t.x, t.y];
+        });
+    } // Function transferToArray()
+
+    /**
+     * 转化数据格式为SPoint[]
+     *
+     * @param   arr number[][]
+     * @return  SP  SPoint[]
+     */
+    static transferToSPoint(arr: number[][]): SPoint[] {
+        return arr.map(
+            (t): SPoint => {
+                return new SPoint(t[0], t[1]);
+            }
+        );
+    } // Function transferToSPoint()
+
+    /**
+     * 计算轮廓线面积
+     * */
+    static calculateArea(arr: SPoint[]): number {
+        let sum = 0;
+        let n = arr.length;
+        arr[n] = arr[0];
+        for (let i = 1; i <= n; i++) {
+            sum += arr[i].x * arr[i - 1].y - arr[i - 1].x * arr[i].y;
+        }
+        return sum / 2;
+    } // Function calculateArea()
+} // Class SMathUtil