Procházet zdrojové kódy

feat: 添加复选操作功能

bill před 5 měsíci
rodič
revize
bf20004ad1
50 změnil soubory, kde provedl 1119 přidání a 535 odebrání
  1. 1 0
      public/icons/state_s.svg
  2. 1 0
      src/constant/index.ts
  3. 3 5
      src/core/components/arrow/arrow.vue
  4. 19 1
      src/core/components/arrow/index.ts
  5. 0 38
      src/core/components/bg-image/bg-image.vue
  6. 0 73
      src/core/components/bg-image/index.ts
  7. 0 75
      src/core/components/bg-image/temp-bg-image.vue
  8. 5 26
      src/core/components/circle/circle.vue
  9. 24 2
      src/core/components/circle/index.ts
  10. 101 0
      src/core/components/group/group.vue
  11. 80 0
      src/core/components/group/index.ts
  12. 83 0
      src/core/components/group/temp-group.vue
  13. 4 5
      src/core/components/icon/icon.vue
  14. 9 1
      src/core/components/icon/index.ts
  15. 4 6
      src/core/components/image/image.vue
  16. 7 1
      src/core/components/image/index.ts
  17. 14 4
      src/core/components/index.ts
  18. 19 1
      src/core/components/line/index.ts
  19. 4 7
      src/core/components/line/line.vue
  20. 18 1
      src/core/components/polygon/index.ts
  21. 4 7
      src/core/components/polygon/polygon.vue
  22. 18 1
      src/core/components/rectangle/index.ts
  23. 5 8
      src/core/components/rectangle/rectangle.vue
  24. 26 1
      src/core/components/serial/index.ts
  25. 4 32
      src/core/components/serial/serial.vue
  26. 4 2
      src/core/components/share/edit-polygon.vue
  27. 1 1
      src/core/components/share/text.vue
  28. 102 31
      src/core/components/table/index.ts
  29. 11 7
      src/core/components/table/table.vue
  30. 36 2
      src/core/components/text/index.ts
  31. 13 37
      src/core/components/text/text.vue
  32. 18 1
      src/core/components/triangle/index.ts
  33. 4 7
      src/core/components/triangle/triangle.vue
  34. 2 2
      src/core/hook/use-alignment.ts
  35. 3 9
      src/core/hook/use-draw.ts
  36. 6 1
      src/core/hook/use-event.ts
  37. 4 4
      src/core/hook/use-expose.ts
  38. 19 88
      src/core/hook/use-global-vars.ts
  39. 34 9
      src/core/hook/use-history.ts
  40. 1 2
      src/core/hook/use-interactive.ts
  41. 35 21
      src/core/hook/use-mouse-status.ts
  42. 228 0
      src/core/hook/use-selection.ts
  43. 96 0
      src/core/hook/use-status.ts
  44. 10 5
      src/core/hook/use-transformer.ts
  45. 1 1
      src/core/hook/use-viewer.ts
  46. 10 6
      src/core/propertys/hover-operate.vue
  47. 2 1
      src/core/propertys/mount.vue
  48. 10 2
      src/core/renderer/renderer.vue
  49. 1 1
      src/deconstruction.d.ts
  50. 15 0
      src/utils/shared.ts

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
public/icons/state_s.svg


+ 1 - 0
src/constant/index.ts

@@ -1,3 +1,4 @@
+import { reactive } from "vue"
 
 export const DomMountId =  'dom-mount'
 export const DomOutMountId =  'dom-out-mount'

+ 3 - 5
src/core/components/arrow/arrow.vue

@@ -18,7 +18,7 @@
 
 <script lang="ts" setup>
 import { PropertyUpdate, Operate } from "../../propertys";
-import { ArrowData, getMouseStyle, defaultStyle } from "./index.ts";
+import { ArrowData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { TempComponent } from "./";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { Line } from "konva/lib/shapes/Line";
@@ -51,8 +51,7 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus<
   getMouseStyle,
   defaultStyle,
   alignment(data, mat) {
-    data.points = data.points.map((p) => mat.point(p));
-    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+    return matResponse({ mat, data, increment: true });
   },
   transformType: "line",
   getRepShape(): Line {
@@ -64,8 +63,7 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus<
     });
   },
   copyHandler(tf, data) {
-    data.points = data.points.map((v) => tf.point(v));
-    return data;
+    return matResponse({ mat: tf, data, increment: true });
   },
   propertys: [
     "fill",

+ 19 - 1
src/core/components/arrow/index.ts

@@ -4,7 +4,8 @@ import { ArrowConfig } from "konva/lib/shapes/Arrow";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { Transform } from "konva/lib/Util";
 
 export { default as Component } from "./arrow.vue";
 export { default as TempComponent } from "./temp-arrow.vue";
@@ -81,3 +82,20 @@ export const interactiveFixData: InteractiveFix<"arrow"> = ({ data, info }) => {
   data.points = [...info.consumed, info.cur!];
   return data;
 };
+
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'arrow'>) => {
+  let transfrom: Transform
+  const attitude = new Transform(data.attitude);
+  if (!increment) {
+    const inverMat = attitude.copy().invert();
+    transfrom = mat.copy().multiply(inverMat);
+  } else {
+    transfrom = mat
+  }
+
+  data.points = data.points.map((v) => transfrom.point(v));
+  data.attitude = transfrom.copy().multiply(attitude).m;
+  return data;
+}

+ 0 - 38
src/core/components/bg-image/bg-image.vue

@@ -1,38 +0,0 @@
-<template>
-  <TempImage :data="tData" :ref="(e: any) => shape = e?.shape" />
-  <Operate :target="shape" :menus="operateMenus" />
-</template>
-
-<script lang="ts" setup>
-import TempImage from "./temp-bg-image.vue";
-import { BGImageData, getMouseStyle, defaultStyle } from "./index.ts";
-import { useComponentStatus } from "@/core/hook/use-component.ts";
-import { Operate } from "../../propertys/index.ts";
-import { Transform } from "konva/lib/Util";
-
-const props = defineProps<{ data: BGImageData }>();
-const emit = defineEmits<{
-  (e: "updateShape", value: BGImageData): void;
-  (e: "addShape", value: BGImageData): void;
-  (e: "delShape"): void;
-}>();
-
-const { shape, tData, operateMenus } = useComponentStatus({
-  emit,
-  props,
-  getMouseStyle,
-  defaultStyle,
-  copyHandler(tf, data) {
-    data.mat = tf.multiply(new Transform(data.mat)).m;
-    return data;
-  },
-  alignment(data, mat) {
-    data.mat = mat.multiply(new Transform(props.data.mat)).m;
-  },
-  propertys: [
-    // "stroke", "strokeWidth",
-    // "opacity",
-    //  "ref", "zIndex"
-  ],
-});
-</script>

+ 0 - 73
src/core/components/bg-image/index.ts

@@ -1,73 +0,0 @@
-import { Transform } from "konva/lib/Util";
-import {
-  BaseItem,
-  generateSnapInfos,
-  getBaseItem,
-  getRectSnapPoints,
-} from "../util.ts";
-import { imageInfo } from "@/utils/resource.ts";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
-import { Size } from "@/utils/math.ts";
-
-export { default as Component } from "./bg-image.vue";
-export { default as TempComponent } from "./temp-bg-image.vue";
-
-export const shapeName = "背景图";
-export const defaultStyle = {};
-
-export const addMode = "dot";
-
-export const getMouseStyle = () => {
-  return {
-    default: {},
-    hover: {},
-    press: {},
-  };
-};
-
-export const getSnapInfos = (data: BGImageData) => {
-  return generateSnapInfos(getSnapPoints(data), true, false);
-};
-
-export const getSnapPoints = (data: BGImageData) => {
-  const tf = new Transform(data.mat);
-  const useData = data.width && data.height;
-  if (!useData && !(data.url in imageInfo)) {
-    return [];
-  }
-
-  const w = useData ? data.width : imageInfo[data.url].width;
-  const h = useData ? data.height : imageInfo[data.url].height;
-  const points = getRectSnapPoints(w, h);
-  return points.map((v) => tf.point(v));
-};
-
-export type BGImageData = Partial<typeof defaultStyle> &
-  BaseItem & Size & {
-    cornerRadius: number;
-    url: string;
-    mat: number[];
-  };
-
-export const interactiveToData: InteractiveTo<"bgImage"> = ({
-  info,
-  preset = {},
-  ...args
-}) => {
-  if (info.cur) {
-    return interactiveFixData({
-      ...args,
-      info,
-      data: { ...getBaseItem(), ...preset } as BGImageData,
-    });
-  }
-};
-
-export const interactiveFixData: InteractiveFix<"bgImage"> = ({
-  data,
-  info,
-}) => {
-  const mat = new Transform().translate(info.cur!.x, info.cur!.y);
-  data.mat = mat.m;
-  return data;
-};

+ 0 - 75
src/core/components/bg-image/temp-bg-image.vue

