Browse Source

修改example结构

bill 7 months ago
parent
commit
861d2cc7fb
51 changed files with 707 additions and 496 deletions
  1. 1 1
      .env
  2. 0 3
      example/fuse/store/index.ts
  3. 0 91
      example/fuse/views/header/funds.ts
  4. 0 64
      example/fuse/views/header/header.vue
  5. 0 2
      example/fuse/views/header/index.ts
  6. 0 4
      example/fuse/views/slide/index.ts
  7. 0 101
      example/fuse/views/slide/menu.ts
  8. 0 0
      public/WX20241031-111850.png
  9. 0 0
      public/icons/BedsideCupboard.svg
  10. 0 0
      public/icons/vue.svg
  11. 44 0
      src/app.vue
  12. 1 1
      src/constant/help-style.ts
  13. 4 1
      src/core/components/arrow/arrow.vue
  14. 5 5
      src/core/components/rectangle/index.ts
  15. 3 1
      src/core/components/rectangle/rectangle.vue
  16. 4 3
      src/core/components/rectangle/temp-rectangle.vue
  17. 2 8
      src/core/components/text/index.ts
  18. 1 3
      src/core/components/text/temp-text.vue
  19. 8 1
      src/core/components/text/text-dom.vue
  20. 53 30
      src/core/components/text/text.vue
  21. 8 17
      src/core/components/text/util.ts
  22. 6 7
      src/core/components/triangle/index.ts
  23. 1 1
      src/core/components/triangle/temp-triangle.vue
  24. 1 0
      src/core/components/triangle/triangle.vue
  25. 8 3
      src/core/helper/active-boxs.vue
  26. 18 40
      src/core/history.ts
  27. 7 0
      src/core/hook/use-animation.ts
  28. 2 0
      src/core/hook/use-expose.ts
  29. 1 1
      src/core/hook/use-snap.ts
  30. 2 1
      src/core/propertys/components/checkbox.vue
  31. 20 5
      src/core/propertys/components/color.vue
  32. 5 1
      src/core/propertys/components/num.vue
  33. 6 6
      src/core/propertys/components/proportion.vue
  34. 2 1
      src/core/propertys/components/select.vue
  35. 53 1
      src/core/propertys/describes.json
  36. 8 1
      src/core/propertys/mount.vue
  37. 1 1
      src/core/renderer/group.vue
  38. 9 1
      src/core/renderer/renderer.vue
  39. 85 72
      src/core/store/index.ts
  40. 81 0
      src/core/store/store.ts
  41. 0 0
      src/example/fuse/App.vue
  42. 0 4
      example/fuse/main.ts
  43. 0 0
      src/example/fuse/styles/global.scss
  44. 114 0
      src/example/fuse/views/header/header.vue
  45. 11 6
      example/fuse/views/home.vue
  46. 4 4
      src/core/store/init.ts
  47. 122 0
      src/example/fuse/views/slide/menu.ts
  48. 0 0
      src/example/fuse/views/slide/slide-item.vue
  49. 1 1
      example/fuse/views/slide/slide.vue
  50. 2 2
      example/fuse/views/use-draw.ts
  51. 3 1
      src/index.ts

+ 1 - 1
.env

@@ -1,3 +1,3 @@
 VITE_PRIMARY='#D8000A'
 VITE_TITLE='绘图'
-VITE_ENTRY='example/fuse/main.ts'
+VITE_ENTRY='src/example/fuse/main.ts'

+ 0 - 3
example/fuse/store/index.ts

@@ -1,3 +0,0 @@
-import { createPinia } from 'pinia'
-
-export const pinia = createPinia()

+ 0 - 91
example/fuse/views/header/funds.ts

@@ -1,91 +0,0 @@
-import { useDraw } from "../use-draw.ts";
-
-export const revoke = () => {
-
-}
-
-export const recover = () => {
-
-}
-
-export const clear = () => {
-
-}
-
-export const rotate = () => {
-
-}
-
-export const full = () => {
-
-}
-
-export const aiImport = () => {
-
-}
-
-export const setBGImage = () => {
-
-}
-
-export const gotoVR = () => {
-
-}
-
-export const saveData = () => {
-}
-
-export const exportData = () => {
-}
-
-export const gotoDrawing = () => {
-}
-
-import bgImage from '../../assets/WX20241031-111850.png'
-export const useHeaderFunds = () => {
-	const draw = useDraw()
-	const setBGImage = () => {
-		draw.value!.addShape('image', { url: bgImage, width: 1, height: 1 }, {x: window.innerWidth / 2, y: window.innerHeight / 2}, true );
-	}
-	const toggleHit = () => {
-		draw.value?.toggleHit()
-	}
-
-
-	const describes = new Map<Function, { name: string, icon: string }>([
-		[revoke, {name: '撤销', icon: ''}],
-		[recover, {name: '撤销', icon: ''}],
-		[rotate, {name: '旋转', icon: ''}],
-		[clear, {name: '清除', icon: ''}],
-		[full, {name: '全屏', icon: ''}],
-		[aiImport, {name: 'ai导入', icon: ''}],
-		[
-			setBGImage,
-			{name: '背景图', icon: ''}
-		],
-		[gotoVR, {name: 'VR', icon: ''}],
-		[saveData, {name: '保存', icon: ''}],
-		[exportData, {name: '导出', icon: ''}],
-		[gotoDrawing, {name: '图纸', icon: ''}],
-	])
-
-	if (import.meta.env.DEV) {
-		describes.set(toggleHit, {name: '显示hit', icon: ''})
-	}
-
-	return {
-		revoke,
-		recover,
-		rotate,
-		clear,
-		full,
-		aiImport,
-		setBGImage,
-		gotoVR,
-		saveData,
-		exportData,
-		gotoDrawing,
-		toggleHit,
-		describes,
-	}
-}

+ 0 - 64
example/fuse/views/header/header.vue

@@ -1,64 +0,0 @@
-<template>
-  <div class="header">
-    <div class="nav">
-      <el-button type="primary" plain>返回</el-button>
-    </div>
-    <div class="draw-operate">
-      <div v-for="group in groups">
-        <el-icon class="operate" v-for="fn in group" @click="fn()">
-          {{ funds.describes.get(fn)?.name }}
-        </el-icon>
-      </div>
-    </div>
-    <div class="saves">
-      <el-button type="primary" plain>保存</el-button>
-      <el-button type="primary" plain>导出</el-button>
-      <el-button>图纸</el-button>
-    </div>
-  </div>
-</template>
-
-<script lang="ts" setup>
-import { ElButton, ElIcon } from "element-plus";
-import { useHeaderFunds } from "./funds.ts";
-const funds = useHeaderFunds();
-const groups = [
-  [funds.revoke, funds.recover],
-  [funds.clear, funds.rotate, funds.full],
-  [funds.aiImport, funds.setBGImage, funds.gotoVR],
-];
-if (import.meta.env.DEV) {
-  groups.push([funds.toggleHit]);
-}
-</script>
-
-<style lang="scss" scoped>
-@use 'element-plus/theme-chalk/src/common/var';
-
-.header {
-  background-color: var.$color-primary;
-  display: flex;
-  align-items: center;
-  padding: 10px;
-  justify-content: space-between;
-}
-
-.draw-operate {
-  text-align: center;
-  color: #fff;
-  display: flex;
-  align-items: center;
-
-  > div:not(:last-child) {
-    padding-right: 10px;
-    margin-right: 10px;
-    border-right: 1px solid #fff;
-  }
-
-  i {
-    width: auto;
-    margin: 0 5px;
-  }
-}
-</style>
-/

