Przeglądaj źródła

feat: 增加绘画时操控View的能力

bill 7 miesięcy temu
rodzic
commit
cbfdb40550

+ 7 - 1
src/constant/mode.ts

@@ -1,7 +1,13 @@
 export enum Mode {
+	// 只读不做任何响应
+	readonly = 'readonly',
+	// 添加模式
 	add = 'add',
+	// 阅读模式
 	viewer = 'viewer',
-	readonly = 'readonly',
+	// 处于拖拽,可与update和add共存
+	draging = 'draging',
+	// 编辑模式
 	update = 'update',
 }
 

+ 2 - 1
src/core/components/index.ts

@@ -59,8 +59,9 @@ export type DrawData = {
 
 export type DrawItem<T extends ShapeType = ShapeType> = DrawDataItem[T]
 
+export type SnapPoint = Pos & { view?: boolean }
 export type ComponentSnapInfo = {
-	point: Pos,
+	point: SnapPoint,
 	links: Pos[]
 	linkDirections: Pos[],
 	linkAngle: number[]

+ 1 - 1
src/core/components/line/index.ts

@@ -29,7 +29,7 @@ export const getMouseStyle = (data: LineData) => {
   };
 };
 
-export const getSnapInfos = (data: LineData) => generateSnapInfos(data.points, true, false)
+export const getSnapInfos = (data: LineData) => generateSnapInfos(data.points, true, true, true)
 
 export type LineData = Partial<typeof defaultStyle> & BaseItem & {
   points: Pos[];

+ 1 - 1
src/core/components/polygon/index.ts

@@ -30,7 +30,7 @@ export const getMouseStyle = (data: PolygonData) => {
 
 export const addMode = "dots";
 
-export const getSnapInfos = (data: PolygonData) => generateSnapInfos(data.points, true, false)
+export const getSnapInfos = (data: PolygonData) => generateSnapInfos(data.points, true, true)
 
 export type PolygonData = Partial<typeof defaultStyle> & BaseItem & {
   fill?: string

+ 1 - 1
src/core/components/triangle/triangle.vue

@@ -22,7 +22,7 @@ const emit = defineEmits<{
   (e: "delShape"): void;
 }>();
 
-const { shape, tData, operateMenus, describes } = useComponentStatus({
+const { shape, tData, operateMenus, describes, data } = useComponentStatus({
   emit,
   props,
   getMouseStyle,

+ 28 - 11
src/core/components/util.ts

@@ -1,4 +1,4 @@
-import { lineVector, Pos, vectorAngle } from "@/utils/math";
+import { lineVector, Pos, vectorAngle, verticalVector } from "@/utils/math";
 import { onlyId, rangMod } from "@/utils/shared";
 import { MathUtils } from "three";
 import { ComponentSnapInfo } from ".";
@@ -36,10 +36,12 @@ export const getRectSnapPoints = (
   ];
 };
 
+
 export const generateSnapInfos = (
-  geo: Pos[],
+  geo: (Pos & { view?: boolean })[],
   hvAxis = true,
-  link = true
+  link = true,
+  vertical = false,
 ): ComponentSnapInfo[] => {
   const len = geo.length;
   return geo.map((point, ndx) => {
@@ -47,17 +49,32 @@ export const generateSnapInfos = (
     const linkDirections: Pos[] = [];
     const linkAngle: number[] = [];
 
-    if (link) {
-      const prev = geo[rangMod(ndx - 1, len)];
-      const next = geo[rangMod(ndx + 1, len)];
-      const prevVector = lineVector([point, prev]);
-      const nextVector = lineVector([point, next]);
-      links.push(prev, next);
-      linkDirections.push(prevVector, nextVector);
+    const pushLink = (p: Pos) => {
+      const prevVector = lineVector([point, p]);
+      links.push(p);
+      linkDirections.push(prevVector);
       linkAngle.push(
         rangMod(MathUtils.radToDeg(vectorAngle(prevVector)), 180),
-        rangMod(MathUtils.radToDeg(vectorAngle(nextVector)), 180)
       );
+      if (vertical) {
+        const vLine = verticalVector(prevVector)
+        linkDirections.push(vLine);
+        linkAngle.push(
+          rangMod(MathUtils.radToDeg(vectorAngle(vLine)), 180),
+        );
+      }
+
+    }
+
+    if (link && geo.length > 1) {
+      if (ndx > 0) {
+        const prev = geo[rangMod(ndx - 1, len)];
+        pushLink(prev)
+      }
+      if (ndx < len - 1) {
+        const next = geo[rangMod(ndx + 1, len)];
+        pushLink(next)
+      }
     }
     if (hvAxis) {
       linkDirections.push({ x: 1, y: 0 }, { y: 1, x: 0 });

+ 1 - 0
src/core/helper/active-boxs.vue

@@ -55,6 +55,7 @@ const shapeListener = (shape: EntityShape, onCleanup: OnCleanup) => {
   });
   shape.on("bound-change", update);
   watch(() => getComponentData(shape).value, update);
+  update();
   onCleanup(() => shape.off("bound-change", update));
 };
 

+ 2 - 2
src/core/helper/snap-lines.vue

@@ -12,7 +12,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, Ref, ref } from "vue";
+import { computed, ComputedRef, ref } from "vue";
 import { SnapInfo, useGlobalSnapInfos, useSnapResultInfo } from "../hook/use-snap";
 import { useViewerTransform } from "../hook/use-viewer";
 import { RectConfig } from "konva/lib/shapes/Rect";
@@ -42,7 +42,7 @@ const viewerTransform = useViewerTransform();
 const info = useSnapResultInfo();
 const minOffset = 10;
 const minAngle = MathUtils.degToRad(5);
-let snapInfos: Ref<SnapInfo[]>;
+let snapInfos: ComputedRef<SnapInfo[]>;
 if (debug) {
   snapInfos = useGlobalSnapInfos();
 }

+ 0 - 1
src/core/history.ts

@@ -20,7 +20,6 @@ export class SingleHistory<T = any> {
 	private syncState() {
 		this.hasRedo.value = this.history.hasRedo;
 		this.hasUndo.value = this.history.hasUndo;
-		console.log(this.history.hasRedo, this.history.hasUndo)
 	}
 
 	get data() {

+ 365 - 0
src/core/hook/use-add.ts

@@ -0,0 +1,365 @@
+import { nextTick, reactive, ref, watch, watchEffect } from "vue";
+import { useCan, useMode, useStage } from "./use-global-vars";
+import {
+  Area,
+  useInteractiveAreas,
+  useInteractiveDots,
+  useInteractiveProps,
+} from "./use-interactive";
+import { Mode } from "@/constant/mode";
+import { mergeFuns } from "@/utils/shared";
+import {
+  Components,
+  components,
+  ComponentSnapInfo,
+  ComponentValue,
+  DrawItem,
+  ShapeType,
+  SnapPoint,
+} from "../components";
+import { useConversionPosition } from "./use-coversion-position";
+import { Pos } from "@/utils/math";
+import { useCustomSnapInfos, useSnap } from "./use-snap";
+import { generateSnapInfos } from "../components/util";
+
+export type AddMessageData<T extends ShapeType> = ComponentValue<
+  T,
+  "addMode"
+> extends "area"
+  ? Area
+  : Pos;
+
+export const useInteractiveAddShapeAPI = () => {
+  const mode = useMode();
+  const can = useCan();
+  const interactiveProps = useInteractiveProps();
+  const conversion = useConversionPosition(true);
+
+  let quitHook: null | (() => void) = null;
+  return {
+    addShape: <T extends ShapeType>(
+      shapeType: T,
+      preset: Partial<DrawItem<T>> = {},
+      data: AddMessageData<T>,
+      pixel = false
+    ) => {
+      if (!can.addMode) {
+        throw "当前状态不允许添加";
+      }
+      mode.push(Mode.add);
+      if (pixel) {
+        data = (
+          Array.isArray(data) ? data.map(conversion) : conversion(data)
+        ) as AddMessageData<T>;
+      }
+      interactiveProps.value = {
+        type: shapeType,
+        preset,
+        callback: () => {
+          mode.pop();
+        },
+        operate: { single: true, immediate: true, data },
+      };
+    },
+    enterMouseAddShape: <T extends ShapeType>(
+      shapeType: T,
+      preset: Partial<DrawItem<T>> = {},
+      single = false
+    ) => {
+      if (!can.addMode || mode.include(Mode.add)) {
+        throw "当前状态不允许添加";
+      }
+      mode.push(Mode.add);
+      quitHook = () => {
+        mode.pop();
+        quitHook = null
+      }
+      interactiveProps.value = {
+        type: shapeType,
+        preset,
+        operate: { single },
+        callback: quitHook,
+      };
+    },
+    quitMouseAddShape: () => {
+      if (quitHook) {
+        mode.pop();
+      }
+      interactiveProps.value = void 0;
+    },
+  };
+};
+
+export const useIsAddRunning = (shapeType?: ShapeType) => {
+  const stage = useStage();
+  const mode = useMode();
+  const interactiveProps = useInteractiveProps();
+  const isRunning = ref<boolean>(false);
+  let currentPreset: any;
+  const updateIsRunning = () => {
+    const isRun = !!(
+      stage.value &&
+      mode.include(Mode.add) &&
+      (!shapeType || shapeType === interactiveProps.value?.type)
+    );
+
+    if (isRunning.value !== isRun) {
+      isRunning.value = isRun;
+    } else if (currentPreset !== interactiveProps.value?.preset) {
+      isRunning.value = false;
+      nextTick(() => {
+        isRunning.value = isRun;
+      });
+    }
+    currentPreset = interactiveProps.value?.preset;
+  };
+  watchEffect(updateIsRunning);
+  return isRunning;
+};
+
+const usePointBeforeHandler = (enableTransform = false, enableSnap = false) => {
+  const conversionPosition = useConversionPosition(enableTransform);
+  const snap = enableSnap && useSnap();
+  const infos = useCustomSnapInfos();
+  const addedInfos: ComponentSnapInfo[] = [];
+
+  return {
+    transform: (
+      point: SnapPoint,
+      prevPoint?: SnapPoint,
+      nextPoint?: SnapPoint
+    ) => {
+      snap && snap.clear();
+      let p = conversionPosition(point);
+      const geo = [p];
+      prevPoint && geo.unshift({ ...prevPoint, view: true });
+      nextPoint && geo.push({ ...nextPoint, view: true });
+      const selfInfos = generateSnapInfos(geo, true, true);
+
+      const transform = snap && snap.move(selfInfos);
+      p = transform ? transform.point(p) : p;
+
+      return p;
+    },
+    addRef(p: Pos | Pos[]) {
+      const geo = Array.isArray(p) ? p : [p];
+      const snapInfos = generateSnapInfos(geo, true, true);
+      snapInfos.forEach((info) => {
+        infos.add(info);
+        addedInfos.push(info);
+      });
+    },
+    clear: () => {
+      snap && snap.clear();
+    },
+    clearRef: () => {
+      addedInfos.forEach((info) => {
+        infos.remove(info);
+      });
+      addedInfos.length = 0;
+    },
+  };
+};
+
+// 拖拽面积确定组件
+export const useInteractiveAddAreas = <T extends ShapeType>(
+  type: T,
+  refSelf = true
+) => {
+  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 beforeHandler = usePointBeforeHandler(true, true);
+  const clear = () => {
+    beforeHandler.clear();
+    beforeHandler.clearRef();
+  };
+
+  const interactive = useInteractiveAreas({
+    shapeType: type,
+    isRuning,
+    quit: () => {
+      quitMouseAddShape();
+      clear();
+    },
+    beforeHandler: (p) => {
+      beforeHandler.clear();
+      return beforeHandler.transform(p);
+    },
+  });
+
+  // 每次拽结束都加组件
+  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);
+    },
+    { immediate: true }
+  );
+
+  return { items, viewItems };
+};
+
+// 多点确定组件
+export const useInteractiveAddDots = <T extends ShapeType>(
+  type: T,
+  single = false,
+  snap = { prev: false, next: false }
+) => {
+  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 beforeHandler = usePointBeforeHandler(true, true);
+
+  // 多点确定组件,
+  const interactive = useInteractiveDots({
+    shapeType: type,
+    isRuning,
+    beforeHandler: (p) => {
+      beforeHandler.clear();
+      return beforeHandler.transform(p, prev, next);
+    },
+    quit: () => {
+      quitMouseAddShape();
+      beforeHandler.clear();
+      item = null;
+    },
+  });
+
+  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();
+            }
+          )
+        );
+      }
+      interactive.consume(dots);
+    }
+  );
+  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];
+        });
+      }
+
+      items.forEach((item) => {
+        const infos = obj.getSnapInfos(item as any);
+        infos.forEach((info) => snapInfos.add(info));
+        addedItems[item.id] = infos;
+      });
+    },
+    { deep: true }
+  );
+
+  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;
+    },
+    { flush: "pre" }
+  );
+
+  return viewItems;
+};

