Browse Source

制作结构树

bill 1 year ago
parent
commit
a7fc02a2af

src/board/core/resource/cursor/pic_pen.ico → public/cursors/pic_pen.ico


src/board/core/resource/cursor/pic_pen_a.ico → public/cursors/pic_pen_a.ico


src/board/core/resource/cursor/pic_pen_r.ico → public/cursors/pic_pen_r.ico


File diff suppressed because it is too large
+ 6 - 0
public/poi-svgs/a.svg


File diff suppressed because it is too large
+ 8 - 0
public/poi-svgs/b.svg


File diff suppressed because it is too large
+ 10 - 0
public/poi-svgs/c.svg


+ 6 - 0
public/poi-svgs/d.svg

@@ -0,0 +1,6 @@
+<svg t="1721719681299" class="icon" viewBox="0 0 1024 1024" version="1.1"
+  xmlns="http://www.w3.org/2000/svg" p-id="2282" width="64" height="64">
+  <path
+    d="M64 224a160 160 0 0 1 160-160h128a32 32 0 1 1 0 64H224a96 96 0 0 0-96 96v128a32 32 0 1 1-64 0V224zM32 512a32 32 0 0 1 32-32h896a32 32 0 1 1 0 64H64a32 32 0 0 1-32-32zM224 960a160 160 0 0 1-160-160v-128a32 32 0 1 1 64 0v128a96 96 0 0 0 96 96h128a32 32 0 1 1 0 64H224zM960 224a160 160 0 0 0-160-160h-128a32 32 0 1 0 0 64h128a96 96 0 0 1 96 96v128a32 32 0 1 0 64 0V224zM800 960a160 160 0 0 0 160-160v-128a32 32 0 1 0-64 0v128a96 96 0 0 1-96 96h-128a32 32 0 1 0 0 64h128z"
+    p-id="2283"></path>
+</svg>

+ 2 - 1
src/App.vue

@@ -9,7 +9,8 @@
 import { ref } from "vue";
 import { ElButton } from "element-plus";
 // import QueryBoard from "./app/4dmap/example/index.vue";
-import QueryBoard from "./app/liantong/example/index.vue";
+// import QueryBoard from "./app/liantong/example/index.vue";
+import QueryBoard from "./app/test/index.vue";
 
 const mound = ref(true);
 const width = window.innerWidth;

+ 77 - 0
src/app/test/index.vue

