Browse Source

feat: 制作新组件,优化操作

bill 5 months ago
parent
commit
d767ab24f2
68 changed files with 2203 additions and 769 deletions
  1. 2 2
      package.json
  2. 9 9
      pnpm-lock.yaml
  3. 1 0
      public/icons/edit_compass.svg
  4. 2 1
      src/constant/index.ts
  5. 17 5
      src/core/components/arrow/arrow.vue
  6. 19 20
      src/core/components/arrow/index.ts
  7. 100 16
      src/core/components/arrow/temp-arrow.vue
  8. 44 39
      src/core/components/bg-image/index.ts
  9. 1 1
      src/core/components/circle/circle.vue
  10. 15 11
      src/core/components/circle/index.ts
  11. 1 1
      src/core/components/circle/temp-circle.vue
  12. 23 18
      src/core/components/icon/icon.vue
  13. 52 30
      src/core/components/icon/index.ts
  14. 16 1
      src/core/components/icon/temp-icon.vue
  15. 44 41
      src/core/components/image/index.ts
  16. 92 62
      src/core/components/index.ts
  17. 27 23
      src/core/components/line/index.ts
  18. 3 2
      src/core/components/line/temp-line.vue
  19. 37 25
      src/core/components/polygon/index.ts
  20. 1 1
      src/core/components/polygon/temp-polygon.vue
  21. 18 16
      src/core/components/rectangle/index.ts
  22. 2 0
      src/core/components/rectangle/temp-rectangle.vue
  23. 71 0
      src/core/components/serial/index.ts
  24. 140 0
      src/core/components/serial/serial-group.vue
  25. 121 0
      src/core/components/serial/serial.vue
  26. 72 0
      src/core/components/serial/temp-serial.vue
  27. 6 9
      src/core/components/share/point.vue
  28. 7 7
      src/core/components/share/text-area.vue
  29. 9 5
      src/core/components/share/text.vue
  30. 44 25
      src/core/components/table/index.ts
  31. 21 10
      src/core/components/table/table.vue
  32. 1 1
      src/core/components/table/temp-table.vue
  33. 37 33
      src/core/components/text/index.ts
  34. 11 10
      src/core/components/triangle/index.ts
  35. 1 0
      src/core/components/util.ts
  36. 45 49
      src/core/helper/back-grid.vue
  37. 29 0
      src/core/helper/back.vue
  38. 54 0
      src/core/helper/facade.vue
  39. 16 4
      src/core/helper/split-line.vue
  40. 62 31
      src/core/hook/use-draw.ts
  41. 39 6
      src/core/hook/use-event.ts
  42. 31 8
      src/core/hook/use-expose.ts
  43. 8 8
      src/core/hook/use-global-vars.ts
  44. 2 2
      src/core/hook/use-history.ts
  45. 1 1
      src/core/hook/use-layer.ts
  46. 97 52
      src/core/hook/use-mouse-status.ts
  47. 0 6
      src/core/hook/use-polygon.ts
  48. 1 1
      src/core/hook/use-snap.ts
  49. 19 12
      src/core/hook/use-transformer.ts
  50. 9 0
      src/core/propertys/describes.json
  51. 2 2
      src/core/propertys/index.ts
  52. 11 5
      src/core/propertys/mount.vue
  53. 2 2
      src/core/renderer/draw-group.vue
  54. 59 25
      src/core/renderer/renderer.vue
  55. 2 1
      src/core/store/index.ts
  56. 11 4
      src/core/store/store.ts
  57. 0 17
      src/example/fuse/views/header/header.vue
  58. 2 4
      src/example/fuse/views/home.vue
  59. 299 2
      src/example/fuse/views/init.ts
  60. 75 0
      src/example/fuse/views/slide/draw-menu.ts
  61. 62 0
      src/example/fuse/views/slide/handler-menu.ts
  62. 17 88
      src/example/fuse/views/slide/menu.ts
  63. 17 4
      src/example/fuse/views/slide/slide.vue
  64. 65 0
      src/example/fuse/views/slide/test-menu.ts
  65. 36 5
      src/example/fuse/views/use-draw.ts
  66. 41 0
      src/utils/bound.ts
  67. 5 5
      src/utils/resource.ts
  68. 17 1
      src/utils/shape.ts

+ 2 - 2
package.json

@@ -15,7 +15,7 @@
     "@types/three": "^0.169.0",
     "element-plus": "^2.8.6",
     "html2canvas": "^1.4.1",
-    "konva": "^9.3.16",
+    "konva": "^9.3.18",
     "localforage": "^1.10.0",
     "mitt": "^3.0.1",
     "pinia": "^2.2.4",
@@ -25,7 +25,7 @@
     "uuid": "^11.0.2",
     "vite-plugin-html": "^3.2.2",
     "vue": "^3.5.13",
-    "vue-konva": "^3.1.2"
+    "vue-konva": "^3.2.0"
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^5.1.4",

+ 9 - 9
pnpm-lock.yaml

@@ -8,7 +8,7 @@ specifiers:
   '@vitejs/plugin-vue': ^5.1.4
   element-plus: ^2.8.6
   html2canvas: ^1.4.1
-  konva: ^9.3.16
+  konva: ^9.3.18
   localforage: ^1.10.0
   mitt: ^3.0.1
   pinia: ^2.2.4
@@ -21,7 +21,7 @@ specifiers:
   vite: ^5.4.9
   vite-plugin-html: ^3.2.2
   vue: ^3.5.13
-  vue-konva: ^3.1.2
+  vue-konva: ^3.2.0
   vue-tsc: ^2.1.6
 
 dependencies:
@@ -31,7 +31,7 @@ dependencies:
   '@types/three': 0.169.0
   element-plus: 2.8.6_vue@3.5.13
   html2canvas: 1.4.1
-  konva: 9.3.16
+  konva: 9.3.18
   localforage: 1.10.0
   mitt: 3.0.1
   pinia: 2.2.4_egv2ww6vjuchfsqp7qchczie5e
@@ -41,7 +41,7 @@ dependencies:
   uuid: 11.0.2
   vite-plugin-html: 3.2.2_vite@5.4.10
   vue: 3.5.13_typescript@5.6.3
-  vue-konva: 3.1.2_konva@9.3.16+vue@3.5.13
+  vue-konva: 3.2.0_konva@9.3.18+vue@3.5.13
 
 devDependencies:
   '@vitejs/plugin-vue': 5.1.4_vite@5.4.10+vue@3.5.13
@@ -1225,8 +1225,8 @@ packages:
       graceful-fs: 4.2.11
     dev: false
 
-  /konva/9.3.16:
-    resolution: {integrity: sha512-qa47cefGDDHzkToGRGDsy24f/Njrz7EHP56jQ8mlDcjAPO7vkfTDeoBDIfmF7PZtpfzDdooafQmEUJMDU2F7FQ==}
+  /konva/9.3.18:
+    resolution: {integrity: sha512-ad5h0Y9phUrinBrKXyIISbURRHQO7Rx5cz7mAEEfdVCs45gDqRD8Y0I0nJRk8S6iqEbiRE87CEZu5GVSnU8oow==}
     dev: false
 
   /lie/3.1.1:
@@ -1850,14 +1850,14 @@ packages:
       vue: 3.5.13_typescript@5.6.3
     dev: false
 
-  /vue-konva/3.1.2_konva@9.3.16+vue@3.5.13:
-    resolution: {integrity: sha512-/CyZSZjmKNED3Pt4XJFmAwv2PAYKBN1X/6AkYKIj+zSnJGnRZ17S+0Q+UzMgziyZ9Qg6yb25ol8XvkAJckHOCA==}
+  /vue-konva/3.2.0_konva@9.3.18+vue@3.5.13:
+    resolution: {integrity: sha512-n1KcOJDvTsgBRy/9HNAEm+5mNgvIxatImIjeuietH5Qt3yHbIK8mp1sP6TQL+a3Pne45UiMO9W+Gwrq1cjptkw==}
     engines: {node: '>= 4.0.0', npm: '>= 3.0.0'}
     peerDependencies:
       konva: '>7'
       vue: ^3
     dependencies:
-      konva: 9.3.16
+      konva: 9.3.18
       vue: 3.5.13_typescript@5.6.3
     dev: false
 

File diff suppressed because it is too large
+ 1 - 0
public/icons/edit_compass.svg


+ 2 - 1
src/constant/index.ts

@@ -1,2 +1,3 @@
 
-export const DomMountId =  'dom-mount'
+export const DomMountId =  'dom-mount'
+export const DomOutMountId =  'dom-out-mount'

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

@@ -1,5 +1,11 @@
 <template>
-  <TempArrow :data="tData" :ref="(v: any) => shape = v?.shape" :id="data.id" />
+  <TempArrow
+    :data="tData"
+    :ref="(v: any) => shape = v?.shape"
+    :id="data.id"
+    @update:position="updatePosition"
+    @update="emit('updateShape', { ...data })"
+  />
   <PropertyUpdate
     :describes="describes"
     :data="data"
@@ -10,14 +16,15 @@
 </template>
 
 <script lang="ts" setup>
-import { Arrow } from "konva/lib/shapes/Arrow";
 import { PropertyUpdate, Operate } from "../../propertys";
 import { ArrowData, getMouseStyle, defaultStyle } from "./index.ts";
 import TempArrow from "./temp-arrow.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { Line } from "konva/lib/shapes/Line";
 import { Transform } from "konva/lib/Util";
