Explorar o código

feat: 架构搭建

bill hai 1 ano
achega
11538378f6
Modificáronse 50 ficheiros con 4909 adicións e 0 borrados
  1. 24 0
      .gitignore
  2. 3 0
      .vscode/extensions.json
  3. 9 0
      README.md
  4. 12 0
      index.html
  5. 25 0
      package.json
  6. 1014 0
      pnpm-lock.yaml
  7. 13 0
      src/App.vue
  8. 1 0
      src/assets/vue.svg
  9. 1 0
      src/board/env.ts
  10. 105 0
      src/board/packages/container.ts
  11. 198 0
      src/board/packages/entity.ts
  12. 1 0
      src/board/packages/index.ts
  13. 99 0
      src/board/packages/whole-line/editable/edit-whole-line.ts
  14. 2 0
      src/board/packages/whole-line/helper/index.ts
  15. 162 0
      src/board/packages/whole-line/helper/whole-line-line-helper.ts
  16. 55 0
      src/board/packages/whole-line/helper/whole-line-point-helper.ts
  17. 8 0
      src/board/packages/whole-line/index.ts
  18. 5 0
      src/board/packages/whole-line/service/constant.ts
  19. 15 0
      src/board/packages/whole-line/service/getWholeLinePointMerge.ts
  20. 4 0
      src/board/packages/whole-line/service/index.ts
  21. 453 0
      src/board/packages/whole-line/service/whole-line-base.ts
  22. 20 0
      src/board/packages/whole-line/service/whole-line-normal.ts
  23. 437 0
      src/board/packages/whole-line/service/whole-line-tear-merge.ts
  24. 27 0
      src/board/packages/whole-line/shapes.ts
  25. 38 0
      src/board/packages/whole-line/style.ts
  26. 4 0
      src/board/packages/whole-line/view/index.ts
  27. 56 0
      src/board/packages/whole-line/view/whole-line-line.ts
  28. 31 0
      src/board/packages/whole-line/view/whole-line-point.ts
  29. 18 0
      src/board/packages/whole-line/view/whole-line-polygon.ts
  30. 153 0
      src/board/packages/whole-line/view/whole-line.ts
  31. 30 0
      src/board/register.ts
  32. 76 0
      src/board/shared/entity-utils.ts
  33. 296 0
      src/board/shared/math.ts
  34. 160 0
      src/board/shared/package-base.ts
  35. 67 0
      src/board/shared/public.ts
  36. 194 0
      src/board/shared/shape-drag.ts
  37. 310 0
      src/board/shared/shape-mose.ts
  38. 215 0
      src/board/shared/util.ts
  39. 57 0
      src/board/store.ts
  40. 66 0
      src/board/type.d.ts
  41. 45 0
      src/components/query-board/index.vue
  42. 230 0
      src/components/query-board/storeData.json
  43. 97 0
      src/components/query-board/streData-merge.json
  44. 5 0
      src/main.ts
  45. 4 0
      src/style.css
  46. 11 0
      src/util/index.ts
  47. 1 0
      src/vite-env.d.ts
  48. 34 0
      tsconfig.json
  49. 11 0
      tsconfig.node.json
  50. 7 0
      vite.config.ts

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 9 - 0
README.md

@@ -0,0 +1,9 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+## Recommended Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (previously Volar) and disable Vetur
+
+- Use [vue-tsc](https://github.com/vuejs/language-tools/tree/master/packages/tsc) for performing the same type checking from the command line, or for generating d.ts files for SFCs.

+ 12 - 0
index.html

@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>drawing-board</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 25 - 0
package.json

@@ -0,0 +1,25 @@
+{
+  "name": "drawing-board",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vue-tsc && vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "element-plus": "^2.7.3",
+    "konva": "^9.3.6",
+    "sass": "^1.77.1",
+    "three": "^0.164.1",
+    "vue": "^3.4.21"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.0.4",
+    "@types/three": "^0.164.0",
+    "typescript": "^5.2.2",
+    "vite": "^5.2.0",
+    "vue-tsc": "^2.0.6"
+  }
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1014 - 0
pnpm-lock.yaml


+ 13 - 0
src/App.vue

@@ -0,0 +1,13 @@
+
+<template>
+  <QueryBoard :width="width" :height="height" />
+</template>
+
+<script setup lang="ts">
+import QueryBoard from './components/query-board/index.vue'
+
+const width = window.innerWidth - 50
+const height = window.innerHeight - 50
+
+
+</script>

+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 1 - 0
src/board/env.ts

@@ -0,0 +1 @@
+export const DEV = true;

+ 105 - 0
src/board/packages/container.ts

@@ -0,0 +1,105 @@
+import { reactive, watch } from "vue";
+import { Attrib, EntityClass } from "../type";
+import { Layer } from "konva/lib/Layer";
+import { PackageBase } from "../shared/package-base";
+import { Stage } from "konva/lib/Stage";
+import {
+  attribsJoinEntity,
+  depPartialUpdate,
+  getChangePart,
+} from "../shared/util";
+import { mergeFuns } from "../../util";
+
+export type ContainerProps<T extends string, R extends Attrib> = {
+  types: {
+    [key in T]: EntityClass<R>;
+  };
+  data?: { [key in T]?: R[] };
+  dom: HTMLDivElement;
+};
+
+export class Container<T extends string, R extends Attrib> extends PackageBase<
+  Attrib,
+  Stage
+> {
+  props: ContainerProps<T, R>;
+  data: { [key in T]?: R[] };
+  entrys = {} as { [key in T]: InstanceType<EntityClass<R>>[] };
+  viewLayer: Layer;
+  tempLayer: Layer;
+
+  constructor(props: ContainerProps<T, R>) {
+    super({ name: "container", attrib: { id: "0" } });
+    this.props = props;
+    this.data = reactive(props.data || {});
+    for (const key in props.types) {
+      this.data[key] = this.data[key] || [];
+    }
+  }
+
+  setData(data: { [key in T]?: R[] }) {
+    this.data = depPartialUpdate(data, this.data);
+  }
+
+  initShape() {
+    const { dom } = this.props;
+    const stage = new Stage({
+      container: dom,
+      width: dom.offsetWidth,
+      height: dom.offsetHeight,
+    });
+
+    this.viewLayer = new Layer();
+    this.tempLayer = new Layer();
+
+    stage.add(this.viewLayer, this.tempLayer);
+    return stage;
+  }
+
+  initView() {
+    for (const [key, data] of Object.entries(this.data)) {
+      this.entrys[key] = (data as R[]).map((attrib) => {
+        const entry = new this.props.types[key]({ attrib });
+        entry.init();
+        entry.attachBefore();
+        entry.setParent(this.viewLayer);
+      });
+    }
+  }
+
+  initReactive() {
+    const childDestoryMaps: { [key in string]: () => void } = {};
+    return mergeFuns(
+      watch(
+        () => Object.keys(this.data),
+        (keys, oldKeys) => {
+          const { addPort, delPort } = getChangePart(keys, oldKeys);
+          delPort.forEach((delKey) => {
+            childDestoryMaps[delKey]();
+            this.entrys[delKey] = [];
+          });
+
+          addPort.forEach((key) => {
+            const join = attribsJoinEntity(
+              this.data[key],
+              this.props.types[key],
+              null,
+              true,
+              (t) => {
+                setTimeout(() => {
+                  this.viewLayer.add(t.shape);
+                });
+              }
+            );
+            this.entrys[key] = join.entitys;
+            childDestoryMaps[key] = join.destory;
+          });
+        },
+        { immediate: true }
+      ),
+      () => {
+        mergeFuns(Object.values(childDestoryMaps))();
+      }
+    );
+  }
+}

+ 198 - 0
src/board/packages/entity.ts

@@ -0,0 +1,198 @@
+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";
+import { depPartialUpdate } from "../shared/util";
+import { watchEffect } from "vue";
+import {
+  DragHandlers,
+  openShapeDrag,
+  openShapeMouseStyles,
+} from "../shared/shape-mose";
+
+export type EntityProps<T extends Attrib> = {
+  name?: string;
+  attrib: T;
+  zIndex?: number;
+  readonly?: boolean;
+  reactive?: boolean;
+};
+
+type ParentShapeType<T extends ShapeType> = T extends Layer
+  ? Stage
+  : T extends Stage
+  ? null
+  : Group;
+
+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;
+
+export abstract class Entity<T extends Attrib, S extends ShapeType> {
+  props: EntityProps<T>;
+  attrib: T;
+  shape: S;
+  name: string;
+
+  private zIndex: number;
+  private teleport: ParentShapeType<S>;
+
+  children: Entity<Attrib, ShapeType>[] = [];
+  parent: Entity<Attrib, ShapeType>;
+
+  constructor(props: EntityProps<T>) {
+    this.name = props.name;
+    this.props = props;
+    this.zIndex = props.zIndex || 0;
+  }
+
+  abstract initShape(): S;
+  getShape() {
+    return this.shape;
+  }
+
+  abstract diffRedraw(): void;
+
+  setAttrib(newAttrib: Omit<T, "id">) {
+    this.attrib = depPartialUpdate(
+      { ...newAttrib, id: this.attrib.id } as T,
+      this.attrib
+    );
+    this.props.reactive || this.diffRedraw();
+  }
+
+  private destoryReactive: () => void;
+  protected initReactive() {
+    this.destoryReactive && this.destoryReactive();
+    return watchEffect(() => {
+      this.diffRedraw();
+    });
+  }
+
+  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>) {
+    if (this.teleport === tel) {
+      return;
+    } else if (this.shape instanceof Stage) {
+      throw "stage 为顶级容器无法挂载到";
+    } else if (this.shape instanceof Layer && !(tel instanceof Stage)) {
+      throw "layer 只能挂载到 Stage";
+    }
+    const parentShape = (tel || this.parent.shape) as Layer;
+    this.shape.remove();
+    parentShape.add(this.shape);
+    this.setZIndex(this.zIndex);
+    this.teleport = tel;
+  }
+
+  setParent(parent: Entity<any, any> | null) {
+    if (this.parent) {
+      const ndx = this.parent.children.indexOf(this as any);
+      ~ndx && this.parent.children.splice(ndx, 1);
+    }
+    this.parent = parent;
+    if (parent) {
+      this.parent.children.push(this as any);
+      this.setTeleport();
+    }
+  }
+
+  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;
+      }
+    }
+    return 0;
+  }
+
+  setZIndex(index: number) {
+    this.zIndex = index;
+    const packNdx = this.getLevelNdx();
+    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 parentShape = this.teleport || (this.parent.shape as any);
+    const levelShapes = parentShape.children;
+    const beforeShapeNdx =
+      packNdx === 0 ? -1 : levelShapes.indexOf(packChild[packNdx - 1]);
+
+    if (~beforeShapeNdx) {
+      for (let i = beforeShapeNdx + 1; i < levelShapes.length; i++) {
+        parentShape.add(levelShapes[i]);
+      }
+    } else {
+      parentShape.add(this.shape);
+    }
+  }
+
+  private dragDestory: () => void;
+  enableDrag(draghandler: DragHandlers<S>) {
+    this.disableDrag();
+    this.dragDestory = openShapeDrag(this.shape, draghandler);
+  }
+  disableDrag() {
+    this.dragDestory();
+    this.dragDestory = null;
+  }
+
+  private activeDestory: () => void;
+  enableActive(activeCallback: (isActive: boolean) => void) {
+    this.disableActive();
+    this.activeDestory = openShapeMouseStyles(this.shape, {
+      active: () => activeCallback(true),
+      common: () => activeCallback(false),
+    });
+  }
+  disableActive() {
+    this.activeDestory && this.activeDestory();
+    this.activeDestory = null;
+  }
+
+  private mouseActDestory: () => void;
+  enableMouseAct(styles: ShapeStyles<Shape>) {
+    this.disableMouseAct();
+    this.mouseActDestory = openShapeMouseStyles(this.shape, styles as any);
+  }
+  disableMouseAct() {
+    this.mouseActDestory && this.mouseActDestory();
+    this.mouseActDestory = null;
+  }
+
+  destory() {
+    this.setParent(null);
+    this.destoryReactive && this.destoryReactive();
+    this.disableDrag();
+    this.disableActive();
+    this.disableActive();
+
+    if (DEV) {
+      console.log(this.name, "destory");
+    }
+    if (this.shape instanceof konva.Group) {
+      this.shape.destroyChildren();
+    } else {
+      this.shape.destroy();
+    }
+  }
+}

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

@@ -0,0 +1 @@
+export * from "./whole-line";

+ 99 - 0
src/board/packages/whole-line/editable/edit-whole-line.ts

@@ -0,0 +1,99 @@
+import { WholeLine, WholeLineLineAttrib, WholeLinePointAttrib } from "../view/";
+import { lineDragStyle, pointDragStyle } from "../style";
+import { mergeFuns } from "../../../../util";
+import {
+  getWholeLineLine,
+  mergeWholeLinePointsByPoint,
+  spliceWholeLineLineByPoint,
+} from "../service";
+import { watchEntitysOpenDrag } from "../../../shared/shape-drag";
+
+export class EditWholeLine extends WholeLine {
+  get points() {
+    return Object.values(this.pointEntitys);
+  }
+
+  get lines() {
+    return Object.values(this.lineEntitys);
+  }
+
+  setPointPositionReady(point: WholeLinePointAttrib) {
+    return [point.x, point.y];
+  }
+
+  setPointPosition(
+    point: WholeLinePointAttrib,
+    move: number[],
+    initPos: number[]
+  ) {
+    point.x = move[0];
+    point.y = move[1];
+  }
+
+  setPointPositionEnd(point: WholeLinePointAttrib, initPos: number[]) {
+    const merge = mergeWholeLinePointsByPoint(this.attrib, point.id);
+    if (merge)
+      spliceWholeLineLineByPoint(this.attrib, merge?.info.main.id || point.id);
+  }
+
+  setLinePositionReady(line: WholeLineLineAttrib) {
+    return getWholeLineLine(this.attrib, line.id).map(({ x, y }) => [x, y]);
+  }
+
+  setLinePosition(
+    line: WholeLineLineAttrib,
+    move: number[],
+    initPositions: number[][]
+  ) {
+    const points = getWholeLineLine(this.attrib, line.id);
+    const currentPositions = initPositions.map((pos) => [
+      pos[0] + move[0],
+      pos[1] + move[1],
+    ]);
+
+    this.setPointPosition(points[0], currentPositions[0], initPositions[0]);
+    this.setPointPosition(points[1], currentPositions[1], initPositions[1]);
+  }
+
+  setLinePositionEnd(line: WholeLineLineAttrib, initPositions: number[][]) {
+    const points = getWholeLineLine(this.attrib, line.id);
+    this.setPointPositionEnd(points[0], initPositions[0]);
+    this.setPointPositionEnd(points[1], initPositions[1]);
+  }
+
+  private stopWatchs: (() => void)[] = [];
+  init() {
+    setTimeout(() => {
+      mergeWholeLinePointsByPoint(this.attrib, "23");
+    });
+    super.init();
+
+    this.stopWatchs.push(
+      // 点位拖拽监听
+      watchEntitysOpenDrag(
+        () => Object.values(this.pointEntitys),
+        {
+          readyHandler: this.setPointPositionReady.bind(this),
+          moveHandler: this.setPointPosition.bind(this),
+          endHandler: this.setPointPositionEnd.bind(this),
+        },
+        pointDragStyle
+      ),
+      // 监听线段拖拽
+      watchEntitysOpenDrag(
+        () => Object.values(this.lineEntitys),
+        {
+          readyHandler: this.setLinePositionReady.bind(this),
+          moveHandler: this.setLinePosition.bind(this),
+          endHandler: this.setLinePositionEnd.bind(this),
+        },
+        lineDragStyle
+      )
+    );
+  }
+
+  destory() {
+    super.destory();
+    mergeFuns(...this.stopWatchs)();
+  }
+}

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

