فهرست منبع

fix: 添加公用方法

bill 1 سال پیش
والد
کامیت
0edde1897a

+ 274 - 0
src/board/core/base/entity-root-server.ts

@@ -0,0 +1,274 @@
+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>;
+
+// 指定某些entity可编辑
+export const openOnlyMode = (
+  root: Root,
+  _entitys: Entity[],
+  includeNews = true
+) => {
+  const prevRootReply = root.replyEvents;
+
+  const entitys: (Entity | null)[] = [];
+  const prevEntitysReply: Entity["replyEvents"][] = [];
+  const pushEntity = (entity: Entity) => {
+    const ndx = entitys.length;
+    entitys[ndx] = entity;
+    prevEntitysReply[ndx] = entity.replyEvents;
+    entity.replyEvents = "all";
+  };
+  const delEntity = (entity: Entity) => {
+    const ndx = entitys.indexOf(entity);
+    prevEntitysReply[ndx] = null;
+    entitys[ndx] = null;
+  };
+
+  _entitys.forEach(pushEntity);
+  // 新增的entity可以使用事件
+  includeNews && root.bus.on("addEntity", pushEntity);
+  root.bus.on("delEntity", delEntity);
+
+  return () => {
+    root.bus.off("addEntity", pushEntity);
+    root.bus.off("delEntity", delEntity);
+    root.replyEvents = prevRootReply;
+    entitys.forEach((entity, ndx) => {
+      if (entity) {
+        entity.replyEvents = prevEntitysReply[ndx];
+      }
+    });
+  };
+};
+
+export type EditModeProps = {
+  entitys: Entity[];
+  only: boolean;
+  includeNews: boolean;
+};
+export const openEditMode = async (
+  root: Root,
+  main: () => any,
+  props: EditModeProps = { entitys: [], only: false, includeNews: false }
+) => {
+  root.bus.emit("dataChangeBefore");
+  const quitOnlyMode =
+    props.only && openOnlyMode(root, props.entitys, props.includeNews);
+
+  const addEntitys = [];
+  const setEntitys = [];
+  const delEntitys = [];
+  const addHandler = (entity: Entity) => {
+    addEntitys.push(entity);
+  };
+  const setHandler = (entity: Entity) => {
+    if (
+      !addEntitys.includes(entity) &&
+      !delEntitys.includes(entity) &&
+      !setEntitys.includes(entity)
+    ) {
+      setEntitys.push(entity);
+    }
+  };
+  const delHandler = (entity: Entity) => {
+    const delNdx = delEntitys.indexOf(entity);
+    if (~delNdx) return;
+
+    const addNdx = addEntitys.indexOf(entity);
+    if (~addNdx) addEntitys.splice(addNdx, 1);
+
+    const setNdx = setEntitys.indexOf(entity);
+    if (~setNdx) setEntitys.splice(setNdx, 1);
+
+    delEntitys.push(entity);
+  };
+
+  root.bus.on("addEntity", addHandler);
+  root.bus.on("updateEntity", setHandler);
+  root.bus.on("delEntity", delHandler);
+
+  let state = "normal";
+  const interrupt = new Promise<void>((resolve) => {
+    state = "interrupt";
+    resolve();
+  });
+
+  const draw = Promise.any([interrupt, Promise.resolve(main())]).then(() => {
+    root.bus.off("addEntity", addHandler);
+    root.bus.off("updateEntity", setHandler);
+    root.bus.off("delEntity", addHandler);
+    quitOnlyMode && quitOnlyMode();
+
+    const change = {
+      addEntitys,
+      delEntitys,
+      setEntitys,
+    };
+    root.bus.emit("dataChangeAfter", change);
+    return { state, change };
+  });
+
+  return {
+    draw,
+    interrupt,
+  };
+};
+
+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;
+};
+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);
+
+  return () => {
+    stack[ndx] = null;
+    let last = stack.length - 1;
+    for (; last >= 0; last--) {
+      if (stack[last] !== null) {
+        break;
+      }
+    }
+    if (last === -1) {
+      setCursor(root, "inherit");
+      stack.length = 0;
+    } else if (last < ndx) {
+      setCursor(root, stack[last]);
+    }
+  };
+};