@@ -0,0 +1,77 @@
+<template>
+  <!-- <Teleport to="#right-pano">
+    <ElButton @click="board.clear()"> 清除 </ElButton>
+    <ElButton :disabled="!board.history.state.hasUndo" @click="board.history.undo()">
+      撤销
+    </ElButton>
+    <ElButton :disabled="!board.history.state.hasRedo" @click="board.history.redo()">
+      重做
+    </ElButton>
+    <ElButton @click="getData">获取数据</ElButton>
+
+    <ElButton v-if="!drawing" @click="drawHandler"> 绘画 </ElButton>
+    <ElButton v-if="drawing" @click="drawing.cancel()"> 停止 </ElButton>
+    <ElButton v-if="drawing" @click="drawing.submit()"> 提交 </ElButton>
+
+    <template v-if="activeEntity">
+      <ElButton @click="activeEntity.copy({ count: 5 })"> 向右复制5个 </ElButton>
+      <ElButton @click="activeEntity.del()"> 删除 </ElButton>
+    </template>
+
+    <ElButton v-if="!addPoiState" @click="addPoiHandler('bzjg')">添加poi</ElButton>
+    <template v-if="addPoiState">
+      <ElButton @click="addPoiState.cancel()"> 停止添加poi </ElButton>
+    </template>
+
+    <ElButton @click="board.showPois()"> 显示图例 </ElButton>
+    <ElButton @click="board.hidenPois()"> 隐藏图例 </ElButton></Teleport
+  > -->
+  <div
+    class="board-layout"
+    :style="{ width: width + 'px', height: height + 'px' }"
+    ref="containerRef"
+  ></div>
+</template>
+
+<script setup lang="ts">
+import { onMounted, shallowRef } from "vue";
+import { Root } from "../../board/core/base/entity-root";
+import { SVGPois } from "../../board/components/poi/svg-pois";
+// import storeData from "./storeData.json";
+// import { ElButton } from "element-plus";
+
+withDefaults(defineProps<{ width?: number; height?: number; pixelRation?: number }>(), {
+  width: 320,
+  height: 150,
+  pixelRation: 1,
+});
+
+const containerRef = shallowRef<HTMLDivElement>();
+const root = new Root<SVGPois>();
+const pois = new SVGPois({
+  attrib: [
+    { id: 2, x: 100, y: 100, type: "/poi-svgs/a.svg" },
+    { id: 3, x: 300, y: 100, type: "/poi-svgs/b.svg" },
+    { id: 4, x: 100, y: 300, type: "/poi-svgs/c.svg" },
+    { id: 5, x: 300, y: 300, type: "/poi-svgs/d.svg" },
+  ],
+});
+root.addChild(pois);
+root.openPointerEvents();
+
+onMounted(() => {
+  root.mount(containerRef.value);
+});
+</script>
+
+<style lang="scss" scoped>
+.board-layout {
+  position: absolute;
+
+  canvas {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+}
+</style>

+ 107 - 0
src/app/test/storeData.json

@@ -0,0 +1,107 @@
+{
+  "rooms": [
+    {
+      "id": "0",
+      "points": [
+        {
+          "x": -2.506408799670238,
+          "y": -840.768063181097,
+          "id": "2"
+        },
+        {
+          "x": -2.506408799670201,
+          "y": -652.7722602090475,
+          "id": "3"
+        },
+        {
+          "x": 5.169935762901897,
+          "y": -730.7705188889404,
+          "id": "4"
+        },
+        {
+          "x": -2.5064087996702518,
+          "y": -966.7652502793854,
+          "id": "5"
+        },
+        {
+          "x": -3.859266647766991,
+          "y": -2.5111666543689317,
+          "id": "6"
+        },
+        {
+          "x": 2.5199777654368916,
+          "y": -2.511166654368933,
+          "id": "7"
+        },
+        {
+          "x": 2.519977765436893,
+          "y": 2.616899987184465,
+          "id": "8"
+        }
+      ],
+      "polygons": [
+        {
+          "id": "2",
+          "lineIds": [
+            "2",
+            "3",
+            "4"
+          ]
+        },
+        {
+          "id": "3",
+          "lineIds": [
+            "5",
+            "6",
+            "7"
+          ]
+        }
+      ],
+      "lines": [
+        {
+          "id": "2",
+          "pointIds": [
+            "2",
+            "3"
+          ]
+        },
+        {
+          "id": "3",
+          "pointIds": [
+            "3",
+            "4"
+          ]
+        },
+        {
+          "id": "4",
+          "pointIds": [
+            "4",
+            "5"
+          ]
+        },
+        {
+          "id": "5",
+          "pointIds": [
+            "6",
+            "7"
+          ]
+        },
+        {
+          "id": "6",
+          "pointIds": [
+            "7",
+            "8"
+          ]
+        },
+        {
+          "id": "7",
+          "pointIds": [
+            "8",
+            "6"
+          ]
+        }
+      ]
+    }
+  ],
+  "pois": []
+}

+ 3 - 0
src/board/components/poi/index.ts

@@ -0,0 +1,3 @@
+export * from "./poi";
+export * from "./svg-poi";
+export * from "./svg-pois";

+ 41 - 0
src/board/components/poi/poi.ts

@@ -0,0 +1,41 @@
+import {
+  Entity,
+  EntityKey,
+  EntityProps,
+  EntityTree,
+} from "../../core/base/entity";
+import konva from "konva";
+import { openEntityDrag } from "../../core/helper/entity-drag";
+
+export type PoiData = {
+  id?: EntityKey;
+  x: number;
+  y: number;
+  rotate?: number;
+};
+
+export class Poi<S extends konva.Group | konva.Shape> extends Entity<
+  PoiData,
+  S,
+  EntityTree,
+  EntityProps<PoiData> & { shape: S }
+> {
+  initShape() {
+    return this.props.shape;
+  }
+
+  diffRedraw() {
+    this.shape.x(this.attrib.x);
+    this.shape.y(this.attrib.y);
+    if ("rotate" in this.attrib) {
+      this.shape.rotate(this.attrib.rotate);
+    }
+  }
+
+  disableDrag = () => {};
+  enableDrag() {
+    this.disableDrag = openEntityDrag(this, () => ({
+      move: (pos) => this.setAttrib(pos),
+    }));
+  }
+}

+ 34 - 0
src/board/components/poi/svg-poi.ts

@@ -0,0 +1,34 @@
+import { Poi, PoiData } from "./poi";
+import {
+  getSVGProps,
+  PathsShapeAct,
+  pathsToShapeAct,
+} from "../../core/helper/svg";
+import { EntityProps } from "../../core/base/entity";
+import konva from "konva";
+
+export type SVGPoiData = PoiData & {
+  type: string;
+};
+
+export class SVGPoi extends Poi<konva.Group> {
+  private act: PathsShapeAct;
+
+  constructor(props: EntityProps<SVGPoiData>) {
+    const shape = new konva.Group();
+    super({ ...props, shape });
+
+    getSVGProps(props.attrib.type).then((paths) => {
+      this.act = pathsToShapeAct(paths, undefined, true);
+      shape.add(this.act.shape);
+
+      this.setPointerStyle({
+        common: this.act.common,
+        hover: () => this.act.setFill("blue"),
+        focus: () => this.act.setFill("red"),
+        drag: () => this.act.setFill("#0cff00"),
+      });
+    });
+    this.enableDrag();
+  }
+}

+ 9 - 0
src/board/components/poi/svg-pois.ts

@@ -0,0 +1,9 @@
+import { EntityProps } from "../../core/base/entity";
+import { EntityGroup } from "../../core/base/entity-group";
+import { SVGPoi, SVGPoiData } from "./svg-poi";
+
+export class SVGPois extends EntityGroup<typeof SVGPoi> {
+  constructor(props: EntityProps<SVGPoiData[]>) {
+    super({ type: SVGPoi, key: "svg-pois", ...props });
+  }
+}

+ 7 - 6
src/board/core/base/entity-factory.ts

@@ -1,6 +1,7 @@
 import { getChangePart } from "../../shared";
 import {
   Entity,
+  EntityKey,
   EntityShape,
   EntityTree,
   EntityType,
@@ -14,7 +15,7 @@ export const entityFactory = <
   C extends EntityType<T, S, EntityTree<P>>
 >(
   attrib: T,
-  key: string,
+  key: EntityKey,
   Type: C,
   parent?: P,
   extra?: (self: EntityTypeInstance<C>) => void,
@@ -90,7 +91,7 @@ export const incEntitysFactoryGenerate = <
   return (attribsRaw: T[]) => {
     const attribs = attribsRaw as any[];
     if (!inited && attribs.length && typeof attribs[0] === "object") {
-      useIndex = !!attribs[0].id;
+      useIndex = "id" in attribs[0];
       inited = true;
     }
 
@@ -98,14 +99,14 @@ export const incEntitysFactoryGenerate = <
       !useIndex &&
       attribs.length &&
       attribs.some(
-        (item) => typeof item !== "object" || typeof item.id !== "string"
+        (item) =>
+          typeof item !== "object" ||
+          (typeof item.id !== "string" && typeof item.id !== "number")
       )
     ) {
       throw "attribs 不合法,缺少id";
     }
-    const newKeys = attribs.map((attrib, ndx) =>
-      useIndex ? ndx.toString() : attrib.id
-    );
+    const newKeys = attribs.map((attrib, ndx) => (useIndex ? ndx : attrib.id));
 
     if (new Set(newKeys).size !== newKeys.length) {
       throw "attribs 的id不合法 不可重复";

+ 2 - 6
src/board/core/base/entity-group.ts

@@ -11,22 +11,18 @@ import { IncEntitysFactory, incEntitysFactoryGenerate } from "./entity-factory";
 type Data<T extends EntityType> = T extends EntityType<infer D> ? D : never;
 
 export class EntityGroup<
-  T extends EntityType<
-    any,
-    EntityShape,
-    EntityTree<EntityGroup<T>, InstanceType<T>>
-  >
+  T extends EntityType<any, EntityShape, EntityTree<any, any>>
 > extends Entity<
   Data<T>[],
   Group,
   EntityTree<EntityGroup<T>, InstanceType<T>>
 > {
   private incFactory: IncEntitysFactory<EntityGroup<T>, Data<T>, T>;
-  private;
 
   constructor({ type, ...props }: EntityProps<Data<T>[]> & { type: T }) {
     super(props);
     this.incFactory = incEntitysFactoryGenerate(type, this as EntityGroup<T>);
+    this.diffRedraw();
   }
 
   diffRedraw() {

+ 146 - 152
src/board/core/base/entity-root-server.ts

@@ -1,127 +1,8 @@
 import { Entity } from "../base/entity";
-import { onEntity, openOnEntityTree, TreeEvent } from "../event/spread";
-import { findEntityByShape } from "../base/entity-server";
-import { debounce, getChangePart, mergeFuns } from "../../shared";
 
 import { Root } from "./entity-root";
-
-export const injectPointerEvents = (root: Root) => {
-  const store = {
-    hovers: new Set<Entity>(),
-    focus: new Set<Entity>(),
-    drag: null as Entity,
-  };
-  const oldStore = {
-    hovers: [] as Entity[],
-    focus: [] as Entity[],
-    drag: null as Entity,
-  };
-
-  const emit = debounce(() => {
-    const hovers = [...store.hovers];
-    const focus = [...store.focus];
-    const hoverChange = getChangePart(hovers, oldStore.hovers);
-    const focusChange = getChangePart(focus, oldStore.hovers);
-
-    hoverChange.addPort.forEach((entity) => entity.bus.emit("hover"));
-    hoverChange.delPort.forEach((entity) => entity.bus.emit("leave"));
-    focusChange.addPort.forEach((entity) => entity.bus.emit("focus"));
-    focusChange.delPort.forEach((entity) => entity.bus.emit("blur"));
-
-    if (oldStore.drag !== store.drag) {
-      oldStore.drag && oldStore.drag.bus.emit("drop");
-      store.drag && store.drag.bus.emit("drag");
-    }
-
-    oldStore.drag = store.drag;
-    oldStore.hovers = hovers;
-    oldStore.focus = focus;
-  }, 16);
-
-  const needReleases = [
-    openOnEntityTree(root, "mouseenter"),
-    openOnEntityTree(root, "mouseleave"),
-    openOnEntityTree(root, "click"),
-    openOnEntityTree(root, "touchend"),
-    onEntity(root, "dragstart", (ev) => {
-      const hit = findEntityByShape(root, ev.target);
-      hit.contenteditable && (store.drag = hit);
-      emit();
-    }),
-    onEntity(root, "dragend", () => {
-      store.drag = null;
-      emit();
-    }),
-  ];
-
-  const enterHandler = ({ paths }: TreeEvent) => {
-    paths.forEach((entity) => {
-      store.hovers.add(entity);
-    });
-    emit();
-  };
-  const leaveHandler = ({ paths }: TreeEvent) => {
-    paths.forEach((entity) => {
-      store.hovers.delete(entity);
-    });
-    emit();
-  };
-  const clickHandler = ({ paths }: TreeEvent) => {
-    store.focus.clear();
-    paths.forEach((entity) => {
-      store.focus.add(entity);
-    });
-    emit();
-  };
-
-  root.bus.on("mouseenter", enterHandler);
-  root.bus.on("mouseleave", leaveHandler);
-  root.bus.on("click", clickHandler);
-  root.bus.on("touchend", clickHandler);
-
-  const destory = () => {
-    mergeFuns(needReleases)();
-    root.bus.off("mouseenter", enterHandler);
-    root.bus.off("mouseleave", leaveHandler);
-    root.bus.off("click", clickHandler);
-    root.bus.off("touchend", clickHandler);
-  };
-
-  root.bus.on("destroyBefore", destory);
-  return {
-    focus(...entitys: Entity[]) {
-      store.focus.clear();
-      entitys.forEach((entity) => store.focus.add(entity));
-      emit();
-    },
-    blur(...entitys: Entity[]) {
-      entitys.forEach((entity) => store.focus.delete(entity));
-      emit();
-    },
-    hover(...entitys: Entity[]) {
-      store.hovers.clear();
-      entitys.forEach((entity) => store.hovers.add(entity));
-      emit();
-    },
-    leave(...entitys: Entity[]) {
-      entitys.forEach((entity) => store.hovers.delete(entity));
-      emit();
-    },
-    drag(entity: Entity) {
-      store.drag = entity;
-      emit();
-    },
-    drop(entity: Entity) {
-      if (store.drag === entity) {
-        store.drag = entity;
-        emit();
-      }
-    },
-    destory,
-  };
-};
-
-export type PointerEvents = ReturnType<typeof injectPointerEvents>;
+import { Pos } from "../type";
+import { Transform } from "konva/lib/Util";
 
 // 指定某些entity可编辑
 export const openOnlyMode = (
@@ -165,7 +46,7 @@ export const openOnlyMode = (
 export type EditModeProps = {
   entitys: Entity[];
   only: boolean;
-  includeNews: boolean;
+  includeNews?: boolean;
 };
 export const openEditMode = async (
   root: Root,
@@ -235,40 +116,153 @@ export const openEditMode = async (
   };
 };
 
-const cursorResources = import.meta.glob("../resource/cursor?url");
-const cursorDefs = ["move", "inherit", "pointer"];
-const cursorMap = new WeakMap<Root, string[]>();
-const setCursorStyle = (root: Root, ico: string) => {
-  const style = cursorDefs.includes(ico)
-    ? ico
-    : ico in cursorResources
-    ? `url("${ico}"), auto`
-    : null;
-  if (!style) throw "ico 不存在!";
-  root.container.style.cursor = ico;
+const cursorResources = {
+  pic_pen_a: "/cursors/pic_pen_a.ico",
+  pic_pen_r: "/cursors/pic_pen_r.ico",
+  pic_pen: "/cursors/pic_pen.ico",
 };
-export const setCursor = (root: Root, ico: string) => {
-  if (!cursorMap.get(root)) {
-    cursorMap.set(root, []);
-  }
-  const stack = cursorMap.get(root);
-  const ndx = stack.length;
-  stack[ndx] = ico;
-  setCursorStyle(root, ico);
+export const addCursorResource = (key: string, url: string) => {
+  cursorResources[key] = url;
+};
+export const injectSetCursor = (root: Root) => {
+  const cursorStack = [];
+  const setCursorStyle = (ico: string) => {
+    const url = ico in cursorResources ? cursorResources[ico] : null;
+    root.container.style.cursor = url ? `url("${ico}"), auto` : ico;
+  };
 
-  return () => {
-    stack[ndx] = null;
-    let last = stack.length - 1;
-    for (; last >= 0; last--) {
-      if (stack[last] !== null) {
-        break;
+  return (ico: string) => {
+    const ndx = cursorStack.length;
+    cursorStack[ndx] = ico;
+    setCursorStyle(ico);
+
+    return () => {
+      cursorStack[ndx] = null;
+      let last = cursorStack.length - 1;
+      for (; last >= 0; last--) {
+        if (cursorStack[last] !== null) {
+          break;
+        }
+      }
+      if (last === -1) {
+        setCursorStyle("inherit");
+        cursorStack.length = 0;
+      } else if (last < ndx) {
+        setCursorStyle(cursorStack[last]);
+      }
+    };
+  };
+};
+
+export const injectConstant = (root: Root) => {
+  const origin: { [key in string]: number | Pos } = {};
+  const current: { [key in string]: number | Pos } = {};
+
+  let mat: Transform;
+  let scale: Pos;
+  let position: Pos;
+  let rCos: number, rSin: number;
+
+  root.bus.on("mounted", function handler() {
+    mat = root.stage.getTransform().invert();
+    scale = root.stage.scale();
+    position = root.stage.position();
+    let radians = root.stage.rotation() * (Math.PI / 180);
+    rCos = Math.cos(radians);
+    rSin = Math.sin(radians);
+
+    root.bus.off("mounted", handler);
+  });
+
+  const invView = (key: string) => {
+    if (key.startsWith("fix:")) {
+      if (typeof origin[key] === "number") {
+        current[key] = mat.point({ x: origin[key], y: 0 }).x;
+      } else {
+        current[key] = mat.point(origin[key]);
       }
     }
-    if (last === -1) {
-      setCursor(root, "inherit");
-      stack.length = 0;
-    } else if (last < ndx) {
-      setCursor(root, stack[last]);
+  };
+  root.bus.on("changeView", () => {
+    mat = root.stage.getTransform().invert();
+    Object.keys(origin).forEach(invView);
+  });
+
+  const invScale = (key: string) => {
+    if (key.startsWith("fixScale:")) {
+      if (typeof origin[key] === "number") {
+        current[key] = origin[key] / scale.x;
+      } else {
+        current[key] = {
+          x: origin[key].x / scale.x,
+          y: origin[key].y / scale.y,
+        };
+      }
     }
   };
+  root.bus.on("changeViewScale", (nscale) => {
+    scale = nscale;
+    Object.keys(origin).forEach(invScale);
+  });
+
+  const invPosition = (key: string) => {
+    if (key.startsWith("fixPosition:")) {
+      if (typeof origin[key] === "number") {
+        current[key] = origin[key] - position.x;
+      } else {
+        current[key] = {
+          x: origin[key].x - position.x,
+          y: origin[key].y - position.y,
+        };
+      }
+    }
+  };
+  root.bus.on("changeViewPosition", (nposition) => {
+    position = nposition;
+    Object.keys(origin).forEach(invPosition);
+  });
+
+  const invRotation = (key: string) => {
+    if (key.startsWith("fixRotation:")) {
+      const p = origin[key];
+      if (typeof p !== "number") {
+        current[key] = {
+          x: p.x * rCos - p.y * rSin,
+          y: p.x * rSin + p.y * rCos,
+        };
+      }
+    }
+  };
+  root.bus.on("changeViewRotation", (rotation) => {
+    let radians = rotation * (Math.PI / 180);
+    rCos = Math.cos(radians);
+    rSin = Math.sin(radians);
+    Object.keys(origin).forEach(invRotation);
+  });
+
+  return {
+    set(key: string, val: number) {
+      origin[key] = val;
+      invView(key);
+      invPosition(key);
+      invRotation(key);
+      invScale(key);
+    },
+    get<T extends number | Pos>(key: string): T {
+      return (current[key] || origin[key]) as T;
+    },
+  };
 };
+
+const rootStack: Root[] = [];
+export const pushRoot = (root: Root) => rootStack.push(root);
+export const popRoot = () => rootStack.pop();
+export const currentRoot = () => rootStack[rootStack.length - 1];
+export const currentConstant = new Proxy(
+  {},
+  {
+    get(_, p) {
+      return currentRoot().constant.get(p as string);
+    },
+  }
+);

+ 71 - 26
src/board/core/base/entity-root.ts

@@ -1,14 +1,16 @@
-import { Layer } from "konva/lib/Layer";
 import { Entity, EntityEvent, EntityTree } from "./entity";
-import { Stage } from "konva/lib/Stage";
-import { entityInit, entityMount } from "./entity-server";
-import { Emitter, Pos } from "../type";
+import konva from "konva";
+import { entityMount } from "./entity-server";
+import { Emitter, Pos, RootMat } from "../type";
 import {
-  injectPointerEvents,
-  PointerEvents,
   EditModeProps,
   openEditMode,
+  injectSetCursor,
+  injectConstant,
 } from "./entity-root-server";
+import { inRevise } from "../../shared";
+import { injectPointerEvents, PointerEvents } from "../event";
+import { Transform } from "konva/lib/Util";
 
 export type RootEvent = EntityEvent & {
   updateEntity: Entity;
@@ -20,30 +22,41 @@ export type RootEvent = EntityEvent & {
     delEntitys: Entity[];
     setEntitys: Entity[];
   };
-  viewChange: void;
+  changeView: RootMat;
+  changeViewPort: Pos;
+  changeViewScale: RootMat["scale"];
+  changeViewPosition: RootMat["position"];
+  changeViewRotation: RootMat["rotation"];
 };
 
 export class Root<T extends Entity = any> extends Entity<
   null,
-  Layer,
+  konva.Layer,
   EntityTree<never, T, Root<Entity>>
 > {
   container?: HTMLDivElement;
-  stage: Stage;
-  tempLayer: Layer;
+  stage: konva.Stage;
+  tempLayer: konva.Layer;
   bus: Emitter<RootEvent>;
+  mat: Transform;
+  invMat: Transform;
 
   constructor() {
     super({
       name: "container",
       attrib: null,
+      key: "root",
     });
     this.root = this;
-    entityInit(this);
+    this.stage = new konva.Stage({ container: document.createElement("div") });
+    this.mat = this.stage.getTransform();
+    this.invMat = this.mat.copy().invert();
   }
 
-  initShape(): Layer {
-    return new Layer();
+  setCursor = injectSetCursor(this);
+
+  initShape() {
+    return new konva.Layer();
   }
 
   trigger: PointerEvents;
@@ -54,7 +67,7 @@ export class Root<T extends Entity = any> extends Entity<
     this.trigger && this.trigger.destory();
   }
 
-  editMode(main: () => void, props: EditModeProps) {
+  editMode(main: () => void, props?: EditModeProps) {
     return openEditMode(this, main, props);
   }
 
@@ -66,24 +79,56 @@ export class Root<T extends Entity = any> extends Entity<
     return this.stage.getTransform().invert().point(pixel);
   }
 
+  constant = injectConstant(this);
   mount(container: HTMLDivElement): void {
     if (container === this.container && this.isMounted) return;
     if (!container) throw "mount 需要 container";
     this.container = container;
 
-    if (!this.isMounted) {
-      this.stage = new Stage({
-        container: this.container,
-        width: this.container.offsetWidth,
-        height: this.container.offsetHeight,
+    this.setTeleport(this.stage);
+    const w = this.container.offsetWidth;
+    const h = this.container.offsetHeight;
+
+    if (w !== this.stage.width() || h !== this.stage.height()) {
+      this.stage.width(w);
+      this.stage.height(h);
+    }
+    console.log(w, h);
+
+    this.stage.setContainer(this.container);
+    this.isMounted || entityMount(this);
+    this.bus.emit("changeViewPort", { x: w, y: h });
+  }
+
+  updateViewMat(mat: { position?: Pos; scale?: Pos; rotation?: number }) {
+    const scaleChange =
+      "scale" in mat && inRevise(mat.scale, this.stage.scale());
+    const positionChange =
+      "position" in mat && inRevise(mat.position, this.stage.position());
+    const rotateChange =
+      "rotation" in mat && inRevise(mat.rotation, this.stage.rotation());
+
+    if (rotateChange || scaleChange || positionChange) {
+      this.bus.emit("changeView", {
+        scale: this.stage.scale(),
+        position: this.stage.position(),
+        rotation: this.stage.rotation(),
       });
-      this.stage.add(this.shape);
-      entityMount(this);
-    } else {
-      this.stage.width(this.container.offsetWidth);
-      this.stage.height(this.container.offsetHeight);
-      this.stage.setContainer(this.container);
-      this.bus.emit("viewChange");
+      this.mat = this.stage.getTransform();
+      this.invMat = this.mat.copy().invert();
+    }
+
+    if (rotateChange) {
+      this.stage.rotate(mat.rotation);
+      this.bus.emit("changeViewRotation", mat.rotation);
+    }
+    if (scaleChange) {
+      this.stage.scale(mat.scale);
+      this.bus.emit("changeViewScale", mat.scale);
+    }
+    if (positionChange) {
+      this.stage.position(mat.scale);
+      this.bus.emit("changeViewPosition", mat.position);
     }
   }
 }

+ 70 - 76
src/board/core/base/entity-server.ts

@@ -1,6 +1,9 @@
 import { Layer } from "konva/lib/Layer";
 import { mergeFuns } from "../../shared";
 import { Entity, EntityShape, EntityTransmit } from "./entity";
+import { Root } from "./entity-root";
+import { Stage } from "konva/lib/Stage";
+import { contain } from "../helper/shape";
 
 export const traversEntityTree = (
   entity: Entity,
@@ -8,21 +11,28 @@ export const traversEntityTree = (
   inverse: boolean | "all" = false
 ) => {
   if (!inverse || inverse === "all") {
-    if (call(entity, false) === "interrupt") return;
+    if (call(entity, false) === "interrupt") {
+      return "interrupt";
+    }
   }
 
   const eqed: Entity[] = [];
   while (true) {
-    const child = entity.children.find((child) => eqed.includes(child));
-    if (!child) {
-      traversEntityTree(child, call, inverse);
+    const child = entity.children.find((child) => !eqed.includes(child));
+    if (child) {
+      if (traversEntityTree(child, call, inverse) === "interrupt") {
+        return "interrupt";
+      }
+      eqed.push(child);
     } else {
       break;
     }
   }
 
   if (inverse || inverse === "all") {
-    if (call(entity, true) === "interrupt") return;
+    if (call(entity, true) === "interrupt") {
+      return "interrupt";
+    }
   }
 };
 
@@ -31,14 +41,17 @@ export const bubbleTraversEntityTree = (
   call: (entity: Entity, inverse: boolean) => void | "interrupt",
   inverse: boolean | "all" = false
 ) => {
+  if (!entity) return;
   if (!inverse || inverse === "all") {
-    if (call(entity, false) === "interrupt") return;
+    if (call(entity, false) === "interrupt") return "interrupt";
   }
 
-  bubbleTraversEntityTree(entity.parent, call, inverse);
+  if (bubbleTraversEntityTree(entity.parent, call, inverse) === "interrupt") {
+    return "interrupt";
+  }
 
   if (inverse || inverse === "all") {
-    if (call(entity, true) === "interrupt") return;
+    if (call(entity, true) === "interrupt") return "interrupt";
   }
 };
 
@@ -61,12 +74,28 @@ export const findEntityByShape = <T extends Entity>(
   shape: EntityShape
 ) => {
   let find: T;
-  traversEntityTree(entity, (child) => {
-    if (child.shape === shape) {
-      find = entity as T;
-      return "interrupt";
+  if (shape instanceof Stage) {
+    if (entity instanceof Root && entity.stage === shape) {
+      return entity;
+    } else {
+      return null;
     }
-  });
+  }
+  const checked: EntityShape[] = [];
+
+  traversEntityTree(
+    entity,
+    (child) => {
+      if (contain(child.shape, shape, checked)) {
+        find = child as T;
+        return "interrupt";
+      } else {
+        checked.push(child.shape);
+      }
+    },
+    true
+  );
+
   return find;
 };
 
@@ -77,7 +106,7 @@ export const getEntityNdx = (entity: Entity) => {
   const zIndex = entity.getZIndex();
   const level = parent.children;
   for (let i = level.length - 1; i >= 0; i--) {
-    if (level[i] !== this && level[i].getZIndex() <= zIndex) {
+    if (level[i] !== entity && level[i].getZIndex() <= zIndex) {
       return i;
     }
   }
@@ -91,7 +120,7 @@ export const summarizeEntity = (entity: Entity) => {
   if (packNdx === null) return;
 
   const packChild = entity.children;
-  const oldNdx = packChild.indexOf(this);
+  const oldNdx = packChild.indexOf(entity);
 
   if (oldNdx !== packNdx + 1) {
     let rep = entity;
@@ -136,21 +165,24 @@ export const entityInit = <T extends Entity>(entity: T) => {
       mergeFuns(releases)();
     });
   }
-
-  const parentShape = (entity.getTeleport() || entity.parent.shape) as Layer;
-  parentShape.add(entity.shape);
 };
 
 export const entityMount = <T extends Entity>(entity: T) => {
-  traversEntityTree(entity, () => {
+  traversEntityTree(entity, (entity) => {
     entity.bus.emit("mountBefore");
+    if (entity.parent) {
+      const transmit = getEntityTransmitProps(entity.parent);
+      for (const key in transmit) {
+        entity[key] = transmit[key];
+      }
+    }
     entity.diffRedraw();
-    summarizeEntity(entity);
   });
 
   traversEntityTree(
     entity,
-    () => {
+    (entity) => {
+      setEntityShapePosition(entity);
       entity.isMounted = true;
       entity.bus.emit("mounted");
     },
@@ -158,14 +190,27 @@ export const entityMount = <T extends Entity>(entity: T) => {
   );
 };
 
+const setEntityShapePosition = (entity: Entity) => {
+  const parentShape = (entity.getTeleport() || entity.parent?.shape) as Layer;
+  if (parentShape) {
+    parentShape.add(entity.shape);
+  }
+
+  summarizeEntity(entity);
+};
+
 export const setEntityParent = <T extends Entity>(
   parent: T["parent"] | null,
   entity: T
 ) => {
-  const ndx = entity.parent.children.indexOf(this);
-  ~ndx && entity.parent.children.splice(ndx, 1);
+  if (entity.parent) {
+    const ndx = entity.parent.children.indexOf(entity);
+    ~ndx && entity.parent.children.splice(ndx, 1);
+  }
+
   entity.parent = parent;
-  parent && entity.parent.children.push(this);
+  parent && entity.parent.children.push(entity);
+  entity.isMounted && setEntityShapePosition(entity);
 };
 
 const getEntityTransmitProps = (parent: Entity): EntityTransmit => {
@@ -179,60 +224,9 @@ export const mountEntityTree = <T extends Entity>(
   entity: T
 ) => {
   if (parent) {
-    const transmit = getEntityTransmitProps(parent);
-    for (const key in transmit) {
-      entity[key] = parent[key];
-    }
     setEntityParent(parent, entity);
   }
-
-  entityInit(entity);
-  if (parent?.isMounted) {
-    entityMount(entity);
-  }
+  entityMount(entity);
   entity.root && entity.root.bus.emit("addEntity", entity);
   return entity;
 };
-
-export type EntityPointerStatus = {
-  drag: boolean;
-  hover: boolean;
-  focus: boolean;
-};
-
-export const openOnEntityPointerStatus = (entity: Entity) => {
-  const status: EntityPointerStatus = {
-    drag: false,
-    hover: false,
-    focus: false,
-  };
-  const handler = (partial: Partial<EntityPointerStatus>) => {
-    Object.assign(status, partial);
-    entity.bus.emit("pointerStatus", status);
-  };
-  const focusHandler = () => handler({ focus: true });
-  const blurHandler = () => handler({ focus: false });
-  const hoverHandler = () => handler({ hover: true });
-  const leaveHandler = () => handler({ hover: false });
-  const dragHandler = () => handler({ drag: true });
-  const dropHandler = () => handler({ drag: false });
-
-  entity.bus.on("focus", focusHandler);
-  entity.bus.on("blur", blurHandler);
-  entity.bus.on("hover", hoverHandler);
-  entity.bus.on("leave", leaveHandler);
-  entity.bus.on("drag", dragHandler);
-  entity.bus.on("drop", dropHandler);
-
-  const destory = () => {
-    entity.bus.off("focus", focusHandler);
-    entity.bus.off("blur", blurHandler);
-    entity.bus.off("hover", hoverHandler);
-    entity.bus.off("leave", leaveHandler);
-    entity.bus.off("drag", dragHandler);
-    entity.bus.off("drop", dropHandler);
-  };
-
-  entity.bus.on("destroyBefore", destory);
-  return destory;
-};

+ 43 - 13
src/board/core/base/entity.ts

@@ -9,7 +9,7 @@ import {
   setEntityParent,
   summarizeEntity,
   traversEntityTree,
-  EntityPointerStatus,
+  entityInit,
 } from "./entity-server";
 import { Stage } from "konva/lib/Stage";
 import { Emitter } from "../type";
@@ -18,16 +18,19 @@ import {
   closeOnEntityTree,
   openOnEntityTree,
   TreeEvent,
+  EntityPointerStatus,
   TreeEventName,
-} from "../event/spread";
+  openOnEntityPointerStatus,
+} from "../event";
 
 export type EntityTransmit = {
   root: Root;
 };
 export type EntityShape = Layer | Group | Shape;
+export type EntityKey = string | number;
 
 export type EntityProps<T> = {
-  key?: string;
+  key?: EntityKey;
   attrib: T;
   name?: string;
   zIndex?: number;
@@ -81,6 +84,13 @@ export type EntityTree<
   parent: P;
 };
 
+type PointerStyle<T extends Entity> = {
+  drag?: (entity: T) => void;
+  hover?: (entity: T) => void;
+  focus?: (entity: T) => void;
+  common?: (entity: T) => void;
+};
+
 type DEntityTree<
   P extends Entity = any,
   C extends Entity = any,
@@ -90,26 +100,26 @@ type DEntityTree<
 export class Entity<
   T = any,
   S extends EntityShape = EntityShape,
-  TR extends EntityTree = DEntityTree
+  TR extends EntityTree = DEntityTree,
+  TP extends EntityProps<T> = EntityProps<T>
 > {
   private zIndex: number;
-  private key: string;
+  private key: EntityKey;
   private teleport: Group | Layer;
 
   replyEvents: "auto" | "none" | "all" = "auto";
-  contenteditable: boolean = false;
   attrib: T;
   shape: S;
   name: string;
-  props: EntityProps<T>;
+  props: TP;
   bus = mitt() as Emitter<EntityEvent>;
 
   // tree
   root: TR["root"];
   parent: TR["parent"];
-  children: TR["children"];
+  children: TR["children"] = [];
 
-  constructor(props: EntityProps<T>) {
+  constructor(props: TP) {
     this.name = props.name;
     this.teleport = props.teleport as any;
     this.props = props;
@@ -117,9 +127,12 @@ export class Entity<
     this.zIndex = props.zIndex || 0;
     this.key = props.key;
 
-    if (typeof this.key !== "string") {
-      throw "entity 的key 必须为string";
+    if (typeof this.key !== "string" && typeof this.key !== "number") {
+      console.log(this);
+      throw "entity 的key 必须为string | number";
     }
+    entityInit(this);
+    openOnEntityPointerStatus(this);
   }
 
   focus() {
@@ -162,6 +175,23 @@ export class Entity<
     return this.teleport;
   }
 
+  private __pointerStatusHandler: (status: EntityPointerStatus) => void;
+  setPointerStyle(style?: PointerStyle<this>) {
+    if (this.__pointerStatusHandler) {
+      this.bus.off("pointerStatus", this.__pointerStatusHandler);
+    }
+    if (!style || Object.keys(style).length === 0) return;
+    this.__pointerStatusHandler = (status: EntityPointerStatus) => {
+      status.hover && style.hover && style.hover(this);
+      status.focus && style.focus && style.focus(this);
+      status.drag && style.drag && style.drag(this);
+      if (!status.hover && !status.focus && !status.drag && style.common) {
+        style.common(this);
+      }
+    };
+    this.bus.on("pointerStatus", this.__pointerStatusHandler);
+  }
+
   getKey() {
     return this.key;
   }
@@ -190,12 +220,12 @@ export class Entity<
 
   addChild(entity: TR["children"] | TR["children"][0]) {
     const entitys = Array.isArray(entity) ? entity : [entity];
+
     for (const entity of entitys) {
-      if (!entity.isMounted) {
+      if (entity.isMounted) {
         mountEntityTree(this, entity);
       } else {
         setEntityParent(this, entity);
-        summarizeEntity(entity);
       }
     }
   }

+ 48 - 0
src/board/core/event/entity-event.ts

@@ -0,0 +1,48 @@
+import { debounce } from "../../shared";
+import { Entity } from "../base/entity";
+
+export type EntityPointerStatus = {
+  drag: boolean;
+  hover: boolean;
+  focus: boolean;
+};
+
+export const openOnEntityPointerStatus = (entity: Entity) => {
+  const status: EntityPointerStatus = {
+    drag: false,
+    hover: false,
+    focus: false,
+  };
+  const emti = debounce(() => {
+    entity.bus.emit("pointerStatus", status);
+  }, 16);
+  const handler = (partial: Partial<EntityPointerStatus>) => {
+    Object.assign(status, partial);
+    emti();
+  };
+  const focusHandler = () => handler({ focus: true });
+  const blurHandler = () => handler({ focus: false });
+  const hoverHandler = () => handler({ hover: true });
+  const leaveHandler = () => handler({ hover: false });
+  const dragHandler = () => handler({ drag: true });
+  const dropHandler = () => handler({ drag: false });
+
+  entity.bus.on("focus", focusHandler);
+  entity.bus.on("blur", blurHandler);
+  entity.bus.on("hover", hoverHandler);
+  entity.bus.on("leave", leaveHandler);
+  entity.bus.on("drag", dragHandler);
+  entity.bus.on("drop", dropHandler);
+
+  const destory = () => {
+    entity.bus.off("focus", focusHandler);
+    entity.bus.off("blur", blurHandler);
+    entity.bus.off("hover", hoverHandler);
+    entity.bus.off("leave", leaveHandler);
+    entity.bus.off("drag", dragHandler);
+    entity.bus.off("drop", dropHandler);
+  };
+
+  entity.bus.on("destroyBefore", destory);
+  return destory;
+};

+ 123 - 0
src/board/core/event/entity-root-event.ts

@@ -0,0 +1,123 @@
+import { onEntity, openOnEntityTree, TreeEvent } from "./index";
+import { findEntityByShape } from "../base/entity-server";
+import { debounce, getChangePart, mergeFuns } from "../../shared";
+import { Root } from "../base/entity-root";
+import { Entity } from "../base/entity";
+
+export const injectPointerEvents = (root: Root) => {
+  const store = {
+    hovers: new Set<Entity>(),
+    focus: new Set<Entity>(),
+    drag: null as Entity,
+  };
+  const oldStore = {
+    hovers: [] as Entity[],
+    focus: [] as Entity[],
+    drag: null as Entity,
+  };
+
+  const emit = debounce(() => {
+    const hovers = [...store.hovers];
+    const focus = [...store.focus];
+    const hoverChange = getChangePart(hovers, oldStore.hovers);
+    const focusChange = getChangePart(focus, oldStore.focus);
+
+    hoverChange.addPort.forEach((entity) => entity.bus.emit("hover"));
+    hoverChange.delPort.forEach((entity) => entity.bus.emit("leave"));
+    focusChange.addPort.forEach((entity) => entity.bus.emit("focus"));
+    focusChange.delPort.forEach((entity) => entity.bus.emit("blur"));
+
+    if (oldStore.drag !== store.drag) {
+      oldStore.drag && oldStore.drag.bus.emit("drop");
+      store.drag && store.drag.bus.emit("drag");
+    }
+
+    oldStore.drag = store.drag;
+    oldStore.hovers = hovers;
+    oldStore.focus = focus;
+  }, 16);
+
+  const needReleases = [
+    openOnEntityTree(root, "mouseover"),
+    openOnEntityTree(root, "mouseout"),
+    openOnEntityTree(root, "click"),
+    openOnEntityTree(root, "touchend"),
+    onEntity(root, "dragstart", (ev) => {
+      const hit = findEntityByShape(root, ev.target);
+      store.drag = hit;
+      emit();
+    }),
+    onEntity(root, "dragend", () => {
+      store.drag = null;
+      emit();
+    }),
+  ];
+
+  const enterHandler = ({ paths }: TreeEvent) => {
+    paths.forEach((entity) => {
+      store.hovers.add(entity);
+    });
+    emit();
+  };
+  const leaveHandler = ({ paths }: TreeEvent) => {
+    paths.forEach((entity) => {
+      store.hovers.delete(entity);
+    });
+    emit();
+  };
+  const clickHandler = ({ paths }: TreeEvent) => {
+    store.focus.clear();
+    paths.forEach((entity) => {
+      store.focus.add(entity);
+    });
+    emit();
+  };
+
+  root.bus.on("mouseover" as any, enterHandler);
+  root.bus.on("mouseout" as any, leaveHandler);
+  root.bus.on("click", clickHandler);
+  root.bus.on("touchend", clickHandler);
+
+  const destory = () => {
+    mergeFuns(needReleases)();
+    root.bus.off("mouseover" as any, enterHandler);
+    root.bus.off("mouseout" as any, leaveHandler);
+    root.bus.off("click", clickHandler);
+    root.bus.off("touchend", clickHandler);
+  };
+
+  root.bus.on("destroyBefore", destory);
+  return {
+    focus(...entitys: Entity[]) {
+      store.focus.clear();
+      entitys.forEach((entity) => store.focus.add(entity));
+      emit();
+    },
+    blur(...entitys: Entity[]) {
+      entitys.forEach((entity) => store.focus.delete(entity));
+      emit();
+    },
+    hover(...entitys: Entity[]) {
+      store.hovers.clear();
+      entitys.forEach((entity) => store.hovers.add(entity));
+      emit();
+    },
+    leave(...entitys: Entity[]) {
+      entitys.forEach((entity) => store.hovers.delete(entity));
+      emit();
+    },
+    drag(entity: Entity) {
+      store.drag = entity;
+      emit();
+    },
+    drop(entity: Entity) {
+      if (store.drag === entity) {
+        store.drag = entity;
+        emit();
+      }
+    },
+    destory,
+  };
+};
+
+export type PointerEvents = ReturnType<typeof injectPointerEvents>;

+ 7 - 3
src/board/core/event/spread.ts

@@ -6,7 +6,7 @@ import {
 } from "../base/entity-server";
 import { Root } from "../base/entity-root";
 
-const canEntityReply = (entity: Entity) => {
+export const canEntityReply = (entity: Entity) => {
   let canReply = true;
   bubbleTraversEntityTree(entity, (entity) => {
     if (entity.replyEvents === "none") {
@@ -63,7 +63,7 @@ export const emitEntityTree = (
   });
 
   let prevStatus = "all";
-  for (let i = paths.length - 1; i >= 0; i++) {
+  for (let i = paths.length - 1; i >= 0; i--) {
     const currStatus = paths[i].replyEvents;
     if (
       currStatus === "none" ||
@@ -103,7 +103,8 @@ const entityTreeDistributor = (root: Root) => {
 
     shape.on(name, (ev: KonvaEventObject<any>) => {
       const self = findEntityByShape(root, ev.target);
-      emitEntityTree(self, name);
+      // console.log(ev.target, self);
+      self && emitEntityTree(self, name);
     });
   };
 
@@ -196,3 +197,6 @@ export const closeOnEntityTree = (entity: Entity, key?: string) => {
   const [name] = getEventArgs(key);
   distributor && distributor.off(entity, name);
 };
+
+export * from "./entity-root-event";
+export * from "./entity-event";

+ 52 - 0
src/board/core/helper/entity-drag.ts

@@ -0,0 +1,52 @@
+import { KonvaEventObject } from "konva/lib/Node";
+import { Entity } from "../base/entity";
+import { canEntityReply } from "../event";
+import { Pos } from "../type";
+
+export type DragHandlers = (ev: KonvaEventObject<any>) => {
+  move: (move: Pos, ev: KonvaEventObject<any>) => void;
+  end?: (ev: KonvaEventObject<any>) => void;
+};
+
+export const openEntityDrag = <T extends Entity>(
+  entity: T,
+  getHandlers: DragHandlers
+) => {
+  entity.shape.draggable(true);
+
+  let canReply = false;
+  let moveHandler: ReturnType<DragHandlers>["move"];
+  let endHandler: ReturnType<DragHandlers>["end"];
+  let resolve: () => void;
+
+  entity.shape.on("dragstart.drag", (ev) => {
+    const promise = new Promise<void>((r) => (resolve = r));
+    entity.root.editMode(() => promise, { only: true, entitys: [entity] });
+
+    canReply = canEntityReply(entity);
+    if (canReply) {
+      const handlers = getHandlers(ev);
+      moveHandler = handlers.move;
+      endHandler = handlers.end;
+    }
+  });
+
+  entity.shape.dragBoundFunc((pos, ev) => {
+    if (canReply) {
+      moveHandler(entity.root.invMat.point(pos), ev);
+    }
+    return entity.shape.absolutePosition();
+  });
+
+  entity.shape.on("dragend.drag", (ev) => {
+    moveHandler = null;
+    canReply = false;
+    endHandler && endHandler(ev);
+    resolve && resolve();
+  });
+
+  return () => {
+    entity.shape.draggable(false);
+    entity.shape.off("dragstart.drag dragend.drag");
+  };
+};

+ 21 - 0
src/board/core/helper/shape.ts

@@ -0,0 +1,21 @@
+import { EntityShape } from "../base/entity";
+
+export const contain = (
+  parent: EntityShape,
+  eqShape: EntityShape,
+  checked: EntityShape[] = []
+) => {
+  if (checked.includes(parent)) return false;
+
+  if (parent === eqShape) {
+    return true;
+  }
+  if ("children" in parent) {
+    for (const child of parent.children) {
+      if (contain(child, eqShape, checked)) {
+        return true;
+      }
+    }
+  }
+  return false;
+};

+ 152 - 0
src/board/core/helper/svg.ts

@@ -0,0 +1,152 @@
+import { Group } from "konva/lib/Group";
+import { Path } from "konva/lib/shapes/Path";
+import { Rect } from "konva/lib/shapes/Rect";
+
+export type SVGPath = {
+  fill?: string;
+  stroke?: string;
+  strokeWidth?: number;
+  data: string;
+};
+
+export type SVGProps = {
+  size: number[];
+  realWidth: number;
+  offset: number[];
+  paths: SVGPath[];
+};
+
+export type SVGPathProps = {
+  realWidth?: number;
+  offset?: number[];
+  fixed?: boolean;
+};
+
+export type PathsShapeAct = ReturnType<typeof pathsToShapeAct>;
+
+export const pathsToShapeAct = (
+  svg: SVGProps,
+  props: SVGPathProps = { fixed: true },
+  test = false
+) => {
+  const size = svg.size;
+  const realSize = props.realWidth || svg.realWidth;
+  const scale = realSize / size[0];
+  const realBound = size.map((p) => p * scale);
+  const offset = (props.offset || svg.offset).map((v) => v * scale);
+
+  const paths = svg.paths.map(
+    (path, ndx) =>
+      new Path({
+        data: path.data,
+        id: `path-${ndx}`,
+        name: `path`,
+        strokeScaleEnabled: !!props.fixed,
+        scale: { x: scale, y: scale },
+      })
+  );
+
+  const common = () => {
+    console.log(svg.paths);
+    paths.forEach((path, ndx) => {
+      const attrib = svg.paths[ndx];
+      attrib.fill && path.fill(attrib.fill);
+      attrib.stroke && path.stroke(attrib.stroke);
+      attrib.strokeWidth && path.strokeWidth(attrib.strokeWidth);
+    });
+  };
+
+  const rect = new Rect({
+    x: offset[0],
+    y: offset[1],
+    name: "rect",
+    width: realBound[0],
+    height: realBound[1],
+    fill: `rgba(0, 0, 0, ${test ? 0.3 : 0})`,
+  });
+
+  const offsetGroup = new Group();
+  offsetGroup.add(...paths, rect);
+  offsetGroup.x(-realBound[0] / 2);
+  offsetGroup.y(-realBound[1] / 2);
+  common();
+
+  return {
+    shape: offsetGroup,
+    common,
+    setStroke(color: string) {
+      paths.forEach((path) => {
+        path.stroke(color);
+      });
+    },
+    setFill(color: string) {
+      paths.forEach((path) => {
+        path.fill(color);
+      });
+    },
+  };
+};
+
+const temp = document.createElement("div");
+export const analysisSvgContent = (svgContent: string) => {
+  temp.innerHTML = svgContent;
+  const svg = temp.querySelector("svg");
+  const viewBoxStr = svg.getAttribute("viewBox");
+  const realWidth = svg.width.baseVal.value;
+  const offset = [0, 0];
+  const size = [svg.width.baseVal.value, svg.height.baseVal.value];
+  if (viewBoxStr) {
+    const viewBox = viewBoxStr.split(" ").map(Number);
+    offset[0] = viewBox[0];
+    offset[1] = viewBox[1];
+    size[0] = viewBox[2] - viewBox[0];
+    size[1] = viewBox[3] - viewBox[1];
+  }
+
+  const paths = Array.from(svg.querySelectorAll("path"));
+  const pathDatas = paths.map((path) => {
+    const fill = path.getAttribute("fill") || "#000";
+    const data = path.getAttribute("d");
+    const stroke = path.getAttribute("stroke") || "#000";
+    const strokeWidth = path.getAttribute("stroke-width");
+    return {
+      fill,
+      data,
+      stroke,
+      strokeWidth: strokeWidth && Number(strokeWidth),
+    };
+  });
+  return {
+    size,
+    realWidth,
+    offset,
+    paths: pathDatas,
+  };
+};
+
+const cache: { [key in string]: Promise<string> | string } = {};
+export const loadSvgContent = (url: string) => {
+  if (url in cache) {
+    if (typeof cache[url] === "string") {
+      return Promise.resolve(cache[url]);
+    } else {
+      return cache[url];
+    }
+  } else {
+    return (cache[url] = fetch(url, { method: "GET" })
+      .then((res) => res.text())
+      .then((data) => {
+        cache[url] = data;
+        return data;
+      }));
+  }
+};
+
+const SVGTypes: { [key in string]: SVGProps } = {};
+export const getSVGProps = async (url: string) => {
+  if (!(url in SVGTypes)) {
+    const svgContent = await loadSvgContent(url);
+    SVGTypes[url] = analysisSvgContent(svgContent);
+  }
+  return SVGTypes[url];
+};

+ 1 - 0
src/board/core/type.d.ts

@@ -13,3 +13,4 @@ export interface Emitter<Events extends Record<EventType, unknown>> {
 }
 
 export type Pos = { x: number; y: number };
+export type RootMat = { position: Pos; scale: Pos; rotation: number };

+ 1 - 0
tsconfig.json

@@ -10,6 +10,7 @@
     "skipLibCheck": true,
     /* Bundler mode */
     "moduleResolution": "Node",
+    "allowSyntheticDefaultImports": true,
     "allowImportingTsExtensions": false,
     "resolveJsonModule": true,
     "isolatedModules": true,