@@ -0,0 +1,2 @@
+export * from "./whole-line-line-helper";
+export * from "./whole-line-point-helper";

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

@@ -0,0 +1,162 @@
+import konva from "konva";
+import { PackageBase, PackageBaseProps } from "../../../shared/package-base";
+import { TArrow, TGroup, TLabel, TText } from "../../../type";
+import { WholeLineLineAttrib } from "../view/whole-line-line";
+import { watch } from "vue";
+import { getLineDireAngle } from "../../../shared/math";
+import { MathUtils } from "three";
+import { WholeLineAttrib } from "../view/whole-line";
+import { lineHelperZIndex } from "../style";
+
+export class PolygonLineHelper extends PackageBase<
+  WholeLineLineAttrib,
+  TGroup
+> {
+  private config: WholeLineAttrib;
+
+  constructor(props: PackageBaseProps<WholeLineLineAttrib>) {
+    props.zIndex = props.zIndex || lineHelperZIndex;
+    props.name = props.name || "line-helper" + props.attrib.id;
+    super(props);
+  }
+
+  setConfig(config: WholeLineAttrib) {
+    this.config = config;
+  }
+
+  initShape() {
+    const label = new konva.Label({
+      opacity: 0.75,
+      name: "label",
+      listening: false,
+    });
+    label.add(
+      new konva.Tag({
+        name: "tag",
+        fill: "rgba(0, 0, 0, 0.8)",
+        pointerDirection: "down",
+        pointerWidth: 5,
+        pointerHeight: 5,
+        lineJoin: "round",
+        shadowColor: "black",
+        shadowBlur: 10,
+        shadowOffsetX: 10,
+        shadowOffsetY: 10,
+        shadowOpacity: 0.5,
+        listening: false,
+      }),
+      new konva.Text({
+        name: "text",
+        text: `text`,
+        fontFamily: "Calibri",
+        fontSize: 10,
+        padding: 3,
+        fill: "white",
+        listening: false,
+      })
+    );
+
+    const arrowSize = 8;
+
+    const shape = new konva.Group();
+    shape.add(label).add(
+      new konva.Arrow({
+        name: "arrow-1",
+        visible: false,
+        points: [0, 0],
+        pointerLength: arrowSize,
+        pointerWidth: arrowSize,
+        fill: "black",
+        stroke: "black",
+        strokeWidth: 4,
+        listening: false,
+      }),
+      new konva.Arrow({
+        name: "arrow-2",
+        visible: false,
+        points: [0, 0],
+        pointerLength: arrowSize,
+        pointerWidth: arrowSize,
+        fill: "black",
+        stroke: "black",
+        strokeWidth: 4,
+        listening: false,
+      })
+    );
+    return shape;
+  }
+
+  initReactive() {
+    return watch(
+      () => {
+        const points = this.attrib.pointIds.map((id) =>
+          this.config.points.find((point) => point.id === id)
+        );
+        if (points.some((point) => !point)) {
+          return null;
+        }
+
+        let twoWay = false;
+        const labels: string[] = [];
+        for (const polygon of this.config.polygons) {
+          for (const lineId of polygon.lineIds) {
+            const line = this.config.lines.find(({ id }) => id === lineId);
+            if (
+              line.pointIds.includes(this.attrib.pointIds[0]) &&
+              line.pointIds.includes(this.attrib.pointIds[1])
+            ) {
+              labels.push(`${line.id} [${line.pointIds.join(",")}]`);
+
+              twoWay = twoWay || line.pointIds[0] === this.attrib.pointIds[1];
+            }
+          }
+        }
+
+        const coords: number[] = [];
+        points.forEach(({ x, y }, ndx) => {
+          coords[ndx * 2] = x;
+          coords[ndx * 2 + 1] = y;
+        });
+        return {
+          twoWay,
+          label: labels.join(" | "),
+          coords: coords,
+        };
+      },
+      (data) => {
+        if (!data) return;
+        if (!data.label) {
+          console.log(this.attrib);
+        }
+
+        const angle = MathUtils.radToDeg(getLineDireAngle(data.coords, [0, 1]));
+        this.shape
+          .findOne<TLabel>(".label")
+          .x((data.coords[0] + data.coords[2]) / 2)
+          .y((data.coords[1] + data.coords[3]) / 2)
+          .rotation(angle < 0 ? angle + 90 : angle - 90);
+
+        this.shape
+          .findOne<TArrow>(".arrow-1")
+          .x(data.coords[2])
+          .y(data.coords[3])
+          .rotation(angle + 90)
+          .visible(true);
+
+        if (data.twoWay) {
+          this.shape
+            .findOne<TArrow>(".arrow-2")
+            .x(data.coords[0])
+            .y(data.coords[1])
+            .rotation(angle - 90)
+            .visible(true);
+        } else {
+          this.shape.findOne<TArrow>(".arrow-2").visible(false);
+        }
+
+        this.shape.findOne<TText>(".text").text(data.label);
+      },
+      { immediate: true }
+    );
+  }
+}

+ 55 - 0
src/board/packages/whole-line/helper/whole-line-point-helper.ts

@@ -0,0 +1,55 @@
+import konva from "konva";
+import { PackageBase, PackageBaseProps } from "../../../shared/package-base";
+import { TLabel } from "../../../type";
+import { watch } from "vue";
+import { WholeLinePointAttrib } from "../view/whole-line-point";
+import { pointHelperZIndex } from "../style";
+
+export class WholeLinePointHelper extends PackageBase<
+  WholeLinePointAttrib,
+  TLabel
+> {
+  constructor(props: PackageBaseProps<WholeLinePointAttrib>) {
+    props.zIndex = props.zIndex || pointHelperZIndex;
+    props.name = props.name || "line-helper" + props.attrib.id;
+    super(props);
+  }
+
+  initShape() {
+    const label = new konva.Label({ opacity: 0.75, name: "label" });
+    label.add(
+      new konva.Tag({
+        name: "tag",
+        fill: "rgba(0, 0, 0, 0.8)",
+        pointerDirection: "down",
+        pointerWidth: 5,
+        pointerHeight: 5,
+        lineJoin: "round",
+        shadowColor: "black",
+        shadowBlur: 10,
+        shadowOffsetX: 10,
+        shadowOffsetY: 10,
+        shadowOpacity: 0.5,
+      }),
+      new konva.Text({
+        name: "text",
+        text: `P${this.attrib.id}`,
+        fontFamily: "Calibri",
+        fontSize: 10,
+        padding: 3,
+        fill: "white",
+      })
+    );
+    return label;
+  }
+
+  initReactive() {
+    return watch(
+      this.attrib,
+      (data) => {
+        this.shape.x(data.x).y(data.y);
+      },
+      { immediate: true, deep: true }
+    );
+  }
+}

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

@@ -0,0 +1,8 @@
+export * as wholeLine 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";

+ 5 - 0
src/board/packages/whole-line/service/constant.ts

@@ -0,0 +1,5 @@
+// 点位相同距离
+export const WHOLE_LINE_POINT_EQ_DIST = 0.001;
+
+// 需要合并距离
+export const WHOLE_LINE_POINT_MERGE_DIST = 10;

+ 15 - 0
src/board/packages/whole-line/service/getWholeLinePointMerge.ts

@@ -0,0 +1,15 @@
+import { WholeLineAttrib, WholeLinePoint } from "../view/whole-line";
+import { getWholeLinePolygonLinesByPoint } from "./whole-line-base";
+
+/**
+ * 重复获取点合并结果
+ * @param config WholeLine
+ * @param point 操作点
+ */
+export const getWholeLinePointMerge = (
+  config: WholeLineAttrib,
+  point: WholeLinePoint
+) => {
+  getWholeLinePolygonLinesByPoint(config);
+  config.points;
+};

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

@@ -0,0 +1,4 @@
+export * from "./constant";
+export * from "./whole-line-base";
+export * from "./whole-line-tear-merge";
+export * from "./whole-line-normal";

+ 453 - 0
src/board/packages/whole-line/service/whole-line-base.ts

