Parcourir la source

序号引用功能制作

bill il y a 3 semaines
Parent
commit
69b3b2d041
38 fichiers modifiés avec 1120 ajouts et 356 suppressions
  1. 1 0
      public/icons/info.svg
  2. 7 5
      src/core/components/line/single-point.vue
  3. 189 38
      src/core/components/serial/index.ts
  4. 6 151
      src/core/components/serial/serial-group.vue
  5. 14 0
      src/core/components/serial/serial.vue
  6. 139 2
      src/core/components/serial/temp-serial.vue
  7. 7 5
      src/core/components/share/edit-point.vue
  8. 1 0
      src/core/components/table/index.ts
  9. 1 0
      src/core/components/table/table.vue
  10. 1 0
      src/core/components/util.ts
  11. 8 3
      src/core/hook/use-draw.ts
  12. 105 22
      src/core/hook/use-expose.ts
  13. 54 12
      src/core/hook/use-interactive.ts
  14. 82 2
      src/core/hook/use-paste.ts
  15. 6 2
      src/core/hook/use-selection.ts
  16. 2 2
      src/core/html-mount/propertys/mount-describes.vue
  17. 2 0
      src/core/renderer/group.vue
  18. 1 0
      src/core/renderer/renderer.vue
  19. 4 0
      src/example/components/header/index.vue
  20. 44 20
      src/example/components/show-vr.vue
  21. 1 0
      src/example/components/slide/menu.ts
  22. 7 1
      src/example/components/slide/slide.vue
  23. 1 0
      src/example/constant.ts
  24. 3 2
      src/example/fuse/enter-case.ts
  25. 2 3
      src/example/fuse/enter.ts
  26. 18 0
      src/example/fuse/global.scss
  27. 10 8
      src/example/fuse/router.ts
  28. 2 0
      src/example/fuse/views/defStyle.ts
  29. 24 16
      src/example/fuse/views/overview/header.vue
  30. 14 1
      src/example/fuse/views/overview/index.vue
  31. 13 3
      src/example/fuse/views/overview/slide.vue
  32. 25 19
      src/example/fuse/views/tabulation/gen-tab.ts
  33. 32 4
      src/example/fuse/views/tabulation/header.vue
  34. 120 0
      src/example/fuse/views/tabulation/index.vue
  35. 8 1
      src/example/fuse/views/tabulation/overview-viewport.vue
  36. 28 19
      src/example/fuse/views/tabulation/slide.vue
  37. 1 0
      src/example/platform/platform-draw.ts
  38. 137 15
      src/utils/math.ts

Fichier diff supprimé car celui-ci est trop grand
+ 1 - 0
public/icons/info.svg


+ 7 - 5
src/core/components/line/single-point.vue

@@ -3,16 +3,17 @@
     <!-- <v-text :text="point.id" :x="point.x" :y="point.y" :fontSize="18" /> -->
     <EditPoint
       :ref="(r: any) => shapes[ndx] = r?.shape"
-      :size="line.strokeWidth"
+      :size="5"
       :points="points"
+      fill="#ffffff"
       :opacity="showEditPoint ? 1 : 0"
       :drawIng="isDrawIng ? (drawMode ? point === drawMode : ndx === 1) : false"
       :ndx="ndx"
       :closed="false"
       :id="line.id"
-      :fixed="line.fixed"
+      :fixed="true"
       :disable="addMode || !!drawMode"
-      :color="isDrawIng ? themeColor : style.stroke"
+      :color="themeColor"
       @dragstart="dragstartHandler([point.id])"
       @update:position="(p) => emit('updatePoint', { ...point, ...p })"
       @dragend="dragendHandler"
@@ -31,10 +32,11 @@ import { DC } from "@/deconstruction.js";
 import { Line } from "konva/lib/shapes/Line";
 import { useLineDataSnapInfos } from "./attach-server.ts";
 import { Circle } from "konva/lib/shapes/Circle";
