Pārlūkot izejas kodu

feat: 添加表格核心功能

bill 6 mēneši atpakaļ
vecāks
revīzija
8e2d32777a

+ 4 - 0
src/core/components/circle/circle.vue

@@ -19,6 +19,7 @@ import { MathUtils } from "three";
 import { cloneRepShape, useCustomTransformer } from "@/core/hook/use-transformer.ts";
 import { Ellipse } from "konva/lib/shapes/Ellipse";
 import { Pos } from "@/utils/math.ts";
+import { copy } from "@/utils/shared.ts";
 
 const props = defineProps<{ data: CircleData }>();
 const emit = defineEmits<{
@@ -68,6 +69,9 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
           shape: repShape,
         };
       },
+      beforeHandler(data, mat) {
+        return matToData(copy(data), mat);
+      },
       handler(data, mat) {
         matToData(data, mat, initRadius);
         return true;

+ 6 - 2
src/core/components/index.ts

@@ -8,8 +8,10 @@ import * as text from './text'
 import * as icon from './icon'
 import * as image from './image'
 import * as bgImage from './bg-image'
+import * as table from './table'
 
 import { ArrowData } from './arrow'
+import { TableData } from './table'
 import { RectangleData } from './rectangle'
 import { CircleData } from './circle'
 import { TriangleData } from './triangle'
@@ -31,7 +33,8 @@ const _components = {
 	text,
 	icon,
 	image,
-	bgImage
+	bgImage,
+	table
 }
 
 export const components = _components as Components
@@ -53,7 +56,8 @@ export type DrawDataItem = {
 	text: TextData,
 	icon: IconData,
 	image: ImageData,
-	bgImage: BGImageData
+	bgImage: BGImageData,
+	table: TableData
 }
 export type ShapeType = keyof DrawDataItem
 

+ 15 - 11
src/core/components/line/temp-line.vue

@@ -11,17 +11,19 @@
       }"
     >
     </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)"
-    />
+    <template v-if="!status.active">
+      <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)"
+      />
+    </template>
   </v-group>
 </template>
 
@@ -36,6 +38,7 @@ import { Pos } from "@/utils/math.ts";
 import { useCustomSnapInfos } from "@/core/hook/use-snap.ts";
 import { generateSnapInfos } from "../util.ts";
 import { ComponentSnapInfo } from "../index.ts";