@@ -0,0 +1,453 @@
+import {
+  WholeLineAttrib,
+  WholeLineLineAttrib,
+  WholeLinePointAttrib,
+  WholeLinePolygonAttrib,
+} from "../view";
+
+export const generateWholeLineLineId = (config: WholeLineAttrib) =>
+  (Math.max(...config.lines.map(({ id }) => Number(id))) + 1).toString();
+
+export const generateWholeLinePointId = (config: WholeLineAttrib) =>
+  (Math.max(...config.points.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));
+  } else {
+    return config.points;
+  }
+};
+
+export const getWholeLineLineRaw = (config: WholeLineAttrib, lineId: string) =>
+  config.lines.find(({ id }) => id === lineId);
+
+export const getWholeLineLinesRaw = (
+  config: WholeLineAttrib,
+  lineIds: string[]
+) => lineIds.map((lineId) => getWholeLineLineRaw(config, lineId));
+
+export const getWholeLineLine = (config: WholeLineAttrib, lineId: string) =>
+  getWholeLinePoints(config, getWholeLineLineRaw(config, lineId).pointIds);
+
+export const getWholeLineLines = (config: WholeLineAttrib) => {
+  return config.lines.map((p) => getWholeLinePoints(config, p.pointIds));
+};
+
+// getWholeLinePolygon
+export const getWholeLinePolygonRaw = (config: WholeLineAttrib, id: string) => {
+  return config.polygons.find((p) => p.id === id);
+};
+
+export const getWholeLinePolygonLines = (
+  config: WholeLineAttrib,
+  polygonId: string
+) => {
+  return getWholeLinePolygonRaw(config, polygonId).lineIds.map((lineId) =>
+    getWholeLineLine(config, lineId)
+  );
+};
+
+export const getWholeLinePolygonLinesRaw = (
+  config: WholeLineAttrib,
+  polygonId: string
+) => {
+  return getWholeLinePolygonRaw(config, polygonId).lineIds.map((lineId) =>
+    getWholeLineLineRaw(config, lineId)
+  );
+};
+
+// getWholeLineLinesByPoint
+export const getWholeLineLinesByPointId = (
+  config: WholeLineAttrib,
+  pointId: string
+) =>
+  config.lines
+    .filter(({ pointIds }) => pointIds.includes(pointId))
+    .map((line) => getWholeLineLine(config, line.id));
+
+// getWholeLinePolygonLinesByPoint
+export const getWholeLinePolygonLinesByPoint = (
+  config: WholeLineAttrib,
+  pointId: string
+) => {
+  const pLines: { polygonId: string; lines: WholeLinePointAttrib[][] }[] = [];
+  for (const polygon of config.polygons) {
+    const lines = polygon.lineIds
+      .filter((lId) =>
+        getWholeLineLineRaw(config, lId).pointIds.includes(pointId)
+      )
+      .map((lineId) => getWholeLineLine(config, lineId));
+
+    if (lines.length) {
+      pLines.push({
+        polygonId: polygon.id,
+        lines,
+      });
+    }
+  }
+  return pLines;
+};
+
+export const getWholeLinePolygonPoints = (
+  config: WholeLineAttrib,
+  polygonId: string
+) => {
+  const { lineIds } = getWholeLinePolygonRaw(config, polygonId);
+  const points: WholeLinePointAttrib[] = [];
+  for (let i = 0; i < lineIds.length; i++) {
+    const lineRaw = getWholeLineLineRaw(config, lineIds[i]);
+    points.push(getWholeLinePoint(config, lineRaw.pointIds[0]));
+    if (i === lineIds.length - 1) {
+      points.push(getWholeLinePoint(config, lineRaw.pointIds[1]));
+    }
+  }
+  return points;
+};
+
+export type WholeLineChange = {
+  lineChange?: {
+    del?: WholeLineLineAttrib[];
+    update?: { before: WholeLineLineAttrib[]; after: WholeLineLineAttrib[] }[];
+    add?: WholeLineLineAttrib[];
+  };
+  polygonChange?: {
+    add?: WholeLinePolygonAttrib[];
+    update?: {
+      before: WholeLinePolygonAttrib;
+      after: WholeLinePolygonAttrib;
+    }[];
+    del?: WholeLinePolygonAttrib[];
+  };
+  pointChange?: {
+    add?: WholeLinePointAttrib[];
+    del?: WholeLinePointAttrib[];
+  };
+};
+
+export const getWholeLineLineNdxByPointIds = (
+  config: WholeLineAttrib,
+  qPointIds: string[]
+) =>
+  config.lines.findIndex(
+    ({ pointIds }) =>
+      pointIds[0] === qPointIds[0] && pointIds[1] === qPointIds[1]
+  );
+
+/**
+ * 添加线段,只有config.lines不存在时会添加
+ * @param config WholeLine
+ * @param addPointIds 线段点id[]
+ * @returns 变化
+ */
+export const wholeLineAddLineByPointIds = (
+  config: WholeLineAttrib,
+  addPointIds: string[]
+) => {
+  const change: WholeLineChange = {
+    lineChange: { add: [] },
+  };
+  let eLineNdx = getWholeLineLineNdxByPointIds(config, addPointIds);
+  if (!~eLineNdx) {
+    eLineNdx =
+      config.lines.push({
+        id: generateWholeLineLineId(config),
+        pointIds: [...addPointIds],
+      }) - 1;
+    change.lineChange.add.push(config.lines[eLineNdx]);
+  }
+  return { line: config.lines[eLineNdx], change };
+};
+
+/**
+ * 删除线段,只有polygon没有引用时会删除
+ * @param config WholeLine
+ * @param delPointIds 线段点id[]
+ * @returns 变化
+ */
+export const wholeLineDelLineByPointIds = (
+  config: WholeLineAttrib,
+  delPointIds: string[]
+) => {
+  const change: WholeLineChange = {
+    lineChange: { del: [] },
+    pointChange: { del: [] },
+  };
+  const eLineNdx = getWholeLineLineNdxByPointIds(config, delPointIds);
+  const eLineRaw = config.lines[eLineNdx];
+  if (!~eLineNdx) {
+    return { change };
+  }
+  let canDel = config.polygons.every(
+    ({ lineIds }) => !lineIds.includes(eLineRaw.id)
+  );
+  if (canDel) {
+    config.lines.splice(eLineNdx, 1);
+    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);
+    }
+  });
+
+  return { line: eLineRaw, change };
+};
+
+/**
+ * 删除点,会影响polygons 和lines points
+ * @param config WholeLine
+ * @param delPointIds 要删除的点ids
+ * @returns 变化
+ */
+export const wholeLineDelPointByPointIds = (
+  config: WholeLineAttrib,
+  delPointIds: string[]
+) => {
+  const change: WholeLineChange = {
+    lineChange: {
+      del: [],
+      add: [],
+    },
+    polygonChange: {
+      update: [],
+      del: [],
+    },
+    pointChange: {
+      del: [],
+    },
+  };
+
+  for (let i = 0; i < config.polygons.length; i++) {
+    const polygon = config.polygons[i];
+    const lineIds = polygon.lineIds;
+    const lines = getWholeLineLinesRaw(config, lineIds);
+    let initPolygonLineIds: string[];
+
+    for (let ndx = 0; ndx < lines.length; ndx++) {
+      const lineRaw = lines[ndx];
+      const prev = delPointIds.includes(lineRaw.pointIds[0]);
+      const last = delPointIds.includes(lineRaw.pointIds[1]);
+      if (!(prev || last)) continue;
+      initPolygonLineIds = initPolygonLineIds || [...lineIds];
+
+      // [1,2][2,3][3,4]
+      // delp 2, 3 delL [2, 3] [3, 4] updateL [1, 2] -> [1, 4]
+      // delp 1, 3 delL [1, 2] [3, 4] updateL [2, 3] -> [2, 4]
+      // delp 4 delL [3, 4]
+      // delp 1 delL [1, 2]
+      // delp 3 delL [3, 4] updateL [2, 3] -> [2, 4]
+
+      let delNdx = ndx;
+      if (last) {
+        // delp 3, 4 delL [2, 3][3, 4]
+        // 特殊情况
+        let nextNdx = ndx + 1;
+        for (nextNdx; nextNdx < lines.length; nextNdx++) {
+          const nextPId = lines[nextNdx].pointIds[1];
+          if (
+            !delPointIds.includes(nextPId) &&
+            nextPId !== lineRaw.pointIds[0]
+          ) {
+            break;
+          }
+        }
+        if (nextNdx !== lines.length) {
+          const adL = wholeLineAddLineByPointIds(config, [
+            lineRaw.pointIds[0],
+            lines[nextNdx].pointIds[1],
+          ]);
+          change.lineChange.add.push(...adL.change.lineChange.add);
+          lineIds[ndx] = adL.line.id;
+          delNdx = -1;
+        }
+      }
+      if (~delNdx) {
+        const delPointIds = lines[ndx].pointIds;
+        lineIds.splice(ndx--, 1);
+
+        const dL = wholeLineDelLineByPointIds(config, delPointIds);
+        change.lineChange.del.push(...dL.change.lineChange.del);
+        change.pointChange.del.push(...dL.change.pointChange.del);
+      }
+    }
+
+    if (initPolygonLineIds) {
+      const beforePolygon = { ...polygon, lineIds: initPolygonLineIds };
+      if (polygon.lineIds.length === 0) {
+        config.polygons.splice(i--, 1);
+        change.polygonChange.del.push(beforePolygon);
+      } else {
+        change.polygonChange.update.push({
+          before: beforePolygon,
+          after: polygon,
+        });
+      }
+    }
+  }
+  return change;
+};
+
+/**
+ * 线段加点
+ * @param config
+ * @param polygonLine
+ * @param addPoint
+ * @param effectLines 要修改对应影响的线
+ */
+// wholeLinePolygonLineAddPoint
+export const wholeLineLineAddPoint = (
+  config: WholeLineAttrib,
+  line: WholeLinePointAttrib[],
+  pointId: string
+) => {
+  const change: WholeLineChange = {
+    lineChange: {
+      del: [],
+      add: [],
+    },
+    polygonChange: {
+      update: [],
+    },
+    pointChange: {
+      add: [],
+      del: [],
+    },
+  };
+
+  let addedPoints: string[] | null = null;
+  const lineIds = line.map((p) => p.id);
+  if (lineIds.includes(pointId)) {
+    return { addedPoints, change };
+  }
+
+  for (const polygon of config.polygons) {
+    let initPolygonLineIds: string[];
+    for (let ndx = 0; ndx < polygon.lineIds.length; ndx++) {
+      const lineRaw = getWholeLineLineRaw(config, polygon.lineIds[ndx]);
+      // 如果需要添加的点已经是线段的起点或终点则直接忽略
+      if (
+        lineIds.includes(lineRaw.pointIds[0]) &&
+        lineIds.includes(lineRaw.pointIds[1])
+      ) {
+        initPolygonLineIds = initPolygonLineIds || [...polygon.lineIds];
+        addedPoints = addedPoints || [
+          lineRaw.pointIds[0],
+          pointId,
+          lineRaw.pointIds[1],
+        ];
+        const addl1 = wholeLineAddLineByPointIds(config, [
+          lineRaw.pointIds[0],
+          pointId,
+        ]);
+        // [1,2][2,3] [2,4, 3]
+        // [1.2][2.4],[4.3]
+        const addl2 = wholeLineAddLineByPointIds(config, [
+          pointId,
+          lineRaw.pointIds[1],
+        ]);
+
+        change.lineChange.add.push(
+          ...addl1.change.lineChange.add,
+          ...addl2.change.lineChange.add
+        );
+        polygon.lineIds.splice(ndx, 1, addl1.line.id, addl2.line.id);
+
+        const dl = wholeLineDelLineByPointIds(config, lineRaw.pointIds);
+        change.lineChange.del.push(...dl.change.lineChange.del);
+        change.pointChange.del.push(...dl.change.pointChange.del);
+      }
+    }
+    if (initPolygonLineIds) {
+      change.polygonChange.update.push({
+        before: { ...polygon, lineIds: initPolygonLineIds },
+        after: polygon,
+      });
+    }
+  }
+  return { addedPoints, change };
+};
+
+/**
+ * 替换点
+ * @param config WholeLine
+ * @param originPointIds 原有点集合
+ * @param replacePointId 最要点id
+ * @returns 变化
+ */
+export const wholeLineReplacePoint = (
+  config: WholeLineAttrib,
+  originPointIds: string[],
+  replacePointId: string
+) => {
+  const change: WholeLineChange = {
+    pointChange: {
+      del: [],
+    },
+    lineChange: {
+      del: [],
+      add: [],
+      update: [],
+    },
+    polygonChange: {
+      del: [],
+      update: [],
+    },
+  };
+
+  for (let i = 0; i < config.polygons.length; i++) {
+    const polygon = config.polygons[i];
+    let initPolygonLineIds: string[];
+
+    for (const originPointId of originPointIds) {
+      const lineIds = polygon.lineIds;
+      for (let j = 0; j < lineIds.length; j++) {
+        const line = getWholeLineLineRaw(config, lineIds[j]);
+        let ndx = line.pointIds.indexOf(originPointId);
+        if (!~ndx) continue;
+        initPolygonLineIds = initPolygonLineIds || [...polygon.lineIds];
+
+        const repPoints = [...line.pointIds];
+        repPoints[ndx] = replacePointId;
+
+        // 如果前后是主点则删除线段
+        if (repPoints[0] === repPoints[1]) {
+          polygon.lineIds.splice(j, 1);
+        } else {
+          const adL = wholeLineAddLineByPointIds(config, repPoints);
+          polygon.lineIds[j] = adL.line.id;
+          change.lineChange.del.push(...adL.change.lineChange.add);
+        }
+
+        const dl = wholeLineDelLineByPointIds(config, line.pointIds);
+        change.lineChange.del.push(...dl.change.lineChange.del);
+        change.pointChange.del.push(...dl.change.pointChange.del);
+        j--;
+      }
+      // 如果合并后只剩一个点则直接删除整个polygon
+      if (initPolygonLineIds) {
+        const beforePolygon = { ...polygon, lineIds: initPolygonLineIds };
+        if (polygon.lineIds.length === 0) {
+          change.polygonChange.del.push(beforePolygon);
+          config.polygons.splice(i--, 1);
+        } else {
+          change.polygonChange.update.push({
+            before: beforePolygon,
+            after: polygon,
+          });
+        }
+      }
+    }
+  }
+
+  return change;
+};

+ 20 - 0
src/board/packages/whole-line/service/whole-line-normal.ts

@@ -0,0 +1,20 @@
+import { polygonCounterclockwise } from "../../../shared/math";
+import { WholeLineAttrib } from "../view/whole-line";
+
+export const normalWholeLinePolygon = (
+  config: WholeLineAttrib,
+  polygonId: string
+) => {
+  // const polygon = config.polygons.find(({ id }) => id === polygonId);
+  // if (!polygon) return null;
+  // const points = polygon.points
+  //   .map((id) => config.points.find((point) => point.id === id))
+  //   .filter((point) => !!point);
+  // const flatPoints = points.map(({ x, y }) => [x, y]);
+  // if (!polygonCounterclockwise(flatPoints)) {
+  //   polygon.points.reverse();
+  // }
+  // // 形成闭合,需要确保整体逆时针方向,如果不是则修正
+  // if (points[0] === points[points.length - 1]) {
+  // }
+};

+ 437 - 0
src/board/packages/whole-line/service/whole-line-tear-merge.ts