+ 4 - 3
src/core/hook/use-coversion-position.ts

@@ -3,9 +3,10 @@ import { useViewerInvertTransform } from "./use-viewer.ts";
 
 export const useConversionPosition = (enable: boolean) => {
 	const invertTransform = enable && useViewerInvertTransform();
-	return (position: Pos) => {
+	return <T extends Pos>(position: T) => {
 		return invertTransform
-			? invertTransform.value.point(position)
+			? {...position, ...invertTransform.value.point(position)}
 			: position;
 	}
-};
+};
+

+ 6 - 3
src/core/hook/use-expose.ts

@@ -1,10 +1,10 @@
 import { useLayers, useMode, useStage } from "./use-global-vars.ts";
 import { Stage } from "konva/lib/Stage";
-import { computed, reactive } from "vue";
-import { useInteractiveProps, useInteractiveShapeAPI } from "./use-interactive.ts";
+import { useInteractiveProps } from "./use-interactive.ts";
 import { useHistory, useStore } from "../store/index.ts";
 import { useViewer } from "./use-viewer.ts";
 import { useGlobalResize } from "./use-event.ts";
+import { useInteractiveAddShapeAPI } from "./use-add.ts";
 
 type PickParams<K extends keyof Stage, O extends string> = Stage[K]  extends (...args: any) => any ?  Omit<Required<Parameters<Stage[K]>>[0], O> : never
 