+ 0 - 2
example/fuse/views/header/index.ts

@@ -1,2 +0,0 @@
-export * from './funds.ts'
-export { default as Header } from './header.vue'

+ 0 - 4
example/fuse/views/slide/index.ts

@@ -1,4 +0,0 @@
-export { default as Slide } from './slide.vue';
-export { default as SlideItem } from './slide-item.vue';
-export { menus } from './menu.ts'
-export type { MenuItem } from './menu.ts'

+ 0 - 101
example/fuse/views/slide/menu.ts

@@ -1,101 +0,0 @@
-import { ShapeType, shapeNames, PresetAdd } from '@/index.ts'
-import { v4 as uuid } from 'uuid'
-
-
-export type MenuItem = {
-	icon: string,
-	name: string,
-	value?: string,
-	children?: MenuItem[]
-	payload?: PresetAdd
-}
-
-const genItem = <T extends ShapeType>(type: T, preset: PresetAdd<T>['preset'] = {}) => ({
-	name: shapeNames[type],
-	value: uuid(),
-	payload: {type, preset}
-})
-
-export const getItem = (value: string, queryMenus = menus): MenuItem | undefined => {
-	for (const menu of queryMenus) {
-		const eqItem = menu.value === value
-			? menu
-			: menu.children?.length ? getItem(value, menu.children) : void 0
-		if (eqItem) return eqItem
-	}
-}
-
-const eqPayload = (p1?: PresetAdd, p2?: PresetAdd) => {
-	if (!p2 || !p1 || p1.type !== p2.type) return false;
-	return !p1.preset || !p2.preset ||  toRaw(p1.preset) === toRaw(p2.preset)
-}
-
-export const getValue = (payload: PresetAdd, queryMenus = menus): string | undefined => {
-	for (const menu of queryMenus) {
-		const eqItem = eqPayload(menu.payload, payload)
-			? menu.value
-			: menu.children?.length ? getValue(payload, menu.children) : void 0
-		if (eqItem) return eqItem
-	}
-}
-
-
-import svg1 from '../../assets/icons/vue.svg'
-import svg2 from '../../assets/icons/BedsideCupboard.svg'
-import { toRaw } from "vue";
-
-export const menus: MenuItem[] = [
-	{
-		icon: '',
-		name: '绘制',
-		value: uuid(),
-		children: [
-			{
-				icon: '',
-				...genItem('line')
-			},
-			{
-				icon: '',
-				...genItem('arrow')
-			},
-
-			{
-				icon: '',
-				...genItem('rectangle')
-			},
-			{
-				icon: '',
-				...genItem('circle')
-			},
-			{
-				icon: '',
-				...genItem('triangle')
-			},
-			{
-				icon: '',
-				...genItem('polygon')
-			},
-		]
-	},
-	{
-		icon: '',
-		name: '图例',
-		value: uuid(),
-		children: [
-			{
-				icon: '',
-				...genItem('icon', { url: svg1, width: 500, height: 500 }),
-				name: 'vue'
-			},
-			{
-				icon: '',
-				...genItem('icon', { url: svg2, width: 300, height: 300, stroke: 'red', strokeWidth: 1, strokeScaleEnabled: false }),
-				name: '自定义'
-			}
-		]
-	},
-	{
-		icon: '',
-		...genItem('text')
-	},
-]

example/fuse/assets/WX20241031-111850.png → public/WX20241031-111850.png


example/fuse/assets/icons/BedsideCupboard.svg → public/icons/BedsideCupboard.svg


example/fuse/assets/icons/vue.svg → public/icons/vue.svg


+ 44 - 0
src/app.vue

@@ -0,0 +1,44 @@
+<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>

+ 1 - 1
src/constant/help-style.ts

@@ -1,6 +1,6 @@
 import { getMouseColors } from "../utils/colors.ts"
 
-export const themeColor = '#D8000A'
+export const themeColor = import.meta.env.VITE_PRIMARY
 export const themeMouseColors = getMouseColors(themeColor)
 
 

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

@@ -24,7 +24,10 @@ const emit = defineEmits<{
   (e: "delShape"): void;
 }>();
 