@@ -0,0 +1,437 @@
+import { DEV } from "../../../env";
+import {
+  getLineIntersection,
+  getLineDist,
+  getLineNearPointDist,
+} from "../../../shared/math";
+import { WholeLineAttrib, WholeLinePointAttrib } from "../view/";
+import {
+  WHOLE_LINE_POINT_EQ_DIST,
+  WHOLE_LINE_POINT_MERGE_DIST,
+} from "./constant";
+import {
+  getWholeLinePoint,
+  getWholeLinePoints,
+  getWholeLinePolygonLines,
+  getWholeLinePolygonLinesByPoint,
+  wholeLineLineAddPoint,
+  wholeLineReplacePoint,
+} from "./whole-line-base";
+
+export const repeatablePushWholeLinePoint = (config: WholeLineAttrib) => {
+  let maxId = Math.max(...config.points.map((point) => Number(point.id) || 0));
+  return (point: Omit<WholeLinePointAttrib, "id">) => {
+    const addPoint = {
+      ...point,
+      id: (++maxId).toString(),
+    };
+    config.points.push(addPoint);
+    return addPoint;
+  };
+};
+
+/**
+ * 重复获取线段与其他线段的交点,加入缓存,提速
+ */
+export type IntersectionsResult = {
+  passive: { polygonId: string; line: WholeLinePointAttrib[] };
+  intersection: WholeLinePointAttrib;
+};
+export const repeatableWholeLineLineIntersections = (
+  config: WholeLineAttrib
+) => {
+  const getWholeLineInterKey = (a: WholeLinePointAttrib[]) =>
+    [a[0].x, a[0].y, a[1].x, a[1].y].join("-");
+  const wholeLineInterCache: { [key in string]: IntersectionsResult[] } = {};
+
+  const getLineInterKeys = (
+    a: WholeLinePointAttrib[],
+    b: WholeLinePointAttrib[]
+  ) => [getWholeLineInterKey(a), getWholeLineInterKey(b)].join("-");
+  const lineInterCache: { [key in string]: WholeLinePointAttrib | null } = {};
+  const addedPonts: WholeLinePointAttrib[] = [];
+  const pushPoint = repeatablePushWholeLinePoint(config);
+
+  /**
+   * 获取wholeLine的线段与其他线段的交点,返回的交点顺序按照离起点距离正序怕排序
+   * @param line 获取的连段
+   * @param otherLines 其他交点
+   * @returns 排序后的交点 以及被切割线段
+   */
+  return (line: WholeLinePointAttrib[]) => {
+    let result: IntersectionsResult[];
+    const wholeLineInterKey = getWholeLineInterKey(line);
+
+    if ((result = wholeLineInterCache[wholeLineInterKey])) {
+      return result;
+    } else if (
+      (result = wholeLineInterCache[getWholeLineInterKey([line[1], line[0]])])
+    ) {
+      // 方向相反
+      result = [...result].reverse();
+      wholeLineInterCache[wholeLineInterKey] = result;
+      return result;
+    }
+
+    result = [];
+    const a = line;
+    for (const polygon of config.polygons) {
+      const polygonLines = getWholeLinePolygonLines(config, polygon.id);
+
+      for (const otherLine of polygonLines) {
+        const key = getLineInterKeys(line, otherLine);
+        const passive = {
+          polygonId: polygon.id,
+          line: otherLine,
+        };
+
+        if (key in lineInterCache) {
+          lineInterCache[key] &&
+            result.push({
+              passive,
+              intersection: lineInterCache[key],
+            });
+        } else {
+          const b = otherLine;
+          const intersection = getLineIntersection(
+            [a[0].x, a[0].y, a[1].x, a[1].y],
+            [b[0].x, b[0].y, b[1].x, b[1].y]
+          );
+          if (intersection) {
+            if (DEV) {
+              console.log("join split", a[0].id, a[1].id, b[0].id, b[1].id);
+            }
+
+            let minNdx = -1;
+            let minDist = Number.MAX_VALUE;
+            for (let ndx = 0; ndx < addedPonts.length; ndx++) {
+              const p1 = [addedPonts[ndx].x, addedPonts[ndx].y];
+              const dist = getLineDist(p1, intersection);
+              if (minDist > dist) {
+                minNdx = ndx;
+                minDist = dist;
+              }
+            }
+            let point: WholeLinePointAttrib;
+            if (minDist <= WHOLE_LINE_POINT_EQ_DIST) {
+              point = addedPonts[minNdx];
+            } else {
+              point = pushPoint({ x: intersection[0], y: intersection[1] });
+              console.log("add", point.id);
+              addedPonts.push(point);
+            }
+            result.push({
+              passive,
+              intersection: point,
+            });
+            lineInterCache[key] = point;
+          } else {
+            lineInterCache[key] = null;
+          }
+        }
+      }
+    }
+
+    // result进行排序
+    const start = [a[0].x, a[0].y];
+    result.sort(
+      (p1, p2) =>
+        getLineDist([p1.intersection.x, p1.intersection.y], start) -
+        getLineDist([p2.intersection.x, p2.intersection.y], start)
+    );
+    wholeLineInterCache[wholeLineInterKey] = result;
+    return result;
+  };
+};
+
+/**
+ * 线段变化级联变更分割线
+ * @param effectLines 分割线
+ * @param refPoint 分割点
+ * @param beforeLine 发起者更改前线段
+ * @param afterLine 发起者更改后线段
+ */
+const lineChangeSplitEffect = (
+  splitLine: WholeLinePointAttrib[],
+  refPoint: WholeLinePointAttrib,
+  beforeLine: WholeLinePointAttrib[],
+  afterPoints: WholeLinePointAttrib[]
+) => {
+  const solitLinePointIds = splitLine.map(({ id }) => id);
+  const isEffect =
+    solitLinePointIds.includes(beforeLine[0].id) &&
+    solitLinePointIds.includes(beforeLine[1].id);
+
+  if (!isEffect) return splitLine;
+
+  let minDist = Number.MAX_VALUE;
+  let minDistNdx = -1;
+  for (let i = 1; i < afterPoints.length; i++) {
+    const afterLine = [afterPoints[i - 1], afterPoints[i]];
+    const dist = getLineNearPointDist(
+      [afterLine[0].x, afterLine[0].y, afterLine[1].x, afterLine[1].y],
+      [refPoint.x, refPoint.y]
+    );
+    if (dist < minDist) {
+      minDist = dist;
+      minDistNdx = i;
+    }
+  }
+
+  if (~minDistNdx) {
+    const sameDire = splitLine[0] === splitLine[0];
+    if (sameDire) {
+      return [afterPoints[minDistNdx - 1], afterPoints[minDistNdx]];
+    } else {
+      return [afterPoints[minDistNdx], afterPoints[minDistNdx - 1]];
+    }
+  } else {
+    return splitLine;
+  }
+};
+
+/**
+ * 通过参考线进行多边形相交线段切割
+ * @param config
+ * @param refLines 参考线
+ */
+export const spliceWholeLineLineByLines = (
+  config: WholeLineAttrib,
+  splitLines: WholeLinePointAttrib[][]
+) => {
+  const getLineSplits = repeatableWholeLineLineIntersections(config);
+
+  const splitInfos: {
+    passive: WholeLinePointAttrib[];
+    splitLine: WholeLinePointAttrib[];
+    intersection: WholeLinePointAttrib;
+  }[] = [];
+
+  // 去重
+  for (let i = 1; i < splitLines.length; i++) {
+    let j = 0;
+    for (; j < i; j++) {
+      if (
+        splitLines[i].includes(splitLines[j][0]) &&
+        splitLines[i].includes(splitLines[j][1])
+      ) {
+        break;
+      }
+    }
+    if (j !== i) {
+      splitLines.splice(i--, 1);
+    }
+  }
+  // const splitLines = polygonsLines.reduce(
+  //   (t, c) => t.concat(c.lines),
+  //   [] as WholeLinePointAttrib[][]
+  // );
+
+  for (const splitLine of splitLines) {
+    const lineSplits = getLineSplits(splitLine);
+    const adL1 = splitLine;
+    lineSplits.forEach((lineSplit) => {
+      const adL2 = [...lineSplit.passive.line];
+      // 去重
+      const repeat = splitInfos.some((etSplitInfo) => {
+        if (lineSplit.intersection.id !== etSplitInfo.intersection.id) {
+          return false;
+        }
+        const etL2 = [...etSplitInfo.passive];
+        const etL1 = [...etSplitInfo.splitLine];
+        return (
+          (etL1.includes(adL1[0]) &&
+            etL1.includes(adL1[1]) &&
+            etL2.includes(adL2[0]) &&
+            etL2.includes(adL2[1])) ||
+          (etL2.includes(adL1[0]) &&
+            etL2.includes(adL1[1]) &&
+            etL1.includes(adL2[0]) &&
+            etL1.includes(adL2[1]))
+        );
+      });
+
+      if (!repeat) {
+        splitInfos.push({
+          splitLine,
+          passive: lineSplit.passive.line,
+          intersection: lineSplit.intersection,
+        });
+      }
+    });
+  }
+
+  for (let i = 0; i < splitInfos.length; i++) {
+    const { passive, splitLine, intersection } = splitInfos[i];
+    const addedPointIds = wholeLineLineAddPoint(
+      config,
+      splitLine,
+      intersection.id
+    ).addedPoints;
+    const passiveAddedPointIds = wholeLineLineAddPoint(
+      config,
+      passive,
+      intersection.id
+    ).addedPoints;
+
+    if (DEV) {
+      if (!addedPointIds) {
+        console.error(
+          "添加 addedPointIds 失败, \n交点:",
+          intersection.id,
+          "\n 线段:",
+          splitLine
+        );
+      }
+      if (!passiveAddedPointIds) {
+        console.error(
+          "添加 passiveAddedPointIds 失败, \n交点:",
+          intersection,
+          "\n线段:",
+          passive
+        );
+      }
+    }
+
+    // ----------切割会污染源数据,需要修改其他未处理过的切割点------
+
+    if (!addedPointIds && !passiveAddedPointIds) {
+      continue;
+    }
+    const addedPoints =
+      addedPointIds && getWholeLinePoints(config, addedPointIds);
+    const passiveAddedPoints =
+      passiveAddedPointIds && getWholeLinePoints(config, passiveAddedPointIds);
+
+    const fixAfter = (
+      line: WholeLinePointAttrib[],
+      intersection: WholeLinePointAttrib
+    ) => {
+      const initLine = line;
+      if (addedPointIds) {
+        line = lineChangeSplitEffect(
+          line,
+          intersection,
+          splitLine,
+          addedPoints
+        );
+      }
+      if (passiveAddedPointIds) {
+        line = lineChangeSplitEffect(
+          line,
+          intersection,
+          passive,
+          passiveAddedPoints
+        );
+      }
+      if (DEV && initLine !== line) {
+        console.log(
+          "fix line",
+          initLine.map(({ id }) => id),
+          "\nafter: ,",
+          line.map(({ id }) => id)
+        );
+      }
+      return line;
+    };
+
+    // 处理被切割线段
+    for (let j = i + 1; j < splitInfos.length; j++) {
+      const after = splitInfos[j];
+      after.passive = fixAfter(after.passive, after.intersection);
+      after.splitLine = fixAfter(after.splitLine, after.intersection);
+    }
+  }
+};
+
+/**
+ * 根据点来进行多边形相交线段切割
+ * @param config wholeLine
+ * @param pointId 操作点id
+ */
+export const spliceWholeLineLineByPoint = (
+  config: WholeLineAttrib,
+  pointId: string
+) => {
+  spliceWholeLineLineByLines(
+    config,
+    getWholeLinePolygonLinesByPoint(config, pointId).reduce((t, c) => {
+      t.push(...c.lines);
+      return t;
+    }, [] as WholeLinePointAttrib[][])
+  );
+};
+
+export type PointMergeResult = {
+  main: WholeLinePointAttrib;
+  merges: WholeLinePointAttrib[];
+  mergeAfterPosition: number[];
+};
+/**
+ * 获取需要合并的点
+ * @param config WholeLine
+ * @param point 操作点
+ */
+export const getWholeLinePointMerge = (
+  config: WholeLineAttrib,
+  point: WholeLinePointAttrib
+) => {
+  const processedPoints = [point];
+  const processPoints = config.points.filter(
+    (oPoint) =>
+      !processedPoints.includes(oPoint) &&
+      getLineDist([oPoint.x, oPoint.y], [point.x, point.y]) <
+        WHOLE_LINE_POINT_MERGE_DIST
+  );
+  if (!processPoints.length) {
+    return null;
+  }
+
+  if (processPoints.length > 1) {
+    return {
+      main: point,
+      merges: processPoints,
+      mergeAfterPosition: [point.x, point.y],
+    };
+  } else {
+    return {
+      main: processPoints[0],
+      merges: [point],
+      mergeAfterPosition: [processPoints[0].x, processPoints[0].y],
+    };
+  }
+};
+
+/**
+ * 通过点id合并距离小于阈值的点
+ * @param config wholeLine
+ * @param pointId 操作点id
+ */
+export const mergeWholeLinePointsByPoint = (
+  config: WholeLineAttrib,
+  pointId: string
+) => {
+  const result = getWholeLinePointMerge(
+    config,
+    getWholeLinePoint(config, pointId)
+  );
+  if (!result) {
+    return;
+  }
+
+  const change = wholeLineReplacePoint(
+    config,
+    result.merges.map(({ id }) => id),
+    result.main.id
+  );
+  const movePoint = getWholeLinePoint(config, result.main.id);
+  if (movePoint) {
+    movePoint.x = result.mergeAfterPosition[0];
+    movePoint.y = result.mergeAfterPosition[1];
+  }
+
+  return {
+    info: result,
+    change,
+  };
+};

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

@@ -0,0 +1,27 @@
+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);
+      },
+    };
+  },
+};

+ 38 - 0
src/board/packages/whole-line/style.ts

@@ -0,0 +1,38 @@
+export const pointStyle = {
+  radius: 7,
+  strokeWidth: 4,
+  fill: "#FF0000",
+  stroke: "#000000",
+};
+
+export const pointDragStyle = {
+  common: pointStyle,
+  hover: {
+    fill: "#00FF00",
+    stroke: "#000000",
+  },
+  active: {
+    fill: "#000000",
+    stroke: "#FF0000",
+  },
+};
+
+export const lineStyle = {
+  stroke: "black",
+  strokeWidth: 5,
+};
+
+export const lineDragStyle = {
+  common: lineStyle,
+  hover: {
+    stroke: "#FF0000",
+  },
+  active: {
+    stroke: "#00FF00",
+  },
+};
+
+export const pointZIndex = 30;
+export const lineZIndex = 1;
+export const lineHelperZIndex = 8;
+export const pointHelperZIndex = 4;

+ 4 - 0
src/board/packages/whole-line/view/index.ts

@@ -0,0 +1,4 @@
+export * from "./whole-line";
+export * from "./whole-line-point";
+export * from "./whole-line-line";
+export * from "./whole-line-polygon";

+ 56 - 0
src/board/packages/whole-line/view/whole-line-line.ts

@@ -0,0 +1,56 @@
+import { wholeLineShapes } from "../shapes";
+import { Attrib } from "../../../type";
+import { lineStyle, lineZIndex } from "../style";
+import { WholeLineAttrib } from "./whole-line";
+import { getWholeLinePoints } from "../service/whole-line-base";
+import { Entity, EntityProps } from "../../entity";
+
+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";
+
+  private config: WholeLineAttrib;
+
+  constructor(props: WholeLineLineProps) {
+    props.zIndex = props.zIndex || lineZIndex;
+    props.name = props.name || WholeLineLine.namespace + props.attrib.id;
+    super(props);
+  }
+
+  private setShapeData: (data: number[]) => void;
+  initShape() {
+    const { shape, setData } = wholeLineShapes.line(lineStyle);
+    this.setShapeData = setData;
+    return shape;
+  }
+
+  diffRedraw(): void {
+    const coords = this.getCoords();
+    if (coords.length) {
+      this.setShapeData(coords);
+    } else {
+      console.error("line:", this.attrib, "找不到对应的点坐标");
+    }
+  }
+
+  setConfig(config: WholeLineAttrib) {
+    this.config = config;
+  }
+
+  getCoords() {
+    const result: number[] = [];
+    const points = getWholeLinePoints(this.config, this.attrib.pointIds);
+    if (!points.some((point) => !point)) {
+      points.forEach(({ x, y }, ndx) => {
+        result[ndx * 2] = x;
+        result[ndx * 2 + 1] = y;
+      });
+    }
+    return result;
+  }
+}

+ 31 - 0
src/board/packages/whole-line/view/whole-line-point.ts

@@ -0,0 +1,31 @@
+import { Attrib } from "../../../type";
+import { pointStyle, pointZIndex } from "../style";
+import { wholeLineShapes } from "../shapes";
+import { Entity, EntityProps } from "../../entity";
+
+export type WholeLinePointAttrib = Attrib & { x: number; y: number };
+
+type PointShape = ReturnType<typeof wholeLineShapes.point>["shape"];
+
+export type WholeLinePointProps = EntityProps<WholeLinePointAttrib>;
+
+export class WholeLinePoint extends Entity<WholeLinePointAttrib, PointShape> {
+  static namespace = "point";
+
+  constructor(props: WholeLinePointProps) {
+    props.zIndex = props.zIndex || pointZIndex;
+    props.name = props.name || WholeLinePoint.namespace + props.attrib.id;
+    super(props);
+  }
+
+  private setShapeData: (data: number[]) => void;
+  initShape() {
+    const { shape, setData } = wholeLineShapes.point(pointStyle);
+    this.setShapeData = setData;
+    return shape;
+  }
+
+  diffRedraw() {
+    this.setShapeData([this.attrib.x, this.attrib.y]);
+  }
+}

+ 18 - 0
src/board/packages/whole-line/view/whole-line-polygon.ts

