Przeglądaj źródła

feat: 制作分组功能

bill 5 miesięcy temu
rodzic
commit
31f3667d65

+ 1 - 2
src/constant/index.ts

@@ -1,6 +1,5 @@
-import { reactive } from "vue"
-
 export const DomMountId =  'dom-mount'
 export const DomOutMountId =  'dom-out-mount'
+export const DataGroupId = 'data-group'
 export const rendererMap = new WeakMap<any, { unmounteds: (() => void)[] }>()
 export const rendererName = 'draw-renderer'

+ 92 - 33
src/core/components/group/group.vue

@@ -1,34 +1,34 @@
 <template>
-  <TempGroup
-    :data="{ ...tData, listening: status.active }"
-    :ref="(e: any) => shape = e?.shape"
-    :pixel="pixel"
-    @pointerdown="cancelBubbleEvent"
-    @pointerup="cancelBubbleEvent"
-    @pointermove="cancelBubbleEvent"
-  />
-  <!-- <Operate :target="shape" :menus="operateMenus" /> -->
+  <template v-if="show">
+    <TempGroup
+      :data="{ ...tData, listening }"
+      :ref="(e: any) => {shape = e?.shape; getGroupShapes = e?.getGroupShapes}"
+      :autoUpdate="autoUpdate"
+    />
+    <Operate :target="shape" :menus="operateMenus" />
+  </template>
 </template>
 
 <script lang="ts" setup>
 import TempGroup from "./temp-group.vue";