-const { shape, tData, operateMenus, describes } = useComponentStatus<Arrow, ArrowData>({
+const { shape, tData, operateMenus, describes, data } = useComponentStatus<
+  Arrow,
+  ArrowData
+>({
   emit,
   props,
   getMouseStyle,

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

@@ -13,18 +13,18 @@ export const defaultStyle = {
   dash: [1, 0],
   strokeWidth: 1,
   stroke: themeMouseColors.theme,
-  fill: "#fff",
 };
 
 export const getMouseStyle = (data: RectangleData) => {
-  const fillStatus = getMouseColors(data.fill || defaultStyle.fill);
+  const fillStatus = data.fill && getMouseColors(data.fill);
   const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
   const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
 
+  console.log(fillStatus)
   return {
-    default: { fill: fillStatus.pub, stroke: strokeStatus.pub, strokeWidth },
-    hover: { fill: fillStatus.hover },
-    press: { fill: fillStatus.press },
+    default: { fill: fillStatus && fillStatus.pub, stroke: strokeStatus.pub, strokeWidth },
+    hover: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
+    press: { fill: fillStatus && fillStatus.press, stroke: strokeStatus.press },
   };
 };
 

+ 3 - 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 { watchEffect } from "vue";
 
 const props = defineProps<{ data: RectangleData }>();
 const emit = defineEmits<{
@@ -27,10 +28,11 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   props,
   getMouseStyle,
   defaultStyle,
-  transformType: 'line',
+  transformType: "line",
   copyHandler(tf, data) {
     data.points = data.points.map((v) => tf.point(v));
     return data;
   },
+  propertys: ["fill", "stroke", "strokeWidth", "dash", "opacity", "ref", "zIndex"],
 });
 </script>

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

@@ -5,7 +5,7 @@
       zIndex: undefined,
       closed: true,
       points: flatPositions(data.points),
-      opacity: addMode ? 0.3 : 1,
+      opacity: addMode ? 0.3 : data.opacity,
     }"
     ref="shape"
   />
@@ -14,10 +14,11 @@
 <script lang="ts" setup>
 import { flatPositions } from "@/utils/shared.ts";
 import { RectangleData } from "./index.ts";
-import { ref } from "vue";
+import { ref, watchEffect } from "vue";
 import { DC } from "@/deconstruction.js";
 import { Line } from "konva/lib/shapes/Line";
-defineProps<{ data: RectangleData; addMode?: boolean }>();
+const props = defineProps<{ data: RectangleData; addMode?: boolean }>();
+
 
 const shape = ref<DC<Line>>();
 defineExpose({

+ 2 - 8
src/core/components/text/index.ts

@@ -15,6 +15,8 @@ export const defaultStyle = {
   // strokeWidth: 0,
   fontFamily: 'Calibri',
   fontSize: 16,
+  align: 'center',
+  fontStyle: "normal"
 };
 
 export const addMode = 'dot'
@@ -48,14 +50,6 @@ export const getSnapInfos = (data: TextData) => {
   return generateSnapInfos(points, true, false);
 };
 
-export const dataToConfig = (data: TextData): TextConfig => ({
-  ...defaultStyle,
-  ...data,
-  align: 'center',
-  verticalAlign: 'center',
-  text: data.content
-});
-
 export const interactiveToData = (
   info: InteractiveMessage,
   preset: Partial<TextData> = {}

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

@@ -3,11 +3,10 @@
     :config="{
       ...data,
       ...matConfig,
-      align: 'center',
       verticalAlign: 'center',
       text: data.content,
       zIndex: undefined,
-      opacity: addMode ? 0.3 : 1,
+      opacity: addMode ? 0.3 : data.opacity,
     }"
     ref="shape"
     name="text"
@@ -29,7 +28,6 @@ defineExpose({
     return shape.value;
   },
 });
-
 const matConfig = computed(() => {
   return new Transform(props.data.mat).decompose();
 });

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

@@ -103,7 +103,14 @@ const refresh = () => {
 };
 
 const transform = useViewerTransform();
-watchEffect(() => props.shape.text(text.value), { flush: "sync" });
+watch(
+  text,
+  () => {
+    props.shape.text(text.value);
+    props.shape.fire("bound-change");
+  },
+  { flush: "sync" }
+);
 watch(
   () => [textarea.value, text.value, transform.value],
   () => textarea.value && refresh()

+ 53 - 30
src/core/components/text/text.vue

@@ -13,7 +13,7 @@
     :target="shape"
     @change="emit('updateShape', { ...data })"
   />
-  <!-- <Operate :target="shape" :menus="operateMenus" v-if="!editText" /> -->
+  <Operate :target="shape" :menus="operateMenus" v-if="!editText" />
 </template>
 
 <script lang="ts" setup>
@@ -29,7 +29,7 @@ import {
 } from "@/core/hook/use-transformer.ts";
 import { Transform } from "konva/lib/Util";
 import { Text } from "konva/lib/shapes/Text";
-import { computed, ref, shallowRef, watchEffect } from "vue";
+import { computed, ref, shallowRef, watch, watchEffect } from "vue";
 import { setShapeTransform } from "@/utils/shape.ts";
 import { zeroEq } from "@/utils/math.ts";
 import { MathUtils } from "three";
@@ -42,7 +42,6 @@ const emit = defineEmits<{
   (e: "delShape"): void;
 }>();
 
-const minWidth = computed(() => (props.data.fontSize || 12) * 2);
 const { shape, tData, data, operateMenus, describes } = useComponentStatus<
   Text,
   TextData
@@ -53,30 +52,6 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
   defaultStyle,
   transformType: "custom",
   customTransform(callback, shape, data) {
-    const update = (mat: Transform, data: TextData) => {
-      const { scaleX, x, y, rotation } = mat.decompose();
-      if (!zeroEq(scaleX - 1)) {
-        let width: number | undefined;
-        if ("width" in data) {
-          width = Math.max(data.width! * scaleX, minWidth.value);
-        } else {
-          width = Math.max(shape.value!.getNode()!.width() * scaleX, minWidth.value);
-        }
-
-        return {
-          width,
-          mat: new Transform()
-            .translate(x, y)
-            .rotate(MathUtils.degToRad(rotation))
-            .scale(1, 1).m,
-        };
-      } else {
-        return {
-          mat: mat.m,
-        };
-      }
-    };
-
     useCustomTransformer(shape, data, {
       openSnap: true,
       getRepShape($shape) {
@@ -91,11 +66,9 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
       },
       transformerConfig: {
         rotateEnabled: true,
-        keepRatio: true,
         enabledAnchors: ["middle-left", "middle-right"],
-        flipEnabled: false,
         boundBoxFunc: (oldBox, newBox) => {
-          if (Math.abs(newBox.width) < minWidth.value) {
+          if (newBox.width - minWidth.value < -0.01) {
             return oldBox;
           }
           return newBox;
@@ -120,8 +93,58 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
       mat: tf.multiply(new Transform(data.mat)).m,
     };
   },
+  propertys: [
+    "fill",
+    "stroke",
+    "strokeWidth",
+    "align",
+    "fontSize",
+    "fontStyle",
+    "dash",
+    "opacity",
+    "ref",
+    "zIndex",
+  ],
 });
 
+const minWidth = computed(() => (data.value.fontSize || 12) * 2);
+const getWidth = (data: TextData, scaleX: number) => {
+  let width: number;
+  if ("width" in data) {
+    width = Math.max(data.width! * scaleX, minWidth.value);
+  } else {
+    width = Math.max(shape.value!.getNode()!.width() * scaleX, minWidth.value);
+  }
+  return width;
+};
+
+const update = (mat: Transform, data: TextData) => {
+  const { scaleX, x, y, rotation } = mat.decompose();
+  if (!zeroEq(scaleX - 1)) {
+    return {
+      width: getWidth(data, scaleX),
+      mat: new Transform()
+        .translate(x, y)
+        .rotate(MathUtils.degToRad(rotation))
+        .scale(1, 1).m,
+    };
+  } else {
+    return {
+      mat: mat.m,
+    };
+  }
+};
+
+// 字体大小变化时,更新width
+watch(
+  () => data.value.fontSize,
+  () => {
+    data.value.width = getWidth(data.value, 1);
+    const $shape = shape.value?.getNode();
+    $shape && $shape.fire("bound-change");
+  }
+);
+
 watchEffect((oncleanup) => {
   if (shape.value?.getNode()) {
     textNodeMap[props.data.id] = shape.value.getNode();

+ 8 - 17
src/core/components/text/util.ts

@@ -1,5 +1,4 @@
 import { useStage } from "@/core/hook/use-global-vars";
-import { useUnitTransform } from "@/core/hook/use-viewer";
 import { Text } from "konva/lib/shapes/Text";
 
 export const useGetPointerTextNdx = () => {
@@ -12,7 +11,7 @@ export const useGetPointerTextNdx = () => {
       return str.length;
     }
 
-    const transform = shape.getAbsoluteTransform().invert()
+    const transform = shape.getAbsoluteTransform().copy().invert()
     const textPos = {x: 0, y: 0}
     const finalPos = transform.point({
       x: pointerPos.x - textPos.x,
@@ -27,11 +26,16 @@ export const useGetPointerTextNdx = () => {
 
     if (lineNdx >= textArr.length || lineNdx < 0) return ndx;
 
-    console.log(textArr, lineNdx)
     const line = textArr[lineNdx];
     const hanlfSize = shape.fontSize() / 2
     let i = 0;
-    let x = (width - line.width) / 2;
+    let x = 0;
+    if (shape.align() === 'center') {
+      x = (width - line.width) / 2;
+    } else if (shape.align() === 'right') {
+      x = width - line.width;
+    }
+
     let after = false;
     for (; i < line.text.length; i++) {
       const size = shape.measureSize(line.text[i]);
@@ -45,7 +49,6 @@ export const useGetPointerTextNdx = () => {
         break;
       }
     }
-    console.log(i, after)
     if (i === line.text.length) {
       ndx = line.text.length - 1;
       after = true;
@@ -82,18 +85,6 @@ export const useGetPointerTextNdx = () => {
       }
     }
     ndx = strNdx + emptyCount + i + (after ? 1 : 0);
-    if (import.meta.env.DEV) {
-      console.log(
-        strNdx,
-        ndx,
-        emptyCount,
-        str.substring(strNdx + emptyCount),
-        i,
-        char,
-        yStr,
-        yStr[i]
-      );
-    }
     return ndx;
   };
 };

+ 6 - 7
src/core/components/triangle/index.ts

@@ -11,25 +11,24 @@ export const shapeName = "三角形";
 export const defaultStyle = {
   stroke: themeMouseColors.theme,
   strokeWidth: 5,
-  fill: "#fff",
 };
 
 export const addMode = "area";
 
 export const getMouseStyle = (data: TriangleData) => {
-  const fillStatus = getMouseColors(data.fill || defaultStyle.fill);
+  const fillStatus = data.fill && getMouseColors(data.fill);
   const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
-  const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
+  const strokeWidth = data.strokeWidth ;
 
   return {
-    default: { fill: fillStatus.pub, stroke: strokeStatus.pub, strokeWidth },
-    hover: { fill: fillStatus.hover },
-    press: { fill: fillStatus.press },
+    default: { fill: fillStatus && fillStatus.pub, stroke: strokeStatus.pub, strokeWidth },
+    hover: { fill: fillStatus && fillStatus.hover, stroke: strokeStatus.hover },
+    press: { fill: fillStatus && fillStatus.press, stroke: strokeStatus.press },
   };
 };
 
 export type TriangleData = Partial<typeof defaultStyle> &
-  BaseItem & { points: Pos[]; attitude: number[] };
+  BaseItem & { points: Pos[]; attitude: number[], fill?: string };
 
 export const getSnapInfos = (data: TriangleData) => generateSnapInfos(data.points, true, false)
 

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

@@ -5,7 +5,7 @@
       zIndex: undefined,
       closed: true,
       points: flatPositions(data.points),
-      opacity: addMode ? 0.3 : 1,
+      opacity: addMode ? 0.3 : data.opacity,
     }"
     ref="shape"
   />

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

@@ -32,5 +32,6 @@ const { shape, tData, operateMenus, describes } = useComponentStatus({
     data.points = data.points.map((v) => tf.point(v));
     return data;
   },
+  propertys: ["fill", "stroke", "strokeWidth", "dash", "opacity", "ref", "zIndex"],
 });
 </script>

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

@@ -38,8 +38,10 @@ const updateBox = ($shape: EntityShape) => {
   });
 };
 
+type OnCleanup = (cleanupFn: () => void) => void;
+
 const transformer = useTransformer();
-const shapeListener = (shape: EntityShape) => {
+const shapeListener = (shape: EntityShape, onCleanup: OnCleanup) => {
   watchEffect((onCleanup) => {
     transformer.on("transform.helper-box", async () => {
       await nextTick();
@@ -47,11 +49,14 @@ const shapeListener = (shape: EntityShape) => {
     });
     onCleanup(() => transformer.off("transform.helper-box"));
   });
+  const handler = () => updateBox(shape);
+  shape.on("bound-change", handler);
+  onCleanup(() => shape.off("bound-change", handler));
 };
 
 watchEffect(
-  () => {
-    status.actives.forEach(shapeListener);
+  (onCleanup) => {
+    status.actives.forEach((shape) => shapeListener(shape, onCleanup));
   },
   { flush: "pre" }
 );

+ 18 - 40
src/core/history.ts

@@ -1,5 +1,5 @@
 import { History } from "stateshot";
-import { inRevise } from "../utils/shared.ts";
+import { computed, reactive, ref } from "vue";
 
 export type HistoryState = {
 	hasUndo: boolean;
@@ -9,28 +9,18 @@ export type HistoryState = {
 export type StateCallback = (state: HistoryState) => void;
 
 export class SingleHistory<T = any> {
-	stateChange: StateCallback;
 	history: History<{ data: T }>;
-	state = {
-		hasUndo: false,
-		hasRedo: false,
-	};
+	hasUndo = ref(false)
+	hasRedo = ref(false)
 
-	constructor(stateChange: StateCallback) {
-		this.stateChange = stateChange;
+	constructor() {
 		this.history = new History();
 	}
 
-	private prevState: HistoryState | null = null;
 	private syncState() {
 		if (!this.history) return;
-		this.state.hasRedo = this.history.hasRedo;
-		this.state.hasUndo = this.history.hasUndo;
-
-		if (inRevise(this.state, this.prevState)) {
-			this.stateChange({ ...this.state });
-			this.prevState = { ...this.state };
-		}
+		this.hasRedo.value = this.history.hasRedo;
+		this.hasUndo.value = this.history.hasUndo;
 	}
 
 	get data() {
@@ -66,54 +56,42 @@ export class SingleHistory<T = any> {
 
 
 export class MergeHistory<T> {
-	stateChange: StateCallback;
-	historyStack: SingleHistory<T>[] = [];
-
-	constructor(stateChange: StateCallback) {
-		this.stateChange = stateChange;
-	}
-
-	get current() {
-		return this.historyStack[this.historyStack.length - 1];
-	}
+	historyStack: SingleHistory<T>[] = reactive([]);
+	current = computed(() => this.historyStack[this.historyStack.length - 1]);
+	hasUndo = computed(() => this.current.value.hasUndo)
+	hasRedo = computed(() => this.current.value.hasRedo)
 
-	private prevState: HistoryState | null = null;
 	branch() {
-		const single = new SingleHistory((state) => {
-			if (inRevise(this.prevState, state)) {
-				this.stateChange({ ...state });
-				this.prevState = { ...state };
-			}
-		});
+		const single = new SingleHistory();
 		this.historyStack.push(single);
 	}
 
 	merge() {
 		const lastStack = this.historyStack.pop()!;
-		if (lastStack.state.hasUndo) {
-			this.current.push(lastStack.data);
+		if (lastStack.hasUndo.value) {
+			this.current.value.push(lastStack.data);
 		}
 		lastStack.clear();
 	}
 
 	undo() {
-		return this.current.undo();
+		return this.current.value.undo();
 	}
 
 	redo() {
-		return this.current.redo();
+		return this.current.value.redo();
 	}
 
 	push(data: T) {
-		return this.current.push(data);
+		return this.current.value.push(data);
 	}
 
 	clear() {
-		return this.current.clear();
+		return this.current.value.clear();
 	}
 
 	get data() {
-		return this.current.data;
+		return this.current.value.data;
 	}
 
 	destory() {

+ 7 - 0
src/core/hook/use-animation.ts

@@ -35,6 +35,13 @@ const animation = <T extends object>(origin: T, target: T) => {
   const tOrigin = {...origin}
   const tTarget = {...target}
 
+  for (const key in oColors) {
+    if (!(key in tColors)) {
+      ;(origin as any)[key] = null
+      delete oColors[key]
+    }
+  }
+
   for (const key in tOrigin) {
     if (typeof tOrigin[key] === 'string') {
       delete tOrigin[key]

+ 2 - 0
src/core/hook/use-expose.ts

@@ -12,6 +12,7 @@ export const useExpose = () => {
 	const interactiveProps = useInteractiveProps()
 	const stage = useStage()
 	const layers = useLayers()
+	const store = useStage()
 
 	const exposeBlob = (config?: PickParams<'toBlob', 'callback'>) => {
 		const $stage = stage.value!.getStage()
@@ -31,6 +32,7 @@ export const useExpose = () => {
 		...useInteractiveShapeAPI(),
 		exposeBlob,
 		toggleHit,
+		store,
 		mode,
 		presetAdd: interactiveProps,
 	})

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

@@ -44,7 +44,7 @@ export const useGlobalSnapInfos = installGlobalVar(() => {
     const comp = components[type];
     if (!("getSnapInfos" in comp)) continue;
     watch(
-      () => store[type],
+      () => store.data[type],
       (items) => {
         if (!items) return;
         for (const item of items) {

+ 2 - 1
src/core/propertys/components/checkbox.vue

@@ -1,6 +1,7 @@
 <template>
   <el-checkbox
     @update:model-value="(val: any) => $emit('update:value', val)"
+    @change="$emit('change')"
     :model-value="value"
     :label="label"
     style="height: auto"
@@ -14,5 +15,5 @@ defineProps<{
   value?: boolean;
   label?: string;
 }>();
-defineEmits<{ (e: "update:value", val: boolean): void }>();
+defineEmits<{ (e: "update:value", val: boolean): void; (e: "change"): void }>();
 </script>

+ 20 - 5
src/core/propertys/components/color.vue

@@ -1,11 +1,12 @@
 <template>
   <el-color-picker
     :modelValue="value"
-    @active-change="changeHandler"
-    @update:model-value="changeHandler"
+    @active-change="inputHandler"
+    @update:model-value="inputHandler"
     popper-class="com-color-pick"
     size="small"
     :predefine="predefineColors"
+    @blur="changeHandler"
   />
 </template>
 
@@ -15,11 +16,25 @@ import { ElColorPicker } from "element-plus";
 
 defineProps<{ value?: string }>();
 
-const emit = defineEmits<{ (e: "update:value", val: string | null): string }>();
-const changeHandler = (color: string | null) => {
-  console.log(color);
+const emit = defineEmits<{
+  (e: "update:value", val: string | null): string;
+  (e: "change"): void;
+}>();
+
+let change = false;
+const inputHandler = (color: string | null) => {
   emit("update:value", color);
+  console.log(color);
+  change = true;
 };
+
+const changeHandler = () => {
+  if (change) {
+    emit("change");
+    change = false;
+  }
+};
+
 const predefineColors = ref([
   "#ff4500",
   "#ff8c00",

+ 5 - 1
src/core/propertys/components/num.vue

@@ -3,6 +3,7 @@
     class="property-num-slider"
     :modelValue="value"
     @update:model-value="(val: any) => changeHandler(val)"
+    @change="$emit('change')"
     size="small"
     height="200px"
     width="50px"
@@ -23,7 +24,10 @@ defineProps<{
   max?: number;
   step?: number;
 }>();
-const emit = defineEmits<{ (e: "update:value", val: number): void }>();
+const emit = defineEmits<{
+  (e: "update:value", val: number): void;
+  (e: "change"): void;
+}>();
 const changeHandler = (val: number) => {
   emit("update:value", val);
 };

+ 6 - 6
src/core/propertys/components/proportion.vue

@@ -4,6 +4,7 @@
     :modelValue="showValue"
     @update:model-value="(val: any) => changeHandler(val)"
     size="small"
+    @change="$emit('change')"
     height="200px"
     width="50px"
     style="cursor: pointer"
@@ -25,17 +26,16 @@ const props = withDefaults(
   }>(),
   { scale: 1 }
 );
-const emit = defineEmits<{ (e: "update:value", val: number[]): void }>();
+const emit = defineEmits<{
+  (e: "update:value", val: number[]): void;
+  (e: "change"): void;
+}>();
 
 const showValue = computed(() => {
   if (!props.value) {
     return 1 * props.scale;
   }
-  if (!props.value[1]) {
-    return 1 * props.scale;
-  } else {
-    return props.value[1];
-  }
+  return props.value[0];
 });
 
 const changeHandler = (val: number) => {

+ 2 - 1
src/core/propertys/components/select.vue

@@ -4,6 +4,7 @@
     @update:model-value="(value) => $emit('update:value', value)"
     placeholder="选择"
     size="small"
+    @change="$emit('change')"
     style="width: 100px"
   >
     <el-option
@@ -25,5 +26,5 @@ defineProps<{
   max?: number;
   step?: number;
 }>();
-defineEmits<{ (e: "update:value", val: number): void; (e: "click"): void }>();
+defineEmits<{ (e: "update:value", val: number): void; (e: "change"): void }>();
 </script>

+ 53 - 1
src/core/propertys/describes.json

@@ -81,6 +81,58 @@
       "max": 10
     }
   },
+  "align": {
+    "type": "select",
+    "label": "对齐方式",
+    "props": {
+      "options": [
+        {
+          "label": "居中对齐",
+          "value": "center"
+        },
+        {
+          "label": "左对齐",
+          "value": "left"
+        },
+        {
+          "label": "右对齐",
+          "value": "right"
+        }
+      ]
+    }
+  },
+  "fontSize": {
+    "type": "num",
+    "label": "字体大小",
+    "props": {
+      "min": 12,
+      "max": 100
+    }
+  },
+  "fontStyle": {
+    "type": "select",
+    "label": "字体样式",
+    "props": {
+      "options": [
+        {
+          "label": "默认",
+          "value": "normal"
+        },
+        {
+          "label": "斜体",
+          "value": "italic"
+        },
+        {
+          "label": "粗体",
+          "value": "bold"
+        },
+        {
+          "label": "粗斜体",
+          "value": "italic bold"
+        }
+      ]
+    }
+  },
   "dash": {
     "type": "proportion",
     "label": "虚线比例",
@@ -88,7 +140,7 @@
       "scale": 30
     },
     "default": [
-      0,
+      30,
       30
     ]
   },

+ 8 - 1
src/core/propertys/mount.vue

@@ -23,7 +23,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed } from "vue";
+import { computed, watch } from "vue";
 import { useStage, useTransformIngShapes } from "../hook/use-global-vars.ts";
 import { PropertyDescribes, propertyComponents } from "./index.ts";
 import { DC, EntityShape } from "@/deconstruction.js";
@@ -59,6 +59,13 @@ const updateValue = (key: string, val: any) => {
   }
   isUpdate = true;
 };
+
+watch(hidden, (nHidden, oHidden) => {
+  if (nHidden && nHidden !== oHidden && isUpdate) {
+    isUpdate = false;
+    emit("change");
+  }
+});
 </script>
 
 <style lang="scss" scoped>

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

@@ -19,5 +19,5 @@ const props = defineProps<{ type: ShapeType }>();
 const store = useStore();
 const type = props.type as "arrow";
 const ShapeComponent = components[type].Component;
-const items = computed(() => store[type] || []);
+const items = computed(() => store.data[type] || []);
 </script>

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

@@ -29,7 +29,7 @@
 <script lang="ts" setup>
 import ShapeGroup from "./group.vue";
 import TempShapeGroup from "./draw-group.vue";
-import { ShapeType, components } from "../components";
+import { DrawData, ShapeType, components } from "../components";
 import { useStage } from "../hook/use-global-vars.ts";
 import { useViewerTransformConfig } from "../hook/use-viewer.ts";
 import { useListener, useResize } from "../hook/use-event.ts";
@@ -38,6 +38,13 @@ import { useInteractiveShapeAPI } from "../hook/use-interactive.ts";
 import { DomMountId } from "../../constant";
 import ActiveBoxs from "../helper/active-boxs.vue";
 import SnapLines from "../helper/snap-lines.vue";
+import { useStore } from "../store/index.ts";
+
+const props = defineProps<{
+  data: DrawData;
+}>();
+const store = useStore();
+store.setStore(props.data);
 
 const stage = useStage();
 const size = useResize();
@@ -62,6 +69,7 @@ defineExpose(useExpose());
 .draw-layout {
   width: 100%;
   height: 100%;
+  overflow: hidden;
 }
 .mount-mask {
   position: absolute;

+ 85 - 72
src/core/store/index.ts

@@ -1,77 +1,90 @@
-import { defineStore } from "pinia";
-import { DrawData, DrawItem, ShapeType } from '../components'
-import { initData } from "./init";
+import { DrawData } from "../components";
+import { SingleHistory } from "../history";
+import { installGlobalVar } from "../hook/use-global-vars";
+import { useStoreRaw } from "./store";
 
-const sortFn = (a: Pick<DrawItem, 'zIndex' | 'createTime'>, b: Pick<DrawItem, 'zIndex' | 'createTime'>) => 
-	a.zIndex - b.zIndex || a.createTime - b.createTime
+type Store = ReturnType<typeof useStoreRaw>
 
-export const useStore = defineStore('draw-data', {
-	state: (): DrawData => (initData),
-	getters: {
-		items() {
-			return Object.values((this as any).$state).flat() as DrawItem[]
-		},
-		sortItems() {
-			return (this.items as any).sort(sortFn) as DrawItem[]
+class DrawHistory extends SingleHistory<string> {
+	preventFlag = false;
+	onceFlag = false;
+	onceHistory: string | null = null
+	initData: string | null = null
+	renderer: (data: string) => void;
+
+	constructor(renderer: (data: string) => void) {	
+		super()
+		this.renderer = renderer
+	}
+
+	init(data: string) {
+		this.initData = data
+	}
+
+	preventTrack(fn: () => void) {
+		this.preventFlag = true;
+		fn();
+		this.preventFlag = false;
+	}
+
+	push(data: string) {
+		if (!this.preventFlag) return;
+		if (this.onceFlag) {
+			this.onceHistory = data
+		} else {
+			super.push(data)
 		}
-	},
-	actions: {
-		setStore(store: DrawData) {
-			this.$patch(store);
-		},
-		getItemNdx<T extends ShapeType>(type: T, id: string) {
-			const items = this.$state[type]
-			if (items) {
-				return items.findIndex(item => item.id === id)
-			}
-			return -1
-		},
-		getItem<T extends ShapeType>(type: T, id: string) {
-			const ndx = this.getItemNdx(type, id)
-			if (~ndx) return this.$state[type]![ndx]
-		},
-		addItems<T extends ShapeType>(type: T, items: DrawItem<T>[]) {
-			this.$patch((state: DrawData) => {
-				if (!(type in state)) {
-					state[type] = []
-				}
-				state[type]!.push(...items);
-			})
-		},
-		addItem<T extends ShapeType>(type: T, item: DrawItem<T>) {
-			this.addItems(type, [item])
-		},
-		delItem<T extends ShapeType>(type: T, id: string) {
-			const ndx = this.getItemNdx(type, id)
-			if (~ndx) {
-				this.$patch(state => {
-					state[type]!.splice(ndx, 1)
-				})
-			}
-		},
-		setItem<T extends ShapeType>(type: T, playData: { value: DrawItem<T>, id: string }) {
-			const ndx = this.getItemNdx(type, playData.id)
-			console.log(JSON.stringify(playData.value))
-			if (~ndx) {
-				this.$patch(state => {
-					Object.assign(state[type]![ndx], playData.value)
-				})
-			}
-		},
-		getItemsZIndex(items?: DrawItem[]) {
-			if (!items) {
-				return this.sortItems;
-			} else {
-				return items.sort(sortFn)
-			}
-		},
-		getType(id: string) {
-			const types = Object.keys(this.$state) as ShapeType[]
-			for (const type of types) {
-				if (this.$state[type]?.some(item=> item.id === id)) {
-					return type
-				}
-			}
+	}
+
+	redo() {
+		const data = super.redo()
+		this.renderer(data)
+		return data;
+	}
+
+	undo() {
+		const data = super.undo()
+		this.renderer(data)
+		return data;
+	}
+
+	onceTrack(fn: () => void) {
+		this.onceFlag = true;
+		fn();
+		if (this.onceHistory) {
+			this.push(this.onceHistory)
+			this.onceHistory = null
 		}
+		this.onceFlag = false;
 	}
-})
+
+	clear(): void {
+		super.clear()
+		this.initData && this.push(this.initData)
+	}
+}
+
+
+export const useStore = installGlobalVar(() => {
+  const store = useStoreRaw() as Store & { history: DrawHistory; };
+	const history = new DrawHistory((data) => {
+		store.data = JSON.parse(data) as DrawData
+	})
+	store.history = history
+	
+  const trackActions = ["setStore", "repStore", "addItem", "delItem", "setItem"];
+  store.$onAction(({ args, name, after, store }) => {
+    if (!trackActions.includes(name)) return;
+		const isInit = name === "setStore"
+		const current = isInit ? null : JSON.stringify(store.data)
+		after(() => {
+			if (isInit) {
+				history.init(JSON.stringify(store.data))
+			} else {
+				history.push(current!)
+			}
+		})
+  });
+
+  return store;
+});

+ 81 - 0
src/core/store/store.ts

@@ -0,0 +1,81 @@
+import { defineStore } from "pinia";
+import { DrawData, DrawItem, ShapeType } from '../components'
+
+const sortFn = (a: Pick<DrawItem, 'zIndex' | 'createTime'>, b: Pick<DrawItem, 'zIndex' | 'createTime'>) => 
+	a.zIndex - b.zIndex || a.createTime - b.createTime
+
+
+export const useStoreRaw = defineStore('draw-data', {
+	state: (): { data: DrawData, version: number } => ({ version: 0, data: {} }),
+	getters: {
+		items() {
+			return Object.values((this as any).data).flat() as DrawItem[]
+		},
+		sortItems() {
+			return (this.items as any).sort(sortFn) as DrawItem[]
+		}
+	},
+	actions: {
+		repStore(store: DrawData) {
+			const newStore = JSON.parse(JSON.stringify(store))
+			this.$patch({ data: newStore });
+		},
+		setStore(store: DrawData) {
+			const newStore = JSON.parse(JSON.stringify(store))
+			this.$patch({ version: 0, data: newStore });
+		},
+		getItemNdx<T extends ShapeType>(type: T, id: string) {
+			const items = this.data[type]
+			if (items) {
+				return items.findIndex(item => item.id === id)
+			}
+			return -1
+		},
+		getItem<T extends ShapeType>(type: T, id: string) {
+			const ndx = this.getItemNdx(type, id)
+			if (~ndx) return this.data[type]![ndx]
+		},
+		addItems<T extends ShapeType>(type: T, items: DrawItem<T>[]) {
+			items.forEach(item => this.addItem(type, item))
+		},
+		addItem<T extends ShapeType>(type: T, item: DrawItem<T>) {
+			this.$patch((state) => {
+				if (!(type in state)) {
+					state.data[type] = []
+				}
+				state.data[type]!.push(item as any);
+			})
+		},
+		delItem<T extends ShapeType>(type: T, id: string) {
+			const ndx = this.getItemNdx(type, id)
+			if (~ndx) {
+				this.$patch(state => {
+					state.data[type]!.splice(ndx, 1)
+				})
+			}
+		},
+		setItem<T extends ShapeType>(type: T, playData: { value: DrawItem<T>, id: string }) {
+			const ndx = this.getItemNdx(type, playData.id)
+			if (~ndx) {
+				this.$patch(state => {
+					Object.assign(state.data[type]![ndx], playData.value)
+				})
+			}
+		},
+		getItemsZIndex(items?: DrawItem[]) {
+			if (!items) {
+				return this.sortItems;
+			} else {
+				return items.sort(sortFn)
+			}
+		},
+		getType(id: string) {
+			const types = Object.keys(this.data) as ShapeType[]
+			for (const type of types) {
+				if (this.data[type]?.some(item=> item.id === id)) {
+					return type
+				}
+			}
+		}
+	}
+})

example/fuse/App.vue → src/example/fuse/App.vue


+ 0 - 4
example/fuse/main.ts

@@ -1,13 +1,9 @@
 import { createApp } from 'vue'
 import './styles/global.scss'
 import 'element-plus/theme-chalk/src/index.scss'
-import VueKonva from "vue-konva";
 import App from './App.vue'
-import { pinia } from './store'
 
 const app = createApp(App)
-app.use(pinia)
-app.use(VueKonva)
 app.mount('#app')
 
 console.log('当前版本', window.__VERSION__)

example/fuse/styles/global.scss → src/example/fuse/styles/global.scss


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

@@ -0,0 +1,114 @@
+<template>
+  <div class="header">
+    <div class="nav">
+      <el-button type="primary" plain>返回</el-button>
+    </div>
+    <div class="draw-operate">
+      <div>
+        <span class="operate">
+          撤销<el-icon><Plus /></el-icon>
+        </span>
+        <span class="operate">
+          重做<el-icon><Plus /></el-icon>
+        </span>
+      </div>
+      <div>
+        <span class="operate">
+          清除<el-icon><Plus /></el-icon>
+        </span>
+        <span class="operate">
+          旋转<el-icon><Plus /></el-icon>
+        </span>
+        <span class="operate">
+          全屏<el-icon><Plus /></el-icon>
+        </span>
+      </div>
+      <div>
+        <span class="operate">
+          ai导入<el-icon><Plus /></el-icon>
+        </span>
+        <span class="operate" @click="bgFileInput?.click()">
+          背景图<el-icon><Plus /></el-icon>
+          <input
+            class="file-input"
+            ref="bgFileInput"
+            type="file"
+            @change="(ev: any) => setBGImage(ev.target.files[0])"
+          />
+        </span>
+        <span class="operate">
+          VR<el-icon><Plus /></el-icon>
+        </span>
+      </div>
+      <div v-if="dev">
+        <span class="operate" @click="draw.toggleHit()">
+          碰撞检测<el-icon><Plus /></el-icon>
+        </span>
+      </div>
+    </div>
+    <div class="saves">
+      <el-button type="primary" plain>保存</el-button>
+      <el-button type="primary" plain>导出</el-button>
+      <el-button>图纸</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ElButton, ElIcon } from "element-plus";
+import { useDraw } from "../use-draw.ts";
+import { ref } from "vue";
+import { Plus } from "@element-plus/icons-vue";
+
+const draw = useDraw();
+const bgFileInput = ref<HTMLInputElement | null>(null);
+const dev = import.meta.env.DEV;
+
+const setBGImage = (file: File) => {
+  draw.addShape(
+    "image",
+    {
+      width: 1000,
+      height: 1000,
+      url: URL.createObjectURL(file),
+    },
+    { x: window.innerWidth / 2, y: window.innerHeight / 2 },
+    true
+  );
+};
+</script>
+
+<style lang="scss" scoped>
+@use 'element-plus/theme-chalk/src/common/var';
+
+.header {
+  background-color: var.$color-primary;
+  display: flex;
+  align-items: center;
+  padding: 10px;
+  justify-content: space-between;
+}
+
+.draw-operate {
+  text-align: center;
+  color: #fff;
+  display: flex;
+  align-items: center;
+
+  > div:not(:last-child) {
+    padding-right: 10px;
+    margin-right: 10px;
+    border-right: 1px solid #fff;
+  }
+
+  i {
+    width: auto;
+    margin: 0 5px;
+  }
+}
+
+.file-input {
+  position: absolute;
+  visibility: hidden;
+}
+</style>

+ 11 - 6
example/fuse/views/home.vue

@@ -1,20 +1,25 @@
 <template>
   <div class="layout">
-    <Header class="header" />
+    <Header class="header" v-if="draw" />
     <div class="container">
-      <Slide class="slide" />
+      <Slide class="slide" v-if="draw" />
       <div class="content" ref="drawEle">
-        <Renderer v-if="drawEle" ref="draw" />
+        <DrawBoard
+          v-if="drawEle"
+          :ref="(e: any) => draw = e.draw"
+          :data="(initData as any)"
+        />
       </div>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { Header } from "./header/";
+import Header from "./header/header.vue";
+import Slide from "./slide/slide.vue";
 import { ref } from "vue";
-import { Slide } from "./slide";
-import { DrawExpose, Renderer } from "@/index.ts";
+import { DrawExpose, DrawBoard } from "@/index";
+import { initData } from "./init.ts";
 import { installDraw } from "./use-draw.ts";
 
 const drawEle = ref<HTMLDivElement | null>(null);

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

@@ -161,28 +161,28 @@ export const initData = {
       createTime: 3,
       zIndex: 0,
       id: "3333",
-      url: "/example/fuse//assets/icons/vue.svg",
+      url: "/icons/vue.svg",
       mat: [1, 0, 0, 1, 791.984375, 464.45703125],
     },
     {
       createTime: 3,
       zIndex: 0,
       id: "3331",
-      url: "/example/fuse//assets/icons/vue.svg",
+      url: "/icons/vue.svg",
       mat: [1, 0, 0, 1, 1072.265625, 467.41796875],
     },
     {
       createTime: 3,
       zIndex: 0,
       id: "3323",
-      url: "/example/fuse/assets/icons/vue.svg",
+      url: "/icons/vue.svg",
       mat: [1, 0, 0, 1, 1281.1015625, 448.33984375],
     },
     {
       createTime: 3,
       zIndex: 0,
       id: "3313",
-      url: "/example/fuse/assets/icons/BedsideCupboard.svg",
+      url: "/icons/BedsideCupboard.svg",
       width: 300,
       height: 300,
       stroke: "red",

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

@@ -0,0 +1,122 @@
+import { DrawItem, ShapeType, shapeNames } from "@/index.ts";
+import { v4 as uuid } from "uuid";
+import { toRaw } from "vue";
+
+type PresetAdd<T extends ShapeType = ShapeType> ={
+  type: T;
+  preset?: Partial<DrawItem<T>>;
+}
+
+export type MenuItem<T extends ShapeType = ShapeType> = {
+  icon: string;
+  name: string;
+  value?: string;
+  children?: MenuItem<T>[];
+  payload?: PresetAdd<T>
+};
+
+const genItem = <T extends ShapeType>(
+  type: T,
+  preset: PresetAdd<T>["preset"] = {}
+) => ({
+  name: shapeNames[type],
+  value: uuid(),
+  payload: { type, preset },
+});
+
+export const getItem = (
+  value: string,
+  queryMenus = menus
+): MenuItem | undefined => {
+  for (const menu of queryMenus) {
+    const eqItem =
+      menu.value === value
+        ? menu
+        : menu.children?.length
+        ? getItem(value, menu.children)
+        : void 0;
+    if (eqItem) return eqItem;
+  }
+};
+
+const eqPayload = (p1?: PresetAdd, p2?: PresetAdd) => {
+  if (!p2 || !p1 || p1.type !== p2.type) return false;
+  return !p1.preset || !p2.preset || toRaw(p1.preset) === toRaw(p2.preset);
+};
+
+export const getValue = (
+  payload: PresetAdd,
+  queryMenus = menus
+): string | undefined => {
+  for (const menu of queryMenus) {
+    const eqItem = eqPayload(menu.payload, payload)
+      ? menu.value
+      : menu.children?.length
+      ? getValue(payload, menu.children)
+      : void 0;
+    if (eqItem) return eqItem;
+  }
+};
+
+export const menus: MenuItem[] = [
+  {
+    icon: "",
+    name: "绘制",
+    value: uuid(),
+    children: [
+      {
+        icon: "",
+        ...genItem("line"),
+      },
+      {
+        icon: "",
+        ...genItem("arrow"),
+      },
+
+      {
+        icon: "",
+        ...genItem("rectangle"),
+      },
+      {
+        icon: "",
+        ...genItem("circle"),
+      },
+      {
+        icon: "",
+        ...genItem("triangle"),
+      },
+      {
+        icon: "",
+        ...genItem("polygon"),
+      },
+    ],
+  },
+  {
+    icon: "",
+    name: "图例",
+    value: uuid(),
+    children: [
+      {
+        icon: "",
+        ...genItem("icon", { url: '/icons/BedsideCupboard.svg', width: 500, height: 500 }),
+        name: "vue",
+      },
+      {
+        icon: "",
+        ...genItem("icon", {
+          url: '/icons/vue.svg',
+          width: 300,
+          height: 300,
+          stroke: "red",
+          strokeWidth: 1,
+          strokeScaleEnabled: false,
+        }),
+        name: "自定义",
+      },
+    ],
+  },
+  {
+    icon: "",
+    ...genItem("text"),
+  },
+];

example/fuse/views/slide/slide-item.vue → src/example/fuse/views/slide/slide-item.vue


+ 1 - 1
example/fuse/views/slide/slide.vue

@@ -23,7 +23,7 @@ const draw = useDraw();
 const selectHandler = (value: string) => {
   const item = getItem(value);
   if (!item || !item.payload) throw "无效菜单";
-  draw.value!.enterMouseAddShape(item.payload.type, item.payload.preset);
+  draw.enterMouseAddShape(item.payload.type, item.payload.preset);
 };
 </script>
 /

+ 2 - 2
example/fuse/views/use-draw.ts

@@ -1,9 +1,9 @@
 import { inject, provide, Ref } from 'vue'
-import { DrawExpose } from "@/index.ts";
+import { DrawExpose } from "../../../index";
 
 const actionKey = Symbol('drawAction');
 export const installDraw = (drawRef: Ref<DrawExpose | undefined>) => {
 	provide(actionKey, drawRef)
 }
 
-export const useDraw = () => inject<Ref<DrawExpose | undefined>>(actionKey)!
+export const useDraw = () => inject<Ref<DrawExpose>>(actionKey)?.value!

+ 3 - 1
src/index.ts

@@ -1,5 +1,5 @@
-export { default as Renderer } from './core/renderer/renderer.vue'
 export { components } from './core/components'
+export { default as DrawBoard } from './app.vue'
 
 import { components } from './core/components'
 
@@ -10,4 +10,6 @@ for (const key in components) {
 	shapeNames[key as ShapeType] = components[key as ShapeType].shapeName
 }
 
+export type { DrawItem, DrawData } from './core/components'
+
 export type { DrawExpose } from './core/hook/use-expose.ts'