Bladeren bron

fix: 添加钢笔绘画

bill 1 jaar geleden
bovenliggende
commit
210da3035a
37 gewijzigde bestanden met toevoegingen van 2000 en 337 verwijderingen
  1. 3 2
      package.json
  2. 6 0
      pnpm-lock.yaml
  3. 1 1
      src/board/env.ts
  4. 3 0
      src/board/index.ts
  5. 74 25
      src/board/packages/container.ts
  6. 123 53
      src/board/packages/entity.ts
  7. 40 9
      src/board/packages/whole-line/editable/edit-whole-line.ts
  8. 264 0
      src/board/packages/whole-line/editable/pen-whole-line.ts
  9. 1 2
      src/board/packages/whole-line/helper/whole-line-line-helper.ts
  10. 1 2
      src/board/packages/whole-line/helper/whole-line-point-helper.ts
  11. 2 2
      src/board/packages/whole-line/index.ts
  12. 3 6
      src/board/packages/whole-line/service/getWholeLinePointMerge.ts
  13. 3 0
      src/board/packages/whole-line/service/index.ts
  14. 103 23
      src/board/packages/whole-line/service/whole-line-base.ts
  15. 262 0
      src/board/packages/whole-line/service/whole-line-edit.ts
  16. 220 0
      src/board/packages/whole-line/service/whole-line-helper.ts
  17. 112 0
      src/board/packages/whole-line/service/whole-line-mouse.ts
  18. 0 27
      src/board/packages/whole-line/shapes.ts
  19. 130 29
      src/board/packages/whole-line/style.ts
  20. 42 15
      src/board/packages/whole-line/view/whole-line-line.ts
  21. 37 14
      src/board/packages/whole-line/view/whole-line-point.ts
  22. 72 1
      src/board/packages/whole-line/view/whole-line-polygon.ts
  23. 86 35
      src/board/packages/whole-line/view/whole-line.ts
  24. 167 0
      src/board/plugins/bound-plugin.ts
  25. 1 0
      src/board/plugins/index.ts
  26. 25 7
      src/board/register.ts
  27. BIN
      src/board/shared/cursor/pic_pen.ico
  28. BIN
      src/board/shared/cursor/pic_pen_a.ico
  29. BIN
      src/board/shared/cursor/pic_pen_r.ico
  30. 12 3
      src/board/shared/entity-utils.ts
  31. 6 0
      src/board/shared/index.ts
  32. 3 5
      src/board/shared/math.ts
  33. 40 0
      src/board/shared/shape-helper.ts
  34. 124 66
      src/board/shared/shape-mose.ts
  35. 13 3
      src/board/shared/util.ts
  36. 20 6
      src/board/type.d.ts
  37. 1 1
      src/components/query-board/index.vue

+ 3 - 2
package.json