-
+import { Pos } from "@/utils/math.ts";
+import { Group } from "konva/lib/Group";
+import { flatPositions } from "@/utils/shared.ts";
 
 const props = defineProps<{ data: ArrowData }>();
 const emit = defineEmits<{
@@ -26,8 +33,13 @@ const emit = defineEmits<{
   (e: "delShape"): void;
 }>();
 
+const updatePosition = ({ ndx, val }: { ndx: number; val: Pos }) => {
+  Object.assign(data.value.points[ndx], val);
+  shape.value?.getNode().fire("bound-change");
+};
+
 const { shape, tData, operateMenus, describes, data } = useComponentStatus<
-  Arrow,
+  Group,
   ArrowData
 >({
   emit,
@@ -44,7 +56,7 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus<
       fill: "rgb(0, 255, 0)",
       visible: false,
       strokeWidth: 0,
-      points: shape.value!.getNode().points(),
+      points: flatPositions(data.value.points),
     });
   },
   copyHandler(tf, data) {

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

@@ -4,15 +4,15 @@ import { ArrowConfig } from "konva/lib/shapes/Arrow";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { AddMessage } from "@/core/hook/use-draw.ts";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./arrow.vue";
 export { default as TempComponent } from "./temp-arrow.vue";
 
 export enum PointerPosition {
-  start = 'start',
-  end = 'end',
-  all = 'all',
+  start = "start",
+  end = "end",
+  all = "all",
 }
 export const shapeName = "箭头";
 export const defaultStyle = {
@@ -22,7 +22,7 @@ export const defaultStyle = {
   pointerLength: 10,
 };
 
-export const addMode = "area";
+export const addMode = "dots";
 
 export const getMouseStyle = (data: ArrowData) => {
   const strokeStatus = getMouseColors(data.fill || defaultStyle.fill);
@@ -55,30 +55,29 @@ export const getSnapInfos = (data: ArrowData) => {
   return generateSnapInfos(getSnapPoints(data), true, false);
 };
 
-export const getSnapPoints = (data: ArrowData) => data.points
+export const getSnapPoints = (data: ArrowData) => data.points;
 
-export const interactiveToData = (
-  info: AddMessage<'arrow'>,
-  preset: Partial<ArrowData> = {}
-): ArrowData | undefined => {
+export const interactiveToData: InteractiveTo<"arrow"> = ({
+  info,
+  preset,
+  ...args
+}) => {
   if (info.cur) {
-    return interactiveFixData(
-      {
+    return interactiveFixData({
+      ...args,
+      info,
+      data: {
         ...getBaseItem(),
         ...preset,
         id: onlyId(),
-        points: [info.cur[0]],
+        points: [],
         attitude: [1, 0, 0, 1, 0, 0],
       },
-      info
-    );
+    });
   }
 };
 
-export const interactiveFixData = (
-  data: ArrowData,
-  info: AddMessage<'arrow'>
-) => {
-  data.points[1] = info.cur![1];
+export const interactiveFixData: InteractiveFix<"arrow"> = ({ data, info }) => {
+  data.points = [...info.consumed, info.cur!];
   return data;
 };

+ 100 - 16
src/core/components/arrow/temp-arrow.vue

@@ -1,31 +1,84 @@
 <template>
-  <v-arrow
-    :config="{
-      ...data,
-      zIndex: undefined,
-      stroke: data.fill,
-      pointerWidth: data.pointerLength,
-      hitStrokeWidth: 20,
-      closed: true,
-      points: flatPositions(data.points),
-      ...eConfig,
-      opacity: addMode ? 0.3 : data.opacity,
-    }"
-    ref="shape"
-  />
+  <v-group ref="shape" :id="data.id">
+    <v-arrow
+      name="arrow"
+      v-for="(_, ndx) in data.points.length - 1"
+      :config="{
+        ...data,
+        zIndex: undefined,
+        hitFunc: hitFunc,
+        stroke: data.fill,
+        fill: undefined,
+        pointerWidth: data.pointerLength,
+        closed: false,
+        points: flatPositions([data.points[ndx], data.points[ndx + 1]]),
+        ...eConfig,
+        opacity: addMode ? 0.3 : data.opacity,
+      }"
+    />
+
+    <template v-if="status.hover && !status.active">
+      <Point
+        v-for="(p, ndx) in data.points"
+        :id="data.id + ndx"
+        :shapeId="data.id"
+        :position="p"
+        :size="Math.max(data.pointerLength + 4, data.strokeWidth + 6)"
+        :color="data.fill"
+        @update:position="(p) => emit('update:position', { ndx, val: p })"
+        @dragend="endHandler()"
+        @dragstart="startHandler(ndx)"
+      />
+    </template>
+  </v-group>
 </template>
 
 <script lang="ts" setup>
+import Point from "../share/point.vue";
 import { ArrowData, defaultStyle, PointerPosition } from "./index.ts";
 import { DC } from "@/deconstruction.js";
-import { computed, ref } from "vue";
+import { computed, ref, watchEffect } from "vue";
 import { flatPositions } from "@/utils/shared.ts";
 import { Arrow } from "konva/lib/shapes/Arrow";
+import { useMouseShapeStatus } from "@/core/hook/use-mouse-status.ts";
+import { useCustomSnapInfos } from "@/core/hook/use-snap.ts";
+import { ComponentSnapInfo } from "../index.ts";
+import { generateSnapInfos } from "../util.ts";
+import { Pos } from "@/utils/math.ts";
+import { LineConfig } from "konva/lib/shapes/Line";
+import { Group } from "konva/lib/Group";
 
 const props = defineProps<{ data: ArrowData; addMode?: boolean }>();
-const shape = ref<DC<Arrow>>();
+const emit = defineEmits<{
+  (e: "update:position", data: { ndx: number; val: Pos }): void;
+  (e: "update"): void;
+}>();
+
+const shape = ref<DC<Group>>();
 
 const data = computed(() => ({ ...defaultStyle, ...props.data }));
+const hitFunc: LineConfig["hitFunc"] = (con, shape) => {
+  con.beginPath();
+  con.moveTo(data.value.points[0].x, data.value.points[0].y);
+  for (let i = 1; i < data.value.points.length; i++) {
+    con.lineTo(data.value.points[i].x, data.value.points[i].y);
+  }
+  con.closePath();
+  con.fillStrokeShape(shape);
+};
+
+watchEffect(
+  (onCleanup) => {
+    const $shape = shape.value?.getNode();
+    if ($shape) {
+      const timeout = setTimeout(() => {
+        $shape.findOne<Arrow>(".arrow")?.fill(data.value.fill);
+      });
+      onCleanup(() => clearTimeout(timeout));
+    }
+  },
+  { flush: "post" }
+);
 
 const eConfig = computed(() => {
   const position =
@@ -41,6 +94,37 @@ const eConfig = computed(() => {
   };
 });
 
+const infos = useCustomSnapInfos();
+const addedInfos = [] as ComponentSnapInfo[];
+const clearInfos = () => {
+  addedInfos.forEach(infos.remove);
+};
+
+const startHandler = (ndx: number) => {
+  clearInfos();
+  const geos = [
+    props.data.points.slice(0, ndx),
+    props.data.points.slice(ndx + 1, props.data.points.length),
+  ];
+  if (ndx > 0 && ndx < props.data.points.length - 1) {
+    geos.push([props.data.points[ndx - 1], props.data.points[ndx + 1]]);
+  }
+  geos.forEach((geo) => {
+    const snapInfos = generateSnapInfos(geo, true, true, true);
+    snapInfos.forEach((item) => {
+      infos.add(item);
+      addedInfos.push(item);
+    });
+  });
+};
+
+const endHandler = () => {
+  clearInfos();
+  emit("update");
+};
+
+const status = useMouseShapeStatus(shape);
+
 defineExpose({
   get shape() {
     return shape.value;

+ 44 - 39
src/core/components/bg-image/index.ts

@@ -1,69 +1,74 @@
 import { Transform } from "konva/lib/Util";
-import { BaseItem, generateSnapInfos, getBaseItem, getRectSnapPoints } from "../util.ts";
+import {
+  BaseItem,
+  generateSnapInfos,
+  getBaseItem,
+  getRectSnapPoints,
+} from "../util.ts";
 import { imageInfo } from "@/utils/resource.ts";
-import { AddMessage } from "@/core/hook/use-draw.ts";
+import { InteractiveFix, InteractiveTo } from "../index.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 defaultStyle = {};
 
-export const addMode = 'dot'
+export const addMode = "dot";
 
 export const getMouseStyle = () => {
   return {
-    default: { },
-    hover: { },
-    press: { },
+    default: {},
+    hover: {},
+    press: {},
   };
 };
 
-
 export const getSnapInfos = (data: BGImageData) => {
-  return generateSnapInfos(
-    getSnapPoints(data),
-    true,
-    false
-  );
+  return generateSnapInfos(getSnapPoints(data), true, false);
 };
 
 export const getSnapPoints = (data: BGImageData) => {
   const tf = new Transform(data.mat);
-  const useData = data.width && data.height
+  const useData = data.width && data.height;
   if (!useData && !(data.url in imageInfo)) {
-    return []
+    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 & {
-  cornerRadius: number
-  width: number;
-  height: number;
-  url: string;
-  mat: number[]
+  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 const interactiveToData = (
-  info: AddMessage<'image'>,
-  preset: Partial<BGImageData> = {}
-): BGImageData | undefined => {
+export type BGImageData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    cornerRadius: number;
+    width: number;
+    height: number;
+    url: string;
+    mat: number[];
+  };
+
+export const interactiveToData: InteractiveTo<"bgImage"> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
   if (info.cur) {
-    return interactiveFixData({ ...getBaseItem(), ...preset, } as unknown as BGImageData, info);
+    return interactiveFixData({
+      ...args,
+      info,
+      data: { ...getBaseItem(), ...preset } as BGImageData,
+    });
   }
 };
 
-export const interactiveFixData = (
-  data: BGImageData,
-  info: AddMessage<'image'>
-) => {
-  const mat = new Transform().translate(info.cur!.x, info.cur!.y)
-  data.mat = mat.m
+export const interactiveFixData: InteractiveFix<"bgImage"> = ({
+  data,
+  info,
+}) => {
+  const mat = new Transform().translate(info.cur!.x, info.cur!.y);
+  data.mat = mat.m;
   return data;
 };

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

@@ -91,7 +91,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
     });
   },
   copyHandler(mat, data) {
-    const tf = shape.value!.getNode().getTransform();
+    const tf = (shape.value!.getNode() as any).findOne(".repShape").getTransform();
     mat.multiply(tf);
     return matToData({ ...data }, mat);
   },

+ 15 - 11
src/core/components/circle/index.ts

@@ -7,9 +7,9 @@ import {
   getRectSnapPoints,
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { AddMessage } from "@/core/hook/use-draw.ts";
-import { lineCenter, lineLen } from "@/utils/math.ts";
+import { lineCenter } from "@/utils/math.ts";
 import { Transform } from "konva/lib/Util";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./circle.vue";
 export { default as TempComponent } from "./temp-circle.vue";
@@ -23,6 +23,7 @@ export const defaultStyle = {
   align: "center",
   fontStyle: "normal",
   fontColor: themeMouseColors.theme,
+  padding: 8
 };
 
 export const addMode = "area";
@@ -60,6 +61,7 @@ export type CircleData = Partial<typeof defaultStyle> &
     radiusX: number
     radiusY: number
     content?: string;
+    padding?: number
   };
 
 export const dataToConfig = (data: CircleData): CircleConfig => ({
@@ -67,23 +69,25 @@ export const dataToConfig = (data: CircleData): CircleConfig => ({
   ...data,
 });
 
-export const interactiveToData = (
-  info: AddMessage<'circle'>,
-  preset: Partial<CircleData> = {}
-): CircleData | undefined => {
+
+export const interactiveToData: InteractiveTo<'circle'> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
   if (info.cur) {
     const item = {
       ...getBaseItem(),
       ...preset,
     } as unknown as CircleData;
-    return interactiveFixData(item, info);
+    return interactiveFixData({ ...args, info, data: item });
   }
 };
 
-export const interactiveFixData = (
-  data: CircleData,
-  info: AddMessage<'circle'>
-) => {
+export const interactiveFixData: InteractiveFix<'circle'> = ({
+  data,
+  info,
+}) => {
   const area = info.cur!;
   const sx = Math.abs((area[1].x - area[0].x)) / 2
   const sy = Math.abs((area[1].y - area[0].y)) / 2

+ 1 - 1
src/core/components/circle/temp-circle.vue

@@ -49,7 +49,7 @@ const textBound = computed(() => {
     .translate(-width / 2, -height / 2)
     .decompose();
   return {
-    padding: 8,
+    padding: data.value.padding,
     ...matConfig,
     width,
     height,

+ 23 - 18
src/core/components/icon/icon.vue

@@ -13,8 +13,12 @@
 import TempIcon from "./temp-icon.vue";
 import { IconData, getMouseStyle, defaultStyle } from "./index.ts";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
-import { PropertyUpdate, Operate } from "../../propertys";
+import { PropertyUpdate, Operate, PropertyKeys, mergeDescribes } from "../../propertys";
 import { Transform } from "konva/lib/Util";
+import originDescribes from "../../propertys/describes.json";
+import { ref } from "vue";
+import { MathUtils } from "three";
+import { rangMod } from "@/utils/shared.ts";
 
 const props = defineProps<{ data: IconData }>();
 const emit = defineEmits<{
@@ -27,7 +31,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   emit,
   props,
   getMouseStyle,
-  transformType: "mat",
+  transformType: props.data.fixScreen ? undefined : "mat",
   defaultStyle,
   alignment(data, mat) {
     data.mat = mat.multiply(new Transform(props.data.mat)).m;
@@ -36,21 +40,22 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
     data.mat = tf.multiply(new Transform(data.mat)).m;
     return data;
   },
-  propertys: [
-    "fill",
-    "stroke",
-    "strokeWidth",
-    "dash",
-    "opacity",
-    "strokeScaleEnabled",
-
-    // "coverFill",
-    // "coverStroke",
-    // "coverStrokeWidth",
-    // "coverOpcatiy",
-
-    // "ref",
-    // "zIndex",
-  ],
+  propertys: ["fill", "stroke", "strokeWidth", "dash", "opacity", "strokeScaleEnabled"],
 });
+
+if (props.data.fixScreen) {
+  const other = ref({ rotate: 0 });
+  mergeDescribes(other, {}, ["rotate"], describes);
+  describes.rotate = {
+    ...originDescribes.rotate,
+    sort: 3,
+    get value() {
+      const deg = new Transform(data.value.mat).decompose().rotation % 360;
+      return Math.round(deg);
+    },
+    set value(val) {
+      data.value.mat = new Transform().rotate(MathUtils.degToRad(val)).m;
+    },
+  } as any;
+}
 </script>

+ 52 - 30
src/core/components/icon/index.ts

@@ -1,8 +1,14 @@
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { Transform } from "konva/lib/Util";
-import { BaseItem, generateSnapInfos, getBaseItem, getRectSnapPoints } from "../util.ts";
+import {
+  BaseItem,
+  generateSnapInfos,
+  getBaseItem,
+  getRectSnapPoints,
+} from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { AddMessage } from "@/core/hook/use-draw.ts";
+import { FixScreen } from "@/utils/bound.ts";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./icon.vue";
 export { default as TempComponent } from "./temp-icon.vue";
@@ -19,27 +25,26 @@ export const defaultStyle = {
 export const addMode = "dot";
 
 export const getSnapInfos = (data: IconData) => {
-  return generateSnapInfos(
-    getSnapPoints(data),
-    true,
-    false
-  );
+  return generateSnapInfos(getSnapPoints(data), true, false);
 };
 
 export const getSnapPoints = (data: IconData) => {
   const tf = new Transform(data.mat);
   const w = data.width || defaultStyle.width;
   const h = data.height || defaultStyle.height;
-  const points = getRectSnapPoints(w, h)
-  return points.map((v) => tf.point(v))
-}
+  const points = getRectSnapPoints(w, h);
+  return points.map((v) => tf.point(v));
+};
 
 export const getMouseStyle = (data: IconData) => {
   const fillStatus = getMouseColors(data.coverFill || defaultStyle.coverFill);
-  const hCoverOpcaoty = data.coverOpcatiy ? data.coverOpcatiy : 0.3
+  const hCoverOpcaoty = data.coverOpcatiy ? data.coverOpcatiy : 0.3;
 
   return {
-    default: { coverFill: fillStatus.pub, coverOpcatiy: data.coverOpcatiy ||0 },
+    default: {
+      coverFill: fillStatus.pub,
+      coverOpcatiy: data.coverOpcatiy || 0,
+    },
     hover: { coverFill: fillStatus.hover, coverOpcatiy: hCoverOpcaoty },
     press: { coverFill: fillStatus.press, coverOpcatiy: hCoverOpcaoty },
   };
@@ -47,9 +52,9 @@ export const getMouseStyle = (data: IconData) => {
 
 export type IconData = Partial<typeof defaultStyle> &
   BaseItem & {
-    fill?: string,
-    stroke?: string,
-    strokeWidth?: number,
+    fill?: string;
+    stroke?: string;
+    strokeWidth?: number;
     coverFill?: string;
     coverStroke?: string;
     coverStrokeWidth?: number;
@@ -57,9 +62,9 @@ export type IconData = Partial<typeof defaultStyle> &
     height: number;
     mat: number[];
     url: string;
+    fixScreen?: FixScreen;
   };
 
-
 export const dataToConfig = (data: IconData) => {
   return {
     ...defaultStyle,
@@ -67,23 +72,40 @@ export const dataToConfig = (data: IconData) => {
   };
 };
 
-export const interactiveToData = (
-  info: AddMessage<'icon'>,
-  preset: Partial<IconData> = {}
-): IconData | undefined => {
+export const interactiveToData: InteractiveTo<"icon"> = ({
+  info,
+  preset = {},
+  viewTransform,
+  ...args
+}) => {
   if (info.cur) {
-    return interactiveFixData(
-      { ...getBaseItem(), ...preset } as unknown as IconData,
-      info
-    );
+    return interactiveFixData({
+      ...args,
+      viewTransform,
+      info,
+      data: { ...getBaseItem(), ...preset } as unknown as IconData,
+    });
   }
 };
 
-export const interactiveFixData = (
-  data: IconData,
-  info: AddMessage<'icon'>
-) => {
-  const mat = new Transform().translate(info.cur!.x, info.cur!.y);
-  data.mat = mat.m;
+export const interactiveFixData: InteractiveFix<"icon"> = ({
+  data,
+  info,
+  viewTransform,
+}) => {
+  if (data.fixScreen) {
+    if ("x" in info.cur! && "y" in info.cur!) {
+      // 存储屏幕坐标
+      const screen = viewTransform.point(info.cur!);
+      data.fixScreen = {
+        left: screen.x,
+        top: screen.y,
+      };
+    }
+    data.mat = [1, 0, 0, 1, 0, 0];
+  } else {
+    const mat = new Transform().translate(info.cur!.x, info.cur!.y);
+    data.mat = mat.m;
+  }
   return data;
 };

+ 16 - 1
src/core/components/icon/temp-icon.vue

@@ -14,6 +14,9 @@ import { getSvgContent, parseSvgContent, SVGParseResult } from "@/utils/resource
 import { Group } from "konva/lib/Group";
 import { DC } from "@/deconstruction.js";
 import { Transform } from "konva/lib/Util";
+import { useViewerInvertTransform } from "@/core/hook/use-viewer.ts";
+import { getFixPosition } from "@/utils/bound.ts";
+import { useResize } from "@/core/hook/use-event.ts";
 
 const props = defineProps<{ data: IconData; addMode?: boolean }>();
 const svg = ref<SVGParseResult | null>(null);
@@ -65,9 +68,21 @@ const initDecMat = computed(() => {
     .decompose();
 });
 
+const viewInvTransform = useViewerInvertTransform();
+const size = useResize();
 const groupConfig = computed(() => {
+  let mat = new Transform(data.value.mat);
+
+  if (data.value.fixScreen) {
+    if (!size.value) return {};
+    const pos = getFixPosition(data.value.fixScreen, data.value, size.value);
+    pos.x += data.value.width / 2;
+    pos.y += data.value.height / 2;
+    mat = viewInvTransform.value.copy().translate(pos.x, pos.y).multiply(mat);
+  }
+
   return {
-    ...new Transform(data.value.mat).decompose(),
+    ...mat.decompose(),
     id: data.value.id,
     opacity: props.addMode ? 0.3 : 1,
   };

+ 44 - 41
src/core/components/image/index.ts

@@ -1,8 +1,13 @@
 import { Transform } from "konva/lib/Util";
-import { BaseItem, generateSnapInfos, getBaseItem, getRectSnapPoints } from "../util.ts";
+import {
+  BaseItem,
+  generateSnapInfos,
+  getBaseItem,
+  getRectSnapPoints,
+} from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { imageInfo } from "@/utils/resource.ts";
-import { AddMessage } from "@/core/hook/use-draw.ts";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./image.vue";
 export { default as TempComponent } from "./temp-image.vue";
@@ -10,68 +15,66 @@ export { default as TempComponent } from "./temp-image.vue";
 export const shapeName = "图片";
 export const defaultStyle = {
   strokeWidth: 0,
-  stroke: '#000000',
+  stroke: "#000000",
 };
 
-export const addMode = 'dot'
+export const addMode = "dot";
 
 export const getMouseStyle = (data: ImageData) => {
   const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
 
   return {
-    default: { stroke: strokeStatus.pub, },
-    hover: { stroke: strokeStatus.hover, },
-    press: { stroke: strokeStatus.press, },
+    default: { stroke: strokeStatus.pub },
+    hover: { stroke: strokeStatus.hover },
+    press: { stroke: strokeStatus.press },
   };
 };
 
-
 export const getSnapInfos = (data: ImageData) => {
-  return generateSnapInfos(
-    getSnapPoints(data),
-    true,
-    false
-  );
+  return generateSnapInfos(getSnapPoints(data), true, false);
 };
 
 export const getSnapPoints = (data: ImageData) => {
   const tf = new Transform(data.mat);
-  const useData = data.width && data.height
+  const useData = data.width && data.height;
   if (!useData && !(data.url in imageInfo)) {
-    return []
+    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 ImageData = Partial<typeof defaultStyle> & BaseItem & {
-  fill?: string;
-  stroke?: string;
-  strokeWidth?: number;
-  cornerRadius: number
-  width: number;
-  height: number;
-  url: string;
-  mat: number[]
+  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 const interactiveToData = (
-  info: AddMessage<'image'>,
-  preset: Partial<ImageData> = {}
-): ImageData | undefined => {
+export type ImageData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    fill?: string;
+    stroke?: string;
+    strokeWidth?: number;
+    cornerRadius: number;
+    width: number;
+    height: number;
+    url: string;
+    mat: number[];
+  };
+
+export const interactiveToData: InteractiveTo<"image"> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
   if (info.cur) {
-    return interactiveFixData({ ...getBaseItem(), ...preset, } as unknown as ImageData, info);
+    return interactiveFixData({
+      ...args,
+      info,
+      data: { ...getBaseItem(), ...preset } as unknown as ImageData,
+    });
   }
 };
 
-export const interactiveFixData = (
-  data: ImageData,
-  info: AddMessage<'image'>
-) => {
-  const mat = new Transform().translate(info.cur!.x, info.cur!.y)
-  data.mat = mat.m
+export const interactiveFixData: InteractiveFix<"image"> = ({ data, info }) => {
+  const mat = new Transform().translate(info.cur!.x, info.cur!.y);
+  data.mat = mat.m;
   return data;
 };

+ 92 - 62
src/core/components/index.ts

@@ -1,76 +1,106 @@
-import * as arrow from './arrow'
-import * as rectangle from './rectangle'
-import * as circle from './circle'
-import * as triangle from './triangle'
-import * as polygon from './polygon'
-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 arrow from "./arrow";
+import * as rectangle from "./rectangle";
+import * as circle from "./circle";
+import * as triangle from "./triangle";
+import * as polygon from "./polygon";
+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 { ArrowData } from './arrow'
-import { TableData } from './table'
-import { RectangleData } from './rectangle'
-import { CircleData } from './circle'
-import { TriangleData } from './triangle'
-import { PolygonData } from './polygon'
-import { LineData } from './line'
-import { TextData } from './text'
-import { IconData } from './icon'
-import { ImageData } from './image'
-import { BGImageData } from './bg-image'
-import { Pos } from '@/utils/math'
+import { ArrowData } from "./arrow";
+import { TableData } from "./table";
+import { RectangleData } from "./rectangle";
+import { CircleData } from "./circle";
+import { TriangleData } from "./triangle";
+import { PolygonData } from "./polygon";
+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 { 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";
 
 const _components = {
-	arrow,
-	rectangle,
-	circle,
-	triangle,
-	polygon,
-	line,
-	text,
-	icon,
-	image,
-	bgImage,
-	table
-}
+  arrow,
+  rectangle,
+  circle,
+  triangle,
+  polygon,
+  line,
+  text,
+  icon,
+  image,
+  bgImage,
+  table,
+  serial,
+};
 
 export const components = _components as Components
 
 export type Components = {
-	[key in keyof typeof _components]: (typeof _components)[key] & {
-		getSnapInfos?: (items: DrawItem<key>) => ComponentSnapInfo[]
-	}
-}
-export type ComponentValue<T extends ShapeType, K extends keyof Components[T]> = Components[T][K]
+  [key in keyof typeof _components]: (typeof _components)[key] & {
+    getSnapInfos?: (items: DrawItem<key>) => ComponentSnapInfo[];
+    GroupComponent: (props: { data: DrawItem[] }) => any;
+  };
+};
+export type ComponentValue<
+  T extends ShapeType,
+  K extends keyof Components[T]
+> = Components[T][K];
 
 export type DrawDataItem = {
-	arrow: ArrowData,
-	rectangle: RectangleData,
-	circle: CircleData,
-	triangle: TriangleData,
-	polygon: PolygonData,
-	line: LineData,
-	text: TextData,
-	icon: IconData,
-	image: ImageData,
-	bgImage: BGImageData,
-	table: TableData
-}
-export type ShapeType = keyof DrawDataItem
+  arrow: ArrowData;
+  rectangle: RectangleData;
+  circle: CircleData;
+  triangle: TriangleData;
+  polygon: PolygonData;
+  line: LineData;
+  text: TextData;
+  icon: IconData;
+  image: ImageData;
+  bgImage: BGImageData;
+  table: TableData;
+  serial: SerialData;
+};
+export type ShapeType = keyof DrawDataItem;
 
 export type DrawData = {
-	[k in ShapeType]?: DrawDataItem[k][]
-}
+  [k in ShapeType]?: DrawDataItem[k][];
+};
 
-export type DrawItem<T extends ShapeType = ShapeType> = DrawDataItem[T]
+export type DrawItem<T extends ShapeType = ShapeType> = DrawDataItem[T];
 
-export type SnapPoint = Pos & { view?: boolean }
+export type SnapPoint = Pos & { view?: boolean };
 export type ComponentSnapInfo = {
-	point: SnapPoint,
-	links: Pos[]
-	linkDirections: Pos[],
-	linkAngle: number[]
-}
+  point: SnapPoint;
+  links: Pos[];
+  linkDirections: Pos[];
+  linkAngle: number[];
+};
+
+export type InteractiveTo<T extends ShapeType> = (args: {
+  info: AddMessage<T>;
+  preset?: Partial<DrawItem<T>>;
+  viewTransform: Transform;
+  store: DrawStore;
+  history: DrawHistory;
+  drawing?: boolean
+}) => DrawItem<T> | undefined;
+
+export type InteractiveFix<T extends ShapeType> = (args: {
+  data: DrawItem<T>;
+  info: AddMessage<T>;
+  notdraw?: boolean
+  viewTransform: Transform;
+  store: DrawStore;
+  history: DrawHistory;
+}) => DrawItem<T>;

+ 27 - 23
src/core/components/line/index.ts

@@ -2,7 +2,7 @@ import { Pos } from "@/utils/math.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { AddMessage } from "@/core/hook/use-draw.ts";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./line.vue";
 export { default as TempComponent } from "./temp-line.vue";
@@ -26,35 +26,39 @@ export const getMouseStyle = (data: LineData) => {
   };
 };
 
-export const getSnapInfos = (data: LineData) => generateSnapInfos(getSnapPoints(data), true, true, true)
-
+export const getSnapInfos = (data: LineData) =>
+  generateSnapInfos(getSnapPoints(data), true, true, true);
 
 export const getSnapPoints = (data: LineData) => {
-  return data.points
-}
-
-
-export type LineData = Partial<typeof defaultStyle> & BaseItem & {
-  points: Pos[];
-  attitude: number[];
+  return data.points;
 };
 
-export const interactiveToData = (
-  info: AddMessage<'line'>,
-  preset: Partial<LineData> = {}
-): LineData | undefined => {
+export type LineData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    points: Pos[];
+    attitude: number[];
+  };
+
+export const interactiveToData: InteractiveTo<"line"> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
   if (info.cur) {
-    return interactiveFixData(
-      { ...getBaseItem(), ...preset, points: [], attitude: [1, 0, 0, 1, 0, 0] },
-      info
-    );
+    return interactiveFixData({
+      ...args,
+      info,
+      data: {
+        ...getBaseItem(),
+        ...preset,
+        points: [],
+        attitude: [1, 0, 0, 1, 0, 0],
+      },
+    });
   }
 };
 
-export const interactiveFixData = (
-  data: LineData,
-  info: AddMessage<'line'>
-) => {
-  data.points = [...info.consumed, info.cur!]
+export const interactiveFixData: InteractiveFix<"line"> = ({ data, info }) => {
+  data.points = [...info.consumed, info.cur!];
   return data;
 };

+ 3 - 2
src/core/components/line/temp-line.vue

@@ -11,7 +11,8 @@
       }"
     >
     </v-line>
-    <template v-if="!status.active">
+
+    <template v-if="status.hover && !status.active">
       <Point
         v-for="(p, ndx) in data.points"
         :id="data.id + ndx"
@@ -31,7 +32,7 @@
 import Point from "../share/point.vue";
 import { defaultStyle, LineData } from "./index.ts";
 import { flatPositions } from "@/utils/shared.ts";
-import { computed, ref } from "vue";
+import { computed, ref, watchEffect } from "vue";
 import { DC } from "@/deconstruction.js";
 import { Line, LineConfig } from "konva/lib/shapes/Line";
 import { Pos } from "@/utils/math.ts";

+ 37 - 25
src/core/components/polygon/index.ts

@@ -2,7 +2,7 @@ import { Pos } from "@/utils/math.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { AddMessage, MessageAction } from "@/core/hook/use-draw.ts";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./polygon.vue";
 export { default as TempComponent } from "./temp-polygon.vue";
@@ -16,10 +16,14 @@ export const defaultStyle = {
 export const getMouseStyle = (data: PolygonData) => {
   const fillStatus = data.fill && getMouseColors(data.fill);
   const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
-  const strokeWidth = data.strokeWidth ;
+  const strokeWidth = data.strokeWidth;
 
   return {
-    default: { fill: fillStatus && fillStatus.pub, stroke: strokeStatus.pub, strokeWidth },
+    default: {
+      fill: fillStatus && fillStatus.pub,
+      stroke: strokeStatus.pub,
+      strokeWidth,
+    },
     hover: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
     press: { fill: fillStatus && fillStatus.press, stroke: strokeStatus.press },
   };
@@ -27,35 +31,43 @@ export const getMouseStyle = (data: PolygonData) => {
 
 export const addMode = "dots";
 
-export const getSnapInfos = (data: PolygonData) => generateSnapInfos(getSnapPoints(data), true, true)
-
+export const getSnapInfos = (data: PolygonData) =>
+  generateSnapInfos(getSnapPoints(data), true, true);
 
 export const getSnapPoints = (data: PolygonData) => {
-  return data.points
-}
-
-export type PolygonData = Partial<typeof defaultStyle> & BaseItem & {
-  fill?: string
-  points: Pos[];
-  attitude: number[];
+  return data.points;
 };
 
-export const interactiveToData = (
-  info: AddMessage<'polygon'>,
-  preset: Partial<PolygonData> = {}
-): PolygonData | undefined => {
+export type PolygonData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    fill?: string;
+    points: Pos[];
+    attitude: number[];
+  };
+
+export const interactiveToData: InteractiveTo<"polygon"> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
   if (info.cur) {
-    return interactiveFixData(
-      { ...getBaseItem(), ...preset, points: [], attitude: [1, 0, 0, 1, 0, 0] },
-      info
-    );
+    return interactiveFixData({
+      ...args,
+      info,
+      data: {
+        ...getBaseItem(),
+        ...preset,
+        points: [],
+        attitude: [1, 0, 0, 1, 0, 0],
+      },
+    });
   }
 };
 
-export const interactiveFixData = (
-  data: PolygonData,
-  info: AddMessage<'polygon'>
-) => {
-  data.points = [...info.consumed, info.cur!]
+export const interactiveFixData: InteractiveFix<"polygon"> = ({
+  data,
+  info,
+}) => {
+  data.points = [...info.consumed, info.cur!];
   return data;
 };

+ 1 - 1
src/core/components/polygon/temp-polygon.vue

@@ -11,7 +11,7 @@
       }"
     >
     </v-line>
-    <template v-if="!status.active">
+    <template v-if="status.hover">
       <Point
         v-for="(p, ndx) in data.points"
         :id="data.id + ndx"

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

@@ -3,7 +3,7 @@ import { themeMouseColors } from "@/constant/help-style.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { onlyId } from "@/utils/shared.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
-import { AddMessage } from "@/core/hook/use-draw.ts";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./rectangle.vue";
 export { default as TempComponent } from "./temp-rectangle.vue";
@@ -47,10 +47,18 @@ export type RectangleData = Partial<typeof defaultStyle> &
     content?: string
   };
 
-export const interactiveToData = (
-  info: AddMessage<'rectangle'>,
-  preset: Partial<RectangleData> = {}
-): RectangleData | undefined => {
+
+  export const getSnapPoints = (data: RectangleData) => {
+    return data.points
+  }
+  
+  export const getSnapInfos = (data: RectangleData) => generateSnapInfos(getSnapPoints(data), true, false)
+  
+export const interactiveToData: InteractiveTo<"rectangle"> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
   if (info.cur) {
     const item = {
       ...getBaseItem(),
@@ -59,20 +67,14 @@ export const interactiveToData = (
       createTime: Date.now(),
       zIndex: 0,
     } as unknown as RectangleData;
-    return interactiveFixData(item, info);
+    return interactiveFixData({ ...args, info, data: item });
   }
 };
 
-export const getSnapPoints = (data: RectangleData) => {
-  return data.points
-}
-
-export const getSnapInfos = (data: RectangleData) => generateSnapInfos(getSnapPoints(data), true, false)
-
-export const interactiveFixData = (
-  data: RectangleData,
-  info: AddMessage<'rectangle'>
-) => {
+export const interactiveFixData: InteractiveFix<"rectangle"> = ({
+  data,
+  info,
+}) => {
   if (info.cur) {
     const area = info.cur!;
     const width = area[1].x - area[0].x;

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

@@ -4,6 +4,7 @@
       name="repShape"
       :config="{
         ...data,
+        zIndex: undefined,
         id: undefined,
         closed: true,
         points: flatPositions(data.points),
@@ -29,6 +30,7 @@ import { DC } from "@/deconstruction.js";
 import { Line } from "konva/lib/shapes/Line";
 import { Transform } from "konva/lib/Util";
 import { MathUtils } from "three";
+import { zIndex } from "html2canvas/dist/types/css/property-descriptors/z-index";
 const props = defineProps<{ data: RectangleData; addMode?: boolean; editer?: boolean }>();
 const emit = defineEmits<{
   (e: "updateContent", data: string): void;

+ 71 - 0
src/core/components/serial/index.ts

@@ -0,0 +1,71 @@
+import { getBaseItem } from "../util.ts";
+import { Transform } from "konva/lib/Util";
+import { CircleData, defaultStyle } from "../circle";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
+import { DrawStore } from "@/core/store/index.ts";
+
+export {
+  defaultStyle,
+  getMouseStyle,
+  getSnapInfos,
+  getSnapPoints,
+  dataToConfig,
+} from "../circle";
+
+export { default as GroupComponent } from "./serial-group.vue";
+export { default as Component } from "./serial.vue";
+export { default as TempComponent } from "./temp-serial.vue";
+
+export const shapeName = "序号";
+export const addMode = "dot";
+
+export type SerialData = CircleData;
+
+export const getSerialFontW = (data: SerialData) => {
+  const fontSize = data.fontSize || defaultStyle.fontSize;
+  const pad = data.padding || 0;
+  const len = (data.content?.length || 1) * 0.6;
+  return fontSize * len + pad * 2;
+};
+
+export const getSerialFontSizeByFontW = (data: SerialData, w: number) => {
+  const pad = data.padding || 0;
+  const len = (data.content?.length || 1) * 0.6;
+  return (w - pad * 2) / len;
+};
+
+export const getCurrentNdx = (store: DrawStore) => {
+  const cs = store
+    .getTypeItems("serial")
+    .map((item) => Number(item.content || 0));
+  return (Math.max(...cs, 0) + 1).toString();
+};
+
+export const interactiveToData: InteractiveTo<"serial"> = ({
+  info,
+  preset = {},
+  store,
+  ...args
+}) => {
+  if (info.cur) {
+    let item = {
+      fontSize: defaultStyle.fontSize,
+      ...getBaseItem(),
+      ...preset,
+      padding: 3,
+      content: getCurrentNdx(store),
+    } as unknown as SerialData;
+    return interactiveFixData({ ...args, info, store, data: item });
+  }
+};
+
+export const interactiveFixData: InteractiveFix<"serial"> = ({
+  data,
+  info,
+}) => {
+  data.mat = new Transform().translate(info.cur!.x, info.cur!.y).m;
+  const radius = (getSerialFontW(data) * Math.sqrt(2)) / 2;
+  data.radiusX = radius;
+  data.radiusY = radius;
+  return data;
+};

+ 140 - 0
src/core/components/serial/serial-group.vue

@@ -0,0 +1,140 @@
+<template>
+  <template v-for="(item, ndx) in data" :key="item.id">
+    <Serial
+      :data="item"
+      @updateShape="(value: any) => store.setItem('serial', { id: item.id, value })"
+      @addShape="addHandler"
+      @delShape="delHandler(ndx)"
+      v-if="!itemHasRegistor(item.id)"
+    />
+    <template v-else>
+      <component :is="renderer(item.id)" :item="item" />
+    </template>
+  </template>
+</template>
+
+<script setup lang="ts">
+import Serial from "./serial.vue";
+import { useStore, useStoreRenderProcessors } from "../../store";
+import { computed, watch } from "vue";
+import { useHistory } from "@/core/hook/use-history";
+import { ShapeType } from "..";
+import { TableData, interactiveToData as tableInteractiveToData } from "../table";
+import { getCurrentNdx, SerialData } from ".";
+
+defineProps<{ type?: ShapeType }>();
+const store = useStore();
+const data = computed(() => store.getTypeItems("serial"));
+const history = useHistory();
+const { itemHasRegistor, renderer } = useStoreRenderProcessors();
+
+const addHandler = (value: SerialData) => {
+  value.content = getCurrentNdx(store);
+  store.addItem("serial", value);
+};
+
+const delHandler = (ndx: number) => {
+  const value = data.value[ndx];
+  const table = jTable.value!;
+  for (let i = ndx + 1; i < data.value.length; i++) {
+    const c = (Number(data.value[i].content) - 1).toString();
+    table.content[i + 1][0].content = data.value[i].content = c.toString();
+  }
+  table.content.splice(ndx + 1, 1);
+  table.height -= table.content[ndx + 1][0].height;
+
+  history.onceTrack(() => {
+    if (data.value.length === 1) {
+      store.delItem("table", table.id);
+    } else {
+      store.setItem("table", { id: table.id, value: table });
+    }
+    store.delItem("serial", value.id);
+  });
+};
+
+const joinKey = "serial-table";
+const jTable = computed(() =>
+  store.getTypeItems("table").find((item) => item.key === joinKey)
+);
+
+const addTable = () => {
+  const last = data.value[data.value.length - 1];
+  const pos = {
+    x: last.mat[4] + last.radiusX + 20,
+    y: last.mat[5] + last.radiusX + 20,
+  };
+  const end = {
+    x: pos.x + 107 * 2,
+    y: pos.y + 32,
+  };
+
+  return tableInteractiveToData({
+    info: { cur: [pos, end] },
+    preset: {
+      notaddRow: true,
+      notaddCol: true,
+      key: joinKey,
+      content: [
+        [
+          { content: "序号", readonly: true, notdel: true },
+          { content: "描述", readonly: true, notdel: true },
+        ],
+      ],
+    },
+    store,
+    history,
+    notdraw: true,
+  } as any)!;
+};
+
+const syncTable = (table: TableData) => {
+  const items = data.value;
+  const cols = table.content.map((row) => row[0]);
+  const tempRow = table.content[table.content.length - 1];
+  let isUpdate = false;
+
+  for (const item of items) {
+    if (cols.some((col) => col.content === item.content)) {
+      continue;
+    }
+    table.height += tempRow[0].height;
+    table.content.push([
+      { ...tempRow[0], content: item.content! },
+      { ...tempRow[1], content: "", readonly: false },
+    ]);
+    isUpdate = true;
+  }
+  for (let i = 1; i < cols.length; i++) {
+    const col = cols[i];
+    if (items.some((item) => col.content === item.content)) {
+      continue;
+    }
+    table.height -= tempRow[0].height;
+    table.content.splice(i, 1);
+    isUpdate = true;
+  }
+  return isUpdate;
+};
+
+const updateJoinTable = () => {
+  const items = data.value;
+
+  let table = jTable.value;
+  if (items.length === 0) {
+    table && history.preventTrack(() => store.delItem("table", table!.id));
+    return;
+  }
+  table || history.preventTrack(() => store.addItem("table", (table = addTable())));
+  if (syncTable(table!)) {
+    history.preventTrack(() =>
+      store.setItem("table", {
+        id: table!.id,
+        value: table!,
+      })
+    );
+  }
+};
+
+watch(() => data.value.length, updateJoinTable, { immediate: true });
+</script>

+ 121 - 0
src/core/components/serial/serial.vue

@@ -0,0 +1,121 @@
+<template>
+  <TempComponent
+    :data="tData"
+    :ref="(v: any) => shape = v?.shape"
+    :id="data.id"
+    @update-content="updateContent"
+  />
+  <PropertyUpdate
+    :describes="describes"
+    :data="data"
+    :target="shape"
+    @change="emit('updateShape', { ...data })"
+  />
+  <Operate :target="shape" :menus="operateMenus" />
+</template>
+
+<script lang="ts" setup>
+import {
+  SerialData,
+  getMouseStyle,
+  defaultStyle,
+  getSerialFontSizeByFontW,
+} 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";
+
+const props = defineProps<{ data: SerialData }>();
+const emit = defineEmits<{
+  (e: "updateShape", value: SerialData): void;
+  (e: "addShape", value: SerialData): void;
+  (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 });
+};
+
+const { shape, tData, data, operateMenus, describes } = useComponentStatus<
+  Ellipse,
+  SerialData
+>({
+  emit,
+  props,
+  getMouseStyle,
+  defaultStyle,
+  alignment: (data, mat) => {
+    const tf = shape.value!.getNode().getTransform();
+    mat.multiply(tf);
+    return matToData(data, mat);
+  },
+  transformType: "custom",
+  customTransform(callback, shape, data) {
+    let initRadius: Pos;
+    useCustomTransformer(shape, data, {
+      openSnap: true,
+      getRepShape($shape) {
+        const repShape = cloneRepShape($shape).shape;
+        initRadius = { x: repShape.radiusX(), y: repShape.radiusY() };
+        return {
+          shape: repShape,
+        };
+      },
+      // beforeHandler(data, mat) {
+      //   return matToData(copy(data), mat);
+      // },
+      handler(data, mat) {
+        matToData(data, mat, initRadius);
+        return true;
+      },
+      callback,
+      transformerConfig: {
+        rotateEnabled: false,
+        enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"],
+      },
+    });
+  },
+  copyHandler(mat, data) {
+    const tf = (shape.value!.getNode() as any).findOne(".repShape").getTransform();
+    mat.multiply(tf);
+    return matToData({ ...data }, mat);
+  },
+  propertys: [
+    "fill",
+    "stroke",
+    "fontColor",
+    "strokeWidth",
+    // "fontSize",
+    // "ref",
+    "opacity",
+    "dash",
+    //  "zIndex"
+    "fontStyle",
+    // "ref", "zIndex"
+  ],
+});
+</script>

+ 72 - 0
src/core/components/serial/temp-serial.vue

@@ -0,0 +1,72 @@
+<template>
+  <v-group ref="shape" :id="data.id">
+    <v-ellipse
+      name="repShape"
+      :config="{
+        ...data,
+        ...matConfig,
+        zIndex: undefined,
+        opacity: addMode ? 0.3 : data.opacity,
+      }"
+    />
+    <ShareText
+      :config="{ ...textConfig, ...textBound }"
+      :parent-id="data.id"
+      :editer="editer"
+      @update-text="(val) => emit('updateContent', val)"
+      @update:is-edit="(val) => emit('update:isEdit', val)"
+    />
+  </v-group>
+</template>
+
+<script lang="ts" setup>
+import ShareText from "../share/text.vue";
+import { SerialData, defaultStyle } from "./index.ts";
+import { computed, ref } from "vue";
+import { DC } from "@/deconstruction.js";
+import { Circle } from "konva/lib/shapes/Circle";
+import { Transform } from "konva/lib/Util";
+
+const props = defineProps<{ data: SerialData; addMode?: boolean; editer?: boolean }>();
+const emit = defineEmits<{
+  (e: "updateContent", data: string): void;
+  (e: "update:isEdit", data: boolean): void;
+}>();
+
+const data = computed(() => ({ ...defaultStyle, ...props.data }));
+
+const matConfig = computed(() => {
+  const mat = new Transform(data.value.mat);
+  return mat.decompose();
+});
+
+const textBound = computed(() => {
+  const r1 = props.data.radiusX;
+  const width = (2 * r1) / Math.sqrt(2); // 宽度
+  const pad = data.value.padding || 0;
+  const height = Math.min(data.value.fontSize + pad * 2);
+  const matConfig = new Transform(data.value.mat)
+    .translate(-width / 2, -height / 2 + data.value.fontSize * 0.1)
+    .decompose();
+  return {
+    padding: pad,
+    ...matConfig,
+    width,
+    height,
+  };
+});
+const textConfig = computed(() => ({
+  fontSize: data.value.fontSize,
+  align: data.value.align,
+  fontStyle: data.value.fontStyle,
+  fill: data.value.fontColor,
+  text: data.value.content,
+}));
+
+const shape = ref<DC<Circle>>();
+defineExpose({
+  get shape() {
+    return shape.value;
+  },
+});
+</script>

+ 6 - 9
src/core/components/share/point.vue

@@ -13,7 +13,7 @@ import { getMouseColors } from "@/utils/colors";
 import { useGlobalSnapInfos, useSnap } from "@/core/hook/use-snap";
 import { generateSnapInfos } from "../util";
 import { ComponentSnapInfo } from "..";
-import { useMouseShapeStatus } from "@/core/hook/use-mouse-status";
+import { useShapeIsHover } from "@/core/hook/use-mouse-status";
 import { useCursor } from "@/core/hook/use-global-vars";
 
 const props = defineProps<{
@@ -52,16 +52,13 @@ const refSnapInfos = computed(() => {
 const snap = useSnap(refSnapInfos);
 const circle = ref<DC<Circle>>();
 const offset = useShapeDrag(circle);
-const status = useMouseShapeStatus(circle);
+const [isHover] = useShapeIsHover(circle);
 const cursor = useCursor();
-watch(
-  () => status.value.hover,
-  (hover, _, onCleanup) => {
-    if (hover) {
-      onCleanup(cursor.push("move"));
-    }
+watch(isHover, (hover, _, onCleanup) => {
+  if (hover) {
+    onCleanup(cursor.push("move"));
   }
-);
+});
 
 let init: Pos;
 watch(offset, (offset, oldOffsert) => {

+ 7 - 7
src/core/components/share/text-area.vue

@@ -2,7 +2,7 @@
   <Teleport :to="mount" v-if="show">
     <textarea
       ref="textarea"
-      :style="styles"
+      :style="getStyles()"
       @input="() => text = textarea!.value"
       @blur="quit"
       @focus="focusHandler"
@@ -15,7 +15,7 @@
 <script lang="ts" setup>
 import { useStage } from "@/core/hook/use-global-vars";
 import { Text } from "konva/lib/shapes/Text";
-import { computed, ref, watch } from "vue";
+import { ref, watch, watchEffect } from "vue";
 import { DomMountId } from "@/constant/index.ts";
 import { useViewerTransform } from "@/core/hook/use-viewer";
 import { listener } from "@/utils/event";
@@ -23,7 +23,7 @@ import { Transform } from "konva/lib/Util";
 import { useGetPointerTextNdx } from "@/core/hook/use-text-pointer";
 import { mergeFuns } from "@/utils/shared";
 
-const props = defineProps<{ shape: Text }>();
+const props = defineProps<{ shape: Text; content?: string }>();
 const emit = defineEmits<{
   (e: "submit", text: string): void;
   (e: "show"): void;
@@ -36,7 +36,7 @@ const isEdge = (document as any).documentMode || /Edge/.test(navigator.userAgent
 const textarea = ref<HTMLTextAreaElement>();
 const stage = useStage();
 const $stage = stage.value?.getNode()!;
-const text = ref(props.shape.text());
+const text = ref("");
 const mount = document.querySelector(`#${DomMountId}`) as HTMLDivElement;
 
 const quit = () => {
@@ -85,7 +85,7 @@ const focusToPointer = () => {
   textarea.value.setSelectionRange(ndx, ndx);
 };
 
-const styles = computed(() => {
+const getStyles = () => {
   const shape = props.shape;
   return {
     fontSize: shape.fontSize() + "px",
@@ -95,7 +95,7 @@ const styles = computed(() => {
     textAlign: shape.align() as CanvasTextAlign,
     color: shape.fill().toString(),
   };
-});
+};
 
 const refresh = () => {
   refreshSize();
@@ -108,7 +108,7 @@ watch(
   () => props.shape,
   (shape) => {
     shape.on("dblclick", () => {
-      text.value = shape.text();
+      text.value = props.content || props.shape.text();
       show.value = true;
       emit("show");
     });

+ 9 - 5
src/core/components/share/text.vue

@@ -2,6 +2,7 @@
   <v-text :config="{ ...config, text }" ref="shape" name="text" />
   <TextDom
     v-if="shape && editer"
+    :content="props.config.text"
     :shape="shape.getNode()"
     @submit="submitHandler"
     @show="showHandler"
@@ -13,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 } from "vue";
+import { computed, ref, watchEffect } from "vue";
 import TextDom from "./text-area.vue";
 
 const props = withDefaults(
@@ -21,9 +22,11 @@ const props = withDefaults(
     config: TextConfig;
     editer?: boolean;
     parentId?: string;
+    vCenter?: boolean;
   }>(),
   { editer: false }
 );
+
 const emit = defineEmits<{
   (e: "updateText", data: string): void;
   (e: "update:isEdit", val: boolean): void;
@@ -34,10 +37,11 @@ const shapesStatus = useMouseShapesStatus();
 const text = computed(() => {
   const pad = props.config.padding || 0;
   if (
-    !props.config.text ||
-    !props.config.fontSize ||
-    !props.config.height ||
-    props.config.height - props.config.fontSize < pad
+    "height" in props.config &&
+    (!props.config.text ||
+      !props.config.fontSize ||
+      !props.config.height ||
+      props.config.height - props.config.fontSize < pad)
   ) {
     return "";
   } else {

+ 44 - 25
src/core/components/table/index.ts

@@ -7,7 +7,7 @@ import {
   getRectSnapPoints,
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { AddMessage } from "@/core/hook/use-draw.ts";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./table.vue";
 export { default as TempComponent } from "./temp-table.vue";
@@ -36,9 +36,13 @@ export type TableCollData = Partial<typeof defaultCollData> & {
   width: number;
   height: number;
   padding: number
+  readonly?: boolean
+  notdel?: boolean
 };
 export type TableData = Partial<typeof defaultStyle> &
   BaseItem & {
+    notaddRow?: boolean
+    notaddCol?: boolean
     mat: number[];
     content: TableCollData[][];
     width: number;
@@ -66,26 +70,28 @@ export const getSnapInfos = (data: TableData) => {
   return generateSnapInfos(getSnapPoints(data), true, false);
 };
 
-export const interactiveToData = (
-  info: AddMessage<"table">,
-  preset: Partial<TableData> = {}
-): TableData | undefined => {
+export const interactiveToData: InteractiveTo<"table"> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
   if (info.cur) {
     const item = {
       ...defaultStyle,
       ...getBaseItem(),
       ...preset,
     } as unknown as TableData;
-    return interactiveFixData(item, info);
+    return interactiveFixData({ ...args, info, data: item });
   }
 };
 
-const autoCollWidth = 100;
-const autoCollHeight = 50;
-export const interactiveFixData = (
-  data: TableData,
-  info: AddMessage<"table">
-) => {
+export const autoCollWidth = 100;
+export const autoCollHeight = 50;
+export const interactiveFixData: InteractiveFix<"table"> = ({
+  data,
+  info,
+  notdraw
+}) => {
   if (info.cur) {
     const area = info.cur!;
     const origin = {
@@ -95,20 +101,33 @@ export const interactiveFixData = (
     data.width = Math.abs(area[0].x - area[1].x)
     data.height = Math.abs(area[0].y - area[1].y)
     
-    const colNum = Math.floor(data.width / autoCollWidth) || 1;
-    const rawNum = Math.floor(data.height / autoCollHeight) || 1;
-    const temp = data.content?.[0]?.[0] || {
-      content: ""
-    };
+    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: ""
+      };
 
-    data.content = Array.from({ length: rawNum }, () =>
-      Array.from({ length: colNum }, () => ({
-        ...temp,
-        width: data.width / colNum,
-        height: data.height / rawNum,
-        padding: 8
-      }))
-    );
+      data.content = Array.from({ length: rawNum }, () =>
+        Array.from({ length: colNum }, () => ({
+          ...temp,
+          width: data.width / colNum,
+          height: data.height / rawNum,
+          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)
+        })
+      })
+    }
     data.mat = new Transform().translate(origin.x, origin.y).m;
   }
   return data;

+ 21 - 10
src/core/components/table/table.vue

@@ -335,8 +335,9 @@ watchEffect(
 let addMenu: any;
 const menuShowHandler = () => {
   const config = tableRef.value!.getMouseIntersect();
-  addMenu = [
-    {
+  addMenu = [];
+  if (!data.value.notaddRow) {
+    addMenu.push({
       icon: Plus,
       label: "插入行",
       handler: () => {
@@ -350,8 +351,11 @@ const menuShowHandler = () => {
         sync(data.value);
         emit("updateShape", { ...data.value });
       },
-    },
-    {
+    });
+  }
+  const canDelRaw = data.value.content[config.rowNdx].every((item) => !item.notdel);
+  if (canDelRaw) {
+    addMenu.push({
       icon: Minus,
       label: "删除行",
       handler: () => {
@@ -365,8 +369,11 @@ const menuShowHandler = () => {
           emit("updateShape", data.value);
         }
       },
-    },
-    {
+    });
+  }
+
+  if (!data.value.notaddCol) {
+    addMenu.push({
       icon: Plus,
       label: "插入列",
       handler: () => {
@@ -379,8 +386,12 @@ const menuShowHandler = () => {
         sync(data.value);
         emit("updateShape", data.value);
       },
-    },
-    {
+    });
+  }
+
+  const canDelCol = data.value.content.every((items) => !items[config.colNdx].notdel);
+  if (canDelCol) {
+    addMenu.push({
       icon: Minus,
       label: "删除列",
       handler: () => {
@@ -397,8 +408,8 @@ const menuShowHandler = () => {
           emit("updateShape", data.value);
         }
       },
-    },
-  ];
+    });
+  }
   operateMenus.unshift(...addMenu);
 };
 const menuHideHandler = () => {

+ 1 - 1
src/core/components/table/temp-table.vue

@@ -30,7 +30,7 @@
             fill: col.fontColor,
           }"
           :parent-id="data.id"
-          :editer="editer"
+          :editer="editer && !col.readonly"
           @update-text="(val) => emit('updateContent', { rowNdx, colNdx, val })"
           @update:is-edit="(val) => emit('update:isEdit', { rowNdx, colNdx, val })"
         />

+ 37 - 33
src/core/components/text/index.ts

@@ -1,10 +1,15 @@
 import { Transform } from "konva/lib/Util";
-import { Text, TextConfig } from "konva/lib/shapes/Text";
+import { Text } from "konva/lib/shapes/Text";
 import { themeMouseColors } from "@/constant/help-style.ts";
-import { BaseItem, generateSnapInfos, getBaseItem, getRectSnapPoints } from "../util.ts";
+import {
+  BaseItem,
+  generateSnapInfos,
+  getBaseItem,
+  getRectSnapPoints,
+} from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { shallowReactive } from "vue";
-import { AddMessage } from "@/core/hook/use-draw.ts";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./text.vue";
 export { default as TempComponent } from "./temp-text.vue";
@@ -14,21 +19,21 @@ export const defaultStyle = {
   // stroke: themeMouseColors.theme,
   fill: themeMouseColors.theme,
   // strokeWidth: 0,
-  fontFamily: 'Calibri',
+  fontFamily: "Calibri",
   fontSize: 16,
-  align: 'center',
-  fontStyle: "normal"
+  align: "center",
+  fontStyle: "normal",
 };
 
-export const addMode = 'dot'
-
-export type TextData = Partial<typeof defaultStyle> & BaseItem & {
-  mat: number[]
-  content: string
-  width?: number
-  heihgt?: number
-};
+export const addMode = "dot";
 
+export type TextData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    mat: number[];
+    content: string;
+    width?: number;
+    heihgt?: number;
+  };
 
 export const getMouseStyle = (data: TextData) => {
   const fillStatus = getMouseColors(data.fill || defaultStyle.fill);
@@ -42,40 +47,39 @@ export const getMouseStyle = (data: TextData) => {
   };
 };
 
-export const textNodeMap: Record<BaseItem['id'], Text> = shallowReactive({})
-
+export const textNodeMap: Record<BaseItem["id"], Text> = shallowReactive({});
 
 export const getSnapPoints = (data: TextData) => {
-  if (!textNodeMap[data.id]) return []
-  const node = textNodeMap[data.id]
-  const tf = new Transform(data.mat)
-  return getRectSnapPoints(data.width || node.width(), node.height(), 0, 0).map((v) => tf.point(v));
-}
+  if (!textNodeMap[data.id]) return [];
+  const node = textNodeMap[data.id];
+  const tf = new Transform(data.mat);
+  return getRectSnapPoints(data.width || node.width(), node.height(), 0, 0).map(
+    (v) => tf.point(v)
+  );
+};
 
 export const getSnapInfos = (data: TextData) => {
   return generateSnapInfos(getSnapPoints(data), true, false);
 };
 
-export const interactiveToData = (
-  info: AddMessage<'text'>,
-  preset: Partial<TextData> = {}
-): TextData | undefined => {
+export const interactiveToData: InteractiveTo<"text"> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
   if (info.cur) {
     const item = {
       ...defaultStyle,
       ...getBaseItem(),
       ...preset,
-      content: preset.content || '文字',
+      content: preset.content || "文字",
     } as unknown as TextData;
-    return interactiveFixData(item, info);
+    return interactiveFixData({ ...args, info, data: item });
   }
 };
 
-export const interactiveFixData = (
-  data: TextData,
-  info: AddMessage<'text'>
-) => {
-  const mat = new Transform().translate(info.cur!.x, info.cur!.y)
-  data.mat = mat.m
+export const interactiveFixData: InteractiveFix<"text"> = ({ data, info }) => {
+  const mat = new Transform().translate(info.cur!.x, info.cur!.y);
+  data.mat = mat.m;
   return data;
 };

+ 11 - 10
src/core/components/triangle/index.ts

@@ -2,7 +2,7 @@ import { Pos } from "@/utils/math.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { BaseItem, generateSnapInfos, getBaseItem } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { AddMessage } from "@/core/hook/use-draw.ts";
+import { InteractiveFix, InteractiveTo } from "../index.ts";
 
 export { default as Component } from "./triangle.vue";
 export { default as TempComponent } from "./temp-triangle.vue";
@@ -50,24 +50,25 @@ export const getSnapPoints = (data: TriangleData) => {
 export const getSnapInfos = (data: TriangleData) =>
   generateSnapInfos(getSnapPoints(data), true, false);
 
-export const interactiveToData = (
-  info: AddMessage<"triangle">,
-  preset: Partial<TriangleData> = {}
-): TriangleData | undefined => {
+export const interactiveToData: InteractiveTo<"triangle"> = ({
+  info,
+  preset = {},
+  ...args
+}) => {
   if (info.cur) {
     const item = {
       ...getBaseItem(),
       ...preset,
       points: [],
     } as unknown as TriangleData;
-    return interactiveFixData(item, info);
+    return interactiveFixData({ ...args, info, data: item });
   }
 };
 
-export const interactiveFixData = (
-  data: TriangleData,
-  info: AddMessage<"triangle">
-) => {
+export const interactiveFixData: InteractiveFix<"triangle"> = ({
+  data,
+  info,
+}) => {
   const area = info.cur!;
 
   data.points[0] = {

+ 1 - 0
src/core/components/util.ts

@@ -9,6 +9,7 @@ export type BaseItem = {
   zIndex: number;
   lock: boolean,
   opacity: number
+  key?: string
   ref: boolean
   listening?: boolean
 };

+ 45 - 49
src/core/helper/back-grid.vue

@@ -1,34 +1,23 @@
 <template>
-  <v-group ref="grid" v-if="hLines && rect" :config="{ listening: false }">
-    <template v-for="item in hLines">
-      <v-line
-        v-for="l in item.children"
-        :config="{
-          points: [rect[0].x, l, rect[1].x, l],
-          ...style,
-          strokeWidth: style.strokeWidth * 0.33,
-        }"
-      />
-    </template>
-    <template v-for="item in vLines">
-      <v-line
-        v-for="l in item.children"
-        :config="{
-          points: [l, rect[0].y, l, rect[1].y],
-          ...style,
-          strokeWidth: style.strokeWidth * 0.33,
-        }"
-      />
-    </template>
-
+  <v-group ref="grid" :config="{ listening: false }">
     <v-line
-      v-for="item in hLines"
-      :config="{ points: [rect[0].x, item.dividing, rect[1].x, item.dividing], ...style }"
+      v-if="hConfig"
+      :config="{
+        points: hConfig.children,
+        ...style,
+        strokeWidth: style.strokeWidth * 0.33,
+      }"
     />
     <v-line
-      v-for="item in vLines"
-      :config="{ points: [item.dividing, rect[0].y, item.dividing, rect[1].y], ...style }"
+      v-if="vConfig"
+      :config="{
+        points: vConfig.children,
+        ...style,
+        strokeWidth: style.strokeWidth * 0.33,
+      }"
     />
+    <v-line v-if="hConfig" :config="{ points: hConfig.dividing, ...style }" />
+    <v-line v-if="vConfig" :config="{ points: vConfig.dividing, ...style }" />
   </v-group>
 </template>
 <script lang="ts" setup>
@@ -69,8 +58,8 @@ const rect = computed(() => {
 });
 
 // 看大格子的像素,100倍数
-const offsetUnit = 100;
-const minScalePixel = offsetUnit / 2;
+const offsetUnit = 200;
+const minScalePixel = 20;
 const viewerTransform = useViewerTransform();
 const viewerTransformConfig = useViewerTransformConfig();
 const offset = ref(offsetUnit);
@@ -100,48 +89,55 @@ const getFinal = (val: number, isTop: boolean) => {
   return offset.value * t;
 };
 
-type DireLine = {
-  dividing: number;
-  children: number[];
-};
-
-const getLines = (min: number, max: number) => {
+const getLines = (min: number, max: number, axis: "x" | "y") => {
   const isReverse = min > max;
   const start = getFinal(min, !isReverse);
   const end = getFinal(max, isReverse);
   const diff = isReverse ? -offset.value : offset.value;
 
   let current = start;
-  const lines: DireLine[] = [];
+  const pushPoint = (items: number[], data: number, start: boolean) => {
+    const p = rect.value![start ? 0 : 1][axis === "x" ? "y" : "x"];
+    if (axis === "y") {
+      items.push(p, data);
+    } else {
+      items.push(data, p);
+    }
+  };
+
+  const dividing: number[] = [];
+  const children: number[] = [];
+  let step = 0;
+  let prevCurrent = 0;
   while (diff > 0 ? current <= end : current >= end) {
-    const item: DireLine = {
-      dividing: current,
-      children: [],
-    };
+    pushPoint(dividing, current, !!((step - 1) % 2));
+    pushPoint(dividing, current, !!(step % 2));
 
     const cOffset = ((diff > 0 ? -1 : 1) * offset.value) / 5;
     for (let i = 1; i < 5; i++) {
-      item.children.push(current + cOffset * i);
+      const cCurrent = current + cOffset * i;
+      pushPoint(children, cCurrent, !!((i - 1) % 2));
+      pushPoint(children, cCurrent, !!(i % 2));
     }
-    lines.push(item);
+    prevCurrent = current;
     current += diff;
+    step++;
   }
-  return lines;
+  return { dividing, children };
 };
 
-const hLines = ref<DireLine[]>([]);
-const vLines = ref<DireLine[]>([]);
+const hConfig = ref<{ dividing: number[]; children: number[] }>();
+const vConfig = ref<{ dividing: number[]; children: number[] }>();
 watch(
   rect,
   debounce(() => {
     if (!rect.value) {
-      hLines.value = [];
-      vLines.value = [];
+      hConfig.value = vConfig.value = undefined;
     } else {
-      hLines.value = getLines(rect.value[0].y, rect.value[1].y);
-      vLines.value = getLines(rect.value[0].x, rect.value[1].x);
+      hConfig.value = getLines(rect.value[0].y, rect.value[1].y, "y");
+      vConfig.value = getLines(rect.value[0].x, rect.value[1].x, "x");
     }
-  }, 16),
+  }, 10),
   { immediate: true }
 );
 </script>

+ 29 - 0
src/core/helper/back.vue

@@ -0,0 +1,29 @@
+<template>
+  <v-rect :config="config" ref="shape" />
+</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();
+const econfig = useExpose().config;
+const config = computed(
+  () =>
+    econfig.back && {
+      ...invConfig.value,
+      listening: false,
+      ...size.value,
+      ...econfig.back,
+      fill: econfig.back.color,
+    }
+);
+</script>

+ 54 - 0
src/core/helper/facade.vue

@@ -0,0 +1,54 @@
+<template>
+  <template v-for="config in borderConfigs">
+    <v-rect :config="config" />
+  </template>
+  <template v-for="config in lineConfigs">
+    <v-line :config="config" />
+  </template>
+</template>
+<script lang="ts" setup>
+import { computed, watchEffect } from "vue";
+import { useExpose } from "../hook/use-expose";
+import { useResize } from "../hook/use-event";
+import { normalPadding } from "@/utils/bound";
+
+const size = useResize();
+const econfig = useExpose().config;
+
+const borderConfigs = computed(() => {
+  if (!econfig.border || !size.value) return [];
+  const bds = Array.isArray(econfig.border) ? econfig.border : [econfig.border];
+
+  return bds.map((border) => {
+    const margin = normalPadding(border.margin);
+    return {
+      x: margin[3],
+      y: margin[0],
+      width: size.value!.width - margin[1] - margin[3],
+      height: size.value!.height - margin[0] - margin[2],
+      strokeWidth: border.lineWidth,
+      stroke: border.color,
+      opacity: border.opacity,
+      listening: false,
+    };
+  });
+});
+
+const lineConfigs = computed(() => {
+  if (!econfig.margin || !size.value) return [];
+  const mrs = normalPadding(econfig.margin);
+  const color = econfig.back?.color || "#fff";
+  const rect = [
+    [0, 0],
+    [size.value.width, 0],
+    [size.value.width, size.value.height],
+    [0, size.value.height],
+  ];
+  return rect.map((point, ndx) => ({
+    listening: false,
+    stroke: color,
+    points: [...point, ...rect[(ndx + 1) % rect.length]],
+    strokeWidth: mrs[ndx] * 2,
+  }));
+});
+</script>

+ 16 - 4
src/core/helper/split-line.vue

@@ -19,6 +19,8 @@ import { useResize } from "../hook/use-event";
 import { Pos } from "@/utils/math";
 import { TextConfig } from "konva/lib/shapes/Text";
 import { Transform } from "konva/lib/Util";
+import { useExpose } from "../hook/use-expose";
+import { normalPadding } from "@/utils/bound";
 
 const style = {
   stroke: "#000",
@@ -48,19 +50,29 @@ const center = computed(() => {
     y: size.value.height / 2,
   };
 });
+
+const { config } = useExpose();
 const rang = computed(() => {
   if (!size.value) return;
+  const mrs = normalPadding(config.margin || 0);
+
   return {
-    lt: { x: props.showOffset, y: props.showOffset },
+    lt: { x: props.showOffset + mrs[3], y: props.showOffset + mrs[0] },
     rb: {
-      x: size.value.width - props.showOffset,
-      y: size.value.height - props.showOffset,
+      x: size.value.width - props.showOffset - mrs[1],
+      y: size.value.height - props.showOffset - mrs[2],
     },
   };
 });
 
 const compsInfo = useComponentsAttach(
-  (type, item) => components[type].getSnapPoints(item as any)
+  (type, item) => {
+    if (type === "icon" && (item as any).fixScreen) {
+      return [];
+    } else {
+      return components[type].getSnapPoints(item as any);
+    }
+  }
   // ["polygon"]
 );
 onUnmounted(compsInfo.cleanup);

+ 62 - 31
src/core/hook/use-draw.ts

@@ -36,6 +36,7 @@ import { useHistory, useHistoryAttach } from "./use-history";
 import penA from "../assert/cursor/pic_pen_a.ico";
 import penR from "../assert/cursor/pic_pen_r.ico";
 import { useCurrentZIndex } from "./use-layer";
+import { useViewerInvertTransform, useViewerTransform } from "./use-viewer";
 
 type PayData<T extends ShapeType> = ComponentValue<T, "addMode"> extends "area"
   ? Area
@@ -60,7 +61,8 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
   const can = useCan();
   const interactiveProps = useInteractiveProps();
   const conversion = useConversionPosition(true);
-  const currentZIndex = useCurrentZIndex()
+  const currentZIndex = useCurrentZIndex();
+  const store = useStore();
 
   let isEnter = false;
   const enter = () => {
@@ -77,23 +79,30 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
   };
 
   return {
+    delShape(id: string) {
+      const type = store.getType(id);
+      type && store.delItem(type, id);
+    },
     addShape: <T extends ShapeType>(
       shapeType: T,
       preset: Partial<DrawItem<T>> = {},
-      data: PayData<T>,
+      data?: PayData<T>,
       pixel = false
     ) => {
+      console.log(can.dragMode)
       if (!can.drawMode) {
         throw "当前状态不允许添加";
       }
       enter();
+      data = (data || {}) as PayData<T>;
       if (pixel) {
         data = (
           Array.isArray(data) ? data.map(conversion) : conversion(data)
         ) as PayData<T>;
       }
+
       if (!preset.zIndex) {
-        preset.zIndex = currentZIndex.max + 1 
+        preset.zIndex = currentZIndex.max + 1;
       }
       interactiveProps.value = {
         type: shapeType,
@@ -115,7 +124,7 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
         throw "当前状态不允许添加";
       }
       if (!preset.zIndex) {
-        preset.zIndex = currentZIndex.max + 1 
+        preset.zIndex = currentZIndex.max + 1;
       }
       interactiveProps.value = {
         type: shapeType,
@@ -127,7 +136,7 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
     },
     quitDrawShape: () => {
       leave();
-      console.error('quit')
+      console.error("quit");
       interactiveProps.value = void 0;
     },
   };
@@ -219,7 +228,9 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
   const obj = components[type] as Components[T];
   const beforeHandler = usePointBeforeHandler(true, true);
   const processors = useStoreRenderProcessors();
+  const viewTransform = useViewerTransform();
   const conversionPosition = useConversionPosition(true);
+  const history = useHistory()
   const store = useStore();
   const processorIds = processors.register(() => DrawShape);
   const clear = () => {
@@ -242,14 +253,17 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
       beforeHandler.clear();
       let geo: SnapPoint[] | undefined;
       if (items.length && getSnapGeo) {
-        const item = obj.interactiveFixData(
-          {...items[0]} as any,
-          {
-            consumed: ia.consumedMessage,
+        const item = obj.interactiveFixData({
+          info: {
             cur: conversionPosition(p),
+            consumed: ia.consumedMessage,
             action: MessageAction.update,
-          } as any
-        );
+          },
+          data: items[0],
+          history,
+          viewTransform: viewTransform.value,
+          store,
+        } as any);
         geo = getSnapGeo(item as any);
       }
       return beforeHandler.transform(p, geo, !geo);
@@ -257,14 +271,13 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
   });
 
   const addItem = (cur: PayData<T>) => {
-    let item: any = obj.interactiveToData(
-      {
-        consumed: ia.consumedMessage,
-        cur,
-        action: MessageAction.add,
-      } as any,
-      ia.preset as any
-    );
+    let item: any = obj.interactiveToData({
+      info: { cur, consumed: ia.consumedMessage, action: MessageAction.add },
+      preset: ia.preset,
+      history,
+      viewTransform: viewTransform.value,
+      store,
+    } as any);
     if (!item) return;
     item = reactive(item);
 
@@ -285,10 +298,16 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
       watch(
         cur,
         () =>
-          obj.interactiveFixData(item, {
-            consumed: ia.consumedMessage,
-            cur,
-            action: MessageAction.update,
+          obj.interactiveFixData({
+            info: {
+              cur,
+              consumed: ia.consumedMessage,
+              action: MessageAction.update,
+            },
+            data: item,
+            history,
+            viewTransform: viewTransform.value,
+            store,
           } as any),
         { deep: true }
       ),
@@ -319,7 +338,7 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
 // 拖拽面积确定组件
 export const useInteractiveDrawAreas = <T extends ShapeType>(type: T) => {
   const cursor = useCursor();
-  let cursorPop: () => void
+  let cursorPop: () => void;
   return useInteractiveDrawTemp({
     type,
     useIA: useInteractiveAreas,
@@ -335,7 +354,7 @@ export const useInteractiveDrawAreas = <T extends ShapeType>(type: T) => {
 
 export const useInteractiveDrawDots = <T extends ShapeType>(type: T) => {
   const cursor = useCursor();
-  let cursorPop: () => void
+  let cursorPop: () => void;
   return useInteractiveDrawTemp({
     type,
     useIA: useInteractiveDots,
@@ -418,6 +437,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
   const history = useHistory();
   const processors = useStoreRenderProcessors();
   const store = useStore();
+  const viewTransform = useViewerTransform();
   const downKeys = useDownKeys();
   const free = computed(() => downKeys.has("Control"));
   const processorIds = processors.register(() => {
@@ -427,7 +447,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
   // 可能历史空间会撤销 重做更改到正在绘制的组件
   const currentCursor = ref(penA);
   const cursor = useCursor();
-  let cursorPop: ReturnType<typeof cursor.push> | null = null; 
+  let cursorPop: ReturnType<typeof cursor.push> | null = null;
   let stopWatch: (() => void) | null = null;
   const ia = useInteractiveDots({
     shapeType: type,
@@ -435,7 +455,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
     enter() {
       cursorPop = cursor.push(currentCursor.value);
       watch(currentCursor, () => {
-        cursorPop?.set(currentCursor.value)
+        cursorPop?.set(currentCursor.value);
       });
     },
     quit: () => {
@@ -484,7 +504,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
     currentCursor.value = penA;
     let pen: null | ReturnType<typeof penUpdatePoints> = null;
     if (!free.value) {
-      pen = penUpdatePoints(messages.value, cur, type === "line");
+      pen = penUpdatePoints(messages.value, cur, type !== "polygon");
       consumed = pen.points;
       cur = pen.cur;
     }
@@ -527,7 +547,13 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
     }
     let item: any = items.length === 0 ? null : items[0];
     if (!item) {
-      item = obj.interactiveToData(setMessage(dot), ia.preset as any);
+      item = obj.interactiveToData({
+        preset: ia.preset as any,
+        info: setMessage(dot),
+        viewTransform: viewTransform.value,
+        history,
+        store,
+      });
       if (!item) return;
       items[0] = item = reactive(item);
     }
@@ -538,8 +564,13 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
     }
 
     const update = () => {
-      obj.interactiveFixData(item, setMessage(dot));
-      // console.log(JSON.parse(JSON.stringify(item)))
+      obj.interactiveFixData({
+        data: item,
+        info: setMessage(dot),
+        viewTransform: viewTransform.value,
+        history,
+        store,
+      });
     };
 
     stopWatch = mergeFuns(

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

@@ -1,4 +1,3 @@
-import { string } from "three/webgpu";
 import { listener } from "../../utils/event.ts";
 import { installGlobalVar, useStage } from "./use-global-vars.ts";
 import { nextTick, ref, watchEffect } from "vue";
@@ -33,6 +32,7 @@ export const useGlobalResize = installGlobalVar(() => {
   const stage = useStage();
   const size = ref<{ width: number; height: number }>();
   const setSize = () => {
+    if (fix.value) return;
     const container = stage.value?.getStage().container();
     if (container) {
       container.style.setProperty("display", "none");
@@ -53,11 +53,44 @@ export const useGlobalResize = installGlobalVar(() => {
       nextTick(() => stopWatch());
     }
   });
-  useListener("resize", setSize, window);
+  let unResize = listener(window, 'resize', setSize)
+  const fix = ref(false);
+  let unWatch: (() => void) | null = null;
+
+  const setFixSize = (fixSize: { width: number; height: number } | null) => {
+    if (fixSize) {
+      size.value = { ...fixSize };
+      unWatch && unWatch();
+      unWatch = watchEffect(() => {
+        const $stage = stage.value?.getStage();
+        if ($stage) {
+          $stage.width(fixSize.width);
+          $stage.height(fixSize.height);
+          nextTick(() => unWatch && unWatch());
+        }
+      });
+    }
+    if (fix.value && !fixSize) {
+      unResize = listener(window, 'resize', setSize)
+      fix.value = false;
+      nextTick(setSize)
+    } else if (!fix.value && fixSize) {
+      fix.value = true;
+      unResize();
+    }
+  };
 
   return {
-    updateSize: setSize,
-    size,
+    var: {
+      setFixSize: setFixSize,
+      updateSize: setSize,
+      size,
+      fix,
+    },
+    onDestroy: () => {
+      fix || unResize();
+      unWatch && unWatch();
+    },
   };
 }, Symbol("resize"));
 
@@ -75,7 +108,7 @@ export const usePreemptiveListener = installGlobalVar(() => {
       const $stage = stage.value!.getStage();
       const dom = $stage.container() as any;
       eventDestorys[eventName] = listener(dom, eventName as any, (ev: any) => {
-        console.log(cbs[eventName], cbs[eventName][0])
+        console.log(cbs[eventName], cbs[eventName][0]);
         cbs[eventName][0].call(this, ev);
       });
     }
@@ -113,7 +146,7 @@ export const usePreemptiveListener = installGlobalVar(() => {
         });
       }
     });
-    return stopWatch
+    return stopWatch;
   };
 
   return on;

+ 31 - 8
src/core/hook/use-expose.ts

@@ -1,4 +1,5 @@
 import {
+  installGlobalVar,
   useCursor,
   useDownKeys,
   useInstanceProps,
@@ -70,7 +71,7 @@ export const useAutoService = () => {
   const downKeys = useDownKeys();
   const mode = useMode();
   const cursor = useCursor();
-  const { set: setCursor } = cursor.push('initial')
+  const { set: setCursor } = cursor.push("initial");
 
   watchEffect(() => {
     let style: string | null = null;
@@ -81,9 +82,9 @@ export const useAutoService = () => {
     } else if (status.hovers.length) {
       style = "pointer";
     } else {
-      style = "initial"
+      style = "initial";
     }
-    setCursor(style)
+    setCursor(style);
   });
 
   // 自动保存历史及恢复服务
@@ -138,7 +139,14 @@ type PickParams<K extends keyof Stage, O extends string> = Stage[K] extends (
   ? Omit<Required<Parameters<Stage[K]>>[0], O>
   : never;
 
-export const useExpose = () => {
+export type Border = {
+  color?: string;
+  opacity: number;
+  margin: number[] | number;
+  lineWidth: number;
+};
+
+export const useExpose = installGlobalVar(() => {
   const mode = useMode();
   const interactiveProps = useInteractiveProps();
   const stage = useStage();
@@ -146,11 +154,26 @@ export const useExpose = () => {
   const store = useStore();
   const history = useHistory();
   const viewer = useViewer().viewer;
-  const { updateSize } = useGlobalResize();
+  const { updateSize, setFixSize, size } = useGlobalResize();
   const config = reactive({
     showGrid: true,
     showLabelLine: true,
-  });
+    get size() {
+      return size.value!;
+    },
+    set size(csize: { width: number; height: number } | null) {
+      setFixSize(csize);
+    },
+  }) as Config;
+
+  type Config = {
+    showGrid: boolean;
+    showLabelLine: boolean;
+    back?: { color?: string; opacity: number };
+    border?: Border | Border[];
+    margin?: number | number[];
+    size?: {width: number, height: number}
+  };
 
   const exposeBlob = (config?: PickParams<"toBlob", "callback">) => {
     const $stage = stage.value!.getStage();
@@ -169,7 +192,7 @@ export const useExpose = () => {
   return {
     ...useInteractiveDrawShapeAPI(),
     get stage() {
-      const $store = stage.value!.getStage();
+      const $store = stage.value?.getStage();
       return $store;
     },
     exposeBlob,
@@ -185,4 +208,4 @@ export const useExpose = () => {
     presetAdd: interactiveProps,
     config,
   };
-};
+});

+ 8 - 8
src/core/hook/use-global-vars.ts

@@ -182,14 +182,14 @@ export const useMode = installGlobalVar(() => {
       modes.forEach((mode) => modeStack.value.delete(mode));
     },
   };
-  if (import.meta.env.DEV) {
-    watchEffect(
-      () => {
-        console.error([...modeStack.value.values()].join(","));
-      },
-      { flush: "sync" }
-    );
-  }
+  // if (import.meta.env.DEV) {
+  //   watchEffect(
+  //     () => {
+  //       console.error([...modeStack.value.values()].join(","));
+  //     },
+  //     { flush: "sync" }
+  //   );
+  // }
   return modeStack;
 }, Symbol("mode"));
 export const useCan = installGlobalVar(() => {

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

@@ -7,7 +7,7 @@ import { copy } from "@/utils/shared";
 import { useStoreRaw } from "../store/store";
 
 type HistoryItem = { attachs: string; data: string };
-class DrawHistory {
+export class DrawHistory {
   history = new SingleHistory<HistoryItem>();
   hasUndo = this.history.hasUndo;
   hasRedo = this.history.hasRedo;
@@ -151,11 +151,11 @@ class DrawHistory {
   onceTrack(fn: () => void) {
     this.onceFlag = true;
     fn();
+    this.onceFlag = false;
     if (this.onceHistory) {
       this.push(this.onceHistory);
       this.onceHistory = null;
     }
-    this.onceFlag = false;
   }
 
   clearCurrent(): void {

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

@@ -130,7 +130,7 @@ export const useGetStaticZIndex = () => {
 
 export const useShapeStaticZindex = (shape: Ref<DC<EntityShape> | undefined>) => {
   const getStaticZindex = useGetStaticZIndex()
-  watchEffect((onCleanup) => {
+  watch(shape, (_a, _b, onCleanup) => {
     if (shape.value) {
       const [indexs, desIndex] = getStaticZindex()
       shape.value.getNode().zIndex(indexs[0])

+ 97 - 52
src/core/hook/use-mouse-status.ts

@@ -1,12 +1,4 @@
-import {
-  computed,
-  reactive,
-  ref,
-  Ref,
-  toRaw,
-  watch,
-  watchEffect,
-} from "vue";
+import { computed, reactive, ref, Ref, toRaw, watch, watchEffect } from "vue";
 import { DC, EntityShape } from "../../deconstruction";
 import { Shape } from "konva/lib/Shape";
 import {
@@ -21,43 +13,89 @@ import { Stage } from "konva/lib/Stage";
 import { listener } from "../../utils/event.ts";
 import { inRevise, mergeFuns } from "../../utils/shared.ts";
 import { ComponentValue, DrawItem, ShapeType } from "../components";
-import { shapeTreeContain } from "../../utils/shape.ts";
+import { shapeTreeContain, shapeTreeContains } from "../../utils/shape.ts";
 import {
   usePointerIsTransformerInner,
   useTransformer,
 } from "./use-transformer.ts";
 import { useAniamtion } from "./use-animation.ts";
 import { KonvaEventObject } from "konva/lib/Node";
-import { useFormalLayer } from "./use-layer.ts";
-import { Layer } from "konva/lib/Layer";
 import { useStore } from "../store/index.ts";
 import { Group } from "konva/lib/Group";
 
-export const getHoverShape = (stage: Stage, layer: Layer) => {
-  const hover = ref<EntityShape>();
+const stageHoverMap = new WeakMap<
+  Stage,
+  { result: Ref<EntityShape | undefined>; count: number; des: () => void }
+>();
+export const getHoverShape = (stage: Stage) => {
+  let isStop = false;
+  const stop = () => {
+    if (isStop || !stageHoverMap.has(stage)) return;
+    isStop = true;
+    const data = stageHoverMap.get(stage)!;
+    if (--data.count <= 0) {
+      data.des();
+    }
+  };
 
+  if (stageHoverMap.has(stage)) {
+    const data = stageHoverMap.get(stage)!;
+    ++data.count;
+    return [data.result, stop] as const;
+  }
+
+  const hover = ref<EntityShape>();
   const enterHandler = (ev: KonvaEventObject<any, Stage>) => {
     const target = ev.target;
     hover.value = target;
-    target.off("pointerleave", leaveHandler);
-    target.on("pointerleave", leaveHandler as any);
+    target.off(`pointerleave`, leaveHandler);
+    target.on(`pointerleave`, leaveHandler as any);
   };
 
   const leaveHandler = () => {
     if (hover.value) {
-      hover.value.off("pointerleave", leaveHandler);
+      hover.value.off(`pointerleave`, leaveHandler);
       hover.value = undefined;
     }
   };
 
-  stage.on("pointerenter", enterHandler);
-  return [
-    hover,
-    () => {
-      stage.off("pointerenter", enterHandler);
+  stage.on(`pointerenter`, enterHandler);
+  stageHoverMap.set(stage, {
+    result: hover,
+    count: 1,
+    des: () => {
+      stage.off(`pointerenter`, enterHandler);
       leaveHandler();
+      stageHoverMap.delete(stage);
+    },
+  });
+  return [hover, stop] as const;
+};
+
+export const useShapeIsHover = (shape: Ref<DC<EntityShape> | undefined>) => {
+  const stage = useStage();
+  const isHover = ref(false);
+  const stop = watch(
+    () => ({ stage: stage.value?.getNode(), shape: shape.value?.getNode() }),
+    ({ stage, shape }, _, onCleanup) => {
+      if (!stage || !shape) {
+        isHover.value = false;
+        return;
+      }
+
+      const [hoverShape, stopHoverListener] = getHoverShape(stage);
+
+      watchEffect(() => {
+        isHover.value = !!(
+          hoverShape.value && shapeTreeContain([shape], toRaw(hoverShape.value))
+        );
+      });
+      onCleanup(stopHoverListener);
     },
-  ] as const;
+    { immediate: true }
+  );
+
+  return [isHover, stop] as const;
 };
 
 export const useShapeIsTransformerInner = () => {
@@ -96,7 +134,6 @@ export const useShapeIsTransformerInner = () => {
 export const useMouseShapesStatus = installGlobalVar(() => {
   const can = useCan();
   const stage = useStage();
-  const formatLayer = useFormalLayer();
   const listeners = ref([]) as Ref<EntityShape[]>;
   const hovers = ref([]) as Ref<EntityShape[]>;
   const press = ref([]) as Ref<EntityShape[]>;
@@ -109,40 +146,47 @@ export const useMouseShapesStatus = installGlobalVar(() => {
     let downTarget: EntityShape | null;
 
     const prevent = computed(() => keys.has(" "));
-    const [hover, hoverDestory] = getHoverShape(stage, formatLayer.value!);
+    const [hover, hoverDestory] = getHoverShape(stage);
     const hoverChange = (onCleanup: any) => {
       if (prevent.value) {
         return;
       }
 
-      const pHover =
-        hover.value && shapeTreeContain(listeners.value, hover.value);
-      // TODO首先确定之前的有没有离开
-      if (hovers.value.length && hovers.value[0] !== pHover) {
-        const check = hovers.value[0];
-        const [inner] = shapeIsTransformerInner(check);
-        onCleanup(
-          watchEffect(() => {
-            if (!inner.value && !prevent.value) {
-              hovers.value.pop();
-            }
-          })
-        );
-      } else if (pHover) {
-        hovers.value[0] = pHover;
-      } else if (hovers.value.length) {
-        hovers.value.pop();
-      }
+      hovers.value = hover.value
+        ? shapeTreeContains(listeners.value, hover.value)
+        : [];
+
+      // const pHover =
+      //   hover.value && shapeTreeContain(listeners.value, hover.value);
+
+      // // TODO首先确定之前的有没有离开
+      // if (hovers.value.length && hovers.value[0] !== pHover) {
+      //   hovers.value.pop();
+      //   console.log('omg?')
+      //   const check = hovers.value[0];
+      //   const [inner] = shapeIsTransformerInner(check);
+      //   onCleanup(
+      //     watchEffect(() => {
+      //       if (!inner.value && !prevent.value) {
+      //         hovers.value.pop();
+      //       }
+      //     })
+      //   );
+      // } else if (pHover) {
+      //   hovers.value[0] = pHover;
+      // } else if (hovers.value.length) {
+      //   hovers.value.pop();
+      // }
     };
 
     const stopHoverCheck = watch(
-      () => [hover.value, prevent.value, hovers.value[0]],
+      () => [hover.value, prevent.value],
       (_a, _b, onCleanup) => hoverChange(onCleanup)
     );
 
     let downTime: number;
     stage.on("pointerdown.mouse-status", (ev) => {
-      downTime = Date.now()
+      downTime = Date.now();
       if (prevent.value) return;
       const target = shapeTreeContain(listeners.value, ev.target);
       if (target && !press.value.includes(target)) {
@@ -151,7 +195,7 @@ export const useMouseShapesStatus = installGlobalVar(() => {
       downTarget = target;
     });
 
-    let upCount = 0
+    let upCount = 0;
     return mergeFuns(
       stopHoverCheck,
       hoverDestory,
@@ -214,9 +258,9 @@ export const useMouseShapesStatus = installGlobalVar(() => {
           .map(toRaw);
       },
       set: (val: EntityShape[]) => {
-        shapes.value = val
-      }
-  });
+        shapes.value = val;
+      },
+    });
   const status = reactive({
     hovers: getShapes(hovers),
     actives: getShapes(actives),
@@ -251,8 +295,10 @@ export const useMouseShapeStatus = (
       active: status.actives.includes($shape),
       press: status.press.includes($shape),
       select: status.selects.includes($shape),
-      pause: () => shape.value?.getNode() && status.pause(shape.value?.getNode()),
-      resume: () => shape.value?.getNode() && status.resume(shape.value?.getNode()),
+      pause: () =>
+        shape.value?.getNode() && status.pause(shape.value?.getNode()),
+      resume: () =>
+        shape.value?.getNode() && status.resume(shape.value?.getNode()),
     };
   });
 
@@ -264,7 +310,6 @@ export const useMouseShapeStatus = (
 
         status.listeners.push(shape);
         onCleanup(() => {
-          console.log('取消监听', shape.id())
           for (const key in status) {
             const k = key as keyof typeof status;
             if (Array.isArray(status[k])) {

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

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

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

@@ -33,7 +33,7 @@ import {
 import { Transform } from "konva/lib/Util";
 import { useCacheUnitTransform, useViewerInvertTransform } from "./use-viewer";
 import { MathUtils } from "three";
-import { arrayInsert, copy, mergeFuns, rangMod } from "@/utils/shared";
+import { arrayInsert, mergeFuns, rangMod } from "@/utils/shared";
 import { useTestPoints } from "./use-debugger";
 
 export type SnapInfo = ComponentSnapInfo & Pick<DrawItem, "id">;

+ 19 - 12
src/core/hook/use-transformer.ts

@@ -1,4 +1,4 @@
-import { useMouseShapeStatus } from "./use-mouse-status.ts";
+import { useMouseShapeStatus, useShapeIsHover } from "./use-mouse-status.ts";
 import { Ref, ref, watch } from "vue";
 import { DC, EntityShape } from "../../deconstruction";
 import {
@@ -232,6 +232,7 @@ export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
   };
 
   watch(
+    
     () =>
       (can.editMode || mode.include(Mode.update)) &&
       (status.value.active || status.value.hover),
@@ -280,6 +281,9 @@ export const useShapeTransformer = <T extends EntityShape>(
     const updateTransform = () => {
       if (!can.dragMode) return;
       let appleTransform = rep.tempShape.getTransform().copy();
+      if (appleTransform.m.some((m) => m === null || Number.isNaN(m))) {
+        return;
+      }
       if (handlerTransform) {
         appleTransform = handlerTransform(appleTransform);
         setShapeTransform(rep.tempShape, appleTransform);
@@ -290,7 +294,7 @@ export const useShapeTransformer = <T extends EntityShape>(
     rep.tempShape.on("transform.shapemer", updateTransform);
 
     const boundHandler = () => {
-      rep.update && rep.update()
+      rep.update && rep.update();
     };
     $shape.on("bound-change", boundHandler);
 
@@ -324,7 +328,8 @@ export const useShapeTransformer = <T extends EntityShape>(
       () => status.value.active,
       (active, _, onCleanup) => {
         const parent = $shape.parent;
-        if (!(active && parent)) return;
+        const rect = rep.tempShape.getClientRect();
+        if (!active || !parent || rect.width === 0 || rect.height === 0) return;
         const oldConfig: TransformerConfig = {};
 
         for (const key in transformerConfig) {
@@ -335,7 +340,7 @@ export const useShapeTransformer = <T extends EntityShape>(
         transformer.nodes([rep.tempShape]);
         transformer.queueShapes.value = [$shape];
         parent.add(transformer);
-        rep.init && rep.init()
+        rep.init && rep.init();
 
         let isEnter = false;
         const downHandler = () => {
@@ -373,7 +378,7 @@ export const useShapeTransformer = <T extends EntityShape>(
 
         const stopLeaveUpdate = watch(
           () => getComponentData($shape).value,
-          (val) => {
+          () => {
             rep.update && rep.update();
           },
           { flush: "post", deep: true }
@@ -401,7 +406,7 @@ export const useShapeTransformer = <T extends EntityShape>(
           transformer.off("pointerdown.shapemer", downHandler);
         });
       },
-      {immediate: true}
+      { immediate: true }
     );
 
     return () => {
@@ -480,7 +485,7 @@ export const transformerRepShapeHandler = <T extends EntityShape>(
 type GetRepShape<T extends EntityShape, K extends object> = (shape: T) => {
   shape: T;
   update?: (data: K, shape: T) => void;
-  init?:  (data: K, shape: T) => void;
+  init?: (data: K, shape: T) => void;
 };
 export type CustomTransformerProps<
   T extends BaseItem,
@@ -489,7 +494,11 @@ export type CustomTransformerProps<
   openSnap?: boolean;
   getRepShape?: GetRepShape<S, T>;
   beforeHandler?: (data: T, mat: Transform) => T;
-  handler?: (data: T, mat: Transform, raw?: Transform) => Transform | void | true;
+  handler?: (
+    data: T,
+    mat: Transform,
+    raw?: Transform
+  ) => Transform | void | true;
   callback?: (data: T, mat: Transform) => void;
   transformerConfig?: TransformerConfig;
 };
@@ -531,7 +540,7 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
         : data.value;
 
       let nTransform;
-      const raw = current
+      const raw = current;
       if (needSnap && (nTransform = needSnap[0](snapData))) {
         current = nTransform.multiply(current);
       }
@@ -565,7 +574,6 @@ export const useLineTransformer = <T extends LineTransformerData>(
   callback: (data: T) => void,
   genRepShape?: ($shape: Line) => Line
 ) => {
-  let tempShape: Line;
   let inverAttitude: Transform;
   let stableVs = data.value.points;
   let tempVs = data.value.points;
@@ -583,7 +591,7 @@ export const useLineTransformer = <T extends LineTransformerData>(
       // 顶点更新
       const transfrom = mat.copy().multiply(inverAttitude);
       data.points = tempVs = stableVs.map((v) => transfrom.point(v));
-      data.attitude = mat.m
+      data.attitude = mat.m;
     },
     callback(data, mat) {
       data.attitude = mat.m;
@@ -601,7 +609,6 @@ export const useLineTransformer = <T extends LineTransformerData>(
       repShape = (repShape as any).points
         ? repShape
         : (repShape as unknown as Group).findOne<Line>(".line")!;
-      tempShape = repShape;
       const update = (data: T) => {
         const attitude = new Transform(data.attitude);
         const inverMat = attitude.copy().invert();

+ 9 - 0
src/core/propertys/describes.json

@@ -38,6 +38,15 @@
       "max": 10
     }
   },
+  "rotate": {
+    "type": "num",
+    "label": "旋转角度",
+    "default": 0,
+    "props": {
+      "min": -180,
+      "max": 180
+    }
+  },
   "coverStrokeWidth": {
     "type": "num",
     "label": "背景边框粗细",

+ 2 - 2
src/core/propertys/index.ts

@@ -37,6 +37,7 @@ export type PropertyDescribes = Record<
     default?: PropertyValue<PropertyType>;
     props?: Partial<PropertyProps<PropertyType>>;
     value?: PropertyValue<PropertyType>;
+    sort?: number
   }
 >;
 export type PropertysData<T extends PropertyDescribes = PropertyDescribes> = {
@@ -46,8 +47,7 @@ export type PropertysData<T extends PropertyDescribes = PropertyDescribes> = {
 export type PropertyKey = keyof typeof originDescribes;
 export type PropertyKeys = PropertyKey[];
 
-export const mergeDescribes = (data: Ref<any>, defData: any, keys: PropertyKeys) => {
-  const describes: PropertyDescribes = {};
+export const mergeDescribes = (data: Ref<any>, defData: any, keys: PropertyKeys, describes: PropertyDescribes = {}) => {
   for (const key of keys) {
     if (!originDescribes[key]) {
       continue;

+ 11 - 5
src/core/propertys/mount.vue

@@ -1,9 +1,9 @@
 <template>
-  <Teleport :to="`#${DomMountId}`" v-if="stage">
+  <Teleport :to="`#${DomOutMountId}`" v-if="stage">
     <transition name="mount-fade">
       <div class="mount-layout" v-if="show">
         <div :size="8" class="mount-controller">
-          <div v-for="(val, key) in describes" :key="key" class="mount-item">
+          <div v-for="[key, val] in describeItems" :key="key" class="mount-item">
             <span class="label">{{ val.label }}</span>
             <component
               v-bind="describes[key].props"
@@ -23,12 +23,12 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, watch, watchEffect } from "vue";
-import { useMode, useStage, useTransformIngShapes } from "../hook/use-global-vars.ts";
+import { computed, ref, watch } from "vue";
+import { useMode, useStage } from "../hook/use-global-vars.ts";
 import { PropertyDescribes, propertyComponents } from "./index.ts";
 import { DC, EntityShape } from "@/deconstruction.js";
 import { useMouseShapeStatus } from "../hook/use-mouse-status.ts";
-import { DomMountId } from "@/constant/index.ts";
+import { DomOutMountId } from "@/constant/index.ts";
 import { Mode } from "@/constant/mode.ts";
 import { debounce } from "@/utils/shared.ts";
 
@@ -40,6 +40,12 @@ const props = defineProps<{
 }>();
 const emit = defineEmits<{ (e: "change"): void }>();
 
+const describeItems = computed(() => {
+  return Object.entries(props.describes).sort(
+    ([_a, a], [_b, b]) => (b.sort || -1) - (a.sort || -1)
+  );
+});
+
 const stage = useStage();
 const status = useMouseShapeStatus(computed(() => props.target));
 const mode = useMode();

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

@@ -1,10 +1,10 @@
 <template>
+  <!-- :real="store.getItemById(item.id)"
+  temp -->
   <ShapeComponent
     :data="(item as any)"
     v-for="item in tempItems"
-    temp
     :key="item.id"
-    :real="store.getItemById(item.id)"
     addMode
   />
 </template>

+ 59 - 25
src/core/renderer/renderer.vue

@@ -1,31 +1,51 @@
 <template>
-  <div class="draw-layout" @contextmenu.prevent :style="{ cursor: cursorStyle }">
-    <div class="mount-mask" :id="DomMountId" ref="maskMount" />
+  <div
+    class="draw-layout"
+    @contextmenu.prevent
+    :style="{ cursor: cursorStyle }"
+    ref="layout"
+  >
+    <div class="mount-mask" :id="DomOutMountId" />
+    <div
+      class="draw-content"
+      :style="fix && { width: size?.width + 'px', height: size?.height + 'px' }"
+    >
+      <div class="mount-mask" :id="DomMountId" />
 
-    <v-stage ref="stage" :config="size" v-if="maskMount">
-      <v-layer :config="viewerConfig" id="formal">
-        <!--	不可去除,去除后移动端拖拽会有溢出	-->
-        <BackGrid v-if="expose.config.showGrid" />
-        <ShapeGroup v-for="type in types" :type="type" :key="type" />
-      </v-layer>
-      <!--	临时组,提供临时绘画,以及高频率渲染	-->
-      <v-layer :config="viewerConfig" id="temp">
-        <template v-if="mode.include(Mode.draw)">
-          <TempShapeGroup v-for="type in types" :type="type" :key="type" />
-        </template>
-      </v-layer>
-      <v-layer id="helper">
-        <ActiveBoxs />
-        <SnapLines />
-        <SplitLine v-if="expose.config.showLabelLine" />
-        <Debugger v-if="isDev" />
-      </v-layer>
-    </v-stage>
+      <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"
+          />
+        </v-layer>
+        <!--	临时组,提供临时绘画,以及高频率渲染	-->
+        <v-layer :config="viewerConfig" id="temp">
+          <template v-if="mode.include(Mode.draw)">
+            <TempShapeGroup v-for="type in types" :type="type" :key="type" />
+          </template>
+        </v-layer>
+        <v-layer id="helper">
+          <ActiveBoxs />
+          <SnapLines />
+          <SplitLine v-if="expose.config.showLabelLine" />
+          <Debugger v-if="isDev" />
+          <Border />
+        </v-layer>
+      </v-stage>
+    </div>
   </div>
 </template>
 
 <script lang="ts" setup>
 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";
@@ -41,9 +61,9 @@ import {
   useStage,
 } from "../hook/use-global-vars.ts";
 import { useViewerTransformConfig } from "../hook/use-viewer.ts";
-import { useResize } from "../hook/use-event.ts";
+import { useGlobalResize, useResize } from "../hook/use-event.ts";
 import { useAutoService, useExpose } from "../hook/use-expose.ts";
-import { DomMountId } from "../../constant";
+import { DomMountId, DomOutMountId } from "../../constant";
 import { useStore } from "../store/index.ts";
 import { Mode } from "@/constant/mode.ts";
 import { computed, getCurrentInstance, ref } from "vue";
@@ -60,12 +80,17 @@ useAutoService();
 
 const isDev = import.meta.env.DEV;
 const stage = useStage();
-const size = useResize();
-const maskMount = ref<HTMLDivElement>();
+const { size, fix } = useGlobalResize();
+const layout = ref();
 const viewerConfig = useViewerTransformConfig();
 const types = Object.keys(components) as ShapeType[];
 const mode = useMode();
 
+const GroupComponentMap = types.reduce((map, type) => {
+  map[type] = components[type].GroupComponent || ShapeGroup;
+  return map;
+}, {} as any);
+
 const cursor = useCursor();
 const cursorStyle = computed(() => {
   if (cursor.value.includes(".")) {
@@ -85,6 +110,15 @@ defineExpose(expose);
   height: 100%;
   overflow: hidden;
   position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  .draw-content {
+    position: relative;
+    width: 100%;
+    height: 100%;
+  }
 }
 
 .mount-mask {

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

@@ -33,7 +33,7 @@ export const useStore = installGlobalVar(() => {
     "delItem",
     "setItem",
   ];
-  store.$onAction(({ args, name, after, store }) => {
+  store.$onAction(({ name, after, store }) => {
     if (!trackActions.includes(name)) return;
     const isInit = name === "setStore";
     after(() => {
@@ -46,6 +46,7 @@ export const useStore = installGlobalVar(() => {
   });
   return store;
 }, Symbol("store"));
+export type DrawStore = ReturnType<typeof useStore>
 
 export const useStoreRenderProcessors = installGlobalVar(() => {
   type Processor<T extends DrawItem = DrawItem> = (data: T) => Component;

+ 11 - 4
src/core/store/store.ts

@@ -18,11 +18,16 @@ export const useStoreRaw = defineStore('draw-data', {
 	actions: {
 		repStore(store: DrawData) {
 			const newStore = JSON.parse(JSON.stringify(store))
-			this.$patch({ data: newStore });
+			this.$patch(state => {
+				state.data = newStore
+			})
 		},
 		setStore(store: DrawData) {
 			const newStore = JSON.parse(JSON.stringify(store))
-			this.$patch({ version: 0, data: newStore });
+			this.$patch(state => {
+				state.data = newStore
+				state.version = 0
+			})
 		},
 		getItemNdx<T extends ShapeType>(type: T, id: string) {
 			const items = this.data[type]
@@ -31,12 +36,14 @@ export const useStoreRaw = defineStore('draw-data', {
 			}
 			return -1
 		},
+		getTypeItems<T extends ShapeType>(type: T): DrawItem<T>[] {
+			return this.data[type] as DrawItem<T>[] || []
+		},
 		getItem<T extends ShapeType>(type: T, id: string) {
 			const ndx = this.getItemNdx(type, id)
 			if (~ndx) return this.data[type]![ndx]
 		},
 		addItems<T extends ShapeType>(type: T, items: DrawItem<T>[]) {
-			console.log('add', items)
 			items.forEach(item => this.addItem(type, item))
 		},
 		addItem<T extends ShapeType>(type: T, item: DrawItem<T>) {
@@ -55,7 +62,7 @@ export const useStoreRaw = defineStore('draw-data', {
 				})
 			}
 		},
-		setItem<T extends ShapeType>(type: T, playData: { value: DrawItem<T>, id: string }) {
+		setItem<T extends ShapeType>(type: T, playData: { value: Partial<DrawItem<T>>, id: string }) {
 			const ndx = this.getItemNdx(type, playData.id)
 			if (~ndx) {
 				this.$patch(state => {

+ 0 - 17
src/example/fuse/views/header/header.vue

@@ -21,17 +21,6 @@
         </span>
       </div>
       <div>
-        <span class="operate" @click="draw.config.showGrid = !draw.config.showGrid">
-          {{ draw.config.showGrid ? "隐藏" : "显示" }}栅格<el-icon><Plus /></el-icon>
-        </span>
-        <span
-          class="operate"
-          @click="draw.config.showLabelLine = !draw.config.showLabelLine"
-        >
-          {{ draw.config.showLabelLine ? "隐藏" : "显示" }}标注线<el-icon
-            ><Plus
-          /></el-icon>
-        </span>
         <span class="operate" @click="draw.history.clearCurrent()">
           清除<el-icon><Plus /></el-icon>
         </span>
@@ -62,11 +51,6 @@
           VR<el-icon><Plus /></el-icon>
         </span>
       </div>
-      <div v-if="dev">
-        <span class="operate" @click="draw.toggleHit()">
-          碰撞检测<el-icon><Plus /></el-icon>
-        </span>
-      </div>
     </div>
     <div class="saves">
       <el-button type="primary" @click="emit('expose')" plain>导出</el-button>
@@ -86,7 +70,6 @@ import { animation } from "@/core/hook/use-animation.ts";
 
 const draw = useDraw();
 const bgFileInput = ref<HTMLInputElement | null>(null);
-const dev = import.meta.env.DEV;
 
 const emit = defineEmits<{ (e: "full"): void; (e: "save"): void; (e: "expose"): void }>();
 

+ 2 - 4
src/example/fuse/views/home.vue

@@ -6,7 +6,6 @@
       <div class="content" ref="drawEle">
         <DrawBoard
           v-if="drawEle"
-          id="asd"
           :handler-resource="handlerResource"
           ref="draw"
           :data="(data as any)"
@@ -19,7 +18,7 @@
 <script lang="ts" setup>
 import Header from "./header/header.vue";
 import Slide from "./slide/slide.vue";
-import { onUnmounted, ref, watch, watchEffect } from "vue";
+import { onUnmounted, ref, watch } from "vue";
 import { DrawExpose, DrawBoard } from "@/index";
 import { data, save } from "./init.ts";
 import { installDraw } from "./use-draw.ts";
@@ -36,7 +35,6 @@ const saveHandler = () => {
 };
 
 const handlerResource = async (file: File) => {
-  console.log("上传资源", file);
   return URL.createObjectURL(file);
 };
 
@@ -97,7 +95,7 @@ onUnmounted(
   }
 
   .content {
-    flex: 1;
+    width: calc(100% - 70px);
   }
 
   &.full {

+ 299 - 2
src/example/fuse/views/init.ts

@@ -614,8 +614,7 @@ const initData = {
       fill: "#000",
       fontFamily: "Calibri",
       fontSize: 30,
-      content:
-        "我爱人人人人爱我",
+      content: "我爱人人人人爱我",
       mat: [
         -1.4930798945178672e-15, 1.0000000000000104, -1.0000000000000155,
         -1.5971633030764742e-15, 1083.2463425727433, 95.00094657517106,
@@ -892,6 +891,304 @@ const initData = {
   ],
 };
 
+export const tInitData = {
+  serial: [
+    {
+      fontSize: 16,
+      id: "bb74bd30-2307-4b21-8187-d46970143c13",
+      createTime: 1740044984050,
+      lock: false,
+      zIndex: 1,
+      opacity: 1,
+      ref: false,
+      content: "1",
+      padding: 3,
+      mat: [1, 0, 0, 1, 147.109375, 193.734375],
+      radiusX: 11.030865786510143,
+      radiusY: 11.030865786510143,
+    },
+    {
+      fontSize: 16.000000000000004,
+      id: "d152d5cb-78e0-4dd0-b719-dd826d5f26e2",
+      createTime: 1740044984390,
+      lock: false,
+      zIndex: 1,
+      opacity: 1,
+      ref: false,
+      content: "2",
+      padding: 3,
+      mat: [1, 0, 0, 1, 718.1640625, 241.56640625],
+      radiusX: 11.030865786510143,
+      radiusY: 11.030865786510143,
+    },
+    {
+      fontSize: 16,
+      id: "9800c6d2-8bf8-4498-8add-174ecb6be7ba",
+      createTime: 1740044984918,
+      lock: false,
+      zIndex: 1,
+      opacity: 1,
+      ref: false,
+      content: "3",
+      padding: 3,
+      mat: [1, 0, 0, 1, 592.4601690730203, 529.20703125],
+      radiusX: 11.030865786510143,
+      radiusY: 11.030865786510143,
+    },
+    {
+      fontSize: 16,
+      id: "ade6a69a-9f19-4905-9d80-555b9dd8e2ba",
+      createTime: 1740044985418,
+      lock: false,
+      zIndex: 1,
+      opacity: 1,
+      ref: false,
+      content: "4",
+      padding: 3,
+      mat: [1, 0, 0, 1, 285.88671875, 565.828125],
+      radiusX: 11.030865786510143,
+      radiusY: 11.030865786510143,
+    },
+    {
+      fontSize: 16,
+      id: "9a8e3fed-fc1d-4979-83d4-9bef8e994510",
+      createTime: 1740044985856,
+      lock: false,
+      zIndex: 1,
+      opacity: 1,
+      ref: false,
+      content: "5",
+      padding: 3,
+      mat: [1, 0, 0, 1, 899.28125, 370.48828125],
+      radiusX: 11.030865786510143,
+      radiusY: 11.030865786510143,
+    },
+    {
+      fontSize: 16,
+      id: "4e7c431a-b5e4-4907-bdfb-ca6031c90fe0",
+      createTime: 1740044986325,
+      lock: false,
+      zIndex: 1,
+      opacity: 1,
+      ref: false,
+      content: "6",
+      padding: 3,
+      mat: [1, 0, 0, 1, 718.6328125, 136.68359375],
+      radiusX: 11.030865786510143,
+      radiusY: 11.030865786510143,
+    },
+  ],
+  table: [
+    {
+      stroke: "#d8000a",
+      strokeWidth: 1,
+      fontSize: 16,
+      align: "center",
+      fontStyle: "normal",
+      fontColor: "#d8000a",
+      id: "3822a4f6-3b84-4d5c-9507-6c71cac1e3df",
+      createTime: 1740044984356,
+      lock: false,
+      zIndex: 0,
+      opacity: 1,
+      ref: false,
+      notaddRow: true,
+      notaddCol: true,
+      key: "serial-table",
+      content: [
+        [
+          {
+            content: "序号",
+            readonly: true,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "描述",
+            readonly: true,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+        [
+          {
+            content: "1",
+            readonly: true,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "1",
+            readonly: false,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+        [
+          {
+            content: "2",
+            readonly: true,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "2",
+            readonly: false,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+        [
+          {
+            content: "3",
+            readonly: true,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "3",
+            readonly: false,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+        [
+          {
+            content: "4",
+            readonly: true,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "4",
+            readonly: false,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+        [
+          {
+            content: "5",
+            readonly: true,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "5",
+            readonly: false,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+        [
+          {
+            content: "6",
+            readonly: true,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "6",
+            readonly: false,
+            notdel: true,
+            width: 107.00000000000001,
+            height: 32.00000000000003,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+      ],
+      width: 214.00000000000003,
+      height: 224.00000000000014,
+      mat: [1, 0, 0, 1, 389.31602203651016, 223.13633453651013],
+    },
+  ],
+  icon: [
+    {
+      id: "5a61fdd6-65f0-4758-bf48-86493d85bd1f",
+      createTime: 1740130563350,
+      lock: false,
+      zIndex: 2,
+      opacity: 1,
+      ref: false,
+      url: "/icons/edit_compass.svg",
+      width: 100,
+      height: 100,
+      key: "compass",
+      fill: "#D8000A",
+      fixScreen: { right: 40, top: 40 },
+      mat: [1, 0, 0, 1, 0, 0],
+    },
+  ],
+};
+
 const dataStr = localStorage.getItem("draw-data");
 export const data = dataStr ? JSON.parse(dataStr) : initData;
 

+ 75 - 0
src/example/fuse/views/slide/draw-menu.ts

@@ -0,0 +1,75 @@
+import { themeColor } from "@/constant/help-style";
+import { DrawItem, ShapeType, shapeNames } from "@/index.ts";
+import { v4 as uuid } from "uuid";
+
+export type PresetAdd<T extends ShapeType = ShapeType> = {
+  type: T;
+  preset?: Partial<DrawItem<T>>;
+};
+
+const genDrawItem = <T extends ShapeType>(
+  type: T,
+  preset: PresetAdd<T>["preset"] = {}
+) => ({
+  name: shapeNames[type],
+  value: uuid(),
+  payload: { type, preset },
+});
+
+
+export const drawMenus = [
+  {
+    icon: "",
+    name: "绘制",
+    value: uuid(),
+    children: [
+      { icon: "", ...genDrawItem("line") },
+      { icon: "", ...genDrawItem("arrow")},
+      { icon: "", ...genDrawItem("rectangle") },
+      { icon: "", ...genDrawItem("circle") },
+      { icon: "", ...genDrawItem("triangle") },
+      { icon: "", ...genDrawItem("polygon") },
+    ],
+  },
+  {
+    icon: "",
+    name: "图例",
+    value: uuid(),
+    children: [
+      {
+        icon: "",
+        ...genDrawItem("icon", {
+          url: "/icons/BedsideCupboard.svg",
+          width: 100,
+          height: 100,
+          fill: themeColor,
+        }),
+        name: "vue",
+      },
+      {
+        icon: "",
+        ...genDrawItem("icon", {
+          url: "/icons/vue.svg",
+          width: 100,
+          height: 100,
+          stroke: "red",
+          strokeWidth: 1,
+          strokeScaleEnabled: false,
+        }),
+        name: "自定义",
+      },
+    ],
+  },
+  {
+    icon: "",
+    ...genDrawItem("text", { content: "文本" }),
+  },
+  {
+    icon: "",
+    ...genDrawItem("table", {}),
+  },
+  {
+    icon: "",
+    ...genDrawItem("serial", { content: "1" }),
+  },
+];

+ 62 - 0
src/example/fuse/views/slide/handler-menu.ts

@@ -0,0 +1,62 @@
+import { v4 as uuid } from "uuid";
+import { Draw } from "../use-draw";
+
+const setPaper = (draw: Draw, p: number[], scale: number) => {
+  const pad = 5 * scale;
+  const size = { width: p[0] * scale, height: p[1] * scale };
+  const margin = [pad, pad, pad, pad * 5];
+
+  draw.config.size = size;
+  draw.config.back = { color: "#fff", opacity: 1 };
+  draw.config.border = {
+    margin: margin,
+    lineWidth: 1,
+    color: "#000",
+    opacity: 1,
+  };
+  draw.config.margin = margin;
+}
+
+export const handlerMenus = [
+  {
+    icon: "",
+    name: "纸张",
+    children: [
+      {
+        value: uuid(),
+        icon: "",
+        name: "满屏",
+        handler: (draw: Draw) => {
+          draw.config.size = undefined
+          draw.config.back = { color: "#fff", opacity: 1 };
+          delete draw.config.margin
+          delete draw.config.border
+        }
+      },
+      {
+        value: uuid(),
+        icon: "",
+        name: "A4竖版",
+        handler: (draw: Draw) => setPaper(draw, [210, 297], 2.8),
+      },
+      {
+        value: uuid(),
+        icon: "",
+        name: "A4横版",
+        handler: (draw: Draw) => setPaper(draw, [297, 210], 3.8)
+      },
+      {
+        value: uuid(),
+        icon: "",
+        name: "A3竖版",
+        handler: (draw: Draw) => setPaper(draw, [297, 450], 1.8)
+      },
+      {
+        value: uuid(),
+        icon: "",
+        name: "A3横版",
+        handler: (draw: Draw) => setPaper(draw, [450, 297], 2.5)
+      }
+    ],
+  },
+];

+ 17 - 88
src/example/fuse/views/slide/menu.ts

@@ -1,30 +1,17 @@
-import { themeColor } from "@/constant/help-style";
-import { DrawItem, ShapeType, shapeNames } from "@/index.ts";
-import { v4 as uuid } from "uuid";
-import { toRaw } from "vue";
+import { reactive, toRaw } from "vue";
+import { Draw } from "../use-draw";
+import { drawMenus } from "./draw-menu";
+import { handlerMenus } from "./handler-menu";
 
-type PresetAdd<T extends ShapeType = ShapeType> ={
-  type: T;
-  preset?: Partial<DrawItem<T>>;
-}
-
-export type MenuItem<T extends ShapeType = ShapeType> = {
+export type MenuItem = {
   icon: string;
   name: string;
   value?: string;
-  children?: MenuItem<T>[];
-  payload?: PresetAdd<T>
+  children?: MenuItem[];
+  payload?: any;
+  handler?: (draw: Draw) => void;
 };
 
-const genItem = <T extends ShapeType>(
-  type: T,
-  preset: PresetAdd<T>["preset"] = {}
-) => ({
-  name: shapeNames[type],
-  value: uuid(),
-  payload: { type, preset },
-});
-
 export const getItem = (
   value: string,
   queryMenus = menus
@@ -40,13 +27,13 @@ export const getItem = (
   }
 };
 
-const eqPayload = (p1?: PresetAdd, p2?: PresetAdd) => {
+const eqPayload = (p1?: any, p2?: any) => {
   if (!p2 || !p1 || p1.type !== p2.type) return false;
   return !p1.preset || !p2.preset || toRaw(p1.preset) === toRaw(p2.preset);
 };
 
 export const getValue = (
-  payload: PresetAdd,
+  payload: any,
   queryMenus = menus
 ): string | undefined => {
   for (const menu of queryMenus) {
@@ -59,69 +46,11 @@ export const getValue = (
   }
 };
 
-export const menus: MenuItem[] = [
-  {
-    icon: "",
-    name: "绘制",
-    value: uuid(),
-    children: [
-      {
-        icon: "",
-        ...genItem("line"),
-      },
-      {
-        icon: "",
-        ...genItem("arrow"),
-      },
+export const menus: MenuItem[] = reactive([...drawMenus, ...handlerMenus]);
 
-      {
-        icon: "",
-        ...genItem("rectangle"),
-      },
-      {
-        icon: "",
-        ...genItem("circle"),
-      },
-      {
-        icon: "",
-        ...genItem("triangle"),
-      },
-      {
-        icon: "",
-        ...genItem("polygon"),
-      },
-    ],
-  },
-  {
-    icon: "",
-    name: "图例",
-    value: uuid(),
-    children: [
-      {
-        icon: "",
-        ...genItem("icon", { url: '/icons/BedsideCupboard.svg', width: 100, height: 100, fill: themeColor }),
-        name: "vue",
-      },
-      {
-        icon: "",
-        ...genItem("icon", {
-          url: '/icons/vue.svg',
-          width: 100,
-          height: 100,
-          stroke: "red",
-          strokeWidth: 1,
-          strokeScaleEnabled: false,
-        }),
-        name: "自定义",
-      },
-    ],
-  },
-  {
-    icon: "",
-    ...genItem("text", { content: "文本" }),
-  },
-  {
-    icon: "",
-    ...genItem("table", { }),
-  }
-];
+
+if (import.meta.env.DEV) {
+  import('./test-menu').then(({testMenus}) => {
+    menus.push(...testMenus)
+  })
+}

+ 17 - 4
src/example/fuse/views/slide/slide.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="slide">
     <el-menu
-      :default-active="draw?.presetAdd && getValue(draw?.presetAdd)"
+      :default-active="active"
       class="slide-menu"
       @select="selectHandler"
       collapse
@@ -18,15 +18,28 @@ import { ElMenu } from "element-plus";
 import { getItem, getValue, menus } from "./menu.ts";
 import SlideItem from "./slide-item.vue";
 import { useDraw } from "../use-draw.ts";
+import { nextTick, ref, watch } from "vue";
 
 const draw = useDraw();
+const active = ref<string>();
+watch(
+  () => draw?.presetAdd && getValue(draw?.presetAdd),
+  (val) => {
+    active.value = val;
+  }
+);
+
 const selectHandler = (value: string) => {
   const item = getItem(value);
-  if (!item || !item.payload) throw "无效菜单";
-  draw.enterDrawShape(item.payload.type, item.payload.preset);
+  if (item?.handler) {
+    active.value = item.value;
+    item.handler(draw);
+    nextTick(() => (active.value = undefined));
+  } else if (item?.payload) {
+    draw.enterDrawShape(item.payload.type, item.payload.preset);
+  }
 };
 </script>
-/
 
 <style lang="scss" scoped>
 @use '../../styles/global';

+ 65 - 0
src/example/fuse/views/slide/test-menu.ts

@@ -0,0 +1,65 @@
+import { v4 as uuid } from "uuid";
+import { Draw } from "../use-draw";
+import { themeColor } from "@/constant/help-style";
+import { tInitData } from '../init'
+
+export const testMenus = [
+  {
+    icon: "",
+    name: "测试",
+    children: [
+      {
+        value: uuid(),
+        icon: "",
+        name: "切换栅栏显示",
+        handler: (draw: Draw) => {
+          draw.config.showGrid = !draw.config.showGrid
+        }
+      },
+      {
+        value: uuid(),
+        icon: "",
+        name: "切换标注线显示",
+        handler: (draw: Draw) => {
+          draw.config.showLabelLine = !draw.config.showLabelLine
+        }
+      },
+      {
+        value: uuid(),
+        icon: "",
+        name: "切换指南针显示",
+        handler: (draw: Draw) => {
+          const compass = draw.store.items.find((item) => item.key === "compass");
+          if (compass) {
+            draw.delShape(compass.id);
+          } else {
+            draw.addShape("icon", {
+              url: "/icons/edit_compass.svg",
+              width: 100,
+              height: 100,
+              key: "compass",
+              fill: themeColor,
+              fixScreen: { right: 40, top: 40 },
+            });
+          }
+        }
+      },
+      {
+        value: uuid(),
+        icon: "",
+        name: "碰撞检测",
+        handler: (draw: Draw) => {
+          draw.toggleHit()
+        }
+      },
+      {
+        value: uuid(),
+        icon: "",
+        name: "载入测试数据",
+        handler: (draw: Draw) => {
+          draw.store.setStore(tInitData)
+        }
+      },
+    ],
+  },
+];

+ 36 - 5
src/example/fuse/views/use-draw.ts

@@ -1,9 +1,40 @@
-import { inject, provide, Ref } from 'vue'
+import { inject, provide, Ref, ShallowUnwrapRef, watch } from "vue";
 import { DrawExpose } from "../../../index";
+import { themeColor } from "@/constant/help-style";
+import { mergeFuns } from "@/utils/shared";
 
-const actionKey = Symbol('drawAction');
+const initDraw = (draw: Draw) => {
+  draw.config.showLabelLine = false;
+  draw.config.showGrid = false;
+	draw.config.back = { color: "#fff", opacity: 1 };
+
+  const compass = draw.store.items.find((item) => item.key === "compass");
+  if (!compass) {
+		console.log('add')
+    draw.history.preventTrack(() => {
+      draw.addShape("icon", {
+        url: "/icons/edit_compass.svg",
+        width: 100,
+        height: 100,
+        key: "compass",
+        fill: themeColor,
+        fixScreen: { right: 40, top: 40 },
+      });
+    });
+  }
+  return mergeFuns([
+	])
+};
+
+const actionKey = Symbol("drawAction");
+export type Draw = ShallowUnwrapRef<DrawExpose>;
 export const installDraw = (drawRef: Ref<DrawExpose | undefined>) => {
-	provide(actionKey, drawRef)
-}
+  watch(() => drawRef?.value?.stage, (_a, _b, onCleanup) => {
+		if (drawRef.value) {
+    	onCleanup(initDraw(drawRef.value as unknown as Draw));
+		}
+  });
+  provide(actionKey, drawRef);
+};
 
-export const useDraw = () => inject<Ref<DrawExpose>>(actionKey)?.value!
+export const useDraw = () => inject<Ref<Draw>>(actionKey)?.value!;

+ 41 - 0
src/utils/bound.ts

@@ -0,0 +1,41 @@
+import { Pos } from "./math";
+
+export type FixScreen = {
+  left?: number;
+  top?: number;
+  right?: number;
+  bottom?: number;
+};
+export const getFixPosition = (
+  pos: FixScreen,
+  selfSize: { width: number; height: number },
+  screenSize: { width: number; height: number }
+) => {
+  let x = pos.left;
+  let y = pos.top;
+  if (typeof x !== "number" && typeof pos.right === "number") {
+    x = screenSize.width - selfSize.width - pos.right;
+  }
+  if (typeof y !== "number" && typeof pos.bottom === "number") {
+    y = screenSize.height - selfSize.height - pos.bottom;
+  }
+  return {
+    x,
+    y,
+  } as Pos;
+};
+
+
+export const normalPadding = (pad: number | number[]) => {
+  if (typeof pad === 'number') {
+    return [pad, pad, pad, pad]
+  } else if (pad.length === 1) {
+    return [pad[0], pad[0], pad[0], pad[0]]
+  } else if (pad.length === 2) {
+    return [pad[0], pad[1], pad[0], pad[1]]
+  } else if (pad.length === 3) {
+    return [pad[0], pad[1], pad[2], pad[0]]
+  } else {
+    return pad
+  }
+}

+ 5 - 5
src/utils/resource.ts

@@ -78,8 +78,8 @@ export let parseSvgContent = (svgContent: string): SVGParseResult => {
     helpDOM.innerHTML = svgContent;
     document.body.appendChild(helpDOM);
 
-    let width = 0;
-    let height = 0;
+    let right = 0;
+    let bottom = 0;
     let x = Number.MAX_VALUE;
     let y = Number.MAX_VALUE;
     const svgPaths = Array.from(helpDOM.querySelectorAll("path"));
@@ -87,8 +87,8 @@ export let parseSvgContent = (svgContent: string): SVGParseResult => {
       const box = path.getBBox();
       x = Math.min(box.x, x);
       y = Math.min(box.y, y);
-      width = Math.max(width, box.width);
-      height = Math.max(height, box.height);
+      right = Math.max(right, box.x + box.width);
+      bottom = Math.max(bottom, box.y + box.height);
 
       const fill = path.getAttribute("fill")!;
       const data = path.getAttribute("d")!;
@@ -104,7 +104,7 @@ export let parseSvgContent = (svgContent: string): SVGParseResult => {
 
     helpDOM.innerHTML = "";
     document.body.removeChild(helpDOM);
-    return (svgParseCache[svgContent] = { paths, width, height, x, y });
+    return (svgParseCache[svgContent] = { paths, width: right - x, height: bottom - y, x, y });
   };
   return parseSvgContent(svgContent);
 };

+ 17 - 1
src/utils/shape.ts

@@ -27,7 +27,7 @@ export const shapeTreeEq = (
 export const shapeParentsEq = (
 	target: EntityShape,
 	eq: (shape: EntityShape) => boolean,
-	checked: EntityShape[] = []
+	checked: EntityShape[] = [],
 ) => {
 	while (target) {
 		if (checked.includes(target)) return null;
@@ -50,6 +50,22 @@ export const shapeTreeContain = (
 	return shapeParentsEq(target, eq, checked);
 };
 
+export const shapeTreeContains = (
+	parent: EntityShape[],
+	target: EntityShape,
+	checked: EntityShape[] = []
+) => {
+	const result: EntityShape[] = []
+	const eq = (shape: EntityShape) => {
+		if (parent.includes(shape) && !result.includes(shape)) {
+			result.push(shape)
+		}
+		return false
+	}
+	shapeParentsEq(target, eq, checked);
+	return result
+};
+
 
 export const setShapeTransform = (shape: EntityShape, transform: Transform) => {
 	const config = transform.decompose()