فهرست منبع

制作钢笔工具

bill 7 ماه پیش
والد
کامیت
073ecb2ecb

+ 1 - 1
public/icons/vue.svg

@@ -1 +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>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 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 - 1
public/vite.svg


BIN
src/core/assert/cursor/pic_pen.ico


BIN
src/core/assert/cursor/pic_pen_a.ico


BIN
src/core/assert/cursor/pic_pen_r.ico


+ 6 - 7
src/core/components/arrow/index.ts

@@ -1,10 +1,10 @@
 import { Pos } from "@/utils/math.ts";
 import { flatPositions, onlyId } from "@/utils/shared.ts";
-import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { ArrowConfig } from "konva/lib/shapes/Arrow";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
+import { AddMessage } from "@/core/hook/use-add.ts";
 
 export { default as Component } from "./arrow.vue";
 export { default as TempComponent } from "./temp-arrow.vue";
@@ -56,16 +56,16 @@ export const getSnapInfos = (data: ArrowData) => {
 };
 
 export const interactiveToData = (
-  info: InteractiveMessage,
+  info: AddMessage<'arrow'>,
   preset: Partial<ArrowData> = {}
 ): ArrowData | undefined => {
-  if (info.area) {
+  if (info.cur) {
     return interactiveFixData(
       {
         ...getBaseItem(),
         ...preset,
         id: onlyId(),
-        points: [info.area[0]],
+        points: [info.cur[0]],
         attitude: [1, 0, 0, 1, 0, 0],
       },
       info
@@ -75,9 +75,8 @@ export const interactiveToData = (
 
 export const interactiveFixData = (
   data: ArrowData,
-  info: InteractiveMessage
+  info: AddMessage<'arrow'>
 ) => {
-  const area = info.area!;
-  data.points[1] = area[1];
+  data.points[1] = info.cur![1];
   return data;
 };

+ 6 - 6
src/core/components/circle/index.ts

@@ -1,4 +1,3 @@
-import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { CircleConfig } from "konva/lib/shapes/Circle";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import {
@@ -8,6 +7,7 @@ import {
   getRectSnapPoints,
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
+import { AddMessage } from "@/core/hook/use-add.ts";
 
 export { default as Component } from "./circle.vue";
 export { default as TempComponent } from "./temp-circle.vue";
@@ -56,14 +56,14 @@ export const dataToConfig = (data: CircleData): CircleConfig => ({
 });
 
 export const interactiveToData = (
-  info: InteractiveMessage,
+  info: AddMessage<'circle'>,
   preset: Partial<CircleData> = {}
 ): CircleData | undefined => {
-  if (info.area) {
+  if (info.cur) {
     const item = {
       ...getBaseItem(),
       ...preset,
-      ...info.area[0],
+      ...info.cur[0],
     } as unknown as CircleData;
     return interactiveFixData(item, info);
   }
@@ -71,9 +71,9 @@ export const interactiveToData = (
 
 export const interactiveFixData = (
   data: CircleData,
-  info: InteractiveMessage
+  info: AddMessage<'circle'>
 ) => {
-  const area = info.area!;
+  const area = info.cur!;
   const xr = Math.abs(area[1].x - area[0].x);
   const yr = Math.abs(area[1].y - area[0].y);
   data.radius = Math.max(xr, yr, 0.01);

+ 5 - 5
src/core/components/icon/index.ts

@@ -1,8 +1,8 @@
-import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { Transform } from "konva/lib/Util";
 import { BaseItem, generateSnapInfos, getBaseItem, getRectSnapPoints } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
+import { AddMessage } from "@/core/hook/use-add.ts";
 
 export { default as Component } from "./icon.vue";
 export { default as TempComponent } from "./temp-icon.vue";
@@ -64,10 +64,10 @@ export const dataToConfig = (data: IconData) => {
 };
 
 export const interactiveToData = (
-  info: InteractiveMessage,
+  info: AddMessage<'icon'>,
   preset: Partial<IconData> = {}
 ): IconData | undefined => {
-  if (info.dot) {
+  if (info.cur) {
     return interactiveFixData(
       { ...getBaseItem(), ...preset } as unknown as IconData,
       info
@@ -77,9 +77,9 @@ export const interactiveToData = (
 
 export const interactiveFixData = (
   data: IconData,
-  info: InteractiveMessage
+  info: AddMessage<'icon'>
 ) => {
-  const mat = new Transform().translate(info.dot!.x, info.dot!.y);
+  const mat = new Transform().translate(info.cur!.x, info.cur!.y);
   data.mat = mat.m;
   return data;
 };

+ 5 - 5
src/core/components/image/index.ts

@@ -1,8 +1,8 @@
-import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { Transform } from "konva/lib/Util";
 import { BaseItem, generateSnapInfos, getBaseItem, getRectSnapPoints } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { imageInfo } from "@/utils/resource.ts";
+import { AddMessage } from "@/core/hook/use-add.ts";
 
 export { default as Component } from "./image.vue";
 export { default as TempComponent } from "./temp-image.vue";
@@ -57,19 +57,19 @@ export type ImageData = Partial<typeof defaultStyle> & BaseItem & {
 };
 
 export const interactiveToData = (
-  info: InteractiveMessage,
+  info: AddMessage<'image'>,
   preset: Partial<ImageData> = {}
 ): ImageData | undefined => {
-  if (info.dot) {
+  if (info.cur) {
     return interactiveFixData({ ...getBaseItem(), ...preset, } as unknown as ImageData, info);
   }
 };
 
 export const interactiveFixData = (
   data: ImageData,
-  info: InteractiveMessage
+  info: AddMessage<'image'>
 ) => {
-  const mat = new Transform().translate(info.dot!.x, info.dot!.y)
+  const mat = new Transform().translate(info.cur!.x, info.cur!.y)
   data.mat = mat.m
   return data;
 };

+ 5 - 12
src/core/components/line/index.ts

@@ -1,11 +1,8 @@
 import { Pos } from "@/utils/math.ts";
-import {
-  InteractiveAction,
-  InteractiveMessage,
-} from "../../hook/use-interactive.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
+import { AddMessage, MessageAction } from "@/core/hook/use-add.ts";
 
 export { default as Component } from "./line.vue";
 export { default as TempComponent } from "./temp-line.vue";
@@ -37,10 +34,10 @@ export type LineData = Partial<typeof defaultStyle> & BaseItem & {
 };
 
 export const interactiveToData = (
-  info: InteractiveMessage,
+  info: AddMessage<'line'>,
   preset: Partial<LineData> = {}
 ): LineData | undefined => {
-  if (info.dot) {
+  if (info.cur) {
     return interactiveFixData(
       { ...getBaseItem(), ...preset, points: [], attitude: [1, 0, 0, 1, 0, 0] },
       info
@@ -50,12 +47,8 @@ export const interactiveToData = (
 
 export const interactiveFixData = (
   data: LineData,
-  info: InteractiveMessage
+  info: AddMessage<'line'>
 ) => {
-  if (info.action === InteractiveAction.delete) {
-    data.points.pop();
-  } else {
-    data.points[info.ndx!] = info.dot!;
-  }
+  data.points = [...info.consumed, info.cur!]
   return data;
 };

+ 5 - 12
src/core/components/polygon/index.ts

@@ -1,11 +1,8 @@
 import { Pos } from "@/utils/math.ts";
-import {
-  InteractiveAction,
-  InteractiveMessage,
-} from "../../hook/use-interactive.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
+import { AddMessage, MessageAction } from "@/core/hook/use-add.ts";
 
 export { default as Component } from "./polygon.vue";
 export { default as TempComponent } from "./temp-polygon.vue";
@@ -39,10 +36,10 @@ export type PolygonData = Partial<typeof defaultStyle> & BaseItem & {
 };
 
 export const interactiveToData = (
-  info: InteractiveMessage,
+  info: AddMessage<'polygon'>,
   preset: Partial<PolygonData> = {}
 ): PolygonData | undefined => {
-  if (info.dot) {
+  if (info.cur) {
     return interactiveFixData(
       { ...getBaseItem(), ...preset, points: [], attitude: [1, 0, 0, 1, 0, 0] },
       info
@@ -52,12 +49,8 @@ export const interactiveToData = (
 
 export const interactiveFixData = (
   data: PolygonData,
-  info: InteractiveMessage
+  info: AddMessage<'polygon'>
 ) => {
-  if (info.action === InteractiveAction.delete) {
-    data.points.pop();
-  } else {
-    data.points[info.ndx!] = info.dot!;
-  }
+  data.points = [...info.consumed, info.cur!]
   return data;
 };

+ 10 - 10
src/core/components/rectangle/index.ts

@@ -1,9 +1,9 @@
 import { Pos } from "@/utils/math.ts";
-import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { onlyId } from "@/utils/shared.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
+import { AddMessage } from "@/core/hook/use-add.ts";
 
 export { default as Component } from "./rectangle.vue";
 export { default as TempComponent } from "./temp-rectangle.vue";
@@ -43,10 +43,10 @@ export type RectangleData = Partial<typeof defaultStyle> &
   };
 
 export const interactiveToData = (
-  info: InteractiveMessage,
+  info: AddMessage<'rectangle'>,
   preset: Partial<RectangleData> = {}
 ): RectangleData | undefined => {
-  if (info.area) {
+  if (info.cur) {
     const item = {
       ...getBaseItem(),
       ...preset,
@@ -62,18 +62,18 @@ export const getSnapInfos = (data: RectangleData) => generateSnapInfos(data.poin
 
 export const interactiveFixData = (
   data: RectangleData,
-  info: InteractiveMessage
+  info: AddMessage<'rectangle'>
 ) => {
-  if (info.area) {
-    const area = info.area!;
+  if (info.cur) {
+    const area = info.cur!;
     const width = area[1].x - area[0].x;
     const height = area[1].y - area[0].y;
 
     data.points = [
-      info.area[0],
-      { x: info.area[0].x + width, y: info.area[0].y },
-      { x: info.area[0].x + width, y: info.area[0].y + height },
-      { x: info.area[0].x, y: info.area[0].y + height },
+      info.cur[0],
+      { x: info.cur[0].x + width, y: info.cur[0].y },
+      { x: info.cur[0].x + width, y: info.cur[0].y + height },
+      { x: info.cur[0].x, y: info.cur[0].y + height },
     ];
     data.attitude = [1, 0, 0, 1, 0, 0];
   }

+ 5 - 5
src/core/components/text/index.ts

@@ -1,10 +1,10 @@
 import { Transform } from "konva/lib/Util";
-import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { Text, TextConfig } from "konva/lib/shapes/Text";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem, getRectSnapPoints } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { shallowReactive } from "vue";
+import { AddMessage } from "@/core/hook/use-add.ts";
 
 export { default as Component } from "./text.vue";
 export { default as TempComponent } from "./temp-text.vue";
@@ -52,10 +52,10 @@ export const getSnapInfos = (data: TextData) => {
 };
 
 export const interactiveToData = (
-  info: InteractiveMessage,
+  info: AddMessage<'text'>,
   preset: Partial<TextData> = {}
 ): TextData | undefined => {
-  if (info.dot) {
+  if (info.cur) {
     const item = {
       ...defaultStyle,
       ...getBaseItem(),
@@ -68,9 +68,9 @@ export const interactiveToData = (
 
 export const interactiveFixData = (
   data: TextData,
-  info: InteractiveMessage
+  info: AddMessage<'text'>
 ) => {
-  const mat = new Transform().translate(info.dot!.x, info.dot!.y)
+  const mat = new Transform().translate(info.cur!.x, info.cur!.y)
   data.mat = mat.m
   return data;
 };

+ 14 - 9
src/core/components/triangle/index.ts

@@ -1,8 +1,8 @@
-import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { Pos } from "@/utils/math.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
+import { AddMessage } from "@/core/hook/use-add.ts";
 
 export { default as Component } from "./triangle.vue";
 export { default as TempComponent } from "./temp-triangle.vue";
@@ -18,25 +18,30 @@ export const addMode = "area";
 export const getMouseStyle = (data: TriangleData) => {
   const fillStatus = data.fill && getMouseColors(data.fill);
   const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
-  const strokeWidth = data.strokeWidth ;
+  const strokeWidth = data.strokeWidth;
 
   return {
-    default: { fill: fillStatus && fillStatus.pub, stroke: strokeStatus.pub, strokeWidth },
+    default: {
+      fill: fillStatus && fillStatus.pub,
+      stroke: strokeStatus.pub,
+      strokeWidth,
+    },
     hover: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
     press: { fill: fillStatus && fillStatus.press, stroke: strokeStatus.press },
   };
 };
 
 export type TriangleData = Partial<typeof defaultStyle> &
-  BaseItem & { points: Pos[]; attitude: number[], fill?: string };
+  BaseItem & { points: Pos[]; attitude: number[]; fill?: string };
 
-export const getSnapInfos = (data: TriangleData) => generateSnapInfos(data.points, true, false)
+export const getSnapInfos = (data: TriangleData) =>
+  generateSnapInfos(data.points, true, false);
 
 export const interactiveToData = (
-  info: InteractiveMessage,
+  info: AddMessage<"triangle">,
   preset: Partial<TriangleData> = {}
 ): TriangleData | undefined => {
-  if (info.area) {
+  if (info.cur) {
     const item = {
       ...getBaseItem(),
       ...preset,
@@ -48,9 +53,9 @@ export const interactiveToData = (
 
 export const interactiveFixData = (
   data: TriangleData,
-  info: InteractiveMessage
+  info: AddMessage<"triangle">
 ) => {
-  const area = info.area!;
+  const area = info.cur!;
 
   data.points[0] = {
     x: area[0].x - (area[1].x - area[0].x),

+ 98 - 86
src/core/history.ts

@@ -2,98 +2,110 @@ import { History } from "stateshot";
 import { computed, reactive, ref } from "vue";
 
 export type HistoryState = {
-	hasUndo: boolean;
-	hasRedo: boolean;
+  hasUndo: boolean;
+  hasRedo: boolean;
 };
 
 export type StateCallback = (state: HistoryState) => void;
 
 export class SingleHistory<T = any> {
-	history: History<{ data: T }>;
-	hasUndo = ref(false)
-	hasRedo = ref(false)
-
-	constructor() {
-		this.history = new History();
-	}
-
-	private syncState() {
-		this.hasRedo.value = this.history.hasRedo;
-		this.hasUndo.value = this.history.hasUndo;
-	}
-
-	get data() {
-		return this.history.get()?.data;
-	}
-
-	undo(): T {
-		if (this.history.hasUndo) {
-			this.history.undo();
-			this.syncState();
-		}
-		return this.data;
-	}
-
-	redo(): T {
-		if (this.history.hasRedo) {
-			this.history.redo();
-			this.syncState();
-		}
-		return this.data;
-	}
-
-	push(data: T) {
-		this.history.pushSync({ data });
-		this.syncState();
-	}
-
-	clear() {
-		this.history.reset();
-	}
+  history: History<{ data: T }>;
+  hasUndo = ref(false);
+  hasRedo = ref(false);
+
+  constructor() {
+    this.history = new History();
+  }
+
+  list() {
+    const history: any = this.history;
+    return history.$records.map((item: any) => {
+      const id = item.hashes[0];
+      return { id, data: JSON.parse(history.$chunks[item.hashes[0]]).data };
+    }) as {id: string, data: T}[];
+  }
+
+  get currentId(): string {
+    const history: any = this.history;
+    return history.$records[history.$index].hashes[0];
+  }
+
+  private syncState() {
+    this.hasRedo.value = this.history.hasRedo;
+    this.hasUndo.value = this.history.hasUndo;
+  }
+
+  get data() {
+    return this.history.get()?.data;
+  }
+
+  undo(): T {
+    if (this.history.hasUndo) {
+      this.history.undo();
+      this.syncState();
+    }
+    console.log(this.history);
+    return this.data;
+  }
+
+  redo(): T {
+    if (this.history.hasRedo) {
+      this.history.redo();
+      this.syncState();
+    }
+    return this.data;
+  }
+
+  push(data: T) {
+    this.history.pushSync({ data });
+    this.syncState();
+  }
+
+  reset() {
+    this.history.reset();
+  }
 }
 
-
-
 export class MergeHistory<T> {
-	historyStack: SingleHistory<T>[] = reactive([]);
-	current = computed(() => this.historyStack[this.historyStack.length - 1]);
-	hasUndo = computed(() => this.current.value.hasUndo)
-	hasRedo = computed(() => this.current.value.hasRedo)
-
-	branch() {
-		const single = new SingleHistory();
-		this.historyStack.push(single);
-	}
-
-	merge() {
-		const lastStack = this.historyStack.pop()!;
-		if (lastStack.hasUndo.value) {
-			this.current.value.push(lastStack.data);
-		}
-		lastStack.clear();
-	}
-
-	undo() {
-		return this.current.value.undo();
-	}
-
-	redo() {
-		return this.current.value.redo();
-	}
-
-	push(data: T) {
-		return this.current.value.push(data);
-	}
-
-	clear() {
-		return this.current.value.clear();
-	}
-
-	get data() {
-		return this.current.value.data;
-	}
-
-	destory() {
-		this.historyStack.length = 0;
-	}
+  historyStack: SingleHistory<T>[] = reactive([]);
+  current = computed(() => this.historyStack[this.historyStack.length - 1]);
+  hasUndo = computed(() => this.current.value.hasUndo);
+  hasRedo = computed(() => this.current.value.hasRedo);
+
+  branch() {
+    const single = new SingleHistory();
+    this.historyStack.push(single);
+  }
+
+  merge() {
+    const lastStack = this.historyStack.pop()!;
+    if (lastStack.hasUndo.value) {
+      this.current.value.push(lastStack.data);
+    }
+    lastStack.reset();
+  }
+
+  undo() {
+    return this.current.value.undo();
+  }
+
+  redo() {
+    return this.current.value.redo();
+  }
+
+  push(data: T) {
+    return this.current.value.push(data);
+  }
+
+  clear() {
+    return this.current.value.reset();
+  }
+
+  get data() {
+    return this.current.value.data;
+  }
+
+  destory() {
+    this.historyStack.length = 0;
+  }
 }

+ 350 - 173
src/core/hook/use-add.ts

@@ -1,7 +1,15 @@
-import { nextTick, reactive, ref, watch, watchEffect } from "vue";
-import { useCan, useMode, useStage } from "./use-global-vars";
+import { h, nextTick, reactive, ref, watch, watchEffect } from "vue";
+import {
+  installGlobalVar,
+  useCan,
+  useCursor,
+  useDownKeys,
+  useMode,
+  useStage,
+} from "./use-global-vars";
 import {
   Area,
+  InteractiveHook,
   useInteractiveAreas,
   useInteractiveDots,
   useInteractiveProps,
@@ -18,77 +26,102 @@ import {
   SnapPoint,
 } from "../components";
 import { useConversionPosition } from "./use-coversion-position";
-import { Pos } from "@/utils/math";
+import { eqPoint, lineInner, linePointLen, Pos, zeroEq } from "@/utils/math";
 import { useCustomSnapInfos, useSnap } from "./use-snap";
 import { generateSnapInfos } from "../components/util";
+import { useStore, useStoreRenderProcessors } from "../store";
+import DrawShape from "../renderer/draw-shape.vue";
+import { useHistory, useHistoryAttach } from "./use-history";
+import penA from "../assert/cursor/pic_pen_a.ico";
+import penR from "../assert/cursor/pic_pen_r.ico";
 
-export type AddMessageData<T extends ShapeType> = ComponentValue<
-  T,
-  "addMode"
-> extends "area"
+type PayData<T extends ShapeType> = ComponentValue<T, "addMode"> extends "area"
   ? Area
   : Pos;
 
-export const useInteractiveAddShapeAPI = () => {
+export enum MessageAction {
+  add,
+  delete,
+  update,
+  replace,
+}
+
+export type AddMessage<T extends ShapeType> = {
+  consumed: PayData<T>[];
+  cur?: PayData<T>;
+  ndx?: number;
+  action: MessageAction;
+};
+
+export const useInteractiveAddShapeAPI = installGlobalVar(() => {
   const mode = useMode();
   const can = useCan();
   const interactiveProps = useInteractiveProps();
   const conversion = useConversionPosition(true);
 
-  let quitHook: null | (() => void) = null;
+  let isEnter = false;
+  const enter = () => {
+    if (!isEnter) {
+      isEnter = true;
+      mode.push(Mode.add);
+    }
+  };
+  const leave = () => {
+    if (isEnter) {
+      isEnter = false;
+      mode.pop();
+    }
+  };
+
   return {
     addShape: <T extends ShapeType>(
       shapeType: T,
       preset: Partial<DrawItem<T>> = {},
-      data: AddMessageData<T>,
+      data: PayData<T>,
       pixel = false
     ) => {
       if (!can.addMode) {
         throw "当前状态不允许添加";
       }
-      mode.push(Mode.add);
+      enter();
       if (pixel) {
         data = (
           Array.isArray(data) ? data.map(conversion) : conversion(data)
-        ) as AddMessageData<T>;
+        ) as PayData<T>;
       }
       interactiveProps.value = {
         type: shapeType,
         preset,
-        callback: () => {
-          mode.pop();
-        },
+        callback: leave,
         operate: { single: true, immediate: true, data },
       };
     },
-    enterMouseAddShape: <T extends ShapeType>(
+    enterMouseAddShape: async <T extends ShapeType>(
       shapeType: T,
       preset: Partial<DrawItem<T>> = {},
       single = false
     ) => {
+      if (isEnter) {
+        leave();
+        await new Promise((resolve) => setTimeout(resolve, 16));
+      }
       if (!can.addMode || mode.include(Mode.add)) {
         throw "当前状态不允许添加";
       }
-      mode.push(Mode.add);
-      quitHook = () => {
-        mode.pop();
-        quitHook = null
-      }
+      enter();
       interactiveProps.value = {
         type: shapeType,
         preset,
         operate: { single },
-        callback: quitHook,
+        callback: leave,
       };
     },
     quitMouseAddShape: () => {
-      if (quitHook) {
-        mode.pop();
-      }
+      leave();
       interactiveProps.value = void 0;
     },
   };
-};
+});
 
 export const useIsAddRunning = (shapeType?: ShapeType) => {
   const stage = useStage();
@@ -161,205 +194,349 @@ const usePointBeforeHandler = (enableTransform = false, enableSnap = false) => {
   };
 };
 
-// 拖拽面积确定组件
-export const useInteractiveAddAreas = <T extends ShapeType>(
-  type: T,
-  refSelf = true
-) => {
+const useInteractiveAddTemp = <T extends ShapeType>({
+  type,
+  useIA,
+  refSelf,
+  enter,
+  quit,
+}: {
+  type: T;
+  useIA: InteractiveHook;
+  refSelf?: boolean;
+  enter?: () => void;
+  quit?: () => void;
+}) => {
   const { quitMouseAddShape } = useInteractiveAddShapeAPI();
   const isRuning = useIsAddRunning(type);
-  const obj = components[type] as Components[T];
   const items = reactive([]) as DrawItem<T>[];
-  const viewItems = reactive([]) as DrawItem<T>[];
+  const obj = components[type] as Components[T];
   const beforeHandler = usePointBeforeHandler(true, true);
+  const processors = useStoreRenderProcessors();
+  const store = useStore();
+  const processorIds = processors.register(() => DrawShape);
   const clear = () => {
     beforeHandler.clear();
     beforeHandler.clearRef();
   };
 
-  const interactive = useInteractiveAreas({
+  const ia = useIA({
     shapeType: type,
     isRuning,
     quit: () => {
+      items.length = 0;
+      processorIds.length = 0;
       quitMouseAddShape();
       clear();
+      quit && quit();
     },
+    enter,
     beforeHandler: (p) => {
       beforeHandler.clear();
       return beforeHandler.transform(p);
     },
   });
 
+  const addItem = (cur: PayData<T>) => {
+    let item: any = obj.interactiveToData(
+      {
+        consumed: ia.consumedMessage,
+        cur,
+        action: MessageAction.add,
+      } as any,
+      ia.preset
+    );
+    if (!item) return;
+    item = reactive(item);
+
+    if (ia.singleDone.value) {
+      store.addItem(type, item);
+      return;
+    }
+
+    items.push(item);
+
+    // 箭头参考自身位置
+    if (refSelf && Array.isArray(cur)) {
+      beforeHandler.addRef(cur[0]);
+    }
+
+    const stop = mergeFuns(
+      // 监听位置变化
+      watch(
+        cur,
+        () =>
+          obj.interactiveFixData(item, {
+            consumed: ia.consumedMessage,
+            cur,
+            action: MessageAction.update,
+          } as any),
+        { deep: true }
+      ),
+      // 监听是否消费完毕
+      watch(ia.singleDone, () => {
+        processorIds.push(item.id);
+        store.addItem(type, item);
+        const ndx = items.indexOf(item);
+        items.splice(ndx, 1);
+        clear();
+        stop();
+      })
+    );
+  };
+
   // 每次拽结束都加组件
   watch(
-    () => interactive.messages,
-    (areas) => {
-      if (areas.length === 0) return;
-      for (const area of areas) {
-        let item: any = obj.interactiveToData({ area }, interactive.preset);
-        if (!item) continue;
-        const ndx = viewItems.length;
-        viewItems[ndx] = item = reactive(item);
-
-        if (interactive.singleDone.value) continue;
-        if (refSelf) {
-          beforeHandler.addRef(area[0]);
-        }
-        const stop = mergeFuns(
-          watch(area, () => obj.interactiveFixData(item, { area }), {
-            deep: true,
-          }),
-          watch(
-            () => [interactive.singleDone.value, interactive.isRunning.value],
-            () => {
-              items[ndx] = viewItems[ndx];
-              clear();
-              stop();
-            }
-          )
-        );
-      }
-      interactive.consume(areas);
+    () => ia.messages,
+    (datas: any) => {
+      datas.forEach(addItem);
+      ia.consume(datas);
     },
     { immediate: true }
   );
+  return items;
+};
 
-  return { items, viewItems };
+// 拖拽面积确定组件
+export const useInteractiveAddAreas = <T extends ShapeType>(type: T) => {
+  const cursor = useCursor();
+  return useInteractiveAddTemp({
+    type,
+    useIA: useInteractiveAreas,
+    refSelf: type === "arrow",
+    enter() {
+      cursor.push("crosshair");
+    },
+    quit() {
+      cursor.pop();
+    },
+  });
 };
 
-// 多点确定组件
-export const useInteractiveAddDots = <T extends ShapeType>(
-  type: T,
-  single = false,
-  snap = { prev: false, next: false }
+export const useInteractiveAddDots = <T extends ShapeType>(type: T) => {
+  const cursor = useCursor();
+  return useInteractiveAddTemp({
+    type,
+    useIA: useInteractiveDots,
+    enter() {
+      cursor.push(penA);
+    },
+    quit() {
+      cursor.pop();
+    },
+  });
+};
+
+export const penUpdatePoints = <T extends Pos>(
+  transfromPoints: T[],
+  cur: T
 ) => {
+  const points = [...transfromPoints];
+  let oper: "del" | "add" | "set" | "no" = "add";
+  const resetCur = () => {
+    if (points.length) {
+      return (cur = points.pop()!);
+    } else {
+      return cur;
+    }
+  };
+
+  let repeatStart = false;
+  for (let i = 0; i < points.length; i++) {
+    if (eqPoint(points[i], cur)) {
+      const isLast = i === points.length - 1;
+      const isStart = i === 0;
+
+      if (!isStart && !isLast) {
+        points.splice(i--, 1);
+        oper = "del";
+      } else if ((oper !== "del" && isLast) || isStart) {
+        oper = "no";
+        if (isStart) {
+          repeatStart = true;
+        }
+      }
+    }
+  }
+  if (oper === "del" || oper === "no") {
+    if (repeatStart) {
+      const change = points.length > 2
+      return { 
+        points, 
+        oper, 
+        cur: change ? cur : resetCur(),
+        unchanged: !change
+      }; 
+    }
+    return { points, oper, cur: repeatStart ? cur : resetCur() };
+  }
+
+  for (let i = 0, ndx = 0; i < transfromPoints.length - 1; i++, ndx++) {
+    const line = [transfromPoints[i], transfromPoints[i + 1]];
+    if (lineInner(line, cur)) {
+      oper = "set";
+      points.splice(++ndx, 0, cur);
+      resetCur();
+    }
+  }
+
+  return { points, oper, cur };
+};
+
+// 钢笔添加
+export const useInteractiveAddPen = <T extends ShapeType>(type: T) => {
   const { quitMouseAddShape } = useInteractiveAddShapeAPI();
   const isRuning = useIsAddRunning(type);
-  const obj = components[type] as Components[T];
   const items = reactive([]) as DrawItem<T>[];
-  const viewItems = reactive([]) as DrawItem<T>[];
+  const obj = components[type] as Components[T];
   const beforeHandler = usePointBeforeHandler(true, true);
+  const history = useHistory();
+  const processors = useStoreRenderProcessors();
+  const store = useStore();
+  const downKeys = useDownKeys();
+  const processorIds = processors.register(() => {
+    return (props: any) => h(DrawShape, { ...props, show: false });
+  });
+
+  let prev: Pos;
+  let firstEntry = true;
+  // 可能历史空间会撤销 重做更改到正在绘制的组件
+  const messages = useHistoryAttach<Pos[]>(`${type}-pen`, isRuning, []);
 
-  // 多点确定组件,
-  const interactive = useInteractiveDots({
+  const currentCursor = ref(penA);
+  const cursor = useCursor();
+  const ia = useInteractiveDots({
     shapeType: type,
     isRuning,
-    beforeHandler: (p) => {
-      beforeHandler.clear();
-      return beforeHandler.transform(p, prev, next);
+    enter() {
+      cursor.push(currentCursor.value);
+      watch(currentCursor, () => {
+        cursor.value = currentCursor.value;
+      });
     },
     quit: () => {
+      items.length = 0;
+      processorIds.length = 0;
       quitMouseAddShape();
       beforeHandler.clear();
-      item = null;
+      cursor.pop();
+    },
+    beforeHandler: (p) => {
+      beforeHandler.clear();
+      return beforeHandler.transform(p, prev);
     },
   });
 
-  let item: any;
-  let prev: Pos, next: Pos;
-  watch(
-    () => interactive.messages,
-    (dots, _) => {
-      if (dots.length === 0) return;
-      for (const dot of dots) {
-        const ndx = interactive.getNdx(dot);
-        const addNdx = single ? 0 : viewItems.length;
-        if (!item || !single) {
-          item = obj.interactiveToData(
-            { dot: dots[0], ndx },
-            interactive.preset
-          );
-          if (!item) continue;
-          viewItems[addNdx] = item = reactive(item);
-        } else {
-          obj.interactiveFixData(item, { dot, ndx: ndx });
-        }
-
-        if (interactive.singleDone.value) continue;
-        const stop = mergeFuns(
-          watch(dot, () => obj.interactiveFixData(item, { dot, ndx }), {
-            deep: true,
-          }),
-          watch(
-            () => [interactive.singleDone.value, interactive.isRunning.value],
-            () => {
-              if (interactive.singleDone.value) {
-                if (!single) {
-                  item = null;
-                  items[addNdx] = viewItems[addNdx];
-                } else {
-                  items[addNdx] = JSON.parse(JSON.stringify(viewItems[addNdx]));
-                  if (snap.prev) {
-                    prev = dot;
-                  }
-                  if (snap.next) {
-                    next = next || dot;
-                  }
-                }
-              }
-              beforeHandler.clear();
-              stop();
-            }
-          )
-        );
+  const getAddMessage = (cur: Pos) => {
+    let consumed = messages.value;
+    currentCursor.value = penA;
+    let pen: null | ReturnType<typeof penUpdatePoints> = null;
+    if (!downKeys.has("Control")) {
+      pen = penUpdatePoints(messages.value, cur);
+      if (pen.oper === "del") {
+        currentCursor.value = penR;
       }
-      interactive.consume(dots);
+      consumed = pen.points;
+      cur = pen.cur;
     }
-  );
-  return { items, viewItems };
-};
 
-export const useInteractiveAdd = <T extends ShapeType>(
-  type: T,
-  addHandler: (items: DrawItem<T>[]) => void
-) => {
-  const obj = components[type];
-  const isRuning = useIsAddRunning(type);
-  const once = obj.addMode === "dots";
-  const { items, viewItems } =
-    obj.addMode === "area"
-      ? useInteractiveAddAreas(type, type === "arrow")
-      : useInteractiveAddDots(type, once);
-
-  const snapInfos = useCustomSnapInfos();
-  const addedItems: Record<string, ComponentSnapInfo[]> = {};
-  watch(
-    items as DrawItem[],
-    (items, oldItems) => {
-      if (!once || !oldItems) {
-        items = items.filter((item) => !addedItems[item.id]);
-      } else {
-        oldItems.forEach((item) => {
-          addedItems[item.id]?.forEach((info) => snapInfos.remove(info));
-          delete addedItems[item.id];
-        });
+    return {
+      pen,
+      consumed,
+      cur,
+      action: firstEntry ? MessageAction.add : MessageAction.replace,
+    } as any;
+  };
+  const pushMessages = (cur: Pos) => {
+    const { pen } = getAddMessage(cur);
+    if (pen) {
+      if (!pen.unchanged) {
+        messages.value = pen.points;
+        cur = pen.cur;
+        messages.value.push(cur);
       }
+    } else {
+      messages.value.push(cur);
+    }
+    
+    return !pen?.unchanged;
+  };
 
-      items.forEach((item) => {
-        const infos = obj.getSnapInfos(item as any);
-        infos.forEach((info) => snapInfos.add(info));
-        addedItems[item.id] = infos;
-      });
-    },
-    { deep: true }
-  );
+  const addItem = (cur: PayData<T>) => {
+    const dot = cur as Pos;
+    if (messages.value.length === 0) {
+      firstEntry = true;
+      items.length = 0;
+    }
+    let item: any = items.length === 0 ? null : items[0];
+    if (!item) {
+      item = obj.interactiveToData(getAddMessage(dot), ia.preset);
+      if (!item) return;
+      items[0] = item = reactive(item);
+    }
+
+    if (ia.singleDone.value) {
+      store.addItem(type, item);
+      return;
+    }
+
+    const update = () => {
+      obj.interactiveFixData(item, getAddMessage(dot));
+    };
 
+    const stop = mergeFuns(
+      watch(dot, update, { immediate: true, deep: true }),
+      watch(
+        messages,
+        () => {
+          if (!messages.value) return;
+          if (messages.value.length === 0) {
+            quitMouseAddShape();
+          } else {
+            update();
+          }
+        },
+        { deep: true }
+      ),
+      // 监听是否消费完毕
+      watch(ia.singleDone, () => {
+        prev = dot;
+        const cItem = JSON.parse(JSON.stringify(item));
+        const isChange = pushMessages(dot);
+        if (isChange) {
+          if (firstEntry) {
+            processorIds.push(item.id);
+            history.preventTrack(() => store.addItem(type, cItem));
+          } else {
+            store.setItem(type, { id: item.id, value: cItem });
+          }
+        }
+        beforeHandler.clear();
+        stop();
+        firstEntry = false;
+      })
+    );
+  };
+
+  // 每次拽结束都加组件
   watch(
-    isRuning,
-    (isRunning) => {
-      // 消费结束,发送添加完毕数据,未消费的则直接丢弃
-      if (!isRunning && items.length > 0) {
-        addHandler([...items]);
-      }
-      for (const key of Object.keys(addedItems)) {
-        addedItems[key].forEach((info) => snapInfos.remove(info));
-        delete addedItems[key];
-      }
-      items.length = 0;
-      viewItems.length = 0;
+    () => ia.messages,
+    (datas: any) => {
+      datas.forEach(addItem);
+      ia.consume(datas);
     },
-    { flush: "pre" }
+    { immediate: true }
   );
+  return items;
+};
 
-  return viewItems;
+export const useInteractiveAdd = <T extends ShapeType>(type: T) => {
+  const obj = components[type];
+  if (obj.addMode === "dots") {
+    return useInteractiveAddPen(type);
+  } else if (obj.addMode === "area") {
+    return useInteractiveAddAreas(type);
+  } else {
+    return useInteractiveAddDots(type);
+  }
 };

+ 2 - 1
src/core/hook/use-expose.ts

@@ -1,10 +1,11 @@
 import { useLayers, useMode, useStage } from "./use-global-vars.ts";
 import { Stage } from "konva/lib/Stage";
 import { useInteractiveProps } from "./use-interactive.ts";
-import { useHistory, useStore } from "../store/index.ts";
+import { useStore } from "../store/index.ts";
 import { useViewer } from "./use-viewer.ts";
 import { useGlobalResize } from "./use-event.ts";
 import { useInteractiveAddShapeAPI } from "./use-add.ts";
+import { useHistory } from "./use-history.ts";
 
 type PickParams<K extends keyof Stage, O extends string> = Stage[K]  extends (...args: any) => any ?  Omit<Required<Parameters<Stage[K]>>[0], O> : never
 

+ 7 - 0
src/core/hook/use-global-vars.ts

@@ -199,6 +199,7 @@ export const usePointerPos = installGlobalVar(() => {
   return pos;
 }, Symbol("pointerPos"));
 
+
 export const useDownKeys = installGlobalVar(() => {
   const keys = reactive(new Set<string>());
   const cleanup = mergeFuns(
@@ -224,3 +225,9 @@ export const useTransformIngShapes = installGlobalVar(
   () => ref<EntityShape[]>([]),
   Symbol("transformIngShapes")
 );
+
+
+export const useCursor = installGlobalVar(
+  () => stackVar('default'),
+  Symbol('cursor')
+)

+ 181 - 0
src/core/hook/use-history.ts

@@ -0,0 +1,181 @@
+import mitt from "mitt";
+import { SingleHistory } from "../history";
+import { installGlobalVar } from "./use-global-vars";
+import { DrawData } from "../components";
+import { Ref, ref, watch } from "vue";
+import { copy } from "@/utils/shared";
+import { useStoreRaw } from "../store/store";
+
+type HistoryItem = { attachs: string; data: string };
+class DrawHistory {
+  history = new SingleHistory<HistoryItem>();
+  hasUndo = this.history.hasUndo;
+  hasRedo = this.history.hasRedo;
+  get list() {
+    return this.history.list()
+  }
+  get currentId() {
+    return this.history.currentId
+  }
+
+  preventFlag = false;
+  onceFlag = false;
+  onceHistory: string | null = null;
+  initData: string | null = null;
+  renderer: (data: HistoryItem) => void;
+  bus = mitt<{
+    attachs: Record<string, any>;
+    renderer: void;
+    push: void;
+    pushed: void;
+    redo: void;
+    undo: void;
+  }>();
+  private pushAttachs: Record<string, any> = {};
+
+  constructor(renderer: (data: string) => void) {
+    this.renderer = ({ data, attachs }: HistoryItem) => {
+      this.bus.emit("attachs", attachs ? JSON.parse(attachs) : {});
+      renderer(data);
+      this.bus.emit("renderer");
+    };
+  }
+  
+  clearHistoryAttach(id: string, name: string) {
+    const history = this.history.history as any
+    if (!history.$chunks[id]) return
+    const data = JSON.parse(history.$chunks[id]).data as HistoryItem
+    if (!data.attachs) return;
+    const attachs = JSON.parse(data.attachs)
+    if (attachs[name]) {
+      delete attachs[name]
+      data.attachs = JSON.stringify(attachs)
+      history.$chunks[id] = JSON.stringify({ data })
+    }
+  }
+
+  setPushAttach(name: string, data: any) {
+    this.pushAttachs[name] = data;
+  }
+
+  setInit(data: string) {
+    this.initData = data;
+    this.history.reset();
+    this.push(data);
+  }
+
+  preventTrack(fn: () => void) {
+    this.preventFlag = true;
+    fn();
+    this.preventFlag = false;
+  }
+
+  push(data: string) {
+    if (this.preventFlag) return;
+    if (this.onceFlag) {
+      this.onceHistory = data;
+    } else {
+      this.bus.emit("push");
+      this.history.push({ attachs: JSON.stringify(this.pushAttachs), data });
+      this.pushAttachs = {};
+      this.bus.emit("pushed");
+    }
+  }
+
+  redo() {
+    const data = this.history.redo();
+    this.bus.emit("redo");
+    this.renderer(data);
+    return data;
+  }
+
+  undo() {
+    const data = this.history.undo();
+    this.bus.emit("undo");
+    this.renderer(data);
+    return data;
+  }
+
+  onceTrack(fn: () => void) {
+    this.onceFlag = true;
+    fn();
+    if (this.onceHistory) {
+      this.push(this.onceHistory);
+      this.onceHistory = null;
+    }
+    this.onceFlag = false;
+  }
+
+  clear(): void {
+    this.renderer({ data: "", attachs: "" });
+    this.push("");
+  }
+
+  init() {
+    if (this.initData) {
+      this.renderer({ data: this.initData, attachs: "" });
+      this.push(this.initData);
+    }
+  }
+}
+
+export const useHistory = installGlobalVar(() => {
+  const store = useStoreRaw();
+  const history = new DrawHistory((dataStr: string) => {
+    const data: DrawData = dataStr ? JSON.parse(dataStr) : {};
+    store.$patch((state) => {
+      state.data = data;
+    });
+  });
+  return history;
+}, Symbol("history"));
+
+export const useHistoryAttach = <T>(
+  name: string,
+  isRuning: Ref<boolean> = ref(true),
+  init: T | null = null,
+  cleanup = true
+) => {
+  const history = useHistory();
+  const current = ref<T>(init ? copy(init) : init!);
+  const setIds = [] as string[]
+
+  const addSetIds = () => setIds.push(history.currentId)
+  const pushHandler = () => {
+    history.setPushAttach(name, current.value);
+  };
+  const attachsHandler = (attachs: any) => {
+    current.value = attachs && attachs[name] ? attachs[name] : copy(init);
+  };
+  const cleanupAttach = () => {
+    setIds.forEach(id => history.clearHistoryAttach(id, name))
+    setIds.length = 0
+  }
+
+  watch(
+    isRuning,
+    (isRun, _, onCleanup) => {
+      if (!isRun) return;
+      history.bus.on("push", pushHandler);
+      history.bus.on("attachs", attachsHandler);
+
+      if (cleanup) {
+        history.bus.on('pushed', addSetIds)
+      }
+
+      onCleanup(() => {
+        history.bus.off("push", pushHandler);
+        history.bus.off("attachs", pushHandler);
+        current.value = void 0;
+        
+        if (cleanup) {
+          history.bus.off('pushed', pushHandler);
+          cleanupAttach()
+        }
+      });
+    },
+    { immediate: true, flush: "sync" }
+  );
+
+  return current;
+};

+ 48 - 46
src/core/hook/use-interactive.ts

@@ -27,15 +27,7 @@ export const useInteractiveProps = installGlobalVar(
 );
 
 export type Area = [Pos, Pos];
-export enum InteractiveAction {
-  delete,
-}
-export type InteractiveMessage = {
-  area?: Area;
-  dot?: Pos;
-  ndx?: number;
-  action?: InteractiveAction;
-};
+export type InteractiveHook = typeof useInteractiveAreas | typeof useInteractiveDots
 export type InteractiveAreas = ReturnType<typeof useInteractiveAreas>;
 export type InteractiveDots = ReturnType<typeof useInteractiveDots>;
 export type Interactive = InteractiveAreas | InteractiveDots;
@@ -52,43 +44,47 @@ const useInteractiveExpose = <T extends object>(
   const stage = useStage();
   const interactiveProps = useInteractiveProps();
 
-  watch(isRunning, (can, _, onCleanup) => {
-    if (can) {
-      const props = interactiveProps.value!;
-      const cleanups = [] as Array<() => void>;
-      if (props.operate?.single) {
-        // 如果指定单次则消息中有信息,并且确定完成则马上退出
-        cleanups.push(
-          watchEffect(
-            () => {
-              if (messages.value.length > 0 && singleDone.value) {
-                quit();
-                props.callback && props.callback();
-              }
-            },
-            { flush: "post" }
-          )
-        );
-      }
+  watch(
+    isRunning,
+    (can, _, onCleanup) => {
+      if (can) {
+        const props = interactiveProps.value!;
+        const cleanups = [] as Array<() => void>;
+        if (props.operate?.single) {
+          // 如果指定单次则消息中有信息,并且确定完成则马上退出
+          cleanups.push(
+            watchEffect(
+              () => {
+                if (messages.value.length > 0 && singleDone.value) {
+                  quit();
+                  props.callback && props.callback();
+                }
+              },
+              { flush: "post" }
+            )
+          );
+        }
 
-      // 单纯添加
-      if (props.operate?.immediate) {
-        messages.value.push(props.operate.data as T);
-        singleDone.value = true;
+        // 单纯添加
+        if (props.operate?.immediate) {
+          messages.value.push(props.operate.data as T);
+          singleDone.value = true;
+        } else {
+          const $stage = stage.value!.getStage();
+          const dom = $stage.container();
+          cleanups.push(init(dom));
+          cleanups.push(() => {
+            quit();
+            props.callback && props.callback();
+          });
+        }
+        onCleanup(mergeFuns(cleanups));
       } else {
-        const $stage = stage.value!.getStage();
-        const dom = $stage.container();
-        cleanups.push(init(dom));
-        cleanups.push(() => {
-          quit();
-          props.callback && props.callback();
-        });
+        messages.value = [];
       }
-      onCleanup(mergeFuns(cleanups));
-    } else {
-      messages.value = [];
-    }
-  });
+    },
+    { immediate: true }
+  );
 
   return {
     isRunning,
@@ -119,6 +115,7 @@ type UseInteractiveProps = {
   isRuning: Ref<boolean>;
   quit: () => void;
   beforeHandler?: (p: Pos) => Pos;
+  enter?: () => void
   shapeType?: ShapeType;
   autoConsumed?: boolean;
 };
@@ -128,6 +125,7 @@ export const useInteractiveAreas = ({
   autoConsumed,
   beforeHandler,
   quit,
+  enter
 }: UseInteractiveProps) => {
   const mode = useMode();
   const can = useCan();
@@ -140,6 +138,7 @@ export const useInteractiveAreas = ({
     let downed = false;
     let tempArea: Area;
     let dragging = false;
+    enter && enter()
 
     return mergeFuns(
       listener(dom, "pointerdown", (ev) => {
@@ -177,7 +176,9 @@ export const useInteractiveAreas = ({
       listener(dom, "pointerup", (ev) => {
         if (downed) {
           mode.del(Mode.draging);
-        } else if (!dragging) return;
+        } 
+        downed = false
+        if (!dragging) return;
 
         if (can.dragMode) {
           const position = getOffset(ev, dom);
@@ -209,6 +210,7 @@ export const useInteractiveDots = ({
   autoConsumed,
   isRuning,
   quit,
+  enter,
   beforeHandler,
 }: UseInteractiveProps) => {
   if (autoConsumed === void 0) autoConsumed = false;
@@ -224,14 +226,14 @@ export const useInteractiveDots = ({
     let pushed = false;
     const empty = { x: -9999, y: -9999 };
     const pointer = ref(empty);
-
+    enter && enter()
     mode.add(Mode.draging);
 
     return mergeFuns(
       () => {
         mode.del(Mode.draging);
       },
-      clickListener(dom, (_, ev) => {
+      clickListener(dom, (_) => {
         if (!moveIng || !can.dragMode) return;
         pointer.value = { ...empty };
         singleDone.value = true;

+ 1 - 1
src/core/hook/use-transformer.ts

@@ -1,5 +1,5 @@
 import { useMouseShapeStatus } from "./use-mouse-status.ts";
-import { Ref, ref, watch, watchEffect } from "vue";
+import { Ref, ref, watch } from "vue";
 import { DC, EntityShape } from "../../deconstruction";
 import {
   installGlobalVar,

+ 2 - 6
src/core/renderer/draw-group.vue

@@ -2,6 +2,7 @@
   <ShapeComponent
     :data="(item as any)"
     v-for="item in tempItems"
+    temp
     :key="item.id"
     addMode
   />
@@ -9,15 +10,10 @@
 
 <script setup lang="ts">
 import { ShapeType, components } from "../components";
-import { useStore } from "../store/index.ts";
 import { useInteractiveAdd } from "../hook/use-add.ts";
 
 const props = defineProps<{ type: ShapeType }>();
-const store = useStore();
-const tempItems = useInteractiveAdd(props.type, (data) => {
-  store.addItems(props.type, data);
-});
-
+const tempItems = useInteractiveAdd(props.type);
 const type = props.type;
 const ShapeComponent = components[type].TempComponent || components[type].Component;
 </script>

+ 34 - 0
src/core/renderer/draw-shape.vue

@@ -0,0 +1,34 @@
+<template>
+  <ShapeComponent :data="(item as any)" add-mode v-if="show" />
+</template>
+
+<script lang="ts" setup>
+import { computed, onUnmounted, watch } from "vue";
+import { ComponentSnapInfo, DrawItem } from "../components";
+import { useStore } from "../store";
+import { components } from "../components";
+import { useCustomSnapInfos } from "../hook/use-snap";
+
+const props = withDefaults(
+  defineProps<{ item: DrawItem; temp?: boolean; show?: boolean }>(),
+  { show: true }
+);
+const store = useStore();
+const component = computed(() => components[store.getType(props.item.id)!]);
+const ShapeComponent = computed(() => component.value.TempComponent);
+
+const customSnapInfos = useCustomSnapInfos();
+let infos: ComponentSnapInfo[] = [];
+watch(
+  () => ({ canSnap: !props.temp && !props.item.ref, item: props.item }),
+  ({ canSnap, item }) => {
+    infos.forEach(customSnapInfos.remove);
+    if (!canSnap) return;
+    infos = component.value.getSnapInfos(item as any);
+    infos.forEach(customSnapInfos.add);
+  },
+  { deep: true, immediate: true, flush: "sync" }
+);
+
+onUnmounted(() => infos.forEach(customSnapInfos.remove));
+</script>

+ 14 - 10
src/core/renderer/group.vue

@@ -1,23 +1,27 @@
 <template>
-  <!-- @add="(value: any) => store.addItem(type, value)" -->
-  <ShapeComponent
-    :data="item"
-    v-for="item in items"
-    :key="item.id"
-    @updateShape="(value: any) => store.setItem(type, { id: item.id, value })"
-    @addShape="(value: any) => store.addItem(type, value)"
-    @delShape="() => store.delItem(type, item.id)"
-  />
+  <template v-for="item in items" :key="item.id">
+    <ShapeComponent
+      :data="item"
+      @updateShape="(value: any) => store.setItem(type, { id: item.id, value })"
+      @addShape="(value: any) => store.addItem(type, value)"
+      @delShape="() => store.delItem(type, item.id)"
+      v-if="!itemHasRegistor(item.id)"
+    />
+    <template v-else>
+      <component :is="renderer(item.id)" :item="item" />
+    </template>
+  </template>
 </template>
 
 <script setup lang="ts">
 import { ShapeType, components } from "../components";
 import { computed } from "vue";
-import { useStore } from "../store";
+import { useStore, useStoreRenderProcessors } from "../store";
 
 const props = defineProps<{ type: ShapeType }>();
 const store = useStore();
 const type = props.type as "arrow";
 const ShapeComponent = components[type].Component;
 const items = computed(() => store.data[type] || []);
+const { itemHasRegistor, renderer } = useStoreRenderProcessors();
 </script>

+ 20 - 5
src/core/renderer/renderer.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="draw-layout">
+  <div class="draw-layout" @contextmenu.prevent :style="{ cursor: cursorStyle }">
     <div class="mount-mask" :id="DomMountId" />
     <v-stage ref="stage" :config="size">
       <v-layer :config="viewerConfig" id="formal">
@@ -16,7 +16,9 @@
       </v-layer>
       <!--	临时组,提供临时绘画,以及高频率渲染	-->
       <v-layer :config="viewerConfig" id="temp">
-        <TempShapeGroup v-for="type in types" :type="type" :key="type" />
+        <template v-if="mode.include(Mode.add)">
+          <TempShapeGroup v-for="type in types" :type="type" :key="type" />
+        </template>
       </v-layer>
       <v-layer id="helper">
         <ActiveBoxs />
@@ -30,7 +32,7 @@
 import ShapeGroup from "./group.vue";
 import TempShapeGroup from "./draw-group.vue";
 import { DrawData, ShapeType, components } from "../components";
-import { useStage } from "../hook/use-global-vars.ts";
+import { useCursor, useMode, useStage } from "../hook/use-global-vars.ts";
 import { useViewerTransformConfig } from "../hook/use-viewer.ts";
 import { useListener, useResize } from "../hook/use-event.ts";
 import { useExpose } from "../hook/use-expose.ts";
@@ -39,6 +41,8 @@ import ActiveBoxs from "../helper/active-boxs.vue";
 import SnapLines from "../helper/snap-lines.vue";
 import { useStore } from "../store/index.ts";
 import { useInteractiveAddShapeAPI } from "../hook/use-add.ts";
+import { Mode } from "@/constant/mode.ts";
+import { computed } from "vue";
 
 const props = defineProps<{
   data: DrawData;
@@ -50,18 +54,29 @@ const stage = useStage();
 const size = useResize();
 const viewerConfig = useViewerTransformConfig();
 const types = Object.keys(components) as ShapeType[];
+const mode = useMode();
 
 // 退出添加模式
 const { quitMouseAddShape } = useInteractiveAddShapeAPI();
 useListener(
   "contextmenu",
   (ev) => {
-    ev.preventDefault();
-    quitMouseAddShape();
+    if (ev.button === 2) {
+      quitMouseAddShape();
+    }
   },
   document.documentElement
 );
 
+const cursor = useCursor();
+const cursorStyle = computed(() => {
+  if (cursor.value.includes(".")) {
+    return `url(${cursor.value}), auto`;
+  } else {
+    return cursor.value;
+  }
+});
+
 defineExpose(useExpose());
 </script>
 

+ 58 - 102
src/core/store/index.ts

@@ -1,110 +1,66 @@
-import { DrawData } from "../components";
-import { SingleHistory } from "../history";
+import { Component, reactive } from "vue";
+import { DrawItem } from "../components";
 import { installGlobalVar } from "../hook/use-global-vars";
 import { useStoreRaw } from "./store";
+import { useHistory } from "../hook/use-history";
 
-const emptyData = JSON.stringify({})
-class DrawHistory extends SingleHistory<string> {
-	preventFlag = false;
-	onceFlag = false;
-	onceHistory: string | null = null
-	initData: string | null = null
-	renderer: (data: string) => void;
-
-	constructor(renderer: (data: string) => void) {	
-		super()
-		this.renderer = renderer
-	}
-
-	setInit(data: string) {
-		this.initData = data
-		super.clear()
-		this.push(data)
-	}
-
-	preventTrack(fn: () => void) {
-		this.preventFlag = true;
-		fn();
-		this.preventFlag = false;
-	}
-
-	push(data: string) {
-		if (this.preventFlag) return;
-		if (this.onceFlag) {
-			this.onceHistory = data
-		} else {
-			super.push(data)
-		}
-	}
-
-	redo() {
-		const data = super.redo()
-		this.renderer(data)
-		return data;
-	}
-
-	undo() {
-		const data = super.undo()
-		this.renderer(data)
-		return data;
-	}
-
-	onceTrack(fn: () => void) {
-		this.onceFlag = true;
-		fn();
-		if (this.onceHistory) {
-			this.push(this.onceHistory)
-			this.onceHistory = null
-		}
-		this.onceFlag = false;
-	}
-
-	clear(): void {
-		this.push(emptyData)
-		this.renderer(emptyData)
-	}
-
-	init() {
-		if (this.initData) {
-			this.push(this.initData)
-			this.renderer(emptyData)
-		}
-	}
-}
-
-
-
-const useStoreAndHistory = installGlobalVar(() => {
+export const useStore = installGlobalVar(() => {
+  const history = useHistory();
   const store = useStoreRaw();
-	const history = new DrawHistory((dataStr) => {
-		const data = JSON.parse(dataStr) as DrawData
-		store.$patch(state => {
-			// console.error('change', state)
-			state.data = data
-		})
-	})
-	
-  const trackActions = ["setStore", "repStore", "addItem", "delItem", "setItem"];
+
+  const trackActions = [
+    "setStore",
+    "repStore",
+    "addItem",
+    "delItem",
+    "setItem",
+  ];
   store.$onAction(({ args, name, after, store }) => {
     if (!trackActions.includes(name)) return;
-		const isInit = name === "setStore"
-		after(() => {
-			if (isInit) {
-				history.setInit(JSON.stringify(store.data))
-			} else {
-				history.push(JSON.stringify(store.data)!)
-			}
-		})
+    const isInit = name === "setStore";
+    after(() => {
+      if (isInit) {
+        history.setInit(JSON.stringify(store.data));
+      } else {
+        history.push(JSON.stringify(store.data!));
+      }
+    });
   });
 
-  return { store, history };
-}, Symbol('storeAndHistory'));
-
-export const useStore = () => {
-	return useStoreAndHistory().store
-}
-
-export const useHistory = () => {
-	return useStoreAndHistory().history
-}
-
+  return store;
+}, Symbol("store"));
+
+export const useStoreRenderProcessors = installGlobalVar(() => {
+  type Processor<T extends DrawItem = DrawItem> = (data: T) => Component;
+  const processors = reactive(new Map<Processor, DrawItem["id"][]>());
+  const store = useStore();
+
+  const result = {
+    register<T extends DrawItem>(
+      processor: Processor<T>,
+      ids: DrawItem["id"][] = []
+    ) {
+      processors.set(processor as any, ids);
+      return reactive(ids);
+    },
+    itemHasRegistor(id: DrawItem["id"]) {
+      for (const ids of processors.values()) {
+        if (ids.includes(id)) return true;
+      }
+      return false;
+    },
+    getProcessor(id: DrawItem["id"]) {
+      for (const [p, ids] of processors.entries()) {
+        if (ids.includes(id)) {
+          return p;
+        }
+      }
+    },
+    renderer(id: DrawItem["id"]) {
+      const processor = result.getProcessor(id);
+      const item = store.items.find((item) => item.id === id);
+      return processor && item && processor(item);
+    },
+  };
+  return result;
+}, Symbol("storeRenderProcessors"));

+ 1 - 0
src/core/store/store.ts

@@ -36,6 +36,7 @@ export const useStoreRaw = defineStore('draw-data', {
 			if (~ndx) return this.data[type]![ndx]
 		},
 		addItems<T extends ShapeType>(type: T, items: DrawItem<T>[]) {
+			console.log('add', items)
 			items.forEach(item => this.addItem(type, item))
 		},
 		addItem<T extends ShapeType>(type: T, item: DrawItem<T>) {

+ 1 - 1
src/example/fuse/views/header/header.vue

@@ -28,7 +28,7 @@
           旋转<el-icon><Plus /></el-icon>
         </span>
         <span class="operate" @click="initView">
-          初始<el-icon><Plus /></el-icon>
+          恢复视图<el-icon><Plus /></el-icon>
         </span>
         <span class="operate" @click="emit('full')">
           全屏<el-icon><Plus /></el-icon>

+ 46 - 16
src/utils/math.ts

@@ -4,7 +4,7 @@ import { round } from "./shared.ts";
 
 export type Pos = { x: number; y: number };
 
-export const vector = (pos: Pos = {x: 0, y: 0}): Vector2 => {
+export const vector = (pos: Pos = { x: 0, y: 0 }): Vector2 => {
   return new Vector2(pos.x, pos.y);
   // if (pos instanceof Vector2) {
   //   return pos;
@@ -16,7 +16,7 @@ export const lVector = (line: Pos[]) => line.map(vector);
 
 export const zeroEq = (n: number) => Math.abs(n) < 0.0001;
 export const numEq = (p1: number, p2: number) => zeroEq(p1 - p2);
-export const vEq = (v1: Pos, v2: Pos) => numEq(v1.x, v2.x) && numEq(v2.y, v2.y);
+export const vEq = (v1: Pos, v2: Pos) => numEq(v1.x, v2.x) && numEq(v1.y, v2.y);
 
 export const vsBound = (positions: Pos[]) => {
   const box = new Box2();
@@ -32,15 +32,13 @@ export const vsBound = (positions: Pos[]) => {
 export const lineVector = (line: Pos[]) =>
   vector(line[1]).sub(vector(line[0])).normalize();
 
-const epsilon = 1e-6; // 误差范围
-
 /**
  * 点是否相同
  * @param p1 点1
  * @param p2 点2
  * @returns 是否相等
  */
-export const eqPoint = vEq
+export const eqPoint = vEq;
 
 /**
  * 方向是否相同
@@ -49,7 +47,7 @@ export const eqPoint = vEq
  * @returns 是否相等
  */
 export const eqNGDire = (p1: Pos, p2: Pos) =>
-  eqPoint(p1, p2) || eqPoint(p1, vector(p2).multiplyScalar(-1))
+  eqPoint(p1, p2) || eqPoint(p1, vector(p2).multiplyScalar(-1));
 
 /**
  * 获取两点距离
@@ -280,13 +278,13 @@ export const lineRelationship = (l1: Pos[], l2: Pos[]) => {
 };
 
 export const createLine = (p: Pos, v: Pos, l?: number) => {
-  const line = [p]
+  const line = [p];
   if (l) {
-    v = vector(v).multiplyScalar(l)
+    v = vector(v).multiplyScalar(l);
   }
-  line[1] = vector(line[0]).add(v)
-  return line
-}
+  line[1] = vector(line[0]).add(v);
+  return line;
+};
 
 /**
  * 获取两线段交点,可延长相交
@@ -323,6 +321,38 @@ export const lineIntersection = (l1: Pos[], l2: Pos[]) => {
 };
 
 /**
+ * 获取点是否在线上
+ * @param line 线段
+ * @param position 点
+ */
+export const lineInner = (line: Pos[], position: Pos) => {
+  
+  // 定义线段的起点和终点坐标
+  const [A, B] = lVector(line);
+  // 定义一个点的坐标
+  const P = vector(position);
+
+  // 计算向量 AP 和 AB
+  const AP = P.clone().sub(A);
+  const AB = B.clone().sub(A);
+
+  // 计算叉积
+  const crossProduct = AP.x * AB.y - AP.y * AB.x;
+
+  // 如果叉积不为 0,说明点 P 不在直线 AB 上
+  if (!zeroEq(crossProduct)) {
+    return false;
+  }
+  // 检查点 P 的坐标是否在 A 和 B 的坐标范围内
+  return (
+    Math.min(A.x, B.x) <= P.x &&
+    P.x <= Math.max(A.x, B.x) &&
+    Math.min(A.y, B.y) <= P.y &&
+    P.y <= Math.max(A.y, B.y)
+  );
+};
+
+/**
  * 获取点在线段上的投影
  * @param line 线段
  * @param position 点
@@ -422,8 +452,8 @@ export function calculateScaleFactor(
 ) {
   const op1 = vector(p1).sub(origin);
   const op2 = vector(p2).sub(origin);
-  const xZero = zeroEq(op1.x)
-  const yZero = zeroEq(op1.y)
+  const xZero = zeroEq(op1.x);
+  const yZero = zeroEq(op1.y);
 
   if (zeroEq(op1.x) || zeroEq(op2.y)) return;
   if (zeroEq(scaleDirection.x)) {
@@ -447,11 +477,11 @@ export function calculateScaleFactor(
   const yScaleFactor = op2.y / (op1.y * scaleDirection.y);
 
   if (xZero) {
-    return yScaleFactor
+    return yScaleFactor;
   } else if (yZero) {
-    return xScaleFactor
+    return xScaleFactor;
   }
-  console.log(xScaleFactor - yScaleFactor)
+  console.log(xScaleFactor - yScaleFactor);
   if (zeroEq(xScaleFactor - yScaleFactor)) {
     return xScaleFactor;
   }

+ 2 - 0
src/utils/shared.ts

@@ -46,6 +46,8 @@ export const mergeFuns = (...fns: (() => void)[] | (() => void)[][]) => {
   };
 };
 
+export const copy = <T>(data: T): T => JSON.parse(JSON.stringify(data))
+
 /**
  * 获取数据类型
  * @param value