@@ -1,8 +1,9 @@
 {
   "name": "drawing-board",
   "private": true,
-  "version": "0.0.0",
+  "version": "0.0.2",
   "type": "module",
+  "main": "./src/board/index.ts",
   "scripts": {
     "dev": "vite",
     "build": "vue-tsc && vite build",
@@ -16,8 +17,8 @@
     "vue": "^3.4.21"
   },
   "devDependencies": {
-    "@vitejs/plugin-vue": "^5.0.4",
     "@types/three": "^0.164.0",
+    "@vitejs/plugin-vue": "^5.0.4",
     "typescript": "^5.2.2",
     "vite": "^5.2.0",
     "vue-tsc": "^2.0.6"

+ 6 - 0
pnpm-lock.yaml

@@ -5,6 +5,7 @@ specifiers:
   '@vitejs/plugin-vue': ^5.0.4
   element-plus: ^2.7.3
   konva: ^9.3.6
+  mitt: ^3.0.1
   sass: ^1.77.1
   three: ^0.164.1
   typescript: ^5.2.2
@@ -15,6 +16,7 @@ specifiers:
 dependencies:
   element-plus: 2.7.3_vue@3.4.27
   konva: 9.3.6
+  mitt: 3.0.1
   sass: 1.77.1
   three: 0.164.1
   vue: 3.4.27_typescript@5.4.5
@@ -822,6 +824,10 @@ packages:
       brace-expansion: 2.0.1
     dev: true
 
+  /mitt/3.0.1:
+    resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+    dev: false
+
   /muggle-string/0.4.1:
     resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
     dev: true

+ 1 - 1
src/board/env.ts

@@ -1 +1 @@
-export const DEV = true;
+export const DEV = false;

+ 3 - 0
src/board/index.ts

@@ -1,2 +1,5 @@
 export * from "./packages";
 export * from "./register";
+export * from "./shared";
+export * from "./plugins";
+export * from "./type.d";

+ 74 - 25
src/board/packages/container.ts

@@ -7,29 +7,32 @@ import {
   IncEntitysFactory,
   incEntitysFactoryGenerate,
 } from "../shared/entity-utils";
+import { Shape } from "konva/lib/Shape";
+import { Group } from "konva/lib/Group";
+import { getAbsoluteTransform } from "../shared";
 
 export type ContainerProps<
-  T extends string,
-  R extends Attrib,
-  S extends ShapeType,
-  C extends EntityType<R, S>
+  T extends string = string,
+  R extends Attrib = Attrib,
+  S extends ShapeType = ShapeType,
+  C extends EntityType<R, S> = EntityType<R, S>
 > = {
   types: {
     [key in T]: C;
   };
-  data?: { [key in T]?: R[] };
+  data?: { [key in T]?: R | R[] };
   dom: HTMLDivElement;
-} & Omit<EntityProps<Attrib & { data: { [key in T]?: R[] } }>, "attrib">;
+} & Omit<EntityProps<Attrib & { data: { [key in T]?: R | R[] } }>, "attrib">;
 
 export class Container<
-  T extends string,
-  R extends Attrib,
-  S extends ShapeType,
-  C extends EntityType<R, S>
-> extends Entity<Attrib & { data: { [key in T]?: R[] } }, Layer> {
+  T extends string = string,
+  R extends Attrib = Attrib,
+  S extends ShapeType = ShapeType,
+  C extends EntityType<R, S> = EntityType<R, S>
+> extends Entity<Attrib & { data: { [key in T]?: R | R[] } }, Layer> {
+  tempLayer: Layer;
   config: ContainerProps<T, R, S, C>;
   entrys = {} as { [key in T]: InstanceType<C>[] };
-  tempLayer: Layer;
   stage: Stage;
   incFactorys = {} as {
     [key in T]: IncEntitysFactory<R, InstanceType<C>>;
@@ -50,27 +53,30 @@ export class Container<
       attrib: { id: "0", data: config.data },
       ...config,
     });
+
     this.config = config;
+    this.container = this;
+    const { dom } = this.config;
+    this.stage = new Stage({
+      container: dom,
+      width: dom.offsetWidth,
+      height: dom.offsetHeight,
+    });
+    // this.tempLayer = new Layer();
+    // this.stage.add(this.tempLayer);
   }
 
   initShape() {
     const viewLayer = new Layer();
-    this.stage.add(viewLayer);
     return viewLayer;
   }
 
   init() {
-    const { dom } = this.config;
-    this.stage = new Stage({
-      container: dom,
-      width: dom.offsetWidth,
-      height: dom.offsetHeight,
-    });
-    this.tempLayer = new Layer();
-    this.stage.add(this.tempLayer);
-
     this.initIncFactory();
     super.init();
+    this.mount(this.stage);
+    this.mounted();
+    // this.tempLayer.zIndex(1);
   }
 
   initIncFactory() {
@@ -97,14 +103,57 @@ export class Container<
   initReactive() {
     return watch(
       () => {
-        const result = {} as { [key in T]: string[] };
+        const result = {} as { [key in T]: string | string[] };
         for (const key in this.attrib.data) {
-          result[key] = this.attrib.data[key].map(({ id }) => id);
+          const child = this.attrib.data[key];
+          result[key] = Array.isArray(child)
+            ? child.map(({ id }) => id)
+            : child.id;
         }
         return result;
       },
       this.diffRedraw.bind(this),
-      { immediate: true }
+      { immediate: true, flush: "sync" }
     );
   }
+
+  // 临时绘制
+  tempDraw(...shapes: Entity<Attrib, Group | Shape>[]) {
+    const tels = shapes.map((shape) => shape.teleport);
+    shapes.forEach((shape) => {
+      shape.mount(this.tempLayer);
+    });
+    return () => {
+      shapes.forEach((shape, ndx) => {
+        shape.mount(tels[ndx]);
+      });
+    };
+  }
+
+  getPixelFromStage(pos: number[]) {
+    const tf = getAbsoluteTransform(this.stage, true);
+    const { x, y } = tf.point({ x: pos[0], y: pos[1] });
+    return [x, y];
+  }
+
+  getRealFromStage(pos: number[]) {
+    const tf = getAbsoluteTransform(this.stage, true).invert();
+    const { x, y } = tf.point({ x: pos[0], y: pos[1] });
+    return [x, y];
+  }
+
+  private prevCursor: string;
+  setCursor(ico: string | null) {
+    const defs = ["move", "inherit", "pointer"];
+    ico = defs.includes(ico) ? ico : ico ? `url("${ico}"), auto` : "inherit";
+    this.config.dom.style.cursor = ico;
+    this.prevCursor = ico;
+
+    return () => {
+      if (this.prevCursor === ico) {
+        this.config.dom.style.cursor = "inherit";
+        this.prevCursor = "inherit";
+      }
+    };
+  }
 }

+ 123 - 53
src/board/packages/entity.ts

@@ -1,7 +1,6 @@
 import konva from "konva";
 import { Attrib, ShapeStyles, ShapeType } from "../type";
 import { DEV } from "../env";
-import { Shape } from "konva/lib/Shape";
 import { Group } from "konva/lib/Group";
 import { Layer } from "konva/lib/Layer";
 import { Stage } from "konva/lib/Stage";
@@ -12,6 +11,7 @@ import {
   openShapeDrag,
   openShapeMouseStyles,
 } from "../shared/shape-mose";
+import { Container } from "./container";
 
 export type EntityBaseProps = {
   reactive?: boolean;
@@ -22,6 +22,7 @@ export type EntityProps<T extends Attrib> = EntityBaseProps & {
   name?: string;
   attrib: T;
   zIndex?: number;
+  teleport?: ShapeType;
 };
 
 type ParentShapeType<T extends ShapeType> = T extends Layer
@@ -30,33 +31,49 @@ type ParentShapeType<T extends ShapeType> = T extends Layer
   ? null
   : Group;
 
+export type EntityTypeEvent<T extends string, R> = {
+  [key in T]: R;
+};
+
 export type EntityType<
   T extends Attrib,
   S extends ShapeType,
   P extends EntityProps<T> = EntityProps<T>,
-  K extends Entity<T, S> = Entity<T, S>
-> = new (props: P) => K;
+  K extends Entity<T, S> = Entity<T, S>,
+  EK extends string = string,
+  ER = any
+> = (new (props: P) => K) & { EventHandler?: { [key in EK]: ER } };
 
-export abstract class Entity<T extends Attrib, S extends ShapeType> {
+export abstract class Entity<
+  T extends Attrib = Attrib,
+  S extends ShapeType = ShapeType
+> {
   props: EntityProps<T>;
+  container: Container<
+    string,
+    Attrib,
+    ShapeType,
+    EntityType<Attrib, ShapeType>
+  >;
   attrib: T;
   shape: S;
   name: string;
 
   private zIndex: number;
-  private teleport: ParentShapeType<S>;
+  teleport: ParentShapeType<S>;
 
   children: Entity<Attrib, ShapeType>[] = [];
   parent: Entity<Attrib, ShapeType>;
 
   constructor(props: EntityProps<T>) {
     this.name = props.name;
+    this.teleport = props.teleport as any;
     this.props = props;
     this.attrib = props.reactive ? (reactive(props.attrib) as T) : props.attrib;
     this.zIndex = props.zIndex || 0;
 
     if (DEV) {
-      console.log(this.name + " create");
+      // console.log(this.name + " create");
     }
   }
 
@@ -66,6 +83,10 @@ export abstract class Entity<T extends Attrib, S extends ShapeType> {
   }
 
   abstract diffRedraw(): void;
+  redraw() {
+    this.diffRedraw();
+    this.children.forEach((child) => child.redraw());
+  }
 
   setAttrib(newAttrib: Omit<T, "id">) {
     this.attrib = depPartialUpdate(
@@ -78,31 +99,49 @@ export abstract class Entity<T extends Attrib, S extends ShapeType> {
   private destoryReactive: () => void;
   protected initReactive() {
     this.destoryReactive && this.destoryReactive();
-    return watchEffect(() => {
-      this.diffRedraw();
-    });
+    return watchEffect(
+      () => {
+        this.diffRedraw();
+      },
+      { flush: "post" }
+    );
   }
 
   init() {
     this.shape = this.initShape();
     this.shape.id(this.name);
-    this.setAttrib(this.props.attrib);
-    if (this.props.reactive) {
-      this.destoryReactive = this.initReactive();
-    }
   }
 
-  setTeleport(tel?: ParentShapeType<S>) {
+  mount(tel?: ParentShapeType<S>) {
     if (this.shape instanceof Stage) {
-      throw "stage 为顶级容器无法挂载到";
-    } else if (this.shape instanceof Layer && !(tel instanceof Stage)) {
+      throw "stage 为顶级容器无法挂载";
+    } else if (tel && this.shape instanceof Layer && !(tel instanceof Stage)) {
+      console.log(this.name, tel);
       throw "layer 只能挂载到 Stage";
     }
+
+    if (this.teleport && this.teleport === tel) {
+      return;
+    }
+
+    this.children.sort((a, b) => a.zIndex - b.zIndex);
+
     const parentShape = (tel || this.parent.shape) as Layer;
-    this.shape.remove();
     parentShape.add(this.shape);
+    this.teleport = parentShape as unknown as ParentShapeType<S>;
     this.setZIndex(this.zIndex);
-    this.teleport = tel;
+
+    this.setAttrib(this.props.attrib);
+    if (this.props.reactive) {
+      this.destoryReactive = this.initReactive();
+    }
+  }
+
+  isMounted = false;
+  mounted() {
+    this.diffRedraw();
+    this.children.forEach((child) => child.mounted());
+    this.isMounted = true;
   }
 
   setParent(parent: Entity<any, any> | null) {
@@ -110,53 +149,63 @@ export abstract class Entity<T extends Attrib, S extends ShapeType> {
       const ndx = this.parent.children.indexOf(this as any);
       ~ndx && this.parent.children.splice(ndx, 1);
     }
-    if (this.parent !== parent) {
-      this.parent = parent;
-      if (parent) {
-        this.parent.children.push(this as any);
-        this.setTeleport();
-      }
+
+    this.parent = parent;
+    if (parent) {
+      this.parent.children.push(this as any);
     }
   }
 
   private getLevelNdx(parent = this.parent) {
-    const level = parent.children;
-    for (let i = level.length - 1; i >= 0; i--) {
-      if (level[i] !== this && level[i].zIndex <= this.zIndex) {
-        return i + 1;
+    if (parent) {
+      const level = parent.children;
+      for (let i = level.length - 1; i >= 0; i--) {
+        if (level[i] !== this && level[i].zIndex <= this.zIndex) {
+          return i;
+        }
       }
+      return -1;
     }
-    return 0;
+    return null;
   }
 
   setZIndex(index: number) {
     this.zIndex = index;
     const packNdx = this.getLevelNdx();
+    if (packNdx === null) return;
+
     const packChild = this.parent.children;
-    let repPack: any = this;
-    for (let i = packNdx; i < packChild.length; i++) {
-      const temp = packChild[i];
-      packChild[i] = repPack;
-      repPack = temp;
+    const oldNdx = packChild.indexOf(this);
+
+    if (oldNdx !== packNdx + 1) {
+      let rep: any = this;
+      for (let i = packNdx + 1; i < packChild.length; i++) {
+        const temp = packChild[i];
+        packChild[i] = rep;
+        rep = temp;
+      }
     }
 
-    const parentShape = this.teleport || (this.parent.shape as any);
+    const parentShape = this.teleport as any;
     const levelShapes = parentShape.children;
     const shapeNdx =
-      packNdx === 0
+      packNdx === -1
         ? 0
-        : levelShapes.indexOf(packChild[packNdx - 1].getShape()) + 1;
-
-    if (shapeNdx !== 0) {
-      let repShape: any = this.shape;
-      for (let i = shapeNdx; i < levelShapes.length; i++) {
-        const temp = levelShapes[i];
-        parentShape.add(repShape);
-        repShape.zIndex(i);
-        repShape = temp;
+        : levelShapes.indexOf(packChild[packNdx].getShape()) + 1;
+    const oldShapeNdx = levelShapes.indexOf(this.shape);
+
+    if (oldShapeNdx !== shapeNdx) {
+      if (shapeNdx !== 0) {
+        let repShape: any = this.shape;
+        for (let i = shapeNdx; i < levelShapes.length; i++) {
+          const temp = levelShapes[i];
+          parentShape.add(repShape);
+          repShape.zIndex(i);
+          repShape = temp;
+        }
+      } else {
+        this.shape.zIndex(0);
       }
-    } else {
-      this.shape.zIndex(0);
     }
   }
 
@@ -173,10 +222,14 @@ export abstract class Entity<T extends Attrib, S extends ShapeType> {
   private activeDestory: () => void;
   enableActive(activeCallback: (isActive: boolean) => void) {
     this.disableActive();
-    this.activeDestory = openShapeMouseStyles(this.shape, {
-      active: () => activeCallback(true),
-      common: () => activeCallback(false),
-    });
+    this.activeDestory = openShapeMouseStyles(
+      this.shape,
+      {
+        active: () => activeCallback(true),
+        common: () => activeCallback(false),
+      },
+      "mouse-active"
+    );
   }
   disableActive() {
     this.activeDestory && this.activeDestory();
@@ -184,9 +237,13 @@ export abstract class Entity<T extends Attrib, S extends ShapeType> {
   }
 
   private mouseActDestory: () => void;
-  enableMouseAct(styles: ShapeStyles<Shape>) {
+  enableMouseAct(styles: ShapeStyles) {
     this.disableMouseAct();
-    this.mouseActDestory = openShapeMouseStyles(this.shape, styles as any);
+    this.mouseActDestory = openShapeMouseStyles(
+      this.shape,
+      styles,
+      "act-mouse"
+    );
   }
   disableMouseAct() {
     this.mouseActDestory && this.mouseActDestory();
@@ -209,4 +266,17 @@ export abstract class Entity<T extends Attrib, S extends ShapeType> {
       this.shape.destroy();
     }
   }
+
+  find(name: string): Entity {
+    for (const child of this.children) {
+      if (child.name === name) {
+        return child;
+      }
+      const childQueryed = child.find(name);
+      if (childQueryed) {
+        return childQueryed;
+      }
+    }
+    return null;
+  }
 }

+ 40 - 9
src/board/packages/whole-line/editable/edit-whole-line.ts

@@ -1,5 +1,6 @@
 import {
   WholeLine,
+  WholeLineAttrib,
   WholeLineLine,
   WholeLineLineAttrib,
   WholeLinePoint,
@@ -11,8 +12,14 @@ import {
   spliceWholeLineLineByPoint,
 } from "../service";
 import { EntityDragHandlers, openEntityDrag } from "../../../shared/shape-mose";
+import { ShapeType } from "../../../type";
 
-export class EditWholeLine extends WholeLine {
+export class EditWholeLine<
+  W extends WholeLineAttrib = WholeLineAttrib,
+  PS extends ShapeType = ShapeType,
+  LS extends ShapeType = ShapeType,
+  PYS extends ShapeType = ShapeType
+> extends WholeLine<W, PS, LS, PYS> {
   points: WholeLinePoint[];
   lines: WholeLineLine[];
 
@@ -27,22 +34,41 @@ export class EditWholeLine extends WholeLine {
     const inc = super.diffRedraw();
     inc.lineEntityInc.adds.forEach((line) => {
       openEntityDrag(line, this.lineDragHandler);
+      line.enableMouseAct({ ...line.actShape, active: undefined });
     });
     inc.pointEntityInc.adds.forEach((point) => {
       openEntityDrag(point, this.pointDragHandler);
+      point.enableMouseAct({ ...point.actShape, active: undefined });
     });
     return inc;
   }
 
+  getChildByPointsAttrib(pointsAttrib: WholeLinePointAttrib[]) {
+    const names: string[] = [];
+
+    for (let i = 0; i < pointsAttrib.length; i++) {
+      const name = WholeLinePoint.namespace + pointsAttrib[i].id;
+      names.includes(name) || names.push(name);
+
+      this.attrib.lines
+        .filter(({ pointIds }) => pointIds.includes(pointsAttrib[i].id))
+        .forEach(({ id }) => {
+          const name = WholeLineLine.namespace + id;
+          names.includes(name) || names.push(name);
+        });
+    }
+    return this.children.filter((line) => names.includes(line.name));
+  }
+
   init() {
     this.lineDragHandler = {
       readyHandler: (lineAttrib) => {
+        this.container.tempDraw();
         const pointAttribs = getWholeLineLine(this.attrib, lineAttrib.id);
         return pointAttribs.map(({ x, y }) => [x, y]);
       },
-      moveHandler: (lineAttrib, move, initPositions) => {
+      moveHandler: (lineAttrib, move, initPositions, ev) => {
         const points = getWholeLineLine(this.attrib, lineAttrib.id);
-
         const currentPositions = initPositions.map((pos) => [
           pos[0] + move[0],
           pos[1] + move[1],
@@ -51,23 +77,28 @@ export class EditWholeLine extends WholeLine {
         this.pointDragHandler.moveHandler(
           points[0],
           currentPositions[0],
-          initPositions[0]
+          initPositions[0],
+          ev
         );
         this.pointDragHandler.moveHandler(
           points[1],
           currentPositions[1],
-          initPositions[1]
+          initPositions[1],
+          ev
         );
       },
-      endHandler: (lineAttrib, initPositions) => {
+      endHandler: (lineAttrib, initPositions, ev) => {
         const points = getWholeLineLine(this.attrib, lineAttrib.id);
-        this.pointDragHandler.endHandler(points[0], initPositions[0]);
-        this.pointDragHandler.endHandler(points[1], initPositions[1]);
+        this.pointDragHandler.endHandler(points[0], initPositions[0], ev);
+        this.pointDragHandler.endHandler(points[1], initPositions[1], ev);
       },
     };
 
     this.pointDragHandler = {
-      readyHandler: ({ x, y }) => [x, y],
+      readyHandler: (attrib) => {
+        // this.container.tempDraw(...this.getChildByPointsAttrib([attrib]));
+        return [attrib.x, attrib.y];
+      },
       moveHandler: (pointAttrib, move) => {
         pointAttrib.x = move[0];
         pointAttrib.y = move[1];

+ 264 - 0
src/board/packages/whole-line/editable/pen-whole-line.ts

@@ -0,0 +1,264 @@
+import { Ref, computed, ref, watchEffect } from "vue";
+import {
+  WLL,
+  WLP,
+  WLPY,
+  WholeLine,
+  WholeLineAttrib,
+  WholeLineLine,
+  WholeLineLineAttrib,
+  WholeLinePoint,
+  WholeLinePointAttrib,
+  WholeLinePolygon,
+  WholeLinePolygonAttrib,
+} from "../view";
+import {
+  WholeLineHelperInfoAL,
+  penWholeLinePoygonsEditWithHelperMouse,
+  wholeLineAnalysisHelperInfo,
+} from "../service/whole-line-helper";
+import {
+  lineShapeFactory,
+  pointShapeFactory,
+  polygonShapeFactory,
+} from "../style";
+import { getWholeLinePolygonLinesRaw } from "../service";
+import {
+  getRealAbsoluteSize,
+  incEntitysFactoryGenerate,
+  openEntityDrag,
+} from "../../../shared";
+import { CustomizeShape, ShapeType } from "../../../type";
+import { KonvaEventObject } from "konva/lib/Node";
+import addMouseIco from "../../../shared/cursor/pic_pen_a.ico";
+import delMouseIco from "../../../shared/cursor/pic_pen_r.ico";
+
+export const penLineActShapeFactory = <T extends WholeLineLineAttrib>(
+  attrib: T,
+  tree: any
+): CustomizeShape<number[]> => {
+  const polygons = tree.parent as PenEditWholeLine;
+  const wll = lineShapeFactory();
+
+  const isActive = () => {
+    if (polygons.editPolygonId.value) {
+      const lines = getWholeLinePolygonLinesRaw(
+        polygons.attrib,
+        polygons.editPolygonId.value
+      );
+      if (lines.some(({ id }) => id === attrib.id)) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  const common = () => {
+    const readyDel = polygons.helperInfo?.value?.delLines?.includes(attrib.id);
+    if (readyDel) {
+      wll.shape.stroke("rgba(0, 0, 0, 0)");
+    } else if (isActive()) {
+      wll.active();
+    } else {
+      wll.common();
+    }
+  };
+  return {
+    shape: wll.shape,
+    setData(data: number[]) {
+      wll.setData(data);
+      common();
+    },
+    common,
+  };
+};
+
+const penPolygonActShapeFactoryGen = (autoClose: boolean) => {
+  return (attrib: WholeLinePolygonAttrib, tree: any) => {
+    const polygons = tree.parent as PenEditWholeLine;
+    const isActive = () => polygons.editPolygonId.value === attrib.id;
+    const wlp = polygonShapeFactory({ autoClose });
+
+    const common = (ev?: KonvaEventObject<any>) => {
+      if (isActive()) {
+        wlp.active(ev);
+      } else {
+        wlp.common(ev);
+      }
+    };
+
+    return {
+      shape: wlp.shape,
+      setData(data: number[]) {
+        common();
+        wlp.setData(data);
+        const needUpdate = polygons.helperInfo?.value?.cloneLines
+          ? Object.keys(polygons.helperInfo?.value?.cloneLines).includes(
+              attrib.id
+            )
+          : false;
+
+        wlp.shape.children[1].visible(!needUpdate);
+      },
+      common,
+    };
+  };
+};
+
+export const closePenPolygonActShapeFactory =
+  penPolygonActShapeFactoryGen(true);
+export const penPolygonActShapeFactory = penPolygonActShapeFactoryGen(false);
+
+export type PenEnterEditProps<P extends WholeLinePointAttrib> = {
+  polygonId?: string;
+  canOper?: (
+    tree: WholeLineLine | WholeLinePoint,
+    operShape: ShapeType
+  ) => boolean;
+  pointAttribFactory?: (pos: number[]) => Omit<P, "id">;
+  canDelPoint?: (p: P, evt: KonvaEventObject<any>) => boolean;
+  quitHandler?: () => void;
+  quotePoint?: boolean;
+};
+
+// 钢笔模式 只允许一个多边形一个多边形的编辑
+export class PenEditWholeLine<
+  W extends WholeLineAttrib = WholeLineAttrib
+> extends WholeLine<W> {
+  editPolygonId = ref<string>();
+  helperInfo?: Ref<WholeLineHelperInfoAL>;
+  autoClose = true;
+
+  diffRedraw() {
+    const inc = super.diffRedraw();
+    inc.pointEntityInc.adds.forEach((point) => {
+      openEntityDrag(point, {
+        readyHandler: (attrib) => {
+          return [attrib.x, attrib.y];
+        },
+        moveHandler: (pointAttrib, move) => {
+          if (this.editPolygonId.value) {
+            pointAttrib.x = move[0];
+            pointAttrib.y = move[1];
+          }
+        },
+      });
+      point.enableMouseAct(point.actShape);
+    });
+
+    inc.lineEntityInc.adds.forEach((line) => {
+      line.enableMouseAct(line.actShape);
+    });
+
+    inc.polygonEntityInc.adds.forEach((py) => {
+      py.enableMouseAct(py.actShape);
+    });
+    return inc;
+  }
+
+  initIncFactory() {
+    this.incLinesFactory = incEntitysFactoryGenerate(
+      WholeLineLine<WLL<W>>,
+      this,
+      (line) => {
+        line.actShapeFactory = penLineActShapeFactory<WLL<W>>;
+        line.setConfig(this.attrib);
+      }
+    );
+
+    this.incPointsFactory = incEntitysFactoryGenerate(
+      WholeLinePoint<WLP<W>>,
+      this
+    );
+
+    this.incPolygonFactory = incEntitysFactoryGenerate(
+      WholeLinePolygon<WLPY<W>>,
+      this,
+      (py) => {
+        py.actShapeFactory = this.autoClose
+          ? closePenPolygonActShapeFactory
+          : penPolygonActShapeFactory;
+        py.setConfig(this.attrib);
+      }
+    );
+  }
+
+  private _leaveEditMode: (() => void) | void = void 0;
+  enterEditMode(props: PenEnterEditProps<WLP<W>>) {
+    const { helperInfo: helperInfoRaw, destory: mouseDestory } =
+      penWholeLinePoygonsEditWithHelperMouse({
+        ...props,
+        tree: this.container,
+        config: this.attrib,
+        changePolygon: (pid: string) => {
+          console.error("changePolygon", pid);
+          this.editPolygonId.value = pid;
+        },
+        quitHandler: () => {
+          stopWatchHelper();
+          props.quitHandler && props.quitHandler();
+        },
+      });
+
+    this.helperInfo = computed(
+      () =>
+        helperInfoRaw.value &&
+        wholeLineAnalysisHelperInfo(helperInfoRaw.value, this.autoClose)
+    );
+
+    const tempPoint = pointShapeFactory();
+    tempPoint.shape.listening(false);
+
+    const tempLine = lineShapeFactory();
+    tempLine.active();
+    tempLine.shape.dash([5, 5]);
+    tempLine.shape.listening(false);
+
+    const stopWatchHelper = watchEffect((onCleanup) => {
+      if (!this.helperInfo.value) return;
+
+      const { addLines, cloneLines, addPoints } = this.helperInfo.value;
+
+      const lineShapes = [...addLines, ...Object.values(cloneLines)].map(
+        (line) => {
+          tempLine.setData(line);
+          return tempLine.shape.clone();
+        }
+      );
+
+      const [scale] = getRealAbsoluteSize(this.shape, [1, 1]);
+      const pointShapes = addPoints.map((point) => {
+        tempPoint.setData(point);
+        tempPoint.shape.scale({ x: scale, y: scale });
+        return tempPoint.shape.clone();
+      });
+
+      this.shape.add(...lineShapes, ...pointShapes);
+
+      let clearCursor;
+      if (this.helperInfo.value.delPoints.length) {
+        clearCursor = this.container.setCursor(delMouseIco);
+      } else if (addPoints.length) {
+        clearCursor = this.container.setCursor(addMouseIco);
+      }
+
+      onCleanup(() => {
+        [...lineShapes, ...pointShapes].forEach((shape) => {
+          shape.remove();
+        });
+        clearCursor && clearCursor();
+      });
+    });
+
+    this._leaveEditMode = mouseDestory;
+  }
+
+  leaveEditMode() {
+    this._leaveEditMode && this._leaveEditMode();
+  }
+
+  destory(): void {
+    this.leaveEditMode();
+    super.destory();
+  }
+}

+ 1 - 2
src/board/packages/whole-line/helper/whole-line-line-helper.ts

@@ -6,7 +6,6 @@ import {
 import { getLineDireAngle } from "../../../shared/math";
 import { MathUtils } from "three";
 import { WholeLineAttrib } from "../view/whole-line";
-import { lineHelperZIndex } from "../style";
 import { Entity } from "../../entity";
 import { Group } from "konva/lib/Group";
 import { Label } from "konva/lib/shapes/Label";
@@ -18,7 +17,7 @@ export class WholeLineLineHelper extends Entity<WholeLineLineAttrib, Group> {
   private config: WholeLineAttrib;
 
   constructor(props: WholeLineLineProps) {
-    props.zIndex = props.zIndex || lineHelperZIndex;
+    props.zIndex = props.zIndex || 4;
     props.name = props.name || WholeLineLineHelper.namespace + props.attrib.id;
     super(props);
   }

+ 1 - 2
src/board/packages/whole-line/helper/whole-line-point-helper.ts

@@ -3,7 +3,6 @@ import {
   WholeLinePointAttrib,
   WholeLinePointProps,
 } from "../view/whole-line-point";
-import { pointHelperZIndex } from "../style";
 import { Entity } from "../../entity";
 import { Label } from "konva/lib/shapes/Label";
 
@@ -11,7 +10,7 @@ export class WholeLinePointHelper extends Entity<WholeLinePointAttrib, Label> {
   static namespace = "point-helper";
 
   constructor(props: WholeLinePointProps) {
-    props.zIndex = props.zIndex || pointHelperZIndex;
+    props.zIndex = props.zIndex || 5;
     props.name = props.name || WholeLinePointHelper.namespace + props.attrib.id;
     super(props);
   }

+ 2 - 2
src/board/packages/whole-line/index.ts

@@ -1,8 +1,8 @@
-export * as wholeLine from "./style";
+export * as wholeLineStyle from "./style";
 export * from "./view/";
 export * from "./service";
 export * from "./helper";
 
 export * from "./view/whole-line";
 export * from "./editable/edit-whole-line";
-export * from "./shapes";
+export * from "./editable/pen-whole-line";

+ 3 - 6
src/board/packages/whole-line/service/getWholeLinePointMerge.ts

@@ -1,4 +1,4 @@
-import { WholeLineAttrib, WholeLinePoint } from "../view/whole-line";
+import { WholeLineAttrib } from "../view/whole-line";
 import { getWholeLinePolygonLinesByPoint } from "./whole-line-base";
 
 /**
@@ -6,10 +6,7 @@ import { getWholeLinePolygonLinesByPoint } from "./whole-line-base";
  * @param config WholeLine
  * @param point 操作点
  */
-export const getWholeLinePointMerge = (
-  config: WholeLineAttrib,
-  point: WholeLinePoint
-) => {
-  getWholeLinePolygonLinesByPoint(config);
+export const getWholeLinePointMerge = (config: WholeLineAttrib) => {
+  // getWholeLinePolygonLinesByPoint(config);
   config.points;
 };

+ 3 - 0
src/board/packages/whole-line/service/index.ts

@@ -2,3 +2,6 @@ export * from "./constant";
 export * from "./whole-line-base";
 export * from "./whole-line-tear-merge";
 export * from "./whole-line-normal";
+export * from "./whole-line-mouse";
+export * from "./whole-line-edit";
+export * from "./whole-line-helper";

+ 103 - 23
src/board/packages/whole-line/service/whole-line-base.ts

@@ -11,12 +11,15 @@ export const generateWholeLineLineId = (config: WholeLineAttrib) =>
 export const generateWholeLinePointId = (config: WholeLineAttrib) =>
   (Math.max(...config.points.map(({ id }) => Number(id))) + 1).toString();
 
+export const generateWholeLinePoygonId = (config: WholeLineAttrib) =>
+  (Math.max(...config.polygons.map(({ id }) => Number(id))) + 1).toString();
+
 export const getWholeLinePoint = (config: WholeLineAttrib, id: string) =>
   config.points.find((point) => point.id === id);
 
 export const getWholeLinePoints = (config: WholeLineAttrib, ids?: string[]) => {
   if (ids) {
-    return ids.map((id) => getWholeLinePoint(config, id));
+    return ids.map((id) => getWholeLinePoint(config, id)!);
   } else {
     return config.points;
   }
@@ -28,7 +31,7 @@ export const getWholeLineLineRaw = (config: WholeLineAttrib, lineId: string) =>
 export const getWholeLineLinesRaw = (
   config: WholeLineAttrib,
   lineIds: string[]
-) => lineIds.map((lineId) => getWholeLineLineRaw(config, lineId));
+) => lineIds.map((lineId) => getWholeLineLineRaw(config, lineId)!);
 
 export const getWholeLineLine = (config: WholeLineAttrib, lineId: string) =>
   getWholeLinePoints(config, getWholeLineLineRaw(config, lineId).pointIds);
@@ -55,9 +58,14 @@ export const getWholeLinePolygonLinesRaw = (
   config: WholeLineAttrib,
   polygonId: string
 ) => {
-  return getWholeLinePolygonRaw(config, polygonId).lineIds.map((lineId) =>
-    getWholeLineLineRaw(config, lineId)
-  );
+  const polygonRaw = getWholeLinePolygonRaw(config, polygonId);
+  if (polygonRaw) {
+    return polygonRaw.lineIds.map(
+      (lineId) => getWholeLineLineRaw(config, lineId)!
+    );
+  } else {
+    return [];
+  }
 };
 
 // getWholeLineLinesByPoint
@@ -96,7 +104,9 @@ export const getWholeLinePolygonPoints = (
   config: WholeLineAttrib,
   polygonId: string
 ) => {
-  const { lineIds } = getWholeLinePolygonRaw(config, polygonId);
+  const polygonRaw = getWholeLinePolygonRaw(config, polygonId);
+  if (!polygonRaw) return [];
+  const { lineIds } = polygonRaw;
   const points: WholeLinePointAttrib[] = [];
   for (let i = 0; i < lineIds.length; i++) {
     const lineRaw = getWholeLineLineRaw(config, lineIds[i]);
@@ -128,6 +138,40 @@ export type WholeLineChange = {
   };
 };
 
+export const mergeChange = (...mchanges: WholeLineChange[]) => {
+  const change: WholeLineChange = {};
+
+  for (const mChange of mchanges) {
+    for (const cKey in mChange) {
+      if (!change[cKey]) {
+        change[cKey] = Object.fromEntries(
+          Object.entries(mChange[cKey]).map(([key, val]) => [
+            key,
+            [...(val as any)],
+          ])
+        );
+      } else {
+        for (const oKey in mChange[cKey]) {
+          if (!change[cKey][oKey]) {
+            change[cKey][oKey] = [...mChange[cKey][oKey]];
+            break;
+          }
+          const targets = mChange[cKey][oKey];
+          const origins = change[cKey][oKey];
+
+          for (const target of targets) {
+            if (origins.every(({ id }) => id !== target.id)) {
+              origins.push(target);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  return change;
+};
+
 export const getWholeLineLineNdxByPointIds = (
   config: WholeLineAttrib,
   qPointIds: string[]
@@ -170,7 +214,8 @@ export const wholeLineAddLineByPointIds = (
  */
 export const wholeLineDelLineByPointIds = (
   config: WholeLineAttrib,
-  delPointIds: string[]
+  delPointIds: string[],
+  realDelPoint = true
 ) => {
   const change: WholeLineChange = {
     lineChange: { del: [] },
@@ -189,16 +234,18 @@ export const wholeLineDelLineByPointIds = (
     change.lineChange.del.push(eLineRaw);
   }
 
-  delPointIds.forEach((delPointId) => {
-    const ndx = config.points.findIndex(({ id }) => id === delPointId);
-    if (
-      ~ndx &&
-      config.lines.every(({ pointIds }) => !pointIds.includes(delPointId))
-    ) {
-      change.pointChange.del.push(config.points[ndx]);
-      config.points.splice(ndx, 1);
-    }
-  });
+  if (realDelPoint) {
+    delPointIds.forEach((delPointId) => {
+      const ndx = config.points.findIndex(({ id }) => id === delPointId);
+      if (
+        ~ndx &&
+        config.lines.every(({ pointIds }) => !pointIds.includes(delPointId))
+      ) {
+        change.pointChange.del.push(config.points[ndx]);
+        config.points.splice(ndx, 1);
+      }
+    });
+  }
 
   return { line: eLineRaw, change };
 };
@@ -211,8 +258,10 @@ export const wholeLineDelLineByPointIds = (
  */
 export const wholeLineDelPointByPointIds = (
   config: WholeLineAttrib,
-  delPointIds: string[]
+  delPointIds: string | string[],
+  realDelPoint = true
 ) => {
+  delPointIds = Array.isArray(delPointIds) ? delPointIds : [delPointIds];
   const change: WholeLineChange = {
     lineChange: {
       del: [],
@@ -227,13 +276,15 @@ export const wholeLineDelPointByPointIds = (
     },
   };
 
+  let max = 100;
   for (let i = 0; i < config.polygons.length; i++) {
     const polygon = config.polygons[i];
     const lineIds = polygon.lineIds;
-    const lines = getWholeLineLinesRaw(config, lineIds);
+    let lineCount = lineIds.length;
     let initPolygonLineIds: string[];
 
-    for (let ndx = 0; ndx < lines.length; ndx++) {
+    for (let ndx = 0; ndx < lineCount; ndx++) {
+      const lines = getWholeLineLinesRaw(config, lineIds);
       const lineRaw = lines[ndx];
       const prev = delPointIds.includes(lineRaw.pointIds[0]);
       const last = delPointIds.includes(lineRaw.pointIds[1]);
@@ -248,6 +299,7 @@ export const wholeLineDelPointByPointIds = (
       // delp 3 delL [3, 4] updateL [2, 3] -> [2, 4]
 
       let delNdx = ndx;
+      let newId: string | null = null;
       if (last) {
         // delp 3, 4 delL [2, 3][3, 4]
         // 特殊情况
@@ -268,16 +320,31 @@ export const wholeLineDelPointByPointIds = (
           ]);
           change.lineChange.add.push(...adL.change.lineChange.add);
           lineIds[ndx] = adL.line.id;
-          delNdx = -1;
+          newId = adL.line.id;
         }
       }
+
       if (~delNdx) {
         const delPointIds = lines[ndx].pointIds;
-        lineIds.splice(ndx--, 1);
+        if (newId) {
+          lineIds.splice(ndx, 1, newId);
+        } else {
+          lineIds.splice(ndx, 1);
+          lines.splice(ndx, 1);
+          lineCount--;
+          ndx--;
+        }
 
-        const dL = wholeLineDelLineByPointIds(config, delPointIds);
+        const dL = wholeLineDelLineByPointIds(
+          config,
+          delPointIds,
+          realDelPoint
+        );
         change.lineChange.del.push(...dL.change.lineChange.del);
         change.pointChange.del.push(...dL.change.pointChange.del);
+        if (max-- < 0) {
+          break;
+        }
       }
     }
 
@@ -294,6 +361,19 @@ export const wholeLineDelPointByPointIds = (
       }
     }
   }
+
+  if (realDelPoint) {
+    // 没有被删说明点不存在引用
+    delPointIds.forEach((dId) => {
+      if (!change.pointChange.del.some(({ id }) => id === dId)) {
+        const ndx = config.points.findIndex(({ id }) => id === dId);
+        if (~ndx) {
+          change.pointChange.del.push(config.points[ndx]);
+          config.points.splice(ndx, 1);
+        }
+      }
+    });
+  }
   return change;
 };
 

+ 262 - 0
src/board/packages/whole-line/service/whole-line-edit.ts

@@ -0,0 +1,262 @@
+import {
+  WholeLineAttrib,
+  WholeLineLine,
+  WholeLinePoint,
+  WholeLinePointAttrib,
+  WholeLinePolygonAttrib,
+} from "../view";
+import { KonvaEventObject } from "konva/lib/Node";
+import { shapeParentsEq } from "../../../shared";
+import {
+  wholeLineAddPoint,
+  wholeLineFixLineAddPoint,
+  wholeLinePolygonLastAddPoint,
+} from "./whole-line-mouse";
+import { Container } from "../../container";
+import { Attrib, ShapeType } from "../../../type";
+import {
+  WholeLineChange,
+  generateWholeLinePoygonId,
+  getWholeLinePolygonPoints,
+  mergeChange,
+  wholeLineAddLineByPointIds,
+  wholeLineDelPointByPointIds,
+} from "./whole-line-base";
+
+/**
+ * 钢笔模式编辑多边形
+ * @param polygonId
+ */
+export const penWholeLinePoygonsEdit = <
+  P extends Omit<WholeLinePointAttrib, "id">
+>({
+  tree,
+  config,
+  polygonId,
+  pointAttribFactory,
+  quotePoint,
+  canOper,
+  canDelPoint,
+  changePolygon,
+  dev,
+}: {
+  tree: Container;
+  config: WholeLineAttrib<P & Attrib>;
+  polygonId?: string;
+  pointAttribFactory?: (pos: number[]) => Omit<P, "id">;
+  quotePoint?: boolean;
+  canOper?: (
+    tree: WholeLineLine | WholeLinePoint,
+    operShape: ShapeType
+  ) => boolean;
+  canDelPoint?: (point: P, evt: KonvaEventObject<any>) => boolean;
+  changePolygon?: (polygonId: string) => void;
+  dev?: boolean;
+}) => {
+  pointAttribFactory =
+    pointAttribFactory ||
+    ((pos: number[]) =>
+      ({
+        x: pos[0],
+        y: pos[1],
+      } as P));
+
+  const getPolygonAttrib = (polygonId: string) => {
+    if (!polygonId) {
+      const id = generateWholeLinePoygonId(config);
+      config.polygons.push({
+        id,
+        lineIds: [],
+      });
+
+      return config.polygons[config.polygons.length - 1];
+    } else {
+      return config.polygons.find(({ id }) => id === polygonId);
+    }
+  };
+
+  let polyginAttrib: WholeLinePolygonAttrib;
+  let newMode = false;
+  let prevId: string | null = null;
+
+  const start = (currentPolygonId: string | null) => {
+    polyginAttrib = getPolygonAttrib(currentPolygonId);
+
+    if (!polyginAttrib) {
+      throw `${currentPolygonId}的多边形不存在!`;
+    }
+    changePolygon && changePolygon(polyginAttrib.id);
+    polygonId = polyginAttrib.id;
+    newMode = !currentPolygonId;
+    prevId = null;
+  };
+
+  start(polygonId);
+
+  const removePolygon = () => {
+    const ndx = config.polygons.indexOf(polyginAttrib);
+    if (~ndx) {
+      config.polygons.splice(ndx, 1);
+    }
+  };
+
+  const continuous = (evt: KonvaEventObject<any>): WholeLineChange => {
+    let change: WholeLineChange = {
+      lineChange: {
+        del: [],
+        update: [],
+        add: [],
+      },
+      polygonChange: {
+        add: [],
+        update: [],
+        del: [],
+      },
+      pointChange: {
+        add: [],
+        del: [],
+      },
+    };
+    const target = shapeParentsEq(evt.target, (shape) => {
+      const id = shape.id();
+      return (
+        id.includes(WholeLineLine.namespace) ||
+        id.includes(WholeLinePoint.namespace)
+      );
+    });
+    const child = target && tree.find(target.id());
+    const pixel = [evt.evt.x, evt.evt.y];
+
+    if (child instanceof WholeLineLine) {
+      if (!canOper || canOper(child, evt.target)) {
+        if (polyginAttrib.lineIds.includes(child.attrib.id)) {
+          const { change: cChange } = wholeLineFixLineAddPoint(
+            config,
+            child.attrib.id,
+            pointAttribFactory(tree.getRealFromStage(pixel))
+          );
+          return mergeChange(change, cChange);
+        }
+      }
+    }
+
+    let pointAttrib: P & Attrib;
+    if (child instanceof WholeLinePoint) {
+      if (canOper && !canOper(child, evt.target)) {
+        return;
+      }
+      pointAttrib = quotePoint ? child.attrib : { ...child.attrib };
+    } else {
+      pointAttrib = pointAttribFactory(tree.getRealFromStage(pixel)) as P &
+        Attrib;
+    }
+
+    const exixts = getWholeLinePolygonPoints(config, polyginAttrib.id).some(
+      ({ id }) => id === pointAttrib.id
+    );
+
+    // 存在的情况下删除
+    if (exixts) {
+      const cChange = wholeLineDelPointByPointIds(
+        config,
+        pointAttrib.id,
+        !canDelPoint || canDelPoint(pointAttrib, evt)
+      );
+      change = mergeChange(change, cChange);
+
+      if (polyginAttrib.lineIds.length === 0) {
+        removePolygon();
+        const repPoint = cChange.pointChange.del.find(
+          ({ id }) => id !== pointAttrib.id
+        );
+        delete repPoint.id;
+        start(null);
+        prevId = wholeLineAddPoint(config, repPoint).id;
+        change.pointChange.add.push({
+          ...repPoint,
+          id: prevId,
+        });
+      }
+      return change;
+    }
+
+    if (!quotePoint) {
+      delete pointAttrib.id;
+    }
+
+    if (polyginAttrib.lineIds.length > 0) {
+      if (newMode) {
+        const mChange = wholeLinePolygonLastAddPoint(
+          config,
+          polygonId,
+          pointAttrib
+        ).change;
+        // 持续添加模式
+        return mergeChange(change, mChange);
+      } else {
+        // 直接当成新建操作
+        start(null);
+        return continuous(evt);
+      }
+    } else if (prevId) {
+      const { line, change: mChange } = wholeLineAddLineByPointIds(config, [
+        prevId,
+        wholeLineAddPoint(config, pointAttrib).id,
+      ]);
+      change = mergeChange(change, mChange, {
+        polygonChange: {
+          update: [
+            {
+              before: { ...polyginAttrib, lineIds: [...polyginAttrib.lineIds] },
+              after: {
+                ...polyginAttrib,
+                lineIds: [...polyginAttrib.lineIds, line.id],
+              },
+            },
+          ],
+        },
+        pointChange: {
+          add: [pointAttrib],
+        },
+      });
+      polyginAttrib.lineIds.push(line.id);
+      return change;
+    } else {
+      const addPoint = wholeLineAddPoint(config, pointAttrib)!;
+      prevId = addPoint.id;
+      change.pointChange.add.push(addPoint);
+      return change;
+    }
+  };
+
+  const end = () => {
+    // 没有两个点以上的多边形直接删除
+    if (polyginAttrib.lineIds.length === 0) {
+      if (prevId) {
+        wholeLineDelPointByPointIds(config, prevId);
+      }
+      removePolygon();
+    }
+    changePolygon && changePolygon(null);
+  };
+  const getStatus = () => ({
+    newMode,
+    polyginAttribId: polyginAttrib.id,
+    prevId,
+    config,
+  });
+
+  return {
+    continuous,
+    end,
+    getStatus,
+    setStatus: (status: ReturnType<typeof getStatus>) => {
+      newMode = status.newMode;
+      polyginAttrib = status.config.polygons.find(
+        ({ id }) => id === status.polyginAttribId
+      );
+      prevId = status.prevId;
+      config = status.config;
+    },
+  };
+};

+ 220 - 0
src/board/packages/whole-line/service/whole-line-helper.ts

@@ -0,0 +1,220 @@
+import { KonvaEventObject } from "konva/lib/Node";
+import {
+  WholeLineChange,
+  getWholeLineLinesRaw,
+  getWholeLinePoints,
+} from "./whole-line-base";
+import { ref } from "vue";
+import {
+  WholeLineAttrib,
+  WholeLineLine,
+  WholeLinePoint,
+  WholeLinePointAttrib,
+} from "../view";
+import { Attrib, ShapeType } from "../../../type";
+import { penWholeLinePoygonsEdit } from "./whole-line-edit";
+import { Container } from "../../container";
+
+export type WholeLineHelperInfo = {
+  change: WholeLineChange;
+  config: WholeLineAttrib;
+};
+export type WholeLineHelperInfoAL = ReturnType<
+  typeof wholeLineAnalysisHelperInfo
+>;
+
+/**
+ * 鼠标绑定钢笔模式编辑多边形,并返回辅助信息
+ * @param props
+ * @returns
+ */
+export const penWholeLinePoygonsEditWithHelperMouse = <
+  P extends Omit<WholeLinePointAttrib, "id">
+>(props: {
+  tree: Container;
+  config: WholeLineAttrib<P & Attrib>;
+  polygonId?: string;
+  pointAttribFactory?: (pos: number[]) => Omit<P, "id">;
+  quotePoint?: boolean;
+  canOper?: (
+    tree: WholeLineLine | WholeLinePoint,
+    operShape: ShapeType
+  ) => boolean;
+  canDelPoint?: (point: P, evt: KonvaEventObject<any>) => boolean;
+  changePolygon?: (polygonId: string) => void;
+  quitHandler?: () => void;
+}) => {
+  const copyAttrib = () =>
+    JSON.parse(JSON.stringify(props.config)) as WholeLineAttrib<P & Attrib>;
+
+  const edit = penWholeLinePoygonsEdit({ ...props, dev: true });
+  const helper = penWholeLinePoygonsEdit({
+    ...props,
+    changePolygon: undefined,
+    config: copyAttrib(),
+  });
+
+  const syncHelper = () => {
+    const status = edit.getStatus();
+    helper.setStatus({
+      ...status,
+      config: copyAttrib(),
+    });
+  };
+
+  const stage = props.tree.stage;
+  const helperInfo = ref<WholeLineHelperInfo>();
+
+  let downing = false;
+  const downHandler = () => {
+    downing = true;
+    helperInfo.value = undefined;
+  };
+  const moveHandler = (evt: KonvaEventObject<any>) => {
+    if (downing) return;
+    syncHelper();
+    const change = helper.continuous(evt);
+    const { config } = helper.getStatus();
+    helperInfo.value = {
+      change,
+      config,
+    };
+  };
+  const upHandler = (evt: KonvaEventObject<any>) => {
+    downing = false;
+    moveHandler(evt);
+  };
+  let prevTime = 0;
+  const clickHandler = (evt: KonvaEventObject<any>) => {
+    if (Date.now() - prevTime < 200) {
+      leaveEditMode();
+    } else {
+      prevTime = Date.now();
+      edit.continuous(evt);
+      syncHelper();
+    }
+  };
+  let leaveEditMode = () => {
+    stage.off("mousedown.editPolygonsMode", clickHandler);
+    stage.off("mousedown.editPolygonsMode", downHandler);
+    stage.off("mousemove.editPolygonsMode", moveHandler);
+    stage.off("mouseup.editPolygonsMode", upHandler);
+
+    helperInfo.value = undefined;
+    edit.end();
+    helper.end();
+    props.quitHandler && props.quitHandler();
+  };
+
+  stage.on("mousedown.editPolygonsMode", downHandler);
+  stage.on("mousemove.editPolygonsMode", moveHandler);
+  stage.on("mouseup.editPolygonsMode", upHandler);
+  stage.on("mousedown.editPolygonsMode", clickHandler);
+
+  return {
+    helperInfo,
+    destory() {
+      if (leaveEditMode) {
+        leaveEditMode();
+        leaveEditMode = null;
+      }
+    },
+  };
+};
+
+/**
+ * 判断是否需要更新多边形闭合线
+ * @param change 变化内容
+ * @param polygonId 判断的多边形
+ * @returns false | 'after' | 'before'
+ */
+export const needUpdatePolygonClose = (
+  change: WholeLineChange,
+  polygonId: string
+) => {
+  const { lineChange, polygonChange } = change;
+  if (!lineChange || !polygonChange?.update?.length) return false;
+
+  const current = polygonChange.update.find(
+    ({ after }) => after.id === polygonId
+  );
+  if (!current) return false;
+
+  const { before, after } = current;
+  if (!lineChange.del?.length) {
+    if (after.lineIds.length >= 2) return "after";
+  } else if (lineChange.add?.length) return false;
+
+  if (after.lineIds.length !== before.lineIds.length) {
+    if (!after.lineIds.includes(before.lineIds[before.lineIds.length - 1])) {
+      return "after";
+    } else if (!after.lineIds.includes(before.lineIds[0])) {
+      return "before";
+    }
+  }
+  return false;
+};
+
+/**
+ * 解析辅助信息
+ * @param useConfig 当前正在使用的attrib
+ * @param helpInfo 辅助信息
+ * @param needClose 是否要解析闭合线
+ */
+export const wholeLineAnalysisHelperInfo = (
+  helpInfo: WholeLineHelperInfo,
+  needClose = false
+) => {
+  const addPoints: number[][] = [];
+  if (helpInfo.change?.pointChange?.add) {
+    addPoints.push(
+      ...helpInfo.change?.pointChange?.add.map(({ x, y }) => [x, y])
+    );
+  }
+  const delPoints: string[] = [];
+  if (helpInfo.change?.pointChange?.del) {
+    delPoints.push(...helpInfo.change?.pointChange?.del.map(({ id }) => id));
+  }
+
+  const addLines: number[][] = [];
+  if (helpInfo.change?.lineChange?.add) {
+    const linePointIds = helpInfo.change.lineChange.add.map(
+      ({ pointIds }) => pointIds
+    );
+    addLines.push(
+      ...linePointIds.map((pointIds) => {
+        const points = getWholeLinePoints(helpInfo.config, pointIds);
+        return points.flatMap(({ x, y }) => [x, y]);
+      })
+    );
+  }
+
+  const delLines: string[] = [];
+  if (helpInfo.change?.lineChange?.del) {
+    delLines.push(...helpInfo.change?.lineChange?.del.map(({ id }) => id));
+  }
+
+  const cloneLines: { [key in string]: number[] } = {};
+  if (needClose && helpInfo.change?.polygonChange?.update) {
+    helpInfo.change.polygonChange.update.forEach((update) => {
+      if (needUpdatePolygonClose(helpInfo.change, update.after.id)) {
+        const lines = getWholeLineLinesRaw(helpInfo.config, [
+          update.after.lineIds[update.after.lineIds.length - 1],
+          update.after.lineIds[0],
+        ]);
+        cloneLines[update.after.id] = getWholeLinePoints(helpInfo.config, [
+          lines[0].pointIds[1],
+          lines[1].pointIds[0],
+        ]).flatMap(({ x, y }) => [x, y]);
+      }
+    });
+  }
+
+  return {
+    addLines,
+    delLines,
+    addPoints,
+    delPoints,
+    cloneLines,
+  };
+};

+ 112 - 0
src/board/packages/whole-line/service/whole-line-mouse.ts

@@ -0,0 +1,112 @@
+import { getLineProjection } from "../../../shared";
+import { WholeLineAttrib, WholeLinePointAttrib } from "../view";
+import {
+  generateWholeLinePointId,
+  getWholeLineLine,
+  getWholeLinePolygonRaw,
+  mergeChange,
+  wholeLineAddLineByPointIds,
+  wholeLineLineAddPoint,
+} from "./whole-line-base";
+
+export type MPoint = Omit<WholeLinePointAttrib, "id"> & { id?: string };
+
+// 加点
+export const wholeLineAddPoint = <T extends MPoint>(
+  config: WholeLineAttrib,
+  pointAttrib: T
+) => {
+  if ("id" in pointAttrib && pointAttrib["id"]) {
+    return pointAttrib;
+  }
+
+  const id = generateWholeLinePointId(config);
+  const point = {
+    ...pointAttrib,
+    id,
+  };
+  config.points.push(point);
+  return point;
+};
+
+/**
+ * 线段加点
+ */
+export const wholeLineFixLineAddPoint = <T extends MPoint>(
+  config: WholeLineAttrib,
+  lineId: string,
+  position: T
+) => {
+  const lineAttrib = getWholeLineLine(config, lineId);
+  const linePosition = getLineProjection(
+    lineAttrib.flatMap(({ x, y }) => [x, y]),
+    [position.x, position.y]
+  ).point;
+
+  const pointAttrib = wholeLineAddPoint(config, {
+    ...position,
+    x: linePosition[0],
+    y: linePosition[1],
+  });
+  const { change, addedPoints } = wholeLineLineAddPoint(
+    config,
+    lineAttrib,
+    pointAttrib.id
+  );
+  change.pointChange.add.push(pointAttrib as any);
+
+  return { change, addedPoints };
+};
+
+/**
+ * 末尾加点
+ */
+export const wholeLinePolygonLastAddPoint = <T extends MPoint>(
+  config: WholeLineAttrib,
+  polygonId: string,
+  pointAttribRaw: T
+) => {
+  const polyginAttrib = getWholeLinePolygonRaw(config, polygonId)!;
+  return wholeLinePolygonAddPoint(
+    config,
+    polygonId,
+    polyginAttrib.lineIds[polyginAttrib.lineIds.length - 1],
+    pointAttribRaw
+  );
+};
+
+/**
+ * 基于某一点追加点
+ */
+export const wholeLinePolygonAddPoint = <T extends MPoint>(
+  config: WholeLineAttrib,
+  polygonId: string,
+  prevLineId: string | null,
+  pointAttribRaw: T
+) => {
+  const polyginAttrib = getWholeLinePolygonRaw(config, polygonId)!;
+  const lastLine = getWholeLineLine(config, prevLineId);
+  const pointAttrib = wholeLineAddPoint(config, pointAttribRaw);
+  let { line, change } = wholeLineAddLineByPointIds(config, [
+    lastLine[1].id,
+    pointAttrib.id as string,
+  ]);
+  change = mergeChange(change, {
+    pointChange: {
+      add: [pointAttrib as any],
+    },
+    polygonChange: {
+      update: [
+        {
+          before: { ...polyginAttrib, lineIds: [...polyginAttrib.lineIds] },
+          after: {
+            ...polyginAttrib,
+            lineIds: [...polyginAttrib.lineIds, line.id],
+          },
+        },
+      ],
+    },
+  });
+  polyginAttrib.lineIds.push(line.id);
+  return { line, change };
+};

+ 0 - 27
src/board/packages/whole-line/shapes.ts

@@ -1,27 +0,0 @@
-import { Circle } from "konva/lib/shapes/Circle";
-import { setShapeConfig } from "../../shared/util";
-import { Line } from "konva/lib/shapes/Line";
-
-export const wholeLineShapes = {
-  point: (style: Record<string, any>) => {
-    const circle = new Circle();
-    setShapeConfig(circle, style);
-    return {
-      shape: circle,
-      setData(point: number[]) {
-        circle.x(point[0]);
-        circle.y(point[1]);
-      },
-    };
-  },
-  line: (style: Record<string, any>) => {
-    const line = new Line();
-    setShapeConfig(line, style);
-    return {
-      shape: line,
-      setData(data: number[]) {
-        line.points(data);
-      },
-    };
-  },
-};

+ 130 - 29
src/board/packages/whole-line/style.ts

@@ -1,38 +1,139 @@
-export const pointStyle = {
-  radius: 7,
-  strokeWidth: 4,
-  fill: "#FF0000",
-  stroke: "#000000",
+import { Circle } from "konva/lib/shapes/Circle";
+import { Line } from "konva/lib/shapes/Line";
+import { getRealAbsoluteSize } from "../../shared";
+import { Group } from "konva/lib/Group";
+import { CustomizeShape } from "../../type";
+import { KonvaNodeEvent } from "konva/lib/types";
+import { KonvaEventObject } from "konva/lib/Node";
+
+export const point = {
+  fill: "#ffffff",
+  radius: 3,
+  stroke: "#409EFF",
+  zIndex: 3,
+  activeFill: "#ffffff",
+  activeStroke: "#E6A23C",
 };
 
-export const pointDragStyle = {
-  common: pointStyle,
-  hover: {
-    fill: "#00FF00",
-    stroke: "#000000",
-  },
-  active: {
-    fill: "#000000",
-    stroke: "#FF0000",
-  },
+export const pointShapeFactory = () => {
+  const p = new Circle({ radius: point.radius });
+  const pub = () => {
+    const [size] = getRealAbsoluteSize(p, [1, 1]);
+    p.scale({ x: size, y: size });
+  };
+
+  const common = () => {
+    pub();
+    p.fill(point.fill).stroke(point.stroke);
+  };
+  const active = () => {
+    pub();
+    p.fill(point.activeFill).stroke(point.activeStroke);
+  };
+
+  common();
+  return {
+    shape: p,
+    setData(data: number[]) {
+      pub();
+      p.x(data[0]);
+      p.y(data[1]);
+    },
+    common,
+    hover: active,
+    active: active,
+    draging: active,
+  };
 };
 
-export const lineStyle = {
-  stroke: "black",
+export const line = {
   strokeWidth: 5,
+  hitStrokeWidth: 20,
+  stroke: "rgba(230, 162, 60, 1)",
+  zIndex: 1,
+
+  activeStroke: "rgba(64, 158, 255, 1)",
+};
+
+export const lineShapeFactory = () => {
+  const path = new Line({
+    strokeScaleEnabled: false,
+    strokeWidth: line.strokeWidth,
+    hitStrokeWidth: line.hitStrokeWidth,
+  });
+
+  const common = () => {
+    path.stroke(line.stroke);
+  };
+  const active = () => {
+    path.stroke(line.activeStroke);
+  };
+
+  common();
+  return {
+    shape: path,
+    setData(data: number[]) {
+      path.points(data);
+    },
+    common,
+    hover: common,
+    active,
+    draging: active,
+  };
+};
+
+export const polygon = {
+  fill: "rgba(230, 162, 60, 0.30)",
+  activeFill: "rgba(64, 158, 255, 0.3)",
+  zIndex: 0,
 };
 
-export const lineDragStyle = {
-  common: lineStyle,
-  hover: {
-    stroke: "#FF0000",
-  },
-  active: {
-    stroke: "#00FF00",
-  },
+type PolygonShapeProps = {
+  autoClose?: boolean;
+  lineFactory?: () => CustomizeShape<number[]>;
 };
 
-export const pointZIndex = 3;
-export const lineZIndex = 1;
-export const lineHelperZIndex = 2;
-export const pointHelperZIndex = 4;
+export const polygonShapeFactory = (props: PolygonShapeProps = {}) => {
+  const group = new Group();
+  const path = new Line({
+    closed: true,
+  });
+  const closeLine = props.autoClose
+    ? props.lineFactory
+      ? props.lineFactory()
+      : lineShapeFactory()
+    : null;
+
+  group.add(path);
+  if (closeLine) {
+    group.add(closeLine.shape);
+  }
+
+  const common = (ev?: KonvaEventObject<any>) => {
+    path.fill(polygon.fill);
+    closeLine && closeLine.common(ev);
+  };
+  const active = (ev: KonvaEventObject<any>) => {
+    path.fill(polygon.activeFill);
+    closeLine && closeLine.active && closeLine.active(ev);
+  };
+
+  common();
+  return {
+    shape: group,
+    setData(data: number[]) {
+      path.points(data);
+      if (data.length > 4) {
+        const ndxs = [data.length - 2, data.length - 1, 0, 1];
+        closeLine.setData(ndxs.map((ndx) => data[ndx]));
+        closeLine.shape.visible(true);
+      } else {
+        closeLine.shape.visible(false);
+      }
+    },
+    common,
+    hover: common,
+    active,
+    draging: active,
+  };
+};

+ 42 - 15
src/board/packages/whole-line/view/whole-line-line.ts

@@ -1,40 +1,62 @@
-import { wholeLineShapes } from "../shapes";
-import { Attrib } from "../../../type";
-import { lineStyle, lineZIndex } from "../style";
+import {
+  Attrib,
+  CustomizeShape,
+  CustomizeShapeFactory,
+  ShapeType,
+} from "../../../type";
+import { lineShapeFactory, line } from "../style";
 import { WholeLineAttrib } from "./whole-line";
 import { getWholeLinePoints } from "../service/whole-line-base";
 import { Entity, EntityProps } from "../../entity";
+import { Line } from "konva/lib/shapes/Line";
 
 export type WholeLineLineAttrib = Attrib & {
   pointIds: string[];
 };
 
-type LineShape = ReturnType<typeof wholeLineShapes.line>["shape"];
-export type WholeLineLineProps = EntityProps<WholeLineLineAttrib>;
-export class WholeLineLine extends Entity<WholeLineLineAttrib, LineShape> {
-  static namespace = "line";
+export type WholeLineLineProps<
+  T extends WholeLineLineAttrib = WholeLineLineAttrib
+> = EntityProps<T>;
 
+export class WholeLineLine<
+  T extends WholeLineLineAttrib = WholeLineLineAttrib,
+  R extends ShapeType = Line
+> extends Entity<T, R> {
+  static namespace = "line";
   private config: WholeLineAttrib;
 
-  constructor(props: WholeLineLineProps) {
-    props.zIndex = props.zIndex || lineZIndex;
+  actShape: CustomizeShape<number[], R>;
+  actShapeFactory: CustomizeShapeFactory<T, number[], R>;
+
+  constructor(props: WholeLineLineProps<T>) {
+    props.zIndex = props.zIndex || line.zIndex;
     props.name = props.name || WholeLineLine.namespace + props.attrib.id;
     super(props);
+
+    this.actShapeFactory = lineShapeFactory as CustomizeShapeFactory<
+      T,
+      number[],
+      R
+    >;
+  }
+
+  setActShapeFactory(actShapeFactory: CustomizeShapeFactory<T, number[], R>) {
+    this.actShapeFactory = actShapeFactory;
   }
 
-  private setShapeData: (data: number[]) => void;
   initShape() {
-    const { shape, setData } = wholeLineShapes.line(lineStyle);
-    this.setShapeData = setData;
-    return shape;
+    this.actShape = this.actShapeFactory(this.attrib, this);
+    return this.actShape.shape;
   }
 
   diffRedraw(): void {
     const coords = this.getCoords();
     if (coords.length) {
-      this.setShapeData(coords);
+      this.actShape.setData(coords);
     } else {
-      console.error("line:", this.attrib, "找不到对应的点坐标");
+      console.error("line:", this.attrib, "找不到对应的点坐标", [
+        ...this.config.points,
+      ]);
     }
   }
 
@@ -53,4 +75,9 @@ export class WholeLineLine extends Entity<WholeLineLineAttrib, LineShape> {
     }
     return result;
   }
+
+  mounted(): void {
+    super.mounted();
+    this.actShape.common();
+  }
 }

+ 37 - 14
src/board/packages/whole-line/view/whole-line-point.ts

@@ -1,31 +1,54 @@
-import { Attrib } from "../../../type";
-import { pointStyle, pointZIndex } from "../style";
-import { wholeLineShapes } from "../shapes";
+import {
+  Attrib,
+  CustomizeShape,
+  CustomizeShapeFactory,
+  ShapeType,
+} from "../../../type";
+import { pointShapeFactory, point } from "../style";
 import { Entity, EntityProps } from "../../entity";
+import { Circle } from "konva/lib/shapes/Circle";
 
 export type WholeLinePointAttrib = Attrib & { x: number; y: number };
 
-type PointShape = ReturnType<typeof wholeLineShapes.point>["shape"];
+export type WholeLinePointProps<
+  T extends WholeLinePointAttrib = WholeLinePointAttrib
+> = EntityProps<T>;
 
-export type WholeLinePointProps = EntityProps<WholeLinePointAttrib>;
-
-export class WholeLinePoint extends Entity<WholeLinePointAttrib, PointShape> {
+export class WholeLinePoint<
+  T extends WholeLinePointAttrib = WholeLinePointAttrib,
+  R extends ShapeType = Circle
+> extends Entity<T, R> {
   static namespace = "point";
+  actShape: CustomizeShape<number[], R>;
+
+  actShapeFactory: CustomizeShapeFactory<T, number[], R>;
 
-  constructor(props: WholeLinePointProps) {
-    props.zIndex = props.zIndex || pointZIndex;
+  constructor(props: WholeLinePointProps<T>) {
+    props.zIndex = props.zIndex || point.zIndex;
     props.name = props.name || WholeLinePoint.namespace + props.attrib.id;
     super(props);
+    this.actShapeFactory = pointShapeFactory as CustomizeShapeFactory<
+      T,
+      number[],
+      R
+    >;
+  }
+
+  setActShapeFactory(actShapeFactory: CustomizeShapeFactory<T, number[], R>) {
+    this.actShapeFactory = actShapeFactory;
   }
 
-  private setShapeData: (data: number[]) => void;
   initShape() {
-    const { shape, setData } = wholeLineShapes.point(pointStyle);
-    this.setShapeData = setData;
-    return shape;
+    this.actShape = this.actShapeFactory(this.attrib, this);
+    return this.actShape.shape;
   }
 
   diffRedraw() {
-    this.setShapeData([this.attrib.x, this.attrib.y]);
+    this.actShape.setData([this.attrib.x, this.attrib.y]);
+  }
+
+  mounted(): void {
+    super.mounted();
+    this.actShape.common();
   }
 }

+ 72 - 1
src/board/packages/whole-line/view/whole-line-polygon.ts

@@ -1,5 +1,76 @@
-import { Attrib } from "../../../type";
+import {
+  Attrib,
+  CustomizeShape,
+  CustomizeShapeFactory,
+  ShapeType,
+} from "../../../type";
+import { polygonShapeFactory, polygon } from "../style";
+import { WholeLineAttrib } from "./whole-line";
+import { getWholeLinePolygonPoints } from "../service/whole-line-base";
+import { Entity, EntityProps } from "../../entity";
+import { Line } from "konva/lib/shapes/Line";
 
 export type WholeLinePolygonAttrib = Attrib & {
   lineIds: string[];
 };
+
+export type WholeLinePolygonProps<
+  T extends WholeLinePolygonAttrib = WholeLinePolygonAttrib
+> = EntityProps<T>;
+
+export class WholeLinePolygon<
+  T extends WholeLinePolygonAttrib = WholeLinePolygonAttrib,
+  R extends ShapeType = Line
+> extends Entity<T, R> {
+  static namespace = "polygon";
+  private config: WholeLineAttrib;
+
+  actShape: CustomizeShape<number[], R>;
+  actShapeFactory: CustomizeShapeFactory<T, number[], R>;
+
+  constructor(props: WholeLinePolygonProps<T>) {
+    props.zIndex = props.zIndex || polygon.zIndex;
+    props.name = props.name || WholeLinePolygon.namespace + props.attrib.id;
+    super(props);
+
+    this.actShapeFactory = polygonShapeFactory as CustomizeShapeFactory<
+      T,
+      number[],
+      R
+    >;
+  }
+
+  setActShapeFactory(actShapeFactory: CustomizeShapeFactory<T, number[], R>) {
+    this.actShapeFactory = actShapeFactory;
+  }
+
+  initShape() {
+    this.actShape = this.actShapeFactory(this.attrib, this);
+    return this.actShape.shape;
+  }
+
+  diffRedraw(): void {
+    this.actShape.setData(this.getCoords());
+  }
+
+  setConfig(config: WholeLineAttrib) {
+    this.config = config;
+  }
+
+  getCoords() {
+    const result: number[] = [];
+    const points = getWholeLinePolygonPoints(this.config, this.attrib.id);
+    if (!points.some((point) => !point)) {
+      points.forEach(({ x, y }, ndx) => {
+        result[ndx * 2] = x;
+        result[ndx * 2 + 1] = y;
+      });
+    }
+    return result;
+  }
+
+  mounted(): void {
+    super.mounted();
+    this.actShape.common();
+  }
+}

+ 86 - 35
src/board/packages/whole-line/view/whole-line.ts

@@ -1,9 +1,9 @@
-import { Attrib } from "../../../type";
+import { Attrib, ShapeType } from "../../../type";
 import { DEV } from "../../../env";
 import { WholeLinePointHelper, WholeLineLineHelper } from "../helper";
 import { WholeLinePoint, WholeLinePointAttrib } from "./whole-line-point";
 import { WholeLineLine, WholeLineLineAttrib } from "./whole-line-line";
-import { WholeLinePolygonAttrib } from "./whole-line-polygon";
+import { WholeLinePolygon, WholeLinePolygonAttrib } from "./whole-line-polygon";
 import { Group } from "konva/lib/Group";
 import { Entity, EntityProps } from "../../entity";
 import {
@@ -12,55 +12,105 @@ import {
 } from "../../../shared/entity-utils";
 import { watch } from "vue";
 
-export type WholeLineAttrib = Attrib & {
-  points: WholeLinePointAttrib[];
-  lines: WholeLineLineAttrib[];
-  polygons: WholeLinePolygonAttrib[];
+export type WholeLineAttrib<
+  P extends WholeLinePointAttrib = WholeLinePointAttrib,
+  L extends WholeLineLineAttrib = WholeLineLineAttrib,
+  PY extends WholeLinePolygonAttrib = WholeLinePolygonAttrib
+> = Attrib & {
+  points: P[];
+  lines: L[];
+  polygons: PY[];
 };
 
-export type WholeLineProps = EntityProps<WholeLineAttrib>;
+export type WholeLineProps<
+  W extends WholeLineAttrib<P, L, PY>,
+  P extends WholeLinePointAttrib = WholeLinePointAttrib,
+  L extends WholeLineLineAttrib = WholeLineLineAttrib,
+  PY extends WholeLinePolygonAttrib = WholeLinePolygonAttrib
+> = EntityProps<W>;
 
-export class WholeLine extends Entity<WholeLineAttrib, Group> {
+export type WLP<W extends WholeLineAttrib> = W extends WholeLineAttrib<infer P>
+  ? P
+  : never;
+
+export type WLL<W extends WholeLineAttrib> = W extends WholeLineAttrib<
+  WholeLinePointAttrib,
+  infer L
+>
+  ? L
+  : never;
+
+export type WLPY<W extends WholeLineAttrib> = W extends WholeLineAttrib<
+  WholeLinePointAttrib,
+  WholeLineLineAttrib,
+  infer PY
+>
+  ? PY
+  : never;
+
+export class WholeLine<
+  W extends WholeLineAttrib = WholeLineAttrib,
+  PS extends ShapeType = ShapeType,
+  LS extends ShapeType = ShapeType,
+  PYS extends ShapeType = ShapeType
+> extends Entity<W, Group> {
   static namespace = "whole-line";
 
+  incLinesFactory: IncEntitysFactory<WLL<W>, WholeLineLine<WLL<W>, LS>>;
+  incPointsFactory: IncEntitysFactory<WLP<W>, WholeLinePoint<WLP<W>, PS>>;
+  incPolygonFactory: IncEntitysFactory<WLPY<W>, WholeLinePolygon<WLPY<W>, PYS>>;
+
   private incLinesHelperFactory: IncEntitysFactory<
     WholeLineLineAttrib,
     WholeLineLineHelper
   >;
-  private incLinesFactory: IncEntitysFactory<
-    WholeLineLineAttrib,
-    WholeLineLine
-  >;
 
   private incPointsHelperFactory?: IncEntitysFactory<
     WholeLinePointAttrib,
     WholeLinePointHelper
   >;
-  private incPointsFactory: IncEntitysFactory<
-    WholeLinePointAttrib,
-    WholeLinePoint
-  >;
 
-  constructor(props: WholeLineProps) {
+  constructor(props: WholeLineProps<W>) {
     props.name = props.name || WholeLine.namespace + props.attrib.id;
     super(props);
   }
 
   initIncFactory() {
     this.incLinesFactory = incEntitysFactoryGenerate(
-      WholeLineLine,
+      WholeLineLine<WLL<W>, LS>,
       this,
-      (line) => line.setConfig(this.attrib)
+      (line) => {
+        line.setConfig(this.attrib);
+      }
+    );
+    this.incPointsFactory = incEntitysFactoryGenerate(
+      WholeLinePoint<WLP<W>, PS>,
+      this
     );
-    this.incLinesHelperFactory =
-      DEV &&
-      incEntitysFactoryGenerate(WholeLineLineHelper, this, (line) =>
-        line.setConfig(this.attrib)
+
+    this.incPolygonFactory = incEntitysFactoryGenerate(
+      WholeLinePolygon<WLPY<W>, PYS>,
+      this
+    );
+
+    if (DEV) {
+      this.incLinesHelperFactory = incEntitysFactoryGenerate(
+        WholeLineLineHelper,
+        this,
+        (line) => {
+          line.setConfig(this.attrib);
+        }
+      );
+      this.incPointsHelperFactory = incEntitysFactoryGenerate(
+        WholeLinePointHelper,
+        this
       );
 
-    this.incPointsFactory = incEntitysFactoryGenerate(WholeLinePoint, this);
-    this.incPointsHelperFactory =
-      DEV && incEntitysFactoryGenerate(WholeLinePointHelper, this);
+      this.incPointsHelperFactory = incEntitysFactoryGenerate(
+        WholeLinePointHelper,
+        this
+      );
+    }
   }
 
   init() {
@@ -74,7 +124,7 @@ export class WholeLine extends Entity<WholeLineAttrib, Group> {
 
   getRedrawLines() {
     // 去重 防止来回线段绘画两次
-    const lines: WholeLineLineAttrib[] = [];
+    const lines: WLL<W>[] = [];
     for (let i = this.attrib.lines.length - 1; i >= 0; i--) {
       const a = this.attrib.lines[i];
       let j = 0;
@@ -88,7 +138,7 @@ export class WholeLine extends Entity<WholeLineAttrib, Group> {
         }
       }
       if (i === j) {
-        lines.push(a);
+        lines.push(a as WLL<W>);
       }
     }
     return lines;
@@ -100,9 +150,13 @@ export class WholeLine extends Entity<WholeLineAttrib, Group> {
       this.incLinesHelperFactory(lineAttribs);
       this.incPointsHelperFactory(this.attrib.points);
     }
+    const pointEntityInc = this.incPointsFactory(this.attrib.points as any);
     const lineEntityInc = this.incLinesFactory(lineAttribs);
-    const pointEntityInc = this.incPointsFactory(this.attrib.points);
-    return { lineEntityInc, pointEntityInc };
+    const polygonEntityInc = this.incPolygonFactory(
+      this.attrib.polygons as any
+    );
+
+    return { lineEntityInc, pointEntityInc, polygonEntityInc };
   }
 
   initReactive() {
@@ -111,11 +165,8 @@ export class WholeLine extends Entity<WholeLineAttrib, Group> {
         pointIds: this.attrib.points.map(({ id }) => id),
         lineIds: this.attrib.lines.map(({ id }) => id),
       }),
-      (newData) => {
-        console.log(newData);
-        this.diffRedraw();
-      },
-      { immediate: true }
+      this.diffRedraw.bind(this),
+      { immediate: true, flush: "sync" }
     );
   }
 }

+ 167 - 0
src/board/plugins/bound-plugin.ts

@@ -0,0 +1,167 @@
+import { Matrix3, Vector2 } from "three";
+import { Container } from "../packages";
+import { openShapeDrag } from "../shared";
+import { getAbsoluteTransform } from "../shared/shape-helper";
+
+export type BoundQueryPluginProps = {
+  move?: boolean;
+  wheel?: boolean;
+  bound?: number[];
+};
+
+const mat = new Matrix3();
+const getMoveAfterBound = (bound: number[], move: number[]) => {
+  mat.identity().translate(-move[0], -move[1]);
+  const lt = new Vector2(bound[0], bound[1]).applyMatrix3(mat);
+  const br = new Vector2(bound[2], bound[3]).applyMatrix3(mat);
+  return [lt.x, lt.y, br.x, br.y];
+};
+
+const getScaleAfterBound = (
+  bound: number[],
+  center: number[],
+  scale: number
+) => {
+  mat
+    .identity()
+    .translate(-center[0], -center[1])
+    .scale(scale, scale)
+    .translate(center[0], center[1]);
+  const lt = new Vector2(bound[0], bound[1]).applyMatrix3(mat);
+  const br = new Vector2(bound[2], bound[3]).applyMatrix3(mat);
+  return [lt.x, lt.y, br.x, br.y];
+};
+
+export class BoundQueryPlugin {
+  tree: Container;
+  props: Omit<BoundQueryPluginProps, "bound">;
+  bound?: number[];
+
+  constructor(props: BoundQueryPluginProps = {}) {
+    this.bound = props.bound;
+    this.props = props;
+    this.props.move = props.move || false;
+    this.props.wheel = props.wheel || false;
+  }
+
+  private dragDestory?: () => void;
+  disableMove() {
+    this.props.move = false;
+    this.dragDestory && this.dragDestory();
+  }
+
+  enableMove(
+    moveHandler?: (newBound: number[], move: number[]) => boolean | number[]
+  ) {
+    this.disableMove();
+    this.props.move = true;
+    if (!this.tree) return;
+
+    const { stage } = this.tree;
+    this.dragDestory = openShapeDrag(
+      stage,
+      {
+        readyHandler: () => ({
+          tf: getAbsoluteTransform(stage, true).invert(),
+          initBound: this.bound,
+        }),
+        moveHandler: (moveRaw, { tf, initBound }) => {
+          const offset = tf.point({ x: moveRaw[0], y: moveRaw[1] });
+          const move = [offset.x, offset.y];
+          let newBound = getMoveAfterBound(initBound, move);
+          if (moveHandler) {
+            const result = moveHandler(newBound, move);
+            if (typeof result !== "boolean") {
+              newBound = result;
+            } else if (!result) {
+              return;
+            }
+          }
+          this.setBound(newBound);
+        },
+      },
+      false
+    );
+  }
+
+  private wheelDestory?: () => void;
+  disableWheel() {
+    this.props.wheel = false;
+    this.wheelDestory && this.wheelDestory();
+  }
+
+  enableWheel(
+    wheelHandler?: (
+      newBound: number[],
+      center: number[],
+      scale: number
+    ) => boolean | number[]
+  ) {
+    this.disableWheel();
+    this.props.wheel = true;
+    if (!this.tree) return;
+
+    const {
+      stage,
+      config: { dom },
+    } = this.tree;
+
+    const whellHandler = (ev: WheelEvent) => {
+      const scale = 1 + ev.deltaY / 1000;
+
+      const pointer = stage.getRelativePointerPosition();
+      const center = [pointer.x, pointer.y];
+
+      let newBound = getScaleAfterBound(this.bound, center, scale);
+      if (wheelHandler) {
+        const result = wheelHandler(newBound, center, scale);
+        if (typeof result !== "boolean") {
+          newBound = result;
+        } else if (!result) {
+          return;
+        }
+      }
+
+      this.bound = newBound;
+      this.update();
+    };
+    dom.addEventListener("wheel", whellHandler);
+    this.wheelDestory = () => dom.removeEventListener("wheel", whellHandler);
+  }
+
+  setBound(bound: number[]) {
+    this.bound = bound;
+    this.update();
+  }
+
+  setSize(width: number, height: number) {
+    this.tree.stage.width(width);
+    this.tree.stage.height(height);
+  }
+
+  setTree(tree: Container) {
+    this.tree = tree;
+    if (this.props.move) {
+      this.enableMove();
+    }
+    if (this.props.wheel) {
+      this.enableWheel();
+    }
+  }
+
+  update() {
+    const { stage } = this.tree;
+    const width = this.bound[2] - this.bound[0];
+    const height = this.bound[3] - this.bound[1];
+
+    const scaleX = stage.width() / width;
+    const scaleY = stage.height() / height;
+    const offsetX = -this.bound[0] * scaleX;
+    const offsetY = -this.bound[1] * scaleY;
+    // 更新Konva Stage的位置和缩放
+
+    stage.scale({ x: scaleX, y: scaleY });
+    stage.position({ x: offsetX, y: offsetY });
+    this.tree.redraw();
+  }
+}

+ 1 - 0
src/board/plugins/index.ts

@@ -0,0 +1 @@
+export * from "./bound-plugin";

+ 25 - 7
src/board/register.ts

@@ -1,17 +1,22 @@
-import { Attrib, ShapeType } from "./type";
+import { Attrib, ShapeType, Plugin } from "./type";
 import { Container } from "./packages/container";
 import { EntityType } from "./packages";
 
 export const register = <
   T extends string,
   R extends Attrib,
-  S extends ShapeType
->(types: {
-  [key in T]: EntityType<R, S>;
-}) => {
+  S extends ShapeType,
+  PT extends string,
+  P extends Plugin
+>(
+  types: {
+    [key in T]: EntityType<R, S>;
+  },
+  plugins?: { [key in PT]: P }
+) => {
   const initBoard = (
     dom: HTMLDivElement,
-    data?: { [key in T]?: R[] },
+    data?: { [key in T]?: R | R[] },
     readonly = false
   ) => {
     const container = new Container({
@@ -23,17 +28,30 @@ export const register = <
     });
     container.init();
 
+    const pluginApis = {} as { [key in PT]: P };
+    if (plugins) {
+      for (const key in plugins) {
+        const plugin = plugins[key];
+        plugin.setTree(container);
+        pluginApis[key] = plugin;
+      }
+    }
     return {
       tree: container,
-      setData(newData: { [key in T]?: R[] }) {
+      setData(newData: { [key in T]?: R | R[] }) {
         container.setAttrib({ data: newData });
       },
       getData() {
         return container.attrib.data;
       },
       destory() {
+        for (const key in plugins) {
+          const plugin = plugins[key];
+          plugin.destory && plugin.destory();
+        }
         container.destory();
       },
+      ...pluginApis,
     };
   };
   return initBoard;

BIN
src/board/shared/cursor/pic_pen.ico


BIN
src/board/shared/cursor/pic_pen_a.ico


BIN
src/board/shared/cursor/pic_pen_r.ico


+ 12 - 3
src/board/shared/entity-utils.ts

@@ -26,13 +26,20 @@ export const entityFactory = <
     ...getExtendsProps(parent),
   }) as InstanceType<C>;
   extra && extra(entity);
+  if (parent) {
+    entity.container = parent.container;
+    entity.setParent(parent);
+  }
   entity.init();
-  entity.setParent(parent);
+  entity.mount(entity.teleport);
+  if (parent.isMounted) {
+    entity.mounted();
+  }
   return entity;
 };
 
 export type IncEntitysFactory<T extends Attrib, E extends Entity<T, any>> = (
-  attribs: T[]
+  attribs: T[] | T
 ) => { adds: E[]; dels: E[]; upds: E[] };
 
 // 增量工厂
@@ -64,7 +71,8 @@ export const incEntitysFactoryGenerate = <
     return addEntity;
   };
 
-  return (attribs: T[]) => {
+  return (attribsRaw: T | T[]) => {
+    const attribs = Array.isArray(attribsRaw) ? attribsRaw : [attribsRaw];
     const { addPort, delPort, changePort } = getChangeAllPoart(
       attribs,
       oldAttribs
@@ -72,6 +80,7 @@ export const incEntitysFactoryGenerate = <
 
     const dels = delPort.map(destory);
     const adds = addPort.map((id) => add(findAttrib(attribs, id)));
+
     const upds = changePort.map((id) => {
       cache[id].setAttrib(findAttrib(attribs, id));
       return cache[id];

+ 6 - 0
src/board/shared/index.ts

@@ -0,0 +1,6 @@
+export * from "./entity-utils";
+export * from "./math";
+export * from "./public";
+export * from "./shape-mose";
+export * from "./util";
+export * from "./shape-helper";

+ 3 - 5
src/board/shared/math.ts

@@ -240,16 +240,14 @@ export const getLineProjection = (line: number[], position: number[]) => {
   const pointToLineStart = point.clone().sub(lineStart);
 
   // 计算点在线段方向上的投影长度
-  const projectionLength = pointToLineStart.dot(lineDir) / lineDir.lengthSq();
+  const t = pointToLineStart.dot(lineDir.normalize());
 
   // 计算投影点的坐标
-  const projectionPoint = lineStart
-    .clone()
-    .add(lineDir.clone().normalize().multiplyScalar(projectionLength));
+  const projectionPoint = lineStart.add(lineDir.multiplyScalar(t));
 
   return {
     point: projectionPoint.toArray(),
-    len: projectionLength,
+    len: projectionPoint.distanceTo(lineStart),
   };
 };
 

+ 40 - 0
src/board/shared/shape-helper.ts

@@ -0,0 +1,40 @@
+import { Node } from "konva/lib/Node";
+import { Transform } from "konva/lib/Util";
+
+export const getAbsoluteTransform = (shape: Node, includeSelf = false) => {
+  let current = shape;
+  const tf = includeSelf ? shape.getTransform().copy() : new Transform();
+  while ((current = current.parent)) {
+    tf.multiply(current.getTransform());
+  }
+  return tf;
+};
+
+export function getTfScaleFactors(tf: Transform) {
+  // 假设matrix是一个形如[a, c, Tx, b, d, Ty, 0, 0, 1]的数组
+  const [a, c, b, d] = tf.m;
+
+  const scaleX = Math.sqrt(a * a + b * b);
+  const scaleY = Math.sqrt(c * c + d * d);
+
+  const rotation = Math.atan2(b, a);
+  const directionX = rotation; // X轴缩放方向的旋转角度
+  const directionY = rotation + Math.PI / 2; // Y轴缩放方向的旋转角度(垂直于X轴)
+
+  console.log(directionX, directionY);
+  Math.sin(directionY);
+
+  return { x: scaleX, y: scaleY };
+}
+
+export const getRealAbsoluteSize = (shape: Node, size: number[]) => {
+  let current = shape;
+  let scale = { x: 1, y: 1 };
+  while ((current = current.parent)) {
+    const cScale = current.scale();
+
+    scale.x *= cScale.x;
+    scale.y *= cScale.y;
+  }
+  return [size[0] / scale.x, size[1] / scale.y];
+};

+ 124 - 66
src/board/shared/shape-mose.ts

@@ -1,21 +1,19 @@
 import { Attrib, ShapeStyles, ShapeType } from "../type";
-import { setShapeConfig } from "./util";
 import { Entity } from "../packages/entity";
 import { Shape } from "konva/lib/Shape";
 import { Group } from "konva/lib/Group";
+import { getAbsoluteTransform } from "./shape-helper";
+import { KonvaEventObject } from "konva/lib/Node";
 
-export const openShapeMouseStyles = <T extends Shape | Group>(
+export const openShapeMouseStyles = <T extends Shape | Group, R>(
   shape: T,
-  styles?: ShapeStyles<T>,
+  styles: ShapeStyles,
   namespace = "mouse-style"
 ) => {
   shape.listening(true);
 
-  const stage = shape.getStage();
-  const dom = stage.container();
-
   const useEvents: string[] = [];
-  const useOn = (name: string, cb: (ev: any) => void) => {
+  const useOn = (name: string, cb: (ev: KonvaEventObject<any>) => void) => {
     shape.on(name, cb);
     useEvents.push(name);
   };
@@ -24,93 +22,108 @@ export const openShapeMouseStyles = <T extends Shape | Group>(
   let draging = false;
   let active = false;
 
-  const mouseHandler = () => {
-    dom.style.cursor = draging || enter ? "pointer" : "inherit";
-
-    if (draging) {
-      styles.draging instanceof Function
-        ? styles.draging()
-        : setShapeConfig(shape, styles.draging);
-    } else if (active) {
-      styles.active instanceof Function
-        ? styles.active()
-        : setShapeConfig(shape, styles.active);
-    } else if (enter) {
-      styles.hover instanceof Function
-        ? styles.hover()
-        : setShapeConfig(shape, styles.hover);
-    } else {
-      styles.common instanceof Function
-        ? styles.common()
-        : setShapeConfig(shape, styles.common);
-    }
+  const mouseHandler = (ev: KonvaEventObject<any>) => {
+    const api = draging
+      ? "draging"
+      : active
+      ? "active"
+      : enter
+      ? "hover"
+      : "common";
+
+    styles[api] && styles[api](ev);
   };
 
   if (styles.hover) {
-    useOn(`mouseenter.${namespace}`, () => {
+    useOn(`mouseenter.${namespace}`, (ev) => {
       enter = true;
-      mouseHandler();
+      mouseHandler(ev);
     });
-    useOn(`mouseleave.${namespace}`, () => {
-      enter = false;
-      mouseHandler();
+    useOn(`mouseleave.${namespace}`, (ev) => {
+      if (!draging) {
+        enter = false;
+        mouseHandler(ev);
+      }
     });
   }
 
   if (styles.active) {
-    const stage = shape.getStage();
-    stage.on(`click.${namespace}${shape.id()}`, (evt) => {
-      active = evt.target === shape;
-      mouseHandler();
+    useOn(`click.${namespace}`, (ev) => {
+      if (!styles.active) return;
+      if (draging) return;
+      active = true;
+      mouseHandler(ev);
+
+      setTimeout(() => {
+        const stage = shape.getStage();
+        stage.on(`click.${namespace}${shape.id()}`, (evt) => {
+          if (evt.target !== shape) {
+            active = false;
+            mouseHandler(evt);
+          }
+          setTimeout(() => {
+            stage.off(`click.${namespace}${shape.id()}`);
+          });
+        });
+      });
     });
   }
 
-  if (styles.draging) {
-    useOn(`dragstart.${namespace} mousedown.${namespace}`, () => {
-      draging = true;
-      mouseHandler();
-    });
-    useOn(`seup.${namespace} dragend.${namespace}`, () => {
+  useOn(`dragstart.${namespace}`, (ev) => {
+    draging = true;
+    mouseHandler(ev);
+  });
+  useOn(`dragend.${namespace}`, (ev) => {
+    setTimeout(() => {
       draging = false;
-    });
-  }
+      mouseHandler(ev);
+    }, 16);
+  });
 
   return () => {
     shape.listening(false);
     shape.off(useEvents.join(" "));
-    if (styles.active) {
-      stage.off(`click.${namespace}${shape.id()}`);
-    }
   };
 };
 
 export type DragHandlers<T> = {
-  readyHandler?: () => T;
-  moveHandler: (move: number[], readyData: T) => void;
-  endHandler?: (readyData: T) => void;
+  readyHandler?: (ev: KonvaEventObject<any>) => T;
+  moveHandler: (
+    move: number[],
+    readyData: T,
+    ev: KonvaEventObject<any>
+  ) => void;
+  endHandler?: (readyData: T, ev: KonvaEventObject<any>) => void;
 };
 
 export const openShapeDrag = <T extends Shape | Group, D>(
   shape: T,
-  handler: DragHandlers<D>
+  handler: DragHandlers<D>,
+  transform = true,
+  tfIncludeSelf = false
 ) => {
   let readlyData = null as D;
 
   shape.draggable(true);
-  shape.dragBoundFunc((pos) => {
-    handler.moveHandler([pos.x, pos.y], readlyData);
+  shape.dragBoundFunc((pos, ev) => {
+    let move = pos;
+    if (transform) {
+      const tf = getAbsoluteTransform(shape, tfIncludeSelf).invert();
+      move = tf.point(pos);
+    }
+    handler.moveHandler([move.x, move.y], readlyData, ev);
     return shape.absolutePosition();
   });
 
   if (handler.readyHandler) {
-    shape.on("dragstart.drag", () => {
-      readlyData = handler.readyHandler();
+    shape.on("dragstart.drag", (ev) => {
+      readlyData = handler.readyHandler(ev);
     });
   }
 
   if (handler.endHandler) {
-    shape.on("dragend.drag", () => {
-      handler.endHandler(readlyData);
+    shape.on("dragend.drag", (ev) => {
+      handler.endHandler(readlyData, ev);
     });
   }
   return () => {
@@ -120,23 +133,68 @@ export const openShapeDrag = <T extends Shape | Group, D>(
 };
 
 export type EntityDragHandlers<R extends Attrib, T> = {
-  readyHandler?: (attrib: R) => T;
-  moveHandler: (attrib: R, move: number[], readyData: T) => void;
-  endHandler?: (attrib: R, readyData: T) => void;
+  readyHandler?: (attrib: R, ev: KonvaEventObject<any>) => T;
+  moveHandler: (
+    attrib: R,
+    move: number[],
+    readyData: T,
+    ev: KonvaEventObject<any>
+  ) => void;
+  endHandler?: (attrib: R, readyData: T, ev: KonvaEventObject<any>) => void;
 };
 export const openEntityDrag = <T extends Attrib, S extends ShapeType, R>(
   entity: Entity<T, S>,
   handler: EntityDragHandlers<T, R>
 ) => {
   entity.enableDrag({
-    readyHandler() {
-      return handler.readyHandler(entity.attrib);
+    readyHandler(ev) {
+      return handler.readyHandler && handler.readyHandler(entity.attrib, ev);
     },
-    moveHandler(move: number[], readyData: R) {
-      return handler.moveHandler(entity.attrib, move, readyData);
+    moveHandler(move: number[], readyData: R, ev) {
+      return handler.moveHandler(entity.attrib, move, readyData, ev);
     },
-    endHandler(readlyData: R) {
-      return handler.endHandler(entity.attrib, readlyData);
+    endHandler(readlyData: R, ev) {
+      return (
+        handler.endHandler && handler.endHandler(entity.attrib, readlyData, ev)
+      );
     },
   });
 };
+
+export const shapeTreeEq = (
+  parent: ShapeType,
+  eq: (shape: ShapeType) => boolean
+) => {
+  if (eq(parent)) {
+    return parent;
+  }
+
+  if ("children" in parent) {
+    for (const child of parent.children) {
+      const e = shapeTreeEq(child, eq);
+      if (e) {
+        return child;
+      }
+    }
+  }
+
+  return null;
+};
+
+export const shapeParentsEq = (
+  target: ShapeType,
+  eq: (shape: ShapeType) => boolean
+) => {
+  while (target) {
+    if (eq(target)) {
+      return target;
+    }
+    target = target.parent as any;
+  }
+  return null;
+};
+
+export const shapeTreeContain = (parent: ShapeType, target: ShapeType) => {
+  const eqShape = shapeTreeEq(parent, (shape) => shape === target);
+  return !!eqShape;
+};

+ 13 - 3
src/board/shared/util.ts

@@ -4,12 +4,14 @@ import { inRevise, toRawType } from "./public";
 import { Shape } from "konva/lib/Shape";
 import { Group } from "konva/lib/Group";
 
-export const setShapeConfig = <T extends Shape | Group>(
+export const setShapeConfig = <T extends Group | Shape>(
   shape: T,
   config: GetSetPick<T>
 ) => {
   for (const key in config) {
-    shape[key as any](config[key]);
+    if (typeof shape[key as any] === "function") {
+      shape[key as any](config[key]);
+    }
   }
 };
 
@@ -54,7 +56,7 @@ export const watchAttribs = (
     (newAttribs, oldAttribs = []) => {
       callback(getChangeAllPoart(newAttribs, oldAttribs));
     },
-    { immediate, flush: "pre" }
+    { immediate, flush: "sync" }
   );
 };
 
@@ -115,10 +117,18 @@ export const depPartialUpdate = <T>(newData: T, oldData: T): T => {
   if (!inRevise(newData, oldData)) {
     return oldData;
   }
+  if (!oldData) {
+    return newData;
+  }
+
   const nData = newData as any,
     oData = oldData as any;
   const type = toRawType(nData);
 
+  if (toRawType(oldData) !== type) {
+    return newData;
+  }
+
   switch (type) {
     case "Array":
       for (let i = 0; i < nData.length; i++) {

+ 20 - 6
src/board/type.d.ts

@@ -3,6 +3,7 @@ import { Shape } from "konva/lib/Shape";
 import { Group } from "konva/lib/Group";
 import { Layer } from "konva/lib/Layer";
 import { Stage } from "konva/lib/Stage";
+import { Container } from "./packages";
 
 type ShapeType = Group | Layer | Stage | Shape;
 
@@ -13,15 +14,28 @@ export type GetSetPick<T extends Shape | Group> = {
   [key in keyof T]?: T[key] extends GetSet<infer V, T> ? V : never;
 };
 
-export type ShapeStyles<T extends Shape | Group> = {
-  common: GetSetPick<T> | (() => void);
-  hover?: GetSetPick<T> | (() => void);
-  active?: GetSetPick<T> | (() => void);
-  draging?: GetSetPick<T> | (() => void);
+export type ShapeStyles = {
+  common: (ev: KonvaEventObject<any>) => void;
+  hover?: (ev: KonvaEventObject<any>) => void;
+  active?: (ev: KonvaEventObject<any>) => void;
+  draging?: (ev: KonvaEventObject<any>) => void;
 };
 
 export type Attrib = {
   id: string;
 };
 
-export type GetShape = <T extends Shape | Group>(style: GetSetPick<T>) => T;
+export type CustomizeShape<T, R = Shape | Group> = {
+  shape: R;
+  setData: (data: T) => void;
+} & ShapeStyles;
+
+export type CustomizeShapeFactory<R, T, S = Shape | Group, A = any> = (
+  data: R,
+  tree: A
+) => CustomizeShape<T, S, A>;
+
+export type Plugin = {
+  setTree(tree: Container);
+  destory?: () => void;
+};

+ 1 - 1
src/components/query-board/index.vue

@@ -23,7 +23,7 @@ const initBoard = register({ rooms: EditWholeLine });
 watch(containerRef, (container, _, onClanup) => {
   if (container) {
     const board = initBoard(container, storeData, false);
-    console.log(board.getData());
+    console.log(board.tree);
     onClanup(() => board.destory);
   }
 });