@@ -35,7 +35,7 @@ export const useExpose = () => {
 	}
 
 	return {
-		...useInteractiveShapeAPI(),
+		...useInteractiveAddShapeAPI(),
 		get stage()  {
 			const $store = stage.value!.getStage()
 			return $store
@@ -46,6 +46,9 @@ export const useExpose = () => {
 		history,
 		store,
 		mode,
+		getData() {
+			return store.data
+		},
 		viewer,
 		presetAdd: interactiveProps,
 	}

+ 206 - 112
src/core/hook/use-global-vars.ts

@@ -1,132 +1,226 @@
 import { DC, EntityShape } from "../../deconstruction";
 import { Stage } from "konva/lib/Stage";
 import {
-	computed,
-	getCurrentInstance,
-	nextTick,
-	onUnmounted,
-	reactive,
-	ref,
-	shallowRef,
-	watch,
-	WatchCallback,
-	watchEffect,
-	WatchOptions,
-	WatchSource,
+  computed,
+  getCurrentInstance,
+  nextTick,
+  onUnmounted,
+  reactive,
+  ref,
+  shallowRef,
+  watch,
+  WatchCallback,
+  watchEffect,
+  WatchOptions,
+  WatchSource,
 } from "vue";
 import { Mode } from "../../constant/mode.ts";
 import { Layer } from "konva/lib/Layer";
 import { Pos } from "@/utils/math.ts";
 import { listener } from "@/utils/event.ts";
+import { mergeFuns } from "@/utils/shared.ts";
 
 export const installGlobalVar = <T>(
-	create: () => { var: T, onDestroy: () => void } | T,
-	key = Symbol('globalVar'),
-	noRefDel = true,
+  create: () => { var: T; onDestroy: () => void } | T,
+  key = Symbol("globalVar"),
+  noRefDel = true
 ) => {
-	let initialed = false
-	let refCount = 0;
-	let onDestroy: (() => void) | null = null
-
-	const useGlobalVar = (): T => {
-		const instance = getCurrentInstance() as any;
-		const ctx = instance.appContext
-		if (!initialed) {
-			let val = create() as any
-			if (typeof val === 'object' && 'var' in val && 'onDestroy' in val) {
-				onDestroy = val.onDestory
-				val = val.var;
-			}
-			ctx[key] = val
-			initialed = true;
-		}
-		return ctx[key];
-	}
-
-	return noRefDel
-		? () => {
-				const instance = getCurrentInstance() as any;
-				const ctx = instance.appContext
-				++refCount;
-				onUnmounted(() => {
-					if (--refCount === 0 && noRefDel) {
-						initialed = false;
-						delete ctx[key]
-						console.log('销毁', key)
-						onDestroy && onDestroy()
-						onDestroy = null
-					}
-				})
-				return useGlobalVar();
-			}
-		: useGlobalVar
+  let initialed = false;
+  let refCount = 0;
+  let onDestroy: (() => void) | null = null;
+
+  const useGlobalVar = (): T => {
+    const instance = getCurrentInstance() as any;
+    const ctx = instance.appContext;
+    if (!initialed) {
+      let val = create() as any;
+      if (typeof val === "object" && "var" in val && "onDestroy" in val) {
+        onDestroy = val.onDestory;
+        val = val.var;
+      }
+      ctx[key] = val;
+      initialed = true;
+    }
+    return ctx[key];
+  };
+
+  return noRefDel
+    ? () => {
+        const instance = getCurrentInstance() as any;
+        const ctx = instance.appContext;
+        ++refCount;
+        onUnmounted(() => {
+          if (--refCount === 0 && noRefDel) {
+            initialed = false;
+            delete ctx[key];
+            console.log("销毁", key);
+            onDestroy && onDestroy();
+            onDestroy = null;
+          }
+        });
+        return useGlobalVar();
+      }
+    : useGlobalVar;
 };
 
 export const stackVar = <T>(init?: T) => {
-	const stack = reactive([init]) as T[]
-	const result = {
-		get value() {
-			return stack[stack.length - 1]
-		},
-		set value(val) {
-			stack[stack.length - 1] = val
-		},
-		push(data: T) {
-			stack.push(data)
-		},
-		pop() {
-			if (stack.length - 1 > 0) {
-				stack.pop()
-			} else {
-				console.error('已到达栈顶')
-			}
-		},
-		cycle<R>(data: T, run: () => R): R {
-			result.push(data)
-			const r = run()
-			result.pop()
-			return r;
-		}
-	}
-	return result
-}
+  const stack = reactive([init]) as T[];
+  const result = {
+    get value() {
+      return stack[stack.length - 1];
+    },
+    set value(val) {
+      stack[stack.length - 1] = val;
+    },
+    push(data: T) {
+      stack.push(data);
+    },
+    pop() {
+      if (stack.length - 1 > 0) {
+        stack.pop();
+      } else {
+        console.error("已到达栈顶");
+      }
+    },
+    cycle<R>(data: T, run: () => R): R {
+      result.push(data);
+      const r = run();
+      result.pop();
+      return r;
+    },
+  };
+  return result;
+};
 
 export const globalWatch = <T>(
-	source: WatchSource<T>, 
-	cb: WatchCallback<T, T>, 
-	options?: WatchOptions
-): () => void => {
-	let stop: () => void
-	nextTick(() => {
-		stop = watch(source, cb as any, options as any)
-	})
-	return () => {
-		stop && stop()
-	}
-}
-
-export const useStage = installGlobalVar(() => shallowRef<DC<Stage> | undefined>(), Symbol("stage"));
-export const useMode = installGlobalVar(() => stackVar(Mode.viewer), Symbol("mode"));
+  source: WatchSource<T>,
+  cb: WatchCallback<T, T>,
+  options?: WatchOptions
+): (() => void) => {
+  let stop: () => void;
+  nextTick(() => {
+    stop = watch(source, cb as any, options as any);
+  });
+  return () => {
+    stop && stop();
+  };
+};
+
+export const useStage = installGlobalVar(
+  () => shallowRef<DC<Stage> | undefined>(),
+  Symbol("stage")
+);
+export const useMode = installGlobalVar(() => {
+  const stack = stackVar(new Set([Mode.viewer]))
+  const modeStack = {
+    ...stack,
+    get value() {
+      return stack.value
+    },
+    set value(val: Set<Mode>) {
+      stack.value = val
+    },
+    push(...modes: Mode[]) {
+      return stack.push(new Set(modes))
+    },
+    include(...modes: Mode[]) {
+      return modes.every((m) => modeStack.value.has(m));
+    },
+    add(...modes: Mode[]) {
+      modes.forEach((mode) => modeStack.value.add(mode));
+    },
+    del(...modes: Mode[]) {
+      modes.forEach((mode) => modeStack.value.delete(mode));
+    }
+  }
+  if (import.meta.env.DEV) {
+    watchEffect(() => {
+      console.error([...modeStack.value.values()].join(','))
+    }, { flush: 'sync' })
+  }
+  return modeStack;
+}, Symbol("mode"));
+export const useCan = installGlobalVar(() => {
+  const mode = useMode();
+  const stage = useStage();
+  const key = useDownKeys()
+  const loaded = computed(() => !!stage.value?.getStage())
+
+  // 鼠标是否可用
+  const mouse = computed(() => loaded.value && !mode.include(Mode.readonly))
+
+  // 可以进入拖拽模式
+  const dragMode = computed(() => {
+    if (!mouse.value || mode.include(Mode.viewer) || key.has(' ')) return false;
+    return mode.include(Mode.add) || mode.include(Mode.update)
+  })
+
+  // 是否在视图模式
+  const viewMode = computed(() => {
+    return mouse.value && (!mode.include(Mode.draging) || key.has(' '))
+  })
+
+  // shape是否可以对鼠标做出反应
+  const mouseReact = computed(() => mouse.value && (mode.include(Mode.viewer) || mode.include(Mode.update)))
+
+  // 可以进入编辑模式
+  const editMode = computed(() => mouse.value && mode.include(Mode.viewer))
+
+  // 可以进入添加模式
+  const addMode = computed(() => mouse.value && mode.include(Mode.viewer))
+
+
+  return reactive({
+    viewMouseReact: mouse,
+    viewMode,
+    addMode,
+    mouseReact,
+    editMode,
+    dragMode,
+  });
+});
+
 export const usePointerPos = installGlobalVar(() => {
-	const stage = useStage()
-	const pos = ref<Pos | null>(null)
-	
-	watchEffect((onCleanup) => {
-		const $stage = stage.value?.getNode()
-		if (!$stage) return;
-
-		const mount = $stage.container().parentElement!
-		pos.value = $stage.pointerPos
-		onCleanup(listener(mount, 'pointermove', ev => {
-			pos.value = $stage.pointerPos
-		}))
-	})
-	return pos
-}, Symbol("pointerPos"))
+  const stage = useStage();
+  const pos = ref<Pos | null>(null);
+
+  watchEffect((onCleanup) => {
+    const $stage = stage.value?.getNode();
+    if (!$stage) return;
+
+    const mount = $stage.container().parentElement!;
+    pos.value = $stage.pointerPos;
+    onCleanup(
+      listener(mount, "pointermove", () => {
+        pos.value = $stage.pointerPos;
+      })
+    );
+  });
+  return pos;
+}, Symbol("pointerPos"));
+
+export const useDownKeys = installGlobalVar(() => {
+  const keys = reactive(new Set<string>());
+  const cleanup = mergeFuns(
+    listener(window, "keydown", (ev) => {
+      keys.add(ev.key);
+    }),
+    listener(window, "keyup", (ev) => {
+      keys.delete(ev.key);
+    })
+  );
+  return {
+    var: keys,
+    onDestroy: cleanup,
+  };
+});
 
 export const useLayers = () => {
-	const stage = useStage()
-	return computed(() => stage.value?.getNode().children as Layer[]) 
-}
+  const stage = useStage();
+  return computed(() => stage.value?.getNode().children as Layer[]);
+};
 
-export const useTransformIngShapes = installGlobalVar(() => ref<EntityShape[]>([]), Symbol("transformIngShapes"))
+export const useTransformIngShapes = installGlobalVar(
+  () => ref<EntityShape[]>([]),
+  Symbol("transformIngShapes")
+);

+ 236 - 239
src/core/hook/use-interactive.ts

@@ -1,268 +1,265 @@
-import { installGlobalVar, useMode, useStage, } from "./use-global-vars.ts";
-import { ComponentValue, DrawItem, ShapeType } from "../components";
-import { nextTick, reactive, Ref, ref, watch, watchEffect } from "vue";
+import {
+  installGlobalVar,
+  useCan,
+  useMode,
+  useStage,
+} from "./use-global-vars.ts";
+import { DrawItem, ShapeType } from "../components";
+import { reactive, Ref, ref, watch, watchEffect } from "vue";
 import { Pos } from "../../utils/math.ts";
-import { clickListener, dragListener, getOffset, listener } from "../../utils/event.ts";
-import { Mode } from "../../constant/mode.ts";
-import { inRevise, mergeFuns } from "../../utils/shared.ts";
-import { useConversionPosition } from "./use-coversion-position.ts";
-
+import { clickListener, getOffset, listener } from "../../utils/event.ts";
+import { mergeFuns } from "../../utils/shared.ts";
+import { Mode } from "@/constant/mode.ts";
 
 export type InteractivePreset<T extends ShapeType = ShapeType> = {
-	type: T;
-	preset?: Partial<DrawItem<T>>,
-	operate?: {
-		immediate?: boolean,
-		single?: boolean,
-		data?: any
-	}
+  type: T;
+  callback?: () => void;
+  preset?: Partial<DrawItem<T>>;
+  operate?: {
+    immediate?: boolean;
+    single?: boolean;
+    data?: any;
+  };
 };
-export const useInteractiveProps = installGlobalVar(() => ref<InteractivePreset | undefined>(), Symbol("interactiveProps"));
+export const useInteractiveProps = installGlobalVar(
+  () => ref<InteractivePreset | undefined>(),
+  Symbol("interactiveProps")
+);
 