@@ -0,0 +1,18 @@
+import { Attrib, TGroup } from "../../../type";
+import { PackageBase } from "../../../shared/package-base";
+import { Group } from "konva/lib/Group";
+
+export type WholeLinePolygonAttrib = Attrib & {
+  lineIds: string[];
+};
+
+export class WholeLinePolygon extends PackageBase<
+  WholeLinePolygonAttrib,
+  TGroup
+> {
+  static namespace = "polygon";
+
+  initShape() {
+    return new Group({ id: this.name });
+  }
+}

+ 153 - 0
src/board/packages/whole-line/view/whole-line.ts

@@ -0,0 +1,153 @@
+import { Attrib } from "../../../type";
+import { mergeFuns } from "../../../../util";
+import { attribsJoinEntity, partialComputed } from "../../../shared/util";
+import { DEV } from "../../../env";
+import { WholeLinePointHelper } from "../helper";
+import { WholeLinePoint, WholeLinePointAttrib } from "./whole-line-point";
+import { WholeLineLine, WholeLineLineAttrib } from "./whole-line-line";
+import { WholeLinePolygonAttrib } from "./whole-line-polygon";
+import { PolygonLineHelper } from "../helper/whole-line-line-helper";
+import { Group } from "konva/lib/Group";
+import { Entity, EntityProps } from "../../entity";
+import {
+  IncEntitysFactory,
+  incEntitysFactoryGenerate,
+} from "../../../shared/entity-utils";
+
+export type WholeLineAttrib = Attrib & {
+  points: WholeLinePointAttrib[];
+  lines: WholeLineLineAttrib[];
+  polygons: WholeLinePolygonAttrib[];
+};
+
+export type WholeLineProps = EntityProps<WholeLineAttrib>;
+
+export class WholeLine extends Entity<WholeLineAttrib, Group> {
+  private incLinesFactory: IncEntitysFactory<
+    WholeLineLineAttrib,
+    WholeLineLine
+  >;
+  private incPointsFactory: IncEntitysFactory<
+    WholeLinePointAttrib,
+    WholeLinePoint
+  >;
+
+  constructor(props: WholeLineProps) {
+    props.name = props.name || "whole-line" + props.attrib.id;
+    super(props);
+
+    this.incLinesFactory = incEntitysFactoryGenerate(
+      WholeLineLine,
+      this,
+      (line) => line.setConfig(this.attrib)
+    );
+    this.incPointsFactory = incEntitysFactoryGenerate(WholeLinePoint, this);
+  }
+
+  pointEntitys: Record<string, WholeLinePoint>;
+  lineEntitys: Record<string, WholeLineLine>;
+
+  initShape() {
+    return new Group({ id: this.name });
+  }
+
+  diffRedraw(): void {
+    this.incLinesFactory(this.attrib.lines);
+    this.incPointsFactory(this.attrib.points);
+
+    this.attrib.points.forEach((point) => {
+      const wPoint = new WholeLinePoint({
+        attrib: point,
+        readonly: true,
+      });
+      wPoint.init();
+      this.pointEntitys[wPoint.name] = wPoint;
+      this.shape.add(wPoint.shape);
+    });
+
+    this.attrib.lines.forEach((line) => {
+      const wLine = new WholeLineLine({
+        attrib: line,
+        readonly: true,
+      });
+      wLine.setConfig(this.attrib);
+      wLine.init();
+      this.lineEntitys[wLine.name] = wLine;
+      this.shape.add(wLine.shape);
+
+      return wLine;
+    });
+  }
+
+  init() {
+    super.init();
+  }
+
+  initReactive() {
+    const unReactives: (() => void)[] = [];
+
+    const lineAttribsComputed = partialComputed(() => {
+      // 去重 防止来回线段绘画两次
+      const lines: WholeLineLineAttrib[] = [];
+      for (let i = this.attrib.lines.length - 1; i >= 0; i--) {
+        const a = this.attrib.lines[i];
+        let j = 0;
+        for (j = 0; j < i; j++) {
+          const b = this.attrib.lines[j];
+          if (
+            b.pointIds.includes(a.pointIds[0]) &&
+            b.pointIds.includes(a.pointIds[1])
+          ) {
+            break;
+          }
+        }
+        if (i === j) {
+          lines.push(a);
+        }
+      }
+      return lines;
+    });
+    unReactives.push(lineAttribsComputed.stop);
+
+    const pointJoin = attribsJoinEntity(
+      this.attrib.points,
+      WholeLinePoint,
+      this,
+      true
+    );
+    unReactives.push(pointJoin.destory);
+    this.pointEntitys = pointJoin.entitys;
+    pointJoin.entitys[0];
+
+    const lineJoin = attribsJoinEntity(
+      lineAttribsComputed.data,
+      WholeLineLine,
+      this,
+      true,
+      (line) => line.setConfig(this.attrib)
+    );
+    unReactives.push(lineJoin.destory);
+    this.lineEntitys = lineJoin.entitys;
+
+    if (DEV) {
+      const pointHelperJoin = attribsJoinEntity(
+        this.attrib.points,
+        WholeLinePointHelper,
+        this,
+        true
+      );
+      unReactives.push(pointHelperJoin.destory);
+
+      const lineHelperJoin = attribsJoinEntity(
+        lineAttribsComputed.data,
+        PolygonLineHelper,
+        this,
+        true,
+        (h) => h.setConfig(this.attrib)
+      );
+      unReactives.push(lineHelperJoin.destory);
+    }
+
+    return mergeFuns(...unReactives);
+  }
+}

+ 30 - 0
src/board/register.ts

@@ -0,0 +1,30 @@
+import { shallowReadonly } from "vue";
+import { Attrib, EntityClass } from "./type";
+import { Container } from "./packages/container";
+
+export const register = <T extends string, R extends Attrib>(types: {
+  [key in T]: EntityClass<R>;
+}) => {
+  const initBoard = (dom: HTMLDivElement, data?: { [key in T]?: R[] }) => {
+    const container = new Container({
+      dom,
+      types,
+      data,
+    });
+    container.init();
+
+    return {
+      stage: container,
+      setData(newData: { [key in T]?: R[] }) {
+        container.setData(newData);
+      },
+      getData() {
+        return shallowReadonly(container.data);
+      },
+      destory() {
+        container.destory();
+      },
+    };
+  };
+  return initBoard;
+};

+ 76 - 0
src/board/shared/entity-utils.ts

@@ -0,0 +1,76 @@
+import { EntityType, Entity } from "../packages/entity";
+import { Attrib, ShapeType } from "../type";
+import { getChangeAllPoart } from "./util";
+
+export const entityFactory = <
+  T extends Attrib,
+  S extends ShapeType,
+  C extends EntityType<T, S, { attrib: T }>
+>(
+  attrib: T,
+  Type: C,
+  parent?: Entity<any, any>,
+  extra?: (self: InstanceType<C>) => void
+) => {
+  const entity = new Type({ attrib }) as InstanceType<C>;
+  extra(entity);
+  extra && extra(entity);
+  entity.init();
+  entity.setParent(parent);
+  return entity;
+};
+
+export type IncEntitysFactory<T extends Attrib, E extends Entity<T, any>> = (
+  attribs: T[]
+) => { adds: E[]; dels: E[]; upds: E[] };
+
+// 增量工厂
+export const incEntitysFactoryGenerate = <
+  T extends Attrib,
+  S extends ShapeType,
+  C extends EntityType<T, S, { attrib: T }>
+>(
+  Type: C,
+  parent?: Entity<any, any>,
+  extra?: (self: InstanceType<C>) => void
+) => {
+  let oldAttribs: T[] = [];
+
+  const findAttrib = (attribs: T[], id: Attrib["id"]) =>
+    attribs.find((attrib) => attrib.id === id);
+
+  const cache: { [key in Attrib["id"]]: InstanceType<C> } = {};
+  const destory = (id: Attrib["id"]) => {
+    const delEntity = cache[id];
+    delEntity.destory();
+    delete cache[id];
+    return delEntity;
+  };
+
+  const add = (attrib: T) => {
+    const addEntity = entityFactory(attrib, Type, parent, extra);
+    cache[attrib.id] = addEntity;
+    return addEntity;
+  };
+
+  return (attribs: T[]) => {
+    const { addPort, delPort, changePort } = getChangeAllPoart(
+      attribs,
+      oldAttribs
+    );
+
+    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];
+    });
+
+    oldAttribs = attribs;
+    return {
+      adds,
+      dels,
+      upds,
+    };
+  };
+};

+ 296 - 0
src/board/shared/math.ts

@@ -0,0 +1,296 @@
+import { Vector2, Line3, Vector3, ShapeUtils } from "three";
+
+const getLineDireVector2 = (line: number[]) =>
+  new Vector2(line[2] - line[0], line[3] - line[1]).normalize();
+
+const epsilon = 1e-6; // 误差范围
+/**
+ * 点是否相同
+ * @param p1 点1
+ * @param p2 点2
+ * @returns 是否相等
+ */
+export const eqPoint = (p1: number[], p2: number[]) =>
+  Math.abs(p1[0] - p2[0]) < epsilon && Math.abs(p1[1] - p2[1]) < epsilon;
+
+/**
+ * 获取两点距离
+ * @param p1 点1
+ * @param p2 点2
+ * @returns 距离
+ */
+export const getLineDist = (p1: number[], p2: number[]) => {
+  return new Vector2(p1[0], p1[1]).distanceTo({ x: p2[0], y: p2[1] });
+};
+
+/**
+ * 获取线段方向
+ * @param line 线段
+ * @returns 方向
+ */
+export const getLineDire = (line: number[]) =>
+  getLineDireVector2(line).toArray();
+
+/**
+ * 获取向量的垂直向量
+ * @param dire 原方向
+ * @returns 垂直向量
+ */
+export const getVerticalDire = (dire: number[]) => {
+  const temp = new Vector2(-dire[1], dire[0]);
+  return temp.normalize().toArray();
+};
+
+/**
+ * 获取线段的垂直方向向量
+ * @param line 原线段
+ * @returns 垂直向量
+ */
+export const getVerticaLineDire = (line: number[]) =>
+  getVerticalDire(getLineDire(line));
+
+/**
+ * 获取向量的垂直线段
+ * @param dire 向量
+ * @param origin 线段原点
+ * @param len 线段长度
+ */
+export const getVerticalDireLine = (
+  dire: number[],
+  origin: number[] = [0, 0],
+  len: number = 1
+) => {
+  const vDire = getVerticalDire(dire);
+  const temp = new Vector2(vDire[0], vDire[1]);
+  temp.multiplyScalar(len).add({ x: origin[0], y: origin[1] });
+
+  return [...origin, ...temp.toArray()];
+};
+
+/**
+ * 获取两向量角度(从向量a出发)
+ * @param dire1 向量a
+ * @param dire2 向量b
+ * @returns 两向量夹角弧度, 逆时针为正,顺时针为负
+ */
+export const getDire2Angle = (dire1: number[], dire2: number[]) => {
+  const start = new Vector2(dire1[0], dire1[1]);
+  const end = new Vector2(dire2[0], dire2[1]);
+  const angle = start.angleTo(end);
+  return start.cross(end) > 0 ? angle : -angle;
+};
+
+/**
+ * 获取两线段角度(从线段a出发)
+ * @param line1 线段a
+ * @param line2 线段b
+ * @returns 两线段夹角弧度
+ */
+export const getLine2Angle = (line1: number[], line2: number[]) =>
+  getDire2Angle(getLineDire(line1), getLineDire(line2));
+
+/**
+ * 获取线段与方向的夹角弧度
+ * @param line 线段
+ * @param dire 方向
+ * @returns 线段与方向夹角弧度
+ */
+export const getLineDireAngle = (line: number[], dire: number[]) => {
+  return getDire2Angle(dire, getLineDire(line));
+};
+
+export enum RelationshipEnum {
+  Overlap = "Overlap",
+  Intersect = "Intersect",
+  ExtendIntersect = "ExtendIntersect",
+  Parallel = "Parallel",
+  Join = "Join",
+  Equal = "Equal",
+  None = "None",
+}
+/**
+ * 获取两线段是什么关系,(重叠、相交、平行、首尾相接等)
+ * @param line1
+ * @param line2
+ * @returns RelationshipEnum
+ */
+export const getLineRelationship = (line1: number[], line2: number[]) => {
+  const startP1 = line1.slice(0, 2);
+  const startP2 = line2.slice(0, 2);
+  const endP1 = line1.slice(2, 4);
+  const endP2 = line2.slice(2, 4);
+
+  if (eqPoint(startP1, startP2) && eqPoint(endP1, endP2)) {
+    return RelationshipEnum.Equal;
+  }
+
+  const isJoin =
+    eqPoint(startP1, startP2) ||
+    eqPoint(startP1, endP2) ||
+    eqPoint(endP1, startP2) ||
+    eqPoint(endP1, endP2);
+
+  const dir1 = getLineDireVector2(line1);
+  const dir2 = getLineDireVector2(line2);
+
+  // 计算线段的法向量
+  const normal1 = new Vector2(-dir1.y, dir1.x).normalize();
+  const normal2 = new Vector2(-dir2.y, dir2.x).normalize();
+
+  const start1 = new Vector2(line1[0], line1[1]);
+  const start2 = new Vector2(line2[0], line2[1]);
+
+  const end1 = new Vector2(line1[2], line1[3]);
+  const end2 = new Vector2(line2[2], line2[3]);
+
+  // 计算线段的参数方程
+  const t1 =
+    normal2.dot(start2.clone().sub(start1).normalize()) / normal2.dot(dir1);
+  const t2 =
+    normal1.dot(start1.clone().sub(start2).normalize()) / normal1.dot(dir2);
+  const n2Negate = normal2.negate();
+
+  if (t1 === 0 && t2 === 0) {
+    return RelationshipEnum.Overlap;
+  } else if (
+    eqPoint([normal1.x, normal1.y], [normal2.x, normal2.y]) ||
+    eqPoint([normal1.x, normal1.y], [n2Negate.x, n2Negate.y])
+  ) {
+    return isJoin ? RelationshipEnum.Overlap : RelationshipEnum.Parallel;
+  } else {
+    if (isJoin) {
+      return RelationshipEnum.Join;
+    }
+    const denominator =
+      (end2.y - start2.y) * (end1.x - start1.x) -
+      (end2.x - start2.x) * (end1.y - start1.y);
+    const ua =
+      ((end2.x - start2.x) * (start1.y - start2.y) -
+        (end2.y - start2.y) * (start1.x - start2.x)) /
+      denominator;
+    const ub =
+      ((end1.x - start1.x) * (start1.y - start2.y) -
+        (end1.y - start1.y) * (start1.x - start2.x)) /
+      denominator;
+
+    if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
+      return RelationshipEnum.Intersect; // 两线段相交
+    } else {
+      return RelationshipEnum.ExtendIntersect; // 延长可相交
+    }
+  }
+};
+
+/**
+ * 获取两线段交点
+ * @param line1 线段1
+ * @param line2 线段2
+ * @returns 交点坐标
+ */
+export const getLineIntersection = (line1: number[], line2: number[]) => {
+  let relationship = getLineRelationship(line1, line2);
+  if (relationship !== RelationshipEnum.Intersect) {
+    return null;
+  }
+
+  // 定义两条线段的起点和终点坐标
+  const line1Start = new Vector2(line1[0], line1[1]);
+  const line1End = new Vector2(line1[2], line1[3]);
+
+  const line2Start = new Vector2(line2[0], line2[1]);
+  const line2End = new Vector2(line2[2], line2[3]);
+
+  // 计算线段的方向向量
+  const dir1 = line1End.clone().sub(line1Start);
+  const dir2 = line2End.clone().sub(line2Start);
+
+  // 计算参数方程中的系数
+  const a = dir1.x;
+  const b = -dir2.x;
+  const c = dir1.y;
+  const d = -dir2.y;
+
+  const e = line2Start.x - line1Start.x;
+  const f = line2Start.y - line1Start.y;
+
+  // 求解参数t和s
+  const t = (d * e - b * f) / (a * d - b * c);
+  // 计算交点坐标
+  return line1Start.clone().add(dir1.clone().multiplyScalar(t)).toArray();
+};
+
+/**
+ * 获取点在线段上的投影
+ * @param line 线段
+ * @param position 点
+ * @returns 投影信息
+ */
+export const getLineProjection = (line: number[], position: number[]) => {
+  // 定义线段的起点和终点坐标
+  const lineStart = new Vector2(line[0], line[1]);
+  const lineEnd = new Vector2(line[2], line[3]);
+
+  // 定义一个点的坐标
+  const point = new Vector2(position[0], position[1]);
+
+  // 计算线段的方向向量
+  const lineDir = lineEnd.clone().sub(lineStart);
+
+  // 计算点到线段起点的向量
+  const pointToLineStart = point.clone().sub(lineStart);
+
+  // 计算点在线段方向上的投影长度
+  const projectionLength = pointToLineStart.dot(lineDir) / lineDir.lengthSq();
+
+  // 计算投影点的坐标
+  const projectionPoint = lineStart
+    .clone()
+    .add(lineDir.clone().normalize().multiplyScalar(projectionLength));
+
+  return {
+    point: projectionPoint.toArray(),
+    len: projectionLength,
+  };
+};
+
+/**
+ * 通过一个点获取到直线上最近的点
+ * @param line 直线
+ * @param point 参考点
+ * @returns 最近的点
+ */
+export const getLineNearPoint = (line: number[], point: number[]) => {
+  // 创建线段
+  const line3 = new Line3(
+    new Vector3(line[0], line[1], 0),
+    new Vector3(line[2], line[3], 0)
+  );
+  // 创建点
+  const point3 = new Vector3(point[0], point[1], 0);
+
+  // 获取点到线段的最近点
+  const closestPoint = line3.closestPointToPoint(point3, true, new Vector3());
+
+  return [closestPoint.x, closestPoint.y];
+};
+
+/**
+ * 获取点距离线段最近距离
+ * @param line 直线
+ * @param point 参考点
+ * @returns 距离
+ */
+export const getLineNearPointDist = (line: number[], point: number[]) => {
+  const p2 = getLineNearPoint(line, point);
+  return getLineDist(point, p2);
+};
+
+/**
+ * 计算多边形是否为逆时针
+ * @param points 多边形顶点
+ * @returns true | false
+ */
+export const polygonCounterclockwise = (points: number[][]) => {
+  const polygon = points.map((point) => new Vector2(point[0], point[1]));
+  return ShapeUtils.isClockWise(polygon);
+};