+import { Operate } from "../../propertys";
 import { GroupData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
-import { cancelBubbleEvent } from "@/core/hook/use-event.ts";
-import {
-  useMouseShapesStatus,
-  useMouseShapeStatus,
-} from "@/core/hook/use-mouse-status.ts";
+import { useMouseShapeStatus } from "@/core/hook/use-mouse-status.ts";
 import { useCustomTransformer } from "@/core/hook/use-transformer.ts";
 import { Rect } from "konva/lib/shapes/Rect";
 import { setShapeTransform } from "@/utils/shape.ts";
-import { useStore } from "../../store/index.ts";
+import { DrawStoreBusArgs, useStore } from "../../store/index.ts";
 import { Transform } from "konva/lib/Util";
 import { useHistory } from "@/core/hook/use-history.ts";
-import { useFormalLayer } from "@/core/hook/use-layer.ts";
-import { nextTick } from "vue";
+import { computed, nextTick, onUnmounted, ref, shallowRef, watchEffect } from "vue";
+import { useOperMode } from "@/core/hook/use-status.ts";
+import { EntityShape } from "@/deconstruction.js";
+import { Top } from "@element-plus/icons-vue";
+import { getBaseItem } from "../util.ts";
+import { useForciblyShowItemIds } from "@/core/hook/use-global-vars.ts";
 
-const props = defineProps<{ data: GroupData; pixel?: boolean }>();
+const props = defineProps<{ data: GroupData }>();
 const emit = defineEmits<{
   (e: "updateShape", value: GroupData): void;
   (e: "addShape", value: GroupData): void;
@@ -37,11 +37,13 @@ const emit = defineEmits<{
 
 const store = useStore();
 const history = useHistory();
-const layer = useFormalLayer();
-const { shape, tData } = useComponentStatus<Rect, GroupData>({
+const autoUpdate = ref(true);
+const { shape, tData, data } = useComponentStatus<Rect, GroupData>({
   emit,
   props,
   getMouseStyle,
+  noOperateMenus: true,
+  noJoinZindex: true,
   transformType: "custom",
   customTransform(callback, shape, data) {
     let prevMat: Transform;
@@ -50,12 +52,10 @@ const { shape, tData } = useComponentStatus<Rect, GroupData>({
       openSnap: false,
       getRepShape($shape) {
         const repShape = new Rect({ fill: "red", opacity: 0.3 });
-        prevMat = $shape.getTransform().copy();
-        history.bus.on("currentChange", function updateCurrent() {
-          shapesStatus.selects = [];
-          history.bus.off("currentChange", updateCurrent);
-        });
-
+        const syncShape = () =>
+          autoUpdate.value && (prevMat = $shape.getTransform().copy());
+        $shape.on("bound-change", syncShape);
+        syncShape();
         const update = () => {
           repShape.x($shape.x());
           repShape.y($shape.y());
@@ -67,26 +67,27 @@ const { shape, tData } = useComponentStatus<Rect, GroupData>({
         update();
         return {
           shape: repShape,
-          // update,
+          update,
+          destory() {
+            $shape.off("bound-change", syncShape);
+          },
         };
       },
       handler(data, mat) {
         setShapeTransform(shape.value!.getNode(), mat);
         matResponse({ data, mat, store }, prevMat);
         prevMat = mat;
-        for (const id of data.ids) {
-          nextTick(() => {
-            layer.value?.findOne(`#${id}`)?.fire("bound-change");
-          });
-        }
+        getGroupShapes!().forEach((shape) => nextTick(() => shape.fire("bound-change")));
         return true;
       },
       start() {
+        autoUpdate.value = false;
         history.onceTrack(
           () => new Promise<void>((resolve) => (transformResolve = resolve))
         );
       },
       callback() {
+        autoUpdate.value = true;
         transformResolve();
         callback();
       },
@@ -96,6 +97,64 @@ const { shape, tData } = useComponentStatus<Rect, GroupData>({
   propertys: [],
 });
 
+let getGroupShapes: (() => EntityShape[]) | undefined;
 const status = useMouseShapeStatus(shape);
-const shapesStatus = useMouseShapesStatus();
+const operMode = useOperMode();
+const listening = computed(() => status.value.active || operMode.value.group);
+const showItemIds = useForciblyShowItemIds();
+const show = computed(
+  () =>
+    listening.value ||
+    operMode.value.mulSelection ||
+    status.value.select ||
+    showItemIds.has(data.value.id)
+);
+
+const delWatch = ([_, id]: DrawStoreBusArgs["delItemBefore"]) => {
+  if (!data.value.ids.includes(id)) return;
+  const nIds = data.value.ids.filter((dId) => dId !== id);
+  if (nIds.length === 0) {
+    emit("delShape");
+  } else {
+    emit("updateShape", { ...data.value, ids: nIds });
+  }
+};
+store.bus.on("delItemBefore", delWatch);
+onUnmounted(() => store.bus.off("delItemBefore", delWatch));
+
+const operateMenus = shallowRef<
+  Array<{
+    icon?: any;
+    label?: string;
+    handler: () => void;
+  }>
+>([]);
+watchEffect(() => {
+  const item = store.getItemById(props.data.id);
+  if (item) {
+    operateMenus.value = [
+      {
+        label: `解除分组`,
+        icon: Top,
+        handler() {
+          emit("delShape");
+        },
+      },
+    ];
+  } else {
+    operateMenus.value = [
+      {
+        label: `添加分组`,
+        icon: Top,
+        handler() {
+          emit("addShape", {
+            ...props.data,
+            ...getBaseItem(),
+            stroke: undefined,
+          });
+        },
+      },
+    ];
+  }
+});
 </script>

+ 21 - 11
src/core/components/group/index.ts

@@ -1,17 +1,17 @@
-import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { DrawItem, InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { Transform } from "konva/lib/Util";
 import { components } from "../index";
+import { normalSelectItems } from "@/core/hook/use-selection.ts";
 
 export { default as Component } from "./group.vue";
 export { default as TempComponent } from "./temp-group.vue";
 
 export const shapeName = "分组";
 export const defaultStyle = {
-  stroke: themeMouseColors.theme,
-  strokeWidth: 4,
+  stroke: '#cccccc',
+  strokeWidth: 2,
   opacity: 0.8,
   dash: [10, 10],
 };
@@ -23,9 +23,10 @@ export const getMouseStyle = (data: GroupData) => {
   return {
     default: {
       stroke: strokeStatus.pub,
+      dash: data.dash || defaultStyle.dash
     },
     hover: { stroke: strokeStatus.hover },
-    select: { stroke: strokeStatus.select },
+    select: { stroke: strokeStatus.select, dash: [10, 0], },
     press: { stroke: strokeStatus.press },
   };
 };
@@ -65,16 +66,25 @@ export const interactiveFixData: InteractiveFix<"group"> = ({ data }) => {
 
 
 export const matResponse = ({data, mat, store}: MatResponseProps<'group'>, prevMat?: Transform) => {
-  if (!store) return;
-
+  if (!store) {
+    return;
+  }
   const incMat = prevMat ? mat.copy().multiply(prevMat.invert()) : mat;
   prevMat = mat;
+
+  const items: DrawItem[] = []
   for (const id of data.ids) {
-    const type = store.getType(id);
-    if (!type) continue;
-    const item = store.getItem(type, id);
+    const item = store.getItemById(id);
     if (!item) continue;
-    components[type].matResponse({ data: item as any, mat: incMat, increment: true });
+    items.push(item)
+  }
+  let fitems = normalSelectItems(store, items, false)
+
+  fitems = fitems.length ? fitems : items
+  for (let i = 0; i < fitems.length; i++) {
+    const type = store.getType(fitems[i].id)!
+    const item = fitems[i]
+    components[type].matResponse({ data: item as any, mat: incMat, increment: true, store });
     store.setItem(type, { value: item, id: item.id });
   }
 }

+ 59 - 20
src/core/components/group/temp-group.vue

@@ -1,39 +1,53 @@
 <template>
-  <v-rect
-    :config="{ ...data, ...rectConfig, zIndex: undefined, strokeScaleEnable: true }"
-    ref="shape"
-  />
+  <v-rect :config="{ ...data, zIndex: undefined, strokeScaleEnable: true }" ref="shape" />
 </template>
 
 <script lang="ts" setup>
 import { defaultStyle, GroupData } from "./index.ts";
 import { computed, nextTick, ref, watch } from "vue";
-import { DC } from "@/deconstruction.js";
+import { DC, EntityShape } from "@/deconstruction.js";
 import { Rect, RectConfig } from "konva/lib/shapes/Rect";
 import { useViewerInvertTransform } from "@/core/hook/use-viewer.ts";
-import { useFormalLayer } from "@/core/hook/use-layer.ts";
 import { useDashAnimation } from "@/core/hook/use-animation.ts";
+import { useStage } from "@/core/hook/use-global-vars.ts";
+import { useOnComponentBoundChange } from "@/core/hook/use-component.ts";
+import { debounce } from "@/utils/shared.ts";
 
-const props = defineProps<{ data: GroupData; addMode?: boolean; pixel?: boolean }>();
+const props = defineProps<{
+  data: GroupData;
+  addMode?: boolean;
+  autoUpdate?: boolean;
+}>();
 const shape = ref<DC<Rect>>();
 useDashAnimation(shape);
 const data = computed(() => ({ ...defaultStyle, ...props.data }));
 
+const stage = useStage();
+const getGroupShapes = () => {
+  const $stage = stage.value?.getNode();
+  if (!$stage) return;
+  const shapes: EntityShape[] = [];
+  for (const id of data.value.ids) {
+    const $shape = $stage!.findOne(`#${id}`)!;
+    if (!$shape) return;
+    shapes.push($shape as EntityShape);
+  }
+  return shapes;
+};
+
 const invMat = useViewerInvertTransform();
-const rectConfig = ref<RectConfig>();
-const formatLayer = useFormalLayer();
 
 const updateBound = () => {
-  if (!data.value.ids.length || !formatLayer.value) {
-    rectConfig.value = undefined;
+  const shapes = $shapes.value;
+  if (!shapes?.length) {
     return;
   }
   let lx = Number.MAX_VALUE;
   let ly = Number.MAX_VALUE;
   let rx = Number.MIN_VALUE;
   let ry = Number.MIN_VALUE;
-  for (const id of data.value.ids) {
-    const shape = formatLayer.value.findOne(`#${id}`);
+
+  for (const shape of shapes) {
     if (!shape) continue;
 
     const rect = shape.getClientRect();
@@ -50,34 +64,59 @@ const updateBound = () => {
       ry = rect.y + rect.height;
     }
   }
+
   const pixelStart = invMat.value.point({ x: lx, y: ly });
   const pixelEnd = invMat.value.point({ x: rx, y: ry });
   lx = pixelStart.x;
   ly = pixelStart.y;
   rx = pixelEnd.x;
   ry = pixelEnd.y;
-  rectConfig.value = {
+  const config: RectConfig = {
     x: lx,
     y: ly,
     width: rx - lx,
     height: ry - ly,
+    rotation: 0,
+    scaleX: 1,
+    scaleY: 1,
   };
-  const $shape = shape.value?.getNode();
+  const $shape = shape.value?.getNode() as any;
   if ($shape) {
-    $shape.rotation(0);
-    $shape.scaleX(1);
-    $shape.scaleY(1);
+    for (const key in config) {
+      $shape[key](config[key]);
+    }
     nextTick(() => $shape.fire("bound-change"));
   }
 };
 
-watch(() => data.value.ids.length, updateBound, { immediate: true });
-console.log(data.value.ids.length);
+watch(
+  () => data.value.ids.length,
+  () => nextTick(updateBound),
+  { immediate: true }
+);
+
+const { on } = useOnComponentBoundChange();
+const $shapes = computed(getGroupShapes);
+const syncUpdateBound = debounce(updateBound, 0);
+watch(
+  () =>
+    data.value.ids.join(",") +
+    (props.autoUpdate ? "1" : "0") +
+    (data.value.listening ? "1" : "0") +
+    (stage.value ? "1" : "0"),
+  (_a, _b, onCleanup) => {
+    if (props.autoUpdate) {
+      onCleanup(on($shapes, () => props.autoUpdate && syncUpdateBound(), false));
+    }
+  },
+  { immediate: true }
+);
 
 defineExpose({
   get shape() {
     return shape.value;
   },
   updateBound,
+  getGroupShapes,
 });
 </script>

+ 2 - 0
src/core/components/icon/temp-icon.vue

@@ -83,6 +83,8 @@ const groupConfig = computed(() => {
 
   return {
     ...mat.decompose(),
+    zIndex: undefined,
+    listening: data.value.listening,
     id: data.value.id,
     opacity: props.addMode ? 0.3 : 1,
   };

+ 1 - 0
src/core/components/image/image.vue

@@ -39,5 +39,6 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
     "opacity",
     //  "ref", "zIndex"
   ],
+  debug: true
 });
 </script>

+ 2 - 0
src/core/components/image/temp-image.vue

@@ -4,6 +4,7 @@
       :config="{
         ...data,
         ...config,
+        id: undefined,
         zIndex: undefined,
       }"
       v-if="image"
@@ -69,6 +70,7 @@ const config = computed(() => {
 
 const groupConfig = computed(() => {
   return {
+    id: data.value.id,
     ...new Transform(data.value.mat).decompose(),
   };
 });

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

@@ -91,6 +91,6 @@ export const matResponse = ({data, mat, increment}: MatResponseProps<'serial'>,
   data.fontSize = getSerialFontSizeByFontW(data, w);
   data.mat = new Transform()
     .translate(dec.x, dec.y)
-    .rotate(MathUtils.degToRad(dec.rotation)).m;
+    .rotate(0).m;
   return data;
 }

+ 4 - 3
src/core/components/text/text.vue

@@ -31,6 +31,7 @@ import { Transform } from "konva/lib/Util";
 import { Text } from "konva/lib/shapes/Text";
 import { watch } from "vue";
 import { setShapeTransform } from "@/utils/shape.ts";
+import { copy } from "@/utils/shared.ts";
 
 const props = defineProps<{ data: TextData }>();
 const emit = defineEmits<{
@@ -53,12 +54,12 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
       openSnap: true,
       getRepShape($shape) {
         const repShape = cloneRepShape($shape).shape;
+        setShapeTransform(repShape, new Transform(data.value.mat));
         return {
           shape: repShape,
           update(data) {
             data.width && repShape.width(data.width);
             data.fontSize && repShape.fontSize(data.fontSize);
-            setShapeTransform(repShape, new Transform(data.mat));
           },
         };
       },
@@ -73,10 +74,10 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
         },
       },
       beforeHandler(data, mat) {
-        return matResponse({ data, mat });
+        return matResponse({ data: copy(data), mat });
       },
       handler(data, mat) {
-        const a = matResponse({ mat, data: { ...data } });
+        const a = matResponse({ mat, data });
         if (a.width) {
           return true;
         }

+ 1 - 7
src/core/helper/back-grid.vue

@@ -1,5 +1,5 @@
 <template>
-  <v-group ref="grid" :config="{ listening: false }">
+  <v-group :config="{ listening: false }">
     <v-line
       v-if="hConfig"
       :config="{
@@ -23,20 +23,14 @@
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
 import { useResize } from "../hook/use-event";
-import { useShapeStaticZindex } from "../hook/use-layer";
 import {
   useViewerInvertTransform,
   useViewerTransform,
   useViewerTransformConfig,
 } from "../hook/use-viewer";
-import { DC } from "@/deconstruction";
-import { Rect } from "konva/lib/shapes/Rect";
 import { lineLen } from "@/utils/math";
 import { debounce } from "@/utils/shared";
 
-const grid = ref<DC<Rect>>();
-useShapeStaticZindex(grid);
-
 const style = {
   stroke: "#ccc",
   strokeWidth: 3,

+ 1 - 7
src/core/helper/back.vue

@@ -1,17 +1,11 @@
 <template>
-  <v-rect :config="config" ref="shape" />
+  <v-rect :config="config" />
 </template>
 <script lang="ts" setup>
 import { computed, ref } from "vue";
-import { useShapeStaticZindex } from "../hook/use-layer";
 import { useViewerInvertTransformConfig } from "../hook/use-viewer";
-import { DC } from "@/deconstruction";
 import { useExpose } from "../hook/use-expose";
 import { useResize } from "../hook/use-event";
-import { Rect } from "konva/lib/shapes/Rect";
-
-const shape = ref<DC<Rect>>();
-useShapeStaticZindex(shape);
 
 const invConfig = useViewerInvertTransformConfig();
 const size = useResize();

+ 57 - 36
src/core/hook/use-component.ts

@@ -5,6 +5,7 @@ import {
   isRef,
   markRaw,
   nextTick,
+  onMounted,
   onUnmounted,
   reactive,
   Ref,
@@ -176,6 +177,9 @@ export type UseComponentStatusProps<
   getMouseStyle: any;
   defaultStyle: any;
   propertys: PropertyKeys;
+  debug?: boolean
+  noJoinZindex?: boolean;
+  noOperateMenus?: boolean;
   transformType?: "line" | "mat" | "custom";
   customTransform?: (
     callback: () => void,
@@ -231,7 +235,9 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
       data
     );
   }
-  useZIndex(shape, data);
+  if (!args.noJoinZindex) {
+    useZIndex(shape, data);
+  }
 
   return {
     data,
@@ -244,13 +250,15 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
       return tData;
     }),
     shape,
-    operateMenus: useComponentMenus(
-      shape,
-      data,
-      args.emit,
-      args.alignment,
-      args.copyHandler
-    ),
+    operateMenus: args.noOperateMenus
+      ? []
+      : useComponentMenus(
+          shape,
+          data,
+          args.emit,
+          args.alignment,
+          args.copyHandler
+        ),
     describes: mergeDescribes(data, args.defaultStyle, args.propertys || []),
   };
 };
@@ -319,46 +327,59 @@ export const useOnComponentBoundChange = () => {
   const destory = () => mergeFuns(quitHooks)();
 
   const on = <T extends EntityShape>(
-    shape: Ref<T | undefined> | T | undefined,
-    callback: (shape: T, type: "transform" | "data") => void
+    shapes: Ref<T | T[] | undefined> | T | T[] | undefined,
+    callback: (shape: T, type: "transform" | "data") => void,
+    viewListener = true
   ) => {
-    const $shape = computed(() => (shape = isRef(shape) ? shape.value : shape));
-    let repShape: T | undefined;
-    const item = getComponentData($shape);
-    const update = (type?: "transform" | "data") => {
-      $shape.value &&
-        !api.isPause &&
-        callback(repShape || $shape.value, type || "data");
+    const $shapes = computed(() => {
+      shapes = isRef(shapes) ? shapes.value : shapes;
+      if (!shapes) return [];
+      return Array.isArray(shapes) ? shapes : [shapes];
+    });
+    const update = ($shape: T, type?: "transform" | "data") => {
+      if (api.isPause) return;
+      callback($shape, type || "data");
     };
-    const sync = () => update();
     const shapeListener = (shape: T) => {
-      repShape = (shape.repShape as T) || shape;
-      repShape.on("transform", sync);
-      shape.on("bound-change", sync);
+      const repShape = (shape.repShape as T) || shape;
+      const syncBd = () => update(repShape);
+      repShape.on("transform", syncBd);
+      shape.on("bound-change", () => syncBd);
       return () => {
-        repShape!.off("transform", sync);
-        shape.off("bound-change", sync);
+        repShape.off("transform", syncBd);
+        shape.off("bound-change", () => syncBd);
       };
     };
+    const cleanups: (() => void)[] = []
+    if (viewListener) {
+      cleanups.push(watch(transform, () =>
+        $shapes.value.forEach(($shape) => update($shape, "transform"))
+      ))
+    }
+    cleanups.push(watch(
+      $shapes,
+      ($shapes, _, onCleanup) => {
+        const cleanups = $shapes.flatMap(($shape) => {
+          const item = getComponentData($shape);
+          return [
+            watch(item, () => nextTick(() => update($shape, "data")), {
+              deep: true,
+            }),
+            shapeListener($shape),
+          ];
+        });
+        onCleanup(mergeFuns(cleanups));
+      },
+      { immediate: true }
+    ))
 
-    const onDestroy = mergeFuns([
-      watch(item, () => nextTick(() => update("data")), { deep: true }),
-      watch(transform, () => update("transform")),
-      watch(
-        $shape,
-        (shape, _, onCleanup) => {
-          if (!shape) return;
-          onCleanup(shapeListener(shape));
-        },
-        { immediate: true }
-      ),
-    ]);
+    const onDestroy = mergeFuns(cleanups);
     quitHooks.push(onDestroy);
 
     return () => {
       const ndx = quitHooks.indexOf(onDestroy);
       ~ndx && quitHooks.splice(ndx, 1);
-      onDestroy;
+      onDestroy();
     };
   };
 

+ 50 - 29
src/core/hook/use-expose.ts

@@ -5,7 +5,7 @@ import {
   useLayers,
   useStage,
 } from "./use-global-vars.ts";
-import { useMode, useOperMode } from './use-status'
+import { useMode, useOperMode } from "./use-status";
 import { Stage } from "konva/lib/Stage";
 import { useInteractiveProps } from "./use-interactive.ts";
 import { useStore } from "../store/index.ts";
@@ -24,6 +24,7 @@ import { isSvgString } from "@/utils/resource.ts";
 import { useResourceHandler } from "./use-fetch.ts";
 import { useConfig } from "./use-config.ts";
 import { useSelectionRevise } from "./use-selection.ts";
+import { useGetFormalChildren } from "./use-layer.ts";
 
 // 自动粘贴服务
 export const useAutoPaste = () => {
@@ -33,6 +34,7 @@ export const useAutoPaste = () => {
   paste.push({
     ["text/plain"]: {
       async handler(pos, val) {
+        console.log(val)
         if (isSvgString(val)) {
           const url = await resourceHandler(val, "svg");
           drawAPI.addShape("icon", { url, stroke: themeColor }, pos, true);
@@ -54,7 +56,7 @@ export const useAutoPaste = () => {
       type: "file",
     },
   });
-}
+};
 
 // 快捷键服务
 export const useShortcutKey = () => {
@@ -63,7 +65,6 @@ export const useShortcutKey = () => {
   useListener(
     "contextmenu",
     (ev) => {
-      console.log('a?')
       if (ev.button === 2) {
         quitDrawShape();
       }
@@ -71,36 +72,57 @@ export const useShortcutKey = () => {
     document.documentElement
   );
 
-  const history = useHistory()
-  const status = useMouseShapesStatus()
-  const store = useStore()
-  useListener('keydown', (ev) => {
-    if (ev.key === 'z' && ev.ctrlKey) {
-      history.hasUndo.value && history.undo();
-    } else if (ev.key === 'y' && ev.ctrlKey) {
-      history.hasRedo.value && history.redo();
-    } else if (ev.key === 's' && ev.ctrlKey) {
-      // 保存
-      history.saveLocal();
-    } else if (ev.key === 'Delete' || ev.key === 'Backspace') {
-      // 删除
-      status.actives.forEach((shape) => {
-        const id = shape.id()
-        const type = id && store.getType(id)
-        if (type) {
-          store.delItem(type, id)
+  const history = useHistory();
+  const status = useMouseShapesStatus();
+  const getChildren = useGetFormalChildren();
+  const store = useStore();
+  const operMode = useOperMode();
+  useListener(
+    "keydown",
+    (ev) => {
+      if (ev.key === "z" && ev.ctrlKey) {
+        history.hasUndo.value && history.undo();
+      } else if (ev.key === "y" && ev.ctrlKey) {
+        history.hasRedo.value && history.redo();
+      } else if (ev.key === "s" && ev.ctrlKey) {
+        // 保存
+        history.saveLocal();
+      } else if (ev.key === "Delete" || ev.key === "Backspace") {
+        // 删除
+        const isSelect = status.selects.length;
+        const shapes = isSelect ? status.selects : status.actives;
+        history.onceTrack(() => {
+          shapes.forEach((shape) => {
+            const id = shape.id();
+            const type = id && store.getType(id);
+            if (type) {
+              store.delItem(type, id);
+            }
+          });
+        });
+        if (isSelect) {
+          status.selects = []
+        } else {
+          status.actives = []
         }
-      });
-    }
-  }, window)
-}
+      } else if (operMode.value.mulSelection && ev.key === "A") {
+        if (status.selects.length) {
+          status.selects = [];
+        } else {
+          status.selects = getChildren();
+        }
+      }
+    },
+    window
+  );
+};
 
 export const useAutoService = () => {
   useAutoPaste();
-  useShortcutKey()
+  useShortcutKey();
   // 鼠标自动变化服务
   const status = useMouseShapesStatus();
-  const operMode = useOperMode()
+  const operMode = useOperMode();
   const mode = useMode();
   const cursor = useCursor();
   const { set: setCursor } = cursor.push("initial");
@@ -163,10 +185,9 @@ export const useAutoService = () => {
     onCleanup(mergeFuns(init(instanceProps.get().id)));
   });
 
-  useSelectionRevise()
+  useSelectionRevise();
 };
 
-
 export type DrawExpose = ReturnType<typeof useExpose>;
 type PickParams<K extends keyof Stage, O extends string> = Stage[K] extends (
   ...args: any

+ 43 - 27
src/core/hook/use-global-vars.ts

@@ -24,41 +24,41 @@ import { StoreData } from "../store/store.ts";
 import { rendererMap, rendererName } from "@/constant/index.ts";
 
 export const useRendererInstance = () => {
-  let instance = getCurrentInstance()!
+  let instance = getCurrentInstance()!;
   while (instance.type.name !== rendererName) {
     if (instance.parent) {
-      instance = instance.parent
+      instance = instance.parent;
     } else {
-      throw '未发现渲染实例'
+      throw "未发现渲染实例";
     }
   }
   return instance;
-}
+};
 
 export const installGlobalVar = <T>(
   create: () => { var: T; onDestroy: () => void } | T,
-  key = Symbol("globalVar"),
+  key = Symbol("globalVar")
 ) => {
   const useGlobalVar = (): T => {
-    const instance = useRendererInstance() as any
-    const { unmounteds } = rendererMap.get(instance)!
+    const instance = useRendererInstance() as any;
+    const { unmounteds } = rendererMap.get(instance)!;
     if (!(key in instance)) {
       let val = create() as any;
       if (typeof val === "object" && "var" in val && "onDestroy" in val) {
-        val.onDestroy && unmounteds.push(val.onDestroy)
+        val.onDestroy && unmounteds.push(val.onDestroy);
         if (import.meta.env.DEV) {
           unmounteds.push(() => {
-            console.log('销毁变量', key)
-          })
+            console.log("销毁变量", key);
+          });
         }
         val = val.var;
       }
-      instance[key] = val
+      instance[key] = val;
     }
     return instance[key];
   };
 
-  return useGlobalVar
+  return useGlobalVar;
 };
 
 export type InstanceProps = {
@@ -93,16 +93,16 @@ export const stackVar = <T>(init?: T) => {
     },
     push(data: T) {
       stack.push(factory(data));
-      const item = stack[stack.length - 1]
+      const item = stack[stack.length - 1];
       const pop = (() => {
         const ndx = stack.findIndex(({ id }) => id === item.id);
         if (~ndx) {
           stack.splice(ndx, 1);
         }
-      }) as (() => void) & {set: (data: T) => void}
+      }) as (() => void) & { set: (data: T) => void };
       pop.set = (data) => {
-        item.var = data
-      }
+        item.var = data;
+      };
       return pop;
     },
     pop() {
@@ -241,21 +241,37 @@ export const useCursor = installGlobalVar(
   Symbol("cursor")
 );
 
-
-export type MPart = {comp: any, props: any}
+export type MPart = { comp: any; props: any };
 export const useMountParts = installGlobalVar(() => {
-  const mParts = reactive<MPart[]>([])
+  const mParts = reactive<MPart[]>([]);
   const del = (part: MPart) => {
-    const ndx = mParts.indexOf(part)
-    ~ndx && mParts.splice(ndx, 1)
-  }
+    const ndx = mParts.indexOf(part);
+    ~ndx && mParts.splice(ndx, 1);
+  };
 
   return {
     value: mParts,
     add(part: MPart) {
-      mParts.push(part)
-      return () => del(part)
+      mParts.push(part);
+      return () => del(part);
     },
-    del
-  }
-})
+    del,
+  };
+});
+
+export const useForciblyShowItemIds = installGlobalVar(
+  () => {
+    const set = new Set() as Set<string> & { cycle: (id: string, fn: () => any) => void }
+    set.cycle = (id, fn) => {
+      set.add(id)
+      const result = fn()
+      if (result instanceof Promise) {
+        result.then(() => set.delete(id))
+      } else {
+        set.delete(id)
+      }
+    }
+    return set
+  },
+  Symbol("forciblyShowItemId")
+);

+ 9 - 20
src/core/hook/use-history.ts

@@ -3,7 +3,6 @@ import { SingleHistory } from "../history";
 import { installGlobalVar } from "./use-global-vars";
 import { Ref, ref, watch } from "vue";
 import { copy } from "@/utils/shared";
-import { getEmptyStoreData, StoreData, useStoreRaw } from "../store/store";
 
 type HistoryItem = { attachs: string; data: string };
 export class DrawHistory {
@@ -23,6 +22,7 @@ export class DrawHistory {
   private preventFlag = 0;
   private onceFlag = 0;
   private onceHistory: string | null = null;
+  private rendererRaw?: (data: string) => void;
   initData: string | null = null;
   clearData: string = ''
   renderer: (data: HistoryItem) => void;
@@ -35,18 +35,22 @@ export class DrawHistory {
     undo: void;
     init: void;
     clear: void;
-    currentChange: void
   }>();
   private pushAttachs: Record<string, any> = {};
 
-  constructor(renderer: (data: string) => void) {
+  constructor(renderer?: (data: string) => void) {
+    this.rendererRaw = renderer
     this.renderer = ({ data, attachs }: HistoryItem) => {
-      renderer(data);
+      this.rendererRaw && this.rendererRaw(data);
       this.bus.emit("renderer");
       this.bus.emit("attachs", attachs ? JSON.parse(attachs) : {});
     };
   }
 
+  setRenderer(renderer: (data: string) => void) {
+    this.rendererRaw = renderer
+  }
+
   setClearData(data:string = '') {
     this.clearData = data
   }
@@ -141,7 +145,6 @@ export class DrawHistory {
     if (this.onceFlag) {
       this.onceHistory = data;
     } else if (data !== this.current?.data) {
-      // console.log(data, this.current?.data)
       this.bus.emit("push");
       this.history.push({ attachs: JSON.stringify(this.pushAttachs), data });
       this.pushAttachs = {};
@@ -152,7 +155,6 @@ export class DrawHistory {
   redo() {
     const data = this.history.redo();
     this.bus.emit("redo");
-    this.bus.emit('currentChange')
     this.renderer(data);
     return data;
   }
@@ -160,7 +162,6 @@ export class DrawHistory {
   undo() {
     const data = this.history.undo();
     this.bus.emit("undo");
-    this.bus.emit('currentChange')
     this.renderer(data);
     return data;
   }
@@ -192,7 +193,6 @@ export class DrawHistory {
     this.history.reset()
     this.clearCurrent()
     this.bus.emit('clear')
-    this.bus.emit('currentChange')
   }
   
   init() {
@@ -200,21 +200,11 @@ export class DrawHistory {
       this.renderer({ data: this.initData, attachs: "" });
       this.push(this.initData);
       this.bus.emit('init')
-      this.bus.emit('currentChange')
     }
   }
 }
 
-export const useHistory = installGlobalVar(() => {
-  const store = useStoreRaw();
-  const history = new DrawHistory((dataStr: string) => {
-    const data: StoreData = dataStr ? JSON.parse(dataStr) : getEmptyStoreData()
-    store.$patch((state) => {
-      state.data = data;
-    });
-  });
-  return history;
-}, Symbol("history"));
+export const useHistory = installGlobalVar(() => new DrawHistory(), Symbol("history"));
 
 export const useHistoryAttach = <T>(
   name: string,
@@ -241,7 +231,6 @@ export const useHistoryAttach = <T>(
   watch(
     isRuning,
     (isRun, _, onCleanup) => {
-      console.log('isRun', isRun)
       if (!isRun) return;
       history.bus.on("push", pushHandler);
       history.bus.on("attachs", attachsHandler);

+ 35 - 21
src/core/hook/use-layer.ts

@@ -1,6 +1,7 @@
 import { DC, EntityShape } from "@/deconstruction";
 import {
   computed,
+  reactive,
   ref,
   Ref,
   toRaw,
@@ -9,9 +10,11 @@ import {
 } from "vue";
 import { DrawItem } from "../components";
 import { useStore } from "../store";
-import { installGlobalVar, useStage } from "./use-global-vars";
+import { globalWatch, installGlobalVar, useStage } from "./use-global-vars";
 import { Layer } from "konva/lib/Layer";
 import { useMouseShapeStatus } from "./use-mouse-status";
+import { DataGroupId } from "@/constant";
+import { Group } from "konva/lib/Group";
 
 // const useRefreshCount = installGlobalVar(() => ref(0));
 // const useRefresh = () => {
@@ -28,6 +31,15 @@ export const useFormalLayer = () => {
   return computed(() => stage.value?.getNode().find<Layer>("#formal")[0]);
 };
 
+export const useGetFormalChildren = () => {
+  const formal = useFormalLayer();
+  return () => {
+    const child = formal.value?.findOne<Group>(`#${DataGroupId}`)?.children
+    return child ? [...child] : []
+  };
+};
+
+
 export const useHelperLayer = () => {
   const stage = useStage();
   return computed(() => stage.value?.getNode().find<Layer>("#helper")[0]);
@@ -36,26 +48,26 @@ export const useHelperLayer = () => {
 export const useMigrateLayer = (shape: Ref<DC<EntityShape> | undefined>) => {
   const formal = useFormalLayer();
   const zIndexs = useZIndexsManage();
-  let rawLayer: Layer;
+  let rawParent: Layer | Group;
   let toLayer: Layer;
 
   const recovery = () => {
     const $shape = shape.value?.getNode();
-    if (rawLayer && $shape) {
+    if (rawParent && $shape) {
       $shape.remove();
-      rawLayer.add($shape);
+      rawParent.add($shape);
 
       if (import.meta.env.DEV) {
         setTimeout(() => {
           console.log(
-            `recovery raw:${rawLayer.id()} ${
-              rawLayer.children.length
+            `recovery raw:${rawParent.id()} ${
+              rawParent.children.length
             } to:${toLayer.id()} ${toLayer.children.length}`
           );
         });
       }
 
-      if (toRaw(formal.value) === toRaw(rawLayer) && zIndexs.get($shape)) {
+      if ((toRaw(formal.value) === toRaw(rawParent) || rawParent.id() === DataGroupId) && zIndexs.get($shape)) {
         zIndexs.refresh();
       }
     }
@@ -63,15 +75,15 @@ export const useMigrateLayer = (shape: Ref<DC<EntityShape> | undefined>) => {
   const migrate = (to: Layer) => {
     if (!shape.value) throw "shape不存在";
     const $shape = shape.value.getNode();
-    rawLayer = $shape.getLayer()!;
+    rawParent = $shape.getParent() as any;
     toLayer = to;
     $shape.remove();
     to.add($shape);
 
     if (import.meta.env.DEV) {
       console.log(
-        `migrate raw:${rawLayer.id()} ${
-          rawLayer.children.length
+        `migrate raw:${rawParent.id()} ${
+          rawParent.children.length
         } to:${toLayer.id()} ${toLayer.children.length}`
       );
     }
@@ -141,17 +153,17 @@ export const useShapeStaticZindex = (shape: Ref<DC<EntityShape> | undefined>) =>
 
 const useZIndexsManage = installGlobalVar(() => {
   const store = useStore();
-  const map = ref(new Map<EntityShape, DrawItem>());
+  const map = reactive(new Map<EntityShape, DrawItem>());
   const current = useCurrentStaticZIndex();
   const formal = useFormalLayer();
   const sortItems = computed(() => {
-    const items = Array.from(map.value.values());
+    const items = Array.from(map.values());
     return store.getItemsZIndex(items);
   });
 
   const setZIndexs = () => {
-    const shapes = Array.from(map.value.keys());
-    const raws = Array.from(map.value.values());
+    const shapes = Array.from(map.keys());
+    const raws = Array.from(map.values());
     let start = current.value;
     sortItems.value.forEach((item) => {
       const rawNdx = raws.findIndex((raw) => raw === item);
@@ -161,18 +173,20 @@ const useZIndexsManage = installGlobalVar(() => {
       shape.zIndex(start++);
     });
   };
-
-  watch(() => [sortItems.value, current.value], setZIndexs);
+  globalWatch(() => sortItems.value.length + ',' + current.value, setZIndexs, {immediate: true})
 
   return {
     set(shape: EntityShape, item: DrawItem) {
-      map.value.set(shape, item);
+      map.set(shape, item);
     },
     del(shape: EntityShape) {
-      map.value.delete(shape);
+      map.delete(shape);
     },
     get(shape: EntityShape) {
-      return map.value.get(shape);
+      return map.get(shape);
+    },
+    clear() {
+      map.clear()
     },
     refresh: setZIndexs,
   };
@@ -207,11 +221,11 @@ export const useZIndex = (
   atData: Ref<DrawItem>
 ) => {
   const zIndexs = useZIndexsManage();
-  watch(shape, (shape, _, onCleanup) => {
+  watch([shape, atData], ([shape, atData], _, onCleanup) => {
     const $shape = shape?.getNode();
     if ($shape) {
       watchEffect(() => {
-        zIndexs.set($shape, atData.value);
+        zIndexs.set($shape, atData);
       });
       onCleanup(() => {
         zIndexs.del($shape);

+ 33 - 28
src/core/hook/use-mouse-status.ts

@@ -141,10 +141,9 @@ export const useMouseShapesStatus = installGlobalVar(() => {
   const selects = ref([]) as Ref<EntityShape[]>;
   const actives = ref([]) as Ref<EntityShape[]>;
   const operMode = useOperMode();
+  const pointerIsTransformerInner = usePointerIsTransformerInner();
 
   const init = (stage: Stage) => {
-    let downTarget: EntityShape | null;
-
     const prevent = computed(() => operMode.value.freeView);
     const [hover, hoverDestory] = getHoverShape(stage);
     const hoverChange = () => {
@@ -162,9 +161,10 @@ export const useMouseShapesStatus = installGlobalVar(() => {
     );
 
     let downTime: number;
-    let downPos: Pos | null = null
+    let downPos: Pos | null = null;
+    let downTarget: EntityShape | null;
     stage.on("pointerdown.mouse-status", (ev) => {
-      downPos = {x: ev.evt.pageX, y: ev.evt.pageY}
+      downPos = { x: ev.evt.pageX, y: ev.evt.pageY };
       downTime = Date.now();
       if (prevent.value) return;
       const target = shapeTreeContain(listeners.value, ev.target);
@@ -181,49 +181,54 @@ export const useMouseShapesStatus = installGlobalVar(() => {
         stage.container().parentElement as HTMLDivElement,
         "pointerup",
         async (ev) => {
-          const target = downTarget
-          const moveDis = downPos ? lineLen(downPos!, {x: ev.pageX, y: ev.pageY} ) : 0
-          const isMove = moveDis > 2
-          downTarget = null
+          const target = downTarget;
+          const moveDis = downPos
+            ? lineLen(downPos!, { x: ev.pageX, y: ev.pageY })
+            : 3;
+          downPos = null;
+          downTarget = null;
+          const isMove = moveDis > 2;
           if (prevent.value) return;
           press.value = [];
           if (Date.now() - downTime > 300 || isMove) return;
-
-          if (!target || ev.button !== 0) {
-            actives.value = [];
-            if (!isMove) {
-              selects.value = [];
-            }
+          if (pointerIsTransformerInner()) return;
+          if (ev.button !== 0) {
+            // actives.value = [];
+            // if (!operMode.value.mulSelection) {
+            //   selects.value = [];
+            // }
             return;
           }
-
           if (operMode.value.mulSelection) {
+            if (!target) return;
+            actives.value = [];
             const ndx = selects.value.findIndex(
               (item) => item.id() === target?.id()
             );
             if (~ndx) {
               selects.value.splice(ndx, 1);
             } else {
-              selects.value.push(target);
+              selects.value.push(target!);
             }
-            actives.value = [];
+            return;
           } else {
-            actives.value = [target];
             selects.value = [];
+            actives.value = target ? [target] : [];
           }
         }
       ),
-      listener(
-        stage.container().parentElement as HTMLDivElement,
-        "pointermove",
-        async (ev) => {
-          if (prevent.value) return;
-          if (downTarget && !operMode.value.mulSelection) {
-            selects.value = []
-          }
-        }
-      ),
+      // listener(
+      //   stage.container().parentElement as HTMLDivElement,
+      //   "pointermove",
+      //   async () => {
+      //     if (prevent.value) return;
+      //     if (downTarget && !operMode.value.mulSelection) {
+      //       selects.value = [];
+      //     }
+      //   }
+      // ),
       () => {
+        console.error("取消鼠标事件监听");
         listeners.value.forEach((shape) => {
           shape.off("pointerleave.mouse-status");
         });

+ 5 - 3
src/core/hook/use-paste.ts

@@ -11,8 +11,6 @@ export const usePaste = installGlobalVar(() => {
   const stage = useStage();
   const handlers = stackVar<PasteHandlers>({});
   const pasteHandler = (ev: ClipboardEvent) => {
-    if (ev.target !== document.body && (ev.target as any).id !== 'dom-mount') return;
-
     const pos = stage.value?.getNode().pointerPos;
     const clipboardData = ev.clipboardData;
     if (!clipboardData || !pos) return;
@@ -36,8 +34,12 @@ export const usePaste = installGlobalVar(() => {
     }
   };
 
+  const stop = listener(window, "paste", pasteHandler)
   return {
     var: handlers,
-    onDestroy: listener(window, "paste", pasteHandler),
+    onDestroy: () => {
+      console.log('取消监听')
+      stop()
+    }
   };
 });

+ 278 - 100
src/core/hook/use-selection.ts

@@ -1,32 +1,105 @@
 import { Rect } from "konva/lib/shapes/Rect";
 import {
   globalWatch,
-  globalWatchEffect,
   installGlobalVar,
+  useForciblyShowItemIds,
   useMountParts,
   useStage,
 } from "./use-global-vars";
-import { useFormalLayer, useHelperLayer } from "./use-layer";
+import {
+  useGetFormalChildren,
+  useFormalLayer,
+  useHelperLayer,
+} from "./use-layer";
 import { themeColor } from "@/constant/help-style";
 import { dragListener } from "@/utils/event";
 import { Layer } from "konva/lib/Layer";
 import { useOperMode } from "./use-status";
-import { computed, markRaw, ref, watch, watchEffect } from "vue";
+import { computed, markRaw, nextTick, reactive, ref, watch, watchEffect } from "vue";
 import { EntityShape } from "@/deconstruction";
 import { Util } from "konva/lib/Util";
-import { useViewerInvertTransform, useViewerTransform } from "./use-viewer";
-import { mergeFuns, onlyId } from "@/utils/shared";
+import { useViewerInvertTransform } from "./use-viewer";
+import {
+  diffArrayChange,
+  mergeFuns,
+  onlyId,
+} from "@/utils/shared";
 import { IRect } from "konva/lib/types";
 import { useMouseShapesStatus } from "./use-mouse-status";
-import { Pos } from "@/utils/math";
 import Icon from "../components/icon/temp-icon.vue";
 import { Group } from "konva/lib/Group";
-import { Component as GroupComp } from "../components/group/";
-import { useStore } from "../store";
+import { Component as GroupComp, GroupData } from "../components/group/";
+import { DrawStore, useStore } from "../store";
+import { DrawItem } from "../components";
+import { Stage } from "konva/lib/Stage";
+import { useOnComponentBoundChange } from "./use-component";
+import { useHistory } from "./use-history";
+
+const normalSelectIds = (
+  store: DrawStore,
+  ids: string[],
+  needChildren = false
+) => {
+  if (!store.data.typeItems.group) return ids;
+
+  const gChildrenIds = store.data.typeItems.group.map((item) => item.ids);
+  const findNdx = (id: string) =>
+    gChildrenIds.findIndex((cIds) => cIds.includes(id));
+  if (!needChildren) {
+    return ids.filter((id) => !~findNdx(id));
+  }
+
+  const groupIds = store.data.typeItems.group.map((item) => item.id);
+  const nIds: string[] = [];
+  for (let i = 0; i < ids.length; i++) {
+    let ndx = findNdx(ids[i]);
+    ~ndx || (ndx = groupIds.indexOf(ids[i]));
+
+    if (!~ndx) {
+      nIds.push(ids[i]);
+      continue;
+    }
+
+    const group = store.data.typeItems.group[ndx];
+    const addIds = [group.id, ...group.ids].filter(
+      (aid) => !nIds.includes(aid)
+    );
+    nIds.push(...addIds);
+  }
+  return nIds
+};
+
+export const normalSelectShapes = (
+  stage: Stage,
+  store: DrawStore,
+  shapes: EntityShape[],
+  needChildren = false
+) => {
+  let ids: string[] = [];
+  for (let i = 0; i < shapes.length; i++) {
+    const shape = shapes[i];
+    const id = shape.id();
+    id && ids.push(id);
+  }
+  ids = normalSelectIds(store, ids, needChildren);
+  return ids.map((id) => stage.findOne(`#${id}`)!) as EntityShape[];
+};
+
+export const normalSelectItems = (
+  store: DrawStore,
+  items: DrawItem[],
+  needChildren = false
+) => {
+  return normalSelectIds(
+    store,
+    items.map((item) => item.id),
+    needChildren
+  ).map((id) => store.getItemById(id)!);
+};
 
 export const useSelection = installGlobalVar(() => {
   const layer = useHelperLayer();
-  const formatLayer = useFormalLayer();
+  const getChildren = useGetFormalChildren();
   const box = new Rect({
     stroke: themeColor,
     strokeWidth: 1,
@@ -37,11 +110,19 @@ export const useSelection = installGlobalVar(() => {
   const stage = useStage();
   const operMode = useOperMode();
   const selections = ref<EntityShape[]>();
-  const viewMat = useViewerTransform();
-  const store = useStore();
 
   let shapeBoxs: IRect[] = [];
-  let shpaes: EntityShape[] = []
+  let shapes: EntityShape[] = [];
+
+  const updateSelections = () => {
+    const boxRect = box.getClientRect();
+    selections.value = [];
+
+    for (let i = 0; i < shapeBoxs.length; i++) {
+      if (Util.haveIntersection(boxRect, shapeBoxs[i]))
+        selections.value.push(shapes[i]);
+    }
+  }
 
   const init = (dom: HTMLDivElement, layer: Layer) => {
     const stopListener = dragListener(dom, {
@@ -55,14 +136,7 @@ export const useSelection = installGlobalVar(() => {
       move({ end }) {
         box.width(end.x - box.x());
         box.height(end.y - box.y());
-
-        const boxRect = box.getClientRect();
-        selections.value = [];
-        for (let i = 0; i < shapeBoxs.length; i++) {
-          if (Util.haveIntersection(boxRect, shapeBoxs[i])) {
-            selections.value!.push(shpaes[i]);
-          }
-        }
+        updateSelections()
       },
       up() {
         selections.value = undefined;
@@ -75,22 +149,32 @@ export const useSelection = installGlobalVar(() => {
     };
   };
 
-  const stopWatch = mergeFuns(
-    globalWatchEffect((onCleanup) => {
-      const dom = stage.value?.getNode().container();
-      if (dom && operMode.value.mulSelection && layer.value) {
-        onCleanup(init(dom, layer.value));
-      }
-    }),
-    globalWatch(
-      () => [viewMat.value, operMode.value.mulSelection],
-      () => {
-        if (operMode.value.mulSelection) {
-          shpaes = formatLayer.value!.children.filter((shape) => store.getItemById(shape.id()))
-          shapeBoxs = shpaes.map((shape) => shape.getClientRect());
-        }
-      }
-    )
+  const updateInitData = () => {
+    shapes = getChildren();
+    shapeBoxs = shapes.map((shape) => shape.getClientRect());
+  };
+  const shpaesChange = () => {
+    updateInitData()
+    updateSelections()
+  }
+
+  const store = useStore()
+  const stopWatch = globalWatch(
+    () => operMode.value.mulSelection,
+    (mulSelection, _, onCleanup) => {
+      if (!mulSelection) return;
+      const dom = stage.value?.getNode().container()!;
+      updateInitData();
+
+      const unInit = init(dom, layer.value!);
+      store.bus.on('dataChangeAfter', shpaesChange)
+      store.bus.on('delItemAfter', shpaesChange)
+      onCleanup(() => {
+        unInit()
+        store.bus.off('dataChangeAfter', shpaesChange)
+        store.bus.off('delItemAfter', shpaesChange)
+      })
+    }
   );
 
   return {
@@ -101,21 +185,117 @@ export const useSelection = installGlobalVar(() => {
 
 export const useSelectionShowIcons = installGlobalVar(() => {
   const mParts = useMountParts();
+  const { on } = useOnComponentBoundChange();
   const iconProps = {
     width: 20,
     height: 20,
-    zIndex: 99999,
     url: "/icons/state_s.svg",
     fill: themeColor,
     stroke: "#fff",
+    listening: false,
   };
 
   const status = useMouseShapesStatus();
-  const formatLayer = useFormalLayer();
-  const mouseSelects = computed(() => {
-    const selectShapes = status.selects.filter((shape) =>
-      formatLayer.value?.children.includes(shape)
+
+  const store = useStore();
+  const invMat = useViewerInvertTransform();
+  const getShapeMat = (shape: EntityShape) => {
+    const rect = shape.getClientRect();
+    const center = invMat.value.point({
+      x: rect.x + rect.width / 2,
+      y: rect.y + rect.height / 2,
+    });
+    return [1, 0, 0, 1, center.x, center.y];
+  };
+  const shapes = computed(() =>
+    status.selects.filter((shape) => store.getType(shape.id()) !== "group")
+  );
+  const unMountMap = new WeakMap<EntityShape, () => void>();
+  watch(shapes, (shapes, oldShapes) => {
+    const { added, deleted } = diffArrayChange(shapes, oldShapes);
+    for (const addShape of added) {
+      const mat = ref(getShapeMat(addShape));
+      const unHooks = [
+        on(addShape, () => (mat.value = getShapeMat(addShape))),
+        mParts.add({
+          comp: markRaw(Icon),
+          props: {
+            data: reactive({ ...iconProps, mat: mat }),
+          },
+        }),
+      ];
+      unMountMap.set(addShape, mergeFuns(unHooks));
+    }
+    for (const delShape of deleted) {
+      const fn = unMountMap.get(delShape);
+      fn && fn();
+    }
+  });
+});
+
+export const useWatchSelectionGroup = () => {
+  const stage = useStage();
+  const store = useStore();
+  const status = useMouseShapesStatus();
+  const addShapes = (allShapes: Set<EntityShape>, iShapes: EntityShape[]) => {
+    const shapes = normalSelectShapes(
+      stage.value!.getNode(),
+      store,
+      iShapes,
+      true
+    );
+    shapes.forEach((shape) => allShapes.add(shape));
+    return allShapes;
+  };
+  const delShapes = (allShapes: Set<EntityShape>, dShapes: EntityShape[]) => {
+    const shapes = normalSelectShapes(
+      stage.value!.getNode(),
+      store,
+      dShapes,
+      true
     );
+    shapes.forEach((item) => allShapes.delete(item));
+    return allShapes;
+  };
+
+  // 分组管理
+  const watchSelectionGroup = () =>
+    watch(
+      () => status.selects,
+      (shapes, oldShapes) => {
+        const { added, deleted } = diffArrayChange(shapes, oldShapes);
+        const filterShapes = new Set(shapes);
+        added.length && addShapes(filterShapes, added);
+        deleted.length && delShapes(filterShapes, deleted);
+
+        if (added.length || deleted.length) {
+          status.selects = Array.from(filterShapes);
+        }
+      },
+      { flush: "post" }
+    );
+
+  return {
+    addShapes,
+    delShapes,
+    watchSelectionGroup,
+  };
+};
+
+export const useSelectionRevise = () => {
+  const mParts = useMountParts();
+  const status = useMouseShapesStatus();
+  const { addShapes, delShapes, watchSelectionGroup } =
+    useWatchSelectionGroup();
+
+  useSelectionShowIcons();
+
+  const getFormatChildren = useGetFormalChildren();
+  const mouseSelects = computed(() => {
+    const children = getFormatChildren();
+    const selectShapes = status.selects.filter((shape) => {
+      return children.includes(shape);
+    });
     return selectShapes;
   });
 
@@ -127,78 +307,34 @@ export const useSelectionShowIcons = installGlobalVar(() => {
 
   const rectSelects = useSelection();
   let initSelections: EntityShape[] = [];
+  let stopWatchSelectionGroup = watchSelectionGroup();
   watch(
     () => rectSelects.value && [...rectSelects.value],
     (rectSelects, oldRectSelects) => {
       if (!oldRectSelects) {
         initSelections = [...mouseSelects.value];
+        stopWatchSelectionGroup();
       } else if (!rectSelects) {
         initSelections = [];
+        stopWatchSelectionGroup = watchSelectionGroup();
       } else {
         status.selects = Array.from(
-          new Set([...initSelections, ...rectSelects])
+          addShapes(new Set(initSelections), rectSelects)
         );
       }
     }
   );
 
-  const selectionCenters = ref<Pos[]>([]);
-  const invMat = useViewerInvertTransform()
-  watch(
-    () => [invMat.value, status.selects],
-    (_a, _b, onCleanup) => {
-      selectionCenters.value = [];
-      onCleanup(mergeFuns(status.selects.map((shape, ndx) => {
-        const set = () => {
-          const rect = shape.getClientRect();
-          selectionCenters.value[ndx] = invMat.value.point({
-            x: rect.x + rect.width / 2,
-            y: rect.y + rect.height / 2,
-          });
-        }
-        set()
-        shape.on('bound-change', set)
-        return () => shape.off('bound-change', set)
-      })))
-    }
-  );
-
-  watchEffect((onCleanup) => {
-    onCleanup(
-      mergeFuns(
-        selectionCenters.value.map((center) =>
-          mParts.add({
-            comp: markRaw(Icon),
-            props: {
-              data: { ...iconProps, mat: [1, 0, 0, 1, center.x, center.y] },
-            },
-          })
-        )
-      )
-    );
-  });
-
-  return computed({
-    get: () => status.selects,
-    set: (val: EntityShape[]) => (status.selects = val),
-  });
-});
-
-export const useSelectionRevise = () => {
-  const mParts = useMountParts();
-  const selects = useSelectionShowIcons();
-
-  const ids = computed(() => selects.value.map((item) => item.id()));
+  const ids = computed(() => status.selects.map((item) => item.id()));
   const groupConfig = {
     id: onlyId(),
     createTime: Date.now(),
-    zIndex: 9999,
     lock: false,
     opacity: 1,
     ref: false,
     listening: false,
+    stroke: themeColor,
   };
-  const status = useMouseShapesStatus();
   const operMode = useOperMode();
   const layer = useFormalLayer();
   watch(
@@ -213,16 +349,58 @@ export const useSelectionRevise = () => {
       }
     }
   );
-  
 
+  const stage = useStage();
+  const store = useStore();
+  const history = useHistory();
+  const showItemId = useForciblyShowItemIds();
   watchEffect((onCleanup) => {
-    if (ids.value.length) {
-      onCleanup(
-        mParts.add({
-          comp: markRaw(GroupComp),
-          props: { data: { ...groupConfig, ids: ids.value } },
-        })
-      );
-    }
+    if (!ids.value.length) return;
+    const props = {
+      data: { ...groupConfig, ids: ids.value },
+      onUpdateShape(data: GroupData) {
+        status.selects
+        data.ids
+      },
+      onDelShape() {
+        console.log('delShape')
+        status.selects = []
+      },
+      onAddShape(data: GroupData) {
+        history.onceTrack(() => {
+          const ids = data.ids;
+          const cIds = ids.filter(id => store.getType(id) !== "group")
+
+          const groups = store.data.typeItems.group
+          const exists = groups?.some(group => {
+            if (group.ids.length !== cIds.length) return false
+            const diff = diffArrayChange(group.ids, cIds)
+            return diff.added.length === 0 && diff.deleted.length == 0
+          })
+          if (exists) return;
+
+          let selects = new Set(status.selects);
+          for (let i = 0; i < ids.length; i++) {
+            if (store.getType(ids[i]) === "group") {
+              delShapes(
+                selects,
+                status.selects.filter((shape) => shape.id() === ids[i])
+              );
+              store.delItem("group", ids[i]);
+            }
+          }
+
+          store.addItem("group", { ...data, ids: cIds });
+          showItemId.cycle(data.id, async () => {
+            await nextTick();
+            const $stage = stage.value!.getNode();
+            const addShape = $stage.findOne("#" + data.id) as EntityShape;
+            addShapes(selects, [addShape]);
+            status.selects = Array.from(selects);
+          });
+        });
+      },
+    };
+    onCleanup(mParts.add({ comp: markRaw(GroupComp), props }));
   });
 };

+ 4 - 2
src/core/hook/use-status.ts

@@ -87,10 +87,12 @@ export const useOperMode = installGlobalVar(() => {
 
   return computed(() => ({
     // 多选模式
-    mulSelection: keys.has('Shift') && !keys.has(' '),
+    mulSelection: keys.has('Shift') && !keys.has(' ') && !keys.has('Alt'),
     // 自由移动视图
     freeView: keys.has(' '),
     // 自由绘图,不吸附
-    freeDraw: keys.has('Control')
+    freeDraw: keys.has('Control'),
+    // 组操作模式
+    group: keys.has('Alt')
   }))
 })

+ 59 - 36
src/core/hook/use-transformer.ts

@@ -1,18 +1,23 @@
 import { useMouseShapeStatus } from "./use-mouse-status.ts";
-import { Ref, ref, watch } from "vue";
+import { nextTick, Ref, ref, toRaw, watch } from "vue";
 import { DC, EntityShape } from "../../deconstruction";
 import {
   installGlobalVar,
   useStage,
   useTransformIngShapes,
 } from "./use-global-vars.ts";
-import { useCan, useMode } from './use-status'
+import { useCan, useMode } from "./use-status";
 import { Mode } from "../../constant/mode.ts";
-import { Transform, Util } from "konva/lib/Util";
+import { Transform } from "konva/lib/Util";
 import { Pos, vector } from "@/utils/math.ts";
 import { useConversionPosition } from "./use-coversion-position.ts";
 import { getOffset, listener } from "@/utils/event.ts";
-import { flatPositions, frameEebounce, mergeFuns, round } from "@/utils/shared.ts";
+import {
+  flatPositions,
+  frameEebounce,
+  mergeFuns,
+  round,
+} from "@/utils/shared.ts";
 import { Line } from "konva/lib/shapes/Line";
 import { setShapeTransform } from "@/utils/shape.ts";
 import { Transformer } from "../transformer.ts";
@@ -86,15 +91,18 @@ export const usePointerIsTransformerInner = () => {
   const stage = useStage();
   return () => {
     const $stage = stage.value!.getStage();
-    const tfRect = transformer.getClientRect();
-    const padding = transformer.padding();
-    tfRect.x -= padding;
-    tfRect.y -= padding;
-    tfRect.width += padding;
-    tfRect.height += padding;
-    const pointRect = { ...$stage.pointerPos!, width: 1, height: 1 };
-
-    return Util.haveIntersection(tfRect, pointRect);
+    const pos = $stage.pointerPos;
+    if (!pos) return false;
+    const hitShapes = $stage.getAllIntersections(pos);
+    if (!hitShapes.length) return false;
+    const selfShapes = [
+      ...transformer.children,
+      ...transformer.queueShapes.value.map(toRaw),
+    ];
+    for (const shape of hitShapes) {
+      if (selfShapes.includes(shape)) return true;
+    }
+    return false;
   };
 };
 
@@ -176,7 +184,7 @@ export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
   const can = useCan();
   const conversion = useConversionPosition(true);
   const transformIngShapes = useTransformIngShapes();
-  const status = useMouseShapeStatus(shape)
+  const status = useMouseShapeStatus(shape);
 
   const init = (shape: EntityShape) => {
     const dom = shape.getStage()!.container();
@@ -231,11 +239,12 @@ export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
     ]);
   };
 
-  const result = usePause(offset)
+  const result = usePause(offset);
   watch(
     () =>
       (can.editMode || mode.include(Mode.update)) &&
-      (status.value.active || status.value.hover) && !result.isPause,
+      (status.value.active || status.value.hover) &&
+      !result.isPause,
     (canEdit, _, onCleanup) => {
       canEdit && onCleanup(init(shape.value!.getNode()));
     }
@@ -278,7 +287,13 @@ export const useShapeTransformer = <T extends EntityShape>(
       };
     }
 
-    const set = frameEebounce((appleTransform: Transform | undefined) => transform.value = appleTransform)
+    let selfFire = false;
+    const set = frameEebounce((appleTransform: Transform | undefined) => {
+      transform.value = appleTransform;
+      selfFire = true;
+      $shape.fire("bound-change");
+      selfFire = false;
+    });
     const updateTransform = () => {
       if (!can.dragMode) return;
       let appleTransform = rep.tempShape.getTransform().copy();
@@ -289,13 +304,15 @@ export const useShapeTransformer = <T extends EntityShape>(
         appleTransform = handlerTransform(appleTransform);
         setShapeTransform(rep.tempShape, appleTransform);
       }
-      set(appleTransform)
+      set(appleTransform);
     };
 
     rep.tempShape.on("transform.shapemer", updateTransform);
 
     const boundHandler = () => {
-      rep.update && rep.update();
+      if (!selfFire) {
+        rep.update && rep.update();
+      }
     };
     $shape.on("bound-change", boundHandler);
 
@@ -407,7 +424,7 @@ export const useShapeTransformer = <T extends EntityShape>(
           transformer.off("pointerdown.shapemer", downHandler);
         });
       },
-      { immediate: true }
+      { immediate: true, flush: "post" }
     );
 
     return () => {
@@ -421,21 +438,23 @@ export const useShapeTransformer = <T extends EntityShape>(
 
   watch(
     () => shape.value,
-    (shape, _) => {
+    (shape, _, onCleanup) => {
       if (!shape) return;
-      watch(
-        () =>
-          (can.editMode || mode.include(Mode.update)) &&
-          (status.value.active || status.value.hover),
-        (canEdit, _, onCleanup) => {
-          if (canEdit) {
-            const stop = init(shape.getStage());
-            onCleanup(stop);
-          } else {
-            onCleanup(() => {});
-          }
-        },
-        { immediate: true }
+      onCleanup(
+        watch(
+          () =>
+            (can.editMode || mode.include(Mode.update)) &&
+            (status.value.active || status.value.hover),
+          (canEdit, _, onCleanup) => {
+            if (canEdit) {
+              const stop = init(shape.getNode());
+              onCleanup(stop);
+            } else {
+              onCleanup(() => {});
+            }
+          },
+          { immediate: true }
+        )
       );
     }
   );
@@ -487,6 +506,7 @@ type GetRepShape<T extends EntityShape, K extends object> = (shape: T) => {
   shape: T;
   update?: (data: K, shape: T) => void;
   init?: (data: K, shape: T) => void;
+  destory?: () => void
 };
 export type CustomTransformerProps<
   T extends BaseItem,
@@ -529,14 +549,17 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
           init: () => {
             repResult.init && repResult.init(data.value, repResult.shape);
           },
-          destory,
+          destory: () => {
+            repResult.destory && repResult.destory()
+            destory()
+          },
         };
       })
   );
   let callMat: Transform;
   watch(transform, (current, oldTransform) => {
     if (!oldTransform) {
-      props.start && props.start()
+      props.start && props.start();
     }
 
     if (current) {

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

@@ -24,7 +24,6 @@ watch(
   ({ canSnap, item }) => {
     infos.forEach(customSnapInfos.remove);
     if (!canSnap) return;
-    console.log("addSnap", item);
     infos = component.value.getSnapInfos(item as any);
     infos.forEach(customSnapInfos.add);
   },

+ 21 - 15
src/core/renderer/renderer.vue

@@ -14,20 +14,26 @@
 
       <v-stage ref="stage" :config="size" v-if="layout">
         <v-layer :config="viewerConfig" id="formal">
-          <Back />
-          <!--	不可去除,去除后移动端拖拽会有溢出	-->
-          <BackGrid v-if="expose.config.showGrid" />
-          <component
-            :is="GroupComponentMap[type]"
-            v-for="type in types"
-            :type="type"
-            :key="type"
-          />
-          <component
-            v-for="part in mountParts.value"
-            :is="part.comp"
-            v-bind="part.props"
-          />
+          <v-group>
+            <Back />
+            <!--	不可去除,去除后移动端拖拽会有溢出	-->
+            <BackGrid v-if="expose.config.showGrid" />
+          </v-group>
+          <v-group :id="DataGroupId">
+            <component
+              :is="GroupComponentMap[type]"
+              v-for="type in types"
+              :type="type"
+              :key="type"
+            />
+          </v-group>
+          <v-group>
+            <component
+              v-for="part in mountParts.value"
+              :is="part.comp"
+              v-bind="part.props"
+            />
+          </v-group>
         </v-layer>
         <!--	临时组,提供临时绘画,以及高频率渲染	-->
         <v-layer :config="viewerConfig" id="temp">
@@ -71,7 +77,7 @@ import { useMode } from "../hook/use-status.ts";
 import { useViewerTransformConfig } from "../hook/use-viewer.ts";
 import { useGlobalResize } from "../hook/use-event.ts";
 import { useAutoService, useExpose } from "../hook/use-expose.ts";
-import { DomMountId, DomOutMountId, rendererMap, rendererName } from "../../constant";
+import { DataGroupId, DomMountId, DomOutMountId, rendererMap, rendererName } from "../../constant";
 import { useStore } from "../store/index.ts";
 import { Mode } from "@/constant/mode.ts";
 import { computed, getCurrentInstance, onUnmounted, ref, watch } from "vue";

+ 61 - 18
src/core/store/index.ts

@@ -1,53 +1,96 @@
-import { Component, reactive } from "vue";
+import { Component, nextTick, reactive } from "vue";
 import { DrawItem } from "../components";
 import { installGlobalVar } from "../hook/use-global-vars";
-import { useStoreRaw } from "./store";
+import { getEmptyStoreData, useStoreRaw } from "./store";
 import { useHistory } from "../hook/use-history";
 import { round } from "@/utils/shared";
+import mitt, { Emitter } from "mitt";
+import { FilterKeysWithPrefix, FilterNever } from "@/deconstruction";
 
 // 放置重复放入历史,我们把小数保留5位小数
-const b = 3
+const b = 3;
 const normalStore = (store: any) => {
-  const norStore = Array.isArray(store) ? [...store] : {...store}
+  const norStore = Array.isArray(store) ? [...store] : { ...store };
   for (const key in norStore) {
-    const val = norStore[key]
-    if (typeof val === 'number') {
-      norStore[key] = round(val, b)
-    } else if (typeof val === 'object') {
-      norStore[key] = normalStore(val)
+    const val = norStore[key];
+    if (typeof val === "number") {
+      norStore[key] = round(val, b);
+    } else if (typeof val === "object") {
+      norStore[key] = normalStore(val);
     } else {
-      norStore[key] = val
+      norStore[key] = val;
     }
   }
-  return norStore
-}
+  return norStore;
+};
+
+type DSR = ReturnType<typeof useStoreRaw>;
+type EDSR<T> = {
+  [key in keyof T as key extends string
+    ? `${key}Before` | `${key}After`
+    : never]: T[key];
+};
+type DSRAction = {
+  [key in keyof DSR]: DSR[key] extends (...args: infer Args) => any
+    ? Args
+    : never;
+};
+
+export type DrawStoreBusArgs = EDSR<
+  { dataChange: void } & FilterKeysWithPrefix<
+    FilterNever<DSRAction>,
+    "$" | "get"
+  >
+>;
+
+export type DrawStore = DSR & { bus: Emitter<DrawStoreBusArgs> };
 
 export const useStore = installGlobalVar(() => {
   const history = useHistory();
-  const store = useStoreRaw();
+  const store = useStoreRaw() as DrawStore;
+  store.bus = mitt();
+
+  history.setRenderer((dataStr: string) => {
+    const data = dataStr ? JSON.parse(dataStr) : getEmptyStoreData();
+    store.$patch((state) => {
+      store.bus.emit('dataChangeBefore')
+      state.data = data;
+      nextTick(() => store.bus.emit("dataChangeAfter"))
+    });
+  });
 
   const trackActions = [
     "setStore",
     "repStore",
     "addItem",
+    "addItems",
     "delItem",
     "setItem",
-    "setConfig"
+    "setConfig",
   ];
-  store.$onAction(({ name, after, store }) => {
+  let isRuning = false;
+  store.$onAction(({ name, after, args }) => {
     if (!trackActions.includes(name)) return;
-    const isInit = name === "setStore";
+    let currentIsRuning = isRuning;
+    isRuning = true;
+    store.bus.emit((name + "Before") as any, args);
+    nextTick(() => store.bus.emit((name + "After") as any, args))
+    
+    if (currentIsRuning) {
+      return;
+    }
+
     after(() => {
-      if (isInit) {
+      if (name === "setStore") {
         history.setInit(JSON.stringify(store.data));
       } else {
         history.push(JSON.stringify(store.data));
       }
+      isRuning = false;
     });
   });
   return store;
 }, Symbol("store"));
-export type DrawStore = ReturnType<typeof useStore>
 
 export const useStoreRenderProcessors = installGlobalVar(() => {
   type Processor<T extends DrawItem = DrawItem> = (data: T) => Component;

+ 9 - 1
src/deconstruction.d.ts

@@ -5,4 +5,12 @@ type DC<T extends any> = {
 	getStage: () => T
 }
 
-type EntityShape = (Konva.Shape | Konva.Stage | Konva.Layer | Konva.Group) & { repShape?: EntityShape }
+type EntityShape = (Konva.Shape | Konva.Stage | Konva.Layer | Konva.Group) & { repShape?: EntityShape, needPenetrate?: boolean }
+
+type FilterNever<T> = {  
+	[K in keyof T as T[K] extends never ? never : K]: T[K]  
+};  
+
+type FilterKeysWithPrefix<T, P extends string> = {  
+	[K in keyof T as K extends `${P}${string}` ? never : K]: T[K]  
+};  

+ 4 - 2
src/example/fuse/views/test.ts

@@ -2,7 +2,7 @@ export const initData = {
   typeItems: {
     image: [
       {
-        id: "-10",
+        id: "10cc6b4dasdasd",
         createTime: 1,
         zIndex: -194,
         url: "/WX20241213-205427@2x.png",
@@ -24,7 +24,6 @@ export const initData = {
         lock: true,
       },
     ],
-    bgImage: [],
     rectangle: [
       {
         id: "0",
@@ -403,6 +402,9 @@ export const initData = {
         radiusY: 101.392578125,
       },
     ],
+    group: [
+      { id: "asdasd", ids: ['333asdt3', '3t3asd33']}
+    ],
     text: [
       {
         createTime: 3,

+ 4 - 0
src/utils/event.ts

@@ -74,6 +74,10 @@ export const dragListener = (dom: HTMLElement, props: DragProps | DragProps['mov
 		props.notPrevent || ev.preventDefault();
 
 		moveHandler = (ev: PointerEvent) => {
+      if (ev.buttons <= 0) {
+        endHandler()
+        return;
+      }
 			const end = getOffset(ev, dom)
 			move!({start, end, prev, ev})
 			prev = end

+ 26 - 0
src/utils/shared.ts

@@ -263,3 +263,29 @@ export const getResizeCorsur = (level = true, r = 0) => {
     }
   }
 };
+
+
+export const diffArrayChange = <T extends Array<any>>(
+  newItems: T,
+  oldItems: T
+) => {
+  const addedItems = [] as unknown as T;
+  const deletedItems = [] as unknown as T;
+
+  for (const item of newItems) {
+    if (!oldItems.includes(item)) {
+      addedItems.push(item);
+    }
+  }
+
+  for (const item of oldItems) {
+    if (!newItems.includes(item)) {
+      deletedItems.push(item);
+    }
+  }
+
+  return {
+    added: addedItems,
+    deleted: deletedItems,
+  };
+};