-type Area = [Pos, Pos];
-export enum InteractiveAction { delete }
-export type InteractiveMessage = { area?: Area; dot?: Pos, ndx?: number, action?: InteractiveAction };
-export type InteractiveMessageData<T extends ShapeType> = ComponentValue<T, 'addMode'> extends 'area' ? Area : Pos
-export type InteractiveAreas = ReturnType<typeof useInteractiveAreas>;
-export type InteractiveDots = ReturnType<typeof useInteractiveDots>;
-export type Interactive = InteractiveAreas | InteractiveDots
-
-export const useInteractiveShapeAPI = () => {
-	const mode = useMode();
-	const interactiveProps = useInteractiveProps();
-	const conversion = useConversionPosition(true)
-	return {
-		addShape: <T extends ShapeType>(
-			shapeType: T, 
-			preset: Partial<DrawItem<T>> = {}, 
-			data: InteractiveMessageData<T>, 
-			pixel = false
-		) => {
-			mode.value = Mode.add
-			if (pixel) {
-				data = (Array.isArray(data) ? data.map(conversion) : conversion(data)) as InteractiveMessageData<T>
-			}
-			interactiveProps.value = {
-				type: shapeType, preset, 
-				operate: { single: true, immediate: true, data }
-			}
-		},
-		enterMouseAddShape: <T extends ShapeType>(shapeType: T, preset: Partial<DrawItem<T>> = {}, single = false) => {
-			mode.value = Mode.add
-			interactiveProps.value = {type: shapeType, preset, operate: {single}}
-		},
-		quitMouseAddShape: () => {
-			mode.value = Mode.viewer
-			interactiveProps.value = void 0
-		}
-	}
+export type Area = [Pos, Pos];
+export enum InteractiveAction {
+  delete,
 }
-
-
-export const useIsRunning = (shapeType?: ShapeType) => {
-	const stage = useStage();
-	const mode = useMode();
-	const interactiveProps = useInteractiveProps();
-	const isRunning = ref<boolean>(false);
-	const updateIsRunning = () => {
-		isRunning.value = !!(stage.value &&
-			mode.value === Mode.add &&
-			(!shapeType || shapeType === interactiveProps.value?.type))
-	}
-	watchEffect(updateIsRunning)
-	watch(
-		() => interactiveProps.value?.preset,
-		(nPreset, oPreset) => {
-			if (isRunning.value && inRevise(nPreset, oPreset)) {
-				isRunning.value = false
-				nextTick(updateIsRunning)
-			}
-		}, {flush: 'post'})
-
-	return isRunning
+export type InteractiveMessage = {
+  area?: Area;
+  dot?: Pos;
+  ndx?: number;
+  action?: InteractiveAction;
 };
-
+export type InteractiveAreas = ReturnType<typeof useInteractiveAreas>;
+export type InteractiveDots = ReturnType<typeof useInteractiveDots>;
+export type Interactive = InteractiveAreas | InteractiveDots;
 
 const useInteractiveExpose = <T extends object>(
-	messages: Ref<T[]>,
-	init: (dom: HTMLDivElement) => () => void,
-	singleDone: Ref<boolean>,
-	shapeType?: ShapeType,
-	autoConsumed?: boolean,
+  messages: Ref<T[]>,
+  init: (dom: HTMLDivElement) => () => void,
+  singleDone: Ref<boolean>,
+  isRunning: Ref<boolean>,
+  quit: () => void,
+  autoConsumed?: boolean
 ) => {
-	const consumedMessages = reactive(new WeakSet<T>()) as WeakSet<T>;
-	const stage = useStage();
-	const interactiveProps = useInteractiveProps();
-	const isRunning = useIsRunning(shapeType);
-	const {quitMouseAddShape} = useInteractiveShapeAPI()
-
-	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) {
-							quitMouseAddShape()
-						}
-					}, {flush: 'post'})
-				)
-			}
+  const consumedMessages = reactive(new WeakSet<T>()) as WeakSet<T>;
+  const stage = useStage();
+  const interactiveProps = useInteractiveProps();
 
-			// 单纯添加
-			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))
-			}
-			onCleanup(mergeFuns(cleanups));
-		} else {
-			messages.value = [];
-		}
-	});
+  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" }
+          )
+        );
+      }
 
-	return {
-		isRunning,
-		get preset() {
-			return interactiveProps.value?.preset;
-		},
-		get messages() {
-			const items = messages.value;
-			const result = items.filter((item) => !consumedMessages.has(item));
-			autoConsumed && result.forEach((item) => consumedMessages.add(item));
-			return result as T[];
-		},
-		getNdx(item: T) {
-			return messages.value.indexOf(item)
-		},
-		get consumedMessage() {
-			const items = messages.value;
-			return items.filter((item) => consumedMessages.has(item)) as T[];
-		},
-		consume(items: T[]) {
-			items.forEach((item) => consumedMessages.add(item));
-		},
-		singleDone
-	};
-}
+      // 单纯添加
+      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 {
+      messages.value = [];
+    }
+  });
 
+  return {
+    isRunning,
+    get preset() {
+      return interactiveProps.value?.preset;
+    },
+    get messages() {
+      const items = messages.value;
+      const result = items.filter((item) => !consumedMessages.has(item));
+      autoConsumed && result.forEach((item) => consumedMessages.add(item));
+      return result as T[];
+    },
+    getNdx(item: T) {
+      return messages.value.indexOf(item);
+    },
+    get consumedMessage() {
+      const items = messages.value;
+      return items.filter((item) => consumedMessages.has(item)) as T[];
+    },
+    consume(items: T[]) {
+      items.forEach((item) => consumedMessages.add(item));
+    },
+    singleDone,
+  };
+};
 
 type UseInteractiveProps = {
-	shapeType?: ShapeType;
-	enableTransform?: boolean;
-	autoConsumed?: boolean;
+  isRuning: Ref<boolean>;
+  quit: () => void;
+  beforeHandler?: (p: Pos) => Pos;
+  shapeType?: ShapeType;
+  autoConsumed?: boolean;
 };
 
-
 export const useInteractiveAreas = ({
-																			shapeType,
-																			enableTransform,
-																			autoConsumed,
-																		}: UseInteractiveProps = {}) => {
-	if (enableTransform === void 0) enableTransform = true;
+  isRuning,
+  autoConsumed,
+  beforeHandler,
+  quit,
+}: UseInteractiveProps) => {
+  const mode = useMode();
+  const can = useCan();
+  const singleDone = ref(true);
+  const messages = ref<Area[]>([]);
 
-	const singleDone = ref(true);
-	const messages = ref<Area[]>([])
-	const conversionPosition = useConversionPosition(enableTransform);
+  const init = (dom: HTMLDivElement) => {
+    let pushed = false;
+    let pushNdx = -1;
+    let downed = false;
+    let tempArea: Area;
+    let dragging = false;
 
-	const init = (dom: HTMLDivElement) => {
-		let pushed = false;
-		let pushNdx = -1;
-		let downed = false;
-		let tempArea: Area;
-		let dragging = false
-		return dragListener(dom, {
-			down(position, ev) {
-				if (ev.button === 0) {
-					tempArea = [conversionPosition(position)] as unknown as Area;
-					downed = true;
-					singleDone.value = false;
-					dragging = false
-				}
-			},
-			move({end}) {
-				if (!downed) return;
-				if (pushed) {
-					messages.value[pushNdx]![1] = conversionPosition(end);
-				} else {
-					tempArea[1] = conversionPosition(end);
-					pushed = true;
-					pushNdx = messages.value.length;
-					messages.value[pushNdx] = tempArea;
-				}
-				dragging = true
-			},
-			up(position) {
-				if (!downed || !dragging) return;
-				messages.value[pushNdx]![1] = conversionPosition(position);
-				pushNdx = -1;
-				pushed = false;
-				downed = false;
-				dragging = false;
-				singleDone.value = true;
-			},
-		});
-	};
+    return mergeFuns(
+      listener(dom, "pointerdown", (ev) => {
+        if (!can.dragMode) return;
+        const position = getOffset(ev, dom);
+        if (ev.button === 0) {
+          tempArea = [
+            beforeHandler ? beforeHandler(position) : position,
+          ] as unknown as Area;
+          downed = true;
+          singleDone.value = false;
+          dragging = false;
+          mode.add(Mode.draging);
+        }
+      }),
+      listener(document.documentElement, "pointermove", (ev) => {
+        if (!can.dragMode) return;
+        const end = getOffset(ev, dom);
+        const point = beforeHandler ? beforeHandler(end) : end;
 
-	return useInteractiveExpose(
-		messages,
-		init,
-		singleDone,
-		shapeType,
-		autoConsumed
-	)
+        if (downed) {
+          if (pushed) {
+            messages.value[pushNdx]![1] = point;
+          } else {
+            tempArea[1] = point;
+            pushed = true;
+            pushNdx = messages.value.length;
+            messages.value[pushNdx] = tempArea;
+          }
+          dragging = true;
+        } else {
+          tempArea = [point] as unknown as Area;
+        }
+      }),
+      listener(dom, "pointerup", (ev) => {
+        if (downed) {
+          mode.del(Mode.draging);
+        } else if (!dragging) return;
+
+        if (can.dragMode) {
+          const position = getOffset(ev, dom);
+          messages.value[pushNdx]![1] = beforeHandler
+            ? beforeHandler(position)
+            : position;
+        }
+
+        pushNdx = -1;
+        pushed = false;
+        downed = false;
+        dragging = false;
+        singleDone.value = true;
+      })
+    );
+  };
+
+  return useInteractiveExpose(
+    messages,
+    init,
+    singleDone,
+    isRuning,
+    quit,
+    autoConsumed
+  );
 };
 
 export const useInteractiveDots = ({
-																		 shapeType,
-																		 enableTransform,
-																		 autoConsumed,
-																	 }: UseInteractiveProps = {}) => {
-	if (enableTransform === void 0) enableTransform = true;
-	if (autoConsumed === void 0) autoConsumed = false;
+  autoConsumed,
+  isRuning,
+  quit,
+  beforeHandler,
+}: UseInteractiveProps) => {
+  if (autoConsumed === void 0) autoConsumed = false;
+
+  const mode = useMode();
+  const can = useCan();
+  const singleDone = ref(true);
+  const messages = ref<Pos[]>([]);
 
-	const singleDone = ref(true);
-	const conversionPosition = useConversionPosition(enableTransform);
-	const messages = ref<Pos[]>([])
+  const init = (dom: HTMLDivElement) => {
+    if (!can.dragMode) return () => {};
+    let moveIng = false;
+    let pushed = false;
+    const empty = { x: -9999, y: -9999 };
+    const pointer = ref(empty);
 
-	const init = (dom: HTMLDivElement) => {
-		let moveIng = false;
-		let pushed = false;
-		const empty = {x: -9999, y: -9999}
-		const pointer = ref(empty);
+    mode.add(Mode.draging);
 
-		return mergeFuns(
-			clickListener(dom, () => {
-				if (!moveIng) return;
-				pointer.value = {...empty}
-				singleDone.value = true
-				moveIng = false
-				pushed = false
-			}),
-			listener(dom, "pointermove", (ev) => {
-				if (!pushed) {
-					messages.value.push(pointer.value);
-					singleDone.value = false;
-					pushed = true
-				}
+    return mergeFuns(
+      () => {
+        mode.del(Mode.draging);
+      },
+      clickListener(dom, (_, ev) => {
+        if (!moveIng || !can.dragMode) return;
+        pointer.value = { ...empty };
+        singleDone.value = true;
+        moveIng = false;
+        pushed = false;
+      }),
+      listener(dom, "pointermove", (ev) => {
+        if (!can.dragMode) return;
+        if (!pushed) {
+          messages.value.push(pointer.value);
+          singleDone.value = false;
+          pushed = true;
+        }
 
-				moveIng = true
-				const current = conversionPosition(getOffset(ev))
-				pointer.value.x = current.x;
-				pointer.value.y = current.y;
-			})
-		);
-	};
-	return useInteractiveExpose(
-		messages,
-		init,
-		singleDone,
-		shapeType,
-		autoConsumed
-	)
+        moveIng = true;
+        const position = getOffset(ev);
+        const current = beforeHandler ? beforeHandler(position) : position;
+        pointer.value.x = current.x;
+        pointer.value.y = current.y;
+      })
+    );
+  };
+  return useInteractiveExpose(
+    messages,
+    init,
+    singleDone,
+    isRuning,
+    quit,
+    autoConsumed
+  );
 };