+ 160 - 0
src/board/shared/package-base.ts

@@ -0,0 +1,160 @@
+import konva from "konva";
+import { Attrib, GetSetPick, TGroup, TShape } from "../type";
+import { DragResult, injectDrag } from "./shape-drag";
+import { DEV } from "../env";
+
+const empty = () => {};
+
+export type PackageBaseProps<T extends Attrib> = {
+  name?: string;
+  attrib: T;
+  zIndex?: number;
+  readonly?: boolean;
+};
+export abstract class PackageBase<T extends Attrib, S extends TShape | TGroup> {
+  attrib: T;
+  shape: S;
+  name: string;
+  drag: DragResult | null;
+  readonly: boolean;
+
+  private zIndex: number;
+  children: PackageBase<Attrib, TGroup | TShape>[] = [];
+  parent: PackageBase<Attrib, TGroup | TShape>;
+
+  protected destoryReactive: () => void;
+
+  constructor(props: PackageBaseProps<T>) {
+    this.name = props.name;
+    this.attrib = props.attrib;
+    this.drag = null;
+    this.zIndex = props.zIndex || 0;
+    this.readonly = props.readonly || false;
+  }
+
+  init() {
+    this.shape = this.initShape();
+    this.shape.id(this.name);
+    if (this.readonly) {
+      this.initView();
+    } else {
+      this.destoryReactive = this.initReactive();
+    }
+  }
+
+  getSameLevelNdx(parent = this.parent) {
+    const sameLevel = parent.children;
+
+    for (let i = sameLevel.length - 1; i >= 0; i--) {
+      if (sameLevel[i] !== this && sameLevel[i].zIndex <= this.zIndex) {
+        return i + 1;
+      }
+    }
+    return 0;
+  }
+
+  setParent(parent: PackageBase<Attrib, TGroup> | null) {
+    const child = this as any;
+
+    if (this.parent) {
+      const ndx = this.parent.children.indexOf(child);
+      ~ndx && this.parent.children.splice(ndx, 1);
+    }
+    this.parent = parent;
+    if (parent) {
+      this.parent.children.push(child);
+
+      const parentShape = this.parent.shape as TGroup;
+      parentShape.add(this.shape);
+      this.setZIndex(child.zIndex);
+    }
+  }
+
+  setZIndex(index: number) {
+    this.zIndex = index;
+    const packNdx = this.getSameLevelNdx();
+    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;
+    }
+    this.shape.zIndex(packNdx);
+  }
+
+  childrenShapesSort() {
+    if (!(this.shape instanceof konva.Group)) {
+      return;
+    }
+    const shapes = this.shape.children;
+    const indexs = shapes.map((_, i) => i);
+
+    this.children.forEach((child) => {
+      const ndx = shapes.indexOf(child.getShape());
+      if (~ndx) {
+        indexs[ndx] = indexs.length + child.zIndex;
+      }
+    });
+    for (let i = 0; i < shapes.length; i++) {
+      shapes[i].zIndex(indexs[i]);
+    }
+  }
+
+  abstract initShape(): S;
+
+  enableDrag(pointDragStyle: {
+    hover?: GetSetPick<S> | (() => void);
+    active?: GetSetPick<S> | (() => void);
+    dragend?: () => void;
+    dragstart?: () => void;
+  }) {
+    this.drag && this.drag.destory();
+    this.drag = injectDrag(this.shape, pointDragStyle);
+  }
+
+  disableDrag() {
+    this.drag?.destory();
+    this.drag = null;
+  }
+
+  setAttrib(newAttrib: T) {
+    this.attrib = newAttrib;
+    console.log("setAttrib 重建", this.name);
+    this.destoryReactive && this.destoryReactive();
+    this.initReactive();
+  }
+
+  needUpdate(newAttrib: T) {
+    return newAttrib !== this.attrib;
+  }
+
+  initReactive() {
+    return empty;
+  }
+
+  initView() {}
+
+  getShape() {
+    return this.shape;
+  }
+
+  destory() {
+    this.setParent(null);
+    this.destoryReactive && this.destoryReactive();
+    this.drag?.destory();
+    if (DEV) {
+      console.log(this.name, "destory");
+    }
+    if (this.shape instanceof konva.Group) {
+      this.shape.destroyChildren();
+    } else {
+      this.shape.destroy();
+    }
+  }
+}
+
+export type PackageBaseType<T extends Attrib, S extends TShape | TGroup> = new (
+  name: string,
+  attrib: T
+) => PackageBase<T, S>;

+ 67 - 0
src/board/shared/public.ts

@@ -0,0 +1,67 @@
+export const objectToString = Object.prototype.toString;
+export const toTypeString = (value: unknown): string =>
+  objectToString.call(value);
+
+export const toRawType = (value: unknown): string => {
+  // extract "RawType" from strings like "[object RawType]"
+  return toTypeString(value).slice(8, -1);
+};
+
+// 是否修改
+const _inRevise = (raw1, raw2, readly: Set<[any, any]>) => {
+  if (raw1 === raw2) return false;
+
+  const rawType1 = toRawType(raw1);
+  const rawType2 = toRawType(raw2);
+
+  if (rawType1 !== rawType2) {
+    return true;
+  } else if (
+    rawType1 === "String" ||
+    rawType1 === "Number" ||
+    rawType1 === "Boolean"
+  ) {
+    if (rawType1 === "Number" && isNaN(raw1) && isNaN(raw2)) {
+      return false;
+    } else {
+      return raw1 !== raw2;
+    }
+  }
+
+  const rawsArray = Array.from(readly.values());
+  for (const raws of rawsArray) {
+    if (raws.includes(raw1) && raws.includes(raw2)) {
+      return false;
+    }
+  }
+  readly.add([raw1, raw2]);
+
+  if (rawType1 === "Array") {
+    return (
+      raw1.length !== raw2.length ||
+      raw1.some((item1, i) => _inRevise(item1, raw2[i], readly))
+    );
+  } else if (rawType1 === "Object") {
+    const rawKeys1 = Object.keys(raw1).sort();
+    const rawKeys2 = Object.keys(raw2).sort();
+
+    return (
+      _inRevise(rawKeys1, rawKeys2, readly) ||
+      rawKeys1.some((key) => _inRevise(raw1[key], raw2[key], readly))
+    );
+  } else if (rawType1 === "Map") {
+    const rawKeys1 = Array.from(raw1.keys()).sort();
+    const rawKeys2 = Array.from(raw2.keys()).sort();
+
+    return (
+      _inRevise(rawKeys1, rawKeys2, readly) ||
+      rawKeys1.some((key) => _inRevise(raw1.get(key), raw2.get(key), readly))
+    );
+  } else if (rawType1 === "Set") {
+    return inRevise(Array.from(raw1.values()), Array.from(raw2.values()));
+  } else {
+    return raw1 !== raw2;
+  }
+};
+
+export const inRevise = (raw1, raw2) => _inRevise(raw1, raw2, new Set());

+ 194 - 0
src/board/shared/shape-drag.ts

@@ -0,0 +1,194 @@
+import { reactive, ref, watch } from "vue";
+import { Attrib, GetSetPick, TGroup, TShape } from "../type";
+import { getChangePart, setShapeConfig } from "./util";
+import { PackageBase } from "./package-base";
+
+export const injectDrag = <T extends TShape | TGroup>(
+  shape: T,
+  styles?: {
+    hover?: GetSetPick<T> | (() => void);
+    active?: GetSetPick<T> | (() => void);
+    dragend?: () => void;
+    dragstart?: () => void;
+  },
+  rangeCheck?: (pos: number[]) => boolean
+) => {
+  shape.draggable(true);
+  shape.listening(true);
+
+  const position = reactive([shape.x(), shape.y()]);
+  shape.dragBoundFunc((pos) => {
+    if (rangeCheck && rangeCheck([pos.x, pos.y])) {
+      return pos;
+    } else {
+      position[0] = pos.x;
+      position[1] = pos.y;
+      return shape.absolutePosition();
+    }
+  });
+
+  const keys = new Set<string>();
+
+  const draging = ref(false);
+  let enter = false;
+  const enterHandler = (ev) => {
+    if (styles.hover instanceof Function) {
+      styles.hover();
+    } else {
+      setShapeConfig(shape, styles.hover);
+      const canvas = ev.evt.toElement as HTMLCanvasElement;
+      canvas && (canvas.style.cursor = "pointer");
+    }
+  };
+  const dragingHandler = () => {
+    if (styles.active instanceof Function) {
+      styles.active();
+    } else {
+      setShapeConfig(shape, styles.active);
+    }
+  };
+  const cancelHandler = (ev) => {
+    if (!draging.value && !enter) {
+      setShapeConfig(shape, pubStyle as any);
+      const canvas = ev.evt.toElement as HTMLCanvasElement;
+      canvas && (canvas.style.cursor = "inherit");
+    } else if (draging.value) {
+      dragingHandler();
+    } else if (enter) {
+      enterHandler(ev);
+    }
+  };
+
+  if (styles.hover) {
+    if (!(styles.hover instanceof Function)) {
+      Object.keys(styles.hover).forEach((key) => keys.add(key));
+    }
+    shape.on("mouseenter.drag", (ev) => {
+      enter = true;
+      enterHandler(ev);
+    });
+
+    shape.on("mouseleave.drag", (ev) => {
+      enter = false;
+      cancelHandler(ev);
+    });
+  }
+
+  if (styles.active) {
+    if (!(styles.active instanceof Function)) {
+      Object.keys(styles.active).forEach((key) => keys.add(key));
+    }
+    shape.on("dragstart.drag mousedown.drag", () => {
+      draging.value = true;
+      dragingHandler();
+    });
+  }
+
+  if (keys.size > 0 || "dragend" in styles) {
+    shape.on("mouseup.drag dragend.drag", (ev) => {
+      draging.value = false;
+      cancelHandler(ev);
+    });
+  }
+  if (styles.dragend) {
+    shape.on("dragend.drag", () => {
+      styles.dragend();
+    });
+  }
+  if (styles.dragstart) {
+    shape.on("dragstart.drag", () => {
+      styles.dragstart();
+    });
+  }
+
+  const pubStyle = Object.fromEntries(
+    [...keys.values()].map((key) => [key, shape[key]()])
+  );
+
+  return {
+    position,
+    draging,
+    destory: () => {
+      shape.listening(false);
+      shape.draggable(false);
+      shape.off(
+        "mouseup.drag dragend.drag dragstart.drag mousedown.drag mouseleave.drag mouseenter.drag"
+      );
+    },
+  };
+};
+
+export type DragResult = ReturnType<typeof injectDrag<TShape | TGroup>>;
+
+type DragWatchandlers<T, K extends Attrib> = {
+  readyHandler: (attrib: K) => T;
+  moveHandler: (attrib: K, move: number[], readyData: T) => void;
+  endHandler: (attrib: K, readyData: T) => void;
+};
+export const entityOpenDrag = <T, K extends Attrib, S extends TShape | TGroup>(
+  entity: PackageBase<K, S>,
+  handlers: DragWatchandlers<T, K>,
+  styles: {
+    hover?: GetSetPick<S> | (() => void);
+    active?: GetSetPick<S> | (() => void);
+  }
+) => {
+  let readlyData: T;
+
+  const moveReadly = () => (readlyData = handlers.readyHandler(entity.attrib));
+  const move = (pos: number[]) =>
+    handlers.moveHandler(entity.attrib, pos, readlyData);
+  const moveEnd = () => handlers.endHandler(entity.attrib, readlyData);
+  const dragArgs = {
+    ...styles,
+    dragstart: moveReadly,
+    dragend: moveEnd,
+  };
+
+  entity.enableDrag(dragArgs);
+
+  const stopDragWatch = watch(() => [...entity.drag.position], move);
+
+  return () => {
+    entity.disableDrag();
+    stopDragWatch();
+  };
+};
+
+export const watchEntitysOpenDrag = <
+  T,
+  K extends Attrib,
+  S extends TShape | TGroup
+>(
+  getter: () => PackageBase<K, S>[],
+  handlers: DragWatchandlers<T, K>,
+  styles: {
+    hover?: GetSetPick<S> | (() => void);
+    active?: GetSetPick<S> | (() => void);
+  }
+) => {
+  const dragWatchStopMap = new WeakMap<PackageBase<K, S>, () => void>();
+  const entryDragStop = (entry: PackageBase<K, S>) => {
+    const f = dragWatchStopMap.get(entry);
+    f && f();
+  };
+  let currentEntrys: PackageBase<K, S>[] = [];
+
+  const stopWatch = watch(
+    getter,
+    (newEntrys, oldEntrys) => {
+      const { addPort, delPort } = getChangePart(newEntrys, oldEntrys);
+      delPort.forEach(entryDragStop);
+      addPort.forEach((entry) => {
+        dragWatchStopMap.set(entry, entityOpenDrag(entry, handlers, styles));
+      });
+      currentEntrys = newEntrys;
+    },
+    { immediate: true, flush: "pre" }
+  );
+
+  return () => {
+    stopWatch();
+    currentEntrys.forEach(entryDragStop);
+  };
+};