@@ -1,75 +0,0 @@
-<template>
-  <v-group :config="groupConfig" v-if="groupConfig" ref="shape">
-    <v-image
-      :config="{
-        ...data,
-        ...config,
-        zIndex: undefined,
-      }"
-      v-if="image"
-    />
-  </v-group>
-</template>
-
-<script lang="ts" setup>
-import { defaultStyle, BGImageData } from "./index.ts";
-import { computed, ref, watch } from "vue";
-import { getImage } from "@/utils/resource.ts";
-import { useResize } from "@/core/hook/use-event.ts";
-import { Transform } from "konva/lib/Util";
-import { Group } from "konva/lib/Group";
-import { DC } from "@/deconstruction.js";
-
-const props = defineProps<{ data: BGImageData; addMode?: boolean }>();
-const data = computed(() => ({ ...defaultStyle, ...props.data }));
-const image = ref<HTMLImageElement | null>(null);
-const shape = ref<DC<Group>>();
-
-defineExpose({
-  get shape() {
-    return shape.value;
-  },
-});
-
-watch(
-  () => data.value.url,
-  async (url) => {
-    image.value = null;
-    image.value = await getImage(url);
-  },
-  { immediate: true }
-);
-
-const size = useResize();
-const config = computed(() => {
-  let w = data.value.width;
-  let h = data.value.height;
-
-  // 认为是百分比
-  if (image.value && size.value && (w <= 1 || h <= 1)) {
-    w = w <= 1 ? size.value.width * w : w;
-    h = h <= 1 ? size.value.height * h : h;
-    w = w || (image.value.width / image.value.height) * h;
-    h = h || (image.value.height / image.value.width) * w;
-  }
-  w = w || image.value?.width || 0;
-  h = h || image.value?.height || 0;
-
-  return {
-    image: image.value,
-    opacity: props.addMode ? 0.3 : data.value.opacity,
-    width: w,
-    height: h,
-    offset: {
-      x: w / 2,
-      y: h / 2,
-    },
-  };
-});
-
-const groupConfig = computed(() => {
-  return {
-    ...new Transform(data.value.mat).decompose(),
-  };
-});
-</script>

+ 5 - 26
src/core/components/circle/circle.vue

@@ -16,12 +16,11 @@
 </template>
 
 <script lang="ts" setup>
-import { CircleData, getMouseStyle, defaultStyle } from "./index.ts";
+import { CircleData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempCircle from "./temp-circle.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { Transform } from "konva/lib/Util";
-import { MathUtils } from "three";
 import { cloneRepShape, useCustomTransformer } from "@/core/hook/use-transformer.ts";
 import { Ellipse } from "konva/lib/shapes/Ellipse";
 import { Pos } from "@/utils/math.ts";
@@ -34,22 +33,6 @@ const emit = defineEmits<{
   (e: "delShape"): void;
 }>();
 
-const matToData = (data: CircleData, mat: Transform, initRadius?: Pos) => {
-  if (!initRadius) {
-    initRadius = {
-      x: data.radiusX,
-      y: data.radiusY,
-    };
-  }
-  const dec = mat.decompose();
-  data.radiusY = dec.scaleY * initRadius.y;
-  data.radiusX = dec.scaleX * initRadius.x;
-  data.mat = new Transform()
-    .translate(dec.x, dec.y)
-    .rotate(MathUtils.degToRad(dec.rotation)).m;
-  return data;
-};
-
 const updateContent = (val: string) => {
   data.value.content = val;
   emit("updateShape", { ...data.value });
@@ -64,9 +47,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
   getMouseStyle,
   defaultStyle,
   alignment: (data, mat) => {
-    const tf = shape.value!.getNode().getTransform();
-    mat.multiply(tf);
-    return matToData(data, mat);
+    return matResponse({ data, mat: mat.multiply(new Transform(data.mat)) });
   },
   transformType: "custom",
   customTransform(callback, shape, data) {
@@ -81,19 +62,17 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
         };
       },
       beforeHandler(data, mat) {
-        return matToData(copy(data), mat);
+        return matResponse({ data: copy(data), mat });
       },
       handler(data, mat) {
-        matToData(data, mat, initRadius);
+        matResponse({ data, mat }, initRadius);
         return true;
       },
       callback,
     });
   },
   copyHandler(mat, data) {
-    const tf = (shape.value!.getNode() as any).findOne(".repShape").getTransform();
-    mat.multiply(tf);
-    return matToData({ ...data }, mat);
+    return matResponse({ data: { ...data }, mat: mat.multiply(new Transform(data.mat)) });
   },
   propertys: [
     "fill",

+ 24 - 2
src/core/components/circle/index.ts

@@ -7,9 +7,10 @@ import {
   getRectSnapPoints,
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { lineCenter } from "@/utils/math.ts";
+import { lineCenter, Pos } from "@/utils/math.ts";
 import { Transform } from "konva/lib/Util";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { MathUtils } from "three";
 
 export { default as Component } from "./circle.vue";
 export { default as TempComponent } from "./temp-circle.vue";
@@ -99,3 +100,24 @@ export const interactiveFixData: InteractiveFix<'circle'> = ({
   data.radiusY = sy
   return data;
 };
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'circle'>, initRadius?: Pos) => {
+if (!initRadius) {
+    initRadius = {
+      x: data.radiusX,
+      y: data.radiusY,
+    };
+  }
+  if (increment) {
+    mat = mat.copy().multiply(new Transform(data.mat))
+  }
+  const dec = mat.decompose();
+  data.radiusY = dec.scaleY * initRadius.y;
+  data.radiusX = dec.scaleX * initRadius.x;
+  
+  data.mat = new Transform()
+    .translate(dec.x, dec.y)
+    .rotate(MathUtils.degToRad(dec.rotation)).m;
+  return data;
+}

+ 101 - 0
src/core/components/group/group.vue

@@ -0,0 +1,101 @@
+<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>
+
+<script lang="ts" setup>
+import TempGroup from "./temp-group.vue";
+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 { 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 { 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";
+
+const props = defineProps<{ data: GroupData; pixel?: boolean }>();
+const emit = defineEmits<{
+  (e: "updateShape", value: GroupData): void;
+  (e: "addShape", value: GroupData): void;
+  (e: "delShape"): void;
+}>();
+
+const store = useStore();
+const history = useHistory();
+const layer = useFormalLayer();
+const { shape, tData } = useComponentStatus<Rect, GroupData>({
+  emit,
+  props,
+  getMouseStyle,
+  transformType: "custom",
+  customTransform(callback, shape, data) {
+    let prevMat: Transform;
+    let transformResolve: () => void;
+    useCustomTransformer(shape, data, {
+      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 update = () => {
+          repShape.x($shape.x());
+          repShape.y($shape.y());
+          repShape.width($shape.width());
+          repShape.height($shape.height());
+          repShape.scale($shape.scale());
+          repShape.rotation($shape.rotation());
+        };
+        update();
+        return {
+          shape: repShape,
+          // update,
+        };
+      },
+      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");
+          });
+        }
+        return true;
+      },
+      start() {
+        history.onceTrack(
+          () => new Promise<void>((resolve) => (transformResolve = resolve))
+        );
+      },
+      callback() {
+        transformResolve();
+        callback();
+      },
+    });
+  },
+  defaultStyle,
+  propertys: [],
+});
+
+const status = useMouseShapeStatus(shape);
+const shapesStatus = useMouseShapesStatus();
+</script>

+ 80 - 0
src/core/components/group/index.ts

@@ -0,0 +1,80 @@
+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 { Transform } from "konva/lib/Util";
+import { components } from "../index";
+
+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,
+  opacity: 0.8,
+  dash: [10, 10],
+};
+
+export const addMode = "dots";
+
+export const getMouseStyle = (data: GroupData) => {
+  const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
+  return {
+    default: {
+      stroke: strokeStatus.pub,
+    },
+    hover: { stroke: strokeStatus.hover },
+    select: { stroke: strokeStatus.select },
+    press: { stroke: strokeStatus.press },
+  };
+};
+
+export type GroupData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    ids: string[];
+  };
+
+export const dataToConfig = (data: GroupData) => ({
+  ...defaultStyle,
+  ...data,
+});
+
+export const getSnapInfos = () => [];
+
+export const getSnapPoints = () => [];
+
+export const interactiveToData: InteractiveTo<"group"> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
+  if (info.cur) {
+    return interactiveFixData({
+      ...args,
+      info,
+      data: { ...getBaseItem(), ...preset, ids: [] } as GroupData,
+    });
+  }
+};
+
+export const interactiveFixData: InteractiveFix<"group"> = ({ data }) => {
+  return data;
+};
+
+
+
+export const matResponse = ({data, mat, store}: MatResponseProps<'group'>, prevMat?: Transform) => {
+  if (!store) return;
+
+  const incMat = prevMat ? mat.copy().multiply(prevMat.invert()) : mat;
+  prevMat = mat;
+  for (const id of data.ids) {
+    const type = store.getType(id);
+    if (!type) continue;
+    const item = store.getItem(type, id);
+    if (!item) continue;
+    components[type].matResponse({ data: item as any, mat: incMat, increment: true });
+    store.setItem(type, { value: item, id: item.id });
+  }
+}

+ 83 - 0
src/core/components/group/temp-group.vue

