Explorar o código

feat: 添加背景栅格

bill hai 7 meses
pai
achega
1958cf34a7

+ 0 - 8
src/constant/help-style.ts

@@ -3,11 +3,3 @@ import { getMouseColors } from "../utils/colors.ts"
 export const themeColor = import.meta.env.VITE_PRIMARY
 export const themeMouseColors = getMouseColors(themeColor)
 
-
-
-export const HelpPointStyle = {
-	radius: 1,
-	fill: 'red',
-	stroke: 'black',
-	strokeWidth: 4,
-}

+ 7 - 4
src/core/components/circle/index.ts

@@ -8,6 +8,7 @@ import {
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
 import { AddMessage } from "@/core/hook/use-draw.ts";
+import { lineCenter, lineLen } from "@/utils/math.ts";
 
 export { default as Component } from "./circle.vue";
 export { default as TempComponent } from "./temp-circle.vue";
@@ -63,7 +64,6 @@ export const interactiveToData = (
     const item = {
       ...getBaseItem(),
       ...preset,
-      ...info.cur[0],
     } as unknown as CircleData;
     return interactiveFixData(item, info);
   }
@@ -74,8 +74,11 @@ export const interactiveFixData = (
   info: AddMessage<'circle'>
 ) => {
   const area = info.cur!;
-  const xr = Math.abs(area[1].x - area[0].x);
-  const yr = Math.abs(area[1].y - area[0].y);
-  data.radius = Math.max(xr, yr, 0.01);
+  const radius = Math.max(lineLen(area[0], area[1]) / 2, 0.5)
+  const center = lineCenter(area)
+  data.x = center.x
+  data.y = center.y
+
+  data.radius = radius
   return data;
 };

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

@@ -25,7 +25,6 @@ const data = computed(() => ({ ...defaultStyle, ...props.data }));
 const image = ref<HTMLImageElement | null>(null);
 const shape = ref<DC<Group>>();
 
-console.log(props.data);
 defineExpose({
   get shape() {
     return shape.value;

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

@@ -1,5 +1,11 @@
 <template>
-  <TempLine :data="tData" :ref="(v: any) => shape = v?.shape" :id="data.id" />
+  <TempLine
+    :data="tData"
+    :ref="(v: any) => shape = v?.shape"
+    :id="data.id"
+    @update:position="updatePosition"
+    @update="emit('updateShape', { ...data })"
+  />
   <PropertyUpdate
     :describes="describes"
     :data="data"
@@ -17,6 +23,7 @@ import TempLine from "./temp-line.vue";
 import { EditPen } from "@element-plus/icons-vue";
 import { useInteractiveDrawShapeAPI } from "@/core/hook/use-draw.ts";
 import { useStore } from "@/core/store/index.ts";
+import { Pos } from "@/utils/math.ts";
 
 const props = defineProps<{ data: LineData }>();
 const emit = defineEmits<{
@@ -39,6 +46,11 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   propertys: ["stroke", "strokeWidth", "dash", "opacity", "ref", "zIndex"],
 });
 
+const updatePosition = ({ ndx, val }: { ndx: number; val: Pos }) => {
+  Object.assign(data.value.points[ndx], val);
+  shape.value?.getNode().fire("bound-change");
+};
+
 const draw = useInteractiveDrawShapeAPI();
 const store = useStore();
 operateMenus.push({

+ 71 - 14
src/core/components/line/temp-line.vue

@@ -1,27 +1,55 @@
 <template>
-  <v-line
-    :config="{
-      ...data,
-      zIndex: undefined,
-      points: flatPositions(data.points),
-      opacity: addMode ? 0.3 : data.opacity,
-      hitFunc,
-    }"
-    ref="shape"
-  />
+  <v-group ref="shape">
+    <v-line
+      name="repShape"
+      :config="{
+        ...data,
+        zIndex: undefined,
+        points: flatPositions(data.points),
+        opacity: addMode ? 0.3 : data.opacity,
+        hitFunc,
+      }"
+    >
+    </v-line>
+    <Point
+      v-for="(p, ndx) in data.points"
+      :id="data.id + ndx"
+      :shapeId="data.id"
+      :position="p"
+      :size="data.strokeWidth + 6"
+      :color="data.stroke"
+      @update:position="(p) => emit('update:position', { ndx, val: p })"
+      @dragend="endHandler()"
+      @dragstart="startHandler(ndx)"
+    />
+  </v-group>
 </template>
 
 <script lang="ts" setup>
+import Point from "../share/point.vue";
 import { defaultStyle, LineData } from "./index.ts";
 import { flatPositions } from "@/utils/shared.ts";
-import { computed, onUnmounted, ref } from "vue";
+import { computed, ref } from "vue";
 import { DC } from "@/deconstruction.js";
 import { Line, LineConfig } from "konva/lib/shapes/Line";
-const props = defineProps<{ data: LineData; addMode?: boolean; real?: LineData }>();
+import { Pos } from "@/utils/math.ts";
+import { useCustomSnapInfos } from "@/core/hook/use-snap.ts";
+import { generateSnapInfos } from "../util.ts";
+import { ComponentSnapInfo } from "../index.ts";
+
+const props = defineProps<{
+  data: LineData;
+  addMode?: boolean;
+  canEdit?: boolean;
+  shapeId?: string;
+}>();
+const emit = defineEmits<{
+  (e: "update:position", data: { ndx: number; val: Pos }): void;
+  (e: "update"): void;
+}>();
+
 const data = computed(() => ({ ...defaultStyle, ...props.data }));
 
-console.log("hit", props.data.id);
-onUnmounted(() => console.error("un hit", props.data.id));
 const hitFunc: LineConfig["hitFunc"] = (con, shape) => {
   con.beginPath();
   con.moveTo(data.value.points[0].x, data.value.points[0].y);
@@ -32,6 +60,35 @@ const hitFunc: LineConfig["hitFunc"] = (con, shape) => {
   con.fillStrokeShape(shape);
 };
 
+const infos = useCustomSnapInfos();
+const addedInfos = [] as ComponentSnapInfo[];
+const clearInfos = () => {
+  addedInfos.forEach(infos.remove);
+};
+
+const startHandler = (ndx: number) => {
+  clearInfos();
+  const geos = [
+    props.data.points.slice(0, ndx),
+    props.data.points.slice(ndx + 1, props.data.points.length),
+  ];
+  if (ndx > 0 && ndx < props.data.points.length - 1) {
+    geos.push([props.data.points[ndx - 1], props.data.points[ndx + 1]]);
+  }
+  geos.forEach((geo) => {
+    const snapInfos = generateSnapInfos(geo, true, true, true);
+    snapInfos.forEach((item) => {
+      infos.add(item);
+      addedInfos.push(item);
+    });
+  });
+};
+
+const endHandler = () => {
+  clearInfos();
+  emit("update");
+};
+
 const shape = ref<DC<Line>>();
 defineExpose({
   get shape() {

+ 14 - 2
src/core/components/polygon/polygon.vue

@@ -1,5 +1,11 @@
 <template>
-  <TempLine :data="tData" :ref="(v: any) => shape = v?.shape" :id="data.id" />
+  <TempLine
+    :data="tData"
+    :ref="(v: any) => shape = v?.shape"
+    :id="data.id"
+    @update:position="updatePosition"
+    @update="emit('updateShape', { ...data })"
+  />
   <PropertyUpdate
     :describes="describes"
     :data="data"
@@ -12,11 +18,12 @@
 <script lang="ts" setup>
 import { PolygonData, getMouseStyle, defaultStyle } from "./index.ts";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
-import { PropertyUpdate, Operate } from "../../propertys";
+import { PropertyUpdate, Operate } from "../../propertys/index.ts";
 import TempLine from "./temp-polygon.vue";
 import { useInteractiveDrawShapeAPI } from "@/core/hook/use-draw.ts";
 import { useStore } from "@/core/store/index.ts";
 import { EditPen } from "@element-plus/icons-vue";
+import { Pos } from "@/utils/math.ts";
 
 const props = defineProps<{ data: PolygonData }>();
 const emit = defineEmits<{
@@ -38,6 +45,11 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   propertys: ["fill", "stroke", "strokeWidth", "dash", "opacity", "ref", "zIndex"],
 });
 
+const updatePosition = ({ ndx, val }: { ndx: number; val: Pos }) => {
+  Object.assign(data.value.points[ndx], val);
+  shape.value?.getNode().fire("bound-change");
+};
+
 const draw = useInteractiveDrawShapeAPI();
 const store = useStore();
 operateMenus.push({

+ 66 - 12
src/core/components/polygon/temp-polygon.vue

@@ -1,23 +1,47 @@
 <template>
-  <v-line
-    :config="{
-      ...data,
-      closed: true,
-      zIndex: undefined,
-      points: flatPositions(data.points),
-      opacity: addMode ? 0.3 : data.opacity,
-    }"
-    ref="shape"
-  />
+  <v-group ref="shape">
+    <v-line
+      name="repShape"
+      :config="{
+        ...data,
+        closed: true,
+        zIndex: undefined,
+        points: flatPositions(data.points),
+        opacity: addMode ? 0.3 : data.opacity,
+      }"
+    >
+    </v-line>
+    <Point
+      v-for="(p, ndx) in data.points"
+      :id="data.id + ndx"
+      :shapeId="data.id"
+      :position="p"
+      :size="data.strokeWidth + 6"
+      :color="data.stroke"
+      @update:position="(p) => emit('update:position', { ndx, val: p })"
+      @dragend="endHandler()"
+      @dragstart="startHandler(ndx)"
+    />
+  </v-group>
 </template>
 
 <script lang="ts" setup>
+import Point from "../share/point.vue";
 import { defaultStyle, PolygonData } from "./index.ts";
-import { flatPositions } from "@/utils/shared.ts";
+import { flatPositions, rangMod } from "@/utils/shared.ts";
 import { computed, ref } from "vue";
 import { DC } from "@/deconstruction.js";
 import { Line } from "konva/lib/shapes/Line";
-const props = defineProps<{ data: PolygonData; addMode?: boolean; preDel?: boolean }>();
+import { Pos } from "@/utils/math.ts";
+import { useCustomSnapInfos } from "@/core/hook/use-snap.ts";
+import { ComponentSnapInfo } from "../index.ts";
+import { generateSnapInfos } from "../util.ts";
+const props = defineProps<{ data: PolygonData; addMode?: boolean }>();
+const emit = defineEmits<{
+  (e: "update:position", data: { ndx: number; val: Pos }): void;
+  (e: "update"): void;
+}>();
+
 const data = computed(() => ({ ...defaultStyle, ...props.data }));
 const shape = ref<DC<Line>>();
 defineExpose({
@@ -25,4 +49,34 @@ defineExpose({
     return shape.value;
   },
 });
+
+const infos = useCustomSnapInfos();
+const addedInfos = [] as ComponentSnapInfo[];
+const clearInfos = () => {
+  addedInfos.forEach(infos.remove);
+};
+
+const startHandler = (ndx: number) => {
+  clearInfos();
+  const len = props.data.points.length;
+  const geos = [props.data.points.slice(0, ndx), props.data.points.slice(ndx + 1, len)];
+  if (len > 2) {
+    geos.push([
+      props.data.points[rangMod(ndx - 1, len)],
+      props.data.points[rangMod(ndx + 1, len)],
+    ]);
+  }
+  geos.forEach((geo) => {
+    const snapInfos = generateSnapInfos(geo, true, true, true);
+    snapInfos.forEach((item) => {
+      infos.add(item);
+      addedInfos.push(item);
+    });
+  });
+};
+
+const endHandler = () => {
+  clearInfos();
+  emit("update");
+};
 </script>

+ 63 - 18
src/core/components/share/point.vue

@@ -1,29 +1,74 @@
 <template>
-	<v-circle
-			:config="{  ...HelpPointStyle, ...position }"
-			ref="circle"
-	/>
+  <v-circle :config="{ ...style, ...position }" ref="circle" />
 </template>
 
 <script lang="ts" setup>
 import { Pos } from "@/utils/math.ts";
-import { HelpPointStyle } from "@/constant/help-style.ts";
-import { ref, watch, watchEffect } from 'vue'
+import { themeColor } from "@/constant/help-style.ts";
+import { computed, ref, watch } from "vue";
 import { DC } from "@/deconstruction";
 import { Circle } from "konva/lib/shapes/Circle";
 import { useShapeDrag } from "@/core/hook/use-transformer.ts";
+import { getMouseColors } from "@/utils/colors";
+import { useGlobalSnapInfos, useSnap } from "@/core/hook/use-snap";
+import { generateSnapInfos } from "../util";
+import { ComponentSnapInfo } from "..";
 
-const props = defineProps<{ position: Pos }>()
-const emit = defineEmits<{ (e: 'update:position', position: Pos): void, (e: 'dragend'): void }>()
+const props = defineProps<{
+  position: Pos;
+  id?: any;
+  color?: string;
+  size?: number;
+  shapeId?: string;
+  getSelfSnapInfos?: (point: Pos) => ComponentSnapInfo[];
+}>();
+const emit = defineEmits<{
+  (e: "update:position", position: Pos): void;
+  (e: "dragend"): void;
+  (e: "dragstart"): void;
+}>();
 
-const circle = ref<DC<Circle>>()
-const offset = useShapeDrag(circle)
+const style = computed(() => {
+  const color = getMouseColors(props.color || themeColor);
+  const size = props.size || 5;
+  return {
+    radius: size / 2,
+    fill: color.disable,
+    stroke: color.press,
+    strokeWidth: size / 4,
+  };
+});
 
-watch(offset, (position) => {
-	if (position) {
-		emit('update:position', position)
-	} else {
-		emit('dragend')
-	}
-})
-</script>
+const snapInfos = useGlobalSnapInfos();
+const refSnapInfos = computed(() => {
+  if (!props.shapeId) {
+    return snapInfos.value;
+  } else {
+    return snapInfos.value.filter((p) => !("id" in p) || p.id !== props.shapeId);
+  }
+});
+const snap = useSnap(refSnapInfos);
+const circle = ref<DC<Circle>>();
+const offset = useShapeDrag(circle);
+let init: Pos;
+watch(offset, (offset, oldOffsert) => {
+  snap.clear();
+  if (!oldOffsert) {
+    init = { ...props.position };
+    emit("dragstart");
+  }
+  if (offset) {
+    const point = {
+      x: init.x + offset.x,
+      y: init.y + offset.y,
+    };
+    const refSnapInfos = props.getSelfSnapInfos
+      ? props.getSelfSnapInfos(point)
+      : generateSnapInfos([point], true, true);
+    const transform = snap.move(refSnapInfos);
+    emit("update:position", transform ? transform.point(point) : point);
+  } else {
+    emit("dragend");
+  }
+});
+</script>

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

@@ -56,7 +56,7 @@ export const interactiveFixData = (
   info: AddMessage<"triangle">
 ) => {
   const area = info.cur!;
-
+ 
   data.points[0] = {
     x: area[0].x - (area[1].x - area[0].x),
     y: area[1].y,

+ 147 - 0
src/core/helper/back-grid.vue

@@ -0,0 +1,147 @@
+<template>
+  <v-group ref="grid" v-if="hLines && rect">
+    <template v-for="item in hLines">
+      <v-line
+        v-for="l in item.children"
+        :config="{
+          points: [rect[0].x, l, rect[1].x, l],
+          ...style,
+          strokeWidth: style.strokeWidth * 0.33,
+        }"
+      />
+    </template>
+    <template v-for="item in vLines">
+      <v-line
+        v-for="l in item.children"
+        :config="{
+          points: [l, rect[0].y, l, rect[1].y],
+          ...style,
+          strokeWidth: style.strokeWidth * 0.33,
+        }"
+      />
+    </template>
+
+    <v-line
+      v-for="item in hLines"
+      :config="{ points: [rect[0].x, item.dividing, rect[1].x, item.dividing], ...style }"
+    />
+    <v-line
+      v-for="item in vLines"
+      :config="{ points: [item.dividing, rect[0].y, item.dividing, rect[1].y], ...style }"
+    />
+  </v-group>
+</template>
+<script lang="ts" setup>
+import { computed, ref, watch } from "vue";
+import { useResize } from "../hook/use-event";
+import { useShapeStaticZindex } from "../hook/use-layer";
+import {
+  useViewerInvertTransform,
+  useViewerTransform,
+  useViewerTransformConfig,
+} from "../hook/use-viewer";
+import { DC } from "@/deconstruction";
+import { Rect } from "konva/lib/shapes/Rect";
+import { lineLen } from "@/utils/math";
+import { debounce } from "@/utils/shared";
+
+const grid = ref<DC<Rect>>();
+useShapeStaticZindex(grid);
+
+const style = {
+  stroke: "#ccc",
+  strokeWidth: 3,
+  opacity: 0.7,
+  strokeScaleEnabled: false,
+};
+
+const pixelSize = useResize();
+const viewerInvertTransform = useViewerInvertTransform();
+// 真实rect
+const rect = computed(() => {
+  if (!pixelSize.value) return null;
+  const start = viewerInvertTransform.value.point({ x: 0, y: 0 });
+  const end = viewerInvertTransform.value.point({
+    x: pixelSize.value.width,
+    y: pixelSize.value.height,
+  });
+  return [start, end];
+});
+
+// 看大格子的像素,100倍数
+const offsetUnit = 100;
+const minScalePixel = offsetUnit / 2;
+const viewerTransform = useViewerTransform();
+const viewerTransformConfig = useViewerTransformConfig();
+const offset = ref(offsetUnit);
+watch(
+  () => `${viewerTransformConfig.value.scaleX},${viewerTransformConfig.value.scaleY}`,
+  () => {
+    offset.value = offsetUnit;
+    const start = viewerTransform.value.point({ x: 0, y: 0 });
+    let i = 0;
+    while (true) {
+      const end = viewerTransform.value.point({ x: offset.value, y: 0 });
+      if (lineLen(start, end) >= minScalePixel) {
+        break;
+      }
+      offset.value *= 2;
+      i++;
+      if (i > 200) {
+        break;
+      }
+    }
+  }
+);
+
+const getFinal = (val: number, isTop: boolean) => {
+  let t = val / offset.value;
+  t = isTop ? Math.floor(t) : Math.ceil(t);
+  return offset.value * t;
+};
+
+type DireLine = {
+  dividing: number;
+  children: number[];
+};
+
+const getLines = (min: number, max: number) => {
+  const isReverse = min > max;
+  const start = getFinal(min, !isReverse);
+  const end = getFinal(max, isReverse);
+  const diff = isReverse ? -offset.value : offset.value;
+
+  let current = start;
+  const lines: DireLine[] = [];
+  while (diff > 0 ? current <= end : current >= end) {
+    const item: DireLine = {
+      dividing: current,
+      children: [],
+    };
+
+    const cOffset = ((diff > 0 ? -1 : 1) * offset.value) / 5;
+    for (let i = 1; i < 5; i++) {
+      item.children.push(current + cOffset * i);
+    }
+    lines.push(item);
+    current += diff;
+  }
+  return lines;
+};
+
+const hLines = ref<DireLine[]>([]);
+const vLines = ref<DireLine[]>([]);
+watch(
+  rect,
+  debounce(() => {
+    if (!rect.value) {
+      hLines.value = [];
+      vLines.value = [];
+    } else {
+      hLines.value = getLines(rect.value[0].y, rect.value[1].y);
+      vLines.value = getLines(rect.value[0].x, rect.value[1].x);
+    }
+  }, 16),
+  { immediate: true }
+);
+</script>

+ 3 - 4
src/core/hook/use-automatic-data.ts

@@ -1,14 +1,13 @@
+import { copy } from '@/utils/shared'
 import { Ref, ref, watch } from 'vue'
 
-const parseCopy = <T>(data: T): T => JSON.parse(JSON.stringify(data))
-
 export const useAutomaticData = <T>(
 	getter: () => T,
-	copy = parseCopy
+	acopy = copy
 ) => {
 	const data = ref() as Ref<T>
 	watch(getter, (newData) => {
-		data.value = copy(newData)
+		data.value = acopy(newData)
 	}, { immediate: true, deep: true })
 	return data;
 }

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

@@ -1,5 +1,5 @@
 import { DC, EntityShape } from "@/deconstruction";
-import { computed, EmitFn, isRef, reactive, Ref, ref, shallowReactive } from "vue";
+import { computed, EmitFn, isRef, Ref, ref, shallowReactive } from "vue";
 import { useAutomaticData } from "./use-automatic-data";
 import { useMouseMigrateTempLayer, useZIndex } from "./use-layer";
 import { useAnimationMouseStyle } from "./use-mouse-status";

+ 11 - 6
src/core/hook/use-draw.ts

@@ -311,7 +311,8 @@ export const useInteractiveDrawAreas = <T extends ShapeType>(type: T) => {
       cursor.push("crosshair");
     },
     quit() {
-      isEnter && console.log("quit");
+      isEnter && cursor.pop()
+      
     },
   });
 };
@@ -334,7 +335,8 @@ export const useInteractiveDrawDots = <T extends ShapeType>(type: T) => {
 
 export const penUpdatePoints = <T extends Pos>(
   transfromPoints: T[],
-  cur: T
+  cur: T,
+  needClose = false
 ) => {
   const points = [...transfromPoints];
   let oper: "del" | "add" | "set" | "no" = "add";
@@ -350,7 +352,7 @@ export const penUpdatePoints = <T extends Pos>(
   for (let i = 0; i < points.length; i++) {
     if (eqPoint(points[i], cur)) {
       const isLast = i === points.length - 1;
-      const isStart = i === 0;
+      const isStart = needClose && i === 0;
 
       if (!isStart && !isLast) {
         points.splice(i--, 1);
@@ -407,6 +409,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
   // 可能历史空间会撤销 重做更改到正在绘制的组件
   const currentCursor = ref(penA);
   const cursor = useCursor();
+  let stopWatch: (() => void) | null = null
   const ia = useInteractiveDots({
     shapeType: type,
     isRuning,
@@ -422,6 +425,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
       quitDrawShape();
       beforeHandler.clear();
       cursor.pop();
+      stopWatch && stopWatch()
     },
     beforeHandler: (p) => {
       beforeHandler.clear();
@@ -461,7 +465,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
     currentCursor.value = penA;
     let pen: null | ReturnType<typeof penUpdatePoints> = null;
     if (!free.value) {
-      pen = penUpdatePoints(messages.value, cur);
+      pen = penUpdatePoints(messages.value, cur, type === 'line');
       consumed = pen.points;
       cur = pen.cur;
     }
@@ -519,7 +523,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
       // console.log(JSON.parse(JSON.stringify(item)))
     };
 
-    const stop = mergeFuns(
+    stopWatch = mergeFuns(
       watch(free, update),
       watch(dot, update, { immediate: true, deep: true }),
       watch(
@@ -549,7 +553,8 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
           }
         }
         beforeHandler.clear();
-        stop();
+        stopWatch && stopWatch();
+        stopWatch = null
         firstEntry = false;
       })
     );

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

@@ -6,6 +6,7 @@ import { useViewer } from "./use-viewer.ts";
 import { useGlobalResize } from "./use-event.ts";
 import { useInteractiveDrawShapeAPI } from "./use-draw.ts";
 import { useHistory } from "./use-history.ts";
+import { reactive } from "vue";
 
 type PickParams<K extends keyof Stage, O extends string> = Stage[K]  extends (...args: any) => any ?  Omit<Required<Parameters<Stage[K]>>[0], O> : never
 
@@ -20,6 +21,9 @@ export const useExpose = () => {
 	const history = useHistory()
 	const viewer = useViewer().viewer
 	const { updateSize } = useGlobalResize()
+	const config = reactive({
+		showGrid: true
+	})
 
 	const exposeBlob = (config?: PickParams<'toBlob', 'callback'>) => {
 		const $stage = stage.value!.getStage()
@@ -52,6 +56,7 @@ export const useExpose = () => {
 		},
 		viewer,
 		presetAdd: interactiveProps,
+		config
 	}
 }
 

+ 1 - 10
src/core/hook/use-global-vars.ts

@@ -228,15 +228,6 @@ export const useTransformIngShapes = installGlobalVar(
 
 
 export const useCursor = installGlobalVar(
-  () => {
-    const cursur = stackVar('default')
-
-    return {
-      var: cursur,
-      onDestroy: () => {
-
-      }
-    }
-  },
+  () => stackVar('default'),
   Symbol('cursor')
 )

+ 59 - 3
src/core/hook/use-history.ts

@@ -17,6 +17,9 @@ class DrawHistory {
   get currentId() {
     return this.history.currentId
   }
+  get current() {
+    return this.list.find(item => item.id === this.currentId)?.data
+  }
 
   preventFlag = false;
   onceFlag = false;
@@ -70,11 +73,60 @@ class DrawHistory {
     this.preventFlag = false;
   }
 
+  private saveKeyPrev = '__history__'
+  private saveKeyId: string | null = null
+  get saveKey() {
+    if (!this.saveKeyId) {
+      throw '未设置本地保存key'
+    }
+    return this.saveKeyPrev + this.saveKeyId
+  }
+  setLocalId(id: string) {
+    this.saveKeyId = id
+  }
+
+  getLocalId() {
+    return this.saveKeyId
+  }
+  
+  saveLocal() {
+    localStorage.setItem(this.saveKey, JSON.stringify(this.list.map(item => item.data)))
+  }
+
+  clearLocal() {
+    localStorage.removeItem(this.saveKey)
+  }
+
+  hasLocal() {
+    for (let i = 0, len = localStorage.length; i < len; i++) {
+      if (localStorage.key(i) === this.saveKey) {
+        return true
+      }
+    }
+    return false
+  }
+
+  loadLocalStorage() {
+    const list = JSON.parse(localStorage.getItem(this.saveKey)!)
+    if (!list.length) {
+      this.clear()
+      return;
+    }
+
+    this.history.reset()
+    this.setInit(list[0].data)
+    for (let i = 1; i < list.length; i++) {
+      this.push(list[i].data)
+    }
+    this.renderer(list[list.length - 1])
+  }
+
   push(data: string) {
     if (this.preventFlag) return;
     if (this.onceFlag) {
       this.onceHistory = data;
-    } else {
+    } else if (data !== this.current?.data) {
+      // console.log(data, this.current?.data)
       this.bus.emit("push");
       this.history.push({ attachs: JSON.stringify(this.pushAttachs), data });
       this.pushAttachs = {};
@@ -106,11 +158,16 @@ class DrawHistory {
     this.onceFlag = false;
   }
 
-  clear(): void {
+  clearCurrent(): void {
     this.renderer({ data: "", attachs: "" });
     this.push("");
   }
 
+  clear(): void {
+    this.history.reset()
+    this.clearCurrent()
+  }
+  
   init() {
     if (this.initData) {
       this.renderer({ data: this.initData, attachs: "" });
@@ -152,7 +209,6 @@ export const useHistoryAttach = <T>(
     setIds.length = 0
   }
 
-  console.log('isRun1', isRuning.value)
   watch(
     isRuning,
     (isRun, _, onCleanup) => {

+ 41 - 21
src/core/hook/use-layer.ts

@@ -1,7 +1,6 @@
 import { DC, EntityShape } from "@/deconstruction";
 import {
   computed,
-  onUnmounted,
   ref,
   Ref,
   toRaw,
@@ -49,9 +48,11 @@ export const useMigrateLayer = (shape: Ref<DC<EntityShape> | undefined>) => {
       if (import.meta.env.DEV) {
         setTimeout(() => {
           console.log(
-            `recovery raw:${rawLayer.id()} ${rawLayer.children.length} to:${toLayer.id()} ${toLayer.children.length}`,
+            `recovery raw:${rawLayer.id()} ${
+              rawLayer.children.length
+            } to:${toLayer.id()} ${toLayer.children.length}`
           );
-        })
+        });
       }
 
       if (toRaw(formal.value) === toRaw(rawLayer) && zIndexs.get($shape)) {
@@ -69,7 +70,9 @@ export const useMigrateLayer = (shape: Ref<DC<EntityShape> | undefined>) => {
 
     if (import.meta.env.DEV) {
       console.log(
-        `migrate raw:${rawLayer.id()} ${rawLayer.children.length} to:${toLayer.id()} ${toLayer.children.length}`,
+        `migrate raw:${rawLayer.id()} ${
+          rawLayer.children.length
+        } to:${toLayer.id()} ${toLayer.children.length}`
       );
     }
   };
@@ -94,27 +97,44 @@ export const useMouseMigrateTempLayer = (
         onCleanup(recovery);
       }
     },
-    { flush: 'sync' }
+    { flush: "sync" }
   );
 };
 
-const useCurrentStaticZIndex = installGlobalVar(() => ref(0), Symbol('currentStaticZIndex'));
+const useCurrentStaticZIndex = installGlobalVar(
+  () => ref(0),
+  Symbol("currentStaticZIndex")
+);
 
-export const useStaticZIndex = (refNum = 1) => {
+export const useGetStaticZIndex = () => {
   const current = useCurrentStaticZIndex();
-  let isDestory = false;
-  const destroy = () => {
-    if (!isDestory) {
-      current.value -= refNum;
-      isDestory = true;
-    }
+  return (refNum = 1) => {
+    let isDestory = false;
+    const destroy = () => {
+      if (!isDestory) {
+        current.value -= refNum;
+        isDestory = true;
+      }
+    };
+    const result = new Array(refNum)
+      .fill(0)
+      .map((_, i) => current.value + i + 1);
+    current.value += refNum;
+    return [result, destroy] as const;
   };
-  onUnmounted(destroy);
-  const result = new Array(refNum).fill(0).map((_, i) => current.value + i + 1);
-  current.value += refNum;
-  return result;
 };
 
+export const useShapeStaticZindex = (shape: Ref<DC<EntityShape> | undefined>) => {
+  const getStaticZindex = useGetStaticZIndex()
+  watchEffect((onCleanup) => {
+    if (shape.value) {
+      const [indexs, desIndex] = getStaticZindex()
+      shape.value.getNode().zIndex(indexs[0])
+      onCleanup(desIndex)
+    }
+  })
+} 
+
 const useZIndexsManage = installGlobalVar(() => {
   const store = useStore();
   const map = ref(new Map<EntityShape, DrawItem>());
@@ -122,8 +142,8 @@ const useZIndexsManage = installGlobalVar(() => {
   const formal = useFormalLayer();
   const sortItems = computed(() => {
     const items = Array.from(map.value.values());
-    return  store.getItemsZIndex(items);
-  })
+    return store.getItemsZIndex(items);
+  });
 
   const setZIndexs = () => {
     const shapes = Array.from(map.value.keys());
@@ -138,7 +158,7 @@ const useZIndexsManage = installGlobalVar(() => {
     });
   };
 
-  watch(sortItems, setZIndexs)
+  watch(sortItems, setZIndexs);
 
   return {
     set(shape: EntityShape, item: DrawItem) {
@@ -152,7 +172,7 @@ const useZIndexsManage = installGlobalVar(() => {
     },
     refresh: setZIndexs,
   };
-}, Symbol('zIndexsManage'));
+}, Symbol("zIndexsManage"));
 
 export const useZIndex = (
   shape: Ref<DC<EntityShape> | undefined>,

+ 96 - 47
src/core/hook/use-mouse-status.ts

@@ -5,6 +5,7 @@ import {
   globalWatch,
   installGlobalVar,
   useCan,
+  useDownKeys,
   useStage,
   useTransformIngShapes,
 } from "./use-global-vars.ts";
@@ -18,69 +19,114 @@ import {
   useTransformer,
 } from "./use-transformer.ts";
 import { useAniamtion } from "./use-animation.ts";
+import { KonvaEventObject } from "konva/lib/Node";
+
+export const getHoverShape = (stage: Stage) => {
+  const hover = ref<EntityShape>();
+  const enterHandler = (ev: KonvaEventObject<any, Stage>) => {
+    leaveHandler();
+    const target = ev.target;
+    hover.value = target;
+    target.on("pointerleave", leaveHandler);
+  };
+  const leaveHandler = () => {
+    if (hover.value) {
+      hover.value.off("pointerleave", leaveHandler);
+      hover.value = undefined;
+    }
+  };
+
+  stage.on("pointerenter", enterHandler);
+  return [
+    hover,
+    () => {
+      stage.off("pointerenter", enterHandler);
+      leaveHandler();
+    },
+  ] as const;
+};
+
+export const useShapeIsTransformerInner = () => {
+  const transformer = useTransformer();
+  const pointerIsTransformerInner = usePointerIsTransformerInner();
+  const stage = useStage();
+
+  return (shape: EntityShape) => {
+    const inner = ref(true);
+    const $stage = stage.value!.getNode();
+    const updateInner = () => {
+      inner.value =
+        transformer.isTransforming() ||
+        (transformer.queueShapes.value.includes(shape) &&
+          pointerIsTransformerInner());
+    };
+
+    const stop = watch(
+      transformer.queueShapes,
+      (_a, _, onCleanup) => {
+        updateInner();
+        if (inner.value) {
+          $stage.on("pointermove", updateInner);
+          onCleanup(() => {
+            $stage.off("pointermove", updateInner);
+          });
+        }
+      },
+      { immediate: true, flush: "sync" }
+    );
+
+    return [inner, stop] as const;
+  };
+};
 
 export const useMouseShapesStatus = installGlobalVar(() => {
-  const can = useCan()
+  const can = useCan();
   const stage = useStage();
   const listeners = ref([]) as Ref<EntityShape[]>;
   const hovers = ref([]) as Ref<EntityShape[]>;
   const press = ref([]) as Ref<EntityShape[]>;
   const selects = ref([]) as Ref<EntityShape[]>;
   const actives = ref([]) as Ref<EntityShape[]>;
-  const transformer = useTransformer();
-  const pointerIsTransformerInner = usePointerIsTransformerInner();
+  const keys = useDownKeys();
+  const shapeIsTransformerInner = useShapeIsTransformerInner();
 
   const init = (stage: Stage) => {
     let downTime: number;
     let downTarget: EntityShape | null;
-    const inner = new WeakMap<EntityShape, boolean>();
 
-    stage.on("pointerenter.mouse-status", async (ev) => {
-      const target = shapeTreeContain(listeners.value, ev.target);
-      if (!target) return;
-      inner.set(target, true);
-      if (hovers.value.includes(target)) return;
-
-      hovers.value.push(target);
-      const targetLeave = () => {
-        target.off("pointerleave.mouse-status");
-        stage.off("pointermove.mouse-status");
-        stopIncludeWatch! && stopIncludeWatch();
-        const ndx = hovers.value.indexOf(target);
-        if (~ndx) {
-          hovers.value.splice(ndx, 1);
-        }
-      };
-
-      let stopIncludeWatch: () => void;
-      target.on("pointerleave.mouse-status", (ev) => {
-        const target = shapeTreeContain(listeners.value, ev.target);
-        if (!target) return;
-        inner.set(target, false);
-
-        // TODO: 有可能在transformer上,需要额外检测
-        stopIncludeWatch! && stopIncludeWatch();
-        stopIncludeWatch = watch(
-          transformer.queueShapes,
-          (queueShapes, _, onCleanup) => {
-            if (inner.get(target)) return;
-
-            if (!queueShapes.includes(target) || !pointerIsTransformerInner()) {
-              targetLeave();
-            } else {
-              stage.on("pointermove.mouse-status", () => {
-                if (!inner.get(target) && !pointerIsTransformerInner()) {
-                  targetLeave();
-                }
-              });
-              onCleanup(() => stage.off("pointermove.mouse-status"));
+    const prevent = computed(() => keys.has(" "));
+    const [hover, hoverDestory] = getHoverShape(stage);
+    const hoverChange = (onCleanup: any) => {
+      if (prevent.value) {
+        return;
+      }
+
+      const pHover =
+        hover.value && shapeTreeContain(listeners.value, hover.value);
+      // TODO首先确定之前的有没有离开
+      if (hovers.value.length && hovers.value[0] !== pHover) {
+        const check = hovers.value[0];
+        const [inner] = shapeIsTransformerInner(check);
+        onCleanup(
+          watchEffect(() => {
+            if (!inner.value && !prevent.value) {
+              hovers.value.pop();
             }
-          },
-          { immediate: true }
+          })
         );
-      });
-    });
+      } else if (pHover) {
+        hovers.value[0] = pHover;
+      } else if (hovers.value.length) {
+        hovers.value.pop();
+      }
+    };
+    const stopHoverCheck = watch(
+      () => [hover.value, prevent.value, hovers.value[0]],
+      (_a, _b, onCleanup) => hoverChange(onCleanup)
+    );
+
     stage.on("pointerdown.mouse-status", (ev) => {
+      if (prevent.value) return;
       downTime = Date.now();
       const target = shapeTreeContain(listeners.value, ev.target);
       if (target && !press.value.includes(target)) {
@@ -90,7 +136,10 @@ export const useMouseShapesStatus = installGlobalVar(() => {
     });
 
     return mergeFuns(
+      stopHoverCheck,
+      hoverDestory,
       listener(stage.container(), "pointerup", () => {
+        if (prevent.value) return;
         press.value = [];
         if (Date.now() - downTime >= 300) return;
         if (downTarget) {

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

@@ -41,6 +41,7 @@ export const useTransformer = installGlobalVar(() => {
     anchorStrokeWidth: 2,
     anchorStroke: themeMouseColors.pub,
     anchorFill: themeMouseColors.press,
+    flipEnabled: false,
     padding: 10,
     useSingleNodeRotation: true,
   }) as TransformerExtends;
@@ -361,7 +362,11 @@ export const useShapeTransformer = <T extends EntityShape>(
           { flush: "post" }
         );
 
+        const boundHandler = () => rep.update && rep.update()
+        $shape.on('bound-change', boundHandler)
+
         onCleanup(() => {
+          $shape.off('bound-change', boundHandler)
           for (const key in oldConfig) {
             (transformer as any)[key](oldConfig[key]);
           }
@@ -412,6 +417,7 @@ export const useShapeTransformer = <T extends EntityShape>(
 export const cloneRepShape = <T extends EntityShape>(
   shape: T
 ): ReturnType<GetRepShape<T, any>> => {
+  shape = (shape as Group)?.findOne && (shape as Group)?.findOne('.repShape') as T || shape
   return {
     shape: shape.clone({
       fill: "rgb(0, 255, 0)",
@@ -458,7 +464,7 @@ export type CustomTransformerProps<
   getRepShape?: GetRepShape<S, T>;
   beforeHandler?: (data: T, mat: Transform) => T;
   handler?: (data: T, mat: Transform) => Transform | void | true;
-  callback?: (data: T) => void;
+  callback?: (data: T, mat: Transform) => void;
   transformerConfig?: TransformerConfig;
 };
 
@@ -491,6 +497,7 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
         };
       })
   );
+  let callMat: Transform
   watch(transform, (current, oldTransform) => {
     if (current) {
       if (!handler) return;
@@ -502,18 +509,20 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
       if (needSnap && (nTransform = needSnap[0](snapData))) {
         current = nTransform.multiply(current);
       }
+      callMat = current
       const mat = handler(data.value, current);
       if (mat) {
         if (repResult.update) {
           repResult.update(data.value, repResult.shape);
         } else if (mat !== true) {
           setShapeTransform(repResult.shape, mat);
+          callMat = mat
         }
         transformer.forceUpdate();
       }
     } else if (oldTransform) {
       needSnap && needSnap[1]();
-      callback && callback(data.value);
+      callback && callback(data.value, callMat);
     }
   });
   return transform;
@@ -549,8 +558,8 @@ export const useLineTransformer = <T extends LineTransformerData>(
       const transfrom = mat.copy().multiply(inverAttitude);
       data.points = tempVs = stableVs.map((v) => transfrom.point(v));
     },
-    callback(data) {
-      data.attitude = tempShape.getTransform().m;
+    callback(data, mat) {
+      data.attitude = mat.m;
       data.points = stableVs = tempVs;
       callback(data);
     },
@@ -561,6 +570,8 @@ export const useLineTransformer = <T extends LineTransformerData>(
       } else {
         repShape = cloneRepShape($shape).shape;
       }
+
+      repShape = (repShape as any).points ? repShape : (repShape as unknown as Group).findOne<Line>('.line')!
       tempShape = repShape;
       const update = (data: T) => {
         const attitude = new Transform(data.attitude);

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

@@ -35,6 +35,7 @@ export const useViewer = installGlobalVar(() => {
       }
     });
     viewer.bus.on("transformChange", (newTransform) => {
+      // console.log(newTransform.m)
       transform.value = newTransform;
     });
     transform.value = viewer.transform;

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

@@ -24,7 +24,6 @@ const emit = defineEmits<{
 let change = false;
 const inputHandler = (color: string | null) => {
   emit("update:value", color);
-  console.log(color);
   change = true;
 };
 

+ 37 - 12
src/core/renderer/renderer.vue

@@ -1,17 +1,11 @@
 <template>
   <div class="draw-layout" @contextmenu.prevent :style="{ cursor: cursorStyle }">
     <div class="mount-mask" :id="DomMountId" />
+
     <v-stage ref="stage" :config="size">
       <v-layer :config="viewerConfig" id="formal">
         <!--	不可去除,去除后移动端拖拽会有溢出	-->
-        <!-- <v-rect
-          :config="{
-            ...size,
-            fill: 'rgba(0,0,0,0)',
-            listener: false,
-            ...invertViewerConfig,
-          }"
-        /> -->
+        <BackGrid v-if="expose.config.showGrid" />
         <ShapeGroup v-for="type in types" :type="type" :key="type" />
       </v-layer>
       <!--	临时组,提供临时绘画,以及高频率渲染	-->
@@ -31,25 +25,55 @@
 <script lang="ts" setup>
 import ShapeGroup from "./group.vue";
 import TempShapeGroup from "./draw-group.vue";
+import ActiveBoxs from "../helper/active-boxs.vue";
+import SnapLines from "../helper/snap-lines.vue";
+import BackGrid from "../helper/back-grid.vue";
 import { DrawData, ShapeType, components } from "../components";
 import { useCursor, useDownKeys, 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 { DomMountId } from "../../constant";
-import ActiveBoxs from "../helper/active-boxs.vue";
-import SnapLines from "../helper/snap-lines.vue";
 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";
 
 const props = defineProps<{
   data: DrawData;
+  id?: string;
 }>();
 const store = useStore();
-store.setStore(props.data);
+const history = useHistory();
+
+const init = async () => {
+  if (props.id) {
+    history.setLocalId(props.id);
+    window.onunload = (ev) => {
+      if (history.hasRedo.value || history.hasUndo.value) {
+        history.saveLocal();
+      }
+    };
+  }
+  if (props.id && history.hasLocal()) {
+    try {
+      await ElMessageBox.confirm("检测到有历史数据,是否要恢复?", {
+        type: "info",
+        confirmButtonText: "恢复",
+        cancelButtonText: "取消",
+      });
+      history.loadLocalStorage();
+      return;
+    } catch {
+      history.clearLocal();
+    }
+  }
+  store.setStore(props.data);
+};
+init();
 
 const stage = useStage();
 const size = useResize();
@@ -97,7 +121,8 @@ watchEffect((onCleanup) => {
   }
 });
 
-defineExpose(useExpose());
+const expose = useExpose();
+defineExpose(expose);
 </script>
 
 <style scoped lang="scss">

+ 19 - 3
src/core/store/index.ts

@@ -3,6 +3,24 @@ import { DrawItem } from "../components";
 import { installGlobalVar } from "../hook/use-global-vars";
 import { useStoreRaw } from "./store";
 import { useHistory } from "../hook/use-history";
+import { round } from "@/utils/shared";
+
+// 放置重复放入历史,我们把小数保留5位小数
+const b = 3
+const normalStore = (store: any) => {
+  const norStore = Array.isArray(store) ? [...store] : {...store}
+  for (const key in norStore) {
+    const val = norStore[key]
+    if (typeof val === 'number') {
+      norStore[key] = round(val, b)
+    } else if (typeof val === 'object') {
+      norStore[key] = normalStore(val)
+    } else {
+      norStore[key] = val
+    }
+  }
+  return norStore
+}
 
 export const useStore = installGlobalVar(() => {
   const history = useHistory();
@@ -17,17 +35,15 @@ export const useStore = installGlobalVar(() => {
   ];
   store.$onAction(({ args, name, after, store }) => {
     if (!trackActions.includes(name)) return;
-    console.log(name, args)
     const isInit = name === "setStore";
     after(() => {
       if (isInit) {
         history.setInit(JSON.stringify(store.data));
       } else {
-        history.push(JSON.stringify(store.data!));
+        history.push(JSON.stringify(store.data));
       }
     });
   });
-
   return store;
 }, Symbol("store"));
 

+ 15 - 0
src/core/viewer.ts

@@ -69,6 +69,11 @@ export class Viewer {
 	}
 
 	movePixel(position: Pos, initMat = this.viewMat) {
+		if (isNaN(position.x) || isNaN(position.y)) {
+			console.error(`无效移动位置${position.x} ${position.y}`)
+			return;
+		}
+
 		const mat = initMat.copy().invert()
 		const p1 = mat.point({x: 0, y: 0})
 		const p2 = mat.point(position)
@@ -83,6 +88,16 @@ export class Viewer {
 
 
 	scale(center: Pos, scale: number, initMat = this.viewMat) {
+		const base = initMat.decompose().scaleX
+		if (base * scale < 0.001 || base * scale > 1000) {
+			console.error('缩放范围0.001~1000 已超过范围无法缩放')
+			return;
+		}
+		if (isNaN(center.x) || isNaN(center.y)) {
+			console.error(`无效中心点${center.x} ${center.y}`)
+			return;
+		}
+
 		this.mutMat(
 			new Transform()
 				.translate(center.x, center.y)

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

@@ -21,7 +21,10 @@
         </span>
       </div>
       <div>
-        <span class="operate" @click="draw.history.clear()">
+        <span class="operate" @click="draw.config.showGrid = !draw.config.showGrid">
+          {{ draw.config.showGrid ? "隐藏" : "显示" }}辅助线<el-icon><Plus /></el-icon>
+        </span>
+        <span class="operate" @click="draw.history.clearCurrent()">
           清除<el-icon><Plus /></el-icon>
         </span>
         <span class="operate" @click="rotateView">

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

@@ -5,6 +5,7 @@
       <Slide class="slide" v-if="draw" />
       <div class="content" ref="drawEle">
         <DrawBoard
+          id="asd"
           v-if="drawEle"
           :ref="(e: any) => draw = e.draw"
           :data="(data as any)"

+ 1 - 0
src/utils/shared.ts

@@ -202,3 +202,4 @@ export const arrayInsert = <T>(
   array.splice(i, 0, item);
   return array;
 };
+