+ 310 - 0
src/board/shared/shape-mose.ts

@@ -0,0 +1,310 @@
+import { reactive, ref, watch } from "vue";
+import { Attrib, GetSetPick, ShapeStyles, TGroup, TShape } from "../type";
+import { getChangePart, setShapeConfig } from "./util";
+import { PackageBase } from "./package-base";
+
+export const openShapeMouseStyles = <T extends TShape | TGroup>(
+  shape: T,
+  styles?: ShapeStyles<T>,
+  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) => {
+    shape.on(name, cb);
+    useEvents.push(name);
+  };
+
+  let enter = false;
+  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);
+    }
+  };
+
+  if (styles.hover) {
+    useOn(`mouseenter.${namespace}`, () => {
+      enter = true;
+      mouseHandler();
+    });
+    useOn(`mouseleave.${namespace}`, () => {
+      enter = false;
+      mouseHandler();
+    });
+  }
+
+  if (styles.active) {
+    const stage = shape.getStage();
+    stage.on(`click.${namespace}${shape.id()}`, (evt) => {
+      active = evt.target === shape;
+      mouseHandler();
+    });
+  }
+
+  if (styles.draging) {
+    useOn(`dragstart.${namespace} mousedown.${namespace}`, () => {
+      draging = true;
+      mouseHandler();
+    });
+    useOn(`seup.${namespace} dragend.${namespace}`, () => {
+      draging = false;
+    });
+  }
+
+  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;
+};
+
+export const openShapeDrag = <T extends TShape | TGroup, D>(
+  shape: T,
+  handler: DragHandlers<D>
+) => {
+  let readlyData = null as D;
+
+  shape.draggable(true);
+  shape.dragBoundFunc((pos) => {
+    handler.moveHandler([pos.x, pos.y], readlyData);
+    return shape.absolutePosition();
+  });
+
+  if (handler.readyHandler) {
+    shape.on("dragstart.drag", () => {
+      readlyData = handler.readyHandler();
+    });
+  }
+
+  if (handler.endHandler) {
+    shape.on("dragend.drag", () => {
+      handler.endHandler(readlyData);
+    });
+  }
+  return () => {
+    shape.draggable(false);
+    shape.off("dragstart.drag dragend.drag");
+  };
+};
+
+export const injectDrag = <T extends TShape | TGroup>(
+  shape: T,
+  styles?: {
+    hover?: GetSetPick<T> | (() => void);
+    active?: GetSetPick<T> | (() => void);
+    dragend?: () => void;
+    dragstart?: () => void;
+  },
+  rangeCheck?: (pos: number[]) => boolean
+) => {
+  shape.draggable(true);
+
+  shape.listening(true);
+
+  const position = reactive([shape.x(), shape.y()]);
+  shape.dragBoundFunc((pos) => {
+    if (rangeCheck && rangeCheck([pos.x, pos.y])) {
+      return pos;
+    } else {
+      position[0] = pos.x;
+      position[1] = pos.y;
+      return shape.absolutePosition();
+    }
+  });
+
+  const keys = new Set<string>();
+
+  const draging = ref(false);
+  let enter = false;
+  const enterHandler = (ev) => {
+    if (styles.hover instanceof Function) {
+      styles.hover();
+    } else {
+      setShapeConfig(shape, styles.hover);
+      const canvas = ev.evt.toElement as HTMLCanvasElement;
+      canvas && (canvas.style.cursor = "pointer");
+    }
+  };
+  const dragingHandler = () => {
+    if (styles.active instanceof Function) {
+      styles.active();
+    } else {
+      setShapeConfig(shape, styles.active);
+    }
+  };
+  const cancelHandler = (ev) => {
+    if (!draging.value && !enter) {
+      setShapeConfig(shape, pubStyle as any);
+      const canvas = ev.evt.toElement as HTMLCanvasElement;
+      canvas && (canvas.style.cursor = "inherit");
+    } else if (draging.value) {
+      dragingHandler();
+    } else if (enter) {
+      enterHandler(ev);
+    }
+  };
+
+  if (styles.hover) {
+    if (!(styles.hover instanceof Function)) {
+      Object.keys(styles.hover).forEach((key) => keys.add(key));
+    }
+    shape.on("mouseenter.drag", (ev) => {
+      enter = true;
+      enterHandler(ev);
+    });
+
+    shape.on("mouseleave.drag", (ev) => {
+      enter = false;
+      cancelHandler(ev);
+    });
+  }
+
+  if (styles.active) {
+    if (!(styles.active instanceof Function)) {
+      Object.keys(styles.active).forEach((key) => keys.add(key));
+    }
+    shape.on("dragstart.drag mousedown.drag", () => {
+      draging.value = true;
+      dragingHandler();
+    });
+  }
+
+  if (keys.size > 0 || "dragend" in styles) {
+    shape.on("mouseup.drag dragend.drag", (ev) => {
+      draging.value = false;
+      cancelHandler(ev);
+    });
+  }
+  if (styles.dragend) {
+    shape.on("dragend.drag", () => {
+      styles.dragend();
+    });
+  }
+  if (styles.dragstart) {
+    shape.on("dragstart.drag", () => {
+      styles.dragstart();
+    });
+  }
+
+  const pubStyle = Object.fromEntries(
+    [...keys.values()].map((key) => [key, shape[key]()])
+  );
+
+  return {
+    position,
+    draging,
+    destory: () => {
+      shape.listening(false);
+      shape.draggable(false);
+      shape.off(
+        "mouseup.drag dragend.drag dragstart.drag mousedown.drag mouseleave.drag mouseenter.drag"
+      );
+    },
+  };
+};
+
+export type DragResult = ReturnType<typeof injectDrag<TShape | TGroup>>;
+
+type DragWatchandlers<T, K extends Attrib> = {
+  readyHandler: (attrib: K) => T;
+  moveHandler: (attrib: K, move: number[], readyData: T) => void;
+  endHandler: (attrib: K, readyData: T) => void;
+};
+export const entityOpenDrag = <T, K extends Attrib, S extends TShape | TGroup>(
+  entity: PackageBase<K, S>,
+  handlers: DragWatchandlers<T, K>,
+  styles: {
+    hover?: GetSetPick<S> | (() => void);
+    active?: GetSetPick<S> | (() => void);
+  }
+) => {
+  let readlyData: T;
+
+  const moveReadly = () => (readlyData = handlers.readyHandler(entity.attrib));
+  const move = (pos: number[]) =>
+    handlers.moveHandler(entity.attrib, pos, readlyData);
+  const moveEnd = () => handlers.endHandler(entity.attrib, readlyData);
+  const dragArgs = {
+    ...styles,
+    dragstart: moveReadly,
+    dragend: moveEnd,
+  };
+
+  entity.enableDrag(dragArgs);
+
+  const stopDragWatch = watch(() => [...entity.drag.position], move);
+
+  return () => {
+    entity.disableDrag();
+    stopDragWatch();
+  };
+};
+
+export const watchEntitysOpenDrag = <
+  T,
+  K extends Attrib,
+  S extends TShape | TGroup
+>(
+  getter: () => PackageBase<K, S>[],
+  handlers: DragWatchandlers<T, K>,
+  styles: {
+    hover?: GetSetPick<S> | (() => void);
+    active?: GetSetPick<S> | (() => void);
+  }
+) => {
+  const dragWatchStopMap = new WeakMap<PackageBase<K, S>, () => void>();
+  const entryDragStop = (entry: PackageBase<K, S>) => {
+    const f = dragWatchStopMap.get(entry);
+    f && f();
+  };
+  let currentEntrys: PackageBase<K, S>[] = [];
+
+  const stopWatch = watch(
+    getter,
+    (newEntrys, oldEntrys) => {
+      const { addPort, delPort } = getChangePart(newEntrys, oldEntrys);
+      delPort.forEach(entryDragStop);
+      addPort.forEach((entry) => {
+        dragWatchStopMap.set(entry, entityOpenDrag(entry, handlers, styles));
+      });
+      currentEntrys = newEntrys;
+    },
+    { immediate: true, flush: "pre" }
+  );
+
+  return () => {
+    stopWatch();
+    currentEntrys.forEach(entryDragStop);
+  };
+};

+ 215 - 0
src/board/shared/util.ts

@@ -0,0 +1,215 @@
+import { reactive, watch } from "vue";
+import {
+  Attrib,
+  Entity,
+  EntityClass,
+  GetSetPick,
+  TGroup,
+  TLayer,
+  TShape,
+} from "../type";
+import { inRevise, toRawType } from "./public";
+
+export const setShapeConfig = <T extends TShape | TGroup>(
+  shape: T,
+  config: GetSetPick<T>
+) => {
+  for (const key in config) {
+    shape[key as any](config[key]);
+  }
+};
+
+export const getChangePart = <T>(newIds: T[], oldIds: T[] = []) => {
+  const addPort = newIds.filter((newId) => !oldIds.includes(newId));
+  const delPort = oldIds.filter((oldId) => !newIds.includes(oldId));
+  const holdPort = oldIds.filter((oldId) => newIds.includes(oldId));
+
+  return { addPort, delPort, holdPort };
+};
+
+export const getChangeAllPoart = <T extends Attrib>(
+  newAttribs: T[],
+  oldAttribs: T[]
+) => {
+  const newIds = newAttribs.map(({ id }) => id);
+  const oldIds = oldAttribs.map(({ id }) => id);
+  const ports = getChangePart(newIds, oldIds);
+
+  // 数组子项引用变化
+  const changePort = newAttribs
+    .filter(
+      (newAttrib) =>
+        !oldAttribs.includes(newAttrib) && oldIds.includes(newAttrib.id)
+    )
+    .map((attrib) => attrib.id);
+  oldAttribs = newAttribs;
+
+  return { ...ports, changePort };
+};
+
+type AttribsChange = ReturnType<typeof getChangePart> & {
+  changePort: string[];
+};
+export const watchAttribs = (
+  attribs: Attrib[],
+  callback: (data: AttribsChange) => void,
+  immediate = true
+) => {
+  return watch(
+    () => [...attribs],
+    (newAttribs, oldAttribs = []) => {
+      callback(getChangeAllPoart(newAttribs, oldAttribs));
+    },
+    { immediate, flush: "pre" }
+  );
+};
+
+export const attribsJoinEntity = <
+  T extends Attrib,
+  C extends EntityClass<T>,
+  P extends Attrib
+>(
+  attribs: T[],
+  Type: C,
+  parent?: Entity<P>,
+  joinDestory = false,
+  extra?: (self: InstanceType<C>) => void
+) => {
+  const findAttrib = (id: Attrib["id"]) =>
+    attribs.find((attrib) => attrib.id === id);
+
+  const cache: { [key in Attrib["id"]]: InstanceType<C> } = reactive({});
+  const destory = (id: Attrib["id"]) => {
+    if (id in cache) {
+      cache[id].destory();
+    }
+    const shape = cache[id].getShape();
+    shape && shape.destroy();
+    delete cache[id];
+  };
+
+  const add = (id: Attrib["id"]) => {
+    const attrib = findAttrib(id);
+    if (attrib) {
+      cache[id] = new Type({
+        attrib,
+      }) as InstanceType<C>;
+      extra && extra(cache[id]);
+      cache[id].init && cache[id].init();
+      cache[id].attachBefore && cache[id].attachBefore();
+      parent && cache[id].setParent(parent);
+      cache[id].attachAfter && cache[id].attachAfter();
+    }
+  };
+
+  const stopWatchAttribs = watchAttribs(
+    attribs,
+    ({ addPort, delPort, changePort }) => {
+      delPort.forEach(destory);
+      addPort.forEach(add);
+      changePort.forEach((id) => {
+        cache[id].setAttrib(findAttrib(id));
+      });
+    }
+  );
+
+  return {
+    entitys: cache,
+    destory: () => {
+      stopWatchAttribs();
+      if (joinDestory) {
+        attribs.forEach((attrib) => destory(attrib.id));
+      }
+    },
+  };
+};
+
+export const deptComputed = <T extends Record<string, any>>(
+  getter: () => T
+) => {
+  const data = reactive(getter());
+  const stop = watch(getter, (newData) => {
+    if (inRevise(data, newData)) {
+      if (Array.isArray(newData)) {
+        newData.forEach((item, ndx) => {
+          data[ndx] = item;
+        });
+      } else {
+        Object.keys(data).forEach((key) => delete data[key]);
+        Object.assign(data, newData);
+      }
+    }
+  });
+  return {
+    data: data as T,
+    stop,
+  };
+};
+
+export const partialComputed = <T extends Attrib>(getter: () => T[]) => {
+  const data = reactive(getter()) as T[];
+  const stop = watch(getter, (newData, oldData) => {
+    const { addPort, delPort, changePort } = getChangeAllPoart(
+      newData,
+      oldData
+    );
+    for (const delId of delPort) {
+      const ndx = data.findIndex((i) => i.id === delId);
+      ~ndx && data.splice(ndx, 1);
+    }
+    for (const addId of addPort) {
+      const addItem = newData.find((i) => i.id === addId);
+      addItem && data.push(addItem);
+    }
+
+    for (const changeId of changePort) {
+      const dataNdx = data.findIndex((i) => i.id === changeId);
+      const newDataNdx = newData.findIndex((i) => i.id === changeId);
+      if (inRevise(data[dataNdx], newData[newDataNdx])) {
+        data[dataNdx] = newData[newDataNdx];
+      }
+    }
+  });
+
+  return {
+    data,
+    stop,
+  };
+};
+
+export const depPartialUpdate = <T>(newData: T, oldData: T): T => {
+  if (!inRevise(newData, oldData)) {
+    return oldData;
+  }
+  const nData = newData as any,
+    oData = oldData as any;
+  const type = toRawType(nData);
+
+  switch (type) {
+    case "Array":
+      for (let i = 0; i < nData.length; i++) {
+        oData[i] = depPartialUpdate(nData[i], oData[i]);
+      }
+      while (oData.length !== nData.length) {
+        oData.pop();
+      }
+      break;
+    case "Object":
+      const oKeys = Object.keys(oData).sort();
+      const nKeys = Object.keys(nData).sort();
+      const { addPort, delPort, holdPort } = getChangePart(nKeys, oKeys);
+
+      for (let i = 0; i < holdPort.length; i++) {
+        oData[oKeys[i]] = depPartialUpdate(
+          nData[holdPort[i]],
+          oData[holdPort[i]]
+        );
+      }
+      addPort.forEach((key) => (oData[key] = nData[key]));
+      delPort.forEach((key) => delete oData[key]);
+      break;
+    default:
+      return newData;
+  }
+  return oldData;
+};