@@ -0,0 +1,83 @@
+<template>
+  <v-rect
+    :config="{ ...data, ...rectConfig, 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 { 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";
+
+const props = defineProps<{ data: GroupData; addMode?: boolean; pixel?: boolean }>();
+const shape = ref<DC<Rect>>();
+useDashAnimation(shape);
+const data = computed(() => ({ ...defaultStyle, ...props.data }));
+
+const invMat = useViewerInvertTransform();
+const rectConfig = ref<RectConfig>();
+const formatLayer = useFormalLayer();
+
+const updateBound = () => {
+  if (!data.value.ids.length || !formatLayer.value) {
+    rectConfig.value = undefined;
+    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}`);
+    if (!shape) continue;
+
+    const rect = shape.getClientRect();
+    if (rect.x < lx) {
+      lx = rect.x;
+    }
+    if (rect.y < ly) {
+      ly = rect.y;
+    }
+    if (rect.x + rect.width > rx) {
+      rx = rect.x + rect.width;
+    }
+    if (rect.y + rect.height > ry) {
+      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 = {
+    x: lx,
+    y: ly,
+    width: rx - lx,
+    height: ry - ly,
+  };
+  const $shape = shape.value?.getNode();
+  if ($shape) {
+    $shape.rotation(0);
+    $shape.scaleX(1);
+    $shape.scaleY(1);
+    nextTick(() => $shape.fire("bound-change"));
+  }
+};
+
+watch(() => data.value.ids.length, updateBound, { immediate: true });
+console.log(data.value.ids.length);
+
+defineExpose({
+  get shape() {
+    return shape.value;
+  },
+  updateBound,
+});
+</script>

+ 4 - 5
src/core/components/icon/icon.vue

@@ -11,7 +11,7 @@
 
 <script lang="ts" setup>
 import TempIcon from "./temp-icon.vue";
-import { IconData, getMouseStyle, defaultStyle } from "./index.ts";
+import { IconData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { PropertyUpdate, Operate, mergeDescribes } from "../../propertys";
 import { Transform } from "konva/lib/Util";
@@ -33,11 +33,10 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   transformType: props.data.fixScreen ? undefined : "mat",
   defaultStyle,
   alignment(data, mat) {
-    data.mat = mat.multiply(new Transform(props.data.mat)).m;
+    return matResponse({ data, mat, increment: true });
   },
-  copyHandler(tf, data) {
-    data.mat = tf.multiply(new Transform(data.mat)).m;
-    return data;
+  copyHandler(mat, data) {
+    return matResponse({ data, mat, increment: true });
   },
   propertys: ["fill", "stroke", "strokeWidth", "dash", "opacity", "strokeScaleEnabled"],
 });

+ 9 - 1
src/core/components/icon/index.ts

@@ -8,7 +8,7 @@ import {
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { FixScreen } from "@/utils/bound.ts";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { Size } from "@/utils/math.ts";
 
 export { default as Component } from "./icon.vue";
@@ -109,3 +109,11 @@ export const interactiveFixData: InteractiveFix<"icon"> = ({
   }
   return data;
 };
+
+
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'icon'>) => {
+  data.mat = increment ? mat.copy().multiply(new Transform(data.mat)).m : mat.m;
+  return data;
+}

+ 4 - 6
src/core/components/image/image.vue

@@ -11,10 +11,9 @@
 
 <script lang="ts" setup>
 import TempImage from "./temp-image.vue";
-import { ImageData, getMouseStyle, defaultStyle } from "./index.ts";
+import { ImageData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
-import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: ImageData }>();
 const emit = defineEmits<{
@@ -30,11 +29,10 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   transformType: "mat",
   defaultStyle,
   alignment(data, mat) {
-    data.mat = mat.multiply(new Transform(props.data.mat)).m;
+    return matResponse({ data, mat, increment: true });
   },
-  copyHandler(tf, data) {
-    data.mat = tf.multiply(new Transform(data.mat)).m;
-    return data;
+  copyHandler(mat, data) {
+    return matResponse({ data, mat, increment: true });
   },
   propertys: [
     // "stroke", "strokeWidth",

+ 7 - 1
src/core/components/image/index.ts

@@ -7,7 +7,7 @@ import {
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { imageInfo } from "@/utils/resource.ts";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { Size } from "@/utils/math.ts";
 
 export { default as Component } from "./image.vue";
@@ -79,3 +79,9 @@ export const interactiveFixData: InteractiveFix<"image"> = ({ data, info }) => {
   data.mat = mat.m;
   return data;
 };
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'image'>) => {
+  data.mat = increment ? mat.copy().multiply(new Transform(data.mat)).m : mat.m;
+  return data;
+}

+ 14 - 4
src/core/components/index.ts

@@ -7,9 +7,9 @@ import * as line from "./line";
 import * as text from "./text";
 import * as icon from "./icon";
 import * as image from "./image";
-import * as bgImage from "./bg-image";
 import * as table from "./table";
 import * as serial from "./serial";
+import * as group from "./group";
 
 import { ArrowData } from "./arrow";
 import { TableData } from "./table";
@@ -21,13 +21,14 @@ import { LineData } from "./line";
 import { TextData } from "./text";
 import { IconData } from "./icon";
 import { ImageData } from "./image";
-import { BGImageData } from "./bg-image";
 import { SerialData } from "./serial";
+import { GroupData } from "./group";
 import { Pos } from "@/utils/math";
 import { AddMessage } from "../hook/use-draw";
 import { Transform } from "konva/lib/Util";
 import { DrawStore } from "../store";
 import { DrawHistory } from "../hook/use-history";
+import { TransformerVectorType } from "../hook/use-transformer";
 
 const _components = {
   arrow,
@@ -39,9 +40,9 @@ const _components = {
   text,
   icon,
   image,
-  bgImage,
   table,
   serial,
+  group
 };
 
 export const components = _components as Components
@@ -67,9 +68,9 @@ export type DrawDataItem = {
   text: TextData;
   icon: IconData;
   image: ImageData;
-  bgImage: BGImageData;
   table: TableData;
   serial: SerialData;
+  group: GroupData
 };
 export type ShapeType = keyof DrawDataItem;
 
@@ -104,3 +105,12 @@ export type InteractiveFix<T extends ShapeType> = (args: {
   store: DrawStore;
   history: DrawHistory;
 }) => DrawItem<T>;
+
+export type MatResponseProps<T extends ShapeType> = {
+  data: DrawItem<T>, 
+  mat: Transform, 
+  increment?: boolean,
+  operType?: TransformerVectorType
+  store?: DrawStore
+  
+}

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

@@ -2,7 +2,8 @@ import { Pos } from "@/utils/math.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { Transform } from "konva/lib/Util";
 
 export { default as Component } from "./line.vue";
 export { default as TempComponent } from "./temp-line.vue";
@@ -64,3 +65,20 @@ export const interactiveFixData: InteractiveFix<"line"> = ({ data, info }) => {
   data.points = [...info.consumed, info.cur!];
   return data;
 };
+
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'line'>) => {
+  let transfrom: Transform
+  const attitude = new Transform(data.attitude);
+  if (!increment) {
+    const inverMat = attitude.copy().invert();
+    transfrom = mat.copy().multiply(inverMat);
+  } else {
+    transfrom = mat
+  }
+
+  data.points = data.points.map((v) => transfrom.point(v));
+  data.attitude = transfrom.copy().multiply(attitude).m;
+  return data;
+}

+ 4 - 7
src/core/components/line/line.vue

@@ -17,7 +17,7 @@
 </template>
 
 <script lang="ts" setup>
-import { LineData, getMouseStyle, defaultStyle } from "./index.ts";
+import { LineData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempLine from "./temp-line.vue";
@@ -25,7 +25,6 @@ import { EditPen } from "@element-plus/icons-vue";
 import { useInteractiveDrawShapeAPI } from "@/core/hook/use-draw.ts";
 import { useStore } from "@/core/store/index.ts";
 import { Pos } from "@/utils/math.ts";
-import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: LineData }>();
 const emit = defineEmits<{
@@ -40,14 +39,12 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   getMouseStyle,
   transformType: "line",
   alignment(data, mat) {
-    data.points = data.points.map((p) => mat.point(p));
-    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+    return matResponse({ mat, data, increment: true });
   },
   // type: "line",
   defaultStyle,
-  copyHandler(tf, data) {
-    data.points = data.points.map((v) => tf.point(v));
-    return data;
+  copyHandler(mat, data) {
+    return matResponse({ mat, data, increment: true });
   },
   propertys: [
     "stroke",

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

@@ -2,7 +2,8 @@ import { Pos } from "@/utils/math.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { Transform } from "konva/lib/Util";
 
 export { default as Component } from "./polygon.vue";
 export { default as TempComponent } from "./temp-polygon.vue";
@@ -73,3 +74,19 @@ export const interactiveFixData: InteractiveFix<"polygon"> = ({
   data.points = [...info.consumed, info.cur!];
   return data;
 };
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'polygon'>) => {
+  let transfrom: Transform
+  const attitude = new Transform(data.attitude);
+  if (!increment) {
+    const inverMat = attitude.copy().invert();
+    transfrom = mat.copy().multiply(inverMat);
+  } else {
+    transfrom = mat
+  }
+
+  data.points = data.points.map((v) => transfrom.point(v));
+  data.attitude = transfrom.copy().multiply(attitude).m;
+  return data;
+}

+ 4 - 7
src/core/components/polygon/polygon.vue

@@ -17,7 +17,7 @@
 </template>
 
 <script lang="ts" setup>
-import { PolygonData, getMouseStyle, defaultStyle } from "./index.ts";
+import { PolygonData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { PropertyUpdate, Operate } from "../../propertys/index.ts";
 import TempLine from "./temp-polygon.vue";
@@ -25,7 +25,6 @@ import { useInteractiveDrawShapeAPI } from "@/core/hook/use-draw.ts";
 import { useStore } from "@/core/store/index.ts";
 import { EditPen } from "@element-plus/icons-vue";
 import { Pos } from "@/utils/math.ts";
-import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: PolygonData }>();
 const emit = defineEmits<{
@@ -41,12 +40,10 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   transformType: "line",
   defaultStyle,
   alignment(data, mat) {
-    data.points = data.points.map((p) => mat.point(p));
-    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+    return matResponse({ mat, data, increment: true });
   },
-  copyHandler(tf, data) {
-    data.points = data.points.map((v) => tf.point(v));
-    return data;
+  copyHandler(mat, data) {
+    return matResponse({ mat, data, increment: true });
   },
   propertys: [
     "fill",

+ 18 - 1
src/core/components/rectangle/index.ts

@@ -3,7 +3,8 @@ import { themeMouseColors } from "@/constant/help-style.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { onlyId } from "@/utils/shared.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { Transform } from "konva/lib/Util";
 
 export { default as Component } from "./rectangle.vue";
 export { default as TempComponent } from "./temp-rectangle.vue";
@@ -91,3 +92,19 @@ export const interactiveFixData: InteractiveFix<"rectangle"> = ({
   }
   return data;
 };
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'rectangle'>) => {
+  let transfrom: Transform
+  const attitude = new Transform(data.attitude);
+  if (!increment) {
+    const inverMat = attitude.copy().invert();
+    transfrom = mat.copy().multiply(inverMat);
+  } else {
+    transfrom = mat
+  }
+
+  data.points = data.points.map((v) => transfrom.point(v));
+  data.attitude = transfrom.copy().multiply(attitude).m;
+  return data;
+}

+ 5 - 8
src/core/components/rectangle/rectangle.vue

@@ -15,11 +15,10 @@
 </template>
 
 <script lang="ts" setup>
-import { RectangleData, getMouseStyle, defaultStyle } from "./index.ts";
+import { RectangleData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempLine from "./temp-rectangle.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
-import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: RectangleData }>();
 const emit = defineEmits<{
@@ -30,7 +29,7 @@ const emit = defineEmits<{
 
 const updateContent = (val: string) => {
   data.value.content = val;
-  emit("updateShape", {...data.value});
+  emit("updateShape", { ...data.value });
 };
 
 const { shape, tData, data, operateMenus, describes } = useComponentStatus({
@@ -40,12 +39,10 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   defaultStyle,
   transformType: "line",
   alignment(data, mat) {
-    data.points = data.points.map((p) => mat.point(p));
-    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+    return matResponse({ mat, data, increment: true });
   },
-  copyHandler(tf, data) {
-    data.points = data.points.map((v) => tf.point(v));
-    return data;
+  copyHandler(mat, data) {
+    return matResponse({ mat, data, increment: true });
   },
   propertys: [
     "fill",

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

@@ -1,8 +1,10 @@
 import { getBaseItem } from "../util.ts";
 import { Transform } from "konva/lib/Util";
 import { CircleData, defaultStyle } from "../circle";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { DrawStore } from "@/core/store/index.ts";
+import { Pos } from "@/utils/math.ts";
+import { MathUtils } from "three";
 
 export {
   defaultStyle,
@@ -69,3 +71,26 @@ export const interactiveFixData: InteractiveFix<"serial"> = ({
   data.radiusY = radius;
   return data;
 };
+
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'serial'>, initRadius?: Pos) => {
+  if (!initRadius) {
+    initRadius = {
+      x: data.radiusX,
+      y: data.radiusY,
+    };
+  }
+  if (increment) {
+    mat = mat.copy().multiply(new Transform(data.mat))
+  }
+  const dec = mat.decompose();
+  data.radiusX = data.radiusY = dec.scaleY * initRadius.y;
+
+  const w = (data.radiusX * 2) / Math.sqrt(2);
+  data.fontSize = getSerialFontSizeByFontW(data, w);
+  data.mat = new Transform()
+    .translate(dec.x, dec.y)
+    .rotate(MathUtils.degToRad(dec.rotation)).m;
+  return data;
+}

+ 4 - 32
src/core/components/serial/serial.vue

@@ -15,17 +15,11 @@
 </template>
 
 <script lang="ts" setup>
-import {
-  SerialData,
-  getMouseStyle,
-  defaultStyle,
-  getSerialFontSizeByFontW,
-} from "./index.ts";
+import { SerialData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys/index.ts";
 import { TempComponent } from "./";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { Transform } from "konva/lib/Util";
-import { MathUtils } from "three";
 import { cloneRepShape, useCustomTransformer } from "@/core/hook/use-transformer.ts";
 import { Ellipse } from "konva/lib/shapes/Ellipse";
 import { Pos } from "@/utils/math.ts";
@@ -37,24 +31,6 @@ const emit = defineEmits<{
   (e: "delShape"): void;
 }>();
 
-const matToData = (data: SerialData, mat: Transform, initRadius?: Pos) => {
-  if (!initRadius) {
-    initRadius = {
-      x: data.radiusX,
-      y: data.radiusY,
-    };
-  }
-  const dec = mat.decompose();
-  data.radiusX = data.radiusY = dec.scaleY * initRadius.y;
-
-  const w = (data.radiusX * 2) / Math.sqrt(2);
-  data.fontSize = getSerialFontSizeByFontW(data, w);
-  data.mat = new Transform()
-    .translate(dec.x, dec.y)
-    .rotate(MathUtils.degToRad(dec.rotation)).m;
-  return data;
-};
-
 const updateContent = (val: string) => {
   data.value.content = val;
   emit("updateShape", { ...data.value });
@@ -69,9 +45,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
   getMouseStyle,
   defaultStyle,
   alignment: (data, mat) => {
-    const tf = shape.value!.getNode().getTransform();
-    mat.multiply(tf);
-    return matToData(data, mat);
+    return matResponse({ mat: mat.multiply(new Transform(data.mat)), data });
   },
   transformType: "custom",
   customTransform(callback, shape, data) {
@@ -89,7 +63,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
       //   return matToData(copy(data), mat);
       // },
       handler(data, mat) {
-        matToData(data, mat, initRadius);
+        matResponse({ data, mat }, initRadius);
         return true;
       },
       callback,
@@ -100,9 +74,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
     });
   },
   copyHandler(mat, data) {
-    const tf = (shape.value!.getNode() as any).findOne(".repShape").getTransform();
-    mat.multiply(tf);
-    return matToData({ ...data }, mat);
+    return matResponse({ data, mat: mat.multiply(new Transform(data.mat)) });
   },
   propertys: [
     "fill",

+ 4 - 2
src/core/components/share/edit-polygon.vue

@@ -1,5 +1,5 @@
 <template>
-  <template v-if="status.hover && canEdit">
+  <template v-if="status.hover && canEdit && !operMode.mulSelection">
     <EditLine
       :data="data"
       :points="data.points"
@@ -17,7 +17,7 @@
     />
   </template>
 
-  <template v-if="status.hover || status.active || addMode">
+  <template v-if="(status.hover || status.active || addMode) && !operMode.mulSelection">
     <Point
       v-for="(_, ndx) in data.points"
       :size="data.strokeWidth + 6"
@@ -41,6 +41,7 @@ import { DC, EntityShape } from "@/deconstruction.js";
 import { Pos } from "@/utils/math.ts";
 import { useMouseShapeStatus } from "@/core/hook/use-mouse-status";
 import { computed } from "vue";
+import { useOperMode } from "@/core/hook/use-status";
 
 const props = defineProps<{
   data: Required<Pick<LineData, "id" | "points" | "stroke" | "strokeWidth">>;
@@ -55,4 +56,5 @@ const emit = defineEmits<{
 }>();
 
 const status = useMouseShapeStatus(computed(() => props.shape));
+const operMode = useOperMode();
 </script>

+ 1 - 1
src/core/components/share/text.vue

@@ -14,7 +14,7 @@
 import { useMouseShapesStatus } from "@/core/hook/use-mouse-status";
 import { DC } from "@/deconstruction";
 import { Text, TextConfig } from "konva/lib/shapes/Text";
-import { computed, ref, watchEffect } from "vue";
+import { computed, ref } from "vue";
 import TextDom from "./text-area.vue";
 
 const props = withDefaults(

+ 102 - 31
src/core/components/table/index.ts

@@ -7,8 +7,10 @@ import {
   getRectSnapPoints,
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
-import { Size } from "@/utils/math.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { numEq, Size } from "@/utils/math.ts";
+import { copy } from "@/utils/shared.ts";
+import { MathUtils } from "three";
 
 export { default as Component } from "./table.vue";
 export { default as TempComponent } from "./temp-table.vue";
@@ -32,16 +34,18 @@ export const defaultCollData = {
 
 export const addMode = "area";
 
-export type TableCollData = Partial<typeof defaultCollData> & Size & {
-  content: string;
-  padding: number
-  readonly?: boolean
-  notdel?: boolean
-};
+export type TableCollData = Partial<typeof defaultCollData> &
+  Size & {
+    content: string;
+    padding: number;
+    readonly?: boolean;
+    notdel?: boolean;
+  };
 export type TableData = Partial<typeof defaultStyle> &
-  BaseItem & Size & {
-    notaddRow?: boolean
-    notaddCol?: boolean
+  BaseItem &
+  Size & {
+    notaddRow?: boolean;
+    notaddCol?: boolean;
     mat: number[];
     content: TableCollData[][];
   };
@@ -59,9 +63,10 @@ export const getMouseStyle = (data: TableData) => {
 
 export const getSnapPoints = (data: TableData) => {
   const tf = new Transform(data.mat);
-  const points = getRectSnapPoints(data.width, data.height, 0, 0)
-    .map((v) => tf.point(v));
-  return points
+  const points = getRectSnapPoints(data.width, data.height, 0, 0).map((v) =>
+    tf.point(v)
+  );
+  return points;
 };
 
 export const getSnapInfos = (data: TableData) => {
@@ -88,22 +93,22 @@ export const autoCollHeight = 50;
 export const interactiveFixData: InteractiveFix<"table"> = ({
   data,
   info,
-  notdraw
+  notdraw,
 }) => {
   if (info.cur) {
     const area = info.cur!;
     const origin = {
       x: Math.min(area[0].x, area[1].x),
       y: Math.min(area[0].y, area[1].y),
-    }
-    data.width = Math.abs(area[0].x - area[1].x)
-    data.height = Math.abs(area[0].y - area[1].y)
-    
+    };
+    data.width = Math.abs(area[0].x - area[1].x);
+    data.height = Math.abs(area[0].y - area[1].y);
+
     if (!notdraw || !(data.content?.length && data.content[0].length)) {
       const colNum = Math.floor(data.width / autoCollWidth) || 1;
       const rawNum = Math.floor(data.height / autoCollHeight) || 1;
       const temp = data.content?.[0]?.[0] || {
-        content: ""
+        content: "",
       };
 
       data.content = Array.from({ length: rawNum }, () =>
@@ -111,22 +116,88 @@ export const interactiveFixData: InteractiveFix<"table"> = ({
           ...temp,
           width: data.width / colNum,
           height: data.height / rawNum,
-          padding: 8
+          padding: 8,
         }))
       );
     } else {
-      const colHeight = data.height / data.content.length
-      const colWidth = data.width / data.content[0].length
-      data.content.forEach(row => {
-        row.forEach(col => {
-          col.width = colWidth
-          col.height = colHeight
-          col.padding = 8
-          console.log(col.content)
-        })
-      })
+      const colHeight = data.height / data.content.length;
+      const colWidth = data.width / data.content[0].length;
+      data.content.forEach((row) => {
+        row.forEach((col) => {
+          col.width = colWidth;
+          col.height = colHeight;
+          col.padding = 8;
+          console.log(col.content);
+        });
+      });
     }
     data.mat = new Transform().translate(origin.x, origin.y).m;
   }
   return data;
 };
+
+export const getColMinSize = (col: TableCollData) => {
+  const minw = (col.padding || 0) * 2 + (col.fontSize || 12) + 4;
+  const minh = (col.padding || 0) * 2 + (col.fontSize || 12) + 4;
+  return { w: minw, h: minh };
+};
+
+export const matResponse = (
+  { data, mat, operType, increment }: MatResponseProps<"table">,
+  initData?: TableData
+) => {
+  if (!initData) {
+    initData = copy(data);
+  }
+  if (increment) {
+    mat = mat.copy().multiply(new Transform(data.mat))
+  }
+
+  const dec = mat.decompose();
+  const oldData = copy(data);
+  data.height = dec.scaleY * initData.height;
+  data.width = dec.scaleX * initData.width;
+
+  let w = 0;
+  let h = 0;
+  data.content.forEach((row, rndx) => {
+    row.forEach((col, cndx) => {
+      const initCol = initData.content[rndx][cndx];
+      const minSize = getColMinSize(initCol);
+      col.width = Math.max(
+        minSize.w,
+        data.width * (initCol.width / initData.width)
+      );
+      col.height = Math.max(
+        minSize.h,
+        data.height * (initCol.height / initData.height)
+      );
+      if (rndx === 0) {
+        w += col.width;
+      }
+      if (cndx === 0) {
+        h += col.height;
+      }
+    });
+  });
+  const eqW = numEq(w, data.width);
+  const eqH = numEq(h, data.height);
+
+  if (!eqW || !eqH) {
+    if (operType) {
+      Object.assign(data, oldData);
+    } else {
+      data.width = w;
+      data.height = h;
+      const initDec = new Transform(initData.mat).decompose();
+      data.mat = new Transform()
+        .translate(eqW ? dec.x : initDec.x, eqH ? dec.y : initDec.y)
+        .rotate(MathUtils.degToRad(dec.rotation)).m;
+    }
+  } else {
+    data.mat = new Transform()
+      .translate(dec.x, dec.y)
+      .rotate(MathUtils.degToRad(dec.rotation)).m;
+  }
+  return data;
+};

+ 11 - 7
src/core/components/table/table.vue

@@ -22,7 +22,13 @@
 
 <script lang="ts" setup>
 import TempTable from "./temp-table.vue";
-import { TableData, getMouseStyle, defaultStyle, TableCollData } from "./index.ts";
+import {
+  TableData,
+  getMouseStyle,
+  defaultStyle,
+  TableCollData,
+  matResponse,
+} from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { Transform } from "konva/lib/Util";
@@ -178,6 +184,7 @@ const matToData = (data: TableData, mat: Transform, initData?: TableData) => {
     }
     return data;
   }
+
   const initDec = new Transform(initData.mat).decompose();
   const move = new Transform().rotate(MathUtils.degToRad(-dec.rotation)).point({
     x: dec.x - initDec.x,
@@ -248,10 +255,9 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
   getMouseStyle,
   defaultStyle,
   alignment: (data, mat) => {
-    const tf = shape.value!.getNode().getTransform();
-    mat.multiply(tf);
-    matToData(data, mat);
+    matResponse({ data, mat, increment: true });
     sync(data);
+    return data;
   },
   transformType: "custom",
   customTransform(callback, shape, data) {
@@ -279,9 +285,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
     });
   },
   copyHandler(mat, data) {
-    const tf = shape.value!.getNode().getTransform();
-    mat.multiply(tf);
-    return matToData({ ...data }, mat);
+    return matResponse({ data, mat, increment: true });
   },
   propertys: [
     "fill",

+ 36 - 2
src/core/components/text/index.ts

@@ -9,7 +9,9 @@ import {
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { shallowReactive } from "vue";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { zeroEq } from "@/utils/math.ts";
+import { MathUtils } from "three";
 
 export { default as Component } from "./text.vue";
 export { default as TempComponent } from "./temp-text.vue";
@@ -44,7 +46,7 @@ export const getMouseStyle = (data: TextData) => {
     default: { fill: fillStatus.pub },
     hover: { fill: fillStatus.hover },
     press: { fill: fillStatus.press },
-    select: { select: fillStatus.select },
+    select: { fill: fillStatus.select },
   };
 };
 
@@ -84,3 +86,35 @@ export const interactiveFixData: InteractiveFix<"text"> = ({ data, info }) => {
   data.mat = mat.m;
   return data;
 };
+
+export const getMinWidth = (data: TextData) =>  (data.fontSize || 12) * 2
+
+
+export const getWidth = (data: TextData, scaleX: number) => {
+  const minWidth = getMinWidth(data)
+  let width: number;
+  if ("width" in data) {
+    width = Math.max(data.width! * scaleX, minWidth);
+  } else {
+    width = Math.max(minWidth * scaleX, minWidth);
+  }
+  return width;
+};
+
+
+export const matResponse = ({ data, mat, increment }: MatResponseProps<"text">) => {
+  if (increment) {
+    mat = mat.copy().multiply(new Transform(data.mat))
+  }
+  const { scaleX, x, y, rotation } = mat.decompose();
+  if (!zeroEq(scaleX - 1)) {
+    data.width = getWidth(data, scaleX)
+    data.mat = new Transform()
+    .translate(x, y)
+    .rotate(MathUtils.degToRad(rotation))
+    .scale(1, 1).m
+  } else {
+    data.mat = mat.m
+  }
+  return data
+};

+ 13 - 37
src/core/components/text/text.vue

@@ -15,17 +15,22 @@
 </template>
 
 <script lang="ts" setup>
-import { TextData, getMouseStyle, defaultStyle } from "./index.ts";
+import {
+  TextData,
+  getMouseStyle,
+  defaultStyle,
+  matResponse,
+  getWidth,
+  getMinWidth,
+} from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempText from "./temp-text.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { cloneRepShape, useCustomTransformer } from "@/core/hook/use-transformer.ts";
 import { Transform } from "konva/lib/Util";
 import { Text } from "konva/lib/shapes/Text";
-import { computed, ref, watch } from "vue";
+import { watch } from "vue";
 import { setShapeTransform } from "@/utils/shape.ts";
-import { zeroEq } from "@/utils/math.ts";
-import { MathUtils } from "three";
 
 const props = defineProps<{ data: TextData }>();
 const emit = defineEmits<{
@@ -61,19 +66,18 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
         rotateEnabled: true,
         enabledAnchors: ["middle-left", "middle-right"],
         boundBoxFunc: (oldBox, newBox) => {
-          if (newBox.width - minWidth.value < -0.01) {
+          if (newBox.width - getMinWidth(data.value) < -0.01) {
             return oldBox;
           }
           return newBox;
         },
       },
       beforeHandler(data, mat) {
-        return { ...data, ...update(mat, data) };
+        return matResponse({ data, mat });
       },
       handler(data, mat) {
-        const setAttrib = update(mat, data);
-        Object.assign(data, setAttrib);
-        if (setAttrib.width) {
+        const a = matResponse({ mat, data: { ...data } });
+        if (a.width) {
           return true;
         }
       },
@@ -100,34 +104,6 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
   ],
 });
 
-const minWidth = computed(() => (data.value.fontSize || 12) * 2);
-const getWidth = (data: TextData, scaleX: number) => {
-  let width: number;
-  if ("width" in data) {
-    width = Math.max(data.width! * scaleX, minWidth.value);
-  } else {
-    width = Math.max(shape.value!.getNode()!.width() * scaleX, minWidth.value);
-  }
-  return width;
-};
-
-const update = (mat: Transform, data: TextData) => {
-  const { scaleX, x, y, rotation } = mat.decompose();
-  if (!zeroEq(scaleX - 1)) {
-    return {
-      width: getWidth(data, scaleX),
-      mat: new Transform()
-        .translate(x, y)
-        .rotate(MathUtils.degToRad(rotation))
-        .scale(1, 1).m,
-    };
-  } else {
-    return {
-      mat: mat.m,
-    };
-  }
-};
-
 // 字体大小变化时,更新width
 watch(
   () => data.value.fontSize,

+ 18 - 1
src/core/components/triangle/index.ts

@@ -2,7 +2,8 @@ import { Pos } from "@/utils/math.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
+import { Transform } from "konva/lib/Util";
 
 export { default as Component } from "./triangle.vue";
 export { default as TempComponent } from "./temp-triangle.vue";
@@ -80,3 +81,19 @@ export const interactiveFixData: InteractiveFix<"triangle"> = ({
   data.points[2] = area[1];
   return data;
 };
+
+
+export const matResponse = ({data, mat, increment}: MatResponseProps<'triangle'>) => {
+  let transfrom: Transform
+  const attitude = new Transform(data.attitude);
+  if (!increment) {
+    const inverMat = attitude.copy().invert();
+    transfrom = mat.copy().multiply(inverMat);
+  } else {
+    transfrom = mat
+  }
+
+  data.points = data.points.map((v) => transfrom.point(v));
+  data.attitude = transfrom.copy().multiply(attitude).m;
+  return data;
+}

+ 4 - 7
src/core/components/triangle/triangle.vue

@@ -16,11 +16,10 @@
 </template>
 
 <script lang="ts" setup>
-import { TriangleData, getMouseStyle, defaultStyle } from "./index.ts";
+import { TriangleData, getMouseStyle, defaultStyle, matResponse } from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempLine from "./temp-triangle.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
-import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: TriangleData }>();
 const emit = defineEmits<{
@@ -40,12 +39,10 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus({
   defaultStyle,
   transformType: "line",
   alignment(data, mat) {
-    data.points = data.points.map((p) => mat.point(p));
-    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+    return matResponse({ mat, data, increment: true });
   },
-  copyHandler(tf, data) {
-    data.points = data.points.map((v) => tf.point(v));
-    return data;
+  copyHandler(mat, data) {
+    return matResponse({ mat, data, increment: true });
   },
   propertys: [
     "fill",

+ 2 - 2
src/core/hook/use-alignment.ts

@@ -7,7 +7,8 @@ import {
   useTempLayer,
 } from "./use-layer";
 import { clickListener, listener } from "@/utils/event";
-import { useCursor, useOperMode, useMode, useStage } from "./use-global-vars";
+import { useCursor, useStage } from "./use-global-vars";
+import { useMode, useOperMode } from './use-status'
 import { mergeFuns } from "@/utils/shared";
 import { getLineRelationMat, Pos } from "@/utils/math";
 import { Circle } from "konva/lib/shapes/Circle";
@@ -134,7 +135,6 @@ export const useJoinShapes = (
       try {
         await new Promise<void>((resolve, reject) => {
           let isSure = false;
-          console.log(dom)
           const stopClick = clickListener(dom, () => {
             if (!points.value[i] || operMode.value.freeView) return;
             stopMove();

+ 3 - 9
src/core/hook/use-draw.ts

@@ -1,12 +1,6 @@
 import { computed, h, nextTick, reactive, ref, watch, watchEffect } from "vue";
-import {
-  installGlobalVar,
-  useCan,
-  useCursor,
-  useOperMode,
-  useMode,
-  useStage,
-} from "./use-global-vars";
+import { installGlobalVar, useCursor, useStage } from "./use-global-vars";
+import { useCan, useMode, useOperMode } from "./use-status";
 import {
   Area,
   InteractiveHook,
@@ -180,7 +174,7 @@ const usePointBeforeHandler = (enableTransform = false, enableSnap = false) => {
     transform: (p: SnapPoint, geo = [p], geoNeedConversion = true) => {
       p = conversionPosition(p);
       if (operMode.value.freeDraw) {
-        return p
+        return p;
       }
       snap && snap.clear();
       if (geoNeedConversion) {

+ 6 - 1
src/core/hook/use-event.ts

@@ -2,6 +2,7 @@ import { Size } from "@/utils/math.ts";
 import { listener } from "../../utils/event.ts";
 import { installGlobalVar, useStage } from "./use-global-vars.ts";
 import { nextTick, ref, watchEffect } from "vue";
+import { KonvaEventObject } from "konva/lib/Node";
 
 export const useListener = <
   T extends HTMLElement,
@@ -20,7 +21,6 @@ export const useListener = <
     if (stage.value) {
       const $stage = stage.value!.getStage();
       const dom = $stage.container() as any;
-      console.log(dom)
       onCleanup(
         listener(target || dom, eventName, function (ev) {
           callback.call(this, ev, dom);
@@ -153,3 +153,8 @@ export const usePreemptiveListener = installGlobalVar(() => {
 
   return on;
 }, Symbol("preemptiveListener"));
+
+export const cancelBubbleEvent = (ev: KonvaEventObject<any>) => {
+  ev.cancelBubble = true
+  ev.evt.stopPropagation
+}

+ 4 - 4
src/core/hook/use-expose.ts

@@ -3,10 +3,9 @@ import {
   useCursor,
   useInstanceProps,
   useLayers,
-  useMode,
-  useOperMode,
   useStage,
 } from "./use-global-vars.ts";
+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 +23,7 @@ import { themeColor } from "@/constant/help-style.ts";
 import { isSvgString } from "@/utils/resource.ts";
 import { useResourceHandler } from "./use-fetch.ts";
 import { useConfig } from "./use-config.ts";
+import { useSelectionRevise } from "./use-selection.ts";
 
 // 自动粘贴服务
 export const useAutoPaste = () => {
@@ -70,13 +70,11 @@ export const useShortcutKey = () => {
     },
     document.documentElement
   );
-  console.error('a?')
 
   const history = useHistory()
   const status = useMouseShapesStatus()
   const store = useStore()
   useListener('keydown', (ev) => {
-    console.log(ev.key)
     if (ev.key === 'z' && ev.ctrlKey) {
       history.hasUndo.value && history.undo();
     } else if (ev.key === 'y' && ev.ctrlKey) {
@@ -164,6 +162,8 @@ export const useAutoService = () => {
   watchEffect((onCleanup) => {
     onCleanup(mergeFuns(init(instanceProps.get().id)));
   });
+
+  useSelectionRevise()
 };
 
 

+ 19 - 88
src/core/hook/use-global-vars.ts

@@ -16,7 +16,6 @@ import {
   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";
@@ -154,80 +153,6 @@ export const useStage = installGlobalVar(
   () => shallowRef<DC<Stage> | undefined>(),
   Symbol("stage")
 );
-export const useMode = installGlobalVar(() => {
-  const stack = stackVar(new Set([Mode.write]));
-  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.static));
-
-  // 可以进入拖拽模式
-  const dragMode = computed(() => {
-    if (!mouse.value || mode.include(Mode.readonly) || key.has(" "))
-      return false;
-    return mode.include(Mode.draw) || 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.write) || mode.include(Mode.update))
-  );
-
-  // 可以进入编辑模式
-  const editMode = computed(() => mouse.value && mode.include(Mode.write));
-
-  // 可以进入绘制模式
-  const drawMode = computed(() => mouse.value && mode.include(Mode.write));
-
-  return reactive({
-    viewMouseReact: mouse,
-    viewMode,
-    drawMode,
-    mouseReact,
-    editMode,
-    dragMode,
-  });
-});
 
 export const usePointerPos = installGlobalVar(() => {
   const stage = useStage();
@@ -301,19 +226,6 @@ export const useDownKeys = installGlobalVar(() => {
   };
 });
 
-export const useOperMode = installGlobalVar(() => {
-  const keys = useDownKeys()
-
-  return computed(() => ({
-    // 多选模式
-    mulSelection: keys.has('Shift'),
-    // 自由移动视图
-    freeView: keys.has(' '),
-    // 自由绘图,不吸附
-    freeDraw: keys.has('Control')
-  }))
-})
-
 export const useLayers = () => {
   const stage = useStage();
   return computed(() => stage.value?.getNode().children as Layer[]);
@@ -328,3 +240,22 @@ export const useCursor = installGlobalVar(
   () => stackVar("default"),
   Symbol("cursor")
 );
+
+
+export type MPart = {comp: any, props: any}
+export const useMountParts = installGlobalVar(() => {
+  const mParts = reactive<MPart[]>([])
+  const del = (part: MPart) => {
+    const ndx = mParts.indexOf(part)
+    ~ndx && mParts.splice(ndx, 1)
+  }
+
+  return {
+    value: mParts,
+    add(part: MPart) {
+      mParts.push(part)
+      return () => del(part)
+    },
+    del
+  }
+})

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

@@ -20,9 +20,9 @@ export class DrawHistory {
     return this.list.find(item => item.id === this.currentId)?.data
   }
 
-  preventFlag = 0;
-  onceFlag = 0;
-  onceHistory: string | null = null;
+  private preventFlag = 0;
+  private onceFlag = 0;
+  private onceHistory: string | null = null;
   initData: string | null = null;
   clearData: string = ''
   renderer: (data: HistoryItem) => void;
@@ -33,6 +33,9 @@ export class DrawHistory {
     pushed: void;
     redo: void;
     undo: void;
+    init: void;
+    clear: void;
+    currentChange: void
   }>();
   private pushAttachs: Record<string, any> = {};
 
@@ -71,12 +74,20 @@ export class DrawHistory {
     this.push(data);
   }
 
-  preventTrack(fn: () => void) {
-    this.preventFlag++
-    fn();
+  private preventTrackCallback() {
     this.preventFlag--
   }
 
+  preventTrack(fn: () => any) {
+    this.preventFlag++
+    const result = fn();
+    if (result instanceof Promise) {
+      result.then(() => this.preventTrackCallback())
+    } else {
+      this.preventTrackCallback()
+    }
+  }
+
   private saveKeyPrev = '__history__'
   private saveKeyId: string | null = null
   get saveKey() {
@@ -141,6 +152,7 @@ export class DrawHistory {
   redo() {
     const data = this.history.redo();
     this.bus.emit("redo");
+    this.bus.emit('currentChange')
     this.renderer(data);
     return data;
   }
@@ -148,13 +160,12 @@ export class DrawHistory {
   undo() {
     const data = this.history.undo();
     this.bus.emit("undo");
+    this.bus.emit('currentChange')
     this.renderer(data);
     return data;
   }
 
-  onceTrack(fn: () => void) {
-    this.onceFlag++
-    fn();
+  private onceTrackCallback() {
     this.onceFlag--
     if (this.onceHistory) {
       this.push(this.onceHistory);
@@ -162,6 +173,16 @@ export class DrawHistory {
     }
   }
 
+  onceTrack(fn: () => any) {
+    this.onceFlag++
+    const result = fn();
+    if (result instanceof Promise) {
+      result.then(() => this.onceTrackCallback())
+    } else {
+      this.onceTrackCallback()
+    }
+  }
+
   clearCurrent(): void {
     this.renderer({ data: this.clearData, attachs: "" });
     this.push(this.clearData);
@@ -170,12 +191,16 @@ export class DrawHistory {
   clear(): void {
     this.history.reset()
     this.clearCurrent()
+    this.bus.emit('clear')
+    this.bus.emit('currentChange')
   }
   
   init() {
     if (this.initData) {
       this.renderer({ data: this.initData, attachs: "" });
       this.push(this.initData);
+      this.bus.emit('init')
+      this.bus.emit('currentChange')
     }
   }
 }

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

@@ -1,9 +1,8 @@
 import {
   installGlobalVar,
-  useCan,
-  useMode,
   useStage,
 } from "./use-global-vars.ts";
+import { useCan, useMode } from './use-status'
 import { DrawItem, ShapeType } from "../components";
 import { reactive, Ref, ref, watch, watchEffect } from "vue";
 import { Pos } from "../../utils/math.ts";

+ 35 - 21
src/core/hook/use-mouse-status.ts

@@ -4,11 +4,10 @@ import { Shape } from "konva/lib/Shape";
 import {
   globalWatch,
   installGlobalVar,
-  useCan,
-  useOperMode,
   useStage,
   useTransformIngShapes,
 } from "./use-global-vars.ts";
+import { useCan, useOperMode } from "./use-status";
 import { Stage } from "konva/lib/Stage";
 import { listener } from "../../utils/event.ts";
 import { inRevise, mergeFuns } from "../../utils/shared.ts";
@@ -23,6 +22,7 @@ import { KonvaEventObject } from "konva/lib/Node";
 import { useStore } from "../store/index.ts";
 import { Group } from "konva/lib/Group";
 import { usePause } from "./use-pause.ts";
+import { lineLen, Pos } from "@/utils/math.ts";
 
 const stageHoverMap = new WeakMap<
   Stage,
@@ -96,7 +96,7 @@ export const useShapeIsHover = (shape: Ref<DC<EntityShape> | undefined>) => {
     { immediate: true }
   );
   const result = usePause([isHover, stop] as const);
-  return result
+  return result;
 };
 
 export const useShapeIsTransformerInner = () => {
@@ -162,16 +162,15 @@ export const useMouseShapesStatus = installGlobalVar(() => {
     );
 
     let downTime: number;
+    let downPos: Pos | null = null
     stage.on("pointerdown.mouse-status", (ev) => {
+      downPos = {x: ev.evt.pageX, y: ev.evt.pageY}
       downTime = Date.now();
       if (prevent.value) return;
       const target = shapeTreeContain(listeners.value, ev.target);
       if (target && !press.value.includes(target)) {
         press.value.push(target);
       }
-      if (!operMode.value.mulSelection)  {
-        selects.value = []
-      } 
       downTarget = target;
     });
 
@@ -182,38 +181,53 @@ 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
           if (prevent.value) return;
           press.value = [];
-          if (Date.now() - downTime > 300) return;
-          if (!downTarget || ev.button !== 0)  {
-            actives.value = []
-            selects.value = []
+          if (Date.now() - downTime > 300 || isMove) return;
+
+          if (!target || ev.button !== 0) {
+            actives.value = [];
+            if (!isMove) {
+              selects.value = [];
+            }
             return;
           }
 
-          if (operMode.value.mulSelection)  {
-            const ndx = selects.value.indexOf(downTarget);
+          if (operMode.value.mulSelection) {
+            const ndx = selects.value.findIndex(
+              (item) => item.id() === target?.id()
+            );
             if (~ndx) {
               selects.value.splice(ndx, 1);
             } else {
-              selects.value.push(downTarget);
+              selects.value.push(target);
             }
-            actives.value = []
-            
+            actives.value = [];
           } else {
-            actives.value = [downTarget];
+            actives.value = [target];
+            selects.value = [];
+          }
+        }
+      ),
+      listener(
+        stage.container().parentElement as HTMLDivElement,
+        "pointermove",
+        async (ev) => {
+          if (prevent.value) return;
+          if (downTarget && !operMode.value.mulSelection) {
             selects.value = []
           }
-          console.log(selects.value)
         }
       ),
       () => {
         listeners.value.forEach((shape) => {
           shape.off("pointerleave.mouse-status");
         });
-        stage.off(
-          "pointerenter.mouse-status pointerdown.mouse-status pointereup.mouse-status pointermove.mouse-status"
-        );
+        stage.off("pointerdown.mouse-status");
         hovers.value = [];
         actives.value = [];
         press.value = [];
@@ -362,7 +376,7 @@ export const useMouseStyle = <T extends ShapeType>(
     ) {
       styleMap.set(mouseStyle.value.drag, true);
     }
-    if ('select' in mouseStyle.value) {
+    if ("select" in mouseStyle.value) {
       styleMap.set(mouseStyle.value.select, status.value.select);
     }
 

+ 228 - 0
src/core/hook/use-selection.ts

@@ -0,0 +1,228 @@
+import { Rect } from "konva/lib/shapes/Rect";
+import {
+  globalWatch,
+  globalWatchEffect,
+  installGlobalVar,
+  useMountParts,
+  useStage,
+} from "./use-global-vars";
+import { 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 { EntityShape } from "@/deconstruction";
+import { Util } from "konva/lib/Util";
+import { useViewerInvertTransform, useViewerTransform } from "./use-viewer";
+import { 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";
+
+export const useSelection = installGlobalVar(() => {
+  const layer = useHelperLayer();
+  const formatLayer = useFormalLayer();
+  const box = new Rect({
+    stroke: themeColor,
+    strokeWidth: 1,
+    fill: "#fff",
+    listening: false,
+    opacity: 0.5,
+  });
+  const stage = useStage();
+  const operMode = useOperMode();
+  const selections = ref<EntityShape[]>();
+  const viewMat = useViewerTransform();
+  const store = useStore();
+
+  let shapeBoxs: IRect[] = [];
+  let shpaes: EntityShape[] = []
+
+  const init = (dom: HTMLDivElement, layer: Layer) => {
+    const stopListener = dragListener(dom, {
+      down(pos) {
+        layer.add(box);
+        box.x(pos.x);
+        box.y(pos.y);
+        box.width(0);
+        box.height(0);
+      },
+      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]);
+          }
+        }
+      },
+      up() {
+        selections.value = undefined;
+        box.remove();
+      },
+    });
+    return () => {
+      stopListener();
+      box.remove();
+    };
+  };
+
+  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());
+        }
+      }
+    )
+  );
+
+  return {
+    onDestroy: stopWatch,
+    var: selections,
+  };
+});
+
+export const useSelectionShowIcons = installGlobalVar(() => {
+  const mParts = useMountParts();
+  const iconProps = {
+    width: 20,
+    height: 20,
+    zIndex: 99999,
+    url: "/icons/state_s.svg",
+    fill: themeColor,
+    stroke: "#fff",
+  };
+
+  const status = useMouseShapesStatus();
+  const formatLayer = useFormalLayer();
+  const mouseSelects = computed(() => {
+    const selectShapes = status.selects.filter((shape) =>
+      formatLayer.value?.children.includes(shape)
+    );
+    return selectShapes;
+  });
+
+  watchEffect(() => {
+    if (mouseSelects.value.length !== status.selects.length) {
+      status.selects = mouseSelects.value;
+    }
+  });
+
+  const rectSelects = useSelection();
+  let initSelections: EntityShape[] = [];
+  watch(
+    () => rectSelects.value && [...rectSelects.value],
+    (rectSelects, oldRectSelects) => {
+      if (!oldRectSelects) {
+        initSelections = [...mouseSelects.value];
+      } else if (!rectSelects) {
+        initSelections = [];
+      } else {
+        status.selects = Array.from(
+          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 groupConfig = {
+    id: onlyId(),
+    createTime: Date.now(),
+    zIndex: 9999,
+    lock: false,
+    opacity: 1,
+    ref: false,
+    listening: false,
+  };
+  const status = useMouseShapesStatus();
+  const operMode = useOperMode();
+  const layer = useFormalLayer();
+  watch(
+    () => [!!ids.value.length, operMode.value.mulSelection],
+    () => {
+      const groupShape = layer.value?.findOne<Group>(`#${groupConfig.id}`);
+      if (!groupShape) return;
+      if (ids.value.length && !operMode.value.mulSelection) {
+        status.actives = [groupShape];
+      } else if (status.actives.includes(groupShape)) {
+        status.actives = [];
+      }
+    }
+  );
+  
+
+  watchEffect((onCleanup) => {
+    if (ids.value.length) {
+      onCleanup(
+        mParts.add({
+          comp: markRaw(GroupComp),
+          props: { data: { ...groupConfig, ids: ids.value } },
+        })
+      );
+    }
+  });
+};

