Browse Source

fix: 添加snap

bill 9 months ago
parent
commit
26a2cebe35
53 changed files with 2427 additions and 995 deletions
  1. 2 0
      src/constant/index.ts
  2. 1 1
      src/core/components/arrow/arrow.vue
  3. 22 8
      src/core/components/arrow/index.ts
  4. 1 1
      src/core/components/circle/circle.vue
  5. 3 1
      src/core/components/circle/index.ts
  6. 1 1
      src/core/components/icon/icon.vue
  7. 3 2
      src/core/components/icon/index.ts
  8. 1 1
      src/core/components/icon/temp-icon.vue
  9. 1 1
      src/core/components/image/image.vue
  10. 3 2
      src/core/components/image/index.ts
  11. 1 1
      src/core/components/image/temp-image.vue
  12. 15 12
      src/core/components/index.ts
  13. 23 15
      src/core/components/line/index.ts
  14. 1 1
      src/core/components/line/line.vue
  15. 19 9
      src/core/components/polygon/index.ts
  16. 1 1
      src/core/components/polygon/polygon.vue
  17. 51 41
      src/core/components/rectangle/index.ts
  18. 81 20
      src/core/components/rectangle/rectangle.vue
  19. 3 3
      src/core/components/rectangle/temp-rectangle.vue
  20. 1 1
      src/core/components/share/point.vue
  21. 3 1
      src/core/components/text/index.ts
  22. 1 1
      src/core/components/text/temp-text.vue
  23. 1 1
      src/core/components/text/text.vue
  24. 11 8
      src/core/components/triangle/index.ts
  25. 1 1
      src/core/components/triangle/triangle.vue
  26. 13 0
      src/core/components/util.ts
  27. 67 0
      src/core/helper/active-boxs.vue
  28. 78 0
      src/core/helper/mouse-line.vue
  29. 80 0
      src/core/helper/snap-lines.vue
  30. 34 25
      src/core/hook/use-animation.ts
  31. 59 0
      src/core/hook/use-copy.ts
  32. 20 11
      src/core/hook/use-global-vars.ts
  33. 173 0
      src/core/hook/use-layer.ts
  34. 49 28
      src/core/hook/use-mouse-status.ts
  35. 364 0
      src/core/hook/use-snap.ts
  36. 305 207
      src/core/hook/use-transformer.ts
  37. 1 0
      src/core/propertys/color.vue
  38. 0 20
      src/core/propertys/controller.ts
  39. 41 67
      src/core/propertys/propertys.vue
  40. 35 0
      src/core/propertys/index.ts
  41. 112 0
      src/core/propertys/mount-property.vue
  42. 45 0
      src/core/propertys/num.vue
  43. 19 0
      src/core/propertys/util.ts
  44. 99 73
      src/core/renderer/draw-group.vue
  45. 16 15
      src/core/renderer/group.vue
  46. 60 49
      src/core/renderer/renderer.vue
  47. 68 200
      src/core/store/index.ts
  48. 212 0
      src/core/store/init.ts
  49. 0 0
      src/deconstruction.d.ts
  50. 0 1
      src/utils/event.ts
  51. 210 163
      src/utils/math.ts
  52. 1 1
      src/utils/shape.ts
  53. 15 1
      src/utils/shared.ts

+ 2 - 0
src/constant/index.ts

@@ -0,0 +1,2 @@
+
+export const DomMountId =  'dom-mount'

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

@@ -7,7 +7,7 @@ import { ArrowData, dataToConfig, style } from "./index.ts";
 import { useMouseStyle } from "../../hook/use-mouse-status.ts";
 import { ref } from "vue";
 import { useLineTransformer } from "../../hook/use-transformer.ts";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { useAniamtion } from "../../hook/use-animation.ts";
 import { Arrow } from "konva/lib/shapes/Arrow";
 

+ 22 - 8
src/core/components/arrow/index.ts

@@ -1,8 +1,9 @@
-import { lineVerticalVector, Pos } from "@/utils/math.ts";
-import { flatPositions } from "@/utils/shared.ts";
+import { Pos } from "@/utils/math.ts";
+import { flatPositions, onlyId } from "@/utils/shared.ts";
 import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { ArrowConfig } from "konva/lib/shapes/Arrow";
 import { themeMouseColors } from "@/constant/help-style.ts";
+import { BaseItem, getBaseItem } from "../util.ts";
 
 export { default as Component } from "./arrow.vue";
 export { default as TempComponent } from "./temp-arrow.vue";
@@ -14,7 +15,7 @@ export const defaultStyle = {
   fill: themeMouseColors.theme,
 };
 
-export const addMode = 'area'
+export const addMode = "area";
 
 export const style = {
   default: defaultStyle,
@@ -25,16 +26,20 @@ export const style = {
   hover: {
     stroke: themeMouseColors.theme,
     fill: themeMouseColors.hover,
-  }
+  },
 };
 