+import { useMouseShapeStatus } from "@/core/hook/use-mouse-status.ts";
 
 const props = defineProps<{
   data: LineData;
@@ -90,6 +93,7 @@ const endHandler = () => {
 };
 
 const shape = ref<DC<Line>>();
+const status = useMouseShapeStatus(shape);
 defineExpose({
   get shape() {
     return shape.value;

+ 15 - 11
src/core/components/polygon/temp-polygon.vue

@@ -11,17 +11,19 @@
       }"
     >
     </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)"
-    />
+    <template v-if="!status.active">
+      <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)"
+      />
+    </template>
   </v-group>
 </template>
 
@@ -36,6 +38,7 @@ import { Pos } from "@/utils/math.ts";
 import { useCustomSnapInfos } from "@/core/hook/use-snap.ts";
 import { ComponentSnapInfo } from "../index.ts";
 import { generateSnapInfos } from "../util.ts";
+import { useMouseShapeStatus } from "@/core/hook/use-mouse-status.ts";
 const props = defineProps<{ data: PolygonData; addMode?: boolean }>();
 const emit = defineEmits<{
   (e: "update:position", data: { ndx: number; val: Pos }): void;
@@ -44,6 +47,7 @@ const emit = defineEmits<{
 
 const data = computed(() => ({ ...defaultStyle, ...props.data }));
 const shape = ref<DC<Line>>();
+const status = useMouseShapeStatus(shape);
 defineExpose({
   get shape() {
     return shape.value;

+ 13 - 0
src/core/components/share/point.vue

@@ -13,6 +13,8 @@ import { getMouseColors } from "@/utils/colors";
 import { useGlobalSnapInfos, useSnap } from "@/core/hook/use-snap";
 import { generateSnapInfos } from "../util";
 import { ComponentSnapInfo } from "..";
+import { useMouseShapeStatus } from "@/core/hook/use-mouse-status";
+import { useCursor } from "@/core/hook/use-global-vars";
 
 const props = defineProps<{
   position: Pos;
@@ -50,6 +52,17 @@ const refSnapInfos = computed(() => {
 const snap = useSnap(refSnapInfos);
 const circle = ref<DC<Circle>>();
 const offset = useShapeDrag(circle);
+const status = useMouseShapeStatus(circle);
+const cursor = useCursor();
+watch(
+  () => status.value.hover,
+  (hover, _, onCleanup) => {
+    if (hover) {
+      onCleanup(cursor.push("move"));
+    }
+  }
+);
+
 let init: Pos;
 watch(offset, (offset, oldOffsert) => {
   snap.clear();

+ 74 - 67
src/core/components/text/text-dom.vue

@@ -1,5 +1,5 @@
 <template>
-  <Teleport :to="mount">
+  <Teleport :to="mount" v-if="show">
     <textarea
       ref="textarea"
       :style="styles"
@@ -15,15 +15,20 @@
 <script lang="ts" setup>
 import { useStage } from "@/core/hook/use-global-vars";
 import { Text } from "konva/lib/shapes/Text";
-import { computed, onUnmounted, ref, watch } from "vue";
+import { computed, ref, watch } from "vue";
 import { DomMountId } from "@/constant/index.ts";
-import { useViewer, useViewerTransform } from "@/core/hook/use-viewer";
+import { useViewerTransform } from "@/core/hook/use-viewer";
 import { listener } from "@/utils/event";
 import { Transform } from "konva/lib/Util";
-import { useGetPointerTextNdx } from "./util";
+import { useGetPointerTextNdx } from "@/core/hook/use-text-pointer";
+import { mergeFuns } from "@/utils/shared";
 
 const props = defineProps<{ shape: Text }>();
-const emit = defineEmits<{ (e: "submit", text: string): void }>();
+const emit = defineEmits<{
+  (e: "submit", text: string): void;
+  (e: "show"): void;
+  (e: "hide"): void;
+}>();
 
 const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
 const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
@@ -34,18 +39,16 @@ const $stage = stage.value?.getNode()!;
 const text = ref(props.shape.text());
 const mount = document.querySelector(`#${DomMountId}`) as HTMLDivElement;
 
-const quit = () => emit("submit", text.value);
+const quit = () => {
+  emit("submit", text.value);
+  emit("hide");
+  show.value = false;
+};
 const focusHandler = (ev: any) => {
   ev.preventDefault();
   mount.scroll(0, 0);
 };
 
-listener(mount, "keydown", (ev) => {
-  if (ev.key === "Escape") {
-    textarea.value?.blur();
-  }
-});
-
 const refreshMat = () => {
   const dom = textarea.value!;
   let mat = props.shape.getAbsoluteTransform();
@@ -61,14 +64,17 @@ const refreshMat = () => {
 
 const refreshSize = () => {
   const dom = textarea.value!;
-  let newWidth = props.shape.width() - props.shape.padding() * 2;
+  let newWidth = props.shape.width();
   if (isSafari || isFirefox) {
     newWidth = Math.ceil(newWidth);
   } else if (isEdge) {
     newWidth += 1;
   }
+  const pad = props.shape.padding();
   dom.style.width = newWidth + "px";
-  dom.style.height = props.shape.height() - props.shape.padding() * 2 + 5 + "px";
+  dom.style.height = props.shape.height() + "px";
+  dom.style.padding = pad + "px";
+  dom.style.height = dom.scrollHeight + 2 + "px";
 };
 
 const getPointerTextNdx = useGetPointerTextNdx();
@@ -91,83 +97,84 @@ const styles = computed(() => {
   };
 });
 
-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();
+const show = ref(false);
 watch(
-  text,
-  () => {
-    props.shape.text(text.value);
-    props.shape.fire("bound-change");
+  () => props.shape,
+  (shape) => {
+    shape.on("dblclick", () => {
+      text.value = shape.text();
+      show.value = true;
+      emit("show");
+    });
   },
-  { flush: "sync" }
-);
-watch(
-  () => [textarea.value, text.value, transform.value],
-  () => textarea.value && refresh()
+  { immediate: true }
 );
 
 watch(
-  () => [props.shape, textarea.value] as const,
-  ([shape, dom]) => {
-    if (shape && dom) {
-      focusToPointer();
-    }
+  () => show.value && textarea.value,
+  (ready, _, onCleanup) => {
+    if (!ready) return;
+
+    props.shape.hide();
+
+    const quitHooks = [
+      watch(
+        text,
+        () => {
+          props.shape.text(text.value);
+          props.shape.fire("bound-change");
+        },
+        { flush: "sync" }
+      ),
+      watch(() => [text.value, transform.value], refresh, { immediate: true }),
+      watch(
+        () => [props.shape, textarea.value] as const,
+        ([shape, dom]) => {
+          if (shape && dom) {
+            focusToPointer();
+          }
+        },
+        { immediate: true }
+      ),
+      listener(mount, "keydown", (ev) => {
+        if (ev.key === "Escape") {
+          textarea.value?.blur();
+        }
+      }),
+    ];
+
+    const timeout = setTimeout(() => {
+      quitHooks.push(listener($stage.container(), "click", quit));
+    }, 16);
+    onCleanup(() => {
+      props.shape.show();
+      mergeFuns(quitHooks)();
+      quitHooks.length = 0;
+      clearTimeout(timeout);
+    });
   }
 );
-
-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;
-  // word-break: break-all;
-  // overflow-wrap: break-word;
+  word-break: break-all;
+  overflow-wrap: break-word;
   transform-origin: left top;
-  color: rgba(255, 0, 0, 0.8) !important;
+  box-sizing: border-box;
+  outline: 1px solid #000;
 }
 
 div {

+ 115 - 0
src/core/components/table/index.ts

@@ -0,0 +1,115 @@
+import { Transform } from "konva/lib/Util";
+import { themeMouseColors } from "@/constant/help-style.ts";
+import {
+  BaseItem,
+  generateSnapInfos,
+  getBaseItem,
+  getRectSnapPoints,
+} from "../util.ts";
+import { getMouseColors } from "@/utils/colors.ts";
+import { AddMessage } from "@/core/hook/use-draw.ts";
+
+export { default as Component } from "./table.vue";
+export { default as TempComponent } from "./temp-table.vue";
+
+export const shapeName = "表格";
+export const defaultStyle = {
+  stroke: themeMouseColors.theme,
+  strokeWidth: 1,
+  fontSize: 16,
+  align: "center",
+  fontStyle: "normal",
+  fontColor: themeMouseColors.theme,
+};
+export const defaultCollData = {
+  fontFamily: "Calibri",
+  fontSize: 16,
+  align: "center",
+  fontStyle: "normal",
+  fontColor: themeMouseColors.theme,
+};
+
+export const addMode = "area";
+
+export type TableCollData = Partial<typeof defaultCollData> & {
+  content: string;
+  width: number;
+  height: number;
+  padding: number
+};
+export type TableData = Partial<typeof defaultStyle> &
+  BaseItem & {
+    mat: number[];
+    content: TableCollData[][];
+    width: number;
+    height: number;
+  };
+
+export const getMouseStyle = (data: TableData) => {
+  const strokeStatus = getMouseColors(data.stroke || defaultStyle.stroke);
+
+  return {
+    default: { stroke: strokeStatus.pub },
+    hover: { stroke: strokeStatus.hover },
+    press: { stroke: strokeStatus.press },
+  };
+};
+
+export const getSnapPoints = (data: TableData) => {
+  const tf = new Transform(data.mat);
+  const points = getRectSnapPoints(data.width, data.height, 0, 0)
+    .map((v) => tf.point(v));
+  return points
+};
+
+export const getSnapInfos = (data: TableData) => {
+  return generateSnapInfos(getSnapPoints(data), true, false);
+};
+
+export const interactiveToData = (
+  info: AddMessage<"table">,
+  preset: Partial<TableData> = {}
+): TableData | undefined => {
+  if (info.cur) {
+    const item = {
+      ...defaultStyle,
+      ...getBaseItem(),
+      ...preset,
+    } as unknown as TableData;
+    return interactiveFixData(item, info);
+  }
+};
+
+const autoCollWidth = 100;
+const autoCollHeight = 50;
+export const interactiveFixData = (
+  data: TableData,
+  info: AddMessage<"table">
+) => {
+  if (info.cur) {
+    const area = info.cur!;
+    const origin = {
+      x: Math.min(area[0].x, area[1].x),
+      y: Math.min(area[0].y, area[1].y),
+    }
+    data.width = Math.abs(area[0].x - area[1].x)
+    data.height = Math.abs(area[0].y - area[1].y)
+    
+    const colNum = Math.floor(data.width / autoCollWidth) || 1;
+    const rawNum = Math.floor(data.height / autoCollHeight) || 1;
+    const temp = data.content?.[0]?.[0] || {
+      content: ""
+    };
+
+    data.content = Array.from({ length: rawNum }, () =>
+      Array.from({ length: colNum }, () => ({
+        ...temp,
+        width: data.width / colNum,
+        height: data.height / rawNum,
+        padding: 8
+      }))
+    );
+    data.mat = new Transform().translate(origin.x, origin.y).m;
+  }
+  return data;
+};

+ 439 - 0
src/core/components/table/table.vue

@@ -0,0 +1,439 @@
+<template>
+  <TempTable :data="tData" ref="tableRef" :id="data.id" />
+  <PropertyUpdate
+    :describes="describes"
+    :data="data"
+    :target="shape"
+    @change="emit('updateShape', { ...data })"
+  />
+  <Operate
+    :target="shape"
+    :menus="operateMenus"
+    @show="menuShowHandler"
+    @hide="menuHideHandler"
+  />
+
+  <template v-for="(raw, rawNdx) in data.content">
+    <template v-for="(_, colNdx) in raw">
+      <TextDom
+        v-if="tableRef?.texts[rawNdx] && tableRef?.texts[rawNdx][colNdx]?.getNode()"
+        :shape="tableRef.texts[rawNdx][colNdx].getNode()!"
+        @submit="(val) => submitInputHandler(val, rawNdx, colNdx)"
+        @show="showInputHandler"
+        @hide="quitInputHandler"
+      />
+    </template>
+  </template>
+</template>
+
+<script lang="ts" setup>
+import TempTable from "./temp-table.vue";
+import TextDom from "../share/text-area.vue";
+import { TableData, getMouseStyle, defaultStyle, TableCollData } from "./index.ts";
+import { PropertyUpdate, Operate } from "../../propertys";
+import { useComponentStatus } from "@/core/hook/use-component.ts";
+import { Transform } from "konva/lib/Util";
+import { MathUtils } from "three";
+import {
+  useCustomTransformer,
+  useGetTransformerOperType,
+} from "@/core/hook/use-transformer.ts";
+import { copy, getResizeCorsur } from "@/utils/shared.ts";
+import { computed, ref, watch, watchEffect } from "vue";
+import { Group } from "konva/lib/Group";
+import { DC } from "@/deconstruction.js";
+import { Minus, Plus } from "@element-plus/icons-vue";
+import { Rect } from "konva/lib/shapes/Rect";
+import { setShapeTransform } from "@/utils/shape.ts";
+import { Text } from "konva/lib/shapes/Text";
+import {
+  useMouseShapesStatus,
+  useMouseShapeStatus,
+} from "@/core/hook/use-mouse-status.ts";
+import { useCursor, usePointerPos } from "@/core/hook/use-global-vars.ts";
+import { numEq, Pos } from "@/utils/math.ts";
+
+const props = defineProps<{ data: TableData }>();
+const emit = defineEmits<{
+  (e: "updateShape", value: TableData): void;
+  (e: "addShape", value: TableData): void;
+  (e: "delShape"): void;
+}>();
+type TableRef = {
+  shape: DC<Group>;
+  texts: DC<Text>[][];
+  getMouseIntersect: (
+    pos?: Pos
+  ) => {
+    rawBorderNdx: number;
+    colBorderNdx: number;
+    rawNdx: number;
+    colNdx: number;
+  };
+};
+const tableRef = ref<TableRef>();
+const status = useMouseShapeStatus(computed(() => tableRef.value?.shape));
+let inter: Pick<
+  ReturnType<TableRef["getMouseIntersect"]>,
+  "rawBorderNdx" | "colBorderNdx"
+> | null = null;
+const pos = usePointerPos();
+const cursor = useCursor();
+const shapesStatus = useMouseShapesStatus();
+watch(
+  () => status.value.hover && !status.value.press,
+  (hover, _, onCleanup) => {
+    if (!hover) return;
+    onCleanup(
+      watch(
+        () => pos.value && tableRef.value?.getMouseIntersect(pos.value),
+        (inter, _, onCleanup) => {
+          const $shape = shape.value?.getNode();
+          if ($shape && inter && (~inter.colBorderNdx || ~inter.rawBorderNdx)) {
+            onCleanup(
+              cursor.push(getResizeCorsur(!~inter.rawBorderNdx, $shape.rotation()))
+            );
+          }
+        },
+        { immediate: true, flush: "post" }
+      )
+    );
+  },
+  { flush: "post" }
+);
+watch(
+  () => status.value.press,
+  (press, _, onCleanup) => {
+    if (!press) return;
+    inter = tableRef.value?.getMouseIntersect() || null;
+    const $shape = shape.value?.getNode();
+    if ($shape && inter && (~inter.colBorderNdx || ~inter.rawBorderNdx)) {
+      const pop = cursor.push(
+        getResizeCorsur(!~inter.rawBorderNdx, shape.value?.getNode().rotation())
+      );
+      const isActive = shapesStatus.actives.includes($shape);
+      if (isActive) {
+        shapesStatus.actives = shapesStatus.actives.filter((s) => s !== $shape);
+      }
+      onCleanup(() => {
+        inter = null;
+        pop();
+        // if (isActive) {
+        //   shapesStatus.actives = [...shapesStatus.actives, $shape];
+        // }
+      });
+    }
+  },
+  { flush: "post" }
+);
+
+const getColMinSize = (col: TableCollData) => {
+  const minw = (col.padding || 0) * 2 + (col.fontSize || 12) + 4;
+  const minh = (col.padding || 0) * 2 + (col.fontSize || 12) + 4;
+  return { w: minw, h: minh };
+};
+
+const getOperType = useGetTransformerOperType();
+const matToData = (data: TableData, mat: Transform, initData?: TableData) => {
+  if (!initData) {
+    initData = copy(data);
+  }
+  const dec = mat.decompose();
+  if (!inter || (!~inter.colBorderNdx && !~inter.rawBorderNdx)) {
+    const oldData = copy(data);
+    data.height = dec.scaleY * initData.height;
+    data.width = dec.scaleX * initData.width;
+
+    let w = 0;
+    let h = 0;
+    data.content.forEach((raw, rndx) => {
+      raw.forEach((col, cndx) => {
+        const initCol = initData.content[rndx][cndx];
+        const minSize = getColMinSize(initCol);
+        col.width = Math.max(minSize.w, data.width * (initCol.width / initData.width));
+        col.height = Math.max(
+          minSize.h,
+          data.height * (initCol.height / initData.height)
+        );
+        if (rndx === 0) {
+          w += col.width;
+        }
+        if (cndx === 0) {
+          h += col.height;
+        }
+      });
+    });
+    const eqW = numEq(w, data.width);
+    const eqH = numEq(h, data.height);
+
+    if (!eqW || !eqH) {
+      const type = getOperType();
+      if (type) {
+        Object.assign(data, oldData);
+      } else {
+        data.width = w;
+        data.height = h;
+        const initDec = new Transform(initData.mat).decompose();
+        data.mat = new Transform()
+          .translate(eqW ? dec.x : initDec.x, eqH ? dec.y : initDec.y)
+          .rotate(MathUtils.degToRad(dec.rotation)).m;
+      }
+    } else {
+      data.mat = new Transform()
+        .translate(dec.x, dec.y)
+        .rotate(MathUtils.degToRad(dec.rotation)).m;
+    }
+    return data;
+  }
+  const initDec = new Transform(initData.mat).decompose();
+  const move = new Transform().rotate(MathUtils.degToRad(-dec.rotation)).point({
+    x: dec.x - initDec.x,
+    y: dec.y - initDec.y,
+  });
+  if (~inter.rawBorderNdx) {
+    const ndxRaw = inter.rawBorderNdx - 1;
+    const ndx = ndxRaw === -1 ? 0 : ndxRaw;
+    let offset = ndxRaw === -1 ? -move.y : move.y;
+    const minSize = getColMinSize(data.content[ndx][0]);
+    const h = Math.max(minSize.h, initData.content[ndx][0].height + offset);
+    offset = h - initData.content[ndx][0].height;
+
+    data.content[ndx].forEach(
+      (col, colNdx) => (col.height = initData.content[ndx][colNdx].height + offset)
+    );
+    data.height = initData.height + offset;
+
+    if (ndxRaw === -1) {
+      const translate = new Transform()
+        .rotate(MathUtils.degToRad(dec.rotation))
+        .point({ x: 0, y: -offset });
+      data.mat = new Transform()
+        .translate(translate.x, translate.y)
+        .multiply(new Transform(initData.mat)).m;
+    }
+  } else {
+    const ndxRaw = inter.colBorderNdx - 1;
+    const ndx = ndxRaw === -1 ? 0 : ndxRaw;
+    let offset = ndxRaw === -1 ? -move.x : move.x;
+    const minSize = getColMinSize(data.content[0][ndx]);
+    const w = Math.max(minSize.w, initData.content[0][ndx].width + offset);
+    offset = w - initData.content[0][ndx].width;
+    data.content.forEach((row, rowNdx) => {
+      row[ndx].width = initData.content[rowNdx][ndx].width + offset;
+    });
+    data.width = initData.width + offset;
+    if (ndxRaw === -1) {
+      const translate = new Transform()
+        .rotate(MathUtils.degToRad(dec.rotation))
+        .point({ x: -offset, y: 0 });
+      data.mat = new Transform()
+        .translate(translate.x, translate.y)
+        .multiply(new Transform(initData.mat)).m;
+    }
+  }
+  return data;
+};
+
+let repShape: Rect | null;
+const sync = (data: TableData) => {
+  if (repShape) {
+    repShape.width(data.width);
+    repShape.height(data.height);
+    const tf = new Transform(data.mat);
+    setShapeTransform(repShape, tf);
+    initData = copy(data);
+  }
+};
+
+let initData: TableData;
+const { shape, tData, data, operateMenus, describes } = useComponentStatus<
+  Group,
+  TableData
+>({
+  emit,
+  props,
+  getMouseStyle,
+  defaultStyle,
+  alignment: (data, mat) => {
+    const tf = shape.value!.getNode().getTransform();
+    mat.multiply(tf);
+    matToData(data, mat);
+    sync(data);
+  },
+  transformType: "custom",
+  customTransform(callback, shape, data) {
+    useCustomTransformer(shape, data, {
+      openSnap: true,
+      transformerConfig: { flipEnabled: false },
+      getRepShape() {
+        repShape = new Rect();
+        sync(data.value);
+        return {
+          shape: repShape as any,
+        };
+      },
+      beforeHandler(data, mat) {
+        return matToData(copy(data), mat, initData);
+      },
+      handler(data, mat) {
+        matToData(data, mat, initData);
+        // sync(data);
+      },
+      callback(data) {
+        callback();
+        sync(data);
+      },
+    });
+  },
+  copyHandler(mat, data) {
+    const tf = shape.value!.getNode().getTransform();
+    mat.multiply(tf);
+    return matToData({ ...data }, mat);
+  },
+  propertys: [
+    "fill",
+    "stroke",
+    "fontColor",
+    "strokeWidth",
+    "fontSize",
+    // "ref",
+    "opacity",
+    //  "zIndex"
+    "align",
+    "fontStyle",
+  ],
+});
+
+watchEffect((onCleanup) => {
+  shape.value = tableRef.value?.shape;
+  onCleanup(() => (shape.value = undefined));
+});
+
+watch(
+  () => data.value.fontSize,
+  () => {
+    data.value.content.forEach((raw) => {
+      raw.forEach((col) => {
+        col.fontSize = data.value.fontSize;
+      });
+    });
+    const $shape = shape.value!.getNode();
+    data.value = matToData(data.value, $shape.getTransform());
+
+    sync(data.value);
+    $shape.fire("bound-change");
+  },
+  { flush: "sync" }
+);
+
+watchEffect(
+  () => {
+    data.value.content.forEach((raw) => {
+      raw.forEach((col) => {
+        col.fontColor = data.value.fontColor;
+        col.fontStyle = data.value.fontStyle;
+        col.align = data.value.align;
+      });
+    });
+  },
+  { flush: "sync" }
+);
+
+let addMenu: any;
+const menuShowHandler = () => {
+  const config = tableRef.value!.getMouseIntersect();
+  addMenu = [
+    {
+      icon: Plus,
+      label: "插入行",
+      handler: () => {
+        const tempRaw = data.value.content[config.rawNdx];
+        data.value.content.splice(
+          config.rawNdx,
+          0,
+          tempRaw.map((item) => ({ ...item, content: "" }))
+        );
+        data.value.height += tempRaw[0].height;
+        sync(data.value);
+        emit("updateShape", { ...data.value });
+      },
+    },
+    {
+      icon: Minus,
+      label: "删除行",
+      handler: () => {
+        const tempRaw = data.value.content[config.rawNdx];
+        data.value.content.splice(config.rawNdx, 1);
+        data.value.height -= tempRaw[0].height;
+        if (data.value.content.length === 0) {
+          emit("delShape");
+        } else {
+          sync(data.value);
+          emit("updateShape", data.value);
+        }
+      },
+    },
+    {
+      icon: Plus,
+      label: "插入列",
+      handler: () => {
+        const tempCol = data.value.content[0][config.colNdx];
+        for (let i = 0; i < data.value.content.length; i++) {
+          const raw = data.value.content[i];
+          raw.splice(config.colNdx, 0, { ...tempCol, content: "" });
+        }
+        data.value.width += tempCol.width;
+        sync(data.value);
+        emit("updateShape", data.value);
+      },
+    },
+    {
+      icon: Minus,
+      label: "删除列",
+      handler: () => {
+        const tempCol = data.value.content[0][config.colNdx];
+        for (let i = 0; i < data.value.content.length; i++) {
+          const raw = data.value.content[i];
+          raw.splice(config.colNdx, 1);
+        }
+        data.value.width -= tempCol.width;
+        if (data.value.content[0].length === 0) {
+          emit("delShape");
+        } else {
+          sync(data.value);
+          emit("updateShape", data.value);
+        }
+      },
+    },
+  ];
+  operateMenus.unshift(...addMenu);
+};
+const menuHideHandler = () => {
+  for (let i = 0; i < addMenu.length; i++) {
+    const ndx = operateMenus.indexOf(addMenu[i]);
+    if (ndx !== -1) {
+      operateMenus.splice(ndx, 1);
+    }
+  }
+  addMenu = [];
+};
+
+const showText = ref(false);
+const showInputHandler = () => {
+  showText.value = true;
+  const ndx = shapesStatus.actives.indexOf(shape.value!.getNode());
+  if (~ndx) {
+    shapesStatus.actives = shapesStatus.actives.filter(
+      (v) => v !== shape.value!.getNode()
+    );
+  }
+};
+const quitInputHandler = () => {
+  showText.value = false;
+};
+const submitInputHandler = (val: string, rawNdx: number, colNdx: number) => {
+  quitInputHandler();
+  data.value.content[rawNdx][colNdx].content = val;
+  emit("updateShape", data.value);
+};
+</script>

+ 181 - 0
src/core/components/table/temp-table.vue

@@ -0,0 +1,181 @@
+<template>
+  <v-group :config="matConfig" ref="shape">
+    <v-rect
+      name="repShape"
+      :config="{
+        x: -rang,
+        y: -rang,
+        width: data.width + 2 * rang,
+        height: data.height + 2 * rang,
+      }"
+    >
+    </v-rect>
+    <v-line
+      ref="line"
+      :config="{
+        points: getBorderPoints(),
+        ...data,
+        closed: true,
+        zIndex: undefined,
+      }"
+    />
+    <template v-for="(raw, rawNdx) in data.content">
+      <template v-for="(col, colNdx) in raw">
+        <v-text
+          :ref="(r: any) => setText(rawNdx, colNdx, r)"
+          :config="{
+            ...defaultCollData,
+            ...col,
+            ...getBound(rawNdx, colNdx),
+            text: col.content,
+            fill: col.fontColor,
+          }"
+        />
+      </template>
+    </template>
+  </v-group>
+</template>
+
+<script lang="ts" setup>
+import { defaultStyle, TableData, defaultCollData } 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 { useStage } from "@/core/hook/use-global-vars.ts";
+import { Line } from "konva/lib/shapes/Line";
+import { useViewerInvertTransform, useViewerTransform } from "@/core/hook/use-viewer.ts";
+import { lineLen, Pos } from "@/utils/math.ts";
+import { Text } from "konva/lib/shapes/Text";
+
+const props = defineProps<{ data: TableData; addMode?: boolean }>();
+const data = computed(() => ({ ...defaultStyle, ...props.data }));
+const texts = ref<DC<Text>[][]>([]);
+
+const shape = ref<DC<Group>>();
+const line = ref<DC<Line>>();
+
+const setText = (rawNdx: number, colNdx: number, text: DC<Text>) => {
+  let raw = texts.value[rawNdx];
+  if (!raw) {
+    raw = texts.value[rawNdx] = [];
+  }
+  raw[colNdx] = text;
+};
+const getBound = (rawNdx: number, colNdx: number) => {
+  let x = 0,
+    y = 0;
+  for (let i = 0; i < rawNdx; i++) {
+    y += data.value.content[i][0].height;
+  }
+  for (let i = 0; i < colNdx; i++) {
+    x += data.value.content[rawNdx][i].width;
+  }
+  const { width, height } = data.value.content[rawNdx][colNdx];
+  return { x, y, width, height };
+};
+
+const getBorderPoints = () => {
+  const points: number[] = [];
+  const raws = data.value.content;
+  let ry = 0;
+  let rx = 0;
+  for (let i = 0; i < raws.length; i++) {
+    const cols = raws[i];
+    for (let j = 0; j < cols.length; j++) {
+      const { x, y, width, height } = getBound(i, j);
+      points.push(x, y, x + width, y, x + width, y + height, x, y + height, x, y);
+      ry = y + height;
+      rx = x;
+    }
+    points.push(rx, ry);
+  }
+  points.push(0, ry);
+  return points;
+};
+
+const matConfig = computed(() => {
+  const mat = new Transform(data.value.mat);
+  return mat.decompose();
+});
+
+let rang = 5;
+const stage = useStage();
+const viewInverTransform = useViewerInvertTransform();
+const viewTransform = useViewerTransform();
+const getMouseIntersect = (pos?: Pos) => {
+  const $line = line.value?.getNode();
+  if (!pos) {
+    const $stage = stage.value?.getStage();
+    pos = $stage?.getPointerPosition() as any;
+  }
+  if (!pos || !$line) return null;
+  if (!$line.intersects(pos)) return null;
+
+  pos = viewInverTransform.value.point(pos);
+  pos = new Transform(data.value.mat).invert().point(pos);
+
+  const swPixel = lineLen(
+    viewTransform.value.point({ x: data.value.strokeWidth, y: 0 }),
+    viewTransform.value.point({ x: 0, y: 0 })
+  );
+  const check = Math.max(rang, swPixel);
+  let rawBorderNdx = -1;
+  let colBorderNdx = -1;
+  let rawNdx = -1;
+  let colNdx = -1;
+
+  for (let i = 0; i < data.value.content.length; i++) {
+    const rb = getBound(i, 0);
+    rb.x += matConfig.value.x;
+    rb.x += matConfig.value.y;
+    const td = pos.y - rb.y;
+    const bd = pos.y - rb.y - rb.height;
+
+    if (td < -check || bd > check) {
+      continue;
+    }
+    if (Math.abs(td) < check) {
+      rawBorderNdx = i;
+    }
+    if (Math.abs(bd) < check) {
+      rawBorderNdx = i + 1;
+    }
+    rawNdx = i;
+
+    const raw = data.value.content[i];
+    let j = 0;
+    for (; j < raw.length; j++) {
+      const cb = getBound(i, j);
+      const ld = pos.x - cb.x;
+      const rd = pos.x - cb.x - cb.width;
+
+      if (ld < -check || rd > check) {
+        continue;
+      }
+      if (Math.abs(ld) < check) {
+        colBorderNdx = j;
+      }
+      if (Math.abs(rd) < check) {
+        colBorderNdx = j + 1;
+      }
+      colNdx = j;
+      break;
+    }
+    if (j !== raw.length) {
+      break;
+    }
+  }
+  return { rawBorderNdx, colBorderNdx, rawNdx, colNdx };
+};
+
+defineExpose({
+  get shape() {
+    return shape.value;
+  },
+  get texts() {
+    return texts.value;
+  },
+  getMouseIntersect,
+});
+</script>

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

@@ -4,6 +4,7 @@
   <v-text
     :config="{
       ...data,
+      wrap: 'char',
       ...matConfig,
       text: data.content,
       zIndex: undefined,

+ 20 - 21
src/core/components/text/text.vue

@@ -1,11 +1,12 @@
 <template>
-  <TextDom v-if="editText && $shape" :shape="$shape" @submit="quitHandler" />
-  <TempText
-    :data="tData"
-    :ref="(v: any) => shape = v?.shape"
-    :id="data.id"
-    @dblclick="dbclickHandler"
+  <TextDom
+    v-if="$shape"
+    :shape="$shape"
+    @submit="submitHandler"
+    @show="showHandler"
+    @hide="quitHandler"
   />
+  <TempText :data="tData" :ref="(v: any) => shape = v?.shape" :id="data.id" />
   <PropertyUpdate
     v-if="!editText"
     :describes="describes"
@@ -20,20 +21,16 @@
 import { TextData, getMouseStyle, defaultStyle } from "./index.ts";
 import { PropertyUpdate, Operate } from "../../propertys";
 import TempText from "./temp-text.vue";
-import TextDom from "./text-dom.vue";
+import TextDom from "../share/text-area.vue";
 import { useComponentStatus } from "@/core/hook/use-component.ts";
-import {
-  cloneRepShape,
-  useCustomTransformer,
-  useTransformer,
-} from "@/core/hook/use-transformer.ts";
+import { cloneRepShape, useCustomTransformer } from "@/core/hook/use-transformer.ts";
 import { Transform } from "konva/lib/Util";
 import { Text } from "konva/lib/shapes/Text";
 import { computed, ref, watch } from "vue";
 import { setShapeTransform } from "@/utils/shape.ts";
 import { zeroEq } from "@/utils/math.ts";
 import { MathUtils } from "three";
-import { KonvaEventObject } from "konva/lib/Node";
+import { useMouseShapesStatus } from "@/core/hook/use-mouse-status.ts";
 
 const props = defineProps<{ data: TextData }>();
 const emit = defineEmits<{
@@ -146,20 +143,22 @@ watch(
   }
 );
 
-const transformer = useTransformer();
+const shapesStatus = useMouseShapesStatus();
 const $shape = computed(() => shape.value?.getNode());
 const editText = ref(false);
-const dbclickHandler = (ev: KonvaEventObject<MouseEvent>) => {
+const showHandler = () => {
   editText.value = true;
-  $shape.value && $shape.value.hide();
-  transformer.hide();
+  const ndx = shapesStatus.actives.indexOf($shape.value!);
+  if (~ndx) {
+    shapesStatus.actives = shapesStatus.actives.filter((v) => v !== $shape.value);
+  }
 };
-const quitHandler = (val: string) => {
-  editText.value = false;
-  transformer.show();
-  $shape.value && $shape.value.show();
+const submitHandler = (val: string) => {
   if (val !== data.value.content) {
     data.value.content = val;
   }
 };
+const quitHandler = () => {
+  editText.value = false;
+};
 </script>

+ 16 - 0
src/core/helper/debugger.vue

@@ -0,0 +1,16 @@
+<template>
+  <v-circle v-for="p in points" :config="{ ...p, radius: 8, fill: 'red' }" />
+</template>
+
+<script lang="ts" setup>
+import { computed } from "vue";
+import { useTestPoints } from "../hook/use-debugger";
+import { useViewerTransform } from "../hook/use-viewer";
+
+const viewerTransform = useViewerTransform();
+
+const testPoints = useTestPoints();
+const points = computed(() =>
+  testPoints.value.map((p) => viewerTransform.value.point(p))
+);
+</script>

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

@@ -9,10 +9,15 @@
     v-for="axisConfig in axisConfigs"
     :config="{ x: axisConfig.x, y: axisConfig.y, radius: 3, fill: '#000' }"
   />
+  <v-circle
+    v-if="debug"
+    v-for="p in selfPoints"
+    :config="{ ...p, radius: 8, fill: 'red' }"
+  />
 </template>
 
 <script lang="ts" setup>
-import { computed, ComputedRef, ref } from "vue";
+import { computed, ComputedRef, ref, watchEffect } from "vue";
 import { SnapInfo, useGlobalSnapInfos, useSnapResultInfo } from "../hook/use-snap";
 import { useViewerTransform } from "../hook/use-viewer";
 import { RectConfig } from "konva/lib/shapes/Rect";
@@ -43,8 +48,12 @@ const info = useSnapResultInfo();
 const minOffset = 10;
 const minAngle = MathUtils.degToRad(5);
 let snapInfos: ComputedRef<SnapInfo[]>;
+let selfPoints: ComputedRef<Pos[]>;
 if (debug) {
   snapInfos = useGlobalSnapInfos();
+  selfPoints = computed(() =>
+    info.selfSnaps.map((p) => viewerTransform.value.point(p.point))
+  );
 }
 
 const deduplication = (items: Axis[]) => {

+ 1 - 8
src/core/hook/use-alignment.ts

@@ -118,14 +118,7 @@ export const useJoinShapes = (
     const $stage = stage.value!.getNode();
     const dom = $stage.container();
     const points = drawLines(ref<(Pos | null)[]>(attribs.map(() => null)));
-
-    cursor.push("crosshair");
-    mode.push(Mode.readonly)
-    
-    const cleanup = () => {
-      cursor.pop()
-      mode.pop()
-    }
+    const cleanup = mergeFuns(cursor.push("crosshair"), mode.push(Mode.readonly)) 
 
     for (let i = 0; i < attribs.length; i++) {
       const attrib = attribs[i];

+ 34 - 25
src/core/hook/use-component.ts

@@ -8,7 +8,6 @@ import {
   onUnmounted,
   reactive,
   Ref,
-  ref,
   shallowReactive,
   shallowRef,
   watch,
@@ -34,14 +33,14 @@ import {
   Unlock,
   Upload,
 } from "@element-plus/icons-vue";
-import { asyncTimeout, mergeFuns, onlyId } from "@/utils/shared";
+import { asyncTimeout, copy, mergeFuns, onlyId } from "@/utils/shared";
 import { Shape } from "konva/lib/Shape";
 import { Transform } from "konva/lib/Util";
 import { mergeDescribes, PropertyKeys } from "../propertys";
 import { useStore } from "../store";
 import { globalWatch, useStage } from "./use-global-vars";
 import { useAlignmentShape } from "./use-alignment";
-import { useViewer, useViewerTransform } from "./use-viewer";
+import { useViewerTransform } from "./use-viewer";
 import { usePause } from "./use-pause";
 
 type Emit<T> = EmitFn<{
@@ -119,7 +118,10 @@ export const useComponentMenus = <T extends DrawItem>(
       async handler() {
         const mat = await alignmentShape();
         alignment(data.value, mat);
-        emit("updateShape", { ...data.value });
+        emit("updateShape", copy({ ...data.value }));
+        // let ndata = { ...data.value, id: onlyId() }
+        // emit('addShape', ndata)
+        // emit('delShape')
       },
       icon: Location,
     });
@@ -138,14 +140,15 @@ export const useComponentMenus = <T extends DrawItem>(
           transform,
           JSON.parse(JSON.stringify(data.value)) as T
         );
+        copyData.id = onlyId();
         emit("addShape", copyData);
         await asyncTimeout(100);
         const $stage = stage.value?.getNode();
         if (!$stage) return;
         const $shape = $stage.findOne<Shape>(`#${copyData.id}`);
+        console.log($shape?.id(), copyData.id, data.value.id);
         if ($shape) {
-          status.actives.length = 0;
-          status.actives.push($shape);
+          status.actives = [$shape];
         }
       },
     });
@@ -311,23 +314,25 @@ export const useComponentsAttach = <T>(
 
 export const useOnComponentBoundChange = () => {
   const getComponentData = useGetComponentData();
-  const transform = useViewerTransform()
+  const transform = useViewerTransform();
   const quitHooks = [] as Array<() => void>;
   const destory = () => mergeFuns(quitHooks)();
 
   const on = <T extends EntityShape>(
     shape: Ref<T | undefined> | T | undefined,
-    callback: (shape: T, type: 'transform' | 'data') => void
+    callback: (shape: T, type: "transform" | "data") => void
   ) => {
     const $shape = computed(() => (shape = isRef(shape) ? shape.value : shape));
     let repShape: T | undefined;
     const item = getComponentData($shape);
-    const update = (type?: 'transform' | 'data') => {
-      $shape.value && !api.isPause && callback(repShape || $shape.value, type || 'data');
+    const update = (type?: "transform" | "data") => {
+      $shape.value &&
+        !api.isPause &&
+        callback(repShape || $shape.value, type || "data");
     };
-    const sync = () => update()
+    const sync = () => update();
     const shapeListener = (shape: T) => {
-      repShape = shape.repShape as T || shape;
+      repShape = (shape.repShape as T) || shape;
       repShape.on("transform", sync);
       shape.on("bound-change", sync);
       return () => {
@@ -337,23 +342,27 @@ export const useOnComponentBoundChange = () => {
     };
 
     const onDestroy = mergeFuns([
-      watch(item, () => nextTick(() => update('data')), { deep: true }),
-      watch(transform, () => update('transform')),
-      watch($shape, (shape, _, onCleanup) => {
-        if (!shape) return;
-        onCleanup(shapeListener(shape));
-      }, {immediate: true}),
-    ])
+      watch(item, () => nextTick(() => update("data")), { deep: true }),
+      watch(transform, () => update("transform")),
+      watch(
+        $shape,
+        (shape, _, onCleanup) => {
+          if (!shape) return;
+          onCleanup(shapeListener(shape));
+        },
+        { immediate: true }
+      ),
+    ]);
     quitHooks.push(onDestroy);
 
     return () => {
-      const ndx = quitHooks.indexOf(onDestroy) 
-      ~ndx && quitHooks.splice(ndx, 1)
-      onDestroy
-    }
+      const ndx = quitHooks.indexOf(onDestroy);
+      ~ndx && quitHooks.splice(ndx, 1);
+      onDestroy;
+    };
   };
 
   const api = usePause({ destory, on });
-  onUnmounted(destory)
-  return api
+  onUnmounted(destory);
+  return api;
 };

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

@@ -0,0 +1,7 @@
+import { ref } from "vue";
+import { installGlobalVar } from "./use-global-vars";
+import { Pos } from "@/utils/math";
+
+export const useTestPoints = installGlobalVar(() => {
+  return ref<Pos[]>([])
+})

+ 13 - 13
src/core/hook/use-draw.ts

@@ -127,6 +127,7 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
     },
     quitDrawShape: () => {
       leave();
+      console.error('quit')
       interactiveProps.value = void 0;
     },
   };
@@ -262,7 +263,7 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
         cur,
         action: MessageAction.add,
       } as any,
-      ia.preset
+      ia.preset as any
     );
     if (!item) return;
     item = reactive(item);
@@ -318,33 +319,31 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
 // 拖拽面积确定组件
 export const useInteractiveDrawAreas = <T extends ShapeType>(type: T) => {
   const cursor = useCursor();
-  let isEnter = false;
+  let cursorPop: () => void
   return useInteractiveDrawTemp({
     type,
     useIA: useInteractiveAreas,
     refSelf: type === "arrow",
     enter() {
-      isEnter = true;
-      cursor.push("crosshair");
+      cursorPop = cursor.push("crosshair");
     },
     quit() {
-      isEnter && cursor.pop();
+      cursorPop && cursorPop();
     },
   });
 };
 
 export const useInteractiveDrawDots = <T extends ShapeType>(type: T) => {
   const cursor = useCursor();
-  let isEnter = false;
+  let cursorPop: () => void
   return useInteractiveDrawTemp({
     type,
     useIA: useInteractiveDots,
     enter() {
-      isEnter = true;
-      cursor.push(penA);
+      cursorPop = cursor.push(penA);
     },
     quit() {
-      isEnter && cursor.pop();
+      cursorPop && cursorPop();
     },
     getSnapGeo(item) {
       return components[type].getSnapPoints(item as any);
@@ -428,14 +427,15 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
   // 可能历史空间会撤销 重做更改到正在绘制的组件
   const currentCursor = ref(penA);
   const cursor = useCursor();
+  let cursorPop: ReturnType<typeof cursor.push> | null = null; 
   let stopWatch: (() => void) | null = null;
   const ia = useInteractiveDots({
     shapeType: type,
     isRuning,
     enter() {
-      cursor.push(currentCursor.value);
+      cursorPop = cursor.push(currentCursor.value);
       watch(currentCursor, () => {
-        cursor.value = currentCursor.value;
+        cursorPop?.set(currentCursor.value)
       });
     },
     quit: () => {
@@ -443,7 +443,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
       processorIds.length = 0;
       quitDrawShape();
       beforeHandler.clear();
-      cursor.pop();
+      cursorPop && cursorPop();
       stopWatch && stopWatch();
     },
     beforeHandler: (p) => {
@@ -527,7 +527,7 @@ export const useInteractiveDrawPen = <T extends ShapeType>(type: T) => {
     }
     let item: any = items.length === 0 ? null : items[0];
     if (!item) {
-      item = obj.interactiveToData(setMessage(dot), ia.preset);
+      item = obj.interactiveToData(setMessage(dot), ia.preset as any);
       if (!item) return;
       items[0] = item = reactive(item);
     }

+ 65 - 8
src/core/hook/use-event.ts

@@ -1,3 +1,4 @@
+import { string } from "three/webgpu";
 import { listener } from "../../utils/event.ts";
 import { installGlobalVar, useStage } from "./use-global-vars.ts";
 import { nextTick, ref, watchEffect } from "vue";
@@ -32,18 +33,18 @@ export const useGlobalResize = installGlobalVar(() => {
   const stage = useStage();
   const size = ref<{ width: number; height: number }>();
   const setSize = () => {
-    const container = stage.value?.getStage().container()
+    const container = stage.value?.getStage().container();
     if (container) {
-      container.style.setProperty('display', 'none')
+      container.style.setProperty("display", "none");
     }
 
     const dom = stage.value!.getNode().container().parentElement!;
     size.value = {
-			width: dom.offsetWidth,
-			height: dom.offsetHeight
-		}
+      width: dom.offsetWidth,
+      height: dom.offsetHeight,
+    };
     if (container) {
-      container.style.removeProperty('display')
+      container.style.removeProperty("display");
     }
   };
   const stopWatch = watchEffect(() => {
@@ -57,7 +58,63 @@ export const useGlobalResize = installGlobalVar(() => {
   return {
     updateSize: setSize,
     size,
-  }
-}, Symbol('resize'))
+  };
+}, Symbol("resize"));
 
 export const useResize = () => useGlobalResize().size;
+
+export const usePreemptiveListener = installGlobalVar(() => {
+  const cbs: Record<string, ((ev: Event) => void)[]> = {};
+  const eventDestorys: Record<string, () => void> = {};
+  const stage = useStage();
+
+  const add = (eventName: string, cb: (ev: Event) => void) => {
+    if (!cbs[eventName]) {
+      cbs[eventName] = [];
+
+      const $stage = stage.value!.getStage();
+      const dom = $stage.container() as any;
+      eventDestorys[eventName] = listener(dom, eventName as any, (ev: any) => {
+        console.log(cbs[eventName], cbs[eventName][0])
+        cbs[eventName][0].call(this, ev);
+      });
+    }
+    cbs[eventName].push(cb);
+  };
+
+  const remove = (eventName: string, cb?: (ev: Event) => void) => {
+    if (!cbs[eventName]) return;
+    if (cb) {
+      const index = cbs[eventName].indexOf(cb);
+      if (index !== -1) {
+        cbs[eventName].splice(index, 1);
+      }
+      if (cbs[eventName].length === 0) {
+        delete cbs[eventName];
+      }
+    } else {
+      delete cbs[eventName];
+    }
+
+    if (!cbs[eventName]) {
+      eventDestorys[eventName]();
+    }
+  };
+
+  const on = <T extends keyof HTMLElementEventMap>(
+    eventName: T,
+    callback: (this: HTMLDivElement, ev: HTMLElementEventMap[T]) => any
+  ) => {
+    const stopWatch = watchEffect((onCleanup) => {
+      if (stage.value) {
+        add(eventName, callback as any);
+        onCleanup(() => {
+          remove(eventName, callback as any);
+        });
+      }
+    });
+    return stopWatch
+  };
+
+  return on;
+}, Symbol("preemptiveListener"));

+ 46 - 40
src/core/hook/use-expose.ts

@@ -26,28 +26,28 @@ import { useResourceHandler } from "./use-fetch.ts";
 export const useAutoService = () => {
   // 自动粘贴服务
   const paste = usePaste();
-	const drawAPI = useInteractiveDrawShapeAPI()
-	const resourceHandler = useResourceHandler()
+  const drawAPI = useInteractiveDrawShapeAPI();
+  const resourceHandler = useResourceHandler();
   paste.push({
     ["text/plain"]: {
       async handler(pos, val) {
-				if (isSvgString(val)) {
-					const url = await resourceHandler(val, 'svg')
-					drawAPI.addShape('icon', { url, stroke: themeColor }, pos, true)
-				} else {
-					drawAPI.addShape('text', { content: val }, pos, true)
-				}
+        if (isSvgString(val)) {
+          const url = await resourceHandler(val, "svg");
+          drawAPI.addShape("icon", { url, stroke: themeColor }, pos, true);
+        } else {
+          drawAPI.addShape("text", { content: val }, pos, true);
+        }
       },
       type: "string",
     },
     ["image"]: {
       async handler(pos, val, type) {
-				const url = await resourceHandler(val, type)
-				if (type.includes('svg')) {
-					drawAPI.addShape('icon', { url, stroke: themeColor }, pos, true)
-				} else {
-					drawAPI.addShape('image', { url }, pos, true)
-				}
+        const url = await resourceHandler(val, type);
+        if (type.includes("svg")) {
+          drawAPI.addShape("icon", { url, stroke: themeColor }, pos, true);
+        } else {
+          drawAPI.addShape("image", { url }, pos, true);
+        }
       },
       type: "file",
     },
@@ -70,8 +70,9 @@ export const useAutoService = () => {
   const downKeys = useDownKeys();
   const mode = useMode();
   const cursor = useCursor();
+  const { set: setCursor } = cursor.push('initial')
 
-  watchEffect((onCleanup) => {
+  watchEffect(() => {
     let style: string | null = null;
     if (downKeys.has(" ")) {
       style = "pointer";
@@ -79,50 +80,55 @@ export const useAutoService = () => {
       style = "move";
     } else if (status.hovers.length) {
       style = "pointer";
+    } else {
+      style = "initial"
     }
-
-    if (style) {
-      cursor.push(style);
-      onCleanup(() => cursor.pop());
-    }
+    setCursor(style)
   });
 
   // 自动保存历史及恢复服务
   const history = useHistory();
   const instanceProps = useInstanceProps();
   const init = (id: any) => {
-		const quitHooks: (() => void)[] = []
+    const quitHooks: (() => void)[] = [];
     if (!id) return quitHooks;
 
-		const unloadHandler = () => {
+    const unloadHandler = () => {
       if (history.hasRedo.value || history.hasUndo.value) {
         history.saveLocal();
       }
-    }
+    };
     history.setLocalId(id);
     window.addEventListener("beforeunload", unloadHandler);
-		quitHooks.push(() => window.removeEventListener('beforeunload', unloadHandler))
+    quitHooks.push(() =>
+      window.removeEventListener("beforeunload", unloadHandler)
+    );
 
     if (!history.hasLocal()) return quitHooks;
 
-		let isOpen = true
-		ElMessageBox.confirm("检测到有历史数据,是否要恢复?", {
-			type: "info",
-			confirmButtonText: "恢复",
-			cancelButtonText: "取消",
-		}).then(() => {
-      history.loadLocalStorage();
-		}).catch(() => {
-      history.clearLocal();
-		}).finally (() => {
-			isOpen = false
-		})
-		quitHooks.push(() => isOpen && ElMessageBox.close())
-		return quitHooks;
+    if (!import.meta.env.DEV) {
+      let isOpen = true;
+      ElMessageBox.confirm("检测到有历史数据,是否要恢复?", {
+        type: "info",
+        confirmButtonText: "恢复",
+        cancelButtonText: "取消",
+      })
+        .then(() => {
+          history.loadLocalStorage();
+        })
+        .catch(() => {
+          history.clearLocal();
+        })
+        .finally(() => {
+          isOpen = false;
+        });
+      quitHooks.push(() => isOpen && ElMessageBox.close());
+    }
+    return quitHooks;
   };
   watchEffect((onCleanup) => {
-		onCleanup(mergeFuns(init(instanceProps.get().id)))
-	});
+    onCleanup(mergeFuns(init(instanceProps.get().id)));
+  });
 };
 
 export type DrawExpose = ReturnType<typeof useExpose>;

+ 48 - 15
src/core/hook/use-global-vars.ts

@@ -11,7 +11,9 @@ import {
   shallowRef,
   watch,
   WatchCallback,
+  WatchEffect,
   watchEffect,
+  WatchEffectOptions,
   WatchOptions,
   WatchSource,
 } from "vue";
@@ -19,7 +21,7 @@ import { Mode } from "../../constant/mode.ts";
 import { Layer } from "konva/lib/Layer";
 import { Pos } from "@/utils/math.ts";
 import { listener } from "@/utils/event.ts";
-import { mergeFuns } from "@/utils/shared.ts";
+import { mergeFuns, onlyId } from "@/utils/shared.ts";
 import { DrawData } from "../components/index.ts";
 
 export const installGlobalVar = <T>(
@@ -83,16 +85,31 @@ export const useInstanceProps = installGlobalVar(() => {
 }, Symbol("instanceId"));
 
 export const stackVar = <T>(init?: T) => {
-  const stack = reactive([init]) as T[];
+  const factory = (init: T) => ({ var: init, id: onlyId() });
+  const stack = reactive([]) as { var: T; id: string }[];
+  if (init) {
+    stack.push(factory(init));
+  }
   const result = {
     get value() {
-      return stack[stack.length - 1];
+      return stack[stack.length - 1]?.var;
     },
     set value(val) {
-      stack[stack.length - 1] = val;
+      stack[stack.length - 1].var = val;
     },
     push(data: T) {
-      stack.push(data);
+      stack.push(factory(data));
+      const item = stack[stack.length - 1]
+      const pop = (() => {
+        const ndx = stack.findIndex(({ id }) => id === item.id);
+        if (~ndx) {
+          stack.splice(ndx, 1);
+        }
+      }) as (() => void) & {set: (data: T) => void}
+      pop.set = (data) => {
+        item.var = data
+      }
+      return pop;
     },
     pop() {
       if (stack.length - 1 > 0) {
@@ -125,6 +142,19 @@ export const globalWatch = <T>(
   };
 };
 
+export const globalWatchEffect = (
+  cb: WatchEffect,
+  options?: WatchEffectOptions
+): (() => void) => {
+  let stop: () => void;
+  nextTick(() => {
+    stop = watchEffect(cb as any, options);
+  });
+  return () => {
+    stop && stop();
+  };
+};
+
 export const useStage = installGlobalVar(
   () => shallowRef<DC<Stage> | undefined>(),
   Symbol("stage")
@@ -152,14 +182,14 @@ export const useMode = installGlobalVar(() => {
       modes.forEach((mode) => modeStack.value.delete(mode));
     },
   };
-  // if (import.meta.env.DEV) {
-  //   watchEffect(
-  //     () => {
-  //       console.error([...modeStack.value.values()].join(","));
-  //     },
-  //     { flush: "sync" }
-  //   );
-  // }
+  if (import.meta.env.DEV) {
+    watchEffect(
+      () => {
+        console.error([...modeStack.value.values()].join(","));
+      },
+      { flush: "sync" }
+    );
+  }
   return modeStack;
 }, Symbol("mode"));
 export const useCan = installGlobalVar(() => {
@@ -234,7 +264,7 @@ export const usePointerPos = installGlobalVar(() => {
     replayIng = false;
   };
 
-  watchEffect((onCleanup) => {
+  const stopWatch = globalWatchEffect((onCleanup) => {
     const $stage = stage.value?.getNode();
     if (!$stage) return;
 
@@ -254,7 +284,10 @@ export const usePointerPos = installGlobalVar(() => {
   });
 
   pos.replay = replay;
-  return pos;
+  return {
+    var: pos,
+    onDestroy: stopWatch,
+  };
 }, Symbol("pointerPos"));
 
 export const useDownKeys = installGlobalVar(() => {

+ 18 - 11
src/core/hook/use-mouse-status.ts

@@ -106,7 +106,6 @@ export const useMouseShapesStatus = installGlobalVar(() => {
   const shapeIsTransformerInner = useShapeIsTransformerInner();
 
   const init = (stage: Stage) => {
-    let downTime: number;
     let downTarget: EntityShape | null;
 
     const prevent = computed(() => keys.has(" "));
@@ -141,10 +140,10 @@ export const useMouseShapesStatus = installGlobalVar(() => {
       (_a, _b, onCleanup) => hoverChange(onCleanup)
     );
 
+    let downTime: number;
     stage.on("pointerdown.mouse-status", (ev) => {
+      downTime = Date.now()
       if (prevent.value) return;
-      console.log(ev.evt.button)
-      downTime = Date.now();
       const target = shapeTreeContain(listeners.value, ev.target);
       if (target && !press.value.includes(target)) {
         press.value.push(target);
@@ -152,17 +151,19 @@ export const useMouseShapesStatus = installGlobalVar(() => {
       downTarget = target;
     });
 
+    let upCount = 0
     return mergeFuns(
       stopHoverCheck,
       hoverDestory,
       listener(
         stage.container().parentElement as HTMLDivElement,
         "pointerup",
-        (ev) => {
+        async (ev) => {
           if (prevent.value) return;
           press.value = [];
-          if (Date.now() - downTime >= 300) return;
-          if (downTarget) {
+          if (Date.now() - downTime > 300) return;
+
+          if (downTarget && ev.button === 0) {
             const ndx = selects.value.indexOf(downTarget);
             if (~ndx) {
               selects.value.splice(ndx, 1);
@@ -206,11 +207,16 @@ export const useMouseShapesStatus = installGlobalVar(() => {
 
   const pauseShapes = ref(new Set<EntityShape>());
   const getShapes = (shapes: Ref<EntityShape[]>) =>
-    computed(() => {
-      return shapes.value
-        .filter((shape) => !pauseShapes.value.has(shape))
-        .map(toRaw);
-    });
+    computed({
+      get: () => {
+        return shapes.value
+          .filter((shape) => !pauseShapes.value.has(shape))
+          .map(toRaw);
+      },
+      set: (val: EntityShape[]) => {
+        shapes.value = val
+      }
+  });
   const status = reactive({
     hovers: getShapes(hovers),
     actives: getShapes(actives),
@@ -258,6 +264,7 @@ export const useMouseShapeStatus = (
 
         status.listeners.push(shape);
         onCleanup(() => {
+          console.log('取消监听', shape.id())
           for (const key in status) {
             const k = key as keyof typeof status;
             if (Array.isArray(status[k])) {

+ 26 - 13
src/core/hook/use-snap.ts

@@ -5,11 +5,12 @@ import {
   ShapeType,
   ComponentSnapInfo,
 } from "../components";
-import { computed, reactive, ref, toRaw, watch, watchEffect } from "vue";
+import { computed, reactive, Ref, ref, toRaw, watch, watchEffect } from "vue";
 import {
   createLine,
   eqNGDire,
   eqPoint,
+  isVertical,
   lineIntersection,
   lineLen,
   linePointLen,
@@ -32,7 +33,8 @@ import {
 import { Transform } from "konva/lib/Util";
 import { useCacheUnitTransform, useViewerInvertTransform } from "./use-viewer";
 import { MathUtils } from "three";
-import { arrayInsert, mergeFuns, rangMod } from "@/utils/shared";
+import { arrayInsert, copy, mergeFuns, rangMod } from "@/utils/shared";
+import { useTestPoints } from "./use-debugger";
 
 export type SnapInfo = ComponentSnapInfo & Pick<DrawItem, "id">;
 const useStoreSnapInfos = () => {
@@ -114,7 +116,10 @@ export const useGlobalSnapInfos = installGlobalVar(() => {
   const storeInfos = useStoreSnapInfos();
   const customInfos = useCustomSnapInfos();
   return {
-    var: computed(() => [...customInfos.infos.value, ...storeInfos.infos.value] as SnapInfo[]),
+    var: computed(
+      () =>
+        [...customInfos.infos.value, ...storeInfos.infos.value] as SnapInfo[]
+    ),
     onDestroy: storeInfos.cleanup,
   };
 }, Symbol("snapInfos"));
@@ -130,13 +135,16 @@ export const useSnapConfig = () => {
 
 export type SnapResultInfo = {
   attractSnaps: AttractSnapInfo[];
+  selfSnaps: ComponentSnapInfo[];
   clear: () => void;
 };
 export const useSnapResultInfo = installGlobalVar(() => {
   const snapInfo = reactive({
     attractSnaps: [],
+    selfSnaps: [],
     clear() {
       snapInfo.attractSnaps.length = 0;
+      snapInfo.selfSnaps.length = 0;
     },
   }) as SnapResultInfo;
   return snapInfo;
@@ -397,7 +405,8 @@ const scaleSnap = (
   selfInfos: ComponentSnapInfo[],
   filter: Omit<FilterAttrib, "type">,
   type: ScaleVectorType,
-  attitude: SelfAttitude
+  attitude: SelfAttitude,
+  testPoints: Ref<Pos[]>
 ) => {
   const { origin, operTarget } = attitude;
   const attractSnaps: AttractSnapInfo[] = [];
@@ -420,7 +429,13 @@ const scaleSnap = (
     if (exclude.has(nor)) return;
     if (
       eqNGDire(nor.refDirection, operVector) ||
-      eqNGDire(nor.refDirection, nor.current.linkDirections[nor.currentLinkNdx])
+      eqNGDire(
+        nor.refDirection,
+        nor.current.linkDirections[nor.currentLinkNdx]
+      ) ||
+      Math.abs(
+        vector(origin).sub(nor.current.point).normalize().dot(operVector)
+      ) < 0.01
     ) {
       exclude.add(nor);
       return;
@@ -447,13 +462,7 @@ const scaleSnap = (
       return;
     }
 
-    if (
-      Math.abs(lineLen(vector(join).sub(origin).normalize(), operVector)) > 0.3
-    ) {
-      exclude.add(nor);
-      return;
-    }
-
+    // testPoints.value = [origin, nor.ref.point];
     return join;
   };
 
@@ -508,6 +517,7 @@ export const useSnap = (
 ) => {
   const snapResultInfo = useSnapResultInfo();
   const snapConfig = useSnapConfig();
+  const testPoints = useTestPoints();
   const afterHandler = (result: ReturnType<typeof moveSnap>) => {
     if (result) {
       snapResultInfo.attractSnaps = result.useAttractSnaps;
@@ -520,6 +530,7 @@ export const useSnap = (
     const result = moveSnap(snapInfos.value, selfSnapInfos, {
       maxOffset: snapConfig.snapOffset,
     });
+    snapResultInfo.selfSnaps.push(...selfSnapInfos);
     return afterHandler(result);
   };
   const scale = (
@@ -531,8 +542,10 @@ export const useSnap = (
       selfSnapInfos,
       { maxOffset: snapConfig.snapOffset },
       attitude.type,
-      attitude
+      attitude,
+      testPoints
     );
+    snapResultInfo.selfSnaps = selfSnapInfos;
     return afterHandler(result);
   };
   return {

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

@@ -11,8 +11,12 @@ export const useGetPointerTextNdx = () => {
       return str.length;
     }
 
+    const toP = shape.getAbsoluteTransform().copy()
     const transform = shape.getAbsoluteTransform().copy().invert()
-    const textPos = {x: 0, y: 0}
+    const origin = toP.point({x: 0, y: 0})
+    const textPos = toP.point({x: shape.padding(), y: shape.padding()})
+    textPos.x -= origin.x
+    textPos.y -= origin.y
     const finalPos = transform.point({
       x: pointerPos.x - textPos.x,
       y: pointerPos.y - textPos.y,

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

@@ -289,7 +289,9 @@ export const useShapeTransformer = <T extends EntityShape>(
 
     rep.tempShape.on("transform.shapemer", updateTransform);
 
-    const boundHandler = () => rep.update && rep.update();
+    const boundHandler = () => {
+      rep.update && rep.update()
+    };
     $shape.on("bound-change", boundHandler);
 
     // 拖拽时要更新矩阵
@@ -299,6 +301,7 @@ export const useShapeTransformer = <T extends EntityShape>(
       (translate, oldTranslate) => {
         if (translate) {
           if (!oldTranslate) {
+            rep.init && rep.init();
             rep.update && rep.update();
           }
           const moveTf = new Transform().translate(translate.x, translate.y);
@@ -332,6 +335,7 @@ export const useShapeTransformer = <T extends EntityShape>(
         transformer.nodes([rep.tempShape]);
         transformer.queueShapes.value = [$shape];
         parent.add(transformer);
+        rep.init && rep.init()
 
         let isEnter = false;
         const downHandler = () => {
@@ -476,6 +480,7 @@ export const transformerRepShapeHandler = <T extends EntityShape>(
 type GetRepShape<T extends EntityShape, K extends object> = (shape: T) => {
   shape: T;
   update?: (data: K, shape: T) => void;
+  init?:  (data: K, shape: T) => void;
 };
 export type CustomTransformerProps<
   T extends BaseItem,
@@ -484,7 +489,7 @@ export type CustomTransformerProps<
   openSnap?: boolean;
   getRepShape?: GetRepShape<S, T>;
   beforeHandler?: (data: T, mat: Transform) => T;
-  handler?: (data: T, mat: Transform) => Transform | void | true;
+  handler?: (data: T, mat: Transform, raw?: Transform) => Transform | void | true;
   callback?: (data: T, mat: Transform) => void;
   transformerConfig?: TransformerConfig;
 };
@@ -510,6 +515,9 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
           update: () => {
             repResult.update && repResult.update(data.value, repResult.shape);
           },
+          init: () => {
+            repResult.init && repResult.init(data.value, repResult.shape);
+          },
           destory,
         };
       })
@@ -523,11 +531,12 @@ export const useCustomTransformer = <T extends BaseItem, S extends EntityShape>(
         : data.value;
 
       let nTransform;
+      const raw = current
       if (needSnap && (nTransform = needSnap[0](snapData))) {
         current = nTransform.multiply(current);
       }
       callMat = current;
-      const mat = handler(data.value, current);
+      const mat = handler(data.value, current, raw);
       if (mat) {
         if (repResult.update) {
           repResult.update(data.value, repResult.shape);

+ 4 - 0
src/core/propertys/describes.json

@@ -11,6 +11,10 @@
     "type": "color",
     "label": "填充色"
   },
+  "fontColor": {
+    "type": "color",
+    "label": "字体颜色"
+  },
   "coverFill": {
     "type": "color",
     "label": "背景颜色"

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

@@ -27,7 +27,7 @@
 
 <script lang="ts" setup>
 import { computed, nextTick, ref, watch, watchEffect } from "vue";
-import { useStage } from "../hook/use-global-vars.ts";
+import { useMode, useStage } from "../hook/use-global-vars.ts";
 import { DC, EntityShape } from "@/deconstruction.js";
 import { useViewerTransformConfig } from "../hook/use-viewer.ts";
 import { Transform } from "konva/lib/Util";
@@ -35,12 +35,16 @@ import { DomMountId } from "@/constant/index.ts";
 import { ElMenu, ElIcon, ElMenuItem } from "element-plus";
 import { useFormalLayer } from "../hook/use-layer.ts";
 import { shapeTreeContain } from "@/utils/shape.ts";
+import { Mode } from "@/constant/mode.ts";
 
 const props = defineProps<{
   target: DC<EntityShape> | undefined;
   data?: Record<string, any>;
   menus: Array<{ icon?: any; label?: string; handler: () => void }>;
 }>();
+const emit = defineEmits<{
+  (e: "show" | 'hide'): void;
+}>();
 
 const layout = ref<HTMLDivElement>();
 const stage = useStage();
@@ -57,9 +61,11 @@ const hasRClick = (ev: MouseEvent) => {
   return !!clickShape && shapeTreeContain(shape, clickShape) === shape;
 };
 
+const mode = useMode();
 watchEffect((onCleanup) => {
   const dom = stage.value?.getStage().container();
-  if (!dom) return;
+  if (!dom || mode.value.has(Mode.draw)) return;
+
   const clickHandler = async (ev: MouseEvent) => {
     const show = hasRClick(ev);
     if (show && rightClick.value) {
@@ -144,6 +150,10 @@ const calcPointer = async () => {
   move.translate(x, y);
   pointer.value = `matrix(${move.m.join(",")})`;
 };
+watch(() => !!pointer.value, (show) => {
+  emit(show ? 'show' : 'hide')
+})
+
 let timeout: any;
 const resetPointer = () => {
   if (hidden.value) {

+ 18 - 5
src/core/propertys/mount.vue

@@ -1,7 +1,7 @@
 <template>
   <Teleport :to="`#${DomMountId}`" v-if="stage">
     <transition name="mount-fade">
-      <div class="mount-layout" v-if="!hidden">
+      <div class="mount-layout" v-if="show">
         <div :size="8" class="mount-controller">
           <div v-for="(val, key) in describes" :key="key" class="mount-item">
             <span class="label">{{ val.label }}</span>
@@ -23,12 +23,14 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, watch } from "vue";
-import { useStage, useTransformIngShapes } from "../hook/use-global-vars.ts";
+import { computed, ref, watch, watchEffect } from "vue";
+import { useMode, useStage, useTransformIngShapes } from "../hook/use-global-vars.ts";
 import { PropertyDescribes, propertyComponents } from "./index.ts";
 import { DC, EntityShape } from "@/deconstruction.js";
 import { useMouseShapeStatus } from "../hook/use-mouse-status.ts";
 import { DomMountId } from "@/constant/index.ts";
+import { Mode } from "@/constant/mode.ts";
+import { debounce } from "@/utils/shared.ts";
 
 const props = defineProps<{
   show?: boolean;
@@ -40,14 +42,22 @@ const emit = defineEmits<{ (e: "change"): void }>();
 
 const stage = useStage();
 const status = useMouseShapeStatus(computed(() => props.target));
-const transformIngShapes = useTransformIngShapes();
+const mode = useMode();
 const hidden = computed(
   () =>
     !props.show &&
     (!stage.value?.getStage() ||
       !props.target?.getNode() ||
       !status.value.active ||
-      transformIngShapes.value.length !== 0)
+      mode.value.has(Mode.draw) ||
+      mode.value.has(Mode.draging))
+);
+const show = ref(false);
+watch(
+  hidden,
+  debounce(() => {
+    show.value = !hidden.value;
+  }, 32)
 );
 
 let isUpdate = false;
@@ -102,10 +112,12 @@ const changeHandler = () => {
       flex-wrap: wrap;
       align-items: center;
     }
+
     .label {
       display: block;
       margin-right: 10px;
       color: #303133;
+
       &::after {
         content: ":";
       }
@@ -117,6 +129,7 @@ const changeHandler = () => {
 .mount-fade-leave-active {
   transition: transform 0.3s ease, opacity 0.3s ease;
 }
+
 .mount-fade-enter-from,
 .mount-fade-leave-to {
   transform: translateX(100%);

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

@@ -18,6 +18,7 @@
         <ActiveBoxs />
         <SnapLines />
         <SplitLine v-if="expose.config.showLabelLine" />
+        <Debugger v-if="isDev" />
       </v-layer>
     </v-stage>
   </div>
@@ -30,7 +31,8 @@ import ActiveBoxs from "../helper/active-boxs.vue";
 import SnapLines from "../helper/snap-lines.vue";
 import BackGrid from "../helper/back-grid.vue";
 import SplitLine from "../helper/split-line.vue";
-import { DrawData, ShapeType, components } from "../components";
+import Debugger from "../helper/debugger.vue";
+import { ShapeType, components } from "../components";
 import {
   InstanceProps,
   useCursor,
@@ -56,6 +58,7 @@ store.setStore(props.data);
 useInstanceProps().set(props);
 useAutoService();
 
+const isDev = import.meta.env.DEV;
 const stage = useStage();
 const size = useResize();
 const maskMount = ref<HTMLDivElement>();

+ 291 - 22
src/example/fuse/views/init.ts

@@ -20,10 +20,10 @@ const initData = {
       height: 1000,
       url: "/五楼.png",
       mat: [1, 0, 0, 1, 591, 436],
+      lock: true,
     },
   ],
-  bgImage: [
-  ],
+  bgImage: [],
   rectangle: [
     {
       id: "0",
@@ -194,12 +194,12 @@ const initData = {
       opacity: 1,
       ref: false,
       points: [
-        { x: 237.44588045440253, y: 143.0442185836713 },
-        { x: 358.0396304544025, y: 143.0442185836713 },
-        { x: 358.0396304544025, y: 240.0207810836713 },
-        { x: 237.44588045440253, y: 240.0207810836713 },
+        { x: 207.06697420440253, y: 85.1184373336713 },
+        { x: 327.6607242044025, y: 85.1184373336713 },
+        { x: 327.6607242044025, y: 182.0949998336713 },
+        { x: 207.06697420440253, y: 182.0949998336713 },
       ],
-      attitude: [1, 0, 0, 1, -299.2142757955975, -65.5182814163287],
+      attitude: [1, 0, 0, 1, -329.5931820455975, -123.4440626663287],
     },
   ],
   triangle: [
@@ -299,15 +299,17 @@ const initData = {
       opacity: 1,
       ref: true,
       points: [
-        { x: 317.55859375, y: 328.6640625 },
-        { x: 596.73828125, y: 328.6640625 },
-        { x: 596.73828125, y: 549.26171875 },
-        { x: 317.55859375, y: 549.26171875 },
-        { x: 67.8828125, y: 549.26171875 },
-        { x: 67.8828125, y: 424.828125 },
-        { x: 177.33203125, y: 305.5390625 },
+        { x: 859.33203125, y: 305.10546875 },
+        { x: 859.33203125, y: 380.2421875 },
+        { x: 859.33203125, y: 548.828125 },
+        { x: 601.39453125, y: 548.828125 },
+        { x: 351.71875, y: 548.828125 },
+        { x: 351.71875, y: 424.39453125 },
+        { x: 351.71875, y: 273.09375 },
+        { x: 351.71875, y: 195.64453125 },
+        { x: 859.33203125, y: 195.64453125 },
       ],
-      attitude: [1, 0, 0, 1, 2.73046875, -1.93359375],
+      attitude: [1, 0, 0, 1, 286.56640625, -2.3671875],
     },
     {
       id: "fbdfc09e-1f81-4772-8c3a-316d50cc7da1",
@@ -316,12 +318,12 @@ const initData = {
       opacity: 1,
       ref: false,
       points: [
-        { x: 596.73828125, y: 328.6640625 },
-        { x: 829.3667899990835, y: 328.6640625 },
-        { x: 829.3667899990835, y: 549.26171875 },
-        { x: 596.73828125, y: 549.26171875 },
+        { x: 1705.741066896583, y: -839.713131499153 },
+        { x: 1938.3695756456666, y: -839.713131499153 },
+        { x: 1938.3695756456666, y: -619.115475249153 },
+        { x: 1705.741066896583, y: -619.115475249153 },
       ],
-      attitude: [1, 0, 0, 1, -54.85032944331101, -177.81612953327397],
+      attitude: [1, 0, 0, 1, 1054.152456203272, -1346.193323532427],
       strokeWidth: 10,
       stroke: "#C71585",
       fill: "#1F93FF",
@@ -388,6 +390,17 @@ const initData = {
       radiusX: 277.6712260605102,
       radiusY: 82.0068317036322,
     },
+    {
+      id: "5a292b3a-ce0e-47a8-a03b-c2c44c38f97f",
+      createTime: 1738834412964,
+      lock: false,
+      zIndex: 3,
+      opacity: 1,
+      ref: false,
+      mat: [1, 0, 0, 1, -540.352979059887, -785.2716514826207],
+      radiusX: 79.5859375,
+      radiusY: 101.392578125,
+    },
   ],
   icon: [
     {
@@ -421,7 +434,7 @@ const initData = {
       stroke: "red",
       strokeWidth: 1,
       strokeScaleEnabled: false,
-      mat: [1, 0, 0, 1, 1136.17578125, 358.5625],
+      mat: [1, 0, 0, 1, 2244.9019780377976, 1091.2727287182463],
       ref: true,
     },
     {
@@ -602,7 +615,7 @@ const initData = {
       fontFamily: "Calibri",
       fontSize: 30,
       content:
-        "Hello\\\\\\\\n  from th\\\\\\\\n\\\\\\\\ne     fr \\\\\\\\n am \\\\\\\\n \\\\\\\\new\\\\\\\\n\\\\\\\\nork. Tr \\\\\\\\ny  \\\\\\\\n   to resize me.",
+        "我爱人人人人爱我",
       mat: [
         -1.4930798945178672e-15, 1.0000000000000104, -1.0000000000000155,
         -1.5971633030764742e-15, 1083.2463425727433, 95.00094657517106,
@@ -621,6 +634,262 @@ const initData = {
       mat: [1, 0, 0, 1, 873.94921875, 659.453125],
     },
   ],
+  table: [
+    {
+      stroke: "#d8000a",
+      strokeWidth: 1,
+      id: "95b321a0-f36b-41e9-a102-96c04b2b4d39",
+      createTime: 1737885095135,
+      lock: false,
+      zIndex: 2,
+      opacity: 1,
+      ref: true,
+      width: 232.73828125,
+      height: 224.91796875,
+      content: [
+        [
+          {
+            content: "qwe",
+            width: 116.369140625,
+            height: 56.2294921875,
+            align: "center",
+            fontStyle: "italic",
+          },
+          {
+            content: "czx",
+            width: 116.369140625,
+            height: 56.2294921875,
+            align: "center",
+            fontStyle: "italic",
+          },
+        ],
+        [
+          {
+            content: "1231",
+            width: 116.369140625,
+            height: 56.2294921875,
+            align: "center",
+            fontStyle: "italic",
+          },
+          {
+            content: "5324",
+            width: 116.369140625,
+            height: 56.2294921875,
+            align: "center",
+            fontStyle: "italic",
+          },
+        ],
+        [
+          {
+            content: "asd",
+            width: 116.369140625,
+            height: 56.2294921875,
+            align: "center",
+            fontStyle: "italic",
+          },
+          {
+            content: "qe",
+            width: 116.369140625,
+            height: 56.2294921875,
+            align: "center",
+            fontStyle: "italic",
+          },
+        ],
+        [
+          {
+            content: "123",
+            width: 116.369140625,
+            height: 56.2294921875,
+            align: "center",
+            fontStyle: "italic",
+          },
+          {
+            content: "zxc",
+            width: 116.369140625,
+            height: 56.2294921875,
+            align: "center",
+            fontStyle: "italic",
+          },
+        ],
+      ],
+      mat: [1, 0, 0, 1, 1867.804575999732, 828.8137443432463],
+      align: "center",
+      fontStyle: "italic",
+      fill: "",
+    },
+    {
+      stroke: "#d8000a",
+      strokeWidth: 1,
+      fontSize: 16,
+      align: "center",
+      fontStyle: "normal",
+      fontColor: "#d8000a",
+      id: "de4475a7-95ef-4438-98ab-d6f0045f7f61",
+      createTime: 1738913187767,
+      lock: false,
+      zIndex: 4,
+      opacity: 1,
+      ref: false,
+      width: 425.8472387828342,
+      height: 262.0582582259674,
+      content: [
+        [
+          {
+            content: "1234",
+            width: 106.46180969570855,
+            height: 99.70181998577598,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "请问请问",
+            width: 106.46180969570855,
+            height: 99.70181998577598,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content:
+              "按时打算自行车按时按时打算自行车按时按时打算自行车按时按时打算自行车按时",
+            width: 106.46180969570855,
+            height: 99.70181998577598,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "亲请问请问请问",
+            width: 106.46180969570855,
+            height: 99.70181998577598,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+        [
+          {
+            content: "45234",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "123asdasdasdasdasdasd按时打算大叔大婶大叔大婶的",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "asdasd",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "自行车这些",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+        [
+          {
+            content: "65345",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "123123",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "123123",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "43214",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+        [
+          {
+            content: "5423234",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "123123",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "43224",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+          {
+            content: "123123",
+            width: 106.46180969570855,
+            height: 54.11881274673046,
+            padding: 8,
+            fontColor: "#d8000a",
+            fontStyle: "normal",
+            align: "center",
+          },
+        ],
+      ],
+      mat: [1, 0, 0, 1, 416.91799465370843, 869.4103464598702],
+    },
+  ],
 };
 
 const dataStr = localStorage.getItem("draw-data");

+ 5 - 1
src/example/fuse/views/slide/menu.ts

@@ -118,6 +118,10 @@ export const menus: MenuItem[] = [
   },
   {
     icon: "",
-    ...genItem("text", { width: 200, height: 100, content: "文本" }),
+    ...genItem("text", { content: "文本" }),
   },
+  {
+    icon: "",
+    ...genItem("table", { }),
+  }
 ];

+ 6 - 0
src/utils/math.ts

@@ -531,3 +531,9 @@ export const getLineRelationMat = (l1: [Pos, Pos], l2: [Pos, Pos]) => {
   }
   return mat
 };
+
+// 判断两向量是否垂直
+export const isVertical = (v1: Pos, v2: Pos) => {
+  console.log(vector(v1).dot(v2))
+  return zeroEq(vector(v1).dot(v2))
+}

+ 34 - 3
src/utils/shared.ts

@@ -205,15 +205,46 @@ export const arrayInsert = <T>(
 
 export const asyncTimeout = (time: number) => {
   let timeout: any;
-  let reject: any
+  let reject: any;
   const promise = new Promise<void>((resolve, r) => {
     timeout = setTimeout(resolve, time);
-    reject = r
+    reject = r;
   }) as Promise<void> & { stop: () => void };
 
   promise.stop = () => {
     clearTimeout(timeout);
-    reject('取消')
+    reject("取消");
   };
   return promise;
 };
+
+export const getResizeCorsur = (level = true, r = 0) => {
+  r = rangMod(r, 360);
+  if (level) {
+    if ((r > 0 && r < 20) || (r > 160 && r <= 200)) {
+      return "ew-resize";
+    }
+    if ((r >= 20 && r <= 70) || (r >= 200 && r <= 250)) {
+      return "nwse-resize";
+    } else if ((r > 70 && r < 110) || (r > 250 && r < 290)) {
+      return "ns-resize";
+    } else if ((r >= 110 && r <= 160) || (r >= 290 && r <= 340)) {
+      return "nesw-resize";
+    } else {
+      return "ew-resize";
+    }
+  } else {
+    if ((r > 0 && r < 20) || (r > 160 && r <= 200)) {
+      return "ns-resize";
+    }
+    if ((r >= 20 && r <= 70) || (r >= 200 && r <= 250)) {
+      return "nesw-resize";
+    } else if ((r > 70 && r < 110) || (r > 250 && r < 290)) {
+      return "ew-resize";
+    } else if ((r >= 110 && r <= 160) || (r >= 290 && r <= 340)) {
+      return "nwse-resize";
+    } else {
+      return "ns-resize";
+    }
+  }
+};