+ 96 - 0
src/core/hook/use-status.ts

@@ -0,0 +1,96 @@
+import { computed, reactive } from "vue";
+import { installGlobalVar, stackVar, useDownKeys, useStage } from "./use-global-vars";
+import { Mode } from "@/constant/mode";
+
+
+export const useMode = installGlobalVar(() => {
+  const stack = stackVar(new Set([Mode.write]));
+  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 operMode = useOperMode()
+  const stage = useStage();
+  const key = useDownKeys();
+  const loaded = computed(() => !!stage.value?.getStage());
+
+  // 鼠标是否可用
+  const mouse = computed(() => loaded.value && !mode.include(Mode.static));
+
+  // 可以进入拖拽模式
+  const dragMode = computed(() => {
+    if (!mouse.value || mode.include(Mode.readonly) || key.has(" "))
+      return false;
+    return mode.include(Mode.draw) || mode.include(Mode.update);
+  });
+
+  // 是否在视图模式
+  const viewMode = computed(() => {
+    return mouse.value && ((!mode.include(Mode.draging) && !operMode.value.mulSelection) || key.has(" "));
+  });
+
+  // shape是否可以对鼠标做出反应
+  const mouseReact = computed(
+    () => mouse.value && (mode.include(Mode.write) || mode.include(Mode.update))
+  );
+
+  // 可以进入编辑模式
+  const editMode = computed(() => mouse.value && mode.include(Mode.write) && !operMode.value.mulSelection);
+
+  // 可以进入绘制模式
+  const drawMode = computed(() => mouse.value && mode.include(Mode.write) && !operMode.value.mulSelection);
+
+  return reactive({
+    viewMouseReact: mouse,
+    viewMode,
+    drawMode,
+    mouseReact,
+    editMode,
+    dragMode,
+  });
+});
+
+
+export const useOperMode = installGlobalVar(() => {
+  const keys = useDownKeys()
+
+  return computed(() => ({
+    // 多选模式
+    mulSelection: keys.has('Shift') && !keys.has(' '),
+    // 自由移动视图
+    freeView: keys.has(' '),
+    // 自由绘图,不吸附
+    freeDraw: keys.has('Control')
+  }))
+})