+import { useFixedScale } from "@/core/hook/use-viewer.ts";
 
 const props = defineProps<{
   lineShape?: DC<Line>;
-  type: 'line' | 'lineChunk';
+  type: "line" | "lineChunk";
   line: LineData["lines"][number];
   addMode?: boolean;
   data: LineData;
@@ -49,7 +51,7 @@ const emit = defineEmits<{
   (e: "updateBefore", value: string[]): void;
   (e: "update"): void;
 }>();
-
+const fixedScale = useFixedScale();
 const points = computed(() => [
   props.data.points.find((p) => p.id === props.line.a)!,
   props.data.points.find((p) => p.id === props.line.b)!,

+ 189 - 38
src/core/components/serial/index.ts

@@ -4,9 +4,19 @@ import { CircleData, defaultStyle as circleDefaultStyle } from "../circle";
 import { InteractiveFix, InteractiveTo, MatResponseProps } from "../index.ts";
 import { DrawStore } from "@/core/store/index.ts";
 import { Pos } from "@/utils/math.ts";
-import { TableData } from "../table/index.ts";
+import {
+  TableCollData,
+  TableData,
+  interactiveToData as tableInteractiveToData,
+} from "../table/index.ts";
 import { copy } from "@/utils/shared.ts";
 import { ref } from "vue";
+import {
+  useGetViewBoxPositionPixel,
+  useViewerInvertTransform,
+} from "@/core/hook/use-viewer.ts";
+import { useHistory } from "@/core/hook/use-history.ts";
+import { DrawExpose } from "@/core/hook/use-expose.ts";
 
 export {
   getMouseStyle,
@@ -37,9 +47,9 @@ export const defaultStyle = {
   strokeWidth: 2,
 };
 
-export const fixedStrokeOptions:number[] = [];
-export const autoGenTable = ref(true)
-export type SerialData = CircleData;
+export const fixedStrokeOptions: number[] = [];
+export const autoGenTable = ref(true);
+export type SerialData = CircleData & { joinIds?: string[] };
 
 export const getSerialFontW = (data: SerialData) => {
   const fontSize = data.fontSize || defaultStyle.fontSize;
@@ -82,6 +92,7 @@ export const interactiveToData: InteractiveTo<"serial"> = ({
       fontSize: defaultStyle.fontSize,
       ...getBaseItem(),
       padding: 3,
+      joinIds: [],
       content: getCurrentNdx(store),
       ...preset,
     } as unknown as SerialData;
@@ -95,6 +106,7 @@ export const interactiveFixData: InteractiveFix<"serial"> = ({
 }) => {
   data.mat = new Transform().translate(info.cur!.x, info.cur!.y).m;
   const radius = (getSerialFontW(data) * Math.sqrt(2)) / 2;
+  data.joinIds = info.attach || [];
   data.radiusX = radius;
   data.radiusY = radius;
   return data;
@@ -103,7 +115,7 @@ export const interactiveFixData: InteractiveFix<"serial"> = ({
 export const delItemRaw = (
   table: TableData,
   data: SerialData[],
-  item: SerialData
+  item: SerialData,
 ) => {
   const ndx = data.indexOf(item);
   const getPosition = (itemNdx: number) => {
@@ -120,41 +132,17 @@ export const delItemRaw = (
       r = s;
       s = getPosition(i);
       for (let j = 0; j < s[2]; j++) {
-        Object.assign(table.content[r[0]][r[1] + j], table.content[s[0]][s[1] + j])
-        // table.content[r[0]][r[1] + j] = copy(table.content[s[0]][s[1] + j]);
-        // if (j === 0) {
-        //   table.content[r[0]][r[1] + j].content = oldItem.content!;
-        // } else {
-        //   table.content[r[0]][r[1] + j].content =
-        //     table.content[s[0]][s[1] + j].content!;
-        // }
+        Object.assign(
+          table.content[r[0]][r[1] + j],
+          table.content[s[0]][s[1] + j],
+        );
       }
-      // let cur = { ...data[i] };
-      // data[i].content = oldItem.content;
-      // const radius = (getSerialFontW(data[i]) * Math.sqrt(2)) / 2;
-      // data[i].radiusX = radius;
-      // data[i].radiusY = radius;
-      // matResponse({
-      //   data: data[i],
-      //   mat: new Transform(data[i].mat),
-      //   increment: false,
-      // });
-      // store.setItem("serial", { id: data[i].id, value: { ...data[i] } });
-      // oldItem = cur;
     }
 
-    console.log(copy(table))
     for (let j = 0; j < s[2]; j++) {
-      console.log({...table.content[s[0]][s[1] + j]})
       table.content[s[0]][s[1] + j].content = "";
-      // if (j === 0) {
-      //   table.content[r[0]][r[1] + j].content = oldItem.content!;
-      // } else {
-      //   table.content[r[0]][r[1] + j].content =
-      //     table.content[s[0]][s[1] + j].content!;
-      // }
     }
-    console.log(copy(table))
+    console.log(copy(table));
     const cols = table.content.flatMap((row) => {
       const cols = [];
       for (let i = 0; i < row.length; i += 2) {
@@ -164,9 +152,8 @@ export const delItemRaw = (
     });
 
     const delRowCount = Math.floor(
-      cols.filter((item) => item.content === "").length / 2
+      cols.filter((item) => item.content === "").length / 2,
     );
-    console.log('delRowCount', delRowCount, cols.filter((item) => item.content === "").length)
     for (let i = 0; i < delRowCount; i++) {
       table.height -= table.content.pop()![0].height;
     }
@@ -174,7 +161,7 @@ export const delItemRaw = (
 };
 
 export const joinKey = "serial-table";
-export const tableTitle = `图示`
+export const tableTitle = `图示`;
 export const delItem = (store: DrawStore, item: SerialData) => {
   const table = store
     .getTypeItems("table")
@@ -194,7 +181,7 @@ export const delItem = (store: DrawStore, item: SerialData) => {
 
 export const matResponse = (
   { data, mat, increment }: MatResponseProps<"serial">,
-  initRadius?: Pos
+  initRadius?: Pos,
 ) => {
   if (!initRadius) {
     initRadius = {
@@ -218,3 +205,167 @@ export const getPredefine = (key: keyof CircleData) => {
     return { canun: true };
   }
 };
+
+const getTempContents = () => {
+  const tempContents: TableCollData[] = [];
+  for (let i = 0; i < defaultTableStyle.repColCount; i++) {
+    tempContents.push(
+      {
+        key: "preset-col",
+        content: "序号",
+        readonly: true,
+        notdel: true,
+        width: defaultTableStyle.nameColWidth,
+        height: defaultTableStyle.colHeight,
+        fontSize: defaultTableStyle.fontSize,
+        padding: defaultTableStyle.padding,
+      },
+      {
+        key: "preset-col",
+        content: "描述",
+        readonly: true,
+        notdel: true,
+        width: defaultTableStyle.valueColWidth,
+        height: defaultTableStyle.colHeight,
+        fontSize: defaultTableStyle.fontSize,
+        padding: defaultTableStyle.padding,
+      },
+    );
+  }
+  return tempContents;
+};
+
+export const addTable = (draw: DrawExpose) => {
+  return draw.runHook(() => {
+    const getPosition = useGetViewBoxPositionPixel();
+    const invMat = useViewerInvertTransform();
+    const history = useHistory();
+    let margin = draw.config.margin || 0;
+    if (!Array.isArray(margin)) {
+      margin = [margin, margin, margin, margin];
+    }
+
+    const w =
+      (defaultTableStyle.nameColWidth + defaultTableStyle.valueColWidth) *
+      defaultTableStyle.repColCount;
+    const h = defaultTableStyle.colHeight;
+
+    let pos = getPosition(
+      {
+        right: defaultTableStyle.right + margin[1],
+        top: defaultTableStyle.top + margin[0],
+      },
+      { width: w, height: h },
+    );
+    pos = invMat.value.point(pos);
+    const end = {
+      x: pos.x + w,
+      y: pos.y + h,
+    };
+
+    // const content: TableCollData[] = [];
+    const content: TableCollData[] = getTempContents();
+    return tableInteractiveToData({
+      info: { cur: [pos, end] },
+      preset: {
+        notaddRow: true,
+        notaddCol: true,
+        key: joinKey,
+        fontSize: defaultTableStyle.fontSize,
+        title: tableTitle,
+        content: [content],
+        strokeWidth: defaultTableStyle.tableStrokeWidth,
+      },
+      store: draw.store,
+      history,
+      notdraw: true,
+    } as any)!;
+  });
+};
+
+export const syncTable = (table: TableData, items: SerialData[]) => {
+  const tempRow = table.content[table.content.length - 1];
+  const presetIndex = table.content.findIndex(
+    (row) => row[0].key === "preset-col",
+  );
+  if (presetIndex !== -1) {
+    table.content.splice(presetIndex, 1);
+    table.height -= tempRow[0].height;
+  }
+
+  const colCount = tempRow.length / 2;
+  const oldData: Record<string, string> = {};
+  table.content.forEach((row) => {
+    for (let i = 0; i < row.length; i += colCount) {
+      if (row[i + 1].joinId) {
+        oldData[row[i + 1].joinId!] = row[i + 1].content;
+      }
+    }
+  });
+  
+  if (~presetIndex) {
+    table.content = table.content.slice(0, presetIndex + 1);
+    table.height = table.content.reduce((t, item) => item[0].height + t, 0)
+  } else {
+    table.content = [];
+    table.height = 0
+  }
+
+  const cols = table.content.flatMap((row) => {
+    const cols = [];
+    for (let i = 0; i < row.length; i += 2) {
+      cols.push(row[i]);
+    }
+    return cols;
+  });
+
+  let isUpdate = false;
+  for (let i = 0; i < items.length; i++) {
+    const item = items[i];
+    if (cols.some((col) => col.content === item.content)) {
+      continue;
+    }
+    let rowNdx = Math.floor(i / colCount);
+    let colNdx = (i % colCount) * 2;
+    const val = item.desc || oldData[item.id] || ""
+    if (colNdx) {
+      table.content[rowNdx][colNdx].content = item.content!;
+      table.content[rowNdx][colNdx + 1].content = val;
+      table.content[rowNdx][colNdx + 1].joinId = item.id
+    } else {
+      table.height += tempRow[0].height;
+      let cols = [
+        {
+          ...tempRow[0],
+          content: item.content!,
+          hidden: false,
+          key: "serial-name",
+        },
+        {
+          ...tempRow[1],
+          content: val,
+          readonly: false,
+          hidden: false,
+          joinId: item.id,
+          key: "serial-desc",
+        },
+      ];
+      for (let i = 1; i < colCount; i++) {
+        cols.push(
+          { ...tempRow[2], content: "", hidden: false, key: "serial-name" },
+          {
+            ...tempRow[3],
+            content: "",
+            readonly: false,
+            hidden: false,
+            key: "serial-desc",
+          },
+        );
+      }
+      table.content.push(cols);
+    }
+
+    isUpdate = true;
+  }
+  return isUpdate;
+};

+ 6 - 151
src/core/components/serial/serial-group.vue

@@ -20,54 +20,17 @@ import { computed, watch } from "vue";
 import { useHistory } from "@/core/hook/use-history";
 import { ShapeType } from "..";
 import {
-  TableCollData,
-  TableData,
-  interactiveToData as tableInteractiveToData,
-} from "../table";
-import {
+  addTable,
   autoGenTable,
-  defaultTableStyle,
   delItem,
   getCurrentNdx,
   joinKey,
   SerialData,
-  tableTitle,
+  syncTable,
 } from ".";
-import {
-  useGetViewBoxPositionPixel,
-  useViewerInvertTransform,
-} from "@/core/hook/use-viewer";
-import { useConfig } from "@/core/hook/use-config";
+import { useExpose } from "@/core/hook/use-expose";
 
 defineProps<{ type?: ShapeType }>();
-const getTempContents = () => {
-  const tempContents: TableCollData[] = [];
-  for (let i = 0; i < defaultTableStyle.repColCount; i++) {
-    tempContents.push(
-      {
-        key: "preset-col",
-        content: "序号",
-        readonly: true,
-        notdel: true,
-        width: defaultTableStyle.nameColWidth,
-        height: defaultTableStyle.colHeight,
-        fontSize: defaultTableStyle.fontSize,
-        padding: defaultTableStyle.padding,
-      },
-      {
-        key: "preset-col",
-        content: "描述",
-        readonly: true,
-        notdel: true,
-        width: defaultTableStyle.valueColWidth,
-        height: defaultTableStyle.colHeight,
-        fontSize: defaultTableStyle.fontSize,
-        padding: defaultTableStyle.padding,
-      }
-    );
-  }
-  return tempContents;
-};
 
 const store = useStore();
 const data = computed(() => store.getTypeItems("serial"));
@@ -83,115 +46,7 @@ const jTable = computed(() =>
   store.getTypeItems("table").find((item) => item.key === joinKey)
 );
 
-const config = useConfig();
-const margin = computed(() => {
-  let margin = config.margin || 0;
-  if (!Array.isArray(margin)) {
-    margin = [margin, margin, margin, margin];
-  }
-  return margin;
-});
-const invMat = useViewerInvertTransform();
-const getPosition = useGetViewBoxPositionPixel();
-const addTable = () => {
-  const w =
-    (defaultTableStyle.nameColWidth + defaultTableStyle.valueColWidth) *
-    defaultTableStyle.repColCount;
-  const h = defaultTableStyle.colHeight;
-
-  let pos = getPosition(
-    {
-      right: defaultTableStyle.right + margin.value[1],
-      top: defaultTableStyle.top + margin.value[0],
-    },
-    { width: w, height: h }
-  );
-  pos = invMat.value.point(pos);
-  const end = {
-    x: pos.x + w,
-    y: pos.y + h,
-  };
-
-  // const content: TableCollData[] = [];
-  const content: TableCollData[] = getTempContents();
-  return tableInteractiveToData({
-    info: { cur: [pos, end] },
-    preset: {
-      notaddRow: true,
-      notaddCol: true,
-      key: joinKey,
-      fontSize: defaultTableStyle.fontSize,
-      title: tableTitle,
-      content: [content],
-      strokeWidth: defaultTableStyle.tableStrokeWidth,
-    },
-    store,
-    history,
-    notdraw: true,
-  } as any)!;
-};
-
-const syncTable = (table: TableData) => {
-  const tempRow = table.content[table.content.length - 1];
-  const presetIndex = table.content.findIndex((row) => row[0].key === "preset-col");
-  if (presetIndex !== -1) {
-    table.content.splice(presetIndex, 1);
-    table.height -= tempRow[0].height;
-  }
-
-  const items = data.value;
-  const cols = table.content.flatMap((row) => {
-    const cols = [];
-    for (let i = 0; i < row.length; i += 2) {
-      cols.push(row[i]);
-    }
-    return cols;
-  });
-  const colCount = tempRow.length / 2;
-
-  let isUpdate = false;
-  for (let i = 0; i < items.length; i++) {
-    const item = items[i];
-    if (cols.some((col) => col.content === item.content)) {
-      continue;
-    }
-    let rowNdx = Math.floor(i / colCount);
-    let colNdx = (i % colCount) * 2;
-    if (colNdx) {
-      table.content[rowNdx][colNdx].content = item.content!;
-      table.content[rowNdx][colNdx + 1].content = item.desc || "";
-    } else {
-      table.height += tempRow[0].height;
-      let cols = [
-        { ...tempRow[0], content: item.content!, hidden: false, key: "serial-name" },
-        {
-          ...tempRow[1],
-          content: item.desc || "",
-          readonly: false,
-          hidden: false,
-          key: "serial-desc",
-        },
-      ];
-      for (let i = 1; i < colCount; i++) {
-        cols.push(
-          { ...tempRow[2], content: "", hidden: false, key: "serial-name" },
-          {
-            ...tempRow[3],
-            content: "",
-            readonly: false,
-            hidden: false,
-            key: "serial-desc",
-          }
-        );
-      }
-      table.content.push(cols);
-    }
-
-    isUpdate = true;
-  }
-  return isUpdate;
-};
-
+const draw = useExpose();
 const updateJoinTable = () => {
   const items = data.value;
 
@@ -203,10 +58,10 @@ const updateJoinTable = () => {
 
   if (!table) {
     history.preventTrack(() => {
-      store.addItem("table", (table = addTable()));
+      store.addItem("table", (table = addTable(draw)));
     });
   }
-  if (syncTable(table!)) {
+  if (syncTable(table!, data.value)) {
     history.preventTrack(() =>
       store.setItem("table", {
         id: table!.id,

+ 14 - 0
src/core/components/serial/serial.vue

@@ -99,5 +99,19 @@ const { shape, tData, data, operateMenus, describes } = useComponentStatus<
   ],
 });
 
+describes.value.content = {
+  type: "inputNum",
+  label: "数值",
+  "layout-type": "row",
+  get value() {
+    return Number(data.value.content || 1);
+  },
+  onChange() {
+    emit("updateShape", { ...data.value });
+  },
+  set value(val) {
+    data.value.content = val.toString();
+  },
+};
 useInstallStrokeWidthDescribe(describes, data, fixedStrokeOptions, undefined, true);
 </script>

+ 139 - 2
src/core/components/serial/temp-serial.vue

@@ -17,16 +17,40 @@
       @update-text="(val) => emit('updateContent', val)"
       @update:is-edit="(val) => emit('update:isEdit', val)"
     />
+
+    <v-arrow
+      v-for="join in joinPoints"
+      :config="{
+        points: flatPositions(join),
+        pointerWidth: 10,
+        listening: false,
+        fill: data.stroke,
+        stroke: data.stroke,
+        strokeWidth: data.strokeWidth,
+      }"
+    />
   </v-group>
 </template>
 
 <script lang="ts" setup>
 import ShareText from "../share/text.vue";
 import { SerialData, defaultStyle } from "./index.ts";
-import { computed, ref } from "vue";
+import { computed, nextTick, ref, watch } from "vue";
 import { DC } from "@/deconstruction.js";
 import { Circle } from "konva/lib/shapes/Circle";
 import { Transform } from "konva/lib/Util";
+import {
+  useFixedScale,
+  useUnitTransform,
+  useViewerInvertTransform,
+} from "@/core/hook/use-viewer.ts";
+import { useStore } from "@/core/store/index.ts";
+import { getCircleLineIntersection, getLineRectIntersection, Pos } from "@/utils/math.ts";
+import { useStage } from "@/core/hook/use-global-vars.ts";
+import { Node } from "konva/lib/Node";
+import { flatPositions } from "@/utils/shared.ts";
+import { MathUtils } from "three";
+import { Group } from "konva/lib/Group";
 
 const props = defineProps<{ data: SerialData; addMode?: boolean; editer?: boolean }>();
 const emit = defineEmits<{
@@ -34,7 +58,14 @@ const emit = defineEmits<{
   (e: "update:isEdit", data: boolean): void;
 }>();
 
-const data = computed(() => ({ ...defaultStyle, ...props.data }));
+const scale = useFixedScale();
+const data = computed(() => {
+  const data = { ...defaultStyle, ...props.data };
+  if (data.fixed) {
+    data.strokeWidth *= scale.value;
+  }
+  return data;
+});
 
 const matConfig = computed(() => {
   const mat = new Transform(data.value.mat);
@@ -64,6 +95,112 @@ const textConfig = computed(() => ({
   text: data.value.content,
 }));
 
+const store = useStore();
+const stage = useStage();
+const unit = useUnitTransform();
+const viewMat = useViewerInvertTransform();
+const getJoinPoint = (shape: Node) => {
+  const rect = shape.getClientRect();
+  const p = viewMat.value.point({
+    x: rect.x + rect.width / 2,
+    y: rect.y + rect.height / 2,
+  });
+  const lt = viewMat.value.point({
+    x: rect.x,
+    y: rect.y,
+  });
+  const rt = viewMat.value.point({
+    x: rect.x + rect.width,
+    y: rect.y + rect.height,
+  });
+  const irect = {
+    x: Math.min(lt.x, rt.x),
+    y: Math.min(lt.y, rt.y),
+    width: Math.abs(lt.x - rt.x),
+    height: Math.abs(lt.y - rt.y),
+  };
+  const origin = { x: matConfig.value.x, y: matConfig.value.y };
+  const xdiff = origin.x - p.x;
+  const ydiff = origin.y - p.y;
+  const xadiff = Math.abs(xdiff);
+  const yadiff = Math.abs(ydiff);
+  const fa = Math.max(50, props.data.radiusX + 10);
+  const cline: Pos[] = [];
+
+  if (yadiff > xadiff) {
+    if (xadiff < fa) {
+      cline.push({ x: origin.x - xdiff, y: origin.y }, p);
+    } else {
+      const diff = xdiff > 0 ? fa : -fa;
+      cline.push({ x: origin.x - diff, y: origin.y }, p);
+    }
+
+    if (xadiff > props.data.radiusX) {
+      const offset = xdiff > 0 ? props.data.radiusX : -props.data.radiusX;
+      cline.unshift({ ...origin, x: origin.x - offset });
+    } else {
+      const j = getCircleLineIntersection(cline, {
+        center: origin,
+        radiusX: props.data.radiusX,
+        radiusY: props.data.radiusY,
+      });
+      if (j) {
+        cline[0] = j;
+      }
+    }
+  } else {
+    if (yadiff < fa) {
+      cline.push({ x: origin.x, y: origin.y - ydiff }, p);
+    } else {
+      const diff = ydiff > 0 ? fa : -fa;
+      cline.push({ x: origin.x, y: origin.y - diff }, p);
+    }
+
+    if (yadiff > props.data.radiusY) {
+      const offset = ydiff > 0 ? props.data.radiusY : -props.data.radiusY;
+      cline.unshift({ ...origin, y: origin.y - offset });
+    } else {
+      const j = getCircleLineIntersection(cline, {
+        center: origin,
+        radiusX: props.data.radiusX,
+        radiusY: props.data.radiusY,
+      });
+      if (j) {
+        cline[0] = j;
+      }
+    }
+  }
+
+  const l = cline.slice(cline.length - 2, cline.length);
+  const join = getLineRectIntersection(l, irect);
+  if (join) {
+    cline[cline.length - 1] = join;
+  }
+  return cline;
+};
+
+const joinPoints = ref<Pos[][]>([]);
+watch(
+  () => {
+    if (props.data.joinIds) {
+      props.data.joinIds.forEach((id) => store.getItemById(id));
+    }
+    return [props.data.mat[4], props.data.mat[5]];
+  },
+  async () => {
+    if (!props.data.joinIds?.length) return;
+    const $stage = stage.value!.getNode();
+    joinPoints.value = props.data.joinIds
+      .map((id) => {
+        const shape = $stage.findOne<Group>(`#${id}`)?.findOne(".repShape");
+        if (!shape) return;
+        return getJoinPoint(shape);
+      })
+      .filter(Boolean) as Pos[][];
+  },
+  { immediate: true }
+);
+
 const shape = ref<DC<Circle>>();
 defineExpose({
   get shape() {

+ 7 - 5
src/core/components/share/edit-point.vue

@@ -30,10 +30,7 @@ import { useShapeIsHover } from "@/core/hook/use-mouse-status";
 import { useCursor } from "@/core/hook/use-global-vars";
 import { mergeFuns, rangMod } from "@/utils/shared";
 import { Operate } from "../../html-mount/propertys/index.ts";
-import {
-  useFixedScale,
-  useViewer,
-} from "@/core/hook/use-viewer.ts";
+import { useFixedScale, useViewer } from "@/core/hook/use-viewer.ts";
 import { useMode } from "@/core/hook/use-status";
 import { Mode } from "@/constant/mode";
 
@@ -47,6 +44,7 @@ const props = defineProps<{
   disable?: boolean;
   drawIng?: boolean;
   opacity?: number;
+  fill?: string;
   closed?: boolean;
   notDelete?: boolean;
   getSelfSnapInfos?: (point: Pos) => ComponentSnapInfo[];
@@ -68,7 +66,11 @@ const style = computed(() => {
 
   return {
     radius: size / 2,
-    fill: props.drawIng || isHover.value || dragIng.value ? "#fff" : color.pub,
+    fill: props.fill
+      ? props.fill
+      : props.drawIng || isHover.value || dragIng.value
+      ? "#fff"
+      : color.pub,
     stroke: color.pub,
     strokeWidth: size / 4,
     opacity: props.opacity !== undefined ? props.opacity : props.disable ? 0.5 : 1,

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

@@ -44,6 +44,7 @@ export type TableCollData = Partial<typeof defaultCollData> &
     readonly?: boolean;
     notdel?: boolean;
     key?: string
+    joinId?: string
   };
 export type TableData = Partial<typeof defaultStyle> &
   BaseItem &

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

@@ -294,6 +294,7 @@ watchEffect(
 let addMenu: any;
 const menuShowHandler = () => {
   const config = tableRef.value!.getMouseIntersect();
+  if (!config) return;
   addMenu = [];
   if (!data.value.notaddRow) {
     addMenu.push({

+ 1 - 0
src/core/components/util.ts

@@ -7,6 +7,7 @@ import { defaultLayer } from "@/constant";
 export type BaseItem = {
   id: string;
   createTime: number;
+  name?: string
   zIndex: number;
   disableTransformer?: boolean
   disableEditText?: boolean

+ 8 - 3
src/core/hook/use-draw.ts

@@ -30,6 +30,7 @@ import DrawShape from "../renderer/draw-shape.vue";
 import { useHistory, useHistoryAttach } from "./use-history";
 import { useCurrentZIndex } from "./use-layer";
 import { useViewerTransform } from "./use-viewer";
+import { useMouseShapesStatus } from "./use-mouse-status";
 
 type PayData<T extends ShapeType> = ComponentValue<T, "addMode"> extends "area"
   ? Area
@@ -46,6 +47,7 @@ export type AddMessage<T extends ShapeType> = {
   consumed: PayData<T>[];
   cur?: PayData<T>;
   ndx?: number;
+  attach?: any
   action: MessageAction;
 };
 
@@ -82,6 +84,7 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
       const type = store.getType(id);
       type && store.delItem(type, id);
     },
+    shapesStatus: useMouseShapesStatus(),
     addShape: <T extends ShapeType>(
       shapeType: T,
       preset: Partial<DrawItem<T>> = {},
@@ -114,7 +117,8 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
       shapeType: T,
       preset: InteractivePreset<T>["preset"] = {},
       single = false,
-      force = false
+      force = false,
+      preSelectIds?: string[]
     ) => {
       if (isEnter) {
         leave();
@@ -129,10 +133,9 @@ export const useInteractiveDrawShapeAPI = installGlobalVar(() => {
       interactiveProps.value = {
         type: shapeType,
         preset,
-        operate: { single },
+        operate: { single, preSelectIds },
         callback: leave,
       };
-      console.log(interactiveProps.value)
       enter();
     },
     quitDrawShape: () => {
@@ -325,11 +328,13 @@ const useInteractiveDrawTemp = <T extends ShapeType>({
       watch(
         cur,
         () => {
+          const info = ia.attachInfos?.get(cur as any)
           obj.interactiveFixData({
             info: {
               cur,
               consumed: ia.consumedMessage,
               action: MessageAction.update,
+              attach: info
             },
             data: item,
             history,

+ 105 - 22
src/core/hook/use-expose.ts

@@ -19,6 +19,8 @@ import {
   useSetViewport,
   useViewer,
   useViewerDebounce,
+  useViewerInvertTransform,
+  useViewerTransform,
   useViewerTransformConfig,
 } from "./use-viewer.ts";
 import { useGlobalResize, useListener } from "./use-event.ts";
@@ -29,23 +31,33 @@ import { usePaste } from "./use-paste.ts";
 import { useMouseShapesStatus } from "./use-mouse-status.ts";
 import { Mode } from "@/constant/mode.ts";
 import { ElMessageBox } from "element-plus";
-import { mergeFuns } from "@/utils/shared.ts";
+import { asyncTimeout, mergeFuns } from "@/utils/shared.ts";
 import { getImage, isSvgString } from "@/utils/resource.ts";
 import { useResourceHandler } from "./use-fetch.ts";
 import { useConfig } from "./use-config.ts";
 import { useExcludeSelection, useSelectionRevise } from "./use-selection.ts";
 import { useFormalLayer, useGetFormalChildren } from "./use-layer.ts";
-import { components } from "../components/index.ts";
+import { components, DrawItem, ShapeType } from "../components/index.ts";
 import { useProportion } from "./use-proportion.ts";
 import { useGetDXF } from "./use-dxf.ts";
 import { getIconStyle } from "../components/icon/index.ts";
 import { useGetShapeBelong } from "./use-component.ts";
+import { IRect } from "konva/lib/types";
+import { getBaseItem } from "../components/util.ts";
+import { Pos } from "@/utils/math.ts";
+import { Transform } from "konva/lib/Util";
+import { getCurrentNdx, getSerialFontW } from "../components/serial/index.ts";
 
 // 自动粘贴服务
 export const useAutoPaste = () => {
   const paste = usePaste();
   const drawAPI = useInteractiveDrawShapeAPI();
   const resourceHandler = useResourceHandler();
+  const viewMat = useViewerTransform();
+  const viewInvMat = useViewerInvertTransform();
+  const store = useStore();
+  const history = useHistory();
+
   paste.push({
     ["text/plain"]: {
       async handler(pos, val) {
@@ -57,7 +69,7 @@ export const useAutoPaste = () => {
             { ...style, fill: undefined, stroke: undefined },
             pos,
             true,
-            true
+            true,
           );
         } else {
           drawAPI.addShape("text", { content: val }, pos, true, true);
@@ -69,13 +81,17 @@ export const useAutoPaste = () => {
       async handler(pos, val, type) {
         const url = await resourceHandler(val, type);
         if (type.includes("svg")) {
-          const style = await getIconStyle(window.platform.getResource(url), 100, 100);
+          const style = await getIconStyle(
+            window.platform.getResource(url),
+            100,
+            100,
+          );
           drawAPI.addShape(
             "icon",
             { ...style, fill: undefined, stroke: undefined },
             pos,
             true,
-            true
+            true,
           );
         } else {
           const image = await getImage(window.platform.getResource(url));
@@ -84,12 +100,75 @@ export const useAutoPaste = () => {
             { url, width: image.width, height: image.height },
             pos,
             true,
-            true
+            true,
           );
         }
       },
       type: "file",
     },
+    ["draw-shape/json"]: {
+      handler(pos, val) {
+        const { center: oldCenter, items } = JSON.parse(val) as {
+          center: Pos;
+          items: {
+            rect: IRect;
+            type: ShapeType;
+            data: any;
+          }[];
+        };
+        const mouse = viewInvMat.value.point(pos);
+
+        const copy = () => {
+          for (const item of items) {
+            const base = getBaseItem();
+            const data = {
+              ...base,
+              ...item.data,
+              id: base.id,
+              createTime: base.createTime,
+            };
+
+            switch (item.type) {
+              case "text":
+                data.mat[4] = mouse.x + item.rect.x;
+                data.mat[5] = mouse.y + item.rect.y;
+                store.addItem(item.type, data);
+                break;
+
+              case "serial":
+                data.content = getCurrentNdx(store);
+                const radius = (getSerialFontW(data) * Math.sqrt(2)) / 2;
+                data.radiusX = radius;
+                data.radiusY = radius;
+              case "image":
+              case "icon":
+              case "circle":
+                data.mat[4] = mouse.x + item.rect.x + item.rect.width / 2;
+                data.mat[5] = mouse.y + item.rect.y + item.rect.height / 2;
+                store.addItem(item.type, data);
+                break;
+
+              case "arrow":
+              case "triangle":
+              case "rectangle":
+              case "polygon":
+                data.points.forEach((p: Pos) => {
+                  p.x = p.x - oldCenter.x + mouse.x;
+                  p.y = p.y - oldCenter.y + mouse.y;
+                });
+                store.addItem(item.type, data);
+                break;
+
+              case 'table':
+                
+            }
+          }
+        };
+
+        return history.onceTrack(copy);
+      },
+      type: "string",
+    },
   });
 };
 
@@ -98,7 +177,7 @@ export const useShortcutKey = () => {
   // 自动退出添加模式
   const { quitDrawShape, enterDrawShape } = useInteractiveDrawShapeAPI();
   const interactiveProps = useInteractiveProps();
-  
+
   const store = useStore();
   useListener(
     "contextmenu",
@@ -115,20 +194,20 @@ export const useShortcutKey = () => {
         }, 10);
       }
     },
-    document.documentElement
+    document.documentElement,
   );
 
   const history = useHistory();
   const status = useMouseShapesStatus();
   const getChildren = useGetFormalChildren();
   const operMode = useOperMode();
-  const getShapeBelong = useGetShapeBelong()
-  const eSelection = useExcludeSelection()
+  const getShapeBelong = useGetShapeBelong();
+  const eSelection = useExcludeSelection();
   useListener(
     "keydown",
     (ev) => {
       if (ev.target !== document.body) return;
-      const key = ev.key.toLowerCase()
+      const key = ev.key.toLowerCase();
       if (key === "z" && ev.ctrlKey) {
         ev.preventDefault();
         history.hasUndo.value && history.undo();
@@ -145,15 +224,17 @@ export const useShortcutKey = () => {
 
         const isSelect = status.selects.length;
         const shapes = isSelect ? status.selects : status.actives;
-        const delItems = shapes
-          .map(getShapeBelong)
-          .filter((item) => !!item);
+        const delItems = shapes.map(getShapeBelong).filter((item) => !!item);
 
-          history.onceTrack(() => {
+        history.onceTrack(() => {
           delItems.forEach((belong) => {
-            const compDelItem = components[belong.type].delItem
+            const compDelItem = components[belong.type].delItem;
             if (compDelItem) {
-              compDelItem(store, belong.item as any, belong.isSelf ? undefined : belong.curId)
+              compDelItem(
+                store,
+                belong.item as any,
+                belong.isSelf ? undefined : belong.curId,
+              );
             } else if (belong.isSelf) {
               store.delItem(belong.type, belong.id);
             }
@@ -171,12 +252,14 @@ export const useShortcutKey = () => {
         if (status.selects.length) {
           status.selects = [];
         } else {
-          console.log('多选')
-          status.selects = getChildren().filter(shape => !eSelection.value.includes(shape.id()));
+          console.log("多选");
+          status.selects = getChildren().filter(
+            (shape) => !eSelection.value.includes(shape.id()),
+          );
         }
       }
     },
-    window
+    window,
   );
 };
 
@@ -192,7 +275,7 @@ export const useAutoService = () => {
 
   watchEffect((onCleanup) => {
     if (operMode.value.freeView) {
-      onCleanup(cursor.push('pointer'))
+      onCleanup(cursor.push("pointer"));
       return;
     }
     let style: string | null = null;
@@ -221,7 +304,7 @@ export const useAutoService = () => {
     history.setLocalId(id);
     window.addEventListener("beforeunload", unloadHandler);
     quitHooks.push(() =>
-      window.removeEventListener("beforeunload", unloadHandler)
+      window.removeEventListener("beforeunload", unloadHandler),
     );
 
     if (!history.hasLocal()) return quitHooks;

+ 54 - 12
src/core/hook/use-interactive.ts

@@ -6,6 +6,8 @@ import { eqPoint, Pos } from "../../utils/math.ts";
 import { clickListener, getOffset, listener } from "../../utils/event.ts";
 import { mergeFuns } from "../../utils/shared.ts";
 import { Mode } from "@/constant/mode.ts";
+import { useMouseShapesStatus } from "./use-mouse-status.ts";
+import { EntityShape } from "@/deconstruction.js";
 
 export type InteractivePreset<T extends ShapeType = ShapeType> = {
   key?: string;
@@ -16,6 +18,7 @@ export type InteractivePreset<T extends ShapeType = ShapeType> = {
     immediate?: boolean;
     single?: boolean;
     data?: any;
+    preSelectIds?: string[];
   };
 };
 export const useInteractiveProps = installGlobalVar(() => {
@@ -37,7 +40,8 @@ const useInteractiveExpose = <T extends object>(
   singleDone: Ref<boolean>,
   isRunning: Ref<boolean>,
   quit: () => void,
-  autoConsumed?: boolean
+  autoConsumed?: boolean,
+  attachInfos?: WeakMap<T, any>,
 ) => {
   const consumedMessages = reactive(new WeakSet<T>()) as WeakSet<T>;
   const stage = useStage();
@@ -59,8 +63,8 @@ const useInteractiveExpose = <T extends object>(
                   props.callback && props.callback();
                 }
               },
-              { flush: "post" }
-            )
+              { flush: "post" },
+            ),
           );
         }
 
@@ -82,10 +86,11 @@ const useInteractiveExpose = <T extends object>(
         messages.value = [];
       }
     },
-    { immediate: true }
+    { immediate: true },
   );
 
   return {
+    attachInfos,
     isRunning,
     get preset() {
       return interactiveProps.value?.preset;
@@ -200,7 +205,7 @@ export const useInteractiveAreas = ({
           tempArea = [point] as unknown as Area;
         }
       }),
-      listener(document.documentElement, "pointerup", upHandler)
+      listener(document.documentElement, "pointerup", upHandler),
     );
   };
 
@@ -210,7 +215,7 @@ export const useInteractiveAreas = ({
     singleDone,
     isRuning,
     quit,
-    autoConsumed
+    autoConsumed,
   );
 };
 
@@ -223,10 +228,14 @@ export const useInteractiveDots = ({
 }: UseInteractiveProps) => {
   if (autoConsumed === void 0) autoConsumed = false;
 
+  const interactiveProps = useInteractiveProps();
+  const attachInfos = new WeakMap<Pos, any>();
   const mode = useMode();
   const can = useCan();
   const singleDone = ref(true);
   const messages = ref<Pos[]>([]);
+  const stage = useStage();
+  const shapesStatus = useMouseShapesStatus();
 
   const init = (dom: HTMLDivElement) => {
     if (!can.dragMode) return () => {};
@@ -255,39 +264,72 @@ export const useInteractiveDots = ({
     const move = (ev: MouseEvent) => {
       posMove(getOffset(ev));
     };
+    const preSelectIds = interactiveProps.value?.operate?.preSelectIds;
 
     let prevPoint: Pos = { ...empty };
+    const $stage = stage.value!.getNode();
+    if (preSelectIds) {
+      shapesStatus.actives = preSelectIds
+        .map((id) => $stage.findOne(`#${id}`))
+        .filter(Boolean) as EntityShape[];
+    }
+
     return mergeFuns(
       () => {
         mode.del(Mode.draging);
+        shapesStatus.actives = []
       },
       watch(singleDone, () => {
         if (singleDone.value) {
           prevPoint = pointer.value;
-          const prevBeforePoint = beforePointer.value
+          const prevBeforePoint = beforePointer.value;
           pointer.value = { ...empty };
-          beforePointer.value = {...empty}
+          beforePointer.value = { ...empty };
           singleDone.value = true;
           moveIng = false;
           pushed = false;
           nextTick(() => posMove(prevBeforePoint));
         }
       }),
-      clickListener(dom, (_) => {
+      clickListener(dom, (_, ev) => {
         if (!moveIng || !can.dragMode || eqPoint(prevPoint, pointer.value))
           return;
+        if (preSelectIds?.length && $stage) {
+          const joinIds: string[] = [];
+          for (const id of preSelectIds) {
+            const $shape = $stage.findOne(`#${id}`);
+            if (!$shape) continue;
+
+            const rect = $shape.getClientRect();
+            let x = ev.offsetX,
+              y = ev.offsetY;
+            if (
+              x > rect.x &&
+              x < rect.x + rect.width &&
+              y > rect.y &&
+              y < rect.y + rect.height
+            ) {
+              joinIds.push(id);
+            }
+          }
+          if (joinIds.length) {
+            attachInfos.set(pointer.value, joinIds);
+            return;
+          }
+        }
         singleDone.value = true;
-        // nextTick(() => move(ev));
       }),
-      listener(dom, "pointermove", move)
+      listener(dom, "pointermove", move),
     );
   };
+
   return useInteractiveExpose(
     messages,
     init,
     singleDone,
     isRuning,
     quit,
-    autoConsumed
+    autoConsumed,
+    attachInfos,
   );
 };

+ 82 - 2
src/core/hook/use-paste.ts

@@ -2,6 +2,11 @@ import { listener } from "@/utils/event";
 import { installGlobalVar, stackVar, useStage } from "./use-global-vars";
 import { Pos } from "@/utils/math";
 import { isEditableElement } from "@/utils/dom";
+import { useMouseShapesStatus } from "./use-mouse-status";
+import { useStore } from "../store";
+import { useViewerInvertTransform } from "./use-viewer";
+import { DrawItem, ShapeType } from "../components";
+import { IRect } from "konva/lib/types";
 
 type PasteHandlers = Record<
   string,
@@ -14,6 +19,79 @@ type PasteHandlers = Record<
 export const usePaste = installGlobalVar(() => {
   const stage = useStage();
   const handlers = stackVar<PasteHandlers>({});
+  const status = useMouseShapesStatus();
+  const store = useStore();
+  const invMat = useViewerInvertTransform();
+
+  const copyHandler = (ev: ClipboardEvent) => {
+    if (isEditableElement(ev.target as HTMLElement) || !ev.clipboardData) {
+      return;
+    }
+
+    const total = {
+      x: Number.MAX_VALUE,
+      y: Number.MAX_VALUE,
+      rx: -Number.MAX_VALUE,
+      ry: -Number.MAX_VALUE,
+    };
+    const items: { rect: IRect; type: ShapeType; data: any }[] = [];
+    for (const shape of status.selects) {
+      const id = shape.id();
+      const item = store.getItemById(id);
+      const type = store.getType(id);
+      if (!item || !type) {
+        continue;
+      }
+
+      const iRect = shape.getClientRect();
+      iRect.x < total.x && (total.x = iRect.x);
+      iRect.y < total.y && (total.y = iRect.y);
+      if (iRect.x + iRect.width > total.rx) {
+        total.rx = iRect.x + iRect.width;
+      }
+      if (iRect.y + iRect.height > total.ry) {
+        total.ry = iRect.y + iRect.height;
+      }
+
+      const pixelStart = invMat.value.point({ x: iRect.x, y: iRect.y });
+      const pixelEnd = invMat.value.point({
+        x: iRect.x + iRect.width,
+        y: iRect.y + iRect.height,
+      });
+      const rect = {
+        ...pixelStart,
+        width: pixelEnd.x - pixelStart.x,
+        height: pixelEnd.y - pixelStart.y,
+      };
+
+      items.push({
+        rect,
+        type,
+        data: { ...item, id: undefined, createTime: undefined },
+      });
+    }
+
+    if (!items.length) {
+      return;
+    }
+
+    const center = invMat.value.point({
+      x: (total.rx + total.x) / 2,
+      y: (total.ry + total.y) / 2,
+    });
+
+    items.forEach((item) => {
+      item.rect.x -= center.x;
+      item.rect.y -= center.y;
+    });
+
+    const jsonData = JSON.stringify({ items, center });
+    ev.clipboardData.setData("draw-shape/json", jsonData);
+    ev.preventDefault();
+  };
+
+  const stopCopy = listener(window, "copy", copyHandler);
+
   const pasteHandler = (ev: ClipboardEvent) => {
     if (isEditableElement(ev.target as HTMLElement)) {
       return;
@@ -25,6 +103,7 @@ export const usePaste = installGlobalVar(() => {
     ev.preventDefault();
     for (const item of clipboardData.items) {
       const handMetas = Object.keys(handlers.value);
+      console.log(item);
       for (const handMeta of handMetas) {
         if (item.type.includes(handMeta)) {
           const handleItem = handlers.value[handMeta];
@@ -41,11 +120,12 @@ export const usePaste = installGlobalVar(() => {
     }
   };
 
-  const stop = listener(window, "paste", pasteHandler);
+  const stopPaste = listener(window, "paste", pasteHandler);
   return {
     var: handlers,
     onDestroy: () => {
-      stop();
+      stopCopy();
+      stopPaste();
     },
   };
 });

+ 6 - 2
src/core/hook/use-selection.ts

@@ -284,7 +284,7 @@ export const useGetShapeSelectionManage = installGlobalVar(() => {
   };
 });
 
-export const useSelectionRevise = () => {
+export const useSelectionRevise = installGlobalVar(() => {
   const getShapeSelectionManage = useGetShapeSelectionManage();
   const mParts = useMountParts();
   const status = useMouseShapesStatus();
@@ -409,4 +409,8 @@ export const useSelectionRevise = () => {
     };
     onCleanup(mParts.add({ comp: markRaw(GroupComp), props }));
   });
-};
+
+  return {
+    groupConfig
+  }
+});

+ 2 - 2
src/core/html-mount/propertys/mount-describes.vue

@@ -118,7 +118,7 @@ const changeHandler = () => {
 .mount-layout {
   pointer-events: all;
   right: 0;
-  top: 0;
+  top: var(--draw-mount-layout-top);
   bottom: 0;
   position: absolute;
   border-left: 1px solid #e6e6e6;
@@ -129,7 +129,7 @@ const changeHandler = () => {
   width: 280px;
   font-size: 14px;
   box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
-  overflow: hidden;
+  overflow-y: auto;
 
   .mount-controller {
     border-bottom: 1px solid #f0f0f0;

+ 2 - 0
src/core/renderer/group.vue

@@ -23,5 +23,7 @@ const store = useStore();
 const type = props.type as "arrow";
 const ShapeComponent = components[type].Component;
 const items = computed(() => store.getTypeItems(type));
+
+
 const { itemHasRegistor, renderer } = useStoreRenderProcessors();
 </script>

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

@@ -173,6 +173,7 @@ defineExpose(expose);
 
 <style scoped lang="scss">
 .draw-layout {
+  --draw-mount-layout-top: 0px;
   width: 100%;
   height: 100%;
   overflow: hidden;

+ 4 - 0
src/example/components/header/index.vue

@@ -73,6 +73,10 @@ defineEmits<{ (e: "back"): void }>();
   transition: margin-top 0.3s ease;
   height: var(--headerSize);
   flex: 0 0 auto;
+
+  .nav {
+    height: 100%;
+  }
 }
 
 .draw-operate {

+ 44 - 20
src/example/components/show-vr.vue

@@ -1,31 +1,42 @@
 <template>
   <div class="vr" :style="{ width: width + 'px', height: height + 'px' }" ref="vrRef">
-    <div class="header" ref="headerRef">
+    <div class="header">
       <span>VR全景</span>
       <Icon class="close" name="close" @click="$emit('close')" />
     </div>
     <div class="content" :style="{ pointerEvents: downPos ? 'none' : 'all' }">
       <iframe :src="tempStrFill(viewURLS[scene.type], scene)" />
-      <Icon class="full" :name="isFull ? 'zoom_s' : 'zoom_b'" @click="fullHandler" />
+      <span ref="headerRef">
+        <Icon class="resize" :name="isFull ? 'zoom_s' : 'zoom_b'" />
+      </span>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
 import { dragListener, listener } from "@/utils/event";
-import { onUnmounted, ref, watch, watchEffect } from "vue";
+import { computed, onUnmounted, ref, watch, watchEffect } from "vue";
 import { Pos } from "@/utils/math";
 import { mergeFuns, tempStrFill } from "@/utils/shared";
 import { Scene } from "../platform/platform-resource";
 
-defineProps<{ scene: Scene }>();
-defineEmits<{ (e: "close"): void }>();
+const props = defineProps<{ scene: Scene; height: number }>();
+const emit = defineEmits<{ (e: "close"): void; (e: "update:height", v: number): void }>();
 
 const viewURLS = window.platform.viewURLS;
 const headerRef = ref<HTMLDivElement>();
 const vrRef = ref<HTMLDivElement>();
-const width = ref(320);
-const height = ref(220);
+const width = ref(280);
+const height = computed({
+  get: () => props.height,
+  set: (val) => emit("update:height", val),
+});
+
+watch(
+  () => props.height,
+  (val) => (height.value = val || 220),
+  { immediate: true }
+);
 
 let isFull = ref(false);
 const fullHandler = () => {
@@ -42,14 +53,21 @@ const fullHandler = () => {
 let unMoveHandler: () => void;
 const move = { x: 0, y: 0 };
 const downPos = ref<Pos>();
+let downSize = { x: width.value, y: height.value };
 const initMoveHandler = () => {
   unMoveHandler && unMoveHandler();
   const syncPosition = () => {
-    // move.x = Math.max(Math.min(move.x, 0), width.value - bound.width);
-    move.y = Math.min(Math.max(move.y, 0), bound.height - height.value);
-    move.x = Math.min(Math.max(move.x, 0), bound.width - width.value);
-    vrRef.value!.style.right = move.x + "px";
-    vrRef.value!.style.top = move.y + "px";
+    // 自由移动
+    // move.y = Math.min(Math.max(move.y, 0), bound.height - height.value);
+    // move.x = Math.min(Math.max(move.x, 0), bound.width - width.value);
+
+    const w = downSize.x + move.x;
+    const h = downSize.y + move.y;
+    width.value = Math.min(Math.max(w, 280), bound.width / 2);
+    height.value = Math.min(Math.max(h, 200), (bound.height / 3) * 2);
+
+    vrRef.value!.style.right = 0 + "px";
+    vrRef.value!.style.top = 0 + "px";
   };
 
   const parent = vrRef.value!.parentElement!;
@@ -64,10 +82,14 @@ const initMoveHandler = () => {
   unMoveHandler = dragListener(headerRef.value!, {
     down() {
       downPos.value = { ...move };
+      downSize = { x: width.value, y: height.value };
     },
     move(info) {
-      move.x = downPos.value!.x + (info.start.x - info.end.x);
-      move.y = downPos.value!.y + (info.end.y - info.start.y);
+      // move.x = downPos.value!.x + (info.start.x - info.end.x);
+      // move.y = downPos.value!.y + (info.end.y - info.start.y);
+
+      move.x = info.start.x - info.end.x;
+      move.y = info.end.y - info.start.y;
       syncPosition();
     },
     up() {
@@ -82,7 +104,7 @@ watchEffect(() => {
     setTimeout(initMoveHandler, 100);
   }
 });
-watch([width, height], initMoveHandler);
+
 onUnmounted(
   mergeFuns(
     listener(window, "resize", initMoveHandler),
@@ -94,6 +116,7 @@ defineExpose({ refresh: initMoveHandler });
 
 <style lang="scss" scoped>
 .vr {
+  margin-top: 10px;
   background: #fff;
   position: absolute;
   z-index: 1;
@@ -107,7 +130,6 @@ defineExpose({ refresh: initMoveHandler });
     flex: 0 0 auto;
 
     display: flex;
-    cursor: move;
     align-items: center;
     justify-content: space-between;
     padding: 0 16px;
@@ -124,15 +146,17 @@ defineExpose({ refresh: initMoveHandler });
   .content {
     flex: 1;
     position: relative;
-    .full {
-      right: 5px;
-      bottom: 5px;
+
+    .resize {
+      left: 0;
+      bottom: 0;
       font-size: 22px;
       color: #fff;
       position: absolute;
-      cursor: pointer;
+      cursor: move;
     }
   }
+
   iframe {
     margin: 0;
     padding: 0;

+ 1 - 0
src/example/components/slide/menu.ts

@@ -12,6 +12,7 @@ export type MenuItem = {
   payload?: any;
   single?: boolean
   handler?: (draw: Draw) => void;
+  getPreSelectIds?: (draw: Draw) => string[]
 };
 
 export const getItem = (

+ 7 - 1
src/example/components/slide/slide.vue

@@ -128,7 +128,13 @@ const selectHandler = async (val: string, immed = false) => {
       nextTick(() => (active.value = undefined));
     }
   } else if (menu.payload) {
-    props.draw.enterDrawShape(menu.payload.type, menu.payload.preset, menu.single);
+    props.draw.enterDrawShape(
+      menu.payload.type,
+      menu.payload.preset,
+      menu.single,
+      undefined,
+      menu.getPreSelectIds && menu.getPreSelectIds(props.draw)
+    );
   } else if (menu.mount) {
     await nextTick();
     active.value = undefined;

+ 1 - 0
src/example/constant.ts

@@ -290,6 +290,7 @@ for (const icon of trIcons) {
 export const tableCoverKey = "__tableCoverKey";
 export const tableCoverScaleKey = "__tableCoverScaleKey";
 export const tableTableKey = "__tableTableKey";
+export const tableSerialTableKey = "__tableTableSerialKey";
 export const tableTitleKey = "__tableTitleKey";
 export const tableCompassKey = "__tableCompassKey";
 export const mapImageKey = "__mapKey";

+ 3 - 2
src/example/fuse/enter-case.ts

@@ -27,8 +27,9 @@ window.platform.login = (isBack = true) => {
           // userName: "super-admin",
 
           password: encodePwd('Aa123456'),
-          username: "liliy",
-          userName: "liliy",
+          username: "super-admin",
+          userName: "super-admin",
+
         })
         .then((res) => {
           params.value.token = res.token;

+ 2 - 3
src/example/fuse/enter.ts

@@ -18,10 +18,9 @@ window.platform.login = (isBack = true) => {
         // password: "Di8r5tFpExMjM0NTY=F39Vd0znQWfBY7W9iG",
         // username: "W测试2",
         // userName: "W测试2",
-
         password: encodePwd("Aa123456"),
-        username: "liliy",
-        userName: "liliy",
+        username: "super-admin",
+        userName: "super-admin",
       })
       .then((res) => {
         params.value.token = res.token;

+ 18 - 0
src/example/fuse/global.scss

@@ -48,4 +48,22 @@ body {
 	flex-direction: column;
 	align-items: center;
 	justify-content: center;
+}
+
+.nav-tabs.el-tabs {
+  height: 100%;
+	margin-left: 10px;
+
+	--el-tabs-header-height: var(--headerSize);
+	--el-font-size-base: 16px;
+	.el-tabs__nav-wrap::after {
+		display: none;
+	}
+	margin-top: -10px;
+	.el-tabs__header {
+		margin: 0;
+	}
+	.el-tabs__item {
+		padding: 0 16px;
+	}
 }

+ 10 - 8
src/example/fuse/router.ts

@@ -4,17 +4,19 @@ import Overview from "./views/overview/index.vue";
 import Tabulation from "./views/tabulation/index.vue";
 
 export const history = createWebHashHistory();
-const routes = [{ path: "/overview", name: "overview", component: Overview }];
-
+export const routes = [
+  { path: "/overview", name: "overview", label: "绘图", component: Overview },
+];
 // if (!window.platform.sceneDraw) {
-  routes.push({ path: "/tabulation", name: "tabulation", component: Tabulation })
+routes.push({
+  path: "/tabulation",
+  name: "tabulation",
+  label: "制表",
+  component: Tabulation,
+});
 // }
 
-
 export const router = createRouter({
   history,
-  routes: [
-   ...routes,
-    { path: "/:pathMatch(.*)*", redirect: "/overview" },
-  ],
+  routes: [...routes, { path: "/:pathMatch(.*)*", redirect: "/overview" }],
 });

+ 2 - 0
src/example/fuse/views/defStyle.ts

@@ -138,6 +138,7 @@ export const overviewCustomStyle = (_draw: Draw) => {
   const backs = [
     () => autoGenTable.value = true,
     setDefStyle(lineDefStyle, { strokeWidth: getOverviewRealPixel(120) }),
+    setDefStyle(serialFixedStrokeOptions, realFixedStrokeOptions),
     setDefStyle(triangleFixedStrokeOptions, realFixedStrokeOptions),
     setDefStyle(circleFixedStrokeOptions, realFixedStrokeOptions),
     setDefStyle(arrowFixedStrokeOptions, realFixedStrokeOptions),
@@ -147,6 +148,7 @@ export const overviewCustomStyle = (_draw: Draw) => {
     setDefStyle(lineIconFixedStrokeOptions, realFixedStrokeOptions),
     setDefStyle(lineChunkFixedStrokeOptions, realFixedStrokeOptions),
 
+    setDefStyle(serialDefStyle, { strokeWidth: defFixelStroke }),
     setDefStyle(lineChunkDefStyle, { strokeWidth: defFixelStroke }),
     setDefStyle(lineIconDefStyle, { strokeWidth: defFixelStroke }),
     setDefStyle(iconDefStyle, { strokeWidth: defFixelStroke }),

+ 24 - 16
src/example/fuse/views/overview/header.vue

@@ -4,36 +4,36 @@
       <el-button type="primary" @click="saveHandler" :disabled="draw.drawing">
         保存
       </el-button>
-      <el-button
-        @click="gotoTabulation"
-        color="#E6E6E6"
-        :disabled="draw.drawing"
-        v-if="showTabulation"
+    </template>
+    <template #nav>
+      <el-tabs
+        :modelValue="(router.currentRoute.value.name as any)"
+        class="nav-tabs"
+        @tab-click="goTab"
       >
-        图纸
-      </el-button>
+        <el-tab-pane :label="route.label" :name="route.name" v-for="route in routes" />
+      </el-tabs>
     </template>
   </Header>
 </template>
 
 <script lang="ts" setup>
 import Header from "../../../components/header/index.vue";
-import { ElButton, ElMessage } from "element-plus";
+import { ElButton, ElMessage, ElTabPane, ElTabs, TabsPaneContext } from "element-plus";
 import { useDraw } from "../../../components/container/use-draw.ts";
 import { selectScene } from "../../../dialog/vr/index.ts";
 import { Scene } from "../../../platform/platform-resource.ts";
 import { getHeaderActions, getImage } from "../../../components/header/actions.ts";
 import { tabulationData, refreshTabulationData, overviewData } from "../../store.ts";
-import { nextTick, onUnmounted } from "vue";
+import { nextTick, onUnmounted, watchEffect } from "vue";
 import { Mode } from "@/constant/mode.ts";
 import { repTabulationStore } from "../tabulation/gen-tab.ts";
-import { router } from "../../router.ts";
+import { router, routes } from "../../router.ts";
 import { overviewId, params, tabulationId } from "@/example/env.ts";
 import { listener } from "@/utils/event.ts";
 import { asyncTimeout, mergeFuns, repeatedlyOnly } from "@/utils/shared.ts";
 import saveAs from "@/utils/file-serve.ts";
 import { setViewToTableCover } from "./actions.ts";
-import { genLoading } from "@/example/loadding.ts";
 
 const props = defineProps<{ title: string }>();
 const draw = useDraw();
@@ -42,8 +42,13 @@ const emit = defineEmits<{
   (e: "saveAfter"): void;
 }>();
 
-// const showTabulation = !params.value.sceneDraw;
-const showTabulation = true;
+const goTab = async (tab: TabsPaneContext) => {
+  if (tab.props.name === "tabulation") {
+    await saveHandler();
+    gotoTabulation();
+  }
+};
+
 const baseActions = getHeaderActions(draw);
 const actions = [
   [baseActions.undo, baseActions.redo],
@@ -72,9 +77,13 @@ const actions = [
           const format = item.text.toLowerCase();
           if (format !== "dxf") {
             const blob = await draw.enterTemp(async () => {
-              const back = draw.config.back;
               const [_, recover] = await setViewToTableCover(draw);
-              if (format === "jpg") draw.config.back = back;
+              if (format === "jpg") {
+                draw.config.back = {
+                  color: "#ffffff",
+                  opacity: 1,
+                };
+              }
               await nextTick();
               const blob = await getImage(draw, `image/${format}`);
               recover();
@@ -218,7 +227,6 @@ const saveHandler = repeatedlyOnly(async () => {
 
   overviewId.value = await window.platform.saveOverviewData(overviewId.value, body);
   tabulationId.value = await window.platform.getTabulationId(overviewId.value);
-  console.log("保存完毕");
   emit("saveAfter");
 });
 

+ 14 - 1
src/example/fuse/views/overview/index.vue

@@ -3,6 +3,8 @@
     :upload-resourse="uploadResourse"
     v-model:full="full"
     :ref="(d: any) => (draw = d?.draw)"
+    :style="{ ['--vrHeight']: vrScene ? vrHeight + 20 + 'px' : '0px' }"
+    class="overview-height"
   >
     <template #header>
       <Header
@@ -16,7 +18,13 @@
       <Slide />
     </template>
     <template #cover>
-      <ShowVR :scene="vrScene" v-if="vrScene" ref="vr" @close="vrScene = undefined" />
+      <ShowVR
+        :scene="vrScene"
+        v-if="vrScene"
+        ref="vr"
+        v-model:height="vrHeight"
+        @close="vrScene = undefined"
+      />
       <div class="three-view" v-if="attach && showThree">
         <component :is="attach" :draw="(draw as any)" />
       </div>
@@ -49,6 +57,7 @@ const full = ref(false);
 const draw = ref<Draw>();
 const vrScene = ref<Scene>();
 const header = ref<any>();
+const vrHeight = ref(220);
 
 const attach = shallowRef<any>();
 if (import.meta.env.DEV) {
@@ -137,4 +146,8 @@ watchEffect(() => {
   bottom: 0;
   width: 50%;
 }
+.overview-height .mount-layout {
+  --draw-mount-layout-top: var(--vrHeight);
+  
+}
 </style>

+ 13 - 3
src/example/fuse/views/overview/slide.vue

@@ -12,7 +12,7 @@ import {
   imp,
   serial,
 } from "../../../components/slide/actions.ts";
-import { useDraw } from "../../../components/container/use-draw.ts";
+import { Draw, useDraw } from "../../../components/container/use-draw.ts";
 import { h, reactive } from "vue";
 import SlideIcons from "../../../components/slide/slide-icons.vue";
 import { iconGroups } from "../../../constant";
@@ -23,12 +23,22 @@ const legend = {
   value: uuid(),
   mount: (props: any) => h(SlideIcons, { ...props, groups: iconGroups }),
 };
+const oSerial = {
+  ...serial,
+  getPreSelectIds(draw: Draw) {
+    console.log(draw.store.getTypeItems("icon"));
+    return draw.store
+      .getTypeItems("image")
+      .filter((item) => item.key === "trace")
+      .map((item) => item.id);
+  },
+};
 const mark = {
-  icon: "line_d",
+  icon: "info",
   name: "注释",
   value: uuid(),
   defSelect: true,
-  children: [text, serial],
+  children: [text, oSerial],
 };
 
 const draw = useDraw();

+ 25 - 19
src/example/fuse/views/tabulation/gen-tab.ts

@@ -45,7 +45,7 @@ export const getPixelReal = (pixel: number, paperKey: PaperKey) => {
 export const transformPaper = (
   real: number,
   paperKey: PaperKey,
-  toPaperKey: PaperKey
+  toPaperKey: PaperKey,
 ) => {
   const scale = paperConfigs[paperKey].scale / paperConfigs[toPaperKey].scale;
   return real * scale;
@@ -58,16 +58,20 @@ export const getCoverPaperScale = (cover: ImageData, paperKey: PaperKey) => {
   return realPixelScale * pixelScale;
 };
 
-export const getCoverWidthRaw = (cover: ImageData, paperKey: PaperKey, pp: number) => {
-  const pixelScale = pp / paperConfigs[paperKey].scale
-  const widthRaw = (pixelScale / cover.proportion!.scale ) * cover.width
-  return widthRaw
-}
+export const getCoverWidthRaw = (
+  cover: ImageData,
+  paperKey: PaperKey,
+  pp: number,
+) => {
+  const pixelScale = pp / paperConfigs[paperKey].scale;
+  const widthRaw = (pixelScale / cover.proportion!.scale) * cover.width;
+  return widthRaw;
+};
 
 export const setCoverPaperScale = (
   cover: ImageData,
   paperKey: PaperKey,
-  scale: number
+  scale: number,
 ) => {
   const realPixelScale = paperConfigs[paperKey].scale;
   cover.width =
@@ -78,12 +82,12 @@ export const setCoverPaperScale = (
 export const genTabulationData = async (
   paperKey: PaperKey,
   compass?: number,
-  cover?: TabCover | null
+  cover?: TabCover | null,
 ) => {
   const temp: Record<string, string> = await window.platform.getTableTemp();
   const { margin, size } = getPaperConfig(
     paperConfigs[paperKey].size,
-    paperConfigs[paperKey].scale
+    paperConfigs[paperKey].scale,
   );
 
   const getTable = async () => {
@@ -101,7 +105,7 @@ export const genTabulationData = async (
       fontColor: "#000000",
     };
     const valueColl = { ...nameColl, width: w2 };
-    const tableTemp = temp.table
+    const tableTemp = temp.table;
     const rows = Object.entries(tableTemp);
     const data = {
       ...getBaseItem(),
@@ -125,12 +129,13 @@ export const genTabulationData = async (
         bottom: getRealPixel(15, paperKey) + margin[2],
       },
       data,
-      size
+      size,
     );
 
     return matResponse({ data, mat: new Transform().translate(pos.x, pos.y) });
   };
 
+
   const getCover = async () => {
     if (!cover) return;
     const image = await getImage(window.platform.getResource(cover.url));
@@ -171,7 +176,7 @@ export const genTabulationData = async (
         bottom: -coverData.height / 2 + margin[2] + getRealPixel(15, paperKey),
       },
       coverData,
-      size
+      size,
     );
     coverData.mat[4] = pos.x;
     coverData.mat[5] = pos.y;
@@ -228,7 +233,7 @@ export const genTabulationData = async (
     const style = await getIconStyle(
       "./icons/compass.svg",
       getRealPixel(37.3366 / 2.3, paperKey),
-      getRealPixel(60 / 2.3, paperKey)
+      getRealPixel(60 / 2.3, paperKey),
     );
 
     const data: IconData = {
@@ -250,7 +255,7 @@ export const genTabulationData = async (
         top: getRealPixel(42, paperKey) + margin[2],
       },
       data,
-      size
+      size,
     );
     data.mat[4] = pos.x;
     data.mat[5] = pos.y;
@@ -305,7 +310,7 @@ export const genTabulationData = async (
     serial: [],
   };
   if (temp.title) {
-    data.text = [getTitle()]
+    data.text = [getTitle()];
   }
   const image = await getCover();
   if (image) {
@@ -328,11 +333,11 @@ export const repTabulationStore = async (
   paperKey: PaperKey,
   compass?: number,
   cover?: TabCover,
-  store?: StoreData
+  store?: StoreData,
 ) => {
   const repData = await genTabulationData(paperKey, compass, cover);
   const layer = store?.layers && store?.layers[defaultLayer];
-  
+
   if (!layer || !store.config) {
     return {
       ...(store || {}),
@@ -364,7 +369,7 @@ export const repTabulationStore = async (
     layer.image = images;
 
     const scaleData = repData.text!.find(
-      (item) => item.key === tableCoverScaleKey
+      (item) => item.key === tableCoverScaleKey,
     )!;
     const texts = layer.text || [];
     const textNdx = texts.findIndex((item) => item.key === scaleData.key);
@@ -392,7 +397,7 @@ export const repTabulationStore = async (
     if (layer.serial) {
       const oldSerials = [...layer.serial];
       const notAutoSerials = oldSerials.filter(
-        (item) => !item.key?.startsWith("join-")
+        (item) => !item.key?.startsWith("join-"),
       );
       const newSerials = [...repData.serial, ...notAutoSerials];
 
@@ -424,6 +429,7 @@ export const repTabulationStore = async (
       layer.serial = repData.serial;
     }
   }
+
   // if (import.meta.env.DEV) {
   //   layer.table = repData.table
   //   layer.text = repData.text

+ 32 - 4
src/example/fuse/views/tabulation/header.vue

@@ -11,11 +11,20 @@
         保存
       </el-button>
     </template>
+    <template #nav>
+      <el-tabs
+        :modelValue="(router.currentRoute.value.name as any)"
+        class="nav-tabs"
+        @tab-click="goTab"
+      >
+        <el-tab-pane :label="route.label" :name="route.name" v-for="route in routes" />
+      </el-tabs>
+    </template>
   </Header>
 </template>
 
 <script lang="ts" setup>
-import { ElButton, ElMessage } from "element-plus";
+import { ElButton, ElMessage, ElTabPane, ElTabs, TabsPaneContext } from "element-plus";
 import { useDraw } from "../../../components/container/use-draw.ts";
 import Header from "../../../components/header/index.vue";
 import { getHeaderActions } from "../../../components/header/actions.ts";
@@ -27,16 +36,30 @@ import { getImage as getResourceImage } from "@/utils/resource.ts";
 import { onUnmounted } from "vue";
 import { tabulationData } from "../../store.ts";
 import { Mode } from "@/constant/mode.ts";
-import { overviewId, tabulationId } from "@/example/env.ts";
+import { overviewId, params, tabulationId } from "@/example/env.ts";
 import { listener } from "@/utils/event.ts";
-import { router } from "../../router.ts";
+import { router, routes } from "../../router.ts";
 import { initViewport, paperConfigs } from "@/example/components/slide/actions.ts";
 import { asyncTimeout } from "@/utils/shared.ts";
-import { Transform } from "konva/lib/Util";
 
 const props = defineProps<{ title: string }>();
 const emit = defineEmits<{ (e: "screenshot", val: boolean): void }>();
 
+const goOverview = async () => {
+  await saveHandler();
+  router.push({
+    ...router.currentRoute.value,
+    name: "overview",
+    query: params.value,
+  } as any);
+};
+
+const goTab = (tab: TabsPaneContext) => {
+  if (tab.props.name === "overview") {
+    goOverview();
+  }
+};
+
 const draw = useDraw();
 const getCoverImage = (format: string) => {
   emit("screenshot", true);
@@ -147,4 +170,9 @@ const saveHandler = async () => {
   });
   isUpload = false;
 };
+
+defineExpose({
+  goOverview,
+  actions,
+});
 </script>

+ 120 - 0
src/example/fuse/views/tabulation/index.vue

@@ -75,6 +75,11 @@ import {
   PaperKey,
 } from "@/example/components/slide/actions";
 import { getFixPosition } from "@/utils/bound";
+import { listener } from "@/utils/event";
+import { router } from "../../router";
+import { params } from "@/example/env";
+import { tableSerialTableKey } from "@/example/constant";
+import { addTable, getCurrentNdxRaw, syncTable } from "@/core/components/serial";
 
 const uploadResourse = window.platform.uploadResourse;
 const full = ref(false);
@@ -148,6 +153,100 @@ const setMapHandler = async (config: { url: string; size: Size }) => {
   }
 };
 
+// 序号处理
+{
+  const serialTable = computed(() => {
+    const tables = draw.value?.store.getTypeItems("table");
+    console.log("update");
+    return tables && tables.find((table) => table.key === tableSerialTableKey);
+  });
+
+  watchEffect((onCleanup) => {
+    if (!draw.value || !serialTable.value) return;
+    const un = draw.value.menusFilter.setShapeMenusFilter(
+      serialTable.value.id,
+      (menu) => [{ label: "更新数据", handler: updateSerialTable }, ...menu]
+    );
+    onCleanup(un);
+  });
+
+  const updateSerialTable = () => {
+    const d = draw.value;
+    const overview = overviewData.value?.store.layers.default;
+    const serials = overview.serial;
+    if (!d || !serials?.length) return;
+    const table = serialTable.value || addTable(d as any);
+    const syncSerials = serials.map((item) => {
+      let desc = "";
+      if (item.joinIds) {
+        desc = item.joinIds
+          .map((id) => overview.image?.find((image) => image.id === id)?.name)
+          .filter(Boolean)
+          .join("、");
+      }
+      return {
+        ...item,
+        desc,
+      };
+    });
+
+    // 找出没有关联的痕迹物证
+    overview.image?.forEach((image) => {
+      if (
+        image.key !== "trace" ||
+        syncSerials.some((item) => item.joinIds?.includes(image.id))
+      )
+        return;
+
+      syncSerials.push({
+        id: image.id,
+        content: getCurrentNdxRaw(syncSerials),
+        desc: image.name || "",
+      } as any);
+    });
+
+    // 合并相同序号数据
+    for (let i = 0; i < syncSerials.length; i++) {
+      const self = syncSerials[i];
+      for (let j = i + 1; j < syncSerials.length; j++) {
+        if (syncSerials[j].content === self.content) {
+          if (syncSerials[j].desc) {
+            self.desc = self.desc + "、" + syncSerials[j].desc;
+          }
+          syncSerials.splice(j--, 1);
+        }
+      }
+    }
+
+    syncTable(table, syncSerials);
+
+    if (serialTable.value) {
+      d.history.preventTrack(() => {
+        d.store.setItem("table", {
+          id: serialTable.value!.id,
+          value: serialTable.value!,
+        });
+      });
+    } else {
+      table.key = tableSerialTableKey;
+      d.history.preventTrack(() => {
+        d.store.addItem("table", table);
+      });
+    }
+  };
+
+  watch(
+    draw,
+    (draw) => {
+      if (!draw) return;
+      if (!serialTable.value) {
+        setTimeout(updateSerialTable, 500);
+      }
+    },
+    { immediate: true }
+  );
+}
+
 const needScreenshot = ref(false);
 const coverSetting = computed(() => {
   const cover = draw.value?.store.items.find((item) => item.key === tableCoverKey);
@@ -481,6 +580,27 @@ watch(compass, (compass, _, onCleanup) => {
   onCleanup(mergeFuns(quits));
 });
 
+watchEffect((onCleanup) => {
+  const d = draw.value;
+  if (!d?.stage?.container()) return;
+
+  const dom = d.stage.container();
+  onCleanup(
+    listener(dom, "dblclick", () => {
+      d.shapesStatus.actives.some((item) => {
+        const id = item.id();
+        if (d.store.getItemById(id)?.key === tableCoverKey) {
+          router.push({
+            ...router.currentRoute.value,
+            name: "overview",
+            query: params.value,
+          } as any);
+        }
+      });
+    })
+  );
+});
+
 const title = computed(() => tabulationData.value?.title || "图纸");
 watchEffect(() => {
   document.title = title.value;

+ 8 - 1
src/example/fuse/views/tabulation/overview-viewport.vue

@@ -187,14 +187,21 @@ const updateOrigin = async () => {
   // 样式设置
   {
     const setStyle = (item: any) => {
-      if (!("fixed" in item)) return;
+      const type = d.store.getType(item.id);
+      const isIcon = type === "icon" || type === "lineIcon";
+      if (!("fixed" in item) && !isIcon) return;
+
       item.fixed = false;
+      item.fix = true;
       if ("strokeWidth" in item && !("__strokeWidth" in item)) {
         item.__strokeWidth = item.strokeWidth;
       }
       if ("__strokeWidth" in item) {
         item.strokeWidth = (item.__strokeWidth * pixelPaperToDrawPixel.value!) / 2;
       }
+      if (isIcon) {
+        item.strokeWidth *= viewScale.value! / 10;
+      }
     };
     d.store.items.forEach(setStyle);
     const lineItems = d.store.getTypeItems("line")[0];

+ 28 - 19
src/example/fuse/views/tabulation/slide.vue

@@ -47,21 +47,35 @@ drawMenu.name = "标注";
 let tileGroups: TileGroup[];
 loading(window.platform.getTileGroups()).then((data: any) => (tileGroups = data));
 
+const mark = {
+  icon: "info",
+  name: "注释",
+  value: uuid(),
+  defSelect: true,
+  children: [
+    {
+      ...text,
+      payload: { ...text.payload, preset: { ...text.payload.preset, fontSize: 16 } },
+    },
+    table,
+  ],
+};
+
 const menus = reactive([
   paper,
-  {
-    value: uuid(),
-    icon: "map",
-    name: "地图",
-    handler: async () => {
-      const result = await selectMap({
-        search: window.platform.searchAddress,
-        activeGroupIndex: 0,
-        tileGroups,
-      });
-      emit("updateMapImage", { url: result.url, size: result.info.size });
-    },
-  },
+  // {
+  //   value: uuid(),
+  //   icon: "map",
+  //   name: "地图",
+  //   handler: async () => {
+  //     const result = await selectMap({
+  //       search: window.platform.searchAddress,
+  //       activeGroupIndex: 0,
+  //       tileGroups,
+  //     });
+  //     emit("updateMapImage", { url: result.url, size: result.info.size });
+  //   },
+  // },
   drawMenu,
   // {
   //   icon: "legend",
@@ -70,12 +84,7 @@ const menus = reactive([
   //   mount: (props: any) =>
   //     h(SlideIcons, { ...props, groups: [iconGroups[iconGroups.length - 1]] }),
   // },
-  {
-    ...text,
-    payload: { ...text.payload, preset: { ...text.payload.preset, fontSize: 16 } },
-  },
-  serial,
-  table,
+  mark,
 ]);
 
 const setPaperAfterHandler = async (paperKey: PaperKey, _oldPaperKey?: PaperKey) => {

+ 1 - 0
src/example/platform/platform-draw.ts

@@ -188,6 +188,7 @@ const getTaggingShapes = async (taggings: SceneResource["taggings"]) => {
       item.rotate && mat.rotate(item.rotate);
     }
 
+    console.log(item.key)
     if (item.isText) {
       texts.push({
         ...getBaseItem(),

+ 137 - 15
src/utils/math.ts

@@ -89,7 +89,7 @@ export const rotateVector = (pos: Pos, angleRad: number) =>
 export const getVectorLine = (
   dire: Pos,
   start: Pos = { x: 0, y: 0 },
-  dis: number = 1
+  dis: number = 1,
 ) => [start, vector(dire).multiplyScalar(dis).add(start)];
 
 /**
@@ -109,7 +109,7 @@ export const lineVerticalVector = (line: Pos[]) =>
 export const verticalVectorLine = (
   dire: Pos,
   start: Pos = { x: 0, y: 0 },
-  len: number = 1
+  len: number = 1,
 ) => getVectorLine(verticalVector(dire), start, len);
 
 /**
@@ -431,7 +431,7 @@ export const isPolygonCounterclockwise = (points: Pos[]) =>
 export const lineSlice = (
   line: Pos[],
   amount: number,
-  unit = lineLen(line[0], line[1]) / amount
+  unit = lineLen(line[0], line[1]) / amount,
 ) =>
   new Array(unit)
     .fill(0)
@@ -452,7 +452,6 @@ export const isPolygonLineIntersect = (polygon: Pos[], line: Pos[]) => {
   return false;
 };
 
-
 /**
  * 线段与多边形交点
  * @param polygon 多边形
@@ -460,15 +459,15 @@ export const isPolygonLineIntersect = (polygon: Pos[], line: Pos[]) => {
  * @returns
  */
 export const polygonLineIntersect = (polygon: Pos[], line: Pos[]) => {
-  const ps: Pos[] =[]
+  const ps: Pos[] = [];
   for (let i = 0; i < polygon.length; i++) {
-    const line2 = [polygon[i], polygon[(i + 1) % polygon.length]]
-    const p = lineIntersection(line2, line)
+    const line2 = [polygon[i], polygon[(i + 1) % polygon.length]];
+    const p = lineIntersection(line2, line);
     if (p && lineInner(line, p) && lineInner(line2, p)) {
-      ps.push(p)
+      ps.push(p);
     }
   }
-  return ps
+  return ps;
 };
 /**
  * 判断点是否在多边形内部
@@ -534,7 +533,7 @@ export function calculateScaleFactor(
   origin: Pos,
   scaleDirection: Pos,
   p1: Pos,
-  p2: Pos
+  p2: Pos,
 ) {
   const op1 = vector(p1).sub(origin);
   const op2 = vector(p2).sub(origin);
@@ -715,11 +714,11 @@ export const getLineEdgeJoinInfo = (
   origin: LEJLine,
   target: LEJLine,
   minAngle: number,
-  palAngle: number
+  palAngle: number,
 ) => {
   const { originNdx, targetPoints, norAngle } = getLEJLineAngle(
     origin.points,
-    target.points
+    target.points,
   );
 
   // 最小可平滑处理角度
@@ -770,7 +769,7 @@ export const getLineEdgeJoinInfo = (
   // }
   if (
     originJoinNdxs.some((i) =>
-      isPolygonPointInner(targetEdges, originEdges[i])
+      isPolygonPointInner(targetEdges, originEdges[i]),
     ) ||
     targetJoinNdxs.some((i) => isPolygonPointInner(originEdges, targetEdges[i]))
   ) {
@@ -830,7 +829,7 @@ export const getLineEdgeJoinInfo = (
       {
         rep: 3,
         points: originEdgesInv ? originInnerPoints : originOuterPoints,
-      }
+      },
     );
   } else {
     originRepInfos.push(
@@ -843,9 +842,132 @@ export const getLineEdgeJoinInfo = (
         points: originEdgesInv
           ? originInnerPoints
           : originOuterPoints.reverse(),
-      }
+      },
     );
   }
 
   return originRepInfos;
 };
+
+/**
+ * 获取圆/椭圆与线段的交点
+ * @param line 线段两点 [{x, y}, {x, y}]
+ * @param circle 圆/椭圆参数 { center: {x, y}, radiusX, radiusY }
+ */
+export function getCircleLineIntersection(
+  line: Pos[],
+  circle: { center: Pos; radiusX: number; radiusY: number },
+) {
+  const { center, radiusX, radiusY } = circle;
+  const [p1, p2] = line;
+
+  // 1. 坐标归一化(将椭圆问题转为单位圆问题)
+  const a = {
+    x: (p1.x - center.x) / radiusX,
+    y: (p1.y - center.y) / radiusY,
+  };
+  const b = {
+    x: (p2.x - center.x) / radiusX,
+    y: (p2.y - center.y) / radiusY,
+  };
+
+  // 2. 线段向量
+  const dx = b.x - a.x;
+  const dy = b.y - a.y;
+
+  // 3. 求解一元二次方程 At^2 + Bt + C = 0
+  // (a.x + dx*t)^2 + (a.y + dy*t)^2 = 1
+  const A = dx * dx + dy * dy;
+  const B = 2 * (a.x * dx + a.y * dy);
+  const C = a.x * a.x + a.y * a.y - 1;
+
+  const discriminant = B * B - 4 * A * C;
+
+  if (discriminant < 0) return null; // 无交点
+
+  const sqrtD = Math.sqrt(discriminant);
+  const t1 = (-B - sqrtD) / (2 * A);
+  const t2 = (-B + sqrtD) / (2 * A);
+
+  // 4. 筛选在 [0, 1] 范围内的有效 t (即在线段上)
+  // 如果有两个交点,通常取第一个碰到的点 (t1)
+  const validT = [t1, t2].filter((t) => t >= 0 && t <= 1);
+
+  if (validT.length === 0) return null;
+
+  // 取最小的 t (最靠近 p1 的点)
+  const t = Math.min(...validT);
+
+  // 5. 将交点映射回原始坐标空间
+  return {
+    x: p1.x + t * (p2.x - p1.x),
+    y: p1.y + t * (p2.y - p1.y),
+  };
+}
+
+/**
+ * 获取线段与矩形边框的交点
+ * @param line 线段两点 [{x, y}, {x, y}]
+ * @param rect 矩形 { x, y, width, height } (x,y通常为左上角)
+ */
+export function getLineRectIntersection(line: Pos[], rect: IRect) {
+  const [p1, p2] = line;
+  const { x, y, width, height } = rect;
+
+  // 定义矩形的四条边
+  const sides = [
+    [
+      { x, y },
+      { x: x + width, y },
+    ], // Top
+    [
+      { x: x + width, y },
+      { x: x + width, y: y + height },
+    ], // Right
+    [
+      { x: x + width, y: y + height },
+      { x, y: y + height },
+    ], // Bottom
+    [
+      { x, y: y + height },
+      { x, y },
+    ], // Left
+  ];
+
+  let closestIntersection = null;
+  let minDistance = Infinity;
+
+  for (const side of sides) {
+    const inter = getLineLineIntersection(p1, p2, side[0], side[1]);
+    if (inter) {
+      // 计算到线段起点 p1 的距离,确保取最近的交点
+      const dist = Math.hypot(inter.x - p1.x, inter.y - p1.y);
+      if (dist < minDistance) {
+        minDistance = dist;
+        closestIntersection = inter;
+      }
+    }
+  }
+
+  return closestIntersection;
+}
+
+/** 辅助函数:计算两条线段的交点 */
+function getLineLineIntersection(p0: Pos, p1: Pos, p2: Pos, p3: Pos) {
+  const s1_x = p1.x - p0.x,
+    s1_y = p1.y - p0.y;
+  const s2_x = p3.x - p2.x,
+    s2_y = p3.y - p2.y;
+
+  const s =
+    (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) /
+    (-s2_x * s1_y + s1_x * s2_y);
+  const t =
+    (s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) /
+    (-s2_x * s1_y + s1_x * s2_y);
+
+  if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
+    return { x: p0.x + t * s1_x, y: p0.y + t * s1_y };
+  }
+  return null;
+}