Преглед изворни кода

feat: 制作层级锁定功能

bill пре 6 месеци
родитељ
комит
e67dcbfe71

+ 0 - 44
src/app.vue

@@ -1,44 +0,0 @@
-<template>
-  <Renderer :data="$props.data!" :ref="(d: any) => draw = d" />
-</template>
-
-<script lang="ts">
-import { createPinia } from "pinia";
-import { App, defineComponent, getCurrentInstance, PropType, ref } from "vue";
-import VueKonva from "vue-konva";
-import { DrawData } from "./core/components";
-import Renderer from "./core/renderer/renderer.vue";
-import { DrawExpose } from "./core/hook/use-expose";
-
-const installApps = new WeakSet<App>();
-const install = (app: App) => {
-  if (installApps.has(app)) return;
-  app.use(VueKonva);
-  app.use(createPinia());
-  installApps.add(app);
-};
-
-export default defineComponent({
-  props: {
-    data: {
-      type: Object as PropType<DrawData>,
-      default: () => ({}),
-    },
-  },
-  name: "App",
-  expose: ["draw"],
-  setup() {
-    const instance = getCurrentInstance();
-    install(instance!.appContext.app);
-
-    const draw = ref<DrawExpose>();
-
-    return {
-      draw,
-    };
-  },
-  components: {
-    Renderer,
-  },
-});
-</script>

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