+ 10 - 5
src/core/hook/use-transformer.ts

@@ -1,19 +1,18 @@
-import { useMouseShapeStatus, useShapeIsHover } from "./use-mouse-status.ts";
+import { useMouseShapeStatus } from "./use-mouse-status.ts";
 import { Ref, ref, watch } from "vue";
 import { DC, EntityShape } from "../../deconstruction";
 import {
   installGlobalVar,
-  useCan,
-  useMode,
   useStage,
   useTransformIngShapes,
 } from "./use-global-vars.ts";
+import { useCan, useMode } from './use-status'
 import { Mode } from "../../constant/mode.ts";
 import { Transform, Util } 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, 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";
@@ -279,6 +278,7 @@ export const useShapeTransformer = <T extends EntityShape>(
       };
     }
 
+    const set = frameEebounce((appleTransform: Transform | undefined) => transform.value = appleTransform)
     const updateTransform = () => {
       if (!can.dragMode) return;
       let appleTransform = rep.tempShape.getTransform().copy();
@@ -289,7 +289,7 @@ export const useShapeTransformer = <T extends EntityShape>(
         appleTransform = handlerTransform(appleTransform);
         setShapeTransform(rep.tempShape, appleTransform);
       }
-      transform.value = appleTransform;
+      set(appleTransform)
     };
 
     rep.tempShape.on("transform.shapemer", updateTransform);