-export type ArrowData = Partial<typeof defaultStyle> & { points: Pos[], attitude: number[] };
+export type ArrowData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    points: Pos[];
+    attitude: number[];
+  };
 
 export const dataToConfig = (data: ArrowData): ArrowConfig => ({
   ...defaultStyle,
   ...data,
   points: flatPositions(data.points),
-  hitStrokeWidth: 20
+  hitStrokeWidth: 20,
 });
 
 export const interactiveToData = (
@@ -42,13 +47,22 @@ export const interactiveToData = (
   preset: Partial<ArrowData> = {}
 ): ArrowData | undefined => {
   if (info.area) {
-    return interactiveFixData({ ...preset, points: [info.area[0]], attitude: [1, 0, 0, 1, 0, 0] }, info);
+    return interactiveFixData(
+      {
+        ...getBaseItem(),
+        ...preset,
+        id: onlyId(),
+        points: [info.area[0]],
+        attitude: [1, 0, 0, 1, 0, 0],
+      },
+      info
+    );
   }
 };
 
 export const interactiveFixData = (
   data: ArrowData,
-  info: InteractiveMessage,
+  info: InteractiveMessage
 ) => {
   const area = info.area!;
   data.points[1] = area[1];

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

@@ -6,7 +6,7 @@
 import { Circle } from "konva/lib/shapes/Circle";
 import { useShapeTransformer, useTransformer } from "../../hook/use-transformer.ts";
 import { CircleData, dataToConfig, style } from "./index.ts";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { ref, watch } from "vue";
 import { useMouseStyle } from "../../hook/use-mouse-status.ts";
 import { useAniamtion } from "../../hook/use-animation.ts";

+ 3 - 1
src/core/components/circle/index.ts

@@ -1,6 +1,7 @@
 import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { CircleConfig } from "konva/lib/shapes/Circle";
 import { themeMouseColors } from "@/constant/help-style.ts";
+import { BaseItem, getBaseItem } from "../util.ts";
 
 export { default as Component } from "./circle.vue";
 export { default as TempComponent } from "./temp-circle.vue";
@@ -25,7 +26,7 @@ export const style = {
   }
 };
 
-export type CircleData = Partial<typeof defaultStyle> & {
+export type CircleData = Partial<typeof defaultStyle> & BaseItem & {
   x: number;
   y: number;
   radius: number;
@@ -42,6 +43,7 @@ export const interactiveToData = (
 ): CircleData | undefined => {
   if (info.area) {
     const item = {
+      ...getBaseItem(),
       ...preset,
       ...info.area[0],
     } as unknown as CircleData;

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

@@ -7,7 +7,7 @@ import TempIcon from "./temp-icon.vue";
 import { IconData, style } from "./index.ts";
 import { ref, watch } from "vue";
 import { Group } from "konva/lib/Group";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { useShapeTransformer } from "../../hook/use-transformer.ts";
 import { useAutomaticData } from "../../hook/use-automatic-data.ts";
 import { useMouseStyle } from "../../hook/use-mouse-status.ts";

+ 3 - 2
src/core/components/icon/index.ts

@@ -1,6 +1,7 @@
 import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { Transform } from "konva/lib/Util";
+import { BaseItem, getBaseItem } from "../util.ts";
 
 export { default as Component } from "./icon.vue";
 export { default as TempComponent } from "./temp-icon.vue";
@@ -32,7 +33,7 @@ export const style = {
   },
 };
 
-export type IconData = Partial<typeof defaultStyle> & {
+export type IconData = Partial<typeof defaultStyle> & BaseItem & {
   coverFill?: string;
   coverStroke?: string,
   coverStrokeWidth?: number,
@@ -55,7 +56,7 @@ export const interactiveToData = (
   preset: Partial<IconData> = {}
 ): IconData | undefined => {
   if (info.dot) {
-    return interactiveFixData({ ...preset } as unknown as IconData, info);
+    return interactiveFixData({ ...getBaseItem(), ...preset } as unknown as IconData, info);
   }
 };
 

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

@@ -12,7 +12,7 @@ import { IconData, dataToConfig } from "./index.ts";
 import { computed, ref, watch } from "vue";
 import { getSvgContent, parseSvgContent, SVGParseResult } from "@/utils/resource.ts";
 import { Group } from "konva/lib/Group";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: IconData, addMode?: boolean }>()

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

@@ -10,7 +10,7 @@ import TempImage from "./temp-image.vue";
 import { ImageData, style } from "./index.ts";
 import { ref, watch } from "vue";
 import { Group } from "konva/lib/Group";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { useShapeTransformer } from "../../hook/use-transformer.ts";
 import { useAutomaticData } from "../../hook/use-automatic-data.ts";
 import { useMouseStyle } from "../../hook/use-mouse-status.ts";

+ 3 - 2
src/core/components/image/index.ts

@@ -1,6 +1,7 @@
 import { themeMouseColors } from "@/constant/help-style.ts";
 import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { Transform } from "konva/lib/Util";
+import { BaseItem, getBaseItem } from "../util.ts";
 
 export { default as Component } from "./image.vue";
 export { default as TempComponent } from "./temp-image.vue";
@@ -25,7 +26,7 @@ export const style = {
   }
 };
 
-export type ImageData = Partial<typeof defaultStyle> & {
+export type ImageData = Partial<typeof defaultStyle> & BaseItem & {
   fill?: string;
   stroke?: string;
   strokeWidth?: number;
@@ -47,7 +48,7 @@ export const interactiveToData = (
   preset: Partial<ImageData> = {}
 ): ImageData | undefined => {
   if (info.dot) {
-    return interactiveFixData({ ...preset, } as unknown as ImageData, info);
+    return interactiveFixData({ ...getBaseItem(), ...preset, } as unknown as ImageData, info);
   }
 };
 

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

@@ -11,7 +11,7 @@ import { getImage } from "@/utils/resource.ts";
 import { useResize } from "@/core/hook/use-event.ts";
 import { Transform } from "konva/lib/Util";
 import { Group } from "konva/lib/Group";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 
 const props = defineProps<{ data: ImageData, addMode?: boolean }>()
 const image = ref<HTMLImageElement | null>(null)

+ 15 - 12
src/core/components/index.ts

@@ -33,18 +33,21 @@ export const components = {
 export type Components = typeof components
 export type ComponentValue<T extends ShapeType, K extends keyof Components[T]> = Components[T][K]
 
-export type DrawData = {
-	arrow?: ArrowData[],
-	rectangle?: RectangleData[],
-	circle?: CircleData[],
-	triangle?: TriangleData[],
-	polygon?: PolygonData[],
-	line?: LineData[],
-	text?: TextData[],
-	icon?: IconData[],
-	image?: ImageData[],
+export type DrawDataItem = {
+	arrow: ArrowData,
+	rectangle: RectangleData,
+	circle: CircleData,
+	triangle: TriangleData,
+	polygon: PolygonData,
+	line: LineData,
+	text: TextData,
+	icon: IconData,
+	image: ImageData,
 }
+export type ShapeType = keyof DrawDataItem
 
-export type DrawItem<T extends ShapeType = ShapeType> = Required<DrawData>[T][number]
+export type DrawData = {
+	[k in ShapeType]?: DrawDataItem[k][]
+}
 
-export type ShapeType = keyof typeof components
+export type DrawItem<T extends ShapeType = ShapeType> = DrawDataItem[T]

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

@@ -1,8 +1,12 @@
 import { Pos } from "@/utils/math.ts";
 import { flatPositions } from "@/utils/shared.ts";
-import { InteractiveAction, InteractiveMessage } from "../../hook/use-interactive.ts";
+import {
+  InteractiveAction,
+  InteractiveMessage,
+} from "../../hook/use-interactive.ts";
 import { LineConfig } from "konva/lib/shapes/Line";
 import { themeMouseColors } from "@/constant/help-style.ts";
+import { BaseItem, getBaseItem } from "../util.ts";
 
 export { default as Component } from "./line.vue";
 export { default as TempComponent } from "./temp-line.vue";
@@ -10,11 +14,10 @@ export { default as TempComponent } from "./temp-line.vue";
 export const shapeName = "线段";
 export const defaultStyle = {
   stroke: themeMouseColors.theme,
-  strokeWidth: 5
+  strokeWidth: 5,
 };
 
-
-export const addMode = 'dots'
+export const addMode = "dots";
 export const style = {
   default: defaultStyle,
   focus: {
@@ -22,23 +25,25 @@ export const style = {
   },
   hover: {
     stroke: themeMouseColors.theme,
-  }
-}
-
+  },
+};
 
-export type LineData = Partial<typeof defaultStyle> & { points: Pos[], attitude: number[] };
+export type LineData = Partial<typeof defaultStyle> & BaseItem & {
+  points: Pos[];
+  attitude: number[];
+};
 
 export const dataToConfig = (data: LineData): LineConfig => ({
   ...defaultStyle,
   ...data,
   points: flatPositions(data.points),
   hitFunc(con, shape) {
-    con.beginPath()
-    con.moveTo(data.points[0].x, data.points[0].y)
+    con.beginPath();
+    con.moveTo(data.points[0].x, data.points[0].y);
     for (let i = 1; i < data.points.length; i++) {
-      con.lineTo(data.points[i].x, data.points[i].y)
+      con.lineTo(data.points[i].x, data.points[i].y);
     }
-    con.closePath()
+    con.closePath();
     con.fillStrokeShape(shape);
   },
 });
@@ -48,7 +53,10 @@ export const interactiveToData = (
   preset: Partial<LineData> = {}
 ): LineData | undefined => {
   if (info.dot) {
-    return interactiveFixData({ ...preset, points: [], attitude: [1, 0, 0, 1, 0, 0] }, info);
+    return interactiveFixData(
+      { ...getBaseItem(), ...preset, points: [], attitude: [1, 0, 0, 1, 0, 0] },
+      info
+    );
   }
 };
 
@@ -57,9 +65,9 @@ export const interactiveFixData = (
   info: InteractiveMessage
 ) => {
   if (info.action === InteractiveAction.delete) {
-    data.points.pop()
+    data.points.pop();
   } else {
-    data.points[info.ndx!] = info.dot!
+    data.points[info.ndx!] = info.dot!;
   }
   return data;
 };

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

@@ -7,7 +7,7 @@ import { LineData, dataToConfig, style } from "./index.ts";
 import { useMouseStyle } from "../../hook/use-mouse-status.ts";
 import { ref } from "vue";
 import { useLineTransformer } from "../../hook/use-transformer.ts";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { useAniamtion } from "../../hook/use-animation.ts";
 import { Line } from "konva/lib/shapes/Line";
 

+ 19 - 9
src/core/components/polygon/index.ts

@@ -1,8 +1,12 @@
 import { Pos } from "@/utils/math.ts";
 import { flatPositions } from "@/utils/shared.ts";
-import { InteractiveAction, InteractiveMessage } from "../../hook/use-interactive.ts";
+import {
+  InteractiveAction,
+  InteractiveMessage,
+} from "../../hook/use-interactive.ts";
 import { LineConfig } from "konva/lib/shapes/Line";
 import { themeMouseColors } from "@/constant/help-style.ts";
+import { BaseItem, getBaseItem } from "../util.ts";
 
 export { default as Component } from "./polygon.vue";
 export { default as TempComponent } from "./temp-polygon.vue";
@@ -11,10 +15,10 @@ export const shapeName = "多边形";
 export const defaultStyle = {
   stroke: themeMouseColors.theme,
   strokeWidth: 5,
-  fill: '#fff',
+  fill: "#fff",
 };
 
-export const addMode = 'dots'
+export const addMode = "dots";
 
 export const style = {
   default: defaultStyle,
@@ -24,10 +28,13 @@ export const style = {
   },
   hover: {
     stroke: themeMouseColors.theme,
-  }
-}
+  },
+};
 
-export type PolygonData = Partial<typeof defaultStyle> & { points: Pos[], attitude: number[] };
+export type PolygonData = Partial<typeof defaultStyle> & BaseItem & {
+  points: Pos[];
+  attitude: number[];
+};
 
 export const dataToConfig = (data: PolygonData): LineConfig => ({
   ...defaultStyle,
@@ -41,7 +48,10 @@ export const interactiveToData = (
   preset: Partial<PolygonData> = {}
 ): PolygonData | undefined => {
   if (info.dot) {
-    return interactiveFixData({ ...preset, points: [], attitude: [1, 0, 0, 1, 0, 0] }, info);
+    return interactiveFixData(
+      { ...getBaseItem(), ...preset, points: [], attitude: [1, 0, 0, 1, 0, 0] },
+      info
+    );
   }
 };
 
@@ -50,9 +60,9 @@ export const interactiveFixData = (
   info: InteractiveMessage
 ) => {
   if (info.action === InteractiveAction.delete) {
-    data.points.pop()
+    data.points.pop();
   } else {
-    data.points[info.ndx!] = info.dot!
+    data.points[info.ndx!] = info.dot!;
   }
   return data;
 };

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

@@ -7,7 +7,7 @@ import { PolygonData, dataToConfig, style } from "./index.ts";
 import { useMouseStyle } from "../../hook/use-mouse-status.ts";
 import { ref } from "vue";
 import { useLineTransformer } from "../../hook/use-transformer.ts";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { useAniamtion } from "../../hook/use-animation.ts";
 import { Line } from "konva/lib/shapes/Line";
 

+ 51 - 41
src/core/components/rectangle/index.ts

@@ -1,64 +1,74 @@
-import { Pos } from "@/utils/math.ts";
+import { lineCenter, Pos } from "@/utils/math.ts";
 import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
-import { flatPositions } from "@/utils/shared.ts";
-import { LineConfig } from "konva/lib/shapes/Line";
+import { getMouseColors } from "@/utils/colors.ts";
+import { onlyId } from "@/utils/shared.ts";
+import { BaseItem, getBaseItem } from "../util.ts";
 
 export { default as Component } from "./rectangle.vue";
 export { default as TempComponent } from "./temp-rectangle.vue";
 
-
 export const shapeName = "矩形";
 export const defaultStyle = {
+  dash: [1, 0],
+  strokeWidth: 1,
   stroke: themeMouseColors.theme,
-  strokeWidth: 2,
-  fill: '#fff',
+  fill: "#fff",
 };
 
-export const addMode = 'area'
+export const getMouseStyle = (data: RectangleData) => {
+  const fillStatus = getMouseColors(data.fill || defaultStyle.fill);
+  const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
+  const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
 
-export const style = {
-  default: defaultStyle,
-  focus: {
-    fill: themeMouseColors.theme,
-    stroke: themeMouseColors.hover,
-  },
-  hover: {
-    stroke: themeMouseColors.theme,
-    // fill: themeMouseColors.hover,
-  },
-  press: {
-    stroke: themeMouseColors.press,
-    fill: themeMouseColors.press,
-  },
-  // select: {
-  //   fill: 'rgba(255,0,0, 1)',
-  // },
-  drag: {
-    strokeScaleEnabled: false,
-    fill: themeMouseColors.hover,
-    stroke: themeMouseColors.press,
-    dashEnabled: true,
-    dash: [30, 30]
-  }
+  return {
+    default: { fill: fillStatus.pub, stroke: strokeStatus.pub, strokeWidth },
+    hover: { fill: fillStatus.hover },
+    press: { fill: fillStatus.press },
+  };
 };
 
-export type RectangleData = Partial<typeof defaultStyle> & {
-  attitude: number[];
-  points: Pos[]
-};
+export const addMode = "area";
 
+export type RectangleData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    id: string;
+    attitude: number[];
+    points: Pos[];
+    createTime: number;
+    zIndex: number;
+    dash?: number[];
+    stroke?: string;
+    fill?: string;
+    strokeWidth?: number;
+  };
 
 export const interactiveToData = (
   info: InteractiveMessage,
   preset: Partial<RectangleData> = {}
 ): RectangleData | undefined => {
   if (info.area) {
-    const item = { ...preset } as unknown as RectangleData;
+    const item = {
+      ...getBaseItem(),
+      ...preset,
+      id: onlyId(),
+      createTime: Date.now(),
+      zIndex: 0,
+    } as unknown as RectangleData;
     return interactiveFixData(item, info);
   }
 };
 
+export const getSnapPoints = (data: RectangleData) => {
+  return [
+    lineCenter([data.points[0], data.points[1]]),
+    lineCenter([data.points[1], data.points[2]]),
+    lineCenter([data.points[2], data.points[3]]),
+    lineCenter([data.points[3], data.points[0]]),
+    ...data.points.map(p => ({...p}))
+  ]
+}
+
 export const interactiveFixData = (
   data: RectangleData,
   info: InteractiveMessage
@@ -70,11 +80,11 @@ export const interactiveFixData = (
 
     data.points = [
       info.area[0],
-      {x: info.area[0].x + width, y: info.area[0].y},
-      {x: info.area[0].x + width, y: info.area[0].y + height},
-      {x: info.area[0].x, y: info.area[0].y + height},
-    ]
-    data.attitude = [1, 0, 0, 1, 0, 0]
+      { x: info.area[0].x + width, y: info.area[0].y },
+      { x: info.area[0].x + width, y: info.area[0].y + height },
+      { x: info.area[0].x, y: info.area[0].y + height },
+    ];
+    data.attitude = [1, 0, 0, 1, 0, 0];
   }
   return data;
 };

+ 81 - 20
src/core/components/rectangle/rectangle.vue

@@ -1,35 +1,96 @@
 <template>
-  <TempLine :data="data" :ref="(v: any) => shape = v.shape" />
-  <Propertys :describes="propertyDescribes" :data="data" :target="shape" />
+  <TempLine
+    :data="{ ...data, ...style }"
+    :ref="(v: any) => shape = v?.shape"
+    :id="data.id"
+  />
+  <PropertyUpdate
+    :describes="describes"
+    :data="data"
+    :target="shape"
+    @change="emit('update', { ...data })"
+  />
+  <!-- <Operate :target="shape" :menus="operateMenus" /> -->
 </template>
 
 <script lang="ts" setup>
-import { RectangleData, style } from "./index.ts";
-import { PropertyDescribes, Propertys } from "../../propertys/controller.ts";
-import { useMouseStyle } from "../../hook/use-mouse-status.ts";
+import { RectangleData, getMouseStyle, defaultStyle } from "./index.ts";
+import { PropertyUpdate, Operate } from "../../propertys";
+import { useAnimationMouseStyle } from "../../hook/use-mouse-status.ts";
 import { ref } from "vue";
 import { useLineTransformer } from "../../hook/use-transformer.ts";
-import { DC } from "@/helper/deconstruction";
-import { useAniamtion } from "../../hook/use-animation.ts";
+import { DC } from "@/deconstruction.js";
 import { Line } from "konva/lib/shapes/Line";
 import TempLine from "./temp-rectangle.vue";
+import { useAutomaticData } from "@/core/hook/use-automatic-data.ts";
+import { generateDescribes } from "@/core/propertys/util.ts";
+import { Delete, DocumentCopy } from "@element-plus/icons-vue";
+import { useGetShapeCopyTransform } from "@/core/hook/use-copy.ts";
+import { onlyId } from "@/utils/shared.ts";
+import { useMouseMigrateTempLayer, useZIndex } from "../../hook/use-layer.ts";
 
-const props = defineProps<{ data: RectangleData; addMode?: boolean }>();
-const emit = defineEmits<{ (e: "update", value: RectangleData): void }>();
+const props = defineProps<{ data: RectangleData }>();
+const emit = defineEmits<{
+  (e: "update", value: RectangleData): void;
+  (e: "add", value: RectangleData): void;
+  (e: "del"): void;
+}>();
 const shape = ref<DC<Line>>();
-
-const [_, data] = useLineTransformer(
+const data = useAutomaticData(() => props.data);
+const [style] = useAnimationMouseStyle({
+  data: data,
   shape,
-  () => ({ ...props.data, ...style.default }),
-  (newData) => {
-    emit("update", { ...props.data, ...newData });
-  }
-);
-const { currentStyle } = useMouseStyle({ style, shape });
-useAniamtion(currentStyle, shape);
+  getMouseStyle,
+});
 
-const propertyDescribes: PropertyDescribes = {
+const describes = generateDescribes(data, defaultStyle, {
   stroke: { type: "color", label: "边框" },
   fill: { type: "color", label: "填充" },
-};
+  strokeWidth: { type: "num", label: "粗细", props: { min: 0.5, max: 10 } },
+  zIndex: { type: "num", label: "层级", props: { min: -1000, max: 1000, step: 1 } },
+  dash: {
+    type: "num",
+    label: "虚线比例",
+    props: { min: 0, max: 30 },
+    get value() {
+      if (!data.value.dash) {
+        return 30;
+      }
+      if (!data.value.dash[1]) {
+        return 30;
+      } else {
+        return data.value.dash[0];
+      }
+    },
+    set value(val) {
+      data.value.dash = [val, 30 - val];
+    },
+  },
+});
+
+const getCopyTransform = useGetShapeCopyTransform(shape);
+const operateMenus = [
+  {
+    label: "删除",
+    icon: Delete,
+    handler() {
+      emit("del");
+    },
+  },
+  {
+    label: "复制",
+    icon: DocumentCopy,
+    handler() {
+      const transform = getCopyTransform();
+      const copyData = JSON.parse(JSON.stringify(data.value)) as RectangleData;
+      copyData.points = copyData.points.map((v) => transform.point(v));
+      copyData.id = onlyId();
+      emit("add", copyData);
+    },
+  },
+];
+
+useLineTransformer(shape, data, (newData) => emit("update", newData));
+useZIndex(shape, data);
+useMouseMigrateTempLayer(shape);
 </script>

+ 3 - 3
src/core/components/rectangle/temp-rectangle.vue

@@ -1,8 +1,8 @@
 <template>
   <v-line
     :config="{
-      ...style.default,
       ...data,
+      zIndex: undefined,
       closed: true,
       points: flatPositions(data.points),
       opacity: addMode ? 0.3 : 1,
@@ -13,9 +13,9 @@
 
 <script lang="ts" setup>
 import { flatPositions } from "@/utils/shared.ts";
-import { RectangleData, style } from "./index.ts";
+import { RectangleData } from "./index.ts";
 import { ref } from "vue";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { Line } from "konva/lib/shapes/Line";
 defineProps<{ data: RectangleData; addMode?: boolean }>();
 

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

@@ -9,7 +9,7 @@
 import { Pos } from "@/utils/math.ts";
 import { HelpPointStyle } from "@/constant/help-style.ts";
 import { ref, watch, watchEffect } from 'vue'
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction";
 import { Circle } from "konva/lib/shapes/Circle";
 import { useShapeDrag } from "@/core/hook/use-transformer.ts";
 

+ 3 - 1
src/core/components/text/index.ts

@@ -2,6 +2,7 @@ import { Transform } from "konva/lib/Util";
 import { InteractiveMessage } from "../../hook/use-interactive.ts";
 import { TextConfig } from "konva/lib/shapes/Text";
 import { themeMouseColors } from "@/constant/help-style.ts";
+import { BaseItem, getBaseItem } from "../util.ts";
 
 export { default as Component } from "./text.vue";
 
@@ -29,7 +30,7 @@ export const style = {
   },
 };
 
-export type TextData = Partial<typeof defaultStyle> & {
+export type TextData = Partial<typeof defaultStyle> & BaseItem & {
   mat: number[]
   content: string
 };
@@ -49,6 +50,7 @@ export const interactiveToData = (
   if (info.dot) {
     const item = {
       ...defaultStyle,
+      ...getBaseItem(),
       ...preset,
       content: preset.content || '文字',
     } as unknown as TextData;

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

@@ -6,7 +6,7 @@
 <script lang="ts" setup>
 import { TextData, dataToConfig } from "./index.ts";
 import { computed, ref } from "vue";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { Transform } from "konva/lib/Util";
 import { Group } from "konva/lib/Group";
 

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

@@ -7,7 +7,7 @@ import TempText from "./temp-text.vue";
 import { TextData, style } from "./index.ts";
 import { computed, ref, watch, watchEffect } from "vue";
 
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { useShapeTransformer } from "../../hook/use-transformer.ts";
 import { useAutomaticData } from "../../hook/use-automatic-data.ts";
 import { useMouseStyle } from "../../hook/use-mouse-status.ts";

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

@@ -3,6 +3,7 @@ import { LineConfig } from "konva/lib/shapes/Line";
 import { Pos } from "@/utils/math.ts";
 import { flatPositions } from "@/utils/shared.ts";
 import { themeMouseColors } from "@/constant/help-style.ts";
+import { BaseItem, getBaseItem } from "../util.ts";
 
 export { default as Component } from "./triangle.vue";
 export { default as TempComponent } from "./temp-triangle.vue";
@@ -11,10 +12,10 @@ export const shapeName = "三角形";
 export const defaultStyle = {
   stroke: themeMouseColors.theme,
   strokeWidth: 5,
-  fill: '#fff',
+  fill: "#fff",
 };
 
-export const addMode = 'area'
+export const addMode = "area";
 
 export const style = {
   default: defaultStyle,
@@ -24,10 +25,11 @@ export const style = {
   },
   hover: {
     stroke: themeMouseColors.theme,
-  }
-}
+  },
+};
 
-export type TriangleData = Partial<typeof defaultStyle> & { points: Pos[], attitude: number[] };
+export type TriangleData = Partial<typeof defaultStyle> &
+  BaseItem & { points: Pos[]; attitude: number[] };
 
 export const dataToConfig = (data: TriangleData): LineConfig => {
   return {
@@ -44,6 +46,7 @@ export const interactiveToData = (
 ): TriangleData | undefined => {
   if (info.area) {
     const item = {
+      ...getBaseItem(),
       ...preset,
       points: [],
     } as unknown as TriangleData;
@@ -56,11 +59,11 @@ export const interactiveFixData = (
   info: InteractiveMessage
 ) => {
   const area = info.area!;
-  
+
   data.points[0] = {
     x: area[0].x - (area[1].x - area[0].x),
-    y: area[1].y
-  }
+    y: area[1].y,
+  };
   data.points[1] = area[0];
   data.points[2] = area[1];
   return data;

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

@@ -7,7 +7,7 @@ import { TriangleData, style, dataToConfig } from "./index.ts";
 import { useMouseStyle } from "../../hook/use-mouse-status.ts";
 import { ref } from "vue";
 import { useLineTransformer } from "../../hook/use-transformer.ts";
-import { DC } from "@/helper/deconstruction";
+import { DC } from "@/deconstruction.js";
 import { useAniamtion } from "../../hook/use-animation.ts";
 import { Line } from "konva/lib/shapes/Line";
 

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

@@ -0,0 +1,13 @@
+import { onlyId } from "@/utils/shared"
+
+export type BaseItem = {
+	id: string,
+	createTime: number,
+	zIndex: number
+}
+
+export const getBaseItem = (): BaseItem => ({
+  id: onlyId(),
+  createTime: Date.now(),
+  zIndex: 0
+})

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

@@ -0,0 +1,67 @@
+<template>
+  <template v-for="(shape, i) in status.actives" :key="shape._id">
+    <v-rect
+      :ref="(rect: any) => rects[i] = rect"
+      :config="boxs.get(shape)"
+      :strokeWidth="4"
+      :stroke="themeColor"
+      :dash="[10, 10]"
+      :listening="false"
+      v-if="boxs.get(shape)"
+    />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { nextTick, reactive, ref, watch, watchEffect } from "vue";
+import { useMouseShapesStatus } from "../hook/use-mouse-status";
+import { IRect } from "konva/lib/types";
+import { useViewer } from "../hook/use-viewer";
+import { DC, EntityShape } from "@/deconstruction";
+import { themeColor } from "@/constant/help-style";
+import { useTransformer } from "../hook/use-transformer";
+import { Rect } from "konva/lib/shapes/Rect";
+import { useDashAnimation } from "../hook/use-animation";
+
+const status = useMouseShapesStatus();
+const boxs = reactive(new WeakMap<EntityShape, IRect>());
+const padding = 8;
+
+const updateBox = ($shape: EntityShape) => {
+  const rect = $shape.getClientRect();
+  boxs.set($shape, {
+    x: rect.x - padding,
+    y: rect.y - padding,
+    width: rect.width + padding * 2,
+    height: rect.height + padding * 2,
+  });
+};
+
+const transformer = useTransformer();
+const shapeListener = (shape: EntityShape) => {
+  watchEffect((onCleanup) => {
+    transformer.on("transform.helper-box", async () => {
+      await nextTick();
+      updateBox(shape);
+    });
+    onCleanup(() => transformer.off("transform.helper-box"));
+  });
+};
+
+watchEffect(
+  () => {
+    status.actives.forEach(shapeListener);
+  },
+  { flush: "pre" }
+);
+
+watch(useViewer().transform, () => {
+  for (const $shape of status.actives) {
+    updateBox($shape);
+  }
+});
+
+// animation
+const rects = ref<DC<Rect>[]>([]);
+useDashAnimation(rects);
+</script>

+ 78 - 0
src/core/helper/mouse-line.vue

@@ -0,0 +1,78 @@
+<template>
+  <template v-if="pointer">
+    <v-line
+      :config="{ ...vConfig, x: pointer[0], y: 0 }"
+      v-if="pointer[0]"
+      :ref="(l: any) => lines[0] = l"
+    />
+    <v-line
+      :config="{ ...hConfig, x: 0, y: pointer[1] }"
+      v-if="pointer[1]"
+      :ref="(l: any) => lines[1] = l"
+    />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { computed, reactive, ref, watchEffect } from "vue";
+import { usePointerPos, useStage } from "../hook/use-global-vars";
+import { useSnapPoints } from "../hook/use-snap";
+import { useViewerInvertTransform, useViewerTransform } from "../hook/use-viewer";
+import { RectConfig } from "konva/lib/shapes/Rect";
+import { themeColor } from "@/constant/help-style";
+import { Pos } from "@/utils/math";
+import { DC } from "@/deconstruction";
+import { Line } from "konva/lib/shapes/Line";
+import { useDashAnimation } from "../hook/use-animation";
+
+const config: RectConfig = {
+  dash: [10, 10],
+  stroke: themeColor,
+  strokeWidth: 1,
+  listening: false,
+};
+const vConfig = reactive({
+  ...config,
+  points: [0, 0, 0, 0],
+});
+const hConfig = reactive({
+  ...config,
+  points: [0, 0, 0, 0],
+});
+
+const pointerPixel = usePointerPos();
+const stage = useStage();
+watchEffect(() => {
+  const $stage = stage.value?.getNode();
+  if (!$stage || !pointerPixel.value) return;
+  vConfig.points[3] = $stage.height();
+  hConfig.points[2] = $stage.width();
+});
+
+const snaps = useSnapPoints();
+const viewerInvertTransform = useViewerInvertTransform();
+const viewerTransform = useViewerTransform();
+const snapOffset = 10;
+const pointer = computed(() => {
+  if (!pointerPixel.value) return;
+  const current = viewerInvertTransform.value.point(pointerPixel.value);
+  const final: Partial<Pos> = {};
+  for (const snap of snaps.value) {
+    if (Math.abs(snap.x - current.x) < snapOffset) {
+      final.x = snap.x;
+      break;
+    }
+  }
+  for (const snap of snaps.value) {
+    if (Math.abs(snap.y - current.y) < snapOffset) {
+      final.y = snap.y;
+      break;
+    }
+  }
+  const finalPixel = viewerTransform.value.point({ x: 0, y: 0, ...final });
+  return ["x" in final ? finalPixel.x : null, "y" in final ? finalPixel.y : null];
+});
+
+const lines = ref<DC<Line>[]>([]);
+useDashAnimation(lines);
+</script>

+ 80 - 0
src/core/helper/snap-lines.vue

@@ -0,0 +1,80 @@
+<template>
+  <v-line
+    v-for="(xAxis, i) in pointer.xAxiss"
+    :config="{ ...vConfig, x: xAxis, y: 0 }"
+    :ref="(l: any) => lines[i] = l"
+  />
+  <v-line
+    v-for="(yAxiss, j) in pointer.yAxiss"
+    :config="{ ...hConfig, x: 0, y: yAxiss }"
+    :ref="(l: any) => lines[pointer.xAxiss.length + j] = l"
+  />
+</template>
+
+<script lang="ts" setup>
+import { computed, onUnmounted, reactive, ref, watchEffect } from "vue";
+import { useStage } from "../hook/use-global-vars";
+import { useSnapInfo } from "../hook/use-snap";
+import { useViewerTransform } from "../hook/use-viewer";
+import { RectConfig } from "konva/lib/shapes/Rect";
+import { themeColor } from "@/constant/help-style";
+import { DC } from "@/deconstruction";
+import { Line } from "konva/lib/shapes/Line";
+import { useDashAnimation } from "../hook/use-animation";
+import { listener } from "@/utils/event";
+
+const config: RectConfig = {
+  dash: [10, 10],
+  stroke: themeColor,
+  strokeWidth: 1,
+  listening: false,
+};
+const vConfig = reactive({
+  ...config,
+  points: [0, 0, 0, 0],
+});
+const hConfig = reactive({
+  ...config,
+  points: [0, 0, 0, 0],
+});
+
+const stage = useStage();
+const setSize = () => {
+  const $stage = stage.value?.getNode();
+  if (!$stage) return;
+  vConfig.points[3] = $stage.height();
+  hConfig.points[2] = $stage.width();
+};
+watchEffect(setSize);
+onUnmounted(listener(window, "resize", setSize));
+
+const viewerTransform = useViewerTransform();
+const info = useSnapInfo();
+const minOffset = 10;
+const deduplication = (nums: number[]) => {
+  for (let i = 0; i < nums.length; i++) {
+    const min = nums[i] - minOffset;
+    const max = nums[i] + minOffset;
+
+    for (let j = i + 1; j < nums.length; j++) {
+      if (nums[j] < max && nums[j] > min) {
+        nums.splice(j--, 1);
+      }
+    }
+  }
+  return nums;
+};
+const pointer = computed(() => {
+  const xAxiss = info.xSnaps.map(
+    (xSnap) => viewerTransform.value.point({ x: xSnap.ref.x, y: 0 }).x
+  );
+  const yAxiss = info.ySnaps.map(
+    (ySnap) => viewerTransform.value.point({ y: ySnap.ref.y, x: 0 }).y
+  );
+
+  return { xAxiss: deduplication(xAxiss), yAxiss: deduplication(yAxiss) };
+});
+
+const lines = ref<DC<Line>[]>([]);
+useDashAnimation(lines);
+</script>

+ 34 - 25
src/core/hook/use-animation.ts

@@ -1,8 +1,9 @@
 import { onUnmounted, reactive, ref, Ref, watch, watchEffect } from "vue"
 import { Tween, Easing } from '@tweenjs/tween.js'
-import { inRevise } from "@/utils/shared.ts"
+import { inRevise, startAnimation } from "@/utils/shared.ts"
 import { Color, RGB } from "three"
-import { DC, EntityShape } from "@/helper/deconstruction"
+import { DC, EntityShape } from "@/deconstruction"
+import { Shape } from "konva/lib/Shape"
 
 const pickColors = <T extends object>(origin: T): Record<string, RGB>  => {
   const originColors = {} as any
@@ -49,7 +50,7 @@ const animation = <T extends object>(origin: T, target: T) => {
   const tw = (origin: any, target: any) => {
     ++count
     return new Tween(origin)
-      .to(target, 150)
+      .to(target, 300)
       .easing(easing)
       .start()
       .onComplete(() => {
@@ -78,35 +79,43 @@ const animation = <T extends object>(origin: T, target: T) => {
   return stop
 }
 
-export const useAniamtion = <T extends object>(data: Ref<T>, shape?: Ref<DC<EntityShape>| undefined>) => {
-  const atData = ref({...data.value}) as Ref<T>
+export const useAniamtion = <T extends object>(data: Ref<T>) => {
+  const atData = ref(JSON.parse(JSON.stringify(data.value))) as Ref<T>
   let animationStop: () => void
-
-  const { pause, resume } = watch(data, (newData) => {
+  let isPause = false
+  watch(data, (newData) => {
     if (!inRevise(newData, atData.value)) return;
     animationStop! && animationStop()
-    animationStop = animation(atData.value, newData)
-  })
-
-  const stopShapeSet = watchEffect(() => {
-    if (!shape?.value) return;
-    const $shape = shape.value.getNode() as any
-    for (const key in atData.value) {
-      $shape[key](atData.value[key])
+    if (isPause) {
+      atData.value = newData
+    } else {
+      animationStop = animation(atData.value, newData)
     }
   })
 
   onUnmounted(() => {
     animationStop && animationStop()
-    stopShapeSet()
   })
-  return reactive({
-    data: atData,
-    pause,
-    resume
-  }) as {
-    data: T,
-    pause: () => void,
-    resume: () => void,
-  }
+  return [
+    atData, () => isPause = true, () => isPause = false
+  ] as const
+}
+
+type DA = DC<Shape> | undefined
+export const useDashAnimation = (shapes: Ref<DA | DA[]>) => {
+  watch(
+    () => (Array.isArray(shapes.value) ? shapes.value : [shapes.value]).map((i) => i),
+    (shapes, _, onCleanup) => {
+      for (const shape of shapes) {
+        if (!shape) continue;
+        const $shape = shape.getNode();
+        let i = 0;
+        onCleanup(
+          startAnimation(() => {
+            $shape.dashOffset(i--);
+          })
+        );
+      }
+    }
+  );
 }

+ 59 - 0
src/core/hook/use-copy.ts

@@ -0,0 +1,59 @@
+import { DC, EntityShape } from "@/deconstruction";
+import { Ref } from "vue";
+import { useStage } from "./use-global-vars";
+import { useViewerInvertTransform } from "./use-viewer";
+import { Transform } from "konva/lib/Util";
+
+
+export const useGetShapeCopyTransform = (shape: Ref<DC<EntityShape> | undefined>, padding = 10) => {
+  const stage = useStage()
+  const invViewTransform = useViewerInvertTransform()
+
+  return () => {
+    const $shape = shape.value!.getNode()
+    const $stage = stage.value!.getNode()
+    const shapeRect = $shape.getClientRect()
+    const stageRect = {
+      x: 0,
+      y: 0,
+      width: $stage.width(),
+      height: $stage.height()
+    }
+    // 可用性,分别对应 右左上下
+    const available = [
+      {
+        diff: stageRect.x + stageRect.width - (shapeRect.x + shapeRect.width * 2 + padding),
+        translate:  { x: shapeRect.width + padding, y: 0 },
+      },
+      {
+        diff: shapeRect.x - shapeRect.width - padding - stageRect.x,
+        translate:  { x: -shapeRect.width - padding, y: 0 },
+      },
+      {
+        diff: shapeRect.y - shapeRect.height - padding - stageRect.y,
+        translate:  { x: 0, y: -shapeRect.height - padding },
+      },
+      {
+        diff: stageRect.y + stageRect.height - (shapeRect.y + shapeRect.height * 2 + padding),
+        translate:  { x: 0, y: shapeRect.height + padding },
+      }
+    ]
+    let translate = available.find(({ diff }) => diff >= 0)?.translate
+    if (!translate) {
+      // 都不可用取最可用复制位置  
+      let maxDiff = available[0].diff
+      let ndx = 0
+      for (let i = 1; i < available.length; i++) {
+        if (available[i].diff > maxDiff) {
+          ndx = i
+        }
+      }
+      translate = available[ndx].translate
+    }
+
+    const origin = invViewTransform.value.point({x: 0, y: 0})
+    const target = invViewTransform.value.point(translate);
+    // 转化为真实坐标
+    return  new Transform().translate(target.x - origin.x, target.y - origin.y)
+  }
+}

+ 20 - 11
src/core/hook/use-global-vars.ts

@@ -1,4 +1,4 @@
-import { DC, EntityShape } from "../../helper/deconstruction";
+import { DC, EntityShape } from "../../deconstruction";
 import { Stage } from "konva/lib/Stage";
 import {
 	computed,
@@ -10,11 +10,14 @@ import {
 	shallowRef,
 	watch,
 	WatchCallback,
+	watchEffect,
 	WatchOptions,
 	WatchSource,
 } from "vue";
 import { Mode } from "../../constant/mode.ts";
 import { Layer } from "konva/lib/Layer";
+import { Pos } from "@/utils/math.ts";
+import { listener } from "@/utils/event.ts";
 
 export const installGlobalVar = <T>(
 	create: () => { var: T, onDestroy: () => void } | T,
@@ -104,20 +107,26 @@ export const globalWatch = <T>(
 
 export const useStage = installGlobalVar(() => shallowRef<DC<Stage> | undefined>(), Symbol("stage"));
 export const useMode = installGlobalVar(() => stackVar(Mode.viewer), Symbol("mode"));
-
-export const useLayers = () => {
+export const usePointerPos = installGlobalVar(() => {
 	const stage = useStage()
-	return computed(() => stage.value?.getNode().children as Layer[]) 
-}
+	const pos = ref<Pos | null>(null)
+	
+	watchEffect((onCleanup) => {
+		const $stage = stage.value?.getNode()
+		if (!$stage) return;
 
-export const useTempLayer = () => {
-	const stage = useStage()
-	return computed(() => stage.value?.getNode().find<Layer>('#temp')[0]) 
-}
+		const mount = $stage.container().parentElement!
+		pos.value = $stage.pointerPos
+		onCleanup(listener(mount, 'pointermove', ev => {
+			pos.value = $stage.pointerPos
+		}))
+	})
+	return pos
+})
 
-export const useFormalLayer = () => {
+export const useLayers = () => {
 	const stage = useStage()
-	return computed(() => stage.value?.getNode().find<Layer>('#formal')[0]) 
+	return computed(() => stage.value?.getNode().children as Layer[]) 
 }
 
 export const useTransformIngShapes = installGlobalVar(() => ref<EntityShape[]>([]))

+ 173 - 0
src/core/hook/use-layer.ts

@@ -0,0 +1,173 @@
+import { DC, EntityShape } from "@/deconstruction";
+import {
+  computed,
+  onUnmounted,
+  ref,
+  Ref,
+  toRaw,
+  watch,
+  watchEffect,
+} from "vue";
+import { DrawItem } from "../components";
+import { useStore } from "../store";
+import { installGlobalVar, useStage } from "./use-global-vars";
+import { Layer } from "konva/lib/Layer";
+import { useMouseShapeStatus } from "./use-mouse-status";
+
+// const useRefreshCount = installGlobalVar(() => ref(0));
+// const useRefresh = () => {
+//   useRefreshCount
+// }
+
+export const useTempLayer = () => {
+  const stage = useStage();
+  return computed(() => stage.value?.getNode().find<Layer>("#temp")[0]);
+};
+
+export const useFormalLayer = () => {
+  const stage = useStage();
+  return computed(() => stage.value?.getNode().find<Layer>("#formal")[0]);
+};
+
+export const useHelperLayer = () => {
+  const stage = useStage();
+  return computed(() => stage.value?.getNode().find<Layer>("#helper")[0]);
+};
+
+export const useMigrateLayer = (shape: Ref<DC<EntityShape> | undefined>) => {
+  const formal = useFormalLayer();
+  const zIndexs = useZIndexsManage();
+  let rawLayer: Layer;
+  let toLayer: Layer;
+
+  const recovery = () => {
+    const $shape = shape.value?.getNode();
+    if (rawLayer && $shape) {
+      $shape.remove();
+      rawLayer.add($shape);
+
+      if (import.meta.env.DEV) {
+        setTimeout(() => {
+          console.log(
+            `recovery raw:${rawLayer.id()} ${rawLayer.children.length} to:${toLayer.id()} ${toLayer.children.length}`,
+          );
+        })
+      }
+
+      if (toRaw(formal.value) === toRaw(rawLayer) && zIndexs.get($shape)) {
+        zIndexs.refresh();
+      }
+    }
+  };
+  const migrate = (to: Layer) => {
+    if (!shape.value) throw "shape不存在";
+    const $shape = shape.value.getNode();
+    rawLayer = $shape.getLayer()!;
+    toLayer = to;
+    $shape.remove();
+    to.add($shape);
+
+    if (import.meta.env.DEV) {
+      console.log(
+        `migrate raw:${rawLayer.id()} ${rawLayer.children.length} to:${toLayer.id()} ${toLayer.children.length}`,
+      );
+    }
+  };
+
+  return [migrate, recovery] as const;
+};
+
+export const useMouseMigrateTempLayer = (
+  shape: Ref<DC<EntityShape> | undefined>
+) => {
+  const status = useMouseShapeStatus(shape);
+  const tempLayer = useTempLayer();
+  const [migrate, recovery] = useMigrateLayer(shape);
+
+  const isMigrate = computed(() => status.value.active || status.value.hover);
+  // 鼠标状态改变则迁移图层
+  watch(
+    isMigrate,
+    (isMigrate, _, onCleanup) => {
+      if (isMigrate && tempLayer.value) {
+        migrate(tempLayer.value);
+        onCleanup(recovery);
+      }
+    },
+    { flush: 'sync' }
+  );
+};
+
+const useCurrentStaticZIndex = installGlobalVar(() => ref(0));
+
+export const useStaticZIndex = (refNum = 1) => {
+  const current = useCurrentStaticZIndex();
+  let isDestory = false;
+  const destroy = () => {
+    if (!isDestory) {
+      current.value -= refNum;
+      isDestory = true;
+    }
+  };
+  onUnmounted(destroy);
+  const result = new Array(refNum).fill(0).map((_, i) => current.value + i + 1);
+  current.value += refNum;
+  return result;
+};
+
+const useZIndexsManage = installGlobalVar(() => {
+  const store = useStore();
+  const map = ref(new Map<EntityShape, DrawItem>());
+  const current = useCurrentStaticZIndex();
+  const formal = useFormalLayer();
+  const sortItems = computed(() => {
+    const items = Array.from(map.value.values());
+    return  store.getItemsZIndex(items);
+  })
+
+  const setZIndexs = () => {
+    const shapes = Array.from(map.value.keys());
+    const raws = Array.from(map.value.values());
+    let start = current.value;
+    sortItems.value.forEach((item) => {
+      const rawNdx = raws.findIndex((raw) => raw === item);
+      const shape = shapes[rawNdx];
+      const layer = shape.getLayer();
+      if (toRaw(layer) !== toRaw(formal.value)) return -1;
+      shape.zIndex(start++);
+    });
+  };
+
+  watch(sortItems, setZIndexs)
+
+  return {
+    set(shape: EntityShape, item: DrawItem) {
+      map.value.set(shape, item);
+    },
+    del(shape: EntityShape) {
+      map.value.delete(shape);
+    },
+    get(shape: EntityShape) {
+      return map.value.get(shape);
+    },
+    refresh: setZIndexs,
+  };
+});
+
+export const useZIndex = (
+  shape: Ref<DC<EntityShape> | undefined>,
+  atData: Ref<DrawItem>
+) => {
+  const zIndexs = useZIndexsManage();
+  watch(shape, (shape, _, onCleanup) => {
+    const $shape = shape?.getNode();
+    if ($shape) {
+      watchEffect(() => {
+        zIndexs.set($shape, atData.value);
+      });
+      onCleanup(() => {
+        zIndexs.del($shape);
+      });
+    }
+  });
+};

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

@@ -1,14 +1,15 @@
 import { computed, reactive, ref, Ref, watch, watchEffect } from "vue";
-import { DC, EntityShape } from "../../helper/deconstruction";
+import { DC, EntityShape } from "../../deconstruction";
 import { Shape } from "konva/lib/Shape";
 import { globalWatch, installGlobalVar, useMode, useStage, useTransformIngShapes } from "./use-global-vars.ts";
 import { Stage } from "konva/lib/Stage";
 import { listener } from "../../utils/event.ts";
-import { mergeFuns } from "../../utils/shared.ts";
-import { ComponentValue, ShapeType } from "../components";
+import { inRevise, mergeFuns } from "../../utils/shared.ts";
+import { ComponentValue, DrawItem, ShapeType } from "../components";
 import { shapeTreeContain } from "../../utils/shape.ts";
-import { usePointerIsTransformerInner, useShapeTransformer, useTransformer } from "./use-transformer.ts";
+import { usePointerIsTransformerInner, useTransformer } from "./use-transformer.ts";
 import { Mode } from "@/constant/mode.ts";
+import { useAniamtion } from "./use-animation.ts";
 
 export const useMouseShapesStatus = installGlobalVar(
 	() => {
@@ -24,8 +25,9 @@ export const useMouseShapesStatus = installGlobalVar(
 
 	
 		const init = (stage: Stage) => {
+			console.log('init?')
 			let downTime: number
-			let downTarget: EntityShape
+			let downTarget: EntityShape | null
 			const inner = new WeakMap<EntityShape, boolean>()
 			
 
@@ -72,16 +74,18 @@ export const useMouseShapesStatus = installGlobalVar(
 			})
 			stage.on('pointerdown.mouse-status', (ev) => {
 				downTime = Date.now()
-				downTarget = ev.target as any
 				const target = shapeTreeContain(listeners.value, ev.target)
 				if (target && !press.value.includes(target)) {
 					press.value.push(target)
 				}
+				downTarget = target
 			})
 	
 			return mergeFuns(
 				listener(stage.container(), 'pointerup', () => {
-					if (Date.now() - downTime < 300) {
+					press.value = []
+					if ( Date.now() - downTime >= 300) return;
+					if (downTarget) {
 						const ndx = selects.value.indexOf(downTarget)
 						if (~ndx) {
 							selects.value.splice(ndx, 1)
@@ -89,8 +93,9 @@ export const useMouseShapesStatus = installGlobalVar(
 							selects.value.push(downTarget)
 						}
 						actives.value = [downTarget]
+					} else{
+						actives.value = []
 					}
-					press.value = []
 				}),
 				() => {
 					listeners.value.forEach((shape) => {
@@ -108,12 +113,12 @@ export const useMouseShapesStatus = installGlobalVar(
 		let cleanup: () => void
 		const stopStatusWatch = globalWatch(
 			() => [stage.value?.getStage(), [Mode.update, Mode.viewer].includes(mode.value)] as const, 
-			([stage, canUse], prev) => {
-				if (prev && (stage !== prev[0] || canUse !== prev[1])) {
+			(current, prev) => {
+				if (inRevise(prev, current)) {
 					cleanup! && cleanup()
-				}
-				if (stage && canUse) {
-					cleanup = init(stage)
+					if (current[0] && current[1]) {
+						cleanup = init(current[0])
+					}
 				}
 			},
 			{ immediate: true }
@@ -141,8 +146,9 @@ export const useMouseShapeStatus = (shape: Ref<DC<EntityShape> | undefined>) =>
 			status.listeners.push(shape)
 			onCleanup(() => {
 				for (const key in status) {
-					const ndx = status[key as keyof typeof status].indexOf(shape)
-					status.listeners.splice(ndx, 1)
+					const k = key as keyof typeof status
+					const ndx = status[k].indexOf(shape)
+					~ndx && status[k].splice(ndx, 1)
 				}
 			})
 		}
@@ -162,7 +168,9 @@ export const useMouseShapeStatus = (shape: Ref<DC<EntityShape> | undefined>) =>
 
 type MouseStyleProps<T extends ShapeType> = {
 	shape?: Ref<DC<EntityShape> | undefined>
-	style: ComponentValue<T, 'style'>
+	// getMouseStyle: ComponentValue<T, 'getMouseStyle'>
+	data: Ref<DrawItem<T>>
+	getMouseStyle: any
 }
 type ValueOf<T> = T[keyof T]
 export const useMouseStyle = <T extends ShapeType>(
@@ -171,20 +179,22 @@ export const useMouseStyle = <T extends ShapeType>(
 	const shape = props.shape || ref()
 	const status = useMouseShapeStatus(shape)
 	const transformIngShapes = useTransformIngShapes()
-
-	const style = computed(() => {
-		const styleMap = new Map([[props.style.default as any, true]])
-		if ('hover' in props.style) {
-			styleMap.set(props.style.hover, status.value.hover)
+	const mouseStyle = computed(() => {
+		return props.getMouseStyle(props.data.value)
+	})
+	const getStyle = () => {
+		const styleMap = new Map([[mouseStyle.value.default as any, true]])
+		if ('hover' in mouseStyle.value) {
+			styleMap.set(mouseStyle.value.hover, status.value.hover)
 		}
-		if ('press' in props.style) {
-			styleMap.set(props.style.press, status.value.press)
+		if ('press' in mouseStyle.value) {
+			styleMap.set(mouseStyle.value.press, status.value.press)
 		}
-		if ('focus' in props.style) {
-			styleMap.set(props.style.focus, status.value.active)
+		if ('focus' in mouseStyle.value) {
+			styleMap.set(mouseStyle.value.focus, status.value.active)
 		}
-		if ( 'drag' in props.style && transformIngShapes.value.includes(shape.value?.getNode()!)) {
-			styleMap.set(props.style.drag, true)
+		if ( 'drag' in mouseStyle.value && transformIngShapes.value.includes(shape.value?.getNode()!)) {
+			styleMap.set(mouseStyle.value.drag, true)
 		}
 		// if ('select' in props.style) {
 		// 	styleMap.set(props.style.select, status.value.select)
@@ -195,8 +205,19 @@ export const useMouseStyle = <T extends ShapeType>(
 			use && Object.assign(finalStyle as any, style)
 		}
 		return finalStyle
+	}
+	const style = ref<ValueOf<ComponentValue<T, 'style'>>>()
+	watchEffect(() => {
+		const newStyle = getStyle()
+		if (inRevise(newStyle, style.value)) {
+			style.value = newStyle
+		}
 	})
-
 	return { currentStyle: style, status, shape }
 }
 
+export const useAnimationMouseStyle = <T extends ShapeType>(props: MouseStyleProps<T>) => {
+	const { currentStyle } = useMouseStyle(props);
+	return useAniamtion(currentStyle as any);
+	// return [currentStyle]
+}

+ 364 - 0
src/core/hook/use-snap.ts

@@ -0,0 +1,364 @@
+import { useStore } from "../store";
+import { components, DrawItem, ShapeType } from "../components";
+import { computed, reactive, watch } from "vue";
+import { calcScaleFactor, lineLen, Pos, vector } from "@/utils/math";
+import { installGlobalVar } from "./use-global-vars";
+import { BaseItem } from "../components/util";
+import {
+  TransformerVectorType,
+  useGetTransformerOrigin,
+  useTransformer,
+} from "./use-transformer";
+import { Transform } from "konva/lib/Util";
+import { Circle } from "konva/lib/shapes/Circle";
+import { useFormalLayer, useTempLayer } from "./use-layer";
+import { useViewerInvertTransform, useViewerTransform } from "./use-viewer";
+import { MathUtils } from "three";
+
+type SnapPoint = Pos & Pick<DrawItem, "id">;
+export const useSnapPoints = installGlobalVar(() => {
+  const store = useStore();
+  const types = Object.keys(components) as ShapeType[];
+  const points = reactive(new Set<SnapPoint>());
+
+  for (const type of types) {
+    const api = (components as any)[type]?.getSnapPoints;
+    if (!api) continue;
+    watch(
+      (store as any)[type],
+      (items) => {
+        for (const item of items) {
+          watch(
+            () => {
+              const snaps = api(item) as SnapPoint[];
+              snaps.forEach((snap) => (snap.id = item.id));
+              return snaps;
+            },
+            (snaps, _, onCleanup) => {
+              snaps.forEach((snap) => points.add(snap));
+              onCleanup(() => {
+                snaps.forEach((snap) => points.delete(snap));
+              });
+            },
+            { immediate: true }
+          );
+        }
+      },
+      { immediate: true, flush: "sync" }
+    );
+  }
+
+  return computed(() => Array.from(points.values()));
+});
+
+export type SnapInfo = {
+  useXSnap?: AxisSnapInfo,
+  useYSnap?: AxisSnapInfo,
+  xSnaps: AxisSnapInfo[];
+  ySnaps: AxisSnapInfo[];
+  clear: () => void;
+};
+export const useSnapInfo = installGlobalVar(() => {
+  const snapInfo = reactive({
+    
+    xSnaps: [],
+    ySnaps: [],
+    clear() {
+      snapInfo.xSnaps.length = 0;
+      snapInfo.ySnaps.length = 0;
+    },
+  }) as SnapInfo;
+  return snapInfo;
+});
+
+type AxisSnapInfo = { ref: Pos; current: Pos; offset: number };
+
+export const getAxisSnapInfos = (
+  refPoints: Pos[],
+  selfPoints: Pos[],
+  snapOffset: number
+) => {
+  let xSnapInfos: AxisSnapInfo[] = [];
+  for (const snap of refPoints) {
+    for (const current of selfPoints) {
+      const offset = snap.x - current.x;
+      if (Math.abs(offset) < snapOffset) {
+        xSnapInfos.push({
+          ref: snap,
+          current,
+          offset: offset,
+        });
+        break;
+      }
+    }
+  }
+
+  let ySnapInfos: AxisSnapInfo[] = [];
+  for (const snap of refPoints) {
+    for (const current of selfPoints) {
+      const offset = snap.y - current.y;
+      if (Math.abs(offset) < snapOffset) {
+        ySnapInfos.push({
+          ref: snap,
+          current,
+          offset: offset,
+        });
+      }
+    }
+  }
+  return { xSnapInfos, ySnapInfos };
+};
+
+type SelfAttitude = {
+  rotation: number;
+  origin: Pos;
+  center: Pos;
+};
+const optimumSnapToAxis = (
+  xSnapInfos: AxisSnapInfo[],
+  ySnapInfos: AxisSnapInfo[],
+  type?: TransformerVectorType,
+  attitude?: SelfAttitude
+) => {
+  if (type === "rotater") return null;
+  const transform = new Transform();
+
+  let minOffset = xSnapInfos[0].offset;
+  let xSnapInfo: AxisSnapInfo | undefined = xSnapInfos[0];
+  for (let i = 0; i < xSnapInfos.length; i++) {
+    if (xSnapInfos[i].offset < minOffset) {
+      xSnapInfo = xSnapInfos[i];
+      minOffset = xSnapInfo.offset;
+    }
+  }
+
+  minOffset = ySnapInfos[0].offset;
+  let ySnapInfo: AxisSnapInfo | undefined = ySnapInfos[0];
+  for (let i = 0; i < ySnapInfos.length; i++) {
+    if (ySnapInfos[i].offset < minOffset) {
+      ySnapInfo = ySnapInfos[i];
+      minOffset = ySnapInfo.offset;
+    }
+  }
+
+  if (!xSnapInfo && !ySnapInfo) return null;
+  if (!type) {
+    return {
+      xSnapInfo,
+      ySnapInfo,
+      transform: transform.translate(
+        xSnapInfo?.offset || 0,
+        ySnapInfo?.offset || 0
+      ),
+    };
+  }
+  if (!attitude) return null;
+
+  const { center, rotation, origin } = attitude;
+
+  const scaleDirection = vector({ x: 0, y: 0 });
+  type.includes("left") && (scaleDirection.x -= 1);
+  type.includes("right") && (scaleDirection.x += 1);
+  type.includes("top") && (scaleDirection.y -= 1);
+  type.includes("bottom") && (scaleDirection.y += 1);
+
+  scaleDirection.normalize();
+  scaleDirection.rotateAround(
+    { x: origin.x - center.x, y: origin.y - center.y },
+    rotation
+  );
+  if (scaleDirection.x === 0) {
+    xSnapInfo = undefined;
+  }
+  if (scaleDirection.y === 0) {
+    ySnapInfo = undefined;
+  }
+  if (!xSnapInfo && !ySnapInfo) return null;
+
+  const xScaleFactor =
+    xSnapInfo &&
+    (xSnapInfo.ref.x - origin.x) /
+      ((xSnapInfo.current.x - origin.x) * scaleDirection.x);
+  const yScaleFactor =
+    ySnapInfo &&
+    (ySnapInfo.ref.y - origin.y) /
+      ((ySnapInfo.current.y - origin.y) * scaleDirection.y);
+
+  if (!xScaleFactor && !yScaleFactor) return null;
+
+  let scaleFactor;
+  if (xScaleFactor && yScaleFactor) {
+    if (xScaleFactor > yScaleFactor) {
+      scaleFactor = yScaleFactor;
+      xSnapInfo = undefined;
+    } else {
+      scaleFactor = xScaleFactor;
+      ySnapInfo = undefined;
+    }
+  } else if (xScaleFactor) {
+    scaleFactor = xScaleFactor;
+    ySnapInfo = undefined;
+  } else {
+    scaleFactor = yScaleFactor;
+    xSnapInfo = undefined;
+  }
+
+  scaleDirection.multiplyScalar(scaleFactor!);
+  transform
+    .translate(origin.x, origin.y)
+    .scale(scaleDirection.x, scaleDirection.y)
+    .translate(-origin.x, -origin.y);
+
+  return {
+    xSnapInfo,
+    ySnapInfo,
+    transform: transform,
+  };
+};
+
+const CanSnapOffset = 15;
+export const useSnapToPoints = (itemId?: string) => {
+  const store = useStore();
+  const type = itemId ? store.getType(itemId as any) : undefined;
+  const points = useSnapPoints();
+  const snapInfo = useSnapInfo();
+  const refPoints = computed(() => points.value.filter((p) => p.id !== itemId));
+  const api = type && (components as any)[type]?.getSnapPoints;
+  const transformer = useTransformer();
+  const getTransformerOrigin = useGetTransformerOrigin();
+  const transform = new Transform();
+  const rotateTransform = new Transform();
+  const invRotateTransform = new Transform();
+  const viewerInvertTransform = useViewerInvertTransform();
+  const layer = useTempLayer();
+  const circle = new Circle({ fill: "rgb(255, 0, 0)", width: 20, height: 20 });
+
+  const snap = (item: BaseItem | Pos, snapOffset = CanSnapOffset) => {
+    const operateType = transformer.getActiveAnchor() as TransformerVectorType;
+
+    let attitude: SelfAttitude | undefined = void 0;
+    if (operateType && operateType !== "rotater") {
+      const origin = getTransformerOrigin();
+      if (!origin) return null;
+      const node = transformer.nodes()[0];
+      const rect = node.getClientRect();
+      attitude = {
+        center: viewerInvertTransform.value.point({
+          x: rect.x + rect.width / 2,
+          y: rect.y + rect.height / 2,
+        }),
+        rotation: MathUtils.degToRad(node.rotation()),
+        origin,
+      };
+    }
+
+    const selfPoints = (api ? api(item) : [item]) as Pos[];
+    const axisInfos = getAxisSnapInfos(refPoints.value, selfPoints, snapOffset);
+
+    snapInfo.clear();
+    snapInfo.xSnaps = axisInfos.xSnapInfos
+    snapInfo.ySnaps = axisInfos.ySnapInfos
+
+    const toAxisResult = optimumSnapToAxis(
+      axisInfos.xSnapInfos,
+      axisInfos.ySnapInfos,
+      operateType,
+      attitude
+    );
+    if (!toAxisResult) return null;
+    
+    snapInfo.useXSnap = toAxisResult.xSnapInfo
+    snapInfo.useYSnap = toAxisResult.xSnapInfo
+    
+    return toAxisResult.transform;
+
+
+    transform.reset();
+    const offset = {
+      x: info.xSnapInfos[0]?.offset || 0,
+      y: info.ySnapInfos[0]?.offset || 0,
+    };
+    if (Math.abs(offset.x) < 0.0001 && Math.abs(offset.y) < 0.0001) return null;
+
+    if (!operateType) {
+      snapInfo.xSnaps = info.xSnapInfos;
+      snapInfo.ySnaps = info.ySnapInfos;
+      return transform.translate(offset.x, offset.y);
+    }
+
+    const origin = getTransformerOrigin();
+    if (!origin) return null;
+
+    const node = transformer.nodes()[0];
+    const rotation = MathUtils.degToRad(node.rotation());
+    const rect = node.getClientRect();
+    const center = viewerInvertTransform.value.point({
+      x: rect.x + rect.width / 2,
+      y: rect.y + rect.height / 2,
+    });
+
+    rotateTransform.reset();
+    rotateTransform
+      .translate(center.x, center.y)
+      .rotate(rotation)
+      .translate(-center.x, -center.y);
+
+    rotateTransform.copyInto(invRotateTransform);
+    invRotateTransform.invert();
+
+    const norOrigin = invRotateTransform.point(origin);
+    let scaleFactor: Partial<Pos> | null = null;
+
+    // const getTarget = (snap: ) => {
+
+    // }
+
+    for (const snap of info.xSnapInfos) {
+      scaleFactor = calcScaleFactor({
+        origin: norOrigin,
+        target: invRotateTransform.point({
+          x: snap.ref.x,
+          y: snap.current.y,
+        }),
+        current: invRotateTransform.point(snap.current),
+      });
+      if (scaleFactor) {
+        console.log("a?");
+        snapInfo.xSnaps = [snap];
+        break;
+      }
+    }
+    if (!scaleFactor) {
+      for (const snap of info.ySnapInfos) {
+        scaleFactor = calcScaleFactor({
+          origin: norOrigin,
+          target: invRotateTransform.point({
+            x: snap.current.x,
+            y: snap.ref.y,
+          }),
+          current: invRotateTransform.point(snap.current),
+        });
+        if (scaleFactor) {
+          console.log("b?");
+          snapInfo.ySnaps = [snap];
+          break;
+        }
+      }
+    }
+
+    if (!scaleFactor) return null;
+
+    const scaleTransform = new Transform()
+      .translate(norOrigin.x, norOrigin.y)
+      .scale(scaleFactor!.x!, 1)
+      .translate(-norOrigin.x, -norOrigin.y);
+
+    transform.reset();
+    return transform
+      .multiply(rotateTransform)
+      .multiply(scaleTransform)
+      .multiply(invRotateTransform);
+  };
+
+  return [snap, snapInfo.clear] as const;
+};

+ 305 - 207
src/core/hook/use-transformer.ts

@@ -1,11 +1,16 @@
 import { useMouseShapeStatus } from "./use-mouse-status.ts";
 import { Ref, ref, watch } from "vue";
-import { DC, EntityShape } from "../../helper/deconstruction";
+import { DC, EntityShape } from "../../deconstruction";
 import { Shape } from "konva/lib/Shape";
-import { installGlobalVar, useMode, useStage, useTransformIngShapes } from "./use-global-vars.ts";
+import {
+  installGlobalVar,
+  useMode,
+  useStage,
+  useTransformIngShapes,
+} from "./use-global-vars.ts";
 import { Mode } from "../../constant/mode.ts";
 import { Transform, Util } from "konva/lib/Util";
-import { Pos } from "@/utils/math.ts";
+import { Pos, vector } from "@/utils/math.ts";
 import { useConversionPosition } from "./use-coversion-position.ts";
 import { getOffset, listener } from "@/utils/event.ts";
 import { flatPositions, mergeFuns } from "@/utils/shared.ts";
@@ -13,14 +18,16 @@ import { Line } from "konva/lib/shapes/Line";
 import { setShapeTransform } from "@/utils/shape.ts";
 import { TransformerConfig, Transformer } from "konva/lib/shapes/Transformer";
 import { themeMouseColors } from "@/constant/help-style.ts";
-import { useAutomaticData } from "./use-automatic-data.ts";
+import { useSnapToPoints } from "./use-snap.ts";
+import { useViewerInvertTransform } from "./use-viewer.ts";
 
-export type TransformerExtends = Transformer &  { queueShapes: Ref<EntityShape[]> }
-export const transformRectPadding = 10;
+export type TransformerExtends = Transformer & {
+  queueShapes: Ref<EntityShape[]>;
+};
 export const useTransformer = installGlobalVar(() => {
-  const anchorCornerRadius = 5
-  const transformer = new Transformer({ 
-    borderStrokeWidth: 2, 
+  const anchorCornerRadius = 5;
+  const transformer = new Transformer({
+    borderStrokeWidth: 2,
     borderStroke: themeMouseColors.pub,
     anchorCornerRadius,
     anchorSize: anchorCornerRadius * 2,
@@ -28,35 +35,97 @@ export const useTransformer = installGlobalVar(() => {
     anchorStroke: themeMouseColors.pub,
     anchorFill: themeMouseColors.press,
     padding: 10,
-    useSingleNodeRotation: true 
+    useSingleNodeRotation: true,
   }) as TransformerExtends;
-  transformer.queueShapes = ref([])
+  transformer.queueShapes = ref([]);
   return transformer;
 }, Symbol("transformer"));
 
 export const usePointerIsTransformerInner = () => {
-  const transformer = useTransformer()
-  const stage = useStage()
+  const transformer = useTransformer();
+  const stage = useStage();
   return () => {
-    const $stage = stage.value!.getStage()
-    const tfRect = transformer.getClientRect()
-    tfRect.x -= transformRectPadding
-    tfRect.y -= transformRectPadding
-    tfRect.width += transformRectPadding
-    tfRect.height += transformRectPadding
-    const pointRect = { ...$stage.pointerPos!, width: 1, height: 1 }
-    
+    const $stage = stage.value!.getStage();
+    const tfRect = transformer.getClientRect();
+    const padding = transformer.padding();
+    tfRect.x -= padding;
+    tfRect.y -= padding;
+    tfRect.width += padding;
+    tfRect.height += padding;
+    const pointRect = { ...$stage.pointerPos!, width: 1, height: 1 };
+
     return Util.haveIntersection(tfRect, pointRect);
-  }
-}
+  };
+};
+
+export type TransformerVectorType =
+  | "middle-left"
+  | "middle-right"
+  | "top-center"
+  | "bottom-center"
+  | "top-right"
+  | "top-left"
+  | "bottom-right"
+  | "bottom-left"
+  | "rotater";
+
+export const useGetTransformerVectors = () => {
+  const viewerInvertTransform = useViewerInvertTransform();
+  const transformer = useTransformer();
 
+  return (type: TransformerVectorType): Pos | null => {
+    if (!transformer.nodes().length) return null;
+    const merTransform = viewerInvertTransform.value
+      .copy()
+      .multiply(transformer.getTransform());
+
+    const getVector = (operateType: TransformerVectorType): Pos => {
+      if (operateType === "rotater") {
+        return vector(getVector("bottom-left"))
+          .add(getVector("bottom-right"))
+          .add(getVector("top-left"))
+          .add(getVector("top-right"))
+          .divideScalar(4);
+      } else {
+        const centerNode = transformer.findOne(`.${operateType}`)!;
+        return {
+          x: centerNode.x(),
+          y: centerNode.y(),
+        };
+      }
+    };
+    return merTransform.point(getVector(type));
+  };
+};
+
+export const useGetTransformerOrigin = () => {
+  const transformer = useTransformer();
+  const getTransformerVectors = useGetTransformerVectors();
+  const originTypeMap = {
+    "middle-left": "middle-right",
+    "middle-right": "middle-left",
+    "top-center": "bottom-center",
+    "bottom-center": "top-center",
+    "top-right": "bottom-left",
+    "top-left": "bottom-right",
+    "bottom-right": "top-left",
+    "bottom-left": "top-right",
+    'rotater': "rotater",
+  } as const;
+
+  return (): Pos | null => {
+    if (!transformer.nodes().length) return null;
+    const operateType = transformer.getActiveAnchor() as TransformerVectorType;
+    if (!operateType || !originTypeMap[operateType]) return null;
+    return getTransformerVectors(originTypeMap[operateType]);
+  };
+};
 
-export const useShapeDrag = (
-  shape: Ref<DC<EntityShape> | undefined>) => {
-  const offset = ref<Pos>()
+export const useShapeDrag = (shape: Ref<DC<EntityShape> | undefined>) => {
+  const offset = ref<Pos>();
   const mode = useMode();
   const conversion = useConversionPosition(true);
-  const transformIngShapes = useTransformIngShapes()
+  const transformIngShapes = useTransformIngShapes();
 
   const init = (shape: EntityShape) => {
     const dom = shape.getStage()!.container();
@@ -64,47 +133,47 @@ export const useShapeDrag = (
     let start: Pos | undefined;
     const enter = (position: Pos) => {
       if (!start) {
-        start = position
+        start = position;
         mode.push(Mode.update);
-        transformIngShapes.value.push(shape)
+        transformIngShapes.value.push(shape);
       }
-    }
+    };
     const leave = () => {
       if (start) {
-        offset.value = void 0
+        offset.value = void 0;
         mode.pop();
         start = void 0;
-        const ndx = transformIngShapes.value.indexOf(shape)
-        ~ndx && transformIngShapes.value.splice(ndx, 1)
+        const ndx = transformIngShapes.value.indexOf(shape);
+        ~ndx && transformIngShapes.value.splice(ndx, 1);
       }
-    }
+    };
 
     shape.draggable(true);
     shape.dragBoundFunc((_, ev) => {
       if (!start) {
-        enter(ev)
+        enter(ev);
       } else {
         const end = conversion(getOffset(ev, dom));
         offset.value = {
           x: end.x - start.x,
-          y: end.y - start.y
-        }
+          y: end.y - start.y,
+        };
       }
-      return shape.absolutePosition()
+      return shape.absolutePosition();
     });
 
     shape.on("pointerdown.mouse-drag", (ev) => {
-      enter(conversion(getOffset(ev.evt)))
+      enter(conversion(getOffset(ev.evt)));
     });
 
     return mergeFuns([
       () => {
         shape.draggable(false);
         shape.off("pointerdown.mouse-status");
-        start && leave()
+        start && leave();
       },
       listener(document.documentElement, "pointerup", () => {
-        start && leave()
+        start && leave();
       }),
     ]);
   };
@@ -118,219 +187,248 @@ export const useShapeDrag = (
   return offset;
 };
 
-type Rep<T> = { tempShape: T, update: () => void, destory: () => void }
-const emptyFn = () => {}
+type Rep<T> = { tempShape: T; update: () => void; destory: () => void };
+const emptyFn = () => {};
 export const useShapeTransformer = <T extends EntityShape>(
-  shape: Ref<DC<T> | undefined>, 
+  shape: Ref<DC<T> | undefined>,
   transformerConfig: TransformerConfig = {},
-  replaceShape?: (transformer: TransformerExtends, shape: T) => Rep<T>
+  replaceShape?: (transformer: TransformerExtends, shape: T) => Rep<T>,
+  handlerTransform?: (transform: Transform) => Transform
 ) => {
-  const offset = useShapeDrag(shape)
-  const transform = ref<Transform>()
+  const offset = useShapeDrag(shape);
+  const transform = ref<Transform>();
   const status = useMouseShapeStatus(shape);
   const mode = useMode();
   const transformer = useTransformer();
-  const transformIngShapes = useTransformIngShapes()
-
-  const init = ($shape: T) => mergeFuns(
-    watch(
-      () => status.value.hover,
-      (active, _, onCleanup) => {
-        const parent = $shape.parent;
-        if (!(active && parent)) return;
-        const oldConfig: TransformerConfig = {}
-
-        for (const key in transformerConfig) {
-          oldConfig[key] = (transformer as any)[key]();
-          (transformer as any)[key](transformerConfig[key]);
-        }
-
-        let rep: Rep<T>
-        if (replaceShape) {
-          rep = replaceShape(transformer, $shape)
-        } else {
-          rep = {
-            tempShape: $shape,
-            destory: emptyFn,
-            update: emptyFn
+  const transformIngShapes = useTransformIngShapes();
+
+  const init = ($shape: T) =>
+    mergeFuns(
+      watch(
+        () => status.value.hover,
+        (active, _, onCleanup) => {
+          const parent = $shape.parent;
+          if (!(active && parent)) return;
+          const oldConfig: TransformerConfig = {};
+
+          for (const key in transformerConfig) {
+            oldConfig[key] = (transformer as any)[key]();
+            (transformer as any)[key](transformerConfig[key]);
           }
-          transformer.nodes([$shape])
-          transformer.queueShapes.value = [$shape]
-        }
-        parent.add(transformer);
-
-        const updateTransform = () => {
-          transform.value = rep.tempShape.getTransform().copy()
-        }
 
-        const downHandler = () => {
-          isEnter && mode.pop()
-          isEnter = true
-          rep.update()
-          mode.push(Mode.update);
-          transformIngShapes.value.push($shape)
-        }
-
-        let isEnter = false
-        transformer.on("pointerdown.shapemer", downHandler);
-        transformer.on("transform.shapemer", updateTransform);
-        const stop = listener($shape.getStage()!.container(), "pointerup", () => {
-          if (isEnter) {
-            mode.pop()
-            transform.value = void 0
-            isEnter = false
-            const ndx = transformIngShapes.value.indexOf($shape)
-            ~ndx && transformIngShapes.value.splice(ndx, 1)
-          }
-        })
-
-        // 拖拽时要更新矩阵
-        let prevMoveTf: Transform | null = null
-        const stopDragWatch = watch(offset, (translate, oldTranslate) => {
-          if (translate) {
-            if (!oldTranslate) {
-              rep.update()
-            }
-            const moveTf = new Transform().translate(translate.x, translate.y)
-            const finalTf = moveTf.copy()
-            prevMoveTf && finalTf.multiply(prevMoveTf.invert())
-            finalTf.multiply(rep.tempShape.getTransform())
-            prevMoveTf = moveTf
-
-            setShapeTransform(rep.tempShape, finalTf);
-            updateTransform()
+          let rep: Rep<T>;
+          if (replaceShape) {
+            rep = replaceShape(transformer, $shape);
           } else {
-            prevMoveTf = null
-            transform.value = void 0
+            rep = {
+              tempShape: $shape,
+              destory: emptyFn,
+              update: emptyFn,
+            };
+            transformer.nodes([$shape]);
+            transformer.queueShapes.value = [$shape];
           }
-        }, { immediate: true })
+          parent.add(transformer);
 
-        onCleanup(() => {
-          for (const key in oldConfig) {
-            ;(transformer as any)[key](oldConfig[key]);
-          }
-          stop()
-          stopDragWatch()
-          parent.add($shape);
-          // TODO: 有可能transformer已经转移
-          if (transformer.queueShapes.value.includes($shape)) {
-            transformer.nodes([]);
-            transformer.queueShapes.value = []
-          }
-          transform.value = void 0;
-          rep.destory()
-          if (isEnter) {
-            mode.pop()
-            const ndx = transformIngShapes.value.indexOf($shape)
-            ~ndx && transformIngShapes.value.splice(ndx, 1)
-          }
-          transformer.off("pointerdown.shapemer", downHandler);
-          transformer.off("transform.shapemer", updateTransform);
-        });
-      },
-      { immediate: true }
-    )
-  )
+          const updateTransform = () => {
+            let appleTransform = rep.tempShape.getTransform().copy();
+            if (handlerTransform) {
+              appleTransform = handlerTransform(appleTransform);
+              setShapeTransform(rep.tempShape, appleTransform);
+            }
+            transform.value = appleTransform;
+          };
+
+          const downHandler = () => {
+            isEnter && mode.pop();
+            isEnter = true;
+            rep.update();
+            mode.push(Mode.update);
+            transformIngShapes.value.push($shape);
+          };
+
+          let isEnter = false;
+          transformer.on("pointerdown.shapemer", downHandler);
+          transformer.on("transform.shapemer", updateTransform);
+          const stop = listener(
+            $shape.getStage()!.container(),
+            "pointerup",
+            () => {
+              if (isEnter) {
+                mode.pop();
+                transform.value = void 0;
+                isEnter = false;
+                const ndx = transformIngShapes.value.indexOf($shape);
+                ~ndx && transformIngShapes.value.splice(ndx, 1);
+              }
+            }
+          );
+
+          // 拖拽时要更新矩阵
+          let prevMoveTf: Transform | null = null;
+          const stopDragWatch = watch(
+            offset,
+            (translate, oldTranslate) => {
+              if (translate) {
+                if (!oldTranslate) {
+                  rep.update();
+                }
+                const moveTf = new Transform().translate(
+                  translate.x,
+                  translate.y
+                );
+                const finalTf = moveTf.copy();
+                prevMoveTf && finalTf.multiply(prevMoveTf.invert());
+                finalTf.multiply(rep.tempShape.getTransform());
+                prevMoveTf = moveTf;
+
+                setShapeTransform(rep.tempShape, finalTf);
+                transformer.fire("transform");
+                // updateTransform()
+              } else {
+                prevMoveTf = null;
+                transform.value = void 0;
+              }
+            },
+            { immediate: true }
+          );
+
+          onCleanup(() => {
+            for (const key in oldConfig) {
+              (transformer as any)[key](oldConfig[key]);
+            }
+            stop();
+            stopDragWatch();
+            // parent.add($shape);
+            // TODO: 有可能transformer已经转移
+            if (transformer.queueShapes.value.includes($shape)) {
+              transformer.nodes([]);
+              transformer.queueShapes.value = [];
+              // transformer.remove();
+            }
+            transform.value = void 0;
+            rep.destory();
+            if (isEnter) {
+              mode.pop();
+              const ndx = transformIngShapes.value.indexOf($shape);
+              ~ndx && transformIngShapes.value.splice(ndx, 1);
+            }
+            transformer.off("pointerdown.shapemer", downHandler);
+            transformer.off("transform.shapemer", updateTransform);
+          });
+        },
+        { immediate: true }
+      )
+    );
   watch(shape, (shape, _, onCleanup) => {
     if (shape) {
-      onCleanup(init(shape.getStage()));
+      const stop = init(shape.getStage());
+      onCleanup(stop);
+    } else {
+      onCleanup(() => {});
     }
   });
   return transform;
 };
 
-
 export const genTransformerRepShape = <T extends Shape>(
-  shape: T, 
+  shape: T,
   transformer: TransformerExtends,
   getAttitudeMat: () => number[],
   resumeData: (repShape: T, inverAttitude: Transform) => void
 ) => {
-  const repShape = shape.clone({ 
-    fill: 'rgb(0, 255, 0)', 
-    visible: false, 
-    strokeWidth: 0
-  })
+  const repShape = shape.clone({
+    fill: "rgb(0, 255, 0)",
+    visible: false,
+    strokeWidth: 0,
+  });
 
   const update = () => {
     const attitude = new Transform(getAttitudeMat());
     const inverAttitude = attitude.copy().invert();
-    setShapeTransform(repShape, attitude)
-    resumeData(repShape, inverAttitude)
-  }
-
+    setShapeTransform(repShape, attitude);
+    resumeData(repShape, inverAttitude);
+  };
 
   if (import.meta.env.DEV) {
-    repShape.visible(true)
-    shape.opacity(0.9)
-    repShape.opacity(0.1)
+    repShape.visible(true);
+    shape.opacity(0.9);
+    repShape.opacity(0.1);
   }
 
-  update()
-  shape.parent!.add(repShape)
-  repShape.zIndex(shape.getZIndex() - 1)
+  update();
+  shape.parent!.add(repShape);
+  repShape.zIndex(shape.getZIndex());
   transformer.nodes([repShape]);
-  transformer.queueShapes.value = [shape]
+  transformer.queueShapes.value = [shape];
 
   return {
     tempShape: repShape,
     update,
     destory: () => {
-      repShape.remove()
-      shape.opacity(1)
-    }
+      repShape.remove();
+      shape.opacity(1);
+    },
   };
-}
-
+};
 
 export type LineTransformerData = {
-  points: Pos[],
-  attitude: number[]
-}
-export const useLineTransformer = (
+  points: Pos[];
+  attitude: number[];
+};
+export const useLineTransformer = <T extends LineTransformerData>(
   shape: Ref<DC<Line> | undefined>,
-  init: () => LineTransformerData,
-  callback: (data: LineTransformerData) => void,
+  data: Ref<T>,
+  callback: (data: T) => void
 ) => {
-  const data = useAutomaticData(() => init())
-  let tempShape: Line
-  let inverAttitude: Transform
-  let stableVs = data.value.points
-  let tempVs = data.value.points
-
-  const transform = useShapeTransformer(shape, undefined, (transformer, $shape) => {
-    const result = genTransformerRepShape(
-      $shape, 
-      transformer, 
-      () => data.value.attitude, (repShape, inverMat) => {
-        const initVs = stableVs.map(v => inverMat.point(v))
-        repShape.points(flatPositions(initVs) )
-        repShape.closed(true)
-        inverAttitude = inverMat
-      }
-    )
-    tempShape = result.tempShape
-    return result
-  })
-
-  watch(() => shape.value?.getNode(), $shape => {
-    if ($shape) {
-      $shape.points(flatPositions(tempVs))
+  let tempShape: Line;
+  let inverAttitude: Transform;
+  let stableVs = data.value.points;
+  let tempVs = data.value.points;
+  let operateShape: Line;
+
+  const transformer = useTransformer();
+  const transform = useShapeTransformer(
+    shape,
+    undefined,
+    (transformer, $shape) => {
+      const result = genTransformerRepShape(
+        $shape,
+        transformer,
+        () => data.value.attitude,
+        (repShape, inverMat) => {
+          const initVs = stableVs.map((v) => inverMat.point(v));
+          repShape.points(flatPositions(initVs));
+          repShape.closed(true);
+          inverAttitude = inverMat;
+          operateShape = repShape;
+        }
+      );
+      tempShape = result.tempShape;
+      return result;
     }
-  })
+  );
 
+  const [snapToPoints, clearSnap] = useSnapToPoints((data as any).value.id);
   watch(transform, (current, prev) => {
     if (current) {
       // 顶点更新
-      const $shape = shape.value!.getNode()
-      const transfrom = current.copy().multiply(inverAttitude)
-      tempVs = stableVs.map(v => transfrom.point(v))
-      $shape.points(flatPositions(tempVs))
+      const transfrom = current.copy().multiply(inverAttitude);
+      tempVs = stableVs.map((v) => transfrom.point(v));
+      const snapTransform = snapToPoints({
+        ...data.value,
+        points: tempVs,
+      } as any);
+      if (snapTransform) {
+        tempVs = tempVs.map((v) => snapTransform.point(v));
+      }
+      data.value.points = tempVs;
     } else if (prev && tempShape) {
       data.value.attitude = tempShape.getTransform().m;
-      data.value.points = stableVs = tempVs
-      callback(data.value)
+      data.value.points = stableVs = tempVs;
+      callback(data.value);
+      clearSnap();
     }
-  })
-  return [transform, data] as const
-}
+  });
+  return transform;
+};

+ 1 - 0
src/core/propertys/color.vue

@@ -2,6 +2,7 @@
   <el-color-picker
     :modelValue="value"
     @active-change="(color) => $emit('update:value', color)"
+    popper-class="com-color-pick"
     size="small"
     :predefine="predefineColors"
   />

+ 0 - 20
src/core/propertys/controller.ts

@@ -1,20 +0,0 @@
-import Color from "./color.vue";
-
-export const colorType = "color";
-
-export const propertyComponents = {
-  [colorType]: Color,
-};
-
-export type PropertyType = keyof typeof propertyComponents;
-export type PropertyComponents<T extends PropertyType> =
-  (typeof propertyComponents)[T];
-
-export type PropertyValue<T extends PropertyType> = InstanceType<PropertyComponents<T>>['$props']['value']
-
-export type PropertyDescribes = Record<string, { type: PropertyType; label: string }>
-export type PropertysData<T extends PropertyDescribes = PropertyDescribes> = {
-  [K in keyof T]: PropertyValue<T[K]['type']>
-}
-
-export { default as Propertys } from './propertys.vue'

+ 41 - 67
src/core/propertys/propertys.vue

@@ -1,67 +1,41 @@
 <template>
-  <Teleport :to="mount" v-if="pointer">
-    <div
-      class="mount-property"
-      @pointerdown.stop
-      @pointermove.stop
-      @pointerup.stop
-      @mousedown.stop
-      @mousemove.stop
-      @mouseup.stop
-    >
-      <div class="mask" @click.stop="prevDefault" :class="{ show: showMask }"></div>
+  <Teleport :to="`#${DomMountId}`" v-if="stage">
+    <transition name="pointer-fade">
       <div
+        v-if="pointer"
         :style="{ transform: pointer }"
         :size="8"
         class="propertys-controller"
         ref="layout"
       >
-        <div class="item" v-for="(val, key) in describes">
-          <span>{{ val.label }}</span>
-          <div>
-            <component
-              @click.stop="showMask = true"
-              v-model:value="data[key]"
-              :is="propertyComponents[val.type]"
-              :key="key"
-            />
-          </div>
+        <div class="item" v-for="menu in menus" @click="menu.handler">
+          <ElButton type="primary" link :icon="menu.icon" size="small">
+            {{ menu.label }}
+          </ElButton>
         </div>
       </div>
-    </div>
+    </transition>
   </Teleport>
 </template>
 
 <script lang="ts" setup>
 import { computed, nextTick, ref, watch } from "vue";
 import { useStage, useTransformIngShapes } from "../hook/use-global-vars.ts";
-import { PropertyDescribes, propertyComponents } from "./controller.ts";
-import { DC, EntityShape } from "@/helper/deconstruction";
+import { DC, EntityShape } from "@/deconstruction.js";
 import { useMouseShapeStatus } from "../hook/use-mouse-status.ts";
 import { useViewerTransformConfig } from "../hook/use-viewer.ts";
 import { Transform } from "konva/lib/Util";
+import { DomMountId } from "@/constant/index.ts";
+import { ElButton } from "element-plus";
 
 const props = defineProps<{
   target: DC<EntityShape> | undefined;
-  data: Record<string, any>;
-  describes: PropertyDescribes;
+  data?: Record<string, any>;
+  menus: Array<{ icon?: any; label?: string; handler: () => void }>;
 }>();
 
 const layout = ref<HTMLDivElement>();
-const showMask = ref(false);
-const prevDefault = (ev: MouseEvent) => {
-  showMask.value = false;
-  const mouseEvent = new MouseEvent("pointermove", ev);
-  mount.value!.querySelector(".konvajs-content")!.dispatchEvent(mouseEvent);
-};
-
 const stage = useStage();
-const mount = computed(() => {
-  if (!stage.value) return;
-  const $stage = stage.value.getStage();
-  return $stage.container();
-});
-
 const status = useMouseShapeStatus(computed(() => props.target));
 const transformIngShapes = useTransformIngShapes();
 const hidden = computed(
@@ -69,12 +43,19 @@ const hidden = computed(
     !stage.value?.getStage() ||
     !props.target?.getNode() ||
     !status.value.hover ||
+    status.value.active ||
     transformIngShapes.value.length !== 0
 );
 
 const move = new Transform();
 const pointer = ref<string | null>(null);
 const calcPointer = async () => {
+  if (hidden.value) {
+    pointer.value = null;
+    return;
+  } else if (pointer.value) {
+    return;
+  }
   const $stage = stage.value!.getStage();
   const mousePosition = $stage.pointerPos;
   if (!mousePosition) {
@@ -127,7 +108,7 @@ const resetPointer = () => {
     return;
   }
   clearTimeout(timeout);
-  timeout = setTimeout(calcPointer, 300);
+  timeout = setTimeout(calcPointer, 500);
 };
 
 watch(hidden, resetPointer);
@@ -135,29 +116,12 @@ watch(useViewerTransformConfig(), () => {
   pointer.value = null;
   resetPointer();
 });
-watch(() => props.data, resetPointer);
+// watch(() => props.data, resetPointer);
 </script>
 
 <style lang="scss" scoped>
-.mount-property {
-  position: absolute;
-  inset: 0;
-  overflow: hidden;
-  pointer-events: none;
-}
-.mask {
-  position: absolute;
-  inset: 0;
-  background-color: rgba(0, 0, 0, 0);
-  pointer-events: none;
-  transition: background-color 0.3s ease;
-  &.show {
-    background-color: rgba(0, 0, 0, 0.1);
-    pointer-events: all;
-  }
-}
-
 .propertys-controller {
+  pointer-events: none;
   position: absolute;
   border-radius: 4px;
   border: 1px solid #e4e7ed;
@@ -167,34 +131,44 @@ watch(() => props.data, resetPointer);
   overflow: hidden;
   padding: 6px 12px;
   color: #303133;
-  width: max-content;
+  max-width: 240px;
   font-size: 12px;
+  max-height: 50px;
   display: flex;
   align-items: center;
   box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
   min-width: 10px;
-  // pointer-events: none;
 
   .item {
+    pointer-events: all;
     display: flex;
+    flex: 0 0 auto;
     align-items: center;
     pointer-events: all;
-    span {
-      margin-right: 4px;
-    }
 
     &:not(:first-child) {
-      span {
-        margin-left: 4px;
-      }
       &::before {
         display: block;
         flex: 0 0 auto;
         content: "|";
         opacity: 0.7;
         margin-left: 4px;
+        margin-right: 4px;
       }
     }
   }
 }
+
+.pointer-fade-enter-active,
+.pointer-fade-leave-active {
+  transition: opacity 0.3s ease, max-height 0.3s ease;
+  .item {
+    pointer-events: none;
+  }
+}
+.pointer-fade-enter-from,
+.pointer-fade-leave-to {
+  opacity: 0;
+  max-height: 0;
+}
 </style>

+ 35 - 0
src/core/propertys/index.ts

@@ -0,0 +1,35 @@
+import Color from "./color.vue";
+import Num from "./num.vue";
+
+export const colorType = "color";
+export const numType = "num";
+
+export const propertyComponents = {
+  [colorType]: Color,
+  [numType]: Num,
+};
+
+export type PropertyType = keyof typeof propertyComponents;
+export type PropertyComponents<T extends PropertyType> =
+  (typeof propertyComponents)[T];
+
+export type PropertyProps<T extends PropertyType> = InstanceType<
+  PropertyComponents<T>
+>["$props"];
+export type PropertyValue<T extends PropertyType> = PropertyProps<T>["value"];
+
+export type PropertyDescribes = Record<
+  string,
+  {
+    type: PropertyType;
+    label: string;
+    props?: Partial<PropertyProps<PropertyType>>;
+    value?: PropertyValue<PropertyType>
+  }
+>;
+export type PropertysData<T extends PropertyDescribes = PropertyDescribes> = {
+  [K in keyof T]: PropertyValue<T[K]["type"]>;
+};
+
+export { default as PropertyUpdate } from "./mount-property.vue";
+export { default as Operate } from "./hover-operate.vue";

+ 112 - 0
src/core/propertys/mount-property.vue

@@ -0,0 +1,112 @@
+<template>
+  <Teleport :to="`#${DomMountId}`" v-if="stage">
+    <transition name="mount-fade">
+      <div class="mount-layout" v-if="!hidden">
+        <div :size="8" class="mount-controller">
+          <template v-for="(val, key) in describes" :key="key">
+            <span>{{ val.label }}</span>
+            <div>
+              <component
+                v-bind="describes[key].props"
+                :value="
+                  'value' in describes[key] ? describes[key].value : data && data[key]
+                "
+                @update:value="(val: any) => updateValue(key, val)"
+                :is="propertyComponents[val.type]"
+                :key="key"
+              />
+            </div>
+          </template>
+        </div>
+      </div>
+    </transition>
+  </Teleport>
+</template>
+
+<script lang="ts" setup>
+import { computed, watch } from "vue";
+import { useStage, useTransformIngShapes } 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";
+
+const props = defineProps<{
+  show?: boolean;
+  target: DC<EntityShape> | undefined;
+  data?: Record<string, any>;
+  describes: PropertyDescribes;
+}>();
+const emit = defineEmits<{ (e: "change"): void }>();
+
+const stage = useStage();
+const status = useMouseShapeStatus(computed(() => props.target));
+const transformIngShapes = useTransformIngShapes();
+const hidden = computed(
+  () =>
+    !props.show &&
+    (!stage.value?.getStage() ||
+      !props.target?.getNode() ||
+      !status.value.active ||
+      transformIngShapes.value.length !== 0)
+);
+
+let isUpdate = false;
+const updateValue = (key: string, val: any) => {
+  if ("value" in props.describes[key]) {
+    props.describes[key].value = val;
+  } else {
+    props.data![key] = val;
+  }
+  isUpdate = true;
+};
+
+watch(hidden, (nHidden, oHidden) => {
+  if (nHidden && nHidden !== oHidden && isUpdate) {
+    isUpdate = false;
+    emit("change");
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.mount-layout {
+  pointer-events: all;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  position: absolute;
+  border-left: 1px solid #e4e7ed;
+  background-color: rgba(255, 255, 255, 0.7);
+  overflow-y: auto;
+  padding: 6px 12px;
+  color: #303133;
+  width: 240px;
+  font-size: 12px;
+  box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
+
+  .mount-controller {
+    display: grid;
+    grid-template-columns: auto 1fr;
+    gap: 10px 0;
+    align-items: center;
+    overflow: hidden;
+    span {
+      margin-right: 4px;
+      &::after {
+        content: ":";
+      }
+    }
+  }
+}
+
+.mount-fade-enter-active,
+.mount-fade-leave-active {
+  transition: transform 0.3s ease, opacity 0.3s ease;
+}
+.mount-fade-enter-from,
+.mount-fade-leave-to {
+  transform: translateX(100%);
+  opacity: 0;
+}
+</style>

+ 45 - 0
src/core/propertys/num.vue

@@ -0,0 +1,45 @@
+<template>
+  <el-slider
+    class="property-num-slider"
+    :modelValue="value"
+    @update:model-value="(val: any) => $emit('update:value', val)"
+    size="small"
+    height="200px"
+    width="50px"
+    style="cursor: pointer"
+    :step="step || 0.1"
+    :min="min"
+    :max="max"
+    show-input
+  />
+</template>
+
+<script lang="ts" setup>
+import { ElSlider, ElPopover, ElInput } from "element-plus";
+
+defineProps<{
+  value: number;
+  min?: number;
+  max?: number;
+  step?: number;
+}>();
+defineEmits<{ (e: "update:value", val: number): void; (e: "click"): void }>();
+</script>
+
+<style lang="scss">
+.property-num-slider {
+  .el-input__wrapper {
+    padding: 0 !important;
+  }
+  .el-slider__input {
+    width: 50px;
+  }
+  .el-input-number__increase,
+  .el-input-number__decrease {
+    display: none;
+  }
+  .el-slider__runway.show-input {
+    margin-right: 10px;
+  }
+}
+</style>

+ 19 - 0
src/core/propertys/util.ts

@@ -0,0 +1,19 @@
+import { Ref } from "vue";
+import { PropertyDescribes } from ".";
+
+export const generateDescribes = (data: Ref<any>, defaultData: any, describes: PropertyDescribes) => {
+  for (const key in describes) {
+    if (!(key in data) && (key in defaultData) && !('value' in describes[key])) {
+      Object.defineProperty(describes[key], 'value', {
+        get() {
+          return data.value[key] || defaultData[key]
+        },
+        set(val) {
+          data.value[key] = val
+          return true
+        }
+      }) 
+    }
+  }
+  return describes;
+}

+ 99 - 73
src/core/renderer/draw-group.vue

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

+ 16 - 15
src/core/renderer/group.vue

@@ -1,21 +1,22 @@
 <template>
-	<ShapeComponent
-			:data="item"
-			v-for="(item, i) in items"
-			@update="value => $emit('updateItem', { ndx: i, value })"
-	/>
+  <ShapeComponent
+    :data="item"
+    v-for="item in items"
+    :key="item.id"
+    @update="(value) => store.setItem(type, { id: item.id, value })"
+    @add="(value: any) => store.addItem(type, value)"
+    @del="() => store.delItem(type, item.id)"
+  />
 </template>
 
 <script setup lang="ts">
-import { ShapeType, DrawData, components, DrawItem } from "../components";
+import { ShapeType, components } from "../components";
 import { computed } from "vue";
+import { useStore } from "../store";
 
-const props = defineProps<{ type: ShapeType, data: DrawData }>()
-const type = props.type as 'arrow'
-const ShapeComponent = components[type].Component
-const items = computed(() => props.data[type] || [])
-
-defineEmits<{ (e: 'updateItem', data: {value: DrawItem, ndx: number}): void }>()
-
-
-</script>
+const props = defineProps<{ type: ShapeType }>();
+const store = useStore();
+const type = props.type as "arrow";
+const ShapeComponent = components[type].Component;
+const items = computed(() => store[type] || []);
+</script>

+ 60 - 49
src/core/renderer/renderer.vue

@@ -1,62 +1,73 @@
 <template>
-	<v-stage ref="stage" :config="size">
-		<v-layer :config="viewerConfig" id="formal">
-			<!--	不可去除,去除后移动端拖拽会有溢出	-->
-			<v-rect :config="{ ...size, fill: 'rgba(0,0,0,0)', listener: false, ...invertViewerConfig }"/>
-			<ShapeGroup
-					v-for="type in types"
-					:type="type"
-					:key="type"
-					:data="data"
-					@updateItem="playData => updateHandler(type, playData)"
-			/>
-		</v-layer>
-		<!--	临时组,提供临时绘画	-->
-		<v-layer :config="viewerConfig" id="temp">
-			<TempShapeGroup v-for="type in types" :type="type" :key="type"
-											@add-items="items => addItemsHandler(type, items)"/>
-		</v-layer>
-	</v-stage>
+  <div class="draw-layout">
+    <div class="mount-mask" :id="DomMountId" />
+    <v-stage ref="stage" :config="size">
+      <v-layer :config="viewerConfig" id="formal">
+        <!--	不可去除,去除后移动端拖拽会有溢出	-->
+        <!-- <v-rect
+          :config="{
+            ...size,
+            fill: 'rgba(0,0,0,0)',
+            listener: false,
+            ...invertViewerConfig,
+          }"
+        /> -->
+        <ShapeGroup v-for="type in types" :type="type" :key="type" />
+      </v-layer>
+      <!--	临时组,提供临时绘画,以及高频率渲染	-->
+      <v-layer :config="viewerConfig" id="temp">
+        <TempShapeGroup v-for="type in types" :type="type" :key="type" />
+      </v-layer>
+      <v-layer id="helper">
+        <ActiveBoxs />
+        <SnapLines />
+      </v-layer>
+    </v-stage>
+  </div>
 </template>
 
 <script lang="ts" setup>
-import ShapeGroup from './group.vue'
-import TempShapeGroup from './draw-group.vue'
-import { ShapeType, components, DrawItem } from "../components";
+import ShapeGroup from "./group.vue";
+import TempShapeGroup from "./draw-group.vue";
+import { ShapeType, components } from "../components";
 import { useStage } from "../hook/use-global-vars.ts";
-import {
-	useViewerInvertTransformConfig,
-	useViewerTransformConfig
-} from "../hook/use-viewer.ts";
+import { useViewerTransformConfig } from "../hook/use-viewer.ts";
 import { useListener, useResize } from "../hook/use-event.ts";
 import { useExpose } from "../hook/use-expose.ts";
-import { useInteractiveShapeAPI } from '../hook/use-interactive.ts'
-import { data } from '../store'
+import { useInteractiveShapeAPI } from "../hook/use-interactive.ts";
+import { DomMountId } from "../../constant";
+import ActiveBoxs from "../helper/active-boxs.vue";
+import SnapLines from "../helper/snap-lines.vue";
 
 const stage = useStage();
-const size = useResize()
-const viewerConfig = useViewerTransformConfig()
-const invertViewerConfig = useViewerInvertTransformConfig()
+const size = useResize();
+const viewerConfig = useViewerTransformConfig();
+const types = Object.keys(components) as ShapeType[];
 
-const types = Object.keys(components) as ShapeType[]
+// 退出添加模式
+const { quitMouseAddShape } = useInteractiveShapeAPI();
+useListener(
+  "contextmenu",
+  (ev) => {
+    ev.preventDefault();
+    quitMouseAddShape();
+  },
+  document.documentElement
+);
 
-const addItemsHandler = (type: ShapeType, items: any) => {
-	if (!data[type]) {
-		data[type] = [];
-	}
-	console.log(JSON.stringify(items, null, 2))
-	data[type].push(...items)
-}
+defineExpose(useExpose());
+</script>
 
-const updateHandler = (type: ShapeType, playData: {value: DrawItem, ndx: number}) => {
-	data[type][playData.ndx] = playData.value
+<style scoped lang="scss">
+.draw-layout {
+  width: 100%;
+  height: 100%;
 }
-
-const {quitMouseAddShape} = useInteractiveShapeAPI()
-useListener('contextmenu', ev => {
-	ev.preventDefault();
-	quitMouseAddShape()
-}, document.documentElement)
-
-defineExpose(useExpose())
-</script>
+.mount-mask {
+  position: absolute;
+  inset: 0;
+  overflow: hidden;
+  pointer-events: none;
+  z-index: 999;
+}
+</style>

+ 68 - 200
src/core/store/index.ts

@@ -1,209 +1,77 @@
-import { reactive } from "vue";
+import { defineStore } from "pinia";
+import { DrawData, DrawItem, ShapeType } from '../components'
+import { initData } from "./init";
 
-export const data = reactive({
-	"image": [
-		// {
-		// 	"url": "/src/assets/WX20241031-111850.png",
-		// 	"width": 1,
-		// 	"height": 1,
-		// 	"x": 594.5,
-		// 	"y": 436
-		// }
-	],
-	"rectangle": [
-		{
-			"attitude": [1, 0, 0, 1, 0, 0],
-			"points": [
-				{
-					"x": 944.984375,
-					"y": 138.04296875
-				},
-				{
-					"x": 1120.96484375,
-					"y": 138.04296875
-				},
-				{
-					"x": 1120.96484375,
-					"y": 332.5234375
-				},
-				{
-					"x": 944.984375,
-					"y": 332.5234375
-				}
-			]
-		}
-	],
-	"triangle": [
-		{
-			"attitude": [1, 0, 0, 1, 0, 0],
-			"points": [
-				{
-					"x": 115.58984375,
-					"y": 626.625
-				},
-				{
-					"x": 229.76953125,
-					"y": 495.828125
-				},
-				{
-					"x": 343.94921875,
-					"y": 626.625
-				}
-			]
-		}
-	],
-	"polygon": [
-		{
-			"attitude": [1, 0, 0, 1, 0, 0],
-			"points": [
-				{
-					"x": 416.5390625,
-					"y": 354.78125
-				},
-				{
-					"x": 715.4765625,
-					"y": 370.1796875
-				},
-				{
-					"x": 686.734375,
-					"y": 538.16796875
-				},
-				{
-					"x": 400.56640625,
-					"y": 531.5390625
-				}
-			]
-		}
-	],
-	"line": [
-		{
-			attitude: [1, 0, 0, 1, 0, 0],
-			"points": [
-				{
-					"x": 327.5546875,
-					"y": 71.54296875
-				},
-				{
-					"x": 623.5703125,
-					"y": 74.07421875
-				},
-				{
-					"x": 602.90234375,
-					"y": 228.5
-				},
-				{
-					"x": 311.03515625,
-					"y": 263.71484375
-				}
-			]
+const sortFn = (a: Pick<DrawItem, 'zIndex' | 'createTime'>, b: Pick<DrawItem, 'zIndex' | 'createTime'>) => 
+	a.zIndex - b.zIndex || a.createTime - b.createTime
+
+export const useStore = defineStore('draw-data', {
+	state: (): DrawData => (initData),
+	getters: {
+		items() {
+			return Object.values((this as any).$state).flat() as DrawItem[]
+		},
+		sortItems() {
+			return (this.items as any).sort(sortFn) as DrawItem[]
 		}
-	],
-	"arrow": [
-		{
-			attitude: [1, 0, 0, 1, 0, 0],
-			"points": [
-				{
-					"x": 962.1484375,
-					"y": 129.10546875
-				},
-				{
-					"x": 744.4921875,
-					"y": 244.49609375
-				}
-			]
+	},
+	actions: {
+		setStore(store: DrawData) {
+			this.$patch(store);
+		},
+		getItemNdx<T extends ShapeType>(type: T, id: string) {
+			const items = this.$state[type]
+			if (items) {
+				return items.findIndex(item => item.id === id)
+			}
+			return -1
 		},
-		{
-			attitude: [1, 0, 0, 1, 0, 0],
-			"points": [
-				{
-					"x": 756.35546875,
-					"y": 94.78125
-				},
-				{
-					"x": 879.94140625,
-					"y": 265.87109375
+		getItem<T extends ShapeType>(type: T, id: string) {
+			const ndx = this.getItemNdx(type, id)
+			if (~ndx) return this.$state[type]![ndx]
+		},
+		addItems<T extends ShapeType>(type: T, items: DrawItem<T>[]) {
+			this.$patch((state: DrawData) => {
+				if (!(type in state)) {
+					state[type] = []
 				}
-			]
-		}
-	],
-	"circle": [
-		{
-			"x": 1372.22265625,
-			"y": 100.3671875,
-			"radius": 84.72265625
-		}
-	],
-	"icon": [
-		{
-			"url": "/example/fuse//assets/icons/vue.svg",
-			"mat": [
-				1,
-				0,
-				0,
-				1,
-				791.984375,
-				464.45703125
-			]
+				state[type]!.push(...items);
+			})
 		},
-		{
-			"url": "/example/fuse//assets/icons/vue.svg",
-			"mat": [
-				1,
-				0,
-				0,
-				1,
-				1072.265625,
-				467.41796875
-			]
+		addItem<T extends ShapeType>(type: T, item: DrawItem<T>) {
+			this.addItems(type, [item])
 		},
-		{
-			"url": "/example/fuse/assets/icons/vue.svg",
-			"mat": [
-				1,
-				0,
-				0,
-				1,
-				1281.1015625,
-				448.33984375
-			]
+		delItem<T extends ShapeType>(type: T, id: string) {
+			const ndx = this.getItemNdx(type, id)
+			if (~ndx) {
+				this.$patch(state => {
+					state[type]!.splice(ndx, 1)
+				})
+			}
 		},
-		{
-			"url": "/example/fuse/assets/icons/BedsideCupboard.svg",
-			"width": 300,
-			"height": 300,
-			"stroke": "red",
-			"strokeWidth": 1,
-			"strokeScaleEnabled": false,
-			"mat": [
-				1,
-				0,
-				0,
-				1,
-				954.53515625,
-				519.8671875
-			]
-		}
-	],
-	"text": [
-		{
-			"fill": "#000",
-			"stroke": "red",
-			"strokeWidth": 3,
-			"fontFamily": "Calibri",
-			"fontSize": 30,
-			"width": 100,
-			"content": "Hello from the Konva framework. Try to resize me.",
-			"mat": [1, 0, 0, 1, 484.13671875, 663.13671875],
+		setItem<T extends ShapeType>(type: T, playData: { value: DrawItem<T>, id: string }) {
+			const ndx = this.getItemNdx(type, playData.id)
+			console.log(JSON.stringify(playData.value))
+			if (~ndx) {
+				this.$patch(state => {
+					Object.assign(state[type]![ndx], playData.value)
+				})
+			}
 		},
-		{
-			"fill": "#000",
-			"stroke": "red",
-			"strokeWidth": 3,
-			"fontFamily": "Calibri",
-			"fontSize": 30,
-			"width": 300,
-			"content": "文字",
-			"mat": [1, 0, 0, 1, 873.94921875, 659.453125],
+		getItemsZIndex(items?: DrawItem[]) {
+			if (!items) {
+				return this.sortItems;
+			} else {
+				return items.sort(sortFn)
+			}
+		},
+		getType(id: string) {
+			const types = Object.keys(this.$state) as ShapeType[]
+			for (const type of types) {
+				if (this.$state[type]?.some(item=> item.id === id)) {
+					return type
+				}
+			}
 		}
-	],
-})
+	}
+})

+ 212 - 0
src/core/store/init.ts

@@ -0,0 +1,212 @@
+export const initData = {
+  // "image": [
+  // 	// {
+  // 	// 	"url": "/src/assets/WX20241031-111850.png",
+  // 	// 	"width": 1,
+  // 	// 	"height": 1,
+  // 	// 	"x": 594.5,
+  // 	// 	"y": 436
+  // 	// }
+  // ],
+  rectangle: [
+    {"id":"0","createTime":1,"zIndex":0,"attitude":[0.49671531254033047,0.6435180082478934,-0.5194746707265486,0.4009693903193004,793.3046144557984,-304.8623779525318],"points":[{"x":1192.8999748096812,"y":353.7459910496289},{"x":1280.3121683458312,"y":466.9925917901596},{"x":1179.2844908791794,"y":544.9733067738582},{"x":1091.8722973430301,"y":431.7267060333284}]},
+    {
+      id: "1",
+      createTime: 0,
+      zIndex: 0,
+      attitude: [1, 0, 0, 1, 0, 0],
+      fill: "red",
+      points: [
+        {
+          x: 758.00390625,
+          y: 138.04296875,
+        },
+        {
+          x: 933.984375,
+          y: 138.04296875,
+        },
+        {
+          x: 933.984375,
+          y: 332.5234375,
+        },
+        {
+          x: 758.00390625,
+          y: 332.5234375,
+        },
+      ],
+    },
+  ],
+  // "triangle": [
+  // 	{
+  // 		"attitude": [1, 0, 0, 1, 0, 0],
+  // 		"points": [
+  // 			{
+  // 				"x": 115.58984375,
+  // 				"y": 626.625
+  // 			},
+  // 			{
+  // 				"x": 229.76953125,
+  // 				"y": 495.828125
+  // 			},
+  // 			{
+  // 				"x": 343.94921875,
+  // 				"y": 626.625
+  // 			}
+  // 		]
+  // 	}
+  // ],
+  // "polygon": [
+  // 	{
+  // 		"attitude": [1, 0, 0, 1, 0, 0],
+  // 		"points": [
+  // 			{
+  // 				"x": 416.5390625,
+  // 				"y": 354.78125
+  // 			},
+  // 			{
+  // 				"x": 715.4765625,
+  // 				"y": 370.1796875
+  // 			},
+  // 			{
+  // 				"x": 686.734375,
+  // 				"y": 538.16796875
+  // 			},
+  // 			{
+  // 				"x": 400.56640625,
+  // 				"y": 531.5390625
+  // 			}
+  // 		]
+  // 	}
+  // ],
+  // "line": [
+  // 	{
+  // 		attitude: [1, 0, 0, 1, 0, 0],
+  // 		"points": [
+  // 			{
+  // 				"x": 327.5546875,
+  // 				"y": 71.54296875
+  // 			},
+  // 			{
+  // 				"x": 623.5703125,
+  // 				"y": 74.07421875
+  // 			},
+  // 			{
+  // 				"x": 602.90234375,
+  // 				"y": 228.5
+  // 			},
+  // 			{
+  // 				"x": 311.03515625,
+  // 				"y": 263.71484375
+  // 			}
+  // 		]
+  // 	}
+  // ],
+  // "arrow": [
+  // 	{
+  // 		attitude: [1, 0, 0, 1, 0, 0],
+  // 		"points": [
+  // 			{
+  // 				"x": 962.1484375,
+  // 				"y": 129.10546875
+  // 			},
+  // 			{
+  // 				"x": 744.4921875,
+  // 				"y": 244.49609375
+  // 			}
+  // 		]
+  // 	},
+  // 	{
+  // 		attitude: [1, 0, 0, 1, 0, 0],
+  // 		"points": [
+  // 			{
+  // 				"x": 756.35546875,
+  // 				"y": 94.78125
+  // 			},
+  // 			{
+  // 				"x": 879.94140625,
+  // 				"y": 265.87109375
+  // 			}
+  // 		]
+  // 	}
+  // ],
+  // "circle": [
+  // 	{
+  // 		"x": 1372.22265625,
+  // 		"y": 100.3671875,
+  // 		"radius": 84.72265625
+  // 	}
+  // ],
+  // "icon": [
+  // 	{
+  // 		"url": "/example/fuse//assets/icons/vue.svg",
+  // 		"mat": [
+  // 			1,
+  // 			0,
+  // 			0,
+  // 			1,
+  // 			791.984375,
+  // 			464.45703125
+  // 		]
+  // 	},
+  // 	{
+  // 		"url": "/example/fuse//assets/icons/vue.svg",
+  // 		"mat": [
+  // 			1,
+  // 			0,
+  // 			0,
+  // 			1,
+  // 			1072.265625,
+  // 			467.41796875
+  // 		]
+  // 	},
+  // 	{
+  // 		"url": "/example/fuse/assets/icons/vue.svg",
+  // 		"mat": [
+  // 			1,
+  // 			0,
+  // 			0,
+  // 			1,
+  // 			1281.1015625,
+  // 			448.33984375
+  // 		]
+  // 	},
+  // 	{
+  // 		"url": "/example/fuse/assets/icons/BedsideCupboard.svg",
+  // 		"width": 300,
+  // 		"height": 300,
+  // 		"stroke": "red",
+  // 		"strokeWidth": 1,
+  // 		"strokeScaleEnabled": false,
+  // 		"mat": [
+  // 			1,
+  // 			0,
+  // 			0,
+  // 			1,
+  // 			954.53515625,
+  // 			519.8671875
+  // 		]
+  // 	}
+  // ],
+  // "text": [
+  // 	{
+  // 		"fill": "#000",
+  // 		"stroke": "red",
+  // 		"strokeWidth": 3,
+  // 		"fontFamily": "Calibri",
+  // 		"fontSize": 30,
+  // 		"width": 100,
+  // 		"content": "Hello from the Konva framework. Try to resize me.",
+  // 		"mat": [1, 0, 0, 1, 484.13671875, 663.13671875],
+  // 	},
+  // 	{
+  // 		"fill": "#000",
+  // 		"stroke": "red",
+  // 		"strokeWidth": 3,
+  // 		"fontFamily": "Calibri",
+  // 		"fontSize": 30,
+  // 		"width": 300,
+  // 		"content": "文字",
+  // 		"mat": [1, 0, 0, 1, 873.94921875, 659.453125],
+  // 	}
+  // ],
+};

src/helper/deconstruction.d.ts → src/deconstruction.d.ts


+ 0 - 1
src/utils/event.ts

@@ -49,7 +49,6 @@ export const dragListener = (dom: HTMLElement, props: DragProps | DragProps['mov
 
 	let moveHandler: any, endHandler: any
 	const downHandler = (ev: PointerEvent) => {
-		console.log(ev.target)
 		const start = getOffset(ev, dom)
 		let prev = start
 		down && down(start, ev)

+ 210 - 163
src/utils/math.ts

@@ -8,10 +8,10 @@ export const vector = (pos: Pos) => new Vector2(pos.x, pos.y);
 export const lVector = (line: Pos[]) => line.map(vector);
 
 export const vsBound = (positions: Pos[]) => {
-	const box = new Box2()
-	box.setFromPoints(positions.map(vector))
-	return box
-}
+  const box = new Box2();
+  box.setFromPoints(positions.map(vector));
+  return box;
+};
 
 /**
  * 获取线段方向
@@ -19,7 +19,7 @@ export const vsBound = (positions: Pos[]) => {
  * @returns 方向
  */
 export const lineVector = (line: Pos[]) =>
-	vector(line[1]).sub(vector(line[0])).normalize();
+  vector(line[1]).sub(vector(line[0])).normalize();
 
 const epsilon = 1e-6; // 误差范围
 
@@ -30,7 +30,7 @@ const epsilon = 1e-6; // 误差范围
  * @returns 是否相等
  */
 export const eqPoint = (p1: Pos, p2: Pos) =>
-	vector(p1).distanceTo(p2) < epsilon;
+  vector(p1).distanceTo(p2) < epsilon;
 
 /**
  * 获取两点距离
@@ -48,7 +48,7 @@ export const vectorLen = (dire: Pos) => vector(dire).length();
  * @returns 垂直向量
  */
 export const verticalVector = (dire: Pos) =>
-	vector({ x: -dire.y, y: dire.x }).normalize();
+  vector({ x: -dire.y, y: dire.x }).normalize();
 
 /**
  * 获取旋转指定度数后的向量
@@ -57,7 +57,7 @@ export const verticalVector = (dire: Pos) =>
  * @returns 旋转后向量
  */
 export const rotateVector = (pos: Pos, angleRad: number) =>
-	new Transform().rotate(angleRad).point(pos);
+  new Transform().rotate(angleRad).point(pos);
 
 /**
  * 创建线段
@@ -68,9 +68,9 @@ export const rotateVector = (pos: Pos, angleRad: number) =>
  */
 
 export const getVectorLine = (
-	dire: Pos,
-	start: Pos = { x: 0, y: 0 },
-	dis: number = 1
+  dire: Pos,
+  start: Pos = { x: 0, y: 0 },
+  dis: number = 1
 ) => [start, vector(dire).multiplyScalar(dis).add(start)];
 
 /**
@@ -79,7 +79,7 @@ export const getVectorLine = (
  * @returns 垂直向量
  */
 export const lineVerticalVector = (line: Pos[]) =>
-	verticalVector(lineVector(line));
+  verticalVector(lineVector(line));
 
 /**
  * 获取向量的垂直线段
@@ -88,9 +88,9 @@ export const lineVerticalVector = (line: Pos[]) =>
  * @param len 线段长度
  */
 export const verticalVectorLine = (
-	dire: Pos,
-	start: Pos = { x: 0, y: 0 },
-	len: number = 1
+  dire: Pos,
+  start: Pos = { x: 0, y: 0 },
+  len: number = 1
 ) => getVectorLine(verticalVector(dire), start, len);
 
 /**
@@ -100,10 +100,10 @@ export const verticalVectorLine = (
  * @returns 两向量夹角弧度, 逆时针为正,顺时针为负
  */
 export const vector2IncludedAngle = (v1: Pos, v2: Pos) => {
-	const start = vector(v1);
-	const end = vector(v2);
-	const angle = start.angleTo(end);
-	return start.cross(end) > 0 ? angle : -angle;
+  const start = vector(v1);
+  const end = vector(v2);
+  const angle = start.angleTo(end);
+  return start.cross(end) > 0 ? angle : -angle;
 };
 
 /**
@@ -113,7 +113,7 @@ export const vector2IncludedAngle = (v1: Pos, v2: Pos) => {
  * @returns 两线段夹角弧度
  */
 export const line2IncludedAngle = (line1: Pos[], line2: Pos[]) =>
-	vector2IncludedAngle(lineVector(line1), lineVector(line2));
+  vector2IncludedAngle(lineVector(line1), lineVector(line2));
 
 /**
  * 获取线段与方向的夹角弧度
@@ -122,7 +122,7 @@ export const line2IncludedAngle = (line1: Pos[], line2: Pos[]) =>
  * @returns 线段与方向夹角弧度
  */
 export const lineAndVectorIncludedAngle = (line: Pos[], v: Pos) =>
-	vector2IncludedAngle(lineVector(line), v);
+  vector2IncludedAngle(lineVector(line), v);
 
 /**
  * 获取线段中心点
@@ -130,97 +130,96 @@ export const lineAndVectorIncludedAngle = (line: Pos[], v: Pos) =>
  * @returns
  */
 export const lineCenter = (line: Pos[]) =>
-	vector(line[0]).add(line[1]).multiplyScalar(0.5);
-
+  vector(line[0]).add(line[1]).multiplyScalar(0.5);
 
 export const pointsCenter = (points: Pos[]) => {
-	if (points.length === 0) return {x: 0, y: 0}
-	const v = vector(points[0])
-	for (let i = 1; i < points.length; i++) {
-		v.add(points[i])
-	}
-	return v.multiplyScalar(1/points.length)
-}
+  if (points.length === 0) return { x: 0, y: 0 };
+  const v = vector(points[0]);
+  for (let i = 1; i < points.length; i++) {
+    v.add(points[i]);
+  }
+  return v.multiplyScalar(1 / points.length);
+};
 
 export const lineJoin = (l1: Pos[], l2: Pos[]) => {
-	const checks = [
-		[l1[0], l2[0]],
-		[l1[0], l2[1]],
-		[l1[1], l2[0]],
-		[l1[1], l2[1]],
-	];
-	const ndx = checks.findIndex((line) => eqPoint(line[0], line[1]));
-	if (~ndx) {
-		return checks[ndx];
-	} else {
-		return false;
-	}
+  const checks = [
+    [l1[0], l2[0]],
+    [l1[0], l2[1]],
+    [l1[1], l2[0]],
+    [l1[1], l2[1]],
+  ];
+  const ndx = checks.findIndex((line) => eqPoint(line[0], line[1]));
+  if (~ndx) {
+    return checks[ndx];
+  } else {
+    return false;
+  }
 };
 
 export const isLineEqual = (l1: Pos[], l2: Pos[]) =>
-	eqPoint(l1[0], l2[0]) && eqPoint(l1[1], l2[1]);
+  eqPoint(l1[0], l2[0]) && eqPoint(l1[1], l2[1]);
 
 export const isLineReverseEqual = (l1: Pos[], l2: Pos[]) =>
-	eqPoint(l1[0], l2[1]) && eqPoint(l1[1], l2[0]);
+  eqPoint(l1[0], l2[1]) && eqPoint(l1[1], l2[0]);
 
 export const isLineIntersect = (l1: Pos[], l2: Pos[]) => {
-	const s1 = l2[1].y - l2[0].y;
-	const s2 = l2[1].x - l2[0].x;
-	const s3 = l1[1].x - l1[0].x;
-	const s4 = l1[1].y - l1[0].y;
-	const s5 = l1[0].y - l2[0].y;
-	const s6 = l1[0].x - l2[0].x;
-
-	const denominator = s1 * s3 - s2 * s4;
-	const ua = round((s2 * s5 - s1 * s6) / denominator, 6);
-	const ub = round((s3 * s5 - s4 * s6) / denominator, 6);
-
-	if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
-		return true;
-	} else {
-		return false;
-	}
+  const s1 = l2[1].y - l2[0].y;
+  const s2 = l2[1].x - l2[0].x;
+  const s3 = l1[1].x - l1[0].x;
+  const s4 = l1[1].y - l1[0].y;
+  const s5 = l1[0].y - l2[0].y;
+  const s6 = l1[0].x - l2[0].x;
+
+  const denominator = s1 * s3 - s2 * s4;
+  const ua = round((s2 * s5 - s1 * s6) / denominator, 6);
+  const ub = round((s3 * s5 - s4 * s6) / denominator, 6);
+
+  if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
+    return true;
+  } else {
+    return false;
+  }
 };
 
 export const lineParallelRelationship = (l1: Pos[], l2: Pos[]) => {
-	const dire1 = lineVector(l1);
-	const dire2 = lineVector(l2);
-
-	// 计算线段的法向量
-	const normal1 = verticalVector(dire1);
-	const normal2 = verticalVector(dire2);
-	const startDire = lineVector([l1[0], l2[0]]);
-
-	// 计算线段的参数方程
-	const t1 = round(normal2.dot(startDire) / normal2.dot(dire1), 6);
-	const t2 = round(normal1.dot(startDire) / normal1.dot(dire2), 6);
-
-	if (t1 === 0 && t2 === 0) {
-		return RelationshipEnum.Overlap;
-	}
-
-	if (eqPoint(normal1, normal2) || eqPoint(normal1, normal2.clone().negate())) {
-		return lineJoin(l1, l2)
-			? RelationshipEnum.Overlap
-			: RelationshipEnum.Parallel;
-	}
+  const dire1 = lineVector(l1);
+  const dire2 = lineVector(l2);
+
+  // 计算线段的法向量
+  const normal1 = verticalVector(dire1);
+  const normal2 = verticalVector(dire2);
+  const startDire = lineVector([l1[0], l2[0]]);
+
+  // 计算线段的参数方程
+  const t1 = round(normal2.dot(startDire) / normal2.dot(dire1), 6);
+  const t2 = round(normal1.dot(startDire) / normal1.dot(dire2), 6);
+
+  if (t1 === 0 && t2 === 0) {
+    return RelationshipEnum.Overlap;
+  }
+
+  if (eqPoint(normal1, normal2) || eqPoint(normal1, normal2.clone().negate())) {
+    return lineJoin(l1, l2)
+      ? RelationshipEnum.Overlap
+      : RelationshipEnum.Parallel;
+  }
 };
 
 export enum RelationshipEnum {
-	// 重叠
-	Overlap = "Overlap",
-	// 相交
-	Intersect = "Intersect",
-	// 延长相交
-	ExtendIntersect = "ExtendIntersect",
-	// 平行
-	Parallel = "Parallel",
-	// 首尾连接
-	Join = "Join",
-	// 一样
-	Equal = "Equal",
-	// 反向
-	ReverseEqual = "ReverseEqual",
+  // 重叠
+  Overlap = "Overlap",
+  // 相交
+  Intersect = "Intersect",
+  // 延长相交
+  ExtendIntersect = "ExtendIntersect",
+  // 平行
+  Parallel = "Parallel",
+  // 首尾连接
+  Join = "Join",
+  // 一样
+  Equal = "Equal",
+  // 反向
+  ReverseEqual = "ReverseEqual",
 }
 /**
  * 获取两线段是什么关系,(重叠、相交、平行、首尾相接等)
@@ -229,22 +228,22 @@ export enum RelationshipEnum {
  * @returns RelationshipEnum
  */
 export const lineRelationship = (l1: Pos[], l2: Pos[]) => {
-	if (isLineEqual(l1, l2)) {
-		return RelationshipEnum.Equal;
-	} else if (isLineReverseEqual(l1, l2)) {
-		return RelationshipEnum.ReverseEqual;
-	}
-
-	const parallelRelationship = lineParallelRelationship(l1, l2);
-	if (parallelRelationship) {
-		return parallelRelationship;
-	} else if (lineJoin(l1, l2)) {
-		return RelationshipEnum.Join;
-	} else if (isLineIntersect(l1, l2)) {
-		return RelationshipEnum.Intersect; // 两线段相交
-	} else {
-		return RelationshipEnum.ExtendIntersect; // 延长可相交
-	}
+  if (isLineEqual(l1, l2)) {
+    return RelationshipEnum.Equal;
+  } else if (isLineReverseEqual(l1, l2)) {
+    return RelationshipEnum.ReverseEqual;
+  }
+
+  const parallelRelationship = lineParallelRelationship(l1, l2);
+  if (parallelRelationship) {
+    return parallelRelationship;
+  } else if (lineJoin(l1, l2)) {
+    return RelationshipEnum.Join;
+  } else if (isLineIntersect(l1, l2)) {
+    return RelationshipEnum.Intersect; // 两线段相交
+  } else {
+    return RelationshipEnum.ExtendIntersect; // 延长可相交
+  }
 };
 
 /**
@@ -254,27 +253,27 @@ export const lineRelationship = (l1: Pos[], l2: Pos[]) => {
  * @returns 交点坐标
  */
 export const lineIntersection = (l1: Pos[], l2: Pos[]) => {
-	// 定义两条线段的起点和终点坐标
-	const [line1Start, line1End] = lVector(l1);
-	const [line2Start, line2End] = lVector(l2);
-
-	// 计算线段的方向向量
-	const dir1 = line1End.clone().sub(line1Start);
-	const dir2 = line2End.clone().sub(line2Start);
-
-	// 计算参数方程中的系数
-	const a = dir1.x;
-	const b = -dir2.x;
-	const c = dir1.y;
-	const d = -dir2.y;
-
-	const e = line2Start.x - line1Start.x;
-	const f = line2Start.y - line1Start.y;
-
-	// 求解参数t和s
-	const t = (d * e - b * f) / (a * d - b * c);
-	// 计算交点坐标
-	return line1Start.clone().add(dir1.clone().multiplyScalar(t)).toArray();
+  // 定义两条线段的起点和终点坐标
+  const [line1Start, line1End] = lVector(l1);
+  const [line2Start, line2End] = lVector(l2);
+
+  // 计算线段的方向向量
+  const dir1 = line1End.clone().sub(line1Start);
+  const dir2 = line2End.clone().sub(line2Start);
+
+  // 计算参数方程中的系数
+  const a = dir1.x;
+  const b = -dir2.x;
+  const c = dir1.y;
+  const d = -dir2.y;
+
+  const e = line2Start.x - line1Start.x;
+  const f = line2Start.y - line1Start.y;
+
+  // 求解参数t和s
+  const t = (d * e - b * f) / (a * d - b * c);
+  // 计算交点坐标
+  return line1Start.clone().add(dir1.clone().multiplyScalar(t)).toArray();
 };
 
 /**
@@ -284,19 +283,19 @@ export const lineIntersection = (l1: Pos[], l2: Pos[]) => {
  * @returns 投影信息
  */
 export const linePointProjection = (line: Pos[], position: Pos) => {
-	// 定义线段的起点和终点坐标
-	const [lineStart, lineEnd] = lVector(line);
-	// 定义一个点的坐标
-	const point = vector(position);
-
-	// 计算线段的方向向量
-	const lineDir = lineEnd.clone().sub(lineStart);
-	// 计算点到线段起点的向量
-	const pointToLineStart = point.clone().sub(lineStart);
-	// 计算点在线段方向上的投影长度
-	const t = pointToLineStart.dot(lineDir.normalize());
-	// 计算投影点的坐标
-	return lineStart.add(lineDir.multiplyScalar(t));
+  // 定义线段的起点和终点坐标
+  const [lineStart, lineEnd] = lVector(line);
+  // 定义一个点的坐标
+  const point = vector(position);
+
+  // 计算线段的方向向量
+  const lineDir = lineEnd.clone().sub(lineStart);
+  // 计算点到线段起点的向量
+  const pointToLineStart = point.clone().sub(lineStart);
+  // 计算点在线段方向上的投影长度
+  const t = pointToLineStart.dot(lineDir.normalize());
+  // 计算投影点的坐标
+  return lineStart.add(lineDir.multiplyScalar(t));
 };
 
 /**
@@ -306,7 +305,7 @@ export const linePointProjection = (line: Pos[], position: Pos) => {
  * @returns 距离
  */
 export const linePointLen = (line: Pos[], position: Pos) =>
-	lineLen(position, linePointProjection(line, position));
+  lineLen(position, linePointProjection(line, position));
 
 /**
  * 计算多边形是否为逆时针
@@ -314,7 +313,7 @@ export const linePointLen = (line: Pos[], position: Pos) =>
  * @returns true | false
  */
 export const isPolygonCounterclockwise = (points: Pos[]) =>
-	ShapeUtils.isClockWise(points.map(vector));
+  ShapeUtils.isClockWise(points.map(vector));
 
 /**
  * 切割线段,返回连段切割点
@@ -324,13 +323,13 @@ export const isPolygonCounterclockwise = (points: Pos[]) =>
  * @returns 点数组
  */
 export const lineSlice = (
-	line: Pos[],
-	amount: number,
-	unit = lineLen(line[0], line[1]) / amount
+  line: Pos[],
+  amount: number,
+  unit = lineLen(line[0], line[1]) / amount
 ) =>
-	new Array(unit)
-		.fill(0)
-		.map((_, i) => linePointProjection(line, { x: i * unit, y: i * unit }));
+  new Array(unit)
+    .fill(0)
+    .map((_, i) => linePointProjection(line, { x: i * unit, y: i * unit }));
 
 /**
  * 线段是否相交多边形
@@ -339,12 +338,12 @@ export const lineSlice = (
  * @returns
  */
 export const isPolygonLineIntersect = (polygon: Pos[], line: Pos[]) => {
-	for (let i = 0; i < polygon.length; i++) {
-		if (isLineIntersect([polygon[i], polygon[i + 1]], line)) {
-			return true;
-		}
-	}
-	return false;
+  for (let i = 0; i < polygon.length; i++) {
+    if (isLineIntersect([polygon[i], polygon[i + 1]], line)) {
+      return true;
+    }
+  }
+  return false;
 };
 
 /**
@@ -354,9 +353,57 @@ export const isPolygonLineIntersect = (polygon: Pos[], line: Pos[]) => {
  * @param rad
  */
 export const joinPoint = (p1: Pos, p2: Pos, rad: number) => {
-	const lvector = new Vector2()
-		.subVectors(p1, p2)
-		.rotateAround({ x: 0, y: 0 }, rad);
+  const lvector = new Vector2()
+    .subVectors(p1, p2)
+    .rotateAround({ x: 0, y: 0 }, rad);
 
-	return vector(p2).add(lvector);
+  return vector(p2).add(lvector);
+};
+
+/**
+ * 计算经过旋转的点位,要缩放多少才能到达目标
+ * @param origin 缩放原点
+ * @param current 当前点位
+ * @param target 目标点位 x轴 或者y轴
+ * @param rotateTransfom 当前点位经过了的旋转
+ * @param type 缩放类型  x | y | xy
+ * @returns
+ */
+type CalcScaleFactorProps = {
+  origin: Pos;
+  current: Pos;
+  target: Partial<Pos>;
+  rotateTransfom?: Transform;
+  type?: "x" | "y" | "xy";
+};
+export const calcScaleFactor = ({
+  origin,
+  current,
+  target,
+  rotateTransfom,
+  type = "xy",
+}: CalcScaleFactorProps): Partial<Pos> | null => {
+  // 将原点和目标点转换到未转转的坐标系
+  let norTarget = { x: 0, y: 0, ...target }
+  if (rotateTransfom) {
+    const invRotate = rotateTransfom.copy().invert();
+    current = invRotate.point(current);
+    norTarget = invRotate.point(norTarget);
+  }
+
+  const oc = vector(origin).sub(current);
+  const ot = vector(origin).sub(norTarget);
+  if (type === "x") {
+    if (oc.x === 0) return null;
+    return { x: ot.x / oc.x };
+  } else if (type === "y") {
+    if (oc.y === 0) return null;
+    return { y: ot.y / oc.y };
+  } else {
+    if (oc.x === 0 || oc.y === 0) return null;
+    return {
+      x: ot.x / oc.x,
+      y: ot.y / oc.y,
+    };
+  }
 };

+ 1 - 1
src/utils/shape.ts

@@ -1,5 +1,5 @@
 import { Transform } from "konva/lib/Util";
-import { DC, EntityShape } from "../helper/deconstruction";
+import { DC, EntityShape } from "../deconstruction";
 
 
 export const shapeTreeEq = (

+ 15 - 1
src/utils/shared.ts

@@ -160,4 +160,18 @@ export const flatToPositions = (coords: number[]) => {
 	return positions
 }
 
-export const onlyId = () => uuid()
+export const onlyId = () => uuid()
+
+export const startAnimation = (update: () => void) => {
+	let isStop = false
+	const animation = () => {
+		requestAnimationFrame(() => {
+			if (!isStop) {
+				update()
+				animation()
+			}
+		})
+	}
+	animation()
+	return () => isStop = true
+}