@@ -22,7 +22,6 @@ const { shape, tData, operateMenus } = useComponentStatus({
   props,
   getMouseStyle,
   defaultStyle,
-  disMouseStatus: true,
   copyHandler(tf, data) {
     data.mat = tf.multiply(new Transform(data.mat)).m;
     return data;

+ 37 - 47
src/core/components/circle/circle.vue

@@ -14,9 +14,12 @@ import { CircleData, getMouseStyle, defaultStyle } from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempCircle from "./temp-circle.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
-import { cloneRepShape, useCustomTransformer } from "@/core/hook/use-transformer.ts";
 import { Transform } from "konva/lib/Util";
-import { Circle } from "konva/lib/shapes/Circle";
+import { MathUtils } from "three";
+import { cloneRepShape, useCustomTransformer } from "@/core/hook/use-transformer.ts";
+import { setShapeTransform } from "@/utils/shape.ts";
+import { Ellipse } from "konva/lib/shapes/Ellipse";
+import { Pos } from "@/utils/math.ts";
 
 const props = defineProps<{ data: CircleData }>();
 const emit = defineEmits<{
@@ -25,74 +28,61 @@ const emit = defineEmits<{
   (e: "delShape"): void;
 }>();
 
+const matToData = (data: CircleData, mat: Transform, initRadius?: Pos) => {
+  // data.mat = mat.m;
+  // return data;
+  if (!initRadius) {
+    initRadius = {
+      x: data.radiusX,
+      y: data.radiusY,
+    };
+  }
+  const dec = mat.decompose();
+  data.radiusY = dec.scaleY * initRadius.y;
+  data.radiusX = dec.scaleX * initRadius.x;
+  data.mat = new Transform()
+    .translate(dec.x, dec.y)
+    .rotate(MathUtils.degToRad(dec.rotation)).m;
+  return data;
+};
+
 const { shape, tData, data, operateMenus, describes } = useComponentStatus<
-  Circle,
+  Ellipse,
   CircleData
 >({
   emit,
   props,
   getMouseStyle,
   defaultStyle,
-  transformType: "custom",
-  alignment(data, mat) {
+  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;
+    mat.multiply(tf);
+    return matToData(data, mat);
   },
+  // transformType: "mat",
+  transformType: "custom",
   customTransform(callback, shape, data) {
-    let initRadius = data.value.radius;
-    const update = (mat: Transform, data: CircleData) => {
-      const { scaleX, x, y } = mat.decompose();
-      if (scaleX !== 1) {
-        return {
-          radius: data.radius * scaleX,
-          x,
-          y,
-        };
-      } else {
-        return { x, y };
-      }
-    };
-
+    let initRadius: Pos;
     useCustomTransformer(shape, data, {
       openSnap: true,
       getRepShape($shape) {
         const repShape = cloneRepShape($shape).shape;
-        initRadius = data.value.radius;
+        initRadius = { x: repShape.radiusX(), y: repShape.radiusY() };
         return {
           shape: repShape,
-          update(data) {
-            repShape.x(data.x).y(data.y).radius(data.radius).scale({ x: 1, y: 1 });
-          },
         };
       },
-      transformerConfig: {
-        rotateEnabled: false,
-        keepRatio: true,
-        enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"],
-      },
-      beforeHandler(data, mat) {
-        return { ...data, ...update(mat, data) };
-      },
       handler(data, mat) {
-        const setAttrib = update(mat, data);
-        Object.assign(data, setAttrib);
-        if (setAttrib.radius) {
-          return true;
-        }
+        matToData(data, mat, initRadius);
+        return true;
       },
       callback,
     });
   },
-  copyHandler(tf, data) {
-    const decTf = tf.decompose();
-    return {
-      ...data,
-      x: data.x + decTf.x,
-      y: data.y + decTf.y,
-    };
+  copyHandler(mat, data) {
+    const tf = shape.value!.getNode().getTransform();
+    mat.multiply(tf);
+    return matToData({ ...data }, mat);
   },
   propertys: [
     "fill",

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

@@ -9,13 +9,14 @@ import {
 import { getMouseColors } from "@/utils/colors.ts";
 import { AddMessage } from "@/core/hook/use-draw.ts";
 import { lineCenter, lineLen } from "@/utils/math.ts";
+import { Transform } from "konva/lib/Util";
 
 export { default as Component } from "./circle.vue";
 export { default as TempComponent } from "./temp-circle.vue";
 
 export const shapeName = "圆形";
 export const defaultStyle = {
-  dash: [1, 0],
+  dash: [30, 0],
   stroke: themeMouseColors.theme,
   strokeWidth: 1,
 };
@@ -38,11 +39,12 @@ export const getSnapInfos = (data: CircleData) => {
 };
 
 export const getSnapPoints = (data: CircleData) => {
-  const size = data.radius * 2;
-  const points = getRectSnapPoints(size, size).map((v) => ({
-    x: v.x + data.x,
-    y: v.y + data.y,
+  const dec = new Transform(data.mat).decompose()
+  const points = getRectSnapPoints(data.radiusX * 2, data.radiusY * 2).map((v) => ({
+    x: v.x + dec.x,
+    y: v.y + dec.y,
   }));
+  // const size = data.radius * 2;
   return points
 }
 
@@ -50,9 +52,9 @@ export type CircleData = Partial<typeof defaultStyle> &
   BaseItem & {
     opacity?: number,
     fill?: string
-    x: number;
-    y: number;
-    radius: number;
+    mat: number[];
+    radiusX: number
+    radiusY: number
   };
 
 export const dataToConfig = (data: CircleData): CircleConfig => ({
@@ -78,11 +80,12 @@ export const interactiveFixData = (
   info: AddMessage<'circle'>
 ) => {
   const area = info.cur!;
-  const radius = Math.max(lineLen(area[0], area[1]) / 2, 0.5)
+  const sx = Math.abs((area[1].x - area[0].x)) / 2
+  const sy = Math.abs((area[1].y - area[0].y)) / 2
   const center = lineCenter(area)
-  data.x = center.x
-  data.y = center.y
 
-  data.radius = radius
+  data.mat = new Transform().translate(center.x, center.y).m
+  data.radiusX = sx
+  data.radiusY = sy
   return data;
 };

+ 9 - 2
src/core/components/circle/temp-circle.vue

@@ -1,13 +1,14 @@
 <template>
-  <v-circle
+  <v-ellipse
     :config="{
       ...data,
+      ...matConfig,
       zIndex: undefined,
       opacity: addMode ? 0.3 : data.opacity,
     }"
     ref="shape"
   >
-  </v-circle>
+  </v-ellipse>
 </template>
 
 <script lang="ts" setup>
@@ -15,10 +16,16 @@ import { CircleData, defaultStyle } from "./index.ts";
 import { computed, ref } from "vue";
 import { DC } from "@/deconstruction.js";
 import { Circle } from "konva/lib/shapes/Circle";
+import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ data: CircleData; addMode?: boolean }>();
 const data = computed(() => ({ ...defaultStyle, ...props.data }));
 
+const matConfig = computed(() => {
+  const mat = new Transform(data.value.mat);
+  return mat.decompose();
+});
+
 const shape = ref<DC<Circle>>();
 defineExpose({
   get shape() {

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

@@ -10,7 +10,7 @@ export { default as TempComponent } from "./temp-rectangle.vue";
 
 export const shapeName = "矩形";
 export const defaultStyle = {
-  dash: [1, 0],
+  dash: [30, 0],
   strokeWidth: 1,
   stroke: themeMouseColors.theme,
 };

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

@@ -3,6 +3,7 @@
     :config="{
       ...data,
       ...matConfig,
+      wrap: 'char',
       verticalAlign: 'center',
       text: data.content,
       zIndex: undefined,

+ 42 - 20
src/core/components/text/text-dom.vue

@@ -1,10 +1,11 @@
 <template>
-  <Teleport :to="`#${DomMountId}`">
+  <Teleport :to="mount">
     <textarea
       ref="textarea"
       :style="styles"
       @input="() => text = textarea!.value"
       @blur="quit"
+      @focus="focusHandler"
       @click.stop
       >{{ text }}</textarea
     >
@@ -14,7 +15,7 @@
 <script lang="ts" setup>
 import { useStage } from "@/core/hook/use-global-vars";
 import { Text } from "konva/lib/shapes/Text";
-import { computed, onUnmounted, ref, watch, watchEffect } from "vue";
+import { computed, onUnmounted, ref, watch } from "vue";
 import { DomMountId } from "@/constant/index.ts";
 import { useViewer, useViewerTransform } from "@/core/hook/use-viewer";
 import { listener } from "@/utils/event";
@@ -31,8 +32,19 @@ const textarea = ref<HTMLTextAreaElement>();
 const stage = useStage();
 const $stage = stage.value?.getNode()!;
 const text = ref(props.shape.text());
+const mount = document.querySelector(`#${DomMountId}`) as HTMLDivElement;
 
 const quit = () => emit("submit", text.value);
+const focusHandler = (ev: any) => {
+  ev.preventDefault();
+  mount.scroll(0, 0);
+};
+
+listener(mount, "keydown", (ev) => {
+  if (ev.key === "Escape") {
+    textarea.value?.blur();
+  }
+});
 
 const refreshMat = () => {
   const dom = textarea.value!;
@@ -82,24 +94,25 @@ const viewer = useViewer();
 const refresh = () => {
   refreshSize();
   refreshMat();
-  const textRect = textarea.value!.getBoundingClientRect();
-  const contRect = $stage.container().getBoundingClientRect();
-  const textR = textRect.x + textRect.width;
-  const textB = textRect.y + textRect.height;
-  const contR = contRect.x + contRect.width;
-  const contB = contRect.y + contRect.height;
-
-  if (
-    textR > contR ||
-    textB > contB ||
-    textRect.x < contRect.x ||
-    textRect.y < contRect.y
-  ) {
-    viewer.viewer.scalePixel(
-      { x: contRect.x + contRect.width / 2, y: contRect.x + contRect.height / 2 },
-      0.8
-    );
-  }
+
+  // const textRect = textarea.value!.getBoundingClientRect();
+  // const contRect = $stage.container().getBoundingClientRect();
+  // const textR = textRect.x + textRect.width;
+  // const textB = textRect.y + textRect.height;
+  // const contR = contRect.x + contRect.width;
+  // const contB = contRect.y + contRect.height;
+
+  // if (
+  //   textR > contR ||
+  //   textB > contB ||
+  //   textRect.x < contRect.x ||
+  //   textRect.y < contRect.y
+  // ) {
+  //   viewer.viewer.scalePixel(
+  //     { x: contRect.x + contRect.width / 2, y: contRect.x + contRect.height / 2 },
+  //     0.8
+  //   );
+  // }
 };
 
 const transform = useViewerTransform();
@@ -150,6 +163,15 @@ textarea {
   resize: none;
   pointer-events: all;
   outline: none;
+  word-break: break-all;
+  transform-origin: left top;
+}
+
+div {
+  position: absolute;
+  background: rgba(0, 0, 0, 0.4);
+  left: 0;
+  top: 0;
   transform-origin: left top;
 }
 </style>

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

@@ -60,6 +60,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
           shape: repShape,
           update(data) {
             data.width && repShape.width(data.width);
+            data.fontSize && repShape.fontSize(data.fontSize);
             setShapeTransform(repShape, new Transform(data.mat));
           },
         };

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

@@ -42,7 +42,6 @@ export const useGetPointerTextNdx = () => {
       x += size.width;
       if (x > finalPos.x) {
         const diff = x - finalPos.x
-        console.log(diff, hanlfSize, shape.fontSize())
         if (diff < hanlfSize && line.text.length >= i + 1) {
           after = true;
         }

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

@@ -7,6 +7,7 @@ export type BaseItem = {
   id: string;
   createTime: number;
   zIndex: number;
+  lock: boolean,
   opacity: number
   ref: boolean
   listening?: boolean
@@ -15,6 +16,7 @@ export type BaseItem = {
 export const getBaseItem = (): BaseItem => ({
   id: onlyId(),
   createTime: Date.now(),
+  lock: false,
   zIndex: 0,
   opacity: 1,
   ref: false

+ 131 - 87
src/core/hook/use-component.ts

@@ -7,16 +7,23 @@ import {
   Ref,
   ref,
   shallowReactive,
-  watch,
   watchEffect,
 } from "vue";
 import { useAutomaticData } from "./use-automatic-data";
-import { useMouseMigrateTempLayer, useZIndex } from "./use-layer";
+import { useCurrentZIndex, useZIndex } from "./use-layer";
 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, Location } from "@element-plus/icons-vue";
+import {
+  Bottom,
+  Delete,
+  DocumentCopy,
+  Location,
+  Lock,
+  Top,
+  Unlock,
+} from "@element-plus/icons-vue";
 import { mergeFuns, onlyId } from "@/utils/shared";
 import { Shape } from "konva/lib/Shape";
 import { Transform } from "konva/lib/Util";
@@ -31,6 +38,93 @@ type Emit<T> = EmitFn<{
   delShape: () => void;
 }>;
 
+export const useComponentMenus = <T extends DrawItem>(
+  shape: Ref<DC<EntityShape> | undefined>,
+  data: Ref<T>,
+  emit: Emit<T>,
+  alignment?: (data: T, mat: Transform) => void,
+  copyHandler?: (transform: Transform, data: T) => T
+) => {
+  const operateMenus: Array<{
+    icon?: any;
+    label?: string;
+    handler: () => void;
+  }> = shallowReactive([]);
+
+  // 锁定 解锁
+  operateMenus.push(
+    reactive({
+      label: computed(() => (data.value.lock ? "解锁" : "锁定")) as any,
+      icon: computed(() => (data.value.lock ? Unlock : Lock)),
+      handler() {
+        data.value.lock = !data.value.lock;
+        emit("updateShape", { ...data.value });
+      },
+    })
+  );
+
+  // 置顶 置底
+  const currentZIndex = useCurrentZIndex();
+  operateMenus.push(
+    {
+      label: `置顶`,
+      icon: Top,
+      handler() {
+        data.value.zIndex = currentZIndex.max + 1;
+        emit("updateShape", { ...data.value });
+      },
+    },
+    {
+      label: `置底`,
+      icon: Bottom,
+      handler() {
+        data.value.zIndex = currentZIndex.min - 1;
+        emit("updateShape", { ...data.value });
+      },
+    }
+  );
+
+  if (alignment) {
+    const [alignmentShape] = useAlignmentShape(shape);
+    operateMenus.push({
+      label: "对齐",
+      async handler() {
+        const mat = await alignmentShape();
+        alignment(data.value, mat);
+        emit("updateShape", { ...data.value });
+      },
+      icon: Location,
+    });
+  }
+
+  if (copyHandler) {
+    const getCopyTransform = useGetShapeCopyTransform(shape);
+    operateMenus.push({
+      label: `复制`,
+      icon: DocumentCopy,
+      handler() {
+        const transform = getCopyTransform();
+        const copyData = copyHandler(
+          transform,
+          JSON.parse(JSON.stringify(data.value)) as T
+        );
+        copyData.id = onlyId();
+        emit("addShape", copyData);
+      },
+    });
+  }
+
+  operateMenus.push({
+    label: `删除`,
+    icon: Delete,
+    handler() {
+      emit("delShape");
+    },
+  });
+
+  return operateMenus;
+};
+
 export type UseComponentStatusProps<
   T extends DrawItem,
   S extends EntityShape
@@ -38,8 +132,7 @@ export type UseComponentStatusProps<
   emit: Emit<T>;
   type?: ShapeType;
   props: { data: T };
-  disMouseStatus?: boolean
-  alignment?: (data: T, mat: Transform) => void
+  alignment?: (data: T, mat: Transform) => void;
   getMouseStyle: any;
   defaultStyle: any;
   propertys: PropertyKeys;
@@ -56,104 +149,57 @@ export type UseComponentStatusProps<
 export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
   args: UseComponentStatusProps<T, S>
 ) => {
-  const {
-    emit,
-    props,
-    getMouseStyle,
-    transformType,
-    alignment,
-    defaultStyle,
-    customTransform,
-    getRepShape,
-    disMouseStatus,
-    propertys,
-    copyHandler,
-  } = args;
-
   const shape = ref<DC<S>>();
-  const data = useAutomaticData(() => props.data);
-  let style: any
-  if (!disMouseStatus) {
-    [style] = useAnimationMouseStyle({
-      data: data,
-      shape,
-      getMouseStyle,
-    }) as any;
-  } else {
-    console.log(props.data)
-  }
+  const data = useAutomaticData(() => args.props.data);
+  const [style] = useAnimationMouseStyle({
+    data: data,
+    shape,
+    getMouseStyle: args.getMouseStyle,
+  }) as any;
 
-  if (transformType === "line") {
+  if (args.transformType === "line") {
     useLineTransformer(
       shape as any,
       data as any,
-      (newData) => emit("updateShape", newData as T),
-      getRepShape as any
+      (newData) => args.emit("updateShape", newData as T),
+      args.getRepShape as any
     );
-  } else if (transformType === "mat") {
+  } else if (args.transformType === "mat") {
     useMatCompTransformer(shape, data as any, (nData) =>
-      emit("updateShape", nData as any)
+      args.emit("updateShape", nData as any)
+    );
+  } else if (args.transformType === "custom" && args.customTransform) {
+    args.customTransform(
+      () => args.emit("updateShape", data.value as any),
+      shape,
+      data
     );
-  } else if (transformType === "custom" && customTransform) {
-    customTransform(() => emit("updateShape", data.value as any), shape, data);
   }
-
   useZIndex(shape, data);
-  // useMouseMigrateTempLayer(shape);
-  const name = args.type ? components[args.type].shapeName : "";
-  const getCopyTransform = useGetShapeCopyTransform(shape);
-  const [alignmentShape] = useAlignmentShape(shape);
-  const operateMenus = shallowReactive([
-    {
-      label: `删除${name}`,
-      icon: Delete,
-      handler() {
-        emit("delShape");
-      },
-    },
-    {
-      label: `复制${name}`,
-      icon: DocumentCopy,
-      handler() {
-        const transform = getCopyTransform();
-        const copyData = copyHandler(
-          transform,
-          JSON.parse(JSON.stringify(data.value)) as T
-        );
-        copyData.id = onlyId();
-        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(() => { 
-      const tData = {...defaultStyle, ...data.value}
+    tData: computed(() => {
+      const tData = { ...args.defaultStyle, ...data.value };
       if (style) {
-        Object.assign(tData, style.value )
+        Object.assign(tData, style.value);
       }
-      return tData
+      return tData;
     }),
     shape,
-    operateMenus,
-    describes,
+    operateMenus: useComponentMenus(
+      shape,
+      data,
+      args.emit,
+      args.alignment,
+      args.copyHandler
+    ),
+    describes: mergeDescribes(
+      data,
+      args.defaultStyle,
+      args.propertys || []
+    )
   };
 };
 
@@ -168,8 +214,6 @@ 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[]

+ 8 - 0
src/core/hook/use-draw.ts

@@ -35,6 +35,7 @@ import DrawShape from "../renderer/draw-shape.vue";
 import { useHistory, useHistoryAttach } from "./use-history";
 import penA from "../assert/cursor/pic_pen_a.ico";
 import penR from "../assert/cursor/pic_pen_r.ico";
+import { useCurrentZIndex } from "./use-layer";
 
 type PayData<T extends ShapeType> = ComponentValue<T, "addMode"> extends "area"
   ? Area
@@ -59,6 +60,7 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
   const can = useCan();
   const interactiveProps = useInteractiveProps();
   const conversion = useConversionPosition(true);
+  const currentZIndex = useCurrentZIndex()
 
   let isEnter = false;
   const enter = () => {
@@ -90,6 +92,9 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
           Array.isArray(data) ? data.map(conversion) : conversion(data)
         ) as PayData<T>;
       }
+      if (!preset.zIndex) {
+        preset.zIndex = currentZIndex.max + 1 
+      }
       interactiveProps.value = {
         type: shapeType,
         preset,
@@ -109,6 +114,9 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
       if (!can.drawMode || mode.include(Mode.draw)) {
         throw "当前状态不允许添加";
       }
+      if (!preset.zIndex) {
+        preset.zIndex = currentZIndex.max + 1 
+      }
       interactiveProps.value = {
         type: shapeType,
         preset,

+ 170 - 51
src/core/hook/use-expose.ts

@@ -1,63 +1,182 @@
-import { useLayers, useMode, useStage } from "./use-global-vars.ts";
+import {
+  useCursor,
+  useDownKeys,
+  useInstanceProps,
+  useLayers,
+  useMode,
+  useStage,
+} from "./use-global-vars.ts";
 import { Stage } from "konva/lib/Stage";
 import { useInteractiveProps } from "./use-interactive.ts";
 import { useStore } from "../store/index.ts";
 import { useViewer } from "./use-viewer.ts";
-import { useGlobalResize } from "./use-event.ts";
+import { useGlobalResize, useListener } from "./use-event.ts";
 import { useInteractiveDrawShapeAPI } from "./use-draw.ts";
 import { useHistory } from "./use-history.ts";
-import { reactive } from "vue";
+import { reactive, watchEffect } from "vue";
+import { usePaste } from "./use-paste.ts";
+import { useMouseShapesStatus } from "./use-mouse-status.ts";
+import { Mode } from "@/constant/mode.ts";
+import { ElMessageBox } from "element-plus";
+import { mergeFuns } from "@/utils/shared.ts";
+import { themeColor } from "@/constant/help-style.ts";
+import { isSvgString } from "@/utils/resource.ts";
+import { useResourceHandler } from "./use-fetch.ts";
 
-type PickParams<K extends keyof Stage, O extends string> = Stage[K]  extends (...args: any) => any ?  Omit<Required<Parameters<Stage[K]>>[0], O> : never
+export const useAutoService = () => {
+  // 自动粘贴服务
+  const paste = usePaste();
+	const drawAPI = useInteractiveDrawShapeAPI()
+	const resourceHandler = useResourceHandler()
+  paste.push({
+    ["text/plain"]: {
+      async handler(pos, val) {
+				if (isSvgString(val)) {
+					const url = await resourceHandler(val, 'svg')
+					drawAPI.addShape('icon', { url, stroke: themeColor }, pos, true)
+				} else {
+					drawAPI.addShape('text', { content: val }, pos, true)
+				}
+      },
+      type: "string",
+    },
+    ["image"]: {
+      async handler(pos, val, type) {
+				const url = await resourceHandler(val, type)
+				if (type.includes('svg')) {
+					drawAPI.addShape('icon', { url, stroke: themeColor }, pos, true)
+				} else {
+					drawAPI.addShape('image', { url }, pos, true)
+				}
+      },
+      type: "file",
+    },
+  });
 
-export type DrawExpose = ReturnType<typeof useExpose>
+  // 自动退出添加模式
+  const { quitDrawShape } = useInteractiveDrawShapeAPI();
+  useListener(
+    "contextmenu",
+    (ev) => {
+      if (ev.button === 2) {
+        quitDrawShape();
+      }
+    },
+    document.documentElement
+  );
 
-export const useExpose = () => {
-	const mode = useMode()
-	const interactiveProps = useInteractiveProps()
-	const stage = useStage()
-	const layers = useLayers()
-	const store = useStore()
-	const history = useHistory()
-	const viewer = useViewer().viewer
-	const { updateSize } = useGlobalResize()
-	const config = reactive({
-		showGrid: true,
-		showLabelLine: true
-	})
-
-	const exposeBlob = (config?: PickParams<'toBlob', 'callback'>) => {
-		const $stage = stage.value!.getStage()
-		return new Promise<Blob>(resolve => {
-			$stage.toBlob({ ...config, resolve } as any)
-		})
-	}
+  // 鼠标自动变化服务
+  const status = useMouseShapesStatus();
+  const downKeys = useDownKeys();
+  const mode = useMode();
+  const cursor = useCursor();
+
+  watchEffect((onCleanup) => {
+    let style: string | null = null;
+    if (downKeys.has(" ")) {
+      style = "pointer";
+    } else if (mode.include(Mode.update)) {
+      style = "move";
+    } else if (status.hovers.length) {
+      style = "pointer";
+    }
+
+    if (style) {
+      cursor.push(style);
+      onCleanup(() => cursor.pop());
+    }
+  });
+
+  // 自动保存历史及恢复服务
+  const history = useHistory();
+  const instanceProps = useInstanceProps();
+  const init = (id: any) => {
+		const quitHooks: (() => void)[] = []
+    if (!id) return quitHooks;
+
+		const unloadHandler = () => {
+      if (history.hasRedo.value || history.hasUndo.value) {
+        history.saveLocal();
+      }
+    }
+    history.setLocalId(id);
+    window.addEventListener("beforeunload", unloadHandler);
+		quitHooks.push(() => window.removeEventListener('beforeunload', unloadHandler))
 
-	const toggleHit = () => {
-		if (!layers.value) return;
-		layers.value.forEach(layer => {
-			layer.toggleHitCanvas()
+    if (!history.hasLocal()) return quitHooks;
+
+		let isOpen = true
+		ElMessageBox.confirm("检测到有历史数据,是否要恢复?", {
+			type: "info",
+			confirmButtonText: "恢复",
+			cancelButtonText: "取消",
+		}).then(() => {
+      history.loadLocalStorage();
+		}).catch(() => {
+      history.clearLocal();
+		}).finally (() => {
+			isOpen = false
 		})
-	}
-
-	return {
-		...useInteractiveDrawShapeAPI(),
-		get stage()  {
-			const $store = stage.value!.getStage()
-			return $store
-		},
-		exposeBlob,
-		toggleHit,
-		updateSize,
-		history,
-		store,
-		mode,
-		getData() {
-			return store.data
-		},
-		viewer,
-		presetAdd: interactiveProps,
-		config
-	}
-}
+		quitHooks.push(() => isOpen && ElMessageBox.close())
+		return quitHooks;
+  };
+  watchEffect((onCleanup) => {
+		onCleanup(mergeFuns(init(instanceProps.get().id)))
+	});
+};
+
+export type DrawExpose = ReturnType<typeof useExpose>;
+type PickParams<K extends keyof Stage, O extends string> = Stage[K] extends (
+  ...args: any
+) => any
+  ? Omit<Required<Parameters<Stage[K]>>[0], O>
+  : never;
+
+export const useExpose = () => {
+  const mode = useMode();
+  const interactiveProps = useInteractiveProps();
+  const stage = useStage();
+  const layers = useLayers();
+  const store = useStore();
+  const history = useHistory();
+  const viewer = useViewer().viewer;
+  const { updateSize } = useGlobalResize();
+  const config = reactive({
+    showGrid: true,
+    showLabelLine: true,
+  });
+
+  const exposeBlob = (config?: PickParams<"toBlob", "callback">) => {
+    const $stage = stage.value!.getStage();
+    return new Promise<Blob>((resolve) => {
+      $stage.toBlob({ ...config, resolve } as any);
+    });
+  };
+
+  const toggleHit = () => {
+    if (!layers.value) return;
+    layers.value.forEach((layer) => {
+      layer.toggleHitCanvas();
+    });
+  };
 
+  return {
+    ...useInteractiveDrawShapeAPI(),
+    get stage() {
+      const $store = stage.value!.getStage();
+      return $store;
+    },
+    exposeBlob,
+    toggleHit,
+    updateSize,
+    history,
+    store,
+    mode,
+    getData() {
+      return store.data;
+    },
+    viewer,
+    presetAdd: interactiveProps,
+    config,
+  };
+};

+ 21 - 0
src/core/hook/use-fetch.ts

@@ -0,0 +1,21 @@
+import { onlyId } from "@/utils/shared";
+import { useInstanceProps } from "./use-global-vars";
+
+export const useResourceHandler = () => {
+  const handler = useInstanceProps().get().handlerResource;
+  const mateMap: Record<string, string> = {
+    svg: "image/svg+xml",
+  };
+
+  return async (data: string | Blob | File, type?: string) => {
+    type = type && mateMap[type!] ? mateMap[type!] : type
+    if (typeof data === "string") {
+      data = new Blob([data], { type });
+    }
+    if (!(data instanceof File)) {
+      const entrie = Object.entries(mateMap).find(([k, v]) => v === type)
+      data = new File([data], onlyId() + (entrie ? `.${entrie![0]}` : ''))
+    }
+    return await handler(data as File)
+  };
+};

+ 18 - 0
src/core/hook/use-global-vars.ts

@@ -19,6 +19,7 @@ import { Layer } from "konva/lib/Layer";
 import { Pos } from "@/utils/math.ts";
 import { listener } from "@/utils/event.ts";
 import { mergeFuns } from "@/utils/shared.ts";
+import { DrawData } from "../components/index.ts";
 
 export const installGlobalVar = <T>(
   create: () => { var: T; onDestroy: () => void } | T,
@@ -63,6 +64,23 @@ export const installGlobalVar = <T>(
     : useGlobalVar;
 };
 
+export type InstanceProps = {
+  id?: string;
+  data: DrawData;
+  handlerResource(file: File): Promise<string>
+}
+export const useInstanceProps = installGlobalVar(() => {
+  const props = ref<InstanceProps>()
+  return {
+    set(val: InstanceProps) {
+      props.value = val
+    },
+    get() {
+      return props.value!
+    }
+  }
+}, Symbol('instanceId'))
+
 export const stackVar = <T>(init?: T) => {
   const stack = reactive([init]) as T[];
   const result = {

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

@@ -178,6 +178,34 @@ const useZIndexsManage = installGlobalVar(() => {
   };
 }, Symbol("zIndexsManage"));
 
+export const useCurrentZIndex = () => {
+  const store = useStore();
+  return {
+    get max() {
+      let cur = Number.MIN_VALUE 
+      for (const items of Object.values(store.data)) {
+        for (const item of items) {
+          if (cur < item.zIndex) {
+            cur = item.zIndex
+          }
+        }
+      }
+      return cur
+    },
+    get min() {
+      let cur = Number.MAX_VALUE 
+      for (const items of Object.values(store.data)) {
+        for (const item of items) {
+          if (cur > item.zIndex) {
+            cur = item.zIndex
+          }
+        }
+      }
+      return cur
+    },
+  }
+}
+
 export const useZIndex = (
   shape: Ref<DC<EntityShape> | undefined>,
   atData: Ref<DrawItem>

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

@@ -22,6 +22,8 @@ import { useAniamtion } from "./use-animation.ts";
 import { KonvaEventObject } from "konva/lib/Node";
 import { useFormalLayer } from "./use-layer.ts";
 import { Layer } from "konva/lib/Layer";
+import { useStore } from "../store/index.ts";
+import { Group } from "konva/lib/Group";
 
 export const getHoverShape = (stage: Stage, layer: Layer) => {
   const hover = ref<EntityShape>();
@@ -86,7 +88,7 @@ export const useShapeIsTransformerInner = () => {
 export const useMouseShapesStatus = installGlobalVar(() => {
   const can = useCan();
   const stage = useStage();
-  const formatLayer = useFormalLayer()
+  const formatLayer = useFormalLayer();
   const listeners = ref([]) as Ref<EntityShape[]>;
   const hovers = ref([]) as Ref<EntityShape[]>;
   const press = ref([]) as Ref<EntityShape[]>;
@@ -106,7 +108,6 @@ export const useMouseShapesStatus = installGlobalVar(() => {
         return;
       }
 
-      
       const pHover =
         hover.value && shapeTreeContain(listeners.value, hover.value);
       // TODO首先确定之前的有没有离开
@@ -230,6 +231,24 @@ export const useMouseShapeStatus = (
   });
 };
 
+export const useActiveItem = <T extends EntityShape>() => {
+  const status = useMouseShapesStatus();
+  const store = useStore();
+  return computed(() => {
+    if (!status.actives[0]) return;
+    let shape = status.actives[0] as T;
+    shape =
+      ((shape as Group)?.findOne &&
+        ((shape as Group)?.findOne(".repShape") as T)) ||
+      shape;
+
+    return {
+      shape,
+      item: store.getItemById(status.actives[0].id()),
+    };
+  });
+};
+
 type MouseStyleProps<T extends ShapeType> = {
   shape?: Ref<DC<EntityShape> | undefined>;
   getMouseStyle: ComponentValue<T, "getMouseStyle">;

+ 43 - 0
src/core/hook/use-paste.ts

@@ -0,0 +1,43 @@
+import { listener } from "@/utils/event";
+import { installGlobalVar, stackVar, useStage } from "./use-global-vars";
+import { Pos } from "@/utils/math";
+
+type PasteHandlers = Record<
+  string,
+  | { handler: (position: Pos, data: string, meta: string) => void; type: "string" }
+  | { handler: (position: Pos, data: File, meta: string) => void; type: "file" }
+>;
+export const usePaste = installGlobalVar(() => {
+  const stage = useStage();
+  const handlers = stackVar<PasteHandlers>({});
+  const pasteHandler = (ev: ClipboardEvent) => {
+    if (ev.target !== document.body && (ev.target as any).id !== 'dom-mount') return;
+
+    const pos = stage.value?.getNode().pointerPos;
+    const clipboardData = ev.clipboardData;
+    if (!clipboardData || !pos) return;
+
+    ev.preventDefault();
+    for (const item of clipboardData.items) {
+      const handMetas = Object.keys(handlers.value)
+      for (const handMeta of handMetas) {
+        if (item.type.includes(handMeta)) {
+          const handleItem = handlers.value[handMeta]
+          if (handleItem.type === 'file') {
+            const file = item.getAsFile()
+            file && handleItem.handler(pos, file, item.type)
+          } else {
+            item.getAsString((str) => {
+              str.trim() && handleItem.handler(pos, str.trim(), item.type)
+            });
+          }
+        }
+      }
+    }
+  };
+
+  return {
+    var: handlers,
+    onDestroy: listener(window, "paste", pasteHandler),
+  };
+});

+ 15 - 4
src/core/hook/use-transformer.ts

@@ -25,6 +25,7 @@ import { Rect } from "konva/lib/shapes/Rect";
 import { Text } from "konva/lib/shapes/Text";
 import { Group } from "konva/lib/Group";
 import { BaseItem } from "../components/util.ts";
+import { useGetComponentData } from "./use-component.ts";
 
 export type TransformerExtends = Transformer & {
   queueShapes: Ref<EntityShape[]>;
@@ -258,6 +259,7 @@ export const useShapeTransformer = <T extends EntityShape>(
   const transformer = useTransformer();
   const transformIngShapes = useTransformIngShapes();
   const viewTransform = useViewerTransform();
+  const getComponentData = useGetComponentData();
   const can = useCan();
 
   const init = ($shape: T) => {
@@ -271,7 +273,7 @@ export const useShapeTransformer = <T extends EntityShape>(
         update: emptyFn,
       };
     }
-    
+
     const updateTransform = () => {
       if (!can.dragMode) return;
       let appleTransform = rep.tempShape.getTransform().copy();
@@ -362,12 +364,21 @@ export const useShapeTransformer = <T extends EntityShape>(
           { flush: "post" }
         );
 
+        const stopLeaveUpdate = watch(
+          () => getComponentData($shape).value,
+          (val) => {
+            rep.update && rep.update();
+          },
+          { flush: "post", deep: true }
+        );
+
         onCleanup(() => {
           for (const key in oldConfig) {
             (transformer as any)[key](oldConfig[key]);
           }
           stopTransformerForceUpdate();
           stopPointupListener();
+          stopLeaveUpdate();
           // TODO: 有可能transformer已经转移
           if (transformer.queueShapes.value.includes($shape)) {
             transformer.nodes([]);
@@ -446,13 +457,13 @@ export const transformerRepShapeHandler = <T extends EntityShape>(
   }
   shape.parent!.add(repShape);
   repShape.zIndex(shape.getZIndex());
-  repShape.listening(false)
-  shape.repShape = repShape
+  repShape.listening(false);
+  shape.repShape = repShape;
 
   return [
     repShape,
     () => {
-      shape.repShape = undefined
+      shape.repShape = undefined;
       repShape.remove();
     },
   ] as const;

+ 2 - 2
src/core/propertys/describes.json

@@ -140,8 +140,8 @@
       "scale": 30
     },
     "default": [
-      30,
-      30
+      1,
+      0
     ]
   },
   "pointerPosition": {

+ 7 - 2
src/core/propertys/hover-operate.vue

@@ -61,8 +61,13 @@ const hasRClick = (ev: MouseEvent) => {
 watchEffect((onCleanup) => {
   const dom = stage.value?.getStage().container();
   if (!dom) return;
-  const clickHandler = (ev: MouseEvent) => {
-    rightClick.value = hasRClick(ev);
+  const clickHandler = async (ev: MouseEvent) => {
+    const show = hasRClick(ev);
+    if (show && rightClick.value) {
+      rightClick.value = false;
+      await nextTick();
+    }
+    rightClick.value = show;
   };
 
   dom.addEventListener("contextmenu", clickHandler);

+ 18 - 70
src/core/renderer/renderer.vue

@@ -31,51 +31,30 @@ import SnapLines from "../helper/snap-lines.vue";
 import BackGrid from "../helper/back-grid.vue";
 import SplitLine from "../helper/split-line.vue";
 import { DrawData, ShapeType, components } from "../components";
-import { useCursor, useDownKeys, useMode, useStage } from "../hook/use-global-vars.ts";
+import {
+  InstanceProps,
+  useCursor,
+  useInstanceProps,
+  useMode,
+  useStage,
+} from "../hook/use-global-vars.ts";
 import { useViewerTransformConfig } from "../hook/use-viewer.ts";
-import { useListener, useResize } from "../hook/use-event.ts";
-import { useExpose } from "../hook/use-expose.ts";
+import { useResize } from "../hook/use-event.ts";
+import { useAutoService, useExpose } from "../hook/use-expose.ts";
 import { DomMountId } from "../../constant";
 import { useStore } from "../store/index.ts";
-import { useInteractiveDrawShapeAPI } from "../hook/use-draw.ts";
 import { Mode } from "@/constant/mode.ts";
-import { computed, watchEffect } from "vue";
-import { useMouseShapesStatus } from "../hook/use-mouse-status.ts";
-import { useHistory } from "../hook/use-history.ts";
-import { ElMessageBox } from "element-plus";
+import { computed, getCurrentInstance } from "vue";
+import { install } from "../../install-lib.ts";
 
-const props = defineProps<{
-  data: DrawData;
-  id?: string;
-}>();
-const store = useStore();
-const history = useHistory();
+const instance = getCurrentInstance();
+install(instance!.appContext.app);
 
-const init = async () => {
-  if (props.id) {
-    history.setLocalId(props.id);
-    window.onunload = (ev) => {
-      if (history.hasRedo.value || history.hasUndo.value) {
-        history.saveLocal();
-      }
-    };
-  }
-  store.setStore(props.data);
-  if (props.id && history.hasLocal()) {
-    try {
-      await ElMessageBox.confirm("检测到有历史数据,是否要恢复?", {
-        type: "info",
-        confirmButtonText: "恢复",
-        cancelButtonText: "取消",
-      });
-      history.loadLocalStorage();
-      return;
-    } catch {
-      history.clearLocal();
-    }
-  }
-};
-init();
+const props = defineProps<InstanceProps>();
+const store = useStore();
+store.setStore(props.data);
+useInstanceProps().set(props);
+useAutoService();
 
 const stage = useStage();
 const size = useResize();
@@ -83,18 +62,6 @@ const viewerConfig = useViewerTransformConfig();
 const types = Object.keys(components) as ShapeType[];
 const mode = useMode();
 
-// 退出添加模式
-const { quitDrawShape } = useInteractiveDrawShapeAPI();
-useListener(
-  "contextmenu",
-  (ev) => {
-    if (ev.button === 2) {
-      quitDrawShape();
-    }
-  },
-  document.documentElement
-);
-
 const cursor = useCursor();
 const cursorStyle = computed(() => {
   if (cursor.value.includes(".")) {
@@ -104,25 +71,6 @@ const cursorStyle = computed(() => {
   }
 });
 
-const status = useMouseShapesStatus();
-const downKeys = useDownKeys();
-
-watchEffect((onCleanup) => {
-  let style: string | null = null;
-  if (downKeys.has(" ")) {
-    style = "pointer";
-  } else if (mode.include(Mode.update)) {
-    style = "move";
-  } else if (status.hovers.length) {
-    style = "pointer";
-  }
-
-  if (style) {
-    cursor.push(style);
-    onCleanup(() => cursor.pop());
-  }
-});
-
 const expose = useExpose();
 defineExpose(expose);
 </script>

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

@@ -96,7 +96,6 @@ const setBGImage = (file: File) => {
     {
       width: window.innerWidth,
       height: window.innerHeight,
-      listening: false,
       url: URL.createObjectURL(file),
       zIndex: -1,
     },

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

@@ -5,9 +5,10 @@
       <Slide class="slide" v-if="draw" />
       <div class="content" ref="drawEle">
         <DrawBoard
-          id="asd"
           v-if="drawEle"
-          :ref="(e: any) => draw = e.draw"
+          id="asd"
+          :handler-resource="handlerResource"
+          ref="draw"
           :data="(data as any)"
         />
       </div>
@@ -34,6 +35,11 @@ const saveHandler = () => {
   save(draw.value?.getData());
 };
 
+const handlerResource = async (file: File) => {
+  console.log("上传资源", file);
+  return URL.createObjectURL(file);
+};
+
 const full = ref(false);
 watch(full, (_f1, _f2, onCleanup) => {
   onCleanup(

+ 23 - 88
src/example/fuse/views/init.ts

@@ -10,9 +10,6 @@ const initData = {
         243.9711189430529,
       ],
     },
-  ],
-  ['bgImage']: [
-
     {
       id: "10cc6b4d-e90a-47c2-ae3f-9ab3a21190e7",
       createTime: 1736762304345,
@@ -25,6 +22,8 @@ const initData = {
       mat: [1, 0, 0, 1, 591, 436],
     },
   ],
+  bgImage: [
+  ],
   rectangle: [
     {
       id: "0",
@@ -188,6 +187,20 @@ const initData = {
       ],
       attitude: [1, 0, 0, 1, 0, 0],
     },
+    {
+      id: "95ed1359-3650-4363-ac01-54c02e72e943",
+      createTime: 1737625169697,
+      zIndex: 0,
+      opacity: 1,
+      ref: false,
+      points: [
+        { x: 237.44588045440253, y: 143.0442185836713 },
+        { x: 358.0396304544025, y: 143.0442185836713 },
+        { x: 358.0396304544025, y: 240.0207810836713 },
+        { x: 237.44588045440253, y: 240.0207810836713 },
+      ],
+      attitude: [1, 0, 0, 1, -299.2142757955975, -65.5182814163287],
+    },
   ],
   triangle: [
     {
@@ -366,92 +379,14 @@ const initData = {
   ],
   circle: [
     {
-      createTime: 3,
-      zIndex: 0,
-      id: "333a3",
-      x: 1372.22265625,
-      y: 100.3671875,
-      radius: 84.72265625,
-    },
-    {
-      id: "9ec46814-ebd9-4b6c-9901-b849a774251b",
-      createTime: 1736386847950,
-      zIndex: 0,
-      opacity: 1,
-      ref: false,
-      x: -253.43698048101692,
-      y: 94.24162863444133,
-      radius: 42.92427036911795,
-    },
-    {
-      id: "bb13c3e5-ce7c-490d-9e32-6ba1d70312d2",
-      createTime: 1736386861123,
-      zIndex: 0,
-      opacity: 1,
-      ref: false,
-      x: -161.3880040008919,
-      y: 94.24162863444133,
-      radius: 49.12470611100707,
-    },
-    {
-      id: "7fc10a01-e211-46a7-b1ac-6f6d136b25e9",
-      createTime: 1736386863425,
-      zIndex: 0,
-      opacity: 1,
-      ref: false,
-      x: -31.04138198657529,
-      y: 94.24162863444133,
-      radius: 81.22191590330954,
-    },
-    {
-      id: "226232c5-c4fe-4281-af03-99877ccd419e",
-      createTime: 1736386866584,
-      zIndex: 0,
-      opacity: 1,
-      ref: false,
-      x: -334.346030360653,
-      y: 94.24162863444133,
-      radius: 37.98477951051814,
-    },
-    {
-      id: "844a8f6c-6b3b-4d1c-b428-8ae415705cb3",
-      createTime: 1736386871198,
-      zIndex: 0,
-      opacity: 1,
-      ref: false,
-      x: -393.4777088349078,
-      y: 94.24162863444133,
-      radius: 21.146898963736646,
-    },
-    {
-      id: "d8b9ddf4-c6e4-4dd9-97ea-90eac6287085",
-      createTime: 1736386872987,
-      zIndex: 0,
-      opacity: 1,
-      ref: false,
-      x: -429.7180232437324,
-      y: 94.24162863444133,
-      radius: 15.093415445087942,
-    },
-    {
-      id: "4dc506af-6117-4624-9d21-9ea41f1fb22a",
-      createTime: 1736386875178,
-      zIndex: 0,
-      opacity: 1,
-      ref: false,
-      x: -455.18342141302026,
-      y: 94.24162863444133,
-      radius: 10.371982724199938,
-    },
-    {
-      id: "9e6c4261-7203-49ee-b1b3-b65c8f54920a",
-      createTime: 1736386877298,
-      zIndex: 0,
+      id: "335dd470-a6b5-4fef-bb26-0a40010c4b96",
+      createTime: 1737621610324,
+      zIndex: 1,
       opacity: 1,
       ref: false,
-      x: 213.96148297434104,
-      y: 103.48016182562202,
-      radius: 162.92261163072476,
+      mat: [1, 0, 0, 1, 610.80859375, 140.28125],
+      radiusX: 277.6712260605102,
+      radiusY: 82.0068317036322,
     },
   ],
   icon: [
@@ -667,7 +602,7 @@ 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.4930798945178672e-15, 1.0000000000000104, -1.0000000000000155,
         -1.5971633030764742e-15, 1083.2463425727433, 95.00094657517106,

+ 1 - 1
src/index.ts

@@ -1,5 +1,5 @@
 export { components } from './core/components'
-export { default as DrawBoard } from './app.vue'
+export { default as DrawBoard } from './core/renderer/renderer.vue'
 
 import { components } from './core/components'
 

+ 11 - 0
src/install-lib.ts

@@ -0,0 +1,11 @@
+import { App } from "vue";
+import VueKonva from "vue-konva";
+import { createPinia } from "pinia";
+
+const installApps = new WeakSet<App>();
+export const install = (app: App) => {
+  if (installApps.has(app)) return;
+  app.use(VueKonva);
+  app.use(createPinia());
+  installApps.add(app);
+};

+ 10 - 0
src/utils/resource.ts

@@ -27,13 +27,23 @@ export let getImage = (url: string): Promise<HTMLImageElement> => {
   return getImage(url);
 };
 
+export const isSvgString = (str: string) => {
+  // 检查字符串是否以 <svg> 标签开头
+  const svgRegex = /^<svg[^>]*>.*<\/svg>$/is;
+  return svgRegex.test(str);
+}
+
 let svgContentCache: Record<string, Promise<string>>;
 export let getSvgContent = (url: string): Promise<string> => {
   svgContentCache = {};
   getSvgContent = async (url: string) => {
+    if (isSvgString(url)) {
+      return url;
+    }
     if (url in svgContentCache) {
       return svgContentCache[url];
     } else {
+
       const res = await fetch(url);
       return (svgContentCache[url] = res.text());
     }