Browse Source

添加公用组件

bill 8 tháng trước cách đây
mục cha
commit
ef55d97d27

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

@@ -29,6 +29,7 @@ const { shape, tData, operateMenus, describes } = useComponentStatus<Arrow, Arro
   props,
   getMouseStyle,
   defaultStyle,
+  transformType: 'line',
   getRepShape(): Line {
     return new Line({
       fill: "rgb(0, 255, 0)",

+ 54 - 21
src/core/components/circle/circle.vue

@@ -14,6 +14,9 @@ import { CircleData, getMouseStyle, defaultStyle } from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempCircle from "./temp-circle.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
+import { cloneRepShape, useCustomTransformer } from "@/core/hook/use-transformer.ts";
+import { Transform } from "konva/lib/Util";
+import { Circle } from "konva/lib/shapes/Circle";
 
 const props = defineProps<{ data: CircleData }>();
 const emit = defineEmits<{
@@ -22,32 +25,62 @@ const emit = defineEmits<{
   (e: "delShape"): void;
 }>();
 
-const { shape, tData, data, operateMenus, describes } = useComponentStatus({
+const { shape, tData, data, operateMenus, describes } = useComponentStatus<
+  Circle,
+  CircleData
+>({
   emit,
   props,
   getMouseStyle,
   defaultStyle,
+  transformType: "custom",
+  customTransform(callback, shape, data) {
+    const update = (mat: Transform, data: CircleData) => {
+      const { x, y, scaleX } = mat.decompose();
+      if (scaleX !== 1) {
+        return { radius: data.radius * scaleX, x, y };
+      } else {
+        return { x, y };
+      }
+    };
+
+    useCustomTransformer(shape, data, {
+      openSnap: true,
+      getRepShape($shape) {
+        const repShape = cloneRepShape($shape).shape;
+        return {
+          shape: repShape,
+          update(data) {
+            repShape.x(data.x).y(data.y).radius(data.radius);
+          },
+        };
+      },
+      transformerConfig: {
+        rotateEnabled: false,
+        keepRatio: true,
+        enabledAnchors: ["top-left", "top-right", "bottom-left", "bottom-right"],
+      },
+      beforeHandler(data, mat) {
+        return { ...data, ...update(mat, data) };
+      },
+      handler(data, mat) {
+        const setAttrib = update(mat, data);
+        Object.assign(data, setAttrib);
+        if (setAttrib.radius) {
+          return true;
+        }
+      },
+      callback,
+    });
+  },
   copyHandler(tf, data) {
-    data.x = tf.multiply(new Transform(data.mat)).m;
-    return data;
+    const decTf = tf.decompose();
+    console.log(data, decTf);
+    return {
+      ...data,
+      x: data.x + decTf.x,
+      y: data.y + decTf.y,
+    };
   },
 });
-// watch(transform, (transform, oldTransform) => {
-//   if (transform) {
-//     const { x, y, scaleX } = transform.decompose();
-//     if (scaleX !== 1) {
-//       atData.value.radius *= scaleX;
-//       setShapeTransform(
-//         shape.value!.getNode(),
-//         new Transform().translate(atData.value.x, atData.value.y)
-//       );
-//       transformer.forceUpdate();
-//     } else {
-//       atData.value.x = x;
-//       atData.value.y = y;
-//     }
-//   } else if (oldTransform) {
-//     emit("update", atData.value);
-//   }
-// });
 </script>

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

@@ -8,7 +8,6 @@ import {
   getRectSnapPoints,
 } from "../util.ts";
 import { getMouseColors } from "@/utils/colors.ts";
-import { vector } from "@/utils/math.ts";
 
 export { default as Component } from "./circle.vue";
 export { default as TempComponent } from "./temp-circle.vue";
@@ -40,6 +39,7 @@ export const getSnapInfos = (data: CircleData) => {
     x: v.x + data.x,
     y: v.y + data.y,
   }));
+  console.log(points)
   return generateSnapInfos(points, true, false);
 };
 

+ 6 - 4
src/core/components/circle/temp-circle.vue

@@ -1,11 +1,13 @@
 <template>
-	<v-circle 
+  <v-circle
     :config="{
       ...data,
       zIndex: undefined,
       opacity: addMode ? 0.3 : 1,
-    }">
-	</v-circle>
+    }"
+    ref="shape"
+  >
+  </v-circle>
 </template>
 
 <script lang="ts" setup>
@@ -21,4 +23,4 @@ defineExpose({
     return shape.value;
   },
 });
-</script>
+</script>

+ 1 - 1
src/core/components/icon/icon.vue

@@ -27,7 +27,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   emit,
   props,
   getMouseStyle,
