Преглед на файлове

feat: 添加花瓣快捷键

bill преди 5 месеца
родител
ревизия
2dea6bee09

+ 1 - 0
package.json

@@ -9,6 +9,7 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
     "@element-plus/icons-vue": "^2.3.1",
     "@tweenjs/tween.js": "^25.0.0",
     "@types/node": "^22.9.0",

+ 6 - 0
pnpm-lock.yaml

@@ -1,6 +1,7 @@
 lockfileVersion: 5.4
 
 specifiers:
+  '@amap/amap-jsapi-loader': ^1.0.1
   '@element-plus/icons-vue': ^2.3.1
   '@tweenjs/tween.js': ^25.0.0
   '@types/node': ^22.9.0
@@ -25,6 +26,7 @@ specifiers:
   vue-tsc: ^2.1.6
 
 dependencies:
+  '@amap/amap-jsapi-loader': 1.0.1
   '@element-plus/icons-vue': 2.3.1_vue@3.5.13
   '@tweenjs/tween.js': 25.0.0
   '@types/node': 22.9.0
@@ -52,6 +54,10 @@ devDependencies:
 
 packages:
 
+  /@amap/amap-jsapi-loader/1.0.1:
+    resolution: {integrity: sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==}
+    dev: false
+
   /@babel/helper-string-parser/7.25.9:
     resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
     engines: {node: '>=6.9.0'}

+ 0 - 3
src/constant/init.ts

@@ -1,3 +0,0 @@
-import { Transform } from "konva/lib/Util";
-
-export const initTransform = new Transform()

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

@@ -8,7 +8,8 @@
         zIndex: undefined,
         hitFunc: hitFunc,
         stroke: data.fill,
-        fill: undefined,
+        fill: data.fill,
+        hitStrokeWidth: data.strokeWidth + 10,
         pointerWidth: data.pointerLength,
         closed: false,
         points: flatPositions([data.points[ndx], data.points[ndx + 1]]),

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

@@ -56,6 +56,7 @@ export type ImageData = Partial<typeof defaultStyle> &
     cornerRadius: number;
     url: string;
     mat: number[];