+ 57 - 0
src/board/store.ts

@@ -0,0 +1,57 @@
+import { reactive, watch } from "vue";
+import { EntityClass, TLayer, Attrib } from "./type";
+import { attribsJoinEntity, getChangePart } from "./shared/util";
+
+export const setStoreData = <T extends string, R extends Attrib>(
+  data: { [key in T]: R[] },
+  newData: { [key in T]?: R[] }
+) => {
+  const { delPort } = getChangePart(Object.keys(newData), Object.keys(data));
+  for (const key in delPort) {
+    data[key] = [];
+  }
+  // 可优化
+  for (const key in newData) {
+    data[key] = newData[key];
+  }
+};
+
+export const parse = <T extends string, R extends Attrib>(
+  layer: TLayer,
+  types: { [key in T]: EntityClass<R> },
+  dataRaw: { [key in T]?: R[] } = {}
+) => {
+  let data = dataRaw as { [key in T]: R[] };
+  for (const key in types) {
+    if (!(key in data)) {
+      data[key] = [];
+    }
+  }
+  data = reactive(data) as { [key in T]: R[] };
+
+  const destory = watch(
+    () => Object.entries(data),
+    (newData, _, onCleanup) => {
+      newData.map(([key, items]) => {
+        console.log("重建" + key);
+        const join = attribsJoinEntity(
+          items as any,
+          types[key],
+          undefined,
+          true,
+          (group) => {
+            setTimeout(() => layer.add(group.getShape()));
+          }
+        );
+
+        onCleanup(join.destory);
+      });
+    },
+    { immediate: true }
+  );
+
+  return {
+    data,
+    destory,
+  };
+};

+ 66 - 0
src/board/type.d.ts

@@ -0,0 +1,66 @@
+import konva from "konva";
+import { injectDrag } from "../../shared/shape-drag";
+import { Shape } from "konva/lib/Shape";
+import { Group } from "konva/lib/Group";
+import { Layer } from "konva/lib/Layer";
+import { Stage } from "konva/lib/Stage";
+
+export type TArrow = InstanceType<typeof konva.Arrow>;
+export type TLabel = InstanceType<typeof konva.Label>;
+export type TText = InstanceType<typeof konva.Text>;
+export type TGroup = InstanceType<typeof konva.Group>;
+export type TLine = InstanceType<typeof konva.Line>;
+export type TShape = InstanceType<typeof konva.Shape>;
+export type TLayer = InstanceType<typeof konva.Layer>;
+export type TCircle = InstanceType<typeof konva.Circle>;
+
+type ShapeType = Group | Layer | Stage | Shape;
+
+interface GetSet<Type, This> {
+  (a: Type): This;
+}
+export type GetSetPick<T extends TShape | TGroup> = {
+  [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 Attrib = {
+  id: string;
+};
+
+export type EntityClassProps<T extends Attrib> = {
+  attrib: T;
+};
+export type EntityClass<T extends Attrib> = new (
+  props: EntityClassProps
+) => Entity<T>;
+
+export type Entity<T extends Attrib> = {
+  init?: () => void;
+  attrib: T;
+  children: Entity<Attrib>[];
+  parent: Entity<Attrib>;
+  name: string;
+  setAttrib: (attrib: T) => void;
+  destory: () => void;
+  setParent: (parent: Entity<Attrib>) => void;
+  attachAfter?: () => void;
+  attachBefore?: () => void;
+  getShape: () => TShape | TGroup;
+};
+
+export type DragEntity<T extends Attrib> = Entity<T> & {
+  drag: DragResult;
+};
+
+export type DragEntityClass<T extends Attrib> = new (
+  props: EntityClassProps<T>
+) => DragEntity<T>;
+
+export type GetShape = <T extends TShape | TGroup>(style: GetSetPick<T>) => T;

+ 45 - 0
src/components/query-board/index.vue

@@ -0,0 +1,45 @@
+<template>
+  <div
+    class="board-layout"
+    :style="{ width: width + 'px', height: height + 'px' }"
+    ref="containerRef"
+  ></div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from "vue";
+import { register } from "../../board/register";
+import { EditWholeLine } from "../../board/packages";
+import storeData from "./storeData.json";
+
+withDefaults(defineProps<{ width?: number; height?: number; pixelRation?: number }>(), {
+  width: 320,
+  height: 150,
+  pixelRation: 1,
+});
+
+console.log(storeData);
+
+const containerRef = ref<HTMLDivElement>();
+const initBoard = register({ rooms: EditWholeLine });
+watch(containerRef, (container, _, onClanup) => {
+  if (container) {
+    const board = initBoard(container, storeData);
+    window.board = board;
+    onClanup(() => board.destory);
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.board-layout {
+  position: relative;
+  display: inline-block;
+
+  canvas {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>

+ 230 - 0
src/components/query-board/storeData.json

@@ -0,0 +1,230 @@
+{
+  "rooms": [
+    {
+      "id": "2",
+      "points": [
+        {
+          "id": "1",
+          "x": 881,
+          "y": 492
+        },
+        {
+          "id": "2",
+          "x": 136,
+          "y": 237
+        },
+        {
+          "id": "8",
+          "x": 371,
+          "y": 699
+        },
+        {
+          "id": "18",
+          "x": 480,
+          "y": 511
+        },
+        {
+          "id": "19",
+          "x": 83,
+          "y": 647
+        },
+        {
+          "id": "3",
+          "x": 178,
+          "y": 803
+        },
+        {
+          "x": 675.8655056700748,
+          "y": 268.3909090194661,
+          "id": "20"
+        },
+        {
+          "x": 163.5307763543757,
+          "y": 608.0099861089675,
+          "id": "21"
+        },
+        {
+          "x": 579.4292704980454,
+          "y": 468.065825925416,
+          "id": "22"
+        },
+        {
+          "x": 579.1720366885647,
+          "y": 467.6217710319711,
+          "id": "23"
+        },
+        {
+          "x": 445.2292582546364,
+          "y": 325.58255334590945,
+          "id": "24"
+        }
+      ],
+      "polygons": [
+        {
+          "id": "1",
+          "lineIds": [
+            "1",
+            "2",
+            "3",
+            "4",
+            "5",
+            "6",
+            "7",
+            "8",
+            "9",
+            "10",
+            "11",
+            "12",
+            "13"
+          ]
+        },
+        {
+          "id": "2",
+          "lineIds": [
+            "14",
+            "15",
+            "16",
+            "17",
+            "18",
+            "19"
+          ]
+        }
+      ],
+      "lines": [
+        {
+          "id": "1",
+          "pointIds": [
+            "1",
+            "22"
+          ]
+        },
+        {
+          "id": "2",
+          "pointIds": [
+            "22",
+            "23"
+          ]
+        },
+        {
+          "id": "3",
+          "pointIds": [
+            "23",
+            "24"
+          ]
+        },
+        {
+          "id": "4",
+          "pointIds": [
+            "24",
+            "2"
+          ]
+        },
+        {
+          "id": "5",
+          "pointIds": [
+            "2",
+            "20"
+          ]
+        },
+        {
+          "id": "6",
+          "pointIds": [
+            "20",
+            "22"
+          ]
+        },
+        {
+          "id": "7",
+          "pointIds": [
+            "22",
+            "8"
+          ]
+        },
+        {
+          "id": "8",
+          "pointIds": [
+            "8",
+            "18"
+          ]
+        },
+        {
+          "id": "9",
+          "pointIds": [
+            "18",
+            "23"
+          ]
+        },
+        {
+          "id": "10",
+          "pointIds": [
+            "23",
+            "20"
+          ]
+        },
+        {
+          "id": "11",
+          "pointIds": [
+            "20",
+            "24"
+          ]
+        },
+        {
+          "id": "12",
+          "pointIds": [
+            "24",
+            "21"
+          ]
+        },
+        {
+          "id": "13",
+          "pointIds": [
+            "21",
+            "19"
+          ]
+        },
+        {
+          "id": "14",
+          "pointIds": [
+            "3",
+            "21"
+          ]
+        },
+        {
+          "id": "15",
+          "pointIds": [
+            "21",
+            "2"
+          ]
+        },
+        {
+          "id": "16",
+          "pointIds": [
+            "2",
+            "24"
+          ]
+        },
+        {
+          "id": "17",
+          "pointIds": [
+            "24",
+            "23"
+          ]
+        },
+        {
+          "id": "18",
+          "pointIds": [
+            "23",
+            "22"
+          ]
+        },
+        {
+          "id": "19",
+          "pointIds": [
+            "22",
+            "1"
+          ]
+        }
+      ]
+    }
+  ]
+}

+ 97 - 0
src/components/query-board/streData-merge.json

@@ -0,0 +1,97 @@
+{
+  "rooms": [
+    {
+      "id": "2",
+      "points": [
+        {
+          "id": "1",
+          "x": 881,
+          "y": 492
+        },
+        {
+          "id": "2",
+          "x": 136,
+          "y": 237
+        },
+        {
+          "id": "8",
+          "x": 371,
+          "y": 699
+        },
+        {
+          "id": "18",
+          "x": 480,
+          "y": 511
+        },
+        {
+          "id": "19",
+          "x": 83,
+          "y": 647
+        },
+        {
+          "id": "3",
+          "x": 178,
+          "y": 803
+        },
+        {
+          "x": 675.8655056700748,
+          "y": 268.3909090194661,
+          "id": "20"
+        },
+        {
+          "x": 163.5307763543757,
+          "y": 608.0099861089675,
+          "id": "21"
+        },
+        {
+          "x": 579.4292704980454,
+          "y": 468.065825925416,
+          "id": "22"
+        },
+        {
+          "x": 579.1720366885647,
+          "y": 467.6217710319711,
+          "id": "23"
+        },
+        {
+          "x": 445.2292582546364,
+          "y": 325.58255334590945,
+          "id": "24"
+        }
+      ],
+      "polygons": [
+        {
+          "id": "1",
+          "points": [
+            "1",
+            "22",
+            "23",
+            "24",
+            "2",
+            "20",
+            "22",
+            "8",
+            "18",
+            "23",
+            "20",
+            "24",
+            "21",
+            "19"
+          ]
+        },
+        {
+          "id": "2",
+          "points": [
+            "3",
+            "21",
+            "2",
+            "24",
+            "23",
+            "22",
+            "1"
+          ]
+        }
+      ]
+    }
+  ]
+}

+ 5 - 0
src/main.ts

@@ -0,0 +1,5 @@
+import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+
+createApp(App).mount('#app')

+ 4 - 0
src/style.css

@@ -0,0 +1,4 @@
+* {
+  margin: 0;
+  padding: 0;
+}

+ 11 - 0
src/util/index.ts

@@ -0,0 +1,11 @@
+export const mergeFuns = (...fns: (() => void)[] | (() => void)[][]) => {
+  return () => {
+    fns.forEach((fn) => {
+      if (Array.isArray(fn)) {
+        fn.forEach((f) => f());
+      } else {
+        fn();
+      }
+    });
+  };
+};

+ 1 - 0
src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 34 - 0
tsconfig.json

@@ -0,0 +1,34 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": [
+      "ES2020",
+      "DOM",
+      "DOM.Iterable"
+    ],
+    "skipLibCheck": true,
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "preserve",
+    /* Linting */
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue"
+  ],
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    }
+  ]
+}

+ 11 - 0
tsconfig.node.json

@@ -0,0 +1,11 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true,
+    "strict": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 7 - 0
vite.config.ts

@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+})