Selaa lähdekoodia

feat: 制作对齐功能

bill 6 kuukautta sitten
vanhempi
commit
abf0fbdcd4

+ 4 - 0
src/constant/mode.ts

@@ -1,6 +1,10 @@
 export enum Mode {
+	// 不响应鼠标行为
+	static = 'static',
 	// 只读不做任何响应
 	readonly = 'readonly',
+	// 写模式
+	write = 'write',
 	// 绘制模式
 	draw = 'draw',
 	// 阅读模式

+ 7 - 2
src/core/components/arrow/arrow.vue

@@ -16,6 +16,7 @@ import { ArrowData, getMouseStyle, defaultStyle } from "./index.ts";
 import TempArrow from "./temp-arrow.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
 import { Line } from "konva/lib/shapes/Line";
+import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: ArrowData }>();
 const emit = defineEmits<{
@@ -32,6 +33,10 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus<
   props,
   getMouseStyle,
   defaultStyle,
+  alignment(data, mat) {
+    data.points = data.points.map((p) => mat.point(p));
+    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+  },
   transformType: "line",
   getRepShape(): Line {
     return new Line({
@@ -51,9 +56,9 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus<
     "strokeWidth",
     "pointerLength",
     "dash",
-    "ref",
+    // "ref",
     "opacity",
-    "zIndex",
+    // "zIndex",
   ],
 });
 </script>

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

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

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

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

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

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

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

@@ -34,6 +34,13 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
   getMouseStyle,
   defaultStyle,
   transformType: "custom",
+  alignment(data, mat) {
+    const tf = shape.value!.getNode().getTransform();
+    const { scaleX, x, y } = mat.multiply(tf).decompose();
+    data.x = x;
+    data.y = y;
+    data.radius = data.radius * scaleX;
+  },
   customTransform(callback, shape, data) {
     let initRadius = data.value.radius;
     const update = (mat: Transform, data: CircleData) => {
@@ -87,6 +94,14 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
       y: data.y + decTf.y,
     };
   },
-  propertys: ["fill", "stroke", "strokeWidth", "dash", "ref", "opacity", "zIndex"],
+  propertys: [
+    "fill",
+    "stroke",
+    "strokeWidth",
+    "dash",
+    // "ref",
+    "opacity",
+    //  "zIndex"
+  ],
 });
 </script>

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

@@ -29,6 +29,9 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   getMouseStyle,
   transformType: "mat",
   defaultStyle,
+  alignment(data, mat) {
+    data.mat = mat.multiply(new Transform(props.data.mat)).m;
+  },
   copyHandler(tf, data) {
     data.mat = tf.multiply(new Transform(data.mat)).m;
     return data;
@@ -37,17 +40,17 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
     "fill",
     "stroke",
     "strokeWidth",
-    "strokeScaleEnabled",
     "dash",
     "opacity",
+    "strokeScaleEnabled",
 
-    "coverFill",
+    // "coverFill",
     // "coverStroke",
     // "coverStrokeWidth",
-    "coverOpcatiy",
+    // "coverOpcatiy",
 
-    "ref",
-    "zIndex",
+    // "ref",
+    // "zIndex",
   ],
 });
 </script>

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

@@ -29,10 +29,17 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   getMouseStyle,
   transformType: "mat",
   defaultStyle,
+  alignment(data, mat) {
+    data.mat = mat.multiply(new Transform(props.data.mat)).m;
+  },
   copyHandler(tf, data) {
     data.mat = tf.multiply(new Transform(data.mat)).m;
     return data;
   },
-  propertys: ["stroke", "strokeWidth", "opacity", "ref", "zIndex"],
+  propertys: [
+    // "stroke", "strokeWidth",
+    "opacity",
+    //  "ref", "zIndex"
+  ],
 });
 </script>

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

@@ -7,6 +7,7 @@ import * as line from './line'
 import * as text from './text'
 import * as icon from './icon'
 import * as image from './image'
+import * as bgImage from './bg-image'
 
 import { ArrowData } from './arrow'
 import { RectangleData } from './rectangle'
@@ -17,6 +18,7 @@ import { LineData } from './line'
 import { TextData } from './text'
 import { IconData } from './icon'
 import { ImageData } from './image'
+import { BGImageData } from './bg-image'
 import { Pos } from '@/utils/math'
 
 const _components = {
@@ -28,7 +30,8 @@ const _components = {
 	line,
 	text,
 	icon,
-	image
+	image,
+	bgImage
 }
 
 export const components = _components as Components
@@ -50,6 +53,7 @@ export type DrawDataItem = {
 	text: TextData,
 	icon: IconData,
 	image: ImageData,
+	bgImage: BGImageData
 }
 export type ShapeType = keyof DrawDataItem
 

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

@@ -24,6 +24,7 @@ import { EditPen } from "@element-plus/icons-vue";
 import { useInteractiveDrawShapeAPI } from "@/core/hook/use-draw.ts";
 import { useStore } from "@/core/store/index.ts";
 import { Pos } from "@/utils/math.ts";
+import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: LineData }>();
 const emit = defineEmits<{
@@ -37,13 +38,23 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   props,
   getMouseStyle,
   transformType: "line",
+  alignment(data, mat) {
+    data.points = data.points.map((p) => mat.point(p));
+    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+  },
   // type: "line",
   defaultStyle,
   copyHandler(tf, data) {
     data.points = data.points.map((v) => tf.point(v));
     return data;
   },
-  propertys: ["stroke", "strokeWidth", "dash", "opacity", "ref", "zIndex"],
+  propertys: [
+    "stroke",
+    "strokeWidth",
+    "dash",
+    "opacity",
+    //  "ref", "zIndex"
+  ],
 });
 
 const updatePosition = ({ ndx, val }: { ndx: number; val: Pos }) => {

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

@@ -24,6 +24,7 @@ import { useInteractiveDrawShapeAPI } from "@/core/hook/use-draw.ts";
 import { useStore } from "@/core/store/index.ts";
 import { EditPen } from "@element-plus/icons-vue";
 import { Pos } from "@/utils/math.ts";
+import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: PolygonData }>();
 const emit = defineEmits<{
@@ -38,11 +39,22 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   getMouseStyle,
   transformType: "line",
   defaultStyle,
+  alignment(data, mat) {
+    data.points = data.points.map((p) => mat.point(p));
+    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+  },
   copyHandler(tf, data) {
     data.points = data.points.map((v) => tf.point(v));
     return data;
   },
-  propertys: ["fill", "stroke", "strokeWidth", "dash", "opacity", "ref", "zIndex"],
+  propertys: [
+    "fill",
+    "stroke",
+    "strokeWidth",
+    "dash",
+    "opacity",
+    //  "ref", "zIndex"
+  ],
 });
 
 const updatePosition = ({ ndx, val }: { ndx: number; val: Pos }) => {

+ 13 - 1
src/core/components/rectangle/rectangle.vue

@@ -14,6 +14,7 @@ import { RectangleData, getMouseStyle, defaultStyle } from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempLine from "./temp-rectangle.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
+import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: RectangleData }>();
 const emit = defineEmits<{
@@ -28,10 +29,21 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   getMouseStyle,
   defaultStyle,
   transformType: "line",
+  alignment(data, mat) {
+    data.points = data.points.map((p) => mat.point(p));
+    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+  },
   copyHandler(tf, data) {
     data.points = data.points.map((v) => tf.point(v));
     return data;
   },
-  propertys: ["fill", "stroke", "strokeWidth", "dash", "opacity", "ref", "zIndex"],
+  propertys: [
+    "fill",
+    "stroke",
+    "strokeWidth",
+    "dash",
+    "opacity",
+    // "ref", "zIndex"
+  ],
 });
 </script>

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