@@ -494,6 +494,7 @@ export type CustomTransformerProps<
 > = {
   openSnap?: boolean;
   getRepShape?: GetRepShape<S, T>;
+  start?: () => void;
   beforeHandler?: (data: T, mat: Transform) => T;
   handler?: (
     data: T,
@@ -534,6 +535,10 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
   );
   let callMat: Transform;
   watch(transform, (current, oldTransform) => {
+    if (!oldTransform) {
+      props.start && props.start()
+    }
+
     if (current) {
       if (!handler) return;
       const snapData = props.beforeHandler

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

@@ -4,9 +4,9 @@ import { dragListener, scaleListener } from "../../utils/event.ts";
 import {
   globalWatch,
   installGlobalVar,
-  useCan,
   useStage,
 } from "./use-global-vars.ts";
+import { useCan } from './use-status'
 import { mergeFuns } from "../../utils/shared.ts";
 import { Transform } from "konva/lib/Util";
 import { lineLen } from "@/utils/math.ts";

+ 10 - 6
src/core/propertys/hover-operate.vue

@@ -27,7 +27,8 @@
 
 <script lang="ts" setup>
 import { computed, nextTick, ref, watch, watchEffect } from "vue";
-import { useMode, useStage } from "../hook/use-global-vars.ts";
+import { useStage } from "../hook/use-global-vars.ts";
+import { useMode } from "../hook/use-status.ts";
 import { DC, EntityShape } from "@/deconstruction.js";
 import { useViewerTransformConfig } from "../hook/use-viewer.ts";
 import { Transform } from "konva/lib/Util";
@@ -43,7 +44,7 @@ const props = defineProps<{
   menus: Array<{ icon?: any; label?: string; handler: () => void }>;
 }>();
 const emit = defineEmits<{
-  (e: "show" | 'hide'): void;
+  (e: "show" | "hide"): void;
 }>();
 
 const layout = ref<HTMLDivElement>();
@@ -57,7 +58,7 @@ const hasRClick = (ev: MouseEvent) => {
   const shape = props.target?.getNode();
   const pos = stage.value?.getNode().pointerPos;
   if (!shape || !pos || !layer.value) return false;
-  let clickShape = layer.value.getIntersection(pos);
+  let clickShape = stage.value?.getNode().getIntersection(pos);
   return !!clickShape && shapeTreeContain(shape, clickShape) === shape;
 };
 
@@ -150,9 +151,12 @@ const calcPointer = async () => {
   move.translate(x, y);
   pointer.value = `matrix(${move.m.join(",")})`;
 };
-watch(() => !!pointer.value, (show) => {
-  emit(show ? 'show' : 'hide')
-})
+watch(
+  () => !!pointer.value,
+  (show) => {
+    emit(show ? "show" : "hide");
+  }
+);
 
 let timeout: any;
 const resetPointer = () => {

+ 2 - 1
src/core/propertys/mount.vue

@@ -24,7 +24,8 @@
 
 <script lang="ts" setup>
 import { computed, ref, watch } from "vue";
-import { useMode, useStage } from "../hook/use-global-vars.ts";
+import { useStage } from "../hook/use-global-vars.ts";
+import { useMode } from "../hook/use-status.ts";
 import { PropertyDescribes, propertyComponents } from "./index.ts";
 import { DC, EntityShape } from "@/deconstruction.js";
 import { useMouseShapeStatus } from "../hook/use-mouse-status.ts";

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

@@ -23,6 +23,11 @@
             :type="type"
             :key="type"
           />
+          <component
+            v-for="part in mountParts.value"
+            :is="part.comp"
+            v-bind="part.props"
+          />
         </v-layer>
         <!--	临时组,提供临时绘画,以及高频率渲染	-->
         <v-layer :config="viewerConfig" id="temp">
@@ -37,6 +42,7 @@
           <Compass v-if="config.showCompass" />
           <Debugger v-if="isDev" />
           <Border />
+          <!-- <MultipleSelection /> -->
         </v-layer>
       </v-stage>
     </div>
@@ -48,7 +54,6 @@ import ShapeGroup from "./group.vue";
 import Back from "../helper/back.vue";
 import Border from "../helper/facade.vue";
 import TempShapeGroup from "./draw-group.vue";
-import ActiveBoxs from "../helper/active-boxs.vue";
 import SnapLines from "../helper/snap-lines.vue";
 import BackGrid from "../helper/back-grid.vue";
 import SplitLine from "../helper/split-line.vue";
@@ -59,9 +64,10 @@ import {
   InstanceProps,
   useCursor,
   useInstanceProps,
-  useMode,
+  useMountParts,
   useStage,
 } from "../hook/use-global-vars.ts";
+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";
@@ -83,6 +89,8 @@ onUnmounted(() => {
   mergeFuns(rendererMap.get(instance)!.unmounteds)();
 });
 
+const mountParts = useMountParts();
+
 const props = defineProps<InstanceProps>();
 const store = useStore();
 watch(

+ 1 - 1
src/deconstruction.d.ts

@@ -5,4 +5,4 @@ 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 }

+ 15 - 0
src/utils/shared.ts

@@ -135,6 +135,21 @@ export const debounce = <T extends (...args: any) => any>(
   };
 };
 
+// 防抖
+export const frameEebounce = <T extends (...args: any) => any>(
+  fn: T,
+) => {
+  let count = 0
+  return function (...args: Parameters<T>) {
+    let current = ++count
+    requestAnimationFrame(() => {
+      if (current === count) {
+        fn.apply(null, args);
+      }
+    })
+  };
+};
+
 /**
  * 获取数据变化
  * @param newIds