+ 40 - 6
src/board/core/base/entity-root.ts

@@ -1,14 +1,25 @@
 import { Layer } from "konva/lib/Layer";
 import { Entity, EntityEvent, EntityTree } from "./entity";
 import { Stage } from "konva/lib/Stage";
-import { entityInit, entityMount } from "./entity-util";
-import { injectMouseEvent } from "../event";
-import { Emitter } from "../mitt";
+import { entityInit, entityMount } from "./entity-server";
+import { Emitter, Pos } from "../type";
+import {
+  injectPointerEvents,
+  PointerEvents,
+  EditModeProps,
+  openEditMode,
+} from "./entity-root-server";
 
 export type RootEvent = EntityEvent & {
-  dataChange: void;
+  updateEntity: Entity;
+  addEntity: Entity;
+  delEntity: Entity;
   dataChangeBefore: void;
-  dataChangeAfter: void;
+  dataChangeAfter: {
+    addEntitys: Entity[];
+    delEntitys: Entity[];
+    setEntitys: Entity[];
+  };
   viewChange: void;
 };
 
@@ -27,14 +38,34 @@ export class Root<T extends Entity = any> extends Entity<
       name: "container",
       attrib: null,
     });
+    this.root = this;
     entityInit(this);
-    injectMouseEvent(this, this.stage);
   }
 
   initShape(): Layer {
     return new Layer();
   }
 
+  trigger: PointerEvents;
+  openPointerEvents() {
+    this.trigger = injectPointerEvents(this);
+  }
+  closePointerEvents() {
+    this.trigger && this.trigger.destory();
+  }
+
+  editMode(main: () => void, props: EditModeProps) {
+    return openEditMode(this, main, props);
+  }
+
+  getPixel(real: Pos) {
+    return this.stage.getTransform().point(real);
+  }
+
+  getReal(pixel: Pos) {
+    return this.stage.getTransform().invert().point(pixel);
+  }
+
   mount(container: HTMLDivElement): void {
     if (container === this.container && this.isMounted) return;
     if (!container) throw "mount 需要 container";
@@ -49,7 +80,10 @@ export class Root<T extends Entity = any> extends Entity<
       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");
     }
   }
 }

+ 44 - 0
src/board/core/base/entity-util.ts

@@ -190,5 +190,49 @@ export const mountEntityTree = <T extends Entity>(
   if (parent?.isMounted) {
     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;
+};

+ 59 - 5
src/board/core/base/entity.ts

@@ -1,6 +1,5 @@
 import { Group } from "konva/lib/Group";
 import { Layer } from "konva/lib/Layer";
-import { EntityMouseStatus } from "../event";
 import mitt from "mitt";
 import { Root } from "./entity-root";
 import { Shape } from "konva/lib/Shape";
@@ -10,10 +9,17 @@ import {
   setEntityParent,
   summarizeEntity,
   traversEntityTree,
-} from "./entity-util";
+  EntityPointerStatus,
+} from "./entity-server";
 import { Stage } from "konva/lib/Stage";
-import { Emitter } from "../mitt";
+import { Emitter } from "../type";
 import { DEV } from "../../env";
+import {
+  closeOnEntityTree,
+  openOnEntityTree,
+  TreeEvent,
+  TreeEventName,
+} from "../event/spread";
 
 export type EntityTransmit = {
   root: Root;
@@ -36,7 +42,19 @@ export type EntityEvent = {
   destroyBefore: void;
   destroyed: void;
   updateAttrib: void;
-  mouseStatus: EntityMouseStatus;
+  pointerStatus: EntityPointerStatus;
+  hover: void;
+  leave: void;
+  focus: void;
+  blur: void;
+  drag: void;
+  drop: void;
+} & EntityPointerEvent;
+
+type EntityPointerEvent = {
+  [key in TreeEventName]: TreeEvent;
+} & {
+  [key in TreeEventName as `${key}.capture`]: TreeEvent;
 };
 
 export type EntityType<
@@ -78,6 +96,7 @@ export class Entity<
   private key: string;
   private teleport: Group | Layer;
 
+  replyEvents: "auto" | "none" | "all" = "auto";
   contenteditable: boolean = false;
   attrib: T;
   shape: S;
@@ -103,6 +122,33 @@ export class Entity<
     }
   }
 