@@ -97,13 +97,13 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
     "fill",
     "stroke",
     "strokeWidth",
-    "align",
-    "fontSize",
-    "fontStyle",
     "dash",
     "opacity",
-    "ref",
-    "zIndex",
+    "fontSize",
+    "align",
+    "fontStyle",
+    // "ref",
+    // "zIndex",
   ],
 });
 

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

@@ -14,6 +14,7 @@ import { TriangleData, getMouseStyle, defaultStyle } from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempLine from "./temp-triangle.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
+import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: TriangleData }>();
 const emit = defineEmits<{
@@ -28,10 +29,21 @@ const { shape, tData, operateMenus, describes, data } = useComponentStatus({
   getMouseStyle,
   defaultStyle,
   transformType: "line",
+  alignment(data, mat) {
+    data.points = data.points.map((p) => mat.point(p));
+    data.attitude = mat.multiply(new Transform(data.attitude)).m;
+  },
   copyHandler(tf, data) {
     data.points = data.points.map((v) => tf.point(v));
     return data;
   },
-  propertys: ["fill", "stroke", "strokeWidth", "dash", "opacity", "ref", "zIndex"],
+  propertys: [
+    "fill",
+    "stroke",
+    "strokeWidth",
+    "dash",
+    "opacity",
+    // "ref", "zIndex"
+  ],
 });
 </script>

+ 8 - 7
src/core/helper/active-boxs.vue

@@ -20,7 +20,6 @@ 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";
 import { useGetComponentData } from "../hook/use-component";
@@ -42,25 +41,27 @@ const updateBox = ($shape: EntityShape) => {
 type OnCleanup = (cleanupFn: () => void) => void;
 
 const getComponentData = useGetComponentData();
-const transformer = useTransformer();
 const shapeListener = (shape: EntityShape, onCleanup: OnCleanup) => {
   const update = async () => {
     await nextTick();
     updateBox(shape);
   };
 
-  watchEffect((onCleanup) => {
-    transformer.on("transform.helper-box", update);
-    onCleanup(() => transformer.off("transform.helper-box"));
-  });
+  const repShape = shape.repShape || shape;
+  repShape.on("transform", update);
   shape.on("bound-change", update);
+
   watch(() => getComponentData(shape).value, update);
   update();
-  onCleanup(() => shape.off("bound-change", update));
+  onCleanup(() => {
+    repShape.off("transform", update);
+    shape.off("bound-change", update);
+  });
 };
 
 watchEffect(
   (onCleanup) => {
+    console.error(status.actives)
     status.actives.forEach((shape) => shapeListener(shape, onCleanup));
   },
   { flush: "pre" }

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

@@ -51,7 +51,7 @@ useShapeStaticZindex(grid);
 const style = {
   stroke: "#ccc",
   strokeWidth: 3,
-  opacity: 0.7,
+  opacity: 0.4,
   strokeScaleEnabled: false,
 };
 

+ 240 - 0
src/core/hook/use-alignment.ts

@@ -0,0 +1,240 @@
+import { DC, EntityShape } from "@/deconstruction";
+import { ElMessage, MessageHandler } from "element-plus";
+import { nextTick, onUnmounted, ref, Ref, watchEffect } from "vue";
+import {
+  useHelperLayer,
+  useMouseMigrateTempLayer,
+  useTempLayer,
+} from "./use-layer";
+import { clickListener, listener } from "@/utils/event";
+import { useCursor, useDownKeys, useMode, useStage } from "./use-global-vars";
+import { mergeFuns } from "@/utils/shared";
+import { getLineRelationMat, Pos } from "@/utils/math";
+import { Circle } from "konva/lib/shapes/Circle";
+import { themeMouseColors } from "@/constant/help-style";
+import { Line } from "konva/lib/shapes/Line";
+import { Shape } from "konva/lib/Shape";
+import { shapeTreeContain } from "@/utils/shape";
+import { useViewerInvertTransform, useViewerTransform } from "./use-viewer";
+import { Mode } from "@/constant/mode";
+import { useMouseShapesStatus, useMouseStyle } from "./use-mouse-status";
+
+export const useJoinShapes = (
+  shapes: (Ref<DC<EntityShape> | undefined> | undefined)[]
+) => {
+  if (shapes.length < 2) throw "连线至少需要两个对象!";
+  const stage = useStage();
+  const tempLayer = useTempLayer();
+  const getAttri = (shape?: Ref<DC<EntityShape> | undefined>) => {
+    let sticky: null | Ref<boolean> = null;
+    if (shape) {
+      sticky = ref(false);
+      useMouseMigrateTempLayer(shape, sticky);
+    }
+    return {
+      sticky,
+      canAdd: () => {
+        const pos = stage.value?.getNode().pointerPos;
+        if (!pos) return false;
+        if (!shape) return true;
+        const $shape = shape.value?.getNode();
+        if (!$shape) return false;
+        const interShape = tempLayer.value!.getIntersection(pos);
+        return interShape && shapeTreeContain($shape, interShape) === $shape;
+      },
+    };
+  };
+
+  const invertTransform = useViewerInvertTransform();
+  const viewTransform = useViewerTransform();
+  const attribs = shapes.map(getAttri);
+  const quitHooks: (() => void)[] = [];
+  const helperLayer = useHelperLayer();
+  const drawLines = (points: Ref<(Pos | null)[]>) => {
+    const size = 10;
+    const shapes: Shape[] = [];
+    const stopWatchs: Array<() => void> = [];
+
+    for (let i = 1; i < points.value.length; i++) {
+      const line = new Line({
+        dash: [5, 10],
+        stroke: themeMouseColors.pub,
+        strokeWidth: size / 4,
+        visible: false,
+      });
+      helperLayer.value?.add(line);
+      shapes.push(line);
+      stopWatchs.push(
+        watchEffect(() => {
+          let cur = points.value[i];
+          let pre = points.value[i - 1];
+          const isShow = !!(pre && cur);
+          line.visible(isShow);
+          if (isShow) {
+            pre = viewTransform.value.point(pre!);
+            cur = viewTransform.value.point(cur!);
+            line.points([pre.x, pre.y, cur.x, cur.y]);
+          }
+        })
+      );
+    }
+
+    const circleConfig = {
+      radius: size / 2,
+      fill: themeMouseColors.disable,
+      stroke: themeMouseColors.press,
+      strokeWidth: size / 4,
+      visible: false,
+    };
+    for (let i = 0; i < points.value.length; i++) {
+      const circle = new Circle(circleConfig);
+      helperLayer.value?.add(circle);
+      shapes.push(circle);
+      stopWatchs.push(
+        watchEffect(() => {
+          let p = points.value[i];
+          circle.visible(!!p);
+          if (p) {
+            p = viewTransform.value.point(p);
+            circle.x(p.x);
+            circle.y(p.y);
+          }
+        })
+      );
+    }
+
+    quitHooks.push(() => {
+      mergeFuns(stopWatchs)();
+      shapes.forEach((shape) => shape.destroy());
+    });
+    return points;
+  };
+
+  const mode = useMode()
+  const cursor = useCursor();
+  const keys = useDownKeys();
+  const status = useMouseShapesStatus()
+  const select = async (sure: (ndx: number) => void) => {
+    const $stage = stage.value!.getNode();
+    const dom = $stage.container();
+    const points = drawLines(ref<(Pos | null)[]>(attribs.map(() => null)));
+
+    cursor.push("crosshair");
+    mode.push(Mode.readonly)
+    
+    const cleanup = () => {
+      cursor.pop()
+      mode.pop()
+    }
+
+    for (let i = 0; i < attribs.length; i++) {
+      const attrib = attribs[i];
+      attrib.sticky && (attrib.sticky.value = true);
+      const stopMove = listener(dom, "mousemove", () => {
+        if (attrib.canAdd() && !keys.has(" ")) {
+          points.value[i] = invertTransform.value.point($stage.pointerPos!);
+        }
+      });
+      const $shape = shapes[i]?.value?.getNode()
+      await nextTick()
+      $shape && status.actives.push($shape)
+      try {
+        await new Promise<void>((resolve, reject) => {
+          let isSure = false;
+          const stopClick = clickListener(dom, () => {
+            if (!points.value[i] || keys.has(" ")) return;
+            stopMove();
+            stopClick();
+            sure(i);
+            isSure = true;
+            resolve();
+          });
+          quitHooks.push(() => {
+            if (!isSure) {
+              stopMove();
+              stopClick();
+              reject();
+            }
+          });
+        });
+      } catch (e) {
+        cleanup()
+        throw e;
+      } finally {
+        attrib.sticky && (attrib.sticky.value = false);
+        if ($shape) {
+          const ndx = status.actives.indexOf($shape)
+          status.actives.splice(ndx, 1)
+        }
+      }
+    }
+    cleanup();
+    return points.value as Pos[];
+  };
+
+  const quit = () => {
+    mergeFuns(quitHooks)();
+    quitHooks.length = 0;
+  };
+  onUnmounted(quit);
+  return [select, quit] as const;
+};
+
+export const useAlignmentShape = (shape: Ref<DC<EntityShape> | undefined>) => {
+  const quitHooks: (() => void)[] = [];
+  const quit = () => mergeFuns(quitHooks)();
+  onUnmounted(quit);
+
+  const joins = new Array(2)
+    .fill(0)
+    .map(() => useJoinShapes([shape, undefined]));
+  quitHooks.push(...joins.map(([_, clear]) => clear));
+
+  let msgHandler: MessageHandler | null = null;
+  const info = (msg: string) => {
+    msgClocse();
+    msgHandler = ElMessage.info({ message: msg, duration: 0 });
+  };
+  const msgClocse = () => {
+    msgHandler?.close();
+    msgHandler = null;
+  };
+  quitHooks.push(msgClocse);
+
+  const keydownHandler = (ev: KeyboardEvent) => {
+    if (ev.key === "Escape") {
+      quit();
+    }
+  };
+
+  const alignment = async () => {
+    const $shape = shape.value?.getNode()!;
+    const initOpacity = $shape.opacity();
+    const alignMaps: Pos[][] = [];
+
+    window.addEventListener("keydown", keydownHandler);
+    quitHooks.push(() => window.removeEventListener("keydown", keydownHandler));
+
+    for (const [join] of joins) {
+      info("请在当前图例选择位置(按esc退出对齐)");
+      try {
+        const map = await join((ndx) => {
+          ndx === 0 && info("请在画板中与图例对应的位置(按esc退出对齐)");
+          $shape.opacity(0.3);
+        });
+        alignMaps.push(map);
+      } finally {
+        $shape.opacity(initOpacity);
+      }
+    }
+
+    const mat = getLineRelationMat(
+      [alignMaps[0][0], alignMaps[1][0]],
+      [alignMaps[0][1], alignMaps[1][1]]
+    );
+    quit();
+    return mat;
+  };
+
+  return [alignment, quit] as const;
+};

+ 40 - 8
src/core/hook/use-component.ts

@@ -16,13 +16,14 @@ import { useAnimationMouseStyle } from "./use-mouse-status";
 import { components, DrawItem, ShapeType } from "../components";
 import { useMatCompTransformer, useLineTransformer } from "./use-transformer";
 import { useGetShapeCopyTransform } from "./use-copy";
-import { Delete, DocumentCopy } from "@element-plus/icons-vue";
+import { Delete, DocumentCopy, Location } from "@element-plus/icons-vue";
 import { mergeFuns, onlyId } from "@/utils/shared";
 import { Shape } from "konva/lib/Shape";
 import { Transform } from "konva/lib/Util";
 import { mergeDescribes, PropertyKeys } from "../propertys";
 import { useStore } from "../store";
 import { globalWatch } from "./use-global-vars";
+import { useAlignmentShape } from "./use-alignment";
 
 type Emit<T> = EmitFn<{
   updateShape: (value: T) => void;
@@ -37,6 +38,8 @@ export type UseComponentStatusProps<
   emit: Emit<T>;
   type?: ShapeType;
   props: { data: T };
+  disMouseStatus?: boolean
+  alignment?: (data: T, mat: Transform) => void
   getMouseStyle: any;
   defaultStyle: any;
   propertys: PropertyKeys;
@@ -58,20 +61,27 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
     props,
     getMouseStyle,
     transformType,
+    alignment,
     defaultStyle,
     customTransform,
     getRepShape,
+    disMouseStatus,
     propertys,
     copyHandler,
   } = args;
 
   const shape = ref<DC<S>>();
   const data = useAutomaticData(() => props.data);
-  const [style] = useAnimationMouseStyle({
-    data: data,
-    shape,
-    getMouseStyle,
-  }) as any;
+  let style: any
+  if (!disMouseStatus) {
+    [style] = useAnimationMouseStyle({
+      data: data,
+      shape,
+      getMouseStyle,
+    }) as any;
+  } else {
+    console.log(props.data)
+  }
 
   if (transformType === "line") {
     useLineTransformer(
@@ -89,9 +99,10 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
   }
 
   useZIndex(shape, data);
-  useMouseMigrateTempLayer(shape);
+  // useMouseMigrateTempLayer(shape);
   const name = args.type ? components[args.type].shapeName : "";
   const getCopyTransform = useGetShapeCopyTransform(shape);
+  const [alignmentShape] = useAlignmentShape(shape);
   const operateMenus = shallowReactive([
     {
       label: `删除${name}`,
@@ -113,14 +124,33 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
         emit("addShape", copyData);
       },
     },
+    
   ]);
 
+  if (alignment) {
+    operateMenus.push({
+      label: "对齐",
+      async handler() {
+        const mat = await alignmentShape();
+        alignment(data.value, mat)
+        emit('updateShape', { ...data.value })
+      },
+      icon: Location,
+    })
+  }
+
   const describes = mergeDescribes(data, defaultStyle, propertys || []);
 
   return {
     data,
     style,
-    tData: computed(() => ({ ...defaultStyle, ...data.value, ...style.value })),
+    tData: computed(() => { 
+      const tData = {...defaultStyle, ...data.value}
+      if (style) {
+        Object.assign(tData, style.value )
+      }
+      return tData
+    }),
     shape,
     operateMenus,
     describes,
@@ -138,6 +168,8 @@ export const useGetComponentData = <D extends DrawItem>() => {
     });
 };
 
+
+
 export const useComponentsAttach = <T>(
   getter: <K extends ShapeType>(type: K, data: DrawItem<K>) => T,
   types = Object.keys(components) as ShapeType[]

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

@@ -107,11 +107,11 @@ export const globalWatch = <T>(
 };
 
 export const useStage = installGlobalVar(
-  () => shallowRef<DC<Stage> | undefined>(),
+  () => shallowRef<DC<Stage> | undefined>(),               
   Symbol("stage")
 );
 export const useMode = installGlobalVar(() => {
-  const stack = stackVar(new Set([Mode.viewer]))
+  const stack = stackVar(new Set([Mode.write]))
   const modeStack = {
     ...stack,
     get value() {
@@ -147,11 +147,11 @@ export const useCan = installGlobalVar(() => {
   const loaded = computed(() => !!stage.value?.getStage())
 
   // 鼠标是否可用
-  const mouse = computed(() => loaded.value && !mode.include(Mode.readonly))
+  const mouse = computed(() => loaded.value && !mode.include(Mode.static))
 
   // 可以进入拖拽模式
   const dragMode = computed(() => {
-    if (!mouse.value || mode.include(Mode.viewer) || key.has(' ')) return false;
+    if (!mouse.value || mode.include(Mode.readonly) || key.has(' ')) return false;
     return mode.include(Mode.draw) || mode.include(Mode.update)
   })
 
@@ -161,13 +161,13 @@ export const useCan = installGlobalVar(() => {
   })
 
   // shape是否可以对鼠标做出反应
-  const mouseReact = computed(() => mouse.value && (mode.include(Mode.viewer) || mode.include(Mode.update)))
+  const mouseReact = computed(() => mouse.value && (mode.include(Mode.write) || mode.include(Mode.update)))
 
   // 可以进入编辑模式
-  const editMode = computed(() => mouse.value && mode.include(Mode.viewer))
+  const editMode = computed(() => mouse.value && mode.include(Mode.write))
 
   // 可以进入绘制模式
-  const drawMode = computed(() => mouse.value && mode.include(Mode.viewer))
+  const drawMode = computed(() => mouse.value && mode.include(Mode.write))
 
 
   return reactive({

+ 8 - 4
src/core/hook/use-layer.ts

@@ -81,14 +81,18 @@ export const useMigrateLayer = (shape: Ref<DC<EntityShape> | undefined>) => {
 };
 
 export const useMouseMigrateTempLayer = (
-  shape: Ref<DC<EntityShape> | undefined>
+  shape: Ref<DC<EntityShape> | undefined>,
+  isMigrate?: Ref<boolean>
 ) => {
-  const status = useMouseShapeStatus(shape);
   const tempLayer = useTempLayer();
   const [migrate, recovery] = useMigrateLayer(shape);
 
-  const isMigrate = computed(() => status.value.active || status.value.hover);
-  // 鼠标状态改变则迁移图层
+  if (!isMigrate) {
+    // 鼠标状态改变则迁移图层
+    const status = useMouseShapeStatus(shape);
+    isMigrate = computed(() => status.value.active || status.value.hover);
+  }
+  
   watch(
     isMigrate,
     (isMigrate, _, onCleanup) => {

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

@@ -1,4 +1,4 @@
-import { computed, reactive, ref, Ref, watch, watchEffect } from "vue";
+import { computed, reactive, ref, Ref, toRaw, watch, watchEffect } from "vue";
 import { DC, EntityShape } from "../../deconstruction";
 import { Shape } from "konva/lib/Shape";
 import {
@@ -20,15 +20,19 @@ import {
 } from "./use-transformer.ts";
 import { useAniamtion } from "./use-animation.ts";
 import { KonvaEventObject } from "konva/lib/Node";
+import { useFormalLayer } from "./use-layer.ts";
+import { Layer } from "konva/lib/Layer";
 
-export const getHoverShape = (stage: Stage) => {
+export const getHoverShape = (stage: Stage, layer: Layer) => {
   const hover = ref<EntityShape>();
+
   const enterHandler = (ev: KonvaEventObject<any, Stage>) => {
-    leaveHandler();
     const target = ev.target;
     hover.value = target;
-    target.on("pointerleave", leaveHandler);
+    target.off("pointerleave", leaveHandler);
+    target.on("pointerleave", leaveHandler as any);
   };
+
   const leaveHandler = () => {
     if (hover.value) {
       hover.value.off("pointerleave", leaveHandler);
@@ -82,6 +86,7 @@ export const useShapeIsTransformerInner = () => {
 export const useMouseShapesStatus = installGlobalVar(() => {
   const can = useCan();
   const stage = useStage();
+  const formatLayer = useFormalLayer()
   const listeners = ref([]) as Ref<EntityShape[]>;
   const hovers = ref([]) as Ref<EntityShape[]>;
   const press = ref([]) as Ref<EntityShape[]>;
@@ -95,12 +100,13 @@ export const useMouseShapesStatus = installGlobalVar(() => {
     let downTarget: EntityShape | null;
 
     const prevent = computed(() => keys.has(" "));
-    const [hover, hoverDestory] = getHoverShape(stage);
+    const [hover, hoverDestory] = getHoverShape(stage, formatLayer.value!);
     const hoverChange = (onCleanup: any) => {
       if (prevent.value) {
         return;
       }
 
+      
       const pHover =
         hover.value && shapeTreeContain(listeners.value, hover.value);
       // TODO首先确定之前的有没有离开

+ 91 - 87
src/core/hook/use-transformer.ts

@@ -248,7 +248,7 @@ const emptyFn = () => {};
 export const useShapeTransformer = <T extends EntityShape>(
   shape: Ref<DC<T> | undefined>,
   transformerConfig: TransformerConfig = {},
-  replaceShape?: (transformer: TransformerExtends, shape: T) => Rep<T>,
+  replaceShape?: (shape: T) => Rep<T>,
   handlerTransform?: (transform: Transform) => Transform
 ) => {
   const offset = useShapeDrag(shape);
@@ -260,9 +260,60 @@ export const useShapeTransformer = <T extends EntityShape>(
   const viewTransform = useViewerTransform();
   const can = useCan();
 
-  const init = ($shape: T) =>
-    watch(
-      () => status.value.hover,
+  const init = ($shape: T) => {
+    let rep: Rep<T>;
+    if (replaceShape) {
+      rep = replaceShape($shape);
+    } else {
+      rep = {
+        tempShape: $shape,
+        destory: emptyFn,
+        update: emptyFn,
+      };
+    }
+    
+    const updateTransform = () => {
+      if (!can.dragMode) return;
+      let appleTransform = rep.tempShape.getTransform().copy();
+      if (handlerTransform) {
+        appleTransform = handlerTransform(appleTransform);
+        setShapeTransform(rep.tempShape, appleTransform);
+      }
+      transform.value = appleTransform;
+    };
+
+    rep.tempShape.on("transform.shapemer", updateTransform);
+
+    const boundHandler = () => rep.update && rep.update();
+    $shape.on("bound-change", boundHandler);
+
+    // 拖拽时要更新矩阵
+    let prevMoveTf: Transform | null = null;
+    const stopDragWatch = watch(
+      offset,
+      (translate, oldTranslate) => {
+        if (translate) {
+          if (!oldTranslate) {
+            rep.update && rep.update();
+          }
+          const moveTf = new Transform().translate(translate.x, translate.y);
+          const finalTf = moveTf.copy();
+          prevMoveTf && finalTf.multiply(prevMoveTf.invert());
+          finalTf.multiply(rep.tempShape.getTransform());
+          prevMoveTf = moveTf;
+
+          setShapeTransform(rep.tempShape, finalTf);
+          rep.tempShape.fire("transform");
+        } else {
+          prevMoveTf = null;
+          transform.value = void 0;
+        }
+      },
+      { immediate: true }
+    );
+
+    const stopTransformerWatch = watch(
+      () => status.value.active,
       (active, _, onCleanup) => {
         const parent = $shape.parent;
         if (!(active && parent)) return;
@@ -273,30 +324,11 @@ export const useShapeTransformer = <T extends EntityShape>(
           (transformer as any)[key](transformerConfig[key]);
         }
 
-        let rep: Rep<T>;
-        if (replaceShape) {
-          rep = replaceShape(transformer, $shape);
-        } else {
-          rep = {
-            tempShape: $shape,
-            destory: emptyFn,
-            update: emptyFn,
-          };
-          transformer.nodes([$shape]);
-          transformer.queueShapes.value = [$shape];
-        }
+        transformer.nodes([rep.tempShape]);
+        transformer.queueShapes.value = [$shape];
         parent.add(transformer);
 
-        const updateTransform = () => {
-          if (!can.dragMode) return;
-          let appleTransform = rep.tempShape.getTransform().copy();
-          if (handlerTransform) {
-            appleTransform = handlerTransform(appleTransform);
-            setShapeTransform(rep.tempShape, appleTransform);
-          }
-          transform.value = appleTransform;
-        };
-
+        let isEnter = false;
         const downHandler = () => {
           if (isEnter) {
             mode.pop();
@@ -309,11 +341,8 @@ export const useShapeTransformer = <T extends EntityShape>(
           mode.add(Mode.draging);
           transformIngShapes.value.push($shape);
         };
-
-        let isEnter = false;
         transformer.on("pointerdown.shapemer", downHandler);
-        transformer.on("transform.shapemer", updateTransform);
-        const stop = listener(
+        const stopPointupListener = listener(
           $shape.getStage()!.container(),
           "pointerup",
           () => {
@@ -327,78 +356,52 @@ export const useShapeTransformer = <T extends EntityShape>(
           }
         );
 
-        // 拖拽时要更新矩阵
-        let prevMoveTf: Transform | null = null;
-        const stopDragWatch = watch(
-          offset,
-          (translate, oldTranslate) => {
-            if (translate) {
-              if (!oldTranslate) {
-                rep.update && rep.update();
-              }
-              const moveTf = new Transform().translate(
-                translate.x,
-                translate.y
-              );
-              const finalTf = moveTf.copy();
-              prevMoveTf && finalTf.multiply(prevMoveTf.invert());
-              finalTf.multiply(rep.tempShape.getTransform());
-              prevMoveTf = moveTf;
-
-              setShapeTransform(rep.tempShape, finalTf);
-              transformer.fire("transform");
-              // updateTransform()
-            } else {
-              prevMoveTf = null;
-              transform.value = void 0;
-            }
-          },
-          { immediate: true }
-        );
-
         const stopTransformerForceUpdate = watch(
           viewTransform,
           () => transformer.forceUpdate(),
           { flush: "post" }
         );
 
-        const boundHandler = () => rep.update && rep.update()
-        $shape.on('bound-change', boundHandler)
-
         onCleanup(() => {
-          $shape.off('bound-change', boundHandler)
           for (const key in oldConfig) {
             (transformer as any)[key](oldConfig[key]);
           }
           stopTransformerForceUpdate();
-          stop();
-          stopDragWatch();
-          // parent.add($shape);
+          stopPointupListener();
           // 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 }
+      }
     );
+
+    return () => {
+      $shape.off("bound-change", boundHandler);
+      rep.tempShape.off("transform.shapemer", updateTransform);
+      stopDragWatch();
+      stopTransformerWatch();
+      rep.destory();
+    };
+  };
+
   watch(
     () => shape.value,
     (shape, _) => {
       if (!shape) return;
       watch(
-        () => can.editMode || mode.include(Mode.update),
+        () =>
+          (can.editMode || mode.include(Mode.update)) &&
+          (status.value.active || status.value.hover),
         (canEdit, _, onCleanup) => {
           if (canEdit) {
             const stop = init(shape.getStage());
@@ -407,7 +410,7 @@ export const useShapeTransformer = <T extends EntityShape>(
             onCleanup(() => {});
           }
         },
-        {immediate: true}
+        { immediate: true }
       );
     }
   );
@@ -417,7 +420,10 @@ export const useShapeTransformer = <T extends EntityShape>(
 export const cloneRepShape = <T extends EntityShape>(
   shape: T
 ): ReturnType<GetRepShape<T, any>> => {
-  shape = (shape as Group)?.findOne && (shape as Group)?.findOne('.repShape') as T || shape
+  shape =
+    ((shape as Group)?.findOne &&
+      ((shape as Group)?.findOne(".repShape") as T)) ||
+    shape;
   return {
     shape: shape.clone({
       fill: "rgb(0, 255, 0)",
@@ -431,7 +437,6 @@ export const cloneRepShape = <T extends EntityShape>(
 };
 
 export const transformerRepShapeHandler = <T extends EntityShape>(
-  transformer: TransformerExtends,
   shape: T,
   repShape: T
 ) => {
@@ -441,12 +446,13 @@ export const transformerRepShapeHandler = <T extends EntityShape>(
   }
   shape.parent!.add(repShape);
   repShape.zIndex(shape.getZIndex());
-  transformer.nodes([repShape]);
-  transformer.queueShapes.value = [shape];
+  repShape.listening(false)
+  shape.repShape = repShape
 
   return [
     repShape,
     () => {
+      shape.repShape = undefined
       repShape.remove();
     },
   ] as const;
@@ -481,13 +487,9 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
     shape,
     transformerConfig,
     getRepShape &&
-      ((transformer: TransformerExtends, shape) => {
+      ((shape) => {
         repResult = getRepShape(shape);
-        const [_, destory] = transformerRepShapeHandler(
-          transformer,
-          shape,
-          repResult.shape
-        );
+        const [_, destory] = transformerRepShapeHandler(shape, repResult.shape);
         return {
           tempShape: repResult.shape,
           update: () => {
@@ -497,7 +499,7 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
         };
       })
   );
-  let callMat: Transform
+  let callMat: Transform;
   watch(transform, (current, oldTransform) => {
     if (current) {
       if (!handler) return;
@@ -509,14 +511,14 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
       if (needSnap && (nTransform = needSnap[0](snapData))) {
         current = nTransform.multiply(current);
       }
-      callMat = current
+      callMat = current;
       const mat = handler(data.value, current);
       if (mat) {
         if (repResult.update) {
           repResult.update(data.value, repResult.shape);
         } else if (mat !== true) {
           setShapeTransform(repResult.shape, mat);
-          callMat = mat
+          callMat = mat;
         }
         transformer.forceUpdate();
       }
@@ -571,7 +573,9 @@ export const useLineTransformer = <T extends LineTransformerData>(
         repShape = cloneRepShape($shape).shape;
       }
 
-      repShape = (repShape as any).points ? repShape : (repShape as unknown as Group).findOne<Line>('.line')!
+      repShape = (repShape as any).points
+        ? repShape
+        : (repShape as unknown as Group).findOne<Line>(".line")!;
       tempShape = repShape;
       const update = (data: T) => {
         const attitude = new Transform(data.attitude);

+ 21 - 5
src/core/propertys/hover-operate.vue

@@ -15,7 +15,7 @@
         </div> -->
 
         <ElMenu>
-          <ElMenuItem v-for="menu in menus" @click="menu.handler">
+          <ElMenuItem v-for="menu in menus" @click="clickHandler(menu.handler)">
             <el-icon><component :is="menu.icon" /></el-icon>
             <span>{{ menu.label }}</span>
           </ElMenuItem>
@@ -27,13 +27,15 @@
 
 <script lang="ts" setup>
 import { computed, nextTick, ref, watch, watchEffect } from "vue";
-import { useStage, useTransformIngShapes } from "../hook/use-global-vars.ts";
+import { useStage } from "../hook/use-global-vars.ts";
 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 { ElMenu, ElIcon, ElMenuItem } from "element-plus";
+import { useFormalLayer } from "../hook/use-layer.ts";
+import { shapeTreeContain } from "@/utils/shape.ts";
 
 const props = defineProps<{
   target: DC<EntityShape> | undefined;
@@ -43,15 +45,24 @@ const props = defineProps<{
 
 const layout = ref<HTMLDivElement>();
 const stage = useStage();
-const status = useMouseShapeStatus(computed(() => props.target));
-const transformIngShapes = useTransformIngShapes();
+// const status = useMouseShapeStatus(computed(() => props.target));
+const layer = useFormalLayer();
 const rightClick = ref(false);
 
+const hasRClick = (ev: MouseEvent) => {
+  if (ev.button !== 2) return false;
+  const shape = props.target?.getNode();
+  const pos = stage.value?.getNode().pointerPos;
+  if (!shape || !pos || !layer.value) return false;
+  let clickShape = layer.value.getIntersection(pos);
+  return !!clickShape && shapeTreeContain(shape, clickShape) === shape;
+};
+
 watchEffect((onCleanup) => {
   const dom = stage.value?.getStage().container();
   if (!dom) return;
   const clickHandler = (ev: MouseEvent) => {
-    rightClick.value = ev.button === 2 && status.value.hover;
+    rightClick.value = hasRClick(ev);
   };
 
   dom.addEventListener("contextmenu", clickHandler);
@@ -62,6 +73,11 @@ watchEffect((onCleanup) => {
   });
 });
 
+const clickHandler = (handler: () => void) => {
+  handler();
+  rightClick.value = false;
+};
+
 const hidden = computed(
   () =>
     !stage.value?.getStage() ||

+ 3 - 1
src/core/renderer/group.vue

@@ -15,7 +15,7 @@
 
 <script setup lang="ts">
 import { ShapeType, components } from "../components";
-import { computed } from "vue";
+import { computed, watchEffect } from "vue";
 import { useStore, useStoreRenderProcessors } from "../store";
 
 const props = defineProps<{ type: ShapeType }>();
@@ -24,4 +24,6 @@ const type = props.type as "arrow";
 const ShapeComponent = components[type].Component;
 const items = computed(() => store.data[type] || []);
 const { itemHasRegistor, renderer } = useStoreRenderProcessors();
+
+watchEffect(() => console.log(props.type, items.value));
 </script>

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

@@ -60,6 +60,7 @@ const init = async () => {
       }
     };
   }
+  store.setStore(props.data);
   if (props.id && history.hasLocal()) {
     try {
       await ElMessageBox.confirm("检测到有历史数据,是否要恢复?", {
@@ -73,7 +74,6 @@ const init = async () => {
       history.clearLocal();
     }
   }
-  store.setStore(props.data);
 };
 init();
 

+ 1 - 1
src/deconstruction.d.ts

@@ -5,4 +5,4 @@ type DC<T extends any> = {
 	getStage: () => T
 }
 
-type EntityShape = Konva.Shape | Konva.Stage | Konva.Layer | Konva.Group
+type EntityShape = (Konva.Shape | Konva.Stage | Konva.Layer | Konva.Group) & { repShape?: EntityShape }

+ 71 - 20
src/example/fuse/views/init.ts

@@ -10,6 +10,9 @@ const initData = {
         243.9711189430529,
       ],
     },
+  ],
+  ['bgImage']: [
+
     {
       id: "10cc6b4d-e90a-47c2-ae3f-9ab3a21190e7",
       createTime: 1736762304345,
@@ -18,7 +21,6 @@ const initData = {
       ref: false,
       width: 1000,
       height: 1000,
-      listening: false,
       url: "/五楼.png",
       mat: [1, 0, 0, 1, 591, 436],
     },
@@ -266,30 +268,78 @@ const initData = {
       createTime: 3,
       zIndex: 0,
       id: "33",
-      attitude: [1, 0, 0, 1, 0, 0],
+      attitude: [1, 0, 0, 1, -294.60546875, -13.67578125],
       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 },
+        { x: 121.93359375, y: 341.10546875 },
+        { x: 420.87109375, y: 356.50390625 },
+        { x: 392.12890625, y: 524.4921875 },
+        { x: 105.9609375, y: 517.86328125 },
+        { x: 18.832558784586354, y: 515.8450055612782 },
+        { x: 18.832558784586354, y: 335.79467688514586 },
       ],
       dash: [18.42, 11.579999999999998],
     },
+    {
+      id: "7105a114-964c-4f55-8bc4-911b37fbc768",
+      createTime: 1736845626186,
+      zIndex: 0,
+      opacity: 1,
+      ref: true,
+      points: [
+        { x: 317.55859375, y: 328.6640625 },
+        { x: 596.73828125, y: 328.6640625 },
+        { x: 596.73828125, y: 549.26171875 },
+        { x: 317.55859375, y: 549.26171875 },
+        { x: 67.8828125, y: 549.26171875 },
+        { x: 67.8828125, y: 424.828125 },
+        { x: 177.33203125, y: 305.5390625 },
+      ],
+      attitude: [1, 0, 0, 1, 2.73046875, -1.93359375],
+    },
+    {
+      id: "fbdfc09e-1f81-4772-8c3a-316d50cc7da1",
+      createTime: 1737012759015,
+      zIndex: 0,
+      opacity: 1,
+      ref: false,
+      points: [
+        { x: 596.73828125, y: 328.6640625 },
+        { x: 829.3667899990835, y: 328.6640625 },
+        { x: 829.3667899990835, y: 549.26171875 },
+        { x: 596.73828125, y: 549.26171875 },
+      ],
+      attitude: [1, 0, 0, 1, -54.85032944331101, -177.81612953327397],
+      strokeWidth: 10,
+      stroke: "#C71585",
+      fill: "#1F93FF",
+      dash: [11.16, 18.84],
+    },
   ],
   line: [
     {
       createTime: 3,
       zIndex: 0,
       id: "333",
-      attitude: [1, 0, 0, 1, 0, 0],
+      attitude: [1, 0, 0, 1, 264.050567067064, -209.36399936114208],
       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 },
+        { x: 691.9908236537299, y: -256.43159647539807 },
+        { x: 834.8596014933705, y: -256.43159647539807 },
+        { x: 834.8596014933705, y: 52.60589610179676 },
+        { x: 668.8302882438311, y: 52.60589610179676 },
+        { x: 668.8302882438311, y: -84.48785389820324 },
+        { x: 569.2892889933705, y: -84.48785389820324 },
+        { x: 569.2892889933705, y: 52.60589610179676 },
+        { x: 471.5666327433705, y: 52.60589610179676 },
+        { x: 471.5666327433705, y: -122.69879139820324 },
+        { x: 569.2892889933705, y: -122.69879139820324 },
+        { x: 569.2892889933705, y: -256.43159647539807 },
+        { x: 471.5666327433705, y: -256.43159647539807 },
+        { x: 471.5666327433705, y: -385.2632664678143 },
       ],
       ref: false,
       dash: [20.66, 9.34],
+      strokeWidth: 10,
+      stroke: "#047E22",
     },
   ],
   arrow: [
@@ -307,10 +357,10 @@ const initData = {
       id: "9",
       createTime: 1,
       zIndex: 0,
-      attitude: [1, 0, 0, 1, 0, 0],
+      attitude: [1, 0, 0, 1, 78.11328125, -42.01171875],
       points: [
-        { x: 756.35546875, y: 94.78125 },
-        { x: 879.94140625, y: 265.87109375 },
+        { x: 834.46875, y: 52.76953125 },
+        { x: 958.0546875, y: 223.859375 },
       ],
     },
   ],
@@ -399,8 +449,8 @@ const initData = {
       zIndex: 0,
       opacity: 1,
       ref: false,
-      x: 213.103145547459,
-      y: 83.86964591024139,
+      x: 213.96148297434104,
+      y: 103.48016182562202,
       radius: 162.92261163072476,
     },
   ],
@@ -437,6 +487,7 @@ const initData = {
       strokeWidth: 1,
       strokeScaleEnabled: false,
       mat: [1, 0, 0, 1, 1136.17578125, 358.5625],
+      ref: true,
     },
     {
       id: "4f8b863e-5383-42fd-90ca-f5a81596067a",
@@ -616,12 +667,12 @@ const initData = {
       fontFamily: "Calibri",
       fontSize: 30,
       content:
-        "Hello\n  from th\n\ne     fr \n am \n \new\n\nork. Tr \ny  \n   to resize me.",
+        "Hello\\n  from th\\n\\ne     fr \\n am \\n \\new\\n\\nork. Tr \\ny  \\n   to resize me.",
       mat: [
-        1.8046845338884818e-14, 0.9999999999999952, -1.000000000000013,
-        1.6450899740986086e-14, 807.4957275390657, 322.26090819029116,
+        -1.4930798945178672e-15, 1.0000000000000104, -1.0000000000000155,
+        -1.5971633030764742e-15, 1083.2463425727433, 95.00094657517106,
       ],
-      width: 60,
+      width: 71.86542465603814,
     },
     {
       createTime: 3,

+ 18 - 0
src/utils/event.ts

@@ -32,6 +32,24 @@ export const clickListener = (dom: HTMLDivElement, callback: (position: Pos, ev:
 	});
 };
 
+export const selectOnlyPosition = (dom: HTMLDivElement) => {
+	let stopListener: any, reject: any;
+	const promise = new Promise<Pos>(function(resolve, _reject) {
+		reject = _reject
+		stopListener = clickListener(dom, (pos) => {
+			stopListener()
+			resolve(pos)
+		})
+	}) as Promise<Pos> & { stop: () => void }
+
+	promise.stop = () => {
+		stopListener()
+		reject()
+	}
+
+	return promise
+}
+
 
 type DragProps = {
 	move?: (info: Record<'start' | 'prev' | 'end', Pos> & {ev: PointerEvent}) => void,

+ 47 - 2
src/utils/math.ts

@@ -326,7 +326,6 @@ export const lineIntersection = (l1: Pos[], l2: Pos[]) => {
  * @param position 点
  */
 export const lineInner = (line: Pos[], position: Pos) => {
-  
   // 定义线段的起点和终点坐标
   const [A, B] = lVector(line);
   // 定义一个点的坐标
@@ -481,8 +480,54 @@ export function calculateScaleFactor(
   } else if (yZero) {
     return xScaleFactor;
   }
-  console.log(xScaleFactor - yScaleFactor);
   if (zeroEq(xScaleFactor - yScaleFactor)) {
     return xScaleFactor;
   }
 }
+
+// 获取两线段的矩阵关系
+export const getLineRelationMat = (l1: [Pos, Pos], l2: [Pos, Pos]) => {
+  // 提取点
+  const P1 = l1[0]; // l1 的起点
+  const P1End = l1[1]; // l1 的终点
+  const P2 = l2[0]; // l2 的起点
+  const P2End = l2[1]; // l2 的终点
+
+  // 计算方向向量
+  const d1 = { x: P1End.x - P1.x, y: P1End.y - P1.y };
+  const d2 = { x: P2End.x - P2.x, y: P2End.y - P2.y };
+
+  // 计算方向向量的长度
+  const lengthD1 = Math.sqrt(d1.x ** 2 + d1.y ** 2);
+  const lengthD2 = Math.sqrt(d2.x ** 2 + d2.y ** 2);
+
+  if (lengthD1 === 0 || lengthD2 === 0) return new Transform();
+
+  // 归一化方向向量
+  const unitD1 = { x: d1.x / lengthD1, y: d1.y / lengthD1 };
+  const unitD2 = { x: d2.x / lengthD2, y: d2.y / lengthD2 };
+
+  // 计算旋转角度
+  const angle = Math.atan2(unitD2.y, unitD2.x) - Math.atan2(unitD1.y, unitD1.x);
+  // 计算旋转矩阵
+  // 计算缩放因子
+  const scale = lengthD2 / lengthD1;
+  // 计算平移向量
+  const translation = [P2.x - P1.x, P2.y - P1.y];
+
+  const mat = new Transform()
+    .translate(translation[0], translation[1])
+    .translate(P1.x, P1.y)
+    .scale(scale, scale)
+    .rotate(angle)
+    .translate(-P1.x, -P1.y);
+
+  
+  if (!eqPoint(mat.point(P1), P2)) {
+    console.error('对准不正确 旋转后P1', mat.point(P1), P2)
+  }
+  if (!eqPoint(mat.point(P1End), P2End)) {
+    console.error('对准不正确 旋转后P2', mat.point(P1End), P1End)
+  }
+  return mat
+};