+    proportion?: { scale: number; unit?: string };
   };
 
 export const interactiveToData: InteractiveTo<"image"> = ({

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

@@ -6,6 +6,7 @@
         ...data,
         zIndex: undefined,
         points: flatPositions(data.points),
+        hitStrokeWidth: data.strokeWidth + 10,
         opacity: addMode ? 0.3 : data.opacity,
         hitFunc,
       }"

+ 11 - 7
src/core/helper/split-line.vue

@@ -14,13 +14,15 @@
 import { computed, onUnmounted } from "vue";
 import { components } from "../components";
 import { useComponentsAttach } from "../hook/use-component";
-import { useViewerTransform } from "../hook/use-viewer";
+import { useViewerInvertTransformConfig, useViewerTransform } from "../hook/use-viewer";
 import { useResize } from "../hook/use-event";
 import { Pos } from "@/utils/math";
 import { TextConfig } from "konva/lib/shapes/Text";
 import { Transform } from "konva/lib/Util";
-import { useExpose } from "../hook/use-expose";
 import { normalPadding } from "@/utils/bound";
+import { useConfig } from "../hook/use-config";
+import { useStore } from "../store";
+import { useProportion } from "../hook/use-proportion";
 
 const style = {
   stroke: "#000",
@@ -51,7 +53,7 @@ const center = computed(() => {
   };
 });
 
-const { config } = useExpose();
+const config = useConfig();
 const rang = computed(() => {
   if (!size.value) return;
   const mrs = normalPadding(config.margin || 0);
@@ -128,6 +130,8 @@ const rectAxisSteps = computed(() => {
   return axis;
 });
 
+const { transform: getWidthText } = useProportion();
+const invConfig = useViewerInvertTransformConfig();
 const fontSize = 12;
 const axissInfo = computed(() => {
   if (!rang.value) return;
@@ -162,7 +166,7 @@ const axissInfo = computed(() => {
 
       infos.left.texts.push({
         width: width,
-        text: Math.floor(width * 100).toString(),
+        text: getWidthText(width * invConfig.value.scaleY),
         ...tf.decompose(),
       });
     }
@@ -192,7 +196,7 @@ const axissInfo = computed(() => {
 
       infos.right.texts.push({
         width: width,
-        text: Math.floor(width * 100).toString(),
+        text: getWidthText(width * invConfig.value.scaleY),
         ...tf.decompose(),
       });
     }
@@ -215,7 +219,7 @@ const axissInfo = computed(() => {
       const width = cur - prev;
       infos.top.texts.push({
         width: width,
-        text: Math.floor(width * 100).toString(),
+        text: getWidthText(width * invConfig.value.scaleX),
         ...lt,
       });
     }
@@ -238,7 +242,7 @@ const axissInfo = computed(() => {
       const width = cur - prev;
       infos.bottom.texts.push({
         width: width,
-        text: Math.floor(width * 100).toString(),
+        text: getWidthText(width * invConfig.value.scaleX),
         ...lt,
       });
     }

+ 0 - 1
src/core/hook/use-config.ts

@@ -62,7 +62,6 @@ export const useConfig = installGlobalVar(() => {
   return reactive({
     ...defConfig,
     compass: fast('compass'),
-    proportion: fast("proportion"),
     size: computed({
       get: () => size.value!,
       set: (size: Size | null) => {

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

@@ -20,6 +20,7 @@ export const useListener = <
     if (stage.value) {
       const $stage = stage.value!.getStage();
       const dom = $stage.container() as any;
+      console.log(dom)
       onCleanup(
         listener(target || dom, eventName, function (ev) {
           callback.call(this, ev, dom);

+ 36 - 3
src/core/hook/use-expose.ts

@@ -14,7 +14,7 @@ import { useViewer } from "./use-viewer.ts";
 import { useGlobalResize, useListener } from "./use-event.ts";
 import { useInteractiveDrawShapeAPI } from "./use-draw.ts";
 import { useHistory } from "./use-history.ts";
-import { reactive, watchEffect } from "vue";
+import { watchEffect } from "vue";
 import { usePaste } from "./use-paste.ts";
 import { useMouseShapesStatus } from "./use-mouse-status.ts";
 import { Mode } from "@/constant/mode.ts";
@@ -25,8 +25,8 @@ import { isSvgString } from "@/utils/resource.ts";
 import { useResourceHandler } from "./use-fetch.ts";
 import { useConfig } from "./use-config.ts";
 
-export const useAutoService = () => {
-  // 自动粘贴服务
+// 自动粘贴服务
+export const useAutoPaste = () => {
   const paste = usePaste();
   const drawAPI = useInteractiveDrawShapeAPI();
   const resourceHandler = useResourceHandler();
@@ -54,19 +54,52 @@ export const useAutoService = () => {
       type: "file",
     },
   });
+}
 
+// 快捷键服务
+export const useShortcutKey = () => {
   // 自动退出添加模式
   const { quitDrawShape } = useInteractiveDrawShapeAPI();
   useListener(
     "contextmenu",
     (ev) => {
+      console.log('a?')
       if (ev.button === 2) {
         quitDrawShape();
       }
     },
     document.documentElement
   );
+  console.error('a?')
 
+  const history = useHistory()
+  const status = useMouseShapesStatus()
+  const store = useStore()
+  useListener('keydown', (ev) => {
+    console.log(ev.key)
+    if (ev.key === 'z' && ev.ctrlKey) {
+      history.hasUndo.value && history.undo();
+    } else if (ev.key === 'y' && ev.ctrlKey) {
+      history.hasRedo.value && history.redo();
+    } else if (ev.key === 's' && ev.ctrlKey) {
+      // 保存
+      history.saveLocal();
+    } else if (ev.key === 'Delete' || ev.key === 'Backspace') {
+      // 删除
+      status.actives.forEach((shape) => {
+        const id = shape.id()
+        const type = id && store.getType(id)
+        if (type) {
+          store.delItem(type, id)
+        }
+      });
+    }
+  }, window)
+}
+
+export const useAutoService = () => {
+  useAutoPaste();
+  useShortcutKey()
   // 鼠标自动变化服务
   const status = useMouseShapesStatus();
   const downKeys = useDownKeys();

+ 6 - 7
src/core/hook/use-history.ts

@@ -20,8 +20,8 @@ export class DrawHistory {
     return this.list.find(item => item.id === this.currentId)?.data
   }
 
-  preventFlag = false;
-  onceFlag = false;
+  preventFlag = 0;
+  onceFlag = 0;
   onceHistory: string | null = null;
   initData: string | null = null;
   clearData: string = ''
@@ -72,9 +72,9 @@ export class DrawHistory {
   }
 
   preventTrack(fn: () => void) {
-    this.preventFlag = true;
+    this.preventFlag++
     fn();
-    this.preventFlag = false;
+    this.preventFlag--
   }
 
   private saveKeyPrev = '__history__'
@@ -153,9 +153,9 @@ export class DrawHistory {
   }
 
   onceTrack(fn: () => void) {
-    this.onceFlag = true;
+    this.onceFlag++
     fn();
-    this.onceFlag = false;
+    this.onceFlag--
     if (this.onceHistory) {
       this.push(this.onceHistory);
       this.onceHistory = null;
@@ -226,7 +226,6 @@ export const useHistoryAttach = <T>(
       }
 
       onCleanup(() => {
-        console.log('clear')
         history.bus.off("push", pushHandler);
         history.bus.off("attachs", attachsHandler);
         current.value = void 0;

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

@@ -0,0 +1,21 @@
+import { computed } from "vue";
+import { useStore } from "../store";
+import { installGlobalVar } from "./use-global-vars";
+
+export const useProportion = installGlobalVar(() => {
+  const store = useStore();
+  const proportion = computed(() => {
+    const image = store.getTypeItems("image").find((item) => !!item.proportion);
+    return image?.proportion || { scale: 1, unit: "" };
+  });
+
+  const transform = (width: number) => {
+    return Math.floor(width * proportion.value.scale).toString() + (proportion.value.unit || "");
+    // return width.toString()
+  }
+
+  return {
+    proportion,
+    transform,
+  }
+});

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

@@ -31,10 +31,10 @@
           </template>
         </v-layer>
         <v-layer id="helper">
-          <Compass v-if="config.showCompass" />
           <ActiveBoxs />
           <SnapLines />
           <SplitLine v-if="expose.config.showLabelLine" />
+          <Compass v-if="config.showCompass" />
           <Debugger v-if="isDev" />
           <Border />
         </v-layer>

+ 3 - 6
src/core/store/store.ts

@@ -10,8 +10,7 @@ export type StoreConfig = {
   compass: {
     rotation: number;
     url?: string;
-  };
-  proportion: { scale: number; unit?: string };
+  }
 };
 export type StoreData = {
   typeItems: DrawData;
@@ -19,7 +18,6 @@ export type StoreData = {
 };
 const defConfig: StoreData["config"] = {
   compass: { rotation: 0 },
-  proportion: { scale: 1 },
 };
 
 export const getEmptyStoreData = (): StoreData => {
@@ -30,7 +28,6 @@ export const useStoreRaw = defineStore("draw-data", {
   state: () => ({ data: getEmptyStoreData() }),
   getters: {
     items() {
-      console.log(this.data);
       return Object.values((this as any).data.typeItems).flat() as DrawItem[];
     },
     sortItems() {
@@ -72,7 +69,7 @@ export const useStoreRaw = defineStore("draw-data", {
     },
     addItem<T extends ShapeType>(type: T, item: DrawItem<T>) {
       this.$patch((state) => {
-        if (!(type in state.data)) {
+        if (!(type in state.data.typeItems)) {
           state.data.typeItems[type] = [];
         }
         state.data.typeItems[type]!.push(item as any);
@@ -113,7 +110,7 @@ export const useStoreRaw = defineStore("draw-data", {
       }
     },
     getItemById(id: string) {
-      const types = Object.keys(this.data) as ShapeType[];
+      const types = Object.keys(this.data.typeItems) as ShapeType[];
       for (const type of types) {
         const item = this.data.typeItems[type]?.find((item) => item.id === id);
         if (item) {

+ 44 - 0
src/example/basemap/dialog.vue

@@ -0,0 +1,44 @@
+<template>
+  <ElDialog
+    :title="props.title"
+    v-model="props.visiable"
+    :width="props.width"
+    :height="props.height"
+    @closed="showContent = false"
+  >
+    <div v-if="showContent">
+      <component :is="props.content" ref="contentRef" />
+    </div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="props.cancel">取消</el-button>
+        <el-button type="primary" @click="submit"> 确定 </el-button>
+      </div>
+    </template>
+  </ElDialog>
+</template>
+
+<script lang="ts" setup>
+import { ElDialog, ElButton } from "element-plus";
+import { ref, watch } from "vue";
+import { props } from ".";
+
+const contentRef = ref<any>(null);
+const showContent = ref(false);
+
+watch(
+  () => props.visiable,
+  () => {
+    if (props.visiable) {
+      showContent.value = true;
+    }
+  }
+);
+
+const submit = async () => {
+  try {
+    const info = await contentRef.value.submit();
+    props.submit(info);
+  } catch {}
+};
+</script>

+ 255 - 0
src/example/basemap/gd-map/selectAMapImage.vue

@@ -0,0 +1,255 @@
+<template>
+  <div class="search-layout">
+    <el-input v-model="keyword" placeholder="输入名称搜索" style="width: 350px" clearable>
+      <template #append>
+        <el-button :icon="Search" />
+      </template>
+    </el-input>
+    <div class="rrr">
+      <div class="search-result" v-show="keyword && showSearch" ref="resultEl"></div>
+      <div class="search-sh" v-show="keyword">
+        <el-button style="width: 100%" @click="showSearch = !showSearch">
+          {{ showSearch ? "收起" : "展开" }}搜索结果
+        </el-button>
+      </div>
+    </div>
+  </div>
+  <div class="def-select-map-layout">
+    <div class="def-select-map" ref="mapEl"></div>
+  </div>
+
+  <div class="def-map-info" v-if="info">
+    <p><span>纬度</span>{{ info.lat }}</p>
+    <p><span>经度</span>{{ info.lng }}</p>
+    <p><span>缩放级别</span>{{ info.zoom }}</p>
+  </div>
+</template>
+
+<script setup lang="ts">
+import AMapLoader from "@amap/amap-jsapi-loader";
+import { ElInput, ElButton } from "element-plus";
+import { Search } from "@element-plus/icons-vue";
+import { ref, watchEffect } from "vue";
+import { debounce } from "@/utils/shared";
+
+export type MapImage = { blob: Blob | null; search: MapInfo | null; ratio: number };
+type MapInfo = { lat: number; lng: number; zoom: number; text: string };
+
+const keyword = ref("");
+const showSearch = ref(true);
+const info = ref<MapInfo>();
+const searchInfo = ref<MapInfo>();
+
+watchEffect(() => {
+  if (keyword.value) {
+    showSearch.value = true;
+  }
+});
+
+const mapEl = ref<HTMLDivElement>();
+const resultEl = ref<HTMLDivElement>();
+const searchAMap = ref<any>();
+
+let AMap: any;
+let map: any;
+watchEffect(async (onCleanup) => {
+  if (!mapEl.value || !resultEl.value) {
+    return;
+  }
+  AMap = await AMapLoader.load({
+    plugins: ["AMap.PlaceSearch", "AMap.Event"],
+    key: "e661b00bdf2c44cccf71ef6070ef41b8",
+    version: "2.0",
+  });
+
+  map = new AMap.Map(mapEl.value, {
+    WebGLParams: {
+      preserveDrawingBuffer: true,
+    },
+    resizeEnable: true,
+  });
+  const placeSearch = new AMap.PlaceSearch({
+    pageSize: 5,
+    showCover: false,
+    pageIndex: 1,
+    map: map,
+    panel: resultEl.value,
+    autoFitView: true,
+  });
+  const setSearch = (data: any) => {
+    searchInfo.value = {
+      text: data.pname + data.cityname + data.adname + data.address,
+      lat: data.location.lat,
+      lng: data.location.lng,
+      zoom: 0,
+    };
+  };
+
+  placeSearch.on("listElementClick", (e: any) => {
+    setSearch(e.data);
+    showSearch.value = false;
+  });
+  let clickMarker: any;
+
+  map.on("click", function (e: any) {
+    // 获取点击位置的经纬度坐标
+    var latitude = e.lnglat.lat;
+    var longitude = e.lnglat.lng;
+
+    searchInfo.value = {
+      text: "",
+      lat: latitude,
+      lng: longitude,
+      zoom: 0,
+    };
+    clickMarker && map.remove(clickMarker);
+    clickMarker = null;
+    // 在地图上添加标记
+    clickMarker = new AMap.Marker({
+      position: [longitude, latitude],
+      title: "点击位置",
+    });
+
+    map.add(clickMarker);
+  });
+  placeSearch.on("complete", function (result: any) {
+    setTimeout(() => {
+      const markers = map.getAllOverlays("marker");
+      for (const marker of markers) {
+        marker.on("click", () => {
+          clickMarker && map.remove(clickMarker);
+          clickMarker = null;
+          setSearch(marker._data);
+        });
+      }
+    }, 500);
+  });
+
+  const getMapInfo = (): MapInfo => {
+    var zoom = map.getZoom(); //获取当前地图级别
+    var center = map.getCenter();
+    return {
+      text: "",
+      zoom,
+      lat: center.lat,
+      lng: center.lng,
+    };
+  };
+  //绑定地图移动与缩放事件
+  map.on("moveend", () => {
+    info.value = getMapInfo();
+  });
+  map.on("zoomend", () => {
+    info.value = getMapInfo();
+  });
+  searchAMap.value = placeSearch;
+
+  onCleanup(() => {
+    searchAMap.value = null;
+    map.destroy();
+  });
+});
+
+const search = debounce((keyword: string) => {
+  searchAMap.value.search(keyword);
+}, 1000);
+watchEffect(() => {
+  searchAMap.value && search(keyword.value);
+});
+
+const getPixelAspectRatio = () => {
+  // 获取地图视口的经纬度范围
+  const bounds = map.getBounds();
+
+  // 计算视口的宽度和高度(单位为米)
+  const southWest = bounds.getSouthWest(); // 西南角
+  const northEast = bounds.getNorthEast(); // 东北角
+  const width = AMap.GeometryUtil.distance(
+    [southWest.lng, northEast.lat],
+    [northEast.lng, northEast.lat]
+  ); // 经度变化
+  // const height = AMap.GeometryUtil.distance(
+  //   [southWest.lng, southWest.lat],
+  //   [southWest.lng, northEast.lat]
+  // ); // 纬度变化
+
+  return width / mapEl.value!.offsetWidth;
+  // console.log(width / height);
+  // console.log(mapEl.value?.offsetWidth / mapEl.value?.offsetHeight);
+};
+
+const submit = () => {
+  return new Promise<MapImage>((resolve) => {
+    if (mapEl.value) {
+      getPixelAspectRatio();
+      const canvas = mapEl.value.querySelector("canvas") as HTMLCanvasElement;
+      canvas.toBlob((blob) =>
+        resolve({ blob, search: searchInfo.value!, ratio: getPixelAspectRatio() })
+      );
+    } else {
+      resolve({ blob: null, search: null, ratio: 1 });
+    }
+  });
+};
+
+defineExpose({ submit });
+</script>
+
+<style lang="scss" scoped>
+.search-layout {
+  display: inline-block;
+  position: relative;
+  margin-bottom: 15px;
+  z-index: 2;
+}
+
+.rrr {
+  position: absolute;
+  left: 0;
+  right: 0;
+  z-index: 1;
+}
+
+.search-sh,
+.search-result {
+  overflow: hidden;
+
+  &.show {
+    max-height: 450px;
+    overflow-y: auto;
+  }
+}
+
+.def-map-info {
+  margin-top: 10px;
+  p {
+    font-size: 14px;
+    color: rgba(0, 0, 0, 0.85);
+    display: inline;
+    &:not(:last-child)::after {
+      content: ",";
+      margin-right: 6px;
+    }
+  }
+
+  span::after {
+    content: ":";
+  }
+}
+
+.def-select-map-layout {
+  --scale: 1.5;
+  width: 100%;
+  padding-top: calc((390 / 540) * 100%);
+  position: relative;
+  z-index: 1;
+}
+
+.def-select-map {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 35 - 0
src/example/basemap/index.ts

@@ -0,0 +1,35 @@
+import { reactive } from "vue";
+import selectAMapImage from "./gd-map/selectAMapImage.vue";
+
+export type BasemapInfo = { blob: Blob; scale: number, ratio: number };
+
+type Props = {
+  title: string;
+  width: string;
+  visiable: boolean;
+  height?: string;
+  content: any;
+  submit: (info: BasemapInfo) => void;
+  cancel: () => void;
+};
+export const props = reactive({
+  title: "底图设置",
+  width: "800px",
+}) as Props;
+
+export const getAMapInfo = () =>
+  new Promise<BasemapInfo>((resolve, reject) => {
+    props.content = selectAMapImage;
+    props.title = "选择高德地图底图";
+    props.visiable = true;
+    props.submit = (info: BasemapInfo) => {
+      resolve(info);
+      props.visiable = false
+    };
+    props.cancel = () => {
+      reject("cancel");
+      props.visiable = false
+    };
+  });
+
+export { default as Mount } from "./dialog.vue";

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

@@ -1,4 +1,5 @@
 <template>
+  <BasemapMount />
   <div class="layout" :class="{ full }">
     <Header
       class="header"
@@ -23,6 +24,7 @@
 <script lang="ts" setup>
 import Header from "./header/header.vue";
 import Slide from "./slide/slide.vue";
+import { Mount as BasemapMount } from "@/example/basemap";
 import { onUnmounted, ref, watch } from "vue";
 import { DrawExpose, DrawBoard } from "@/index";
 import { installDraw } from "./use-draw.ts";

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

@@ -1,5 +1,7 @@
 import { v4 as uuid } from "uuid";
 import { Draw } from "../use-draw";
+import { getAMapInfo } from "@/example/basemap";
+import { getImageSize } from "@/utils/shape";
 
 const setPaper = (draw: Draw, p: number[], scale: number) => {
   const pad = 5 * scale;
@@ -20,6 +22,42 @@ const setPaper = (draw: Draw, p: number[], scale: number) => {
 export const handlerMenus = [
   {
     icon: "",
+    name: "底图",
+    children: [
+      {
+        value: uuid(),
+        icon: "",
+        name: "高德地图",
+        handler: async (draw: Draw) => {
+          const info = await getAMapInfo()
+          const size = await getImageSize(info.blob)
+          console.log(info.ratio)
+          let proportion = { scale: info.ratio * 100, unit: "mm" }
+          if (info.ratio > 1000) {
+            proportion = { scale: info.ratio / 1000, unit: "km" }
+          } else if (info.ratio > 1) {
+            proportion = { scale: info.ratio, unit: "m" }
+          } 
+
+          draw.history.onceTrack(() => {
+            draw.addShape(
+              "image",
+              {
+                ...size,
+                url: URL.createObjectURL(info.blob),
+                zIndex: -1,
+                proportion: proportion,
+              },
+              { x: window.innerWidth / 2, y: window.innerHeight / 2 },
+              true
+            );
+          })
+        }
+      }
+    ],
+  },
+  {
+    icon: "",
     name: "纸张",
     children: [
       {

+ 2 - 2
src/example/fuse/views/test.ts

@@ -690,7 +690,7 @@ export const initData = {
   config: {
     drawSize: null,
     compass: { rotation: 0 },
-    proportion: {scale: 1}
+    proportion: {scale: 100, unit: 'mm'},
   }
 };
 
@@ -977,7 +977,7 @@ export const tInitData = {
   },
   config: {
     drawSize: null,
-    proportion: {scale: 1},
+    proportion: {scale: 100, unit: 'mm'},
     compass: { rotation: 0 },
   }
 };

+ 1 - 1
src/example/fuse/views/use-draw.ts

@@ -3,7 +3,7 @@ import { DrawExpose } from "../../../index";
 import { mergeFuns } from "@/utils/shared";
 
 const initDraw = (draw: Draw) => {
-  draw.config.showLabelLine = false;
+  draw.config.showLabelLine = true;
   draw.config.showGrid = false;
   draw.config.showCompass = true
 

+ 17 - 1
src/utils/shape.ts

@@ -1,5 +1,6 @@
 import { Transform } from "konva/lib/Util";
 import { DC, EntityShape } from "../deconstruction";
+import { Size } from "./math";
 
 
 export const shapeTreeEq = (
@@ -77,4 +78,19 @@ export const setShapeTransform = (shape: EntityShape, transform: Transform) => {
 export const packShape = <T extends EntityShape>(shape: T): DC<T> => ({
 	getNode: () => shape,
 	getStage: () => shape
-})
+})
+
+export const getImageSize = (url: string | Blob): Promise<Size> => {
+	return new Promise((resolve, reject) => {
+		const image = new Image();
+		image.onload = () => {
+			resolve({ width: image.width, height: image.height });
+			typeof url !== 'string' && URL.revokeObjectURL(image.src);
+		};
+		image.onerror = (e) => {
+			reject(e);
+			typeof url !== 'string' && URL.revokeObjectURL(image.src);
+		};
+		image.src = typeof url === 'string' ? url : URL.createObjectURL(url);
+	});
+}