+ 5 - 11
src/core/hook/use-mouse-status.ts

@@ -4,7 +4,7 @@ import { Shape } from "konva/lib/Shape";
 import {
   globalWatch,
   installGlobalVar,
-  useMode,
+  useCan,
   useStage,
   useTransformIngShapes,
 } from "./use-global-vars.ts";
@@ -17,11 +17,10 @@ import {
   usePointerIsTransformerInner,
   useTransformer,
 } from "./use-transformer.ts";
-import { Mode } from "@/constant/mode.ts";
 import { useAniamtion } from "./use-animation.ts";
 
 export const useMouseShapesStatus = installGlobalVar(() => {
-  const mode = useMode();
+  const can = useCan()
   const stage = useStage();
   const listeners = ref([]) as Ref<EntityShape[]>;
   const hovers = ref([]) as Ref<EntityShape[]>;
@@ -32,7 +31,6 @@ export const useMouseShapesStatus = installGlobalVar(() => {
   const pointerIsTransformerInner = usePointerIsTransformerInner();
 
   const init = (stage: Stage) => {
-    console.log("init?");
     let downTime: number;
     let downTarget: EntityShape | null;
     const inner = new WeakMap<EntityShape, boolean>();
@@ -124,16 +122,12 @@ export const useMouseShapesStatus = installGlobalVar(() => {
 
   let cleanup: () => void;
   const stopStatusWatch = globalWatch(
-    () =>
-      [
-        stage.value?.getStage(),
-        [Mode.update, Mode.viewer].includes(mode.value),
-      ] as const,
+    () => can.mouseReact,
     (current, prev) => {
       if (inRevise(prev, current)) {
         cleanup! && cleanup();
-        if (current[0] && current[1]) {
-          cleanup = init(current[0]);
+        if (current) {
+          cleanup = init(stage.value!.getNode());
         }
       }
     },

+ 6 - 0
src/core/hook/use-polygon.ts

@@ -0,0 +1,6 @@
+import { Pos } from "@/utils/math";
+
+
+const getPolygonSnapInfos = (points: Pos[]) => {
+  
+}

+ 84 - 38
src/core/hook/use-snap.ts

@@ -5,7 +5,7 @@ import {
   ShapeType,
   ComponentSnapInfo,
 } from "../components";
-import { computed, reactive, watch, watchEffect } from "vue";
+import { computed, reactive, ref, toRaw, watch, watchEffect } from "vue";
 import {
   createLine,
   eqNGDire,
@@ -21,7 +21,7 @@ import {
   verticalVector,
   zeroEq,
 } from "@/utils/math";
-import { installGlobalVar } from "./use-global-vars";
+import { globalWatch, installGlobalVar } from "./use-global-vars";
 import { BaseItem } from "../components/util";
 import {
   ScaleVectorType,
@@ -32,52 +32,92 @@ import {
 import { Transform } from "konva/lib/Util";
 import { useCacheUnitTransform, useViewerInvertTransform } from "./use-viewer";
 import { MathUtils } from "three";
-import { arrayInsert, rangMod } from "@/utils/shared";
+import { arrayInsert, mergeFuns, rangMod } from "@/utils/shared";
 
 export type SnapInfo = ComponentSnapInfo & Pick<DrawItem, "id">;
-export const useGlobalSnapInfos = installGlobalVar(() => {
+const useStoreSnapInfos = () => {
   const store = useStore();
   const types = Object.keys(components) as ShapeType[];
   const infos = reactive(new Set<SnapInfo>());
+  const cleanups = [] as Array<() => void>;
 
   for (const type of types) {
     const comp = components[type];
     if (!("getSnapInfos" in comp)) continue;
-    watch(
-      () => store.data[type],
-      (items) => {
-        if (!items) return;
-        for (const item of items) {
-          const snaps = computed(
-            () => comp.getSnapInfos!(item as any) as SnapInfo[]
-          );
-          const snapInfoWatchStop = watch(
-            snaps,
-            (snaps, _, onCleanup) => {
-              snaps.forEach((snap) => {
-                snap.id = item.id;
-                infos.add(snap);
-              });
-              onCleanup(() => {
-                snaps.forEach((snap) => infos.delete(snap));
-              });
-            },
-            { immediate: true }
-          );
-          const existsWatchStop = watchEffect(() => {
-            if (!items.includes(item as any)) {
+
+    cleanups.push(
+      globalWatch(
+        () => store.data[type]?.map((item) => item),
+        (items, _, onCleanup) => {
+          if (!items) return;
+          for (const item of items) {
+            const snaps = computed(() => {
+              if (item.ref) {
+                return comp.getSnapInfos!(item as any) as SnapInfo[];
+              } else {
+                return [];
+              }
+            });
+            const snapInfoWatchStop = watch(
+              snaps,
+              (snaps, _, onCleanup) => {
+                snaps.forEach((snap) => {
+                  snap.id = item.id;
+                  infos.add(snap);
+                });
+                onCleanup(() => {
+                  snaps.forEach((snap) => infos.delete(snap));
+                });
+              },
+              { immediate: true }
+            );
+            const existsWatchStop = watchEffect(() => {
+              if (!items.includes(item as any)) {
+                snapInfoWatchStop();
+                existsWatchStop();
+              }
+            });
+            onCleanup(() => {
               snapInfoWatchStop();
               existsWatchStop();
-            }
-          });
-        }
-      },
-      { immediate: true }
+            });
+          }
+        },
+        { immediate: true }
+      )
     );
   }
 
-  return computed(() => Array.from(infos.values()));
-}, Symbol('snapInfos'));
+  return {
+    infos: computed(() => Array.from(infos.values())),
+    cleanup: mergeFuns(cleanups),
+  };
+};
+
+export const useCustomSnapInfos = installGlobalVar(() => {
+  const infos = ref<ComponentSnapInfo[]>([]);
+  return {
+    infos,
+    add: (snap: ComponentSnapInfo) => {
+      infos.value.push(snap);
+    },
+    remove: (snap: ComponentSnapInfo) => {
+      const index = infos.value.findIndex((p) => toRaw(p) === toRaw(snap));
+      if (index !== -1) {
+        infos.value.splice(index, 1);
+      }
+    },
+  };
+});
+
+export const useGlobalSnapInfos = installGlobalVar(() => {
+  const storeInfos = useStoreSnapInfos();
+  const customInfos = useCustomSnapInfos();
+  return {
+    var: computed(() => [...customInfos.infos.value, ...storeInfos.infos.value] as SnapInfo[]),
+    onDestroy: storeInfos.cleanup,
+  };
+}, Symbol("snapInfos"));
 
 export const useSnapConfig = () => {
   const unitTransform = useCacheUnitTransform();
@@ -100,7 +140,7 @@ export const useSnapResultInfo = installGlobalVar(() => {
     },
   }) as SnapResultInfo;
   return snapInfo;
-}, Symbol('snapResultInfo'));
+}, Symbol("snapResultInfo"));
 
 export type AttractSnapInfo = {
   ref: ComponentSnapInfo;
@@ -127,6 +167,7 @@ export const filterAttractSnapInfos = (
 ) => {
   const attractSnapInfosGroups: AttractSnapInfo[][] = [];
   for (const self of selfInfos) {
+    if (self.point.view) continue;
     const attractSnapInfos: AttractSnapInfo[] = [];
     for (const ref of refInfos) {
       let infos = filters(self, ref, attractSnapInfos);
@@ -249,6 +290,10 @@ const moveSnap = (
     if (nor === act || exclude.get(nor) === act) {
       return void 0;
     }
+    if (eqPoint(nor.current.point, act.join)) {
+      return addExclude(nor, act);
+    }
+    // console.log(nor.current.point, act.join)
 
     const norJoin = nor.join;
     const norDire = vector(nor.refDirection);
@@ -307,8 +352,8 @@ const moveSnap = (
 
     // TODO 如果没有同一个点的两线段,则使用2垂直的两点线段
     const move = vector(end!).sub(start!);
-    for (let i = 1; i < attractSnapGroups.length; i++) {
-      let j = 0;
+    for (let i = 0; i < attractSnapGroups.length; i++) {
+      let j = i === 0 ? 1 : 0;
       for (; j < attractSnapGroups[i].length; j++) {
         const attractSnap = attractSnapGroups[i][j];
         const rDire = attractSnap.refDirection;
@@ -316,6 +361,7 @@ const moveSnap = (
         if (!numEq(rangMod(angle, Math.PI), Math.PI / 2)) {
           continue;
         }
+
         const cPoint = vector(attractSnap.current.point).add(move);
         const rPoint = attractSnap.ref.point;
         const inter = lineIntersection(
@@ -505,7 +551,7 @@ export const useComponentSnap = (componentId: string) => {
 
   const snapInfos = useGlobalSnapInfos();
   const refSnapInfos = computed(() =>
-    snapInfos.value.filter((p) => p.id !== componentId)
+    snapInfos.value.filter((p) => !("id" in p) || p.id !== componentId)
   );
   const baseSnap = useSnap(refSnapInfos);
   const getOperType = useGetTransformerOperType();

+ 158 - 126
src/core/hook/use-transformer.ts

@@ -1,8 +1,9 @@
 import { useMouseShapeStatus } from "./use-mouse-status.ts";
-import { Ref, ref, watch } from "vue";
+import { Ref, ref, watch, watchEffect } from "vue";
 import { DC, EntityShape } from "../../deconstruction";
 import {
   installGlobalVar,
+  useCan,
   useMode,
   useStage,
   useTransformIngShapes,
@@ -19,7 +20,7 @@ import { Transformer } from "../transformer.ts";
 import { TransformerConfig } from "konva/lib/shapes/Transformer";
 import { themeColor, themeMouseColors } from "@/constant/help-style.ts";
 import { useComponentSnap } from "./use-snap.ts";
-import { useViewerInvertTransform } from "./use-viewer.ts";
+import { useViewerInvertTransform, useViewerTransform } from "./use-viewer.ts";
 import { Rect } from "konva/lib/shapes/Rect";
 import { Text } from "konva/lib/shapes/Text";
 import { Group } from "konva/lib/Group";
@@ -170,6 +171,7 @@ export const useGetTransformerOperDirection = () => {
 export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
   const offset = ref<Pos>();
   const mode = useMode();
+  const can = useCan();
   const conversion = useConversionPosition(true);
   const transformIngShapes = useTransformIngShapes();
 
@@ -178,9 +180,11 @@ export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
 
     let start: Pos | undefined;
     const enter = (position: Pos) => {
+      mode.push(Mode.update);
       if (!start) {
         start = position;
-        mode.push(Mode.update);
+        if (!can.dragMode) return;
+        mode.add(Mode.draging);
         transformIngShapes.value.push(shape);
       }
     };
@@ -198,7 +202,7 @@ export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
     shape.dragBoundFunc((_, ev) => {
       if (!start) {
         enter(ev);
-      } else {
+      } else if (can.dragMode) {
         const end = conversion(getOffset(ev, dom));
         offset.value = {
           x: end.x - start.x,
@@ -215,7 +219,7 @@ export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
     return mergeFuns([
       () => {
         shape.draggable(false);
-        shape.off("pointerdown.mouse-status");
+        shape.off("pointerdown.mouse-drag");
         start && leave();
       },
       listener(document.documentElement, "pointerup", () => {
@@ -225,15 +229,20 @@ export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
   };
 
   watch(
-    () => !!shape.value?.getStage(),
-    (loaded, _, onCleanup) => {
-      loaded && onCleanup(init(shape.value!.getNode()));
+    () => (can.editMode || mode.include(Mode.update)) && shape.value?.getNode(),
+    (canEdit, _, onCleanup) => {
+      canEdit && onCleanup(init(shape.value!.getNode()));
     }
   );
   return offset;
 };
 
-type Rep<T> = { tempShape: T; init?: () => void; update?: () => void; destory: () => void };
+type Rep<T> = {
+  tempShape: T;
+  init?: () => void;
+  update?: () => void;
+  destory: () => void;
+};
 const emptyFn = () => {};
 export const useShapeTransformer = <T extends EntityShape>(
   shape: Ref<DC<T> | undefined>,
@@ -247,133 +256,156 @@ export const useShapeTransformer = <T extends EntityShape>(
   const mode = useMode();
   const transformer = useTransformer();
   const transformIngShapes = useTransformIngShapes();
+  const viewTransform = useViewerTransform();
+  const can = useCan();
 
   const init = ($shape: T) =>
-    mergeFuns(
-      watch(
-        () => status.value.hover,
-        (active, _, onCleanup) => {
-          const parent = $shape.parent;
-          if (!(active && parent)) return;
-          const oldConfig: TransformerConfig = {};
-
-          for (const key in transformerConfig) {
-            oldConfig[key] = (transformer as any)[key]();
-            (transformer as any)[key](transformerConfig[key]);
-          }
-
-          let rep: Rep<T>;
-          if (replaceShape) {
-            rep = replaceShape(transformer, $shape);
-          } else {
-            rep = {
-              tempShape: $shape,
-              destory: emptyFn,
-              update: emptyFn,
-            };
-            transformer.nodes([$shape]);
-            transformer.queueShapes.value = [$shape];
-          }
-          parent.add(transformer);
+    watch(
+      () => status.value.hover,
+      (active, _, onCleanup) => {
+        const parent = $shape.parent;
+        if (!(active && parent)) return;
+        const oldConfig: TransformerConfig = {};
+
+        for (const key in transformerConfig) {
+          oldConfig[key] = (transformer as any)[key]();
+          (transformer as any)[key](transformerConfig[key]);
+        }
 
-          const updateTransform = () => {
-            let appleTransform = rep.tempShape.getTransform().copy();
-            if (handlerTransform) {
-              appleTransform = handlerTransform(appleTransform);
-              setShapeTransform(rep.tempShape, appleTransform);
-            }
-            transform.value = appleTransform;
+        let rep: Rep<T>;
+        if (replaceShape) {
+          rep = replaceShape(transformer, $shape);
+        } else {
+          rep = {
+            tempShape: $shape,
+            destory: emptyFn,
+            update: emptyFn,
           };
+          transformer.nodes([$shape]);
+          transformer.queueShapes.value = [$shape];
+        }
+        parent.add(transformer);
+
+        const updateTransform = () => {
+          if (!can.dragMode) return;
+          let appleTransform = rep.tempShape.getTransform().copy();
+          if (handlerTransform) {
+            appleTransform = handlerTransform(appleTransform);
+            setShapeTransform(rep.tempShape, appleTransform);
+          }
+          transform.value = appleTransform;
+        };
 
-          const downHandler = () => {
-            isEnter && mode.pop();
-            isEnter = true;
-            rep.update && rep.update();
-            mode.push(Mode.update);
-            transformIngShapes.value.push($shape);
-          };
+        const downHandler = () => {
+          if (isEnter) {
+            mode.pop();
+          }
+          mode.push(Mode.update);
+          isEnter = true;
+          if (!can.dragMode) return;
 
-          let isEnter = false;
-          transformer.on("pointerdown.shapemer", downHandler);
-          transformer.on("transform.shapemer", updateTransform);
-          const stop = listener(
-            $shape.getStage()!.container(),
-            "pointerup",
-            () => {
-              if (isEnter) {
-                mode.pop();
-                transform.value = void 0;
-                isEnter = false;
-                const ndx = transformIngShapes.value.indexOf($shape);
-                ~ndx && transformIngShapes.value.splice(ndx, 1);
-              }
-            }
-          );
-
-          // 拖拽时要更新矩阵
-          let prevMoveTf: Transform | null = null;
-          const stopDragWatch = watch(
-            offset,
-            (translate, oldTranslate) => {
-              if (translate) {
-                if (!oldTranslate) {
-                  rep.update && rep.update();
-                }
-                const moveTf = new Transform().translate(
-                  translate.x,
-                  translate.y
-                );
-                const finalTf = moveTf.copy();
-                prevMoveTf && finalTf.multiply(prevMoveTf.invert());
-                finalTf.multiply(rep.tempShape.getTransform());
-                prevMoveTf = moveTf;
-
-                setShapeTransform(rep.tempShape, finalTf);
-                transformer.fire("transform");
-                // updateTransform()
-              } else {
-                prevMoveTf = null;
-                transform.value = void 0;
-              }
-            },
-            { immediate: true }
-          );
+          rep.update && rep.update();
+          mode.add(Mode.draging);
+          transformIngShapes.value.push($shape);
+        };
 
-          onCleanup(() => {
-            for (const key in oldConfig) {
-              (transformer as any)[key](oldConfig[key]);
-            }
-            stop();
-            stopDragWatch();
-            // parent.add($shape);
-            // TODO: 有可能transformer已经转移
-            if (transformer.queueShapes.value.includes($shape)) {
-              transformer.nodes([]);
-              transformer.queueShapes.value = [];
-              // transformer.remove();
-            }
-            transform.value = void 0;
-            rep.destory();
+        let isEnter = false;
+        transformer.on("pointerdown.shapemer", downHandler);
+        transformer.on("transform.shapemer", updateTransform);
+        const stop = listener(
+          $shape.getStage()!.container(),
+          "pointerup",
+          () => {
             if (isEnter) {
               mode.pop();
+              transform.value = void 0;
+              isEnter = false;
               const ndx = transformIngShapes.value.indexOf($shape);
               ~ndx && transformIngShapes.value.splice(ndx, 1);
             }
-            transformer.off("pointerdown.shapemer", downHandler);
-            transformer.off("transform.shapemer", updateTransform);
-          });
-        },
-        { immediate: true }
-      )
+          }
+        );
+
+        // 拖拽时要更新矩阵
+        let prevMoveTf: Transform | null = null;
+        const stopDragWatch = watch(
+          offset,
+          (translate, oldTranslate) => {
+            if (translate) {
+              if (!oldTranslate) {
+                rep.update && rep.update();
+              }
+              const moveTf = new Transform().translate(
+                translate.x,
+                translate.y
+              );
+              const finalTf = moveTf.copy();
+              prevMoveTf && finalTf.multiply(prevMoveTf.invert());
+              finalTf.multiply(rep.tempShape.getTransform());
+              prevMoveTf = moveTf;
+
+              setShapeTransform(rep.tempShape, finalTf);
+              transformer.fire("transform");
+              // updateTransform()
+            } else {
+              prevMoveTf = null;
+              transform.value = void 0;
+            }
+          },
+          { immediate: true }
+        );
+
+        const stopTransformerForceUpdate = watch(
+          viewTransform,
+          () => transformer.forceUpdate(),
+          { flush: "post" }
+        );
+
+        onCleanup(() => {
+          for (const key in oldConfig) {
+            (transformer as any)[key](oldConfig[key]);
+          }
+          stopTransformerForceUpdate();
+          stop();
+          stopDragWatch();
+          // parent.add($shape);
+          // TODO: 有可能transformer已经转移
+          if (transformer.queueShapes.value.includes($shape)) {
+            transformer.nodes([]);
+            transformer.queueShapes.value = [];
+            // transformer.remove();
+          }
+          transform.value = void 0;
+          rep.destory();
+          if (isEnter) {
+            mode.pop();
+            const ndx = transformIngShapes.value.indexOf($shape);
+            ~ndx && transformIngShapes.value.splice(ndx, 1);
+          }
+          transformer.off("pointerdown.shapemer", downHandler);
+          transformer.off("transform.shapemer", updateTransform);
+        });
+      },
+      { immediate: true }
     );
-  watch(shape, (shape, _, onCleanup) => {
-    if (shape) {
-      const stop = init(shape.getStage());
-      onCleanup(stop);
-    } else {
-      onCleanup(() => {});
+  watch(
+    () => shape.value,
+    (shape, _) => {
+      if (!shape) return;
+      watch(
+        () => can.editMode || mode.include(Mode.update),
+        (canEdit, _, onCleanup) => {
+          if (canEdit) {
+            const stop = init(shape.getStage());
+            onCleanup(stop);
+          } else {
+            onCleanup(() => {});
+          }
+        },
+        {immediate: true}
+      );
     }
-  });
+  );
   return transform;
 };
 
@@ -387,8 +419,8 @@ export const cloneRepShape = <T extends EntityShape>(
       strokeWidth: 0,
     }),
     update: (_, rep) => {
-      setShapeTransform(rep, shape.getTransform())
-    }
+      setShapeTransform(rep, shape.getTransform());
+    },
   };
 };
 
@@ -535,7 +567,7 @@ export const useLineTransformer = <T extends LineTransformerData>(
         const inverMat = attitude.copy().invert();
         setShapeTransform(repShape, attitude);
         const initVs = data.points.map((v) => inverMat.point(v));
-        stableVs = tempVs = data.points
+        stableVs = tempVs = data.points;
         repShape.points(flatPositions(initVs));
         repShape.closed(true);
         inverAttitude = inverMat;
@@ -566,4 +598,4 @@ export const useMatCompTransformer = <T extends BaseItem & { mat: number[] }>(
     callback,
     openSnap: true,
   });
-};
+};

+ 18 - 9
src/core/hook/use-viewer.ts

@@ -1,9 +1,11 @@
 import { Viewer } from "../viewer.ts";
 import { computed, ref, watch } from "vue";
-import { dragListener, scaleListener } from "../../utils/event.ts";
+import { dragListener, listener, scaleListener } from "../../utils/event.ts";
 import {
   globalWatch,
   installGlobalVar,
+  useCan,
+  useDownKeys,
   useMode,
   useStage,
 } from "./use-global-vars.ts";
@@ -15,15 +17,22 @@ import { lineLen } from "@/utils/math.ts";
 export const useViewer = installGlobalVar(() => {
   const stage = useStage();
   const viewer = new Viewer();
-  const interactive = useMode();
+  const can = useCan()
   const transform = ref(new Transform());
 
   const init = (dom: HTMLDivElement) => {
-    const dragDestroy = dragListener(dom, ({ end, prev }) => {
-      viewer.movePixel({ x: end.x - prev.x, y: end.y - prev.y });
+    const dragDestroy = dragListener(dom, {
+      move: ({ end, prev }) => {
+        if (can.viewMode) {
+          viewer.movePixel({ x: end.x - prev.x, y: end.y - prev.y }); 
+        }
+      },
+      notPrevent: true,
     });
     const scaleDestroy = scaleListener(dom, (info) => {
-      viewer.scalePixel(info.center, info.scale);
+      if (can.viewMode) {
+        viewer.scalePixel(info.center, info.scale);
+      }
     });
     viewer.bus.on("transformChange", (newTransform) => {
       transform.value = newTransform;
@@ -38,7 +47,7 @@ export const useViewer = installGlobalVar(() => {
       viewer,
     },
     onDestroy: globalWatch(
-      () => stage.value && interactive.value === Mode.viewer,
+      () =>  can.viewMouseReact,
       (can, _, onCleanup) => {
         if (can) {
           const dom = stage.value!.getNode().container();
@@ -88,10 +97,10 @@ export const useUnitTransform = installGlobalVar(() => {
       );
     },
   };
-}, Symbol('unitTransform'));
+}, Symbol("unitTransform"));
 
 export const useCacheUnitTransform = installGlobalVar(() => {
-	const unitTransform = useUnitTransform()
+  const unitTransform = useUnitTransform();
   const transform = useViewerTransform();
   const invTransform = useViewerInvertTransform();
   let pixelCache: Record<string, number> = {};
@@ -119,4 +128,4 @@ export const useCacheUnitTransform = installGlobalVar(() => {
       }
     },
   };
-}, Symbol('cacheUnitTransform'));
+}, Symbol("cacheUnitTransform"));

+ 13 - 106
src/core/renderer/draw-group.vue

@@ -1,116 +1,23 @@
 <template>
-  <ShapeComponent :data="item" v-for="item in tempItems" :key="item" addMode />
+  <ShapeComponent
+    :data="(item as any)"
+    v-for="item in tempItems"
+    :key="item.id"
+    addMode
+  />
 </template>
 
 <script setup lang="ts">
-import { ShapeType, DrawData, components } from "../components";
-import { ref, reactive, watch } from "vue";
-import {
-  Interactive,
-  InteractiveAction,
-  useInteractiveAreas,
-  useInteractiveDots,
-} from "../hook/use-interactive.ts";
-import { mergeFuns } from "@/utils/shared.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 type = props.type as "arrow";
-const obj = components[type];
-const ShapeComponent =
-  (components[type] as any).TempComponent || components[type].Component;
-const tempItems = ref<Required<DrawData>[typeof type]>([]);
-const single = ["dots"].includes(obj.addMode);
-
-let gInteractive: Interactive;
-if (obj.addMode === "area") {
-  const interactive = useInteractiveAreas({ shapeType: type });
-  // 每次拽结束都加组件
-  watch(
-    () => interactive.messages,
-    (areas) => {
-      if (areas.length === 0) return;
-      for (const area of areas) {
-        let item = obj.interactiveToData({ area }, interactive.preset);
-        if (!item) continue;
-        item = reactive(item);
-        tempItems.value.push(item);
-
-        if (interactive.singleDone.value) continue;
-        const stop = mergeFuns(
-          watch(area, () => obj.interactiveFixData(item, { area }), { deep: true }),
-          watch(
-            () => [interactive.singleDone.value, interactive.isRunning.value],
-            () => stop()
-          )
-        );
-      }
-      interactive.consume(areas);
-    },
-    { immediate: true }
-  );
-  gInteractive = interactive;
-} else {
-  // 多点确定组件,
-  const interactive = useInteractiveDots({ shapeType: type });
-  let item: any;
-  watch(
-    () => interactive.messages,
-    (dots, _) => {
-      if (dots.length === 0) return;
-      for (const dot of dots) {
-        const ndx = interactive.getNdx(dot);
-        if (!item) {
-          item = obj.interactiveToData({ dot: dots[0], ndx }, interactive.preset);
-          if (!item) continue;
-          item = reactive(item);
-          tempItems.value.push(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 (!single) {
-                item = null;
-              }
-              stop();
-            }
-          )
-        );
-      }
-      interactive.consume(dots);
-    }
-  );
-  gInteractive = interactive;
-}
-
-if (gInteractive!) {
-  watch(
-    gInteractive!.isRunning,
-    (isRunning) => {
-      if (isRunning || tempItems.value.length === 0) return;
-      console.log(gInteractive.singleDone.value);
-      if (!gInteractive.singleDone.value) {
-        if (single) {
-          obj.interactiveFixData(tempItems.value[0], {
-            action: InteractiveAction.delete,
-          });
-        } else {
-          tempItems.value.pop();
-        }
-      }
-
-      // 消费结束,发送添加完毕数据,未消费的则取
-      store.addItems(props.type, tempItems.value);
-      tempItems.value = [];
-    },
-    { flush: "pre" }
-  );
-}
+const type = props.type;
+const ShapeComponent = components[type].TempComponent || components[type].Component;
 </script>

+ 2 - 2
src/core/renderer/renderer.vue

@@ -34,11 +34,11 @@ import { 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";
-import { useInteractiveShapeAPI } from "../hook/use-interactive.ts";
 import { DomMountId } from "../../constant";
 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";
 
 const props = defineProps<{
   data: DrawData;
@@ -52,7 +52,7 @@ const viewerConfig = useViewerTransformConfig();
 const types = Object.keys(components) as ShapeType[];
 
 // 退出添加模式
-const { quitMouseAddShape } = useInteractiveShapeAPI();
+const { quitMouseAddShape } = useInteractiveAddShapeAPI();
 useListener(
   "contextmenu",
   (ev) => {

+ 4 - 1
src/core/store/index.ts

@@ -78,7 +78,10 @@ const useStoreAndHistory = installGlobalVar(() => {
   const store = useStoreRaw();
 	const history = new DrawHistory((dataStr) => {
 		const data = JSON.parse(dataStr) as DrawData
-		store.data = data
+		store.$patch(state => {
+			// console.error('change', state)
+			state.data = data
+		})
 	})
 	
   const trackActions = ["setStore", "repStore", "addItem", "delItem", "setItem"];

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

@@ -58,8 +58,8 @@
       </div>
     </div>
     <div class="saves">
-      <el-button type="primary" plain>保存</el-button>
-      <el-button type="primary" plain>导出</el-button>
+      <el-button type="primary" @click="emit('expose')" plain>导出</el-button>
+      <el-button type="primary" @click="emit('save')" plain>保存</el-button>
       <el-button>图纸</el-button>
     </div>
   </div>
@@ -77,7 +77,7 @@ const draw = useDraw();
 const bgFileInput = ref<HTMLInputElement | null>(null);
 const dev = import.meta.env.DEV;
 
-const emit = defineEmits<{ (e: "full"): void }>();
+const emit = defineEmits<{ (e: "full"): void; (e: "save"): void; (e: "expose"): void }>();
 
 const setBGImage = (file: File) => {
   draw.addShape(

+ 7 - 3
src/example/fuse/views/home.vue

@@ -1,13 +1,13 @@
 <template>
   <div class="layout" :class="{ full }">
-    <Header class="header" v-if="draw" @full="fullHandler" />
+    <Header class="header" v-if="draw" @full="fullHandler" @save="saveHandler" />
     <div class="container">
       <Slide class="slide" v-if="draw" />
       <div class="content" ref="drawEle">
         <DrawBoard
           v-if="drawEle"
           :ref="(e: any) => draw = e.draw"
-          :data="(initData as any)"
+          :data="(data as any)"
         />
       </div>
     </div>
@@ -19,7 +19,7 @@ import Header from "./header/header.vue";
 import Slide from "./slide/slide.vue";
 import { onUnmounted, ref, watch, watchEffect } from "vue";
 import { DrawExpose, DrawBoard } from "@/index";
-import { initData } from "./init.ts";
+import { data, save } from "./init.ts";
 import { installDraw } from "./use-draw.ts";
 import { listener } from "@/utils/event.ts";
 import { ElMessage } from "element-plus";
@@ -29,6 +29,10 @@ const drawEle = ref<HTMLDivElement | null>(null);
 const draw = ref<DrawExpose>();
 installDraw(draw);
 
+const saveHandler = () => {
+  save(draw.value?.getData());
+};
+
 const full = ref(false);
 watch(full, (_f1, _f2, onCleanup) => {
   onCleanup(

+ 10 - 1
src/example/fuse/views/init.ts

@@ -1,4 +1,4 @@
-export const initData = {
+const initData = {
   image: [
     {
       id: "-10",
@@ -46,6 +46,7 @@ export const initData = {
   ],
   triangle: [
     {
+      ref: false,
       createTime: 3,
       zIndex: 0,
       id: "3",
@@ -222,3 +223,11 @@ export const initData = {
     },
   ],
 };
+
+const dataStr = localStorage.getItem("draw-data");
+export const data = dataStr ? JSON.parse(dataStr) : initData;
+
+export const save = (data: any) => {
+  console.log(data)
+  localStorage.setItem("draw-data", JSON.stringify(data));
+};

+ 4 - 3
src/utils/event.ts

@@ -37,6 +37,7 @@ type DragProps = {
 	move?: (info: Record<'start' | 'prev' | 'end', Pos> & {ev: PointerEvent}) => void,
 	down?: (pos: Pos, ev: PointerEvent) => void,
 	up?: (pos: Pos, ev: PointerEvent) => void,
+	notPrevent?: boolean
 }
 export const dragListener = (dom: HTMLElement, props: DragProps | DragProps['move'] = {}) => {
 	if (typeof props === 'function') {
@@ -52,20 +53,20 @@ export const dragListener = (dom: HTMLElement, props: DragProps | DragProps['mov
 		const start = getOffset(ev, dom)
 		let prev = start
 		down && down(start, ev)
-		ev.preventDefault();
+		props.notPrevent || ev.preventDefault();
 
 		moveHandler = (ev: PointerEvent) => {
 			const end = getOffset(ev, dom)
 			move!({start, end, prev, ev})
 			prev = end
 
-			ev.preventDefault();
+			props.notPrevent || ev.preventDefault();
 		}
 		endHandler = (ev: PointerEvent) => {
 			up && up(getOffset(ev, dom), ev)
 			mount.removeEventListener('pointermove', moveHandler);
 			mount.removeEventListener('pointerup', endHandler);
-			ev.preventDefault();
+			props.notPrevent || ev.preventDefault();
 		}
 	
 		

+ 0 - 1
src/utils/shared.ts

@@ -57,7 +57,6 @@ export const toRawType = (value: unknown): string =>
 // 是否修改
 const _inRevise = (raw1: any, raw2: any, readly: Set<[any, any]>): boolean => {
   if (raw1 === raw2) return false;
-
   const rawType1 = toRawType(raw1);
   const rawType2 = toRawType(raw2);
 

+ 1 - 1
vite.config.ts

@@ -31,7 +31,7 @@ export default ({ mode }: any) => {
       },
     },
     server: {
-      port: 9000,
+      port: 9010,
       open: true,
       host: "0.0.0.0",
     },