+  focus() {
+    this.root.trigger && this.root.trigger.focus(this);
+  }
+  blur() {
+    this.root.trigger && this.root.trigger.blur(this);
+  }
+  hover() {
+    this.root.trigger && this.root.trigger.hover(this);
+  }
+  leave() {
+    this.root.trigger && this.root.trigger.leave(this);
+  }
+  drag() {
+    this.root.trigger && this.root.trigger.drag(this);
+  }
+  drop() {
+    this.root.trigger && this.root.trigger.drop(this);
+  }
+
+  openEvent(name: TreeEventName) {
+    openOnEntityTree(this, name);
+  }
+
+  closeEvent(name: TreeEventName) {
+    closeOnEntityTree(this, name);
+  }
+
   initShape(): S {
     return new Group() as S;
   }
@@ -135,6 +181,7 @@ export class Entity<
     this.attrib = newAttrib as T;
     this.isMounted && this.diffRedraw();
     attribUpdated && this.bus.emit("updateAttrib");
+    this.root && this.root.bus.emit("updateEntity", this);
   }
 
   needReleases(): (() => void) | Array<() => void> {
@@ -144,7 +191,12 @@ export class Entity<
   addChild(entity: TR["children"] | TR["children"][0]) {
     const entitys = Array.isArray(entity) ? entity : [entity];
     for (const entity of entitys) {
-      mountEntityTree(entity, this);
+      if (!entity.isMounted) {
+        mountEntityTree(this, entity);
+      } else {
+        setEntityParent(this, entity);
+        summarizeEntity(entity);
+      }
     }
   }
   isMounted = false;
@@ -164,6 +216,8 @@ export class Entity<
 
   destory() {
     this.bus.emit("destroyBefore");
+    this.root && this.root.bus.emit("delEntity", this);
+
     while (this.children.length) {
       this.children[0].destory();
     }

+ 0 - 113
src/board/core/event/index.ts

@@ -1,113 +0,0 @@
-import {
-  bubbleTraversEntityTree,
-  findEntityByShape,
-  traversEntityTree,
-} from "../base/entity-util";
-import { KonvaEventListener, KonvaEventObject } from "konva/lib/Node";
-import { getChangePart } from "../../shared";
-import { Stage } from "konva/lib/Stage";
-import { Entity, EntityShape } from "../base/entity";
-
-export type EntityMouseStatus = {
-  drag: boolean;
-  hover: boolean;
-  focus: boolean;
-};
-
-export const onEntityEvent = (
-  entity: Entity,
-  namespace: string,
-  shape: EntityShape | Stage = entity.shape
-) => {
-  const events: string[] = [];
-  const use = (
-    name: string,
-    cb: KonvaEventListener<any, KonvaEventObject<any>>
-  ) => {
-    const eventName = `${name}.${namespace}`;
-    shape.on(eventName, cb);
-    events.push(eventName);
-  };
-  entity.bus.on("destroyBefore", () => shape.off(events.join(" ")));
-
-  return use;
-};
-
-export const injectMouseEvent = (root: Entity, stage: Stage) => {
-  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 use = onEntityEvent(root, "mouse-style", stage);
-
-  const emit = () => {
-    const hovers = [...store.hovers];
-    const focus = [...store.focus];
-    const hoverChange = getChangePart(hovers, oldStore.hovers);
-    const focusChange = getChangePart(focus, oldStore.hovers);
-    const changeEntitys = new Set<Entity>([
-      ...hoverChange.addPort,
-      ...hoverChange.delPort,
-      ...focusChange.addPort,
-      ...focusChange.delPort,
-    ]);
-    if (oldStore.drag !== store.drag) {
-      oldStore.drag && changeEntitys.add(oldStore.drag);
-      store.drag && changeEntitys.add(store.drag);
-    }
-
-    for (const entity of changeEntitys) {
-      const status: EntityMouseStatus = {
-        hover: hovers.includes(entity),
-        focus: focus.includes(entity),
-        drag: store.drag === entity,
-      };
-      entity.bus.emit("mouseStatus", status);
-    }
-
-    oldStore.drag = store.drag;
-    oldStore.hovers = hovers;
-    oldStore.focus = focus;
-  };
-
-  use(`mouseenter`, (ev) => {
-    const entity = findEntityByShape(root, ev.target);
-    bubbleTraversEntityTree(entity, (hit) => {
-      store.hovers.add(hit);
-    });
-    emit();
-  });
-
-  use(`mouseleave`, (ev) => {
-    const entity = findEntityByShape(root, ev.target);
-    traversEntityTree(entity, (hit) => {
-      store.hovers.delete(hit);
-    });
-    emit();
-  });
-
-  use(`click touchend`, (ev) => {
-    store.focus.clear();
-    const entity = findEntityByShape(root, ev.target);
-    bubbleTraversEntityTree(entity, (hit) => {
-      hit.contenteditable && store.focus.add(hit);
-    });
-    emit();
-  });
-
-  use(`dragstart`, (ev) => {
-    const hit = findEntityByShape(root, ev.target);
-    hit.contenteditable && (store.drag = hit);
-    emit();
-  });
-  use(`dragend`, () => {
-    store.drag = null;
-    emit();
-  });
-};

+ 131 - 85
src/board/core/event/spread.ts

@@ -1,94 +1,136 @@
-import { Entity, EntityShape } from "../base/entity";
+import { Entity } from "../base/entity";
 import { KonvaEventObject } from "konva/lib/Node";
 import {
   bubbleTraversEntityTree,
   findEntityByShape,
-} from "../base/entity-util";
-import { Stage } from "konva/lib/Stage";
+} from "../base/entity-server";
+import { Root } from "../base/entity-root";
 
-const getEventArgs = (key: string) => {
-  const [name, ...args] = key.split(".");
-  const useCapture = args.includes("capture");
-  return [name, useCapture] as const;
+const canEntityReply = (entity: Entity) => {
+  let canReply = true;
+  bubbleTraversEntityTree(entity, (entity) => {
+    if (entity.replyEvents === "none") {
+      canReply = false;
+      return "interrupt";
+    } else if (entity.replyEvents === "all") {
+      return "interrupt";
+    }
+  });
+  return canReply;
 };
 
-// 将konva的时间转发到entity
-const entityTreeDistributor = (
-  root: Entity,
-  shape: EntityShape | Stage = root.shape
+// 仅限内部使用
+export const onEntity = (
+  entity: Entity,
+  event: string,
+  cb: (ev: KonvaEventObject<any>) => void
 ) => {
-  const handlerMap: { [key in string]: Entity[] } = {};
-  const addedKeys: string[] = [];
+  const handler = (ev: KonvaEventObject<any>) =>
+    canEntityReply(entity) && cb(ev);
+  const shape = entity === entity.root ? entity.root.shape : entity.shape;
+  shape.on(event, handler);
+  const destory = () => shape.off(event, handler);
+  entity.bus.on("destroyBefore", destory);
+  return destory;
+};
+
+export type TreeEvent = {
+  paths: Entity[];
+  target: Entity;
+  cancelBubble: boolean;
+};
+export type TreeEventName = "mouseenter" | "mouseleave" | "click" | "touchend";
+
+export const emitEntityTree = (
+  entity: Entity,
+  name: TreeEventName,
+  one = false
+) => {
+  const ev: TreeEvent = {
+    cancelBubble: false,
+    target: entity,
+    paths: [entity],
+  };
+  if (one) {
+    entity.bus.emit(name as any, ev);
+    entity.bus.emit((name + ".capture") as any, ev);
+    return;
+  }
+
+  let paths: Entity[] = [];
+  bubbleTraversEntityTree(entity, (t) => {
+    paths.push(t);
+  });
+
+  let prevStatus = "all";
+  for (let i = paths.length - 1; i >= 0; i++) {
+    const currStatus = paths[i].replyEvents;
+    if (
+      currStatus === "none" ||
+      (prevStatus === "none" && currStatus === "auto")
+    ) {
+      paths.splice(i--, 1);
+      prevStatus = "none";
+    } else {
+      prevStatus = "all";
+    }
+  }
+
+  ev.paths = paths;
+  for (const entity of paths) {
+    entity.bus.emit(name as any, ev);
+    if (ev.cancelBubble) break;
+  }
+
+  ev.cancelBubble = false;
+  paths = paths.reverse();
+  ev.paths = paths;
+  for (const entity of paths) {
+    entity.bus.emit((name + ".capture") as any, ev);
+    if (ev.cancelBubble) break;
+  }
+};
+
+// 将konva的时间转发到entity
+const entityTreeDistributor = (root: Root) => {
+  const shape = root.stage;
+  const listenEvents: TreeEventName[] = [];
+  const handlerMap: { [key in TreeEventName]?: Entity[] } = {};
+
+  const addEvent = (name: TreeEventName) => {
+    if (listenEvents.includes(name)) return;
+    listenEvents.push(name);
 
-  const addEvent = (key: string) => {
-    const [name] = getEventArgs(key);
-    if (addedKeys.includes(name)) return;
     shape.on(name, (ev: KonvaEventObject<any>) => {
       const self = findEntityByShape(root, ev.target);
-      const paths: Entity[] = [];
-      bubbleTraversEntityTree(self, (t) => {
-        paths.push(t);
-      });
-
-      const bubbles: Entity[] = [];
-      const captures: Entity[] = [];
-      for (const key in handlerMap) {
-        const [cname, useCapture] = getEventArgs(key);
-        if (name === cname) {
-          if (useCapture) {
-            captures.push(...handlerMap[key]);
-          } else {
-            bubbles.push(...handlerMap[key]);
-          }
-        }
-      }
-
-      const bubblePaths = bubbles
-        .filter((entity) => paths.includes(entity))
-        .sort((a, b) => paths.indexOf(a) - paths.indexOf(b));
-      for (const entity of bubblePaths) {
-        entity.bus.emit(name as any, ev);
-        if (ev.cancelBubble) break;
-      }
-      ev.cancelBubble = false;
-
-      const capturePaths = captures
-        .filter((entity) => paths.includes(entity))
-        .sort((a, b) => paths.indexOf(b) - paths.indexOf(a));
-      for (const entity of capturePaths) {
-        entity.bus.emit((name + ".capture") as any, ev);
-        if (ev.cancelBubble) break;
-      }
-      ev.cancelBubble = false;
+      emitEntityTree(self, name);
     });
-    addedKeys.push(name);
   };
 
-  const delEvent = (key: string) => {
-    const [name] = getEventArgs(key);
-    const ndx = addedKeys.indexOf(name);
+  const delEvent = (name: TreeEventName) => {
+    const ndx = listenEvents.indexOf(name);
     if (!~ndx) return;
+    listenEvents.splice(ndx, 1);
     shape.off(name);
-    addedKeys.splice(ndx, 1);
   };
 
   return {
-    on: (entity: Entity, key: string) => {
-      if (!handlerMap[key]) {
-        handlerMap[key] = [];
+    on: (entity: Entity, name: TreeEventName) => {
+      if (!handlerMap[name]) {
+        handlerMap[name] = [];
       }
-      handlerMap[key].push(entity);
-      addEvent(key);
+
+      handlerMap[name].push(entity);
+      addEvent(name);
     },
-    off: (entity: Entity, key?: string) => {
-      const keys = key ? [key] : Object.keys(handlerMap);
+    off: (entity: Entity, key?: TreeEventName) => {
+      const keys = (key ? [key] : Object.keys(handlerMap)) as TreeEventName[];
       const vals = key
         ? entity[key]
           ? [entity[key]]
           : []
         : Object.values(handlerMap);
 
-      const delKeys: string[] = [];
       for (let i = 0; i < vals.length; i++) {
         const ckey = keys[i];
         const items = vals[i];
@@ -102,23 +144,14 @@ const entityTreeDistributor = (
         }
         if (j === 0) {
           delete handlerMap[ckey];
-          delKeys.push(ckey);
-        }
-      }
-      if (!delKeys.length) return;
-
-      const ckeys = Object.keys(handlerMap);
-      for (const delKey of delKeys) {
-        const delName = getEventArgs(delKey)[0];
-        if (!ckeys.some((ckey) => getEventArgs(ckey)[0] === delName)) {
-          delEvent(delKey);
+          delEvent(ckey);
         }
       }
     },
-    exixtsEvents: addedKeys,
+    handlerMap: handlerMap,
     destory() {
-      while (addedKeys.length) {
-        delEvent(addedKeys[0]);
+      while (listenEvents.length) {
+        delEvent(listenEvents[0]);
       }
     },
   };
@@ -137,16 +170,29 @@ const getDistributor = (entity: Entity) => {
   return distributor;
 };
 
-const canHandlerEvents = [""];
-export const onEntity = (entity: Entity, key: string) => {
+const getEventArgs = (key: string) => {
+  const [name, ...args] = key.split(".");
+  const useCapture = args.includes("capture");
+  return [name as TreeEventName, useCapture] as const;
+};
+
+// 外部使用
+export const openOnEntityTree = (entity: Entity, key: string) => {
   const distributor = getDistributor(entity);
-  distributor.on(entity, key);
-  entity.bus.on("mountBefore", () => {
-    distributor.off(entity, key);
-  });
+  const [name] = getEventArgs(key);
+  const destory = () => closeOnEntityTree(entity, key);
+  if (
+    !distributor.handlerMap[name] ||
+    !distributor.handlerMap[name].includes(entity)
+  ) {
+    distributor.on(entity, name);
+    entity.bus.on("destroyBefore", destory);
+  }
+  return destory;
 };
 
-export const offEntity = (entity: Entity, key?: string) => {
+export const closeOnEntityTree = (entity: Entity, key?: string) => {
   const distributor = getDistributor(entity);
-  distributor.off(entity, key);
+  const [name] = getEventArgs(key);
+  distributor && distributor.off(entity, name);
 };

BIN
src/board/core/resource/cursor/pic_pen.ico


BIN
src/board/core/resource/cursor/pic_pen_a.ico


BIN
src/board/core/resource/cursor/pic_pen_r.ico


+ 2 - 0
src/board/core/mitt.d.ts

@@ -11,3 +11,5 @@ export interface Emitter<Events extends Record<EventType, unknown>> {
     type: undefined extends Events[Key] ? Key : never
   ): void;
 }
+
+export type Pos = { x: number; y: number };

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

@@ -214,3 +214,18 @@ export const mergeFuns = (...fns: (() => void)[] | (() => void)[][]) => {
     });
   };
 };
+
+// 防抖
+export const debounce = <T extends (...args: any) => any>(
+  fn: T,
+  delay: number = 160
+) => {
+  let timeout: any;
+
+  return function <This>(this: This, ...args: Parameters<T>) {
+    clearTimeout(timeout);
+    timeout = setTimeout(() => {
+      fn.apply(this, args);
+    }, delay);
+  };
+};

+ 2 - 2
tsconfig.json

@@ -1,9 +1,9 @@
 {
   "compilerOptions": {
-    "target": "es2015",
+    "target": "es2021",
     "module": "ESNext",
     "lib": [
-      "es2015",
+      "es2021",
       "DOM",
       "DOM.Iterable"
     ],