-  line: false,
+  transformType: 'mat',
   defaultStyle,
   copyHandler(tf, data) {
     data.mat = tf.multiply(new Transform(data.mat)).m;

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

@@ -27,7 +27,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   emit,
   props,
   getMouseStyle,
-  line: false,
+  transformType: 'mat',
   defaultStyle,
   copyHandler(tf, data) {
     data.mat = tf.multiply(new Transform(data.mat)).m;

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

@@ -26,6 +26,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   emit,
   props,
   getMouseStyle,
+  transformType: 'line',
   defaultStyle,
   copyHandler(tf, data) {
     data.points = data.points.map((v) => tf.point(v));

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

@@ -26,6 +26,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   emit,
   props,
   getMouseStyle,
+  transformType: 'line',
   defaultStyle,
   copyHandler(tf, data) {
     data.points = data.points.map((v) => tf.point(v));

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

@@ -27,6 +27,7 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus({
   props,
   getMouseStyle,
   defaultStyle,
+  transformType: 'line',
   copyHandler(tf, data) {
     data.points = data.points.map((v) => tf.point(v));
     return data;

+ 30 - 17
src/core/components/text/index.ts

@@ -1,38 +1,51 @@
 import { Transform } from "konva/lib/Util";
 import { InteractiveMessage } from "../../hook/use-interactive.ts";
-import { TextConfig } from "konva/lib/shapes/Text";
+import { Text, TextConfig } from "konva/lib/shapes/Text";
 import { themeMouseColors } from "@/constant/help-style.ts";
-import { BaseItem, getBaseItem } from "../util.ts";
+import { BaseItem, generateSnapInfos, getBaseItem, getRectSnapPoints } from "../util.ts";
+import { getMouseColors } from "@/utils/colors.ts";
+import { shallowReactive } from "vue";
 
 export { default as Component } from "./text.vue";
 
 export const shapeName = "文字";
 export const defaultStyle = {
-  stroke: themeMouseColors.theme,
+  // stroke: themeMouseColors.theme,
   fill: themeMouseColors.theme,
-  strokeWidth: 1,
+  // strokeWidth: 0,
   fontFamily: 'Calibri',
   fontSize: 16,
-  width: 300
 };
 
 export const addMode = 'dot'
 
-export const style = {
-  default: defaultStyle,
-  hover: {
-    stroke: themeMouseColors.hover,
-    fill: themeMouseColors.hover,
-  },
-  press: {
-    stroke: themeMouseColors.press,
-    fill: themeMouseColors.press,
-  },
-};
-
 export type TextData = Partial<typeof defaultStyle> & BaseItem & {
   mat: number[]
   content: string
+  width?: number
+};
+
+
+export const getMouseStyle = (data: TextData) => {
+  const fillStatus = getMouseColors(data.fill || defaultStyle.fill);
+  // const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
+  // const strokeWidth = data.strokeWidth || defaultStyle.strokeWidth;
+
+  return {
+    default: { fill: fillStatus.pub },
+    hover: { fill: fillStatus.hover },
+    press: { fill: fillStatus.press },
+  };
+};
+
+export const textNodeMap: Record<BaseItem['id'], Text> = shallowReactive({})
+
+export const getSnapInfos = (data: TextData) => {
+  if (!textNodeMap[data.id]) return []
+  const node = textNodeMap[data.id]
+  const tf = new Transform(data.mat)
+  const points = getRectSnapPoints(data.width || node.width(), node.height(), 0, 0).map((v) => tf.point(v));
+  return generateSnapInfos(points, true, false);
 };
 
 export const dataToConfig = (data: TextData): TextConfig => ({

+ 26 - 20
src/core/components/text/temp-text.vue

@@ -1,30 +1,36 @@
 <template>
-	<v-text :config="config" ref="shape" name="text">
-	</v-text>
+  <v-text
+    :config="{
+      ...data,
+      ...matConfig,
+      align: 'center',
+      verticalAlign: 'center',
+      text: data.content,
+      zIndex: undefined,
+      opacity: addMode ? 0.3 : 1,
+    }"
+    ref="shape"
+    name="text"
+  >
+  </v-text>
 </template>
 
 <script lang="ts" setup>
-import { TextData, dataToConfig } from "./index.ts";
+import { TextData } from "./index.ts";
 import { computed, ref } from "vue";
 import { DC } from "@/deconstruction.js";
 import { Transform } from "konva/lib/Util";
-import { Group } from "konva/lib/Group";
+import { Text } from "konva/lib/shapes/Text";
 
-const props = defineProps<{ data: TextData, addMode?: boolean }>()
-const shape = ref<DC<Group>>()
-defineExpose({ 
+const props = defineProps<{ data: TextData; addMode?: boolean }>();
+const shape = ref<DC<Text>>();
+defineExpose({
   get shape() {
-    return shape.value
-  } 
-})
+    return shape.value;
+  },
+});
 
-const config = computed(() => {
-	const dec = new Transform(props.data.mat).decompose()
-	const conf = {
-		...dataToConfig(props.data),
-		...dec,
-		opacity: props.addMode ? 0.3 : 1
-	}
-	return conf
-})
-</script>
+const matConfig = computed(() => {
+  return new Transform(props.data.mat).decompose();
+});
+</script>

+ 148 - 0
src/core/components/text/text-dom.vue

@@ -0,0 +1,148 @@
+<template>
+  <Teleport :to="`#${DomMountId}`">
+    <textarea
+      ref="textarea"
+      :style="styles"
+      @input="() => text = textarea!.value"
+      @blur="quit"
+      @click.stop
+      >{{ text }}</textarea
+    >
+  </Teleport>
+</template>
+
+<script lang="ts" setup>
+import { useStage } from "@/core/hook/use-global-vars";
+import { Text } from "konva/lib/shapes/Text";
+import { computed, onUnmounted, ref, watch, watchEffect } from "vue";
+import { DomMountId } from "@/constant/index.ts";
+import { useViewer, useViewerTransform } from "@/core/hook/use-viewer";
+import { listener } from "@/utils/event";
+import { Transform } from "konva/lib/Util";
+import { useGetPointerTextNdx } from "./util";
+
+const props = defineProps<{ shape: Text }>();
+const emit = defineEmits<{ (e: "submit", text: string): void }>();
+
+const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
+const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
+const isEdge = (document as any).documentMode || /Edge/.test(navigator.userAgent);
+const textarea = ref<HTMLTextAreaElement>();
+const stage = useStage();
+const $stage = stage.value?.getNode()!;
+const text = ref(props.shape.text());
+
+const quit = () => emit("submit", text.value);
+
+const refreshMat = () => {
+  const dom = textarea.value!;
+  let mat = props.shape.getAbsoluteTransform();
+  if (isFirefox) {
+    mat = new Transform()
+      .translate(0, -2 - Math.round(props.shape.fontSize() / 20))
+      .multiply(mat);
+  } else {
+    mat = new Transform().translate(0, -0.5).multiply(mat);
+  }
+  dom.style.transform = `matrix(${mat.m.join(",")})`;
+};
+
+const refreshSize = () => {
+  const dom = textarea.value!;
+  let newWidth = props.shape.width();
+  if (isSafari || isFirefox) {
+    newWidth = Math.ceil(newWidth);
+  } else if (isEdge) {
+    newWidth += 1;
+  }
+  dom.style.width = newWidth + "px";
+  dom.style.height = props.shape.height() + props.shape.fontSize() / 2 + "px";
+};
+
+const getPointerTextNdx = useGetPointerTextNdx();
+const focusToPointer = () => {
+  if (!textarea.value) return;
+  const ndx = getPointerTextNdx(props.shape);
+  textarea.value.focus();
+  textarea.value.setSelectionRange(ndx, ndx);
+};
+
+const styles = computed(() => {
+  const shape = props.shape;
+  return {
+    fontSize: shape.fontSize() + "px",
+    lineHeight: shape.lineHeight(),
+    fontFamily: shape.fontFamily(),
+    textAlign: shape.align() as CanvasTextAlign,
+    color: shape.fill().toString(),
+  };
+});
+
+const viewer = useViewer();
+const refresh = () => {
+  refreshSize();
+  refreshMat();
+  const textRect = textarea.value!.getBoundingClientRect();
+  const contRect = $stage.container().getBoundingClientRect();
+  const textR = textRect.x + textRect.width;
+  const textB = textRect.y + textRect.height;
+  const contR = contRect.x + contRect.width;
+  const contB = contRect.y + contRect.height;
+
+  if (
+    textR > contR ||
+    textB > contB ||
+    textRect.x < contRect.x ||
+    textRect.y < contRect.y
+  ) {
+    viewer.viewer.scalePixel(
+      { x: contRect.x + contRect.width / 2, y: contRect.x + contRect.height / 2 },
+      0.8
+    );
+  }
+};
+
+const transform = useViewerTransform();
+watchEffect(() => props.shape.text(text.value), { flush: "sync" });
+watch(
+  () => [textarea.value, text.value, transform.value],
+  () => textarea.value && refresh()
+);
+
+watch(
+  () => [props.shape, textarea.value] as const,
+  ([shape, dom]) => {
+    if (shape && dom) {
+      focusToPointer();
+    }
+  }
+);
+
+let unListener: () => void;
+const timeout = setTimeout(() => {
+  unListener = listener($stage.container(), "click", quit);
+}, 16);
+
+onUnmounted(() => {
+  if (unListener!) {
+    unListener();
+  } else {
+    clearTimeout(timeout);
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+textarea {
+  position: absolute;
+  border: 0;
+  padding: 0;
+  margin: 0;
+  overflow: hidden;
+  background: none;
+  resize: none;
+  pointer-events: all;
+  outline: none;
+  transform-origin: left top;
+}
+</style>

+ 135 - 44
src/core/components/text/text.vue

@@ -1,60 +1,151 @@
 <template>
-  <TempText :data="{ ...atData, ...animation.data }" :ref="(e: any) => shape = e.shape" />
+  <TextDom v-if="editText && $shape" :shape="$shape" @submit="quitHandler" />
+  <TempText
+    :data="tData"
+    :ref="(v: any) => shape = v?.shape"
+    :id="data.id"
+    @dblclick="dbclickHandler"
+  />
+  <PropertyUpdate
+    v-if="!editText"
+    :describes="describes"
+    :data="data"
+    :target="shape"
+    @change="emit('updateShape', { ...data })"
+  />
+  <!-- <Operate :target="shape" :menus="operateMenus" v-if="!editText" /> -->
 </template>
 
 <script lang="ts" setup>
+import { TextData, getMouseStyle, defaultStyle, textNodeMap } from "./index.ts";
+import { PropertyUpdate, Operate } from "../../propertys";
 import TempText from "./temp-text.vue";
-import { TextData, style } from "./index.ts";
-import { computed, ref, watch, watchEffect } from "vue";
-
-import { DC } from "@/deconstruction.js";
-import { useShapeTransformer } from "../../hook/use-transformer.ts";
-import { useAutomaticData } from "../../hook/use-automatic-data.ts";
-import { useMouseStyle } from "../../hook/use-mouse-status.ts";
-import { useAniamtion } from "../../hook/use-animation.ts";
+import TextDom from "./text-dom.vue";
+import { useComponentStatus } from "@/core/hook/use-component.ts";
+import {
+  cloneRepShape,
+  useCustomTransformer,
+  useTransformer,
+} 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 { setShapeTransform } from "@/utils/shape.ts";
+import { zeroEq } from "@/utils/math.ts";
+import { MathUtils } from "three";
+import { KonvaEventObject } from "konva/lib/Node";
 
-const props = defineProps<{ data: TextData; addMode?: boolean }>();
-const emit = defineEmits<{ (e: "update", value: TextData): void }>();
-const shape = ref<DC<Text>>();
+const props = defineProps<{ data: TextData }>();
+const emit = defineEmits<{
+  (e: "updateShape", value: TextData): void;
+  (e: "addShape", value: TextData): void;
+  (e: "delShape"): void;
+}>();
 
 const minWidth = computed(() => (props.data.fontSize || 12) * 2);
-const transform = useShapeTransformer(shape, {
-  enabledAnchors: ["middle-left", "middle-right"],
-  flipEnabled: false,
-  boundBoxFunc: (oldBox, newBox) => {
-    if (Math.abs(newBox.width) < minWidth.value) {
-      return oldBox;
-    }
-    return newBox;
-  },
-});
-const atData = useAutomaticData(() => props.data);
-
-watchEffect(() => {
-  const $text = shape.value?.getNode();
-  if (!$text) return;
-  $text.on("transform", () => {
-    const newWidth = Math.max($text.width() * $text.scaleX(), minWidth.value);
-    $text.setAttrs({
-      width: newWidth,
-      scaleX: 1,
-      scaleY: 1,
+const { shape, tData, data, operateMenus, describes } = useComponentStatus<
+  Text,
+  TextData
+>({
+  emit,
+  props,
+  getMouseStyle,
+  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) {
+        const repShape = cloneRepShape($shape).shape;
+        return {
+          shape: repShape,
+          update(data) {
+            data.width && repShape.width(data.width);
+            setShapeTransform(repShape, new Transform(data.mat));
+          },
+        };
+      },
+      transformerConfig: {
+        rotateEnabled: true,
+        keepRatio: true,
+        enabledAnchors: ["middle-left", "middle-right"],
+        flipEnabled: false,
+        boundBoxFunc: (oldBox, newBox) => {
+          if (Math.abs(newBox.width) < minWidth.value) {
+            return oldBox;
+          }
+          return newBox;
+        },
+      },
+      beforeHandler(data, mat) {
+        return { ...data, ...update(mat, data) };
+      },
+      handler(data, mat) {
+        const setAttrib = update(mat, data);
+        Object.assign(data, setAttrib);
+        if (setAttrib.width) {
+          return true;
+        }
+      },
+      callback,
     });
-  });
+  },
+  copyHandler(tf, data) {
+    return {
+      ...data,
+      mat: tf.multiply(new Transform(data.mat)).m,
+    };
+  },
 });
 
-watch(transform, (transform, oldTransform) => {
-  if (!transform && oldTransform) {
-    const $text = shape.value!.getNode();
-    emit("update", {
-      ...atData.value,
-      width: $text.width(),
-      mat: $text.getTransform().m,
-    });
+watchEffect((oncleanup) => {
+  if (shape.value?.getNode()) {
+    textNodeMap[props.data.id] = shape.value.getNode();
+    oncleanup(() => delete textNodeMap[props.data.id]);
   }
 });
 
-const { currentStyle } = useMouseStyle({ style, shape });
-const animation = useAniamtion(currentStyle);
+const transformer = useTransformer();
+const $shape = computed(() => shape.value?.getNode());
+const editText = ref(false);
+const dbclickHandler = (ev: KonvaEventObject<MouseEvent>) => {
+  editText.value = true;
+  $shape.value && $shape.value.hide();
+  transformer.hide();
+};
+const quitHandler = (val: string) => {
+  console.log("quit");
+  editText.value = false;
+  transformer.show();
+  $shape.value && $shape.value.show();
+  if (val !== data.value.content) {
+    data.value.content = val;
+  }
+};
 </script>
+
+<style scoped lang="scss"></style>

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

@@ -0,0 +1,99 @@
+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 = () => {
+  const stage = useStage();
+
+  return (shape: Text) => {
+    let pointerPos = stage.value?.getNode().pointerPos;
+    const str = shape.text();
+    if (!pointerPos || str.length === 0) {
+      return str.length;
+    }
+
+    const transform = shape.getAbsoluteTransform().invert()
+    const textPos = {x: 0, y: 0}
+    const finalPos = transform.point({
+      x: pointerPos.x - textPos.x,
+      y: pointerPos.y - textPos.y,
+    });
+
+    const width = shape.width();
+    const textHeight = shape.textHeight;
+    const lineNdx = Math.floor(finalPos.y / textHeight);
+    const textArr = shape.textArr;
+    let ndx = str.length;
+
+    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 after = false;
+    for (; i < line.text.length; i++) {
+      const size = shape.measureSize(line.text[i]);
+      x += size.width;
+      if (x > finalPos.x) {
+        const diff = x - finalPos.x
+        console.log(diff, hanlfSize, shape.fontSize())
+        if (diff < hanlfSize && line.text.length >= i + 1) {
+          after = true;
+        }
+        break;
+      }
+    }
+    console.log(i, after)
+    if (i === line.text.length) {
+      ndx = line.text.length - 1;
+      after = true;
+    } else {
+      ndx = i;
+    }
+
+    let strNdx = 0;
+    let emptyCount = 0;
+    for (let i = 0; i < lineNdx; i++) {
+      const cStr = textArr[i].text;
+      if (!cStr) {
+        // emptyCount++;
+      } else if (i !== lineNdx) {
+        strNdx = strNdx + str.substring(strNdx).indexOf(cStr) + cStr.length;
+        emptyCount = 0;
+      } else {
+        strNdx = strNdx + str.substring(strNdx).indexOf(cStr);
+        emptyCount = 0;
+      }
+      if (i !== lineNdx) {
+        if (textArr[i].lastInParagraph) {
+          strNdx += 1;
+        }
+      }
+    }
+
+    const char = line.text[ndx];
+    const yStr = str.substring(strNdx + emptyCount);
+    i = ndx;
+    for (; i < yStr.length; i++) {
+      if (yStr[i] === char) {
+        break;
+      }
+    }
+    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;
+  };
+};

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

@@ -27,6 +27,7 @@ const { shape, tData, operateMenus, describes } = useComponentStatus({
   props,
   getMouseStyle,
   defaultStyle,
+  transformType: "line",
   copyHandler(tf, data) {
     data.points = data.points.map((v) => tf.point(v));
     return data;

+ 1 - 0
src/core/helper/snap-lines.vue

@@ -5,6 +5,7 @@
     :ref="(l: any) => lines[i] = l"
   />
   <v-circle
+    v-if="debug"
     v-for="axisConfig in axisConfigs"
     :config="{ x: axisConfig.x, y: axisConfig.y, radius: 3, fill: '#000' }"
   />

+ 15 - 14
src/core/hook/use-component.ts

@@ -5,7 +5,7 @@ import { useMouseMigrateTempLayer, useZIndex } from "./use-layer";
 import { useAnimationMouseStyle } from "./use-mouse-status";
 import { DrawItem } from "../components";
 import {
-  useCompTransformer,
+  useMatCompTransformer,
   useLineTransformer,
 } from "./use-transformer";
 import { useGetShapeCopyTransform } from "./use-copy";
@@ -22,12 +22,13 @@ type Emit<T> = EmitFn<{
   delShape: () => void;
 }>;
 
-export type UseComponentStatusProps<T extends DrawItem> = {
+export type UseComponentStatusProps<T extends DrawItem, S extends EntityShape> = {
   emit: Emit<T>;
   props: { data: T };
   getMouseStyle: any;
   defaultStyle: any;
-  line?: boolean;
+  transformType?: 'line' | 'mat' | 'custom'
+  customTransform?: (callback: () => void, shape: Ref<DC<S> | undefined>, data: Ref<T>) => void,
   getRepShape?: () => Shape;
   copyHandler: (transform: Transform, data: T) => T;
 };
@@ -62,14 +63,15 @@ const getPropertyDescribes = (data: Ref<any>): PropertyDescribes => ({
 });
 
 export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
-  args: UseComponentStatusProps<T>
+  args: UseComponentStatusProps<T, S>
 ) => {
   const {
     emit,
     props,
     getMouseStyle,
+    transformType,
     defaultStyle,
-    line = true,
+    customTransform,
     getRepShape,
     copyHandler,
   } = args;
@@ -82,23 +84,22 @@ export const useComponentStatus = <S extends EntityShape, T extends DrawItem>(
     getMouseStyle,
   }) as any;
 
-  if (line) {
+  if (transformType === 'line') {
     useLineTransformer(
       shape as any,
       data as any,
-      (newData) => {
-        emit("updateShape", newData as T);
-      },
+      (newData) => emit("updateShape", newData as T),
       getRepShape as any
     );
-  } else {
-    useCompTransformer(
+  } else if (transformType === 'mat') {
+    useMatCompTransformer(
       shape,
       data as any,
-      (nData) => {
-        emit("updateShape", nData as any);
-      },
+      (nData) => emit("updateShape", nData as any),
     );
+  } else if (transformType === 'custom' && customTransform) {
+    console.log('????')
+    customTransform(() => emit("updateShape", data.value as any), shape, data)
   }
 
   useZIndex(shape, data);

+ 6 - 3
src/core/hook/use-snap.ts

@@ -30,7 +30,7 @@ import {
   useTransformer,
 } from "./use-transformer";
 import { Transform } from "konva/lib/Util";
-import { useUnitTransform, useViewerInvertTransform } from "./use-viewer";
+import { useCacheUnitTransform, useViewerInvertTransform } from "./use-viewer";
 import { MathUtils } from "three";
 import { arrayInsert, rangMod } from "@/utils/shared";
 
@@ -80,7 +80,7 @@ export const useGlobalSnapInfos = installGlobalVar(() => {
 });
 
 export const useSnapConfig = () => {
-  const unitTransform = useUnitTransform();
+  const unitTransform = useCacheUnitTransform();
   return {
     get snapOffset() {
       return unitTransform.getPixel(5);
@@ -400,7 +400,10 @@ const scaleSnap = (
       exclude.add(nor);
       return;
     }
-    if (!eqPoint(vector(join).sub(cur).normalize(), operVector)) {
+
+    if (
+      Math.abs(lineLen(vector(join).sub(origin).normalize(), operVector)) > 0.3
+    ) {
       exclude.add(nor);
       return;
     }

+ 3 - 3
src/core/hook/use-transformer.ts

@@ -424,7 +424,7 @@ export type CustomTransformerProps<
   openSnap?: boolean;
   getRepShape?: GetRepShape<S, T>;
   beforeHandler?: (data: T, mat: Transform) => T;
-  handler?: (data: T, mat: Transform) => Transform | void;
+  handler?: (data: T, mat: Transform) => Transform | void | true;
   callback?: (data: T) => void;
   transformerConfig?: TransformerConfig;
 };
@@ -473,7 +473,7 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
       if (mat) {
         if (repResult.update) {
           repResult.update(data.value, repResult.shape);
-        } else {
+        } else if (mat !== true) {
           setShapeTransform(repResult.shape, mat);
         }
         transformer.forceUpdate();
@@ -548,7 +548,7 @@ export const useLineTransformer = <T extends LineTransformerData>(
   });
 };
 
-export const useCompTransformer = <T extends BaseItem & { mat: number[] }>(
+export const useMatCompTransformer = <T extends BaseItem & { mat: number[] }>(
   shape: Ref<DC<EntityShape> | undefined>,
   data: Ref<T>,
   callback: (data: T) => void

+ 103 - 93
src/core/hook/use-viewer.ts

@@ -1,112 +1,122 @@
 import { Viewer } from "../viewer.ts";
 import { computed, ref, watch } from "vue";
 import { dragListener, scaleListener } from "../../utils/event.ts";
-import { globalWatch, installGlobalVar, useMode, useStage } from "./use-global-vars.ts";
+import {
+  globalWatch,
+  installGlobalVar,
+  useMode,
+  useStage,
+} from "./use-global-vars.ts";
 import { Mode } from "../../constant/mode.ts";
 import { mergeFuns } from "../../utils/shared.ts";
 import { Transform } from "konva/lib/Util";
 import { lineLen } from "@/utils/math.ts";
 
-export const useViewer = installGlobalVar(
-	() => {
-		const stage = useStage();
-		const viewer = new Viewer();
-		const interactive = useMode();
-		const transform = ref(new Transform())
-	
-		const init = (dom: HTMLDivElement) => {
-			const dragDestroy = dragListener(dom, ({end, prev}) => {
-				viewer.movePixel({x: end.x - prev.x, y: end.y - prev.y});
-			});
-			const scaleDestroy = scaleListener(dom, (info) => {
-				viewer.scalePixel(info.center, info.scale);
-			});
-			viewer.bus.on('transformChange', newTransform => {
-				transform.value = newTransform
-			})
-			transform.value = viewer.transform
-			return mergeFuns(dragDestroy, scaleDestroy);
-		};
-	
-		return {
-			var: {
-				transform: transform,
-				viewer,
-			},
-			onDestroy: globalWatch(
-				() => stage.value && interactive.value === Mode.viewer,
-				(can, _, onCleanup) => {
-					if (can) {
-						const dom = stage.value!.getNode().container();
-						onCleanup(init(dom));
-					}
-				},
-				{immediate: true}
-			)
-		}
-	},
-	Symbol("viewer")
-)
+export const useViewer = installGlobalVar(() => {
+  const stage = useStage();
+  const viewer = new Viewer();
+  const interactive = useMode();
+  const transform = ref(new Transform());
 
+  const init = (dom: HTMLDivElement) => {
+    const dragDestroy = dragListener(dom, ({ end, prev }) => {
+      viewer.movePixel({ x: end.x - prev.x, y: end.y - prev.y });
+    });
+    const scaleDestroy = scaleListener(dom, (info) => {
+      viewer.scalePixel(info.center, info.scale);
+    });
+    viewer.bus.on("transformChange", (newTransform) => {
+      transform.value = newTransform;
+    });
+    transform.value = viewer.transform;
+    return mergeFuns(dragDestroy, scaleDestroy);
+  };
 
-export const useViewerTransform = installGlobalVar(
-	() => {
-		const viewer = useViewer()
-		return viewer.transform
-	},
-	Symbol('viewTransform')
-)
+  return {
+    var: {
+      transform: transform,
+      viewer,
+    },
+    onDestroy: globalWatch(
+      () => stage.value && interactive.value === Mode.viewer,
+      (can, _, onCleanup) => {
+        if (can) {
+          const dom = stage.value!.getNode().container();
+          onCleanup(init(dom));
+        }
+      },
+      { immediate: true }
+    ),
+  };
+}, Symbol("viewer"));
+
+export const useViewerTransform = installGlobalVar(() => {
+  const viewer = useViewer();
+  return viewer.transform;
+}, Symbol("viewTransform"));
 
 export const useViewerTransformConfig = () => {
-	const transform = useViewerTransform()
-	return computed(() => transform.value.decompose());
-}
+  const transform = useViewerTransform();
+  return computed(() => transform.value.decompose());
+};
 
 export const useViewerInvertTransform = () => {
-	const transform = useViewerTransform()
-	return computed(() => transform.value.copy().invert());
-}
+  const transform = useViewerTransform();
+  return computed(() => transform.value.copy().invert());
+};
 
 export const useViewerInvertTransformConfig = () => {
-	const transform = useViewerInvertTransform()
-	return computed(() => transform.value.decompose());
-}
-
+  const transform = useViewerInvertTransform();
+  return computed(() => transform.value.decompose());
+};
 
 export const useUnitTransform = installGlobalVar(() => {
-	const transform = useViewerTransform()
-	const invTransform = useViewerInvertTransform()
-	let pixelCache: Record<string, number> = {}
-	let realCache: Record<string, number> = {}
-	watch(transform, () => {
-		pixelCache = {}
-	})
-	watch(invTransform, () => {
-		realCache = {}
-	})
+  const transform = useViewerTransform();
+  const invTransform = useViewerInvertTransform();
+
+  return {
+    getPixel(real: number) {
+      return lineLen(
+        invTransform.value.point({ x: real, y: 0 }),
+        invTransform.value.point({ x: 0, y: 0 })
+      );
+    },
+    getReal(pixel: number) {
+      return lineLen(
+        transform.value.point({ x: pixel, y: 0 }),
+        transform.value.point({ x: 0, y: 0 })
+      );
+    },
+  };
+});
+
+export const useCacheUnitTransform = installGlobalVar(() => {
+	const unitTransform = useUnitTransform()
+  const transform = useViewerTransform();
+  const invTransform = useViewerInvertTransform();
+  let pixelCache: Record<string, number> = {};
+  let realCache: Record<string, number> = {};
+  watch(transform, () => {
+    pixelCache = {};
+  });
+  watch(invTransform, () => {
+    realCache = {};
+  });
 
-	return {
-		getPixel(real: number) {
-			if (real in pixelCache) {
-				return pixelCache[real];
-			} else {
-				const len = lineLen(
-					invTransform.value.point({x: real, y: 0}),
-					invTransform.value.point({x: 0, y: 0})
-				)
-				return pixelCache[real] = len
-			}
-		},
-		getReal(pixel: number) {
-			if (pixel in realCache) {
-				return realCache[pixel];
-			} else {
-				const len = lineLen(
-					transform.value.point({x: pixel, y: 0}),
-					transform.value.point({x: 0, y: 0})
-				)
-				return pixelCache[pixel] = len
-			}
-		}
-	}
-})
+  return {
+    getPixel(real: number) {
+      if (real in pixelCache) {
+        return pixelCache[real];
+      } else {
+        return (pixelCache[real] = unitTransform.getPixel(real));
+      }
+    },
+    getReal(pixel: number) {
+      if (pixel in realCache) {
+        return realCache[pixel];
+      } else {
+        return (pixelCache[pixel] = unitTransform.getReal(pixel));
+      }
+    },
+  };
+});

+ 40 - 29
src/core/store/init.ts

@@ -146,13 +146,16 @@ export const initData = {
       ],
     },
   ],
-  // "circle": [
-  // 	{
-  // 		"x": 1372.22265625,
-  // 		"y": 100.3671875,
-  // 		"radius": 84.72265625
-  // 	}
-  // ],
+  circle: [
+    {
+      createTime: 3,
+      zIndex: 0,
+      id: "333a3",
+      x: 1372.22265625,
+      y: 100.3671875,
+      radius: 84.72265625,
+    },
+  ],
   icon: [
     {
       createTime: 3,
@@ -188,26 +191,34 @@ export const initData = {
       mat: [1, 0, 0, 1, 954.53515625, 519.8671875],
     },
   ],
-  // "text": [
-  // 	{
-  // 		"fill": "#000",
-  // 		"stroke": "red",
-  // 		"strokeWidth": 3,
-  // 		"fontFamily": "Calibri",
-  // 		"fontSize": 30,
-  // 		"width": 100,
-  // 		"content": "Hello from the Konva framework. Try to resize me.",
-  // 		"mat": [1, 0, 0, 1, 484.13671875, 663.13671875],
-  // 	},
-  // 	{
-  // 		"fill": "#000",
-  // 		"stroke": "red",
-  // 		"strokeWidth": 3,
-  // 		"fontFamily": "Calibri",
-  // 		"fontSize": 30,
-  // 		"width": 300,
-  // 		"content": "文字",
-  // 		"mat": [1, 0, 0, 1, 873.94921875, 659.453125],
-  // 	}
-  // ],
+  text: [
+    {
+      createTime: 3,
+      zIndex: 0,
+      id: "333asdt3",
+      fill: "#000",
+      fontFamily: "Calibri",
+      fontSize: 30,
+      content:
+        "Hello\n  from th\n\ne     fr \n am \n \new\n\nork. Tr \ny  \n   to resize me.",
+      mat: [
+        1.8046845338884818e-14, 0.9999999999999952, -1.000000000000013,
+        1.6450899740986086e-14, 807.4957275390657, 322.26090819029116,
+      ],
+      width: 60,
+    },
+    {
+      createTime: 3,
+      zIndex: 0,
+      id: "3t3asd33",
+      fill: "#000",
+      // "stroke": "red",
+      // "strokeWidth": 3,
+      fontFamily: "Calibri",
+      fontSize: 30,
+      width: 300,
+      content: "文字",
+      mat: [1, 0, 0, 1, 873.94921875, 659.453125],
+    },
+  ],
 };

+ 41 - 0
src/utils/dom.ts

@@ -0,0 +1,41 @@
+
+function getCaretPosition(element: HTMLInputElement | HTMLTextAreaElement, x: number, y: number) {  
+  const text = element.childNodes[0]
+  const range = element.ownerDocument.createRange();  
+  const start = 0;  
+  const end = element.value.length;  
+  let caretPosition = start;  
+
+  range.setStart(text, 0);  
+  range.setEnd(text, end);  
+
+  range.setStart(text, 0);  
+  for (let i = start; i <= end; i++) {  
+    range.setEnd(text, i);  
+    console.log(range.getBoundingClientRect())
+    if (range.getBoundingClientRect().left >= x && range.getBoundingClientRect().top >= y) {  
+      caretPosition = i;  
+      break;  
+    }  
+  }  
+
+  return caretPosition;  
+}  
+
+function setCaretPosition(element: any, position: number) {  
+  element.focus();  
+  if (element.setSelectionRange) {  
+    element.setSelectionRange(position, position);  
+  } else if (element.createTextRange) {  
+    const range = element.createTextRange();  
+    range.collapse(true);  
+    range.moveEnd('character', position);  
+    range.moveStart('character', position);  
+    range.select();  
+  }  
+}  
+
+export const focusToMouse = (element: HTMLInputElement | HTMLTextAreaElement, event: MouseEvent) => {
+  const caretPosition = getCaretPosition(element, event.clientX, event.clientY);  
+  setCaretPosition(element, caretPosition);  
+}